【技术】PyTorch从入门到入土(一)

Dymay
Dymay
发布于 2023-02-25 / 199 阅读
0
0

【技术】PyTorch从入门到入土(一)

1 构建第一个神经网络

一般流程:

  • 设计网络结构
    • 确定各网络层
    • 前向传播过程
  • 设计损失函数
  • 选用优化算法
  • 制作数据集
  • 训练
    • 前向传播
    • 计算损失
    • 后向传播更新参数
  • 保存模型
  • 使用模型

下面是一个简单的有监督神经网络的构建、训练、保存和加载过程的示例,主要关注流程即可:

import torch 
import torch.nn as nn

# 设定训练超参数
input_size = 5 
hidden_size = 10 
num_classes = 3 
num_epochs = 10
learning_rate = 0.001

# 创建一个神经网络模型 
class NeuralNet(nn.Module): 
    def __init__(self, input_size, hidden_size, num_classes): 
        super(NeuralNet, self).__init__() 
        self.fc1 = nn.Linear(input_size, hidden_size)  
        self.relu = nn.ReLU() 
        self.fc2 = nn.Linear(hidden_size, num_classes)  

    def forward(self, x): 
        out = self.fc1(x) 
        out = self.relu(out) 
        out = self.fc2(out)  
        return out 

# 实例化一个神经网络模型
model = NeuralNet(input_size, hidden_size, num_classes)

# 损失函数和优化器 
criterion = nn.CrossEntropyLoss() 
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

# 生成样本数据
x_train = torch.randn(100, input_size) 
y_train = torch.randint(0, num_classes, (100,))

# 训练神经网络模型
for epoch in range(num_epochs): 
   # 前向传播 
    outputs = model(x_train) 
    
   # 计算每轮的损失函数并打印
    loss = criterion(outputs, y_train) 
    print('epoch',epoch+1,'  loss:',float(loss))
    
   # 后向传播和优化参数 
    optimizer.zero_grad() 
    loss.backward() 
    optimizer.step()
    
# 保存训练好的模型
torch.save(model,'model.pth') #保存整个模型
# torch.save(model.state_dict(), 'model.ckpt') 保存checkpoints

model_loaded = torch.load('model.pth')
model_loaded.eval() #调整为评估模式
# model_loaded = NeuralNet(input_size, hidden_size, num_classes) 读取checkpoints时还需要实例化网络或者使用原来的网络
# model_loaded.load_state_dict(torch.load('model.ckpt'))
# model_loaded.eval()

# 测试保存的模型是否一致
test_input = torch.tensor([4,2,3,5,1],dtype=torch.float32)
print('trained_net ouput:',model(test_input))
print('loaded_net ouput:',model_loaded(test_input))

输出:

epoch 1   loss: 1.1384679079055786
epoch 2   loss: 1.1370964050292969
epoch 3   loss: 1.1357471942901611
epoch 4   loss: 1.1344249248504639
epoch 5   loss: 1.1331151723861694
epoch 6   loss: 1.1318265199661255
epoch 7   loss: 1.1305594444274902
epoch 8   loss: 1.1293138265609741
epoch 9   loss: 1.1280888319015503
epoch 10   loss: 1.1268807649612427
trained_net ouput: tensor([-0.4656,  0.0194, -0.2546], grad_fn=<AddBackward0>)
loaded_net ouput: tensor([-0.4656,  0.0194, -0.2546], grad_fn=<AddBackward0>)

你已经掌握神经网络了!快开始炼丹吧!

2 从网络结构开始

参考:详解PyTorch中的ModuleList和Sequential - 知乎 (zhihu.com)

简单而言 PyTorch 构建网络会用容器 (Containers) 中两个比较常用的类:ModuleList 和 Sequential(虽然在上面的简单例子还没有)

ModuleList 是一个储存不同 module,并自动将每个 module 的 parameters 添加到网络之中的容器。你可以把任意 nn.Module 的子类 (比如 nn.Conv2d, nn.Linear 之类的) 加到这个 list 里面,方法和 Python 自带的 list 一样,无非是 extend,append 等操作。但不同于一般的 list,加入到 nn.ModuleList 里面的 module 是会自动注册到整个网络上的,同时 module 的 parameters 也会自动添加到整个网络中。

Sequential也很类似,它里面的模块按照顺序进行排列的。必须确保前一个模块的输出大小和下一个模块的输入大小是一致的,这也意味着前向过程实际上在给出结构后确定了,内部实现了forward。

主要记住几点不同:

  • nn.Sequential 内部实现了 forward 函数,因此可以不用写 forward 函数。而n n.ModuleList 则没有实现内部forward函数。如果完全直接用 nn.Sequential,确实是可以的,但这么做的代价就是失去了部分灵活性,不能自己去定制 forward 函数里面的内容了。一般情况下 nn.Sequential 的用法是来组成卷积块 (block),然后像拼积木一样把不同的 block 拼成整个网络,让代码更简洁,更加结构化。

    class net_seq(nn.Module):
        def __init__(self):
            super(net2, self).__init__()
            self.seq = nn.Sequential(
                            nn.Conv2d(1,20,5),
                            nn.ReLU(),
                            nn.Conv2d(20,64,5),
                            nn.ReLU()
                           )      
        def forward(self, x):
            return self.seq(x) # forward已经在Sequential内部实现
    
  • nn.Sequential 可以使用 OrderedDict 对每层进行命名:

    class net_seq(nn.Module):
        def __init__(self):
            super(net_seq, self).__init__()
            self.seq = nn.Sequential(OrderedDict([
                            ('conv1', nn.Conv2d(1,20,5)),
                            ('relu1', nn.ReLU()),
                            ('conv2', nn.Conv2d(20,64,5)),
                            ('relu2', nn.ReLU())
                           ]))
        def forward(self, x):
            return self.seq(x)
    
  • nn.Sequential 里面的模块按照顺序进行排列的,所以必须确保前一个模块的输出大小和下一个模块的输入大小是一致的。而 nn.ModuleList 并没有定义一个网络,它只是将不同的模块储存在一起,这些模块之间并没有什么先后顺序可言。个 ModuleList 里面的顺序不能决定什么,网络的执行顺序是根据 forward 函数来决定的。若将forward函数中几行代码互换,使输入输出之间的大小不一致,则程序会报错。此外,为了使代码具有更高的可读性,最好把 ModuleList 和 forward中的顺序保持一致。

    class net3(nn.Module):
        def __init__(self):
            super(net3, self).__init__()
            self.linears = nn.ModuleList([nn.Linear(10,20), nn.Linear(20,30), nn.Linear(5,10)])
        def forward(self, x):
            x = self.linears[2](x)
            x = self.linears[0](x)
            x = self.linears[1](x)
    
            return x
    
    net3 = net3()
    print(net3)
    #net3(
    #  (linears): ModuleList(
    #    (0): Linear(in_features=10, out_features=20, bias=True)
    #    (1): Linear(in_features=20, out_features=30, bias=True)
    #    (2): Linear(in_features=5, out_features=10, bias=True)
    #  )
    #)
    
  • 有的时候网络中有很多相似或者重复的层,我们一般会考虑用 for 循环来创建它,就会使用ModuleList:

    class net4(nn.Module):
        def __init__(self):
            super(net4, self).__init__()
            layers = [nn.Linear(10, 10) for i in range(5)]
            self.linears = nn.ModuleList(layers)
    
        def forward(self, x):
            for layer in self.linears:
                x = layer(x)
            return x
    
  • nn.Xxx继承于nn.Module, 能够很好的与nn.Sequential结合使用, 而nn.functional.xxx无法与nn.Sequential结合使用。(具体nn 与 nn.functional 的区别参考这个回答

    class net4(nn.Module):
        def __init__(self):
            super(net4, self).__init__()
            layers = [nn.Linear(10, 10) for i in range(5)]
            self.linears = nn.ModuleList(layers)
    
        def forward(self, x):
            for layer in self.linears:
                x = layer(x)
            return x
    

3 损失函数和优化

应用神经网络无非最终也是解决一个最优化的问题:

minL(f(x,θ),y)\min L\left(f(\boldsymbol{x},\theta),\boldsymbol{y}\right)

自然就涉及两个问题:

  • LL 这个Loss function 怎么选
  • 怎么最小化

这个数学上讲清楚显然复杂了去了,各种不同的实际问题下也不尽相同,这里主要关注代码实现,就简单谈谈常用的损失函数和优化器。

3.1 损失函数

参考:深入浅出PyTorch

对于损失函数,简单的可以直接使用 nn 模块里自带的一些(就如同第一个例子中的交叉熵损失函数),更多时候我们需要自定义损失函数。我们当然可以简单地定义一个函数,如均方误差定义为:

def my_loss(output, target):
    loss = torch.mean((output - target)**2)
    return loss

但更常用的是以类方式定义,在以类方式定义损失函数时,我们如果看每一个损失函数的继承关系我们就可以发现Loss函数部分继承自_loss, 部分继承自_WeightedLoss, 而_WeightedLoss继承自_loss _loss继承自 nn.Module。我们可以将其当作神经网络的一层来对待,同样地,我们的损失函数类就需要继承自 nn.Module 类。比如参考文章中 Dice Loss 的例子,Dice Loss 是一种在分割领域常见的损失函数,定义如下:

DSC=2XYX+YD S C=\frac{2|X \cap Y|}{|X|+|Y|}

import torch.nn.functional  as F

class DiceLoss(nn.Module):
    def __init__(self,weight=None,size_average=True):
        super(DiceLoss,self).__init__()
        
    def forward(self,inputs,targets,smooth=1):
        inputs = F.sigmoid(inputs)       
        inputs = inputs.view(-1)
        targets = targets.view(-1)
        intersection = (inputs * targets).sum()                   
        dice = (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)  
        return 1 - dice

# 使用方法    
criterion = DiceLoss()
loss = criterion(input,targets)

3.2 优化器

优化器同样可以采用torch.optim下自带的各类优化器,也可以自定义。在 PyTorch 中自定义优化器需要继承基类 torch.optim.Optimizer,然后实现其step方法即可。

参考:pytorch 自定义损失函数、优化器(Optimizer)和学习率策略_51CTO博客_pytorch的optimizer

Optimizer的初始化方法需要两个必填参数:

  • params:这个就是模型参数,严格来说应该是要进行更新的参数。Optimizer会将其放在param_groups这个变量下。
  • defaults:这个是模型的一些默认配置。传空字典也没关系。
class Optimizer(object):
    
    def __init__(self, params, defaults):
        ...
        param_groups = list(params) 
        ...
        if not isinstance(param_groups[0], dict):
            param_groups = [{'params': param_groups}]
        ...
    
    def step(self, closure):
        raise

Optimizer最重要的是step方法,这个是用户需要进行参数更新的地方,用户需要自己实现该方法,通常是从param_groups拿出模型参数,然后进行更新。

参考文章中给出了一个自定义优化器设置,和SGD类似,但是每次学习率都乘以一个[0-1]的随机数。代码如下:

class MyOptimizer(Optimizer):

    def __init__(self, params, lr):
        self.lr = lr
        super(MyOptimizer, self).__init__(params, {})


    def step(self, closure=False):    # 这个closure我也不知道干啥的,应该不重要
        random_num = 0.4 # 假设生成的随机数为0.4

        for param_group in self.param_groups:
            params = param_group['params']
            # 从param_group中拿出参数
            for param in params:
                # 循环更新每一个参数的值
                param.data = param.data - self.lr * random_num * param.grad

使用方法:

optimizer = MyOptimizer([model.theta, model.b], lr=2)
optimizer.step()
print("theta:", model.theta)
print("b:", model.b)

3.3 学习策略

要定义学习策略,其实最好的方式就是把他集成在Optimizer中,例如torch.optim.Adagrad就是这么做的。但很多时候我们并不想改变原有的复杂算法,只是想对他的学习率动一下手脚,此时就用到了自定义学习策略。


评论