解析./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt

解析./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt

解析./build/tools/caffe train --solver=examples/mnist/lenet_solver.prototxt 
第一个参数build/tools/caffe是Caffe框架的主要框架,由tools/caffe.cpp文件编译而来,第二个参数train表示是要训练网络,第三个参数是 solver的protobuf描述文件。 
在Caffe中,网络模型的描述及其求解都是通过 protobuf 定义的,模型的参数也是通过 protobuf 实现加载和存储,包括 CPU 与 GPU 之间的无缝切换,不需要通过硬编码的方式实现。在caffe.cpp中main函数之外,通过宏RegisterBrewFunction将train(),test(),device_query(),time()等函数及其对应的函数指针添加到了g_brew_map中, 通过GetBrewFunction可以得到需要调用的函数的函数指针。

1.网络初始化过程

第二个参数train调用caffe.cpp中的int train()函数。在train函数中:

// caffe.cpp
shared_ptr<caffe::Solver<float>> solver(caffe::SolverRegistry<float>::CreateSolver(solver_param);

首先定义了一个指向Solver的shared_ptr,然后其通过调用SolverRegistry类的静态成员函数CreateSolver得到一个指向Solver的指针来构造shared_ptr类型的solver。 
由于C++多态性,尽管solver是一个指向基类Solver类型的指针,通过solver这个智能指针来调用各个子类(SGDSolver等)的函数。在caffe.proto文件中默认的优化type为SGD,所以上面的代码会实例化一个SGDSolver的对象,SGDSolver类继承于Solver类,在新建SGDSolver对象时会调用其构造函数如下所示:

//sgd_solvers.hpp 
explicit SGDSolver(const SolverParameter& param)
                : Solver<Dtype>(param) { PreSolve(); }

其中会先调用父类的Solver的构造函数。

//solver.cpp
template <typename Dtype>
Solver<Dtype>::Solver(const SolverParameter& param, const Solver* root_solver)
                : net_(), callbacks_(), root_solver_(root_solver),requested_early_exit_(false)
{
  Init(param);
}

Solver类的构造函数通过Init(param)函数来初始化网络。在Init(param)函数中,又主要是通过InitTrainNet()和InitTestNets()函数分别来搭建训练网络结构和测试网络结构。训练网络只能有一个,在InitTrainNet()函数中首先会设置一些基本参数,包括设置网络的状态为TRAIN,确定训练网络只有一个等,然后会通过net_.reset(new Net<Dtype>(net_param))新建了一个Net对象。新建了Net对象之后会调用Net类的构造函数:

//net.cpp
template <typename Dtype> 
Net<Dtype>::Net(const NetParameter& param, const Net* root_net)
            : root_net_(root_net) {   Init(param); }

Net类的构造函数是通过Init(param)函数来初始化网络结构的。 
在Init()函数中,LayerRegistry<Dtype>::CreateLayer(layer_param)主要是通过调用LayerRegistry这个类的静态成员函数CreateLayer得到一个指向Layer类的shared_ptr类型指针,并把每一层的指针保存到vector<shared_ptr<Layer<Dtype>>>layers_指针容器里。即根据每层的参数layer_param实例化了对应的各个子类层,比如conv_layer(卷积层)和pooling_layer(池化层),实例化了各层就会调用每个层的构造函数。 
Init()函数主要有四部分: 
- AppendBottom:设置每一层的输入数据 。 
- AppendTop:设置每一层的输出数据。 
- layers_[layer_id]->SetUp:对上面设置的输入输出数据计算分配空间,并设置每层的可学习参数(权值和偏置)。 
- AppendParam:对上面申请的可学习参数进行设置,主要包括学习率和正则率等。

//net.cpp Init()
for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id) 
{//param是网络参数,layer_size()返回网络拥有的层数
    const LayerParameter& layer_param = param.layer(layer_id);//获取当前layer的参数
    layers_.push_back(LayerRegistry<Dtype>::CreateLayer(layer_param));//根据参数实例化layer

    //下面的两个for循环将此layer的bottom blob的指针和top blob的指针放入bottom_vecs_和top_vecs_,bottom blob和top blob的实例全都存放在blobs_中。
    //相邻的两层,前一层的top blob是后一层的bottom blob,所以blobs_的同一个blob既可能是bottom blob,也可能使top blob。
    for (int bottom_id = 0; bottom_id < layer_param.bottom_size();++bottom_id)
    {
       const int blob_id=AppendBottom(param,layer_id,bottom_id,&available_blobs,&blob_name_to_idx);
    }

    for (int top_id = 0; top_id < num_top; ++top_id) 
    {
       AppendTop(param, layer_id, top_id, &available_blobs, &blob_name_to_idx);
    }

    // 调用layer类的Setup函数进行初始化,输入参数:每个layer的输入blobs以及输出blobs
    layers_[layer_id]->SetUp(bottom_vecs_[layer_id], top_vecs_[layer_id]);

    //接下来的工作是将每层的parameter的指针塞进params_,尤其是learnable_params_
    const int num_param_blobs = layers_[layer_id]->blobs().size();
    for (int param_id = 0; param_id < num_param_blobs; ++param_id) 
    {
       AppendParam(param, layer_id, param_id);
    }
}

Layer类的Setup()函数,对每一层的设置主要由两个函数组成: 
LayerSetUp(bottom, top):由Layer类派生出的特定类都需要重写这个函数,主要功能是设置权值参数(包括偏置)的空间以及对权值参数经行随机初始化。 
Reshape(bottom, top):根据输出blob和权值参数计算输出blob的维数,并申请空间。

//layer.hpp
// layer 初始化设置
void SetUp(const vector<Blob<Dtype>*>& bottom,   
    const vector<Blob<Dtype>*>& top) {
  InitMutex();
  CheckBlobCounts(bottom, top);
  LayerSetUp(bottom, top);
  Reshape(bottom, top);
  SetLossWeights(top);
}

经过上述过程基本上就完成了初始化的工作,总体的流程是新建一个Solver对象,然后调用Solver类的构造函数,然后在Solver的构造函数中又会新建Net类实例,在Net类的构造函数中又会新建各个Layer的实例,一直具体到设置每个Blob。

2.训练过程

网络的初始化即创建一个solver指针并逐步调用Solver、Net、Layer、Blob类的构造函数,完成整个网络的初始化。完成初始化工作之后,指向Solver类的指针solver开始调用Solver类的成员函数Solve():solver->Solve()。 
Solve函数其实主要就是调用了Solver的另一个成员函数Step()来完成实际的迭代训练过程。

//solver.cpp
template <typename Dtype>
void Solver<Dtype>::Solve(const char* resume_file) 
{
  ...
  int start_iter = iter_;
  ...
  // 然后调用了Step函数,这个函数执行了实际的逐步的迭代过程
  Step(param_.max_iter() - iter_);
  ...
  LOG(INFO) << "Optimization Done.";
}

Step()函数主要分为三个部分,首先是一个大循环设置了总的迭代次数,在每次迭代中训练iter_size * batch_size个样本(在GPU的显存不够的时候使用),例如设置batch_size为128,iter_size是默认为1的,但是会out_of_memory,借助这个方法,可以设置batch_size=32,iter_size=4,那实际上每次迭代还是处理了128个数据。

//solver.cpp
template <typename Dtype>
void Solver<Dtype>::Step(int iters) 
{
  ...
  //迭代
  while (iter_ < stop_iter) 
  {
    ...
    // iter_size也是在solver.prototxt里设置,实际上的batch_size=iter_size * batch_size,
    // 因此每一次迭代的loss是iter_size次迭代的和,再除以iter_size,loss是通过调用Net::ForwardBackward函数得到

    for (int i = 0; i < param_.iter_size(); ++i) 
    {
      //主要完成了前向后向的计算,
      //前向用于计算模型的最终输出和Loss,后向用于计算每一层网络和参数的梯度。      
      loss += net_->ForwardBackward();
    }
    ...
    //主要对Loss进行平滑。由于Caffe的训练方式是SGD,无法把所有的数据同时放入模型进行训练,那么部分数据产生的Loss就可能会和全样本的平均Loss不同,在必要时候将Loss和历史过程中更新的Loss求平均就可以减少Loss的振荡问题。
    UpdateSmoothedLoss(loss, start_iter, average_loss);
    ...
    // 执行梯度的更新,这个函数在基类Solver中没有实现,会调用每个子类自己的实现
    ApplyUpdate();
    // 迭代次数加1
    ++iter_;
    ...
  }
}

ForwardBackward()函数如下:

// net.hpp
// 进行一次正向传播,一次反向传播
Dtype ForwardBackward() {
  Dtype loss;
  Forward(&loss);
  Backward();
  return loss;
}

Forward(&loss)函数最终会执行到如下代码:

//net.cpp
for (int i = start; i <= end; ++i) {
// 对每一层进行前向计算,返回每层的loss,其实只有最后一层loss不为0
  Dtype layer_loss = layers_[i]->Forward(bottom_vecs_[i], top_vecs_[i]);
  loss += layer_loss;
  if (debug_info_) { ForwardDebugInfo(i); }
}

具体的每一层Layer的派生类均会重写Forward()函数来实现不同层的前向计算功能。Backward()反向求导函数也和Forward()类似,调用不同层的Backward()函数来计算每层的梯度。 
ApplyUpdate()函数是Solver类的纯虚函数,需要派生类来实现。SGDSolver类实现的ApplyUpdate()函数:

template <typename Dtype>
void SGDSolver<Dtype>::ApplyUpdate() 
{
  CHECK(Caffe::root_solver());

  // GetLearningRate根据设置的lr_policy来计算当前迭代的learning rate的值
  Dtype rate = GetLearningRate();

  // 判断是否需要输出当前的learning rate
  if (this->param_.display() && this->iter_ % this->param_.display() == 0) 
  {
    LOG(INFO) << "Iteration " << this->iter_ << ", lr = " << rate;
  }

  // 避免梯度爆炸,如果梯度的二范数超过了某个数值则进行scale操作,将梯度减小
  ClipGradients();

  // 对所有可更新的网络参数进行操作
  for (int param_id = 0; param_id < this->net_->learnable_params().size();
  {
    // 将第param_id个参数的梯度除以iter_size,
    // 这一步的作用是保证实际的batch_size=iter_size*设置的batch_size
    Normalize(param_id);

    // 将正则化部分的梯度降入到每个参数的梯度中
    Regularize(param_id);

    // 计算SGD算法的梯度(momentum等)
    ComputeUpdateValue(param_id, rate);
  }
  // 调用`Net::Update`更新所有的参数
  this->net_->Update();
}

ApplyUpdate()函数主要完成以下工作: 
- 设置参数的学习率; 
- 对梯度进行Normalize; 
- 对反向求导得到的梯度添加正则项的梯度; 
- 最后根据SGD算法计算最终的梯度; 
- 最后的最后把计算得到的最终梯度对权值进行更新。

posted @ 2017-07-16 09:41  菜鸡一枚  阅读(292)  评论(0编辑  收藏  举报