@ -2,26 +2,26 @@
实现新的网络层
================
这份教程指导你 在PaddlePaddle中实现一个自定义的网络层。在这里我们使用全连接层作为例子来指导你完成实现新网络层需要的几 个步骤。
这份教程展示了如何 在PaddlePaddle中实现一个自定义的网络层。在这里我们使用全连接层作为例子来展示实现新网络层所需要的四 个步骤。
- 推导该层前向和后向传递的方程。
- 实现该层的C++类。
- 写梯度检测的测试单元 ,以保证梯度的正确计算。
- 实现该层的python封装 。
1. 推导该层前向和后向传递的方程。
2. 实现该层的C++类。
3. 增加梯度检测的单元测试 ,以保证梯度的正确计算。
4. 封装该层的Python接口 。
推导方程
================
首先我们需要推导该网络层的*前向传播* 和*后向传播* 的方程。前向传播给定输入,计算输出。后向传播给定输出的梯度,计算输入和参数的梯度。
下图是一个全链 接层的示意图。在全连接层中,每个输出节点都连接到所有的输入节点上。
下图是一个全连 接层的示意图。在全连接层中,每个输出节点都连接到所有的输入节点上。
.. image :: FullyConnected.jpg
:align: center
:scale: 60 %
一个网络层的前向传播部分把输入转化为相应的输出。
全连接层以一个维度为 :math: `D_i` 稠密的向量作为输入。其 用一个尺度为 :math: `D_i \times D_o` 的变换矩阵 :math: `W` 把 :math: `x` 映射到一个维度为 :math: `D_o` 的向量,并在其 上再加上维度为 :math: `D_o` 的偏置向量 :math: `b` 。
全连接层以一个维度为 :math: `D_i` 的稠密向量作为输入,使 用一个尺度为 :math: `D_i \times D_o` 的变换矩阵 :math: `W` 把 :math: `x` 映射到一个维度为 :math: `D_o` 的向量,并在乘积结果 上再加上维度为 :math: `D_o` 的偏置向量 :math: `b` 。
.. math ::
@ -29,9 +29,9 @@
其中 :math: `f(.)` 是一个非线性的*激活方程* , 例如sigmoid, tanh, 以及Relu。
变换矩阵 :math: `W` 和偏置向量 :math: `b` 是该网络层的*参数* 。一个网络层的参数是在*反向传播* 时被训练的。反向传播对所有的参数和输入都计算输出函数 的梯度。优化器则用链式法则来对每个参数计算损失函数的梯度。
变换矩阵 :math: `W` 和偏置向量 :math: `b` 是该网络层的*参数* 。一个网络层的参数是在*反向传播* 时被训练的。反向传根据输出的梯度,分别计算每个参数的梯度,以及输入 的梯度。优化器则用链式法则来对每个参数计算损失函数的梯度。
假设我们的 损失函数是 :math: `c(y)` ,那么
假设损失函数是 :math: `c(y)` ,那么
.. math ::
@ -43,9 +43,9 @@
\frac{\partial y}{\partial z} = \frac{\partial f(z)}{\partial z}
我们 的base layer类可以自动计算上面的导数。
PaddlePaddle 的base layer类可以自动计算上面的导数。
因而 ,对全连接层来说,我们需要计算:
因此 ,对全连接层来说,我们需要计算:
.. math ::
@ -60,23 +60,23 @@
一个网络层的C++类需要实现初始化,前向和后向。全连接层的实现位于:code: `paddle/gserver/layers/FullyConnectedLayer.h` 及:code: `paddle/gserver/layers/FullyConnectedLayer.cpp` 。这里我们展示一份简化过的代码。
这个类需要继承 :code: `paddle::Layer` 这个基类,并且需要重写以下 基类中的虚函数:
这个类需要继承 :code: `paddle::Layer` 这个基类,并且需要重写基类中的以下几个 虚函数:
- 类的构造函数和析构析构 函数。
- 类的构造函数和析构函数。
- :code: `init` 函数。用于初始化参数和设置。
- :code: `forward` 。实现网络层的前向传播。
- :code: `backward` 。实现网络层的后向传播。
- :code: `prefetch` 。用于确定由参数服务器预取的行相关的参数矩阵。如果该网络层不需要远程稀疏更新的话,你 不需要重写该函数。(大多数网络层不需要支持远程稀疏更新)
- :code: `prefetch` 。用来从参数服务器预取参数矩阵相应的行。如果网络层不需要远程稀疏更新,则 不需要重写该函数。(大多数网络层不需要支持远程稀疏更新)
头文件在下面列出 :
头文件如下 :
.. code-block :: c++
namespace paddle {
/**
* 全连接层的每个输出都连接到上一层的所有的神经元上。
* 其用一些学习过 的参数做内积并加上偏置(可选)。
* 它的输入与经过学习 的参数做内积并加上偏置(可选)。
*
* 配置文件接口是fc_layer。
*/
@ -101,9 +101,9 @@
};
} // namespace paddle
头文件中把参数定位 为类的成员变量。我们使用 :code: `Weight` 类作为参数的抽象,它支持多线程更新。该类的实现细节在“实现细节”中由 详细介绍。
头文件中把参数定义 为类的成员变量。我们使用 :code: `Weight` 类作为参数的抽象,它支持多线程更新。该类的实现细节在“实现细节”中详细介绍。
- :code: `weights_` 是存有变换矩阵的一系列 权重。在当前的实现方式下,网络层可以有多个输入。因此,它可能有不止一个权重。每个权重对应一个输入。
- :code: `weights_` 是存有一系列 变换矩阵的权重。在当前的实现方式下,网络层可以有多个输入。因此,它可能有不止一个权重。每个权重对应一个输入。
- :code: `biases_` 是存有偏置向量的权重。
全连接层没有网络层配置的超参数。如果一个网络层需要配置的话,通常的做法是将配置存于 :code: `LayerConfig& config` 中,并在类构建函数中把它放入一个类成员变量里。
@ -173,7 +173,7 @@
MatrixPtr outV = getOutputValue();
// 对每个输入乘上转化 矩阵
// 对每个输入乘上变换 矩阵
for (size_t i = 0; i != inputLayers_.size(); ++i) {
auto input = getInput(i);
CHECK(input.value) << "The input of 'fc' layer must be matrix";
@ -193,9 +193,9 @@
实现后向传播的部分有下面几个步骤。
- :code: `backwardActivation()` 计算激活函数的梯度。梯度会就地(不使用额外空间)乘上输出的梯度,并可以 通过 :code: `getOutputGrad()` 来获得。
- 计算偏置的梯度。注意,我们使用 :code: `biases_->getWGrad()` 来得到某个特定参数的梯度矩阵。在一个参数的梯度被更新后,**必须** 要调用 :code: `getParameterPtr()->incUpdate(callback);` 。这是用来在多线程和多机上更新参数的 。
- 之 后,计算转换矩阵和输入的梯度,并对相应的参数调用 :code: `incUpdate` 。这给了框架一个机会去了解自己是否已经把所有的梯度收集到一个参数中,使得框架可以进行有时间重叠的工作。(例如,网络通信)
- :code: `backwardActivation()` 计算激活函数的梯度。通过 :code: `getOutputGrad()` 来获得输出的梯度,调用该函数后,梯度会就地(不使用额外空间)乘上输出的梯度 。
- 计算偏置的梯度。注意,我们使用 :code: `biases_->getWGrad()` 来得到某个特定参数的梯度矩阵。在一个参数的梯度被更新后,**必须** 要调用 :code: `getParameterPtr()->incUpdate(callback);` 。这用于在多线程和多机上更新参数 。
- 最 后,计算转换矩阵和输入的梯度,并对相应的参数调用 :code: `incUpdate` 。PaddlePaddle可以通过该机制判断是否已经收集齐所有的梯度, 从而可以做一些与计算重叠的工作( 例如, 网络通信) 。
.. code-block :: c++
@ -208,7 +208,6 @@
if (biases_ && biases_->getWGrad()) {
biases_->getWGrad()->collectBias(*getOutputGrad(), 1);
/* 加上偏置的梯度 * /
biases_->getParameterPtr()->incUpdate(callback);
}
@ -238,7 +237,7 @@
}
}
:code: `prefetch` 函数指出了在训练时需要从参数服务器取出的行。仅在远程稀疏训练时有效。在远程稀疏训练时,完整的参数矩阵被分布式的保存在参数服务器上。当网络层用一个批次做训练时,该批次中,输入仅有一个子集是非零的。因此,该层仅需要这些非零样本位置所对应的转 换矩阵的那些行。 :code: `prefetch` 表明了这些行的标号。
:code: `prefetch` 函数指出了在训练时需要从参数服务器取出的行。仅在远程稀疏训练时有效。使用远程稀疏方式训练时,完整的参数矩阵被分布在不同的参数服务器上。当网络层用一个批次做训练时,该批次的输入中仅有一个子集是非零的。因此,该层仅需要这些非零样本位置所对应的变 换矩阵的那些行。 :code: `prefetch` 表明了这些行的标号。
大多数层不需要远程稀疏训练函数。这种情况下不需要重写该函数。
@ -271,7 +270,7 @@
写梯度检查单元测试是一个验证新实现的层是否正确的相对简单的办法。梯度检查单元测试通过有限差分法来验证一个层的梯度。首先对输入做一个小的扰动 :math: `\Delta x` ,然后观察到输出的变化为 :math: `\Delta y` ,那么,梯度就可以通过这个方程计算得到 :math: `\frac{\Delta y}{\Delta x }` 。之后,再用这个梯度去和 :code: `backward` 函数得到的梯度去对比,以保证梯度计算的正确性。需要注意的是梯度检查仅仅验证了梯度的计算,并不保证 :code: `forward` 和 :code: `backward` 函数的实现是正确的。你需要一些更复杂的单元测试来保证你实现的网络层是正确的。
所有的梯度检测单侧 都位于 :code: `paddle/gserver/tests/test_LayerGrad.cpp` 。我们建议你在写新网络层时把测试代码放入新的文件中。下面列出了全连接层的梯度检查单元测试。它包含以下几步:
所有网络层的梯度检查单测 都位于 :code: `paddle/gserver/tests/test_LayerGrad.cpp` 。我们建议你在写新网络层时把测试代码放入新的文件中。下面列出了全连接层的梯度检查单元测试。它包含以下几步:
+ 生成网络层配置。网络层配置包含以下几项:
- 偏置参数的大小。( 例子中是4096)
@ -294,10 +293,10 @@
- 非零数字的个数,仅对稀疏数据有效。
- 稀疏数据的格式,仅对稀疏数据有效。
+ 对每个输入,都需要调用一次 :code: `config.layerConfig.add_inputs();` 。
+ 调用 :code: `testLayerGrad` 来做梯度检查。它包含下面的 参数。
+ 调用 :code: `testLayerGrad` 来做梯度检查。它包含以 下参数。
- 层和输入的配置。(例子中是 :code: `config` )
- 输入 的类型。(例子中是 :code: `fc` )
- 梯度检查的批次大小。( 例子中是100)
- 网络层 的类型。(例子中是 :code: `fc` )
- 梯度检查的输入数据的 批次大小。( 例子中是100)
- 输入是否是转置的。大多数层需要设置为 :code: `false` 。(例子中是 :code: `false` )
- 是否使用权重。有些层或者激活需要做归一化以保证它们的输出的和是一个常数。例如, softmax激活的输出的和总是1。在这种情况下, 我们不能通过常规的梯度检查的方式来计算梯度。因此我们采用输出的加权和( 非常数) 来计算梯度。( 例子中是 :code: `true` , 因为全连接层的激活可以是softmax)
@ -309,7 +308,7 @@
config.biasSize = 4096;
config.layerConfig.set_type("fc");
config.layerConfig.set_size(4096);
config.layerConfig.set_active_type("sigmoid ");
config.layerConfig.set_active_type("softmax ");
config.layerConfig.set_drop_rate(0.1);
// Setup inputs.
config.inputDefs.push_back(
@ -323,7 +322,7 @@
}
}
如果你要为了测试而增加新的文件,例如 :code: `paddle/gserver/tests/testFCGrad.cpp` ,你需要把该文件加入 :code: `paddle/gserver/tests/CMakeLists.txt` 中。下面给出了一个例子。当你执行命令 :code: `make tests` 时,所有的单侧都会被执行一次。注意,有些层可能需要高精度来保证梯度检查单侧 正确执行。你需要在配置cmake时将 :code: `WITH_DOUBLE` 设置为 `ON` 。
如果你要为了测试而增加新的文件,例如 :code: `paddle/gserver/tests/testFCGrad.cpp` ,你需要把该文件加入 :code: `paddle/gserver/tests/CMakeLists.txt` 中。下面给出了一个例子。当你执行命令 :code: `make tests` 时,所有的单测都会被执行一次。注意,有些层可能需要高精度来保证梯度检查单测 正确执行。你需要在配置cmake时将 :code: `WITH_DOUBLE` 设置为 `ON` 。
.. code-block :: bash
@ -344,7 +343,7 @@ python封装的实现使得我们可以在配置文件中使用新实现的网
- 所有的Python封装都使用 :code: `@config_layer('fc')` 这样的装饰器。网络层的标识符为 :code: `fc` 。
- 实现构造函数 :code: `__init__` 。
- 它首先调用基构造函数 :code: `super(FCLayer, self).__init__(name, 'fc', size, inputs=inputs, **xargs)` 。 :code: `FCLayer` 是Python封装的类名。 :code: `fc` 是网络层的标识符。为了封装能够正确工作,这些名字必须要写对。
- 之后,计算转 换矩阵的大小和格式(是否稀疏)。
- 之后,计算变 换矩阵的大小和格式(是否稀疏)。
.. code-block :: python