目录
3.1 从感知机到神经网络
神经网络的一个重要性质就是可以自动从数据中学习到合适的权重参数。下图表示神经网络的一个例子。
中间层也称为隐藏层,隐藏层的神经元(和输入层、输出层不同)肉眼看不见。上图中网络一共由3层神经元构成,但实质上只有2层神经元有权重,因此将其称为“2层网络”。
感知机公式与网络结构如下
b为偏置,用于控制神经元被激活的容易程度;而w1和w2 是表示各个信号的权重的参数,用于控制各个信号的重要性。下图添加了权重为b的输入信号1
这个感知机将x1、x2、1三个信号作为神经元的输入,将其和各自的权重相乘后,传送至下一个神经元。在下一个神经元中,计算这些加权信号的总和。如果 这个总和超过0,则输出1,否则输出0。另外,由于偏置的输入信号一直是1, 所以为了区别于其他神经元,在图中把这个神经元整个涂成灰色。
现引入新函数h(x),将上式简化为
其中
即输入信号的总和会被h(x)转换,转换后的值就是输出y。
3.2 激活函数
上述h(x)即为激活函数,激活函数的作用在于决定如何来激活输入信号的总和。现可将简化后的函数分成下面两个式子。其中,a为加权输入信号和偏置的总和。
下图展示了激活函数的计算过程,即信号的加权总和为节点a,然后节点a被激活函数h()转换成节点y。
一般来说,朴素感知机即单层感知机,是指单层网络,指的是激活函数使用了阶跃函数的模型;多层感知机是指神经网络,即使用了sigmoid函数等平滑的激活函数的多层网络。
上述的激活函数以阈值为界,一旦输入超过阈值,就切换输出,这种激活函数即称为“阶跃函数”。
3.2.1 sigmoid函数
神经网络经常使用的激活函数就是sigmoid函数。
其中,exp(-x)即e的-x次幂。神经网络中用sigmoid函数作为激活函数,进行信号的转换,转换后的信号被传送给下一个神经元。
3.2.2 阶跃函数的实现
def step_function(x):
if x > 0:
return 1
else:
return 0
上述代码简单地实现了阶跃函数,但是参数x只能接收实数(浮点数),但不允许参数取NumPy数组。现将其改进为
def step_function(x):
y = x > 0
return y.astype(np.int)#astype是pandas中的方法,进行类型转换
对NumPy数组进行不等号运算后,数组的各个元素都会进行不等号运算,生成一个布尔型数组。这里,数组x中大于0的元素被转换为True,小于等 于0的元素被转换为False,从而生成一个新的数组y。数组y是一个布尔型数组,但是我们想要的阶跃函数是会输出int型的0 或1的函数。因此,需要把数组y的元素类型从布尔型转换为int型。
3.2.3 阶跃函数的图形
使用matplotlib库来画出上面定义的阶跃函数。
import numpy as np
import matplotlib.pylab as plt
def step_function(x):
return np.array(x > 0, dtype=np.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()
np.arange(-5.0, 5.0, 0.1)在−5.0到5.0的范围内,以0.1为单位,生成 NumPy数组([-5.0, -4.9, ……, 4.9])。结果如下图所示。
3.2.4 sigmoid函数的实现
def sigmoid(x):
return 1 / (1 + np.exp(-x))
与阶跃函数代码不同,此处参数传入NumPy数组时,结果也能被正确计算。这是因为NumPy数组的广播功能。
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 两函数比较
相较于阶跃函数只能返回0或1,sigmoid函数可以返回一系列实数。也就是说,感知机中神经元之间的流动是0或1的二元信号,而神经网络中流动的是连续的实数值信号。
二者共同点是具有相似的形状,输入小时,输出接近0(为0); 随着输入增大,输出向1靠近(变成1)。也就是说,当输入信号为重要信息时,二者都会输出较大的值,当输入信号为不重要的信息时,二者都输出较小的值。还有一个共同点是,不管输入信号有多小或多大,输出信号都在0和1之间。二者均为非线性函数。
3.2.6 非线性函数
函数本来是输入某个值后会返回一个值的转换器。向这个转换器输入某个值后,输出值是输入值的常数倍的函数称为线性函数(用数学式表示为h(x)=cx。c为常数)。因此,线性函数是一条笔直的直线。 而非线性函数,顾名思义,指的是不像线性函数那样呈现出一条直线的函数。
神经网络的激活函数必须使用非线性函数。因为如果使用线性函数的话,加深神经网络的层数就没有意义了。
线性函数的问题在于,不管如何加深层数,总是存在与之等效的“无隐藏层的神经网络”。为了具体地(稍微直观地)理解这一点,我们来思考下面这个简单的例子。这里我们考虑把线性函数h(x)=cx作为激活函数,把y(x)=h(h(h(x)))的运算对应3层神经网络。这个运算会进行y(x) = c ×c×c×x的乘法运算,但是同样的处理可以由y(x)=ax(注意,a =c3)这一次乘法运算(即没有隐藏层的神经网络)来表示。如本例所示, 使用线性函数时,无法发挥多层网络带来的优势。因此,为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。
3.2.7 ReLU函数
ReLU函数在输入大于0时,直接输出该值;在输入小于等于0时,输出0
3.3 多维数组的运算
3.3.1 多维数组
生成一维数组
import numpy as np
A = np.array([1, 2, 3, 4])
A.shape#此处输出为(4,)---是元组
生成二维数组(矩阵)
B = np.array([[1,2], [3,4], [5,6]])
3.3.2 矩阵乘法
使用np.dot(A,B)来进行矩阵相乘。矩阵乘法为线性代数内容,比较简单,此处不再赘述。
3.3.3 神经网络的内积
实现该神经网络时,注意X、W、Y的形状,特别是X和W的维度。使用np.dot(多维数组的点积)一次计算出Y的结果。
3.4 3层神经网络的实现
3.4.1 符号确认
3.4.2 各层之间信号传递的实现
如果用矩阵乘法来表示,即
其中
现在设置输入信号、权重、偏置值
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) # (2, 3)
print(X.shape) # (2,)
print(B1.shape) # (3,)
A1 = np.dot(X, W1) + B1
接下来观察第1层中激活函数的计算过程。如图所示,隐藏层的加权和(加权信号和偏置的总和)用a表示,被激活函数转换后的信号用z表示。此外,图中h()表示激活函数,使用sigmoid函数。
Z1 = sigmoid(A1)
print(A1) # [0.3, 0.7, 1.1]
print(Z1) # [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) # (3,)
print(W2.shape) # (3, 2)
print(B2.shape) # (2,)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
最后是第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()函数(也称为“恒等函数”),并将其作为输出层的激活函数。恒等函数会将输入按原样输出。这里使用恒等函数只是为了和之前的流程保持统一。输出层的激活函数用σ()表示,不同于隐藏层的h()。
输出层所用的激活函数,要根据求解问题的性质决定。一般地,回归问题可以使用恒等函数,二元分类问题可以使用sigmoid函数,多元分类问题可以使用softmax函数。
回归问题是根据某个输入预测一个(连续的)数值的问题。比如根据一个人的图像来预测体重。
3.5 输出层的设计
3.5.1 恒等函数和softmax函数
恒等函数会将输入按原样输出。因此,在输出层使用恒等函数时,输入信号会原封不动地被输出。将恒等函数处理过程用之前的神经网络图来表示的话,如下图所示。
分类问题中使用softmax函数可以用下式表示
用图表示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函数的实现虽然正确描述了函数公式,但在计算机上运行时会出现溢出。softmax函数的实现可以像下式这样进行改进。
这里的C'可以使用任何值,但是为了防 止溢出,一般会使用输入信号中的最大值。例如下面的计算过程。
通过减去输入信号中的最大值来实现计算。改进后的softmax函数如下。
3.5.3 softmax函数特性
softmax函数的输出是0.0到1.0之间的实数,且softmax函数的输出值总和是1。正因为有了这个性质,才可以把softmax函数的输出解释为“概率”。比如上面所示y[0]的概率是1.8%……
需要注意的是,即便使用了softmax函数,各个元素之间的大小关系不会改变。这是因为指数函数(y=exp(x))是单调递增函数。实际上,上例中a的各元素的大小关系和y的各元素的大小关系并没有改变。比如,a的最大值是第2个元素,y的最大值也仍是第2个元素。
求解机器学习问题步骤可以分为“学习”和“推理”两个阶段。先在学习阶段进行模型的学习,然后在推理阶段,用学到的模型对未知的数据进行推理(分类)。
3.5.4 输出层神经元的数量
输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。比如,对于某个输入图像,预测图中的数字是0到9中的哪一个的问题(10类别分类问题),可以如下图一样,将输出层神经元数量设定为10个。
其中,神经元y2颜色最深,输出的值最大。这表明这个神经网络预测的是y2对应的类别,也就是“2”。
3.6 手写数字识别
假设已经结束学习,现在使用学习到的参数实现神经网络的推理处理,推理处理也称为神经网络的前向传播。
3.6.1 MNIST数据集
MNIST数据集有6万张训练图像,1万张测试图像。图像数据是28×28像素的灰度图像(通道1),各个像素的取值在0-255之间。mnist.py支持从下载MNIST数据集到将这些数据转换成NumPy数组来处理,其具体运行代码不做解析。
现在读入MNIST数据
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
import numpy as np
from dataset.mnist import load_mnist # 从dataset目录下的mnist模块中导入load_mnist函数。
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
print(x_train.shape) # x_train是一个NumPy数组,形状为(60000, 784),表示共有60000张训练图像,每张图像被展开为长度为784的一维数组。
print(x_test.shape)
print(t_train.shape)
print(t_test.shape)
其中,load_mnist函数以"(训练图像,训练标签),(测试图像,测试标签)"的形式返回读入的MNIST数据。flatten为True则输入图像保存为由784个元素构成的一维数组,若为False,则输入图像为1×28×28的三维数组。normalize设置是否将输入图像正规化为0.0-1.0的值,若为False则输入图像像素保持原来的0-255。
现在来显示MNIST图像,使用PIL模块,将训练图像的第一张显示出来。在上述代码的基础上进行修改。
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
import numpy as np
from dataset.mnist import load_mnist
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)
np.uint8 是 NumPy 提供的一个数据类型,表示 8 位无符号整数。这个数据类型可以表示的值范围是 0 到 255。np.uint8(img) 将数组 img 中的所有元素转换为 8 位无符号整数。这样做是为了确保图像数据的类型与 PIL 库处理图像所期望的数据类型一致,因为图像像素值通常在 0 到 255 的范围内。
Image.fromarray 是 PIL(Python Imaging Library)库中的一个函数,它用于将一个 NumPy 数组转换为一个 PIL 图像对象。这个方法接受一个 NumPy 数组作为参数,并返回一个对应的 PIL 图像对象。NumPy 数组通常是二维的(灰度图像)或三维的(彩色图像)。
3.6.2 神经网络的推理处理
在本次实验中,神经网络的输入层有784(28×28)个神经元,输出层有10个神经元,来源于10类别分类(数字0-9)。此外,这个神经网络还有两个隐藏层,分别有50,100个神经元。这个50和100可以设置任意值。
先定义3个函数。其中,init_network()会读入保存在pickle文件sample_weight.pkl中学习到的权重参数。该文件以字典变量的形式保存了权重和偏置参数。
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
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
现在进行推理处理。先使用for语句逐一取出保存在x中的图像数据,用predict()函数进行分类。predict()函数以NumPy数组形式输出各个标签对应的概率。比如[0.1, 0.3, ……]表示0的概率是0.1……然后取出概率最大的值的索引赋给p,最后比较神经网络所预测的答案和正确解标签,将正确回答的概率作为识别精度。
x, t = get_data()
print(x.shape)
print(t.shape)
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)))
normalize设置为True后,函数内部会进行转换,将图像的各个像素值除以255,使数据的值在0.0-1.0之间。像这样把数据限定到某个范围内的处理称为正规化。对神经网络的输入数据进行某种既定的转换称为预处理。
3.6.2 批处理
在上述的推理过程中,是将一张张的图像分别进行了处理,对应的数组形状变化如下
输入X是1个由784个元素组成的一维数组,输出Y有10个元素的一维数组,这是只输入一张图像的处理流程。
现考虑使用predict()函数一次性打包100张图像进行批处理。如下图所示。
下面进行基于批处理的代码实现。
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] # 100×784
y_batch = predict(network, x_batch) # 100×10
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)))
其中的range()函数若指定为range(start, end)则会生成一个由start到end-1之间的整数构成的列表,若像range(start, end, step)则生成的列表中的下一个元素会增加step指定的值。
for语句得到的数据为100,200,300......9900,通过x[i:i+batch_size]从输入数据中抽出批数据,x[i:i+batch_n]会取出i到第i+batch_n个之间的数据。本例是x[0:100]、x[100,200]。
p = np.argmax(y_batch, axis=1)用来取出y_batch中每行的最大值,其中参数axis = 1指定了在100×10的数组中,沿着第1维方向找到最大值的元素的索引。矩阵的第0维是列方向,第1维是行方向。
3.7 小结
神经网络中的激活函数使用平滑变化的sigmoid函数或ReLU函数,而感知机使用信号急剧变化的阶跃函数。
机器学习的问题大体上可分为回归问题和分类问题。
输出层的激活函数,回归问题一般使用恒等函数,分类问题用softmax函数。
分类问题中,输出层神经元的数量设置为要分类的类别数。