现在人工智能飞速发展时代,LLM绝对可以算是人工智能领域得一颗明珠,也是现在许多AI项目落地得必不可少得一个模块,可以说,不管你之前得研究领域是AI得哪个方向,现在都需要会一些LLM基础,在这个系列,文章会从最基础的数据处理开始,一步步构建属于我们自己的GPT2模型,如果之前没有了解过LLM,强烈推荐Build a Large Language Model (From Scratch)这本书,本文也是对这本书学习总结,代码图片都出自随书教学视频。

  • github地址:https://github.com/rasbt/LLMs-from-scratch

项目概览

本项目将分为三个核心阶段,每个阶段都有其独特的挑战和技术要点:

🔧 Stage 1: 数据处理与模型架构构建

在这个阶段,我们将深入探讨:

  • 数据预处理与采样管道:如何高效处理海量文本数据
  • 注意力机制的实现:Transformer架构的核心组件
  • LLM架构设计:从零搭建完整的语言模型框架

🚀Stage 2: 大模型预训练

预训练是整个项目的核心,包含:

  • 训练循环设计:如何稳定训练大规模模型
  • 模型评估策略:设计损失函数,实时监控训练效果
  • 权重管理:预训练模型的保存与openAI预训练GPT2权重加载

🎯 Stage 3: 模型微调与应用

最终阶段将专注于实际应用:

  • 分类任务微调:针对特定任务优化模型性能
  • 指令数据集有监督微调(SFT):让模型更好地理解人类指令

下载文本数据集

以一个简单的txt文本为例,接下来将进行word embedding,同时之后也会利用这个文本训练自己的LLM,

import os
import urllib.request
if not os.path.exists("the-verdict.txt"):url = ("https://raw.githubusercontent.com/rasbt/""LLMs-from-scratch/main/ch02/01_main-chapter-code/""the-verdict.txt")file_path = "the-verdict.txt"urllib.request.urlretrieve(url, file_path)

分词器

拿到一个text文本,先对其进行划分。这一过程就叫Tokenized,先得到Tokenized text,对划分的每一个块(Token),进行数字编码得到Token IDs,对于Tokenized其实简单来说就是来做文本划分,弄清楚这个就可以实现一个最简单的分词器

import re
text = "Hello, world. This, is a test."result = re.split(r'(\s)', text)
print(result)

得到结果:[‘Hello,’, ’ ', ‘world.’, ’ ', ‘This,’, ’ ', ‘is’, ’ ', ‘a’, ’ ', ‘test.’]
目前主流LLM的分词器无非是在这个的基础上做一些改进优化,比如说增加根据逗号句号等标点符号分词

text = "Hello, world. Is this-- a test?"
result = re.split(r'([,.:;?_!"()\']|--|\s)', text)
result = [item.strip() for item in result if item.strip()]
print(result)

结果:[‘Hello’, ‘,’, ‘world’, ‘.’, ‘Is’, ‘this’, ‘–’, ‘a’, ‘test’, ‘?’]
将文本分块之后就是将由token得到token IDs,把所有token放入集合中,利用集合的唯一性去重,就可以得到每一个不同的token的ids

all_words = sorted(set(preprocessed))
vocab_size = len(all_words)
print(vocab_size)
vocab = {token:integer for integer,token in enumerate(all_words)}
for i, item in enumerate(vocab.items()):print(item)if i >= 50:break

相应的,根据唯一的ids也可以将token ids解码成对应的token,总之token ids与token是一一对应的关系,由此就可以得到一个最简单的分词器SimpleTokenizerV1

class SimpleTokenizerV1:
def __init__(self, vocab):self.str_to_int = vocabself.int_to_str = {i:s for s,i in vocab.items()}
def encode(self, text):preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)preprocessed = [item.strip() for item in preprocessed if item.strip()]ids = [self.str_to_int[s] for s in preprocessed]return ids
def decode(self, ids):text = " ".join([self.int_to_str[i] for i in ids])# Replace spaces before the specified punctuationstext = re.sub(r'\s+([,.?!"()\'])', r'\1', text)return text

可以使用分词器将文本编码(即分词)为整数ids,这些ids随后可以被向量嵌入作为大型语言模型(LLM)的输入

tokenizer = SimpleTokenizerV1(vocab)
text = """"It's the last he painted, you know,"           Mrs. Gisburn said with pardonable pride."""
ids = tokenizer.encode(text)
print(ids)
tokenizer.decode(tokenizer.encode(text))

这一个简便的分词器有一个最明显的缺点,LLM在得到输入时不可能每个token都在训练集里面出现过,遇到没有出现在训练集里的token ids就会报错

tokenizer = SimpleTokenizerV1(vocab)
text = "Hello, do you like tea. Is this-- a test?"
tokenizer.encode(text)

如这个hallo就没在txt文本中出现过,运行代码就会报错,因此需要改进,我们可以增加特殊标记,如“<|unk|>”来表示未知单词。“<|endoftext|>”表示文本的结束,这对于训练集包含不同本文txt时这个标记是非常有必要的

all_tokens = sorted(list(set(preprocessed)))
all_tokens.extend(["<|endoftext|>", "<|unk|>"])
vocab = {token:integer for integer,token in enumerate(all_tokens)}
len(vocab.items())
for i, item in enumerate(list(vocab.items())[-5:]):print(item)

由此得到改进后的分词器:

class SimpleTokenizerV2:
def __init__(self, vocab):self.str_to_int = vocabself.int_to_str = { i:s for s,i in vocab.items()}
def encode(self, text):preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)preprocessed = [item.strip() for item in preprocessed if item.strip()]preprocessed = [item if item in self.str_to_int else "<|unk|>" for item in preprocessed]ids = [self.str_to_int[s] for s in preprocessed]return ids
def decode(self, ids):text = " ".join([self.int_to_str[i] for i in ids])# Replace spaces before the specified punctuationstext = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)return text
tokenizer = SimpleTokenizerV2(vocab)
text1 = "Hello, do you like tea?"
text2 = "In the sunlit terraces of the palace."
text = " <|endoftext|> ".join((text1, text2))print(text)

当然这个版本离实际GPT2使用的分词器还有些差距,比如说面对未知词时,并不是直接都将其归为“<|unk|>,
例如,如果 GPT-2 的词汇表中没有“unfamiliarword”这个词,它可能会将其分词为 [“unfam”, “iliar”, “word”] 或其他子词分解,具体取决于其训练的 BPE 合并规则。这里推荐一个在线网页可以体验不同LLM的分词策略

https://tiktokenizer.vercel.app/?model=Qwen%2FQwen2.5-72B

在这里插入图片描述
接下来我们使用来自 OpenAI 的开源 tiktoken 库的 BPE 分词器,该库使用 Rust 实现了其核心算法以提高计算性能。

import importlib
import tiktoken
print("tiktoken version:", importlib.metadata.version("tiktoken"))
tokenizer = tiktoken.get_encoding("gpt2")
text = ("Hello, do you like tea? <|endoftext|> In the sunlit terraces""of someunknownPlace.")integers = tokenizer.encode(text, allowed_special={"<|endoftext|>"})print(integers)
strings = tokenizer.decode(integers)
print(strings)

数据采样

有了分词器我们就可以对txt文本数据进行采样了
LLM的input和target是什么呢,其实从LLM的工作流程就可以看出一些,LLM都是根据现有文本预测下一个token,
在这里插入图片描述
所以LLM是通过滑动窗口对数据进行采样,一段在训练文本中连续的token作为输入,那么他的target就是滑动一定窗口后得到的相同长度的token序列,所以LLM依旧属于有监督学习的范畴,值得注意的是这里为了便于理解只滑动了一个token,但是实际LLM训练是有step可以指定的,很少有设为1的,由此就可以实现一个简单的文本数据加载函数
在这里插入图片描述

from torch.utils.data import Dataset, DataLoader
class GPTDatasetV1(Dataset):def __init__(self, txt, tokenizer, max_length, stride):self.input_ids = []self.target_ids = []token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})assert len(token_ids) > max_length, "Number of tokenized inputs must at least be                     equal to max_length+1"   for i in range(0, len(token_ids) - max_length, stride):input_chunk = token_ids[i:i + max_length]target_chunk = token_ids[i + 1: i + max_length + 1]self.input_ids.append(torch.tensor(input_chunk))self.target_ids.append(torch.tensor(target_chunk))def __len__(self):return len(self.input_ids)def __getitem__(self, idx):return self.input_ids[idx], self.target_ids[idx]
def create_dataloader_v1(txt, batch_size=4, max_length=256, stride=128, shuffle=True, drop_last=True, num_workers=0):# Initialize the tokenizertokenizer = tiktoken.get_encoding("gpt2")# Create datasetdataset = GPTDatasetV1(txt, tokenizer, max_length, stride)# Create dataloaderdataloader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last, num_workers=num_workers)return dataloader

有了这个dataloader,就可以得到任意batch size的token id序列,

dataloader = create_dataloader_v1(raw_text, batch_size=8, max_length=4, stride=4, shuffle=False)data_iter = iter(dataloader)
inputs, targets = next(data_iter)
print("Inputs:\n", inputs)
print("\nTargets:\n", targets)

向量嵌入

然而,输入大型语言模型(LLM)的并不是直接的 token IDs,而是需要通过嵌入层(token embeddings,也称为词嵌入)将其转换为连续的向量表示。这些向量作为嵌入层的权重,在模型训练过程中会通过梯度下降不断更新,以优化对下一个 token 的预测能力。值得一提的是,虽然每个 token ID 对应的嵌入向量值在训练中会发生变化,但其在嵌入矩阵中的行索引(即与 token ID 的固定对应关系)始终保持不变。这种稳定的映射关系确保了模型在编码和解码过程中的一致性。
在这里插入图片描述
嵌入层方法本质上是一种更高效的实现方式,等价于先进行 one-hot 编码,然后通过全连接层进行矩阵乘法,由于嵌入层只是独热编码加矩阵乘法的一种更高效的实现方式,因此它可以被视为一个神经网络层,并且可以通过反向传播进行优化。

input_ids = torch.tensor([2, 3, 5, 1])
vocab_size = 6
output_dim = 3torch.manual_seed(123)
embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
print(embedding_layer.weight)

位置编码:

只对不同token进行词嵌入对于LLM来说是不够的,因为Transformer本身是不具备处理序列顺利的能力的,而同一个单词位于一句话的不同位置是可以表达出不同的意思的。
在这里插入图片描述
所以要引入token的位置编码,为txt文本的每一个token引入一个位置编码,这里我们用的是绝对位置编码,也是个gpt2所使用的,绝对位置嵌入是指为输入序列中每个位置分配一个固定编号(0, 1, 2, …),并为每个编号对应地创建一个向量。这些向量表示每个 token 在序列中的具体位置,与 token 本身无关。所以位置编码的行数就是整个文本的长度

vocab_size = 50257 
output_dim = 256 token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
max_length = 4 
dataloader = create_dataloader_v1( raw_text, batch_size=8, max_length=max_length, stride=max_length, shuffle=False 
) 
data_iter = iter(dataloader) 
inputs, targets = next(data_iter)
print("Token IDs:\n", inputs) 
print("\nInputs shape:\n", inputs.shape)
token_embeddings = token_embedding_layer(inputs) 
print(token_embeddings.shape)
context_length = max_length 
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)
pos_embeddings = pos_embedding_layer(torch.arange(max_length)) 
print(pos_embeddings.shape)

为了创建在大语言模型(LLM)中使用的输入嵌入,我们只需将词元嵌入和位置嵌入相加。

input_embeddings = token_embeddings + pos_embeddings
print(input_embeddings.shape)

在这里插入图片描述

总结

上图总结了一个完整的文本数据的处理过程:
文本经过分词器划分为token,得到token id后经行向量嵌入,这一步可以模型得以进行loss的反向传播,每一个词向量还需要加上与之对应的位置编码,这是为了提升LLM的顺序序列处理能力,得到的input embedings就可以输入
LLM进行训练了,关于位置编码,目前的改进很多,绝对位置编码的应用少了很多,比如千文3使用的RoPE(Rotary Position Embedding),也是目前大语言模型中常用的一种位置编码方式
和相对位置编码相比,RoPE 具有更好的外推性,目前是大模型相对位置编码中应用最广的方式之一:

其原理用直观的话来说就是将位置编码看作一个二维旋转角度,让QK的点乘运算本身隐含顺序差异

因为旋转可以表示相对位置,所以天然支持相对位置感知

备注:什么是大模型外推性?
外推性是指大模型在训练时和预测时的输入长度不一致,导致模型的泛化能力下降的问题。例如,如果一个模型在训练时一个batch只使用了512个 token
的文本,那么在预测时如果输入超过512个 token,模型可能无法正确处理。这就限制了大模型在处理长文本或多轮对话等任务时的效果。

详细可以参考博客:https://www.zhihu.com/tardis/bd/art/647109286

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

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

相关文章

Redis ubuntu下载Redis的C++客户端

1. 安装 redis-plus-plus C 操作 Redis 的库有很多&#xff0c;这里选择使用 redis-plus-plus&#xff0c;这个库的功能强大&#xff0c;使用简单。 Github 地址&#xff1a;GitHub - sewenew/redis-plus-plus: Redis client written in C 访问不了Github 地址的可以使用Ste…

nm命令和nm -D命令参数

出现这种差异的原因在于&#xff1a;动态库中的符号分为两种类型&#xff1a; 常规符号表&#xff08;regular symbol table&#xff09;&#xff1a;通常用于静态链接和调试&#xff0c;默认不包含在动态库中&#xff08;除非显式保留&#xff09;。动态符号表&#xff08;dyn…

Windows下cuda的安装和配置

今天开始做一个cuda教程。由于本人主要在windows下使用visual studio进行开发&#xff0c;因此这里讲一下windows下的cuda开发环境。 下载cuda_toolkit 从网站https://developer.nvidia.com/cuda-toolkit中下载&#xff0c;先选择Download Now,然后跳转到如下页面&#xff1a…

【代码随想录day 19】 力扣 450.删除二叉搜索树中的节点

视频讲解&#xff1a;https://www.bilibili.com/video/BV1tP41177us/?share_sourcecopy_web&vd_sourcea935eaede74a204ec74fd041b917810c 文档讲解&#xff1a;https://programmercarl.com/0450.%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%A…

智慧养老丨实用科普+避坑指南:科技如何让晚年生活更安全舒适?

随着老龄化社会的到来&#xff0c;智慧养老产品逐渐成为改善老年人生活质量的重要工具。从智能手表到便携洗浴机&#xff0c;科技正为老年人的健康、安全与生活便利提供创新解决方案。我们这次主要介绍四类典型智慧养老产品&#xff0c;结合真实体验给出选购建议&#xff0c;并…

系统垃圾清理批处理脚本 (BAT)

系统垃圾清理批处理脚本 (BAT) 以下是一个Windows系统垃圾清理的批处理脚本&#xff0c;它可以清理常见的系统临时文件、缓存和日志等&#xff1a; echo off title 系统垃圾清理工具 color 0a echo. echo 正在清理系统垃圾文件&#xff0c;请稍候... echo.:: 清理临时文件 echo…

Terraform的零基础学习教程

一、Terraform 是什么&#xff1f; Terraform 是由 HashiCorp 开发的开源工具&#xff0c;用于自动化管理云基础设施&#xff08;如 AWS、Azure、GCP 等&#xff09;。 核心特点&#xff1a; 基础设施即代码&#xff08;IaC&#xff09;&#xff1a;用代码定义和管理资源。跨…

429. N 叉树的层序遍历(中等)题解

题目描述给定一个 N 叉树&#xff0c;返回其节点值的层序遍历。&#xff08;即从左到右&#xff0c;逐层遍历&#xff09;。树的序列化输入是用层序遍历&#xff0c;每组子节点都由 null 值分隔&#xff08;参见示例&#xff09;。示例 1&#xff1a;输入&#xff1a;root [1,…

Java 课程,每天解读一个简单Java之题目:输入一行字符,分别统计出其中英文字母、空格、数字和其它字符的个数。

package ytr250813;import java.io.IOException;public class CharacterCounter {public static void main(String[] args) throws IOException {// 初始化计数器变量int letterCount 0; // 英文字母计数器int spaceCount 0; // 空格计数器int digitCount 0; // 数字计数器i…

GitLab CI + Docker 自动构建前端项目并部署 — 完整流程文档

一、环境准备1. 服务器准备一台Linux服务器&#xff08;CentOS/Ubuntu皆可&#xff09;&#xff0c;推荐至少4核8GB内存已安装 Docker&#xff08;及 Docker 服务已启动&#xff09;已安装 GitLab Runner2. 服务器上安装 Docker &#xff08;如果没装&#xff09;# CentOS9以下…

LCP 17. 速算机器人

目录 题目链接&#xff1a; 题目&#xff1a; 解题思路&#xff1a; 代码&#xff1a; 总结&#xff1a; 题目链接&#xff1a; LCP 17. 速算机器人 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; # LCP 17. 速算机器人 小扣在秋日市集发现了一款速算机器人。…

Spring cloud集成ElastictJob分布式定时任务完整攻略(含snakeyaml报错处理方法)

ElasticJob 是一款轻量级、可扩展的分布式定时任务解决方案&#xff0c;基于 Quartz 二次开发&#xff0c;支持任务分片、失效转移、任务追踪等功能&#xff0c;非常适合在 Spring Cloud 微服务场景中使用。我将带你完成 Spring Cloud 集成 ElasticJob 的全过程&#xff0c;并分…

了解 Linux 中的 /usr 目录以及 bin、sbin 和 lib 的演变

Linux 文件系统层次结构是一个复杂且引人入胜的体系&#xff0c;其根源深植于类 Unix 操作系统的历史之中。在这一结构的核心&#xff0c;/usr 目录是一个至关重要的组成部分&#xff0c;随着时间的推移&#xff0c;它经历了显著的演变。与此同时&#xff0c;/bin、/sbin、/lib…

高级IO(五种IO模型介绍)

文章目录一、IO为什么慢&#xff1f;一、阻塞IO二、非阻塞IO三、信号驱动IO四、IO多路复用五、异步IO一、IO为什么慢&#xff1f; IO操作往往都是和外设交互&#xff0c;比如键盘、鼠标、打印机、磁盘。而最常见的就是内存与磁盘的交互&#xff0c;要知道磁盘是机械设备&#…

第十二节:粒子系统:海量点渲染

第十二节&#xff1a;粒子系统&#xff1a;海量点渲染 引言 粒子系统是创造动态视觉效果的神器&#xff0c;从漫天繁星到熊熊火焰&#xff0c;从魔法特效到数据可视化&#xff0c;都离不开粒子技术。Three.js提供了强大的粒子渲染能力&#xff0c;可轻松处理百万级粒子。本文将…

LeetCode Day5 -- 二叉树

目录 1. 啥时候用二叉树&#xff1f; &#xff08;1&#xff09;典型问题 &#xff08;2&#xff09;核心思路 2. BFS、DFS、BST 2.1 广度优先搜索BFS &#xff08;1&#xff09;适用任务 &#xff08;2&#xff09;解决思路​​&#xff1a;使用队列逐层遍历 2.2 深度…

<c1:C1DateTimePicker的日期时间控件,控制日期可以修改,时间不能修改,另外控制开始时间的最大值比结束时间小一天

两个时间控件 <c1:C1DateTimePicker Width"170" EditMode"DateTime" CustomDateFormat"yyyy-MM-dd" CustomTimeFormat"HH:mm:ss" Style"{StaticResource ListSearch-DateTimePicker}" x:Name"dateTimePicker"…

文件完整性监控工具:架构和实现

文件完整性监控(FIM)作为一道关键的防御层&#xff0c;确保系统和网络中文件及文件夹的完整性与安全性。文件完整性监控工具通过监控关键文件的变更并检测未经授权的修改&#xff0c;提供关于潜在安全漏洞、恶意软件感染和内部威胁的早期警报。为了使文件完整性监控工具发挥实效…

Linux信号量和信号

1.温故知新上一篇博客&#xff0c;我们又知道了一种进程间通信的方案&#xff1a;共享内存。它是在物理内存中用系统调用给我们在物理内存开辟一个共享内存&#xff0c;可以由多个进程的页表进行映射&#xff0c;共享内存不和管道一样&#xff0c;它的生命周期是随内核的&#…

腾讯测试岗位面试真题分析

以下是对腾讯测试工程师面试问题的分类整理、领域占比分析及高频问题精选&#xff08;基于​​92道问题&#xff0c;总出现次数118次​​&#xff09;。问题按​​7大技术领域​​划分&#xff0c;高频问题标注优先级&#xff08;1-5&#x1f31f;&#xff09;&#xff1a; 不…