每日算法 — 使用java实现拒绝采样:概率分布生成与接受-拒绝策略

算法概述

拒绝采样(Rejection Sampling)是一种从复杂概率分布中生成随机样本的基础蒙特卡洛方法。当目标分布的概率密度函数复杂、难以直接采样时,可以通过一个易于采样的提议分布(Proposal Distribution)配合接受-拒绝机制,间接获得服从目标分布的样本。

本文将用Java实现拒绝采样算法,涵盖均匀分布、正态分布、Beta分布等典型场景的采样实现,并分析其效率与适用范围。

核心原理

设目标分布的概率密度函数为 (f(x)),提议分布为 (g(x)),且存在常数 (M) 使得对所有 (x) 满足 (f(x) \leq M \cdot g(x))。拒绝采样的步骤如下:

  1. 从提议分布 (g(x)) 中采样得到一个候选值 (x)
  2. 从均匀分布 (U(0, 1)) 中采样得到 (u)
  3. 若 (u \leq \frac{f(x)}{M \cdot g(x)}),则接受 (x) 作为样本;否则拒绝并重复

接受概率为 (\frac{1}{M}),因此 (M) 越接近1,采样效率越高。

Java实现

项目结构

rejection-sampling/
├── RejectionSampler.java      // 拒绝采样核心框架
├── UniformSampler.java        // 均匀分布采样示例
├── NormalSampler.java         // 正态分布采样示例
├── BetaSampler.java           // Beta分布采样示例
└── Main.java                  // 测试入口

核心框架:RejectionSampler.java

import java.util.Random;
import java.util.function.DoubleFunction;

/**
 * 拒绝采样通用框架
 * 通过提议分布和接受-拒绝机制从目标分布中采样
 */
public class RejectionSampler {

    private final Random random;

    public RejectionSampler() {
        this.random = new Random();
    }

    public RejectionSampler(long seed) {
        this.random = new Random(seed);
    }

    /**
     * 执行一次拒绝采样
     * @param proposalSampler 提议分布采样器(返回候选值)
     * @param targetDensity   目标分布概率密度函数
     * @param proposalDensity 提议分布概率密度函数
     * @param m              上界常数 M,需满足 target(x) <= M * proposal(x)
     * @return 采样值
     */
    public double sample(
            java.util.function.Supplier<Double> proposalSampler,
            DoubleFunction<Double> targetDensity,
            DoubleFunction<Double> proposalDensity,
            double m) {

        if (m <= 0) {
            throw new IllegalArgumentException("M必须为正数");
        }

        while (true) {
            // 步骤1:从提议分布采样
            double x = proposalSampler.get();

            // 步骤2:生成均匀随机数
            double u = random.nextDouble();

            // 步骤3:计算接受概率
            double acceptanceProb = targetDensity.apply(x) / (m * proposalDensity.apply(x));

            // 接受或拒绝
            if (u <= acceptanceProb) {
                return x;
            }
        }
    }

    /**
     * 批量采样
     */
    public double[] sampleBatch(
            java.util.function.Supplier<Double> proposalSampler,
            DoubleFunction<Double> targetDensity,
            DoubleFunction<Double> proposalDensity,
            double m,
            int n) {

        double[] samples = new double[n];
        for (int i = 0; i < n; i++) {
            samples[i] = sample(proposalSampler, targetDensity, proposalDensity, m);
        }
        return samples;
    }

    /**
     * 有界拒绝采样:在区间[a, b]内采样
     * 使用均匀分布作为提议分布
     */
    public double sampleBounded(
            DoubleFunction<Double> targetDensity,
            double a, double b,
            double maxDensity) {

        if (a >= b || maxDensity <= 0) {
            throw new IllegalArgumentException("参数非法");
        }

        double interval = b - a;
        // 均匀分布密度为 1/(b-a),因此 M = maxDensity * (b-a)
        double m = maxDensity * interval;

        while (true) {
            double x = a + random.nextDouble() * interval;
            double u = random.nextDouble();
            if (u <= targetDensity.apply(x) / maxDensity) {
                return x;
            }
        }
    }
}

正态分布采样:NormalSampler.java

/**
 * 使用拒绝采样从标准正态分布 N(0,1) 中采样
 * 提议分布使用柯西分布(Cauchy),因其尾部与正态分布相似
 */
public class NormalSampler {

    private final RejectionSampler sampler;
    private final java.util.Random random;

    public NormalSampler() {
        this.sampler = new RejectionSampler();
        this.random = new java.util.Random();
    }

    /**
     * 标准正态分布概率密度函数
     */
    public static double normalPdf(double x) {
        return Math.exp(-x * x / 2.0) / Math.sqrt(2.0 * Math.PI);
    }

    /**
     * 柯西分布概率密度函数
     */
    public static double cauchyPdf(double x) {
        return 1.0 / (Math.PI * (1.0 + x * x));
    }

    /**
     * 柯西分布采样:使用逆变换法
     * CDF的逆函数为 tan(π * (u - 0.5))
     */
    public double sampleCauchy() {
        double u = random.nextDouble();
        return Math.tan(Math.PI * (u - 0.5));
    }

    /**
     * 通过拒绝采样获取标准正态分布样本
     * 理论最优M = sqrt(2π/e) ≈ 1.52
     */
    public double sampleStandardNormal() {
        double m = Math.sqrt(2.0 * Math.PI / Math.E); // ≈ 1.52

        return sampler.sample(
            this::sampleCauchy,
            NormalSampler::normalPdf,
            NormalSampler::cauchyPdf,
            m
        );
    }

    /**
     * 一般正态分布 N(mu, sigma^2)
     */
    public double sampleNormal(double mu, double sigma) {
        return mu + sigma * sampleStandardNormal();
    }

    public static void main(String[] args) {
        NormalSampler sampler = new NormalSampler();

        // 生成10000个样本并统计均值和方差
        int n = 10000;
        double sum = 0, sumSq = 0;
        double[] samples = new double[n];

        for (int i = 0; i < n; i++) {
            samples[i] = sampler.sampleStandardNormal();
            sum += samples[i];
            sumSq += samples[i] * samples[i];
        }

        System.out.printf("样本均值: %.4f (理论: 0)%n", sum / n);
        System.out.printf("样本方差: %.4f (理论: 1)%n", sumSq / n - (sum / n) * (sum / n));
    }
}

Beta分布采样:BetaSampler.java

/**
 * Beta分布拒绝采样实现
 * Beta分布在[0,1]区间有界,适合使用均匀分布作为提议分布
 */
public class BetaSampler {

    private final RejectionSampler sampler;

    public BetaSampler() {
        this.sampler = new RejectionSampler();
    }

    /**
     * Beta分布概率密度函数
     * f(x; alpha, beta) = x^(alpha-1) * (1-x)^(beta-1) / B(alpha, beta)
     */
    public static double betaPdf(double x, double alpha, double beta) {
        if (x <= 0 || x >= 1) return 0;
        // 使用对数避免溢出
        double logBeta = logGamma(alpha) + logGamma(beta) - logGamma(alpha + beta);
        double logPdf = (alpha - 1) * Math.log(x) + (beta - 1) * Math.log(1 - x) - logBeta;
        return Math.exp(logPdf);
    }

    /**
     * 伽马函数的对数近似(Stirling近似)
     * 对于正整数和半整数足够精确
     */
    private static double logGamma(double z) {
        if (z <= 0) throw new IllegalArgumentException("z必须为正");
        // Lanczos近似简化版
        if (z < 1) return logGamma(z + 1) - Math.log(z);
        if (z == 1 || z == 2) return 0;

        double x = z - 1;
        double[] p = {0.99999999999980993, 676.5203681218851, -1259.1392167224028,
                      771.32342877765313, -176.61502916214059, 12.507343278686905,
                      -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7};
        double a = p[0];
        for (int i = 1; i < p.length; i++) {
            a += p[i] / (x + i);
        }
        double t = x + p.length - 0.5;
        return 0.5 * Math.log(2 * Math.PI) + Math.log(a) - t + (x + 0.5) * Math.log(t);
    }

    /**
     * 采样Beta(alpha, beta)
     * 使用均匀分布作为提议分布,M取PDF在众数处的值
     */
    public double sampleBeta(double alpha, double beta) {
        if (alpha <= 0 || beta <= 0) {
            throw new IllegalArgumentException("alpha和beta必须为正");
        }

        // Beta分布的众数:(alpha-1)/(alpha+beta-2),当alpha,beta>1时
        double mode;
        if (alpha > 1 && beta > 1) {
            mode = (alpha - 1) / (alpha + beta - 2);
        } else if (alpha <= 1 && beta <= 1) {
            mode = 0.5; // 双凹,取中点
        } else if (alpha <= 1) {
            mode = 0.01; // 右偏,靠近0
        } else {
            mode = 0.99; // 左偏,靠近1
        }

        double maxDensity = betaPdf(mode, alpha, beta);
        double m = maxDensity; // 均匀分布在[0,1]的密度为1

        return sampler.sampleBounded(
            x -> betaPdf(x, alpha, beta),
            0, 1, maxDensity
        );
    }

    public static void main(String[] args) {
        BetaSampler sampler = new BetaSampler();
        double alpha = 2.0, beta = 5.0;

        int n = 5000;
        double sum = 0;
        for (int i = 0; i < n; i++) {
            double s = sampler.sampleBeta(alpha, beta);
            sum += s;
        }

        System.out.printf("Beta(%.1f, %.1f) 样本均值: %.4f (理论: %.4f)%n",
            alpha, beta, sum / n, alpha / (alpha + beta));
    }
}

测试入口:Main.java

public class Main {
    public static void main(String[] args) {
        System.out.println("=== 拒绝采样算法测试 ===\n");

        // 测试1:标准正态分布
        System.out.println("--- 标准正态分布 N(0,1) ---");
        NormalSampler normalSampler = new NormalSampler();
        int n = 10000;
        double[] normalSamples = new double[n];
        double sum = 0, sumSq = 0;
        for (int i = 0; i < n; i++) {
            normalSamples[i] = normalSampler.sampleStandardNormal();
            sum += normalSamples[i];
            sumSq += normalSamples[i] * normalSamples[i];
        }
        double mean = sum / n;
        double var = sumSq / n - mean * mean;
        System.out.printf("样本量: %d, 均值: %.4f, 方差: %.4f%n%n", n, mean, var);

        // 测试2:Beta分布
        System.out.println("--- Beta(2, 5) 分布 ---");
        BetaSampler betaSampler = new BetaSampler();
        sum = 0;
        for (int i = 0; i < n; i++) {
            sum += betaSampler.sampleBeta(2.0, 5.0);
        }
        System.out.printf("样本量: %d, 均值: %.4f (理论: 0.2857)%n%n", n, sum / n);

        // 测试3:有界采样 - 半圆分布
        System.out.println("--- 半圆分布(Wigner半圆律)---");
        RejectionSampler sampler = new RejectionSampler();
        sum = 0; sumSq = 0;
        for (int i = 0; i < n; i++) {
            double x = sampler.sampleBounded(
                x -> Math.sqrt(Math.max(0, 1 - x * x)) * 2 / Math.PI,
                -1, 1, 2.0 / Math.PI
            );
            sum += x;
            sumSq += x * x;
        }
        System.out.printf("样本量: %d, 均值: %.4f, 二阶矩: %.4f%n", n, sum / n, sumSq / n);
    }
}

复杂度分析

指标 复杂度
单次采样期望时间 (O(M)),其中 (M) 为接受概率倒数
空间复杂度 (O(1)),仅需存储常数和随机数生成器
批量采样 (n) 个 (O(nM)) 期望时间

拒绝采样的核心瓶颈在于接受率。当目标分布与提议分布差异较大时((M \gg 1)),大量候选样本被拒绝,效率急剧下降。在高维空间中,(M) 通常随维度指数增长,导致拒绝采样在高维场景下失效——这被称为”维度诅咒”。

适用场景与局限性

适用场景:
– 低维概率分布(1-3维)的随机采样
– 概率密度函数已知但累积分布函数难以求逆的情况
– 需要精确服从目标分布的蒙特卡洛模拟

局限性:
– 高维空间中效率极低
– 需要已知目标分布的密度上界 (M)
– 对于重尾分布,难以找到合适的提议分布

扩展方向

  1. 自适应拒绝采样:动态调整提议分布以提高接受率
  2. 切片采样:将多维采样转化为均匀分布采样
  3. Metropolis-Hastings算法:马尔可夫链蒙特卡洛方法的推广

总结

拒绝采样是理解蒙特卡洛方法的入门算法。通过Java实现,我们掌握了从复杂分布中生成样本的核心思想:用简单的提议分布包裹复杂的目标分布,以概率接受机制保证样本的正确性。尽管在高维场景下存在局限,但在低维概率建模、A/B测试、贝叶斯推断等领域,拒绝采样仍是一种简洁而有效的工具。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注