【教程】从0开始搭建大语言模型:构造GPT模型

从0开始搭建大语言模型:构造GPT模型

  • 从0开始搭建大语言模型:构造GPT模型
    • GPT模型
    • Layer Normalization
    • GELU激活函数
    • Feed Forward网络
    • 增强shortcut连接
    • 构造Transformer Block
    • 构造GPT模型
    • 使用GPT模型生成文本

从0开始搭建大语言模型:构造GPT模型

接上文:【教程】从0开始搭建大语言模型:实现Attention机制

GPT模型

GPT,全称是Generative Pretrained Transformer,它是大型深度神经网络架构,旨在一次生成一个单词(或token)。GPT的大致流程为:
在这里插入图片描述
GPT-2的最小版本也有1.24亿个参数,我们通过以下Python字典指定小型GPT-2模型的配置:

GPT_CONFIG_124M = {"vocab_size": 50257, # Vocabulary size"context_length": 1024, # Context length"emb_dim": 768, # Embedding dimension"n_heads": 12, # Number of attention heads"n_layers": 12, # Number of layers"drop_rate": 0.1, # Dropout rate"qkv_bias": False # Query-Key-Value bias
}

参数的意义为:

  • vocab_size:一个包含50257个单词的词汇表,由BPE tokenizer使用
  • context_length:表示模型可以处理的最大输入token数
  • emb_dim:表示embedding大小,将每个token转换为768维向量
  • n_heads:表示多头注意机制中注意头的数量
  • n_layers:指定模型中Transformer块的数量
  • drop_rate:表示 dropout机制的概率(0.1表示删除10%的神经元),以防止过拟合
  • qkv_bias:决定是否在多头attention中查询、键和值的线性层中包含一个偏置向量

要构建一个GPT模型,我们需要依次完成下图的模块:
在这里插入图片描述
下图展示了输入数据如何tokenized、embedding并提供给GPT模型的整体概述:
在这里插入图片描述
在LLM中,embedding的输入token维度通常与输出维度匹配。

下面将展示如何实现GPT的各个模块。

Layer Normalization

由于梯度消失或梯度爆炸等问题,训练具有许多层的深度神经网络有时可能具有挑战性。这些问题导致训练动态不稳定,并使网络难以有效调整其权重,这意味着学习过程很难找到一组神经网络参数(权重),以最小化损失函数。换句话说,网络很难学习数据中的基本模式,以使其做出准确的预测或决策。

为了解决这个问题,可以采用一些归一化来防止梯度消失或梯度爆炸。在LLM中,我们使用Layer Normalization来达到这一点。

层归一化背后的主要思想是调整神经网络层的激活(输出),使其均值为0,方差为1,这种调整加快了收敛到有效权重的速度,并确保了一致、可靠的训练。

层归一化的一个示例如下:
在这里插入图片描述
要简单地实现层归一化,代码为:

torch.manual_seed(123)
batch_example = torch.randn(2, 5) #A
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:\n", mean)
print("Variance:\n", var)
# 层归一化
out_norm = (out - mean) / torch.sqrt(var)
mean = out_norm.mean(dim=-1, keepdim=True)
var = out_norm.var(dim=-1, keepdim=True)
print("Normalized layer outputs:\n", out_norm)
print("Mean:\n", mean)
print("Variance:\n", var)

需要注意,代码中dim参数指定在张量中计算统计量(这里是均值或方差)时应该沿着的维度,-1表示最后一维。不同dim的数据计算展示如下:
在这里插入图片描述
Layer Normalization类的代码为:

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

需要注意,因为layer normalization是作用于最后一个维度,因此self.scaleself.shift的参数维度也是emb_dim。eps是为了防止分母为0。scale和shift是两个可训练参数(与输入相同维度),LLM会在训练期间自动调整。这允许模型学习最适合其处理数据的适当缩放和平移

和批归一化的比较:与批归一化对批维度进行归一化不同,层归一化对特征维度进行归一化。LLM通常需要大量的计算资源,而可用的硬件或特定的用例可以决定训练或推理期间的批大小。由于层归一化独立于批量大小对每个输入进行归一化,因此它在这些场景中提供了更多的灵活性和稳定性。

GELU激活函数

在LLM中,通常采用GELUSwiGLU函数,而不是传统的ReLUGELUSwiGLU是更复杂和平滑的激活函数,分别包含高斯门控线性单元和sigmoid门控线性单元。本部分主要介绍GELU

GELU的公式为:GELU(x)=x Φ(x),其中Φ(x)是标准高斯分布的累积分布函数。但通常不使用这种方法计算,而是使用另外一种计算更高效的估计:

GELU(x) ≈ 0.5 ⋅ x ⋅ (1 + tanh[√((2/π)) ⋅ (x + 0.044715 ⋅ x^3])

用代码实现为:

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.0 / torch.pi)) *(x + 0.044715 * torch.pow(x, 3))))

ReLU和GELU的图像比较如下:
在这里插入图片描述
从图中可以看出,ReLU是一个分段线性函数,如果输入为正,则直接输出;否则,它输出0。GELU是一个光滑的非线性函数,它近似于ReLU,但对于负值具有非零梯度,这种平滑属性可以让模型在训练过程中更好地优化。

ReLU在零处有一个尖角,这有时会使优化变得更加困难,特别是在深度非常深或具有复杂架构的网络中。对于负数,GELU允许一个较小的非零输出。这一特性意味着在训练过程中,接受负输入的神经元仍然可以对学习过程做出贡献,尽管程度小于正输入。

Feed Forward网络

有了GELU后,我们将它应用在Feed Forward Network(FFN)中,FFN模块是一个由两个线性层和一个GELU激活函数组成的小型神经网络,代码如下:

class FeedForward(nn.Module):def __init__(self, cfg):super().__init__()self.layers = nn.Sequential(nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),GELU(),nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),)def forward(self, x):return self.layers(x)

这个模块可以增强模型从数据中学习和泛化的能力。尽管该模块的输入和输出维度相同,但它通过第一个线性层在内部将embedding维度扩展到更高维的空间。这种扩展之后是非线性的GELU激活,然后通过第二个线性变换收缩回到原始维度。这样的设计允许探索更丰富的表征空间

增强shortcut连接

shortcut connections,也被称为skip connections或者residual connections,它是为了缓解梯度消失的问题。梯度消失问题是指梯度(在训练期间指导权重更新)随着在各层中反向传播而逐渐变小的问题,使其难以有效训练较早的层。

有shortcut connections和没有shortcut connections的结构比较如下:
在这里插入图片描述
从图中可以看出,shortcut connections通过跳过一个或多个层为梯度创建了一个替代的、更短的路径,以通过网络,这是通过将一层的输出添加到后面一层的输出来实现的。

shortcut connections是非常大的模型(如LLM)的核心构建块,当我们训练GPT模型时,它们将通过确保跨层的一致梯度流来帮助促进更有效的训练。

如果你想对shortcut connections的作用实验,可以通过如下代码:

class ExampleDeepNeuralNetwork(nn.Module):def __init__(self, layer_sizes, use_shortcut):super().__init__()self.use_shortcut = use_shortcutself.layers = nn.ModuleList([# Implement 5 layersnn.Sequential(nn.Linear(layer_sizes[0], layer_sizes[1]), GELU()),nn.Sequential(nn.Linear(layer_sizes[1], layer_sizes[2]), GELU()),nn.Sequential(nn.Linear(layer_sizes[2], layer_sizes[3]), GELU()),nn.Sequential(nn.Linear(layer_sizes[3], layer_sizes[4]), GELU()),nn.Sequential(nn.Linear(layer_sizes[4], layer_sizes[5]), GELU())])def forward(self, x):for layer in self.layers:# 计算输出layer_output = layer(x)# shortcut是否被应用if self.use_shortcut and x.shape == layer_output.shape:x = x + layer_outputelse:x = layer_outputreturn xlayer_sizes = [3, 3, 3, 3, 3, 1]
sample_input = torch.tensor([[1., 0., -1.]])
torch.manual_seed(123) # specify random seed for the initial weights for reproducibility
model_without_shortcut = ExampleDeepNeuralNetwork(layer_sizes, use_shortcut=False
)
# 打印梯度
def print_gradients(model, x):# 前向过程output = model(x)target = torch.tensor([[0.]])# 计算损失loss = nn.MSELoss()loss = loss(output, target)# 后向过程计算梯度loss.backward()for name, param in model.named_parameters():if 'weight' in name:# 输出梯度的均值print(f"{name} has gradient mean of {param.grad.abs().mean().item()}")
# 没有shortcut连接的输出
print_gradients(model_without_shortcut, sample_input)torch.manual_seed(123)
model_with_shortcut = ExampleDeepNeuralNetwork(layer_sizes, use_shortcut=True
)
# 有shortcut连接的输出
print_gradients(model_with_shortcut, sample_input)

构造Transformer Block

Transformer块的组成如下:
在这里插入图片描述
当transformer块处理输入序列时,序列中的每个元素(例如,单词或子单词token)由固定大小的向量(768维)表示。transformer块内的操作,包括多头注意力和前馈层,旨在以保留其维度的方式变换这些向量。

多头注意力块中的自注意力机制识别并分析输入序列中元素之间的关系,而前馈网络在每个位置单独修改数据。这种组合不仅能够更细致地理解和处理输入,而且还增强了模型处理复杂数据模式的整体能力。

Transformer块的代码为:

class TransformerBlock(nn.Module):def __init__(self, cfg):super().__init__()self.att = MultiHeadAttention(d_in=cfg["emb_dim"],d_out=cfg["emb_dim"],block_size=cfg["context_length"],num_heads=cfg["n_heads"],dropout=cfg["drop_rate"],qkv_bias=cfg["qkv_bias"])self.ff = FeedForward(cfg)self.norm1 = LayerNorm(cfg["emb_dim"])self.norm2 = LayerNorm(cfg["emb_dim"])self.drop_resid = nn.Dropout(cfg["drop_rate"])def forward(self, x):#Ashortcut = xx = self.norm1(x)x = self.att(x)x = self.drop_resid(x)x = x + shortcut # short连接shortcut = x #Bx = self.norm2(x)x = self.ff(x)x = self.drop_resid(x)x = x + shortcut #Creturn x

需要注意的是,在MultiHeadAttentionFeedForward之前应用层归一化(LayerNorm),在它们之后应用dropout,以使模型规范化并防止过拟合。这种在之前应用LayerNorm的方式称为Pre-LayerNorm。在自注意力和前馈网络之后应用层归一化的方式称为Post-LayerNorm,这种可能导致不稳定的训练。

transformer块在其输出中保持输入尺寸,这表明transformer架构在处理数据序列时不会改变它们在整个网络中的形状。这种设计使其能够在广泛的序列到序列任务中有效应用,其中每个输出向量直接对应于一个输入向量,保持一对一的关系。

然而,输出是一个上下文向量,它封装了来自整个输入序列的信息。这意味着虽然序列的物理维度(长度和特征大小)在通过transformer块时保持不变,但每个输出向量的内容被重新编码,以整合整个输入序列的上下文信息

构造GPT模型

GPT模型的整体结构为:
在这里插入图片描述
其中最后一个transformer块的输出在到达线性输出层之前经过最后一个层归一化步骤。线性输出层将transformer的输出映射到高维空间(在本例中,50,257维,对应于模型的词汇表大小),以预测序列中的下一个token。

在之前代码的基础上,我们可以构造最终的GPT模型,代码为:

class GPTModel(nn.Module):def __init__(self, cfg):super().__init__()self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])self.drop_emb = nn.Dropout(cfg["drop_rate"])self.trf_blocks = nn.Sequential(*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])self.final_norm = LayerNorm(cfg["emb_dim"])self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)def forward(self, in_idx):batch_size, seq_len = in_idx.shapetok_embeds = self.tok_emb(in_idx)#Apos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))x = tok_embeds + pos_embedsx = self.drop_emb(x)x = self.trf_blocks(x)x = self.final_norm(x)logits = self.out_head(x)return logits

输出模型的参数量大小:

torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
total_params = sum(p.numel() for p in model.parameters())
print(f"Total number of parameters: {total_params:,}")

最终输出的参数量会是163 million,跟124 million不符合。

这个原因是原始的GPT-2架构中使用了一个名为权重绑定的概念,这意味着原始的GPT-2架构正在重用来自token embedding层和输出层的权重。

移除掉输出层的参数量后,最终的参数量会和124 million一致:

total_params_gpt2 = total_params - sum(p.numel() for p in model.out_head.parameters())
print(f"Number of trainable parameters considering weight tying: {total_params_gpt2:,}")

使用GPT模型生成文本

LLM每次生成一个word,过程展示如下:
在这里插入图片描述
模型在每次迭代中预测一个后续token,并将其附加到输入上下文以进行下一轮预测。

在GPT生成文本过程中:

  • 模型会输出一个矩阵,它表示可能的下一个单词的向量。
  • 接着提取与下一个token对应的向量,并通过softmax函数转换为概率分布。
  • 在包含结果概率分数的向量中,位于最高值的索引,它转换为token ID。
  • 然后,这个token ID被解码回文本,生成序列中的下一个token。
  • 最后,这个标记被添加到前面的输入中,形成一个新的输入序列用于后续的迭代。

具体过程如下:

在这里插入图片描述
该部分的代码为:

def generate_text_simple(model, idx, max_new_tokens, context_size): #Afor _ in range(max_new_tokens):idx_cond = idx[:, -context_size:] #Bwith torch.no_grad():logits = model(idx_cond)logits = logits[:, -1, :] #Cprobas = torch.softmax(logits, dim=-1) #Didx_next = torch.argmax(probas, dim=-1, keepdim=True) #Eidx = torch.cat((idx, idx_next), dim=1) #Freturn idx

该代码迭代生成指定数量的新token,裁剪当前上下文以适应模型的最大上下文大小,计算预测,然后根据最高概率预测选择下一个token。

我们使用softmax函数将logits转换为概率分布,并通过torch.argmax确定具有最大值的位置。实际上,softmax是多余的,因为logit的位置softmax的值也最大。这么做是为了说明将logits转换为概率的整个过程,这可以增加额外的直觉,例如模型生成最有可能的下一个token,这被称为greedy decoding

调用GPT模型来预测下一个文本的代码为:

# 编码
start_context = "Hello, I am"
encoded = tokenizer.encode(start_context)
print("encoded:", encoded)
encoded_tensor = torch.tensor(encoded).unsqueeze(0) #A
print("encoded_tensor.shape:", encoded_tensor.shape)# GPT模型
model.eval() #A
out = generate_text_simple(
model=model,
idx=encoded_tensor,
max_new_tokens=6,
context_size=GPT_CONFIG_124M["context_length"]
)
print("Output:", out)
print("Output length:", len(out[0]))
# 解码
decoded_text = tokenizer.decode(out.squeeze(0).tolist())
print(decoded_text)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/26756.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024-2025最新软考系统架构设计师的复习资料教材,解决如何快速高效通过该考试,试题的重点和难点在哪里?案例分析题和论文题的要点和踩坑点分析

目录 引言考试概述 考试结构考试内容 复习策略 制定复习计划学习资源 知识点详解 系统架构基础设计原则与模式系统分析与设计软件开发过程项目管理系统集成性能与优化安全性设计新兴技术 试题解析 选择题案例分析题论文题 重点与难点分析模拟试题与答案参考资料总结 引言 系…

QT基础-简介,安装(6.7.1编译)

目录 QT简介 一.QT编译 国内镜像网站 1. For windows a.下载:qt-everywhere-src-6.7.1.zip b.下载Cmake c.下载python d.查看readme.md e. x64 native Tools cd 到 源码目录 f.输入 g. 然后输入 ​编辑 h.最后输入 1.2. qt-creator 1.3. 配置编译 2. For Ubu…

驱动开发(三):驱动操作寄存器

驱动开发系列文章: 驱动开发(一):驱动代码的基本框架 驱动开发(二):创建字符设备驱动 驱动开发(三):驱动操作寄存器 ←本文 目录 驱动是如何操作…

使用Python保护或加密Excel文件的7种方法

目录 安装Python Excel库 Python 使用文档打开密码保护 Excel 文件 Python 使用文档修改密码保护 Excel 文件 Python 将 Excel 文件标记为最终版本 Python 保护 Excel 工作表 Python 在保护 Excel 工作表的同时允许编辑某些单元格 Python 锁定 Excel 工作表中的特定单元…

怕怕怕怕怕怕怕怕怕怕

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和…

如何获知lib cell的用途

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 除了databook可以查询cell的用途外,还可以通过在pr工具中获取lib cell属性的方法知晓其用途。 ICC2: report_attribute -app -class lib_cell SDFFXXX 通过看is_…

网络安全等级保护基本要求解读- 安全计算环境-应用系统和数据安全

概述 越来越多的企业用户已将核心业务系统转移到网络上,Web浏览器成为业 务系统的窗口,应用系统面临更多的安全威胁;并且由于各种原因使得其 存在较多的安全漏洞。 在此背景下,如何保障企业的应用安全,尤其是Web应用…

RabbitMQ实践——在管理后台测试消息收发功能

在《RabbitMQ实践——在Ubuntu上安装并启用管理后台》中,我们搭建完RabbitMQ服务以及管理后台。本文我们将管理后台,进行一次简单的消息收发实验。 赋予admin账户权限 登录到管理后台,进入到用户admin的管理页面 点击“set permission”&a…

ListView的使用

📖ListView的使用 ✅1. 创建ListView✅2. 创建适配器Adapter✅3. 开始渲染数据 主要3步骤: 创建ListView 创建适配器Adapter,和Adapter对应的视图 开始渲染数据 效果图: ✅1. 创建ListView 例如现有DemoActivity页面&#xf…

Eureka到Nacos迁移实战:解决配置冲突与启动异常

问题:Eureka到Nacos迁移实战:解决配置冲突与启动异常 在进行微服务架构升级,特别是注册中心从Eureka转向Nacos的过程中,我遇到了一个典型的技术挑战。目标是为了减少因配置变更导致的服务重启频率,我决定拥抱Nacos以其…

云平台DNS故障导致网站访问卡顿异常排查过程,wireshark、strace等工具在实际问题排查过程中的应用方法

一、问题现象 项目上使用华为私有云,前段时间华为升级云平台后,云上用户反馈业务系统出现卡顿,之前几秒可以刷新出来的页面现在需要几十秒。提供了一个比较明显的url和curl调用方法。 10.213.x.xxx:8082/files/login curl -H "Content-…

项目实战--文档搜索引擎

在我们的学习过程中,会阅读很多的文档,例如jdk的API文档,但是在这样的大型文档中,如果没有搜索功能,我们是很难找到我们想查阅的内容的,于是我们可以实现一个搜索引擎来帮助我们阅读文档。 1. 实现思路 1…

Java——IO流(一)-(4/8):前置知识-字符集、UTF-8、GBK、ASCII、乱码问题、编码和解码等

目录 常见字符集介绍 标准ASCII字符集 GBK(汉字内码扩展规范,国标) Unicode字符集(统一码,万国码) 小结 字符集的编码、解码操作 方法 实例演示 常见字符集介绍 标准ASCII字符集 ASCll(American St…

Sklearn中逻辑回归建模

分类模型的评估 回归模型的评估方法,主要有均方误差MSE,R方得分等指标,在分类模型中,我们主要应用的是准确率这个评估指标,除此之外,常用的二分类模型的模型评估指标还有召回率(Recall&#xff…

Golang | Leetcode Golang题解之第150题逆波兰表达式求值

题目: 题解: func evalRPN(tokens []string) int {stack : make([]int, (len(tokens)1)/2)index : -1for _, token : range tokens {val, err : strconv.Atoi(token)if err nil {indexstack[index] val} else {index--switch token {case ""…

WPF学习(3)--不同类通过接口实现同种方法

一、接口概述 1.接口的概念 在C#中,接口(interface)是一种引用类型,它定义了一组方法、属性、事件或索引器,但不提供实现。接口只定义成员的签名,而具体的实现由实现接口的类或结构体提供。接口使用关键字…

【车载音视频电脑】嵌入式AI分析车载DVR,支持8路1080P

产品特点 采用H.265 & H.264编解码,节约存储空间、传输流量; 高分辨率:支持8路1080P*15FPS/4路1080P*30FPS、720P、D1等编解码; 支持1张SATA硬盘,取用方便,满足大容量存储要求; 支持1个…

ChatGPT中文镜像网站分享

ChatGPT 是什么? ChatGPT 是 OpenAI 开发的一款基于生成预训练变换器(GPT)架构的大型语言模型。主要通过机器学习生成文本,能够执行包括问答、文章撰写、翻译等多种文本生成任务。截至 2023 年初,ChatGPT 的月活跃用户…

12.实战私有数据微调ChatGLM3

实战私有数据微调ChatGLM3 实战私有数据微调ChatGLM3实战构造私有的微调数据集基于 ChatGPT 设计生成训练数据的 Prompt使用 LangChain GPT-3.5-Turbo 生成训练数据样例训练数据解析、数据增强和持久化存储自动化批量生成训练数据集流水线提示工程(Prompt Engineer…

OpenCV读取图片

import cv2 as cv # 读取图像 image cv.imread(F:\\mytupian\\xihuduanqiao.jpg) # 创建窗口 cv.namedWindow(image, cv.WINDOW_NORMAL) #显示图像后,允许用户随意调整窗口大小 # 显示图像 cv.imshow(image, image) cv.waitKey(0)import cv2 as cv srccv.imread(…