本文主要参考caffe官方文档[《Fine-tuning a Pretrained Network for Style Recognition》](http://nbviewer.jupyter.org/github/BVLC/caffe/blob/master/examples/02-fine-tuning.ipynb)
是第二篇案例。笔者对其进行了为期一周的断断续续的研究,笔者起先对python/caffe并不了解+英语不好,阅读+理解的时间有点长,前前后后过了不下十遍终于从这第二篇文档看出些端倪来了。
本系列主要是以python编译实现为主,不涉及caffe命令行训练。
本系列纯粹笔者自己领悟,如有问题,非常,非常欢迎看客指出,感激~
.
.
一、fine-tuning模型的三种状态
从阅读《Fine-tuning a Pretrained Network for Style Recognition》这篇文章来看,主要把fine-tuning的三种状态都简述了一遍,该教程可谓足够良心:
- 状态一:只预测,不训练。
相对快、简单,针对那些已经训练好,现在要实际对未知数据进行标注的项目,其中用DummyData来做模具,非常高效;
- 状态二:训练,但只训练最后分类层。
针对一类,fine-tuning的模型最终的分类以及符合要求,现在只是在他们的基础上进行类别降维。
- 状态三:完全训练,分类层+之前卷积层都训练
跟状态二的差异很小,当然状态三比较耗时+需要训练GPU资源,不过非常适合fine-tuning到自己想要的模型里面,预测精度相比状态二也提高不少。
一般来看,在分类任务里面,imagenet的一系列模型或者更好的google inception V3模型,他们已经分别有900W、6000W的数量和1000、6000分类,足够应付很多识别任务的时候,可以采取状态二、三的训练模式。fine-tuning好了之后就可以采用状态一,只进行相对应的预测。
一些成品框架整理:
imagenet的一系列模型:https://github.com/BVLC/caffe/tree/master/models google
inception V3模型caffe代码(未训练):Google Inception V3 for
Caffe
google inception V3数据:https://github.com/openimages/dataset# google
inception
V3&V4框架叙述:GoogleNet的Inception_v1、Inception_v2、Inception_v3、Inception_v4(整理)
.
.
二、函数介绍
《Fine-tuning a Pretrained Network for Style Recognition》在本篇官方文档中,主要应用的是caffeNet这一套框架。
1、caffe训练文件种类
caffe在训练时候会有以下几类训练必须文件:
- deploy.prototxt:框架文件,用在预测+训练场景,caffenet函数生成
- solver.prototxt:参数文件,用在训练场景,solver函数生成
- train_val.prototxt:数据集文件,用在训练场景
- x.caffemodel:模型权值参数文件
以上的一些函数,会在后续应用中由下面的函数生成。
.
2、函数介绍一:caffenet函数
caffenet函数主要作用就是返回、制造caffeNet框架训练时候所需的框架文件:deploy.prototxt
输入:数据、标签、层名、学习率设置
输出:deploy文件
caffenet(data, label=None, train=True, num_classes=1000,classifier_name='fc8', learn_all=False)
# data代表训练数据
# label代表新数据标签,跟num_classes类似,跟给出数据的标签数量一致,譬如新数据的标签有5类,那么就是5
# train,是否开始训练,默认为true
# num_classes代表fine-tuning来得模型的标签数,如果fine-tuning是ImageNet是千分类,那么num_classes=1000
# classifier_name,最后的全连接层名字,如果是fine-tuning需要重新训练的话,则需要修改最后的全连接层
# learn_all,这个变量用于将学习率设置为0,在caffenet中,如果learn_all=False,则使用frozen_param设置网络层的学习率,即学习率为0
其中label和num_classes比较容易混淆,label是这次训练时候要使用的标签数量。num_classes代表要微调的模型原来的标签数量。
learn_all状态二与状态三主要区别,决定着是否训练卷积层。
一般只是设置train参数较多:caffenet(train=FLASE)
.
3、函数介绍二:style_net 风格函数
该函数封装了上面的函数一caffenet,返回的是具有style_net特色的框架训练文件deploy.prototxt
同时该函数也是《Fine-tuning a Pretrained Network for Style Recognition》中唯一的数据输入的地方。
而且还使用了ImageData层,这一函数笔者看了很久。
输入:学习率设置、子集设置、训练设置
输出:deploy文件
def style_net(train=True, learn_all=False, subset=None):
if subset is None:
subset = 'train' if train else 'test'
source = caffe_root + 'data/flickr_style/%s.txt' % subset
transform_param = dict(mirror=train, crop_size=227,
mean_file=caffe_root + 'data/ilsvrc12/imagenet_mean.binaryproto')
style_data, style_label = L.ImageData(
transform_param=transform_param, source=source,
batch_size=50, new_height=256, new_width=256, ntop=2)
return caffenet(data=style_data, label=style_label, train=train,
num_classes=NUM_STYLE_LABELS,
classifier_name='fc8_flickr',
learn_all=learn_all)
注意点:train和subset之间的冲突。subset在这的权限就是决定是用验证集还是测试集,当然其实这一功能train参数也可以实现,subset在这是为了更灵活操作函数使用。train在这功能还挺多:
- train=TRUE,如果在不设置subset情况下则是代表使用训练集数据。同时框架文件deploy的训练是打开的,数据输入的时候,mirror功能也是打开着的。
- train=FALSE,代表使用验证集,框架文件deploy不训练,只预测。
- 其中还有一个ImageData层,作为数据输入层,在整个文档中,是唯一数据输入入口,source是数据的来源。会在其他博客详细说明ImageData(caffe︱ImageData层、DummyData层作为原始数据导入的应用)。
- 该函数需要在data/flickr_style/**.txt有训练集+测试集的txt文件
- 需要有数据集的mean文件,此时应用的是caffeNet的mean文件
- 重置了最后的全连接层fc8_flickr
.
4、函数介绍三:caffe.Net预测框引擎(网络生成)
输入:deploy框架文件、模型weight权值
输出:caffe引擎文件,用于后续
caffe.Net(imagenet_net_filename, weights, caffe.TEST)
# caffe.Net(deploy文件,模型权值,caffe.TEST)
imagenet_net_filename是deploy文件,是整个框架的文件,也是caffenet/style_net得到的文件;
专门用于整个网络的生成
.
5、函数介绍四:预测函数disp_preds
输入:引擎文件(函数三)、新预测图片image,标签labels,K代表预测top多少、name便于输出
输出:返回print,top的准确率+分类标签,后期要改成return
def disp_preds(net, image, labels, k=5, name='ImageNet'):
input_blob = net.blobs['data']
net.blobs['data'].data[0, ...] = image
probs = net.forward(start='conv1')['probs'][0]
top_k = (-probs).argsort()[:k]
print 'top %d predicted %s labels =' % (k, name)
print '\n'.join('\t(%d) %5.2f%% %s' % (i+1, 100*probs[p], labels[p])
for i, p in enumerate(top_k))
预测函数disp_preds,通过函数三得到的引擎,进行net.forward前馈,通过net.forward(start=’conv1’)[‘probs’][0]获得所有标签的概率,然后通过排序输出top5
其中,labels如果是在验证集上就没有,那么可以不填的。
那么在之上作者封装了用于风格识别的函数:
def disp_imagenet_preds(net, image):
disp_preds(net, image, imagenet_labels, name='ImageNet')
def disp_style_preds(net, image):
disp_preds(net, image, style_labels, name='style')
.
6、函数介绍五:solver函数
输入:框架文件,(一般是caffenet/style_net之后)、学习率base_lr
输出:参数文件solver
def solver(train_net_path, test_net_path=None, base_lr=0.001)
# train_net_path训练框架文件,一般是caffenet函数之后的deploy文件内容
# test_net_path
# base_lr,学习率
.
7、函数介绍六:run_solvers训练函数
输入:迭代次数(niter)、参数文件solvers(函数五)、迭代间隔(disp_interval)
输出:loss/acc/weights值
def run_solvers(niter, solvers, disp_interval=10):
"""Run solvers for niter iterations,
returning the loss and accuracy recorded each iteration.
`solvers` is a list of (name, solver) tuples."""
blobs = ('loss', 'acc')
loss, acc = ({name: np.zeros(niter) for name, _ in solvers}
for _ in blobs)
for it in range(niter):
for name, s in solvers:
s.step(1) # run a single SGD step in Caffe
loss[name][it], acc[name][it] = (s.net.blobs[b].data.copy()
for b in blobs)
if it % disp_interval == 0 or it + 1 == niter:
loss_disp = '; '.join('%s: loss=%.3f, acc=%2d%%' %
(n, loss[n][it], np.round(100*acc[n][it]))
for n, _ in solvers)
print '%3d) %s' % (it, loss_disp)
# Save the learned weights from both nets.
weight_dir = tempfile.mkdtemp()
weights = {}
for name, s in solvers:
filename = 'weights.%s.caffemodel' % name
weights[name] = os.path.join(weight_dir, filename)
s.net.save(weights[name])
return loss, acc, weights
# niter,迭代次数
# solvers,solver参数文件
# disp_interval,
.
8、函数介绍七:eval_style_net
输入:模型权值(run_solver之后)、迭代间隔10
输出:caffe.Net引擎、总体精确度
def eval_style_net(weights, test_iters=10):
test_net = caffe.Net(style_net(train=False), weights, caffe.TEST)
accuracy = 0
for it in xrange(test_iters):
accuracy += test_net.forward()['acc']
accuracy /= test_iters
return test_net, accuracy
在文档中,该函数主要用于对比精准度+生成预测引擎
.
.
三、三重天的状态简述
前篇也有提到模型fine-tuning的时候,官方文档《Fine-tuning a Pretrained Network for Style Recognition》中的三种微调状态:
- 状态一:只预测,不训练
- 状态二:训练,但只训练最后分类层
- 状态三:完全训练,分类层+之前卷积层都训练
当然,状态一+状态三的组合最佳,从官方文档的实验来看,状态三比状态二仅仅只训练最后的一层精度提高不少。
一个项目,拿到新的图像,进行一部分的标注,利用状态三进行训练,训练完的模型灌给状态一进行新图片的预测。
那么接下来会简单以函数串联的形式来复盘一下:
.
1、状态一:纯预测,不训练
应用场景:单独预测阶段,模型已经训练完毕。借用DummyData层来较快完成预测任务
- 1、利用DummyData设置备选图框,给新来的图像留个坑,L.DummyData函数
dummy_data=L.DummyData(shape=dict(dim=[1, 3, 227, 227]))
不同框架size不一致,要根据框架来定,caffenet在这里就是227
- 2、构建框架文件deploy.prototxt,用caffenet函数
net_filename=caffenet(data=dummy_data, train=False)
- 3、设置预测引擎,caffe.Net函数
weights = caffe_root +'models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel'
assert os.path.exists(weights)
net=caffe.Net(net_filename, weights, caffe.TEST)
其中需要加载框架权值,以及上面的deploy文件
- 4、单张预测,disp_imagenet_preds函数
disp_imagenet_preds(net, image)
image为新图片信息,net为前面的网络引擎net
其中image是图像的信息
改进的方向:数据输入的改进,可以通过imageData每个batch输入,并进行预测
.
2、状态二:训练,只训练最后的全连接层
应用场景:当数据基本类似,那么全连接层之前就不变,学习率不变; 最后的全连接层进行一次训练。
- 1、★生成框架文件deploy.prototxt:
style_net(train=True)
Learn_all 没打开,所以只能训练全连接层。状态二与状态三唯一的区别就在这,如果learn_all=False,则使用frozen_param设置网络层的学习率,即学习率为0
- 2、生成参数文件solver.prototxt ,solver函数
style_solver_filename =solver(style_net(train=True))
只输入框架文件,然后根据框架文件生成了solver参数文件,其中也附带了数据导入,那么如果是自己fine-tuning就需要进行修改。
- 3、生成训练引擎get_solver
style_solver = caffe.get_solver(style_solver_filename)
输入的是上面生成的参数文件。输出的是训练引擎
- 4、加载模型权值copy_from,.net.copy_from
weights = caffe_root +'models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel'
assert os.path.exists(weights)
style_solver.net.copy_from(weights)
这一步是加载已经训练好的权值,一般第一次是fine-tuning来的.caffemodel权值,那么如果中途停掉在启动,那么启动的权值就是上次训练到一半的权值参数,可以继续训练下去。
- 5、训练run_solvers
solvers = [('pretrained', style_solver)]
niter=200
loss, acc, weights =run_solvers(niter, solvers)
这里只迭代200次,返回整个训练重要的三项内容:loss/acc/weights
- 6、返回预测引擎+训练准确率eval_style_net
style_weights=weights['pretrained']
test_net, accuracy = eval_style_net(style_weights)
返回预测引擎caffeNet+整个训练的准确率
- 7、预测函数disp_style_preds
disp_style_preds(test_net, image)
输入的是,上面生成的训练引擎+新图像的特征信息,文档中是用imageData层来作为前期图像数据信息提取的方式,那么也可以自己用其他的方式来transformer
然后返回print,top5的准确率+分类标签。
- 8、图片展示函数deprocess_net_image
plt.imshow(deprocess_net_image(image))
deprocess_net_image为图像转化函数,可以显示图像。
.
3、状态三:end-to-end 完全训练
**应用场景:**fine-tuning整个框架,精度来说,比状态二要好不少,而且具有自己的特色,这一状态是fine-tuning主要使用的状态。
- 1、★生成框架文件deploy.prototxt:
end_to_end_net=style_net(train=True, learn_all=True)
Learn_all 打开,所以训练卷积层+全连接层。状态二与状态三的区别就在这。learn_all参数默认值为False,当其为False时,意味着预训练的层(conv1到fc7)的lr_mult=0,我们仅仅学习了最后一层。
- 2、★生成参数文件solver.prototxt ,solver函数
style_solver_filename =solver(end_to_end_net,base_lr=base_lr)
只输入框架文件,然后根据框架文件生成了solver参数文件,其中也附带了数据导入,那么如果是自己fine-tuning就需要进行修改。
状态三,这里还要设置学习率base_lr为0.001,因为是fine-tuning,越小越好。
- 3、生成训练引擎get_solver
style_solver = caffe.get_solver(style_solver_filename)
输入的是上面生成的参数文件。输出的是训练引擎
- 4、加载模型权值copy_from,.net.copy_from
weights = caffe_root +'models/bvlc_reference_caffenet/bvlc_reference_caffenet.caffemodel'
assert os.path.exists(weights)
style_solver.net.copy_from(weights)
这一步是加载已经训练好的权值,一般第一次是fine-tuning来的.caffemodel权值,那么如果中途停掉在启动,那么启动的权值就是上次训练到一半的权值参数,可以继续训练下去。
- 5、训练run_solvers
solvers = [('pretrained', style_solver)]
niter=200
loss, acc, weights =run_solvers(niter, solvers)
这里只迭代200次,返回整个训练重要的三项内容:loss/acc/weights
- 6、返回预测引擎+训练准确率eval_style_net
style_weights=weights['pretrained']
test_net, accuracy = eval_style_net(style_weights)
输入的是已经训练好的weight,返回预测引擎caffeNet+整个训练的准确率
- 7、预测函数disp_style_preds
disp_style_preds(test_net, image)
输入的是,上面生成的训练引擎+新图像的特征信息,文档中是用imageData层来作为前期图像数据信息提取的方式,那么也可以自己用其他的方式来transformer
然后返回print,top5的准确率+分类标签。
- 8、图片展示函数deprocess_net_image
plt.imshow(deprocess_net_image(image))
deprocess_net_image为图像转化函数,可以显示图像。
.
.
延伸一:状态一,直接更改了全连接层可以直接使用吗?
同时,对于状态一,官方文档中也是进行了预测,如果在没有训练卷积层+全连接层的情况下,强行更改会出现以下的情况——所有的预测分类都是均等的:
untrained_style_net = caffe.Net(style_net(train=False, subset='train'), weights, caffe.TEST)
disp_style_preds(untrained_style_net, image)
top 5 predicted style labels =
(1) 20.00% Detailed
(2) 20.00% Pastel
(3) 20.00% Melancholy
(4) 20.00% Noir
(5) 20.00% HDR
untrained_style_net中,train=False代表关闭训练引擎(solver/caffe.get_solver/run_solver),subset=’train’代表使用训练集。
因为我们的分类器初始化为0(观察caffenet,没有权值滤波其经过最后的内积层),softmax的输入应当全部为0,因此,我们看到每个标签的预测结果为1/N,这里n为5,因此所有的预测结果都为20%。(来源博客:caffe学习笔记10.1–Fine-tuning a Pretrained Network for Style Recognition(new))
所以,如果要修改最后的全连接层,那么至少需要状态二,不然输出不了你想要的结果。