Предсказване на следващия токен
Накратко: Езиковият модел пише текст, като смята вероятностите на следващия токен и избира един; температурата и top-k решават колко случаен е изборът.
Проблем
Едно начало, три отговора
Ботът на поддръжката трябва да дописва отговори. Колега пуска един и същ начален текст, „поръчката“, три пъти, с различно семе на генератора. Ето трите отговора:
var table = countNext(TEXT);for (int seed : new int[] {2026, 34567, 12345}) System.out.println("семе " + seed + ": " + generate(table, "поръчката", 1, 100, new Rng(seed)));семе 2026: поръчката ще пристигне до три днисеме 34567: поръчката пристигна в офиса на куриерсеме 12345: поръчката ще ви се връщат по картата ви утреПървият звучи разумно. Вторият е почти изречение. Третият е безсмислица, макар че всяка две съседни думи са от текста на магазина. И трите са от една и съща програма, а програмата не търси отговор никъде: тя пише дума по дума.
Откъде знае коя дума да сложи? И защо всеки път е различно?
Идеята
Вероятността на следващата дума
Езиковият модел получава текст и дава вероятност за всеки възможен следващ токен. Нищо повече: не знае какво е поръчка, знае само какво обикновено идва след какво.
Текстът се пише с цикъл:
- Моделът дава вероятностите на следващия токен.
- Един токен се избира според тях.
- Добавя се към текста и всичко се повтаря, докато дойде краят или лимитът.
Изборът е отделна стъпка, а не част от модела. Затова един и същ модел може да дава различни отговори.
Моделът в урока е най-малкият възможен: таблица колко пъти коя дума е идвала точно след коя. Гледа само последната дума и брои цели думи, а не токени. Истинските модели са невронни мрежи, които гледат целия текст, но дават същото: вероятностите на следващия токен.
Виж го
Таблицата: коя дума след коя
Текстът на магазина е същият като в урока за токените. За всяко изречение броим всяка двойка съседни думи. Точката „.“ отбелязва края на изречението, за да знае моделът кога да спре.
// една възможна следваща дума и вероятността ѝrecord Choice(String word, double p) {}
// колко пъти коя дума идва точно след коя; „.“ е краят на изречениетоstatic Map<String, Map<String, Integer>> countNext(String[] text) { Map<String, Map<String, Integer>> table = new LinkedHashMap<>(); for (String line : text) { String[] w = (line + " .").split(" "); for (int i = 0; i + 1 < w.length; i++) table.computeIfAbsent(w[i], key -> new LinkedHashMap<>()) .merge(w[i + 1], 1, Integer::sum); } return table;}Вероятността на следващата дума е броят ѝ, делен на сбора. Засега гледай само t = 1 и „всички думи“: тогава теглото е самият брой. Температурата t и k идват по-долу.
// k-те най-чести думи, тегло брой^(1/t) / сбораstatic List<Choice> nextWords(Map<String, Integer> counts, double t, int k) { List<String> words = new ArrayList<>(counts.keySet()); words.sort((a, b) -> counts.get(b) - counts.get(a)); List<String> top = words.subList(0, Math.min(k, words.size())); double total = 0; for (String w : top) total += Math.pow(counts.get(w), 1 / t); List<Choice> choices = new ArrayList<>(); for (String w : top) { double weight = Math.pow(counts.get(w), 1 / t); choices.add(new Choice(w, weight / total)); } return choices;}var table = countNext(TEXT);Map<String, Integer> counts = table.get("ще");System.out.println("след \"ще\":");for (Choice c : nextWords(counts, 1, 100)) System.out.printf("%-11s%2d %5.1f%%%n", c.word(), counts.get(c.word()), c.p() * 100);след "ще":пристигне 4 40.0%бъде 2 20.0%дойде 1 10.0%ви 1 10.0%бъдат 1 10.0%получите 1 10.0%След „ще“ в текста идват шест различни думи, общо 10 пъти. „пристигне“ е 4 от тях: 40,0%. Ето това е езиковият модел: за „ще“ той казва „пристигне“ с 40,0%, „бъде“ с 20,0% и всяка от другите с 10,0%.
Виж го
Изборът: число от 0 до 1
Как се избира дума с вероятност 40,0%? Генераторът със семе дава число r от 0 до 1. Лентите на думите застават една до друга: „пристигне“ от 0 до 0,4, „бъде“ от 0,4 до 0,6 и така нататък. Думата, в чиято лента пада r, е избраната.
class Rng { private long state;
Rng(long seed) { state = seed; }
// следващото число между 0 и 1; едно семе дава едни и същи числа double next() { state = state * 48271 % 2147483647; return (double) state / 2147483647; }}// число от 0 до 1 пада в една от лентитеstatic String pick(List<Choice> choices, Rng rng) { double r = rng.next(); double sum = 0; for (Choice c : choices) { sum += c.p(); if (r < sum) return c.word(); } return choices.get(choices.size() - 1).word();}Генерирането повтаря това дума по дума от началото, докато дойде „.“ или 12 думи:
// продължи текста дума по дума до „.“ или до 12 думиstatic String generate(Map<String, Map<String, Integer>> table, String start, double t, int k, Rng rng) { List<String> text = new ArrayList<>(List.of(start.split(" "))); String word = text.get(text.size() - 1); while (text.size() < 12) { word = pick(nextWords(table.get(word), t, k), rng); if (word.equals(".")) break; text.add(word); } return String.join(" ", text);}Ако винаги вземаш най-вероятната дума (k = 1), генераторът няма какво да избира и отговорът е един и същ при всяко семе:
var table = countNext(TEXT);for (int seed : new int[] {2026, 34567, 12345}) System.out.println("семе " + seed + ": " + generate(table, "поръчката", 1, 1, new Rng(seed)));семе 2026: поръчката ще пристигне до два днисеме 34567: поръчката ще пристигне до два днисеме 12345: поръчката ще пристигне до два дниСъс случаен избор отговорът зависи от семето. Едно и също семе обаче дава едни и същи числа, значи и един и същ отговор:
var table = countNext(TEXT);for (int seed : new int[] {2026, 2026, 34567}) System.out.println("семе " + seed + ": " + generate(table, "поръчката", 1, 100, new Rng(seed)));семе 2026: поръчката ще пристигне до три днисеме 2026: поръчката ще пристигне до три днисеме 34567: поръчката пристигна в офиса на куриерЗатова в началото трите отговора бяха различни: всяко семе дава други числа, а оттам други думи.
Пробвай
Предвиди изхода
След „доставката“ в текста има пет продължения, едното е краят „.“. Какво печата програмата с k = 1?
var table = countNext(TEXT);for (Choice c : nextWords(table.get("доставката"), 1, 1)) System.out.printf("%s %.1f%%%n", c.word(), c.p() * 100);k = 1 оставя само най-честата дума, „до“. Сборът на теглата вече е само нейното тегло, а теглото, делено на себе си, е 1: 100,0%. 33,3% е вероятността ѝ сред всички думи, но тях k = 1 ги е изрязало.
Идеята
Температура и top-k
Изборът има две ръчки, които имат и истинските модели:
- температура
t: теглото е брой^(1/t). Приtпод 1 честите думи стават още по-вероятни и отговорът е по-предвидим. Приtнад 1 вероятностите се изравняват и редките думи излизат по-често. - top-k: остават само
k-те най-чести думи, а другите не могат да се изберат. Сборът се смята само по тях, затова пак е 100%.
var table = countNext(TEXT);Map<String, Integer> counts = table.get("ще");double[] temps = {0.5, 1.0, 2.0};List<List<Choice>> rows = new ArrayList<>();StringBuilder head = new StringBuilder(" ".repeat(11));for (double t : temps) { rows.add(nextWords(counts, t, 100)); head.append(String.format("%7s", "t=" + (t == (int) t ? String.valueOf((int) t) : String.valueOf(t))));}System.out.println(head);for (int i = 0; i < counts.size(); i++) { StringBuilder line = new StringBuilder(String.format("%-11s", rows.get(0).get(i).word())); for (List<Choice> row : rows) line.append(String.format("%6.1f%%", row.get(i).p() * 100)); System.out.println(line);} t=0.5 t=1 t=2пристигне 66.7% 40.0% 27.0%бъде 16.7% 20.0% 19.1%дойде 4.2% 10.0% 13.5%ви 4.2% 10.0% 13.5%бъдат 4.2% 10.0% 13.5%получите 4.2% 10.0% 13.5%При t = 0,5 „пристигне“ е 66,7%, а при t = 2 само 27,0%. Думите, видени веднъж, са 4,2%, 10,0% и 13,5%: колкото по-висока е температурата, толкова по-често моделът ги избира.
Ето лентите след „ще“ при трите температури, после с k = 3 и изборът с едно число от генератора:
// k-те най-чести думи, тегло брой^(1/t) / сбораstatic List<Choice> nextWords(Map<String, Integer> counts, double t, int k) { List<String> words = new ArrayList<>(counts.keySet()); words.sort((a, b) -> counts.get(b) - counts.get(a)); List<String> top = words.subList(0, Math.min(k, words.size())); double total = 0; for (String w : top) total += Math.pow(counts.get(w), 1 / t); List<Choice> choices = new ArrayList<>(); for (String w : top) { double weight = Math.pow(counts.get(w), 1 / t); choices.add(new Choice(w, weight / total)); } return choices;}
// число от 0 до 1 пада в една от лентитеstatic String pick(List<Choice> choices, Rng rng) { double r = rng.next(); double sum = 0; for (Choice c : choices) { sum += c.p(); if (r < sum) return c.word(); } return choices.get(choices.size() - 1).word();}Стъпка по стъпка, с думи
- След „ще“ в текста идват 6 думи. Лентата е броят, делен на всички 10: „пристигне“ е 4 от 10, 40,0%.
- t = 0,5: теглото е брой². Честата дума става още по-вероятна: „пристигне“ 66,7%.
- t = 2: теглото е √брой. Лентите се изравняват: „пристигне“ 27,0%, „получите“ 13,5%.
- t = 1, k = 3: остават само трите най-чести и сборът им пак е 100%: 57,1%, 28,6%, 14,3%.
- Трите ленти застават една до друга от 0 до 1. Генераторът със семе 31337 дава 0,7044.
- 0,7044 пада в лентата на „бъде“ (от 57,1% до 85,7%): следващата дума е „бъде“, не най-честата.
var table = countNext(TEXT);List<Choice> choices = nextWords(table.get("ще"), 1, 3);System.out.println("k=3:");for (Choice c : choices) System.out.printf("%-11s%5.1f%%%n", c.word(), c.p() * 100);Rng rng = new Rng(31337);Rng copy = new Rng(31337);System.out.printf("число от генератора: %.4f%n", copy.next());System.out.println("избрана: " + pick(choices, rng));k=3:пристигне 57.1%бъде 28.6%дойде 14.3%число от генератора: 0.7044избрана: бъдеС k = 3 остават „пристигне“, „бъде“ и „дойде“, а вероятностите им са 4, 2 и 1 от 7. Генераторът със семе 31337 дава 0,7044, а то пада в лентата на „бъде“, от 57,1% до 85,7%. Изборът е по вероятностите, но не е винаги най-вероятната дума.
Пробвай
Попълни
Кое число стои на мястото на ___ в теглото на думата?
for (String w : top)
total += Math.pow(counts.get(w), 1 / );Температурата t: теглото е брой^(1/t). При t = 1 това е самият брой, при t = 0,5 е брой², а при t = 2 е корен от броя.
Пробвай
Намери грешката
Колега иска top-k с трите най-вероятни думи, но моделът му пише само редки думи. На кой ред е грешката?
Избери реда с грешката.
Подреждането е от най-рядката към най-честата, затова първите k са трите най-редки думи. Трябва да е обратното: counts.get(b) - counts.get(a).
Идеята
Кога боли
- Вероятното не е вярно. Моделът пише „до три дни“ толкова лесно, колкото „до два дни“: и двете са в текста. Той не знае кога пристига точно тази поръчка. Гладкият текст звучи уверено и когато е грешен. За това е урокът за границите на модела.
- Всяка двойка е вярна, изречението не е. „поръчката ще ви се връщат по картата ви утре“ е от съседни думи, които съществуват. Нашият модел гледа само една дума назад. Истинските гледат целия текст и пишат много по-добре, но пак могат да слепят нещо гладко и безсмислено.
- Висока температура, повече грешки. Редките продължения излизат по-често. За отговори на клиенти, където трябва точност, температурата е ниска. За идеи и варианти може да е по-висока.
- Едно семе не е обещание. В урока едно семе дава един отговор. При истински модел през API една и съща заявка понякога дава различен отговор и при едни и същи настройки, затова не разчитай на точно същия текст, а проверявай отговора.
По-точно, за любопитните
Softmax и температурата. Истинският модел е невронна мрежа: за всеки токен от речника тя дава число (logit), а softmax ги прави вероятности. Температурата дели числата на t преди softmax. Теглото брой^(1/t) от урока е точно това: softmax от log(брой) / t.
top-p. Освен top-k има и top-p: остават най-честите токени, докато сборът им стигне p (например 0,9). Така при уверен модел остават малко токени, а при неуверен повече.
Токени, не думи. Истинският модел избира токени от речника си, а думата се получава от няколко избора поред. Температурата, top-k и семето се задават в заявката към модела (урокът за извикването).
Колко назад гледа. Нашият модел помни една дума. Истинският вижда целия разговор до лимита си, и това е прозорецът на контекста.
Упражнение
Колко вероятно е изречението
Реши в редактора си.
Вероятността на цяло изречение е произведението на вероятностите на всяка следваща дума, включително „.“ накрая. Напиши функция, която я смята с таблицата от урока, и я отпечатай в проценти за три изречения. Ако двойка думи никога не е виждана, вероятността ѝ е 0.
пратката ще пристигне до два дни: 2.86%пратката ще пристигне до 14 дни: 1.43%пратката пристигна утре: 0.00%Първото изречение е 2,86%, второто 1,43%, макар че и двете не са в текста буква по буква: всички двойки в тях са виждани. Третото е 0,00%: „пратката“ и „пристигна“ са в текста, но никога една след друга. Така моделът казва колко „естествен“ му изглежда текстът.
Още задачи
- Най-вероятното изречение. Започни от „пратката“ и всеки път вземи най-вероятната дума. Каква е вероятността на изречението, което се получава?
- Две думи назад. Брой коя дума идва след двойка думи, не след една. Как се променят отговорите от началото на урока?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: record, switch и шаблони · Какво е машинно учене · Цифрите от касовата бележка · Текстът на парчета: токени
- Продължава в: Ембединги: смисълът като вектори · Прозорецът на контекста · Границите на модела · Извикване на модел през API
- Задачи по темата: Как работи езиков модел