之前的博客已经手敲了一个 Transformer 模型,实际最终训练出来的模型,参数量大概在 1.2 亿左右,文件大小约 505M,在这里我们再来探究一个非常有意思的问题:这个 505M 的文件内部到底存放的是什么? 之前的另外一个博客也讲过本地运行过 ChatGLM3-6B,6B 的模型文件分为 8 个,有的版本是 5 个,几个文件加在一起大约 20G,6B 的 130B 模型文件加起来近 240G。不知道你有没有同样的疑问,在我最早接触大语言模型的时候,就非常好奇,大模型文件里到底存的是什么?随着不断地研究学习,总算有了一知半解,这个博客就来分享一下。
模型文件
所谓模型文件,也可以叫模型权重,里面大部分空间存放的是模型的参数:权重(Weights)和偏置(Biases),当然也有一些其他信息,比如优化器状态、其他元数据,比如 epoch 数等。我们使用的是 PyTorch 框架,生成的模型权重文件格式是.pth,如果使用 TensorFlow 或者 Hugging Face Transformers 等框架,也有可能是.bin 格式的文件。模型预训练完成后,我们可以调用下面的代码保存模型。
只保存权重
torch.save(model.state_dict(), 'model_weights.pth')
保存权重和架构(模型的结构)
torch.save(model, model_path)
一般来说,生产环境不建议把模型的架构也一起保存,因为这个方法保存的模型与 Python 的版本和模型定义的代码紧密相关,如果在不同的环境或 PyTorch 版本中加载,可能会遇到兼容性的问题。而方法一具有更好的兼容性,当你需要迁移到不同的平台或更新项目依赖的时候,这种方式通常会带来更少的问题。
权重和偏置
权重是神经网络中最重要的参数之一。在前向传播过程中,输入数据会与权重相乘,这是神经网络学习特征和模式的基本方式。权重决定了输入数据如何影响输出结果。
偏置是添加到加权输入之后的参数,用于调整输出。偏置允许模型输出在没有输入或所有输入都为零的时候调整到某个基线值。
我们可以简单理解,在线性方程 y=kx+b 中,k 就是权重,b 就是偏置。在神经网络中,权重 k 决定了每个输入特征对于输出的重要性和影响力。偏置 b 是一个常数项,它提供除了输入特征之外的额外输入,允许模型输出可以在没有任何输入或所有输入都为零的时候调整到一个基线或阈值。
根据神经网络的信息知道,在复杂的神经网络中,每个神经元可能都有这样的权重和偏置,用来从前一层接收多个输入信号,对这些信号加权求和后加上偏置,然后通常通过一个非线性激活函数,比如 tanh 和 relu 来生成输出信号,这个输出信号会传递到下一层。每层的权重和偏置都是模型需要学习的参数,它们会根据训练数据进行调整,以最小化模型的预测误差。
模型可视化
通过一些工具,比如 Netron、TensorBoard、torchviz 等,我们可以窥探一下模型内部的结构。我们先通过 Netron 看一下之前训练好的模型的内部结构。Netron 的使用方法比较简单,下载安装,然后运行软件,出现了这样一个页面:
直接点击 Open Model,加载本地存放的权重文件。之后就出现了模型的结构图。
安装参考:
网络可视化工具netron详细安装流程
#方式1 在终端命令下输入(虚拟环境transformer)
pip install netron
#下载好后,在终端下输入 netron,在浏览器上输入 loaclhost:8080 即可
netron
# 方式2 https://github.com/lutzroeder/netron
打开官方Github链接,点击Download处下载文件,并运行安装命令
打开服务器的模型
因为会在本地打开,所以只能选择本地的模型,但是有时候需要打开的是服务器中的模型,可以使用如下方法:
参考:https://blog.youkuaiyun.com/weixin_38346042/article/details/136881037
netron --port 8078 --host 51.159.175.229 /root/test_yang/practice_transformer/model/transformer_model.pth
# 结果返回
Serving '/root/test_yang/practice_transformer/model/transformer_model.pth' at http://51.159.175.229:8078
http://51.159.175.229:8078就是模型的连接,可以直接看
回想上个博客的构建模型的代码,我们设计这个模型的时候,选择了 6 层 Transformer Decoder-only 架构,通过可视化模型结构,我们可以看到模型内部包含的各个层,比如 Embedding、6 个 TransformerDecoderLayer、Linear 等。接下来我们挨个看一下。
Embedding 层
点击第一个 Embedding 节点,右边会展示这个节点的详细信息。
weight(98631*512) 表明这是一个有 98631 行和 512 列的矩阵。在这个矩阵里,每一行对应一个特定的词向量。对一个词汇表中的每一个可能的词或标记,这个矩阵提供了一个 512 维的嵌入向量。在实际处理过程中,这一步不需要乘法计算,只需要一个索引查找操作即可。tensor: float32[98631,512]表明这是一个 FP32 精度的变量,98631 表明训练时使用了 98631 个词汇。后面在模型轻量化的部分我会给你介绍精度的概念
TransformerDecoderLayer
选中其中一个 TransformerDecoderLayer 层,右侧会展示这一层的详细信息,可以看到具体的实现类是 torch.nn.modules.transformer.TransformerDecoderLayer,包含下面这些组件:
torch.nn.modules.activation.MultiheadAttention, torch.nn.modules.activation.MultiheadAttention,
torch.nn.modules.linear.Linear,
torch.nn.modules.dropout.Dropout,
torch.nn.modules.linear.Linear,
torch.nn.modules.normalization.LayerNorm,
torch.nn.modules.normalization.LayerNorm,
torch.nn.modules.normalization.LayerNorm,
torch.nn.modules.dropout.Dropout,
torch.nn.modules.dropout.Dropout,
torch.nn.modules.dropout.Dropout
查看数据流向的方法
1. torchviz
这是模型的整体结构图,我们用另一个工具,查看一下具体的节点图。安装 torchviz,执行下面的代码:
# 安装torchviz包
pip install torchviz
# 跑测试任务
from torchviz import make_dot
# 具体参考下面完整代码
dot_c = make_dot(model.forward(inputs), params=dict(model.named_parameters()), show_attrs=True, show_saved=True)
# dot_c.render(filename="/root/test_yang/practice_transformer/model/myNetModel", format='png')
dot_c.render(filename='/root/test_yang/practice_transformer/model/myNetModel', view=False, format='pdf') # 保存 pdf 到指定路径不打开
问题解决:
# 问题
graphviz.backend.execute.ExecutableNotFound: failed to execute ‘dot‘, make sure the Graphviz
# 安装插件
pip install graphviz
sudo apt install graphviz
解决方案:https://blog.youkuaiyun.com/m0_56367027/article/details/135109523
生成一张大的 png 图片,当然也支持 pdf 格式,改成 format=‘pdf’ 就可以。实际测试下来,pdf 比 png 清晰度更高,我截取了一部分图片,你可以看一下。
2.保存onnx,再netron打开
# 1 onnx模型下载
for epoch in range(num_epochs):
# 循环遍历数据加载器中的每个批次
for i, (inputs, targets) in enumerate(dataloader):
# 将输入数据转置,以符合模型的期望输入维度
inputs = inputs.t()
inputs = inputs.to(device)
targets = targets.to(device)
# 在每次迭代前清空梯度
optimizer.zero_grad()
# 前向传播:计算模型对当前批次的输出
outputs = model.forward(inputs)
# 选择输出的最后一个元素进行损失计算
outputs = outputs[-1]
# 计算损失值
loss = criterion(outputs, targets)
# 反向传播:计算损失的梯度
loss.backward()
# 更新模型的参数
optimizer.step()
# 每隔50步打印一次当前的训练状态
if i % 50 == 0:
# model.eval()
if i % 50 == 0:
# model.eval()
print(f'Epoch [{epoch + 1}/{num_epochs}], Step [{i + 1}/{len(dataloader)}], Loss: {loss.item()}')
# dummy_input = inputs.long()
torch.save(model.state_dict(), '/root/test_yang/practice_transformer/model/transformer_model_{}.pth'.format(i))
# model.export(format='onnx')
# shape = inputs.shape
# print(shape)
# dummy_input = torch.randn(shape)
# make_dot数据流向方式
dot_c = make_dot(model.forward(inputs), params=dict(model.named_parameters()), show_attrs=True, show_saved=True)
# dot_c.render(filename="/root/test_yang/practice_transformer/model/myNetModel", format='png')
dot_c.render(filename='/root/test_yang/practice_transformer/model/myNetModel', view=False, format='pdf') # 保存 pdf 到指定路径不打开
# onnx数据流向方式
torch.onnx.export(model, inputs, "/root/test_yang/practice_transformer/model/test.onnx", do_constant_folding=False)
# 2. 打开onnx
netron --port 8078 --host 51.159.175.229 /root/test_yang/practice_transformer/model/test.onnx
这里有整个网络结构的顺序图,具体每个节点的含义,感兴趣的话你可以查询下,比如 AccumulateGrad、SplitBackward0、UnsqueezeBackward0 这些,基本都是和计算损失相关的。你要知道,模型的训练过程就是不断调整权重的过程,调整权重的依据就是根据损失函数计算损失,通过反向传播不断调整权重使损失最小,达到理想损失值后,把权重记录下来保存。
在推理的时候就简单了,数据达到节点(神经元)后,先从输入层到隐藏层,再从隐藏层到输出层。每一层都执行类似 y=kx+b 的公式计算,然后应用激活函数,比如 a=ReLU(y),最后把 a 继续传递到下一层,当做
x
2
x_{2}
x2,下一层的权重
k
2
k_{2}
k2 和偏置
b
2
b_{2}
b2 是已知的,继续计算得出
y
2
y_{2}
y2。这里就不赘述了。另外,基本都是张量相乘,而不是简单的整数小数相乘。
模型内部的逻辑基本就是这样,机器学习框架如 PyTorch 可以根据描述文件,将模型重构出来,进行训练和推理。接下来我们再看一下模型的容量。
模型容量
这节课我们使用的模型大小是 505M,根据上节课模型参数量的计算方式,我们得出参数量 N=1.2 亿,模型的精度使用的是 float32。也就是说,每个参数需要 4 字节的存储空间,所以我们大致算一下,纯参数方面需要约 460M 的空间(1.2 亿 *4/1024/1024),其余的 40M 空间可能存放的是模型结构、元数据等等。
上面我们提到的 Embedding 层,权重是 tensor: float32[98631,512],表示在 Embedding 层参数量 =98631*512=50491392,存储量大约在 200M,所以这么看来 Embedding 层在存储方面基本就占用了整个模型的 40%,参数方面,5049 万 /1.2 亿也约等于 40%,所以 Embedding 层在整个模型中的作用还是非常大的。
模型参数
基于这些知识,我们再来梳理一下参数的含义,还是以 embedding 层来解释,我们举一个小一点的例子,用 5*10 的矩阵来讲。
这是一个具有 50 个参数的矩阵,每个参数(矩阵中的每个数值)表示词向量在特定维度上的权重或特征。例如,第一行 [0.5, -0.2, 0.3, …, 0.7] 表示第一个词在 10 个不同特征维度上的数值表示。如果我们把 Embedding 层设置为“可训练”,那么在模型训练的过程中会根据损失计算,反向传播后更新某个参数,这样可以让模型更好地表示某个词,来达到训练的目的。在最终推理过程中,词向量会被传播下去,作为下一层的神经网络的输入,参与到后续计算过程中。
小结
这节课的内容比较有意思,而且很少有人从这个角度去分析。实际上是前面课程内容的一个反向理解,就好比通过 Java 字节码文件反推 Java 编码过程一样。
- 模型里存放的是各个层的权重和偏置,类似于 y=kx+b 里的 k 和 b。
- 机器学习框架如 PyTorch 可以识别模型文件,并且把模型结构重构出来进行训练和推理。
- 模型训练过程就是不断前向传播、损失计算、反向传播、参数更新的过程。
- 模型推理就是根据训练好的参数,进行前向传播的过程。
- 我们可以使用 Netron、torchviz 等工具可视化模型结构,辅助理解。