尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
从零手写AI系统:BPE、反向传播与Transformer实战
1. 这个项目想解决什么与其调 API不如拆一台发动机看看如果你翻过我的仓库会发现ai-engineering-from-scratch不是那种三天上手大模型的教学 Demo也不是一个包装华丽的工具集。它更像是一份我自己的学习工程笔记假设你面前没有 OpenAI、没有 HuggingFace、没有 LangChain只剩 Python 和 numpy你能不能徒手搭出一个能跑、能训练、能推理的 AI 系统为什么要做这件事原因挺现实的。过去两年我大量使用现成的大模型 API 做应用越用越心虚。模型回答错了我不知道该怪数据、怪训练、怪提示词还是怪温度参数想微调一个小模型却连 loss 曲线都读不太懂。你可以在 API 层面工作很久但一旦遇到幻觉、重复生成、上下文窗口溢出这类工程问题没有底层理解排查全靠撞运气。这就好比你会踩油门但发动机灯亮了完全不知道哪里出问题。所以这个项目的思路很直白每一层都自己造。Tokenizer 不调tiktoken模型不用transformers优化器不碰torch.optim。所有组件的代码量控制在能一屏看完的程度但逻辑必须完整。目标读者我觉得有两类人一类是刚入门 AI 工程、想知道大模型到底怎么从 0 变成会说话的同学另一类是已经用 API 做了一堆应用、想补底层课、提升排查能力的开发者。仓库的整体结构我按从数据到智能的路径来组织每一课都对应一个可运行的脚本。你在 README 里能看到训练好的字符级模型是怎么一步步从输出乱码变成输出像模像样的句子这种从无到有的正反馈比任何原理图都更有说服力。2. 第一块积木不调 Tokenizer 库徒手实现 Byte Pair Encoding很多人学大模型第一个拦路虎其实是输入层。模型不认识字符串它只认数字。你当然可以用最简单的方式——每个字符映射一个整数但这样做出的模型泛化能力很差遇到训练集里没出现的词就是个死局。真实大模型用的都是子词切分其中 Byte Pair EncodingBPE是最经典的一种。2.1 BPE 到底在做什么想象你有一堆英文单词要编码。BPE 的思路是先按单个字符拆开然后统计相邻字符对的出现频率把最高频的一对合并成一个新符号反复执行这个合并过程直到符号表达到预定大小。这样做的好处是常见词比如 the 很快就变成单独一个 token不常见的词则退化成多个子词 token模型永远有办法表示没见过的新词。我在项目里实现的核心逻辑很朴素def get_stats(ids): counts {} for pair in zip(ids, ids[1:]): counts[pair] counts.get(pair, 0) 1 return counts def merge(ids, pair, idx): newids [] i 0 while i len(ids): if i len(ids) - 1 and ids[i] pair[0] and ids[i1] pair[1]: newids.append(idx) i 2 else: newids.append(ids[i]) i 1 return newidsget_stats统计相邻 token 对的频率merge把最高频的那一对替换成一个新的 token 编号。训练过程中重复这两个操作vocab里记录每次合并产生的符号推理时按同样的合并规则把文本编码成 token 序列。2.2 实现时最容易被忽略的两个细节第一个坑是编码和解码必须严格对称。很多人只关注编码端怎么合成新 token忘了解码端的规则表要一次不漏地保存。我在项目里把合并规则按顺序存在merges字典里从bytes到 token 是可逆的。第二个坑是原始语料必须按 UTF-8 先编码成字节再在字节层面做 BPE。否则中文这种多字节字符会被拆得乱七八糟或者干脆因为字符种类太多导致词表爆炸。这一点在真实大模型里也是通用做法早期 GPT-2 就是这么处理的。BPE 的实际效果很直观同样一段文本字符级编码可能有上千个 tokenBPE 能压到两三百个sequence 变短意味着训练时注意力计算量显著下降也能捕获更多语言规律。我在仓库里放了一个可视化脚本把同一个句子在不同词表大小下的切分结果打印出来对比效果非常明显。3. 最难的一课手写反向传播把线代落成代码模型输入搞定了接下来是神经网络本体。很多人用 PyTorch 久了写nn.Linear、nn.CrossEntropyLoss都是一行事但真要问 y xW b 的梯度怎么回传反而卡壳。ai-engineering-from-scratch的核心主张就是你至少要有能力不用自动微分框架手推一个全连接网络的训练。3.1 一个干净的 MLP 反向传播实现我在项目里写了一个两层的 MLP不依赖任何深度学习框架纯 numpy 实现前向和反向。核心代码是这样class TwoLayerNet: def __init__(self, input_size, hidden_size, output_size): self.W1 np.random.randn(input_size, hidden_size) * 0.01 self.b1 np.zeros(hidden_size) self.W2 np.random.randn(hidden_size, output_size) * 0.01 self.b2 np.zeros(output_size) def forward(self, x): self.z1 x self.W1 self.b1 self.a1 np.tanh(self.z1) self.z2 self.a1 self.W2 self.b2 return self.z2 def backward(self, x, y, output, loss_grad): dloss loss_grad dW2 self.a1.T dloss db2 dloss.sum(axis0) da1 dloss self.W2.T dz1 da1 * (1 - self.a1 ** 2) dW1 x.T dz1 db1 dz1.sum(axis0) return dW1, db1, dW2, db2这段代码看起来简单但每一步都对应链式法则的某一环。dz1 da1 * (1 - self.a1 ** 2)就是 tanh 激活函数的导数。我建议你在本地手推一遍梯度公式再看这段代码会有种哦原来自动微分干的就是这事的通透感。3.2 为什么维度经常搞错反向传播最容易出错的地方是矩阵乘法的维度对齐。我的经验口诀是任何一层权重的梯度形状必须和这层权重完全一致。如果算出来不一致十有八九是某个转置写错了。比如dW2的公式是self.a1.T dloss因为前向是a1 W2反向时输入和梯度要交换位置并转置。检查维度还有一个笨办法在每步反向计算后打印.shape和前向的对应张量比对一遍这种防御性编程在调试期特别值。4. 从 MLP 到最小 Transformer我终于看懂了注意力层MLP 是热身真正的重头戏是把 transformer 从零搭出来。这部分我参考了 Andrej Karpathy 的教学思路结合自己对工程细节的理解做了一套极简实现。Transformer 的优点大家都听过并行计算、长距离依赖建模。但只有自己写一遍注意力矩阵你才会真正明白它为什么能 work。4.1 自注意力机制的最小实现自注意力的公式是 Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) V。直接看公式会有点懵我的理解方式是这样的每个 token 都发出一个查询Q想知道自己该关注谁同时每个 token 都亮出一个键K告诉别人我是什么最后每个 token 还带着值V是真正要传递的信息。Q 和 K 做点积就是在衡量我和你有多少关系除以 sqrt(d_k) 是防止点积数值过大把 softmax 推成极端分布。最后用这个权重去加权求和所有 V得到新的表示。代码实现核心也就十几行def self_attention(x, Wq, Wk, Wv, Wo): B, T, C x.shape q x Wq # (B, T, head_dim) k x Wk v x Wv attn_weights q k.transpose(-2, -1) / np.sqrt(head_dim) attn_weights np.tril(attn_weights) # 因果掩码 attn_weights softmax(attn_weights, axis-1) out attn_weights v return out Wonp.tril是关键细节语言模型只能看左侧的 token所以注意力矩阵的上三角要全部屏蔽成负无穷softmax 之后这些位置的概率就会变成 0。很多人实现时漏了这个掩码模型训练时偷看未来 tokenloss 看起来正常但生成效果一塌糊涂。4.2 多头注意力其实只是个 reshape 技巧多头注意力听起来高级本质就是把head_dim拆成几份分别做注意力再拼接。我在仓库里用 numpy 实现时直接 reshape 成(B, num_heads, T, head_dim)每个头独立计算最后再拼接回原维度。这一步请务必注意拼接后要经过一个输出投影层才能让不同头的信息有效融合。只拼接不投影多头就退化成单头了。5. 训练一个会说话的小模型损失曲线会告诉你一切模型搭好了接下来是最磨人的训练阶段。我的仓库里用莎士比亚文集做语料训练一个字符级的小 transformer参数量大约 300 万。这个规模在 GPU 上几分钟能跑完一轮在 CPU 上也能忍非常适合学习迭代。5.1 训练循环里最容易被低估的小事一个看似不起眼但至关重要的细节是 batch 采样。很多人直接按顺序把文本切成固定长度片段这样相邻 batch 的数据高度相关模型会记住顺序而不是学会语言。我通常的做法是把文档随机切成长度为 block_size 的块再用随机 shuffle 抽 batch。PyTorch 的DataLoader里有shuffleTrue但如果你是自己写循环非常容易忘了这一步。第二个细节是梯度累积。小模型显存有限batch size 可能只有 8 或 16训练很不稳定。工程上常见的做法是小 batch 算完梯度后不马上更新累积几个 batch 再一起更新权重。我在项目里实现了一个简单的梯度累积optimizer.zero_grad() for micro_step in range(grad_accum_steps): logits, loss model(X_batch, Y_batch) loss loss / grad_accum_steps loss.backward() optimizer.step()5.2 我踩过的学习率深坑Loss 不降的时候我第一反应是调高学习率结果损失震荡模型直接发散。后来养成看曲线的习惯就明白了学习率太低 loss 下降平缓太高 loss 上下剧烈跳。我自己摸索出来的有效策略是带 warmup 的余弦衰减——前几百步让学习率从极小值线性爬升到峰值后面按余弦曲线衰减到接近 0。这么做的直觉是训练刚开始权重初始化完全随机太激进的学习率会把模型推到奇怪的损失地形里warmup 给了模型一个先站稳再跑的机会。我在仓库里记录了完整的训练日志loss 从最初的 4.2 左右逐步降到 1.5 附近。你会发现一个规律前几千步 loss 下降飞快后面变得很慢这是正常的因为模型先学会的是高频语法模式剩余的是低频语义细节收敛速度自然放缓。6. 从哑模型到能用解码策略和提示词工程一起上训练出来的模型不会自动好用还得靠推理端的解码策略。我在项目里实现了三种经典采样方法贪心解码、温度采样、top-k 采样。它们的区别直接决定了生成文本的质量。贪心解码每一步选概率最高的 token输出稳定但容易陷入重复语句。温度采样把 logits 除以温度 T。T 越小分布越尖锐文本越保守T 越大分布越平坦文本越有创造性。常用范围是 0.7 到 1.2。Top-k 采样只从概率最高的 k 个 token 里采样避免把极小概率的垃圾 token 选出来。建议你把三种策略都跑一遍用同一个 prompt 生成 100 个字符马上能感受到差异。实际产品里通常组合着用top-k 温度调参既保证多样性又避免失控。提示词工程看起来和模型训练是两个世界但这个项目里我用一个例子把它们串起来了。比如你想让模型扮演一个毒舌评论家直接在 prompt 里写你是一个毒舌评论家效果远不如给一个完整的格式示例。我在仓库里放了一套模板化 prompt 的写法核心原则是给模型展示你期望的输出结构而不是抽象描述。任务给下面这句话写一条毒舌评论 要求每条约 20 字带一个讽刺比喻 示例 输入——我今天早起跑了五公里。 输出——太阳都还没准备好上班你倒是先卷起来了。 输入——{user_input} 输出——这套方法在 API 时代照样适用但是理解它生成机制的人更容易写出好模板。提示工程不是玄学本质是你和概率分布的一场对话。7. Agent 和工具调用AI 工程的最后一公里模型能生成文本不等于能解决问题。真正让 AI 变得有用的是 Agent——让模型能调用工具、能看工具返回结果、能根据反馈决定下一步行动。这是我在ai-engineering-from-scratch里新增的一个模块实现了一个极简的 ReAct 式循环思考、行动、观察交替进行。7.1 Agent 循环的最小骨架我实现的方式很直观模型输出一个 JSON 结构里面包含thought和action字段。程序解析出 action 后执行对应的工具函数再把结果塞回 prompt让模型继续思考。核心循环如下tools { search: search_function, calculate: calculate_function, } def run_agent(query, max_steps5): messages [{role: user, content: query}] for _ in range(max_steps): response llm_complete(messages) action parse_json(response) if action.get(type) final_answer: return action[content] tool_result tools[action[name]](action[args]) messages.append({role: assistant, content: response}) messages.append({role: tool, content: str(tool_result)}) return 达到最大步数这个实现的难点在于模型输出格式的稳定性。模型经常把 JSON 写成多行、加注释、或者少个引号。我的解决办法是在 prompt 里给一个严格的 JSON 示例如果解析失败就把错误信息反馈给模型让它重新输出。工程上这叫自修正循环虽然简单但真的能大幅提高成功率。7.2 防呆设计不能少Agent 一旦跑起来你要面对的不只是模型能力问题还有失控风险。我在项目里至少做了三层防护第一最大循环次数必须硬性限制第二工具调用参数必须强制类型校验防止calculate(abc)这种输入让程序崩溃第三所有工具执行要加 try-except一个工具挂了不能把整个 agent 拖死。这些都是在真实生产环境里被坑过之后总结出来的不要等出事了再补。8. 从头搭完整个项目之后我留下的几条工程经验这个项目写到这里主体内容已经完成但我想额外多聊几句实际操作层面的体会因为光有代码是不够的。第一永远要有一个基线实现用于对照排查。我在手写反向传播时就留了一份 PyTorch 版本的参考实现同样的数据、同样的超参数两边 loss 曲线对照着看。梯度数值不一致立刻知道是自己反向传播哪里写错了。没有这个参照系你会在茫茫 bug 里找不到方向。第二小规模验证一切。transformer 这种模型最烦人的是代码有问题时不一定报错只是 loss 降得慢或者生成结果奇怪。我的习惯是先在极小的数据集上跑通——比如语料只取 5000 个字符block_size 取 32模型层数减到 2。如果小规模能过拟合loss 接近 0说明模型实现没问题再放大规模。如果小规模都学不动果断回去查代码。第三时长评估必须提前做。别用完整数据集做实验。先用 1% 的数据跑一遍看一个 batch 需要多少秒估算出完整训练大概几小时再决定要不要加 batch size、上不上 GPU。我一开始没做这个估算三个月前第一次跑训练直接开了满配数据集结果睡醒发现才跑了 2%白白烧了好多时间。第四数据质量永远值得花时间。我从莎翁文集换到代码语料后模型效果明显变化。同样规模的参数量训练出来的分布式完全不一样。所以说到底AI 工程不只是模型结构的事数据工程占了一半比重。你要是想深入研究不妨把仓库里的数据管道改成爬自己领域的内容你会对垃圾进垃圾出这句话有深刻的真实感受。这个项目我还会继续迭代下一步打算加入多模态输入的基础支持以及更系统的评测模块。最后说一句掏心窝的话从零实现一个 AI 系统不会让你立刻成为专家但它能让你在遇到问题时有底气问出正确的问题。在这个以 API 调用为常态的时代这种知道引擎如何工作的能力反而越来越稀缺了。
RELATED

相关推荐

Hadoop核心技术解析与大数据实战指南

Hadoop核心技术解析与大数据实战指南

1. Hadoop如何重塑大数据分析格局 十年前我第一次接触Hadoop时,面对PB级日志文件的手足无措至今记忆犹新。当时传统数据库在千万级数据面前就已力不从心,而Hadoop的出现彻底改变了游戏规则。如今这个开源框架已成为大数据领域的基石,全球超过…

📅 2026/11/3 14:29:03
Windows下uv工具硬链接失败问题解决方案

Windows下uv工具硬链接失败问题解决方案

1. Windows下uv环境硬链接失败问题解析最近在Windows平台上使用uv工具管理Python环境时,遇到一个典型问题:Failed to hardlink files; falling back to full copy警告频繁出现。这个问题看似不影响功能,但实际上会显著降低依赖安装效率。作为…

📅 2026/11/3 14:29:03
解决VS Code中Python虚拟环境激活报错问题

解决VS Code中Python虚拟环境激活报错问题

1. 问题现象与背景解析最近在Windows系统下使用VS Code开发Python项目时,不少开发者遇到了一个典型报错:当尝试激活虚拟环境时,PowerShell终端抛出错误提示"无法加载文件Activate.ps1,因为在此系统上禁止运行脚本"。这个…

📅 2026/11/3 14:29:03
MORE NEWS

更多资讯

📰

Paperclip模式:基于Claude+OpenClaw+React的本地AI工作流实践

1. 项目概述:Paperclip 不是回形针,而是一个被严重误读的 AI 工程实践入口“Paperclip”这个词在当前技术社区里,正经历一场典型的语义漂移——它不再指代办公室抽屉里那个弯折金属丝制成的日常小物,而是悄然演变为一个指向特定技…

📰

CRC-8三种实现对比:逐位、半字节与全字节查表选型指南

CRC-8 这个校验算法,几乎每个写过嵌入式通信协议的人都绕不开。但有意思的是,同样一个 CRC-8,你在不同项目里看到的实现代码可能长得完全不一样——有人用一张 256 字节的常量表,有人写了个双层循环逐位算,还有人用半字…

📰

Paperclip:面向 React 与 Node.js 的轻量级 AI Agent 运行时框架

1. 项目概述:Paperclip 不是回形针,而是一个正在悄悄改变前端开发范式的 AI Agent 框架你搜“paperclip”时,第一反应可能是办公桌抽屉里那枚银色小金属件——但最近在 GitHub 和掘金社区里,这个词正以惊人的速度脱离文具范畴&…

📰

Hindsight复盘系统:把事后视角变成可复用的判断力

很多人第一次听到 hindsight 这个词,是在讨论心理学里的"后见之明偏差"——说白了就是事后诸葛亮:事情有了结果,人脑会自动改写记忆,让你觉得"我早就知道会是这样"。以前我拿它当贬义,直到连续三次…

📰

Paperclip:轻量级Node.js+React AI Agent任务闭环框架

1. 项目概述:Paperclip 不是回形针,而是一个被严重低估的 AI Agent 开发范式你搜“paperclip”,第一反应可能是办公桌抽屉里那枚银色小金属件——但此刻它正悄悄在开源 AI 工程师圈子里发酵成一个代号。这不是某个大厂新发布的闭源框架&#…

📰

双指针法解决四数之和问题及优化策略

1. 问题背景与核心思路四数之和问题是一个经典的算法挑战,给定一个包含n个整数的数组和一个目标值,要求找出所有不重复的四元组,使得它们的和等于目标值。在这个特定问题中,我们的目标值是0。我第一次遇到这个问题是在准备技术面试…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬