4. 数据集构建:从田间到模型的第一步
说实话,数据集构建这件事,我见过太多团队栽跟头了。模型选得再好,代码写得再漂亮,数据不行,一切都是白搭。今天咱们就聊聊,怎么把一堆乱七八糟的田间照片,变成模型能吃的“干净粮食”。
核心观点:数据质量决定了模型性能的上限。算法只是逼近这个上限的手段。
4.1 数据采集:别让模型“没见过”真实场景
数据采集听起来简单——拿手机去田里拍呗。但这里面的门道,比你想象的多。
我个人习惯把采集策略分成三个维度:
- 环境多样性:晴天、阴天、清晨、傍晚、雨后。我做过一个水稻病害项目,模型在实验室跑得飞起,一到田间就崩。后来发现,训练数据全是正午拍的,模型根本没见过露水反射的光斑。
- 视角与距离:俯拍、侧拍、近景、远景。别只盯着病害部位拍特写,模型需要学会在整片叶子里找到病灶。
- 设备与分辨率:手机、单反、无人机。不同设备拍出来的纹理细节差异很大。如果你最终要部署在手机上,训练数据里最好也混入一些手机拍的图。
我的小技巧:每次去田间采集,带一张色卡。后期做色彩归一化时,能省不少事。
4.2 数据清洗:把“垃圾”筛出去
采集回来的数据,我敢打赌,至少有10%是不能用的。清洗这一步,千万别偷懒。
我曾经接手过一个外包数据集,里面居然混了十几张自拍照。嗯,你没看错,自拍照。所以清洗流程我建议这样走:
- 去重:连续拍摄很容易产生重复帧。用哈希值或感知哈希算法,把一模一样的图去掉。
- 模糊检测:拉普拉斯方差法,阈值设个100左右。低于阈值的,直接删。
- 曝光异常:过曝或欠曝的图,模型学不到什么有用特征。检查直方图,两端截断的图就扔掉。
- 无关内容:拍到了人、机器、天空,跟病虫害无关的,统统剔除。
注意:清洗不是越狠越好。有些模糊的图,恰恰是实际部署时常见的场景。我的建议是:保留一部分“轻度模糊”的样本,让模型学会鲁棒性。
4.3 数据标注:标注规范决定模型上限
标注这件事,说白了就是给数据打标签。但怎么打,差别很大。
对于病虫害识别,我通常分两类:
| 任务类型 | 标注方式 | 适用场景 |
|---|---|---|
| 分类 | 整张图一个标签 | 判断叶片是否有病 |
| 检测 | 边界框 + 类别 | 定位病斑位置 |
| 分割 | 像素级掩码 | 精确计算病斑面积 |
我个人更推荐用检测或分割。为什么?因为分类任务太“粗糙”了。一张叶子上同时有稻瘟病和褐斑病,你只给一个标签,模型学得会才怪。
标注工具方面,LabelImg 和 Labelme 够用了。如果预算充足,可以上 SuperAnnotate 或 CVAT,支持多人协作。
避坑指南:我曾经让标注员按自己的理解去框病斑,结果有人框得紧贴病灶,有人留了一圈“安全区”。后来我强制规定了边界框的边距规则——框的边缘距离病灶边界不超过5个像素。从此模型收敛速度快了一截。
4.4 数据集划分:别让验证集“作弊”
训练集、验证集、测试集,怎么分?很多人随手一 split 就完事了。但这里有个坑——数据泄露。
举个例子:你从同一棵树上拍了100张图,随机分到训练集和验证集。模型在验证集上表现很好,但一到新树就拉胯。为什么?因为训练集和验证集太“像”了,模型其实是在“记住”这棵树,而不是学会识别病害。
我的划分策略:
- 按植株划分:同一株植物的所有图片,必须分到同一个集合里。
- 按时间划分:如果数据是连续采集的,用前70%的时间段做训练,后30%做验证。
- 比例建议:70% 训练,15% 验证,15% 测试。数据量少于1万张时,可以适当提高验证集比例到20%。
重要:测试集只能用来做最终评估,绝对不能参与任何调参或模型选择。我见过有人用测试集调了三次参数,结果上线后准确率直接掉了8个点。
4.5 数据加载器实现:让 GPU 别闲着
数据准备好了,怎么喂给模型?这里我直接给代码。PyTorch 的 DataLoader 是标配,但配置上有讲究。
import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import os
class PestDataset(Dataset):
def __init__(self, img_dir, label_file, transform=None):
self.img_dir = img_dir
self.transform = transform
# 读取标签文件,格式:图片名, 类别ID
with open(label_file, 'r') as f:
self.samples = [line.strip().split(',') for line in f]
def __len__(self):
return len(self.samples)
def __getitem__(self, idx):
img_name, label = self.samples[idx]
img_path = os.path.join(self.img_dir, img_name)
image = Image.open(img_path).convert('RGB')
if self.transform:
image = self.transform(image)
return image, int(label)
# 数据增强策略
train_transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(15),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
val_transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# 加载器配置
train_dataset = PestDataset('data/train', 'data/train_labels.csv', transform=train_transform)
val_dataset = PestDataset('data/val', 'data/val_labels.csv', transform=val_transform)
train_loader = DataLoader(
train_dataset,
batch_size=32,
shuffle=True,
num_workers=4, # 根据CPU核心数调整
pin_memory=True, # 加速GPU传输
drop_last=True # 丢弃最后一个不完整的batch
)
val_loader = DataLoader(
val_dataset,
batch_size=32,
shuffle=False,
num_workers=4,
pin_memory=True
)
关于 num_workers:我习惯设置为CPU核心数的一半。设太高反而会因为进程切换开销导致变慢。另外,Windows 用户注意,num_workers 设成0可以避免一些多进程bug。
数据增强这块,我多说两句。对于病虫害识别,RandomHorizontalFlip 和 RandomRotation 基本是标配。但 ColorJitter 要谨慎——过度调整颜色可能会让模型学到错误的颜色特征。我一般把 brightness 和 contrast 的幅度控制在0.2以内。
4.6 整体流程:一张图看懂
说了这么多,咱们用一张图把整个流程串起来。