当前位置 博文首页 > 咖啡花园:简单的识别猫狗的模型

    咖啡花园:简单的识别猫狗的模型

    作者:[db:作者] 时间:2021-08-01 21:00

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

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

    cs