PyTorch 数据加载:Dataset、DataLoader 与 WeightedRandomSampler 解决类别不平衡

PyTorch 数据加载:Dataset、DataLoader 与 WeightedRandomSampler 解决类别不平衡

flyfish

当数据集中多数类样本数远多于少数类时,模型会天然偏向预测多数类。

PyTorch 提供了一套分层、灵活的数据加载机制:通过 Dataset 封装数据访问逻辑、DataLoader 组织批次迭代,再配合 WeightedRandomSampler 实现加权随机采样,可以在数据层面调控每个批次的类别比例。

以下是通过加权采样精准控制类别采样比例

一、Dataset:定义数据集的访问逻辑

torch.utils.data.Dataset 的作用是封装数据与标签,并向外部提供按索引取样本的能力。所有自定义数据集都需要继承这个抽象类。

方法

自定义 Dataset 必须实现两个魔法方法:

__len__(self):返回数据集的总样本数量,供 DataLoader 判断迭代长度;

__getitem__(self, idx):接收索引 idx,返回对应位置的样本数据和标签。

代码实现:模拟不平衡数据集

先构造一个二分类的不平衡数据集:包含 2400 个"猫"样本(标签 0)和 400 个"狗"样本(标签 1),原始类别比例为 6:1。

python 复制代码
import torch
from torch.utils.data import Dataset

class ImbalancedDataset(Dataset):
    def __init__(self, num_cat=2400, num_dog=400):
        self.num_cat = num_cat
        self.num_dog = num_dog
        self.total = num_cat + num_dog
        
        # 构造标签列表
        self.labels = [0] * num_cat + [1] * num_dog
        # 模拟图像数据:形状为 [通道, 高度, 宽度],实际项目中替换为真实图像读取逻辑
        self.data = torch.randn(self.total, 3, 32, 32)  
    
    def __len__(self):
        return self.total
    
    def __getitem__(self, idx):
        return self.data[idx], self.labels[idx]

实际项目中通常不会一次性把全部数据加载进内存,而是在 __getitem__ 中根据文件路径实时读取图像、执行预处理,实现"懒加载"。

二、DataLoader:把数据集包装为可迭代批次

仅有 Dataset 只能逐个取样本,训练模型需要按批次(batch)投喂数据。torch.utils.data.DataLoader 就是负责把 Dataset 包装成可迭代对象的工具,同时支持批处理、打乱、多进程加速、自定义采样等能力。

能力

自动将单样本拼接为 batch,适配模型批量计算;

控制数据读取顺序(随机打乱 / 顺序读取 / 自定义采样);

通过多进程加载数据,缓解训练时的数据读取瓶颈。

关键参数

参数 作用 备注
dataset 传入自定义 Dataset 对象 必传参数
batch_size 每个批次包含的样本数 根据显存大小调整
shuffle 每个 epoch 是否随机打乱顺序 sampler 互斥,指定 sampler 时勿设置
sampler 自定义采样器 解决类别不平衡的入口
num_workers 数据加载的子进程数 Windows 环境建议设为 0
drop_last 是否丢弃最后一个不足 batch_size 的批次 避免批次维度不一致

在类别不平衡场景中,sampler 参数是入口------将通过传入加权采样器,控制每个批次的类别分布。

三、WeightedRandomSampler:加权采样平衡类别分布

torch.utils.data.WeightedRandomSampler 是 PyTorch 内置的加权随机采样器。它的逻辑是:给每个样本分配一个采样权重,权重越高的样本被抽中的概率越大。通过给少数类分配更高权重,可以人为提升少数类在批次中的出现频率。

原理

  1. 为数据集中每一个样本指定一个权重值,组成与数据集等长的权重列表;
  2. 采样器根据权重的概率分布,有放回地抽取样本;
  3. 样本被抽取的概率,与它的权重值成正比。

权重计算:如何精准控制目标比例

很多初学者最困惑的点是"权重到底怎么算"。目标是:让采样后两类的出现比例,等于设定的目标比例(例如 3:1)。

推导逻辑非常直观:

设猫的样本总数为 N,目标采样比例为 target_ratio_cat

设狗的样本总数为 O,目标采样比例为 target_ratio_dog

单个猫样本的权重 = target_ratio_cat / N

单个狗样本的权重 = target_ratio_dog / O

此时:

猫类的总权重 = N × (target_ratio_cat / N) = target_ratio_cat

狗类的总权重 = O × (target_ratio_dog / O) = target_ratio_dog

两类的总权重之比恰好等于目标比例,而采样概率与总权重成正比,因此最终采样出的类别分布就会贴合预期。

代码实现:构建加权采样器

python 复制代码
from torch.utils.data import WeightedRandomSampler
from collections import Counter

def create_balanced_sampler(dataset, target_ratio_cat=3, target_ratio_dog=1):
    labels = dataset.labels
    class_counts = Counter(labels)
    N = class_counts[0]   # 猫的样本数
    O = class_counts[1]   # 狗的样本数
    
    print(f"原始分布: cat={N}, dog={O}, 比例 {N/O:.2f}:1")
    
    # 为每个样本计算权重
    weights = [target_ratio_cat / N if label == 0 else target_ratio_dog / O 
               for label in labels]
    
    sampler = WeightedRandomSampler(
        weights=weights,        # 每个样本的权重列表
        num_samples=len(weights),  # 每个 epoch 采样的总样本数
        replacement=True        # 有放回采样,必须开启
    )
    
    print(f"已创建 WeightedRandomSampler,目标比例 {target_ratio_cat}:{target_ratio_dog}")
    return sampler

参数说明

weights:与数据集等长的权重列表,每个元素对应该位置样本的采样权重;

num_samples:每个 epoch 一共抽取多少个样本。通常设为数据集总大小,也可根据需求自定义;

replacement:是否有放回采样。解决类别不平衡时必须设为 True------无放回采样下,少数类最多被抽一次,无法提升出现比例。

四、组装

接下来把三个组件串联起来,完整实现不平衡数据集的加权采样加载,并实际验证采样比例是否符合预期。

1. 组装 DataLoader

python 复制代码
from torch.utils.data import DataLoader
import numpy as np

if __name__ == "__main__":
    # 创建不平衡数据集:原始 6:1
    dataset = ImbalancedDataset(num_cat=2400, num_dog=400)
    
    # 创建加权采样器,目标采样比例 3:1
    sampler = create_balanced_sampler(dataset, target_ratio_cat=3, target_ratio_dog=1)
    
    # 构建 DataLoader,传入自定义 sampler
    batch_size = 128
    loader = DataLoader(
        dataset,
        batch_size=batch_size,
        sampler=sampler,  # 使用自定义采样器,无需设置 shuffle
        num_workers=4,
        drop_last=True
    )

2. 验证实际采样效果

遍历前 10 个 batch,统计每个批次内的类别数量与比例,验证加权采样是否生效。

python 复制代码
    print("\n" + "="*60)
    print("实际采样比例验证(前 10 个 batch):")
    print("="*60)
    
    ratios = []
    for i, (images, labels) in enumerate(loader):
        if i >= 10:
            break
        n_cat = (labels == 0).sum().item()
        n_dog = (labels == 1).sum().item()
        ratio = n_cat / n_dog if n_dog > 0 else float('inf')
        ratios.append(ratio)
        print(f"Batch {i:2d}: cat={n_cat:3d} | dog={n_dog:2d} | 比例 {ratio:.2f}:1")
    
    avg_ratio = np.mean(ratios)
    print(f"\n平均比例: {avg_ratio:.2f}:1 (接近目标 3:1)")

运行代码后,会看到每个 batch 的猫狗比例都在 3:1 附近波动,平均比例与目标高度吻合,说明加权采样已经生效。

原始分布: cat=2400, dog=400, 比例 6.00:1

已创建 WeightedRandomSampler,目标比例 3:1

完整代码

python 复制代码
import torch
from torch.utils.data import Dataset, DataLoader, WeightedRandomSampler
from collections import Counter
import numpy as np

# ====================== 1. 模拟不平衡数据集 ======================
class ImbalancedDataset(Dataset):
    def __init__(self, num_cat=2400, num_dog=400):
        self.num_cat = num_cat
        self.num_dog = num_dog
        self.total = num_cat + num_dog
        
        # 标签:0 = cat, 1 = dog
        self.labels = [0] * num_cat + [1] * num_dog
        # 模拟图像(实际使用时替换为真实图像)
        self.data = torch.randn(self.total, 3, 32, 32)  
    
    def __len__(self):
        return self.total
    
    def __getitem__(self, idx):
        return self.data[idx], self.labels[idx]

# ====================== 2. 创建 WeightedRandomSampler (3:1) ======================
def create_balanced_sampler(dataset, target_ratio_cat=3, target_ratio_dog=1):
    labels = dataset.labels
    class_counts = Counter(labels)
    N = class_counts[0]   # cat 数量
    O = class_counts[1]   # dog 数量
    
    print(f"原始分布: cat={N}, dog={O}, 比例 {N/O:.2f}:1")
    
    # 关键:设置权重
    weights = [target_ratio_cat / N if label == 0 else target_ratio_dog / O 
               for label in labels]
    print("weights:",weights)
    
    sampler = WeightedRandomSampler(
        weights=weights,
        num_samples=len(weights),   # 通常等于数据集大小
        replacement=True            # 必须开启!允许重复采样少数类
    )
    
    print(f"已创建 WeightedRandomSampler,目标比例 {target_ratio_cat}:{target_ratio_dog}")
    return sampler

# ====================== 3. 主函数 ======================
if __name__ == "__main__":
    # 创建数据集(模拟 6:1 不平衡)
    dataset = ImbalancedDataset(num_cat=2400, num_dog=400)
    
    # 创建 Sampler(目标 3:1)
    sampler = create_balanced_sampler(dataset, target_ratio_cat=3, target_ratio_dog=1)
    
    # 创建 DataLoader
    batch_size = 128
    loader = DataLoader(
        dataset,
        batch_size=batch_size,
        sampler=sampler,
        num_workers=4,
        drop_last=True
    )
    
    # ====================== 验证实际 batch 比例 ======================
    print("\n" + "="*60)
    print("实际采样比例验证(前 10 个 batch):")
    print("="*60)
    
    ratios = []
    for i, (images, labels) in enumerate(loader):
        if i >= 10:
            break
        n_cat = (labels == 0).sum().item()
        n_dog = (labels == 1).sum().item()
        ratio = n_cat / n_dog if n_dog > 0 else float('inf')
        ratios.append(ratio)
        print(f"Batch {i:2d}: cat={n_cat:3d} | dog={n_dog:2d} | 比例 {ratio:.2f}:1")
    
    avg_ratio = np.mean(ratios)
    print(f"\n平均比例: {avg_ratio:.2f}:1 (接近目标 3:1)")

输出

python 复制代码
============================================================
实际采样比例验证(前 10 个 batch):
============================================================
Batch  0: cat= 97 | dog=31 | 比例 3.13:1
Batch  1: cat= 88 | dog=40 | 比例 2.20:1
Batch  2: cat= 97 | dog=31 | 比例 3.13:1
Batch  3: cat=100 | dog=28 | 比例 3.57:1
Batch  4: cat= 99 | dog=29 | 比例 3.41:1
Batch  5: cat= 95 | dog=33 | 比例 2.88:1
Batch  6: cat= 91 | dog=37 | 比例 2.46:1
Batch  7: cat= 92 | dog=36 | 比例 2.56:1
Batch  8: cat= 97 | dog=31 | 比例 3.13:1
Batch  9: cat= 97 | dog=31 | 比例 3.13:1

看重点代码

python 复制代码
weights = [target_ratio_cat / N if label == 0 else target_ratio_dog / O 
           for label in labels]

这是一个 Python 列表推导式,作用是遍历数据集中每一个样本的标签,给每个样本计算一个专属的采样权重,最终生成一个和数据集等长的权重列表。

拆解成普通循环写法,逻辑会更清晰:

python 复制代码
weights = []
for label in labels:
    if label == 0:   # 猫样本
        w = target_ratio_cat / N
    else:            # 狗样本
        w = target_ratio_dog / O
    weights.append(w)

输入:labels 是所有样本的标签列表(长度=数据集总样本数)

输出:weights 是每个样本对应的权重值(长度和 labels 完全一一对应)

规则:同类别的所有样本,权重值完全相同;不同类别权重值不同

每个变量与公式的含义

先把所有变量的定义对齐:

变量 含义 例子中的数值
N 类别 0(猫)的原始样本总数 2400
O 类别 1(狗)的原始样本总数 400
target_ratio_cat 我们期望的「猫」在采样中的比例份额 3
target_ratio_dog 我们期望的「狗」在采样中的比例份额 1

两个公式

单个猫样本的权重:权重 = 目标比例份额 / 该类样本总数 = target_ratio_cat / N

单个狗样本的权重:权重 = 目标比例份额 / 该类样本总数 = target_ratio_dog / O

为什么要除以该类样本总数?

要控制的是整个类别的总权重,而不是单个样本的权重。

来算一下整个类别的总权」:

猫类总权重 = 猫的数量 × 单个猫权重 = N × (target_ratio_cat / N) = target_ratio_cat

狗类总权重 = 狗的数量 × 单个狗权重 = O × (target_ratio_dog / O) = target_ratio_dog

会发现:除以样本总数之后,每个类别的总权重恰好等于设定的目标比例值

代入例子数值验证:

猫类总权重 = 2400 × (3 / 2400) = 3

狗类总权重 = 400 × (1 / 400) = 1

两类总权重之比 = 3 : 1,和设定的目标比例完全一致

WeightedRandomSampler 怎么用这些权重?

WeightedRandomSampler 的采样规则非常简单:

每个样本被抽中的概率,和它的权重值成正比。

一个类别的总采样概率,就等于这个类别的总权重占全部总权重的比例。

继续用例子算一遍实际采样概率:

  1. 全部样本的权重总和 = 猫总权重 + 狗总权重 = 3 + 1 = 4
  2. 猫类的总采样概率 = 猫总权重 / 总权重和 = 3 / 4 = 75%
  3. 狗类的总采样概率 = 狗总权重 / 总权重和 = 1 / 4 = 25%

也就是说,每抽一个样本,有 75% 概率抽到猫,25% 概率抽到狗,最终采样出来的数量比例就会稳定在 3:1,完美符合目标。

单个样本的采样概率

也可以看单个样本的概率,直观感受「少数类单样本权重更高」:

单个猫样本权重:3 / 2400 = 0.00125

单个狗样本权重:1 / 400 = 0.0025

可以看到:单个狗样本的权重是猫的 2 倍。虽然狗的总数少,但每个狗样本被抽中的概率更高,最终整体比例就被拉平到了想要的 3:1。

三类的例子

python 复制代码
import torch
from torch.utils.data import Dataset, DataLoader, WeightedRandomSampler
from collections import Counter
import numpy as np

# ====================== 1. 模拟三分类不平衡数据集 ======================
class ImbalancedDataset(Dataset):
    def __init__(self, num_cat=2400, num_dog=400, num_fish=800):
        # 标签约定:0=cat, 1=dog, 2=fish
        self.num_cat = num_cat
        self.num_dog = num_dog
        self.num_fish = num_fish
        self.total = num_cat + num_dog + num_fish
        
        # 构造标签列表
        self.labels = [0]*num_cat + [1]*num_dog + [2]*num_fish
        # 模拟图像数据
        self.data = torch.randn(self.total, 3, 32, 32)  
    
    def __len__(self):
        return self.total
    
    def __getitem__(self, idx):
        return self.data[idx], self.labels[idx]

# ====================== 2. 通用版加权采样器(支持任意多分类) ======================
def create_balanced_sampler(dataset, target_ratios):
    """
    Args:
        dataset: 自定义Dataset对象,必须有labels属性
        target_ratios: dict,格式 {类别标签: 目标比例值}
                       例如 {0:3, 1:1, 2:2} 表示 cat:dog:fish = 3:1:2
    """
    labels = dataset.labels
    class_counts = Counter(labels)  # 统计每个类别的原始样本数
    
    print("原始类别分布:")
    for label, count in class_counts.items():
        print(f"  类别 {label}: {count} 个样本")
    
    # 为每个样本计算权重
    # 同类样本权重相同 = 该类目标比例 / 该类原始样本总数
    weights = [target_ratios[label] / class_counts[label] for label in labels]
    
    sampler = WeightedRandomSampler(
        weights=weights,
        num_samples=len(weights),
        replacement=True  # 必须开启有放回采样
    )
    
    print(f"\n目标采样比例: {target_ratios}")
    return sampler

# ====================== 3. 主函数验证 ======================
if __name__ == "__main__":
    # 创建三分类数据集(原始比例 cat:dog:fish = 2400:400:800 = 6:1:2)
    dataset = ImbalancedDataset(num_cat=2400, num_dog=400, num_fish=800)
    
    # 设定目标采样比例:cat:dog:fish = 3:1:2
    target_ratios = {0: 3, 1: 1, 2: 2}
    sampler = create_balanced_sampler(dataset, target_ratios)
    
    # 创建 DataLoader
    batch_size = 128
    loader = DataLoader(
        dataset,
        batch_size=batch_size,
        sampler=sampler,
        num_workers=0,  
        drop_last=True
    )
    
    # ====================== 验证实际 batch 比例 ======================
    print("\n" + "="*70)
    print("实际采样比例验证(前 10 个 batch):")
    print("="*70)
    
    all_counts = []
    for i, (images, labels) in enumerate(loader):
        if i >= 10:
            break
        n_cat = (labels == 0).sum().item()
        n_dog = (labels == 1).sum().item()
        n_fish = (labels == 2).sum().item()
        
        # 以 dog 为基准,计算相对比例
        ratio_cat = n_cat / n_dog if n_dog > 0 else float('inf')
        ratio_fish = n_fish / n_dog if n_dog > 0 else float('inf')
        
        all_counts.append((n_cat, n_dog, n_fish))
        print(f"Batch {i:2d}: cat={n_cat:3d} | dog={n_dog:2d} | fish={n_fish:3d} | 比例 {ratio_cat:.2f}:1:{ratio_fish:.2f}")
    
    # 计算平均比例
    avg_cat = np.mean([c[0] for c in all_counts])
    avg_dog = np.mean([c[1] for c in all_counts])
    avg_fish = np.mean([c[2] for c in all_counts])
    print(f"\n平均数量: cat={avg_cat:.1f}, dog={avg_dog:.1f}, fish={avg_fish:.1f}")
    print(f"平均比例: {avg_cat/avg_dog:.2f} : 1 : {avg_fish/avg_dog:.2f} (目标 3:1:2)")

输出

python 复制代码
原始类别分布:
  类别 0: 2400 个样本
  类别 1: 400 个样本
  类别 2: 800 个样本

目标采样比例: {0: 3, 1: 1, 2: 2}

======================================================================
实际采样比例验证(前 10 个 batch):
======================================================================
Batch  0: cat= 61 | dog=25 | fish= 42 | 比例 2.44:1:1.68
Batch  1: cat= 65 | dog=14 | fish= 49 | 比例 4.64:1:3.50
Batch  2: cat= 60 | dog=19 | fish= 49 | 比例 3.16:1:2.58
Batch  3: cat= 62 | dog=22 | fish= 44 | 比例 2.82:1:2.00
Batch  4: cat= 67 | dog=23 | fish= 38 | 比例 2.91:1:1.65
Batch  5: cat= 66 | dog=19 | fish= 43 | 比例 3.47:1:2.26
Batch  6: cat= 63 | dog=22 | fish= 43 | 比例 2.86:1:1.95
Batch  7: cat= 59 | dog=21 | fish= 48 | 比例 2.81:1:2.29
Batch  8: cat= 56 | dog=22 | fish= 50 | 比例 2.55:1:2.27
Batch  9: cat= 60 | dog=21 | fish= 47 | 比例 2.86:1:2.24

平均数量: cat=61.9, dog=20.8, fish=45.3
平均比例: 2.98 : 1 : 2.18 (目标 3:1:2)
相关推荐
智慧物业老杨2 小时前
物业绿化数智化权责管控方案:基于工单平台的双层权限追溯模块落地实践
python
Metaphor6922 小时前
使用 Python 在 Word 文档中添加批注
python·word
FreakStudio2 小时前
WIZnet 开源方案分享:W55MH32 开发板 + 小智 AI实现以太网版本的小智机器人
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
会周易的程序员2 小时前
使用 pybind11 封装 C++ 日志库 microLog 为 Python 模块
java·c++·python·物联网·架构·日志·aiot
一只专注api接口开发的技术猿3 小时前
电商评论自动化监控与情感数据分析完整落地教程(附可直接运行 Python 代码)
大数据·数据库·python·数据分析·自动化
HAPPY酷3 小时前
【ROS2】 Conda 环境搭建(命令行版)
linux·运维·python·ubuntu·机器人·conda
柒和远方4 小时前
LeetCode 4. 寻找两个正序数组的中位数 —— 二分划分的艺术
javascript·python·算法
学也学不废5 小时前
Flask问答系统与LAUR模型
人工智能·python·机器学习·分类·数据挖掘
lewis_lk5 小时前
uv: Python 新一代极速包管理工具
python
promising_xxx5 小时前
深度学习个人开源知识库 深度筑基 | DeepBase
人工智能·python·深度学习·计算机视觉·ai·语言模型·nlp