Към съдържанието
wannacode

Предсказване на следващия токен

Накратко: Езиковият модел пише текст, като смята вероятностите на следващия токен и избира един; температурата и top-k решават колко случаен е изборът.

Проблем

Едно начало, три отговора

Ботът на поддръжката трябва да дописва отговори. Колега пуска един и същ начален текст, „поръчката“, три пъти, с различно семе на генератора. Ето трите отговора:

table = count_next(TEXT)
for seed in [2026, 34567, 12345]:
print(f"семе {seed}:", generate(table, "поръчката", 1, 100, Rng(seed)))
Изход
семе 2026: поръчката ще пристигне до три дни
семе 34567: поръчката пристигна в офиса на куриер
семе 12345: поръчката ще ви се връщат по картата ви утре

Първият звучи разумно. Вторият е почти изречение. Третият е безсмислица, макар че всяка две съседни думи са от текста на магазина. И трите са от една и съща програма, а програмата не търси отговор никъде: тя пише дума по дума.

Откъде знае коя дума да сложи? И защо всеки път е различно?

Идеята

Вероятността на следващата дума

Езиковият модел получава текст и дава вероятност за всеки възможен следващ токен. Нищо повече: не знае какво е поръчка, знае само какво обикновено идва след какво.

Текстът се пише с цикъл:

  1. Моделът дава вероятностите на следващия токен.
  2. Един токен се избира според тях.
  3. Добавя се към текста и всичко се повтаря, докато дойде краят или лимитът.

Изборът е отделна стъпка, а не част от модела. Затова един и същ модел може да дава различни отговори.

Моделът в урока е най-малкият възможен: таблица колко пъти коя дума е идвала точно след коя. Гледа само последната дума и брои цели думи, а не токени. Истинските модели са невронни мрежи, които гледат целия текст, но дават същото: вероятностите на следващия токен.

Виж го

Таблицата: коя дума след коя

Текстът на магазина е същият като в урока за токените. За всяко изречение броим всяка двойка съседни думи. Точката „.“ отбелязва края на изречението, за да знае моделът кога да спре.

# колко пъти коя дума идва точно след коя; „.“ е краят на изречението
def count_next(text):
table = {}
for line in text:
w = (line + " .").split(" ")
for word, after in zip(w, w[1:]):
counts = table.setdefault(word, {})
counts[after] = counts.get(after, 0) + 1
return table

Вероятността на следващата дума е броят ѝ, делен на сбора. Засега гледай само t = 1 и „всички думи“: тогава теглото е самият брой. Температурата t и k идват по-долу.

# k-те най-чести думи, тегло брой^(1/t) / сбора
def next_words(counts, t, k):
top = sorted(counts, key=counts.get, reverse=True)[:k]
total = sum(counts[w] ** (1 / t) for w in top)
return [(w, counts[w] ** (1 / t) / total)
for w in top]
table = count_next(TEXT)
counts = table["ще"]
print('след "ще":')
for word, p in next_words(counts, 1, 100):
print(f"{word:<11}{counts[word]:2} {p * 100:5.1f}%")
Изход
след "ще":
пристигне 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:
def __init__(self, seed):
self.state = seed
# следващото число между 0 и 1; едно семе дава едни и същи числа
def next(self):
self.state = self.state * 48271 % 2147483647
return self.state / 2147483647
# число от 0 до 1 пада в една от лентите
def pick(choices, rng):
r = rng.next()
total = 0.0
for word, p in choices:
total += p
if r < total:
return word
return choices[-1][0]

Генерирането повтаря това дума по дума от началото, докато дойде „.“ или 12 думи:

# продължи текста дума по дума до „.“ или до 12 думи
def generate(table, start, t, k, rng):
text = start.split(" ")
word = text[-1]
while len(text) < 12:
word = pick(next_words(table[word], t, k), rng)
if word == ".":
break
text.append(word)
return " ".join(text)

Ако винаги вземаш най-вероятната дума (k = 1), генераторът няма какво да избира и отговорът е един и същ при всяко семе:

table = count_next(TEXT)
for seed in [2026, 34567, 12345]:
print(f"семе {seed}:", generate(table, "поръчката", 1, 1, Rng(seed)))
Изход
семе 2026: поръчката ще пристигне до два дни
семе 34567: поръчката ще пристигне до два дни
семе 12345: поръчката ще пристигне до два дни

Със случаен избор отговорът зависи от семето. Едно и също семе обаче дава едни и същи числа, значи и един и същ отговор:

table = count_next(TEXT)
for seed in [2026, 2026, 34567]:
print(f"семе {seed}:", generate(table, "поръчката", 1, 100, Rng(seed)))
Изход
семе 2026: поръчката ще пристигне до три дни
семе 2026: поръчката ще пристигне до три дни
семе 34567: поръчката пристигна в офиса на куриер

Затова в началото трите отговора бяха различни: всяко семе дава други числа, а оттам други думи.

Пробвай

Предвиди изхода

След „доставката“ в текста има пет продължения, едното е краят „.“. Какво печата програмата с k = 1?

table = count_next(TEXT)
for word, p in next_words(table["доставката"], 1, 1):
print(f"{word} {p * 100:.1f}%")
Отговор

Идеята

Температура и top-k

Изборът има две ръчки, които имат и истинските модели:

  • температура t: теглото е брой^(1/t). При t под 1 честите думи стават още по-вероятни и отговорът е по-предвидим. При t над 1 вероятностите се изравняват и редките думи излизат по-често.
  • top-k: остават само k-те най-чести думи, а другите не могат да се изберат. Сборът се смята само по тях, затова пак е 100%.
table = count_next(TEXT)
counts = table["ще"]
temps = [0.5, 1.0, 2.0]
rows = [next_words(counts, t, 100) for t in temps]
print(" " * 11 + "".join(f"t={t:g}".rjust(7) for t in temps))
for i in range(len(counts)):
print(f"{rows[0][i][0]:<11}" + "".join(f"{row[i][1] * 100:6.1f}%" for row in rows))
Изход
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) / сбора
def next_words(counts, t, k):
top = sorted(counts, key=counts.get, reverse=True)[:k]
total = sum(counts[w] ** (1 / t) for w in top)
return [(w, counts[w] ** (1 / t) / total)
for w in top]
# число от 0 до 1 пада в една от лентите
def pick(choices, rng):
r = rng.next()
total = 0.0
for word, p in choices:
total += p
if r < total:
return word
return choices[-1][0]
Стъпка по стъпка, с думи
  1. След „ще“ в текста идват 6 думи. Лентата е броят, делен на всички 10: „пристигне“ е 4 от 10, 40,0%.
  2. t = 0,5: теглото е брой². Честата дума става още по-вероятна: „пристигне“ 66,7%.
  3. t = 2: теглото е √брой. Лентите се изравняват: „пристигне“ 27,0%, „получите“ 13,5%.
  4. t = 1, k = 3: остават само трите най-чести и сборът им пак е 100%: 57,1%, 28,6%, 14,3%.
  5. Трите ленти застават една до друга от 0 до 1. Генераторът със семе 31337 дава 0,7044.
  6. 0,7044 пада в лентата на „бъде“ (от 57,1% до 85,7%): следващата дума е „бъде“, не най-честата.
table = count_next(TEXT)
choices = next_words(table["ще"], 1, 3)
print("k=3:")
for word, p in choices:
print(f"{word:<11}{p * 100:5.1f}%")
rng = Rng(31337)
copy = Rng(31337)
print(f"число от генератора: {copy.next():.4f}")
print(f"избрана: {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%. Изборът е по вероятностите, но не е винаги най-вероятната дума.

Пробвай

Попълни

Кое число стои на мястото на ___ в теглото на думата?

total = sum(counts[w] ** (1 / ) for w in top)

Пробвай

Намери грешката

Колега иска top-k с трите най-вероятни думи, но моделът му пише само редки думи. На кой ред е грешката?

Избери реда с грешката.

Идеята

Кога боли

  • Вероятното не е вярно. Моделът пише „до три дни“ толкова лесно, колкото „до два дни“: и двете са в текста. Той не знае кога пристига точно тази поръчка. Гладкият текст звучи уверено и когато е грешен. За това е урокът за границите на модела.
  • Всяка двойка е вярна, изречението не е. „поръчката ще ви се връщат по картата ви утре“ е от съседни думи, които съществуват. Нашият модел гледа само една дума назад. Истинските гледат целия текст и пишат много по-добре, но пак могат да слепят нещо гладко и безсмислено.
  • Висока температура, повече грешки. Редките продължения излизат по-често. За отговори на клиенти, където трябва точност, температурата е ниска. За идеи и варианти може да е по-висока.
  • Едно семе не е обещание. В урока едно семе дава един отговор. При истински модел през API една и съща заявка понякога дава различен отговор и при едни и същи настройки, затова не разчитай на точно същия текст, а проверявай отговора.
По-точно, за любопитните

Softmax и температурата. Истинският модел е невронна мрежа: за всеки токен от речника тя дава число (logit), а softmax ги прави вероятности. Температурата дели числата на t преди softmax. Теглото брой^(1/t) от урока е точно това: softmax от log(брой) / t.

top-p. Освен top-k има и top-p: остават най-честите токени, докато сборът им стигне p (например 0,9). Така при уверен модел остават малко токени, а при неуверен повече.

Токени, не думи. Истинският модел избира токени от речника си, а думата се получава от няколко избора поред. Температурата, top-k и семето се задават в заявката към модела (урокът за извикването).

Колко назад гледа. Нашият модел помни една дума. Истинският вижда целия разговор до лимита си, и това е прозорецът на контекста.

В Python. sorted(..., reverse=True) пази реда на равните елементи, като Java, C# и JavaScript. Затова думите с еднакъв брой са в реда на първата си поява на всичките четири езика.

Упражнение

Колко вероятно е изречението

Реши в редактора си.

Вероятността на цяло изречение е произведението на вероятностите на всяка следваща дума, включително „.“ накрая. Напиши функция, която я смята с таблицата от урока, и я отпечатай в проценти за три изречения. Ако двойка думи никога не е виждана, вероятността ѝ е 0.

Изход
пратката ще пристигне до два дни: 2.86%
пратката ще пристигне до 14 дни: 1.43%
пратката пристигна утре: 0.00%

Първото изречение е 2,86%, второто 1,43%, макар че и двете не са в текста буква по буква: всички двойки в тях са виждани. Третото е 0,00%: „пратката“ и „пристигна“ са в текста, но никога една след друга. Така моделът казва колко „естествен“ му изглежда текстът.

Още задачи

  • Най-вероятното изречение. Започни от „пратката“ и всеки път вземи най-вероятната дума. Каква е вероятността на изречението, което се получава?
  • Две думи назад. Брой коя дума идва след двойка думи, не след една. Как се променят отговорите от началото на урока?

Тест

Провери се

1. Какво дава езиковият модел, когато му подадеш текст?
2. Пускаш генерирането два пъти със семе 2026. Какво ще видиш?
3. Как температурата t = 0,5 променя вероятностите след „ще“?
4. Ботът пише „поръчката ще пристигне до три дни“, а реално доставката е до два дни. Защо?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·