开发者文档 在 GitHub 上编辑
编写自己的 nn 模块
我们将看到如何创建自己的新模块,以及如何测试它们。您应该能够将它们无缝地插入到现有的神经网络中。
如果模块很简单并且性能不关键,那么您可以简单地在几行 Lua 代码中编写它们(第 1 部分)。
如果模块在计算方面更重,或者您想为 CPU 或 GPU 创建专门的优化代码,您可能希望在 C / CUDA 级别创建模块(第 2 部分)。
模块是构建神经网络的积木。模块本身就是一个神经网络,但它可以使用容器类与其他网络组合来创建复杂的神经网络。Module 是一个抽象类,它定义了训练神经网络所需的必要基本方法。所有模块都是可序列化的。
模块包含两个状态变量:output
和 gradInput
。在这里,我们回顾一下 Module
需要实现的基本函数集
[output] forward(input)
接受一个输入对象,并计算模块的相应输出。通常情况下,输入和输出都是张量。但是,一些特殊的子类,例如表层,可能需要其他东西。有关更多信息,请参阅每个模块的规范。
在 forward()
之后,output
状态变量应该更新为新值。
不建议覆盖此函数。相反,应该实现 updateOutput(input)
函数。抽象父类 Module
中的 forward(input)
函数将调用 updateOutput(input)
。
[gradInput] backward(input, gradOutput)
对给定输入执行相对于模块的反向传播步骤。通常,此方法假设 forward(input)
之前已经调用过,并且具有相同的输入。这是出于优化原因所必需的。
如果不遵守此规则,
backward()
将计算出不正确的梯度。
通常,input
、gradOutput
和 gradInput
都是 Tensors
。但是,一些特殊的子类,例如 table
层,可能需要其他东西。有关更多信息,请参阅每个模块的规范。
反向传播步骤包括在给定 gradOutput
(相对于模块输出的梯度)的情况下,计算输入处的两种梯度。
此函数只是使用两个函数调用来执行此任务
- 对
updateGradInput(input, gradOutput)
的函数调用。 - 对
accGradParameters(input,gradOutput)
的函数调用。
不建议在自定义类中覆盖此函数调用。最好覆盖 updateGradInput(input, gradOutput)
和 accGradParameters(input, gradOutput)
函数。
[output] updateOutput(input)
在定义新模块时,应该重载此方法。
使用类的当前参数集和输入计算输出。此函数返回存储在 output
字段中的结果。
[gradInput] updateGradInput(input, gradOutput)
在定义新模块时,应该重载此方法。
计算模块相对于其自身 input
的梯度。这将返回到 gradInput
中。此外,gradInput
状态变量将相应更新。
accGradParameters(input, gradOutput)
在定义新模块时,如果模块具有可训练参数,则可能需要重载此方法。
计算模块相对于其自身参数的梯度。许多模块不执行此步骤,因为它们没有任何参数。参数的状态变量名称取决于模块。该模块应将相对于参数的梯度累积到某个变量中。
用 zeroGradParameters()
将此累积归零,并根据此累积使用 updateParameters()
更新参数。
reset()
此方法定义了如何在训练之前重置可训练参数,即初始化它们。
如果计划不使用 optim
包,则模块提供一些其他方法,您可能希望定义它们。这些方法有助于 zero()
参数,并使用非常基本的技术更新它们。
在代码结构方面,Torch 提供了一个类模型,我们使用它来进行继承,通常用于定义 nn 中的所有模块。
这是一个典型的新类的空占位符
local NewClass, Parent = torch.class('nn.NewClass', 'nn.Module')
function NewClass:__init()
Parent.__init(self)
end
function NewClass:updateOutput(input)
end
function NewClass:updateGradInput(input, gradOutput)
end
function NewClass:accGradParameters(input, gradOutput)
end
function NewClass:reset()
end
在定义新类时,我们需要做的就是填写这些空函数。请注意,在定义构造函数 __init()
时,我们始终首先调用父类的构造函数。
现在让我们看一些实际的例子。
-
在 Lua 级别编写模块:实现 Dropout 激活单元
Dropout 单元有一个核心思想,就是通过随机将某些隐藏单元归零,来扰乱隐藏单元的激活。
这样的类可以这样定义
local Dropout, Parent = torch.class('nn.Dropout', 'nn.Module')
function Dropout:__init(p)
Parent.__init(self)
self.p = p or 0.5
if self.p >= 1 or self.p < 0 then
error('<Dropout> illegal percentage, must be 0 <= p < 1')
end
self.noise = torch.Tensor()
end
function Dropout:updateOutput(input)
self.output:resizeAs(input):copy(input)
self.noise:resizeAs(input)
self.noise:bernoulli(1-self.p)
self.output:cmul(self.noise)
return self.output
end
function Dropout:updateGradInput(input, gradOutput)
self.gradInput:resizeAs(gradOutput):copy(gradOutput)
self.gradInput:cmul(self.noise) -- simply mask the gradients with the noise vector
return self.gradInput
end
在使用梯度估计编写模块时,始终非常重要地测试您的实现。这可以使用 nn
中提供的 Jacobian
类轻松完成,该类将梯度方法的实现 (updateGradInput()
和 accGradParameters()
) 与通过有限差分 (扰乱模块的输入,并估计输出上的增量) 获得的 Jacobian
矩阵进行比较。这可以这样完成
-- parameters
local precision = 1e-5
local jac = nn.Jacobian
-- define inputs and module
local ini = math.random(10,20)
local inj = math.random(10,20)
local ink = math.random(10,20)
local percentage = 0.5
local input = torch.Tensor(ini,inj,ink):zero()
local module = nn.Dropout(percentage)
-- test backprop, with Jacobian
local err = jac.testJacobian(module,input)
print('==> error: ' .. err)
if err<precision then
print('==> module OK')
else
print('==> error too large, incorrect implementation')
end
Jacobian
类的一个小问题是,它假设模块的输出相对于输入是确定性的。对于这个特定的模块,情况并非如此,因此为了测试目的,我们需要冻结噪声生成,即只执行一次
– 我们重载了 updateOutput() 函数,以只生成噪声 – 整个测试只进行一次。
function Dropout:updateOutput(input)
self.output:resizeAs(input):copy(input)
self.noise = self.noise or input.new():resizeAs(input):bernoulli(1-self.p)
self.output:cmul(self.noise)
return self.output
end
-
在 C 或 CUDA 级别编写模块
基于 C 宏的模板
在编写 Torch C 代码之前,首先要熟悉散布在 Torch 和 nn 中的 C 宏语法。
例如,看看出现在 THTensorMath.c 中的这段代码
void THTensor_(add)(THTensor *r_, THTensor *t, real value)
{
THTensor_(resizeAs)(r_, t);
if (THTensor_(isContiguous)(r_) && THTensor_(isContiguous)(t) && THTensor_(nElement)(r_) == THTensor_(nElement)(t)) {
real *tp = THTensor_(data)(t);
real *rp = THTensor_(data)(r_);
long sz = THTensor_(nElement)(t);
long i;
#pragma omp parallel for if(sz > TH_OMP_OVERHEAD_THRESHOLD) private(i)
for (i=0; i<sz; i++)
rp[i] = tp[i] + value;
} else {
TH_TENSOR_APPLY2(real, r_, real, t, *r__data = *t_data + value;);
}
}
您看到的奇怪的 _(add)(THTensor *r_ ....)
语法是一个预处理器宏。
lib/TH/THTensor.h:
#define THTensor_(NAME) TH_CONCAT_4(TH,Real,Tensor_,NAME)
它导致了...
lib/TH/THGeneral.h.in:
#define TH_CONCAT_4(x,y,z,w) TH_CONCAT_4_EXPAND(x,y,z,w)
最后...
lib/TH/THGeneral.h.in:
#define TH_CONCAT_4_EXPAND(x,y,z,w) x ## y ## z ## w
因此,(以及经过使用更多宏的预处理后),
void THTensor_(add)(THTensor *r_, THTensor *t, real value)
最终变成了这样
long THRealTensor_add(const THRealTensor *r_, THRealTensor *t, real value)
Real 和 real 定义为特定类型,例如,对于浮点精度
#define Real Float
#define real float
最终使该函数原型成为
long THFloatTensor_add(const THFloatTensor *r_, THFloatTensor *t, float value)
您也会在 nn 库中看到类似的语法,所以请做好准备。
C nn 模块
理解如何编写新 nn 模块的最佳方法是查看现有的模块。
以下是编写 nn.Threshold 的步骤
步骤 1:编写 Lua 部分
https://github.com/torch/nn/blob/master/Threshold.lua
- 编写构造函数
- 编写 updateOutput / updateGradInput,它们只是调用 C 函数
调用 C 函数有一个有效但略微奇怪的语法
https://github.com/torch/nn/blob/b80e26e8b849a69b8121acf62f3487095c2f10e8/Threshold.lua#L20
input.nn.Threshold_updateOutput(self, input)
这行代码只是调用了这里定义的函数:https://github.com/torch/nn/blob/b80e26e8b849a69b8121acf62f3487095c2f10e8/generic/Threshold.c#L5
它之所以调用它,是因为您在输入.nn. 表中注册了 C 函数:https://github.com/torch/nn/blob/b80e26e8b849a69b8121acf62f3487095c2f10e8/generic/Threshold.c#L61-L63
这有助于我们编写适用于任何定义的张量类型的通用代码,同时无需执行任何复杂的动态函数调度逻辑。
完整的 nn.Threshold 模块由两个文件组成
- Lua 部分:https://github.com/torch/nn/blob/b80e26e8b849a69b8121acf62f3487095c2f10e8/Threshold.lua
- C 部分:https://github.com/torch/nn/blob/b80e26e8b849a69b8121acf62f3487095c2f10e8/generic/Threshold.c
这些文件在以下行中包含到包中
- init.lua : https://github.com/torch/nn/blob/b80e26e8b849a69b8121acf62f3487095c2f10e8/init.lua#L68
- init.c : https://github.com/torch/nn/blob/b80e26e8b849a69b8121acf62f3487095c2f10e8/init.c#L41-L42
- init.c : https://github.com/torch/nn/blob/b80e26e8b849a69b8121acf62f3487095c2f10e8/init.c#L153
- init.c : https://github.com/torch/nn/blob/b80e26e8b849a69b8121acf62f3487095c2f10e8/init.c#L194
CUDA nn 模块
要为 nn.Threshold 模块添加 CUDA 支持,类似于编写 Threshold.c,我们将编写 Threshold.cu
- https://github.com/torch/cunn/blob/master/lib/THCUNN/Threshold.cu
并将其包含在这里
- https://github.com/torch/cunn/blob/master/init.cu#L36
- https://github.com/torch/cunn/blob/master/utils.h#L30