5 PyTorch构建模型的三种方式

欢迎订阅本专栏:《PyTorch深度学习实践》
订阅地址:https://blog.csdn.net/sinat_33761963/category_9720080.html

  • 第二章:认识Tensor的类型、创建、存储、api等,打好Tensor的基础,是进行PyTorch深度学习实践的重中之重的基础。
  • 第三章:学习PyTorch如何读入各种外部数据
  • 第四章:利用PyTorch从头到尾创建、训练、评估一个模型,理解与熟悉PyTorch实现模型的每个步骤,用到的模块与方法。
  • 第五章:学习如何利用PyTorch提供的3种方法去创建各种模型结构。
  • 第六章:利用PyTorch实现简单与经典的模型全过程:简单二分类、手写字体识别、词向量的实现、自编码器实现。
  • 第七章:利用PyTorch实现复杂模型:翻译机(nlp领域)、生成对抗网络(GAN)、强化学习(RL)、风格迁移(cv领域)。
  • 第八章:PyTorch的其他高级用法:模型在不同框架之间的迁移、可视化、多个GPU并行计算。

pytorch在torch.nn提供了神经网络模型的所有子模块.这些子模块包含了构建神经网络的块,这些块在pytorch中称为module(你可能会在其他深度学习框架中发现被称为layer,其实是一个东东)。

也就是说神经网络完整模型其实是由一个一个module组成的。但我们想构建一个自己的模型的时候,只需要调取对应的module,像搭积木一样搭成自己想要的模型结构即可。

所有的moduel都是一个python类,都是继承自nn.Module这个父类的。一个Module类有两类基本属性:

  • Parameter instatance, 即moduel中有一个或多个参数实例,这些tensor形式的参数是模型的灵魂,在数据上训练模型就是为了估计这些参数的值,从而也就有了模型本身。比如线性回归中的w和b
  • submodule (sub class of nn.Module), 即子模块,这些子模块的组合才成就了整个大模块。

本章结构

本章讲介绍如何利用pytorch提供的模型组件来构造自己想要的模型。分别由三小节:

  • 第一节:介绍利用nn.Module构建单一模型与获取参数
  • 第二节:介绍利用nn.Sequence构建略复杂的需要组合组件的模型于获取参数
  • 第三节:介绍利用nn.Module的subclass构建更复杂的模型与获取参数

5.1 利用nn.Module构建线性回归模型

nn.Module里有一个子模块叫做nn.Linear,顾名思义,它进行的是wb+b的线性计算。

5.1.1 构建模型

还记得第四章我们是如何建立线性模型的吗,我们写了一个函数:

def model(x, w, b):
    return w*x + b

现在我们用nn.Linear去代替这个自己撰写函数

import torch.nn as nn

linear_model = nn.Linear(1,1)

nn.Linear(1,1)构建了一个一元线性回归模型结构实例,第一个参数代表输入的特征数,第二个参数代表输出的个数,因为是一元回归,所有输入是1,输出也是1。同时,已经在该实例内部初始化了模型需要的参数w,b,无需自己创建于初始化,可以如下查看参数:

linear_model.weight
Parameter containing:
tensor([[-0.6442]], requires_grad=True)
linear_model.bias
Parameter containing:
tensor([0.1683], requires_grad=True)
linear_model.parameters()
list(linear_model.parameters())
[Parameter containing:
 tensor([[-0.6442]], requires_grad=True), Parameter containing:
 tensor([0.1683], requires_grad=True)]

5.1.2 单个样本前向计算

构建好了模型,就可以拿去做前向计算了:

import torch

x = torch.tensor([0.5])

# 前向计算
y = linear_model(x)
print(y)

tensor([-0.1538], grad_fn=<ThAddBackward>)

以上,调用模型实例linear_model输入参数x,其实是调用了nn.Linear类中的方法forward(),模型的前向计算过程会全部写在forward方法中。

5.1.3 批量样本前向计算

nn中的所有module都是可以同时输入批量多元的input,并输出批量多元的output的。比如你想一次性处理10个样本,你只需将数据处理成Bn_in大小的tensor一次性喂给模型,它会输出Bn_out的大小的tensor,其中B是批量数据的数量,n_in是输入特征的数目,n_out是输出特征的数目。比如:

# 生成10个样本,输入特征数为1
x = torch.ones(10,1)
print(x)

y = linear_model(x)
print(y)
tensor([[1.],
        [1.],
        [1.],
        [1.],
        [1.],
        [1.],
        [1.],
        [1.],
        [1.],
        [1.]])
tensor([[-0.4758],
        [-0.4758],
        [-0.4758],
        [-0.4758],
        [-0.4758],
        [-0.4758],
        [-0.4758],
        [-0.4758],
        [-0.4758],
        [-0.4758]], grad_fn=<ThAddmmBackward>)

注意B个数据的大小应为B*n_in,是两维的tensor, 还记得第四章的老例子吗,x和y的tensor大小是一维的,需要做如下转换:
.unsqueeze(1)指axis=1处额外增加一个维度

x = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0]
y = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4]
x = torch.tensor(x).unsqueeze(1)
y = torch.tensor(y).unsqueeze(1)

print(x)
print(x.shape)
tensor([[ 0.5000],
        [14.0000],
        [15.0000],
        [28.0000],
        [11.0000],
        [ 8.0000],
        [ 3.0000],
        [-4.0000],
        [ 6.0000],
        [13.0000],
        [21.0000]])
torch.Size([11, 1])

5.1.4 完整训练过程

使用Pytorch提供的module进行一元线性回归模型的构建于训练,完整代码如下:

import torch
import torch.nn as nn
import torch.optim as optim

# 1.准备数据
x = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0]
y = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4]
x = torch.tensor(x).unsqueeze(1)
y = torch.tensor(y).unsqueeze(1)

n_sample = x.shape[0]  # 总样本数
n_val = int(0.2 * n_sample)

shuffled_indices = torch.randperm(n_sample) # 获取随机正整数

train_indices = shuffled_indices[: -n_val] # 训练集的索引
val_indices = shuffled_indices[-n_val:] # 验证集的索引

train_x = 0.1 * x[train_indices]  # 别忘了乘以0.1缩小x规模
train_y = y[train_indices]

val_x = 0.1 * x[val_indices]
val_y = y[val_indices]

# 2.构建模型
linear_model = nn.Linear(1, 1)

# 3.构建优化器
optimizer = optim.SGD(linear_model.parameters(), lr=1e-2)  # 注意直接通过模型实例获取参数,无需自己创建参数

# 4.训练
def training_loop(n_epochs, optimizer, model, loss_fn, train_x, val_x, train_y, val_y):
    for epoch in range(1, n_epochs+1):
        # 训练集的前向
        train_y_p = model(train_x)
        train_loss = loss_fn(train_y_p, train_y)
        
        # 验证集的前向
        val_y_p = model(val_x)
        val_loss = loss_fn(val_y_p, val_y)
        
        optimizer.zero_grad()
        train_loss.backward()
        optimizer.step()
        
        if epoch % 100 == 0:
            print('Epoch {}, Train Loss {}, Val Loss {}'.format(epoch, float(train_loss), float(val_loss)))

training_loop(
    n_epochs = 1000,
    optimizer = optimizer,
    model = linear_model,   # 注意这里
    loss_fn = nn.MSELoss(),  # 注意这里,直接调用pytorch提供的损失类,其实它也是一个子modual
    train_x = train_x,
    val_x = val_x,
    train_y = train_y,
    val_y = val_y
)
                                                                                                                                                                                       
Epoch 100, Train Loss 32.69783020019531, Val Loss 59.25724792480469
Epoch 200, Train Loss 15.095704078674316, Val Loss 35.80534362792969
Epoch 300, Train Loss 10.374916076660156, Val Loss 25.756357192993164
Epoch 400, Train Loss 9.098362922668457, Val Loss 21.192996978759766
Epoch 500, Train Loss 8.753169059753418, Val Loss 18.99950408935547
Epoch 600, Train Loss 8.659825325012207, Val Loss 17.907453536987305
Epoch 700, Train Loss 8.634584426879883, Val Loss 17.35272216796875
Epoch 800, Train Loss 8.62775993347168, Val Loss 17.06779670715332
Epoch 900, Train Loss 8.62590503692627, Val Loss 16.920541763305664
Epoch 1000, Train Loss 8.625411033630371, Val Loss 16.844257354736328

上面的过程,和4.4中是一样的结果,区别就在于,我们用pytorch提供的module代替了手工写的模型(nn.Linear)与损失函数(nn.MSELoss()),也无需自己创建于初始化参数,在创建模型实例的时候会自动创建,只需module.parameters就能获取参数。

module.parameters是开启了requries_grad=True的,因此当loss.backwards的时候,会对parameters计算梯度,并在optimizer.step时累加更新梯度。

到目前为止,我们已经学会了如何用Pytorch去创建模型,构建优化器与损失函数,并进行迭代训练。虽然只是用了最简单的线性回归,写来写去都是这么二十几行代码,但是这确是在授人以渔。接下去你可以将线性模型替换成其他复杂的模型,可以将SGD换成其他更优秀的优化器,将MSEloss换成其他损失函数,去自定义创造自己的项目,钓自己的鱼。

至于pytorch提供了什么submodule, optimizer, loss function,以及其他各种功能,可以直接去官网文档中一探究竟。

5.2 利用nn.Sequence组合子模块

5.1节中构建了一个一元线性回归模型,那是一个极其简单的模块,只需要直接调用nn.Linear类即可。

但是实际上我们往往需要去构建更复杂一点的模型,这就需要我们调用不同的子模块然后将它们拼接起来组成大模块。比如先经过一个线性函数,再经过tanh激活函数,再经过一个线性函数,这是三个步骤的组合,前者的输出事后者的输入。

如何组合子模块成一个完整的模型,可以用nn.Sequence。

5.2.1 直接创建

import torch.nn as nn

seq_model = nn.Sequential(
    nn.Linear(1,13),
    nn.Tanh(),
    nn.Linear(13,1)
)

seq_model
Sequential(
  (0): Linear(in_features=1, out_features=13, bias=True)
  (1): Tanh()
  (2): Linear(in_features=13, out_features=1, bias=True)
)
  • 代码的解释:
    这样,我们创建了一个模型,里面有三个子模块,分别是Linean,tanh,Linear,前者的输出是后者的出入,因此注意维度的对应,第一个Linear是输入1输出13,tanh不改变维度,第二个Linear的输入需要与第一个Linear的输出保持一致。

  • 打印信息的解释
    括号中数字的是每个子模块的名字,按照其再过程中的顺序默认以标号来取名。标号后面是每个子模块,以及其输入输出的维度。

5.2.2 带名字创建

如果不想用默认的标号作为子模块的名字,可以用OrderedDict的字典模式创建,并取上自己的想要的名字,以方便后续调用子模块。

from collections import OrderedDict

seq_model = nn.Sequential(OrderedDict([
    ('hidden_linear',nn.Linear(1,13)),
    ('hidden_activation', nn.Tanh()),
    ('output_linear', nn.Linear(13,1))
    
]))

seq_model
Sequential(
  (hidden_linear): Linear(in_features=1, out_features=13, bias=True)
  (hidden_activation): Tanh()
  (output_linear): Linear(in_features=13, out_features=1, bias=True)
)

5.2.3 获取参数

(1)model.parameters

model.parameters是一个generator,需要循环打印,其中依次包含了从前至后的计算中涉及的参数,如下分别是hidden_linear中的w,b和output_linear中的w,b的shape

[param.shape for param in seq_model.parameters()]
[torch.Size([13, 1]), torch.Size([13]), torch.Size([1, 13]), torch.Size([1])]

(2)model.named_parameters

直接用model.parameters打印出来的信息要按照顺序去数着对应,很不方便,用named_parameters可以打印出参数对应子模块的名字,很是便利

for name, param in seq_model.named_parameters():
    print(name, param.shape)
hidden_linear.weight torch.Size([13, 1])
hidden_linear.bias torch.Size([13])
output_linear.weight torch.Size([1, 13])
output_linear.bias torch.Size([1])

(3)model.submodule.weight

除了以上方法,可以直接调用子模块的参数,如:

seq_model.output_linear.bias
Parameter containing:
tensor([0.0821], requires_grad=True)

同理,也可以同过这种方式获取参数对应的梯度

print(seq_model.output_linear.weight.grad)
None

5.3 利用nn.Module subcalssg构建复杂模型

5.1节讲了直接调用nn.Module构建简单的模型,5.2节将了用nn.Sequencce来组合简单的子模块构建稍微复杂一点的模型。但是当需要构建的模型非常复杂的时候(实际业务中往往需要复杂模型),就需要用到另一个构建模型的方式:自定义nn.Module的子类来构建模型。

5.3.1 创建模型

为了方便大家理解,这里还是用5.2节的模型结构来举例,代码如下

import torch.nn as nn

class SubclassModel(nn.Module):
    def __init__(self):
        super().__init__()
        
        self.hidden_linear = nn.Linear(1,13)
        self.hidden_activation = nn.Tanh()
        self.output_linear = nn.Linear(13,1)
        
    def forward(self, input):
        hidden = self.hidden_linear(input)
        activation = self.hidden_activation(hidden)
        output = self.output_linear(activation)
        
        return output
    
sub_model = SubclassModel()
print(sub_model)
SubclassModel(
  (hidden_linear): Linear(in_features=1, out_features=13, bias=True)
  (hidden_activation): Tanh()
  (output_linear): Linear(in_features=13, out_features=1, bias=True)
)

撰写一个正确的nn.Module的子类:

  • 子类一定要继承nn.Module父类
  • 在__init__中创建模型需要的各个子模块
  • 需要一个forward()方法,当外部调用SubclassModel实例的时候,实际上是在运行forward函数
  • forward()方法中是根据需要的顺序调用在__init__中创建好的各组件,最后返回最终结果

从模型的打印结果来看,它和5.2节用nn.sequence创建的模型是一模一样哒~

5.3.2 获取参数

同理,可以用之前的方式来获取模型的参数与梯度。

for name, param in sub_model.named_parameters():
    print(name, param.shape)
hidden_linear.weight torch.Size([13, 1])
hidden_linear.bias torch.Size([13])
output_linear.weight torch.Size([1, 13])
output_linear.bias torch.Size([1])

为什么可以通过这种形式获取参数呢,因为在__init__()中创建子模块时已经对这些模块进行了注册,所以可以在外面用model.parameters()可以调用这些子模块的参数。那么是不是说如果这个子模块根本不产生参数,其实就没有必要再初始化的时候注册了,直接在forward中调用即可。

如激活函数tanh,不需要参数,可以如下改造:

import torch

class SubclassModel(nn.Module):
    def __init__(self):
        super().__init__()
        
        self.hidden_linear = nn.Linear(1,13)
        
        self.output_linear = nn.Linear(13,1)
        
    def forward(self, input):
        hidden = self.hidden_linear(input)
        activation = torch.tanh(hidden)
        output = self.output_linear(activation)
        
        return output
    
sub_model = SubclassModel()
print(sub_model)
SubclassModel(
  (hidden_linear): Linear(in_features=1, out_features=13, bias=True)
  (output_linear): Linear(in_features=13, out_features=1, bias=True)
)

此时,激活函数并不是模型组件中的一员了。

虽然这些激活函数在pytorch1.0中仍然存在于nn.function中,但是之后会渐渐被移除,而旨在torch包中被调用。

©️2020 CSDN 皮肤主题: 终极编程指南 设计师:CSDN官方博客 返回首页