【Pytorch】深度学习之优化器


优化器
根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值,使得模型输出更加接近真实标签的工具
学习目标
image.png

Pytorch提供的优化器

优化器的库torch.optim
优化器举例:
image.png

所有优化器的基类Optimizer

optimizer定义

class Optimizer(object):
	def __init__(self, params, defaults):
		self.defaults = defaults
		self.state = defaultdict(dict)
		self.param_groups = []

Optimizer属性
defaults:存储优化器的超参数,举个例子

# 使用的超参数包括:学习率lr,动量momentum,阻尼动量抑制项dampening,权重衰减weight_decay,nesterov——bool值,决定是否使用Nesterov动量方法
{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}

state: 参数缓存

# defaultdict类型的参数缓存,存储的是一个tensor键值对,key值为一个需要计算梯度的模型参数,value值为一个momentum_buffer的键值对存储动量缓冲张量
defaultdict(<class 'dict'>, {tensor([[ 0.3864, -0.0131],
        [-0.1911, -0.4511]], requires_grad=True):{'momentum_buffer': tensor([[0.0052, 0.0052],
        [0.0052, 0.0052]])}})

param_groups: 参数组,一个list,每个元素是一个字典,字典的key值顺序是params,lr,momentum,dampening,weight_decay,nesterov

# 'params'参数对应的是一个存储待优化参数的list
[{'params': [tensor([[-0.1022, -1.6890],[-1.5116, -1.7846]], requires_grad=True)], 'lr': 1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]

Optimizer方法
zero_grad()方法、step()方法、add_param_group()方法、load_state_dict()方法、state_dict()方法
zero_grad(): 清空所管理参数的梯度,由于Tensor的梯度不会自动清零,因而每次backward时均需要清空梯度

def zero_grad(self, set_to_none: bool = False)for group in self.param_groups: # 遍历optimizer的参数组,不同参数组往往有着不同的超参数
        for p in group['params']:  # 遍历参数组中的tensor参数
            if p.grad is not None:  #梯度不为空,即需要优化的参数
                if set_to_none: 
                    p.grad = None # 将参数的梯度设置为None,表示在backward过程中不再跟踪这个梯度的计算图
                else:
                    if p.grad.grad_fn is not None: # 判断参数的梯度是否有梯度函数
                        p.grad.detach_() # 有梯度函数的参数梯度,即其是通过某个操作计算得到,使用`detach_`方法将其从计算图中分离
                    else:
                        p.grad.requires_grad_(False) # 没有梯度函数的参数梯度,通过`requires_grad`方法设置其不需要梯度
                    p.grad.zero_()# 梯度设置为0 

step():执行一步梯度更新,参数更新

def step(self, closure): 
    raise NotImplementedError # 在Optimizer基类中,step()函数被定义为抛出`NotImplementedError`异常,表明继承Optimizer的优化器类必须实现自己的step方法

add_param_group():添加参数组

def add_param_group(self, param_group):
	# 参数类型检查:检查传入的`param_group`是否为字典类型,如果不是则抛出异常
    assert isinstance(param_group, dict), "param group must be a dict"
    # 参数整理:获取传入参数组中的`params`字段,将其整理为list形式
	# 检查类型是否为tensor
    params = param_group['params']
    if isinstance(params, torch.Tensor):
        param_group['params'] = [params]
    elif isinstance(params, set):
	    # 如果参数为set类型,抛出异常
        raise TypeError('optimizer parameters need to be organized in ordered collections, but the ordering of tensors in sets will change between runs. Please use a list instead.')
    else:
        param_group['params'] = list(params)

	# 参数检查:对每个参数进行检查,确保是 leaf Tensor 类型
    for param in param_group['params']:
        if not isinstance(param, torch.Tensor):
            raise TypeError("optimizer can only optimize Tensors, but one of the params is " + torch.typename(param))
        if not param.is_leaf:
            raise ValueError("can't optimize a non-leaf Tensor")

    # 超参数设置检查:检查在该优化器defaults中要求的超参数是否被提供,若未提供则抛出异常
    for name, default in self.defaults.items():
        if default is required and name not in param_group:
            raise ValueError("parameter group didn't specify a value of required optimization parameter " + name)
        else:
            param_group.setdefault(name, default)

	# 检查当前提供的参数组中是否有重复参数,若有则抛出warning
    params = param_group['params']
    if len(params) != len(set(params)):
        warnings.warn("optimizer contains a parameter group with duplicate parameters; in future, this will cause an error; see github.com/PyTorch/PyTorch/issues/40967 for more information", stacklevel=3)
# 上面好像都在进行一些类的检测,报Warning和Error
	# 参数集合检查:检查当前参数组的参数是否与之前已有的参数组参数集合没有交集
    param_set = set()
    for group in self.param_groups:
        param_set.update(set(group['params']))

    if not param_set.isdisjoint(set(param_group['params'])):
        raise ValueError("some parameters appear in more than one parameter group")
# 添加参数
    self.param_groups.append(param_group)

load_state_dict():加载状态参数字典,可以实现模型的断点续训练

def load_state_dict(self, state_dict):
    r"""Loads the optimizer state.
    Arguments:
        state_dict (dict): optimizer state. Should be an object returned from a call to :meth:`state_dict`.
    """
    # deepcopy, to be consistent with module API
    state_dict = deepcopy(state_dict)
    # Validate the state_dict: 检验状态字典的参数组是否和当前优化器的参数组一致
    groups = self.param_groups
    saved_groups = state_dict['param_groups']
	# 检验参数组长度和参数组下tensor参数长度
    if len(groups) != len(saved_groups):
        raise ValueError("loaded state dict has a different number of parameter groups")
    param_lens = (len(g['params']) for g in groups)
    saved_lens = (len(g['params']) for g in saved_groups)
    if any(p_len != s_len for p_len, s_len in zip(param_lens, saved_lens)):
        raise ValueError("loaded state dict contains a parameter group that doesn't match the size of optimizer's group")

    # Update the state
    # 创建id映射,将状态字典中的参数与当前优化器中的参数对应起来
    id_map = {old_id: p for old_id, p in zip(chain.from_iterable((g['params'] for g in saved_groups)), chain.from_iterable((g['params'] for g in groups)))}

    def cast(param, value):
        r"""Make a deep copy of value, casting all tensors to device of param."""
   		.....

    # Copy state assigned to params (and cast tensors to appropriate types).
    # State that is not assigned to params is copied as is (needed for backward compatibility).
    # 转换并更新状态:将状态字典中的状态转换并更新到当前优化器中。
    state = defaultdict(dict)
    for k, v in state_dict['state'].items():
        if k in id_map:
            param = id_map[k]
            state[param] = cast(param, v)
        else:
            state[k] = v

    # Update parameter groups, setting their 'params' value
    def update_group(group, new_group):
       ...
    # 更新参数组
    param_groups = [update_group(g, ng) for g, ng in zip(groups, saved_groups)]
    # 调用`__setstate__`方法,实现将更新后的状态设置到当前优化器中
    self.__setstate__({'state': state, 'param_groups': param_groups})

state_dict():获取优化器当前状态信息字典

def state_dict(self):
    r"""Returns the state of the optimizer as a :class:`dict`.
    It contains two entries:
    * state - a dict holding current optimization state. Its content differs between optimizer classes.
    * param_groups - a dict containing all parameter groups
    """
    # Save order indices instead of Tensors
    param_mappings = {}
    start_index = 0
    
	# 将Optimizer类的状态字典进行打包操作
    def pack_group(group):
		......
	# 使用pack_group函数对param_groups中所有参数组进行打包
    param_groups = [pack_group(g) for g in self.param_groups]
    # Remap state to use order indices as keys
    # 遍历当前优化器状态字典(`self.state`)中的每一项,将键映射到参数组的顺序索引
    packed_state = {(param_mappings[id(k)] if isinstance(k, torch.Tensor) else k): v for k, v in self.state.items()}
    return {
        'state': packed_state,
        'param_groups': param_groups,
    }

实际操作

代码示例

import os
import torch

# 设置权重
weight = torch.randn((2,2), requires_grad=True)
# 设置梯度
weight.grad = torch.ones((2,2))
# 输出当前权重和梯度
print("The data of weight before step:\n{}".format(weight.data))
print("The grad of weight before step:\n{}".format(weight.grad))
# 实例化优化器
optimizer = torch.optim.SGD([weight], lr=0.1, momentum=0.9)
# 一步优化操作
optimizer.step()
# 查看一步更新后的参数结果
print("The data of weight before step:\n{}".format(weight.data))
print("The grad of weight before step:\n{}".format(weight.grad))
# 权重清零
optimizer.zero_grad()
# 检验清零是否成功
print("The grad of weight after optimizer.zero_grad():\n{}".format(weight.grad))
# 输出优化器参数
print("optimizer.params_group is \n{}".format(optimizer.param_groups))
# 查看参数位置 --optimizer和weight的位置一样
print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight)))
# 添加参数
weight2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({'params':weight2, 'lr': 0.0001, 'nesterov':True})
# 查看现有参数
print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
# 查看当前优化器的状态信息
opt_state_dict = optimizer.state_dict()
print("state_dict before step:\n", opt_state_dict)
# 进行5次step操作
for _ in range(50):
    optimizer.step()
# 输出现有状态信息
print("state_dict after step:\n", optimizer.state_dict())
# 保存参数信息 --路径自行更换
torch.save(optimizer.state_dict(), os.path.join(r"D:\pythonProject\Attention_Unet", "optimizer_state_dict.pkl"))
print("Done!")
# 加载参数信息
state_dict = torch.load(r"D:\pythonProject\Attention_Unet\optimizer_state_dict.pkl") # 需要修改为你自己的路径
optimizer.load_state_dict(state_dict)
print("load state_dict successfully\n{}".format(state_dict))
# 输出属性信息
print("\n{}".format(optimizer.defaults))
print("\n{}".format(optimizer.state))
print("\n{}".format(optimizer.param_groups))

注意事项

  1. 每个优化器都是一个类,只有其经过实例化之后才能使用
class Net(nn.Module):
	...
net = Net()
optim = torch.optim.SGD(net.parameters(), lr = lr)
optim.step
  1. optimizer的操作分为两步:梯度置零,梯度更新
optimizer = torch.optim.SGD(net.parameters(), lr=1e-5)
for epoch in range(EPOCH):
	...
	optimizer.zero_grad()
	loss = ...
	loss.backward()
	optimizer.step()
  1. 以层为单位,设置每个优化器更新的参数权重
from torch import optim
from torchvision.models import resnet18

net = resnet18

optimizer = optim.SGD([
	{'params': net.fc.parameters()},
	{'params': net.layer4[0].conv1.parameters(), 'lr': 1e-2}
], lr=1e-5)

实验

数据生成

a = torch.linspace(-1, 1, 1000)
# 利用unsqueeze进行升维操作
x = torch.unsqueeze(a, dim=1)
y = x.pow(2) + 0.1*torch.normal(torch.zeros(x.size()))

# 数据可视化
import matplotlib.pyplot as plt
plt.scatter(x,y)
plt.title('Generated Data') 
plt.xlabel('X-axis') 
plt.ylabel('Y-axis') 
plt.show()

网络结构

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.hidden = nn.Linear(1, 20)
        self.predict = nn.Linear(20, 1)

    def forward(self, x):
        x = self.hidden(x)
        x = F.relu(x)
        x = self.predict(x)
        return x

利用不同的优化器对该网络结构的权重参数进行优化,并绘制loss随着step变化的图示,得到收敛速度

import torch.optim as optim
import torch.nn.functional as F

# 定义模型
model1 = Net()
model2 = Net()

# 定义损失函数
criterion = nn.MSELoss()

# 定义两个不同的优化器
optimizer1 = optim.SGD(model1.parameters(), lr=0.01)
optimizer2 = optim.Adam(model2.parameters(), lr=0.01)

# 训练模型
num_epochs = 1000
losses1, losses2 = [], []

for epoch in range(num_epochs):
    # 将数据转换为 PyTorch 张量
    x_tensor = torch.FloatTensor(x).view(-1, 1)
    y_tensor = torch.FloatTensor(y).view(-1, 1)

    # 使用第一个优化器进行训练
    optimizer1.zero_grad()
    outputs1 = model1(x_tensor)
    loss1 = criterion(outputs1, y_tensor)
    loss1.backward()
    optimizer1.step()
    losses1.append(loss1.item())

    # 使用第二个优化器进行训练
    optimizer2.zero_grad()
    outputs2 = model2(x_tensor)
    loss2 = criterion(outputs2, y_tensor)
    loss2.backward()
    optimizer2.step()
    losses2.append(loss2.item())

# 绘制损失变化图
import matplotlib.pyplot as plt

plt.plot(range(num_epochs), losses1, label='SGD')
plt.plot(range(num_epochs), losses2, label='Adam')
plt.title('Loss over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.show()

训练结果图示:
image.png

问题说明
使用不同的optimizer对相同数据进行优化时,应该要用不同的模型,因为如果使用相同的模型,两个优化器的优化过程是相互干扰的
总结一下就是,相同输入数据,不同model实例,不同optimizer,相同criterion标准

参考资料

  1. datawhale through-pytorch repo
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值