AI/ML 详解:编码模型

Day 24 · 2026-06-10
面向:有编程经验的非 AI 方向工程师
工程对应 → super-individual D11: RAG 实战工程(embedding / re-rank 的落地选型)

前几天讲的都是 decoder(GPT 这类生成模型)。今天换主线:encoder(编码器)——它不生成文字,只把文字压成向量,是 RAG 检索、语义搜索、分类的底座。四个概念串成进化线:BERT 奠基 → RoBERTa 优化配方 → Sentence-BERT 让向量可比 → ColBERT 平衡精度与速度。

BERT 双向编码器Bidirectional Encoder

奠基架构表示学习
一句话类比

GPT 像流式日志处理——只能从左往右一条条读,预测下一条时看不到未来。BERT 反过来:它像一次性把整张表加载进内存做全表扫描,每个 token 都能同时看到左边和右边的全部上下文。代价是 BERT 不能用来生成(它没"下一个词"这个任务),但理解一句话的能力更强——因为"理解"本来就需要前后文一起看。

它解决什么问题 + 工作机制

2018 年之前,理解类任务(分类、命名实体识别、问答)大多用单向语言模型或浅层词向量。痛点:单向看不全。"苹果发布新机" vs "苹果削了皮"——"苹果"的含义取决于后文,左到右模型在读到"苹果"时还看不到后面。BERT(Devlin et al. 2018)的解法是 encoder-only 架构 + 两个自监督预训练任务:

  • MLM(Masked Language Modeling,掩码语言建模):随机遮住句子里 15% 的词,让模型靠双向上下文猜被遮的词。这是"完形填空"——因为答案在中间,模型被迫同时利用左右两侧信息,去掉了因果掩码(causal mask)。
  • NSP(Next Sentence Prediction,下一句预测):判断两句话是否原文相邻,让模型学句子间关系。

预训练完,BERT 学到了通用的语言表示;下游只需在它上面接一个小分类头微调(fine-tune),就能在分类 / NER / 抽取式问答上刷出 SOTA。这就是"预训练 + 微调"范式的开端。

同样输入「苹果 发布 新 [MASK]」,两种读法

GPT(decoder,因果):
苹果 发布 ?  每步只看左边

BERT(encoder,双向):
苹果 发布 [MASK]
└── 每个 token 同时吸收左右全部上下文 ──┘ → 输出向量
代码示例
from transformers import pipeline

# BERT 最自然的演示就是它的预训练任务:完形填空
fill = pipeline("fill-mask", model="bert-base-uncased")

# [MASK] 处让 BERT 用双向上下文补全
for r in fill("The capital of France is [MASK].")[:3]:
    print(f"{r['token_str']:>8}  {r['score']:.3f}")
# paris 0.42 / lyon 0.05 ... ——它靠前后文推断,不是"续写"

# 取句子的隐藏向量(下游分类/检索的原料)
from transformers import AutoModel, AutoTokenizer
tok = AutoTokenizer.from_pretrained("bert-base-uncased")
mdl = AutoModel.from_pretrained("bert-base-uncased")
out = mdl(**tok("hello world", return_tensors="pt"))
print(out.last_hidden_state.shape)  # [1, 序列长, 768] 每个token一个向量
常见误区 + 实践场景
"BERT 是老古董,有了 GPT 还要它干嘛?"——错。生成(写文章、对话)用 decoder;但理解 / 编码(把文本变成向量做检索、分类、聚类)至今仍是 encoder 的主场。你的 RAG 系统里那个 embedding 模型,血统上就是 BERT 这一脉。两者是分工,不是新旧替代。
📌 BigCat 场景:给你的个人知识库做自动打标签 / 分类(把笔记归到「分布式」「佛学」「育儿」)——这是典型的 encoder 微调任务,几百条标注样本就能训出一个又快又准的小分类器,比每次调 LLM 便宜几个数量级。
Takeaway + 思考题
💡 Decoder 负责"说话",Encoder 负责"读懂"——双向是理解的前提,因果掩码是生成的代价。
🤔 你现在用 LLM 做的事里,有哪些其实只需要"理解 / 分类"而非"生成"?那些任务用 encoder 是不是更快更省?

RoBERTa 优化配方Robustly Optimized BERT

训练配方复现研究
一句话类比

RoBERTa(Liu et al. 2019)和 BERT 架构一模一样——没换 CPU、没改 schema。它只是把训练超参和数据配方重新调了一遍,就大幅超越原版。就像你拿到一台跑得很慢的数据库,没换硬件,只是调大了 buffer pool、关掉了一个没用的后台任务、喂了 10 倍数据,性能就翻番——结论是:原来 BERT 被严重训练不足(undertrained)了。

它解决什么问题 + 工作机制

BERT 出来后,大家默认"想更强就得改架构"。RoBERTa 是一篇严谨的消融研究(ablation),它证明:很多收益不来自新结构,而来自把旧配方做对。核心改动:

  • 去掉 NSP 任务:实验发现 NSP 几乎没帮助、甚至有害。删掉它、改用整段连续文本,效果反而更好——一个被广泛沿用的"想当然"任务被证伪。
  • 动态掩码(dynamic masking):BERT 在预处理时一次性定好每句遮哪些词(静态),整个训练都用同一套。RoBERTa 改成每次喂数据时重新随机遮,让模型见到更多样的"完形填空",防止记死。
  • 更大批量 + 更多数据 + 训更久:数据量从 16GB 提到约 160GB,batch 更大、步数更多。
  • 更大的字节级 BPE 词表:减少"未登录词"问题。

没有一个改动是"新架构",但叠加起来全面刷新了 GLUE / SQuAD 等基准。RoBERTa 的真正贡献是方法论:在宣称"我发明了更好的架构"之前,先确认你把baseline 训练充分了——否则你比较的是"训练不足的旧模型 vs 训练充分的新模型",结论不可信。

代码示例
from transformers import pipeline

# RoBERTa 接口与 BERT 完全一致——架构没变,换权重即可
# 注意:RoBERTa 没有 NSP,掩码符号是 <mask> 不是 [MASK]
fill = pipeline("fill-mask", model="roberta-base")

for r in fill("Better data beats a fancier <mask>.")[:3]:
    print(f"{r['token_str']:>12}  {r['score']:.3f}")

# 真实工程里,RoBERTa 常作为"理解类"任务的微调基座
# 例如情感分类、自然语言推理(NLI)——同样的 fine-tune 流程
clf = pipeline("sentiment-analysis",
               model="cardiffnlp/twitter-roberta-base-sentiment-latest")
print(clf("调配方就把分数刷上去了,太优雅了"))
常见误区 + 实践场景
"模型不行 = 架构不行,该换更花哨的结构"——这是 RoBERTa 专门反驳的直觉陷阱。很多时候问题出在训练不充分 / 数据不够 / baseline 没调对。换架构前先问:现有方案训到饱和了吗?(这条认知对你做任何系统调优都通用——别急着重写,先确认旧方案被用满了没。)
📌 BigCat 场景:跨学科思考时把它当一条认识论原则——评估任何"新方法吊打旧方法"的论文 / 产品宣传,先追问"对照组是不是被故意削弱了"。RoBERTa 提醒我们:很多"突破"只是把对照组训练充分后就消失了。
Takeaway + 思考题
💡 配方常常比架构更值钱——RoBERTa 没动一行架构代码,靠"把基线训到位"就拿下 SOTA。
🤔 你手头哪个"效果不好"的系统,其实从没被认真调优 / 喂饱数据过?你确定瓶颈是设计,而不是没用满?

句向量模型Sentence-BERT / Sentence Transformers

向量检索对比学习
一句话类比

原版 BERT 比较两句话相似度,必须把两句拼在一起喂进去算一次——像每次查询都做一次全表 JOIN。1 万句话两两比较 = 约 5000 万次 BERT 推理,几十小时。Sentence-BERT 的思路是建索引:把每句话独立编码成一个固定向量、提前算好存起来,比较时只算向量余弦相似度——从"JOIN 时现算"变成"预计算 + 查表",几十小时降到几秒。

它解决什么问题 + 工作机制

痛点有两层。一是速度:上面说的两两比较组合爆炸。二是质量:很多人以为"BERT 输出的向量直接拿来算余弦就行"——大错。Sentence-BERT 论文(Reimers & Gurevych 2019)实测,直接平均 BERT 的 token 向量、或取 [CLS] 向量,做句子相似度还不如平均 GloVe 词向量。原因:BERT 的预训练目标(完形填空)从没要求"语义相近的句子向量也相近",所以它的向量空间不是为余弦比较设计的。

解法是 Siamese(孪生)网络 + 对比式微调

  • 两句话过同一个 BERT(共享权重,故称"孪生"),各自池化(通常对 token 向量取平均)得到句向量 u、v;
  • 带标签的句对训练:相似句对拉近、不相似句对推远(分类 / 回归 / triplet 目标)。这是对比学习的思想——不是教模型"这句是什么",而是教它"哪两句该挨着";
  • 训练后,语义相近的句子在向量空间里余弦距离也近,可以直接拿去建向量库。

这就是今天所有 embedding 模型 / RAG 检索的直接祖先,也是 sentence-transformers 这个库的来历。Day 22 讲的 dense retrieval、Day 4 的 RAG embedding,底座都是它。

两种比较句子的方式

Cross-encoder(原版BERT): 句A+句B 一次BERT 相似分
准,但每对都要现算 → N 句话要算 N² 次 → 不可预计算

Bi-encoder(Sentence-BERT):
句A BERT 向量u    句B BERT 向量v
└─ 各自独立编码,提前存好 ─┘ → 查询时只算 cos(u,v),毫秒级
代码示例
from sentence_transformers import SentenceTransformer, util

# 一个轻量句向量模型(384维),专为余弦比较训练过
model = SentenceTransformer("all-MiniLM-L6-v2")

docs = ["分布式系统的一致性权衡",
        "如何给孩子讲量子力学",
        "CAP 定理与最终一致性"]

# 1) 离线:把文档全部编码成向量,存进向量库(此处简化为内存)
doc_emb = model.encode(docs, convert_to_tensor=True)

# 2) 在线:query 编码后,只做一次余弦相似度查表
q_emb = model.encode("一致性和分区容忍怎么取舍", convert_to_tensor=True)
hits = util.cos_sim(q_emb, doc_emb)[0]
for i in hits.argsort(descending=True)[:2]:
    print(f"{hits[i]:.3f}  {docs[i]}")
# 命中"CAP定理"——它和query没有共同词,纯靠语义匹配
常见误区 + 实践场景
"任意 BERT / LLM 的隐藏向量都能拿来做语义检索"——错。没经过对比微调的向量空间,余弦相似度往往不可靠(语义近的句子不一定向量近)。一定要用专门训练过的 embedding 模型(sentence-transformers 系、OpenAI / Cohere embedding 等),别直接抠通用 LLM 的 hidden state。
📌 BigCat 场景:给你几年积累的笔记 / 收藏建一个语义搜索——不再靠关键词(搜"一致性"漏掉只写了"CAP"的笔记),而是按意思召回。离线把所有笔记编码入库,之后每次查询毫秒级返回,这是个人知识库最实用的 encoder 应用。
Takeaway + 思考题
💡 "能编码"不等于"向量可比"——向量空间是被训练目标塑造的,要余弦可比,就得用对比目标专门训。
🤔 把"语义相近 = 向量相近"作为约束训出来后,我们其实把"相似"的定义交给了训练数据。你的检索系统里,"相似"到底是谁定义的?
工程对应 → super-individual D11: RAG 实战工程(embedding 选型)

ColBERT 延迟交互Late Interaction

检索架构多向量精度/速度
一句话类比

上面两种方案是两个极端:Bi-encoder 快但糙(整句压成一个向量,细节丢失,像把一整行数据 hash 成一个值),Cross-encoder 准但慢(每个 query-doc 对都要现算,无法预计算)。ColBERT 走中间:它给文档每个 token 都存一个向量(像建了一个细粒度的列索引),查询时做一次轻量的 token 级匹配。既保留了 token 级细节,又能把文档侧的重活提前算好。

它解决什么问题 + 工作机制

单向量 bi-encoder 有个根本瓶颈:把一整段话压成一个 768 维向量,长文档或多主题文档的细节必然被平均掉——query 里某个关键词精确命中文档某句,这个信号在"全局平均"里被冲淡了。Cross-encoder 没这问题(它让 query 和 doc 的每个 token 充分交互),但代价是无法预计算:文档向量依赖当前 query,来一个 query 就得重跑整个库。

ColBERT(Khattab & Zaharia 2020)的关键词是"延迟交互"(late interaction):把"交互"这一步推迟到最后,且做得很轻。流程:

  • 离线:文档过 BERT,保留每个 token 的向量(不池化成单一向量),整库的 token 向量建好索引;
  • 在线:query 也编码成一串 token 向量;打分用 MaxSim——query 的每个 token,去文档所有 token 里找最相似的那一个,取这个最大相似度;再把所有 query token 的最大值加起来得到总分。

MaxSim 公式:S(q,d) = Σi∈q maxj∈d (Eq,i · Ed,j)。直觉:i 遍历 query 的每个 token,maxj 是"这个 query token 在文档里最匹配的地方有多匹配",外层 Σ 把每个 query token 的最佳匹配累加。它本质是"软性的关键词匹配"——既有 BM25 那种精确 term 命中的优点,又是在语义向量空间里做的(同义词也能匹配)。文档 token 向量能预存,交互只是查表+取max,因此比 cross-encoder 快约两个数量级,精度却接近。

三种检索架构的取舍

Bi-encoder 1向量/文档   最快·可预计算   细节丢失
ColBERT   N向量/文档   较快·文档侧可预计算   保留token细节
Cross-encoder 交互/查询   最准   最慢·无法预计算

MaxSim:每个 query token → 文档里找最佳匹配 → 全部相加
代码示例
# 用 MaxSim 直观演示 ColBERT 的打分逻辑(简化版)
import torch
from sentence_transformers import SentenceTransformer

enc = SentenceTransformer("all-MiniLM-L6-v2")

def token_vecs(text):
    # 取每个token的向量并归一化(真ColBERT用专训模型,此处仅示意)
    feats = enc.tokenize([text])
    out = enc[0].auto_model(**{k: v for k, v in feats.items()})
    return torch.nn.functional.normalize(out.last_hidden_state[0], dim=-1)

def maxsim(q, d):
    Q, D = token_vecs(q), token_vecs(d)
    sim = Q @ D.T            # [query_tok, doc_tok] 所有token两两相似度
    return sim.max(dim=1).values.sum().item()  # 每个q取max再求和

q = "how to keep data consistent"
print(maxsim(q, "eventual consistency in distributed databases"))
print(maxsim(q, "a recipe for chocolate cake"))  # 明显更低
常见误区 + 实践场景
"ColBERT 又快又准,那就全用它替掉单向量检索"——要小心它的存储代价:每个文档存的不是 1 个向量而是几十上百个(每 token 一个),索引体积比单向量大一两个数量级。ColBERTv2(2021)用残差压缩缓解了这点,但相比 bi-encoder 仍重很多。它更适合对召回精度要求高、库规模可控的场景,而非无脑全替换。
📌 BigCat 场景:RAG 召回质量不满意时,可把 ColBERT 当"既是召回又是重排"的一档——尤其文档里专业术语 / 精确名词多(技术文档、论文)时,它的 token 级匹配比单向量更能抓住"那个关键词命中了"的信号。理解机制后,选型时你就知道是在拿"存储 / 速度"换"精度"。
Takeaway + 思考题
💡 检索架构的本质是"交互的早晚 vs 可预计算性"的权衡:交互越早越准、越晚越快,ColBERT 把交互推到最后且做轻,卡在甜点上。
🤔 "早交互=准、晚交互=快"这条轴,在你熟悉的系统(数据库 join 策略、缓存预计算)里是不是也成立?同一个权衡换了层皮反复出现,说明了什么?
工程对应 → super-individual D11: RAG 实战工程(re-rank 选型)

深入资源Further Reading

深入思考Deep Questions

1. 既然 decoder(GPT 系)也能产生文本向量,为什么 RAG / 语义搜索至今主流仍是 encoder(BERT 系)?二者做"理解"的差别在哪?
核心差在注意力是否双向。Encoder 没有因果掩码,每个 token 编码时能同时吸收左右全部上下文,天然适合"把整段话压成一个理解后的表示"。Decoder 受因果掩码约束,token i 只能看到 i 之前,靠最后一个 token 的隐藏态代表全句时,前面的词"看不到后面",表示天然有偏。其次是训练目标:GPT 学"预测下一个词",从没被要求"语义近的句子向量也近";要把 decoder 当 embedding 用,得像 Sentence-BERT 那样额外做对比微调(近年的 LLM-based embedding 如 E5-mistral 正是这么做的,而且效果很好)。所以不是"decoder 不能",而是 encoder 更省、更对口:理解类任务模型小一两个数量级、推理快、双向架构本就为表示而生。2024 年起趋势是两条腿走路——小任务用 BERT 系,追求极致检索质量时用经对比微调的 LLM embedding。
2. RoBERTa 删掉 NSP 反而更好,这件事的方法论意义远超 NLP。它在提醒研究者什么?
提醒至少三点。其一,"看起来合理的设计"不等于"有用的设计":NSP 在 BERT 论文里有理有据(学句间关系),却经不起严格消融。任何凭直觉加进系统的组件,都该问"删掉它会变差吗"。其二,对照组必须被训练充分:RoBERTa 揭示之前很多"超越 BERT"的工作,比的是"训练不足的 BERT",一旦把 baseline 喂饱,优势就缩水甚至消失——这是科学比较的公平性问题,在 AI 论文里普遍存在。其三,负结果同样有价值:RoBERTa 几乎没有"新发明",它的贡献是把变量一个个拆开做干净的对比,告诉社区"收益来自哪、不来自哪"。对 BigCat 你做技术决策的迁移:看到任何"新方案吊打旧方案"的对比,第一反应应是"对照组配置公平吗、被训/调到位了吗"。
3. Bi-encoder → ColBERT → Cross-encoder 是一条"交互越来越早、计算越来越重"的连续谱。把它和你熟悉的数据库 / 缓存权衡对照,你看到什么共同结构?
这是同一个权衡反复换皮:"提前算好(预计算/物化) vs 临场现算(按需计算)"。Bi-encoder ≈ 物化视图 / 倒排索引:离线把昂贵计算做完,查询时只查表,极快,但索引一旦建好就"凝固"了,无法针对当前 query 定制,损失精度。Cross-encoder ≈ 查询时全量 JOIN + 现算:每次都用上当前 query 的全部信息,最准,但无法预计算,贵且慢。ColBERT ≈ 建细粒度索引 + 轻量 join:文档侧重活离线做,交互推迟到查询时但只做廉价的 MaxSim,卡在甜点。共同结构:把计算沿时间轴重新分配——能与 query 解耦的部分前移预计算,必须依赖 query 的部分尽量轻、尽量后置。这条原则在 CDN、prepared statement、KV cache(Day 9)、查询优化器里反复出现。
4. Sentence-BERT 把"相似"的定义外包给了训练数据(哪些句对被标为相近)。这对你设计个人语义搜索意味着什么风险?
意味着"相似"不是客观的,而是被训练集的标注偏好塑造的。通用 embedding 模型多在新闻、问答、NLI 等公开语料上训练,它眼中的"相似"是那些数据里的相似。用到你的个人知识库可能错位:你认为"佛学的'无常'和分布式的'最终一致性'在讲同一种东西",但通用模型的训练数据里没人这样配对,它不会把这两条召回到一起——你最珍视的跨学科连接恰恰是通用相似度的盲区。实际风险:(1) 领域术语被泛化("一致性"在你这是 CAP,模型可能往"性格一致"靠);(2) 你独特的概念关联无法被检索到。应对:对高价值场景考虑用自己的笔记微调 / 用带标注的正样本对把"你定义的相似"教给模型;或在检索层加入你手工维护的概念图谱。深层启示:外包"相似性判断"等于外包了一部分"你如何组织世界",值得警惕哪些连接你不愿交出去。
5. 这四个模型跨度六年(2018→2024),但没有一个比另一个"过时"。它们更像一套并存的工具箱而非新陈代谢。这说明 AI 进步的真实形态是什么?
说明"AI 进步 = 旧的被淘汰"是种误解。真实形态更像生态位分化:每个模型占住一个"成本/精度/场景"的生态位,新模型多数是开辟新生态位或优化某条权衡,而非全面替代。BERT 占住"通用理解基座",RoBERTa 是同生态位的更优个体,Sentence-BERT 开辟"可比向量"生态位,ColBERT 在"精度/速度"轴上插入中间档。即便 2024 的大 LLM 也没让 BERT 失业——"用 340M 模型做分类"和"用 GPT 做分类"是不同成本量级的事,前者在高吞吐、低延迟场景不可替代。对 BigCat 追求"AI 超级个体"的启示:别只追最新最大的模型,而要建一个分层工具箱——便宜的小 encoder 处理高频理解任务,贵的大模型留给真正需要推理生成的场景。会"为任务选对生态位的工具",比"永远用最强模型"更接近超级个体的本质。