理解模型维度


前言

最近做实验时发现对模型架构理解不够深入,理论联系不了实际。在微调时候总会出现由于参数不同导致的错误。所以写下此篇文章,期望能够进一步了解模型每一层的作用。

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
2
3
4
5
6
7
8
(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: 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”。

组成部分:

  1. 12个 RobertaLayer
  • 栈式的Transformer编码器层,用于逐层提取和转换特征

而每个 RobertaLayer 又包含:

  • RobertaAttention注意力层
  • RobertaIntermediate中间层
  • RobertaOutput输出层

具体来说,这三层分别又包括:

  1. RobertaAttention注意力层
  • self(自注意力机制): RobertaSdpaSelfAttention
    • 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)

      • 应用丢弃,防止过拟合
  1. RobertaIntermediate中间层

将自注意力层的输出扩展到3072维,通过GELU激活函数增加非线性,增强模型对复杂模式的捕捉能力

  • dense(全连接层): Linear(in_features=768, out_features=3072, bias=True)
    • 将输入向量扩展到更高维度(通常是4倍的隐藏维度),以增强模型的表达能力。
  • intermediate_act_fn(激活函数): GELUActivation()
    • 应用GELU激活函数,引入非线性。
  1. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
class InputFeatures(object):
"""A single training/test features for a example."""
def __init__(self,
input_tokens,
input_ids,
position_idx,
dfg_to_code,
dfg_to_dfg,
idx,
label

):
self.input_tokens = input_tokens
self.input_ids = input_ids
self.position_idx = position_idx
self.dfg_to_code = dfg_to_code
self.dfg_to_dfg = dfg_to_dfg
self.idx=str(idx)
self.label=label


def convert_examples_to_features(js,tokenizer,args):
#source
code=' '.join(js['func'].split())
dfg, index_table, code_tokens = extract_dataflow(code, "c")

code_tokens=[tokenizer.tokenize('@ '+x)[1:] if idx!=0 else tokenizer.tokenize(x) for idx,x in enumerate(code_tokens)]
ori2cur_pos={}
ori2cur_pos[-1]=(0,0)
for i in range(len(code_tokens)):
ori2cur_pos[i]=(ori2cur_pos[i-1][1],ori2cur_pos[i-1][1]+len(code_tokens[i]))
code_tokens=[y for x in code_tokens for y in x]

code_tokens=code_tokens[:args.code_length+args.data_flow_length-2-min(len(dfg),args.data_flow_length)]
source_tokens =[tokenizer.cls_token]+code_tokens+[tokenizer.sep_token]
source_ids = tokenizer.convert_tokens_to_ids(source_tokens)
position_idx = [i+tokenizer.pad_token_id + 1 for i in range(len(source_tokens))]
dfg = dfg[:args.code_length+args.data_flow_length-len(source_tokens)]
source_tokens += [x[0] for x in dfg]
position_idx+=[0 for x in dfg]
source_ids+=[tokenizer.unk_token_id for x in dfg]
padding_length=args.code_length+args.data_flow_length-len(source_ids)
position_idx+=[tokenizer.pad_token_id]*padding_length
source_ids+=[tokenizer.pad_token_id]*padding_length

reverse_index={}
for idx,x in enumerate(dfg):
reverse_index[x[1]]=idx
for idx,x in enumerate(dfg):
dfg[idx]=x[:-1]+([reverse_index[i] for i in x[-1] if i in reverse_index],)
dfg_to_dfg=[x[-1] for x in dfg]
dfg_to_code=[ori2cur_pos[x[1]] for x in dfg]
length=len([tokenizer.cls_token])
dfg_to_code=[(x[0]+length,x[1]+length) for x in dfg_to_code]

return InputFeatures(source_tokens, source_ids, position_idx, dfg_to_code, dfg_to_dfg, js['idx'],js['target'])

class TextDataset(Dataset):
def __init__(self, tokenizer, args, file_path=None):
self.examples = []
self.args=args
# 首先看看有没有cache的文件.
file_type = file_path.split('/')[-1].split('.')[0]
folder = '/'.join(file_path.split('/')[:-1]) # 得到文件目录

cache_file_path = os.path.join(folder, 'cached_{}'.format(
file_type))

print('\n cached_features_file: ',cache_file_path)
try:
self.examples = torch.load(cache_file_path)
logger.info("Loading features from cached file %s", cache_file_path)

except:
logger.info("Creating features from dataset file at %s", file_path)
with open(file_path) as f:
for line in tqdm(f):
js=json.loads(line.strip())
self.examples.append(convert_examples_to_features(js,tokenizer,args))
# 这里每次都是重新读取并处理数据集,能否cache然后load
logger.info("Saving features into cached file %s", cache_file_path)
torch.save(self.examples, cache_file_path)
if 'train' in file_path:
for idx, example in enumerate(self.examples[:3]):
logger.info("*** Example ***")
logger.info("idx: {}".format(idx))
logger.info("label: {}".format(example.label))
logger.info("input_tokens: {}".format([x.replace('\u0120','_') for x in example.input_tokens]))
logger.info("input_ids: {}".format(' '.join(map(str, example.input_ids))))

def __len__(self):
return len(self.examples)

def __getitem__(self, item):
#calculate graph-guided masked function
total_length = self.args.code_length + self.args.data_flow_length
attn_mask=np.zeros((self.args.code_length+self.args.data_flow_length,
self.args.code_length+self.args.data_flow_length),dtype=bool)
#calculate begin index of node and max length of input

node_index=sum([i>1 for i in self.examples[item].position_idx])
max_length=sum([i!=1 for i in self.examples[item].position_idx])

# 确保 node_index 和 max_length 不超过 total_length
node_index = min(node_index, total_length-1)
max_length = min(max_length, total_length-1)

#sequence can attend to sequence
attn_mask[:node_index,:node_index]=True
#special tokens attend to all tokens
for idx,i in enumerate(self.examples[item].input_ids):
if i in [0,2]:
idx = min(idx, total_length -1)
attn_mask[idx,:max_length]=True
#nodes attend to code tokens that are identified from
for idx,(a,b) in enumerate(self.examples[item].dfg_to_code):
if a<node_index and b<node_index:

attn_mask[min(idx+node_index,total_length-1),a:b]=True
attn_mask[a:b,min(idx+node_index,total_length-1)]=True
#nodes attend to adjacent nodes
for idx,nodes in enumerate(self.examples[item].dfg_to_dfg):
for a in nodes:
if a+node_index<len(self.examples[item].position_idx):
attn_mask[min(idx+node_index,total_length-1),min(a+node_index,total_length-1)]=True

return (torch.tensor(self.examples[item].input_ids),
torch.tensor(attn_mask),
torch.tensor(self.examples[item].position_idx),
torch.tensor(self.examples[item].label))

这段代码是在进行模型训练和推理前的数据准备,具体包括:

InputFeatures:用于封装单个样本的特征。

convert_examples_to_features 函数:将原始数据转换为 InputFeatures 实例。

TextDataset:一个 PyTorch 的 Dataset,负责加载、缓存和提供数据样本。

主要来看convert_examples_to_features函数:

首先需要注意的**input_ids**,这是是一个包含词元(tokens)对应的词汇表索引的列表。这些索引用于模型的嵌入层,将词元转换为向量表示。它的维度是由什么决定的呢

1
2
3
4
code_tokens = code_tokens[:args.code_length + args.data_flow_length - 2 - min(len(dfg), args.data_flow_length)]
source_tokens = [tokenizer.cls_token] + code_tokens + [tokenizer.sep_token]
source_ids = tokenizer.convert_tokens_to_ids(source_tokens)
position_idx = [i+tokenizer.pad_token_id + 1 for i in range(len(source_tokens))]

这段代码确保了代码部分的词元被截断以确保总长度不超过 args.code_length + args.data_flow_length - 2 - min(len(dfg), args.data_flow_length),而position_idx是由source_tokens长度决定的

1
2
3
dfg = dfg[:args.code_length + args.data_flow_length - len(source_tokens)]
source_tokens += [x[0] for x in dfg]
source_ids += [tokenizer.unk_token_id for x in dfg]

DFG 信息被添加到 source_tokenssource_ids 中,确保总长度不超过 args.code_length + args.data_flow_length

1
2
padding_length = args.code_length + args.data_flow_length - len(source_ids)
source_ids += [tokenizer.pad_token_id] * padding_length

如果 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 = 512args.data_flow_length = 128,则:input_ids的长度为 512 + 128 = 640。

这样会导致一个问题,如果input_id的维度是[block_size,args.code_length + args.data_flow_length],如果embedding的维度不同,就会导致模型参数不匹配,进一步导致训练终止

00A38A07理解输入嵌入层工作流程

在上述列子中,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)
))

工作流程:

  1. word_embeddings:将 input_ids 中的每个词元索引通过 word_embeddings 转换为 768 维的向量
  2. Position Embedding:将 position_idx 中的每个位置索引通过 position_embeddings 转换为 768 维的向量
  3. Token Type Embedding
  4. 合并嵌入(Combine Embeddings),将词嵌入、位置嵌入和标记类型嵌入相加,得到每个词元的最终嵌入表示,然后进行 LayerNormdropout 处理,得到模型的输入表示
  5. 输入传递到编码器(Encoder),经过嵌入层处理后的向量被传递到编码器(RobertaEncoder),开始进一步的特征提取和表示学习

问题

应该很容易就看出来,(position_embeddings): Embedding(514, 768, padding_idx=1)的维度是514,而input_idsposition_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.优化和定制模型

这点就不用多说了,比如在预训练好的模型基础上增加一个分类头,专门用于分类训练(我就是这么干的)


文章作者: jingxiaoyang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jingxiaoyang !
评论
  目录