神经网络学习(四)
上一篇我们推导和实验了使用交叉熵作为损失函数能够明显提高神经网络训练的精度和速度,但是为什么呢?这一篇就结合我再在网上看到的资料进行一个探索。
假设
- 样本维度是2维
- 分类结果有三类
- 一种两个样本 ( x 11 , x 12 , y 1 ) , ( x 21 , x 22 , y 2 ) (x_{11},x_{12},y_1),(x_{21},x_{22},y_2) (x11,x12,y1),(x21,x22,y2)
公式推导
前向传播
和之前的推导类似,前向传播公式:
输出层使用softmax的公式:
z
1
=
ω
11
x
1
+
ω
12
x
2
+
b
1
z
2
=
ω
21
x
1
+
ω
22
x
2
+
b
2
z
3
=
ω
31
x
1
+
ω
32
x
2
+
b
3
a
1
=
e
z
1
e
z
1
+
e
z
2
+
e
z
3
a
2
=
e
z
2
e
z
1
+
e
z
2
+
e
z
3
a
3
=
e
z
3
e
z
1
+
e
z
2
+
e
z
3
\begin{aligned} z_1 &= \omega_{11}x_1+ \omega_{12}x_2+b_1 \\ z_2 &= \omega_{21}x_1+ \omega_{22}x_2+b_2 \\ z_3 &= \omega_{31}x_1+ \omega_{32}x_2+b_3 \\ a_1 &= \frac{e^{z_1}}{e^{z_1}+e^{z_2}+e^{z_3}} \\ a_2 &= \frac{e^{z_2}}{e^{z_1}+e^{z_2}+e^{z_3}} \\ a_3 &= \frac{e^{z_3}}{e^{z_1}+e^{z_2}+e^{z_3}} \\ \end{aligned}
z1z2z3a1a2a3=ω11x1+ω12x2+b1=ω21x1+ω22x2+b2=ω31x1+ω32x2+b3=ez1+ez2+ez3ez1=ez1+ez2+ez3ez2=ez1+ez2+ez3ez3
输出层使用sigmoid的公式:
z
1
=
ω
11
x
1
+
ω
12
x
2
+
b
1
z
2
=
ω
21
x
1
+
ω
22
x
2
+
b
2
z
3
=
ω
31
x
1
+
ω
32
x
2
+
b
3
a
1
=
1
1
+
e
z
1
a
2
=
1
1
+
e
z
2
a
3
=
1
1
+
e
z
3
\begin{aligned} z_1 &= \omega_{11}x_1+ \omega_{12}x_2+b_1 \\ z_2 &= \omega_{21}x_1+ \omega_{22}x_2+b_2 \\ z_3 &= \omega_{31}x_1+ \omega_{32}x_2+b_3 \\ a_1 &= \frac{1}{1+e^{z_1}} \\ a_2 &= \frac{1}{1+e^{z_2}} \\ a_3 &= \frac{1}{1+e^{z_3}} \\ \end{aligned}
z1z2z3a1a2a3=ω11x1+ω12x2+b1=ω21x1+ω22x2+b2=ω31x1+ω32x2+b3=1+ez11=1+ez21=1+ez31
使用交叉熵损失函数:
E
=
y
1
log
a
1
+
y
2
log
a
2
+
y
3
log
a
3
\begin{aligned} E=y_1\text{log} a_1+y_2\text{log} a_2 +y_3\text{log}a_3 \end{aligned}
E=y1loga1+y2loga2+y3loga3
使用平方误差损失函数:
E
=
1
2
[
(
y
1
−
a
1
)
2
+
(
y
2
−
a
2
)
2
+
(
y
3
−
a
3
)
2
]
\begin{aligned} E=\frac{1}{2}\left[(y_1-a_1)^2+(y_2-a_2)^2+(y_3-a_3)^2\right] \end{aligned}
E=21[(y1−a1)2+(y2−a2)2+(y3−a3)2]
反向传播
我们反向传播是为了看损失函数对神经网络的权值的影响,因此这里只求对
ω
11
\omega_{11}
ω11的影响,别的可以以此类推
对于平方误差损失函数,有我之前神经网络学习(二)的结论可以写出:
∂
E
∂
ω
11
=
−
(
y
1
−
a
1
)
a
1
(
1
−
a
1
)
x
11
\begin{aligned} \frac{\partial E}{\partial \omega_{11}}=-(y_1-a_1)a_1(1-a_1)x_{11} \end{aligned}
∂ω11∂E=−(y1−a1)a1(1−a1)x11
**我们要明确,在使用平方误差损失函数时我们的目的是什么?**我们是想知道
∇
ω
11
\nabla \omega_{11}
∇ω11和误差
y
1
−
a
1
y_1-a_1
y1−a1的关系,即
∇
ω
11
=
f
(
y
1
−
a
1
)
\nabla \omega_{11}=f(y_1-a_1)
∇ω11=f(y1−a1)
我们知道
y
1
y_1
y1的取值只能是0或1,而a_1的取值范围是
0
∼
1
0\sim 1
0∼1。
取
A
=
∣
y
1
−
a
1
∣
A=|y_1-a_1|
A=∣y1−a1∣
当
y
1
=
1
y_1=1
y1=1时,
A
=
y
1
−
a
1
A=y_1-a_1
A=y1−a1,
a
1
=
y
1
−
A
a_1=y_1-A
a1=y1−A带入上式得
∂
E
∂
ω
11
=
−
A
(
y
1
−
A
)
(
1
−
(
y
1
−
A
)
)
x
11
=
−
A
2
(
1
−
A
)
x
11
\begin{aligned} \frac{\partial E}{\partial \omega_{11}}=-A(y_1-A)(1-(y_1-A))x_{11}=-A^2(1-A)x_{11} \end{aligned}
∂ω11∂E=−A(y1−A)(1−(y1−A))x11=−A2(1−A)x11
当
y
1
=
0
y_1=0
y1=0时,
A
=
a
1
A=a_1
A=a1,带入上式得
∂
E
∂
ω
11
=
−
(
−
A
)
A
(
1
−
A
)
x
11
=
A
2
(
1
−
A
)
x
11
\begin{aligned} \frac{\partial E}{\partial \omega_{11}}=-(-A)A(1-A)x_{11}=A^2(1-A)x_{11} \end{aligned}
∂ω11∂E=−(−A)A(1−A)x11=A2(1−A)x11
上面的推导中
x
11
x_{11}
x11是样本,在训练中是常量,只有误差A是变量,因此
A
2
(
1
−
A
)
A^2(1-A)
A2(1−A)是参数更新的因素
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
A = np.linspace(0, 1, 100)
plt.plot(A, A ** 2 * (1 - A))
plt.xlabel("|error|")
plt.ylabel("$\delta w_{11}$")
plt.title("$\delta w_{11}$=f(A)")
plt.show()
从上图可以看出,当误差不断增大的时候,更新的参数先变大后变小,也就是说当误差太大的时候,反向传播变慢,乃至可能停止,这个是跟我们的期望相反,我们期望的是向误差小的地方移动,并且误差越大移动越快。
对于交叉熵损失函数,有我之前神经网络学习(三)的结论可以写出:
∂
E
∂
ω
11
=
a
1
−
y
1
\begin{aligned} \frac{\partial E}{\partial \omega_{11}}=a_1-y_1 \end{aligned}
∂ω11∂E=a1−y1
当
y
1
=
1
y_1=1
y1=1时,
A
=
y
1
−
a
1
A=y_1-a_1
A=y1−a1,
a
1
=
y
1
−
A
a_1=y_1-A
a1=y1−A带入上式得:
∂
E
∂
ω
11
=
−
A
\begin{aligned} \frac{\partial E}{\partial \omega_{11}}=-A \end{aligned}
∂ω11∂E=−A
当
y
1
=
0
y_1=0
y1=0时,
A
=
a
1
A=a_1
A=a1,带入上式得:
∂
E
∂
ω
11
=
A
\begin{aligned} \frac{\partial E}{\partial \omega_{11}}=A \end{aligned}
∂ω11∂E=A
此时,就可以之间看出,交叉熵损失函数中误差和参数更新完全是线性关系,服务我们对参数更新的期望。
实验验证
对刚刚的分析仅仅是理论的分析,下面可以把实验中的数据打印出来,验证我们的猜想。
交叉熵代价函数
import numpy as np
from sklearn.datasets import load_digits #导入手写数字数据集
from sklearn.preprocessing import LabelBinarizer # 标签二值化
from sklearn.model_selection import train_test_split # 切割数据,交叉验证法
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def dsigmoid(x):
return x * (1 - x)
def softmax(x):
sum = 0
temp = np.zeros(len(x[0]))
for i in range(len(x[0])):
sum += np.exp(x[0][i])
for i in range(len(x[0])):
temp[i] = np.exp(x[0][i]) / sum
temp = np.atleast_2d(temp)
return temp
class NeuralNetwork:
def __init__(self, layers): # (64,100,10)
# 权重的初始化,范围-1到1:+1的一列是偏置值
self.V = np.random.random((layers[0] + 1, layers[1] + 1)) * 2 - 1
self.W = np.random.random((layers[1] + 1, layers[2])) * 2 - 1
def train(self, X, y, lr=0.11, epochs=10000):
# 添加偏置值:最后一列全是1
temp = np.ones([X.shape[0], X.shape[1] + 1])
temp[:, 0:-1] = X
X = temp
for n in range(epochs + 1):
# 在训练集中随机选取一行(一个数据):randint()在范围内随机生成一个int类型
i = np.random.randint(X.shape[0])
x = [X[i]]
# 转为二维数据:由一维一行转为二维一行
x = np.atleast_2d(x)
# L1:输入层传递给隐藏层的值;输入层64个节点,隐藏层100个节点
# L2:隐藏层传递到输出层的值;输出层10个节点
L1 = sigmoid(np.dot(x, self.V))
L2 = softmax(np.dot(L1, self.W))
# L2_delta:输出层对隐藏层的误差改变量
# L1_delta:隐藏层对输入层的误差改变量
Error.append(np.abs(y[i][0] - L2[0][0]))
L2_delta = y[i] - L2
gradient.append(L2_delta[0][0])
L1_delta = L2_delta.dot(self.W.T) * dsigmoid(L1)
#print(L2)
# 计算改变后的新权重
self.W += lr * L1.T.dot(L2_delta)
self.V += lr * x.T.dot(L1_delta)
if n > 40000:
lr = lr * 0.99
# 每训练1000次输出一次准确率
if n % 1000 == 0:
predictions = []
for j in range(X_test.shape[0]):
# 获取预测结果:返回与十个标签值逼近的距离,数值最大的选为本次的预测值
o = self.predict(X_test[j])
# 将最大的数值所对应的标签返回
predictions.append(np.argmax(o))
# np.equal():相同返回true,不同返回false
accuracy = np.mean(np.equal(predictions, y_test))
print('迭代次数:', n, '准确率:', accuracy)
def predict(self, x):
# 添加偏置值:最后一列全是1
temp = np.ones([x.shape[0] + 1])
temp[0:-1] = x
x = temp
# 转为二维数据:由一维一行转为二维一行
x = np.atleast_2d(x)
# L1:输入层传递给隐藏层的值;输入层64个节点,隐藏层100个节点
# L2:隐藏层传递到输出层的值;输出层10个节点
L1 = sigmoid(np.dot(x, self.V))
L2 = softmax(np.dot(L1, self.W))
return L2
# 载入数据:8*8的数据集
digits = load_digits()
X = digits.data
Y = digits.target
# 输入数据归一化:当数据集数值过大,乘以较小的权重后还是很大的数,代入sigmoid激活函数就趋近于1,不利于学习
X -= X.min()
X /= X.max()
NN = NeuralNetwork([64, 80, 10])
# sklearn切分数据
X_train, X_test, y_train, y_test = train_test_split(X, Y)
# 标签二值化:将原始标签(十进制)转为新标签(二进制)
labels_train = LabelBinarizer().fit_transform(y_train)
labels_test = LabelBinarizer().fit_transform(y_test)
global Error
global gradient
Error = []
gradient = []
print('开始训练')
NN.train(X_train, labels_train, epochs=40000)
print('训练结束')
plt.plot(Error, gradient,".")
可以看出误差和梯度是标准的线性关系,并且是其实在误差特别大的点并不是很多。
平方误差代价函数
import numpy as np
from sklearn.datasets import load_digits #导入手写数字数据集
from sklearn.preprocessing import LabelBinarizer # 标签二值化
from sklearn.model_selection import train_test_split # 切割数据,交叉验证法
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def dsigmoid(x):
return x * (1 - x)
def softmax(x):
sum = 0
temp = np.zeros(len(x[0]))
for i in range(len(x[0])):
sum += np.exp(x[0][i])
for i in range(len(x[0])):
temp[i] = np.exp(x[0][i]) / sum
temp = np.atleast_2d(temp)
return temp
class NeuralNetwork:
def __init__(self, layers): # (64,100,10)
# 权重的初始化,范围-1到1:+1的一列是偏置值
self.V = np.random.random((layers[0] + 1, layers[1] + 1)) * 2 - 1
self.W = np.random.random((layers[1] + 1, layers[2])) * 2 - 1
def train(self, X, y, lr=0.11, epochs=10000):
# 添加偏置值:最后一列全是1
temp = np.ones([X.shape[0], X.shape[1] + 1])
temp[:, 0:-1] = X
X = temp
for n in range(epochs + 1):
# 在训练集中随机选取一行(一个数据):randint()在范围内随机生成一个int类型
i = np.random.randint(X.shape[0])
x = [X[i]]
# 转为二维数据:由一维一行转为二维一行
x = np.atleast_2d(x)
# L1:输入层传递给隐藏层的值;输入层64个节点,隐藏层100个节点
# L2:隐藏层传递到输出层的值;输出层10个节点
L1 = sigmoid(np.dot(x, self.V))
L2 = sigmoid(np.dot(L1, self.W))
# L2_delta:输出层对隐藏层的误差改变量
# L1_delta:隐藏层对输入层的误差改变量
L2_delta = (y[i] - L2) * dsigmoid(L2)
L1_delta = L2_delta.dot(self.W.T) * dsigmoid(L1)
Error.append(np.abs(y[i][0] - L2[0][0]))
gradient.append(L2_delta[0][0])
#print(L2)
# 计算改变后的新权重
self.W += lr * L1.T.dot(L2_delta)
self.V += lr * x.T.dot(L1_delta)
if n > 40000:
lr = lr * 0.99
# 每训练1000次输出一次准确率
if n % 1000 == 0:
predictions = []
for j in range(X_test.shape[0]):
# 获取预测结果:返回与十个标签值逼近的距离,数值最大的选为本次的预测值
o = self.predict(X_test[j])
# 将最大的数值所对应的标签返回
predictions.append(np.argmax(o))
# np.equal():相同返回true,不同返回false
accuracy = np.mean(np.equal(predictions, y_test))
print('迭代次数:', n, '准确率:', accuracy)
def predict(self, x):
# 添加偏置值:最后一列全是1
temp = np.ones([x.shape[0] + 1])
temp[0:-1] = x
x = temp
# 转为二维数据:由一维一行转为二维一行
x = np.atleast_2d(x)
# L1:输入层传递给隐藏层的值;输入层64个节点,隐藏层100个节点
# L2:隐藏层传递到输出层的值;输出层10个节点
L1 = sigmoid(np.dot(x, self.V))
L2 = softmax(np.dot(L1, self.W))
return L2
# 载入数据:8*8的数据集
digits = load_digits()
X = digits.data
Y = digits.target
# 输入数据归一化:当数据集数值过大,乘以较小的权重后还是很大的数,代入sigmoid激活函数就趋近于1,不利于学习
X -= X.min()
X /= X.max()
NN = NeuralNetwork([64, 80, 10])
# sklearn切分数据
X_train, X_test, y_train, y_test = train_test_split(X, Y)
# 标签二值化:将原始标签(十进制)转为新标签(二进制)
labels_train = LabelBinarizer().fit_transform(y_train)
labels_test = LabelBinarizer().fit_transform(y_test)
global Error
global gradient
Error = []
gradient = []
print('开始训练')
NN.train(X_train, labels_train, epochs=40000)
print('训练结束')
plt.plot(Error, gradient,".")
此时就可以看到,误差和梯度不是一个线性的变化,而且误差分布在较大的地方的密集程度是比交叉熵的明显增多的。