【神经网络】从0至1构建神经网络,进行手写数字的识别

1. 神经网络?

神经网络是机器学习中的一类算法,其基于多层的线性结构和激活函数。深度学习发展到现在,出现了各种各样的封装库,可以轻易就实现神经网络算法。但声明一下,神经网络与人脑的人精网络毫无关系。用图来表示一下神经网路算法的结构:

在这里插入图片描述

我们把做左边一列称为输入层,最右边一列称为输出层,中间的一列称为中间层,也称为隐藏层。上述的神经网络一般称为两层神经网络(从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+w2x20)(b+w1x1+w2x2>0)
其实神经网络也是类似的方式,在每一个神经元内部,将原本通过线性公式得到的结果进行二次映射。

2. 激活函数

上述的函数会将输入信号的综合转换为输出信号,这种函数一般称之为激活函数,激活函数的作用在于决定如何来激活输入信号的总和。为了进一步表现出神经元之间的联系,我们将上述图略作改造:

在这里插入图片描述

如图所示,a代表通过线性公式得到的初步结果,y代表经激活函数映射后得到的最终结果。

上述的激活函数以阈值为界,一旦输入超过阈值,就切换输出。 这样的函数称为“阶跃函数”。因此,可以说感知机中使用了阶跃函数作为激活函数。下面来介绍一下神经网络中使用的激活函数。

2.1 sigmoid函数

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))

2.2 ReLU函数

在神经网络发展的历史上,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)(x0)
ReLU 函数是一个非常简单的函数。因此, ReLU函数的实现也很简单。

def relu(x):
	return np.maximum(0, x)

2.3 softmax函数

对于输出层的设计,如果是回归问题,一般使用恒等函数,分类问题则使用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)=Ci=1nexp(ai)Cexp(ak)=i=1nexp(ai+C)exp(ak+C)
这里的C撇可以使用任何值,但是为了防止溢出,一般会使用输入信号中的最大值。

2.4 思考与分析

为什么激活函数一般是非线性的,或者说,为什么要有激活函数?我们已知神经元之间通过线性关系进行联系,神经元内部通过激活函数进行二次转换。如果说激活函数是线性的,那么这里多层线性模型叠加在一起,做的也仅仅是一个多项式回归了,那么加深神经网络的层数是没有意义的。此外,由于多层函数的嵌套叠加,神经网络的表达式一般也是难以直接写出来的。

3. 损失函数

为什么要有损失函数,神经网络在训练中以某个指标为线索寻找最优权重参数,神经网络中的损失函数可以用各种函数,但一般使用均方误差和交叉熵误差。

3.1 均方误差

均方误差是非常常用的一种损失函数,一般用在回归问题上。
E = 1 2 ∑ k ( y k − t k ) 2 E=\frac{1}{2} \sum_{k}\left(y_{k}-t_{k}\right)^{2} E=21k(yktk)2
这里,yk是表示神经网络的输出,tk表示监督数据,k表示数据的维数。

def mean_squared_error(y, t):
	return 0.5 * np.sum((y-t)**2)

3.2 交叉熵误差

除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损失函数。交叉熵误差一般用于分类问题。
E = − ∑ k t k log ⁡ y k E=-\sum_{k} t_{k} \log y_{k} E=ktklogyk
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=N1nktnklogynk

3.3 mini-batch学习

当我们使用某些大数据集进行训练时候,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据, 数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。

神经网络的学习也是从训练数据中选出一批数据(称为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]

4. 手写数字识别

了解基本架构以后,我们尝试解决实际问题,这里及逆行手写数字图像的分类。使用的数据集是MNIST手写数字图像集,MNIST数据集是由0到9的数字图像构成的。训练图像有6万张, 测试图像有1万张。每张图片的像素为28*28。

4.1 导入数据集

首先,我们还是导入一下相关库。

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)

4.2 其他准备工作

提前定义好激活函数,这肯定是十分必要的。

# 定义一下激活函数
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

4.3 定义一个二层神经网络类

下面,我们打算建立一个二层神经网络,输出层是长度 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)
4.3.1 预测函数

传入神经网络,和 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
4.3.2 计算损失

根据交叉熵误差的公式,计算当前参数下的损失函数。

# x:输入数据, t:监督数据/实际答案   返回某一项的交叉熵误差(损失)
def loss(self, x, t):
    y = self.predict(x)
    return cross_entropy_error(y, t)
4.3.3 计算准确率
# 计算准确率
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
4.3.4 梯度函数

计算权重参数的梯度,需要调用损失函数和求导函数。

    # 定义一个梯度函数,返回梯度 / 导数
    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

上面四小节里定义的函数,都放置在神经网络类里面。

4.4 模型训练

根据前面定义的类,我们首先初始化超参数,并定义一个神经网络。

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()

5. 不足和改进

(1)参数优化。神经元的个数、学习率、梯度下降的迭代次数,这些参数都是需要或者说值得进一步进行优化的,既要保证模型的可靠性,但也要保证算法的运行时间。

(2)算法改进。本文的程序,总体的运行速度时间很长,虽然本人暂时不知道从何下手进行优化,但相比于已经打包好的 api ,我的算法运行速度就极为逊色了。另外,我也挺说过一些比梯度下降更加复杂、但运行更加迅速的优化算法,值得探索。

6. 参考资料

【1】深度学习入门基于 Python 的理论与实现[M].斋藤康毅

【2】机器学习–Andrew Ng

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

⁠脱欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值