提示:本文面向神经网络与深度学习基础的人群,如果想初学神经网络,建议翻我之前的博客。
一、自动梯度计算和预定义算子
自动梯度计算和预定义算子
虽然我们能够通过模块化的方式比较好地对神经网络进行组装,但是每个模块的梯度计算过程仍然十分繁琐且容易出错。在深度学习框架中,已经封装了自动梯度计算的功能,我们只需要聚焦模型架构,不再需要耗费精力进行计算梯度。
1.1 利用预定义算子重新实现前馈神经网络二分类任务
利用预定义算子重新实现前馈神经网络 下面我们使用torch的预定义算子来重新实现二分类任务。 主要使用到的预定义算子为torch.nn.Linear :
class torch.nn.Linear(in_features, out_features, weight_attr=None, bias_attr=None, name=None)
torch.nn.Linear 算子可以接受一个形状为[batch_size,?,in_features]的输入张量,其中"?"表示张量中可以有任意的其它额外维度,并计算它与形状为[in_features, out_features]的权重矩阵的乘积,然后生成形状为[batch_size,?,out_features]的输出张量。 torch.nn.Linear 算子默认有偏置参数,可以通过bias_attr=False 设置不带偏置。
具体代码如下:
import torch.nn as nn
import torch.nn.functional as F
import torch
class Model_MLP_L2_V2(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(Model_MLP_L2_V2, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, output_size)
self.act_fn = F.sigmoid
def forward(self, inputs):
z1 = self.fc1(inputs)
a1 = self.act_fn(z1)
z2 = self.fc2(a1)
a2 = self.act_fn(z2)
return a2
1.2 基于已有的RunnerV2_1完善RunnerV2_2
基于上一节实现的 RunnerV2_1 类,本节的 RunnerV2_2 类在训练过程中使用自动梯度计算;模型保存时,使用state_dict方法获取模型参数;模型加载时,使用set_state_dict方法加载模型参数. 具体代码如下:
class RunnerV2_2(object):
def __init__(self, model, optimizer, metric, loss_fn, **kwargs):
self.model = model
self.optimizer = optimizer
self.loss_fn = loss_fn
self.metric = metric
self.train_scores = []
self.dev_scores = []
self.train_loss = []
self.dev_loss = []
def train(self, train_set, dev_set, **kwargs):
self.model.train()
num_epochs = kwargs.get("num_epochs", 0)
log_epochs = kwargs.get("log_epochs", 100)
save_path = kwargs.get("save_path", "best_model.pt")
custom_print_log = kwargs.get("custom_print_log", None)
best_score = 0
for epoch in range(num_epochs):
X, y = train_set
logits = self.model(X)
trn_loss = self.loss_fn(logits, y)
self.train_loss.append(trn_loss.item())
trn_score = self.metric(logits, y).item()
self.train_scores.append(trn_score)
trn_loss.backward()
if custom_print_log is not None:
custom_print_log(self)
self.optimizer.step()
self.optimizer.zero_grad()
dev_score, dev_loss = self.evaluate(dev_set)
if dev_score > best_score:
self.save_model(save_path)
print(f"[Evaluate] best accuracy performence has been updated: {best_score:.5f} --> {dev_score:.5f}")
best_score = dev_score
if log_epochs and epoch % log_epochs == 0:
print(f"[Train] epoch: {epoch}/{num_epochs}, loss: {trn_loss.item()}")
@torch.no_grad()
def evaluate(self, data_set):
self.model.eval()
X, y = data_set
logits = self.model(X)
loss = self.loss_fn(logits, y).item()
self.dev_loss.append(loss)
score = self.metric(logits, y).item()
self.dev_scores.append(score)
return score, loss
@torch.no_grad()
def predict(self, X):
self.model.eval()
return self.model(X)
def save_model(self, saved_path):
torch.save(self.model.state_dict(), saved_path)
def load_model(self, model_path):
state_dict = torch.load(model_path)
self.model.set_state_dict(state_dict)
1.3 实现模型训练
实例化RunnerV2类,并传入训练配置,代码实现如下:
input_size = 2
hidden_size = 5
output_size = 1
model = Model_MLP_L2_V2(input_size=input_size, hidden_size=hidden_size, output_size=output_size)
loss_fn = F.binary_cross_entropy
optimizer = torch.optim.SGD(model.parameters(), lr=0.2)
def accuracy(preds, labels):
if preds.shape[1] == 1:
preds = (preds>=0.5).to(torch.float32)
else:
preds = torch.argmax(preds,dim=1).int()
return torch.mean((preds == labels).float())
metric = accuracy
epoch_num = 1000
saved_path = 'best_model.pt'
runner = RunnerV2_2(model, optimizer, metric, loss_fn)
runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=epoch_num, log_epochs=50, save_path="best_model.pt")
实现结果展现: 可以看出的是,损失达到了0.26,效果还是比较好,精度达到了0.9000. 将训练过程中训练集与验证集的准确率变化情况进行可视化。 代码如下:
def plot(runner, fig_name):
plt.figure(figsize=(10,5))
epochs = [i for i in range(len(runner.train_scores))]
plt.subplot(1,2,1)
plt.plot(epochs, runner.train_loss, color='#e4007f', label="Train loss")
plt.plot(epochs, runner.dev_loss, color='#f19ec2', linestyle='--', label="Dev loss")
plt.ylabel("loss", fontsize='large')
plt.xlabel("epoch", fontsize='large')
plt.legend(loc='upper right', fontsize='x-large')
plt.subplot(1,2,2)
plt.plot(epochs, runner.train_scores, color='#e4007f', label="Train accuracy")
plt.plot(epochs, runner.dev_scores, color='#f19ec2', linestyle='--', label="Dev accuracy")
plt.ylabel("score", fontsize='large')
plt.xlabel("epoch", fontsize='large')
plt.legend(loc='lower right', fontsize='x-large')
plt.savefig(fig_name)
plt.show()
plot(runner, 'fw-acc.pdf')
可视化结果示意: 可以看出训练集的损失不断在下降,训练集和测试集的精度都在上升,整体效果不错。
1.4 性能评价
使用测试数据对训练完成后的最优模型进行评价,观察模型在测试集上的准确率以及loss情况。代码如下:` 这里如果不能加载模型的话,就用现成我们训练好了的模型就可以,直接可以注释掉,现在只是学习阶段,如果需要保存的话以后再学也不是难事哈。
score, loss = runner.evaluate([X_test, y_test])
print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))
结果图: 从结果来看,模型在测试集上取得了较高的准确率。
附:增加一个3个神经元的隐藏层,再次实现二分类,并与1做对比。
修改后的Model_MLP_L2_V2类:
import torch.nn as nn
import torch.nn.functional as F
import torch
class Model_MLP_L2_V2(nn.Module):
def __init__(self, input_size, hidden_size,hidden_size_, output_size):
super(Model_MLP_L2_V2, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, hidden_size_)
self.fc3 = nn.Linear(hidden_size_,output_size)
self.act_fn = F.sigmoid
def forward(self, inputs):
z1 = self.fc1(inputs)
a1 = self.act_fn(z1)
z2 = self.fc2(a1)
a2 = self.act_fn(z2)
z3 = self.fc3(a2)
a3 = self.act_fn(z3)
return a3
Runner类及其他代码不变。 训练代码:
input_size = 2
hidden_size = 5
hidden_size_ = 3
output_size = 1
model = Model_MLP_L2_V2(input_size=input_size, hidden_size=hidden_size,hidden_size_ = hidden_size_, output_size=output_size)
loss_fn = F.binary_cross_entropy
optimizer = torch.optim.SGD(model.parameters(), lr=0.2)
def accuracy(preds, labels):
if preds.shape[1] == 1:
preds = (preds>=0.5).to(torch.float32)
else:
preds = torch.argmax(preds,dim=1).int()
return torch.mean((preds == labels).float())
metric = accuracy
epoch_num = 1000
saved_path = 'best_model.pt'
runner = RunnerV2_2(model, optimizer, metric, loss_fn)
runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=epoch_num, log_epochs=50, save_path="best_model.pt")
可视化结果图: 查看评价: 我们对分类后的结果可视化,发现是这样子的: 于是,我们重新修改学习率lr=5,得到如下图: 此时我们得到的训练集和验证集损失的可视化: 总体准确率达到了99.5%还是比较不错的。
结论: 可以看出的是,再增加一层神经网元个数为3的隐藏层后,调整学习率为5,一样能够得出99.5%的准确率结果和0.009的损失。
二、优化问题
在本节中,我们通过实践来发现神经网络模型的优化问题,并思考如何改进。
2.1 关于参数初始化的问题
实现一个神经网络前,需要先初始化模型参数。如果对每一层的权重和偏置都用0初始化,那么通过第一遍前向计算,所有隐藏层神经元的激活值都相同;在反向传播时,所有权重的更新也都相同,这样会导致隐藏层神经元没有差异性,出现对称权重现象。
接下来,将模型参数全都初始化为0,看实验结果。这里重新定义了一个类TwoLayerNet_Zeros,两个线性层的参数全都初始化为0。 具体代码如下:
import torch.nn as nn
import torch.nn.functional as F
class Model_MLP_L2_V4(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(Model_MLP_L2_V4, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, output_size)
self.act_fn = F.sigmoid
def forward(self, inputs):
z1 = self.fc1(inputs)
a1 = self.act_fn(z1)
z2 = self.fc2(a1)
a2 = self.act_fn(z2)
return a2
def print_weights(runner):
print('The weights of the Layers:')
for item in runner.model.named_parameters():
print(item)
for _,param in enumerate(runner.model.named_parameters()):
print(param)
利用Runner类训练模型: 训练代码:
input_size = 2
hidden_size = 5
output_size = 1
model = Model_MLP_L2_V4(input_size=input_size, hidden_size=hidden_size, output_size=output_size)
loss_fn = F.binary_cross_entropy
learning_rate = 0.2
optimizer = torch.optim.SGD(model.parameters(),lr=learning_rate)
metric = accuracy
epoch = 2000
saved_path = 'best_model.pt'
runner = RunnerV2_2(model, optimizer, metric, loss_fn)
runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=5, log_epochs=50, save_path="best_model.pt",custom_print_log=print_weights)
训练权重结果: 可视化训练和验证集上的主准确率和loss变化:
plot(runner, "fw-zero.pdf")
可视化结果: 从输出结果看,二分类准确率为50%左右,说明模型没有学到任何内容。训练和验证loss几乎没有怎么下降。 为了避免对称权重现象,可以使用高斯分布或均匀分布初始化神经网络的参数。
高斯分布和均匀分布采样的实现和可视化代码如下:
gausian_weights = torch.normal(mean=0.0, std=1.0, size=[10000])
uniform_weights = torch.Tensor(10000)
uniform_weights.uniform_(-1,1)
gausian_weights=gausian_weights.numpy()
uniform_weights=uniform_weights.numpy()
print(uniform_weights)
print(gausian_weights)
plt.figure()
plt.subplot(1,2,1)
plt.title('Gausian Distribution')
plt.hist(gausian_weights, bins=200, density=True, color='#f19ec2')
plt.subplot(1,2,2)
plt.title('Uniform Distribution')
plt.hist(uniform_weights, bins=200, density=True, color='#e4007f')
plt.savefig('fw-gausian-uniform.pdf')
plt.show()
可视化结果:
2.2 关于梯度消失的问题
在神经网络的构建过程中,随着网络层数的增加,理论上网络的拟合能力也应该是越来越好的。但是随着网络变深,参数学习更加困难,容易出现梯度消失问题。
由于Sigmoid型函数的饱和性,饱和区的导数更接近于0,误差经过每一层传递都会不断衰减。当网络层数很深时,梯度就会不停衰减,甚至消失,使得整个网络很难训练,这就是所谓的梯度消失问题。 在深度神经网络中,减轻梯度消失问题的方法有很多种,一种简单有效的方式就是使用导数比较大的激活函数,如:ReLU。
下面通过一个简单的实验观察前馈神经网络的梯度消失现象和改进方法。
2.2.1 模型构建
定义一个前馈神经网络,包含4个隐藏层和1个输出层,通过传入的参数指定激活函数。代码实现如下:
class Model_MLP_L5(nn.Module):
def __init__(self, input_size, output_size, act='sigmoid', w_init=torch.normal(mean=torch.tensor(0.0), std=torch.tensor(0.01)), b_init=torch.tensor(1.0)):
super(Model_MLP_L5, self).__init__()
self.fc1 = torch.nn.Linear(input_size, 3)
self.fc2 = torch.nn.Linear(3, 3)
self.fc3 = torch.nn.Linear(3, 3)
self.fc4 = torch.nn.Linear(3, 3)
self.fc5 = torch.nn.Linear(3, output_size)
if act == 'sigmoid':
self.act = F.sigmoid
elif act == 'relu':
self.act = F.relu
elif act == 'lrelu':
self.act = F.leaky_relu
else:
raise ValueError("Please enter sigmoid relu or lrelu!")
self.init_weights(w_init, b_init)
def init_weights(self, w_init, b_init):
for n, m in self.named_parameters():
if isinstance(m, nn.Linear):
w_init(m.weight)
b_init(m.bias)
def forward(self, inputs):
outputs = self.fc1(inputs)
outputs = self.act(outputs)
outputs = self.fc2(outputs)
outputs = self.act(outputs)
outputs = self.fc3(outputs)
outputs = self.act(outputs)
outputs = self.fc4(outputs)
outputs = self.act(outputs)
outputs = self.fc5(outputs)
outputs = F.sigmoid(outputs)
return outputs
2.2.2 使用sigmoid函数进行训练
使用Sigmoid型函数作为激活函数,为了便于观察梯度消失现象,只进行一轮网络优化。代码实现如下: 定义梯度打印函数:
def print_grads(runner):
print('The gradient of the Layers:')
for name, item in runner.model.named_parameters():
if(len(item.size())==2):
print(name, torch.norm(input=item, p=2))
lr = 0.01
model = Model_MLP_L5(input_size=2, output_size=1, act='sigmoid')
optimizer = torch.optim.SGD(model.parameters(),lr=lr)
loss_fn = F.binary_cross_entropy
metric = accuracy
custom_print_log=print_grads
实例化RunnerV2_2类,并传入训练配置。代码实现如下:
runner = RunnerV2_2(model, optimizer, metric, loss_fn)
模型训练,打印网络每层梯度值的
?
2
\ell_2
?2?范数。代码实现如下:
runner.train([X_train, y_train], [X_dev, y_dev],
num_epochs=10, log_epochs=None,
save_path="best_model.pdparams",
custom_print_log=custom_print_log)
训练结果权重展示: 观察实验结果可以发现,梯度经过每一个神经层的传递都会不断衰减,最终传递到第一个神经层时,梯度几乎完全消失。
2.2.3 使用ReLU函数进行训练
lr = 0.01
model = Model_MLP_L5(input_size=2, output_size=1, act='relu')
optimizer = torch.optim.SGD(model.parameters(),lr=lr)
loss_fn = F.binary_cross_entropy
metric = accuracy
runner = RunnerV2_2(model, optimizer, metric, loss_fn)
runner.train([X_train, y_train], [X_dev, y_dev],
num_epochs=10, log_epochs=None,
save_path="best_model.pdparams",
custom_print_log=custom_print_log)
结果: 图4.4 展示了使用不同激活函数时,网络每层梯度值的
?
2
\ell_2
?2?范数情况。从结果可以看到,5层的全连接前馈神经网络使用Sigmoid型函数作为激活函数时,梯度经过每一个神经层的传递都会不断衰减,最终传递到第一个神经层时,梯度几乎完全消失。改为ReLU激活函数后,梯度消失现象得到了缓解,每一层的参数都具有梯度值。
图4.4:网络每层梯度的L2范数变化趋势
# 三、死亡ReLU问题 ReLU激活函数可以一定程度上改善梯度消失问题,但是ReLU函数在某些情况下容易出现死亡 ReLU问题,使得网络难以训练。这是由于当𝑥<0时,ReLU函数的输出恒为0。在训练过程中,如果参数在一次不恰当的更新后,某个ReLU神经元在所有训练数据上都不能被激活(即输出为0),那么这个神经元自身参数的梯度永远都会是0,在以后的训练过程中永远都不能被激活。而一种简单有效的优化方式就是将激活函数更换为Leaky ReLU、ELU等ReLU的变种。 ## 3.1 模型训练
使用之前定义的多层全连接前馈网络进行实验,使用ReLU作为激活函数,观察死亡ReLU现象和优化方法。当神经层的偏置被初始化为一个相对于权重较大的负值时,可以想像,输入经过神经层的处理,最终的输出会为负值,从而导致死亡ReLU现象。 定义网络:
model = Model_MLP_L5(input_size=2, output_size=1, act='relu', b_init=torch.tensor(-8.0))
实例化RunnerV2类,启动模型训练,打印网络每层梯度值的
?
2
\ell_2
?2?范数。代码实现如下:
runner = RunnerV2_2(model, optimizer, metric, loss_fn)
runner.train([X_train, y_train], [X_dev, y_dev],
num_epochs=1, log_epochs=0,
save_path="best_model.pt",
custom_print_log=custom_print_log)
训练结果: 从输出结果可以发现,使用 ReLU 作为激活函数,当满足条件时,会发生死亡ReLU问题,网络训练过程中 ReLU 神经元的梯度始终为0,参数无法更新。
针对死亡ReLU问题,一种简单有效的优化方式就是将激活函数更换为Leaky ReLU、ELU等ReLU 的变种。接下来,观察将激活函数更换为 Leaky ReLU时的梯度情况。
3.2 使用Leaky ReLU进行训练
将激活函数更换为Leaky ReLU进行模型训练,观察梯度情况。代码实现如下:
model = Model_MLP_L5(input_size=2, output_size=1, act='lrelu', b_init=torch.tensor(-8.0))
runner = RunnerV2_2(model, optimizer, metric, loss_fn)
runner.train([X_train, y_train], [X_dev, y_dev],
num_epochs=10, log_epochps=None,
save_path="best_model.pdparams",
custom_print_log=custom_print_log)
训练结果图: 从输出结果可以看到,将激活函数更换为Leaky ReLU后,死亡ReLU问题得到了改善,梯度恢复正常,参数也可以正常更新。但是由于 Leaky ReLU 中,
x
<
0
\mathcal{x<0}
x<0 时的斜率默认只有0.01,所以反向传播时,随着网络层数的加深,梯度值越来越小。如果想要改善这一现象,将 Leaky ReLU 中,
x
<
0
\mathcal{x<0}
x<0 时的斜率调大即可。
总结思考问题:
自定义梯度计算和自动梯度计算:
从计算性能、计算结果等多方面比较,谈谈自己的看法。
解: 先说自定义梯度:自定义梯度计算需要手工推导计算式,再用程序将计算式表达出来带入数值进行计算,从理论上来说相同情况下自定义梯度应该时运算时间最短且最佳的。 再说torch的自动梯度计算:Torch的自动梯度计算采用记录历史操作的办法,将每次求导结果都记录下来,然后从根到叶子结点追踪图,利用链式法则进行计算,理论上来说相同情况下自动梯度计算应该是会比自定义梯度的计算时间更长,因为在计算过程中需要追踪图浪费很多时间。 综合来说,虽然理论上自定义梯度的运算时间是优于自动梯度计算的,但是现实好像不是这样,我看到了自动梯度进行运算的流程,感觉思路设计的很好,现实程序运行也是自动梯度的时间少于自定义梯度的时间,我不知道为啥,可能是因为自动梯度内部的DAG动态调整的原因?不过这也不至于超过自定义梯度吧?想了很久都没想明白,如果你们有好想法的话,可以和我说说哈哈哈哈。
至于计算结果,自定义梯度和自动梯度还是差别很大的,可能是因为自动梯度内置DAG再加上优化器的原因,导致计算图不断被调整。这是之前的作业,是用自定义梯度和自动梯度实现的,结果差别很大,有兴趣的可以看一下:神经网络与深度学习-作业3
以上就是全部内容了,感觉思考题回答的不是很完美,但是已经很努力在想了。 参考博客:
【pytorch学习笔记】第三篇——自动梯度(torch.autograd) NNDL 实验五 前馈神经网络(2)自动梯度计算 & 优化问题 自动微分-动手学深度学习 前向传播、反向传播和计算图 nndl NNDL 实验4(上) NNDL 实验4(下)
主要参考:邱老师(邱锡鹏),神经网络与深度学习,机械工业出版社,https://nndl.github.io/, 2020.
|