引言
线性回归(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 等进阶模型打下坚实基础,更能帮助建立”建模-求解-评估”的完整机器学习思维框架。