文章目录
写在前面
大型模型的神秘并不是不可透视的,今天我们以ChatGLM-6B为例,解析一下模型结构和代码。你会发现,大模型结构并没有那么神秘,相反还挺清晰的,就是Transformer的decoder改造而来的。我们还会看到模型中参数最密集的部分,这也是模型“大”的原因。
一、整体流程
ChatGLM和其他生成模型一样,都是迭代输出,每次生成一个token,然后把输出的token
拼到输入,进行下一轮迭代。我们以输入“你好吗”为例,假设输出最大长度512个token,简单梳理一下整个生成流程:
1.输入
上图中模拟了两轮迭代时模型中数据的形状,黑字是第一次迭代时模型中数据的形状,如果在第二次迭代中发生变化我用红字标出,在以后的轮次中都会遵循规律进行下去。可以看到,模型输入有三个,他们其实都要送入模型的核心模块—GLMBlock:
a.input_ids: 输入的token序列,它会经过Embedding转换成hidden_state,每次迭代这个值都会拼上输出,进行下一次迭代。
b.attention_mask: 根据输入的维度计算出的mask矩阵,每次迭代宽高都会加1,规律如下图,其中x是会被mask掉的token:
c.position_ids: 2d旋转位置编码,
2.流程
a.将输入做Embedding转换成hidden_state;根据输入的维度计算出的mask矩阵;根据input_ids和mask计算出position_ids。
b.将上面三个值送入28个GLMBlock,这28个GLMBlock序贯排列。每个GLMBlock的输入除了上述三个参数,还有一个layer_id,layer_id每轮+1,也就是从1-28。每个GLMBlock的输出是hidden_state,重新赋值hidden_state后进入下一轮迭代,总共28轮。
c.28个GLMBlock之后输出最后一个的hidden_state,更大规模的模型,GLMBlock就会更多。
d.hidden_state经过线性变换。也就是全连接转换成词典大小130528。
e.130528的维的结果执行argmax,得到输出的token
f.将新的token拼到input_ids后面
g.如果token序列达到预设的最大长度或最后的token=eos_token (30005)则迭代结束;否则继续迭代。
3.代码
while True:
...
# 1.准备输入,调用modeling_chatglm中的ChatGLMForConditionalGeneration.prepare_inputs_for_generation()方法
model_inputs = self.prepare_inputs_for_generation(input_ids, **model_kwargs)
# 2.执行模型推理,调用modeling_chatglm中的ChatGLMModel.forword()方法
outputs = self(
**model_inputs,
return_dict=True,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
)
if synced_gpus and this_peer_finished:
continue # don't waste resources running the code we don't need
# 3.输出后处理
next_token_logits = outputs.logits[:, -1, :]
next_tokens_scores = logits_processor(input_ids, next_token_logits)
...
# 4.判断结束
if eos_token_id_tensor is not None:
unfinished_sequences = unfinished_sequences.mul(
next_tokens.tile(eos_token_id_tensor.shape[0], 1).ne(eos_token_id_tensor.unsqueeze(1)).prod(dim=0)
)
# stop when each sentence is finished
if unfinished_sequences.max() == 0:
this_peer_finished = True
this_peer_finished = 真
# stop if we exceed the maximum length
if stopping_criteria(input_ids, scores):
this_peer_finished = True
# 5.修改input_ids 和一些状态数据,为下一和token的预测做准备
input_ids = torch.cat([input_ids, next_tokens[:, None]], dim=-1)
...
ChatGLMModel源码如下:
class ChatGLMModel(ChatGLMPreTrainedModel):
def __init__(self, config: ChatGLMConfig, empty_init=True):
super().__init__(config)
if empty_init:
init_method = skip_init
else:
init_method = default_init
# recording parameters
self.max_sequence_length = config.max_sequence_length
self.hidden_size = config.hidden_size
self.params_dtype = torch.half
self.num_attention_heads = config.num_attention_heads
self.vocab_size = config.vocab_size
self.num_layers = config.num_layers
self.layernorm_epsilon = config.layernorm_epsilon
self.inner_hidden_size = config.inner_hidden_size
self.hidden_size_per_attention_head = self.hidden_size // self.num_attention_heads
self.position_encoding_2d = config.position_encoding_2d
self.pre_seq_len = config.pre_seq_len
self.prefix_projection = config.prefix_projection
self.word_embeddings = init_method(
torch.nn.Embedding,
num_embeddings=self.vocab_size, embedding_dim=self.hidden_size,
dtype=self.params_dtype
)
self.gradient_checkpointing = False
def get_layer(layer_id):
return GLMBlock(
self.hidden_size,
self.num_attention_heads,
self.layernorm_epsilon,
layer_id,
inner_hidden_size=self.inner_hidden_size,
hidden_size_per_attention_head=self.hidden_size_per_attention_head,
layernorm=LayerNorm,
use_bias=True,
params_dtype=self.params_dtype,
position_encoding_2d=self.position_encoding_2d,
empty_init=empty_init
)
self.layers = torch.nn.ModuleList(
[get_layer(layer_id) for layer_id in range(self.num_layers)]
)
# Final layer norm before output.
self.final_layernorm = LayerNorm(self.hidden_size, eps=self.layernorm_epsilon)
if self.pre_seq_len is not None:
for param in self.parameters():
param.requires_grad = False
self.prefix_tokens = torch.arange(self.pre_seq_len).long()
self.prefix_encoder = PrefixEncoder(config)
self.dropout = torch.nn.Dropout(0.1)
# total_params = sum(p.numel() for p in self.parameters())
# trainable_params = sum(p.numel() for p in self.parameters() if p.requires_grad)
# print("Using p-tuning v2: # trainable_params = {} / {}".format(trainable_params, total_params))
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
position_ids: Optional[torch.LongTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
past_key_values: Optional[Tuple[Tuple[torch.Tensor, torch.Tensor], ...]] = None,
inputs_embeds: Optional[torch.LongTensor] = None,
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
)
-> Union[Tuple[torch.Tensor, ...], BaseModelOutputWithPast]:
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
use_cache = use_cache if use_cache is not None else self.config.use_cache
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
if input_ids is not None and inputs_embeds is not None:
raise ValueError("You cannot specify both input_ids and inputs_embeds at the same time")
elif input_ids is not None:
batch_size, seq_length = input_ids.shape[:2]
elif inputs_embeds is not None:
batch_size, seq_length = inputs_embeds.shape[:2]
else:
raise ValueError("You have to specify either input_ids or inputs_embeds")
# [seq_len, batch, hidden_size]
hidden_states = inputs_embeds.transpose(0, 1)
presents = () if use_cache else None
all_self_attentions = () if output_attentions else None
all_hidden_states = () if output_hidden_states else None
if attention_mask is None:
attention_mask = torch.zeros(1, 1, device=input_ids.device).bool()
else:
attention_mask = attention_mask.to(hidden_states.device)
for i, layer in enumerate(self.layers):
if output_hidden_states:
all_hidden_states = all_hidden_states + (hidden_states,)
layer_past = past_key_values[i]
layer_ret = layer(
hidden_states,
position_ids=position_ids,
attention_mask=attention_mask,
layer_id=torch.tensor(i),
layer_past=layer_past,
use_cache=use_cache,
output_attentions=output_attentions
)
hidden_states = layer_ret[0]
if use_cache:
presents = presents + (layer_ret[1],)
if output_attentions:
all_self_attentions = all_self_attentions + (layer_ret[2 if use_cache else 1],)
# Final layer norm.
hidden_states = self.final_layernorm(hidden_states)
if output_hidden_states:
all_hidden_states = all_hidden_states + (hidden_states,)
if not return_dict:
return tuple(v for v in [hidden_states, presents, all_hidden_states, all_self_attentions] if v is not None)
return BaseModelOutputWithPast(
last_hidden_state=hidden_states,
past_key_values=presents,
hidden_states=all_hidden_states,
attentions=all_self_attentions,
)
二、GLMBlock
GLMBlock是核心模块,整体流程如下:
1.整体流程
a.LayerNorm: hidden_states经过LayerNorm,然后送入SelfAttention
b.SelfAttention:
输入经过全连接升维至12288,然后拆出QKV;
QK结合position_ids和block_position_ids,分别经过2d旋转位置编码重新编排;
新的Q和K做内积并进行Attention缩放,得到attention_scores;
使用mask将部分attention_scores的值去掉(设置成一个很小的值),再经过softmax处理,attention_scores是一个概率图,标志每个token和其它token的相关性;
将attention_scores与V做内积,即对V施加注意力机制,得到attention_output;
attention_input与attention_output做残差相加,attention_input的系数是,其中num_layers=28。
c.LayerNorm: 再LayerNorm一遍
d.GLU: Gated Linear Unit(门控线性单元),也可以叫做FFN(Feedforward Neural Network)或者MLP(Multilayer Perceptron),由两个全连接夹着一个GELU激活函数组成。GLU之后接一个带权重的残差输出。
GLMBlock代码:
class GLMBlock(torch.nn.Module):
def __init__(
self,
hidden_size,
num_attention_heads,
layernorm_epsilon,
layer_id,
inner_hidden_size=None,
hidden_size_per_attention_head=None,
layernorm=LayerNorm,
use_bias=True,
params_dtype=torch.float,
num_layers=28,
position_encoding_2d=True,
empty_init=True
):
super(GLMBlock, self).__init__()
# Set output layer initialization if not provided.
self.layer_id = layer_id
# Layernorm on the input data.
self.input_layernorm = layernorm(hidden_size, eps=layernorm_epsilon)
self.position_encoding_2d = position_encoding_2d
# Self attention.
self.attention = SelfAttention(
hidden_size,
num_attention_heads,
layer_id,
hidden_size_per_attention_head=hidden_size_per_attention_head,
bias=use_bias,
params_dtype=params_dtype,
position_encoding_2d=self.position_encoding_2d,
empty_init=empty_init
)
# Layernorm on the input data.
self.post_attention_layernorm = layernorm(hidden_size, eps=layernorm_epsilon)
self.num_layers = num_layers
# GLU
self.mlp = GLU(
hidden_size,
inner_hidden_size=inner_hidden_size,
bias=use_bias,
layer_id=layer_id,
params_dtype=params_dtype,
empty_init=empty_init
)
def forward(
self,
hidden_states: torch.Tensor,
position_ids,
attention_mask: torch.Tensor,
layer_id,
layer_past: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
use_cache: bool = False,
output_attentions: bool = False,
):
"""
hidden_states: [seq_len, batch, hidden_size]
attention_mask: [(1, 1), seq_len, seq_len]
"""
# Layer norm at the begining of the transformer layer.
# [seq_len, batch, hidden_size]
attention_input = self.input_layernorm(hidden_states)
# Self attention.
attention_outputs = self.attention(
attention_input,
position_ids,
attention_mask=attention_mask,
layer_id=layer_id,
layer_past=layer_past,
use_cache=use_cache,
output_attentions=output_attentions
)
attention_output = attention_outputs[0]
outputs = attention_outputs[1:]
# Residual connection.
alpha = (2 * self.num_layers) ** 0.5
hidden_states = attention_input * alpha + attention_output
mlp_input = self.post_attention_layernorm(hidden_states)
# MLP.
mlp_output = self.mlp(mlp_input)
# Second residual connection.
output = mlp_input * alpha + mlp_output
if use_cache:
outputs = (output,) + outputs
else:
outputs = (output,) + outputs[1:]
return outputs # hidden_states, present, attentions
2.旋转位置编码
上面提到的旋转位置编码是一种用绝对位置编码的方式实现相对位置编码的编码方式,
3.Attention缩放
上面是Attention公式,其中d是查询向量和键向量的维度,这里是128,Attention缩放指的是。那为什么QK要除以呢,原因如下:
a.如果softmax的输入值过大,那它的梯度会趋近于0,导致梯度消失,所以要保证softmax的输入大小合适,最好是一个有效的概率分布。
b.在注意力机制中,通常会对注意力分数进行归一化处理,即N(0, 1)。在经过layernom之后,QK能符合这个标准,他俩的内积均值还是0 ,但是标准差变成了d,推导过程如下:
由于每个元素 和 都是独立的,并且符合 N(0,1) 分布,所以矩阵 Q 和 K 的内积的方差等于所有元素的方差之和。
由于 和 都是来自 N(0,1)的独立随机变量,它们的方差都是 1,所以矩阵 Q 和 K 的内积的方差为所有元素的方差之和。
因此,在这种情况下,矩阵 Q 和 K 内积的标准差是它们的维度 nn。所以要让std(QK)=1,除以就可以了。
4.mask的作用
我们观察mask矩阵发现,在生成每个token时,通常会“mask掉”刚刚生成的token,这主要有下面几个原因:
a.避免重复 :如果模型在生成下一个token时还看到刚刚生成的token,它可能会过分关注这个token,并倾向于重复它,这会使得生成的文本显得不自然和重复。
b.防止错误累积:在生成较长的文本序列时,如果模型在某个步骤生成了一个不合适的token,而这个token又被用于生成后续的token,可能会导致错误在序列中累积,影响整个生成文本的质量。通过mask掉刚刚生成的token,可以减少这种错误累积的可能性。
c.缺乏远见:当模型在生成文本时,它应该考虑整个上下文的历史,并预测一个能够推动故事或对话向前发展的token。如果模型过分关注刚刚生成的token,它可能会忽视更广泛的上下文,导致生成的文本缺乏远见和创造性。
5.残差系数
在两个残差处,都使用了缩放系数,其中num_layers=28。
因为28个GLMBlock是串联的,每个GLMBlock的输出都要与输入做融合。那么有一个缩放系数是必要的,从这个系数可以看出输入与输出的权重比例大约为7.4:1,随着层数的增加,每层输出的影响力是一点一点增加的,这很合理。
那缩放系数为什么是这个样子呢,我能力有限没有找到原因,欢迎知道的大佬指点一下。但是可以简单的看一下这个系数的意义:
a.模型的规模变大,num_layers变大,组成结果的层数变大,每层(即输出)的权重变小是合理的;反之亦然。所以这个系数与层数成正比是合理的。
b.开根号可以使这个数的分布更平滑
c.2可以调节输入的权重规模,起到一定的人为调控的作用。
6. FFN (又称MLP)
FFN由两个全连接夹着一个GELU组成,两个全连接的中间维度一般设置为hidden_state的整数倍,这里是4(即4096*4=16384)
值得一提的是这里是模型参数最密集的地方,有两个全连接。而且从Transformer诞生到现在,有很多部分已经得到优化,唯独没有这块的优化。实验表明,如果FFN变小,模型整体的性能也会随之下降,基本可以认定,模型的“知识”是存在于FFN中的,所以这里需要更多的参数。
代码如下:
class GLU(torch.nn.Module):
def __init__(self, hidden_size, inner_hidden_size=None,
layer_id=None, bias=True, activation_func=gelu, params_dtype=torch.float, empty_init=True):
super(GLU, self).__init__()
if empty_init:
init_method = skip_init
else:
init_method = default_init
self.layer_id = layer_id
self.activation_func = activation_func
# Project to 4h.
self.hidden_size = hidden_size
if inner_hidden_size is None:
inner_hidden_size = 4 * hidden_size
self.inner_hidden_size = inner_hidden_size
self.dense_h_to_4h = init_method(
torch.nn.Linear,
self.hidden_size,
self.inner_hidden_size,
bias=bias,
dtype=params_dtype,
)
# Project back to h.
self.dense_4h_to_h = init_method(
torch.nn.Linear,
self.inner_hidden_size,
self.hidden_size,
bias=bias,
dtype=params_dtype,
)
def forward(self, hidden_states):
"""
hidden_states: [seq_len, batch, hidden_size]
"""
# [seq_len, batch, inner_hidden_size]
intermediate_parallel = self.dense_h_to_4h(hidden_states)
intermediate_parallel = self.activation_func(intermediate_parallel)
output = self.dense_4h_to_h(intermediate_parallel)
return output
三、总结
最后总结一下:
1.ChatGLM是根据Transformer的decoder改造的;
2.位置编码方式是旋转位置编码;
3.残差缩放系数 ;
4.GLMBlock的结构是LayerNorm+SelfAttention+LayerNorm+GLU(FFN);
5.ChatGLM的核心是GLMBlock, 不通大小的模型是通过调整GLMBlock的个数来实现,比如6B有28个,10B有48个;
6.模型的“知识”是存在于FFN中的,这也是大模型最“大”的地方。
7.从模型结构来看,理论上输入和输出的长度是没有限制的,但实际使用中一定要设置一个最大长度(从ChatGLM的原来来看,这个长度值得是输入和输出的总长度),这是因为算力限制,而且训练集的长度不是无限的,一般情况下训练集限制在什么长度决定了推理时能支持的最大长度。
ChatGLM的结构整体看下来中规中矩,但是效果很不错,除了模型本身,训练方式也很重要,接下来开始训练模型,敬请期待。
ChatGLM模型结构就介绍到这里,关注不迷路(#.#)
零基础入门学习大模型
还贴心为大家准备好了一系列的资源,都是通过作者花费大量时间在各个平台收集到的,决定把这些AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费
】🆓
👉AI大模型学习路线汇总👈
大模型学习路线图,整体分为7个大的阶段:(全套教程文末领取哈)
第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;
第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;
第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;
第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;
第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;
第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;
第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。
👉大模型实战案例👈
光学理论是没用的,要学会跟着一起做,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
👉大模型视频和PDF合集👈
观看零基础学习书籍和视频,看书籍和视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
👉学会后的收获:👈
• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;
• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;
• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;
• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。
👉点击获取
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费
】🆓