目录
1.模型架构
2.BERT Tokenizer
3.预训练任务
3.1.MLM Task
3.2.NSP Task
3.3.动态学习率
4.各种类Bert模型
5.下游任务
5.1.文本分类
5.2.文本关系分类
5.3.序列标注
5.4.多任务学习
扩展阅读:https://kexue.fm/archives/8747
论文:https://arxiv.org/pdf/1810.04805
BERT (Bidirectional Encoder Representations from Transformers) 是一种Encoder-Only架构的预训练模型,它的出现标志着深度学习攻克了语义理解的堡垒。在进行了大规模的预训练之后,BERT学习到了非常好的语义表征(特征)。在预训练模型(特征)的基础上,继续通过微调训练下游任务的模型 (文本分类、命名实体识别、阅读理解等)。通过“预训练+微调”的,只需要相对少量的微调训练数据(千条的量级)就可以得到非常好的下游任务的效果。
BERT一经推出,迅速占领了各种自然语言理解 (NLU) 榜单的榜首位置,但在自然语言生成 (NLG) 方面还没有突破。
1.模型架构
BERT模型架构是Encoder-Only Transformer,模型输入可以是一个句子或多个句子。在对文本进行了分词 (Tokenization) 之后,第一个句子的token之前会被插入一个特殊token "[CLS]",用于表示这一段文本的特征。在每个句子的尾部会插入另外一个特殊token "[SEP]",用于分割句子。
BERT的输入特征由三个分量相加得到:
Token Embedding:每个token都训练一个嵌入向量 (Embedding Vector),表示token的内容特征。
Segment Embeddings:每个句子被赋予一个segment id,同样训练嵌入向量。
Position Embeddings: 每个token在输入中的位置也被表示为嵌入向量。BERT的位置编码基于学习的,在0~MaxLength之间的每个整数id,对应一个可学习的嵌入向量。
以上三种嵌入向量是等长的。对于每个位置,三个向量的和即为BERT编码器的输入。
BERT的模型架构即为Transformer Encoder。BERT训练了不同大小的预训练模型,它们的架构和参数量分别为:
BERT Base:12 Transformer Encoder Blocks,隐藏层特征向量的长度为768,12个自注意力头,参数量为110M (1.1亿)。
BERT Large:24 Transformer Encoder Blocks, 隐藏层特征向量的长度为1024,16个自注意力头,参数量为340M (3.4亿)。
模型结构+计算逻辑 与 模型权重文件
一、模型结构 + 计算逻辑
1. 模型结构
模型结构是神经网络的架构定义,规定网络由哪些基础组件构成、组件之间如何连接、各层维度配置。 它描述网络的“骨架”,不包含任何学习到的知识。 以Transformer大模型为例,结构定义内容包括:
基础模块:词嵌入层、位置编码、多头自注意力模块、前馈神经网络、层归一化、残差连接;
超参配置:网络层数、注意力头数量、隐藏层大小、词表维度等。
2. 计算逻辑
计算逻辑定义数据在网络中运算的规则,分为前向推理和反向训练两套逻辑:
前向计算(推理阶段):输入数据按照结构定义的流向,逐层做矩阵运算,最终输出预测结果。这是模型回答、生成内容时执行的计算流程。
反向传播(训练阶段):对比模型预测和真实标签,计算损失值,通过链式法则求梯度,用来更新参数。
结构+计算逻辑,本质是一套固定的数学运算框架。仅有框架,没有参数,输入数据只会输出随机值,模型无法完成任务。
二、模型权重文件(训练结果)
权重文件是模型训练完成后保存的可学习参数集合,文件后缀常见
.bin、.safetensors、.ckpt、.pth。
内容:全部可学习参数,包括各层权重矩阵、偏置项(bias)等大量浮点数;
来源:在海量数据集上,依靠反向传播迭代不断调整数值,把文本、图像等知识编码进这些数字中;
作用:为计算公式提供具体数值,是模型学到知识的载体。
权重不能脱离对应的模型结构使用。架构不匹配时,权重无法加载;相同结构,替换权重文件,就会得到能力不一样的模型(基座模型、微调模型)。
三、二者关系
模型结构+计算逻辑:定义怎么算,属于算法、代码、配置层面;
权重文件:提供代入计算的参数值,是训练产出的实体文件;
模型完整运行 = 加载模型结构 + 载入权重参数,执行前向计算。
阶段区分
训练:固定模型结构,持续迭代更新权重数值;
推理:结构固定,载入训练好的权重,只运行前向计算。
通俗类比
模型结构+计算逻辑 = 计算器的硬件电路设计与运算规则; 权重文件 = 提前录入到计算器里的知识库。 电路不变,更换知识库,计算器能解决的问题就变了。
四、补充概念
微调:不修改模型结构,只在原有权重基础上,用少量数据继续更新部分权重,让模型适配特定场景(下游任务);
量化:不改动网络结构,降低权重数值精度(如FP16转为INT8),缩小权重文件体积,加速推理;
开源模式:可以单独开源模型结构,闭源权重;也可以结构、权重全部开源。
2.BERT Tokenizer
BERT Tokenizer (分词器) 所输出token的粒度是小于词的。对于中文,每个汉字作为一个token;对于英文,BERT采用的是WordPiece Embeddings,一种基于子词 (subword) 的表示方法,vocab大小为30k (3万)。
采用子词表示的原因是:英文中的词频分布非常长尾,有大量平时不常使用的词。如果在vocab中加入长尾词,就会使得模型复杂度迅速增大,容易导致过拟合的情况;如果忽略长尾词,又会提高未登录词 (Ouf-of-Vocabulary, 简写为OOV) 出现的概率。将文本拆分为子词可以很好地解决这个问题。
WordPiece tokenizer将文本切分为subword,为了能从token sequence没有歧义地恢复出原文本,定义一个特殊的字符表示词边界。以下是一个将文本切分为WordPiece序列的例子。
WordPiece选取subword的算法是数据驱动的,需要准备一个训练语料库,并确定期望的subword vocab的大小。先将单词拆分为最小单元,比如英文中26个字母再加上各种符号,这些作为初始词表。之后逐步合并当前vocab中的子词。假设句子由n个子词组成,且假设各个子词的出现是独立的 (当然这个假设和实际不符,是提高提高计算效率而作出的简化),那么句子S的对数似然概率可以表示为:
如果我们将相邻的两个子词和
相合并,合并后产生的子词为
,那么此时句子S对数似然概率的变化为:
WordPiece取增益最大的子词进行合并,即将添加到vocab中。如果合并之后,在训练语料中不在出现
或
了,则将其从vocab中删除。算法不断地选择增益最大的相邻子词对进行合并,直到vocab的大小达到预期为止。
from transformers import BertTokenizer, AutoTokenizer import torch model_path = "/Users/wangruonan/Documents/Code/SF130/深度学习/model/bert-base-chinese" # 两种加载分词器方式:BertTokenizer专用于Bert,AutoTokenizer自动识别模型类型,通用推荐 # tokenizer = BertTokenizer.from_pretrained(model_path) tokenizer = AutoTokenizer.from_pretrained(model_path) # 测试文本 texts = [ "张翰现身上海机场,普通搭配穿出不一样的风格 网友:走清秀风了", "老梁:高考状元放弃72万元奖学金,而去选择复读,这到底是为什么", "《奥特曼空想特摄》游戏解说 奥特曼空想特摄,游戏", "火箭晋级西决赛后,巴克利称:火箭无法击败勇士,最多5场比赛解决战斗,你怎么看?" ] # ===================== 1. tokenize:原始文本切分为token子词列表 ===================== # 将文本分割为token text = texts[0] tokens = tokenizer.tokenize(text) print(tokens) # ===================== 2. encode:token转为token id,自动添加[CLS][SEP]特殊token ===================== # 将token转换为id, 并添加特殊token token_ids = tokenizer.encode(tokens) print(token_ids) # ===================== 3. decode:token id还原回可读字符串 ===================== # 将id转换为token tokens = tokenizer.decode(token_ids) print(tokens) # ===================== 4. 分词器 __call__ 方法(项目最常用方式) ===================== # 批量处理文本,固定最大长度64,不足长度补padding,返回PyTorch张量tensor格式 inputs = tokenizer(texts, max_length=64, padding='max_length', return_tensors="pt") # return_tensors="pt" 指tensor是pytorch格式的tensor print(inputs) # 输入一个句子的情况 [CLS] sentence [SEP] # 批量文本编码:开启截断,padding按批次最长句子补齐,pad_to_multiple_of=8 保证维度是8的倍数(GPU加速优化) inputs = tokenizer(texts, max_length=512, truncation=True, padding='longest', pad_to_multiple_of=8, return_tensors="pt") print(inputs) # 打印input_ids张量形状:[batch_size, seq_len] 批次大小 × 序列长度 print(inputs['input_ids'].shape) # 输入一对句子的情况 [CLS] sentence1 [SEP] sentence2 [SEP] # BERT句子对格式: [CLS]句子1[SEP]句子2[SEP] texts_1 = texts[0:2] texts_2 = texts[2:] # 传入两组文本,构造句子对输入,自动拼接+添加特殊标记 inputs = tokenizer(texts_1, texts_2, max_length=512, truncation=True, padding='longest', pad_to_multiple_of=8, return_tensors="pt") print(inputs) # 打印句子对编码后张量shape print(inputs['input_ids'].shape)
3.预训练任务
BERT最大的贡献就是证明了通过预训练,可以学习出"普适“的语义特征,因此对于各种特定的下游任务,只需要进行简单微调就能得到理想的效果。预训练任务的设计需要同时满足以下要求:
训练数据和训练标注容易获得。
训练任务依赖语义特征的学习。
BERT设计了两种预训练任务,即Masked Language Model (MLM) 和 Next Sentence Prediction (NSP),分别用于学习token级和句子级的语义。
3.1.MLM Task
很多语言模型都是单向的,Left-to-Right占多数 (Left-to-Right和Autoregressive是一个意思,都是用过去预测未来)。在Left-to-Right LM中,每个token都只能"看到"它之前位置的tokens,Left-to-Right LM的损失函数为:
直觉上来说,单向语言模型的建模能力应该不如双向的,因此BERT希望采用双向的语言模型任务,即基于来预测
。但由于双向上下文建模,每个token实际上都能“看到"
的信息,这样就会出现“自己预测自己”的问题。
BERT随机选取一些token (15%的比例) 作为预测目标。为了避免“自己预测自己”的问题,目标token所在的位置会被替换为"[MASK]"。"[MASK]"会代替原token与句子里的其他token进行交互 (self-attention的过程),在经过FFN的变换,最后计算得到最后一个隐藏层的特征表示,这个特征表示再用于预测当前位置的token。
MLM任务的损失函数为:
在实际训练中,BERT随机选择15%的token进行预测。在这15%的token中,又随机选择其中的10%随机替换为其他token,10%保持不变,80%替换为"[MASK]"。据说这样做是为了减少预训练和微调时模型输入的不一致性 (微调时模型输入的都是正常句子),但也有研究结果显示这样做和直接替换为"[MASK]"区别不大。
token预测被建模为一个多分类任务,类别数等于vocab的长度,输出层的每个节点代表一个特定的token,输出层激活函数为Softmax,损失函数为交叉熵。
MLM Task(掩码语言模型任务)
一句话先概括:MLM就是给句子里随机挖掉一些词,让模型结合整句话的上下文,猜出被挖掉的词是什么。BERT就是靠这个任务训练出来的。
1. 对比一下,看懂它特殊在哪
普通单向模型(比如GPT):读句子只能从左往右看。
例子:
这朵花是红的读到“这朵花是”,只能靠前面几个字,预测下一个字“红”,看不到后面“的”这个字。
MLM不一样,是双向看。 把句子里部分单词盖住,模型猜词的时候,既能看左边的文字,也能看右边的全部文字,完整读懂整句话的信息。
2. 具体怎么“挖词”
训练时,随机挑句子里15%的单词进行处理,分3种情况:
80%情况:直接换成
[MASK](掩码标记,相当于空白遮挡)原句:这朵花是红的 → 处理后:这朵花是[MASK]的 任务:让模型猜出这个[MASK]位置原本是“红”
10%情况:随机替换成别的无关单词
原句:这朵花是红的 → 变成:这朵花是黄的
10%情况:单词保持原样,啥也不改
👉 为什么不能全部都换成
[MASK]?防止模型耍小聪明:只记住“只要看到符号
[MASK]才需要猜”。混入替换、不变的单词,强迫模型认真理解每个词和句子逻辑,而不是识别标记符号。
3. 训练目标
模型拿到带[MASK]的句子,读完前后所有文字,输出一个结果:这个空位,是各个单词分别有多大概率。
我们对比模型猜出来的概率和真实单词的差距,不断调整模型,越猜越准。
4. MLM的核心作用
训练完之后,模型就学会了双向理解上下文。
比如一句话“银行开门了”,它能结合前后文字分清:是存钱的银行,还是河岸,不像单向模型只能看单侧文字,理解能力弱。
极简一句话总结
MLM任务:随机遮住句子里少量单词,逼着模型同时看句子前后所有内容,猜被遮住的词,以此训练模型双向理解语言。
输出层和输入层的参数共享
MLM 任务在预测token时,输出层节点数与输入层Token Embedding的数量是相同的,Bert对其进行了参数共享。输出层的每个节点对应于一个token,与其连接的权重与该token的embedding vector参数是共享的,不过Embedding层的参数会乘以一个大小为的缩放系数,使得Token Embedding相对于Segment Embedding和Positional Embedding在幅度上占据优势。
先搞懂两个基础角色:
- 输入层 Embedding(词嵌入):把文字单词,转换成一串数字向量,方便模型计算。比如单词
苹果→ 变成一组数字。- 输出层:MLM 任务最后一步,拿到向量,反过来预测:这个 [MASK] 位置到底是哪个单词,输出每个单词的概率。
参数共享 = 输入、输出共用同一套 “转换规则”
正常思路:
单独做一套规则,把文字转数字(输入);再另外造一套全新规则,把数字转回单词(输出)。两套独立参数,参数量翻倍,模型更大,训练更占内存。
BERT 的做法:
输入 “文字→数字” 用的那套权重参数,直接拿过来,给输出层 “数字→文字预测” 用,只加一个小小的缩放系数。
✅好处:
- 减少模型总参数量,模型更小、训练更快,省显存;
- 约束向量空间:单词转出来的向量,和预测单词时用的向量是同一个空间,学到的表示更稳定。
举个生活化类比:
就像一本字典。
输入阶段:查字典,把汉字翻译成编号向量;
输出阶段:还是同一本字典,用编号反向查找对应的汉字。
不用单独再编一本反向字典,省了工作量,两本字典含义还能保持一致。
一句话总结:
输入把词转向量、输出用向量猜词,共用同一套参数,省空间还统一向量含义。
标签平滑(Label Smoothing)
MLM任务的损失函数是交叉熵。交叉熵损失函数度量了分类任务的目标分布与模型预测的类别分布之间的差距。目标分布一般是一个one-hot vector,此时交叉熵等同于负对数似然概率函数。
在预测token时,目标类别是基于训练样本中的实际文本来设置的。我们来比较下边两个句子:
句子1: 这 朵 花 是 红 的 。 句子2: 这 朵 花 是 黄 的 。如果我们将句子进行随机MASK,输入到模型的token序列为:
这 朵 花 是 [MASK] 的 。
那么,对于句子1,预测目标是“红”,而“黄”所对应的概率输出目标为0;对于句子1,预测目标是“黄”,那么“黄”所对应的概率输出目标为1。这种情况对于参数优化是不好的,因为预测值无论靠近哪一边,都会引起另外一边非常大的损失值 (对数函数的输入接近0时,输出接近负无穷)。
标签平滑的方法非常简单:将目标token的概率 (1.0)中分出一小部分概率,并将其平均分到每一个预测token上。
例如,假如类别数为5(当然实际不可能这么小),目标类别在向量中的序号为2,那么原本的目标分布为:(0.0, 0.0, 1.0, 0.0, 0.0)。标签平滑从1.0分出一小部分概率(例如0.1),将其均分到每一个类别上,那么目标分布将变为 (0.02, 0.02, 0.92, 0.02, 0.02)。如果模型预测的概率分布为 (0.05, 0.1, 0.75, 0.05, 0.05),那么损失函数计算为:
当预测目标不唯一 (或带有噪音)的情况下,标签平滑能够提高模型训练的效果。
没有标签平滑(原始 one-hot 标签)
举例子:句子“”
这朵花是[MASK]的”,真实答案是红。原始标签设定:
“红” 的概率 = 100%,剩下所有单词(黄、蓝、绿……)概率 = 0%
相当于强制告诉模型:答案绝对唯一,其他所有词完全不可能。
❌缺点:
模型会学得特别极端、过度自信。只要预测不是 100% 红,损失就巨大;
现实语言里很多词本身有模糊性,强行 “非黑即白” 容易过拟合。模型死记硬背训练集,遇到真实陌生句子效果变差。
标签平滑怎么做
不再把标准答案设置成 100% 和 0%,抠一点点概率分给其他候选词。
还用例子,候选词一共 5 个:红、黄、蓝、绿、黑。
平滑之后:
红:92%,黄、蓝、绿、黑,各 2%
也就是告诉模型:大概率是红,但不能完全排除其他一点点可能性。
✅好处:
防止模型过度自信,训练的时候损失变化更平稳;
提升模型泛化能力,遇到文本有噪声、歧义的句子,预测更稳;
缓解过拟合,模型不会死记训练样本。
一句话总结:
训练时不要设置 “答案百分百确定”,分一点点概率给其他词,避免模型学太死、太自信。
3.2.NSP Task
NSP (Next Sentence Prediction,下一句预测任务) 将一对句子作为输入,预测这两个句子是否为文本中连续的两个句子,是一个二分类任务(若两个句子是文本中连续出现的,则NSP标签是1;若两个句子不是文本中连续出现的,则NSP标签是0)。由于在实际文本中衔接的两个句子,在语义上具有相关性,因此NSP任务主要是为了学习句子级的语义特征。
MLM和NSP任务的损失函数加在一起,就是BERT预训练的总损失函数。输入的文本为经过MASK的两个句子,MASK的比例为15%,NSP任务的正负样本比例为1:1。
1. 任务怎么玩
这是个简单的二选一判断题:
标签 = 1 ✅:两句在原文里是连续挨着的。
例:
句子 A:小猫蹲在草地上。
句子 B:它正盯着飞过的蝴蝶。
A 说完紧接着就是 B → 标签 1
标签 = 0 ❌:两句不是原文连续的,是从别的地方随便揪出来拼在一起的。
例:
句子 A:小猫蹲在草地上。
句子 B:今天外卖很好吃。
这两句完全不衔接,硬凑一对 → 标签 0
训练的时候,正负样本 1:1,一半是真连续句子,一半是乱拼的句子。
2. 为什么 BERT 要做 NSP?
MLM 是学单个句子内部词语之间的关系(猜被遮住的词)。
NSP 是逼着模型学会两个句子之间的关系,理解句子和句子之间的逻辑、上下文。
目标:让模型看懂段落级别的语义,明白上下句是不是连贯的,方便后续做问答、句子匹配这类任务。
3. 和 MLM 的关系
BERT 预训练是MLM + NSP 两个任务一起训练,两个任务的损失加在一起,作为总的损失。
一边做挖词猜词(MLM),一边做判断两句是否连续(NSP),双线学习。
4. 小提示
后来很多研究发现 NSP 这个任务效果有限,现在很多新版本 BERT 会去掉 NSP。但原始 BERT 的预训练,它是核心任务之一。
极简一句话总结
NSP 任务:拿一对句子,训练模型判断第二句是不是原文紧跟第一句的那句话,用来学习句子与句子之间的语义关系。
用同一套特征去做了两套预测任务(BERT的MLM+NSP)
简单一句话:BERT编码器提取出来的那一组句子特征(向量),不单独造两套特征,直接拿去同时干两件事:猜被遮住的词(MLM)、判断两句是不是连续的(NSP)。
拆开讲
1、先提取公共特征
把一对句子输入BERT,经过Transformer编码器计算,得到一整套数字向量(也就是句子的特征)。
这一步只算一次,只生成一套特征,不是MLM算一套、NSP另外再单独算一套。
2、一套特征,分流给两个任务
第一路(MLM掩码任务):拿这套特征,去预测每个被
[MASK]盖住的位置原本是什么单词;第二路(NSP下一句预测):拿这套特征里
[CLS]位置的向量,去判断第二个句子是不是原文紧跟第一句的那句话。3、训练一起优化
两个任务各自算出自己的误差(损失),把误差加在一起,再反过来调整BERT整套参数。
BERT在训练时,会同时学习两种能力:
① 句子内部词语之间的关系(MLM)
② 句子和句子之间的上下文关联(NSP)
生活化类比
好比你读一段文字,只读一遍(只提取一次信息),同时做两道题:
题1:填空,猜出文中被遮住的词语(MLM)
题2:判断题,判断第二段是不是紧接着第一段写的(NSP)
你阅读得到的理解(特征)是共用的,不用重新再读一遍文章去做第二道题。
好处
高效:只需要一次前向计算,不用重复提取特征;
学到更全面的语言表达:模型同时掌握词级、句子级两种语义信息。
补充:图里也能看到,Pre-training预训练阶段,同一个BERT输出,同时接到Mask LM和NSP两个预测头。
BertPooler 的输出,就是 BERT 做 NSP(Next Sentence Prediction,下一句预测)任务分类头的输入。
调loss.backward()等同于先调loss1.backward(),再调loss2.backward() ——多任务训练。
import torch from transformers import AutoTokenizer, BertForPreTraining import numpy as np model_name = "/Users/wangruonan/Documents/Code/SF130/深度学习/model/bert-base-chinese" tokenizer = AutoTokenizer.from_pretrained(model_name) model = BertForPreTraining.from_pretrained(model_name) print(model) # 开始准备训练数据 texts_1 = [ "张翰现身上海机场,普通搭配穿出不一样的风格 网友:走清秀风了", "老梁:高考状元放弃72万元奖学金,而去选择复读,这到底是为什么", "梯度检查点在上述两种方式之间取了一个平衡。" ] texts_2 = [ "《奥特曼空想特摄》游戏解说 奥特曼空想特摄,游戏", "火箭晋级西决赛后,巴克利称:火箭无法击败勇士,最多5场比赛解决战斗,你怎么看?", "它通过在反向传播过程中重新计算某些输出的方式减少显存的使用量。" ] # nsp labels nsp_labels = torch.LongTensor([0, 0, 1]) # 处理mlm数据 inputs = tokenizer(texts_1, texts_2, max_length=512, truncation=True, padding='longest', pad_to_multiple_of=8, return_tensors="pt") mask_prob = 0.15 mask_id = tokenizer.encode("[MASK]", add_special_tokens=False)[0] # mask采样 input_ids = inputs['input_ids'] #print(input_ids) mask_probability_matrix = torch.zeros(inputs['input_ids'].shape) + mask_prob special_tokens_mask = [tokenizer.get_special_tokens_mask(id, already_has_special_tokens=True) for id in input_ids.tolist()] special_tokens_mask = torch.tensor(special_tokens_mask, dtype=torch.bool) mask_probability_matrix.masked_fill_(special_tokens_mask, value=0.0) masked_indices = [(1 if np.random.rand() < prob else 0) for prob in mask_probability_matrix.reshape(-1).tolist()] masked_indices = torch.tensor(masked_indices, dtype=torch.bool).reshape(mask_probability_matrix.shape) # 生成MLM task的labels mlm_labels = input_ids.clone() mlm_labels[~masked_indices] = -100 # 替换input_ids中的token input_ids[masked_indices] = mask_id #print(inputs['input_ids']) #print(mlm_labels) # forward process output = model(input_ids=input_ids, token_type_ids=inputs['token_type_ids'], attention_mask=inputs['attention_mask'], labels=mlm_labels, next_sentence_label=nsp_labels) print(output.loss) output.loss.backward() # 更新参数 optimizer = torch.optim.Adam(model.parameters(), lr=1e-5) optimizer.step() optimizer.zero_grad() output2 = model(input_ids=input_ids, token_type_ids=inputs['token_type_ids'], attention_mask=inputs['attention_mask'], labels=mlm_labels, next_sentence_label=nsp_labels) print(output2.loss)
3.3.动态学习率
学习率是深度学习训练超参数,需要预先指定。动态学习率是指训练过程中的学习率不是固定不变的,而是随着training steps (迭代步数) 的增加进行调整。一般地,在训练初期需要较大的学习率加速收敛,而在训练后期需要较小的学习率来避免在极小点附近震荡。因此我们需要设置一个初始学习率,然后在训练过程中逐步衰减这个学习率。
常用的学习率衰减策略有以下几种:
1、基于步长的衰减 (StepLR):每隔一定的迭代步数,将学习率乘以一个衰减系数 (gamma),例如:
import torch import torch.optim as optim # 定义优化器 optimizer = optim.Adam(model.parameters(), lr=0.01) # 定义学习率调度器 scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=100, gamma=0.5) # 训练过程 for epoch in range(epochs): for batch_x, batch_y in dataloader: optimizer.zero_grad() train(...) optimizer.step() scheduler.step() # 动态调整学习率, shceduler内部的step计数器+1, # 根据新的step为optimizer设置学习率思考:如果要按epoch数调整学习率该如何设置?
2、线性衰减 (Linear Decay)
学习率从初始值initial_lr线性降低到一个较小的终值end_lr(甚至接近0),第t个step的学习率计算公式如下:
其中T为总的迭代步数。
import torch.optim as optim from torch.optim.lr_scheduler import LambdaLR optimizer = optim.Adam(model.parameters(), lr=0.001) # 定义线性衰减函数 def linear_decay(t, total_steps, initial_lr, end_lr=1e-5): return (1 - t / total_steps) * initial_lr + (t / total_steps) * end_lr scheduler = LambdaLR( optimizer, lr_lambda=lambda t: linear_decay(t, total_steps=1000, initial_lr=0.001, end_lr=1e-5) ) # 每个batch后调用 scheduler.step()3、余弦退火 (Cosine Annealing)
学习率按余弦函数周期性调整,公式:
其中T为周期长度,t为当前步数。由于余弦函数的周期性,如果t超出了T,学习率会被重置到初始学习率的水平。
可以直接使用torch.nn.lr_scheduler.CosineAnnealingLR
scheduler = CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-6)学习率的预热 (Warm-Up)
不过,当模型参数是随机初始化时,在训练开始时就使用较大的学习率,可能会导致训练的不稳定。在预训练模型的训练过程中,研发人员发现在训练开始时使用较小的学习率,随着训练的进行,学习率逐渐增加到预设的初始学习率,然后再按照学习率衰减的策略进行调整。
BERT的预训练过程使用了线性Warmup,即学习率从0线性增加到预设的学习率 (细节在原论文附录A.2中)。在前10%的训练steps中,学习率从0按迭代步数线性增加到预设的学习率;之后再线性逐渐衰减到0。这个过程中的学习率变化曲线如下图所示。
4.各种类Bert模型
除了google发布的Bert系列模型之外,还有很多Transformer Encoder-Only架构的预训练模型,它们也被统称为“类Bert模型”。这些模型的架构相似,都用Transformer Encoder来计算特征,用[CLS]位置上的特征来表征整个文本。但各个公司(组织)预训练的数据和预训练任务不同,在网络结构上也会存在微小的不同。
1、RoBERTa (A Robustly Optimized BERT Pretraining Approach, paper: https://arxiv.org/pdf/1907.11692),是由FaceBook发布的一种类BERT模型,主要优化有:
- 使用了更大的训练数据集 (Wikipedia, Common Crawl, OpenWebText, BooksCorpus等),总数据量达到了160GB,远大于BERT的训练数据量 (16GB)。
- 采用了动态掩码策略,每次训练时随机选择掩码单词。增加了训练任务的多样性。
- 去掉了NSP这个预训练任务,RoBERTa的实验表明NSP任务没有太大用处。
- 更大的微批大小 (2K ~ 8K),而Bert预训练的微批大小为256。这相当于提高了模型训练的数据吞吐量,对提高模型效果有明显影响。
- RoBERTa-wwm (whole word masking)是一种RoBERTa的改进模型,掩码是针对词的,即一次可能会mask多个token。这种训练方式能够促使模型更好地学习上下文特征。
2、DistillBERT (paper: https://arxiv.org/pdf/1910.01108) 是一种通过知识蒸馏技术从原始BERT模型中提取知识的方法,旨在创建一个更小、更快的模型,同时保留原BERT模型的大部分效果方面的性能。DistillBERT蒸馏算法的要点:
层数减半与初始化:DistillBERT将BERT的层数减半,并从教师模型的层中初始化学生模型层的参数。
蒸馏函数的设计:包括交叉熵损失和向量相似度损失。交叉熵损失用于衡量教师模型和学生模型输出概率分布之间的差异,而向量相似度损失则用于对齐两个模型输出的隐藏向量。
DistillBERT适合在资源有限的设备上运行,例如普通的笔记本电脑或手机。但似乎没有官方的DistillBERT中文模型。
3、BGE系列:专注于Sentence Embedding的预训练模型,是由智源研究院开源的系列模型 (Github project: https://github.com/FlagOpen/FlagEmbedding)。
此外还有在通用预训练模型的基础上,进一步用垂直领域数据(例如:金融,医疗,中医等等)训练的垂域大模型,在此就不列举了。
5.下游任务
BERT的贡献在于证明了预训练+微调的学习范式的先进性。业务中的算法工程师一般不会从头进行预训练,而是直接利用预训练模型。预训练模型的权重文件编码了语义特征,因此,使用预训练模型初始化参数,能够极大减小下游任务所需要的训练数据量,加快收敛速度。
接下来我们来学习如何基于BERT预训练模型完成下游任务。BERT的下游任务大多是各种分类任务,包括序列分类和序列标注任务。
5.1.文本分类
模型的输入是一段文本 (一个“句子”),输出为一个类别标签。使用[CLS]位置上的输出 (最后一层隐藏层的输出)作为分类特征。常见的文本分类任务有:
- 意图识别:在对话系统中,将用户可能的输入预先划分为有限个“意图”,每个意图就是一个类别。在服务过程中,识别出用户的意图之后,就会将对用户输入的处理流程分发到对应的模块中。
- 情感分析 (Sentiment Analysis):识别用户评论的情感“极性”,如正向、负向和中性等,这些标签可以作为后续商品推荐的特征(以及理由)、业务分析、风险管理以及舆情监控的数据。
- 新闻分类:将新闻根据领域打上类别标签,作为内容推荐的特征等等。
- 企业内部的文档分类:根据产品线、业务线、流程等标签进行分类,作为文档管理、搜索等应用的特征。
5.2.文本关系分类
文本关系分类任务的输入是一对文本(即两个“句子”),通常是二分类任务。这一对“句子”在不同的应用里会有不同的定义。
- 搜索:第一个“句子”是用户query (或问题),第二个“句子”是候选文档(或文段)。如果两者是相关的,则标签为1,否则标签为0。
- FAQ:两个“句子”均为question,判断它们是否是同一个问题,如果是同一个问题,则可以调取预先写好的回答。
- 语义相似度:判断两段文本的语义是否相同或相似 (具体定义取决于应用场景),可以用于语义去重。
文本关系分类有两种方式:
- 将每个文本分别编码,即将它们分别输入BERT (每次输入一个文本),分别取[CLS]位置的输出作为它们的特征表示 (表征),再通过计算这两个向量之间的余弦相似度判断它们的语义关系。
- 同时将一对文本输入BERT,格式为 [CLS] sentence1 [SEP] sentence2 [SEP],取[CLS]位置的输出作为文本关系分类的特征。
5.3.序列标注
文本中的每个token分别进行分类,MLM、NER (命名实体识别) 都是序列标注任务。分类特征就是对应token位置上的输出,输出层参数一般在各个位置之间是共享的。
5.4.多任务学习
我们已经知道,神经网络的隐藏层是特征学习器。在隐藏层(编码器)学习好特征之后,只需要一个简单的输出层就能很好地完成学习任务。
很多不同的自然语言处理任务,它们都会依赖语义特征。因此在同一套编码器之上,可以同时连接多个任务的输出层,而隐藏层(编码器)的参数是共享的。多个任务的损失函数也会加到总损失中 (total_loss),通过调用total_loss.backward()计算所有参数的梯度,进行参数更新。
预训练通常都是多任务学习,例如BERT就同时对MLM和NSP两个任务进行学习,ERNIE的预训练任务更是多达几十种。在下游任务中,意图识别和填槽 (抽象为序列标注任务)也是很典型的多任务学习。