Головна / Статті / Як заякорити донавчання LLM на перевіреному вручну наборі реальних даних

Як заякорити донавчання LLM на перевіреному вручну наборі реальних даних

Як зібрати невеликий набір реальних даних для донавчання: обрати авторитетні джерела, отримати захищені сторінки й PDF у вигляді Markdown, оформити рядки, провалідувати й відкласти контрольну вибірку.

1504 слів

Навчайте модель лише на синтетиці, цикл за циклом, і вона потроху відходить усе далі від реальних знань, які мала засвоїти. На третьому-четвертому перенавчанні вона добре імітує те, як звучать тексти предметної галузі, і погано передає те, що в них сказано.

Ліки — затравковий набір реальних даних: невеликий набір прикладів, перевірених вручну й узятих з авторитетних джерел до того, як почнеться синтетичне розширення. Він має бути досить малим, щоб ви ще могли перевірити кожен рядок. Він має бути авторитетним, бо модель вважатиме рядки істиною. І часто його доводиться брати звідти, куди звичайні краулери не дістаються, адже багато найцінніших знань предметної галузі сховано за обмеженнями доступу.

Далі — як зібрати такий набір, перетворити його на навчальні рядки, провалідувати й перевірити, чи витримало донавчання. Теми по черзі:

  • чому суто синтетичний цикл дрейфує
  • як обрати правильні джерела
  • як отримувати захищені сторінки у вигляді чистого 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.