跳转至

04.4 多样本计算

4.4 多样本单特征值计算⚓︎

在前面的代码中,我们一直使用单样本计算来实现神经网络的训练过程,但是单样本计算有一些缺点:

  1. 前后两个相邻的样本很有可能会对反向传播产生相反的作用而互相抵消。假设样本1造成了误差为 \(0.5\)\(w\) 的梯度计算结果是 \(0.1\);紧接着样本2造成的误差为 \(-0.5\)\(w\) 的梯度计算结果是 \(-0.1\),那么前后两次更新 \(w\) 就会产生互相抵消的作用。
  2. 在样本数据量大时,逐个计算会花费很长的时间。由于我们在本例中样本量不大(200个样本),所以计算速度很快,觉察不到这一点。在实际的工程实践中,动辄10万甚至100万的数据量,轮询一次要花费很长的时间。

如果使用多样本计算,就要涉及到矩阵运算了,而所有的深度学习框架,都对矩阵运算做了优化,会大幅提升运算速度。打个比方:如果200个样本,循环计算一次需要2秒的话,那么把200个样本打包成矩阵,做一次计算也许只需要0.1秒。

下面我们来看看多样本运算会对代码实现有什么影响,假设我们一次用3个样本来参与计算,每个样本只有1个特征值。

4.4.1 前向计算⚓︎

由于有多个样本同时计算,所以我们使用 \(x_i\) 表示第 \(i\) 个样本,\(X\) 是样本组成的矩阵,\(Z\) 是计算结果矩阵,\(w\)\(b\) 都是标量:

\[ Z = X \cdot w + b \tag{1} \]

把它展开成3个样本(3行,每行代表一个样本)的形式:

\[ X=\begin{pmatrix} x_1 \\\\ x_2 \\\\ x_3 \end{pmatrix} \]
\[ Z= \begin{pmatrix} x_1 \\\\ x_2 \\\\ x_3 \end{pmatrix} \cdot w + b =\begin{pmatrix} x_1 \cdot w + b \\\\ x_2 \cdot w + b \\\\ x_3 \cdot w + b \end{pmatrix} =\begin{pmatrix} z_1 \\\\ z_2 \\\\ z_3 \end{pmatrix} \tag{2} \]

\(z_1,z_2,z_3\) 是三个样本的计算结果。根据公式1和公式2,我们的前向计算Python代码可以写成:

    def __forwardBatch(self, batch_x):
        Z = np.dot(batch_x, self.w) + self.b
        return Z
Python中的矩阵乘法命名有些问题,np.dot()并不是矩阵点乘,而是矩阵叉乘,请读者习惯。

4.4.2 损失函数⚓︎

用传统的均方差函数,其中,\(z\) 是每一次迭代的预测输出,\(y\) 是样本标签数据。我们使用 \(m\) 个样本参与计算,因此损失函数为:

\[J(w,b) = \frac{1}{2m}\sum_{i=1}^{m}(z_i - y_i)^2\]

其中的分母中有个2,实际上是想在求导数时把这个2约掉,没有什么原则上的区别。

我们假设每次有3个样本参与计算,即 \(m=3\),则损失函数实例化后的情形是:

\[ \begin{aligned} J(w,b) &= \frac{1}{2\times3}[(z_1-y_1)^2+(z_2-y_2)^2+(z_3-y_3)^2] \\\\ &=\frac{1}{2\times3}\sum_{i=1}^3[(z_i-y_i)^2] \end{aligned} \tag{3} \]

公式3中大写的 \(Z\)\(Y\) 都是矩阵形式,用代码实现:

    def __checkLoss(self, dataReader):
        X,Y = dataReader.GetWholeTrainSamples()
        m = X.shape[0]
        Z = self.__forwardBatch(X)
        LOSS = (Z - Y)**2
        loss = LOSS.sum()/m/2
        return loss
Python中的矩阵减法运算,不需要对矩阵中的每个对应的元素单独做减法,而是整个矩阵相减即可。做求和运算时,也不需要自己写代码做遍历每个元素,而是简单地调用求和函数即可。

4.4.3 求 \(w\) 的梯度⚓︎

我们用 \(J\) 的值作为基准,去求 \(w\) 对它的影响,也就是 \(J\)\(w\) 的偏导数,就可以得到 \(w\) 的梯度了。从公式3看 \(J\) 的计算过程,\(z_1,z_2,z_3\)都对它有贡献;再从公式2看 \(z_1,z_2,z_3\) 的生成过程,都有 \(w\) 的参与。所以,\(J\)\(w\) 的偏导应该是这样的:

\[ \begin{aligned} \frac{\partial{J}}{\partial{w}}&=\frac{\partial{J}}{\partial{z_1}}\frac{\partial{z_1}}{\partial{w}}+\frac{\partial{J}}{\partial{z_2}}\frac{\partial{z_2}}{\partial{w}}+\frac{\partial{J}}{\partial{z_3}}\frac{\partial{z_3}}{\partial{w}} \\\\ &=\frac{1}{3}[(z_1-y_1)x_1+(z_2-y_2)x_2+(z_3-y_3)x_3] \\\\ &=\frac{1}{3} \begin{pmatrix} x_1 & x_2 & x_3 \end{pmatrix} \begin{pmatrix} z_1-y_1 \\\\ z_2-y_2 \\\\ z_3-y_3 \end{pmatrix} \\\\ &=\frac{1}{m} \sum^m_{i=1} (z_i-y_i)x_i \\\\ &=\frac{1}{m} X^{\top} \cdot (Z-Y) \\\\ \end{aligned} \tag{4} \]

其中: $$ X = \begin{pmatrix} x_1 \\ x_2 \\ x_3 \end{pmatrix}, X^{\top} = \begin{pmatrix} x_1 & x_2 & x_3 \end{pmatrix} $$

公式4中最后两个等式其实是等价的,只不过倒数第二个公式用求和方式计算每个样本,最后一个公式用矩阵方式做一次性计算。

4.4.4 求 \(b\) 的梯度⚓︎

\[ \begin{aligned} \frac{\partial{J}}{\partial{b}}&=\frac{\partial{J}}{\partial{z_1}}\frac{\partial{z_1}}{\partial{b}}+\frac{\partial{J}}{\partial{z_2}}\frac{\partial{z_2}}{\partial{b}}+\frac{\partial{J}}{\partial{z_3}}\frac{\partial{z_3}}{\partial{b}} \\\\ &=\frac{1}{3}[(z_1-y_1)+(z_2-y_2)+(z_3-y_3)] \\\\ &=\frac{1}{m} \sum^m_{i=1} (z_i-y_i) \\\\ &=\frac{1}{m}(Z-Y) \end{aligned} \tag{5} \]

公式5中最后两个等式也是等价的,在Python中,可以直接用最后一个公式求矩阵的和,免去了一个个计算 \(z_i-y_i\) 最后再求和的麻烦,速度还快。

    def __backwardBatch(self, batch_x, batch_y, batch_z):
        m = batch_x.shape[0]
        dZ = batch_z - batch_y
        dW = np.dot(batch_x.T, dZ)/m
        dB = dZ.sum(axis=0, keepdims=True)/m
        return dW, dB

代码位置⚓︎

ch04, HelperClass/NeuralNet.py