Предсказване на следващия токен
Накратко: Езиковият модел пише текст, като смята вероятностите на следващия токен и избира един; температурата и top-k решават колко случаен е изборът.
Проблем
Едно начало, три отговора
Ботът на поддръжката трябва да дописва отговори. Колега пуска един и същ начален текст, „поръчката“, три пъти, с различно семе на генератора. Ето трите отговора:
var table = CountNext(text);foreach (var seed in new[] { 2026, 34567, 12345 }) Console.WriteLine("семе " + seed + ": " + Generate(table, "поръчката", 1, 100, new Rng(seed)));семе 2026: поръчката ще пристигне до три днисеме 34567: поръчката пристигна в офиса на куриерсеме 12345: поръчката ще ви се връщат по картата ви утреПървият звучи разумно. Вторият е почти изречение. Третият е безсмислица, макар че всяка две съседни думи са от текста на магазина. И трите са от една и съща програма, а програмата не търси отговор никъде: тя пише дума по дума.
Откъде знае коя дума да сложи? И защо всеки път е различно?
Идеята
Вероятността на следващата дума
Езиковият модел получава текст и дава вероятност за всеки възможен следващ токен. Нищо повече: не знае какво е поръчка, знае само какво обикновено идва след какво.
Текстът се пише с цикъл:
- Моделът дава вероятностите на следващия токен.
- Един токен се избира според тях.
- Добавя се към текста и всичко се повтаря, докато дойде краят или лимитът.
Изборът е отделна стъпка, а не част от модела. Затова един и същ модел може да дава различни отговори.
Моделът в урока е най-малкият възможен: таблица колко пъти коя дума е идвала точно след коя. Гледа само последната дума и брои цели думи, а не токени. Истинските модели са невронни мрежи, които гледат целия текст, но дават същото: вероятностите на следващия токен.
Виж го
Таблицата: коя дума след коя
Текстът на магазина е същият като в урока за токените. За всяко изречение броим всяка двойка съседни думи. Точката „.“ отбелязва края на изречението, за да знае моделът кога да спре.
// една възможна следваща дума и вероятността ѝrecord Choice(string Word, double P);
// колко пъти коя дума идва точно след коя; „.“ е краят на изречениетоstatic OrderedDictionary<string, OrderedDictionary<string, int>> CountNext(string[] text){ var table = new OrderedDictionary<string, OrderedDictionary<string, int>>(); foreach (var line in text) { var w = (line + " .").Split(' '); for (int i = 0; i + 1 < w.Length; i++) { if (!table.TryGetValue(w[i], out var counts)) table[w[i]] = counts = new OrderedDictionary<string, int>(); counts[w[i + 1]] = counts.GetValueOrDefault(w[i + 1]) + 1; } } return table;}Вероятността на следващата дума е броят ѝ, делен на сбора. Засега гледай само t = 1 и „всички думи“: тогава теглото е самият брой. Температурата t и k идват по-долу.
// k-те най-чести думи, тегло брой^(1/t) / сбораstatic List<Choice> NextWords( IDictionary<string, int> counts, double t, int k){ var top = counts.OrderByDescending(c => c.Value) .Take(k).ToList(); double total = 0; foreach (var c in top) total += Math.Pow(c.Value, 1 / t); var choices = new List<Choice>(); foreach (var c in top) { double weight = Math.Pow(c.Value, 1 / t); choices.Add(new Choice(c.Key, weight / total)); } return choices;}var table = CountNext(text);var counts = table["ще"];Console.WriteLine("след \"ще\":");foreach (var c in NextWords(counts, 1, 100)) Console.WriteLine($"{c.Word,-11}{counts[c.Word],2} {c.P * 100,5:F1}%");след "ще":пристигне 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(long seed){ long state = seed;
// следващото число между 0 и 1; едно семе дава едни и същи числа public 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; foreach (var c in choices) { sum += c.P; if (r < sum) return c.Word; } return choices[^1].Word;}Генерирането повтаря това дума по дума от началото, докато дойде „.“ или 12 думи:
// продължи текста дума по дума до „.“ или до 12 думиstatic string Generate(OrderedDictionary<string, OrderedDictionary<string, int>> table, string start, double t, int k, Rng rng){ var text = start.Split(' ').ToList(); var word = text[^1]; while (text.Count < 12) { word = Pick(NextWords(table[word], t, k), rng); if (word == ".") break; text.Add(word); } return string.Join(" ", text);}Ако винаги вземаш най-вероятната дума (k = 1), генераторът няма какво да избира и отговорът е един и същ при всяко семе:
var table = CountNext(text);foreach (var seed in new[] { 2026, 34567, 12345 }) Console.WriteLine("семе " + seed + ": " + Generate(table, "поръчката", 1, 1, new Rng(seed)));семе 2026: поръчката ще пристигне до два днисеме 34567: поръчката ще пристигне до два днисеме 12345: поръчката ще пристигне до два дниСъс случаен избор отговорът зависи от семето. Едно и също семе обаче дава едни и същи числа, значи и един и същ отговор:
var table = CountNext(text);foreach (var seed in new[] { 2026, 2026, 34567 }) Console.WriteLine("семе " + seed + ": " + Generate(table, "поръчката", 1, 100, new Rng(seed)));семе 2026: поръчката ще пристигне до три днисеме 2026: поръчката ще пристигне до три днисеме 34567: поръчката пристигна в офиса на куриерЗатова в началото трите отговора бяха различни: всяко семе дава други числа, а оттам други думи.
Пробвай
Предвиди изхода
След „доставката“ в текста има пет продължения, едното е краят „.“. Какво печата програмата с k = 1?
var table = CountNext(text);foreach (var c in NextWords(table["доставката"], 1, 1)) Console.WriteLine($"{c.Word} {c.P * 100:F1}%");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);var counts = table["ще"];double[] temps = [0.5, 1, 2];var rows = temps.Select(t => NextWords(counts, t, 100)).ToList();Console.WriteLine(new string(' ', 11) + string.Concat(temps.Select(t => $"t={t}".PadLeft(7))));for (int i = 0; i < counts.Count; i++) Console.WriteLine($"{rows[0][i].Word,-11}" + string.Concat(rows.Select(row => $"{row[i].P * 100,6:F1}%"))); 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( IDictionary<string, int> counts, double t, int k){ var top = counts.OrderByDescending(c => c.Value) .Take(k).ToList(); double total = 0; foreach (var c in top) total += Math.Pow(c.Value, 1 / t); var choices = new List<Choice>(); foreach (var c in top) { double weight = Math.Pow(c.Value, 1 / t); choices.Add(new Choice(c.Key, weight / total)); } return choices;}
// число от 0 до 1 пада в една от лентитеstatic string Pick(List<Choice> choices, Rng rng){ double r = rng.Next(); double sum = 0; foreach (var c in choices) { sum += c.P; if (r < sum) return c.Word; } return choices[^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);var choices = NextWords(table["ще"], 1, 3);Console.WriteLine("k=3:");foreach (var c in choices) Console.WriteLine($"{c.Word,-11}{c.P * 100,5:F1}%");var rng = new Rng(31337);var copy = new Rng(31337);Console.WriteLine($"число от генератора: {copy.Next():F4}");Console.WriteLine($"избрана: {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%. Изборът е по вероятностите, но не е винаги най-вероятната дума.
Пробвай
Попълни
Кое число стои на мястото на ___ в теглото на думата?
foreach (var c in top)
total += Math.Pow(c.Value, 1 / );Температурата t: теглото е брой^(1/t). При t = 1 това е самият брой, при t = 0,5 е брой², а при t = 2 е корен от броя.
Пробвай
Намери грешката
Колега иска top-k с трите най-вероятни думи, но моделът му пише само редки думи. На кой ред е грешката?
Избери реда с грешката.
Подреждането е от най-рядката към най-честата, затова първите k са трите най-редки думи. Трябва да е обратното: OrderByDescending.
Идеята
Кога боли
- Вероятното не е вярно. Моделът пише „до три дни“ толкова лесно, колкото „до два дни“: и двете са в текста. Той не знае кога пристига точно тази поръчка. Гладкият текст звучи уверено и когато е грешен. За това е урокът за границите на модела.
- Всяка двойка е вярна, изречението не е. „поръчката ще ви се връщат по картата ви утре“ е от съседни думи, които съществуват. Нашият модел гледа само една дума назад. Истинските гледат целия текст и пишат много по-добре, но пак могат да слепят нещо гладко и безсмислено.
- Висока температура, повече грешки. Редките продължения излизат по-често. За отговори на клиенти, където трябва точност, температурата е ниска. За идеи и варианти може да е по-висока.
- Едно семе не е обещание. В урока едно семе дава един отговор. При истински модел през 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
- Задачи по темата: Как работи езиков модел