原文链接 http://blog.csdn.net/cyh_24/article/details/51537709
刚学习的新手可以参考下该文章,后续更新自己的心得体会
caffe 参数介绍
solver.prototxt
net: "models/bvlc_alexnet/train_val.prototxt"
test_iter: 1000 #
test_interval: 1000 #
base_lr: 0.01 # 开始的学习率
lr_policy: "step" # 学习率的drop是以gamma在每一次迭代中
gamma: 0.1
stepsize: 100000 # 每stepsize的迭代降低学习率:乘以gamma
display: 20 # 没display次打印显示loss
max_iter: 450000 # train 最大迭代max_iter
momentum: 0.9 #
weight_decay: 0.0005 #
snapshot: 10000 # 没迭代snapshot次,保存一次快照
snapshot_prefix: "models/bvlc_reference_caffenet/caffenet_train"
solver_mode: GPU # 使用的模式是GPU
-
test_iter
在测试的时候,需要迭代的次数,即test_iter* batchsize(测试集的)=测试集的大小,测试集的 batchsize可以在prototx文件里设置。 -
test_interval
训练的时候,每迭代test_interval次就进行一次测试。 -
momentum
灵感来自于牛顿第一定律,基本思路是为寻优加入了“惯性”的影响,这样一来,当误差曲面中存在平坦区的时候,SGD可以更快的速度学习。wi←m∗wi−η∂E∂wi
train_val.prototxt
layer { # 数据层
name: "data"
type: "Data"
top: "data"
top: "label"
include {
phase: TRAIN # 表明这是在训练阶段才包括进去
}
transform_param { # 对数据进行预处理
mirror: true # 是否做镜像
crop_size: 227
# 减去均值文件
mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
}
data_param { # 设定数据的来源
source: "examples/imagenet/ilsvrc12_train_lmdb"
batch_size: 256
backend: LMDB
}
}
layer {
name: "data"
type: "Data"
top: "data"
top: "label"
include {
phase: TEST # 测试阶段
}
transform_param {
mirror: false # 是否做镜像
crop_size: 227
# 减去均值文件
mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
}
data_param {
source: "examples/imagenet/ilsvrc12_val_lmdb"
batch_size: 50
backend: LMDB
}
}
-
lr_mult
学习率,但是最终的学习率需要乘以 solver.prototxt 配置文件中的 base_lr .如果有两个 lr_mult, 则第一个表示 weight 的学习率,第二个表示 bias 的学习率
一般 bias 的学习率是 weight 学习率的2倍’ -
decay_mult
权值衰减,为了避免模型的over-fitting,需要对cost function加入规范项。wi←wi−η∂E∂wi−ηλwi
-
num_output
卷积核(filter)的个数 -
kernel_size
卷积核的大小。如果卷积核的长和宽不等,需要用 kernel_h 和 kernel_w 分别设定
-
stride
卷积核的步长,默认为1。也可以用stride_h和stride_w来设置。 -
pad
扩充边缘,默认为0,不扩充。扩充的时候是左右、上下对称的,比如卷积核的大小为5*5,那么pad设置为2,则四个边缘都扩充2个像素,即宽度和高度都扩充了4个像素,这样卷积运算之后的特征图就不会变小。
也可以通过pad_h和pad_w来分别设定。 -
weight_filler
权值初始化。 默认为“constant”,值全为0.
很多时候我们用”xavier”算法来进行初始化,也可以设置为”gaussian”
weight_filler {
type: "gaussian"
std: 0.01
}
- bias_filler
偏置项的初始化。一般设置为”constant”, 值全为0。
bias_filler {
type: "constant"
value: 0
}
-
bias_term
是否开启偏置项,默认为true, 开启
-
group
分组,默认为1组。如果大于1,我们限制卷积的连接操作在一个子集内。
卷积分组可以减少网络的参数,至于是否还有其他的作用就不清楚了。每个input是需要和每一个kernel都进行连接的,但是由于分组的原因其只是与部分的kernel进行连接的
如: 我们根据图像的通道来分组,那么第i个输出分组只能与第i个输入分组进行连接。 -
pool
池化方法,默认为MAX。目前可用的方法有 MAX, AVE, 或 STOCHASTIC -
dropout_ratio
丢弃数据的概率
在数据集中镜像处理 mirror
参考博客 https://www.baidu.com/link?url=5nPLnW_lY_YV8XONuSyX2kBi-KrRT1rswWBtwlCTeFw2Gg30E5eWYrTpn4NwoHi4E_Ae5CFNw-WJQsj3FaI7g_&wd=&eqid=b2145bbb000094f10000000358d9ccad
IO(二)中,我们已经将原始数据缓冲至Datum,Datum又存入了生产者缓冲区,不过,这离消费,还早得很呢。
在消费(使用)之前,最重要的一步,就是数据变形。
ImageNet
ImageNet提供的数据相当Raw,不仅图像尺寸不一,ROI焦点内容比例也不一,如图:
[Krizhevsky12]给出了CNN打ImageNet的基本预处理,非常经典的" Random 256-224 Crop",即:
首先,对图片进行统一的缩放,无视宽高比,统一缩放成256*256(可利用OpenCV)
(注:保留宽高比是没有意义的,CNN的滑动卷积本身就会破坏宽高比,见Faster-RCNN的RPN设计原理)
预先计算好256*256图像的均值,在硬盘上存储为均值文件。之后,分为训练阶段和测试阶段。
【训练阶段】:
对256*256的图片,只选择224*224的crop区域,目的是做Data Augmentation。
crop方式很特殊,采用的是随机crop。由于256-224=32,宽高轴上各有32单元的平移空间。
于是在训练时,每次Rand(0,32),宽高轴一共就有32*32种crop结果,达到了数据增幅效果。
同时,还要对crop结果,做一次镜像,这样就有2*32*32=2048倍的增幅数据了。
【测试阶段】:
对256*256的图片,将224*224的crop区域分别定位在4角和图片中心,加上镜像,共计10种结果。
累加Softmax的prob,做平均,得到最终prob,最后再作出prediction。
均值标准化
作为经典的通用数据预处理手段,均值标准化相当廉价,效果不俗。
默认有俩种均值标准化:逐像素(精)、逐通道(糙)。
Caffe中对逐像素均值数据进行的是外挂存储,和图像数据是分开的,这样的存储相当灵活。
代价就是,对每一张图要进行减均值操作,在GPU模式中,CPU的这点计算量其实没什么。
对于逐通道均值,直接在proto文本中,作为参数指定。
数值缩放
[Krizhevsky12] 中,使用更灵活的Gaussian初始化,网络首层参数初始化的标准差缩小100倍(0.0001)
以此免除了传统意义上的数值缩放。
如果你需要使用Xavier初始化,仍然需要校正输入范围至[-1,1]。
[0,256]范围需要乘以1/256=0.00390625的缩放因子。
[-128,128]范围(做了均值标准化)需要乘以1/128=0.0078125的缩放因子。
镜像
可以OpenCV做。因为镜像不涉及插值,也可以人工逆转坐标完成。
数据结构
(注:Transformer中含有大量OpenCV函数的使用,以下将精简掉所有OpenCV功能,请读者按需自行补充)
在proto文件中,补上TransformationParameter 。
message TransformationParameter{
optional float scale=1 [default=1.0];
optional bool mirror=2 [default=false];
optional uint32 crop_size=3 [default=0];
optional string mean_file=4;
repeated float mean_value=5;
optional bool force_color=6 [default=false];
optional bool force_gray=7 [default=false];
}
在LayerParameter,补上:
optional TransformationParameter transform_param=XX;
Transformer将作为DataLayer的成员变量,接受LayerParameter传进来的transform_param进行构造。
建立data_transformer.hpp
template <typename Dtype>
class DataTransformer
{
public:
DataTransformer(const TransformationParameter& param, Phase phase);
vector<int> inferBlobShape(const Datum& datum);
void transform(const Datum& datum, Blob<Dtype>* shadow_blob);
void transform(const Datum& datum, Dtype* shadow_data);
void initRand();
~DataTransformer() {}
int rand(int n);
private:
TransformationParameter param;
Phase phase;
Blob<Dtype> mean_blob;
vector<Dtype> mean_vals;
boost::shared_ptr<Dragon::RNG> ptr_rng;
};
inferBlobShape、transfrom都是外调成员函数,将被DataLayer使用。
分别用于根据数据推测DataLayer的Blob大小、以及对数据变形。
initRand将构造梅森发生器ptr_rng,rand用于Random-Crop。
根据均值标准化的不同,mean_blob存储逐像素均值,mean_val则是简单的逐通道均值。
Protocol Buffer的文件IO封装
反序列化以二进制存储的均值文件,需要操作Protocol Buffer的底层文件系统API,为了便于调用,做一个Wrapper。
建立io.hpp。
#include <fcntl.h>
#include <unistd.h>
#include <google/protobuf/message.h>
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/text_format.h>
inline bool readProtoFromBinaryFile(const char* filename, Message* proto){
// get OS kernel‘s file descriptor(fd)
// successful range: [0,OPEN_MAX]
// replace open(filename, O_RDONLY) as open(filename, O_RDONLY | O_BINARY)
int fd = open(filename, O_RDONLY | O_BINARY);
CHECK_NE(fd, -1) << "File not found: " << filename;
ZeroCopyInputStream *raw_input = new FileInputStream(fd);
CodedInputStream *coded_input = new CodedInputStream(raw_input);
coded_input->SetTotalBytesLimit(INT_MAX, 536870912); // 0..512M..2G
bool success = proto->ParseFromCodedStream(coded_input);
delete raw_input;
delete coded_input;
close(fd);
return success;
}
值得在意的是OS提供的API函数open,返回的是fd(file descriptor),这和OS的文件系统有关。
Linux的open函数,默认是以O_RDONLY打开的,而Windows则不是。
因此,移植Linux版Caffe的第一步就是追加O_RDONLY这个Flag。
ZeroCopyInputStream相比于PB提供的InputStream,速度要更快。
CodedInputStream为了解除二进制的编码,SetTotalBytesLimit两参数分别是文件大小上界和警告阈值(2G/512M)。
最后,将二进制编码数据,反序列化成为Message结构。
实现
建立data_transformer.cpp
template <typename Dtype>
DataTransformer<Dtype>::DataTransformer(const TransformationParameter& param, Phase phase):
param(param), phase(phase)
{
// normally, we get mean_value from mean_file
if (param.has_mean_file()){
CHECK_EQ(param.mean_value_size(), 0)
<< "System wants to use mean_file but specified mean_value.";
const string& mean_file = param.mean_file();
LOG(INFO) << "Loading mean file from: " << mean_file;
BlobProto proto;
readProtoFromBinaryFileOrDie(mean_file.c_str(), &proto);
mean_blob.FromProto(proto);
}
// using each channel's mean value
// mean_value_size() is between 1 and 3
if (param.mean_value_size()>0){
CHECK(param.has_mean_file() == false)
<< "System wants to use mean_value but specified mean_file.";
for (int i = 0; i < param.mean_value_size(); i++)
mean_vals.push_back(param.mean_value(i));
}
initRand();
}
构造函数中,主要做两件事:
①恢复均值数据,逐像素从文件读,逐通道从指定的proto参数里读。
逐通道参数指定方法:
layer {
.........
transform_param {
mean_val: 102
mean_val: 107
mean_val: 112
.........
}
}
proto的repeated类型,可以通过相同的名字,连续指定。
②初始化梅森发生器。
均值数据的序列化,是放在BlobProto里的,反序列会成为BlobProto。
关于如何存储均值,见:https://github.com/neopenx/Dragon/blob/master/Dragon/compute_mean.cpp
template<typename Dtype>
vector<int> DataTransformer<Dtype>::inferBlobShape(const Datum& datum){
const int crop_size = param.crop_size();
const int channels = datum.channels();
const int height = datum.height();
const int width = datum.width();
CHECK_GT(channels, 0);
CHECK_GE(height, crop_size);
CHECK_GE(width,crop_size);
vector<int> shape(4);
shape[0] = 1; shape[1] = channels;
shape[2] = crop_size ? crop_size : height;
shape[3] = crop_size ? crop_size : width;
return shape;
}
InferBlobShape接受一个Datum,返回推测的shape,用于构建DataLayer中,Flow的Blob。
template<typename Dtype>
void DataTransformer<Dtype>::initRand(){
const bool must_rand = (phase == TRAIN && param.crop_size());
if (must_rand){
const unsigned int rng_seed = Dragon::get_random_value();
ptr_rng.reset(new Dragon::RNG(rng_seed));
}
}
梅森发生器的构建使用了主进程管理器的梅森发生器提供的一个随机数作为种子。
这步可以省略,使用进程相关的cluster_seedgen也是可以的。
template<typename Dtype>
int DataTransformer<Dtype>::rand(int n){
CHECK(ptr_rng);
CHECK_GT(n, 0);
rng_t* rng = ptr_rng->get_rng();
return (*rng)() % n;
}
32位的梅森发生器默认产生一个unsigned int32值,如果需要指定范围,需要做求余操作。
同时,注意Random-Crop不需要负随机值。
DataTransformer::transform()
上面是几种transform的核心操作,还是比较冗繁的。
首先从Datum获得输入数据尺寸,做Random-Crop。
在训练阶段,得到基于原图的两个偏移h_off,w_off。
在测试阶段,默认没有实现[Krizhevsky12]的10个测试区域多重预测,只提供单中心crop区域。
需要根据具体要求,重写这部分代码。比如GoogleNet就扩大到了144个测试区域,具体见[Szegedy14]
接着,逐通道、逐像素(crop之后的宽高):
data_idx由crop位置+偏移位置联合而成,代表原图的像素位置。
top_idx代表的是crop图的位置。
如果需要镜像(反转width轴),在计算top_idx的最后,用(width - 1 - w)替代w。
uint8这里需要特别注意:
string里的字符类型是char,而uint8是unsigned char,需要强制转换。
诸如MNIST、Cifar10这样的数据集,像素单元是以uint8存储的。
8Bit的顶位用于存储符号位,unit8范围是[0,255],int8范围是[-127,127]。
如果不转换,从char(string)中获取的值,顶位将用于符号,显然不能表达我们的像素要求。
最后,均值和缩放可以在一行完成。
template<typename Dtype>
void DataTransformer<Dtype>::transform(const Datum& datum, Blob<Dtype>* shadow_blob){
const int num = shadow_blob->num();
const int channels = shadow_blob->channels();
const int height = shadow_blob->height();
const int width = shadow_blob->width();
CHECK_EQ(channels, datum.channels());
CHECK_GE(num, 1);
CHECK_LE(height, datum.height()); //allowing crop
CHECK_LE(width, datum.width());
Dtype *base_data = shadow_blob->mutable_cpu_data();
transform(datum, base_data);
}
这个transform的重载函数是对Blob的封装。(可选)
完整代码
io.hpp
https://github.com/neopenx/Dragon/blob/master/Dragon/include/utils/io.hpp
data_transformer.hpp
https://github.com/neopenx/Dragon/blob/master/Dragon/include/data_transformer.hpp
data_transformer.cpp
https://github.com/neopenx/Dragon/blob/master/Dragon/src/data_transformer.cpp