数据集采用的是kaagle⽐赛中的Cifar10,⽹址是https://www.kaggle.com/c/cifar-10
比赛数据集分为训练集和测试集,其中训练集包含
50000
张、测试集包含
300000
张图像。
两个数据集中的图像都是
png
格式,⾼度和宽度均为
32
像素并有三个颜⾊通道(
RGB
)。这些图
⽚共涵盖
10
个类别:⻜机、汽⻋、⻦类、猫、⿅、狗、⻘蛙、⻢、船和卡⻋。
一、导入所需要的包和模块
import collections
import math
import os
import shutil
import pandas as pd
import torch
import torchvision
from torch import nn
二、加载并整理数据集
def read_csv_labels(fname):
"""读取fname来给标签字典返回一个文件名"""
with open(fname, 'r') as f:
#只读模式,跳过文件头行(列名)
lines = f.readlines()[1:]
tokens = [l.rstrip().split(',') for l in lines]
#形成二维列表,每个列表中为编号以及标签
return dict(((name, label) for name, label in tokens))
#形成字典key为name,value为label
data_dir='./kaggle/'
labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
print('# 训练样本 :', len(labels))
print('# 类别 :', len(set(labels.values())))
我这里将数据集存放到了一个叫kaggle的文件夹下面
# 训练样本 : 50000 # 类别 : 10
将验证集从原始的训练集就划分出来
#将验证集从原始训练集中拆分出来
def copyfile(filename, target_dir):
"""将文件复制到目标目录"""
os.makedirs(target_dir, exist_ok=True)
#判断并新建指定的目标目录。
shutil.copy(filename, target_dir)
#实现具体的文件复制功能
def reorg_train_valid(data_dir, labels, valid_ratio):
"""将验证集从原始的训练集中拆分出来"""
# 训练数据集中样本最少的类别中的样本数
n = collections.Counter(labels.values()).most_common()[-1][1]
#Counter(lables.values())返回的是一个字典,其中包含了每个标签出现的次数
# most_common()[-1][1]表示将字典从大到小排列,
#并取[-1][1]表示取最后一个键值对中取最小的标签出现次数赋予n
#验证集中每个类别的样本数
n_valid_per_label = max(1, math.floor(n * valid_ratio))
# math.floor具体作用是返回不大于输入参数的最大整数
label_count = {}
for train_file in os.listdir(os.path.join(data_dir, 'train')):
#os.listdir() 函数则返回指定路径下的所有文件和子目录的名称列表。
#循环语句的作用是遍历训练集文件夹中的所有文件名,针对每个文件名进行特定的处理。
label = labels[train_file.split('.')[0]]
#例如将train下面的图片文件1.png中转化为列表['1','png']
#然后[0]提取出数字为索引,然后得到该索引对应的标签为cat,dog之类
fname = os.path.join(data_dir, 'train', train_file)
#每张图片的路径
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'train_valid', label))
if label not in label_count or label_count[label] < n_valid_per_label:
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'valid', label))
label_count[label] = label_count.get(label, 0) + 1
else:
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'train', label))
return n_valid_per_label
#将train数据集划分为train_valid文件,其中文件下有各类文件夹下的图片。
#然后有valid文件,文件下有采集一定数量的各类图片
#剩余数据图片在train里的各类图片
定义
reorg_train_valid
函数来将验证集从原始的训练集中拆分出来。此函数中的参
数
valid_ratio
是验证集中的样本数与原始训练集中的样本数之比。更具体地说,令
n
等于样本最少的类别 中的图像数量,⽽r
是⽐率。验证集将为每个类别拆分出
max
([nr],
1)
张图像。让我们以
valid_ratio=0.1
为 例,由于原始的训练集有50000
张图像,因此
train_valid_test/train
路径中将有
45000
张图像⽤于训练,而剩下5000
张图像将作为路径
train_valid_test/valid
中的验证集。组织数据集后,同类别的图像将放置在同⼀⽂件夹下。
def reorg_test(data_dir):
"""在预测期间整理测试集,以方便读取"""
for test_file in os.listdir(os.path.join(data_dir, 'test')):
copyfile(os.path.join(data_dir, 'test', test_file),
os.path.join(data_dir, 'train_valid_test', 'test',
'unknown'))
def reorg_cifar10_data(data_dir, valid_ratio):
labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
reorg_train_valid(data_dir, labels, valid_ratio)
reorg_test(data_dir)
batch_size = 128
valid_ratio = 0.1
reorg_cifar10_data(data_dir, valid_ratio)
以上图片为划分好数据集后的展示,每个数据集下面都是对应标签的10个类别的图片
三、进行图像增广
#图像增广
transform_train = torchvision.transforms.Compose([
# 在高度和宽度上将图像放大到40像素的正方形
torchvision.transforms.Resize(40),
# 随机裁剪出一个高度和宽度均为40像素的正方形图像,
# 生成一个面积为原始图像面积0.64~1倍的小正方形,
# 然后将其缩放为高度和宽度均为32像素的正方形
torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0),
ratio=(1.0, 1.0)),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
# 标准化图像的每个通道
torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],
[0.2023, 0.1994, 0.2010])])
#测试期间,我们只对图像执⾏标准化,以消除评估结果中的随机性
transform_test = torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],
[0.2023, 0.1994, 0.2010])])
四、读取数据集
#读取数据集
train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_train) for folder in ['train', 'train_valid']]
valid_ds, test_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_test) for folder in ['valid', 'test']]
train_iter, train_valid_iter = [torch.utils.data.DataLoader(
dataset, batch_size, shuffle=True, drop_last=True)
for dataset in (train_ds, train_valid_ds)]
valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,
drop_last=True)
test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,
drop_last=False)
五、模型
def resnet18(num_classes, in_channels=1):
"""稍加修改的ResNet-18模型"""
def resnet_block(in_channels, out_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(d2l.Residual(in_channels, out_channels,
use_1x1conv=True, strides=2))
else:
blk.append(d2l.Residual(out_channels, out_channels))
return nn.Sequential(*blk)
# 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层
net = nn.Sequential(
nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU())
net.add_module("resnet_block1", resnet_block(
64, 64, 2, first_block=True))
net.add_module("resnet_block2", resnet_block(64, 128, 2))
net.add_module("resnet_block3", resnet_block(128, 256, 2))
net.add_module("resnet_block4", resnet_block(256, 512, 2))
net.add_module("global_avg_pool", nn.AdaptiveAvgPool2d((1,1)))
net.add_module("fc", nn.Sequential(nn.Flatten(),
nn.Linear(512, num_classes)))
return net
这里使用的是修改过的resnet18模型,将第一层的7*7的卷积层换为了3*3的卷积层,stride为1,且填充为1,最后删去了最大汇聚层。
def get_net():
num_classes = 10
net=resnet18(num_classes, 3)
return net
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
六、训练
from torch.utils.tensorboard import SummaryWriter
train_data_size = len(train_ds)
test_data_size = len(valid_ds)
model = get_net()
init_weights(model)
device = torch.device("cuda")
#损失函数
loss_fn = nn.CrossEntropyLoss()
loss_fn = loss_fn.to(device)
# 设置训练网络的一些参数
# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0
# 训练的轮数
epoch = 250
# 优化器
# learning_rate = 0.01
# 1e-2=1 x (10)^(-2) = 1 /100 = 0.01
learning_rate = 1e-2
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
# 添加tensorboard
writer = SummaryWriter("../logs_train")
for i in range(epoch):
print("-------第 {} 轮训练开始-------".format(i+1))
# 训练步骤开始
model.train()
for data in train_iter:
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
model=model.to(device)
outputs = model(imgs)
loss = loss_fn(outputs, targets)
# 优化器优化模型
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_train_step = total_train_step + 1
if total_train_step % 100 == 0:
print("训练次数:{}, Loss: {}".format(total_train_step, loss.item()))
writer.add_scalar("train_loss", loss.item(), total_train_step)
# 测试步骤开始
model.eval()
total_test_loss = 0
total_accuracy = 0
with torch.no_grad():
for data in valid_iter:
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
outputs = model(imgs)
loss = loss_fn(outputs, targets)
total_test_loss = total_test_loss + loss.item()
accuracy = (outputs.argmax(1) == targets).sum()
total_accuracy = total_accuracy + accuracy
print("整体测试集上的Loss: {}".format(total_test_loss))
print("整体测试集上的正确率: {}".format(total_accuracy/test_data_size))
writer.add_scalar("test_loss", total_test_loss, total_test_step)
writer.add_scalar("test_accuracy", total_accuracy/test_data_size, total_test_step)
total_test_step = total_test_step + 1
torch.save(model, "resnet_{}.pth".format(i))
print("模型已保存")
writer.close()
训练次数设置为250,学习率为0.01,优化器为SGD,用tensorboard可视化训练过程,并保存训练参数。最后训练发现验证集的准确率为0.875左右。
本文的部分代码参考自《动手学深度学习》以及训练部分参考tudui