简单的识别猫狗的模型

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/hekaiyou/article/details/89256579

从Google下载猫狗训练集与验证集的zip压缩包,提取到项目目录下。这个文件夹里面包含训练(train)和验证(validation)数据集的子目录,而且每个子目录都包含猫和狗的子目录。

image.png

可以直接在它这个目录下创建一个python文件,就叫猫和狗(cats_and_dogs.py),然后配置好训练集、验证集的目录。

base_dir = '../cats_and_dogs_filtered'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')

# 用于训练的猫图目录。
train_cats_dir = os.path.join(train_dir, 'cats')
# 用于训练的狗图目录。
train_dogs_dir = os.path.join(train_dir, 'dogs')

# 用于验证的猫图目录。
validation_cats_dir = os.path.join(validation_dir, 'cats')
# 用于验证的狗图目录。
validation_dogs_dir = os.path.join(validation_dir, 'dogs')

现在可以看看猫(cats)和狗(dogs)在训练(train)和验证(validation)目录中的文件命名是怎么约定的。

train_cat_fnames = os.listdir(train_cats_dir)
print('猫的训练、验证目录中的文件命名约定:\n%s' % (train_cat_fnames[:10]))

train_dog_fnames = os.listdir(train_dogs_dir)
train_dog_fnames.sort()
print('狗的训练、验证目录中的文件命名约定:\n%s' % (train_dog_fnames[:10]))

'''
猫的训练、验证目录中的文件命名约定:
['cat.952.jpg', 'cat.946.jpg', 'cat.6.jpg', 'cat.749.jpg', 'cat.991.jpg', 'cat.985.jpg', 'cat.775.jpg', 'cat.761.jpg', 'cat.588.jpg', 'cat.239.jpg']
狗的训练、验证目录中的文件命名约定:
['dog.0.jpg', 'dog.1.jpg', 'dog.10.jpg', 'dog.100.jpg', 'dog.101.jpg', 'dog.102.jpg', 'dog.103.jpg', 'dog.104.jpg', 'dog.105.jpg', 'dog.106.jpg']
'''

还可以找出训练和验证目录中猫和狗图像的总数。

print('总共用于训练的猫图像:', len(os.listdir(train_cats_dir)))
print('总共用于训练的狗图像:', len(os.listdir(train_dogs_dir)))
print('总共用于验证的猫图像:', len(os.listdir(validation_cats_dir)))
print('总共用于验证的狗图像:', len(os.listdir(validation_dogs_dir)))

'''
总共用于训练的猫图像: 1000
总共用于训练的狗图像: 1000
总共用于验证的猫图像: 500
总共用于验证的狗图像: 500
'''

现在知道了,有1,000个猫狗的训练图片,还有500个猫狗的验证图片。现在可以再看几张图片,以便提前地了解一下猫狗数据集里都是些什么图片。

import matplotlib.pyplot as plt
import matplotlib.image as mpimg

# 输出图表的参数,将以4x4的配置输出猫狗数据集的部分图片。
nrows = 4
ncols = 4

# 迭代图像的当前索引。
pic_index = 0

# 设置matplotlib(Python的2D绘图库)图,并将其设置为适合4x4图片大小。
fig = plt.gcf()
fig.set_size_inches(ncols * 4, nrows * 4)

pic_index += 8
next_cat_pix = [os.path.join(train_cats_dir, fname)
                for fname in train_cat_fnames[pic_index-8:pic_index]]
next_dog_pix = [os.path.join(train_dogs_dir, fname)
                for fname in train_dog_fnames[pic_index-8:pic_index]]

for i, img_path in enumerate(next_cat_pix+next_dog_pix):
  # 设置子图,子图的索引从1开始。
  sp = plt.subplot(nrows, ncols, i + 1)
  # 不显示轴(或者说网格线)。
  sp.axis('Off')
  img = mpimg.imread(img_path)
  plt.imshow(img)

plt.show()

输出的实际效果如下图所示,每次重新运行都会查看到新的一批图片。

image.png

现在准备从头开始构建小型Convnet(开源的卷积神经网络代码),并期望能达到72%的准确率。我们要处理的图片是150x150的彩色图片,编写代码,将堆叠3个卷积层(convolution)、修正线性激活函数(relu)和最大池化层(maxpooling)模块。

我们的卷积层(convolution)过滤器大小为3x3,我们的最大池化层(maxpooling)过滤器为2x2。

  • 第一个卷积层(convolution):设置16个3x3大小的过滤器,即识别16种轮廓及边缘,最后通过修正线性激活函数(relu)形成一个3x3x16的立方体。
  • 第二个卷积层(convolution):设置32个3x3大小的过滤器,即识别32种轮廓及边缘,最后通过修正线性激活函数(relu)形成一个3x3x32的立方体。
  • 第三个卷积层(convolution):设置64个3x3大小的过滤器,即识别64种轮廓及边缘,最后通过修正线性激活函数(relu)形成一个3x3x64的立方体。

每一个卷积层(convolution)成形之后,都会追加一个最大池化层(maxpooling),通过池化操作来降低卷积层输出的特征向量,同时改善结果,使其不易出现过拟合。

注意:这是一种广泛使用的配置,并且已知可以很好地用于图像分类。

此外,由于我们只有相对较少的训练样本(1,000张),仅使用三个卷积模块就可以保持模型较小,从而降低过度拟合(overfitting)的风险。过拟合(overfitting)的意思是:对见过的数据,分类效果极好;而对没见过的数据,表现很糟糕。

from tensorflow import keras

# 输入特征图是150x150x3,其中150x150用于图像像素,3用于三个颜色通道(R、G和B)。
img_input = keras.layers.Input(shape=(150, 150, 3))

# 第一个卷积层提取3x3x16的特征,使用修正线性激活函数(`relu`),然后是具有2x2大小的最大池化层。
x = keras.layers.Conv2D(16, 3, activation='relu')(img_input)
x = keras.layers.MaxPooling2D(2)(x)

# 第二个卷积层提取3x3x32的特征,使用修正线性激活函数(`relu`),然后是具有2x2大小的最大池化层。
x = keras.layers.Conv2D(32, 3, activation='relu')(x)
x = keras.layers.MaxPooling2D(2)(x)

# 第三个卷积层提取3x3x64的特征,使用修正线性激活函数(`relu`),然后是具有2x2大小的最大池化层。
x = keras.layers.Conv2D(64, 3, activation='relu')(x)
x = keras.layers.MaxPooling2D(2)(x)

然后是最重要一步,创建两个全连接层(dense),因为我们正面临着两个分类问题,即二元分类问题,我们将以sigmoid函数(一个常用的神经网络激励函数)激活我们的网络,以便我们的网络输出将是0~1之间的单个标量,当前编码的概率图像是一维(而不是零维)。

# 将特征图展平为一维数据(`1-dim`)张量,以便添加全连接层。
x = keras.layers.Flatten()(x)

# 使用修正线性激活函数(`relu`)和512个隐藏单元(或神经元)创建全连接层。
x = keras.layers.Dense(512, activation='relu')(x)

# 使用单个节点(或神经元)和`sigmoid`激活函数创建输出层。
output = keras.layers.Dense(1, activation='sigmoid')(x)

# 创建模型:
# input = 输入特征映射
# output = 输入特征映射 + 堆叠卷积层/最大池化层数 + 全连接层
# 全连接层 + `sigmoid`输出层
model = keras.Model(img_input, output)

最后,可以总结一下整个模型的框架,欣赏一下我们创建的卷积神经网络模型。

model.summary()

'''
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 150, 150, 3)       0         
_________________________________________________________________
conv2d (Conv2D)              (None, 148, 148, 16)      448       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 74, 74, 16)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 72, 72, 32)        4640      
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 36, 36, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 34, 34, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 17, 17, 64)        0         
_________________________________________________________________
flatten (Flatten)            (None, 18496)             0         
_________________________________________________________________
dense (Dense)                (None, 512)               9470464   
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 513       
=================================================================
Total params: 9,494,561
Trainable params: 9,494,561
Non-trainable params: 0
_________________________________________________________________
'''

上面输出中的“Output Shape”列显示特征图的大小如何在每个连续图层中演变。很明显,由于没有设置填充(padding),卷积层(conv)会将特征映射的大小减少一点,并且每个最大池化层(max_pooling)将特征映射减半。

接下来,我们要配置模型训练的规范,我们将使用交叉熵损失函数(binary_crossentropy)训练我们的模型,因为这是一个二进制分类问题,我们的最终激活函数是一个sigmoid函数。

我们将使用学习率为0.001的rmsprop(一种常用的深度学习优化算法)优化器。同时,在训练期间,我们想监控分类准确性。

注意:在当前情况下,使用rmsprop优化算法优于随机梯度下降(SGD)算法,因为rmsprop会自动调整学习速率。

model.compile(loss='binary_crossentropy',
              optimizer=keras.optimizers.RMSprop(lr=0.001),
              metrics=['acc'])

现在,让我们设置数据生成器,它将读取源文件夹中的图片,将它们转换为float32的张量,并将它们附带上标签提供给我们的网络。这样的话,我们将有一个用于训练图像的生成器和一个用于验证图像的生成器,生成器将生产20个尺寸为150x150的图像及其标签(二进制)。

通常进入神经网络的数据应该以某种方式进行标准化,以使其更适合网络处理。(将原始像素直接输入网络的情况并不常见)在这里,我们通过将像素值归一化为0~1范围来预处理图像(最初所有值都在0~255范围内)。

在Keras(用Python编写的高级神经网络API)中,可以使用rescale参数通过keras.preprocessing.image.ImageDataGenerator类完成此操作。此类允许我们通过.flow(data, labels).flow_from_directory(directory)实例化增强图像批次(及其标签)的生成器。然后,这些生成器可以与接受数据生成器作为输入的Keras模型方法一起使用:fit_generatorevaluate_generatorpredict_generator

# 对所有图像按照指定的尺度因子,进行放大或缩小,设置值在0~1之间,通常为1./255。
train_datagen = keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
test_datagen = keras.preprocessing.image.ImageDataGenerator(rescale=1./255)

# 使用train_datagen生成器分批轮流训练20张图像。
train_generator = train_datagen.flow_from_directory(
        # 这是训练图像的源目录。
        train_dir,
       # 所有图像将调整为150x150大小。
        target_size=(150, 150),
        batch_size=20,
        # 由于我们使用binary_crossentropy损失算法,我们需要二进制标签。
        class_mode='binary')

# 使用test_datagen生成器批量生成20个流程验证图像。
validation_generator = test_datagen.flow_from_directory(
        validation_dir,
        target_size=(150, 150),
        batch_size=20,
        class_mode='binary')

现在让我们训练所有的图像,一共2000个,分15个时期,并验证所有验证图像,一个1000个。

# 开始训练。
history = model.fit_generator(
      train_generator,
      # 2000张图片 = 批量大小 * 步进。
      steps_per_epoch=100,
      epochs=15,
      validation_data=validation_generator,
      # 1000张图片 = 批量大小 * 步进。
      validation_steps=50,
      verbose=2)

我们可以做一个有意思的事情,就是可视化在训练时如何模型是怎么变化的,让我们从训练集中选择一个随机的猫或狗图像,然后生成一个图形,其中每一行是图层的输出,并且该行中的每个图像都是该输出特征图中的特定滤镜。

import numpy as np
import random

# 定义一个新的模型,它将图像作为输入,并在第一个模型之后输出前一个模型中所有图层的中间表示。
successive_outputs = [layer.output for layer in model.layers[1:]]
visualization_model = keras.Model(img_input, successive_outputs)

# 从训练集中准备一只猫或狗的随机输入图像。
cat_img_files = [os.path.join(train_cats_dir, f) for f in train_cat_fnames]
dog_img_files = [os.path.join(train_dogs_dir, f) for f in train_dog_fnames]
img_path = random.choice(cat_img_files + dog_img_files)

# 这是PIL图像。
img = keras.preprocessing.image.load_img(img_path, target_size=(150, 150))
# Numpy数组形状(150,150,3)。
x = keras.preprocessing.image.img_to_array(img)
# Numpy数组形状(1,150,150,3)。
x = x.reshape((1,) + x.shape)

# 重新缩放1/255。
x /= 255

# 通过神经网络运行我们的图像,从而获得该图像的所有中间表示。
successive_feature_maps = visualization_model.predict(x)

# 这些是图层的名称,因此可以将它们作为我们图表的一部分。
layer_names = [layer.name for layer in model.layers]

# 现在展示一下。
for layer_name, feature_map in zip(layer_names, successive_feature_maps):
  if len(feature_map.shape) == 4:
    # 只需对conv/maxpool(卷积/最大化池)图层执行此操作,而不是全连接层。
    # 特征图中的要素数量。
    n_features = feature_map.shape[-1]
    # 特征图具有形状(1,size,size,n_features)。
    size = feature_map.shape[1]
    # 我们将在此矩阵中平铺图像。
    display_grid = np.zeros((size, size * n_features))
    for i in range(n_features):
      # 对特征该进行后处理,使其在视觉上更加美观。
      x = feature_map[0, :, :, I]
      x -= x.mean()
      x /= x.std()
      x *= 64
      x += 128
      x = np.clip(x, 0, 255).astype('uint8')
      # 我们将每个过滤器平铺到这个大的水平网格中。
      display_grid[:, i * size : (i + 1) * size] = x
    # 显示网格。
    scale = 20. / n_features
    plt.figure(figsize=(scale * n_features, scale))
    plt.title(layer_name)
    plt.grid(False)
    plt.imshow(display_grid, aspect='auto', cmap='viridis')
    # 显示图像。
    plt.show()

当上面的代码运行完之前,会依次显示下面的几张图片。

Figure_1.png

Figure_2.png

Figure_3.png

Figure_4.png

Figure_5.png

Figure_6.png

正如上面所显示的,我们看到了图像从原始像素变为越来越抽象和紧凑的展示,下游的展示开始突出显示神经网络关注的内容,并且它们显示越来越少的特征被“激活”;大多数都设置为零,这被称为“稀疏性”,稀疏性是深度学习的关键特征。

这些展示关于图像的原始像素的信息越来越少,但是关于图像类别的信息越来越精细,我们可以将convnet(卷积神经或一般的深层网络)视为信息蒸馏管道。

接下来我们可以评估一下模型的准确性和损失,绘制训练期间收集的训练/验证准确性和损失。

# 检索每个训练时期的训练和验证数据集的准确度结果列表。
acc = history.history['acc']
val_acc = history.history['val_acc']

# 检索每个训练时期的训练和验证数据集的结果列表。
loss = history.history['loss']
val_loss = history.history['val_loss']

# 获取时期数。
epochs = range(len(acc))

# 绘制每个时期的训练和验证准确性。
plt.plot(epochs, acc)
plt.plot(epochs, val_acc)
plt.title('Training and validation accuracy')

plt.figure()

# 绘制每个时期的训练和验证损失。
plt.plot(epochs, loss)
plt.plot(epochs, val_loss)
plt.title('Training and validation loss')

plt.show()

上面的代码运行结束前,会显示下面图片。

Figure_7.png

Figure_8.png

正如上面图片所展示的,我们的模型明显过度拟合了,我们的训练准确度(蓝线)接近100%,而我们的验证准确度(橙线)停滞在70%上下。我们的验证损失在仅仅5个时期后达到最小值,这是因为我们的训练样本数量相对较少(2000个)。

因此,过度拟合应成为我们的首要关注点,当太少特征样本的模型,不推广到新数据的模式时,即当模型开始使用不相关的特征进行预测时,就会发生过度拟合。例如,如果我们作为人类,只能看到三个伐木工人的图像,以及三个水手的图像,其中只有一个戴帽子的人是伐木工人,你可能会开始认为戴着帽子是一名伐木工人而不是水手的标志。

然后我们就会做出一个非常糟糕的伐木工人/水手分类器,过度拟合是机器学习中的核心问题:假设我们将模型的参数拟合到给定的数据集,我们如何确保模型适用于以前从未见过的数据?我们如何避免学习特定于训练数据的内容?

最后清理一下数据,运行以下代码以终止内核并释放内存资源。

os.kill(os.getpid(), signal.SIGKILL)

通过以上内容,就建立了一个简单的识别猫狗的模型。

猜你喜欢

转载自blog.csdn.net/hekaiyou/article/details/89256579