Когда впервые сталкиваешься с современными языковыми моделями, легко потеряться в количестве новых терминов. Embeddings, Attention, KV Cache, Multi-Head Attention, Positional Encoding — в статьях про GPT и Llama всё это появляется уже на первых страницах.

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

Гораздо проще посмотреть на этот путь шаг за шагом:

Bigram model
↓
RNN
↓
Attention
↓
Transformer
↓
LLM

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


Часть I. Как устроен LLM

Шаг 1. Bigram Model: попугай на 49 параметров

Начнем с максимально простой языковой модели.

Пусть наш словарь состоит всего из семи токенов:

vocab = [
    "John",
    "Mary",
    "likes",
    "hates",
    "cats",
    "dogs",
    "<END>"
]

Обучающие данные:

John likes cats
John likes dogs
Mary likes cats
Mary hates dogs

Самая простая языковая модель хранит вероятность следующего токена после текущего.

Например:

P(likes | John)
P(hates | Mary)
P(cats | likes)
P(dogs | hates)

Такую модель можно представить как матрицу:

import numpy as np

vocab_size = 7

weights = np.random.randn(vocab_size, vocab_size)

print(weights.shape)

Результат:

(7, 7)

Всего:

7 × 7 = 49 параметров

Для генерации текста используется Softmax.

# превращает произвольные числовые оценки модели в вероятности,
# сумма которых равна 1
def softmax(x):
    e = np.exp(x - np.max(x))
    return e / e.sum()

john_id = 0

probs = softmax(weights[john_id])

print(probs)

Теперь модель умеет отвечать на вопрос:

Какой токен вероятнее всего идет после "John"?

Это уже настоящий Language Model.

Но есть проблема: после слова "likes" модель не помнит, кто именно любит кошек.

John likes cats
Mary likes cats

Для нее это одна и та же ситуация, так как памяти нет.


Шаг 2. RNN (Recurrent Neural Network): добавляем память

Логичное решение — хранить внутреннее состояние. Пусть модель читает предложение по одному слову.

John → likes → cats

После каждого токена обновляется скрытое состояние.

import numpy as np

# В начале предложения память пустая
h = np.zeros(2)

# Параметры модели, которые будут обучаться
Wx = np.random.randn(2, 2)
Wh = np.random.randn(2, 2)

def step(x, h):
    # Влияние текущего слова
    input_part = Wx @ x

    # Влияние прошлой памяти
    memory_part = Wh @ h

    # Новое скрытое состояние
    return np.tanh(input_part + memory_part)

Теперь вычисления выглядят так:

John
 ↓
h1

likes
 ↓
h2

cats
 ↓
h3

Вектор h играет роль памяти. Если в начале предложения встретился John, информация может сохраниться внутри состояния и использоваться позже.Это огромный шаг вперед.

Но появляется новая проблема. Если предложение становится длинным:

The cat that lived in the house near the river ...

то информация постепенно забывается. Все прошлое приходится сжимать в один маленький вектор. Это называется bottleneck.


Шаг 3. Attention: перестаем всё помнить

RNN решил проблему отсутствия памяти, но породил новую. Вся история предложения должна теперь помещаться в один скрытый вектор:

John likes cats

Пока предложения короткие, это работает неплохо. Но что произойдет, если предложение состоит из многих слов?

The cat that lived in the house near the river ...

Модели приходится постоянно сжимать всё прошлое в один небольшой набор чисел. Чем длиннее становится текст, тем сложнее сохранить важную информацию.

Возникает естественная идея: А что если не пытаться помнить всё? Что если хранить все предыдущие токены и при необходимости просто обращаться к нужным? Предположим, мы обрабатываем предложение:

John likes cats

В данный момент модель находится на слове:

cats

Чтобы понять контекст, она может посмотреть назад на предыдущие слова:

John
likes
cats

Но как определить, какие слова важнее? Как вариант, можно вычислить некоторое число для каждого токена, показывающее степень его релевантности текущему слову. Например, допустим, модель решила, что для слова "cats":

John  → 1.2
likes → 0.8
cats  → 0.3

Чем больше число, тем сильнее внимание. Пока это ещё не вероятности, а просто оценки важности (attention scores). Для преобразования их в вероятности используется Softmax:

import numpy as np

scores = np.array([1.2, 0.8, 0.3])

weights = np.exp(scores)
weights /= weights.sum()

print(weights)

Получаем:

John   0.47
likes  0.32
cats   0.21

Теперь веса суммируются к единице и могут интерпретироваться как распределение внимания. Модель буквально говорит:

При обработке слова "cats" я примерно на 47% смотрю на "John", на 32% — на "likes" и на 21% — на текущее слово.

Это и есть основная идея Attention. Вместо попытки запомнить всё прошлое в одном векторе мы сохраняем всю историю и динамически выбираем, какие её части важны прямо сейчас. Но пока остаётся один важный вопрос.

Откуда вообще берутся числа:

1.2
0.8
0.3

Ответ на него приводит нас к Transformer.


Шаг 4. Transformer: превращаем Attention в архитектуру

До сих пор мы просто предполагали, что модель каким-то образом умеет вычислять attention scores. Теперь разберёмся, как это происходит на самом деле.

В 2017 году вышла статья: "Attention Is All You Need". Её авторы предложили радикальную идею. Если Attention умеет выбирать важные части контекста, то, возможно, рекурсия вообще не нужна. Можно полностью отказаться от RNN и построить всю архитектуру вокруг механизма внимания. Для каждого токена модель сначала вычисляет его эмбеддинг.

Например:

John  → embedding
likes → embedding
cats  → embedding

Затем из каждого эмбеддинга строятся три новых вектора:

import numpy as np

embedding_dim = 4

embedding = np.random.randn(embedding_dim)

Q = np.random.randn(embedding_dim)
K = np.random.randn(embedding_dim)
V = np.random.randn(embedding_dim)

Именно отсюда появляются знаменитые Query, Key и Value. Можно представить их следующим образом:

  • Query отвечает на вопрос: "Что я сейчас ищу?"

  • Key отвечает: "Какую информацию я содержу?"

  • Value отвечает: "Какую информацию я передам дальше, если меня выберут?"

Теперь предположим, что мы снова обрабатываем слово:

cats

Его Query сравнивается с Key каждого токена в контексте:

score = Q @ K

Скалярное произведение показывает степень сходства между запросом и кандидатом. Для каждого слова получается свой score:

John  → 1.2
likes → 0.8
cats  → 0.3

Вот откуда появились числа из предыдущего раздела. Они не берутся случайно и не задаются вручную. Это результат сравнения Query текущего токена с Key всех остальных токенов. После этого применяется Softmax и получаются веса внимания:

John   0.47
likes  0.32
cats   0.21

Затем эти веса используются для смешивания Value-векторов.

В результате формируется новая репрезентация слова "cats", которая уже учитывает весь контекст предложения. Самое важное здесь то, что каждый токен может напрямую обращаться к любому другому токену внутри контекстного окна. Больше не нужно передавать информацию через длинную цепочку скрытых состояний, как в RNN. Именно поэтому Transformer лучше работает с длинными текстами, легче обучается на GPU и стал фундаментом всех современных LLM.


Шаг 5. LLM: просто очень большой Transformer

После понимания Transformer исчезает большая часть магии вокруг современных языковых моделей.

С инженерной точки зрения GPT, Claude, Gemini или Llama выглядят удивительно скучно. Это всё тот же Transformer, только масштабированный до десятков или сотен слоёв, обученный на огромном количестве текстов и содержащий миллиарды параметров.

Типичная схема выглядит примерно так:

Tokenizer
    ↓
Embeddings
    ↓
Transformer × N
    ↓
Softmax

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

The capital of France is

модель вычисляет вероятности:

{
  "Paris": 0.93,
  "London": 0.02,
  "Berlin": 0.01,
  "Rome": 0.01
}

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


Часть II. Откуда берётся рассуждение

Шаг 6. Где находится мышление?

После знакомства с архитектурой Transformer многие испытывают одинаковое разочарование. Несколько разделов подряд мы говорили про внимание, эмбеддинги, матрицы и миллиарды параметров. А затем выяснилось, что вся система решает одну-единственную задачу — предсказывает следующий токен. Возникает закономерный вопрос: где именно находится мышление?

На первый взгляд его вообще не должно быть. Если модель обучалась продолжать тексты, то почему она умеет писать программы, решать задачи и объяснять сложные технические концепции? Ответ начинается с понимания того, на чём именно обучаются современные модели. Они видят не просто слова. Они видят миллиарды примеров человеческого мышления, записанного в текстовой форме.


Шаг 7. Почему модель пишет промежуточные шаги?

В обучающих данных постоянно встречаются последовательности вроде:

Вопрос
    ↓
Решение
    ↓
Промежуточные вычисления
    ↓
Ответ

Для человека это выглядит как рассуждение. Для модели — как статистическая закономерность. Если в интернете миллионы раз встречалась структура:

17 × 20 = 340
17 × 3 = 51
340 + 51 = 391

то после появления фрагмента:

17 × 20 =

следующим наиболее вероятным продолжением становится именно число 340. После этого контекст меняется, и модель может продолжить следующую строку решения. Каждый отдельный шаг остаётся предсказанием следующего токена, однако вся последовательность выглядит как логическое рассуждение.


Шаг 8. Chain of Thought: внешний блокнот для модели

Теперь становится понятно, почему техника Chain of Thought работает настолько хорошо.

Когда мы пишем:

Рассуждай шаг за шагом

мы не включаем какой-то скрытый модуль логики. Мы заставляем модель генерировть дополнительные промежуточные токены. Эти токены затем попадают обратно в контекст и становятся доступными для следующих вычислений. Получается любопытный цикл:

Модель пишет текст
        ↓
Текст попадает в контекст
        ↓
Модель читает этот текст
        ↓
Использует его для следующих шагов

Фактически модель использует собственный вывод как внешний блокнот. Часть вычислений оказывается вынесена из весов сети непосредственно в текст.


Шаг 9. Почему модель умеет решать новые задачи?

На этом этапе обычно возникает ещё один вопрос.

Если модель просто воспроизводит шаблоны из обучающих данных, почему она способна решать задачи, которых никогда раньше не видела?

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

Предположим, в обучающих данных встречаются предложения:

John likes cats
John likes dogs
Mary likes cats
Mary likes dogs

Если бы модель попыталась запомнить каждое предложение отдельно, ей пришлось бы независимо выучить четыре разных случая.

Но градиентный спуск находит более простое решение.

Во всех примерах слова "John" и "Mary" появляются в похожем контексте. А слова "cats" и "dogs" тоже часто оказываются взаимозаменяемыми. Во время обучения параметры постепенно изменяются так, чтобы похожие слова получили похожие представления внутри модели.

В результате эмбеддинги могут стать примерно такими:

John  → [0.8, 0.2]
Mary  → [0.7, 0.3]
cats  → [0.1, 0.9]
dogs  → [0.2, 0.8]

Точные числа не важны. Важно, что John и Mary оказываются близко друг к другу в пространстве признаков, а cats и dogs — в другом кластере. После этого модель фактически начинает работать не с конкретными словами, а с их свойствами.

Поэтому предложение "Bob likes cats" оказывается не полностью новой ситуацией.

Если эмбеддинг Bob похож на эмбеддинги John и Mary, модель может применить уже известные закономерности к новому слову.

Именно так возникает обобщение. Модель не выводит правило в символьном виде:

<имя> likes <животное>

как сделал бы программист.

Вместо этого она постепенно организует внутреннее пространство представлений так, чтобы похожие объекты располагались рядом и могли использоваться одинаковым образом.


Шаг 10. Почему фраза "LLM просто предсказывает следующий токен" вводит в заблуждение

Фраза:

LLM всего лишь предсказывает следующий токен.

технически абсолютно верна. Но она создаёт ложное впечатление, будто модель является очень сложным автодополнением текста. Это примерно то же самое, что сказать:

Процессор всего лишь выполняет инструкции.

или:

Мозг всего лишь передаёт электрические импульсы.

На уровне отдельной операции это правда. Однако сложное поведение возникает на уровне всей системы.

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

Рассуждение возникает не вопреки механизму next-token prediction.

Рассуждение возникает именно из него.

Комментарии (0)