Векторы, скалярное произведение, нормы
Вектор как список чисел, точка и стрелка; длина вектора, скалярное произведение и косинусное сходство — та самая математика, на которой работает поиск похожих эмбеддингов.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Внутри языковой модели нет слов — только числа. Когда большая языковая модельбольшая языковая модель (БЯМ, LLM)EN large language model (LLM)В китайском «большая модель» («да мосин») — разговорное общее название для LLM и вообще foundation models. читает токен «кошка», она достаёт из таблицы строку из нескольких тысяч чисел. У DeepSeek-V3 таких чисел 7168 на каждый токен. Эта строка и есть эмбеддингэмбеддинг, векторное представлениеEN embedding — вектор. Дальше вся работа модели — это действия над векторами: их складывают, растягивают, сравнивают.
В этом уроке три операции, которые встретятся вам буквально в каждой теме курса:
- длина вектора (норма) — на ней держатся нормализация слоёв и обрезка градиентов;
- скалярное произведение — из него сделаны нейрон, внимание и выходной слой модели;
- косинусное сходство — им ищут похожие тексты в RAGгенерация с дополнением извлечёнными данными (RAG)EN retrieval-augmented generation (RAG) (так называют схему, где модель перед ответом находит в базе документов подходящие куски текста, ступень 8) и в рекомендательных системах.
flowchart LR A["Токен «кошка»"] --> B["Вектор<br/>из d чисел"] B --> C["Скалярные произведения<br/>внимание, нейроны"] B --> D["Косинусное сходство<br/>поиск похожих"] C --> E["Логиты<br/>следующего токена"]
Три взгляда на вектор
Заголовок раздела «Три взгляда на вектор»ВекторвекторEN vectorУпорядоченный список чисел; его же можно видеть как точку или как стрелку (направление + длина). Китайский термин буквально означает «направленная величина». — это упорядоченный набор чисел. Записывают его жирной буквой или списком в скобках: . Числа внутри называют координатами или компонентами: , . Количество чисел — размерность вектора. Запись читается « — вектор из действительных чисел». Одиночное число, в отличие от вектора, называют скаляромскалярEN scalarОдиночное число — в отличие от вектора, матрицы или тензора..
Один и тот же вектор полезно видеть тремя способами.
- Список чисел — взгляд программиста. В Python это
[3, 1], в NumPy —np.array([3, 1]). Эмбеддинг DeepSeek-V3 — список из 7168 чисел. - Точка в пространстве — взгляд аналитика данных. Квартира площадью 54 м² на 7 этаже — точка на плоскости. Похожие квартиры лежат рядом. Похожие слова в пространстве эмбеддингов — тоже.
- Стрелка — взгляд физика. Вектор — это перемещение «на 3 вправо и на 1 вверх». У стрелки есть направление и длина, а откуда её рисовать, не важно.
Для двух и трёх измерений можно рисовать картинки. Для 7168 измерений картинку не нарисовать, но все формулы работают точно так же — просто в сумме больше слагаемых. Поэтому интуицию набирают на плоскости, а считают в коде.
Сложение и умножение на число
Заголовок раздела «Сложение и умножение на число»Векторы одинаковой размерности складывают покоординатно, а умножение на число растягивает каждую координату:
Индекс у буквы — номер координаты, как в уроке 1.0.3: — первое число вектора , — второе. Для векторов длины 3 и больше формула та же, просто координат больше.
Пример: , а . Геометрически сложение — это «пройти сначала по стрелке , потом по стрелке ». Умножение на число растягивает стрелку, а умножение на отрицательное число ещё и разворачивает её.
Сумму векторов, умноженных на числа, например , называют линейной комбинацией. Для наших и это . Это понятие станет главным в следующем уроке: умножение матрицы на вектор — это линейная комбинация её столбцов.
Где это в LLM. Каждый блок трансформера не заменяет вектор токена, а прибавляет к нему поправку: . Это остаточная связьостаточная связьEN residual connection (урок 3.4.2). Среднее нескольких векторов (сложить и умножить на ) — простейший способ получить вектор целого предложения из векторов слов. А знаменитое «король − мужчина + женщина ≈ королева» — тоже линейная комбинация (урок 4.3.1).
import numpy as np
a = np.array([3, 1])b = np.array([1, 2])print(a + b) # [4 3]print(2 * a - b) # [5 0]print((a + b) / 2) # [2. 1.5] — среднее двух векторовДлина вектора: нормы
Заголовок раздела «Длина вектора: нормы»Как измерить «размер» вектора? Способов несколько, и каждый называют нормойнорма вектораEN vector norm«Размер» вектора. L2-норма — длина по Пифагору √(Σ vᵢ²), L1-норма — сумма модулей Σ |vᵢ|.. Обозначение — двойные вертикальные черты: .
L2-норма — обычная длина стрелки. Вспомним школьную теорему Пифагора: в прямоугольном треугольнике квадрат длинной стороны (гипотенузы) равен сумме квадратов двух коротких (катетов). Стрелка — это гипотенуза треугольника с катетами 3 (вправо) и 4 (вверх), поэтому её длина — корень из :
Теорема Пифагора работает в любой размерности: . Когда индекс нормы не написан, почти всегда имеют в виду L2.
L1-норма — сумма модулей координат. Её называют «манхэттенской»: такси в городе с прямоугольными кварталами не может ехать по диагонали и проезжает кварталов вместо 5:
| Вектор | L1 | L2 | Что заметно |
|---|---|---|---|
| 7 | 5 | L1 всегда не меньше L2 | |
| 1 | 1 | вся «масса» в одной координате — нормы равны | |
| 4 | 2 | «масса» размазана — L1 заметно больше |
Расстояние между точками — это норма их разности: . Расстояние, измеренное L2-нормой, называют евклидовымевклидово расстояниеEN Euclidean distanceL2-норма разности двух векторов ‖a − b‖₂ — обычное расстояние «по прямой».. На нём построен метод ближайших соседей (урок 2.3.1).
Единичный вектор. Если поделить вектор на его длину, получится единичный векторединичный векторEN unit vectorВектор длины 1. Любой ненулевой вектор v превращается в единичный делением на его длину ‖v‖ (нормировка). с тем же направлением и длиной 1: . Эта операция — нормировка — понадобится через пару разделов.
v = np.array([3.0, 4.0])print(np.linalg.norm(v)) # 5.0 — L2 по умолчаниюprint(np.linalg.norm(v, ord=1)) # 7.0 — L1print(v / np.linalg.norm(v)) # [0.6 0.8] — единичный векторГде нормы в LLM.
- RMSNorm — нормализация в каждом слое Llama, Qwen и DeepSeek (урок 4.5.3). Она делит вектор на . RMS (root mean square) — «корень из среднего квадрата»: возвели координаты в квадрат, усреднили, взяли корень. Для это , а после деления получается — вектор длины . Как бы ни «раздувались» векторы внутри сети, после RMSNorm их длина снова (потом каждую координату ещё умножают на обучаемый вес, но это уже отдельный шаг).
- Обрезка градиентов (gradient clipping, урок 4.6.3). Градиент — вектор поправок ко всем весам модели на очередном шаге обучения (модуль 1.2). Если его L2-норма больше порога (обычно 1.0), все поправки уменьшают в одно и то же число раз, так чтобы норма стала равна порогу. Это страховка от слишком больших шагов обучения.
- L2-регуляризация (её родственник в оптимизаторах — weight decay) — штраф, который не даёт весам разрастаться и мешает модели «зазубривать» обучающие данные. Он равен сумме квадратов весов, то есть квадрату L2-нормы (уроки 1.4.2 и 2.1.2).
Скалярное произведение
Заголовок раздела «Скалярное произведение»Скалярное произведениескалярное произведениеEN dot product (inner product)a · b = Σ aᵢbᵢ = ‖a‖‖b‖cos θ. Основа нейрона, внимания (q · k) и выходного слоя LLM. двух векторов одинаковой размерности — это сумма попарных произведений координат. Результат — одно число, скаляр (отсюда и название):
Пример: . Знак из урока 1.0.3 здесь превращается в цикл:
a, b = [1, 2, 3], [4, 5, 6]total = 0for x, y in zip(a, b): # идём по парам координат total += x * yprint(total) # 32print(np.dot(a, b), np.array(a) @ np.array(b)) # 32 32 — то же в NumPyЗаметьте: скалярное произведение вектора на себя — это квадрат длины, .
Геометрический смысл
Заголовок раздела «Геометрический смысл»Та же величина записывается через длины и угол («тета») между стрелками:
Проверим на и . По координатам: . Через угол: длины равны и , угол между стрелками 45°, , и . Сошлось.
Отсюда главное правило чтения знака:
| Скалярное произведение | Угол | Смысл |
|---|---|---|
| больше 0 | острый | векторы «смотрят в одну сторону» |
| равно 0 | прямой | векторы ортогональныортогональные (перпендикулярные) векторыEN orthogonal vectorsВекторы, скалярное произведение которых равно нулю. Ортогональная матрица — та, у которой столбцы единичные и попарно ортогональны. (перпендикулярны) — «не связаны» |
| меньше 0 | тупой | векторы «смотрят в разные стороны» |
Ещё одна интерпретация: если — единичный вектор, то — длина тени (проекции) стрелки на направление . Скалярное произведение измеряет, «сколько в направления ».
Где скалярное произведение в LLM
Заголовок раздела «Где скалярное произведение в LLM»- Нейрон считает — та самая «линейная функция многих переменных» из урока 1.0.1. Выход большой, когда вход похож на «шаблон» . Нейрон — детектор узора.
- Внимание (урок 4.4.2): насколько токен интересуется токеном , решает скалярное произведение запроса на ключ .
- Выходной слой: логитлогитEN logitСтрого — логарифм шансов log(p / (1 − p)). В нейросетях логитами называют любые выходы до softmax или сигмоиды. каждого токена словаря — скалярное произведение последнего скрытого вектора на вектор этого токена. У DeepSeek-V3 словарь из 129 280 токенов, значит, на каждом шаге генерации — 129 280 скалярных произведений векторов длины 7168.
Стоимость одного скалярного произведения — умножений и сложений, примерно операций. Запомните эту оценку: в уроке 5.1.1 из неё вырастет формула стоимости обучения целой модели.
Косинусное сходство
Заголовок раздела «Косинусное сходство»Скалярное произведение зависит и от угла, и от длин. Если нас интересует только направление — «про одно и то же ли эти векторы» — длины нужно убрать. Поделим на них и получим косинусное сходствокосинусное сходствоEN cosine similarityКосинус угла между векторами — скалярное произведение, делённое на произведение длин. Для нормированных векторов равно скалярному произведению.:
Это число всегда лежит между −1 и 1. Для и : длины и , косинус , угол около 13°. Векторы почти сонаправлены, хотя второй длиннее первого больше чем вдвое.
Почему для эмбеддингов берут косинус. Длина вектора часто несёт «посторонний» сигнал — например, зависит от частоты слова или длины текста. Нам же важно, о чём текст. Косинус сравнивает только направления: вектор и вектор для него одинаковы.
Трюк с нормировкой. Если заранее поделить все векторы на их длины, знаменатель станет равен 1, и косинус превратится в обычное скалярное произведение. Тогда сходство запроса со всеми документами базы считается одним матричным умножением — так работает поиск в RAG (урок 8.2.1). Иногда говорят о косинусном расстоянии : 0 для одинаковых направлений, 2 для противоположных.
Пример: «слова-векторы»
Заголовок раздела «Пример: «слова-векторы»»Возьмём игрушечные эмбеддинги с четырьмя осями, смысл которых мы придумали сами: «живое», «маленькое», «пьют», «связано с Китаем». В настоящих моделях осей сотни и тысячи, и подписей у них нет, но принцип тот же.
| Слово | живое | маленькое | пьют | Китай |
|---|---|---|---|---|
| кошка | 0.9 | 0.8 | 0.0 | 0.1 |
| cat (англ. «кошка») | 0.85 | 0.75 | 0.05 | 0.1 |
| собака | 0.9 | 0.3 | 0.0 | 0.2 |
| чай | 0.0 | 0.1 | 0.9 | 0.7 |
| tea (англ. «чай») | 0.05 | 0.1 | 0.85 | 0.75 |
| кофе | 0.0 | 0.0 | 0.9 | 0.2 |
Косинусы считаем в NumPy сразу для всех пар: нормируем строки и умножаем матрицу на её транспонированную
(матричное умножение @ вы видели в уроке 0.3.2, подробно — в следующем уроке):
import numpy as np
words = ["кошка", "cat", "собака", "чай", "tea", "кофе"]E = np.array([ [0.9, 0.8, 0.0, 0.1], [0.85, 0.75, 0.05, 0.1], [0.9, 0.3, 0.0, 0.2], [0.0, 0.1, 0.9, 0.7], [0.05, 0.1, 0.85, 0.75], [0.0, 0.0, 0.9, 0.2],])En = E / np.linalg.norm(E, axis=1, keepdims=True) # каждую строку — на длину 1S = En @ En.T # все косинусы разом: (6, 4) @ (4, 6) → (6, 6)np.set_printoptions(precision=2, suppress=True)print(S)[[1. 1. 0.91 0.11 0.15 0.02] [1. 1. 0.91 0.15 0.18 0.06] [0.91 0.91 1. 0.15 0.2 0.04] [0.11 0.15 0.15 1. 1. 0.9 ] [0.15 0.18 0.2 1. 1. 0.87] [0.02 0.06 0.04 0.9 0.87 1. ]]Читаем таблицу: «кошка» и «cat» почти совпадают (0.999, после округления 1.0), «кошка» и «собака» близки (0.91), а «кошка» и «кофе» почти перпендикулярны (0.02). Многоязычные модели эмбеддингов устроены так же: «кошка» и «cat» у них оказываются рядом, потому что значат одно и то же.
Сюрприз многомерных пространств
Заголовок раздела «Сюрприз многомерных пространств»На плоскости два случайных направления легко оказываются близкими. А в пространстве размерности 4096? Проверим:
import numpy as np
rng = np.random.default_rng(0)for n in [2, 10, 100, 768, 4096]: # размерность векторов a = rng.standard_normal((1000, n)) # 1000 случайных векторов — строки матрицы b = rng.standard_normal((1000, n)) # и ещё 1000 # (a * b).sum(axis=1) — 1000 скалярных произведений пар «строка a, строка b» сразу (урок 0.3.2), # norm(..., axis=1) — длины всех строк; делим одно на другое и получаем 1000 косинусов cos = (a * b).sum(axis=1) / (np.linalg.norm(a, axis=1) * np.linalg.norm(b, axis=1)) print(f"n = {n:5d} средний |cos| = {np.abs(cos).mean():.3f} max |cos| = {np.abs(cos).max():.3f}")n = 2 средний |cos| = 0.644 max |cos| = 1.000n = 10 средний |cos| = 0.244 max |cos| = 0.818n = 100 средний |cos| = 0.079 max |cos| = 0.292n = 768 средний |cos| = 0.029 max |cos| = 0.124n = 4096 средний |cos| = 0.013 max |cos| = 0.060Чем больше размерность, тем ближе к нулю косинус случайной пары: типичное значение порядка . В многомерном пространстве почти все направления почти перпендикулярны друг другу. Из этого два практических вывода:
- Для случайных векторов размерности 1024 типичный косинус около 0.03, так что значение 0.3 для них было бы невероятным совпадением. Но у обученных моделей эмбеддингов векторы обычно сбиты в узкий «конус» (это называют анизотропией), и даже у текстов на совсем разные темы косинус часто заметно больше нуля. Поэтому абсолютное значение косинуса без контекста мало что значит: порог «похоже / не похоже» подбирают под конкретную модель, а при поиске смотрят прежде всего на порядок кандидатов.
- В пространстве из тысяч измерений помещается огромное число почти независимых направлений. Модель может «разложить» по ним много разных понятий, и они почти не будут мешать друг другу.
Практика
Заголовок раздела «Практика»Реализуйте скалярное произведение, нормы, косинус и поиск ближайших слов на чистом Python — без NumPy, чтобы каждая формула прошла через ваши руки. Словарь «слов-векторов» уже в заготовке.
import math
# Игрушечные «эмбеддинги»: 4 оси с понятным смыслом
# [живое, маленькое, пьют, связано с Китаем]. Настоящие эмбеддинги — сотни осей без подписей.
# Английские слова — переводы русских: в многоязычной модели их векторы почти совпадают.
WORDS = {
"кошка": [0.9, 0.8, 0.0, 0.1],
"cat": [0.85, 0.75, 0.05, 0.1],
"собака": [0.9, 0.3, 0.0, 0.2],
"dog": [0.85, 0.35, 0.0, 0.2],
"чай": [0.0, 0.1, 0.9, 0.7],
"tea": [0.05, 0.1, 0.85, 0.75],
"кофе": [0.0, 0.0, 0.9, 0.2],
"дракон": [0.6, 0.0, 0.0, 0.95],
"dragon": [0.55, 0.05, 0.0, 0.9],
}
def dot(a, b):
"""Скалярное произведение: a[0]*b[0] + a[1]*b[1] + ...
Пример: dot([1, 2, 3], [4, 5, 6]) → 1·4 + 2·5 + 3·6 = 32.
Если длины векторов разные — бросьте ValueError.
"""
# TODO: 1) сравните len(a) и len(b); 2) total = 0; 3) в цикле по парам zip(a, b) копите x * y; 4) return total
...
def norm(a, p=2):
"""Норма вектора. p=2 — длина (корень из суммы квадратов), p=1 — сумма модулей.
Примеры: norm([3, 4]) → √(9 + 16) = 5.0, norm([3, -4], p=1) → |3| + |−4| = 7.
"""
# TODO: если p == 1 — сумма abs(x); иначе — math.sqrt от суммы x * x
...
def cosine(a, b):
"""Косинусное сходство: dot(a, b) / (|a| * |b|). Для нулевого вектора верните 0.0.
Примеры: cosine([1, 2], [10, 20]) → 1.0 (одно направление), cosine([1, 0], [0, 5]) → 0.0.
"""
# TODO: посчитайте обе длины через norm; если одна из них 0 — return 0.0; иначе делите dot на произведение длин
...
def most_similar(word, vectors, k=3):
"""k слов из словаря vectors, самых похожих на word по косинусу (по убыванию).
Само слово word в ответ не входит. Возвращает список строк.
Пример: most_similar("чай", WORDS, k=2) → ['tea', 'кофе'].
"""
# TODO: соберите пары (косинус, слово) для всех слов, кроме word; отсортируйте по убыванию; верните первые k слов
...
# Проверьте себя кнопкой «Запустить»:
print(dot([1, 2, 3], [4, 5, 6])) # 32
print(norm([3, 4]), norm([3, 4], p=1)) # 5.0 7
print(cosine([1, 2, 3], [4, 5, 6])) # ≈ 0.9746
print(most_similar("кошка", WORDS)) # ['cat', 'dog', 'собака']
Задание для Jupyter (по желанию). Поменяйте в словаре вектор «кофе» на [0.0, 0.0, 0.9, 0.9] («очень китайский
кофе»). Насколько вырос косинус между «кофе» и «tea»? Удалось ли «кофе» обогнать «чай» в списке ближайших к «tea»?
Объясните результат, сравнив координаты векторов. Затем умножьте вектор «кошка» на 10 и убедитесь, что
most_similar выдаёт те же слова, а скалярные произведения — уже нет.
Проверь себя
Заголовок раздела «Проверь себя»- Вектор — это список чисел, точка и стрелка одновременно. Складываем и умножаем на число покоординатно (в NumPy, а не в списках Python!).
- L2-норма — длина по Пифагору, L1 — сумма модулей. На нормах держатся RMSNorm, обрезка градиентов и weight decay.
- Скалярное произведение — основа нейрона, внимания и выходного слоя LLM.
- Косинусное сходство сравнивает только направления. После нормировки оно равно скалярному произведению, и поиск похожих эмбеддингов становится матричным умножением.
- В многомерном пространстве случайные векторы почти перпендикулярны: их типичный косинус порядка . У обученных моделей эмбеддингов шкала косинуса своя, и порог сходства подбирают под модель.
Дальше: матрицы — как одно умножение превращает вектор в другой вектор и почему линейный слой нейросети — это матрица.