← 返回深度学习目录
← 返回学习笔记首页
BERT与预训练语言模型
深度学习专题 · 自然语言处理的预训练革命
专题: 深度学习 · 自然语言处理
核心概念: BERT, 预训练, MLM, WordPiece, HuggingFace, RoBERTa, DistilBERT, 微调
适用人群: 具备基础深度学习知识的NLP从业者、AI算法工程师、自然语言处理研究人员
一、预训练语言模型概述
预训练语言模型(Pre-trained Language Models, PLMs)是近年来自然语言处理领域最具突破性的技术方向。其核心思路是在大规模无标注文本语料上通过自监督学习训练一个通用的语言表示模型,再针对下游任务进行微调(Fine-tuning)。这种"预训练+微调"范式彻底改变了NLP的研究和应用格局,被广泛认为是NLP领域的"ImageNet时刻"。
在BERT出现之前,主流的词向量方法(如Word2Vec、GloVe、FastText)为每个词生成一个固定且上下文无关的向量表示。这意味着"苹果"一词在"我吃了一个苹果"和"苹果发布了新手机"中具有完全相同的向量表示,无法捕捉一词多义现象。ELMo(Embeddings from Language Models)首次引入了上下文相关的表示,但采用的是双向LSTM分别训练两个方向的模型再拼接,并非真正的双向上下文建模。GPT系列使用单向Transformer解码器进行自回归语言建模,但仅利用了上文信息。
BERT(Bidirectional Encoder Representations from Transformers)由Google在2018年提出,通过掩码语言模型(Masked Language Model, MLM)实现了真正的双向上下文建模,在11项NLP基准测试上取得显著提升,开启了预训练语言模型的新纪元。此后,RoBERTa、ALBERT、DistilBERT、TinyBERT、ELECTRA、XLNet、T5等一系列模型相继提出,在性能、效率、规模等维度不断演进。
预训练范式演进:
静态词向量(2013-2017): Word2Vec, GloVe, FastText — 上下文无关,无法处理多义词
上下文词向量(2018): ELMo — 双向LSTM拼接,浅层双向
单向预训练(2018): GPT — Transformer解码器,自回归语言建模
双向预训练(2018): BERT — Transformer编码器,MLM+NSP,真正双向
改进与蒸馏(2019-2021): RoBERTa, ALBERT, DistilBERT, ELECTRA, T5
超大模型(2020-2023): GPT-3, PaLM, LLaMA, GPT-4 — 涌现能力与上下文学习
二、BERT核心原理
2.1 Transformer编码器架构
BERT的核心骨架是Transformer的编码器(Encoder)部分。与GPT使用Transformer解码器不同,BERT采用编码器结构,可以利用注意力机制同时关注输入序列中所有位置的上下文信息。一个标准的BERT模型由多层堆叠的Transformer编码器块组成,每个编码器块包含两个子层:多头自注意力(Multi-Head Self-Attention)和前馈神经网络(Feed-Forward Network, FFN),每个子层后接残差连接(Residual Connection)和层归一化(Layer Normalization)。
BERT-base由12层Transformer编码器堆叠而成,隐藏维度为768,自注意力头数为12,总参数量约1.1亿。BERT-large则扩展到24层、1024维、16个注意力头,总参数量约3.4亿。多头注意力机制允许模型在不同的表示子空间中并行学习不同类型的注意力关系,使每个位置的表示能够聚合来自序列中任意位置的信息。
Transformer编码器计算流程:
输入:Token Embeddings + Segment Embeddings + Position Embeddings
经过多头自注意力层:MultiHead(Q,K,V) = Concat(head_1,...,head_h)W^O
残差连接 + 层归一化:LayerNorm(x + Sublayer(x))
前馈神经网络:FFN(x) = max(0, xW_1 + b_1)W_2 + b_2 (GELU激活)
再次残差连接 + 层归一化
共重复L层,最终输出上下文感知的向量表示序列
BERT模型配置对照表
参数 BERT-base BERT-large
Transformer层数 (L) 12 24
隐藏维度 (H) 768 1024
自注意力头数 (A) 12 16
总参数量 110M 340M
前馈网络维度 3072 4096
2.2 双向上下文建模
BERT的最关键创新在于实现了真正的双向上下文建模。之前的语言模型(如GPT)使用自回归方式从左到右逐个预测下一个词,只能利用上文信息。ELMo虽然拼接了从左到右和从右到左两个方向的LSTM,但本质上仍然是两个独立单向模型输出的拼接,并非真正的双向。
BERT通过掩码语言模型(MLM)预训练任务解决了这个问题。在MLM中,输入序列中的部分token被随机替换为[MASK]标记,模型需要根据所有可见token(包括被掩码token的左右两侧上下文)来预测被掩码的原始token。这种方式迫使模型学习到真正的双向上下文表示,因为预测一个被掩码的词需要同时依赖其左侧和右侧的信息。
"We argue that this (MLM) allows the representation to fuse the left and the right context, which is essential for many NLP tasks." — BERT论文, Devlin et al., 2018
2.3 MLM 掩码语言模型
掩码语言模型的训练策略在BERT中有详细的设计考量。对于每个输入序列,随机选择15%的token参与预测任务。但这些被选中的token并非全部替换为[MASK],而是采用以下策略:80%的概率替换为[MASK];10%的概率替换为随机词;10%的概率保持原词不变。这种设计的原因是:如果预训练阶段只使用[MASK]标记,微调阶段输入中不会出现[MASK]标记,会导致预训练和微调之间的不匹配。加入随机替换和保持不变的策略,迫使模型在预训练时学习到对每个输入token的分布式上下文表示,而不是只依赖[MASK]标记进行预测。
每个被选中的token在输出层经过一个分类头(由BERT的隐藏层输出经过一个带GELU激活的全连接层和一个层归一化层构成),映射到词汇表大小的概率分布上,使用交叉熵损失函数计算预测误差。由于BERT的字典大小约为30,000,这个分类头的参数矩阵约为 768 x 30,000,占了模型参数中相当的比例。
# MLM掩码策略实现(伪代码)
def mask_tokens (inputs, tokenizer, mask_prob=0.15 ):
labels = inputs.clone()
# 随机选择15%的token
probability_matrix = torch.full(labels.shape, mask_prob)
# 特殊token([CLS], [SEP], [PAD])不参与掩码
special_tokens_mask = [
tokenizer.get_special_tokens_mask(val, already_has_special_tokens=True )
for val in labels.tolist()
]
probability_matrix.masked_fill_(torch.tensor(special_tokens_mask, dtype=torch.bool), value=0.0 )
masked_indices = torch.bernoulli(probability_matrix).bool()
labels[~masked_indices] = -100 # 忽略非掩码位置的损失
# 80%替换为[MASK]
indices_replaced = torch.bernoulli(torch.full(labels.shape, 0.8 )).bool() & masked_indices
inputs[indices_replaced] = tokenizer.mask_token_id
# 10%替换为随机token
indices_random = torch.bernoulli(torch.full(labels.shape, 0.5 )).bool() & masked_indices & ~indices_replaced
random_words = torch.randint(len(tokenizer), labels.shape, dtype=torch.long)
inputs[indices_random] = random_words[indices_random]
# 10%保持不变(用于缓解预训练-微调不匹配)
return inputs, labels
2.4 NSP 下一句预测
除了MLM预训练任务,BERT还引入了下一句预测(Next Sentence Prediction, NSP)作为辅助预训练任务。NSP的目标是判断给定的两个句子是否为连续句子。具体而言,在构建训练样本时,50%的情况下选择真实的连续句子对(正样本,标签为IsNext),另外50%的情况下从语料中随机选取一个句子作为第二个句子(负样本,标签为NotNext)。模型取[CLS]位置的最终隐藏状态作为句对级别的聚合表示,通过一个二分类器判断这两个句子是否连续。
NSP任务的设计初衷是让模型学习句间关系,这对于问答(QA)、自然语言推理(NLI)等需要理解两个句子之间关系的下游任务非常重要。然而,后续的研究(如RoBERTa论文)发现NSP任务的效果存在争议,移除NSP或改进为SOP(Sentence Order Prediction)任务可以在某些任务上取得更好的效果。
BERT预训练整体损失函数:
L = L_MLM + L_NSP
其中L_MLM是所有掩码token位置的平均交叉熵损失,L_NSP是句对分类的二分类交叉熵损失。两个任务共享底层的Transformer编码器参数,联合优化。
三、BERT输入表示
3.1 三层Embedding结构
BERT的输入表示由三层Embedding向量相加构成:Token Embeddings、Segment Embeddings和Position Embeddings。三个向量在相同维度上逐元素相加,得到最终的输入表示,送入第一层Transformer编码器。
Token Embeddings :将输入序列中的每个token映射为768维(BERT-base)的稠密向量。BERT使用WordPiece子词分词方法,将词汇表大小控制在约30,000个token,词汇表外的词会被拆分为多个子词单元。
Segment Embeddings :用于区分输入序列中的不同句子。BERT支持输入单句或句对,对于句对输入,第一个句子的所有token被赋予Segment A的嵌入(全零向量),第二个句子的token被赋予Segment B的嵌入(全一向量)。对于单句输入,所有token均使用Segment A的嵌入。这一层使模型能够区分两个不同的句子。
Position Embeddings :由于Transformer编码器的自注意力机制本身不具备序列位置信息(与RNN的顺序处理不同),BERT通过可学习的位置编码为每个位置i分配一个768维的向量。BERT采用了绝对位置编码,最大位置为512(即最长输入序列长度),因此位置嵌入矩阵的形状为512 x 768。位置编码的引入使模型能够感知token在序列中的相对和绝对位置。
输入表示计算公式
input_embedding = TokenEmbedding(token) + SegmentEmbedding(segment_id) + PositionEmbedding(position_id)
其中所有嵌入向量维度均为H(BERT-base中H=768),三者逐元素求和后经过LayerNorm和Dropout后送入Transformer编码器。
3.2 特殊标记:[CLS]与[SEP]
BERT的输入序列中包含两个重要的特殊标记:[CLS](Classifier)和[SEP](Separator)。[CLS]标记始终被插入到输入序列的最开头位置。它的设计目的是用作整个输入序列的汇总表示。在预训练阶段,[CLS]位置的输出来用于NSP任务的句对分类。在微调阶段,[CLS]位置的输出被用作分类任务的序列级特征表示,接上分类层即可进行文本分类、情感分析等任务。
[SEP]标记用于分隔不同的句子。在句对输入中,[CLS] sentence_A [SEP] sentence_B [SEP]。在单句输入中,[CLS] sentence [SEP]。模型通过Segment Embeddings和[SEP]标记的位置共同区分两个句子。
3.3 WordPiece子词分词
WordPiece是一种数据驱动的子词分词算法,最初由Schuster和Nakajima(2012)在日语和韩语语音识别系统中提出,后被BERT采用。与BPE(Byte Pair Encoding)类似,WordPiece通过合并最频繁的相邻subtoken对来构建词汇表,但合并标准不同:BPE选择出现频率最高的相邻字节对进行合并,而WordPiece选择使训练数据似然值增加最多的token对进行合并。
WordPiece词汇表的大小通常为30,000-100,000个token。对于不在词汇表中的词,WordPiece会将其拆分为多个子词单元。拆分过程使用"##"前缀标记子词的非首词部分。例如:"unhappiness"会被拆分为["un", "##happiness"];"playing"可能被拆分为["play", "##ing"]。这种机制使得任意词都可以被表示,同时控制了词汇表的大小,有效缓解了OOV(Out-of-Vocabulary)问题。
# HuggingFace WordPiece分词示例
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese' )
# 中文分词:BERT使用单字分词
sentence = "BERT模型彻底改变了自然语言处理领域"
tokens = tokenizer.tokenize(sentence)
print (tokens)
# ['BERT', '模', '型', '彻', '底', '改', '变', '了', '自', '然', '语', '言', '处', '理', '领', '域']
# 英文WordPiece分词
tokenizer_en = BertTokenizer.from_pretrained('bert-base-uncased' )
tokens_en = tokenizer_en.tokenize("unhappiness is a feeling" )
print (tokens_en)
# ['un', '##happiness', 'is', 'a', 'feeling']
# 转为input_ids并查看特殊标记
inputs = tokenizer(sentence, return_tensors="pt" , padding=True , truncation=True )
print (inputs.input_ids)
# tensor([[ 101, 14324, 3619, 2582, 4842, 2342, 1440, 102, 0, ...]])
# 101=[CLS] 102=[SEP] 0=[PAD]
3.4 完整输入处理流程
一个输入样本的完整处理流程如下:原始文本首先通过WordPiece分词器转换为token序列,在开头插入[CLS],句尾插入[SEP]。如果输入是句对,在两个句子之间和第二个句子的末尾各插入一个[SEP]。然后将token序列填充或截断到固定长度(如512),并生成对应的attention_mask(标识哪些位置是真实token,哪些是填充的padding token)。同时生成token_type_ids(Segment IDs),用于区分句子A和句子B。最后,token_ids、token_type_ids和position_ids分别通过对应的嵌入层转换为向量并逐元素相加,得到最终的输入表示。
# BERT完整输入处理流程
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased' )
# 句对输入:用于NSP、QA、NLI等任务
sentence_a = "BERT is a powerful model."
sentence_b = "It achieves state-of-the-art results."
# 自动处理[CLS]/[SEP]和padding/truncation
inputs = tokenizer(
text=sentence_a,
text_pair=sentence_b,
max_length=128 ,
padding='max_length' ,
truncation=True ,
return_tensors="pt"
)
print ("input_ids shape:" , inputs.input_ids.shape) # (1, 128)
print ("token_type_ids shape:" , inputs.token_type_ids.shape) # (1, 128)
print ("attention_mask shape:" , inputs.attention_mask.shape) # (1, 128)
# 三部分嵌入相加构成最终输入
# final_embedding = token_embed + segment_embed + position_embed
四、BERT微调(Fine-tuning)
4.1 微调范式和策略
BERT的微调是在预训练权重的基础上,针对具体的下游NLP任务,在任务相关的标注数据上进一步训练模型参数。微调的核心思想是:预训练阶段学习到的通用语言表示已经包含了丰富的语法、语义和世界知识,在下游任务上只需要很少量的任务特定参数和标注数据即可达到优秀的性能。
微调时通常采用较小的学习率(如2e-5到5e-5),使用AdamW优化器,训练3-5个epoch。由于BERT参数量较大(1.1亿到3.4亿),微调时需要在GPU上进行,通常使用混合精度训练以加速并减少显存占用。对于不同类型的下游任务,需要在BERT的输出上添加不同的任务头部(task-specific head)。
微调超参数推荐:
优化器:AdamW (β1=0.9, β2=0.999, ε=1e-8)
学习率:2e-5 ~ 5e-5(需根据任务和batch size适当调整)
batch size:16 ~ 32(受GPU显存限制)
训练轮数:3 ~ 5 epoch
学习率调度:线性衰减(warmup proportion = 0.1)
权重衰减:0.01
dropout:0.1
4.2 不同任务的任务头部
文本分类 / 情感分析
对于文本分类任务(如情感分析、主题分类),使用[CLS]位置的输出表示,通过一个全连接层(通常与BERT隐藏维度相同或更小)映射到类别数量大小的向量上,再经过Softmax获得各类别的概率分布。损失函数为交叉熵损失。这是最简单也最常用的微调方式。
序列标注 / 命名实体识别(NER)
对于命名实体识别、词性标注等序列标注任务,每个输入token的输出表示都会通过一个全连接层映射到标签类别空间。在预测时,通常使用CRF(条件随机场)层来建模标签之间的转移约束(如B-PERSON后不能接I-ORGANIZATION),但也可以直接使用Softmax分类器。损失函数为每个token位置的交叉熵损失之和(或CRF损失)。
问答系统(QA / SQuAD)
在抽取式问答任务(如SQuAD 2.0)中,给定一个问题和一个包含答案的上下文段落,模型需要预测答案在段落中的起始和结束位置。具体而言,每个token的输出表示分别通过两个独立的分类头(起始分类器和结束分类器),得到每个位置作为答案起始位置和结束位置的概率,然后选择概率最大的有效区间作为答案。
任务头部结构总结
任务类型 输入 输出层 损失函数
文本分类 [CLS]表示 Linear(H, C) + Softmax CrossEntropy
序列标注/NER 每个token表示 Linear(H, L) + CRF/Softmax CRF/CrossEntropy
抽取式QA 每个token表示 Linear(H, 2) 始末位置 CrossEntropy
句对分类/NLI [CLS]表示 Linear(H, C) + Softmax CrossEntropy
其中H为BERT隐藏维度,C为分类类别数,L为序列标注标签数。
4.3 GLUE基准
GLUE(General Language Understanding Evaluation)是评估NLP模型通用语言理解能力的一组标准化基准测试。GLUE包含9项任务,涵盖单句分类(如CoLA语法可接受性判断、SST-2情感分析)、句对分类(如MNLI自然语言推理、QQP语义等价判定、RTE文本蕴含、MRPC释义检测、WNLI指代消解)、以及问答(STS-B语义相似度评分)等任务。BERT在GLUE上发布时的平均得分为80.5(BERT-base)和82.1(BERT-large),而当时的最优基线得分仅为72.8,提升幅度之大引发了NLP社区的广泛关注。
# 使用HuggingFace Trainer进行GLUE SST-2微调
from datasets import load_dataset
from transformers import (
BertForSequenceClassification,
BertTokenizer,
Trainer,
TrainingArguments
)
# 1. 加载数据和tokenizer
dataset = load_dataset("glue" , "sst2" )
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased" )
def tokenize_function (examples):
return tokenizer(
examples["sentence" ],
padding="max_length" ,
truncation=True ,
max_length=128
)
tokenized_datasets = dataset.map(tokenize_function, batched=True )
# 2. 加载预训练BERT + 分类头
model = BertForSequenceClassification.from_pretrained(
"bert-base-uncased" ,
num_labels=2
)
# 3. 配置训练参数
training_args = TrainingArguments(
output_dir="./results" ,
evaluation_strategy="epoch" ,
learning_rate=2e-5 ,
per_device_train_batch_size=32 ,
per_device_eval_batch_size=64 ,
num_train_epochs=3 ,
weight_decay=0.01 ,
warmup_ratio=0.1 ,
logging_dir="./logs" ,
logging_steps=100 ,
save_strategy="epoch" ,
load_best_model_at_end=True ,
metric_for_best_model="accuracy" ,
)
# 4. 创建Trainer并训练
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets["train" ],
eval_dataset=tokenized_datasets["validation" ],
)
trainer.train()
4.4 数据增强与正则化微调技巧
在实际的微调应用中,标注数据量通常有限,以下几点技巧可以显著提升微调效果:层逐渐解冻(Layer-wise Learning Rate Decay, LLRD)——靠近底层的Transformer层包含更多通用语言特征,应使用较小的学习率;靠近顶层的层包含更多任务相关的特征,应使用较大的学习率。数据增强——使用回译(back-translation)、随机掩码、同义词替换等方法扩充标注数据。对抗训练——在Embedding层添加小的对抗扰动(如FGM、PGD),增强模型的鲁棒性。重新初始化顶层——在微调小数据集时,重新随机初始化靠近输出的1-2层Transformer参数,可以防止过拟合。
# 层逐渐解冻(LLRD)实现示例
import torch
from transformers import BertForSequenceClassification
model = BertForSequenceClassification.from_pretrained("bert-base-uncased" )
# 设置不同层的学习率倍率
def get_llrd_params (model, lr_base=2e-5 , lr_mult=0.95 ):
opt_params = []
# 嵌入层学习率最低
opt_params.append({
'params' : model.bert.embeddings.parameters(),
'lr' : lr_base * lr_mult ** model.config.num_hidden_layers
})
# 各Transformer层逐渐增大学习率
for i, layer in enumerate (model.bert.encoder.layer):
opt_params.append({
'params' : layer.parameters(),
'lr' : lr_base * lr_mult ** (model.config.num_hidden_layers - 1 - i)
})
# 分类头使用最大学习率
opt_params.append({
'params' : model.classifier.parameters(),
'lr' : lr_base
})
return opt_params
params = get_llrd_params(model)
optimizer = torch.optim.AdamW(params, lr=2e-5 , weight_decay=0.01 )
五、BERT模型的演进与改进
5.1 RoBERTa:更充分的预训练
RoBERTa(Robustly Optimized BERT Approach)由Meta AI在2019年提出,系统性地研究了BERT预训练中超参数和训练数据的影响,提出了几个关键改进:第一,使用动态掩码(Dynamic Masking)替代BERT的静态掩码——BERT在数据预处理时对每个样本只进行一次掩码处理,而RoBERTa在每个训练epoch中对同一数据复制10份并使用不同的掩码模式,或者直接在训练时动态生成掩码,使模型在更多样化的掩码上下文中学习。第二,移除NSP任务——RoBERTa的实验表明NSP任务对下游任务帮助有限,移除后可以提升性能。第三,使用更大的mini-batch(8K)和更多的训练数据(160GB vs BERT的16GB,新增CC-News、OpenWebText、Stories等语料)。第四,更长的训练步数(500K步)和更大的学习率。RoBERTa在GLUE和SQuAD等基准上显著超越BERT,成为许多后续研究的基线模型。
RoBERTa的改进
动态掩码:每个epoch不同的掩码模式
移除NSP:简化预训练目标
更大批量:8K batch size
更多数据:160GB语料
更长训练:500K步
更大词汇表:50K BPE (SentencePiece)
BERT的局限
静态掩码:数据预处理一次掩码
包含NSP:效果有限且增加复杂度
较小批量:256 batch size
较少数据:16GB (BooksCorpus+Wikipedia)
较短训练:1M步(但batch size也更小)
较小词汇表:30K WordPiece
# 使用RoBERTa进行文本分类
from transformers import RobertaTokenizer, RobertaForSequenceClassification
# RoBERTa使用BPE分词器(与BERT不同)
tokenizer = RobertaTokenizer.from_pretrained("roberta-base" )
model = RobertaForSequenceClassification.from_pretrained("roberta-base" )
inputs = tokenizer("RoBERTa improves upon BERT with dynamic masking." ,
return_tensors="pt" )
outputs = model(**inputs)
logits = outputs.logits
5.2 ALBERT:轻量化BERT
ALBERT(A Lite BERT)由Google在2019年提出,在保持模型性能的同时大幅减少了参数量。其核心创新包括:因式分解嵌入参数化(Factorized Embedding Parameterization)——BERT中词嵌入维度等于隐藏层维度(均为768),ALBERT将词嵌入矩阵分解为两个小矩阵,使嵌入维度独立于隐藏层维度,参数量从O(VxH)减少为O(VxE + ExH),其中E是嵌入维度(通常远小于H),V是词汇表大小。跨层参数共享(Cross-layer Parameter Sharing)——ALBERT在所有Transformer层之间共享注意力权重和FFN参数,虽然在计算量上并没有减少,但参数量大幅降低。此外,ALBERT使用SOP(Sentence Order Prediction)替代NSP,即判断两个句子的原始顺序是否正确,而非判断它们是否连续,实验表明SOP比NSP更能有效学习句间关系。
BERT vs ALBERT 参数量对比(以base配置为例)
模型 隐藏维度 嵌入维度 参数共享 总参数量
BERT-base 768 768 无 110M
ALBERT-base 768 128 跨层共享 12M
ALBERT-large 1024 128 跨层共享 18M
ALBERT-xxlarge 4096 128 跨层共享 235M
ALBERT-xxlarge虽然参数量只有235M(BERT-large的约70%),但隐藏维度高达4096,计算量反而更大,在GLUE和SQuAD上取得了与BERT-large相当甚至更优的性能。
5.3 DistilBERT:知识蒸馏的典范
DistilBERT由HuggingFace在2019年提出,使用知识蒸馏(Knowledge Distillation)技术将BERT-base压缩为参数量减少40%、推理速度提升60%的轻量模型,同时保留了95%以上的性能。具体方法包括:第一,参数量减少——层数从12层减少到6层(隐藏维度保持不变为768)。第二,知识蒸馏——学生模型(DistilBERT)同时学习教师模型(BERT-base)的输出概率分布(通过最小化KL散度)和真实标签的交叉熵损失,以及隐藏层表示的余弦相似度损失。第三,训练技巧——使用更大的batch size、动态掩码、移除NSP目标。DistilBERT的蒸馏损失函数为:L = α*L_ce + β*L_distill + γ*L_cos,其中L_ce是学生模型对真实标签的交叉熵,L_distill是学生与教师输出概率的KL散度,L_cos是学生与教师隐藏层表示的余弦相似度损失。
5.4 TinyBERT:两阶段蒸馏
TinyBERT由华中科技大学和华为在2019年提出,进一步将BERT压缩到参数量仅14.5M(BERT-base的13.3%),推理速度提升9.4倍。TinyBERT的核心创新在于两阶段蒸馏框架:通用蒸馏(General Distillation)——在预训练阶段,TinyBERT学生模型从教师BERT-base中学习通用语言知识,蒸馏目标包括Transformer层的隐藏状态、注意力矩阵和输出分布。任务特定蒸馏(Task-specific Distillation)——在下游任务的标注数据上,进一步蒸馏教师模型微调后的知识。这种两阶段策略使得TinyBERT在压缩效率上远超单纯的蒸馏方法。
模型效率对比
模型 参数量 相对大小
BERT-base 110M 100%
DistilBERT 66M 60%
TinyBERT 14.5M 13%
ALBERT-base 12M 11%
# 使用DistilBERT进行推理(速度优化)
from transformers import DistilBertTokenizer, DistilBertForSequenceClassification
import torch
tokenizer = DistilBertTokenizer.from_pretrained("distilbert-base-uncased" )
model = DistilBertForSequenceClassification.from_pretrained(
"distilbert-base-uncased-finetuned-sst-2-english"
)
inputs = tokenizer(
"DistilBERT is much faster than BERT while maintaining high accuracy." ,
return_tensors="pt"
)
with torch.no_grad():
outputs = model(**inputs)
probs = torch.nn.functional.softmax(outputs.logits, dim=-1 )
predicted_class = torch.argmax(probs, dim=-1 ).item()
print (f"预测类别: {predicted_class}, 概率: {probs[0][predicted_class]:.4f}" )
六、HuggingFace Transformers 库实战
6.1 库概述
HuggingFace Transformers是目前最流行的NLP预训练模型库,提供了统一的API接口来加载、使用和微调超过10万个预训练模型。该库支持PyTorch、TensorFlow和JAX三大主流深度学习框架,通过模型配置中的from_pt和from_tf参数实现框架间的无缝切换。Transformers库的设计哲学是"提供最高级别的抽象,同时允许最低级别的控制",通过Pipeline、AutoModel和Trainer三重抽象层次满足不同用户的需求。
6.2 Pipeline:最高级别的抽象
Pipeline是Transformers库中最简单的使用方式,它将tokenizer、模型和后处理封装为一个端到端的调用接口。用户只需要传入原始文本,即可获得结构化的推理结果。Pipeline支持情感分析、文本分类、命名实体识别、问答、摘要、翻译、文本生成等多种任务。
# HuggingFace Pipeline快速入门
from transformers import pipeline
# 情感分析pipeline(自动下载并加载模型)
classifier = pipeline("sentiment-analysis" )
result = classifier("BERT revolutionized the field of NLP!" )
print (result)
# [{'label': 'POSITIVE', 'score': 0.999}]
# 命名实体识别pipeline
ner = pipeline("ner" , grouped_entities=True )
entities = ner("Google released BERT in 2018 in San Francisco." )
print (entities)
# [{'entity_group': 'ORG', 'score': 0.99, 'word': 'Google', ...},
# {'entity_group': 'MISC', 'score': 0.99, 'word': 'BERT', ...},
# {'entity_group': 'LOC', 'score': 0.99, 'word': 'San Francisco', ...}]
# 问答pipeline
qa = pipeline("question-answering" )
answer = qa(
question="When was BERT released?" ,
context="BERT (Bidirectional Encoder Representations from Transformers)
was released by Google in 2018 and marked a major milestone in NLP."
)
print (f"答案: {answer['answer']}, 置信度: {answer['score']:.4f}" )
# 答案: 2018, 置信度: 0.98
6.3 AutoModel与AutoTokenizer:灵活的模型加载
AutoModel和AutoTokenizer是Transformers库中通过名称自动加载对应模型和分词器的机制。用户只需提供模型的名称(如"bert-base-uncased"、"roberta-large"、"distilbert-base-uncased"),库会自动识别模型架构并加载对应的权重和配置。AutoModel类家族包括:AutoModel(基类,返回隐藏状态)、AutoModelForSequenceClassification(分类任务)、AutoModelForTokenClassification(标注任务)、AutoModelForQuestionAnswering(问答任务)、AutoModelForMaskedLM(MLM任务)等。
# AutoModel与AutoTokenizer使用详解
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
AutoModelForMaskedLM
)
# 自动识别模型类型并加载对应分词器和模型
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 自动选择正确的模型架构
model_cls = AutoModelForSequenceClassification.from_pretrained(
model_name, num_labels=3
)
# 查看模型架构和参数量
print (model_cls.config)
print (f"参数量: {model_cls.num_parameters():,}" )
# 处理输入
inputs = tokenizer(
["BERT is amazing." , "I love NLP." ],
padding=True ,
truncation=True ,
return_tensors="pt"
)
# 前向传播
outputs = model_cls(**inputs)
logits = outputs.logits
predictions = logits.argmax(dim=-1 )
# MLM示例:预测被掩码的词
mlm_model = AutoModelForMaskedLM.from_pretrained("bert-base-uncased" )
text = "The capital of France is [MASK]."
inputs = tokenizer(text, return_tensors="pt" )
with torch.no_grad():
outputs = mlm_model(**inputs)
predictions = outputs.logits
# 获取[MASK]位置的预测结果
mask_idx = torch.where(inputs.input_ids == tokenizer.mask_token_id)[1 ]
pred_token_id = predictions[0 , mask_idx, :].argmax(dim=-1 )
pred_token = tokenizer.decode(pred_token_id)
print (f"预测结果: {pred_token}" ) # 输出: paris
6.4 Trainer:完整的训练框架
Trainer是Transformers库提供的完整训练和评估框架,封装了训练循环、梯度累积、混合精度训练、日志记录、模型保存、评估等复杂流程。结合TrainingArguments类,用户只需要配置超参数即可启动训练。Trainer的高级特性包括:自动混合精度训练(fp16/bf16)、梯度累积、梯度裁剪、早停策略、模型检查点、断点续训、分布式训练支持(DeepSpeed、FSDP)等。对于更高级的需求,Transformers库还提供了Seq2SeqTrainer用于序列到序列模型(如T5、BART)的训练。
# 使用Trainer完整微调BERT
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
Trainer,
TrainingArguments,
DataCollatorWithPadding
)
# 加载IMDb情感分析数据集
dataset = load_dataset("imdb" )
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased" )
def tokenize (examples):
return tokenizer(examples["text" ], truncation=True ,
max_length=256 , padding="max_length" )
tokenized_datasets = dataset.map(tokenize, batched=True )
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
model = AutoModelForSequenceClassification.from_pretrained(
"bert-base-uncased" , num_labels=2
)
training_args = TrainingArguments(
output_dir="./bert-imdb-finetune" ,
learning_rate=2e-5 ,
per_device_train_batch_size=16 ,
per_device_eval_batch_size=64 ,
gradient_accumulation_steps=2 ,
num_train_epochs=3 ,
weight_decay=0.01 ,
warmup_ratio=0.1 ,
logging_steps=500 ,
evaluation_strategy="steps" ,
eval_steps=500 ,
save_strategy="steps" ,
save_total_limit=2 ,
load_best_model_at_end=True ,
fp16=True ,
report_to="none" ,
remove_unused_columns=True ,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets["train" ].select(range(2000 )),
eval_dataset=tokenized_datasets["test" ].select(range(500 )),
tokenizer=tokenizer,
data_collator=data_collator,
)
trainer.train()
trainer.evaluate()
6.5 自定义微调示例:中文命名实体识别
下面给出一个完整的中文命名实体识别微调示例,展示如何加载中文BERT模型、处理中文NER数据、配置模型并在自定义数据上微调。
# 中文NER微调完整示例
from transformers import (
BertTokenizerFast,
BertForTokenClassification,
Trainer,
TrainingArguments
)
import torch
# 加载中文BERT
model_name = "bert-base-chinese"
tokenizer = BertTokenizerFast.from_pretrained(model_name)
model = BertForTokenClassification.from_pretrained(
model_name,
num_labels=7 # O, B-PER, I-PER, B-ORG, I-ORG, B-LOC, I-LOC
)
# 构造示例数据:句子 + 标签序列
sentences = [
"张三在北京的谷歌工作" ,
"李四在上海的阿里巴巴上班"
]
# BIO标签序列 (7类)
labels = [
[1 , 2 , 5 , 6 , 0 , 3 , 4 , 0 , 0 ],
[1 , 2 , 5 , 6 , 0 , 0 , 3 , 4 , 0 ]
]
# 对齐tokenizer和标签
def align_labels (tokens, labels, label_all_tokens=False ):
# WordPiece分词会导致子词,需要对齐标签
aligned_labels = []
word_ids = tokens.word_ids()
previous_word_idx = None
for word_idx in word_ids:
if word_idx is None :
aligned_labels.append(-100 ) # 特殊token忽略损失
elif word_idx != previous_word_idx:
aligned_labels.append(labels[word_idx])
else :
if label_all_tokens:
aligned_labels.append(labels[word_idx])
else :
aligned_labels.append(-100 ) # 子词忽略损失
previous_word_idx = word_idx
return aligned_labels
# 批处理encodings
encodings = tokenizer(sentences, padding=True ,
truncation=True , return_tensors="pt" )
aligned_labels = [align_labels(encodings[i], labels[i])
for i in range (len(sentences))]
# 转换为PyTorch Dataset并训练
# ...(创建自定义Dataset类,传入Trainer训练)
七、模型选择指南与最佳实践
在实际项目中选择预训练模型时,可参考以下建议:
追求最佳效果(不计成本): 选择BERT-large、RoBERTa-large或DeBERTa-large,使用充分的数据和微调策略
平衡效果和效率: 选择BERT-base、RoBERTa-base,配合知识蒸馏或模型剪枝
推理速度优先: 选择DistilBERT、TinyBERT、ALBERT-base,可部署到CPU
资源极度受限(移动端/边缘设备): 选择TinyBERT_4L-312H或MobileBERT
中文任务: 首选哈工大讯飞版BERT-wwm-ext、RoBERTa-wwm-ext或MacBERT
多语言场景: 使用mBERT或多语言XLM-RoBERTa
长文本处理(>512 tokens): 考虑Longformer、BigBird或LED(Longformer-Encoder-Decoder)
微调过程中的常见问题与解决方案
问题1:过拟合 标注数据量小(<1000条)时容易出现过拟合。解决方案:使用更强的正则化(增大dropout到0.2-0.3)、减少微调epoch(2-3)、使用LLRD、增加数据增强。
问题2:灾难性遗忘 模型在微调过程中丢失了预训练阶段学习到的通用知识。解决方案:使用混合目标训练(同时优化MLM+下游任务损失)、使用逐渐解冻策略。
问题3:类别不平衡 序列标注任务中某些标签(如O类)远多于其他标签。解决方案:使用加权交叉熵损失、Focal Loss、过采样少数类别。
问题4:OOM显存不足 BERT-large需要约16GB显存。解决方案:使用梯度累积、混合精度训练、gradient checkpointing、或更换小模型。
八、核心要点总结
BERT核心创新: 通过Masked Language Model(MLM)实现真正的双向上下文建模,打破了一直以来单向语言模型的局限,使模型能够同时利用左右两侧的上下文信息进行预测
三层输入表示: Token Embeddings(词嵌入)+ Segment Embeddings(句嵌入)+ Position Embeddings(位置嵌入)三者相加构成输入,每个部分都承载不同类型的语义信息
WordPiece分词: 基于数据驱动的子词分词算法,通过"##"前缀标识子词的延续,有效缓解OOV问题,使任意词都能被表示
微调策略: 根据下游任务类型(分类/NER/QA)添加不同的任务头部,通常只需3-5个epoch即可收敛,小数据集上使用LLRD可以有效防止过拟合
RoBERTa改进: 动态掩码(每epoch不同的掩码模式)、移除NSP、更大批量(8K)、更多数据(160GB)、更长训练,是BERT的最强基线
效率优化: ALBERT通过因式分解嵌入和跨层参数共享大幅减少参数量;DistilBERT通过知识蒸馏保留95%性能的同时减少40%参数;TinyBERT通过两阶段蒸馏将模型压缩到14.5M参数
HuggingFace三层次: Pipeline(最高级别抽象,一行代码完成推理)→ AutoModel/AutoTokenizer(灵活的模型加载)→ Trainer(完整的训练框架),逐层提供更细粒度的控制
Transformer编码器: 多头自注意力机制使模型可以同时关注输入序列中的所有位置,通过残差连接、层归一化和前馈网络堆叠多层,实现深度上下文表示学习
九、进一步思考与学习路径
BERT及其后续工作代表了NLP领域从"特征工程"到"表示学习"再到"预训练+微调"的范式转变。理解这一范式的关键在于把握"通用的语言知识是什么"以及"如何有效地将通用知识迁移到特定任务"这两个核心问题。从BERT的MLM预训练到GPT的自回归预训练,再到T5的文本到文本统一框架,预训练语言模型的发展呈现出从单向到双向、从小规模到大规模、从单模态到多模态的趋势。
推荐深入学习方向:
深入理解Transformer注意力机制:从头实现Multi-Head Self-Attention
对比学习与表示学习:SimCSE、Sentence-BERT等句子表示方法
参数高效微调(PEFT):LoRA、Adapter、Prefix Tuning等高效微调方法
大语言模型(LLM):GPT-4、LLaMA、ChatGLM等对话模型的技术演进
多模态预训练:CLIP、BLIP、BEiT等视觉-语言联合预训练模型
模型部署与工程化:ONNX导出、TensorRT加速、量化、服务化部署
# 进一步学习:使用LoRA进行参数高效微调
from peft import LoraConfig, get_peft_model, TaskType
# LoRA配置:只微调Attention层的低秩矩阵
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS,
r=8 , # 低秩矩阵的秩
lora_alpha=32 , # LoRA缩放参数
lora_dropout=0.1 , # Dropout比率
target_modules=["query" , "value" ] # 只微调Q和V矩阵
)
# 加载BERT并应用LoRA
model = AutoModelForSequenceClassification.from_pretrained(
"bert-base-uncased" , num_labels=2
)
lora_model = get_peft_model(model, lora_config)
# 查看可训练参数量(通常只有总参数的0.1%-1%)
lora_model.print_trainable_parameters()
# trainable params: 294,914 || all params: 109,487,618 || trainable%: 0.269
预训练语言模型的发展仍在加速。从BERT时代的上亿参数模型到如今千亿甚至万亿参数的大语言模型,规模法则(Scaling Laws)证明了在更多数据上训练更大的模型可以持续提升性能。与此同时,模型效率优化(量化、剪枝、蒸馏、PEFT)、推理加速(FlashAttention、 speculative decoding)、以及对齐技术(RLHF、DPO)也在快速发展。理解BERT这一基础工作,对于掌握整个预训练语言模型的发展脉络具有重要的基石意义。