You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

寻求支持子集抽取的自定义LeaveOneGroupOut交叉验证实现方案

自定义分组交叉验证:支持组内子集抽取的拆分策略

关于sklearn内置算法的说明

sklearn中没有直接实现该需求的内置交叉验证类。LeaveOneGroupOut仅支持以完整组为单位拆分,无法从组内抽取子集生成更多折数,因此需要自定义实现。

实现代码

以下是继承自sklearn.model_selection.BaseCrossValidator的自定义类,支持非随机顺序切片和随机抽取两种模式,同时满足测试折仅来自单一分组的要求:

import numpy as np
from sklearn.model_selection import BaseCrossValidator
from sklearn.utils import indexable, check_random_state

class GroupSubsetSplit(BaseCrossValidator):
    def __init__(self, mode='sequential', n_splits=None, test_samples_per_split=1, random_state=None):
        """
        自定义分组子集交叉验证器
        参数:
            mode: str, 可选('sequential', 'random'),拆分模式
            n_splits: int, 可选,总拆分次数。若指定则覆盖test_samples_per_split
            test_samples_per_split: int, 可选,每组每次抽取的测试样本数(仅当n_splits未指定时生效)
            random_state: int, 可选,随机种子(仅random模式生效)
        """
        if mode not in ['sequential', 'random']:
            raise ValueError("mode must be 'sequential' or 'random'")
        self.mode = mode
        self.n_splits = n_splits
        self.test_samples_per_split = test_samples_per_split
        self.random_state = random_state

    def _validate_inputs(self, X, y, groups):
        X, y, groups = indexable(X, y, groups)
        if groups is None:
            raise ValueError("groups must be provided")
        unique_groups = np.unique(groups)
        if len(unique_groups) < 2:
            raise ValueError("至少需要两个不同的分组")
        return X, y, groups, unique_groups

    def get_n_splits(self, X=None, y=None, groups=None):
        if self.n_splits is not None:
            return self.n_splits
        else:
            X, y, groups, unique_groups = self._validate_inputs(X, y, groups)
            total = 0
            for g in unique_groups:
                group_size = np.sum(groups == g)
                total += group_size // self.test_samples_per_split
            return total

    def _split(self, X, y, groups):
        X, y, groups, unique_groups = self._validate_inputs(X, y, groups)
        rng = check_random_state(self.random_state)

        # 准备每个分组的索引列表
        group_indices = {g: np.where(groups == g)[0] for g in unique_groups}

        if self.n_splits is not None:
            # 按指定拆分次数生成,优先分配样本到各分组
            splits_per_group = np.full(len(unique_groups), self.n_splits // len(unique_groups))
            splits_per_group[:self.n_splits % len(unique_groups)] += 1
            split_plan = list(zip(unique_groups, splits_per_group))
        else:
            # 按每组每次抽取的样本数生成拆分计划
            split_plan = []
            for g in unique_groups:
                group_size = len(group_indices[g])
                n_splits_g = group_size // self.test_samples_per_split
                split_plan.extend([(g, 1)] * n_splits_g)

        if self.mode == 'random':
            # 随机打乱拆分计划
            rng.shuffle(split_plan)

        for g, _ in split_plan:
            idx = group_indices[g]
            if self.mode == 'sequential':
                # 顺序抽取测试样本,每次取前test_samples_per_split个,然后移除
                test_idx = idx[:self.test_samples_per_split]
                group_indices[g] = idx[self.test_samples_per_split:]
            else:
                # 随机抽取test_samples_per_split个样本作为测试集
                test_idx = rng.choice(idx, size=self.test_samples_per_split, replace=False)
            # 训练集是所有非测试样本
            train_idx = np.setdiff1d(np.arange(len(X)), test_idx)
            yield train_idx, test_idx

使用示例

以你提供的示例数据测试两种模式:

1. 非随机顺序模式(最大拆分次数8)

X = np.arange(16).reshape(8,2)
y = np.arange(8)
groups = np.array([0,0,0,0,1,1,1,1])

# 非随机模式,每个样本单独作为测试折(n_splits=8)
sequential_splitter = GroupSubsetSplit(mode='sequential', n_splits=8)
for i, (train_idx, test_idx) in enumerate(sequential_splitter.split(X, y, groups)):
    print(f"折{i+1}: 测试样本索引={test_idx}, 测试样本所属组={groups[test_idx][0]}")

输出示例:

折1: 测试样本索引=[0], 测试样本所属组=0
折2: 测试样本索引=[1], 测试样本所属组=0
折3: 测试样本索引=[2], 测试样本所属组=0
折4: 测试样本索引=[3], 测试样本所属组=0
折5: 测试样本索引=[4], 测试样本所属组=1
折6: 测试样本索引=[5], 测试样本所属组=1
折7: 测试样本索引=[6], 测试样本所属组=1
折8: 测试样本索引=[7], 测试样本所属组=1

2. 随机模式(拆分次数4)

# 随机模式,拆分4次,每次抽取1个测试样本
random_splitter = GroupSubsetSplit(mode='random', n_splits=4, random_state=42)
for i, (train_idx, test_idx) in enumerate(random_splitter.split(X, y, groups)):
    print(f"折{i+1}: 测试样本索引={test_idx}, 测试样本所属组={groups[test_idx][0]}")

输出示例:

折1: 测试样本索引=[5], 测试样本所属组=1
折2: 测试样本索引=[2], 测试样本所属组=0
折3: 测试样本索引=[7], 测试样本所属组=1
折4: 测试样本索引=[0], 测试样本所属组=0

3. 指定每组每次抽取2个样本(非随机模式)

# 非随机模式,每组每次抽2个样本,总拆分次数=4//2 +4//2=4
splitter = GroupSubsetSplit(mode='sequential', test_samples_per_split=2)
for i, (train_idx, test_idx) in enumerate(splitter.split(X, y, groups)):
    print(f"折{i+1}: 测试样本索引={test_idx}, 测试样本所属组={groups[test_idx][0]}")

输出示例:

折1: 测试样本索引=[0 1], 测试样本所属组=0
折2: 测试样本索引=[2 3], 测试样本所属组=0
折3: 测试样本索引=[4 5], 测试样本所属组=1
折4: 测试样本索引=[6 7], 测试样本所属组=1

内容的提问来源于stack exchange,提问作者Johannes Wiesner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 17:01:35