Как заякорить дообучение LLM на проверенном вручную наборе реальных данных
Как собрать небольшой набор реальных данных для дообучения: выбрать авторитетные источники, получить защищённые страницы и PDF в виде Markdown, оформить строки, провалидировать и отложить контрольную выборку.
Обучайте модель только на синтетике, цикл за циклом, и она понемногу уходит всё дальше от реальных знаний, которые должна была усвоить. К третьему-четвёртому переобучению она хорошо имитирует то, как звучат тексты предметной области, и плохо передаёт то, что в них сказано.
Лекарство — затравочный набор реальных данных: небольшой набор примеров, проверенных вручную и взятых из авторитетных источников до того, как начнётся синтетическое расширение. Он должен быть достаточно маленьким, чтобы вы ещё могли проверить каждую строку. Он должен быть авторитетным, потому что модель будет считать строки истиной. И часто его приходится брать оттуда, куда обычные краулеры не добираются, ведь многие самые ценные знания предметной области спрятаны за ограничениями доступа.
Дальше — как собрать такой набор, превратить его в обучающие строки, провалидировать и проверить, выдержало ли дообучение. Темы по порядку:
- почему чисто синтетический цикл дрейфует
- как выбрать правильные источники
- как получать защищённые страницы в виде чистого Markdown
- что делать с PDF и другими не-HTML-документами
- как превратить контент в пары «инструкция — ответ», пары предпочтений или сырой текст предметной области
- как провалидировать набор и измерить результат
Почему чисто синтетические циклы дрейфуют
Статистически дообучение ведёт себя иначе, чем предобучение. При предобучении шум размазан по миллиардам токенов, так что один плохой абзац растворяется в среднем. В наборе для дообучения может быть несколько сотен строк, и там одно ошибочно размеченное извлечение составляет заметную долю всего обучающего сигнала.
В типичном синтетическом пайплайне генератор пишет примеры с привычками модели-учителя. Затем судья фильтрует их по тем же самым привычкам, и ученик впитывает и привычки, и их перекосы. Без внешнего якоря каждое переобучение накладывается на предыдущее. Модель учится звучать как тексты предметной области, а не отражать их.
Реальный набор разрывает этот цикл. Он даёт генератору якорь, а судье — то, с чем можно сравнивать, помимо его собственного вывода. Дело не в размере. Для одной задачи извлечения обычно хватает нескольких сотен сильных примеров. Важно, чтобы каждая строка вела к источнику, за который вы можете поручиться, через извлечение, которое можно проверить.
Загвоздка в том, что авторитетные материалы, как правило, труднее всего достать. Они прячутся за рендерингом на JavaScript, лимитами запросов по IP и правилами доступа, которые останавливают стандартные краулеры.
Что включать в набор
Определитесь с содержимым до того, как писать код. Для модели в e-commerce это каталоги товаров и проверенные отзывы покупателей. Финансовой модели нужны отчёты для SEC, расшифровки звонков по результатам и разъяснения регуляторов. Медицинской — клинические рекомендации, инструкции к препаратам и рецензируемые исследования.
Четыре критерия подходят для любой области:
- Авторитетность: источнику доверяют практики отрасли, а не только поисковик.
- Структурированность: контент следует устойчивым шаблонам, на которые может опереться схема извлечения.
- Актуальность: он описывает область такой, какая она сегодня, а не два года назад.
- Релевантность: он напрямую связан с задачей, а не лежит где-то рядом.
Не берите SEO-контент-фермы, текст, сгенерированный ИИ, и всё, происхождение чего вы не смогли бы назвать, если бы вас спросили, откуда взялись обучающие данные.
Как получать защищённые страницы
Голый вызов requests.get() отправляет простой TLS-отпечаток и никаких браузерных заголовков, и защищённые сайты замечают это сразу. Если сайт рендерится на JavaScript, в ответе придёт пустая оболочка.
Fetch-API может взять на себя рендеринг, обход ограничений доступа и конвертацию в Markdown за один запрос. В примере mode=auto включает адаптивный стелс-режим: сервис для каждого запроса чередует рендеринг JavaScript и эскалацию через прокси и останавливается на той конфигурации, которая нужна конкретному сайту.
import os
import requests
from dotenv import load_dotenv
load_dotenv()
apikey = os.getenv("ZENROWS_API_KEY")
url = "https://www.scrapingcourse.com/ecommerce/"
params = {
"url": url,
"apikey": apikey,
"mode": "auto",
"response_type": "markdown",
}
response = requests.get("https://api.zenrows.com/v1/", params=params)
print(response.text)
Возвращаемые данные меньше и чище сырого HTML, потому что большая часть навигации, текста подвала и шаблонного мусора уже убрана. Меньше шума на входе — меньше шума, на котором учится модель.
PDF и другие не-HTML-источники
Многие лучшие источники предметной области — вовсе не веб-страницы. Инструкции к препаратам, регуляторные отчёты, клинические рекомендации и технические стандарты распространяются в PDF. Для них задайте response_type=pdf, а также js_render=true — это обязательно для ответов в PDF.
params = {
"url": url,
"apikey": apikey,
"js_render": "true",
"response_type": "pdf",
}
response = requests.get("https://api.zenrows.com/v1/", params=params)
with open("source-document.pdf", "wb") as f:
f.write(response.content)
Сервис отдаёт файл, но не извлекает из него текст. Достаньте его через pypdf или pdfplumber, а затем разбейте на чанки, прежде чем что-то из этого станет обучающим примером. Режьте по структуре, а не по фиксированному числу символов. Собственные заголовки и разделы документа — естественные границы, так что каждый чанк сохраняет контекст и ни одна мысль не обрывается на середине.
Фиксируйте происхождение
Авторитетный набор остаётся авторитетным, только если вы можете сказать, откуда взялась каждая строка. Добавьте поле метаданных с URL источника, датой получения и лицензией, и набор данных останется проверяемым и защитимым.
source = {
"url": pdf_url,
"retrieved": date.today().isoformat(),
"license": license_url,
}
Важнее всего это становится, когда кто-то ставит под сомнение ответы модели. Вам понадобится сказать, какая строка из какого источника и разрешает ли этот источник такое использование.
Три формы обучающей строки
Пример для дообучения может принимать три формы, и каждая учит своему.
Пары «инструкция — ответ» — обычный выбор для supervised fine-tuning. Каждый пример связывает Markdown, полученный со страницы, с правильным структурированным результатом, так что модель усваивает и что извлекать, и в каком формате это возвращать.
def to_training_example(instruction, markdown_content, extracted_json):
return {
"messages": [
{"role": "system", "content": instruction},
{"role": "user", "content": markdown_content},
{"role": "assistant", "content": json.dumps(extracted_json)},
]
}
Пары предпочтений нужны для DPO и RLHF. В них два ответа на один и тот же вход, и один помечен как предпочтительный. Используйте их, чтобы подтолкнуть модель, которая уже выдаёт приличные результаты, к лучшим.
def to_preference_pair(prompt, chosen, rejected):
return {
"prompt": prompt,
"chosen": chosen,
"rejected": rejected,
}
Продолжение текста предметной области — это просто текст области без разделения на вход и выход. Он знакомит модель с лексикой и стилем, но для извлечения слаб, потому что модель так и не видит, что именно нужно доставать.
Так что для задач извлечения лучше всего подходят пары «инструкция — ответ». Две другие формы служат другим целям.
Фильтрация до попадания в набор
Не каждая собранная страница заслуживает места в наборе данных. Сначала каждая строка должна пройти три проверки:
def keep(markdown, extracted):
if len(markdown.strip()) < 200:
return False
try:
validate(instance=extracted, schema=schema)
except ValidationError:
return False
h = hashlib.sha256(" ".join(markdown.lower().split()).encode()).hexdigest()
if h in seen:
return False
seen.add(h)
return True
Первая проверка отсеивает страницы, которые после рендеринга оказались почти пустыми. Вторая, проверка схемы, отклоняет неверные типы и пустые поля. Третья сравнивает хэши содержимого и отмечает точные копии и копии, отличающиеся только форматированием. Чтобы попасть в обучение, страница должна пройти все проверки.
Как понять, что сработало
До того как в дело вступит синтетика, отрежьте 10–20 процентов реального набора и никогда на них не обучайте. Когда дообучение закончится, прогоните модель по этой отложенной выборке и посчитайте ответы, совпавшие с ожидаемыми.
random.seed(42)
random.shuffle(seed_examples)
split = int(len(seed_examples) * 0.85)
train_examples = seed_examples[:split]
holdout_examples = seed_examples[split:]
Повторяйте проверку после каждого переобучения и записывайте результат во времени. Стабильная или растущая цифра говорит, что набор работает. Падающая — ранний сигнал дрейфа, заметный до того, как он начнёт накапливаться.
Что стоит запомнить
- Набор из нескольких сотен качественных реальных примеров ценнее большого синтетического набора без такой основы.
- Авторитетные источники почти всегда спрятаны за ограничениями доступа, через которые стандартные краулеры не проходят.
- Адаптивный режим справляется с рендерингом JavaScript и детекцией ботов для каждого запроса, без настройки под каждый сайт.
- Режьте PDF на чанки по структуре, а не по числу символов, чтобы каждый кусок оставался связным.
- Проверяйте каждую строку на входе, ведь одно плохое извлечение весит много в маленьком обучающем наборе.
- Отложите часть реальных данных до любого синтетического расширения, а потом оценивайте на ней дообученную модель.
Для сбора в большем масштабе естественный следующий шаг — пакетный эндпоинт, который сам управляет параллельностью и повторами на длинных списках URL.