前言
总结一些常见的NLP基础知识。
一、Bert相关
参考资料
1、Bert输出
- 第一个元素:
last_hidden_state
shape:(batch_size, sequence_length, hidden_size)
hidden_size:768
- 它是模型最后一层输出的隐藏状态(包括
cls
和sep
),即每一个token的embedding。
- 第二个元素:
pooler_output
shape:(batch_size, hidden_size)
hidden_size:768
- 这是序列的第一个token(即
cls
)的最后一层的隐藏状态(即cls的embedding),经过线性层和Tanh
激活函数进一步处理得到的,last_hidden_state[:,0,:]
经过线性层和Tanh
激活函数即是pooler_output
。
2、Bert输入
- 外部使用时:Bert模型输入为,
input_ids
:每个token(token都是字粒度)在词表中的索引值,包括[CLS]
和[SEP]
;token_type_ids
:识别句子界限,如果是单句子分类任务,默认为全0,可以不用设置该参数,如果是文本蕴含关系等句对任务,则第一个句子对应的位置全0,句子间分隔符[SEP]处置为0,第二个句子对应的位置全1,句尾分隔符[SEP]处置位1,[PAD]处全置为0;attention_mask
:界定注意力机制范围,1代表让Bert关注该位置,0代表是[PAD],不用关注,即[PAD]处为0,其余位置为1;
- 内部运转时:Bert模型输入实际为,
word_embeddings
:又名为token embeddings
,词向量表示,从一个(vocab_size, hidden_size)
的向量表中查找,将每一个token转换成固定维度的向量(转换依据即是上面的input_ids
)。转换后整段文本的词向量维度为(seq_length, hidden_size)
;token_type_embeddings
:又名为segment embeddings
,根据上述的token_type_ids
,从一个(2, hidden_size)
的向量表中查找,转换后向量大小为(seq_length, hidden_size)
;同一个句子的每个token对应的token_type_embeddings是相同的;position_embeddings
:从一个(max_position_embeddings, hidden_size)
的向量表中查找,转换后向量大小为(seq_length, hidden_size)
,这个不依赖于上述的id。
- BertEmbedding前向传播过程:输入
inputs_ids
,token_type_ids
,attention_mask
并生成word_embeddings
,token_type_embeddings
和position_embeddings
,三个维度都是(seq_length, hidden_size)
。将三者直接相加,维度不变。- bert与transformer的
position_embeddings
区别:bert自训练位置编码(称为训练式位置编码),即随着迭代逐步更新位置编码,transformer是三角式位置编码;两者都是绝对位置编码;- 位置编码的作用: 在不添加位置编码的情况下,改变句子token的顺序,经过自注意力层处理后,每个token的表征不会发生任何变化,这样就丢失了token之间的上下文关系。
max_position_embeddings
是预训练模型支持的最长token数,在config文件中有设置,例如bert模型的max_position_embeddings
为512;- 如果是单句子任务,
token_type_ids
也可以不输入,bert底层代码中有默认值,即都会默认为0; - 三个
embedding
相加之后,会依次经过LayerNorm
层和Dropout
层,然后继续向后传播;
- bert与transformer的
- Bert结构图:如下图所示。
- LayerNorm & Dropout & Residual:Bert中所有涉及这三个操作的结构,都是先经过Dropout操作,再经过Residual操作,最后经过LayerNorm操作。
- LayerNorm & Dropout & Residual:Bert中所有涉及这三个操作的结构,都是先经过Dropout操作,再经过Residual操作,最后经过LayerNorm操作。
3、Bert预训练模型
- 英文预训练模型:
- bert-base-uncased:bert-base-uncased;
- bert-base-cased:bert-base-cased;
- bert-large-uncased:bert-large-uncased;
- bert-large-cased:bert-large-cased;
- 区分:uncased会在分词前将大写字母转换为小写字母,cased会保留大写字母Hugging face预训练模型下载和使用。
- 中文预训练模型:
- bert-base-chinese:bert-base-chinese;
- chinese-bert-wwm-ext: chinese-bert-wwm-ext;
- chinese-roberta-wwm-ext:chinese-roberta-wwm-ext;
- chinese-roberta-wwm-ext-large: chinese-roberta-wwm-ext-large;
- 对比:Chinese-BERT-wwm;
- roberta和bert的区别:RoBERTa相比BERT的改进、roberta跟bert的对比
- NSP调整:去掉NSP任务;
- MASK调整:静态masking转换为动态masking;
- 训练调整:更大的mini-batch(256 → \rightarrow → 8000),更多的训练数据(13G → \rightarrow → 160G)。
- albert和bert的区别:Albert vs Bert
- 词向量矩阵分解:生成word_embeddings、token_type_embeddings、position_embeddings的embedding table,都分解为两个低秩矩阵;
- 共享参数:每一层的attention层、FFN层参数共享;
- NSP改为SOP任务:正例构建方法一致,负例调整:将正例的两个句子顺序调整作为负例。
- wwm:Chinese-BERT-wwm
- Whole Word Masking(全词Mask): 某些词在切分为token后,原来的mask规则是同一个词对应的子词,可能只有部分被[mask, 替换, 保持不变],现在的规则是,同一个词对应的子词,会同时被[mask, 替换, 保持不变]。
- WordPiece和BPE的区别:WordPiece和BPE的区别。BPE和WordPiece的区别在于如何选择两个子词进行合并。
- BPE:BPE的词表创建过程如下。
- 首先初始化词表,词表中包含了训练数据中出现的所有字符;
- 然后两两拼接字符,统计字符对在训练数据中出现的频率;
- 选择出现频率最高的一组字符对加入词表中;
- 反复2和3,直到词表大小达到指定大小。
- WordPiece:WordPiece的词表创建过程如下。
- 首先初始化词表,词表包含了训练数据中出现的所有字符;
- 然后两两拼接字符,统计字符对加入词表后对语言模型的似然值的提升程度;
- 选择提升语言模型似然值最大的一组字符对加入词表中;
- 反复2和3,直到词表大小达到指定大小。
- BPE:BPE的词表创建过程如下。
- roberta和bert的区别:RoBERTa相比BERT的改进、roberta跟bert的对比
- 预训练模型下载:一般最关键的三个文件:
- 第一个是配置文件,config.json;
- 第二个是词典文件,vocab.json或vocab.txt;
- 第三个是预训练模型文件,pytorch_model.bin或tf_model.h5。根据你使用的框架选择;
- 如何使用Pytorch-huggingface-Bert预训练模型。
- 下游任务修改Bert层数:
import torch from transformers import BertModel, BertConfig config = BertConfig.from_pretrained("bert-base-chinese") # 创建3层BERT模型 config.num_hidden_layers = 3 model = BertModel.from_pretrained("bert-base-chinese", config=config)
4、Bert的预训练任务
预训练任务 | 参考资料 |
---|---|
MLM(Masked Language Modeling) | BERT预训练的任务MLM和NSP |
NSP(Next Sentence Prediction) | BERT原理与NSP和MLM |
- MLM任务:随机MASK掉15%的词,目标是对这15%的词进行预测。注意,这15%被预测的词包括下面这三种,不仅仅是真正被替换为[MASK]的词。
- 训练数据构造:15%被MASK掉的词分为以下三种情况,这三种情况的词都会参与训练过程中的损失计算。分为这三种情况的原因,一是减弱训练与推理之间的差别,二是防止模型过度的关注[MASK]标记。
- 80%的词会被替换成[MASK];
- 10%的词会被替换成词表中的任意一个词;
- 10%的词保持不变。
- 模型训练:模型在训练过程中,最终的目标是对这15%被MASK掉的词进行预测,具体来说,是对被MASK掉的每个词,在整个词表上进行分类预测。
- 训练数据构造:15%被MASK掉的词分为以下三种情况,这三种情况的词都会参与训练过程中的损失计算。分为这三种情况的原因,一是减弱训练与推理之间的差别,二是防止模型过度的关注[MASK]标记。
5、Bert的小知识点
-
迁移学习:Bert在下游任务上的Fine-Tuning,其实是一个迁移学习过程(将知识迁移到新任务上);
-
句子对任务:Bert在处理句子对任务(例如文本蕴含任务)时,文本编码工具
tokenizer
的输入是以下形式:from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained("bert-base-uncased") # 文本编码方式一:批次数据,以列表输入,列表中每个元素是一个句子对构成的子列表 input_data = tokenizer( [[text1_a, text1_b], [text2_a, text2_b]], padding=True, truncation=True, max_length=args.max_seq_length, # 自定义参数 return_tensors="pt" ) # 文本编码方式一:单句子对数据,直接将一个句子对中的两个句子以两个参数的形式传入即可 input_data = tokenizer( text1_a, text1_b, padding=True, truncation=True, max_length=args.max_seq_length, # 自定义参数 return_tensors="pt" )
6、预训练模型综述
- Bert:自然语言处理:Bert及其他;
- Ernie:ERNIE三个版本进化史。
7、分词技术
二、模型训练
1、设置CUDA_VISIBLE_DEVICES
-
简介:服务器中有多个GPU,选择特定的GPU运行程序可在程序运行命令前使用:CUDA_VISIBLE_DEVICES=0命令。0为服务器中的GPU编号,可以为0, 1, 2, 3等,表明对程序可见的GPU编号;
-
说明:
说明 DA_VISIBLE_DEVICES=1 DA_VISIBLE_DEVICES=0,2,3 DA_VISIBLE_DEVICES=2,0,3 -
使用:
- 临时设置:
Linux: export CUDA_VISIBLE_DEVICES=1 windows: set CUDA_VISIBLE_DEVICES=1
- python运行时设置:
【注】:上述设置方式,必须要在第一次使用 cuda 之前进行设置。import os os.environ["CUDA_VISIBLE_DEVICES"] = "0, 1" 或 CUDA_VISIBLE_DEVICES=0,1 python **.py
- 永久设置:
linux: 在~/.bashrc 的最后加上export CUDA_VISIBLE_DEVICES=1,然后source ~/.bashrc windows: 打开我的电脑环境变量设置的地方,直接添加就行了。
- 临时设置:
2、保存加载模型
- 模型的保存:
torch.save()
- 方式一:
torch.save(model, path)
,保存模型所有参数,包括模型参数以及框架参数,在加载过程中可能因为框架版本问题报错,该方式继承nn.Module。因此不推荐该方式保存模型; - 方式二:
torch.save(model.state_dict(), path)
,保存模型的参数,以字典形式保存,state可以翻译为状态,来说明模型参数处于什么状态,该方式不依赖其它环境,继承有序字典OrderDict。推荐使用该方式,无论保存到GPU设备,还是CPU设备。
- 方式一:
- 模型的加载:
torch.load()
,模型加载需要设置model.to(device)
和map_location=device
两个参数(记住,不管是cpu加载gpu,还是gpu加载cpu,这两个参数都加上就行),所以先定义device:- cpu:
torch.device('cpu')
- gpu:
torch.device('cuda:0')
- 方式一:无需初始化模型,
model = torch.load(path, map_location=device)
model.to(device)
- 方式二:需先初始化模型,
model = Model() #初始化模型
model.load_state_dict(torch.load(path, map_location=device))
model.to(device)
- cpu:
3、PyTorch常用命令
import torch
#检查cuda是否可以使用
torch.cuda.is_available()
#查看当前gpu索引号
torch.cuda.current_device()
#返回gpu名字,设备索引默认从0开始
torch.cuda.get_device_name(0)
#选择device
torch.cuda.device(1)
torch.cuda.get_device_capability(device=0)
#查看有多少个GPU设备
torch.cuda.device_count()
#查看gpu的容量
torch.cuda.get_device_capability(device=0)
#查看tensor占用的显存
torch.cuda.memory_allocated("cuda:0")
4、PyTorch模型训练细节
-
模型训练推理模式切换:
model.eval()
与model.train()
。- 作用:主要用于通知dropout层和batchnorm层在train和val模式间切换;
- train:在train模式下,dropout网络层会按照设定的参数p设置保留激活单元的概率(保留概率=p); batchnorm层会继续计算数据的mean和var等参数并更新;
- eval:在eval模式下,dropout层会让所有的激活单元都通过,而batchnorm层会停止计算和更新mean和var,直接使用在训练阶段已经学出的mean和var值。
- 备注:两种模式切换不会影响各层的gradient计算行为,即gradient的计算和存储,eval与train模式一样,同样会构建计算图,保证反向传播可进行(反传要配合
loss.backward()
命令)。
- 作用:主要用于通知dropout层和batchnorm层在train和val模式间切换;
-
梯度反传开关:在使用pytorch时,并不是所有的操作都需要进行计算图的生成,只是想要网络结果的话就不需要反向传播 ,如果你想通过网络输出的结果去进一步优化网络的话 就需要反向传播了。
with torch.no_grad()
:- 不使用
with torch.no_grad()
:此时tensor有grad_fn=属性,表示计算的结果在一计算图当中,可以进行梯度反传等操作; - 使用
with torch.no_grad()
:表明当前计算不需要反向传播,使用之后,强制后边的内容不进行计算图的构建。
- 不使用
@torch.no_grad()
:作用同with torch.no_grad()
,只是表现形式是装饰器。
-
模型训练三步曲:
optimizer.zero_grad()
:先将梯度归零;loss.backward()
:反向传播计算得到每个参数的梯度值;optimizer.step()
:通过梯度下降执行一步参数更新。- 参见:optimizer.zero_grad() loss.backward() optimizer.step()原理和使用方法
-
pytorch框架中的权重说明:
.weight.data
:得到的是一个Tensor的张量(向量),不可训练的类型;.weight
:得到的是一个parameter的变量,可以计算梯度的。
因此,若要对weight值在后续迭代中进行优化更新,不能带data,且需要配合
require_grad
,比如词向量矩阵,torch.nn.embedding.weight.require_grad = True
。 -
tensor转换为list:
.cpu().detach().numpy().tolist()
.cpu()
:将数据从GPU转移至CPU上;.detach()
:去掉tensor的梯度;.numpy()
:tensor转换为numpy数据类型(可有可无);.tolist()
:最终转换为list数据类型;
5、macro与micro
- classification_report的用法:
from sklearn.metrics import classification_report y_true = [0, 1, 2, 2, 2] y_pred = [0, 0, 2, 2, 1] target_names = ['class 0', 'class 1', 'class 2'] print(classification_report(y_true, y_pred, target_names=target_names)) precision recall f1-score support class 0 0.50 1.00 0.67 1 class 1 0.00 0.00 0.00 1 class 2 1.00 0.67 0.80 3 accuracy 0.60 5 macro avg 0.50 0.56 0.49 5 weighted avg 0.70 0.60 0.61 5
6、多标签准确率评价方法
7、Sigmoid和Softmax
二分类:
采用Sigmoid函数的时候,最后一层全连接层的神经元个数为1;
采用Softmax函数的时候,最后一层全连接层的神经元个数是2。
多分类:
采用Softmax函数。
多标签分类:
采用Sigmoid函数。
- BCELoss和BCEWithLogitsLoss的区别:Pytorch详解BCELoss和BCEWithLogitsLoss。
- 交叉熵损失CrossEntropyLoss详解:Pytorch常用的交叉熵损失函数CrossEntropyLoss()详解。
9、学习率调整
num_warmup_steps
:学习率变化步数阈值:最初的num_warmup_steps中以线性的方式进行增长(增长到自定义的学习率大小,即torch.optim.Adam
或torch.optim.AdamW
中的超参数lr
),之后便是以相应函数的方式进行周期性变换;num_training_steps
:总训练步数,学习率在这个范围内进行变换;num_cycles
:变换周期,比如说余弦函数,num_cycles=2
,则在总训练步数内,要变换两个余弦周期。
10、优化器设定
transformers.AdamW
等价于torch.optim.AdamW
,来源一致。
11、tokenizer使用
Tokenizer快速使用
BertTokenizer使用方法
tokenizer()
和tokenizer.encode_plus()
的区别:- transformers 4.0.0版本之前用
tokenizer.encode_plus()
,之后用tokenizer()
。tokenizer()
增加了一些安全性判断,底层还是调用encode_plus()
; tokenizer.encode_plus()
不支持列表输入,只支持文本直接输入;tokenizer()
两者都支持;tokenizer()
相当于集成了tokenizer.encode_plus()
和tokenizer.batch_encode_plus()
。
- transformers 4.0.0版本之前用
tokenizer()
的padding
参数:tokenizer(inputs, padding=True)
:以inputs
中数据的最大长度为基准,长度不足的数据进行padding;tokenizer(inputs, padding="longest")
,作用同上;tokenizer(inputs, padding="max_length", max_length=128, truncation=True)
:以max_length
为基准,长度不足的数据进行padding,长度超出的数据,进行截断。
tokenizer()
的pad_to_multiple_of
参数:pad_to_multiple
被设置成None以外的数值时,通常是8或者16,输出的input_ids
的长度会被调节成设置值的整数倍,这样做的目的是更高效利用计算能力。tokenizer.encode()
和tokenizer.decode()
:tokenizer.encode()
:相对于tokenizer.encode_plus()
,tokenizer.encode()
仅返回input_ids
,tokenizer.encode_plus()
返回input_ids
、token_type_ids
、attention_mask
;tokenizer.decode()
:- 输入:input_ids;
- 输出:token拼接结果;
- 示例:
from transformers import AutoModel, AutoTokenizer model = AutoModel.from_pretrained("chinese-roberta-wwm-ext") tokenizer = AutoTokenizer.from_pretrained("chinese-roberta-wwm-ext") result = tokenizer.decode([791, 1921, 1921, 3698, 4696, 1962]) print(result) # 今 天 天 气 真 好
12、DataLoader和Dataset
pytorch中dataloader 的sampler 参数详解
- 下面两种模式等价:
-
定义sampler:
train_sampler = RandomSampler(train_dataset) train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=self.batch_size)
-
定义shuffle:
train_dataloader = DataLoader(train_dataset, batch_size=self.batch_size, shuffle=True)
-
当sampler有输入时:shuffle的值没有意义;
-
当sampler没有输入时:
- shuffle为默认值False时,sampler是SequentialSampler,就是按顺序取样;
- shuffle为True时,sampler是RandomSampler,就是按随机取样。
-
13、固定卷积计算方法
torch.backends.cudnn.benchmark和torch.backends.cudnn.deterministic解读
- 两个要搭配使用:
torch.backends.cudnn.deterministic = True
;torch.backends.cudnn.benchmark = False
(默认即为False)。
- 原因:benchmark设置False,是为了保证不使用选择卷积算法的机制,使用固定的卷积算法。但是,就算是固定的卷积算法,由于其实现不同,也可能是不可控制的,即相同的值,同一个算法卷积出来有细微差别,deterministic设置True保证使用确定性的卷积算法,二者配合起来,才能保证卷积操作的一致性。
15、BN、LN作用与区别
- 作用:
- 加快收敛速度:比如说一个网络有50层,第一层的参数更新,经过层层相乘,会使得第50层的输入变化非常剧烈,就要不断去适应,降低收敛速度;
- 提高模型效果:其次底层输入过大或者过小,会使得输出落入饱和区(sigmoid),导致梯度消失,使得学习过早停止。所以将输入数据映射到非饱和区提高模型表达能力是一个非常重要的任务。
- 区别:
-
计算维度不同:BN是所有batch_size和所有seq_len的数值,在hidden_size维度进行归一化,即最终会生成hidden_size个均值和方差;LN是所有seq_len和所有hidden_size的数值,在batch_size维度进行归一化,最终会生成batch_size个均值和方差;NLP中的LN还会更特殊一些,是单个token的所有features做标准化(下图左下角),并不是对单个样本内所有tokens、所有features一起做标准化,所以和句子长度seq_len和batch_size大小无关。
-
使用方式不同:BN的计算涉及batch_size这个维度,因此训练时是计算一个batch_size的均值和方差,但推理时一般只有一个文本,就会导致训练和推理时使用的是不同的标准化方法,这样就会产生偏差。针对这个问题,BN有两种解决方案可供选择。
- 简单平均法:把每个batch_size的均值和方差都保存下来,然后训练完了求均值的平均值,方差的平均值即可,在推理时直接使用;
- 移动平均法:仅以
μ
\mu
μ的计算为例:
μ t o t a l = d e c a y ∗ μ t o t a l + ( 1 − d e c a y ) ∗ μ \mu_{total}=decay *\mu_{total}+(1-decay)*\mu μtotal=decay∗μtotal+(1−decay)∗μ
其中 d e c a y decay decay是衰减系数。即总均值 μ t o t a l \mu_{total} μtotal是前一个batch_size统计的总均值和本次batch_size的 μ \mu μ加权求和。至于衰减率 d e c a y decay decay在区间[0,1]之间。同简单平均法一言,在训练完成后,获取最终的 μ t o t a l \mu_{total} μtotal和 σ t o t a l \sigma_{total} σtotal,在推理时直接使用。
LN和BN不同,LN的计算不涉及batch_size维度,完全有样本自身决定,因此训练和推理时一致,直接计算即可。
-
实践意义不同:BN抹平了不同特征之间的大小关系,保留了不同样本之间的大小关系,如果具体任务依赖于不同样本之间的大小关系,则适合采用BN,比如图片分类。而LN正相反,抹平了不同样本之间的大小关系,保留了不同特征之间的大小关系,NLP任务主要学习文本上下文的时序关系,LN可以保留这种时序关系。
-
16、权重衰减
- 对模型中的所有参数进行L2正则处理防止过拟合,包括权重w和偏置b。此时,参数在进行梯度更新时,公式会稍有变化。以参数w为例,如下图所示。在随机梯度下降算法(SGD)中,因为引入L2正则,使得在更新参数前,把当前的权重进行了一次缩小(一次衰退)。
- 在实际开发中,我们可以指定哪些参数进行权重衰减操作:
# named_parameters()获取模型中的参数和参数名字 optimizer_param = list(model.named_parameters()) # no_decay中存放不进行权重衰减的参数 no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight'] # any()函数用于判断给定的可迭代参数iterable是否全部为False,则返回False,如果有一个为True,则返回True # 判断optimizer_param中所有的参数。如果不在no_decay中,则进行权重衰减;如果在no_decay中,则不进行权重衰减 optimizer_grouped_parameters = [ {'params': [param for name, param in optimizer_param if not any((name in no_decay_name) for no_decay_name in no_decay)], 'weight_decay': 0.01}, {'params': [param for name, param in optimizer_param if any((name in no_decay_name) for no_decay_name in no_decay)], 'weight_decay': 0.0} ] # 使用带有权重衰减功能的Adam优化器Adamw optimizer = torch.optim.AdamW(optimizer_grouped_parameters, lr=5e-5, eps=1e-8)
17、Adam和Adamw
- 区别:AdamW即Adam + weight decay,效果与Adam + L2正则化相同,但是计算效率更高,因为L2正则化需要在loss中加入正则项,之后再算梯度,最后再反向传播,而AdamW直接将正则项的梯度加入反向传播的公式中,省去了手动在loss中加正则项这一步。如下图所示,粉色部分,为传统L2正则添加的位置;而AdamW,则将正则加在了绿色位置。
- Adam:粉色保留,绿色去掉;
- AdamW:绿色保留,粉色去掉。
18、节省显存trick
gradient_accumulation_steps
:通常batch_size越大,模型效果越好,但是会导致模型显存占用量太大。为了节省显存,可以采用梯度累积的方法来曲线救国,间接增大batchsize。参考深度学习节省显存的trick之gradient_accumulation_steps。
19、PyTorch安装技巧
- 在线安装:正常安装是到PyTorch官网获取对应版本的PyTorch安装命令,在本地Python环境中执行即可,例如:
pip install torch==2.0.0+cu117 torchvision==0.15.1+cu117 torchaudio==2.0.1 --index-url https://download.pytorch.org/whl/cu117
- 本地安装:上述方式在线下载PyTorch安装包严重依赖网络环境,偶尔由于网络波动,会造成下载中断,因此可以采取先下载PyTorch的安装文件,下载地址是Download PyTorch,然后在本地Python环境中执行
pip intall ***.whl
即可,这样速度会快很多。另外,最后记得再安装对应版本的torchvision
和torchaudio
。例如我们在Python3.8环境下安装,执行以下两步即可:pip install torch-2.0.0+cu117-cp310-cp310-linux_x86_64.whl pip install torchvision==0.15.1+cu116 torchaudio==2.0.1 --extra-index-url https://download.pytorch.org/whl/cu116
20、正向传播与反向传播
- 正向传播:神经网络从输入到输出的过程;
- 反向传播:神经网络从输出到计算参数梯度的过程;
- 两者关联:在训练深度学习模型时,正向传播和反向传播之间相互依赖。一方面,正向传播的计算可能依赖于模型参数的当前值,这些模型参数是在反向传播的梯度计算后通过优化算法迭代的,而这些当前值是优化算法根据上一次反向传播算出的梯度后迭代得到的。另一方面,反向传播的梯度计算可能依赖于各变量的当前值,而这些变量的当前值是通过正向传播计算得到的。换句话说,这个当前值是通过从输入层到输出层的正向传播计算并存储获取的。因此,在模型参数初始化完成后,我们交替地进行正向传播和反向传播,并根据反向传播计算的梯度更新模型参数。既然我们在反向传播中使用了正向传播中计算得到的中间变量来避免重复计算,那么这个复用也导致正向传播结束后不能立即释放中间变量内存。这也是训练要比预测占用更多内存的一个重要原因。另外需要指出的是,这些中间变量的个数大体上与网络层数线性相关,每个变量的大小跟批次大小也是线性相关的,它们是导致较深的神经网络使用较大批次训练时更容易超内存的主要原因。
三、NLP任务实践
1、基于prompt的中文文本分类
参考文档基于Prompt-Tuning实现情感分类,实现了事件立场倾向识别任务,具体代码参见基于Prompt-Tuning实现事件立场倾向识别。其他相关文档,可参考基于prompt的中文文本分类。
2、模型蒸馏
-
知识蒸馏:知识蒸馏(knowledge distillation)是模型压缩的一种常用的方法,对于一个完整的知识蒸馏过程,有两个模型,分别为Teacher模型和Student模型,通过学习将已经训练好的Teacher模型中的知识迁移到小的Student模型中。其具体过程如下图所示,论文参考Distilling the Knowledge in a Neural Network。
对于Student模型,其目标函数有两个,分别为蒸馏的loss(distillation loss)和自身的loss(student loss),Student模型最终的损失函数为:
L = α L s o f t + ( 1 − α ) L h a r d L=\alpha L_{soft}+(1-\alpha)L_{hard} L=αLsoft+(1−α)Lhard
其中, L s o f t L_{soft} Lsoft表示的是蒸馏的loss, L h a r d L_{hard} Lhard表示的是自身的loss。loss函数可自定义,一般自身loss选择交叉熵损失,蒸馏loss选择交叉熵损失或者MSE损失(个人认为MSE更合适)。 -
模型蒸馏:以TinyBert为例,介绍下模型蒸馏流程,参考TinyBert。TinyBert提出了Two-Stage Learning框架,分别在预训练和精调阶段蒸馏教师模型,得到了参数量减少7.5倍,速度提升9.4倍的4层Bert,效果可以达到教师模型的96.8%,同时这种方法训练出的6层模型甚至接近Bert-base。
Bert模型是由多个Transformer模块(Self-Attention+FFN)组成,单个Self-Attention+FFN模块如下图所示:
假设Bert模型中有 N N N层的Transformer Layer,在蒸馏的过程中,Bert模型作为Teacher模型,而需要蒸馏的模型TinyBert模型作为Student模型,其Transformer Layer的层数假设为 M M M,则有 M < N M < N M<N,此时需要找到一个对应关系: n = g ( m ) n=g(m) n=g(m),表示的是在Student模型中的第 m m m层对应于Teacher模型中的第 n n n层,即 g ( m ) g(m) g(m)层。Tinybert的Embedding层和预测层也是从Bert的相应层学习知识的,其中Embedding层对应的层数为0,预测层对应的层数为 M + 1 M+1 M+1,对应到BERT中的层数分别为 0 = g ( 0 ) 0=g(0) 0=g(0)和 N + 1 = g ( M + 1 ) N+1=g(M+1) N+1=g(M+1)。在形式上,学生模型可以通过最小化以下的目标函数来获取教师模型的知识:
L m o d e l = ∑ x ∈ χ ∑ m = 0 M + 1 λ m L l a y e r ( f m S ( x ) , f g ( m ) T ( x ) ) L_{model} = \sum_{x\in\chi}\sum_{m=0}^{M+1}\lambda_{m}L_{layer}(f_{m}^{S}(x), f_{g(m)}^{T}(x)) Lmodel=x∈χ∑m=0∑M+1λmLlayer(fmS(x),fg(m)T(x))
其中, L l a y e r L_{layer} Llayer是给定的模型层的损失函数, f m f_{m} fm表示的是由第 m m m层得到的结果, λ m \lambda_{m} λm表示第 m m m层蒸馏的重要程度。在TinyBert的蒸馏过程中,又可以分为以下三个部分:Transformer-layer Distillation、Embedding-Layer Distillation、Prediction-Layer Distillation。- Transformer-layer Distillation(中间层):Transformer-layer的蒸馏由Attention Based蒸馏和Hidden States Based蒸馏两部分组成,具体如下图所示:
- Attention Based蒸馏:在BERT中多头注意力层能够捕获到丰富的语义信息,因此,在蒸馏到TinyBert中,提出了Attention Based蒸馏,其目的是希望使得蒸馏后的Student模型能够从Teacher模型中学习到这些语义上的信息。具体到模型中,就是让TinyBert网络学习拟合BERT网络中的多头注意力矩阵,目标函数定义如下:
L a t t n = 1 h ∑ i = 1 h M S E ( A i S , A i T ) L_{attn} = \frac{1}{h}\sum_{i=1}^{h}MSE(A_{i}^{S},A_{i}^{T}) Lattn=h1i=1∑hMSE(AiS,AiT)
其中, h h h代表注意力头数, A i ∈ R l × l A_{i}\in R^{l \times l} Ai∈Rl×l代表Student或者Teacher模型中的第 i i i个注意力头对应的注意力矩阵, l l l代表输入文本的长度。在公式中使用注意力矩阵 A A A而不是 s o f t m a x ( A ) softmax(A) softmax(A),是因为实验结果显示这样可以得到更快的收敛速度和更好的性能表现。 - Hidden States Based 蒸馏:Hidden States Based的蒸馏是对Transformer层进行了知识蒸馏处理,目标函数定义为:
L h i d n = M S E ( H S W h , H T ) L_{hidn} = MSE(H^{S}W_{h},H^{T}) Lhidn=MSE(HSWh,HT)
其中,矩阵 H S ∈ R l × d ′ H^{S}\in R^{l \times d'} HS∈Rl×d′和 H T ∈ R l × d H^{T}\in R^{l \times d} HT∈Rl×d分别代表Student网络和Teacher网络的隐状态,且都是FFN的输出。 d d d和 d ′ d' d′代表Teacher网络和Student网络的隐藏状态大小,且 d ′ < d d'<d d′<d,因为Student网络总是小于Teacher网络。 W h ∈ R d ′ × d W_{h}\in R^{d'\times d} Wh∈Rd′×d是一个参数矩阵,将Student网络的隐藏状态投影到Teacher网络隐藏状态所在的空间。
- Attention Based蒸馏:在BERT中多头注意力层能够捕获到丰富的语义信息,因此,在蒸馏到TinyBert中,提出了Attention Based蒸馏,其目的是希望使得蒸馏后的Student模型能够从Teacher模型中学习到这些语义上的信息。具体到模型中,就是让TinyBert网络学习拟合BERT网络中的多头注意力矩阵,目标函数定义如下:
- Embedding-layer Distillation(输入层):Embedding层的蒸馏与Hidden States Based蒸馏一致,其目标函数为:
L e m b d = M S E ( E S W e , E T ) L_{embd} = MSE(E^{S}W_{e},E^{T}) Lembd=MSE(ESWe,ET)
其中 E S E^{S} ES和 E T E^{T} ET分别代表Student网络和Teacher网络的Embedding, W e W_{e} We的作用与 W h W_{h} Wh的作用一致。 - Prediction-layer Distillation(输出层):除了对中间层做蒸馏,同样对于最终的预测层也要进行蒸馏,其目标函数为:
L p r e d = C E ( z T t , z S t ) L_{pred} = CE(\frac{z^{T}}{t},\frac{z^{S}}{t}) Lpred=CE(tzT,tzS)
其中, z S z^{S} zS, z T z^{T} zT分别是Student网络和Teacher网络预测的 l o g i t s logits logits向量( l o g i t s logits logits通常是指模型输出的未经过 s o f t m a x softmax softmax函数处理的原始数值。注意,这里虽然使用的是 l o g i t s logits logits向量,但在实际操作中,还是会把 l o g i t s logits logits向量做 s o f t m a x softmax softmax处理,不然 l o g i t s logits logits向量可能存在负值没,导致无法计算交叉熵损失。其实这一步就是前文知识蒸馏的蒸馏loss部分), C E CE CE表示的是交叉熵损失, t t t是温度值,在实验中得知,当 t = 1 t=1 t=1时效果最好。
综合上述三个部分的Loss函数,则可以得到Teacher网络和Student网络之间对应层的蒸馏损失如下:
L l a y e r = { L e m b d m = 0 L h i d n + L a t t n M ≥ m > 0 L p r e d m = M + 1 L_{layer}=\begin{cases} L_{embd} & m=0 \\ L_{hidn}+L_{attn} & M\ge m>0 \\ L_{pred} & m=M+1 \end{cases} Llayer=⎩⎪⎨⎪⎧LembdLhidn+LattnLpredm=0M≥m>0m=M+1- Two-Stage Learning:对于Bert的训练来说分为两个阶段,分别为预训练和fine-tunning,预训练阶段可以使得Bert模型能够学习到更强的语义信息,fine-tunning阶段是为了使模型更适配具体的任务。因此在蒸馏的过程中也需要针对两个阶段都蒸馏,即general distillation和task-specific distillation,具体如下图所示:
在general distillation阶段,通过蒸馏使得TinyBert能够学习到Bert中的语义知识,能够提升TinyBert的泛化能力,而task-specific distillation可以进一步获取到Bert在特定任务中的知识。论文在最后的实验中,预训练阶段只对中间层进行了蒸馏;精调阶段则先对中间层蒸馏20个epochs,再对最后一层蒸馏3个epochs。具体参考模型蒸馏-TinyBERT。
- Transformer-layer Distillation(中间层):Transformer-layer的蒸馏由Attention Based蒸馏和Hidden States Based蒸馏两部分组成,具体如下图所示:
-
真实任务蒸馏实践:上述内容介绍了两种蒸馏方案:知识蒸馏和模型蒸馏,同时介绍了TinyBert蒸馏的具体流程,包括预训练蒸馏和特定任务蒸馏两部分。我们在真实任务上进行蒸馏实践时,一般会采用知识蒸馏或者特定任务蒸馏。知识蒸馏不过多赘述,比较简单。使用特定任务蒸馏时,采用的流程和TinyBert一致,先对中间层蒸馏若干个epochs(称作TD1阶段),再对最后一层蒸馏若干个epochs(称作TD2阶段)。具体的代码实践,未来会进行补充。
3、增量学习
总结
细节决定成败!!!