Task2
一、优化失败的原因二
① 临界点(局部最小值或鞍点)处,损失无法再下降。
② 若梯度不断来回“震荡”,虽然损失不再下降,但梯度并没有变得很小,大概就是说损失可以下降但步伐太大走不进谷底。
③ 或者,B→C 梯度平缓,学习率太小导致下降不动了。
所以,如果在某一方向上梯度的值很小,非常平坦,我们会希望学习率调大一点;如果在某一方向上梯度的值很大,非常陡峭,我们会希望学习率调小一点。
二、解决学习率问题的方法
总结在最前面,最优改进结果:
方法一:自适应学习率
梯度更新公式增加,其与参数类别(i)、迭代轮次(t)相关。即:
AdaGrad
采取梯度的均方根。那梯度大的时候更新率就慢,梯度小的时候更新率就快。
- 缺点1:若在某一个方向上,梯度突然变大或变小,学习率是需要及时调整的。而均方根认为过往每一个梯度同等重要,可能会掩盖当前梯度的大小变化,不能及时调整学习率。(改进:RMSprop)
- 缺点2:AdaGrad虽然可以从B往C走了,但在快走到终点时突然爆炸了。因为BC段纵轴的方向梯度很小,累积了很小的σ,累计到一定程度后步伐就会变得很大,走到梯度很大的地方,σ又变大了,则更新率又修正回原来较小的值。循环往复,呈现出爆炸的样子。(改进:退火)
RMSprop
加权版均方根,可以调整每个梯度的重要性。
- 是一个需要调整的超参数。越小,模型越看重当前梯度,则能根据梯度的大小变化及时调整学习率。
Adam
看作RMSprop + 动量,即以动量作为参数更新方向,且学习率自适应。
- 之前做机器学习任务的时候好像都是默认这个优化器。
- m考虑梯度大小&方向,σ只考虑梯度大小,二者的效果不会相互抵消。
方法二:学习率调度
在自适应学习率基础上,η 变成 ,与更新轮次/时间有关,即:
学习率退火
随参数不断更新,让 η 越来越小。
学习率预热
随参数不断更新,让 η 先变大后变小。
- 原因:σ是一个统计结果,一开始数据不足则σ是不精准的,需要较小的学习率来探索收集一些误差表面的基本情报,等σ比较精准后再让学习率爬升。
- η 变到多大、变大的速度、变小的速度,都是超参数。
三、回归和分类的网络结构
回归
分类
one-hot 编码
避免用数字标号,出现1与2更相近、与3更相远的假象(其实只是三种平行的类别)。
网络架构
把回归中输出一个数值的方法,重复三次()。即:把a1、a2、a3乘上三个不同的权重,加上偏置,得到 ;再分别乘上另外三个权重,加上另一个偏置,得到 ;...
Softmax
通常不直接比较上述 与 之间的距离,而是用softmax转化为 ,比较 与 之间的距离。
- 优点:① y是独热的0和1,softmax可以把 归一化到0~1之间,方便比较;② softmax可以让大的值跟小的值的差距更大。
- 当只有两个类时,sigmoid和softmax等价。
损失函数
① 均方误差:
② 交叉熵(最小化交叉熵=最大化似然):
- 改变损失函数可以改变优化的难度。交叉熵通常比均方误差更好训练分类任务;选均方误差的时候最好用Adam优化器,避免优化失败。
四、实践:CNN图像分类
创建实例:阿里云PAI-DSW → 管理控制台
!git lfs install
!git clone https://www.modelscope.cn/datasets/Datawhale/LeeDL-HW3-CNN.git
# 创建文件夹并解压缩数据集,忽略输出
!wget -O hw3_data.zip !"https://www.modelscope.cn/datasets/Datawhale/LeeDL-HW3-CNN/resolve/master/hw3_data.zip"
!mkdir -p ./hw3_data
!unzip -o hw3_data.zip -d ./hw3_data > /dev/null
-
数据集含11个食物类别,编号0~10。
train : 10000张图片
valid:3643张图片
test:3000张图片
① 导入库
import numpy as np
import pandas as pd
import torch
import os
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
from torch.utils.data import ConcatDataset, DataLoader, Subset, Dataset
from torchvision.datasets import DatasetFolder, VisionDataset
from tqdm.auto import tqdm
import random
- “ConcatDataset” 和 “Subset” 在进行半监督学习时可能是有用的。
myseed = 6666
# 确保在使用CUDA时,卷积运算具有确定性,以增强实验结果的可重复性
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# 为numpy和pytorch设置随机种子
np.random.seed(myseed)
torch.manual_seed(myseed)
# 如果使用CUDA,为所有GPU设置随机种子
if torch.cuda.is_available():
torch.cuda.manual_seed_all(myseed)
- 设置随机数种子,可确保实验结果的可重复性。
② 定义图像数据预处理方法
# 在valid和test阶段,通常不需要图像增强。
# 我们所需要的只是调整PIL图像的大小并将其转换为Tensor。
test_tfm = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
])
# 不过,在test阶段使用图像增强也是有可能的。
# 你可以使用train_tfm生成多种图像,然后使用集成方法进行测试。
train_tfm = transforms.Compose([
transforms.Resize((128, 128)), # 将图像调整为固定大小
# 你可以在这里添加一些图像增强的操作。
transforms.ToTensor(), # ToTensor() 应该是所有变换中的最后一个
])
- Torchvision为图像预处理、数据增强和数据加载提供了一系列的API(torchvision-transforms)。
- 张量(tensor),即元素为相同类型的多维矩阵。
class FoodDataset(Dataset):
"""
用于加载食品图像数据集的类。
该类继承自Dataset,提供了对食品图像数据集的加载和预处理功能。
它可以自动从指定路径加载所有的jpg图像,并对这些图像应用给定的变换。
"""
def __init__(self, path, tfm=test_tfm, files=None):
"""
初始化FoodDataset实例。
- path: 图像数据所在的目录路径。
- tfm: 应用于图像的变换方法(默认为test变换)。
- files: 可选参数,用于直接指定图像文件的路径列表(默认为None)。
"""
super(FoodDataset).__init__()
self.path = path
# 列出目录下所有jpg文件,并按顺序排序
self.files = sorted([os.path.join(path, x) for x in os.listdir(path) if x.endswith(".jpg")])
if files is not None:
self.files = files # 如果提供了文件列表,则使用该列表
self.transform = tfm # 图像变换方法
def __len__(self):
"""返回数据集中图像的数量。"""
return len(self.files)
def __getitem__(self, idx):
"""
获取给定索引的图像及其标签。
参数:
idx: 图像在数据集中的索引。
返回:
im: 应用了变换后的图像。
label: 图像对应的标签(若无,返回-1)。
"""
fname = self.files[idx]
im = Image.open(fname)
im = self.transform(im) # 应用图像变换
# 尝试从文件名中提取标签
try:
label = int(fname.split("/")[-1].split("_")[0])
# split(/)是去掉文件路径,split(_)是因为图片文件名是"1_994"的格式,1代表类别
except:
label = -1 # 如果无法提取标签,则设置为-1(测试数据无标签)
return im, label
③ 定义模型结构
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) # 通过全连接神经网络得到最终输出
- 卷积神经网络处理输入,5层;全连接神经网络得到输出,2层。
④ 训练前配置(损失函数、优化器、数据集)
# 定义实验名称,用于标识实验配置或运行的标识符
_exp_name = "sample"
# 根据GPU是否可用选择设备类型
device = "cuda" if torch.cuda.is_available() else "cpu"
# 初始化模型,并将其放置在指定的设备上
model = Classifier().to(device)
# 定义批量大小
batch_size = 64
# 定义训练轮数
n_epochs = 8
# 如果在'patience'轮中没有改进,则提前停止
patience = 5
# ① 对于分类任务,使用交叉熵作为损失函数
criterion = nn.CrossEntropyLoss()
# ② 初始化优化器(Adam),您可以自行调整一些超参数,如学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)
# ③ 构建训练和验证数据集
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)
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)
DataLoader数据加载器:设置批量大小、是否打乱数据顺序、是否使用多线程加载、是否固定内存地址(提高数据加载效率)
⑤ 训练模型
# 初始化追踪器,这些不是参数,不应该被更改
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}")
# ---------- 验证阶段 ----------
# 确保模型处于评估模式,以便某些模块如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}")
# 保存模型
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
-
训练阶段通过前向传播、计算损失、反向传播和参数更新来优化模型,验证阶段评估模型在未见过的数据上的表现。如果验证集的准确率超过了之前的最好成绩,保存当前模型,并在连续多轮验证性能未提升时提前停止训练。
-
最佳模型在第5轮找到,验证集准确率为0.59。
⑥ 测试集评估
test_set = FoodDataset("./hw3_data/test", tfm=test_tfm)
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True)
# 实例化分类器模型,并将其转移到指定的设备上
model_best = Classifier().to(device)
# 加载模型的最优状态字典
model_best.load_state_dict(torch.load(f"{_exp_name}_best.ckpt"))
# 将模型设置为评估模式
model_best.eval()
# 初始化一个空列表,用于存储所有预测标签
prediction = []
with torch.no_grad():
for data, _ in tqdm(test_loader):
# 将数据转移到指定设备上,并获得模型的预测结果
test_pred = model_best(data.to(device))
# 选择具有最高分数的类别作为预测标签
test_label = np.argmax(test_pred.cpu().data.numpy(), axis=1)
# 将预测标签添加到结果列表中
prediction += test_label.squeeze().tolist()
torch.no_grad()上下文管理器:禁用梯度计算
# 创建测试csv文件
def pad4(i):
"""
将输入数字i转换为长度为4的字符串,如果长度不足4,则在前面补0。
:param i: 需要转换的数字
:return: 补0后的字符串
"""
return "0" * (4 - len(str(i))) + str(i)
df = pd.DataFrame()
# 使用列表推导式生成Id列,列表长度等于测试集的长度
df["Id"] = [pad4(i) for i in range(len(test_set))]
# 将预测结果赋值给Category列
df["Category"] = prediction
df.to_csv("submission.csv", index=False)
- 预测结果保存在 submission.csv 文件之中。
可优化方向
在下一个task任务时会详细学习这部分内容,先做一个大致了解。
- 数据增强
- 几何变换
- 颜色变换
- 噪声添加
- 裁剪和填充
- 其他高级技术
- 改变网络结构
- AlexNet:作为深度学习在图像分类领域的开创性架构,AlexNet通过较大的卷积核和ReLU激活函数引领了深度学习的应用潮流。
- VGG:使用多层3x3卷积核构建深层网络,既简化了网络设计又提升了模型的准确性,成为多个任务的基准模型。
- Inception:引入多路径结构,使得网络能够在不同的尺度上并行提取特征,有效提高了计算效率和模型的表现力。
- ResNet(残差网络):引入跳跃连接(skip connections),有效解决了深度网络中的梯度消失和梯度爆炸问题,使得训练非常深的网络成为可能。
- 正则化技术
- 如L2正则化、Dropout、Batch Normalization等,以防止过拟合。
- 优化激活函数
- 选择适当的激活函数,如ReLU、Leaky ReLU、Swish等。
- 优化算法
- 采用先进的优化算法如Adam、RMSprop或学习率调度器。(可以Adam + 学习率调度)
- 约束初始化权重
- 利用He或Xavier初始化,确保训练的稳定性。
- 损失函数调整
- 模型压缩
- 通过剪枝、量化或知识蒸馏来减少模型复杂度,提高推理速度。
- 混合精度训练
- 结合半精度和单精度浮点数训练,加快训练速度并减少显存占用。
- 硬件加速
- 利用GPU、TPU等硬件加速器,以显著提升训练和推理的效率。