Operators in MXNet-BatchNorm

Operators in MXNet-BatchNormOperatorsinMXNet-BatchNorm

大家好,又见面了,我是你们的朋友全栈君。

       本篇文章将对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 <dmlc/logging.h> // mxnet的日志头文件. 在dmlc-core/include/dmlc下, 
#include <dmlc/parameter.h> // mxnet的参数头文件, 在dmlc-core/include/dmlc下, 定义参数的. 
#include <mxnet/operator.h> // 在include/mxnet下, 定义操作基类(operator), 操作属性类, 方法等. 对OP或Prop的函数进行声明. 
#include <map> // 关联式容器, 元素的值与某个特定的键相关联, 而并非通过元素在数组中的位置类获取. 
#include <vector> // 向量容器. 
#include <string> // 字符串. 
#include <utility> // utility头文件定义重载的关系运算符, 简化关系运算符的写入, 还定义了pair类型,
// pair类型是一种模板类型, 可以存储一对值. 
#include "./operator_common.h" // src/operator下, mxnet的层一些常用的属性.
#include "./mshadow_op.h" // src/operator下, 定义了一些结构体. 这些结构体用来接收数据实现某些层的前向输出和反向输出, 如激活函数 
// 层有softplus, softplus_grad. 一个计算前向的输出, 一个计算反向的输出. 

#include <iostream>

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<xpu> object kTempSpace, // request temporay space }; Type type; }; */ 
}  // namespace batchnorm

struct BatchNormParam : public dmlc::Parameter<BatchNormParam> { // 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<TBlob> &in_data,
                       const std::vector<OpReqType> &req,
                       const std::vector<TBlob> &out_data,
                       const std::vector<TBlob> &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<xpu> *s = ctx.get_stream<xpu>();
    const real_t scale = static_cast<real_t>(in_data[batchnorm::kData].shape_[1]) /
                         static_cast<real_t>(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]: "<<in_data[batchnorm::kData].shape_[0]<<endl; cout<<"in_data[batchnorm::kData].shape_[1]: "<<in_data[batchnorm::kData].shape_[1]<<endl; cout<<"in_data[batchnorm::kData].shape_[2]: "<<in_data[batchnorm::kData].shape_[2]<<endl; cout<<"in_data[batchnorm::kData].shape_[3]: "<<in_data[batchnorm::kData].shape_[3]<<endl; cout<<"in_data[batchnorm::kData].shape_.Size(): "<<in_data[batchnorm::kData].shape_.Size()<<endl;*/

    Tensor<xpu, 4> data; // data, xpu下的4维张量. 
    Tensor<xpu, 4> 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_shape<xpu, 4, real_t>(dshape, s);
      out = out_data[batchnorm::kOut].get_with_shape<xpu, 4, real_t>(dshape, s);
      /* 将in_data[0](输入数据)拉成4维的张量. 这里将TBlob数据拉成Tensor数据时没有使用FlatTo2D, 而是用了get_with_shape. 定义如下: mshadow::Tensor<Device, dim, DType> mxnet::TBlob::get_with_shape(const mshadow::Shape<dim> & 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].get<xpu, 4, real_t>(s);
      out = out_data[batchnorm::kOut].get<xpu, 4, real_t>(s);
      /* BN层的输入数据不是2维的, 就是4维的. 就直接使用get函数将in_data[batchnorm::kData], out_data[batchnorm::kOut]拉成4维的张量. mshadow::Tensor<Device, dim, DType> mxnet::TBlob::get(mshadow::Stream<Device> *stream = NULL)const. */
    }// if else语句执行的结果是类似的, 均是定义4维张量data和out. 区别是BN层的前一层, 根据输入数据的维数来确定data和out如何确定. 

    Tensor<xpu, 1> slope = in_data[batchnorm::kGamma].get<xpu, 1, real_t>(s); // gamma. 
    Tensor<xpu, 1> bias = in_data[batchnorm::kBeta].get<xpu, 1, real_t>(s); // beta. 
    Tensor<xpu, 1> moving_mean = aux_states[batchnorm::kMovingMean].get<xpu, 1, real_t>(s);
    Tensor<xpu, 1> moving_var = aux_states[batchnorm::kMovingVar].get<xpu, 1, 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 计算期望和方差. 
      Tensor<xpu, 1> mean = out_data[batchnorm::kMean].get<xpu, 1, real_t>(s);
      Tensor<xpu, 1> var = out_data[batchnorm::kVar].get<xpu, 1, 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<mshadow_op::square>(
          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<int dimkeep, typename SrcExp, typename DType, int etype> inline ReduceTo1DExp<SrcExp, DType, red::sum, ExpInfo<SrcExp>::kDim - dimkeep> sumall_except_dim(const Exp<SrcExp, DType, etype> &exp){...}. sumall_except_dim的功能是对除dimkeep维度外, 所有exp的维度进行求和. 返回expresion with type Tensor<Device,1>. 参数: exp: 输入表达式, 必须是一个Tensor<?,2>, 即一个矩阵. 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<mshadow_op::square>(data - broadcast<1>(mean, data.shape_))). F<mshadow_op::square>(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<int dimcast, typename SrcExp, typename DType, int etype, int dimdst> inline Broadcast1DExp<SrcExp, DType, dimdst, dimdst - dimcast> broadcast(const expr::Exp<SrcExp, DType, etype> &src, Shape<dimdst> shape) {..}. src Tensor<Device,1>; shape: shape of output; 返回 a expresion with type Tensor<Device, dimdst>, dimdst为4, 返回的Tensor的维数为4, 和shape的个数是有关的. * input: Tensor<Device,1>: ishape[0] * output: Tensor<Device,dimdst> : 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<xpu, 4>. 即 (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<mshadow_op::square_root>(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<mshadow_op::square_root>(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, 结构体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<xpu, 1>的, 因此再将(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<mshadow_op::square_root>(moving_var + param_.eps),
                                          data.shape_) * data +
             broadcast<1>(bias - (slope * moving_mean) /
                          F<mshadow_op::square_root>(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<mshadow_op::square_root>(moving_var + param_.eps), F<mshadow_op::square_root>即单目开方运算, moving_var是1维张量, 和eps相加. 然后利用broadcast<1>(), 将 slope / a 扩展成和data具有同样shape的Tensor, 即 broadcast<1>(slope / a, data.shape_), 然后再和 data 相乘, 即可得 *1. 令 b = F<mshadow_op::square_root>(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<TBlob> &out_grad,
                        const std::vector<TBlob> &in_data,
                        const std::vector<TBlob> &out_data,
                        const std::vector<OpReqType> &req,
                        const std::vector<TBlob> &in_grad,
                        const std::vector<TBlob> &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<xpu> *s = ctx.get_stream<xpu>();
    Tensor<xpu, 4> data, grad, grad_in; // 定义data, grad, grad_in. xpu下的4维张量. 下面会对这三个变量进行赋值. 
    const real_t scale = static_cast<real_t>(out_grad[batchnorm::kOut].shape_[1]) /
                         static_cast<real_t>(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_shape<xpu, 4, real_t>(dshape, s);
      grad = out_grad[batchnorm::kOut].get_with_shape<xpu, 4, real_t>(dshape, s);
      grad_in = in_grad[batchnorm::kData].get_with_shape<xpu, 4, real_t>(dshape, s);
      /* 对420行定义Tensor<xpu, 4> 对象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].get<xpu, 4, real_t>(s);
      grad = out_grad[batchnorm::kOut].get<xpu, 4, real_t>(s);
      grad_in = in_grad[batchnorm::kData].get<xpu, 4, real_t>(s);
      /* 如果in_data[batchnorm::kData].ndim()不是2维的数据, 那么就是4维的. 利用get函数直接将in_data[batchnorm::kData]等拉成4维的 张量即可. */
    } // 这和前向传播的操作基本是类似的. 

    Tensor<xpu, 1> mean = out_data[batchnorm::kMean].get<xpu, 1, real_t>(s);
    Tensor<xpu, 1> var = out_data[batchnorm::kVar].get<xpu, 1, real_t>(s);
    Tensor<xpu, 1> slope = in_data[batchnorm::kGamma].get<xpu, 1, 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<xpu, 1> bias = in_data[kBeta].get<xpu, 1, real_t>(s);
    Tensor<xpu, 1> gslope = in_grad[batchnorm::kGamma].get<xpu, 1, real_t>(s);
    Tensor<xpu, 1> gbias = in_grad[batchnorm::kBeta].get<xpu, 1, 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
    Tensor<xpu, 1> moving_mean = aux_states[batchnorm::kMovingMean].get<xpu, 1, real_t>(s);
    Tensor<xpu, 1> moving_var = aux_states[batchnorm::kMovingVar].get<xpu, 1, 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的反向操作会申请一个临时的资源空间, 这个空间任意. */
      Tensor<xpu, 2> workspace = ctx.requested[batchnorm::kTempSpace].get_space<xpu>(
          mshadow::Shape2(3, mean.shape_[0]), s);
      /* OpContext: 结构体, 定义在include/mxnet/operator.h中, 该结构体可以记录操作在前向和后向传播中的信息. ctx是结构体OpContext定 义的对象, requested是OPContext结构体下的函数: // brief Resources requested by the operator std::vector<Resource> 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<typename xpu, int ndim> inline mshadow::Tensor<xpu, ndim, real_t> get_space(mshadow::Shape<ndim> shape, mshadow::Stream<xpu> *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, 是一个临时空间, 额外内存. */    

      Tensor<xpu, 1> gmean = workspace[0];
      Tensor<xpu, 1> gvar = workspace[1];
      Tensor<xpu, 1> 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<mshadow_op::power>(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<mshadow_op::power>(*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<mshadow_op::square_root>(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<mshadow_op::square_root>()是单目开方运算. 1维的 Tensor可以看做是一个标量, 因此用-1f / F<mshadow_op::square_root>(). *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<mshadow_op::square_root>(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<mshadow_op::square_root>(*)是单目开方运算. *是[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<mshadow_op::square_root>(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<mshadow_op::square_root>(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<mshadow_op::square_root>(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
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/140931.html原文链接:https://javaforall.net

(0)
上一篇 2022年5月8日 下午8:40
下一篇 2022年5月8日 下午8:40


相关推荐

  • 浮动QQ在线咨询(js,c#,NET)[通俗易懂]

    浮动QQ在线咨询(js,c#,NET)[通俗易懂] 浮动QQ在线咨询(js,c#,NET)浮动在网页上面的QQ在线咨询代码:4@:O5V,o7_6];L7qb310波球论坛b310波球网,波球网,博球,博球网,行家心水,赛事推荐,赛事分析,资料库,足球赛事,篮球,NBA,赔率,比分,篮球数据,足球数据,分析,推荐+k0O1I2d$z6o,c”k.f(感谢:http://www.ijavascript.cn

    2022年7月21日
    18
  • 漂亮的复制零_一张图片有很多小图片组成

    漂亮的复制零_一张图片有很多小图片组成前言磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存10倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝、直接I/O、异步I/O等等,这些优化的目的就是为了提高系统的吞吐量,另外操作系统内核中的磁盘高速缓存区,可以有效的减少磁盘的访问次数。这次,我们就以「文件传输」作为切入点,来分析I/O工作方式,以及如何优化传输文件的性能。正文为什么要有DMA技术?在没有DMA技术前,I/O的过程是这样的:CPU发出对应的指令给磁盘控制器,然后返回;磁盘控制器收到..

    2026年2月10日
    7
  • SSM框架讲解(史上最详细的文章)[通俗易懂]

    SSM框架讲解(史上最详细的文章)[通俗易懂]SSM框架(白痴都看完都会)文章目录SSM框架(白痴都看完都会)介绍SSM框架<原理>一、什么是SSM框架?1.Spring2.SpringMVC3.Mybatis(核心是SqlSession)二、代码实战1.创建配置工程2.代码书写介绍SSM框架<原理>一、什么是SSM框架?SSM框架是spring、springMVC、和mybatis框架的整合,是标准的MVC模式。标准的SSM框架有四层,分别是dao层(mapper),service层,controller层

    2022年7月12日
    27
  • OpenClaw 最新版本安装完全指南

    OpenClaw 最新版本安装完全指南

    2026年3月13日
    3
  • python升序和降序排序_sql升序和降序(orderby升序和降序排列显示)

    python升序和降序排序_sql升序和降序(orderby升序和降序排列显示)sql 升序和降序排列作者 王伟同学任编 郭芮 Pandas 是用于数据处理和数据分析的非常方便的类库 这也是每个 Pythoner 进行数据分析的必备工具 但是不可否认的是 并不是所有的程序员都会使用 Python 也不是所有的 Python 用户都会使用 Pandas 但是好消息是 有了 pandassql 您可以使用 SQL 来操纵 DataFrame 导入相关库将 numpy 作为 np 导入 pandas 作为 pdfro

    2026年3月19日
    2
  • 海量数据挖掘MMDS week3:社交网络之社区检测:基本技巧「建议收藏」

    http://blog.csdn.net/pipisorry/article/details/49052057海量数据挖掘MiningMassiveDatasets(MMDs)-JureLeskovec courses学习笔记之社交网络之社区检测:基本技巧-生成模型及其参数的梯度上升方法求解博客内容:社区检测的基本技巧部分,覆盖”overlappingcommunities”寻找最好集合

    2022年4月13日
    120

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号