Головна / Статті / Практичні зауваження: від дрібної налаштуваності до точності: значне зменшення

Практичні зауваження: від дрібної налаштуваності до точності: значне зменшення

Покрокове пояснення до практичних порад: від доробки до точності: суттєве скорочення кількості контрактів, перевірок та слотів для коду для команд, які використовують цю схему.

2822 слів

У цьому посібнику описано процес створення системи від сировини до готового рішення для проекту «Від доробки до точності: суттєве зменшення галюцинацій у вашому конвеєрі RAG». Основна увага приділяється крокам, які можна виконати, чітким перевіркам та коду, який можна просто додати до репозиторію, не здогадуючись про його призначення.

1. Вступ

На етапі вступу необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не здогадуючись про прихований стан системи. Вважайте цей етап угодою між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте мовчазного часткового завершення роботи. Наводьте конкретні уривки тексту, які лягли в основу відповіді; без цитат оператори не зможуть відрізнити галюцинації від проблем із індексуванням.

You: What was the revenue from contracts with customers in 2024?
Assistant: The revenue was €179,058,000.  ← Wrong! Correct answer is €159,088,000.

2. Чому саме доробка?

На етапі деталізованої налаштування «2 чому» необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись визначити прихований стан. Записуйте час виконання та витрати на токени або запити поруч із функціональними результатами. Чітке відображення витрат заздалегідь запобігає несподіваним рахункам під час переходу з демо-середовища у спільні. Наводьте конкретні уривки, які лягли в основу відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.

Рішення: деталізована налаштування за допомогою MLX

Для деталізованої налаштування рішення з використанням етапів необхідно визначити вхідні дані, відповідальну особу за кожен етап та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити етап з відомої точки контролю, не намагаючись визначити прихований стан. Конфігурацію слід зберігати окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функціоналу мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Необхідно наводити конкретні уривки тексту, які лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.

3. Діаграма архітектури

На етапі 3 «Архітектурна діаграма» необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись визначити прихований стан. Необхідно документувати як шлях успішного виконання, так і шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.

┌─────────────────────────────────────────────────────────────────────────────┐
│                       PART 3: FINE-TUNING WORKFLOW (M1)                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   1. Dataset                2. MLX Fine-Tuning                              │
│  ┌──────────────────┐      ┌────────────────────────────────────────────┐   │
│  │ Chart Images     │─────▶│ Base Model (Qwen2-VL-2B)                   │   │
│  │ Q&A Pairs        │      │  + LoRA Adapters (mlx_vlm.lora)            │   │
│  │ (train.jsonl)    │      │  + Unified Memory Training on Apple Silicon│   │
│  └──────────────────┘      └────────────────┬───────────────────────────┘   │
│                                             │                               │
│                                             ▼                               │
│                            ┌──────────────────────────────────────────┐     │
│                            │ Fine-Tuned LoRA Adapters                 │     │
│                            │ (./fine_tuned_adapters/)                 │     │
│                            └────────────────┬─────────────────────────┘     │
│                                             │                               │
│                                             ▼                               │
│                            ┌──────────────────────────────────────────┐     │
│                            │ 3. Merge & Export to GGUF                │     │
│                            │ (mlx_vlm.fuse + convert_hf_to_gguf.py)   │     │
│                            └────────────────┬─────────────────────────┘     │
│                                             │                               │
│                                             ▼                               │
│                            ┌──────────────────────────────────────────┐     │
│                            │ 4. Deploy with Ollama                    │     │
│                            │ ollama create my-chart-model             │     │
│                            │ (text.gguf + mmproj.gguf)                │     │
│                            └────────────────┬─────────────────────────┘     │
│                                             │                               │
│                                             ▼                               │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ 5. Update rag_engine.py                                              │   │
│  │                                                                      │   │
│  │   VISION_MODEL = "my-chart-model"  # ← Change ONE line               │   │
│  │   TEXT_MODEL   = "llama3.2:3b"     # Unchanged                       │   │
│  │                                                                      │   │
│  │   ✔ Existing app.py (from Part 2) automatically uses the new model!  │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

4. Підготовка набору даних

На етапі 4 «Підготовка набору даних» необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість знову виконати крок з відомої точки контролю, не намагаючись вгадати прихований стан. Краще використовувати невеликі, перевірювані одиниці коду замість об’ємних скриптів. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану структуру обробки даних. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням. На етапі 4 «Підготовка набору даних» необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість знову виконати крок з відомої точки контролю, не намагаючись вгадати прихований стан. Записуйте час виконання та витрати на токени або запити поруч із функціональними результатами. Чітке відображення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-режиму до спільного середовища.

Model sees:    (Chart image)
Model reads:   (Question)
Model learns:  (Correct answer)

Джерела набору даних

Під час роботи з етапом джерел набору даних спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успіх та наслідки часткової невдачі. Такий перелік допомагає зберігати чесність у подальших змінах коду. Зберігайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Перед налаштуванням запитів вимірюйте рівень відтворення даних на фіксованому наборі запитань. Зміна формулювань запитів рідко допомагає покращити якість пошуку.

Поетапна підготовка даних

Під час виконання етапу підготовки даних крок за кроком спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Одночасно задокументуйте шлях успішного виконання та шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Перед налаштуванням запитів вимірюйте рівень відтворення інформації на фіксованому наборі запитань. Зміна запитів рідко допомагає вирішити проблеми з низькою ефективністю пошуку.

Step 1: Create Raw Dataset       →  chart_dataset/raw_train.jsonl
Step 2: Convert to MLX Format    →  chart_dataset/train.jsonl
Step 3: Verify Dataset           →  Check that train.jsonl exists

Крок 1: Створення необробленого набору даних

Під час виконання Кроку 1 «Створити етап» спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність у подальших змінах коду. Віддавайте перевагу невеликим, тестованим одиницям перед об’ємними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність операцій. Перед налаштуванням запитів вимірюйте рівень відтворення інформації за фіксованим набором запитань. Часта зміна формулювань запитів рідко допомагає покращити ефективність пошуку. Під час виконання Кроку 1 «Створити етап» спочатку запишіть контракт: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність у подальших змінах коду. Поруч із функціональними результатами записуйте час виконання та витрати на токени або запити. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-середовищ до спільних.

# download_chartqa.py
from datasets import load_dataset
import json
import os

os.makedirs("chart_dataset", exist_ok=True)

# Download ChartQA dataset
dataset = load_dataset("ahmed-masry/ChartQA", split="train")

# Convert to flat format with standard keys
converted = []
for item in dataset:
    converted.append({
        "image": item["imgname"],      # Path to chart image
        "question": item["query"],
        "answer": item["label"]
    })

# Save as raw dataset
with open("chart_dataset/raw_train.jsonl", "w") as f:
    for entry in converted:
        f.write(json.dumps(entry) + "\n")

print(f"✅ Converted {len(converted)} ChartQA samples to chart_dataset/raw_train.jsonl")
README.md: 100%|█████████████████████████████████████████████| 2.12k/2.12k [00:00<00:00, 3.72MB/s]
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
data/train-00000-of-00003.parquet: downloading bytes: ████████████████████████|  213MB, 17.3MB/s
......
Generating test split: 100%|████████████████████████| 2500/2500 [00:00<00:00, 20517.87 examples/s]
✅ Converted 28299 ChartQA samples to raw_train.jsonl

Варіант B: Створення синтетичних даних

Етап створення синтетичних даних у Варіанті B працює найкраще, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію поза кодом додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь граф. Розділіть політику часткового оброблення даних від політики їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

# generate_synthetic_data.py
import json
import matplotlib.pyplot as plt
import numpy as np
import os

os.makedirs("chart_dataset/images", exist_ok=True)

dataset = []
for i in range(100):
    categories = ['Q1', 'Q2', 'Q3', 'Q4']
    values = np.random.randint(100, 500, 4)

    plt.figure()
    plt.bar(categories, values)
    plt.title(f"Quarterly Revenue {i}")
    plt.savefig(f"chart_dataset/images/chart_{i:03d}.png")
    plt.close()

    dataset.append({
        "image": f"images/chart_{i:03d}.png",
        "question": "Which quarter had the highest revenue?",
        "answer": f"Q{np.argmax(values) + 1} with ${max(values)} million"
    })

with open("chart_dataset/raw_train.jsonl", "w") as f:
    for entry in dataset:
        f.write(json.dumps(entry) + "\n")

print("✅ Generated 100 synthetic samples in chart_dataset/raw_train.jsonl")

Крок 2: Перетворення у формат MLX

Крок 2 «Перетворення на етап» найкраще функціонує, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Документуйте як успішний, так і відновлювальний сценарії роботи разом. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої доробки. Розділіть політику часткової обробки даних від політики їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

# convert_to_mlx_format.py
import json
import os

def convert_raw_to_mlx_format(data_dir="chart_dataset"):
    """
    Convert raw dataset to MLX format.

    Input:  chart_dataset/raw_train.jsonl
    Output: chart_dataset/train.jsonl (MLX-compatible)
    """
    raw_file = os.path.join(data_dir, "raw_train.jsonl")
    out_file = os.path.join(data_dir, "train.jsonl")

    if not os.path.exists(raw_file):
        print(f"❌ {raw_file} not found. Run download_chartqa.py or generate_synthetic_data.py first.")
        return None

    dataset = []
    with open(raw_file, "r") as f:
        for line in f:
            item = json.loads(line)

            # Build full image path
            img_str = item["image"]
            image_path = img_str if img_str.startswith(data_dir) else os.path.join(data_dir, img_str)

            dataset.append({
                "images": [image_path],  # MUST be a list
                "messages": [
                    {"role": "user", "content": item["question"]},
                    {"role": "assistant", "content": item["answer"]}
                ]
            })

    with open(out_file, "w") as f:
        for entry in dataset:
            f.write(json.dumps(entry) + "\n")

    print(f"✅ Converted {len(dataset)} samples to {out_file}")
    return dataset

if __name__ == "__main__":
    convert_raw_to_mlx_format()
python convert_to_mlx_format.py

Крок 3: Перевірка набору даних

Крок 3: Перевірка фази найкраще здійснюється, якщо її розглядати як вимірювану поверхню. Збережіть один ідеальний зразок виконання, один випадок невдачі та примітку про скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям перед складними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність дій. Розділяйте політику часткового оброблення даних та політику їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

ls -la chart_dataset/
chart_dataset/
├── raw_train.jsonl     # Raw Q&A pairs (flat keys)
├── train.jsonl         # MLX-formatted (overwritten by prepare_mlx_dataset)
└── images/             # Chart images

📌 Important: Before running the fine-tuning command, ensure train.jsonl exists in your dataset directory.
If you see a FileNotFoundError, you haven't run the conversion step yet.

5. Дооптимізація за допомогою MLX

Етап 5 «Тонке налаштування з використанням MLX» працює найкраще, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний результат, один випадок невдачі та примітку про скасування змін перед розширенням обсягу роботи. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте мовчазного часткового виконання завдань. Розділіть політику часткового оброблення даних від політики їх пошуку. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

Попередні вимоги

Етап попередніх умов працює найкраще, якщо його розглядати як вимірювану основу. Зафіксуйте один ідеальний результат, один випадок збою та примітку щодо скасування змін перед розширенням обсягу роботи. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Візуалізація витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-середовища до спільних середовищ. Розділяйте політику часткової обробки даних та політику їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

# For fine-tuning vision models
pip install mlx-vlm

# For merging adapters (needed after training)
pip install mlx-lm

Запустити доробку

Етап деталізованої налаштування працює найкраще, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь граф. Розділіть політику часткової обробки даних від політики їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу, коли змінюються показники якості.

python -m mlx_vlm.lora \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --dataset ./chart_dataset/train.jsonl \
    --iters 1000 \
    --batch-size 1 \
    --lora-rank 8 \
    --gradient-accumulation-steps 4 \
    --max-seq-length 512

Розуміння результатів навчання

Етап генерації результатів навчання на розуміння працює найкраще, якщо його розглядати як вимірювану поверхню. Зафіксуйте один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу. Документуйте одночасно шлях успішного виконання та шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої доробки. Розділіть політику часткової обробки даних від політики їх пошуку. Зміна однієї не повинна змушувати до переписування іншої при зміні показників якості.

INFO:__main__:Loading model from Qwen/Qwen2-VL-2B-Instruct
Fetching 11 files: 100%|████████████████████████████████████████| 11/11 [00:00<00:00, 1465.47it/s]
Download complete: :                                                          |  0.00B
Reconstruction complete: |                                           |  0.00B /  0.00B
INFO:__main__:Loading dataset from ./chart_dataset/train.jsonl
INFO:__main__:Setting up LoRA
#trainable params: 9.232384 M || all params: 2208.9856 M || trainable%: 0.418%
INFO:__main__:Setting up optimizer
INFO:__main__:Training model (sft)
Starting training..., iterations: 1000
No validation dataset provided — training will run without validation.
......

Iter 120: Train loss 7.77592850, Learning Rate 2.000e-05,
It/sec 0.242, Tokens/sec 103.862, Trained Tokens 51480, Peak mem 8.069 GB
.....

Saved final adapter weights to adapters.safetensors.
INFO:__main__:Training completed! Model saved to adapters.safetensors

6. Усунення несправностей

Етап усунення несправностей працює найкраще, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям перед складними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність дій. Розділяйте політику часткової обробки даних та політику їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

Помилка „Дані недоступні“

Етап обробки помилки „Дані недоступні“ працює найкраще, якщо його розглядати як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи.

FileNotFoundError: Couldn't find any data file at .../chart_dataset/train.jsonl
python convert_to_mlx_format.py

Помилка „Не вистачає пам’яті“

RuntimeError: [METAL] Command buffer execution failed: Insufficient Memory
python -m mlx_vlm.lora \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --dataset ./chart_dataset/train.jsonl \
    --iters 1000 \
    --batch-size 1 \              # ← Reduced from 4 to 1
    --lora-rank 4 \               # ← Reduced from 8 to 4
    --gradient-accumulation-steps 8 \  # ← Added
    --max-seq-length 256               # ← Added

Шлях адаптера не знайдено

FileNotFoundError: The adapter path does not exist: fine_tuned_adapters
ls -la adapters.safetensors adapter_config.json
python -m mlx_lm fuse \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --adapter-path . \
    --save-path ./fine_tuned_model_merged

Помилка неповної кеш-пам’яті

IncompleteSnapshotError: The cached snapshot for 'Qwen/Qwen2-VL-2B-Instruct' is incomplete
rm -rf ~/.cache/huggingface/hub/models--Qwen--Qwen2-VL-2B-Instruct

7. Скільки часу займе навчання?

Total Time (seconds) = Total Iterations ÷ It/sec
Total Time (minutes) = Total Time (seconds) ÷ 60
1000 ÷ 0.242 = 4,132 seconds
4,132 ÷ 60 = ~69 minutes

8. Об’єднання адаптерів LoRA

python -m mlx_lm fuse \
    --model Qwen/Qwen2-VL-2B-Instruct \
    --adapter-path . \
    --save-path ./fine_tuned_model_merged

9. Експорт у формат GGUF для Ollama

Крок 1: Експорт MLX у формат Hugging Face

#!/usr/bin/env python3
import mlx_lm
from mlx_lm import load, save

model, tokenizer, config = load("./fine_tuned_model_merged")
save(model, tokenizer, config, "./hf_export")
print("✅ Exported to ./hf_export")
python export_to_hf.py

Крок 2: Перетворення у GGUF

# Clone llama.cpp (if not already done)
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# Convert text model
python convert_hf_to_gguf.py ../hf_export \
    --outfile chart_model-text.gguf \
    --outtype f16

Крок 3: Перетворення Vision Projector

python convert_hf_to_gguf.py ../hf_export \
    --outfile chart_model-mmproj.gguf \
    --outtype f16 \
    --mmproj
ls ~/.cache/huggingface/hub/models--Qwen--Qwen2-VL-2B-Instruct/snapshots/

# sample output . You'll see a hash directory (e.g., 895c3a49...)
# 895c3a49bc3fa70a340399125c650a463535e71c
cd llama.cpp
# replace <hash> with the output above (e.g., 895c3a49...)

python convert_hf_to_gguf.py ~/.cache/huggingface/hub/models--Qwen--Qwen2-VL-2B-Instruct/snapshots/<hash>/ \
    --outfile base-mmproj.gguf \
    --outtype f16 \
    --mmproj

10. Розгортання з використанням Ollama

FROM ./chart_model-text.gguf
FROM ./chart_model-mmproj.gguf
PARAMETER temperature 0.2

Створення моделі

ollama create my-chart-model -f Modelfile

Перевірка моделі

ollama list

# output :

# NAME                     ID              SIZE      MODIFIED
# my-chart-model:latest    b3fd6d8fd742    4.4 GB    7 hours ago
# qwen2.5vl:3b             fb90415cde1e    3.2 GB    9 days ago
# llama3.2:3b              a80c4f17acd5    2.0 GB    10 days ago
# llama3:latest            365c0bd3c000    4.7 GB    3 months ago

# You should see my-chart-model in the list.

Тестування моделі

ollama run my-chart-model "What is 2+2?"

11. Інтеграція у вашу існуючу програму RAG

# In rag_engine.py (from Parts 1 & 2)

# Before:
VISION_MODEL = "qwen2.5vl:3b"

# After:
VISION_MODEL = "my-chart-model"  # Your fine-tuned model
TEXT_MODEL = "llama3.2:3b"       # Unchanged
python app.py

12. Оцінка: базова модель проти налаштованої

Порівняння

13. Висновки

Отримати повний код

git clone https://github.com/froilan-sia/m1_multimodal_rag.git
cd m1_multimodal_rag

Зв’язатися зі мною

Чек-лист для роботи