诚信为本,市场在变,诚信永远不变...
  咨询电话:400-123-4567

公司新闻

一文搞定PyTorch中优化器optimizer的所有属性和方法

端庄的汤汤:pytorch中model、conv、linear、nn.Module和nn.optim模块参数方法一站式理解+finetune应用(上)

本文和之前的文章是连续的,欢迎阅读之前的关于model的属性和方法文章。

PyTorch中的优化器:管理并更新模型中可学习参数的值,使得模型输出更接近真实标签。

Optimizer类是所有优化器的基类,下面先分析一下其属性和常用方法。

因为分析Optimizer类的__init__方法,需要用到子类的某些参数,我们以SGD为例,先说明一下,看一下SGD类的__init__方法代码,非常的简单,一系列判断,然后将params这个参数单独列出来,将其余参数以字典的形式放到defaults里面,然后继承父类的初始化,将params和defaults传进去。这里面值得注意的就是params和defaults的形式。

class SGD(Optimizer):
    def __init__(self, params, lr=required, momentum=0, dampening=0,
                 weight_decay=0, nesterov=False):
        if lr is not required and lr < 0.0:
            raise ValueError("Invalid learning rate:{}".format(lr))
        if momentum < 0.0:
            raise ValueError("Invalid momentum value:{}".format(momentum))
        if weight_decay < 0.0:
            raise ValueError("Invalid weight_decay value:{}".format(weight_decay))

        defaults=dict(lr=lr, momentum=momentum, dampening=dampening,
                        weight_decay=weight_decay, nesterov=nesterov)
        if nesterov and (momentum <=0 or dampening !=0):
            raise ValueError("Nesterov momentum requires a momentum and zero dampening")
        super(SGD, self).__init__(params, defaults)

Optimizer类的初始化代码如下。

class Optimizer(object):
    Args:
        params (iterable): an iterable of :class:`torch.Tensor` s or
            :class:`dict` s. Specifies what Tensors should be optimized.
        defaults: (dict): a dict containing default values of optimization
            options (used when a parameter group doesn't specify them).
    """

    def __init__(self, params, defaults):
        torch._C._log_api_usage_once("python.optimizer")
        self.defaults=defaults

        self._hook_for_profile()

        if isinstance(params, torch.Tensor):
            raise TypeError("params argument given to the optimizer should be "
                            "an iterable of Tensors or dicts, but got " +
                            torch.typename(params))

        self.state=defaultdict(dict)
        self.param_groups=[]

        param_groups=list(params)          #   1
        if len(param_groups)==0:
            raise ValueError("optimizer got an empty parameter list")
        if not isinstance(param_groups[0], dict):
            param_groups=[{'params': param_groups}]#  2

        for param_group in param_groups:   # 3
            self.add_param_group(param_group)

关键几个点为:

  1. self.defaults=defaults
  2. self.state=defaultdict(dict)
  3. self.param_groups=[]# 特别重要,最需要记住的属性,特别是属性的内容和其形式。
  4. 最后一句,self.add_param_group方法,往self.param_groups里面放东西。

代码虽长,就这几个点就可以了,甚至self.state也不需关注,只需要明白其余三个点即可。

该初始化方法接收两个参数,一个是params,一个是defaults。这两个分开说,先说params,最常见的就是model.parameters(),当然net.parameters()也是一样的,就是模型类的对象的变量名不同,如下所示。

optimizer=optim.SGD(
***************************
    net.parameters(),               # params的一种形式
***************************
    lr=LR, momentum=0.9
)

通过之前的文章,我们知道这种params是个生成器,只返回各模型层的参数,没有参数名。model.parameters(),注意__init__方法中我注释的1部分的那句代码,我们得到一个新的变量------------------> param_groups,和self.param_groups好像,前者就是为后者服务的。param_groups=list(params),list可以把生成器的元素都取出来,所以,很明显,param_groups就是一个Parameter类对象的列表,里面的元素是每个网络层的参数weight和bias(如果有)。

很明显,param_groups[0]是Parameter类,不是dict,所以,这种形式的param_groups会被改造,将整个param_groups作为值,"params"作为键,形成一个键值对,放在字典里,然后重新赋值给param_groups。

现在我们要记得param_groups的形式,一个列表,里面是一个字典,字典的键是"params",值为所有网络层的参数。

注释为3的代码,将param_groups中的每个元素送进self.add_param_group这个列表中。现在的param_groups里只有一个元素{"param":[参数]},看一下这个方法的作用,代码如下。

功能:将参数放到self.param_groups这个列表中

参数:param_group,一个字典

返回值:无

    def add_param_group(self, param_group):
        r"""Add a param group to the :class:`Optimizer` s `param_groups`.

        This can be useful when fine tuning a pre-trained network as frozen layers can be made
        trainable and added to the :class:`Optimizer` as training progresses.

        Args:
            param_group (dict): Specifies what Tensors should be optimized along with group
            specific optimization options.
        """
        assert isinstance(param_group, dict), "param group must be a dict"        #  1

        params=param_group['params']# 2
        if isinstance(params, torch.Tensor):
            param_group['params']=[params]
        elif isinstance(params, 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)  # 3

        for param in param_group['params']:       # 4
            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")

        for name, default in self.defaults.items():    # 5
            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)

        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)

        param_set=set()         # 6
        for group in self.param_groups:
            param_set.update(set(group['params']))

        if not param_set.isdisjoint(set(param_group['params'])):     # 7
            raise ValueError("some parameters appear in more than one parameter group")

        self.param_groups.append(param_group) # 8

将上述代码分为8个步骤,第1步判断传进来的参数是否是一个字典,必然是一个字典,不是字典报错。第2步,取出字典里的"params"的值,就是参数的列表,这是个列表,然后一系列判断,走到第3步,又重新以列表的形式赋值回去,一套走下来,还是熟悉的配方,没变化。第4步,判断参数的列表里边的元素类型,那必然是Parameter类型的,也就是Tensor类型的,并且是叶子结点没问题。第5步,将defaults这个字典里的键值对拿出来,放到现在的param_group这个字典里,这样该字典构成一个具有完整参数的字典,其所有键为:dict_keys(['params', 'lr', 'momentum', 'dampening', 'weight_decay', 'nesterov']),方便step()方法调用。

第6步和第7步是一起的,判定当前字典中的参数组和之前的参数组是不是一样的。对于当前来说,self.param_groups是空的,所以直接到第7步,判断param_set集合是否和param_group["params"]这个集合中具有相同元素,没有返回True,反之False。显然没有,所以7不执行。然后执行第8步,将构造完整的param_group这个字典,加到我们一直强调非常重要的self.param_groups中去。

现在我们知道self.param_groups这个列表中具有字典,每个字典的keys为dict_keys(['params', 'lr', 'momentum', 'dampening', 'weight_decay', 'nesterov']),当然,每个键都有其对应的值。这些键值对是构建SGD实例时,传进来的参数。

开头的SGD类的defaults变量的代码如下:

defaults=dict(lr=lr, momentum=momentum, dampening=dampening,
                        weight_decay=weight_decay, nesterov=nesterov)

构建SGD实例后打印一下defaults这个变量的结果:
{'lr': 0.01,
 'momentum': 0.9,
 'dampening': 0,
 'weight_decay': 0,
 'nesterov': False}

重新看一下第5步,对于defaults中的items(),获取键值对,然后判断一下,肯定是False,因为default和required不是一个东西。default是float是bool,required是一个表示优化器参数的单例类,代码如下:

class _RequiredParameter(object):
    """Singleton class representing a required parameter for an Optimizer."""
    def __repr__(self):
        return "<required parameter>"

required=_RequiredParameter()

所以,在这个地方会把lr,momentum,nag,weight_decay等参数全部加到param_group这个字典里去,然后再加到self.param_groups这个列表中。

总结一下:SGD类对象初始化时,继承父类的属性和方法,经过一系列操作,self.param_groups这个列表中,具有一个字典,字典里面的键为dict_keys(['params', 'lr', 'momentum', 'dampening', 'weight_decay', 'nesterov']),每个键具有对应的值。

params还有一种常见形式如下。

fcParamsId=list(map(id, resnet18_ft.fc.parameters()))     # 返回的是parameters的 内存地址
features_params=filter(lambda p: id(p) not in fcParamsId, resnet18_ft.parameters())

optimizer=optim.SGD(
*************************************************************
[{'params': features_params, 'lr': LR * 0.1},                         #  这个列表是params的另一种形式
{'params': resnet18_ft.fc.parameters()}], 
***************************************************************
    'lr': LR, momentum=0.9
)

我们可以对比一下这种形式和上述model.parameters()这种方式的不同。

  1. 前者是包含着字典的列表,后者是生成器
  2. 进入到Optimizer类的初始化函数中,前者不变,还是包含2个字典的列表,后者先变成参数的列表,再变成只有一个字典的列表,该字典中只有一个键值对,"params"和所有参数值
  3. 在执行self.add_param_group()方法的循环里,前者需要遍历两个字典,后者只有一个字典,也就是说,最终在self.param_groups这个列表中,前者最终具有两个具有全部键值对的字典,后者只有一个
  4. 在如上self.add_param_group()方法中,标记为2处,取出前者字典中的"params"对应的值为生成器,在3处,将其变成了参数的列表,后者在2和3处没变化,因为在初始化一开始就使用list()将model.parameters()的所有参数取出来了。
  5. 在如上self.add_param_group()方法中,标记为4处,没区别。在5处有区别,并且该区别非常关键。这是对不同参数设置不同学习率和动量的地方。param_group是循环中取出的字典,对于如上有两个字典的来说,就是将self.defaults里面取出来的键值对,分别放到两个字典中去。param_group.setdefault(name, default)这句代码。如果param_group这个字典中比如第一个字典具有"lr"这个键的,保持不变,对于第二个字典里面没有"lr"这个键的,将default和name设置为新的键值对。不知道的同学可以看一下dict中setdefault这个方法的功能。
  6. 前者的第6和7步,由于存在两个字典,所以第二次需要和第一次进行对比,看看两个字典里面的"params"这个键对应的参数是否相同,如果相同,引用的就是同一块地址,设置不同的学习率等参数就没意义了,就会报错。
  7. 最后,将两个具有所有键值对,但是参数不同的两个字典,加入到self.param_groups这个列表中。

两个字典的有什么用呢?如上我们两个字典里面的"params",一个是特征提取层的参数,一个是fc层的参数。我们如果只有小数据集进行finetune时,可以将第一个字典里面的lr设置的很小,那么他就几乎不更新。着重训练fc层的参数。

这是怎么实现的呢?我们可以看一下SGD类中step()方法的代码结构如下:将self.param_groups里的字典分别取出,对每个参数进行处理,然后放到sgd中进行参数更新。注意这里不是反向传播,不是梯度计算过程。各参数梯度在这step之前已经计算完保存好了,此处只是更新参数,所以读取进来的字典没有先后顺序。

def step(self, closure=None):
    ......
    for group in self.param_groups:
        ......
        for p in group['params']:
            ......
    F.sgd(params_with_grad,
                  d_p_list,
                  momentum_buffer_list,
                  weight_decay=weight_decay,
                  momentum=momentum,
                  lr=lr,
                  dampening=dampening,
                  nesterov=nesterov)

常用优化器算法原理,看这两篇就够了。

Alex Chung:深度学习中常用优化器的总结陶将:优化算法Optimizer比较和总结

平台注册入口