来喝无糖汽水

“用CNN网络实现树叶样态的分类”

数据集简介

数据集中树叶的样子有四种,分别为锈病,健康,黑腐,黑星四个种类,而我们要做的工作。就是让机器使用神经网络学习给定的树叶数据集,实现对不同样态的特征提取。然后我们再利用训练好的神经网络,投喂新的树叶图片,让机器自己划分其样态的种类,实现机器识别叶子的智能操作。

图片无法加载时显示的文字

将数据集打包分类后,变成traindata和testdata两个数据集。以及对应的train_label和test_label.

图片无法加载时显示的文字

代码部分

当时这个项目是在笔记本上完成的,环境是tensorflow2.1.0,话不多说直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import os
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from PIL import Image
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# 设置相关路径
train_txt = './second/train_label.txt'
x_train_savepath = './second/model_x_train.npy'
y_train_savepath = './second/model_y_train.npy'

test_txt = './second/test_label.txt'
x_test_savepath = './second/model_x_test.npy'
y_test_savepath = './second/model_y_test.npy'
checkpoint_save_path = "./checkpoint/model_data.ckpt"
# 为数据增广的方法设置参数 图中分别设置了随机旋转,水平/垂直的平移变换,和缩放变换等等
image_gen_train = ImageDataGenerator(
rotation_range=90,
width_shift_range=.15,
height_shift_range=.15,
zoom_range=0.5
)
# 设置函数:从label中找到文件路径,读取文件到返回参数x中,并设置返回参数y_作为相应标签
def generateds(txt):
f = open(txt, 'r') # 以只读形式打开txt文件
contents = f.readlines() # 读取文件中所有行
f.close() # 关闭txt文件
x, y_ = [], [] # 建立空列表
for content in contents: # 逐行取出
print(content)
value = content.split(",") # 以,分开,图片路径为value[0] , 标签为value[1] , 存入列表
img_path = value[0] # 拼出图片路径和文件名
print(img_path)
img = Image.open(img_path) # 读入图片
img = np.array(img.convert('L')) # 由于笔记本内存有限,图片全部转化为单通道灰度图像
img = img / 255. # 数据归一化 (实现预处理)
x.append(img) # 归一化后的数据,贴到列表x
y_.append(value[1]) # 标签贴到列表y_
print('loading : ' + content) # 打印状态提示

x = np.array(x) # 变为np.array格式
y_ = np.array(y_) # 变为np.array格式
y_ = y_.astype(np.int64) # 变为64位整型
return x, y_ # 返回输入特征x,返回标签y_

# 这里是判断是否已存在保存好的图片文件,这种操作可以使得第二次加载模型时不用再单独读取每张图片,而仅仅只是读取之前保存好的npy格式的东东,大大提升读取速度
if os.path.exists(x_train_savepath) and os.path.exists(y_train_savepath) and os.path.exists(
x_test_savepath) and os.path.exists(y_test_savepath):
print('-------------Load Datasets-----------------')
x_train_save = np.load(x_train_savepath)
y_train = np.load(y_train_savepath)
x_test_save = np.load(x_test_savepath)
y_test = np.load(y_test_savepath)
# 将数据集图像最后增加一维
x_train = np.reshape(x_train_save, (len(x_train_save), 256, 256, 1))
x_test = np.reshape(x_test_save, (len(x_test_save), 256, 256, 1))
else:
print('-------------Generate Datasets-----------------')
x_train, y_train = generateds(train_txt)
x_test, y_test = generateds(test_txt)
x_train = np.reshape(x_train, (len(x_train), 256, 256, 1))
x_test = np.reshape(x_test, (len(x_test), 256, 256, 1))
print('-------------Save Datasets-----------------')
# 将数据集图像最后增加一维
x_train_save = np.reshape(x_train, (len(x_train), 256, 256, 1))
x_test_save = np.reshape(x_test, (len(x_test), 256256, 1))
np.save(x_train_savepath, x_train_save)
np.save(y_train_savepath, y_train)
np.save(x_test_savepath, x_test_save)
np.save(y_test_savepath, y_test)
# 网络结构
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(filters=36, kernel_size=(3, 3), padding='same'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('relu'),
tf.keras.layers.Conv2D(filters=12, kernel_size=(3, 3), padding='same'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('relu'),
tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2, padding='same'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(4, activation='softmax')
])
# 设置优化器
model.compile(optimizer=tf.keras.optimizers.Adam(lr=0.01),
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics=['sparse_categorical_accuracy'])
# 学习率自动调整
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', patience=10, mode='auto')

if os.path.exists(checkpoint_save_path + '.index'):
print('------------------ restore model successfully------------------')
model.load_weights(checkpoint_save_path)
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
save_weights_only=True,
save_best_only=True)
history = model.fit(image_gen_train.flow(x_train, y_train, batch_size=24), epochs=200, validation_data=(x_test, y_test), validation_freq=1, callbacks=[reduce_lr, cp_callback])
# 数据可视化
acc = history.history['sparse_categorical_accuracy']
val_acc = history.history['val_sparse_categorical_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']

plt.subplot(1, 2, 1)
plt.plot(acc, label='Training Accuracy')
plt.plot(val_acc, label='Validation Accuracy')
plt.title('Training and Validation Accuracy')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(loss, label='Training Loss')
plt.plot(val_loss, label='Validation Loss')
plt.title('Training and Validation Loss')
plt.legend()
plt.show()

第一次改进:由于数据集样本比较少,需要使用数据增广,但上述代码的数据增广特别简陋,是强行每次用image_gen_train.flow方法操作的,会导致每次喂入神经网络的训练集不同,可能会导致模型不收敛。于是之后在此基础上,单独增加了一个py文件,进行可视化的图像增广,专门设置了输出增广后图像的代码,直接生成增广后的图像数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from tensorflow.keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img
import numpy as np
beishu = 10 # 增广倍数
in_path = "" # 输入类别子文件夹的上一级文件夹
out_path = # 输出类别子文件夹的上一级文件夹
# 具体增广操作
datagen = ImageDataGenerator(
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest')

gener = datagen.flow_from_directory(in_path, batch_size=108, shuffle=False, save_to_dir=out_path, save_prefix='', save_format='jpg')

for i in range(beishou):
gener.next()

这样增广后的图像可以直接可见,下图是增广后的部分图像

图片无法加载时显示的文字

然鹅鉴于博主技术水平有限,这样的数据增广方式仍然存在许多局限性,譬如在增广时,只能将增广后的不同种类图像输出到同一个文件夹,这就意味着,生成label文件时,不能用直接引用当前图片的父文件夹名称作为标签。不过好在这个方法数据增广后的图片文件在图片名字前自带标签,算是一定程度上缓解了这个问题。

新的发现:直接这样用,不需要label文件!不需要之前的generate函数!只需要train和test两个部分的标签文件夹包含路径,具体操作是:先创建一个ImageDataGenerator的实例并定义好伸缩变化那些参数后,直接分别创建ImageDataGenerator.flow_from_directory的两个实例,就可以直接在model.fit中用,真是太吊拉!代码长度如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import os
import numpy as np
import tensorflow as tf
from PIL import Image
from tensorflow.keras.preprocessing.image import ImageDataGenerator
checkpoint_save_path = #检查点的路径
train_path = #类别文件夹父路径
test_path = #同理
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(filters=64, kernel_size=(5, 5), padding='same'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('relu'),
tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2, padding='same'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Conv2D(filters=64, kernel_size=(5, 5), padding='same'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2, padding='same'),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(4, activation='softmax')
])

model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics=['sparse_categorical_accuracy'])
# reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', patience=10, mode='auto')
if os.path.exists(checkpoint_save_path + '.index'):
print('--------------- save model successfully------------------')
model.load_weights(checkpoint_save_path)
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
save_weights_only=True,
save_best_only=True)
train_datagen = ImageDataGenerator(
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)

# this is the augmentation configuration we will use for testing:
# only rescaling
test_datagen = ImageDataGenerator(rescale=1. / 255)

train_generator = train_datagen.flow_from_directory(
train_path,
target_size=(256, 256),
batch_size=24,
class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
test_path,
target_size=(256, 256),
batch_size=24,
class_mode='binary')
model.fit(train_generator, steps_per_epoch=24, epochs=500, validation_data=validation_generator,
validation_steps=1, callbacks=[cp_callback])

下图是train_path的示例

图片无法加载时显示的文字

先扬后抑:然鹅新的方法简单是简单,也存在和最开始方法相同的问题。就是看不见变化后的图像,而且每次模型的训练集和测试集都不一样。这个方法虽然让新手入门网络训练简化了很多步骤,但是有的时候如果只会用这些简化的方法就会有很多限制。之前的第一次的方法虽然代码长度比这个新的发现长了不少,但是里面的generate函数我还可以在其他神经网络结构里面复用,也是很香的。

参考资料:

人工智能实践: Tensorflow笔记

Tensorflow官方API代码示例


本文由 rufus 创作,采用 知识共享署名 4.0 国际许可协议。

本站文章除注明转载/出处外,均为本站原创或翻译,转载请务必署名。