尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CNN图像识别项目代码拆解:数据流、训练验证与TensorFlow版本迁移

CNN图像识别项目代码拆解:数据流、训练验证与TensorFlow版本迁移 简介一套基于Python与TensorFlow实现的CNN图像识别分类项目面向有一定编程基础、希望入门深度学习或完成课程设计的学习者。项目以卷积神经网络为核心覆盖数据加载、模型构建、训练、验证与评估等完整流程可应用于图像分类识别场景。压缩包共13个文件包含6个Python脚本、3个XML配置、2个pyc编译文件等整体仅28KB结构清晰。已有129人学习下载。资源提供CNN理论基础与TensorFlow实现代码涉及卷积层、池化层、激活函数、全连接层等关键组件并包含数据预处理、模型训练与评估等模块可作为毕业设计、课程项目或工程实训的参考资料。学习者需具备基础编程能力自行调试代码、处理报错并根据实际需求修改扩展功能从而深入理解图像识别技术并掌握TensorFlow实战技能。1. 为什么这个CNN图像识别项目值得拆一遍你下载过那种“基于Python TensorFlow的CNN图像识别分类”源码包吗解压后是input_data.py、model.py、training.py、evaluateDisease.py一堆文件还有个model.cpython-35.pyc暗示它跑在 Python 3.5 上。直接运行training.py大概率先报No module named tensorflow装完又报AttributeError: module tensorflow has no attribute placeholder这一套组合拳能劝退一半初学者。但这正是一个典型的 CNN 图像识别工程骨架数据读取、模型搭建、训练、验证被拆成独立模块适合作为毕设、课程设计或工程实训的起点。它的价值不在开箱即用而在于你能顺着代码理解从图片到分类标签的完整数据流并学会处理 TensorFlow 的版本迁移问题。2. 先把训练管线理清从 input_data.py 到 model.py 的数据流2.1 数据加载模块input_data.py 应该怎么组织图片样本很多初学者拿到项目后先打开training.py看到model.py就开始读卷积层但真正决定训练能不能跑通的是数据入口。input_data.py在工程里承担的是把磁盘上的图片文件转换成模型能读的Tensor。如果这个模块里的目录路径写的是绝对路径换一台机器就废了如果它没有统一图片尺寸模型输入的shape就会在跑到一半时炸掉。常见做法是先扫描data/train下的子目录把每个子目录名当作类别标签然后构造一个数据读取器。下面这段代码基本还原了这类工程里input_data.py的核心逻辑import os import numpy as np from PIL import Image class DataReader: def __init__(self, data_dir, image_size(64, 64), batch_size32): self.data_dir data_dir self.image_size image_size self.batch_size batch_size # 子目录名就是类别名排序后顺序固定保证每次训练标签一致 self.class_names sorted(os.listdir(data_dir)) self.num_classes len(self.class_names) self._load_paths_and_labels() def _load_paths_and_labels(self): self.paths [] self.labels [] for idx, class_name in enumerate(self.class_names): class_dir os.path.join(self.data_dir, class_name) for fname in os.listdir(class_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): self.paths.append(os.path.join(class_dir, fname)) self.labels.append(idx) self.paths np.array(self.paths) self.labels np.array(self.labels) def _read_image(self, path): img Image.open(path).convert(RGB).resize(self.image_size) return np.array(img, dtypenp.float32) / 255.0 def next_batch(self): indices np.random.choice(len(self.paths), self.batch_size, replaceFalse) batch_x np.stack([self._read_image(self.paths[i]) for i in indices]) batch_y np.eye(self.num_classes, dtypenp.float32)[self.labels[indices]] return batch_x, batch_y这段代码里最容易被忽略的是标签编码方式np.eye(num_classes)[labels]生成 one-hot 编码shape 是[batch_size, num_classes]后面计算交叉熵时直接和模型输出的logits对齐。next_batch()每次随机采样能让每个 batch 的样本分布更接近全局。缺点是它没有 shuffle 完整数据集也没有数据增强如果训练集很小后面 val 精度会很难看这一点在后面章节会专门说。2.2 模型定义model.py 中的卷积、池化、全连接层级关系CNN 的核心在于用卷积核在图像上滑动提取局部特征。卷积层通过参数共享大幅减少参数量池化层则在保留主要特征的同时降低空间维度。一个标准的图像分类 CNN 结构通常由两个卷积块加一个全连接分类头组成model.py里最典型的实现如下import tensorflow as tf def inference(images, num_classes, keep_prob1.0): # images: [batch_size, height, width, channels] with tf.variable_scope(conv1): w tf.get_variable(weight, [5, 5, 3, 32], initializertf.truncated_normal_initializer(stddev0.1)) b tf.get_variable(bias, [32], initializertf.constant_initializer(0.0)) conv1 tf.nn.relu(tf.nn.conv2d(images, w, strides[1, 1, 1, 1], paddingSAME) b) pool1 tf.nn.max_pool(conv1, ksize[1, 2, 2, 1], strides[1, 2, 2, 1], paddingSAME) with tf.variable_scope(conv2): w tf.get_variable(weight, [5, 5, 32, 64], initializertf.truncated_normal_initializer(stddev0.1)) b tf.get_variable(bias, [64], initializertf.constant_initializer(0.0)) conv2 tf.nn.relu(tf.nn.conv2d(pool1, w, strides[1, 1, 1, 1], paddingSAME) b) pool2 tf.nn.max_pool(conv2, ksize[1, 2, 2, 1], strides[1, 2, 2, 1], paddingSAME) flatten tf.layers.flatten(pool2) with tf.variable_scope(fc1): fc1 tf.layers.dense(flatten, 128, activationtf.nn.relu) fc1 tf.nn.dropout(fc1, keep_prob) with tf.variable_scope(logits): logits tf.layers.dense(fc1, num_classes) return logits第一层卷积核尺寸是5x5输入通道 3RGB输出 32 个特征图paddingSAME让卷积输出尺寸和输入一致这样边缘信息不会过快丢失。随后接2x2最大池化步长也是 2宽高各减半。第二层卷积把 32 个通道扩展到 64 个相当于让网络在更高抽象层上学习更多模式。如果输入是64x64经过两次池化后特征图变成16x16展平后长度是16*16*6416384再接128维全连接层。keep_prob是 dropout 保留概率训练时设为 0.5 能抑制过拟合验证和预测时必须设为 1.0。这里有个容易踩的坑如果num_classes是 1网络不会报错但 softmax 交叉熵会失效因为二分类也要两个输出节点。2.3 训练脚本training.py 里的 loss、优化器与学习率设置training.py的任务是把数据和模型“缝合”起来。它要定义 placeholder、损失函数、优化器然后循环喂数据。下面这段精简后的训练循环是这个项目常见的写法import tensorflow as tf from input_data import DataReader from model import inference train_reader DataReader(data/train, image_size(64, 64), batch_size32) images tf.placeholder(tf.float32, [None, 64, 64, 3]) labels tf.placeholder(tf.float32, [None, train_reader.num_classes]) keep_prob tf.placeholder(tf.float32) logits inference(images, train_reader.num_classes, keep_prob) loss tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logitslogits, labelslabels)) optimizer tf.train.AdamOptimizer(learning_rate1e-4).minimize(loss) correct tf.equal(tf.argmax(logits, 1), tf.argmax(labels, 1)) accuracy tf.reduce_mean(tf.cast(correct, tf.float32)) saver tf.train.Saver() with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for epoch in range(50): for step in range(len(train_reader.paths) // 32): batch_x, batch_y train_reader.next_batch() _, cur_loss, cur_acc sess.run( [optimizer, loss, accuracy], feed_dict{images: batch_x, labels: batch_y, keep_prob: 0.5}) print(epoch, epoch, loss, cur_loss, acc, cur_acc) saver.save(sess, ckpt/model, global_stepepoch)这里用softmax_cross_entropy_with_logits_v2而不是先算 softmax 再算交叉熵是因为它内部做了 logits 与 labels 的数值稳定处理避免log(0)导致 loss 变成NaN。优化器选择 Adam学习率1e-4在中小型 CNN 上是一个稳妥起点。saver.save每轮都保存 checkpoint后面验证时直接用latest_checkpoint恢复。超参数调整时下面这张表可以作为参考参数建议范围说明image_size64x64 或 128x128太小丢失空间信息太大显存占用高batch_size16~64小数据集推荐 32显存不够就降learning_rate1e-4~1e-3Adam 下从 1e-4 起步震荡就再降卷积核大小3x3 或 5x55x5 感受野大但参数量也大dropout keep_prob训练 0.5验证 1.0验证阶段 dropout 会引入随机噪声epoch30~100看 loss 是否收敛不必强制固定如果训练到 30 轮后 loss 仍然在 0.7 左右徘徊优先检查input_data.py里的图片是不是没有归一化到[0,1]或者标签和目录名没有对齐。这类问题在工程里比模型结构问题更常见。3. 用 evaluateDisease.py 和 CNNTensorflowValidate.py 验证模型别只看准确率3.1 checkpoint 恢复与单张图像预测训练完成后CNNTensorflowValidate.py这类文件负责把训练好的 checkpoint 加载回来对新的图像做预测。恢复模型时不需要重新构建优化器只需要模型结构和变量名一致即可。下面是一个完整的最小预测脚本import tensorflow as tf import numpy as np from PIL import Image from model import inference class_names [cat, dog] # 一定与训练时的目录顺序一致 tf.reset_default_graph() images tf.placeholder(tf.float32, [None, 64, 64, 3]) keep_prob tf.placeholder(tf.float32) logits inference(images, len(class_names), keep_prob1.0) predict tf.nn.softmax(logits) saver tf.train.Saver() with tf.Session() as sess: ckpt tf.train.latest_checkpoint(ckpt) saver.restore(sess, ckpt) img Image.open(test_01.jpg).convert(RGB).resize((64, 64)) x np.expand_dims(np.array(img, dtypenp.float32) / 255.0, axis0) probs sess.run(predict, feed_dict{images: x, keep_prob: 1.0}) pred_idx np.argmax(probs[0]) print(pred:, class_names[pred_idx], prob:, probs[0][pred_idx])这里有个细节tf.train.latest_checkpoint(ckpt)会自动在ckpt目录下找最新的.index文件但如果训练脚本保存的文件名带了global_step恢复时模型权重里的所有变量都会被读入不需要手动指定epoch。输入图像的预处理必须和训练完全一致包括resize尺寸、除以 255、convert(RGB)。如果训练时用(128, 128)预测时却用了(64, 64)shape不匹配会在sess.run时直接报错。3.2 计算混淆矩阵找到真正的盲区evaluateDisease.py这个文件名暗示作者当时处理的是疾病相关的图像数据但验证思路是通用的。单张预测只能验证个别样本批量验证时准确率是最容易骗人的指标。当数据集中类别不均衡时比如 90% 是猫、10% 是狗全预测成猫也能有 90% 准确率。这时候要看混淆矩阵。from sklearn.metrics import confusion_matrix, classification_report import numpy as np preds [] trues [] for i in range(0, len(val_x), batch_size): bx val_x[i:ibatch_size] logit_val sess.run(logits, feed_dict{images: bx, keep_prob: 1.0}) preds.extend(np.argmax(logit_val, axis1)) trues.extend(np.argmax(val_y[i:ibatch_size], axis1)) cm confusion_matrix(trues, preds) print(classification_report(trues, preds, target_namesclass_names))confusion_matrix的行是真实类别列是预测类别。看一个例子真实\预测catdogcat8515dog3070这个矩阵里 dog 被误判为 cat 的数量有 30明显高于 cat 被误判为 dog 的 15。这说明模型对 dog 的特征表达不足或者 dog 的训练样本里存在大量和 cat 共用的背景。此时优先检查训练集中 dog 的图片是否足够多而不是盲目增大卷积通道数。classification_report会给出每个类的 precision、recall、f1-score重点关注 recall 低的类别。3.3 验证集划分的一个硬性要求有些项目为了省事直接用input_data.py读出来的所有图片既训练又验证然后打印出 98% 的准确率换个文件夹就崩。这是典型的数据泄漏CNN 记住了训练样本而不是学到了类型特征。我一般会在input_data.py里预留一个val_split参数按类别分层抽样切出 20% 作为验证集from sklearn.model_selection import train_test_split train_paths, val_paths, train_labels, val_labels train_test_split( paths, labels, test_size0.2, stratifylabels, random_state42)stratifylabels可以保证每个类别在训练集和验证集中的比例与原数据集一致。如果数据集小到只有几百张更好的做法是直接使用交叉验证但在工程里时间成本偏高最常见的还是固定一个随机种子做一次划分。4. 跑通这个项目的关键依赖与排错Python、TensorFlow 版本匹配4.1 从代码细节判断项目适用的 TensorFlow 版本项目里留下了一个model.cpython-35.pyc这是 Python 3.5 编译后的缓存文件说明原作者的环境是 Python 3.5 加 TensorFlow 1.x。再看tf.placeholder、tf.Session、tf.variable_scope这些写法可以确定它不是 TensorFlow 2.x 的 Keras 风格。如果你电脑上装的是 TensorFlow 2.x直接跑会报AttributeError: module tensorflow has no attribute placeholder。用下面这张表可以快速判断一个老项目属于哪个版本代码特征TensorFlow 1.xTensorFlow 2.xtf.placeholder广泛使用移除用tf.keras.Inputtf.Session必需移除tf.variable_scope常见不推荐用 Keras 层tf.nn.conv2d手动管理变量被tf.keras.layers.Conv2D取代tensorflow.contrib存在移除在 macOS 上新建 Python 3.5 环境已经不太方便最简单的做法是用 Anaconda 创建独立环境来装 TensorFlow 1.15。命令如下conda create -n tf1 python3.5 conda activate tf1 pip install tensorflow1.15这里注明一点如果你没有 NVIDIA 显卡装 CPU 版本就够CNN 在小数据集上也能训练只是慢一些。如果有 GPU还需要单独安装匹配的 CUDA 和 cuDNN版本对应关系建议直接查 TensorFlow 官方支持矩阵不同小版本的匹配规则经常变化别照抄网上任意一篇教程。如果你不想回到 Python 3.5也可以在 TensorFlow 2.x 里跑老代码只需要在training.py最前面加两行import tensorflow.compat.v1 as tf tf.disable_v2_behavior()这样tf.placeholder、tf.Session都会恢复成 1.x 的行为大部分老工程可以直接跑通。但要注意disable_v2_behavior和tf.keras混用时会有一些副作用尽量不要在一个工程里一半用 compat、一半用 Keras。4.2 常见报错与处理把最常见的报错整理成一个速查表真正动手时能省很多时间报错信息原因解决No module named tensorflow没有安装conda activate tf1 pip install tensorflow1.15module tensorflow has no attribute placeholder用了 TF2 运行 TF1 代码使用tf.compat.v1或改写为 KerasShape must be rank 4 but is rank 2输入图像没有[batch, h, w, c]维度用np.expand_dims(x, axis0)补 batch 维度Invalid argument: Expected image in [0, 1]没有归一化或通道数不一致convert(RGB)后除以 255.0Resource exhausted: OOMbatch 太大或模型参数太多减小 batch_size或减少卷积核数量或降为灰度图以减通道数除了这些还有一个很隐蔽的问题读取灰度图时PIL.Image.open返回的数组 shape 是(h, w)不是(h, w, 1)直接喂给卷积层会报维度错误。我一般会在数据读取时统一用.convert(RGB)把灰度图转成三通道这样模型输入始终是[batch, h, w, 3]。如果非要保留灰度通道就在np.array后手动np.expand_dims(axis-1)同时把模型第一层卷积输入通道数改成 1。5. 把这份项目改造成自己的分类任务最小改动路径5.1 替换数据集时只需要改三处第一处是input_data.py里的data_dir第二处是model.py的输入图像尺寸第三处是training.py的 placeholder。类别数量不用手改因为DataReader.num_classes会根据目录下有几个子目录自动计算。但目录名必须是小写英文不要用中文否则sorted(os.listdir())的排序顺序在不同操作系统上可能不一致。我一般会把数据目录做成环境变量注入这样切换数据集时不用改代码import os data_dir os.environ.get(DATA_DIR, data/train)启动训练时用DATA_DIRflower_data python training.py每个实验都留一个独立日志目录便于后面对比。5.2 用 TensorBoard 观察训练过程为了确认模型是否真的在收敛可以在training.py中加入 summary把 loss 和 acc 写入logs目录tf.summary.scalar(loss, loss) tf.summary.scalar(acc, accuracy) merged tf.summary.merge_all() writer tf.summary.FileWriter(logs, sess.graph) # 训练循环内 summary, _ sess.run([merged, optimizer], feed_dict{images: batch_x, labels: batch_y, keep_prob: 0.5}) writer.add_summary(summary, global_stepepoch)启动命令tensorboard --logdirlogs浏览器打开http://localhost:6006即可看到曲线。如果 loss 一直震荡不降先把学习率调到1e-5重新跑如果 loss 降了但 val 准确率不上涨把 dropout 的 keep_prob 从 1.0 改成 0.5并观察训练集和验证集的差异是否缩小。调参时一次只动一个变量比如这一轮只动学习率下一轮只动卷积核数量否则很难定位是哪个改动起到了作用。本文还有配套的精品资源点击获取
返回列表