ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+TensorFlow构建CNN:MNIST分类从卷积到训练全解析

Python+TensorFlow构建CNN:MNIST分类从卷积到训练全解析 简介面向深度学习初学者的卷积神经网络实现指南基于Python语言并使用TensorFlow框架完整展示了从环境准备到模型训练的全过程。文档以手写数字识别任务为例围绕一个含两个卷积层、一个最大池化层、一个全连接层及Dropout层的网络结构展开细致讲解各层张量形状变化、滤波器尺寸、步长、填充方式以及ReLU和Softmax激活函数的选择。训练部分采用Adam优化器与交叉熵损失函数在MNIST数据集上验证模型准确率。文档后半部分还给出两组对比实验分别固定模型改变训练集样本量、固定数据改变卷积核数目分析二者对预测性能的差异并总结出“增大数据量比盲目增加卷积核更有效”的结论。资源为单个PDF文件体积仅56KB便于下载与快速查阅目前已有3760人学习下载适合刚入门人工智能、神经网络或计算机视觉的开发者参考。1. 这份 Python TensorFlow 的 CNN 代码实现把 MNIST 分类从卷积到训练一次说透市面上讲卷积神经网络原理的文章很多但能直接跑通、每一行都能对上概念的代码其实不多。这份 Python TensorFlow 写的 CNN 代码实现就是一个「裸奔版」卷积网络脚本输入 MNIST 手写数字经过两层 5×5 卷积、两层 2×2 最大池化、一层 512 维全连接、Dropout 和 Softmax最后用交叉熵加 Adam 完成十类分类。它最有用的部分是附带的两组对照实验训练集从 1000 扩到 100000、卷积核从 16/32 加到 32/64结论直接挑战「模型越宽越好」的直觉——数据量不够时加卷积核的收益十分有限。想入门深度学习、用代码验证 CNN 原理的人或做课程设计想直接改代码的人都能从这里拿到完整的可复现链路。2. 网络结构拆解从 784 维输入到 10 类输出每一层张量形状都算清楚2.1 卷积层5×5 卷积核如何扫描 28×28 灰度图MNIST 每张图是 28×28 的单通道灰度图展平后就是 784 个像素。代码里先用占位符把输入接住再 reshape 成 CNN 需要的四维张量。[-1, 28, 28, 1]里的-1表示批大小自动推导最后一维1是通道数彩色图这里会是 3。这个 reshape 是做卷积前最容易忽略的一步直接拿 784 维向量去卷积TensorFlow 会立刻报维度错误。def conv2d(x, W): # strides[1, 1, 1, 1]: 高、宽方向步长都是 1 # paddingSAME: 卷积后尺寸不变 return tf.nn.conv2d(x, W, strides[1, 1, 1, 1], paddingSAME) def max_pool_2x2(x): # 2×2 最大池化, 步长 2, 输出尺寸减半 return tf.nn.max_pool(x, ksize[1, 2, 2, 1], strides[1, 2, 2, 1], paddingSAME)这两个函数把卷积和池化封装成固定模式。strides有四个元素对应 batch、高、宽、通道四个方向卷积和池化时 batch 与通道方向都不动真正起作用的是中间两个。paddingSAME时5×5 卷积核扫过 28×28 的图输出还是 28×28如果换成VALID输出会缩成28 - 5 1 24相当于每卷积一次边缘信息就丢一圈池化后特征图会明显变小。再看第一层卷积核的定义# 第一层卷积核: 5×5, 输入 1 通道, 输出 16 个特征图 W_conv weight_variable([5, 5, 1, 16]) b_conv bias_variable([16]) h_conv tf.nn.relu(conv2d(x_image, W_conv) b_conv) h_pool max_pool_2x2(h_conv)权重用tf.truncated_normal初始化标准差 0.1截断是避免一开始就出现极端大权重偏置统一初始化为 0.1主要目的是防止 ReLU 神经元在训练初期就进入死区。16 个卷积核意味着同一张图会被扫描 16 遍每遍提取一种特征响应输出就是 16 张特征图。第二层[5, 5, 16, 32]的含义是输入通道从 1 变成 16在前一层的 16 张特征图基础上再做组合输出 32 张特征图。越靠后的卷积层越是在组合低级特征边缘、角点形成稍高一级的结构特征。有个容易看混的细节原代码里第一层变量叫W_conv第二层叫W_conv2命名区分度不高但不影响运行。我一般会改写成W_conv1/W_conv2方便做实验时替换卷积核数量。2.2 池化层、全连接层和 Dropout尺寸减半后怎么拉平经过第一层卷积 池化张量从 28×28×16 变成 14×14×16第二层再走一遍变成 7×7×32。最大池化的逻辑很简单在每个 2×2 窗口里取最大值。它保留的是「这个区域有没有特征响应」丢掉精确位置相当于给网络一定的平移不敏感能力同时计算量直接降到四分之一。MNIST 是 28×28 的小图两次池化后正好是 7×7这个数字全连接层要用改网络时最容易在这里对不上。# 全连接层: 把 7×7×32 拉平成 1568 维 W_fc weight_variable([7 * 7 * 32, 512]) b_fc bias_variable([512]) h_pool_flat tf.reshape(h_pool2, [-1, 7 * 7 * 32]) h_fc tf.nn.relu(tf.matmul(h_pool_flat, W_fc) b_fc)7 * 7 * 32 1568必须和池化后的张量严格一致。很多人改完卷积核数量忘了同步改这里报错信息会明确告诉你是reshape维度不匹配。tf.reshape里的-1同样是自动推导批大小W_fc的第一维 1568 是写死的只要前面任一层的输出通道或池化方式变了这里就要跟着算一遍。Dropout 是防过拟合的关键。代码里keep_prob是个占位符训练时喂 0.5测试时喂 1.0这个区别特别重要测试阶段如果把 0.5 传进去等于随机砍掉一半神经元预测结果每次都不一样出现过「同一个模型两次测试准确率差好几个点」的怪现象。Dropout 的机制是训练时随机把部分神经元的输出置零同时按保留比例缩放剩余输出保证整体的期望强度不变迫使网络不把赌注压在单个节点上。2.3 Softmax 输出与关键参数速查表最后一层把 512 维特征压缩成 10 类得分Softmax 把这些得分归一化成概率每一行加起来等于 1argmax取最大概率对应的索引就是预测数字。组件参数值说明输入图像尺寸28×28×1灰度单通道reshape 成四维张量卷积层 1卷积核 / 输出通道5×516SAME padding输出 28×28×16池化层 1窗口 / 步长2×2 / 2输出 14×14×16卷积层 2卷积核 / 输出通道5×532输入通道必须是 16池化层 2窗口 / 步长2×2 / 2输出 7×7×32全连接层维度1568→512ReLU 激活Dropoutkeep_prob训练 0.5 / 测试 1.0防止过拟合输出层维度512→10Softmax对应数字 0-9这套结构和 LeNet-5 一脉相承是 CNN 入门最经典的骨架。想改成彩色图只需要把x_image最后一维从 1 改成 3并把第一层卷积核定义里的输入通道同步改掉其余逻辑不用动。3. 训练闭环批次喂入、交叉熵损失和 Adam 优化器怎么配合3.1 数据读取MNIST 的 one_hot 标签与 next_batch 批次机制from tensorflow.examples.tutorials.mnist import input_data # one_hotTrue: 标签从整数变成 10 维向量, 比如 3 - [0,0,0,1,0,0,0,0,0,0] mnist input_data.read_data_sets(C:/Users/zhen/MNIST_data_bak/, one_hotTrue)read_data_sets会检查目录缺文件就自动下载。原代码里的路径是作者 Windows 机器上的绝对路径换一台机器大概率直接报错这个坑后面会单独讲。为什么非要 one_hot因为 Softmax 输出的是 10 维概率向量标签也必须是 10 维向量才能逐元素相乘算交叉熵。整数标签没法直接参与这个运算。输入和标签都用占位符定义x tf.placeholder(tf.float32, [None, 784]) # None 批大小不定 y tf.placeholder(tf.float32, [None, 10]) # one_hot 标签None是关键设计训练时一个批次可以塞 50 张测试时一次塞全部 10000 张占位符的形状不会因此报错。数据通过feed_dict在run时传入这是 TensorFlow 1.x 最典型的执行方式。3.2 交叉熵损失与 Adam这份代码里最值得改的两行cross_entropy tf.reduce_mean(-tf.reduce_sum(y * tf.log(y_conv), reduction_indices[1])) train_step tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)交叉熵的展开式有两层含义y * tf.log(y_conv)因为y是 one_hot 向量只有真实类别那一维是 1其余都是 0所以只计算真实类别对应的预测概率reduce_sum按行求和得到每个样本的交叉熵reduce_mean再对批次取平均得到最终损失。这个写法是手写展开教学上很清楚但数值上不够稳如果某个预测概率因为浮点溢出变成 0log(0)会直接算出NaN。我自己的习惯是教学代码保留这种展开式实际项目里换成tf.nn.softmax_cross_entropy_with_logits把 Softmax 和交叉熵合并计算数值稳定性好得多还能顺带去掉一层y_conv的中间存储。Adam 优化器的1e-4是初始学习率对 MNIST 这种小任务偏保守但很稳。想加快收敛可以提到1e-3试试但到训练后期容易在最优解附近抖动所以原代码选这个值是有道理的——它调出来的不是最快而是最不容易翻车。3.3 训练循环与测试评估20 步到底学了多少tf.global_variables_initializer().run() # TF1.x 显式初始化所有变量 for i in range(20): batch mnist.train.next_batch(50) # 每步抽 50 张 train_step.run(feed_dict{x: batch[0], y: batch[1], keep_prob: 0.5}) print(test accuracy %g % accuracy.eval( feed_dict{x: mnist.test.images, y: mnist.test.labels, keep_prob: 1.0}))关键点在第 67 行for i in range(20)只有 20 步每步 50 张一共只看过 1000 张图。MNIST 训练集有 55000 条20 步只过了约 1/55远远没学完。这就是为什么很多新手跑完这个脚本发现测试准确率只有 90% 上下和论文里动辄 99% 差一大截——不是代码写错了是训练量压根不够。测试时keep_prob必须给 1.0这一步是最容易被 Dropout 坑到的地方。准确率计算用的是argmax把预测概率最大的类别和真实标签的类别逐个比较相等记 1最后取平均。测试集 10000 张图一次性全部喂进去约 30MB 的 float 数据内存完全扛得住这也是 MNIST 这个数据集对新手友好的原因之一。注意原脚本里accuracy定义重复写了两行纯属复制粘贴不影响运行删掉一行更清爽。改训练量时记住这个换算关系训练步数 想看的总样本数 ÷ 批大小。想看 10000 条就改range(200)想看完整训练集一个 epoch 就改range(1100)。4. 两组对照实验训练数据量和卷积核数量谁更决定准确率4.1 实验一模型固定为 16/32 卷积核训练集从 1000 涨到 100000第一组实验保持网络结构完全不变第一层 16 个卷积核、第二层 32 个卷积核、全连接层 512 维、10 分类唯一变化的是训练样本量。1000 条对应 20 步10000 条对应 200 步100000 条对应 2000 步实现上只需要调整range里的数字和next_batch的调用次数。源实验记录里的结论很明确训练集数据量提升带来的准确率变化十分明显。样本从 1000 涨到 10000测试准确率会上一大截再涨到 100000还能继续往上走。这里有个背景知识需要说清楚标准 MNIST 训练集只有 55000 条100000 条的实验是怎么做出来的大概率是把测试集或验证集也混进训练或者对训练集做了重复采样。后者的本质其实是「加大训练步数」也就是让模型多看几遍同样的图。复现时不用死磕这个数字把步数等比放大就能得到等价效果。4.2 实验二训练样本不变卷积核从 16/32 加到 32/64第二组实验把第一层卷积核改成 32 个、第二层改成 64 个其余设置不动。源实验记录的结论是在训练集较小时一味增加卷积核数量对预测性能的提升十分有限。这里有一个很容易忽略的连锁反应第二层输出通道从 32 变成 64 后池化输出从 7×7×32 变成 7×7×64拉平维度从 1568 变成 3136全连接层的权重矩阵也跟着从 1568×512 膨胀到 3136×512。也就是说改两个数字不只是「加了点卷积核」整个网络的总参数量从约 82 万直接涨到约 166 万翻了一倍还多。在小数据集上容量暴涨带来的结果是网络更容易把训练样本里的细节噪声一起记住泛化能力不升反降这在机器学习里是再正常不过的事。4.3 结论先堆数据再看容量最后才谈调参对比组固定条件变量结论实验一卷积核 16/32全连接 512训练样本 1000 → 10000 → 100000准确率随数据量明显上升实验二训练样本量不变卷积核 16/32 → 32/64数据少时提升十分有限源实验记录的原话是训练集较小时增加卷积核数量对预测性能的提升十分有限提升训练集数据量则效果明显想要更高性能可以两者兼得。这句话背后是一条朴素的深度学习经验模型的表达能力必须先有足够的数据去约束。1000 条数据时32 个卷积核其实已经把能学的特征学得差不多了再加到 64 个只是让参数膨胀当数据量到 100000 这个量级模型容量不够反而会成为瓶颈这时候增大卷积核才能看到真实收益。做这种对照实验有个前提除了被测变量其余条件必须完全一致。同样的权重初始化方式、同样的批大小、同样的训练步数、同样的 Dropout 比例缺一个都可能让结果失真。很多人实验做完发现结论和别人相反十有八九是训练步数没对齐——一个跑了 200 步另一个只跑了 20 步准确率差异根本分不清是结构变化还是训练量变化带来的。5. 常见问题排查五个真实踩过的坑5.1 数据加载报错路径不存在或下载失败现象运行到read_data_sets直接抛异常提示找不到C:/Users/zhen/MNIST_data_bak/或者卡在下载界面半天没反应。原因路径是原作者 Windows 机器上的绝对路径换系统、换机器就失效MNIST 官方数据源在国内访问经常很慢下载超时是家常便饭。解决改成相对路径让脚本在任意目录都能找到数据import os mnist_dir os.path.join(os.getcwd(), MNIST_data) mnist input_data.read_data_sets(mnist_dir, one_hotTrue)目录不存在时read_data_sets会自动创建并下载前提是网络能连上数据源。如果反复下载失败直接手动下载四个 gz 文件放进MNIST_data/目录再跑最省事。5.2 TensorFlow 2.x 跑不起来placeholder 和 InteractiveSession 已被移除现象报AttributeError: module tensorflow has no attribute placeholder紧接着InteractiveSession也找不到。原因这份代码是 TensorFlow 1.x 风格。TF2 默认开启 eager executiontf.placeholder、tf.InteractiveSession、global_variables_initializer().run()这些机制全部被移除也没有了「先建图再喂数据」的执行模式。解决两条路。想原样跑通装 TF1.15Python 版本要匹配想迁移到 TF2按下面的对应关系改TF1.x 原写法TF2.x 替代tf.placeholder函数入参或tf.keras.Inputtf.InteractiveSession不需要默认 eager 执行tf.nn.dropout(h_fc, keep_prob)tf.nn.dropout(h_fc, rate1-keep_prob)tf.global_variables_initializer().run()不需要变量按需初始化说实话TF2 里最省事的方案是直接用 Keras 把 Conv2D、MaxPool2D、Dense 堆起来代码量更少。但如果目的是搞懂 CNN 内部张量怎么流动先把这份 1.x 版本啃明白再迁移理解会扎实很多。5.3 准确率只有 90% 上下20 步根本没学完现象脚本跑完测试准确率在 0.9 附近徘徊和 MNIST 常见的 99% 差一大截以为是网络结构写错了。原因for i in range(20)只跑 20 步每步 50 张总共只看过 1000 张图一个 epoch 都没走完。解决按样本量等比放大步数。1000 条对应 20 步10000 条对应 200 步完整训练集 55000 条至少要 1100 步total_steps int(55000 / 50) # 一个 epoch 需要 1100 步 for i in range(total_steps): batch mnist.train.next_batch(50) train_step.run(feed_dict{x: batch[0], y: batch[1], keep_prob: 0.5})训练步数的计算不是玄学就是「样本总量 ÷ 批大小」。调参前先把这个数算对不然后面所有实验结论都是空中楼阁。5.4 训练集接近满分、测试集差一截过拟合的典型信号现象把训练步数加到 1100 之后训练集准确率冲到 99% 以上测试集却低好几个点且差距随训练时间越拉越大。原因模型容量偏大加数据量有限网络把训练样本的细节噪声也记住了。512 维全连接层对 MNIST 这种小图其实是偏大的容量配置。解决按这个顺序试——确认训练时keep_prob0.5一直开着把全连接层从 512 降到 256参数量立减一半数据层面做平移、旋转增强。大多数情况下前两步就能把训练集和测试集的差距压回来。5.5 「100000 条训练集」复现不了MNIST 训练集一共就 55000 条现象想照实验描述把训练样本抽到 100000 条结果next_batch报错或者怎么算都不够数。原因标准 MNIST 是 60000 张图官方划分为 55000 训练、5000 验证、10000 测试。100000 这个数字本身超过了训练集上限。解决原实验的 100000 大概率是把验证集、测试集也混进训练或对训练集做多次采样。复现时直接用加大训练步数来等效55000 条数据跑两个 epoch模型见过的样本量就是 110000。对照实验真正要比的是「模型见过的总样本量」而不是「去重后的图片数量」想明白这一点复现实验描述里的任何数字都不成问题。6. 进阶验证固定随机种子、多轮取均值把准确率跑成能写进报告的结论单次运行报出来的准确率其实不太可信。权重初始化是随机的next_batch抽的样本是随机的Dropout 丢哪些神经元也是随机的。同一个脚本连续跑三次结果可能差一两个点。想做对比实验或者把数字写进报告我一般会加两样东西固定随机种子 多次重复取均值。import numpy as np tf.set_random_seed(42) # 固定图级随机种子, 初始化与抽样序列可复现 acc_list [] for trial in range(5): # 同一套配置跑 5 次 tf.global_variables_initializer().run() # 重新初始化, 等于重新训练 for step in range(1100): batch mnist.train.next_batch(50) train_step.run(feed_dict{x: batch[0], y: batch[1], keep_prob: 0.5}) acc accuracy.eval(feed_dict{x: mnist.test.images, y: mnist.test.labels, keep_prob: 1.0}) acc_list.append(acc) print(trial %d test acc %g % (trial, acc)) print(mean: %.4f std: %.4f % (np.mean(acc_list), np.std(acc_list)))tf.set_random_seed必须放在构建网络之前对单机单卡环境有效。每次run一遍初始化 op所有变量都会重置回初始值这是 TF1 里做多次重复训练最直接的方式。5 次的均值和标准差比单次最高分有说服力得多。对照实验里两个配置各跑 5 次比较均值以及标准差代表的波动区间而不是拿 A 的最好成绩去踩 B 的最差成绩。如果还想更直观地验证「卷积核到底学到了什么」可以把中间层的特征图取出来看一眼# 取第一层卷积输出, 看某张测试图的 16 个特征图 sample mnist.test.images[0:1] # 取 1 张 feat_map h_conv.eval(feed_dict{x: sample, y: mnist.test.labels[0:1], keep_prob: 1.0}) print(feat_map.shape) # (1, 28, 28, 16)feat_map[0, :, :, k]就是第 k 个卷积核在这张图上留下的响应图把它保存成图片能直接看到边缘、笔画被逐层放大的过程比看任何示意图都直观。从那以后我每次做模型对照都强制走一遍「固定种子、多轮取均值、训练步数按样本量等比放大」这三步才敢把准确率数字发出去。这套代码和实验记录都在资源包里下载后把数据路径和训练步数一改就能跑希望帮到你。本文还有配套的精品资源点击获取
返回列表