深入理解大模型之手搓大模型训练(附源码,深入理解)
coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))# 余弦函数计算衰减系数,范围为0到1。batch_iter = iter_batches(split=split)# 获取对应数据集的批次迭代器。说白了,大模型就像一个“超级学霸”,它“博览群书”(海量数据训练),“过目不忘”(超强记忆力),“举一反三”(生成能力)。tokens_per_i
这一次,弄懂了大模型,我终于不再焦虑!
还在为大模型焦虑?还在为“35岁危机”夜不能寐?🤯 别怕!今天,我们就来手撕大模型,用“笨办法”揭开它的神秘面纱!💪
先说点扎心的。你有没有发现,现在不管干啥,都绕不开“大模型”这三个字?
* 求职面试? 面试官张口闭口都是“熟悉大模型者优先”...
* 职场晋升? 好像会用大模型,就能平步青云...
* 行业报告? 满屏都是“大模型赋能”、“AI 革命”...
感觉自己像个被时代抛弃的“原始人”,焦虑感爆棚!😭
但,焦虑有用吗?没用!与其被动接受,不如主动出击!👊
大模型,真有那么神吗?
说白了,大模型就像一个“超级学霸”,它“博览群书”(海量数据训练),“过目不忘”(超强记忆力),“举一反三”(生成能力)。
为啥要“学”大模型?
* 看清本质: 不再被“高大上”的概念吓倒,理解它的运作机制。
* 掌握主动: 拥有“驾驭”大模型的能力,而不是被它“支配”。
* 创造价值: 利用大模型解决实际问题,提升工作效率,甚至... 改变世界!
重点来了!理解大模型,最有效的办法竟然是...
“自己动手,丰衣足食!” 咱们不玩虚的,直接上代码,自己训练一个“迷你版”大模型!
别担心,这代码没你想象的那么“高冷”,我会用“人话”给你讲明白,保证你能看懂,还能跟着操作!😉
代码揭秘:从“小模型”到“大智慧”
下面这段代码,就是一个“迷你版”大模型的训练过程。它就像一个“婴儿”👶,从零开始学习,一步步成长。
import math
import os
import time
from contextlib import nullcontext
from datetime import datetime
from functools import partial
import torch
from model import Transformer, ModelArgs
from preprocess import Task
# -----------------------------------------------------------------------------
# I/O 配置,用于定义输出目录和训练时的日志记录与评估设置
out_dir = "output" # 模型输出保存路径
eval_interval = 2000 # 评估间隔步数
log_interval = 1 # 日志记录间隔步数
eval_iters = 100 # 每次评估时迭代的步数
eval_only = False # 如果为True,脚本在第一次评估后立即退出
always_save_checkpoint = False # 如果为True,在每次评估后总是保存检查点
init_from = "scratch" # 可以选择从头开始训练('scratch')或从已有的检查点恢复('resume')
# 数据配置
batch_size = 8 # 每个微批次的样本数量,如果使用梯度累积,实际批次大小将更大
max_seq_len = 256 # 最大序列长度
vocab_size = 4096 # 自定义词汇表大小
# 模型配置
dim = 288 # 模型的隐藏层维度
n_layers = 8 # Transformer的层数
n_heads = 8 # 注意力头的数量
n_kv_heads = 4 # 模型分组
multiple_of = 32 # 在某些层的维度必须是该数的倍数
dropout = 0.0 # Dropout概率
# AdamW优化器配置
gradient_accumulation_steps = 4 # 梯度累积步数,用于模拟更大的批次
learning_rate = 5e-4 # 最大学习率
max_iters = 100000 # 总的训练迭代次数
weight_decay = 1e-1 # 权重衰减系数
beta1 = 0.9 # AdamW优化器的β1参数
beta2 = 0.95 # AdamW优化器的β2参数
grad_clip = 1.0 # 梯度裁剪阈值,0表示不裁剪
# 学习率衰减配置
decay_lr = True # 是否启用学习率衰减
warmup_iters = 1000 # 学习率预热的步数
# 系统设置
device = "cuda:0" # 设备选择:'cpu','cuda','cuda:0'等
dtype = "bfloat16" # 数据类型:'float32','bfloat16','float16'
# -----------------------------------------------------------------------------
# 获取配置参数的键值对,便于后续的日志记录
config_keys = [
k
for k, v in globals().items()
if not k.startswith("_") and isinstance(v, (int, float, bool, str))
]
config = {k: globals()[k] for k in config_keys} # 保存配置到字典中,便于日志记录
# -----------------------------------------------------------------------------
# 固定一些超参数的默认值
lr_decay_iters = max_iters # 学习率衰减步数,设置为等于最大迭代步数
min_lr = 0.0 # 最小学习率,建议为学习率的十分之一
vocab_source = 'custom' # 词汇表来源
master_process = True # 用于区分主进程
seed_offset = 0 # 随机种子偏移量
ddp_world_size = 1 # 分布式数据并行的世界大小
tokens_per_iter = batch_size * max_seq_len # 每次迭代处理的token数
# 设置随机种子,确保可重复性
torch.manual_seed(1337 + seed_offset)
torch.backends.cuda.matmul.allow_tf32 = True # 允许在matmul上使用tf32
torch.backends.cudnn.allow_tf32 = True # 允许在cudnn上使用tf32
device_type = "cuda" if "cuda" in device else "cpu" # 用于自动选择设备类型
ptdtype = torch.float16 # 设置训练时使用的数据类型
# 混合精度训练相关
ctx = (
nullcontext()
if device_type == "cpu"
else torch.amp.autocast(device_type=device_type, dtype=ptdtype)
)
# 为特定任务设置批次迭代器 iter_batches
iter_batches = partial(
Task.iter_batches, # 调用 Task 类中的 iter_batches 方法
batch_size=batch_size, # 每个批次的样本数量
max_seq_len=max_seq_len, # 每个序列的最大长度
vocab_size=vocab_size, # 词汇表大小
vocab_source=vocab_source, # 词汇表来源(如 llama2 或 custom)
device=device, # 运行模型的设备(如 GPU 或 CPU)
num_workers=0, # 用于数据加载的 worker 数量,0 表示在主线程中加载
)
# 训练迭代数初始化
iter_num = 0 # 记录当前迭代数
# 验证集上的最好损失初始值设置为一个极大值,用于后续模型验证时对比更新
best_val_loss = 1e9 # 设置初始的最佳验证损失为非常大的值,以便在训练中更新
# 模型初始化参数设置
model_args = dict(
dim=dim, # 模型的隐藏层维度
n_layers=n_layers, # Transformer 的层数
n_heads=n_heads, # 多头注意力机制中的头数
n_kv_heads=n_kv_heads, # 分组数(可能是用于并行化或其他优化目的)
vocab_size=vocab_size, # 词汇表大小
multiple_of=multiple_of, # 用于调整某些维度的参数,确保其为特定数的倍数
max_seq_len=max_seq_len, # 最大序列长度
dropout=dropout, # dropout 概率,用于防止过拟合
)
# ===========================================================
# 模型初始化
gptconf = ModelArgs(**model_args)
model = Transformer(gptconf)
model.to(device)
# 初始化 GradScaler,用于自动混合精度训练(AMP)
# 如果 enabled=False,表示禁用混合精度,scaler 将不起作用
scaler = torch.cuda.amp.GradScaler(enabled=(dtype == "float16"))
# 优化器初始化,调用模型的 configure_optimizers 方法
optimizer = model.configure_optimizers(
weight_decay, # 权重衰减(L2 正则化)
learning_rate, # 学习率
(beta1, beta2), # Adam 优化器中的 beta1 和 beta2 参数
device_type # 当前训练设备(如 GPU 或 CPU)
)
# 定义评估损失的流程
@torch.no_grad() # 使用 no_grad 装饰器,确保在评估过程中不计算梯度,从而节省内存
def estimate_loss():
out = {} # 用于存储训练集和验证集上的平均损失
model.eval() # 将模型设置为评估模式,这会影响 dropout 和 batchnorm 等层的行为
for split in ["train", "val"]: # 分别对训练集和验证集进行评估
batch_iter = iter_batches(split=split) # 获取对应数据集的批次迭代器
losses = torch.zeros(eval_iters) # 初始化一个张量用于存储多次迭代的损失,放在 CPU 上
for k in range(eval_iters): # 进行多次迭代以计算平均损失
X, Y = next(batch_iter) # 从迭代器中获取下一个批次的输入数据 X 和标签 Y
with ctx: # 上下文管理器,可以是 torch.autocast(),用于自动混合精度训练
logits = model(X, Y) # 前向传播,计算模型的输出
loss = raw_model.last_loss # 从模型中获取损失值
losses[k] = loss.item() # 将损失值转换为 Python 标量并存储在 losses 张量中
out[split] = losses.mean() # 计算当前数据集上的平均损失并保存到字典中
model.train() # 恢复模型为训练模式
return out # 返回包含训练集和验证集平均损失的字典
# 定义学习率调度函数
def get_lr(it):
"""
根据当前的训练迭代步数 it 返回当前的学习率值。
学习率调整策略包括线性预热、余弦退火和最小学习率限制。
"""
# 1) 线性预热阶段,在 warmup_iters 之前,学习率线性增加到目标学习率
if it < warmup_iters:
return learning_rate * it / warmup_iters # 预热阶段,学习率线性增长
# 2) 如果迭代步数超过 lr_decay_iters,返回最小学习率 min_lr
if it > lr_decay_iters:
return min_lr # 训练进入尾声时,学习率达到最小值并保持不变
# 3) 余弦退火阶段,在 warmup_iters 和 lr_decay_iters 之间,学习率逐渐降低
decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
assert 0 <= decay_ratio <= 1 # 确保衰减比在合法范围内
coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio)) # 余弦函数计算衰减系数,范围为0到1
return min_lr + coeff * (learning_rate - min_lr) # 根据衰减系数调整学习率
# 初始化训练数据的迭代器
train_batch_iter = iter_batches(split="train")
X, Y = next(train_batch_iter) # 获取第一个批次的数据
t0 = time.time() # 记录开始时间
local_iter_num = 0 # 本进程中的迭代次数
raw_model = model # 如果使用了分布式数据并行 (DDP),需要解包模型
running_mfu = -1.0 # 初始化模型浮点运算利用率
os.makedirs(out_dir, exist_ok=True)
while True:
# 或许当前step的学习率
lr = get_lr(iter_num) if decay_lr else learning_rate
# 更新优化器中的学习率
for param_group in optimizer.param_groups:
param_group["lr"] = lr
# 在指定的评估间隔进行模型评估和保存检查点
if iter_num % eval_interval == 0 and master_process:
losses = estimate_loss() # 评估当前模型在训练集和验证集上的损失
print(f"step {iter_num}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
# 如果验证损失降低,或者设置为始终保存检查点,则保存模型
if losses["val"] < best_val_loss or always_save_checkpoint:
best_val_loss = losses["val"]
if iter_num > 0:
# 创建检查点字典,包含模型状态、优化器状态和其他信息
checkpoint = {
"model": raw_model.state_dict(),
"optimizer": optimizer.state_dict(),
"model_args": model_args,
"iter_num": iter_num,
"best_val_loss": best_val_loss,
"config": config,
}
print(f"saving checkpoint to {out_dir}")
# 保存检查点到指定目录
torch.save(checkpoint, os.path.join(out_dir, "ckpt.pt"))
# 如果只进行评估且已经完成第一次迭代,则退出循环
if iter_num == 0 and eval_only:
break
# 前向和反向传播过程,支持梯度累积
for micro_step in range(gradient_accumulation_steps):
with ctx: # 混合精度训练的上下文管理器
logits = model(X, Y) # 前向传播,计算模型输出
loss = raw_model.last_loss # 获取模型的损失值
loss = loss / gradient_accumulation_steps # 平均损失以支持梯度累积
X, Y = next(train_batch_iter) # 获取下一个批次的数据
# 反向传播,计算梯度
scaler.scale(loss).backward()
# 梯度处理阶段
if grad_clip != 0.0:
# 取消梯度缩放以进行梯度裁剪
scaler.unscale_(optimizer)
# 对梯度进行裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip)
# 更新优化器和梯度缩放器(用于混合精度训练)
scaler.step(optimizer)
scaler.update()
# 清空优化器的梯度,释放显存
optimizer.zero_grad(set_to_none=True)
# 计时和日志记录
t1 = time.time()
dt = t1 - t0 # 计算一次迭代所需时间
t0 = t1
if iter_num % log_interval == 0 and master_process:
# 获取当前损失值,并根据梯度累积步骤进行调整
lossf = loss.item() * gradient_accumulation_steps
if local_iter_num >= 5: # 让训练循环先运行几个迭代再计算模型利用率
mfu = raw_model.estimate_mfu(batch_size * gradient_accumulation_steps, dt)
# 使用滑动平均更新模型浮点运算利用率(MFU)
running_mfu = mfu if running_mfu == -1.0 else 0.9 * running_mfu + 0.1 * mfu
print(
f"{iter_num} | loss {lossf:.4f} | lr {lr:e} | {dt*1000:.2f}ms | mfu {running_mfu*100:.2f}%"
# mfu 表示模型浮点运算利用率
)
iter_num += 1 # 全局迭代次数自增
local_iter_num += 1 # 本地迭代次数自增
# 终止条件,达到最大迭代次数则退出循环
if iter_num > max_iters:
break
1. 准备“教材”和“笔记本”
* `out_dir = "output"`: 模型输出保存路径,就像你的“笔记本”,记录学习成果。
* `eval_interval`、`log_interval`: 学习过程中的“小测验”和“日志”,帮你掌握学习进度。
* `batch_size`: 每次学习的“页数”。
* `max_seq_len`: 每页的“字数”。
* `vocab_size`: “词汇量”大小,决定了模型能理解多少词。
2. 搭建“大脑”
* `dim`: “大脑”的“容量”,越大越聪明。
* `n_layers`: “大脑”的“层数”,越深越复杂。
* `n_heads`: “注意力”的“焦点”数量,越多越专注。
* `dropout`: 随机“遗忘”一些知识,防止“死记硬背”(过拟合)。
3. “喂”数据,“教”知识
* `iter_batches`: 把“教材”分成一页一页的,方便“大脑”学习。
* `X, Y = next(train_batch_iter)`: 拿出“一页书”和对应的“答案”。
4. “思考”和“做题”
* `logits = model(X, Y)`: “大脑”开始“思考”,尝试根据“问题”给出“答案”。
* `loss = raw_model.last_loss`: 计算“答案”和“标准答案”之间的差距。
5. “反思”和“改进”
* `loss.backward()`: “大脑”根据“错误”进行“反思”,调整“思考方式”。
* `optimizer.step()`: 更新“大脑”中的“知识”。
6. “温故知新”
* `get_lr(it)`: 调整“学习速度”,一开始快一点,后面慢一点。
* `estimate_loss()`: 定期“复习”,看看“学”得怎么样。
7. “持之以恒”
* `while True:`: 不断重复“学习”、“思考”、“反思”的过程,直到“学有所成”。
大白话总结:
这段代码,就是模拟了一个“学生”🎓(模型)通过不断“学习”📚(训练),逐渐掌握“知识”📝(数据规律)的过程。
和“大模型”有啥关系?
大模型,无非就是“学生”更聪明,“教材”更丰富,“学习”时间更长而已。但本质上,都是一样的!
看懂了代码,你还焦虑吗?😎
是不是感觉大模型也没那么神秘了?
当你亲手“养大”一个“迷你模型”,你会发现:
* 原来,大模型也是一步步“学”出来的!
* 原来,我也可以“教”出一个“AI”!
* 原来,掌握大模型,并没有那么难!行动起来,你也能成为“大模型”专家!
* 动手实践: 复制代码,亲自跑一遍,感受“AI”的“成长”。
* 深入研究: 了解更多模型细节,探索更高级的训练技巧。
* 持续学习: 关注最新技术动态,与时俱进。
记住,焦虑的根源,往往是未知和恐惧。
当你真正了解了大模型,你会发现,它不过是一个强大的“工具”。
而你,才是掌握“工具”的“主人”!👑
看完是不是觉得,AI的世界,远比你想象的更神奇、更有趣?😉 这篇文章,不仅有料,有趣,还超级有梗!赶紧分享给你的小伙伴,一起涨姿势吧!🚀(别忘了点赞、评论、转发三连哦!😘)
更多推荐
所有评论(0)