目录
卷积神经网络
1 卷积神经网络简介
1.1 卷积运算
1.2 最大池化运算
2.2 实例——dogs-vs-cats
2.1 数据准备
2.2 数据生成器及数据增强
2.3 预训练的模型
2 Pytorch实现CNN
卷积神经网络(convnet,CNN)是计算机视觉应用中最常见的深度学习模型,对于图像问题具有很好的性能。
和密集连接层相比,卷积层能够学习到特征空间中的局部模式,这得益于卷积运算的特性。
卷积神经网络具有两个重要性质:
CNN所处理的图像,包含两个空间轴(高度和宽度)和一个深度轴。
CNN利用卷积运算,改变图像的深度,从而生成特征图。特征图的每一层具有的含义不再和原始图像相同。卷积运算的通常为3×3或5×5的区域,由于边界效应,图的宽度和高度会缩小。另外,卷积运算还有步幅的概念,即相邻的两个卷积核之间的距离。步幅和边界效应以及是否对原图进行填充 ,将决定输出图像的尺寸。
当模型中只有卷积层时,模型很难学习到图像的空间层级结构(所学习到的窗口会越来越小)。而且所包含的元素会越来越多,导致模型的过拟合。因此引入了下采样的概念。
通过将图像以某种方式缩小大小,则卷积窗口在原始图像中的大小会越来越大,并且特征图的元素个数不会过大。可以通过在卷积层中添加步幅来实现,也可以通过最大池化层。
2.1.3 CNN的基本结构
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 26, 26, 32) 320
max_pooling2d (MaxPooling2D (None, 13, 13, 32) 0
)
conv2d_1 (Conv2D) (None, 11, 11, 64) 18496
max_pooling2d_1 (MaxPooling (None, 5, 5, 64) 0
2D)
conv2d_2 (Conv2D) (None, 3, 3, 64) 36928
flatten (Flatten) (None, 576) 0
dense (Dense) (None, 64) 36928
dense_1 (Dense) (None, 10) 650
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
_________________________________________________________________
可以看到,这个示例中的CNN由两部分组成:
①卷积层和最大池化层的堆叠
②Dense层堆叠得到的分类器
两者之间还有一个flattern层,用于将多维数据展平成一维向量,可供密集连接层处理。
图像数据处理的步骤较为繁琐,主要有以下几步(keras):
首先是数据准备。书作者的方法比较便于查看,将数据根据文件名划分到不同文件夹里,train和test,其内分别包含cat和dog两个文件夹。这样在后续读取的时候很方便去贴标签,也容易检查数据个数是否正确。
original_dataset_dir = 'D:/DeepLearning/kaggle_original_data/train'
# 生成存放新数据的文件夹
base_dir = 'D:/DeepLearning/cats_and_dogs_small'
os.mkdir(base_dir)
# 生成子文件夹(训练集,验证集,测试集)
train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)
# 训练集子文件夹(训练集猫,训练集狗)
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
# Directory with our training dog pictures
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
# 生成验证级 猫,狗文件夹
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
# 生成测试集 猫,狗文件夹
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)
# 利用名字来检索1000只猫
fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
#将图像从路径src复制到dst
shutil.copyfile(src, dst)
# 将之后的500只猫复制到验证集下的猫文件夹
fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)
# 将最后的500只猫复制到训练集下的狗文件夹
fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)
# 重复上述操作,把猫改成狗
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)
fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)
fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)
数据生成器的概念类似于pytorch中的dataloader。可以在数据生成器中加入旋转、平移、缩放等操作,增加训练数据量,也就是数据增强(data augment)。
keras中的imageDataGenarator能够定义生成器,flow_from_directory则从文件进行读取和分类
需要注意的是,batch_size和steps_per_epoch应当乘积为数据总量,或者可以不定义其中一个,模型会自动计算另外一个。
from keras.preprocessing.image import ImageDataGenerator
# 利用imageDataGenerator创建生成器,读取图像的同时进行数据的缩放
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
# 从目标路径读取数据,根据子文件夹来分类
train_dir,
# All images will be resized to 150x150
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')
# 利用生成器进行训练和验证
history = model.fit_generator(
train_generator,
steps_per_epoch=100,#一次抽取20个,总共2000个,因此抽取100次
epochs=30, #迭代30次
validation_data=validation_generator,
validation_steps=50)
绘制得到精度曲线图:
模型出现了典型的过拟合。
下面给出利用数据增强来训练的实例:
#训练模型生成器(包括数据增强)
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,)
# 测试数据生成器(没有数据增强)
test_datagen = ImageDataGenerator(rescale=1./255)
#训练数据生成器实例化
train_generator = train_datagen.flow_from_directory(
train_dir,
target_size=(150, 150),
batch_size=32,
class_mode='binary')
#测试数据生成器实例化
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=32,
class_mode='binary')
#利用生成器拟合
history = model.fit_generator(
train_generator,
# steps_per_epoch=63,
epochs=100,
validation_data=validation_generator,
# validation_steps=32
)
这里虽然使用了数据增强,但输入的steps_per_epoch参数和原来一样。也就是说数据增强的过程在这其中已经加入了。
得到精度图像
模型不再过拟合,数据增强起到了作用。
前面说过,卷积层能够学习到局部特征,并且具有平移不变性,因此其学习的特征比密集连接层要更通用,也就更适合重复使用。
有许多在ImageNet上已经训练好的模型,如VGG16,VGG19,Xception,Inception V3,ResNet50,MobileNet等。书中介绍了利用预训练网络来提取图像特征,再输入到密集连接分类器中学习的方法。主要包括以下几种:
①利用VGG网络predict训练数据,得到输出特征图,再输入到密集分类器。
首先初始化基础模型,用于生成特征图
from keras.applications import VGG16
# 初始化一个VGG16模型
conv_base = VGG16(weights='imagenet',
include_top=False,
input_shape=(150, 150, 3))
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator
base_dir = 'D:/DeepLearning/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
#定义函数,用于生成特征图,并规定数量上限sample_count
def extract_features(directory, sample_count):
#初始化特征图和标签(全为0)
features = np.zeros(shape=(sample_count, 4, 4, 512))
labels = np.zeros(shape=(sample_count))
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:
#predict生成特征批量
features_batch = conv_base.predict(inputs_batch)
#给特征图的第i个批量赋值,label无需处理
features[i * batch_size : (i + 1) * batch_size] = features_batch
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)
剩下的就是将数据展平然后训练和验证,不再赘述。
这种计算方法的代价很低,因为只对一个密集连接层做训练。它的缺点在于无法使用数据增强,因为数据增强需要在每轮拟合输入不同的随机增强的样本,而在这个方法中每轮必定是一样的样本,否则便需要不断运行卷积基,导致巨大的计算代价。也因此导致模型迅速过拟合,效果不够理想。
②在VGG之后添加一个密集连接分类器
这种方法直接在预训练模型的后面增加分类器,这会导致计算代价非常大,但好处在于可以使用数据增强来降低过拟合。
不仅如此,训练模型的时候可以选择将预训练模型冻结,从而实现只对密集连接分类器的权重进行训练。
③微调预训练模型
微调模型将预训练模型部分解冻,来对预训练模型的权重进行更新,使其更适合具体的问题。但需要注意的是,在微调之前必须先将分类器的权重训练好,而不是随机初始化,否则会将解冻的几层严重破坏。
其中,通常解冻的是卷积基的底部几层。因为卷积基中更靠顶部的层编码的是更专业化的特征,而靠近底部的是更普通使用的特征。同时训练过多层会导致参数过多,过拟合的风险增加。
三种方法思路很清晰,以下附上书本的代码和自己添加的注释。
from keras import models
from keras import layers
from keras.preprocessing.image import ImageDataGenerator
# 在卷积基后面添加密集连接分类器
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'))
# 冻结卷积基
conv_base.trainable = False
# 对训练数据进行数据增强
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,
verbose=2)
conv_base.trainable = True
# 循环遍历模型中的层,定义冻结层
set_trainable = False
for layer in conv_base.layers:
# 微调的层很少,直接用if语句判断
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)
书中提到一个有意思的现象:在这两张图中,验证损失一直上升,而验证精度却保持在一个水平,这打破了之前我以为的“验证损失就是验证精度的反面”的观念。实际上验证损失是损失值的平均值,而验证精度和损失的分布有关,两者并不能互相替代。
也正因此,即使平均损失增大,模型也可能在进步。换言之,精度才是模型好坏的标准。损失值只能用于调整权重。
class CNNnet(torch.nn.Module):
def __init__(self):
super(CNNnet,self).__init__()
self.conv1 = torch.nn.Sequential(
torch.nn.Conv2d(in_channels=1,
out_channels=16,
kernel_size=3,
stride=1,
padding=1),
torch.nn.ReLU()
nn.MaxPool2d(kernel_size=2)
)
self.conv2 = torch.nn.Sequential(
torch.nn.Conv2d(in_channels=16,
out_channels=32,
kernel_size=3,
stride=1,
padding=1),
torch.nn.ReLU()
nn.MaxPool2d(kernel_size=2)
)
self.mlp1 = torch.nn.Sequential(
torch.nn.Linear(2*2*64,100)
torch.nn.ReLU()
)
self.mlp2 = torch.nn.Sequential(
torch.nn.Linear(100,10)
torch.nn.ReLU()
)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.mlp1(x)
x = self.mlp2(x)
return x
这里的代码仅作模型构建示意,结构也很简单。pytorch实际上也只是多了一个Relu需要单独写出来,并且反向传播等过程需要一步步写,其它方面并没有显著区别。