caffe入门教程1——Caffe代码层次:Blob、Layer、Net、Solver

参考知乎:https://www.zhihu.com/question/27982282/answer/39350629
本博客是博主学习唐宇迪caffe课程做的笔记,算是caffe入门系列,以具体项目应用为目的。

1. 初识Caffe

1.1. Caffe相对与其他DL框架的优点和缺点:

优点:速度快。Google Protocol Buffer数据标准为Caffe提升了效率。学术论文采用此模型较多。不确定是不是最多,但接触到的不少论文都与Caffe有关(R-CNN,DSN,最近还有人用Caffe实现LSTM)
缺点:曾更新过重要函数接口。有人反映,偶尔会出现接口变换的情况,自己很久前写的代码可能过了一段时间就不能和新版本很好地兼容了。

1.2. Caffe代码层次

要读懂caffe,首先要熟悉Blob,Layer,Net,Solver这几个大类。这四个大类自下而上,环环相扣,贯穿了整个caffe的结构,下面先分别简单地介绍一下这四个类的主要作用。
Blob:作为数据传输的媒介,无论是网络权重参数,还是输入数据,都是转化为Blob数据结构来存储。
Layer:作为网络的基础单元,神经网络中层与层间的数据节点、前后传递都在该数据结构中被实现,层类种类丰富,比如常用的卷积层、全连接层、pooling层等等,大大地增加了网络的多样性
Net:作为网络的整体骨架,决定了网络中的层次数目以及各个层的类别等信息Solver:作为网络的求解策略,涉及到求解优化问题的策略选择以及参数确定方面,修改这个模块的话一般都会是研究DL的优化求解的方向。

1.3. Blob

1.3.1 Blob的类型描述

Caffe内部采用的数据类型主要是对protocol buffer所定义的数据结构的继承,因此可以在尽可能小的内存占用下获得很高的效率,虽然追求性能的同时Caffe也会牺牲了一些代码可读性。
直观来说,可以把Blob看成一个有4维的结构体(包含数据和梯度),而实际上,它们只是一维的指针而已,其4维结构通过shape属性得以计算出来。

1.3.2. Blob的重要成员函数和变量

shared_ptr data_ //数据
shared_ptr diff_ //梯度
void Blob::Reshape(const int num, const int channels, const int height, const int width)

重新修改Blob的形状(4维),并根据形状来申请动态内存存储数据和梯度。

inline int count(int start_axis, int end_axis) const

计算Blob所需要的基本数据单元的数量。

1.4. Layer

1.4.1. Layer的类型描述

Layer是网络模型和计算的核心,在数据存储上,主要分成bottom_vecs、top_vecs、weights&bias三个部分;在数据传递上,也主要分为LayerSetUp、Reshape、Forward、Backward四个过程,符合直观上对层与层之间连接的理解,贴切自然。

1.4.2. Layer的重要成员函数和变量

vector loss_ //每一层都会有一个loss值,但只有LossLayer才会产生非0的loss
vector<shared_ptr > > blobs_ //Layer所学习的参数,包括权值和偏差
virtual void LayerSetUp(const vector*>& bottom, const vector*>& top) 

通过bottom Blob对象的形状以及LayerParameter(从prototxt读入)来确定Layer的学习参数(以Blob类型存储)的形状。

virtual void Reshape(const vector*>& bottom, const vector*>& top) 

通过bottom Blob对象的形状以及Layer的学习参数的形状来确定top Blob对象的形状。

virtual void Forward(const vector*> &bottom, vector*> *top) = 0

Layer内部数据正向传播,从bottom到top方向。

virtual void Backward(const vector*> &top,
                      const vector<bool> &propagate_down, 
                      vector*> *bottom) = 0

Layer内部梯度反向传播,从top到bottom方向。

1.5. Net

1.5.1 Net的类型描述

Net用容器的形式将多个Layer有序地放在一起,其自身实现的功能主要是对逐层Layer进行初始化,以及提供Update( )的接口(更新网络参数),本身不能对参数进行有效地学习过程。

1.5.2. Net的重要成员函数和变量

vector<shared_ptr > > layers_ //构成该net的layers
vector<vector*> > bottom_vecs_ //每一层layer中的bottom Blobs
vector<vector*> > top_vecs_ //每一层layer中的top Blobs
vector<shared_ptr > > params_ //整个net中的learnable parameter
void Init(const NetParameter& param)

根据NetParameter进行net初始化,简单的来说就是先把网络中所有层的bottom Blobs&top Blobs(无重复)实例化,并从输入层开始,逐层地进行Setup的工作,从而完成了整个网络的搭建,为后面的数据前后传输打下基础。

vector*>& Forward(const vector* > & bottom, Dtype* loss = NULL)
void Net::Backward()

是对整个网络的前向和方向传导,各调用一次就可以计算出网络的loss了。

1.6. Solver

1.6.1. Solver的类型描述

Solver类中包含一个Net的指针,主要是实现了训练模型参数所采用的优化算法,根据优化算法的不同会派生不同的类,而基于这些子类就可以对网络进行正常的训练过程。

1.6.2. Solver的重要成员函数和变量**

shared_ptr > net_ //net对象
void Step(int iters)

对已初始化后的网络进行固定次数的训练迭代过程。

ComputeUpdateValue();
net_->Update();

不同的模型训练方法通过重载函数ComputeUpdateValue( )实现计算update参数的核心功能。

2. 网络配置文件——layer

caffe入门教程1——Caffe代码层次:Blob、Layer、Net、Solver_第1张图片
caffe入门教程1——Caffe代码层次:Blob、Layer、Net、Solver_第2张图片

2.1 数据层

layer {
  name: "cifar"  
  type: "Data"   
  top: "data"    
  top: "label"  #一般用bottom表示每个层的输入,top表示输出,多个top代表有多个输出。
  include {
    phase: TRAIN #训练网络分为训练阶段和自测试阶段,如果没写include则表示该层即在测试中,又在训练中
  }
  transform_param {
    mean_file: "examples/cifar10/mean.binaryproto" #用一个配置文件来进行均值的操作
    transform_param {
        scale: 0.00390625
        mirror: 1  # 1表示开启镜像,0表示关闭,也可用ture和false来表示
        # 剪裁一个 227*227的图块,在训练阶段随机剪裁,在测试阶段从中间裁剪
        crop_size: 227
     }
  }
  data_param {
    source: "examples/cifar10/cifar10_train_lmdb" #数据库来源
    batch_size: 64 #每次批处理的个数
    backend: LMDB #选用数据的名称
  }
}

### 使用LMDB源
layer {
  name: "mnist"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  }
  transform_param {
    scale: 0.00390625
  }
  data_param {
    source: "examples/mnist/mnist_train_lmdb"
    batch_size: 64
    backend: LMDB
  }
}

###使用HDF5数据源
layer {
  name: "data"
  type: "HDF5Data"
  top: "data"
  top: "label"
  hdf5_data_param {
    source: "examples/hdf5_classification/data/train.txt"
    batch_size: 10
  }
}

###数据直接来源与图片
#/path/to/images/img3423.jpg 2  
#/path/to/images/img3424.jpg 13  
#/path/to/images/img3425.jpg 8

layer {
  name: "data"
  type: "ImageData" #类型
  top: "data"
  top: "label"
  transform_param {
    mirror: false
    crop_size: 227
    mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
  }
  image_data_param {
    source: "examples/_temp/file_list.txt"
    batch_size: 50
    new_height: 256 #如果设置就对图片进行resize操作
    new_width: 256
  }
}

解析:
(1)第1个layer:

  • name: “cifar” #代表层的名字,起名比较随意;
  • type: “Data” #告诉网络,第一层是数据层;
  • 一般用bottom表示每个层的输入,top表示输出,多个top代表有多个输出。这里是数据层作为网络的第一层,所以就没有bottom这一项了。Data层的输出一般为两个:data和label。
  • include { phase: TRAIN }
    训练网络分为训练阶段和自测试阶段。phase:TRAIN表示这一layer的数据只有在训练时才会用到,在测试时不会用到;如果是phase: TEST,则表示这一layer的数据只能在测试阶段用到。如果没写include则表示该层既在测试中,又在训练中。
  • 接下来是transform参数:
    • mean_file:对数据进行预处理,通常会做对数据减去均值。数据通常是3通道RGB图像。首先求出各通道的均值m_R, m_G, m_B,然后R、G、B各通道的数据分别减去m_R, m_G, m_B。Mean_file的参数怎样通过求出,后面会讲到。
    • scale::data的数据都在0-255范围内,越大代表该点越亮。我们要做一个归一化操作,将这些像素点数据压缩到0-1范围内。
      这里写图片描述
      scale就实现了这个归一化。如果你不想实现归一化,可以不写scale。
    • mirror和crop_size:数据扩充的两种常用方法。mirror对原始图像做镜像,1表示开启镜像,0表示关闭,也可用ture和false来表示。crop_size对原始图像做裁剪,比如原图像尺寸为256*256,现在crop_size: 227剪裁一个227*227的图块。
  • 接下来是data参数:
    • data从哪里来的?一般caffe要求data有两种格式:LMDB(常用来做分类)和HDF5(常用来做回归)。
      一般我们要将数据做成数据库的格式,然后把数据库的源交给caffe,caffe就会读取你的数据库。source:
      “examples/cifar10/cifar10_train_lmdb” ,source写下数据库来源的路径。
    • batch_size: 64 #每次批处理的个数,一般为2^n。一般希望一次批处理的数据更多些,避免过拟合,比如选128,这通常由GPU来决定,GPU比较大batch_size也可以设置的大些。
    • backend: LMDB #选用数据的名称

(2)接下来是使用LMDB源、使用HDF5数据源,与前面的定义都类似。也可以不做数据库,比如我们在image文件夹下有很多图片。

  • 我们就:source: “examples/_temp/file_list.txt”,将source源写成.txt文件,.txt文件中每一行前面写图片的路径,后面写它的label。
  • 接下来可以有一些新的参数new_height和new_width,可以将图像数据resize,来将所有图像shape统一。

2.2 卷积层

layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  param {
    lr_mult: 1  #lr_mult: 学习率的系数,最终的学习率是这个数乘以solver.prototxt配置文件中的base_lr。如果有两个lr_mult, 则第一个表示权值的学习率,第二个表示偏置项的学习率。一般偏置项的学习率是权值学习率的两倍。
  }
  param {
    lr_mult: 2
  }
  convolution_param {
    num_output: 20 #卷积核(filter)的个数
    kernel_size: 5 #卷积核的大小 
    stride: 1 #卷积核的步长,默认为1 
    pad: 0 #扩充边缘,默认为0,不扩充
    weight_filler {
      type: "xavier" #权值初始化。 默认为“constant",值全为0,很多时候我们用"xavier"算法来进行初始化,也可以设置为”gaussian"
    }
    bias_filler {
      type: "constant" #偏置项的初始化。一般设置为"constant",值全为0
    }
  }
}

输入:n*c0*w0*h0
输出:n*c1*w1*h1
其中,c1就是参数中的num_output,生成的特征图个数
 w1=(w0+2*pad-kernel_size)/stride+1;
 h1=(h0+2*pad-kernel_size)/stride+1;

解析:

  • name: “conv1”,定义该layer的名字
  • type: “Convolution”,说明该层是卷积层
  • bottom: “data”和top: “conv1”,该层前面连接data层,输出是他卷积后的结果。
  • 参数:我们通过solver文件设置的学习率是一个基础学习率,这里可以的lr_mult是控制当前这个层的学习率。
    lr_mult: 学习率的系数,最终的学习率是这个数乘以solver.prototxt配置文件中的base_lr。如果有两个lr_mult, 则第一个表示权值w的学习率,第二个表示偏置项b的学习率。一般偏置项的学习率是权值学习率的两倍,lr_mult: 2。
    如果你希望某些层在反向传播过程中参数不做更新,则可以通过控制lr_mult: 0来实现。
  • 卷积参数:
    • num_output和kernel_size:卷积核(filter)的个数、卷积核的大小 。这里隐含了一个深度d的概念,比如上一层的输出是RGB三个通道的,那么这里的filter尺寸就应该是5*5*d=5*5*3,即卷积核的深度等于前面一层输出的深度
    • stride: 1,卷积核的步长,默认为1 ;pad: 0,填充边缘,默认为0,不填充。
    • weight_filler { type: “xavier” } ,权值初始化。 默认为“constant”,值全为0,很多时候我们用”xavier”算法来进行初始化,也可以设置为”gaussian”。
    • bias_filler {type: “constant” }, 偏置项的初始化。一般设置为”constant”,值全为0。
  • 输入:n*c0*w0*h0;输出:n*c1*w1*h1,其中,c1就是参数中的num_output,生成的特征图个数
    w1=(w0+2*pad-kernel_size)/stride+1;
    h1=(h0+2*pad-kernel_size)/stride+1;
    即:这里写图片描述

2.3 pooling层

layer {
  name: "pool1"
  type: "Pooling"
  bottom: "conv1"
  top: "pool1"
  pooling_param {
    pool: MAX    #池化方法,默认为MAX。目前可用的方法有MAX, AVE
    kernel_size: 3  #池化的核大小
    stride: 2       #池化的步长,默认为1。一般我们设置为2,即不重叠。
  }
}

#pooling层的运算方法基本是和卷积层是一样的。

2.4 激活函数

#在激活层中,对输入数据进行激活操作,是逐元素进行运算的,在运算过程中,没有改变数据的大小,即输入和输出的数据大小是相等的。

###Sigmoid


layer {
  name: "test"
  bottom: "conv"
  top: "test"
  type: "Sigmoid"
}

#ReLU是目前使用最多的激活函数,主要因为其收敛更快,并且能保持同样效果。标准的ReLU函数为max(x, 0),当x>0时,输出x; 当x<=0时,输出0
f(x)=max(x,0)



layer {
  name: "relu1"
  type: "ReLU"
  bottom: "pool1"
  top: "pool1"
}

解析:
Sigmoid函数可能会发生梯度消失的现象。Relu激活函数是用到最多的激活函数。

2.5 全连接层

#全连接层,输出的是一个简单向量  参数跟卷积层一样
layer {
  name: "ip1"
  type: "InnerProduct"
  bottom: "pool2"
  top: "ip1"
  param {
    lr_mult: 1
  }
  param {
    lr_mult: 2
  }
  inner_product_param {
    num_output: 500
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
#测试的时候输入准确率
layer {
  name: "accuracy"
  type: "Accuracy"
  bottom: "ip2"
  bottom: "label"
  top: "accuracy"
  include {
    phase: TEST
  }
}

解析:

  • 全连接层和卷积层差不多,也可以设置学习率:lr_mult。
  • num_output: 500,全连接层输出的是一个向量。全连接层相当于将之前的所提取的特征再总结成一个向量形式,来进行分类等任务。
  • 还包括一个layer:来查看当前迭代的准确率。它的bottom比较特别,一个是ip2,即最终的全连接层分类结果,一个是label,来将预测结果与原始输入进行一个匹配得到准确率。

2.6 softmax-loss

#softmax-loss layer:输出loss值
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "ip1"
  bottom: "label"
  top: "loss"
}

#softmax layer: 输出似然值
layers {
  bottom: "cls3_fc"
  top: "prob"
  name: "prob"
  type: “Softmax"
}

解析:既可以输出loss值,也可以输出比如输出是猫的概率值(即似然值)

2.7 reshape层

#在不改变数据的情况下,改变输入的维度

layer {
    name: "reshape"
    type: "Reshape"
    bottom: "input"
    top: "output"
    reshape_param {
      shape {
        dim: 0  # copy the dimension from below
        dim: 2
        dim: 3
        dim: -1 # infer it from the other dimensions
      }
    }
  }

有一个可选的参数组shape, 用于指定blob数据的各维的值(blob是一个四维的数据:n*c*w*h)。

dim:0  表示维度不变,即输入和输出是相同的维度。

dim:2 或 dim:3 将原来的维度变成23

dim:-1 表示由系统自动计算维度。数据的总量不变,系统会根据blob数据的其它三维来自动计算当前维的维度值 。

假设原数据为:32*3*28*28, 表示323通道的28*28的彩色图片
    shape {
    dim: 0 
    dim: 0
    dim: 14
    dim: -1 
    }
输出数据为:32*3*14*56

#Dropout是一个防止过拟合的层
#只需要设置一个dropout_ratio就可以了。
layer {
  name: "drop7"
  type: "Dropout"
  bottom: "fc7-conv"
  top: "fc7-conv"
  dropout_param {
    dropout_ratio: 0.5
  }
}

解析:

  • Dropout是一个防止过拟合的层。来在前向传播和反向传播过程中杀死一些神经元。只需要定义一个参数:dropout_ratio:
    0.5,代表一次迭代要杀死50%的神经元。如果神经网络过拟合,可以将这个数值设置得更高些,比如杀死60%、70%的神经元。

2.8 下面给出一个完整的net配置示例:

即将上面讲述的各种layer按一定顺序连接起来。

layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  transform_param {
    scale: 0.00392156862745
  }
  data_param {
    source: "examples/mnist/mnist_test_lmdb"
    batch_size: 100
    backend: LMDB
  }
}
layer {
  name: "conv1"
  type: "Convolution"
  bottom: "data"
  top: "conv1"
  convolution_param {
    num_output: 20
    kernel_size: 5
    weight_filler {
      type: "xavier"
    }
  }
}
layer {
  name: "pool1"
  type: "Pooling"
  bottom: "conv1"
  top: "pool1"
  pooling_param {
    pool: MAX
    kernel_size: 2
    stride: 2
  }
}
layer {
  name: "conv2"
  type: "Convolution"
  bottom: "pool1"
  top: "conv2"
  convolution_param {
    num_output: 50
    kernel_size: 5
    weight_filler {
      type: "xavier"
    }
  }
}
layer {
  name: "pool2"
  type: "Pooling"
  bottom: "conv2"
  top: "pool2"
  pooling_param {
    pool: MAX
    kernel_size: 2
    stride: 2
  }
}
layer {
  name: "ip1"
  type: "InnerProduct"
  bottom: "pool2"
  top: "ip1"
  inner_product_param {
    num_output: 500
    weight_filler {
      type: "xavier"
    }
  }
}
layer {
  name: "relu1"
  type: "ReLU"
  bottom: "ip1"
  top: "ip1"
}
layer {
  name: "ip2"
  type: "InnerProduct"
  bottom: "ip1"
  top: "ip2"
  inner_product_param {
    num_output: 10
    weight_filler {
      type: "xavier"
    }
  }
}
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "ip2"
  bottom: "label"
  top: "loss"
}

3. solver

#往往loss function是非凸的,没有解析解,我们需要通过优化方法来求解。
#caffe提供了六种优化算法来求解最优参数,在solver配置文件中,通过设置type类型来选择。

    Stochastic Gradient Descent (type: "SGD"),
    AdaDelta (type: "AdaDelta"),
    Adaptive Gradient (type: "AdaGrad"),
    Adam (type: "Adam"),
    Nesterov’s Accelerated Gradient (type: "Nesterov") and
    RMSprop (type: "RMSProp")


net: "examples/mnist/lenet_train_test.prototxt"  
test_iter: 100
test_interval: 500
base_lr: 0.01
momentum: 0.9
type: SGD
weight_decay: 0.0005
lr_policy: "inv"
gamma: 0.0001
power: 0.75
display: 100
max_iter: 20000
snapshot: 5000
snapshot_prefix: "examples/mnist/lenet"
solver_mode: CPU

net: "examples/mnist/lenet_train_test.prototxt" #网络位置
train_net: "examples/hdf5_classification/logreg_auto_train.prototxt" #也可以分别设定train和test
test_net: "examples/hdf5_classification/logreg_auto_test.prototxt"

test_iter: 100 #迭代了多少个测试样本呢? batch*test_iter 假设有5000个测试样本,一次测试想跑遍这5000个则需要设置test_iter×batch=5000

test_interval: 500 #测试间隔。也就是每训练500次,才进行一次测试。


base_lr: 0.01 #base_lr用于设置基础学习率

lr_policy: "inv" #学习率调整的策略

        - fixed:   保持base_lr不变.
        - step:    如果设置为step,则还需要设置一个stepsize,  返回 base_lr * gamma ^ (floor(iter / stepsize)),其中iter表示当前的迭代次数
        - exp:     返回base_lr * gamma ^ iter, iter为当前迭代次数
        - inv:      如果设置为inv,还需要设置一个power, 返回base_lr * (1 + gamma * iter) ^ (- power)
        - multistep: 如果设置为multistep,则还需要设置一个stepvalue。这个参数和step很相似,step是均匀等间隔变化,而multistep则是根据                                          stepvalue值变化
        - poly:     学习率进行多项式误差, 返回 base_lr (1 - iter/max_iter) ^ (power)
        - sigmoid: 学习率进行sigmod衰减,返回 base_lr ( 1/(1 + exp(-gamma * (iter - stepsize))))

momentum :0.9 #动量

display: 100 #每训练100次,在屏幕上显示一次。如果设置为0,则不显示。

max_iter: 20000 #最大迭代次数,2W次就停止了

snapshot: 5000 #快照。将训练出来的model和solver状态进行保存,snapshot用于设置训练多少次后进行保存
snapshot_prefix: "examples/mnist/lenet" 

solver_mode: CPU #设置运行模式。默认为GPU,如果你没有GPU,则需要改成CPU,否则会出错。

解析:

  • 一共有6种优化算法可选:
    Stochastic Gradient Descent (type: “SGD”),
    AdaDelta (type: “AdaDelta”),
    Adaptive Gradient (type: “AdaGrad”),
    Adam (type: “Adam”),
    Nesterov’s Accelerated Gradient (type: “Nesterov”) and
    RMSprop (type: “RMSProp”)
    这里选择type: SGD
  • net: “examples/mnist/lenet_train_test.prototxt”,告诉solver文件,刚才写好的网络配置文件在什么位置。
    也可以指定train_net和test_net,来指定用哪个net进行训练、用哪个net进行测试。不过一般只写一个net就够了。
  • test_iter: 100 。一次迭代了测试样本的个数=batch*test_iter。假设有5000个测试样本,一次测试想跑遍这5000个样本(这样具有一般性)则需要设置test_iter×batch=5000
  • test_interval: 500 #测试间隔。也就是每训练500次,才进行一次测试。一般设成1000。
  • base_lr: 0.01 #base_lr用于设置基础学习率,很重要。如果网络出现过拟合,很有可能是base_lr过高,可以改为0.001甚至更低。
  • lr_policy: “inv”。我们希望在随着迭代次数增大,学习率能够逐渐下降,就要调整学习率。学习率调整的策略有很多可选项,一般用inv比较多。如下:
    • fixed: 保持base_lr不变.
    • step: 如果设置为step,则还需要设置一个stepsize, 返回 base_lr * gamma ^ (floor(iter / stepsize)),其中 iter表示当前的迭代次数
    • exp: 返回base_lr * gamma ^ iter, iter为当前迭代次数
    • inv: 如果设置为inv,还需要设置一个power, 返回base_lr * (1 + gamma * iter) ^ (- power)
    • multistep: 如果设置为multistep,则还需要设置一个stepvalue。这个参数和step很相似,step是均匀等间隔变化,而multistep则是根据stepvalue值变化
    • poly: 学习率进行多项式误差, 返回 base_lr (1 - iter/max_iter) ^ (power)
    • sigmoid: 学习率进行sigmod衰减,返回 base_lr ( 1/(1 + exp(-gamma * (iter - stepsize))))
  • momentum :0.9 #动量,加快梯度下降的速度,0.9几乎是一个固定值。
  • display: 100 #每训练100次,在屏幕上显示一次。如果设置为0,则不显示。
  • max_iter: 20000 #最大迭代次数,2W次就停止了
  • snapshot: 5000 #快照。将训练出来的model和solver状态进行保存,snapshot用于设置训练多少次后进行保存。我们保存下来的模型越多,越可以后期比较这些模型那个最好。
    snapshot_prefix: “examples/mnist/lenet” 是将model保存的文件夹。
  • solver_mode: CPU #设置运行模式。默认为GPU,如果你没有GPU,则需要改成CPU,否则会出错。
  • solver文件中一般情况下我们只需要调节学习率base_lr就可以了。

下一节我们解决:在ubuntu系统下我们怎样把caffe数据源做好,然后把网络跑通?

你可能感兴趣的:(Caffe)