数据集简介

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

20210327190128

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

image-20210327190745857

代码部分

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

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), 256 256, 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文件,进行可视化的图像增广,专门设置了输出增广后图像的代码,直接生成增广后的图像数据。

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()

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

zengguang

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

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

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的示例

lujin

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

参考资料:

人工智能实践: Tensorflow笔记

Tensorflow官方API代码示例