神经网络向量化
From Ufldl
Line 1: | Line 1: | ||
在本节,我们将引入神经网络的向量化版本。在前面关于[[神经网络]]介绍的章节中,我们已经给出了一个部分向量化的实现,它在一次输入一个训练样本时是非常有效率的。下边我们看看如何实现同时处理多个训练样本的算法。具体来讲,我们将把正向传播、反向传播这两个步骤以及稀疏特征集学习扩展为多训练样本版本。 | 在本节,我们将引入神经网络的向量化版本。在前面关于[[神经网络]]介绍的章节中,我们已经给出了一个部分向量化的实现,它在一次输入一个训练样本时是非常有效率的。下边我们看看如何实现同时处理多个训练样本的算法。具体来讲,我们将把正向传播、反向传播这两个步骤以及稀疏特征集学习扩展为多训练样本版本。 | ||
+ | |||
==正向传播== | ==正向传播== | ||
考虑一个三层网络(一个输入层、一个隐含层、以及一个输出层),并且假定x是包含一个单一训练样本<math>x^{(i)} \in \Re^{n}</math> 的列向量。则向量化的正向传播步骤如下: | 考虑一个三层网络(一个输入层、一个隐含层、以及一个输出层),并且假定x是包含一个单一训练样本<math>x^{(i)} \in \Re^{n}</math> 的列向量。则向量化的正向传播步骤如下: | ||
+ | |||
:<math>\begin{align} | :<math>\begin{align} | ||
Line 12: | Line 14: | ||
\end{align}</math> | \end{align}</math> | ||
- | 这对于单一训练样本而言是非常有效的一种实现,但是当我们需要处理<tt>m</tt> | + | |
+ | 这对于单一训练样本而言是非常有效的一种实现,但是当我们需要处理<tt>m</tt>个训练样本时,则需要把如上步骤放入一个<tt>for</tt>循环中。 | ||
+ | |||
更具体点来说,参照逻辑回归向量化的例子,我们用Matlab/Octave风格变量<tt>x</tt>表示包含输入训练样本的矩阵,<tt>x(:,i)</tt>代表第<math>\textstyle i</math>个训练样本。则x正向传播步骤可如下实现: | 更具体点来说,参照逻辑回归向量化的例子,我们用Matlab/Octave风格变量<tt>x</tt>表示包含输入训练样本的矩阵,<tt>x(:,i)</tt>代表第<math>\textstyle i</math>个训练样本。则x正向传播步骤可如下实现: | ||
- | <syntaxhighlight> | + | |
+ | <syntaxhighlight> | ||
% 非向量化实现 | % 非向量化实现 | ||
for i=1:m, | for i=1:m, | ||
Line 26: | Line 31: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
- | |||
- | <syntaxhighlight> | + | 这个<tt>for</tt>循环能否去掉呢?对于很多算法而言,我们使用向量来表示计算过程中的中间结果。例如在前面的非向量化实现中,<tt>z2</tt>,<tt>a2</tt>,<tt>z3</tt>都是列向量,分别用来计算隐层和输出层的激励结果。为了充分利用并行化和高效矩阵运算的优势,我们希望算法能同时处理多个训练样本。让我们先暂时忽略前面公式中的<tt>b1</tt>和<tt>b2</tt>(把它们设置为0),那么可以实现如下: |
+ | |||
+ | |||
+ | <syntaxhighlight> | ||
% 向量化实现 (忽略 b1, b2) | % 向量化实现 (忽略 b1, b2) | ||
z2 = W1 * x; | z2 = W1 * x; | ||
Line 36: | Line 43: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
- | |||
- | 在上面的实现中,我们假定激活函数 <tt>f(z)</tt>接受矩阵形式的输入<tt>z</tt>,并对输入矩阵按列分别施以激活函数。需要注意的是,你在实现<tt>f(z)</tt>的时候要尽量多用Matlab/Octave的矩阵操作,并尽量避免使用for循环。假定激活函数采用Sigmoid函数,则实现代码如下所示: | + | 在这个实现中,<tt>z2</tt>,<tt>a2</tt>,<tt>z3</tt>都是矩阵,每个训练样本对应矩阵的一列。在对多个训练样本实现向量化时常用的设计模式是,虽然前面每个样本对应一个列向量(比如<tt>z2</tt>),但我们可把这些列向量堆叠成一个矩阵以充分享受矩阵运算带来的好处。这样,在这个例子中,<tt>a2</tt>就成了一个<math>s_2</math> X <math>m</math>的矩阵(<math>s_2</math>是网络第二层中的神经元数,<math>m</math>是训练样本个数)。矩阵<tt>a2</tt>的物理含义是,当第<math>i</math>个训练样本<tt>x(:i)</tt>输入到网络中时,它的第<math>i</math>列就表示这个输入信号对隐神经元 (网络第二层)的激励结果。 |
+ | |||
+ | |||
+ | 在上面的实现中,我们假定激活函数<tt>f(z)</tt>接受矩阵形式的输入<tt>z</tt>,并对输入矩阵按列分别施以激活函数。需要注意的是,你在实现<tt>f(z)</tt>的时候要尽量多用Matlab/Octave的矩阵操作,并尽量避免使用for循环。假定激活函数采用Sigmoid函数,则实现代码如下所示: | ||
+ | |||
<syntaxhighlight> | <syntaxhighlight> | ||
Line 50: | Line 60: | ||
end; | end; | ||
end | end | ||
+ | |||
% 高效的、向量化激活函数实现 | % 高效的、向量化激活函数实现 | ||
function output = vectorized_f(z) | function output = vectorized_f(z) | ||
- | output = 1./(1+exp(-z)); % "./" | + | output = 1./(1+exp(-z)); % "./" 在Matlab或Octave中表示对矩阵的每个元素分别进行除法操作 |
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
- | 最后,我们上面的正向传播向量化实现中忽略了<tt>b1</tt>和<tt>b2</tt> | + | |
+ | 最后,我们上面的正向传播向量化实现中忽略了<tt>b1</tt>和<tt>b2</tt>,现在要把他们包含进来,为此我们需要用到Matlab/Octave的内建函数<tt>repmat</tt>: | ||
+ | |||
<syntaxhighlight> | <syntaxhighlight> | ||
- | % | + | % 正向传播的向量化实现 |
z2 = W1 * x + repmat(b1,1,m); | z2 = W1 * x + repmat(b1,1,m); | ||
a2 = f(z2); | a2 = f(z2); | ||
Line 66: | Line 79: | ||
h = f(z3) | h = f(z3) | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
<tt>repmat(b1,1,m)</tt>的运算效果是,它把列向量<tt>b1</tt>拷贝<math>m</math>份,然后堆叠成如下矩阵: | <tt>repmat(b1,1,m)</tt>的运算效果是,它把列向量<tt>b1</tt>拷贝<math>m</math>份,然后堆叠成如下矩阵: | ||
+ | |||
::<math> | ::<math> | ||
Line 77: | Line 92: | ||
</math> | </math> | ||
- | |||
- | + | 这就构成一个<math>s_2</math> X <math>m</math>的矩阵。它和<tt>W1 * x</tt>相加,就等于是把<tt>W1 * x</tt>矩阵(译者注:这里<tt>x</tt>是训练矩阵而非向量, 所以<tt>W1 * x</tt>代表两个矩阵相乘,结果还是一个矩阵)的每一列加上<tt>b1</tt>。如果不熟悉的话,可以参考Matlab/Octave的帮助文档获取更多信息(输入“<tt>help repmat</tt>”)。<tt>rampat</tt>作为Matlab/Octave的内建函数,运行起来是相当高效的,远远快过我们自己用<tt>for</tt>循环实现的效果。 | |
- | + | ||
+ | ==反向传播== | ||
+ | |||
+ | |||
+ | 现在我们来描述反向传播向量化的思路。在阅读这一节之前,强烈建议各位仔细阅读前面介绍的正向传播的例子代码,确保你已经完全理解。下边我们只会给出反向传播向量化实现的大致纲要,而由你来完成具体细节的推导(见[[Exercise:Vectorization|向量化练习]])。 | ||
+ | |||
+ | |||
+ | 对于监督学习,我们有一个包含<math>m</math>个带类别标号样本的训练集<math>\{ (x^{(1)}, y^{(1)}), \ldots, (x^{(m)}, y^{(m)}) \}</math>。 | ||
(对于自编码网络,我们只需令<math>y^{(i)} = x^{(i)}</math>即可, 但这里考虑的是更一般的情况。) | (对于自编码网络,我们只需令<math>y^{(i)} = x^{(i)}</math>即可, 但这里考虑的是更一般的情况。) | ||
- | + | ||
+ | 假定网络的输出有<math>s_3</math>维,因而每个样本的类别标号向量就记为<math>y^{(i)} \in \Re^{s_3}</math>。在我们的Matlab/Octave数据结构实现中,把这些输出按列合在一起形成一个Matlab/Octave风格变量<tt>y</tt>,其中第<tt>i</tt>列<tt>y(:,i)</tt>就是<math>y^{(i)}</math>。 | ||
+ | |||
现在我们要计算梯度项<math>\nabla_{W^{(l)}} J(W,b)</math>和<math>\nabla_{b^{(l)}} J(W,b)</math>。对于梯度中的第一项,就像过去在反向传播算法中所描述的那样,对于每个训练样本<math>(x,y)</math>,我们可以这样来计算: | 现在我们要计算梯度项<math>\nabla_{W^{(l)}} J(W,b)</math>和<math>\nabla_{b^{(l)}} J(W,b)</math>。对于梯度中的第一项,就像过去在反向传播算法中所描述的那样,对于每个训练样本<math>(x,y)</math>,我们可以这样来计算: | ||
+ | |||
::<math> | ::<math> | ||
Line 97: | Line 121: | ||
</math> | </math> | ||
- | |||
- | + | 在这里<math>\bullet</math>表示对两个向量按对应元素相乘的运算(译者注:其结果还是一个向量)。为了描述简单起见,我们这里暂时忽略对参数<math>b^{(l)}</math>的求导,不过在你真正实现反向传播时,还是需要计算关于它们的导数的。 | |
+ | |||
+ | |||
+ | 假定我们已经实现了向量化的正向传播方法,如前面那样计算了矩阵形式的变量<tt>z2</tt>, <tt>a2</tt>, <tt>z3</tt>和<tt>h</tt>,那么反向传播的非向量化版本可如下实现: | ||
+ | |||
<syntaxhighlight> | <syntaxhighlight> | ||
Line 113: | Line 140: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
- | |||
- | 为做到这一点,我们先把向量<tt>delta3</tt>和<tt>delta2</tt>替换为矩阵,其中每列对应一个训练样本。我们还要实现一个函数<tt>fprime(z)</tt>,该函数接受矩阵形式的输入<tt>z</tt> | + | 在这个实现中,有一个<tt>for</tt>循环。而我们想要一个能同时处理所有样本、且去除这个<tt>for</tt>循环的向量化版本。 |
+ | |||
+ | |||
+ | 为做到这一点,我们先把向量<tt>delta3</tt>和<tt>delta2</tt>替换为矩阵,其中每列对应一个训练样本。我们还要实现一个函数<tt>fprime(z)</tt>,该函数接受矩阵形式的输入<tt>z</tt>,并且对矩阵的按元素分别执行<math>f'(\cdot)</math>。这样,上面<tt>for</tt>循环中的4行Matlab代码中每行都可单独向量化,以一行新的(向量化的)Matlab代码替换它(不再需要外层的<tt>for</tt>循环)。 | ||
+ | |||
+ | |||
+ | 在[[Exercise:Vectorization|向量化练习]]中,我们要求你自己去推导出这个算法的向量化版本。如果你已经能从上面的描述中了解如何去做,那么我们强烈建议你去实践一下。虽然我们已经为你准备了[[Backpropagation vectorization hints|反向传播的向量化实现提示]],但还是鼓励你在不看提示的情况下自己去推导一下。 | ||
- | |||
==稀疏自编码网络== | ==稀疏自编码网络== | ||
- | [[自编码算法与稀疏性|稀疏自编码]]网络中包含一个额外的稀疏惩罚项,目的是限制神经元的平均激活率,使其接近某个(预设的)目标激活率<math>\rho</math> | + | |
+ | [[自编码算法与稀疏性|稀疏自编码]]网络中包含一个额外的稀疏惩罚项,目的是限制神经元的平均激活率,使其接近某个(预设的)目标激活率<math>\rho</math>。其实在对单个训练样本上执行反向传播时,我们已经考虑了如何计算这个稀疏惩罚项,如下所示: | ||
+ | |||
:<math>\begin{align} | :<math>\begin{align} | ||
Line 128: | Line 161: | ||
+ \beta \left( - \frac{\rho}{\hat\rho_i} + \frac{1-\rho}{1-\hat\rho_i} \right) \right) f'(z^{(2)}_i) . | + \beta \left( - \frac{\rho}{\hat\rho_i} + \frac{1-\rho}{1-\hat\rho_i} \right) \right) f'(z^{(2)}_i) . | ||
\end{align}</math> | \end{align}</math> | ||
+ | |||
在非向量化的实现中,计算代码如下: | 在非向量化的实现中,计算代码如下: | ||
+ | |||
<syntaxhighlight> | <syntaxhighlight> | ||
Line 140: | Line 175: | ||
end; | end; | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
但在上面的代码中,仍旧含有一个需要在整个训练集上运行的<tt>for</tt>循环,这里<tt>delta2</tt>是一个列向量。 | 但在上面的代码中,仍旧含有一个需要在整个训练集上运行的<tt>for</tt>循环,这里<tt>delta2</tt>是一个列向量。 | ||
- | 作为对照,回想一下在向量化的情况下,<tt>delta2</tt>现在应该是一个有m列的矩阵,分别对应着<math>m</math>个训练样本。还要注意,稀疏惩罚项<tt>sparsity_delta</tt>对所有的训练样本一视同仁。这意味着要向量化实现上面的计算,只需在构造<tt>delta2</tt>时,往矩阵的每一列上分别加上相同的值即可。因此,要向量化上面的代码,我们只需简单的用<tt>repmat</tt>命令把<tt>sparsity_delta</tt>加到<tt>delta2</tt> | + | |
+ | 作为对照,回想一下在向量化的情况下,<tt>delta2</tt>现在应该是一个有m列的矩阵,分别对应着<math>m</math>个训练样本。还要注意,稀疏惩罚项<tt>sparsity_delta</tt>对所有的训练样本一视同仁。这意味着要向量化实现上面的计算,只需在构造<tt>delta2</tt>时,往矩阵的每一列上分别加上相同的值即可。因此,要向量化上面的代码,我们只需简单的用<tt>repmat</tt>命令把<tt>sparsity_delta</tt>加到<tt>delta2</tt>的每一列上即可(译者注:这里原文描述得不是很清楚,看似应加到上面代码中<tt>delta2</tt>行等号右边第一项,即<tt>W2'*delta3</tt>上)。 | ||
+ | |||
+ | |||
+ | |||
+ | ==中英文对照== | ||
+ | |||
+ | :向量化 vectorization | ||
+ | :正向传播 forward propagation | ||
+ | :反向传播 backpropagation | ||
+ | :训练样本 training examples | ||
+ | :激活函数 activation function | ||
+ | :稀疏自编码网络 sparse autoencoder | ||
+ | :稀疏惩罚 sparsity penalty | ||
+ | :平均激活率 average firing rate | ||
+ | |||
+ | ==中文译者== | ||
+ | |||
+ | 阎志涛(zhitao.yan@gmail.com), 谭晓阳(x.tan@nuaa.edu.cn), 邓亚峰(dengyafeng@gmail.com) | ||
+ | |||
+ | |||
+ | |||
+ | {{矢量化编程实现}} | ||
+ | |||
+ | |||
+ | {{Languages|Neural_Network_Vectorization|English}} |