caffe模型优化流程解析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiequnyi/article/details/76100043

因公司项目所需,需要将torch版本下实现的模型移植到caffe下,并尽量复现至torch版本下的测试精度。模型的移植,无非就是找到torch下模型层所对应的caffe层,然后写一个caffe版本的net.prototxt,并根据torch下的优化方法写一个对应caffe版本的solver.prototxt,然后写一个train.sh的训练脚本训练模型即可。但在移植过程中, 发现caffe版本下的模型训练(利用SGD优化)收敛速度慢,且精度不及torch版本,所以想一探究竟,研究下问题何在?
本文不考虑模型与数据集对模型训练的影响,即假设两个框架下的模型是一致的,且使用的是相同的数据集,关于模型的移植实现,有机会另开博文详述,本文在以上假设下,研讨caffe模型的优化流程,分析其中的函数调用关系,并据此,比对一种优化算法Adadelta在两种框架下的实现。

这里首先给出adadelta优化方法的一般参数配置方法 solver.prototxtnet: "./examples/train_net.prototxt"
test_iter: 4
test_interval: 1000
base_lr: 1.0
lr_policy: "fixed"
display: 100
max_iter: 450000
momentum: 0.9
weight_decay: 0.0005
snapshot: 5000
snapshot_prefix: "./examples/net_iter"
solver_mode: GPU
type: "AdaDelta"
delta: 1e-6

一. caffe 模型优化的流程解析

已知,模型的训练起于训练脚本(train.sh),一般train.sh的主体内容包括:

./build/tools/caffe train \
    -solver ./examples/resnet-bankcard/solver.prototxt \
    -gpu $GPU_ID 2>&1 | tee -a ${log_file_name}

意思是,./build/tools下有一个caffe的执行文件,由tools/caffe.cpp编译得到,主要负责模型的搭建,参数的读取,模型优化,模型及训练现场保护等工作。后面所跟的参数,-solver是模型的优化参数,具体每个值的含义,定义在src/caffe/proto/caffe.proto【SolverParameter】中,-gpu为选择运行训练的GPU的卡号,如果只有一个卡,默认为0,最后一项,2>&1 | tee -a ${log_file_name},表示将模型训练的中间信息及错误信息同时输出到屏幕与log文件中。
下面解析模型优化的分步流程:
1. 首先调用tools/caffe.cpp的train(),通过-solver传入的优化参数文档的名称,依次调用util/upgrade_proto.cpp【ReadSolverParamsFromTextFileOrDie】,io.cpp【ReadProtoFromTextFile】,解析文档中的优化参数,保存至SolverParam变量中并返回train()函数。需要说明的是,从底层ReadProtoFromTextFile的调用中,可以看出SolverParam其实是一个Message类型的变量中,是不是很熟悉,对了,就是caffe.proto中定义的Message类型的SolverParameter类型。
2. 获取了优化参数变量后,继续执行train()函数,根据GPU设备的设定设置训练模式,然后236行,调用Solver方法,注册一个优化器,这里根据优化参数的【type:Adadelta】会创建一个Adadelta优化器,在创建时调用构造函数,初始化优化器及模型。在solver.cpp中调用InitTrainNet()与InitTestNet()分别构建好训练网络与测试网络,其主要区别在于loss与精度的计算,这一步骤具体的工作其实是调用net.cpp方法中的函数实现的。
3. 接着,调用solver优化器的Solver()方法,进行逐步迭代优化,这里主要运行了solver.cpp的step()方法,该方法中,首先调用test(),计算初始模型参数下的loss,然后调用train(),开启训练,训练主要包括前向(计算loss)与后向(根据loss计算梯度),然后调用ApplyUpdate(),利用梯度来更新模型参数。各种不同的优化算法(包括本文提到的adadelta)就在该步骤下运行。特别需要说明的是,在log文件中,会发现,不论使用何种优化算法,都会显示调用了sgd_solver.cpp,而非其本身的cpp,为何??原因在于,ApplyUpdate()是sgd_solver的成员函数,但该方法中真正核心的部分,是动态执行函数ComputeUpdateValue()。由于该函数是虚函数,又AdaDeltaSolver继承自SGDSolver,是通过指针来动态调用的。结合步骤2中所讲,优化器是根据type类型动态注册的,所以当type为adadelta时,该步就会动态调用adadelta类的ComputeUpdateValue()方法,从而实现adadelta优化。
详细解释下ComputeUpdateValue()方法中的重点代码:
adadelta

//计算梯度的平方,对应公式3部分
caffe_powx(net_params[param_id]->count(),
        net_params[param_id]->cpu_diff(), Dtype(2),
        this->update_[param_id]->mutable_cpu_data());
 // 计算梯度的期望,对应公式3完整,momentum即为solver.prototxt中的momentum
caffe_cpu_axpby(net_params[param_id]->count(), Dtype(1) - momentum,
        this->update_[param_id]->cpu_data(), momentum,
        this->history_[param_id]->mutable_cpu_data());
// 计算RMS的平方,对应公式2部分,delta即为solver.prototxt中的delta
caffe_set(net_params[param_id]->count(), delta,
        this->temp_[param_id]->mutable_cpu_data());
//  计算v的RMS的平方
caffe_add(net_params[param_id]->count(),
        this->temp_[param_id]->cpu_data(),
        this->history_[update_history_offset + param_id]->cpu_data(),
        this->update_[param_id]->mutable_cpu_data());
//  计算梯度的RMS的平方
caffe_add(net_params[param_id]->count(),
        this->temp_[param_id]->cpu_data(),
        this->history_[param_id]->cpu_data(),
        this->temp_[param_id]->mutable_cpu_data());
// v的RMS的平方除以梯度的RMS的平方
caffe_div(net_params[param_id]->count(),
        this->update_[param_id]->cpu_data(),
        this->temp_[param_id]->cpu_data(),
        this->update_[param_id]->mutable_cpu_data());
//上述结果开根号,对应公式1部分,
caffe_powx(net_params[param_id]->count(),
        this->update_[param_id]->cpu_data(), Dtype(0.5),
        this->update_[param_id]->mutable_cpu_data());
// 上述结果乘以梯度,对应公式1完整,得到最新的v
caffe_mul(net_params[param_id]->count(),
        net_params[param_id]->cpu_diff(),
        this->update_[param_id]->cpu_data(),
        net_params[param_id]->mutable_cpu_diff());
//更新v的平方
caffe_powx(net_params[param_id]->count(),
        net_params[param_id]->cpu_diff(), Dtype(2),
        this->update_[param_id]->mutable_cpu_data());
 //更新v的期望,并保存,下一次可使用
caffe_cpu_axpby(net_params[param_id]->count(), Dtype(1) - momentum,
        this->update_[param_id]->cpu_data(), momentum,
        this->history_[update_history_offset + param_id]->mutable_cpu_data());
//计算学习率,对应公式4部分,其中local_rate = rate * net_params_lr[param_id]; rate为solver.prototxt中的base_lr,net_params_lr[param_id]为layer参数中的param中的lr_mult();为与公式对应,这里base_lr与net_params_lr[param_id]必须为1;
caffe_cpu_scale(net_params[param_id]->count(), local_rate,
        net_params[param_id]->cpu_diff(),
        net_params[param_id]->mutable_cpu_diff());

最后的权重跟新,会根据此学习率,调用this->net_->Update();来执行;执行时,本质是调用blob的update函数,执行:参数【t+1】 = 参数【t】- 学习率;
4. 更新完参数后,判断是否当前迭代步骤下,需要保存snapshot,然后循环迭代,直至优化结束。

二. toech下的optim.adadelta方法:
https://github.com/torch/optim/blob/master/adadelta.lua

# 对应公式3
state.paramVariance:mul(rho):addcmul(1-rho,dfdx,dfdx)
# 对应公式2
state.paramStd:resizeAs(state.paramVariance):copy(state.paramVariance):add(eps):sqrt()    
# 对应公式1
state.delta:resizeAs(state.paramVariance):copy(state.accDelta):add(eps):sqrt():cdiv(state.paramStd):cmul(dfdx)
# 对应公式4,x即为模型参数,x -> x-state.delta,即公式4中的alpha为1;
x:add(-1, state.delta)
state.accDelta:mul(rho):addcmul(1-rho, state.delta, state.delta)

结论:比较两者的实现过程,其他都是一样,只是torch下的alpha恒为1,caffe下的alpha即为local_rate = rate * net_params_lr[param_id];因此,为保证两者一致的话,需要base_lr与模型参数的lr_mult()同时设置为1。

没有更多推荐了,返回首页