开发者文档 在 GitHub 上编辑

编写自己的 nn 模块

我们将看到如何创建自己的新模块,以及如何测试它们。您应该能够将它们无缝地插入到现有的神经网络中。

如果模块很简单并且性能不关键,那么您可以简单地在几行 Lua 代码中编写它们(第 1 部分)。

如果模块在计算方面更重,或者您想为 CPU 或 GPU 创建专门的优化代码,您可能希望在 C / CUDA 级别创建模块(第 2 部分)。

模块是构建神经网络的积木。模块本身就是一个神经网络,但它可以使用容器类与其他网络组合来创建复杂的神经网络。Module 是一个抽象类,它定义了训练神经网络所需的必要基本方法。所有模块都是可序列化的。

模块包含两个状态变量:outputgradInput。在这里,我们回顾一下 Module 需要实现的基本函数集

[output] forward(input)

接受一个输入对象,并计算模块的相应输出。通常情况下,输入和输出都是张量。但是,一些特殊的子类,例如表层,可能需要其他东西。有关更多信息,请参阅每个模块的规范。

forward() 之后,output 状态变量应该更新为新值。

不建议覆盖此函数。相反,应该实现 updateOutput(input) 函数。抽象父类 Module 中的 forward(input) 函数将调用 updateOutput(input)

[gradInput] backward(input, gradOutput)

对给定输入执行相对于模块的反向传播步骤。通常,此方法假设 forward(input) 之前已经调用过,并且具有相同的输入。这是出于优化原因所必需的。

如果不遵守此规则,backward() 将计算出不正确的梯度。

通常,inputgradOutputgradInput 都是 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() 时,我们始终首先调用父类的构造函数。

现在让我们看一些实际的例子。

  1. 在 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
  1. 在 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