Bug记录——nn.Parameter()参数不更新、根据loss自学习权重变量不更新、pytorch 模型自定义参数不更新、网络梯度为None,参数不更新解、tensor参数有梯度,但不更新

系列文章目录

PyTorch学习——关于tensor、Variable、nn.Parameter()、叶子节点、非叶子节点、detach()函数、查看网络层参数
pytorch优化器——add_param_group()介绍及示例、Yolov7 优化器代码示例


Bug关键词:pytorch 模型自定义参数不更新、网络梯度为None,参数不更新解、tensor参数有梯度,但不更新、nn.Parameter()参数不更新。

1、Bug介绍

记录一个bug。
bug描述:这是关于一个Pytorch模型自定义nn.Parameter()参数不更新的bug。
就是用nn.Parameter()定义一个参数变量,它可以根据loss调整大小。

2、Bug复现、解决bug的心路历程

程序非常简单,但我发现定义的权重变量(self.w)并没有变化。
为了复现bug,这里我用了resnet18网络跑数字手写体mini数据集,网络模型如下:

class resnet18(torch.nn.Module):
    def __init__(self):
        super(resnet18, self).__init__()
        self.block1 = torch.nn.Sequential(
            torch.nn.Conv2d(1, 10, 5),
            torch.nn.MaxPool2d(2),
            torch.nn.ReLU(True),
            torch.nn.BatchNorm2d(10),
        )
        self.block2 = torch.nn.Sequential(
            torch.nn.Conv2d(10, 20, 5),
            torch.nn.MaxPool2d(2),
            torch.nn.ReLU(True),
            torch.nn.BatchNorm2d(20),
        )
        self.fc = torch.nn.Sequential(
            torch.nn.Flatten(),
            torch.nn.Linear(320, 10)
        )
        self.w = torch.nn.Parameter(torch.ones(2)) # 定义自学习参数
        
    def forward(self, x):
        x = self.block1(x)*self.w[0]
        x = self.block2(x)*self.w[1]
        x = self.fc(x)
        return x

输出结果如下:
在这里插入图片描述
从上面的程序看逻辑上没有问题。程序正常运行,就是self.w没有变化。在网上查资料很多人说的是定义的变量不是叶子节点、或者没有求梯度、没有加入网络层等原因。我把这些不确定的原因都通过一些输出函数打印了出来。
结果
是否是叶子节点:发现self.w在初始化的前两个输出是叶子节点,但后面就不是叶子节点了。
梯度:显卡单卡是可以求出梯度的,但梯度很小(例如0.0001),多卡运行梯度输出为None。

这里问题就出现了,我自己nn.Parameter()定义的变量正常来说跟网络层的权重一样是叶子节点,而且默认求梯度,为什么输出是非叶子节点且梯度为None。然后我开是蒙了。

关于叶子节点和梯度的介绍可以看这个:PyTorch学习——关于tensor、Variable、nn.Parameter()、叶子节点、非叶子节点、detach()函数、查看网络层参数

之后我想是不是梯度太小加学习率太小。导致为0了。然后开始向学习率优化器方向入手。结果bug就是出现在了这里。想到这里就差不多知道bug出现的原因了,但并不是学习率太小。而是:
bug:自己定义的self.w参数并没有加入到优化器中迭代。 很多网络是直接对全局定义,那么自己定义的self.w参数自然加入了优化器。比如这样:

optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.5)

而我用的是yolo网络,优化器函数如下:
self.w参数没有加入了优化器的迭代行列自然不会根据损失调整大小。

#---------------------------------------------------------------------# 
# 构造损失函数和优化函数
# 损失
criterion = torch.nn.CrossEntropyLoss()
pg0, pg1, pg2 ,pg3= [], [], [], [] 
for name, p in model.named_modules():
    if hasattr(p, "bias") and isinstance(p.bias, nn.Parameter):  # 把带有bias属性且性质为nn.Parameter的层选出来 添加到到pg2列表
        pg2.append(p.bias) 
    if isinstance(p, nn.BatchNorm2d) or "bn" in name: # 把标准化层选出来 添加到到pg0列表
        pg0.append(p.weight)
    elif hasattr(p, "weight") and isinstance(p.weight, nn.Parameter):  # 把带有weight属性且性质为nn.Parameter的层选出来 添加到到pg1列表
        pg1.append(p.weight)
        #print('22',name,p)  # print打印出来 调试用
        
# hasattr() 函数用于判断对象是否包含对应的属性。
# isinstance()检查对象是否是指定的类型。
# append() 向列表末尾添加元素

optimizer = torch.optim.SGD(pg0, lr=0.1, momentum=0.5)  #初始化优化器,定义一个参数组
optimizer.add_param_group({
    
    "params": pg1})    # 增加一组参数 性质与pg0一样
optimizer.add_param_group({
    
    "params": pg2})    # 增加一组参数 性质与pg0一样
# 可以看到,参数组是一个list,一个元素是一个dict,每个dict中都有lr, momentum等参数,这些都是可单独管理,单独设定。
#---------------------------------------------------------------------#

解决办法:

方法1
在上述最后一行增加一组参数,把self.w加入优化器进行迭就好了。

#optimizer.add_param_group({
      
      "params": model.w,'lr': 0.12, 'momentum': 0.8}) # 这个是我网络中定义的自学习权重参数 

这里关于optimizer.add_param_group的详细介绍可以参考:pytorch优化器——add_param_group()介绍及示例、Yolov7 优化器代码示例

3、复现总代码

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision import transforms
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import os  # 添加代码①
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"  # 添加代码②

batch_size = 256    #设置batch大小
transform = transforms.Compose([
    transforms.ToTensor(),                        #转换为张量
    transforms.Normalize((0.1307,), (0.3081,))    #设定标准化值
])
#训练集
train_dataset = datasets.MNIST(
    root='../data/mnist',
    train=True,
    transform=transform,
    download=True)
#测试集
test_dataset = datasets.MNIST(
    root='../data/mnist',
    train=False,
    transform=transform,
    download=True)
 
#训练集加载器
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size,shuffle=True)
#测试集加载器
test_loader = DataLoader(dataset=test_dataset,batch_size=batch_size, shuffle=False)

class resnet18(torch.nn.Module):
    def __init__(self):
        super(resnet18, self).__init__()
        self.block1 = torch.nn.Sequential(
            torch.nn.Conv2d(1, 10, 5),
            torch.nn.MaxPool2d(2),
            torch.nn.ReLU(True),
            torch.nn.BatchNorm2d(10),
        )
        self.block2 = torch.nn.Sequential(
            torch.nn.Conv2d(10, 20, 5),
            torch.nn.MaxPool2d(2),
            torch.nn.ReLU(True),
            torch.nn.BatchNorm2d(20),
        )
        self.fc = torch.nn.Sequential(
            torch.nn.Flatten(),
            torch.nn.Linear(320, 10)
        )
        self.w = torch.nn.Parameter(torch.ones(2)) # 定义自学习参数
        
    def forward(self, x):
        x = self.block1(x)*self.w[0]
        x = self.block2(x)*self.w[1]
        x = self.fc(x)
        return x
    
model = resnet18()
device=torch.device("cuda:0"if torch.cuda.is_available()else"cpu")#使用GPU进行计算
model.to(device)#把model模型放进去
#---------------------------------------------------------------------# 
# 构造损失函数和优化函数
# 损失
criterion = torch.nn.CrossEntropyLoss()
pg0, pg1, pg2 ,pg3= [], [], [], [] 
for name, p in model.named_modules():
    if hasattr(p, "bias") and isinstance(p.bias, nn.Parameter):  # 把带有bias属性且性质为nn.Parameter的层选出来 添加到到pg2列表
        pg2.append(p.bias) 
    if isinstance(p, nn.BatchNorm2d) or "bn" in name: # 把标准化层选出来 添加到到pg0列表
        pg0.append(p.weight)
    elif hasattr(p, "weight") and isinstance(p.weight, nn.Parameter):  # 把带有weight属性且性质为nn.Parameter的层选出来 添加到到pg1列表
        pg1.append(p.weight)
        #print('22',name,p)  # print打印出来 调试用
        
# hasattr() 函数用于判断对象是否包含对应的属性。
# isinstance()检查对象是否是指定的类型。
# append() 向列表末尾添加元素

optimizer = torch.optim.SGD(pg0, lr=0.1, momentum=0.5)  #初始化优化器,定义一个参数组
optimizer.add_param_group({
    
    "params": pg1})    # 增加一组参数 性质与pg0一样
optimizer.add_param_group({
    
    "params": pg2})    # 增加一组参数 性质与pg0一样
# 不加入下面这一行self.w是不更新的
optimizer.add_param_group({
    
    "params": model.w,'lr': 0.12, 'momentum': 0.8}) # 这个是我网络中定义的自学习权重参数 
# 可以看到,参数组是一个list,一个元素是一个dict,每个dict中都有lr, momentum等参数,这些都是可单独管理,单独设定。
#---------------------------------------------------------------------#
def train(epoch):
    
#     adjust_learning_rate(optimizer, epoch, start_lr)  # 动态调整学习率
#     print("Lr:{}".format(optimizer.state_dict()['param_groups'][0]['lr']))  # 查看学习率
#     print("Lr:{}".format(optimizer.state_dict()['param_groups'][1]['lr']))
#     print("Lr:{}".format(optimizer.state_dict()['param_groups'][2]['lr']))
#     print(optimizer.state_dict()["param_groups"])  # 查看优化器完整参数

    running_loss = 0.0        #每一轮训练重新记录损失值
    for batch_idx, data in enumerate(train_loader, 0):    #提取训练集中每一个样本
        inputs, target = data        
        inputs, target = inputs.to(device), target.to(device)  # 这里的数据(原数据)也要迁移过去
        # outputs输出为0-9的概率  256*10
        outputs = model(inputs)              #代入模型
        loss = criterion(outputs, target)    #计算损失值
        loss.backward()                      #反向传播计算得到每个参数的梯度值
        optimizer.step()                     #梯度下降参数更新
        optimizer.zero_grad()                #将梯度归零
        running_loss += loss.item()          #损失值累加
    
        if batch_idx % 300 == 299:           #每300个样本输出一下结果
            print('[%d,%5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
            running_loss = 0.0          # (训练轮次,  该轮的样本次,  平均损失值)
    
    return running_loss


 
def test():
    correct = 0
    total = 0
    with torch.no_grad():            #执行计算,但不希望在反向传播中被记录
        for data in test_loader:     #提取测试集中每一个样本
            images, labels = data
            images, labels = images.to(device), labels.to(device)
            # outputs输出为0-9的概率  256*10
            outputs = model(images)  #带入模型
            # torch.max()这个函数返回的是两个值,第一个值是具体的value(我们用下划线_表示)
            # 第二个值是value所在的index(也就是predicted)
            _, pred = torch.max(outputs.data, dim=1)    #获得结果中的最大值
            total += labels.size(0)                     #测试数++
            correct += (pred == labels).sum().item()    #将预测结果pred与标签labels对比,相同则正确数++
        print('%d %%' % (100 * correct / total))    #输出正确率
        
        
        
if __name__ == '__main__':
    # 这两个数组主要是为了画图
    lossy = []        #定义存放纵轴数据(损失值)的列表
    epochx = []       #定义存放横轴数据(训练轮数)的列表
    
    for epoch in range(10):    #训练10轮
        epochx.append(epoch)   #将本轮轮次存入epochy列表
        lossy.append(train(epoch))  #执行训练,将返回值loss存入lossy列表  
        test()                 #每轮训练完都测试一下正确率
        print('w',model.w)
    path = "D:/code/text/model2.pth"
    #torch.save(model,path)
    torch.save(model.state_dict(),path)   # 保存模型
    model = torch.load("D:/code/text/model2.pth")  # 加载模型
 
    #可视化一下训练过程
    plt.plot(epochx, lossy)
    plt.grid()
    plt.show()

4、关于解决tensor参数梯度,weight不更新的若干思路的总结:

1、看该变量的梯度是否为0或者为None。
2、学习率是否过小和过大。
3、是否加入了optimal的优化参数序列中。
4、softmax也会使结果失去梯度,用log_softmax代替。
5、torch.argmax(),会使结果失去梯度。

至于梯度输出、是否加入了optimal的优化参数序列中、叶子节点,请参考系列文章目录另外两篇。


总结

现在回头看这个bug真是很简单,但自己就是没有发现。还是太菜了。

猜你喜欢

转载自blog.csdn.net/weixin_45464524/article/details/130471296