什么是微调模型

本文介绍了微调预训练模型的方法,包括如何在PyTorch中保存和加载模型,下载第三方模型,以及在不同训练策略下(如微调所有参数、冻结部分参数等)如何进行模型调整和训练。重点在于理解和应用模型的参数保存与加载,以及在新任务上进行有效微调的技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

微调模型

例如:第一章 微调模型



前提:微调模型

微调(fine-tuning)的步骤

  1. 在源数据集(例如ImageNet数据集)上预训练神经网络模型,即源模型。
  2. 创建一个新的神经网络模型,即目标模型。这将复制源模型上的所有模型设计及其参数(输出层除外)。我们假定这些模型参数包含从源数据集中学到的知识,这些知识也将适用于目标数据集。我们还假设源模型的输出层与源数据集的标签密切相关;因此不在目标模型中使用该层。
  3. 向目标模型添加输出层,其输出数是目标数据集中的类别数。然后随机初始化该层的模型参数。
  4. 在目标数据集(如椅子数据集)上训练目标模型。输出层将从头开始进行训练,而所有其他层的参数将根据源模型的参数进行微调。

一、保存自己训练出的模型并加载模型

要保存自己训练的模型,您可以使用PyTorch提供的torch.save()函数来保存模型的状态字典或整个模型。通常,保存模型的状态字典是更常见的做法,因为它仅包含模型的参数,而不包含模型的结构,这样在加载时可以更加灵活地构建模型。

1、保存【模型框架+保存模型的状态字典(参数)】并加载模型

(1)保存模型框架+保存模型的状态字典(参数)

希望将模型的框架和状态参数都保存下来
可以使用torch.save()函数保存整个模型,包括模型的结构和状态参数。示例代码如下:

import torch

# 假设您的模型为model,并且希望保存整个模型
torch.save(model, "path/to/save/complete_model.pth")

在这个示例中,model是您已经训练好的模型。torch.save()函数将模型保存到名为"model_params.pth"的文件中。您可以根据需要更改文件路径和文件名。

(2)加载模型

要加载模型,您可以使用torch.load()函数加载整个模型

# 加载保存的整个模型
loaded_model = torch.load("path/to/save/complete_model.pth")

2、保存【保存模型的状态字典(参数)】并加载模型

(1)保存模型的状态字典

希望将模型的状态参数保存下来,但不保存框架
这种情况下,您可以直接使用torch.save()函数保存模型的状态字典,不需要保存整个模型的结构。示例代码如下:

# 假设您的模型为model,并且希望保存模型的框架
torch.save(model.state_dict(), "path/to/save/model_structure.pth")

(2)加载模型

请注意,当您需要重新加载模型时,您需要确保重新创建一个与原始模型相同结构的新模型,然后再使用load_state_dict()函数加载保存的参数。这样可以保证参数的维度匹配,从而正确加载模型的状态字典。

# 创建新的模型实例
new_model = YourModel()  # 替换成您的模型的实例化方法

# 加载保存的模型结构
new_model.load_state_dict(torch.load("path/to/save/model_structure.pth"))

3、保存【保存模型的框架】并加载模型

(1)保存模型的框架

希望将模型的框架保存下来,不保存状态参数
如果只想保存模型的结构而不包括状态参数,可以直接使用torch.save()函数保存整个模型,不必使用state_dict()方法。示例代码如下:

# 假设您的模型为model,并且希望保存整个模型
torch.save(model, "path/to/save/complete_model.pth")

(2)加载模型

在这个示例中,model是您已经训练好的模型。**torch.save()函数将整个模型保存到名为"model_structure.pth"的文件中**。要加载模型结构,您可以直接使用torch.load()函数加载整个模型:

# 加载保存的模型结构
loaded_model = torch.load("path/to/save/model_structure.pth")

不保存状态参数的话,加载时模型的参数将是随机初始化的。所以在加载模型后,需要自行根据需求对模型参数进行初始化

二、下载第三方提供的模型框架和参数

1、使用torchvision下载模型

(1)pretrained决定是否下载参数

pretrained=True  #-----> 将模型的框架+模型的参数 都下载下来
pretrained=False  #-----> 只将模型的框架下载下来

(2)下载模型

pretrained_net = torchvision.models.resnet18(pretrained=True)
print(pretrained_net)
# 输出
ResNet(
  (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu): ReLU(inplace=True)
  (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (layer1): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
    (1): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer2): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer3): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer4): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (fc): Linear(in_features=512, out_features=1000, bias=True)
)

三、创建目标模型

1、如何在原有模型后面再添加一层或者多层

import torch
import torch.nn as nn

# 假设您的模型为model,并且希望加载保存的模型参数
class YourModel(nn.Module):
    def __init__(self):
        super(YourModel, self).__init__()
        # 定义您的模型的结构,包括您希望保存的模型部分和新增的部分

        # 示例:保存的模型部分
        self.features = nn.Sequential(
            # 定义保存的模型的层
            nn.Conv2d(3, 64, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(64, 128, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
        )

        # 示例:新增的部分(还未指定输出特征数)
        self.new_layer = nn.Sequential(
            # 新增的层,输出特征数暂未指定
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.new_layer(x)
        return x

# 创建新的模型实例
model = YourModel()

# 加载之前保存的模型参数
saved_model_state = torch.load("path/to/save/model_params.pth")
model.features.load_state_dict(saved_model_state)  # 加载保存的模型部分的参数

# 获取保存的模型部分的输出特征数
# 假设模型的输出特征数在保存的模型中的全连接层中
# 如果输出特征数在其他层中,相应地进行调整
saved_output_features = saved_model_state['fc.weight'].size(1)

# 在新的模型实例后面添加新增的层
model.new_layer = nn.Sequential(
    nn.Linear(saved_output_features, 256),
    nn.ReLU(),
    nn.Linear(256, num_classes)  # 假设 num_classes 是新任务的类别数
)

# 可以在新增的层后面继续添加其他层,根据需要进行设计

# 现在 model 就是加载了保存的模型参数并在后面添加了新层的模型
# 您可以使用 model 进行微调或其他任务


核心操作在于

1、 首先重新创建一个新的模型类
2、将旧的模型框架包装在一个nn.Sequential
3、将新添加的模型框架包装在一个nn.Sequential中(可以直接写好新添加的模型框架到目标模型类中,也可以后续添加)

2、如何修改原有模型的最后一层

# 假设您的模型为model,并且希望修改最后一层的框架
in_features = model.fc.in_features  # 获取最后一层的输入特征数
out_features = 10  # 新的输出特征数
model.fc = nn.Linear(in_features, out_features)  # 替换最后一层为新的全连接层

四、冻结参数+从头开始训练

核心在于优化函数和

1、微调源模型的参数+从头开始训练新加的层

finetune_net = torchvision.models.resnet18(pretrained=True)
# 修改模型最后一层
finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2)
nn.init.xavier_uniform_(finetune_net.fc.weight)

# 如果param_group=True,输出层中的模型参数将使用十倍的学习率
def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5,
                      param_group=True):
    train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'train'), transform=train_augs),
        batch_size=batch_size, shuffle=True)
    test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'test'), transform=test_augs),
        batch_size=batch_size)
    devices = d2l.try_all_gpus()
    loss = nn.CrossEntropyLoss(reduction="none")
    if param_group:
        params_1x = [param for name, param in net.named_parameters()
             if name not in ["fc.weight", "fc.bias"]]
        trainer = torch.optim.SGD([{'params': params_1x},
                                   {'params': net.fc.parameters(),
                                    'lr': learning_rate * 10}],
                                lr=learning_rate, weight_decay=0.001)
    else:
        trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
                                  weight_decay=0.001)
    d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
                   devices)
train_fine_tuning(finetune_net, 5e-5)

微调源模型的所有参数

在这里插入图片描述

2、冻结源模型的所有参数+从头开始训练新加的层

代码如下(示例):

freeze_all_net = torchvision.models.resnet18(pretrained=True)
freeze_all_net.fc = nn.Linear(finetune_net.fc.in_features, 2)
nn.init.xavier_uniform_(freeze_all_net.fc.weight);
# 如果原本的模型的参数全部冻结
# 如果param_group=True,输出层中的模型参数将使用十倍的学习率
def train_freeze_all(net, learning_rate, batch_size=128, num_epochs=5,
                      param_group=True):
    train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'train'), transform=train_augs),
        batch_size=batch_size, shuffle=True)
    test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'test'), transform=test_augs),
        batch_size=batch_size)
    devices = d2l.try_all_gpus()
    loss = nn.CrossEntropyLoss(reduction="none")
    # for name, param in pretrained_net.named_parameters():
    #     if "fc" not in name:  # 排除输出层的参数
    #         param.requires_grad = False

    if param_group:
        trainer = torch.optim.SGD([{'params': net.fc.parameters(),
                                    'lr': learning_rate * 10}],
                                lr=learning_rate, weight_decay=0.001)
    else:
        trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
                                  weight_decay=0.001)
    d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
                   devices)
train_freeze_all(freeze_all_net, 5e-5)

冻结模型的全部参数

在这里插入图片描述


3、冻结源模型的部分参数+从头开始训练新添加的层

下面是冻源模型的15%参数,保证其不改变

import random
freeze_some_net = torchvision.models.resnet18(pretrained=True)
freeze_some_net.fc = nn.Linear(finetune_net.fc.in_features, 2)
nn.init.xavier_uniform_(freeze_some_net.fc.weight)
total_params = sum(p.numel() for p in pretrained_net.parameters())
params_to_freeze = int(0.15 * total_params)
all_params = list(pretrained_net.parameters())
random.shuffle(all_params)
# 冻结前 params_to_freeze 个参数
for param in all_params[:params_to_freeze]:
    param.requires_grad = False
# 如果原本的模型的参数全部冻结
# 如果param_group=True,输出层中的模型参数将使用十倍的学习率
def train_freeze_some(net, learning_rate, batch_size=128, num_epochs=5,
                      param_group=True):
    train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'train'), transform=train_augs),
        batch_size=batch_size, shuffle=True)
    test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'test'), transform=test_augs),
        batch_size=batch_size)
    devices = d2l.try_all_gpus()
    loss = nn.CrossEntropyLoss(reduction="none")
    # for name, param in pretrained_net.named_parameters():
    #     if "fc" not in name:  # 排除输出层的参数
    #         param.requires_grad = False

    if param_group:
        trainer = torch.optim.SGD([
            {'params': filter(lambda p: p.requires_grad, pretrained_net.parameters())},
            {'params': net.fc.parameters(),
                                    'lr': learning_rate * 10}],
                                lr=learning_rate, weight_decay=0.001)
    else:
        trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
                                  weight_decay=0.001)
    d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
                   devices)
train_freeze_some(freeze_some_net , 5e-5)

冻结15%的参数的结果:

在这里插入图片描述

冻结5%的参数的结果

在这里插入图片描述

4、整个目标模型全部重新训练

scratch_net = torchvision.models.resnet18()
scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2)
# 如果原本的模型的参数全部冻结
# 如果param_group=True,输出层中的模型参数将使用十倍的学习率
def train(net, learning_rate, batch_size=128, num_epochs=5,
                      param_group=True):
    train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'train'), transform=train_augs),
        batch_size=batch_size, shuffle=True)
    test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
        os.path.join(data_dir, 'test'), transform=test_augs),
        batch_size=batch_size)
    devices = d2l.try_all_gpus()
    loss = nn.CrossEntropyLoss(reduction="none")
    trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
                                  weight_decay=0.001)
    d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
                   devices)
train(scratch_net, 5e-5)

目标模型全部重新训练的结果:

在这里插入图片描述

总结

1、微调源模型的所有参数:
loss 0.245, train acc 0.903, test acc 0.931
2、固定源模型的部分参数:
固定5%:loss 0.393, train acc 0.881, test acc 0.890
固定15%:loss 0.322, train acc 0.897, test acc 0.874
固定50%:loss 0.295, train acc 0.901, test acc 0.865
固定75%:loss 0.635, train acc 0.834, test acc 0.899
3. 固定源模型的全部参数(不然在模型训练的过程中发生改变)
loss 0.319, train acc 0.889, test acc 0.910
4.目标模型中的参数全部重新训练
loss 0.369, train acc 0.852, test acc 0.856

总结
1、如果新任务与预训练任务相似,且预训练模型在大规模数据上表现良好,则微调原有模型的所有参数通常是一个较好的选择,因为它在较少的训练数据上可以取得较好的效果。
2、如果任务复杂性较低,微调原本模型的15%参数可能足以达到较好的性能,并且可以减少训练成本。
3、而如果任务与预训练任务差异较大,或者没有合适的预训练模型可用,那么全部从头开始训练可能是更好的选择,尽管它需要更多的计算资源和训练时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值