🎯 目标:深入理解Transformer架构(整个大模型的基石),掌握BERT/GPT等预训练模型,熟练使用HuggingFace生态进行NLP任务开发。 📋 前置要求:阶段二(神经网络、CNN、RNN/LSTM、PyTorch、注意力机制基础)
本阶段知识依赖图
阶段二基础(RNN/LSTM + 注意力机制)
│
├──→ Transformer架构(⭐最核心)
│ ├── Self-Attention ──→ Multi-Head Attention
│ ├── Positional Encoding
│ ├── Feed-Forward Network
│ ├── Add & Norm(残差连接 + 层归一化)
│ ├── Encoder Block ──→ Encoder × N
│ └── Decoder Block ──→ Decoder × N
│
├──→ 预训练语言模型
│ ├── BERT(Encoder-only)──→ 文本分类、NER、阅读理解
│ ├── GPT(Decoder-only)──→ 文本生成、对话
│ └── Seq2Seq模型(Encoder-Decoder)──→ 翻译、摘要
│
└──→ HuggingFace生态(实战工具)
├── Tokenizer ──→ 文本编码/解码
├── Datasets ──→ 数据集加载与处理
├── Pipeline ──→ 一行代码完成推理
├── Trainer ──→ 模型训练框架
└── Evaluate ──→ 评估指标
模块一:Transformer架构——大模型的基石 ⭐⭐⭐
这是整个课程中最重要的一个模块。 Transformer是GPT、BERT、LLaMA、ChatGPT等所有现代大模型的基础架构。理解了Transformer,就理解了大模型的"骨架"。
1.1 从宏观视角理解Transformer
Transformer之前的问题——为什么需要它?
RNN/LSTM的两大致命缺陷:
缺陷1:串行计算(效率低)
处理第5个词时,必须等第1-4个词全部处理完
→ 无法利用GPU的并行能力
→ 一个1000词的句子,需要串行计算1000步
缺陷2:长距离依赖(效果差)
虽然LSTM缓解了梯度消失,但信息仍然需要"逐步传递"
第1个词的信息要经过999步才能到达第1000个词
→ 每一步都有信息损失 → 超长序列效果仍然不好
Transformer的革命性解决方案:
1. 完全抛弃循环结构,用Self-Attention替代
→ 所有位置同时计算(并行),训练速度提升10-100倍
2. 任意两个位置直接相连(不需要逐步传递)
→ 第1个词和第1000个词之间只有一步之遥
→ 天然支持长距离依赖
Transformer的整体结构——先看大局
类比:翻译任务就像"理解+写作"两个步骤
Encoder(编码器)= "理解" —— 读懂原文
输入:"I love AI"
输出:每个词的"理解向量"(包含上下文信息)
Decoder(解码器)= "写作" —— 根据理解写出译文
输入:已生成的部分译文 + Encoder的理解
输出:下一个词的概率分布
┌─────────────────────────────┐
│ Decoder │
│ ┌───────────────────────┐ │
│ │ Output Embedding │ │
│ │ + Positional Encoding │ │
│ └───────────┬───────────┘ │
│ ↓ │
│ ┌───────────────────────┐ │
│ │ Masked Multi-Head │ │ ← 只能看已生成的词
│ │ Attention │ │
│ └───────────┬───────────┘ │
│ ↓ │
│ ┌───────────────────────┐ │
│ │ Multi-Head Attention │ │ ← 看Encoder的输出
│ │ (Encoder-Decoder) │ │
│ └───────────┬───────────┘ │
│ ↓ │
│ ┌───────────────────────┐ │
│ │ Feed-Forward Network │ │
│ └───────────────────────┘ │
│ × N层 │
└──────────────┬──────────────┘
↑
┌──────────────┴──────────────┐
│ Encoder │
│ ┌───────────────────────┐ │
│ │ Input Embedding │ │
│ │ + Positional Encoding │ │
│ └───────────┬───────────┘ │
│ ↓ │
│ ┌───────────────────────┐ │
│ │ Multi-Head │ │ ← 所有词互相看
│ │ Self-Attention │ │
│ └───────────┬───────────┘ │
│ ↓ │
│ ┌───────────────────────┐ │
│ │ Feed-Forward Network │ │
│ └───────────────────────┘ │
│ × N层 │
└─────────────────────────────┘
↑
Input序列
原论文标题:“Attention Is All You Need”(2017年,Google) 核心洞察:不需要循环(RNN),不需要卷积(CNN),只需要注意力机制就够了!
1.2 Self-Attention——Transformer的灵魂
直觉理解:Self-Attention到底在做什么?
类比:一场会议讨论
想象一个6人会议,讨论"The cat sat on the mat"这个句子的含义:
每个人(词)都要弄清楚"我和谁的关系最密切"
"cat"发言时:
- 看了看"The":嗯,你是我的限定词,关系一般(权重0.05)
- 看了看自己"cat":我当然了解自己(权重0.15)
- 看了看"sat":你是我做的动作!关系最紧密(权重0.60)
- 看了看"on":介词,关系不大(权重0.05)
- 看了看了看"the":和我没关系(权重0.03)
- 看了看了看"mat":我坐在你上面,有点关系(权重0.12)
"cat"的新表示 = 0.05×The + 0.15×cat + 0.60×sat + 0.05×on + 0.03×the + 0.12×mat
现在"cat"的向量不仅包含自己的信息,还融合了所有相关词的信息!
特别是"s"的信息(权重最大),所以"cat"现在"知道"自己是句子的主语。
Self-Attention的数学计算:Query, Key, Value——完整推导
为什么要引入Q、K、V三个概念?
类比:图书馆找书
你(Query):想找一本关于"深度学习"的书
书架上的书(Key):每本书的标签/标题
书的内容(Value):每本书的实际内容
匹配过程:
1. 你的需求(Query)和每本书的标签(Key)做匹配 → 得到相关性分数
2. 根据相关性分数,从书的内容(Value)中提取信息
3. 相关性高的书,多提取一些;相关性低的书,少提取一些
具体计算步骤(带维度标注):
假设:句子长度 seq_len=4,模型维度 d_model=512,头数 n_heads=8,每头维度 d_k=64
输入矩阵 X:(4, 512) — 4个词,每个词512维
Step 1:生成Q、K、V(通过3个不同的线性变换)
Q = X · W_Q (4, 512) × (512, 64) = (4, 64) ← 每个词的"需求"
K = X · W_K (4, 512) × (512, 64) = (4, 64) ← 每个词的"特征标签"
V = X · W_V (4, 512) × (512, 64) = (4, 64) ← 每个词的"实际内容"
为什么需要3个不同的变换?
→ 因为一个词的"我需要什么"(Q)、"我能提供什么"(K)、"我的内容"(V)是不同的
Step 2:计算注意力分数
scores = Q · K^T (4, 64) × (64, 4) = (4, 4) ← 每对词之间的相关性
例如:
w1 w2 w3 w4
w1 [0.1 0.8 0.05 0.05]
w2 [0.7 0.1 0.1 0.1 ]
w3 [0.05 0.1 0.7 0.15]
w4 [0.1 0.05 0.2 0.65]
→ w1和w2的相关性很高(0.8),w3和w4的相关性很高(0.7)
Step 3:缩放(除以√d_k = √64 = 8)
scaled_scores = scores / 8
为什么要缩放?
→ 当d_k很大时,Q·K^T的值可能很大
→ 大的输入值会导致softmax输出接近one-hot(梯度接近0)
→ 除以√d_k让方差回到1,softmax的梯度正常
Step 4:Softmax归一化
attention_weights = softmax(scaled_scores) (4, 4) — 每行和为1
现在每行都是一个概率分布,表示"每个词应该关注谁"
Step 5:加权求和
output = attention_weights · V (4, 4) × (4, 64) = (4, 64)
每个词的输出 = 所有词的Value的加权和(权重=注意力分数)
一句话总结:Self-Attention让每个词都能"看到"句子中的所有其他词,并根据相关性加权聚合信息,生成包含上下文信息的新表示。
1.3 位置编码——告诉模型"词的顺序"
为什么需要位置编码?
核心问题:Self-Attention是"位置无关"的
"狗咬人"和"人咬狗"——如果只看Self-Attention的计算:
- 两个句子包含的词相同
- Q、K、V的计算方式相同
→ Self-Attention无法区分这两个句子!
但它们的意思完全不同!→ 必须额外告诉模型"每个词在哪个位置"
正弦位置编码——为什么用sin/cos?
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
其中:
- pos:词在句子中的位置(0, 1, 2, ...)
- i:编码维度的索引(0, 1, 2, ..., d_model/2-1)
- d_model:模型维度
为什么选择正弦/余弦?三个巧妙的原因:
原因1:每个位置有唯一的编码
sin和cos的组合可以唯一标识每个位置
就像每个GPS坐标是唯一的一样
原因2:相对距离可以用线性变换表示
PE(pos+k) 可以用 PE(pos) 的线性变换来表示
(因为 sin(a+b) = sin(a)cos(b) + cos(a)sin(b))
→ 模型可以学到"距离关系"(相邻、隔一个、隔两个...)
原因3:可以外推到更长的序列
正弦函数是周期性的,可以计算任意位置的编码
→ 即使训练时没见过1000个词的句子,推理时也能处理
词嵌入 + 位置编码——两者如何结合?
最终输入 = Token Embedding + Positional Encoding
Token Embedding:词本身的语义信息(可学习的参数矩阵)
"猫" → [0.2, 0.8, -0.1, ...] (512维向量)
Positional Encoding:词的位置信息(固定的正弦函数)
位置0 → [0.0, 1.0, 0.0, ...] (512维向量)
相加后:[0.2, 1.8, -0.1, ...]
→ 模型同时知道"这个词是猫"(语义)和"它在第1个位置"(位置)
类比:就像给每个词发了一张"身份证",上面写着:
姓名:猫(Token Embedding)
地址:第1个位置(Positional Encoding)
1.4 多头注意力与Encoder Block
多头注意力——为什么要"多头"?
类比:多角度分析
单头注意力 = 一个人看问题,只能从一个角度理解
多头注意力 = 多个人同时看问题,每人从不同角度理解,最后综合
例如理解"The animal didn't cross the street because it was too tired"
- Head 1 可能学到:"it"指代"animal"(指代关系)
- Head 2 可能学到:"tired"修饰"animal"(修饰关系)
- Head 3 可能学到:"didn't cross"和"tired"有因果关系(逻辑关系)
单头很难同时学到这三种关系,但多头可以!
数学实现——维度追踪:
假设:d_model=512, n_heads=8, d_k=d_model/n_heads=64
输入 X:(batch, seq_len, 512)
每个头的处理:
head_i = Attention(X·W_Q_i, X·W_K_i, X·W_V_i)
= softmax((X·W_Q_i)(X·W_K_i)^T / √64) · (X·W_V_i)
每个head的输出:(batch, seq_len, 64)
合并所有头:
MultiHead = Concat(head_1, ..., head_8) · W_O
= (batch, seq_len, 8×64) · (512, 512)
= (batch, seq_len, 512)
输出维度和输入维度相同(512),方便堆叠多层!
Encoder Block的完整结构——逐层理解
输入 x:(batch, seq_len, 512)
│
├──→ Multi-Head Self-Attention(x, x, x)
│ 输出:(batch, seq_len, 512)
│ │
│ ↓
├──→ Add & LayerNorm:norm₁(x + attn_output)
│ 残差连接:保留原始信息 + 新学到的信息
│ 层归一化:稳定数值范围
│ │
│ ↓
├──→ Feed-Forward Network
│ Linear(512 → 2048) → ReLU → Linear(2048 → 512)
│ │
│ ↓
└──→ Add & LayerNorm:norm₂(h + ffn_output)
│
↓
输出:(batch, seq_len, 512) ← 维度不变,可以堆叠N层
Feed-Forward Network(FFN)——每个位置的"独立思考"
FFN(x) = max(0, x · W₁ + b₁) · W₂ + b₂
维度变化:512 → 2048 → 512
↑ 先升维4倍(512→2048),增加表达能力
↑ 再降回原维度(2048→512),方便后续处理
关键:FFN对每个位置是独立计算的!
→ 位置1的FFN不看位置2、3、4的输出
→ FFN的作用是"独立思考":在Attention已经收集了全局信息后,
每个位置独立地对这些信息做非线性变换
Attention vs FFN的分工:
Attention:收集信息("看看别人说了什么")
FFN:处理信息("想想自己该怎么理解")
类比:一场考试
Attention = 看参考资料(收集信息)
FFN = 独立答题(处理信息)
1.5 Masked Self-Attention与Decoder
为什么Decoder需要Mask?
类比:考试时不能偷看答案
训练时,我们知道完整的译文:"我 爱 大 模型"
但我们不能让模型"偷看"未来的词!
预测"爱"时:只能看到"<start>"和"我"
预测"大"时:只能看到"<start>"、"我"和"爱"
预测"模型"时:只能看到"<start>"、"我"、"爱"和"大"
如果不加Mask,模型会直接抄答案,什么都学不到!
Mask的实现——下三角矩阵
注意力分数矩阵(4个词):
<start> 我 爱 大
<start> ✓ ✗ ✗ ✗
我 ✓ ✓ ✗ ✗
爱 ✓ ✓ ✓ ✗
大 ✓ ✓ ✓ ✓
✓ = 可以看到(正常计算注意力)
✗ = 不能看到(设为-∞,softmax后变为0)
实现方式:
mask矩阵 = [[1, 0, 0, 0],
[1, 1, 0, 0],
[1, 1, 1, 0],
[1, 1, 1, 1]]
scores = scores.masked_fill(mask == 0, -1e9) # 0的位置设为-∞
Encoder-Decoder Cross-Attention——连接两个世界的桥梁
Decoder的第二个Attention层(Cross-Attention):
- Query来自Decoder("我在找什么信息?")
- Key和Value来自Encoder("源语言提供了什么信息?")
类比:翻译时的"参考原文"
Query = 你正在翻译的当前词的需求("我现在需要翻译'爱',原文中哪里有相关信息?")
Key/Value = 原文中每个词的信息
Cross-Attention让Decoder在生成每个译文词时,都能"回头看"原文的相关部分
→ 这就是"注意力对齐":自动学到"哪个译文词对应哪个原文词"
1.6 Transformer代码实现
import torch
import torch.nn as nn
import math
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads # 每个头的维度
# Q、K、V的线性变换矩阵
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model) # 输出变换
def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
# 线性变换并分头
# (batch, seq_len, d_model) → (batch, n_heads, seq_len, d_k)
Q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
K = self.W_K(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
V = self.W_V(V).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
# 计算注意力分数:Q·K^T / √d_k
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
# 应用Mask(如果有的话)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# Softmax归一化
attn_weights = torch.softmax(scores, dim=-1)
# 用注意力权重对V加权求和
context = torch.matmul(attn_weights, V)
# 合并多头:(batch, n_heads, seq_len, d_k) → (batch, seq_len, d_model)
context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
output = self.W_O(context)
return output
class TransformerBlock(nn.Module):
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
super().__init__()
self.attention = MultiHeadAttention(d_model, n_heads)
self.norm1 = nn.LayerNorm(d_model)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff), # 升维
nn.ReLU(),
nn.Linear(d_ff, d_model) # 降维
)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None):
# Self-Attention + 残差连接 + LayerNorm
attn_output = self.attention(x, x, x, mask) # Q=K=V=x(自注意力)
x = self.norm1(x + self.dropout(attn_output))
# FFN + 残差连接 + LayerNorm
ffn_output = self.ffn(x)
x = self.norm2(x + self.dropout(ffn_output))
return x
1.7 核心总结
Transformer的五大核心创新
1. Self-Attention:让每个位置都能直接访问其他所有位置
→ 解决了RNN的"逐步传递"问题
2. Multi-Head Attention:从多个角度并行建模关系
→ 同时捕获语法、语义、位置等多种关系
3. Positional Encoding:用正弦/余弦函数注入位置信息
→ 让模型知道"词的顺序"
4. 残差连接 + LayerNorm:稳定深层网络训练
→ 来自ResNet的智慧,让梯度可以"跳过"层
5. 并行计算:所有位置同时计算
→ 训练速度比RNN快10-100倍
Transformer vs RNN 对比
特性 RNN Transformer
─────────────────────────────────────────────────
计算方式 串行(逐步) 并行(所有位置同时)
长距离依赖 困难(梯度消失) 容易(直接连接)
计算复杂度 O(n·d²) O(n²·d)
训练速度 慢 快(10-100倍)
序列长度限制 无理论限制 受限于显存(n²注意力)
参数量 少 多(但效果更好)
实际表现 较好 显著更好(大模型时代的基石)
模块二:BERT与GPT——预训练语言模型
2.1 BERT——双向理解的大模型
BERT的核心思想——为什么要"双向"?
类比:阅读理解
单向(GPT):从左到右阅读,像"遮住后面的文字"
"I went to the bank to [???]"
→ 只知道前面是"bank",不知道后面是"deposit"还是"river"
→ 无法确定"bank"是银行还是河岸
双向(BERT):同时看前后文,像"通读全文后回答问题"
"I went to the [bank] to deposit money"
→ 看到了后面的"deposit",确定"bank"是银行
"I sat on the [bank] of the river"
→ 看到了后面的"river",确定"bank"是河岸
双向理解 = 更好的语义理解能力
BERT的两个预训练任务——从海量文本中学习语言
任务1:Masked Language Model(MLM)——完形填空
训练方式:随机遮盖15%的词,让模型预测被遮盖的词
输入: "The [MASK] sat on the mat"
目标: 预测 [MASK] = "cat"
为什么遮盖15%而不是100%?
- 遮盖太多:上下文信息不足,模型猜不准
- 遮盖太少:训练效率低(每个句子只学一个词)
- 15%是实验验证的最佳比例
为什么用[MASK]而不是直接删除?
- 删除后模型知道"这里缺了一个词"
- 用[MASK]替换,模型需要根据上下文推断
任务2:Next Sentence Prediction(NSP)——判断句子关系
训练方式:给模型两个句子,判断它们是否是连续的
正例:"他去超市买了牛奶。" + "然后回家做早餐。" → IsNext
负例:"他去超市买了牛奶。" + "今天天气很好。" → NotNext
为什么需要NSP?
- 很多NLP任务需要理解句子之间的关系(问答、推理、自然语言推断)
- NSP让模型学到"句子之间的逻辑关系"
BERT的架构——基于Transformer Encoder
BERT = Transformer Encoder × 12层(BERT-base)或24层(BERT-large)
BERT-base:12层,768维,12头,1.1亿参数
BERT-large:24层,1024维,16头,3.4亿参数
输入表示(三种嵌入相加):
┌─────────────────────────────────────────┐
│ Token Embedding ← 词的语义信息 │
│ + Segment Embedding ← 句子A/B标记 │
│ + Position Embedding ← 位置信息 │
│ = 最终输入 │
└─────────────────────────────────────────┘
特殊标记:
[CLS]:放在句子开头,用于分类任务的输出
[SEP]:分隔两个句子
[MASK]:遮盖标记
BERT微调——一个预训练模型解决多种NLP任务
BERT的强大之处:预训练一次,微调多次
文本分类:取[CLS]的输出 → 加一个分类头 → 完成
[CLS] 我 很 喜欢 这部 电影
↓
[CLS的输出] → Linear → softmax → 正面/负面
命名实体识别:取每个token的输出 → 序列标注
[CLS] 小 明 在 北京 上学
↓ ↓ ↓ ↓ ↓ ↓
B-PER I-PER O B-LOC I-LOC O
问答系统:取每个token的输出 → 预测答案的起止位置
问题:谁在北京上学?
段落:小明在北京上学
→ 预测答案起始位置=1("小"),结束位置=2("明")
→ 答案:"小明"
核心思想:BERT已经"理解"了语言,只需要在上面加一个简单的分类头
2.2 GPT——自回归生成的大模型
GPT的核心思想——预测下一个词
GPT = Generative Pre-trained Transformer = Transformer Decoder
训练目标极其简单:给定前文,预测下一个词
"今天" → 预测"天气"
"今天天气" → 预测"很好"
"今天天气很好" → 预测"。"
这就是自回归(Autoregressive)生成:
每次只生成一个词,然后把这个词加入输入,继续生成下一个词
就像"滚雪球"一样,越滚越大
GPT vs BERT——核心区别对比
特性 BERT GPT
────────────────────────────────────────────────────────
架构 Transformer Encoder Transformer Decoder
方向 双向(同时看前后文) 单向(只看前面的词)
训练目标 遮盖词预测(MLM) 预测下一个词(自回归)
注意力 没有Mask,所有位置互相看 有Mask,只能看前面的位置
擅长任务 理解类(分类、NER、问答) 生成类(文本生成、对话)
代表模型 BERT, RoBERTa, ALBERT GPT-1/2/3/4, ChatGPT
核心区别:BERT像"阅读理解"——读完全文再回答
GPT像"写作"——一个字一个字地写
GPT系列演进——从学术到改变世界
GPT-1 (2018):1.17亿参数,12层Decoder
→ 证明了"预训练+微调"范式的有效性
GPT-2 (2019):15亿参数,48层Decoder
→ 展示了零样本能力(不需要微调就能做任务)
→ 因为"太危险"而延迟发布
GPT-3 (2020):1750亿参数,96层Decoder
→ 展示了少样本学习能力(给几个例子就能做任务)
→ 开启了"大模型时代"
ChatGPT (2022):GPT-3.5 + RLHF(人类反馈强化学习)
→ 通过人类反馈让模型更"对齐"人类意图
→ 改变了整个AI行业
GPT-4 (2023):多模态,推理能力大幅提升
→ 可以理解图片、代码、数学
2.3 BERT实战——微调中文分类
from transformers import BertTokenizer, BertForSequenceClassification
import torch
# 1. 加载预训练模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=2)
# 2. 数据预处理——把文本转换为模型能理解的数字
text = "这部电影真的很好看"
inputs = tokenizer(
text,
return_tensors='pt', # 返回PyTorch张量
padding=True, # 填充到相同长度
truncation=True # 截断过长的文本
)
# inputs包含:
# input_ids: [101, 2023, 3124, 4638, 5011, 2552, 4518, 1993, 102]
# [CLS] 这 部 电影 真的 很 好看 [SEP]
# attention_mask: [1, 1, 1, 1, 1, 1, 1, 1, 1] (哪些位置是真实词)
# token_type_ids: [0, 0, 0, 0, 0, 0, 0, 0, 0] (句子A/B标记)
# 3. 前向传播
outputs = model(**inputs, labels=torch.tensor([1])) # label=1表示正面
loss = outputs.loss # 损失值
logits = outputs.logits # 预测分数,shape: (1, 2)
# 4. 预测
prediction = torch.argmax(logits, dim=1) # 0=负面,1=正面
print(f"预测:{'正面' if prediction == 1 else '负面'}")
2.4 预训练语言模型总结对比
模型 架构 方向 预训练任务 擅长任务
────────────────────────────────────────────────────────────────
BERT Encoder 双向 MLM + NSP 分类、NER、问答
GPT Decoder 单向 预测下一个词 文本生成、对话
T5 Encoder-Decoder 双向 文本到文本 翻译、摘要、所有任务
LLaMA Decoder 单向 预测下一个词 通用大模型
ChatGLM Prefix LM 双向 混合 对话、理解
模块三:HuggingFace生态——NLP开发的瑞士军刀
3.1 Tokenizer——文本与数字的桥梁
Tokenizer到底在做什么?
人类理解:文字("我爱AI")
计算机理解:数字([101, 2023, 9932, 102])
Tokenizer = 翻译官:把人类的文字翻译成计算机的数字,以及反过来
完整流程:
"我爱AI" → 分词["我", "爱", "AI"] → 查词表→ [101, 2023, 9932, 102]
→ 添加特殊标记[CLS]...[SEP]
→ 填充/截断到固定长度
不同的分词算法——为什么分词方式很重要?
Word-level(按词分割):
"I love AI" → ["I", "love", "AI"]
问题1:词表太大(英文几十万词)
问题2:无法处理未登录词(OOV)—— "ChatGPT"如果不在词表中怎么办?
Character-level(按字符分割):
"AI" → ["A", "I"]
问题:序列太长(一个句子变成几百个字符),语义信息弱
Subword-level(按子词分割)——主流方案:
"playing" → ["play", "##ing"] (##表示这是词的后半部分)
"ChatGPT" → ["Chat", "##G", "##PT"]
优点:
1. 词表大小适中(通常3万-5万)
2. 能处理任何未登录词(总能拆成子词)
3. 保留了语义信息("play"和"playing"共享"play")
3.2 Datasets——数据集的统一接口
from datasets import load_dataset
# 加载数据集(自动下载和缓存)
dataset = load_dataset('imdb') # IMDB电影评论数据集
dataset = load_dataset('csv', data_files='my_data.csv') # 自定义CSV
# 查看数据结构
print(dataset)
# DatasetDict({
# train: Dataset({features: ['text', 'label'], num_rows: 25000})
# test: Dataset({features: ['text', 'label'], num_rows: 25000})
# })
# 查看第一个样本
print(dataset['train'][0])
# {'text': 'I loved this movie...', 'label': 1}
# 数据预处理(用map函数批量处理)
def preprocess(examples):
return tokenizer(examples['text'], truncation=True, padding=True)
tokenized_dataset = dataset.map(preprocess, batched=True)
# map函数会自动把tokenizer应用到每个样本上
# batched=True表示一次处理一批样本(更快)
3.3 Pipeline——一行代码完成推理
from transformers import pipeline
# 情感分析——一行代码!
classifier = pipeline('sentiment-analysis')
result = classifier("I love this movie!")
# [{'label': 'POSITIVE', 'score': 0.9998}]
# 文本生成
generator = pipeline('text-generation', model='gpt2')
result = generator("Once upon a time", max_length=50)
# 问答系统
qa = pipeline('question-answering')
result = qa(question="What is AI?", context="AI is artificial intelligence...")
# 命名实体识别
ner = pipeline('ner', grouped_entities=True)
result = ner("My name is John and I live in New York.")
# 零样本分类(不需要训练数据!)
zero_shot = pipeline('zero-shot-classification')
result = zero_shot(
"This is a great movie!",
candidate_labels=["positive", "negative", "neutral"]
)
# {'labels': ['positive', 'neutral', 'negative'], 'scores': [0.9, 0.07, 0.03]}
Pipeline的魔力:它自动帮你完成所有的预处理、模型加载、后处理。一行代码 = 完整的推理流程。
3.4 Trainer——标准化的训练框架
from transformers import Trainer, TrainingArguments
# 训练参数配置
training_args = TrainingArguments(
output_dir='./results', # 输出目录
num_train_epochs=3, # 训练轮数
per_device_train_batch_size=16, # 每个设备的batch大小
per_device_eval_batch_size=64, # 评估时的batch大小
warmup_steps=500, # 预热步数(学习率从0逐渐增大)
weight_decay=0.01, # 权重衰减(L2正则化)
logging_dir='./logs', # 日志目录
evaluation_strategy='epoch', # 每个epoch评估一次
save_strategy='epoch', # 每个epoch保存一次
load_best_model_at_end=True, # 训练结束后加载最优模型
)
# 创建Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
compute_metrics=compute_metrics, # 自定义评估函数
)
# 训练
trainer.train()
# 评估
results = trainer.evaluate()
# 保存模型
trainer.save_model('./my_model')
3.5 实战——中文情感分类完整流程
# ===== 完整的情感分类项目 =====
from datasets import load_dataset
from transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments
# 1. 加载数据
dataset = load_dataset('csv', data_files='ChnSentiCorp.csv')
# 2. 分词
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
def tokenize_function(examples):
return tokenizer(examples['text'], padding='max_length', truncation=True, max_length=128)
tokenized = dataset.map(tokenize_function, batched=True)
# 3. 加载预训练模型
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=2)
# 4. 训练
training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=3,
per_device_train_batch_size=32,
learning_rate=2e-5,
evaluation_strategy='epoch',
)
trainer = Trainer(model=model, args=training_args,
train_dataset=tokenized['train'], eval_dataset=tokenized['test'])
trainer.train()
# 5. 预测
from transformers import pipeline
classifier = pipeline('sentiment-analysis', model='./results/checkpoint-best')
print(classifier("这部电影太棒了!"))
# [{'label': 'POSITIVE', 'score': 0.999}]
模块四:NLP实战与综合复习
4.1 NLP高级实战任务
文本摘要——压缩信息
输入:长文本(如新闻文章)
输出:简短摘要
方法1:抽取式——从原文中挑选重要句子组合成摘要
方法2:生成式——用模型重新"写"一段摘要(更灵活,但可能编造信息)
使用HuggingFace:
summarizer = pipeline('summarization')
summary = summarizer(long_text, max_length=50)
机器翻译——跨语言理解
使用预训练翻译模型:
translator = pipeline('translation_en_to_zh')
result = translator("I love deep learning")
# [{'translation_text': '我喜欢深度学习'}]
4.2 AI写诗项目——综合实战
项目流程:
1. 数据准备:收集古诗数据集(如唐诗三百首)
2. 数据预处理:分词、构建词表、转为数字序列
3. 模型选择:使用GPT2-Chinese进行微调
4. 训练:在古诗数据上微调模型
5. 生成:给定开头(如"春风"),让模型生成完整诗歌
关键代码:
from transformers import GPT2LMHeadModel, BertTokenizer
model = GPT2LMHeadModel.from_pretrained('uer/gpt2-chinese-poem')
4.3 阶段总结——核心知识地图
1. Transformer = Self-Attention + FFN + 残差连接 + LayerNorm
2. Self-Attention = Q·K^T/√d_k → softmax → ·V
3. Multi-Head = 多组Q/K/V并行计算,从不同角度理解
4. 位置编码 = 正弦/余弦函数(注入顺序信息)
5. BERT = Transformer Encoder × N(双向,擅长理解)
6. GPT = Transformer Decoder × N(单向,擅长生成)
7. HuggingFace = Tokenizer + Datasets + Pipeline + Trainer
8. 微调 = 预训练模型 + 下游任务分类头 + 少量标注数据
📝 自测题
- Self-Attention:给定3个词的向量,手写Q/K/V的计算过程,标注每个矩阵的维度
- Multi-Head:解释为什么需要多头,一个头不行吗?用具体例子说明
- 位置编码:为什么Transformer需要位置编码而RNN不需要?解释正弦编码的三个优点
- Mask:解释Encoder的Self-Attention和Decoder的Masked Self-Attention的区别
- BERT vs GPT:从架构、训练目标、擅长任务三个维度对比
- BERT预训练:解释MLM和NSP两个预训练任务,为什么需要它们?
- 微调:为什么在预训练模型上微调比从头训练效果好?用"迁移学习"的直觉解释
- HuggingFace:用Pipeline实现零样本分类(写代码)
- Tokenizer:解释BPE分词算法的工作原理,为什么Subword-level是主流?
- 综合:设计一个完整的情感分析系统(从数据到部署),列出每个步骤
📚 推荐补充资源
| 知识点 | 推荐资源 | 说明 |
|---|---|---|
| Transformer | Jay Alammar《The Illustrated Transformer》 | 图解Transformer,必看 |
| BERT | Jay Alammar《The Illustrated BERT》 | 图解BERT |
| GPT | Andrej Karpathy《Let’s build GPT》 | 从零实现GPT |
| HuggingFace | HuggingFace官方课程 | 免费的NLP实战课程 |
| 注意力机制 | 斯坦福CS224n | NLP经典课程 |
| Tokenizer | HuggingFace Tokenizers文档 | 分词器详解 |
...