NLP 与 Transformer
Tokenization、Embedding、Attention。
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))