每日算法 — 使用java实现逻辑回归:sigmoid对数几率模型与交叉熵损失优化

引言

逻辑回归(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 测试分析和基线模型构建的首选工具。掌握逻辑回归,意味着掌握了从”预测值”到”预测概率”的思维跃迁。