发布时间:2026/9/2 16:15:51
纯Python手写CNN:从零实现卷积、池化与反向传播 简介手写实现的二维卷积神经网络2D CNNPython代码面向希望深入理解CNN底层原理的Python学习者与图像处理入门者适合在PyCharm中运行调试。资源共2个文件包括一个.py源码文件和一个.docx实验报告压缩包仅24KB代码轻量精炼便于逐行研读。源码手动实现卷积层、激活函数、池化层、批量归一化、全连接层、损失函数、优化器以及前向/反向传播流程而非直接调用高层框架API能让读者清楚看到每个参数如何参与计算实验报告则补充了网络结构设计、超参数设置与训练验证步骤帮助排查过拟合、收敛慢等常见问题。当前已有895人下载学习适合用于课程实验、毕业设计或自学补充材料。 最近在一个项目里需要把模型从PyTorch部署到嵌入式环境光依赖框架不行逼着我把网络里的每一层都重新实现一遍。索性用纯Python加NumPy手写了一个二维卷积神经网络CNN卷积、池化、全连接、反向传播、训练循环全都有没有用任何深度学习框架。这篇文章把完整过程写下来整体设计思路、每层代码实现、训练调试方法以及踩过的坑希望对正在学深度学习原理、或者准备面试想脱离框架黑盒的朋友有帮助。手写CNN的最大价值不是为了替代PyTorch或TensorFlow而是让你真正理解卷积层、池化层的输入输出到底怎么流动梯度怎么从loss一路传回第一层卷积为什么某些初始化策略会让网络训不动。这些东西在用框架的时候通常被封装得严严实实一旦模型效果不对你连排查的入口都找不到。1. 整体设计与思路拆解1.1 卷积运算的计算本质二维卷积神经网络里最核心的运算就是卷积。深度学习框架里的卷积层实际做的是互相关cross-correlation跟数学课本里的卷积差一个卷积核翻转。因为滤波器的权重是训练学出来的翻转与否并不会影响表达能力所以框架全用互相关实现。你只需要记住一个朴素的图景一个小的滑动窗口卷积核在输入特征图上按步长移动每次做逐元素相乘再求和得到一个输出像素。假设输入尺寸是H×W卷积核大小是K填充padding是P步长stride是S那么输出尺寸由下面这个公式决定H_out (H 2P - K) / S 1 W_out (W 2P - K) / S 1这是手写CNN时必须刻在脑子里的公式几乎所有的维度报错都跟它有关。padding的作用是保留边缘信息stride控制下采样的力度。多通道输入的情况也简单输入是C个通道就有C个二维kernel分别跟对应通道做卷积再把C个结果加起来得到一个输出通道。输出有几个通道就对应几组这样的kernel。1.2 网络骨架设计我实现的是一个经典的小型CNN结构专门用于MNIST手写数字识别Conv(1→8, 3×3) → ReLU → MaxPool(2×2) → Conv(8→16, 3×3) → ReLU → MaxPool(2×2) → Flatten → Dense(256→10)选择这种结构有明确理由。MNIST是28×28的单通道灰度图其笔画特征比较简单两层卷积足以提取局部边缘和组合模式池化层把特征图从28×28降到14×14再到7×7既减少计算量也给全连接层缩小参数量。全连接层放在最后做分类输出的10维向量对应0到9十个数字。实现时我刻意不用im2col这类工程优化技巧而是用最直白的滑窗四重循环。虽然慢但每一个维度、每一次乘加都跟公式对得上理解完再去做向量化优化思路会清晰得多。2. 核心模块代码实现2.1 Conv2D层的正向传播与反向传播先看Conv2D层的完整实现。初始化时采用He初始化权重初始化为均值为0、方差为2/(fan_in)的正态分布这样做是为了配合ReLU激活函数避免梯度消失或爆炸。import numpy as np class Conv2D: def __init__(self, in_channels, out_channels, kernel_size, stride1, padding0): self.in_channels in_channels self.out_channels out_channels self.kernel_size kernel_size self.stride stride self.padding padding fan_in in_channels * kernel_size * kernel_size scale np.sqrt(2.0 / fan_in) self.W np.random.randn(out_channels, in_channels, kernel_size, kernel_size) * scale self.b np.zeros(out_channels) self.cache None self.dW None self.db None def forward(self, x): # x形状: (N, C_in, H, W) N, C, H, W x.shape K self.kernel_size P self.padding S self.stride H_out (H 2 * P - K) // S 1 W_out (W 2 * P - K) // S 1 x_pad np.pad(x, ((0, 0), (0, 0), (P, P), (P, P)), modeconstant) out np.zeros((N, self.out_channels, H_out, W_out)) for i in range(H_out): for j in range(W_out): h_start i * S w_start j * S x_patch x_pad[:, :, h_start:h_start K, w_start:w_start K] for f in range(self.out_channels): out[:, f, i, j] np.sum( x_patch * self.W[f, :, :, :], axis(1, 2, 3) ) self.b[f] self.cache (x, x_pad) return out前向传播的逻辑就是滑窗加乘累加。把输入pad好之后对每个输出位置取一个patch跟每个卷积核做逐元素乘法再求和加上偏置。反向传播是手写CNN最绕的部分。Conv2D层需要回传三个东西权重梯度dW、偏置梯度db、以及往上层传的输入梯度dx。观察前向公式可以发现梯度计算并不复杂def backward(self, dout, lr): x, x_pad self.cache N, C, H, W x.shape K self.kernel_size P self.padding S self.stride H_out (H 2 * P - K) // S 1 W_out (W 2 * P - K) // S 1 dx_pad np.zeros_like(x_pad) self.dW np.zeros_like(self.W) self.db np.zeros_like(self.b) for f in range(self.out_channels): self.db[f] np.sum(dout[:, f, :, :]) for i in range(H_out): for j in range(W_out): h_start i * S w_start j * S x_patch x_pad[:, :, h_start:h_start K, w_start:w_start K] for f in range(self.out_channels): d dout[:, f, i, j] self.dW[f, :, :, :] np.sum( x_patch * d[:, None, None, None], axis0 ) dx_pad[:, :, h_start:h_start K, w_start:w_start K] ( self.W[f, :, :, :] * d[:, None, None, None] ) self.dx dx_pad[:, :, P:P H, P:P W] self.W - lr * self.dW self.b - lr * self.db return self.dx反向的三个更新各自有明确含义。dW是输入patch和输出梯度相乘后累加db是输出梯度通道维度求和dx是把权重和梯度相乘后再累加回输入。这里有一个很容易踩的认知坑很多教程会画一大段推导证明卷积反向传播需要把kernel旋转180度但那是针对数学上的严格卷积。深度学习层的卷积层本身做的是互相关反向时权重不需要翻转。我第一次实现时照搬资料里的旋转操作梯度检查一直对不上去掉之后才通过。2.2 池化层与全连接层的实现池化层没有参数不需要学习但要记住前向时最大值的位置反向时才可以把梯度原样放回到那个位置其他位置填零。这就是MaxPool反向传播的全部逻辑class MaxPool: def __init__(self, pool_size2, stride2): self.pool_size pool_size self.stride stride def forward(self, x): N, C, H, W x.shape P self.pool_size S self.stride H_out (H - P) // S 1 W_out (W - P) // S 1 out np.zeros((N, C, H_out, W_out)) self.x x self.mask np.zeros_like(x) for i in range(H_out): for j in range(W_out): h_start i * S w_start j * S patch x[:, :, h_start:h_start P, w_start:w_start P] out[:, :, i, j] np.max(patch, axis(2, 3)) return out def backward(self, dout): dx np.zeros_like(self.x) N, C, H, W self.x.shape P self.pool_size S self.stride H_out (H - P) // S 1 W_out (W - P) // S 1 for i in range(H_out): for j in range(W_out): h_start i * S w_start j * S for n in range(N): for c in range(C): patch self.x[n, c, h_start:h_start P, w_start:w_start P] h_idx, w_idx np.unravel_index(np.argmax(patch), (P, P)) dx[n, c, h_start h_idx, w_start w_idx] dout[n, c, i, j] return dx严格来说mask可以在forward阶段就存下来backward直接查表不用再到原输入里找最大值位置。我代码里保留了两种写法倾向实际上存mask更高效但直接在backward里重新计算最大值位置更省内存两种都能跑通。全连接层就是普通的线性变换加反向传播矩阵运算比卷积层简单很多class Dense: def __init__(self, in_features, out_features): scale np.sqrt(2.0 / in_features) self.W np.random.randn(in_features, out_features) * scale self.b np.zeros(out_features) def forward(self, x): self.x x return x self.W self.b def backward(self, dout, lr): dx dout self.W.T self.dW self.x.T dout self.db np.sum(dout, axis0) self.W - lr * self.dW self.b - lr * self.db return dx线性层反向传播的核心就是两条链式法则对输入求梯度是拿dout乘W转置对权重求梯度是拿输入转置乘dout。有了dW之后用梯度下降更新参数即可。3. 训练一个能用的CNN3.1 数据准备与参数初始化在MNIST上做实验数据加载和预处理直接决定最终能跑多快、能收敛到什么水平。我用的方式是from sklearn.datasets import fetch_openml mnist fetch_openml(mnist_784, version1, as_frameFalse) X mnist.data.astype(np.float32) / 255.0 y mnist.target.astype(np.int64) X X.reshape(-1, 1, 28, 28) train_x, test_x X[:60000], X[60000:] train_y, test_y y[:60000], y[60000:]注意两个细节。第一个是归一化像素值从0到255缩放到0到1。如果不做这一步输入范围太大第一层卷积输出的数值会很大ReLU之后更容易梯度爆炸损失直接变成NaN。第二个是reshape成(N, 1, 28, 28)保留通道维后续卷积层才能按四维张量处理。参数初始化用的是He初始化。对ReLU网络来说Xavier初始化按fan_in方差放缩会让方差偏小层数一多输出趋近于零。He初始化把方差放大到2/fan_in让前向传播的信号和反向传播的梯度在深层网络里保持一个合适的尺度。全连接层我也沿用这个策略。超参方面我选择batch size 64、学习率1e-3训练10个epoch。这个组合在MNIST这种小数据集上相当稳定。学习率再大一点比如1e-2损失会在前几步疯狂震荡后面即使降下来准确率也上不去。3.2 训练循环与数值梯度检查有了数据就可以组装训练循环。软目标损失用Softmax加交叉熵前向输出后计算loss反向时把梯度从最后一层往回传def softmax(x): e np.exp(x - np.max(x, axis1, keepdimsTrue)) return e / np.sum(e, axis1, keepdimsTrue) def cross_entropy_loss(y_pred, y_true): N y_pred.shape[0] eps 1e-12 return -np.sum(np.log(y_pred[np.arange(N), y_true] eps)) / N def softmax_backward(y_pred, y_true): N y_pred.shape[0] dout y_pred.copy() dout[np.arange(N), y_true] - 1 return dout / N def relu(x): return np.maximum(0, x) def relu_backward(dout, x): dx dout.copy() dx[x 0] 0 return dx # 网络初始化 conv1 Conv2D(1, 8, 3, stride1, padding1) pool1 MaxPool(2, 2) conv2 Conv2D(8, 16, 3, stride1, padding1) pool2 MaxPool(2, 2) dense Dense(16 * 7 * 7, 10) batch_size 64 lr 1e-3 for epoch in range(10): for i in range(0, len(train_x), batch_size): x_batch train_x[i:i batch_size] y_batch train_y[i:i batch_size] h1 conv1.forward(x_batch) a1 relu(h1) h2 pool1.forward(a1) h3 conv2.forward(h2) a2 relu(h3) h4 pool2.forward(a2) flat h4.reshape(batch_size, -1) logits dense.forward(flat) probs softmax(logits) loss cross_entropy_loss(probs, y_batch) dout softmax_backward(probs, y_batch) dout dense.backward(dout, lr) dout dout.reshape(h4.shape) dout pool2.backward(dout) dout relu_backward(dout, h3) dout conv2.backward(dout, lr) dout pool1.backward(dout) dout relu_backward(dout, h1) dout conv1.backward(dout, lr) acc evaluate(conv1, conv2, dense, test_x, test_y) print(fepoch {epoch 1}, loss: {loss:.4f}, test acc: {acc:.4f})这段代码训练10轮最终测试准确率可以稳定跑到95%左右。如果你想复现可以观察前几个epoch的loss变化正常情况下第一个epoch结束loss就能从2.3附近降到0.15以下如果看不到这个下降趋势说明代码某处有bug先别急着调超参先做梯度检查。数值梯度检查是手写反向传播代码的救命工具。原理很简单用极限定义去近似梯度和backward算出来的解析梯度做对比。我通常先在小batch比如2个样本上单步检查def numerical_grad_check(layer, x, dout, eps1e-7): layer.forward(x) layer.backward(dout, lr0) # lr0不更新参数 numeric_dW np.zeros_like(layer.W) for idx in np.ndindex(layer.W.shape): orig layer.W[idx] layer.W[idx] orig eps out_plus layer.forward(x) layer.W[idx] orig - eps out_minus layer.forward(x) layer.W[idx] orig # 这里需要把输出转成标量loss再比较一般取 sum(out * dout) numeric_dW[idx] np.sum((out_plus - out_minus) * dout) / (2 * eps) diff np.max(np.abs(numeric_dW - layer.dW) / (np.abs(numeric_dW) np.abs(layer.dW) 1e-8)) print(fgrad diff: {diff:.2e})如果相对误差在1e-7到1e-5数量级说明解析梯度是对的。我通常在两层网络只有一个Conv和一个Dense上先验证逻辑通了再扩展到完整模型。这个方法帮我抓出过不少size对不上的问题。4. 常见问题与排查技巧4.1 典型报错与维度匹配手写CNN遇到最多的问题就是维度对不上。下面这份是我总结的速查表基本覆盖了初写者会撞上的大部分情况错误现象原因解决办法np.dot维度不匹配卷积输出尺寸算错用维度公式逐层手算打印每层shape核对loss一直是NaN输入没归一化或学习率过大数据除以255学习率降到1e-3梯度检查diff很大反向传播里kernel被旋转确认用的是互相关反向不转kernel池化反向得到全零梯度最大值位置记录错误打印argmax索引和原图比对训练loss降不下来初始化scale太小改用He初始化scale放大到sqrt(2/fan_in)尤其要强调第一行的维度检查。我在训练循环的关键节点都会加一行print(x.shape)从输入到输出每一层都打印一遍跟手算的结果对得上再往下走。这种每步验证的习惯能省下大量调试时间。4.2 训练不收敛的排查方法训练不收敛时我有一套固定的排查顺序。第一步先看loss是不是NaN如果是基本可以断定是输入范围太大或者学习率过高。第二步看loss的震荡幅度如果相邻几步的loss跳来跳去多半是学习率太大把1e-3降到1e-4试试。第三步看初始化的scale如果第一层输出全是一个常数反向时梯度全是零那多半是初始化sigma太小导致ReLU把神经元全关死了。这里有个小经验交叉熵softmax层的反向梯度是probs减去one-hot再除以batch_size。很多人在这一步忘记除以N导致梯度比真实值大N倍表现是刚开始训练时loss下降很快然后突然发散。我一开始也踩过加了那个除法之后训练就从过山车变成了滑滑梯。4.3 性能优化方向纯循环版在CPU上跑MNIST一个epoch大概要几分钟做研究肯定不行但理解原理完全够用。想进一步提速主要路径是把卷积运算换成矩阵乘法学名叫im2col。思路是把每个滑窗patch展开成一行所有patch堆成一个二维矩阵然后把卷积核也展开成矩阵一次矩阵乘就完成所有位置的计算。数据量翻了不少但线性代数库的矩阵乘高度优化实际速度可以快几十倍。在确保逻辑正确之后我还会用向量化把循环层数减少比如对batch维度、对输出通道维度的循环都可以用广播技术去掉。最终版本在CPU上跑MNIST的一轮加batch训练可以做到秒级和纯循环版正确率接近这就验证了向量化没有引入逻辑错误。5. 写在最后的体会这个手写CNN的项目做完之后我最大的收获不是终于写出了一个能识别的网络而是对框架里的每个参数都有了感觉。改学习率、换初始化、调卷积核大小时我不再是盲猜而是能大致猜出梯度会怎么流动、哪一层会先出问题。写框架代码时梯度检查这个习惯也被我保留了下来每次自定义网络结构都先跑一遍数值梯度验证再进训练循环效率反而比直接训练高得多。如果你也在学深度学习强烈建议找个小数据集把网络手写一遍。过程会有点折腾但过了这一关理解模型的视角会完全不一样。本文还有配套的精品资源点击获取

相关新闻

2026/9/2 16:15:51

WMM2020COF地磁模型系数文件解析:从球谐展开到工程实现

简介:这份压缩包为WMM2020地磁模型的CGGM计算工具包,面向钻井、物探等需要高精度地磁参考的工程师与科研人员,用于解决2020年地磁模型系数解析、磁场分量计算及数据格式转换等问题。内含7个文件,包括3个MATLAB脚本、3个TXT数据文本…

2026/9/2 16:15:51

兰博基尼Temerario对比911 Turbo S:马力之外的门道

把新兰博基尼 Temerario 和保时捷 911 Turbo S 放到同一个对比里,大多数人最先看到的数字差是马力:Temerario 综合输出接近 920 马力,911 Turbo S 是 650 马力。只看这个差距,好像没什么可聊的。但只要你真的研究过两台车的定位、…

2026/9/2 16:15:51

单片机毕设选题推荐:基于 STM32 的 App 远程环境加湿供水监控平台设计 基于 STM32 传感器的室内环境智能监测与自动调控系统设计(011606)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/9/2 16:25:53

超越承重:铸铁台选型评估框架与工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/2 16:25:53

免安装版JDK 1.8下载、环境变量配置与Docker部署实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/2 16:25:53

Multisim仿真入门:三极管共射极放大器设计与调试全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/2 16:25:53

国产大模型代码生成能力实测:从TodoList案例看工程化差异

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/2 16:20:51

TypePHP开源:PHP原生编译器如何重塑部署工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/1 16:02:17

vSound小提琴数字处理器实操指南:从接线到演出的完整配置

电小提琴或者原声小提琴插电演出,第一个绕不开的坎就是声音难听。原声琴的共鸣和空气感一旦进了拾音器,出来的往往是一坨干瘪、发尖、带着奇怪塑料味的信号。我当初第一次把琴接上乐队调音台,直接被主唱吐槽"你这声音像在锯钢丝"。…

2026/9/2 9:00:32

传感器接口IC如何攻克生物化学传感的微弱信号难题?

1. 从电极到比特流:为什么生物化学传感必须依赖专用接口IC 做生物化学传感的人都有过类似的经历:明明传感器本身性能很好,信号输出却一塌糊涂——噪声大、漂移明显、重复性差,怎么调都达不到预期。很多时候问题并不在传感器&#…

2026/9/2 8:41:06

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起,其实就是嵌入式开发里最常遇到的一类需求:用一块不算贵的 MCU,同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控,主频…

2026/9/2 0:03:41

单片机毕业设计-基于单片机与蓝牙通讯的输液状态监测终端设计与开发 基于 STM32 或 51 单片机的液位‑滴速‑温度多参数输液监护装置设计(024005)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/9/2 0:03:41

DeepSeek字幕翻译实战:从API调用到批量SRT转中文的完整方案

这次我们来看一个很实用的 DeepSeek 落地场景:用 DeepSeek 把英文视频字幕自动翻译成中文。具体案例是《恶魔君》1989 年第 28 集的英转中字幕任务,标题写得很直白,但背后其实是一整套可以复用的技术流程:字幕解析、模型调用、批量…

2026/9/2 0:03:41

用Python搭建搞笑语音助手:从语音识别到语音合成全教程

当你家里摆着一台天猫精灵,却总希望语音助手偶尔“不正经”一点,不用官方腔回答问题,而是张口就接几句搞笑段子,会是什么体验?我最近动手验证了一下这个想法——没有去改装任何市面上现有的智能音箱,而是直…

2026/9/2 1:15:22

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/2 1:15:22

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/2 1:15:20

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…