Слова как векторы
Почему one-hot не годится, как дистрибутивная гипотеза превращается в word2vec, откуда берётся «король − мужчина + женщина ≈ королева» и почему слой nn.Embedding — это просто таблица.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Токенизатор (модуль 4.2) превращает текст в номера: у Qwen слово «кот» становится одним или несколькими id из словаря на 152 тысячи токенов. Но номер — плохой вход для нейросети. Токены 1234 и 1235 стоят рядом в словаре, но по смыслу ничем не связаны, а «кот» и «кошка» могут иметь номера, различающиеся на сотню тысяч.
Поэтому первый слой любой LLM превращает номер токена в вектор из сотен или тысяч чисел — эмбеддинг, векторное представлениеэмбеддинг, векторное представлениеEN embedding. Эти векторы обучаются вместе с моделью, и геометрия пространства начинает отражать смысл: похожие слова оказываются рядом, а направления кодируют отношения вроде «мужской → женский» или «страна → столица».
В первом уроке модуля 4.1 мы видели третью беду n-граммных моделей: знание о «коте» никак не переносится на «кошку».
Эмбеддинги решают ровно эту проблему.
Сегодня разберём, откуда берётся такая геометрия, обучим word2vec своими руками и увидим, что слой nn.Embedding
в PyTorch устроен проще, чем кажется.
One-hot и его проблемы
Заголовок раздела «One-hot и его проблемы»Самый прямолинейный способ сделать из номера вектор — one-hot кодированиеone-hot кодирование (унитарный код)EN one-hot encodingВектор длины V из нулей с единицей в позиции номера токена. Все one-hot векторы попарно ортогональны.: вектор длины из нулей с единицей на месте номера токена. Мы уже подавали его на вход биграммной нейросети во втором уроке модуля 4.1.
Число под единицей — её позиция, то есть номер токена в словаре (номера здесь условные).
Три проблемы:
- Нет сходства. Скалярное произведение любых двух разных one-hot векторов равно 0, значит косинусное сходствокосинусное сходствоEN cosine similarityКосинус угла между векторами — скалярное произведение, делённое на произведение длин. Для нормированных векторов равно скалярному произведению. тоже 0. «Кот» одинаково далёк и от «кошки», и от «трактора».
- Огромная размерность. При каждый вектор — 150 тысяч чисел, из которых одно ненулевое.
- Нет обобщения. Всё, что модель узнала о «коте», хранится в весах, связанных с позицией 812, и никак не влияет на позицию 5407.
Нужны плотные векторы небольшой размерности (от 50 до нескольких тысяч), где близость векторов означает близость смысла. Вопрос: как их получить без ручной разметки?
Дистрибутивная гипотеза
Заголовок раздела «Дистрибутивная гипотеза»Ответ дала лингвистика середины XX века. дистрибутивная гипотезадистрибутивная гипотезаEN distributional hypothesisСлова, которые встречаются в похожих контекстах, имеют похожий смысл (Harris, 1954; Firth, 1957). (Зеллиг Харрис, 1954) говорит: слова, которые встречаются в похожих контекстах, имеют похожий смысл. Джон Ферт (1957) сформулировал это короче: «слово узнают по его компании» (You shall know a word by the company it keeps).
Проверьте на себе:
Утром я выпил чашку горячего ___.
Сюда подходят «чая», «кофе», «какао», но не «трактора». Даже не зная слова, вы догадались бы, что это напиток. В других языках то же самое: в китайском переводе этой фразы («каждое утро я выпиваю чашку горячего …») на место пропуска встают те же «чай» и «кофе».
Значит, смысл слова можно извлечь из статистики его соседей. Самый прямой способ — посчитать, сколько раз каждое слово стояло рядом с каждым контекстным словом. Вот такие счётчики для окна ±2 слова на синтетическом корпусе, который мы ниже используем для обучения word2vec:
| ест | рыбу | кость | спит | пьёт | горячий | столица | |
|---|---|---|---|---|---|---|---|
| кот | 119 | 68 | 0 | 86 | 0 | 0 | 0 |
| кошка | 102 | 52 | 0 | 92 | 0 | 0 | 0 |
| собака | 112 | 0 | 51 | 99 | 0 | 0 | 0 |
| чай | 0 | 0 | 0 | 0 | 130 | 199 | 0 |
| кофе | 0 | 0 | 0 | 0 | 130 | 198 | 0 |
| москва | 0 | 0 | 0 | 0 | 0 | 0 | 105 |
Строки уже работают как векторы. Посчитаем косинус по этим семи столбцам: «кот» и «кошка» — 0.99, «кот» и «собака» — 0.85, «кот» и «чай» — 0, «чай» и «кофе» — 1.00. Настоящая матрица имеет размер (сотни тысяч столбцов, почти все нули), поэтому её сжимают до измерений. Например, SVDсингулярное разложение (SVD)EN singular value decomposition (SVD)A = UΣVᵀ — любую матрицу можно разложить на поворот, растяжение вдоль осей и ещё один поворот. Эквивалентно сумме слоёв ранга 1 σᵢuᵢvᵢᵀ. оставляет главных направлений (модуль 1.1, урок о SVD). Так устроены счётные методы: LSA (1990) применяет SVD к матрице «слово × документ», а GloVe (Стэнфорд, 2014) подбирает векторы так, чтобы их скалярные произведения приближали логарифмы счётчиков совместной встречаемости. Word2vec пошёл предсказательным путём.
word2vec: skip-gram
Заголовок раздела «word2vec: skip-gram»В 2013 году Томаш Миколов и коллеги из Google предложили word2vec — две простые архитектуры, которые меньше чем за сутки обучаются на корпусе из 1.6 млрд слов. Мы разберём skip-gramskip-gramEN skip-gramВариант word2vec — по центральному слову предсказываются слова контекста. В старых китайских текстах — «модель пропуска слов».: по центральному слову предсказать слова вокруг него.
Возьмём предложение «кот спит на тёплом диване» и окно ±2. Для центрального слова «на» получаем обучающие пары (центр, контекст): (на, кот), (на, спит), (на, тёплом), (на, диване). И так для каждой позиции в каждом предложении корпуса.
У каждого слова два вектора: — когда слово в центре, — когда оно в контексте. Вероятность увидеть контекстное слово рядом с центральным — softmax от скалярных произведений:
Запись — это просто скалярное произведениескалярное произведениеEN dot product (inner product)a · b = Σ aᵢbᵢ = ‖a‖‖b‖cos θ. Основа нейрона, внимания (q · k) и выходного слоя LLM. векторов и (модуль 1.1): чем лучше они «смотрят в одну сторону», тем оно больше. Значит, формула — обычный softmax из модуля 1.3, где логит слова равен . Мини-пример: словарь из трёх слов, и скалярные произведения с равны . Тогда : первое слово — самый вероятный сосед.
Обучаем, как всегда, минимизируя NLL правильных пар. Чтобы предсказывать похожие контексты, слова с похожими контекстами вынуждены получить похожие векторы — это и есть дистрибутивная гипотеза, превращённая в loss.
flowchart LR A["Центральное слово<br/>«кот»"] --> B["v_кот<br/>(строка таблицы W_in)"] B --> C["Скалярные произведения<br/>с u контекстов"] C --> D["σ(u_ест · v_кот) → 1<br/>настоящий контекст"] C --> E["σ(u_столица · v_кот) → 0<br/>k случайных слов"]
Негативное сэмплирование
Заголовок раздела «Негативное сэмплирование»Знаменатель softmax — сумма по всему словарю. При в сотни тысяч и миллиардах пар это слишком дорого. Во второй статье Миколов с соавторами заменили задачу более дешёвой — негативное сэмплированиенегативное сэмплированиеEN negative samplingВместо softmax по всему словарю модель учится отличать настоящую пару (слово, контекст) от k случайных «шумовых» пар.: вместо «какое из слов — контекст?» модель решает бинарную задачу «эта пара настоящая или случайная?». Для каждой настоящей пары берём случайных «шумовых» слов :
Здесь — сигмоида из логистической регрессии (модуль 2.2). Она превращает скалярное произведение в вероятность «пара настоящая». Поскольку , второе слагаемое — это вероятности того, что случайная пара будет признана фальшивой. Получается обычная бинарная cross-entropy: одна пара с меткой 1 и пар с меткой 0. Вместо суммы по словам — всего скалярное произведение.
Числовой пример: если , то и первое слагаемое равно . Если случайное слово даёт , его вклад — большой штраф, и градиент оттолкнёт векторы друг от друга.
Первое слагаемое тянет и друг к другу, второе отталкивает от случайных слов. Градиенты этого loss мы выведем руками в следующем уроке. Детали из статьи:
- от 5 до 20 для небольших корпусов и 2–5 для больших;
- шумовые слова берут не равномерно, а пропорционально частоте в степени : это немного поднимает шансы редких слов;
- самые частые слова («и», «на», «в») прореживают — они мало говорят о смысле соседей.
Вторая архитектура, CBOWCBOW (непрерывный мешок слов)EN continuous bag of words (CBOW)Вариант word2vec — по словам контекста предсказывается центральное слово., делает наоборот: по среднему векторов контекста предсказывает центральное слово. Она быстрее, а skip-gram лучше работает с редкими словами.
Обучаем skip-gram сами
Заголовок раздела «Обучаем skip-gram сами»Сгенерируем синтетический корпус, где похожие по смыслу слова стоят в похожих контекстах, и обучим skip-gram с негативным сэмплированием на PyTorch. Обучение занимает несколько секунд на CPU.
Две новые конструкции Python в коде. Запись (a := rng.choice(animals)) («морж», walrus operator) вычисляет значение,
сохраняет его в переменную a и тут же подставляет: так в одном шаблоне «кот ест …» еда подбирается к тому же
животному. И B в комментариях вида (B, d) — размер батча, как в модуле 3.3.
import randomimport torchimport torch.nn.functional as F
torch.set_num_threads(1) # для крошечных тензоров один поток быстрее
# 1. Синтетический корпус: похожие по смыслу слова стоят в похожих контекстахrng = random.Random(0)animals = ["кот", "кошка", "собака", "пёс"]food = {"кот": ["рыбу", "мясо"], "кошка": ["рыбу", "мясо"], "собака": ["мясо", "кость"], "пёс": ["мясо", "кость"]}people = ["мама", "папа", "бабушка", "дедушка"]drinks = ["чай", "кофе", "сок", "молоко"]capitals = {"москва": "россии", "пекин": "китая", "париж": "франции", "токио": "японии"}templates = [ lambda: f"{(a := rng.choice(animals))} ест {rng.choice(food[a])}", lambda: f"{rng.choice(animals)} спит на диване", lambda: f"{rng.choice(people)} пьёт {rng.choice(drinks)} утром", lambda: f"{rng.choice(people)} любит горячий {rng.choice(['чай', 'кофе'])}", lambda: f"{(c := rng.choice(list(capitals)))} столица {capitals[c]}", lambda: f"мы летим в {rng.choice(list(capitals))} летом", lambda: f"{rng.choice(list(capitals))} большой красивый город",]corpus = [rng.choice(templates)().split() for _ in range(3000)] # 3000 случайных предложений-списков словvocab = sorted({w for s in corpus for w in s}) # все разные слова корпусаstoi = {w: i for i, w in enumerate(vocab)}V = len(vocab)print("предложений:", len(corpus), "| словарь:", V, "| пример:", " ".join(corpus[0]))
# 2. Обучающие пары (центр, контекст) в окне ±2 слова:# для каждого слова s[i] берём соседей s[j] с j от i-2 до i+2, кроме самого ipairs = torch.tensor([(stoi[s[i]], stoi[s[j]]) for s in corpus for i in range(len(s)) for j in range(max(0, i - 2), min(len(s), i + 3)) if j != i])print("пар (центр, контекст):", len(pairs))
# 3. Skip-gram с негативным сэмплированием (SGNS)torch.manual_seed(0)d, k, epochs = 16, 5, 40freq = torch.bincount(pairs[:, 0], minlength=V).float() # bincount: сколько раз встретилось каждое число 0…V-1noise = freq**0.75 / (freq**0.75).sum() # негативы ~ частота^0.75, как в word2vecemb_in = torch.nn.Embedding(V, d) # векторы слов-центров — это и есть эмбеддингиemb_out = torch.nn.Embedding(V, d) # векторы слов-контекстовopt = torch.optim.Adam([*emb_in.parameters(), *emb_out.parameters()], lr=0.03)for epoch in range(1, epochs + 1): perm = torch.randperm(len(pairs)) # случайная перестановка номеров пар — перемешиваем данные # k случайных «шумовых» слов на каждую пару; view(-1, k) — форма (число пар, k) negatives = torch.multinomial(noise, len(pairs) * k, replacement=True).view(-1, k) for b in range(0, len(pairs), 512): # мини-батчи по 512 пар c, o = pairs[perm[b:b + 512]].T # .T: из (B, 2) получаем две строки — центры c и контексты o v = emb_in(c) # (B, d) pos = (v * emb_out(o)).sum(-1) # (B,) «свой» контекст # (B, k, d) @ (B, d, 1) → (B, k, 1): скалярные произведения v с каждым из k шумовых векторов neg = (emb_out(negatives[b:b + 512]) @ v.unsqueeze(-1)).squeeze(-1) # (B, k) «чужие» # logsigmoid(x) = log σ(x); это формула L из текста, усреднённая по батчу loss = -(F.logsigmoid(pos) + F.logsigmoid(-neg).sum(-1)).mean() opt.zero_grad() loss.backward() opt.step() if epoch % 10 == 0: print(f"эпоха {epoch}: loss {loss.item():.3f}")
# 4. Ближайшие соседи по косинусному сходствуE = F.normalize(emb_in.weight.detach(), dim=1) # каждую строку делим на её длину → скалярное произведение = косинусfor w in ["кот", "чай", "москва", "мама"]: sims = E @ E[stoi[w]] # косинусы слова w со всеми словами словаря best = sims.argsort(descending=True)[1:4] # [0] — само слово (косинус 1), берём следующие три print(f"{w:7} → " + ", ".join(f"{vocab[i]} ({sims[i]:.2f})" for i in best))предложений: 3000 | словарь: 39 | пример: токио большой красивый городпар (центр, контекст): 28240эпоха 10: loss 1.259эпоха 20: loss 1.253эпоха 30: loss 1.250эпоха 40: loss 1.236кот → кошка (0.98), собака (0.82), пёс (0.80)чай → кофе (0.89), сок (0.72), молоко (0.68)москва → париж (0.80), пекин (0.79), токио (0.77)мама → папа (0.94), бабушка (0.88), дедушка (0.87)Модель не знала ничего, кроме того, какие слова стоят рядом. Но кошачьи оказались рядом с кошачьими, напитки — с напитками, столицы — со столицами, родственники — с родственниками. Обратите внимание: «москва» и «пекин» ни разу не встретились в одном предложении. Они похожи, потому что встречаются в одинаковых окружениях («… столица …», «мы летим в …»). Это дистрибутивная гипотеза в действии.
Почему loss почти не падает после 10-й эпохи? Корпус шаблонный: рядом с «пьёт» с равной вероятностью стоит любой из четырёх напитков, и эту неопределённость никакие векторы не уберут. Loss упирается в неустранимую случайность данных (её мера — энтропияэнтропия (информационная)EN (Shannon) entropyH(p) = −Σ p log p — среднее «удивление», мера неопределённости распределения. Максимальна у равномерного распределения.), а не в ёмкость модели. К тому же мы печатаем loss последнего батча, а он шумит от батча к батчу.
Векторы из 16 чисел глазами не увидеть, но их можно спроецировать на плоскость. Допишите в конец скрипта
(нужен matplotlib, модуль 0.3):
# 5. Проекция на плоскость: два главных направления из SVD (модуль 1.1)import matplotlib.pyplot as plt
X = emb_in.weight.detach()X = X - X.mean(dim=0) # центрируем облако точекU, S, Vh = torch.linalg.svd(X, full_matrices=False)xy = X @ Vh[:2].T # координаты вдоль двух главных направленийprint(f"доля разброса в 2D: {(S[:2] ** 2).sum() / (S ** 2).sum():.2f}")plt.scatter(xy[:, 0], xy[:, 1])for i, w in enumerate(vocab): plt.annotate(w, (xy[i, 0].item(), xy[i, 1].item()))plt.show()доля разброса в 2D: 0.32На картинке животные собираются в одном углу, столицы — в другом. А вот люди и напитки слипаются в одно облако, хотя по косинусу в 16 измерениях «мама» ближе всего к «папе», а «чай» — к «кофе». Две оси сохранили лишь треть разброса, и часть различий пропала. Отсюда правило: 2D-картинки эмбеддингов (PCAметод главных компонент (PCA)EN principal component analysis (PCA), t-SNEt-SNE (стохастическое вложение соседей)EN t-distributed stochastic neighbor embedding (t-SNE)Метод визуализации, сохраняющий соседство точек. Размеры кластеров и расстояния между далёкими кластерами на картинке t-SNE почти ничего не значат., модуль 2.3) хороши для интуиции, а выводы о сходстве делайте по косинусу в полном пространстве.
Арифметика смыслов
Заголовок раздела «Арифметика смыслов»Самый знаменитый результат word2vec — отношения между словами превращаются в направления:
Интуиция: разность кодирует «мужской → женский», и та же разность кодирует то же самое. Если направления совпадают, то, прибавив к «королю» сдвиг «мужчина → женщина», мы попадём в окрестность «королевы». Так же работают «страна → столица» () и грамматические отношения («идти → шёл» как «бежать → бежал»).
аналогии слованалогии слов (векторная арифметика)EN word analogyЗадача «a относится к b, как c к ?», решается поиском слова, ближайшего к вектору b − a + c. «a относится к b, как c к ?» решают так:
- Вычислить (обычно из нормированных векторов).
- Найти слово с максимальным косинусом к .
- Исключить сами a, b и c. Без этого ответом часто оказывается или : например, «король» ближе всех к . Это важная оговорка — эффектные примеры аналогий отчасти держатся на этом исключении.
Ограничения статических эмбеддингов
Заголовок раздела «Ограничения статических эмбеддингов»У word2vec один вектор на слово, независимо от контекста. Отсюда проблемы:
- Многозначность (многозначностьмногозначность (полисемия)EN polysemyОдно слово с несколькими значениями («ключ», «коса», «лук»). Статический эмбеддинг даёт им один вектор на все значения.). «Ключ» от двери, «ключ» — родник и скрипичный «ключ» получают один вектор — смесь всех значений. В китайском одно и то же слово означает и яблоко, и компанию Apple.
- Морфология. «кот», «кота», «коту», «котом», «коты», «котов» для word2vec — шесть разных слов, и каждое учится отдельно. Для русского с его падежами это особенно больно. Поэтому русские word2vec-модели (в том числе на RusVectores) часто обучают на лемматизированных текстах, где все формы сведены к начальной («кот»), а fastText (Facebook, 2017) собирает вектор слова из векторов его буквенных n-грамм.
- Новые слова. Слова, которого не было в обучении, у модели просто нет.
Современные LLM решают все три проблемы. Морфологию и новые слова — подсловной токенизацией (модуль 4.2): «котом» режется на кусочки, которые встречались и в других словах. Многозначность — контекстными представлениями: на входе у токена «ключ» один и тот же эмбеддинг, но после слоёв внимания (модуль 4.4) его вектор зависит от соседей и для «ключ от двери» и «бьёт ключом» получается разным.
nn.Embedding — это просто таблица
Заголовок раздела «nn.Embedding — это просто таблица»Теперь главное для инженера. В LLM нет отдельно обученного word2vec. Первый слой — слой эмбеддинговслой эмбеддингов (таблица эмбеддингов)EN embedding layerМатрица V × d; по номеру токена возвращает его строку. Эквивалентно one-hot × матрица. В PyTorch — nn.Embedding.: матрица размера , инициализированная случайно и обучаемая вместе со всей моделью. По номеру токена слой возвращает строку матрицы.
В уроке о биграммах мы видели, что one-hot × матрица — это выбор строки. nn.Embedding делает то же самое,
но без умножения на огромный разреженный вектор, просто индексацией:
import torchimport torch.nn.functional as F
torch.manual_seed(0)emb = torch.nn.Embedding(num_embeddings=5, embedding_dim=3) # словарь 5 токенов, векторы длины 3print(emb.weight.shape)
ids = torch.tensor([2, 0, 2]) # три токена: «2», «0», снова «2»a = emb(ids) # lookup: строки 2, 0, 2b = F.one_hot(ids, num_classes=5).float() @ emb.weightprint(torch.allclose(a, b)) # то же самое, что one-hot × матрица
a.sum().backward()print(emb.weight.grad) # градиент пришёл только в строки 0 и 2
batch = torch.tensor([[1, 4, 2, 0], [3, 3, 1, 2]]) # (B=2, T=4) — номера токеновprint(emb(batch).shape) # (B, T, d) — вход трансформераtorch.Size([5, 3])Truetensor([[1., 1., 1.], [0., 0., 0.], [2., 2., 2.], [0., 0., 0.], [0., 0., 0.]])torch.Size([2, 4, 3])Три вывода:
- Lookup = one-hot × матрица. Математически это линейный слойлинейный (полносвязный) слойEN linear layer (fully connected layer)y = xWᵀ + b. В PyTorch nn.Linear(in, out) хранит weight формы (out, in) — частый источник путаницы. без сдвига, а на практике — индексация.
- Градиент разреженный. Обновляются только строки токенов, которые были в батче. Строка 2 встретилась дважды
и получила градиент 2 — вклады складываются. Редкие токены обновляются редко, поэтому их эмбеддинги плохо
обучены — отсюда «глючные» токены вроде
SolidGoldMagikarpиз модуля 4.2. - Форма . Батч номеров токенов превращается в батч векторов. С этого тензора начинается каждый трансформер.
Сколько это весит
Заголовок раздела «Сколько это весит»Таблица эмбеддингов — параметров:
| Модель | Параметров в таблице | ||
|---|---|---|---|
| Наш skip-gram | 39 | 16 | 624 |
| Qwen3-0.6B | 151 936 | 1 024 | ≈ 156 млн — около четверти модели |
| DeepSeek-V3 | 129 280 | 7 168 | ≈ 927 млн |
У DeepSeek-V3 одна только входная таблица — почти миллиард параметров, около 1.85 ГБ в BF16. Но на каждый токен из неё читается одна строка, поэтому вычислений она почти не требует. У маленьких моделей таблица — заметная доля всех весов, поэтому Qwen3-0.6B использует связанные весасвязывание весов (общие эмбеддинги входа и выхода)EN weight tying (tied embeddings)Одна и та же матрица служит эмбеддингом токенов и LM head. Её градиент — сумма вкладов от обоих мест. В Hugging Face — флаг tie_word_embeddings. (weight tying): та же матрица работает и выходным слоем, который превращает скрытое состояние в логиты по словарю. Подробно — в модуле 4.6.
Практика
Заголовок раздела «Практика»Найдите ближайших соседей и решите аналогии на игрушечных эмбеддингах. Векторы собраны из смысловых признаков («королевский», «мужской», «столица», «Китай»…), к ним добавлен шум, а оси спрятаны случайным поворотом. Как и в настоящих эмбеддингах, отдельные координаты ничего не значат, смысл живёт в углах между векторами.
import numpy as np
# ── Игрушечные эмбеддинги (готово, менять не нужно) ─────────────────────────────
# У каждого слова есть «рецепт» из смысловых признаков. Мы добавляем шум и прячем оси
# случайным поворотом: как и в настоящих эмбеддингах, отдельные координаты ничего не значат,
# смысл живёт только в углах и расстояниях между векторами.
AXES = ["королевский", "мужской", "взрослый", "человек", "животное", "кошачий", "собачий",
"город", "столица", "страна", "Россия", "Китай", "Франция", "Япония",
"напиток", "горячий", "еда"]
RECIPES = {
"король": "королевский мужской взрослый человек",
"королева": "королевский мужской:-1 взрослый человек",
"принц": "королевский мужской взрослый:0.2 человек",
"принцесса": "королевский мужской:-1 взрослый:0.2 человек",
"мужчина": "мужской взрослый человек",
"женщина": "мужской:-1 взрослый человек",
"мальчик": "мужской взрослый:0.1 человек",
"девочка": "мужской:-1 взрослый:0.1 человек",
"кот": "животное кошачий мужской:0.3",
"кошка": "животное кошачий мужской:-0.3",
"пёс": "животное собачий мужской:0.3",
"собака": "животное собачий мужской:-0.3",
"москва": "город столица Россия",
"петербург": "город Россия",
"россия": "страна Россия",
"пекин": "город столица Китай",
"шанхай": "город Китай",
"китай": "страна Китай",
"париж": "город столица Франция",
"франция": "страна Франция",
"токио": "город столица Япония",
"япония": "страна Япония",
"чай": "напиток горячий Китай:0.3",
"кофе": "напиток горячий",
"сок": "напиток",
"молоко": "напиток еда:0.3",
"рис": "еда Китай:0.3",
"хлеб": "еда Россия:0.3",
}
def make_embeddings(dim=20, noise=0.08, seed=0):
rng = np.random.default_rng(seed)
words = list(RECIPES)
E = np.zeros((len(words), dim))
for i, w in enumerate(words):
for part in RECIPES[w].split():
name, _, val = part.partition(":")
E[i, AXES.index(name)] = float(val) if val else 1.0
E += noise * rng.standard_normal(E.shape)
Q, _ = np.linalg.qr(rng.standard_normal((dim, dim))) # случайный поворот
return words, E @ Q
WORDS, E = make_embeddings() # WORDS — список из 28 слов, E — матрица 28 × 20
# ── Ваши функции ───────────────────────────────────────────────────────────────
def normalize(E):
"""Делит каждую строку E на её L2-норму. Возвращает новую матрицу (E не меняет)."""
# TODO: ваш код
...
def most_similar(word, words, E, k=3):
"""k ближайших к word слов по косинусному сходству, без самого word.
Возвращает список пар (слово, сходство) по убыванию сходства.
"""
# TODO: ваш код
...
def analogy(a, b, c, words, E):
"""Решает аналогию «a относится к b, как c к ?».
Строит вектор b − a + c из НОРМИРОВАННЫХ эмбеддингов и возвращает ближайшее к нему
по косинусу слово, исключая сами a, b и c.
Пример: analogy("мужчина", "король", "женщина", ...) → "королева".
"""
# TODO: ваш код
...
def one_hot(ids, V):
"""Матрица len(ids) × V из нулей и единиц: в строке r единица стоит в столбце ids[r]."""
# TODO: ваш код
...
# Кнопка «Запустить»:
print("Соседи «кот»:", most_similar("кот", WORDS, E))
print("мужчина : король = женщина :", analogy("мужчина", "король", "женщина", WORDS, E))
После решения попробуйте сами: что ближе к «пекин» — «москва» или «шанхай», и почему? Какой ответ даст аналогия
«кот : кошка = пёс : ?»? Сравните соседей «чай» и «кофе» при k=5: почему у «чая» в списке появляются «китай»
и «пекин», а у «кофе» — нет? Загляните в RECIPES.
Проверь себя
Заголовок раздела «Проверь себя»- One-hot векторы огромны и попарно ортогональны: сходства слов в них нет.
- Дистрибутивная гипотеза: смысл слова определяется его контекстами. Счётные методы (LSA, GloVe) сжимают матрицу совместной встречаемости, предсказательные (word2vec) учат векторы предсказывать соседей.
- Skip-gram с негативным сэмплированием притягивает векторы настоящих пар (слово, контекст) и отталкивает случайные. Похожие слова получают похожие векторы, отношения становятся направлениями.
- Аналогия ищется по косинусу с исключением . Эмбеддинги наследуют и предрассудки данных.
nn.Embedding— таблица : lookup = one-hot × матрица, градиент разреженный, форма . У DeepSeek-V3 в ней почти миллиард параметров.
Дальше: «Обучаем word2vec сами: skip-gram и negative sampling» — выведем градиенты негативного сэмплирования руками, обучим векторы на NumPy без автограда, как в оригинальном word2vec, и увидим, как из этой идеи выросли эмбеддинги предложений для RAG и CLIP.