NLP 与 Transformer
这一课你将能做到
- 用 jieba 分词 + TF-IDF + 余弦相似度,做出一个「相似题推荐」原型
- 理解词向量的来源——「分布假设」如何把词变成可计算的向量
- 用 NumPy 手算一遍 self-attention,彻底拆掉 Transformer 的神秘感
- 说清多头注意力、位置编码、BERT 与 GPT 的区别
为什么要学
本站每天都有用户搜题、刷题,系统要回答两个问题:「这道题和哪道题相似」「这道题讲的是什么知识点」。前几课的模型只会处理表格数字,而文本是高维、离散、带语序的信号。
NLP 方法近十年经历三段演进:词袋(数频率)→ 词向量(数语义)→ Transformer(数关系)。本课沿这条线各走一步,最后手算一次 attention——公式只有三行,但写过一遍和背十遍是两回事。
先建立图像
self-attention 的一句话:每个词提出「查询 Q」,和全句每个词的「键 K」对暗号,
按匹配度(softmax 权重)加权融合它们的「值 V」——
输出_i = Σ_j softmax(QKᵀ/√d)ᵢⱼ · Vⱼ
「括号匹配」这句话里,「括号」会把注意力更多地分给「匹配」。
跟着做
步骤 1:分词 + TF-IDF,找最相似题
# pip install jieba scikit-learn
import jieba
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
STOP = {"如何", "用", "实现", "求", "与"}
def tokenize(t):
return [w for w in jieba.cut(t) if w.strip() and w not in STOP]
titles = ["用栈实现括号匹配校验", "用队列做任务调度与消息缓冲",
"二叉树的层序遍历", "滑动窗口找无重复字符的最长子串"]
M = TfidfVectorizer(tokenizer=tokenize).fit_transform(titles)
sim = cosine_similarity(M)
for i in range(len(titles)):
j = sorted(range(len(titles)), key=lambda k: -sim[i, k])[1]
print(f"{titles[i]} → 最相似:{titles[j]}({sim[i, j]:.2f})")
「栈」和「队列」的语境高度重叠,得分自然领先——这是纯词频方法的诚实结果。
步骤 2:词向量从哪来?转置一下就懂
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import TruncatedSVD
cv = CountVectorizer(tokenizer=tokenize, binary=True)
words_docs = cv.fit_transform(titles).toarray().T # 词 × 文档矩阵
svd = TruncatedSVD(n_components=2, random_state=42)
word_vec = svd.fit_transform(words_docs) # 每词一个 2 维向量
for w, v in zip(cv.get_feature_names_out(), word_vec):
print(f"{w:>4} {v.round(2)}")
「栈」「队列」常出现在相似语境,向量就挨得近——这就是分布假设。Word2Vec 把它训练得更充分,BERT 则用深层网络生成随上下文变化的动态向量。
步骤 3:NumPy 手算 attention
import numpy as np
rng = np.random.default_rng(0)
d, T = 4, 3 # 词向量维度 4,句子 3 个词
X = rng.normal(0, 1, (T, d))
Wq, Wk, Wv = (rng.normal(0, 0.5, (d, d)) for _ in range(3))
Q, K, V = X @ Wq, X @ Wk, X @ Wv
scores = Q @ K.T / np.sqrt(d) # 除 √d 防点积过大
A = np.exp(scores - scores.max(axis=1, keepdims=True))
A /= A.sum(axis=1, keepdims=True) # softmax:每行和为 1
out = A @ V
print(A.round(3), "\n每行和:", A.sum(axis=1).round(3))
A[i, j] 是第 i 个词分给第 j 个词的注意力权重,每行和恒为 1;out 的第 i 行就是加权融合全句后的新表示——每个词都「看过」了上下文。
步骤 4:认识完整架构
Transformer 编码器块(堆 N 层):
输入 + 位置编码 → 多头自注意力 → 残差 & 层归一化 → 前馈网络 → 残差 & 层归一化
- 位置编码:attention 天生无序,必须显式注入词的位置;
- 多头:多组 Q/K/V 并行,各自捕捉不同模式(语法、指代、主题);
- BERT vs GPT:BERT 是双向编码器,擅长理解类任务(分类、相似度);GPT 是单向解码器,擅长生成。两者共享「预训练 + 微调」范式。
讲清楚要点
- 词袋 → 词向量的跃迁:TF-IDF 得到的是稀疏的「词频指纹」,词向量是稠密的「语义坐标」,后者能算词与词的相似度。
- attention 的三重价值:长距离依赖不再被距离稀释;矩阵运算可并行(对比 RNN 逐词串行);权重矩阵天然可视化,具备一定可解释性。
- 中文与 subword:英文有天然空格,中文要分词;BERT/GPT 类模型按字或 BPE 子词切分,从机制上回避了分词错误传播。
- 预训练 + 微调:大模型在海量语料上学通用语言能力,你只需几百条标注样本微调头部,这是现代 NLP 的标准打开方式。
你可能会懵的地方
Q1:为什么点积要除 √d? 维度越高点积方差越大,分数过大会让 softmax 饱和成 one-hot,梯度接近零。除 √d 把方差拉回常数级,训练才稳定。
Q2:BERT 和 GPT 到底差在哪? 训练目标和方向:BERT 双向掩码预测,看得见上下文左右,适合理解;GPT 单向预测下一个词,适合生成。选型按任务定,别拿 BERT 硬写文章。
Q3:需要手写 Transformer 吗?
生产中不需要,用 transformers 库加载预训练模型即可。本课手算的目的只有一个:让公式从「背的」变成「摸过的」。
Q4:中文任务还要 jieba 分词吗? 传统流程(TF-IDF)需要;BERT 类模型按字/子词切分,不需要。但分词在关键词抽取、搜索场景依然有用。
Q5:数据很少时用微调还是特征抽取? 几百条样本用特征抽取:冻结 BERT,取 [CLS] 向量接一个逻辑回归;数据多且领域差异大(医疗、法律)再整体微调。
错法对照
| 错法 | 后果 | 正确做法 |
|---|---|---|
| 用原始词频算相似度 | 常见词主导结果 | TF-IDF 加权 |
| attention 忘 softmax | 权重不可比、无归一 | 每行 softmax,和为 1 |
| 手算时忘除 √d | softmax 饱和、梯度消失 | scores / np.sqrt(d) |
| 拿 BERT 做文本生成 | 架构与任务错配 | 生成任务用 GPT 类解码器 |
| 小数据从头训练语言模型 | 效果远差于预训练 | 预训练 + 微调/特征抽取 |
卡点急救
ModuleNotFoundError: jieba→pip install jieba。- TfidfVectorizer 报 empty vocabulary → tokenizer 被停用词滤空了,先 print 检查分词结果。
- TruncatedSVD 报 n_components 过大 → 不能超过特征数,用
min(2, len(vocab)-1)兜底。
面试常问
Q1:讲一下 self-attention 的计算过程? 输入经三个投影得到 Q/K/V;分数 = QKᵀ/√d,按行 softmax 得权重,再加权求和 V。每个词的输出都是全句信息的加权融合。
Q2:BERT 和 GPT 的区别? 双向编码 vs 单向解码、掩码语言模型 vs 下一词预测、理解 vs 生成,二者是同一 Transformer 架构在不同训练目标下的分支。
Q3:词向量为什么能表达语义? 分布假设:语义由上下文分布决定。语境相似的词在向量空间中距离近,于是「 king - man ≈ queen - woman」这类代数关系得以成立。
自测三问
- softmax 之后注意力矩阵每一行的和是多少?(恰好 1)
- 为什么必须有位置编码?(attention 对输入顺序不敏感)
- 余弦相似度的取值范围?([-1, 1],TF-IDF 文本通常落在 0~1)
小结一句话
NLP 的三段演进各解决一层问题:词袋数频率、词向量数语义、attention 数关系——Transformer 把「让每个词看全句」机制化。
延伸
- 官方文档:PyTorch Transformer 教程
- 下一课预告:PyTorch、TensorFlow、JAX、XGBoost 各守哪块阵地?再给训练好的模型搭一个线上推理接口。