猫狗分类来源于Kaggle上的一个入门竞赛。
https://www.kaggle.com/competitions/dogs-vs-cats-redux-kernels-edition/overview
首先,导入一系列的库。
import numpy as np
from PIL import Image
from pathlib import Path
import torch
from torch import nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader, random_split
from torchvision import transforms
import matplotlib.pyplot as plt
这段代码主要是导入了一些Python库,包括:
这些库的导入是PyTorch实践项目中经常用到的基础操作,其中PIL、numpy和matplotlib主要用于读取和展示图像、transforms用于对图像进行数据增强,torch和nn则是构建和训练深度神经网络的核心。
而后,启用GPU加速。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("device: ", device)
get_label = lambda x: x.name.split('.')[0]
class get_dataset(Dataset):
def __init__(self, root, transform=None):
self.images = list(Path(root).glob('*.jpg'))
self.transform = transform
def __len__(self):
return len(self.images)
def __getitem__(self, idx):
img = self.images[idx]
label = get_label(img)
label = 1 if label == 'dog' else 0
if self.transform:
img = self.transform(Image.open(img))
return img, torch.tensor(label, dtype=torch.int64)
这段代码定义了一个类get_dataset,用于加载和预处理数据集。
在类的初始化函数中,root为数据集路径,transform为数据预处理函数。通过list和glob函数获取符合条件的文件名,即所有后缀为jpg的图片文件名,并将其转为列表self.images。同时记录transform函数,即数据预处理函数。
__len__函数返回数据集中的图片数量,__getitem__函数根据索引idx获取对应图片和标签。首先获取索引对应的图片img,并通过get_label函数获取该图片对应的标签。该函数将图片文件名以’.‘分割,并将第一个分割出来的字符串作为标签。如果标签等于’dog’,则将其转为数字1,否则转为数字0。
接着如果有定义transform函数,就将img通过transform函数进行数据预处理。最后返回处理后的图片和标签,其中标签用torch.tensor转为整型。
注意:glob 是 Python 标准库中 pathlib 模块下的一个函数,用于查找指定路径下符合特定规则的文件或文件夹路径,返回一个生成器(generator)。
在这个实现中,glob(‘*.jpg’) 表示查找 root 目录下所有后缀为 .jpg 的文件。通配符 * 表示匹配任意字符,因此 *.jpg 表示匹配任意字符开头、.jpg 结尾的文件名。
举个例子,如果 root 目录下有这些文件:
那么 list(Path(root).glob(‘*.jpg’)) 将返回这些文件的路径:
Copy Code[
'root/cat.1.jpg',
'root/cat.2.jpg',
'root/dog.1.jpg',
'root/dog.2.jpg',
'root/rabbit.1.jpg'
]
这里需要注意,glob 函数返回的是一个生成器,需要通过 list() 转换成列表才能进行遍历或其他操作。
transforms = transforms.Compose([
transforms.Resize([224, 224]), # resize the input image to a uniform size
transforms.ToTensor(), # convert PIL Image or numpy.ndarray to tensor and normalize to somewhere between [0,1]
transforms.Normalize( # standardized processing
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
这段代码定义了一个数据预处理过程,用于将读入的图像进行标准化处理,从而方便模型训练。
数据预处理使用了 transforms 模块中的 Compose 函数,它可以将多种图像变换串联起来,形成一个可复用的数据处理流程。这里串联了三个变换:
最终 transforms 变量定义了一个复合的预处理过程,可以方便地应用于训练集和测试集中的图像数据。
注意:transforms.Normalize() 是一个数据标准化操作,通过对每个像素在 RGB 颜色通道上的取值进行归一化,将图像的统计性质进行规范化,从而方便模型的使用和训练。
具体地,该函数接受两个参数:mean 和 std,分别表示要减去的均值和要除以的标准差。在这里,mean 和 std 的取值是根据 Imagenet 数据集的统计特征来计算的,这些统计特征对于大多数视觉任务都是通用的:
- mean:表示在 Imagenet 训练集上所有图片在 RGB 颜色通道上的平均取值。对于彩色图像,mean 是一个长度为 3 的列表,分别表示 RGB 三个颜色通道上的平均值。在这里,mean 的取值为 [0.485, 0.456, 0.406]。
- std:表示在 Imagenet 训练集上所有图片在 RGB 颜色通道上的标准差。同样地,std 也是一个长度为 3 的列表,在这里,std 的取值为 [0.229, 0.224, 0.225]。
假设输入图像为 x,则 Normalize 操作的具体计算为:(x−mean)/std,即将输入图像减去均值再除以标准差。最终得到的图像每个像素在每个颜色通道上的取值都在 [0,1][0,1] 区间内。
这样的标准化操作可以帮助模型更好地训练和收敛,提高了模型的鲁棒性,同时加快了模型训练的速度。
train_data, valid_data = random_split(dataset,
lengths=[int(len(dataset)*0.8),int(len(dataset)*0.2)],
generator=torch.Generator().manual_seed(7))
# print("train: ", len(train_data))
# print("valid: ", len(valid_data))
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
valid_loader = DataLoader(valid_data, batch_size=64)
这段代码定义了数据集的划分方式以及相应的数据加载器。
首先,数据集(即 dataset)被随机划分为训练集和验证集,划分的方式是使用 PyTorch 中的 random_split() 函数。其中,lengths 参数指定了划分后训练集和验证集的长度比例,即训练集长度为总数据集的 80%,验证集长度为总数据集的 20%。同时,这里也设置了随机数生成器的种子为 7,保证每次划分结果的一致性。
接着,使用 PyTorch 中的 DataLoader() 函数对训练集和验证集进行批量数据的加载,具体来说:
最终,train_loader 和 valid_loader 可以作为模型的输入数据来源,在模型的训练和验证过程中起到很重要的作用。
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(3, 32, 3),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.conv2 = nn.Sequential(
nn.Conv2d(32, 64, 3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.conv3 = nn.Sequential(
nn.Conv2d(64, 128, 3),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.fc1 = nn.Sequential(
nn.Linear(128*26*26, 512),
nn.BatchNorm1d(512),
nn.ReLU()
)
self.fc2 = nn.Linear(512, 2)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x.view(x.size(0), -1)
x = self.fc1(x)
x = self.fc2(x)
x = F.log_softmax(x, dim=1)
return x
这段代码定义了一个卷积神经网络模型,包含三个卷积层和两个全连接层。
首先,在 init 方法中,对网络的各个层次进行了定义。其中,
接着,在 forward 方法中定义了前向传播过程,即按照 conv1、conv2、conv3、fc1、fc2 的顺序依次处理输入数据,并最终经过一个 log_softmax 函数进行输出。其中, view() 方法将卷积层输出的二维张量展平成一维张量,用于作为全连接层的输入。
在卷积神经网络中,每一层卷积和池化操作都会缩小输入特征图的尺寸,同时增加通道数。在这个模型中,第一个卷积层对输入的大小进行了卷积操作,将其从224x224的RGB图像转换成了(3, 32, 32)的特征图。接着,通过使用步幅为2的2x2最大池化层,将特征图的尺寸减半,得到(32, 112, 112)大小的特征图。后续的卷积层和最大池化层同理可以得到(64, 56, 56)和(128, 26, 26)大小的特征图。
因此,在全连接层之前,需要将最后一层卷积输出的(128, 26, 26)的特征图展开成一个一维向量,作为全连接层的输入。由于其中包含128个26x26大小的特征图,每个特征图又有128个通道,因此展开后的一维向量大小为128x26x26=86528。该大小即为nn.Linear(1282626, 512)中输入的大小。
CNN(
(conv1): Sequential(
(0): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
(3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(conv2): Sequential(
(0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
(3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(conv3): Sequential(
(0): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
(3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(fc1): Sequential(
(0): Linear(in_features=86528, out_features=512, bias=True)
(1): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU()
)
(fc2): Linear(in_features=512, out_features=2, bias=True)
)
model = CNN()
model.to(device)
print(model)
loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
epochs = 25
train_loss_list = []
train_acc_list = []
for epoch in range(epochs):
print("Epoch {} / {}".format(epoch + 1, epochs))
t_loss, t_corr = 0.0, 0.0
model.train()
for inputs, labels in train_loader:
inputs = inputs.to(device)
labels = labels.to(device)
preds = model(inputs)
loss = loss_function(preds, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
t_loss += loss.item() * inputs.size(0)
t_corr += torch.sum(preds.argmax(1) == labels)
# preds.argmax(1)返回预测结果中概率最大的类别标签,即预测的类别
train_loss = t_loss / len(train_loader.dataset)
train_acc = t_corr.cpu().numpy() / len(train_loader.dataset)
train_loss_list.append(train_loss)
train_acc_list.append(train_acc)
print('Train Loss: {:.4f} Accuracy: {:.4f}%'.format(train_loss, train_acc * 100))
这段代码实现了使用交叉熵损失函数和Adam优化器对一个卷积神经网络模型进行训练,并记录每个epoch的训练损失和训练准确率。
具体地,该代码首先定义了一种损失函数(loss_function)nn.CrossEntropyLoss(),用于计算模型预测结果与真实标签之间的交叉熵损失。接着,定义了一个优化器(optimizer)torch.optim.Adam(),用于对模型参数进行Adam梯度下降优化,其中初始学习率为1e-3。
接下来,在25个epoch的循环中,对训练数据集(train_loader)中的每个批次数据进行模型训练。在每个epoch开始时,打印当前epoch的编号。然后,将模型设置为训练模式(model.train()),并初始化该epoch的总训练损失(t_loss)和正确预测的样本数量(t_corr)为0。
在每个批次中,将输入数据(inputs)和标签(labels)转移到设备(device)上(例如GPU),通过前向传播得到模型对该批次输入数据的预测结果(preds)。接着,通过loss_function计算该批次预测结果的交叉熵损失(loss),通过optimizer.zero_grad()将优化器的梯度清零,再通过loss.backward()自动计算参数的梯度,并通过optimizer.step()更新参数值。同时,累加该批次的训练损失(t_loss)和正确预测的样本数量(t_corr)。
在完成该epoch的所有批次训练后,计算该epoch的平均训练损失(train_loss)和训练准确率(train_acc),并将其分别存储在train_loss_list和train_acc_list列表中。最后打印该epoch的平均训练损失和训练准确率。
:::tips
Epoch 1 / 25
Train Loss: 0.5553 Accuracy: 72.3900%
Epoch 2 / 25
Train Loss: 0.4048 Accuracy: 81.5550%
Epoch 3 / 25
Train Loss: 0.3101 Accuracy: 86.6150%
Epoch 4 / 25
Train Loss: 0.2128 Accuracy: 91.3250%
Epoch 5 / 25
Train Loss: 0.1293 Accuracy: 95.0350%
Epoch 6 / 25
Train Loss: 0.0817 Accuracy: 96.9900%
Epoch 7 / 25
Train Loss: 0.0529 Accuracy: 98.0900%
Epoch 8 / 25
Train Loss: 0.0535 Accuracy: 98.0000%
Epoch 9 / 25
Train Loss: 0.0389 Accuracy: 98.6350%
Epoch 10 / 25
Train Loss: 0.0234 Accuracy: 99.2100%
Epoch 11 / 25
Train Loss: 0.0387 Accuracy: 98.6900%
Epoch 12 / 25
Train Loss: 0.0370 Accuracy: 98.7700%
Epoch 13 / 25
Train Loss: 0.0265 Accuracy: 99.1250%
Epoch 14 / 25
Train Loss: 0.0229 Accuracy: 99.1650%
Epoch 15 / 25
Train Loss: 0.0181 Accuracy: 99.4150%
Epoch 16 / 25
Train Loss: 0.0171 Accuracy: 99.3800%
Epoch 17 / 25
Train Loss: 0.0287 Accuracy: 99.0500%
Epoch 18 / 25
Train Loss: 0.0246 Accuracy: 99.1350%
Epoch 19 / 25
Train Loss: 0.0177 Accuracy: 99.3900%
Epoch 20 / 25
Train Loss: 0.0172 Accuracy: 99.4250%
Epoch 21 / 25
Train Loss: 0.0097 Accuracy: 99.7200%
Epoch 22 / 25
Train Loss: 0.0173 Accuracy: 99.4150%
Epoch 23 / 25
Train Loss: 0.0138 Accuracy: 99.5250%
Epoch 24 / 25
Train Loss: 0.0076 Accuracy: 99.7050%
Epoch 25 / 25
Train Loss: 0.0057 Accuracy: 99.8250%
:::
plt.figure()
plt.title('Train Loss and Accuracy')
plt.xlabel('Epoch')
plt.ylabel('')
plt.plot(range(1, epochs+1), np.array(train_loss_list), color='blue',
linestyle='-', label='Train_Loss')
plt.plot(range(1, epochs+1), np.array(train_acc_list), color='red',
linestyle='-', label='Train_Accuracy')
plt.legend() # 凡例
plt.show() # 表示
这是一个用于展示模型训练过程中训练损失和准确率的函数。主要功能包括:
plt.figure() 用于创建一个新的窗口,并将其设置为当前活动窗口,以便在其中进行数据可视化操作。
plt.title()、plt.xlabel() 和 plt.ylabel() 用于设置图形标题、横轴标签和纵轴标签的文本内容。
plt.plot() 用于绘制训练损失和准确率曲线,第一个参数表示 x 轴的取值范围(从 1 到 epochs+1),第二个参数则表示 y 轴的取值范围(train_loss_list 或 train_acc_list)。颜色和线条样式则可通过 color 和 linestyle 参数进行设置。
plt.legend() 用于添加图例,其中包含每个曲线的标签(即 Train_Loss 和 Train_Accuracy)。
plt.show() 用于显示绘制出来的图形。
test_path = './kaggle/inputs/test'
# get dataset
test_data = get_dataset(test_path, transform=transforms)
# print(len(test_data))
test_loader = DataLoader(test_data, batch_size=64)
_loss, _corr = 0.0, 0.0
model.eval()
with torch.no_grad():
for inputs, labels in test_loader:
inputs = inputs.to(device)
labels = labels.to(device)
y = model(inputs)
preds = y.argmax(1)
_loss += loss.item() * inputs.size(0)
_corr += torch.sum(preds== labels)
print('Test Loss: {:.4f} Accuracy: {:.4f}%'.format(_loss / len(test_loader.dataset),
(_corr / len(test_loader.dataset)) * 100))
这段代码用于在测试集上评估经过训练的模型的性能。
首先,test_path 是测试数据集所在的路径。
get_dataset() 是一个自定义函数,用于加载测试集的图像和标签,并将其存储在test_data中。此外,transform 是一个可选参数,用于数据增强操作。
然后,创建一个 DataLoader 对象 test_loader,该对象用于从测试集中为模型提供数据。batch_size 参数指定了每个批次的大小。在此处,batch_size 设定为 64。
接着,_loss 和 _corr 都被初始化为零。这两个变量分别用于累计模型在测试集上的损失和分类正确率。
使用 model.eval() 将模型设置为评估模式,以便禁用一些训练时的特性(如 dropout 层),并确保在每个前向传播时保留所有权重。
为了节省内存,使用 torch.no_grad() 上下文管理器禁用梯度计算,避免在测试期间不必要地计算梯度。
对于测试集中的每个批次(即 inputs 和 labels),将其转移到 GPU 上(如果有的话),然后利用模型预测输入数据的标签,并计算该批次的交叉熵损失。具体来说,preds 是预测标签的最大值索引,而 labels 是实际标签。因此,preds == labels 返回一个布尔张量,其中每个元素指示对应的预测标签是否与真实标签匹配,torch.sum() 函数返回 True 值的数量。
最后,通过将 _loss 和 _corr 分别除以测试集样本数目,得到模型在测试集上的平均损失和准确率,并使用 print() 函数将这些信息打印出来。特别地,_loss / len(test_loader.dataset) 表示平均损失,(_corr / len(test_loader.dataset)) * 100 表示准确率,乘以 100 是为了将其转换为百分比。
import numpy as np
from PIL import Image
from pathlib import Path
import torch
from torch import nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader, random_split
from torchvision import transforms
import matplotlib.pyplot as plt
transforms = transforms.Compose([
transforms.Resize([224, 224]), # resize the input image to a uniform size
transforms.ToTensor(), # convert PIL Image or numpy.ndarray to tensor and normalize to somewhere between [0,1]
transforms.Normalize( # standardized processing
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
get_label = lambda x: x.name.split('.')[0]
class get_dataset(Dataset):
def __init__(self, root, transform=None):
self.images = list(Path(root).glob('*.jpg'))
self.transform = transform
def __len__(self):
return len(self.images)
def __getitem__(self, idx):
img = self.images[idx]
label = get_label(img)
label = 1 if label == 'dog' else 0
if self.transform:
img = self.transform(Image.open(img))
return img, torch.tensor(label, dtype=torch.int64)
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(3, 32, 3),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.conv2 = nn.Sequential(
nn.Conv2d(32, 64, 3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.conv3 = nn.Sequential(
nn.Conv2d(64, 128, 3),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.fc1 = nn.Sequential(
nn.Linear(128 * 26 * 26, 512),
nn.BatchNorm1d(512),
nn.ReLU()
)
self.fc2 = nn.Linear(512, 2)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x.view(x.size(0), -1)
x = self.fc1(x)
x = self.fc2(x)
x = F.log_softmax(x, dim=1)
return x
if __name__ == '__main__':
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
train_path = './train'
dataset = get_dataset(train_path, transform=transforms)
train_data, valid_data = random_split(dataset,
lengths=[int(len(dataset) * 0.8), int(len(dataset) * 0.2)],
generator=torch.Generator().manual_seed(7))
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
valid_loader = DataLoader(valid_data, batch_size=64)
model = CNN()
model.to(device)
# print(model)
loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
epochs = 25
train_loss_list = []
train_acc_list = []
for epoch in range(epochs):
print("Epoch {} / {}".format(epoch + 1, epochs))
t_loss, t_corr = 0.0, 0.0
model.train()
for inputs, labels in train_loader:
inputs = inputs.to(device)
labels = labels.to(device)
preds = model(inputs)
loss = loss_function(preds, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
t_loss += loss.item() * inputs.size(0)
t_corr += torch.sum(preds.argmax(1) == labels)
train_loss = t_loss / len(train_loader.dataset)
train_acc = t_corr.cpu().numpy() / len(train_loader.dataset)
train_loss_list.append(train_loss)
train_acc_list.append(train_acc)
print('Train Loss: {:.4f} Accuracy: {:.4f}%'.format(train_loss, train_acc * 100))
v_loss, v_corr = 0.0, 0.0
model.eval()
with torch.no_grad():
for inputs, labels in valid_loader:
inputs = inputs.to(device)
labels = labels.to(device)
preds = model(inputs)
v_loss += loss.item() * inputs.size(0)
v_corr += torch.sum(preds.argmax(1) == labels)
print('Valid Loss: {:.4f} Accuracy: {:.4f}%'.format(v_loss / len(valid_loader.dataset),
(v_corr / len(valid_loader.dataset)) * 100))
test_path = './test'
# get dataset
test_data = get_dataset(test_path, transform=transforms)
# print(len(test_data))
test_loader = DataLoader(test_data, batch_size=64)
_loss, _corr = 0.0, 0.0
model.eval()
with torch.no_grad():
for inputs, labels in test_loader:
inputs = inputs.to(device)
labels = labels.to(device)
y = model(inputs)
preds = y.argmax(1)
_loss += loss.item() * inputs.size(0)
_corr += torch.sum(preds == labels)
print('Test Loss: {:.4f} Accuracy: {:.4f}%'.format(_loss / len(test_loader.dataset),
(_corr / len(test_loader.dataset)) * 100))