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

文章目录

本节内容

  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 =100 x +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 =100 ∗3 +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 =(y −y ^​)2 =(θx +b −y ^​)2 =(350 −348 )=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(1003+50-348)3 = 12 ∂θ∂L ​=2 (θx +b −y ^​)x =2 (100 ∗3 +50 −348 )∗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 ∂b ∂L ​=2 (θx +b −y ^​)=2 (100 ∗3 +50 −348 )=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 θ←θ−l r ∗∂θ∂L ​=100 −2 ∗12 =76

b ← θ − l r ∗ ∂ L ∂ b = 50 − 2 ∗ 4 = 42 b \leftarrow \theta – lr * \frac{\partial L}{\partial b} = 50 -2*4=42 b ←θ−l r ∗∂b ∂L ​=50 −2 ∗4 =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 =76 x +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>)
</addbackward0>
  1. 之后我们使用均方误差损失函数(MSE)来求出损失:
L = (y-y_hat) ** 2
L

输出为:

tensor([4.], grad_fn=<powbackward0>)
</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}θ←θ−l r ∗∂θ∂L ​。因为在更新梯度上,并不是简单的梯度乘以学习率,不同的人有不同的想法,所以才会出现Adam等不同的优化器。

自定义损失函数

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

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

class SimpleModel(nn.Module):

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

        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):
        random_num = 0.4

        for param_group in self.param_groups:
            params = param_group['params']

            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

theta_list = []

theta = Variable(torch.FloatTensor([1]), requires_grad=True)

optimizer = torch.optim.SGD([theta], lr=0.1)

lr_scheduler = StepLR(optimizer, step_size=10, gamma=0.5)

for i in range(epoch):

    theta.grad = torch.FloatTensor([0.1])

    optimizer.step()

    lr_scheduler.step()

    optimizer.zero_grad()

    theta_list.append(theta.item())

    print("epoch:{}, lr:{}, theta:{}".format(i, optimizer.param_groups[0]['lr'], theta.item()))

plt.plot(range(epoch), theta_list)
plt.show()

输出为:

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

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

上述例子中,我使用了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 = [group['lr'] for group in optimizer.param_groups]

        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:

            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

theta_list = []

theta = Variable(torch.FloatTensor([1]), requires_grad=True)

optimizer = torch.optim.SGD([theta], lr=0.1)

for i in range(epoch):

    theta.grad = torch.FloatTensor([0.1])

    optimizer.step()
"""
    直接调整optimizer的学习率
"""
    for p in optimizer.param_groups:
        if (i + 1) % 10 == 0:
            p['lr'] = p['lr'] * 0.5

    optimizer.zero_grad()

    theta_list.append(theta.item())

    print("epoch:{}, lr:{}, theta:{}".format(i, optimizer.param_groups[0]['lr'], theta.item()))

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

Original: https://blog.csdn.net/zhaohongfei_358/article/details/125759911
Author: iioSnail
Title: pytorch 自定义损失函数、优化器(Optimizer)和学习率策略

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/707908/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球