cocoindex 语义检索实战:从 BERT 论文精读到向量索引构建
2026/9/15 19:58:25 网站建设 项目流程

cocoindex 语义检索实战:从 BERT 论文精读到向量索引构建

【免费下载链接】cocoindexIncremental engine for long horizon agents 🌟 Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex

本篇技术指南以 cocoindex 仓库中text_embedding示例的语料——BERT 经典论文(1810.04805v2.md)为核心文本,完整梳理 BERT 的模型架构、预训练任务(Masked LM 与 Next Sentence Prediction)、微调范式与消融实验结论,并结合 text_embedding 示例 的 Rust/Python 管道,讲解如何把这篇论文按 Markdown 分块、向量化并存入 Postgres/pgvector 做语义检索。读完你将同时掌握 BERT 的技术原理与一套可复制的"论文语料 → 可搜索向量索引"的增量管道实现。

BERT 论文在 cocoindex 示例中的角色

text_embedding示例(Rust 版位于 examples/rust/text_embedding,Python 版位于 examples/text_embedding)的输入语料是markdown_files/目录下的三份经典文档:

  • 1810.04805v2.md:BERT 论文(本文主体)
  • 1706.03762v7.md:Attention Is All You Need
  • rfc8259.md:JSON 规范

管道的工作方式为:遍历**/*.md文件 → 按 Markdown 语义递归分块 → 用all-MiniLM-L6-v2本地嵌入 → 写入 Postgres/pgvector。因此,理解这篇论文正文,也是在理解这套向量索引里"被检索的知识本体"。

BERT 核心思想:深度双向的预训练表示

BERT(Bidirectional Encoder Representations from Transformers)由 Google AI Language 团队提出,针对此前语言表示模型的根本局限——标准语言模型是单向的。OpenAI GPT 采用从左到右的架构,每个 token 在自注意力层中只能关注其左侧的 token;ELMo 则是独立训练的从左到右与从右到左两个 LM 的浅层拼接。两者都不是"在所有层同时基于左右上下文联合建模"的深度双向表示。

BERT 的关键设计是用Masked Language Model(MLM)预训练目标来解除单向性约束:随机遮蔽一部分输入 token,让模型仅依据上下文预测被遮蔽词的原始词表 id。MLM 灵感源自 Cloze 任务(Taylor, 1953),它使表示能够融合左右上下文,从而预训练出深度双向的 Transformer。此外,BERT 还引入Next Sentence Prediction(NSP)任务联合预训练文本对表示。

论文的三大贡献可概括为:

  1. 证明双向预训练对语言表示的重要性——对比 GPT(单向 LM)与 ELMo(浅层双向拼接);
  2. 证明预训练表示能显著减少对精心设计的任务专属架构的依赖,BERT 是首个在大量句子级与 token 级任务上超越众多任务专属架构的基于微调的代表模型;
  3. 在 11 个 NLP 任务上刷新当时的最优水平。

模型架构与两种规格

BERT 的模型架构是基于 Vaswani 等人(2017)的多层双向 Transformer 编码器。论文用三个超参描述模型:层数 L、隐藏维度 H、自注意力头数 A,前馈网络维度固定为 4H。两种主要规格如下:

规格L(层数)H(隐藏维度)A(注意力头数)总参数前馈维度
BERT_BASE1276812110M3072
BERT_LARGE24102416340M4096

BERT_BASE 特意选择了与 OpenAI GPT 相同的模型尺寸以便公平对比,关键差异在于:BERT 使用双向自注意力,而 GPT 的 Transformer 使用受限自注意力(每个 token 只能关注左侧上下文)。论文同时指出,双向 Transformer 在文献中常被称为"Transformer 编码器",而仅看左侧上下文的版本被称为"Transformer 解码器"。

BERT 的另一个显著特征是跨任务统一架构:预训练架构与下游微调架构几乎无差别,除输出层外,同一套预训练参数被用来初始化不同下游任务的模型。

输入/输出表示

BERT 的输入表示需要无歧义地表达单个句子与句子对(如 ⟨Question, Answer⟩)。其中"句子"可以是任意连续文本片段,而非语言学意义上的句子;"序列"指输入 BERT 的 token 序列,可以是单句或打包的两句。

  • 使用 WordPiece 词表(30,000 token);
  • 每个序列的首个 token 固定为特殊分类 token[CLS],其最终隐藏状态 C ∈ R^H 用作分类任务的聚合序列表示;
  • 句子对打包进单一序列,用特殊 token[SEP]分隔,并为每个 token 附加学习到的 segment embedding 以标识其属于句子 A 还是 B;
  • 单个 token 的输入表示由 token embedding、segment embedding 与 position embedding 三者求和构成。

预训练:两个无监督任务

Task #1:Masked LM(MLM)

标准条件语言模型只能从左到右或从右到左训练,因为双向条件化会让每个词间接"看到自己",模型在多层级上下文中可以平凡地预测目标词。BERT 的做法是随机遮蔽一定比例的输入 token,再预测被遮蔽 token。实验中,每个序列随机遮蔽 15% 的 WordPiece token;与去噪自编码器不同,BERT 只预测被遮蔽的词,而非重建整个输入。

由于[MASK]token 在微调阶段不会出现,纯遮蔽会造成预训练与微调之间的不匹配。为此,训练数据生成器选出 15% 的 token 位置后,按如下混合策略替换:

  • 80% 概率替换为[MASK]token:my dog is hairy → my dog is [MASK]
  • 10% 概率替换为随机词:my dog is hairy → my dog is apple
  • 10% 概率保持不变:my dog is hairy → my dog is hairy(目的是让表示偏向实际观察到的词)

随后用 T_i 以交叉熵损失预测原始 token。这一策略的优势在于:Transformer 编码器不知道哪些词会被要求预测、哪些被随机词替换,因此被迫对所有输入 token 保持分布式的上下文表示。由于随机替换只发生在 1.5% 的 token 上(15% × 10%),不会损害模型的语言理解能力。相比标准语言模型训练,MLM 每批只对 15% 的 token 做预测,收敛需要更多预训练步数。

Task #2:Next Sentence Prediction(NSP)

问答(QA)与自然语言推理(NLI)等任务依赖对两个句子之间关系的理解,这是语言建模无法直接捕获的。BERT 为此预训练一个二分类的 NSP 任务:选择句子 A 与 B 构造预训练样本时,50% 情况下 B 是 A 的真实后续句(标注IsNext),50% 情况下 B 是语料中的随机句子(标注NotNext);[CLS]的表示 C 用于 NSP 预测。论文报告最终模型在 NSP 上达到 97%–98% 的准确率,并指出 C 在未经微调时并非有意义的句子表示(因为它只按 NSP 目标训练)。NSP 对 QA 和 NLI 任务非常有益。

论文给出的两个 NSP 示例:

  • Input = [CLS] the man went to [MASK] store [SEP] he bought a gallon [MASK] milk [SEP] Label = IsNext
  • Input = [CLS] the man [MASK] to the store [SEP] penguin [MASK] are flight ##less birds [SEP] Label = NotNext

预训练数据与训练细节

预训练语料为 BooksCorpus(8 亿词)与英文 Wikipedia(25 亿词,仅抽取文本段落,忽略列表、表格与标题)。论文强调必须使用文档级语料(而非 Billion Word Benchmark 这类打乱的句子级语料)以提取长连续序列。

训练配置(附录 A.2):

  • 每个训练序列采样两段文本(即两个"句子",通常比单句长),组合长度 ≤ 512 token;句子 A 用 A embedding,句子 B 用 B embedding;
  • 批量大小 256 序列(256 × 512 = 128,000 token/批),训练 1,000,000 步,约等于在 33 亿词语料上跑 40 个 epoch;
  • 优化器 Adam,学习率 1e-4,β1 = 0.9,β2 = 0.999,L2 权重衰减 0.01,前 10,000 步学习率 warmup,之后线性衰减;
  • 所有层 dropout 概率 0.1;激活函数用 gelu(跟随 OpenAI GPT),而非标准 relu;
  • 训练损失为平均 MLM 似然与平均 NSP 似然之和;
  • BERT_BASE 在 4 个 Cloud TPU(Pod 配置,共 16 个 TPU 芯片)上训练,BERT_LARGE 在 16 个 Cloud TPU(64 芯片)上训练,每次预训练约 4 天;
  • 由于注意力复杂度随序列长度呈二次增长,90% 的训练步使用 128 序列长度,其余 10% 使用 512 以学习位置嵌入。

微调范式

微调非常直接:Transformer 的自注意力机制使 BERT 能建模单文本或文本对的下游任务,只需替换适当的输入与输出。对于文本对任务,BERT 用自注意力统一了"独立编码 + 双向交叉注意力"两个阶段——对拼接后的文本对做自注意力天然包含了两个句子间的双向交叉注意力。

输入侧,预训练中的句子 A/B 分别对应:释义中的句子对、蕴含中的假设-前提对、问答中的问题-段落对、以及文本分类/序列标注中的退化 text-∅ 对。输出侧,token 级任务(序列标注、问答)使用 token 表示喂入输出层;分类任务(蕴含、情感分析)使用[CLS]表示 C 喂入输出层。

与预训练相比,微调相对廉价:论文所有结果可在单个 Cloud TPU 上最多 1 小时复现(如 SQuAD 模型约 30 分钟达到 Dev F1 91.0),或在 GPU 上数小时。

微调超参数(附录 A.3)

微调时大部分超参与预训练一致,dropout 始终为 0.1,仅在批量大小、学习率与 epoch 数上不同。论文发现以下范围在全部任务上表现良好:

  • 批量大小:16、32
  • 学习率(Adam):5e-5、3e-5、2e-5
  • epoch 数:2、3、4

大数据集(10 万+ 标注样本)对超参数选择远没有小数据集敏感。微调通常很快,合理的做法是对上述参数做穷举搜索,并在开发集上选择最优模型。

实验:11 个 NLP 任务

GLUE

GLUE 是多样的自然语言理解任务集合。微调时,用[CLS]的最终隐藏向量 C 作为聚合表示,唯一新增参数是分类层权重 W ∈ R^(K×H)(K 为标签数),以 log(softmax(C·W^T)) 计算标准分类损失。表 1 为 GLUE 测试集结果(F1 用于 QQP 与 MRPC,Spearman 相关系数用于 STS-B,其余为准确率;平均列排除有问题的 WNLI):

SystemMNLI-(m/mm)QQPQNLISST-2CoLASTS-BMRPCRTEAverage
(训练样本数)392k363k108k67k8.5k5.7k3.5k2.5k-
Pre-OpenAI SOTA80.6/80.166.182.393.235.081.086.061.774.0
BiLSTM+ELMo+Attn76.4/76.164.879.890.436.073.384.956.871.0
OpenAI GPT82.1/81.470.387.491.345.480.082.356.075.1
BERT_BASE84.6/83.471.290.593.552.185.888.966.479.6
BERT_LARGE86.7/85.972.192.794.960.586.589.370.182.1

所有 GLUE 任务使用批量大小 32、微调 3 个 epoch,从 5e-5、4e-3、3e-5、2e-5 中按 Dev 集选择最优学习率。BERT_LARGE 在小数据集上微调偶有不稳,因此做多次随机重启(相同预训练 checkpoint,不同数据打乱与分类层初始化)并选 Dev 最优。BERT_BASE 与 BERT_LARGE 相比此前最优系统分别获得 4.5% 与 7.0% 的平均准确率提升;在最大的 GLUE 任务 MNLI 上获得 4.6% 的绝对提升。BERT_LARGE 在所有任务上显著优于 BERT_BASE,尤其是训练数据极少的任务。

SQuAD v1.1

SQuAD v1.1 含 10 万个人工标注问答对。输入将问题与段落打包为单一序列(问题用 A embedding,段落用 B embedding)。微调仅引入起始向量 S ∈ R^H 与结束向量 E ∈ R^H;词 i 作为答案起点的概率为 softmax(S·T_i),终点同理;候选片段 (i, j) 的得分为 S·T_i + E·T_j,取 j ≥ i 的最大得分片段作为预测。微调 3 个 epoch、学习率 5e-5、批量大小 32。表 2 的结果中,BERT 最优系统(先微调 TriviaQA 再做 SQuAD 数据增强)在集成上超过当时榜首系统 +1.5 F1、单模型 +1.3 F1;单模型 BERT 甚至超过当时的 top 集成系统的 F1:

SystemDev EMDev F1Test EMTest F1
Human--82.391.2
#1 Ensemble - nlnet--86.091.7
#2 Ensemble - QANet--84.590.5
BiDAF+ELMo (Single)-85.6-85.8
R.M. Reader (Ensemble)81.287.982.388.5
BERT_BASE (Single)80.888.5--
BERT_LARGE (Single)84.190.9--
BERT_LARGE (Ensemble)85.891.8--
BERT_LARGE (Sgl.+TriviaQA)84.291.185.191.8
BERT_LARGE (Ens.+TriviaQA)86.292.287.493.2

SQuAD v2.0

SQuAD 2.0 允许段落中不存在答案,更贴近现实。BERT 的扩展方式很简洁:把无答案问题视为答案起止都在[CLS]token 的片段,将概率空间扩展到包含[CLS]位置;预测时比较无答案片段的得分 s_null = S·C + E·C 与最佳非空片段得分 ŝ_ij,当 ŝ_ij > s_null + τ 时预测非空答案(τ 在 Dev 集上按最大化 F1 选取)。未使用 TriviaQA 数据,微调 2 个 epoch、学习率 5e-5、批量大小 48,相比此前最优系统获得 +5.1 F1 的提升:

SystemDev EMDev F1Test EMTest F1
Human86.389.086.989.5
#1 Single - MIR-MRC (F-Net)--74.878.0
#2 Single - nlnet--74.277.1
unet (Ensemble)--71.474.9
SLQA+ (Single)--71.474.4
BERT_LARGE (Single)78.781.980.083.1

SWAG

SWAG 数据集含 11.3 万句子对补全样本,用于评估接地常识推理:给定句子,从四个选项中选出最合理的延续。微调时构造四个输入序列,每个为给定句子(A)与一个可能延续(B)的拼接;唯一新增参数是向量与[CLS]表示 C 的点积得分,经 softmax 归一化。微调 3 个 epoch、学习率 2e-5、批量大小 16。BERT_LARGE 相比作者的 ESIM+ELMo 基线提升 +27.1%,比 OpenAI GPT 高 8.3%:

SystemDevTest
ESIM+GloVe51.952.7
ESIM+ELMo59.159.2
OpenAI GPT-78.0
BERT_BASE81.6-
BERT_LARGE86.686.3

消融研究:什么在起作用

预训练任务的效果

论文用与 BERT_BASE 完全相同的预训练数据、微调方案与超参数评估两种预训练目标:"No NSP"(有 MLM、无 NSP 的双向模型)与"LTR & No NSP"(标准从左到右 LM,微调时同样施加左向约束以避免预训练/微调不匹配,可比作使用更大数据集、BERT 输入表示与微调方案的 OpenAI GPT)。表 5 显示:移除 NSP 在 QNLI、MNLI 与 SQuAD 1.1 上显著损害性能;LTR 模型在所有任务上都比 MLM 模型差,在 MRPC 与 SQuAD 上大幅下降:

TasksMNLI-m (Acc)QNLI (Acc)MRPC (Acc)SST-2 (Acc)SQuAD (F1)
BERT_BASE84.488.486.792.788.5
No NSP83.984.986.592.687.9
LTR & No NSP82.184.377.592.177.8
+ BiLSTM82.184.175.791.684.9

在 LTR 模型上加随机初始化的 BiLSTM 能显著改善 SQuAD,但仍远差于预训练双向模型,且对 GLUE 任务有害。论文也讨论了 ELMo 式的"分别训练 LTR 与 RTL 模型再拼接"方案:一是成本翻倍;二是对 QA 不直观(RTL 模型无法让答案条件化在问题上);三是严格弱于深度双向模型(后者每一层都能同时使用左右上下文)。

模型规模的影响

论文训练了不同层数、隐藏单元与注意力头数的 BERT 模型(其余超参与流程相同)。表 6 显示更大的模型在四个数据集上带来严格一致的准确率提升,即使 MRPC 只有 3,600 个标注样本、且与预训练任务差异显著。对比:Vaswani 等人最大的 Transformer 为 (L=6, H=1024, A=16)、编码器 1 亿参数;文献中最大的 Transformer 为 (L=64, H=512, A=2)、2.35 亿参数;而 BERT_BASE 有 1.1 亿、BERT_LARGE 有 3.4 亿参数:

#L#H#ALM (ppl)MNLI-mMRPCSST-2
3768125.8477.979.888.4
6768125.2480.682.290.7
676834.6881.984.891.3
12768123.9984.486.792.9
121024163.5485.786.993.3
241024163.2386.687.893.7

论文指出,这是首次令人信服地证明:只要模型被充分预训练,将模型规模扩展到极端水平也能在小规模任务上带来大幅提升。此前 Peters 等人的工作对增大预训练 bi-LM 规模呈现混合结果,Melamud 等人仅顺带提及隐藏维度从 200 增至 600 有帮助、增至 1000 无进一步提升——两者都是特征式方法;论文推测,当模型直接在下游任务上微调且仅随机初始化极少量附加参数时,任务模型能受益于更大、更具表达力的预训练表示,即便下游数据非常小。

特征式方法与微调式方法的对比

并非所有任务都能用 Transformer 编码器架构简单表达,且预先计算训练数据的昂贵表示、再在其上跑廉价模型实验有巨大的计算收益。论文在 CoNLL-2003 NER 上比较两种方式:特征式方法使用保留大小写的 WordPiece 模型,提取一个或多个层的激活(不微调任何参数)作为输入,喂给随机初始化的两层 768 维 BiLSTM 与分类层;token 级分类器使用首个子 token 的表示。表 7 显示 BERT_LARGE 与当时最先进方法竞争力相当;最佳特征式方法拼接预训练 Transformer 顶部四层的 token 表示,仅比微调整个模型低 0.3 F1,证明 BERT 对微调与特征式两种方式都有效:

SystemDev F1Test F1
ELMo (Peters et al., 2018a)95.792.2
CVT (Clark et al., 2018)-92.6
CSE (Akbik et al., 2018)-93.1
BERT_LARGE(微调)96.692.8
BERT_BASE(微调)96.492.4
Embeddings(特征式)91.0-
Second-to-Last Hidden(特征式)95.6-
Last Hidden(特征式)94.9-
Weighted Sum Last Four Hidden(特征式)95.9-
Concat Last Four Hidden(特征式)96.1-
Weighted Sum All 12 Layers(特征式)95.5-

训练步数与遮蔽策略的消融

附录 C.1 回答了两个问题:其一,BERT 是否真的需要如此大量的预训练(128,000 词/批 × 1,000,000 步)?答案是需要——BERT_BASE 用 1M 步相比 500k 步在 MNLI 上多获得约 1.0% 的准确率。其二,MLM 预训练是否因每批只预测 15% 的词而比 LTR 收敛更慢?答案是略慢,但从绝对准确率看 MLM 几乎立刻开始超越 LTR。

附录 C.2 对遮蔽策略做了消融(MASK 表示替换为[MASK],SAME 表示保持不变,RND 表示替换为随机 token;左半部分为各策略概率,右半部分为 Dev 结果;特征式方法拼接 BERT 最后 4 层,即 5.3 节的最优做法)。表 8 显示微调对不同的遮蔽策略出人意料地稳健,但只用 MASK 策略在特征式 NER 上有问题,而只用 RND 策略明显更差:

Masking RatesDev Set Results
MASKSAMERNDMNLI Fine-tuneNER Fine-tuneNER Feature-based
80%10%10%84.295.494.9
100%0%0%84.394.994.0
80%0%20%84.195.294.6
80%20%0%84.495.294.7
0%20%80%83.794.894.6
0%0%100%83.694.994.6

与 ELMo、OpenAI GPT 的系统性对比

三类模型的本质差异:BERT 是双向 Transformer;OpenAI GPT 是左到右 Transformer;ELMo 是独立训练的左右 LSTM 的拼接以生成特征。三者中只有 BERT 的表示在所有层同时基于左右上下文联合条件化;且 BERT 与 GPT 是微调式方法,ELMo 是特征式方法。

BERT 在设计中刻意尽量贴近 GPT 以做最小化对比,但仍有几处训练差异:GPT 只在 BooksCorpus(8 亿词)上训练,BERT 额外使用 Wikipedia(25 亿词);GPT 的[SEP][CLS]只在微调时引入,BERT 在预训练期间就学习[SEP][CLS]与句子 A/B 嵌入;GPT 以 32,000 词/批训练 1M 步,BERT 以 128,000 词/批训练 1M 步;GPT 所有微调统一用 5e-5 学习率,BERT 按任务在开发集上选择最优学习率。5.1 节的消融证明,绝大多数改进来自两个预训练任务及其带来的双向性。

从论文精读到可检索的向量索引

理解了 BERT 论文的内容之后,回到 cocoindex 场景:这篇论文(连同 Attention 论文与 RFC 文档)正是text_embedding示例的检索语料。Rust 示例的 main.rs 中,default_sourcedir()指向markdown_files/,索引流程为:

  1. 遍历walk_items(&sourcedir, &["**/*.md"])递归收集 Markdown 文件(Python 版用localfs.walk_dir+PatternFilePathMatcher(included_patterns=["**/*.md"]));
  2. 分块RecursiveSplitter::new()配合RecursiveChunkConfig,其中chunk_size: 2000chunk_overlap: 500language: Some("markdown")。配置字段定义见 rust/sdk/cocoindex/src/ops/text.rs:chunk_size为目标块大小(字节)、min_chunk_size默认取 chunk_size 的一半、chunk_overlap为相邻块重叠、language指定语法感知切分的语言;Python 版等价写法为_splitter.split(text, chunk_size=2000, chunk_overlap=500, language="markdown")
  3. 嵌入SentenceTransformerEmbedder加载sentence-transformers/all-MiniLM-L6-v2(384 维),embed_batch批量生成向量,并复用同一模型做查询向量,保证索引与检索一致;
  4. 入库postgres::mount_table_targetcoco_examplesschema 下建doc_embeddings表,embedding列类型为vector(384),并通过declare_vector_index声明 HNSW 向量索引;process_file标注#[cocoindex::function(memo)],未变化的文件被 memo 跳过,被删除/编辑文件的块由托管目标自动 reconcile 删除孤儿行;
  5. 检索query子命令将查询文本嵌入后执行SELECT filename, text, embedding <=> $1::vector AS distance ... ORDER BY distance ASC LIMIT $2,用 pgvector 余弦距离(<=>)取 TOP_K=5,并按1.0 - distance打印相似度得分。

因此,"What is bidirectional self-attention?" 这类问题即使与原文不共享关键词,也能通过语义向量命中 BERT 论文中关于 MLM 双向性的段落——这正是本文所讲解的 BERT 概念在工程管道中的落地形态。

结论

BERT 论文的核心贡献是把语言模型的预训练从深度单向架构推广到深度双向架构:通过 MLM 解除单向性限制、以 NSP 捕获句子关系,使同一套预训练模型仅靠一个额外输出层就能在句子级与 token 级任务上全面达到当时的顶尖水平。对 cocoindex 用户而言,这篇论文不仅是 NLP 发展史上的里程碑文献,也是text_embedding示例中可直接检索的高质量知识语料——从分块参数到 pgvector 索引,整个语义检索管道在仓库中均有完整可运行的参考实现。

【免费下载链接】cocoindexIncremental engine for long horizon agents 🌟 Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/co/cocoindex

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询