ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CNN 卷积神经网络——从卷积核到手写数字识别

2026/10/5 9:03:41 拓冰建站 浏览量
CNN 卷积神经网络——从卷积核到手写数字识别 个人主页 for_ever_love__ 欢迎各位大佬莅临其他栏目: 我想学python了 其他栏目: iOS项目总结大全 其他栏目: iOS UI 文章目录CNN 卷积神经网络——从卷积核到手写数字识别一、全连接层的致命缺陷二、卷积到底在做一件事找特征1. 卷积核滑动示意2. 三个关键超参数3. 权值共享参数量为什么炸不了三、动手实现先看 numpy 版再看 PyTorch 版四、五个高频踩坑五、经典网络演进一览六、小结CNN 卷积神经网络——从卷积核到手写数字识别一、全连接层的致命缺陷假设你要处理一张 224×224 的彩色图片做图像分类。如果直接用全连接层每个像素连到下一层的每个神经元第一层假设有 1000 个神经元参数量是多少输入维度224 × 224 × 3 150,528 参数量 150,528 × 1000 ≈ 1.5 亿1.5 亿个参数只为了第一层。这还只是个小网络。这么大的参数量带来三个问题显存爆炸光存参数就要几百 MB训练时的梯度、优化器状态更是翻倍必然过拟合参数量远超实际数据量模型会把训练集背下来丢失空间结构把图片拉平成一维向量后左上角第 3 个像素和它右边那个像素的相邻关系彻底消失了——而相邻关系恰恰是图像最重要的信息CNN 用两个核心思想解决了这些问题局部感受野和权值共享。一句话概括人眼识别某个物体时不需要一次看完整张图而是先看局部的边缘、纹理再逐层组合成高级语义。CNN 就是在模仿这个过程。二、卷积到底在做一件事找特征1. 卷积核滑动示意想象一个 3×3 的小窗口卷积核在图片上从左到右、从上到下滑动每停一次就做一次对应位置相乘再求和输入图片5×5 卷积核3×3 输出特征图3×3 ┌──┬──┬──┬──┬──┐ ┌──┬──┬──┐ │ 1│ 1│ 1│ 0│ 0│ │ 1│ 0│-1│ 第一步算左上角 ├──┼──┼──┼──┼──┤ ├──┼──┼──┤ 1×1 1×0 1×(-1) │ 0│ 1│ 1│ 1│ 0│ * │ 1│ 0│-1│ ───────▶ 0×1 1×0 1×(-1) ├──┼──┼──┼──┼──┤ ├──┼──┼──┤ 0×1 0×0 1×(-1) │ 0│ 0│ 1│ 1│ 1│ │ 1│ 0│-1│ 0 0 - 1 0 0 - 1 0 0 - 1 -3 ├──┼──┼──┼──┼──┤ └──┴──┴──┘ │ 0│ 0│ 1│ 1│ 0│ ├──┼──┼──┼──┼──┤ │ 0│ 1│ 1│ 0│ 0│ 这个卷积核的特点左列全 1、右列全 -1 └──┴──┴──┴──┴──┘ → 它专门找左亮右暗的竖直边缘这就是卷积的本质卷积核是一种特征探测器。上面这个核会强烈响应竖直边缘因为它左右两列一正一负——如果图片里存在从左到右由亮变暗的跳变输出值就会很大。至于为什么叫卷积数学上的卷积要先把核翻转 180°而深度学习里大家都不翻转其实是互相关但约定俗成继续叫卷积。2. 三个关键超参数参数作用常见取值kernel_size核大小决定每次看多大范围3×3最主流、5×5、7×7stride步长决定每次挪多远1保细节、2做下采样padding边缘补零控制输出尺寸是否会缩小0、1same padding输出尺寸计算公式出问题的时候第一个就该算它H_out (H_in 2×padding - kernel_size) / stride 1举例输入 32×32kernel3padding1stride1 → (322-3)/11 32尺寸不变。这就是所谓的same padding。3. 权值共享参数量为什么炸不了同样是找边缘图片的左上角和右下角用的是同一个卷积核。这就是权值共享——一个 3×3 的核从头到尾只有 9 个参数外加 1 个 bias不管图片多大。对比一下方案参数量说明全连接输入维度 × 输出神经元数随图片尺寸平方增长卷积kernel_h × kernel_w × in_channels × out_channels与图片尺寸完全无关一个 3×3 卷积处理 64 通道输入、输出 128 通道3×3×64×128 ≈ 7.4 万参数比全连接的 1.5 亿少了三个数量级。三、动手实现先看 numpy 版再看 PyTorch 版理解原理最好的方式是亲手实现一次二维卷积。下面这段代码不依赖任何深度学习框架importnumpyasnpdefconv2d_numpy(image,kernel,stride1,padding0): 手写二维卷积单通道版本理解 sliding window 用 image: (H, W) kernel: (kH, kW) ifpadding0:imagenp.pad(image,pad_widthpadding,modeconstant,constant_values0)H,Wimage.shape kH,kWkernel.shape H_out(H-kH)//stride1W_out(W-kW)//stride1outputnp.zeros((H_out,W_out))foriinrange(H_out):forjinrange(W_out):# 取出当前窗口与核逐元素相乘后求和h_start,w_starti*stride,j*stride windowimage[h_start:h_startkH,w_start:w_startkW]output[i,j]np.sum(window*kernel)returnoutput# 构造一张左半边亮、右半边暗的测试图中间有一条竖直边缘imgnp.zeros((6,6))img[:,:3]10img[:,3:]0# 竖直边缘检测核vertical_edgenp.array([[1,0,-1],[1,0,-1],[1,0,-1]])resultconv2d_numpy(img,vertical_edge,stride1,padding0)print(输入图片\n,img)print(\n卷积结果\n,result)print(f\n输出尺寸:{result.shape})运行结果输入图片 [[10. 10. 10. 0. 0. 0.] [10. 10. 10. 0. 0. 0.] ...共 6 行 卷积结果 [[ 0. 30. 0. 0.] [ 0. 30. 0. 0.] [ 0. 30. 0. 0.] [ 0. 30. 0. 0.]] 输出尺寸: (4, 4)看到中间那一列醒目的30了吗那正是 pic 里从第 3 列跳到第 4 列的竖直边界。卷积核精准地点亮了边缘位置——这就是 CNN 第一层在做的事。真实项目当然用框架。下面是 PyTorch 版本的完整 CNN跑 MNIST 手写数字识别importtorchimporttorch.nnasnnimporttorch.nn.functionalasFfromtorchvisionimportdatasets,transformsfromtorch.utils.dataimportDataLoader# 1. 数据预处理注意 ToTensor 会把像素值从 [0,255] 缩到 [0,1]transformtransforms.Compose([transforms.ToTensor(),transforms.Normalize((0.1307,),(0.3081,)),# MNIST 的均值和标准差])train_setdatasets.MNIST(./data,trainTrue,downloadTrue,transformtransform)test_setdatasets.MNIST(./data,trainFalse,downloadTrue,transformtransform)train_loaderDataLoader(train_set,batch_size64,shuffleTrue)test_loaderDataLoader(test_set,batch_size1000,shuffleFalse)# 2. 定义 CNN卷积 → 池化 → 卷积 → 池化 → 全连接classSimpleCNN(nn.Module):def__init__(self):super().__init__()self.conv1nn.Conv2d(1,32,kernel_size3,padding1)# 1→32 通道self.conv2nn.Conv2d(32,64,kernel_size3,padding1)# 32→64 通道self.poolnn.MaxPool2d(2,2)# 2×2 池化尺寸减半self.dropoutnn.Dropout(0.5)self.fc1nn.Linear(64*7*7,128)# 关键28→14→7两次池化后是 7×7self.fc2nn.Linear(128,10)# 10 类数字defforward(self,x):xself.pool(F.relu(self.conv1(x)))# (1,28,28) → (32,14,14)xself.pool(F.relu(self.conv2(x)))# (32,14,14) → (64,7,7)xtorch.flatten(x,1)# 展平成 (batch, 64*7*7)xF.relu(self.fc1(x))xself.dropout(x)returnself.fc2(x)# 最后一层不加激活交给 CrossEntropyLossmodelSimpleCNN()optimizertorch.optim.Adam(model.parameters(),lr1e-3)criterionnn.CrossEntropyLoss()# 3. 训练循环deftrain(epochs3):model.train()forepochinrange(epochs):total_loss0.0forbatch_x,batch_yintrain_loader:optimizer.zero_grad()# ① 清零梯度忘了必炸outmodel(batch_x)# ② 前向losscriterion(out,batch_y)# ③ 算损失loss.backward()# ④ 反向传播optimizer.step()# ⑤ 更新参数total_lossloss.item()# 每个 epoch 后评估一次model.eval()# ⑥ 切到评估模式关掉 dropoutcorrect0withtorch.no_grad():# 评估时不需要计算梯度省显存fortx,tyintest_loader:predmodel(tx).argmax(dim1)correct(predty).sum().item()acccorrect/len(test_set)print(fEpoch{epoch1}/{epochs}| loss{total_loss/len(train_loader):.4f}| 测试准确率{acc:.4f})model.train()train(3)典型输出CPU 上 3 个 epoch 约 2-4 分钟Epoch 1/3 | loss0.1532 | 测试准确率0.9812 Epoch 2/3 | loss0.0521 | 测试准确率0.9885 Epoch 3/3 | loss0.0374 | 测试准确率0.9907一个手写的两层 CNN 就能在 MNIST 上做到99% 准确率这就是卷积结构的威力。四、五个高频踩坑坑现象解决全连接层输入维度算错RuntimeError: mat1 and mat2 shapes cannot be multiplied用公式反推每层的 H/W或先print(x.shape)看实际尺寸忘记flatten卷积输出是 4 维直接喂全连接报错torch.flatten(x, 1)保留 batch 维通道顺序搞混图像颜色错乱PyTorch 是NCHWnumpy/OpenCV 是HWC用permute转换输入没归一化loss 不降、训练震荡Normalize到均值 0 附近加速收敛忘了model.eval()测试准确率莫名比训练低很多评估前切 evaltorch.no_grad()包住推理其中维度计算是新手最大的噩梦。记住这条链式推导习惯写法输入 28×28 conv(padding1) → 28×28 [(282-3)/11 28] pool(2) → 14×14 conv(padding1) → 14×14 pool(2) → 7×7 flatten → 64×7×7 3136不确定时就print(x.shape)比猜快十倍。五、经典网络演进一览网络年份核心贡献Top-5 错误率LeNet-51998CNN 开山之作用于支票识别—AlexNet2012ReLU Dropout GPU 训练深度学习复兴15.3%VGG2014全部用 3×3 小卷积堆叠结构规整7.3%GoogLeNet2014Inception 多尺度并行6.7%ResNet2015残差连接解决深层网络退化问题3.6%ResNet 的残差连接值得单独说一句网络深到一定程度后再加层反而让训练误差变大。何恺明等人的解法是加一条捷径让层学习残差而不是直接学习目标映射输入 x ─────────────┐ │ │恒等映射直接加到输出 ▼ │ 权重层 F(x) ────────┴──→ 输出 H(x) F(x) x这样一来即使某层学不到有用特征让 F(x) → 0 就能退化成恒等映射至少不会比不加这层更差。这个朴素的思想把网络深度从几十层推到了上千层。六、小结CNN 解决的三个问题参数量爆炸、丢失空间结构、必然过拟合靠的是局部感受野 权值共享卷积核本质是特征探测器固定的一组权重专门抽出某种边缘/纹理模式尺寸公式(H 2p - k) / s 1必须手算得出来不然搭网络时会卡死PyTorch 搭 CNN 的套路卷积(padding1) → 池化(/2) → 重复 → flatten → 全连接数据维度是 NCHW归一化、eval 模式、no_grad这三个忘了就会得到奇怪的结果下一篇我会接着讲 CNN 的实战进阶数据增强、迁移学习以及如何用预训练模型在几百张图的小数据集上做出可用的分类器。一句话总结全连接记住的是像素的位置卷积学到的是特征的模式。