Od przewidywania następnego tokena do wykonywania instrukcji: drobna optymalizacja modelu GPT
Jak z góry przeszkolony GPT uczy się wykonywać polecenia: formatowanie rekordów, zmiana etykiet, maski wypełniania, pętla treningowa, ograniczenia oceny oraz krok w kierunku preferencji DPO.
Źródła: From Next-Token Prediction to ChatGPT <> Build an LLM From Scratch [6]
Wstępne szkolenie już daje modelowi językowemu umiejętność pisania. Brakuje mu jednak zrozumienia tego, czego chce osiągnąć użytkownik. Dostosowanie modelu na podstawie instrukcji to uzupełnia. Podaj dwa modele temu samemu zapytaniu, na przykład wyjaśnienie recyrkulacji dziesięcioletniemu dziecku – różnica jest oczywista. Model podstawowy ma tendencję do powtarzania zapytania i wprowadzania nudnych, typowo podręcznikowych wyjaśnień na temat kursów informatyki. W ogóle nie odpowiada, a jedynie rozszerza tekst. Model dostosowany natomiast przedstawia obrazek: pudełko zawierające mniejsze pudełko, a w nim jeszcze mniejsze pudełko, przy czym recyrkulacja jest opisywana jako problem, który ciągle przekazuje sobie mniejszą wersję, aż ta najmniejsza staje się łatwa do rozwiązania.
Poza tym oba modele robią to samo – przewidują następny token – jednak zachowują się zupełnie inaczej. Produkty do czatowania sprawiają, że przestrzeganie instrukcji wydaje się naturalne, co ukrywa tę różnicę. Na pytania znajdują się odpowiedzi, prośby o kod generują kod, a streszczenia są pisane. Nic z tego nie przychodzi bezpłatnie dzięki lekturze ogromnych ilości tekstu. Wstępne szkolenie wpaja coś bardziej podstawowego: biorąc pod uwagę wszystko, co dotychczas wiadomo, który token jest najprawdopodobniejszy dalej? Nic w tym nie mówi o tym, że prośba użytkownika musi zostać spełniona. Tematem tego tekstu jest właśnie przezwyciężenie tej luki.
Akta 1: wstępne szkolenie uczy uzupełniania, a nie posłuszeństwa
Zadanie wstępnego szkolenia to po prostu przewidywanie następnego tokena. Samo to jest wystarczająco skuteczne, by opanować gramatykę, fakty, składnię kodu, style pisania oraz niektóre zaskakująco złożone wzorce, ale przestrzeganie instrukcji nie należy do nich. Podaj modelowi bazowemu następujący prompt:
Explain why the sky appears blue.
i może kontynuować się w następujący sposób:
Explain why the sky appears blue.
The following questions are commonly asked in introductory physics...
To niekoniecznie oznacza uszkodzony model. Robi dokładnie to, czego wymagało szkolenie wstępne, czyli kontynuuje tekst. Pożądane zachowanie wygląda bardziej w ten sposób:
The sky appears blue because Earth's atmosphere scatters
shorter wavelengths of sunlight more strongly than longer
wavelengths...
Podstawowy model jest taki sam, ale oczekiwane zachowanie się różni. Pytanie zmienia się w następujący sposób:
What text probably comes next?
do tego:
When the text looks like an instruction,
what kind of continuation should I produce?
Nauczanie modelu odpowiadania na to drugie pytanie jest właśnie celem drobnej optymalizacji pod kątem instrukcji.
Act 2: formatowanie danych instrukcji
Najprostszym sposobem na nauczenie modelu przestrzegania instrukcji jest pokazanie mu wielu instrukcji w połączeniu z dobrymi odpowiedziami – to naprawdę tak proste. Zapis treningowy może wyglądać w ten sposób:
example = {
"instruction": "Convert the following sentence to passive voice.",
"input": "The developer fixed the bug.",
"output": "The bug was fixed by the developer."
}
Transformator nie może przetwarzać słowników Pythona, więc zapis jest najpierw serializowany do zwykłego tekstu, mniej więcej w ten sposób:
Below is an instruction that describes a task.
### Instruction:
Convert the following sentence to passive voice.
### Input:
The developer fixed the bug.
### Response:
The bug was fixed by the developer.
Formatowanie ma znaczenie, ponieważ model wielokrotnie widzi ten sam wzorzec:
instruction
↓
optional context
↓
response
Krótko mówiąc, każdy zapis zostaje przekształcony w tekst sformatowany, następnie w tokeny, a potem w uzupełnione grupy, które dokonują drobnej optymalizacji modelu GPT wstępnie nauczonym.
Widok tokenów
Po tokenizacji przykład zamienia się w listę identyfikatorów, mniej więcej w takim wyglądzie:
[21106, 318, 281, 12064, ... 4435, 257, 3126, ...]
Te identyfikatory tokenów tworzą sekwencję treningową. W przypadku modelu autoregresyjnego etykiety to po prostu sekwencja przesunięta o jedną pozycję:
Tokens:
[A, B, C, D, E]
Input:
[A, B, C, D]
Labels:
[B, C, D, E]
Zatem dostosowywanie instrukcji nie wprowadza żadnego zupełnie nowego celu uczenia się. Model nadal przewiduje następny token. To, co się zmienia, to rozkład tekstu, który ma przewidzieć. Zamiast głównie zwykłych dokumentów, teraz wielokrotnie widzi to samo mapowanie:
instruction → useful response
To jest główna idea.
Dodatki wymagają specjalnego traktowania
Przykłady treningowe różnią się długością, ale zbiory wymagają tensorów o kompatybilnych kształtach. Weźmy dwa przykłady:
Example 1:
[10, 20, 30, 40]
Example 2:
[11, 21]
Można je wypełnić:
Example 1:
[10, 20, 30, 40]
Example 2:
[11, 21, PAD, PAD]
Tokeny wypełniające nie są znaczącymi celami. Model nie powinien otrzymywać nagrody za ich naukę:
PAD → PAD → PAD → PAD
Zatem te pozycje są zazwyczaj zastępowane wartością ignorowania:
ignore_index = -100
Strata cross-entropy w PyTorch pomija je. Koncepcyjnie:
loss = cross_entropy(
logits.reshape(-1, vocab_size),
targets.reshape(-1),
ignore_index=-100,
)
Detale są ważne. Bez maskowania część sygnału treningowego jest marnowana na artefakty powstające w wyniku grupowania danych, zamiast na sam język.
Akt 3: doprecyzowanie zachowania
Sam cykl treningowy nie jest tu niczym egzotycznym, ponieważ optymalizacja modelu następnego tokena jest już dobrze znana. Ważne jest to, jakie przykłady trafiają do tego cyklu. W uproszczeniu:
for batch in train_loader:
optimizer.zero_grad()
input_ids = batch[:, :-1]
targets = batch[:, 1:]
logits = model(input_ids)
loss = cross_entropy(
logits.flatten(0, 1),
targets.flatten(),
ignore_index=-100,
)
loss.backward()
optimizer.step()
W tym kodzie nie ma nic, co brzmiałoby w ten sposób:
model.learn_to_be_helpful()
Zachowanie pochodzi z danych. Załóżmy, że tysiące przykładów wygląda w ten sposób:
### Instruction:
Summarize this paragraph.
### Response:
<clear concise summary>
Aktualizacje gradientowe nieustannie skłaniają model do generowania odpowiedzi o takiej strukturze po zadaniach o podobnym kształcie. Załóżmy, że inne przykłady wyglądają w ten sposób:
### Instruction:
Write Python code that reverses a string.
### Response:
def reverse_string(s):
return s[::-1]
Uczy się kolejnego powiązania pomiędzy wzorcem instrukcji a odpowiednią kontynuacją. Patrząc na poziomie tokenów, drobna kalibracja instrukcji nie jest tak tajemnicza – kontekst wokół przewidywań został celowo zmieniony. Wcześniej:
The CPU cache is...
Po sformatowaniu instrukcji:
### Instruction:
Explain CPU caching to a beginner.
### Response:
A CPU cache is...
Nie wprowadzono żadnego celu w rodzaju „zrozumienie użytkowników”. Zamiast tego zmieniono rozkład tekstu treningowego tak, aby pomocne odpowiedzi stawały się statystycznie najbardziej prawdopodobnymi kontynuacjami po otrzymaniu instrukcji. To właśnie ta zmiana powoduje nowe zachowanie. W przykładzie pokazanym na żywo model GPT-2-medium, który został wcześniej przeszkolony, przechodzi fine-tuning pod kątem instrukcji, generuje odpowiedzi i zapisuje otrzymany model.
Akt 4: ocena staje się kłopotliwa
Klasyfikacja oferowała prostą metodę oceny. W przypadku wykrywania spamu:
Prediction: SPAM
Label: SPAM
Correct.
Przestrzeganie instrukcji nie oferuje takiego luksusu. Rozważmy prośbę o proste wyjaśnienie rekurencji. Jedna z możliwych odpowiedzi:
Recursion is when a function solves a problem by calling
itself on a smaller version of the same problem.
Kolejny:
Recursion is a technique where a problem is reduced into
smaller instances of itself until a stopping condition is reached.
Oba mogą być poprawne, a żadne z nich nie musi odpowiadać słowu referencyjnemu dosłownie. Dlatego dokładność dopasowania dosłownego jest w większości przypadków bezużyteczna przy odpowiedziach otwartych.
Porównywanie z referencją
Jedną z opcji jest dostarczenie referencji:
{
"instruction": "Explain recursion simply.",
"reference": "Recursion solves a problem by repeatedly reducing it...",
"model_response": "A recursive function calls itself..."
}
i porównać wygenerowaną odpowiedź z nią. Jednak podobieństwo to nie jest jakość – model może podać doskonałą odpowiedź używając zupełnie innych słów. To prowadzi do innego podejścia.
Poprosić inny LLM o ocenę
Osoba oceniająca otrzymuje instrukcję, odpowiedź referencyjną oraz odpowiedź do oceny:
Instruction:
Explain recursion simply.
Reference answer:
...
Model answer:
...
Rate the model answer from 0 to 100 based on correctness,
relevance, and clarity.
Sądowy sędzia lokalny może korzystać z Ollamy, co jest wygodne, ale stwarza nowy problem: sam sędzia jest modelem. Jego ocena może zależeć od pytania oceniającego, wyboru osoby oceniającej, kolejności odpowiedzi, stopnia szczegółowości, upodobań stylistycznych oraz od błędów samego sędziego. Niektóre przykłady pokazują, że lokalny sędzia przyznaje zaskakująco wysokie oceny słabym odpowiedziom modelu bazowego, co przypomina, że liczby uzyskiwane przy użyciu LLM jako sędziego nie stanowią prawdy obiektywnej.
Niewielki projekt może składać się z trzech warstw:
1. Inspect representative outputs manually
2. Compare against reference answers where useful
3. Use an LLM judge for aggregate comparison
Żaden z nich sam w sobie nie jest doskonały. Razem dają jaśniejszy obraz niż pojedyncza liczba przedstawiana jako prawda.
Akcja 5: trudniejszy problem – preferencje
Dokładna kalibracja instrukcji pokazuje modelowi, jak powinna wyglądać rozsądna odpowiedź. Teraz porównajmy dwie odpowiedzi. Odpowiedź A:
Recursion is when a function calls itself.
Odpowiedź B:
Recursion is a technique where a function solves a problem
by calling itself on a smaller version of that problem.
The process stops when it reaches a base case.
Oba są ważne, ale może być preferowany B. Sygnał treningowy nie jest już taki:
correct vs incorrect
To jest to:
chosen vs rejected
Zapis preferencji może wyglądać w ten sposób:
{
"prompt": "Explain recursion simply.",
"chosen": """
Recursion solves a problem by repeatedly reducing it
until reaching a base case.
""",
"rejected": """
Recursion is when recursion happens recursively.
"""
}
To inny rodzaj nadzoru. Zamiast mówić „oto odpowiedź do naśladowania”, mówi się „biorąc pod uwagę te dwie odpowiedzi, wybierz tę”. W tym miejscu wchodzą takie techniki jak Direct Preference Optimization (DPO). Na wysokim poziomie:
Prompt
↓
Chosen response ─────┐
├── preference objective
Rejected response ───┘
↓
model update
DPO zasługuje na odrębne potraktowanie, więc jego mechanizmy nie wchodzą w skład tej dyskusji. Przydatnym odnośnikiem jest ogólny przebieg rozwoju:
Pretraining
↓
learn language patterns
------------------------------
Instruction fine-tuning
↓
learn instruction → response behavior
------------------------------
Preference tuning
↓
learn which acceptable responses we prefer
Tworzenie zbiorów danych preferencji za pomocą Llama 3.1 przez Ollama oraz wdrażanie DPO od zera to tematy na później. Istnieje jedna ważna uwaga: fakt, że DPO jest dostępny jako materiał dodatkowy, nie oznacza, że jest to odpowiedni następny krok dla każdego modelu czy zbioru danych. Niektóre eksperymenty pokazują ograniczenia jego efektywności, co jest dokładnie tym sceptycyzmem, który należy zachować podczas dostosowywania preferencji.
Co tak naprawdę się zmieniło
Punktem wyjścia był model wstępnie nauczony, który potrafił kontynuować tekst. Architektura transformera nie została zastąpiona specjalną architekturą asystenta. Przykłady związane z problemem następnego tokena uległy zmianie, co doprowadziło do zupełnie innego założenia behawioralnego. Gdy model teraz widzi:
### Instruction:
Zdarzyło się już wiele przypadków, gdy kontynuacją nie był kolejny akapit z Internetu, lecz odpowiedź. Patrząc w ten sposób, rozumienie toku jest łatwiejsze:
Pretraining:
What token should come next?
Instruction tuning:
What does a good answer look like after an instruction?
Preference tuning:
Of several reasonable answers, which behavior should we prefer?
Architektura, optymalizator oraz szybkość uczenia się mają znaczenie, ale przy dostosowywaniu do instrukcji dane odgrywają szczególną rolę, ponieważ określają zachowanie, które należy naśladować. Gdy istnieje kilka akceptowalnych odpowiedzi, prawidłowy wynik przestaje być jedynym celem. Pytanie zmienia się na to, która z akceptowalnych odpowiedzi zasługuje na większą wagę, i to właśnie jest brama, którą otwiera DPO.
Wstępne szkolenie uczy, jaki tekst zazwyczaj pojawia się dalej. Dostosowywanie do instrukcji zmienia to, które kontynuacje uznawane są za przydatne odpowiedzi na polecenia. Dostosowywanie według preferencji robi kolejny krok i decyduje, które z tych przydatnych odpowiedzi należy preferować.
Literatura pokrewna
- Szkolenie małego GPT na indyjskich opowieściach ludowych: tokenizer, maska i strata — GPT o 9 milionów parametrów stworzony od zera: dlaczego tokenizer GPT-2 zawodzi przy słowach z opowieści ludowych, jak działają BPE i maska przyczynowa oraz jak jedna partia danych daje 4 096 strat dla następnego tokena.
- Wewnątrz LLM: inferycja w czystym Pythonie dla Qwen3-0.6B na MLX — Krok po kroku budowa mechanizmu inferycji LLM przy użyciu około 200 linijek kodu w Pythonie z MLX: tokenizer, RMSNorm, RoPE, grupowana uwaga do zapytań, SwiGLU, bufor KV i próbkowanie.