神经网络是机器学习中的一类算法,其基于多层的线性结构和激活函数。深度学习发展到现在,出现了各种各样的封装库,可以轻易就实现神经网络算法。但声明一下,神经网络与人脑的人精网络毫无关系。用图来表示一下神经网路算法的结构:
我们把做左边一列称为输入层,最右边一列称为输出层,中间的一列称为中间层,也称为隐藏层。上述的神经网络一般称为两层神经网络(从0开始计数),第0、1、2层分别为输入层,中间层,输出层。
在感知机中,内部网络结构大致如下所示:
感知机通过接受到的x1和x2信号,各自对应的权重w1和w2,以及偏置,返回0或者1。其内部逻辑公式如下所示:
y = { 0 ( b + w 1 x 1 + w 2 x 2 ⩽ 0 ) 1 ( b + w 1 x 1 + w 2 x 2 > 0 ) y=\left\{\begin{array}{ll} 0 & \left(b+w_{1} x_{1}+w_{2} x_{2} \leqslant 0\right) \\ 1 & \left(b+w_{1} x_{1}+w_{2} x_{2}>0\right) \end{array}\right. y={01(b+w1x1+w2x2⩽0)(b+w1x1+w2x2>0)
其实神经网络也是类似的方式,在每一个神经元内部,将原本通过线性公式得到的结果进行二次映射。
上述的函数会将输入信号的综合转换为输出信号,这种函数一般称之为激活函数,激活函数的作用在于决定如何来激活输入信号的总和。为了进一步表现出神经元之间的联系,我们将上述图略作改造:
如图所示,a代表通过线性公式得到的初步结果,y代表经激活函数映射后得到的最终结果。
上述的激活函数以阈值为界,一旦输入超过阈值,就切换输出。 这样的函数称为“阶跃函数”。因此,可以说感知机中使用了阶跃函数作为激活函数。下面来介绍一下神经网络中使用的激活函数。
h ( x ) = 1 1 + exp ( − x ) h(x)=\frac{1}{1+\exp (-x)} h(x)=1+exp(−x)1
神经网络中使用 sigmoid 函数作为激活函数,进行信号的转换,转换后的信号被传送给下一个神经元。事实上,上面的感知机和接下来的神经网络的主要区别就在于这个激活函数。其他方面,比如神经元的多层 连接的构造、信号的传递方法等,基本上和感知机是一样的。
def sigmoid(x):
return 1 / (1 + np.exp(-x))
在神经网络发展的历史上,sigmoid函数很早就开始使用了,经典的逻辑回归算法使用的也是sigmoid函数。而最近主要使用的是ReLU函数,ReLU函数在输入大于0时,直接输出该值;在输入小于等于0时,输出0。
h ( x ) = { x ( x > 0 ) 0 ( x ⩽ 0 ) h(x)=\left\{\begin{array}{ll} x & (x>0) \\ 0 & (x \leqslant 0) \end{array}\right. h(x)={x0(x>0)(x⩽0)
ReLU 函数是一个非常简单的函数。因此, ReLU函数的实现也很简单。
def relu(x):
return np.maximum(0, x)
对于输出层的设计,如果是回归问题,一般使用恒等函数,分类问题则使用softmax函数。softmax的公式如下:
y k = exp ( a k ) ∑ i = 1 n exp ( a i ) y_{k}=\frac{\exp \left(a_{k}\right)}{\sum_{i=1}^{n} \exp \left(a_{i}\right)} yk=∑i=1nexp(ai)exp(ak)
其中n代表假设输出层的神经元个数,计算第k个神经元的输出y_k。函数的分子是输入信号a_k的指数函数分母是所有输入信号的指数函数的和。
其实,用softmax得到的结果y_k,正是在输入信号为某种情况下时第k种分类结果的概率。另外,softmax函数的代码如下:
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
注意事项,softmax函数的实现中有指数的运算,可能会出现超大值,超大值之间进行除法运算,结果会出现“不确定”的情况。因此可以对softmax函数略作改进:
y k = exp ( a k ) ∑ i = 1 n exp ( a i ) = Cexp ( a k ) C ∑ i = 1 n exp ( a i ) = exp ( a k + C ′ ) ∑ i = 1 n exp ( a i + C ′ ) \begin{aligned} y_{k}=\frac{\exp \left(a_{k}\right)}{\sum_{i=1}^{n} \exp \left(a_{i}\right)} &=\frac{\operatorname{Cexp}\left(a_{k}\right)}{\mathrm{C} \sum_{i=1}^{n} \exp \left(a_{i}\right)} \\ &=\frac{\exp \left(a_{k}+\mathrm{C}^{\prime}\right)}{\sum_{i=1}^{n} \exp \left(a_{i}+\mathrm{C}^{\prime}\right)} \end{aligned} yk=∑i=1nexp(ai)exp(ak)=C∑i=1nexp(ai)Cexp(ak)=∑i=1nexp(ai+C′)exp(ak+C′)
这里的C撇可以使用任何值,但是为了防止溢出,一般会使用输入信号中的最大值。
为什么激活函数一般是非线性的,或者说,为什么要有激活函数?我们已知神经元之间通过线性关系进行联系,神经元内部通过激活函数进行二次转换。如果说激活函数是线性的,那么这里多层线性模型叠加在一起,做的也仅仅是一个多项式回归了,那么加深神经网络的层数是没有意义的。此外,由于多层函数的嵌套叠加,神经网络的表达式一般也是难以直接写出来的。
为什么要有损失函数,神经网络在训练中以某个指标为线索寻找最优权重参数,神经网络中的损失函数可以用各种函数,但一般使用均方误差和交叉熵误差。
均方误差是非常常用的一种损失函数,一般用在回归问题上。
E = 1 2 ∑ k ( y k − t k ) 2 E=\frac{1}{2} \sum_{k}\left(y_{k}-t_{k}\right)^{2} E=21k∑(yk−tk)2
这里,yk是表示神经网络的输出,tk表示监督数据,k表示数据的维数。
def mean_squared_error(y, t):
return 0.5 * np.sum((y-t)**2)
除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损失函数。交叉熵误差一般用于分类问题。
E = − ∑ k t k log y k E=-\sum_{k} t_{k} \log y_{k} E=−k∑tklogyk
log表示以e为底数的自然对数。yk是神经网络的输出,tk是正确解标签。并且,tk中只有正确解标签的索引为1,其他均为0(one-hot表示)。如果要求所有训练数据的损失函数的综合,那么应该写成下面形式:
E = − 1 N ∑ n ∑ k t n k log y n k E=-\frac{1}{N} \sum_{n} \sum_{k} t_{n k} \log y_{n k} E=−N1n∑k∑tnklogynk
当我们使用某些大数据集进行训练时候,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据, 数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。
神经网络的学习也是从训练数据中选出一批数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。比如,从60000个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习。代码实现如下:
train_size = x_train.shape[0]
batch_size = 100
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
了解基本架构以后,我们尝试解决实际问题,这里及逆行手写数字图像的分类。使用的数据集是MNIST手写数字图像集,MNIST数据集是由0到9的数字图像构成的。训练图像有6万张, 测试图像有1万张。每张图片的像素为28*28。
首先,我们还是导入一下相关库。
import time
import sys,os
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image # 用来显示图片
接下来导入数据集:
sys.path.append(os.pardir)
from dataset.mnist import load_mnist
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True,normalize=False)
当 flatten 参数为 true 时,将原本28*28的矩阵转化为长度为784的一维矩阵。normalize 属性代表是否要进行标准化。我们也可以打开图片进行展示:
# 展示图片
def img_show(img):
pil_img = Image.fromarray(np.uint8(img))
pil_img.show()
img = x_train[0]
label = t_train[0]
img = img.reshape(28,28) # 把形状变为原来的尺寸
img_show(img)
提前定义好激活函数,这肯定是十分必要的。
# 定义一下激活函数
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 溢出对策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
定义好损失函数,即交叉熵误差。这里有考虑到根据 softmax 函数,我们输出的 y 其实是一个长度为10的一维矩阵,
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
接下来,我们有必要定义一个求导函数。为什么要定义一个求导函数,我们前面提到神经网络的函数表达式是不可以直接解析出来的,因此它的损失函数其实也无法准确解析出来,所以我们只有根据导数定义的方式进行求导。
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = tmp_val + h
fxh1 = f(x) # 计算出f(x+h)
x[idx] = tmp_val - h
fxh2 = f(x) # 计算出f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h) # 求解导数
x[idx] = tmp_val
it.iternext()
return grad
下面,我们打算建立一个二层神经网络,输出层是长度 784 的数组,中间层的神经元个数为 50 个,使用 sigmoid 激活函数;输出层的激活函数使用 softmax 函数。下图中仅仅展现部分神经元,h() 和 g() 分别代表两个激活函数。
首先进行定义和初始化:参数从头开始依次表示输入层的神经元数、隐藏层 的神经元数、输出层的神经元数。
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
# 初始化权重
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
传入神经网络,和 x ,返回对应的预测值。
def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1) # 第一次激活函数
a2 = np.dot(z1, W2) + b2
y = softmax(a2) # 第二个激活函数
return y
根据交叉熵误差的公式,计算当前参数下的损失函数。
# x:输入数据, t:监督数据/实际答案 返回某一项的交叉熵误差(损失)
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
# 计算准确率
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
计算权重参数的梯度,需要调用损失函数和求导函数。
# 定义一个梯度函数,返回梯度 / 导数
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
return grads
上面四小节里定义的函数,都放置在神经网络类里面。
根据前面定义的类,我们首先初始化超参数,并定义一个神经网络。
train_loss_list = []
# 超参数
iters_num = 1000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.4
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
接下来,我们开始进行梯度下降算法:
for i in range(iters_num):
# 获取mini-batch
batch_mask = np.random.choice(train_size, batch_size) # 随机筛选出一百个出来
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 计算梯度
grad = network.numerical_gradient(x_batch, t_batch)
# 更新参数
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
# print(i,loss,end=' ')
print(network.accuracy(x_train,t_train))
值得注意,前面定义的一系列函数,虽然乍一看并不复杂,但实际上一些求导和函数求解涉及到了大量的向量/矩阵计算,导致整体的程序运行速度较慢。完成训练以后,我们也可以对结果进行可视化:
plt.plot(train_loss_list)
plt.title('损失函数迭代曲线')
plt.savefig('loss.png')
plt.show()
(1)参数优化。神经元的个数、学习率、梯度下降的迭代次数,这些参数都是需要或者说值得进一步进行优化的,既要保证模型的可靠性,但也要保证算法的运行时间。
(2)算法改进。本文的程序,总体的运行速度时间很长,虽然本人暂时不知道从何下手进行优化,但相比于已经打包好的 api ,我的算法运行速度就极为逊色了。另外,我也挺说过一些比梯度下降更加复杂、但运行更加迅速的优化算法,值得探索。
【1】深度学习入门基于 Python 的理论与实现[M].斋藤康毅
【2】机器学习–Andrew Ng