上一篇:速通 AI(三):Transformer 架构详解 — Self-Attention、多头注意力、位置编码、Encoder/Decoder
目标:掌握 BERT/GPT 预训练模型的核心原理与实战,熟练使用 HuggingFace 生态进行 NLP 任务开发。
前置要求:上一篇(Transformer 架构:Self-Attention、Multi-Head、位置编码、Encoder/Decoder)
本阶段知识依赖图
flowchart TD
A["Transformer 架构(上一篇)"]
A --> L[预训练语言模型]
L --> L1["BERT(Encoder-only)"] --> L1a[文本分类、NER、阅读理解]
L --> L2["GPT(Decoder-only)"] --> L2a[文本生成、对话]
L --> L3[Seq2Seq模型] --> L3a[翻译、摘要]
A --> H["HuggingFace生态(实战工具)"]
H --> H1[Tokenizer] --> H1a[文本编码/解码]
H --> H2[Datasets] --> H2a[数据集加载与处理]
H --> H3[Pipeline] --> H3a[一行代码完成推理]
H --> H4[Trainer] --> H4a[模型训练框架]
H --> H5[Evaluate] --> H5a[评估指标]
1. BERT 与 GPT——预训练语言模型
1.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"是银行。
双向理解 = 更好的语义理解能力。
BERT 双向的代价:BERT 不能做自回归生成——因为训练时每个位置都能看到全文,无法模拟"逐词生成"的过程。这就是为什么 GPT(单向)适合生成任务,BERT(双向)适合理解任务。
BERT 的两个预训练任务——从海量文本中学习语言
任务 1 : Masked Language Model (MLM)——完形填空
训练方式:随机遮盖 15% 的词,让模型预测被遮盖的词。输入:“The [MASK] sat on the mat”,目标:预测 [MASK] = “cat”。
MLM 的损失函数:
$$ \mathcal{L}_{\text{MLM}} = -\sum_{i \in \text{masked}} \log P(x_i | x_{\backslash i}) $$直觉:只计算被遮盖位置的损失——让模型在这些位置"填空",填对了损失小,填错了损失大。
为什么遮盖 15% 而不是 100% ?遮盖太多:上下文信息不足,模型猜不准。遮盖太少:训练效率低(每个句子只学一个词)。 15% 是实验验证的最佳比例。
15% 被选中词的处理策略(80/10/10 规则):
| 处理方式 | 比例 | 例子 | 为什么这样做 |
|---|---|---|---|
| 替换为 [MASK] | 80% | “The [MASK] sat on the mat” | 让模型学习"根据上下文填空"的能力 |
| 替换为随机词 | 10% | “The happy sat on the mat” | 迫使模型不能只依赖 [MASK] 标记,要真正理解上下文 |
| 保持原词不变 | 10% | “The cat sat on the mat” | 让模型学习"判断每个词是否正确"的能力,保持输入分布接近真实 |
为什么需要混合策略? 如果 100% 用 [MASK],模型会学到"只在看到 [MASK] 时才预测"的捷径。混合策略让模型无法投机取巧,必须真正理解语言。
为什么用[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 亿参数
输入表示(三种嵌入相加):
$$ \text{input}_i = \text{TokenEmb}(x_i) + \text{SegEmb}(s_i) + \text{PosEmb}(i) $$- Token Embedding:词本身的语义信息
- Segment Embedding:句子 A/B 标记(区分两个句子)
- Position Embedding:位置信息(让模型知道词序)
特殊标记:[CLS] 放在句子开头,用于分类任务的输出(通过 Self-Attention 聚合了所有位置的信息,可代表整句语义)。[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 已经"理解"了语言,只需要在上面加一个简单的分类头。
为什么预训练+微调范式有效?
BERT 只需在少量标注数据上微调就能达到很好的效果,这背后有三个关键原因:
1. 预训练学到了通用语言表示
在海量无标注文本上做 MLM 预训练时,BERT 学到了:
- 语法知识:词性、句法结构(“cat sat on the mat"中 cat 是主语)
- 语义知识:词义、同义词关系(“happy"和"glad"含义接近)
- 世界知识:事实性知识(“巴黎是法国的首都”)
这些知识是"通用的”——不管下游任务是情感分类还是命名实体识别,都需要理解语言。
2. 迁移学习的直觉
类比:一个学过英语的人去学法语,比一个没学过任何语言的人学法语容易得多——因为语言之间有共通的结构(语法、语序、时态)。
同理,BERT 在预训练阶段已经"学会了语言的结构”,微调时只需要"学一个新的任务规则"(如"判断情感=看[CLS]的输出"),而不是从零开始学语言。
3. 为什么微调学习率要很小?——灾难性遗忘
如果微调学习率太大,模型会"忘记"预训练学到的通用知识——这就是灾难性遗忘。类比:你学了 10 年英语,突然要学法语。如果学习速度太快,你可能会把英语语法忘掉。
解决方案:用很小的学习率(如 $2 \times 10^{-5}$),让模型在保留预训练知识的基础上,缓慢适应新任务。
面试要点:回答"预训练+微调为什么有效?“时,从通用语言表示、迁移学习直觉、灾难性遗忘三个角度展开。
1.2 GPT——自回归生成的大模型
GPT 的核心思想——预测下一个词
GPT = Generative Pre-trained Transformer = Transformer Decoder
训练目标极其简单:给定前文,预测下一个词。数学上,这等价于最大化序列的联合概率:
$$ P(x_1, x_2, \ldots, x_n) = \prod_{i=1}^{n} P(x_i | x_1, x_2, \ldots, x_{i-1}) $$训练损失函数:
$$ \mathcal{L}_{\text{GPT}} = -\sum_{i=1}^{n} \log P(x_i | x_1, \ldots, x_{i-1}; \theta) $$直觉:把一句话拆成"逐词预测"的条件概率乘积。“今天天气很好” = P(天气|今天) × P(很好|今天天气) × …,每个词的预测都依赖前面所有词。
- “今天” → 预测"天气”
- “今天天气” → 预测"很好"
- “今天天气很好” → 预测"。"
这就是自回归(Autoregressive)生成:每次只生成一个词,然后把这个词加入输入,继续生成下一个词。就像"滚雪球"一样,越滚越大。
动手计算:GPT 自回归生成过程
假设 GPT 模型已经训练好,输入"I love",让我们手算它如何生成下一个词。
Step 1:输入 “I love” → 模型输出下一个词的概率分布
假设模型最后一层输出的 logits(未归一化分数)为:
| 候选词 | AI | the | cat | deep | learning |
|---|---|---|---|---|---|
| logits | 3.2 | 1.5 | 0.8 | 2.1 | 1.9 |
Step 2:Softmax 归一化 → 概率分布
$$ \text{softmax}([3.2, 1.5, 0.8, 2.1, 1.9]) = [0.532, 0.097, 0.048, 0.177, 0.145] $$Step 3:采样(或取 argmax)→ 选择 “AI”(概率最高 53.2%)
Step 4:将 “AI” 拼接到输入 → “I love AI” → 再次前向传播 → 预测下一个词
| 候选词 | 。 | ! | very | so | and |
|---|---|---|---|---|---|
| 概率 | 0.45 | 0.20 | 0.15 | 0.12 | 0.08 |
选择 “。” → 生成完成:“I love AI。”
解读:GPT 每次只生成一个 token,但每次生成都要重新计算整个序列的注意力(因为新 token 需要看到前面所有 token)。这就是为什么生成 100 个 token 需要 100 次前向传播,比编码慢得多。
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):多模态,推理能力大幅提升。可以理解图片、代码、数学。
1.3 BERT 实战——微调中文分类
from transformers import BertTokenizer, BertForSequenceClassification
import torch
# 1. 加载预训练模型和分词器(自动下载 ~400MB 的 bert-base-chinese 权重)
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=2)
# num_labels=2:二分类(正面/负面)。模型会自动在 [CLS] 输出上加一个 768→2 的线性层
# 2. 数据预处理:文本 → token IDs → attention mask
text = "这部电影真的很好看"
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True)
# padding=True:短句子填充到相同长度;truncation=True:长句子截断到 512
# 3. 前向传播(labels 用于计算交叉熵损失,训练时需要,推理时可省略)
outputs = model(**inputs, labels=torch.tensor([1])) # label=1 表示正面
loss = outputs.loss # 交叉熵损失(训练时反向传播用)
logits = outputs.logits # shape: (1, 2) — 两个类别的原始分数
# 4. 预测:取 logits 中较大的那个类别
prediction = torch.argmax(logits, dim=1) # 0=负面,1=正面
print(f"预测:{'正面' if prediction == 1 else '负面'}")
1.4 预训练语言模型总结对比
| 模型 | 架构 | 方向 | 预训练任务 | 擅长任务 |
|---|---|---|---|---|
| BERT | Encoder | 双向 | MLM + NSP | 分类、 NER 、问答 |
| GPT | Decoder | 单向 | 预测下一个词 | 文本生成、对话 |
| T5 | Encoder-Decoder | 双向 | 文本到文本 | 翻译、摘要、所有任务 |
| LLaMA | Decoder | 单向 | 预测下一个词 | 通用大模型 |
| ChatGLM | Prefix LM | 双向 | 混合 | 对话、理解 |
三种 Transformer 架构对比
| 架构 | 代表模型 | 注意力方式 | 擅长任务 | 典型应用 |
|---|---|---|---|---|
| Encoder-only | BERT, RoBERTa | 双向(全连接) | 理解、分类、NER | 文本分类、问答 |
| Decoder-only | GPT, LLaMA | 单向(因果Mask) | 生成、对话 | 文本生成、ChatGPT |
| Encoder-Decoder | T5, BART | 编码双向 + 解码单向 | 翻译、摘要 | Seq2Seq 任务 |
模块小结: BERT 与 GPT
| 你学到了什么 | 为什么重要 |
|---|---|
| BERT 双向理解 | 理解类任务的基石 |
| MLM + NSP 预训练任务 | 从海量文本学习语言知识 |
| GPT 自回归生成 | 生成类任务的基石 |
| BERT vs GPT 核心区别 | Encoder vs Decoder ,双向 vs 单向 |
| 预训练 + 微调范式 | 一个模型解决多种 NLP 任务 |
| 理解了 Transformer 架构和 BERT/GPT 的预训练原理后,一个现实问题是:从零实现这些模型需要数千行代码。有没有更高效的方式?HuggingFace 生态正是为此而生——它提供了统一的 API,让你用几行代码就能加载、训练和部署最先进的 NLP 模型。本节将系统介绍 Tokenizer、Datasets、Pipeline、Trainer 四大核心工具。 |
2. HuggingFace 生态——NLP 开发的瑞士军刀
2.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”]。问题:词表太大(英文几十万词),无法处理未登录词(OOV)。
- Character-level(按字符分割):“AI” → [“A”, “I”]。问题:序列太长(一个句子变成几百个字符),语义信息弱。
- Subword-level (按子词分割)——主流方案:“playing” → [“play”, “##ing”](##表示这是词的后半部分)。“ChatGPT” → [“Chat”, “##G”, “##PT”]。
Subword 优点:
- 词表大小适中(通常 3 万-5 万)
- 能处理任何未登录词(总能拆成子词)
- 保留了语义信息(“play"和"playing"共享"play”)
BPE 分词算法数值示例
BPE(Byte Pair Encoding)是最常用的 Subword 算法。以训练语料为 [“low”, “lower”, “newest”, “widest”] 为例,展示它如何自动学习子词:
Step 1:初始化——每个字符为一个 token,并加上词尾标记 </w>
| 词 | 初始拆分 |
|---|---|
| low | l o w |
| lower | l o w e r |
| newest | n e w e s t |
| widest | w i d e s t |
Step 2:统计所有相邻字符对的出现频率
| 字符对 | 频率 |
|---|---|
| e s | 2(newest + widest) |
| s t | 2(newest + widest) |
| l o | 2(low + lower) |
| o w | 2(low + lower) |
| … | … |
Step 3:合并频率最高的对(假设 e+s → es)
| 词 | 合并后 |
|---|---|
| low | l o w |
| lower | l o w e r |
| newest | n e w es t |
| widest | w i d es t |
Step 4:重复合并(es+t → est → new+est → newest…)
最终学到的词表可能包含:[l, o, w, </w>, lo, low, est, newest, es, er, ...]
解读:BPE 通过频率自动发现"low"是一个整体、“est"是常见后缀、“er"是另一个后缀。这样 “lowest”(训练时没见过)可以拆分为 low + est,“newer” 可以拆分为 new + er——完美解决了未登录词问题。
2.2 Datasets——数据集的统一接口
from datasets import load_dataset
# 加载数据集(自动下载和缓存)
dataset = load_dataset('imdb')
# 查看数据结构
print(dataset)
# 查看第一个样本
print(dataset['train'][0])
# 数据预处理(用map函数批量处理)
def preprocess(examples):
return tokenizer(examples['text'], truncation=True, padding=True)
tokenized_dataset = dataset.map(preprocess, batched=True)
2.3 Pipeline——一行代码完成推理
from transformers import pipeline
# 情感分析——一行代码!
classifier = pipeline('sentiment-analysis')
result = classifier("I love this movie!")
# 文本生成
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"])
Pipeline 的魔力:它自动帮你完成所有的预处理、模型加载、后处理。一行代码 = 完整的推理流程。
2.4 Trainer——标准化的训练框架
from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
output_dir='./results', # checkpoint 和日志保存目录
num_train_epochs=3, # 训练轮数(微调通常 2-5 轮,太多会过拟合)
per_device_train_batch_size=16, # 训练批大小(越大梯度越稳定,但显存消耗越大)
per_device_eval_batch_size=64, # 评估批大小(不需要反向传播,可以用更大的 batch)
warmup_steps=500, # 预热步数(前 500 步学习率从 0 线性增长到设定值)
# 为什么需要预热?训练初期梯度不稳定,大学习率会破坏预训练权重
weight_decay=0.01, # 权重衰减(L2 正则化,防止权重过大,减少过拟合)
logging_dir='./logs', # TensorBoard 日志目录
evaluation_strategy='epoch', # 每个 epoch 结束时评估一次(也可设为 'steps')
save_strategy='epoch', # 每个 epoch 结束时保存 checkpoint
load_best_model_at_end=True, # 训练结束后自动加载最佳 checkpoint(而非最后一个)
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
compute_metrics=compute_metrics, # 自定义评估函数(如准确率、F1)
)
trainer.train() # 开始训练(自动处理梯度裁剪、混合精度等)
results = trainer.evaluate() # 在评估集上计算指标
trainer.save_model('./my_model') # 保存最终模型(用于部署或后续推理)
2.5 实战——中文情感分类完整流程
from datasets import load_dataset
from transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments
from transformers import pipeline
# 1. 加载数据(ChnSentiCorp 是中文情感分类数据集,约 1 万条酒店评论)
dataset = load_dataset('seamew/ChnSentiCorp')
# 2. 分词:文本 → token IDs(BERT 输入格式)
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
def tokenize_function(examples):
return tokenizer(examples['text'], padding='max_length', truncation=True, max_length=128)
# max_length=128:中文评论通常较短,128 足够(BERT 最大支持 512)
tokenized = dataset.map(tokenize_function, batched=True) # batched=True:批量处理,速度快 10 倍
# 3. 加载预训练模型(在 BERT 上加一个 768→2 的分类头)
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=2)
# 4. 训练(微调 BERT 的所有参数,学习率很小 2e-5 以避免破坏预训练知识)
training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=3, # 3 个 epoch 通常足够(微调不需要太多轮)
per_device_train_batch_size=32, # 批大小 32(越大越稳定,但显存消耗越大)
learning_rate=2e-5, # 微调学习率:比从头训练小 100 倍(避免灾难性遗忘)
evaluation_strategy='epoch', # 每个 epoch 结束时评估一次
)
trainer = Trainer(model=model, args=training_args,
train_dataset=tokenized['train'], eval_dataset=tokenized['test'])
trainer.train() # 约 5-10 分钟(GPU),准确率可达 90%+
# 5. 预测(加载最佳 checkpoint,用 Pipeline 一行推理)
classifier = pipeline('sentiment-analysis', model='./results/checkpoint-best')
print(classifier("这部电影太棒了!")) # [{'label': 'LABEL_1', 'score': 0.99}]
模块小结: HuggingFace 生态
| 你学到了什么 | 为什么重要 |
|---|---|
| Tokenizer 分词 | 文本与数字的桥梁 |
| Subword 分词算法 | 平衡词表大小和表达能力 |
| Datasets 数据加载 | 统一的数据集接口 |
| Pipeline 一行推理 | 快速验证模型效果 |
| Trainer 训练框架 | 标准化的训练流程 |
| 有了 Transformer(骨架)、BERT/GPT(预训练模型)、HuggingFace(工具链),我们现在可以把三者组合起来,解决真实的 NLP 任务。本节通过文本摘要、机器翻译、命名实体识别等实战任务,串联前面所有知识点,展示"从原始文本到模型部署"的完整流程。 |
3. NLP 实战与综合复习
3.1 NLP 任务全景——从输入到输出
flowchart LR
RAW["原始文本"] --> TOK["Tokenizer<br/>分词+编码"]
TOK --> MODEL["预训练模型<br/>BERT/GPT/T5"]
MODEL --> HEAD["任务头"]
HEAD --> T1["分类<br/>[CLS]→Linear"]
HEAD --> T2["序列标注<br/>每个token→标签"]
HEAD --> T3["生成<br/>自回归解码"]
HEAD --> T4["问答<br/>起止位置预测"]
每种 NLP 任务的区别在于任务头的设计,而不是模型本身。BERT 已经"理解"了语言,我们只需要在上面加不同的"帽子”。
3.2 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': '我喜欢深度学习'}]
命名实体识别(NER)——序列标注实战
NER 是信息提取的基础任务:从文本中识别出人名、地名、组织名等实体。
from transformers import pipeline
# 使用预训练 NER 模型(一行代码)
ner = pipeline('ner', grouped_entities=True, model='dslim/bert-base-NER')
result = ner("George Washington went to Washington D.C. last Monday.")
# [{'entity_group': 'PER', 'word': 'George Washington', 'score': 0.99},
# {'entity_group': 'LOC', 'word': 'Washington D.C.', 'score': 0.98}]
# 中文 NER 示例
# 注意:bert-base-chinese 未经 NER 微调,效果较差。
# 生产环境应使用微调过的模型,如 ericje/bert-base-chinese-ner
ner_zh = pipeline('ner', grouped_entities=True, model='bert-base-chinese')
result_zh = ner_zh("小明在北京大学读书。")
NER 的工作原理:取 BERT 每个 token 的输出 → 加一个分类头 → 预测 BIO 标签(B-PER=人名开始,I-PER=人名延续,O=非实体)。
阅读理解——问答系统实战
给模型一个问题和一段文本,让它找出答案所在的片段:
from transformers import pipeline
qa = pipeline('question-answering', model='bert-large-uncased-whole-word-masking-finetuned-squad')
result = qa(
question="What is the capital of France?",
context="France is a country in Europe. Its capital is Paris, which is known for the Eiffel Tower."
)
# {'answer': 'Paris', 'score': 0.99, 'start': 45, 'end': 50}
问答系统的工作原理:BERT 同时编码问题和文本 → 预测答案的起始位置和结束位置 → 提取片段。
3.3 AI 写诗项目——综合实战
项目流程:
- 数据准备:收集古诗数据集(如唐诗三百首)
- 数据预处理:分词、构建词表、转为数字序列
- 模型选择:使用 GPT2-Chinese 进行微调
- 训练:在古诗数据上微调模型
- 生成:给定开头(如"春风”),让模型生成完整诗歌
关键代码:
from transformers import GPT2LMHeadModel, BertTokenizer
# uer/gpt2-chinese-poem 使用 BERT 词表训练,因此用 BertTokenizer
model = GPT2LMHeadModel.from_pretrained('uer/gpt2-chinese-poem')
3.4 阶段总结——核心知识地图
- Transformer = Self-Attention + FFN + 残差连接 + LayerNorm
- Self-Attention = $Q \cdot K^T / \sqrt{d_k}$ → softmax → $\cdot V$
- Multi-Head = 多组 Q/K/V 并行计算,从不同角度理解
- 位置编码 = 正弦/余弦函数(注入顺序信息)
- BERT = Transformer Encoder × N (双向,擅长理解)
- GPT = Transformer Decoder × N (单向,擅长生成)
- HuggingFace = Tokenizer + Datasets + Pipeline + Trainer
- 微调 = 预训练模型 + 下游任务分类头 + 少量标注数据
模块小结: NLP 实战与综合复习
| 你学到了什么 | 为什么重要 |
|---|---|
| NLP 任务全景 | 所有任务的区别在于任务头,而非模型本身 |
| 文本摘要 | 抽取式 vs 生成式 |
| 命名实体识别 | 从文本中提取结构化信息 |
| 问答系统 | 让模型"阅读理解"并定位答案 |
| 综合实战项目 | 串联所有知识的实践 |
3.5 初学者常见踩坑与解决方案
| 踩坑 | 症状 | 原因 | 解决方案 |
|---|---|---|---|
| 忘记设置 pad_token | GPT2 训练时报错 “pad_token not found” | GPT2 默认没有 pad_token | tokenizer.pad_token = tokenizer.eos_token |
| GPU 内存不足(OOM) | 训练时崩溃,报 CUDA out of memory | batch_size 太大或序列太长 | 减小 batch_size、缩短 max_length、使用 fp16=True 混合精度 |
| 学习率过大 | 训练 loss 不降反升,准确率 ~50% | 微调学习率应比从头训练小 100 倍 | 使用 2e-5 ~ 5e-5,配合 warmup_steps |
| 灾难性遗忘 | 微调后模型在通用任务上表现下降 | 学习率过大,破坏了预训练知识 | 使用小学习率 + weight_decay + 早停 |
| tokenizer 不匹配 | 模型输出全是乱码 | 用了错误的 tokenizer(如用英文 tokenizer 处理中文) | 确保 tokenizer 和模型来自同一个 from_pretrained() |
| 评估时忘记 model.eval() | 每次推理结果不同 | Dropout 和 BatchNorm 在训练和推理时行为不同 | 推理前调用 model.eval(),Trainer 自动处理 |
推荐补充资源
| 知识点 | 推荐资源 | 说明 |
|---|---|---|
| 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 文档 | 分词器详解 |
下一篇预告:本文掌握了预训练语言模型和 HuggingFace 实战。在速通 AI(五):现代大模型架构——LLaMA 系列中,你将看到 Transformer 如何演进为 LLaMA——开源大模型的事实标准,以及 RMSNorm、RoPE、SwiGLU、KV Cache、GQA 等关键改进。