PyTorch 神经网络基础笔记

PyTorch 神经网络基础学习上手笔记

1.层和块

  • (block)可以描述单个层、由多个层组成的组件或整个模型本身。

  • 使用块进行抽象的一个好处是可以将一些块组合成更大的组件,这一过程通常是递归的,如 图5.1.1所示。 通过定义代码来按需生成任意复杂度的块, 我们可以通过简洁的代码实现复杂的神经网络。

在这里插入图片描述

  • 块由(class)表示。 它的任何子类都必须定义一个将其输入转换为输出的前向传播函数, 并且必须存储任何必需的参数。
 # 多层感知机 
import torch
from torch import nn
from torch.nn import functional as F

net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

X = torch.rand(2, 20)
net(X)

​ 在这个例子中,我们通过实例化nn.Sequential来构建我们的模型, 层的执行顺序是作为参数传递的。

nn.Sequential定义了一种特殊的Module,即在PyTorch中表示一个块的类, 它维护了一个由Module组成的有序列表。

​ 我们一直在通过net(X)调用我们的模型来获得模型的输出。 这实际上是net.__call__(X)的简写。 这个前向传播函数非常简单: 它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。

自定义块

​ 每个块必须提供的基本功能:

  1. 将输入数据作为其前向传播函数的参数。
  2. 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。
  3. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
  4. 存储和访问前向传播计算所需的参数。
  5. 根据需要初始化模型参数。
从零开始编写一个块

​ 下面的MLP类继承了nn.Module表示块的类。 我们的实现只需要提供我们自己的构造函数(Python中的__init__函数)和前向传播函数。

class MLP(nn.Module):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用MLP的父类Module的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
        super().__init__()
        self.hidden = nn.Linear(20, 256)  # 隐藏层
        self.out = nn.Linear(256, 10)  # 输出层

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def forward(self, X):
        # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
        return self.out(F.relu(self.hidden(X)))
    
    
# 实例化
net = MLP()
net(X)

首先,我们定制的__init__函数通过super().__init__() 调用父类的__init__函数, 省去了重复编写模版代码的痛苦。 然后,我们实例化两个全连接层, 分别为self.hiddenself.out。 注意,除非我们实现一个新的运算符, 否则我们不必担心反向传播函数或参数初始化, 系统将自动生成这些。

顺序块

​ 我们只需要定义两个关键函数:

  1. 一种将块逐个追加到列表中的函数。
  2. 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for idx, module in enumerate(args):
            # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
            # 变量_modules中。_module的类型是OrderedDict
            self._modules[str(idx)] = module

    def forward(self, X):
        # OrderedDict保证了按照成员添加的顺序遍历它们
        for block in self._modules.values():
            X = block(X)
        return X
    
    
# 实例化    
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)

*args是收集参数,相当于把若干个参数打包成一个来传入

_modules的主要优点是: 在模块的参数初始化过程中, 系统知道在_modules字典中查找需要初始化参数的子块。

在前向传播函数中执行代码

​ 我们可以混合搭配各种组合块的方法。 在下面的例子中,我们以一些想到的方法嵌套块。

class NestMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
                                 nn.Linear(64, 32), nn.ReLU())
        self.linear = nn.Linear(32, 16)

    def forward(self, X):
        return self.linear(self.net(X))

chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)

小结

  • 一个块可以由许多层组成;一个块可以由许多块组成。
  • 块可以包含代码。
  • 块负责大量的内部处理,包括参数初始化和反向传播。
  • 层和块的顺序连接由Sequential块处理。

拓展

self指针:

​ self在方法中代表当前对象。self是指针变量,指向自己(当前对象)。

实例化:
  • 这是python的一个语法糖,net()实际上调用net.__call__(),而__call__()调用了forward
  • __call__可以通过类名直接调用函数;
  • 通过内置函数__call__ 把实例化对象当成函数调用.

2.参数管理

访问参数,用于调试、诊断和可视化。
我们可以通过索引来访问模型的任意层。 这就像模型是一个列表一样,每层的参数都在其属性中。 
print(net[2].state_dict())

​ 输出的结果告诉我们一些重要的事情: 首先,这个全连接层包含两个参数,分别是该层的权重和偏置。 两者都存储为单精度浮点数(float32)。 注意,参数名称允许唯一标识每个参数,即使在包含数百个层的网络中也是如此。

参数是复合的对象,包含值、梯度和额外信息。

print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)

# 另一种访问网络参数的方式
net.state_dict()['2.bias'].data

net[2].weight.grad == None
一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])

​ 也可直接打印实例化网络,因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。

print(net)  
参数初始化。
内置初始化

net.apply(*):对网络模块的操作API

def init_normal(m):
    if type(m) == nn.Linear:
    	# 将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为0。
        nn.init.normal_(m.weight, mean=0, std=0.01)
        nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]

# Xavier初始化方法
def init_xavier(m):
    if type(m) == nn.Linear:
        nn.init.xavier_uniform_(m.weight)
def init_42(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 42)

net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
自定义初始化
def my_init(m):
    if type(m) == nn.Linear:
        print("Init", *[(name, param.shape)
                        for name, param in m.named_parameters()][0])
        nn.init.uniform_(m.weight, -10, 10)
        m.weight.data *= m.weight.data.abs() >= 5

net.apply(my_init)
net[0].weight[:2]

​ 当然,我们始终可以直接设置参数。

net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
在不同模型组件间共享参数。
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))


net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])

3.自定义层

​ 与自定义网络的方法相同,通过继承nn.Module来构造类方法,注意定义参数需要调用nn.parameter()方法。

不带参数的层

CenteredLayer类要从其输入中减去均值,只需继承基础层类并实现前向传播功能

import torch
import torch.nn.functional as F
from torch import nn


class CenteredLayer(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, X):
        return X - X.mean()
        
# 调用
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))

带参数的层

​ 实现自定义版本的全连接层。 回想一下,该层需要两个参数,一个用于表示权重,另一个用于表示偏置项。 在此实现中,我们使用修正线性单元作为激活函数。 该层需要输入参数:in_unitsunits,分别表示输入数和输出数。

​ 使用内置函数nn.Parameter()来创建参数,这些函数提供一些基本的管理功能。 比如管理访问、初始化、共享、保存和加载模型参数。 这样做的好处之一是:我们不需要为每个自定义层编写自定义的序列化程序。

class MyLinear(nn.Module):
    def __init__(self, in_units, units):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(in_units, units))
        self.bias = nn.Parameter(torch.randn(units,))
    def forward(self, X):
        linear = torch.matmul(X, self.weight.data) + self.bias.data
        return F.relu(linear)
        
 # 实例化MyLinear类并访问其模型参数       
linear = MyLinear(5, 3)
linear.weight

# 使用自定义层直接执行前向传播计算
linear(torch.rand(2, 5))

#使用自定义层构建模型,就像使用内置的全连接层一样使用自定义层。
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))

小结

  • 我们可以通过基本层类设计自定义层。这允许我们定义灵活的新层,其行为与深度学习框架中的任何现有层不同。
  • 在自定义层定义完成后,我们就可以在任意环境和网络架构中调用该自定义层。
  • 层可以有局部参数,这些参数可以通过内置函数nn.Parameter()创建。

4.读写文件

加载和保存张量

​ 对于单个张量,我们可以直接调用loadsave函数分别读写它们。 这两个函数都要求我们提供一个名称,save要求将要保存的变量作为输入。

x = torch.arange(4)    # tensor([0, 1, 2, 3])
# 存储在文件中
torch.save(x, 'x-file')

# 读回内存。
x2 = torch.load('x-file')
x2

​ 也可以存储一个张量列表,然后把它们读回内存。

y = torch.zeros(4)   
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)

>>>(tensor([0, 1, 2, 3]), tensor([0., 0., 0., 0.]))

​ 甚至可以写入或读取从字符串映射到张量的字典。 当我们要读取或写入模型中的所有权重时,这很方便。

mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2

>>>{'x': tensor([0, 1, 2, 3]), 'y': tensor([0., 0., 0., 0.])}

加载和保存模型参数

​ 深度学习框架提供了内置函数来保存和加载整个网络。 需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。

​ 为了恢复模型,我们需要用代码生成架构, 然后从磁盘加载参数。

# 将模型的参数存储在一个叫做“mlp.params”的文件中。
torch.save(net.state_dict(), 'mlp.params')

#恢复模型时,需要实例化了原始模型的一个备份。
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()

# 验证
Y_clone = clone(X)
Y_clone == Y

小结

  • saveload函数可用于张量对象的文件读写。
  • 我们可以通过参数字典保存和加载网络的全部参数。
  • 保存架构必须在代码中完成,而不是在参数中完成。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值