ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI工程从零开始:手写迷你Transformer与LLM训练推理全指南

AI工程从零开始:手写迷你Transformer与LLM训练推理全指南 “AI engineering from scratch”这个标题我在各种技术社区、GitHub仓库里见了不少次。说真的每次看到都想点进去看看作者到底是怎么“从零”开始的——是真的从矩阵乘法手写反向传播还是只是“从零”指没学过深度学习但直接调库这两种“从零”的含金量天差地别。今天这篇内容我就以自己这些年带团队、带新人、自己也从数学系半路出家搞AI的亲身经历把“AI工程从零开始”这件事拆开揉碎讲清楚。适合谁看想转行做AI的刚入职做算法但是感觉每天都在调参的还有那些看了不少论文但是一动手就卡壳的学生。我会尽量把学习路径、核心原理、实操步骤和踩坑经验都讲透不整虚的。1. 为什么要“从零开始”先搞清楚学习的底层逻辑1.1 从零开始不是重复造轮子是拆掉黑盒很多人不理解为啥要“从零开始”学AI。现在PyTorch、HuggingFace这些框架都这么成熟了load一个预训练模型就三行代码训练一个模型也就是写写配置文件的事为什么要自己从头写我打个比方你就懂了。学做饭你天天用外卖App点餐永远不知道一道菜是怎么做出来的。等到哪天外卖平台挂了、或者你想做的菜外卖没有你就抓瞎了。自己从买食材、洗菜、切菜、下锅开始做一遍哪怕做的不好吃你至少知道一道菜从生到熟经历了什么也知道哪个环节出了问题可以怎么补救。AI工程也是一样。框架帮你把forward、backward都写好了但如果你不知道梯度是怎么流的、损失函数为什么对某些样本不敏感、学习率为什么要有warmup那你遇到问题的时候连排查方向都没有。我见过太多“调参工程师”了——模型效果不好就改学习率、改batch size、换loss跟庙里摇签一样。但如果你从零手写过一次模型训练你就知道学习率只是表象真正的问题可能出在数据分布、特征尺度、甚至是你写的那一行view到底有没有把维度搞对。所以“from scratch”的核心价值不是让你把时间花在重复造轮子上而是帮你把黑盒拆成白盒。你不需要所有东西都从零写但关键环节比如数据流、前向传播、反向传播、优化器更新、推理解码你至少要亲手走一遍。1.2 自顶向下还是自底向上两条学习路线怎么选在规划“从零开始”的学习路径之前你得先选一条主线。市面上的学习路线大致分两种自顶向下Top-Down先学框架怎么用、模型怎么调遇到问题再补底层知识。好处是上手快一两周就能跑通一个模型成就感强。坏处是根基不稳遇到非常规问题容易翻车。自底向上Bottom-Up先学数学基础、手动实现核心算法再过渡到框架。好处是原理通透换个框架、换个任务都不慌。坏处是曲线陡很多人坚持不到动手写模型就放弃了。我最推荐的其实是“混合路线”用一个小目标牵引但实现过程中把关键模块都手写一遍。比如你的目标是训练一个能生成唐诗的模型。那你就不要直接model GPT2LMHeadModel.from_pretrained(...)而是自己用nn.Transformer或者干脆从nn.Linear开始搭一个极简的decoder-only模型数据加载、embedding、attention mask、loss、采样全自己写。这个过程走通了你再去用框架里的现成模块、预训练模型感觉完全不一样。我现在带新人基本也是这个思路。先让他手写一个两层的MLP在MNIST上跑通然后让他手写一个mini transformer在莎士比亚数据集上训一个字符级语言模型。这两关过了后面的工作基本就是“搭积木”而不是“看天书”。2. 搭建AI工程知识栈数学、编程与框架选型2.1 数学基础学到什么程度才算够说到“from scratch”很多人的第一反应是“数学是不是要学到博士水平”。我的答案是不需要但核心概念必须会算、会推。具体来说线性代数你需要掌握矩阵乘法这个真的会有人搞错维度、转置、矩阵求导的基本法则特别是链式法则的矩阵形式。微积分你需要掌握偏导数、梯度、链式法则。概率论你需要掌握条件概率、贝叶斯公式、期望与方差、常见的分布正态、伯努利、多项分布。信息论方面交叉熵、KL散度是要懂的因为绝大多数分类任务的loss都跟这个有关。很多人会问那矩阵求导要不要像数学系那样一个个推导真不用。你只需要知道当一个(batch, seq_len, hidden)的张量经过某个变换后梯度应该怎么流向它的输入。说白了你只要会检查“形状对不对”就够了这就是深度学习框架为什么叫“自动求导”的原因——梯度计算交给autograd但你要有能力判断梯度是否合理。我记得自己第一次手写softmax cross entropy的反向传播时被那个(y_hat - y) / batch_size的形状搞得头大。后来发现只要把softmax的雅可比矩阵推导一遍你就永远都不会怕梯度维度对不上了。建议你也可以试试手推一遍真的会有质变。2.2 编程基础Python之外的隐形门槛AI工程里的“工程”俩字意味着你光会写模型代码是不够的。首先是Python本身的熟练度。不是那种“会写for循环”的熟练度而是你要对list、dict、set的复杂度心里有数要熟悉*args、**kwargs、装饰器、上下文管理器这些Pythonic的写法。为什么重要因为你在调试的时候会疯狂跟Python解释器打交道如果你连异常堆栈都不会读那就寸步难行。其次是NumPy。PyTorch的tensor和NumPy的array长得非常像但机制完全不同。NumPy是immediate执行PyTorch是构建计算图。你如果NumPy玩得溜学PyTorch会快很多。我自己带人的时候会让新人先用NumPy手写一个最简单的线性回归不用PyTorch感受一下什么是手动更新参数。这个练习做完再切到PyTorch你会觉得“自动求导也太爽了吧”。再就是工程配套工具。Git是必须会的别用那种“把代码发给我”的方式协作虚拟环境管理conda或venv必须会用不然装包能把环境搞崩还有requirements.txt、日志输出、断点调试pdb或IDE里的debugger、以及基本的性能分析这些都是“工程”二字的重要组成部分。我把这部分排优先级的话Python熟练度 NumPy Git与命令行 调试工具 虚拟环境。这些不指望你一门课学完但要有个意识然后在一个个项目的推进中逐步补齐。2.3 框架选型为什么推荐从PyTorch开始每个时代都有主流框架。早几年是TensorFlow这两年PyTorch基本是学术界和工业界的默认选择了。新入门的话我建议直接学PyTorch。原因很简单第一生态好。绝大多数论文的官方实现是PyTorchHuggingFace Transformers库也是PyTorch优先这意味着你能找到海量的开源代码可以参考。第二动态计算图。PyTorch默认是eager模式写起来就像在写普通的Python代码调试特别方便你可以在forward里加print、加断点不会像静态图那样让你感觉在跟一个黑盒编译器较劲。第三Pythonic。PyTorch的API设计很符合Python直觉torch.Tensor的用法和NumPy几乎一样。当然TensorFlow也不一无是处尤其在生产部署的某些场景还有存量。但你学的时候如果有一个主框架把深度学习核心概念彻底搞懂切框架是很轻松的事。真正在底层起作用的还是你对张量操作、自动求导、模型结构这些共性的理解。框架只是工具。另外多说一句JAX也值得留意。它是Google出的一个可微编程框架近年来在科研圈增长很快特点是函数式风格和JIT编译。但新手阶段我不建议用它入门因为它的编程范式跟常规的Python直觉差异大容易把你绕晕。3. 从零构建一个小型推理模型的完整实操3.1 定义问题先选一个玩具级任务理论说了一堆下面进入实战。我想用一个非常经典也很有代表性的任务来演示训练一个字符级的mini语言模型让它学会生成莎士比亚风格的文本。这个任务是“AI Engineering from Scratch”的最佳入门原因有三点第一它覆盖了大语言模型LLM的核心全流程数据分词字符级、embedding、transformer block、decoder-only结构、语言建模loss交叉熵、推理采样。第二不需要大算力。一个几百M参数的小模型在哪怕只有一个普通显卡的机器上也能跑。如果你连GPU都没有用Colab的免费版也能跑起来就是慢点。第三直观有趣。训完之后你可以让模型自己续写文本看到它从输出一堆乱码到越来越像人话那个过程很有成就感。《Build a Large Language Model (From Scratch)》这本书里的实现思路基本上也是沿着这个路线走的。我觉得这个思路很值得学习——先做一个玩具级模型理解核心机制再逐步scale up到更大的数据和更大的模型。3.2 数据准备字符级分词与张量化做语言模型第一个环节是数据准备。如果你用HuggingFace的tokenizer是看不到“分词”内部原理的。所以我建议在入门阶段直接用字符级分词character-level tokenization这会让你彻底搞明白token到底长什么样。步骤很简单import numpy as np text open(shakespeare.txt, r, encodingutf-8).read() chars sorted(list(set(text))) vocab_size len(chars) stoi {ch: i for i, ch in enumerate(chars)} itos {i: ch for i, ch in enumerate(chars)} encode lambda s: [stoi[c] for c in s] decode lambda l: .join(itos[i] for i in l)这里有一个关键点字符级分词意味着词表大小就是字符的种类数通常只有几十到上百个。这个模型一个token就是一个字符所以它生成文本时是一字一字蹦出来的。工业级的大模型会用到BPEByte Pair Encoding这类子词分词器把“tokenization”变成更复杂的过程但字符级是理解这套流程的最简入口。接着要构造训练样本。给定一段连续的文本序列模型的任务是预测下一个字符。所以你要构造输入x和目标y其中y是x向左平移一位的结果。我习惯这样写import torch block_size 64 # 每个样本的序列长度 data torch.tensor(encode(text), dtypetorch.long) batch_size 32 def get_batch(data, block_size, batch_size): ix torch.randint(len(data) - block_size, (batch_size,)) x torch.stack([data[i:iblock_size] for i in ix]) y torch.stack([data[i1:iblock_size1] for i in ix]) return x, y这段代码简单直接但有几个细节需要展开说。block_size相当于“上下文窗口”决定模型能看到多长的历史。太小了模型记不住前文太大了训练成本高。对字符级模型64到128就够用了。batch_size决定了每个step能看到多少条序列。这里的索引采样是随机的也就是说每个batch从整篇文本里随机抽取起点而不是按顺序切分。这么做的好处是训练效率高坏处是有轻微的序列边界截断问题但对玩具模型无伤大雅。3.3 实现模型架构手写一个极简Decoder-Only Transformer数据搞定了现在到了核心从零实现一个小型Transformer。我见过太多人的代码是直接调nn.Transformer的但你用这个库时注意力mask是啥形状、为什么需要causal mask、residual connection在哪、layer norm放在什么位置这些关键细节都是隐藏的。所以我建议至少要手写一个简化版。我先把代码结构列出来然后逐模块解释import torch.nn as nn import torch.nn.functional as F class LayerNorm(nn.Module): def __init__(self, dim, eps1e-5): super().__init__() self.gamma nn.Parameter(torch.ones(dim)) self.beta nn.Parameter(torch.zeros(dim)) self.eps eps def forward(self, x): mean x.mean(-1, keepdimTrue) var x.var(-1, keepdimTrue, unbiasedFalse) return (x - mean) / torch.sqrt(var self.eps) * self.gamma self.beta class AttentionHead(nn.Module): def __init__(self, dim, head_dim): super().__init__() self.q nn.Linear(dim, head_dim, biasFalse) self.k nn.Linear(dim, head_dim, biasFalse) self.v nn.Linear(dim, head_dim, biasFalse) def forward(self, x): B, T, C x.shape q self.q(x) k self.k(x) v self.v(x) attn_weights q k.transpose(-2, -1) / (q.shape[-1] ** 0.5) # 下三角mask保证只能看前面的token causal_mask torch.tril(torch.ones(T, T, devicex.device)).view(1, T, T) attn_weights attn_weights.masked_fill(causal_mask 0, float(-inf)) attn_weights F.softmax(attn_weights, dim-1) out attn_weights v return out为什么要scaled by sqrt(d)如果不除attention分数会随着维度增大而变得很大在softmax之后分布会很尖锐接近one-hot导致梯度变得很小。除以根号d是为了把方差拉回可控范围。为什么必须用因果mask因为训练的时候模型的任务是“根据前面的token预测下一个token”在预测第t个位置的时候它不应该看到t之后的任何信息。如果不mask模型就“作弊”了直接看到了答案。推理阶段输入只有当前能看到的tokens天然满足因果性但训练阶段每个样本是完整序列所以必须mask掉未来信息。然后是Multi-Head的组装。多头注意力就是把维度切分成几份每个头独立做attention然后concat起来。注意不同头会学到不同位置的关系——有的头关注语法有的头关注邻近词搭配有的头关注长距离依赖。我们不需要手动指定哪个头干什么训练自己会分化。class MultiHeadAttention(nn.Module): def __init__(self, dim, n_heads, head_dim): super().__init__() self.heads nn.ModuleList([AttentionHead(dim, head_dim) for _ in range(n_heads)]) self.proj nn.Linear(n_heads * head_dim, dim) def forward(self, x): out torch.cat([h(x) for h in self.heads], dim-1) return self.proj(out)接下去是前馈网络Feed-Forward Network, FFN。Transformer里attention负责“收集信息”FFN负责“加工信息”。FFN通常是把维度先放大4倍做非线性激活再缩回去。这里用GELU或者ReLU都可以GELU在很多大模型里更流行。最后把每个block组合起来block内部顺序是attn residual然后FFN residual。注意LayerNorm在现代架构里通常是Pre-LN在attention和FFN之前做归一层而不是原始论文里的Post-LN。Pre-LN的好处是训练稳定适合深网络。你如果喜欢折腾可以两种都试试看看对loss收敛的影响。完整blockclass Block(nn.Module): def __init__(self, dim, n_heads): super().__init__() head_dim dim // n_heads self.ln1 LayerNorm(dim) self.attn MultiHeadAttention(dim, n_heads, head_dim) self.ln2 LayerNorm(dim) self.ffn nn.Sequential( nn.Linear(dim, 4 * dim), nn.GELU(), nn.Linear(4 * dim, dim), ) def forward(self, x): x x self.attn(self.ln1(x)) x x self.ffn(self.ln2(x)) return x最后是整个语言模型本体token embedding position embedding 若干个Block LayerNorm 输出线性层 cross entropy loss。embedding层把离散的token映射为连续向量位置embedding给序列注入位置信息。因为attention本身是顺序无关的它只是计算两两token的相似度不看它们在序列里的先后位置所以必须额外注入位置信息。class MiniGPT(nn.Module): def __init__(self, vocab_size, dim, n_heads, n_blocks, block_size): super().__init__() self.token_embedding nn.Embedding(vocab_size, dim) self.pos_embedding nn.Parameter(torch.zeros(1, block_size, dim)) self.blocks nn.Sequential(*[Block(dim, n_heads) for _ in range(n_blocks)]) self.ln_f LayerNorm(dim) self.lm_head nn.Linear(dim, vocab_size) def forward(self, idx, targetsNone): B, T idx.shape tok_emb self.token_embedding(idx) pos_emb self.pos_embedding[:, :T, :] x tok_emb pos_emb x self.blocks(x) x self.ln_f(x) logits self.lm_head(x) if targets is not None: B, T, C logits.shape logits logits.view(B * T, C) targets targets.view(B * T) loss F.cross_entropy(logits, targets) return logits, loss return logits这里有个小知识点为什么不直接把nn.CrossEntropyLoss放进forward里而是要做view(B*T, C)因为交叉熵期望的logits形状是(N, C)targets形状是(N,)而我们原始的logits是三维(B, T, C)需要把它压平。这个mini模型参数总量大概在几百万量级。相比之下GPT-2是1.5亿参数GPT-3是1750亿参数。但麻雀虽小五脏俱全你能从这个模型中观察到attention的pattern、loss的变化曲线、生成的文本演进这些体验和在大模型上是一致的只是规模小太多跑起来很快。3.4 训练循环从loss不下降到loss稳定模型篇代码写完下面进入训练环节。很多教程会把训练代码一笔带过但我认为训练循环才是新手最容易写错的地方。最典型的错误有忘掉optimizer.zero_grad()、把loss.backward()写在step之后、学习率设置不合理。这些看起来不起眼但每一个都足以让你的模型白练。一个标准的PyTorch训练循环optimizer torch.optim.AdamW(model.parameters(), lr3e-4) for step in range(5000): xb, yb get_batch(data, block_size, batch_size) _, loss model(xb, yb) optimizer.zero_grad() loss.backward() optimizer.step() if step % 500 0: print(fstep {step}, loss {loss.item():.4f})几个细节要特别说明关于学习率语言模型的常见学习率在1e-4到3e-4之间。太大会导致loss震荡甚至发散太小会收敛很慢。如果你用的是AdamW默认的betas(0.9, 0.999)一般不用动weight_decay设置0.01到0.1之间。很多人只关注学习率而忽略weight decay其实这个对泛化能力影响也不小。关于梯度裁剪大模型训练里梯度裁剪几乎是标配。在.backward()之后、.step()之前加一行nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。这个操作可以防止梯度爆炸特别是在训练初期、loss快速变化的阶段。我看到过不少模型loss突然变成NaN往往就是没做裁剪。关于batch大小与显存如果你的显存不够batch_size可以调小到8甚至4同时适当把学习率也调小一点。因为batch越小梯度噪声越大学习率太高会不稳。这也是一个工程经验改batch size时记得考虑是不是该同步改学习率。跑起来之后你会看到loss从初始的ln(vocab_size)附近开始下降。注意这个初始值如果你的词表有65个字符ln(65)≈4.17。如果初始loss远高于这个值说明模型架构有问题如果一开始就低于这个值也说明有问题因为模型不可能在没训练过的情况下就比随机猜测更好。这个初始值check是一个很有用的调试技巧。很多人遇到模型不work第一反应是改网络结构但我会先看初始loss对不对。随着训练推进loss会慢慢降到2.5、2.0、甚至1.5左右。对你来说训练2000-5000步就够了不需要等它收敛到完美——玩具模型的目的是跑通流程不是刷指标。3.5 推理实现让模型“开口说话”训练完的模型怎么生成文本这里需要“自回归生成”每次给模型一个前缀让它预测下一个token的概率分布从中采样一个token然后把新token拼接到输入里重复这个过程。注意这里不是简单地取argmax而是采样否则模型会陷入重复的死循环。一个朴素的采样函数def generate(model, idx, max_new_tokens): model.eval() for _ in range(max_new_tokens): idx_cond idx[:, -block_size:] logits model(idx_cond) logits logits[:, -1, :] probs F.softmax(logits, dim-1) idx_next torch.multinomial(probs, num_samples1) idx torch.cat((idx, idx_next), dim1) return idx几个关键点为什么只取最后一个位置的logits因为在自回归模型中预测下一个token只需要用到最后一个位置的输出它已经编码了前面所有上下文的信息。为什么要限制idx_cond不超过block_size因为模型训练时只见过最多block_size长度的输入超过这个长度位置编码是没训练过的严格来说pos_embedding里的参数覆盖了block_size但后续位置是越界的。在推理时我们只保留最近block_size个token作为上下文超出部分直接截掉。为什么用torch.multinomial而不是argmaxargmax会得到确定性输出模型会不断重复同一句话。采样则引入了随机性让生成文本更自然但也可能导致偶尔语法不通。你可以加个temperature参数来控制采样随机性logits logits / temperature温度越低越保守温度越高越发散。我实际跑出来的效果是这样刚开始训练几百步模型输出的是一串随机字符训练一两千步后它开始能拼出一些像英文单词的片段到四五千步它偶尔会蹦出一个完整的莎士比亚式短语。这个过程真的很神奇你亲眼看到一个“没有灵魂”的矩阵运算组合通过梯度下降慢慢学会了语言的统计规律。4. 从模型到工程训练、评估与部署的完整链路4.1 训练实验管理留痕比跑分更重要很多“from scratch”的教学到训练完就结束了但真正的AI工程远不止于此。你训练的时候难道不想知道哪个配置效果好、哪种学习率最优吗如果每次都手动记笔记很快你就会发疯。我的习惯是从一开始就养成“实验留痕”的习惯。最简单的方式是固定的目录结构加csv日志experiments/ 2025-01-01_shake_char_base/ config.py train.log model.pth train_losses.csvconfig.py记录超参数train_losses.csv记录每个step的loss。有了这些你不仅能看到自己试过什么还能复盘“为什么这个配置不行”。等你的项目复杂到需要几十上百次实验时这个习惯就变成刚需了。再往上走可以学习一下wandb或mlflow这类实验管理工具但新手阶段先自己搭个简单的就好。4.2 评估指标loss下降不等于模型好用loss是训练指标但用户真正关心的是生成质量。训练集上的loss和实际生成质量之间的关系不是线性的——有时候loss降得很好生成质量却一塌糊涂。一个很常见的现象模型在训练集上loss很低但你让它生成新文本时它总是在复述训练集里的句子。这就是过拟合。怎么在玩具模型上快速判断把你的数据分成训练集和验证集每训练几轮就在验证集上跑一次loss。如果验证loss不再下降甚至上升而训练loss还在降说明过拟合了。要不要用更复杂的指标比如BLEU、ROUGE对字符级玩具模型来说这些指标意义不大。但我建议你用“人工评估”的方式直观感受让模型生成几段文本自己看看流畅度、语法、风格。这其实也对应了大模型评估中的核心方法论——人类偏好评估。别小看这个很多人训练完模型连生成几段样本看一眼都不做就急着改结构、调参数这本质上是一种盲目。4.3 部署与性能优化从“能跑”到“跑得快”模型训完了也要会用这里就涉及工程化的部署。推理部署相比训练有一个完全不同的优化思路。训练关注的是吞吐量部署关注的是延迟。比如你是做一个Web服务用户点击“生成”你肯定希望200ms内返回而不是等好几秒。最少要会做几件事。第一模型导出。PyTorch的训练模型不能直接用于生产服务通常要转成更高效的格式。最简单的做法是直接用model.eval()加torch.no_grad()但这还不够快。进一步你可以用torch.compile()它会对计算图做融合优化在某些场景下能提升30%-50%的推理速度。再进一步可以用ONNX导出然后配合ONNX Runtime或者TensorRT做推理。第二权重量化。模型在推理时默认是float32但你可以转成float16甚至int8。内存占用直接减半甚至减到四分之一速度也明显提升。代价是精度略微下降。对生成任务来说float16几乎无损int8则要具体评估。第三批处理。Web服务如果在高并发场景下单请求一个batch太浪费GPU算力。工程上可以做动态batching把多个请求凑成一个batch喂给模型大幅提升吞吐。这一点在实际生产里非常常用但很多人只知道“训练的时候用batch”不知道“部署也能用batch”。记住这个核心理念工程化的本质不是“模型有多聪明”而是“在给定的硬件和延迟约束下尽可能把模型能力最大化地交付给用户”。这个意识才是“AI engineering”里“engineering”的真正含义。5. 常见问题与排查技巧实录5.1 损失不降反升先别急着改模型结构遇到loss不降的情况很多人第一反应是“模型太简单了我要加深加宽”。这个判断往往错误。根据我的经验loss不corner通常有几种常见原因按优先级排查现象可能原因排查方法loss完全不降维持初始值数据加载出错标签跟输入错位打印一个batch检查x和y的关系loss在震荡学习率过大把学习率降一个数量级再试loss正常下降但突然变成NaN梯度爆炸加梯度裁剪clip_grad_norm_训练loss降但验证loss不降过拟合增加数据/减小模型/加正则化输出全是一个tokensoftmax温度太低导致重复或注意力mask写错检查推理代码温度调高这里我想单独说一下“数据加载出错”这个问题。别看它low这是我见过最多的bug来源。比如我构造y x[i1:...]的时候如果不小心把切片写成了x[i:...]模型学到的就是“预测当前token”loss也会降但生成结果一定是胡说八道。这种错误只有在你手动检查一个batch的实际内容时才会发现。我专门写过一个函数打印batch里的中英文字符序列给新手看让他们亲眼确认“输入是前一个token预测目标是后一个token”。这种检查听起来很笨但真的很有效。5.2 因果mask写错模型偷看未来效果虚高因果mask是自回归模型最容易写错的地方之一。常见的错误包括mask的方向搞反、mask没有正确广播到batch维度、mask在attention weights加在softmax之后等等。一个经典的debug方法是构造一个只有两个token的序列[A, B]单独跑一次forward打印attention权重。如果第一个token的attention分数在softmax之后只对自己有非零值第二个token对第一个和第二个都有非零值那说明mask是对的。如果第二个token对自己和第一个token的softmax分数一样高那很可能是方向写反了。另一个相关的错误是在推理时有些实现会把完整生成的序列每一轮都重新计算一遍attention重复计算所以很慢。正确做法是KV Cache——把已经计算过的K和V缓存下来每步只计算新token的K和V。玩具模型里可以不做这个优化但你应该知道这个机制的名字。5.3 显存不够batch size不是唯一可调的旋钮很多人在本地机器上跑模型一张消费级显卡显存8G或16G。batch size稍微调大一点就OOM。除了调小batch size你还有几个选择用gradient_accumulation_steps做梯度累积。每N个小batch累积一次梯度再更新参数。效果等同于增大batch size显存却不受影响。降低序列长度block_size。显存占用跟序列长度也成正比。开启torch.cuda.amp混合精度训练。把部分操作从float32转成float16显存和速度都能改善。不过有一点提醒混合精度在大的语言模型上是标配但对小模型收益不大甚至可能因为额外的转换开销变慢。所以不是所有优化手段都适合所有场景你得自己实验。5.4 生成文本质量差从工程和模型两个方向排查如果模型训练完生成的文本不连贯先别急着骂模型“学得不好”。分两步排查。第一步检查训练数据是否干净。如果语料里有大量乱码、重复片段、不同语种混杂模型学到的东西当然不伦不类。第二步检查推理超参。temperature设太低模型倾向于输出概率最高的token链容易陷入重复temperature设太高输出就变成随机的字符序列。top-k和top-p采样也能显著影响生成质量。我自己常用的一组保守参数是temperature0.8top_k40top_p0.9。你可以用这组参数做baseline再微调。第三步如果前面都没问题再考虑是不是训练步数不够、模型容量不足。注意别一上来就堆参数先把前面几步排查干净再说。5.5 一个我自己踩过的坑忘记model.train()这里说一个比较搞笑的经历。有次我用一个已经训练好的模型继续训练因为刚做完推理还没切回训练模式模型里的dropout层和BatchNorm层仍然在推理模式导致loss降得特别慢而且曲线怪怪的。我排查了半天最后发现是忘了加model.train()。在PyTorch里model.eval()和model.train()会切换dropout、batch normalization这些层的行为。如果你在训练时忘记切回train模式dropout不会生效BN也不会计算batch的统计数据最终影响就可能很隐蔽。对于纯Transformer结构来说它的残差、层归一化不依赖这个切换但只要你用了dropout就必须检查这里。我习惯在代码里把训练前几行写成固定的模板model.train() optimizer.zero_grad()这三行连在一起形成肌肉记忆就不会忘了。6. 学习资源、动手实践与进阶路径6.1 那些经典的“from scratch”学习材料怎么用如果你要把“AI engineering from scratch”这条路走通市面上有几份非常经典的学习材料我亲测过也带人用过可以负责任地推荐给你。第一份是Sebastian Raschka的《Build a Large Language Model (From Scratch)》这本书。这本书最大的优点就是“真的从零开始”——不依赖任何现成的大模型库从数据准备到模型架构再到训练全部手写代码而且代码都能跑。我建议按章节跟着敲千万不要只看不写。每章后面的练习一定要做那才是真正检验你是否理解的地方。第二份是Andrej Karpathy的YouTube视频教程特别是“Let‘s build GPT: from scratch”那一期。Karpathy是那种能把复杂东西讲得很通透的人他带着你一行行写代码从bigram模型开始逐步演进到mini版GPT。他的代码有一个特点风格极简没有花哨的类封装就是为了让你看清本质。但我觉得他视频的最大价值还不是代码而是讲“为什么”——为什么要做normalization、为什么会有residual connection、为什么需要用position encoding这些都讲透了。第三份是大模型入门必读的原始论文。不要怕读论文推荐先读《Attention Is All You Need》Transformer论文和GPT系列论文GPT-1、GPT-2、GPT-3。读论文的目的不是让你背诵结构而是理解论文里的某些设计决策背后的动机。你会发现当初的Transformer是没有LayerNorm位置的讨论的是后来训练不稳定才慢慢摸索出Pre-LN架构。这些历史沉淀下来才是真正的工程经验。我必须要提醒的是学习资源别贪多。把上面其中一份资料彻底啃完比存十份资料在收藏夹吃灰有价值得多。这个领域最大的学习陷阱不是“没有资料”而是“资料太多导致行动瘫痪”。6.2 从玩具模型到reasoning model差距在哪里标题里提到了一个很热的概念build a reasoning model from scratch。我自己最近也在折腾reasoning model这个方向非常新还处于快速演进阶段。从我们前面写的mini语言模型到能进行数学推理、逻辑推理的reasoning model中间的差距可以拆成几层。第一层规模和数据。reasoning model的基础是一个足够强的预训练语言模型这个模型要有足够大的参数量、足够丰富的训练数据。你手写的mini模型是几十万参数而reasoning model的底座动辄几十亿、几百亿参数这之间差了四个数量级。规模带来的不只是量的提升还会产生质的改变——小模型学不会的推理能力在大模型上会“涌现”出来。第二层训练范式。Reasoning model不是简单地在预训练基础上继续预测下一个token而是要引入“思考”的环节常用的范式有Self-Consistency、Chain-of-Thought思维链微调、以及R1-style的强化学习训练。这些方法的核心思想是让模型在给出最终答案之前先产生一段推理过程。你可以猜到这个流程和自回归生成的基本机制完全一样——它只不过把“推理轨迹”也当成token序列来生成。第三层评测与对齐。Reasoning model要能解决数学题、编程题、逻辑推理题所以需要搭建评测集并且要防止模型“为了推理而推理”——生成很长一段推理过程但是最终答案错误。对于入门者我的建议是先把基础的LLM全流程走通再进阶到SFT监督微调、LoRA、RLHF或GRPO这些训练范式最后才去碰reasoning model。别一上来就追最热的概念底子不牢追概念也只是看热闹。6.3 进阶路线下一步可以做什么如果你已经把上面的代码跑通了那恭喜你你已经不再是“AI小白”了。接下来怎么进阶我根据自己的经验给你几条可选的路线。一是往“广度”走。用HuggingFace Transformers库替换手写代码在真实的预训练模型上做微调比如微调一个对话模型、做情感分类、做命名实体识别。这条路线适合想快速做应用的人。二是往“深度”走。继续手写更复杂的东西实现一个BPE分词器、实现KV Cache、实现LoRA微调、手写一个简单的训练分布式框架。这条路线适合想搞懂底层原理、将来做框架开发或算法研究的人。三是往“数据”走。自己从网上爬数据做清洗去重构建一个高质量的自有数据集然后训练自己的领域模型。这条路线适合想在垂直场景做差异化应用的人。还有一个建议给自己找一个“项目式目标”。不要泛泛地“学习AI”而是选定一个小项目比如“写一个唐诗生成器”“做一个客服意图分类器”“做一个本地运行的问答助手”。目标明确之后你自然知道该学什么、该查什么学习效率会高出很多倍。写在最后文章写到这里我把“AI engineering from scratch”的整个学习路径、核心原理和实操细节都过了一遍。还是想跟你强调一下从零开始做的事情看上去效率低但它构建的是你的判断力和解决问题的能力。我见过太多用框架用得飞起、但是模型一出错就束手无策的人也见过数学功底扎实但写代码像屎山、谁都不想接手的人。真正的AI工程能力是在这两个方向上有基础、还能把它们串起来。最后分享一个小技巧每当你学完一个环节试着把代码删掉然后凭记忆重新实现一遍。如果第二次写的时候还能流畅跑通说明你真的掌握了。如果卡住了就回去翻资料继续练。这一招我每次带新人都会用效果出奇的好。这条路很长但每一步都值得。希望你也能从零开始建出属于你自己的第一座AI大厦。
返回列表