null

Дрессировка парнокопытных, или как дообучить LLAMA3 с минимальными жертвами

Каждый первый человек, имеющий минимальное представление об интернет пространстве и его состоянии на текущий момент, уже слышал такие странные слова, как ChatGPT, Deepseek и прочие искусственно-интелектуальные штуки. Все они работают на серьезных вычислительных мощностях где-то там, где их и разработали, а мы, как обыватели, лишь используем их API для получения ответов на поставленные нами вопросы. 

Какое-то резкое начало получилось... Все же, давайте с начала.

Что такое LLM?

Large Language Model - дословно, большая языковая модель, обученная на огромном количестве текстовых данных с использованием различных архитектур нейронных сетей (о них чуть позже).

По факту, LLM - лишь алгоритм, который умеет понимать и генерировать текст, его уже "натренировали" на чтении миллиардов предложений. Этот алгоритм умеет предсказывать, какое слово (или символ/токен/etc) должно идти дальше в текущем контексте.

К 2025 году каждая крупная корпорация захотела создать свою языковую модель, и на данный момент их обилие настолько велико, что перечислять всех - тяжело, долго и не нужно. Обсудим в двух словах главных "слонов" из этого мира:

ChatGPT - на данный момент самая популярная модель от компании OpenAI
Gemini - модель, умеющая долго держать один контекст в реализации от Google
Mistral - быстрая и точная модель от одноименной компании
Jurassic-2 - поделка от AI21 Labs, ориентированая на сложные и большие тексты
Tongyi Qianwen - прородитель Deepseek родом из Китая, поддерживаемый компанией Alibaba

Что объединяет вышеописанные продукты? Правильно, как и было сказано ранее, они "живут" на вычислительных мощностях своих компаний, учатся и ищут данные для обучения по запросам сотен тысяч пользователей со всего мира.

При чем тут парнокопытные?

Обсудили умные и крутые модели, теперь поговорим про их младших собратьев - открытые LLM.
Они отличаются от предыдущих возможностью локального запуска у себя на компьютере, возможностью дообучения и кастомизации.
Оных моделей очень и очень много, но сейчас речь пойдет про одну конкретную -  LLaMa от Meta каких-то там запрещенных разработчиков.

На оффициальном сайте разработчика в разделе Models and Products можно лицезреть следующую картину:

В чем разница? Зачем так много? На самом деле все довольно просто.

8B: лёгкая и быстрая модель, подходит для локального запуска
1B и 3B: минимальные размеры, идеальны для мобильных устройств и edge-компьютеров.
70B: высокопроизводительная модель с акцентом на оптимизацию и доступность.
405B: гигантская модель (весовая категория GPT-4), флагман, скорее всего, используется в облаке, не для локального запуска.

Я думаю не сложно провести сопоставление функционала и числа перед буквой B, тут вроде все очевидно.

Про саму модель можно говорить довольно долго, но в силу уважения времени уважаемого читателя сократим этот список.

Архитектура базируется на трех основых приницпах:

Transformer Decoder-Only (как GPT): предсказывает следующее слово по контексту (бОльшая часть LLM существует именно на этой архитектуре)

Causal Attention: смотрит только в прошлое, не «заглядывает» вперёд (но получается не всегда)

Оптимизация под inference: быстрая генерация текста, низкие задержки

В целом, этих вводных данных достаточно для осознания того, что нам предлагают развернуть у себя на локальной машине своего маленького друга, который заведомо куда менее умный, нежели интернет-друзья, но в комплекте с ним мы можем получить учебник, шмякнув которым пару раз по нашей модели (или корпусу ПК) мы сделаем друга умнее.

И вот, спустя три с половиной тысячи символов мы переходим к...

Развертывание и запуск

Для решения поставленной задачи предлагаю использовать все прелести жизни в 21 веке - Docker и все, что идет с ним в комплекте.

Напишем базовый docker-compose.yml файл:

version: '3.8'

services:
  ollama:
    image: ollama/ollama
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama-data:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]

volumes:
  ollama-data:

Ремарка: ллама будет хорошо жить и кушать травку при наличии GPU, но и на CPU она может существовать, сжирая во время ответа 32гб оперативной памяти только в путь

Окей, теперь запустим зверька в контейнере (как бы прискорбно это не звучало)

docker compose up -d
docker exec -it ollama ollama run llama3

И вот мы попали в CLI интерфейс LLaMa3. Уже сейчас с ней можно немного поговорить, на базовые вопросы и размышления она (в конфигурации  8B) умеет отлично отвечать.

Но ведь если мы бы хотели просто поболтать, куда разумнее было бы просто зайти на любой из уже известных сайтов и получить тонну информации на любой счет, не так ли? 

Если мы развернули модель локально - значит у нас есть для нее специфическая задача. В моем случае такой задачей стало определение необходимой конфигурации запуска сервиса в зависимости от пользовательского запроса.

Из коробки ллама не умела давать точный и однозначный ответ на данный вопрос. Вывод прост - поехали ее дообучать!

Дрессировка

Дообучить LLM == адаптировать её ответы под конкретную задачу. 

Существует несколько подходов к данному процессу.

Тип Описание
Full fine-tuning Полное переобучение весов модели. Требует много VRAM и данных.
LoRA / QLoRA Дообучаются только маленькие адаптационные слои. Дёшево и быстро.
Instruction tuning Дообучение на вопрос-ответ, формат prompt → response.
DPO / RLAIF Дообучение с предпочтениями (модель учится выбирать лучший ответ).

 

Странные аббревиатуры из мира ML заставляют задуматься любого человека, но не переживайте, данный вопрос был успешно обсужен с ChatGPT более матерыми и продвинутыми юзерами нейронных сетей и в кротчайшие сроки был получен ответ - для моей задачи идеально подходил LoRA метод.

Каво? Метод чево? 

В двух словах - вместо того чтобы изменять все веса большой модели (что требует много GPU-памяти и времени), LoRA (Low-Rank Adaptation) добавляет небольшие обучаемые слои (ранг-n матрицы) только в определённые места — обычно в attention.

На этом моменте нужно немного подумать. И так:

Допустим, есть матрица весов W размером 4096×4096 в attention. Обновлять её напрямую — дорого. LoRA же проведет следующую махинацию:
 

W' = W + ΔW  
ΔW = A @ B

Где 

A — матрица размера (4096×r),

B — (r×4096),

r — маленький ранг (например, r=8)

Обучаются только A и B, а W останется замороженной (!важно!)

Что такое attention и почему мы будем "что-то" добавлять туда? 

Attention - ключевой механизм "внимания", который позволяет LLM генерировать контекст и обращать внимание на нужные слова. Говоря человеческим языком - "На какие слова в предложении стоит обращать внимание, чтобы понять или сгенерировать следующее слово?"

Пример:

"Разработчик пошел в магазин готовой еды, потому что он голоден"
Кто "он"? Магазин или разработчик?

Каждое слово в предложении получает три вектора: Query (Q) – что ищем, Key (K) – что у нас есть, Value (V) – информация, которую хотим извлечь

Алгоритм:

  1. Считаем сходство Q*K между словами

  2. Получаем веса внимания (attention scores) — насколько важны остальные слова для текущего

  3. Используем эти веса, чтобы усреднить значения V и получить «смысловой контекст»

  Слово «разработчик» «пошел» «в магазин»
«он» 0.9 0.05 0.05

 

Фух, от теории перейдем к практике.

Скрещиваем ламу с асколотлем (???)

Дабы не сойти с ума с ручным пересчетом матриц и просчета датасетов используем инновации - CLI утилиту Axolotl для реализации LoRA дообучения из коробки. 

Для начала работы с тулзой необходимсо провести процедуру инсталляции. Для этого нужен Python >=3.10, CUDA и немного (немало, на самом то деле) VRAM. Чтобы ничего не прокисло на этапе запуска утилита хочет иметь во владении 12-16гб.

git clone https://github.com/OpenAccess-AI-Collective/axolotl
cd axolotl
pip install -r requirements.txt

И так, для того, чтобы модель стала умнее нам необходимо иметь аж 2 вещи: датасет и конфиг-файл для Axolotl. Все остальное утилита сделает сама. 

По итогу структура проекта должна будет принять следующий вид

project/
├── configs/
│   └── llama3-qlora.yml     # <-- Конфига Axolotl
├── data/
│   └── tuneit_query.jsonl   # <-- Датасет
└── outputs/
    └── tune-it-llama-w      # <-- Веса LoRA, их нам даст Axolotl

Определяемся с форматом датасета. Зачастую, хватает лишь формы ["Запрос:{...}","Ответ:{...}"], но их вариация и дополнительные поля будут варьироваться от конкретной модели.

Сколько данных нам нужно? Условимся, что тут у нас нет ограничений, ведь precision модели будет пытаться стремиться к единице. Методологией пристального взгляда и тыка пальцем в небо у меня получилось полторы тысячи подобных пар, объединенных в формате jsonl. По меркам профессиональных "дообучателей" это совсем мало, но, забегая на перед, можно сделать вывод - достаточно.

Учитывая специфику ollama формат получился следующий:
​​​​

{"instruction": "Назови столицу Франции", "input": "", "output": "Париж."}

Где взять полторы тысячи данных для составления датасета? Ответ как никогда прост - сходите в chatgpt-4o через программку, написанную на коленке на Python по вашему API Ключу от OpenAI и вы получите околонеограниченное количество тестовых и однотипных данных.

После того, как мы смогли нагенерить датасет, настала пора разбираться в yaml-кодинге для Axolotl.

Из минимально необхоимого нам нужно:

  1. Базовая модель - в нашем случае meta-llama/Meta-Llama-3-8B-Instruct
  2. Модель, на базе которой идёт дообучение. Можно использовать любую HuggingFace-модель
  3. Подключение 4-х битного квантирования (Квантование — разбиение диапазона значений некоторой величины на конечное число уровней и округление этих значений до ближайших к ним уровней) для экономии ресурсов
  4. Указываем тип токенизатора (штуки, которая будет разбивать наш текст на токена для скармливания в алгоритм), у каждой модели есть свой собственный токенизатор
  5. Путь к датасету
  6. Базовые данные для старта обучения - количество эпох, количество примеров для обработки за раз, количество накопленных градиентов (можете не вдаваться в подробности, оно особо не надо)
  7. Конфигурация для самой LoRA
  8. Стартовая скорость обучения

Вот полный конфиг-файл с минимальными комментариями:

# Название базовой модели
base_model: meta-llama/Meta-Llama-3-8B-Instruct

# Тип модели - для LLaMA обязательно LlamaForCausalLM
model_type: LlamaForCausalLM

trust_remote_code: true

# Тип токенизатора
tokenizer_type: LlamaTokenizer

# Все для квантирования
load_in_4bit: true
adapter: qlora

# Описание датасета
datasets:
  - path: ./tuneit_query.jsonl
    type: alpaca                   

# Размер валидационной выборки (сколько мы оставим данных на валидацию)
val_set_size: 0.05

output_dir: ./outputs/tune-it-llama-w

# Все для описания обучения
num_epochs: 3

micro_batch_size: 4

gradient_accumulation_steps: 8

# Периодичность валидации (в шагах обучения)
eval_steps: 50

# Параметры LoRA
lora_r: 8                           
lora_alpha: 16                     
lora_dropout: 0.05                  
lora_target_modules: ["q_proj", "v_proj"]

# Настройки обучения
lr_scheduler: cosine              
learning_rate: 0.0002               
warmup_steps: 20                    

logging_steps: 10

use_flash_attention: true

# Настройки 4-битной квантизации
bnb_4bit_quant_type: nf4        
bnb_4bit_compute_dtype: float16

Фух, последнее что от нас требуется - запуск и последующее тестирование.

Запускаем!

python -m axolotl.cli.train configs/llama3-qlora.yml

Дальнейшие действия провернет уже сама утилита. Осталось подождать (долго)

После того, как в директории ./outputs что-то появилось, можем приступить к тестированию. Очевидно на готовых коробочных решениях Python

from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

base = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct", device_map="auto", load_in_4bit=True)
model = PeftModel.from_pretrained(base, "./outputs/tune-it-llama-w")

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct")

prompt = "Дак какая столица у Франции и какая у нее история?"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=100)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))\

Да, сейчас мы руками пропихнули и прибили гвоздями новые веса и не сможем получить новые данные из коробки докера.
Далее можно все соединить в .gguf файл и запускать через образ, предварительно объединив матрицы через merge_peft_adapter, но об этом в другой раз...
​​​​​​​

Вывод

Что мы имеем? LLaMa, которая умеет правильно отвечать в нашем контексте, имеем готовый конфиг файл для дообучения, фактически, любой модели на любые данные и новый багаж новых непонятных слов...

Спасибо за внимание

 

Previous Next

Коротко о себе:

Дикий кабан устроил переполох на пляже в Крыму

Работаю кем-то в компании Tune-it. Занимаюсь какими-то проектами, связанными с чем-то.

Nothing has been found. n is 0