Datawhale X 李宏毅苹果书 AI夏令营-task02笔记


前言

本笔记是夏令营第五期李宏毅老师《深度学习》的第三章内容的学习

一、理论部分

1.自适应学习率:
零界点不一定是深度学习时的一个最大原因。一般来说,走到零界点,梯度非常小。但是损失不在变小的时候,梯度不一定很小,这是因为可能出现梯度震荡。
为此我们可以通过自适应学习率方法改善。比如AdaGrad、RSMProp、Adam优化算法。

2.学习率调度
之前的学习率都是一个固定的值,其实学习率也和时间有关系。我们常用的方式是学习率衰减(退火),随着参数的跟新,学习率越来越小。还有一种是预热法:学习率先变大,再变小(超参数,手动调整)

3.优化总结:
在梯度下降的公式中添加动量

4.分类与回归:
分类可以当作回归来看,是一个二元问题,softmax是一个归一化【0-1】的函数,计算预测值和目标标签的相似度。

二、分类模型实践

1.基本流程

卷积神经网络是深度学习中的一个非常重要的分支,

  • 准备数据
  • 训练模型
  • 应用模型
    要完成一个深度神经网络训练模型的代码,大概需要完成下面的内容:
  1. 导入所需要的库/工具包
  2. 数据准备与预处理
  3. 定义模型
  4. 定义损失函数和优化器等其他配置
  5. 训练模型
  6. 评估模型
  7. 进行预测

2.代码详解

  1. 导入所需要的库/工具包
  2. 数据准备与预处理
    从指定路径加载图像数据,并对其进行预处理。对图像的预处理操作包括调整大小和将图像转换为Tensor格式。为了增强模型的鲁棒性,可以对训练集进行数据增强。
# 在测试和验证阶段,通常不需要图像增强。
# 我们所需要的只是调整PIL图像的大小并将其转换为Tensor。
test_tfm = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor(),
])

# 不过,在测试阶段使用图像增强也是有可能的。
# 你可以使用train_tfm生成多种图像,然后使用集成方法进行测试。
train_tfm = transforms.Compose([
    # 将图像调整为固定大小(高度和宽度均为128)
    transforms.Resize((128, 128)),
    # TODO:你可以在这里添加一些图像增强的操作。

    # ToTensor()应该是所有变换中的最后一个。
    transforms.ToTensor(),
])
# 构建训练和验证数据集
# "loader" 参数定义了torchvision如何读取数据
train_set = FoodDataset("./hw3_data/train", tfm=train_tfm)
# 创建训练数据加载器,设置批量大小、是否打乱数据顺序、是否使用多线程加载以及是否固定内存地址
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
# 构建验证数据集
# "loader" 参数定义了torchvision如何读取数据
valid_set = FoodDataset("./hw3_data/valid", tfm=test_tfm)
# 创建验证数据加载器,设置批量大小、是否打乱数据顺序、是否使用多线程加载以及是否固定内存地址
valid_loader = DataLoader(valid_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
  1. 定义模型
    定义了一个图像分类器类(Classifier),继承自PyTorch的nn.Module。该分类器通过一系列卷积层、批归一化层、激活函数和池化层构建卷积神经网络(CNN),用于提取图像特征。随后,这些特征被输入到全连接层进行分类,最终输出11个类别的概率,用于图像分类任务。
class Classifier(nn.Module):
    """
    定义一个图像分类器类,继承自PyTorch的nn.Module。
    该分类器包含卷积层和全连接层,用于对图像进行分类。
    """
    def __init__(self):
        """
        初始化函数,构建卷积神经网络的结构。
        包含一系列的卷积层、批归一化层、激活函数和池化层。
        """
        super(Classifier, self).__init__()
        # 定义卷积神经网络的序列结构
        self.cnn = nn.Sequential(
            nn.Conv2d(3, 64, 3, 1, 1),  # 输入通道3,输出通道64,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(64),        # 批归一化,作用于64个通道
            nn.ReLU(),                 # ReLU激活函数
            nn.MaxPool2d(2, 2, 0),      # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(64, 128, 3, 1, 1), # 输入通道64,输出通道128,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(128),        # 批归一化,作用于128个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),      # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(128, 256, 3, 1, 1), # 输入通道128,输出通道256,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(256),        # 批归一化,作用于256个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),      # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(256, 512, 3, 1, 1), # 输入通道256,输出通道512,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(512),        # 批归一化,作用于512个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),       # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(512, 512, 3, 1, 1), # 输入通道512,输出通道512,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(512),        # 批归一化,作用于512个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),       # 最大池化,池化窗口大小2,步长2,填充0
        )
        # 定义全连接神经网络的序列结构
        self.fc = nn.Sequential(
            nn.Linear(512*4*4, 1024),    # 输入大小512*4*4,输出大小1024
            nn.ReLU(),
            nn.Linear(1024, 512),        # 输入大小1024,输出大小512
            nn.ReLU(),
            nn.Linear(512, 11)           # 输入大小512,输出大小11,最终输出11个类别的概率
        )

    def forward(self, x):
        """
        前向传播函数,对输入进行处理。
        
        参数:
        x -- 输入的图像数据,形状为(batch_size, 3, 128, 128)
        
        返回:
        输出的分类结果,形状为(batch_size, 11)
        """
        out = self.cnn(x)               # 通过卷积神经网络处理输入
        out = out.view(out.size()[0], -1)  # 展平输出,以适配全连接层的输入要求
        return self.fc(out)             # 通过全连接神经网络得到最终输出
  1. 定义损失函数和优化器等其他配置

图像分类模型的初始化和训练配置,目的是准备好训练环境和参数。它选择合适的设备(GPU或CPU),设置模型、批量大小、训练轮数、提前停止策略,定义了损失函数和优化器,为后续的模型训练奠定了基础。

  1. 训练模型
    这段代码实现了一个图像分类模型的训练和验证循环,目的是通过多轮训练(epochs)逐步优化模型的参数,以提高其在验证集上的性能,并保存效果最好的模型。训练阶段通过前向传播、计算损失、反向传播和参数更新来优化模型,验证阶段评估模型在未见过的数据上的表现。如果验证集的准确率超过了之前的最好成绩,保存当前模型,并在连续多轮验证性能未提升时提前停止训练。
# 初始化追踪器,这些不是参数,不应该被更改
stale = 0
best_acc = 0

for epoch in range(n_epochs):
    # ---------- 训练阶段 ----------
    # 确保模型处于训练模式
    model.train()

    # 这些用于记录训练过程中的信息
    train_loss = []
    train_accs = []

    for batch in tqdm(train_loader):
        # 每个批次包含图像数据及其对应的标签
        imgs, labels = batch
        # imgs = imgs.half()
        # print(imgs.shape,labels.shape)

        # 前向传播数据。(确保数据和模型位于同一设备上)
        logits = model(imgs.to(device))

        # 计算交叉熵损失。
        # 在计算交叉熵之前不需要应用softmax,因为它会自动完成。
        loss = criterion(logits, labels.to(device))

        # 清除上一步中参数中存储的梯度
        optimizer.zero_grad()

        # 计算参数的梯度
        loss.backward()

        # 为了稳定训练,限制梯度范数
        grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)

        # 使用计算出的梯度更新参数
        optimizer.step()

        # 计算当前批次的准确率
        acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

        # 记录损失和准确率
        train_loss.append(loss.item())
        train_accs.append(acc)

    train_loss = sum(train_loss) / len(train_loss)
    train_acc = sum(train_accs) / len(train_accs)

    # 打印信息
    print(f"[ 训练 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")
  1. 评估模型
# ---------- 验证阶段 ----------
    # 确保模型处于评估模式,以便某些模块如dropout被禁用,模型能够正常工作
    model.eval()

    # 这些用于记录验证过程中的信息
    valid_loss = []
    valid_accs = []

    # 按批次迭代验证集
    for batch in tqdm(valid_loader):
        # 每个批次包含图像数据及其对应的标签
        imgs, labels = batch
        # imgs = imgs.half()

        # 我们在验证阶段不需要梯度。
        # 使用 torch.no_grad() 加速前向传播过程。
        with torch.no_grad():
            logits = model(imgs.to(device))

        # 我们仍然可以计算损失(但不计算梯度)。
        loss = criterion(logits, labels.to(device))

        # 计算当前批次的准确率
        acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

        # 记录损失和准确率
        valid_loss.append(loss.item())
        valid_accs.append(acc)
        # break

    # 整个验证集的平均损失和准确率是所记录值的平均
    valid_loss = sum(valid_loss) / len(valid_loss)
    valid_acc = sum(valid_accs) / len(valid_accs)

    # 打印信息
    print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

    # 更新日志
    if valid_acc > best_acc:
        with open(f"./{_exp_name}_log.txt", "a"):
            print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> 最佳")
    else:
        with open(f"./{_exp_name}_log.txt", "a"):
            print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# 李老师的课程原文件里面确实缺少write,如果想要log文件里面有内容,可以按照下面的参考,此部分不是重点
#if valid_acc > best_acc:
#    with open(f"./{_exp_name}_log.txt", "a") as log_file:
#        log_file.write(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> best\n")
#    print(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> best")
#else:
#    with open(f"./{_exp_name}_log.txt", "a") as log_file:
#        log_file.write(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}\n")
#    print(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")




    # 保存模型
    if valid_acc > best_acc:
        print(f"在第 {epoch} 轮找到最佳模型,正在保存模型")
        torch.save(model.state_dict(), f"{_exp_name}_best.ckpt")  # 只保存最佳模型以防止输出内存超出错误
        best_acc = valid_acc
        stale = 0
    else:
        stale += 1
        if stale > patience:
            print(f"连续 {patience} 轮没有改进,提前停止")
            break

7.进行预测
构建一个测试数据集和数据加载器,以便高效地读取数据。实例化并加载预训练的分类器模型,并将其设置为评估模式。在不计算梯度的情况下,遍历测试数据,使用模型进行预测,并将预测标签存储在列表中。将预测结果与测试集的ID生成一个DataFrame,并将其保存为submission.csv文件。


总结

本次学习有个疑问就是,如何将这些理论知识用于实践中呢。

  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值