资讯动态

BERT预训练与微调实战:从原理到中文情感分类

发布时间:2026/10/22 4:09:08 来源:尧图企业网站定制
如果只让我选一个必须搞懂的预训练模型我肯定会选BERT。别被“大模型”三个字吓住BERT的“大”主要体现在预训练阶段真正到下游任务时我们做的其实是微调。我见过太多新手把“预训练微调”当成一回事结果不是重复造轮子就是把几千万参数从头训了一遍既烧时间又不出效果。这篇就带你从BERT的原理、预训练和微调的区别一路走到跑通一个中文文本分类微调实战全程尽量说人话允许你从零基础开始看。1. 先别急着写代码把BERT的预训练微调范式看懂1.1 两阶段到底在解决什么问题自然语言处理领域长期有一个尴尬每个新任务都要重新设计模型结构、重新准备大量标注数据。早期的词向量方法虽然能提供一定的语义信息但它们是“静态”的同一个词在任何上下文里都是同一个向量遇到“苹果很好吃”和“苹果这家公司很赚钱”这种场景就抓瞎了。BERT的思路是把这个过程拆成两个阶段。第一阶段叫预训练用海量无标注文本去训练一个通用语言模型第二阶段叫微调把这个已经会“读句子”的模型放到具体任务上用少量标注数据做进一步训练。这个范式的厉害之处在于语言本身的通用规律可以在第一阶段学到下游任务只需要在第二阶段做轻量适配不需要每次都从零开始。很多小白会问为什么不直接在下游任务上训练一个大模型因为标注数据太贵了。真实业务里能拿出几千条高质量标注已经不错了几千条数据想训练几亿参数的大模型结果基本是欠拟合或者过拟合二选一。预训练相当于让模型先“上了几年学”微调则是“岗前培训”两者配合才能用少量数据撬动大模型的能力。1.2 BERT预训练时干了哪两件“苦力活”BERT的核心结构是Transformer的Encoder部分它和常见的单向语言模型不同用的是双向注意力机制也就是说模型在理解某个词时能看到它左右两侧的所有上下文。为了让这种双向结构真正学起来预训练阶段设计了两个任务。第一个任务叫“掩码语言模型”英文简称MLM。具体做法是随机把输入文本中的一部分词遮盖住让模型根据上下文去预测被遮掉的内容。原始论文里是随机遮盖15%的词其中80%换成[MASK]标记10%保持不变10%换成一个随机词。这个设计很巧妙如果全部换成[MASK]模型只在预训练阶段见过[MASK]微调阶段根本没有这个标记效果会打折扣加入随机替换则是逼着模型不依赖某个固定标记而是真正理解上下文。第二个任务叫“下一句预测”英文简称NSP。模型会看到一对句子需要判断第二句是不是第一句在原文中的下一句相当于二分类任务。这个任务让BERT学到了句子之间的连贯关系对问答、推理类任务帮助很大。虽然后来很多模型发现这个任务不一定最优但BERT通过这两个任务确实把语言知识预训练到了参数里这一点是公认的。1.3 输入序列里CLS、SEP和分词的秘密BERT的输入不是直接把一句话丢进去。需要先把文本切成词片英文用WordPiece中文则按字或者词切分然后拼成模型能读的格式。每个序列开头要加上[CLS]用来表示整个序列的信息句子之间要用[SEP]隔开再叠加三种向量词向量、句向量、位置向量。[CLS]位在分类任务里特别关键。因为BERT是双向模型每个位置都会看到整个序列的信息而[CLS]位没有具体的词义负担可以专门用来汇聚整句话的表征。做文本分类时通常取[CLS]最后一层的向量再接一个全连接层输出类别概率。位置向量也是学习出来的不是固定的三角函数。这样一来模型可以感知词的先后顺序。很多人一开始不理解为什么BERT要同时加三种向量其实你把它理解成一套坐标系统就行词向量表示“是什么字”句向量表示“属于哪句话”位置向量表示“在第几个位置”三者相加后输入给模型模型才知道当前处理的是哪个词、在哪句话里、处于什么位置。2. 预训练和微调的区别新手最容易理解错2.1 预训练用海量文本做“填空”预训练阶段的目标是让模型学会语言本身的规律。训练数据通常是几十亿甚至上百亿字的网页文本、书籍、百科等不需要人工标注因为“填空”的答案就是文本本身。这个阶段的训练非常昂贵原始BERT base模型有1.1亿参数在当时的硬件条件下也需要大量专业加速卡训练很多天普通开发者几乎不可能复现。那普通开发者怎么用预训练模型答案是不用自己训直接用别人发布好的权重。开源社区把训练好的模型参数放出来谁都可以下载使用。这就是为什么现在做NLP越来越像“搭积木”底座别人已经帮你造好了你只需要拿过来针对自己的任务拧几个螺丝。这里的核心认知是预训练模型学到的不是某一个任务的解法而是一套通用的语言表征能力。它知道“苹果”可以被“水果”修饰也知道“苹果公司”语境下的“苹果”是品牌名它知道“虽然”后面往往接“但是”。这些知识迁移到任何任务上都有用。2.2 微调在预训练基础上做“定制”微调阶段做的事情是加载预训练权重在模型顶部换一个适配下游任务的输出头然后用你自己的标注数据继续训练。比如文本分类就把BERT原本预测“下一个词”的头换成一个二分类或全连接层实体识别就把输出改成给每个token打标签。微调时的训练方式虽然和普通神经网络训练没有本质区别但有一个重要特点整个模型的所有参数都会继续更新不只是顶部的新增层。也就是说预训练阶段学到的通用知识会在微调过程中被“调整方向”更加适配你的任务。这个动作既要充分利用已有知识又不能力度太大否则会把预训练学到的通用能力冲掉。这也是为什么微调BERT时学习率一般设置得很小常见的是2e-5到5e-5。你可以把预训练权重想成一个精心调好的音准微调只是微微调一下音量而不是把琴弦全松了重来。2.3 为什么微调比从零训练便宜这么多从零训练一个BERT模型可能需要几十上百TB的文本和昂贵的算力微调同一个模型可能只需要几千条数据和一块普通显卡。差距为什么这么大关键在于参数初始化的起点。从零训练时模型参数基本是随机噪声需要从数据中一点点摸索语言规律微调时模型已经是一个“懂语言”的状态要做的只是把已有能力映射到新任务上。这就像教一个成年人使用新软件和教一个婴儿认识世界难度完全不在一个量级。当然如果微调数据太少或者任务和预训练差异过大效果也会打折。但整体来说预训练微调范式把NLP任务的成本降到了适合个人和小团队操作的级别。这也是当时BERT能迅速普及的核心原因。3. 实操用BERT微调一个中文情感分类模型3.1 准备环境与数据动手之前先把环境准备好。推荐用Python 3.8以上版本安装PyTorch和Transformers库。训练数据我用的是自定义的中文评论数据CSV格式包含两列text和label。label为1表示正面0表示负面。这里为了演示数据规模故意做小正式使用时建议至少几千条。安装命令pip install torch transformers datasets scikit-learn pandas如果只有CPU也能跑通就是速度慢一点有NVIDIA显卡的话建议顺便装好CUDA对应的PyTorch版本训练会快很多。数据文件示意text,label 这家店的奶茶很好喝下次还来,1 等了一个小时还没上菜体验太差,0 服务态度不错环境也干净,1 分量太少价格还贵,0读取数据时要注意类别分布。如果正负样本差距太大可以提前做平衡处理比如随机下采样或给少数类加权重否则模型很容易学成“无脑预测多数类”。3.2 加载预训练模型和分词器我用的是中文预训练模型bert-base-chinese这个权重在开源社区可以直接下载。第一次运行时会自动下载模型文件到本地缓存。加载模型和分词器的代码import pandas as pd import torch from torch.utils.data import Dataset from transformers import AutoTokenizer, AutoModelForSequenceClassification MODEL_NAME bert-base-chinese tokenizer AutoTokenizer.from_pretrained(MODEL_NAME) model AutoModelForSequenceClassification.from_pretrained( MODEL_NAME, num_labels2 ) print(模型加载完成参数量, model.num_parameters())为什么用AutoTokenizer和AutoModelForSequenceClassification这是Transformers库为不同模型提供的统一接口它会根据你填的MODEL_NAME自动匹配对应的分词器和模型结构。以后换RoBERTa、ALBERT这些模型只需要改一个字符串代码其他地方不用动。接下来把数据包装成PyTorch的Datasetclass CommentDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len128): self.texts texts self.labels labels self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text self.texts[idx] label self.labels[idx] encoding self.tokenizer( text, truncationTrue, paddingmax_length, max_lengthself.max_len, return_tensorspt, ) return { input_ids: encoding[input_ids].squeeze(0), attention_mask: encoding[attention_mask].squeeze(0), labels: torch.tensor(label, dtypetorch.long), }这里max_len我设为128对大多数短文本评论够用了。注意truncationTrue很关键如果某个样本超过最大长度会被截断否则会报错。attention_mask是让模型区分真实词元和填充词元的不能去掉。3.3 设置训练超参数BERT微调的超参数有相对成熟的区间新手直接参考下面的配置不容易翻车from transformers import Trainer, TrainingArguments training_args TrainingArguments( output_dir./bert_finetuned, evaluation_strategyepoch, save_strategyepoch, learning_rate2e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs3, weight_decay0.01, load_best_model_at_endTrue, metric_for_best_modeleval_accuracy, logging_steps50, save_total_limit2, fp16torch.cuda.is_available(), )几个关键点学习率2e-5是BERT微调的经典值。预训练模型已经接近一个较优状态学习率太大会让参数剧烈震荡甚至丢失预训练知识太小则收敛太慢。批次大小16是显存和训练效果之间的平衡点。如果你的显卡显存只有6G可以改成8甚至4如果显存充足32往往也能取得不错效果。显存不够时还可以开启梯度累积先把小批次的梯度攒起来再更新。fp16True是混合精度训练能显著减少显存占用并加速训练。不过CPU上不支持代码里用torch.cuda.is_available()做判断。评估指标这里用的是准确率如果你的数据类别不平衡建议换成F1值from sklearn.metrics import accuracy_score, f1_score def compute_metrics(eval_pred): logits, labels eval_pred preds logits.argmax(-1) return { accuracy: accuracy_score(labels, preds), f1: f1_score(labels, preds, averagebinary), }3.4 完整训练与效果验证数据加载和训练的完整代码如下df pd.read_csv(data.csv).dropna() texts df[text].tolist() labels df[label].tolist() from sklearn.model_selection import train_test_split train_texts, dev_texts, train_labels, dev_labels train_test_split( texts, labels, test_size0.2, random_state42 ) train_dataset CommentDataset(train_texts, train_labels, tokenizer) dev_dataset CommentDataset(dev_texts, dev_labels, tokenizer) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetdev_dataset, compute_metricscompute_metrics, ) trainer.train()训练过程中会在终端输出每个step的loss和每个epoch的评估指标。正常来说loss会逐步下降评估准确率逐步上升。如果训练曲线非常诡异先停下来检查数据而不是继续等。训练完成后可以做一次简单预测text 这家店味道不错环境也舒服 inputs tokenizer( text, return_tensorspt, truncationTrue, max_length128, ) outputs model(**inputs) pred outputs.logits.argmax(-1).item() print(预测结果, pred) # 1 表示正面0 表示负面3.5 模型保存与复现建议训练结束后把模型和分词器保存下来方便后续加载model.save_pretrained(./my_bert_model) tokenizer.save_pretrained(./my_bert_model)以后使用保存好的模型只要把MODEL_NAME换成./my_bert_model就能直接加载不需要重新下载权重也不依赖网络。复现实验时建议固定随机种子。不同的随机种子会带来不同的初始化、数据拆分顺序直接影响最终结果。最好在训练前设置import os import random import numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) os.environ[PYTHONHASHSEED] str(seed) set_seed(42)固定种子后同一份数据和参数再次运行结果基本一致这对自己比较不同实验很有帮助。4. 新手微调BERT的踩坑实录与排查手册4.1 显存不足怎么办显存不足是BERT微调最常见的报错报错信息通常是CUDA out of memory。很多人一看到这个就慌其实解决方案就那么几招。优先级最高的是降低per_device_train_batch_size从16降到8再不行降到4。批次变小单次计算量变小显存自然够用。缺点是一个epoch更新次数变多训练时间变长。这时候可以配合gradient_accumulation_steps比如批次大小设为8梯度累积设为2相当于每两步累积一次梯度再更新与批次16的效果接近显存却省了一半。其次是开启fp16True。混合精度训练在N卡上能明显降低显存占用和计算压力。要确认你的显卡支持一般GTX 16系列以上都支持老显卡可能要谨慎。还可以检查输入序列长度。max_len128和max_len512的显存消耗差距非常大。如果数据是短文本没必要512128甚至64就够。长文本任务才需要增大max_len否则纯属浪费。4.2 模型不收敛、效果差先检查什么如果训练后准确率一直很低先不要着急调参按照这个顺序排查。第一步看数据。把训练样本打印出来看看有没有标签错误、空文本、重复文本。文本分类任务里数据质量问题导致效果差的比例远比想象中高。比如把“味道不错但环境一般”标成正面模型会学得很混乱。第二步看loss。如果loss下降很慢甚至不降可能是学习率太小如果loss剧烈震荡说明学习率太大要把2e-5降到1e-5。如果loss直接变成NaN检查数据里有没有异常值。第三步看验证集划分。数据量少的时候单次随机划分可能很不稳定建议做5折交叉验证。另外确保训练集和验证集没有交叉样本否则评估结果虚高上线后一塌糊涂。第四步看类别权重。极端不平衡时默认的交叉熵损失会偏向多数类。可以设置Trainer的class_weight或者在损失函数里给少数类更高权重。4.3 预训练权重下载、缓存和版本问题第一次运行from_pretrained时需要从网上下载预训练权重。如果网络不稳定很容易卡住或者报连接错误。解决办法是手动下载模型文件放到本地目录然后直接用本地路径加载。Transformers库的版本差异也可能导致问题。不同版本对于一些参数名、默认行为有调整比如老版本里evaluation_strategy是新版本eval_strategy的前身。遇到“unexpected keyword argument”或“loading from checkpoint”一类报错先检查库版本pip show transformers如果版本太新或太旧最省事的做法是固定一个经过验证的版本例如某个稳定的2.x版本。不要频繁升级因为你的代码可能是按老接口写的升级后可能破坏兼容性。bert-base-chinese对中文的支持总体不错但对某些专业领域词、生僻词可能效果一般。如果你做的领域是医学、法律、代码等建议优先找领域预训练模型而不是直接拿通用中文模型硬上。4.4 BERT微调超参数速查表我整理了一张常用超参数表新手可以先按这个区间设置再结合自己的数据微调参数推荐区间备注learning_rate2e-5 ~ 5e-5首选2e-5效果差再上下试探per_device_train_batch_size8 ~ 32根据显存调整小批量配合梯度累积num_train_epochs2 ~ 5数据少就少跑几轮防过拟合max_len64 ~ 512短文本128够用长文本512但不便宜warmup_ratio0.05 ~ 0.1让学习率先缓升再缓降更稳定weight_decay0.01对非bias和LayerNorm参数加权重衰减fp16True/False有N卡且支持就开启这张表不是死的。如果你的任务很简单数据量又大可以适当缩短训练轮数如果任务复杂可以加大num_train_epochs但要同时观察验证集指标防止过拟合。5. 入门之后再往前走的几条路线5.1 把主干模型当成“积木”一旦你理解了BERT的预训练微调范式后面看其他预训练模型就会轻松很多。RoBERTa只是把MLM做得更充分、移除了NSPALBERT通过参数共享大幅减少参数量ELECTRA用“替换词检测”代替掩码预测训练更高效DeBERTa改进了注意力机制在多项任务上超过BERT。它们的共同点都是先在海量文本上预训练再到下游任务微调。对于中文场景可以尝试更大更先进的中文预训练模型。使用方式几乎一样把模型名换掉再根据任务微调即可。Transformers库的Auto接口让这个过程变得极其简单这也是预训练微调范式能普及的重要原因。5.2 先做减法再做加法我的建议是入门阶段不要追求一堆技巧先把最简单的BERT微调跑通理解数据如何输入、模型如何训练、预测结果怎么出来。之后再去尝试学习率调度、对抗训练、模型蒸馏、知识增强这些复杂技巧一个个叠加到自己的流程里。大语言模型流行之后不少人觉得BERT过时了。但实际不是这样。BERT这种规模的编码器模型在文本分类、检索召回、实体识别等任务上仍然性价比很高很多大模型也把BERT作为检索器或打分器。理解BERT就是理解现代NLP最核心的零件之一。最后说句实在话所有模型效果提升都是建立在你能稳定复现一个结果的基础上的。先固定随机种子跑通一个基线再一点点改数据、改结构、改参数才是做NLP实验的正确节奏。这个顺序不能乱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑