算法可视化与交互学习平台

学习 Embedding:从 Token 表示到语义向量空间Embedding: From Tokens to Semantic Vector Spaces

作为 RAG → Agent 系列的起点,从 text → tokenizer → embedding → vector space → cosine similarity 建立完整数学与工程直觉;通过 2D/3D 空间、Pooling、归一化、相似度矩阵和 Top-k 检索实验,把经典词向量自然过渡到 modern sentence embedding。

RAG & AgentsBeginnerFree
Kernel
1

RAG 的第一块地基:把文本变成可以比较的坐标

No.9 已经给出关键边界:**领域知识不应该全部塞进 LoRA 权重,而应由 RAG 在需要时检索。** 但检索系统首先要回答一个更基础的问题:计算机怎样判断 query 与 document 在语义上接近?

text → tokenizer → token embeddings → contextual encoder → pooling → sentence / document vector → similarity → top-k

本模块不把 Embedding 当成黑盒 API,也不使用手写语义轴或模拟向量。全部语言实验调用同一个多语言 MiniLM 模型,在服务器 CPU 上执行真实 tokenizer、Transformer forward、Pooling、Normalization、PCA 与最近邻检索;每组实验附有可复制的 Python 代码。

2

No.14–18:从语义坐标走到可行动的 Agent

模块核心问题交付给下一站
No.14 Embedding文本怎样成为可比较的语义向量?query / chunk vectors、similarity、top-k
No.15 Vector Searchchunk 数增长后,怎样用 Exact / HNSW / IVF 高效找回近邻?可评估的向量索引与 search contract
No.16 RAG Pipeline怎样组合 chunking、retrieval、context、citation 与 evaluation?有来源、可诊断的生成回答
No.17 ReAct Agent模型怎样依据 Observation 选择下一动作,并在预算内恢复、停止?可控的 state → action → observation 循环
No.18 Planning Agent怎样把完整 Goal 分解成 DAG,执行、追踪并在结构性失败后重新规划?可验收的全局任务完成闭环
Embedding 决定“哪些内容看起来接近” Vector Search 决定“以多快、多少召回损失找到它” RAG 决定“怎样把证据交给生成模型” ReAct 决定“当前下一步做什么” Planning 决定“整个任务怎样完成”
3
Concept 01 · Parameter table, autograd & checkpoint

先消除歧义:参数表、上下文表示、句向量与相关性分数

先严格区分四个常被统称为 Embedding 的量,再深入拆解 nn.Embedding:词表如何确定行地址、weight 怎样初始化、训练目标怎样经 backward 聚合到参数行、optimizer 怎样更新并保存 checkpoint。页面同时读取当前多语言 MiniLM 的真实 250,037×384 参数表,并执行可逐项复核的 PyTorch autograd 实验。

准确名称形状示例怎样得到用途是 embedding?
Token embedding table row[d_model]token id 从可训练参数矩阵 W 中选中的一行;尚未结合上下文Transformer 输入的组成部分
Contextual token state[tokens, d_model]经过 Transformer 后、已经吸收整句上下文的每个 token 表示NER、token prediction、下游 pooling通常是
Sentence / chunk embedding[d_embedding]把 contextual states 聚合并按训练约定处理后的单一文本向量RAG、聚类、去重、语义搜索
Cross-encoder relevance score[1]query 与 document 联合编码后输出的相关性标量,不是向量表示对召回结果做 rerank不是
词表确定地址

Tokenizer 先固定 token ↔ id 映射。id 只回答“访问 W 的哪一行”,相邻 id 没有相邻语义。

分配并初始化参数

创建 W∈ℝ^{V×d}。随机初值只打破对称性,此时各行还没有可靠语义。

前向查表并完成任务

W[input_ids] 进入后续网络,模型依据 MLM、对比学习等真实训练目标计算 loss。

反向传播聚合梯度

同一 token 在批次出现多次时,对应位置的梯度会累加到 W 的同一行;未出现行通常没有本步梯度。

优化器更新并保存

optimizer 修改被访问的参数行;海量批次反复更新后,checkpoint 保存学到的 W,推理时再加载并只做查表。

Embedding lookup 的核心计算
token_id = input_ids[0, position]
embedding_table = model.get_input_embeddings().weight
vector = embedding_table[token_id]

one_hot = F.one_hot(token_id, num_classes=embedding_table.shape[0])
same_vector = one_hot.to(embedding_table.dtype) @ embedding_table

W 的数值由训练学习;k 只负责选择哪一行。下面的实验直接读取模型当前参数,不使用手写向量。

真实 CPU 推理 · sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
首次运行会下载模型到服务器缓存;之后复用同一常驻 Python worker。加载失败时不会回退到模拟向量。
尚未生成任何结果。点击“运行真实实验”后,图表、矩阵和计算步骤才会出现。
真实 MiniLM 代码:读取已训练参数表、验证 lookup、计算完整 BERT embedding 子层
import torch
import torch.nn.functional as F
from transformers import AutoModel, AutoTokenizer

MODEL_ID = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
TEXT = "The bank approved the loan."
TOKEN_POSITION = 1

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
model = AutoModel.from_pretrained(MODEL_ID).eval().to("cpu")
batch = tokenizer([TEXT], return_tensors="pt", truncation=True, max_length=128)

with torch.inference_mode():
    input_ids = batch["input_ids"]
    embedding_layer = model.get_input_embeddings()
    embedding_table = embedding_layer.weight
    token_id = input_ids[0, TOKEN_POSITION]

    # 实际 forward:nn.Embedding 使用整数索引选择参数矩阵的一行。
    lookup_by_index = embedding_table[token_id]

    # 仅用于验证代数等价;生产推理不会构造这个巨大 one-hot。
    one_hot = F.one_hot(token_id, num_classes=embedding_table.shape[0])
    lookup_by_one_hot = one_hot.to(embedding_table.dtype) @ embedding_table
    torch.testing.assert_close(lookup_by_index, lookup_by_one_hot)

    # 当前 BERT/MiniLM 的完整“模型输入 embedding 子层”还会加入位置与类型。
    token_type_ids = batch.get("token_type_ids", torch.zeros_like(input_ids))
    model_input = model.embeddings(
        input_ids=input_ids,
        token_type_ids=token_type_ids,
    )  # word + position + token_type → LayerNorm → Dropout(eval 时关闭)

tokens = tokenizer.convert_ids_to_tokens(input_ids[0].tolist())
parameter_name = next(
    name for name, parameter in model.named_parameters()
    if parameter is embedding_table
)
print("tokens:", tokens)
print("selected token / id:", tokens[TOKEN_POSITION], token_id.item())
print("module:", embedding_layer)
print("parameter:", parameter_name)
print("embedding table shape:", tuple(embedding_table.shape))
print("parameter count:", embedding_table.numel())
print("memory bytes:", embedding_table.numel() * embedding_table.element_size())
print("lookup[:8]:", lookup_by_index[:8].tolist())
print("max lookup error:", (lookup_by_index - lookup_by_one_hot).abs().max().item())
print("model input[:8]:", model_input[0, TOKEN_POSITION, :8].tolist())
真实 PyTorch 代码:nn.Embedding 初始化、forward、backward 与 SGD 更新
import torch
import torch.nn.functional as F

# 固定种子让页面与读者本地得到同一组真实 PyTorch 初值。
torch.manual_seed(17)
V, d, padding_idx = 6, 3, 0
learning_rate = 0.25
input_ids = torch.tensor([[1, 2, 2, 0], [3, 2, 4, 0]])
target_value = 0.5

# 原生 nn.Embedding:W 是 Parameter,不是由 token id 临时算出的数组。
embedding = torch.nn.Embedding(V, d, padding_idx=padding_idx)
optimizer = torch.optim.SGD(embedding.parameters(), lr=learning_rate)
optimizer.zero_grad(set_to_none=True)
W_before = embedding.weight.detach().clone()

# 1) forward lookup
Y = embedding(input_ids)

# 2) 用小词表真实验证 one_hot(input_ids) @ W 与 lookup 完全相同
one_hot = F.one_hot(input_ids, num_classes=V).to(W_before.dtype)
Y_by_one_hot = one_hot @ W_before
torch.testing.assert_close(Y, Y_by_one_hot)

# 3) 一个真实执行、但目标受控的 MSE 机制审计步。
# 0.5 是为了便于手算而选择的常数,不是语料提供的语义标签。
target = torch.full_like(Y, target_value)
Y.retain_grad()
loss = F.mse_loss(Y, target, reduction="mean")
loss.backward()

# 4) 按微积分公式手算应有的行梯度,再与 autograd 比较
manual_grad = torch.zeros_like(W_before)
for token_id in input_ids.flatten().tolist():
    if token_id != padding_idx:
        manual_grad[token_id] += 2 * (
            W_before[token_id] - target_value
        ) / Y.numel()
torch.testing.assert_close(embedding.weight.grad, manual_grad)

# 5) SGD 更新:W_new = W_old - learning_rate * dL/dW
optimizer.step()
W_after = embedding.weight.detach().clone()

print("input_ids:\n", input_ids)
print("W_before:\n", W_before)
print("Y shape:", tuple(Y.shape))
print("loss:", loss.item())
print("dL/dY:\n", Y.grad)
print("dL/dW:\n", embedding.weight.grad)
print("W_after:\n", W_after)
print("one-hot max error:", (Y.detach() - Y_by_one_hot).abs().max().item())
print("gradient max error:", (embedding.weight.grad - manual_grad).abs().max().item())
4
Interactive 01 · Real tokenizer & hidden states

实验:text → tokenizer → embedding → pooling → vector

承接上一卡已经解释清楚的模型输入 embedding 子层:输入中英文句子,由真实多语言 MiniLM tokenizer 与 12 层 Transformer 在 CPU 上计算 contextual hidden states,再对同一真实结果切换 Mean / [CLS] / Max Pooling 和 L2 normalization,得到固定 384 维句向量。

Model card · 当前真实编码器

paraphrase-multilingual-MiniLM-L12-v2

12-layer BERT encoder384 dimensions50 languagesCPU inference

它是 Sentence-Transformers 体系中的多语言句子编码器:把句子或段落映射到固定 384 维空间,适合语义搜索、聚类和相似句比较。它不是生成式语言模型,也不是把 query 与 document 拼接后打分的 cross-encoder;在 RAG 中它承担的是可离线预计算的 bi-encoder 向量生成。

为什么本实验选择它?怎样得到正确的 sentence embedding?
真实输出并不是直接的一条句向量

AutoModel 首先返回形状为 [batch, tokens, 384] 的 contextual token states。必须再按 attention mask 做 mean pooling,才能得到 [batch, 384] 的句向量;这也是该模型发布时给出的 Transformers 用法。

适合作为 RAG 数学基础实验

384 维比大型生成模型的 hidden size 更轻,模型可在 CPU 常驻运行;多语言词表让中英文输入使用同一个向量空间。页面因此能用同一真实 encoder 演示 pooling、normalization、cosine 与 top-k。

不额外引入 sentence-transformers 依赖

项目直接复用已经安装的 torchtransformers,显式实现 tokenizer、forward 和 masked mean pooling。这样每一步都可见、可复制,也更适合只有 CPU 的服务器。

本页的实验约定

模型架构最多支持 512 个位置;页面为控制 CPU 延迟把单次输入截断到 128 tokens。Mean pooling 是默认句向量路径,[CLS] 与 Max 只作为对照实验。更换模型、pooling 或 normalization 后,旧向量不能继续混用。

这个 checkpoint 是怎样训练出来的?(教师模型 → 多语言学生 → 384 维蒸馏目标)

它不是一次训练直接得到的,而是把两条已经训练过的分支汇合起来:英文教师负责提供“什么样的句向量空间是有语义的”,多语言学生负责读懂不同语言的 token。最后再用平行翻译句把学生的多种语言映射到教师的同一语义坐标。

1 · 先得到英文句向量教师
microsoft/MiniLM-L12-H384-uncased
↓ sentence-level training + mean pooling
paraphrase-MiniLM-L12-v2

英文 MiniLM 基座本身由 BERT-Base 规模的 UniLM v2 教师做深层 self-attention 蒸馏,得到 12 层、hidden size 384 的小模型;随后用多来源的语义相关句对继续训练,才成为这里冻结的英文 sentence-embedding 教师。

2 · 准备可训练的多语言学生
XLM-R Base
↓ deep self-attention distillation
microsoft/Multilingual-MiniLM-L12-H384

这个学生起点同样是 12 层、384 hidden size,但由 XLM-R Base 压缩而来,并沿用 XLM-R 的多语言 tokenizer。它已经能处理多语言 token,却还没有继承英文教师那套适合相似度与检索的句向量几何。

3 · 用平行句做多语言知识蒸馏
frozen English teacher
+ trainable multilingual student
+ 50+ languages parallel sentences
↓ MSE
current checkpoint

对每个“英文原句 ↔ 译文”样本,教师只编码英文原句并生成目标向量;学生分别编码英文与译文。训练同时拉近两条学生向量与同一个教师向量,结束后只保留学生参数。

真实监督信号:不是人工填写一个类别或 0.5
z_T    = mean_pool(Teacher(s_en))       # [B, 384], stop-gradient
z_S,en = mean_pool(Student(s_en))       # [B, 384]
z_S,x  = mean_pool(Student(t_x))        # [B, 384]

L = MSE(z_S,en, z_T) + MSE(z_S,x, z_T)

这里的 z_T 是教师根据真实英文句子算出的 384 个浮点数。对 PyTorch 默认的 mean reduction,单个学生分支中第 j 个坐标的梯度为 2(z_S[j] − z_T[j]) / (B × 384)。梯度再穿过 mean pooling、12 层 Transformer,回到学生的 attention、MLP 与 embedding table;教师参数始终不更新。

教师和学生使用不同 tokenizer 也没有矛盾:训练不要求两边 token 一一对应,只要求整句 pooling 后的 384 维输出一致。这样“同义句应接近”的英文几何会迁移给中文、德文等语言。

教师为什么已有语义几何?

官方 Sentence-Transformers 资料列出的教师训练来源包括 AllNLI、sentence-compression、SimpleWiki、AltLex、MS MARCO triples、Quora duplicates、COCO / Flickr30k captions、Yahoo title-question、S2ORC citation pairs、Stack Exchange duplicate questions 与 Wikipedia atomic edits。它们提供复述、蕴含、问答、检索、图文描述和编辑前后文本等多种“语义相关句对”。

因此,多语言阶段不是从零发明 384 个坐标的含义,而是让学生模仿一个已经由这些句级任务塑造好的教师空间。译文与原句共享同一 z_T,跨语言对齐由此出现。

可复现边界:公开资料明确给出了基座、教师、学生、平行语料形式、mean pooling 与 MSE 目标,但没有为这个旧 checkpoint 保存一份包含精确数据快照、全量 batch 顺序、随机种子和每项超参数的完整训练清单。因此这里不虚构那些缺失值;下面代码复现真实模型与真实目标的一次更新,而不是声称逐 bit 重建原始发布权重。
真实 PyTorch 代码:从公开 student 初始化执行一次多语言蒸馏

只使用项目已有的 torchtransformers。示例句对是官方 parallel-sentences-talks 文档公开的真实 en-de 样本;首次运行会下载教师与学生模型。单步可在 CPU 运行,完整 50+ 语言训练则是离线训练任务,不应放进网页请求。

import torch
import torch.nn.functional as F
from transformers import AutoModel, AutoTokenizer

# The published models used by the original training recipe.
TEACHER_ID = "sentence-transformers/paraphrase-MiniLM-L12-v2"
STUDENT_INIT_ID = "microsoft/Multilingual-MiniLM-L12-H384"
MAX_LENGTH = 128

# This is the real en-de sample row shown in the official
# sentence-transformers/parallel-sentences-talks documentation.
source_texts = [
    "So I think practicality is one case where it's worth "
    "teaching people by hand."
]
translated_texts = [
    "Ich denke, dass es sich aus diesem Grund lohnt, den Leuten "
    "das Rechnen von Hand beizubringen."
]

device = torch.device("cpu")
teacher_tokenizer = AutoTokenizer.from_pretrained(TEACHER_ID)
student_tokenizer = AutoTokenizer.from_pretrained(STUDENT_INIT_ID)
teacher = AutoModel.from_pretrained(TEACHER_ID).to(device).eval()
student = AutoModel.from_pretrained(STUDENT_INIT_ID).to(device).train()

# The teacher is a fixed target generator: only the student is updated.
for parameter in teacher.parameters():
    parameter.requires_grad_(False)

# A runnable continuation-step setting; the original full optimizer schedule
# was not preserved in this old checkpoint's public reproducibility metadata.
optimizer = torch.optim.AdamW(student.parameters(), lr=2e-5)

def masked_mean(last_hidden_state, attention_mask):
    mask = attention_mask.unsqueeze(-1).to(last_hidden_state.dtype)
    summed = (last_hidden_state * mask).sum(dim=1)
    counts = mask.sum(dim=1).clamp_min(1e-9)
    return summed / counts

def encode(model, tokenizer, texts):
    batch = tokenizer(
        texts,
        padding=True,
        truncation=True,
        max_length=MAX_LENGTH,
        return_tensors="pt",
    ).to(device)
    hidden = model(**batch).last_hidden_state
    return masked_mean(hidden, batch["attention_mask"])

# z_T is a real 384D teacher output, not a hand-written scalar label.
with torch.no_grad():
    z_teacher = encode(teacher, teacher_tokenizer, source_texts)

# The student has its own multilingual tokenizer. Token boundaries and IDs
# may differ from the teacher; only the final sentence vectors must agree.
z_student_source = encode(student, student_tokenizer, source_texts)
z_student_translation = encode(student, student_tokenizer, translated_texts)

# PyTorch MSE averages over the batch and all 384 coordinates.
loss_source = F.mse_loss(z_student_source, z_teacher)
loss_translation = F.mse_loss(z_student_translation, z_teacher)
loss = loss_source + loss_translation

optimizer.zero_grad()
loss.backward()       # gradients pass through pooling, Transformer and lookup
first_gradient = next(
    parameter.grad
    for parameter in student.parameters()
    if parameter.grad is not None
)
optimizer.step()      # one real continuation-distillation update

print("teacher shape:", tuple(z_teacher.shape))       # (1, 384)
print("source MSE:", loss_source.item())
print("translation MSE:", loss_translation.item())
print("total loss:", loss.item())
print("first gradient norm:", first_gradient.norm().item())
真实 CPU 推理 · sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
首次运行会下载模型到服务器缓存;之后复用同一常驻 Python worker。加载失败时不会回退到模拟向量。
Embedding dimension
d = 384(模型固定)
尚未生成任何结果。点击“运行真实实验”后,图表、矩阵和计算步骤才会出现。
完整可运行代码:串联 tokenizer、forward、pooling、normalization
import torch
from transformers import AutoModel, AutoTokenizer

MODEL_ID = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
TEXT = "A kitten is sleeping on the sofa."

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
model = AutoModel.from_pretrained(MODEL_ID).eval().to("cpu")
batch = tokenizer(
    [TEXT],
    padding=True,
    truncation=True,
    max_length=128,
    return_tensors="pt",
)

POOLING = "mean"  # mean | cls | max

with torch.inference_mode():
    hidden = model(**batch).last_hidden_state

mask = batch["attention_mask"].unsqueeze(-1).to(hidden.dtype)
if POOLING == "cls":
    pooled = hidden[:, 0]
elif POOLING == "max":
    masked = hidden.masked_fill(mask == 0, torch.finfo(hidden.dtype).min)
    pooled = masked.max(dim=1).values
else:
    pooled = (hidden * mask).sum(dim=1) / mask.sum(dim=1).clamp_min(1.0)

NORMALIZE = True
embedding = (
    torch.nn.functional.normalize(pooled, p=2, dim=1)
    if NORMALIZE
    else pooled
)

print(tokenizer.convert_ids_to_tokens(batch["input_ids"][0].tolist()))
print("hidden shape:", tuple(hidden.shape))
print("embedding:", embedding[0].tolist())
5A

Pooling 与归一化:从 token matrix 到 sentence vector

Encoder 产生 n 个 contextual token vectors。Pooling 把 [n,d] 压成 [d];L2 normalization 再把它投到单位球面。Mean pooling 给每个有效 token 相同权重,[CLS] pooling 依赖专门训练过的汇总位置,Max pooling 保留每一维最强激活。

第 i 个 contextual token embedding
contextual state of token i
有效 token 数,不应包含 padding
number of non-padding tokens
句向量维度;影响存储、速度与表达容量
embedding dimensionality
范数为 1 的归一化向量
unit-normalized embedding

Mean pooling 必须使用 attention mask

批量输入含有 padding 时,只能平均真实 token;否则不同长度句子的向量会被 padding 污染。

sum(mask_i * h_i) / sum(mask_i)

维度不是可直接解释的标签

384 维并不表示 384 个可以逐一命名的主题。语义通常分布在许多方向组合中;维度越高,存储和索引成本也越高。

1M vectors × 768 dims × float32 ≈ 3.07 GB 尚未计入索引与 metadata

模型决定 Pooling 规则

不要看到 Transformer 就任意取 [CLS]。应遵循 embedding 模型的训练方式、query/document prefix 与官方 pooling 配置。

5B

Cosine similarity:比较方向,而不是绝对长度

Dot product 同时受方向与向量长度影响;cosine 除以两个范数后只比较夹角。如果 q 与 d 都预先 L2-normalize,那么 dot product 与 cosine 数值相同,可以用高效内积索引完成检索。

query embedding
query embedding
document 或 chunk embedding
document or chunk embedding
内积 / dot product
inner product
query 的 L2 范数
L2 norm of the query

方向相同但长度不同

cosine 仍接近 1;raw dot product 会偏向长度更大的向量。

零向量没有 cosine

工程实现需要防止除以 0,并检查空文本或异常 encoder 输出。

距离度量必须与索引一致

模型文档、离线向量生成、向量数据库 metric 与在线 query normalization 必须使用同一约定。

6
Interactive 02 · Exact vector arithmetic

实验:Dot product 与 Cosine 到底差在哪里

拖动夹角和两个向量的范数,比较 raw dot product、cosine 与 normalized dot。重点观察:向量长度改变时,cosine 不变,而 dot product 会改变排序偏好。

这个实验不需要语言模型:两根向量由滑杆给出的长度和角度精确构造,图、公式与数值使用同一个数组计算。
raw dot
2.3876
cosine
0.8290
active dot
2.3876
Real calculation trace · 当前输出代入

从坐标逐项算出 Dot Product 与 Cosine

所有显示值都直接来自当前 a、b 数组;没有语义标签或预设分数。

1
极坐标 → 笛卡尔坐标
θ = 34° = 0.593412 rad
a = [1.2000, 0]
b = [2.4000cosθ, 2.4000sinθ]
  = [1.9897, 1.3421]
a=[1.2000, 0.0000], b=[1.9897, 1.3421]

b 的两个坐标由当前长度与角度直接计算。

2
Dot product
a·b = a₁b₁ + a₂b₂
    = 1.2000×1.9897 + 0.0000×1.3421
    = 2.3876
raw dot = 2.3876

内积同时包含长度和方向。

3
L2 norms
‖a‖₂ = √(1.2000²+0.0000²) = 1.2000
‖b‖₂ = √(1.9897²+1.3421²) = 2.4000
denominator = 2.8800

范数来自同一坐标数组。

4
Cosine / normalized dot
cos(a,b) = 2.3876 / (1.2000×2.4000) = 0.8290
â·b̂ = 0.8290
active dot = 2.3876

两向量归一化后,dot 与 cosine 数值相同。

复制并运行:NumPy 复现当前滑杆数值
import numpy as np

theta = np.deg2rad(34)
a = np.array([1.2, 0.0])
b = np.array([2.4 * np.cos(theta), 2.4 * np.sin(theta)])

dot_product = a @ b
norm_a, norm_b = np.linalg.norm(a), np.linalg.norm(b)
cosine = dot_product / (norm_a * norm_b)
normalized_dot = (a / norm_a) @ (b / norm_b)
print({"a": a, "b": b, "dot": dot_product,
       "cosine": cosine, "normalized_dot": normalized_dot})
7
Interactive 03 · Real 384D embeddings → PCA

实验:句子如何在 2D / 3D 语义空间中移动

先用真实 MiniLM 生成 384 维句向量,再通过实际 PCA 投影到 2D / 3D;在向量插值过程中观察真实高维 cosine 最近邻怎样改变,并明确区分高维检索与低维投影。

真实 CPU 推理 · sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
首次运行会下载模型到服务器缓存;之后复用同一常驻 Python worker。加载失败时不会回退到模拟向量。
尚未生成任何结果。点击“运行真实实验”后,图表、矩阵和计算步骤才会出现。
source: A cat is resting on the sofa.
target: A kitten is sleeping on the couch.
当前最近邻 · 真实 384D cosine
复制并运行:真实编码、PCA 与最近邻
import numpy as np
import torch
from transformers import AutoModel, AutoTokenizer

MODEL_ID = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
sentences = [
  "A cat is resting on the sofa.",
  "A kitten is sleeping on the couch.",
  "A dog is running through the park.",
  "Python reads a CSV file with pandas.",
  "React renders an interactive user interface.",
  "The train arrives at a station in Paris.",
  "Passengers transfer between railway platforms.",
  "Tomato soup tastes better with fresh basil."
]
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
model = AutoModel.from_pretrained(MODEL_ID).eval().to("cpu")
batch = tokenizer(sentences, padding=True, truncation=True,
                  max_length=128, return_tensors="pt")
with torch.inference_mode():
    hidden = model(**batch).last_hidden_state
mask = batch["attention_mask"].unsqueeze(-1).to(hidden.dtype)
E = (hidden * mask).sum(1) / mask.sum(1).clamp_min(1)
E = torch.nn.functional.normalize(E, p=2, dim=1).numpy()

t = 0
target_index = 1
current = (1 - t) * E[0] + t * E[target_index]
current /= np.linalg.norm(current)

# Real PCA: center then SVD; plotting uses only the projection.
centered = E - E.mean(axis=0, keepdims=True)
_, singular_values, vt = np.linalg.svd(centered, full_matrices=False)
points_3d = centered @ vt[:3].T
current_3d = (current - E.mean(axis=0)) @ vt[:3].T
scores = E @ current
print("PCA point:", current_3d)
print("nearest indices:", np.argsort(-scores)[:4])
8
Interactive 04 · Real dense retrieval

实验:Semantic similarity matrix 与 Query–Document Nearest Neighbors

用同一真实 encoder 编码 query 与公开可见的文档语料,改变 top-k,并切换 normalized cosine 与 raw dot product;所有排行榜和 pairwise similarity matrix 都由当前 384 维模型输出即时计算。

真实 CPU 推理 · sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
首次运行会下载模型到服务器缓存;之后复用同一常驻 Python worker。加载失败时不会回退到模拟向量。
D1–D6 在当前代码实验中分别是什么?(展开查看完整语料与向量行号)

D1–D6 是这个小型检索实验的 6 条候选文档,不是模型内置标签,也不是 embedding 的第 1–6 个维度。 页面只把每条记录的 text 送入 tokenizer;id / title / category 只用于把模型输出重新对应到可读文档。

IDE 中的行D_raw 中的行主题标题真正送入模型的文本
D1E[1]D_raw[0]codePython CSVLoad a CSV file with Python by calling pandas.read_csv.
D2E[2]D_raw[1]codePandas tableA pandas DataFrame stores tabular rows and columns.
D3E[3]D_raw[2]travelParis trainGare du Nord is a major railway station in Paris.
D4E[4]D_raw[3]travelRail transferPassengers can transfer between trains at the central station.
D5E[5]D_raw[4]petsCat sofaA cat is sleeping quietly on the sofa.
D6E[6]D_raw[5]petsKitten couchThe kitten rests on a comfortable couch.
行号约定为什么重要?

批次顺序是 [query, D1, D2, D3, D4, D5, D6],所以输出矩阵 E 的第 0 行属于 Query,第 1–6 行依次属于 D1–D6。切出 D_raw = E[1:] 后,Python 的 0-based 行号才变成 D_raw[0] ↔ D1

当前语料在可运行代码中的定义
documents = [
    {
        "id": "D1",
        "title": "Python CSV",
        "category": "code",
        "text": "Load a CSV file with Python by calling pandas.read_csv."
    },
    {
        "id": "D2",
        "title": "Pandas table",
        "category": "code",
        "text": "A pandas DataFrame stores tabular rows and columns."
    },
    {
        "id": "D3",
        "title": "Paris train",
        "category": "travel",
        "text": "Gare du Nord is a major railway station in Paris."
    },
    {
        "id": "D4",
        "title": "Rail transfer",
        "category": "travel",
        "text": "Passengers can transfer between trains at the central station."
    },
    {
        "id": "D5",
        "title": "Cat sofa",
        "category": "pets",
        "text": "A cat is sleeping quietly on the sofa."
    },
    {
        "id": "D6",
        "title": "Kitten couch",
        "category": "pets",
        "text": "The kitten rests on a comfortable couch."
    }
]

# 只有 text 会送入 tokenizer;id、title、category 是展示与追踪用 metadata。
texts = [query, *[document["text"] for document in documents]]

# 模型完成 forward + mean pooling 后,行号关系保持不变:
# E[0] = query
# E[1] = D1, E[2] = D2, ..., E[6] = D6
q_raw = E[0]
D_raw = E[1:]             # D_raw[0] 是 D1,D_raw[5] 是 D6
尚未生成任何结果。点击“运行真实实验”后,图表、矩阵和计算步骤才会出现。
复制并运行:真实 query-document 检索与相似度矩阵
import torch
import torch.nn.functional as F
from transformers import AutoModel, AutoTokenizer

MODEL_ID = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
query = "How can Python open a CSV file?"
documents = [
    {
        "id": "D1",
        "title": "Python CSV",
        "category": "code",
        "text": "Load a CSV file with Python by calling pandas.read_csv."
    },
    {
        "id": "D2",
        "title": "Pandas table",
        "category": "code",
        "text": "A pandas DataFrame stores tabular rows and columns."
    },
    {
        "id": "D3",
        "title": "Paris train",
        "category": "travel",
        "text": "Gare du Nord is a major railway station in Paris."
    },
    {
        "id": "D4",
        "title": "Rail transfer",
        "category": "travel",
        "text": "Passengers can transfer between trains at the central station."
    },
    {
        "id": "D5",
        "title": "Cat sofa",
        "category": "pets",
        "text": "A cat is sleeping quietly on the sofa."
    },
    {
        "id": "D6",
        "title": "Kitten couch",
        "category": "pets",
        "text": "The kitten rests on a comfortable couch."
    }
]

# 顺序决定输出行号:E[0]=Query,E[1]=D1,...,E[6]=D6。
texts = [query, *[document["text"] for document in documents]]
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
model = AutoModel.from_pretrained(MODEL_ID).eval().to("cpu")
batch = tokenizer(texts, padding=True, truncation=True,
                  max_length=128, return_tensors="pt")

with torch.inference_mode():
    hidden = model(**batch).last_hidden_state       # [7, L, 384]
    mask = batch["attention_mask"].unsqueeze(-1).to(hidden.dtype)
    E = (hidden * mask).sum(dim=1) / mask.sum(dim=1).clamp_min(1)

q_raw = E[0]       # [384],当前 Query
D_raw = E[1:]      # [6, 384],D1...D6
print("hidden:", tuple(hidden.shape))
print("E / q / D:", tuple(E.shape), tuple(q_raw.shape), tuple(D_raw.shape))

NORMALIZE_BEFORE_SEARCH = True

if NORMALIZE_BEFORE_SEARCH:
    # 对每一个向量单独做 L2 normalization。
    q_search = F.normalize(q_raw, p=2, dim=0)
    D_search = F.normalize(D_raw, p=2, dim=1)
    metric_name = "cosine"
else:
    # 不改变长度;点积会同时受到方向与向量模长影响。
    q_search = q_raw
    D_search = D_raw
    metric_name = "raw dot product"

print("raw query norm:", torch.linalg.vector_norm(q_raw).item())
print("search query norm:", torch.linalg.vector_norm(q_search).item())

# [6, 384] @ [384] -> [6]:每个文档得到一个真实分数。
scores = D_search @ q_search

for row_index, score in enumerate(scores):
    document = documents[row_index]       # row 0 -> D1, ..., row 5 -> D6
    print(document["id"], document["text"], float(score))

TOP_K = 3
order = torch.argsort(scores, descending=True)  # 对 6 个真实分数降序排列
top_indices = order[:TOP_K]                     # exact top-k,不是 ANN

for rank, row_index in enumerate(top_indices.tolist(), start=1):
    document = documents[row_index]
    print(rank, document["id"], document["title"],
          f"{metric_name}={scores[row_index].item():.6f}")

# Similarity matrix 始终使用 cosine,与上面的检索开关相互独立。
# dim=1 表示每一行(每一篇文档)独立归一化,而不是按列归一化。
D_unit = F.normalize(D_raw, p=2, dim=1)
row_norms = torch.linalg.vector_norm(D_unit, ord=2, dim=1)
print(row_norms)  # tensor([1., 1., 1., 1., 1., 1.])
# [6, 384] @ [384, 6] -> [6, 6]
M = D_unit @ D_unit.T

# Python 是 0-based:M[0, 1] 就是页面标出的 M₁₂,即 D1 与 D2。
i, j = 0, 1
cell = M[i, j]
manual = (D_raw[i] * D_raw[j]).sum() / (
    torch.linalg.vector_norm(D_raw[i]) * torch.linalg.vector_norm(D_raw[j])
)
print("M12:", cell.item(), "manual cosine:", manual.item())

# 验证全部 36 个格子,而不只验证示例 M12。
for i in range(len(documents)):
    for j in range(len(documents)):
        direct = F.cosine_similarity(D_raw[i], D_raw[j], dim=0)
        torch.testing.assert_close(M[i, j], direct)

torch.testing.assert_close(M, M.T)              # Mij = Mji
torch.testing.assert_close(torch.diag(M), torch.ones(6))
print("all 36 cells verified")
print("similarity matrix:
", M)
9

从相似度到 RAG 召回:Top-k 不是最终答案

Query encoder f_q 与 document encoder f_d 把两侧文本映射到兼容空间,再按 similarity 取前 k 个候选。Dual encoder 让文档向量可以离线预计算;Top-k 只负责召回,后面仍可能需要 metadata filter、dedup、reranker 与 context budget 控制。

已切分并建立索引的文档 chunk 集合
indexed document chunks
query encoder;有些模型要求 query prefix
query-side encoder
document encoder;权重可共享但输入格式可能不同
document-side encoder
召回候选数量,与上下文预算共同调节
number of retrieved candidates

Exact search vs ANN

小数据可以逐一计算相似度;百万级向量通常使用 HNSW、IVF 等近似最近邻索引,以少量召回损失换取速度与内存效率。

Bi-encoder recall + cross-encoder rerank

第一阶段独立编码、快速召回;第二阶段把 query 与候选拼在一起联合编码,成本更高但相关性判断更细。

Top-k 不是越大越好

过小可能漏证据,过大可能引入噪声、挤占上下文并诱发错误回答。应使用 Recall@k、MRR、nDCG 与端到端回答指标共同评估。

10
Interactive 05 · Real model hypothesis test

真实检验:king − man + woman ≈ queen 吗?走向 Contextual Sentence Embedding

让真实 sentence encoder 检验 king − man + woman 是否真的把 queen 排在最前,并直接比较 bank 在金融与河岸句子中的 lookup vector、最终 contextual hidden state 和 sentence embedding;不预设答案,不伪造 Attention 贡献。

真实 CPU 推理 · sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
首次运行会下载模型到服务器缓存;之后复用同一常驻 Python worker。加载失败时不会回退到模拟向量。
尚未生成任何结果。点击“运行真实实验”后,图表、矩阵和计算步骤才会出现。
复制并运行:真实类比排名与 bank contextual hidden states
import numpy as np
import torch
from transformers import AutoModel, AutoTokenizer

MODEL_ID = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
words = ["king","queen","man","woman","prince","princess","duke","duchess","emperor","empress","boy","girl","monarch","royal"]
contexts = ["The bank approved the loan.","We sat on the river bank."]
texts = words + contexts
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
model = AutoModel.from_pretrained(MODEL_ID).eval().to("cpu")
batch = tokenizer(texts, padding=True, truncation=True,
                  max_length=128, return_tensors="pt")
with torch.inference_mode():
    hidden = model(**batch).last_hidden_state
    lookup = model.get_input_embeddings()(batch["input_ids"])
mask = batch["attention_mask"].unsqueeze(-1).to(hidden.dtype)
pooled = (hidden * mask).sum(1) / mask.sum(1).clamp_min(1)
E = torch.nn.functional.normalize(pooled, p=2, dim=1).numpy()

index = {word: i for i, word in enumerate(words)}
analogy = E[index["king"]] - E[index["man"]] + E[index["woman"]]
analogy /= np.linalg.norm(analogy)
scores = E[:len(words)] @ analogy
for blocked in ["king", "man", "woman"]:
    scores[index[blocked]] = -np.inf
print("actual analogy ranking:", [(words[i], float(scores[i]))
      for i in np.argsort(-scores)[:6]])

# Compare the real contextual bank hidden states.
for row, sentence in enumerate(contexts, start=len(words)):
    tokens = tokenizer.convert_ids_to_tokens(batch["input_ids"][row].tolist())
    bank_index = next(i for i, token in enumerate(tokens)
                      if token.lstrip("▁Ġ").lower() == "bank")
    print(sentence, "token_id=", int(batch["input_ids"][row, bank_index]),
          "lookup[:6]=", lookup[row, bank_index, :6].tolist(),
          "hidden[:6]=", hidden[row, bank_index, :6].tolist())
11

Modern sentence embedding:训练目标比模型名字更重要

方案怎样得到相关性速度最适合的位置
Static Word2Vec / GloVe词级静态向量,句子常靠简单平均教学、词语相似与轻量基线
通用 LLM hidden states + 任意 pooling有上下文,但未必为全句相似度优化实验基线,不应默认等同检索模型
Sentence dual encoder通过对比学习拉近正样本、推远负样本检索快大规模 embedding 与首阶段召回
Cross encoder / rerankerquery 与 document 联合 Attention 后打分对少量候选做精排

选择模型时检查什么

  • 语言与领域是否匹配,是否支持跨语言 query-document 对齐。
  • 最大 token 长度、截断策略、embedding dimension 与许可证。
  • 是否要求 query: / passage: 前缀,是否默认 normalize。
  • 训练目标是 symmetric similarity、asymmetric retrieval,还是 instruction-aware embedding。
12

交给 No.15 的接口:Embedding 已经准备好了什么

离线:documents → clean → chunk → embed → normalize → vector records 在线:query → same embedding contract → vector search → top-k → filter / rerank → context → LLM 必须保存:model_id、model_version、dimension、pooling、normalization、distance metric
本模块已经解决No.15 继续解决
文本怎样映射到固定维度向量Brute-force 全扫描与 Top-K heap 怎样工作
dot / cosine / normalization 的关系metric 契约怎样进入 HNSW / IVF 索引
similarity matrix 与 query top-k怎样用 Exact ground truth 评估 Recall@K
低维图是投影,不是完整语义空间怎样权衡 latency、内存、更新与重建成本
AI
问问 LLM:把 Token、Sentence Embedding 与 RAG 召回串起来

正在检查登录状态与模型配置…