【动手学深度学习PyTorch版】10 PyTorch 神经网络基础

上一篇移步【动手学深度学习PyTorch版】9 Kaggle房价预测_水w的博客-CSDN博客

目录

一、PyTorch 神经网络基础

1.1 层和块

◼ 层和块

◼ 自定义块

◼ 顺序块

◼ 正向传播

◼ 混合组合块

1.2 参数管理

◼ 参数访问

◼ 嵌套块

◼ 内置初始化

◼ 参数替换

◼ 参数绑定

1.3 自定义层

◼ 自定义层

1.4 读写文件

◼ 加载和保存张量

◼  加载和保存模型参数


一、PyTorch 神经网络基础

1.1 层和块

◼ 层和块

首先我们来回顾一下多层感知机,其中:

随机生成input:x是一个2x20的随机矩阵,2是批量大小,20是输入维度,输出大小为10。

# 回顾一下多层感知机
import torch
from torch import nn
from torch.nn import functional as F
# 构造简单的单层神经网络:线性层,relu,线性层
net = nn.Sequential(nn.Linear(20,256),nn.ReLU(),nn.Linear(256,10))
X = torch.rand(2,20)    # x是一个2x20的随机矩阵,2是批量大小,20是输入维度,输出大小为10
net(X)

nn.Sequential 定义了一种特殊的Module。使用在pyTorch里面,Module是一个很重要的概念。

所以我们来看一下Module。

◼ 自定义块

任何一个层或者任何一个网络,应该都是Module的子类。

我们自定义一个MLP去实现我们刚使用的函数,那我们应该怎么做呢?

定义一个MLP类,它是Module的子类:

  • 它的__init__函数:调用父类的__init__函数,接下来定义两个全连接层,分别存储在类的成员变量里;
  • 它的前向函数forward(self, X):输入参数X,

其中, 在前向函数forward(self, X),我们先把输入X放入hidden层里,也就是第一层,得到隐藏层的输出。然后调用functional(F)里实现好的relu()函数,做了激活之后,就放入到输出out里面,最后返回出来。

使用这个MLP类的时候,我们需要实例化MLP类进行调用。

class MLP(nn.Module):
    def __init__(self):
        super().__init__()  # 调用父类的__init__函数
        # 定义两个全连接层,存储在类的成员变量里
        self.hidden = nn.Linear(20,256)
        self.out = nn.Linear(256,10)
        
    def forward(self, X):
        return self.out(F.relu(self.hidden(X)))
    
# 实例化多层感知机的层,然后在每次调用正向传播函数调用这些层
net = MLP()
X = torch.rand(2,20)
net(X)

◼ 顺序块

用同样的方法来实现之前我们使用过的nn.Sequential这个类,几乎一样的功能。

定义一MySequential类,它是Module的子类:

  • 它的__init__函数:调用父类的__init__函数,对于传进来的这些layer层,block 本身作为它的key,按顺序存在_modules里面的,以字典的形式;
  • 它的前向函数forward(self, X):输入参数X,对于所有放进来的排好序的这些layer层,按顺序放进去,一层一层调用,返回我们所有的类,得到output;
class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for block in args:
            self._modules[block] = block # block 本身作为它的key,存在_modules里面的为层,以字典的形式
            
    def forward(self, X):
        for block in self._modules.values():
            print(block)
            X = block(X)
        return X
    
net = MySequential(nn.Linear(20,256),nn.ReLU(),nn.Linear(256,10))
X = torch.rand(2,20)
net(X)

◼ 正向传播

我们可以在__init__函数和forward(self, X)函数中,做大量的自定义的计算,可以做比较灵活的方法。

# 在正向传播函数中执行代码
class FixedHiddenMLP(nn.Module):
    def __init__(self):
        super().__init__()
        # 用torch.rand直接生成随机的rand_weight,不参与训练,不会计算梯度
        self.rand_weight = torch.rand((20,20),requires_grad=False)
        self.linear = nn.Linear(20,20)
    
    def forward(self, X):
        X = self.linear(X)    # 线性
        X = F.relu(torch.mm(X, self.rand_weight + 1))    # 把rand_weight和X做矩阵乘法+1,做激活函数
        X = self.linear(X)    # 线性
        while X.abs().sum() > 1:    # 当绝对值求和大于1时,不断除以2
            X /= 2
        return X.sum()    # 返回标量
    
net = FixedHiddenMLP()
X = torch.rand(2,20)
net(X)

可以看到,返回的是一个标量。

◼ 混合组合块

我们可以很灵活的嵌套使用,混合搭配各自组合块的方法:

# 混合搭配各种组合块的方法
class NestMLP(nn.Module):
    def __init__(self):
        """是一个Sequential类和单独的linear类的嵌套"""
        super().__init__()
        # net是一个Sequential类
        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))
    
chimear = nn.Sequential(NestMLP(),nn.Linear(16,20),FixedHiddenMLP())
X = torch.rand(2,20)
chimear(X)

 

1.2 参数管理

◼ 参数访问

我们首先关注最简单的具有一个单隐藏层的MLP,

# 首先关注具有单隐藏层的多层感知机
import torch
from torch import nn

# # 构造简单的单层神经网络:线性层,relu,线性层
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))
X = torch.rand(size=(2,4))
print(net(X))

进行参数访问,我们可以直接访问某一个具体的参数,也可以拿出整个网络全部的参数。

"""1.参数访问:
net[0]拿到的是nn.Linear(4,8),那么net[1]是nn.ReLU(),而net[2]拿到的就是最后一个输出层nn.Linear(8,1)"""
print(net[2].state_dict()) # 访问参数,net[2]最后一个输出层里的参数

"""2.我们也可以直接访问某一个具体的参数"""
print(type(net[2].bias)) # 目标参数:net[2]输出层里的偏差是一个parameter
print(net[2].bias) # net[2]输出层里的偏差里面有有一个tensor
print(net[2].bias.data) # 这个tensor可以通过.data来访问参数本身的值,可以通过.grad来访问参数的梯度
print(net[2].weight.grad == None) # 这里因为我们还没进行反向计算,所以grad为None

"""3.我们也可以拿出整个网络全部的参数"""
print(*[(name, param.shape) for name, param in net[0].named_parameters()])  # 一次性访问net[0]第一层的所有参数         
print(*[(name, param.shape) for name, param in net.named_parameters()])  # 拿出整个网络全部的参数,其中的0.weight表示:0是第一层名字,weight是参数名,1是ReLU,它没有参数

"""4.通过名字获取参数"""
print(net.state_dict()['2.bias'].data) # 通过名字获取参数

 

◼ 嵌套块

当网络有嵌套时,我们这样拿参数值,

# 从嵌套块收集参数
def block1():
    """一个Sequential:包括线性层+relu+线性层"""
    return nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,4),nn.ReLU())

def block2():
    """一个Sequential:嵌套4个block1"""
    net = nn.Sequential()
    for i in range(4):
        net.add_module(f'block{i}',block1()) # f'block{i}' 可以传一个字符串名字过来,block2可以嵌套四个block1                                      
    return net

rgnet = nn.Sequential(block2(), nn.Linear(4,1))
print(rgnet(X))
print(rgnet)

我们已经设计了网络,让我们看看它是如何组织的。

我们可以通过print大致了解我们的网络是什么样的。

从图中可以看到,

  • 首先外层是一个Sequential:
    • (0):第0元素是我们的block2,也是一个Sequential,
      • block 0 :block 0是我们传进去的字符串,包括线性层+relu+线性层,
      • block 1:block 0是我们传进去的字符串,包括线性层+relu+线性层,
      • block 2 :block 0是我们传进去的字符串,包括线性层+relu+线性层,
      • block 3 :block 0是我们传进去的字符串,包括线性层+relu+线性层,

◼ 内置初始化

我们之前谈论的都是怎么样去访问我们的参数,那么接下来我们谈论怎么样初始参数?

net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))

def init_normal(m):
    """初始化参数"""
    if type(m) == nn.Linear: # 如果Module是一个线性类,全连接层
        nn.init.normal_(m.weight, mean=0, std=0.01) # 下划线表示把m.weight的值替换成符合正态分布的向量   
        nn.init.zeros_(m.bias) # bias赋值为0
        
net.apply(init_normal) # 对net里的Module去调用这个函数,嵌套遍历一遍,属于递归调用,直到所有层都初始化
print(net[0].weight.data[0])
print(net[0].bias.data[0])

 我们也可以将weight初始为一个constant为1的值,

net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))

def init_constant(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight,1)  # 将weight初始为一个constant为1的值
        nn.init.zeros_(m.bias)
        
net.apply(init_constant)
print(net[0].weight.data[0]) 
print(net[0].bias.data[0])

 对某些块应用不同的初始化,我们可以对任何一个层调用不同的初始化函数,

# 对某些块应用不同的初始化
def xavier(m):
    if type(m) == nn.Linear:
        nn.init.xavier_uniform_(m.weight)  # 对weight做xavier初始化
        
def init_42(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 42) # 将weight初始为一个constant为42的值
        
net[0].apply(xavier)  # 第一个线性全连接层用xavier初始化
net[2].apply(init_42)  # 第一个线性全连接层用init_42初始化
print(net[0].weight.data[0]))  # 第一个线性全连接层的参数就是一个xavier的均匀分布
print(net[2].weight.data)  # 第一个线性全连接层的参数weight就是一个constant为42的值

 

◼ 参数替换

自定义一个函数,

  • 首先打印名字是啥,形状是啥 ;
  • 然后均匀初始化;
  • 保留绝对值大于5的那些权重,不是的话就设为0;
# 自定义初始化
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 # 这里*=的代码相当于先计算一个布尔矩阵(先判断>=),然后再用布尔矩阵的对应元素去乘以原始矩阵的每个元素。保留绝对值大于5的权重,不是的话就设为0

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

net[0].weight.data[:] += 1 # 直接把参数拿出来做替换
net[0].weight.data[0,0] = 42
print(net[0].weight.data[0])

 

◼ 参数绑定

比如是有两个数据流进来,我们想在其中一些层直接share共享一些参数,比如权重。

那么我们需要做参数绑定:

  • 首先把share的这个层先构造出来;
  • 定义Sequential:
    • 其中的第2个隐藏层和第3个隐藏层是share权重的,改了一层另外一层也会变;
    • 第一个和第四个是自己的;
"""参数绑定"""

# 首先把shared的这个层先构造出来
shared = nn.Linear(8,8)
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),shared,nn.ReLU(),shared,nn.ReLU(),nn.Linear(8,1))  # 第2个隐藏层和第3个隐藏层是share权重的,第一个和第四个是自己的  
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])

 

1.3 自定义层

◼ 自定义层

自定义层与自定义网络其实没有本质区别,因为自定义层也是一个nn.Module的子类。

接下来,我们构造一个没有任何参数的自定义层,

# 构造一个没有任何参数的自定义层
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()  # X减去自己的均值,使得均值变为0
    

layer = CenteredLayer()  # 实例化
print(layer(torch.FloatTensor([1,2,3,4,5])))  # 输入tensor,放进去

将层作为组件合并到构建更复杂的模型中,

# 将层作为组件合并到构建更复杂的模型中
net = nn.Sequential(nn.Linear(8,128),CenteredLayer())
Y = net(torch.rand(4,8))
print(Y.mean())

定义一个自带有参数的层,其中nn.Parameter使得这些参数加上了梯度。

# 带参数的图层
class MyLinear(nn.Module):
    def __init__(self, in_units, units):  # 输入维度in_units,输出维度units
        super().__init__()
        self.weight = nn.Parameter(torch.randn(in_units,units))   # 随机化一个输入维度乘以输出维度大小的矩阵,作为weight 
        # 将随机化好的参数放进nn.Parameter,使得这些参数加上了梯度    
        self.bias = nn.Parameter(torch.randn(units,))    # 随机化bais

    def forward(self, X):
        # 拿到weight的值,与X做矩阵乘法, 加偏移bais值
        linear = torch.matmul(X, self.weight.data) + self.bias.data
        return F.relu(linear)
    
dense = MyLinear(5,3)
print(dense.weight)


# 使用自定义层直接执行正向传播计算
print(dense(torch.rand(2,5)))
# 使用自定义层构建模型
net = nn.Sequential(MyLinear(64,8),MyLinear(8,1))
print(net(torch.rand(2,64)))

1.4 读写文件

◼ 加载和保存张量

# 加载和保存张量
import torch
from torch import nn
from torch.nn import functional as F

x = torch.arange(4)  # 构造长为4的向量X
torch.save(x, 'x-file')  # 存在x-file文件中
x2 = torch.load("x-file")  # 加载
print(x2)


#存储一个张量列表,然后把它们读回内存
y = torch.zeros(4)
torch.save([x,y],'x-files')
x2, y2 = torch.load('x-files')
print(x2)
print(y2)


# 写入或读取从字符串映射到张量的字典
mydict = {'x':x,'y':y}
torch.save(mydict,'mydict')
mydict2 = torch.load('mydict')
print(mydict2)

◼  加载和保存模型参数

对于一个神经网络,我要存什么东西?

我们不方便把整个网络的模型定义存储下来,所以需要存储模型参数权重,计算那部分就不用存了。

将模型的参数存储为一个叫做"mlp.params"的文件。

# 加载和保存模型参数
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(20,256)
        self.output = nn.Linear(256,10)
    
    def forward(self, x):
        return self.output(F.relu(self.hidden(x)))
    
net = MLP()
X = torch.randn(size=(2,20))
Y = net(X)

# 将模型的参数存储为一个叫做"mlp.params"的文件,以字典形式
torch.save(net.state_dict(),'mlp.params')


# 实例化了原始多层感知机模型的一个备份。直接读取文件中存储的参数
clone = MLP()  # 必须要先声明一下,才能导入参数。这里面的参数其实已经被随机初始化了,但是我们不用管,
clone.load_state_dict(torch.load("mlp.params"))  # load读取出文件中的参数字典,覆盖掉随机初始化的参数
print(clone.eval()) # eval()是进入测试模式

Y_clone = clone(X)
print(Y_clone == Y)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值