自适应学习率
ADaGrad
梯度比较大的时候,学习率就减小,梯度比较小的时候,学习率就放大。
RMSProp
同一个参数的同个方向,学习率也是需要动态调整的
Adam
Adam 可以看作 RMSprop 加上动量,其使用动量作为参数更新方向,并且能够自适应调整学习率。
学习率调度
学习率调度中最常见的策略是学习率衰减(learning rate decay),也称为学习率退火(learning rateannealing)。
除了学习率下降以外,还有另外一个经典的学习率调度的方式———预热。预热的方法是让学习率先变大后变小.
分类
softmax
最小化交叉熵=最大化似然
实战:图像分类
模型
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) # 通过全连接神经网络得到最终输出
配置
# 根据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()
# 初始化优化器,您可以自行调整一些超参数,如学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)
加载数据
# 构建训练和验证数据集
# "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)
开始训练
# 初始化追踪器,这些不是参数,不应该被更改
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
测试
# 实例化分类器模型,并将其转移到指定的设备上
model_best = Classifier().to(device)
# 加载模型的最优状态字典
model_best.load_state_dict(torch.load(f"{_exp_name}_best.ckpt"))
# 将模型设置为评估模式
model_best.eval()
# 初始化一个空列表,用于存储所有预测标签
prediction = []
# 使用torch.no_grad()上下文管理器,禁用梯度计算
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()
数据增强
train_tfm = transforms.Compose([
# 将图像调整为固定的尺寸(高度 = 宽度 = 128)
transforms.Resize((128, 128)),
# 你可以在这里添加一些数据转换操作
transforms.ToTensor(),
])
t-SNE可视化
from tqdm import tqdm
import numpy as np
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
import matplotlib.cm as cm
import torch
def forward_to_layer(model, input_tensor, layer_index):
outputs = []
for i, layer in enumerate(model.children()):
input_tensor = layer(input_tensor)
if i == layer_index:
break
outputs.append(input_tensor)
return outputs[-1] # 返回所选层的输出
# 假设model, test_tfm, FoodDataset, DataLoader已经被定义且正确初始化
# 加载由TA定义的验证集
valid_set = FoodDataset("./hw3_data/valid", tfm=test_tfm)
valid_loader = DataLoader(valid_set, batch_size=64, shuffle=False, num_workers=0, pin_memory=True)
# 提取模型特定层的表示
index = 19 # 假设你想提取第19层的特征
features = []
labels = []
# 定义设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
for batch in tqdm(valid_loader):
imgs, lbls = batch
imgs, lbls = imgs.to(device), lbls.to(device) # 确保数据在正确的设备上
with torch.no_grad():
# 获取特定层的特征
logits = forward_to_layer(model.cnn, imgs, index)
logits = logits.view(logits.size(0), -1)
labels.extend(lbls.cpu().numpy())
features.extend(logits.cpu().numpy())
# 将features和labels列表转换为numpy数组
features = np.array(features)
labels = np.array(labels)
# 应用t-SNE到特征上
features_tsne = TSNE(n_components=2, init='pca', random_state=42).fit_transform(features)
# 绘制t-SNE可视化图
plt.figure(figsize=(10, 8))
for label in np.unique(labels):
# 使用布尔索引选择特定标签的数据点
mask = (labels == label)
plt.scatter(features_tsne[mask, 0], features_tsne[mask, 1], label=f'Class {label}', s=5)
plt.legend()
plt.title('All Classes t-SNE Visualization')
plt.show()
# 绘制特定类别的t-SNE可视化图
plt.figure(figsize=(10, 8))
selected_label = 5
mask = (labels == selected_label)
if mask.any(): # 使用 .any() 替代 .sum() 来检查是否有True值
plt.scatter(features_tsne[mask, 0], features_tsne[mask, 1], label=f'Class {selected_label}', s=5)
plt.legend()
plt.title(f'Class {selected_label} t-SNE Visualization')
plt.show()