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

本文涉及的产品
函数计算FC,每月15万CU 3个月
简介: 【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即可输出修改后的网络结构,如下图所示:

image.png

比如:

# 定义网络结构
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,全部被冻结。

image.png

那么,现在可以把全部参数送入优化器,优化器也只会对全连接层的数据进行处理。

# 这个优化器只对最后一层的参数进行更新,可以加快训练速度
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}]


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


相关实践学习
【文生图】一键部署Stable Diffusion基于函数计算
本实验教你如何在函数计算FC上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。函数计算提供一定的免费额度供用户使用。本实验答疑钉钉群:29290019867
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
目录
相关文章
|
6月前
|
机器学习/深度学习 自然语言处理
大语言模型(LLM)框架及微调 (Fine Tuning)
大语言模型(LLM)框架及微调 (Fine Tuning)
470 0
|
2月前
|
机器学习/深度学习 存储 自然语言处理
如何微调(Fine-tuning)大语言模型?
本文介绍了微调的基本概念,以及如何对语言模型进行微调。
|
4月前
|
机器学习/深度学习 自然语言处理 异构计算
预训练与微调
预训练与微调
144 5
|
6月前
|
数据采集 人工智能 自然语言处理
【AI大模型应用开发】【Fine-Tuning】0. 从一个例子开始学习大模型Fine-Tuning
【AI大模型应用开发】【Fine-Tuning】0. 从一个例子开始学习大模型Fine-Tuning
182 0
|
机器学习/深度学习 存储 人工智能
人工智能大语言模型微调技术:SFT 监督微调、LoRA 微调方法、P-tuning v2 微调方法、Freeze 监督微调方法
人工智能大语言模型微调技术:SFT 监督微调、LoRA 微调方法、P-tuning v2 微调方法、Freeze 监督微调方法
人工智能大语言模型微调技术:SFT 监督微调、LoRA 微调方法、P-tuning v2 微调方法、Freeze 监督微调方法
|
数据采集 机器学习/深度学习 人工智能
利用GPT-3 Fine-tunes训练专属语言模型
ChatGPT强大的自然语言理解力和表达力,目前只表现在通用领域。一旦进入专业领域,ChatGPT经常“一本正经,胡说八道”。此时用特定领域的知识对模型进行微调是时间成本和经济成本最高的解决方案。
775 0
利用GPT-3 Fine-tunes训练专属语言模型
|
人工智能 自然语言处理 API
有了Fine-tune-CoT方法,小模型也能做推理,完美逆袭大模型
有了Fine-tune-CoT方法,小模型也能做推理,完美逆袭大模型
715 0
|
机器学习/深度学习 自然语言处理 算法
Unsupervised NMT、PBSMT、coarse-to-fine...你都掌握了吗?一文总结机器翻译必备经典模型(1)
Unsupervised NMT、PBSMT、coarse-to-fine...你都掌握了吗?一文总结机器翻译必备经典模型
|
机器学习/深度学习 自然语言处理
Unsupervised NMT、PBSMT、coarse-to-fine...你都掌握了吗?一文总结机器翻译必备经典模型(2)
Unsupervised NMT、PBSMT、coarse-to-fine...你都掌握了吗?一文总结机器翻译必备经典模型
145 0