每日算法 — 使用java实现贝叶斯优化:高斯过程与采集函数超参数调优

引言

在机器学习实践中,模型超参数的选择直接决定了最终性能。网格搜索与随机搜索虽然简单,但在高维空间中效率极低。贝叶斯优化(Bayesian Optimization)是一种高效的全局优化方法,通过构建目标函数的概率代理模型,在有限的评估次数内找到近似最优解。它已被广泛应用于神经网络架构搜索、超参数调优、材料设计等领域。

本文将用Java实现完整的贝叶斯优化框架,核心包括高斯过程回归建模目标函数的不确定性,以及采集函数(UCB/EI)平衡探索与利用的采样策略。

核心原理

1. 问题定义

寻找黑箱函数的最小值:
$$x^* = \arg\min_{x \in \mathcal{X}} f(x)$$

其中 $f(x)$ 计算代价高昂,无闭式表达式,仅能通过采样获取函数值(可能含噪声)。

2. 高斯过程回归(Gaussian Process Regression)

高斯过程是定义在函数空间上的概率分布,任意有限个采样点的函数值服从联合高斯分布:
$$f(x) \sim GP(m(x), k(x, x’))$$

其中:
– $m(x)$:均值函数,通常设为0
– $k(x, x’)$:核函数(协方差函数),衡量两点间的相似度

常用的径向基核函数(RBF):
$$k(x, x’) = \sigma^2 \exp\left(-\frac{|x – x’|^2}{2l^2}\right)$$

给定观测数据 $D = {(x_i, y_i)}{i=1}^n$,对新点 $x$ 的预测分布为:
$$\mu(x_
) = k_^T (K + \sigma_n^2 I)^{-1} y$$
$$\sigma^2(x_
) = k(x_, x_) – k_^T (K + \sigma_n^2 I)^{-1} k_$$

其中 $K_{ij} = k(x_i, x_j)$,$k_ = [k(x_, x_1), …, k(x_*, x_n)]^T$。

3. 采集函数(Acquisition Function)

采集函数指导下一个采样点的选择,在探索(高不确定性区域)与利用(高期望收益区域)之间取得平衡。

上置信界(UCB)

$$UCB(x) = \mu(x) + \kappa \cdot \sigma(x)$$

其中 $\kappa$ 控制探索程度,值越大越偏向探索未知区域。

期望改进(Expected Improvement, EI)

$$EI(x) = E[\max(0, f_{best} – f(x))]$$

对于高斯分布,EI有闭式解:
$$EI(x) = (f_{best} – \mu(x)) \Phi(Z) + \sigma(x) \phi(Z)$$

其中 $Z = \frac{f_{best} – \mu(x)}{\sigma(x)}$,$\Phi$ 和 $\phi$ 分别为标准正态分布的CDF和PDF。

Java实现

import java.util.*;

/**
 * 贝叶斯优化框架
 * 高斯过程回归 + UCB/EI采集函数
 */
public class BayesianOptimizer {

    private final int dim; // 参数维度
    private final double[] lowerBound;
    private final double[] upperBound;
    private final Kernel kernel;
    private final AcquisitionFunction acquisition;
    private final double noiseSigma; // 观测噪声

    // 观测数据
    private final List<double[]> X = new ArrayList<>();
    private final List<Double> y = new ArrayList<>();

    // 高斯过程缓存
    private double[][] K; // 核矩阵
    private double[][] KInv; // 核矩阵逆
    private double[] alpha; // (K + sigma^2 I)^{-1} y

    public BayesianOptimizer(int dim, double[] lowerBound, double[] upperBound,
                             String kernelType, String acqType, double noiseSigma) {
        this.dim = dim;
        this.lowerBound = lowerBound.clone();
        this.upperBound = upperBound.clone();
        this.noiseSigma = noiseSigma;

        // 初始化核函数
        if ("rbf".equalsIgnoreCase(kernelType)) {
            this.kernel = new RBFKernel(1.0, 1.0); // 默认长度尺度和信号方差
        } else {
            this.kernel = new RBFKernel(1.0, 1.0);
        }

        // 初始化采集函数
        if ("ucb".equalsIgnoreCase(acqType)) {
            this.acquisition = new UCB(2.0); // kappa = 2.0
        } else if ("ei".equalsIgnoreCase(acqType)) {
            this.acquisition = new ExpectedImprovement();
        } else {
            this.acquisition = new UCB(2.0);
        }
    }

    /**
     * 核函数接口
     */
    interface Kernel {
        double eval(double[] x1, double[] x2);
    }

    /**
     * RBF径向基核函数
     */
    static class RBFKernel implements Kernel {
        private final double lengthScale;
        private final double signalVariance;

        RBFKernel(double lengthScale, double signalVariance) {
            this.lengthScale = lengthScale;
            this.signalVariance = signalVariance;
        }

        @Override
        public double eval(double[] x1, double[] x2) {
            double distSq = 0;
            for (int i = 0; i < x1.length; i++) {
                double diff = x1[i] - x2[i];
                distSq += diff * diff;
            }
            return signalVariance * Math.exp(-distSq / (2 * lengthScale * lengthScale));
        }
    }

    /**
     * 采集函数接口
     */
    interface AcquisitionFunction {
        double eval(double mean, double std, double bestY);
    }

    /**
     * 上置信界(UCB)采集函数
     */
    static class UCB implements AcquisitionFunction {
        private final double kappa;

        UCB(double kappa) {
            this.kappa = kappa;
        }

        @Override
        public double eval(double mean, double std, double bestY) {
            // 注意:这里最小化问题,所以用LCB
            return mean - kappa * std;
        }
    }

    /**
     * 期望改进(EI)采集函数
     */
    static class ExpectedImprovement implements AcquisitionFunction {
        @Override
        public double eval(double mean, double std, double bestY) {
            if (std < 1e-10) return 0;
            double z = (bestY - mean) / std;
            double ei = (bestY - mean) * normalCDF(z) + std * normalPDF(z);
            return ei;
        }
    }

    /**
     * 标准正态分布PDF
     */
    static double normalPDF(double x) {
        return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
    }

    /**
     * 标准正态分布CDF(使用误差函数近似)
     */
    static double normalCDF(double x) {
        return 0.5 * (1 + erf(x / Math.sqrt(2)));
    }

    /**
     * 误差函数近似(Abramowitz and Stegun)
     */
    static double erf(double x) {
        double sign = x >= 0 ? 1 : -1;
        x = Math.abs(x);
        double a1 = 0.254829592, a2 = -0.284496736;
        double a3 = 1.421413741, a4 = -1.453152027, a5 = 1.061405429;
        double p = 0.3275911;
        double t = 1 / (1 + p * x);
        double y = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
        return sign * y;
    }

    /**
     * 添加观测数据点
     */
    public void observe(double[] x, double y) {
        X.add(x.clone());
        this.y.add(y);
        updateGP();
    }

    /**
     * 更新高斯过程模型
     */
    private void updateGP() {
        int n = X.size();
        K = new double[n][n];

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                K[i][j] = kernel.eval(X.get(i), X.get(j));
                if (i == j) K[i][j] += noiseSigma * noiseSigma;
            }
        }

        // 计算 K^{-1} y
        KInv = invertMatrix(K);
        alpha = new double[n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                alpha[i] += KInv[i][j] * y.get(j);
            }
        }
    }

    /**
     * 高斯过程预测:均值与标准差
     */
    public double[] predict(double[] x) {
        int n = X.size();
        double[] kStar = new double[n];
        for (int i = 0; i < n; i++) {
            kStar[i] = kernel.eval(x, X.get(i));
        }

        double mean = 0;
        for (int i = 0; i < n; i++) {
            mean += kStar[i] * alpha[i];
        }

        double kStarStar = kernel.eval(x, x);
        double v = 0;
        for (int i = 0; i < n; i++) {
            double sum = 0;
            for (int j = 0; j < n; j++) {
                sum += kStar[j] * KInv[j][i];
            }
            v += sum * kStar[i];
        }
        double variance = kStarStar - v;
        double std = Math.sqrt(Math.max(0, variance));

        return new double[]{mean, std};
    }

    /**
     * 选择下一个采样点(最大化采集函数)
     */
    public double[] suggestNext() {
        double bestY = Double.POSITIVE_INFINITY;
        for (double val : y) {
            if (val < bestY) bestY = val;
        }

        // 使用随机采样 + 局部优化寻找采集函数最大值
        double[] bestX = null;
        double bestAcq = Double.NEGATIVE_INFINITY;

        Random rand = new Random();
        int nCandidates = 1000;

        for (int i = 0; i < nCandidates; i++) {
            double[] candidate = new double[dim];
            for (int d = 0; d < dim; d++) {
                candidate[d] = lowerBound[d] + rand.nextDouble() * (upperBound[d] - lowerBound[d]);
            }

            double[] pred = predict(candidate);
            double acq = acquisition.eval(pred[0], pred[1], bestY);

            if (acq > bestAcq) {
                bestAcq = acq;
                bestX = candidate.clone();
            }
        }

        // 简单局部优化:在最佳候选点附近微调
        if (bestX != null) {
            for (int step = 0; step < 100; step++) {
                double[] neighbor = bestX.clone();
                int d = rand.nextInt(dim);
                double delta = (rand.nextDouble() - 0.5) * (upperBound[d] - lowerBound[d]) * 0.1;
                neighbor[d] += delta;
                neighbor[d] = Math.max(lowerBound[d], Math.min(upperBound[d], neighbor[d]));

                double[] pred = predict(neighbor);
                double acq = acquisition.eval(pred[0], pred[1], bestY);
                if (acq > bestAcq) {
                    bestAcq = acq;
                    bestX = neighbor;
                }
            }
        }

        return bestX;
    }

    /**
     * 获取当前最优解
     */
    public double[] getBest() {
        int bestIdx = 0;
        for (int i = 1; i < y.size(); i++) {
            if (y.get(i) < y.get(bestIdx)) bestIdx = i;
        }
        return X.get(bestIdx).clone();
    }

    public double getBestValue() {
        double best = Double.POSITIVE_INFINITY;
        for (double val : y) {
            if (val < best) best = val;
        }
        return best;
    }

    /**
     * 矩阵求逆(高斯-约旦消元法)
     */
    private double[][] invertMatrix(double[][] matrix) {
        int n = matrix.length;
        double[][] aug = new double[n][2 * n];

        for (int i = 0; i < n; i++) {
            System.arraycopy(matrix[i], 0, aug[i], 0, n);
            aug[i][n + i] = 1;
        }

        for (int i = 0; i < n; i++) {
            double pivot = aug[i][i];
            for (int j = 0; j < 2 * n; j++) aug[i][j] /= pivot;

            for (int k = 0; k < n; k++) {
                if (k == i) continue;
                double factor = aug[k][i];
                for (int j = 0; j < 2 * n; j++) {
                    aug[k][j] -= factor * aug[i][j];
                }
            }
        }

        double[][] inv = new double[n][n];
        for (int i = 0; i < n; i++) {
            System.arraycopy(aug[i], n, inv[i], 0, n);
        }
        return inv;
    }

    /**
     * 主优化流程
     */
    public double[] optimize(ObjectiveFunction obj, int nIterations, int nInit) {
        Random rand = new Random();

        // 1. 随机初始化
        for (int i = 0; i < nInit; i++) {
            double[] x = new double[dim];
            for (int d = 0; d < dim; d++) {
                x[d] = lowerBound[d] + rand.nextDouble() * (upperBound[d] - lowerBound[d]);
            }
            observe(x, obj.evaluate(x));
        }

        // 2. 迭代优化
        for (int iter = 0; iter < nIterations; iter++) {
            double[] nextX = suggestNext();
            observe(nextX, obj.evaluate(nextX));
        }

        return getBest();
    }

    /**
     * 目标函数接口
     */
    interface ObjectiveFunction {
        double evaluate(double[] x);
    }

    /**
     * 测试:优化经典测试函数
     */
    public static void main(String[] args) {
        // 测试1:Branin函数(2维,3个全局最小值)
        BayesianOptimizer bo = new BayesianOptimizer(
            2, 
            new double[]{-5, 0}, 
            new double[]{10, 15},
            "rbf", "ei", 1e-5
        );

        ObjectiveFunction branin = x -> {
            double a = x[1] - 5.1/(4*Math.PI*Math.PI)*x[0]*x[0] + 5/Math.PI*x[0] - 6;
            return a*a + 10*(1-1/(8*Math.PI))*Math.cos(x[0]) + 10;
        };

        double[] best = bo.optimize(branin, 30, 5);
        System.out.println("Branin函数最优值: " + bo.getBestValue());
        System.out.println("最优参数: [" + best[0] + ", " + best[1] + "]");
        System.out.println("观测次数: " + bo.X.size());

        // 测试2:Rosenbrock函数(高维山谷)
        BayesianOptimizer bo2 = new BayesianOptimizer(
            3,
            new double[]{-2, -2, -2},
            new double[]{2, 2, 2},
            "rbf", "ucb", 1e-5
        );

        ObjectiveFunction rosenbrock = x -> {
            double sum = 0;
            for (int i = 0; i < x.length - 1; i++) {
                sum += 100 * Math.pow(x[i+1] - x[i]*x[i], 2) + Math.pow(1 - x[i], 2);
            }
            return sum;
        };

        double[] best2 = bo2.optimize(rosenbrock, 50, 10);
        System.out.println("\nRosenbrock函数最优值: " + bo2.getBestValue());
        System.out.println("最优参数: [" + best2[0] + ", " + best2[1] + ", " + best2[2] + "]");
    }
}

算法复杂度分析

时间复杂度

  • 高斯过程预测:$O(n^2)$,其中 $n$ 为观测点数(核向量与逆矩阵的乘积)
  • 核矩阵求逆:$O(n^3)$,使用高斯-约旦消元法。当 $n$ 较大时可改用Cholesky分解或稀疏近似
  • 单次优化迭代:$O(C \cdot n^2)$,$C$ 为候选点数量
  • 总优化时间:$O(N \cdot n^3)$,$N$ 为总迭代次数

空间复杂度

  • 核矩阵及逆矩阵:$O(n^2)$
  • 观测数据:$O(n \cdot d)$,$d$ 为维度
  • 总空间:$O(n^2 + n \cdot d)$

扩展与改进

1. 核函数自适应

通过最大化边际似然(Marginal Likelihood)自动优化核函数的超参数(长度尺度 $l$ 和信号方差 $\sigma^2$):
$$\log p(y|X) = -\frac{1}{2}y^T K^{-1} y – \frac{1}{2}\log|K| – \frac{n}{2}\log(2\pi)$$

2. 稀疏高斯过程

当观测点超过1000时,使用诱导点(Inducing Points)或随机特征(Random Features)近似核矩阵,将复杂度降至 $O(m^2 n)$ 或 $O(n)$。

3. 并行采集函数

在分布式计算场景下,使用qEI(批量期望改进)或局部惩罚方法同时推荐多个评估点,加速并行优化。

4. 约束处理

对于带约束的优化问题,使用约束采集函数如CEI(Constrained Expected Improvement),同时建模目标函数和约束违反概率。

总结

贝叶斯优化通过概率建模将”黑箱”目标函数转化为可分析的不确定性分布,以采集函数智能引导采样,在样本效率上远超传统网格/随机搜索。本文实现的Java框架包含完整的高斯过程回归、UCB/EI采集函数及优化流程,可直接用于超参数调优、实验设计等场景。其核心思想——在不确定性中做决策——也是强化学习、主动学习等领域的共同基础。