资讯动态

从像素矩阵到高维向量:纯NumPy手写前馈神经网络

发布时间:2026/10/26 12:49:27 来源:尧图企业网站定制
图像到底是怎么喂给神经网络、又是怎么一步步变成那个高维度向量的这个疑问我在后台收到了无数次。很多读者学完了前面的基础理论矩阵乘法也懂了反向传播公式也能推导但一到图像分类实战就卡住一张图片明明是二维的像素点凭什么能被模型识别成猫或者狗中间那几百个浮点数到底代表什么这一篇我们就围绕这个问题做一次完整的实战拆解——从前馈神经网络的视角把图像从像素矩阵到特征向量再到分类输出的全过程走一遍。代码继续沿用系列一贯的原则只用纯 Python NumPy不依赖任何深度学习框架。这样虽然实现细节多一些但每一步的矩阵长什么样、梯度从哪里来都清清楚楚。先说明这篇的定位。如果你已经跟着这个系列读到了第十二篇说明反向传播、激活函数、损失函数这些底子已经打过了。今天更像一次串糖葫芦式的综合实战把那些零散知识点串成一条完整链路数据预处理 - 网络结构设计 - 前向传播 - 反向传播 - 训练调参 - 问题排查。如果你是刚点开这篇的新读者也不用慌我会尽量把每一步的前因后果解释清楚你只需要带着一个疑问往下读一张 28x28 的灰度图究竟是如何变成一个 10 维向量的1. 图像的本来面目像素矩阵是唯一的入口1.1 一张图在计算机里就是一堆数字先把最底层的概念钉死计算机不认识图像只认识矩阵。拿常见的 MNIST 手写数字数据集举例每一张图都是 28 像素宽、28 像素高、单通道的灰度图。读进内存后它就是一个形状为 (28, 28) 的二维数组每个元素是 0 到 255 之间的整数0 代表纯黑255 代表纯白中间值是深浅不同的灰。彩色图像则是三维数组形状是 (高, 宽, 通道数)通道数一般为 3对应红、绿、蓝三个颜色通道。大家看热词里频繁出现图像进入神经网络到输出高维度向量这个入口就是矩阵。把矩阵想象成一块由数字构成的马赛克拼图神经网络能做的只是在这块拼图上做各种线性变换和非线性变换从来没真正看见过图像。有个容易混淆的点是通道顺序。OpenCV 读图默认是 BGR 顺序也就是蓝色通道在前而日常处理时我们通常希望是 RGB。如果直接把 OpenCV 读出来的图喂给按 RGB 顺序训练的网络结果会非常诡异——颜色信息全乱了。所以读图之后第一件事往往是cv2.cvtColor(img, cv2.COLOR_BGR2RGB)做通道转换。这一点在热词里反复出现python下载cv2、python安装cv2很多初学者装完库就直接读图跑模型最后发现准确率上不去排查半天其实是通道顺序错了。1.2 预处理不是走形式是在给梯度修路原始像素值 0~255 直接送进网络在数学上不是不行但会产生一个实际痛点数值范围太大梯度计算容易不稳定。我们看权重更新公式w w - lr * grad。如果输入特征是几百量级的数字那么矩阵乘法的结果可能也是几百量级经过激活函数之后梯度要么大得离谱导致发散要么因为某些激活函数饱和区梯度极小导致学不动。常见的处理手段是归一化。把像素值从 0~255 映射到 0~1 区间直接除以 255.0 就行更进一步还可以做标准化让数据均值为 0、标准差为 1。说白了归一化是给梯度下降修了一条平缓的路各个维度的数值量级一致权重更新时不会因为某个特征天然特别大而被带偏。我在实战中还习惯加上一步把二维矩阵拉平成向量。对于 28x28 的灰度图这一步就是img.reshape(1, 784)或者一次性处理大批量数据时用X.reshape(-1, 784)。很多初学者在这里犯迷糊为什么好好的二维矩阵要拉成一维因为标准的全连接网络前馈神经网络输入层要求的是一个一维特征向量每个像素就是一个特征维度。这个拉平操作等价于把图像的二维空间结构暂时丢弃这对全连接网络是必要的牺牲。注意预处理里有一类操作要特别小心——归一化统计量要单独用训练集计算不能把验证集或测试集的数据混进来一起算。否则相当于测试信息提前泄露给了模型验证结果会偏乐观上了真实环境准确率就露馅。2. 网络骨架怎么搭从输入层的 784 到输出层的 102.1 隐藏层宽度怎么定输入维度定为 784 是确定的——28x28 拉平之后就是这个数。输出维度定为 10 也是确定的——MNIST 有 0 到 9 共 10 个类别。卡在设计表中间的隐藏层到底几层每层多少个神经元这里没有绝对正确的标准答案但有一条工程经验可以分享先把隐藏层设置为单层、宽度取 64 或 128作为基线版本跑通全流程。等确认正向传播、反向传播、损失计算都准确无误再逐步加深加宽对比效果。一上来就搭一个 [784, 512, 256, 128, 10] 的五层大网万一效果不好你根本分不清是代码 bug 还是模型容量问题。我在初学阶段踩过这个坑五层网络跑出来的损失是 nan排查了整整半天最后发现只是初始化不当导致的梯度爆炸。对于手写数字这类简单任务单隐藏层 128 个神经元其实已经够用了。模型容量这个词听起来玄乎打个比方神经元数量就像做菜时手头的调料种类。调料太少很多味道调不出来欠拟合调料太多容易照着一道特定的菜死记硬背过拟合换个环境就抓瞎。2.2 权重矩阵的维度推导手把手算一遍这一节是整个实战里最值得盯着的部分。我们用 W1、b1 表示输入层到隐藏层的参数W2、b2 表示隐藏层到输出层的参数。输入样本 x 的形状是 (1, 784)矩阵乘法规则要求 W1 的形状必须是 (784, 128)。算出来的隐藏层输出形状就是 (1, 128)即z1 x W1 b1 # (1, 784) (784, 128) - (1, 128)这里的是 NumPy 的矩阵乘法运算符要求左边矩阵的列数等于右边矩阵的行数。W1 的 784 就是这么来的。b1 的形状是 (1, 128)广播机制会自动把偏置加到每一行上。隐藏层到输出层同理z2 的输出维度必须是 10所以 W2 的形状是 (128, 10)b2 是 (1, 10)。整体参数加起来算一下W1: 784 x 128 100352 个参数b1: 128 个参数W2: 128 x 10 1280 个参数b2: 10 个参数总计约 101770 个可学习参数。这个数字意味着在没有引入任何卷积结构的前提下模型需要通过训练从几万张图片中学习约十万个权重。对一台普通笔记本电脑来说CPU 跑几十个 epoch 也就是几分钟的量级完全可接受。参数初始化的方法我多说一句。常见做法是用小随机数np.random.randn生成标准正态分布后除以某个衰减系数。为什么要除因为如果权重初始值太大经过多层矩阵乘法后信号会指数级放大很容易导致激活函数进入饱和区梯度趋近于零。工程上常用的是配合激活函数来选择初始化的量级比如配合 ReLU 时可以用np.sqrt(2.0 / n_input)作为标准差。别小看这一步初始化对训练收敛速度的影响比大多数人想象的大得多。2.3 输出层的高维向量到底意味着什么很多人卡住的点来了输出层那 10 个数字是什么意思我先给结论网络输出的是一个形状为 (1, 10) 的向量里面每个元素是这张图属于对应类别的得分logit。经过 softmax 之后这些得分被转换成 10 个和为 1 的概率值。请注意在训练过程中我们不会直接把 softmax 后的概率拿去和标签做比较而是直接对 logits 做交叉熵损失计算。这么设计是因为 softmax 内部有 exp 运算如果先算概率再算 log 损失两步合并时数值上会有精度损失而且梯度表达式也能化简成非常优雅的形式这个等讲到反向传播再展开。向量里的第 0 个元素是是 0 的概率第 1 个元素是是 1 的概率以此类推。训练时标签做 one-hot 编码比如真实标签是 3那就构造向量 [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]。模型要做的就是不断调整那约十万个权重让网络对真实类别给出的得分尽可能高、对其他类别的得分尽可能低。这个高维向量的维度等于类别数——所以如果你做的是 1000 类图像分类最后一层就输出 1000 维向量如果做的是人脸识别里的特征提取输出层可能是一个 128 维的向量这个向量就是人脸特征的数学表示。我在之前写人脸识别相关的热词搜索里也看到类似的疑惑人脸识别图像进入神经网络到输出高维度向量的过程。其实核心链路和我们这里完全一样网络前几层提取像素特征中间层组合成抽象特征最后输出一个固定维度的向量。区别只在于训练目标——分类任务让向量不同维度的数值对应不同类别的得分人脸特征提取任务则是让同一个人的两张脸对应的两个向量在空间里距离更近。3. 前向传播与反向传播两步核心代码3.1 从输入到输出的一次完整旅行前向传播在纯 NumPy 里真的很直接代码长相如下def forward(x, W1, b1, W2, b2): z1 x W1 b1 a1 np.maximum(0, z1) # ReLU 激活 z2 a1 W2 b2 return z1, a1, z2这里有个小技巧训练时我们不仅需要最终的输出 z2还需要中间量 z1 和 a1 缓存下来因为反向传播要用到它们。很多初学者写 forward 只返回最终结果到写 backward 时才发现缺了中间量又得重算一遍。缓存中间结果这个习惯是从业者都会遵守的省下的不只是重复计算的算力更是排查 bug 时清晰的路线图。激活函数选 ReLU 而不是 sigmoid原因在于梯度特性。sigmoid 在两端的导数非常接近 0误差信号经过多层传播后会衰减到几乎消失这是梯度消失问题的根源。ReLU 在正区间导数恒为 1负区间导数恒为 0不存在饱和区信号可以比较顺畅地反向传播。但代价是负区间的神经元一旦输出为 0它的梯度就永远是 0对应的权重再也无法更新——这是神经元死亡问题。后面排查章节会再聊它。3.2 反向传播交叉熵的梯度到底是怎么简化的反向传播是整个系列里劝退率最高的部分但今天这套网络结构其实算起来很规整就两条链路。先说损失函数我们用交叉熵。定义式是L -log(softmax(z2)[0, y_true])意思是取 softmax 之后真实类别对应的那个概率值然后取负对数。真实类别概率越接近 1损失越接近 0概率越小损失越大。对整个输出向量做梯度推导最终结果非常优美softmax 加上交叉熵的组合对 z2 的梯度恰好就是模型预测的概率向量减去 one-hot 标签向量。da2 probs - y_onehot # 形状 (1, 10)为什么能化简成这么干净因为 softmax 求导后会出现自己对自己和自己对别人的分支而交叉熵里的 log 恰好把这些分支全部约掉了。这就是我当时说的两步合并的威力。推导过程我在系列前面某一篇详细展开过这里不再重复但每个入门者都值得自己完整推一遍推通了之后神经网络对你再无秘密。有了 da2后面就是纯粹的链式法则机械操作dW2 a1.T da2 # (128, 1) (1, 10) - (128, 10) db2 np.sum(da2, axis0, keepdimsTrue) da1 da2 W2.T # (1, 10) (10, 128) - (1, 128) dz1 da1 * (z1 0) # ReLU 的导数正区间为 1负区间为 0 dW1 x.T dz1 # (784, 1) (1, 128) - (784, 128) db1 np.sum(dz1, axis0, keepdimsTrue)注意 db 为什么用np.sum而不是直接等于某个矩阵。因为偏置 b 是广播到每个样本上的梯度回传时需要把所有样本的贡献加起来。这是初学者最容易写错的地方——觉得 db 应该是某个矩阵结果形状对不上。3.3 训练循环和参数更新参数更新的代码反而最简单W1 - lr * dW1 b1 - lr * db1 W2 - lr * dW2 b2 - lr * db2但训练循环这个整体设计里有个容易被忽略的选择每次更新梯度时用一个样本算还是用一批样本算这叫 batch size 的选择。用全量样本算出平均梯度再更新数学上最稳定但每轮更新都要跑完整数据集速度慢用单个样本更新速度快但梯度噪声大损失曲线会像心电图一样乱跳折中的 mini-batch 是主流做法比如一次取 64 个样本算平均梯度。批次越小梯度噪声越大越小越容易跳。批次越大梯度估计越准确但内存压力和计算开销同步上升。对于 MNIST 这种只有 784 个输入维度的简单任务全量更新其实也能接受。不过真实项目中数据量动辄几十万甚至上亿mini-batch 就是必选项了。我在这个系列里一直建议理解原理时先跑全量等逻辑验证通过再改成 mini-batch 也不迟。4. 收尾工作初始化、学习率与训练节奏4.1 初始化和学习率是最难调试的超参数神经网络训练里有一句老话模型结构决定上限优化过程决定能不能达到上限。而优化过程里初始化方式、学习率、批次大小这三个因素几乎决定了训练能否正常收敛。我见过不少同行花大量时间调网络结构最后发现瓶颈只是学习率设置不当。学习率太大损失函数会在最小值附近来回震荡甚至直接发散表现就是 loss 变成 nan 或者准确率在 10% 左右疯狂抖动。学习率太小每一步更新都小心翼翼训练几百个 epoch 还在原地踏步损失函数下降得肉眼看不见。一个粗暴有效的经验初次跑一个任务时把学习率定在 0.001 量级观察前几个 epoch 的损失曲线。如果损失在下降哪怕不快也说明方向对了如果发散减到 0.0001 再试如果几乎不动加大到 0.01。在这个网络里我常用的初始学习率是 0.01 配合全量梯度更新或者 0.001 配合 mini-batch。如果实现正确通常 5 个 epoch 左右训练集准确率就能到 95% 以上。如果 20 个 epoch 后训练集还不到 90%那大概率是代码哪里写错了别急着加网络宽度。4.2 训练过程中的心电图怎么盯训练过程不只是盯着最终准确率看中间过程信息量巨大。我习惯每隔几个 epoch 打印一次训练损失和验证集准确率。损失下降说明模型还在学准确率跟着上升说明学对了方向但如果损失的下降速度远快于准确率的上升速度就要警惕过拟合了——模型可能在死记训练样本的模式而不是泛化出通用规律。另一个直观经验是看损失曲线是否平滑。如果曲线剧烈震荡且始终不收敛优先调低学习率而不是怀疑数据有问题。如果损失曲线呈现阶梯状下降每过若干轮突然跌一块多半是学习率调度器的效果这属于正常现象。如果不做任何调度曲线一般是一条平滑下降的弧线后期逐渐走平。5. 踩坑实录六个最容易翻车的细节5.1 维度不匹配的时候怎么破案初学阶段最频繁的报错就是维度对不上。我的调试思路是在 forward 和 backward 的每一行中间都打印张量的 shape从输入一直看到输出确认每一步矩阵乘法都符合预期。这个手段虽然笨但非常可靠。工具方面推荐用 assert 断言初步排查例如assert a1.shape (1, 128)跑不过去立刻定位。等代码稳定后再删掉也不迟。5.2 死亡 ReLU隐藏层一半神经元罢工ReLU 的负区间梯度为 0这意味着一旦某个神经元在训练中落入负输出区间它的权重梯度永远为 0权重就再也无法被更新——这个神经元死了。表现是训练损失卡住不动但网络结构表面看起来完全正常。排查方法是打印隐藏层激活值的分布。如果数据集中某个 epoch 之后隐藏层输出大量为 0说明死亡 ReLU 发生了。诱因通常是学习率太大权重一步更新过猛把一大批神经元直接推到负区间。解法是降低学习率或者换用带泄漏的 ReLULeaky ReLU。5.3 损失变成 NaN 的三种原因NaN 是训练幺蛾子里最常见的一号选手。它的常见原因有三个学习率太大导致数值溢出softmax 内部 exp 运算遇到极大 logits 产生数值上溢标签错误导致 one-hot 向量错位。解决思路第一个直接降学习率第二个在计算 softmax 时给 logits 先减去最大值这是数值稳定性的经典技巧不影响最终概率结果第三个检查数据加载代码。5.4 数据顺序不洗牌模型学到的是排列规律如果训练时每次都按固定顺序喂数据模型可能不是在学图像特征而是在记忆第几号样本属于第几类的排列规律。这样训练集准确率可能虚高验证集一换顺序立刻打回原形。正确做法是每次 epoch 开始前把训练数据索引打乱mini-batch 再从打乱后的数据里抽。5.5 归一化参数只用训练集的别让测试集偷看答案这个前面提过但实际工程里真的很常见。我见过一个项目开发者在预处理时对所有数据一起做归一化结果离线测试准确率 98%上线后跌到 80% 出头最后排查发现就是归一化统计量混入了测试数据。生产环境里的图像分布和训练集有差异这件事本身已经够头疼了千万别在预处理环节再给自己挖坑。5.6 全连接网络的高维向量迷信最后说一个观念层面的坑。全连接网络对图像分类确实能跑通但图像的空间结构比如相邻像素之间的相关性在拉平操作中被抹掉了。这也是为什么复杂一点的图像任务大家会转向卷积神经网络——卷积天然保留空间邻接信息。这次我们从前馈网络角度把流程跑通明白图像 - 矩阵 - 特征向量 - 分类输出的全貌对后续理解卷积神经网络会有极大帮助。卷积核本质上做的事情和全连接一样都是在做特征提取 线性变换只是加上了只看局部的约束。6. 实测参数参考与效果预期以 MNIST 为例在普通家用电脑上用纯 NumPy 实现单隐藏层 128 神经元网络配合 mini-batch 64、学习率 0.01、交叉熵损失、ReLU 激活训练 20 个 epoch 左右验证集准确率大约在 94%~96% 区间。作为对照加上卷积结构后能轻松超过 99%但那是后面系列的话题。前馈网络能达到的这个数字对理解全链路这个目标来说合格了。如果想快速验证自己的代码是否有 bug有一个省事的妙招用极小数据集做训练。比如只取 100 张图训练 100 个 epoch如果模型能把损失降到接近 0说明前向传播和反向传播是自洽的如果损失居高不下代码大概率写错了。这个小技巧几乎可以推广到所有神经网络项目谁用谁知道。另外提一句从零开始这四个字有读者问我为什么不用 PyTorch / TensorFlow答案是只有亲手写过一遍权重矩阵的维度推导、亲自在反向传播推导上翻过车后面用框架时遇到报错才知道框架在帮自己做什么、哪一步出了什么问题。框架封装的越高级底层原理越容易被忽略但恰恰是这些被封装掉的东西决定了你能把模型调到多好。我个人的体会是当你能闭上眼睛在纸上画出 784 - 128 - 10 这个简单网络的全量前向传播和反向传播计算图时再看任何深度学习框架的文档、任何模型的代码实现都会觉得异常亲切。因为不管网络层数加到几十层、结构换成卷积还是 Transformer底层那几个核心操作——线性变换、非线性激活、损失函数、梯度回传一直都没有变过。这一篇到这里算是把图像输入到高维向量输出的全过程从零串了一遍。如果你正卡在某个维度报错或者损失不降的问题上不妨把代码里每个张量的 shape 全部打印出来一行行对着上面第 2.2 节的计算过程检查问题通常会自己现身。下一篇我们在这个基础上加入卷积结构到时候你会看到图像的空间信息是怎么被重新利用起来的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑