从零开始大模型之从头实现GPT模型
- 1.大语言模型整体架构
- 2 大语言的Transformer模块
- 2.1 层归一化
- 2.2 GELU激活函数
- 2.3 前馈神经网络
- 2.4 快捷连接
- 3 附录
- 3.1 anaconda+python环境搭建

1.数据预处理:原始数据进行词元化,以及通过,依据词汇表生成ID编号,对ID编号随机生成嵌入向量,特别注意的是嵌入向量事实上也是一个权重,通过反向传播进行更新。
2.掩码多头注意力机制:增加掩码多头注意力机制,其中增加掩码机制是为了确定序列的输出的因果关系,多头注意力机制是为了对不同路径的来源数据进行上下文联系。
3.LLM架构transform块:进行层归一化,GELU激活函数,前馈神经网络,快捷连接。
1.大语言模型整体架构
数据预处理,掩码多头注意力机制已经学过了,接下来进行最重要的LLM架构的transform块的学习。
#1 Transform块总体概述
整个transform块包含以下内容,层归一化,GELU激活函数,前馈神经网络,快捷连接等等。
2 大语言的Transformer模块
2.1 层归一化
层归一化是为了调整神经网络的输出,让其均值为0,方差为1。目的是为了神经网络更加稳定。具体做法是依据以下的数学公式,对emb_dim维度上进行μ=x−μσ\mu=\frac{x-\mu}{\sigma}μ=σx−μ,最终能够对该维度嵌入向量进行归一化,最后再加上动态可训练参数缩放和平移。
#%% 层归一化
import torch #导入torch包 类似于一个目录,包含了__init__.py以及其他模块的py文件或者其他子包
import torch.nn as nn # 导入torch包中的nn模块 一个模块就是一个.py文件,一个py文件下有函数以及类
torch.manual_seed(123) #固定随机种子
batch_example = torch.randn(2,5) #随机生成两行五列的张量,然后每行是均值为0,方差为1的正态分布
print("batch_example,随机生成的输入为:",batch_example)
layer = nn.Sequential(nn.Linear(5,6),nn.ReLU()) # 容器类装有神经网络类,并通过序列连接,输入连输出,输出也是一个神经网络模块
out = layer(batch_example)
print("自定义的神经网络结构的输出为:",out)
mean = out.mean(dim=-1,keepdim=True) #按照列的方向进行求均值,同时保持维度不变
var = out.var(dim=-1,keepdim=True) #按照行的方向进行求均值,同时保持维度不变
print("未归一化的输出的均值为:",mean,"未归一化的输出的方差为:",var)
#接下来进行归一化
out_norm = (out- mean)/torch.sqrt(var)
torch.set_printoptions(sci_mode=False)
print("归一化后的均值为:",out_norm.mean(dim=-1,keepdim=True))
print("归一化后的方差为:",out_norm.var(dim=-1,keepdim=True))
将上述归一化代码进行整合在一个类中:
class LayerNorm(nn.Module):def __init__(self, emb_dim):super().__init__()self.eps = 1e-5self.scale = nn.Parameter(torch.ones(emb_dim))self.shift = nn.Parameter(torch.zeros(emb_dim))def forward(self, x):mean = x.mean(dim=-1, keepdim=True)var = x.var(dim=-1, keepdim=True, unbiased=False)norm_x = (x - mean) / torch.sqrt(var + self.eps)return self.scale * norm_x + self.shift
最后测试用例:
#%% 归一化测试用例
import torch #导入torch包 类似于一个目录,包含了__init__.py以及其他模块的py文件或者其他子包
import torch.nn as nn # 导入torch包中的nn模块 一个模块就是一个.py文件,一个py文件下有函数以及类
torch.manual_seed(123) #固定随机种子
batch_example = torch.randn(2,5) #随机生成两行五列的张量,然后每行是均值为0,方差为1的正态分布
print("batch_example,随机生成的输入为:",batch_example)
class LayerNorm(nn.Module):def __init__(self, emb_dim):super().__init__()self.eps = 1e-5self.scale = nn.Parameter(torch.ones(emb_dim))self.shift = nn.Parameter(torch.zeros(emb_dim))def forward(self, x):mean = x.mean(dim=-1, keepdim=True)var = x.var(dim=-1, keepdim=True, unbiased=False)norm_x = (x - mean) / torch.sqrt(var + self.eps)return self.scale * norm_x + self.shift
ln = LayerNorm(emb_dim=5)
out_ln = ln(batch_example)
mean = out_ln.mean(dim=-1, keepdim=True)
var = out_ln.var(dim=-1, unbiased=False, keepdim=True)print("Mean:\n", mean)
print("Variance:\n", var)
2.2 GELU激活函数
精确的定义GELU(x)=x⋅ϕ(x)GELU(x)=x \cdot \phi{(x)}GELU(x)=x⋅ϕ(x),其中ϕ(x)\phi{(x)}ϕ(x)是标准高斯分布的累积分布函数,但是在实际操作中,我们会使用一种计算量较小的近似实现,可通过曲线拟合的方法近似得到,GELU(x)≈0.5⋅x⋅{1+tanh[π2⋅(x+0.044715⋅x3)]}GELU(x) \approx 0.5 \cdot x \cdot \{1+\tanh[\sqrt{\frac{\pi}{2}} \cdot (x+0.044715 \cdot x^{3})]\}GELU(x)≈0.5⋅x⋅{1+tanh[2π⋅(x+0.044715⋅x3)]}
#%% GELU激活函数的前馈神经网络
#GELU激活类
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
class GELU(nn.Module):def __init__(self):# 参数初始化super().__init__()def forward(self,x): #输入与输出return 0.5*x*(1+torch.tanh((torch.sqrt(torch.tensor(2/torch.pi)))*(x+0.044715 * torch.power(x,3))))
# GELU激活类与ReLU激活类的对比
gelu,relu = GELU(),nn.ReLU() #神经网络初始化
x = torch.linspace(-3,3,100)#生成-3到3之间的100个数
y_gelu = GELU(x)#前向传播
y_relu= nn.ReLU(x)#前向传播
plt.figure(figsize=(8,3))
for i,(label,y) in enumerate(zip(["ReLU","GELU"],[y_relu,y_gelu]),1):plt.subplot(1,2,i)plt.plot(x,y)plt.title(f"{label} activation function")plt.xlabel("x")plt.ylabel("{label}(x)")plt.grid(True)
plt.tight_layout()
plt.show()
RELU激活函数存在两个问题,第一个问题是当x=0的梯度不存在,第二个问题,输入小于零的输出部分直接被置为0,没有后续神经元的运算。从GELU激活函数与ReLU激活函数对比图可以看出,GELU在小于零部分梯度存在,同时在输入小于0的部分,输出接近于0,并没有置零,还能参与后续的运算。
2.3 前馈神经网络
前馈神经网络先放大输入的节点数,经过GELU激活函数,然后再缩小节点数到与原来的输入的节点数相同,通过这样一个过程,相当于去粗取精。
#%% 前馈神经网络
import torch
import torch.nn as nn
class FeedForward(nn.Module):#通过大模型进行层数的放大,然后再将层数缩小def __init__(self,cfg):#参数初始化,以及神经网络结构的定义super().__init__()self.layers = nn.Sequential(nn.Linear(cfg['emb_dim'],4*cfg['emb_dim']),nn.GELU(),nn.Linear(4*cfg['emb_dim'],cfg['emb_dim']))#用容器来装载神经网络结构def forward(self,x):#定义输入与输出的关系return self.layers(x)
2.4 快捷连接
残差连接实际上就是建立一个输入直接连接到输出,这样的话,下一层输入就变成了上一层的输出和上一层的输入的叠加。建立残差连接能够缓解梯度消失问题。
#%% 残差连接 shortcut
import torch
import torch.nn as nn
class ShortCutNn(nn.Module):def __init__(self,layer_sizes,use_shortcut):#参数初始化以及网络结构的定义super().__init__()self.use_shortcut = use_shortcutself.layers = nn.ModuleList([nn.Sequential(nn.Linear(layer_sizes[0],layer_sizes[1]),nn.GELU())#定义网络列表,nn.Sequential(nn.Linear(layer_sizes[1],layer_sizes[2]),nn.GELU()),nn.Sequential(nn.Linear(layer_sizes[2],layer_sizes[3]),nn.GELU()),nn.Sequential(nn.Linear(layer_sizes[3],layer_sizes[4]),nn.GELU()),nn.Sequential(nn.Linear(layer_sizes[4],layer_sizes[5]),nn.GELU())])def forward(self,x):for layer in self.layers:layer_out = layer(x)if self.use_shortcut and x.shape == layer_out.shape:x = x + layer_outelse:x = layer_outreturn x
3 附录
3.1 anaconda+python环境搭建
接下来进行代码准备,首先下载所有的requirements.txt中的所有依赖库,本次实验是在conda+pycharm环境下,推荐大家在conda+pycharm下进行:
首先创建conda下的环境LLM
conda create --p D:/.conda/envs/LLM
然后激活conda下的LLM环境:
conda activate D:/.conda/envs/LLM
conda install requirements.txt
#%% 1 初始化信息查看
from importlib.metadata import version
print("matplotlib version:", version("matplotlib"))
print("torch version:", version("torch"))
print("tiktoken version:", version("tiktoken"))
#%% 2 配置GPT模型参数配置
GPT_CONFIG_124M = {"vocab_size": 50257, # Vocabulary size 表示会被BPE分词器使用的由50257个单词的词汇表"context_length": 1024, # Context length 能够处理最大词元token的个数"emb_dim": 768, # Embedding dimension 嵌入向量的纬度"n_heads": 12, # Number of attention heads 多头的头数"n_layers": 12, # Number of layers 神经网络的层数"drop_rate": 0.1, # Dropout rate 丢弃率,为了防止过拟合,会随机丢弃10%的隐藏单元"qkv_bias": False # Query-Key-Value bias qkv的偏置项开启与否,默认是关闭
}
nn | 用法 |
---|---|
Embedding | |
Dropout | |
Sequential | |
Linear |