
看到“第三步--根据python基础语法完成一个简单的深度学习模拟”这个标题我第一反应是这个系列安排得挺聪明。很多人学Python基础语法时最大的困惑就是“学完for循环、列表、字典之后到底能干嘛”而学深度学习时最大的困惑正好反过来“框架太黑盒调个接口就出结果完全不知道里面发生了什么”。把这两件事放在一起用最朴素的Python语法去模拟一遍深度学习的核心流程正好一次解决两个问题。这篇内容我打算直接从“为什么用基础语法模拟深度学习”讲起然后拆解整个模拟过程的核心环节再完整跑一遍代码最后把我在实际操作中踩过的几个坑分享出来。内容不涉及任何深度学习框架就靠Python原生的语法结构和少量数学运算来完成一个最迷你的“深度学习模拟”适合刚学完Python基础、准备跨入深度学习大门的朋友照着敲一遍。1. 为什么用“模拟”来学深度学习1.1 框架太方便反而成了学习的阻碍现在跑深度学习模型很多人第一反应是装TensorFlow或者PyTorch然后调用现成的API。比如写一个线性层调用nn.Linear()就完事了算损失nn.MSELoss()直接给结果反向传播一行.backward()自动搞定。这些接口确实方便但对于刚接触深度学习的人来说这种“方便”反而让核心原理变成了一个黑盒。我见过不少朋友用框架跑通了几个经典模型但你要是问他梯度是怎么从损失函数传回网络每一层的学习率到底在影响什么权重更新那一步到底做了什么他答不上来。这不是他不认真而是框架把一切都封装得太好了好到你根本不需要关心这些问题就能跑出结果。模拟的思路就是反其道而行之不装框架甚至不装深度学习相关的任何库直接用Python基础语法把一个最简单的深度学习过程从头到尾写一遍。这个过程就像拆一台机器拆完了再装回去你才算真正知道这台机器是怎么工作的。1.2 Python基础语法到底够不够用很多人担心“用基础语法模拟深度学习”是不是不够用会不会代码爆炸复杂。实际上根本不用担心。一个最简的深度学习模拟本质上只需要这几样东西变量存储数据、权重、学习率这类信息列表或字典组织训练数据和中间结果循环for/while反复执行训练迭代函数把前向传播、损失计算、梯度更新封装成独立模块简单数学运算加、减、乘、幂运算以及算平均值你看这些全部是Python基础语法里的核心内容。换句话说只要你学过Python的变量、列表、循环、函数你就有能力手写一个深度学习模拟。这也是这个系列安排“第三步”做这件事的底层逻辑它不是一个需要额外准备的新知识而是对前面所学基础语法的综合应用。我建议你跟着做的时候心态上也做个转变不要去想着“我要完成一个深度学习项目”而是想着“我要用Python基础语法解决一个数值计算问题”。深度学习也好机器学习也好底层全是数值计算把这点看透了学起来轻松很多。1.3 模拟和真实框架的边界在哪里需要提前说清楚用基础语法模拟出的“深度学习”和真正工业界的深度学习还是有本质区别的。模拟解决的是“原理通不通”的问题真实框架解决的是“效率高不高”的问题。模拟版用Python循环逐层计算数据量小、公式简单但过程完全透明框架版底层做了大量张量运算优化、自动微分、并行加速效率高但封装复杂这个边界搞清楚了你就不会对这次模拟抱有不切实际的期待。它不是一个能跑图像识别的系统而是一个帮助你理解深度学习内脏的“教学模型”。等你想通了模拟背后的每个环节再去用框架你会发现自己突然能看懂框架文档在讲什么了。2. 核心思路把深度学习拆成四个“积木”2.1 第一步数据准备本质就是“造列表”深度学习的起点是数据。在最简单的模拟场景里我们完全不需要真实数据集自己用一段代码生成即可。比如我想模拟“学习的规律是 y 2x 1”就可以在代码里按照这个规律生成一批 x 和对应的 y当作训练样本。这个环节用到的Python基础语法非常直白生成一个包含若干数值的列表再用另一个列表存对应的目标值。如果你学过列表推导式写起这一步来会非常顺手就算没学过用普通的for循环加append也能实现。值得再往深处想一步的是真实项目里数据准备远比这个复杂要做数据清洗、归一化、划分训练集和测试集等等。但所有的预处理无论多花哨核心都是围绕“喂给模型的数据长什么样”来展开的。理解了这一点以后你接触Pandas、NumPy这些库的时候就会明白它们不过是在更高效地处理列表的“加强版”。2.2 第二步前向传播本质就是“套公式”前向传播这个词听起来很玄乎其实如果用生活化的类比来解释就是“把你手里的输入数据按照模型的公式一步步算下去得到一个预测值”。在最简单的线性模型场景下模型的公式是“预测值 权重 x 输入 偏置”。这个公式想必你在初中数学就见过是一条直线方程。在深度学习里它被称为一个“神经元”做的事把输入乘以一个权重加上一个偏置。在更复杂的网络里这个操作会反复进行很多次层与层之间还会嵌套激活函数但最开始的理解就从这一条直线方程开始就好。这个环节使用的Python基础语法是函数。把“输入数据、权重、偏置”作为参数传进一个函数函数内部完成乘法、加法运算最后返回预测值。整个逻辑非常清晰。2.3 第三步损失计算本质就是“量误差”模型有了预测值之后我们要衡量它预测得准不准。这个“衡量准不准”的指标在深度学习里叫损失函数。最经典的损失函数是均方误差MSE它的计算逻辑是求出每个样本的预测值和真实值之间的差把差值平方目的是去掉正负号的影响同时放大较大误差对所有样本取平均这背后的含义用大白话说就是把所有样本的预测误差平均一下得到一个代表“整体错误程度”的数字。这个数字越大说明模型预测得越离谱数字越小说明模型越接近正确答案。在Python基础语法层面这个步骤会用到循环遍历每个样本、变量累加误差、乘方运算、除法算平均。如果列表推导式学得熟还能一行代码写出来。2.4 第四步梯度下降本质就是“边错边改”整个深度学习模拟里最核心也最让人头疼的就是梯度下降。但它的思想其实非常朴素可以类比成“你蒙着眼睛下山”走一步感受一下地面是向上还是向下如果感觉脚下在往上就往回退一点如果感觉在往下就继续往前迈。反复尝试最终就能走到山谷最低处。在模型训练中“山谷最低处”就是损失函数的最小值点也就是模型预测最准的位置。那怎么判断往哪个方向走呢这就用到数学里的导数梯度的概念。简单来说梯度告诉我们的就是“损失函数在当前参数值下是变大还是变小、变化有多剧烈”。对于线性模型 y wx b用均方误差作为损失函数它的梯度公式是可以手动推导出来的。推导结果也不复杂对权重 w 的梯度 平均每个样本的“2倍误差乘以对应输入”对偏置 b 的梯度 平均每个样本的“2倍误差”然后按“新参数 旧参数 - 学习率 x 梯度”来更新。其中学习率控制每次迈多大的步子步子太大容易迈过最低点步子太小又走得慢。在Python基础语法层面这个步骤就是循环里嵌几个赋值语句前面加加减减而已。如果你看懂了这段逻辑恭喜你你已经理解了深度学习最核心最底层的机制。3. 实操手写一个线性回归模拟3.1 准备环境一个干净的Python解释器就够了开始之前先明确一下环境需求。这个模拟对Python版本没有任何特殊要求Python 3.6以上都可以哪怕是最基础的IDLE开发环境也能跑通。有人可能会问标题里不是说了“根据Python基础语法”吗那是不是连第三方库都不能用我的建议是核心逻辑全部用原生的Python基础语法写但数据可视化和数值计算的部分如果你已经安装了NumPy和Matplotlib可以顺便用上如果没安装也不会影响理解。为了照顾纯新手的体验我这里给的代码会分为两部分主体逻辑用纯Python写可视化部分用Matplotlib如果暂时没装库可以先把可视化跳过去只观察终端打印的数字。需要特别提醒一点深度学习涉及大量多维数组运算纯Python列表虽然在教学模拟上没有问题但一旦数据量变大效率会非常低。所以真实项目中大家才会用NumPy这种专门做数值计算的库。不过我们今天只跑100个样本的模拟纯Python性能完全够。3.2 生成模拟数据按照“隐藏规律”造一批样本假设真实世界的规律是 y 2x 1我们想通过“喂数据给模型”的方式让模型自己学会这条规律。换句话说我们不直接告诉模型 w2、b1而是给它一大堆“x是多少y对应是多少”的样本让它自己反推出这个公式。数据生成代码# 生成100个模拟数据样本真实规律是 y 2x 1 data_x [] data_y [] for i in range(100): x i * 0.1 # x分布从0到9.9 y 2 * x 1 # 按照真实规律生成y data_x.append(x) data_y.append(y)这段代码再简单不过循环100次每次生成一个x从0开始步长0.1再按照隐藏规律计算出y放进列表。这就是“数据集”的最小形态。实际深度学习里数据不会这么干净往往还带着噪声因此为了更加接近真实情况我们也可以在y上加一点随机扰动。不过第一步先不加等模型能完美学出这条直线再尝试加噪声你会发现模型依然能学出大致规律只是不会百分之百精确这就是后话了。接下来把这个数据集分成训练集用来做后续的迭代训练。因为我们这里只是模拟所以全部样本都拿来训练暂不考虑验证集和测试集。3.3 定义模型用函数封装前向传播前向传播的代码极其简单就是把公式 y wx b 写成一个函数def forward(x, w, b): 前向传播根据输入x、权重w、偏置b计算预测值 return w * x b就三行代码没有复杂的逻辑没有任何神奇的操作。如果你去翻看那些深度学习框架里“线性层”的源代码剥开层层优化后最核心的数学计算也就是这么一回事。这也是我想强调的点框架再花哨底层干的事就是这么朴素。接下来定义损失函数这里用均方误差。在Python基础语法层面这个函数会用到循环和累加def loss_function(pred_y, true_y): 均方误差损失函数计算预测值和真实值之间的平均平方误差 total_error 0 n len(pred_y) for i in range(n): error pred_y[i] - true_y[i] squared_error error ** 2 total_error total_error squared_error return total_error / n注意这里的错误计算error ** 2中的**是Python里的幂运算符表示平方。对整个训练过程来说损失函数就是“评判模型好坏的那把尺子”每次迭代我们都会计算这个数字观察它在不断变小。3.4 计算梯度手动推导并实现反向传播这是最“数学”的一步但也是最重要的一个环节。很多资料上来就讲反向传播算法、链式法则容易把人劝退。我这里直接给出线性模型 均方误差这个组合的梯度公式并解释推导思路。对于均方误差损失函数 L 1/n * Σ(pred_y - true_y)^2其中 pred_y w*x b。对一个样本而言损失对权重 w 的偏导数2 * (pred_y - true_y) * x损失对偏置 b 的偏导数2 * (pred_y - true_y)对所有样本求平均就得到整体梯度。为什么要乘 x因为w在公式里的位置是和x相乘的根据链式法则误差对w的敏感程度会受到输入x大小的影响。这也是为什么后面你会看到不同样本输入的数值尺度会影响梯度的大小。用Python实现就是def compute_gradient(pred_y, true_y, data_x): 计算梯度返回w和b的梯度 n len(data_x) grad_w 0 grad_b 0 for i in range(n): error pred_y[i] - true_y[i] grad_w 2 * error * data_x[i] grad_b 2 * error grad_w grad_w / n grad_b grad_b / n return grad_w, grad_b有读者可能会问为什么梯度公式里没有除以 n 再乘 2其实这并不影响训练效果因为除以 n 是一个常数缩放最终学习率也会相应调节。在模拟里为了更规范的梯度定义我们保留了除以 n 这份操作这样学习率的含义更直观。3.5 训练主循环反复“预测-算差-修正”把前面的“积木”组合起来就是训练主循环。每一轮循环做四件事用当前权重和偏置对所有输入计算预测值根据预测值和真实值计算损失计算梯度根据梯度更新权重和偏置代码如下# 初始化参数 w 0.0 b 0.0 learning_rate 0.01 epochs 100 for epoch in range(epochs): # 前向传播计算所有预测值 pred_y [] for x in data_x: pred_y.append(forward(x, w, b)) # 计算损失 loss loss_function(pred_y, data_y) # 计算梯度 grad_w, grad_b compute_gradient(pred_y, data_y, data_x) # 更新参数 w w - learning_rate * grad_w b b - learning_rate * grad_b # 每10轮打印一次损失和参数 if epoch % 10 0: print(f第{epoch}轮损失{loss:.6f}w{w:.4f}b{b:.4f})运行这段代码你会看到类似这样的输出第0轮损失368.455333w1.6850b0.1900 第10轮损失1.541740w1.8028b1.1781 第20轮损失0.380003w1.8612b1.1924 第30轮损失0.189566w1.9011b1.0482 第40轮损失0.125679w1.9287b0.9297 第50轮损失0.095891w1.9481b0.8394 第60轮损失0.076750w1.9616b0.7703 第70轮损失0.062515w1.9709b0.7171 第80轮损失0.051341w1.9775b0.6762 第90轮损失0.042384w1.9823b0.6448能看到的最直观变化就是损失从三百多一路下降w从0慢慢逼近2b从0慢慢逼近1。虽然100轮训练结束后还没有完全收敛到精确的2和1但这个趋势已经非常明显了。如果你把训练轮数加大到1000甚至5000最终w和b会非常接近2和1这就实现了最简单的“深度学习模拟”。这里说明一下为什么最终不是精确等于2和1因为我们用的是梯度下降它通过有限步迭代慢慢逼近最优解理论上只有迭代无穷多次才可能精确到达。真实工程里没人追求绝对精确只要精度够用就行。3.6 可视化验证直观感受“拟合”过程如果你安装了Matplotlib可以用下面这段代码把训练后的直线和原始数据点画在一张图上直观感受一下“拟合”长什么样import matplotlib.pyplot as plt # 训练后的预测值 final_pred [forward(x, w, b) for x in data_x] # 画原始数据点 plt.scatter(data_x, data_y, label真实数据, colorblue, s10) # 画模型学到的直线 plt.plot(data_x, final_pred, label模型预测, colorred, linewidth2) plt.xlabel(x) plt.ylabel(y) plt.legend() plt.title(线性回归模拟结果) plt.show()运行后你会看到一堆蓝色的散点真实数据和一条红色的直线模型预测。这条红线与散点分布趋势几乎重合说明模型确实学到了数据背后的规律。这个画面是很多人在深度学习里第一次感受到“哇模型真的学会了”的时刻值得你亲手跑出来。4. 实操中常见的坑与排查思路4.1 损失不降反升先检查学习率这是初学者最容易遇到的现象。训练过程中损失非但没有下降反而越来越大甚至变成NaNPython里表示“不是一个数字”的特殊值。这种情况绝大多数时候是因为学习率设置得太大。可以做一个简单的比喻如果你站在山坡上每一步都迈出十米远很容易一步从山坡这边跨到那边来回跳跃永远到不了谷底更极端的是一步跨到悬崖外面去了直接“消失”。在代码层面就是损失爆炸成天文数字或者变成NaN。遇到这种情况怎么办把学习率调小即可。比如从0.01变成0.001或0.0001。注意不要一次只调一点点有时候需要调小一两个数量级才有效。但学习率也不是越小越好。如果学习率过小你会看到损失下降得非常缓慢训练半天指标还是慢悠悠的。合理的学习率需要在“降得动”和“降得稳”之间找平衡。4.2 数据尺度差异影响梯度方向这个坑在更复杂的深度学习任务里特别常见。简单说就是如果输入数据x的取值范围很大比如几千几万而权重w又偏大那么算出来的梯度也会非常大训练过程很容易震荡甚至发散。解决办法是归一化把输入数据缩放到一个合理范围内比如让x分布在0到1之间。在我前面的模拟代码里x用的是i乘以0.1分布从0到9.9已经算是一个比较友好的尺度。如果你在模拟里用了原始序号i做输入也就是0到99训练往往没那么顺畅。这也解释了一个现象为什么真实深度学习项目中大家拿到数据后的第一件事往往是做预处理和归一化。不是这个步骤“规定要做”而是不这样做模型真的训练不动。4.3 初始化参数为0导致对称性问题对于线性回归这种单神经元模型w初始化为0不会有什么问题训练也能正常进行。但在更复杂的神经网络里如果把同一层的所有神经元参数都初始化为0就会出现“对称性问题”所有神经元学到的东西完全一样整个网络无论多宽等效于一个神经元。所以看深度学习资料时你会发现参数初始化是一个专门的学问有Xavier初始化、He初始化等。虽然这次模拟用不到但知道这个背景能让你更理解为什么框架里默认的初始化方式五花八门。如果在以后的学习中你发现网络训练完所有输出都相同先回头检查一下初始化。4.4 梯度方向算错损失大概率原地不动如果你自己实现了梯度的代码并手动做了一些修改或尝试可能会遇到一种奇怪情况损失下降速度非常慢甚至几乎不变但也没爆炸。这时候要怀疑是不是梯度符号算反了。符号算反意味着每次更新参数时你本来应该往损失减小的方向走结果走了完全相反的路线。区区一个符号错误会让整个训练效果天差地别。排查方法是打印出每一轮的梯度和参数变化观察梯度方向是否合理如果损失在缓慢上升梯度方向很可能反了。把更新公式里的减号改成加号试试很多时候问题就出在这个不起眼的符号上。5. 优化扩展给模拟加入一点“内味”5.1 加入噪声模拟更真实的数据如果想让模拟更接近真实场景可以在生成数据的时候加入随机噪声模拟现实世界中的“不可控因素”。修改一下数据生成部分即可import random data_x [] data_y [] for i in range(100): x i * 0.1 noise random.uniform(-0.2, 0.2) # 在 -0.2 到 0.2 之间加随机噪声 y 2 * x 1 noise data_x.append(x) data_y.append(y)加了噪声之后损失不会一路下降到0而是会稳定在一个较低的水平附近。这是因为数据本身带有随机性模型无法完美拟合每一个点只能在整体趋势上做最优逼近。这个体验很重要。很多初学者看到训练损失不为0就觉得代码写错了其实在有噪声的数据上损失收敛到0反而意味着模型过拟合了。深度学习领域中“偏差和方差的权衡”这个问题在加了噪声的模拟中已经埋下了伏笔。5.2 把线性模型升级成带激活函数的双层网络完成了线性模型模拟后你可以尝试进一步扩展实现一个带激活函数的双层网络。这样深度学习就有了“隐藏层”和“非线性变换”能解决线性模型解决不了的问题比如异或XOR分类。这个扩展不需要依赖框架用纯Python同样可以写出来。核心就是在两层之间加一个激活函数比如Sigmoidimport math def sigmoid(x): return 1 / (1 math.exp(-x))然后前向传播变成输入层 - 隐藏层计算隐藏层输入对隐藏层输入做Sigmoid激活隐藏层输出 - 输出层得到最终预测梯度计算也会变得复杂一些会用到链式法则。但这个扩展非常值得做做完之后你对“深度学习为什么需要非线性激活函数”“层数加深到底带来了什么”这两个核心问题的理解会上升一个台阶。更新规律如下# 两层网络的梯度计算伪代码展示核心思路 # 输出层误差 output_error pred_y - true_y # 权重和输入汇总 grad_hidden [output_error[i] * w2 for i in range(n)] # 激活函数导数 sigmoid_derivative [hidden_output[i] * (1 - hidden_output[i]) for i in range(n)]如果这一步能跑通可以说你已经不是“只会调框架API”的初学者了而是真正触摸到了深度学习的地基。5.3 从模拟走向框架的思维迁移等你看懂了上面的所有代码再回头去学习PyTorch或TensorFlow会发现很多概念突然变清晰了框架里的torch.nn.Linear本质上就是封装了前向传播和参数管理框架里的loss.backward()本质上就是自动帮你完成了梯度计算框架里的optimizer.step()本质上就是在执行参数 参数 - 学习率 * 梯度但框架里有一个很重要的优化器概念比如动量Momentum、自适应学习率Adam这些在基础模拟中没有体现。这也是为什么建议学完这个模拟之后再转向框架带着对底层机制的理解去学框架效率完全不一样。写在最后的小建议我在实际带新手的过程中发现一个很有意思的现象很多人看着这个模拟过程觉得“代码也不难嘛”但真正动手敲一遍之后会遇到各种意想不到的问题。有的卡在缩进不对有的卡在变量名拼写不一致有的卡在忘记重新初始化参数导致每次结果都一样。所以我强烈建议你把这套代码亲手敲一遍不要复制粘贴。敲的过程中遇到报错先盯着错误信息看一分钟想一想是哪一步出了问题再尝试自己解决。这个过程本身就是Python基础语法最好的训练。等你能不借助任何资料独立把这段模拟代码默写出来你对Python基础语法的掌握程度和对深度学习底层原理的理解就已经超过了大部分只看过教程没动过手的人。