[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)

contents

  • 前馈神经网络(part 2)
    • 写在开头
    • 自动梯度计算
      • torch中自动梯度的封装
        • 简介
        • 过程内容
        • 对比
        • 模型简化
          • 直接创建
      • 利用预定义算子重新实现前馈神经网络
        • 使用pytorch预定义算子重新实现二分类
        • 增加一个3个神经元的隐藏层,再次实现二分类,进行对比
      • 完善Runner类
      • 模型训练、性能评价
      • 思考
    • 优化问题
      • 参数初始化
      • 梯度消失问题
      • 死亡ReLU问题
      • 额外科普:早停
    • 附:Git使用(以GitHub为例)
      • Git安装
      • 配置个人信息
      • 创建版本库
      • 文件操作
      • 查看提交历史
      • 版本回退
      • 连接Github
        • 生成ssh key
        • 添加远程仓库及操作
        • 指令太多啦
    • 写在最后

前馈神经网络(part 2)

写在开头

上一部分我们已经了解了神经元和一些基础的元素,以及如何使用他们来进行简单的二分类任务。本章我们将进行自动梯度计算以及优化问题的研究学习。

自动梯度计算

torch中自动梯度的封装

简介

在hw3中,我们通过自己造轮子实现了这一部分的基础结构,其实事实远不止我们造轮子的那般简单。自动梯度计算在torch中拥有更好的封装格式和自动梯度结构:自动微分引擎 torch.autograd

过程内容

该核心功能主要在Tensor类中进行构建。当requires_grad被设置为True时,将会对当前的Tensor开始追踪梯度。该过程也是基于计算图的。当在进行前向传播时,torch将会对其构建计算图,计算图记录下所有的tensor数据、执行过的算子等(这些能够自动微分的都基于torch.autograd.Function,前向和反向的计算核心都在这个函数中,相当于将hw3中抽象的类再进一步抽象出通用的前向和反向部分。官方传送门)
要实现自动梯度计算,离不开这个类。下面给出官方的使用样例:

# 这边随便定义一个继承自Function的类,以线性为例,本部分仅包含前向和反向的计算函数
class SomeFunc(Function):
@staticmethods
    @staticmethod
    def forward(ctx, input, weight, bias=None):
        ctx.save_for_backward(input, weight, bias)
        output = input.mm(weight.t())
        if bias is not None:
            output += bias.unsqueeze(0).expand_as(output)
        return output
        
    @staticmethod
    def backward(ctx, grad_output):
        input, weight, bias = ctx.saved_tensors
        grad_input = grad_weight = grad_bias = None
        if ctx.needs_input_grad[0]:
            grad_input = grad_output.mm(weight)
        if ctx.needs_input_grad[1]:
            grad_weight = grad_output.t().mm(input)
        if bias is not None and ctx.needs_input_grad[2]:
            grad_bias = grad_output.sum(0)

        return grad_input, grad_weight, grad_bias

由此我们能将一些复杂且量大的计算封装,也能自定义各种算子和层,以及一些torch不可导的操作。同时,由于一些计算方法需要在前向传播时就保存一些数据(如Linear需要保存输入来进行梯度计算),torch还提供张量钩子,用以保存数据。
由Function类开始,能够进行具体的层和算子的构建。对此torch又构建了一个Module类(但是很奇怪,Module不继承Function类,个人理解是有些不需要单独构建前向或反向,全部依赖内部使用的层和算子的梯度进行链式求导即可,比如构建一个模型,这样泛用性更好,要用时利用tensor_hook处理)。可以如此理解:Module是由一系列Function组成,Function和Variable组成计算图,因此在反向时回调Function的backward,不需要自己定义backward。同时,Module还有很多对应参数和其他函数及变量。
总体的自动微分还是基于计算图的,具体过程通过官网可知:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第1张图片
这边的计算图是动态的,每次backward操作都会重新操作一次计算图的创建。这就是为什么动态图比较慢。如果一些常量不需要创建计算图,只需要将requires_grad设置成False即可。

对比

paddlepaddle中有层的概念,torch中通过查看代码显然可以发现:不论是Linear,Conv还是Sigmoid等算子,都继承了nn.Module,我们所构建的模型也是基于nn.Module,因此torch中相应的都为nn.Module,偶尔使用如上文所说的Function也需要通过构建Module并使用apply函数来进行对接使用。
反向传播通过调用backward函数来进行,torch自动计算梯度并在优化时更新参数。一般的模型结构和优化过程代码如下:

class Model(nn.Module):
	def __init__(self, *args, **kwargs):
		super(Model, self).__init__()
		self.layer1 = ...
		self.layer2 = ...
		...
	def parameters(self): # 一般不需要定义,不进行显式计算时自动有parameters
		...
	def forward(self, x):
		...
...
...
# train
for i in range(epochs):
	y_pred = model(x)
	loss = criterion(y, y_pred)
	optimizer.zero_grad() #清零梯度
	loss.backward() # 梯度反向传播
	optimizer.step() # 更新参数
	... # 一些输出和测试
...
...

模型简化

在前面hw3的pytorch代码进行模型构建的时候,我们使用了torch.nn.Sequential进行模型的简化创建,nn.Sequential将其中的各个层(基于nn.Module)进行级联并得到最终模型。官方文档中有个有趣的小东西:torch中同时还存在着一个ModuleList,但是它只是存下来了各个层,并没有连接操作,因此它只是用来缓存一些结构,方便批量创建和遍历等使用的。
Sequential有多种创建方式,这边只介绍常用的两种:

直接创建

直接创建如hw3中,直接在类初始化中填入各个层即可模型将会按照顺序进行模型构建(问我为啥按顺序?SEQUENTIAL就是答案):

model = Sequential(
		foo,
		bar,
		...
		)

这里给个小科普:foo,bar在英语中就像中文的张三李四,甲乙丙丁,小A小B一样,没有含义,文中放在这边仅仅代表可以是任意层的实例化类XP

我们也可以通过add_module一层一层地添加:

model = Sequential()
model.add_module('foo', foo)
model.add_module('bar', bar)
...
...

利用预定义算子重新实现前馈神经网络

使用pytorch预定义算子重新实现二分类

有了前面的基础,这边我们直接给出各部分代码和结果:

###数据集构建
dataset = DatasetGenerator()
# 定义两类数据生成函数
def func_claz1(x):
    ox = torch.cos(x).reshape(-1,1) + torch.normal(0,0.05,size=(x.shape[0],1))
    oy = torch.sin(x).reshape(-1,1) + torch.normal(0,0.05,size=(x.shape[0],1))
    return torch.hstack([ox,oy])
def func_claz2(x):
    ox = 1-torch.cos(x).reshape(-1,1) + torch.normal(0,0.05,size=(x.shape[0],1))
    oy = 0.5- torch.sin(x).reshape(-1,1) + torch.normal(0,0.05,size=(x.shape[0],1))
    return torch.hstack([ox,oy])
#生成两类数据
dataset.generate(torch.linspace(0,torch.pi,1000),func_claz1,0)
dataset.generate(torch.linspace(0,torch.pi,1000),func_claz2,1)
dataset.shuffle()
#生成训练、评估、测试集
dataset_train,dataset_test = dataset.train_test_split(False,split_size=(1600,400))
dataset_eval,dataset_test = dataset_test[:200],dataset_test[200:]
# 可视化
plt.rcParams['font.family'] = 'Microsoft YaHei'
plt.scatter(dataset_train[:,0],dataset_train[:,1],label='train',c='r',alpha=0.5)
plt.scatter(dataset_eval[:,0],dataset_eval[:,1],label='eval',c='g',alpha=0.5)
plt.scatter(dataset_test[:,0],dataset_test[:,1],label='test',c='b',alpha=0.5)
plt.title('依然是弯月数据集')
plt.legend()


### 模型和损失函数、优化器、Runner类构建
model = torch.nn.Sequential(
    torch.nn.Linear(2,5),
    torch.nn.Sigmoid(),
    torch.nn.Linear(5,1),
    torch.nn.Sigmoid()
)
criterion = torch.nn.MSELoss
optimizer = torch.optim.SGD
runner = Runner(model, criterion, optimizer, {'lr':1})


### 训练2000代,以最后一位为标签值,每隔200代输出测试结果
runner.train(dataset_train,-1,2000,dataset_eval,200)


### 模型测试
runner.test(dataset_test, -1)

输出结果如下:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第2张图片[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第3张图片

增加一个3个神经元的隐藏层,再次实现二分类,进行对比

这边只需要对模型进行简单修改即可:

model = torch.nn.Sequential(
    torch.nn.Linear(2,5),
    torch.nn.Sigmoid(),
    torch.nn.Linear(5,3),
    torch.nn.Sigmoid(),
    torch.nn.Linear(3,1),
    torch.nn.Sigmoid()
)

输出结果如下:

[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第4张图片
和前面少一个3神经元隐藏层的模型相比,保持训练代数不变且学习率不变,我们发现模型收敛速度反而慢了,准确率也并没有显著提升。对此分析,可能是因为网络参数变多,导致的收敛变慢和欠拟合,也可能是学习率过低导致模型训练2000代后依然效果不佳。当尝试提高学习率到8后,得到在验证集上的准确率为1:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第5张图片
另外,由于是随机权重初始化,因此每次运行的结果也会有所变化。

完善Runner类

本次的Runner类有较大改动,具体为:

  • 在类中实例化损失函数和优化器
  • 增加重置模型函数
  • 修改训练、测试、评估部分的代码
    代码如下:
class Runner(object):
    def __init__(self, model, criterion, optimizer, **kwargs):
        self.model = model
        self.criterion = criterion
        self.optimizer = optimizer
        self.kwargs = kwargs

    def train(self, dataset_train, split_x_y_pos, epochs, dataset_eval, epochs_display):
        if self.imodel is None:
            self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))
        icriterion = self.criterion()
        ioptimizer = self.optimizer(
            self.imodel.parameters(),
            self.kwargs.get('lr',0.01),
            self.kwargs.get('momentum',0)
            )
        accum_loss = []

        train_x, train_y = dataset_train[:,:split_x_y_pos],dataset_train[:,split_x_y_pos:]
        
        for i in range(1, epochs + 1):
            out = self.imodel(train_x)
            loss = icriterion(out, train_y)
            accum_loss.append(loss.data.item())
            ioptimizer.zero_grad()
            loss.backward()
            ioptimizer.step()

            if i % epochs_display == 0:
                print('[{} / {}] loss = {}, acc = {}'.format(i,epochs,loss))
                self.eval(dataset_eval,-1)
        return accum_loss

    def eval(self,model,dataset_eval,split_x_y_pos):
        if self.imodel is None:
            self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))
        eval_x, eval_y = dataset_eval[:,:split_x_y_pos],dataset_eval[:,split_x_y_pos:]
        icriterion = self.criterion()
        loss = icriterion(model(eval_x),eval_y)
        print('eval loss = {}'.format(loss.data.item()))
        return loss

    def test(self, dataset_test, split_x_y_pos, additional_func = None, description = ''):
        if self.imodel is None:
            self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))
        test_x, test_y = dataset_test[:,:split_x_y_pos],dataset_test[:,split_x_y_pos:]
        icriterion = self.criterion()
        y_pred = self.imodel(test_x)
        loss = icriterion(y_pred, test_y)
        print('test loss = {}, '.format(loss.data.item()), end='')
        if additional_func is not None:
            print('{} = {}'.format(description, additional_func(test_x, test_y, y_pred, loss)))
        else:
            print('')

    def save_model(self, path):
        if self.imodel is None:
            self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))
        torch.save(self.imodel, path)

    def load_model(self, path):
        self.imodel = torch.load(path)

    def reset_model(self):
        self.imodel = eval('self.model({})'.format(','.join(self.kwargs.get('model_init_params', None))))

模型训练、性能评价

该部分在前面的小题中就已经完成了,故此不再赘述。

思考

自定义梯度计算和自动梯度计算:
从计算性能、计算结果等多方面比较,谈谈自己的看法。

答:计算性能:由于自动梯度计算在torch中是由C++进行底层实现的,而自定义梯度计算由自己定义并使用python进行创建,因此自动梯度计算更加快。个人认为,如果全都使用python实现,自定义梯度已经将梯度过程写好,而自动梯度则还需要创建计算图,则自定义速度更快。
计算结果:如果自定义的梯度计算写对了,按理说是与自动梯度完全相同的,在hw3中自造轮子的反向传播,已经检验发现与torch自动梯度得到的值完全相同,不会存在计算结果的差异。

优化问题

参数初始化

实现一个神经网络前,需要先初始化模型参数。

如果对每一层的权重和偏置都用0初始化,那么通过第一遍前向计算,所有隐藏层神经元的激活值都相同;在反向传播时,所有权重的更新也都相同,这样会导致隐藏层神经元没有差异性,出现对称权重现象。
测试的代码我们使用hw3中对应的pytorch代码:

import torch
x = torch.tensor([0.5,0.3])
weights = torch.tensor([0.2, -0.4, 0.5, 0.6, 0.1, -0.5, -0.3, 0.8])
y = torch.tensor([0.23, -0.07])
print('inputs={}'.format(x))
print('real outputs={}'.format(y))
model = torch.nn.Sequential(
    torch.nn.Linear(2,2,False),
    torch.nn.Sigmoid(),
    torch.nn.Linear(2,2,False),
    torch.nn.Sigmoid()
)
model[0].weight.data = torch.zeros(4).reshape(2,2)
model[2].weight.data = torch.zeros(4).reshape(2,2)
optimizer = torch.optim.SGD(model.parameters(),1,momentum=0)
loss_fn = torch.nn.MSELoss()
for i in range(1):
    y_pred = model(x)
    loss = loss_fn(y,y_pred)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
print('w1,w3,w2,w4:{}'.format(model[0].weight.data.detach().reshape(1,-1).squeeze()))
print('w5,w7,w6,w8:{}'.format(model[2].weight.data.detach().reshape(1,-1).squeeze()))
y_pred = model(x)
print('pred={}'.format(y_pred))
loss = loss_fn(y,y_pred)
print('MSE loss={}'.format(loss))

在进行一轮反向传播后,得到:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第6张图片
经过训练我们发现准确率≈50%,即模型废掉了,所以我们要使用随机权重的初始化。

梯度消失问题

在神经网络的构建过程中,随着网络层数的增加,理论上网络的拟合能力也应该是越来越好的。但是随着网络变深,参数学习更加困难,容易出现梯度消失问题。

由于Sigmoid型函数的饱和性,饱和区的导数更接近于0,误差经过每一层传递都会不断衰减。当网络层数很深时,梯度就会不停衰减,甚至消失,使得整个网络很难训练,这就是所谓的梯度消失问题。
在深度神经网络中,减轻梯度消失问题的方法有很多种,一种简单有效的方式就是使用导数比较大的激活函数,如:ReLU。
我们在原来的网络上增加两层,训练一代并观察其中权重:

import torch
x = torch.tensor([2.,3.])
y = torch.tensor([1.23, 1.07])
print('inputs={}'.format(x))
print('real outputs={}'.format(y))
model = torch.nn.Sequential(
    torch.nn.Linear(2,1,False),
    torch.nn.Sigmoid(),
    torch.nn.Linear(1,1,False),
    torch.nn.Sigmoid(),
    torch.nn.Linear(1,2,False),
    torch.nn.Sigmoid()
)
optimizer = torch.optim.SGD(model.parameters(),1,momentum=0)
loss_fn = torch.nn.MSELoss()
for i in range(1):
    y_pred = model(x)
    loss = loss_fn(y,y_pred)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

for i in range(0,len(model),2):
    print(model[i].weight.grad)

训练一代,我们发现梯度在反向传播中越来越小:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第7张图片

我们将sigmoid替换成relu再次实验,可见:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第8张图片
使用relu确实能够实现对梯度消失的抑制。

死亡ReLU问题

ReLU激活函数可以一定程度上改善梯度消失问题,但是在某些情况下容易出现死亡ReLU问题,使得网络难以训练。

这是由于当x<0x<0时,ReLU函数的输出恒为0。在训练过程中,如果参数在一次不恰当的更新后,某个ReLU神经元在所有训练数据上都不能被激活(即输出为0),那么这个神经元自身参数的梯度永远都会是0,在以后的训练过程中永远都不能被激活。

一种简单有效的优化方式就是将激活函数更换为Leaky ReLU、ELU等ReLU的变种。
在前面的代码基础上训练两代,即可发现所有权重因为ReLU的存在全部变成了0。我们尝试LeakyReLU来补偿这个梯度:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第9张图片
相比普通的ReLU在第二代全部清零的效果好太多了。

额外科普:早停

模型训练代数越多,其拟合效果越好,但是会存在过于好的情况,即过拟合问题,过拟合后模型泛化能力显著下降。
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第10张图片
对此,我们设计早停的方法:当在模型的验证集上权重更新低于某个值或错误率低于某个值或到了迭代次数就停止训练。
我们定义早停类用于进行带早停的训练,这部分也可以整合进Runner类。

class EarlyStopping():
    def __init__(self,patience=7,verbose=False,delta=0):
        self.patience = patience
        self.verbose = verbose
        self.counter = 0
        self.best_score = None
        self.early_stop = False
        self.val_loss_min = np.Inf
        self.delta = delta
    def __call__(self,val_loss,model,path):
        print("val_loss={}".format(val_loss))
        score = -val_loss
        if self.best_score is None:
            self.best_score = score
            self.save_checkpoint(val_loss,model,path)
        elif score < self.best_score+self.delta:
            self.counter+=1
            print(f'EarlyStopping counter: {self.counter} out of {self.patience}')
            if self.counter>=self.patience:
                self.early_stop = True
        else:
            self.best_score = score
            self.save_checkpoint(val_loss,model,path)
            self.counter = 0
    def save_checkpoint(self,val_loss,model,path):
        if self.verbose:
            print(
                f'Validation loss decreased ({self.val_loss_min:.6f} --> {val_loss:.6f}).  Saving model ...')
        torch.save(model.state_dict(), path+'/'+'model_checkpoint.pth')
        self.val_loss_min = val_loss

附:Git使用(以GitHub为例)

Git 是一个免费和开源的分布式版本控制系统,旨在以速度和效率处理从小型到大型项目的所有内容。
Git 易于学习,占用空间小,性能快如闪电。 它优于 SCM 工具,如 Subversion、CVS、Perforce 和 ClearCase,具有廉价的本地分支、方便的暂存区域和多个工作流等功能。
通过学习Git的使用,我们能够更好地进行版本控制和项目协作。

Git安装

Git可以通过访问Git官网的下载页面,根据操作系统进行安装。在安装完成后能够打开终端使用git:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第11张图片

配置个人信息

第一次使用时,我们需要配置好个人信息,具体指令如下:

git config --global user.name "name"
git config --global user.email "[email protected]"

创建版本库

配置完成后,我们进入一个文件夹,这里以test_git文件夹为例:
输入

git init

此时可见如下初始化内容:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第12张图片

文件操作

此时对该文件目录进行的所有编辑操作,都将会被记录下来,以创建readme.md为例,首先创建文件,然后添加到仓库,最后提交到仓库。删除、编辑等操作是相同的。

echo # hello, git > readme.md
git add -A
git commit -m "initial commit"

结果如下:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第13张图片

查看提交历史

通过如下指令

git log

能够查看到所有的编辑内容:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第14张图片

版本回退

当修改错了的时候,我们需要进行版本回退。当前版本为,HEAD,在前面追加^经过验证这个符号在新版本已经不支持了)或是使用HEAD~xxx来进行回退量的控制:

git reset --hard HEAD^

[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第15张图片
因此我们使用

git reset --hard HEAD~1

可见版本回退成功,内容也变成了上一个提交后的内容。
在这里插入图片描述
我们还能够使用版本的commit id来进行回退,只需要记住前面的几位就行:

git reset --hard a85d

在这里插入图片描述

连接Github

Github作为全球最大的开源仓库,里面有非常多的优质开源代码,我们也同样可以通过Git来提交自己的开源代码。

生成ssh key

自己的仓库当然只能自己进行编辑,因此需要有个令牌告诉这是你自己在进行操作。Github通过ssh key来进行访问的控制。
首先我们需要生成ssh key:

ssh-keygen -t rsa -C "[email protected]"

中途全部回车即可,得到如下结果即为成功
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第16张图片
然后根据此部分:
在这里插入图片描述
从目录下即可看到ssh key的公钥和私钥文件。私钥需要自己保管好,提交至GitHub的是公钥:打开settings>SSH and GPG keys
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第17张图片
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第18张图片
在这里插入图片描述
点击添加并粘贴刚刚公钥(在id_rsa.pub文件中)并确定
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第19张图片
点击“Add SSH key”后即可在列表中看见:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第20张图片

添加远程仓库及操作

当在GitHub上创建一个新仓库后,即有远程仓库的添加教程。相对在本地编辑,远程多了push用于将本地修改最终传送至远程仓库、pull和fetch用于从远程仓库下载内容(pull获取最新版本且与本地仓库合并)。

指令太多啦

git非常好用,但是有些懒人(比如我)想要更为傻瓜式地进行仓库的管理维护。由此我再次案例地表最强编辑器VSCode(不许有人不知道VSCode!XD)。在远程或本地仓库初始化完毕后,用VSCode打开这个仓库所在文件夹,就能享受一键上床下载!具体过程就算不教都能够明白,直接放之前数模时候的一个仓库:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第21张图片
超级简单!修改完点击提交并填写commit内容即可享受一条龙服务~其中的菜单还支持几乎所有git功能:
[2022-10-06]神经网络与深度学习第3章-前馈神经网络(part2)_第22张图片

写在最后

通过之前的实验基础以及对原理的认识,本次实验难度并不是很大,但是本次让我们更加直观地见识了深度学习中一些巧妙结构如计算图、自动微分等大杀器,也通过阅读文献和技术文档对于我们将长时间使用下去的框架有了一个更深的认识。同时,我们也了解了不同算子在不同数据分布上的性能表现,了解了解决如梯度消失、死亡ReLU等问题的可行手段。另外,通过了解git的使用,我们能够更好地在团队协作的项目中保持高效。

你可能感兴趣的:([DL]神经网络与深度学习,神经网络,深度学习)