Python 中的多层感知器
关于单隐层 MLP。
引言
在本文中,我们将探讨一种称为多层感知器 (MLP) 的监督学习算法,以及单隐藏层 MLP 的实现。
感知器
感知器是一种单元,它通过根据其输入权重形成线性组合,然后可能将输出通过称为激活函数的非线性函数,从多个实值输入计算出单个输出。
下图说明了感知器的操作
[图片来自此链接。]
感知器的输出可以表示为
$f(x) = G( W^T x+b)$
- (x) 是输入向量
- ((W,b)) 是感知器的参数
- (f) 是非线性函数
多层感知器
MLP 网络由输入层、输出层和隐藏层组成。每个隐藏层由许多感知器组成,这些感知器称为隐藏单元。
下图说明了多层感知器的前馈神经网络架构。
单隐藏层 MLP 包含一个感知器数组。
MLP 隐藏层的输出可以表示为一个函数
(f(x) = G( W^T x+b))
(f: R^D \rightarrow R^L),
其中 D 是输入向量 (x) 的大小
(L) 是输出向量的大小
(G) 是激活函数。
如果激活函数 G 是 sigmoid 函数,则仅包含输出层的单层 MLP 等同于逻辑回归分类器
$\begin{align} f_{i}(x)=\frac{e^{W_{i}x+b_{i}}}{\sum_{j} e^{W_{j}x+b_{j}}} \end{align}$
输入层的每个单元对应于输入向量的元素。
逻辑回归分类器的每个输出单元生成一个预测概率,表示输入向量属于特定类。
前馈神经网络
我们首先考虑最经典的情况,即单隐藏层神经网络。
输入到隐藏层的数量是 (d),隐藏层的输出数量是 (m)。
隐藏层执行将维度为 (d) 的向量映射到维度为 (m) 的向量。
MLP 的每个隐藏层单元都可以通过权重矩阵和偏置向量 (W,b) 以及激活函数 (\mathcal{G}) 来参数化。隐藏层的输出是激活函数应用于输入和权重向量的线性组合。
权重矩阵和偏置向量的维度由所需的输出单元数决定。
如果隐藏层的输入数量/输入的维度是 (\mathcal{M}),输出数量是 (\mathcal{N}),那么权重向量的维度是 (\mathcal{N}x\mathcal{M}),偏置向量的维度是 (\mathcal{N}x1)。
我们可以认为隐藏层由 (\mathcal{N}) 个隐藏单元组成,每个隐藏单元接收一个 (\mathcal{M}) 维向量并产生一个单一输出。
输出是输入层的仿射变换,然后应用函数 $f(x)$,该函数通常是非线性函数,如 sigmoid 或反正切双曲函数。
向量值函数 (h(x)) 是隐藏层的输出。
$ h(x) = f(W^T x + c ) $
MLP 的输出层通常是逻辑回归分类器,如果需要概率输出用于分类,则激活函数是 softmax 回归函数。
单隐藏层多层感知器
设:
- (h_{i-1}) 表示第 i 层的输入向量
- (h_{i}) 表示第 i 层的输出向量
- (h_{0})=x 是表示输入层的向量
- (h_{n}=y) 是输出层,它产生所需的预测输出
- (f(x)) 表示激活函数
因此,我们表示每个隐藏层的输出为
$h_{k}(x) = f(b_{k} + w_{k}^T h_{i-1}(x)) = f(a_{k}) $
考虑 sigmoid 激活函数,函数关于参数的梯度可以写为
$\begin{align} \frac{\partial \mathbf{h}_{k}(x) }{\partial \mathbf{a}_{k}}= f(a_{k})(1- f(a_{k})) \end{align}$
层中每个隐藏单元 (i) 的计算可以表示为
$h_{k,i}(x) = f(b_{k,i} + W_{k,i}^T h_{i-1}(x)) = f(a_{k}(x))$
输出层是逻辑回归分类器。输出是一个概率输出,表示输入属于预测类的置信度。为此定义的成本函数是训练数据上的负对数似然
$L = -log (p_{y}) $
目的是最大化 (p_{y}= P( Y =y_{i} \| x )) 作为给定输入是 (x) 的类 (y) 的条件概率的估计器。这是训练算法的成本函数。
反向传播算法
反向传播算法是递归梯度算法,用于优化 MLP 参数相对于定义的损失函数。因此,我们的目标是计算 MLP 的每一层隐藏单元,以便最大化成本函数。
与逻辑回归一样,我们计算权重相对于成本函数的梯度。计算各种隐藏层中所有权重的成本函数梯度。执行标准梯度优化以获得最小化似然函数的参数。
输出层决定成本函数。由于我们将逻辑回归作为输出层。成本函数是 softmax 函数。设 L 表示成本函数。
反向传播算法与其他优化技术没有什么不同。目的是确定网络中权重和偏置如何变化。
$ \begin{align} \frac{\partial L}{\partial W_{k,i,j} } \text{ and } \frac{\partial L}{\partial b_{k,i,j} } \end{align}$.
输出层
$\begin{align} L = -log ( f(a_{k,i}) ) \end{align}$
$\begin{align} \frac{\partial L }{\partial \mathbf{a}_{k,i}} = \frac{\partial L }{\partial \mathbf{h}_{k,i}} \frac{\partial \mathbf{h}_{k,i} }{\partial \mathbf{a}_{k,i}} = -\frac{1}{h_{k,i}} * h_{k,i}*(1-h_{k,i}) = (h_{k,i}-1)\end{align} $
$ \begin{align} \frac{\partial L }{\partial \mathbf{a}_{k,i}} =\mathbf{h}_{k,j} - 1_{y=y_{i}} \end{align}$
上述表达式可以视为输出中的误差。当 (y=y_{i}) 时,误差为 (1-p_{i}),当 (y \ne y_{i}) 时,预测误差为 (p_{i})。
隐藏层
$\begin{align}\frac{\partial L }{\partial \mathbf{a}_{k-1,j}} = \frac{\partial L }{\partial \mathbf{h}_{k-1,j}} \frac{\partial \mathbf{h}_{k-1,j} }{\partial \mathbf{a}_{k-1,j}} \end{align}$
因此,思想是从最底层开始计算梯度。要计算第 i 层参数的成本函数梯度,我们需要知道第 (i+1) 层参数的成本函数梯度。
我们从逻辑回归分类器级别开始梯度计算。它们向后传播,更新每一层的参数。
让我们考虑其他隐藏层的情况。
$\begin{align} \frac{\partial L }{\partial \mathbf{h}_{k-1,j}} = \sum_i \frac{\partial L }{\partial \mathbf{a}_{k,i}}\frac{\partial \mathbf{a}_{k,i} }{\partial \mathbf{h}_{k-1,j}} = \sum_i \frac{\partial L }{\partial \mathbf{a}_{k,i}} W_{k,i,j} \end{align} $
上述公式的实现如下:
def linear_gradient(self,weights,error):
""" The function computes gradient of likelyhood function wrt output of hidden layer
:math:`\\begin{align} \\frac{\partial L }{\partial \mathbf{h}_{k-1,j}} \\end{align}`
Parameters
------------
weights : ndarray,shape=(n_out,n_hidden)
weights of next hidden layer, :math:`\\begin{align} \mathbf{W}_{k,i,j} \\end{align}`
error : ndarray,shape=(n_out,)
backpropagated error from next layer :math:`\\begin{align} \\frac{\partial L }{\partial \mathbf{a}_{k,i}} \\end{align}`
Returns
-----------
out : ndarray,shape=(n_hidden,)
compute the backpropagated error, :math:`\\begin{align} \\frac{\partial L }{\partial \mathbf{h}_{k-1,j}} \\end{align}`
"""
return numpy.dot(error,weights);
隐藏层的参数梯度计算如下:
$\begin{align}\frac{\partial L }{\partial \mathbf{W}_{k-1,i,j}} = \frac{\partial L }{\partial \mathbf{a}_{k-1,j}} \frac{\partial \mathbf{a}_{k-1,j} }{\partial \mathbf{W}_{k-1,i,j}}=\frac{\partial L }{\partial \mathbf{a}_{k-1,j}} \mathbf{h}_{k-2,j} \end{align}$
$\begin{align}\frac{\partial L }{\partial \mathbf{b}_{k-1,i}} = \frac{\partial L }{\partial \mathbf{a}_{k-1,i}} \frac{\partial \mathbf{a}_{k-1,i} }{\partial \mathbf{b}_{k-1,i}}=\frac{\partial L }{\partial \mathbf{a}_{k-1,i}} \end{align}$
这作为以下方式实现,其中输入
- (x) 代表 (\begin{align} \frac{\partial \mathbf{h}_{k,j} }{\partial \mathbf{a}_{k,j}} \end{align}) - 输出梯度
- (y) 代表 (\begin{align} h_{k-2,j} \end{align}) - 激活
- (w) 代表 (\begin{align} \frac{\partial L }{\partial \mathbf{a}_{k-1,i}}\end{align}) - 误差
def compute_error(self,x,w,y):
"""
function computes the gradient of the likelyhood function wrt to parameters of the hidden layer for single input
Parameters
-------------
x : ndarray,shape=(n_hidden,)
w : ndarray,shape=(n_hidden,)
`w` represents :math:`\\begin{align} \\frac{\partial L }{\partial \mathbf{h}\_{k,i}}\end{align}` the gradient of the likelyhood fuction wrt output of hidden layer
y : ndarray,shape=(n_in,)
`y` represents :math:`\mathbf{h}\_{k-2,j}` the input hidden layer
Returns
------------
res : ndarray,shape=(n_in+1,n_hidden)
:math:`\\begin{align} \\frac{\partial L }{\partial \mathbf{W}\_{k-1,i,j}} \\text{ and } \\frac{\partial L }{\partial \mathbf{W}\_{k-1,i}} \end{align}`
"""
x=x*w;
#gradient of likelyhood function wrt input activation
res1=x.reshape(x.shape[0],1);
#gradient of likelyhood function wrt weight matrix
res=np.dot(res1,y.reshape(y.shape[0],1).T);
self.eta=0.0001
#code for L1 and L2 regularization
if self.Regularization==2:
res=res+self.eta*self.W;
if self.Regularization==1:
res=res+self.eta*np.sign(self.W);
#stacking the parameters and preparing for returning
res=np.hstack((res,res1));
return res.T;
def cost_gradients(self,weights,activation,error):
""" function to compute the gradient of log
likelyhood function wrt the parameters of the hidden layer
averaged over all the input samples.
Parameters
-------------
weights : numpy,shape(n_out,n_hidden),
weight matrix of the next layer,W\_{k,i,j}
activation: numpy,shape=(N,n_in)
input to the hidden layer \mathbf{h}\_{k-2,j}
error : numpy,shape=(n_out,)
\frac{\partial L }{\partial \mathbf{a}\_{k,i}}
Returns
-------------
gW : ndarray,shape=(n_hidden,n_in+1)
coefficient parameter matrix of next hidden layer,
:math:`\\begin{align} \\frac{\partial L }{\partial \mathbf{W}\_{k-1,i,j}} \\text{ and } \\frac{\partial L }{\partial \mathbf{W}\_{k-1,i}} \end{align}`
"""
we=self.linear_gradient(weights,error)
ag=self.activation_gradient()
e=[ self.compute_error(a,we,b) for a,b in izip(ag,activation)]
gW=np.mean(e,axis=0).T
return gW;
一旦我们获得了梯度并计算了新参数,就会调用 `update` 函数来更新模型中的新参数。
该函数由优化器模块调用,该模块执行基于 SGD 的优化,所有优化参数(如学习率)均由优化器方法处理。
def update_parameters(self,params):
""" function to updated the learn parameters to the model
Parameters
----------
grads : ndarray,shape=(n_hidden,n_in+1)
coefficient parameter matrix
"""
self.params=params;
param1=self.params.reshape(-1,self.nparam);
self.W=param1[:,0:self.nparam-1];
self.b=param1[:,self.nparam-1];
实现细节
HiddenLayer
类封装了预测、分类、训练、梯度计算和误差传播所需的所有方法。
HiddenLayer
类的重要属性是
Attributes
-----------
`out` : array-like ,shape=[n_out]
The output of hidden layer
`params`:array-like ,shape=[n_out,n_in+1]
parameters of hidden layer
`W,b`:array-like,shape=[n_out,n_int],shape=[n_out,1]
parameters in the form of weight matrix and bias vector characterizing
the hidden layer
`activation`:function
the non linear activation function
.. note :
in the below functions to n_hidden denotes the number of output units of present hidden layer
n_out denotes the number of output units of next hidden layer
and n_in denotes the size of input vector to present hidden layer
def compute(self,input):
"""function computes the output of the hidden layer for input matrix
Parameters
----------
input : ndarray,shape=(N,n_in)
:math:`h_{i-1}(x)` is the `input`
Returns
-----------
output : ndarray ,shape=(N,n_out)
:math:`f(b_k + w_k^T h_{i-1}(x))` ,affine transformation over input
"""
#performs affine transformation over input vector
linout=numpy.dot(self.W,input.T)+np.reshape(self.b,(self.b.shape[0],1));
#applies non linear activation function over computed linear transformation
self.output=self.activation(linout).T;
return self.output;
MLP 类封装了预测、分类、训练、前向和后向传播、保存和加载模型等所有方法。
下面显示了三个重要函数。`learn` 函数在每个优化器循环中调用。
这会调用前向和后向迭代方法并更新每个隐藏层的参数。
前向迭代仅计算网络的输出,而 `propagate_backward` 函数负责将适当的输入和权重传递给每个隐藏层,以便它可以执行后向算法循环。
前向迭代仅计算网络的输出,而 `propagate_backward` 函数负责将适当的输入和权重传递给每个隐藏层,以便它可以执行后向算法循环
def propagate_backward(self,error,weights,input):
""" the function that executes the backward propagation loop on hidden layers
Parameters
----------------
error : numpy array,shape=(n_out,)
average prediction error over all the input samples in output layer
:math:`\\begin{align}\frac{\partial L }{\partial \mathbf{a}_{k,i}} \\end{align}`
weight : numpy array,shape=(n_out,n_hidden)
parameter weight matrix of the output layer
input : ndarray,shape=(n_samples,n_in)
input training data
Returns
----------------
None
"""
#input matrix for the hidden layer
input1=input;
for i in range(self.n_hidden_layers):
prev_error=np.inf;
best_grad=[];
for k in range(1):
""" computing the derivative of the parameters of the hidden layers"""
hidden_layer=self.hiddenLayer[self.n_hidden_layers-i-1];
hidden_layer.compute(input1);
# computing the gradient of likelyhood function wrt the parameters of the hidden layer
grad=hidden_layer.cost_gradients(weights,input1,error);
#update the parameter of hidden layer
res=self.update(hidden_layer.params,grad.flatten(),0.13);
""" update the parameters """
hidden_layer.update_parameters(res);
#set the weights ,inputs and error required for the back propagation algorithm
#for the next layer
weights=hidden_layer.W;
error=grad[:,hidden_layer.n_in];
self.hiddenLayer[self.n_hidden_layers-i-1]=hidden_layer;
input1=hidden_layer.output;
def propagate_forward(self,input):
"""the function that performs forward iteration to compute the output
Parameters
-----------
input : ndarray,shape=(n_samples,n_in)
input training data
"""
self.predict(input)
def learn(self,update):
""" the main function that performs learning,computing gradients and updating parameters
this is called by the optimizer module for each iteration
Parameters
----------
update - python function
this represents the update function that performs the gradient descent iteration
"""
#set the training data
x,y=self.args;
#set the update function
self.update=update;
#execute the forward iteration loop
self.propagate_forward(x)
#set the input for output layer
args1=(self.hidden_output,y);
#set the input for the output logistic regression layer
self.logRegressionLayer.set_training_data(args1);
#gradient computation and parameter updation of output layer
[params,grad]=self.logRegressionLayer.learn(update);
self.logRegressionLayer.update_params(params);
#initialize the gradiients and weights for backward error propagation
error=grad;
weights=self.logRegressionLayer.W;
#perform the backward iteration over the hidden layers
if self.n_hidden_layers >0:
weights=self.logRegressionLayer.W;
self.propagate_backward(error,weights,x)
return [None,None];
选择模型参数
如前所述,MLP 由输入层、隐藏层和输出层组成。确定隐藏单元数量没有固定规则。参数是特定于应用程序的,并且最佳参数通常通过经验测试过程得出。较少的隐藏单元会导致泛化和训练误差增加,而拥有大量训练单元会导致训练大量参数以及训练时间显著增加的问题。
MLP 的问题
MLP 训练中观察到的一个问题是学习速度慢。下图说明了选择较小的学习参数或不合适的正则化常数时的学习过程性质。可以实现各种自适应方法来提高性能,但收敛慢和学习时间长是基于神经网络的学习算法的问题。
代码
与 MLP 相关的重要文件是
- MLP.py
- LogisticRegression.py
- Optimizer.py
该代码的最新版本可以在 Github 存储库中找到:www.github.com/pi19404/pyVision。
本文使用的文件可以从以下链接下载:
数据集和模型文件可以在 models 和 data 存储库下找到:
- MLP.pyvision - 模型文件
- mnist.pkl.gz - 数据文件
在运行代码之前,请在 MLP.py 文件中做出适当的路径更改。