与单特征值的线性回归问题类似,多变量(多特征值)的线性回归可以被看做是一种高维空间的线性拟合。以具有两个特征的情况为例,这种线性拟合不再是用直线去拟合点,而是用平面去拟合点。

定义神经网络结构

我们定义一个如图5-1所示的一层的神经网络,输入层为2或者更多,反正大于2了就没区别。这个一层的神经网络的特点是:

  1. 没有中间层,只有输入项和输出层(输入项不算做一层);
  2. 输出层只有一个神经元;
  3. 神经元有一个线性输出,不经过激活函数处理,即在下图中,经过$$\Sigma$$求和得到$$Z$$值之后,直接把$$Z$$值输出。

神经网络法 - 图1

与上一章的神经元相比,这次仅仅是多了一个输入,但却是质的变化,即,一个神经元可以同时接收多个输入,这是神经网络能够处理复杂逻辑的根本。

输入层

单独看第一个样本是这样的:

x1 = \begin{pmatrix} x{11} & x_{12} \end{pmatrix} = \begin{pmatrix} 10.06 & 60 \end{pmatrix}

y_1 = \begin{pmatrix} 302.86 \end{pmatrix}

一共有1000个样本,每个样本2个特征值,X就是一个1000 \times 2的矩阵:

$$
\begin{aligned}
&X=\left(\begin{array}{c}
x{1} \
x
{2} \
\cdots \
x{1000}
\end{array}\right)=\left(\begin{array}{cc}
x
{1,1} & x{1,2} \
x
{2,1} & x{2,2} \
\dots & \dots \
x
{1000,1} & x{1000,2}
\end{array}\right)\
&Y=\left(\begin{array}{c}
y
{1} \
y{2} \
\cdots \
y
{1000}
\end{array}\right)=\left(\begin{array}{c}
302.86 \
393.04 \
\cdots \
450.59
\end{array}\right)
\end{aligned}
$$

x1表示第一个样本,x{1,1}表示第一个样本的一个特征值,y_1是第一个样本的标签值。

权重W和B

由于输入层是两个特征,输出层是一个变量,所以w的形状是2x1,而b的形状是1x1。

$$
W=\left(\begin{array}{l}
w{1} \
w
{2}
\end{array}\right) \
B = \left(b\right)
$$

B是个单值,因为输出层只有一个神经元,所以只有一个bias,每个神经元对应一个bias,如果有多个神经元,它们都会有各自的b值。

输出层

由于我们只想完成一个回归(拟合)任务,所以输出层只有一个神经元。由于是线性的,所以没有用激活函数。 这是单个样本的情况。

$$
z=\left(\begin{array}{ll}
x{11} & x{12}
\end{array}\right)\left(\begin{array}{l}
w{1} \
w
{2}
\end{array}\right)+(b)=x{11} w{1}+x{12} w{2}+b
$$

按照图5-1的情况如公式如下:

$$
z=\left(\begin{array}{ll}
x{11} & x{12} \
x{21} & x{22}
\end{array}\right)\left(\begin{array}{ll}
w{11} & w{21} \
w{12} & w{22}
\end{array}\right)+\left(\begin{array}{ll}
b{1} & b{2}
\end{array}\right)
$$

其中偏置b本来为一行要与之前其他项的计算得益于numpy的广播机制,numpy的广播机制如下

神经网络法 - 图2

写成矩阵形式:

Z = X\cdot W + B

损失函数

因为是线性回归问题,所以损失函数使用均方差函数。

loss(w,b) = \frac{1}{2} (z_i-y_i)^2 \tag{1}

其中,z_i是样本预测值,y_i是样本的标签值。

反向传播

单样本多特征计算

与上一章不同,本章中的前向计算是多特征值的公式:

$$
\begin{aligned}
&z{i}=x{i 1} \cdot w{1}+x{i 2} \cdot w{2}+b\
&=\left(\begin{array}{ll}
x
{i 1} & x{i 2}
\end{array}\right)\left(\begin{array}{l} \tag{2}
w
{1} \
w_{2}
\end{array}\right)+b
\end{aligned}
$$

因为x有两个特征值,对应的W也有两个权重值。x_{i1}表示第i个样本的第1个特征值,所以无论是x还是W都是一个向量或者矩阵了,那么我们在反向传播方法中的梯度计算公式还有效吗?答案是肯定的,我们来一起做个简单推导。

由于W被分成了w1w2两部分,根据公式1和公式2,我们单独对它们求导:

\frac{\partial loss}{\partial w1}=\frac{\partial loss}{\partial z_i}\frac{\partial z_i}{\partial w_1}=(z_i-y_i) \cdot x{i1} \tag{3}

\frac{\partial loss}{\partial w2}=\frac{\partial loss}{\partial z_i}\frac{\partial z_i}{\partial w_2}=(z_i-y_i) \cdot x{i2} \tag{4}

求损失函数对W矩阵的偏导是无法直接求的,所以要变成求各个W的分量的偏导。由于W的形状是:

$$
W=\left(\begin{array}{l}
w{1} \
w
{2}
\end{array}\right)
$$

所以求lossW的偏导,由于W是个矩阵,所以应该这样写:

$$
\begin{array}{l}
\frac{\partial \operatorname{loss}}{\partial W}=\left(\begin{array}{c}
\frac{\partial \operatorname{loss}}{\partial w{1}} \
\frac{\partial \operatorname{loss}}{\partial w
{2}}
\end{array}\right) \
=\left(\begin{array}{c}
\left(z{i}-y{i}\right) \cdot x{i 1} \
\left(z
{i}-y{i}\right) \cdot x{i 2}
\end{array}\right) \
=\left(\begin{array}{c}
x{i 1} \
x
{i 2}
\end{array}\right)\left(z{i}-y{i}\right) \
=\left(\begin{array}{ll}
x{i 1} & x{i 2}
\end{array}\right){T}\left(z{i}-y{i}\right)
\end{array} \tag{5}
$$

{\partial loss \over \partial B}=z_i-y_i \tag{6}

多样本多特征计算

当进行多样本计算时,我们用m=3个样本做一个实例化推导:

z1 = x{11}w1+x{12}w_2+b

z2= x{21}w1+x{22}w_2+b

z3 = x{31}w1+x{32}w_2+b

J(w,b) = \frac{1}{2 \times 3}[(z_1-y_1)2+(z_3-y_3)^2]

神经网络法 - 图3

{\partial J \over \partial B}={1 \over m}(Z-Y) \tag{8}

代码实现

公式6和多样本单特征值计算中的公式5一样,所以我们依然采用之前已经写好的HelperClass目录中的那些类,来表示我们的神经网络。虽然此次神经元多了一个输入,但是不用改代码就可以适应这种变化,因为在前向计算代码中,使用的是矩阵乘的方式,可以自动适应x的多个列的输入,只要对应的w的矩阵形状是正确的即可。

但是在初始化时,我们必须手动指定x和w的形状,如下面的代码所示:

  1. if __name__ == '__main__':
  2. # net
  3. params = HyperParameters(2, 1, eta=0.1, max_epoch=100, batch_size=1, eps = 1e-5)
  4. net = NeuralNet(params)
  5. net.train(reader)
  6. # inference
  7. x1 = 15
  8. x2 = 93
  9. x = np.array([x1,x2]).reshape(1,2)
  10. print(net.inference(x))

在参数中,指定了学习率0.1,最大循环次数100轮,批大小1个样本,以及停止条件损失函数值1e-5。

在神经网络初始化时,指定了input_size=2,且output_size=1,即一个神经元可以接收两个输入,最后是一个输出。

最后的inference部分,是把两个条件(15公里,93平方米)代入,查看输出结果。

在下面的神经网络的初始化代码中,W的初始化是根据input_size和output_size的值进行的。

  1. class NeuralNet(object):
  2. def __init__(self, params):
  3. self.params = params
  4. self.W = np.zeros((self.params.input_size, self.params.output_size))
  5. self.B = np.zeros((1, self.params.output_size))

正向计算的代码

  1. class NeuralNet(object):
  2. def __forwardBatch(self, batch_x):
  3. Z = np.dot(batch_x, self.W) + self.B
  4. return Z

误差反向传播的代码

  1. class NeuralNet(object):
  2. def __backwardBatch(self, batch_x, batch_y, batch_z):
  3. m = batch_x.shape[0]
  4. dZ = batch_z - batch_y
  5. dB = dZ.sum(axis=0, keepdims=True)/m
  6. dW = np.dot(batch_x.T, dZ)/m
  7. return dW, dB

运行结果

运行代码后,会遇到一个令人沮丧的打印输出:

  1. epoch=0
  2. NeuralNet.py:32: RuntimeWarning: invalid value encountered in subtract
  3. self.W = self.W - self.params.eta * dW
  4. 0 500 nan
  5. epoch=1
  6. 1 500 nan
  7. epoch=2
  8. 2 500 nan
  9. epoch=3
  10. 3 500 nan
  11. ......

减法怎么会出问题?什么是nan?

nan的意思是数值异常,导致计算溢出了,出现了没有意义的数值。现在是每500个迭代监控一次,我们把监控频率调小一些,再试试看:

  1. epoch=0
  2. 0 10 6.838664338516814e+66
  3. 0 20 2.665505502247752e+123
  4. 0 30 1.4244204612680962e+179
  5. 0 40 1.393993758296751e+237
  6. 0 50 2.997958629609441e+290
  7. NeuralNet.py:76: RuntimeWarning: overflow encountered in square
  8. LOSS = (Z - Y)**2
  9. 0 60 inf
  10. ...
  11. 0 110 inf
  12. NeuralNet.py:32: RuntimeWarning: invalid value encountered in subtract
  13. self.W = self.W - self.params.eta * dW
  14. 0 120 nan
  15. 0 130 nan

前10次迭代,损失函数值已经达到了6.83e+66,而且越往后运行值越大,最后终于溢出了。下面的损失函数历史记录也表明了这一过程。

神经网络法 - 图4

寻找失败的原因

我们可以在NeuralNet.py文件中,在图5-3代码行上设置断点,跟踪一下训练过程,以便找到问题所在。

神经网络法 - 图5

在VS2017中用F5运行debug模式,看第50行的结果:

  1. batch_x
  2. array([[ 4.96071728, 41. ]])
  3. batch_y
  4. array([[244.07856544]])

返回的样本数据是正常的。再看下一行:

  1. batch_z
  2. array([[0.]])

第一次运行前向计算,由于W和B初始值都是0,所以z也是0,这是正常的。再看下一行:

  1. dW
  2. array([[ -1210.80475712],
  3. [-10007.22118309]])
  4. dB
  5. array([[-244.07856544]])

dW和dB的值都非常大,这是因为图5-4所示这行代码。

神经网络法 - 图6

batch_z是0,batch_y是244.078,二者相减,是-244.078,因此dB就是-244.078,dW因为矩阵乘了batch_x,值就更大了。

再看W和B的更新值,一样很大:

  1. self.W
  2. array([[ 121.08047571],
  3. [1000.72211831]])
  4. self.B
  5. array([[24.40785654]])

如果W和B的值很大,那么再下一轮进行前向计算时,会得到更糟糕的结果:

  1. batch_z
  2. array([[82459.53752331]])

果不其然,这次的z值飙升到了8万多,如此下去,几轮以后数值溢出是显而易见的事情了。

那么我们到底遇到了什么情况?

其实是因为样本的特征值不再一个尺度上导致的,我们需要对数据先进性一个归一化操作,这里我们放在下一节进行分析。

代码位置

原代码位置:ch05, Level2

个人代码:NeuralNetwork