Предсказване на следващия токен
Накратко: Езиковият модел пише текст, като смята вероятностите на следващия токен и избира един; температурата и top-k решават колко случаен е изборът.
Проблем
Едно начало, три отговора
Ботът на поддръжката трябва да дописва отговори. Колега пуска един и същ начален текст, „поръчката“, три пъти, с различно семе на генератора. Ето трите отговора:
const table = countNext(TEXT);for (const seed of [2026, 34567, 12345]) console.log(`семе ${seed}: ${generate(table, "поръчката", 1, 100, new Rng(seed))}`);семе 2026: поръчката ще пристигне до три днисеме 34567: поръчката пристигна в офиса на куриерсеме 12345: поръчката ще ви се връщат по картата ви утреПървият звучи разумно. Вторият е почти изречение. Третият е безсмислица, макар че всяка две съседни думи са от текста на магазина. И трите са от една и съща програма, а програмата не търси отговор никъде: тя пише дума по дума.
Откъде знае коя дума да сложи? И защо всеки път е различно?
Идеята
Вероятността на следващата дума
Езиковият модел получава текст и дава вероятност за всеки възможен следващ токен. Нищо повече: не знае какво е поръчка, знае само какво обикновено идва след какво.
Текстът се пише с цикъл:
- Моделът дава вероятностите на следващия токен.
- Един токен се избира според тях.
- Добавя се към текста и всичко се повтаря, докато дойде краят или лимитът.
Изборът е отделна стъпка, а не част от модела. Затова един и същ модел може да дава различни отговори.
Моделът в урока е най-малкият възможен: таблица колко пъти коя дума е идвала точно след коя. Гледа само последната дума и брои цели думи, а не токени. Истинските модели са невронни мрежи, които гледат целия текст, но дават същото: вероятностите на следващия токен.
Виж го
Таблицата: коя дума след коя
Текстът на магазина е същият като в урока за токените. За всяко изречение броим всяка двойка съседни думи. Точката „.“ отбелязва края на изречението, за да знае моделът кога да спре.
// колко пъти коя дума идва точно след коя; „.“ е краят на изречениетоfunction countNext(text) { const table = new Map(); for (const line of text) { const w = `${line} .`.split(" "); for (let i = 0; i + 1 < w.length; i++) { if (!table.has(w[i])) table.set(w[i], new Map()); const counts = table.get(w[i]); counts.set(w[i + 1], (counts.get(w[i + 1]) ?? 0) + 1); } } return table;}Вероятността на следващата дума е броят ѝ, делен на сбора. Засега гледай само t = 1 и „всички думи“: тогава теглото е самият брой. Температурата t и k идват по-долу.
// k-те най-чести думи, тегло брой^(1/t) / сбораfunction nextWords(counts, t, k) { const top = [...counts.keys()] .sort((a, b) => counts.get(b) - counts.get(a)) .slice(0, k); let total = 0; for (const w of top) total += counts.get(w) ** (1 / t); return top.map((w) => { const weight = counts.get(w) ** (1 / t); return { word: w, p: weight / total }; });}const table = countNext(TEXT);const counts = table.get("ще");console.log('след "ще":');for (const c of nextWords(counts, 1, 100)) console.log(c.word.padEnd(11) + String(counts.get(c.word)).padStart(2) + " " + (c.p * 100).toFixed(1).padStart(5) + "%");след "ще":пристигне 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 { constructor(seed) { this.state = seed; }
// следващото число между 0 и 1; едно семе дава едни и същи числа next() { this.state = (this.state * 48271) % 2147483647; return this.state / 2147483647; }}// число от 0 до 1 пада в една от лентитеfunction pick(choices, rng) { const r = rng.next(); let sum = 0; for (const c of choices) { sum += c.p; if (r < sum) return c.word; } return choices[choices.length - 1].word;}Генерирането повтаря това дума по дума от началото, докато дойде „.“ или 12 думи:
// продължи текста дума по дума до „.“ или до 12 думиfunction generate(table, start, t, k, rng) { const text = start.split(" "); let word = text[text.length - 1]; while (text.length < 12) { word = pick(nextWords(table.get(word), t, k), rng); if (word === ".") break; text.push(word); } return text.join(" ");}Ако винаги вземаш най-вероятната дума (k = 1), генераторът няма какво да избира и отговорът е един и същ при всяко семе:
const table = countNext(TEXT);for (const seed of [2026, 34567, 12345]) console.log(`семе ${seed}: ${generate(table, "поръчката", 1, 1, new Rng(seed))}`);семе 2026: поръчката ще пристигне до два днисеме 34567: поръчката ще пристигне до два днисеме 12345: поръчката ще пристигне до два дниСъс случаен избор отговорът зависи от семето. Едно и също семе обаче дава едни и същи числа, значи и един и същ отговор:
const table = countNext(TEXT);for (const seed of [2026, 2026, 34567]) console.log(`семе ${seed}: ${generate(table, "поръчката", 1, 100, new Rng(seed))}`);семе 2026: поръчката ще пристигне до три днисеме 2026: поръчката ще пристигне до три днисеме 34567: поръчката пристигна в офиса на куриерЗатова в началото трите отговора бяха различни: всяко семе дава други числа, а оттам други думи.
Пробвай
Предвиди изхода
След „доставката“ в текста има пет продължения, едното е краят „.“. Какво печата програмата с k = 1?
const table = countNext(TEXT);for (const c of nextWords(table.get("доставката"), 1, 1)) console.log(`${c.word} ${(c.p * 100).toFixed(1)}%`);k = 1 оставя само най-честата дума, „до“. Сборът на теглата вече е само нейното тегло, а теглото, делено на себе си, е 1: 100,0%. 33,3% е вероятността ѝ сред всички думи, но тях k = 1 ги е изрязало.
Идеята
Температура и top-k
Изборът има две ръчки, които имат и истинските модели:
- температура
t: теглото е брой^(1/t). Приtпод 1 честите думи стават още по-вероятни и отговорът е по-предвидим. Приtнад 1 вероятностите се изравняват и редките думи излизат по-често. - top-k: остават само
k-те най-чести думи, а другите не могат да се изберат. Сборът се смята само по тях, затова пак е 100%.
const table = countNext(TEXT);const counts = table.get("ще");const temps = [0.5, 1, 2];const rows = temps.map((t) => nextWords(counts, t, 100));console.log(" ".repeat(11) + temps.map((t) => `t=${t}`.padStart(7)).join(""));for (let i = 0; i < counts.size; i++) console.log(rows[0][i].word.padEnd(11) + rows.map((row) => ((row[i].p * 100).toFixed(1) + "%").padStart(7)).join("")); 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) / сбораfunction nextWords(counts, t, k) { const top = [...counts.keys()] .sort((a, b) => counts.get(b) - counts.get(a)) .slice(0, k); let total = 0; for (const w of top) total += counts.get(w) ** (1 / t); return top.map((w) => { const weight = counts.get(w) ** (1 / t); return { word: w, p: weight / total }; });}
// число от 0 до 1 пада в една от лентитеfunction pick(choices, rng) { const r = rng.next(); let sum = 0; for (const c of choices) { sum += c.p; if (r < sum) return c.word; } return choices[choices.length - 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%): следващата дума е „бъде“, не най-честата.
const table = countNext(TEXT);const choices = nextWords(table.get("ще"), 1, 3);console.log("k=3:");for (const c of choices) console.log(c.word.padEnd(11) + (c.p * 100).toFixed(1).padStart(5) + "%");const rng = new Rng(31337);const copy = new Rng(31337);console.log(`число от генератора: ${copy.next().toFixed(4)}`);console.log(`избрана: ${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 (const w of top) total += 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%: „пратката“ и „пристигна“ са в текста, но никога една след друга. Така моделът казва колко „естествен“ му изглежда текстът.
Още задачи
- Най-вероятното изречение. Започни от „пратката“ и всеки път вземи най-вероятната дума. Каква е вероятността на изречението, което се получава?
- Две думи назад. Брой коя дума идва след двойка думи, не след една. Как се променят отговорите от началото на урока?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Данни, switch и разглобяване · Какво е машинно учене · Цифрите от касовата бележка · Текстът на парчета: токени
- Продължава в: Ембединги: смисълът като вектори · Прозорецът на контекста · Границите на модела · Извикване на модел през API
- Задачи по темата: Как работи езиков модел