基础随机采样
图1 · 简单随机采样:总体(蓝色小点),随机抽出若干(红色大点)
### 原理 简单随机采样(Simple Random Sampling):从 N 个样本中等概率(每个样本被抽中概率 1/N)、通常无放回地抽取 n 个。任意大小为 n 的子集被抽中的概率都相等,因此样本是无偏的。适用于总体分布比较均匀的场景;样本量小时方差较大,可能漏掉稀有模式。 ### 示例(sklearn + 鸢尾花数据集) ```python import numpy as np from sklearn.datasets import load_iris X, y = load_iris(return_X_y=True) rng = np.random.default_rng(seed=42) idx = rng.choice(len(y), size=30, replace=False) # 无放回抽 30 条 X_sample, y_sample = X[idx], y[idx] print(np.bincount(y_sample)) # 三类各抽到多少条(比例不保证精确 1:1:1) ``` 输出(随机,各类数量可能不均衡): ```text [12 9 9] ```
图2 · 分层采样 vs 简单随机采样:按 30% 抽样后的类别数量对比 setosa 原 50 条 / 随机抽后约 41 条 versicolor 原 50 条 / 随机抽后约 59 条 virginica 原 50 条 / 随机抽后约 50 条 分层 30% 三类都精确抽 15 条,比例严格保持
### 原理 分层采样(Stratified Sampling):先把总体按类别/关键属性切成若干"层",再在每层内部分别随机抽样,最后合并。设总体第 h 层占比 P(h),抽 n 个样本,则第 h 层抽 n·P(h) 个。它保证每个类别在训练集、验证集、测试集中比例一致,是分类任务划分数据集的默认做法,能显著降低抽样方差。 ### 示例(train_test_split 分层划分) ```python from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split X, y = load_iris(return_X_y=True) X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.3, stratify=y, # 关键参数:按 y 分层 random_state=42 ) import numpy as np print(np.bincount(y_tr), np.bincount(y_te)) ``` 输出(每类恰好按 70/30 划分): ```text [35 35 35] [15 15 15] ```
图3 · 系统采样:每 N/k=5 个取一个,起点随机(此处为第 3 个) 1 2 3 4 5 6 7 8 9 10 11 红框 = 入选样本(3, 6, 10, ...);间隔固定,仅起点随机
### 原理 系统采样(Systematic Sampling):把总体排成序列,随机选一个起点 r(1 ≤ r ≤ k),然后每隔 k = N/n 取一个,即抽中 r, r+k, r+2k, ...。实现简单、样本在序列上均匀散布;但要求数据没有与 k 同周期的规律(否则会引入偏差,例如按天记录的数据恰逢每 7 天一个周期,而 k 恰为 7 的倍数)。 ### 示例(对时序日志做等距抽样) ```python import numpy as np N, n = 10000, 200 k = N // n # 间隔 50 start = np.random.randint(0, k) # 随机起点 idx = np.arange(start, N, k) sample_idx = idx[:n] print(sample_idx[:8]) ``` 输出: ```text [ 37 87 137 187 237 287 337 387] ```
### 原理 加权采样(Weighted Sampling):每个样本 i 带权重 w(i),被抽中概率正比于 w(i),即 P(i) = w(i)/Σw。适合"不同样本重要性不同"的场景:样本新鲜度越高权重越大、难分样本权重加大、海量候选按热度/点击率期望抽样等。pandas 的 `sample(weights=...)` 与 numpy 的 `choice(p=...)` 都支持。 ### 示例(按点击热度加权抽候选商品) ```python import pandas as pd df = pd.DataFrame({ "item": ["A", "B", "C", "D", "E"], "click": [500, 100, 50, 20, 10], # 权重 }) sampled = df.sample(n=3, weights="click", random_state=1) print(sampled["item"].tolist()) ``` 输出(热门商品 A 更容易被抽到): ```text ['A', 'B', 'C'] ```
不平衡类别采样
### 原理 随机过采样(Random Over-Sampling, ROS):从少数类中有放回地重复抽样,把少数类样本复制到与多数类相当的数量。简单、不丢信息,但复制样本意味着同一少数类点的"权重"变大,容易过拟合——模型反复看到完全相同的少数类样本,倾向于死记硬背。 ### 示例(imbalanced-learn) ```python from sklearn.datasets import make_classification from imblearn.over_sampling import RandomOverSampler import collections X, y = make_classification(n_samples=2000, weights=[0.9, 0.1], random_state=42) print("原分布:", collections.Counter(y)) ros = RandomOverSampler(random_state=42) X_res, y_res = ros.fit_resample(X, y) print("重采样后:", collections.Counter(y_res)) ``` 输出: ```text 原分布: Counter({0: 1798, 1: 202}) 重采样后: Counter({0: 1798, 1: 1798}) ```
图4 · 欠采样的取舍:优先删掉靠近类别边界的多数类点(空心) 左:多数类(蓝) 右:少数类(红) 虚线:类别边界
### 原理 随机欠采样(Random Under-Sampling, RUS):随机丢弃多数类样本,直到两类数量接近。不复制数据、不会因重复样本过拟合、训练更快;代价是丢掉了多数类的信息,若丢弃的样本里包含重要模式(如多数类的不同子簇),模型对多数类的理解会变差。当多数类样本量本身很大(几十万级)时最适用。 ### 示例(欠采样 + 对比前后规模) ```python from sklearn.datasets import make_classification from imblearn.under_sampling import RandomUnderSampler import collections X, y = make_classification(n_samples=5000, weights=[0.95, 0.05], random_state=42) rus = RandomUnderSampler(random_state=42) # 默认删到与少数类等量 X_res, y_res = rus.fit_resample(X, y) print("前:", collections.Counter(y), "总量", len(y)) print("后:", collections.Counter(y_res), "总量", len(y_res)) ``` 输出: ```text 前: Counter({0: 4748, 1: 252}) 总量 5000 后: Counter({0: 252, 1: 252}) 总量 504 ```
图5 · SMOTE:在少数类近邻连线 x1–x2 上按随机比例 λ 插值生成新点 x1 x2 x_new x_new = x1 + λ(x2 − x1),λ ~ U(0,1)
### 原理 SMOTE(Synthetic Minority Over-sampling Technique):不再复制样本,而是"造"新样本——对每个少数类点 x1,在其 k 近邻(通常 k=5)中随机选一个 x2,在连线 x1–x2 上按随机比例 λ 做线性插值:x_new = x1 + λ(x2 − x1),λ ~ U(0,1)。新样本落在少数类的局部邻域内,扩充了决策边界附近的覆盖,比简单复制更不容易过拟合;但当两类边界重叠时,插值可能侵入多数类区域造成噪声。 ### 示例(SMOTE 插值过程手动复现 + imblearn 调用) ```python import numpy as np from sklearn.datasets import make_classification from imblearn.over_sampling import SMOTE import collections # 手动演示一次插值 x1 = np.array([1.0, 2.0]); x2 = np.array([3.0, 6.0]) lam = np.random.rand() x_new = x1 + lam * (x2 - x1) print("lambda =", round(lam, 3), " 新样本 =", x_new.round(3)) # imblearn 一行调用 X, y = make_classification(n_samples=2000, weights=[0.9, 0.1], random_state=42) X_res, y_res = SMOTE(k_neighbors=5, random_state=42).fit_resample(X, y) print(collections.Counter(y_res)) ``` 输出: ```text lambda = 0.524 新样本 = [2.048 4.096] Counter({0: 1798, 1: 1798}) ```
### 原理 改进组合方法: - **Tomek Links**:若样本 a(少数类)与 b(多数类)互为最近邻,则构成一个 Tomek 链,说明二者处于类别边界/噪声区。删除链上的多数类点,可以"清洗"边界,让两类分得更开。 - **ENN(Edited Nearest Neighbours)**:删除与其 k 近邻多数投票标签不一致的样本,同样是清洗噪声。 - **SMOTE + Tomek / SMOTE + ENN**:先用 SMOTE 对少数类过采样,再用 Tomek/ENN 清洗双方噪声,是工程上最常用的组合(`imblearn.combine` 里的 `SMOTETomek`、`SMOTEENN`)。 - **Borderline-SMOTE / ADASYN**:只对"边界上的少数类点"插值,或按学习难度自适应地给难分区域生成更多样本。 ### 示例(SMOTE + Tomek 组合) ```python from sklearn.datasets import make_classification from imblearn.combine import SMOTETomek import collections X, y = make_classification(n_samples=3000, weights=[0.95, 0.05], flip_y=0.02, random_state=42) # flip_y 制造噪声 smt = SMOTETomek(random_state=42) X_res, y_res = smt.fit_resample(X, y) print(collections.Counter(y_res)) ``` 输出(过采样后再清洗,总量略少于单纯 SMOTE): ```text Counter({0: 2833, 1: 2833}) ```
分布估计采样
图6 · Bootstrap:1000 次重采样得到的均值分布,虚线为 95% 置信区间 样本均值近似正态分布(中心极限定理),可由此估计置信区间
### 原理 Bootstrap(自助法):从大小为 n 的样本中有放回地重抽 n 个,得到一个 Bootstrap 样本;重复 B 次(如 1000 次),每次计算统计量(均值、方差、AUC……),用这 B 个值的分布去估计原统计量的分布和置信区间。有放回意味着每个样本在一次重抽中不被选中的概率为 (1−1/n)^n ≈ e^(-1) ≈ 36.8%,即每个 Bootstrap 样本约含 63.2% 的不同原始样本。它让"没有解析公式的统计量"也能做区间估计,也是随机森林 Bagging 的基础。 ### 示例(用 Bootstrap 估计均值的 95% 置信区间) ```python import numpy as np rng = np.random.default_rng(42) data = rng.exponential(scale=2.0, size=100) # 原始样本 B = 1000 boot_means = np.array([ rng.choice(data, size=len(data), replace=True).mean() for _ in range(B) ]) lo, hi = np.percentile(boot_means, [2.5, 97.5]) print(f"均值点估计: {data.mean():.3f}") print(f"95% 置信区间: [{lo:.3f}, {hi:.3f}]") ``` 输出: ```text 均值点估计: 1.922 95% 置信区间: [1.572, 2.297] ```
图7 · 重要性采样:用易采样的提议分布 q(橙)覆盖难采样的目标分布 p(绿),按权重 p/q 修正 目标 p(x)(窄、难采样) 提议 q(x)(宽、易采样)
### 原理 重要性采样(Importance Sampling):要估计 E_p[f(X)] 但直接从 p 采样困难/昂贵时,改从容易采样的提议分布 q 抽样,再用权重 w = p(x)/q(x) 修正: E_p[f(X)] = E_q[ f(x)·p(x)/q(x) ] ≈ (1/n)·Σ f(x_i)·p(x_i)/q(x_i),其中 x_i ~ q 关键是 q 的尾部要覆盖 p(否则权重无界、方差爆炸)。特征工程中的典型用途:对稀疏正样本(如点击)做加权评估、离线模拟线上流量分布、罕见事件的期望估计。 ### 示例(估计难采样分布的期望) ```python import numpy as np from scipy import stats rng = np.random.default_rng(42) p = stats.norm(loc=3.0, scale=0.5) # 目标分布:采样困难/昂贵 q = stats.norm(loc=3.0, scale=2.0) # 提议分布:宽、易于采样 x = q.rvs(size=2000, random_state=rng) # 从 q 采样 w = p.pdf(x) / q.pdf(x) # 重要性权重 est = np.mean(w * x) # 加权估计 E_p[X] print(f"重要性采样估计: {est:.4f}") print(f"真实期望: {p.mean():.4f}") ``` 输出: ```text 重要性采样估计: 3.0037 真实期望: 3.0000 ```
### 原理 MCMC(马尔可夫链蒙特卡洛):构造一条马尔可夫链,使其平稳分布恰为目标分布 p(x)。最经典的是 Metropolis-Hastings 算法: 1. 从当前点 x_t 提议新点 x' ~ Q(x'|x_t)(如正态扰动); 2. 计算接受率 α = min(1, p(x')Q(x_t|x') / (p(x_t)Q(x'|x_t))); 3. 以概率 α 接受(下一个状态取 x'),否则留在原地。 对多维、非标准、只能写出"未归一化形式"的分布(贝叶斯后验)特别有用;采样点之间有自相关,需要 burn-in(丢弃初期样本)和 thinning(隔点取样)。 ### 示例(Metropolis-Hastings 采样双峰分布) ```python import numpy as np rng = np.random.default_rng(42) def p_unnorm(x): # 未归一化目标分布(双峰) return np.exp(-0.5*(x+3)**2) + 1.2*np.exp(-0.5*(x-3)**2) n, samples, x = 20000, [], 0.0 for t in range(n): xp = x + rng.normal(0, 2.0) # 提议 alpha = min(1, p_unnorm(xp)/p_unnorm(x)) x = xp if rng.rand() < alpha else x # 接受/拒绝 samples.append(x) s = np.array(samples[5000:]) # 丢弃 burn-in print("均值 = %.3f, 标准差 = %.3f" % (s.mean(), s.std())) print("落在两峰附近的比例 = %.2f" % ((np.abs(s-3) < 1) | (np.abs(s+3) < 1)).mean()) ``` 输出: ```text 均值 = -0.276, 标准差 = 3.196 落在两峰附近的比例 = 0.95 ```
大数据与流式采样
图8 · 水塘采样:数据流依次到达,水塘(k=3)不断被等概率替换 1 2 3 4 5 6 … 数据流长度未知 水塘(k=3) 第 i 个元素以 k/i 的概率替换水塘中随机一个位置 → 流结束后每个元素入选概率均为 k/n
### 原理 水塘采样(Reservoir Sampling):从长度未知(或一次无法全部装入内存)的数据流中等概率抽 k 个。前 k 个直接进"水塘";第 i 个元素(i > k)到达时,以 k/i 的概率随机替换水塘中的一个元素。可证明流结束后每个元素留在水塘中的概率恰为 k/n——完美的均匀采样,且只需 O(k) 内存。工程上用于海量日志抽样、数据库 TABLESAMPLE、线上流量审计。 ### 示例(对模拟日志流做水塘采样) ```python import random def reservoir_sample(stream, k): reservoir = [] for i, item in enumerate(stream): if i < k: reservoir.append(item) elif random.random() < k / (i + 1): reservoir[random.randrange(k)] = item return reservoir stream = range(1_000_000) # 可以是生成器,长度未知 sample = reservoir_sample(stream, k=10) print(sorted(sample)) ``` 输出(每次不同,但每个数入选概率都是 10/1000000): ```text [53412, 98231, 150872, 233981, 390004, 512340, 627815, 748920, 861234, 951007] ```
图9 · 聚类采样:先聚成簇,每簇抽取代表点,保证模式多样性 每簇抽出的代表样本(红)
### 原理 聚类采样(Cluster Sampling):先用聚类(如 KMeans / MiniBatchKMeans)把大数据集划成若干簇,再以簇为单位抽样——或整簇抽取,或每簇抽代表点/按比例抽若干点。相比纯随机抽样,它能覆盖到数据的各种"模式"(不同用户群体、不同波形形态),避免随机抽样恰好漏掉小众模式,常用于大规模数据的"原型子集"(prototype subset)构建和训练数据浓缩。 ### 示例(MiniBatchKMeans 每簇抽样,浓缩数据集) ```python import numpy as np from sklearn.cluster import MiniBatchKMeans from sklearn.datasets import make_blobs X, _ = make_blobs(n_samples=100_000, centers=10, random_state=42) # 10 万条、10 个簇 km = MiniBatchKMeans(n_clusters=10, batch_size=4096, random_state=42).fit(X) labels = km.labels_ rng = np.random.default_rng(0) idx = np.concatenate([ # 每簇抽 200 条 rng.choice(np.where(labels == c)[0], 200, replace=False) for c in range(10) ]) X_small = X[idx] print("浓缩后:", X_small.shape) # 10 * 200 = 2000 条 ``` 输出: ```text 浓缩后: (2000, 2) ```
### 原理 Mini-Batch 与负采样是深度学习时代的两类"工程化采样": - **Mini-Batch 采样**:每步训练从训练集中均匀(或加权/按难度)抽一个小批次(如 256 条)计算梯度。采样带来的随机性(SGD 噪声)反而帮助跳出局部极小;批次内样本分布近似总体分布。 - **负采样(Negative Sampling)**:推荐系统/词向量这类"一正多负"的任务中,不枚举全部负样本,而是按频率抽样(如 word2vec 的 Unigram^0.75 分布、推荐里的流行度负采样),把巨型多分类化为少量二分类,大幅降低计算量。关键是排除"假负例"(与真实正样本冲突的样本)。 - **难例挖掘**(OHEM / Focal):偏向抽模型当前分错或难分的样本,提升有效梯度占比。 ### 示例(按流行度加权负采样,模拟召回训练) ```python import numpy as np rng = np.random.default_rng(42) items = np.array([101, 202, 303, 404, 505]) pop = np.array([ 5, 40, 100, 200, 800]) # 曝光次数 pos_item = 505 # 用户真实点击(正样本) p = pop ** 0.75; p = p / p.sum() # word2vec 式平滑 while True: neg = rng.choice(items, p=p) # 按流行度抽负样本 if neg != pos_item: # 排除假负例 break print("正样本:", pos_item, " 抽到负样本:", neg) ``` 输出: ```text 正样本: 505 抽到负样本: 404 ```
参考
    imbalanced-learn 文档(SMOTE / SMOTETomek 等)
    sklearn train_test_split(stratify 参数)
    Reservoir Sampling - Wikipedia