前言
最近做实验时发现对模型架构理解不够深入,理论联系不了实际。在微调时候总会出现由于参数不同导致的错误。所以写下此篇文章,期望能够进一步了解模型每一层的作用。
RoBERTaModel
以RoBERTaModel为例,先将加载模型打印出来:
RobertaModel(
(embeddings): RobertaEmbeddings(
(word_embeddings): Embedding(50265, 768, padding_idx=1)
(position_embeddings): Embedding(514, 768, padding_idx=1)
(token_type_embeddings): Embedding(1, 768)
(LayerNorm): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(encoder): RobertaEncoder(
(layer): ModuleList(
(0-11): 12 x RobertaLayer(
(attention): RobertaAttention(
(self): RobertaSdpaSelfAttention(
(query): Linear(in_features=768, out_features=768, bias=True)
(key): Linear(in_features=768, out_features=768, bias=True)
(value): Linear(in_features=768, out_features=768, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(output): RobertaSelfOutput(
(dense): Linear(in_features=768, out_features=768, bias=True)
(LayerNorm): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
(intermediate): RobertaIntermediate(
(dense): Linear(in_features=768, out_features=3072, bias=True)
(intermediate_act_fn): GELUActivation()
)
(output): RobertaOutput(
(dense): Linear(in_features=3072, out_features=768, bias=True)
(LayerNorm): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
)
)
)
然后来逐一分析:
RobertaEmbeddings(嵌入层)
1 | (RobertaEmbeddings( |
将输入的文本序列通过词嵌入、位置嵌入和标记类型嵌入相加,再进行层归一化和丢弃处理,得到模型的初始输入表示
word_embeddings: Embedding(50265, 768, padding_idx=1)
- 将输入的词汇表中的每个词(最多50265个)映射到768维的向量空间。
比如:输入句子 “Hello world”,每个词被转换为768维的向量表示,用于后续处理
position_embeddings
: Embedding(514, 768, padding_idx=1)
- 为每个位置添加一个位置嵌入,帮助模型理解标记的顺序。
比如:句子中的第一个词 “Hello” 添加位置0的嵌入,第二个词 “world” 添加位置1的嵌入,依此类推
token_type_embeddings
: Embedding(1, 768)
- 用于区分不同类型的标记(如句子A和句子B)。在 RoBERTa 中通常不使用(单一类型)。
比如:在双句子任务中,第一个句子所有词的 token_type_embeddings
为0,第二个句子为1
LayerNorm
: LayerNorm((768,), eps=1e-05, elementwise_affine=True)
- 对嵌入进行层归一化,稳定训练过程。
dropout
: Dropout(p=0.1, inplace=False)
- 在训练过程中随机丢弃部分神经元,防止过拟合。
RobertaEncoder(编码器)
计算输入序列中每个词对其他词的注意力权重,生成上下文感知的词表示。例如,在句子 “The cat sat on the mat”,模型会根据上下文调整每个词的表示,如 “sat” 可能更多关注 “cat” 和 “mat”。
组成部分:
- 12个 RobertaLayer
- 栈式的Transformer编码器层,用于逐层提取和转换特征
而每个 RobertaLayer 又包含:
- RobertaAttention注意力层
- RobertaIntermediate中间层
- RobertaOutput输出层
具体来说,这三层分别又包括:
- RobertaAttention注意力层
- self(自注意力机制):
RobertaSdpaSelfAttention
- query, key, value(查询、键、值): Linear(in_features=768, out_features=768, bias=True)
- 将输入向量线性变换为查询、键和值向量,用于计算注意力分数。
- query, key, value(查询、键、值): Linear(in_features=768, out_features=768, bias=True)
比如:对输入的词向量进行变换,生成用于自注意力计算的Q、K、V矩阵。
- dropout(丢弃层): Dropout(p=0.1, inplace=False)
- 在注意力权重上应用丢弃,防止过拟合。
output(自注意力输出):
RobertaSelfOutput
dense(全连接层): Linear(in_features=768, out_features=768, bias=True)
- 将注意力输出进行线性变换,恢复原始维度。
LayerNorm(层归一化): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
- 归一化处理,稳定训练过程。
dropout(丢弃层): Dropout(p=0.1, inplace=False)
- 应用丢弃,防止过拟合
- RobertaIntermediate中间层
将自注意力层的输出扩展到3072维,通过GELU激活函数增加非线性,增强模型对复杂模式的捕捉能力
- dense(全连接层):
Linear(in_features=768, out_features=3072, bias=True)
- 将输入向量扩展到更高维度(通常是4倍的隐藏维度),以增强模型的表达能力。
- intermediate_act_fn(激活函数):
GELUActivation()
- 应用GELU激活函数,引入非线性。
- RobertaOutput输出层
将高维的中间表示降维回768维,并进行归一化和丢弃处理,准备传递到下一层或输出
- dense(全连接层):
Linear(in_features=3072, out_features=768, bias=True)
- 将中间层的高维表示降维回768维,与原始隐藏维度保持一致。
- LayerNorm(层归一化):
LayerNorm((768,), eps=1e-05, elementwise_affine=True)
- 归一化处理,稳定训练过程。
- dropout(丢弃层):
Dropout(p=0.1, inplace=False)
- 应用丢弃,防止过拟合
了解模型架构有什么用
1.调试和故障排除
拿我遇到的实际问题举例,在分类任务中,我们给出一段代码,首先需要将这个代码片段进行分词处理并转换成模型能够接受的输入特征形式(token_id)
代码示意如下:
1 | class InputFeatures(object): |
这段代码是在进行模型训练和推理前的数据准备,具体包括:
InputFeatures
类:用于封装单个样本的特征。
convert_examples_to_features
函数:将原始数据转换为 InputFeatures
实例。
TextDataset
类:一个 PyTorch 的 Dataset
,负责加载、缓存和提供数据样本。
主要来看convert_examples_to_features函数:
首先需要注意的**input_ids
**,这是是一个包含词元(tokens)对应的词汇表索引的列表。这些索引用于模型的嵌入层,将词元转换为向量表示。它的维度是由什么决定的呢
1 | code_tokens = code_tokens[:args.code_length + args.data_flow_length - 2 - min(len(dfg), args.data_flow_length)] |
这段代码确保了代码部分的词元被截断以确保总长度不超过 args.code_length + args.data_flow_length - 2 - min(len(dfg), args.data_flow_length)
,而position_idx是由source_tokens长度决定的
1 | dfg = dfg[:args.code_length + args.data_flow_length - len(source_tokens)] |
DFG 信息被添加到 source_tokens
和 source_ids
中,确保总长度不超过 args.code_length + args.data_flow_length
1 | padding_length = args.code_length + args.data_flow_length - len(source_ids) |
如果 source_ids
的长度不足,总长度为 args.code_length + args.data_flow_length
,则使用 pad_token_id
进行填充
- 上述逻辑保持input_ids与args.code_length + args.data_flow_length相等,所以**
input_ids
的长度始终为**args.code_length + args.data_flow_length
假设 args.code_length = 512
和 args.data_flow_length = 128
,则:input_ids的长度为 512 + 128 = 640。
这样会导致一个问题,如果input_id的维度是[block_size,args.code_length + args.data_flow_length],如果embedding的维度不同,就会导致模型参数不匹配,进一步导致训练终止
理解输入嵌入层工作流程
在上述列子中,InputFeatures
类封装了模型的输入特征,主要包括:
input_ids
: 一个包含词元(tokens)对应的词汇表索引的列表。position_idx
: 一个包含每个词元位置索引的列表,用于指示词元在序列中的位置
嵌入层的架构如下
(RobertaEmbeddings(
(word_embeddings): Embedding(50265, 768, padding_idx=1)
(position_embeddings): Embedding(514, 768, padding_idx=1)
(token_type_embeddings): Embedding(1, 768)
(LayerNorm): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
))
工作流程:
- word_embeddings:将
input_ids
中的每个词元索引通过word_embeddings
转换为 768 维的向量 - Position Embedding:将
position_idx
中的每个位置索引通过position_embeddings
转换为 768 维的向量 - Token Type Embedding
- 合并嵌入(Combine Embeddings),将词嵌入、位置嵌入和标记类型嵌入相加,得到每个词元的最终嵌入表示,然后进行
LayerNorm
和dropout
处理,得到模型的输入表示 - 输入传递到编码器(Encoder),经过嵌入层处理后的向量被传递到编码器(
RobertaEncoder
),开始进一步的特征提取和表示学习
问题
应该很容易就看出来,(position_embeddings): Embedding(514, 768, padding_idx=1)
的维度是514,而input_ids
和 position_idx
的长度必须与模型嵌入层的期望长度一致,即 args.code_length + args.data_flow_length
有两种情况会导致出现问题:
位置索引超出了
position_embeddings
的最大位置数(514)词汇表索引超出了
word_embeddings
的词汇表大小(50265)
很明显,我们的例子中,position_idx维度是640,已经超出模型的position_embeddings预期了。
解决:调整args.code_length + args.data_flow_length的和小于514,同时需要确保每一个id是符合预期的
2.优化和定制模型
这点就不用多说了,比如在预训练好的模型基础上增加一个分类头,专门用于分类训练(我就是这么干的)