每日算法 — 使用java实现线性回归:最小二乘法闭式解与批量梯度下降迭代

引言

线性回归(Linear Regression)是机器学习领域最基础、最经典的监督学习算法之一。它试图找到一条最佳的直线(或超平面),使得所有样本点到这条直线的垂直距离之和最小。从统计学中的”最小二乘法”到机器学习中的”梯度下降优化”,线性回归横跨了统计推断与优化计算两大领域。本文将用 Java 从零实现线性回归的两种求解方式——最小二乘法的解析闭式解和批量梯度下降的迭代优化,并在一个真实的房价预测数据集上进行验证。

线性回归的数学模型

给定 n 个样本,每个样本有 m 个特征,线性回归的假设函数为:

h(x) = w₀ + w₁x₁ + w₂x₂ + ... + wₘxₘ = wᵀx

其中 w 为待求参数向量,x 为特征向量(通常 x₀ = 1 以引入偏置项 w₀)。

目标函数(均方误差 MSE):

J(w) = (1/2n) × Σ(h(xⁱ) - yⁱ)²

我们的目标是找到使 J(w) 最小的参数 w。

方法一:最小二乘法(解析解)

当特征矩阵 X 的列满秩时,目标函数对 w 求导并令导数为零,可得闭式解:

w = (XᵀX)⁻¹Xᵀy

Java 实现

import java.util.Arrays;

/**
 * 线性回归 —— 最小二乘法解析解实现
 * 使用矩阵运算求解闭式解:w = (X^T * X)^(-1) * X^T * y
 */
public class LinearRegressionAnalytical {

    private double[] weights;  // 参数向量 [w0, w1, w2, ...]

    /**
     * 训练模型:计算最小二乘闭式解
     * @param X 特征矩阵,每行一个样本,每列一个特征(已包含偏置列1)
     * @param y 目标值向量
     */
    public void fit(double[][] X, double[] y) {
        int n = X.length;      // 样本数
        int m = X[0].length;   // 特征数(含偏置)

        // 计算 X^T * X(m × m 矩阵)
        double[][] XtX = matrixMultiply(transpose(X), X);

        // 计算 X^T * y(m × 1 向量)
        double[] Xty = matrixVectorMultiply(transpose(X), y);

        // 计算 (X^T * X)^(-1)
        double[][] XtXInv = matrixInverse(XtX);

        // w = (X^T * X)^(-1) * X^T * y
        weights = matrixVectorMultiply(XtXInv, Xty);
    }

    /**
     * 预测单个样本
     */
    public double predict(double[] x) {
        double result = 0;
        for (int i = 0; i < weights.length; i++) {
            result += weights[i] * x[i];
        }
        return result;
    }

    // ========== 矩阵工具方法 ==========

    /**
     * 矩阵转置
     */
    private double[][] transpose(double[][] A) {
        int rows = A.length, cols = A[0].length;
        double[][] T = new double[cols][rows];
        for (int i = 0; i < rows; i++)
            for (int j = 0; j < cols; j++)
                T[j][i] = A[i][j];
        return T;
    }

    /**
     * 矩阵乘法 A × B
     */
    private double[][] matrixMultiply(double[][] A, double[][] B) {
        int aRows = A.length, aCols = A[0].length;
        int bCols = B[0].length;
        double[][] C = new double[aRows][bCols];
        for (int i = 0; i < aRows; i++)
            for (int j = 0; j < bCols; j++)
                for (int k = 0; k < aCols; k++)
                    C[i][j] += A[i][k] * B[k][j];
        return C;
    }

    /**
     * 矩阵与向量相乘
     */
    private double[] matrixVectorMultiply(double[][] A, double[] v) {
        int rows = A.length;
        double[] result = new double[rows];
        for (int i = 0; i < rows; i++)
            for (int j = 0; j < v.length; j++)
                result[i] += A[i][j] * v[j];
        return result;
    }

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

        // 构造增广矩阵 [A | I]
        for (int i = 0; i < n; i++) {
            System.arraycopy(A[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[] getWeights() {
        return weights;
    }
}

方法二:批量梯度下降(迭代优化)

当特征数很大或 XᵀX 不可逆时,解析解不再适用,此时梯度下降是更通用的求解方法。

梯度公式:

∂J/∂wⱼ = (1/n) × Σ(h(xⁱ) - yⁱ) × xⱼⁱ

参数更新:

wⱼ := wⱼ - α × ∂J/∂wⱼ

Java 实现

/**
 * 线性回归 —— 批量梯度下降实现
 * 通过迭代优化逐步逼近最优参数
 */
public class LinearRegressionGD {

    private double[] weights;
    private double learningRate;
    private int maxIterations;
    private double convergenceThreshold;

    public LinearRegressionGD(double lr, int maxIter, double threshold) {
        this.learningRate = lr;
        this.maxIterations = maxIter;
        this.convergenceThreshold = threshold;
    }

    /**
     * 训练模型
     * @param X 特征矩阵(已包含偏置列1)
     * @param y 目标值
     */
    public void fit(double[][] X, double[] y) {
        int n = X.length;
        int m = X[0].length;
        weights = new double[m];

        // 随机初始化参数
        java.util.Random rand = new java.util.Random(42);
        for (int i = 0; i < m; i++) {
            weights[i] = rand.nextDouble() * 0.01;
        }

        double prevCost = Double.MAX_VALUE;

        for (int iter = 0; iter < maxIterations; iter++) {
            double[] gradients = new double[m];
            double cost = 0;

            // 计算所有样本的梯度和损失
            for (int i = 0; i < n; i++) {
                double prediction = predict(X[i]);
                double error = prediction - y[i];
                cost += error * error;

                for (int j = 0; j < m; j++) {
                    gradients[j] += error * X[i][j];
                }
            }

            cost /= (2 * n);

            // 平均梯度
            for (int j = 0; j < m; j++) {
                gradients[j] /= n;
            }

            // 更新参数
            for (int j = 0; j < m; j++) {
                weights[j] -= learningRate * gradients[j];
            }

            // 检查收敛
            if (Math.abs(prevCost - cost) < convergenceThreshold) {
                System.out.println("梯度下降在迭代 " + iter + " 次后收敛");
                break;
            }
            prevCost = cost;

            if (iter % 1000 == 0) {
                System.out.println("迭代 " + iter + ", 损失 = " + String.format("%.6f", cost));
            }
        }
    }

    public double predict(double[] x) {
        double result = 0;
        for (int i = 0; i < weights.length; i++) {
            result += weights[i] * x[i];
        }
        return result;
    }

    public double[] getWeights() {
        return weights;
    }
}

特征标准化

梯度下降对特征的尺度敏感,标准化处理能加速收敛。

/**
 * 特征标准化工具类
 * 将每个特征缩放到均值为0、标准差为1的分布
 */
public class StandardScaler {
    private double[] means;
    private double[] stds;

    public double[][] fitTransform(double[][] X) {
        int n = X.length, m = X[0].length;
        means = new double[m];
        stds = new double[m];

        // 计算均值
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                means[j] += X[i][j];
        for (int j = 0; j < m; j++) means[j] /= n;

        // 计算标准差
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                stds[j] += Math.pow(X[i][j] - means[j], 2);
        for (int j = 0; j < m; j++) stds[j] = Math.sqrt(stds[j] / n);

        // 标准化
        double[][] scaled = new double[n][m];
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                scaled[i][j] = (X[i][j] - means[j]) / (stds[j] + 1e-8);
        return scaled;
    }
}

完整测试:房价预测

public class LinearRegressionDemo {
    public static void main(String[] args) {
        // 模拟房价数据:面积(平方米) -> 房价(万元)
        // 第一列固定为1(偏置项),第二列为面积
        double[][] X = {
            {1, 50}, {1, 60}, {1, 70}, {1, 80}, {1, 90},
            {1, 100}, {1, 110}, {1, 120}, {1, 130}, {1, 140}
        };
        double[] y = {150, 180, 210, 240, 270, 300, 330, 360, 390, 420};

        System.out.println("===== 最小二乘法解析解 =====");
        LinearRegressionAnalytical model1 = new LinearRegressionAnalytical();
        model1.fit(X, y);
        System.out.println("参数: " + Arrays.toString(model1.getWeights()));
        System.out.println("预测面积=85: " + model1.predict(new double[]{1, 85}) + " 万元");

        System.out.println("\n===== 批量梯度下降 =====");
        LinearRegressionGD model2 = new LinearRegressionGD(0.001, 50000, 1e-9);
        model2.fit(X, y);
        System.out.println("参数: " + Arrays.toString(model2.getWeights()));
        System.out.println("预测面积=85: " + model2.predict(new double[]{1, 85}) + " 万元");

        // 计算 R² 决定系数
        double ssTot = 0, ssRes = 0;
        double yMean = Arrays.stream(y).average().orElse(0);
        for (int i = 0; i < y.length; i++) {
            ssTot += Math.pow(y[i] - yMean, 2);
            ssRes += Math.pow(y[i] - model1.predict(X[i]), 2);
        }
        double r2 = 1 - ssRes / ssTot;
        System.out.println("\nR² 决定系数: " + String.format("%.6f", r2));
    }
}

复杂度分析

方法 训练时间复杂度 预测时间复杂度 空间复杂度 适用场景
最小二乘法 O(m²n + m³) O(m) O(m²) 特征数 m 较小
批量梯度下降 O(knm) O(m) O(m) 特征数大或矩阵不可逆

其中 n 为样本数,m 为特征数,k 为迭代次数。

总结

线性回归虽然是最简单的机器学习模型,却蕴含了统计推断与优化计算的核心思想。最小二乘法的解析解直接而优雅,让我们看到了线性代数在机器学习中的力量;批量梯度下降的迭代优化则更具通用性,是理解神经网络反向传播等高级优化算法的基础。掌握线性回归的这两种实现方式,不仅能为后续的多元回归、岭回归、LASSO 等进阶模型打下坚实基础,更能帮助建立”建模-求解-评估”的完整机器学习思维框架。