pytorch 自定义损失函数、优化器(Optimizer)和学习率策略(Scheduler)

本节内容

  1. 梯度下降回顾
  2. 理解Pytorch模型定义、前向传播、反向传播、更新梯度的过程
  3. 学会并理解自定义损失函数
  4. 学会并理解优化器的作用和使用
  5. 学会并理解自定义学习策略的多种方法

梯度下降回顾

我们首先来简单的回顾一下梯度下降,我们使用一个很简单的例子来说明。

假设我们要进行一元线性回归,即我们想求 y = θ x + b y = \theta x +b y=θx+b 中的未知参数 θ \theta θ b b b

  1. 首先我们会对 θ \theta θ b b b 进行初始化,例如,初始化为 100 100 100 50 50 50,则我们的初始化方程为:

y = θ x + b = 100 x + 50 y = \theta x + b = 100 x + 50 y=θx+b=100x+50

  1. 之后,我们拿一组样本来带入上述函数,求出这组样本的预测值,这里我们假设只有一组样本 ( x , y ^ ) = ( 3 , 348 ) (x, \hat{y})=(3, 348) (x,y^)=(3,348),之后我们会将 x = 3 x=3 x=3 带入到上式中,求出预测值:

y = θ ∗ 3 + b = 100 ∗ 3 + 50 = 350 y = \theta * 3 + b = 100 * 3+ 50 = 350 y=θ3+b=1003+50=350

  1. 之后我们使用均方误差损失函数(MSE)来求出损失:

L = ( y − y ^ ) 2 = ( θ x + b − y ^ ) 2 = ( 350 − 348 ) = 2 L = (y-\hat{y})^2 = (\theta x+b - \hat{y})^2 =(350-348)=2 L=(yy^)2=(θx+by^)2=(350348)=2

  1. 与此同时,我们可以对使用损失函数 L L L θ \theta θ b b b 进行求导,来算出他们的梯度:

∂ L ∂ θ = 2 ( θ x + b − y ^ ) x = 2 ( 100 ∗ 3 + 50 − 348 ) ∗ 3 = 12 \frac{\partial L}{\partial \theta} = 2(\theta x+b-\hat{y})x=2(100*3+50-348)*3 = 12 θL=2(θx+by^)x=2(1003+50348)3=12

∂ L ∂ b = 2 ( θ x + b − y ^ ) = 2 ( 100 ∗ 3 + 50 − 348 ) = 4 \frac{\partial L}{\partial b} = 2(\theta x + b- \hat{y}) = 2(100*3+50-348)=4 bL=2(θx+by^)=2(1003+50348)=4

  1. 然后就可以使用优化器(Optimizer)更新 θ \theta θ b b b 参数了,如果使用SGD,那就是如下公式,假设学习率是2:

θ ← θ − l r ∗ ∂ L ∂ θ = 100 − 2 ∗ 12 = 76 \theta \leftarrow \theta - lr * \frac{\partial L}{\partial \theta} = 100 - 2*12=76 θθlrθL=100212=76

b ← θ − l r ∗ ∂ L ∂ b = 50 − 2 ∗ 4 = 42 b \leftarrow \theta - lr * \frac{\partial L}{\partial b} = 50 -2*4=42 bθlrbL=5024=42

  1. 最终,在一次迭代后, θ \theta θ 变为了 76 76 76 b b b 变为了 42 42 42,即:

y = θ x + b = 76 x + 42 y = \theta x + b = 76 x+42 y=θx+b=76x+42

Pytorch 实现梯度下降与参数更新

本节我们来对上一节的例子使用Pytorch进行实验,来感受一下Pytorch的梯度下降:

  1. 首先我们会对 θ \theta θ b b b 进行初始化,例如,初始化为 100 100 100 50 50 50,我们的初始化代码为:
theta = Variable(torch.FloatTensor([100]), requires_grad=True) # θ
b = Variable(torch.FloatTensor([50]), requires_grad=True) 
x = 3
y_hat = 348 # 真实值

由于 theta 和 b 是需要求梯度的,所以用Variable封装

  1. 接下来我们将 x x x 带入到 y = θ x + b y = \theta x + b y=θx+b 中求出预测值:
y = theta * x + b
y

输出为:

tensor([350.], grad_fn=<AddBackward0>)
  1. 之后我们使用均方误差损失函数(MSE)来求出损失:
L = (y-y_hat) ** 2
L

输出为:

tensor([4.], grad_fn=<PowBackward0>)
  1. 接下来,我们使用损失对需要L函数中的变量进行求导:
L.backward()
print("theta.grad:", theta.grad)
print("b.grad:", b.grad)

输出为:

theta.grad: tensor([12.])
b.grad: tensor([4.])
  1. 然后就可以使用优化器更新 θ \theta θ b b b 参数了,这里使用SGD,学习率为2:
optimizer = torch.optim.SGD([theta, b], lr=2) # 传入要更新的参数和学习率
optimizer.step()  # 更新参数
print("theta:", theta)
print("b:", b)
  1. 最终,在一次迭代后, θ \theta θ 变为了 76 76 76 b b b 变为了 42 42 42

在看完这个例子后,相信你已经知道pytorch的模型、损失函数、优化器在整个模型训练中都扮演者什么样的作用了,其实就是以下几点:

  1. 模型的作用其实就是执行一系列的函数运算,在上述例子中模型就是 y = theta * x + b,入参为 x x x,调用forward也就是执行该式子
  2. 损失函数其实也是执行一系列函数运算,在上述例子就是 L = (y-y_hat) ** 2,所以其实损失函数和模型在Pytorch中并没有什么本质的区别,它们都是继承nn.Module。只是模型A(模型)的输出作为模型B(损失函数)的输入又进行了一些计算,只不过最后计算梯度的时候是对模型B进行求微分,也就是上面的L.backward()
  3. 最后,要将变量的梯度更新到变量上,这时候就是优化器出场了,它负责执行公式 θ ← θ − l r ∗ ∂ L ∂ θ \theta \leftarrow \theta - lr * \frac{\partial L}{\partial \theta} θθlrθL。因为在更新梯度上,并不是简单的梯度乘以学习率,不同的人有不同的想法,所以才会出现Adam等不同的优化器。

自定义损失函数

看完了上面两节,自定义损失函数应该就不攻自破了。其实损失函数就是定义一个模型,再多进行一步前向传递即可。这里我们将上一节的例子再进行一下改造。

首先,我们定义出我们的模型:

class SimpleModel(nn.Module):

    def __init__(self):
        super(SimpleModel, self).__init__()
        # 补充:这里正确的做法应该是使用nn.Parameter进行封装。
        #       nn.Parameter的作用是将该参数作为模型参数。
        #       这样才能在调用model.parameters()时获取到。
        self.theta = Variable(torch.FloatTensor([100]), requires_grad=True)  # θ
        self.b = Variable(torch.FloatTensor([50]), requires_grad=True)	
        

    def forward(self, x):
        return self.theta * x + self.b

接下来定义损失函数:

class SimpleMSELoss(nn.Module):

    def __init__(self):
        super(SimpleMSELoss, self).__init__()

    def forward(self, y, y_hat):
        return (y - y_hat) ** 2

读者们可以尝试下如何把它们两个合并

最后我们来使用一下:

x = 3
y_hat = 348  # 真实值

model = SimpleModel()
criteria = SimpleMSELoss()
y = model(3)
loss = criteria(y, y_hat)
loss.backward()
print("theta.grad:", model.theta.grad)
print("b.grad:", model.b.grad)

输出为:

theta.grad: tensor([12.])
theta.b: tensor([4.])

自定义优化器

在Pytorch中自定义优化器需要继承基类torch.optim.Optimizer,然后实现其step方法即可。

Optimizer的部分源码如下:

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 NotImplementedError

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

  • params:这个就是模型参数,严格来说应该是要进行更新的参数。Optimizer会将其放在param_groups这个变量下
  • defaults:这个是模型的一些默认配置。传空字典也没关系

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一致:

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

输出为:

theta: tensor([90.4000], requires_grad=True)
b: tensor([46.8000], requires_grad=True)

自定义学习率策略

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

Pytorch实现了很多自定义学习策略,具体可参考该链接。

我们先使用一个Pytorch实现好的,来看一下怎么用。假设需求为:更新100次参数,每10步将学习率减小一半。针对这个需求,我们可以使用torch.optim.lr_scheduler.StepLR来完成。代码如下:

epoch = 100 # 循环100次,每一个epoch更新一次参数

theta_list = [] # 记录一下theta的变化
# 1.初始化要更新的参数theta,初始化为1
theta = Variable(torch.FloatTensor([1]), requires_grad=True)
# 2. 定义SGD优化器,学习率设为0.1
optimizer = torch.optim.SGD([theta], lr=0.1)
# 3. 定义学习率策略,每{step_size}步,学习率乘以{gamma}
lr_scheduler = StepLR(optimizer, step_size=10, gamma=0.5)

# 4. 开始进行模型训练
for i in range(epoch):
    # 4.1 手动设置theta的梯度为0.1
    theta.grad = torch.FloatTensor([0.1])
    # 4.2 将梯度更新到theta参数上
    optimizer.step()
    # 4.3 调整学习率。注意:调整学习率要放在`optimizer.step()`之后
    lr_scheduler.step()
    # 4.4 清空梯度
    optimizer.zero_grad()

    # 记录一下theta的变化
    theta_list.append(theta.item())
    # 打印一下学习率和theta参数
    print("epoch:{}, lr:{}, theta:{}".format(i, optimizer.param_groups[0]['lr'], theta.item()))

# 5.绘制theta变化
plt.plot(range(epoch), theta_list)
plt.show()

输出为:

epoch:0, lr:0.1, theta:0.9900000095367432
epoch:1, lr:0.1, theta:0.9800000190734863
... 略
epoch:8, lr:0.1, theta:0.9100000858306885
epoch:9, lr:0.05, theta:0.9000000953674316
epoch:10, lr:0.05, theta:0.8950001001358032
...略
epoch:18, lr:0.05, theta:0.8550001382827759
epoch:19, lr:0.025, theta:0.8500001430511475
epoch:20, lr:0.025, theta:0.8475001454353333
... 略

在这里插入图片描述

上述例子中,我使用了0.1学习率的来对参数theta进行更新,而参数theta的梯度每次都固定为1。可以看到,前9次theta每次都是下降0.01。从第10次开始,学习率减小了一半,然后每隔10次学习率都会减小一半。

使用 LambdaLR 实现简单的学习率策略

通常我们的学习率策略并不是很复杂,此时我们可以使用Pytorch提供的torch.optim.lr_scheduler.LambdaLR 来实现,该方法接收两个重要参数:

  • optimizer:该参数就是优化器对象,每个学习率策略都要传
  • lr_lambda:该参数接收一个函数,该函数有一个参数last_epoch,表示上次是第几个epoch(其实指的就是你之前调用过几次lr_scheduler.step())。你可以通过该参数自定义你的学习率策略,优化器在实际执行时使用的学习率会乘以该函数的返回结果,即 lr * lr_lambda(last_epoch)。注意,这里的lr是你定义Optimizer时传入的那个学习率,而且始终不变。

例如,要实现前面的每10次将学习率降低一半,我们可以这么写:

lr_scheduler = LambdaLR(optimizer, lr_lambda=lambda epoch: 0.5 ** (epoch // 10))

通过继承 _LRScheduler 实现自定义的学习率策略

可能你的学习率策略比较复杂,LambdaLR 也无法满足你,此时你就要自己实现一个学习率策略。

Pytorch中,所有的学习率策略都要继承基类 torch.optim.lr_scheduler._LRScheduler,并实现该基类的一个重要方法def get_lr(self),你只需要按照你的学习率策略返回学习率即可。

但要实现你的学习率策略,你可能还需要用到_LRScheduler类的这些方法和属性:

  • self.last_epoch:获取之前执行了多少次lr_scheduler.step()
  • self.base_lrs:基础学习率,也就是定义Optimizer时指定的那个学习率。注意,这个是一个list,可能是因为Optimizer要更新的那些参数也不一定就是使用同一个学习率,一般只有一个。同理,get_lr 方法也要返回一个list,且数量与self.base_lrs相同。
  • self.optimizer:你可以通过该属性得到优化器对象。你可以通过代码 [group['lr'] for group in self.optimizer.param_groups] 获取到优化器中的学习率。你的学习率策略改变的就是这个值。
  • self.get_last_lr():获取上次的学习率。这个方法其实就是返回了属性self._last_lr。但这里有个坑,self._last_lr是在第一次调用step()方法后才会生成,所以使用的时候可能会报错,你可以通过在__init__方法中初始化self._last_lr来避免报错。

有了上述这些方法,我们就可以自定义学习率策略来实现前面的每10次将学习率降低一半的策略了,代码如下:

class MyLR(_LRScheduler):

    def __init__(self, optimizer):
        # 初始化self._last_lr避免报错
        self._last_lr = [group['lr'] for group in optimizer.param_groups]
        # 在_LRScheduler的初始化时,会调用step()方法
        super(MyLR, self).__init__(optimizer)



    def get_lr(self):
        # 获取之前的学习率
        last_lrs = self.get_last_lr()
        if self.last_epoch != 0 and self.last_epoch % 10 == 0:
            # 每10个epoch将学习率降低一半
            return [lr * 0.5 for lr in last_lrs]
        else:
            return last_lrs
        
lr_scheduler = MyLR(optimizer)

通过手动更新Optimizer中的学习率来自定义学习策略

其实要自定义学习策略不一定非要向上述说的那样那么麻烦,你可以直接像如下代码直接更新optimizer中的学习率:

for p in optimizer.param_groups:
    p['lr'] = rate

这样其实就灵活多了,我可以自定义一个普通类来记录学习率策略需要用到的东西,然后使用上述代码更新即可。例如:

epoch = 100 # 循环100次,每一个epoch更新一次参数

theta_list = [] # 记录一下theta的变化
# 1.初始化要更新的参数theta,初始化为1
theta = Variable(torch.FloatTensor([1]), requires_grad=True)
# 2. 定义SGD优化器,学习率设为0.1
optimizer = torch.optim.SGD([theta], lr=0.1)

# 3. 开始进行模型训练
for i in range(epoch):
    # 3.1 手动设置theta的梯度为0.1
    theta.grad = torch.FloatTensor([0.1])
    # 3.2 将梯度更新到theta参数上
    optimizer.step()
    """
    直接调整optimizer的学习率
    """
    for p in optimizer.param_groups:
        if (i + 1) % 10 == 0:
            p['lr'] = p['lr'] * 0.5

    # 3.4 清空梯度
    optimizer.zero_grad()

    # 记录一下theta的变化
    theta_list.append(theta.item())
    # 打印一下学习率和theta参数
    print("epoch:{}, lr:{}, theta:{}".format(i, optimizer.param_groups[0]['lr'], theta.item()))

# 4.绘制theta变化
plt.plot(range(epoch), theta_list)
plt.show()





参考资料

torch.optim.lr_scheduler._LRScheduler Class Reference:https://www.ccoderun.ca/programming/doxygen/pytorch/classtorch_1_1optim_1_1lr__scheduler_1_1__LRScheduler.html

SOURCE CODE FOR TORCH.OPTIM.LR_SCHEDULER

torch.optim.lr_scheduler:调整学习率: https://blog.csdn.net/qyhaill/article/details/103043637

  • 37
    点赞
  • 102
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
PyTorch中可以通过自定义损失函数学习率调整策略来优化模型的训练效果。 自定义损失函数: 在PyTorch中,可以通过继承`nn.Module`类,自定义损失函数。下面是一个例子: ```python import torch.nn as nn class CustomLoss(nn.Module): def __init__(self): super().__init__() def forward(self, inputs, targets): loss = ... # 计算自定义损失函数 return loss ``` 在自定义损失函数中,需要实现`forward`方法,接受模型的输出与标签作为输入,并返回损失值。 学习率调整策略PyTorch中提供了多种学习率调整策略,包括学习率衰减、学习率重启、余弦退火等。可以通过继承`torch.optim.lr_scheduler._LRScheduler`类,自定义学习率调整策略。下面是一个例子: ```python import torch.optim.lr_scheduler as lr_scheduler class CustomScheduler(lr_scheduler._LRScheduler): def __init__(self, optimizer, last_epoch=-1): super().__init__(optimizer, last_epoch) def get_lr(self): lr = ... # 计算自定义学习率调整策略 return [lr for _ in self.optimizer.param_groups] ``` 在自定义学习率调整策略中,需要实现`get_lr`方法,返回一个列表,其中每个元素是一个浮点数,代表每个参数组的学习率。 在训练过程中,可以将自定义损失函数学习率调整策略传递给优化器,例如: ```python import torch.optim as optim model = ... criterion = CustomLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = CustomScheduler(optimizer) for epoch in range(num_epochs): for batch in data_loader: inputs, targets = batch outputs = model(inputs) loss = criterion(outputs, targets) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() ``` 在每个epoch结束时,调用`scheduler.step()`方法来更新学习率

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

iioSnail

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

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

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

打赏作者

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

抵扣说明:

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

余额充值