跳转至

05.6 标准化标签值

5.6 对标签值标准化⚓︎

5.6.1 发现问题⚓︎

这一节里我们重点解决在训练过程中的数值的数量级的问题。

我们既然已经对样本数据特征值做了标准化,那么如此大数值的损失函数值是怎么来的呢?看一看损失函数定义:

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

其中,\(z_i\) 是预测值,\(y_i\) 是标签值。初始状态时,\(W\)\(B\) 都是 \(0\),所以,经过前向计算函数 \(Z=X \cdot W+B\) 的结果是 \(0\),但是 \(Y\) 值很大,处于 \([181.38, 674.37]\) 之间,再经过平方计算后,一下子就成为至少5位数的数值了。

再看反向传播时的过程:

    def __backwardBatch(self, batch_x, batch_y, batch_z):
        m = batch_x.shape[0]
        dZ = batch_z - batch_y
        dB = dZ.sum(axis=0, keepdims=True)/m
        dW = np.dot(batch_x.T, dZ)/m
        return dW, dB
第二行代码求得的dZ,与房价是同一数量级的,这样经过反向传播后,dWdB的值也会很大,导致整个反向传播链的数值都很大。我们可以debug一下,得到第一反向传播时的数值是:
dW
array([[-142.59982906],
       [-283.62409678]])
dB
array([[-443.04543906]])
上述数值又可能在读者的机器上是不一样的,因为样本做了shuffle,但是不影响我们对问题的分析。

这么大的数值,需要把学习率设置得很小,比如0.001,才可以落到 \([0,1]\) 区间,但是损失函数值还是不能变得很小。

如果我们像对特征值做标准化一样,把标签值也标准化到 \([0,1]\) 之间,是不是有帮助呢?

5.6.2 代码实现⚓︎

参照X的标准化方法,对Y的标准化公式如下:

\[y_{new} = \frac{y-y_{min}}{y_{max}-y_{min}} \tag{2}\]

SimpleDataReader类中增加新方法如下:

class SimpleDataReader(object):
    def NormalizeY(self):
        self.Y_norm = np.zeros((1,2))
        max_value = np.max(self.YRaw)
        min_value = np.min(self.YRaw)
        # min value
        self.Y_norm[0, 0] = min_value 
        # range value
        self.Y_norm[0, 1] = max_value - min_value 
        y_new = (self.YRaw - min_value) / self.Y_norm[0, 1]
        self.YTrain = y_new

原始数据中,\(Y\) 的数值范围是:

  • 最大值:674.37
  • 最小值:181.38
  • 平均值:420.64

标准化后,\(Y\) 的数值范围是: - 最大值:1.0 - 最小值:0.0 - 平均值:0.485

注意,我们同样记住了Y_norm的值便于以后使用。

修改主程序代码,增加对 \(Y\) 标准化的方法调用NormalizeY()

# main
if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    reader.NormalizeX()
    reader.NormalizeY()
    ......

5.6.3 运行结果⚓︎

运行上述代码得到的结果其实并不令人满意:

......
199 99 0.0015663978030319194 
[[-0.08194777] [ 0.80973365]] [[0.12714971]]
W= [[-0.08194777]
 [ 0.80973365]]
B= [[0.12714971]]
z= [[0.61707273]]

虽然W和B的值都已经处于 \([-1,1]\) 之间了,但是 \(z\) 的值也在 \([0,1]\) 之间,一套房子不可能卖0.61万元!

聪明的读者可能会想到:既然对标签值做了标准化,那么我们在得到预测结果后,需要对这个结果应该做反标准化。

根据公式2,反标准化的公式应该是:

\[y = y_{new}(y_{max}-y_{min})+y_{min} \tag{3}\]

代码如下:

if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    reader.NormalizeX()
    reader.NormalizeY()
    # net
    params = HyperParameters(eta=0.01, max_epoch=200, batch_size=10, eps=1e-5)
    net = NeuralNet(params, 2, 1)
    net.train(reader, checkpoint=0.1)
    # inference
    x1 = 15
    x2 = 93
    x = np.array([x1,x2]).reshape(1,2)
    x_new = reader.NormalizePredicateData(x)
    z = net.inference(x_new)
    print("z=", z)
    Z_real = z * reader.Y_norm[0,1] + reader.Y_norm[0,0]
    print("Z_real=", Z_real)

倒数第二行代码,就是公式3。运行结果如下:

W= [[-0.08149004]
 [ 0.81022449]]
B= [[0.12801985]]
z= [[0.61856996]]
Z_real= [[486.33591769]]

Z_real的值,完全满足要求!

总结一下从本章中学到的正确的方法:

  1. \(X\) 必须标准化,否则无法训练;
  2. \(Y\) 值不在 \([0,1]\) 之间时,要做标准化,好处是迭代次数少;
  3. 如果 \(Y\) 做了标准化,对得出来的预测结果做关于 \(Y\) 的反标准化

至此,我们完美地解决了北京通州地区的房价预测问题!

5.6.4 总结⚓︎

归纳总结一下前面遇到的困难及解决办法:

  1. 样本不做标准化的话,网络发散,训练无法进行;
  2. 训练样本标准化后,网络训练可以得到结果,但是预测结果有问题;
  3. 还原参数值后,预测结果正确,但是此还原方法并不能普遍适用;
  4. 标准化测试样本,而不需要还原参数值,可以保证普遍适用;
  5. 标准化标签值,可以使得网络训练收敛快,但是在预测时需要把结果反标准化,以便得到真实值。

代码位置⚓︎

ch05, Level6