4. 数据集构建:从田间到模型的第一步

说实话,数据集构建这件事,我见过太多团队栽跟头了。模型选得再好,代码写得再漂亮,数据不行,一切都是白搭。今天咱们就聊聊,怎么把一堆乱七八糟的田间照片,变成模型能吃的“干净粮食”。

核心观点:数据质量决定了模型性能的上限。算法只是逼近这个上限的手段。

4.1 数据采集:别让模型“没见过”真实场景

数据采集听起来简单——拿手机去田里拍呗。但这里面的门道,比你想象的多。

我个人习惯把采集策略分成三个维度:

  • 环境多样性:晴天、阴天、清晨、傍晚、雨后。我做过一个水稻病害项目,模型在实验室跑得飞起,一到田间就崩。后来发现,训练数据全是正午拍的,模型根本没见过露水反射的光斑。
  • 视角与距离:俯拍、侧拍、近景、远景。别只盯着病害部位拍特写,模型需要学会在整片叶子里找到病灶。
  • 设备与分辨率:手机、单反、无人机。不同设备拍出来的纹理细节差异很大。如果你最终要部署在手机上,训练数据里最好也混入一些手机拍的图。

我的小技巧:每次去田间采集,带一张色卡。后期做色彩归一化时,能省不少事。

4.2 数据清洗:把“垃圾”筛出去

采集回来的数据,我敢打赌,至少有10%是不能用的。清洗这一步,千万别偷懒。

我曾经接手过一个外包数据集,里面居然混了十几张自拍照。嗯,你没看错,自拍照。所以清洗流程我建议这样走:

  1. 去重:连续拍摄很容易产生重复帧。用哈希值或感知哈希算法,把一模一样的图去掉。
  2. 模糊检测:拉普拉斯方差法,阈值设个100左右。低于阈值的,直接删。
  3. 曝光异常:过曝或欠曝的图,模型学不到什么有用特征。检查直方图,两端截断的图就扔掉。
  4. 无关内容:拍到了人、机器、天空,跟病虫害无关的,统统剔除。

注意:清洗不是越狠越好。有些模糊的图,恰恰是实际部署时常见的场景。我的建议是:保留一部分“轻度模糊”的样本,让模型学会鲁棒性。

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 整体流程:一张图看懂

说了这么多,咱们用一张图把整个流程串起来。

数据集构建流程 数据采集 数据清洗 数据标注 数据集划分 数据加载器 环境、视角、设备多样性 至少覆盖3种光照条件 去重、模糊检测、曝光检查 保留10%轻度模糊样本 分类/检测/分割 统一边界框边距规则 按植株/时间划分 70/15/15 比例 num_workers=CPU核数/2 pin_memory=True