1. 从零开始:为什么选择Kaggle树叶分类竞赛作为你的第一个实战项目?
如果你刚学完一些深度学习的理论,或者跟着教程敲过几行代码,但心里总感觉不踏实,不知道这些知识怎么用在真实世界里,那你来对地方了。Kaggle的树叶分类竞赛,简直就是为新手量身定做的“毕业设计”。我第一次接触这个比赛时,感觉它完美地卡在了“有挑战性”和“可实现”的甜蜜点上。它不像那些动辄几百万张图片、需要几周训练时间的庞然大物,也不至于简单到用几行代码就能搞定,失去学习的意义。
这个比赛的任务很直观:给你一堆树叶的图片,让你训练一个模型,能准确识别出它是哪种树。听起来是不是很像我们小时候玩的“看图识物”?只不过这次,我们是用神经网络这个“超级大脑”来玩。数据量适中,类别清晰,目标明确,这让我们可以专注于模型构建、训练和优化的核心流程,而不会被复杂的数据清洗、海量存储或者分布式训练这些进阶问题一下子吓退。
更重要的是,这个项目能给你带来一个完整的、闭环的深度学习项目体验。从下载数据、理解数据格式,到亲手搭建一个经典的ResNet模型,再到训练、调试、评估,最后生成提交文件,整个过程你都能自己走一遍。这种成就感,是看十篇教程都比不上的。我当年做完这个项目后,对PyTorch的理解和信心都上了一个大台阶。所以,别犹豫了,跟着我一起,咱们不用任何现成的“黑箱”工具,就从最基础的代码开始,手搓一个ResNet18,把这个比赛拿下。
2. 数据准备:你的第一步不是写代码,而是“看”数据
很多新手朋友一拿到项目就急着打开编辑器写模型,这是个大坑。我踩过,所以告诉你:磨刀不误砍柴工,花半小时好好看看你的数据,能省去后面好几天的调试时间。
首先,我们去Kaggle竞赛页面把数据下载下来。解压后,你会看到几个文件:一个images文件夹,里面装满了树叶图片;一个train.csv文件,这是训练集的标签;一个test.csv文件,这是测试集的信息。我们先打开train.csv看看,它通常长这样:
| image | label |
|---|---|
| images/0.jpg | quercus_robur |
| images/1.jpg | acer_campestre |
| ... | ... |
第一列是图片在images文件夹下的路径,第二列是树叶的类别标签,比如“quercus_robur”(夏栎)。这里有个关键点:标签是英文的物种名。模型最终需要输出的是数字,所以我们必须把这些文本标签转换成数字编码,这个过程叫“标签编码”。同时,我们还得把这个映射关系记下来,因为最后预测出数字后,还得再转换回英文名才能提交。
接下来,我们得看看图片本身。用几行简单的Python脚本,或者直接在文件夹里预览一下,你会发现图片的尺寸、亮度、角度可能都不一样。有的叶子是完整的,有的可能只拍了一部分;有的背景干净,有的背景复杂。这种多样性是好事,也是挑战。好事在于它能让模型更鲁棒,挑战在于我们需要通过数据增强来人工创造更多的多样性,帮助模型学习。
这里我分享一个我自己的小习惯:在写任何数据处理代码前,我会随机挑几十张图片,用PIL或OpenCV打开看看,直观感受一下数据的“质感”。比如,我发现这个数据集里有些图片比较暗,那我在做数据增强时,就可能会有意地加入一些亮度调整。这个步骤看似多余,但它能帮你建立对数据的“直觉”,后面调参时心里更有底。
3. 打造高效数据管道:PyTorch Dataset和DataLoader实战
数据处理是深度学习工程里的重头戏,写得好,训练顺风顺水;写得烂,各种Bug和性能瓶颈能把你搞崩溃。PyTorch提供了Dataset和DataLoader这两个神器,我们来好好用上。
3.1 创建自定义Dataset类
我们要创建两个Dataset类,一个用于训练和验证,一个专门用于最后的测试预测。因为训练集需要图片和标签,而测试集只需要图片。下面这个CustomDataset类是核心:
import pandas as pd
from PIL import Image
from torch.utils.data import Dataset
from sklearn.preprocessing import LabelEncoder
class CustomDataset(Dataset):
def __init__(self, csv_file, img_dir, transform=None):
"""
Args:
csv_file (string): CSV文件路径(如'train.csv')。
img_dir (string): 图片文件夹的根目录路径。
transform (callable, optional): 可选的数据增强/转换函数。
"""
self.data = pd.read_csv(csv_file)
self.img_dir = img_dir
self.transform = transform
# 关键步骤:标签编码
self.label_encoder = LabelEncoder()
# 将文本标签(如‘quercus_robur’)转换为数字(0,1,2...)
self.data['label'] = self.label_encoder.fit_transform(self.data['label'])
# 保存这个编码器,预测时要用它把数字变回文字
self.classes = self.label_encoder.classes_
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
# 构造完整的图片路径
img_name = self.data.iloc[idx, 0] # 假设第一列是‘image’
img_path = os.path.join(self.img_dir, img_name)
# 读取图片
image = Image.open(img_path).convert('RGB') # 确保是三通道
label = self.data.iloc[idx, 1] # 假设第二列是‘label’,此时已是数字
if self.transform:
image = self.transform(image)
return image, label
这里有几个容易踩坑的细节:
- 路径拼接:一定要用
os.path.join,而不是字符串相加,这样在不同操作系统(Windows/macOS/Linux)上都能工作。 - 图片模式:用
.convert('RGB')强制转换,能避免一些灰度图或带透明通道(RGBA)的图片引发维度错误。 - 标签编码器保存:
self.label_encoder和self.classes一定要作为属性保存下来!这是很多新手会忘的。等你训练完模型,对测试集进行预测,得到的是数字0,1,2...,你必须用同一个编码器的inverse_transform方法,才能把这些数字变回‘quercus_robur’这样的提交格式。
3.2 设计强大的数据增强策略
数据增强是防止模型过拟合、提升泛化能力的廉价且有效的方法。对于图像任务,我们可以做很多操作。下面这个transform组合是我经过多次实验后,觉得对这个树叶分类任务比较有效的:
from torchvision import transforms
train_transform = transforms.Compose([
# 随机缩放裁剪:模拟拍摄时距离和角度的变化
transforms.RandomResizedCrop(224, scale=(0.08, 1.0), ratio=(3./4., 4./3.)),
# 随机水平翻转:叶子可能朝左也可能朝右
transforms.RandomHorizontalFlip(p=0.5),
# 随机旋转:叶子在自然界中角度是任意的
transforms.RandomRotation(degrees=15),
# 颜色抖动:模拟光照、季节变化
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
# 转换为Tensor,并归一化
transforms.ToTensor(),
# 使用ImageNet的均值和标准差进行归一化(这是通用做法,因为很多预训练模型用这个)
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
对于验证集和测试集,我们不应该使用这些带有“Random”的增强,否则每次评估的结果都会波动。我们只需要进行确定性的 resize、中心裁剪和归一化:
val_test_transform = transforms.Compose([
transforms.Resize(256), # 先将短边缩放到256
transforms.CenterCrop(224), # 再从中心裁剪出224x224
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
3.3 用DataLoader组装数据流水线
有了Dataset和transform,我们就可以用DataLoader把它们组装成可以迭代的数据流了。DataLoader会帮我们处理批处理(batching)、打乱数据(shuffling)和多进程数据加载,极大地简化了我们的工作。
from torch.utils.data import DataLoader
# 假设你的数据路径
train_csv = './classify-leaves/train.csv'
img_dir = './classify-leaves/images'
# 创建训练集和验证集Dataset
full_dataset = CustomDataset(train_csv, img_dir, transform=train_transform)
# 手动划分训练集和验证集,比如7:3
train_size = int(0.7 * len(full_dataset))
val_size = len(full_dataset) - train_size
train_dataset, val_dataset = torch.utils.data.random_split(full_dataset, [train_size, val_size])
# 注意:验证集应该用val_test_transform,这里为了演示,假设我们重新创建
val_dataset.dataset.transform = val_test_transform # 修改子数据集的transform
# 创建DataLoader
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2, pin_memory=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=2, pin_memory=True)
参数解释:
batch_size=32:一次训练喂入32张图片。太小了训练慢、不稳定,太大了可能内存放不下。32或64是常见的起点。shuffle=True:每个epoch(完整遍历一遍数据)开始时打乱数据顺序,防止模型学到数据的顺序信息。num_workers=2:用2个子进程来加载数据,可以加速数据准备。根据你的CPU核心数调整。pin_memory=True:如果使用GPU,这个设置可以将数据锁在主机内存中,加速向GPU的传输。
4. 亲手搭建ResNet18:理解残差连接的精髓
现在来到最激动人心的部分:从零开始构建ResNet18。你可能听说过ResNet很深,解决了梯度消失问题,但它的核心思想其实非常直观——捷径连接。
想象一下你在学一道复杂的数学题。如果直接从头推导,很容易在中间某步卡住。但如果参考答案给了你一个关键的中间公式(一条“捷径”),你就能绕过复杂的推导,直接利用这个结果继续前进。ResNet的残差块就是给网络提供了这样的“捷径”。
4.1 构建基础的残差块
残差块是ResNet的基石。它的目标不是让网络直接学习一个复杂的映射 H(x),而是学习残差 F(x) = H(x) - x。这样,原本的映射就变成了 H(x) = F(x) + x。这个“+ x”就是那条捷径。
import torch
import torch.nn as nn
import torch.nn.functional as F
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(ResidualBlock, self).__init__()
# 主路径:两个卷积层
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 捷径路径:如果输入输出维度不一致(通道数不同或空间尺寸不同),就需要一个1x1卷积来调整
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
# 主路径 forward
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out)) # 注意:这里先不加ReLU!
# 加上捷径
out += self.shortcut(x)
# 最后再加ReLU激活
out = F.relu(out)
return out
关键点剖析:
bias=False:因为后面紧跟着批归一化(BatchNorm)层,它自带可学习的偏移参数,所以卷积层可以省去bias,减少参数且不影响性能。shortcut:这是残差连接的核心。只有当输入和输出的维度(通道数和尺寸)完全匹配时,才能直接相加(x)。如果不匹配(比如下采样时stride=2,或者通道数变化),就需要用一个1x1的卷积(配合BatchNorm)来将x投影到正确的维度。这个1x1卷积就像一座桥,让两条路径得以汇合。- ReLU的位置:注意在主路径中,第一个卷积后有ReLU,但第二个卷积后没有立即加ReLU。这是原论文的设计,残差相加之后再加最后一个ReLU。这个顺序很重要。
4.2 组装完整的ResNet18模型
ResNet18就是由一堆这样的残差块堆叠起来的。名字里的“18”大概是指有18层带参数的层(卷积层和全连接层)。它的结构是:开头一个卷积和池化层,然后4个“阶段”(stage),每个阶段由若干个残差块组成,最后是一个全局平均池化和全连接层。
class ResNet(nn.Module):
def __init__(self, block, num_blocks, num_classes=176): # 树叶分类有176类
super(ResNet, self).__init__()
self.in_channels = 64 # 初始通道数
# 入口层:快速降低图片尺寸,提取初步特征
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 四个阶段(stage)
self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1) # 输出尺寸:56x56
self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2) # 输出尺寸:28x28
self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2) # 输出尺寸:14x14
self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2) # 输出尺寸:7x7
# 分类头
self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 自适应平均池化到1x1
self.fc = nn.Linear(512, num_classes)
def _make_layer(self, block, out_channels, num_blocks, stride):
"""构建一个由多个残差块组成的阶段。"""
layers = []
# 第一个块可能需要下采样(stride可能为2)
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels
# 后续的块,输入输出通道数一致,stride为1
for _ in range(1, num_blocks):
layers.append(block(out_channels, out_channels, stride=1))
return nn.Sequential(*layers)
def forward(self, x):
# 入口层
x = F.relu(self.bn1(self.conv1(x))) # 输出尺寸减半
x = self.maxpool(x) # 输出尺寸再减半
# 四个阶段
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
# 分类头
x = self.avgpool(x) # [batch, 512, 1, 1]
x = torch.flatten(x, 1) # [batch, 512]
x = self.fc(x) # [batch, num_classes]
return x
如何实例化一个ResNet18?
ResNet18的配置是每个阶段有2个残差块,即 num_blocks = [2, 2, 2, 2]。
def ResNet18(num_classes=176):
return ResNet(ResidualBlock, [2, 2, 2, 2], num_classes=num_classes)
# 创建模型
model = ResNet18(num_classes=176)
print(model) # 可以打印看看网络结构
亲手敲完这些代码,你对ResNet的理解会比单纯调用torchvision.models.resnet18()深刻得多。你会明白每一层在做什么,维度是如何变化的,残差连接具体加在了哪里。这种“造轮子”的经历对于打牢基础至关重要。
5. 模型训练与调优:让模型真正“学”起来
模型搭好了,数据管道也建好了,接下来就是让它们运转起来。训练一个深度学习模型就像教小孩认字,需要耐心、合适的方法,还要不断检查他学得怎么样。
5.1 编写训练和验证循环
训练循环的核心步骤是:前向传播 -> 计算损失 -> 反向传播 -> 更新参数。验证循环则去掉反向传播和参数更新。
def train_one_epoch(model, train_loader, criterion, optimizer, device, epoch):
model.train() # 切换到训练模式(影响Dropout、BatchNorm等层)
running_loss = 0.0
correct = 0
total = 0
# 使用tqdm添加进度条,训练时看着更直观
from tqdm import tqdm
pbar = tqdm(train_loader, desc=f'Epoch {epoch+1} [Train]')
for images, labels in pbar:
images, labels = images.to(device), labels.to(device)
# 清零梯度
optimizer.zero_grad()
# 前向传播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向传播和优化
loss.backward()
optimizer.step()
# 统计信息
running_loss += loss.item() * images.size(0)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
# 更新进度条描述
pbar.set_postfix({'Loss': loss.item(), 'Acc': correct/total})
epoch_loss = running_loss / total
epoch_acc = correct / total
return epoch_loss, epoch_acc
def validate(model, val_loader, criterion, device):
model.eval() # 切换到评估模式
running_loss = 0.0
correct = 0
total = 0
with torch.no_grad(): # 关闭梯度计算,节省内存和计算
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
running_loss += loss.item() * images.size(0)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
epoch_loss = running_loss / total
epoch_acc = correct / total
return epoch_loss, epoch_acc
5.2 选择损失函数和优化器
对于多分类任务,交叉熵损失(CrossEntropyLoss) 是标准选择。它结合了Softmax和负对数似然,非常适合。
优化器我推荐使用 Adam,它对学习率不那么敏感,通常能给出一个不错的起点。但也可以尝试 SGD with Momentum,它在调优得当后可能达到更好的最终精度。
import torch.optim as optim
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = ResNet18(num_classes=176).to(device)
criterion = nn.CrossEntropyLoss() # 损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001) # 优化器,初始学习率0.001
# 也可以试试SGD: optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
# 学习率调度器:在训练过程中动态降低学习率,有助于模型后期收敛
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1) # 每30个epoch,学习率乘0.1
5.3 开始训练并监控过程
现在,我们把所有部分组合起来,进行多轮(epoch)训练。同时,我们要记录训练和验证的损失、准确率,以便后续分析。
num_epochs = 100
history = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': []}
for epoch in range(num_epochs):
train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device, epoch)
val_loss, val_acc = validate(model, val_loader, criterion, device)
# 记录历史
history['train_loss'].append(train_loss)
history['train_acc'].append(train_acc)
history['val_loss'].append(val_loss)
history['val_acc'].append(val_acc)
# 打印信息
print(f'Epoch {epoch+1:03d}/{num_epochs} | '
f'Train Loss: {train_loss:.4f} Acc: {train_acc:.4f} | '
f'Val Loss: {val_loss:.4f} Acc: {val_acc:.4f} | '
f'LR: {scheduler.get_last_lr()[0]:.6f}')
# 更新学习率
scheduler.step()
# (可选)保存验证集上最好的模型
# if val_acc > best_acc:
# best_acc = val_acc
# torch.save(model.state_dict(), 'best_model.pth')
训练过程中,你要密切关注两个指标:损失(Loss) 和 准确率(Accuracy)。理想情况下,训练损失和验证损失都应该稳步下降,训练准确率和验证准确率都应该稳步上升。如果出现训练准确率很高但验证准确率上不去(过拟合),或者两者都很低(欠拟合),就需要调整策略了。
6. 模型评估与预测:检验成果并生成提交文件
训练完成后,我们不仅要看最后的准确率数字,还要可视化训练过程,并对测试集进行预测,生成Kaggle要求的提交文件。
6.1 可视化训练曲线
画图能直观地告诉我们模型学得怎么样。
import matplotlib.pyplot as plt
def plot_training_history(history):
epochs = range(1, len(history['train_loss']) + 1)
plt.figure(figsize=(12, 4))
# 绘制损失曲线
plt.subplot(1, 2, 1)
plt.plot(epochs, history['train_loss'], 'b-', label='Training Loss')
plt.plot(epochs, history['val_loss'], 'r-', label='Validation Loss')
plt.title('Training and Validation Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
# 绘制准确率曲线
plt.subplot(1, 2, 2)
plt.plot(epochs, history['train_acc'], 'b-', label='Training Accuracy')
plt.plot(epochs, history['val_acc'], 'r-', label='Validation Accuracy')
plt.title('Training and Validation Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
plot_training_history(history)
通过曲线,你可以判断:
- 是否过拟合:训练损失持续下降,但验证损失在某个点后开始上升。这说明模型只记住了训练数据,没学会泛化。
- 是否欠拟合:训练和验证的损失/准确率都很差,且早早就停滞不前。这说明模型能力不足或训练不够。
- 学习率是否合适:如果损失曲线剧烈震荡,可能是学习率太高;如果下降极其缓慢,可能是学习率太低。
6.2 对测试集进行预测
我们之前为测试集创建了一个PredictionDataset(只返回图片,没有标签)。现在用训练好的模型给它做预测。
def predict_test_set(model, test_loader, device):
model.eval()
all_preds = []
all_ids = [] # 如果需要保存图片ID的话
with torch.no_grad():
for batch in test_loader:
# 注意:测试Dataset只返回image,不返回label
images = batch.to(device)
outputs = model(images)
_, preds = torch.max(outputs, 1)
all_preds.extend(preds.cpu().numpy()) # 转移到CPU并转成numpy
return all_preds
# 创建测试集DataLoader (batch_size可以大一些,比如64)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=2)
predictions = predict_test_set(model, test_loader, device)
6.3 生成提交文件
预测得到的是数字标签(0,1,2...),我们需要用之前保存在CustomDataset里的label_encoder,把它们变回原始的英文物种名。
# 假设我们有一个train_dataset对象,里面保存了label_encoder
# 在实际代码中,你可能需要将这个编码器保存下来,或者从训练数据中重新拟合一个
label_encoder = train_dataset.label_encoder
# 将数字预测转换回文本标签
predicted_labels = label_encoder.inverse_transform(predictions)
# 读取测试集CSV,获取图片文件名
test_df = pd.read_csv('./classify-leaves/test.csv')
submission_df = pd.DataFrame({
'image': test_df['image'], # 图片文件名列
'label': predicted_labels # 预测的标签列
})
# 保存为CSV文件
submission_df.to_csv('my_submission.csv', index=False)
print("提交文件已保存为 'my_submission.csv',快去Kaggle提交吧!")
把这个my_submission.csv文件上传到Kaggle竞赛的提交页面,就能看到你的模型在隐藏测试集上的分数和排名了。第一次提交看到分数时,那种感觉真的很棒。
7. 性能提升与进阶技巧:从能跑到跑得好
如果你的第一个模型成绩不太理想,别灰心,这太正常了。深度学习很大程度上是一个“炼丹”过程,需要不断调试。这里分享几个我试过且有效的提升技巧。
1. 使用预训练模型进行迁移学习 这是我们最厉害的“外挂”。与其从头训练ResNet18,不如使用在ImageNet上预训练好的模型。ImageNet有上千万张图片,模型已经学会了识别边缘、纹理、形状等通用特征,我们只需要针对树叶分类这个特定任务微调(Fine-tune)最后几层即可。
import torchvision.models as models
def get_pretrained_resnet18(num_classes):
# 加载预训练模型,pretrained=True会自动下载权重
model_ft = models.resnet18(pretrained=True)
# 冻结所有卷积层的参数(不更新)
# for param in model_ft.parameters():
# param.requires_grad = False
# 更常见的做法:只冻结前面的层,微调后面的层
# 例如,冻结除了最后一层(fc)之外的所有层
# for name, param in model_ft.named_parameters():
# if 'fc' not in name:
# param.requires_grad = False
# 替换最后的全连接层,以适应我们的分类数(176类)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, num_classes)
return model_ft
使用预训练模型通常能让你在更少的epoch内获得更高的准确率。记得要用和预训练时相同的图像归一化参数(就是之前用的ImageNet的mean和std)。
2. 尝试更深的网络或集成学习 ResNet18只是起点。你可以轻松换成ResNet34, ResNet50,甚至更复杂的ResNeXt、EfficientNet等。在Kaggle上,为了冲击更高的排名,模型集成是常见策略。简单来说,就是训练多个不同的模型(比如ResNet18, ResNet50, DenseNet),然后对它们的预测结果进行投票或平均,往往能获得比单个模型更好的效果。
3. 更精细的数据增强与处理 除了我们之前用的,还可以尝试:
CutMix或MixUp:将两张图片混合在一起生成新样本,能显著提升模型鲁棒性。AutoAugment或RandAugment:自动搜索或随机选择增强策略组合。- 针对树叶的特点,可以尝试模拟不同季节的颜色变化、叶片残缺等。
4. 调整超参数
- 学习率:这是最重要的超参数。可以尝试学习率预热(Warmup)、余弦退火(Cosine Annealing)等更先进的调度策略。
- 批大小:增大批大小可能使训练更稳定,但需要调整学习率(通常线性缩放)。
- 优化器:试试AdamW(Adam的改进版,权重衰减处理更正确)或者带动量的SGD。
- 权重衰减:一种正则化,防止过拟合。Adam优化器中的
weight_decay参数就是它。
5. 使用交叉验证 我们之前只是简单地将训练集按7:3划分。更严谨的做法是使用K折交叉验证,用K-1折训练,1折验证,循环K次,最后取平均。这样得到的模型性能评估更可靠,模型也更稳健。
这个过程可能会反复很多次,记录下你每次修改的参数和结果(可以用Excel或专门的工具如Weights & Biases)。慢慢地,你就能找到最适合这个数据集的“配方”。记住,提升模型性能没有银弹,需要耐心地实验、分析和迭代。当你看到自己的排名一点点往上爬的时候,所有的努力都是值得的。
206



被折叠的 条评论
为什么被折叠?



