天天看点

第五章 深度学习用于计算机视觉(三)

5.3  Using a pre-trained convnet 使用预训练的网络

预训练网络

  • 使用预训练网络是一种非常高效的小图像数据集的深度学习方法;
  • 预训练网络是一个保存好的网络,之前已经在大型数据集上训练好。
  • 使用预训练网络的两种方法: 1、特征提取 2、微调模型

特征提取

        使用之前网络学习到的表示来从新样本中提取出有趣的特征,然后将这些特征输入一个新的分类器,从头开始训练。

卷积基:一系列池化层和卷积层

特征提取就是取出之前训练好的网络的卷积基,在上面运行新数据,然后在输出上面训练一个新的分类器。

卷积基的模型是非常通用的,但是分类器是只针对某个训练集的。

模型的底部往往是具有通用特征的,而上层是各异性比较强的。所以特征提取只使用模型的前几层做特征提取,而不是整个卷积基。

#将VGG16卷积基实例化
from keras.applications import VGG16
conv_base = VGG16(weights='imagenet', #指定模型初始化的权重检查点
                 include_top=False, # 指定模型是否包含密集连接分类器
                 input_shape=(150, 150, 3)) #输入到网络的张量形状,不传参数可以进行任意形状的输入
           

        在colab中运行时,上面的写法是会报错的。报错是:cannot import name 'VGG16' from 'keras.applications'。原因应该是keras版本问题,解决方案是将from keras.applications import VGG16 改为from keras.applications.vgg16 import VGG16。上面框中的代码是在本地jupyter notebook中可以运行的。

        下面我门首先看看刚刚定义的模型conv_base的结构是怎么样的。

conv_base.summary()
           
结果如下:
Model: "vgg16"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 150, 150, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 150, 150, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 150, 150, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 75, 75, 64)        0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 75, 75, 128)       73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 75, 75, 128)       147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 37, 37, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 37, 37, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 18, 18, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 18, 18, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 9, 9, 512)         0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 4, 4, 512)         0         
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________      

下一步工作是在得到的特征(None, 4, 4, 512)上增加一个密集连接分类器,共有两种方法:

1、在数据集上运行卷积基,将输出保存为硬盘中存储的numpy数组,然后用这个数据作为输入,输入到独立的密集连接分类器中,不可以使用数据增强,但是速度很快。

2、在顶部添加Dense层扩展模型,可以使用数据增强,但是计算代价大。

方法1: 

# 使用预训练的卷积基提取特征
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator

base_dir = 'D:/ALL_code\Anaconda/Deep_Learning_Study/chapter5/kaggle_original_data/cats_and_dogs_small'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')

datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20

def extract_features(directory, sample_count):  # directory文件夹目录,sample_count是数据量
    features =np.zeros(shape=(sample_count, 4, 4, 512))  #开辟一个四维的零矩阵(四维张量空间),大小就是足够存放这2000个图像的空间
    labels = np.zeros(shape=(sample_count))
    #generator是一个存放处理过的图像数据的容器
    generator = datagen.flow_from_directory(
        directory,               #图像存放目录
        target_size=(150, 150),  #目标大小
        batch_size=batch_size,   #一次处理批量的多少
        class_mode='binary')     #分类模式:当然是二元
    i = 0
    #使用循环做处理过的图像经过模型预测后的结果
    for inputs_batch, labels_batch in generator:
        features_batch = conv_base.predict(inputs_batch)   #将generator中的数据经过VGG16模型预测    
        features[i * batch_size : (i + 1) * batch_size] = features_batch #将预测后的结果存放在刚刚开辟的空间中
        #只对第0轴进行切片,是因为features是一个四维张量,而这里的数据也是四维的,所以对第0轴进行切片就可以了,其他的会自己存储。
        #就像在一个箱子里放入正方体一样,只考虑一边就可以了,其他的边会自动分配。
        labels[i * batch_size : (i + 1) * batch_size] = labels_batch
        i +=1
        if i * batch_size >= sample_count:
            break    #生成器在循环中是连续不断的,必须设置断点
    return features, labels

train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)

#目前提取的特征形状为(samples, 4, 4, 512)我们要将其输入到密集连接分类器中,所以首先要将其形状展平为(samples, 8192)
train_features = np.reshape(train_features, (2000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features = np.reshape(test_features, (1000, 4 * 4 * 512))
           

得到这个结果,说明特征提取完毕:

第五章 深度学习用于计算机视觉(三)
#定义并训练密集连接分类器
from keras import models
from keras import layers 
from keras import optimizers

model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
             loss='binary_crossentropy',
             metrics=['acc'])

history = model.fit(train_features, train_labels, epochs=30, batch_size=20, validation_data=(validation_features, validation_labels))
           

这个过程因为只添加了两个密集连接层,所以运行速度非常快,在CPU上面每一个epoch也只2秒钟,下面绘制一下损失曲线和验证曲线

# 绘制结果
import matplotlib.pyplot as plt 
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
           

 绘制结果如下:

第五章 深度学习用于计算机视觉(三)
第五章 深度学习用于计算机视觉(三)

验证精度达到了90%,比上一节中从头开始训练的结果好很多。从图中可以看出,虽然Dropout比率相当大,但是模型几乎从一开始就过拟合。这是因为这里没有使用数据增强,数据增强对于降低过拟合非常重要。

方法二:使用数据增强的特征提取

扩展conv_base模型,然后在输入数据上端到端的运行模型,可以使用数据增强。

模型的行为和层类似,所以可以向Sequential模型中添加一个模型(conv_base),就像添加一个层一样。

这边代码在colab上运行,注意在colab上运行需要进行google drive的挂载

#在卷积基上添加一个密集连接分类器
from keras import models
from keras import layers 

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
           
 >>>model.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
vgg16 (Model)                (None, 4, 4, 512)         14714688  
_________________________________________________________________
flatten_1 (Flatten)          (None, 8192)              0         
_________________________________________________________________
dense_3 (Dense)              (None, 256)               2097408   
_________________________________________________________________
dense_4 (Dense)              (None, 1)                 257       
=================================================================
Total params: 16,812,353
Trainable params: 16,812,353
Non-trainable params: 0
_________________________________________________________________      

VGG16的卷积基有 14 714 688个参数,在编译和训练模型之前一定要"冻结"卷积基。冻结一个或多个层是指在训练过程中保持其权重不变。如果不这么做,那么卷积基之前学到的表示将会在训练过程中被修改。因为其上添加的Dense层使随机初始化的,所以非常大的权重更新将会在网络中传播,对之前学到的表示造成很大的破坏。这个model中冻结VGG的参数,冻结14714688这么多

在keras中,冻结网络的方法是将其trainable属性更改为False

print('This is the number of trainable weights'
     'before freezing the conv base:', len(model.trainable_weights))
conv_base.trainable = False
print('This is the number of trainable weights'
     'after freezing the conv base:', len(model.trainable_weights))
           

 运行结果如下:

第五章 深度学习用于计算机视觉(三)

 冻结前为什么是估30个trainable weight,是因为不仅要估计一个W矩阵,还有一个截距项bias,所以每一层都要估计两个trainable weight,共有15层。

>>>model.summary()

很显然可以看出,模型中vgg16的部分被冻结了:

第五章 深度学习用于计算机视觉(三)

 下面对冻结后的模型开始训练:

#利用冻结的卷积基端到端地训练模型
from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers

train_datagen = ImageDataGenerator(
    rescale=1./255,
    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')

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
    train_dir,
    target_size=(150, 150),
    batch_size=20,
    class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
    validation_dir,
    target_size=(150, 150),
    batch_size=20,
    class_mode='binary')

model.compile(loss='binary_crossentropy',
             optimizer=optimizers.RMSprop(lr=2e-5),
             metrics=['acc'])

history = model.fit_generator(
    train_generator,
    steps_per_epoch=100,
    epochs=30,
    validation_data=validation_generator,
    validation_steps=50)
           

 今天在Google Colab上面运行的时候,发现了一个bug,在compile的时候,optimizer竟然报错了,具体原因也没有查到,看样子应该还是版本问题:Error module 'keras.optimizers' has no attribute 'RMSprop'。具体解决方案:在keras前面加上tensorflow,即from tensorflow.keras import optimizers。

 保存模型:

model.save('cats_and_dogs_small_3.h5')
           
# 绘制结果
import matplotlib.pyplot as plt 

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
           

 绘制结果如下:

第五章 深度学习用于计算机视觉(三)
第五章 深度学习用于计算机视觉(三)

微调模型

        冻结VGG16的卷积基是为了能够在上面训练一个随机初始化的分类器。只有上面的分类器训练好了才能微调卷积基的顶部几层。如果分类器没有训练好,那么训练期间通过网络传播的误差信号会特别大,微调的几层之前学到的表示都会受到破坏。

微调网络步骤如下:

(1)在已经训练好的基网络(base network)上添加自定义网络

(2)冻结基网络

(3)训练所添加的部分

(4)解冻基网络的一些层

(5)联合训练解冻的这些层和添加的部分。

前面三个步骤在特征提取时做完了

为什么不微调最后三个卷积层?为什么不微调整个卷积基?需要考虑的问题:

卷积基中更多靠底部的层编码的更加通用的可复用特征,而更可靠顶部的层编码的是更专业化的特征。微调这些更专业化的特征更加有用。微调更靠底部的层,得到的回报会更少。

训练集上的参数越多,过拟合的风险越大。

#冻结直到某一层的所有层
conv_base.trainable = True

set_trainable = False
for layer in conv_base.layers:
    if layer.name == 'block5_conv1':
        set_trainable = True
    if set_trainable :
        layer.trainable = True
    else:
        layer.trainable = False
           
#微调模型
model.compile(loss='binary_crossentropy',
             optimizer=optimizers.RMSprop(lr=1e-5),
             metrics=['acc'])

history = model.fit_generator(
    train_generator,
    steps_per_epoch=100,
    epochs=100,
    validation_data=validation_generator,
    validation_steps=50)
           
model.save('cats_and_dogs_small_4.h5')
#保存模型
           
# 绘制结果
import matplotlib.pyplot as plt 

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
           

绘制结果如下:

第五章 深度学习用于计算机视觉(三)
第五章 深度学习用于计算机视觉(三)

这些曲线看起来很紧凑。为了使它们更易读,我们可以用这些量的指数移动平均值替换每一个损失和精度。下面是一个简单的实用程序函数:

#使曲线变得平滑
def smooth_curve(points, factor=0.8):
    smoothed_points = []
    for point in points:
        if smoothed_points:
            previous = smoothed_points[-1]
            smoothed_points.append(previous * factor + point * (1 - factor))
        else:
            smoothed_points.append(point)
    return smoothed_points
plt.plot(epochs, smooth_curve(acc), 'bo', label='Smoothed training acc')
plt.plot(epochs, smooth_curve(val_acc), 'b', label='Smoothed validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs,
        smooth_curve(loss), 'bo', label='Smoothed training loss')
plt.plot(epochs,
        smooth_curve(val_loss), 'b', label='Smoothed validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
           

 结果如下:

第五章 深度学习用于计算机视觉(三)
第五章 深度学习用于计算机视觉(三)

        这些曲线看起来更清晰、更稳定。我们看到一个不错的1%的绝对改善。 请注意,损失曲线没有显示出任何真正的改善(事实上,它正在恶化)。你可能想知道,如果损失不减少,精确度如何提高?答案很简单:我们显示的是逐点损失值的平均值,但实际影响准确性的是损失值的分布,而不是它们的平均值,因为准确性是模型预测的类概率的二元阈值化的结果。即使这没有反映在平均损失中,模型可能仍然在改进。 我们现在可以根据测试数据最终评估此模型:

#在测试集上评估这个模型
test_generator = test_datagen.flow_from_directory(
test_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')

test_loss, test_acc = model.evaluate_generator(test_generator, steps=50)
print('test acc: ', test_acc)
#CPU运行很快,由于colab挂载云端硬碟,所有的文件都在云端,访问也要从云访问,所以运行速度很慢
           

 运行结果如下:

第五章 深度学习用于计算机视觉(三)

 上面结果是Colab上面跑出来的,下面是我用jupyter跑出来的:

第五章 深度学习用于计算机视觉(三)

其实有些不太懂,代码完全一致 ,使用数据集完全一致,为什么跑出来的结果和绘制的曲线会有差别。jupyter notebook使用起来比colab顺手,但是跑的时候感觉前面的训练会对下面的数据有影响,会发生这些诡异的事情。

 下一篇整理5.4,卷积神经网络可视化。

继续阅读