文章目录

  • VGG19 实现数字图像分类
  • VGGNet介绍
  • 1.基本概念
  • 2.网络结构
  • (1)常见的神经网络结构
  • (2)卷积层
  • (3)最大池化层
  • 3.VGG19网络结构
  • (1)数据加载模块
  • (2)Flatten 层
  • (3)卷积层 与 池化层
  • (4)网络结构模块
  • (5)网络推断模块
  • 4.图像分类实例
  • (1) 将图像与模型加载


VGG19 实现数字图像分类

使用VGG19网络模型实现图像分类:

  • 输入:数字图像(一张猫,狗,等图像)
  • 输出:图像中猫,狗对应ImageNet 数据集类别编号

VGGNet介绍

1.基本概念

VGGNet是牛津大学计算机视觉组和Google DeepMind公司一起研发的深度卷积神经网络,并取得了2014年Imagenet比赛定位项目第一名和分类项目第二名。该网络主要是泛化性能很好,容易迁移到其他的图像识别项目上,可以下载VGGNet训练好的参数进行很好的初始化权重操作,很多卷积神经网络都是以该网络为基础,比如FCN,UNet,SegNet等。vgg版本很多,常用的是VGG16,VGG19网络。

2.网络结构

(1)常见的神经网络结构

常见的卷积神经网络结构如图所示。卷积层后面会使用 ReLU 等激活函数,N 个卷

积层后通常会使用一个最大池化层(也有使用平均池化的);卷积和池化组合出现 M 次之后,提取出来的卷积特征会经过 K 个全连接层映射到若干个输出特征上,最后再经过一个全连接层或 Softmax 层来决定最终的输出。在第2.1节实验中,已经介绍了全连接层、ReLU激活函数、Softmax 层,本节介绍本实验中新增的基本单元:卷积层和最大池化层。

卷积神经网络 画图 卷积神经网络图片分类_分类

(2)卷积层

与全连接层类似,卷积层中的参数包括权重(即卷积核参数)和偏置。VGG19 中使用
的都是多输入输出特征图的卷积运算。假设输入特征图 X 的维度为 N ×Cin × Hin × Win,其中 N 是输入的样本个数(在本实验中 N = 1),Cin 是输入的通道数,Hin 和 Win 是输入特征图的高和宽。卷积核张量 W 用四维矩阵表示,维度为 Cin × K × K × Cout,其中 K × K 为卷积核的高度 × 宽度,Cout 为输出特征图的通道数。卷积层的偏置 b 用一维向量表示,维度为 Cout。同时定义输入特征图的边界扩充大小 p、卷积步长 s。输出特征图 Y 由输入 X 与卷积核 W 内积并加偏置 b 计算得到,Y 的维度为 N × Cout × Hout × Wout,其中 Hout 和Wout 是输出特征图的高和宽。

(3)最大池化层

为了有效地减少计算量,CNN使用的另一个有效的工具被称为“池化(Pooling)”。池化就是将输入图像进行缩小,减少像素信息,只保留重要信息。
池化(pool)即下采样(downsamples),目的是为了减少特征图。

假设最大池化层的输入特征图 X 的维度为 N × C × Hin × Win,其中 N 是输入的样本个

数(在本实验中 N = 1),C 是输入的通道数,Hin 和 Win 是输入特征图的高和宽。池化窗口的高和宽均为 K,池化步长为 s,输出特征图 Y 的维度为 N ×C × Hout × Wout,其中 Hout和 Wout 是输出特征图的高和宽。

前向传播计算时,输出特征图 Y 中某一位置的值是输入特征图 X 的对应池化窗口内的

最大值,计算公式为

卷积神经网络 画图 卷积神经网络图片分类_卷积_02


池化窗口 和 池化步长

池化窗口一般为2x2,步幅为2,相对于卷积层进行卷积运算,池化层进行的运算一般有以下几种:

  • 最大池化(Max Pooling)。取4个点的最大值。这是最常用的池化方法。
  • 均值池化(Mean Pooling)。取4个点的均值。
  • 高斯池化。借鉴高斯模糊的方法。不常用。
  • 可训练池化。训练函数 ff ,接受4个点为输入,出入1个点。不常用。

举例

卷积神经网络 画图 卷积神经网络图片分类_深度学习_03

3.VGG19网络结构

VGG19是经典的深度卷积神经网络结构,包含 5 个阶段共 16 个卷积层和 3 个全连接层,如表所示。前 2 个阶段各有 2 个卷积层,后 3 个阶段各有 4 个卷积层。每个卷积层均使用 3 × 3 大小的卷积核,边界扩充大小为 1,步长为 1,即保持输入输出特征图的高

和宽不变。每个阶段的卷积层的通道数在不断变化。在每个阶段的第一个卷积层,输入通

道数为上一个卷积层的输出通道数(第一个阶段的输入通道数为原始图像通道数)。5 个阶段的卷积层输出通道数分别为 64、128、256、512、512。每个阶段除第一个卷积层外,其他卷积层均保持输入和输出通道数相同。每个卷积层后面都跟随有 ReLU 层作为激活函数,每个阶段最后都跟随有一个最大池化层,将特征图的高和宽缩小为原来的 1/2。3 个全连接层中前 2 个全连接层后面也跟随有 ReLU 层。值得注意的是,第五阶段输出的特征图会进行变形,将四维特征图变形为二维矩阵作为全连接层的输入。网络最后是 Softmax 层计算分类概率。VGG19 的超参数配置详见表3.1,注意表中省略了卷积层和全连接层后的 ReLU层。

卷积神经网络 画图 卷积神经网络图片分类_分类_04


卷积神经网络 画图 卷积神经网络图片分类_cnn_05

(1)数据加载模块

本实验采用 ImageNet 图像数据集,该数据集以.jpg 或.png 压缩文件格式存放每张 RGB 图像,且不同图像的尺寸可能不同。为了统一神经网络输入的大小,读入图像数据后,首先需要将图像缩放到 224×224大小,并存储在矩阵中。其次,需要对输入图像做标准化,将输入值范围从 [0,255] 标准化为均值为 0 的区间,从而提高神经网络的训练速度和稳定性。具体做法是图像的每个像素值减去 ImageNet 数据集的像素均值,该图像均值在加载 VGG19 模型参数的同时读入。本实验中使用 VGG19 模型中自带的图像均值进行输入图像标准化,是为了确保与官方使用VGG19 网络时的预处理方式保持一致。最后,将标准化后的图像矩阵转换为神经网络输入的统一维度,即 N × C × H × W,其中 N 是输入的样本数(由于图像是逐张读入的,因此N = 1),C 是输入的通道数(本实验输入图像是 RGB 彩色图像,因此 C = 3),H 和 W 分别表示输入的高和宽(缩放后的图像的高和宽均为 224)。

def load_image(self, image_dir):
        print('Loading and preprocessing image from ' + image_dir)
        self.input_image = scipy.misc.imread(image_dir)
        self.input_image = scipy.misc.imresize(self.input_image,[224,224,3])
        self.input_image = np.array(self.input_image).astype(np.float32)
        self.input_image -= self.image_mean
        self.input_image = np.reshape(self.input_image, [1]+list(self.input_image.shape))
        # input dim [N, channel, height, width]
        self.input_image = np.transpose(self.input_image, [0, 3, 1, 2])
(2)Flatten 层
  • 层的初始化:flatten 层用于改变特征图的维度,将输入特征图中每个样本的特征平铺
    成一个向量。初始化 flatten 层时需要定义输入特征图和输出特征图的维度。
  • 前向传播计算:假设输入特征图 X 的维度为 N × C × H × W,其中 N 是输入的样
    本个数(在本实验中 N = 1),C 是输入的通道数,H 和 W 是输入特征图的高和宽。将输入特征图中每个样本的特征平铺成一个向量后,输出特征图的维度变为 N × (CHW)。注意 VGG19 官方模型所使用的深度学习平台 MatConvNet[6] 的特征图存储方式与本实验中不同。MatConvNet 中特征图维度为 N×H×W×C,而本实验中特征图 X 的维度为 N×C×H×W。因此为避免使用官方模型计算出现错误,flatten 层在改变输入特征图的维度前,需要将输入特征图进行维度交换,保持与 MatConvNet 的特征图存储方式一致。
class FlattenLayer(object):
    def __init__(self, input_shape, output_shape):  # 扁平化层的初始化
        self.input_shape = input_shape
        self.output_shape = output_shape
        assert np.prod(self.input_shape) == np.prod(self.output_shape)
        print('\tFlatten layer with input shape %s, output shape %s.' % (str(self.input_shape), str(self.output_shape)))
    def forward(self, input):  # 前向传播的计算
        assert list(input.shape[1:]) == list(self.input_shape)
        # matconvnet feature map dim: [N, height, width, channel]
        # ours feature map dim: [N, channel, height, width]
        self.input = np.transpose(input, [0, 2, 3, 1])
        self.output = self.input.reshape([self.input.shape[0]] + list(self.output_shape))
        show_matrix(self.output, 'flatten out ')
        return self.output
(3)卷积层 与 池化层

这里不再过多赘述,直接上代码

# 卷积层
class ConvolutionalLayer(object):
    def __init__(self, kernel_size, channel_in, channel_out, padding, stride):
        # 卷积层的初始化
        self.kernel_size = kernel_size
        self.channel_in = channel_in
        self.channel_out = channel_out
        self.padding = padding
        self.stride = stride
        print('\tConvolutional layer with kernel size %d, input channel %d, output channel %d.' % (self.kernel_size, self.channel_in, self.channel_out))
    def init_param(self, std=0.01):  # 参数初始化
        self.weight = np.random.normal(loc=0.0, scale=std, size=(self.channel_in, self.kernel_size, self.kernel_size, self.channel_out))
        self.bias = np.zeros([self.channel_out])
    def forward(self, input):  # 前向传播的计算
        start_time = time.time()
        self.input = input # [N, C, H, W]
        height = self.input.shape[2] + self.padding * 2
        width = self.input.shape[3] + self.padding * 2
        self.input_pad = np.zeros([self.input.shape[0], self.input.shape[1], height, width])
        self.input_pad[:, :, self.padding:self.padding+self.input.shape[2], self.padding:self.padding+self.input.shape[3]] = self.input
        height_out = (height - self.kernel_size) / self.stride + 1
        width_out = (width - self.kernel_size) / self.stride + 1
        self.output = np.zeros([self.input.shape[0], self.channel_out, height_out, width_out])
        for idxn in range(self.input.shape[0]):
            for idxc in range(self.channel_out):
                for idxh in range(height_out):
                    for idxw in range(width_out):
                        # TODO: 计算卷积层的前向传播,特征图与卷积核的内积再加偏置
                        self.output[idxn, idxc, idxh, idxw] = _______________________
        return self.output
    def load_param(self, weight, bias):  # 参数加载
        assert self.weight.shape == weight.shape
        assert self.bias.shape == bias.shape
        self.weight = weight
        self.bias = bias
# 最大池化层
class MaxPoolingLayer(object):
    def __init__(self, kernel_size, stride):  # 最大池化层的初始化
        self.kernel_size = kernel_size
        self.stride = stride
        print('\tMax pooling layer with kernel size %d, stride %d.' % (self.kernel_size, self.stride))
    def forward_raw(self, input):  # 前向传播的计算
        start_time = time.time()
        self.input = input # [N, C, H, W]
        self.max_index = np.zeros(self.input.shape)
        height_out = (self.input.shape[2] - self.kernel_size) / self.stride + 1
        width_out = (self.input.shape[3] - self.kernel_size) / self.stride + 1
        self.output = np.zeros([self.input.shape[0], self.input.shape[1], height_out, width_out])
        for idxn in range(self.input.shape[0]):
            for idxc in range(self.input.shape[1]):
                for idxh in range(height_out):
                    for idxw in range(width_out):
			            # TODO: 计算最大池化层的前向传播, 取池化窗口内的最大值
                        self.output[idxn, idxc, idxh, idxw] = _______________________
        return self.output
(4)网络结构模块
  • 神经网络初始化:确定神经网络相关的超参数。为方便起见,本实验在网络初始化
    时仅设定每层的名称,在建立网络结构时再设定每层的具体超参数。
  • 建立网络结构:定义整个神经网络的拓扑结构,设定每层的超参数,实例化基本单
    元模块中定义的层并将这些层堆叠,组成 VGG19 网络结构。根据表3.1中 VGG19 的网络结构和每层的超参数进行实例化。注意每个卷积层和 3 个全连接层中的前 2 个全连接层后面都跟随有 ReLU 层作为激活函数。此外,pool5 层和 fc6 层中间有一个 flatten 层改变特征图的维度。最后是 Softmax 层计算分类概率。
  • 神经网络参数初始化:依次调用神经网络中包含参数的层的参数初始化函数。在本
    实验中,VGG19 中的 16 个卷积层和 3 个全连接层包含参数,因此需要依次调用其参数初始化函数
class VGG19(object):
    def __init__(self, param_path='../../imagenet-vgg-verydeep-19.mat'):
        self.param_path = param_path
        self.param_layer_name = (
            'conv1_1', 'relu1_1', 'conv1_2', 'relu1_2', 'pool1',
            'conv2_1', 'relu2_1', 'conv2_2', 'relu2_2', 'pool2',
            'conv3_1', 'relu3_1', 'conv3_2', 'relu3_2', 'conv3_3', 'relu3_3', 'conv3_4', 'relu3_4', 'pool3',
            'conv4_1', 'relu4_1', 'conv4_2', 'relu4_2', 'conv4_3', 'relu4_3', 'conv4_4', 'relu4_4', 'pool4',
            'conv5_1', 'relu5_1', 'conv5_2', 'relu5_2', 'conv5_3', 'relu5_3', 'conv5_4', 'relu5_4', 'pool5',
            'flatten', 'fc6', 'relu6', 'fc7', 'relu7', 'fc8', 'softmax'
        )        

    def build_model(self):
        # TODO:定义VGG19 的网络结构
        print('Building vgg-19 model...')

        self.layers = {}
        self.layers['conv1_1'] = ConvolutionalLayer(3, 3, 64, 1, 1)
        self.layers['relu1_1'] = ReLULayer()
        self.layers['conv1_2'] = ConvolutionalLayer(3, 64, 64, 1, 1)
        self.layers['relu1_2'] = ReLULayer()
        self.layers['pool1'] = MaxPoolingLayer(2, 2)

        _______________________

        self.layers['conv5_4'] = ConvolutionalLayer(3, 512, 512, 1, 1)
        self.layers['relu5_4'] = ReLULayer()
        self.layers['pool5'] = MaxPoolingLayer(2, 2)

        self.layers['flatten'] = FlattenLayer([512, 7, 7], [512*7*7])
        self.layers['fc6'] = FullyConnectedLayer(512*7*7, 4096)
        self.layers['relu6'] = ReLULayer()

        _______________________

        self.layers['fc8'] = FullyConnectedLayer(4096, 1000)

        self.layers['softmax'] = SoftmaxLossLayer()

        self.update_layer_list = []
        for layer_name in self.layers.keys():
            if 'conv' in layer_name or 'fc' in layer_name:
                self.update_layer_list.append(layer_name)
(5)网络推断模块
  • 神经网络的前向传播:前向传播的输入是预处理后的图像。首先将预处理后的图像
    输入到 VGG19 网络的第一层;然后根据之前定义的 VGG19 网络的结构,顺序依次调用每层的前向传播函数,每层的输出作为下一层的输入。由于 VGG19 中的网络层数较多,可以利用网络初始化时定义的层队列,建立循环实现前向传播。
  • 神经网络参数的加载:利用官方训练好的 VGG19 模型参数,依次将其中的参数加载到 VGG19 对应的层中。本实验使用的官方模型的下载地址为http://www.vlfeat.org/
    matconvnet/models/beta16/imagenet-vgg-verydeep-19.mat。VGG19 中包含参数的网络层是卷积层和全连接层,可以根据层的编号依次读入对应卷积层和全连接层的权重和偏置。注意在本实验的网络初始化中,在 pool5 层和 fc6 层之间添加了 flatten 层来改变特征图的维度,而官方提供的模型不包含 flatten 层,因此 fc6 层及之后的层在读取参数时需要偏移。同时值得注意的是,VGG19 官方模型使用的深度学习平台 MatConvNet[6] 的卷积权重的存储方式与本实验不同。MatConvNet 中卷积权重维度为 H × W ×Cin ×Cout,而本实验中权重的维度为 Cin × H × W × Cout。为防止使用官方模型计算出现错误,在读取卷积层权重时需要对输入权重做维度交换,保持与 MatConvNet 的权重存储方式一致。此外还可以从该模型中读取预处理图像时使用的图像均值。
  • 神经网络推断函数主体:本实验仅需要对给定的一张图像进行分类,因此给定一张
    预处理好的图像,执行网络前向传播函数即可获得 VGG19 预测的 1000 个类别的分类概率,然后取其中概率最大的类别作为最终预测的分类类别。在实际应用中,可能需要对一个数据集中的多张测试图像依次进行分类,然后与测试图像对应的标记进行比对,即可得到测试数据集的分类正确率。
def init_model(self):
        print('Initializing parameters of each layer in vgg-19...')
        for layer_name in self.update_layer_list:
            self.layers[layer_name].init_param()

    def load_model(self):
        print('Loading parameters from file ' + self.param_path)
        params = scipy.io.loadmat(self.param_path)
        self.image_mean = params['normalization'][0][0][0]
        self.image_mean = np.mean(self.image_mean, axis=(0, 1))
        print('Get image mean: ' + str(self.image_mean))

        for idx in range(43):
            if 'conv' in self.param_layer_name[idx]:
                weight, bias = params['layers'][0][idx][0][0][0][0]
                # matconvnet: weights dim [height, width, in_channel, out_channel]
                # ours: weights dim [in_channel, height, width, out_channel]
                weight = np.transpose(weight,[2,0,1,3])
                bias = bias.reshape(-1)
                self.layers[self.param_layer_name[idx]].load_param(weight, bias)
            if idx >= 37 and 'fc' in self.param_layer_name[idx]:
                weight, bias = params['layers'][0][idx-1][0][0][0][0]
                weight = weight.reshape([weight.shape[0]*weight.shape[1]*weight.shape[2], weight.shape[3]])
                self.layers[self.param_layer_name[idx]].load_param(weight, bias)

    def load_image(self, image_dir):
        print('Loading and preprocessing image from ' + image_dir)
        self.input_image = scipy.misc.imread(image_dir)
        self.input_image = scipy.misc.imresize(self.input_image,[224,224,3])
        self.input_image = np.array(self.input_image).astype(np.float32)
        self.input_image -= self.image_mean
        self.input_image = np.reshape(self.input_image, [1]+list(self.input_image.shape))
        # input dim [N, channel, height, width]
        self.input_image = np.transpose(self.input_image, [0, 3, 1, 2])

    def forward(self):
        print('Inferencing...')
        start_time = time.time()
        current = self.input_image
        for idx in range(len(self.param_layer_name)):
            print('Inferencing layer: ' + self.param_layer_name[idx])
            current = self.layers[self.param_layer_name[idx]].forward(current)
        print('Inference time: %f' % (time.time()-start_time))
        return current

    def evaluate(self):
        prob = self.forward()
        top1 = np.argmax(prob[0])
        print('Classification result: id = %d, prob = %f' % (top1, prob[0, top1]))


if __name__ == '__main__':
    vgg = VGG19()
    vgg.build_model()
    vgg.init_model()
    vgg.load_model()
    vgg.load_image('../../cat1.jpg')
    prob = vgg.evaluate()

4.图像分类实例

使用VGG19卷积神经网络模型查看下列图像分类:

卷积神经网络 画图 卷积神经网络图片分类_卷积_06

(1) 将图像与模型加载

def test_conv_and_pool_layer():
    test_data = np.random.rand(1, 3, 22, 22)
    test_filter = np.random.rand(3, 3, 3, 64)
    test_bias = np.random.rand(64)

    conv = ConvolutionalLayer(3, 3, 64, 1, 1)
    conv.init_param()
    conv.load_param(test_filter, test_bias)
    conv_result = conv.forward(test_data)

    std_conv = standardConv(3, 3, 64, 1, 1)
    std_conv.init_param()
    std_conv.load_param(test_filter, test_bias)
    std_conv_result = std_conv.forward(test_data)
    
    conv_mse = computeMse(conv_result.flatten(), std_conv_result.flatten())
    print('test conv err rate: %f%%'%(conv_mse*100))

    pool = MaxPoolingLayer(2, 2)
    pool_result = pool.forward(test_data)

    std_pool = standardPool(2, 2)
    std_pool_result = std_pool.forward(test_data)

    pool_mse = computeMse(pool_result.flatten(), std_pool_result.flatten())
    print('test pool err rate: %f%%'%(pool_mse*100))

    if conv_mse < 0.003 and pool_mse < 0.003:
        print('TEST CONV AND POOL LAYER PASS.')
    else:
        print('TEST CONV AND POOL LAYER FAILED.')
        exit()

def forward(vgg):
    print('Inferencing...')
    start_time = time.time()
    current = vgg.input_image
    pool5 = np.array([])
    for idx in range(len(vgg.param_layer_name)):
        print('Inferencing layer: ' + vgg.param_layer_name[idx])
        current = vgg.layers[vgg.param_layer_name[idx]].forward(current)
        if 'pool5' in vgg.param_layer_name[idx]:
            pool5 = current
    print('Inference time: %f' % (time.time()-start_time))
    return current, pool5

def check_pool5(stu_pool5):
    data = np.load('pool5_dump.npy')
    pool5_mse = computeMse(stu_pool5.flatten(), data.flatten())
    print('test pool5 mse: %f'%pool5_mse)

    if pool5_mse < 0.003:
        print('CHECK POOL5 PASS.')
    else:
        print('CHECK POOL5 FAILED.')
        exit()

def evaluate(vgg):
    prob, pool5 = forward(vgg)
    top1 = np.argmax(prob[0])
    print('Classification result: id = %d, prob = %f' % (top1, prob[0, top1]))
    return pool5

if __name__ == '__main__':
    test_conv_and_pool_layer()
    print('-------------------------------')
    vgg = VGG19(param_path='../imagenet-vgg-verydeep-19.mat')
    vgg.build_model()
    vgg.init_model()
    vgg.load_model()
    vgg.load_image('../cat1.jpg')
    pool5 = evaluate(vgg)
    print('-------------------------------')
    check_pool5(pool5)

实验结果

卷积神经网络 画图 卷积神经网络图片分类_卷积神经网络 画图_07


图像分类编号为281,预测准确率50.7269%