1. 安装

  • 首先,导入numpy和matplotlib库
# numpy是常用的科学计算库,matplot是常用的绘图库
import numpy as np
import matplotlib.pyplot as plt
# 在notebook中展示图例
%matplotlib inline

# 设置图例展示的默认参数
plt.rcParams['figure.figsize'] = (10, 10)  # 图片大小为10*10
plt.rcParams['image.interpolation'] = 'nearest'  # 图片的放缩采用最近邻插值法
plt.rcParams['image.cmap'] = 'gray'  # 图片的色彩图为灰阶
  • 导入caffe框架
import caffe
# 传入caffe的地址,因为该例程的一些数据都存在caffe目录中
caffe_root = r'G:/Projects/caffe/'

2. 载入网络并对输入进行预处理

  • 设置caffe为cpu模式
caffe.set_mode_cpu()
  • 基于已有的一些配置项定义网络模型
model_def = caffe_root + 'models/bvlc_reference_caffenet/deploy.prototxt'
model_weights = caffe_root + 'models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel'

net = caffe.Net(model_def,      # 定义模型的结构
                model_weights,  # 载入预训练模型的权重
                caffe.TEST)     # 用测试模型进行测试(测试模型中不包括dropout层)
  • 详细解释一下模型配置文件(deploy.prototxt)
name: "CaffeNet"        # 模型的名称(自定义)
layer {                 # 第一层(输入层)
  name: "data"          # 该层的名称(自定义),此处为“data”
  type: "Input"         # 该层的类型(重要参数),此处为输入层(Input)
  top: "data"           # 该层的输出数组名为“data”
  input_param { shape: { dim: 10 dim: 3 dim: 227 dim: 227 } }
}                       # 配置输入层参数,4维数组,数量*通道*高*宽,即10张RGB彩色图像,大小为227*227
layer {                 # 第二层(卷积层1)
  name: "conv1"         # 该层的名称(自定义),此处为“conv1”
  type: "Convolution"   # 该层的类型(重要参数),此处为卷积层(Convolution)
  bottom: "data"        # 该层的输入数组名为“data”
  top: "conv1"          # 该层的输出数组名为“conv1”
  convolution_param {   # 配置卷积层参数 
    num_output: 96      # 卷积核数量为96
    kernel_size: 11     # 卷积核大小为11*11
    stride: 4           # 卷积核的步长为4
  } 
}
layer {                 # 第三层(激活层1)
  name: "relu1"         # 该层的名称(自定义),此处为“relu1”
  type: "ReLU"          # 该层的类型(重要参数),此处是以ReLu为激活函数的激活层
  bottom: "conv1"       # 该层的输入数组名为“conv1”
  top: "conv1"          # 该层的输出数组名为“conv1”
}
layer {                 # 第四层(池化层1)
  name: "pool1"         # 该层的名称(自定义),此处为“pool1”
  type: "Pooling"       # 该层的类型(重要参数),此处是池化层
  bottom: "conv1"       # 该层的输入数组名为“conv1”
  top: "pool1"          # 该层的输出数字名为“pool1”
  pooling_param {       # 配置池化层参数
    pool: MAX           # 池化方式为 Max-pooling
    kernel_size: 3      # 池化区域大小为3*3
    stride: 2           # 池化操作的步长为2
  }
}
layer {                 # 第五层(局部响应归一化层)
  name: "norm1"         # 该层的名称(自定义),此处为“norm1”
  type: "LRN"           # 该层的类型(重要参数),此处是LRN(Local Response Normalization)
  bottom: "pool1"       # 该层的输入数组名为“pool1”
  top: "norm1"          # 该层的输出数组名为“norm1”
  lrn_param {           # 配置LRN层参数
    local_size: 5       # 参与求和的矩形区域边长
    alpha: 0.0001       # 尺度参数
    beta: 0.75          # 指数参数
  }
}
layer {                 # 第六层(卷积层2)
  name: "conv2"         # 该层的名称(自定义),此处为“conv2”
  type: "Convolution"   # 该层的类型(重要参数),此处为卷积层(Convolution)
  bottom: "norm1"       # 该层的输入数组名为“norm1”
  top: "conv2"          # 该层的输出数组名为“conv2”
  convolution_param {   # 配置卷积层参数
    num_output: 256     # 卷积核数量为256
    pad: 2              # 指定添加到输入每条边的像素点个数
    kernel_size: 5      # 卷积核大小为5*5
    group: 2            # 分组卷积操作的组数为2,即输入和输出的通道都被分为2组,第1组输入只会连接到第1组输出,第2组输入只会连接到第2组输出。
  }
}
layer {
  name: "relu2"
  type: "ReLU"
  bottom: "conv2"
  top: "conv2"
}
layer {
  name: "pool2"
  type: "Pooling"
  bottom: "conv2"
  top: "pool2"
  pooling_param {
    pool: MAX
    kernel_size: 3
    stride: 2
  }
}
layer {
  name: "norm2"
  type: "LRN"
  bottom: "pool2"
  top: "norm2"
  lrn_param {
    local_size: 5
    alpha: 0.0001
    beta: 0.75
  }
}
layer {
  name: "conv3"
  type: "Convolution"
  bottom: "norm2"
  top: "conv3"
  convolution_param {
    num_output: 384
    pad: 1
    kernel_size: 3
  }
}
layer {
  name: "relu3"
  type: "ReLU"
  bottom: "conv3"
  top: "conv3"
}
layer {
  name: "conv4"
  type: "Convolution"
  bottom: "conv3"
  top: "conv4"
  convolution_param {
    num_output: 384
    pad: 1
    kernel_size: 3
    group: 2
  }
}
layer {
  name: "relu4"
  type: "ReLU"
  bottom: "conv4"
  top: "conv4"
}
layer {
  name: "conv5"
  type: "Convolution"
  bottom: "conv4"
  top: "conv5"
  convolution_param {
    num_output: 256
    pad: 1
    kernel_size: 3
    group: 2
  }
}
layer {
  name: "relu5"
  type: "ReLU"
  bottom: "conv5"
  top: "conv5"
}
layer {
  name: "pool5"
  type: "Pooling"
  bottom: "conv5"
  top: "pool5"
  pooling_param {
    pool: MAX
    kernel_size: 3
    stride: 2
  }
}
layer {                    # 第(我也不知道多少)层(全连接层)
  name: "fc6"              # 该层的名称(自定义),此处为“fc6”
  type: "InnerProduct"     # 该层的类型(重要参数),此处为内积层(也叫全连接层)
  bottom: "pool5"          # 该层的输入数组名为“pool5”
  top: "fc6"               # 该层的输出数组名为“fc6”
  inner_product_param {    # 配置全连接层参数
    num_output: 4096       # 该层的神经元个数
  }
}
layer {                    
  name: "relu6"
  type: "ReLU"
  bottom: "fc6"
  top: "fc6"
}
layer {                     # 第(我也不知道多少+2)层(Dropout层,防止过拟合)
  name: "drop6"             # 该层的名称(自定义),此处为“drop6”
  type: "Dropout"           # 该层的类型(重要参数),此处为Dropout层
  bottom: "fc6"             # 该层的输入数组名为“fc6”
  top: "fc6"                # 该层的输出数组名为“fc6”
  dropout_param {           # 配置Dropout层参数
    dropout_ratio: 0.5      # dropout的比例系数为0.5
  }
}
layer {
  name: "fc7"
  type: "InnerProduct"
  bottom: "fc6"
  top: "fc7"
  inner_product_param {
    num_output: 4096
  }
}
layer {
  name: "relu7"
  type: "ReLU"
  bottom: "fc7"
  top: "fc7"
}
layer {
  name: "drop7"
  type: "Dropout"
  bottom: "fc7"
  top: "fc7"
  dropout_param {
    dropout_ratio: 0.5
  }
}
layer {
  name: "fc8"
  type: "InnerProduct"
  bottom: "fc7"
  top: "fc8"
  inner_product_param {
    num_output: 1000
  }
}
layer {                      # 第(最后一)层(输出层)
  name: "prob"               # 该层的名称(自定义),此处为“prob”,含义是输出概率
  type: "Softmax"            # 该层的类型(重要参数),此处为Softmax层
  bottom: "fc8"              # 该层的输入数组名为“fc8”
  top: "prob"                # 该层的输出数组名为“prob”
}

  • 给每一层都命名的好处在于之后可以方便的调用任意一层的输入或者输出来进行分析(如可视化,或者是提取特征等等)

  • 一般而言,习惯于将激活层、Dropout层的输入输出给予同样的名称,因为这并不是我们所关心的神经网络黑箱中的内容,相对而言,我们更关心卷积层、池化层的输出(常见的finetune技巧即提取conv或者是pooling层的输出作为特征)

  • 一些预处理的准备工作

# 载入ImageNet的均值图,是一个3*256*256的矩阵
mu = np.load(caffe_root + 'python/caffe/imagenet/ilsvrc_2012_mean.npy')
mu = mu.mean(1).mean(1)  # 计算出BGR通道的平均像素值
print('Mean-subtracted values:', list(zip('BGR', mu)))
Mean-subtracted values: [('B', 104.0069879317889), ('G', 116.66876761696767), ('R', 122.6789143406786)]
  • 这里我们使用 caffe.io.Transformer 对输入数据进行预处理。

    我们默认的CaffeNet是将彩色图片以BGR格式读入,并且范围是[0, 255],通道维在第一个(outermost)维度上。

# 为该网络的输入层(“data”层,10*3*227*227的数组)创建一个transformer对象  
transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape})

transformer.set_transpose('data', (2,0,1))  # 用caffe.io.load_image读入的图像以row*col*channel形式储存,而在caffe中我们定义的是channel*row*col,
                                            # 因此需要转换读入的图像数据,将原来下标为2的channel维移至下标为0的位置。
transformer.set_mean('data', mu)            # 对每一个色彩通道做减均值的操作
transformer.set_raw_scale('data', 255)      # 将归一化的数据转为[0, 255]范围的数据
transformer.set_channel_swap('data', (2,1,0))  # 将色彩通道的顺序由RGB改为BGR(这样减去均值的时候才是对应的)

3. 基于CPU的分类

  • 尽管我们只分类一张图片,也还是可以设置batch-size为50
# 设置输入图片的大小
net.blobs['data'].reshape(50,        # batch size
                          3,         # 3-channel (BGR) images
                          227, 227)  # image size is 227x227
image = caffe.io.load_image(caffe_root + 'examples/images/cat.jpg')
transformed_image = transformer.preprocess('data', image)
plt.imshow(image)
<matplotlib.image.AxesImage at 0x150008d3128>

【caffe范例详解】 - 1.Classification分类

  • 下面开始进行分类
# 将该图片传入网络作为输入(这样似乎是把输入的十张图都表示成该图像)
net.blobs['data'].data[...] = transformed_image

# 开始进行分类
# 将输入参数进行前向传播(因为我们已经载入了预先训练的网络,因此一次前向传播即可获得最终结果)
output = net.forward()
# 获得对第一张图片的输出预测向量
output_prob = output['prob'][0] 
# 选取其中获得最大预测值的向量下标,作为预测的分类下标
print('predicted class is:', output_prob.argmax())
predicted class is: 281
  • 通过比对下标在ImageNet中表示的真实类别,验证分类结果的准确性
import os
# load ImageNet labels
labels_file = caffe_root + 'data/ilsvrc12/synset_words.txt'
if not os.path.exists(labels_file):
    !caffe_root + 'data/ilsvrc12/get_ilsvrc_aux.sh'
    
labels = np.loadtxt(labels_file, str, delimiter='\t')

print('output label:', labels[output_prob.argmax()])
output label: b'n02123045 tabby, tabby cat'
  • 该类别下标表示的是 tabby cat, 结果正确!

  • 我们可以看一下该图片在1000类中top5可能性的分类结果

# 将softmax输出的结果用argsort()方法排序并翻转(因为该方法默认由小到大排序)
# 取前5项输出
top_inds = output_prob.argsort()[::-1][:5]  

print('probabilities and labels:')
list(zip(output_prob[top_inds], labels[top_inds]))
probabilities and labels:





[(0.3124359, "b'n02123045 tabby, tabby cat'"),
 (0.23797129, "b'n02123159 tiger cat'"),
 (0.1238722, "b'n02124075 Egyptian cat'"),
 (0.10075704, "b'n02119022 red fox, Vulpes vulpes'"),
 (0.070956774, "b'n02127052 lynx, catamount'")]

4. 切换到GPU模式

  • 我们来看一下用cpu做计算时,一次前向传播的耗时
%timeit net.forward()
1 loop, best of 3: 1.61 s per loop
  • 切换到GPU模式,再看一下前向传播的耗时
caffe.set_device(0)  # 如果我们有多个GPU的话,选择第一个
caffe.set_mode_gpu() # 切换到GPU模式
net.forward()  # 进行一次前向传播运算
%timeit net.forward()
10 loops, best of 3: 26 ms per loop

5. 检查中间输出

  • 我们来打开网络中间这个“黑箱子”,看看其中的一些参数和激活值

  • 首先,我们来看一下网络中各层神经元输出的参数(即激活值)的shape

  • 对于每一层,我们关注以下信息:(批处理数据大小, 通道数, 高度, 宽度),即 (batch_size, channel_dim, height, width)

  • 激活值保存在net.blobs中

for layer_name, blob in net.blobs.items():
    print(layer_name, '\t', str(blob.data.shape))
data 	 (10, 3, 227, 227)
conv1 	 (10, 96, 55, 55)
pool1 	 (10, 96, 27, 27)
norm1 	 (10, 96, 27, 27)
conv2 	 (10, 256, 27, 27)
pool2 	 (10, 256, 13, 13)
norm2 	 (10, 256, 13, 13)
conv3 	 (10, 384, 13, 13)
conv4 	 (10, 384, 13, 13)
conv5 	 (10, 256, 13, 13)
pool5 	 (10, 256, 6, 6)
fc6 	 (10, 4096)
fc7 	 (10, 4096)
fc8 	 (10, 1000)
prob 	 (10, 1000)
  • 然后,我们来看一下各层之间参数的shape

  • 参数值保存在net.params中,权重(weight)在[0]下标的索引中,偏置(bias)在[1]下标的索引中。

  • 对于权重,我们关注(输出通道数,输入通道数,滤波器高度,滤波器宽度),即 (output_channels, input_channels, filter_height, filter_width)

  • 对于偏置,我们关注(输出通道数,),即(output_channels,)

for layer_name, param in net.params.items():
    print( layer_name, '\t', str(param[0].data.shape), str(param[1].data.shape))
conv1 	 (96, 3, 11, 11) (96,)
conv2 	 (256, 48, 5, 5) (256,)
conv3 	 (384, 256, 3, 3) (384,)
conv4 	 (384, 192, 3, 3) (384,)
conv5 	 (256, 192, 3, 3) (256,)
fc6 	 (4096, 9216) (4096,)
fc7 	 (4096, 4096) (4096,)
fc8 	 (1000, 4096) (1000,)
  • 我们来定义一个矩形热力图的可视化函数来帮助我们观察“黑箱”中的特征
def vis_square(data):
    """
    输入矩阵的大小为(n, height, width)或者(n, height, width, 3)
    可视化每一个输入以sqrt(n)*sqrt(n)的方格展示
    """
    
    # 归一化
    data = (data - data.min()) / (data.max() - data.min())
    
    # force the number of filters to be square
    n = int(np.ceil(np.sqrt(data.shape[0])))
    padding = (((0, n ** 2 - data.shape[0]),
               (0, 1), (0, 1))                 # add some space between filters
               + ((0, 0),) * (data.ndim - 3))  # don't pad the last dimension (if there is one)
    data = np.pad(data, padding, mode='constant', constant_values=1)  # pad with ones (white)
    
    # 将滤波器排列成图片的shape用于展示
    data = data.reshape((n, n) + data.shape[1:]).transpose((0, 2, 1, 3) + tuple(range(4, data.ndim + 1)))
    data = data.reshape((n * data.shape[1], n * data.shape[3]) + data.shape[4:])
    
    plt.imshow(data); plt.axis('off')
  • 首先,我们来看第一层滤波器conv1
# 可视化的参数是 [weights, biases]
filters = net.params['conv1'][0].data
vis_square(filters.transpose(0, 2, 3, 1))

【caffe范例详解】 - 1.Classification分类

  • 第一层conv1的输出(只展示前36个)
feat = net.blobs['conv1'].data[0, :36]
vis_square(feat)

【caffe范例详解】 - 1.Classification分类

  • 第五层pooling之后的输出,pool5
feat = net.blobs['pool5'].data[0]
vis_square(feat)

【caffe范例详解】 - 1.Classification分类

  • 第一个全连接层,fc6
  • 展示输出值以及正值的直方图
feat = net.blobs['fc6'].data[0]
plt.subplot(2, 1, 1)
plt.plot(feat.flat)
plt.subplot(2, 1, 2)
_ = plt.hist(feat.flat[feat.flat > 0], bins=100)

【caffe范例详解】 - 1.Classification分类

  • 最终概率预测输出的直方图
feat = net.blobs['prob'].data[0]
plt.figure(figsize=(15, 3))
plt.plot(feat.flat)
[<matplotlib.lines.Line2D at 0x15017512278>]

【caffe范例详解】 - 1.Classification分类