Эмбеддинги слов — word2vec и векторы, которые что-то значат
Многие, кто знакомится с ИИ сегодня, начинают с вершины стека — трансформеры, головы внимания, файнтюнинг, чат-модель, которая уже вроде бы всё понимает. Это отличное место, чтобы технологию использовать. И слишком высокоуровневое место, чтобы её изучать: большие модели — это глубокие стопки идей, накопленных более чем за пятьдесят лет, и почти каждая из этих идей меньше и куда яснее видна по отдельности. Чтобы понять всё это, лучше собирать снизу из деталей, чем стартовать с готового стека, — а деталь, которая важнее всех, та, вариациями которой являются и модели в стиле GPT, и BERT, и любая поисковая система, — это идея, что слова можно превратить в векторы, геометрия которых кодирует смысл.
Вы наверняка уже видели знаменитый пример king − man + woman ≈ queen — вероятно, самый зашкриншоченный результат в NLP. Посмотрите на виджет ниже.
Он демонстрирует эту арифметику геометрически: каждая сторона картинки — своя маленькая система координат,
но направление «королевскости», вычисленное в системе A как v(king) − v(man), указывает одинаково всюду в пространстве эмбеддингов.
Перенесите этот вектор в систему B, приложите его к woman — и его конец приземлится на queen.
Так что king − man выделяет направление, а + woman применяет его к другой опорной точке.
Нажимайте кнопку step, чтобы посмотреть построение по шагам.
Виджет демонстрирует плотное векторное представление слов: каждое слово — точка в многомерном пространстве, а геометрия между точками кодирует семантические отношения. Никто не задавал эти значения вручную, и никто не говорил модели, что «королевскость» или «род» вообще существуют как понятия — тут нет размеченных осей, нет категориальных тегов, никто не помечал king как «королевское», а queen как «женское». Геометрия целиком выпадает из обучающей задачи, и разбор того, как это происходит, и составляет бо́льшую часть статьи.
Впрочем, бо́льшую часть истории NLP слова вообще не были векторами — они были самостоятельными символами, целочисленными идентификаторами или one-hot-индикаторами, без всякой встроенной связи друг с другом. Всё семантическое приходилось привинчивать ниже по конвейеру: сконструированные признаки, статистика по счётчикам, лексиконы вроде WordNet. Модели работали, но плохо обобщались между словами: то, что система выучивала про cat, вообще не переносилось на kitten.
Эта слепая зона была вшита в господствовавший рецепт эпохи. Документ превращался в TF-IDF матрицу «документ — термин» — одна разреженная строка на документ, один столбец на термин словаря, каждая ячейка взвешивает, насколько термин характерен для документа, — после цепочки предобработки из токенизации, стемминга и удаления стоп-слов. Эта матрица подавалась признаками в классический ML-классификатор — SVM, логистическую регрессию, наивный байес, случайный лес — для задач вроде анализа тональности, кластеризации документов или ранжирования по релевантности. Конвейер работал, но нёс тот же изъян в самих костях: каждый термин — независимый столбец без всякого представления о том, какие термины значат похожее, а словарь из 30 000+ терминов делает вектор каждого документа огромным и почти целиком нулевым.
Плотные эмбеддинги — это то, что пришло на смену. Каждый токен становится вектором из нескольких сотен вещественных чисел, полностью выученных из данных, и геометрия этого пространства выстраивается по смыслу: похожие слова оказываются рядом друг с другом, а пары связанных слов стоят на похожих смещениях — именно это и заставляет работать арифметику выше.
Вот здесь и появляется word2vec. Представленный в 2013 году, он сделал плотные эмбеддинги практичными: простая мелкая архитектура, которая училась непрерывным представлениям слов прямо из сырого текста, но превосходила более тяжёлые модели на целом ряде синтаксических и семантических задач — анализ тональности, обнаружение перефразирований, извлечение отношений. Часть привлекательности была в чистой эффективности — обучение на миллиардах слов было по карману обычному железу, — и это сочетание сильных результатов с низкой стоимостью превратило эмбеддинги слов из исследовательской диковинки в стандартный строительный блок современного NLP.
Смысл этим векторам придаёт процесс обучения. При инициализации каждое слово — просто короткий массив случайных чисел, ничего семантического там ещё нет. Смысл они приобретают исключительно как побочный эффект того, что их заставляют хорошо справляться с самообучающейся предсказательной задачей — предсказать контекст слова (skip-gram) или слово по его контексту (CBOW). Никто не говорит модели: «king и queen связаны». Но чтобы точно предсказывать контексты king и точно предсказывать контексты queen, когда эти два распределения контекстов сильно пересекаются, единственный способ для крошечной сети преуспеть — дать king и queen похожие векторы. Близость в пространстве эмбеддингов и есть механизм модели, позволяющий делить предсказательные свидетельства между словами, которые ведут себя одинаково. Геометрия выпадает из оптимизации.
От гипотезы к геометрии
Как гипотеза о значении слов в итоге порождает настоящую векторную геометрию — где похожие слова оказываются рядом, а устойчивые отношения становятся устойчивыми векторными смещениями? Рассуждение состоит из четырёх шагов, каждый из которых вынуждает следующий: посылка (сама дистрибутивная гипотеза), самообучающаяся задача, построенная прямо на этой посылке, представительский бутылочный горлышко, ограничивающее способы решения задачи, и геометрия, возникающая на выходе. Пройдите их по порядку — и скачок от «у слов есть значение» к «слова становятся векторами со структурой» перестанет быть загадочным.
Начинаем с дистрибутивной гипотезы: значение слова хорошо приближается распределением контекстов, в которых оно встречается. Слова, появляющиеся в одинаковых типах предложений — couch и sofa, huge и enormous, — значат похожее, что тихо переформулирует вопрос «что слово значит» в вопрос «рядом с чем оно появляется», а на это можно ответить из одного лишь сырого текста, без словаря и человеческой разметки.
Из этой посылки вытекает самообучающаяся задача: возьмите слово и предскажите слова, которые его окружают, — или возьмите окружающие слова и предскажите то, что посередине. Поскольку гипотеза утверждает, что смысл несут именно соседи, преуспеть в задаче — значит вынудить модель кодировать смысл; и подготовка ничего не стоит, потому что правильные ответы уже лежат в тексте.
Затем идёт ограничение на эту задачу — бутылочное горлышко. Всё представление слова должно уместиться в один короткий вектор — скажем, триста чисел, — и это единственное место, где может жить его информация. Триста чисел — не так уж много: и близко не хватит, чтобы дать каждому слову собственное независимое описание. Так что, чтобы хорошо предсказывать для всех сразу, модели приходится экономить: представлять слова общими переиспользуемыми признаками и ставить слова, играющие одинаковую роль, почти в одни и те же координаты. Похожие слова оказываются рядом не потому, что кто-то этого требовал, а потому что их скучивание — единственный способ всё уместить. Уберите бутылочное горлышко — пусть вектор растёт до размера самого словаря, чтобы каждое слово могло заявить собственную выделенную размерность и никогда ни с кем не делиться, — и давление исчезнет: модель просто запомнит, какие слова с какими встречаются, по слову за раз, и не выучит ничего, что переносится на следующее.
На выходе получается геометрия: слова с похожими распределениями контекстов получают похожие векторы, а устойчивые реляционные шаблоны — столица-чего-то, множественное число, прошедшее время — становятся устойчивыми векторными смещениями. Смысл в итоге закодирован как положение и направление в пространстве — это и показывает картинка в начале.
То, через что вы только что прошли — предсказать контекст, продавить через бутылочное горлышко, считать геометрию, — не уникально для word2vec. Каждая модель эмбеддингов после word2vec разыгрывает ту же комбинацию. Самообучающееся предсказание через представительское бутылочное горлышко — это то, как BERT учит свои представления, как энкодеры предложений за RAG учат свои и как строится слой эмбеддингов внутри любой современной LLM.
В сегодняшних архитектурах NLP «эмбеддинг» может означать три довольно разные вещи — все построены одинаково, через самообучающееся предсказание сквозь бутылочное горлышко, но каждая применена к более крупной единице текста, чем предыдущая:
- Статические эмбеддинги слов — word2vec, GloVe, fastText. Один фиксированный вектор на слово, извлекаемый из таблицы поиска и замороженный после обучения. Порядок слов выбрасывается, а
bankполучает единственный вектор, усредняющий все его значения. - Контекстные эмбеддинги — ELMo, затем BERT и его потомки. Один вектор на слово в том виде, как оно встречается в предложении:
bankв «river bank» иbankв «bank account» выходят разными, потому что окружающие токены переформовывают вектор. Обучающая задача сдвигается к маскированному языковому моделированию — предсказать скрытый токен по его двустороннему контексту, пропустив через глубокий энкодер, а не через мелкое усреднение, — но базовый принцип, самообучающееся предсказание через бутылочное горлышко, не меняется. - Эмбеддинги предложений / пассажей — Sentence-BERT и современные поисковые модели. Сворачивают контекстную модель до одного вектора на предложение или документ. Это движок retrieval-augmented generation: закодировать корпус один раз, кодировать каждый запрос и возвращать ближайшие фрагменты по косинусной близости — ровно та геометрическая ставка, демонстрацией которой служит картинка выше.
Возможно, вы задаётесь вопросом, где здесь генеративные LLM — ChatGPT, Claude, Llama. Не на отдельном уровне: они содержат все три. Самый нижний слой каждой из них — статическая таблица эмбеддингов слов, ровно как у word2vec: заходит идентификатор токена, выходит выученный вектор. Каждый трансформерный слой над ним производит контекстные эмбеддинги, как в BERT. LLM делает LLM не новый вид эмбеддингов, а то, что вы считываете её распределение следующего токена и сэмплируете из него, превращая машину представлений в машину генерации.
Эта статья — про первый уровень, статические эмбеддинги слов. Мы начнём с идеи, лежащей в основе всего (дистрибутивная гипотеза), пройдёмся по внутреннему устройству skip-gram и CBOW, исследуем знаменитую аналогию king–queen на настоящем предобученном словаре и дойдём до места, где статические эмбеддинги перестают работать, — а именно там подхватывают BERT и второй уровень в следующей статье. Третий уровень — эмбеддинги предложений и пассажей за retrieval-augmented generation — придёт позже в серии.
Дистрибутивная гипотеза
Ещё в 1957 году лингвист Дж. Р. Фёрт знаменито написал: «Слово познаётся по компании, которую оно водит». В этом вся идея. Слова, появляющиеся в похожих контекстах, имеют похожие значения. Чтобы понять почему, рассмотрим три предложения с пропущенным словом:
- ___ залаял на почтальона.
- ___ мурлыкал у меня на коленях.
- ___ улетел на юг зимовать.
Вам не нужно знать пропущенные слова, чтобы понимать, что они обозначают разные виды животных. Контекст — слова вокруг пропуска — сужает то, что сюда подходит. Это одна половина идеи: контекст предсказывает слово.
Вторая половина переворачивает это. Соберите все слова, которые устойчиво заполняют слот в «___ залаял»: собака, щенок, пёс, ретривер. Все они подходят, потому что все означают нечто собачье. Любое слово, устойчиво заполняющее «___ мурлыкал» — кот, котёнок, кошка, — означает нечто кошачье. Значит, слова, заполняющие одинаковые слоты в одинаковых типах предложений, должны означать похожее. Именно это направление и окупается для представлений: модель, обученная предсказывать контекст, обязана дать словам с похожими контекстами похожие векторы, потому что только так она сможет хорошо предсказывать их все разом.
Дистрибутивная гипотеза говорит: определяйте слово через его контексты. Постройте представление, которое ставит слова близко, если их распределения контекстов похожи. Такое представление закодирует семантическую близость, синтаксическую роль и удивительный объём знаний о мире — и всё это без какой-либо человеческой разметки, из одного сырого текста.
Вопрос в том, как вычислить это эффективно. Классический ответ, восходящий к началу 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 — в пределах определённого окна. Диагональ всегда нулевая, поскольку слово не встречается совместно с самим собой.
Матрица строится скольжением окна по корпусу и подсчётом.
В каждой позиции с центральным словом i смотрим на слова в его окне ±W и для каждого соседа j в этом окне увеличиваем M[i][j] на единицу. Прокрутите виджет от начала до конца — и матрица заполнится ячейка за ячейкой: один проход по корпусу, и все совместные встречаемости записаны.
Это первый шаг конвейера «посчитай, потом факторизуй», и над ним есть ещё два. Форма вектора слова существенно меняется на каждой стадии:
Стадия 1 — сырые счётчики. Матрица выше как есть. Каждая строка длиной V чисел, по слоту на каждое слово словаря, — это уже вектор слова, просто дико раздутый. Каждая запись буквально означает «сколько раз это конкретное слово словаря появлялось в окне ±2 данного слова по всему корпусу». Строка для cat — это [1, 0, 1, 1, 0]. В игрушечном корпусе всего шесть токенов, поэтому числа крошечные, но важна структура при масштабе.
Стадия 2 — поправка на случайность. В сырых счётчиках доминирует частота: the в реальном корпусе встречается рядом с cat часто, но только потому, что the встречается рядом со всем. Чтобы извлечь настоящий сигнал, каждую совместную встречаемость нужно дисконтировать тем, что предсказала бы одна лишь случайность: частым словам не должно доставаться заслуги за соседство со всем подряд. Стандартное решение — PMI (поточечная взаимная информация): логарифм отношения, который велик, когда два слова встречаются вместе чаще, чем предсказала бы случайность, мал, когда реже, и равен нулю, когда их совместная частота совпадает с той, что дали бы два независимых слова с такими частотами. Форма матрицы остаётся (V, V); меняются только значения — с сырых счётчиков на скорректированные на случайность оценки.
Стадия 3 — сжатие. Даже после поправки на случайность матрица всё ещё (V, V) — дико широка, чтобы служить рабочей таблицей векторов слов. Стандартное решение — SVD (сингулярное разложение): найти d направлений наибольшей вариации в матрице и спроецировать каждую строку из V измерений в d — тот же приём «оставь главные направления вариации и спроецируй на них», который PCA делает с двумерным облаком, только здесь применённый к матрице совместной встречаемости V × V:
raw cat vector (V long): [1, 0, 1, 1, 0]
PMI-weighted (V long): [−0.4, 0, 0.7, 0.3, 0] ← log-ratios per vocab word (illustrative)
SVD-compressed (d long): [0.21, -0.34, 0.18, …] ← d abstract latent dimensionsСжатый вектор — это d вещественных чисел (несколько сотен), и размерности больше не означают «совместная встречаемость с одним конкретным словом». Каждая размерность — линейная комбинация всех V словных осей, выученное латентное направление, схватывающее один доминирующий шаблон вариации совместной встречаемости по корпусу. Размерность 47 больше не значит, как часто cat встречался рядом со snowboard; теперь она измеряет, насколько общий шаблон совместной встречаемости cat согласован с 47-м по значимости направлением в данных.
Эта сжатая таблица (V, d) — плотные вещественные числа, по строке на слово — структурно идентична векторам word2vec, о которых пойдёт речь дальше. Отличается путь: «посчитай, потом факторизуй» против «обучи сеть напрямую». Оба маршрута приходят по сути в один пункт назначения — это и формализуют математически Леви и Голдберг (2014).
А значит, маршрут «посчитай, потом факторизуй» производит ту же геометрию, что поддерживает king − man + woman ≈ queen — знаменитая арифметика аналогий выпадает на любом из путей.
Хорошо настроенные PPMI-SVD модели догоняют word2vec на стандартных бенчмарках аналогий с точностью до пары пунктов; тот же пункт назначения, другая инженерия.
Два десятилетия этот рецепт «посчитай, потом факторизуй» лежал в основе LSA (латентно-семантический анализ, 1990 — матрица «слово — документ», для информационного поиска) и HAL (hyperspace analog to language, 1996 — матрица «слово — слово», в когнитивной науке для моделирования человеческой семантической памяти). Оба производили векторы слов как побочный продукт других целей; word2vec (2013) стал первым методом, сделавшим таблицу векторов слов явной целью.
Общей ценой для всех была чистая масштабность, и росла она быстрее, чем сам размер словаря. При реальном V матрица — монстр V × V: миллион слов на каждой стороне — это триллион ячеек, почти все нулевые, потому что большинство пар слов никогда не встречаются вместе. Хранить её тяжело; факторизовать классическим SVD ещё тяжелее. Помимо сырых вычислений, конвейер — это цепочка явных, вручную настраиваемых шагов: посчитать, взвесить, факторизовать, обрезать, отмасштабировать, — и у каждого свои ручки, неочевидно взаимодействующие с остальными. К началу 2010-х это был рецепт, которым пользовались все и который никто не любил.
В 2013 году прорывной метод под названием word2vec выдал плотные векторы из нескольких сотен измерений с теми же семантическими свойствами — и вообще без построения этой матрицы. Вместо того чтобы считать совместные встречаемости и потом сжимать результат, он учит векторы напрямую, обучая крошечную нейросеть на предсказательной задаче. (Годом позже GloVe вернулся к построению матрицы и её лобовой факторизации; fastText расширил word2vec символьными n-граммами. Статья о семействе статических эмбеддингов разбирает эти альтернативы и их связь друг с другом.
word2vec
Статья про word2vec предложила два подхода, которые полностью обходят матрицу совместной встречаемости: они дают ту же таблицу эмбеддингов (V, d), ни разу не построив сетку V × V. Вместо того чтобы считать совместные встречаемости, а затем сжимать матрицу через SVD, обучите крошечную нейросеть предсказывать слова вокруг каждого слова, в любом из двух направлений, а когда обучение остановится, считайте векторы слов с весов сети. Предсказание настоящее, но нужно вам не оно: оно нужно лишь затем, чтобы вытолкнуть из сети структурированные векторы. Вы оставляете таблицу выученных эмбеддингов слов, а остальное выбрасываете. Тот же пункт назначения, что у LSA/HAL, совсем другая инженерия: потоково обрабатывать пары слов по одной и подталкивать векторы по ходу вместо построения матрицы на триллион ячеек и её факторизации.
Такая постановка обучения задаёт word2vec и жёсткое ограничение области действия — он работает с отдельными словами, а не с предложениями. Модель производит один фиксированный вектор на слово и ничего больше: ни представления предложения, ни осведомлённости о порядке слов, ни композициональности между словами фразы. В течение десятилетия между выходом word2vec и захватом власти трансформерами стандартный конвейер NLP закрывал этот пробел, надстраивая LSTM (или похожую рекуррентную сеть) поверх эмбеддингов word2vec: эмбеддинги несли значение каждого слова, а LSTM разбиралась, как эти значения складываются в предложении. Почему это разделение в итоге рухнуло — тема заключительных разделов.
Два алгоритма, введённых word2vec, — skip-gram и CBOW, альтернативные рецепты, гоняющие предсказание в противоположных направлениях. Используют один или другой, никогда оба:
- Skip-gram. По центральному слову предсказать слова в небольшом окне вокруг него. Мы подаём центральное слово на вход и предсказываем каждое из окружающих контекстных слов по очереди — каждая пара
(центр, контекст)является отдельным обучающим примером, так что одно и то же центральное слово переиспользуется по разу на соседа. - CBOW (continuous bag of words). Наоборот: по словам в окне предсказать центральное. Мы подаём набор контекстных слов и предсказываем единственное слово посередине. Именно эта форма «заполни пропуск» позже наследуется и масштабируется маскированным языковым моделированием BERT.
Оба делают одно и то же, просто гоняя предсказание в противоположных направлениях. Мы пройдём skip-gram от начала до конца, а затем вернёмся к CBOW.
Обучающие данные и one-hot-входы
Весь конвейер skip-gram чисто делится на две стадии с резкой границей: предобработка превращает сырой текст в список обучающих примеров, а затем обучение прогоняет эти примеры через нейросеть.
Сначала посмотрим на предобработку — превращение сырого текста в длинный список целочисленных пар. Никакая нейросеть здесь пока не участвует:
- Токенизировать корпус — разбить текст на список словных токенов (разделённых пробелами, обычно в нижнем регистре; word2vec использует токены уровня слов, а не подслов).
- Построить словарь — назначить каждому различному токену целочисленный идентификатор; позже он будет служить индексом токена в 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, ещё четыре пары. Ещё шаг, ещё четыре, и так далее, пока целый корпус не свернётся в длинный список пар (центр, контекст), порождённых целиком из самого текста, без единой человеческой разметки.
По окончании предобработки у вас есть последовательность целочисленных пар (c, t), где c — идентификатор центрального слова, а t — идентификатор целевого (контекстного), готовая к подаче. Вся стадия умещается примерно в дюжину строк NumPy на нашем сквозном примере:
# 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, — это четыре отдельных обучающих примера с одинаковым входом sat и четырьмя разными целями предсказания (the, cat, on, the). Через прямой проход идёт только вход; цель вообще не участвует в вычислениях сети — к ней обращаются только на шаге функции потерь, чтобы оценить, насколько предсказанное распределение ей соответствует.
Это похоже на 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 имеет длину V — его длина есть размер словаря, около миллиона для настоящего word2vec. Так что каждое слово буквально становится вектором на миллион чисел, где 999 999 записей — нули и только одна — единица. Сразу очевидно, что это чудовищно расточительное представление: почти всё хранилище и почти все умножения работают над нулями, не вносящими вклада.
Что сеть делает с этим one-hot? Она передаёт его прямиком первому слою как вход, который скалярно умножает его на матрицу весов слоя — запись за записью, с суммированием. При V около миллиона это матричное умножение «миллион на d» на каждый обучающий пример, почти целиком состоящее из умножений на ноль. Сделанное буквально, это огромное количество впустую потраченной арифметики.
К счастью, есть изящный трюк линейной алгебры, позволяющий вообще не строить 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Так что в коде мы просто храним целое 3 (индекс в словаре) и используем его как прямой поиск строки — конкретно увидим это в разделе про прямой проход. Математически one-hot V-вектор и целочисленный индекс несут одну и ту же информацию; one-hot мы используем для математики, потому что с ним линейная алгебра выглядит чисто, а целое — для кода, потому что оно в V раз дешевле.
Ментальная модель — что мы пытаемся сделать
Прежде чем пристально смотреть на архитектуру сети, стоит зафиксировать, чего сеть на самом деле пытается достичь.
Ключевая идея в одну строку: имея список пар (центр, цель), для каждой пары вычислить близость по измерениям между центральным словом и каждым словом словаря — а затем продолжать подталкивать веса, пара за парой, чтобы цели постепенно поднимались наверх рейтинга близости.
Близость измеряется косинусной близостью между векторами слов (эмбеддингами), а softmax ранжирует каждое слово словаря по предсказанной вероятности быть целью. Остаток раздела распаковывает, что это за веса, как сеть это реализует и почему прогон этого цикла по корпусу порождает осмысленную геометрию.
У нас будет две обучаемые матрицы весов — каждая между парой слоёв сети, — вместе дающие каждому слову словаря два d-мерных вектора (по эмбеддингу на роль):
Eформы(V, d)— каждая строка есть входной эмбеддинг одного слова, используемый, когда слово выступает центром обучающей пары (слово, на которое мы обусловливаемся).E'формы(d, V)— каждый столбец есть выходной эмбеддинг одного слова, используемый, когда слово выступает целью/контекстом, который предсказывают (слово, оцениваемое как кандидат).
Обратите внимание на разницу форм: E' — по сути транспонированная E, те же V × d чисел на матрицу, но разложенные как (d, V), так что слова идут столбцами, а не строками.
Так что одно слово w имеет и строку в E (когда оно центр), и столбец в E' (когда оно цель) — два независимых обучаемых d-мерных вектора, по одному на роль.
Они получают независимые градиентные обновления и приходят к разным значениям.
Асимметрия намеренна: центр играет в предсказательной задаче иную роль, чем цель (один — обусловливающий контекст, другой оценивается на правдоподобность), и модель выразительнее, когда может выучить для этих двух ролей разные векторы, чем когда вынуждена их делить. После обучения E — это то, что уходит в релиз как итоговая таблица эмбеддингов слов, а E' выбрасывается.
Виджет ниже делает этот шаг конкретным — вычисление близости по измерениям между центральным словом и каждым словом словаря, взаимодействие E и E'. Мы фокусируемся на sat как центре (подсвечен зелёным в E). Прошагайте, чтобы посмотреть, как матричное умножение оценивает его против каждого слова словаря — по одному скалярному произведению на слово, заполняя вектор оценок запись за записью. (Только стадии 1 и 2 — softmax и функция потерь придут парой разделов позже.)
Оценка пары (c, t) — это просто v_c · v'_t, скалярное произведение входного эмбеддинга c на выходной эмбеддинг t, то есть ненормированная косинусная близость.
Это же и причина, по которой E' хранится транспонированной (столбцы = выходные эмбеддинги), а не как ещё одна таблица (V, d): хранение их столбцами означает, что scores = v_c @ E' вычисляет скалярное произведение v_c с выходным эмбеддингом каждого слова за одно матрично-векторное умножение. Будь E' формы (V, d), как E, вам пришлось бы делать цикл или транспонирование, чтобы получить те же V скалярных произведений. Форма (d, V) — ровно та раскладка, которая делает «вычислить близость по всем словам разом» однострочной операцией.
Это та же операция, которой MNIST оценивал цифры: у каждого выходного класса был свой шаблон признаков, скалярно умножаемый на скрытый вектор, чтобы оценить, насколько изображение соответствует классу. word2vec разыгрывает ту же комбинацию, где столбцы E' служат пословными шаблонами, а v_c — вектором признаков центрального слова; разница лишь в том, что здесь признаки — выученные семантические измерения (королевскость, множественность, род), а не интерпретируемые вручную фрагменты (края, штрихи, петли).
С учётом этого архитектура из следующего раздела — просто простейшая возможная нейросеть, которая вычисляет близость (v_c · v'_t) для каждой пары-кандидата (c, t) и обновляет веса в двух матрицах градиентным спуском.
Повторённое миллиарды раз по корпусу, это порождает геометрию, где входной эмбеддинг каждого слова стоит рядом с выходными эмбеддингами слов, с которыми оно встречается совместно, и транзитивно — где слова, делящие одних соседей, оказываются рядом друг с другом. Никто не говорит king и queen быть похожими; они становятся похожими, потому что обоих тянет к royal, throne, crown, monarch. То же с cat и dog: они делят соседей вроде pet, fur, tail.
Архитектура
Собрав пары, мы по сути запускаем задачу предсказания с метками — по центральному слову предсказать, какое слово из словаря окажется рядом, — так что постановка похожа на 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' выбрасывается.
Чтобы сделать структуру слоёв конкретной, вся базовая сеть — это шесть строк Keras:
from tensorflow import keras
from tensorflow.keras import layers
V = vocab_size # e.g. 1_000_000
d = embedding_dim # e.g. 300
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 и Dense, оба линейные, без активации между ними.
Embedding(V, d) — это E, кератовское название для поиска строки в таблице (V, d) по целочисленному индексу. Dense(V, use_bias=False) — это E', обычная линейная проекция (d, V).
Reshape между ними убирает лишнее измерение длины 1, которое добавляет кератовский Embedding. Это причуда Keras: Embedding рассчитан на последовательности токенов и добавляет лишнюю ось, даже когда мы подаём по одному токену.
Softmax превращает V сырых оценок в распределение вероятностей по словарю, а sparse_categorical_crossentropy — стандартная классификационная функция потерь поверх него, тот же softmax-плюс-кросс-энтропия, что использует MNIST, только с V классами словаря вместо 10 цифр. Функцию потерь и её градиент мы разберём подробно в следующих разделах.
Число измерений эмбеддинга слова d — гиперпараметр, для выбора которого нет формулы; бо́льшую часть работы делает соглашение.
Заезженное умолчание — d = 300, то, что статья word2vec 2013 года использовала на Google News, и то, что GloVe поставляет как самый крупный предобученный вариант.
На практике вы увидите d = 50–100 для лёгких и краевых применений, d = 200–300 как золотую середину и редко что-то больше.
Причина, по которой d должно быть много меньше V, — аргумент о бутылочном горлышке из начала статьи: если бы каждое слово могло растечься в собственную выделенную размерность, модель запоминала бы совместные встречаемости вместо построения общих признаков, и никакой геометрии бы не возникло.
То, что мы только что описали, — базовый прямой проход, с 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’ → V оценок. Прямой проход второго слоя: scores = v_c @ E', дающий V вещественных чисел (по одному на слово словаря). Это настоящее матрично-векторное умножение (d, V) — без сокращений, без разреженности, которую можно эксплуатировать. Каждая оценка w равна v_c · E'[:, w] — скалярное произведение v_c с w-м столбцом E', по одному такому произведению на слово словаря. Именно здесь и происходит бо́льшая часть работы матричного умножения: каждый столбец E' вносит вклад в прямой проход и получает градиент на обратном, так что почти все вычисления шага — и почти всё обучение — живут на стороне скрытый → выход.
Матричное умножение никогда не смотрит на целевое слово. Оно использует только 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 превращает их в вероятности.
Пять скалярных произведений, пять сырых оценок, затем softmax. На стадии оценок выше всех оказывается on (+0.39), что логично — on действительно стоит рядом с sat в «the cat sat on the mat», — и после softmax он же несёт наибольшую долю вероятностной массы.
В skip-gram эти оценки — скалярные произведения v_c · E'[:, w] для каждого слова словаря w, а получающиеся вероятности — это P(w | c). Вытащив из виджета выше только шаг softmax, пять оценок [-0.19, 0.26, 0.39, -0.07, -0.45] превращаются в полноценное распределение вероятностей с суммой 1:
Механика проста: возвести в экспоненту каждую оценку, сложить их, чтобы вычислить нормировщик (здесь ~5.17), затем поделить каждое exp(score) на эту сумму. Результат — распределение вероятностей, сумма которого по построению ровно 1, каковы бы ни были входы. О тонкостях softmax — поведении мягкого argmax, инвариантности к сдвигу, температуре — см. статью про MNIST, где они разобраны подробно.
Функция потерь
Потери для одного обучающего примера с целевым словом 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. Кликните по другому слову, чтобы назначить его целью, — маркер скользит по кривой, и видно напрямую, как цель, которую модель и так предпочитает, почти ничего не стоит, а цель, которую она недооценивает, платит резкую цену.
График показывает, что когда модель правильно назначает цели большую вероятность, потери крошечные, а когда она недооценивает цель, давая правильному ответу лишь малую вероятность, потери огромны. Из-за логарифмической формы −log(P) переход между этими крайностями резкий, а не плавный: чем круче становится кривая, тем агрессивнее градиент толкает вероятность цели вверх.
Сравните настоящую цель cat с равномерным ориентиром. cat сидит в (0.16, 1.83) — цель в паре (sat, cat), — тогда как точка равномерного ориентира находится в (0.20, 1.61). Эта равномерная база — то, откуда стартует любая необученная модель: при инициализации softmax размазывает вероятность примерно поровну по всем словам словаря.
Вероятность cat ниже равномерной, поэтому её потери выше базы — на этой паре модель работает чуть хуже случайной, ровно та ситуация, которую обучение и призвано исправлять.
Градиент
Градиент похож на MNIST: градиент потерь по каждому логиту равен P(w) − 𝟙[w == t] — предсказанная вероятность минус one-hot-цель. Градиент целевого слова равен P(t) − 1 (отрицательный — толкает его оценку вверх); у всех остальных он равен P(w) (положительный — толкает их оценку вниз, пропорционально тому, сколько вероятности они сейчас держат). Слова, которые модель уже верно считает маловероятными, почти не двигаются; слова, в которых она ошибается, получают больше всего сигнала.
Виджет ниже делает это вычитание конкретным на тех же пяти вероятностях. Кликните другую цель, чтобы строка градиента перерисовалась: один высокий синий столбик тянет оценку цели вверх, четыре коротких красных толкают остальные вниз.
Каждое слово, кроме цели, получает маленький положительный градиент — SGD толкает его оценку (а значит, и его столбец E') вниз пропорционально тому, сколько вероятности оно сейчас крадёт. Цель получает один большой отрицательный градиент размера P(target) − 1 (около -0.84 для cat) — SGD тянет её оценку вверх. Чем дальше модель от P(target) = 1, тем ближе эта тяга к −1 (сильнейшая возможная одношаговая тяга для одного примера). Просуммируйте по строке — получится ноль: вероятностная масса перемещается, а не создаётся.
Обратное распространение переносит эти градиенты по оценкам в 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получает градиент ровно на одну строку на обучающий пример — строку центрального слова. У остальных V−1 строк градиент нулевой, потому чтоv_c = E[c]читал только из этой одной строки. (Сравните с MNIST, гдеW₁обновляет каждый вес на каждом примере, потому что вход — плотные пиксели.)E'получает градиент на каждый столбец. Столбец настоящей цели тянется кv_c; столбец каждого другого слова отталкивается, с масштабомP(w) − 𝟙[w == t].
Затем эти градиенты используются для шага градиентного спуска: применить их к весам как E -= lr × ∂L/∂E и E' -= lr × ∂L/∂E'. Это работа оптимизатора, и выбор оптимизатора (обычный SGD, момент, Adam, RMSprop, AdaGrad…) имеет значение только на этом шаге — все они потребляют одни и те же градиенты, но используют их по-разному.
Мини-батчи и эпохи
Разбор выше обрабатывал пары по одной. Большинство обучений нейросетей обобщают это в мини-батчевый SGD — группировать примеры в батчи по B и обрабатывать целый батч за один прямой + обратный проход, ровно тот же мини-батчевый SGD, что и в MNIST, просто усреднённый по B примерам на шаг. Это тот рецепт, который используют BERT, GPT и практически любая современная модель, и именно его описывают пункты ниже.
Word2vec — исключение. Оригинальный релиз 2013 года использовал размер батча 1 — чистый SGD, одна пара за шаг, с многопоточной Hogwild!-параллельностью на CPU (каждый поток гонит свои пары и пишет в общие E / E' без блокировок; редкие коллизии обновлений молча поглощаются). Именно так Gensim работает и в 2026 году, и для word2vec конкретно это правильный выбор — подраздел про Gensim ниже объясняет почему. Пункты ниже описывают более общий мини-батчевый поток, потому что именно он переносится на BERT и дальше; просто держите в уме, что для word2vec конкретно выигрывают B=1 и Hogwild!.
Эпоха — это один полный проход по всем обучающим парам, независимо от размера батча. word2vec обычно обучается 5–15 эпох; умолчание Gensim — 5. Каждая пара просматривается многократно, потому что один шаг SGD на одной паре не формирует соответствующие строки полностью: повторные проходы сводят E и E' к геометрии, которую предпочитает функция потерь.
По батчам цикл выглядит так:
- Прямой проход на батче из B примеров — каждая пара
(c, t)течёт через сеть. В векторизованном виде поиск складывает B строк изEв матрицу(B, d), шаг оценивания становится одним матричным умножением(B, d) @ (d, V) → (B, V), а softmax идёт по строкам. - Потери — вычислить кросс-энтропию на пример (та же формула, что при B=1), затем усреднить по батчу в один скаляр.
- Обратный проход (backprop). Вычислить градиенты по цепному правилу — чистый анализ от потерь назад к каждому весу, без обновления весов. Выход: тензоры градиентов
∂L/∂Eи∂L/∂E'той же формы, что тензоры весов. - Шаг градиентного спуска — применить градиенты к весам через оптимизатор, ровно как в случае одной пары выше. Повторить для следующего батча.
Прогоните этот цикл по многим батчам за эпоху и по нескольким эпохам корпуса — и строки E и E' осядут в геометрию, которую предпочитает функция потерь.
CBOW — и мост к BERT
CBOW (continuous bag of words) — второй алгоритм word2vec, зеркальное отражение skip-gram. Там, где skip-gram берёт центральное слово и предсказывает целевое как его контекст, CBOW берёт несколько слов как контекст и предсказывает центр — пропущенное слово.
Эта рамка «заполни пропуск» — в точности клоуз-тест из исследований понимания прочитанного 1950-х: спрятать несколько слов и попросить модель заполнить их по оставшемуся — что работает только при наличии рабочей модели окружающего языка. Это же в точности и целевая функция маскированного языкового моделирования BERT, задачи предобучения, которая двигала каждый контекстный энкодер с 2018 года. CBOW можно читать как крошечный линейный BERT, а BERT — как повзрослевший CBOW с вниманием: та же обучающая задача, мелкое усреднение заменено глубокой стопкой трансформеров, симметричное окно — целым предложением, единственная маска — 15% токенов сразу, статический выход — контекстными векторами на каждый токен. Статья про BERT разбирает всё это подробно.
Механически 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 — это 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:
shape: C × V
shape: V × d
shape: d
Усреднение и даёт «мешку слов» его имя: порядок слов внутри окна отбрасывается, контекст становится мультимножеством. На каждом шаге обучения поиски происходят по текущим, «на лету», значениям E — строки, которые вы читаете на прямом проходе, это те же строки, которые вы обновляете на обратном, как и в любом SGD-обучении нейросети. Как только h вычислен, всё ниже по течению — scores = h @ E', softmax, кросс-энтропия, обратное распространение, SGD по E и E' и сокращение через негативное сэмплирование — идентично skip-gram.
Обучение — три небольших отличия от skip-gram
Прямой проход, функция потерь, градиент, мини-батчинг и сокращение через негативное сэмплирование переносятся из skip-gram без изменений. Три отличия стоит держать в уме:
- Один прямой проход на позицию окна. Skip-gram выдаёт C отдельных пар на окно и выполняет C прямых проходов. CBOW выдаёт один пример на окно — весь мешок контекста предсказывает центр. Так что CBOW обучается примерно в C раз быстрее, и это его главное практическое преимущество.
- Градиентные обновления затрагивают больше строк
Eна пример. Skip-gram подталкивает одну строкуEна пару (строку центра). CBOW подталкивает C строк на пример (строку каждого контекстного слова, с градиентом, поделённым на C из-за усреднения). Поэтому CBOW лучше справляется с частыми словами и хуже с редкими: распространённые стоп-слова затрагиваются на большинстве окон, а редкие слова редко оказываются в контексте. - Обновления
E'той же формы, просто с другим ключом. В skip-gram каждая пара(c, t)обновляет столбецE'целевого слова. В CBOW каждый пример обновляет столбецE'центрального слова. Тот же шаблон обновления, только выбирает его другое слово.
Как word2vec обучают на практике — Gensim
Возможно, это удивит в 2026 году — в эпоху, когда трансформерные энкодеры доминируют в NLP, а каждая заголовочная модель есть та или иная разновидность внимания, — но word2vec всё ещё обучают, разворачивают и поставляют в продакшен каждый день. Причина отчасти в архитектурной пригодности (word2vec — нагрузка CPU-формы, к чему мы ещё придём), отчасти в том, что для множества задач дешёвых статических векторов просто достаточно (и запрашивать их на порядки быстрее, чем делать прямой проход BERT), и отчасти в том, что некоторым конвейерам — рекомендательным системам, ранжированию в поиске, бутстрапу векторных баз, лёгким семантическим признакам — нужно именно поведение «поиск по эмбеддингам», а не глубокая контекстная модель. Так что вопрос, как такое обучить, не исторический — это рабочий инженерный вопрос для множества команд.
Де-факто стандартная библиотека для обучения word2vec — Gensim, сокращение от «Generate Similar»: открытый Python, в разработке с 2009 года, и, несмотря на существование Keras, PyTorch и JAX, всё ещё то, на чём в 2026 году работает большинство продакшен-конвейеров word2vec. Из описания самой библиотеки:
Gensim — свободная опенсорсная Python-библиотека для представления документов семантическими векторами, настолько эффективно (для компьютера) и безболезненно (для человека), насколько это возможно. Gensim спроектирован обрабатывать сырые неструктурированные цифровые тексты («простой текст») алгоритмами обучения без учителя. Алгоритмы в Gensim — Word2Vec, FastText, латентно-семантическое индексирование (LSI/LSA), латентное размещение Дирихле (LDA) и другие — автоматически обнаруживают семантическую структуру документов, изучая статистические шаблоны совместной встречаемости в корпусе обучающих документов. Эти алгоритмы работают без учителя, а значит, человеческий ввод не нужен: вам требуется только корпус документов простого текста.
Два момента, которые стоит заметить в этом описании.
Оно охватывает семейство алгоритмов, а не только word2vec. Word2Vec, FastText, LSA, LDA — каждый обсуждавшийся в этой статье метод статистической совместной встречаемости (и ещё несколько) живёт в одной библиотеке. Статья разбирала конкретно word2vec; Gensim — библиотека, в которой вы реально запустили бы любой из них.
Эффективно (для компьютера) и безболезненно (для человека) — ровно тот размен, под который Gensim оптимизирован пятнадцать лет: чрезвычайно быстрый на железе, которое этим алгоритмам действительно нужно (CPU, как мы увидим через мгновение), с однострочным API, прячущим каждую нудную деталь построения словаря, подвыборки, таблиц сэмплирования и обучающего цикла. Оба этих свойства и объясняют, почему за ним по-прежнему тянутся.
Одна строка даёт вам обученные векторы:
from gensim.models import Word2Vec
model = Word2Vec(sentences, vector_size=300, window=5, min_count=5, sg=1, workers=8)
v_king = model.wv["king"]Построение словаря, подвыборка частых слов, alias-таблица для негативного сэмплирования, обучающий цикл, сохранение/загрузка, запросы близости, арифметика аналогий — Gensim берёт всё это на себя. Чтобы повторить это на голом Keras или PyTorch, вы написали бы в десять раз больше кода, получили бы худшую пропускную способность и в итоге векторы, которые могут не воспроизводить канонические результаты word2vec.
Gensim держится отчасти по инерции, отчасти из-за эргономики, но глубочайшая причина в том, что word2vec — нагрузка CPU-формы, а Gensim — лучшая существующая CPU-реализация.
Почему word2vec нагружает CPU, а не GPU
Стандартная интуиция «глубокое обучение = GPU» идёт от моделей, чьё узкое место — плотное матричное умножение на больших тензорах: свёрточные сети, трансформеры, крупные MLP. GPU для этого и спроектированы: тысячи ядер выполняют одну и ту же операцию над разными элементами параллельно, питаясь от памяти с высокой пропускной способностью, разложенной под векторизованный доступ. Дайте GPU умножение (4096, 4096) — и она прожуёт его за микросекунды.
Word2vec структурно никогда не делает большого матричного умножения. Посмотрите, что реально вычисляет одна обучающая пара:
1. one lookup in E: read row E[center] ← 1 row out of V
2. lookup target + k negatives in E': read k+1 rows of E' ← k+1 rows out of V
3. dot products against v_c: k+1 dot products of d-dim vectors ← ~k+1 × d multiply-adds
4. gradient updates: update 1 row of E, k+1 rows of E' ← k+2 rows touchedПри k = 5–20 и d = 300 каждый шаг обучения делает порядка 10⁴ умножений-сложений — примерно в тысячу раз меньше вычислений на пример, чем один прямой проход через небольшой классификатор изображений. «Вычислений» здесь почти нет.
Это задача той же формы, что подробно разбирается в статье «MNIST обучается быстрее на CPU»: когда вычислений на шаг достаточно мало, накладные расходы на запуск ядер и стоимость передачи по PCIe доминируют над любым преимуществом GPU во FLOPS, и CPU выигрывает по времени на стенных часах.
Это показывает, что узкое место — доступ к памяти, а не арифметика, и вот там CPU и оказывается правильным выбором. E и E' вместе занимают 2 × V × d × 4 байта — около 2.4 ГБ при V = 10⁶, d = 300, — и каждый шаг делает горсть широко разбросанных чтений строк в них, шаблон, которому предвыборка кэша помочь не может. Кэши L1/L2 поглощают эти случайные чтения, системная оперативная память держит 2.4 ГБ модели с запасом, а потоки ОС могут забивать общую память обновлениями — трюк Hogwild! — куда дешевле, чем эквивалент на GPU (атомарные операции через тысячи ядер).
Gensim опирается на всё это.
Его внутренний цикл написан на Cython, с прямым и обратным проходом на одну пару, с lock-free параллелизмом Hogwild! по workers потокам CPU.
На мощном ноутбуке это 8 потоков, несколько миллионов пар в секунду, миллиарды обучающих пар в час — и всё это не покидая CPU. Этого достаточно, чтобы обучить word2vec на полном дампе Википедии за несколько часов на железе, не требующем дорогих GPU.
Бутылочное горлышко softmax — и дорога, по которой не пошли
Всё выше использовало полный softmax: каждый шаг обучения нормирует по всем V словам, складывая exp(score) по всему словарю для знаменателя. Это цена O(V) на каждом отдельном примере, и при V в сотни тысяч она становится доминирующей статьёй расходов обучения — причиной, по которой наивная версия слишком медленна для реального корпуса.
Миколов с соавторами предложили два обходных пути. Один — негативное сэмплирование, которое практические реализации выше уже используют и которому посвящена отдельная статья: не нормировать вовсе, а превратить каждый шаг в горстку бинарных классификаций «настоящее против шума» стоимостью O(k). Другой, предложенный в тех же статьях, — иерархический softmax, о котором стоит знать именно потому, что он проиграл.
Иерархический softmax располагает V слов как листья бинарного дерева (word2vec использует дерево Хаффмана, так что частые слова сидят ближе к корню). Каждый из V − 1 внутренних узлов несёт свой обучаемый вектор, заменяя пословные выходные векторы в E'. Вероятность слова — это произведение решений «влево/вправо» вдоль пути от корня к листу, каждое из которых есть сигмоида σ(v_node · v_c). Поскольку путь имеет длину около log₂V, шаг затрагивает лишь ~log₂V векторов узлов вместо всех V — и, в отличие от негативного сэмплирования, он остаётся настоящим нормированным распределением: просуммируйте P(w | c) по всем листьям и получите ровно 1.
Так почему же он не используется сегодня? Три причины, и они обобщаются за пределы word2vec:
- Негативное сэмплирование просто обучало эмбеддинги лучше на больших корпусах, и оно проще — никакого дерева строить и поддерживать, — так что оно выиграло очную дуэль даже внутри word2vec.
- Он враждебен GPU. Каждое слово идёт своим путём, затрагивая разные векторы узлов, — нерегулярная, ветвистая, разбросанная работа, противоположность плотному батчевому матричному умножению, под которое GPU и построены. На современном железе «больше FLOPS, но регулярно» бьёт «меньше FLOPS, но нерегулярно».
- Узкое место, которое он решал, исчезло. Подсловная токенизация сжала словари до ~30–100 тысяч кусочков, что сделало обычный
O(V)-softmax достаточно дешёвым, чтобы никакое приближение вообще не требовалось. Именно это, а не какой-либо конкурирующий трюк, и отправило иерархический softmax на пенсию в языковых моделях.
Он выживает в основном как унаследованная опция в fastText (унаследовавшем кодовую базу word2vec) и как предок древесных softmax, до сих пор используемых в задачах экстремальной классификации и рекомендательных системах с миллионами меток. Для эмбеддингов слов, впрочем, история заканчивается на негативном сэмплировании.
Где статические эмбеддинги ломаются
word2vec производит статические эмбеддинги: один фиксированный вектор на слово, независимо от контекста. Это ровно правильная форма для дистрибутивной гипотезы в исходной формулировке, но у неё три режима отказа, которые становились всё заметнее по мере перехода NLP к более сложным задачам.
Полисемия
Рассмотрим два предложения:
- Я внёс чек в банк.
- Мы устроили пикник на берегу реки.
В английском оба места занимает одно и то же слово bank. Статический эмбеддинг даёт bank один вектор. Этот вектор — своего рода среднее по обоим значениям, а значит, хорошим представлением он не является ни для одного из них.
bank with two sense clustersglove-wiki-gigaword-300Виджет выше показывает косинусную близость между bank и двумя кластерами контекстных слов: money, loan, account, deposit, interest с одной стороны; river, shore, water, creek, flood — с другой. Оба кластера тянут выше нуля — единственный вектор покрывает оба значения, — но финансовый кластер побеждает. Это факт не о слове, а о перекосе корпуса.
У кода ниже по течению нет способа восстановить, какое значение имелось в виду в конкретном предложении, потому что у него есть один вектор и окружающая последовательность других одиночных векторов, все слепые к значениям ровно тем же образом.
Нечувствительность к синтаксису
Представление «мешок векторов» выбрасывает порядок слов. Предложения:
- Собака кусает человека.
- Человек кусает собаку.
содержат одинаковые наборы слов и потому одинаковые представления «мешок векторов», несмотря на противоположные значения. Всё, что построено поверх статических эмбеддингов, обязано восстанавливать порядок откуда-то ещё — обычно из рекуррентного или свёрточного слоя, обрабатывающего последовательность напрямую. Это работает (так и строилась каждая NLP-модель до 2018 года), но означает, что сами эмбеддинги делают лишь часть работы.
Заморожены на момент обучения
Статические эмбеддинги фиксируются после обучения. Новые значения, новые составные слова, новая доменная лексика — векторы не обновляются. Хуже того, у слов, не встретившихся в обучающем корпусе, векторов попросту нет.
Эти три отказа выглядят по-разному на поверхности, но имеют одну причину: статический эмбеддинг есть функция слова, а не предложения. Всё, что зависит от предложения — значение, роль, дискурсивная позиция, — приходится обрабатывать вне эмбеддинга. Эта работа настолько велика, что она ограничивала архитектуры всей эпохи до 2018 года.
Те же три отказа бьют и по любому другому статическому эмбеддингу слов: GloVe и fastText производят ту же таблицу поиска (V, d) другими процедурами обучения, а значит, наследуют те же слепые зоны. Их решение требует полностью отказаться от «одного вектора на слово», что и делают BERT и контекстные энкодеры.
Что дальше
Исправление легко сформулировать и было трудно построить: сделать вектор зависящим от окружающего предложения, а не только от слова. У bank не должно быть одного вектора — у него должен быть тот, который он заслужил в «river bank», и другой в «bank account». Именно этот разрыв и закрыли архитектуры в стиле BERT.
BERT наследует от word2vec больше, чем можно ожидать при таком архитектурном разрыве. Дистрибутивная гипотеза переносится (предсказывать пропущенные токены по контексту); переносится и трюк с фиктивной предсказательной задачей, существующей лишь затем, чтобы вытолкнуть из сети хорошие векторы; переносится и ставка на то, что геометрия кодирует смысл. Сама обучающая задача — заполнить пропуск — та же, что CBOW гонял в крошечном масштабе ещё в 2013 году.
Изменились архитектура и масштаб. ELMo (2018) выпустил первую серьёзную версию с двунаправленной LSTM и скрытыми состояниями на позицию. BERT (тоже 2018) заменил LSTM трансформерным энкодером и ввёл маскированное языковое моделирование — структурно это CBOW с почти снятыми ограничениями CBOW: глубокая стопка двунаправленных трансформерных слоёв по всему предложению вместо мелкого усреднения по пятисловному окну, несколько масок на пример вместо одной и контекстные векторы на каждый токен на выходе вместо единственной замороженной строки поиска. Статья про BERT разбирает эту машинерию подробно.