图像分类代码学习笔记
图像分类代码学习笔记
数据集描述
数据集为美国手语图像数据(American Sign Language),包含87000张图像,每张图片都是200×200像素,总共有29个类,其中26个类表示字母 A-Z,剩下三种表示空格、删除和什么都没有。
代码笔记
train_data_path = '../input/asl-alphabet/asl_alphabet_train/asl_alphabet_train/'
train_data_path 存储训练数据集的轮径。
train_transforms = transforms.Compose([
transforms.Resize(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
test_transforms = transforms.Compose([
transforms.Resize(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
原始数据的格式不一定和训练所需的数据格式相同。我们使用transforms模块对数据进行操作,使其适合训练。
transforms.Compose()
的主要作用是串联多个图片变换的操作。
transforms.Resize()
是调整 PILImage 对象的尺寸,注意不能是用 io.imread 或者 cv2.imread 读取的图片,这两种方法得到的是 ndarray。
将图片短边缩放至x,长宽比保持不变:
transforms.Resize(x)
而一般输入深度网络的特征图长宽是相等的,就不能采取等比例缩放的方式了,需要同时指定长宽:
transforms.Resize([h, w])
transform.ToTensor()
将PILImage 或者 numpy 的 ndarray 转化成 Tensor。
transform.Normalize()
逐channel的对图像进行标准化(均值变为0,标准差变为1),可以加快模型的收敛。
这里的参数 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
使用的数据是 ImageNet 数据集的均值和标准差。
如何计算图片的 mean 和 std:如何计算pytorch中图像输入的均值和方差
train_dataset = datasets.ImageFolder(train_data_path, transform=train_transforms)
val_dataset = datasets.ImageFolder(train_data_path, transform=test_transforms)
Datasets 都是 torch.utils.data.Dataset 的子类,所以,他们也可以通过 torch.utils.data.DataLoader
使用多线程(python的多进程)。
ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:
ImageFolder(root, transform=None, target_transform=None, loader=default_loader)
- root:在 root 指定的路径下寻找图片
- transform:对 PIL Image 进行的转换操作,transform 的输入是使用 loader 读取图片的返回对象
- target_transform:对 label 的转换
- loader:给定路径后如何读取图片,默认读取为 RGB 格式的 PIL Image 对象
# 数据集划分
torch.manual_seed(1)
num_train_samples = len(full_dataset)
# num_train_samples = 5000
full_dataset, _ = torch.utils.data.random_split(full_dataset, [num_train_samples, len(full_dataset)-num_train_samples])
train_size = int(0.6 * num_train_samples)
val_size = int(0.2 * num_train_samples)
test_size = num_train_samples - train_size - val_size
train_dataset, val_dataset, test_dataset = torch.utils.data.random_split(full_dataset, [train_size, val_size, test_size])
len(train_dataset), len(val_dataset), len(test_dataset)
torch.manual_seed()
设置随机数种子。
torhc.utils.data.random_split
对数据集进行随机划分,其中第一个参数是待划分的数据集,第二个参数是一个列表,其中存储划分后各个数据集占原数据集的比例。
这里划分后训练集、验证集和测试集的比例为 6:2:2。
# 设置 Dataloader
batch_size = 64 # 设置 batch_size
train_dataloader = torch.utils.data.DataLoader(
dataset=train_dataset,
batch_size=batch_size,
shuffle=True
)
val_dataloader = torch.utils.data.DataLoader(
dataset=val_dataset,
batch_size=batch_size,
shuffle=False
)
这一步将数据集装入 Pytorch 的 DataLoader,DataLoader 可以在训练时自动从 Dataset 中抽取样本进行训练,并且能够成批抽取,形成 Batch。
resnet = models.resnet50(pretrained=True) # 直接使用 Pytorch 官方提供的 resnet 模型,并且获取预训练参数
Pytorch 封装了许多常见的模型,这里使用 Pytorch 提供的 ResNet50 模型,并且加载了模型提供的预训练参数。
# 冻结特征层的参数,进行迁移学习
for param in resnet.parameters():
param.requires_grad = False
# 更换全连接层,调整输入和输出,释放全连接层的参数
in_features = resnet.fc.in_features
fc = nn.Linear(in_features=in_features, out_features=len(classes))
resnet.fc = fc
这一步冻结了特征层的参数,在后续的训练中不再需要对特征层的参数进行反向传播和更新,降低了训练的难度,这一步属于迁移学习。同时,由于全连接层的大小取决于输出的类别,所以需要对 ResNet50 模型的全连接层进行修改。
# 需要更新的参数(上一步中更换的全连接层参数)
params_to_update = []
for name, param in resnet.named_parameters():
if param.requires_grad:
params_to_update.append(param)
确定需要更新的参数,其实就是全连接层的参数,最后的全连接层主要负责分类,而之前的层叫做特征层,主要用于特征提取。
criterion = nn.CrossEntropyLoss() # 交叉熵损失
optimizer = torch.optim.Adam(params_to_update, lr=0.001) # Adam 优化器
设置损失函数和优化器,多分类任务常用的损失函数就是交叉熵损失,这里优化器选择 Adam 优化器。
# 选择显卡进行训练
device = 'cuda' if torch.cuda.is_available() else 'cpu'
device
设置显卡。
from time import time
from tqdm import tqdm # 进度条
# 训练过程
def train(model,
criterion,
optimizer,
train_dataloader,
val_dataloader,
print_every,
num_epoch):
'''
:param model: Pytorch模型
:param criterion: 损失函数
:param optimizer: 优化器
:param train_dataloader: 训练集
:param val_dataloader: 验证集
:param print_every: 多少个 epochs 输出一次
:param num_epoch: 训练多少个 epochs
:returns model: 模型
:returns train_losses 训练集损失
:returns val_losses 验证集损失
'''
steps = 0
train_losses, val_losses = [], []
train_accuracy, val_accuracy = [], []
model.to(device)
for epoch in tqdm(range(num_epoch)):
running_loss = 0
correct_train = 0
total_train = 0
start_time = time()
iter_time = time()
model.train()
for i, (images, labels) in enumerate(train_dataloader):
steps += 1
# 把数据放到 GPU
images = images.to(device)
labels = labels.to(device)
if epoch == 0 and i == 0:
torch.onnx.export(resnet, images, 'resnet.pth')
# 前向传播
output = model(images)
loss = criterion(output, labels)
correct_train += (torch.max(output, dim=1)[1] == labels).sum().item()
total_train += labels.size(0)
# 反向传播与优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.item()
# 记录输出
if steps % print_every == 0:
print(f'Epoch [{epoch + 1}]/[{num_epoch}]. Batch [{i + 1}]/[{len(train_dataloader)}].', end=' ') # epoch数量
print(f'Train loss {running_loss / steps:.3f}.', end=' ')
print(f'Train acc {correct_train / total_train * 100:.3f}.', end=' ')
train_accuracy.append(correct_train / total_train)
# 使用验证集评估模型训练过程中的表现
with torch.no_grad():
model.eval()
correct_val, total_val = 0, 0
val_loss = 0
for images, labels in val_dataloader:
images = images.to(device)
labels = labels.to(device)
output = model(images)
loss = criterion(output, labels)
val_loss += loss.item()
correct_val += (torch.max(output, dim=1)[1] == labels).sum().item()
total_val += labels.size(0)
print(f'Val loss {val_loss / len(val_dataloader):.3f}. Val acc {correct_val / total_val * 100:.3f}.', end=' ')
print(f'Took {time() - iter_time:.3f} seconds')
val_accuracy.append(correct_val / total_val)
iter_time = time()
train_losses.append(running_loss / total_train)
val_losses.append(val_loss / total_val)
print(f'Epoch took {time() - start_time}')
# 保存断点
checkpoint = {"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"epoch": epoch}
path_checkpoint = "./checkpoint_{}_epoch.pkl".format(epoch)
torch.save(checkpoint, path_checkpoint)
return model, train_losses, val_losses, train_accuracy, val_accuracy
定义训练过程,包括前向传播、反向传播、优化,每50次迭代使用验证集进行评估测试,并且输出信息。
print_every = 50 # 每50次迭代输出一次
num_epoch = 5 # 训练的epochs数量
resnet, train_losses, val_losses, train_accuracy, val_accuracy = train(
model=resnet,
criterion=criterion,
optimizer=optimizer,
train_dataloader=train_dataloader,
val_dataloader=val_dataloader,
print_every=print_every,
num_epoch=num_epoch
)
实际调用训练过程,设置多少次迭代输出一次,以及设置训练多少个 epoch。
# 召回变化
plt.rcParams.update({"font.size": 10})
plt.plot(train_losses, label='Training loss')
plt.plot(val_losses, label='Validation loss')
plt.legend(frameon=False)
plt.savefig('/kaggle/working/loss', dpi=250)
plt.show()
使用 matplotlib 绘制损失变化曲线。
# 准确率变化
plt.rcParams.update({"font.size": 10})
plt.plot(train_accuracy, label='Training Accuracy')
plt.plot(val_accuracy, label='Validation Accuracy')
plt.legend(frameon=False)
plt.savefig('/kaggle/working/accuracy', dpi=250)
plt.show()
准确率变化曲线。
y_true = []
y_pred = []
# 测试过程
i, j = 0, 0
for img, label in test_dataset:
img = torch.Tensor(img[None])
img = img.to(device)
resnet.eval()
prediction = resnet(img) # 在 img 最前面加上一维(batch维度)
prediction_class = int(torch.max(prediction, dim=1)[1])
y_pred.append(prediction_class)
y_true.append(label)
测试过程,后续使用 scikit-learn 对测试结果进行评估,评估过程记录每个样本的真实标签 y_true 和预测值(标签)y_pred。
# 正确率
from sklearn.metrics import accuracy_score
accuracy_score(y_true, y_pred)
# 混淆矩阵
from sklearn.metrics import confusion_matrix
conf_matrix = confusion_matrix(y_true, y_pred)
xtick = classes
ytick = classes
plt.rcParams.update({"font.size": 5})
plt.savefig('/kaggle/working/conf_matrix', dpi=250)
sns.heatmap(conf_matrix, fmt='g', cmap='Blues', annot=True, cbar=False, xticklabels=xtick, yticklabels=ytick)
# 精确率
from sklearn.metrics import precision_score
precision_list = precision_score(y_true, y_pred, average=None)
precision_list
# 精确率
from sklearn.metrics import recall_score
recall_list = recall_score(y_true, y_pred, average=None)
recall_list
# 分类准确率/雅卡得相似系数
from sklearn.metrics import jaccard_score
accuracy_list = jaccard_score(y_true, y_pred, average=None)
accuracy_list
# f1 and macro-f1
from sklearn.metrics import f1_score
f1_list = f1_score(y_true, y_pred, average=None)
macro_f1 = f1_score(y_true, y_pred, average='macro')
f1_list, macro_f1
使用 scikit-learn 对结果进行评估,包括正确率、分类准确率、分类召回率、混淆矩阵、F1分数等指标,具体评估方法可以参考 scikit-learn 的参考手册。