发现问题

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

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

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位数的数值了。

再看反向传播时的过程:

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

第二行代码求得的dZ,与房价是同一数量级的,这样经过反向传播后,dW和dB的值也会很大,导致整个反向传播链的数值都很大。我们可以debug一下,得到第一反向传播时的数值是:

  1. dW
  2. array([[-142.59982906],
  3. [-283.62409678]])
  4. dB
  5. array([[-443.04543906]])

上述数值又可能在读者的机器上是不一样的,因为样本做了shuffle,但是不影响我们对问题的分析。

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

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

代码实现

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

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

在SimpleDataReader类中增加新方法如下:

  1. class SimpleDataReader(object):
  2. def NormalizeY(self):
  3. self.Y_norm = np.zeros((1,2))
  4. max_value = np.max(self.YRaw)
  5. min_value = np.min(self.YRaw)
  6. # min value
  7. self.Y_norm[0, 0] = min_value
  8. # range value
  9. self.Y_norm[0, 1] = max_value - min_value
  10. y_new = (self.YRaw - min_value) / self.Y_norm[0, 1]
  11. self.YTrain = y_new

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

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

标准化后,Y的数值范围是:

  • 最大值:1.0
  • 最小值:0.0
  • 平均值:0.485

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

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

  1. # main
  2. if __name__ == '__main__':
  3. # data
  4. reader = SimpleDataReader()
  5. reader.ReadData()
  6. reader.NormalizeX()
  7. reader.NormalizeY()
  8. ......

运行结果

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

  1. ......
  2. 199 99 0.0015663978030319194
  3. [[-0.08194777] [ 0.80973365]] [[0.12714971]]
  4. W= [[-0.08194777]
  5. [ 0.80973365]]
  6. B= [[0.12714971]]
  7. z= [[0.61707273]]

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

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

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

$$
y=y{n e w} *\left(y{\max }-y{\min }\right)+y{\min } \tag{3}
$$

代码如下:

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

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

  1. W= [[-0.08149004]
  2. [ 0.81022449]]
  3. B= [[0.12801985]]
  4. z= [[0.61856996]]
  5. Z_real= [[486.33591769]]

看Z_real的值,完全满足要求!

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

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

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

总结

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

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

代码位置

原代码位置:ch05, Level6

个人代码:NormalizeLabelData**

keras实现

  1. from keras.layers import Dense
  2. from keras.models import Sequential
  3. from keras.callbacks import EarlyStopping
  4. from sklearn.preprocessing import StandardScaler
  5. from HelperClass.DataReader_1_0 import *
  6. import matplotlib.pyplot as plt
  7. def get_data():
  8. sdr = DataReader_1_0("../data/ch05.npz")
  9. sdr.ReadData()
  10. X,Y = sdr.GetWholeTrainSamples()
  11. x_mean = np.mean(X)
  12. x_std = np.std(X)
  13. y_mean = np.mean(Y)
  14. y_std = np.std(Y)
  15. ss = StandardScaler()
  16. # 对训练样本做归一化
  17. X = ss.fit_transform(X)
  18. # 对训练标签做归一化
  19. Y = ss.fit_transform(Y)
  20. # test data
  21. x1 = 15
  22. x2 = 93
  23. x = np.array([x1, x2]).reshape(1, 2)
  24. # 对测试数据做归一化
  25. x_new = NormalizePredicateData(x, x_mean, x_std)
  26. return X, Y, x_new, y_mean, y_std
  27. # 手动进行归一化过程
  28. def NormalizePredicateData(X_raw, x_mean, x_std):
  29. X_new = np.zeros(X_raw.shape)
  30. n = X_raw.shape[1]
  31. for i in range(n):
  32. col_i = X_raw[:,i]
  33. X_new[:,i] = (col_i - x_mean) / x_std
  34. return X_new
  35. def build_model():
  36. model = Sequential()
  37. model.add(Dense(1, activation='linear', input_shape=(2,)))
  38. model.compile(optimizer='SGD',
  39. loss='mse')
  40. return model
  41. # 绘制loss曲线
  42. def plt_loss(history):
  43. loss = history.history['loss']
  44. epochs = range(1, len(loss) + 1)
  45. plt.plot(epochs, loss, 'b', label='Training loss')
  46. plt.title('Training loss')
  47. plt.xlabel('Epochs')
  48. plt.ylabel('Loss')
  49. plt.legend()
  50. plt.show()
  51. if __name__ == '__main__':
  52. X, Y, x_new, y_mean, y_std = get_data()
  53. x = np.array(X)
  54. y = np.array(Y)
  55. print(x.shape)
  56. print(y.shape)
  57. print(x)
  58. model = build_model()
  59. # patience设置当发现loss没有下降的情况下,经过patience个epoch后停止训练
  60. early_stopping = EarlyStopping(monitor='loss', patience=100)
  61. history = model.fit(x, y, epochs=200, batch_size=10, callbacks=[early_stopping])
  62. w, b = model.layers[0].get_weights()
  63. print(w)
  64. print(b)
  65. plt_loss(history)
  66. # inference
  67. z = model.predict(x_new)
  68. print("z=", z)
  69. # 将标签还原回真实值
  70. Z_true = z * y_std + y_mean
  71. print("Z_true=", Z_true)