引言
逻辑回归(Logistic Regression)虽然名字中带有”回归”二字,但它实际上是分类问题中最基础、最广泛应用的算法之一。从垃圾邮件识别到疾病诊断,从信用评分到广告点击率预测,逻辑回归凭借其可解释性强、训练速度快、输出概率化的特点,成为工业界首选的基线模型。与线性回归直接预测连续值不同,逻辑回归通过 sigmoid 函数将线性组合映射到 (0, 1) 区间,从而表示样本属于某一类别的概率。本文将用 Java 从零实现二分类逻辑回归,涵盖 sigmoid 激活、交叉熵损失、梯度下降优化以及 L2 正则化等核心内容。
从线性回归到逻辑回归
线性回归的输出范围是 (-∞, +∞),无法直接表示概率。逻辑回归引入了 sigmoid 函数:
σ(z) = 1 / (1 + e^(-z))
其中 z = wᵀx。sigmoid 函数将任意实数映射到 (0, 1) 区间,输出可以被解释为样本属于正类的概率:
P(y=1|x) = σ(wᵀx)
P(y=0|x) = 1 - σ(wᵀx)
交叉熵损失函数
对于二分类问题,采用交叉熵(Cross-Entropy)作为损失函数:
J(w) = -(1/n) × Σ[yⁱ log(h(xⁱ)) + (1-yⁱ) log(1-h(xⁱ))]
交叉熵损失相比均方误差,在分类问题中具有更好的凸性和梯度特性。
Java 实现
核心类结构
/**
* 二分类逻辑回归实现
* 支持梯度下降优化和 L2 正则化
*/
public class LogisticRegression {
private double[] weights; // 参数向量
private double learningRate; // 学习率
private int maxIterations; // 最大迭代次数
private double lambda; // L2 正则化系数
private java.util.List<Double> lossHistory; // 损失历史记录
public LogisticRegression(double lr, int maxIter, double lambda) {
this.learningRate = lr;
this.maxIterations = maxIter;
this.lambda = lambda;
this.lossHistory = new java.util.ArrayList<>();
}
/**
* Sigmoid 激活函数
* σ(z) = 1 / (1 + e^(-z))
* 将实数映射到 (0, 1) 区间
*/
private double sigmoid(double z) {
// 防止指数溢出,对 z 进行裁剪
if (z > 500) return 1.0;
if (z < -500) return 0.0;
return 1.0 / (1.0 + Math.exp(-z));
}
/**
* 预测单个样本属于正类的概率
* @param x 特征向量(第一维为1表示偏置)
* @return 概率值 (0, 1)
*/
public double predictProba(double[] x) {
double z = 0;
for (int i = 0; i < weights.length; i++) {
z += weights[i] * x[i];
}
return sigmoid(z);
}
/**
* 预测类别(0 或 1)
*/
public int predict(double[] x) {
return predictProba(x) >= 0.5 ? 1 : 0;
}
梯度下降训练
/**
* 使用批量梯度下降训练模型
* @param X 特征矩阵(每行一个样本,第一列为1)
* @param y 标签向量(0 或 1)
*/
public void fit(double[][] X, int[] y) {
int n = X.length; // 样本数
int m = X[0].length; // 特征维度(含偏置)
weights = new double[m];
// Xavier 风格初始化
java.util.Random rand = new java.util.Random(42);
double scale = Math.sqrt(1.0 / m);
for (int i = 0; i < m; i++) {
weights[i] = (rand.nextDouble() - 0.5) * 2 * scale;
}
for (int iter = 0; iter < maxIterations; iter++) {
double[] gradients = new double[m];
double totalLoss = 0;
// 计算所有样本的梯度和损失
for (int i = 0; i < n; i++) {
double proba = predictProba(X[i]);
double error = proba - y[i]; // 预测误差
// 累加交叉熵损失(加入小值防止 log(0))
double epsilon = 1e-15;
totalLoss -= y[i] * Math.log(proba + epsilon)
+ (1 - y[i]) * Math.log(1 - proba + epsilon);
// 累加梯度
for (int j = 0; j < m; j++) {
gradients[j] += error * X[i][j];
}
}
// 平均损失
totalLoss /= n;
lossHistory.add(totalLoss);
// 平均梯度并加入 L2 正则化(偏置项不惩罚)
for (int j = 0; j < m; j++) {
gradients[j] /= n;
if (j > 0) { // 跳过偏置项
gradients[j] += (lambda / n) * weights[j];
}
}
// 更新参数
for (int j = 0; j < m; j++) {
weights[j] -= learningRate * gradients[j];
}
// 每 1000 轮打印损失
if (iter % 1000 == 0 || iter == maxIterations - 1) {
System.out.println("迭代 " + iter + ", 交叉熵损失 = "
+ String.format("%.6f", totalLoss));
}
// 早停:损失变化很小则停止
if (iter > 100 && Math.abs(lossHistory.get(iter) - lossHistory.get(iter - 100)) < 1e-7) {
System.out.println("迭代 " + iter + " 早停");
break;
}
}
}
模型评估
/**
* 计算准确率
*/
public double accuracy(double[][] X, int[] y) {
int correct = 0;
for (int i = 0; i < X.length; i++) {
if (predict(X[i]) == y[i]) correct++;
}
return (double) correct / X.length;
}
/**
* 计算 AUC(基于预测概率排序)
*/
public double auc(double[][] X, int[] y) {
int n = X.length;
// 收集预测概率和真实标签
double[][] pairs = new double[n][2];
for (int i = 0; i < n; i++) {
pairs[i][0] = predictProba(X[i]);
pairs[i][1] = y[i];
}
// 按预测概率降序排序
java.util.Arrays.sort(pairs, (a, b) -> Double.compare(b[0], a[0]));
int posCount = 0, negCount = 0;
for (int label : y) {
if (label == 1) posCount++;
else negCount++;
}
double auc = 0;
int cumNeg = 0;
for (int i = 0; i < n; i++) {
if (pairs[i][1] == 1) {
auc += cumNeg;
} else {
cumNeg++;
}
}
return auc / ((double) posCount * negCount);
}
public double[] getWeights() {
return weights;
}
public java.util.List<Double> getLossHistory() {
return lossHistory;
}
}
特征标准化
/**
* 标准化工具:Z-score 标准化
* 逻辑回归对特征尺度敏感,标准化有助于梯度下降收敛
*/
public class FeatureScaler {
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 j = 1; j < m; j++) {
for (int i = 0; i < n; i++) means[j] += X[i][j];
means[j] /= n;
}
// 计算标准差
for (int j = 1; j < m; j++) {
for (int i = 0; i < n; i++)
stds[j] += Math.pow(X[i][j] - means[j], 2);
stds[j] = Math.sqrt(stds[j] / n);
}
// 标准化(偏置列保持为1)
double[][] scaled = new double[n][m];
for (int i = 0; i < n; i++) {
scaled[i][0] = 1; // 偏置
for (int j = 1; j < m; j++) {
scaled[i][j] = (X[i][j] - means[j]) / (stds[j] + 1e-8);
}
}
return scaled;
}
}
完整测试:学生录取预测
public class LogisticRegressionDemo {
public static void main(String[] args) {
// 模拟数据:考试1成绩, 考试2成绩 -> 是否录取(0/1)
double[][] X = {
{1, 34, 78}, {1, 30, 43}, {1, 40, 60}, {1, 50, 55}, {1, 60, 70},
{1, 70, 80}, {1, 80, 85}, {1, 90, 90}, {1, 45, 50}, {1, 55, 65},
{1, 65, 75}, {1, 75, 82}, {1, 85, 88}, {1, 95, 92}, {1, 35, 45},
{1, 48, 58}, {1, 58, 68}, {1, 68, 78}, {1, 78, 83}, {1, 88, 87}
};
int[] y = {0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1};
// 标准化特征
FeatureScaler scaler = new FeatureScaler();
double[][] XScaled = scaler.fitTransform(X);
System.out.println("===== 逻辑回归训练 =====");
LogisticRegression model = new LogisticRegression(0.5, 20000, 0.01);
model.fit(XScaled, y);
System.out.println("\n模型参数: " + java.util.Arrays.toString(model.getWeights()));
System.out.println("训练集准确率: " + String.format("%.2f%%", model.accuracy(XScaled, y) * 100));
System.out.println("训练集 AUC: " + String.format("%.4f", model.auc(XScaled, y)));
// 预测新样本
double[] newStudent = {1, 72, 76}; // 原始特征
double[] newStudentScaled = scaler.fitTransform(new double[][]{newStudent})[0];
double proba = model.predictProba(newStudentScaled);
System.out.println("\n新学生(72, 76)录取概率: " + String.format("%.2f%%", proba * 100));
}
}
算法复杂度分析
| 阶段 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 训练(每轮迭代) | O(nm) | O(m) |
| 训练(总 k 轮) | O(knm) | O(m) |
| 预测(单样本) | O(m) | O(1) |
其中 n 为样本数,m 为特征维度,k 为迭代轮数。
与线性回归的对比
| 特性 | 线性回归 | 逻辑回归 |
|---|---|---|
| 任务类型 | 回归(预测连续值) | 分类(预测离散类别) |
| 输出范围 | (-∞, +∞) | (0, 1) |
| 激活函数 | 无(线性) | sigmoid |
| 损失函数 | 均方误差 MSE | 交叉熵 Cross-Entropy |
| 应用场景 | 房价预测、销量预测 | 垃圾邮件识别、疾病诊断 |
总结
逻辑回归是理解分类问题的最佳入门模型。它以简洁的数学形式——sigmoid 函数将线性决策边界映射为概率——架起了线性模型与分类任务之间的桥梁。交叉熵损失函数的引入,使得参数优化过程具有良好的数学性质;L2 正则化的加入,则有效防止了过拟合。尽管深度学习和集成学习在复杂任务中表现更优,逻辑回归凭借其可解释性强、训练极快、内存占用小的优势,至今仍是工业界特征筛选、A/B 测试分析和基线模型构建的首选工具。掌握逻辑回归,意味着掌握了从”预测值”到”预测概率”的思维跃迁。