Caffe可以通过LMDB或LevelDB数据格式实现图像数据及标签的输入,不过这只限于单标签图像数据的输入。由于研究生期间所从事的研究是图像标注领域,在进行图像标注时,每幅图像都是多标签的,因此在使用Caffe进行迁移学习时需要实现多标签图像数据的输入。走过许多弯路,要毕业了,现在将这种比较实用的方法做一下总结方便后面学弟学妹的学习。
经过百度查找,发现目前的主流做法有两种:1、修改Caffe的C++源码实现多标签数据的输入,如果不是C++大神建议不要轻易尝试,很容易最后你的Caffe也被你改崩了;2、使用HDF5数据实现多标签数据的输入(标签数据是一组0和1组成的向量),利用Caffe的Slice层实现标签数据的提取,依次为每个类别构建一个num_out=2的全连接层,也就是为每个类别构建一个二分类器,判别这幅图像是否包含这个标注词,这种方法小类别图像数据时可以试试,当图像类别过高时就很难受了,当时我写这些二分类器都要哭了。
后来偷了个懒,采用了简单复制的策略,即一幅图像包含几个标签就将这张图像复制几次,每次让它对应不同的标签,这种方法贼简单,其实不需复制图像,就是在生成LMDB文件的txt文件上略作修改即可。
后来意外在网上发现了这篇国外老哥写的文章:http://nbviewer.jupyter.org/github/BVLC/caffe/blob/master/examples/pascal-multilabel-with-datalayer.ipynb。发现原来竟然可以通过Python接口实现多标签数据的输入,并且Caffe也提供了可以实现多标签数据训练的损失函数SigmoidCrossEntropyLoss。
国外老哥写的这篇文章是基于VOC212数据集的,下载地址为:http://host.robots.ox.ac.uk/pascal/VOC/voc2012/index.html
这些代码时一体的,这里将其分成几部分,建议读者也是分成这几部分一步一步的运行,这样方便理解代码的用意,这些段组起来就是完整的流程。运行时强烈建议实用CMD窗口运行,CMD窗口运行时如果是网络文件有误会显示网络文件的错误,同时也可以看见网络运行时的Loss值以及迭代次数。用python的IDE运行只能看见每迭代100次的准确率,而且如果网络文件有问题它不会报错,只会提示进程死了,无从下手去修改。
import sys
import os
import numpy as np
import os.path as osp
import matplotlib.pyplot as plt
from copy import copy
# 用于显示图像
plt.rcParams['figure.figsize'] = (6, 6)
# 导入Caffe
import caffe
from caffe import layers as L, params as P
# 添加路径这里面有网络和代码运行的文件(python的data层以及下面导入的tools)
sys.path.append(r"D:\caffe-master\examples\pycaffe")
sys.path.append(r"D:\caffe-master\examples\pycaffe\layers")
import tools
transformer = tools.SimpleTransformer()
# 数据集路径
pascal_root = r"E:\VOC2012"
# 数据集内图像标签的类别
classes = np.asarray(['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor'])
# 指定权重文件的路径
if not os.path.isfile(r'D:\caffe-master\models\bvlc_reference_caffenet\bvlc_reference_caffenet.caffemodel'):
print("Please downloading pre-trained CaffeNet model...")
# 设定网络训练的方式为GPU训练
caffe.set_mode_gpu()
sys.path.append(r"D:\caffe-master\examples\pycaffe")
要用到这个文件夹下的tools.py,实现图像数据的预处理。建议看一下这个文件,方便后面为自己的数据集定制图像数据处理的文件。
sys.path.append(r"D:\caffe-master\examples\pycaffe\layers")
要调用这个文件夹下的pascal_multilabel_datalayers.py实现网络数据的输入。建议看一下这个文件,方便后面为自己的数据集定制图像数据输入的文件。
# 定义用于生成卷积层的函数
def conv_relu(bottom, ks, nout, stride=1, pad=0, group=1):
conv = L.Convolution(bottom, kernel_size=ks, stride=stride,
num_output=nout, pad=pad, group=group)
return conv, L.ReLU(conv, in_place=True)
# 定义用于生成全连接层的函数
def fc_relu(bottom, nout):
fc = L.InnerProduct(bottom, num_output=nout)
return fc, L.ReLU(fc, in_place=True)
# 定义用于生成池化层的函数
def max_pool(bottom, ks, stride=1):
return L.Pooling(bottom, pool=P.Pooling.MAX, kernel_size=ks, stride=stride)
# 定义生成CaffeNet网络结构的函数
def caffenet_multilabel(data_layer_params, datalayer):
# 生成Python的data layer
n = caffe.NetSpec()
n.data, n.label = L.Python(module = 'pascal_multilabel_datalayers', layer = datalayer,
ntop = 2, param_str=str(data_layer_params))
# CaffeNet所包含的各层
n.conv1, n.relu1 = conv_relu(n.data, 11, 96, stride=4)
n.pool1 = max_pool(n.relu1, 3, stride=2)
n.norm1 = L.LRN(n.pool1, local_size=5, alpha=1e-4, beta=0.75)
n.conv2, n.relu2 = conv_relu(n.norm1, 5, 256, pad=2, group=2)
n.pool2 = max_pool(n.relu2, 3, stride=2)
n.norm2 = L.LRN(n.pool2, local_size=5, alpha=1e-4, beta=0.75)
n.conv3, n.relu3 = conv_relu(n.norm2, 3, 384, pad=1)
n.conv4, n.relu4 = conv_relu(n.relu3, 3, 384, pad=1, group=2)
n.conv5, n.relu5 = conv_relu(n.relu4, 3, 256, pad=1, group=2)
n.pool5 = max_pool(n.relu5, 3, stride=2)
n.fc6, n.relu6 = fc_relu(n.pool5, 4096)
n.drop6 = L.Dropout(n.relu6, in_place=True)
n.fc7, n.relu7 = fc_relu(n.drop6, 4096)
n.drop7 = L.Dropout(n.relu7, in_place=True)
n.score = L.InnerProduct(n.drop7, num_output=20)
n.loss = L.SigmoidCrossEntropyLoss(n.score, n.label)
return str(n.to_proto())
# 定义用于存储网络文件与solver文件的路径
workdir = r"D:\caffe-master\data\VOC2012"
if not os.path.isdir(workdir):
os.makedirs(workdir)
# 生成Solver文件
solverprototxt = tools.CaffeSolver(trainnet_prototxt_path = r"D:/caffe-master/data/VOC2012/trainnet.prototxt", testnet_prototxt_path = r"D:/caffe-master/data/VOC2012/valnet.prototxt")
solverprototxt.sp['display'] = "1"
solverprototxt.sp['base_lr'] = "0.0001"
solverprototxt.write(osp.join(workdir, 'solver.prototxt'))
# 生成用于训练的网络文件.
with open(osp.join(workdir, 'trainnet.prototxt'), 'w') as f:
# 设定data层的参数
data_layer_params = dict(batch_size = 128, im_shape = [227, 227], split = 'train', pascal_root = pascal_root)
f.write(caffenet_multilabel(data_layer_params, 'PascalMultilabelDataLayerSync'))
# 生成用于验证的网络文件.
with open(osp.join(workdir, 'valnet.prototxt'), 'w') as f:
data_layer_params = dict(batch_size = 128, im_shape = [227, 227], split = 'val', pascal_root = pascal_root)
f.write(caffenet_multilabel(data_layer_params, 'PascalMultilabelDataLayerSync'))
运行这段代码,会在指定的文件夹下生成trainnet.prototxt、valnet.prototxt和solver.prototxt文件
生成网络的data层如下所示:
生成网络的Loss层如下所示:
网络文件除data层与loss层与CaffeNet的网络结构不同外,其它层与经典的CaffeNet的结构一模一样,Caffe没有多标签的accuracy层,因此网络中没有accuracy层。
# 导入网络运行的solver文件
solver = caffe.SGDSolver(osp.join(workdir, 'solver.prototxt'))
# 导入权重文件
solver.net.copy_from(r'D:\caffe-master\models\bvlc_reference_caffenet\bvlc_reference_caffenet.caffemodel')
solver.test_nets[0].share_with(solver.net)
# 网络运行1次
solver.step(1)
#用于检测导入的数据是否正确
#提取batch中的第一幅图像,并显示图像与其对应的标签
image_index = 0
plt.figure()
plt.imshow(transformer.deprocess(copy(solver.net.blobs['data'].data[image_index, ...])))
gtlist = solver.net.blobs['label'].data[image_index, ...].astype(np.int)
plt.title('GT: {}'.format(classes[np.where(gtlist)]))
plt.axis('off');
gtlist = solver.net.blobs['label'].data[image_index, ...].astype(np.int)
这句代码的意思是提取网络data层所输出一个batch中的第一幅图像的label数据。
这段代码运行结果如下所示:
由于Caffe不提供多标签的accuracy层,因此需要自己写一个判断准确率的函数。这里不对这个准去率判别函数进行推导了,个人觉得,没有准确率的话,可以看Loos值啊,只要Loss值一直在下降就可以了。感兴趣的可以自己推一下这个国外老哥写的准确率函数的原理。
#测试准确率
#计算预测数据与真实数据相同的个数,占总个数的比例
def hamming_distance(gt, est):
return sum([1 for (g, e) in zip(gt, est) if g == e]) / float(len(gt))
#这里的batch_size就是设定验证网络的batch_size
def check_accuracy(net, num_batches, batch_size = 128):
acc = 0.0
for t in range(num_batches):
net.forward()
gts = net.blobs['label'].data
ests = net.blobs['score'].data > 0
for gt, est in zip(gts, ests):
acc += hamming_distance(gt, est)
return acc / (num_batches * batch_size)
for itt in range(6):
solver.step(100)
# 这里计算50个batch的平均准确率
print 'itt:{:3d}'.format((itt + 1) * 100), 'accuracy:{0:.4f}'.format(check_accuracy(solver.test_nets[0], 50))
#检测基本准确率
#这里的batch_size就是设定验证网络的batch_size
def check_baseline_accuracy(net, num_batches, batch_size = 128):
acc = 0.0
for t in range(num_batches):
net.forward()
gts = net.blobs['label'].data
ests = np.zeros((batch_size, len(gts)))
for gt, est in zip(gts, ests):
acc += hamming_distance(gt, est)
return acc / (num_batches * batch_size)
#个人认为这里的5823是指验证集内所包含的图像数
print 'Baseline accuracy:{0:.4f}'.format(check_baseline_accuracy(solver.test_nets[0], 5823/128))
#测试训练的网络
#取5幅图像测试一下训练网络的结果
test_net = solver.test_nets[0]
for image_index in range(5):
plt.figure()
plt.imshow(transformer.deprocess(copy(test_net.blobs['data'].data[image_index, ...])))
gtlist = test_net.blobs['label'].data[image_index, ...].astype(np.int)
#将最后一层输出数值大于0的标签定义为图像的标签
estlist = test_net.blobs['score'].data[image_index, ...] > 0
plt.title('GT: {} \n EST: {}'.format(classes[np.where(gtlist)], classes[np.where(estlist)]))
plt.axis('off')
plt.show()
代码运行结果如下所示下(GT:标准答案,EST:模型预测答案):
使用python的data层与SigmoidCrossEntropyLoss损失函数可以实现图像多标签分类,主要是这种方法十分简单。网络模型只迭代训练了600次,因此有可能网络精调的还不充分。大部分图像还是可以预测出所包含的标签的,还是有少数图像无法预测出标签,因为代码最后显示5幅图像的预测标签是选取分类分数大于0的位置作为预测标签,这里的score层输出没有经过Softmax处理,所以才会有的图像未显示出标签。通常情况下多分类标签结果都是取经过Softmax处理后的最后一层(本文是score层)输出结果的Top-N作为图像的类别。
这是在学习过程中的一点个人见解,如果理解有误,恳请批评指正。
下一篇博客里,会介绍如何实现自己的一个多标签图像数据集的多分类训练,看过tools.py与pascal_multilabel_datalayers.py的读者,只需为目标数据集仿制tools.py与xxxx_multilabel_datalayers.py即可实现基于Caffe的多标签图像分类。