正文
3.1 从感知机到神经网络
3.1.1 神经网络的例子
我们把最左边的一列称为输入层,最右边的一列称为输出层,中间的一列称为中间层。中间层有时也称为隐藏层。“隐藏”一词的意思是,隐藏层的神经元(和输入层、输出层不同)肉眼看不见。另外,本书中把输入层到输出层依次称为第 0 层、第 1 层、第 2 层(层号之所以从 0 开始,是为了方便后面基于 Python 进行实现)。
第 0 层对应输入层,第 1 层对应中间层,第 2 层对应输出层。
上图中的网络一共由 3 层神经元构成,但实质上只有 2 层神经元有权重,因此将其称为“2 层网络”。请注意,有的书也会根据构成网络的层数,把上图的网络称为“3 层网络”。本书将根据实质上拥有权重的层数(输入层、隐藏层、输出层的总数减去 1 后的数量)来表示网络的名称。
3.1.2 复习感知机
将式子
改写为更加简洁的式子,引入新函数 :,其中
输入信号的总和会被函数 转换,转换后的值就是输出 。 然后,上式所表示的函数 ,在输入超过 0 时返回 1,否则返回 0。
3.1.3 激活函数登场
刚才登场的 函数会将输入信号的总和转换为输出信号,这种函数一般称为激活函数(activation function)。
上图表示神经元的○中明确显示了激活函数的计算过程,即信号的加权总和为节点 ,然后节点 被激活函数 转换成节点 。本书中,“神经元”和“节点”两个术语的含义相同。这里,我们称 和 为“节点”,其实它和之前所说的“神经元”含义相同。
3.2 激活函数
在激活函数的众多候选函数中,感知机使用了阶跃函数。那么,如果感知机使用其他函数作为激活函数的话会怎么样呢?实际上,如果将激活函数从阶跃函数换成其他函数,就可以进入神经网络的世界了。
3.2.1sigmoid 函数
神经网络中经常使用的一个激活函数:sigmoid 函数(sigmoid function)
3.2.2 阶跃函数的实现
def step_function(x):
"""
阶跃函数(允许参数取 Numpy 数组的形式)
"""
y = x > 0
return y.astype(int)对 NumPy 数组进行不等号运算后,数组的各个元素都会进行不等号运算,生成一个布尔型数组。这里,数组 x 中大于 0 的元素被转换为 True,小于等 于 0 的元素被转换为 False,从而生成一个新的数组 y。数组 y 是一个布尔型数组,但是我们想要的阶跃函数是会输出 int 型的 0 或 1 的函数。因此,需要把数组 y 的元素类型从布尔型转换为 int 型。
import numpy as np
x = np.array([-1.0, 1.0, 2.0])
y = x > 0
yarray([False, True, True])
y = y.astype(int)
yarray([0, 1, 1])
3.2.3 阶跃函数的图形
import numpy as np
import matplotlib.pyplot as plt
def strp_function(x):
return np.array(x > 0, dtype=int)
x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # 指定 y 轴的范围
plt.show()
3.2.4sigmoid 函数的实现
def sigmoid(x):
return 1 / (1 + np.exp(-x))x = np.array([-1.0, 1.0, 2.0])
sigmoid(x) # 根据 NumPy 的广播功能,如果在标量和 NumPy 数组之间进行运算,则标量会和 NumPy 数组的各个元素进行运算。array([0.26894142, 0.73105858, 0.88079708])
x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)
plt.show()
3.2.6 非线性函数
-
神经网络的激活函数必须使用非线性函数。
-
使用线性函数的话,加深神经网络的层数就没有意义了。
-
为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。
-
3.2.7 ReLU 函数
在神经网络发展的历史上,sigmoid 函数很早就开始被使用了,而最近则主要使用**ReLU(Rectified Linear Unit)**函数。
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)
plt.show()
3.3 多维数组的运算
3.3.3 神经网络的内积
上图中的简单神经网络为对象。这个神经网络省略了偏置和激活函数,只有权重。
实现该神经网络时,要注意 、、 的形状,特别是 和 的对应 维度的元素个数是否一致,这一点很重要。
X = np.array([1, 2])
W = np.array([[1, 3, 5], [2, 4, 6]])
Y = np.dot(X, W)
Yarray([ 5, 11, 17])
使用np.dot(多维数组的点积),可以一次性计算出 的结果。
这意味着,即便 的元素个数为 100 或 1000,也可以通过一次运算就计算出结果!如果不使用 np.dot,就必须单独计算 的每一个元素(或者说必须使用for语句),非常麻烦。因此,通过矩阵的乘积一次性完成计算的技巧,在实现的层面上可以说是非常重要的。
3.4 3 层神经网络的实现
3.4.1 符号确认
3.4.2 各层间信号传递的实现
如果使用矩阵的乘法运算,则可以将第 1 层的加权和表示成下面的式:
其中,、、、 如下所示:
,,
输入层到第 1 层的信号传递:
import numpy as np
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]])
print(W1.shape)
print(X.shape)
print(B1.shape)(2, 3)
(2,)
(1, 3)
A1 = np.dot(X, W1) + B1
A1array([[0.3, 0.7, 1.1]])
Z1 = sigmoid(A1)
Z1array([[0.57444252, 0.66818777, 0.75026011]])
实现第 1 层到第 2 层的信号传递:
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])
print(Z1.shape)
print(W2.shape)
print(B2.shape)(1, 3)
(3, 2)
(2,)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)实现第 2 层到输出层的信号传递:
def identity_function(x):
"""
这里我们定义了 identity_function()函数(也称为“恒等函数”),并将
其作为输出层的激活函数。恒等函数会将输入按原样输出,因此,这个例子
中没有必要特意定义 identity_function()。这里这样实现只是为了和之前的
流程保持统一。
"""
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输出层的激活函数用 表示,不同于隐藏层的激活函数 。
输出层所用的激活函数,要根据求解问题的性质决定。
-
一般地,回归问题可以使用恒等函数,二元分类问题可以使用 sigmoid 函数
-
多元分类问题可以使用 softmax 函数
3.4.3 代码实现小结
def init_network():
"""
按照神经网络的实现惯例,只把权重记为大写字母 W1,其他的(偏置或中间结果等)都用小写字母表示。
"""
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 输出层的设计
3.5.1 恒等函数和 softmax 函数
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y3.5.2 实现 softmax 函数时的注意事项
由于要进行指数函数的运算,可能会导致溢出。
进行改进:
\begin{eqnarray} y_k &= \frac{\exp(a_k)}{\sum^n_{i=1}\exp(a_i)} &= \frac{C\exp(a_k)}{C\sum^n_{i=1}\exp(a_i)} \\ & &=\frac{\exp(a_k+\log C)}{\sum^n_{i=1}\exp(a_i+\log C)} \\ & &=\frac{\exp(a_k+C')}{\sum^n_{i=1}\exp(a_i+C')} \end{eqnarray}其中 任意的常数,记 ,这里的 可以使用任何值,但是为了防止溢出,一般会使用输入信号中的最大值。
a = np.array([1010, 1000, 990])
np.exp(a) / np.sum(np.exp(a))C:\Users\gzjzx\AppData\Local\Temp\ipykernel_15664\832863605.py:2: RuntimeWarning: overflow encountered in exp
np.exp(a) / np.sum(np.exp(a))
C:\Users\gzjzx\AppData\Local\Temp\ipykernel_15664\832863605.py:2: RuntimeWarning: invalid value encountered in true_divide
np.exp(a) / np.sum(np.exp(a))
array([nan, nan, nan])
c = np.max(a)
a - carray([ 0, -10, -20])
np.exp(a-c) / np.sum(np.exp(a-c))array([9.99954600e-01, 4.53978686e-05, 2.06106005e-09])
综上,我们可以像下面这样实现 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 y3.5.3softmax 函数的特征
a = np.array([0.3, 2.9, 4.0])
y = softmax(a)
yarray([0.01821127, 0.24519181, 0.73659691])
np.sum(y)1.0
- softmax 函数的输出是 0.0 到 1.0 之间的实数
- softmax 函数的输出值的总和是 1
3.5.4 输出层的神经元数量
输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。
3.6 手写数字识别
假设学习已经全部结束,我们使用学习到的参数,先实现神经网络的“推理处理”。这个推理处理也称为神经网络的前向传播(forward propagation)。
3.6.1 MNIST 数据集
MNIST 的图像数据是 28 像素× 28 像素的灰度图像(1 通道),各个像素的取值在 0 到 255 之间。每个图像数据都相应地标有“7”“2”“1”等标签。
本书提供了便利的 Python 脚本 mnist.py,该脚本支持从下载 MNIST 数据 集到将这些数据转换成 NumPy 数组等处理(mnist.py 在 dataset 目录下)。使用 mnist.py 时,当前目录必须是 ch01、ch02、ch03、…、ch08 目录中的一个。使 用 mnist.py 中的 load_mnist()函数,就可以按下述方式轻松读入 MNIST 数据。
import sys, os
sys.path.append(os.pardir) # 为了导入父目录中的文件而进行的设定
from dataset.mnist import load_mnist
# 第一次调用会花费几分钟……
# load_mnist 函数以“( 训练图像, 训练标签),( 测试图像,测试标签)”的形式返回读入的 MNIST 数据
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)Downloading train-images-idx3-ubyte.gz ...
Done
Downloading train-labels-idx1-ubyte.gz ...
Done
Downloading t10k-images-idx3-ubyte.gz ...
Done
Downloading t10k-labels-idx1-ubyte.gz ...
Done
Converting train-images-idx3-ubyte.gz to NumPy Array ...
Done
Converting train-labels-idx1-ubyte.gz to NumPy Array ...
Done
Converting t10k-images-idx3-ubyte.gz to NumPy Array ...
Done
Converting t10k-labels-idx1-ubyte.gz to NumPy Array ...
Done
Creating pickle file ...
Done!
# 输出各个数据的形状
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)(60000, 784)
(60000,)
(10000, 784)
(10000,)
试着显示 MNIST 图像,同时也确认一下数据
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image
import matplotlib.pyplot as plt
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)
plt.imshow(img, cmap='gray')5
(784,)
(28, 28)
<matplotlib.image.AxesImage at 0x1f5a63afca0>
3.6.2 神经网络的推理处理
对这个 MNIST 数据集实现神经网络的推理处理。 神经网络的输入层有 784 个神经元,输出层有 10 个神经元。
-
输入层的 784 这个数字来源于图像大小的 28 × 28 = 784
-
输出层的 10 这个数字来源于 10 类别分类(数字 0 到 9,共 10 类别)
-
这个神经网络有 2 个隐藏层,第 1 个隐藏层有 50 个神经元,第 2 个隐藏层有 100 个神经元。这个 50 和 100 可以设置为任何值。
def get_data():
"""
获取数据
"""
(x_train, t_train), (x_test, t_test) = \
load_mnist(normalize=True, flatten=True, one_hot_label=False)
return x_test, t_test将 normalize 设置成True后,函数内部会进行转换,将图像的各个像素值除以 255,使得数据的值在 0.0~1.0 的范围内。像这样把数据限定到某个范围内的处理称为正规化(normalization)。此外,对神经网络的输入数据进行某种既定的转换称为预处理(pre-processing)。
这里,作为对输入图像的一种预处理,我们进行了正规化。
def init_network():
"""
读入保存在 pickle 文件 sample_weight.pkl 中的学习到的权重参数
这个文件中以字典变量的形式保存了权重和偏置参数
"""
with open("sample_weight.pkl", 'rb') as f:
network = pickle.load(f)
return networkdef 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 yimport pickle
x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
"""
用 for 语句逐一取出保存在 x 中的图像数据,用 predict()函数进行分类。
predict()函数以 NumPy 数组的形式输出各个标签对应的概率。
"""
y = predict(network, x[i])
p = np.argmax(y) # 获取概率最高的元素的索引
if p == t[i]:
accuracy_cnt += 1
print("Accuracy: " + str(float(accuracy_cnt) / len(x)))Accuracy:0.9352
3.6.3 批处理
输出刚才的神经网络的各层的权重的形状。
x, _ = get_data()
network = init_network()
W1, W2, W3 = network['W1'], network['W2'], network['W3']x.shape(10000, 784)
x[0].shape(784,)
W1.shape(784, 50)
W2.shape(50, 100)
W3.shape(100, 10)
从整体的处理流程来看,图 3-26 中,输入一个由 784 个元素(原本是一个 28 × 28 的二维数组)构成的一维数组后,输出一个有 10 个元素的一维数组。这是只输入一张图像数据时的处理流程。
现在我们来考虑打包输入多张图像的情形。比如,我们想用predict()
函数一次性打包处理 100 张图像。为此,可以把 的形状改为 100 × 784,将
100 张图像打包作为输入数据。
这种打包式的输入数据称为批(batch)。批有“捆”的意思,图像就如同纸币一样扎成一捆。
x, t = get_data()
network = init_network()
batch_size = 100 # 批数量
accuracy_cnt = 0
for i in range(0, len(x), batch_size):
"""
在 range()函数生成的列表的基础上,通过 x[i:i+batch_size]从输入数据中抽出批数据。
x[i:i+batch_n]会取出从第 i 个到第 i+batch_n 个之间的数据。
"""
x_batch = x[i:i+batch_size]
y_batch = predict(network, x_batch)
"""
参数 axis=1。这指定了在 100 × 10 的数组中,沿着第 1 维方向(以第 1 维为轴)
找到值最大的元素的索引(第 0 维对应第 1 个维度)。
"""
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)))Accuracy:0.9352
3.7 小结
-
神经网络中的激活函数使用平滑变化的 sigmoid 函数或 ReLU 函数。
-
通过巧妙地使用 NumPy 多维数组,可以高效地实现神经网络。
-
机器学习的问题大体上可以分为回归问题和分类问题。
-
关于输出层的激活函数,回归问题中一般用恒等函数,分类问题中一般用 softmax 函数。
-
分类问题中,输出层的神经元的数量设置为要分类的类别数。
-
输入数据的集合称为批。通过以批为单位进行推理处理,能够实现高速的运算。