预训练与微调( Pre-training and
Fine-tuning)是现代深度学习中最成功的迁移学习范式之一。 2018 年 BERT
的横空出世彻底改变了 NLP 领域的研究范式,预训练模型在 CV
、语音、多模态等领域也取得了巨大成功。但预训练为什么有效?微调时应该如何调整学习率?哪些层应该冻结?这些问题背后有着深刻的理论与工程考量。
本文将从第一性原理出发,推导预训练的数学基础,解析对比学习与掩码语言模型的损失函数,详细讲解微调的各种策略,并提供一个完整的
BERT
微调实现(包含梯度累积、混合精度、学习率调度等工业级技巧)。我们会看到,预训练本质上是在学习一个强大的先验分布,而微调则是用少量标注数据进行贝叶斯更新。
预训练的动机:为什么要预训练
从数据稀缺到知识迁移
深度学习模型通常需要大量标注数据才能训练出好的性能。但在实际应用中,标注数据往往稀缺且昂贵:
医疗影像诊断 :需要专业医生标注,一张 CT
图像的标注成本可达$100-500
法律文本分类 :需要专业律师审阅,标注速度极慢
小语种翻译 :缺乏双语语料,标注困难
而无标注数据却极为丰富:互联网上有数 TB
的文本、图像、视频。预训练的核心思想就是利用大规模无标注数据学习通用表示,然后在特定任务上用少量标注数据微调 。
预训练的数学视角:贝叶斯先验
从贝叶斯角度看,预训练是在学习一个强先验分布。设 为模型参数, 为预训练数据,
为任务数据,标准训练直接最大化:
而预训练+微调则分两步:
预训练 :学习先验
微调 :贝叶斯更新
这解释了为什么预训练有效:当任务数据稀缺时,强先验能显著提升后验估计的质量 。
预训练的信息论视角:特征复用
从信息论角度,预训练学习的是数据的共同结构 ( common
structure)。设输入空间为 ,不同任务的标签空间为 ,预训练学习的特征提取器 满足:
$$
I(f(X); Y_i) i $$
其中
是互信息。换句话说,预训练学习的表示 对多个下游任务都保留了有用信息。
直觉例子 :在 ImageNet
预训练的模型学到的低层特征(边缘、纹理)和中层特征(物体部件)对很多视觉任务都有用;在大规模文本语料预训练的模型学到的语法、语义知识对各种
NLP 任务都有帮助。
预训练 vs
从头训练:收敛速度与泛化
实验表明预训练不仅提升最终性能,还能加速收敛 。原因有二:
更好的初始化 :预训练参数位于损失曲面的低损失区域,微调时只需做局部调整
正则化效应 :预训练引入的先验限制了参数空间,防止过拟合
形式化地,设预训练参数为 ,微调损失为 ,在 附近二阶泰勒展开:
$$
L() L(_0) + L(_0)^(- _0) + (- _0)^H (- _0) $$
若
已经很接近最优解,则 很小,收敛更快。
自监督学习:构造预训练任务
预训练的关键是设计自监督学习任务 ( Self-Supervised
Learning, SSL),从无标注数据中自动生成监督信号。
对比学习( Contrastive
Learning)
对比学习的核心思想是:相似样本的表示应该接近,不相似样本的表示应该远离 。
SimCLR 框架
SimCLR 是 CV 领域最成功的对比学习方法之一。给定一批图像Extra close brace or missing open brace \{x_i} _{i=1}^N ,对每张图像进行两次随机数据增强得到 ,这两个是正样本对 。设编码器为$
f, 投 影 头 为 g$,则损失函数为:
𝟙
其中 是投影表示, 是余弦相似度,
是温度参数。
关键直觉 : - 分子 希望正样本对相似度高 - 分母 是归一化项,包含所有负样本 - 温度 控制分布的平滑度: 小时对 hard negatives 敏感
InfoNCE 损失的理论基础
SimCLR 的损失是 InfoNCE 损失的实例,可以证明最小化 InfoNCE
等价于最大化互信息的下界。设正样本对 来自联合分布$ p(x, x^+), 负 样 本 x^-来 自 边 缘 分 布
p(x^-)$,则:
$$
I(X; X^+) N - [_{} ] $$
证明利用了 Jensen
不等式和重要性采样。这表明对比学习在隐式地最大化正样本对的互信息 。
MoCo:动量对比学习
SimCLR 需要大 batch size(通常 4096-8192)才能有足够的负样本。 MoCo
通过维护一个动量更新的队列 来解决这个问题:
其中 是 query
编码器参数, 是 key
编码器参数, 是动量系数。队列大小可达
65536,提供了丰富的负样本。
掩码语言模型( Masked
Language Model)
掩码语言模型是 NLP 预训练的主流方法,由 BERT 首次提出。
BERT 的 MLM 任务
给定输入序列 ,随机选择 15%的 token
进行掩码(替换为特殊 token ),设掩码位置集合为 ,模型需要预测被掩码的 token:
$$
L_{} = -{i M} P(x_i | x {M}) $$
其中 表示除了掩码位置外的所有 token 。
15%掩码策略的细节 : - 80%概率替换为 - 10%概率替换为随机 token
- 10%概率保持不变
这样做是为了缓解预训练和微调的分布偏移(因为微调时没有 token)。
MLM 的自回归分解
虽然 MLM
是非自回归的(所有掩码位置并行预测),但可以用自回归方式分解其损失。设 是掩码 token 按某个顺序排列,则:
$$
P(x_M | x_{M}) = {j=1}^{|M|} P(x {i_j} | x_{M}, x_{i_1}, ,
x_{i_{j-1}} ) $$
但 BERT 的 MLM 假设掩码 token 之间独立:
$$
P_{} (x_M | x_{M}) = {i M} P(x_i | x {M}) $$
这是一个独立性假设 ,忽略了掩码 token
之间的依赖关系。 XLNet 通过 Permutation Language Modeling
解决了这个问题。
掩码策略的数学分析
为什么选择 15%的掩码比例?太少(如 5%)学习信号弱,太多(如
50%)上下文信息不足。可以通过信息论分析:
设掩码比例为 ,则条件熵为:
$$
H(X_M | X_{M}) = -{x_M, x {M}} P(x_M, x_{M}) P(x_M | x_{M})
$$
当 太小时, 很小(容易预测);当 太大时, 信息不足以预测 。实验发现 15%是一个较好的平衡点。
Next Sentence Prediction
(NSP)
BERT 还引入了 NSP 任务:给定两个句子 和 ,判断 是否是 的下一句。损失函数为:
$$
L_{} = -P(y | ) $$
其中$ y {0, 1} , $ 是特殊 token
的表示。
但后续研究( RoBERTa)表明 NSP 的效果不明显,甚至有害。原因是 NSP
任务太简单:模型可能只是学到了主题区分( topic
discrimination),而非句间关系。
Sentence Order Prediction
(SOP)
ALBERT 提出用 SOP 替代
NSP:给定两个连续句子,判断它们的顺序是否正确。这比 NSP
更难,需要理解句间的细粒度关系。
微调策略:如何高效适配下游任务
预训练模型通常有上亿参数,如何高效地适配到下游任务是关键问题。
全参数微调( Full
Fine-Tuning)
最直接的方法是微调所有参数。设预训练参数为 ,下游任务损失为 ,微调优化:
其中
是正则项,防止偏离预训练参数太远。这对应elastic weight
consolidation (EWC) 的简化版本。
学习率调整:
discriminative fine-tuning
全参数微调时,不同层应该用不同的学习率。直觉是: -
底层 (如 embedding
层)学到的是通用特征,应该小幅调整(小学习率) -
顶层 (如分类头)是任务特定的,应该大幅调整(大学习率)
ULMFiT 提出discriminative fine-tuning :设模型有 层,第 层的学习率为:
其中 是顶层学习率, 是衰减因子(通常取
2.6)。这样底层学习率比顶层小
倍。
学习率调度: warmup 与 cosine
decay
预训练模型微调时,常用的学习率调度策略是:
Warmup :前 步线性增加学习率 2. Cosine decay :之后余弦衰减
Warmup
的直觉是:微调初期梯度方差大(因为模型还未适应新任务),小学习率能稳定训练。
层冻结( Layer Freezing)
对于数据量较少的任务,冻结部分层可以防止过拟合。
冻结策略的选择
常见策略有三种:
冻结底层 :冻结 embedding 和前几层
Transformer,只微调顶层
冻结顶层 :冻结顶层,只微调底层(较少使用)
逐层解冻 :先冻结所有层,逐步解冻(从顶层到底层)
ULMFiT
采用逐层解冻:先微调顶层,收敛后解冻倒数第二层,以此类推。这样能逐步适配任务,避免灾难性遗忘 。
冻结的数学解释:正则化视角
冻结部分参数等价于对这些参数施加无穷大的 正则:
这是带等式约束的优化问题。用拉格朗日乘数法,等价于:
因此冻结是一种极端的正则化。
适配器(
Adapter):参数高效微调
全参数微调需要为每个任务存储一份完整模型。 Adapter
通过在预训练模型中插入小型模块,只微调这些模块,大幅减少参数量。
Adapter 架构
Adapter 是一个 bottleneck 结构,插入在 Transformer 的每一层:
$$
h' = h + f(h) = h + W_2 (W_1 h) $$
其中 是 Transformer 层的输出, , , 是 bottleneck 维度(通常$ r
= 64, d = 768$)。
参数量为 ,远小于 Transformer
层的参数量 (自注意力+FFN)。
Adapter 的理论:低秩更新
Adapter
本质上是对预训练模型进行低秩更新 。设预训练权重为 ,微调后为 ,则 Adapter 假设:
即 是秩为
的低秩矩阵。这背后的假设是:任务适配只需在参数空间的低维子空间中移动 。
LoRA:低秩适配
LoRA( Low-Rank Adaptation)进一步简化
Adapter,直接对权重矩阵进行低秩分解:
$$
W = W_0 + W = W_0 + BA $$
其中 , ,$ r (d, k)。 训 练 时 冻 结 W_0, 只 更 新 B$ 和 。
LoRA 的优势: - 参数高效 :只需存储 和 (参数量为$ r(d+k)) 推 理 无 开 销 : 可 以 将 BA$ 合并到 中,推理时无额外计算 -
易于切换 :可以快速切换不同任务(只需替换 )
BERT 预训练与微调
BERT 架构回顾
BERT( Bidirectional Encoder Representations from
Transformers)是一个多层双向 Transformer 编码器。设输入序列为 , BERT 通过多层自注意力学习上下文表示:
每个 Transformer 层包含多头自注意力和前馈网络:
BERT 预训练任务
BERT 使用两个预训练任务:
Masked Language Model (MLM) :随机掩码 15% token
并预测
Next Sentence Prediction
(NSP) :判断两个句子是否连续
总损失为:
$$
L = L_{} + L_{} $$
BERT 微调范式
微调时, BERT 可以适配多种 NLP 任务:
文本分类
在输入前加
token,用其表示 进行分类:
$$
y = (W h_{} + b) $$
损失函数为交叉熵:
$$
L = -_{i=1}^C y_i^{} y_i $$
序列标注(如 NER)
对每个 token 预测标签:
$$
y_i = (W h_i + b), i = 1, , T $$
问答(如 SQuAD)
预测答案的起始和结束位置:
GPT 预训练与微调
GPT( Generative Pre-trained
Transformer)使用自回归语言模型进行预训练:
$$
L = -{t=1}^T P(x_t | x {<t}) $$
微调时, GPT 在输入末尾加上任务特定的 token,并用最后一个 token
的表示进行预测。
完整实现: BERT 微调文本分类
下面提供一个完整的 BERT
微调实现,包含梯度累积、混合精度训练、学习率调度等工业级技巧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 import torchimport torch.nn as nnfrom torch.utils.data import DataLoader, Datasetfrom transformers import BertTokenizer, BertModel, AdamW, get_linear_schedule_with_warmupfrom torch.cuda.amp import autocast, GradScalerfrom tqdm import tqdmimport numpy as npfrom sklearn.metrics import accuracy_score, f1_scoreclass BERTClassifier (nn.Module): """BERT 文本分类器""" def __init__ (self, bert_model_name='bert-base-uncased' , num_classes=2 , dropout=0.1 ): super ().__init__() self.bert = BertModel.from_pretrained(bert_model_name) self.dropout = nn.Dropout(dropout) self.classifier = nn.Linear(self.bert.config.hidden_size, num_classes) def forward (self, input_ids, attention_mask ): outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask) pooled_output = outputs.pooler_output pooled_output = self.dropout(pooled_output) logits = self.classifier(pooled_output) return logits class TextDataset (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.encode_plus( 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(), 'label' : torch.tensor(label, dtype=torch.long) } class BERTFineTuner : """BERT 微调训练器""" def __init__ ( self, model, train_dataloader, val_dataloader, num_epochs=3 , learning_rate=2e-5 , warmup_ratio=0.1 , gradient_accumulation_steps=1 , max_grad_norm=1.0 , device='cuda' , use_amp=True , discriminative_lr=False , lr_decay=2.6 ): self.model = model.to(device) self.train_dataloader = train_dataloader self.val_dataloader = val_dataloader self.num_epochs = num_epochs self.device = device self.use_amp = use_amp self.gradient_accumulation_steps = gradient_accumulation_steps self.max_grad_norm = max_grad_norm self.total_steps = len (train_dataloader) * num_epochs // gradient_accumulation_steps self.warmup_steps = int (self.total_steps * warmup_ratio) if discriminative_lr: self.optimizer = self._create_discriminative_optimizer(learning_rate, lr_decay) else : self.optimizer = AdamW(model.parameters(), lr=learning_rate, eps=1e-8 ) self.scheduler = get_linear_schedule_with_warmup( self.optimizer, num_warmup_steps=self.warmup_steps, num_training_steps=self.total_steps ) self.scaler = GradScaler() if use_amp else None self.criterion = nn.CrossEntropyLoss() self.train_losses = [] self.val_losses = [] self.val_accuracies = [] def _create_discriminative_optimizer (self, lr, decay ): """创建判别式优化器:不同层使用不同学习率""" num_layers = len (self.model.bert.encoder.layer) param_groups = [] param_groups.append({ 'params' : self.model.bert.embeddings.parameters(), 'lr' : lr / (decay ** num_layers) }) for i in range (num_layers): param_groups.append({ 'params' : self.model.bert.encoder.layer[i].parameters(), 'lr' : lr / (decay ** (num_layers - i - 1 )) }) param_groups.append({ 'params' : list (self.model.bert.pooler.parameters()) + list (self.model.classifier.parameters()), 'lr' : lr }) return AdamW(param_groups, eps=1e-8 ) def train_epoch (self ): """训练一个 epoch""" self.model.train() total_loss = 0 progress_bar = tqdm(self.train_dataloader, desc='Training' ) for step, batch in enumerate (progress_bar): input_ids = batch['input_ids' ].to(self.device) attention_mask = batch['attention_mask' ].to(self.device) labels = batch['label' ].to(self.device) if self.use_amp: with autocast(): logits = self.model(input_ids, attention_mask) loss = self.criterion(logits, labels) loss = loss / self.gradient_accumulation_steps self.scaler.scale(loss).backward() else : logits = self.model(input_ids, attention_mask) loss = self.criterion(logits, labels) loss = loss / self.gradient_accumulation_steps loss.backward() if (step + 1 ) % self.gradient_accumulation_steps == 0 : if self.use_amp: self.scaler.unscale_(self.optimizer) torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.max_grad_norm) self.scaler.step(self.optimizer) self.scaler.update() else : torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.max_grad_norm) self.optimizer.step() self.scheduler.step() self.optimizer.zero_grad() total_loss += loss.item() * self.gradient_accumulation_steps progress_bar.set_postfix({'loss' : loss.item() * self.gradient_accumulation_steps}) avg_loss = total_loss / len (self.train_dataloader) return avg_loss def evaluate (self ): """评估模型""" self.model.eval () total_loss = 0 all_preds = [] all_labels = [] with torch.no_grad(): for batch in tqdm(self.val_dataloader, desc='Evaluating' ): input_ids = batch['input_ids' ].to(self.device) attention_mask = batch['attention_mask' ].to(self.device) labels = batch['label' ].to(self.device) logits = self.model(input_ids, attention_mask) loss = self.criterion(logits, labels) total_loss += loss.item() preds = torch.argmax(logits, dim=1 ).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.cpu().numpy()) avg_loss = total_loss / len (self.val_dataloader) accuracy = accuracy_score(all_labels, all_preds) f1 = f1_score(all_labels, all_preds, average='weighted' ) return avg_loss, accuracy, f1 def train (self ): """完整训练流程""" print (f"Total steps: {self.total_steps} " ) print (f"Warmup steps: {self.warmup_steps} " ) print (f"Gradient accumulation steps: {self.gradient_accumulation_steps} " ) best_val_loss = float ('inf' ) for epoch in range (self.num_epochs): print (f"\nEpoch {epoch + 1 } /{self.num_epochs} " ) train_loss = self.train_epoch() self.train_losses.append(train_loss) val_loss, val_acc, val_f1 = self.evaluate() self.val_losses.append(val_loss) self.val_accuracies.append(val_acc) print (f"Train Loss: {train_loss:.4 f} " ) print (f"Val Loss: {val_loss:.4 f} , Val Acc: {val_acc:.4 f} , Val F1: {val_f1:.4 f} " ) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(self.model.state_dict(), 'best_model.pt' ) print ("Saved best model!" ) return self.train_losses, self.val_losses, self.val_accuracies def main (): BERT_MODEL = 'bert-base-uncased' NUM_CLASSES = 2 MAX_LENGTH = 128 BATCH_SIZE = 16 NUM_EPOCHS = 3 LEARNING_RATE = 2e-5 GRADIENT_ACCUMULATION_STEPS = 2 train_texts = ["This is great!" * 10 , "This is terrible!" * 10 ] * 500 train_labels = [1 , 0 ] * 500 val_texts = ["This is great!" * 10 , "This is terrible!" * 10 ] * 100 val_labels = [1 , 0 ] * 100 tokenizer = BertTokenizer.from_pretrained(BERT_MODEL) train_dataset = TextDataset(train_texts, train_labels, tokenizer, MAX_LENGTH) val_dataset = TextDataset(val_texts, val_labels, tokenizer, MAX_LENGTH) train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True ) val_dataloader = DataLoader(val_dataset, batch_size=BATCH_SIZE) model = BERTClassifier(BERT_MODEL, NUM_CLASSES) trainer = BERTFineTuner( model=model, train_dataloader=train_dataloader, val_dataloader=val_dataloader, num_epochs=NUM_EPOCHS, learning_rate=LEARNING_RATE, gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS, discriminative_lr=True , lr_decay=2.6 , use_amp=True ) train_losses, val_losses, val_accuracies = trainer.train() if __name__ == '__main__' : main()
代码详解
判别式学习率
_create_discriminative_optimizer方法实现了不同层使用不同学习率:
embedding 层使用 ,分类器使用 。
梯度累积
当显存不足时,可以通过梯度累积模拟大 batch size:
1 2 3 4 5 6 loss = loss / self.gradient_accumulation_steps loss.backward() if (step + 1 ) % self.gradient_accumulation_steps == 0 : optimizer.step() optimizer.zero_grad()
每隔gradient_accumulation_steps步更新一次参数,等效于
batch size 扩大gradient_accumulation_steps倍。
混合精度训练
使用torch.cuda.amp进行混合精度训练,显著减少显存占用和训练时间:
1 2 3 4 5 6 7 with autocast(): logits = self.model(input_ids, attention_mask) loss = self.criterion(logits, labels) self.scaler.scale(loss).backward() self.scaler.step(self.optimizer) self.scaler.update()
深度 Q&A
Q1:
为什么预训练通常比从头训练效果好?
理论解释 : 1.
数据效率 :预训练利用了大规模无标注数据,学到了数据的共同结构
2.
正则化 :预训练参数作为先验,限制了参数空间,防止过拟合
3.
优化景观 :预训练参数位于损失曲面的低损失区域,微调更容易收敛
实验证据 : - BERT 在 GLUE 基准上的 9 个任务中, 8
个任务都超过了从头训练的模型 - ImageNet 预训练在 COCO 目标检测上提升 10+
mAP
Q2: 对比学习为什么需要负样本?
对比学习的目标是学习一个表示空间,使得相似样本接近、不相似样本远离。负样本提供了推力 (
repulsive force),防止所有样本坍缩到同一点(模型崩溃)。
数学上, SimCLR 的损失可以分解为:
- 第一项
拉近正样本对 - 第二项
包含负样本,推开负样本对
没有负样本时,第二项退化为常数,模型容易坍缩。
Q3: 为什么 BERT
使用双向编码而 GPT 使用单向编码?
BERT :双向编码能利用上下文信息,适合理解类任务(分类、
NER 、 QA)
GPT :单向编码符合自回归生成,适合生成类任务(文本生成、对话)
实验表明:在理解类任务上,双向 >
单向;在生成类任务上,单向更自然。
Q4: 微调时为什么需要 warmup?
微调初期,模型参数还未适应新任务,梯度方差大。如果直接使用大学习率,可能导致:
1. 梯度爆炸 :某些样本的梯度非常大,破坏预训练知识 2.
参数振荡 :优化轨迹剧烈振荡,难以收敛
Warmup 逐渐增大学习率,让模型平滑地过渡到新任务。数学上, warmup
相当于使用自适应学习率:
Q5: 如何选择微调的学习率?
经验法则:微调学习率应该比预训练小 1-2
个数量级 。
预训练学习率:
微调学习率:
原因:预训练参数已经接近最优解,微调只需小幅调整。学习率太大会破坏预训练知识。
实践中,可以用学习率搜索 ( learning rate
finder):从小学习率开始,逐步增大,观察 loss 曲线,选择 loss
下降最快的学习率。
Q6: 冻结哪些层效果最好?
取决于任务与预训练数据的相似度:
相似度
数据量
建议策略
高相似
少
冻结底层,微调顶层
高相似
多
全参数微调
低相似
少
冻结中间层,微调底层和顶层
低相似
多
全参数微调 + 判别式学习率
直觉 :底层学习的是通用特征(边缘、纹理、语法),顶层学习的是任务特定特征。高相似任务复用底层特征,低相似任务需要调整底层特征。
Q7: 如何判断模型是否过拟合?
过拟合的信号: 1. 训练 loss 下降但验证 loss
上升 (最明显的信号) 2. 训练 acc 很高但验证 acc
停滞 3.
模型对训练样本的预测非常自信 (输出概率接近 0 或 1)
解决方法: 1. 增大正则化 :增大 dropout 、 weight
decay 2. Early stopping :在验证 loss 最低时停止训练 3.
数据增强 :增加训练样本的多样性 4.
减小模型容量 :使用更小的模型或冻结更多层
Q8:
混合精度训练如何保证精度不损失?
混合精度训练使用 FP16 存储和计算,但在关键步骤使用 FP32:
Loss scaling :将 loss 乘以一个大数(如 1024),防止
FP16 下溢
Master weights :优化器维护 FP32 的权重副本
动态 loss scaling :自动调整 scaling
因子,避免溢出
数学上, FP16 的动态范围是 ,而梯度通常在 范围内,直接使用 FP16 会导致小梯度下溢。 Loss scaling
将梯度放大到 ,在
FP16 范围内。
Q9: 预训练需要多少数据才有效?
没有统一答案,但有一些经验法则:
NLP :至少几百 MB 文本(如 Wikipedia dump 约
4GB)
CV :至少几百万张图像(如 ImageNet 120 万张)
关键不是数据量,而是数据多样性 。 1000
万张同一类别的图像不如 100 万张涵盖多种类别的图像。
实验表明:当预训练数据量增加 10 倍时,下游任务性能提升约 2-5
个百分点(收益递减)。
Q10:
如何评估预训练模型的质量?
三种评估方法:
下游任务性能 :在多个任务上微调,计算平均性能(如
GLUE benchmark)
表示质量 :评估学到的表示是否有意义(如线性探测、最近邻检索)
预训练 loss : loss 越低,模型越好(但不绝对)
最可靠的是下游任务性能,但成本高。线性探测是一个快速评估方法:冻结预训练模型,只训练一个线性分类器,如果准确率高,说明表示质量好。
Q11:
如何处理预训练和微调的分布偏移?
分布偏移( distribution shift)是预训练的常见问题。例如 BERT
预训练时有
token,但微调时没有。
解决方法:
BERT 的掩码策略 : 10%概率替换为随机 token,
10%概率保持不变,缓解分布偏移
Domain-adaptive
pre-training :在目标域数据上继续预训练
Gradual
unfreezing :逐层解冻,让模型逐步适应新分布
理论上,可以用重要性加权 ( importance
weighting)校正分布偏移:
$$
L_{} = {x p {} } $$
但实践中直接估计密度比很困难。
Q12:
预训练和微调的计算成本如何分配?
通常预训练占据 90%以上的计算成本。例如 BERT-large 预训练需要:
硬件 : 64 个 TPU v3(相当于 512 个 V100 GPU)
时间 : 4 天
成本 :约$10,000
而微调只需: - 硬件 :单个 V100 GPU -
时间 :几小时 - 成本 :约$10
因此,预训练一次,微调多次 是最经济的策略。大公司(如
Google 、 OpenAI)预训练通用模型,开源给社区使用。
相关论文
BERT: Pre-training of Deep Bidirectional Transformers for
Language Understanding
Devlin et al., NAACL 2019
https://arxiv.org/abs/1810.04805
Improving Language Understanding by Generative
Pre-Training (GPT)
Radford et al., OpenAI Technical Report 2018
https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf
A Simple Framework for Contrastive Learning of Visual
Representations (SimCLR)
Chen et al., ICML 2020
https://arxiv.org/abs/2002.05709
Momentum Contrast for Unsupervised Visual Representation
Learning (MoCo)
He et al., CVPR 2020
https://arxiv.org/abs/1911.05722
Universal Language Model Fine-tuning for Text
Classification (ULMFiT)
Howard and Ruder, ACL 2018
https://arxiv.org/abs/1801.06146
RoBERTa: A Robustly Optimized BERT Pretraining
Approach
Liu et al., arXiv 2019
https://arxiv.org/abs/1907.11692
Parameter-Efficient Transfer Learning for NLP
(Adapter)
Houlsby et al., ICML 2019
https://arxiv.org/abs/1902.00751
LoRA: Low-Rank Adaptation of Large Language
Models
Hu et al., ICLR 2022
https://arxiv.org/abs/2106.09685
ALBERT: A Lite BERT for Self-supervised Learning of
Language Representations
Lan et al., ICLR 2020
https://arxiv.org/abs/1909.11942
Representation Learning with Contrastive Predictive
Coding
van den Oord et al., arXiv 2018
https://arxiv.org/abs/1807.03748
Understanding the Difficulty of Training Deep Feedforward
Neural Networks
Glorot and Bengio, AISTATS 2010
http://proceedings.mlr.press/v9/glorot10a.html
Scaling Laws for Neural Language Models
Kaplan et al., arXiv 2020
https://arxiv.org/abs/2001.08361
总结
预训练与微调是迁移学习最成功的范式。本文从第一性原理出发,推导了预训练的贝叶斯视角(学习先验分布)和信息论视角(学习共同结构),详细解析了对比学习(
SimCLR 、 MoCo)和掩码语言模型( BERT MLM)的数学基础。
在微调策略上,我们讨论了全参数微调、判别式学习率、层冻结、 Adapter
等方法,并从正则化、低秩更新等角度给出了理论解释。最后,我们提供了一个完整的
BERT 微调实现,包含梯度累积、混合精度训练、学习率调度等工业级技巧。
预训练不是万能的,它的有效性依赖于预训练数据和下游任务的相似度。下一章我们将深入探讨域适应方法 ,解决预训练和下游任务分布不一致的问题。