主页 > IT百科 > 机器学习之五:神经网络、反向传播算法
2014年05月21日

机器学习之五:神经网络、反向传播算法

一、逻辑回归的局限

在逻辑回归一节中,����使用逻辑回归的多分类,��实现了识别20*20的图片上的数字。

但所使用的是一个一阶的模型,�������并没有使用多项式,������为什么?

可以设想一下,���在原有400个特征的数据样本中,增加二次、三次、四次多项式,会是什么情形?

很显然,训练样本的特征数量将会拔高多个数量级,而且,更重要的,要在一个式子中拟合这么多的特征,其难度是非常大的,可能无法收敛到一个比较理想的状态。

也就是说,逻辑回归没法提供很复杂的模型。

因为其本质上是一个线性的分类器,擅长解决的是线性可分的问题。

那么非线性可分问题,要怎么解决?

解决思路

如果有一种方法,将非线性可分问题先进行特征提取,变为接近线性可分,那么再应用一次逻辑回归,是否就能解决非线性问题了?

这便是神经网络的思想。

二、神经网络

1、结构

神经网络的结构,如下图所示

image

上面是一个最简单的模型,分为三层:输入层、隐藏层、输出层。

其中,隐藏层可以是多层结构,通过扩展隐藏层的结构,可以构建更得杂的模型,例如下面的模型:

image

每一层的输出,皆是下一层的输出,层层连接而成,形成一个网络。

网络中的节点,称为神经元。每个神经元,其实就是进行一次类似逻辑回归的运算(之所以说是"类似",是因为可以使用逻辑回归,也有别的算法代替,但可以使用逻逻回归来理解它的运算机理)。

根据上面前言中的分析,显然,隐藏层是进行特征的提取,而输出层,其实就是进行逻辑回归。

为何说隐藏层是进行特征提取?

为方便理解,这里假设所有神经元执行逻辑回归。

一次逻辑回归,可以将平面一分为二。神经网络中,执行的是 N 多个逻辑回归,那么可以将平面切割为 N 多个区域,这些区域最后由输出层进行综合后做为结果。

如果只关注输出层,那么这些前面切割出来的区域,其实可以当作是一种特征,是一种更高级的特征,由原始样本提取出来的。这就是特征的提取。

2、计算原理

2.1 前向传播,计算输出

下面求解当一个样本从输入层输入时,如何得到最终结果。

假设每个神经元,都执行逻辑回归的计算,则第 \(i\) 层网络的输出为:\[a^{(i)} = g(z^{(i)}) = g(\Theta^Ta^{(i-1)}) \tag{1}\]

以如下三层网络为例:

image

各层的输入输出如下:

Input layer:

\[ a^{(1)} = x \]
Hidden layer:

\[ \begin{split} z^{(2)} &= \Theta^{(1)}a^{(1)} \\ a^{(2)} &= g(z^{(2)}) \end{split} \]
Output layer:

\[ \begin{split} z^{(3)} &= \Theta^{(2)}a^{(2)} \\ a^{(3)} &= g(z^{(2)}) \end{split} \]

即整个网络的最终结果为:

\[ h_\theta(x) = a^{(3)} \]

上述流程:以上一层的输出,作为下一层的输入,一层一层叠加运算后,得到最终的输出,这个计算方法,称为“前向传播”

2.2 反向传播,求theta矩阵

训练算法的目的是“求取使得误差函数最小化的参数矩阵”,用梯度下降法处理最小化误差,需要计算误差函数J、以及J对theta的偏导数。

2.2.1 误差函数J

\[ J(\Theta) = -\frac{1}{m} \sum_{i=1}^{m}\sum_{k=1}^{K}[y_k^{(i)}log(h_\Theta(x^{(i)}))_k + (1-y_k^{(i)})log(1-h_\theta(x^{(i)}))_k] + \frac{\lambda}{2m}\sum_{l=1}^{L-1}\sum_{i=1}^{S_l}\sum_{j=1}^{S_l+1}(\Theta_{ji}^{(l)})^2 \tag{2} \]

其中 \(K\) 为输出层的单元数,即类别数。在计算误差的时候,需要将每一类都计算进去。后面的正则项是整个神经网络中所有的参数 \(\Theta\) 的值之和。

2.2.2 J对theta偏导数

这里先给结果,后面再做推导:

\[ \frac{\partial}{\partial\Theta_{ij}^{(l)}}J(\Theta) = \frac{1}{m}\sum_{t=1}^{m}\delta_i^{(t)(l+1)}a_j^{(t)(l)} + \frac{\lambda}{m}\sum_{l=1}^{L-1}\sum_{i=1}^{S_l}\sum_{j=1}^{S_l+1}(\Theta_{ji}^{(l)}) \tag{3} \]

其中

\[ \begin{cases} \delta^{(L)} &=& a^{(L)}-y \\ \\ \delta^{(l)} &=& \delta^{(l+1)}*(\Theta^{(l+1)})^T*g'(z^{(l)}) \\ \\ \delta^{(0)} &=& 0 \\ \end{cases} \tag{4} \]

上述公式描述的内容

\(l\) 层的误差,可以通过第 \(l+1\) 层的误差计算出来,而最后一层的误差,就是系统通过前向传播计算出的值与样本 \(Y\) 值的差。
也就是说,从输出层开始,各层误差能通过一层一层反向迭代的方式得到,确定误差之后,偏导数便也随之计算出来,进而可进行模型的调整。这就是,“反向传播算法”

而反向传播的内容,其实是误差。

  • 关于误差的直观理解:

输出层的误差,即为系统的总误差;

中间层的误差,即为每一层对总误差的贡献值(所以,\(\theta\) 矩阵,在前向传播中,是特征权重,而在反向传播中,就是误差权重);

而输入层,其输出即为原始数据,即无误差。

2.2.3 反向传播算法的推导过程

(1) 第一部分,推导偏导数

上面给出了反向传播的结论,以下进行推导。

矩阵形式计算第 \(l\) 层的偏导数:

\[ \begin{split} \frac{\partial J(\Theta)}{\partial\Theta^{(l)}} &= \frac{\partial J(\Theta)}{\partial z^{(l+1)}} * \frac{\partial z^{(l+1)}}{\partial \Theta^{(l)}} \\ \\ &= \frac{\partial J(\Theta)}{\partial z^{(l+1)}} * \frac{\partial (\Theta^{(l)}*a^{(l)})}{\partial \Theta^{(l)}} \\ \\ &= \frac{\partial J(\Theta)}{\partial z^{(l+1)}} * a^{(l)} \end{split} \tag{5} \]

\[\delta^{(l)} = \frac{\partial J(\Theta)}{\partial z^{(l)}} \tag{6}\]

则有

\[ \begin{split} \frac{\partial J(\Theta)}{\partial\Theta^{(l)}} &=& \frac{\partial J(\Theta)}{\partial z^{(l+1)}} * a^{(l)} \\ \\ &=& \delta^{(l+1)} * a^{(l)} \\ \end{split} \tag{7} \]

(2) 第二部分,推导误差delta

上面推导过程中,有这个式子:

\[ \delta^{(l)} = \frac{\partial J(\Theta)}{\partial z^{(l)}} \]

表示了什么意思?下面分别从输出层、及中间层来推导、解释这个式子。

  • 输出层

因误差函数如下(这里省略掉正则项)

\[ J(\Theta) = -\frac{1}{m} \sum_{i=1}^{m}\sum_{k=1}^{K}[y_k^{(i)}log(h_\Theta(x^{(i)}))_k + (1-y_k^{(i)})log(1-h_\theta(x^{(i)}))_k] \]

此式表达的是总误差,那么,对于输出层的每个神经元的误差,可用矩阵表示为:

\[ C = - [ylog(h_\Theta(x)) + (1-y)log(1-h_\theta(x))] \tag{8} \]

故输出层的误差为:

\[ \begin{split} \delta^{(L)} &=& \frac{\partial J(\Theta)}{\partial z^{(L)}} = \frac{\partial C}{\partial z^{(L)}} \\ \\ &=& \frac{\partial }{\partial z^{(L)}} [ylog(h_\Theta(x)) + (1-y)log(1-h_\theta(x))] \\ \\ &=& -\frac{y}{g(z^{(L)})}g'(z^{(L)}) - \frac{1-y}{1-g(z^{(L)})}(-g'(z^{(L)})) \\ \\ &=& \frac{g(z^{(L)})-y}{g(z^{(L)})(1-g(z^{(L)}))}(g'(z^{(L)})) \\ \\ &=& g(z^{(L)})-y \\ \\ &=& a^{(L)}-y \end{split} \tag{9} \]

这个结果,有点意思了,表示出输层的 \(\delta\) 值,就是系统输出值与样本 \(Y\) 值的差。所以,我们称 \(\delta\) 为神经系统各层结构的各个神经元的误差。

  • 中间层误差推导

对于第 \(l\)

\[ \begin{split} \delta^{(l)} &=& \frac{\partial J(\Theta)}{\partial z^{(l)}} \\ \\ &=& \frac{\partial J(\Theta)}{\partial z^{(l+1)}} * \frac{\partial z^{(l+1)}}{\partial z^{(l)}} \\ \\ &=& \delta^{(l+1)} * \frac{\partial [(\Theta^{(l+1)})^T*g(z^{(l)})]}{\partial z^{(l)}} \\ \\ &=& \delta^{(l+1)} * (\Theta^{(l+1)})^T*g'(z^{(l)}) \\ \end{split} \tag{10}\]

即第 \(l\) 层的误差,能用第 \(l+1\) 层的误差计算得到,与先前所定的结论完全一致。

这就是反向传播的所有推导的内容。

三、程序实现

例子来源于,吴恩达的机器学习编程题。样本与逻辑回归中的多分类的数字识别相同。

1、计算损失函数、及梯度

function [J grad] = nnCostFunction(nn_params, ...
                                   input_layer_size, ...
                                   hidden_layer_size, ...
                                   num_labels, ...
                                   X, y, lambda)
Theta1 = reshape(nn_params(1:hidden_layer_size * (input_layer_size + 1)), ...
                 hidden_layer_size, (input_layer_size + 1));

Theta2 = reshape(nn_params((1 + (hidden_layer_size * (input_layer_size + 1))):end), ...
                 num_labels, (hidden_layer_size + 1));

% Setup some useful variables
m = size(X, 1);
         
% You need to return the following variables correctly 
J = 0;
Theta1_grad = zeros(size(Theta1));
Theta2_grad = zeros(size(Theta2));


% ------ 前向传播计算输出 ------

% input layer
a1 = [ones(m, 1) X]; %add +1 to X;
% hidden layer
a2 = sigmoid(a1 * Theta1');
a2 = [ones(m, 1) a2];
% output layer
a3 = sigmoid(a2 * Theta2');

% ------ 样本的Y值 ------
% [1 0 0 0 0 0 0 0 0 0] -- the value is 1
% [0 1 0 0 0 0 0 0 0 0] -- the value is 2
Y = zeros(m,num_labels);
for i = 1 : m
    Y(i,y(i)) = 1;
end

% ------ 损失函数J ------
J = (sum(sum(-Y .* log(a3))) - sum(sum((1-Y) .* log(1-a3)))) / m ;
% remove theta0
t1 = Theta1(:,2:end);
t2 = Theta2(:,2:end);
regularize = lambda / 2 / m * (sum(sum(t1.^2)) + sum(sum(t2.^2)));
J = J + regularize;

% ------ 反向传播计算各层误差 ------
delta3 = a3 - Y;
delta2 = delta3 * Theta2 .* a2 .* (1 - a2);
delta2 = delta2(:,2:end);

% ------ 计算梯度 ------
Theta1_grad = ( delta2' * a1 + [zeros(size(t1,1),1) t1] * lambda) / m;
Theta2_grad = ( delta3' * a2 + [zeros(size(t2,1),1) t2] * lambda) / m;

% Unroll gradients
grad = [Theta1_grad(:) ; Theta2_grad(:)];

end

2、前向传播及计算delta中,需要用到sigmoid函数及其导数

2.1 sigmoid函数

function g = sigmoid(z)
g = 1.0 ./ (1.0 + exp(-z));
end

2.2 sigmoid函数的导数

function g = sigmoidGradient(z)
g = sigmoid(z) .* (1 - sigmoid(z));
end

3、训练过程

3.1、随机初始化theta参数矩阵

initial_Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size);
initial_Theta2 = randInitializeWeights(hidden_layer_size, num_labels);

% Unroll parameters
initial_nn_params = [initial_Theta1(:) ; initial_Theta2(:)];

逻辑回归中,theta矩阵可以初始化为同一个值,如全0或全1。但神经网络中却不行。

原因在于:神经网络中,神经元是以全连接的形式组织起来的,即n-1层的任意一个节点,都与第n层所有节点相连接。

若是初始化时theta矩阵初始化为同一个值,同一个层的每一个神经元都进行相同的运算,多个神经元进行相同的运算,这对于数据的拟合没有任何用处,只是浪费资源,造成冗余。此为对称现象。

随机初始化参数的实现如下:

function W = randInitializeWeights(L_in, L_out)
W = zeros(L_out, 1 + L_in);
epsilon_init = 0.12;
W = rand(L_out, 1 + L_in) * 2 * epsilon_init - epsilon_init;
end

3.2、初始化参数

options = optimset('MaxIter', 100);

% 正则项参数
lambda = 1;

% 损失函数
costFunction = @(p) nnCostFunction(p, ...
                                   input_layer_size, ...
                                   hidden_layer_size, ...
                                   num_labels, X, y, lambda);
% 梯度下降计算参数
[nn_params, cost] = fmincg(costFunction, initial_nn_params, options);

% 获取两层神经网络的参数
Theta1 = reshape(nn_params(1:hidden_layer_size * (input_layer_size + 1)), ...
                 hidden_layer_size, (input_layer_size + 1));

Theta2 = reshape(nn_params((1 + (hidden_layer_size * (input_layer_size + 1))):end), ...
                 num_labels, (hidden_layer_size + 1));

4、预测

pred = predict(Theta1, Theta2, X);

fprintf('\nTraining Set Accuracy: %f\n', mean(double(pred == y)) * 100);

可以看到,其预测结果,比逻辑回归准确率高接近3个点。

原因在于:神经网络所能构建的模型,比逻辑回归更为复杂,其对数据的拟合能力也更强。

predict函数,使用训练得到的参数矩阵,前向传播计算得到结果即为输出层,输出层表示一个输入样本经过经神网络计算之后,其可能属于各个分类的概率值。与逻辑回归类似,取最大值即为最终的结果。

function p = predict(Theta1, Theta2, X)

m = size(X, 1);
num_labels = size(Theta2, 1);

p = zeros(size(X, 1), 1);

h1 = sigmoid([ones(m, 1) X] * Theta1');
h2 = sigmoid([ones(m, 1) h1] * Theta2');
[dummy, p] = max(h2, [], 2);

end