高中觉得只是"带箭头的线段"的向量,其实是大模型把"意思"变成"方向"的地基

2026-07-05 · Steve Chan

向量、坐标、模长、数量积(点积)、向量加减……高中学了一整章,考完只记得"力的分解"和"a·b=|a||b|cosθ"。 因为老师只教你"算箭头、求夹角、解物理题",没告诉你两件事:

第一,向量的灵魂不是"带箭头的线段",是"用一个方向来表示一个意思"。 第二,今天每一个大模型(LLaMA、Qwen、GPT、DeepSeek)之所以懂"猫和狗很像、和苹果不像", 靠的正是把每个词变成一个向量,再用点积去量"两个方向有多一致"——这套东西撑起了整个注意力机制。

这篇文章不背一个公式、不做一道受力分析,全部用能跑起来的 PyTorch 代码, 把向量最核心的"方向 = 意义、点积 = 相关度"本质,一路接到大模型的注意力。 并且顺手回答四个灵魂拷问:

  • 向量明明是算力、算位移的,怎么会和"词义""大模型"扯上关系?
  • 一个"词"到底怎么变成向量的?这串坐标里藏着什么?
  • 为什么衡量两个词像不像,偏偏用"点积",而不是别的算法?
  • 注意力里的 Q·K 除以 √d 是在干嘛?点积和高中的"夹角、模长"又是什么关系?

0. 一句话主线

如果只能留一句话,那就是这句:

向量把"意思"编码成"方向";点积衡量"两个方向有多一致",也就是"两个东西有多相关"。

意思相近的词,方向就相近;点积就大。意思无关的词,方向近乎垂直;点积就趋近 0。 大模型判断"这个词该关注那个词多少",从头到尾就是在做一件事——算两个向量的点积。

import torch

# 向量就是一串坐标 —— 空间里的一个"位置 / 方向"
v = torch.tensor([3.0, 4.0])
print("向量:", v)
print("模长(箭头有多长):", v.norm())      # 5.0,正是高中的勾三股四

记住这个画面:向量 = 空间里的一根箭头,它的"方向"才是灵魂,"长度"只是次要属性。 下面所有东西都挂在"方向表示意义"这一件事上。


1. 向量不是箭头,是"空间里的一个方向"

高中把向量画成一根带箭头的线段,把你的注意力锁在"长度和角度"上。 但真正有用的视角是:一串坐标,就唯一确定了空间里的一个方向。 把长度归一化掉,只看方向:

import torch

a = torch.tensor([1.0, 1.0])
b = torch.tensor([2.0, 2.1])     # 坐标不同,但和 a 几乎指同一个方向
c = torch.tensor([-1.0, -0.9])   # 和 a 几乎指相反方向

for name, v in [("a", a), ("b≈同向", b), ("c≈反向", c)]:
    print(f"{name} 的纯方向:", (v / v.norm()).round(decimals=2))
# a 和 b 归一化后几乎一样(同向);c 是它的负方向(反向)

b 的坐标是 a 的两倍多,但归一化后方向几乎重合c 则指向正相反。 这就是关键的视角切换:我们真正关心的从来不是箭头多长,而是它指向哪。 下一节的"点积",量的正是"两根箭头指的方向有多一致"。


2. 点积:衡量"两个方向有多一致"的标尺

高中的数量积 a·b,被埋在"求夹角、求投影"的题里。但它最有用的身份只有一个——相关度标尺

import torch

a = torch.tensor([1.0, 0.0])
for name, v in [("同向", torch.tensor([1.0, 0.0])),
                ("垂直", torch.tensor([0.0, 1.0])),
                ("反向", torch.tensor([-1.0, 0.0]))]:
    print(f"{name}: 点积 a·v = {torch.dot(a, v):+.1f}")
# 同向 -> +1(最相关)   垂直 -> 0(毫不相关)   反向 -> -1(最相反)

点积把"两个方向的关系"压缩成一个数:同向为正、越同向越大;垂直为 0;反向为负。 如果再把长度的影响除掉,就得到只看方向的"余弦相似度"——大模型判断词义相似度用的就是它:

import torch
import torch.nn.functional as F

a = torch.tensor([2.0, 1.0])
b = torch.tensor([4.0, 2.0])                       # b 就是 2a,方向完全一致
print("余弦相似度:", F.cosine_similarity(a, b, dim=0).item())   # 1.0:方向一样就是 1

1.0方向完全一致,余弦相似度就是 1,跟长度差多少倍无关。 记住这把标尺:点积(及其归一化版余弦)= 两个向量有多"像"。 它是后面一切的核心零件。


🤔 疑惑点一:一个"词"到底怎么变成向量的?这串坐标里藏着什么?

模型里有一张巨大的"词 → 向量"对照表(叫嵌入表 embedding),每个词对应表里的一行坐标。这些坐标不是人写死的,是训练出来的:模型读了海量文本后,自动把"经常出现在相似语境里的词",摆到了相似的方向上。所以坐标里藏着的,就是这个词的"意思"。

第一步很朴素——每个词就是查一张表,取出属于它的那一行:

import torch
import torch.nn as nn

torch.manual_seed(0)
vocab = ["猫", "狗", "苹果"]
emb = nn.Embedding(len(vocab), 4)      # 3 个词,每个用一个 4 维向量表示
ids = torch.tensor([0, 1, 2])          # 词的编号
print(emb(ids).round(decimals=2))      # 取出每个词对应的那一行"坐标"

刚初始化时这些坐标是随机的,什么也不代表。但训练之后,这张表会自我组织: "猫""狗"这种常一起出现的词,方向会被拉到一起;"苹果"会被推到另一个方向。 下一节我们直接摆出训练完的效果——意思,变成了几何。


3. 词向量:把"意思"摆进空间,意思近的方向就近

训练好的词向量最迷人的一点是:语义相似度,直接变成了几何上的方向相似度。 手工摆三个词向量,感受一下"意思近 → 点积大":

import torch
import torch.nn.functional as F

vec = {
    "猫":   torch.tensor([0.90, 0.10]),
    "狗":   torch.tensor([0.80, 0.20]),   # 和"猫"同为动物 -> 方向接近
    "苹果": torch.tensor([0.10, 0.90]),   # 水果 -> 指向另一个方向
}

def cos(x, y):
    return F.cosine_similarity(vec[x], vec[y], dim=0).item()

print("猫 vs 狗  :", round(cos("猫", "狗"), 3))    # 高:意思近,方向近
print("猫 vs 苹果:", round(cos("猫", "苹果"), 3))  # 低:意思远,方向远

"猫"和"狗"的余弦接近 1,"猫"和"苹果"则很低。在这个空间里,"相似"不再是抽象的语感, 而是一个能算出来的角度。 你能在词向量空间里做"最近邻搜索"找近义词、能给文本聚类、 能做语义检索(RAG 的召回底层就是这个)——全都建立在"意思 = 方向"这一步之上。


4. 向量加减 = 语义运算:国王 - 男人 + 女人 ≈ 王后

既然"意思"变成了空间里的方向,那"意思之间的关系"就变成了向量的加减。 这就是词向量最著名的魔法——你可以对"意义"做算术:

import torch
import torch.nn.functional as F

king  = torch.tensor([0.90, 0.70, 0.20])
man   = torch.tensor([0.80, 0.10, 0.10])
woman = torch.tensor([0.20, 0.10, 0.80])
queen = torch.tensor([0.35, 0.72, 0.88])

guess = king - man + woman              # 向量加减 = 语义加减
print("king - man + woman =", guess.round(decimals=2))
print("和 queen 的余弦相似度:", round(F.cosine_similarity(guess, queen, dim=0).item(), 3))

king - man + woman 得到的向量,和 queen 的方向几乎重合(余弦≈1)。 "从国王里减掉'男性'、加上'女性'"这句人话,在向量空间里是一次真实的加减运算。 "性别""王室身份"这些抽象概念,各自对应着空间里的一个方向——这就是为什么大模型能做类比、 能理解"巴黎之于法国,如同东京之于日本"。语义关系,被编码成了几何位移。


5. 注意力的内核,就是一堆点积

现在把第 2 节的"点积 = 相关度"接到大模型最核心的注意力上。 注意力要做的事,一句话:让每个词去"关注"和它最相关的其他词。 而"相关度",就是点积:

import torch
import torch.nn.functional as F

torch.manual_seed(0)
X = torch.randn(3, 4)                 # 3 个词,每个 4 维向量

scores = X @ X.T                      # 两两做点积 -> 相关度矩阵
weights = F.softmax(scores, dim=-1)   # 每行归一化成"注意力分配"

print("相关度(点积)矩阵:\n", scores.round(decimals=2))
print("每个词该关注谁(softmax 后):\n", weights.round(decimals=2))

X @ X.T 就是让每个词和每个词做一次点积,得到一张"谁和谁多相关"的矩阵; 再用 softmax 把每一行变成"注意力该怎么分配"。注意力机制里那句吓人的 softmax(QKᵀ/√d)V, 拆开看,QKᵀ 就是你高中学的点积在做"批量相关度计算"。 (真实的 Q、K 是词向量再各乘一个投影矩阵,但内核——"用点积量相关度"——分毫不差。)


🤔 疑惑点二:Q·K 为什么要除以 √d?点积和高中的"夹角、模长"是什么关系?

点积天生等于 |a||b|cosθ——它同时被"两个向量的长度"和"夹角"影响。而向量维度 d 越高,点积的数值就天然越大(一堆维度的乘积累加起来)。维度动辄上千时,点积会大到让 softmax 饱和、梯度消失,所以要除以 √d 把它拉回正常范围。这个 √d,就是在给"维度带来的膨胀"做校正。

先把高中那条 a·b = |a||b|cosθ 用代码还原一遍,确认点积和"模长×夹角"是一回事:

import torch
import math

a = torch.tensor([3.0, 4.0])
b = torch.tensor([4.0, 3.0])

dot = torch.dot(a, b)
cos = dot / (a.norm() * b.norm())
print("点积 a·b       :", dot.item())                        # 24
print("= |a||b|cosθ   :", round((a.norm() * b.norm() * cos).item(), 2))  # 也是 24
print("夹角(度)       :", round(math.degrees(math.acos(cos.item())), 1)) # ≈16.3°

点积确实就是 |a||b|cosθ。再看维度带来的"数值膨胀",以及 √d 如何救场:

import torch

torch.manual_seed(0)
for d in [4, 64, 1024]:
    q, k = torch.randn(d), torch.randn(d)
    raw = torch.dot(q, k)
    print(f"d={d:5d}  原始点积≈{raw:7.2f}   除以√d 后≈{raw / d ** 0.5:6.2f}")
# 维度越高原始点积越大越野;除以 √d 后,各维度都被拉回 O(1) 的稳定范围

维度从 4 涨到 1024,原始点积的量级越来越野;而除以 √d 之后,数值稳稳待在个位数。 softmax(QKᵀ/√d) 里那个不起眼的 √d,就是在补偿"高维让点积膨胀"这件事—— 让注意力分数不管在多少维里,都落在 softmax 最灵敏、梯度最健康的区间。你高中的"模长、夹角", 在这里以最朴素的方式,决定了整个大模型能不能训得动。


🎬 动手:一个最小注意力,看点积如何让"同类互相吸引"

把上面所有画面缝进一个能跑的最小注意力:手工摆 4 个词向量,看点积如何决定"谁关注谁":

import torch
import torch.nn.functional as F

words = ["cat", "kitten", "car", "truck"]
X = torch.tensor([[1.0, 0.1],      # cat
                  [0.9, 0.2],      # kitten —— 和 cat 几乎同向
                  [0.1, 1.0],      # car
                  [0.2, 0.9]])     # truck  —— 和 car 几乎同向

scores = X @ X.T / X.shape[1] ** 0.5     # 点积 / √d
scores.fill_diagonal_(float("-inf"))     # 先不让词只顾自己
weights = F.softmax(scores, dim=-1)

for i, w in enumerate(words):
    print(f"{w:7s} 最关注 -> {words[weights[i].argmax()]}")
# cat 最关注 kitten,car 最关注 truck —— 点积让方向相近的词互相吸引

cat 把注意力给了 kittencar 把注意力给了 truck——因为它们的向量方向相近,点积最大。 这就是注意力的全部秘密:方向相近 = 点积大 = 注意力高。 语义上的"相关", 被向量的点积翻译成了模型能算的数字。

仓库里的动画脚本把这个"词向量自我组织"的过程画了出来:

python word_vectors_visualization.py

左边是一堆随机初始化的词向量箭头,训练时你会看到"动物"那几根慢慢转到一起、"水果"那几根转向另一边—— 意思相近的词,方向真的在往一块儿聚; 右边则是它们两两之间的余弦相似度热力图,从一片模糊逐渐变得"同类发亮、异类变暗"。 最后你会直观看到:训练,就是在把"意义"一点点摆进空间的方向里。


缝合:把所有画面接起来

回到开头,现在每个概念都有了画面和代码出处:

向量概念 高中怎么讲 这篇文章怎么看(画面) 在大模型里是什么
向量 / 坐标 带箭头的线段 空间里的一个"方向"(第 0、1 节) 每个词的"意思"(词向量)
点积 a·b 数量积、求夹角 衡量"两个方向多一致"的标尺(第 2 节) 注意力里的"相关度"
余弦相似度 cosθ = a·b/|a||b| 只看方向的"像不像"(第 2、3 节) 语义检索 / 找近义词
向量加减 平行四边形法则 语义加减:国王-男人+女人(第 4 节) 类比推理、概念运算
a·b=|a||b|cosθ 一条要背的公式 点积被模长和维度撑大(疑惑点二) QKᵀ 要除以 √d 的原因

三句话总结这篇文章:

向量的灵魂是"用方向表示意义"——意思近的词,方向就近(第 1、3 节); 点积是"衡量两个方向多一致"的标尺,也就是相关度,归一化后就是余弦相似度(第 2 节); 于是注意力的内核 QKᵀ 就是一堆点积,让每个词去关注和它方向最近(意思最相关)的词(第 5 节、疑惑点二)。

当年向量学得那么枯燥,不是因为它难,是因为没人告诉你: 那根你以为只用来分解力的箭头,最后变成了每个词的"意思", 而高中那个 a·b 的点积,成了大模型判断"该关注谁"的唯一标尺。 现在把上面每段代码跑一遍,比高中刷十套向量题都值。


备注(选题/标题): 这篇走的是"高中知识其实是 AI 基石"的钩子(和《sin 和 cos》《对数》《求根公式》同一路子)。 若想并入"大学4年"主系列,标题可换成: 《大学4年没让你真正搞懂的向量,被一段注意力代码讲透了》。 与《线性代数》那篇有承接:那篇讲"矩阵是变换",这篇讲"向量是意义",可互相引流; 与《对数》《sin 和 cos》共同拼出"注意力"的三块地基(相关度、损失、位置)。