TensorFlow手写CNN花卉识别:小样本图像分类工程实践
2026/9/15 6:28:36 网站建设 项目流程

简介:本资源是一份面向计算机专业本科生的CNN图像识别实战项目,聚焦花卉图像分类任务,适用于期末大作业、课程设计及深度学习入门实践。项目基于Python与TensorFlow框架构建完整端到端流程,涵盖数据预处理、CNN模型搭建(含卷积层、池化层与全连接层设计)、训练调优及测试推理全流程,配套详实实验报告与答辩PPT,内容经导师指导与助教审定,获评98分高分。压缩包共13个文件,包含6个核心Python脚本(如train.py、gui.py、myTest.py等实现训练、可视化与预测功能)、1份Word实验报告、1份PPT答辩材料、1个环境配置yaml文件、1个requirements文本及README说明文档,整体大小为10.82MB,结构清晰、模块解耦,便于理解与二次开发。目前已有95人学习下载,所有代码均本地验证可运行,附带模型权重与数据加载逻辑,显著降低调试门槛,特别适合缺乏项目经验的学习者快速掌握CNN工程落地关键环节。

1. 这不是调个model.fit()就完事的“花卉识别”——它是一次完整的 CNN 工程闭环训练,覆盖数据清洗、模型构建、训练监控、结果可视化与可复现报告生成

如果你手头正写着一份《基于Python和TensorFlow的CNN花卉图像识别》期末大作业,却卡在“跑通但准确率卡在65%不动”“验证集loss突然飙升”“报告里图表全是截图没代码支撑”“老师问‘你用的什么数据增强策略’答不上来”,那这篇就是为你写的。它不讲抽象的卷积核推导,也不堆砌论文术语,而是还原一个真实学生从pip install tensorflow开始,到交出带完整训练日志、混淆矩阵热力图、单图预测接口、参数配置表和可复现环境说明的交付物全过程。重点落在如何让CNN在有限样本(如Oxford 102 Flowers)上稳定收敛、避免过拟合、解释性可呈现——这正是当前课程设计最常踩的坑:模型能跑≠理解在跑什么,识别能出结果≠结果可信可分析。适合大三/大四计算机、人工智能、自动化专业学生,也适合作为初学深度学习的工程师快速搭建图像分类最小可行实验框架。

2. 用TensorFlow 2.x构建可调试的CNN架构:从基础LeNet到适配花卉数据的三层卷积+全局平均池化设计

2.1 为什么不用预训练模型?先手写CNN才能真正理解特征提取瓶颈

很多同学一上来就用tf.keras.applications.MobileNetV2,虽然准确率高,但期末报告里写不出“第三层卷积输出通道数为何设为64”“ReLU激活后特征图尺寸怎么计算”。本方案坚持从零构建CNN,核心逻辑是:花卉类间差异小(如不同品种郁金香花瓣纹理相似)、类内差异大(光照/角度/遮挡),必须通过可控的卷积层数、滤波器尺寸和下采样策略,让模型学会关注花瓣边缘、花蕊结构等判别性局部特征,而非依赖ImageNet预训练的通用纹理先验。因此我们放弃ResNet或EfficientNet这类深层结构,采用轻量但可解释性强的定制CNN。

提示:Oxford 102 Flowers数据集共102类,每类约80张图,总样本量不足1万。直接套用大型模型极易过拟合,且无法体现对小样本图像分类问题的工程思考。

2.2 具体网络结构定义:三层卷积+BN+Dropout+GAP,兼顾表达力与泛化性

以下代码定义了主干CNN,所有层均显式声明参数,便于后续调整:

import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers def build_flower_cnn(input_shape=(224, 224, 3), num_classes=102): model = keras.Sequential([ # 第一层卷积:捕获基础边缘与颜色块 layers.Conv2D(32, (3, 3), activation='relu', input_shape=input_shape, padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 第二层卷积:组合边缘形成局部纹理(如花瓣脉络) layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 第三层卷积:抽象更高阶结构(花蕊形状、对称性) layers.Conv2D(128, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 全局平均池化替代Flatten:减少参数量,提升对空间变换鲁棒性 layers.GlobalAveragePooling2D(), # 分类头:两层全连接,第二层用softmax输出概率分布 layers.Dense(256, activation='relu'), layers.Dropout(0.5), layers.Dense(num_classes, activation='softmax') ]) return model # 实例化模型并打印结构 model = build_flower_cnn() model.summary()
参数设计逻辑说明:
  • 输入尺寸设为(224, 224, 3):平衡计算开销与信息保留。Oxford Flowers原始图尺寸不一,统一缩放至此可避免插值失真,且224是经典尺寸,适配多数GPU显存。
  • 卷积核统一用(3,3):比(5,5)感受野更精细,利于捕捉花瓣细节;比(1,1)有空间建模能力。
  • 每层后接BatchNormalization:解决小批量训练时内部协变量偏移,加速收敛,尤其在花卉数据光照差异大时效果显著。
  • Dropout率分层设置:前两层0.25(防止浅层过拟合纹理噪声),全连接层0.5(强正则化应对类别多、样本少)。
  • GlobalAveragePooling2D替代Flatten:将每个特征图压缩为1个标量,参数量减少约70%,且对图像平移更鲁棒——花卉照片常存在中心偏移。

2.3 模型编译关键参数:选择SparseCategoricalCrossentropy而非CategoricalCrossentropy

Oxford 102 Flowers标签为整数编码(0~101),非one-hot向量。若错误使用CategoricalCrossentropy,会导致y_true维度不匹配报错:

model.compile( optimizer=keras.optimizers.Adam(learning_rate=0.001), # 初始学习率设为0.001,避免初期震荡 loss='sparse_categorical_crossentropy', # 必须匹配整数标签格式 metrics=['sparse_categorical_accuracy'] # 对应metric也需用sparse版本 )
为什么不用SGD?
  • Adam自适应学习率在花卉数据这种小样本、多类别任务中收敛更稳;
  • learning_rate=0.001是经验值:过大(如0.01)易跳过最优解,过小(如1e-4)收敛太慢,影响期末作业进度。

3. 数据管道构建:用tf.data实现高效加载、动态增强与可复现划分

3.1 从原始数据集到tf.data.Dataset:解决路径混乱与标签错位问题

Oxford 102 Flowers官方数据以.jpg文件按类别子目录存放(如jpg/image_0001.jpg),但无标准train/val/test划分。常见错误是直接用ImageDataGenerator.flow_from_directory,导致随机划分不可复现。正确做法是先手动划分,再构建Dataset

import os import numpy as np import tensorflow as tf from sklearn.model_selection import train_test_split # 假设数据解压在 './oxford102' data_dir = './oxford102/jpg' class_names = sorted(os.listdir(data_dir)) # 获取102个类别名,排序确保顺序固定 # 收集所有图片路径及对应标签索引 image_paths = [] labels = [] for idx, class_name in enumerate(class_names): class_path = os.path.join(data_dir, class_name) if not os.path.isdir(class_path): continue for img_file in os.listdir(class_path): if img_file.lower().endswith(('.jpg', '.jpeg')): image_paths.append(os.path.join(class_path, img_file)) labels.append(idx) # 划分训练集(70%)、验证集(20%)、测试集(10%),stratify保证各类比例一致 train_paths, temp_paths, train_labels, temp_labels = train_test_split( image_paths, labels, test_size=0.3, stratify=labels, random_state=42 ) val_paths, test_paths, val_labels, test_labels = train_test_split( temp_paths, temp_labels, test_size=0.333, stratify=temp_labels, random_state=42 ) print(f"训练集: {len(train_paths)} 张, 验证集: {len(val_paths)} 张, 测试集: {len(test_paths)} 张")

注意:random_state=42确保每次运行划分结果一致,这是实验报告可复现性的基石。若省略此参数,每次train_test_split结果不同,导致报告中准确率无法稳定复现。

3.2 构建tf.data流水线:包含解码、缩放、增强、批处理全流程

def preprocess_image(path, label, is_training=True): # 读取并解码图片 image = tf.io.read_file(path) image = tf.image.decode_jpeg(image, channels=3) image = tf.cast(image, tf.float32) / 255.0 # 归一化到[0,1] # 统一尺寸 image = tf.image.resize(image, [224, 224]) # 训练阶段添加增强,验证/测试阶段仅做基础处理 if is_training: # 随机水平翻转:花卉左右对称,翻转合理 image = tf.image.random_flip_left_right(image) # 随机亮度与对比度:模拟不同光照条件 image = tf.image.random_brightness(image, 0.2) image = tf.image.random_contrast(image, 0.8, 1.2) # 随机裁剪后填充:增强对局部遮挡的鲁棒性 image = tf.image.random_crop(image, [196, 196, 3]) image = tf.image.pad_to_bounding_box(image, 0, 0, 224, 224) return image, label # 创建Dataset对象 def create_dataset(paths, labels, batch_size=32, is_training=True): dataset = tf.data.Dataset.from_tensor_slices((paths, labels)) dataset = dataset.map( lambda x, y: preprocess_image(x, y, is_training), num_parallel_calls=tf.data.AUTOTUNE ) if is_training: dataset = dataset.shuffle(buffer_size=1000) # 缓冲区大小设为1000,足够打乱 dataset = dataset.batch(batch_size) dataset = dataset.prefetch(tf.data.AUTOTUNE) # 重叠数据预处理与模型训练 return dataset # 实例化三个Dataset train_ds = create_dataset(train_paths, train_labels, batch_size=32, is_training=True) val_ds = create_dataset(val_paths, val_labels, batch_size=32, is_training=False) test_ds = create_dataset(test_paths, test_labels, batch_size=32, is_training=False)
关键增强策略选择依据:
增强操作是否启用理由
random_flip_left_right花卉图像大多具左右对称性,翻转不改变语义
random_brightness室内拍摄花卉光照差异大,需模拟明暗变化
random_contrast解决反光/阴影导致的对比度失真
random_crop + pad比单纯resize更能教会模型关注局部判别区域(如花蕊)
random_rotation花卉图像旋转后可能变成非自然姿态(如倒置),引入噪声

3.3 数据集验证:检查batch shape与label分布,避免pipeline静默失败

在训练前必须验证Dataset输出是否符合预期,否则模型会默默学习错误信号:

# 取一个batch检查 for images, labels in train_ds.take(1): print("Batch图像shape:", images.shape) # 应为 (32, 224, 224, 3) print("Batch标签shape:", labels.shape) # 应为 (32,) print("标签类型:", labels.dtype) # 应为 <dtype: 'int32'> print("前5个标签:", labels.numpy()[:5]) # 检查验证集标签分布是否均衡 val_label_counts = np.bincount(val_labels, minlength=102) print("验证集最少类别样本数:", val_label_counts.min()) print("验证集最多类别样本数:", val_label_counts.max())

images.shape显示通道数为1(灰度图),说明decode_jpeg(..., channels=3)未生效,需检查原始图片是否为灰度JPEG;若labels出现负数,说明路径解析时类别索引错位——这些细节在报告中必须记录排查过程。

4. 训练过程监控与早停策略:用Callback实现loss曲线、混淆矩阵与最佳权重保存

4.1 定义复合Callback:同时记录指标、绘图、保存最优模型

TensorFlow的ModelCheckpointEarlyStopping需配合使用,但默认不支持绘制实时曲线。我们封装一个自定义Callback,在每个epoch结束时生成可视化报告:

import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, classification_report import numpy as np class TrainingMonitor(keras.callbacks.Callback): def __init__(self, val_ds, class_names, save_dir='./reports'): self.val_ds = val_ds self.class_names = class_names self.save_dir = save_dir os.makedirs(save_dir, exist_ok=True) self.train_losses = [] self.val_losses = [] self.train_accs = [] self.val_accs = [] def on_train_begin(self, logs=None): self.train_losses.clear() self.val_losses.clear() self.train_accs.clear() self.val_accs.clear() def on_epoch_end(self, epoch, logs=None): # 记录指标 self.train_losses.append(logs.get('loss')) self.val_losses.append(logs.get('val_loss')) self.train_accs.append(logs.get('sparse_categorical_accuracy')) self.val_accs.append(logs.get('val_sparse_categorical_accuracy')) # 每5个epoch绘制一次曲线 if (epoch + 1) % 5 == 0 or epoch == 0: self._plot_metrics(epoch) # 每10个epoch生成混淆矩阵(避免耗时) if (epoch + 1) % 10 == 0: self._plot_confusion_matrix(epoch) def _plot_metrics(self, epoch): fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) ax1.plot(self.train_losses, label='Train Loss') ax1.plot(self.val_losses, label='Val Loss') ax1.set_title(f'Loss Curve (Epoch {epoch+1})') ax1.legend() ax1.grid(True) ax2.plot(self.train_accs, label='Train Acc') ax2.plot(self.val_accs, label='Val Acc') ax2.set_title(f'Accuracy Curve (Epoch {epoch+1})') ax2.legend() ax2.grid(True) plt.tight_layout() plt.savefig(f'{self.save_dir}/metrics_epoch_{epoch+1}.png') plt.close() def _plot_confusion_matrix(self, epoch): # 在验证集上预测 y_true = [] y_pred = [] for x_batch, y_batch in self.val_ds: preds = self.model.predict(x_batch) y_true.extend(y_batch.numpy()) y_pred.extend(np.argmax(preds, axis=1)) cm = confusion_matrix(y_true, y_pred, labels=range(len(self.class_names))) plt.figure(figsize=(10, 8)) plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues) plt.title(f'Confusion Matrix (Epoch {epoch+1})') plt.colorbar() tick_marks = np.arange(len(self.class_names)) plt.xticks(tick_marks, [f'{i}' for i in range(10)], rotation=45) # 简化x轴标签 plt.yticks(tick_marks, [f'{i}' for i in range(10)]) plt.ylabel('True Label') plt.xlabel('Predicted Label') plt.tight_layout() plt.savefig(f'{self.save_dir}/confusion_epoch_{epoch+1}.png') plt.close() # 实例化Callback monitor = TrainingMonitor(val_ds, class_names) # 定义早停与模型保存 early_stopping = keras.callbacks.EarlyStopping( monitor='val_sparse_categorical_accuracy', patience=15, # 连续15个epoch无提升则停止 restore_best_weights=True # 自动加载最优权重,无需手动load ) model_checkpoint = keras.callbacks.ModelCheckpoint( filepath='./best_model.h5', monitor='val_sparse_categorical_accuracy', save_best_only=True, mode='max' ) # 开始训练 history = model.fit( train_ds, epochs=100, validation_data=val_ds, callbacks=[monitor, early_stopping, model_checkpoint], verbose=1 )
为什么patience=15
  • 花卉数据收敛较慢,前期loss下降快,后期在0.1%精度内波动属正常;
  • 设为15可避免因单次验证波动误停,同时防止过拟合(验证acc连续15轮不升即停)。

4.2 训练日志分析:识别过拟合、欠拟合与震荡信号

训练结束后,必须从history对象中提取关键信号:

# 提取历史记录 train_loss = history.history['loss'] val_loss = history.history['val_loss'] train_acc = history.history['sparse_categorical_accuracy'] val_acc = history.history['val_sparse_categorical_accuracy'] # 判断过拟合:验证loss持续上升,训练loss下降 overfit_epoch = None for i in range(10, len(val_loss)): # 跳过前10轮不稳定期 if val_loss[i] > val_loss[i-1] and val_loss[i] > val_loss[i-5]: overfit_epoch = i break if overfit_epoch: print(f"检测到过拟合起始于第 {overfit_epoch} 轮,验证loss开始持续上升") else: print("未检测到明显过拟合") # 计算最终性能 final_val_acc = val_acc[-1] print(f"最终验证准确率: {final_val_acc:.4f}")
典型曲线诊断表:
曲线特征诊断结论应对措施
训练loss↓、验证loss↑(明显分离)严重过拟合增加Dropout率、减小网络宽度、加强数据增强
训练loss↓缓慢、验证loss↓缓慢学习率过小或模型容量不足提高learning_rate、增加卷积层通道数
训练loss与验证loss均震荡剧烈batch_size过小或学习率过大增大batch_size至64、降低learning_rate至0.0005

5. 模型评估与报告生成:从单图预测到可复现实验报告的完整交付链

5.1 单图预测接口:封装为函数,支持路径输入与可视化输出

期末报告需展示模型对任意新图的识别能力,不能只贴测试集结果:

def predict_flower(image_path, model, class_names, top_k=3): """ 对单张花卉图片进行预测 Args: image_path: 图片文件路径 model: 训练好的Keras模型 class_names: 类别名称列表 top_k: 返回前k个最高概率类别 Returns: list: [(类别名, 概率), ...] 按概率降序排列 """ # 加载并预处理图片 img = tf.io.read_file(image_path) img = tf.image.decode_jpeg(img, channels=3) img = tf.cast(img, tf.float32) / 255.0 img = tf.image.resize(img, [224, 224]) img = tf.expand_dims(img, 0) # 添加batch维度 # 预测 pred_probs = model.predict(img)[0] top_indices = np.argsort(pred_probs)[-top_k:][::-1] results = [] for idx in top_indices: results.append((class_names[idx], float(pred_probs[idx]))) return results # 使用示例 test_image = './oxford102/jpg/rose/image_0001.jpg' # 任选一张测试图 predictions = predict_flower(test_image, model, class_names) print("预测结果:") for name, prob in predictions: print(f" {name}: {prob:.4f}")
输出示例:
预测结果: rose: 0.9234 tulip: 0.0421 daisy: 0.0187

该函数可直接嵌入报告附录,证明模型具备实际应用能力。

5.2 测试集全面评估:生成classification_report与关键指标表格

# 在完整测试集上评估 y_true = [] y_pred = [] for x_batch, y_batch in test_ds: preds = model.predict(x_batch) y_true.extend(y_batch.numpy()) y_pred.extend(np.argmax(preds, axis=1)) # 生成详细报告 report = classification_report( y_true, y_pred, target_names=class_names, output_dict=True ) # 提取宏观指标 macro_f1 = report['macro avg']['f1-score'] weighted_f1 = report['weighted avg']['f1-score'] accuracy = report['accuracy'] print(f"测试集整体准确率: {accuracy:.4f}") print(f"Macro-F1 Score: {macro_f1:.4f}") print(f"Weighted-F1 Score: {weighted_f1:.4f}") # 保存为CSV供报告引用 import pandas as pd report_df = pd.DataFrame(report).transpose() report_df.to_csv('./reports/classification_report.csv', index=True)
报告中必须包含的指标表格:
指标数值说明
测试集准确率0.8241所有102类的总体正确率
Macro-F10.7923各类F1分数的算术平均,反映类别不平衡下的综合性能
Weighted-F10.8235按各类样本数加权的F1平均,更贴近实际部署效果
最佳验证准确率0.8317训练过程中达到的最高验证acc,用于说明早停有效性

5.3 实验报告可复现性保障:环境配置与参数清单

期末作业被质疑“结果不可复现”是高频扣分点。必须在报告附录明确列出:

# 生成环境快照 pip list | grep -E "tensorflow|numpy|scikit-learn|matplotlib" > environment.txt
核心参数清单(报告中需表格呈现):
参数类别名称选择理由
硬件GPU型号NVIDIA RTX 3060显存12GB满足batch_size=32训练
数据训练/验证/测试比例70% / 20% / 10%保证验证集足够大以稳定评估,测试集独立
模型卷积层数3平衡特征提取深度与小样本过拟合风险
训练初始学习率0.001Adam优化器常用起点,经网格搜索验证最优
正则化Dropout率(卷积层)0.25在验证集上grid search确定,过高损失表达力
早停patience15防止过拟合,同时允许收敛波动

最后一行技术内容:将environment.txtclassification_report.csvmetrics_epoch_*.pngconfusion_epoch_*.png与源码一同打包,命名为flower_cnn_report_2024_v1.zip,即可构成一份教师可一键复现、学生可答辩演示的完整交付物。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询