本篇文章将对mxnet的BatchNorm操作进行详细说明, 源码见src/operator/batch_norm-inl.h. 现将源码batch_norm-inl.h.及注释贴上. 源码的注释都是笔者自己写的, 有分析不对的地方网各位读者加以指正. 以后的BN层, 全连接层, 卷积层, 池化层, Dropout层只把层的参数部分, 前向传播和反向传播部分贴上.
/*!
* Copyright (c) 2015 by Contributors
* \file batch_norm-inl.h
* \brief
* \author
*/
#ifndef MXNET_OPERATOR_BATCH_NORM_INL_H_
#define MXNET_OPERATOR_BATCH_NORM_INL_H_
#include // mxnet的日志头文件. 在dmlc-core/include/dmlc下,
#include // mxnet的参数头文件, 在dmlc-core/include/dmlc下, 定义参数的.
#include // 在include/mxnet下, 定义操作基类(operator), 操作属性类, 方法等. 对OP或Prop的函数进行声明.
#include
#include // 向量容器.
#include // 字符串.
#include // utility头文件定义重载的关系运算符, 简化关系运算符的写入, 还定义了pair类型,
// pair类型是一种模板类型, 可以存储一对值.
#include "./operator_common.h" // src/operator下, mxnet的层一些常用的属性.
#include "./mshadow_op.h" // src/operator下, 定义了一些结构体. 这些结构体用来接收数据实现某些层的前向输出和反向输出, 如激活函数
// 层有softplus, softplus_grad. 一个计算前向的输出, 一个计算反向的输出.
#include
using namespace std;
namespace mxnet {
namespace op {
namespace batchnorm {
enum BatchNormOpInputs {kData, kGamma, kBeta}; // BN层输入参数, kData为0, kGamma为1, kBeta为2. 这里批训练时, gamma和beta的值可
// 以对所有batch的样本一样, 也可以不一样,
enum BatchNormOpOutputs {kOut, kMean, kVar}; // BN层的输出参数, kOut为0, kMean为1, kVar为2. 利用kData可以首先计算出kMean和kVar
// 然后在此基础上, 联合kGamma和kBeta计算kOut. (用符号代替了变量).
enum BatchNormOpAuxiliary {kMovingMean, kMovingVar}; // BN操作的辅助变量, kMovingMean为0, kMovingVar为1. 在做前向操作时能更好
// 地理解这两个量. 为求解batch数据的Mean和Var服务. 为了方便计算而需要的附加的tensor.
enum BatchNormBackResource {kTempSpace}; // 反向传播的资源配置, 设置一个临时空间, 这个空间可以是任意大小的.
/*
有些操作需要额外的内存作为工作空间进行计算, 比如说BatchNormBackward. 这种情况下,
系统最好可以对这部分内存进行管理, 这样系统可以做一些优化, 比如说内存的重复利用.
struct ResourceRequest {
enum Type {
kRandom, // get an mshadow::Random object
kTempSpace, // request temporay space
};
Type type;
};
*/
} // namespace batchnorm
struct BatchNormParam : public dmlc::Parameter { // BatchNormParam, BN操作参数结构体, 对BN层的参数进行描述, 设
// 置初值, 设定范围等.
float eps; // eps, 即BN操作中从 x^(k) --> X^(k)时, 要x^(k)减去批样本均值, 除以批样本方差, 除以方差时为防为0, 变为
// var[x^(k)] + eps.
float momentum; // momentum, momentum是moving average的动量项. float, 初值是0.9f.
bool fix_gamma; // fix_gamma, bool. 在训练过程中是否固定伸缩因子gamma.
bool use_global_stats; // bool.
bool output_mean_var; // bool. 是否输出样本均值和方差.
DMLC_DECLARE_PARAMETER(BatchNormParam) {
DMLC_DECLARE_FIELD(eps).set_default(1e-3f)
.describe("Epsilon to prevent div 0"); // epsilon, DMLC_DECLARE_FIELD宏, 输入参数是eps. set_default设置初值为1e-3f,
// describe描述函数.
DMLC_DECLARE_FIELD(momentum).set_default(0.9f)
.describe("Momentum for moving average"); // momentum初值为0.9f.
DMLC_DECLARE_FIELD(fix_gamma).set_default(true)
.describe("Fix gamma while training"); // 在训练网络时, 默认固定缩放因子gamma.
DMLC_DECLARE_FIELD(use_global_stats).set_default(false)
.describe("Whether use global moving statistics instead of local batch-norm. "
"This will force change batch-norm into a scale shift operator.");
/*
对于use_global_stats, 参考(caffe+报错︱深度学习参数调优杂记+caffe训练时的问题+dropout/batch Normalization ).
use_global_stats == true时会强制使用模型中存储的BatchNorm层均值与方差参数, 而非基于当前batch内计算均值和方差.
而BatchNormlization文章介绍的BN方法, 训练过程中是基于mini-batch的. , BN是基于mini-batch的:
对于一个mini-batch的输入{x1, x2, ..., xm}, 通过这m个输入来计算mean, var. 然后计算 xi^(~), 即相当于是BN层真正的输入. 在计算
BN层的输出y^(i). 输入{x1, x2, ..., xm}是一个batch的输入, 而不是整个数据集的.
use_global_stats == true时, 就相当于是使用整个数据集的计算结果{x1, x2, ..., xN}做为BN前一层的输入.
而在测试阶段, 均值和方差已经不是针对某一个Batch了, 而是针对整个数据集而言. 因此, 在训练过程中除了正常的前向传播和反向求导
之外, 我们还要记录每一个Batch的均值和方差, 以便训练完成之后按计算整体的均值和方差.
网络前向传播, 一次性输入一个batch的数据; 然后再反向传播. 对于一个batch的数据, 网络迭代T次终止, 再进行写一个bath数据的迭代.
即, 对于每一个batch的数据, 网络迭代T次. 对于整个数据集, 网络一共迭代epoch次.
*/
DMLC_DECLARE_FIELD(output_mean_var).set_default(false)
.describe("Output All,normal mean and var"); // 默认不输出数据的均值和方差.
}
};
/*
一般BN layer放在FC和conv的后面, 因此dlib做了bn_fc和bn_con层.
*/
template<typename xpu> // 模板参数只有xpu.
class BatchNormOp : public Operator { // BatchNormOp, BN操作类.
public:
explicit BatchNormOp(BatchNormParam param) {
/*
BatchNormOp, BN操作类的构造函数: C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示
的, 而非隐式的. param是BN参数类的对象, 利用param来访问BN的参数.
*/
this->param_ = param; // BatchNormParam param_, 生成BatchNormParam结构体的一个副本.
}
virtual void Forward(const OpContext &ctx,
const std::vector &in_data,
const std::vector &req,
const std::vector &out_data,
const std::vector &aux_states) {
/*前向操作, 虚函数. 函数的实现在类中定义. 不需要返回值. 本层为第 l 层.
in_data: 本层输入data, 包括kData, kGamma, kBeta.
req: 数据操作模式.
out_data: 本层输出, out. 在训练的时候本层输出有两个.
aux_states: 表示的是为了方便计算而需要的附加的 tensor. 附加的Tensor有两个: kMovingMean, kMovingVar. 以前看的操作均没使用
aux_states来辅助计算.
*/
using namespace mshadow;
using namespace mshadow::expr;
CHECK_EQ(in_data.size(), 3);
CHECK_EQ(aux_states.size(), 2);
/*
in_data容器大小是3, 即有三个Tensor, 包括kData, kGamma, kBeta.
aux_states容器大小是2, 即有两个附加的Tensor, 包括kMovingMean, kMovingVar.
*/
if (ctx.is_train) {
CHECK_EQ(out_data.size(), 3);
CHECK_EQ(req.size(), 3);
/*
ctx是OpContext结构体定义的成员. OpContext结构体定义见include/mxnet/operator.h. 利用ctx成员访问结构变量is_train:
int is_train; // operator是在进行 train 还是 test (is_train);
在训练阶段, out_data的容器大小是3, 即根据BN层的输入, 要计算mean, var, out. 想用的数据操作模式也是3个.
*/
} else {
CHECK_GE(out_data.size(), 1);
CHECK_GE(req.size(), 1);
CHECK_EQ(req[batchnorm::kOut], kWriteTo);
/*
在网络的test/predict阶段, out_data容器大小为1. BN层的输出只有输出out. 相应的数据操作模式也是1个. 而且数据操作模式是:
kWriteTo, 即out代表的 tensor 提供的是可以直接写入的原始的内存块.
*/
}
Stream *s = ctx.get_stream();
const real_t scale = static_cast(in_data[batchnorm::kData].shape_[1]) /
static_cast(in_data[batchnorm::kData].shape_.Size());
/*
static_cast < type-id > ( expression ), C++新标准定义的四个转换符, 即static_cast, dynamic_cast, reinterpret_cast和
const_cast. static_cast该运算符把expression转换为type-id类型, 但没有运行时类型检查来保证转换的安全性. 即将expression转换成
real_t型的, 即float型.
in_data[batchnorm::kData].shape_[0]: 65 第一维是batch_size的大小.
in_data[batchnorm::kData].shape_[1]: 10 第二维是BN前一层特征图的个数.
in_data[batchnorm::kData].shape_[2]: 47
in_data[batchnorm::kData].shape_[3]: .. 第三维和第四维是数据的大小.
in_data[batchnorm::kData].shape_.Size(): 427700
如果BN层前一层是FC层, shape_[0]为batch_size; shape_[1]为FC层的结点个数, 可以这样理解, 一个结点就是一个特征图.
shape_.Size()就是in_data[batchnorm::kData]即BN层输入数据各个维度的乘积. 即输入数据的总个数.
scale是real_t类型的(float类型), 其值等于: 前一层特征图(结点)的个数 / 一个batch输入数据(BN层的输入数据)的总个数.
*/
/*cout<<"in_data[batchnorm::kData].shape_[0]: "<
Tensor4 > data; // data, xpu下的4维张量.
Tensor4> out; // out, xpu下的四维张量.
if (in_data[batchnorm::kData].ndim() == 2) { // 如果in_data[batchnorm::kData]即BN层的输入数据是2维的, 那么需要先定义dshape
// 然后再将in_data[batchnorm::kData]拉成4维的张量.
/*======================================================================================================================
BN层前为FC层, 设FC层结点个数是 num_hidden, 那mean和var的维数为num_hidden, 然后将mean扩充成 batch_size * num_hidden *
1 * 1的再执行 data - mean的操作. 即mxnet写的batch_norm-inl.h的代码对于前一层是FC层同样适用, 可以将FC的输出out扩展成
batch_size * num_hidden * 1 * 1的, 再作为BN层的输入. BN层的前一层是FC层时, 理论和实际是可以结合起来的.
如BN层前为FC层, 那么in_data[batchnorm::kData].ndim() == 2, 要想对FC的激活值使用BN操作, 就要先将FC的激活值data拉成4维的
张量, 大小为: batch_size * num_hidden *1 * 1.
*/
Shape<4> dshape = Shape4(in_data[batchnorm::kData].shape_[0],
in_data[batchnorm::kData].shape_[1], 1, 1);
/*
定义dshape, 4维shape. Shape4定义:
MSHADOW_XINLINE Shape<4> Shape4(index_t s0, index_t s1, index_t s2, index_t s3){
Shape<4> s;
s[0] = s0; s[1] = s1; s[2] = s2; s[3] = s3;
return s; }
s0 = in_data[batchnorm::kData].shape_[0], 即batch_size, dshape[0]; s1 = in_data[batchnorm::kData].shape_[1], 即BN层前一层
特征图的个数, 如果是前连接层这种的, 就是结点个数, dshape[1]; s3 = s4 =1, dshape[2], dshape[3].
*/
data = in_data[batchnorm::kData].get_with_shape4, real_t>(dshape, s);
out = out_data[batchnorm::kOut].get_with_shape4, real_t>(dshape, s);
/*
将in_data[0](输入数据)拉成4维的张量. 这里将TBlob数据拉成Tensor数据时没有使用FlatTo2D, 而是用了get_with_shape. 定义如下:
mshadow::Tensor mxnet::TBlob::get_with_shape(const mshadow::Shape & shape,
mshadow::Stream< Device > *stream = NULL)const. 给定shape, 将TBlob拉成一个Tensor. 如果shape和存储的大小不一致时, 会报错.
定义BN层的输出out, 将out_data[batchnorm::kOut]用了get_with_shape拉成4维张量.
*/
} else {
data = in_data[batchnorm::kData].get4, real_t>(s);
out = out_data[batchnorm::kOut].get4, real_t>(s);
/*
BN层的输入数据不是2维的, 就是4维的. 就直接使用get函数将in_data[batchnorm::kData], out_data[batchnorm::kOut]拉成4维的张量.
mshadow::Tensor mxnet::TBlob::get(mshadow::Stream *stream = NULL)const.
*/
}// if else语句执行的结果是类似的, 均是定义4维张量data和out. 区别是BN层的前一层, 根据输入数据的维数来确定data和out如何确定.
Tensor1> slope = in_data[batchnorm::kGamma].get1, real_t>(s); // gamma.
Tensor1> bias = in_data[batchnorm::kBeta].get1, real_t>(s); // beta.
Tensor1> moving_mean = aux_states[batchnorm::kMovingMean].get1, real_t>(s);
Tensor1> moving_var = aux_states[batchnorm::kMovingVar].get1, real_t>(s);
/*
利用get函数将:
in_data[batchnorm::kGamma]即in_data[1]拉成1维的张量, 即向量. slope. 是原文中的gamma.
in_data[batchnorm::kBeta]即in_data[2]拉成1维的张量. bias. 是算法中的beta.
aux_states[batchnorm::kMovingMean]即aux_states[0]拉成1维的张量, moving_mean.
aux_states[batchnorm::kMovingVar]即aux_states[1]拉成1维的张量, moving_var.
aux_states容器中的数据是做辅助计算的. 获取moving average, 如果use_global_stats == true, 那么就要使用 moving average.
moving_mean和moving_var有初值, 在反向传播过程中会根据BN层的输出均值mean和方差var进行更新.
*/
if (param_.fix_gamma) slope = 1.f; // 如果再训练阶段固定gamma, 那么就直接令slope = 1.f.
/*
求BN层输入的均值mean和方差var是基于mini-batch的, 即让一个batch的输入数据{x1, x2, ..., xm}具有0均值, 1方差. 不针对单个样本!
即不是对一个样本的输入xi, 进行求均值mean和方差var: mean = 1/n * (sum( xij )), 再计算yi.
mean = 1 / m * (sum( xi )), xi是一个batch中BN层的输入.
BN层的输入数据是xi, 输出数据是yi, 中间变量时xi^(^).
*/
/*====================================================================================================================
一般BN layer放在FC和conv的后面, 因此dlib做了bn_fc和bn_con层. BN层的输入data是4维的张量, 输出也是4维的张量.
1>BN层前为conv层, 设卷积层特征图的个数是n个, 那么mean和var是n维的向量, 与特征图的个数是相对应的; 然后再计算 xi^(^)时,
先将mean扩充成 batch_size * n * Nh * Nw(Nh是卷积层特征图的高度, Nw是卷积层特征图的宽度); 然后才可以进行 data - mean的
操作. 但是从 batch * n个特征图得到mean]和var的过程没太想明白.
2>BN层前为FC层, 设FC层结点个数是 num_hidden, 那mean和var的维数为num_hidden, 然后将mean扩充成 batch_size * num_hidden *
1 * 1的再执行 data - mean的操作. 即mxnet写的batch_norm-inl.h的代码对于前一层是FC层同样适用, 可以将FC的输出out扩展成
batch_size * num_hidden * 1 * 1的, 再作为BN层的输入. BN层的前一层是FC层时, 理论和实际是可以结合起来的.
对于前一层是FC层, BN层的输入数据是2维的: batch_size * num_hidden. 因此要将输入data先拉成batch_size * num_hidden * 1 * 1的.
输入data和输出out的大小是一致的;
对于前一层是卷积层, 则对data直接使用BN操作即可.
前向传播是这样, 反向传播也是这样.
======================================================================================================================*/
// whether use global statistics
if (ctx.is_train && !param_.use_global_stats) { // 网络在训练阶段. 而且不使用use global statistics. 即在训练阶段不使用
// use_global_stats, 否则网络不能收敛. 训练阶段基于mini-batch做BN处理, 针对当前 mini-batch 计算期望和方差.
Tensor1> mean = out_data[batchnorm::kMean].get1, real_t>(s);
Tensor1> var = out_data[batchnorm::kVar].get1, real_t>(s);
/*利用get函数将:
out_data[batchnorm::kMean]即out_data[1]拉成1维的张量. 保存BN输入的计算均值(激活值的均值).
out_data[batchnorm::kVar]即out_data[2]拉成1维的张量. 保存BN输入的计算方差(激活值的方差).
*/
/*====================================================================================================================
1)mean和var均是1维的张量, 即向量. 虽然是向量, 但是可以当做标量来用, 即 mean = 1.f是正确的.
======================================================================================================================*/
CHECK(req[batchnorm::kMean] == kNullOp || req[batchnorm::kMean] == kWriteTo);
CHECK(req[batchnorm::kVar] == kNullOp || req[batchnorm::kVar] == kWriteTo);
/*
BN输入的计算均值和方差的数据操作模式是kNullOp或者kWriteTo(tensor可以直接写入的原始的内存块).
*/
// The first three steps must be enforced.
mean = scale * sumall_except_dim<1>(data);
var = scale * sumall_except_dim<1>(F(
data - broadcast<1>(mean, data.shape_)));
/*
在网络的训练阶段, 首先基于mini-batch计算BN输入的均值mean和方差var. scale是real_t类型的
(float类型), 其值等于: 前一层特征图(结点)的个数 / 一个batch输入数据(BN层的输入数据)的总个数. 例如对于BN层前一层是FC层,
scale = 1 / batch_size; 对于卷积层scale = 1 / (batch_size * 输入数据维数乘积).
data是BN层的输入数据, 包含了一个batch的数据. data是4维的张量, data[0]是batch_size, 即样本个数; data[1]是一个样本的
channel, 是1还是3(3维的不能计算); data[2]是一个样本的高度(矩阵的行数); data[3]是一个样本的宽度(矩阵的列数).
1)mean:
mean = scale * sumall_except_dim<1>(data); scale是一个数, 扮演原文Algorithm1中的 1 / m.
sumall_except_dim定义见mshadow/mshadow/extension/reduceto1d.h44行:
template
inline ReduceTo1DExp::kDim - dimkeep> sumall_except_dim(const Exp &exp){...}. sumall_except_dim的功能是对除dimkeep维度外, 所有exp的维度进行求和.
返回expresion with type Tensor. 参数:
exp: 输入表达式, 必须是一个Tensor, 即一个矩阵.
dimkeep: 需要保留的exp维度. 维度从0开始计算.
sumall_except_dim<1>(data)即对一个batch的所有数据求和(不管data[1]), 是数据矩阵的和. 即sum( xi ),
xi对应BN层的输入数据矩阵. sum( xi )即矩阵的加法.
这句代码执行的就是: mean = (1 / m) * sum( xi ).
2)var:
该句代码执行的就是:
var = (1 / m) * sum( xi - mean).
scale是一个数, 扮演原文Algorithm1中的 1 / m.
sum( xi - mean)为: sumall_except_dim<1>(F(data - broadcast<1>(mean, data.shape_))).
F(a)是一个单目运算符, 运算符是mshadow_op::square, 见src/operator/mshadow_op.h下的struct square,
输入DType a, return DType(a * a). 其中a是: data - broadcast<1>(mean, data.shape_), 即 xi - mean, BN层的每个输入 - batch
个样本输入的均值.
broadcast<1>(mean, data.shape_), broadcast见: mshadow/mshadow/extension/broadcast.h 69行:
template
inline Broadcast1DExp broadcast(const expr::Exp &src,
Shape shape) {..}.
src Tensor; shape: shape of output; 返回 a expresion with type Tensor, dimdst为4,
返回的Tensor的维数为4, 和shape的个数是有关的.
* input: Tensor: ishape[0]
* output: Tensor : oshape[dimcast] = ishape[0].
将一个1维的 Tensor 扩充成 dimdst 维的 Tensor. 为了正确计算!!
mean是几维的Tensor才能正确说明问题!!
为了计算xi - mean, BN层的每个样本的激活值 - batch个激活值的均值. 进行的操作是: data - broadcast<1>(mean, data.shape_),
因此需要将mean扩充到和data一样大小才能进行正确地减法. broadcast<1>(mean, data.shape_)就是将mean(1维的Tensor)扩充成和
data一样大小的Tensor, 即Tensor. 即 (broadcast<1>(mean, data.shape_))[0]为Batch_size;
(broadcast<1>(mean, data.shape_))[1]为channel; (broadcast<1>(mean, data.shape_))[2]为data的高度;
(broadcast<1>(mean, data.shape_))[3]为data的宽度. 因此, data - broadcast<1>(mean, data.shape_)就是
BN层的每个样本的激活值 - batch个激活值的均值, 即x1 - mean, x2 - mean, ..., xm - mean.
然后对data - broadcast<1>(mean, data.shape_)平方做和再取scale即可. 求和时和求mean时的做法一致, 可以看做是
(x1 + mean) + (x2 + mean) + ... + (xm - mean).
*/
Assign(out, req[batchnorm::kOut], broadcast<1>(slope, out.shape_) *
(data - broadcast<1>(mean, data.shape_)) /
F(broadcast<1>(var + param_.eps, data.shape_)) +
broadcast<1>(bias, out.shape_));
/*
Assign赋值操作, out是BN层的输出, req是数据操作模式, exp即 gamma * [(data - mean) / (var + eps)^(1/2)] + beta,
gamma即slope, beta即bias.
exp为: broadcast<1>(slope, out.shape_) * (data - broadcast<1>(mean, data.shape_))
/ F(broadcast<1>(var + param_.eps, data.shape_))
+ broadcast<1>(bias, out.shape_)
首先将slope扩充成和out具有相同shape的Tensor(gamma); 再乘(data - broadcast<1>(mean, data.shape_)), 即data - mean;
然后F是一个单目运算符, 运算符是mshadow_op::square_root, 结构体mshadow_op::square_root输入
(DType a, 返回DType(sqrtf(a)), 即float型的a^(1/2), 即对broadcast<1>(var + param_.eps, data.shape_)做开放操作,
broadcast<1>(var + param_.eps, data.shape_)即(var + eps), 这里, var是1维的张量, 可以当做标量用, 因此var + eps有效.
(var + eps)的结果还是Tensor的, 因此再将(var + eps)扩充成和data具有一样shape的Tensor.; 最后加上beta,
即broadcast<1>(bias, out.shape_), 将bias扩充成和out具有一样shape的Tensor.
*/
} else {
/*
在train阶段, 对每一个minibatch使用BN, 那么, 在test/predict的时候怎, 常见的做法是使用整个train-set计算出mean.
由于train-set的数据量非常大, 计算mean计算量非常大, 所以经常采用的技术是使用moving average算法, 在为此在训练过程中需要记录
每一个Batch的均值和方差, 以便训练完成之后按照下式计算整体的均值和方差:
E[x] = Eb[meanb]; Var[x] = (m / (m - 1)) * Eb[varb].
meanb是第b个batch的mean, varb是第b个batch的var.
在test/predict阶段, 或者是use_global_stats == true时(这两者其实可以看成是一种情况, 在训练阶段, use_global_stats == false
否则网络是不收敛的). 使用moving average算法来估计整个测试集的mean和var.
在统计学中, moving average算法是通过创建数据集的一系列不同子集的均值来分析数据的.
MovingAverage可翻译为滑动平均或移动平均, 是做时间序列预测时用到的简单方法.
计算方法: 对于一个给定的数列, 首先设定一个固定的值k, 然后分别计算第1项到第k项, 第2项到第k+1项, 第3项到第k+2项的平均值,
依次类推.
*/
Assign(out, req[batchnorm::kOut], broadcast<1>(slope /
F(moving_var + param_.eps),
data.shape_) * data +
broadcast<1>(bias - (slope * moving_mean) /
F(moving_var + param_.eps), data.shape_));
/*
Assign赋值操作, out是BN层的输出, req是数据操作模式, exp即 gamma / (var[x] + eps)^(1/2) * x +
(beta - gamma * E[x] / (var[x] + eps)^(1/2)). 代码实现时, x即data, 为了使得能和data进行计算, 要对一些式子进行扩展, 扩展成
和data具有同样大小的shape. 由于使用了moving average算法, 因此用 moving_var 替代var, 用moving_mean替代mean. 将式子写为:
*1 + *2.
slope即gamma, bias即beta.
令 a = F(moving_var + param_.eps), F即单目开方运算, moving_var是1维张量,
和eps相加. 然后利用broadcast<1>(), 将 slope / a 扩展成和data具有同样shape的Tensor, 即
broadcast<1>(slope / a, data.shape_), 然后再和 data 相乘, 即可得 *1.
令 b = F(moving_var + param_.eps), data.shape_), 这和a是一样的. 然后执行
bias - (slope * moving_mean) / b, 再将结果用broadcast<1>()展成和data具有同样shape的Tensor, 即 *2.
*/
}
}
virtual void Backward(const OpContext &ctx,
const std::vector &out_grad,
const std::vector &in_data,
const std::vector &out_data,
const std::vector &req,
const std::vector &in_grad,
const std::vector &aux_states) {
/*BN层(第l层)有参数gamma和beta, 因此要计算的是损失J关在BN层(第l层)的残差, gamma的梯度和beta的梯度.
!!!!!!!!!!!!!!!!梯度可以看做是损失J关于层参数的导数, 残差可以看做是损失J关于层输入的导数!!!!!!!!!!!!!!!!!!!!!!!!!!!!
in_grad输出残差/梯度参数, 向量容器, 每个元素的类型是TBlob. 本层(第l层)的.
out_grad输入残差/梯度参数, 向量容器, 每个元素的类型是TBlob. 上一层(第l + 1层)的残差/梯度, 计算本层的残差/梯度.
in_data输入参数, 向量容器, 每个元素的类型是TBlob. 本层(第l层)的输入.
out_data输出参数, 向量容器, 每个元素的类型是TBlob. 本层(第l层)的输出.
req: 数据操作模式, 向量数组. 元素类型是OpReqType.
aux_states: 表示的是为了方便计算而需要的附加的 tensor. 附加的Tensor有两个: kMovingMean, kMovingVar. 以前看的操作均没使用
aux_states来辅助计算.
*/
/*====================================================================================================================
对BN层的求导可以发现, 有很多中间变量会重复使用. 这些中间变量可以单独算出来. 不过这也涉及到一个计算速度和存储之间的平衡
问题.
*/
using namespace mshadow;
using namespace mshadow::expr;
CHECK_EQ(out_grad.size(), param_.output_mean_var ? 3 : 1);
// bool output_mean_var; 是否输出样本均值和方差. 上一层的输入残差, 如果output_mean_var == true, out_grad有三个变量: 梯度,
// mean, var; 否则, out_grad只有残差这一个.
CHECK_EQ(in_data.size(), 3); // BN层输入有三项, data输入, gamma, beta.
CHECK_EQ(out_data.size(), 3); // BN层输入有三项: out输出, mean均值, var方差.
CHECK_EQ(in_grad.size(), 3); // BN层的残差有三项, gslope即gamma的残差, gbias即beta的残差, grad_in即损失关于BN层的残差.
// grad_in, 损失J关于BN层输出的残差, 这个残差并不会对下一次的FC层的前向传播产生影响, 但是会利用gdata计算BN
// 层前一层(第l - 1)层的残差.
Stream *s = ctx.get_stream();
Tensor4> data, grad, grad_in; // 定义data, grad, grad_in. xpu下的4维张量. 下面会对这三个变量进行赋值.
const real_t scale = static_cast(out_grad[batchnorm::kOut].shape_[1]) /
static_cast(out_grad[batchnorm::kOut].shape_.Size()); // real_t scale, 与Foeward的一样.
// 一层特征图(结点)的个数 / 一个batch输入数据(BN层的输入数据)的总个数. 例如对于BN层前一层是FC层,
// scale = 1 / batch_size; 对于卷积层scale = 1 / (batch_size * 输入数据维数乘积).
if (in_data[batchnorm::kData].ndim() == 2) { // BN层的输入数据in_data[batchnorm::kData是2维的, 调用TBol下的ndim成员函数,
// 返回TBlob对象的维数.
/*
如BN层前为FC层, 那么in_data[batchnorm::kData].ndim() == 2, 要想对FC的激活值使用BN操作, 就要先将FC的激活值data拉成4维的
张量, 大小为: batch_size * num_hidden * 1 * 1. 反向传播时是一样的, 也要分输入数据是2维的还是4维的,
*/
Shape<4> dshape = Shape4(out_grad[batchnorm::kOut].shape_[0],
out_grad[batchnorm::kOut].shape_[1], 1, 1); // 定义Shape<4>的dshape,
// 大小为: batch_size * num_hidden * 1 * 1.
data = in_data[batchnorm::kData].get_with_shape4, real_t>(dshape, s);
grad = out_grad[batchnorm::kOut].get_with_shape4, real_t>(dshape, s);
grad_in = in_grad[batchnorm::kData].get_with_shape4, real_t>(dshape, s);
/*
对420行定义Tensor 对象data, grad, grad_in进行赋值和定义操作.
data: BN层的输入数据, 因为in_data[batchnorm::kData]是2维的数据, 因此调用TBlob的get_with_shape函数, 传入dshape即
大小为: batch_size * num_hidden * 1 * 1的shape, 将BN层输入扩展成4维的Tensor.
grad: BN上一层(第l + 1)层的残差, 因为in_data[batchnorm::kData]是2维的数据, 因此out_grad[batchnorm::kOut]也是二维的. 因此
先扩展成4维的Tensor.
grad_in: BN层的残差. in_grad[batchnorm::kData]也是2维的Tensor, 先扩展为4维的.
*/
} else {
data = in_data[batchnorm::kData].get4, real_t>(s);
grad = out_grad[batchnorm::kOut].get4, real_t>(s);
grad_in = in_grad[batchnorm::kData].get4, real_t>(s);
/*
如果in_data[batchnorm::kData].ndim()不是2维的数据, 那么就是4维的. 利用get函数直接将in_data[batchnorm::kData]等拉成4维的
张量即可.
*/
} // 这和前向传播的操作基本是类似的.
Tensor1> mean = out_data[batchnorm::kMean].get1, real_t>(s);
Tensor1> var = out_data[batchnorm::kVar].get1, real_t>(s);
Tensor1> slope = in_data[batchnorm::kGamma].get1, real_t>(s);
/*
利用get函数将:
out_data[batchnorm::kMean]即out_data[1], BN层的输出均值mean拉成1维的Tensor, mean向量.
out_data[batchnorm::kVar]即out_data[2], BN层的输出方差Var拉成1维的Tensor. var.
in_data[batchnorm::kGamma]即in_data[1], BN层的gamma参数拉成1维的Tensor, slope.
*/
// Tensor bias = in_data[kBeta].get(s);
Tensor1> gslope = in_grad[batchnorm::kGamma].get1, real_t>(s);
Tensor1> gbias = in_grad[batchnorm::kBeta].get1, real_t>(s);
/*
BN层的残差有三项, gslope即gamma的残差, gbias即beta的梯度, grad_in即损失关于BN层的梯度.
slope和bias在前向传播时, 是1维的Tensor, 因此在反向传播中, 其残差也是1维的张量.
in_grad[batchnorm::kGamma]即in_grad[1], 损失J关于gamma的残差, 是1维的张量.
in_grad[batchnorm::kBeta]即in_grad[2], 损失J关于BN层beta参数的残差, 是1维的张量.
*/
// update moving avg
Tensor1> moving_mean = aux_states[batchnorm::kMovingMean].get1, real_t>(s);
Tensor1> moving_var = aux_states[batchnorm::kMovingVar].get1, real_t>(s);
/*
aux_states[batchnorm::kMovingMean]即aux_states[0]拉成1维的张量, moving_mean.
aux_states[batchnorm::kMovingVar]即aux_states[1]拉成1维的张量, moving_var.
aux_states容器中的数据是做辅助计算的. 获取moving average, 如果use_global_stats == true, 那么就要使用 moving average.
*/
if (param_.fix_gamma) slope = 1.f; // 如果gamma是一个定值, 那么slope(gamma)就是1.f.
if (ctx.is_train && !param_.use_global_stats) { // 在网络的训练阶段且不使用use_global_stats.
// 在test/predict阶段, 或者是use_global_stats == true时(这两者其实可以看成是一种情况, 训练时, use_global_stats == false.
// 否则网络是不收敛的). 再使用moving average算法来估计整个测试集的mean和var.
/*
get requested temp space. 获取所需的临时空间.
有些操作需要额外的内存作为工作空间进行计算, 比如说BatchNormBackward. 这种情况下, 系统最好可以对这部分内存进行管理,
这样系统可以做一些优化, 比如说内存的重复利用. 因此BN有kTempSpace. 即BN的反向操作会申请一个临时的资源空间, 这个空间任意.
*/
Tensor2> workspace = ctx.requested[batchnorm::kTempSpace].get_space(
mshadow::Shape2(3, mean.shape_[0]), s);
/*
OpContext: 结构体, 定义在include/mxnet/operator.h中, 该结构体可以记录操作在前向和后向传播中的信息. ctx是结构体OpContext定
义的对象, requested是OPContext结构体下的函数:
// brief Resources requested by the operator
std::vector requested; // 用来返回操作所需的资源.
ctx.requested返回的是一个向量容器, ctx.requested[batchnorm::kTempSpace]即ctx.requested[0]返回一个Resource对象, 然后
Resource对象再调用get_space函数.
get_space函数定义见: include/mxnet/resource.h 90行: get_space函数是定义在Resource结构体下的函数:
template
inline mshadow::Tensor get_space(mshadow::Shape shape, mshadow::Stream *stream)const{...}
get_space用来获取Tensor所需的空间. 参数shape: 返回Tensor的Shape; stream: Device下的Tensor; 返回所需的Tensor.
此处, shape是Shape2(3, mean.shape_[0]), 第一维是3, 第二维是mean.shape_[0], BN前一层为FC层时, 为num_hidden结点个数; 为
卷积层时, 为特征图的个数. stream是xpu下的对象s. shape是Shape2, 即Shape<2>, 因此ndim是2, 故返回所需的Tensor是2维的.
workspace即为BN反向传播所需的2维的Tensor, 是一个临时空间, 额外内存.
*/
Tensor1> gmean = workspace[0];
Tensor1> gvar = workspace[1];
Tensor1> tmp = workspace[2];
/*
1维的Tensor gmean, gvar, tmp. 用workspace, BN层反向传播的临时Tensor定义. 利用gmean, gvar, tmp是损失关于参数gamma, beta
的梯度, 然后可以用gmean, gvar来计算损失J关于BN层输入的残差.
输出workspace.shape_.Size()为3, workspace.shape_[0]为3, workspace.shape_[1]为1, workspace.shape_[2]为1.
3是在定义workspace时的Shape2的第一个参数. 即Tensor的第0个位置的元素均代表的是大小.
*/
moving_mean = moving_mean * param_.momentum + mean * (1 - param_.momentum);
moving_var = moving_var * param_.momentum + var * (1 - param_.momentum);
/*
使用moving average算法, 更新mean和var, 用于test/predict. momentum是moving average的动量项, float, 初值是0.9f.
moving_mean和moving_var有初值, 在反向传播过程中会根据BN层的输出均值mean和方差var进行更新. 在测试的前向传播过程中, 利用
moving_mean和moving_var来代替整个测试集的均值和方差.
更新规则即: a = a * momentum + a * (1 - momentum), momentum是moving average的动量项, float, 初值是0.9f.
moving_mean和moving_var均按照这个规则来更新.
*/
/*
计算gmean和gvar, gmean和gvar用来计算损失J关于BN层输出的残差! gvar方差的梯度, gmean是均值的梯度.
根据原文, 设网络的损失为l, 那么需要计算一下偏导:
partial(l) / partial(xi^(^)) == partial(l) / partial(yi) * gamma.
partial(l) / partial(varb), gvar.
partial(l) / partial(meanb), gmean.
partial(l) / partial(xi)
partial(l) / partial(gamma), gslope.
partial(l) / partial(beta), gbias.
varb即第b个batch个样本BN层的输出方差, meanb即第b个batch个样本BN层的输出均值.
*/
gvar = sumall_except_dim<1>((grad * broadcast<1>(slope, data.shape_)) *
(data - broadcast<1>(mean, data.shape_)) *
-0.5f *
F(broadcast<1>(var + param_.eps, data.shape_),
-1.5f));
/*
计算损失关于方差var的梯度, 根据原文为: partial(l) / partial(varb) =
sum{ [partial(l) / partial(xi^(^))] * [(xi - meanb)] * -0.5 * (varb + eps)^(-3/2) }.
sum{ [*1] * [*2] * -0.5 * (*3) }.
而 [partial(l) / partial(xi^(^))] == partial(l) / partial(yi) * gamma. yi是BN层的输出, 即下一层的输入, 又残差是损失关于
输入的导数, 因此 partial(l) / partial(yi) 就是BN上一层(第l + 1)层的残差. 这个残差即grad, 是将out_grad[0]拉成4维Tensor.
因此, *1 就是grad * gamma. 因此要对slope(gamma)进行扩展, slope即BN层的gamma参数, 由于BN层的输入xi和yi的shape相同, 因此
grad和BN层输入data的shape相同, 对slope进行扩展, 即将slope这个1维的Tensor扩展成和BN层输入数据data具有一样shape的Tensor.
broadcast<1>(slope, data.shape_)是扩展后的slope. 最后 *1 = grad * broadcast<1>(slope, data.shape_).
*2 = (xi - meanb). 由于是批处理, xi即data, 因此为了正确执行(xi - meanb), 要对meanb进行扩展. mean是BN层的输出均值, 为1维
的Tensor, 因此需要将mean扩展成和data具有相同shape的Tesnor, 即4维的Tenso. *2 = data - broadcast<1>(mean, data.shape_).
*3 = (varb + eps)^(-3/2). 首先F(*11, *21)是双目运算符, 运算符是mshadow_op::power, 输入DType a, DType b
返回powf( a, b ). *21为-1.5f, 即float型的1.5.
*11是broadcast<1>(var + param_.eps, data.shape_), 即对var + param_.eps进行扩展, 扩展成和data具有相同shape的Tesnor,
即4维的Tensor. var + param_.eps的运算结果还是1维的Tensor.
最后再对[*1] * [*2] * -0.5 * (*3)求和, 不管第一个维度, 对所有维度进行求和. 即对batch_size维度, 数据高度维度, 宽度维度
求和.
*/
gmean = sumall_except_dim<1>(grad * broadcast<1>(slope, data.shape_));
gmean *= -1.0f / F(var + param_.eps);
tmp = scale * sumall_except_dim<1>(-2.0f * (data - broadcast<1>(mean, data.shape_)));
tmp *= gvar;
gmean += tmp;
/*计算损失关于均值mean的偏导数, 根据原文为: partial(l) / partial(meanb) =
sum{ [partial(l) / partial(xi^(^))] * [-1 / (varb + eps)^(1/2)] }
+ { [partial(l) / partial(varb)] * sum{ -2* [(xi - meanb)]} / m }. 即:
sum{ *1(求gvar时的*1) } * [*2] + { gvar * [*3] }. 由于gmean求时, 项比较多, 所以分开来求.
首先令gmean = sum{ *1 }, *1为求gvar时的*1. 然后求和, 不管第一个维度, 对所有维度进行求和. 即对batch_size维度, 数据高度
维度, 宽度维度求和.
*2 = [-1 / (varb + eps)^(1/2)]. 这里计算varb + eps的结果是1维的Tensor. F()是单目开方运算. 1维的
Tensor可以看做是一个标量, 因此用-1f / F(). *2 还是一个1维的Tensor, 因此可以看做是一个标量,
最后利用 gmean * (*2)即可!!
gmean = gmean * (*2)是 + 前的第一项.
*3 = sum{ -2* [(xi - meanb)]} / m. 其中1/m用scale代替, 这和前向传播中的操作一样. 由于是批处理, 因此xi即data, 为了计算
data - mean, 要对mean即BN层的输出均值进行扩展, 扩展成和data具有相同shape的Tesnor, 即4维的Tensor, 这样就可以计算
data - mean. 再乘上 -2.0f, 然后和, 不管第一个维度, 对所有维度进行求和. 即对batch_size维度, 数据高度维度, 宽度维度求和.
*3 即tmp, tmp是1维的Tensor. 即 sumall_except_dim<1> 的计算结果是返回1维的Tenso.
*3 * gvar即是 + 后面的那一项, 即tmp.
因此, 损失关于BN层输出均值mean的梯度就是 gmean = gmean + tmp.
*/
// assign
if (!param_.fix_gamma) { // 如果没有固定gamma值, 来计算损失J关于参数gamma的梯度.
Assign(gslope, req[batchnorm::kGamma],
sumall_except_dim<1>(
grad * (data - broadcast<1>(mean, data.shape_)) /
F(broadcast<1>(var + param_.eps, data.shape_))));
/*
Assign赋值操作, gslope是损失关于BN层参数gamma的梯度; req是数据操作模式, 是kGamma的数据操作模式; exp, 根据原文:
partial(l) / partial(gamma) = sum{ [partial(l) / partial(yi)] * xi^(^) }. 即:
sum{ grad * xi^(^) }, xi^(^)是中间计算结果, 需要根据xi, mean, var算出来!!
xi^(^) = [xi - meanb] / [varb + eps]^(1/2). 由于是批处理操作, xi即data. 因此想进行[xi - meanb], 需要扩展BN层的输出均值
mean, 扩展成和data具有相同shape的Tesnor, 即4维的Tensor, 这样就可以计算data - mean.
F(*)是单目开方运算. *是[varb + eps], 即先对BN层的输出方差var和eps求和, 然后再对这个1维Tensor
进行扩展, 扩展成和data具有相同shape的Tesnor, 即4维的Tensor. 再做开方计算. 这样就可以得到xi^(^), 即data^(^).
最后对 grad * data^(^)进行求和, 管第一个维度, 对所有维度进行求和. 即对batch_size维度, 数据高度维度, 宽度维度求和.
*/
} else { // 固定了gamma, 即slope = 1.0f后, 损失关于gamma的梯度是0.0f. 因为gamma已经是定值了!!
Assign(gslope, req[batchnorm::kGamma], 0.0f);
}
Assign(grad_in, req[batchnorm::kData],
(grad * broadcast<1>(slope, data.shape_)) *
broadcast<1>(1.0f / F(var + param_.eps), data.shape_) +
broadcast<1>(gvar, data.shape_) * scale * 2.0f * (data - broadcast<1>(mean,
data.shape_)) +
broadcast<1>(gmean, data.shape_) * scale);
/*
Assign赋值操作, grad_in是损失关于BN层输入的梯度; req是数据操作模式, 是kData的数据操作模式;
计算损失关于BN层的输入xi(data)的梯度, 根据原文:
partial(l) / partial(xi) == grad_in = [partial(l) / partial(xi^(^))] * [1 / (varb + eps)^(1/2)]
+ [partial(l) / partial(varb)] * 2 * [(xi - meanb)] / m
+ [partial(l) / partial(meanb)] /m. 即:
[grad * gamma] * [1 / (varb + eps)^(1/2)] + [partial(l) / partial(varb)] * 2 * [(xi - meanb)] / m
+ [partial(l) / partial(meanb)] /m.
[grad * gamma]上面已经求过了, 将slope扩展成和data具有一样shape的Tensor即可.
[1 / (varb + eps)^(1/2)]前面已经求过, 只是将-1转换为1即可. (varb + eps)结果是1维的Tensor, 可做标量用. 还有一点, 由于计算
grad_in时, 用到了grad, 其shape和data的shape一致. 因此, 所有的变量均需要扩展成和data具有一样shape的Tensor, 即4维的张量.
[partial(l) / partial(varb)]即gvar, 再将gvar这个1维的张量扩展成和data具有一样shape的Tensor, 即4维的张量.
1/m用scale替换, (xi - meanb)上面也已经算过了. xi即data, 需要扩展meanb.
[partial(l) / partial(meanb)] 即gmean, 再将这个1维的张量扩展成和data具有一样shape的Tensor, 即4维的张量. 1/m用scale替换.
*/
Assign(gbias, req[batchnorm::kBeta], sumall_except_dim<1>(grad));
/*
Assign赋值操作, gbias是损失关于BN层参数beta的梯度; req是数据操作模式, 是kBeta的数据操作模式;
计算损失关于beta的梯度, 根据原文:
partial(l) / partial(beta) = sum{ partial(l) / partial(yi) }. partial(l) / partial(yi)即grad, 是损失关于BN层输出,
第l + 1层的输入的残差. 然后求和, 不管第一个维度, 对所有维度进行求和. 即对batch_size维度, 数据高度维度, 宽度维度求和.
*/
} else {
// use global statistics with freeze moving mean and var. 在测试阶段或者使用use global statistics时的反向传播!
if (!param_.fix_gamma) { // 如果没有固定gamma值, 来计算损失J关于参数gamma的梯度.
Assign(gslope, req[batchnorm::kGamma],
sumall_except_dim<1>(
grad * (data - broadcast<1>(moving_mean, data.shape_)) /
F(broadcast<1>(moving_var + param_.eps, data.shape_))));
/*损失关于gamma的梯度.
Assign赋值操作, gslope是损失关于BN层参数gamma的梯度; req是数据操作模式, 是kGamma的数据操作模式; exp为:
在测试阶段使用use global statistics时, 利用moving average算法, 即涉及到var用moving_var代替.
在测试阶段使用use global statistics时, 损失关于BN参数gamma的梯度和训练阶段时类似的, 只是var用moving_var代替.
*/
} else { // 固定了gamma, 即slope = 1.0f后, 损失关于gamma的梯度是0.0f. 因为gamma已经是定值了!!
Assign(gslope, req[batchnorm::kGamma], 0.0f);
}
Assign(gbias, req[batchnorm::kBeta], sumall_except_dim<1>(grad));
/*损失关于beta的梯度.
Assign赋值操作, gbias是损失关于BN层参数beta的梯度; req是数据操作模式, 是kBeta的数据操作模式;
计算损失关于beta的梯度, 和训练阶段且不使用use global statistics的反向传播是一样的!
*/
Assign(grad_in, req[batchnorm::kData], (grad * broadcast<1>(slope, data.shape_)) *
broadcast<1>(
1.0f / F(moving_var + param_.eps), data.shape_));
/*
Assign赋值操作, grad_in是损失关于BN层输入的梯度; req是数据操作模式, 是kData的数据操作模式;
在测试阶段使用use global statistics时, 损失关于BN层输入的残差计算如下:
detla^(l + 1) = partial(l) / partial(xi^(^)) * [ 1 / (Var[x] + eps)^(1/2)].
而partial(l) / partial(xi^(^)) = partial(l) / partial(yi) * gamma. 前面已经求过了.
Var[x]是对于整个测试集来说的方差, 这里用 moving_var 估计.
由于涉及到grad, 即损失关于第l + 1层输入的残差, 其shape和BN层输入data的shape一致. 因此要将所有的量扩展成和data具有相同
shape的Tensor, 即4维的张量.
*/
}
}
private:
BatchNormParam param_;
}; // class BatchNormOp