BERT (Bidirectional Encoder Representations from Transformers) 是一个基于 Transformer 架构的预训练语言模型,它开启了 NLP 的预训练时代,是深度学习在自然语言处理领域的里程碑。
目录
1. BERT 概述
1.1 BERT 是什么?
一句话概括:
BERT 是一个基于 Transformer 架构的预训练语言模型。它的核心思想是,通过让模型在大量无标注文本上进行"自我学习",完成特定的预训练任务,从而获得对语言深层次的理解能力。然后,这个"学成归来"的模型可以被微调,以应对各种下游任务(如文本分类、问答、情感分析等),并取得极佳的效果。
形象比喻:
可以把 BERT 想象成一个阅读了互联网上海量文本的"语言通"。它不需要从零开始学习每一个新任务,而是带着已经具备的丰富语言知识,只需要稍加"点拨"(微调),就能成为特定领域的专家。
全称解释:
BERT = Bidirectional Encoder Representations from Transformers
中文:基于 Transformer 的双向编码器表示
1.2 BERT 的成功关键
BERT 的成功主要源于两个关键设计:
-
Transformer 的编码器:完全基于 Transformer 模型中的编码器部分,抛弃了传统的循环神经网络和卷积神经网络,完全依赖自注意力机制。
-
全新的预训练任务:采用双向的预训练任务,同时使用掩码语言模型(MLM)和下一句预测(NSP)两个任务来训练模型。
1.3 为什么 BERT 如此重要?
BERT 开启了 NLP 的预训练时代,是深度学习在自然语言处理领域的里程碑:
- 性能飞跃:在发布时,在 11 项 NLP 任务上刷新了纪录,取得了当时最好的性能
- 双向上下文理解:与之前的单向模型相比,BERT 的双向性使其对语境的理解更加深刻和准确
- 推动应用落地:由于预训练模型可以公开获取,企业和开发者可以轻松地将其应用于自己的业务中,极大地加速了 NLP 技术的产业化
- 确立新范式:BERT 的成功确立了"预训练 + 微调"作为 NLP 领域的新范式
2. 背景与动机
2.1 NLP的发展历程
传统方法的局限:
1. 词袋模型 (Bag of Words)
- 丢失词序信息
- 无法捕捉语义
2. Word2Vec / GloVe
- 静态词向量
- 无法处理一词多义
- 上下文无关
3. RNN / LSTM
- 难以并行化
- 长距离依赖问题
- 训练缓慢
4. ELMo (2018)
- 上下文相关词向量
- 但使用LSTM,效率低
2.2 为什么需要BERT?
BERT解决的核心问题:
- 双向上下文: 同时考虑左右上下文
- 预训练+微调: 大规模无监督预训练 → 下游任务微调
- 通用性: 一个模型适配多种NLP任务
- 迁移学习: 充分利用无标注数据
2.3 BERT的创新点
| 模型 | 上下文 | 预训练 | 参数规模 | 发布时间 |
|---|---|---|---|---|
| Word2Vec | 静态 | 无监督 | ~百万 | 2013 |
| ELMo | 双向(浅层) | 语言模型 | ~9400万 | 2018.02 |
| GPT | 单向(左→右) | 语言模型 | ~1.17亿 | 2018.06 |
| BERT | 真正双向 | MLM+NSP | ~3.4亿 | 2018.10 |
| GPT-2 | 单向 | 语言模型 | ~15亿 | 2019.02 |
BERT的突破:
- GLUE榜单:80.5% → 82.1% (+1.6%)
- SQuAD 1.1:93.2% → 93.6% F1
- 11个NLP任务刷新SOTA
3. 核心概念
3.1 基本概念
核心思想:
传统语言模型: P(w_t | w_1, ..., w_{t-1}) [单向]
BERT: 同时利用左右上下文理解每个词
3.2 模型规模
BERT-Base:
层数(L): 12
隐藏维度(H): 768
注意力头数(A): 12
参数量: 110M (1.1亿)
BERT-Large:
层数(L): 24
隐藏维度(H): 1024
注意力头数(A): 16
参数量: 340M (3.4亿)
3.3 输入表示
BERT 的输入被设计为可以同时处理单个句子或句子对。
BERT的输入由三部分相加组成:
最终输入 = Token Embeddings + Segment Embeddings + Position Embeddings
示例:
输入句子: [CLS] 我爱北京天安门 [SEP] 天安门真美 [SEP]
Token Embeddings: [我] [爱] [北] [京] ... (词嵌入)
Segment Embeddings: [A] [A] [A] [A] [A] [A] [B] [B] [B] [B] (区分句子)
Position Embeddings: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] (位置信息)
输入序列的构成:
- [CLS]:位于序列开头,该位置的输出向量通常被用作整个序列的聚合表示,用于分类任务
- [SEP]:用于分隔两个句子
- Token Embeddings:词嵌入
- Segment Embeddings:区分一个词属于第一个句子还是第二个句子
- Position Embeddings:表示每个词在序列中的位置信息
这三种嵌入相加,共同构成了 BERT 的输入。
特殊Token:
[CLS]: 分类标记,用于分类任务[SEP]: 句子分隔符[MASK]: 掩码标记,用于预训练[PAD]: 填充标记[UNK]: 未知词标记
4. 技术原理
4.1 Transformer Encoder
BERT使用的是Transformer的Encoder部分(不是Decoder)
单层Encoder结构:
输入
↓
Multi-Head Attention
↓
Add & Norm (残差连接+层归一化)
↓
Feed Forward Network
↓
Add & Norm
↓
输出
4.2 Multi-Head Attention
Self-Attention机制:
Query(Q) = X · W_Q
Key(K) = X · W_K
Value(V) = X · W_V
Attention(Q, K, V) = softmax(Q·K^T / √d_k) · V
自注意力机制的核心价值:
简单来说,它允许模型在处理一个词的时候,同时关注到输入序列中的所有其他词,并动态地计算每个词对当前词的重要性。这使得模型能够更好地理解上下文语境。
直观理解:
句子: "The animal didn't cross the street because it was too tired"
或中文: "苹果公司发布了新的手机,它非常畅销"
计算 "it"(或"它")的表示时:
- 与 "animal"(或"苹果公司")的attention权重: 0.6
- 与 "street"(或"手机")的attention权重: 0.1
- 与 "tired"(或"畅销")的attention权重: 0.2
→ "it" 更多地关注 "animal",理解为动物
→ "它" 需要同时看"苹果公司"和"手机"来理解指代
自注意力机制能很好地捕捉这种长距离依赖关系。
Multi-Head:
多个注意力头并行计算,捕捉不同角度的信息
Head_1: 语法关系
Head_2: 语义关系
Head_3: 共指关系
...
最终输出 = Concat(Head_1, ..., Head_h) · W_O
4.3 Position Encoding
为什么需要位置编码?
- Attention操作是置换不变的
- "我爱你" 和 "你爱我" 会得到相同的表示
- 需要位置信息
BERT使用可学习的位置编码:
python
# 最大序列长度512
position_embeddings = nn.Embedding(512, hidden_size)
# 每个位置有独立的embedding向量
pos_0, pos_1, pos_2, ..., pos_511
4.4 层归一化 (Layer Normalization)
公式:
LayerNorm(x) = γ · (x - μ) / √(σ² + ε) + β
其中:
- μ: 均值
- σ²: 方差
- γ, β: 可学习参数
作用:
- 稳定训练
- 加速收敛
- 缓解梯度消失
5. 模型架构
5.1 整体架构图
下游任务
↑
┌──────────┴──────────┐
│ │
[CLS]表示 Token表示
↑ ↑
┌───────┴───────┐ ┌──────┴──────┐
│ │ │ │
┌───────────────────────────────────────────┐
│ Transformer Encoder (×12/24) │
│ ┌─────────────────────────────────────┐ │
│ │ Multi-Head Self-Attention │ │
│ │ ↓ │ │
│ │ Add & Norm │ │
│ │ ↓ │ │
│ │ Feed Forward Network │ │
│ │ ↓ │ │
│ │ Add & Norm │ │
│ └─────────────────────────────────────┘ │
│ ×12 │
└───────────────────────────────────────────┘
↑
┌────────────┴────────────┐
│ │
Token Emb + Segment Emb + Position Emb
↑ ↑
┌───┴───┐ ┌────┴────┐
[CLS] [SEP] Token序列
5.2 详细参数
BERT-Base:
python
配置参数:
{
"hidden_size": 768, # 隐藏层维度
"num_hidden_layers": 12, # Transformer层数
"num_attention_heads": 12, # 注意力头数
"intermediate_size": 3072, # FFN中间层维度
"hidden_act": "gelu", # 激活函数
"hidden_dropout_prob": 0.1, # Dropout概率
"attention_probs_dropout_prob": 0.1,
"max_position_embeddings": 512, # 最大序列长度
"type_vocab_size": 2, # Segment类型数
"vocab_size": 30522 # 词表大小
}
5.3 前向传播流程
python
# 伪代码
def bert_forward(input_ids, segment_ids):
# 1. Embedding
token_emb = token_embedding(input_ids) # [batch, seq, 768]
segment_emb = segment_embedding(segment_ids)
position_emb = position_embedding(range(seq_len))
embeddings = token_emb + segment_emb + position_emb
embeddings = LayerNorm(embeddings)
embeddings = Dropout(embeddings)
# 2. Transformer Layers (×12)
hidden_states = embeddings
for layer in transformer_layers:
# Multi-Head Attention
attention_output = layer.attention(hidden_states)
attention_output = Dropout(attention_output)
hidden_states = LayerNorm(hidden_states + attention_output)
# Feed Forward
ff_output = layer.feed_forward(hidden_states)
ff_output = Dropout(ff_output)
hidden_states = LayerNorm(hidden_states + ff_output)
# 3. 输出
sequence_output = hidden_states # 所有token的表示
pooled_output = tanh(fc([CLS]表示)) # 整个序列的表示
return sequence_output, pooled_output
5.4 参数量计算
Embedding层:
Token Embedding: 30522 × 768 = 23,440,896
Segment Embedding: 2 × 768 = 1,536
Position Embedding: 512 × 768 = 393,216
Total: ~23.8M
单层Transformer:
Multi-Head Attention:
Q, K, V权重: 3 × (768 × 768) = 1,769,472
输出映射: 768 × 768 = 589,824
Feed Forward:
第一层: 768 × 3072 = 2,359,296
第二层: 3072 × 768 = 2,359,296
Layer Norm (×2): 2 × (768 × 2) = 3,072
单层Total: ~7.1M
BERT-Base总参数:
Embedding: 23.8M
12层Transformer: 12 × 7.1M = 85.2M
Pooler: 768 × 768 = 0.6M
Total: ~110M
6. 预训练任务
6.1 Masked Language Model (MLM)
核心思想: 随机掩盖输入词,预测被掩盖的词
a) 掩码语言模型(MLM)详解
做法:随机地将输入句子中 15% 的词语用 [MASK] 标记替换掉,然后训练模型去预测这些被遮盖的原始词语。
为什么重要:因为词语是被随机遮盖的,模型为了准确预测,必须结合该词左右两侧的上下文信息。这迫使模型学习真正的双向表征。
掩码策略:
原始句子: 我 爱 北 京 天 安 门
或: 我喜欢吃美味的苹果
随机选择15%的token进行掩码:
- 80%的概率: 替换为[MASK]
→ 我 爱 [MASK] 京 天 安 门
→ 我喜欢吃美味的 [MASK]
- 10%的概率: 替换为随机token
→ 我 爱 上 京 天 安 门
→ 我喜欢吃美味的 香蕉
- 10%的概率: 保持不变
→ 我 爱 北 京 天 安 门
→ 我喜欢吃美味的 苹果
目标: 预测被选中的token(这里是"北"或"苹果")
例子:
原始句子:"我喜欢吃美味的苹果"
输入 BERT:"我喜欢吃美味的 [MASK]"
模型任务:根据上下文 "我喜欢吃美味的" 和 "",预测 [MASK] 位置最可能是哪个词(如"苹果"、"香蕉"等)
为什么这样设计?
1. 80% [MASK]:
- 主要训练方式
- 让模型学习上下文信息
2. 10% 随机替换:
- 下游任务没有[MASK]
- 避免预训练-微调不一致
- 让模型更鲁棒
3. 10% 保持不变:
- 增加难度
- 让模型学习所有token的表示
损失函数:
L_MLM = -Σ log P(x_masked | x_unmasked)
只计算被掩码token的损失
6.2 Next Sentence Prediction (NSP)
核心思想: 预测两个句子是否连续
b) 下一句预测(NSP)详解
做法:在训练时,给模型输入两个句子 A 和 B,其中 50% 的情况下 B 是 A 的真实下一句,另外 50% 的情况下 B 是随机从语料库中抽取的无关句子。模型需要判断句子 B 是否是句子 A 的下一句。
为什么重要:许多自然语言处理任务(如问答、自然语言推理)都需要理解两个句子之间的关系。NSP 任务让模型学会了捕捉句子间的逻辑联系,而不仅仅是词与词之间的关系。
训练样本构造:
输入: [CLS] 句子A [SEP] 句子B [SEP]
标签: IsNext (1) 或 NotNext (0)
正样本 (50%):
句子A: 我今天很开心
句子B: 因为考试得了满分
→ IsNext
负样本 (50%):
句子A: 我今天很开心
句子B: 明天天气预报有雨 [随机从其他文档选择]
→ NotNext
损失函数:
L_NSP = -log P(IsNext | [CLS]表示)
使用[CLS]位置的输出做二分类
6.3 完整预训练目标
L = L_MLM + L_NSP
联合优化两个任务
革命性突破:
在 BERT 之前,预训练语言模型(如 GPT)通常采用自左向右或自右向左的单向语言模型。BERT 的核心突破在于它使用了双向的预训练任务,同时结合 MLM 和 NSP 两个任务,使模型能够真正理解双向上下文。
6.4 预训练数据
BERT原始论文使用的数据:
数据来源:在海量无标注文本上进行(如维基百科、图书语料库)
1. BooksCorpus / 图书语料库 (8亿词)
- 11,038本未出版书籍
2. English Wikipedia / 英文维基百科 (25亿词)
- 去除列表、表格、标题
- 只保留文本段落
总计: ~33亿词
中文BERT数据:
1. 中文Wikipedia
2. 百度百科
3. 新闻语料
4. 小说、评论等
总计: 通常5-10GB文本
6.5 预训练细节
训练配置:
python
# BERT-Base
batch_size = 256
max_seq_length = 512
learning_rate = 1e-4
warmup_steps = 10000
total_steps = 1,000,000
优化器: Adam (β1=0.9, β2=0.999, ε=1e-6)
学习率调度: Warmup + Linear Decay
训练时间:
- 4个Cloud TPU (16 TPU芯片)
- 训练4天
内存需求:
BERT-Base预训练:
- 模型参数: 110M × 4字节 = 440MB
- Adam优化器状态: 440MB × 3 = 1.32GB
- 梯度: 440MB
- 激活值: ~2GB (取决于batch size)
总计: ~4GB+ (单卡)
7. 微调方法
7.1 微调范式
这是 BERT 范式最核心的部分,也是它如此强大的原因。
预训练 vs 微调:
┌─────────────────────────────────────┐
│ 预训练 (Pre-training) │
│ - 大规模无标注数据 │
│ - MLM + NSP任务 │
│ - 学习通用语言表示 │
│ 数据:海量无标注文本(如维基百科、 │
│ 图书语料库) │
│ 任务:同时进行 MLM 和 NSP 两个任务 │
│ 目标:得到对通用语言知识有深刻 │
│ 理解的"基础模型" │
│ 成本:这个步骤计算成本极高,通常由 │
│ 大型研究机构或公司完成,但他 │
│ 们会将训练好的模型公开 │
└──────────────┬──────────────────────┘
↓
┌─────────────────────────────────────┐
│ 微调 (Fine-tuning) │
│ - 特定任务的标注数据 │
│ - 添加任务特定的输出层 │
│ - 端到端训练 │
│ 数据:特定的、有标注的下游任务数据 │
│ 做法:在预训练好的 BERT 模型后面 │
│ 接一个简单的输出层,然后用 │
│ 下游任务的数据对整个模型进行 │
│ 端到端的再训练 │
│ 优势:由于 BERT 已经具备了强大的 │
│ 语言能力,微调过程只需要相对 │
│ 较少的有标注数据和计算资源, │
│ 就能让模型在该任务上达到非常 │
│ 好的性能。这大大降低了应用先 │
│ 进 NLP 技术的门槛。 │
└─────────────────────────────────────┘
7.2 四大任务类型
1. 单句分类 (Single Sentence Classification)
任务: 情感分析、垃圾邮件检测
输入: [CLS] 这部电影真好看 [SEP]
输出: 使用[CLS]的表示 → 分类层
模型结构:
BERT输出的[CLS]
↓
Dense(768 → num_classes)
↓
Softmax
2. 句对分类 (Sentence Pair Classification)
任务: 自然语言推理(NLI)、问答匹配
输入: [CLS] 前提句 [SEP] 假设句 [SEP]
输出: [CLS]表示 → 分类
示例:
前提: 一个男人在踢足球
假设: 一个人在运动
标签: Entailment (蕴含)
3. 序列标注 (Sequence Labeling)
任务: 命名实体识别(NER)、词性标注
输入: [CLS] 我 爱 北 京 天 安 门 [SEP]
输出: 每个token的表示 → 标签
模型结构:
BERT输出的每个token表示
↓
Dense(768 → num_labels)
↓
Softmax
输出: O O B-LOC I-LOC I-LOC I-LOC I-LOC O
4. 抽取式问答 (Extractive QA)
任务: SQuAD、机器阅读理解
输入: [CLS] 问题 [SEP] 文章 [SEP]
输出: 预测答案的起始和结束位置
模型结构:
BERT输出的文章token表示
↓
两个线性层: start_logits, end_logits
↓
Softmax
答案: 文章[start : end+1]
7.3 微调技巧
学习率设置:
python
# 微调的学习率要远小于预训练
fine_tuning_lr = 2e-5 # 或 3e-5, 5e-5
# 不同层使用不同学习率 (层次化学习率)
optimizer = AdamW([
{'params': bert.embeddings.parameters(), 'lr': 1e-5},
{'params': bert.encoder.layer[:6].parameters(), 'lr': 2e-5},
{'params': bert.encoder.layer[6:].parameters(), 'lr': 3e-5},
{'params': classifier.parameters(), 'lr': 5e-5}
])
训练策略:
python
# 1. Warmup
warmup_steps = int(0.1 * total_steps)
# 2. 梯度裁剪
max_grad_norm = 1.0
# 3. Epoch数
epochs = 3-5 # 通常3-4个epoch即可
# 4. Batch size
batch_size = 16-32 # 根据显存调整
# 5. 早停
patience = 3
7.4 防止过拟合
技巧:
python
# 1. 数据增强
def augment_text(text):
# 同义词替换
# 随机删除
# 回译 (中文→英文→中文)
return augmented_text
# 2. Dropout
hidden_dropout = 0.1
attention_dropout = 0.1
# 3. 对抗训练
# 在embedding上添加扰动
epsilon = 1e-3
perturbation = epsilon * grad / grad.norm()
adv_embedding = embedding + perturbation
# 4. R-Drop
# 同一样本两次前向传播,约束输出一致性
loss_ce = (loss1 + loss2) / 2
loss_kl = KL(output1, output2) + KL(output2, output1)
loss = loss_ce + α * loss_kl
8. 代码实现
8.1 使用Hugging Face Transformers
安装:
bash
pip install transformers
pip install torch
加载预训练模型:
python
from transformers import BertTokenizer, BertModel
import torch
# 加载预训练模型和分词器
model_name = 'bert-base-chinese' # 或 'bert-base-uncased'
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name)
# 文本编码
text = "我爱自然语言处理"
inputs = tokenizer(
text,
return_tensors='pt',
padding=True,
truncation=True,
max_length=512
)
# 前向传播
with torch.no_grad():
outputs = model(**inputs)
# 获取输出
last_hidden_state = outputs.last_hidden_state # [batch, seq_len, 768]
pooler_output = outputs.pooler_output # [batch, 768] [CLS]表示
print(f"Token表示: {last_hidden_state.shape}")
print(f"[CLS]表示: {pooler_output.shape}")
8.2 文本分类任务
python
from transformers import BertForSequenceClassification, Trainer, TrainingArguments
from torch.utils.data import Dataset
import pandas as pd
# ============ 数据准备 ============
class TextClassificationDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_length=128):
self.encodings = tokenizer(
texts,
truncation=True,
padding=True,
max_length=max_length,
return_tensors='pt'
)
self.labels = torch.tensor(labels)
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
item = {key: val[idx] for key, val in self.encodings.items()}
item['labels'] = self.labels[idx]
return item
# 示例数据
train_texts = [
"这部电影真好看",
"太难看了,浪费时间",
"演员演技很棒",
"剧情无聊透了"
]
train_labels = [1, 0, 1, 0] # 1=正面, 0=负面
# 创建数据集
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
train_dataset = TextClassificationDataset(
train_texts, train_labels, tokenizer
)
# ============ 模型初始化 ============
model = BertForSequenceClassification.from_pretrained(
'bert-base-chinese',
num_labels=2 # 二分类
)
# ============ 训练配置 ============
training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=3,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
learning_rate=2e-5,
weight_decay=0.01,
warmup_steps=500,
logging_dir='./logs',
logging_steps=10,
save_strategy='epoch',
evaluation_strategy='epoch'
)
# ============ 训练器 ============
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
# eval_dataset=val_dataset, # 如果有验证集
)
# 开始训练
trainer.train()
# ============ 预测 ============
test_text = "这个电影还不错"
inputs = tokenizer(
test_text,
return_tensors='pt',
padding=True,
truncation=True
)
model.eval()
with torch.no_grad():
outputs = model(**inputs)
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
predicted_class = torch.argmax(predictions, dim=-1)
print(f"预测结果: {'正面' if predicted_class == 1 else '负面'}")
print(f"置信度: {predictions[0][predicted_class].item():.4f}")
8.3 命名实体识别 (NER)
python
from transformers import BertForTokenClassification
import torch
# ============ 标签定义 ============
label2id = {
'O': 0,
'B-PER': 1, 'I-PER': 2, # 人名
'B-LOC': 3, 'I-LOC': 4, # 地名
'B-ORG': 5, 'I-ORG': 6 # 机构名
}
id2label = {v: k for k, v in label2id.items()}
# ============ NER数据集 ============
class NERDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_length=128):
"""
Args:
texts: List[List[str]], 例: [['我', '爱', '北', '京'], ...]
labels: List[List[str]], 例: [['O', 'O', 'B-LOC', 'I-LOC'], ...]
"""
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
self.max_length = max_length
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
words = self.texts[idx]
labels = self.labels[idx]
# 分词(注意子词问题)
encoding = self.tokenizer(
words,
is_split_into_words=True,
truncation=True,
padding='max_length',
max_length=self.max_length,
return_tensors='pt'
)
# 对齐标签
word_ids = encoding.word_ids(0)
label_ids = []
for word_id in word_ids:
if word_id is None:
label_ids.append(-100) # 特殊token不计算损失
else:
label_ids.append(label2id[labels[word_id]])
encoding['labels'] = torch.tensor(label_ids)
return {k: v.squeeze(0) for k, v in encoding.items()}
# 示例数据
train_texts = [
['我', '爱', '北', '京', '天', '安', '门'],
['张', '三', '在', '上', '海', '工', '作']
]
train_labels = [
['O', 'O', 'B-LOC', 'I-LOC', 'I-LOC', 'I-LOC', 'I-LOC'],
['B-PER', 'I-PER', 'O', 'B-LOC', 'I-LOC', 'O', 'O']
]
# ============ 模型初始化 ============
model = BertForTokenClassification.from_pretrained(
'bert-base-chinese',
num_labels=len(label2id)
)
# ============ 训练 ============
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
train_dataset = NERDataset(train_texts, train_labels, tokenizer)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset
)
trainer.train()
# ============ 预测 ============
def predict_ner(text, model, tokenizer):
words = list(text)
inputs = tokenizer(
words,
is_split_into_words=True,
return_tensors='pt',
padding=True,
truncation=True
)
model.eval()
with torch.no_grad():
outputs = model(**inputs)
predictions = torch.argmax(outputs.logits, dim=-1)
# 解析结果
word_ids = inputs.word_ids(0)
results = []
for i, word_id in enumerate(word_ids):
if word_id is not None:
pred_label = id2label[predictions[0][i].item()]
results.append((words[word_id], pred_label))
return results
# 测试
text = "李四在北京大学学习"
entities = predict_ner(text, model, tokenizer)
print(entities)
8.4 问答系统 (SQuAD)
python
from transformers import BertForQuestionAnswering
# ============ 模型初始化 ============
model = BertForQuestionAnswering.from_pretrained('bert-base-chinese')
# ============ 问答函数 ============
def answer_question(question, context, model, tokenizer):
"""
抽取式问答
Args:
question: 问题
context: 文章/上下文
Returns:
answer: 答案文本
"""
# 编码
inputs = tokenizer(
question,
context,
return_tensors='pt',
truncation=True,
max_length=512
)
# 预测
model.eval()
with torch.no_grad():
outputs = model(**inputs)
# 获取答案位置
start_logits = outputs.start_logits
end_logits = outputs.end_logits
start_idx = torch.argmax(start_logits)
end_idx = torch.argmax(end_logits)
# 提取答案
answer_tokens = inputs['input_ids'][0][start_idx:end_idx+1]
answer = tokenizer.decode(answer_tokens, skip_special_tokens=True)
# 计算置信度
start_prob = torch.softmax(start_logits, dim=-1)[0][start_idx]
end_prob = torch.softmax(end_logits, dim=-1)[0][end_idx]
confidence = (start_prob * end_prob).item()
return answer, confidence
# 示例
question = "北京的首都是哪里?"
context = "北京是中华人民共和国的首都,也是全国的政治、文化中心。"
answer, conf = answer_question(question, context, model, tokenizer)
print(f"问题: {question}")
print(f"答案: {answer}")
print(f"置信度: {conf:.4f}")
8.5 从零实现BERT核心组件
python
import torch
import torch.nn as nn
import math
# ============ Multi-Head Attention ============
class MultiHeadAttention(nn.Module):
def __init__(self, hidden_size, num_heads, dropout=0.1):
super().__init__()
assert hidden_size % num_heads == 0
self.hidden_size = hidden_size
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
self.q_linear = nn.Linear(hidden_size, hidden_size)
self.k_linear = nn.Linear(hidden_size, hidden_size)
self.v_linear = nn.Linear(hidden_size, hidden_size)
self.out_linear = nn.Linear(hidden_size, hidden_size)
self.dropout = nn.Dropout(dropout)
def forward(self, query, key, value, mask=None):
batch_size = query.size(0)
# Linear projections
Q = self.q_linear(query) # [batch, seq, hidden]
K = self.k_linear(key)
V = self.v_linear(value)
# Split into multiple heads
Q = Q.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
K = K.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
V = V.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
# [batch, num_heads, seq, head_dim]
# Scaled dot-product attention
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attention = torch.softmax(scores, dim=-1)
attention = self.dropout(attention)
# Apply attention to values
context = torch.matmul(attention, V) # [batch, heads, seq, head_dim]
# Concatenate heads
context = context.transpose(1, 2).contiguous().view(
batch_size, -1, self.hidden_size
)
# Final linear projection
output = self.out_linear(context)
return output, attention
# ============ Feed Forward Network ============
class FeedForward(nn.Module):
def __init__(self, hidden_size, intermediate_size, dropout=0.1):
super().__init__()
self.fc1 = nn.Linear(hidden_size, intermediate_size)
self.fc2 = nn.Linear(intermediate_size, hidden_size)
self.gelu = nn.GELU()
self.dropout = nn.Dropout(dropout)
def forward(self, x):
x = self.fc1(x)
x = self.gelu(x)
x = self.dropout(x)
x = self.fc2(x)
x = self.dropout(x)
return x
# ============ Transformer Encoder Layer ============
class TransformerEncoderLayer(nn.Module):
def __init__(self, hidden_size, num_heads, intermediate_size, dropout=0.1):
super().__init__()
self.attention = MultiHeadAttention(hidden_size, num_heads, dropout)
self.feed_forward = FeedForward(hidden_size, intermediate_size, dropout)
self.norm1 = nn.LayerNorm(hidden_size)
self.norm2 = nn.LayerNorm(hidden_size)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None):
# Multi-Head Attention
attn_output, _ = self.attention(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_output))
# Feed Forward
ff_output = self.feed_forward(x)
x = self.norm2(x + self.dropout(ff_output))
return x
# ============ BERT Model ============
class SimpleBERT(nn.Module):
def __init__(self, vocab_size, hidden_size=768, num_layers=12,
num_heads=12, intermediate_size=3072, max_seq_len=512,
dropout=0.1):
super().__init__()
# Embeddings
self.token_embedding = nn.Embedding(vocab_size, hidden_size)
self.position_embedding = nn.Embedding(max_seq_len, hidden_size)
self.segment_embedding = nn.Embedding(2, hidden_size)
self.norm = nn.LayerNorm(hidden_size)
self.dropout = nn.Dropout(dropout)
# Transformer Layers
self.layers = nn.ModuleList([
TransformerEncoderLayer(hidden_size, num_heads, intermediate_size, dropout)
for _ in range(num_layers)
])
# Pooler
self.pooler = nn.Linear(hidden_size, hidden_size)
self.pooler_activation = nn.Tanh()
def forward(self, input_ids, segment_ids=None, attention_mask=None):
batch_size, seq_len = input_ids.size()
# Position IDs
position_ids = torch.arange(seq_len, dtype=torch.long, device=input_ids.device)
position_ids = position_ids.unsqueeze(0).expand_as(input_ids)
# Segment IDs
if segment_ids is None:
segment_ids = torch.zeros_like(input_ids)
# Embeddings
token_emb = self.token_embedding(input_ids)
position_emb = self.position_embedding(position_ids)
segment_emb = self.segment_embedding(segment_ids)
embeddings = token_emb + position_emb + segment_emb
embeddings = self.norm(embeddings)
embeddings = self.dropout(embeddings)
# Attention mask
if attention_mask is not None:
attention_mask = attention_mask.unsqueeze(1).unsqueeze(2)
# Transformer Layers
hidden_states = embeddings
for layer in self.layers:
hidden_states = layer(hidden_states, attention_mask)
# Pooler
pooled_output = self.pooler(hidden_states[:, 0]) # [CLS] token
pooled_output = self.pooler_activation(pooled_output)
return hidden_states, pooled_output
# ============ 使用示例 ============
vocab_size = 30522
model = SimpleBERT(vocab_size)
# 示例输入
input_ids = torch.randint(0, vocab_size, (2, 128)) # [batch=2, seq=128]
segment_ids = torch.zeros(2, 128, dtype=torch.long)
attention_mask = torch.ones(2, 128)
sequence_output, pooled_output = model(input_ids, segment_ids, attention_mask)
print(f"Sequence output: {sequence_output.shape}") # [2, 128, 768]
print(f"Pooled output: {pooled_output.shape}") # [2, 768]
9. 实战应用
9.1 情感分析完整流程
python
import torch
from transformers import BertTokenizer, BertForSequenceClassification
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score, classification_report
import pandas as pd
# ============ 1. 数据加载 ============
def load_data(file_path):
"""
假设CSV格式: text, label
"""
df = pd.read_csv(file_path)
texts = df['text'].tolist()
labels = df['label'].tolist()
return texts, labels
# ============ 2. 数据集类 ============
class SentimentDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_length=128):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
self.max_length = max_length
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
text = str(self.texts[idx])
label = self.labels[idx]
encoding = self.tokenizer(
text,
add_special_tokens=True,
max_length=self.max_length,
padding='max_length',
truncation=True,
return_attention_mask=True,
return_tensors='pt'
)
return {
'input_ids': encoding['input_ids'].flatten(),
'attention_mask': encoding['attention_mask'].flatten(),
'labels': torch.tensor(label, dtype=torch.long)
}
# ============ 3. 训练函数 ============
def train_epoch(model, data_loader, optimizer, device, scheduler=None):
model.train()
total_loss = 0
for batch in data_loader:
optimizer.zero_grad()
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels
)
loss = outputs.loss
total_loss += loss.item()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
if scheduler:
scheduler.step()
return total_loss / len(data_loader)
# ============ 4. 评估函数 ============
def eval_model(model, data_loader, device):
model.eval()
predictions = []
true_labels = []
with torch.no_grad():
for batch in data_loader:
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask
)
preds = torch.argmax(outputs.logits, dim=1)
predictions.extend(preds.cpu().numpy())
true_labels.extend(labels.cpu().numpy())
accuracy = accuracy_score(true_labels, predictions)
f1 = f1_score(true_labels, predictions, average='weighted')
return accuracy, f1, predictions, true_labels
# ============ 5. 主训练流程 ============
def main():
# 配置
MODEL_NAME = 'bert-base-chinese'
MAX_LENGTH = 128
BATCH_SIZE = 32
EPOCHS = 5
LEARNING_RATE = 2e-5
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 加载数据
texts, labels = load_data('sentiment_data.csv')
# 划分数据集
train_texts, val_texts, train_labels, val_labels = train_test_split(
texts, labels, test_size=0.2, random_state=42
)
# 加载tokenizer和模型
tokenizer = BertTokenizer.from_pretrained(MODEL_NAME)
model = BertForSequenceClassification.from_pretrained(
MODEL_NAME,
num_labels=3 # 假设3分类: 负面、中性、正面
)
model.to(device)
# 创建数据集和数据加载器
train_dataset = SentimentDataset(train_texts, train_labels, tokenizer, MAX_LENGTH)
val_dataset = SentimentDataset(val_texts, val_labels, tokenizer, MAX_LENGTH)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE)
# 优化器和调度器
optimizer = torch.optim.AdamW(model.parameters(), lr=LEARNING_RATE)
total_steps = len(train_loader) * EPOCHS
scheduler = torch.optim.lr_scheduler.LinearLR(
optimizer,
start_factor=0.1,
total_iters=int(0.1 * total_steps)
)
# 训练循环
best_acc = 0
for epoch in range(EPOCHS):
print(f'\nEpoch {epoch + 1}/{EPOCHS}')
print('-' * 50)
# 训练
train_loss = train_epoch(model, train_loader, optimizer, device, scheduler)
print(f'Train Loss: {train_loss:.4f}')
# 验证
val_acc, val_f1, _, _ = eval_model(model, val_loader, device)
print(f'Val Accuracy: {val_acc:.4f}')
print(f'Val F1: {val_f1:.4f}')
# 保存最佳模型
if val_acc > best_acc:
best_acc = val_acc
torch.save(model.state_dict(), 'best_model.pth')
print(f'Best model saved! (Acc: {best_acc:.4f})')
# 加载最佳模型
model.load_state_dict(torch.load('best_model.pth'))
# 最终评估
val_acc, val_f1, predictions, true_labels = eval_model(model, val_loader, device)
print('\n' + '='*50)
print('Final Results:')
print(f'Accuracy: {val_acc:.4f}')
print(f'F1 Score: {val_f1:.4f}')
print('\nClassification Report:')
print(classification_report(true_labels, predictions))
return model, tokenizer
# ============ 6. 推理函数 ============
def predict(text, model, tokenizer, device):
model.eval()
encoding = tokenizer(
text,
add_special_tokens=True,
max_length=128,
padding='max_length',
truncation=True,
return_attention_mask=True,
return_tensors='pt'
)
input_ids = encoding['input_ids'].to(device)
attention_mask = encoding['attention_mask'].to(device)
with torch.no_grad():
outputs = model(input_ids=input_ids, attention_mask=attention_mask)
probs = torch.softmax(outputs.logits, dim=1)
pred_class = torch.argmax(probs, dim=1).item()
confidence = probs[0][pred_class].item()
label_map = {0: '负面', 1: '中性', 2: '正面'}
return label_map[pred_class], confidence
# 运行
if __name__ == '__main__':
model, tokenizer = main()
# 测试预测
test_texts = [
"这个产品质量太差了,非常失望",
"价格合理,性价比不错",
"超级棒!强烈推荐!"
]
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
for text in test_texts:
sentiment, conf = predict(text, model, tokenizer, device)
print(f"文本: {text}")
print(f"情感: {sentiment} (置信度: {conf:.4f})\n")
9.2 文本相似度计算
python
import torch
from transformers import BertTokenizer, BertModel
from scipy.spatial.distance import cosine
class BERTSimilarity:
def __init__(self, model_name='bert-base-chinese'):
self.tokenizer = BertTokenizer.from_pretrained(model_name)
self.model = BertModel.from_pretrained(model_name)
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
self.model.to(self.device)
self.model.eval()
def get_embedding(self, text):
"""获取文本的BERT表示"""
inputs = self.tokenizer(
text,
return_tensors='pt',
padding=True,
truncation=True,
max_length=512
).to(self.device)
with torch.no_grad():
outputs = self.model(**inputs)
# 方法1: 使用[CLS]表示
cls_embedding = outputs.pooler_output
# 方法2: 使用所有token的平均
# attention_mask = inputs['attention_mask']
# token_embeddings = outputs.last_hidden_state
# mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
# sum_embeddings = torch.sum(token_embeddings * mask_expanded, 1)
# sum_mask = torch.clamp(mask_expanded.sum(1), min=1e-9)
# mean_embedding = sum_embeddings / sum_mask
return cls_embedding.cpu().numpy()[0]
def compute_similarity(self, text1, text2):
"""计算两个文本的余弦相似度"""
emb1 = self.get_embedding(text1)
emb2 = self.get_embedding(text2)
# 余弦相似度
similarity = 1 - cosine(emb1, emb2)
return similarity
def batch_similarity(self, query, candidates):
"""批量计算query与多个候选文本的相似度"""
query_emb = self.get_embedding(query)
similarities = []
for candidate in candidates:
cand_emb = self.get_embedding(candidate)
sim = 1 - cosine(query_emb, cand_emb)
similarities.append(sim)
return similarities
# 使用示例
similarity_model = BERTSimilarity()
# 示例1: 两两相似度
text1 = "我喜欢看电影"
text2 = "我爱看电影"
text3 = "今天天气很好"
sim_12 = similarity_model.compute_similarity(text1, text2)
sim_13 = similarity_model.compute_similarity(text1, text3)
print(f"'{text1}' vs '{text2}': {sim_12:.4f}")
print(f"'{text1}' vs '{text3}': {sim_13:.4f}")
# 示例2: 语义搜索
query = "如何学习人工智能"
documents = [
"人工智能学习指南",
"机器学习入门教程",
"深度学习实战",
"今天吃什么好呢",
"Python编程基础"
]
scores = similarity_model.batch_similarity(query, documents)
# 排序
ranked = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True)
print(f"\n查询: {query}")
print("相关文档排序:")
for doc, score in ranked:
print(f" {doc}: {score:.4f}")
9.3 文本生成 (基于BERT的MLM)
python
from transformers import BertTokenizer, BertForMaskedLM
import torch
class BERTTextGenerator:
def __init__(self, model_name='bert-base-chinese'):
self.tokenizer = BertTokenizer.from_pretrained(model_name)
self.model = BertForMaskedLM.from_pretrained(model_name)
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
self.model.to(self.device)
self.model.eval()
def fill_mask(self, text, top_k=5):
"""
填充[MASK]位置的词
Args:
text: 包含[MASK]的文本
top_k: 返回top-k个预测
Returns:
List of (token, probability)
"""
inputs = self.tokenizer(text, return_tensors='pt').to(self.device)
# 找到[MASK]的位置
mask_token_index = torch.where(
inputs['input_ids'] == self.tokenizer.mask_token_id
)[1]
with torch.no_grad():
outputs = self.model(**inputs)
predictions = outputs.logits
# 获取[MASK]位置的预测
mask_predictions = predictions[0, mask_token_index]
probs = torch.softmax(mask_predictions, dim=-1)
# Top-k预测
top_k_weights, top_k_indices = torch.topk(probs, top_k, dim=-1, sorted=True)
results = []
for i in range(top_k):
token = self.tokenizer.decode([top_k_indices[0][i]])
prob = top_k_weights[0][i].item()
results.append((token, prob))
return results
def generate_sentence(self, prompt, max_length=50):
"""
基于提示生成句子(贪婪解码)
注意: BERT不是生成式模型,这种方法效果有限
"""
tokens = self.tokenizer.tokenize(prompt)
for _ in range(max_length - len(tokens)):
# 在末尾添加[MASK]
text = ' '.join(tokens + ['[MASK]'])
# 预测[MASK]
predictions = self.fill_mask(text, top_k=1)
next_token = predictions[0][0].strip()
# 添加预测的token
tokens.append(next_token)
# 如果生成了句号,停止
if next_token in ['。', '.', '!', '?']:
break
return ''.join(tokens)
# 使用示例
generator = BERTTextGenerator()
# 示例1: 填空
text = "今天天气[MASK]好"
predictions = generator.fill_mask(text, top_k=5)
print(f"原文: {text}")
print("预测:")
for token, prob in predictions:
filled_text = text.replace('[MASK]', token)
print(f" {filled_text} (概率: {prob:.4f})")
# 示例2: 多个[MASK]
text = "我[MASK]欢[MASK]电影"
print(f"\n原文: {text}")
# 需要迭代填充每个[MASK]
# 示例3: 句子续写
prompt = "人工智能"
generated = generator.generate_sentence(prompt, max_length=20)
print(f"\n提示: {prompt}")
print(f"生成: {generated}")
10. 调优技巧
10.1 学习率调优
学习率范围:
python
# 不同任务的推荐学习率
learning_rates = {
'大数据集': 2e-5,
'中等数据集': 3e-5,
'小数据集': 5e-5
}
# 层次化学习率(推荐)
def get_optimizer_grouped_parameters(model, learning_rate):
no_decay = ['bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
# Embedding层
{
'params': [p for n, p in model.bert.embeddings.named_parameters()
if not any(nd in n for nd in no_decay)],
'weight_decay': 0.01,
'lr': learning_rate * 0.5
},
# 底层Transformer (0-6)
{
'params': [p for n, p in model.bert.encoder.layer[:6].named_parameters()
if not any(nd in n for nd in no_decay)],
'weight_decay': 0.01,
'lr': learning_rate * 0.7
},
# 高层Transformer (7-11)
{
'params': [p for n, p in model.bert.encoder.layer[6:].named_parameters()
if not any(nd in n for nd in no_decay)],
'weight_decay': 0.01,
'lr': learning_rate
},
# 分类器
{
'params': [p for n, p in model.classifier.named_parameters()
if not any(nd in n for nd in no_decay)],
'weight_decay': 0.01,
'lr': learning_rate * 2
},
# 不衰减的参数
{
'params': [p for n, p in model.named_parameters()
if any(nd in n for nd in no_decay)],
'weight_decay': 0.0,
'lr': learning_rate
}
]
return optimizer_grouped_parameters
# 使用
params = get_optimizer_grouped_parameters(model, learning_rate=3e-5)
optimizer = AdamW(params)
学习率调度器:
python
from transformers import get_scheduler
# 1. Linear Warmup + Linear Decay (推荐)
scheduler = get_scheduler(
name="linear",
optimizer=optimizer,
num_warmup_steps=int(0.1 * total_steps),
num_training_steps=total_steps
)
# 2. Cosine Warmup + Cosine Decay
scheduler = get_scheduler(
name="cosine",
optimizer=optimizer,
num_warmup_steps=int(0.1 * total_steps),
num_training_steps=total_steps
)
# 3. 自定义: Warmup + 常数
class WarmupConstantSchedule(torch.optim.lr_scheduler.LambdaLR):
def __init__(self, optimizer, warmup_steps):
def lr_lambda(step):
if step < warmup_steps:
return float(step) / float(max(1.0, warmup_steps))
return 1.0
super().__init__(optimizer, lr_lambda)
10.2 防止过拟合
1. Dropout调优:
python
config = BertConfig.from_pretrained('bert-base-chinese')
# 调整dropout比例
config.hidden_dropout_prob = 0.2 # 默认0.1
config.attention_probs_dropout_prob = 0.2
model = BertForSequenceClassification(config)
2. 数据增强:
python
import nlpaug.augmenter.word as naw
import nlpaug.augmenter.sentence as nas
# 同义词替换
aug_synonym = naw.SynonymAug(aug_src='wordnet')
augmented_text = aug_synonym.augment(text)
# 回译增强 (Translation Augmentation)
aug_back_translation = naw.BackTranslationAug(
from_model_name='Helsinki-NLP/opus-mt-zh-en',
to_model_name='Helsinki-NLP/opus-mt-en-zh'
)
augmented_text = aug_back_translation.augment(text)
# 上下文词插入
aug_insert = naw.ContextualWordEmbsAug(
model_path='bert-base-chinese',
action="insert"
)
augmented_text = aug_insert.augment(text)
3. 对抗训练 (FGM):
python
class FGM:
"""Fast Gradient Method"""
def __init__(self, model, epsilon=1.0):
self.model = model
self.epsilon = epsilon
self.backup = {}
def attack(self, emb_name='word_embeddings'):
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
self.backup[name] = param.data.clone()
norm = torch.norm(param.grad)
if norm != 0 and not torch.isnan(norm):
r_at = self.epsilon * param.grad / norm
param.data.add_(r_at)
def restore(self, emb_name='word_embeddings'):
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
assert name in self.backup
param.data = self.backup[name]
self.backup = {}
# 训练时使用
fgm = FGM(model)
for batch in train_loader:
# 正常训练
loss = model(**batch).loss
loss.backward()
# 对抗训练
fgm.attack()
loss_adv = model(**batch).loss
loss_adv.backward()
fgm.restore()
optimizer.step()
optimizer.zero_grad()
4. R-Drop:
python
import torch.nn.functional as F
def compute_kl_loss(p, q, pad_mask=None):
p_loss = F.kl_div(F.log_softmax(p, dim=-1), F.softmax(q, dim=-1), reduction='none')
q_loss = F.kl_div(F.log_softmax(q, dim=-1), F.softmax(p, dim=-1), reduction='none')
if pad_mask is not None:
p_loss.masked_fill_(pad_mask, 0.)
q_loss.masked_fill_(pad_mask, 0.)
p_loss = p_loss.sum()
q_loss = q_loss.sum()
loss = (p_loss + q_loss) / 2
return loss
# 训练时
for batch in train_loader:
# 两次前向传播
logits1 = model(**batch).logits
logits2 = model(**batch).logits
# 交叉熵损失
ce_loss = 0.5 * (
F.cross_entropy(logits1, labels) +
F.cross_entropy(logits2, labels)
)
# KL散度损失
kl_loss = compute_kl_loss(logits1, logits2)
# 总损失
loss = ce_loss + 0.5 * kl_loss
loss.backward()
optimizer.step()
10.3 模型压缩
1. 知识蒸馏:
python
class DistillationTrainer:
def __init__(self, teacher_model, student_model, temperature=3.0, alpha=0.5):
self.teacher = teacher_model
self.student = student_model
self.temperature = temperature
self.alpha = alpha
self.teacher.eval()
for param in self.teacher.parameters():
param.requires_grad = False
def distillation_loss(self, student_logits, teacher_logits, labels):
# Hard loss (学生与真实标签)
hard_loss = F.cross_entropy(student_logits, labels)
# Soft loss (学生与教师)
soft_loss = F.kl_div(
F.log_softmax(student_logits / self.temperature, dim=-1),
F.softmax(teacher_logits / self.temperature, dim=-1),
reduction='batchmean'
) * (self.temperature ** 2)
# 组合
loss = self.alpha * hard_loss + (1 - self.alpha) * soft_loss
return loss
def train_step(self, batch, optimizer):
# 教师预测
with torch.no_grad():
teacher_outputs = self.teacher(**batch)
teacher_logits = teacher_outputs.logits
# 学生预测
student_outputs = self.student(**batch)
student_logits = student_outputs.logits
# 蒸馏损失
loss = self.distillation_loss(
student_logits,
teacher_logits,
batch['labels']
)
loss.backward()
optimizer.step()
optimizer.zero_grad()
return loss.item()
# 使用
teacher = BertForSequenceClassification.from_pretrained('bert-large-chinese')
student = BertForSequenceClassification.from_pretrained('bert-base-chinese')
trainer = DistillationTrainer(teacher, student)
# 训练...
2. 量化:
python
import torch.quantization as quantization
# 动态量化(推理时)
model = BertForSequenceClassification.from_pretrained('bert-base-chinese')
quantized_model = torch.quantization.quantize_dynamic(
model,
{torch.nn.Linear},
dtype=torch.qint8
)
# 模型大小减少约4倍,推理速度提升2-3倍
torch.save(quantized_model.state_dict(), 'quantized_model.pth')
3. 剪枝:
python
from transformers import BertForSequenceClassification
import torch.nn.utils.prune as prune
model = BertForSequenceClassification.from_pretrained('bert-base-chinese')
# 结构化剪枝: 移除整个注意力头
def prune_attention_heads(model, heads_to_prune):
"""
heads_to_prune: {layer_idx: [head_indices]}
"""
for layer_idx, heads in heads_to_prune.items():
layer = model.bert.encoder.layer[layer_idx].attention.self
# 剪枝逻辑...
# 非结构化剪枝: 权重置零
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
prune.l1_unstructured(module, name='weight', amount=0.3) # 剪枝30%
# 使剪枝永久化
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
prune.remove(module, 'weight')
10.4 长文本处理
1. 滑动窗口:
python
def sliding_window_inference(text, model, tokenizer, window_size=510, stride=256):
"""
处理超长文本
Args:
text: 长文本
window_size: 窗口大小 (512 - 2 for [CLS] and [SEP])
stride: 步长
"""
tokens = tokenizer.tokenize(text)
if len(tokens) <= window_size:
# 短文本直接处理
inputs = tokenizer(text, return_tensors='pt', truncation=True)
return model(**inputs)
# 滑动窗口
all_logits = []
for i in range(0, len(tokens), stride):
window_tokens = tokens[i:i+window_size]
window_text = tokenizer.convert_tokens_to_string(window_tokens)
inputs = tokenizer(window_text, return_tensors='pt')
outputs = model(**inputs)
all_logits.append(outputs.logits)
if i + window_size >= len(tokens):
break
# 聚合结果
aggregated_logits = torch.mean(torch.stack(all_logits), dim=0)
return aggregated_logits
2. 层次化BERT:
python
class HierarchicalBERT(nn.Module):
"""
将长文本分成多个段落,先对每个段落编码,再聚合
"""
def __init__(self, bert_model, num_labels):
super().__init__()
self.bert = bert_model
self.pooler = nn.LSTM(768, 384, bidirectional=True, batch_first=True)
self.classifier = nn.Linear(768, num_labels)
def forward(self, input_ids_segments, attention_mask_segments):
"""
Args:
input_ids_segments: [batch, num_segments, seq_len]
attention_mask_segments: [batch, num_segments, seq_len]
"""
batch_size, num_segments, seq_len = input_ids_segments.size()
# 对每个段落编码
segment_embeddings = []
for i in range(num_segments):
outputs = self.bert(
input_ids=input_ids_segments[:, i, :],
attention_mask=attention_mask_segments[:, i, :]
)
segment_embeddings.append(outputs.pooler_output)
# [batch, num_segments, 768]
segment_embeddings = torch.stack(segment_embeddings, dim=1)
# LSTM聚合
lstm_out, _ = self.pooler(segment_embeddings)
# 分类
logits = self.classifier(lstm_out[:, -1, :])
return logits
11. 优缺点分析
11.1 优点
1. 双向上下文理解
传统LM: The animal didn't cross the street because it was too ___
↓ 只能看左边
单向信息
BERT: The animal didn't cross the street because it was too ___
↑←───────────────────────────────────────────────→↑
双向信息,更准确理解"it"指代"animal"
2. 预训练+微调范式
- 大规模无监督预训练: 学习通用语言知识
- 少量标注数据微调: 快速适配下游任务
- 迁移学习效果显著
3. 通用性强
一个BERT模型 + 不同输出层 = 多种NLP任务
- 分类: + Linear层
- NER: + CRF层
- QA: + 起始/结束预测层
- 相似度: Siamese BERT
4. 效果显著
BERT发布后的提升:
- GLUE: +7% 平均分
- SQuAD: +1.5 F1
- CoNLL-2003 NER: +1 F1
11.2 缺点
尽管非常强大,BERT 也有其局限性:
1. 计算资源需求大
预训练成本:
- BERT-Base: 4天 × 4 Cloud TPU = 16 TPU天
- BERT-Large: 4天 × 16 Cloud TPU = 64 TPU天
- 估算成本: $数千 - $数万
推理成本:
- 单次推理: 50-100ms (CPU)
- 内存占用: ~400MB (BERT-Base)
预训练和微调(尤其是大型模型)仍然需要可观的算力。
2. 最大长度限制
最大序列长度: 512 tokens
- 无法处理长文档
- 需要截断或滑动窗口
- 信息可能丢失
由于 Transformer 的自注意力机制计算复杂度随序列长度平方级增长,
BERT 通常有最大输入长度限制(如 512 个 token)。
3. 预训练-微调不一致
预训练: [MASK]标记
微调: 没有[MASK]
→ 导致分布偏移
在微调阶段,输入数据中不会出现 [MASK] 标记,
这导致预训练和微调之间存在一定的差异。
改进:
- ELECTRA: 判别式预训练
- ALBERT: SOP替代NSP
4. 无法生成文本
BERT是Encoder-only:
- 适合理解任务
- 不适合生成任务
BERT 是编码器模型,擅长"理解"和"分析",
但不能像 GPT 那样的解码器模型一样"生成"连贯的文本。
生成需要:
- GPT系列 (Decoder-only)
- T5/BART (Encoder-Decoder)
11.3 与其他模型对比
| 模型 | 类型 | 上下文 | 参数量 | 优势 | 劣势 |
|---|---|---|---|---|---|
| BERT | Encoder | 双向 | 110M/340M | 理解任务强 | 无法生成 |
| GPT-2 | Decoder | 单向 | 1.5B | 生成流畅 | 理解不如BERT |
| RoBERTa | Encoder | 双向 | 125M/355M | 改进训练策略 | 成本更高 |
| ALBERT | Encoder | 双向 | 12M/235M | 参数高效 | 推理稍慢 |
| ELECTRA | Encoder | 双向 | 110M | 样本效率高 | 训练复杂 |
| T5 | Enc-Dec | 双向 | 220M/11B | 统一框架 | 参数量大 |
12. 进阶话题
12.1 BERT变体
1. RoBERTa (2019.07)
改进点:
1. 去除NSP任务
2. 动态Masking
3. 更大batch size
4. 更多训练数据
5. 更长训练时间
效果: GLUE +1-2%
2. ALBERT (2019.09)
创新:
1. 参数共享 (跨层)
- 大幅减少参数
2. Factorized Embedding
- Embedding维度解耦
- V×E → V×e + e×E
3. SOP (Sentence Order Prediction)
- 替代NSP
- 判断句子顺序是否正确
效果: 参数减少18倍,性能不降反升
3. ELECTRA (2020.03)
创新: 判别式预训练
- 生成器: 小BERT,生成替换词
- 判别器: BERT,判断每个词是否被替换
训练目标:
预测每个token是原始的还是被替换的
优势:
- 所有token都有训练信号 (vs BERT只有15%)
- 样本效率更高
- 小模型也能达到好效果
效果: 相同算力下性能更优
4. ERNIE (百度, 2019)
创新: 知识增强
1. Entity-level Masking
- 不是随机mask单词
- mask整个实体: [北京][大学]
2. Phrase-level Masking
- mask短语: [take care of]
3. 知识图谱融合
- 注入外部知识
效果: 中文NLP任务领先
12.2 领域适应
继续预训练 (Domain-Adaptive Pretraining):
python
from transformers import BertForMaskedLM, DataCollatorForLanguageModeling
# 1. 加载通用BERT
model = BertForMaskedLM.from_pretrained('bert-base-chinese')
# 2. 准备领域数据 (医疗、法律、金融等)
domain_texts = [...] # 医疗文本
# 3. 继续MLM预训练
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=True,
mlm_probability=0.15
)
trainer = Trainer(
model=model,
args=training_args,
data_collator=data_collator,
train_dataset=domain_dataset
)
trainer.train()
# 4. 保存领域BERT
model.save_pretrained('bert-medical')
任务自适应预训练 (Task-Adaptive Pretraining):
python
# 在目标任务的无标注数据上继续预训练
# 例如: 情感分析任务
# 1. 收集大量相关领域无标注文本
# 2. MLM预训练
# 3. 微调分类任务
12.3 多模态BERT
ViLBERT (Vision-and-Language BERT):
架构:
- 视觉流: 处理图像特征
- 语言流: 处理文本
- 跨模态注意力: 融合视觉和语言
应用:
- 图像描述
- 视觉问答
- 视觉推理
LXMERT (Learning Cross-Modality Encoder Representations):
三个Encoder:
1. 对象关系编码器 (图像)
2. 语言编码器 (文本)
3. 跨模态编码器 (融合)
预训练任务:
- MLM
- 对象标签预测
- 图像-文本匹配
12.4 轻量化BERT
DistilBERT:
方法: 知识蒸馏
- 6层 (vs BERT 12层)
- 参数减少40%
- 速度提升60%
- 性能保留97%
适用场景:
- 移动端部署
- 实时推理
- 资源受限环境
TinyBERT:
两阶段蒸馏:
1. 通用蒸馏 (预训练阶段)
2. 任务蒸馏 (微调阶段)
蒸馏目标:
- Embedding层
- Attention矩阵
- Hidden states
- 预测logits
效果: 参数减少7.5倍,速度提升9倍
MobileBERT:
设计原则:
- 瓶颈结构
- 平衡模型宽度和深度
- 优化移动设备
特点:
- 24层但窄
- 推理优化
- 移动端友好
12.5 长文本BERT
Longformer:
创新: 稀疏注意力
- 局部窗口注意力: 关注邻近token
- 全局注意力: [CLS]等特殊token
- 扩展注意力: 部分token全局关注
最大长度: 4096 tokens
复杂度: O(n) vs BERT O(n²)
BigBird:
稀疏注意力机制:
1. 随机注意力
2. 窗口注意力
3. 全局注意力
理论保证: 是图灵完备的
最大长度: 4096+
Hierarchical BERT:
层次化处理:
1. 段落级BERT
2. 文档级聚合 (LSTM/Transformer)
适用: 文档分类、摘要
12.6 最新进展
BERT → GPT-3 → ChatGPT时代:
2018: BERT (双向理解)
2020: GPT-3 (大规模生成)
2022: ChatGPT (指令微调)
2023: GPT-4 (多模态)
趋势:
- 模型规模持续增大
- 从理解到生成
- 从单模态到多模态
- 从微调到prompt
Prompt-based Learning:
python
# 传统微调
输入: "这部电影真好看"
输出: label=1 (正面)
# Prompt-based
输入: "这部电影真好看。总的来说,这部电影[MASK]。"
输出: [MASK] = "很好" → 正面
[MASK] = "很差" → 负面
优势:
- 更少标注数据
- 更接近预训练任务
- Few-shot学习
13. 总结与展望
13.1 核心要点回顾
-
BERT = Transformer Encoder + MLM + NSP
- 双向上下文建模
- 预训练+微调范式
- 通用NLP基座模型
-
关键技术
- Multi-Head Self-Attention
- 三种Embedding相加
- 层归一化和残差连接
-
应用范式
- 单句分类: [CLS] + 分类层
- 句对分类: [CLS] 句子A [SEP] 句子B [SEP]
- 序列标注: 每个token + 标注层
- 问答: 预测答案起始/结束位置
-
调优关键
- 学习率: 2e-5 - 5e-5
- Epoch: 3-5
- Warmup: 10% steps
- 防止过拟合: Dropout、对抗训练、R-Drop
13.2 学习路径建议
第1周: BERT原理
- Transformer基础
- Self-Attention机制
- BERT论文精读
第2周: 代码实践
- Hugging Face使用
- 文本分类任务
- 理解每个组件
第3周: 进阶应用
- NER、QA任务
- 多任务学习
- 模型调优
第4周: 生产部署
- 模型压缩
- 推理优化
- 服务化部署
13.3 推荐资源
论文:
- BERT原论文: "BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding" (NAACL 2019)
- Transformer: "Attention Is All You Need" (NIPS 2017)
- RoBERTa: "RoBERTa: A Robustly Optimized BERT Pretraining Approach" (2019)
代码:
- Hugging Face Transformers: https://github.com/huggingface/transformers
- 最全面的预训练模型库
书籍:
- 《自然语言处理实战》李纪为
- 《Transformers for Natural Language Processing》Denis Rothman
课程:
- Stanford CS224N: Natural Language Processing with Deep Learning
- Hugging Face Course: https://huggingface.co/course
14. 附录
14.1 常见问题FAQ
Q1: BERT vs GPT有什么区别?
A:
- BERT: Encoder-only, 双向, 适合理解任务 (分类、NER等)
- GPT: Decoder-only, 单向, 适合生成任务 (文本生成)
- 选择: 理解用BERT,生成用GPT
Q2: 为什么BERT不能用于文本生成?
A: BERT是双向模型,训练时能看到未来信息,无法自回归生成。生成需要GPT、T5等。
Q3: 如何处理超过512长度的文本?
A:
- 截断 (简单但可能丢信息)
- 滑动窗口 + 结果聚合
- 使用Longformer/BigBird
- 层次化BERT
Q4: 中文BERT和英文BERT有什么不同?
A:
- 分词: 中文用字符/词,英文用WordPiece
- 词表: 不同语言的词表
- 训练数据: 不同语言的语料
- 性能: 需要在对应语言上评估
Q5: Fine-tuning时应该冻结哪些层?
A:
- 数据充足: 全部微调
- 数据较少: 冻结底层,微调高层
- 数据极少: 只微调分类器
Q6: BERT预训练需要多少数据?
A:
- 英文: 16GB+ 文本 (BooksCorpus + Wikipedia)
- 中文: 5-10GB 起步
- 领域适应: 1GB+ 领域数据
14.2 数学符号表
| 符号 | 含义 |
|---|---|
| X | 输入序列 |
| H | 隐藏状态 |
| Q, K, V | Query, Key, Value |
| d_model | 模型维度 (768) |
| d_k | 每个头的维度 (64) |
| h | 注意力头数 (12) |
| L | 层数 (12/24) |
| σ | Softmax函数 |
| ⊕ | 元素相加 |
14.3 BERT配置速查
BERT-Base:
json
{
"hidden_size": 768,
"num_hidden_layers": 12,
"num_attention_heads": 12,
"intermediate_size": 3072,
"max_position_embeddings": 512,
"vocab_size": 21128, // 中文
"type_vocab_size": 2
}
BERT-Large:
json
{
"hidden_size": 1024,
"num_hidden_layers": 24,
"num_attention_heads": 16,
"intermediate_size": 4096,
"max_position_embeddings": 512,
"vocab_size": 21128,
"type_vocab_size": 2
}
14.4 性能基准
| 任务 | 数据集 | BERT-Base | BERT-Large | 人类 |
|---|---|---|---|---|
| GLUE | 多任务 | 78.3% | 80.5% | 87% |
| SQuAD 1.1 | 问答 | 88.5 F1 | 90.9 F1 | 91.2 F1 |
| SQuAD 2.0 | 问答 | 76.3 F1 | 81.8 F1 | 89.5 F1 |
| CoNLL-2003 | NER | 92.4 F1 | 92.8 F1 | - |
总结
BERT 是一个通过双向 Transformer 编码器,利用掩码语言模型和下一句预测任务进行预训练得到的通用语言理解模型。其"预训练+微调"的范式,使得研究者和小型团队能够利用一个强大的通用语言基础,快速、高效地解决各种具体的自然语言处理问题,是自然语言处理领域一个划时代的里程碑。
BERT 的影响与意义
-
性能飞跃:BERT 在发布时,在 11 项 NLP 任务上刷新了纪录,取得了当时最好的性能
-
开启预训练时代:BERT 的成功确立了"预训练 + 微调"作为 NLP 领域的新范式。后续的模型如 GPT系列、RoBERTa、T5 等都是在这一范式下的发展和改进
-
双向上下文理解:与之前的单向模型相比,BERT 的双向性使其对语境的理解更加深刻和准确
-
推动应用落地:由于预训练模型可以公开获取,企业和开发者可以轻松地将其应用于自己的业务中,极大地加速了 NLP 技术的产业化
13.4 结语
BERT开启了NLP的预训练时代,是深度学习在自然语言处理领域的里程碑。通过本教程,您应该已经掌握:
✓ BERT的架构和原理
✓ 预训练和微调方法
✓ 完整的代码实现
✓ 实战应用技巧
✓ 调优和部署经验
下一步行动:
- 在自己的任务上fine-tune BERT
- 尝试不同的变体 (RoBERTa, ALBERT等)
- 探索多模态和长文本扩展
- 关注最新的预训练模型进展
希望这个详细的教程能帮助你透彻地理解 BERT!祝您在NLP领域取得成功!有任何问题欢迎交流探讨。