图解深度学习入门:用NumPy和Matplotlib手写神经网络

发布时间:2026/10/20 16:45:24
图解深度学习入门:用NumPy和Matplotlib手写神经网络 1. 为什么用图解方式切入深度学习比啃公式更高效很多人第一次接触深度学习翻开教材看到满屏的偏导数、矩阵求导、链式法则翻不到二十页就合上了。这不是智力问题而是学习路径选错了。我见过太多人从数学推导入手花了三个月还在纠结反向传播的公式怎么推结果连一个最简单的图像分类任务都跑不起来。这种挫败感是劝退新手的第一杀手。图解深度学习的核心逻辑是先建立直觉再补数学。人的大脑对图形的理解速度比对符号快几个数量级。当你看到一张图里数据点被一条曲线分成两类你立刻能理解分类在做什么但如果你看到的是一堆权重矩阵和激活函数嵌套你可能要花十分钟才能反应过来这其实就是在做同一件事。这个速成课程的第一篇目标很明确让你在Python环境下用可视化的方式理解深度学习最核心的几个概念——张量、计算图、前向传播、损失函数、梯度下降。不要求你会推公式但要求你能画出每一步在做什么。等你脑子里有了这些图后面再去补数学你会发现那些公式突然变得可读了。适合谁看有Python基础但没碰过深度学习的人看过理论但一动手就懵的人以及那些被数学劝退过、想换个角度重新入门的人。如果你已经能熟练训练模型调参这篇可能对你偏浅但里面关于可视化调试的思路仍然值得参考。提示本篇所有代码基于Python 3.10核心依赖只有NumPy和Matplotlib。不装PyTorch或TensorFlow目的是让你看清底层在算什么而不是被框架的API遮住眼睛。2. 环境准备与最小工具链的搭建逻辑2.1 为什么只用NumPy和Matplotlib就够了新手最容易犯的错是一上来就装PyTorch然后照着教程调model.fit()跑通了也不知道发生了什么。我的建议是第一周不要碰任何深度学习框架。用NumPy手写一遍前向传播和反向传播用Matplotlib把每一层的输出画出来你对深度学习在干什么的理解会完全不一样。NumPy负责数值计算Matplotlib负责可视化这两个库加起来不到50MB安装秒完成。等你用手写代码跑通了一个两层神经网络再去用PyTorch你会发现框架帮你做的每一件事你都能对应到自己的代码上那种通透感是直接调API永远给不了的。安装命令很简单pip install numpy matplotlib如果你用condaconda install numpy matplotlib验证安装import numpy as np import matplotlib.pyplot as plt print(np.__version__) print(matplotlib.__version__)2.2 可视化环境的两个关键配置Matplotlib默认的绘图风格偏工程感做深度学习可视化时建议改两个配置。第一是开启内联显示如果你用Jupyter第二是调整DPI让图片更清晰import matplotlib.pyplot as plt %matplotlib inline plt.rcParams[figure.dpi] 120 plt.rcParams[figure.figsize] (8, 5) plt.rcParams[axes.grid] True plt.rcParams[font.size] 11这里有个我踩过的坑中文标签显示为方块。原因是Matplotlib默认字体不含中文。解决办法是指定中文字体plt.rcParams[font.sans-serif] [SimHei, DejaVu Sans] plt.rcParams[axes.unicode_minus] Falseaxes.unicode_minus设为False是为了让负号正常显示否则坐标轴上的负数会变成方块。这个细节很多教程不讲但实际画损失曲线时一定会遇到。2.3 目录结构建议虽然只是入门项目但养成好的文件组织习惯很重要。我建议这样组织dl_tutorial/ ├── notebooks/ │ ├── 01_tensor_basics.ipynb │ ├── 02_forward_pass.ipynb │ └── 03_gradient_descent.ipynb ├── utils/ │ └── plot_helpers.py └── data/ └── (后续放数据集)把绘图辅助函数抽到plot_helpers.py里避免每个notebook都复制一遍绘图代码。这个习惯在项目变大后会救你很多时间。3. 张量深度学习里一切数据的统一容器3.1 从标量到张量的直观理解深度学习中所有数据——输入图片、权重、偏置、输出——都是张量。这个词听起来唬人其实就是一个多维数组。用图解的方式理解标量0维张量一个数比如学习率0.01向量1维张量一列数比如一张28x28图片展平后的784个数矩阵2维张量一张表比如一个批次的32张图片形状是(32, 784)3维及以上比如彩色图片(高度, 宽度, 通道数)用NumPy创建这些张量import numpy as np scalar np.array(3.14) vector np.array([1, 2, 3, 4]) matrix np.random.randn(3, 4) tensor_3d np.random.randn(2, 3, 4) print(scalar.shape) # () print(vector.shape) # (4,) print(matrix.shape) # (3, 4) print(tensor_3d.shape) # (2, 3, 4)关键点shape是理解张量最重要的属性。每次看到报错说维度不匹配99%的情况是你没搞清楚两个张量的shape。3.2 形状变换reshape和transpose的实战意义深度学习里最常见的操作之一就是改变张量形状。比如一张28x28的图片送入全连接层前要展平成784维向量img np.random.randn(28, 28) flat img.reshape(-1) # 或 img.flatten() print(flat.shape) # (784,)reshape(-1)里的-1表示自动计算这一维这是我最常用的写法因为不用手动算乘积。另一个高频操作是transpose尤其在处理批次数据时。比如一个形状为(批次, 高度, 宽度, 通道)的张量要转成(批次, 通道, 高度, 宽度)batch np.random.randn(32, 28, 28, 3) transposed batch.transpose(0, 3, 1, 2) print(transposed.shape) # (32, 3, 28, 28)transpose(0, 3, 1, 2)的意思是新张量的第0维来自原张量的第0维第1维来自原张量的第3维以此类推。这个操作在可视化特征图时特别有用。3.3 广播机制让不同形状的张量能一起运算广播broadcasting是NumPy最强大也最容易让人困惑的机制。简单说当两个形状不同的张量做运算时NumPy会自动扩展较小的那个使形状匹配。a np.random.randn(3, 4) b np.random.randn(4) c a b # b被广播成(3, 4) print(c.shape) # (3, 4)广播规则从最后一维开始比较要么相等要么其中一个是1要么其中一个不存在。不满足就报错。我踩过的坑以为(3, 1)和(1, 4)能直接相加得到(3, 4)结果确实可以但如果你想要的是外积效果得用np.outer或者显式reshape。广播虽然方便但用错了会得到看起来对但实际错的结果而且不报错非常隐蔽。注意调试张量运算时养成打印shape的习惯。我通常在关键运算前后各加一行print(x.shape)比事后猜维度快得多。4. 计算图把前向传播画成一张流程图4.1 什么是计算图为什么它重要计算图computational graph是把计算过程表示成有向图节点是张量或运算边是数据流动方向。深度学习框架的核心就是自动构建和遍历计算图。举个最简单的例子y (x 2) * 3。用计算图表示就是x -- [加2] -- a -- [乘3] -- y看起来多此一举但当网络有几十层、每层有几百个运算时计算图能让你清晰地追踪数据从输入到输出的完整路径更重要的是——反向传播就是沿着计算图反向走一遍。4.2 用Python手写一个微型计算图下面这段代码实现了一个极简的计算图支持加法和乘法并能自动计算梯度。这是理解PyTorch autograd的基础class Node: def __init__(self, value, parentsNone, opNone): self.value value self.parents parents or [] self.op op self.grad 0.0 def __add__(self, other): other other if isinstance(other, Node) else Node(other) return Node(self.value other.value, [self, other], add) def __mul__(self, other): other other if isinstance(other, Node) else Node(other) return Node(self.value * other.value, [self, other], mul) def backward(self, grad1.0): self.grad grad if self.op add: for p in self.parents: p.backward(grad) elif self.op mul: self.parents[0].backward(grad * self.parents[1].value) self.parents[1].backward(grad * self.parents[0].value)用这个类验证y (x 2) * 3在x1处的梯度x Node(1.0) y (x 2) * 3 y.backward() print(fy {y.value}, dy/dx {x.grad}) # y 9.0, dy/dx 3.0结果dy/dx3符合预期。这段代码虽然简陋但已经包含了自动微分的核心思想前向计算时记录运算关系反向传播时按链式法则逐节点传递梯度。4.3 可视化计算图把计算图画出来能极大帮助理解。用Matplotlib画一个简单的import matplotlib.pyplot as plt import networkx as nx G nx.DiGraph() G.add_edge(x, a, label2) G.add_edge(2, a) G.add_edge(a, y, label*3) G.add_edge(3, y) pos {x: (0, 0), 2: (0, -1), a: (1, 0), 3: (1, -1), y: (2, 0)} nx.draw(G, pos, with_labelsTrue, node_size2000, node_colorlightblue, font_size14, arrowsTrue) labels nx.get_edge_attributes(G, label) nx.draw_networkx_edge_labels(G, pos, edge_labelslabels) plt.title(计算图示例: y (x 2) * 3) plt.show()这张图的价值在于当你看到反向传播时你能想象梯度从y出发沿着箭头反向流回x每经过一个节点就按该节点的运算规则计算局部梯度。图在脑子里公式就不再是死记硬背。5. 前向传播数据从输入到输出的完整旅程5.1 单层神经网络的图解一个神经元做的事接收输入加权求和加偏置过激活函数。用图表示x1 --w1--\ x2 --w2--- [求和 b] -- [激活函数] -- 输出 x3 --w3--/用NumPy实现单个神经元def neuron(x, w, b, activationrelu): z np.dot(x, w) b if activation relu: return np.maximum(0, z) elif activation sigmoid: return 1 / (1 np.exp(-z)) return z多个神经元并排就是一层。一层的计算可以写成矩阵乘法def layer(x, W, b, activationrelu): z x W b return np.maximum(0, z) if activation relu else z这里x的形状是(批次, 输入维度)W的形状是(输入维度, 输出维度)b的形状是(输出维度,)。矩阵乘法自动处理了批次维度这就是为什么深度学习离不开线性代数。5.2 两层网络的完整前向传播下面搭建一个完整的两层网络输入784维28x28图片隐藏层128维输出10维10分类np.random.seed(42) def init_params(): W1 np.random.randn(784, 128) * 0.01 b1 np.zeros(128) W2 np.random.randn(128, 10) * 0.01 b2 np.zeros(10) return W1, b1, W2, b2 def forward(X, params): W1, b1, W2, b2 params Z1 X W1 b1 A1 np.maximum(0, Z1) # ReLU Z2 A1 W2 b2 # softmax exp_Z2 np.exp(Z2 - np.max(Z2, axis1, keepdimsTrue)) A2 exp_Z2 / np.sum(exp_Z2, axis1, keepdimsTrue) return Z1, A1, Z2, A2注意softmax里的np.max(Z2, axis1, keepdimsTrue)减法操作。这是数值稳定技巧如果不减最大值np.exp可能溢出成inf。这个细节在教科书里经常一笔带过但实际写代码时不做就会遇到nan。5.3 可视化每层的激活值分布前向传播跑完后把每层的输出分布画出来能直观看到数据在网络中如何变化X np.random.randn(64, 784) # 模拟一个批次 params init_params() Z1, A1, Z2, A2 forward(X, params) fig, axes plt.subplots(1, 3, figsize(15, 4)) axes[0].hist(Z1.flatten(), bins50, colorsteelblue) axes[0].set_title(第一层线性输出 Z1) axes[1].hist(A1.flatten(), bins50, colorcoral) axes[1].set_title(第一层激活后 A1 (ReLU)) axes[2].hist(A2.flatten(), bins50, colorseagreen) axes[2].set_title(输出层概率 A2 (Softmax)) plt.tight_layout() plt.show()你会看到Z1是近似正态分布A1因为ReLU有一半是0A2是概率分布且每行和为1。这三张图就是前向传播的体检报告。如果A1全是0说明ReLU死了学习率太大或初始化不好如果A2某一行全是0.1说明模型还没学到东西。提示初始化权重时乘0.01是为了防止第一层输出过大导致ReLU全部激活或全部死亡。这个缩放系数不是随便定的标准做法是用He初始化或Xavier初始化后面章节会展开。6. 损失函数与梯度下降让网络自己学会调整6.1 交叉熵损失为什么是分类任务的首选分类任务用交叉熵损失回归任务用均方误差。为什么分类不用均方误差因为交叉熵配合softmax的梯度形式特别简洁而且对错误预测的惩罚是指数级的学习信号更强。交叉熵的公式L -sum(y_true * log(y_pred))。用NumPy实现def cross_entropy(y_pred, y_true): n y_pred.shape[0] log_likelihood -np.log(y_pred[range(n), y_true] 1e-8) return np.sum(log_likelihood) / n1e-8是防止log(0)的平滑项。y_pred[range(n), y_true]是取出每个样本对应真实类别的预测概率这是交叉熵计算的向量化写法比循环快得多。6.2 数值梯度验证解析梯度梯度下降需要知道损失对每个参数的梯度。手写反向传播容易出错所以要用数值梯度验证。数值梯度的原理是f(x) ≈ (f(xh) - f(x-h)) / (2h)。def numerical_gradient(f, x, h1e-5): grad np.zeros_like(x) it np.nditer(x, flags[multi_index]) while not it.finished: idx it.multi_index old x[idx] x[idx] old h fx1 f(x) x[idx] old - h fx2 f(x) grad[idx] (fx1 - fx2) / (2 * h) x[idx] old it.iternext() return grad这个函数很慢每个参数要算两次前向但用来验证解析梯度是否正确非常可靠。我通常在小网络上跑一次确认解析梯度和数值梯度的相对误差小于1e-6就可以放心用解析梯度了。6.3 梯度下降的三种变体与选择依据批量梯度下降BGD用全部数据算梯度稳定但慢随机梯度下降SGD用一个样本快但震荡小批量梯度下降Mini-batch折中是实际最常用的。def sgd_update(params, grads, lr0.01): W1, b1, W2, b2 params dW1, db1, dW2, db2 grads W1 - lr * dW1 b1 - lr * db1 W2 - lr * dW2 b2 - lr * db2 return W1, b1, W2, b2学习率的选择太大震荡不收敛太小收敛慢。经验值从0.01开始试观察损失曲线。如果损失上下跳动降一个数量级如果损失几乎不变升一个数量级。6.4 把损失曲线画出来训练过程的可视化训练时每轮记录损失画成曲线losses [] for epoch in range(100): # ... 前向、反向、更新 ... losses.append(current_loss) plt.plot(losses, linewidth2) plt.xlabel(迭代轮次) plt.ylabel(损失值) plt.title(训练损失曲线) plt.yscale(log) # 对数坐标更容易看清收敛趋势 plt.show()用对数坐标是个实用技巧损失从1降到0.1线性坐标上看起来变化不大对数坐标上是一条明显的下降直线。健康的训练曲线应该是对数坐标下近似线性的下降如果出现平台期或上升说明学习率或初始化有问题。7. 实操中容易踩的坑与调试心得7.1 梯度消失与梯度爆炸的识别训练深层网络时反向传播的梯度可能变得极小消失或极大爆炸。识别方法打印每层梯度的范数。for i, (W, dW) in enumerate(zip([W1, W2], [dW1, dW2])): print(f第{i1}层权重梯度范数: {np.linalg.norm(dW):.6f})如果第一层梯度范数比最后一层小好几个数量级就是梯度消失。解决办法用ReLU代替Sigmoid用He初始化或者加BatchNorm。梯度爆炸则相反通常用梯度裁剪解决def clip_gradient(grad, max_norm5.0): norm np.linalg.norm(grad) if norm max_norm: grad grad * (max_norm / norm) return grad7.2 权重初始化的讲究全零初始化会让所有神经元学到相同的东西对称性无法打破。小随机数初始化是基础做法但更好的选择是He初始化针对ReLUdef he_init(fan_in, fan_out): std np.sqrt(2.0 / fan_in) return np.random.randn(fan_in, fan_out) * stdHe初始化的推导依据是保持每层输出的方差一致避免信号在传播中放大或衰减。这个细节在《Deep Learning》书里有完整推导但实际用的时候记住ReLU配HeSigmoid配Xavier就够了。7.3 过拟合的早期信号与应对训练损失下降但验证损失上升就是过拟合。早期信号是验证损失曲线开始上翘。应对手段按优先级加数据 加正则化 减小模型 早停。L2正则化的实现很简单在损失里加权重平方和def l2_regularization(params, lambda_reg0.001): W1, _, W2, _ params return lambda_reg * (np.sum(W1**2) np.sum(W2**2))注意正则化只加在权重上不加偏置。因为偏置不影响模型的复杂度正则化它没有意义。7.4 调试神经网络的通用检查清单我总结了一个排查顺序遇到模型不学习时按这个顺序查检查项正常表现异常处理损失初始值分类任务约等于ln(类别数)偏差大则检查初始化损失下降趋势对数坐标下近似线性不降则调学习率梯度范数各层在同一数量级差异大则换激活函数激活值分布不全为0也不全饱和全0则调初始化训练/验证损失同步下降后验证略高验证上翘则加正则这个表我贴在显示器旁边每次调模型都过一遍能省下大量瞎试的时间。8. 从手写代码到框架的平滑过渡8.1 你手写的每一行在PyTorch里对应什么当你用手写代码跑通了整个流程再看PyTorch会豁然开朗你的forward函数 →nn.Module.forward你的backward手动推导 →loss.backward()自动微分你的sgd_update→optim.SGD.step()你的init_params→nn.Linear自动初始化对应关系清楚了框架就不再是黑盒。你知道loss.backward()背后在做什么知道optimizer.zero_grad()为什么必须调用因为PyTorch默认累积梯度。8.2 用PyTorch重写同一个网络import torch import torch.nn as nn class TwoLayerNet(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(784, 128) self.fc2 nn.Linear(128, 10) self.relu nn.ReLU() def forward(self, x): x self.relu(self.fc1(x)) return self.fc2(x) model TwoLayerNet() criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.01)对比手写版本代码量少了三分之二但每一行你都能对应到之前手写的逻辑。这就是先手写后框架的价值。8.3 可视化工具链的升级路径手写阶段用Matplotlib就够了。进入框架阶段后可以引入TensorBoard或Weights Biases来记录损失、梯度、权重分布。但我的建议是先用Matplotlib把该看的都看一遍再上工具。因为工具会帮你自动记录你反而容易忽略去看。等你对手动记录的内容有了肌肉记忆再用工具提效顺序不能反。到这里图解深度学习速成课程的第一篇就完整了。从张量到计算图从前向传播到梯度下降每一步都用可视化的方式呈现。你手里现在有一套能跑的手写代码脑子里有一组能画出来的图接下来无论是继续深入数学推导还是直接上手框架做项目都有了扎实的落脚点。我个人在实际教学中最深的体会是那些能自己画出计算图的人后面学什么都快因为他们不是在记公式而是在理解数据怎么流动。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询