【20】迁移学习与微调(fine-tuning)方法

这里关于迁移学习与微调的内容不再细说,有关概念可以参考:https://blog.csdn.net/weixin_44751294/article/details/116844391

1. 迁移学习


这里介绍迁移学习调用模型的我用过的方法,关键步骤是pretrained=True,使用预训练的参数。

1.1 使用list列表直接截取

这种方法直接使用了list截取了前面部分的网络结构,然后用Flatten()对数据进行处理,再添加最后一层全连接层来实现分类

trained_model = resnet18(pretrained=True)
model = nn.Sequential(*list(trained_model.children())[:-1],  # torch.Size([32, 512, 1, 1])
                      Flatten(),          # torch.Size([32, 512])
                      nn.Linear(512, 5)   # torch.Size([32, 5])
                      )

其中,Flatten的代码如下:

# 打平操作
class Flatten(nn.Module):

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

    def forward(self, x):
        shape = torch.prod(torch.tensor(x.shape[1:])).item()
        return x.view(-1, shape)

1.2 直接对结构进行修改

同样是使用迁移学习,这种方法不需要使用什么特别的函数,直接用自定义的全连接层替换原本resnet的全连接层即可,比较方便。

finetune_net = torchvision.models.resnet18(pretrained=True)
finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2)

直接打印模型print(finetune_net),或者通过finetune_net.modules即可输出修改后的网络结构,如下图所示:
在这里插入图片描述
比如:

# 定义网络结构
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool1 = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.pool2 = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)
        
        self.initialize_weights()
#         for m in self.modules():
#             print(m)

    def forward(self, x):
        x = self.pool1(F.relu(self.conv1(x)))
        x = self.pool2(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

    # 定义权值初始化
    def initialize_weights(self):
        
        # 其中self.modules()展示网络的层结构
        for m in self.modules():
            
            # 对nn.Conv2d层进行处理
            if isinstance(m, nn.Conv2d):
                # 采用 torch.nn.init.xavier_normal 方法对该层的 weight 进行初始化
                torch.nn.init.xavier_normal_(m.weight.data)
                # 并判断是否存在偏置(bias),若存在,将 bias 初始化为全 0
                if m.bias is not None:
                    m.bias.data.zero_()
            
            # 对BatchNorm2d层进行处理
            elif isinstance(m, nn.BatchNorm2d):
                # 对于BatchNorm2d,其是不需要bias的,所以对应的bias设置为0;其他设置为1,以下是两种设置方法,功能是一样的
                # 方法1:
                m.weight.data.fill_(1)
                m.bias.data.zero_()
                
                # 方法2:使用常数初始化
#                 nn.init.constant_(m.weight, 1)
#                 nn.init.constant_(m.bias, 0)
            
            # 对Linear层进行处理
            elif isinstance(m, nn.Linear):
                # 正态分布初始化,使值服从正态分布 N(mean, std)
                torch.nn.init.normal_(m.weight.data, 0, 0.01)
                m.bias.data.zero_()


net = Net()

# self.modules()结果展示如下
net.modules
<bound method Module.modules of Net(
  (conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)>

2. 微调


对于加载预训练的参数,我也使用过两种方法,这里记录一下。

在此之前,需要注意,新增加的全连接层的参数是随机的,所以需要对其进行初始化操作,这里使用Xavier均匀分布(avier 初始化方法中服从均匀分布 U(−a,a)):

nn.init.xavier_uniform_(finetune_net.fc.weight)

输出:

Parameter containing:
tensor([[ 0.1061, -0.0440,  0.0247,  ...,  0.0166,  0.0326, -0.0786],
        [-0.0324, -0.0513, -0.0037,  ..., -0.0276, -0.0958,  0.0679]],
       requires_grad=True)

其他的初始化方法可以参考博文:pytorch中的权值初始化方法,这里不再做详细介绍。

2.1 冻结层

由于上诉例子,只是修改了最后的一层全连接层,对于该层之外的全部层可以冻结其参数,只训练最后的全连接层,做法如下。

# 冻结除最后一层外的全部参数
for name, param in finetune_net.named_parameters():
    if name not in ["fc.weight", "fc.bias"]:
#         print(name)
        param.requires_grad=False   # 关键一步,设置为False之后,优化器溜不会对参数进行更新

param.requires_grad=False的时候,表示该参数没有梯度信息,也就是不会反向传播更新参数,也就是相当于冻结了,数值保持不变。

查看效果:

for name, param in finetune_net.named_parameters():
    print(name)
    print(param)

可以看见,除了最后的一层全脸曾的权值与偏置,其他的requires_grad=False,全部被冻结。
在这里插入图片描述
那么,现在可以把全部参数送入优化器,优化器也只会对全连接层的数据进行处理。

# 这个优化器只对最后一层的参数进行更新,可以加快训练速度
optimizer = torch.optim.SGD(finetune_net.fc.parameters(), lr=1e-2, momentum=0.9)
# 或者是这一条,个人感觉这两句的效果是一致的(如有错误恳请指出)
# optimizer = torch.optim.SGD(finetune_net.parameters(), lr=learning_rate, weight_decay=0.001)
print(optimizer)

输出:

SGD (
Parameter Group 0
    dampening: 0
    lr: 0.01
    momentum: 0.9
    nesterov: False
    weight_decay: 0
)

2.2 分组设置参数组

由于这里我们只需要对最后一层修改后的全连接层进行训练,而对其他层不需要怎么训练,根据这种特性,可以分为两个参数组丢进优化器中。

learning_rate = 1e-3

# 获取除了最后一层全连接层的全部层参数, 因为这部分数据参数只需要微调
params_1x = [param for name, param in finetune_net.named_parameters()
             if name not in ["fc.weight", "fc.bias"]]

# 设置了两个参数组
# 其中学习率是不同的,对于其他组的学习率会低点,全连接组的学习率会高点
trainer = torch.optim.SGD([{
    
    'params': params_1x},  # 其他组
						   {
    
    'params': finetune_net.fc.parameters(),'lr': learning_rate * 10}],  # 全连接组
                            lr=learning_rate, weight_decay=0.001)
  
print(trainer)

输出:

# 这里也分别对应了两组参数
SGD (
Parameter Group 0
    dampening: 0
    lr: 0.001
    momentum: 0
    nesterov: False
    weight_decay: 0.001

Parameter Group 1
    dampening: 0
    lr: 0.01
    momentum: 0
    nesterov: False
    weight_decay: 0.001
)
  • 这里进一步说明一下设置两组的意图:

在利用 pre-trained model 的参数做初始化之后,我们可能想让 fc 层更新相对快一些,而希望前面的权值更新小一些,这就可以通过为不同的层设置不同的学习率来达到此目的。为不同层设置不同的学习率,主要通过优化器对多个参数组进行设置不同的参数。所以,只需要将原始的参数组,划分成两个,甚至更多的参数组,然后分别进行设置学习率。这里将原始参数“切分”成 fc3 层参数和其余参数,为 fc3 层设置更大的学习率。

对参数分组进行训练同样也是一种方法,但是时间可能比冻结需要的时间要长点。

在这一节中涉及到了设置学习率的问题,在之后我会再对如何动态的调整学习率做一个笔记总结。之后再接触到其他方法再进行补充。

3. 优化器基类:Optimizer

对于参数组的设置,其实是优化器基类的一个参数,这里补充一下这方面的笔记。

当数据、模型和损失函数确定,任务的数学模型就已经确定,接着就要选择一个合适的优化器(Optimizer)对该模型进行优化。

PyTorch 中所有的优化器(如:optim.Adadelta、optim.SGD、optim.RMSprop 等)均是Optimizer 的子类,Optimizer 中定义了一些常用的方法,有 zero_grad()、step(closure)、state_dict()、load_state_dict(state_dict)和add_param_group(param_group)

1. param_groups

认识 Optimizer 的方法之前,需要了解一个概念,叫做参数组(param_groups)。在finetune,某层定制学习率,某层学习率置零操作中,都会设计参数组的概念,因此首先了解参数组的概念非常有必要。
optimizer 对参数的管理是基于组的概念,可以为每一组参数配置特定的lr,momentum,weight_decay 等等。
参数组在 optimizer 中表现为一个 list(self.param_groups),其中每个元素是dict,表示一个参数及其相应配置,在 dict 中包含’params’、‘weight_decay’、‘lr’ 、'momentum’等字段。

import torch
import torch.optim as optim


w1 = torch.randn(2, 2)
w1.requires_grad = True

w2 = torch.randn(2, 2)
w2.requires_grad = True

w3 = torch.randn(2, 2)
w3.requires_grad = True

# 一个参数组
optimizer_1 = optim.SGD([w1, w3], lr=0.1)
print('len(optimizer.param_groups): ', len(optimizer_1.param_groups))
# print(optimizer_1.param_groups, '\n')

# 两个参数组
optimizer_2 = optim.SGD([{
    
    'params': w1, 'lr': 0.1},
                         {
    
    'params': w2, 'lr': 0.001}])
print('len(optimizer.param_groups): ', len(optimizer_2.param_groups))
# print(optimizer_2.param_groups)
len(optimizer.param_groups):  1
len(optimizer.param_groups):  2
# 一组参数
optimizer_1.param_groups
[{'params': [tensor([[-0.3037,  1.4797],
           [ 0.5660,  0.0942]], requires_grad=True),
   tensor([[0.0423, 0.2110],
           [1.4629, 0.7293]], requires_grad=True)],
  'lr': 0.1,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]
# 两组参数
optimizer_2.param_groups
[{'params': [tensor([[-0.3037,  1.4797],
           [ 0.5660,  0.0942]], requires_grad=True)],
  'lr': 0.1,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False},
 {'params': [tensor([[ 0.5763, -0.2417],
           [ 0.9620, -0.2617]], requires_grad=True)],
  'lr': 0.001,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]

2. zero_grad

功能:将梯度清零

w1 = torch.randn(2, 2)
w1.requires_grad = True

w2 = torch.randn(2, 2)
w2.requires_grad = True

optimizer = optim.SGD([w1, w2], lr=0.001, momentum=0.9)

optimizer.param_groups[0]['params'][0].grad = torch.randn(2, 2)

print('参数w1的梯度:')
print(optimizer.param_groups[0]['params'][0].grad, '\n')  # 参数组,第一个参数(w1)的梯度

optimizer.zero_grad()
print('执行zero_grad()之后,参数w1的梯度:')
print(optimizer.param_groups[0]['params'][0].grad)  # 参数组,第一个参数(w1)的梯度
参数w1的梯度:
tensor([[ 2.0855, -1.7181],
        [ 1.4635,  0.1929]]) 

执行zero_grad()之后,参数w1的梯度:
tensor([[0., 0.],
        [0., 0.]])
optimizer.param_groups
[{'params': [tensor([[ 0.7282, -1.6298],
           [-0.8011, -0.4588]], requires_grad=True),
   tensor([[-1.9988, -0.2675],
           [ 0.4767,  0.2058]], requires_grad=True)],
  'lr': 0.001,
  'momentum': 0.9,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]

3. state_dict

功能:获取模型当前的参数,以一个有序字典形式返回。这个有序字典中,key 是各层参数名,value 就是参数。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 1, 3)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(1 * 3 * 3, 2)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = x.view(-1, 1 * 3 * 3)
        x = F.relu(self.fc1(x))
        return x


net = Net()

# 获取网络当前参数
net_state_dict = net.state_dict()
net_state_dict
OrderedDict([('conv1.weight',
              tensor([[[[ 0.1543, -0.1407,  0.0830],
                        [-0.1273, -0.1226, -0.0813],
                        [ 0.0063,  0.0947,  0.0870]],
              
                       [[-0.0373, -0.0405, -0.1581],
                        [-0.1434, -0.0394, -0.0907],
                        [ 0.1879,  0.0017,  0.1906]],
              
                       [[-0.0318, -0.1629, -0.0959],
                        [ 0.1870,  0.0410, -0.0414],
                        [ 0.1877, -0.0737,  0.0832]]]])),
             ('conv1.bias', tensor([0.0677])),
             ('fc1.weight',
              tensor([[-0.0222, -0.0308,  0.3086, -0.0744,  0.1465,  0.2873,  0.1144, -0.1305,
                        0.1582],
                      [-0.1610,  0.2409,  0.0661, -0.1861, -0.2027,  0.1601, -0.2494,  0.1504,
                        0.1627]])),
             ('fc1.bias', tensor([-0.2310,  0.2416]))])

4. load_state_dict

功能:将 state_dict 中的参数加载到当前网络,常用于 finetune。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 1, 3)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(1 * 3 * 3, 2)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = x.view(-1, 1 * 3 * 3)
        x = F.relu(self.fc1(x))
        return x

    def zero_param(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                torch.nn.init.constant_(m.weight.data, 0)
                if m.bias is not None:
                    m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                torch.nn.init.constant_(m.weight.data, 0)
                m.bias.data.zero_()
net = Net()

# 保存,并加载模型参数(仅保存模型参数)
torch.save(net.state_dict(), 'net_params.pkl')   # 假设训练好了一个模型net
pretrained_dict = torch.load('net_params.pkl')

# 将net的参数全部置0,方便对比
net.zero_param()
net_state_dict = net.state_dict()
print('conv1层的权值为:\n', net_state_dict['conv1.weight'], '\n')

# 通过load_state_dict 加载参数
net.load_state_dict(pretrained_dict)
print('加载之后,conv1层的权值变为:\n', net_state_dict['conv1.weight'])
conv1层的权值为:
 tensor([[[[0., 0., 0.],
          [0., 0., 0.],
          [0., 0., 0.]],

         [[0., 0., 0.],
          [0., 0., 0.],
          [0., 0., 0.]],

         [[0., 0., 0.],
          [0., 0., 0.],
          [0., 0., 0.]]]]) 

加载之后,conv1层的权值变为:
 tensor([[[[ 0.1342,  0.0739,  0.1349],
          [-0.0289, -0.0936,  0.1227],
          [-0.0100, -0.1250, -0.1766]],

         [[ 0.1367,  0.0436,  0.1686],
          [ 0.1190, -0.1689, -0.0090],
          [-0.0925,  0.1353, -0.0834]],

         [[ 0.1444, -0.1853,  0.0623],
          [ 0.1150,  0.1841,  0.0029],
          [ 0.1390,  0.1746, -0.0154]]]])
pretrained_dict
OrderedDict([('conv1.weight',
              tensor([[[[ 0.1342,  0.0739,  0.1349],
                        [-0.0289, -0.0936,  0.1227],
                        [-0.0100, -0.1250, -0.1766]],
              
                       [[ 0.1367,  0.0436,  0.1686],
                        [ 0.1190, -0.1689, -0.0090],
                        [-0.0925,  0.1353, -0.0834]],
              
                       [[ 0.1444, -0.1853,  0.0623],
                        [ 0.1150,  0.1841,  0.0029],
                        [ 0.1390,  0.1746, -0.0154]]]])),
             ('conv1.bias', tensor([-0.0964])),
             ('fc1.weight',
              tensor([[ 0.0834,  0.3215, -0.2080,  0.1315,  0.0505, -0.2244,  0.1805,  0.1946,
                        0.0444],
                      [-0.3096,  0.3098,  0.2564, -0.0232,  0.3014,  0.1928,  0.1730,  0.0521,
                        0.0925]])),
             ('fc1.bias', tensor([ 0.3252, -0.1319]))])

5. add_param_group

功能:给 optimizer 管理的参数组中增加一组参数,可为该组参数 定制 lr, momentum, weight_decay 等

w1 = torch.randn(2, 2)
w1.requires_grad = True

w2 = torch.randn(2, 2)
w2.requires_grad = True

w3 = torch.randn(2, 2)
w3.requires_grad = True

# 一个参数组
optimizer_1 = optim.SGD([w1, w2], lr=0.1)
optimizer_1.param_groups
[{'params': [tensor([[-0.5223, -0.5817],
           [-3.5183,  1.2317]], requires_grad=True),
   tensor([[ 0.5794, -1.6020],
           [-0.5890,  1.3211]], requires_grad=True)],
  'lr': 0.1,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]
# 增加一组参数
optimizer_1.add_param_group({
    
    'params': w3, 'lr': 0.001, 'momentum': 0.8})
optimizer_1.param_groups
[{'params': [tensor([[-0.5223, -0.5817],
           [-3.5183,  1.2317]], requires_grad=True),
   tensor([[ 0.5794, -1.6020],
           [-0.5890,  1.3211]], requires_grad=True)],
  'lr': 0.1,
  'momentum': 0,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False},
 {'params': [tensor([[0.6763, 1.1298],
           [0.8228, 0.5904]], requires_grad=True)],
  'lr': 0.001,
  'momentum': 0.8,
  'dampening': 0,
  'weight_decay': 0,
  'nesterov': False}]

具体的优化器使用方法之后再补充

猜你喜欢

转载自blog.csdn.net/weixin_44751294/article/details/120316110
今日推荐