原文来源:http://closure11.com/caffe%E5%9C%A8python%E4%B8%AD%E4%BD%BF%E7%94%A8%E5%86%85%E5%AD%98%E6%95%B0%E6%8D%AEmemorydata%E8%BF%9B%E8%A1%8C%E8%AE%AD%E7%BB%83/
最近在一个新的数据集(CompCar)上需要做很多层次的分类任务,做baseline时不可避免的会用到caffe,最开始时是用官网教程的方式先生成带标签的list列表,然后转成lmdb,最后caffe train --solver=xxx --weights=xxx这样进行训练,然而。。。实验做着做着硬盘就满了。。。原因一是因为数据集很大,二是在不同分类任务里要把图片列表分配成不同的标签,三是训练集测试集的split也有很多种,四是我还要把原始图片生成LMDB,整个过程算起来原始数据集已经增大了好几倍,后来想到了每次训练时不使用lmdb形式的数据,而改用内存数据,即手动读入图片列表中所有图片,然后做train、test的split,再分配label,这样只需在硬盘中保存最原始的图片文件和几个list列表文件即可,不过到网上搜了一下,关于Caffe的MemoryDataLayer、Python接口进行训练的资料居然非常少,也没有直观的例子。。。最后跟了一下caffe的各种PR加上fast-rcnn的代码,总算大概搞了明白,在此记录一下,代码可以参看https://github.com/nicklhy/CompCar_Analysis,仍在更新中(好吧,其实没多少代码)。
一般做训练的话就是直接使用SGDSolver这个类了,它本身由C++实现的,是底层Net类的进一步封装,但是在Python接口中已经完美导出,可以很方便的使用。官方介绍非常简单“Optimizes the parameters of a Net using stochastic gradient descent (SGD) with momentum”,实际用也挺方便的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import
#
solver
=
#
if
is
None
:
solver.net.copy_from(pretrained_model)
while
:
# 准备数据(如果模型定义的是MemoryData)
solver.net.set_input_arrays(train_X, train_Y)
# 迭代
solver.step(
1
)
|
但是实际在写代码时有一些关键点需要注意:
- caffe.set_device一定要放在最前面,不要等Net或者SGDSolver变量都定义完了以后在执行。。。(悲伤的debug回忆)
- 如果模型定义时有区分training和validation的不同phase,那么在solver中实际上是存在两个表示网络的成员变量:solver.net和solver.test_nets,注意,前者直接就是一个Net的对象,而后者是Net对象的列表,如果像GoogleNet那样,存在一个training和一个testing(validation而不是真正的testing,做测试的文件其实是deploy.prototxt),那么应该通过solver.test_nets[0]来引用这个测试网络;另外,测试网络和训练网络应该是共享中间的特征网络层权重,只有那些标出include { phase: TRAIN }或者include { phase: TEST }的网络层有区分;
- 训练数据train_X, train_Y必须是numpy中的float32浮点矩阵,train_X维度是sample_num*channels*height*width,train_Y是sample_num维度的label向量,这里sample_num必须是trainning输入batch_size的整数倍,为了方便,我在实际使用时每次迭代只在整个训练集中随机选取一个batch_size的图片数据放进去;
- solver.step(1)即迭代一次,包括了forward和backward,solver.iter标识了当前的迭代次数;
- 如果在solver.prototxt中设置了test_interval,那么在solver.test_interval的整数倍迭代时需要做一次完整的测试,此时设置数据应该是
1
solver.test_nets[
0
].set_input_arrays(test_X, test_Y)
之后计算loss或者准确率时也不能使用solver.step函数,应为测试不需要backward过程
1solver.test_nets[
0
].forward()
这之后就可以访问solver.test_nets[0].blobs['blob_name'].data来获取loss或者准确率了。
-
solver.prototxt文件中的参数可以通过google.protobuf这个模块实现
123456import
caffeimport
google.protobuf as pb2solver_param
=
caffe.proto.caffe_pb2.SolverParameter()with
open
(solver_prototxt,
'rt'
) as fd:
pb2.text_format.Merge(fd.read(), solver_param)
其中solver_param首先从caffe.proto.caffe_pb2.SolverParameter()获取默认参数,然后接下来从solver.prototxt文件里获取实际定义的参数,之后在引用时就可以很方便的看到诸如学习率、测试周期等变量了
-
在通过Python接口设置MemoryData时有时候会遇到一个奇怪的问题
1Segmentation
faults and Check failed: status == CUBLAS_STATUS_SUCCESS (14 vs. 0) CUBLAS_STATUS_INTERNAL_ERROR.这个在Google了之后看到了https://github.com/BVLC/caffe/issues/2334这个帖子,题主说他已经解决了这个问题,通过在数据传输时使用deep copy,并且提交了一个pr,测试了一下居然真的可以,但奇怪的是这个request还没有合并到master分支里。。。不知道是不是因为Python的这个接口用的人少还是什么情况。
-
需要保存模型时
1solver.net.save(model_path)
此时会生成一个caffemodel和一个solverstate文件。
学习率及更新方式等参数只要在solver.prototxt中已经设置,不需要在代码中手动设置,比如我在solver.prototxt里设置了base_lr是0.005,lr_policy是"step",stepsize是6000,gama是0.7,那么只要你不停的step,在6000次以后学习率就会自动变成0.005*0.7。
-
如果在solver.prototxt文件里有average_loss这个参数(比如GoogleNet),也就是说要取多次forward操作的loss做平均,那么在set_input_arrays时记得输入average_loss*training_batch_size这么多的训练数据,然后调用solver.step(average_loss),如果仍然是每次step一次,那么average_loss参数就会失效,相当于没有起作用,具体实现可以在Caffe的源代码里找到,主要是memory_data_layer.cpp文件里:
123456789101112template
<typename
Dtype>void
MemoryDataLayer<Dtype>::Forward_cpu(const
vector<Blob<Dtype>*>& bottom,¦
const
vector<Blob<Dtype>*>& top) {CHECK(data_) <<
"MemoryDataLayer needs to be initalized by calling Reset"
;
top[0]->Reshape(batch_size_, channels_, height_, width_);
top[1]->Reshape(batch_size_, 1, 1, 1);
top[0]->set_cpu_data(data_ + pos_ * size_);
top[1]->set_cpu_data(labels_ + pos_);
pos_ = (pos_ + batch_size_) % n_;
if
(pos_ == 0)has_new_data_ =
false
;
}
如果之前通过Reset函数设置的data shape是多个batch_size,那么每次Step时就会调用到pos_ = (pos_+batch_size_)%n,不断feed下一个batch_size的数据。
完整代码见https://github.com/nicklhy/CompCar_Analysis中的src/caffe/train.py文件。