机器学习(ML)八之正向传播、反向传播和计算图,及数值稳定性和模型初始化

正向传播

正向传播的计算图

通常绘制计算图来可视化运算符和变量在计算中的依赖关系。下图绘制了本节中样例模型正向传播的计算图,其中左下角是输入,右上角是输出。可以看到,图中箭头方向大多是向右和向上,其中方框代表变量,圆圈代表运算符,箭头表示从输入到输出之间的依赖关系。

反向传播

 

训练深度学习模型

在训练深度学习模型时,正向传播和反向传播之间相互依赖。一方面,正向传播的计算可能依赖于模型参数的当前值,而这些模型参数是在反向传播的梯度计算后通过优化算法迭代的而这些当前值是优化算法最近一次根据反向传播算出梯度后迭代得到的。另一方面,反向传播的梯度计算可能依赖于各变量的当前值,而这些变量的当前值是通过正向传播计算得到的。这个当前值是通过从输入层到输出层的正向传播计算并存储得到的。因此,在模型参数初始化完成后,我们交替地进行正向传播和反向传播,并根据反向传播计算的梯度迭代模型参数。既然我们在反向传播中使用了正向传播中计算得到的中间变量来避免重复计算,那么这个复用也导致正向传播结束后不能立即释放中间变量内存。这也是训练要比预测占用更多内存的一个重要原因。另外需要指出的是,这些中间变量的个数大体上与网络层数线性相关,每个变量的大小跟批量大小和输入个数也是线性相关的,它们是导致较深的神经网络使用较大批量训练时更容易超内存的主要原因。

数值稳定性和模型初始化

理解了正向传播与反向传播以后,我们来讨论一下深度学习模型的数值稳定性问题以及模型参数的初始化方法。深度模型有关数值稳定性的典型问题是衰减(vanishing)和爆炸(explosion)。

衰减和爆炸

在神经网络中,通常需要随机初始化模型参数。下面我们来解释这样做的原因。

为了方便解释,假设输出层只保留一个输出单元o1(删去o2o3以及指向它们的箭头),且隐藏层使用相同的激活函数。如果将每个隐藏单元的参数都初始化为相等的值,那么在正向传播时每个隐藏单元将根据相同的输入计算出相同的值,并传递至输出层。在反向传播中,每个隐藏单元的参数梯度值相等。因此,这些参数在使用基于梯度的优化算法迭代后值依然相等。之后的迭代也是如此。在这种情况下,无论隐藏单元有多少,隐藏层本质上只有1个隐藏单元在发挥作用。因此,正如在前面的实验中所做的那样,我们通常将神经网络的模型参数,特别是权重参数,进行随机初始化。

MXNet的默认随机初始化

随机初始化模型参数的方法有很多。在机器学习(ML)一之 Linear Regression一节中,我们使用net.initialize(init.Normal(sigma=0.01))使模型net的权重参数采用正态分布的随机初始化方式。如果不指定初始化方法,如net.initialize(),MXNet将使用默认的随机初始化方法:权重参数每个元素随机采样于-0.07到0.07之间的均匀分布,偏差参数全部清零。

Xavier随机初始化

还有一种比较常用的随机初始化方法叫作Xavier随机初始化。 假设某全连接层的输入个数为a,输出个数为b,Xavier随机初始化将使该层中权重参数的每个元素都随机采样于均匀分布

  

 它的设计主要考虑到,模型参数初始化后,每层输出的方差不该受该层输入个数影响,且每层梯度的方差也不该受该层输出个数影响。

实战Kaggle比赛:房价预测代码实现

  1 #!/usr/bin/env python
  2 # coding: utf-8
  3 
  4 # In[1]:
  5 
  6 
  7 # 如果没有安装pandas,则反注释下面一行
  8 # !pip install pandas
  9 
 10 get_ipython().run_line_magic('matplotlib', 'inline')
 11 import d2lzh as d2l
 12 from mxnet import autograd, gluon, init, nd
 13 from mxnet.gluon import data as gdata, loss as gloss, nn
 14 import numpy as np
 15 import pandas as pd
 16 
 17 
 18 # In[2]:
 19 
 20 
 21 train_data = pd.read_csv('./data/kaggle_house_pred_train.csv')
 22 test_data = pd.read_csv('./data/kaggle_house_pred_test.csv')
 23 
 24 
 25 # In[3]:
 26 
 27 
 28 
 29 train_data.shape
 30 
 31 
 32 # In[4]:
 33 
 34 
 35 test_data.shape
 36 
 37 
 38 # In[5]:
 39 
 40 
 41 train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]]
 42 
 43 
 44 # In[6]:
 45 
 46 
 47 all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
 48 
 49 
 50 # ### 预处理数据
 51 # 我们对连续数值的特征做标准化(standardization):设该特征在整个数据集上的均值为μ,标准差为σ。那么,我们可以将该特征的每个值先减去μ再除以σ得到标准化后的每个特征值。对于缺失的特征值,我们将其替换成该特征的均值。
 52 
 53 # In[7]:
 54 
 55 
 56 numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
 57 all_features[numeric_features] = all_features[numeric_features].apply(
 58     lambda x: (x - x.mean()) / (x.std()))
 59 # 标准化后,每个特征的均值变为0,所以可以直接用0来替换缺失值
 60 all_features[numeric_features] = all_features[numeric_features].fillna(0)
 61 
 62 
 63 # 接下来将离散数值转成指示特征。举个例子,假设特征MSZoning里面有两个不同的离散值RL和RM,那么这一步转换将去掉MSZoning特征,并新加两个特征MSZoning_RL和MSZoning_RM,其值为0或1。如果一个样本原来在MSZoning里的值为RL,那么有MSZoning_RL=1且MSZoning_RM=0。
 64 
 65 # In[9]:
 66 
 67 
 68 # dummy_na=True将缺失值也当作合法的特征值并为其创建指示特征
 69 all_features = pd.get_dummies(all_features, dummy_na=True)
 70 all_features.shape
 71 
 72 
 73 # 可以看到这一步转换将特征数从79增加到了331。
 74 # 
 75 # 最后,通过values属性得到NumPy格式的数据,并转成NDArray方便后面的训练。
 76 
 77 # In[10]:
 78 
 79 
 80 n_train = train_data.shape[0]
 81 train_features = nd.array(all_features[:n_train].values)
 82 test_features = nd.array(all_features[n_train:].values)
 83 train_labels = nd.array(train_data.SalePrice.values).reshape((-1, 1))
 84 
 85 
 86 # In[11]:
 87 
 88 
 89 loss = gloss.L2Loss()
 90 
 91 def get_net():
 92     net = nn.Sequential()
 93     net.add(nn.Dense(1))
 94     net.initialize()
 95     return net
 96 
 97 
 98 # 下面定义比赛用来评价模型的对数均方根误差。给定预测值$\hat y_1, \ldots, \hat y_n$和对应的真实标签$y_1,\ldots, y_n$,它的定义为
 99 # 
100 # $$\sqrt{\frac{1}{n}\sum_{i=1}^n\left(\log(y_i)-\log(\hat y_i)\right)^2}.$$
101 #  
102 # 对数均方根误差的实现如下。
103 
104 # In[12]:
105 
106 
107 def log_rmse(net, features, labels):
108     # 将小于1的值设成1,使得取对数时数值更稳定
109     clipped_preds = nd.clip(net(features), 1, float('inf'))
110     rmse = nd.sqrt(2 * loss(clipped_preds.log(), labels.log()).mean())
111     return rmse.asscalar()
112 
113 
114 # In[13]:
115 
116 
117 def train(net, train_features, train_labels, test_features, test_labels,
118           num_epochs, learning_rate, weight_decay, batch_size):
119     train_ls, test_ls = [], []
120     train_iter = gdata.DataLoader(gdata.ArrayDataset(
121         train_features, train_labels), batch_size, shuffle=True)
122     # 这里使用了Adam优化算法
123     trainer = gluon.Trainer(net.collect_params(), 'adam', {
124         'learning_rate': learning_rate, 'wd': weight_decay})
125     for epoch in range(num_epochs):
126         for X, y in train_iter:
127             with autograd.record():
128                 l = loss(net(X), y)
129             l.backward()
130             trainer.step(batch_size)
131         train_ls.append(log_rmse(net, train_features, train_labels))
132         if test_labels is not None:
133             test_ls.append(log_rmse(net, test_features, test_labels))
134     return train_ls, test_ls
135 
136 
137 # ### K 折交叉验证
138 # 我们在“模型选择、欠拟合和过拟合”中介绍了K折交叉验证。它将被用来选择模型设计并调节超参数。下面实现了一个函数,它返回第i折交叉验证时所需要的训练和验证数据。
139 
140 # In[14]:
141 
142 
143 def get_k_fold_data(k, i, X, y):
144     assert k > 1
145     fold_size = X.shape[0] // k
146     X_train, y_train = None, None
147     for j in range(k):
148         idx = slice(j * fold_size, (j + 1) * fold_size)
149         X_part, y_part = X[idx, :], y[idx]
150         if j == i:
151             X_valid, y_valid = X_part, y_part
152         elif X_train is None:
153             X_train, y_train = X_part, y_part
154         else:
155             X_train = nd.concat(X_train, X_part, dim=0)
156             y_train = nd.concat(y_train, y_part, dim=0)
157     return X_train, y_train, X_valid, y_valid
158 
159 
160 # In[15]:
161 
162 
163 #在 K 折交叉验证中我们训练 K 次并返回训练和验证的平均误差。
164 def k_fold(k, X_train, y_train, num_epochs,
165            learning_rate, weight_decay, batch_size):
166     train_l_sum, valid_l_sum = 0, 0
167     for i in range(k):
168         data = get_k_fold_data(k, i, X_train, y_train)
169         net = get_net()
170         train_ls, valid_ls = train(net, *data, num_epochs, learning_rate,
171                                    weight_decay, batch_size)
172         train_l_sum += train_ls[-1]
173         valid_l_sum += valid_ls[-1]
174         if i == 0:
175             d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse',
176                          range(1, num_epochs + 1), valid_ls,
177                          ['train', 'valid'])
178         print('fold %d, train rmse %f, valid rmse %f'
179               % (i, train_ls[-1], valid_ls[-1]))
180     return train_l_sum / k, valid_l_sum / k
181 
182 
183 # In[16]:
184 
185 
186 #模型选择----我们使用一组未经调优的超参数并计算交叉验证误差。可以改动这些超参数来尽可能减小平均测试误差。
187 k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
188 train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr,
189                           weight_decay, batch_size)
190 print('%d-fold validation: avg train rmse %f, avg valid rmse %f'
191       % (k, train_l, valid_l))
192 
193 
194 # In[17]:
195 
196 
197 #预测并在Kaggle提交结果
198 def train_and_pred(train_features, test_features, train_labels, test_data,
199                    num_epochs, lr, weight_decay, batch_size):
200     net = get_net()
201     train_ls, _ = train(net, train_features, train_labels, None, None,
202                         num_epochs, lr, weight_decay, batch_size)
203     d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse')
204     print('train rmse %f' % train_ls[-1])
205     preds = net(test_features).asnumpy()
206     test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
207     submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
208     submission.to_csv('submission.csv', index=False)
209 
210 
211 # In[18]:
212 
213 
214 train_and_pred(train_features, test_features, train_labels, test_data,
215                num_epochs, lr, weight_decay, batch_size)
View Code
posted @ 2020-02-15 14:54  Jaww  阅读(956)  评论(0编辑  收藏  举报