一·迁移学习代码逻辑的讲解

resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)  # 即调用了resnet18网络,又使用了训练好的模型
# weights=models.ResNet18_Weights.DEFAULT表示使用在 ImageNet 数据集上预先训练好的权重来初始化模型参数,可进入源代码查看

1、选择预训练的模型和适当的层:通常,我们会选择在大规模图像数据集(如ImageNet)上预训练的模型,如VGG、ResNet等。然后,根据新数据集的特点,选择需要微调的模型层。对于低级特征的任务(如边缘检测),最好使用浅层模型的层,而对于高级特征的任务(如分类),则应选择更深层次的模型。

for param in resnet_model.parameters():  # 逐层去除权重参数
    print(param)
    param.requires_grad = False  # 冻结
# 模型所有参数(即权重和偏差)的requires_grad属性设置为False,从而冻结所有模型参数。

2、冻结预训练模型的参数:保持预训练模型的权重不变,只训练新增加的层或者微调一些层,避免因为在数据集中过拟合导致预训练模型过度拟合。

import torch
import torchvision.models as models
from torch import nn
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import numpy as np

# 不再需要自己来搭建模型了。预训练的文件也加载进去了。
'''将resnet18模型迁移到食物分类项目中'''  # 残差网络是固定的网络结构,不需要你自己来类定义了。
resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)  # 即调用了resnet18网络,又使用了训练好的模型
# weights=models.ResNet18_Weights.DEFAULT表示使用在 ImageNet 数据集上预先训练好的权重来初始化模型参数,可进入源代码查看
for param in resnet_model.parameters():  # 逐层去除权重参数
    print(param)
    param.requires_grad = False  # 冻结
# 模型所有参数(即权重和偏差)的requires_grad属性设置为False,从而冻结所有模型参数。

in_features = resnet_model.fc.in_features  # 获取模型原输入的特征个数
resnet_model.fc = nn.Linear(in_features, 20)  # 创建一个全连接层,输入特征为in_features,输出为20

params_to_update = []  # 保存需要训练的参数,仅仅包含全连接层的参数
for param in resnet_model.parameters():
    if param.requires_grad == True:
        params_to_update.append(param)
# 不冻结,需要全部重头训练,注释13、14、20~23行,并修改82行代码

data_transforms = {
    'train':
        transforms.Compose([
            transforms.Resize([300, 300]),
            transforms.RandomRotation(45),
            transforms.CenterCrop(244),
            transforms.RandomHorizontalFlip(p=0.5),
            transforms.RandomVerticalFlip(p=0.5),
            transforms.RandomGrayscale(p=0.1),
            transforms.ToTensor(),
            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
        ]),
    'valid':
        transforms.Compose([
            transforms.Resize([244, 244]),
            transforms.ToTensor(),
            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
        ]),
}


class food_dataset(Dataset):  # food_dataset是自己创建的类名称,可以改为你需要的名称
    def __init__(self, file_path, transform=None):  # 类的初始化,解析数据文件txt
        self.file_path = file_path
        self.imgs = []  # 存储图片的路径
        self.labels = []  # 存储图片的标签结果
        self.transform = transform
        with open(self.file_path) as f:  # 是把train.txt文件中图片的路径保存在 self.imgs,train.txt文件中标签保
            samples = [x.strip().split(' ') for x in f.readlines()]
            for img_path, label in samples:
                self.imgs.append(img_path)  # 图像的路径
                self.labels.append(label)  # 标签,还不是tensor
    # 初始化:把图片目录加载到self.
    def __len__(self):  # 类实例化对象后,可以使用len函数测量对象的个数 ls=[12,3,4,4] len(training_data)
        return len(self.imgs)
    # training_data[1]
    def __getitem__(self, idx):  # 关键,可通过索引的形式获取每一个图片数据及标签
        image = Image.open(self.imgs[idx])  # 读取到图片数据,还不是tensor,BGR
        if self.transform:  # 将pil图像数据转换为tensor
            image = self.transform(image)  # 图像处理为256*256,转换为tenor
        label = self.labels[idx]  # label还不是tensor
        label = torch.from_numpy(np.array(label, dtype=np.int64))  # label也转换为tensor,
        return image, label

training_data = food_dataset(file_path = './trainda.txt',transform = data_transforms['train'])
test_data = food_dataset(file_path = './testda.txt',transform = data_transforms['valid'])

train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")

model = resnet_model.to(device)

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(params_to_update, lr=0.001)
# optimizer = torch.optim.Adam(resnet_model.parameters(), lr=0.001)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)  # 调整学习率函数


def train(dataloader, model, loss_fn, optimizer):
    model.train()
    # batch_size_num = 1
    for X, y in dataloader:
        X, y = X.to(device), y.to(device)
        pred = model(X)
        loss = loss_fn(pred, y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        loss_value = loss.item()
        # if batch_size_num % 100 == 0:
        #     print(f"loss: {loss_value}  [number: {batch_size_num}]")
        # batch_size_num += 1

best_acc = 0
def test(dataLoader, model, loss_fn):
    global best_acc
    size = len(dataLoader.dataset)
    num_batches = len(dataLoader)
    model.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in dataLoader:
            X, y = X.to(device), y.to(device)
            pred = model.forward(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= num_batches
    correct /= size
    print(f"Test result: \n Accuracy: {100 * correct}%, Avg loss: {test_loss}")
    acc_s.append(correct)
    loss_s.append(test_loss)

    if correct > best_acc:
        best_acc =correct


loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer,step_size=5, gamma=0.5)

epochs = 10
acc_s = []
loss_s = []

for t in range(epochs):
    print(f"Epoch {t + 1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    scheduler.step()
print(best_acc)
test(test_dataloader, model, loss_fn)

1. 导入必要的库

代码开头导入了 PyTorch 及其相关模块,包括神经网络 (nn)、数据加载工具 (Dataset, DataLoader)、图像变换工具 (transforms) 以及 PIL 图像处理库等。

2. 迁移学习模型构建

python

运行

resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

这里加载了预训练的 ResNet18 模型,weights参数指定使用在 ImageNet 数据集上预训练好的权重。

for param in resnet_model.parameters():
    param.requires_grad = False  # 冻结预训练参数

冻结了 ResNet18 的所有参数,这是迁移学习的常用策略,避免破坏预训练好的特征提取能力。

in_features = resnet_model.fc.in_features  # 获取原全连接层输入特征数
resnet_model.fc = nn.Linear(in_features, 20)  # 替换为新的全连接层,输出20类

替换了 ResNet18 的最后一层全连接层,将输出类别数改为 20(食物分类的类别数)

params_to_update = []  # 收集需要训练的参数(仅新的全连接层参数)
for param in resnet_model.parameters():
    if param.requires_grad == True:
        params_to_update.append(param)

收集需要训练的参数,由于前面冻结了所有参数,这里实际只包含新全连接层的参数。

3. 数据预处理

data_transforms = {
    'train': transforms.Compose([...]),  # 训练集的数据增强
    'valid': transforms.Compose([...])   # 验证集的简单处理
}

定义了数据变换管道,训练集使用了多种数据增强技术(旋转、裁剪、翻转等)来提高模型泛化能力,验证集则只进行简单的尺寸调整和标准化。

4. 自定义数据集类

class food_dataset(Dataset):
    def __init__(self, file_path, transform=None):
        # 从文件中读取图片路径和标签
        ...
    def __len__(self):
        # 返回数据集大小
        ...
    def __getitem__(self, idx):
        # 读取图片并应用变换,返回图片和标签的tensor
        ...

自定义了数据集类,用于加载食物图像数据,实现了标准的__init____len____getitem__方法。

5. 数据加载器

training_data = food_dataset(file_path='./trainda.txt', transform=data_transforms['train'])
test_data = food_dataset(file_path='./testda.txt', transform=data_transforms['valid'])

train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

创建了训练集和测试集的数据加载器,用于按批次加载数据,batch_size=64表示每批加载 64 张图片,shuffle=True表示打乱数据顺序。

6. 设备配置

device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
model = resnet_model.to(device)

自动选择训练设备(优先 GPU,其次是 MPS,最后是 CPU),并将模型移动到选定的设备上。

7. 训练配置

loss_fn = nn.CrossEntropyLoss()  # 交叉熵损失函数,适用于分类任务
optimizer = torch.optim.Adam(params_to_update, lr=0.001)  # Adam优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)  # 学习率调度器

定义了损失函数、优化器和学习率调度器,学习率调度器会每 5 个 epoch 将学习率减半。

8. 训练和测试函数

  • train函数:实现模型的训练过程,包括前向传播、计算损失、反向传播和参数更新。
  • test函数:实现模型的测试过程,计算测试集上的准确率和平均损失,并保存最佳准确率的模型。

9. 模型训练和评估

epochs = 10  # 训练10个epoch
for t in range(epochs):
    print(f"Epoch {t + 1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    scheduler.step()
test(test_dataloader, model, loss_fn)  # 最后在测试集上评估

循环训练模型 10 个 epoch,每个 epoch 结束后更新学习率,最后在测试集上评估模型性能。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐