“深度学习01”——很多人看到这个编号第一反应是教材目录或者网课列表里的某一讲。但我更愿意把它看作一个动手项目用最底层的方式亲手做第一个能自主学习的神经网络。这几年大模型很火聊天、画图、写代码仿佛只要调接口就能拥有一切。可回到根上深度学习解决的核心问题其实特别朴素扔给计算机一堆数据让它自己找规律然后用找到的规律去预测从没见过的新样本。这篇博文就是带你把“找规律”这件事从原理到代码完整走一遍。这个系列第一篇我不打算让你去装任何重型框架不调高级封装的接口只用一个脚本手写一个迷你神经网络。它能做什么它能学会区分两类数据并且在训练完成后对没见过的数据做出预测。适合谁零基础想入门AI的新手——尤其是那些已经调过接口、但完全不清楚内部发生了什么的人。顺便说一句哪怕你以后要投身大模型方向底层那套“前向传播、计算损失、反向传播、更新参数”的流程也半点没变。先把这条主线打通后面什么都顺。1. 神经网络到底在学什么先拆掉“深度学习”的神秘感1.1 一个神经元就是一台小计算器很多人被“神经网络”这个词吓住以为里面有什么高深莫测的生物机制。实际上你可以把每个神经元理解成一台微型计算器它接收一组输入给每个输入配一个权重重要程度把输入乘权重再加一个偏置最后丢进一个激活函数做非线性变换。打个比方。你在调音响音量旋钮就是权重音调旋钮是另一个权重偏置相当于“就算所有旋钮归零也仍然存在的底噪”。模型要学的东西就是找到一组最合适的旋钮位置让最终输出的声音最好听。深度学习里的“深度”就是很多这样的神经元一层接一层串联。每一层把上一层的结果重新加工逐层提取更抽象的信息。第一层可能只能看到像素边缘中层看到纹理高层看到物体轮廓这个类似“层层抽象”的过程就是深度学习在图像、语音、文本上多次奏效的根本原因。1.2 损失函数模型的“错题分数”模型学得好不好得有个量化标准。这个标准就是损失函数。训练时网络拿一批样本做预测把预测结果和真实标签答案比一下算出一个分数——这个分数就是损失英文叫loss。分数越低说明预测越接近真实答案。举个例子你让孩子做口算题每道题错得越离谱扣分越多。损失函数就是那套扣分规则。深度学习的训练过程说白了就是“刷题”一遍遍看样本算分数然后调整参数让分数下降。常见的损失函数有两种。回归问题常用均方误差MSE就是预测值和真实值差的平方分类问题常用交叉熵它衡量的是预测概率分布和真实分布的差异。我在第一个实验里用均方误差因为它求导直观方便新手跟着推导走一遍不会在数学上卡壳。1.3 梯度下降下山才是正经事模型有上万个参数时怎么降低损失答案是梯度下降。梯度是个向量指向损失函数上升最快的方向。我们沿着相反方向走就能让损失下降。你可以想象自己站在山坡上四周起雾看不见路但能感觉到脚下哪边坡度更陡。梯度下降就是朝坡下最陡的方向迈一步到新位置再感觉一次再迈一步直到走到谷底。关键是“迈多大步”——这一步的尺寸就是学习率。学习率太大可能一步跨过谷底在两边来回震荡学习率太小训练半天还在山腰磨蹭。后面我会专门讲这里踩过的坑。2. 准备一个零依赖的深度学习实验台2.1 为什么我劝你别一上来就装框架现在网上很多教程开局就让装某个流行框架拖下来几百兆依赖再跑个“经典数据集”十有八九会在环境配置上栽跟头。而且就算跑通了本质还是“拿着计算器按了一个按钮”大脑里对神经网络的概念依然模糊。我的建议第一个实验零依赖纯标准库。只需要Python不需要任何多余组件。这样有两个好处。第一不安装那些庞然大物你可以在十分钟内搭起实验台。第二所有代码都摊在你面前每一行乘法你都看得懂反向传播的每一环都能用肉眼追踪这对建立直觉太重要了。等你理解了核心原理再去碰现成框架那是升级工具而不是寻找救命稻草。这个顺序千万别反。2.2 环境准备只需一步确保电脑上有Python 3就可以。在终端里敲一下python --version如果能看到版本号这台机器就能跑完本篇全部代码。我建议你创建一个文件夹专门放这个实验。比如叫deep_learning_01把后面所有代码存成一个.py文件。如果你之前没用过Python也不需要去找集成开发环境记事本加命令行足够如果更习惯用编辑器挑个轻量的就行。重点是别在工具上浪费时间代码本身才是主角。2.3 构造数据两个有重叠的簇深度学习离不开数据。为了控制变量我不去下载真实数据集而是用程序造一份玩具数据。这份数据一共200个样本每个样本有两个特征x1、x2标签y是0或1代表两个类别。我把0类放在坐标系原点附近1类放在右上角(2,2)附近同时给两个簇加上随机扰动让它们有一部分区域是重叠的。这样一来简单的“画一条直线”很难完美分开两类神经网络才有表现非线性的空间。为什么要让它们重叠如果两类数据完全分开那一个线性分类器就够了神经网络的优势根本看不出来。重叠区域恰恰是考验模型泛化能力的场景——它必须学会妥协找到最合理的决策边界。顺便说一句这200个样本里会有150个作训练集50个作测试集。训练集像是平时的练习题测试集像是期末考试的卷子。考卷绝对不能提前给学生看否则成绩再漂亮也是假的。下面这段代码生成原始数据import math import random random.seed(42) def gauss(): u1 random.random() while u1 1e-9: u1 random.random() u2 random.random() return math.sqrt(-2 * math.log(u1)) * math.cos(2 * math.pi * u2) def generate_data(n_a100, n_b100): samples [] for _ in range(n_a): x1 gauss() * 1.2 x2 gauss() * 1.2 samples.append((x1, x2, 0)) for _ in range(n_b): x1 gauss() * 1.6 2.0 x2 gauss() * 1.6 2.0 samples.append((x1, x2, 1)) random.shuffle(samples) return samples all_data generate_data() train_data all_data[:150] test_data all_data[150:]看到没完全没用到第三方库只用random生成随机分布用math做开方和三角函数。这里有个小细节random.seed(42)固定了随机种子这样每次运行生成的样本都一样实验结果可以复现和对比。实际项目中随机种子也是个很有用的习惯不是只在学校实验里才用。3. 手写一个能学习的迷你神经网络3.1 网络结构2-4-1这个网络的形状我定成输入层2个节点、隐藏层4个节点、输出层1个节点。输入是(x1, x2)两个特征隐藏层负责把特征做非线性组合输出层最终输出“属于1类”的概率。算一下参数数量输入层到隐藏层有2×48个权重隐藏层每个节点有1个偏置共4个隐藏层到输出层有4个权重输出层有1个偏置。总参数13个。就这么点参数已经能构成一个完整的学习系统。隐藏层的激活函数我用tanh输出层用sigmoid。tanh把输出压缩到-1到1之间中心为0梯度更平稳sigmoid把最终输出压缩到0到1之间天然适合表示概率。这里不展开讲每个函数的导数但你会在代码和梯度推导中看到它们的重要性。初始化时所有权重取(-0.5, 0.5)之间的随机数。为什么不能全设成0如果同一个隐藏层的所有神经元初始一样训练中它们会一直保持一样等于白白浪费了容量。随机初始化才能让每个神经元学到的模式各不相同。3.2 前向传播让数据流过网络前向传播就是逐个神经元计算的过程。对每个隐藏神经元j先算加权和z_j sum(w1[j][i] * x[i]) b1[j]然后过激活函数h_j tanh(z_j)。输出层再把这些隐藏特征加权过sigmoid得到最终概率p。input_size 2 hidden_size 4 output_size 1 w1 [[random.uniform(-0.5, 0.5) for _ in range(input_size)] for _ in range(hidden_size)] b1 [0.0] * hidden_size w2 [random.uniform(-0.5, 0.5) for _ in range(hidden_size)] b2 0.0 def forward(x): hidden [] for j in range(hidden_size): z b1[j] for i in range(input_size): z w1[j][i] * x[i] hidden.append(math.tanh(z)) z_out b2 for j in range(hidden_size): z_out w2[j] * hidden[j] prob 1.0 / (1.0 math.exp(-z_out)) return hidden, prob我建议你亲手对着纸画一遍这个流程输入x进去走一层过函数再走一层出来一个概率。这个动作就是“前向传播”。后面所有训练逻辑都是为了调整w1、b1、w2、b2让这个输出概率逼近真实标签。3.3 反向传播把损失“分摊”回每个参数到了整个实验最核心的环节。我当年第一次看反向传播满脑子都是“为什么一个式子要连着求三次导”。后来画图才算真正理解它做的事就是把最终的损失一步一步“分摊”回去让每个参数知道自己该往哪个方向调。用链式法则从后往前推。对于输出层导数值 (概率 - 标签) × 概率 × (1 - 概率)这个值叫“输出层梯度”。拿到它之后就能算出每个w2[j]的梯度输出层梯度 × 第j个隐藏节点的输出。偏置b2的梯度就是输出层梯度本身。然后继续往前传先把输出层梯度乘上w2[j]作为第j个隐藏节点的“责任”再乘上tanh的导数(1 - h[j]²)就得到隐藏层每个神经元的梯度。最终给到w1[j][i]的梯度就是隐藏层梯度 × 输入x[i]。这些过程听起来拗口代码落地其实不长learning_rate 0.15 epochs 2000 for epoch in range(epochs): total_loss 0.0 for x, y_true in train_data: hidden, prob forward(x) err prob - y_true total_loss 0.5 * err * err d_out err * prob * (1.0 - prob) grad_w2 [d_out * h for h in hidden] grad_b2 d_out grad_hidden [d_out * w2[j] for j in range(hidden_size)] grad_z1 [grad_hidden[j] * (1.0 - hidden[j] * hidden[j]) for j in range(hidden_size)] grad_w1 [[grad_z1[j] * x[i] for i in range(input_size)] for j in range(hidden_size)] grad_b1 list(grad_z1) b2 - learning_rate * grad_b2 for j in range(hidden_size): b1[j] - learning_rate * grad_b1[j] w2[j] - learning_rate * grad_w2[j] for i in range(input_size): w1[j][i] - learning_rate * grad_w1[j][i] if epoch % 200 0: print(fepoch {epoch}, loss {total_loss / len(train_data):.4f})有个细节必须注意梯度要在更新参数之前全部算完。代码里我先算好所有grad开头的变量然后再统一更新。如果你在循环里边算边更新那后面节点拿到的梯度已经是“用过旧权重”的值整个推导链就会乱掉。3.4 训练完毕看它到底学会没有训练结束用测试集来验收。测试的逻辑和训练时完全一样走一遍前向传播输出概率大于0.5判为1类否则判为0类。把预测标签和真实标签对齐统计准确率。def predict(x): _, prob forward(x) return 1 if prob 0.5 else 0 correct 0 for x, y_true in test_data: pred predict(x) if pred y_true: correct 1 print(ftest accuracy: {correct / len(test_data):.4f})我本机跑这段代码时2000轮后训练损失稳定在0.30附近测试集准确率有九成左右。你别背这个数字因为每次运行受随机种子影响其实不大种子固定了和你手写代码后得到的几乎一样。如果你把学习率从0.15改成0.5很可能会看到损失在某个区间震荡准确率反而下降——这就是学习率过大的经典症状。3.5 为什么这么小的网络也能有效有人会问13个参数是不是太少了它凭什么能学出一个非线性分界线关键是隐藏层。4个隐藏神经元相当于把这一个平面上的坐标变换成了4个特征再用这4个特征做决策。神经网络的本质往往就是把原始输入映射到一个更容易分类的空间。这个想法很简单却是深度学习所有奇迹的起点。4. 训练过程常见的坑与排查技巧4.1 损失不降别急着调代码先看学习率新手最常遇到的情况就是loss卡住不动或者来回震荡。我见过不少人反复检查网络、检查梯度最后发现只是学习率设得离谱。经验值在普通小玩具问题上学习率从0.01到0.3之间比较安全。如果损失曲线像心电图多半是学习率太大降一档如果学得慢吞吞可以适当加大一点。我个人的习惯是先用较大的学习率快速确认代码能跑通再逐步缩小观察损失曲线收敛情况。这比一上来就追求“完美学习率”要高效得多。4.2 梯度消失sigmoid的隐形陷阱代码里隐藏层我特意选了tanh而非sigmoid不是随手的决定。sigmoid的输出不是0中心而且在两端导数几乎为0。当层数变深时梯度一层层乘下来很容易趋近于0导致浅层参数无法更新——这就是“梯度消失”。在这个玩具模型里只有4个隐藏神经元sigmoid影响还不明显。可一旦你想堆神经网络层数问题就会爆发。后面的实验你可以试着把tanh换成sigmoid把隐藏层加到20层再训练大概率会发现前几层权重几乎没变化。4.3 别让训练集和测试集共享任何信息我在划分数据时用了shuffle再把前150个作为训练集、后50个作为测试集。有朋友犯过一个毛病先随机抽了100个训练再在剩下的里面又挑训练集结果测试集里混进了训练样本最终准确率高得离谱。评估模型要像参加考试测试集必须“没见过”。更严格的流程还会把数据分成训练、验证、测试三份验证集用来调超参数测试集只用于最终评估。玩具项目也要养成这个习惯将来面对真实项目会少走很多弯路。4.4 数据归一化量纲不同会要命我这份数据里x1和x2的量纲差不多所以不归一化也能收敛。但真实数据经常一个特征是百分制一个特征是小数甚至差了几个数量级。权重更新时大数值特征会主导梯度其他特征几乎学不到东西。归一化通常就是把每个特征减均值、除以标准差让所有特征都落到类似的尺度。养成看到数据就先想想“量纲统一了没有”的习惯能帮你避开大量训练不收敛的问题。5. 从玩具模型到真实项目下一步怎么走5.1 这个实验你真正学到的东西不要小看这个几十行的迷你网络。它包含了深度学习中所有的核心骨架参数化模型、前向传播、损失函数、梯度下降、反向传播、数据集划分、超参数设置。你现在可以自豪地说我不是只会“调包”而是知道包装纸里面长什么样。以后不管用哪个主流框架你会发现训练流程和我这个实验如出一辙定义网络结构、选损失函数、写训练循环、前向算loss、反向算梯度、更新参数。框架只是把这些步骤的底层运算做成高效且自动求导的引擎。5.2 换掉玩具数据马上会遇到新问题接着做下一个升级实验把数据换成一幅真实图片的分类任务。你会发现直接沿用这个网络根本没法收敛。为什么输入特征从2个变成几万个隐藏层神经元数量继续停留在4个那参数量爆炸计算量让人等不起过拟合也随之而来。到这一步你再去学卷积、池化、dropout、批量归一化这些进阶武器就会明白它们全是冲着“真实数据的规模”和“网络过深后的各种病”去的。带着问题去学新概念效率远高于单纯背公式。5.3 一份可以立刻动手的升级清单我建议你照着这个顺序玩下几个变体每个改动都能加深理解把tanh换成sigmoid观察损失下降速度变化。把隐藏层从4个节点改成32个观察准确率和训练时间变化。把学习率从0.15改成1.5记录loss曲线如何震荡。把数据集改成三类输出层变成3个节点用softmax输出概率。在训练循环里加入mini-batch梯度下降不每样本更新而是每32个样本累计一次梯度再更新。这些变体看起来都只是小改动但每做完一个你对模型的掌控感都会实打实提升一截。最后说点我自己的体会。我第一次写这类手写网络时吃亏最大的地方不是公式而是维度。w1、w2、b1、b2在脑子里全是乱的梯度更新时经常把一个二维列表和一个一维列表对错了位。后来我花了很长时间画了一张计算图把每个张量旁边标注了形状shape才彻底理顺。所以我真心建议你把这几十行代码亲手敲一遍不要复制粘贴。以后跑再大的模型遇到维度不匹配报错你都能靠这张“张量形状地图”快速定位问题。这个迷你网络虽然小但它已经是一个完整的生命体会学习、会犯错、也能被理解。把这份感觉记住后面无论学什么深度学习内容你都不会虚。