ARTICLE DETAIL

资讯详情

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

用TensorFlow从零搭建CNN:数据量与卷积核谁更影响精度?

用TensorFlow从零搭建CNN:数据量与卷积核谁更影响精度? 简介面向深度学习初学者与TensorFlow入门者这份PDF以MNIST手写数字识别为例完整演示了用Python实现CNN的代码过程网络包含两个卷积层和一个全连接层卷积层采用ReLU激活并配合2x2最大池化全连接层使用Softmax输出十类概率同时加入Dropout防止过拟合损失函数选用交叉熵并用Adam优化器完成训练。包内为1个PDF文件大小仅56KB内容紧凑代码段与运行结果均做了转写标注便于随时对照阅读。作者还专门做了两组实验训练数据从1000增至100000时准确率明显提升而仅把卷积核从16/32增至32/64对性能提升有限这一结论对初学调参、理解数据量与模型容量关系很有参考价值。资源已有3760人学习下载适合快速掌握CNN基础流程并希望少走弯路的读者。1. 用 TensorFlow 从零搭一个 CNN这套代码为什么值得照着敲一遍不少刚接触深度学习的读者把 CNN 的代码来回抄了十几遍却始终没搞明白每一层张量到底变成了什么样、为什么卷积核数量往上加效果却不明显。这份 Python CNN 卷积神经网络代码实现正好把这两件事说透了。它基于 TensorFlow 1.x 的经典接口在 MNIST 上构造了一个两层卷积加一层全连接的小型网络并用「固定模型增大训练集」和「固定训练集增大卷积核」两组对照实验直接回答了一个很多教程回避的问题精度上不去到底是数据不够还是模型不够大适合正在入门 CNN、想亲手改参数看效果以及准备在课程设计或实验报告中复现一组可对比数据的读者。代码本身不复杂十几分钟就能跑通但里面藏着的网络设计逻辑、参数联动关系和过拟合处理方式才是这份资源真正的价值所在。下面按我自己的拆解习惯从网络结构逐步讲到踩坑点最后再分享几个改代码做扩展的小技巧。2. 先看懂网络骨架两层卷积加池化为什么是 16→32→512 这个组合这份代码的网络结构属于 LeNet 风格的小型 CNN针对 MNIST 这种 28×28 灰度手写数字图片设计。MNIST 每张图是单通道分辨率不高类别只有 10 个所以不需要 ResNet 那种几十层的深度两层卷积加一层全连接已经足够跑出不错的精度。理解这个网络关键不是背代码而是跟着张量的维度变化走一遍。2.1 输入张量的 reshape从 784 到 28×28×1MNIST 数据集的原始输入是 784 维的向量也就是把 28×28 的图片按行展开。CNN 要求输入保持空间结构所以第一步必须 reshape 成四维张量对应 TensorFlow 的 NHWC 格式batch、height、width、channels。x tf.placeholder(tf.float32, [None, 784]) y tf.placeholder(tf.float32, [None, 10]) x_image tf.reshape(x, [-1, 28, 28, 1])这里-1表示 batch 维度由 TensorFlow 自动推断你喂多少张图它就变成多少。784是 28×28 的展平长度1是通道数因为 MNIST 是灰度图。如果换成 RGB 彩色图这个位置要改成 3后面的卷积核输入通道也要跟着变。placeholder 的第一个维度设为None是为了让训练和测试阶段使用不同的 batch 大小训练时一次喂 50 张测试时可以一次性喂全部 10000 张。2.2 卷积核尺寸和通道数5×5 滤波器的作用代码里第一个卷积层是weight_variable([5, 5, 1, 16])第二个是[5, 5, 16, 32]。这四个数字分别表示卷积核的高度、宽度、输入通道数、输出通道数。5×5 是经典的卷积核尺寸在 MNIST 这种小分辨率图像上它比 3×3 有更大的感受野能更早地捕捉到笔画级别的局部特征。第一层输入通道是 1灰度图输出 16 个特征图第二层输入通道变成 16因为要承接第一层的输出输出 32 个特征图。通道数从 16 翻倍到 32是一个很常见的 CNN 设计惯例。每经过一次池化特征图的空间尺寸减半信息被压缩通道数随之翻倍以保留足够多的特征表达。全连接层的输入维度7*7*32就是第二层池化后的结果原始 28×28 经过两次 2×2 最大池化尺寸变成 7×7通道数 32展平后就是 1568。如果改动卷积层的输出通道数这个 7×7 的数字本身不变但最后的7*7*32必须同步修改这是改代码时最容易翻车的地方。2.3 参数初始化为什么用截断正态分布加常数偏置def weight_variable(shape): initial tf.truncated_normal(shape, stddev0.1) return tf.Variable(initial) def bias_variable(shape): initial tf.constant(0.1, shapeshape) return tf.Variable(initial)权重的初始化方式用的是truncated_normal标准差 0.1。截断正态分布和普通正态分布的区别在于它会丢弃距离均值超过两个标准差的样本避免初始化时出现极端大的权重值。神经网络训练初期过大的权重很容易让激活函数饱和梯度消失或爆炸的问题会提前出现。偏置全部初始化为 0.1 的常数是为了防止 ReLU 神经元在初始化阶段就大量死于负区间保证前向传播时有一定比例的神经元处于激活状态。实际使用中如果发现训练初期 loss 不下降可以检查一下偏置初始化和权重标准差。如果发现收敛速度明显偏慢把标准差调到 0.05 或 0.15 试试但幅度别太大。2.4 卷积与池化的参数语义SAME padding 和 2×2 窗口def conv2d(x, W): return tf.nn.conv2d(x, W, strides[1, 1, 1, 1], paddingSAME) def max_pool_2x2(x): return tf.nn.max_pool(x, ksize[1, 2, 2, 1], strides[1, 2, 2, 1], paddingSAME)卷积的 strides 四元组对应 NHWC 格式[1, 1, 1, 1]表示 batch 和通道维度步长为 1高和宽方向也以 1 像素滑动。padding 设为 SAME意思是输出尺寸与输入尺寸保持相同边缘不足的部分自动补零。池化层的 ksize 是[1, 2, 2, 1]步长同样是[1, 2, 2, 1]即每个 2×2 窗口取最大值然后窗口向右和向下移动 2 个像素不重叠。配合 SAME padding两次池化后 28×28 → 14×14 → 7×7每一次空间尺寸减半。这里要注意一个很容易踩的坑如果后面改用了 paddingVALID7×7 的输出尺寸会变成 6×6 或者需要重新计算全连接层的输入维度就得重新推。SAME 只是保证卷积输出尺寸不变池化仍然会把尺寸减半不要看到 SAME 就以为池化也不降尺寸。3. 训练过程拆开看损失函数、Dropout 和优化器是如何协同工作的网络结构只是骨架真正决定模型能不能收敛的是训练环节的四个选择交叉熵损失、Adam 优化器、Dropout 比例和 batch 大小。这段代码把每个环节都压缩到了极简形式但每一行的作用都需要展开理解。3.1 交叉熵损失为什么分类任务不用均方误差cross_entropy tf.reduce_mean(-tf.reduce_sum(y * tf.log(y_conv), reduction_indices[1]))这一行是手工展开的交叉熵公式。y是 one-hot 编码的真实标签y_conv是 softmax 输出的预测概率。逐元素相乘后求和得到每个样本的交叉熵再用reduce_mean取平均。分类任务不用均方误差是因为 softmax 输出的概率分布和 one-hot 标签之间的均方误差梯度在饱和区域会变得非常小收敛速度明显变慢交叉熵配合 softmax 的梯度形式更干净这是深度学习中「默认组合」级别的共识。这里把reduction_indices[1]放在行内等价于现在更常见的axis1。如果要在新版 TensorFlow 或 PyTorch 里复刻这行语义完全一样的写法是F.cross_entropy(logits, target)但手工展开的版本对理解原理更有帮助。3.2 Adam 优化器与学习率1e-4 这个量级怎么选train_step tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)Adam 是自适应矩估计优化器它会为每个参数单独维护学习率对梯度稀疏或尺度差异大的问题有较好的适应性。相比原始的 SGDAdam 对学习率的敏感度低很多但这不意味着可以乱设。1e-4 是 MNIST 这类小数据集上比较稳妥的起点如果学习率调到 1e-2 或 1e-3前几步 loss 可能会急剧抖动甚至出现 NaN。判断学习率是否合适的做法是先跑几十个 batch 观察 loss 变化曲线如果 loss 震荡不降优先调低学习率如果 loss 降得很慢再考虑调高。代码里只训练 20 轮每轮 50 张图总共只看了 1000 个样本这个量级配合 1e-4 是够用的。如果复制这套代码去训练更大的数据集比如 CIFAR-101e-4 会显得偏保守可以把迭代轮数提高或者改用学习率衰减策略。3.3 Dropout 的 keep_prob 两个取值训练 0.5测试 1.0keep_prob tf.placeholder(tf.float32) h_fc_drop tf.nn.dropout(h_fc, keep_prob)Dropout 在训练时随机把一部分神经元的输出置零迫使网络不依赖单一神经元从而减轻过拟合。这里keep_prob设计成 placeholder就是为了训练和测试阶段使用不同的值。训练时设为 0.5即随机丢弃一半的神经元测试时设为 1.0即保留全部神经元。需要注意测试时设置 keep_prob 为 1.0 不只是「关闭随机性」这么简单它还隐含了权重缩放逻辑。TensorFlow 的 dropout 实现会在训练时把保留的神经元输出除以 keep_prob保证期望值不变所以测试时不缩放也不会影响输出尺度。如果自己手写 dropout记得在训练时做 scaled 处理否则测试阶段的输出会比预期偏小。3.4 训练循环与准确率评估为什么要分开喂 feed_dicttf.global_variables_initializer().run() for i in range(20): batch mnist.train.next_batch(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}))训练循环只跑了 20 个 step每个 step 取 50 张图累计 1000 条训练样本。这个设定在资源里是有意为之的目的是控制变量去对比不同条件下的精度差异而不是追求最高分。注意train_step.run()和accuracy.eval()都需要通过 feed_dict 提供数据区别在于训练时 keep_prob 是 0.5测试时是 1.0。如果把 keep_prob 忘了喂TensorFlow 会直接报错因为 placeholder 没有被赋值。还有一个是初学者容易踩错的点mnist.train.next_batch(50)每次会从训练集里随机抽取 50 张图并且是有放回的抽取所以 20 个 step 看到的不是 20×50 条独立样本而是重复抽样。这在小训练量实验里是正常的不影响控制变量对比的结论但如果想复现一个稳定的最终精度建议把训练轮数提到 500 以上。4. 这份资源真正的核心两组对照实验揭示精度瓶颈在哪我在拆很多课程设计资源时最怕碰到那种只丢一份代码、不给实验结论的。这份资源好就好在代码后面直接附了两组控制变量实验结果而且结论非常反直觉训练集只有 1000 条时把卷积核从 16→32 加到 32→64精度提升极其有限反过来保持模型不变把训练集从 1000 加到 10000精度提升非常明显。这个结论如果自己不去做实验很难从直觉上判断对错。4.1 第一组实验模型固定训练集从 1000 加到 100000资源里的实验设置很清晰算法模型结构保持不变第一层 16 个卷积核第二层 32 个卷积核全连接层 512 维10 分类。训练集分别取 1000、10000、100000 条。代码里只写了 20 个 step如果要复现 10000 和 100000 组的实验需要把训练轮数和 batch 对应地调大。我建议按这样的方式复现保持 batch50 不变1000 条训练集对应 20 轮10000 条对应 200 轮100000 条时要注意MNIST 训练集本身只有 55000 条next_batch是有放回抽样所以 100000 条实际上是多个 epoch 的组合设置 2000 轮即可。# 复现时建议改成变量控制而不是硬编码 20 train_steps 200 for i in range(train_steps): batch mnist.train.next_batch(50) train_step.run(feed_dict{x: batch[0], y: batch[1], keep_prob: 0.5})在固定模型的条件下增大训练集带来的收益是单调且显著的。原因在于模型容量没有变欠拟合是主要矛盾更多数据让网络看到了更多笔画变体对数字形态的覆盖更完整。MNIST 本身类别均衡、标注质量高不存在数据噪声问题所以数据量越大梯度估计越稳定收敛到的局部最优也越好。4.2 第二组实验训练集固定卷积核从 16→32 加到 32→64第二组实验把训练集分别固定为 1000、10000、100000模型改为第一层 32 个卷积核第二层 64 个卷积核全连接层保持 512。对应的改动位置在代码里很清楚。W_conv weight_variable([5, 5, 1, 32]) b_conv bias_variable([32]) W_conv2 weight_variable([5, 5, 32, 64]) b_conv2 bias_variable([64]) W_fc weight_variable([7 * 7 * 64, 512]) b_fc bias_variable([512])第二层卷积核的输出通道从 32 改成 64 之后全连接层的输入维度从7*7*32必须同步改成7*7*64否则矩阵乘法维度不匹配直接报错。这一步是改动网络结构时最容易出的问题。我可以直接说结论在训练集只有 1000 条时加大卷积核数量几乎不涨点因为模型已经进入过拟合区间参数多了反而更容易记住训练集里的个别样本泛化能力没有实质提升当训练集达到 10000 甚至 100000 条时增大的模型容量才被数据「喂饱」精度提升才开始显现。4.3 两组实验合起来读先加数据还是先加参数资源结尾的总结非常到位训练集较小时一味增加卷积核数量对预测性能的提升十分有限相同模型下适当增加训练集数据对模型提升非常明显想达到更高性能两者可以兼得。这句话其实揭示了深度学习调参的一个底层逻辑模型容量和数据量必须匹配。签出一个实际判断标准如果增大模型后训练集精度显著提升但测试集精度不动说明模型已经过拟合此时优先加数据或加正则化如果增大模型后训练集和测试集精度一起提升说明模型欠拟合此时加大参数才有意义。资源里 1000 条训练集的场景就属于前者100000 条的场景接近后者。提示复现这两组实验时不要只改一个参数就急着看结果至少跑三次取平均。小训练量下精度波动范围可能有 12 个百分点一次实验结果很容易误导判断。5. 照着跑会遇到的问题TensorFlow 版本兼容与数据路径的四个高频坑这份代码是基于 TensorFlow 1.x 的经典接口写的用现在的 TensorFlow 2.x 直接跑会报一堆错。我在复现时踩过几个典型的坑逐个写出来按「现象 → 原因 → 解决」的方式整理省得你来回翻文档。5.1 tf.InteractiveSession 报错或不存在现象在 TensorFlow 2.x 环境中运行sess tf.InteractiveSession()直接报 AttributeError或者提示Session只能在 eager mode 关闭时使用。原因TensorFlow 2.0 开始默认开启 eager execution并且把tf.Session等图执行接口移到了tf.compat.v1命名空间。原始代码基于 2018 年的 TensorFlow 1.x 编写当时InteractiveSession是最常见的写法。解决在代码开头强制关闭 eager mode并调用兼容接口。import tensorflow.compat.v1 as tf tf.disable_v2_behavior()加上这两行之后tf.InteractiveSession、tf.placeholder、tf.nn.dropout这些旧接口都能在 TensorFlow 2.x 下正常工作。建议把原来的import tensorflow as tf改成import tensorflow.compat.v1 as tf这样后面所有 API 都走兼容层不容易出现一半新版一半旧版的混乱局面。5.2 MNIST 数据集下载卡死或路径识别不了现象运行到input_data.read_data_sets时长时间卡住或者提示EOFError、文件不存在。原因read_data_sets在本地找不到数据集时会自动尝试从网络下载但国内访问原始下载地址经常超时或连接中断。原代码里给的是本地路径C:/Users/zhen/MNIST_data_bak/别人机器上当然没有这个目录。解决提前手动下载 MNIST 四个文件放到本地目录然后把路径改成自己的目录。需要的是 train-images-idx3-ubyte.gz、train-labels-idx1-ubyte.gz、t10k-images-idx3-ubyte.gz、t10k-labels-idx1-ubyte.gz 这四个文件read_data_sets会自动识别 gz 压缩格式不需要手动解压。路径中不要出现中文Windows 下写成D:/data/mnist/这种正斜杠形式。5.3 全连接层维度不匹配报矩阵乘法错误现象修改卷积核数量后运行到tf.matmul(h_pool_flat, W_fc)报维度错误提示 shape 对不上。原因h_pool_flat的维度由7*7*32硬编码在 reshape 里而这一步和W_conv2的输出通道数强相关。只改了卷积层的输出通道数忘记了同步修改全连接层输入维度两者不匹配。解决要么把张量形状打印出来做精确计算要么把这个数字提炼成变量集中管理。conv2_output_channels 64 h_pool2_shape 7 * 7 * conv2_output_channels W_fc weight_variable([h_pool2_shape, 512]) h_pool_flat tf.reshape(h_pool2, [-1, h_pool2_shape])我一般会建议用第二种方式把7*7*32和7*7*64这类魔法数字统一定义改模型结构时只动一处。如果你用的是不同尺寸的输入图片7×7 这个数字也要跟着重新推算不能照抄。5.4 训练不收敛或 loss 为 NaN现象loss 在前几步就跳到 NaN或者一直不下降准确率维持在 10% 左右等于瞎猜。原因常见原因有三个学习率过大、权重初始化标准差过大、输入数据没有归一化。MNIST 的数据已经做了归一化处理所以这个数据集上后两个问题不太常见更多是学习率设置问题。解决确认你用的是 1e-4 而不是 1e-2把初始化标准差从 0.1 降到 0.05打印第一个 batch 的 loss 值如果初始 loss 就是 NaN优先检查标签是否为 one-hot 编码、tf.log(y_conv)是否出现了 log(0) 的情况。softmax 输出理论上在 (0,1) 区间但由于浮点精度极小概率会被截断为 0可以给tf.log(y_conv)加上一个极小 epsilon 做保护。6. 改代码做扩展的实用技巧从 MNIST 迁移到自己的数据只需要动四个位置很多人拿到这份代码以后第一反应是想让它跑自己的数据集。这个迁移过程比想象中简单关键位置只有四个输入尺寸、通道数、分类数量、数据加载方式。只要按顺序改基本不会出问题。6.1 替换数据加载从 MNIST 到自己的图片文件夹原始代码使用mnist.train.next_batch直接取数据换成自己的数据后最小改动方式是保留 placeholder 结构用批量读取代替。import cv2 import os import numpy as np def load_images_and_labels(data_dir): images, labels [], [] for label_name in os.listdir(data_dir): label int(label_name) label_dir os.path.join(data_dir, label_name) for file_name in os.listdir(label_dir): img_path os.path.join(label_dir, file_name) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (28, 28)) img img.astype(np.float32) / 255.0 images.append(img.reshape(-1)) labels.append(label) return np.array(images), np.array(labels) data_dir ./my_dataset X_data, y_data load_images_and_labels(data_dir) y_data_onehot np.eye(10)[y_data]这段代码的核心是把每张图片统一缩放到 28×28、归一化到 01、展平成 784 维向量然后做 one-hot 编码。np.eye(10)[y_data]这行的意思是生成一个 10 行 10 列的单位矩阵按标签值索引取出对应的 one-hot 行向量。如果你的类别不是 10 个把 10 改成实际类别数。6.2 动态调整网络输入和输出维度图片如果不再是 28×28比如 64×64第一层卷积的输入尺寸、池化后的中间维度都要改。原代码里全连接层的输入是硬编码的7*7*32换成 64×64 输入经过两次 2×2 池化后变成 16×16全连接输入就应该是16*16*32。input_h, input_w 64, 64 conv1_output 32 conv2_output 64 pooled_h input_h // 4 pooled_w input_w // 4 x_image tf.reshape(x, [-1, input_h, input_w, 1]) W_conv2 weight_variable([5, 5, conv1_output, conv2_output]) fc_input_dim pooled_h * pooled_w * conv2_output W_fc weight_variable([fc_input_dim, 512]) h_pool_flat tf.reshape(h_pool2, [-1, fc_input_dim])// 4是因为两次 2×2 池化尺寸各缩一半合计缩到四分之一。这里假设输入尺寸是偶数且能被 4 整除否则需要配合 padding 策略重新计算。6.3 预测结果输出与保存模型原代码只打印了测试准确率实际使用中往往需要把预测结果以图像形式可视化或者保存模型权重供后续推理。# 保存模型 saver tf.train.Saver() saver.save(sess, ./mnist_cnn_model/model.ckpt) # 加载模型 saver.restore(sess, ./mnist_cnn_model/model.ckpt) # 对单张图片预测 prediction tf.argmax(y_conv, 1) result prediction.eval(feed_dict{x: img.reshape(1, -1), keep_prob: 1.0})tf.argmax(y_conv, 1)的作用是在预测概率向量中取最大值所在的下标就是最终的类别编号。keep_prob在预测时设为 1.0这一点和测试阶段保持一致不要在这里设成 0.5。6.4 学习率衰减与训练轮数调整想在新数据集上得到更高精度可以直接在同一份代码上增加学习率衰减不需要改网络结构。常见的做法是指数衰减每训练一定轮数学习率乘以一个衰减系数。global_step tf.Variable(0, trainableFalse) learning_rate tf.train.exponential_decay( 1e-4, global_step, decay_steps500, decay_rate0.96, staircaseTrue) train_step tf.train.AdamOptimizer(learning_rate).minimize( cross_entropy, global_stepglobal_step)decay_steps500表示每 500 步衰减一次decay_rate0.96表示每次乘以 0.96staircaseTrue表示阶梯式衰减而不是连续衰减。这套参数是很多小型 CNN 实验的常见起点跑出来的精度通常比固定学习率更稳定。我自己的习惯是拿到任何一份 CNN 代码第一件事就是先打印每一层的输出张量形状确认维度链路完整后再开始训练。这份代码我在复现时踩过最多的坑就是全连接层维度没同步修改以及 TensorFlow 2.x 环境下 Session 兼容问题。从那以后我每次改动卷积核数量或输入尺寸都会强制走一遍「改通道数 → 同步改 fc 输入维度 → 打印形状确认」的流程十几秒的事能省掉大量排错时间。希望这份拆解能帮你在复现时少走几步弯路。本文还有配套的精品资源点击获取
返回列表