AI Research Foundations, курс 5, часть 1: от продолжателя текста к ответам на вопросы
Перевод лабораторных 5.1–5.3 курса Google DeepMind «Finetune Your Model»: базовая модель не умеет отвечать на вопросы, шаблон диалога со специальными токенами и полное дообучение маленькой модели с маскированием вопроса в потерях.
Курс 5 программы AI Research Foundations называется «Finetune Your Model» («Дообучите свою модель»). Языковая модель, обученная предсказывать следующий токен, умеет продолжать текст, но не отвечать на вопросы. В этом курсе вы превращаете её в генератор ответов для учебных карточек (flashcards): сначала сами убеждаетесь, что базовая модель на вопросы отвечает плохо, затем готовите данные в формате диалога и дообучаете всю модель. Продолжение — часть 2: Gemma, нехватка памяти и LoRA.
Как проходить вместе с курсом ChinaAI
Заголовок раздела «Как проходить вместе с курсом ChinaAI»| Лабораторная | Время | Когда делать | Что нужно |
|---|---|---|---|
| 5.1. Задайте SLM вопросы | 15 мин | Вместе с уроком «От базовой модели к ассистенту» | Keras на JAX, желательно GPU |
| 5.2. Формат текста для диалога по репликам | 15 мин | Вместе с уроком «SFT — данные, шаблоны, маскирование» | pandas, CPU |
| 5.3. Дообучите все параметры своей SLM | 30 мин + дообучение | После урока «SFT — данные, шаблоны, маскирование» | Keras на JAX, TensorFlow (tf.data), GPU желателен |
Всё, что в лабораторной 5.3 делается руками — шаблон <start_of_turn>user … <end_of_turn>, склейка вопроса и ответа,
замена токенов вопроса на <PAD> в целях, маленький learning rate, — это ровно то, что в уроке 6.1.2 курса ChinaAI делает
SFT на PyTorch и в библиотеке TRL. Полезно увидеть эти шаги без «магии» библиотек.
Лабораторная 5.1. Задайте SLM вопросы
Заголовок раздела «Лабораторная 5.1. Задайте SLM вопросы»Ноутбук: gdm_lab_5_1_prompt_your_slm_with_questions.ipynb · открыть в Colab
Исследуйте, как ваша маленькая языковая модель реагирует на промпты в разных форматах. 15 минут.
В предыдущем задании вы предсказывали, как поведёт себя ваша обученная модель. В этой лабораторной вы проверите эти предсказания. Вы загрузите трансформер, обученный на наборе Africa Galore, и посмотрите, как он справляется с промптами разных видов — от простых продолжений предложений до прямых вопросов.
Чему вы научитесь
Заголовок раздела «Чему вы научитесь»К концу лабораторной вы сможете сравнить качество продолжений для промптов в виде утверждений и в виде вопросов у модели, которая обучалась только предсказывать следующий токен.
В этой короткой лабораторной вы испытаете свой обученный трансформер, чтобы понять его возможности и ограничения.
В этой лабораторной вы:
- загрузите свой обученный трансформер;
- дадите модели ряд разных промптов — и утверждения, и вопросы;
- разберёте сгенерированные ответы, чтобы понять, как модель ведёт себя на разных видах промптов.
Если есть возможность, мы настоятельно советуем запускать лабораторную на экземпляре Colab с GPU.
Импорты
Заголовок раздела «Импорты»В этой лабораторной вы используете трансформер, обученный в прошлых курсах, и Keras, чтобы задать модель и загрузить её параметры.
%%capture!pip install jax[cuda12]==0.7.2# Ставим специальный пакет курса.!pip install "git+https://github.com/google-deepmind/ai-foundations.git@main"
# Используемые пакеты.import os # Чтобы задать переменные конфигурации Keras.
# Следующая строка настраивает Keras: вычисления — на JAX.os.environ["KERAS_BACKEND"] = "jax"
import keras # Чтобы задать и загрузить модель.import re # Чтобы делить текст по пробелам.from urllib import request # Чтобы скачать веса модели.
# Для более понятных сообщений об ошибках.from IPython.display import display, HTML
from ai_foundations import training # Чтобы задать модель.from ai_foundations import generation # Чтобы давать модели промпты.# Чтобы загрузить токенизатор.from ai_foundations.tokenization import BPEWordTokenizerЗагружаем токенизатор и модель
Заголовок раздела «Загружаем токенизатор и модель»В этой лабораторной вы работаете с уже обученными токенизатором и моделью. Вместо того чтобы снова обучать их с нуля, следующая ячейка загружает правила слияния BPE-токенизатора и параметры трансформера. И модель, и токенизатор обучены на наборе Africa Galore.
url = "https://storage.googleapis.com/dm-educational/assets/ai_foundations/africa_galore_qa_tokenizer_3000.pkl"
tokenizer = BPEWordTokenizer.from_url(url)
model_url = "https://storage.googleapis.com/dm-educational/assets/ai_foundations/africa_galore_qa_1000ep.weights.h5"model_filename = "africa_galore_qa_1000ep.weights.h5"request.urlretrieve(model_url, model_filename)
# Задаём модель (те же гиперпараметры, что при обучении).model = training.create_model( max_length=399, vocabulary_size=tokenizer.vocabulary_size,)
# Загружаем параметры модели из файла.model.load_weights(model_filename)
print( f"Initialized and loaded transformer model with {model.count_params():,}" f" parameters.")Задание 1. Промпты модели
Заголовок раздела «Задание 1. Промпты модели»Теперь, когда обученная модель загружена, можно проверить, как она себя ведёт.
# @title Промпт вашей маленькой языковой моделиprompt = "Jollof rice is" #@param {type: "string"}num_tokens_to_generate = 10 #@param {type: "number"}generated_text, probs = generation.generate_text( prompt, num_tokens_to_generate, model=model, tokenizer=tokenizer, pad_token_id=tokenizer.pad_token_id, sampling_mode="greedy" # Генерируем самые вероятные токены.)
print(f"Generated text: {generated_text}")В этой лабораторной вы давали промпты базовой модели (base model), обученной предсказывать следующий токен. Вы своими глазами увидели: модель отлично справляется с задачей, на которой обучалась, — продолжать утверждения, — но на прямые вопросы надёжно не отвечает. Это и есть ключевая разница между универсальным продолжателем текста и моделью, дообученной следовать инструкциям (instruction-tuned).
Лабораторная 5.2. Формат текста для диалога по репликам
Заголовок раздела «Лабораторная 5.2. Формат текста для диалога по репликам»Ноутбук: gdm_lab_5_2_format_text_for_turn_based_dialogue.ipynb · открыть в Colab
Отформатируйте данные для генератора учебных карточек для повторения. 15 минут.
В прошлой лабораторной вы увидели, что обученная базовая модель плохо отвечает на вопросы. В этой лабораторной вы сделаете первый шаг к решению: подготовите качественный набор данных для дообучения. Цель — набор, отформатированный так, чтобы на нём можно было обучить модель генерировать ответы для учебных карточек по африканской культуре и географии. Вы загрузите набор пар «вопрос — ответ» и переформатируете их специальными токенами-разделителями (delimiter tokens), чтобы чётко отделить реплику пользователя (вопрос) от реплики модели (ответа).
Чему вы научитесь
Заголовок раздела «Чему вы научитесь»К концу лабораторной вы сможете:
- объяснить, зачем нужны специальные токены-разделители, чтобы структурировать диалоговые данные для дообучения;
- написать на Python функцию, которая превращает сырые данные в нужную структуру.
Главная задача — написать функцию, которая форматирует данные для задачи генерации карточек.
В этой лабораторной вы:
- загрузите JSON-файл с парами «вопрос — ответ»;
- напишете функцию, которая принимает на вход одну запись данных;
- добавите внутри функции специальные токены-разделители, отмечающие начало и конец вопроса и ответа.
Импорты
Заголовок раздела «Импорты»В этой лабораторной вы в основном работаете с библиотекой pandas, чтобы загрузить набор данных.
%%capture# Ставим специальный пакет курса.!pip install "git+https://github.com/google-deepmind/ai-foundations.git@main"
# Используемые пакеты.import pandas as pd # Чтобы загрузить набор данных.from textwrap import fill # Чтобы красиво переносить длинные абзацы.
# Функции для обратной связи.from ai_foundations.feedback.course_5 import formatting as feedbackЗагружаем и изучаем набор данных
Заголовок раздела «Загружаем и изучаем набор данных»Сначала загрузите набор вопросов и ответов и изучите его структуру. В нём информация, которую ваша модель в итоге должна выучить.
Запустите следующую ячейку, чтобы увидеть число примеров и поля первой записи набора.
africa_galore_qa = pd.read_json( "https://storage.googleapis.com/dm-educational/assets/ai_foundations/africa_galore_qa_v2.json")
print( f"Loaded dataset with {africa_galore_qa.shape[0]:,}" f" question-answer pairs.\n")# Печатаем названия всех полей набора.print(f"Dataset columns: {', '.join(africa_galore_qa.columns)}\n")
# Печатаем первый пример набора.print(f"First example:")print(f"Category: {africa_galore_qa['category'][0]}")print(f"Name: {africa_galore_qa['name'][0]}")print(f"Question: {africa_galore_qa['question'][0]}")print(fill(f"Answer: {africa_galore_qa['answer'][0]}"))Обратите внимание: сырой ответ содержателен, но не приспособлен для конкретной задачи — ответа чат-бота или оборота учебной карточки. Прежде чем дообучать модель давать лучшие, более сжатые ответы, данные нужно переформатировать. Тогда на них можно будет обучить модель генерировать формат «вопрос — ответ», нужный генератору карточек.
Форматируем вопросы и ответы
Заголовок раздела «Форматируем вопросы и ответы»Модель, обученная на повествовательном тексте, не умеет работать с диалогом, где реплики идут туда и обратно. Чтобы обучить её этому, нужно явно отметить, где кончается реплика одного собеседника и начинается реплика другого. Как говорилось в предыдущей статье, для этого каждый вопрос и ответ оборачивают специальными токенами-разделителями. Функция ниже принимает вопрос и ответ и готовит их в формате реплик. В формат входит и информация о том, чья реплика (пользователя или модели).
def format_qa( question: str, answer: str, sot: str = "<start_of_turn>", eot: str = "<end_of_turn>",) -> tuple[str, str]:
"""Добавляет специальные разделители в начало и конец вопроса и ответа.
Аргументы: question: вопрос карточки. answer: ответ карточки. sot: токен начала реплики. eot: токен конца реплики.
Возвращает: formatted_q: отформатированная строка вопроса. formatted_a: отформатированная строка ответа. """
formatted_q = f"{sot}user\n{question}{eot}\n" formatted_a = f"{sot}model\n{answer}{eot}"
return formatted_q, formatted_aЗапустите следующую ячейку, чтобы отформатировать заданную вами пару «вопрос — ответ». Посмотрите, что делает функция с разными парами.
# @title Форматируем пару «вопрос — ответ»question = "What is Jollof rice?" #@param {type: "string"}answer = "Jollof rice is a tasty African dish that is made with a red sauce." #@param {type: "string"}
formatted_q, formatted_a = format_qa(question,answer)
print(formatted_q + formatted_a)Ожидаемый вывод:
<start_of_turn>userWhat is Jollof rice?<end_of_turn><start_of_turn>modelJollof rice is a tasty African dish that is made with a red sauce.<end_of_turn>Как видно из вывода, функция оборачивает текст токенами <start_of_turn> и <end_of_turn>. Они похожи на токены <PAD>
и <UNK>, которые вы уже встречали: как и все специальные токены, они служат для модели сигналами.
Кроме того, каждая реплика начинается с указания, кто говорит, — пользователь (user) или модель (model). Если использовать этот формат во всём наборе данных, модель выучит не только содержание, но и форму диалога: вопрос, за которым следует ответ.
Более удобный собственный формат
Заголовок раздела «Более удобный собственный формат»Разметка реплик — первый шаг, но генератору карточек нужен более конкретный и полезный формат. Хорошая карточка сразу переходит к делу. Один из способов этого добиться — указывать в начале ответа на карточке категорию, например так:
Category: Food
Jollof rice is a popular and iconic one-pot rice dish that is a staple in many West African countries.
(«Категория: еда. Джолоф — популярное и знаковое блюдо из риса, которое готовят в одном котле; основная еда во многих странах Западной Африки».)
Чтобы модель выучила такое поведение, набор данных нужно обработать новой функцией, которая не только добавляет разделители реплик, но и вставляет в строку ответа категорию, которая есть в наборе.
Задание 1. Создаём формат карточки
Заголовок раздела «Задание 1. Создаём формат карточки»def format_qa( data: pd.Series | dict[str, str], sot: str = "<start_of_turn>", eot: str = "<end_of_turn>",) -> tuple[str, str]: """Добавляет специальные разделители в начало и конец вопроса и ответа.
Аргументы: data: строка таблицы с полями "category", "question" и "answer". sot: строка — токен начала реплики. eot: строка — токен конца реплики.
Возвращает: formatted_q: отформатированная строка вопроса. formatted_a: отформатированная строка ответа. """
category = data["category"] question = data["question"] answer = data["answer"]
formatted_q = # Добавьте свой код здесь. formatted_a = # Добавьте свой код здесь.
return formatted_q, formatted_a
# Добавьте здесь код, чтобы проверить функцию.# Например, запустите функцию и напечатайте результат. Убедитесь, что он# совпадает с тем, что вы ожидаете.# Набор данных доступен в переменной `africa_galore_qa`.# Запустите эту ячейку, чтобы проверить код.feedback.check_qa_format(format_qa, africa_galore_qa)В этой лабораторной вы отформатировали набор данных в нужный формат «вопрос — ответ», на котором теперь можно обучать ваш трансформер. Вы готовы дообучить модель, чтобы использовать её как генератор карточек.
Решения
Заголовок раздела «Решения»Задание 1
# Полная реализация format_qa.def format_qa( data: pd.Series | dict[str, str], sot: str = "<start_of_turn>", eot: str = "<end_of_turn>",) -> tuple[str, str]: category = data["category"] question = data["question"] answer = data["answer"]
formatted_q = f"{sot}user\n{question}{eot}\n" formatted_a = f"{sot}model\nCategory: {category}\n{answer}{eot}"
return formatted_q, formatted_aОбратите внимание: в эталонном решении после вопроса стоит перевод строки \n, а в примере из задания его нет. Ориентируйтесь
на решение — именно так формат устроен у Gemma, и так его ждёт проверка. (примечание переводчика)
Лабораторная 5.3. Дообучите все параметры своей SLM
Заголовок раздела «Лабораторная 5.3. Дообучите все параметры своей SLM»Ноутбук: gdm_lab_5_3_fine_tune_all_parameters_of_your_slm.ipynb · открыть в Colab
Научите модель, обученную предсказывать следующий токен, генерировать ответы на вопросы. 30 минут.
В прошлой лабораторной вы написали функцию, которая выдаёт ответ в нужном формате карточки. В этой лабораторной вы узнаете, как обучить языковую модель генерировать ответы для карточек в конкретном формате. Это помогает автоматизировать создание ответов для карточек: по вашему вопросу модель сама дописывает и категорию, и ответ.
Для этого понадобятся два компонента:
- трансформерная языковая модель из прошлых курсов, которая умеет предсказывать следующий токен;
- новый небольшой набор вопросов и ответов — Africa Galore QA.
Вы будете использовать метод полного дообучения (full-parameter fine-tuning): продолжите обучать трансформер на новой задаче, делая дополнительные шаги градиентного спуска. В отличие от обучения с нуля, так можно получить специализированную модель за меньшее число итераций обучения. Дообученная модель объединит свои языковые способности и другие закономерности, выученные при предобучении, со сведениями о формате задачи.
Лабораторная проведёт вас через весь конвейер дообучения: загрузку обученной модели, загрузку нового набора данных, его подготовку, дообучение модели и её оценку.
flowchart LR A["Обученная SLM<br/>(курсы 1–4)"] --> D["model.fit<br/>маленький learning rate"] B["Africa Galore QA"] --> C["format_qa → токены →<br/>вход и цель, вопрос → PAD"] C --> D D --> E["SLM, которая отвечает<br/>в формате карточки"]
Чему вы научитесь
Заголовок раздела «Чему вы научитесь»К концу лабораторной вы сможете:
- назвать, чем дообучение похоже на обучение с нуля;
- понять сходства и различия между общей и более конкретной задачей — предсказанием следующего токена и ответом на вопрос в виде карточки;
- подготовить данные для дообучения;
- дообучить существующую модель.
В этой лабораторной вы:
- загрузите небольшой набор данных для новой задачи;
- отформатируете данные так, чтобы они подходили обученной модели;
- загрузите обученную модель;
- дообучите её на новых данных для задачи карточек;
- оцените модель.
Если есть возможность, мы настоятельно советуем запускать лабораторную на экземпляре Colab с GPU.
Импорты
Заголовок раздела «Импорты»В этой лабораторной вы будете дообучать трансформер. Для этого понадобятся пакет Keras, чтобы задать и обучить трансформерную языковую модель, пакет Pandas, чтобы загрузить набор данных, и пакет TensorFlow, чтобы перемешать и дополнить набор для дообучения.
%%capture!pip install jax[cuda12]==0.7.2# Ставим специальный пакет курса.!pip install "git+https://github.com/google-deepmind/ai-foundations.git@main"
import os # Чтобы задать переменные конфигурации Keras.
# Настраиваем Keras: вычисления — на JAX.os.environ["KERAS_BACKEND"] = "jax"
from textwrap import fill # Чтобы переносить и форматировать текст.from urllib.request import urlretrieve # Чтобы загрузить модель по ссылке.
import keras # Чтобы задать и обучить модель.import pandas as pd # Чтобы загрузить набор данных.import tensorflow as tf # Чтобы перемешать набор данных.
# Для более понятных сообщений об ошибках.from IPython.display import display, HTMLfrom ai_foundations import training # Чтобы обучать модель.from ai_foundations import generation # Чтобы давать модели промпты.# Чтобы загрузить токенизатор.from ai_foundations.tokenization.bpe_tokenizer import BPEWordTokenizerfrom ai_foundations import formatting # Чтобы форматировать обучающие данные.
keras.utils.set_random_seed(812) # Чтобы обучение было воспроизводимым.Загружаем токенизатор
Заголовок раздела «Загружаем токенизатор»Запустите следующую ячейку, чтобы загрузить токенизатор, который использовался при предобучении.
url = "https://storage.googleapis.com/dm-educational/assets/ai_foundations/africa_galore_qa_tokenizer_3000.pkl"
tokenizer = BPEWordTokenizer.from_url(url)Загружаем обученную модель
Заголовок раздела «Загружаем обученную модель»Следующая ячейка загружает обученную модель. В Keras это делается так: сначала задаётся модель Keras с теми же
гиперпараметрами, что у исходной обученной модели, а затем метод model.load_weights присваивает всем весам модели
итоговые значения параметров обученной модели.
Запустите следующую ячейку, чтобы скачать параметры модели и загрузить обученную модель.
model_url = "https://storage.googleapis.com/dm-educational/assets/ai_foundations/africa_galore_qa_1000ep.weights.h5"model_filename = "africa_galore_qa_1000ep.weights.h5"urlretrieve(model_url, model_filename)
# Задаём модель.model = training.create_model( max_length=399, vocabulary_size=tokenizer.vocabulary_size,)
model.load_weights(model_filename)Загружаем и готовим данные
Заголовок раздела «Загружаем и готовим данные»Теперь загрузите набор Africa Galore QA и примените формат карточек, созданный в прошлой лабораторной. Запустите следующую ячейку, чтобы загрузить набор для дообучения и напечатать первые записи.
# Загружаем набор вопросов и ответов.africa_galore_qa = pd.read_json( "https://storage.googleapis.com/dm-educational/assets/ai_foundations/africa_galore_qa_v2.json")
africa_galore_qa.head(2)Форматируем и токенизируем
Заголовок раздела «Форматируем и токенизируем»На этом шаге вы подготовите набор данных к дообучению модели. Как и при обучении с нуля, нужно задать вход и выход модели. Для этого три шага:
- отформатировать вопросы и ответы;
- токенизировать их, чтобы подавать на вход модели;
- сохранить токенизированные вопросы и ответы в списки, из которых получатся входы и цели для дообучения.
Запустите следующую ячейку, чтобы выполнить эти три шага для каждого примера набора Africa Galore QA.
encoded_questions = [] # Список отформатированных и токенизированных вопросов.encoded_answers = [] # Список отформатированных и токенизированных ответов.
# Проходим по всем записям набора.for idx, row in africa_galore_qa.iterrows():
# Форматируем вопрос и ответ функцией format_qa из прошлой лабораторной. question, answer = formatting.format_qa(row)
# Токенизируем вопрос и ответ. tokenized_q = tokenizer.encode(question) tokenized_a = tokenizer.encode(answer)
# Добавляем токенизированные вопрос и ответ в общие списки. encoded_questions.append(tokenized_q) encoded_answers.append(tokenized_a)Задание 1. Проверяем закодированные вопросы и ответы
Заголовок раздела «Задание 1. Проверяем закодированные вопросы и ответы»Готовя данные, всегда полезно проверить, что примеры обработаны правильно. Например, убедиться, что в списках
encoded_questions и encoded_answers одинаковое число вопросов и ответов, и напечатать несколько обработанных примеров,
чтобы просмотреть их глазами.
# Печатаем число вопросов.print(f"Number of questions: {len(encoded_questions)}")
# Если число ответов не совпадает с числом вопросов — ошибка.assert(len(encoded_questions) == len(encoded_answers))
# Напечатайте первые 10 токенов первых двух вопросов.print() # Добавьте свой код здесь.
# Напечатайте первые 10 токенов первых двух ответов.print() # Добавьте свой код здесь.Подсказка к вопросам (наше пояснение)
Каждый вопрос начинается с одних и тех же токенов шаблона: <start_of_turn>, user, перевод строки, и часто «What is».
Каждый ответ начинается с <start_of_turn>, model, перевода строки и Category: — отсюда ещё более длинный одинаковый
префикс. Длину набора печатает первая строка ячейки.
Задаём входные и целевые последовательности
Заголовок раздела «Задаём входные и целевые последовательности»Вспомните из курса «01 Build Your Own Small Language Model»: языковые модели обучаются на последовательности входных (input) токенов и последовательности целевых (target) токенов. Целевые токены — это входные, сдвинутые на одну позицию влево. При обучении модель учится предсказывать целевой токен в каждой позиции по всем входным токенам до позиции включительно. Параметры обновляются по потерям на каждом целевом токене, чтобы на следующей итерации обучения модель лучше предсказывала каждый токен обучающих данных.
При дообучении на задаче вроде генерации карточек всё это по-прежнему верно. Во-первых, данные тоже нужно подготовить как последовательность входных и целевых токенов. Поэтому для задач «вопрос — ответ», как генерация карточек, пару «вопрос — ответ» нужно превратить в одну последовательность токенов. Для этого вопрос и ответ склеивают:
<start_of_turn>user What is Jollof rice?<end_of_turn><start_of_turn>model Category: Food. \nJollof rice is a...
При дообучении модель выучит, что после последовательности токенов ["<end_of_turn>", "<start_of_turn>", "model"] нужно
предсказать «Category», затем категорию, перевод строки (\n) и ответ на вопрос.
В теории входы и цели для этого примера можно подготовить так (точная последовательность зависит от токенизатора):
inputs = ["<start_of_turn>", "user", "What", "is", "Jollof", "rice", "?", "<end_of_turn>", "<start_of_turn>", "model", "Category", ":", "Food", "\n", "Jollof", "rice", "is", ...]targets = ["user", "What", "is", "Jollof", "rice", "?", "<end_of_turn>", "<start_of_turn>", "model", "Category", ":", "Food", "\n", "Jollof", "rice", "is", ...]На этих двух последовательностях можно продолжить обучение модели. Скорее всего, это сработает, и модель со временем научится генерировать ответы карточек в правильном формате. Но заметьте: обучая модель на таких входах и выходах, вы учите её не только хорошо отвечать, но и задавать вопросы. Ведь потери считаются не только на токенах ответа, но и на токенах промпта.
Чтобы не учить модель лишнему (напомним, задача устроена так, что вопросы всегда задаёт пользователь), можно считать функцию потерь только на токенах ответа. Модель всё равно научится строить ответ по промпту: предсказывая токены ответа, она может обращать внимание на токены промпта. Но веса будут обновляться только так, чтобы модель лучше справлялась с нужной задачей — генерацией ответов для карточек.
Один из способов исключить токены вопроса из подсчёта потерь — заменить их в целевой последовательности специальным токеном
<PAD>. Функцию потерь обычно реализуют так, что она игнорирует предсказания во всех позициях, где целевой токен — <PAD>.
С этим приёмом получается такая пара входов и целей:
# Входы — те же, что раньше.inputs = ["<start_of_turn>", "user", "What", "is", "Jollof", "rice", "?", "<end_of_turn>", "<start_of_turn>", "model", "Category", ":", "Food", "\n", "Jollof", "rice", "is", ...]targets = ["<PAD>", "<PAD>", "<PAD>", "<PAD>", "<PAD>", "<PAD>", "<PAD>", "<start_of_turn>", "model", "Category", ":", "Food", "\n", "Jollof", "rice", "is", ...]Так модель дообучается только на нужной задаче — генерации ответов.
Следующая ячейка готовит входы и цели из закодированных вопросов и ответов предыдущей ячейки. Обратите внимание:
в concatenated_masked все токены вопроса заменены специальным токеном <PAD>, который берётся из токенизатора.
encoded_inputs = []encoded_targets = []pad_token_id = tokenizer.pad_token_idfor idx in range(len(encoded_questions)): # Длина вопроса. len_q = len(encoded_questions[idx]) # Склеиваем вопрос и ответ. concatenated_tokens = encoded_questions[idx] + encoded_answers[idx] # Склеиваем вопрос и ответ, но вопрос заменён токенами дополнения. concatenated_masked = [pad_token_id] * len_q + encoded_answers[idx] # Во вход идут склеенные токены, кроме последнего. encoded_inputs.append(concatenated_tokens[:-1]) # В цель — склеенные (с маской) токены, сдвинутые на 1 позицию. encoded_targets.append(concatenated_masked[1:])Пример с числами (наш): если вопрос — 8 токенов q0…q7, а ответ — 5 токенов a0…a4, то вход — q0 … q7 a0 a1 a2 a3
(12 токенов), а цель — PAD ×7, a0, a1, a2, a3, a4 (тоже 12). Потери считаются только в пяти последних позициях: модель
учится по q0…q7 предсказать a0, по q0…q7 a0 — a1 и так далее.
Остальная подготовка данных
Заголовок раздела «Остальная подготовка данных»Как вы уже знаете из прошлых курсов, примеры нужно ещё дополнить и обрезать, чтобы их можно было собрать в батчи. Кроме того, перемешивание данных помогает обучать эффективнее: оно не даёт очень похожим примерам попадать в один батч.
Учтите: при дообучении нужно соблюдать максимальную длину обученной модели. Поэтому максимальная длина последовательности не выводится из данных, а берётся у обученной модели.
Запустите следующую ячейку, чтобы выполнить дополнение, обрезку, разбиение на батчи и перемешивание.
# Максимальная длина входного текста. input_shape[0] — размер батча.max_input_length = model.input_shape[1]max_output_length = model.output_shape[1]
# Входные последовательности — дополненные вопросы (с ответами).# Дополняем и обрезаем средствами Keras.input_sequences = keras.preprocessing.sequence.pad_sequences( encoded_inputs, # Данные, которые нужно обрезать. maxlen=max_input_length, # Максимальная длина. padding="post", # При необходимости дополняем в конце. truncating="post", # При необходимости обрезаем в конце. value=tokenizer.pad_token_id # id токена дополнения.)
# Целевые последовательности — дополненные ответы.target_sequences = keras.preprocessing.sequence.pad_sequences( encoded_targets, maxlen=max_output_length, padding="post", truncating="post", value=tokenizer.pad_token_id)
# Создаём набор данных TensorFlow, чтобы подготовить последовательности.tf_dataset = tf.data.Dataset.from_tensor_slices( (input_sequences, target_sequences))
# Случайно перемешиваем набор.tf_dataset = tf_dataset.shuffle(buffer_size=len(input_sequences))
# Задаём размер батча.batch_size = 4
# Создаём батчи.batches = tf_dataset.batch(batch_size)Дообучаем модель
Заголовок раздела «Дообучаем модель»Данные готовы, теперь можно дообучить модель на задаче генерации карточек. Чтобы дообучить модель Keras, реализующую трансформер, достаточно продолжить исходное обучение.
Для этого нужно задать модель и загрузить её параметры (как вы сделали в начале лабораторной), а затем продолжить обучение
методом model.fit(), которым вы обучали модели с нуля.
Скорость обучения
Заголовок раздела «Скорость обучения»Важный момент при дообучении — скорость обучения (learning rate). Вспомните из курса «03 Design and Train Neural Networks»: скорость обучения определяет, насколько сильно обновляются веса на каждом шаге стохастического градиентного спуска.
При дообучении важно сохранить уже выученные знания, а не вносить большие изменения. Поэтому скорость обучения при дообучении обычно берут меньше, чем при предобучении. Типичная скорость обучения при предобучении — 0,0001 (1e-4), а при дообучении обычно нужна на один-два порядка меньше: хорошо подходят значения от 0,00001 (1e-5) до 0,000001 (1e-6). Если взять больше, обучение может сильнее страдать от катастрофического забывания (catastrophic forgetting).
Поставьте в следующей ячейке скорость обучения 0,00005 и запустите её, чтобы обновить скорость обучения оптимизатора.
# @title Задаём скорость обученияlearning_rate = 5e-5 # @param {type: "number"}model.optimizer.learning_rate = learning_rateЧисло эпох
Заголовок раздела «Число эпох»Сколько эпох нужно для успешного обучения, сильно зависит от размера набора данных и скорости обучения. Чем меньше скорость обучения, тем больше нужно обновлений. Точно так же, будь ваш набор в десять раз больше, одна эпоха на большом наборе равнялась бы десяти эпохам на маленьком: данные разнообразнее, но и обучение дороже. Помните: если обучать слишком долго, модель испытает катастрофическое забывание или другой вид переобучения.
Запустите следующую ячейку, чтобы задать число эпох. Советуем начать с 50 и увеличивать, только если после дообучения модель, похоже, не выучила формат задачи.
num_epochs = 50 # @param {type: "number"}Функция обратного вызова
Заголовок раздела «Функция обратного вызова»Обучая модель в курсе «01 Build Your Own Small Language Model», вы реализовали функцию обратного вызова (callback). Она позволяет следить за ходом обучения и понимать, что модель выучила после каждой эпохи. Так можно решить, когда остановить обучение и разумна ли скорость обучения.
Запустите следующий код, чтобы задать функцию обратного вызова. Вас интересует, как модель справляется с генерацией карточек, поэтому промпт должен быть похож на промпты из набора для дообучения.
prompt = "<start_of_turn>user\nWhat is Jollof rice?<end_of_turn>\n"
prompt_ids = tokenizer.encode(prompt)text_gen_callback = training.TextGenerator( max_tokens=30, start_tokens=prompt_ids, tokenizer=tokenizer,)Задание 2. Обучаем модель
Заголовок раздела «Задание 2. Обучаем модель»# Добавьте свой код здесь.Задание 3. Проверяем модель
Заголовок раздела «Задание 3. Проверяем модель»В первой лабораторной курса вы проверяли, как обученная модель отвечает на вопросы. Теперь, когда модель дообучена, повторите это упражнение.
# @title Промпт вашей дообученной моделиquestion = "Jollof rice is" #@param {type: "string"}is_question = True #@param {type: "boolean"}if is_question: # Оборачиваем вопрос в шаблон реплики пользователя. prompt = "<start_of_turn>user\n" + question + "<end_of_turn>\n"else: prompt = questionnum_tokens_to_generate = 30 #@param {type: "number"}generated_text, probs = generation.generate_text( prompt, num_tokens_to_generate, model=model, tokenizer=tokenizer, pad_token_id=tokenizer.pad_token_id, sampling_mode="greedy" # Генерируем самый вероятный вариант.)
print("Generated text: ", fill(generated_text, replace_whitespace=False))print("\n")Что вы заметили?
Заголовок раздела «Что вы заметили?»Скорее всего, вы увидели, что модель по-прежнему умеет полезно продолжать промпты-начала утверждений вроде «Jollof rice is». Но, в отличие от первой лабораторной курса, модель теперь, скорее всего, даёт толковые ответы на вопросы, и эти ответы следуют особому формату карточки.
Вероятно, вы заметили и то, что вопрос про южноамериканскую гору Аконкагуа не дал осмысленного ответа. Дело в том, что ни в данных предобучения модели (наборе Africa Galore), ни в данных дообучения нет сведений об этой горе.
В этой лабораторной вы дообучили трансформер генерировать ответы для автоматического генератора карточек. Так языковая модель, оптимизированная только для автодополнения утверждений, превратилась в модель, которая отвечает на вопросы пользователя.
В этом процессе было несколько шагов:
- Форматирование нового набора данных. Вы отформатировали набор Africa Galore QA, добавив специальные разделители
"<start_of_turn>"и"<end_of_turn>", и соединили категорию с ответом в формат карточки. - Загрузка обученной модели. Вы загрузили модель, которая уже умела обрабатывать текст: предсказывать следующее слово по контексту, — но не умела отвечать на вопросы и генерировать ответы в формате карточки.
- Подготовка набора данных. Вы превратили пары «вопрос — ответ» в формат, на котором можно продолжить обучение языковой модели, генерирующей текст по одному токену.
- Дообучение модели. Вы продолжили обучать модель на наборе вопросов и ответов.
Лабораторная также познакомила вас с важными гиперпараметрами дообучения: скоростью обучения, числом эпох и размером батча.
В следующих заданиях вы узнаете об идее базовых моделей (foundation models) и о том, как получить результаты лучше, взяв модель крупнее, обученную на гораздо большем объёме данных. Так можно построить модель, которая отвечает на вопросы по более разнообразным темам — например, на вопрос о южноамериканской горе Аконкагуа.
Решения
Заголовок раздела «Решения»Задание 1
print(encoded_questions[0][:20]) # Первые 20 токенов первого вопроса.print(encoded_questions[1][:20]) # Первые 20 токенов второго вопроса.print(encoded_answers[0][:30]) # Первые 30 токенов первого ответа.print(encoded_answers[1][:30]) # Первые 30 токенов второго ответа.(В решении печатается больше токенов, чем просили в задании, — так лучше видно общий префикс.)
Задание 2
# Добавьте этот код в ячейку выше, чтобы дообучить модель.history = model.fit( x=batches, epochs=num_epochs, callbacks=[text_gen_callback])То же маскирование на PyTorch
Заголовок раздела «То же маскирование на PyTorch»Не часть оригинала: в PyTorch вместо <PAD> в целях обычно ставят -100 — это значение ignore_index
по умолчанию у F.cross_entropy, и такие позиции не попадают в потери. Так же делают TRL и Hugging Face (урок
«SFT — данные, шаблоны, маскирование»).
import torchimport torch.nn.functional as F
q = [5, 17, 42, 8] # id токенов вопроса (с шаблоном)a = [9, 33, 21, 2] # id токенов ответа (с шаблоном)tokens = q + ainputs = torch.tensor(tokens[:-1])targets = torch.tensor(([-100] * len(q) + a)[1:]) # вопрос в целях → -100print(inputs.tolist(), targets.tolist())
logits = torch.randn(len(inputs), 50) # заглушка вместо выхода моделиloss = F.cross_entropy(logits, targets) # ignore_index=-100 по умолчаниюprint(loss.item())Словарик
Заголовок раздела «Словарик»| English | Русский |
|---|---|
| base model | базовая модель |
| instruction-tuned model | модель, дообученная следовать инструкциям |
| flashcard, revision | учебная карточка, повторение |
| turn, turn-based dialogue | реплика, диалог по репликам |
| delimiter token: start / end of turn | токен-разделитель: начало / конец реплики |
| chat template | шаблон диалога |
| fine-tuning, full-parameter fine-tuning | дообучение, полное дообучение |
| pre-training | предобучение |
| input / target sequence | входная / целевая последовательность |
| loss masking | маскирование потерь |
| catastrophic forgetting | катастрофическое забывание |
| foundation model | базовая (фундаментальная) модель |