神经网络向量化(Neural Network Vectorization)

注:本章节翻译完全参考旧版 UFLDL 中文教程。

在本节,我们将引入神经网络的向量化版本。在前面关于神经网络介绍的章节中,我们已经给出了一个部分向量化的实现,它在一次输入一个训练样本时是非常有效率的。下边我们看看如何实现同时处理多个训练样本的算法。具体来讲,我们将把正向传播、反向传播这两个步骤以及稀疏特征集学习扩展为多训练样本版本。

正向传播( Forward propagation )

考虑一个三层网络(一个输入层、一个隐含层、以及一个输出层),并且假定 $x$ 是包含一个单一训练样本 $x^{(i)} \in \Re^{n}$ 的列向量。则向量化的正向传播步骤如下:

$$ \begin{align} z^{(2)} &= W^{(1)} x + b^{(1)} \ a^{(2)} &= f(z^{(2)}) \ z^{(3)} &= W^{(2)} a^{(2)} + b^{(2)} \ h_{W,b}(x) &= a^{(3)} = f(z^{(3)}) \end{align} $$

这对于单一训练样本而言是非常有效的一种实现,但是当我们需要处理 $m$ 个训练样本时,则需要把如上步骤放入一个 for 循环中。

更具体点来说,参照逻辑回归向量化的例子,我们用 Matlab/Octave 风格变量 $x$ 表示包含输入训练样本的矩阵, $x(:,i)$ 代表第 $\textstyle i$ 个训练样本。则x正向传播步骤可如下实现:

  1. % 非向量化实现
  2. for i=1:m,
  3. z2 = W1 * x(:,i) + b1;
  4. a2 = f(z2);
  5. z3 = W2 * a2 + b2;
  6. h(:,i) = f(z3);
  7. end;

这个 for 循环能否去掉呢?对于很多算法而言,我们使用向量来表示计算过程中的中间结果。例如在前面的非向量化实现中, $z2,a2,z3$ 都是列向量,分别用来计算隐层和输出层的激励结果。为了充分利用并行化和高效矩阵运算的优势,我们希望算法能同时处理多个训练样本。让我们先暂时忽略前面公式中的 $b1$ 和 $b2$ (把它们设置为 $0$ ),那么可以实现如下:

  1. % 向量化实现 (忽略 b1, b2)
  2. z2 = W1 * x;
  3. a2 = f(z2);
  4. z3 = W2 * a2;
  5. h = f(z3)

在这个实现中, $z2,a2,z3$ 都是矩阵,每个训练样本对应矩阵的一列。在对多个训练样本实现向量化时常用的设计模式是,虽然前面每个样本对应一个列向量(比如 $z2$ ),但我们可把这些列向量堆叠成一个矩阵以充分享受矩阵运算带来的好处。这样,在这个例子中, $a2$ 就成了一个 $s2 \times m$ 的矩阵( $s2$ 是网络第二层中的神经元数, $m$ 是训练样本个数)。矩阵 $a2$ 的物理含义是,当第 $i$ 个训练样本 $x(:i)$ 输入到网络中时,它的第 $i$ 列就表示这个输入信号对隐神经元(网络第二层)的激励结果。

在上面的实现中,我们假定激活函数 $f(z)$ 接受矩阵形式的输入 $z$ ,并对输入矩阵按列分别施以激活函数。需要注意的是,你在实现 $f(z)$ 的时候要尽量多用 Matlab/Octave 的矩阵操作,并尽量避免使用 for 循环。假定激活函数采用 $Sigmoid$ 函数,则实现代码如下所示:

  1. % 低效的、非向量化的激活函数实现
  2. function output = unvectorized_f(z)
  3. output = zeros(size(z))
  4. for i=1:size(z,1),
  5. for j=1:size(z,2),
  6. output(i,j) = 1/(1+exp(-z(i,j)));
  7. end;
  8. end;
  9. end
  10. % 高效的、向量化激活函数实现
  11. function output = vectorized_f(z)
  12. output = 1./(1+exp(-z)); % "./" MatlabOctave中表示对矩阵的每个元素分别进行除法操作
  13. end

最后,我们上面的正向传播向量化实现中忽略了 $b1$ 和 $b2$ ,现在要把他们包含进来,为此我们需要用到 Matlab/Octave 的内建函数 repmat :

  1. % 正向传播的向量化实现
  2. z2 = W1 * x + repmat(b1,1,m);
  3. a2 = f(z2);
  4. z3 = W2 * a2 + repmat(b2,1,m);
  5. h = f(z3)

repmat(b1,1,m) 的运算效果是,它把列向量 $b1$ 拷贝 $m$ 份,然后堆叠成如下矩阵:

$$ \begin{bmatrix} | & | & & | \ {\rm b1} & {\rm b1} & \cdots & {\rm b1} \ | & | & & | \end{bmatrix}. $$

这就构成一个 $s2 \times m$ 的矩阵。它和 $W1 x$ 相加,就等于是把 $W1 x$ 矩阵(译者注:这里 $x$ 是训练矩阵而非向量, 所以 $W1 * x$ 代表两个矩阵相乘,结果还是一个矩阵)的每一列加上 $b1$ 。如果不熟悉的话,可以参考 Matlab/Octave 的帮助文档获取更多信息(输入 “help repmat” )。 repmat 作为 Matlab/Octave 的内建函数,运行起来是相当高效的,远远快过我们自己用 for 循环实现的效果。

反向传播(Backpropagation)

现在我们来描述反向传播向量化的思路。在阅读这一节之前,强烈建议各位仔细阅读前面介绍的正向传播的例子代码,确保你已经完全理解。下边我们只会给出反向传播向量化实现的大致纲要,而由你来完成具体细节的推导(见向量化练习)。

对于监督学习,我们有一个包含 $m$ 个带类别标号样本的训练集 ${ (x^{(1)}, y^{(1)}), \ldots, (x^{(m)}, y^{(m)}) }$ 。(对于自编码网络,我们只需令 $y(i) = x(i)$ 即可,但这里考虑的是更一般的情况。)

假定网络的输出有 $s_3$ 维,因而每个样本的类别标号向量就记为 $y^{(i)} \in \Re^{s_3}$ 。在我们的 Matlab/Octave 数据结构实现中,把这些输出按列合在一起形成一个 Matlab/Octave 风格变量 $y$ ,其中第 $i$ 列 $y(:,i)$ 就是 $y(i)$ 。

现在我们要计算梯度项 $\nabla{W^{(l)}} J(W,b)$ 和 $\nabla{b^{(l)}} J(W,b)$ 。对于梯度中的第一项,就像过去在反向传播算法中所描述的那样,对于每个训练样本 $(x,y)$ ,我们可以这样来计算:

$$ \begin{align} \delta^{(3)} &= - (y - a^{(3)}) \bullet f’(z^{(3)}), \ \delta^{(2)} &= ((W^{(2)})^T\delta^{(3)}) \bullet f’(z^{(2)}), \ \nabla{W^{(2)}} J(W,b;x,y) &= \delta^{(3)} (a^{(2)})^T, \ \nabla{W^{(1)}} J(W,b;x,y) &= \delta^{(2)} (a^{(1)})^T. \end{align} $$

在这里 $\bullet$ 表示对两个向量按对应元素相乘的运算(译者注:其结果还是一个向量)。为了描述简单起见,我们这里暂时忽略对参数 $b(l)$ 的求导,不过在你真正实现反向传播时,还是需要计算关于它们的导数的。

假定我们已经实现了向量化的正向传播方法,如前面那样计算了矩阵形式的变量 $z2, a2, z3$ 和 $h$ ,那么反向传播的非向量化版本可如下实现:

  1. gradW1 = zeros(size(W1));
  2. gradW2 = zeros(size(W2));
  3. for i=1:m,
  4. delta3 = -(y(:,i) - h(:,i)) .* fprime(z3(:,i));
  5. delta2 = W2'*delta3(:,i) .* fprime(z2(:,i));
  6. gradW2 = gradW2 + delta3*a2(:,i)';
  7. gradW1 = gradW1 + delta2*a1(:,i)';
  8. end;

在这个实现中,有一个 for 循环。而我们想要一个能同时处理所有样本、且去除这个 for 循环的向量化版本。

为做到这一点,我们先把向量 $delta3$ 和 $delta2$ 替换为矩阵,其中每列对应一个训练样本。我们还要实现一个函数 fprime(z) ,该函数接受矩阵形式的输入 $z$ ,并且对矩阵的按元素分别执行 $f’(\cdot)$ 。这样,上面 for 循环中的 $4$ 行 Matlab 代码中每行都可单独向量化,以一行新的(向量化的) Matlab 代码替换它(不再需要外层的 for 循环)。

在向量化练习中,我们要求你自己去推导出这个算法的向量化版本。如果你已经能从上面的描述中了解如何去做,那么我们强烈建议你去实践一下。虽然我们已经为你准备了反向传播的向量化实现提示,但还是鼓励你在不看提示的情况下自己去推导一下。

稀疏自编码网络(Sparse autoencoder )

稀疏自编码网络中包含一个额外的稀疏惩罚项,目的是限制神经元的平均激活率,使其接近某个(预设的)目标激活率 $ρ$ 。其实在对单个训练样本上执行反向传播时,我们已经考虑了如何计算这个稀疏惩罚项,如下所示:

$$ \begin{align} \delta^{(2)}i = \left( \left( \sum{j=1}^{s{2}} W^{(2)}{ji} \delta^{(3)}_j \right) + \beta \left( - \frac{\rho}{\hat\rho_i} + \frac{1-\rho}{1-\hat\rho_i} \right) \right) f’(z^{(2)}_i) . \end{align} $$

在非向量化的实现中,计算代码如下:

  1. % 稀疏惩罚Delta
  2. sparsity_delta = - rho ./ rho_hat + (1 - rho) ./ (1 - rho_hat);
  3. for i=1:m,
  4. ...
  5. delta2 = (W2'*delta3(:,i) + beta*sparsity_delta).* fprime(z2(:,i));
  6. ...
  7. end;

但在上面的代码中,仍旧含有一个需要在整个训练集上运行的 for 循环,这里 $delta2$ 是一个列向量。

作为对照,回想一下在向量化的情况下, $delta2$ 现在应该是一个有 $m$ 列的矩阵,分别对应着 $m$ 个训练样本。还要注意,稀疏惩罚项 sparsity_delta 对所有的训练样本一视同仁。这意味着要向量化实现上面的计算,只需在构造 $delta2$ 时,往矩阵的每一列上分别加上相同的值即可。因此,要向量化上面的代码,我们只需简单的用 repmat 命令把 sparsity_delta 加到 $delta2$ 的每一列上即可(译者注:这里原文描述得不是很清楚,看似应加到上面代码中 $delta2$ 行等号右边第一项,即 $W2’*delta3$ 上)。