kaggle关于树叶的竞赛总结

kaggle关于树叶的竞赛总结

预测叶子图像的类别,该数据集包含 176 个类别,18353 个训练图像,8800 个测试图像。
每个类别至少有 50 张图像用于训练,测试集平均分为公共和私人排行榜,网址为:https://www.kaggle.com/competitions/classify-leaves/code
由于images文件夹包含测试集和训练集所有图像,因此需要手动把测试集和训练集分开,所有代码如下,运行在一个GPU上面(lr,weight_decay,epochs = 1e-4, 1e-4, 100):

一、导入相关包和定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import collections
import math
import os.path
import shutil

import d2l.torch
import torch
import torchvision.transforms
import torch.utils.data
from torch import nn


def read_csv_data(fname):
# 打开指定的 CSV 文件,使用 'r' 模式表示只读
with open(fname, 'r') as f:
# 读取文件的所有行,并去掉第一行(标题行)
lines = f.readlines()[1:]

# line.rstrip() 去掉每行末尾的换行符
# 读取文件中每一行数据,去除每行末尾的换行符,并按逗号分隔
tokens = [line.rstrip().split(',') for line in lines]

# 创建一个字典,字典的键是图片的索引号,值是对应的标签
# name.split('.')[0] 获取文件名部分,去掉扩展名
# name.split('/')[1] 获取文件名部分,去掉路径
return dict(((name.split('.')[0].split('/')[1], label) for name, label in tokens))


def copy_file(fname, target_dir):
# 创建文件夹,如果存在,就不再重复创建
os.makedirs(name=target_dir, exist_ok=True)
# 将源文件图片复制到指定文件夹下
shutil.copy(fname, target_dir)


# 从训练集中拆分一部分图片用作验证集,然后复制到指定文件夹下面
def split_copy_train_valid_test(data_dir, labels, split_to_valid_ratio):
# 使用collections.Counter()函数对训练集类别数目进行计数,然后从大到小排列,获取最少的一类数目
split_num = collections.Counter(labels.values()).most_common()[-1][1]
# 计算从训练集中每一类需要选出多少个样本作为验证集,确保至少有一个样本
num_valid_per_label = max(1, math.floor(split_num * split_to_valid_ratio))
valid_label_count = {}
absolute_path = os.path.join(data_dir, 'train_valid_test')
for image_file in os.listdir(os.path.join(data_dir, 'images')):
# 获取当前图片的标签
key = image_file.split('.')[0]
#print(".....", key)
if key in labels:
label = labels[key]
train_file_path = os.path.join(data_dir, 'images', image_file)
# 复制训练集的图片到'train_valid'文件夹下
copy_file(train_file_path, os.path.join(absolute_path, 'train_valid', label))
if label not in valid_label_count or valid_label_count[label] < num_valid_per_label:
# 复制训练集的图片到'valid'文件夹下
copy_file(train_file_path, os.path.join(absolute_path, 'valid', label))
valid_label_count[label] = valid_label_count.get(label, 0) + 1
else:
# 复制训练集的图片到'train'文件夹下
copy_file(train_file_path, os.path.join(absolute_path, 'train', label))
else:
# 如果图片没有对应的标签,将其复制到'test'文件夹下的'unknown'子文件夹
copy_file(os.path.join(data_dir, 'images', image_file),
os.path.join(data_dir, 'train_valid_test', 'test', 'unknown'))
return num_valid_per_label


def copy_classify_leaves_data(data_dir, split_to_valid_ratio):
labels = read_csv_data(fname=os.path.join(data_dir, 'train.csv'))
split_copy_train_valid_test(data_dir, labels, split_to_valid_ratio)

二、读 176 个类别的标签文件

1
2
3
4
5
6
7
8
9
10
data_dir = './'

# 读取训练数据集的标签文件
labels = read_csv_data(os.path.join(data_dir, 'train.csv'))
# 打印样本数,即标签数据的条目数
print('样本数:', len(labels))
# 打印类别数,即标签数据中不同类别的数量
print('类别数:', len(set(labels.values())))

#print(labels.keys())
1
2
样本数: 18353
类别数: 176

三、拆分验证集

1
2
3
4
batch_size = 32
num_classes = 176
split_to_valid_ratio = 0.1
copy_classify_leaves_data(data_dir, split_to_valid_ratio)

四、数据增广

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
transform_train = torchvision.transforms.Compose([
torchvision.transforms.Resize(256),
torchvision.transforms.RandomResizedCrop(96, scale=(0.64, 1.0), ratio=(1.0, 1.0)),
torchvision.transforms.RandomHorizontalFlip(),
# 随机更改亮度,对比度和饱和度
torchvision.transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4),
# 随机旋转
torchvision.transforms.RandomRotation(30),
# 随机平移
torchvision.transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

transform_test = torchvision.transforms.Compose([
torchvision.transforms.Resize(96),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

五、ImageFolder重新组织数据集

1
2
3
4
5
6
7
8
9
10
11
12
train_datasets, train_valid_datasets = [torchvision.datasets.ImageFolder(
root=os.path.join(data_dir, 'train_valid_test', folder), transform=transform_train) for folder in
['train', 'train_valid']]
test_datasets, valid_datasets = [
torchvision.datasets.ImageFolder(root=os.path.join(data_dir, 'train_valid_test', folder), transform=transform_test)
for folder in ['test', 'valid']]
#创建数据集迭代器
train_iter, train_valid_iter = [
torch.utils.data.DataLoader(dataset=ds, batch_size=batch_size, shuffle=True, drop_last=True) for ds in
[train_datasets, train_valid_datasets]]
test_iter, valid_iter = [torch.utils.data.DataLoader(dataset=ds, batch_size=batch_size, shuffle=False, drop_last=False)
for ds in [test_datasets, valid_datasets]]

六、定义损失函数为交叉熵损失,不进行损失值的平均或求和

1
loss = nn.CrossEntropyLoss(reduction='none')

七、定义一个函数 get_net,用于创建并返回一个 ResNet18/50 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_net():
# 使用预训练 ResNet50
net = torchvision.models.resnet50(weights=torchvision.models.ResNet50_Weights.IMAGENET1K_V1)

# 修改全连接层(简化结构)
net.fc = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(2048, num_classes)
)

# 初始化分类层权重
nn.init.kaiming_normal_(net.fc[1].weight, mode='fan_out', nonlinearity='relu')
nn.init.constant_(net.fc[1].bias, 0)

return net
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 修改 get_net() 全连接层,增加特征交互能力
def get_net():
net = torchvision.models.resnet50(weights=torchvision.models.ResNet50_Weights.IMAGENET1K_V1)
net.fc = nn.Sequential(
nn.Linear(2048, 1024),
nn.BatchNorm1d(1024),
nn.SiLU(), # 比 ReLU 更平滑
nn.Dropout(0.3),
nn.Linear(1024, num_classes)
)
# 初始化策略
nn.init.kaiming_normal_(net.fc[0].weight, mode='fan_out')
nn.init.constant_(net.fc[0].bias, 0)
# 返回创建的模型
return net

八、定义一个函数 train,用于训练模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def train(net, train_iter, valid_iter, num_epochs, lr, weight_decay, lr_period, lr_decay, devices):
# 定义优化器函数,使用随机梯度下降(SGD)优化器
optim = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9, weight_decay=weight_decay)
# 定义学习率调度器,每隔 lr_period 轮,学习率衰减为 lr * lr_decay

# 替换 StepLR 为 CosineAnnealingLR
# lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer=optim, step_size=lr_period, gamma=lr_decay)
lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer=optim,
T_max=num_epochs, # 总训练周期数
eta_min=1e-6 # 最小学习率下限
)

# 初始化计时器和训练批次数
timer, num_batches = d2l.torch.Timer(), len(train_iter)
# 初始化图例,用于绘制训练损失和训练准确率
legend = ['train loss', 'train acc']
# 如果有验证集,添加验证准确率到图例中
if valid_iter is not None:
legend.append('valid acc')
# 初始化动画绘制器,用于绘制训练过程中的指标
animator = d2l.torch.Animator(xlabel='epoch', xlim=[1, num_epochs],legend=legend)

# 使用 DataParallel 实现多 GPU 并行计算,并将模型移动到主 GPU 上
net = nn.DataParallel(module=net, device_ids=devices).to(devices[0])
# 初始化每轮的计时器
timer_epoch = d2l.torch.Timer()

# 初始化早停法的参数
# patience = 5 # 连续5轮没有提升就停止训练
# min_delta = 0.001 # 验证集精度提升的最小阈值
# no_improvement_count = 0 # 记录验证集准确率没有提升的轮数

best_valid_acc = 0.0

# 开始训练循环
for epoch in range(num_epochs):
timer_epoch.start() # 开始计时当前轮
accumulator = d2l.torch.Accumulator(3) # 初始化累加器,用于累加损失、准确率和样本数
net.train() # 将模型设置为训练模式
# 遍历训练集中的每个批次
for i, (X, y) in enumerate(train_iter):
timer.start() # 开始计时当前批次
# 计算当前批次的损失和准确率
batch_loss, batch_acc = d2l.torch.train_batch_ch13(net, X, y, loss, optim, devices)
# 累加当前批次的损失、准确率和样本数
accumulator.add(batch_loss, batch_acc, y.shape[0])
timer.stop() # 结束计时当前批次
# 每隔 (num_batches // 5) 个批次或最后一个批次,更新动画绘制器
if i % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(accumulator[0] / accumulator[2], accumulator[1] / accumulator[2], None))
timer_epoch.stop() # 结束计时当前轮
net.eval() # 将模型设置为评估模式,用于验证集
# 记录当前轮的训练损失和训练准确率
measures = f'train loss {accumulator[0] / accumulator[2]}, train acc {accumulator[1] / accumulator[2]},\n'
# 如果有验证集,计算验证集的准确率并更新动画绘制器
if valid_iter is not None:
valid_acc = d2l.torch.evaluate_accuracy_gpu(net, valid_iter, devices[0])
animator.add(epoch + 1, (None, None, valid_acc))
measures += f'valid acc {valid_acc},'
# 早停法
if valid_acc > best_valid_acc:
best_valid_acc = valid_acc
# no_improvement_count = 0
torch.save(net.state_dict(), 'best_model.pth') # 保存最佳模型
print(f'Saved the best model at epoch {epoch + 1}, valid acc {valid_acc:.3f}')
# else:
# no_improvement_count += 1
# if no_improvement_count >= patience:
# print(f'Early stopping at epoch {epoch + 1}')
# break
# 更新学习率调度器,判断是否需要进行学习率衰减
lr_scheduler.step()
# 打印训练过程中的各项指标,包括训练损失、训练准确率、验证准确率、每秒处理的样本数和每轮的平均时间
print(
measures + f'\n{num_epochs * accumulator[2] / timer.sum()} examples/sec and {timer_epoch.avg():.1f}秒/轮, on {str(devices[0])}')

九、超参数定义

1
2
3
lr, weight_decay, epochs = 1e-4, 1e-4, 100
lr_decay, lr_period, net = 0.5, 10, get_net()
devices = d2l.torch.try_all_gpus()

十、带valid训练

1
2
net.train()
train(net, train_iter, valid_iter, epochs, lr, weight_decay, lr_period, lr_decay, devices)

十一、不带valid训练

这一部分相当于进行训练完后再使用全部训练集重新训练一遍,检测效果

1
2
3
net.eval()
#当训练完后,得到合适的超参数,然后重新把之前训练集和验证集合在一起重新进行训练,得到训练好的网络,用于预测
train(net, train_valid_iter, None, epochs, lr, weight_decay, lr_period, lr_decay, devices)

最后训练好的模型用于测试集


kaggle关于树叶的竞赛总结
http://example.com/2025/05/25/kaggle关于树叶的竞赛总结/
作者
Alaskaboo
发布于
2025年5月25日
更新于
2025年6月28日
许可协议