简介:该PDF文档是围绕基于卷积神经网络的花朵品种识别问题的学术论文,适合深度学习、机器学习与图像识别方向的学习者,以及需要参考图像分类项目设计思路的研究人员。文档从数据来源与预处理、CNN模型构建到BP参数优化展开,采用ReLU激活函数调节输出,在牛津大学102种花卉数据集基础上新增5类,实验获得83.01%的整体准确率,随机抽取5种花卉识别时最高准确率达到85%。这份单文件PDF容量约5.59MB,内容覆盖卷积层、池化层、全连接层结构、图像尺寸统一与花朵区域选择算法、模型训练与评估方法,可帮助读者理解花卉图像识别的完整实验流程和调参细节。目前站点已有239人学习,适合用作物种识别任务入门、深度学习课程设计及论文写作中的结构参考。
1. 一份能直接照抄的 CNN 花朵识别方案
拿到《基于卷积神经网络的花朵品种的识别》这篇论文 PDF,我的第一反应是:这不就是现成的计算机视觉课程设计模板吗。全篇没有复杂花活——标准 CNN 结构,两层卷积、两层池化、两层全连接,在牛津大学 102 种花卉数据集基础上新增 5 类共 107 类样本,训练准确率 83.01%,随机抽 5 类测试单类最高 85%。这个数字不算惊艳,但对固定数据集、有限算力、要从零搭模型的课设来说,已经很有说服力。它覆盖了图像分类项目最完整的链路:数据收集、预处理、造 Tfrecord、搭网络、调参、保存模型、随机抽样测试。适合两类人——准备做图像识别毕设或课设的学生,以及想在 TensorFlow 老式队列输入流程里完整跑一遍数据管道的从业者。
2. 数据集先于模型:102 类底库加 5 类手筛样本,预处理避坑是关键
2.1 107 类数据是怎么搭出来的
论文的数据集不是凭空造的,它建立在牛津大学 102 类花卉数据集之上。这个公开数据集本身有 8188 张图片,每类样本量在 40 到 250 张之间。作者在此基础上从互联网收集了 5 类花卉共 500 张图片,手动过滤和分类,最终得到 107 类 8688 张样本,每类 40 到 300 张不等。
| 数据来源 | 类别数 | 样本量 | 说明 |
|---|---|---|---|
| Oxford 102 Flowers 公开集 | 102 | 8188 张 | 每类 40 ~ 250 张,类间相似度高 |
| 互联网收集后手工筛选 | 5 | 500 张 | 手动过滤噪声、按类别分文件夹 |
| 合并后总计 | 107 | 8688 张 | 每类 40 ~ 300 张,按类存放 |
这里有个容易被忽略的工程点:新增的 5 类不是简单爬下来就完事,而是“手动过滤和分类”。花卉图片爬下来往往包含无关背景、多人合影、文字水印,不筛掉直接进训练集,模型学到的是噪声而不是花瓣纹理。常见做法是下载后先做一轮人工清洗,把模糊、遮挡严重、主体占比太小的图删掉,再按品种文件夹归类。
作者把样本按类别放到 107 个文件夹下,这一点对后续做标签对齐很重要。工程上目录结构类似:
data/ ├── 001_oxford_daisy/ │ ├── image_0001.jpg │ └── ... ├── 002_oxford_sunflower/ │ └── ... └── 103_extra_xxx/ └── ...花卉分类为什么比汽车、桌面这类粗粒度分类难?论文给了一个很准确的说法:类间相似性高、类内变异大。同一品种在不同光照、角度、盛开程度下差异很大,而不同品种之间可能只差在花瓣边缘曲率或颜色深浅。这种细粒度分类问题,靠手工设计特征往往抓不住关键差异。
2.2 先找花朵中心再统一尺寸:直接裁剪大概率翻车
论文预处理部分有一个让我印象深刻的细节:不能直接裁图。如果直接把所有图片强行压缩到固定尺寸,那些花朵不在图像中心的样本会被裁掉花朵本身,只剩背景。作者的处理方式是先用花朵区域选择算法确定花的区域中心点,再基于该中心对图像进行放缩,统一到 100×100 尺寸。
我一般会把这个流程拆成四步:第一步检测或标注出花朵包围盒,第二步以包围盒中心为基准做裁剪或平移填充,第三步 resize 到 100×100,第四步对 RGB 三通道做归一化,把像素值从 0~255 缩到模型好处理的区间。论文明确写了“对所有图片的 RGB 进行归一化处理,根据不同种类添加标签”,这一步能明显加快收敛。
这里说句实在话:100 ppi × 100 ppi 这个单位在论文里写得不严谨。ppi 是打印分辨率概念,工程实现时按 100×100 像素处理就好。真按打印分辨率去换算,图像尺寸反而会乱套。
2.3 生成 Tfrecord:把图像和标签打包成一个文件
论文选择了 Tfrecord 格式,理由是“能更好地利用内存,在 TensorFlow 中快速地复制、移动、读取、存储”。Tfrecord 本质是二进制文件,把图像数据和标签统一存储,训练时顺序读取比逐张读图片要快得多,尤其是在机械硬盘环境下,能省掉大量小文件寻道时间。
一个可复现的生成脚本,核心逻辑大致如下:
# build_tfrecord.py import cv2 import numpy as np import tensorflow as tf IMAGE_W, IMAGE_H = 100, 100 def _bytes_feature(value): # Tfrecord 的 bytes 类型特征封装 return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value])) def process_one_image(img_path, label): # 读取图片并统一尺寸,注意 BGR 到 RGB 的通道转换 img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (IMAGE_W, IMAGE_H)) img = img.astype(np.float32) / 255.0 # RGB 归一化到 [0, 1] return img.tobytes() def create_tfrecord(image_list, label_list, output_file): writer = tf.python_io.TFRecordWriter(output_file) # TF2 中改为 tf.io.TFRecordWriter for img_path, label in zip(image_list, label_list): raw_image = process_one_image(img_path, label) feature = { "image": _bytes_feature(raw_image), "label": tf.train.Feature(int64_list=tf.train.Int64List(value=[label])) } example = tf.train.Example(features=tf.train.Features(feature=feature)) writer.write(example.SerializeToString()) writer.close()这段代码里有几个参数值得说明。IMAGE_W和IMAGE_H对应论文里设定的 100×100 输入尺寸;tobytes()把归一化后的浮点数组序列化成字节流,读取时再还原;标签用int64存储,训练时直接作为交叉熵的 ground truth。tf.python_io.TFRecordWriter是 TensorFlow 1.x 的写法,如果环境是 2.x,换成tf.io.TFRecordWriter即可。
生成完 Tfrecord 之后,论文还做了一步关键操作:把 image_list 和 label_list 分别打乱,按 4:1 比例划分训练集和测试集。打乱顺序这件事不能省,否则连续 batch 里全是同一类别,模型会很快过拟合。传入get_batch()函数生成输入队列时,还要设置固定的图像高度宽度、batch_size 和队列最大容量。
3. 网络结构拆解:两个卷积层加两个全连接,参数为什么这么设
3.1 为什么不走传统特征工程路线
论文对比的基线是 Nilsback 和 Zisserman 的方法:提取颜色、形状、梯度直方图等底层特征,训练 SVM 分类器,在 103 类数据集上准确率 72.8%。作者的 CNN 在类别数多 4 类的前提下,把准确率拉到 83.01%,差距超过 10 个百分点。
这个对比背后是两类技术路线的本质区别。传统方法要先定义“什么特征重要”——颜色直方图、纹理、形状描述子,然后期望这些手工特征恰好能区分所有品种。但花分类是细粒度问题,不同品种可能共享大量底层特征,手工设计很难覆盖全部判别信息。CNN 的做法是让卷积核自己学习该看什么:第一层学边缘和颜色块,第二层学花瓣纹理组合,高层学品种级整体模式。
论文对 CNN 的定位是“前馈神经网络”,分成输入层、卷积层、激活函数层、池化层、全连接层和输出层。卷积层通过局部感受野挖掘空间局部关联,每个神经元只和上一层的部分像素相连,权重数量等于卷积核大小。这比全连接网络参数量小得多,也符合图像“邻近像素关系紧密、远处像素关系弱”的先验。
3.2 结构参数与模型骨架
论文的模型结构很清晰:两个卷积层、两个池化层、两个全连接层,最后接 softmax。具体参数如下:
| 层名 | 卷积核 / 池化窗口 | 核数量 | 步长 | 输出说明 |
|---|---|---|---|---|
| Conv1 | 3×3 | 64 个 3×3×3 | 1 | 保持原尺寸,提取底层特征 |
| MaxPool1 | 3×3 | — | 2 | 降采样,降低维度 |
| Conv2 | 3×3 | 16 个 3×3×64 | 1 | 输入为上一层特征图 |
| MaxPool2 | 3×3 | — | 2 | 再次降采样 |
| FC1 | — | 128 个神经元 | — | 全连接,接 ReLU |
| FC2 | — | 128 个神经元 | — | 全连接,接 softmax |
卷积核数量从第一层的 64 降到第二层的 16,这个设计值得琢磨。第一层直接面对原始图像,需要较多卷积核去覆盖边缘、颜色、纹理等多样性特征;第二层输入已经是池化压缩后的高层语义特征,信息密度更高,16 个卷积核足够提取品种级判别信息。如果你复现时发现模型容量不够,优先加第一层卷积核数量,而不是盲目加层。
基于 TensorFlow 的模型骨架大致长这样:
# cnn_model.py import tensorflow as tf def cnn_model(x, keep_prob=0.5): # conv1: 64 个 3x3 卷积核,步长 1,SAME 保持尺寸 conv1 = tf.layers.conv2d(x, filters=64, kernel_size=3, strides=1, padding="SAME", activation=tf.nn.relu) # 池化: 3x3 窗口,步长 2,尺寸减半 pool1 = tf.layers.max_pooling2d(conv1, pool_size=3, strides=2) # conv2: 16 个 3x3 卷积核,输入通道数自动取上一层 64 conv2 = tf.layers.conv2d(pool1, filters=16, kernel_size=3, strides=1, padding="SAME", activation=tf.nn.relu) pool2 = tf.layers.max_pooling2d(conv2, pool_size=3, strides=2) flat = tf.layers.flatten(pool2) # 两层全连接,每层 128 个神经元 fc1 = tf.layers.dense(flat, units=128, activation=tf.nn.relu) fc1 = tf.layers.dropout(fc1, rate=keep_prob) # 随机忽略神经元,防过拟合 fc2 = tf.layers.dense(fc1, units=128, activation=tf.nn.relu) logits = tf.layers.dense(fc2, units=107) # 107 类输出,softmax 在损失函数中计算 return logits代码里filters=64对应论文的第一层 64 个卷积核,kernel_size=3对应 3×3 卷积,strides=1对应步长 1。池化层用pool_size=3, strides=2,窗口有重叠,这比 2×2 步长 2 的无重叠池化保留了更多位置信息。units=107是输出类别数,必须和数据集类别数一致,改数据集时最容易漏改这里。
3.3 激活函数与 Dropout:一个提速,一个防过拟合
激活函数的选型,论文给出了明确理由:ReLU 在 x<0 时硬饱和,x>0 时导数为 1,梯度不衰减,能缓解梯度消失问题,收敛更快。Sigmoid 在两端饱和,反向传播时梯度会越乘越小,深层网络基本训不动;Tanh 比 Sigmoid 好一些,但同样存在饱和区。ReLU 的计算代价最低,实际训练中收敛速度优势非常明显。
这里顺带提一个论文排版问题:公式(12)把 ReLU 写成 f(x)=0(x<0)、f(x)=1(x≥0),这明显是笔误,正确写法是 f(x)=x(x≥0)。复现的时候不要被这个公式误导。
Dropout 加在两个全连接层之间,而不是卷积层之间,原因在于全连接层参数量最大、最容易过拟合。107 类 8688 张图的数据规模不算大,全连接层如果不做约束,训练准确率会很高但测试准确率跟不上。随机忽略一部分神经元,本质上是让模型不依赖单一路径,增强鲁棒性。keep_prob=0.5是常见配置,训练时用 0.5,测试时要设 1.0。
4. 训练节奏与调参次序:先用 5 类冒烟,再上 107 类全量
4.1 冒烟测试:batch size 和学习率怎么摸出来
论文的训练策略很务实:先用 5 种花卉小规模训练调参,摸清参数变化对准确率的影响,再上 107 类全量数据。这种做法我在实际项目里也常用——小样本跑一个 epoch 只要几十秒,可以快速验证模型能不能收敛、梯度有没有爆掉,比直接全量训练省时间。
图 3 和图 4 是两个关键结论。第一,batch size 对准确率的影响:相同迭代次数下,batch_size=100 时准确率达到最大稳定值最快。batch 太小,梯度方向抖动大,收敛慢;batch 太大,每个 step 的更新次数变少,同等迭代轮次下反而学不够。第二,学习率的影响:learning_rate=0.001 时准确率最高。学习率本质是梯度下降的步长,步长太大会在最优解附近来回震荡,甚至发散;步长太小则沿着目标函数斜率方向移动太慢,训练时间成倍拉长。
这两个参数是联动的。批量越大,梯度估计越准,可以适当加大学习率;批量越小,梯度噪声越大,学习率要调小。论文给出的组合不是玄学,而是对比实验扫出来的结果。
4.2 输入队列参数:batch、capacity 与 max_step
论文的表 1 给出了完整参数配置,我整理成方便对照的格式:
| 参数名 | 取值 | 作用 |
|---|---|---|
| Bias | 1 | 偏置初始值 |
| batch_size | 100 | 每个训练批次样本数 |
| learning_rate | 0.001 | Adam 优化器学习率 |
| capacity | 200 | 输入队列最大容量 |
| max_step | 1000 | 最大训练步数 |
capacity=200这个值是 batch_size 的两倍,目的是保证队列里有足够的缓冲数据,不至于每次取 batch 都阻塞等待磁盘 I/O。如果 capacity 设得比 batch_size 还小,训练进程会频繁停下等数据,GPU 利用率上不去。输入队列用get_batch()生成,需要预先设置 image_W、image_H、batch_size、capacity 四个参数,队列内部从 Tfrecord 中读取图像和标签。
4.3 全量训练与模型保存
全量训练时,论文按 4:1 比例划分训练集和测试集,随机初始化权值矩阵和阈值,然后反复迭代直到收敛。训练过程的核心逻辑如下:
# train_cnn.py import tensorflow as tf # 省略 get_batch 实现,核心是构造输入队列 images, labels = get_batch(image_list, label_list, IMAGE_W, IMAGE_H, batch_size=100, capacity=200) logits = cnn_model(images, keep_prob=0.5) # 多分类损失,内部包含 softmax 计算 loss = tf.reduce_mean( tf.nn.sparse_softmax_cross_entropy_with_logits(labels=labels, logits=logits)) # Adam 优化器,学习率 0.001 train_op = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss) # 统计准确率 correct_pred = tf.equal(tf.argmax(logits, 1), labels) accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32)) saver = tf.train.Saver() with tf.Session() as sess: sess.run(tf.global_variables_initializer()) coord = tf.train.Coordinator() threads = tf.train.start_queue_runners(sess=sess, coord=coord) for step in range(1000): # max_step = 1000 _, train_acc = sess.run([train_op, accuracy]) if step % 100 == 0: print("step %d, acc %.4f" % (step, train_acc)) saver.save(sess, "model/model.ckpt") # 保存模型供测试调用 coord.request_stop() coord.join(threads)训练代码里有三个参数需要特别说明。learning_rate=0.001是图中扫出来的最优值,如果你换了数据集,这个值必须重新验证。keep_prob=0.5只在训练时生效,测试时要改成 1.0,否则预测结果会带上随机性。max_step=1000是论文设定的训练步数,但收敛步数会随数据量变化,建议边训边看准确率曲线,准确率连续多步不再上升就是该停的时候。
论文特别提到,与一般神经网络相比,CNN 准确率提高、收敛速度加快,但训练时间拉长了。这是因为卷积层有大量参数需要反向传播更新,每一轮 forward 和 backward 的计算量都比普通全连接网络大。如果你用 CPU 训练 107 类数据,1000 步可能要跑数小时,这是正常现象。
4.4 测试方法:随机抽 5 类看单类准确率
论文的测试方法很有代表性:从 107 类花卉中随机抽取 5 种,每种 20 张放入同一个文件夹,调用训练保存的model.ckpt,自动识别分类。测试结果是一张单类准确率表:
| 花朵种类 | 测试数量 | 成功识别数量 | 准确率 |
|---|---|---|---|
| 种 1 | 20 | 15 | 75% |
| 种 2 | 20 | 16 | 80% |
| 种 3 | 20 | 17 | 85% |
| 种 4 | 20 | 15 | 75% |
| 种 5 | 20 | 14 | 70% |
单类最高 85%,最低 70%。这种测试方式的价值在于能看出模型在哪些类上表现差,而不只是给一个笼统的平均准确率。如果你复现时发现某一类准确率特别低,优先检查这一类是不是和某个品种长得太像。
5. 避坑清单:从数据到训练,我踩过的五个坑
5.1 直接裁剪把中心花朵裁掉了
现象:训练集图片里花朵主体不完整,模型准确率一直上不去,看起来像没收敛。
原因:预处理时直接按固定窗口裁剪或缩放,花朵不在图片中心的样本会被裁掉,模型学到的是残缺花瓣和背景的混合特征。这个问题在公开数据集里非常常见,摄像师不会总把花放在画面正中。
解决:先用花朵区域选择算法确定花的包围盒,以包围盒中心为基准做裁剪或平移填充,再 resize 到统一尺寸。论文明确走了这条路,这也是它的预处理比普通课程设计严谨的地方。复现时如果没有现成的检测器,可以先用标注工具标出花朵位置,至少保证训练集里主体完整。
5.2 相似品种被混淆,单类准确率上不去
现象:整体准确率 83%,但细看单类测试,总有一两类准确率掉到 70% 甚至更低。
原因:花卉品种之间类间相似性高,比如颜色相近、花瓣形态接近的品种,卷积核提取的特征区分度不足。
解决:不要只看 top-1 准确率,把预测结果按真实类别分组,看错误样本被识别成了哪一类。如果是两三类之间互相混,优先增加这些类别的样本数量,或者调大第一层卷积核数量,让模型有更多特征图去捕捉细微差异。
5.3 tfrecord 队列 capacity 设太小,训练被拖慢
现象:训练时显存或 CPU 利用率忽高忽低,一个 step 要等很久,训练速度远低于预期。
原因:输入队列容量不够,数据生产速度跟不上模型消费速度,队列频繁空转等待磁盘 I/O。
解决:capacity 至少设置为 batch_size 的 2 倍。论文里 batch_size=100、capacity=200 就是这个思路。如果数据量更大,建议用tf.train.shuffle_batch配合多个 reader 并行读取,把 capacity 再往上提,同时保证队列里有足够的随机样本。
5.4 学习率和 batch size 照搬,换数据就失效
现象:在 5 类小样本上调好的参数,直接套到 107 类上,准确率起不来或者震荡剧烈。
原因:batch size 影响梯度估计的噪声水平,学习率影响更新步长,这两个参数和数据规模、类别数、图像尺寸都有关。小样本最优参数是全局最优的概率并不高,照搬自然会出问题。
解决:每次换数据集都重新扫一遍参数。不用全量扫,先固定 batch size,对比学习率 0.01、0.001、0.0001 三档;再固定学习率,对比 batch size 64、100、128。训练几百步看曲线趋势就能判断,不必等完全收敛。
5.5 全连接层过拟合,训练和测试差距大
现象:训练准确率接近 100%,测试准确率却只有 80% 左右,差十几个百分点。
原因:全连接层参数量占整个网络的大头,107 类 8688 张图的规模相对模型容量偏小,网络把训练集细节背了下来,没有学到可泛化的特征。
解决:在全连接层之间加 Dropout,训练时随机忽略一部分神经元,论文的做法是 keep_prob 设为 0.5。如果你复现时过拟合仍然严重,可以再把 Dropout 概率调低到 0.3,或者减少全连接层神经元数量,从 128 降到 64。
6. 模型评测的进阶玩法:逐类准确率反推模型短板
6.1 用混淆矩阵定位“长得像”的类
论文的随机抽 5 类测试已经迈出了正确的一步,但它只给出了单类准确率,没有进一步展示误分类的方向。我复现这类项目时,会顺手把混淆矩阵算出来,因为它的信息密度比单类准确率高得多。测试脚本大致这样扩展:
# evaluate_per_class.py import numpy as np results = [] # 每条记录是 (真实标签, 预测标签),由 model.ckpt 批量预测得到 num_classes = 107 confusion = np.zeros((num_classes, num_classes), dtype=int) for true_label, pred_label in results: confusion[true_label][pred_label] += 1 # 按行归一化,对角线就是每个类别的准确率 row_sum = confusion.sum(axis=1, keepdims=True) per_class_acc = confusion.diagonal() / row_sum.squeeze() # 找出准确率最低的 5 类,再看它们被误判成了谁 worst = np.argsort(per_class_acc)[:5] for cls in worst: confusion_row = confusion[cls] top_confuse = np.argsort(-confusion_row)[:3] print("类 %d 准确率 %.2f%%" % (cls, per_class_acc[cls] * 100)) print(" 主要误判目标:", [int(c) for c in top_confuse if c != cls])这段代码把预测结果按真实类别聚合,对角线是每类自评准确率,非对角线是误判方向。它会告诉你两个关键信息:哪些类别是模型的短板,以及这些短板被错认成了谁。我一般会在得到混淆矩阵后,把“类 A 被误判为类 B”次数最多的那几对图片翻出来对比,几乎每次都能发现花瓣颜色或花型轮廓的相似之处——这就是细粒度分类里最典型的失败模式。
6.2 结构升级的方向与算力代价
论文在结论里给出了几个可扩展方向:设置隐层、调整网络连接结构、改变滤波器数量和大小、增减网络层数和神经元数量。如果你复现完基础版想继续涨点,最常见的路径是从“两个卷积层”升级到更深的网络骨架。VGG 的核心是连续堆叠 3×3 卷积,Inception 是在同一层并行多种卷积核尺寸,ResNet 通过残差连接缓解深层梯度消失。论文的 83.01% 是浅层 CNN 的上限附近,换更深结构在同样数据集上通常能到 88% 以上,但训练时间会成倍增长——CNN 本身比一般神经网络训练时间长的特点,在深层网络里会更明显。
这篇论文最好的使用方式,是把 PDF 留在手边,对照第 3 章的结构参数表和这段推理逻辑,亲手把完整流程跑一遍。从那以后我每次复现类似论文,都强制先跑一遍 5 类小样本冒烟测试,确认 batch size 和学习率合理、损失在下降,再上全量数据。这个习惯帮我省掉的无效训练时间,远比想象中多。希望帮到你。
本文还有配套的精品资源,点击获取