Перейти к содержимому
ПереводыGoogle и DeepMind

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.

ЛабораторнаяВремяКогда делатьЧто нужно
5.1. Задайте SLM вопросы15 минВместе с уроком «От базовой модели к ассистенту»Keras на JAX, желательно GPU
5.2. Формат текста для диалога по репликам15 минВместе с уроком «SFT — данные, шаблоны, маскирование»pandas, CPU
5.3. Дообучите все параметры своей SLM30 мин + дообучениеПосле урока «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. Полезно увидеть эти шаги без «магии» библиотек.


Ноутбук: 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."
)

Теперь, когда обученная модель загружена, можно проверить, как она себя ведёт.

# @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>user
What is Jollof rice?<end_of_turn>
<start_of_turn>model
Jollof 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.

(«Категория: еда. Джолоф — популярное и знаковое блюдо из риса, которое готовят в одном котле; основная еда во многих странах Западной Африки».)

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

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/>в формате карточки"]
Конвейер полного дообучения в лабораторной 5.3

К концу лабораторной вы сможете:

  • назвать, чем дообучение похоже на обучение с нуля;
  • понять сходства и различия между общей и более конкретной задачей — предсказанием следующего токена и ответом на вопрос в виде карточки;
  • подготовить данные для дообучения;
  • дообучить существующую модель.

В этой лабораторной вы:

  • загрузите небольшой набор данных для новой задачи;
  • отформатируете данные так, чтобы они подходили обученной модели;
  • загрузите обученную модель;
  • дообучите её на новых данных для задачи карточек;
  • оцените модель.

Если есть возможность, мы настоятельно советуем запускать лабораторную на экземпляре 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, HTML
from ai_foundations import training # Чтобы обучать модель.
from ai_foundations import generation # Чтобы давать модели промпты.
# Чтобы загрузить токенизатор.
from ai_foundations.tokenization.bpe_tokenizer import BPEWordTokenizer
from 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) токенов. Целевые токены — это входные, сдвинутые на одну позицию влево. При обучении модель учится предсказывать целевой токен в каждой позиции ii по всем входным токенам до позиции ii включительно. Параметры обновляются по потерям на каждом целевом токене, чтобы на следующей итерации обучения модель лучше предсказывала каждый токен обучающих данных.

При дообучении на задаче вроде генерации карточек всё это по-прежнему верно. Во-первых, данные тоже нужно подготовить как последовательность входных и целевых токенов. Поэтому для задач «вопрос — ответ», как генерация карточек, пару «вопрос — ответ» нужно превратить в одну последовательность токенов. Для этого вопрос и ответ склеивают:

<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_id
for 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,
)
# Добавьте свой код здесь.

В первой лабораторной курса вы проверяли, как обученная модель отвечает на вопросы. Теперь, когда модель дообучена, повторите это упражнение.

# @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 = question
num_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 вместо <PAD> в целях обычно ставят -100 — это значение ignore_index по умолчанию у F.cross_entropy, и такие позиции не попадают в потери. Так же делают TRL и Hugging Face (урок «SFT — данные, шаблоны, маскирование»).

import torch
import torch.nn.functional as F
q = [5, 17, 42, 8] # id токенов вопроса (с шаблоном)
a = [9, 33, 21, 2] # id токенов ответа (с шаблоном)
tokens = q + a
inputs = torch.tensor(tokens[:-1])
targets = torch.tensor(([-100] * len(q) + a)[1:]) # вопрос в целях → -100
print(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базовая (фундаментальная) модель