数据集转换是指将数据集转换为模型可接受的格式,让数据能够被模型所“理解”,而损失函数则是用于评估模型的性能,一般作为负反馈信号,用于指导模型的训练,使模型逐渐收敛到最优解。
数据集转换
以图像分类模型为例,图像数据集一般是RGB格式的图片,但是对于模型来说,图像并不能直接被理解,所以我们需要将图像数据集转换为对应的张量格式,以便于模型的输入。
Pytorch提供了torchvision.transforms模块,用于数据集的转换。此模块支持常见的计算机视觉变换及增强操作,在 Torchvision 0.15 版本中,torchvision.transforms已经发布了v2版本,提供了更多的数据增强方法,并且更快,且向下兼容v1版本。
下面是一个简单的数据集转换示例:
import torch
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda
ds = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor(),
target_transform=Lambda(lambda y: torch.zeros(10, dtype=torch.float).scatter_(0, torch.tensor(y), value=1))
)ToTensor():将PIL图像或numpy.ndarray转换为张量。Lambda:将目标标签转换为One-Hot编码。
损失函数
以图像分类模型为例,模型的输出是一个概率分布,而真实标签是一个One-Hot编码的向量,我们需要一个损失函数来评估模型的输出与真实标签之间的差异,通过最小化损失函数,使模型逐渐收敛到最优解。
Pytorch提供了torch.nn模块,用于构建神经网络模型,此模块提供了常见的损失函数,如CrossEntropyLoss、MSELoss等。
下面是一个简单的损失函数示例:
import torch
import torch.nn as nn
output = torch.randn(10, 5, requires_grad=True)
target = torch.randint(5, (10,), dtype=torch.int64)
criterion = nn.CrossEntropyLoss()
loss = criterion(output, target)CrossEntropyLoss:交叉熵损失函数,用于多分类问题。MSELoss:均方误差损失函数,用于回归问题。
Pytorch提供的损失函数可查看Pytorch损失函数
常见的损失函数
按解决问题的不同,损失函数也有所不同,常见的损失函数有:
分类问题:
CrossEntropyLoss、NLLLoss、BCELoss等。回归问题:
MSELoss、L1Loss、SmoothL1Loss等。对抗生成网络:
BCEWithLogitsLoss、HingeEmbeddingLoss、WassersteinLoss等。强化学习:
MSELoss、HuberLoss、KLDivLoss等。目标检测:
SmoothL1Loss、FocalLoss、IoULoss等。
在我们实际开发中,选择合适的损失函数至关重要,以Yolo目标检测算法为例,其损失函数是由均方误差、交叉熵、IoU等多个损失函数组合而成,通过不同的损失函数组合,可以更好地指导模型的训练,提高模型的性能。
优化模型参数
在前面已经实现了数据集的读取和转换,以及损失函数的计算,接下来我们需要优化模型参数,使模型逐渐收敛到最优解。
实际上,训练模型就是不断的迭代,每次迭代都会计算损失函数,并通过优化器来更新模型参数,使损失函数逐渐减小,模型性能逐渐提高。
在前面的代码中,以GTTSRB数据集为例,加载数据集并随机展示的代码如下:
import os
import pandas as pd
from torch.utils.data import Dataset
from torchvision.transforms import ToTensor
from PIL import Image
class GTSRB(Dataset):
def __init__(self, root, split="train", transform=None):
self.root = root
self.split = split
self.transform = transform
self.data = []
self.label = []
if self.split == "train":
data_dir = os.path.join(self.root, "GTSRB", "Training")
elif self.split == "test":
data_dir = os.path.join(self.root, "GTSRB", "Final_Test", "Images")
for class_dir in os.listdir(data_dir):
if not os.path.isdir(os.path.join(data_dir, class_dir)):
continue
csv_file = os.path.join(data_dir, class_dir, f"GT-{class_dir}.csv")
if not os.path.exists(csv_file):
continue
df = pd.read_csv(csv_file, delimiter=";")
for index, row in df.iterrows():
filename = os.path.join(data_dir, class_dir, row["Filename"])
label = row["ClassId"]
self.data.append(filename)
self.label.append(label)
def __len__(self):
return len(self.data)
def __getitem__(self, index):
image = Image.open(self.data[index])
label = self.label[index]
if self.transform:
image = self.transform(image)
else:
image = ToTensor()(image)
return image, label
# 随机挑选一张图片进行展示, 以及其对应的标签
import matplotlib.pyplot as plt
import numpy as np
import torch
dataset = GTSRB(root="./data/gtsrb", split="train")
imagePPM, label = dataset[np.random.randint(0, len(dataset))]
image = imagePPM.permute(1, 2, 0).numpy()
plt.imshow(image)
plt.title(f"Label: {label}")
plt.show()在以上代码中,我们定义了一个GTSRB类,该类继承自torch.utils.data.Dataset类,实现了__init__、__len__、__getitem__三个方法。在__init__方法中,我们加载了GTSRB数据集,并将数据集的路径与标签信息保存在self.data与self.label中。在__getitem__方法中,我们加载了图片,并返回图片与标签信息。
接下来,我们先了解一些基础知识,然后再来优化模型参数。
超参数
超参数指的是在模型训练过程中,需要人为设定的参数,如学习率、批大小、迭代次数等。超参数的选择对模型的性能有着重要的影响,合理的超参数选择可以提高模型的性能,加快模型的收敛速度。
超参数注意与模型参数区分开来,模型参数是指模型在训练过程中需要学习的参数,如权重、偏置等。
因此,超参数的选择是一个重要的问题,好在Ray Tune提供了超参数优化的工具,可以帮助我们自动搜索最优的超参数组合,提高模型的性能。
Ray Tune是一个分布式超参数优化库,支持多种深度学习框架,如Pytorch、Tensorflow等。可参考Ray Tune官方文档。这里暂时不做赘述。
在这里,我们主要用到的超参数有:
学习率:学习率是指模型在训练过程中,每次迭代更新参数的步长,学习率过大会导致模型不稳定,学习率过小会导致模型收敛速度慢。
批大小:批大小是指模型在训练过程中,每次迭代所处理的样本数量,批大小过大会导致内存不足,批大小过小会导致模型收敛速度慢。
迭代次数:迭代次数是指模型在训练过程中,总共迭代的次数,迭代次数过少会导致模型欠拟合,迭代次数过多会导致模型过拟合。
learning_rate = 0.001
batch_size = 64
epochs = 10优化循环
在完成超参数设置后,我们需要构建优化循环,即不断迭代计算损失函数,并通过优化器来更新模型参数,使模型逐渐收敛到最优解。
优化循环的每次迭代称为一个时期,每个时期包含以下步骤:
训练循环:计算损失函数,并通过优化器来更新模型参数。
验证/测试循环:计算验证集的损失函数,用于评估模型的性能。
优化器
优化器是用于更新模型参数的算法,常见的优化器有SGD、Adam、RMSprop等。
Pytorch提供了
torch.optim模块,用于构建优化器,可查看Pytorch优化器
每一个训练循环需要经过以下步骤:
前向传播:计算模型的输出。
计算损失函数:计算模型输出与真实标签之间的差异。
反向传播:计算损失函数对模型参数的梯度。
更新模型参数:通过优化器来更新模型参数。
以SGD优化器为例,优化循环的代码如下:
import torch
import torch.nn as nn
import torch.optim as optim
# 定义模型
model = nn.Linear(784, 10)
# 定义损失函数
criterion = nn.CrossEntropyLoss()
# 定义优化器,这里传入了两个参数,一个是模型的参数,一个是学习率
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 训练循环
for epoch in range(epochs):
for data, target in train_dataloader:
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()在以上代码中,我们首先定义了一个线性模型model,然后定义了交叉熵损失函数criterion,最后定义了SGD优化器optimizer,并传入了模型的参数与学习率。
接下来,我们进入训练循环,每个时期都会遍历train_dataloader,计算损失函数,并通过优化器来更新模型参数。
基础实现
有了以上的基础知识,那么我们以图像分类模型为例,实现的流程就很清晰了:
加载数据集 -> 定义模型 -> 定义损失函数 -> 定义优化器 -> 训练循环 -> 评估模型 -> 保存模型
根据我们前面的GTSRB数据集,我们可以实现一个简单的图像分类模型,代码如下:
import os
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchvision.transforms import ToTensor, Compose, Resize, Normalize
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
# 检查 CUDA 是否可用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("使用设备:", device)
# 定义 GTSRB 数据集类
class GTSRB(Dataset):
def __init__(self, root, split="train", transform=None):
self.root = root
self.split = split
self.transform = transform
self.data = []
self.label = []
if self.split == "train":
data_dir = os.path.join(self.root, "GTSRB", "Training")
for class_dir in os.listdir(data_dir):
class_path = os.path.join(data_dir, class_dir)
if not os.path.isdir(class_path):
continue
csv_file = os.path.join(class_path, f"GT-{class_dir}.csv")
if not os.path.exists(csv_file):
continue
df = pd.read_csv(csv_file, delimiter=";")
for index, row in df.iterrows():
filename = os.path.join(class_path, row["Filename"])
label = row["ClassId"]
self.data.append(filename)
self.label.append(label)
elif self.split == "test":
data_dir = os.path.join(self.root, "GTSRB", "Final_Test", "Images")
csv_file = os.path.join(self.root, "GTSRB", "GT-final_test.csv")
df = pd.read_csv(csv_file, delimiter=";")
for index, row in df.iterrows():
filename = os.path.join(data_dir, row["Filename"])
label = row["ClassId"]
self.data.append(filename)
self.label.append(label)
def __len__(self):
return len(self.data)
def __getitem__(self, index):
image = Image.open(self.data[index])
label = self.label[index]
if self.transform:
image = self.transform(image)
else:
image = ToTensor()(image)
return image, label
# 数据预处理和加载
transform = Compose([Resize((32, 32)), ToTensor(), Normalize((0.5,), (0.5,))])
train_dataset = GTSRB(root="./data/gtsrb", split="train", transform=transform)
test_dataset = GTSRB(root="./data/gtsrb", split="test", transform=transform)
# 批次大小
batch_size = 64
# 加载训练集和测试集 shuffle=True 表示打乱数据顺序
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
# 定义模型
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 输入 3 通道,输出 32 通道,卷积核大小 5
self.conv1 = nn.Conv2d(3, 32, kernel_size=5)
# 输入 32 通道,输出 64 通道,卷积核大小 5
self.conv2 = nn.Conv2d(32, 64, kernel_size=5)
# 全连接层,输入 64 * 5 * 5,输出 256
self.fc1 = nn.Linear(64 * 5 * 5, 256)
# 全连接层,输入 256,输出 43
self.fc2 = nn.Linear(256, 43)
# 最大池化层,核大小 2,步长 2
self.pool = nn.MaxPool2d(2, 2)
# Dropout 层,丢弃概率 0.5
self.dropout = nn.Dropout(0.5)
# ReLU 激活函数
self.relu = nn.ReLU()
def forward(self, x):
# 卷积 -> 池化 -> 激活
x = self.pool(self.relu(self.conv1(x))) # [batch, 32, 14, 14]
x = self.pool(self.relu(self.conv2(x))) # [batch, 64, 5, 5]
x = x.view(-1, 64 * 5 * 5)
x = self.relu(self.fc1(x)) # [batch, 256]
x = self.dropout(x)
x = self.fc2(x) # [batch, 43]
return x
model = Net().to(device) # 将模型移动到 GPU
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
# 学习率 0.01,动量 0.9 的 SGD 优化器
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# 训练模型
epochs = 100
for epoch in range(epochs):
model.train()
running_loss = 0.0
for i, (inputs, labels) in enumerate(train_loader):
inputs, labels = inputs.to(device), labels.to(device) # 将数据移动到 GPU
# 清零参数梯度
optimizer.zero_grad()
# 前向传播
outputs = model(inputs)
# 计算损失
loss = criterion(outputs, labels)
# 反向传播和优化
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99: # 每 100 个批次打印一次
print(f"[{epoch + 1}, {i + 1}] loss: {running_loss / 100:.3f}")
running_loss = 0.0
# 在测试集上评估模型
model.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in test_loader:
inputs, labels = inputs.to(device), labels.to(device) # 将数据移动到 GPU
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f"第 {epoch + 1} 轮测试集准确率:{100 * correct / total:.2f}%")
# 保存模型
torch.save(model.state_dict(), "model.pth")
print("训练完成,模型已保存为 model.pth。")在以上代码中,我们首先定义了一个Net类,该类继承自nn.Module类,实现了一个简单的卷积神经网络模型。然后我们定义了一个model模型,并将模型移动到 GPU。接着我们定义了交叉熵损失函数criterion和SGD优化器optimizer,并传入了模型的参数与学习率。最后我们进入训练循环,每个时期都会遍历train_loader,计算损失函数,并通过优化器来更新模型参数。在每个时期结束后,我们会在测试集上评估模型的性能,并打印测试集的准确率。最后我们保存了训练好的模型。
具体来说,我们的分类是这样实现的:
定义一个简单的卷积神经网络模型
Net。加载
GTSRB数据集,并进行预处理。定义交叉熵损失函数
criterion和SGD优化器optimizer。进入训练循环,每个时期都会遍历
train_loader,计算损失函数,并通过优化器来更新模型参数。在每个时期结束后,我们会在测试集上评估模型的性能,并打印测试集的准确率。
其图示如下:
流程图如下: