资讯动态

BERT文本分类实战:从自注意力到微调技巧全解析

发布时间:2026/10/17 14:53:48 来源:尧图企业网站定制
文本分类大概是NLP里最常被拉出来练手的任务了。做推荐系统的拿它给内容打标做客服机器人的拿它识别用户意图做舆情监测的拿它判断情感倾向。早些年我用词典和TF-IDF硬凑特征的时候怎么也没想到一个叫BERT的模型会把这件事彻底改成另一套玩法。这个系列我打算分上、中、下三篇来写。上篇先解决一个很多人没真正想明白的问题BERT是怎么“读”懂一句话的为什么它在文本分类这类的判别式任务上这么稳如果你刚刚开始做文本分类或者已经会用BERT但一直对内部机制半懂不懂这篇就是给你准备的。我会用尽可能直白的语言把判别式路线、双向编码、自注意力、CLS向量这些概念串起来讲清楚顺便带上一些实测经验。1. 判别式与生成式为什么BERT天生适合做分类很多初学者看到“大语言模型”这个词第一反应是ChatGPT那类能聊天的模型。但在文本分类这件事上两条技术路线非常不一样选错方向会带来一连串麻烦。BERT走的是判别式路线GPT走的是生成式路线二者的出发点在数学上就有本质区别。1.1 两条路线学“边界”和学“分布”判别式模型建模的是条件概率 P(标签|文本)翻译成大白话就是给定一句话模型直接判断它属于哪个类别或者说模型学习的是类别之间的决策边界。生成式模型建模的则是联合概率 P(文本, 标签)更进一步说它试图学习文本本身的结构和分布标签更像是生成文本时的一个条件。用一个水果的例子来说明。判别式模型像是尝过苹果和橘子之后学会用颜色、形状、表皮粗糙程度来判断手里这个果子到底是苹果还是橘子。生成式模型则像是吃透了两类水果的所有特征你给它说“画一个苹果”它能画出一只完整的苹果来。分类任务只需要判个归属不需要想象力和创造力判别式模型天然更匹配这个目标。这直接影响了部署成本和稳定性。同样一个分类任务BERT这类判别式模型把输入句子编码成一个向量再接一个简单的线性分类器就能出结果训练流程短推理速度快。生成式模型则要“更重”内部要维护极其庞大的语言生成能力往往需要更大的模型、更高的显存还可能出现输出格式不稳定、token长度消耗过多的问题。1.2 预训练与双向编码BERT的底气BERT能成为文本分类这条线上的老江湖靠的是一套“预训练微调”的范式。预训练阶段它在大规模无标注语料上做自监督学习学到的是通用语言知识。到了你的分类任务上只需要在BERT的顶部加一个小分类头然后在你自己的标注数据上做微调就行不需要从零开始学习语言。这里有一个非常关键的设计也是BERT和GPT最大路线分歧点BERT用的是双向编码。GPT用的是单向编码也就是从左到右逐个词地预测BERT的预训练目标是掩码语言模型MLM训练时随机把句子中15%的词换成[MASK]让模型根据整个句子的上下文去猜被遮住的词是什么。因为猜测的时候能看到这个词左边和右边的所有内容模型被迫建立了真正的双向语义理解。双向到底有多重要做一个非常直观的对比就知道。这句话“他因为没赶上火车错过了今天的会议。”如果只从左往右读读到“错过了”三个字时你并不知道错过的是什么如果只从右往左读早期信息也会丢失。双向模型一次看到完整上下文所以“火车”“会议”这两个词能够共同参与理解整件事。对文本分类来说很多类别的判断依据藏在句子的多个位置双向编码几乎是一个刚需。1.3 双向编码到底解决了什么问题文本分类里最常见的两个难点一是同词多义二是远距离依赖。双向编码对这两个问题的解决效果非常明显。先说同词多义。“苹果”在“今天买了两斤苹果”里是水果在“库克刚发布了新款苹果手机”里是品牌。单向模型读到第一个“苹果”时还看不到后面的“手机”容易误判双向模型则能用右侧信息反推。BERT的做法本质上就是通过上下文来动态调整每个词的表示这比传统Word2Vec那种一个词一个固定向量的思路先进太多。再说远距离依赖。做情感分析时经常出现这种句子“这家餐厅的装修我很喜欢服务员态度也好就是菜品实在不敢恭维。”最终判断是负面但依据在最后半句前面都是干扰信息。BERT的自注意力机制允许任意两个位置直接建立联系哪怕“菜品”和“恭维”隔着12个字也能在一次计算中产生交互。这是传统RNN/CNN模型很难做到的也是BERT在长距离语义捕捉上一个很大的优势。2. 自注意力与位置编码BERT的“读”法清楚了BERT走的是双向预训练路线之后接下来就要看它具体怎么把一个句子变成计算机能计算的向量序列。这部分涉及三个核心机制输入表示、自注意力、位置编码。这三个机制结合起来构成了BERT“读”懂一句话的基本方式。2.1 自注意力一句话同时入眼BERT的核心是Transformer的编码器部分其中最要紧的模块就是自注意力Self-Attention。自注意力做的事情很直观在处理每个词的时候同时考虑句子里的所有其他词计算出每个其他词跟当前词的相关性权重再带着这些权重去融合相关信息。我习惯用一个生活化类比你在一场多人会议上需要理解某人说的某一句话。你不会只盯住发言者的嘴你会扫视在场所有人的反应谁点头、谁皱眉、谁打断这些都会帮你判断这句话的真实含义。自注意力干的就是这件事。差分是它不需要真的“扫视”而是通过矩阵计算为每个词对计算一个注意力分数。具体实现可以简化理解每个词生成三个向量Query查什么、Key能被谁查、Value实际提供的信息。当前词的Query去和句子所有词的Key做点积得到注意力分数再经Softmax归一化最后把每个词的Value按这个分数加权求和就得到当前词的新表示。这个过程就是一次自注意力计算BERT-base的配置是768维隐藏层、12个头、12层规模说大不大说小也不小但在文本分类场景下效果足够稳。2.2 位置编码没有顺序就没有语义Transformer有一个天然的短板它是并行计算的输入的所有词同时进入模型模型本身并不知道哪个词在前、哪个词在后。如果完全不管顺序“我打你”和“你打我”传入模型后初始表示是一样的句子就成了一个词袋。为了解决这个问题BERT在输入层面引入了位置编码Position Embedding。BERT-base用的是可学习的位置嵌入也就是给不同位置分配一组可训练的参数向量后来的很多模型会改用正弦余弦函数来生成位置信息。无论哪种方式目的都是把“每个词在句子中的位置”这个信息注入到词向量里这样模型才能感知到先后顺序和相对距离。更细一点说BERT的输入表示是三层向量相加的产物Token Embedding词本身的意思、Segment Embedding区分句子A/B在分类任务里通常是整句输入、Position Embedding位置信息。三者相加得到初始向量再送入后续的Transformer层。这里有个实操提示在做微调时tokenizer的职责不只是切词它还会把输入转成input_ids、attention_mask、token_type_ids三样东西这三个就是BERT输入层的直接来源。2.3 多层堆叠从词法到语义的逐级抽象BERT一共有12层以base版为例每一层都由多头注意力和前馈网络组成。这里有一个非常有意思的特性不同层学习的语言知识层次并不一样。很多研究做过探针实验发现底层更关注词法层面的信息比如词性、单复数中层开始捕捉语法结构比如主谓宾关系高层才更贴近语义和上下文判断也是分类真正依赖的那一层。这一点对实际操作有指导意义。如果你做的是句子级别的分类决定用哪一层的输出是有讲究的后面我会专门对比CLS向量和池化方案。如果你做的是token级别的任务比如命名实体识别则需要从底层或中间层抽取更细粒度的特征。很多朋友一上来就知道用最后一层输出这其实不一定是最优解后面细说。3. 从整句话到向量CLS token与池化策略实战BERT输出的是每个位置的向量但文本分类是一个句子级任务需要把一句话压缩成一个固定的向量再喂给分类器。最常见的方案是使用[CLS]位置的输出也就是开头那个特殊token的向量。这个方案确实简单但实际用起来有一些坑不同池化策略之间也存在真实的性能差异。3.1 [CLS]是什么为什么它能在句子开头BERT在处理任意输入前会在句子开头加上一个特殊的[CLS] token。在预训练的时候它被设计成可以汇聚整句话有效信息的位置。配合下一句预测任务NSP判断句子A和句子B是否相邻等训练目标[CLS]位置的最终隐藏状态被训练成可以作为“句子整体表示”使用。这不是说这个词本身有什么含义它是一个容器。在12层Transformer的计算过程中每层做自注意力时[CLS]都会去“看”句子里的所有词从它们那里收集信息。层数越深[CLS]向量里沉淀的句子级语义越多。文本分类的做法通常就是对[CLS]向量做一个dropout然后接到一个线性层上输出维度是类别数再接一个Softmax。3.2 CLS、平均池化、最大池化到底怎么选理论上[CLS]很好用但我在真实数据集上对比下来发现一个很微妙的事实在部分任务里直接取最后一层的[CLS]效果并不总是最好的。我自己在一份中文新闻分类语料上做过一次小对比效果趋势大致是这样池化策略使用位置我的实测印象CLS向量最后一层最省事大部分任务够用但少数字段有信息丢失平均池化最后一层或最后四层拼接在很多短文本分类里能反超CLS最大池化最后一层对关键词敏感型任务友好但容易丢失上下文细节最后四层加权多个层输出加权拼接稳定但显存和速度稍微吃亏为什么会出现这样的差异因为[CLS]的预训练目标和你的分类目标并不完全一致。对某些任务来说分类判断依赖的是若干局部强特征比如“贵”“太慢”“不推荐”这些词平均池化可以把这些局部特征很好地保留下来而[CLS]在高层可能已经把细节“抹平”的更多。当然[CLS]的优势是稳定地学习了句子级表征在多数规范任务里仍然是最省心的起点。我的建议是先跑一个CLS版本作为基线再跑一个平均池化版本对比几轮。如果两个版本差异在1个点以内用哪一个都无所谓如果差异明显就选效果更好的那个。不要在项目一开始就在池化策略上过多纠结先把整体流程跑通更重要。3.3 不止最后一层多层输出能救急不少朋友遇到过这种怪事无论怎么调微调超参模型在某个验证集指标上就是上不去而且不同训练轮次之间波动特别大。这时候可以考虑一个进阶操作——从多层抽取输出。BERT的中间层同样包含丰富的语义信息。常见的做法是取最后四层的输出做拼接或加权平均再把结果喂给分类器。有人觉得这样会让向量维度膨胀确实最后四层拼接的话768维就变成3072维但接一个线性层把维度压回类别数计算量增加并不多在文本分类这种场景里完全可接受。我做项目X的时候遇到过一个小样本分类问题总共只有四五百条标注数据直接用最后一层CLS只有72%的准确率换成最后四层加权池化后提到了76%。原因也好理解小样本微调时高层向量更容易发生过拟合多层融合等于拿更丰富的中间特征做缓冲。当然这个方案也不是百试百灵如果数据集本身很大差异会缩小这时就更倾向于用便宜的做法。4. 把BERT接到分类器微调与超参数实操前面讲的都是模型本身怎么理解一句话真正落地到文本分类任务时还需要一个分类头还要处理微调这件事。很多入门同学在这一步犯的错误是把微调想得太复杂一上来就搞各种奇技淫巧结果反而不稳定。我的经验是先按最朴素的方式搭起来再根据具体问题做针对性的调整。4.1 分类头别一上来就做复杂结构所谓分类头其实就是BERT顶上接出的那一层。最常见的做法是把CLS位置的向量拿出来经过dropout然后接一个线性层线性层输入维度是BERT的hidden_size输出维度是类别数。以BERT-base为例就是768维映射到N类N你定。如果类别数非常多比如几千个标签的多标签分类任务可以在这个线性层后面再加一个sigmoid而不是softmax多标签场景下每个标签都要独立判断二分类概率。这个细节看着简单但忘了改激活函数指标会比预期差不少。有没有必要在这个头里加一层隐藏层我的答案是可以试但别拿第一版项目冒险。一个隐藏层最多加到256维或者128维再做输出能带来一定的非线性表达能力。但也会引入额外的过拟合风险。我做文本分类的默认做法是单层线性头dropout设0.1到0.3之间先跑通。只有数据量足够大比如几万条起步我才会认真考虑加深分类头。4.2 学习率、max len、batch size怎么定微调BERT最典型的特征是预训练已经把模型参数带到了一个很好的位置微调时步子要小。BERT官方推荐的微调学习率在2e-5到5e-5之间这跟训练一个普通神经网络动辄1e-3的学习率有数量级差异。从实际表现来看学习率给大了损失下降速度看上去很快但验证指标往往在震荡中停滞甚至直接发散。我说一个常见的组合适用于大多数中文文本分类任务参数推荐值我的备注学习率2e-5到5e-5做一次小范围扫描先选3e-5batch size16或32显存不够就减batch配合梯度累积max len128到512之间太短会截掉关键信息太长显存受不了epoch数3到10必须配合早停多看验证集warmup比例前10%的step让模型先平稳进入状态weight decay0.01到0.1对长文本数据效果好max len这个参数有个很常见的误用。有些人为了追求信息完整直接把max len设成512导致显存吃紧训练速度大幅下降。实际上如果你的数据平均长度只有100到150个字那max len设成128就够了。如果必需要长文本可以先做一个长度统计看看90分位点在哪个范围再决定截断阈值。不要拍脑袋选一个很大的值要让数据告诉你答案。4.3 过拟合控制与调试心得微调BERT尤其是在小数据集上微调过拟合现象来得非常快。有时候到第二个epoch训练loss还在降验证集指标已经在下滑了。很多人第一次观察到这个现象时觉得模型出了问题其实这是BERT微调的正常表现预训练模型容量大标注数据少根本没有机会把分布外的噪声彻底学完之前就已经开始记住了。面对这个情况有几个我经常用的方法一是早停这是性价比最高的。设定一个忍耐区间比如连续3个epoch的验证集指标不再提升就停止训练并恢复最佳checkpoint。二是冻结底层参数新数据量特别少时可以把BERT前几层冻结只微调后面几层和分类头这样大幅减少可训练参数量过拟合风险明显下降。冻结到底从第几层开始要根据任务调一般从第8层往后保留就行。三是分层学习率分类头用相对更“大”一点的学习率BERT主体用更小学习率用一个参数区分比如分类头的5e-4和BERT的1e-4分开设置。小样本项目还有一个很实用的技巧用对抗训练或者时序性增强的方式制造更多变体。比如在Embedding层加一点随机噪声或者对样本做同义词替换扩充。这个办法不能保证每轮收益都明显但在某些对措辞高度敏感的分类任务里稳定性能确实能提升不少。5. 常见问题与排查思路做文本分类尤其是把BERT接到自己的业务数据上时几乎每天都要面对各种报错和效果问题。这里把我在多个项目里踩过的坑整理一下按常见程度排序方便你快速对照。5.1 样本太少、标签不均衡、长文本怎么办第一个高频问题数据量很少只有几百条模型怎么都过拟合。这个场景非常常见比如做某个垂直行业的意图分类标注成本太高能拿出手的数据极其有限。这种情况下不要直接硬训整个BERT推荐的做法有三种一是冻结BERT主体只训练分类头把BERT当成一个特征抽取器二是用一些开源的大模型接口先做数据增强比如同义改写三是干脆换用更小的预训练模型比如6层的小模型或者蒸馏版本参数少很多小数据上反而更稳。第二个高频问题标签分布严重不均衡。新闻分类里体育类的样本可能几万条艺术类只有几百条。直接上交叉熵会让模型把所有样本都推向多数类。解决方案不外乎几类用类别权重加权交叉熵给小类更大的损失权重用Focal Loss降低易分样本的权重让模型更关注难分样本或者反过来对小类做上采样、对大类做欠采样作为简单的数据层处理。我的实测感受是加权交叉熵改动最小、收到效果也快Focal Loss要看任务类型在标签噪声较大的场景里往往效果更好。第三个高频问题长文本怎么办。BERT的输入长度上限是512个token但很多业务文本比如风控领域的合同条款、医疗领域的病历动不动就超长。我处理这类问题时通常考虑两个方向一是分段截取把长文本切成多个片段每个片段分别过BERT得到各自的CLS向量再做平均或加权汇总二是只抽取关键区域比如用句子的Attention分数或者TextRank选最相关的几句再把这几句拼起来放入BERT。后者在主题集中的长文本上效果更明显。5.2 训练异常与显存不足的定位方法先说训练不收敛的问题。BERT微调很少出现不收敛因为预训练权重已经很强了通常“不收敛”都是配置错误。先检查分词结果看看特殊token有没有被错误截掉再看attention_mask是否正确尤其是多段拼接时mask没处理好会让模型把填充符也当成有效内容表现就是loss剧烈震荡。如果这些都没问题再看数据标签是不是有噪声我遇到过一次验证集乱跳最后发现是标注样本里有几条标签标反了。再说显存不足OOM。模型装不下的第一反应是减小batch size但如果小到4甚至2BN等结构可能就不稳定了。更好的组合方案是batch size维持在8或16梯度累积次数设成4或8让等效batch size保持不变同时把max len尽可能压到数据允许的下限再加上混合精度训练fp16显存占用一般能砍掉一半左右。我用某跨平台系统做长文本分类时这几个优化一起上单卡显存占用从接近爆掉降到了66%左右训练速度还快了不少。还有一个经常被忽略的点显存不足不一定是模型太大也可能是数据加载环节出了问题。比如某个字段没做截断某几条异常长文本被直接塞进了batch导致瞬间显存峰值爆掉。排查方法是打印每个batch输入的最大长度看是否超出预期。我曾经排查过一个问题就是数据里有几条几万字的纯文本tokenizer之后长度远超max len但因为我忘了做长度截断处理某几个batch直接把显存顶穿。写在最后的小经验BERT做文本分类这件事我最大的体会是它是一个极其稳定的老江湖你不需要玩出太多花活把数据清洗干净、把超参设置合理、把过拟合控制住效果就能超过绝大多数非预训练方案。如果你碰到某个数据集上效果不如预期先不要怀疑模型不行多数时候是数据输入的头没开好或者标签噪声超出了合理范围。文本分类这个方向BERT哪怕放到今天也不是过时方案尤其在数据量不算特别大的业务场景里它的性价比依然很高。后面中篇我会继续写微调时的更多进阶技巧以及如何把BERT模型收得更小、跑得更快下篇再聊蒸馏和部署层面的实战心得。如果你正准备做自己的文本分类任务可以先从BERT基线开始顺着这篇文章的思路把流程完整跑一遍再说。

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

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

免费获取报价 →
↑