Эмбеддинги слов — word2vec и векторы, которые что-то значат

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

Известная аналогия king − man + woman ≈ queen иллюстрирует возможное соотношение векторов. На диаграмме точки 2D размещены вручную, а не получены из word2vec. Смещения равны по построению; реальные аналогии приблизительны и зависят от корпуса, модели и способа сравнения.

Нажимайте кнопку step, чтобы посмотреть построение по шагам.

Schematic: hand-placed 2D points illustrate vector arithmetic.

mankingwomanv(man), v(king)v(woman)

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

One-hot и TF-IDF выделяют каждому элементу словаря отдельную координату. Плотные векторы используют меньшее число общих измерений. Оба подхода полезны; эмбеддинги добавляют обучаемый способ сравнивать слова за пределами точного совпадения.

Word2vec, представленный в 2013 году, сделал обучение векторов на больших корпусах эффективнее благодаря неглубоким предсказательным моделям. Он развивал прежние распределённые представления, а не впервые предложил обучать векторы слов.

Skip-gram предсказывает контекст по центральному слову, а CBOW — центральное слово по контексту. Сходное употребление даёт сходные предсказательные сигналы и может приводить к связанным представлениям. Это статистический эффект, а не гарантия простого векторного смещения для любой смысловой связи.

От гипотезы к геометрии

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

Малая размерность ограничивает ранг матрицы оценок, поощряя общую предсказательную структуру. Но она не заставляет слова иметь одинаковые координаты. Замена E на E A, а E' на A⁻¹ E' для обратимой A сохраняет все скалярные оценки, хотя может изменить расстояния и углы между входными векторами.

Полезно различать три употребления слова «эмбеддинг»:

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

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

В статье мы пройдём от подсчёта совместных появлений к skip-gram и CBOW, а затем разберём, что меняется при переходе к контекстным представлениям.

Дистрибутивная гипотеза

Ещё в 1957 году лингвист Дж. Р. Фёрт знаменито написал: «Слово познаётся по компании, которую оно водит». В этом вся идея. Слова, появляющиеся в похожих контекстах, имеют похожие значения. Чтобы понять почему, рассмотрим три предложения с пропущенным словом:

  • ___ залаял на почтальона.
  • ___ мурлыкал у меня на коленях.
  • ___ улетел на юг зимовать.

Вам не нужно знать пропущенные слова, чтобы понимать, что они обозначают разные виды животных. Контекст — слова вокруг пропуска — сужает то, что сюда подходит. Это одна половина идеи: контекст предсказывает слово.

Во многих предложениях слова dog, puppy и hound имеют общие контекстные закономерности. Предсказательная модель может использовать для них общие параметры. Общий контекст указывает не только на сходство, но и на тематическую связь или грамматическую совместимость.

Вопрос в том, как вычислить это эффективно. Классический ответ, восходящий к началу 1990-х, — очень большие разреженные матрицы совместной встречаемости: латентно-семантический анализ и его родня. Для предложения «the cat sat on the mat» с окном в 2 слова (смотрим до 2 токенов влево и вправо от каждого центрального слова) матрица выглядит так:

                the     cat     sat     on     mat
the         [    0,      1,      2,      1,     1 ]
cat         [    1,      0,      1,      1,     0 ]
sat         [    2,      1,      0,      1,     0 ]
on          [    1,      1,      1,      0,     1 ]
mat         [    1,      0,      0,      1,     0 ]

M[i,j] считает появления слова j в окне слова i. Исключается центральная позиция, а не все вхождения того же слова. Здесь диагональ нулевая, но повторы слов внутри окна могут дать ненулевые диагональные элементы.

Матрица строится скольжением окна по корпусу и подсчётом.

window half-width:(5 words wide)
accumulated matrix · 2 of 18 co-occurrences tallied
the
cat
sat
on
mat
the
0
1
1
0
0
cat
0
0
0
0
0
sat
0
0
0
0
0
on
0
0
0
0
0
mat
0
0
0
0
0

В каждой позиции с центральным словом i смотрим на слова в его окне ±W и для каждого соседа j в этом окне увеличиваем M[i][j] на единицу. Прокрутите виджет от начала до конца — и матрица заполнится ячейка за ячейкой: один проход по корпусу, и все совместные встречаемости записаны.

Это первый шаг конвейера «посчитай, потом факторизуй», и над ним есть ещё два. Форма вектора слова существенно меняется на каждой стадии:

Стадия 1 — сырые счётчики. Матрица выше как есть. Каждая строка длиной V чисел, по слоту на каждое слово словаря, — это уже вектор слова, просто дико раздутый. Каждая запись буквально означает «сколько раз это конкретное слово словаря появлялось в окне ±2 данного слова по всему корпусу». Строка для cat — это [1, 0, 1, 1, 0]. В игрушечном корпусе всего шесть токенов, поэтому числа крошечные, но важна структура при масштабе.

Этап 2 — поправка на частоту. PMI сравнивает вероятность совместного появления с произведением маргинальных вероятностей. Для ненаблюдавшихся пар PMI не определена или равна минус бесконечности, поэтому часто используют положительную PMI, PPMI = max(PMI, 0), задавая таким элементам ноль. Сглаживание и веса влияют на результат.

Этап 3 — сжатие. Усечённое SVD приближает взвешенную матрицу с помощью наибольших сингулярных значений и соответствующих векторов. Часто таблицу слов берут как U_d Σ_d. Это низкоранговое приближение, не обязательно PCA: PCA сначала центрирует данные, а здесь это не требуется.

raw cat counts:     [1, 0, 1, 1, 0]
PPMI (rounded):     [0.182, 0, 0.405, 0.405, 0]
SVD representation: a row of U_d @ Sigma_d

Полученные d координат объединяют информацию от множества контекстных слов. Отдельные координаты не обязаны соответствовать именованным признакам вроде «королевского статуса» или «пола».

Факторизация счётчиков и word2vec создают плотные таблицы, но оптимизируют разные цели. Связь со сдвинутой PMI относится к skip-gram с negative sampling при определённых предпосылках; не любой word2vec эквивалентен SVD. Вывод приведён в статье о negative sampling.

У матрицы слово–контекст V×V возможных элементов, но разреженное хранение не выделяет память под каждый ноль. Подсчёт и факторизация всё равно могут быть дороги на больших корпусах. Word2vec обновляет векторы непосредственно по примерам, не строя эту матрицу.

word2vec

Word2vec обучает таблицу векторов, предсказывая слова в локальных окнах. Начнём с полного softmax, чтобы явно разобрать прямой проход и градиенты, затем отделим от него более дешёвые практические цели.

Сама таблица даёт слову один вектор независимо от предложения. Последующая последовательностная модель может учитывать эти векторы вместе с порядком слов и контекстом. Статические векторы — исходное представление, а не полная модель предложения.

Word2vec предлагает две альтернативные архитектуры обучения:

  • Skip-gram. По центральному слову предсказать слова в небольшом окне вокруг него. Мы подаём центральное слово на вход и предсказываем каждое из окружающих контекстных слов по очереди — каждая пара (центр, контекст) является отдельным обучающим примером, так что одно и то же центральное слово переиспользуется по разу на соседа.
  • CBOW (continuous bag of words). Наоборот: по словам в окне предсказать центральное. Мы подаём набор контекстных слов и предсказываем единственное слово посередине. Именно эта форма «заполни пропуск» позже наследуется и масштабируется маскированным языковым моделированием BERT.

Оба делают одно и то же, просто гоняя предсказание в противоположных направлениях. Мы пройдём skip-gram от начала до конца, а затем вернёмся к CBOW.

Обучающие данные и one-hot-входы

Весь конвейер skip-gram чисто делится на две стадии с резкой границей: предобработка превращает сырой текст в список обучающих примеров, а затем обучение прогоняет эти примеры через нейросеть.

Сначала посмотрим на предобработку — превращение сырого текста в длинный список целочисленных пар. Никакая нейросеть здесь пока не участвует:

  1. Токенизировать корпус — разбить текст на список словных токенов (разделённых пробелами, обычно в нижнем регистре; word2vec использует токены уровня слов, а не подслов).
  2. Построить словарь — назначить каждому различному токену целочисленный идентификатор; позже он будет служить индексом токена в one-hot-векторах и номером строки в матрице эмбеддингов. Общее количество — V (размер словаря).
  3. Извлечь пары (центр, контекст) — прокатить окно по потоку идентификаторов и выдать по одному обучающему примеру на соседа.

В примере возьмём по два токена с каждой стороны. Размер окна — гиперпараметр; практические реализации могут выбирать меньший эффективный радиус и отбрасывать часть частых слов.

Возьмём предложение «the cat sat on the mat» как пример, зададим окно ±2 и покатим его слово за словом. Когда окно центрировано на sat, соседи — the, cat, on, the, и эта единственная позиция выдаёт четыре пары: (sat, the), (sat, cat), (sat, on), (sat, the). Шаг вперёд к on — и окно находит cat, sat, the, mat, ещё четыре пары. Ещё шаг, ещё четыре, и так далее, пока целый корпус не свернётся в длинный список пар (центр, контекст), порождённых целиком из самого текста, без единой человеческой разметки.

skip-gram windowing · slide the window over a passage, collect the training examples
window half-width:(5 words wide)
The green word is the center. The blue words are its neighbours inside the window. Every center → neighbour pairing is one training example. Step the window across the text (the controls below, or click any word) and they pile up into the training set.
pairs from this center
(the → cat)(the → sat)
training set so far — 2 of 18 pairs
the→catthe→sat

Пары целочисленных индексов можно получить коротким циклом Python:

# Step 1: tokenize.
corpus = "the cat sat on the mat"
tokens = corpus.split()
# ['the', 'cat', 'sat', 'on', 'the', 'mat']

# Step 2: build vocabulary and convert tokens to integer IDs.
vocab = sorted(set(tokens))                       # ['cat', 'mat', 'on', 'sat', 'the']
word2id = {w: i for i, w in enumerate(vocab)}     # {'cat': 0, 'mat': 1, 'on': 2, 'sat': 3, 'the': 4}
V = len(vocab)                                    # 5
ids = [word2id[w] for w in tokens]                # [4, 0, 3, 2, 4, 1]

# Step 3: slide a ±2 window over the ID stream, emit (center, context) pairs.
window = 2
pairs = []
for i, c in enumerate(ids):
    for j in range(max(0, i - window), min(len(ids), i + window + 1)):
        if i != j:
            pairs.append((c, ids[j]))

len(pairs)  # 18 — exactly the (center_id, context_id) pairs the widget above emits.

Предобработка по сути одинакова независимо от того, какой вариант word2vec вы обучаете дальше: токенизация, словарь и оконное скольжение идентичны. Различается только формат выдаваемых примеров: skip-gram упаковывает их парами, а CBOW выдаёт по одному «мешку» контекста плюс его центр на окно. Собственно алгоритм живёт в обучении, и остаток этого раздела разбирает его подробно.

Каждая пара — обучающий пример

Предобработка позади; посмотрим, как алгоритм обучения использует эти пары.

Каждая пара задаёт входное слово и целевую метку. Окно вокруг sat даёт четыре примера с целями the, cat, on, the — три разных слова, причём the встречается дважды. В полном softmax цель используется в потере, а не при прямом вычислении оценок.

Это похоже на MNIST: каждый пример MNIST сопоставляет изображению его цифровую метку, а здесь каждая skip-gram-пара (c, t) сопоставляет центральное слово c (вход) одному контекстному слову t (цель). Для (sat, cat): подать sat, получить предсказанное распределение по словарю, сравнить его с cat, сделать шаг SGD. Затем следующая пара.

Прежде чем что-либо подавать в сеть, нужно представить слово вектором чисел. В MNIST этот шаг почти бесплатен: изображение и так является сеткой интенсивностей пикселей, так что мы просто разворачиваем его в вектор из 784 чисел. У слова нет собственного числового содержания, поэтому мы его придумали на шаге построения словаря выше — у каждого слова уже есть целочисленный индекс от 0 до V−1. Чтобы подать его в сеть, мы разворачиваем этот индекс в one-hot V-вектор — вектор длиной V чисел, весь нулевой, кроме единственной 1 на индексе слова.

Для нашего сквозного словаря из 5 слов (V=5) кодировка выглядит так:

"cat" → [1, 0, 0, 0, 0]
"mat" → [0, 1, 0, 0, 0]
"on"  → [0, 0, 1, 0, 0]
"sat" → [0, 0, 0, 1, 0]
"the" → [0, 0, 0, 0, 1]

К счастью, есть изящный трюк линейной алгебры, позволяющий вообще не строить one-hot-вектор: умножение one-hot на матрицу — это то же самое, что выбрать одну строку этой матрицы (поиск по целочисленному идентификатору слова). Для нашего словаря из 5 слов, где sat стоит на индексе 3, и некоторой матрицы M формы (5, d):

       one-hot for "sat"            M  (5 rows × 3 cols)            result
[ 0  0  0  1  0 ]    ·    [ row 0:  0.21  -0.43   0.15 ]    =    [ 0.33  -0.27   0.84 ]
                          [ row 1:  0.07   0.62  -0.31 ]              (just row 3)
                          [ row 2: -0.55   0.18   0.40 ]
                          [ row 3:  0.33  -0.27   0.84 ]
                          [ row 4: -0.12   0.49  -0.06 ]

Всякое слагаемое, касающееся 0 из one-hot, исчезает, оставляя только вклад строки 3, — так что ответ есть просто строка 3 матрицы M.

# dot product, column by column:
  col 0:  0·0.21  + 0·0.07  + 0·(-0.55) + 1·0.33  + 0·(-0.12)  =  0.33
  col 1:  0·(-0.43) + 0·0.62 + 0·0.18   + 1·(-0.27) + 0·0.49   = -0.27
  col 2:  0·0.15  + 0·(-0.31) + 0·0.40  + 1·0.84  + 0·(-0.06)  =  0.84

Ментальная модель — что мы пытаемся сделать

Для каждой пары (center, target) сравниваем оценку цели с другими словами и настраиваем обе таблицы векторов, уменьшая предсказательную потерю.

При обучении используется ненормированное скалярное произведение, а не косинусное сходство. Косинус делит его на нормы обоих векторов и часто применяется для сравнения после обучения. Softmax переводит оценки в распределение, сохраняя их порядок.

У нас будет две обучаемые матрицы весов — каждая между парой слоёв сети, — вместе дающие каждому слову словаря два d-мерных вектора (по эмбеддингу на роль):

  • E формы (V, d) — каждая строка есть входной эмбеддинг одного слова, используемый, когда слово выступает центром обучающей пары (слово, на которое мы обусловливаемся).
  • E' формы (d, V) — каждый столбец есть выходной эмбеддинг одного слова, используемый, когда слово выступает целью/контекстом, который предсказывают (слово, оцениваемое как кандидат).

E и E' — независимые матрицы параметров, а не транспонированные копии друг друга. Входные векторы просто размещены в строках, выходные — в столбцах. У слова есть отдельный обучаемый вектор для каждой роли.

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

Виджет ниже делает этот шаг конкретным — вычисление близости по измерениям между центральным словом и каждым словом словаря, взаимодействие E и E'. Мы фокусируемся на sat как центре (подсвечен зелёным в E). Прошагайте, чтобы посмотреть, как матричное умножение оценивает его против каждого слова словаря — по одному скалярному произведению на слово, заполняя вектор оценок запись за записью. (Только стадии 1 и 2 — softmax и функция потерь придут парой разделов позже.)

1 of 5 scores computed
E (V=5 × d=3)
row sat = v_c
cat
0.21
-0.43
0.15
mat
0.07
0.62
-0.31
on
-0.55
0.18
0.40
sat
0.33
-0.27
0.84
the
-0.12
0.49
-0.06
E' (d=3 × V=5)
one column per word — v_c is dotted with each in turn
cat
mat
on
sat
the
0.45
-0.31
0.18
-0.22
0.07
0.62
0.15
-0.40
0.33
0.55
-0.20
0.48
0.27
0.11
-0.39
scores (V=5)
cat
mat
on
sat
the
-0.19
·
·
·
·
dot product for cat
score[cat] = E[sat, :] · E'[:, cat]
= 0.33 · 0.45 + (-0.27) · 0.62 + 0.84 · (-0.20)
= 0.149 + (-0.167) + (-0.168)
= -0.187

Оценка равна v_c · v'_t. При выходных векторах в столбцах scores = v_c @ E' вычисляет все оценки сразу. Если они хранятся в строках Ep, эквивалентная запись — scores = Ep @ v_c; цикл Python не нужен ни в одном варианте.

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

Архитектура

Собрав пары, мы по сути запускаем задачу предсказания с метками — по центральному слову предсказать, какое слово из словаря окажется рядом, — так что постановка похожа на MNIST в ключевых чертах: один скрытый слой, softmax по выходным классам, кросс-энтропия против метки. Отличаются масштаб (здесь размер словаря V против 10 классов цифр в MNIST), формат входа (one-hot против плотных вещественных пикселей) и цель (нам нужны обученные эмбеддинги, а не само предсказание).

Архитектурно word2vec (2013) — это двухслойная нейросеть прямого распространения, feed-forward сеть из двух полносвязных слоёв (двух матриц весов E и E'), где данные текут вход → скрытый → выход и нет петель. Это то же семейство, что и MNIST-классификатор, с одним нюансом: скрытый слой чисто линейный, так что он учит кодировки слов, а не выступает нелинейным извлекателем признаков.

Конкретно, вход — V-мерный one-hot центрального слова; скрытый слой имеет d нейронов (например, 300) и чисто линеен (без смещения, без нелинейности); выход имеет V нейронов с softmax по всем V, дающим P(w | c) — вероятность каждого слова словаря при данном центре. Две матрицы E и E', введённые в предыдущем разделе, живут между этими слоями: E — матрица весов вход → скрытый (форма (V, d)), E' — скрытый → выход (форма (d, V)). Это единственные выучиваемые параметры сети; обе стартуют случайными, и после обучения только E уходит как итоговая таблица эмбеддингов слов, а E' выбрасывается.

Полная softmax-архитектура в Keras для небольшого словаря выше:

from tensorflow import keras
from tensorflow.keras import layers

V = len(vocab)  # five words from the example above
d = 3

model = keras.Sequential([
    keras.Input(shape=(1,), dtype='int32'),         # integer ID of the center word
    layers.Embedding(input_dim=V, output_dim=d),    # E:  shape (V, d), the lookup
    layers.Reshape((d,)),                           # (1, d) → (d,)
    layers.Dense(V, use_bias=False),                # E': shape (d, V), the linear layer
    layers.Softmax(),                               # softmax over V vocab scores
])

model.compile(optimizer='sgd', loss='sparse_categorical_crossentropy')

Embedding(V, d) извлекает входные векторы, а Dense(V, use_bias=False) хранит выходные веса. Вход формы (batch, 1) даёт выход (batch, 1, d). Reshape убирает ось однотокенной последовательности перед вычислением оценок. Между обучаемыми слоями нет нелинейной активации.

Softmax превращает V сырых оценок в распределение вероятностей по словарю, а sparse_categorical_crossentropy — стандартная классификационная функция потерь поверх него, тот же softmax-плюс-кросс-энтропия, что использует MNIST, только с V классами словаря вместо 10 цифр. Функцию потерь и её градиент мы разберём подробно в следующих разделах.

Размерность d — гиперпараметр ёмкости, памяти и вычислений. Часто используют несколько сотен измерений, но подходящий размер зависит от корпуса и задачи, а не только от размера словаря.

То, что мы только что описали, — базовый прямой проход, с softmax по всему словарю на выходной стороне.

Прямой проход — поиск, оценивание, softmax

Теперь, когда мы разобрали архитектуру на высоком уровне — one-hot на входе, поиск в скрытом слое, оценки на выходе, — приблизимся и пройдём точно, что происходит, когда один обучающий пример течёт через сеть. Для центрального слова c прямой проход движется слева направо в три стадии: поиск, оценивание и softmax.

Виджет из раздела про ментальную модель уже показал первые две стадии по отдельности — one-hot, умноженный на матрицу, схлопывающийся до чтения строки, а затем строка, умноженная на матрицу, дающая V оценок. Здесь мы их назовём, добавим сверху softmax и проследим числа от начала до конца.

Стадия 1: вход × E → скрытый (поиск). Математически это матричное умножение one_hot(c) @ E, дающее d-мерный скрытый вектор. Вход разрежен — V−1 записей нулевые, — так что почти всякое умножение обращается в ноль, и всё матричное умножение (V, d) схлопывается до чтения одной строки: v_c = E[c].

Этап 2: скрытый вектор × E’ → оценки словаря. scores = v_c @ E' вычисляет скалярное произведение для каждого выходного слова за O(Vd). Обратный проход может дать градиенты всем выходным столбцам и выбранной входной строке.

Матричное умножение никогда не смотрит на целевое слово. Оно использует только v_c = E[sat] и оценивает sat против всего словаря, производя все V оценок разом. Так что эти пять оценок — cat -0.19, mat 0.26, on 0.39, sat -0.07, the -0.45 — идентичны для каждой обучающей пары с центром sat: (sat, cat), (sat, on) и (sat, the) все выполняют одно и то же умножение и приходят к тем же пяти числам. Целевое слово вступает только позже, в функции потерь; прямой проход его никогда не видит.

Стадия 3: softmax → вероятности. V оценок, называемых логитами, — произвольные вещественные числа: они могут быть отрицательными, неограниченными, ни во что конкретное не суммирующимися. Softmax превращает их в P(w | c) — V неотрицательных чисел с суммой 1, предсказанную моделью вероятность того, что слово w находится в контексте c.

На том же словаре из 5 слов виджет ниже проходит все три стадии. Выберите обучающую пару (center, context), затем нажимайте step, чтобы заполнять вектор оценок по одному скалярному произведению за раз; когда все V оценок на месте, softmax превращает их в вероятности.

training pair
E (V=5 × d=3)stage 1 · lookup
row sat = v_c
cat
0.21
-0.43
0.15
mat
0.07
0.62
-0.31
on
-0.55
0.18
0.40
sat
0.33
-0.27
0.84
the
-0.12
0.49
-0.06
E' (d=3 × V=5)stage 2 · score
column cat = v'_w
cat
mat
on
sat
the
0.45
-0.31
0.18
-0.22
0.07
0.62
0.15
-0.40
0.33
0.55
-0.20
0.48
0.27
0.11
-0.39
scores (V=5)
cat
mat
on
sat
the
-0.19
0.26
0.39
-0.07
-0.45
P (V=5)stage 3 · softmax
cat
mat
on
sat
the
0.16
0.25
0.29
0.18
0.12
sum = 1.00

В этом вручную подобранном числовом примере у on наибольшая оценка, примерно 0.39. Числа иллюстрируют вычисление, а не доказывают, что маленькая модель выучила предложение.

В skip-gram эти оценки — скалярные произведения v_c · E'[:, w] для каждого слова словаря w, а получающиеся вероятности — это P(w | c). Вытащив из виджета выше только шаг softmax, пять оценок [-0.19, 0.26, 0.39, -0.07, -0.45] превращаются в полноценное распределение вероятностей с суммой 1:

softmax · the operation that turns scores into a probability distribution
1. raw scores sw (any real number — these are the V scores from the matmul above)
cat
-0.19
mat
0.26
on
0.39
sat
-0.07
the
-0.45
2. apply exp: esw (all positive; large scores blow up, negative scores shrink toward 0)
cat
0.83
mat
1.30
on
1.48
sat
0.93
the
0.64
3. divide by the sum: esw / Σ es (probabilities — non-negative, sum to 1)
cat
0.160
mat
0.251
on
0.286
sat
0.180
the
0.123

Softmax берёт экспоненты оценок и делит их на сумму. Для численной устойчивости сначала вычитают максимальную оценку; вероятности от этого не меняются. Их сумма равна единице, но нормировка сама по себе не гарантирует калибровку предсказаний.

Функция потерь

Потери для одного обучающего примера с целевым словом t — это отрицательный логарифм вероятности, назначенной моделью этой цели:

loss = −log P(t | c)

Это даёт одно число на обучающую пару (c, t) — маленькое, когда softmax навалил вероятность на настоящую цель, большое, когда нет. Это кросс-энтропия с one-hot-меткой, та же функция потерь, что использует MNIST, и градиент через softmax выведен там же.

Конкретно, для пары (sat, cat) с оценками и вероятностями из примера выше:

                  cat    mat    on    sat    the
scores        = [-0.19,  0.26,  0.39, -0.07, -0.45]
probabilities = [ 0.16,  0.25,  0.29,  0.18,  0.12]
target        = cat
P(cat | sat)  = 0.16
loss          = −log(0.16) ≈ 1.83

# what-ifs — how the loss responds to different P(cat):
P(cat) = 0.90  →  loss = −log(0.90) ≈ 0.11   (good prediction)
P(cat) = 0.01  →  loss = −log(0.01) ≈ 4.6    (bad prediction)

Виджет ниже переносит пять softmax-вероятностей сверху и наносит их на кривую −log. Кликните по другому слову, чтобы назначить его целью, — маркер скользит по кривой, и видно напрямую, как цель, которую модель и так предпочитает, почти ничего не стоит, а цель, которую она недооценивает, платит резкую цену.

loss · how a single probability becomes a single number
plug P(cat) into −log: large probability → tiny loss, tiny probability → huge loss
01234500.250.50.751P(target)loss = −log Puniform · (1/V, log V)(0.16, 1.83)
loss = −log P(cat) = −log(0.16) = 1.83

Потеря растёт при приближении вероятности цели к нулю. Но после дифференцирования через softmax градиент по логиту равен P(w) − 1[w=t] и лежит между −1 и 1. Градиенты параметров зависят также от участвующих векторов.

Равномерное распределение по пяти словам даёт любой цели вероятность 0.20 и потерю log(5) ≈ 1.61. В примере у cat около 0.16, поэтому потеря выше. Равномерный выход — полезный ориентир, а не свойство любой случайной инициализации.

Градиент

Градиент похож на MNIST: градиент потерь по каждому логиту равен P(w) − 𝟙[w == t] — предсказанная вероятность минус one-hot-цель. Градиент целевого слова равен P(t) − 1 (отрицательный — толкает его оценку вверх); у всех остальных он равен P(w) (положительный — толкает их оценку вниз, пропорционально тому, сколько вероятности они сейчас держат). Слова, которые модель уже верно считает маловероятными, почти не двигаются; слова, в которых она ошибается, получают больше всего сигнала.

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

gradient · subtracting the one-hot target from the softmax distribution
pick a target word — the model is trying to push P(cat) toward 1
∂loss / ∂score[w] = P(w) − 𝟙[w == t]
P(w)
probability the softmax assigned
cat
0.16
mat
0.25
on
0.29
sat
0.18
the
0.12
−
𝟙[w == t]
one-hot for the true target word
cat
1.00
mat
0.00
on
0.00
sat
0.00
the
0.00
=
gradient
positive → SGD pushes score down · negative → SGD pulls score up
cat
-0.84
mat
0.25
on
0.29
sat
0.18
the
0.12

Обратное распространение переносит эти градиенты по оценкам в E' и E по цепному правилу:

scores = v_c @ E'                       (the forward step we're differentiating)

∂loss / ∂E'[:, w]  =  ( P(w) − 𝟙[w == t] ) · v_c       ← gradient on column w of E'
∂loss / ∂v_c       =  E' @ ( P − one_hot_t )           ← gradient into the hidden vector
∂loss / ∂E[c]      =  ∂loss / ∂v_c                     ← because v_c = E[c]

Два следствия стоит держать в голове, оба из-за one-hot-входа:

Поиск в таблице даёт градиент только строке E[c]; остальные входные строки для этого примера имеют нулевой градиент. Полный softmax задействует каждый столбец E'. Эти градиенты относятся к скалярным оценкам и не гарантируют изменения всех расстояний согласно метафоре «притяжения и отталкивания».

Затем эти градиенты используются для шага градиентного спуска: применить их к весам как E -= lr × ∂L/∂E и E' -= lr × ∂L/∂E'. Это работа оптимизатора, и выбор оптимизатора (обычный SGD, момент, Adam, RMSprop, AdaGrad…) имеет значение только на этом шаге — все они потребляют одни и те же градиенты, но используют их по-разному.

Мини-батчи и эпохи

Разбор выше обрабатывал пары по одной. Большинство обучений нейросетей обобщают это в мини-батчевый SGD — группировать примеры в батчи по B и обрабатывать целый батч за один прямой + обратный проход, ровно тот же мини-батчевый SGD, что и в MNIST, просто усреднённый по B примерам на шаг. Это тот рецепт, который используют BERT, GPT и практически любая современная модель, и именно его описывают пункты ниже.

Оригинальный word2vec и Gensim используют оптимизированное обучение на CPU с асинхронными обновлениями рабочих потоков. Описание мини-батчей с полным softmax ниже — общая схема, а не внутренняя реализация Gensim.

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

По батчам цикл выглядит так:

  1. Прямой проход на батче из B примеров — каждая пара (c, t) течёт через сеть. В векторизованном виде поиск складывает B строк из E в матрицу (B, d), шаг оценивания становится одним матричным умножением (B, d) @ (d, V) → (B, V), а softmax идёт по строкам.
  2. Потери — вычислить кросс-энтропию на пример (та же формула, что при B=1), затем усреднить по батчу в один скаляр.
  3. Обратный проход (backprop). Вычислить градиенты по цепному правилу — чистый анализ от потерь назад к каждому весу, без обновления весов. Выход: тензоры градиентов ∂L/∂E и ∂L/∂E' той же формы, что тензоры весов.
  4. Шаг градиентного спуска — применить градиенты к весам через оптимизатор, ровно как в случае одной пары выше. Повторить для следующего батча.

Прогоните этот цикл по многим батчам за эпоху и по нескольким эпохам корпуса — и строки E и E' осядут в геометрию, которую предпочитает функция потерь.

CBOW — и мост к BERT

CBOW (continuous bag of words) — второй алгоритм word2vec, зеркальное отражение skip-gram. Там, где skip-gram берёт центральное слово и предсказывает целевое как его контекст, CBOW берёт несколько слов как контекст и предсказывает центр — пропущенное слово.

CBOW и маскированное языковое моделирование BERT предсказывают слово или токен по окружению. Но CBOW усредняет небольшой неупорядоченный набор слов, а BERT строит контекстные состояния с учётом позиций через трансформер. BERT — не просто CBOW с большим окном.

Механически CBOW отличается от skip-gram ровно одним шагом усреднения на входе; остальной цикл обучения идентичен. Так что бо́льшая часть разделов про skip-gram переносится без изменений — здесь мы фокусируемся только на отличиях.

Предобработка — мешки контекста вместо пар

Конвейер предобработки тот же, что у skip-gram: токенизировать, построить словарь, прокатить окно по потоку токенов. Меняется форма того, что выдаётся на позицию окна:

Position centered on:    Skip-gram emits (per position):       CBOW emits (per position):
─────────────────────    ──────────────────────────────       ──────────────────────────
the      (pos 0)         (the, cat), (the, sat)                ({cat, sat}, the)
cat      (pos 1)         (cat, the), (cat, sat), (cat, on)     ({the, sat, on}, cat)
sat      (pos 2)         (sat, the), (sat, cat),               ({the, cat, on, the}, sat)
                         (sat, on), (sat, the)
on       (pos 3)         (on, cat), (on, sat),                 ({cat, sat, the, mat}, on)
                         (on, the), (on, mat)
the      (pos 4)         (the, sat), (the, on), (the, mat)     ({sat, on, mat}, the)
mat      (pos 5)         (mat, on), (mat, the)                 ({on, the}, mat)
                         ─────────────────────                  ────────────────
                         total: 18 pairs                         total: 6 examples

Тот же корпус, то же окно: skip-gram производит 18 отдельных обучающих пар (центр, сосед), CBOW — 6 примеров (мешок_контекста, центр). Пройдите это по шагам в виджете:

CBOW windowing · slide the window over a passage, collect the training examples
window half-width:(5 words wide)
The blue words are the context; their vectors get averaged into one input vector. The green word is the center — the prediction target. Each window position emits one example: context → center. Step the window across the text (the controls below, or click any word) and they pile up into the training set.
training example at this position
(cat, sat) → the
training set so far — 1 of 6 examples
(cat sat)→the

Архитектура

Архитектура CBOW — это skip-gram с перевёрнутыми концами: на входе контекстные слова, на выходе центральное. Те же две матрицы весов E и E', та же размерность d, тот же softmax по V. Единственное архитектурное отличие — на входной стороне: skip-gram смотрит одну строку E (центр), CBOW смотрит C строк (по одной на контекстное слово) и усредняет их в единственный d-мерный скрытый вектор h:

Skip-gram:  h = E[c]                                  (one row read)
CBOW:       h = mean(E[c_1], E[c_2], ..., E[c_C])     (C rows read, then averaged)

Визуализировано ниже — C one-hot-входов, C соответствующих чтений строк в E и шаг усреднения, дающий h:

CBOW · dim = d, vocab = V
input
the
1
0
0
…
cat
0
1
0
…
on
0
0
1
…
the
1
0
0
…
the C context words, one-hot each
shape: C × V
lookup C rows
input embedding E
learned table
shape: V × d
the C rows
average them → h
mean ( + )
↓
0.05
-0.11
0.22
0.31
-0.04
0.18
-0.09
0.12
h = mean(vthe, vcat, …) ∈ ℝd
shape: d

Усреднение и даёт «мешку слов» его имя: порядок слов внутри окна отбрасывается, контекст становится мультимножеством. На каждом шаге обучения поиски происходят по текущим, «на лету», значениям E — строки, которые вы читаете на прямом проходе, это те же строки, которые вы обновляете на обратном, как и в любом SGD-обучении нейросети. Как только h вычислен, всё ниже по течению — scores = h @ E', softmax, кросс-энтропия, обратное распространение, SGD по E и E' и сокращение через негативное сэмплирование — идентично skip-gram.

Обучение — три небольших отличия от skip-gram

Прямой проход, функция потерь, градиент, мини-батчинг и сокращение через негативное сэмплирование переносятся из skip-gram без изменений. Три отличия стоит держать в уме:

CBOW делает одно предсказание на окно, skip-gram — на каждое вхождение контекстного слова, поэтому у CBOW часто меньше работы выходного слоя. При усреднении каждое входное вхождение получает grad_h / C; вклады повторных индексов суммируются. Полный softmax обновляет все выходные столбцы, negative sampling — выбранные векторы. Скорость и качество зависят от данных и настроек.

Как word2vec обучают на практике — Gensim

Word2Vec в Gensim поддерживает построение словаря, выборку контекста, negative sampling и получение векторов. sentences — итерируемый набор токенизированных предложений. Этот маленький пример проверяет API; данных недостаточно для полезных смысловых аналогий.

from gensim.models import Word2Vec

sentences = [
    "the king sat in the palace".split(),
    "the queen sat in the palace".split(),
    "the king and the queen ruled".split(),
]
model = Word2Vec(sentences, vector_size=50, window=2, min_count=1,
                 sg=1, negative=5, hs=0, workers=1, seed=42, epochs=20)
v_king = model.wv["king"]

Обучение на CPU и доступ к памяти

При negative sampling одна пара задействует лишь несколько векторов. Многие реализации хранят обе таблицы по строкам; в предыдущем столбцовом формате выход извлекается как E'[:, w]. Примерная работа на пару:

1. Read the input vector:       E[center]
2. Read output vectors:        target + k sampled negatives
3. Compute scores:             k+1 dot products, each length d
4. Accumulate gradients:       one input row and sampled output vectors
5. Apply the SGD update:       repeated indices receive summed contributions

Две таблицы float32 занимают 2 × V × d × 4 байт: около 2,4 GB для миллиона слов и 300 измерений, без словаря и состояния оптимизатора. Небольшие нерегулярные обновления подходят CPU, но скорость GPU зависит от батчей, размещения памяти и реализации. Универсального правила о превосходстве CPU для word2vec нет.

Альтернативы полному softmax

Объяснительная модель выше оценивает весь словарь за O(Vd) на пример. Пример Gensim вместо этого использует negative sampling. Два известных способа избежать вычисления по всему словарю:

Negative sampling заменяет словарную классификацию выбранными бинарными метками за O((k+1)d) для положительного и k отрицательных примеров. Цель меняется; это не несмещённое приближение градиента softmax. Следующая статья выводит потерю и показывает рабочее обновление.

Иерархический softmax размещает слова в листьях бинарного дерева, назначая вектор каждому внутреннему узлу. Вероятность слова — произведение нужных вероятностей выбора левой или правой ветви вдоль пути, по одному скалярному произведению на узел. У сбалансированного дерева пути порядка log₂V; дерево Хаффмана даёт частым словам более короткие пути. Вероятности листьев образуют нормированное распределение.

Иерархический softmax доступен в Gensim, например при hs=1, negative=0. Выбор зависит от стоимости обучения и нужных представлений; это не устаревший метод, который всегда проигрывает negative sampling.

Где статические эмбеддинги ломаются

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

Полисемия

Рассмотрим два предложения:

  • Я внёс чек в банк.
  • Мы устроили пикник на берегу реки.

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

cosine similarity of bank with two sense clusters
"bank" as financial institution
money
0.436
loan
0.418
account
0.403
deposit
0.451
interest
0.409
"bank" as river bank
river
0.273
shore
0.284
water
0.159
creek
0.145
flood
0.199
the financial sense dominates — that's the corpus skew, not a deep fact about the word. but both senses pull above zero from the same vector, because there's only one vector to give. a static embedding can't tell the model which sense is meant in any specific sentence; that's left to whatever sits on top.
cosine similarities computed on glove-wiki-gigaword-300

Виджет сравнивает bank с финансовыми и речными словами в одной предобученной модели. Сходства отражают её корпус и обучение. Последующая модель, видящая окружающие слова, всё ещё может различить значения; один статический вектор этого не делает.

Нечувствительность к синтаксису

Представление «мешок векторов» выбрасывает порядок слов. Предложения:

  • Собака кусает человека.
  • Человек кусает собаку.

имеют одинаковый набор слов, но разный порядок токенов. Усреднение статических векторов даёт одно представление. Если сохранить последовательность векторов, порядок остаётся доступен RNN, свёртке или трансформеру.

Покрытие словаря и адаптация

«Статический» значит независимый от контекста, а не навсегда замороженный. Векторы можно дообучать или обучать заново. В обычной таблице word2vec нет записи для неизвестного слова; нужна стратегия неизвестных токенов или другое представление для расширения словаря.

fastText частично решает проблему неизвестных слов, составляя векторы из символьных n-грамм. Представления остаются независимыми от контекста; обработка нового написания и определение смысла в предложении — разные задачи.

Что дальше

ELMo использовал состояния двунаправленной языковой модели для контекстных представлений; BERT использует трансформерный энкодер с предсказанием маскированных токенов. Продолжение темы целевых функций — negative sampling и контрастивное обучение, где различаются сигмоидная потеря word2vec и softmax-потеря CLIP.