Оглавление

В мире больших языковых моделей основное внимание обычно приковано к архитектурам, данным и оптимизации. Однако стратегии декодирования, такие как лучевой поиск, играют ключевую роль в генерации текста и часто остаются в тени, пишет Hugging Face.

Как работают LLM на самом деле

Распространенное заблуждение: языковые модели типа GPT-2 непосредственно генерируют текст. В реальности они вычисляют логиты — оценки для каждого возможного токена в их словаре. Токенизатор (чаще всего байт-парное кодирование) преобразует входной текст в идентификаторы токенов, модель предсказывает следующий наиболее вероятный токен, а затем генерирует логиты, которые преобразуются в вероятности через softmax-функцию.

Диаграмма процесса генерации текста LLM: токенизация и расчет вероятностей

Жадный поиск: простота ценой качества

Жадный поиск — метод декодирования, который на каждом шаге выбирает наиболее вероятный токен в качестве следующего в последовательности. Он сохраняет только самый вероятный токен на каждом этапе, отбрасывая все другие потенциальные варианты.

На примере фразы «I have a dream»:

  • Шаг 1: Вход: «I have a dream» → Наиболее вероятный токен: » of»
  • Шаг 2: Вход: «I have a dream of» → Наиболее вероятный токен: » being»
  • Шаг 3: Вход: «I have a dream of being» → Наиболее вероятный токен: » a»
  • Шаг 4: Вход: «I have a dream of being a» → Наиболее вероятный токен: » doctor»
  • Шаг 5: Вход: «I have a dream of being a doctor» → Наиболее вероятный токен: «.»

Этот подход короткозоркий: он учитывает только наиболее вероятный токен на каждом шаге, не рассматривая общее влияние на последовательность. Это делает его быстрым и эффективным, но он может упускать лучшие последовательности, которые могли бы появиться с немного менее вероятными следующими токенами.

from transformers import GPT2LMHeadModel, GPT2Tokenizer
import torch

device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = GPT2LMHeadModel.from_pretrained('gpt2').to(device)
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
model.eval()

text = "I have a dream"
input_ids = tokenizer.encode(text, return_tensors='pt').to(device)

outputs = model.generate(input_ids, max_length=len(input_ids.squeeze())+5)
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(f"Generated text: {generated_text}")

Жадный поиск — это как шахматист, думающий только на один ход вперед. В мире генерации текста такая стратегия приводит к предсказуемым и часто банальным результатам. Именно поэтому современные продвинутые LLM используют более сложные методы вроде лучевого поиска или сэмплинга — они позволяют находить баланс между креативностью и осмысленностью текста.

Практическая реализация

Вот как выглядит реализация жадного поиска с использованием Graphviz и Networkx:

import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import time

def get_log_prob(logits, token_id):
 probabilities = torch.nn.functional.softmax(logits, dim=-1)
 log_probabilities = torch.log(probabilities)
 token_log_probability = log_probabilities[token_id].item()
 return token_log_probability

def greedy_search(input_ids, node, length=5):
 if length == 0:
 return input_ids

 outputs = model(input_ids)
 predictions = outputs.logits
 logits = predictions[0, -1, :]
 token_id = torch.argmax(logits).unsqueeze(0)
 token_score = get_log_prob(logits, token_id)
 new_input_ids = torch.cat([input_ids, token_id.unsqueeze(0)], dim=-1)
 next_token = tokenizer.decode(token_id, skip_special_tokens=True)
 current_node = list(graph.successors(node))[0]
 graph.nodes[current_node]['tokenscore'] = np.exp(token_score) * 100
 graph.nodes[current_node]['token'] = next_token + f"_{length}"
 input_ids = greedy_search(new_input_ids, current_node, length-1)
 return input_ids

Математическая основа

Авторегрессионные модели типа GPT предсказывают следующий токен в последовательности на основе предыдущих токенов. Для последовательности токенов $w = (w_1, w_2, \ldots, w_t)$ совместная вероятность этой последовательности $P(w)$ раскладывается как:

P(w)=P(w1)P(w2∣w1)P(w3∣w2,w1)…P(wt∣w1,…,wt−1)=∏i=1tP(wi∣w1,…,wi−1).

Для каждого токена $w_i$ в последовательности, $P(w_i | w_1, \ldots, w_{i-1})$ представляет условную вероятность $w_i$ при условии всех предыдущих токенов $(w_1, \ldots, w_{i-1})$. GPT-2 вычисляет эту условную вероятность для каждого из 50,257 токенов в своем словаре.