Multilayer Perceptron
下面我们使用Theano来介绍一下单隐藏层的多层感知机(MLP)。MLP可以看成一个logistic回归分类器,它使用一个已经学习的非线性转换器处理输入。这个转换器把输入变成一个线性可分离的空间。中间层被看作是隐藏层。单个隐藏层足够让MLPs普遍逼近,但是我们会在后面看到使用多层隐藏层是很有效的。
The Model
一个只有单层隐藏层的MLP可以表示成一下形式:
通常地,一个单隐藏层的MLP是一个函数。其中D是输入x的大小,L是输出向量f(x)的大小,f(x)的矩阵表示如下:
其中偏置项b,权重项W和激活函数G和s。
向量组成了隐藏层。是一个权值矩阵链接输入层和隐藏层。每一列代表到第i个隐藏层每个输入的权值。s的传统选择包括tanh函数,,或者sigmoid函数,。本教程我们使用tanh函数因为它训练得更快(有时会达到一个更好的局部最小值).tanh和sigmoid函数都是标量到标量的函数但是它们可以使用元素依次运算自然地扩展到向量或张量。
输出向量是。读者需知道我们之前在前一课的形式。以前,类别的成员概率可以被softmax函数来表示。
要训练一个MLP,我们要学习所有的参数,这里我们使用小批量随机下降。要学习的参数集是。使用梯度可以进行后向传播算法。值得感谢的是,Theano会自动实现这个过程,这里就不详述。
Going from logistic regression to MLP
本教程会关注单隐藏层的MLP。我们实现一个类来表示一个隐藏层。为了构造MLP我们将在后面在顶层使用logistic回归层。
class HiddenLayer(object):
def __init__(self, rng, input, n_in, n_out, W=None, b=None, activation=T.tanh):
‘‘‘
传统MLP的隐藏层:全链接并且使用sigmoid激活函数。权重矩阵大小为(n_in, n_out)
偏置项b是(n_out,)
提醒:非线性转换器使用tanh
隐藏层单元的激活函数: tanh(dot(input, W) + b)
:rng 类型: numpy.random.RandomState
:rng 参数: 随机初始化权值
:input 类型: theano.tensor.dmatrix
:input 参数:符号张量,形状(n_examples, n_in)
:n_in 类型: 整数
:n_in 参数: 输入的维数
:n_out 类型: 整数
:n_out 参数: 隐藏层的维数
:activation 类型: theano.Op
:actibation 参数: 应用在隐藏层的非线性
‘‘‘
self.input = input
隐藏层权值的初始化需要在对应激活函数的对称区间内抽样。对于tanh函数,,其中(fan)in是第i-1层单元的数量,(fan)out是第i层单元的数量。对于sigmoid函数。这样初始化保证了在训练的时候,激活函数的神经运算信息可以更容易地前向或者后向传播。
# ‘W‘被‘W_values‘初始化,区间为tanh的[sqrt(-6./(n_in+n_hidden))
# 到 sqrt(6./(n_in+n_hidden))]。
# 定义输出的dtype为theano.config.floatX有利于在GPU上运算。
# 提示:最佳的权值初始化取决于激活函数的使用。
# 例如:[Xavier10]的结果建议你,对比tanh使用4倍大的sigmoid的权重
# 但是我们没有其他函数的信息,所以我们使用和tanh相同的。
if W is None:
W_values = numpy.array(
rng.uniform(
low = -numpy.sqrt(6. / (n_in + n_out)),
high = numpy.sqrt(6. / (n_in + n_out)),
size = (n_in, n_out)
),
dtype = theano.config.floatX
)
if activation == theano.tensor.nnet.sigmoid:
W_values *= 4
W = thenao.shared(value=W_values, name=‘W‘, borrow=True)
if b is None:
b_values = numpy.zeros((n_out,), dtype=theano.config.floatX)
b = theano.shared(value=b_values, name=‘b‘, borrow=True)
self.W = W
self.b = b
我们使用一个给定的非线性函数当作隐藏层的激活函数。默认使用tanh,但是有些情况我们会使用其他函数。
lin_output = T.dot(input, self.W) + self.b
self.output = (
lin_output if activation is None
else activation(lin_output)
)
如果你看过类实现图的理论计算。如果给定这个图当作输入传给之前实现的LogisticRegression类,你会获得MLP的输出。
class MLP(object):
"""Muti-Layer Perceptron Class
一个多层感知机是一个前馈人工神经网络,它有一个或多个隐藏单元和非线性的激活器。
中间层有一个激活函数tanh或者sigmoid函数(HiddenLayer类),另外顶层是一个
softmax层(LogisticRegression)
"""
def __init__(self, rng, input, n_in, n_hidden, n_out):
# 当我们处理一个隐藏层,它链接一个激活函数tanh和一个Logistic回归层。
# 激活函数可以被sigmoid函数或其他代替
self.hiddenLayer = HiddenLayer(
rng = rng,
input=input,
n_in=n_in,
n_out=n_hidden,
activation=T.tanh
)
# Logistic回归成获得隐藏层的输入
self.logRegressionLayer = LogisticRegression(
input = self.hiddenLayer.output,
n_in = n_hidden,
n_out=n_out
)
本教程我们会使用L1和L2正则
# L1正则
self.L1 = (
abs(self.hiddenLayer.W).sum()
+ abs(self.logRegressionLayer.W).sum()
)
# L2正则
self.L2_sqr = (
(self.hiddenLayer.W ** 2).sum()
+ (self.logRegressionLayer.W ** 2).sum()
)
# 负对数似然
self.negative_log_likelihood = (
self.logRegressionLayer.negative_log_likelihood
)
# 错误率
self.errors = self.logRegressionLayer.errors
# 两层的参数
self.params = self.hiddenLayer.params + self.logRegressionLayer.parmas
在之前,我们用小批量随机下降法来训练模型。不同的是我们使用损失函数是带有L1和L2正则式的。L1_reg和L2_reg是控制权重的正则式。
cost = (
classifier.negative_log_likelihood(y)
+ L1_reg * classifier.L1
+ L2_reg * classifier.L2_sqr
)
然后我们使用梯度来更新模型的参数。这段代码类似logsitic函数。只是参数数量不同而已。为了做到这样(可以运行在不同数量参数的代码),我们会使用参数列表,在每一代计算梯度。
# 对列表的参数一个个求导
gparams = [T.grad(cost, param) for param in classfier.params]
# 约定怎样更新权值,形式是(值, 更新表达式)对
# 给定两个相同长度的列表 A=[a1, a2, a3, a4]和
# B = [b1, b2, b3, b4],zip操作生成一个
# C = [(a1, b1), ..., (a4, b4)]
updates = [
(param, param - learning_rate * gparam)
for param, gparam in zip(classifier.params, gparams)
]
# 编译一个Theano函数‘train_model’返回损失并且更新权值
train_model = theano.function(
inputs = [index],
outputs = cost,
updates = updates,
givens = {
x: train_set_x[index * batch_size : (index + 1) * batch_size]
y: train_set_y[index * batch_size : (index + 1) * batch_size]
}
)
Putting it All Together
Tips and Tricks for training MLPs
在上面的代码中有一些超参数,它不能用梯度下降来优化。严格来说,找到一个最佳的解集不是一个可行的问题。第一,我们不能独立地优化每一个参数。第二,我们不能一下子应用前面说的梯度下降技术(一部分是因为一些参数是离散的,而另一些是实数的)。第三,优化问题不是一个凸优化,找到一个(局部)最优解会涉及一堆不重要的工作。
在过去的25年里,好消息是研究者在神经网络设计了很多选择超参数的规则。想了解更多可以看Yann LeCun,Leon Bottou,Genevieve Orr和Klaus-Robert的Efficient BackPro。在这里,我们总结一些方法,是一些我们应用在代码中的重点。
Nonlinearity
两个常用的方法是sigmoid和tanh,原因在Section 4.4中。非线性在刚开始对称因为它使输入变成平均值为0的输入到下一层。经验告诉我们,tanh有更好地收敛性质。
Weight initialization
在初始化我们想权值在刚开始时足够小,那样的话激活函数的运算就会在线性工作状态,这样导数最大。另外合适的性质,特别在深度网络,是保存激活器的方差和层与层之间的反向传播梯度的方差。这允许信息在向上和向下都很好地流动,减少层之间的差异。在某些假设下,两个限制的妥协导致了下面的初始化方式:
tanh初始化权值
sigmoid初始化权值
数学推导请看Xavier10
Learning rate
在文献上有好的处理方法。最简单的方法就是常数学习率。经验法则是:尝试几个对数空间值(10的-1, 10的-2, …)和缩小(对数的)格子去搜索哪里获得最小检验错误。
多次降低学习率有时是一个很好的方法。一个简单的规则就是,其中u0是初始学习率,d是下降常数来控制下降速度(通常一个小的正数,10的-3或更小),t是epoch/stage。
Section 4.7参看更多信息。
Number of hidden units
超参数很依赖于数据集。含糊地说,输入的分布越复杂,网络要建模的能力越强,就需要越多的隐藏层。
除非我们使用正则化,典型数量的隐藏层 vs. 泛化表现就像一个U字。
Regularization parameter
经典的方法就是使用L1/L2正则参数,10的-2,10的-3, …,在以后的框架里,优化这个参数不会带来什么显著的变化,但是有时也值得尝试。