LLM大模型: llama源码要点解读(一)
- 来自meta,一线互联网大厂,质量有保证;自称70b参数的表现比chatGPT3还好(Llama 2:Open Foundation and Fine-Tuned Chat Models)!
- 可能会成为大模型界的Android:各种基于llama的微调和应用会越来越多(llama的模型的参数量7B、13B、70B,凡是和这个参数量一样的的大模型,很有可能是基于llama二次改造的)
核心类不多,就这些:
打开modeling_llama文件:
1、第一个映入眼帘的就是归一化了:
class LlamaRMSNorm(nn.Module): def __init__(self, hidden_size, eps=1e-6): """ LlamaRMSNorm is equivalent to T5LayerNorm """ super().__init__() self.weight = nn.Parameter(torch.ones(hidden_size)) #初始化权重为1 self.variance_epsilon = eps #防止分母为0 def forward(self, hidden_states): input_dtype = hidden_states.dtype hidden_states = hidden_states.to(torch.float32) variance = hidden_states.pow(2).mean(-1, keepdim=True) hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) return self.weight * hidden_states.to(input_dtype)
用公式总结就是:
那么问题来了:llama为啥要用RMSNorm,而不用batchNorm或layerNorm?
3种norm方式综合对比:
- 计算开销:RMSNorm > LayerNorm > BatchNorm。RMSNorm的计算开销最低,因为它不需要计算均值。
- 对小批量数据的适用性:RMSNorm和LayerNorm均优于BatchNorm,适用于小批量甚至单个样本的数据。
- 适用场景:RMSNorm和LayerNorm在序列建模、NLP以及需要处理变长输入的任务中表现更好,而BatchNorm更适合于图像处理和需要大批量数据训练的任务。
- 训练稳定性:RMSNorm通过均方根归一化,在深层神经网络中提供了较为稳定的训练效果。
2、(1)第二个重要的类就是MLP,有的地方也要FFN,就是常见的深度神经网络层:
class LlamaMLP(nn.Module): def __init__(self, config): super().__init__() self.config = config self.hidden_size = config.hidden_size self.intermediate_size = config.intermediate_size # 中间层大小 self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias) #输入升维到中间层 self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias) #输入升维到中间层 self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=config.mlp_bias) #中间层到输出层 self.act_fn = ACT2FN[config.hidden_act] #激活函数 def forward(self, x): if self.config.pretraining_tp > 1: slice = self.intermediate_size // self.config.pretraining_tp #切片 gate_proj_slices = self.gate_proj.weight.split(slice, dim=0) #输入切片 up_proj_slices = self.up_proj.weight.split(slice, dim=0) down_proj_slices = self.down_proj.weight.split(slice, dim=1) #输出切片 gate_proj = torch.cat( [F.linear(x, gate_proj_slices[i]) for i in range(self.config.pretraining_tp)], dim=-1 #每个切片并行执行线性变换后拼接 ) up_proj = torch.cat([F.linear(x, up_proj_slices[i]) for i in range(self.config.pretraining_tp)], dim=-1) #每个切片并行做线性变换后拼接 intermediate_states = (self.act_fn(gate_proj) * up_proj).split(slice, dim=2) down_proj = [ F.linear(intermediate_states[i], down_proj_slices[i]) for i in range(self.config.pretraining_tp) ] down_proj = sum(down_proj)#结果相加 else: down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) return down_proj
和传统的FFN比,llama的MLP只有1点是一样的:输入通过线性变换升维到intermediate中间层,再从intermediate中间层降维到输出层!其他的差异较大,主要体现在:
- 输入切片,并行处理
- gate经过激活函数后继续和up相乘;整个过程图示如下:
那么问题来了:为啥要把input分成gate和up,gate经过激活函数后再乘以up了?
(2)记得10多年前大数据这个概念刚火热时,很多互联网公司利用用户的基础画像、行为数据等做搜广推。这些数据类从业人员最常用炫技的说辞之一:通过亿级的数据维度快速、精准地做推荐!当时我就纳闷了:用户画像一般有几百到几千维度,用户行为数据大概也这个量级,某些从业人员所谓的亿级别维度是哪来的?后来找了好多资料才发现,这么高维度数据的来源竟然是:特征组合!比如“女性”+“年龄” 两两组合成新维度,计算化妆品、服装等的ctr; “男性” + “运动” 两个原本独立的维度两两组合,计算体育用品、体育类视频的ctr;这只是基础特征维度的两两组合,还有三个、四个、五个、甚至更多特征维度的组合了?这么来看,基础特征组合成上亿个维度完全是有可能的!原本线性不可分的样本,经过维度组合后,产生了非线性特征,更容易区分样本,秒啊!顺着这个思路,从代数角度解释神经网络的一些特性就容易多了:
- 为什么神经网络要用激活函数?这里的二阶到N阶,展开后不就是特征的2阶到N阶的组合么?
- 神经网络为什么要先维,再降维? 升维的核心是特征组合:维度提高了激活函数就多啦,组合的特征就多了,能捕捉到的非线性特征就多了!为什么又要降维了?去掉无用的特征组合!
回到llama这里来:输入为什么要分成gate和up?为什么gate经过激活函数后还要和up相乘了?
- 激活函数:参考上述,激活函数经过tylor展开后,不就是特征的N阶组合么?
- 和up相乘,不也是特征之间的两两组合么?
说到底,干的这些事,最终都是做特征的各种花式组合,让线性不可分的数据产生非线性特征,为最终的分类或回归任务产生强特征!
(3) swiglu函数:先把输入向量均分成2份,1份用silu激活后和另一份相乘,本质也是特征两两组合!
silu激活函数和relu比:0点平滑可导,工程上更合适!
3、loss:我个人觉得这个是机器学习最核心的思路了!网络结构确定后,先按照一定的原则随机初始化各个参数,然后正向传播feed forward,计算得到一个结果,和target比得到差距,然后根据这个差距反向传播back propogation,调整网络结构中的各个参数,用来减小loss,最终的目的尽可能贴近target,整个过程和人类学习的思路是一样的!整个transformer架构中,loss计算最核心的两个方法:
(1)compute_loss: 使用传入的model和input,计算loss值后返回
def compute_loss(self, model, inputs, return_outputs=False): """ How the loss is computed by Trainer. By default, all models return the loss in the first element. Subclass and override for custom behavior. """ if self.label_smoother is not None and "labels" in inputs: labels = inputs.pop("labels") else: labels = None outputs = model(**inputs)#使用模型,根据input计算output # Save past state if it exists # TODO: this needs to be fixed and made cleaner later. if self.args.past_index >= 0: self._past = outputs[self.args.past_index] if labels is not None: unwrapped_model = self.accelerator.unwrap_model(model) if _is_peft_model(unwrapped_model): model_name = unwrapped_model.base_model.model._get_name() else: model_name = unwrapped_model._get_name() if model_name in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES.values(): loss = self.label_smoother(outputs, labels, shift_labels=True) else: loss = self.label_smoother(outputs, labels) else: if isinstance(outputs, dict) and "loss" not in outputs: raise ValueError( "The model did not return a loss from the inputs, only the following keys: " f"{','.join(outputs.keys())}. For reference, the inputs it received are {','.join(inputs.keys())}." ) # We don't use .loss here since the model may return tuples instead of ModelOutput. loss = outputs["loss"] if isinstance(outputs, dict) else outputs[0] #取出output的loss return (loss, outputs) if return_outputs else loss
(2)根据loss反向传播更改网络参数的值:
def training_step(self, model: nn.Module, inputs: Dict[str, Union[torch.Tensor, Any]]) -> torch.Tensor: """ Perform a training step on a batch of inputs. Subclass and override to inject custom behavior. Args: model (`nn.Module`): The model to train. inputs (`Dict[str, Union[torch.Tensor, Any]]`): The inputs and targets of the model. The dictionary will be unpacked before being fed to the model. Most models expect the targets under the argument `labels`. Check your model's documentation for all accepted arguments. Return: `torch.Tensor`: The tensor with training loss on this batch. """ # 设置模型为训练模式 model.train() # 准备输入数据 inputs = self._prepare_inputs(inputs) # 如果启用了SageMaker模型并行 if is_sagemaker_mp_enabled(): # 使用SageMaker模型并行进行前向和后向传播,并计算损失 loss_mb = smp_forward_backward(model, inputs, self.args.gradient_accumulation_steps) # 返回平均损失,并将其移动到指定设备(如GPU) return loss_mb.reduce_mean().detach().to(self.args.device) # 使用上下文管理器计算损失 with self.compute_loss_context_manager(): loss = self.compute_loss(model, inputs) # 删除输入数据,释放内存 del inputs torch.cuda.empty_cache() # 初始化空字典,存储额外参数 kwargs = {} # 如果使用LOMO或ADALOMO优化器,需要显式传递学习率 if self.args.optim in [OptimizerNames.LOMO, OptimizerNames.ADALOMO]: kwargs["learning_rate"] = self._get_learning_rate() # 如果使用多GPU训练,计算平均损失 if self.args.n_gpu > 1: loss = loss.mean() # mean() to average on multi-gpu parallel training # 如果使用apex混合精度训练 if self.use_apex: with amp.scale_loss(loss, self.optimizer) as scaled_loss: scaled_loss.backward() # 进行后向传播,计算梯度 else: # 否则,使用加速器进行后向传播 self.accelerator.backward(loss, **kwargs) # 返回损失值,除以梯度累积步骤数 return loss.detach() / self.args.gradient_accumulation_steps
其他要点总结:
1、做所有的重要操作前(包括但不限于attention、softmax等)前都要乘以一个矩阵做线性变换,所以这些操作都是在新的矩阵空间进行的,目的是和之前的旧空间隔开来,避免互相影响:每个矩阵空间只干一件事,如果要做其他事,继续通过矩阵乘法进入下一个空间操作,使得每个矩阵空间都是专用的,互不干扰(有点像每个厕所坑位只能蹲一个人,坑位之间严格隔开,避免互相影响拉便便)!
2、特征组合让线性不可分变成线性可分举例:经典的XOR问题;整个思路很简单:现有的维度分不开了怎么办?那就生成新的能分开的维度呗!怎么生成新维度了?下面这个例子用的是旧维度两两相乘。实际生产环境,还可以N个维度互相相乘,或则N个旧维度之间线性组合(神经网络不就是这么干的嘛!)
3、阅读源码时,建议先看类的两个方法:
- _init_方法:看看生成对象时会生成并初始化哪些变量,肯定都是重要的变量才会在这里生成
- forward方法:看看数据的流转细节,体现了模型的架构实现
参考:
1、https://blog.csdn.net/qq_35812205/article/details/136587013 LLM2模型
2、https://www.bilibili.com/video/BV1qj411y7kF/?spm_id_from=333.788&vd_source=241a5bcb1c13e6828e519dd1f78f35b2 transformers源码阅读——如何看懂模型代码(以llama为例)