1. VGG 模型架构
VGG 由牛津大学视觉几何组(Visual Geometry Group)开发。包含两个版本:VGG16 和 VGG19,分别有16个层级和19个层级。本文只介绍 VGG16 。根据 arxiv.org 上公布的论文,VGG 的卷积核大小为 (3, 3),最大池化层核大小 (2, 2),隐藏层激活函数为 ReLu
, 输出层激活函数为 softmax
。如果我们能知道模型各层的输入输出 shape 及层叠顺序,就能使用 Keras 自己搭建一个 VGG 。幸运的是,我们不需要从晦涩难懂的论文中提炼出模型的这些参数细节,Keras 可以直接给到我们这个模型全部细节。
如下使用 Keras 直接创建一个 VGG16 模型,并加载在 ImageNet 上训练好的权重:
代码语言:javascript复制from keras.applications.vgg16 import VGG16
VGG16_model = VGG16(weights='imagenet')
代码语言:javascript复制Using TensorFlow backend.
既然这是一个 Keras 模型,是不是和自己搭建的模型一样可以使用 summary()
方法一览模型的架构呢?答案是可以的。
VGG16_model.summary()
代码语言:javascript复制_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 224, 224, 3) 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, 224, 224, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 224, 224, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, 112, 112, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, 112, 112, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, 112, 112, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 56, 56, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 56, 56, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 28, 28, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 14, 14, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 7, 7, 512) 0
_________________________________________________________________
flatten (Flatten) (None, 25088) 0
_________________________________________________________________
fc1 (Dense) (None, 4096) 102764544
_________________________________________________________________
fc2 (Dense) (None, 4096) 16781312
_________________________________________________________________
predictions (Dense) (None, 1000) 4097000
=================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0
_________________________________________________________________
有了以上信息,我们已经具备手动构建一个 VGG16 模型所有信息。SO,撸起袖子,说干咱就干!
2. 从零构建 VGG16
本文使用 Keras 函数式 API 构建,当然也可以使用序列化模型,读者可以自己尝试。
2.1 导入 Keras 模型和层
从上文打印出来的模型架构,可以看到,VGG16 用到了卷积层(Conv2D), 最大池化层(MaxPooling2D), 扁平层(Flatten), 全联接层(Dense)。因此,我们从 keras.layers 中导入这些层。
代码语言:javascript复制from keras.models import Model
from keras.layers import Input, Conv2D, MaxPooling2D, Flatten, Dense
2.2 设计模型层
VGG16 包含了 13 个卷积层,3个全连接层(最后1个是输出层),一共16个有参数的层,这也是 VGG16 中 16 的含义。当然还有 5 个最大池化层和 1 个扁平层,而这些层是没有参数或者权重的,因此,VGG 不把这些层计入总层数。
代码语言:javascript复制# 输入层
inputs = Input(shape=(224, 224, 3))
# 卷积层和最大池化层
conv1 = Conv2D(64, (3,3), padding='same', activation='relu')(inputs)
conv2 = Conv2D(64, (3,3), padding='same', activation='relu')(conv1)
pool1 = MaxPooling2D(pool_size=2)(conv2)
conv3 = Conv2D(128, (3,3), padding='same', activation='relu')(pool1)
conv4 = Conv2D(128, (3,3), padding='same', activation='relu')(conv3)
pool2 = MaxPooling2D(pool_size=2)(conv4)
conv5 = Conv2D(256, (3,3), padding='same', activation='relu')(pool2)
conv6 = Conv2D(256, (3,3), padding='same', activation='relu')(conv5)
conv7 = Conv2D(256, (3,3), padding='same', activation='relu')(conv6)
pool3 = MaxPooling2D(pool_size=2)(conv7)
conv8 = Conv2D(512, (3,3), padding='same', activation='relu')(pool3)
conv9 = Conv2D(512, (3,3), padding='same', activation='relu')(conv8)
conv10 = Conv2D(512, (3,3), padding='same', activation='relu')(conv9)
pool4 = MaxPooling2D(pool_size=2)(conv10)
conv11 = Conv2D(512, (3,3), padding='same', activation='relu')(pool4)
conv12 = Conv2D(512, (3,3), padding='same', activation='relu')(conv11)
conv13 = Conv2D(512, (3,3), padding='same', activation='relu')(conv12)
pool5 = MaxPooling2D(pool_size=2)(conv13)
# 扁平层
flat = Flatten()(pool5)
# 全联接层
fc1 = Dense(4096, activation='relu')(flat)
fc2 = Dense(4096, activation='relu')(fc1)
# 输出层
outputs = Dense(1000, activation='softmax')(fc2)
2.3 创建并预览模型
在使用函数是API,对照前文打印 VGG16 模型架构,依葫芦画瓢,设计出模型各层参数及层间衔接关系后,就可以使用 Model(inputs, outputs)
,指定 inputs
和 outputs
参数创建自己的 VGG16 模型。
my_VGG16_model = Model(inputs=inputs, outputs=outputs)
使用 summary()
方法查看自己的 VGG16 模型,检查是否和前文的模型结构一致。
my_VGG16_model.summary()
代码语言:javascript复制_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) (None, 224, 224, 3) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 224, 224, 64) 1792
_________________________________________________________________
conv2d_2 (Conv2D) (None, 224, 224, 64) 36928
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 112, 112, 64) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 112, 112, 128) 73856
_________________________________________________________________
conv2d_4 (Conv2D) (None, 112, 112, 128) 147584
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 56, 56, 128) 0
_________________________________________________________________
conv2d_5 (Conv2D) (None, 56, 56, 256) 295168
_________________________________________________________________
conv2d_6 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________
conv2d_7 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 28, 28, 256) 0
_________________________________________________________________
conv2d_8 (Conv2D) (None, 28, 28, 512) 1180160
_________________________________________________________________
conv2d_9 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________
conv2d_10 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 14, 14, 512) 0
_________________________________________________________________
conv2d_11 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
conv2d_12 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
conv2d_13 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 7, 7, 512) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 25088) 0
_________________________________________________________________
dense_1 (Dense) (None, 4096) 102764544
_________________________________________________________________
dense_2 (Dense) (None, 4096) 16781312
_________________________________________________________________
dense_3 (Dense) (None, 1000) 4097000
=================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0
_________________________________________________________________
3. 使用模型对图片进行分类
3.1 图片预处理
要使用 VGG16 对图片进行分类,首先需要对图片进行预处理,转换成张量,如下助手函数就是完成这一功能,指定图片存储路径,返回一个 VGG16 模型能够处理的 4 维张量(numpy 多维数组)。
代码语言:javascript复制import numpy as np
from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input
def path_to_tensor(img_path):
# 用 PIL 加载 RGB 图像为 PIL.Image.Image 类型
img = image.load_img(img_path, target_size=(224, 224))
# 将 PIL.Image.Image 类型转化为格式为 (224, 224, 3) 的3维张量
x = image.img_to_array(img)
# 将3维张量转化为格式为 (1, 224, 224, 3) 的4维张量
tensor = np.expand_dims(x, axis=0)
# VGG 张量预处理
return preprocess_input(tensor)
3.2 使用模型预测分类
模型直接输出是一个长为 1000 numpy
数组,对应 1000 个分类的概率。我们这里不需要知道每一个分类的概率值,而只需要知道概率最大的分类,作为模型预测的分类。因此,model_predict()
函数在返回前,使用 numpy.argmax()
获取概率最大的分类索引(也是图片的分类标签)。
def model_predict(model, img_path):
tensor = path_to_tensor(img_path)
predict_label = model.predict(tensor)
return np.argmax(predict_label)
3.3 对比测试
分别使用前文创建的 VGG16_model 和自己创建的 my_VGG16_model 对同一张图片进行预测。
代码语言:javascript复制img_path = 'dog.jpeg'
print('VGG16_model predict label: {}'.format(model_predict(VGG16_model, img_path)))
print('my_VGG16_model predict label: {}'.format(model_predict(my_VGG16_model, img_path)))
代码语言:javascript复制VGG16_model predict label: 245
my_VGG16_model predict label: 788
发现,我们自己创建的 VGG16 和系统加载的 VGG16 预测值不一样。哪里出问题了呢?事实上,我们刚才只是创建了一个和 VGG16 架构一样的模型,但是它还未经过训练,模型的权重还是随机初始化的,而加载的 VGG16 已经加载了 ImageNet 数据集上预训练的权重。知道问题,是不是,只要我们将模型的权重设置成和 VGG16 一样就可以了?不妨一试。
3.4 设置权重
Keras 的模型提供了 get_weights()
和 set_weights()
方法,分别用来获取和设置模型的权重。因此,我顺理成章地想到,获取 VGG16_model
的权重,并用这个权重设置 my_VGG16_model
的权重。
weights = VGG16_model.get_weights()
my_VGG16_model.set_weights(weights)
一切正常,接下来,再重新运行前面的代码,看看两个模型的预测是否回归一致。
代码语言:javascript复制print('VGG16_model predict label: {}'.format(model_predict(VGG16_model, img_path)))
print('my_VGG16_model predict label: {}'.format(model_predict(my_VGG16_model, img_path)))
代码语言:javascript复制VGG16_model predict label: 245
my_VGG16_model predict label: 245
Awsome! 一切如我们所料。可见两个模型给出了同一个数值 245 。 问题来了,这个 245 到底指代什么呢?正如前面所说,这个值是 ImageNet 给出的 1000 个常见分类的索引。我们想象一个字典,它的值是分类的文本名称,它的键就是我们模型预测出来的标签。那么这个字典在哪里呢?答案在这里:https://gist.github.com/yrevar/942d3a0ac09ec9e5eb3a
3.5 真实分类名称
上文链接的作者给出了使用 pickle
加载得到字典的 .pkl
文件地址 。将这个文件下载到本地,然后使用 pickle.load()
加载文件即可得到我们需要的字典。
注意:浏览器直接另存下载后的文件会自动增加一个 .txt 的后缀,笔者这里已经手动去掉了这个后缀。否则,完整文件名为 imagenet1000_clsid_to_human.pkl.txt
。
import pickle
with open('imagenet1000_clsid_to_human.pkl', 'rb') as f:
cat1000 = pickle.load(f)
接下来,终于可以瞅瞅刚才模型预测的 245 到底是个啥?
代码语言:javascript复制cat1000[245]
代码语言:javascript复制'French bulldog'
英文不好,找谷歌和百度翻译一下,’French bulldog’ 是“法国斗牛犬”。从文件名 ‘dog.jpeg’ ,我们可以初步判定模型预测对了一半,至少它成功识别出是一条狗子。到底是不是法国斗牛犬。看下图:
此时此刻,是不是应该为我们自己亲手搭建的模型来点掌声呢^^。
3.6 更友好的图片分类函数
既然,我们验证了自己刚才搭建的 VGG 模型已经奏效,并且我们还得到了 1000 个分类的文本字典,不妨将我们的模型封装一下,以便更加容易使用。有了前面的基础,我们只需要简单组合一下,就能得到一个很好用的函数,它传入图片路径 img_path
返回图片分类的文本名称。如果你的英文和笔者一样,建议勤加使用 Goolgle or 百度翻译。
def imgcate(img_path):
label = model_predict(my_VGG16_model, img_path)
return cat1000[label]
读者可以尽情尝试各种图片,体验AI的乐趣。
代码语言:javascript复制print(imgcate('dog.jpeg'))
print(imgcate('xx.jpeg'))
代码语言:javascript复制French bulldog
maillot, tank suit