Як заякорити донавчання 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.