目录
3 神经网络
前一章介绍了感知机模型。单层感知机只能表示线性空间,无法解决非线性分类问题。(当然多层可以解决)
另一个非常严重的问题就是,感知机的参数需要人为设定,这无疑是个非常庞大的工程。
神经网络的出现就是为了解决这些问题。神经网络的一个重要性质就是它可以根据反向传播过程自动地从数据中学习到合适的权重参数。
3.1 从感知机到神经网络
3.1.1 神经网络的例子
神经网络主要由,最左侧的输入层,中间隐藏层(可以有多层,不限),最右侧输出层。
事实上,就神经网络中神经元(就是每一个节点)的连接方式而言,和感知机没有任何区别。
3.1.2 复习感知机
(注意上图没有画出偏置项)
对于上图所示的感知机中,接收和两个输入信号,输出。用数学公式表示如下:
表示偏置项,表示神经元被激活的容易程度;而和是各个信号的权重,表示各个信号的重要性。
为了简化上式引入一个函数:
3.1.3 激活函数登场
刚引入的会将输入信号和总和转换为输出信号,这种函数一般被称为激活函数。
3.2 激活函数
前面引入的以阈值为届,一旦超过这个阈值,就切换为输出。这样的函数称为“阶跃函数”。感知机使用的激活函数就是这样的阶跃函数。当把激活函数从阶跃函数换成其他函数,就进入神经网络的世界了。
3.2.1 sigmoid函数
神经网络中经常使用的一个激活函数就是sigmoid函数,这是一个s型函数:
3.2.2 阶跃函数的实现
采用一种支持NumPy数组的实现:
def step_function(x):
y = x > 0
return y.astype(np.int)
3.2.3 阶跃函数的图像
import numpy as np
import matplotlib.pyplot as plt
def set_function(x):
return np.array(x > 0, dtype=np.int)
x = np.arange(-5.0, 5.0, 0.1)
y = set_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # 指定y轴范围
plt.show()
3.2.4 sigmoid函数的实现
def sigmoid(x):
return 1 / (1 + np.exp(-x))
图像:
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # 指定y轴范围
plt.show()
3.2.5 sigmoid函数和阶跃函数的比较
sigmoid较阶跃函数有更平滑的特性,对神经网络的学习有重要意义(梯度下降)。
另一个不同点是,阶跃函数只能返回0和1,而sigmoid可以返回0到1之间的任意数(这一点和平滑性也有关)。
共同点是:1. 两者结构均是“输入小时,输出接近0(为0);随着输入增大,输出向1靠近(变成1)”。也就是说,当输入信号为重要信息时,阶跃函数和sigmoid函数都会输出较大的值;当输入信号为不重要的信息时,两者都输出较小的值。2. 不骨干输入的信号有多小,或者有多大,输出信号的值都在0到1之间。
3.2.6 非线性函数
阶跃函数和sigmoid函数均为非线性函数。神经网络的激活函数一般都必须是非线性函数。如果是非线性函数,加深隐藏层就没有意义了,因为该结构(全连接)总可以表示为:。
3.2.7 ReLU函数
除了前面介绍的两个非线性函数,还有一个常用的(尤其在RNN中,用以解决梯度消失问题)ReLU函数(Rectified Linear Unit, 线性整流函数)。
import numpy as np
import matplotlib.pyplot as plt
def relu(x):
return np.maximum(0, x)
x = np.arange(-5.0, 5.0, 0.1)
y = relu(x)
plt.plot(x, y)
plt.ylim(-0.1, 5.1) # 指定y轴范围
plt.show()
3.3 多维数组的运算
3.3.1 多维数组
a = np.array([[1.1, 1.2, 1.3], [2.1, 2.2, 2.3], [3.1, 3.2, 3.3]])
print(a)
np.ndim(a) # 返回a的纬度
a.shape # tuple类型,返回a的形状
3.3.2 矩阵乘法
np.dot(A, B)
矩阵乘法即线性代数中的常规矩阵乘法。
3.3.3 神经网络的内积
对于如图所示的神经网络(省略了偏置和激活函数):
X = np.array([1, 2])
W = np.array([[1, 3, 5], [2, 4, 6]])
Y = np.dot(X, W)
3.4 3层神经网络的实现
3层神经网络:输入层(第0层)有两个神经元,第一层隐藏层(第1层)有三个神经元,第二层隐藏层(第2层)有两个神经元,输出层(第3层)有两个神经元。
3.4.1 符号确认
3.4.2 各层间信号传递的实现
从输入层到第1层的信号传递。
这里添加了表示偏置的神经元“1”。
如果使用矩阵乘法,则该式可表示为:
其中:
,
,
,
下面使用NumPy实现上式(这里参数均为任意给出值,只为演示作用):
X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])
A1 = np.dot(X, W1) + B1
然后在上面的计算过程中加入激活函数:
Z1 = sigmoid(A1)
对于第1层到第2层的信号传递:
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
除了第1层的输出(Z1)变成了第二层的输入这一点以外,两者没有区别。
最后是第2层到输出层的信号传递:
def identity_function(x):
return x
W3 = np.array([[0.1, 0.3], [0.2, 0.4]])
B3 = np.array([0.1, 0.2])
A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3) # 或 Y = A3
输出层的实现也和之前的实现基本相同,不过,这里的激活函数和前面的隐藏层有所不同。这里定义了identity_function()函数(也称为“恒等函数”),并将其作为输出层的激活函数。恒等函数会将输入按照原样输出。
输出层所用的激活函数,要根据求解问题的性质决定。一般地,回归问题可以使用恒等函数,二分类问题可以使用sigmoid函数,多分类问题可以使用softmax函数。
3.4.3 代码实现小结
把前面3层神经网络的实现整理一下:
import numpy as np
def identity_function(x):
return x
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def init_network():
network = {}
network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
network['b2'] = np.array([0.1, 0.2])
network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
network['b3'] = np.array([0.1, 0.2])
return network
def forward(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
a1 = np.dot(x , W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = identity_function(a3)
return y
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y) # [0.31682708 0.69627909]
3.5 输出层的设计
神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出层的激活函数。一般而言,回归问题使用恒等函数,分类问题使用softmax函数。
分类问题是数据属于哪一个类别的问题,而回归问题是根据某个输入预测一个(连续的)数值问题。
3.5.1 恒等函数和softmax函数
恒等函数会将输入按原样输出,对于输入的信息,不加以任何改动地直接输出。
分类问题中使用的softmax函数可以使用下面的公式表示:
上式表示假设输出层共有个神经元,计算第个神经元的输出。
softmax的分子是输入信号的指数函数,分母是所有输入信号的指数的和。
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
3.5.2 实现softmax函数时的注意事项
上述实现存在一定缺陷,可能会溢出,由于在softmax中要进行指数函数运算,但是此时指数函数的值容易变的非常大,例的结果会返回一个表示无穷大的inf。
上式说明,在进行softmax的指数函数的运算时,加上(或减去)某个常数并不会改变运算的结果。这里可以使用任意值,但是为了防止溢出,一般会使用输入信号中的最大值。
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
3.5.3 softmax函数的特征
a = np.array([0.3, 2.9, 4.0])
y = softmax(a)
print(y) # [0.01821127 0.24519181 0.73659691]
如上图所示,softmax函数的输出是到之间的实数,并且输出值的总和总是1。因此softmax的输出可以解释为概率。
需要注意的是,即便使用了softmax函数,各个元素之间的大小关系也不会改变。这是因为指数函数是单调递增函数。一般而言,神经网络只把输出值最大的神经元所对应的类别作为识别结果。并且即便使用softmax函数,输出值最大值的位置也不会改变,因此,神经网络在进行分类时,输出层的softmax函数可视需求省略掉(指数运算需要一定的计算量)。
3.5.4 输出层的神经元数量
输出层的神经元数量需要根据待解决的问题来决定。对于分类问题而言,输出层的神经元数量一般设定为类别的数量(预测结果可采用one-hot编码,例如数字预测,图中的数是0到9十个数字中的哪一个,[0, 0, 1, 0, 0, 0, 0, 0, 0, 0]则表示预测结果为2,向量预测结果对应的索引位置值为1,其它位置为0。)。回归问题一般只有一个输出。二分类问题一般也只有一个输出,只需要表示是或者不是即可。
3.6 手写数字识别
设计一个MNIST数据集的前向传播过程。
3.6.1 MNIST数据集
MNIST数据集是右0到9的数字图像构成的。训练图像有6万张,测试图像有1万张,这些图像可用于学习和推理。
MNIST的图像数据是28px * 28px的灰度图像(1通道),各个像素的取值在0到255之间。每个图像数据都相应地标有该图实际对应的数字标签。
from dataset.mnist import load_mnist
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)
load_mnist函数以“(训练图像,训练标签),(测试图像,测试标签)”的形式返回读入的MNIST数据。
此外,还可以像load_mnist(normalize=True, flatten=True, one_hot_label=False)。
第一个参数normalize设置是否将输入图像像素正规化为0.0 ~ 1.0的值。如果该参数为False,则输入图像的像素会保持原来的0 ~ 255。
第二个参数flatten设置是否展开成一维数组。如果为false,则输入图像为 1*28*28的三维数组,若设置为True,则输入图像为长度为784(=1*28*28)的一维数组。
第三个参数one_hot_label是是否将标签保存为one-hot编码。如果为True,则输入标签为长度为10的数组,标签对应索引值为1,其它为0。如果为False,则输入标签为实际对应值。举个栗子:如果标签值为2,则True情况下输入为[0, 0, 1, 0, 0, 0, 0, 0, 0, 0]。如果为False输入就是 2。
# pip install Pillow
from dataset.mnist import load_mnist
import numpy as np
from PIL import Image
def img_show(img):
pil_img = Image.fromarray(np.uint8(img))
pil_img.show()
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
img = x_train[0]
label = t_train[0]
print(label) # 5
print(img.shape) # (784,)
img = img.reshape(28, 28)
print(img.shape) # (28, 28)
img_show(img)
3.6.2 神经网络的推理处理
2个隐藏层,第一个隐藏层有50个神经元,第二个隐藏层有100个神经元。(这里50和100可以换成其它值,需要在数组维度上保持一致即可)
先定义三个函数
def get_data():
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=True, one_hot_label=False)
return x_test, t_test
# init_network()会读入保存在pickle文件sample_weight.pkl中学习到的权重参数
# 这个文件中以字典变量的形式保存了权重和偏置参数
def init_network():
with open("sample_weight.pkl", "rb") as f:
network = pickle.load(f)
return network
def predict(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = softmax(a3)
return y
x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
y = predict(network, x[i])
p = np.argmax(y)
if p == t[i]:
accuracy_cnt += 1
print("accuracy: " + str(float(accuracy_cnt) / len(x)))
这基本就是前向传播的流程了。不过这个程序本身是不能直接运行的,原文并没有训练这一部分,原文是吧事先训练好的参数直接拿过来用了,而不是这里当场训练(由init_network()可知)。
简单的神经网络对MNIST数据集的最好训练结果目前基本就是92%~93%。后面可以使用别的方法进一步优化。
3.6.3 批处理
神经网络对训练数据一般会打包一次性处理一批,称为一个batch。这对训练的效率会有提升。
对矩阵乘法不熟的可以仔细看看原文这部分的描述。这里只感受一下批处理的用法。
x, t = get_data()
network = init_network()
batch_size = 100 # 批数量
accuracy_cnt = 0
for i in range(0, len(x), batch_size):
x_batch = x[i:i+batch_size]
y_batch = predict(network, x_batch)
p = np.argmax(y_batch, axis=1)
accuracy_cnt += np.sum(p == t[i:i+batch_size])
print("accuracy: " + str(float(accuracy_cnt) / len(x)))
本章所学内容
- 神经网络中的激活函数使用平滑变化的sigmoid函数或ReLU函数
- 通过巧妙地使用NumPy多维数组,可以高效地实现神经网络
- 机器学习的问题大体上可以分为回归问题和分类问题
- 关于输出层的激活函数,回归问题中一般用恒等函数,分类问题中一般用softmax函数
- 分类问题中,输出层的神经元的数量设置为要分类的类别数
- 输入数据的集合称为批。通过以批为单位进行推理处理,能够实现高速的运算。