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 上训练好的权重:
from keras.applications.vgg16 import VGG16
VGG16_model = VGG16(weights='imagenet')
Using TensorFlow backend.
既然这是一个 Keras 模型,是不是和自己搭建的模型一样可以使用 summary()
方法一览模型的架构呢?答案是可以的。
VGG16_model.summary()
_________________________________________________________________
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,撸起袖子,说干咱就干!
本文使用 Keras 函数式 API 构建,当然也可以使用序列化模型,读者可以自己尝试。
从上文打印出来的模型架构,可以看到,VGG16 用到了卷积层(Conv2D), 最大池化层(MaxPooling2D), 扁平层(Flatten), 全联接层(Dense)。因此,我们从 keras.layers 中导入这些层。
from keras.models import Model
from keras.layers import Input, Conv2D, MaxPooling2D, Flatten, Dense
VGG16 包含了 13 个卷积层,3个全连接层(最后1个是输出层),一共16个有参数的层,这也是 VGG16 中 16 的含义。当然还有 5 个最大池化层和 1 个扁平层,而这些层是没有参数或者权重的,因此,VGG 不把这些层计入总层数。
# 输入层
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)
在使用函数是API,对照前文打印 VGG16 模型架构,依葫芦画瓢,设计出模型各层参数及层间衔接关系后,就可以使用 Model(inputs, outputs)
,指定 inputs
和 outputs
参数创建自己的 VGG16 模型。
my_VGG16_model = Model(inputs=inputs, outputs=outputs)
使用 summary()
方法查看自己的 VGG16 模型,检查是否和前文的模型结构一致。
my_VGG16_model.summary()
_________________________________________________________________
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
_________________________________________________________________
要使用 VGG16 对图片进行分类,首先需要对图片进行预处理,转换成张量,如下助手函数就是完成这一功能,指定图片存储路径,返回一个 VGG16 模型能够处理的 4 维张量(numpy 多维数组)。
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)
模型直接输出是一个长为 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)
分别使用前文创建的 VGG16_model 和自己创建的 my_VGG16_model 对同一张图片进行预测。
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)))
VGG16_model predict label: 245
my_VGG16_model predict label: 788
发现,我们自己创建的 VGG16 和系统加载的 VGG16 预测值不一样。哪里出问题了呢?事实上,我们刚才只是创建了一个和 VGG16 架构一样的模型,但是它还未经过训练,模型的权重还是随机初始化的,而加载的 VGG16 已经加载了 ImageNet 数据集上预训练的权重。知道问题,是不是,只要我们将模型的权重设置成和 VGG16 一样就可以了?不妨一试。
Keras 的模型提供了 get_weights()
和 set_weights()
方法,分别用来获取和设置模型的权重。因此,我顺理成章地想到,获取 VGG16_model
的权重,并用这个权重设置 my_VGG16_model
的权重。
weights = VGG16_model.get_weights()
my_VGG16_model.set_weights(weights)
一切正常,接下来,再重新运行前面的代码,看看两个模型的预测是否回归一致。
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)))
VGG16_model predict label: 245
my_VGG16_model predict label: 245
Awsome! 一切如我们所料。可见两个模型给出了同一个数值 245 。 问题来了,这个 245 到底指代什么呢?正如前面所说,这个值是 ImageNet 给出的 1000 个常见分类的索引。我们想象一个字典,它的值是分类的文本名称,它的键就是我们模型预测出来的标签。那么这个字典在哪里呢?答案在这里:https://gist.github.com/yrevar/942d3a0ac09ec9e5eb3a
上文链接的作者给出了使用 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 到底是个啥?
cat1000[245]
'French bulldog'
英文不好,找谷歌和百度翻译一下,‘French bulldog’ 是“法国斗牛犬”。从文件名 ‘dog.jpeg’ ,我们可以初步判定模型预测对了一半,至少它成功识别出是一条狗子。到底是不是法国斗牛犬。看下图:
此时此刻,是不是应该为我们自己亲手搭建的模型来点掌声呢^^。
既然,我们验证了自己刚才搭建的 VGG 模型已经奏效,并且我们还得到了 1000 个分类的文本字典,不妨将我们的模型封装一下,以便更加容易使用。有了前面的基础,我们只需要简单组合一下,就能得到一个很好用的函数,它传入图片路径 img_path
返回图片分类的文本名称。如果你的英文和笔者一样,建议勤加使用 Goolgle or 百度翻译。
def imgcate(img_path):
label = model_predict(my_VGG16_model, img_path)
return cat1000[label]
读者可以尽情尝试各种图片,体验AI的乐趣。
print(imgcate('dog.jpeg'))
print(imgcate('xx.jpeg'))
French bulldog
maillot, tank suit
微信扫描二维码 获取最新技术原创