今天又来啦记笔记啦!
下载完成后,把我们的数据集拖到 Cloud Studio 工作台中,等待上传完成
上传完成后,移动到顺眼的目录 unzip 解压一下
看一下解压后的标签,可以看到标签的风格是 YOLO 风格的,其中每个标签的第一列是类别,后面是归一化的边界框坐标。不过我想先尝试一下不用 label,不对内容进行目标检测,仅仅用图片名称的分类数据来进行分类检测
然后就可以开工啦!
Kaggle 数据集 “Bird vs Drone” 是个很不错的入门项目,适合我这种小白练习分类任务(鸟 vs 无人机),也可以延伸到目标检测或视频分析。
这个数据集的目标是 区分图像中是鸟还是无人机。常见任务有两个方向:
你需要:
从 Kaggle 页面描述看,这个数据集大概包含:
照例先来个简单流程:
首先,我们需要创建一个数据集类,用来加载图像和对应的标签。这里我们会使用 torchvision
和 PIL
来处理图像,以及 torch.utils.data.Dataset
来定义我们自己的数据集。
class BirdVsDroneSimpleDataset(Dataset):
def __init__(self, img_dir, transform=None):
self.img_dir = img_dir
self.transform = transform
self.img_paths = [os.path.join(img_dir, fname) for fname in os.listdir(img_dir) if fname.endswith('.jpg')]
def __len__(self):
return len(self.img_paths)
def __getitem__(self, idx):
img_path = self.img_paths[idx]
img = Image.open(img_path).convert('RGB')
# 用文件名判断标签:B 开头是鸟,D 开头是无人机
fname = os.path.basename(img_path)
if fname.startswith('B'):
label = 0 # 鸟
elif fname.startswith('D'):
label = 1 # 无人机
else:
raise ValueError(f"Unknown file name format: {fname}")
if self.transform:
img = self.transform(img)
return img, label
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor()
])
train_dataset = BirdVsDroneSimpleDataset("train/images", transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
每个标签文件包含了物体的类和归一化的边界框坐标。一开始,我们的目标是简单分类,不需要直接用到边界框坐标。但如果以后需要物体检测,我们可以利用这些坐标来训练模型。
接下来我们使用一个预训练模型(比如 ResNet50)来进行图像分类。我们将替换模型的最后一层以适应我们的二分类任务。
import torch.nn as nn
import torchvision.models as models
# 使用 ResNet50 预训练模型
model = models.resnet50(pretrained=True)
# 修改最后一层全连接层,以适应2分类问题
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 2) # 2类(鸟 vs 无人机)
# 发送到 GPU(如果可用)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
我们使用交叉熵损失(CrossEntropyLoss
)来训练分类模型,优化器选用 AdamW
。
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=1e-4)
定义训练循环,并记录损失和准确率。
def train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs=10):
train_losses, val_losses = [], []
train_accuracies, val_accuracies = [], []
for epoch in range(num_epochs):
# ---------- 训练 ----------
model.train()
running_loss = 0.0
correct, total = 0, 0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item() * inputs.size(0)
preds = torch.argmax(outputs, dim=1)
correct += (preds == labels).sum().item()
total += labels.size(0)
epoch_loss = running_loss / total
epoch_acc = correct / total
train_losses.append(epoch_loss)
train_accuracies.append(epoch_acc)
# ---------- 验证 ----------
model.eval()
val_loss = 0.0
val_correct, val_total = 0, 0
with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
loss = criterion(outputs, labels)
val_loss += loss.item() * inputs.size(0)
preds = torch.argmax(outputs, dim=1)
val_correct += (preds == labels).sum().item()
val_total += labels.size(0)
val_epoch_loss = val_loss / val_total
val_epoch_acc = val_correct / val_total
val_losses.append(val_epoch_loss)
val_accuracies.append(val_epoch_acc)
# ---------- 日志打印 ----------
print(f"[{epoch+1}/{num_epochs}] Train Loss: {epoch_loss:.4f}, Acc: {epoch_acc:.4f} | Val Loss: {val_epoch_loss:.4f}, Acc: {val_epoch_acc:.4f}")
return train_losses, train_accuracies, val_losses, val_accuracies
验证集部分的代码与训练相似,区别在于需要设置为 model.eval()
来关闭 dropout 等。在上面训练的时候已经验证了,这里就不写了
import matplotlib.pyplot as plt
def plot_metrics(train_losses, train_accuracies, val_losses, val_accuracies):
epochs = range(1, len(train_losses) + 1)
plt.figure(figsize=(12, 5))
# Loss
plt.subplot(1, 2, 1)
plt.plot(epochs, train_losses, 'b', label='Train Loss')
plt.plot(epochs, val_losses, 'r', label='Val Loss')
plt.title('Loss over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
# Accuracy
plt.subplot(1, 2, 2)
plt.plot(epochs, train_accuracies, 'b', label='Train Acc')
plt.plot(epochs, val_accuracies, 'r', label='Val Acc')
plt.title('Accuracy over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.tight_layout()
plt.show()
下面是我训练10轮得到的结果:
Epoch | Train Loss | Train Acc | Val Loss | Val Acc |
---|---|---|---|---|
1 | 0.0019 | 0.9994 | 0.4266 | 0.7920 |
2 | 0.0011 | 0.9997 | 0.3262 | 0.8172 |
3 | 0.0021 | 0.9991 | 0.7032 | 0.7241 |
4 | 0.0003 | 0.9999 | 0.6701 | 0.7977 |
5 | 0.0003 | 0.9999 | 0.6050 | 0.7517 |
6 | 0.0001 | 1.0000 | 0.4924 | 0.7822 |
7 | 0.0029 | 0.9993 | 0.9964 | 0.6937 |
8 | 0.0034 | 0.9989 | 1.8049 | 0.5994 |
9 | 0.0001 | 1.0000 | 2.0533 | 0.5989 |
10 | 0.0001 | 1.0000 | 1.4974 | 0.6707 |
这就是典型的:
模型在训练集学得太好了,但对没见过的验证图泛化能力变差。
优化点 | 解释 | 建议操作 |
---|---|---|
✅ 数据增强 | 增强模型泛化能力,防止过拟合 | 加入随机旋转、裁剪、颜色扰动 |
✅ 模型正则化 | 强迫模型不要过于“记住”训练集 | 使用 |
✅ 更强 backbone | 如果模型本身太弱,学习不稳定 | 尝试 EfficientNet 等等 |
✅ 更平滑训练 | 减小学习率、加入 early stopping | 调低 |
✅ 类别不平衡 | 检查是否鸟和无人机数量严重不均 | 若不平衡,考虑使用 |
从测试集中找张图推理看看效果
from PIL import Image
import torch.nn.functional as F
def predict_image(model, image_path, transform, class_names=['bird', 'drone']):
model.eval()
image = Image.open(image_path).convert('RGB')
img_tensor = transform(image).unsqueeze(0).to(device)
with torch.no_grad():
output = model(img_tensor)
probs = F.softmax(output, dim=1)
pred_class = torch.argmax(probs).item()
print(f"Prediction: {class_names[pred_class]}, Confidence: {probs[0][pred_class]:.4f}")
return image
调用上面的函数
image_path = "test/images/BT/BIRD_123.jpg"
predict_image(model, image_path, transform)
可以看到测试集的图片能够被正确识别出,
但下面我在网上找的图片却被判定为 bird
接下来从简单的增广方法开始,比如随机旋转、随机裁剪、颜色扰动等。这样能增强训练数据的多样性,减少过拟合的风险。
可以使用 torchvision.transforms
来实现数据增强:
from torchvision import transforms
# 数据增强(训练集)
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224), # 随机裁剪并缩放到224x224
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.RandomRotation(20), # 随机旋转 -20 到 20 度
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.2), # 随机调整亮度、对比度等
transforms.ToTensor(), # 转换为Tensor
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), # 正常化
])
# 验证集一般不需要增强,只做归一化
valid_transform = transforms.Compose([
transforms.Resize(256), # 调整为统一大小
transforms.CenterCrop(224), # 中心裁剪为224x224
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
修改 train_loader
和 val_loader
来应用这些转换:
# 训练集应用增强
train_dataset = BirdVsDroneSimpleDataset("train/images", transform=train_transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
# 验证集只做归一化
val_dataset = BirdVsDroneSimpleDataset("valid/images", transform=valid_transform)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)
继数据增强之后,正则化(Regularization)是非常重要的一步,它能进一步减少过拟合、提升模型的泛化能力。我们来一步步加上 几种主流的正则化方法:
虽然 ResNet 本身没有太多 Dropout,但可以在全连接层前加上一个:
import torch.nn as nn
import torchvision.models as models
class ResNetWithDropout(nn.Module):
def __init__(self, num_classes=2):
super().__init__()
self.base_model = models.resnet50(pretrained=True)
in_features = self.base_model.fc.in_features
self.base_model.fc = nn.Sequential(
nn.Dropout(0.5), # 添加Dropout
nn.Linear(in_features, num_classes)
)
def forward(self, x):
return self.base_model(x)
这个是在 optimizer 中设置的,默认不会加上。示例如下:
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-4)
weight_decay=1e-4
表示给每个参数增加一个 L2 惩罚。1e-4 ~ 1e-6
之间调试。这个不改模型,但改训练逻辑。
一句话概括:验证集 loss 连续 N 次没有下降,就提前终止训练,避免过拟合。
可以在训练代码中这样做:
best_val_loss = float('inf')
patience = 3
counter = 0
for epoch in range(num_epochs):
train(...) # 训练逻辑
val_loss = validate(...)
if val_loss < best_val_loss:
best_val_loss = val_loss
counter = 0
# 保存模型
torch.save(model.state_dict(), "best_model.pth")
else:
counter += 1
if counter >= patience:
print("早停触发,终止训练")
break
在 loss function
中设置:
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
它的作用是:不让模型对标签太自信,降低过拟合。
EfficientNet 在多个计算机视觉任务中都表现得非常强大,尤其是在高效的计算资源利用上,通过优化网络结构,能够在保持高性能的同时减少参数量和计算量。
首先,我们需要安装 efficientnet-pytorch
库,来使用 EfficientNet:
pip install efficientnet-pytorch
将 ResNet50 替换为 EfficientNet。EfficientNet 可以直接用来替换模型的 backbone,同时保留全连接层。以下是如何进行修改:
import torch
import torch.nn as nn
from efficientnet_pytorch import EfficientNet
class EfficientNetWithDropout(nn.Module):
def __init__(self, num_classes=2):
super().__init__()
# 加载预训练的 EfficientNetB0(可以选择不同的版本:B0, B1, B2, B3, B4)
self.base_model = EfficientNet.from_pretrained('efficientnet-b0')
# 替换全连接层
in_features = self.base_model._fc.in_features
self.base_model._fc = nn.Sequential(
nn.Dropout(0.5), # Dropout
nn.Linear(in_features, num_classes) # 输出层,假设是二分类
)
def forward(self, x):
return self.base_model(x)
在训练过程中,我们用这个新的 EfficientNetWithDropout
模型来替换原来的 ResNet50WithDropout
,然后继续训练:
model = EfficientNetWithDropout(num_classes=2).to(device)
模型融合(Ensemble Learning) 是高手级提升模型性能的重要策略之一,接下来一步步完成 EfficientNet + ConvNeXt 的融合。
常用模型融合方法:
方法 | 特点 |
---|---|
Soft Voting | 平均多个模型的预测概率(常用于分类) |
Hard Voting | 多数投票(取每个模型预测类别,选最多) |
Weighted Voting | 给表现更好的模型更高权重 |
Stacking | 用一个“元模型”学习多个子模型输出 |
建议小白先从 Soft Voting 或 Weighted Voting 开始,比较简单实用。
from efficientnet_pytorch import EfficientNet
from torchvision.models import convnext_base, ConvNeXt_Base_Weights
class EnsembleModel(nn.Module):
def __init__(self, num_classes=2):
super().__init__()
# EfficientNet
self.efficientnet = EfficientNet.from_pretrained('efficientnet-b0')
in_feat_e = self.efficientnet._fc.in_features
self.efficientnet._fc = nn.Linear(in_feat_e, num_classes)
# ConvNeXt
self.convnext = convnext_base(weights=ConvNeXt_Base_Weights.IMAGENET1K_V1)
in_feat_c = self.convnext.classifier[2].in_features
self.convnext.classifier[2] = nn.Linear(in_feat_c, num_classes)
def forward(self, x):
out1 = self.efficientnet(x)
out2 = self.convnext(x)
# Soft Voting (平均两个输出的 softmax 概率)
prob1 = torch.softmax(out1, dim=1)
prob2 = torch.softmax(out2, dim=1)
final_out = (prob1 + prob2) / 2
return final_out
可以先冻结 EfficientNet 和 ConvNeXt 的大部分参数,只训练最后一层,全连层的效果可能就很好;也可以后期解冻微调:
# 冻结特征提取层,只训练最后的线性层
for param in model.efficientnet.parameters():
param.requires_grad = False
for param in model.convnext.parameters():
param.requires_grad = False
# 只解冻最后一层
for param in model.efficientnet._fc.parameters():
param.requires_grad = True
for param in model.convnext.classifier[2].parameters():
param.requires_grad = True
用原来的交叉熵损失函数、准确率评估方式评估一下:
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4)
final_out = 0.7 * prob1 + 0.3 * prob2 # 给 EfficientNet 更高权重
可以做超参数搜索,找最优组合。
调参(Hyperparameter Tuning)是模型优化的关键步骤,像高手一样调参,可以让你在 Kaggle 或项目中脱颖而出。
调参 = 在 训练速度、模型泛化能力、最终准确率 之间找到一个最优平衡点。
目前已经有了不错的模型(EfficientNet + ConvNeXt),现在我们要做的是榨干它的潜力!
参数 | 常见范围 | 作用说明 |
---|---|---|
| 1e-4 ~ 1e-2 | 学习速度,太大震荡,太小太慢 |
| 16, 32, 64 | 影响稳定性和训练效率 |
| SGD / Adam / AdamW | 不同优化器适应不同模型结构 |
| 1e-5 ~ 1e-3 | L2 正则化,抑制过拟合 |
| 0.2 ~ 0.5 | 防止过拟合,尤其是全连接层 |
| CosineAnnealingLR / StepLR / ReduceLROnPlateau | 控制学习率下降节奏 |
| 权重融合比例(0.5/0.5、0.7/0.3) | 用于模型融合 |
简单粗暴,适合现在阶段。每次改一个参数,观察变化,比如:
learning_rates = [1e-3, 5e-4, 1e-4]
for lr in learning_rates:
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
train_model(model, ...)
用表格记录结果:
lr | train_acc | val_acc | notes |
---|---|---|---|
1e-3 | 0.99 | 0.78 | 初始尝试 |
5e-4 | 0.99 | 0.81 | 好一点 |
1e-4 | 0.98 | 0.83 | 更稳泛化更好 |
穷举全部组合,比如:
from itertools import product
lrs = [1e-3, 1e-4]
bss = [32, 64]
wds = [0.0, 1e-4]
for lr, bs, wd in product(lrs, bss, wds):
print(f"正在尝试 lr={lr}, bs={bs}, weight_decay={wd}")
# 设置 optimizer、DataLoader 等重新训练
🔍 每轮尝试后保存结果,选出最优参数组合。
等熟练之后,建议用:
用 Optuna 示例(调 lr):
import optuna
def objective(trial):
lr = trial.suggest_loguniform('lr', 1e-5, 1e-2)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
acc = train_and_eval(model, optimizer)
return acc # 越高越好
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=20)
print("Best params:", study.best_params)
希望这篇文章对你有所帮助!下次见!🚀
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。