基于ResNet的花卉图片分类 —— tensorflow版(Flowers image classification based on ResNet——tensorflow)

最近看了《TensorFlow深度学习实战(微课视频版)》 —— 清华大学出版社一书中的 11 章节《基于ResNet的花卉图片分类》,觉得写的不错,是个关于ResNet的好例子,所以整理下,分享给大家~

本代码使用 Tensorflow 框架,搭建 ResNet50 模型,对花卉数据集 —— Oxford 102 Flowers 中的图片进行迁移学习,从而实现对花卉图片的分类任务。

1. 环境搭建

python==3.7
tensorflow==2.5.0
scipy==1.6.2
Pillow==6.2.0
joblib==1.0.1

本人使用的是 CPU 进行训练

2. 数据集

数据集网址:https://www.robots.ox.ac.uk/~vgg/data/flowers/102/

3. 划分数据集

获取数据集后,还要根据数据集的划分文件对数据集进行划分。Python urllib 库提供了 urlretrieve() 函数可以直接将远程数据下载到本地。可以使用 urlretrieve() 函数下载所需文件;然后把压缩的图片文件进行解压,并解析分类标记文件和数据集划分文件;再根据数据集划分文件把数据集分成训练集、验证集和测试集;最后,向不同类别的数据集中按图片所标识的花的种类分类存放图片文件。代码如下所示:

 1 # encoding: utf-8
 2 import os
 3 from urllib.request import urlretrieve
 4 import tarfile
 5 from scipy.io import loadmat
 6 from shutil import copyfile
 7 import glob
 8 import numpy as np
 9 
10 
11 # 函数说明:按照分类(labels)拷贝未分组的图片到指定的位置
12 # Parameters:
13 #     data_path - 数据存放目录
14 #     labels - 数据对应的标签,需要按标签放到不同的目录
15 def copy_data_files(data_path, labels):
16     if not os.path.exists(data_path):
17         os.mkdir(data_path)
18 
19     # 创建分类目录,该数据集一共有102个类别
20     for i in range(0, 102):
21         os.mkdir(os.path.join(data_path, str(i)))
22 
23     for label in labels:
24         # 原图片路径
25         src_path = str(label[0])
26         # 目的图片路径
27         dst_path = os.path.join(data_path, label[1], src_path.split(os.sep)[-1])
28         copyfile(src_path, dst_path)
29 
30 
31 if __name__ == '__main__':
32     # 检查本地数据集目录是否存在,不存在则创建
33     data_set_path = "./data"
34     if not os.path.exists(data_set_path):
35         os.mkdir(data_set_path)
36     
37     # 下载 102 Category Flower 数据集并解压(图片文件)
38     flowers_archive_file = "102flowers.tgz"
39     # 官网网址
40     flowers_url_frefix = "https://www.robots.ox.ac.uk/~vgg/data/flowers/102/"
41     flowers_archive_path = os.path.join(data_set_path, flowers_archive_file)
42     if not os.path.exists(flowers_archive_path):
43         print("正在下载图片文件...")
44         urlretrieve(flowers_url_frefix + flowers_archive_file, flowers_archive_path)
45         print("图片文件下载完成.")
46         print("正在解压图片文件...")
47         tarfile.open(flowers_archive_path).extractall(path=data_set_path)
48         print("图片文件解压完成.")
49 
50     # 下载标识文件,标识不同文件的类别(标签文件)
51     flowers_labels_file = "imagelabels.mat"
52     flowers_labels_path = os.path.join(data_set_path, flowers_labels_file)
53     if not os.path.exists(flowers_labels_path):
54         print("正在下载标识文件...")
55         urlretrieve(flowers_url_frefix + flowers_labels_file, flowers_labels_path)
56         print("标识文件下载完成")
57     flower_labels = loadmat(flowers_labels_path)['labels'][0] - 1
58 
59     # 下载数据集分类文件,包含训练集、验证集和测试集(数据集划分文件)
60     sets_splits_file = "setid.mat"
61     sets_splits_path = os.path.join(data_set_path, sets_splits_file)
62     if not os.path.exists(sets_splits_path):
63         print("正在下载数据集分类文件...")
64         urlretrieve(flowers_url_frefix + sets_splits_file, sets_splits_path)
65         print("数据集分类文件下载完成")
66     sets_splits = loadmat(sets_splits_path)
67     
68     # 由于数据集分类文件中测试集数量比训练集多,所以进行了对调,并且把索引值-1,让它们从0开始
69     train_set = sets_splits['tstid'][0] - 1
70     valid_set = sets_splits['valid'][0] - 1
71     test_set = sets_splits['trnid'][0] - 1
72     
73     # 获取图片文件名并找到图片对应的分类标识
74     image_files = sorted(glob.glob(os.path.join(data_set_path, 'jpg', '*.jpg')))
75     # image_labels的大小是[8189, 2],第一列是图片名称,第二列是花的标签
76     image_labels = np.array([i for i in zip(image_files, flower_labels)])
77 
78     # 将训练集、验证集和测试集分别放在不同的目录下
79     print("正在进行训练集的拷贝...")
80     copy_data_files(os.path.join(data_set_path, 'train'), image_labels[train_set, :])
81     print("已完成训练集的拷贝,开始拷贝验证集...")
82     copy_data_files(os.path.join(data_set_path, 'valid'), image_labels[valid_set, :])
83     print("已完成验证集的拷贝,开始拷贝测试集...")
84     copy_data_files(os.path.join(data_set_path, 'test'), image_labels[test_set, :])
85     print("已完成测试集的拷贝,所有的图片下载和预处理工作已完成.")
86     

处理好后的结果如下图所示:

 4. 训练模型代码

  1 import numpy as np
  2 import os
  3 import glob
  4 import math
  5 from os.path import join as join_path
  6 import joblib
  7 from tensorflow.keras import backend as K
  8 from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
  9 from tensorflow.keras.preprocessing.image import ImageDataGenerator
 10 from tensorflow.keras.optimizers import Adam
 11 from tensorflow.keras.applications.resnet50 import ResNet50
 12 from tensorflow.keras.layers import (Input, Flatten, Dense, Dropout)
 13 from tensorflow.keras.models import Model
 14 
 15 
 16 # 函数说明:该函数用于重写 DirectoryIterator 的 next 函数,用于将 RGB 通道换成 BGR 通道
 17 def override_keras_directory_iterator_next():
 18     from keras.preprocessing.image import DirectoryIterator
 19 
 20     original_next = DirectoryIterator.next
 21 
 22     # 防止多次覆盖
 23     if 'custom_next' in str(original_next):
 24         return
 25 
 26     def custom_next(self):
 27         batch_x, batch_y = original_next(self)
 28         batch_x = batch_x[:, ::-1, :, :]
 29         return batch_x, batch_y
 30 
 31     DirectoryIterator.next = custom_next
 32 
 33 
 34 # 函数说明:创建 ResNet50 模型
 35 # Parameters:
 36 #     classes - 所有的类别
 37 #     image_size - 输入图片的尺寸
 38 # Returns:
 39 #     Model - 模型
 40 def create_resnet50_model(classes, image_size):
 41     # 利用 Keras 的 API 创建模型,并在该模型的基础上进行修改
 42     # include_top:是否保留顶层的全连接网络;input_tensor: 可选,可填入 Keras tensor 作为模型的输入(即 layers.Input() 输出的 tensor)
 43     # 权重下载地址:https://github.com/fchollet/deep-learning-models/releases/download/v0.1/resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5
 44     # 注意,一定要使用v0.1版本,不能使用v0.2版本,不然会报错
 45     base_model = ResNet50(include_top=False, input_tensor=Input(shape=image_size + (3,)),
 46                           weights="resnet50_weights_tf_dim_ordering_tf_kernels_notop_v0.1.h5")
 47     num = 0
 48     for layer in base_model.layers:
 49         num = num + 1
 50         layer.trainable = False
 51     print("num = ", num)
 52     
 53     x = base_model.output
 54     x = Flatten()(x)
 55     x = Dropout(0.5)(x)
 56     output = Dense(len(classes), activation='softmax', name='predictions')(x)
 57     return Model(inputs=base_model.input, outputs=output)  # !!!!!这里为什么输出的是178层?感觉只有2层的样子
 58 
 59 
 60 # 函数说明:根据每一类图片的数量不同给每一类图片附上权重
 61 # Parameters:
 62 #     classes - 所有的类别
 63 #     dir - 图片所在的数据集类别的目录,可以是训练集或验证集
 64 # Returns:
 65 #     classes_weight - 每一类的权重
 66 def get_classes_weight(classes, dir):
 67     class_number = dict()
 68     # k = 0
 69     # 获取每一类的图片数量
 70     for class_name in classes:
 71         # !!!原先错误代码,因为classed并不是按照自然顺序排序
 72         # class_number[k] = len(glob.glob(os.path.join(dir, class_name, '*.jpg')))
 73         # k += 1
 74         class_number[int(class_name)] = len(glob.glob(os.path.join(dir, class_name, '*.jpg')))
 75     
 76     # 计算每一类的权重
 77     total = np.sum(list(class_number.values()))  # 总图片数
 78     max_samples = np.max(list(class_number.values()))  # 最大的图片数量
 79     mu = 1. / (total / float(max_samples))
 80     keys = class_number.keys()
 81     classes_weight = dict()
 82     for key in keys:
 83         # 约等于 ln( float(max_samples)/float(class_number[key]) )
 84         score = math.log(mu * total / float(class_number[key]))
 85         classes_weight[key] = score if score > 1. else 1.
 86 
 87     return classes_weight
 88 
 89 
 90 if __name__ == '__main__':
 91     # 训练集、验证集、模型输出目录
 92     train_dir = "./data/train"
 93     valid_dir = "./data/valid"
 94     output_dir = "./saved_model"
 95 
 96     # 经过训练后的权重、模型、分类文件
 97     fine_tuned_weights_path = join_path(output_dir, 'fine-tuned-resnet50-weights.h5')
 98     weights_path = join_path(output_dir, 'model-weights.h5')
 99     model_path = join_path(output_dir, 'model-resnet50.h5')
100     classes_path = join_path(output_dir, 'classes-resnet50')
101 
102     # 创建输出目录
103     if not os.path.exists(output_dir):
104         os.mkdir(output_dir)
105 
106     # 由于使用 tensorflow 作为 keras 的 backone,所以图片格式设置为 channels_last
107     # 修改 DirectoryIterator 的 next 函数,改变 GRB 通道顺序
108     # 设置图片数据格式,channels_last表示图片数据的通道在最后一个维度
109     K.set_image_data_format('channels_last')
110     override_keras_directory_iterator_next()
111 
112     # 获取花卉数据类别(不同类别的图片放在不同的目录下,获取目录名即可)
113     # classes = sorted([o for o in os.listdir(train_dir) if os.path.isdir(os.path.join(train_dir, o))])
114     classes = sorted([o for o in os.listdir(train_dir) if os.path.isdir(os.path.join(train_dir, o))], key=int)
115 
116     # 获取花卉训练和验证图片的数量
117     train_sample_number = len(glob.glob(train_dir + '/**/*.jpg'))
118     valid_sample_number = len(glob.glob(valid_dir + '/**/*.jpg'))
119     
120     # 创建 Resnet50 模型
121     image_size = (224, 224)
122     model = create_resnet50_model(classes, image_size)
123     
124     # 冻结前 fr_n 层
125     fr_n = 10
126     for layer in model.layers[:fr_n]:
127         layer.trainable = False
128     for layer in model.layers[fr_n:]:
129         layer.trainable = True
130     
131     # 模型配置,使用分类交叉熵作为损失函数,使用 Adam 作为优化器,步长是 1e-5,并使用精确的性能指标
132     model.compile(loss='categorical_crossentropy', optimizer=Adam(lr=1e-5), metrics=['accuracy'])
133     
134     # 获取训练数据和验证数据的 generator
135     channels_mean = [103.939, 116.779, 123.68]
136     # otation_range: 整数。随机旋转的度数范围。
137     # shear_range: 浮点数。剪切强度(以弧度逆时针方向剪切角度)。
138     # zoom_range: 浮点数 或 [lower, upper]。随机缩放范围。如果是浮点数,[lower, upper] = [1-zoom_range, 1+zoom_range]。
139     #  horizontal_flip: 布尔值。随机水平翻转。
140     image_data_generator = ImageDataGenerator(rotation_range=30., shear_range=0.2, zoom_range=0.2, horizontal_flip=True)
141     image_data_generator.mean = np.array(channels_mean, dtype=np.float32).reshape((3, 1, 1))
142     # 从文件夹中读取图像
143     # 第一个参数是 directory:目标文件夹路径,对于每一个类,该文件夹都要包含一个子文件夹。
144     # target_size:整数tuple,默认为(256, 256)。图像将被resize成该尺寸
145     # classes:可选参数,为子文件夹的列表,如['cat','dog'],默认为None。若未提供,则该类别列表将从directory下的子文件夹名称/结构自动推断。每一个
146     # 子文件夹都会被认为是一个新的类。(类别的顺序将按照字母表顺序映射到标签值)。
147     train_data = image_data_generator.flow_from_directory(train_dir, target_size=image_size, classes=classes)
148 
149     image_data_generator = ImageDataGenerator()
150     image_data_generator.mean = np.array(channels_mean, dtype=np.float32).reshape((3, 1, 1))
151     valid_data = image_data_generator.flow_from_directory(valid_dir, target_size=image_size, classes=classes)
152     
153     # 回调函数,用于在训练过程中输出当前进度和设置是否保存过程中的权重,以及早停的判断条件和输出
154     # 该回调函数将在每个epoch后保存模型到filepath
155     # 第一个参数是保存模型的路径;save_best_only:当设置为True时,将只保存在验证集上性能最好的模型
156     # save_weights_only:若设置为True,则只保存模型权重,否则将保存整个模型(包括模型结构,配置信息等)
157     # monitor:需要监视的值,通常为:val_acc 或 val_loss 或 acc 或 loss
158     model_checkpoint_callback = ModelCheckpoint(fine_tuned_weights_path, save_best_only=True, save_weights_only=True, monitor='val_loss')
159     # early stop是训练模型的过程中,避免过拟合,节省训练时间的一种非常场用的方法。
160     # verbose:是否输出更多的调试信息。
161     # patience: 在监控指标没有提升的情况下,epochs 等待轮数。等待大于该值监控指标始终没有提升,则提前停止训练。
162     # monitor: 监控指标,如val_loss
163     early_stopping_callback = EarlyStopping(verbose=1, patience=20, monitor='val_loss')
164 
165     # 获取不同类别的权重
166     class_weight = get_classes_weight(classes, train_dir)
167     batch_size = 10.0
168     epoch_number = 50
169     
170     print("开始训练...")
171     # 利用Python的生成器,逐个生成数据的batch并进行训练。生成器与模型将并行执行以提高效率。
172     # 该函数允许我们在CPU上进行实时的数据提升,同时在GPU上进行模型训练
173     model.fit(
174         train_data,  # 生成器函数
175         # steps_per_epoch=train_sample_number / batch_size,  # 每轮的步数,整数,当生成器返回 stesp_per_epoch次数据时,进入下一轮。
176         epochs=epoch_number,  # 数据的迭代次数
177         validation_data=valid_data,  # 验证集数据
178         validation_steps=valid_sample_number / batch_size,  # 当validation_data为生成器时,本参数指定验证集的生成器返回次数
179         callbacks=[early_stopping_callback, model_checkpoint_callback],
180         class_weight=class_weight  # 规定类别权重的字典,将类别映射为权重,常用于处理样本不均衡问题。
181         )
182     print("模型训练结束,开始保存模型..")
183     model.save(model_path)
184     model.save_weights(weights_path)
185     joblib.dump(classes, classes_path)
186     print("模型保存成功,训练任务全部结束.")
187     pass

注意:权重下载地址:https://github.com/fchollet/deep-learning-models/releases/download/v0.1/resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5

注意这里一定要使用v0.1版本,不能使用v0.2版本,不然会报错

本人跑了 50 个 epoch 就达到了 99.56% 的准确率

5. 预测模型结果代码

 1 import os
 2 import numpy as np
 3 from tensorflow.keras.preprocessing import image
 4 from tensorflow.keras.applications.imagenet_utils import preprocess_input
 5 from train_copy import create_resnet50_model
 6 from tensorflow.keras.models import load_model
 7 
 8 if __name__ == '__main__':
 9     # 需要预测的图片的位置
10     # predict_image_path = "./data/test/22/image_03399.jpg"
11     predict_image_path = "./data/test/74/image_02075.jpg"
12     
13     # 图片预处理
14     image_size = (224, 224)
15     img = image.load_img(predict_image_path, target_size=image_size)  # 此时导入的图片是PIL格式
16     img_array = np.expand_dims(image.img_to_array(img), axis=0)  # 把图片转换成numpy
17     prepared_img = preprocess_input(img_array)  # Preprocesses a tensor or Numpy array encoding a batch of images
18 
19     # 获取花卉数据类别(不同类别的图片放在不同的目录下,获取目录名即可)
20     test_dir = "./data/test"
21     # classes = sorted([o for o in os.listdir(test_dir) if os.path.isdir(os.path.join(test_dir, o))])
22     classes = sorted([o for o in os.listdir(test_dir) if os.path.isdir(os.path.join(test_dir, o))], key=int)
23     
24     # 创建模型并导入训练后的权重
25     # 两种方法:第一种导入模型权重
26     model = create_resnet50_model(classes, image_size)
27     model.load_weights("./saved_model/model-weights.h5")
28     # 另一种方法是导入整个模型
29     # model = load_model("./saved_model/model-resnet50.h5")
30     
31     # 预测
32     out = model.predict(prepared_img)
33 
34     top10 = out[0].argsort()[-10:][::-1]
35 
36     class_indices = dict(zip(classes, range(len(classes))))
37     keys = list(class_indices.keys())
38     values = list(class_indices.values())
39 
40     print("Top10 的分类及概率:")
41     for i, t in enumerate(top10):
42         print("class:", keys[values.index(t)], "probability:", out[0][t])

代码运行结果:

 注意:根据数据集划分代码可知,test 文件下的文件序号名就是最终的类别,现在测试的是 test 74 文件中的内容,而最终的结果也是 74 类别排名第一,准确率高达99%,可知分类效果不错。

有问题的话欢迎大家多多指正~

 

posted @ 2022-12-01 15:51  ttweixiao9999  阅读(1180)  评论(0编辑  收藏  举报