两层单入单出的神经网络能做什么
定义神经网络结构
我们定义一个两层的神经网络,输入层不算,一个隐藏层,含128个神经元,一个输出层。
数学理论证明:具有足够数量神经元的两层神经网络能够拟合任意精度的连续函数。所以,今天咱们就用实际数据来验证一下这个理论。我们假设一个连续函数的形式为:
\[y=0.4x^2 + 0.3xsin(15x) + 0.01cos(50x)-0.3\]
输入层
输入层就是一个标量X值。
权重矩阵W1/B1
它是连接两层之间的纽带,有的人理解它应该属于输入层,有的人理解应该属于隐藏层,各有各的道理,我个人倾向于把它归到隐藏层,理由是\(Z1=W1*X+B1\),在X固定的前提下,W1决定了Z1的值。另外一个理由是B1的存在位置,在本例中B1是一个128×1的矩阵,它是隐藏层128个神经元的偏移,所以它应该属于隐藏层。
其实这里的B1所在的圆圈里应该是个常数1,而B1连接到Z1-1…Z1-128的权重线B1-1…B1-128应该是个浮点数。我们为了说明问题方便,就写了个B1,而实际的B1是指B1-1…B1-128的矩阵/向量。
W1的尺寸是128×1,B1的尺寸是128×1。
隐藏层
我们用一个128个神经元的网络来模拟函数,这个大家可以自己试验一下,把代码中的神经元数量修改一下,然后在保持迭代次数和其它(超)参数不变的情况,看看最终的精确度有何区别,训练时间的差异,以及内存占用有何差异。
每个神经元的输入\(Z1 = W1 * X + B1\),我们在这里使用双曲sigmoid正切函数,所以输出是\(A1 = sigmoid(Z1)\)。当然也可以使用其它激活函数如果tanh, Relu等等。
权重矩阵W2/B2
与W1/B1类似,我个人认为它属于输出层。W2的尺寸是1×128,B2的尺寸是1×1。
输出层
由于我们只想完成一个拟合任务,所以输出层只有一个神经元。它们的左侧是\(Z2=W2*A1+B2\),右侧是\(A2=Z2\)。
为什么在最后一步没有用激活函数,而是直接令A2=Z2呢?我们后面再说。
创造训练数据
让我们先自力更生创造一些模拟数据:
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
def TargetFunction(x):
p1 = 0.4 * (x**2)
p2 = 0.3 * x * np.sin(15 * x)
p3 = 0.01 * np.cos(50 * x)
y = p1 + p2 + p3 - 0.3
return y
def CreateSampleDataXY(m):
S = np.random.random((m,2))
S[:,1] = TargetFunction(S[:,0])
return S
def CreateTestData(n):
TX = np.linspace(0,1,100)
TY = TargetFunction(TX)
TZ = np.zeros(n)
return TX, TY, TZ
其函数图像在[0,1]之间的样子是:
生成的数据格式如下:
\[
\begin{pmatrix}
x_1, y_1\\
x_2, y_2\\
\dots\\
x_m, y_m\\
\end{pmatrix}
\]
其中,x就是上图中蓝色点的横坐标值,y是纵坐标值。在[0,1]之外的函数曲线没这么复杂,似乎拟合起来没什么难度,所以我们特点选择了[0,1]之间这一段来做试验。
定义前向计算过程
至此,我们得到了以下一串公式:
\[Z1=W1*X+B1\]
\[A1=sigmoid(Z1)\]
\[Z2=W2*A1+B2\]
\[A2=Z2 \tag{这一步可以省略}\]
def ForwardCalculation(x, dictWeights):
W1 = dictWeights["W1"]
B1 = dictWeights["B1"]
W2 = dictWeights["W2"]
B2 = dictWeights["B2"]
Z1 = np.dot(W1,x) + B1
A1 = sigmoid(Z1)
Z2 = np.dot(W2,A1) + B2
A2 = Z2 # 这一步可以省略
dictCache ={"A1": A1, "A2": A2}
return A2, dictCache
由于参数较多,所以我们用一个dictionary(dictWeights)来保存W,B这些参数,如果是更多层的神经网络,就会有更多的参数,我们这里使用的还是一些最基本的参数。
定义代价函数
我们用传统的均方差函数: \(loss = \frac{1}{2}(Z-Y)^2\),其中,Z是每一次迭代的预测输出,Y是样本标签数据。我们使用所有样本参与训练,因此损失函数实际为:
\[Loss = \frac{1}{2}(Z – Y) ^ 2\]
其中的分母中有个2,实际上是想在求导数时把这个2约掉,没有什么原则上的区别。
定义针对w和b的梯度函数
看一下计算图,然后用链式求导法则反推:
求W1的梯度
因为:
\[Z2 = W2*A1+B2\]
\[Loss = \frac{1}{2}(Z2-Y2)^2\]
所以我们用Loss的值作为基准,去求w对它的影响,也就是loss对w的偏导数:
\[
\frac{\partial{Loss}}{\partial{W2}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z2}}{\partial{W2}}
\]
其中:
\[
\frac{\partial{Loss}}{\partial{Z2}} = \frac{\partial{}}{\partial{Z2}}[\frac{(Z2-Y)^2}{2}] = Z2-Y
\]
而:
\[
\frac{\partial{Z2}}{\partial{W2}} = \frac{\partial{}}{\partial{W2}}(W2*A1+B2) = A1^T
\]
所以:
\[
\frac{\partial{Loss}}{\partial{W2}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z}}{\partial{W2}} = (Z2-Y)*A1^T
\]
矩阵求导的理论部分较为复杂,请大家参考我们的《基本数学导数公式》章节。
求B2的梯度
\[
\frac{\partial{Loss}}{\partial{B2}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z2}}{\partial{B2}}
\]
其中第一项前面算w的时候已经有了,而:
\[
\frac{\partial{Z2}}{\partial{B2}} = \frac{\partial{(W2*A1+B2)}}{\partial{B2}} = 1
\]
所以:
\[
\frac{\partial{Loss}}{\partial{B2}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z2}}{\partial{B2}} = Z2-Y
\]
求W1的梯度
因为:
\[A1 = sigmoid(Z1)\]
\[Z1 = W1*X+B1\]
对Z1求导:
\[
\frac{\partial{Loss}}{\partial{Z1}} = \frac{\partial{Loss}}{\partial{Z2}}*\frac{\partial{Z2}}{\partial{A1}}*\frac{\partial{A1}}{\partial{Z1}}
\]
其中前面推导过:
\[
\frac{\partial{Loss}}{\partial{Z2}} = Z2-Y = dZ2
\]
而:
\[
\frac{\partial{Z2}}{\partial{A1}} = \frac{\partial{}}{\partial{A1}}(W2*A1+B2) = W2^T
\]
\[
\frac{\partial{A1}}{\partial{Z1}} = \frac{\partial{}}{\partial{Z1}}(sigmoid(Z1)) = A1*(1-A1)
\]
所以:
\[
\frac{\partial{Loss}}{\partial{Z1}} = W2^T * dZ2 * A1 * (1-A1) = dZ1
\]
而W1,B1的求导结果和W2,B2类似:
\[
\frac{\partial{Loss}}{\partial{W1}} = \frac{\partial{Loss}}{\partial{Z1}}*\frac{\partial{Z1}}{\partial{W1}}=dZ1*\frac{\partial{(W1*X+B1)}}{\partial{W1}}=dZ1*X^T
\]
\[
\frac{\partial{Loss}}{\partial{B1}} = \frac{\partial{Loss}}{\partial{Z1}}*\frac{\partial{Z1}}{\partial{B1}}=dZ1*\frac{\partial{(W1*X+B1)}}{\partial{B1}}=dZ1
\]
变成代码:
def BackPropagation(x, y, dictCache, dictWeights):
A1 = dictCache["A1"]
A2 = dictCache["A2"]
W2 = dictWeights["W2"]
dLoss_Z2 = A2 - y
dZ2 = dLoss_Z2
dW2 = dZ2 * A1.T
dB2 = dZ2
dZ2_A1 = W2.T * dZ2
dA1_Z1 = A1 * (1 - A1)
# dZ1 is dLoss_Z1
dZ1 = dZ2_A1 * dA1_Z1
dW1 = dZ1 * x
dB1 = dZ1
dictGrads = {"dW1":dW1, "dB1":dB1, "dW2":dW2, "dB2":dB2}
return dictGrads
每次迭代后更新w,b的值
def UpdateWeights(dictWeights, dictGrads, learningRate):
W1 = dictWeights["W1"]
B1 = dictWeights["B1"]
W2 = dictWeights["W2"]
B2 = dictWeights["B2"]
dW1 = dictGrads["dW1"]
dB1 = dictGrads["dB1"]
dW2 = dictGrads["dW2"]
dB2 = dictGrads["dB2"]
W1 = W1 - learningRate * dW1
W2 = W2 - learningRate * dW2
B1 = B1 - learningRate * dB1
B2 = B2 - learningRate * dB2
dictWeights = {"W1": W1,"B1": B1,"W2": W2,"B2": B2}
return dictWeights
帮助函数
第一个show_result函数用于最后输出结果。第二个print_progress函数用于训练过程中的输出。
def sigmoid(x):
s=1/(1+np.exp(-x))
return s
def initialize_with_zeros(n_x,n_h,n_y):
np.random.seed(2)
# W1=np.random.randn(n_h,n_x)*0.00000001 # W1=np.random.randn(n_h,n_x)
W1=np.random.uniform(-np.sqrt(6)/np.sqrt(n_x+n_h),np.sqrt(6)/np.sqrt(n_h+n_x),size=(n_h,n_x))
# W1=np.reshape(32,784)
B1=np.zeros((n_h,1))
# W2=np.random.randn(n_y,n_h)*0.00000001 # W2=np.random.randn(n_y,n_h)
W2=np.random.uniform(-np.sqrt(6)/np.sqrt(n_y+n_h),np.sqrt(6)/np.sqrt(n_y+n_h),size=(n_y,n_h))
B2=np.zeros((n_y,1))
assert (W1.shape == (n_h, n_x))
assert (B1.shape == (n_h, 1))
assert (W2.shape == (n_y, n_h))
assert (B2.shape == (n_y, 1))
dictWeights = {"W1": W1,"B1": B1,"W2": W2,"B2": B2}
return dictWeights
主程序初始化
m = 1000
S = CreateSampleDataXY(m)
#plt.scatter(S[:,0], S[:,1], 1)
#plt.show()
n_input, n_hidden, n_output = 1, 128, 1
learning_rate = 0.1
eps = 1e-10
dictWeights = initialize_with_zeros(n_input, n_hidden, n_output)
max_iteration = 1000
loss, prev_loss, diff_loss = 0, 0, 0
程序主循环
for iteration in range(max_iteration):
for i in range(m):
x = S[i,0]
y = S[i,1]
A2, dictCache = ForwardCalculation(x, dictWeights)
dictGrads = BackPropagation(x,y,dictCache,dictWeights)
dictWeights = UpdateWeights(dictWeights, dictGrads, learning_rate)
print("iteration", iteration)
测试并输出拟合结果
tm = 100
TX, TY, TZ = CreateTestData(tm)
correctCount = 0
for i in range(tm):
x = TX[i]
y = TY[i]
a2, dict = ForwardCalculation(x, dictWeights)
TZ[i] = a2
plt.scatter(TX, TY)
plt.plot(TX, TZ, 'r')
str = str.format("cell:{0} sample:{1} iteration:{2} rate:{3}", n_hidden, m, max_iteration, learning_rate)
plt.title(str)
plt.show()
上面的TX是[0,1]之间的连续数,共100个,间隔相同。TY是更加被模拟的函数计算出来的精确值。TZ是我们训练的模型的预测值。我们的目的就是要比较TY和TZ之间的差距。
下图就是拟合结果,还比较令人满意。
参数调整
经常听人说起“调参”,这次咱们亲身经历一下调参的痛(快)苦(乐)!我们下面一切的比较都是以下面这组参数为基准:
- 神经元数=128
- 输入训练数据量=1000
- 迭代次数=1000
- 权重调整步进值=0.1
以上这些标准值如何得到呢?试了很多组合后得到的,这就是所谓“试错”的过程了。
神经元数量的变化(标准值128)
神经元数量=64
神经元数量=96
神经元数量=256,迭代次数=500
基准神经元数为128,在96时,拟合效果很差,在64时,尽管我们增加了迭代次数为2000,仍然很差。
第三张图,尽管神经元数量翻了一倍,成为256个,但是迭代次数为500,少了一倍,也会造成奇怪的结果。
样本量的变化(标准值1000)
输入数据量=500
输入数据量=1500
样本数据量不够时,拟合效果不好。但是当样本数据量超过一定值后,就没多大作用了。
迭代次数的变化(标准值1000)
迭代次数=500
输入数据量=1500
迭代次数少,拟合效果不好。迭代次数超过一定值后,容易造成过拟合,效果不大。
步长值的变化(标准值0.1)
步长=0.5
步长=0.01
步长值太大或者太小,都会造成不好的效果。
总结如下(效果5分为最好):
神经元数量 | 样本量 | 迭代次数 | 步长值 | 效果 | |
---|---|---|---|---|---|
0 | 128 | 1000 | 1000 | 0.1 | 5 |
1 | 64 | 1000 | 2000 | 0.1 | 3 |
2 | 96 | 1000 | 1000 | 0.1 | 2.5 |
3 | 256 | 1000 | 500 | 0.1 | 0 |
4 | 128 | 500 | 1000 | 0.1 | 2 |
5 | 128 | 1500 | 1000 | 0.1 | 5 |
6 | 128 | 1000 | 500 | 0.1 | 2.5 |
7 | 128 | 1000 | 1500 | 0.1 | 5 |
8 | 128 | 1000 | 1000 | 0.5 | 1 |
9 | 128 | 1000 | 1000 | 0.05 | 2 |