☰
从零构建AI工程能力:手写反向传播与Transformer实战
2026/10/2 12:15:17 网站建设 项目流程

做了这么多年AI相关的工作,我越来越发现一个现象:很多人用框架用得贼溜,PyTorch、TensorFlow的API信手拈来,但一旦遇到网上搜不到的报错、模型效果死活提不上去、或者要在没有现成库的环境里部署,就开始抓瞎。这个现象背后缺的不是"调包能力",而是对AI工程底层原理的理解。

"ai-engineering-from-scratch"这个项目标题,说白了就是一条从零开始构建AI工程能力的路线。它不是教你怎么调库、怎么套模板,而是带着你从数学基础、手写实现、训练调试、部署评估一路走下来,把AI工程这条链路完整走一遍。这篇文章我会把这套路线的核心思路、具体实操环节、以及我实际踩过的坑都拆开讲一遍。如果你是准备转行做AI工程的新手,或者已经是"框架熟手"但想补齐底层认知,这篇文章应该能帮你省下不少试错时间。

1. AI工程的能力全景:先看清地图再出发

1.1 AI工程不是"写模型",是一条完整链路

很多人对"AI工程"的理解就是训练模型,这是个很普遍的误区。我在实际带项目的时候经常碰到这样的新人:模型训练出来效果看着不错,但一上线就崩,数据分布一变化就失效,或者推理速度慢到没法用。这些问题基本都不是模型本身的问题,而是工程链路的问题。

我习惯把AI工程拆成九个环节:业务问题定义、数据采集与清洗、特征工程(现在更常叫数据预处理)、模型设计与实现、训练调优、评估验证、部署上线、监控运维、迭代更新。任何一个环节出问题,整个系统都会出问题。更要命的是,这九个环节在真实项目里的耗时比例和多数人想象的不一样。根据我自己做过的项目经验,数据侧的投入通常能占到整个项目40%以上的时间,模型训练和调优反而只占20%左右,剩下的时间基本砸在部署、监控和迭代上。

这就牵扯出一个关键认知:AI工程师的能力模型应该是一个T型结构。横向要懂整条链路,知道每个环节之间怎么衔接;纵向要在某几个环节上有深度,比如你特别擅长训练调优,或者特别擅长推理性能优化。from-scratch的学习路线天然适合建立这种T型结构,因为你每一步都是从原理出发,链路之间的"黏连感"会非常强,而不是像很多人那样只盯着模型训练那一小块。

1.2 为什么"从零实现"是最稳的成长路径

市面上并不缺AI课程和教程,大部分走的是"调包流"路线——告诉你用这个函数就能实现注意力机制,用那个API就能加载预训练模型。这种学习方式的短期反馈确实很好,两三天就能跑通一个效果还不错的情感分析项目,很有成就感。但问题在于,它给你的是"如何使用工具"的能力,而不是"理解工具"的能力。

我这里不是否定工具的价值,PyTorch、HuggingFace这些工具本身非常优秀,我日常工作也离不开它们。我反对的是"只会用工具"的状态。举一个很实在的例子:你在用PyTorch训练模型时遇到loss不降,如果你理解反向传播的每一行计算逻辑、理解学习率和梯度之间的关系,你排查的思路可能是在学习率策略、梯度消失、数据分布等问题上找原因。但如果你只把模型当黑盒,遇到问题就只能去网上搜"loss不降怎么办",能搜到的往往只是别人的经验,很难迁移到你的具体场景里。

"from-scratch"的学习路径之所以稳,因为它解决的问题不是"怎么跑通一个模型",而是"模型为什么这样设计""为什么这样训练""这里出问题了根源在哪"。这种能力是迁移性最强的。今天大模型火了你去做LLM应用,明天多模态火了你去做多模态,底层原理一通,换框架、换模型架构都只是时间问题。

2. 核心环节逐层拆解:从手写反向传播到自注意力

2.1 数学够用就行,但要真懂

说到从零开始学AI工程,"数学基础"通常是最劝退的一关。我的观点很明确:你不需要成为数学家,但线性代数、概率论和微积分这三门课的核心概念必须真正理解,注意是"理解",不是"背下来"。

你需要的线性代数其实只有几个东西:向量和矩阵的基本运算、矩阵乘法、转置、范数。你需要的微积分就是链式法则和偏导数,这是反向传播的基石。概率论部分你需要的是分布、期望、方差、条件概率、极大似然估计的基本概念。就这些,复杂的东西在工程里会由框架帮你算,但你要能看懂公式的流向。

怎么检验自己"真懂"了?我给个土办法:你用NumPy写一个两层的全连接神经网络,手动实现前向传播、损失函数计算、反向传播和参数更新。这个练习我几乎推荐给每一个想进入AI工程的人,因为当你不用PyTorch等框架的自动求导,而是自己拿纸笔推梯度公式、再把它一行行写成代码时,那种理解是任何课程都替代不了的。

import numpy as np # 一个两层全连接网络的NumPy实现,反向传播手动推导 class TwoLayerNet: def __init__(self, input_size, hidden_size, output_size): # 使用He初始化,对ReLU激活更友好 self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size) self.b1 = np.zeros((1, hidden_size)) self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size) self.b2 = np.zeros((1, output_size)) def forward(self, X): self.z1 = X.dot(self.W1) + self.b1 self.a1 = np.maximum(0, self.z1) # ReLU激活 self.z2 = self.a1.dot(self.W2) + self.b2 return self.z2 def backward(self, X, y, output): batch_size = X.shape[0] # 输出层梯度 dz2 = output - y dW2 = self.a1.T.dot(dz2) / batch_size db2 = np.sum(dz2, axis=0, keepdims=True) / batch_size # 隐藏层梯度,经过ReLU的导数 da1 = dz2.dot(self.W2.T) dz1 = da1 * (self.z1 > 0) dW1 = X.T.dot(dz1) / batch_size db1 = np.sum(dz1, axis=0, keepdims=True) / batch_size return dW1, db1, dW2, db2

这段代码虽然简单,但里面藏了很多值得琢磨的细节:为什么初始化要除以sqrt(input_size)?为什么反向传播要除以batch_size?ReLU的梯度为什么用(z1 > 0)就能表示?每一个细节都能延伸出一个面试题,也都是实际调参时你需要具备的直觉。

2.2 亲手实现Transformer:当代AI工程的必修课

如果说2020年之前,AI工程的核心骨架还是CNN和RNN,那到了现在,Transformer已经成了几乎所有主流模型的基础架构。LLM、多模态模型、甚至推荐系统都在用Transformer的变体。所以from-scratch路线里,亲手从零实现一个Transformer几乎是最重要的一课。

很多教程会直接让你用PyTorch的nn.Transformer或HuggingFace的GPT2模型,但这样写出来的代码基本封装了所有细节,你什么都学不到。我的建议是:分模块手写。先写多头注意力机制,再写位置编码和前馈网络,最后组装成一个完整的decoder-only模型。

这里有一个所有新手都会困惑的点:QKV到底是什么?我用生活化类比解释一下。你在图书馆找书,K(Key)相当于每本书的标签编号,Q(Query)相当于你心里的检索需求,V(Value)就是书架上的实体书。注意力机制做的工作就是:根据你的检索需求(Q),去匹配每本书的标签(K),计算匹配度分数,再按分数把实体书(V)加权取回来。匹配度高的书权重高,匹配度低的书权重低。

class SelfAttention(nn.Module): def __init__(self, embed_dim, num_heads): super().__init__() self.embed_dim = embed_dim self.num_heads = num_heads self.head_dim = embed_dim // num_heads assert self.head_dim * num_heads == embed_dim, "embed_dim必须能被num_heads整除" self.W_q = nn.Linear(embed_dim, embed_dim) self.W_k = nn.Linear(embed_dim, embed_dim) self.W_v = nn.Linear(embed_dim, embed_dim) self.out_proj = nn.Linear(embed_dim, embed_dim) def forward(self, x, mask=None): batch_size, seq_len, _ = x.shape Q = self.W_q(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) K = self.W_k(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) V = self.W_v(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) # 缩放点积注意力,除以sqrt(d_k)防止softmax梯度消失 scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim) if mask is not None: scores = scores.masked_fill(mask == 0, -1e9) attn_weights = torch.softmax(scores, dim=-1) out = torch.matmul(attn_weights, V) out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, self.embed_dim) return self.out_proj(out)

这个实现里最值得琢磨的是为什么要除以sqrt(head_dim)。解释起来并不复杂:当维度增大时,点积的结果数值会变大,导致softmax输出趋于极端(接近0或1),梯度会变得非常小。除以sqrt(d)相当于把数值拉回到一个温和的范围。这种级别的理解,靠"调包"是永远学不到的。

2.3 训练稳定的关键参数与显存规划

训练阶段是新手最容易心态爆炸的环节。loss不降、loss爆成NaN、显存不够、训练速度太慢,每个问题都够喝一壶的。这里我先讲几个最核心的参数,再讲一个我经常用来规划资源的公式。

学习率恐怕是影响训练结果最重要的超参数。我过去的经验是:学习率太小loss下降太慢,学习率太大loss会震荡甚至在早期就发散。比较通用的做法是用warmup策略,前若干步把学习率从很小的值逐步升到目标值,之后再按余弦曲线衰减。这个问题在Transformer的训练里尤其明显。

Batch size的选择不只是影响精度,还直接影响显存和训练速度。在显存允许的范围内,更大的batch size通常能带来更稳定的梯度,但也不是越大越好,过大时模型容易收敛到泛化能力较差的局部最优。这里我提供一个显存粗算的思路:训练时的显存占用大约是参数量的12到20倍(以字节计)。也就是说,一个7B参数的模型做全量微调,按16倍估算差不多需要112GB显存。这个估算在选机器、估成本时非常管用。

如果你是个人学习者,没有多卡A100的预算,也不用太焦虑。学习的早期阶段完全可以在小模型上完成,理解原理远比"训练一个大模型"重要。等你需要跑7B甚至更大的模型时,用LoRA这类参数高效微调方法可以大幅降低显存需求,这是工程上非常实用的技能。

3. 两条从零到一的最小实战项目

3.1 用纯Python训练一个字符级语言模型

理论知识学完,光看不练约等于白学。我给新手推荐两个实战项目,第一个是用纯NumPy或纯PyTorch实现一个字符级语言模型,让模型学会"写"一段风格类似的文本。

这个项目为什么好?因为它用小数据量就能看到反馈,而且数据不需要额外标注。你可以用莎士比亚的十四行诗、红楼梦的片段、或者你喜欢的任何中文文本作为训练语料。核心是让模型根据前面的字符预测下一个字符,本质是一个字符级别的分类任务。当你看它从输出完全无意义的字符,到慢慢学会输出有语法结构的文本,那种成就感会给你坚持下去的动力。

具体实现路径分四步。第一,字符到token的映射,构建字典;第二,用embedding把token变成向量;第三,搭一个小的LSTM或Transformer模型,输入前面的字符序列,预测下一个字符的概率分布;第四,训练时用交叉熵损失,生成时用temperature参数控制随机性。每一步都不复杂,但每一环都会用到前面学到的知识。

3.2 做一个能跑的RAG问答系统

第二个项目我建议做RAG(检索增强生成),这个方向在当前的AI工程实践里非常火热。核心思想很简单:当你要让大模型回答问题时,先从一个知识库里检索相关的文本片段,把它们作为上下文一起喂给模型,让模型的回答基于检索到的真实信息而不是凭空编造。

原理不复杂,但做起来细节极其多。第一个坑就是文档切分。我在刚做RAG时,习惯把整篇文章丢给切分器,结果检索出的片段内容非常割裂,回答质量也很差。后来我总结出一套经验:切分粒度要结合业务场景。如果是做规章制度问答,500到800字一段比较合适;如果是长篇小说知识问答,可能得放到1000字以上。切分时尽量按段落或语义边界来,避免硬切。

第二个坑是embedding模型的选择。市面上的embedding模型参差不齐,好的和差的检索效果能差出一大截。选模型时不能光看排行榜分数,要拿自己的业务数据实测。第三个坑是检索结果的排序和后处理。实测下来,只用向量相似度检索的召回效果往往不够好,比较稳的方案是"向量检索+BM25关键词检索"的混合模式,再用一个rerank模型对召回结果精排。加上rerank环节后,我的RAG系统答案准确率大概提升了20%左右,这个提升非常可观。

评估一个RAG系统不能只看最后答案好不好,要分环节拆解:检索阶段看召回率,生成阶段看答案的忠实度和完整性。这样当最终效果不佳时,你能定位到底是"没检出来"还是"模型没用对",而不是无头苍蝇一样乱调。

3.3 工程设施要花多少钱:算力选择的实操建议

很多自学AI工程的人都会卡在算力这个问题上,总觉得"没有A100怎么搞AI"。我的看法是:学习阶段完全不需要大卡。你在本地用一张消费级显卡(比如几年前的RTX 3060级别)就足够跑通绝大多数教学项目,7B以下的开源模型用LoRA也能在24GB显存的卡上玩得转。

如果你的学习已经进入需要跑较大模型的阶段,我再给一个性价比建议:优先考虑云GPU按需租用,而不是直接买硬件。因为训练任务往往是"短时高消耗"的,你租一台配置拉满的机器跑48小时,然后释放,成本远低于买一张同级别显卡。不少云厂商都提供按小时计费的GPU实例,实测下来非常灵活。真正需要长期跑推理服务的场景,才值得考虑包月或买卡。

训练时的工程技巧同样重要。混合精度训练(FP16/BF16)能显著降低显存占用和计算开销,这在7B以上模型的训练里几乎是标配。梯度累积可以让你在batch size很小的情况下模拟出大batch的效果。我经常把这套组合用在本地小卡上跑微调,效果立竿见影。

4. 从实验到上线的工程化:部署与迭代的实战细节

4.1 模型导出与推理优化:不同场景怎么选

模型训练完了只是万里长征走完了一半,另一半是让它稳定地在线上服务。我在不同的项目里用过三种部署方案,各有各的适用场景。

第一种是直接用PyTorch的TorchServe或HuggingFace的TGI框架部署,优点是集成度高、上手快,适合快速原型验证。第二种是用ONNX导出模型后,用ONNX Runtime或TensorRT加速推理。这在延迟敏感的场景特别有用。我做过一个文本分类服务,原生的PyTorch推理延迟在30毫秒左右,导出到ONNX并开启FP16量化后,延迟降到了8毫秒,服务吞吐翻了快两倍。第三种是vLLM这类专门为LLM推理优化的框架。如果你要部署一个大模型在线服务,vLLM的PagedAttention技术能大幅提升显存利用率。实测在同样显存下单卡吞吐差不多能提升5到10倍,这个差距已经能直接影响成本了。

选型的核心逻辑很简单:看你的瓶颈是什么。如果是延迟,优化推理引擎;如果是吞吐,做并发和批处理优化;如果是成本,考虑量化和模型蒸馏。没有一套方案通吃所有场景。

4.2 评估不是"测一下准确性"那么简单

模型上线之前的评估环节,很多团队做得非常草率。跑一遍测试集,出一个准确率或者F1分数,看一眼差不多就部署了。这在真实工程里是很危险的。我在做AI工程时有一套自己的评估体系,分三个层次。

第一层是离线指标评估,包括准确率、召回率、F1等。这一层告诉你模型在静态测试集上的表现。第二层是分场景评估,不只看总体准确率,还要按数据子集拆开看。比如一个客服意图识别系统,你把"退款"和"投诉"这两个意图单独拎出来看准确率,往往会发现总体指标被简单意图拉高了,难样本的表现其实很差。第三层是badcase驱动迭代。我会专门收集线上预测失败的case,逐个分析错误原因,判断是数据标注问题、特征缺失还是模型理解能力不足。这种做法看起来费时间,但实际上是模型效果提升的最快路径。

这里多说一句数据回流。很多AI项目上线后效果下滑,根源在于线上数据分布一直在变化,而模型没有及时用新数据做更新。稳定的AI系统必须有一个数据回流机制:线上积累的新样本经过清洗和人工标注后,定期加入训练集进行增量训练。这个机制谁先做谁受益,越早做效果越稳。

5. 避坑指南:我踩过的环境、数据与训练大坑

5.1 环境配置的经典连环坑

AI工程的环境配置问题,是我这些年见过劝退新人最多的环节。很多人好不容易把代码写完了,结果在装依赖这一步就卡了两天,心态直接崩了。

几个高频坑值得提前预防。Python版本和PyTorch版本不匹配,这是最常见的。PyTorch新版本对Python版本有明确要求,PyTorch 2.x系列通常要求Python 3.8及以上,某些新特性甚至需要3.10以上。所以装PyTorch之前一定要先查官方文档的版本对应表。第二个常见坑是CUDA版本给环境配错了,导致装了GPU版推理性能反而更差,那多半是CUDA Toolkit和PyTorch对应的CUDA版本不一致。GPU驱动用的CUDA版本,和PyTorch编译时链接的CUDA版本,可以不一样,PyTorch自带运行库。这个认知能帮你排查掉一半的CUDA报错问题。

虚拟环境一定是必需品。我在团队里要求所有项目必须建独立的conda环境或venv环境,哪怕是一个小demo。因为AI工程涉及的依赖太复杂,不同的项目对同一依赖的版本要求经常冲突,没有虚拟环境隔离,你迟早会被"解决了一个依赖,又破坏了另一个项目"的问题逼疯。

5.2 数据侧的隐蔽陷阱

数据是整个AI工程里最不能马虎的部分。我在这里分享几个我实际踩过或见过的数据坑,希望你能绕开。

标签错位是隐蔽性最强的数据问题。我见过一个项目,训练集的标签因为Excel排序操作整体错位了一行,模型训练出来效果奇差无比,排查了整整两天才发现是数据对齐问题。这个问题的教训是:代码里写数据加载逻辑时,一定不要手动操作排序、筛选,一切以代码为准。

数据泄漏也是个很有意思的问题。如果你在数据预处理阶段用了全量数据的统计量(比如用全量数据的均值和标准差做标准化),然后在划分训练测试集之后评估,看起来测试集指标很好,但其实已经泄漏了,上新数据时效果会大幅下滑。正确做法是只用训练集的统计量来做标准化,再应用到测试集。

还有一个几乎所有新手都会犯的错:排序导致的时间泄漏。你在做时序预测时,如果数据的切分方式没有保证训练集在时间上早于测试集,模型就等于"偷看了未来"。训练时指标漂亮,上线后一团糟。这个教训我深有体会,做时间序列的AI工程一定不能随机划分数据。

5.3 常见问题速查表

我把实操中最常遇到问题的排查思路整理成了一张速查表,建议收藏备用。

问题现象优先排查方向处理建议
loss不降学习率设置尝试降低学习率或加入warmup
loss变成NaN学习率过大或梯度爆炸降低学习率,开启梯度裁剪
训练速度极慢数据加载瓶颈检查dataloader的num_workers设置
显存不足batch size过大减小batch size,使用梯度累积
训练集指标好、测试集差过拟合增加数据量、加入正则化、做数据增强
上线后效果大跌训练测试分布不一致检查数据泄漏,引入新数据回流
推理延迟过高模型过大或推理引擎不匹配考虑ONNX导出或量化加速

这张表不会解决所有问题,但能在你遇到这类问题时快速给出起点,避免无头苍蝇式排查。

回到开头那个话题,"ai-engineering-from-scratch"这条路走下来确实比"跟着教程调包"慢很多,但慢得有价值。我个人在实际带人和自学的过程中最大的感受是:知识体系这东西,拼图是自己一块一块拼出来的才牢固,别人递给你一整幅拼图,你看着好看,却说不清每一块为什么在那里。所以我的建议很朴素:从今天开始,把第一个全连接网络用NumPy手写出来,然后一路写下去。别急着跑大模型,先把手底下的每一步都走稳,后面自然就平了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询