从零搭建AI工程体系:手写神经网络与训练循环实战
发布时间:2026/10/2 12:59:54来源:尧图网络
1. 从零搭建AI工程体系为什么我劝你别一上来就调包“ai-engineering-from-scratch”这个标题第一次看到的时候我愣了一下。市面上讲AI的教程铺天盖地但绝大多数都是教你import torch然后跑一个预训练模型或者调个API接口就完事。真正讲“从零开始搭建AI工程体系”的内容少得可怜。我自己在这个行业摸爬滚打了十来年带过团队做过从0到1的项目也接手过别人留下的烂摊子。说实话我见过太多人——包括几年前的我——一上来就急着装框架、下模型、跑demo结果遇到一个维度不匹配的报错就卡半天完全不知道从哪查起。这种感觉就像你想学开车直接坐进驾驶座就踩油门连方向盘和刹车在哪都没搞清楚。这个项目标题的核心其实不是教你“怎么用AI”而是教你“AI系统是怎么运转起来的”。它面向的是那些不满足于当调包侠、想真正理解底层逻辑的开发者。你需要从最基础的数据处理开始一步步搭建起一个完整的工程链路数据加载、预处理、模型定义、训练循环、评估、部署。每一步都要自己动手写而不是靠框架帮你隐藏细节。我个人的经验是走完这一整套流程之后你再回头看那些高级框架会有一种“原来你帮我做了这些事”的顿悟感。这种理解深度是看一百篇教程都换不来的。所以这篇文章我想把这条从零搭建的路子完整拆解一遍包括每一步为什么这么做、我当时踩过哪些坑、有哪些可以直接抄作业的代码结构。2. 整体设计思路为什么选择“手写优先”的路线2.1 核心思路先理解再抽象从零搭建AI工程体系最核心的设计原则就是手写优先。什么意思就是能用原生代码实现的功能先不要用框架的高级封装。比如矩阵乘法你先用嵌套循环写一遍理解计算过程再去用numpy.dot。比如梯度下降你先手动推导公式、手动更新参数再去用自动求导。这个思路背后的逻辑很简单框架是别人抽象出来的你不理解抽象层下面的东西出了问题就只能靠猜。我见过太多人遇到loss不下降就疯狂调学习率但其实问题可能出在数据归一化没做对或者标签编码搞反了。如果你自己手写过一遍训练循环这些环节你都有感知排查起来就是几分钟的事。当然手写优先不等于永远不用框架。正确的节奏是先手写理解原理再用框架提升效率。就像学编程先学汇编再学高级语言你会对内存管理、指针这些概念有深刻理解写高级语言时也更清楚哪些操作有性能代价。2.2 技术选型为什么是Python NumPy整个项目的基础工具链我建议用Python NumPy起步。原因有几个第一Python的语法足够简单你可以把精力集中在算法逻辑上而不是跟语言特性搏斗。第二NumPy提供了高效的数组操作但它的API设计很接近底层数学运算不会像PyTorch那样隐藏太多细节。第三生态成熟遇到问题容易找到参考资料。具体来说你需要准备的环境包括Python 3.9以上版本3.11的语法更简洁但3.9兼容性更好NumPy用于矩阵运算和数值计算Matplotlib用于可视化训练过程和结果Jupyter Notebook用于交互式开发和调试注意不要一开始就装PyTorch或TensorFlow。等你手写完成一个完整的训练流程之后再去用这些框架你会发现你对它们的理解完全不一样。2.3 项目结构设计模块化思维从零搭建不代表把所有代码堆在一个文件里。相反模块化设计是从一开始就要养成的习惯。我建议的项目结构是这样的ai-from-scratch/ ├── data/ │ ├── loader.py # 数据加载 │ └── preprocess.py # 数据预处理 ├── model/ │ ├── layers.py # 网络层定义 │ ├── loss.py # 损失函数 │ └── network.py # 网络组装 ├── train/ │ ├── optimizer.py # 优化器 │ └── loop.py # 训练循环 ├── eval/ │ └── metrics.py # 评估指标 └── utils/ └── visualizer.py # 可视化工具这个结构的好处是每个模块职责单一方便单独测试和替换。比如你想换一个损失函数只需要改loss.py不用动训练循环的代码。这种解耦思维在实际工程项目中非常重要。3. 核心细节解析从数据到模型的每一步3.1 数据加载与预处理最容易被忽视的关键环节很多人觉得数据加载很简单不就是读个CSV吗但实际操作中数据环节出的问题占了整个项目Bug的一半以上。我踩过的坑包括编码格式不对导致中文乱码、缺失值处理方式不当导致模型学到错误模式、特征缩放没做导致梯度爆炸。从零搭建的话我建议你先用纯Python实现一个简单的数据加载器。比如处理CSV文件import csv import numpy as np def load_csv(filepath): features [] labels [] with open(filepath, r, encodingutf-8) as f: reader csv.reader(f) header next(reader) # 跳过表头 for row in reader: features.append([float(x) for x in row[:-1]]) labels.append(float(row[-1])) return np.array(features), np.array(labels)这段代码看起来简单但有几个细节值得注意。encodingutf-8是必须的不然遇到中文就崩。next(reader)跳过表头避免把列名当成数据。列表推导式里用float()转换确保数据类型正确。预处理环节标准化Standardization和归一化Normalization是两个必须掌握的操作。标准化是把数据变成均值为0、标准差为1的分布公式是(x - mean) / std。归一化是把数据缩放到[0,1]区间公式是(x - min) / (max - min)。为什么需要这些操作因为不同特征的量纲差异会导致梯度下降走“之”字形路线收敛极慢。举个例子一个特征范围是0到1另一个是0到10000那么损失函数的等高线会变成一个极其扁的椭圆梯度下降会在长轴方向来回震荡。def standardize(X): mean np.mean(X, axis0) std np.std(X, axis0) std[std 0] 1 # 防止除零 return (X - mean) / std, mean, std实操心得标准化参数mean和std必须从训练集计算然后应用到验证集和测试集。如果从整个数据集计算会造成数据泄露模型评估结果会虚高。3.2 模型定义从单个神经元到多层网络从零搭建模型我建议从单个神经元开始。一个神经元做的事情很简单接收输入做加权求和加上偏置然后通过激活函数输出。class Neuron: def __init__(self, n_inputs): self.weights np.random.randn(n_inputs) * 0.01 self.bias 0.0 def forward(self, x): z np.dot(self.weights, x) self.bias return self.activation(z) def activation(self, z): return 1 / (1 np.exp(-z)) # Sigmoid这里的np.random.randn(n_inputs) * 0.01是权重初始化。为什么要乘以0.01因为如果权重太大会导致激活函数饱和梯度接近0学习停滞。乘以一个小系数可以让初始输出接近线性区域梯度更健康。然后你可以把多个神经元组合成一层再把多层堆叠成网络。前向传播Forward Propagation就是数据从输入层经过隐藏层到输出层的过程。每一层的输出是下一层的输入。class Layer: def __init__(self, n_inputs, n_neurons): self.neurons [Neuron(n_inputs) for _ in range(n_neurons)] def forward(self, x): return np.array([n.forward(x) for n in self.neurons])反向传播Backpropagation是从零搭建中最难理解的部分。它的核心是链式法则损失函数对某个权重的梯度等于损失对输出的梯度乘以输出对权重的梯度。我建议你先在一张纸上推导两层网络的梯度公式然后再写代码。def backward(self, x, grad_output, learning_rate): # grad_output是损失对本层输出的梯度 grad_z grad_output * self.sigmoid_derivative(self.last_z) grad_weights np.outer(grad_z, x) grad_bias grad_z # 更新参数 self.weights - learning_rate * grad_weights self.bias - learning_rate * grad_bias # 返回传递给上一层的梯度 return np.dot(self.weights.T, grad_z)注意反向传播的梯度计算顺序必须和前向传播相反。前向是从输入到输出反向是从输出到输入。这个顺序搞反了梯度就全错了。3.3 损失函数衡量模型好坏的标准损失函数是模型优化的目标。回归问题用均方误差MSE分类问题用交叉熵Cross-Entropy。这两个是最基础的必须手写一遍。均方误差的公式是MSE (1/n) * sum((y_pred - y_true)^2)。它的梯度是2/n * (y_pred - y_true)。交叉熵的公式是CE -sum(y_true * log(y_pred))配合Softmax激活函数使用时梯度简化为y_pred - y_true非常优雅。def mse_loss(y_pred, y_true): return np.mean((y_pred - y_true) ** 2) def mse_grad(y_pred, y_true): return 2 * (y_pred - y_true) / y_true.shape[0] def cross_entropy_loss(y_pred, y_true): epsilon 1e-12 # 防止log(0) y_pred np.clip(y_pred, epsilon, 1 - epsilon) return -np.mean(y_true * np.log(y_pred))np.clip那行很重要。如果模型输出恰好是0或1log(0)会变成负无穷导致NaN。加上一个极小值可以避免这个问题。3.4 优化器让模型真正“学习”起来最基础的优化器是随机梯度下降SGD。它的更新规则是w w - learning_rate * gradient。学习率是最重要的超参数太大导致震荡不收敛太小导致收敛太慢。我建议你从零实现三种优化器对比它们的效果优化器更新规则优点缺点SGDw - lr * g简单内存占用小收敛慢容易陷入局部最优Momentumv betav g; w - lrv加速收敛减少震荡需要调betaAdam结合动量和自适应学习率收敛快对学习率不敏感内存占用大可能过拟合class SGD: def __init__(self, learning_rate0.01): self.lr learning_rate def update(self, params, grads): for param, grad in zip(params, grads): param - self.lr * grad class Momentum: def __init__(self, learning_rate0.01, beta0.9): self.lr learning_rate self.beta beta self.velocity None def update(self, params, grads): if self.velocity is None: self.velocity [np.zeros_like(p) for p in params] for i, (param, grad) in enumerate(zip(params, grads)): self.velocity[i] self.beta * self.velocity[i] grad param - self.lr * self.velocity[i]实操心得Adam优化器的默认学习率0.001在大多数情况下都能工作但如果你发现loss震荡厉害先试试降到0.0001。如果收敛太慢试试升到0.01。不要一上来就调网络结构。4. 实操过程完整搭建一个手写数字识别系统4.1 数据准备与加载我们以经典的MNIST手写数字数据集为例。这个数据集包含60000张训练图片和10000张测试图片每张是28x28的灰度图。从零搭建的话你需要自己解析数据格式。MNIST的原始格式是二进制文件前16个字节是文件头信息之后每784个字节是一张图片再之后是标签。解析代码如下import struct def load_mnist_images(filepath): with open(filepath, rb) as f: magic, num, rows, cols struct.unpack(IIII, f.read(16)) images np.frombuffer(f.read(), dtypenp.uint8) images images.reshape(num, rows * cols) return images.astype(np.float32) / 255.0 def load_mnist_labels(filepath): with open(filepath, rb) as f: magic, num struct.unpack(II, f.read(8)) labels np.frombuffer(f.read(), dtypenp.uint8) return labelsstruct.unpack(IIII, ...)中的表示大端字节序I表示无符号整数。这是MNIST文件的标准格式。除以255是为了把像素值归一化到[0,1]区间。标签需要转换成独热编码One-Hot Encoding因为交叉熵损失函数需要这种格式。比如数字3的独热编码是[0,0,0,1,0,0,0,0,0,0]。def one_hot(labels, num_classes10): return np.eye(num_classes)[labels]np.eye(10)生成一个10x10的单位矩阵然后用标签作为索引去取对应的行就得到了独热编码。这个技巧很常用比循环快得多。4.2 网络结构设计与初始化我设计的网络结构是输入层784个神经元对应28x28像素一个隐藏层128个神经元输出层10个神经元对应0-9十个类别。激活函数隐藏层用ReLU输出层用Softmax。class NeuralNetwork: def __init__(self, layer_sizes): self.weights [] self.biases [] for i in range(len(layer_sizes) - 1): w np.random.randn(layer_sizes[i], layer_sizes[i1]) * np.sqrt(2.0 / layer_sizes[i]) b np.zeros((1, layer_sizes[i1])) self.weights.append(w) self.biases.append(b)权重初始化用了He初始化即乘以sqrt(2/n_inputs)。这是专门为ReLU激活函数设计的可以保持每一层输出的方差稳定避免梯度消失或爆炸。如果用Sigmoid激活函数应该用Xavier初始化即sqrt(1/n_inputs)。4.3 前向传播与反向传播实现前向传播就是逐层做矩阵乘法和激活def forward(self, X): self.activations [X] self.z_values [] for i in range(len(self.weights) - 1): z np.dot(self.activations[-1], self.weights[i]) self.biases[i] self.z_values.append(z) a np.maximum(0, z) # ReLU self.activations.append(a) # 输出层用Softmax z np.dot(self.activations[-1], self.weights[-1]) self.biases[-1] self.z_values.append(z) a self.softmax(z) self.activations.append(a) return a def softmax(self, z): exp_z np.exp(z - np.max(z, axis1, keepdimsTrue)) return exp_z / np.sum(exp_z, axis1, keepdimsTrue)np.max(z, axis1, keepdimsTrue)是为了数值稳定性。如果z的值很大exp(z)会溢出变成无穷大。减去最大值之后最大的指数变成exp(0)1不会溢出。反向传播是核心难点我把它拆成几个步骤def backward(self, X, y_true, learning_rate): m X.shape[0] # 输出层梯度Softmax 交叉熵的梯度简化为 y_pred - y_true delta self.activations[-1] - y_true for i in range(len(self.weights) - 1, -1, -1): grad_w np.dot(self.activations[i].T, delta) / m grad_b np.sum(delta, axis0, keepdimsTrue) / m if i 0: delta np.dot(delta, self.weights[i].T) delta[self.z_values[i-1] 0] 0 # ReLU的导数 self.weights[i] - learning_rate * grad_w self.biases[i] - learning_rate * grad_b这里的关键是delta[self.z_values[i-1] 0] 0。ReLU的导数是输入大于0时为1小于等于0时为0。所以反向传播时把z值小于等于0的位置的梯度置零。4.4 训练循环与超参数设置训练循环就是把前向传播、损失计算、反向传播、参数更新串起来def train(model, X_train, y_train, X_val, y_val, epochs50, lr0.1, batch_size64): n_samples X_train.shape[0] losses [] for epoch in range(epochs): # 打乱数据 indices np.random.permutation(n_samples) X_shuffled X_train[indices] y_shuffled y_train[indices] epoch_loss 0 for i in range(0, n_samples, batch_size): X_batch X_shuffled[i:ibatch_size] y_batch y_shuffled[i:ibatch_size] y_pred model.forward(X_batch) loss cross_entropy_loss(y_pred, y_batch) epoch_loss loss model.backward(X_batch, y_batch, lr) avg_loss epoch_loss / (n_samples // batch_size) losses.append(avg_loss) # 验证集评估 val_pred model.forward(X_val) val_acc np.mean(np.argmax(val_pred, axis1) np.argmax(y_val, axis1)) if epoch % 10 0: print(fEpoch {epoch}, Loss: {avg_loss:.4f}, Val Acc: {val_acc:.4f}) return losses超参数设置方面我实测下来比较稳的组合是学习率0.1配合SGD、batch size 64、训练50轮。学习率0.1看起来很大但因为用了He初始化和ReLU梯度比较健康大学习率反而收敛快。如果loss出现NaN先把学习率降到0.01试试。4.5 评估与可视化训练完成后在测试集上评估def evaluate(model, X_test, y_test): y_pred model.forward(X_test) predictions np.argmax(y_pred, axis1) true_labels np.argmax(y_test, axis1) accuracy np.mean(predictions true_labels) # 混淆矩阵 confusion np.zeros((10, 10), dtypeint) for t, p in zip(true_labels, predictions): confusion[t][p] 1 return accuracy, confusion可视化训练损失曲线可以直观判断模型是否过拟合或欠拟合import matplotlib.pyplot as plt plt.plot(losses) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training Loss Curve) plt.show()如果训练loss持续下降但验证loss开始上升说明过拟合了。解决办法是加Dropout层、L2正则化或者减少网络参数量。如果训练loss都不下降说明欠拟合需要增加网络容量或调整学习率。5. 常见问题与排查技巧实录5.1 梯度消失与梯度爆炸这是从零搭建时最常遇到的问题。梯度消失表现为靠近输入层的权重几乎不更新loss下降极慢。梯度爆炸表现为loss变成NaN或者权重数值变得极大。排查方法在反向传播时打印每一层梯度的范数。如果某一层的梯度范数接近0就是梯度消失如果超过1000就是梯度爆炸。解决方案问题原因解决方案梯度消失Sigmoid/Tanh饱和深层网络换ReLU用He初始化加BatchNorm梯度爆炸学习率太大权重初始化太大梯度裁剪降低学习率减小初始化方差梯度裁剪的代码很简单def clip_gradients(grads, max_norm5.0): total_norm np.sqrt(sum(np.sum(g**2) for g in grads)) if total_norm max_norm: scale max_norm / total_norm grads [g * scale for g in grads] return grads5.2 损失函数不下降的排查思路Loss不下降的原因有很多我按照排查优先级列一个清单检查数据标签是否对应正确。我遇到过标签和图片错位的情况模型学了半天全是噪声。检查学习率。太大导致震荡太小导致停滞。试试0.001、0.01、0.1三个值。检查数据预处理。有没有做标准化有没有把像素值归一化到[0,1]检查激活函数和损失函数是否匹配。输出层用Sigmoid但损失用交叉熵梯度公式就不对了。检查权重初始化。全零初始化会导致所有神经元学到相同的东西必须随机初始化。避坑技巧在训练循环里加一行print(np.mean(np.abs(grad_w)))如果梯度值在1e-7以下基本可以确定是梯度消失。5.3 过拟合与欠拟合的判断与处理过拟合训练准确率很高比如99%但验证准确率低比如85%。模型记住了训练数据的噪声泛化能力差。欠拟合训练准确率和验证准确率都低比如70%。模型容量不够或者训练不充分。处理过拟合的手段增加训练数据最有效加Dropout层随机丢弃部分神经元L2正则化在损失函数里加上权重平方和早停验证loss开始上升就停止训练处理欠拟合的手段增加网络层数或每层神经元数量训练更长时间减小正则化强度Dropout的实现def dropout_forward(x, drop_rate0.5, trainingTrue): if not training: return x mask np.random.binomial(1, 1 - drop_rate, sizex.shape) / (1 - drop_rate) return x * mask注意除以(1 - drop_rate)是为了保持输出的期望值不变。训练时随机丢弃推理时使用完整网络。5.4 数值稳定性问题从零实现时数值稳定性问题特别常见。除了前面提到的Softmax减最大值、log加epsilon还有几个地方需要注意计算方差时如果数据量很大sum(x^2)可能溢出。可以用np.var代替手动计算。矩阵乘法时如果维度不匹配NumPy会报错。养成打印shape的习惯。学习率乘以梯度时如果梯度是NaN参数会变成NaN。可以在更新前加np.nan_to_num。grad_w np.nan_to_num(grad_w, nan0.0, posinf1.0, neginf-1.0)这个操作会把NaN替换成0把无穷大替换成有限值防止污染整个参数矩阵。6. 从手写实现到工程化部署的过渡6.1 代码重构与性能优化手写版本跑通之后下一步是重构。我建议做这几件事第一向量化。把循环操作改成矩阵运算。比如计算所有样本的损失不要用for循环直接用np.mean。第二批处理。把数据分成小批次利用矩阵运算的并行性。第三缓存中间结果。前向传播的激活值在反向传播时要用不要重复计算。重构后的代码训练速度通常能提升5到10倍。我实测过一个三层网络纯循环版本跑一个epoch要30秒向量化之后只要3秒。6.2 模型保存与加载训练好的模型需要保存下来不然每次都要重新训练。最简单的保存方式是np.savezdef save_model(model, filepath): np.savez(filepath, weightsmodel.weights, biasesmodel.biases) def load_model(filepath): data np.load(filepath, allow_pickleTrue) model NeuralNetwork([784, 128, 10]) model.weights list(data[weights]) model.biases list(data[biases]) return modelallow_pickleTrue是为了支持对象数组的加载。如果只是数值数组可以不加这个参数。6.3 从NumPy到PyTorch的迁移思路手写版本理解透了之后迁移到PyTorch就是水到渠成的事。你会发现model.forward()对应model(x)model.backward()对应loss.backward()optimizer.update()对应optimizer.step()手写的Layer类对应nn.Linear手写的ReLU对应nn.ReLU迁移的时候重点对比两者的差异。比如PyTorch的自动求导帮你做了反向传播但你需要理解它底层是怎么实现的。PyTorch的优化器帮你做了参数更新但你需要知道SGD和Adam的区别在哪里。我个人体会手写一遍之后再看PyTorch的文档很多之前看不懂的参数和设计突然就明白了。比如为什么optimizer.zero_grad()是必须的因为PyTorch默认会累积梯度不清零的话梯度会越加越大。6.4 工程化部署的注意事项模型训练好之后要部署成服务。从零搭建的模型部署起来反而简单因为依赖少。你可以用Flask写一个简单的APIfrom flask import Flask, request, jsonify import numpy as np app Flask(__name__) model load_model(model.npz) app.route(/predict, methods[POST]) def predict(): data request.json[image] # 784维数组 x np.array(data).reshape(1, -1) / 255.0 y_pred model.forward(x) digit int(np.argmax(y_pred)) confidence float(np.max(y_pred)) return jsonify({digit: digit, confidence: confidence})部署时要注意输入数据的预处理必须和训练时一致。训练时做了标准化推理时也要做。训练时像素除以255推理时也要除。这个不一致是线上事故的高发区。7. 我踩过的坑与实操心得7.1 数据泄露最隐蔽的Bug数据泄露是指训练时用到了测试集的信息。最常见的场景是在划分训练集和测试集之前就对整个数据集做了标准化。这样测试集的均值和方差信息“泄露”到了训练过程中导致评估结果虚高。正确的做法是先划分数据集然后只用训练集的均值和方差去标准化训练集、验证集和测试集。# 错误做法 X_normalized (X - X.mean()) / X.std() X_train, X_test split(X_normalized) # 正确做法 X_train, X_test split(X) mean, std X_train.mean(), X_train.std() X_train (X_train - mean) / std X_test (X_test - mean) / std这个坑我踩过不止一次每次都是模型在测试集上表现好得离谱仔细一查才发现是泄露了。7.2 学习率调参的经验法则学习率是最重要的超参数没有之一。我的经验法则是先用一个较大的学习率比如0.1跑几个epoch观察loss曲线。如果loss震荡或变成NaN除以10。如果loss下降太慢乘以10。找到让loss稳定下降的最大学习率然后除以2作为最终值。还有一个技巧是学习率预热Warmup前几个epoch用很小的学习率然后逐渐增大。这样可以避免训练初期梯度不稳定导致模型跑偏。7.3 随机种子的重要性从零搭建的项目如果每次运行结果都不一样很难判断改动是否有效。设置随机种子可以保证结果可复现np.random.seed(42)42这个数字没什么特殊含义只是习惯用法。关键是每次实验都用同一个种子这样对比不同超参数的效果时差异才可信。7.4 小批量大小的选择Batch size影响训练速度和稳定性。太小比如1导致梯度噪声大训练不稳定。太大比如整个数据集导致内存不够且容易陷入局部最优。我实测下来64到256之间是比较好的范围。如果显存或内存不够用梯度累积模拟大batchaccumulation_steps 4 for i, (X_batch, y_batch) in enumerate(data_loader): loss compute_loss(X_batch, y_batch) loss loss / accumulation_steps model.backward(loss) if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()这样等效于batch size扩大了4倍但内存占用不变。7.5 调试工具与技巧从零搭建时没有现成的调试工具需要自己造。我常用的几个技巧第一梯度检查。用数值近似计算梯度和反向传播的梯度对比。如果差异很大说明反向传播实现有Bug。def gradient_check(model, X, y, epsilon1e-7): # 数值梯度 for i in range(len(model.weights)): for j in range(model.weights[i].shape[0]): for k in range(model.weights[i].shape[1]): original model.weights[i][j, k] model.weights[i][j, k] original epsilon loss_plus cross_entropy_loss(model.forward(X), y) model.weights[i][j, k] original - epsilon loss_minus cross_entropy_loss(model.forward(X), y) numerical_grad (loss_plus - loss_minus) / (2 * epsilon) model.weights[i][j, k] original # 对比numerical_grad和反向传播计算的梯度第二单元测试。每个模块单独测试。比如测试ReLU函数输入负数输出0输入正数输出原值。测试Softmax输出之和为1。第三可视化中间结果。把第一层的权重画成图像可以看到模型学到了什么模式。如果权重看起来像随机噪声说明模型没学好。8. 后续扩展方向与进阶路线走完从零搭建的完整流程之后你可以往几个方向深入。第一个方向是性能优化学习如何用BLAS库加速矩阵运算如何用多线程或GPU并行化训练。第二个方向是架构探索手写卷积层、循环层、注意力机制理解CNN、RNN、Transformer的核心原理。第三个方向是工程化学习如何把模型打包成Docker镜像如何做A/B测试如何监控线上模型的性能衰减。我个人觉得最有价值的是第二个方向。因为现在的大模型本质上就是注意力机制的堆叠你手写一遍注意力机制再去看Transformer的论文会发现一切都是那么自然。这种从底层到顶层的贯通感是单纯调包永远体会不到的。最后分享一个小技巧把你手写的代码和PyTorch的源码对照着看。PyTorch的nn.Linear实现其实就是一个矩阵乘法加偏置nn.ReLU就是一个torch.clamp(x, min0)。你会发现那些看起来很神秘的框架底层逻辑和你手写的版本一模一样只是做了更多的工程优化。这种“祛魅”的过程是从零搭建AI工程体系最大的收获。
网站建设高端定制企业官网