对于感知机,面对复杂的函数,感知机也隐含着能够表示它的可能性。即便是计算机进行的复杂处理,感知机(理论上)也可以将其表示出来。但是,设定权重的工作,即确定合适的、能符合预期的输入与输出的权重,现在还是由人工进行的。神经网络的出现就是为了解决这个的坏消息。具体地讲,神经网络它可以自动地从数据中学习到合适的权重参数。
神经网络和感知机有很多共同点。用图来表示神经网络的话,我们把最左边的一列称为输入层,最右边的一列称为输出层,中间的一列称为中间层(隐藏层):
【说明】:上图中的神经网络一共由3层神经元构成,但实质上只有2 层神经元有权重。神经网络的形状类似感知机。实际上,就神经元的连接方式而言,与感知机并没有任何差异。
上面的数学表达式是之前我们分析的感知机数学关系式,b是偏置:控制神经元被激活的容易程度;而w1和w2是表示各个信号的权重,用于控制各个信号的重要性。现在我们将式子分解变形,引入新函数h(x):
【说明】:首先,计算加权输入信号和偏置的总和;然后,用h(x)函数将a转换为输出y。
【说明】:表示神经元的○中明确显示了激活函数的计算过程,即信号的加权总和为节点a,然后节点a被激活函数h(x)转换成节点y。激活函数是连接感知机和神经网络的桥梁。
之前例子中的感知机使用了阶跃函数作为激活函数。也就是说,在激活函数的众多候选函数中,感知机使用了阶跃函数。那么,如果感知机使用其他函数作为激活函数的话会怎么样呢?实际上,如果将激活函数从阶跃函数换成其他函数,就可以进入神经网络的世界了。下面我们就来介绍一下神经网络使用的激活函数:
sigmoid函数是非常常见的一种激活函数,其数学表达式如下:
import numpy as np
import matplotlib.pylab 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)
plt.show()
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)
plt.show()
【说明】:阶跃函数以0为界,输出从0切换为1(或者从1切换为 0)它的值呈阶梯式变化,所以称为阶跃函数。
import numpy as np
import matplotlib.pylab as plt
x = np.arange(-5.0, 5.0, 0.1)
y1 = sigmoid(x)
y2 = step_function(x)
plt.plot(x, y1)
plt.plot(x, y2, 'k--')
plt.ylim(-0.1, 1.1) #指定图中绘制的y轴的范围
plt.show()
【说明】:
(1)首先“平滑性”不同:sigmoid函数是一条平滑的曲线,输出随着输入发生连续性的变化。而阶跃函数以0为界,输出发生急剧性的变化。sigmoid函数的平滑性对神经网络的学习具有重要意义。 (2)另一个不同点是,相对于阶跃函数只能返回0或1,sigmoid函数可以返回在0-1之间无数的实数(这一点和刚才的平滑性有关)。也就是说,感知机中神经元之间流动的是0或1的二元信号,而神经网络中流动的是连续的实数值信号。
神经网络的激活函数必须使用非线性函数。换句话说,激活函数不能使用线性函数。因为使用线性函数的话,加深神经网络的层数就没有意义了,不管如何加深层数,总是存在与之等效的“无隐藏层的神经网络”。
【说明】:例如,我们把线性函数h(x)=cx作为激活函数,把 y(x) = h(h(h(X)))的运算对应3层神经网络。这个运算会进行y(x)=c×c×c× X 的乘法运算,但是同样的处理可以由y(x)=ax(a=c的三次方)一次乘法运算(即没有隐藏层的神经网络)来表示。如本例所示,使用线性函数时,无法发挥多层网络带来的优势。因此,为了发挥叠加层所带来的优势,激活函数必须使用非线性函数。
神经网络中除了常见的sigmoid函数,还有一种常用的激活函数:ReLU函数:
import numpy as np
import matplotlib.pylab 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(-1.0, 5.5)
plt.show()
3层神经网络:输入层有2个神经元,第一个隐藏层有3个神经元,第二个隐藏层有2个神经元,输出层有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])
A1 = np.dot(X, W1) + B1
Z1 = sigmoid(A1)
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)
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函数等等...
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]
【说明】:init_network()函数:进行权重和偏置的初始化,并将它们保存在字典变量network中。这个字典变量network中保存了每一层所需权重和偏置。forward()函数:封装了将输入信号转换为输出信号的处理过程。这里出现了 forward表示的是从输入到输出方向的传递处理。后面在进行神经网络的训练时,我们将使用后向(backward,从输出到输入方向)的处理方式。
至此,神经网络的前向处理的实现就完成了。通过巧妙地使用 NumPy 多维数组,我们高效地实现了神经网络。
神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出层的激活函数。一般而言,回归问题用恒等函数,分类问题用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
【说明】:softmax函数在计算机的运算上有一定的缺陷:这个缺陷就是溢出问题。softmax函数的实现中要进行指数函数的运算,但是此时指数函数的值很容易变得非常大。如果在这些超大值之间进行除法运算,结果会出现“不确定”的情况。
>>> a = np.array([1010, 1000, 990])
>>> np.exp(a) / np.sum(np.exp(a)) # softmax函数的运算
array([ nan, nan, nan]) # 没有被正确计算
>>>
>>> c = np.max(a) # 1010
>>> a - c
array([ 0, -10, -20])
>>>
>>> np.exp(a - c) / np.sum(np.exp(a - c))
array([ 9.99954600e-01, 4.53978686e-05, 2.06106005e-09])
因此,为了避免溢出问题将代码改成下面这样:
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函数的输出是0.0到1.0之间的实数。并且softmax函数的输出值的总和是1。输出总和为1是softmax 函数的一个重要性质。正因为有了这个性质,我们才可以把 softmax 函数的输出解释为“概率”。
>>> a = np.array([0.3, 2.9, 4.0])
>>> y = softmax(a)
>>> print(y)
[ 0.01821127 0.24519181 0.73659691]
>>> np.sum(y)
1.0
一般而言,神经网络只把输出值最大的神经元所对应的类别作为识别结果。并且,即便使用 softmax 函数,输出值最大的神经元的位置也不会变。因此,神经网络在进行分类时,输出层的 softmax 函数可以省略。在实际的问题中,由于指数函数的运算需要一定的计算机运算量,因此输出层的 softmax 函数一般会被省略。
输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量,例如手写数字识别:
这里使用的数据集是MNIST手写数字图像集。MNIST数据集是由0到9的数字图像构成的。训练图像有6万张,测试图像有1万张,这些图像可以用于学习和推理。MNIST数据集的一般使用方法是,先用训练图像进行学习,再用学习到的模型度量能在多大程度上对测试图像进行正确的分类。MNIST的图像数据是28像素 × 28像素的灰度图像(1通道),各个像素的取值在0-255之间,每个图像数据都相应地标有“7”“2”“1”等标签。
【下载数据集】:
def load_mnist(normalize=True, flatten=True, one_hot_label=False):
"""读入MNIST数据集
Parameters
----------
normalize : 将图像的像素值正规化为0.0~1.0
one_hot_label :
one_hot_label为True的情况下,标签作为one-hot数组返回
one-hot数组是指[0,0,1,0,0,0,0,0,0,0]这样的数组
flatten : 是否将图像展开为一维数组
Returns
-------
(训练图像, 训练标签), (测试图像, 测试标签)
"""
if not os.path.exists(save_file):
init_mnist()
with open(save_file, 'rb') as f:
dataset = pickle.load(f)
if normalize:
for key in ('train_img', 'test_img'):
dataset[key] = dataset[key].astype(np.float32)
dataset[key] /= 255.0
if one_hot_label:
dataset['train_label'] = _change_one_hot_label(dataset['train_label'])
dataset['test_label'] = _change_one_hot_label(dataset['test_label'])
if not flatten:
for key in ('train_img', 'test_img'):
dataset[key] = dataset[key].reshape(-1, 1, 28, 28)
return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label'])
【可以提前查看数据集相关情况】:
import numpy as np
from Deep_Learning_From_Scratch.dataset.mnist import load_mnist
from PIL import Image
# 载入数据: 参数flatten:是否转成一维数组;参数normalize:是否将特征转成0.0-1.0之间
(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,)
# 查看第一张手写数字
img = x_train[0]
label = t_train[0]
print("第一张手写数字为:", label) # 5
print("第一张手写数字形状为:", img.shape) # (784,)
img = img.reshape(28, 28) # 把图像的形状变为原来的尺寸,以方便显示
# 显示手写数字照片
def img_show(img):
pil_img = Image.fromarray(np.uint8(img))
pil_img.show()
img_show(img)
训练集特征形状: (60000, 784)
训练集目标形状: (60000,)
测试集特征形状: (10000, 784)
测试集特征形状: (10000,)
第一张手写数字为: 5
第一张手写数字形状为: (784,)
【基础神经网络构建】:
# coding: utf-8
import numpy as np
import pickle
from Deep_Learning_From_Scratch.dataset.mnist import load_mnist
from Deep_Learning_From_Scratch.common.functions import sigmoid, softmax
# 载入数据:返回测试集
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
# 利用测试集进行测试
x_test, t_test = get_data()
network = init_network()
# 一次批处理100张
batch_size = 100
accuracy_cnt = 0
for i in range(0, len(x_test), batch_size):
x_batch = x_test[i:i+batch_size]
y_batch = predict(network, x_batch) # 神经网络预测:y是包含10个概率的数组
p = np.argmax(y_batch, axis=1) # 获取概率最高的元素的索引
accuracy_cnt += np.sum(p == t_test[i:i+batch_size])
print("正确率为:" + str(float(accuracy_cnt) / len(x_test)))
正确率为:0.9352
评论