逻辑回归实战:从零实现到调优,打通机器学习工程化思维
2026/8/2 17:59:15 网站建设 项目流程

1. 从“头歌实训”到逻辑回归:一个机器学习新手的实战起点

如果你正在学习机器学习,尤其是刚刚接触分类算法,那么“逻辑回归”这个名字你一定不陌生。它常常被放在线性回归之后讲解,是很多人进入分类世界的第一个正式算法。但很多教程和课程,包括一些知名的在线平台,往往把重点放在了公式推导和数学原理上,导致很多学习者虽然能看懂公式,却不知道如何用代码一步步实现,更不清楚在实际数据上会遇到哪些“坑”。最近,我在辅导一些同学完成“头歌实训”平台的机器学习任务时,发现逻辑回归这个看似简单的算法,恰恰是新手从理论迈向实践的第一个“拦路虎”。这个实训任务本身可能只提供了一个框架或几个待填空的代码块,但背后隐藏的,是如何将数学公式转化为可运行的代码、如何处理数据、如何评估模型这一整套工程化思维。今天,我就结合这个常见的实训场景,抛开复杂的数学外壳,带你手把手、接地气地走一遍逻辑回归的完整实现与调优过程。你会发现,它不仅是算法,更是一套解决问题的标准流程。

2. 逻辑回归的本质:为什么叫“回归”却干着“分类”的活?

在开始敲代码之前,我们必须先搞清楚一个根本问题:逻辑回归明明是做分类的,为什么名字里带着“回归”二字?这个问题不搞清楚,后面的理解都会很别扭。

2.1 线性回归的延伸与局限

我们先回想一下线性回归。它的目标是找到一条直线(或超平面)$y = \theta^T x$,使得预测值$y$和真实值尽可能接近。这里的$y$是连续的,比如预测房价、销量。但如果我们要预测一个离散的结果,比如“是否生病”(0或1)、“是哪一类动物”(猫、狗、兔),直接把线性回归的连续值输出当作类别判断,就会出问题。线性回归的输出范围是$(-\infty, +\infty)$,而概率值需要在[0,1]之间,类别标签是离散的整数。

2.2 Sigmoid函数:将任意值压缩到概率区间

逻辑回归的精髓就在于引入了一个“激活函数”——Sigmoid函数(也叫Logistic函数)。它的公式是: $g(z) = \frac{1}{1 + e^{-z}}$ 这个函数长得很像一个拉长的“S”形曲线。它的魔力在于,无论输入$z$(即线性回归的结果$\theta^T x$)是多少,是正无穷大还是负无穷大,经过它的处理,输出都会被稳稳地压缩到0和1之间。你可以把它想象成一个非常公平的“概率转换器”。

  • 当 $z$ 趋近于正无穷时,$e^{-z}$ 趋近于0,因此 $g(z)$ 趋近于1。
  • 当 $z$ 趋近于负无穷时,$e^{-z}$ 趋近于正无穷,因此 $g(z)$ 趋近于0。
  • 当 $z = 0$ 时,$g(z) = 0.5$。

于是,逻辑回归的完整形式就变成了:$h_{\theta}(x) = g(\theta^T x) = \frac{1}{1 + e^{-\theta^T x}}$。这里的 $h_{\theta}(x)$ 就可以被解释为“给定输入特征$x$,其类别标签为1的概率”,即 $P(y=1|x;\theta)$。

注意:这里存在一个初学者常见的误解,认为Sigmoid函数是逻辑回归独有的。实际上,它是连接线性回归与概率模型的桥梁。理解了这个,你就能明白,逻辑回归可以看作是在用线性回归的模型去拟合数据对数几率(log-odds)的结果。

2.3 决策边界:概率到类别的临门一脚

模型输出了概率,我们怎么得到最终的分类结果呢?这就需要设定一个阈值,通常是0.5。

  • 如果 $h_{\theta}(x) \geq 0.5$,我们预测 $y=1$。
  • 如果 $h_{\theta}(x) < 0.5$,我们预测 $y=0$。

由于 $h_{\theta}(x) \geq 0.5$ 等价于 $\theta^T x \geq 0$,所以这个决策边界实际上是一个线性边界:$\theta^T x = 0$。在二维特征空间里,它就是一条直线;在三维空间里,它是一个平面。这就是为什么逻辑回归本质上是一个线性分类器。它能解决的问题,是那些类别可以通过一条直线(或平面)大致分开的问题。

3. 实战准备:构建你的第一个逻辑回归模型环境

理论聊完了,我们进入实战。假设你现在就在“头歌实训”的代码编辑界面,或者在自己的Jupyter Notebook里,我们一步步来。我以Python生态为例,因为这是目前机器学习实践的主流。

3.1 核心工具库的选择与安装

对于逻辑回归,我们不需要一开始就动用TensorFlow或PyTorch这样的深度学习框架。科学计算的基础库和Scikit-learn就完全足够了。

# 通常使用pip安装,如果你的实训环境已提供,则可跳过 pip install numpy pandas matplotlib scikit-learn
  • NumPy: 所有数值计算的基石。我们的数据、参数向量、矩阵运算全都依赖它。务必熟悉它的数组(ndarray)操作。
  • Pandas: 数据处理的瑞士军刀。用于加载、清洗、探索结构化数据(如CSV文件)。在实训中,数据很可能以DataFrame的形式给你。
  • Matplotlib/Seaborn: 数据可视化。画图查看数据分布、决策边界,是理解模型和调试的必备技能。
  • Scikit-learn: 核心中的核心。它提供了LogisticRegression这个高度优化的现成类。但在实训中,你很可能被要求从零实现,所以我们后续会重点讲手动实现。不过,用它来验证我们手写模型的正确性,是极好的方法。

3.2 数据:逻辑回归的“食粮”

任何模型都离不开数据。实训平台通常会提供一个数据集,比如经典的鸢尾花数据集(Iris)用于多分类,或者乳腺癌数据集(Breast Cancer)用于二分类。我们以二分类为例,因为这是逻辑回归最自然的形式。

import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.datasets import load_breast_cancer # 加载内置的乳腺癌数据集 data = load_breast_cancer() X = data.data # 特征矩阵,形状 (n_samples, n_features) y = data.target # 标签向量,0表示恶性,1表示良性 # 查看数据基本信息 print(f"特征形状: {X.shape}") # 通常是(569, 30) print(f"标签类别分布: {np.bincount(y)}") # 查看两类分别有多少样本 # 划分训练集和测试集(非常重要!) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) print(f"训练集大小: {X_train.shape}, 测试集大小: {X_test.shape}")

注意:random_state参数固定了随机种子,这能确保每次运行代码时,数据集的划分方式是一样的,从而使你的结果可复现。这在做实验和提交作业时至关重要。

3.3 特征工程前传:标准化

逻辑回归的损失函数(我们后面会讲)虽然不像SVM或KNN那样对尺度极度敏感,但进行特征标准化(Standardization)依然是一个好习惯。它能让梯度下降等优化算法收敛得更快、更稳。

标准化通常是指将每个特征减去其均值,再除以其标准差,使得处理后的特征数据符合标准正态分布(均值为0,标准差为1)。

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) # 在训练集上计算均值和标准差,并转换 X_test_scaled = scaler.transform(X_test) # 使用训练集的参数转换测试集,避免数据泄露! # 切记:测试集的标准化必须使用训练集拟合出来的scaler,这是原则性问题。

4. 核心实现:从零手撕逻辑回归算法

这是实训的核心环节。我们将不借助sklearn.linear_model.LogisticRegression,自己实现模型训练和预测的全过程。

4.1 模型定义:Sigmoid与假设函数

首先,我们把核心的Sigmoid函数和假设函数写出来。

def sigmoid(z): """计算Sigmoid函数值。""" # 为了防止数值溢出(当z为很大的负数时,e^z会溢出),我们做一个稳定化处理 # 使用 np.clip 将z限制在一个合理的范围内,或者使用更稳定的实现: # return 1 / (1 + np.exp(-np.clip(z, -250, 250))) return 1 / (1 + np.exp(-z)) def hypothesis(X, theta): """计算逻辑回归的假设函数 h_theta(x) = sigmoid(theta^T * x)。""" # X: (m, n) 特征矩阵,m是样本数,n是特征数(已包含偏置项) # theta: (n, ) 参数向量 z = np.dot(X, theta) # 线性组合 return sigmoid(z)

这里有一个关键细节:偏置项(Intercept)。参数向量$\theta$的第一个元素$\theta_0$通常是偏置项,它对应的特征$x_0$恒等于1。因此,我们需要在特征矩阵X的最前面加上一列1。

def add_intercept(X): """在特征矩阵X前添加一列1,用于偏置项。""" intercept = np.ones((X.shape[0], 1)) return np.hstack((intercept, X)) X_train_with_intercept = add_intercept(X_train_scaled) X_test_with_intercept = add_intercept(X_test_scaled) # 现在 X_train_with_intercept 的形状是 (m, n+1)

4.2 损失函数:交叉熵损失及其推导

逻辑回归不使用线性回归的均方误差(MSE)作为损失函数,因为那会导致损失函数非凸,难以优化。它使用的是交叉熵损失(Cross-Entropy Loss),也叫对数损失(Log Loss)。

对于一个样本$(x^{(i)}, y^{(i)})$,其损失为: $Cost(h_{\theta}(x^{(i)}), y^{(i)}) = -[y^{(i)} \log(h_{\theta}(x^{(i)})) + (1-y^{(i)}) \log(1-h_{\theta}(x^{(i)}))]$

直观理解:

  • 如果真实标签 $y=1$,损失就是 $-\log(h_{\theta}(x))$。预测概率$h_{\theta}(x)$越接近1,损失越接近0;预测概率越接近0,损失会趋向无穷大。这很好,因为预测错了就要重罚。
  • 如果真实标签 $y=0$,损失就是 $-\log(1-h_{\theta}(x))$。预测概率$h_{\theta}(x)$越接近0,损失越接近0;预测概率越接近1,损失趋向无穷大。

对所有训练样本求平均,得到代价函数: $J(\theta) = -\frac{1}{m} \sum_{i=1}^{m} [y^{(i)} \log(h_{\theta}(x^{(i)})) + (1-y^{(i)}) \log(1-h_{\theta}(x^{(i)}))]$

def compute_cost(X, y, theta): """计算交叉熵损失。""" m = len(y) # 样本数量 h = hypothesis(X, theta) # 计算预测概率 # 为了避免log(0)导致数值错误(NaN),给h一个极小的偏移量 epsilon = 1e-15 h = np.clip(h, epsilon, 1 - epsilon) cost = - (1/m) * np.sum(y * np.log(h) + (1 - y) * np.log(1 - h)) return cost

4.3 参数更新:梯度下降算法

我们的目标是找到一组参数$\theta$,使得代价函数$J(\theta)$最小。梯度下降(Gradient Descent)是最常用的方法。其核心思想是:沿着当前点损失函数下降最快的方向(负梯度方向)更新参数。

对代价函数$J(\theta)$求关于参数$\theta_j$的偏导数,经过推导(这里省略推导过程,但建议你亲手推一遍),可以得到一个非常简洁的梯度公式: $\frac{\partial J(\theta)}{\partial \theta_j} = \frac{1}{m} \sum_{i=1}^{m} (h_{\theta}(x^{(i)}) - y^{(i)}) x_j^{(i)}$

用向量形式表示整个梯度向量: $\nabla_{\theta} J(\theta) = \frac{1}{m} X^T (h_{\theta}(X) - y)$

这个形式和线性回归的梯度形式一模一样!这是巧合吗?不是,这是广义线性模型(GLM)性质决定的。有了梯度,参数更新规则为: $\theta := \theta - \alpha \nabla_{\theta} J(\theta)$ 其中,$\alpha$是学习率(Learning Rate),控制每一步更新的幅度。

def gradient_descent(X, y, theta, learning_rate, num_iterations): """使用批量梯度下降优化逻辑回归参数。""" m = len(y) cost_history = [] # 记录每次迭代的损失,用于可视化 for i in range(num_iterations): h = hypothesis(X, theta) # 当前预测概率 gradient = (1/m) * np.dot(X.T, (h - y)) # 计算梯度 theta = theta - learning_rate * gradient # 更新参数 cost = compute_cost(X, y, theta) # 计算当前损失 cost_history.append(cost) # 每100次迭代打印一次损失,方便观察 if i % 100 == 0: print(f"Iteration {i}: Cost = {cost:.6f}") return theta, cost_history

4.4 训练模型与预测

现在,我们把所有部件组装起来,开始训练。

# 初始化参数 theta,通常初始化为0或很小的随机数 np.random.seed(42) # 固定随机种子,保证结果可复现 initial_theta = np.zeros(X_train_with_intercept.shape[1]) # 参数个数 = 特征数 + 1 (偏置) # 设置超参数 learning_rate = 0.01 num_iterations = 1000 # 开始训练! theta_trained, cost_history = gradient_descent( X_train_with_intercept, y_train, initial_theta, learning_rate, num_iterations ) print(f"训练完成,最终参数 theta 的形状: {theta_trained.shape}")

训练完成后,我们可以用学习到的参数进行预测。

def predict(X, theta, threshold=0.5): """根据训练好的参数进行预测。""" probabilities = hypothesis(X, theta) # 计算概率 # 将概率根据阈值转换为类别标签 return (probabilities >= threshold).astype(int) # 在训练集和测试集上进行预测 y_train_pred = predict(X_train_with_intercept, theta_trained) y_test_pred = predict(X_test_with_intercept, theta_trained)

5. 模型评估:你的模型真的“学会”了吗?

模型训练出来,不能光看损失下降就完事了。我们必须用一些客观的指标来评估它在未知数据(测试集)上的表现。这是机器学习工作流中至关重要的一环。

5.1 基础评估指标:准确率、精确率、召回率、F1

  • 准确率 (Accuracy): 预测正确的样本占总样本的比例。这是最直观的指标,但在类别不平衡的数据集上可能会失真。Accuracy = (TP + TN) / (TP + TN + FP + FN)

  • 精确率 (Precision)在所有被模型预测为正类的样本中,真正是正类的比例。它关注的是预测的“准确性”。比如在垃圾邮件检测中,我们关心被判定为垃圾邮件的邮件里,有多少真的是垃圾邮件。Precision = TP / (TP + FP)

  • 召回率 (Recall)在所有真实的正类样本中,被模型正确预测出来的比例。它关注的是模型的“查全率”。比如在疾病筛查中,我们关心所有患病的人里,有多少被成功检测出来了。Recall = TP / (TP + FN)

  • F1分数 (F1-Score): 精确率和召回率的调和平均数。当精确率和召回率都重要,且需要找一个平衡点时,F1分数是一个很好的综合指标。F1 = 2 * (Precision * Recall) / (Precision + Recall)

这里TP、TN、FP、FN分别代表真阳性、真阴性、假阳性、假阴性。

我们可以用Scikit-learn快速计算这些指标,并与我们手写的预测结果对比。

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix print("=== 在测试集上的表现 ===") print(f"准确率: {accuracy_score(y_test, y_test_pred):.4f}") print(f"精确率: {precision_score(y_test, y_test_pred):.4f}") # 默认对正类(1)计算 print(f"召回率: {recall_score(y_test, y_test_pred):.4f}") print(f"F1分数: {f1_score(y_test, y_test_pred):.4f}") # 混淆矩阵能给我们更直观的反馈 cm = confusion_matrix(y_test, y_test_pred) print("混淆矩阵:") print(cm) # 通常格式为: # [[TN FP] # [FN TP]]

5.2 更深入的评估工具:ROC曲线与AUC

对于二分类模型,尤其是输出概率的模型(如逻辑回归),ROC曲线AUC是更强大的评估工具。

  • ROC曲线: 全称是“受试者工作特征曲线”。它以**假正率(False Positive Rate, FPR)**为横轴,**真正率(True Positive Rate, TPR,即召回率)**为纵轴。通过不断改变分类阈值(从1到0),得到一系列(FPR, TPR)点,连起来就是ROC曲线。
  • AUC: 即ROC曲线下的面积。AUC的取值范围在0.5到1之间。
    • AUC = 0.5: 模型没有区分能力,相当于随机猜测。
    • AUC = 1: 模型是完美的分类器。
    • AUC越接近1,模型性能越好。AUC有一个很好的概率解释:随机选取一个正样本和一个负样本,模型对正样本的预测概率高于负样本的概率。
from sklearn.metrics import roc_curve, auc import matplotlib.pyplot as plt # 计算测试集的预测概率(而非类别) y_test_prob = hypothesis(X_test_with_intercept, theta_trained) # 计算ROC曲线的点 fpr, tpr, thresholds = roc_curve(y_test, y_test_prob) roc_auc = auc(fpr, tpr) # 绘制ROC曲线 plt.figure(figsize=(8, 6)) plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.4f})') plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Random Guess') plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.05]) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('Receiver Operating Characteristic (ROC) Curve') plt.legend(loc="lower right") plt.grid(True, alpha=0.3) plt.show()

通过观察ROC曲线和AUC值,你可以更全面地评估模型在不同阈值下的表现,而不仅仅依赖于默认的0.5阈值。

6. 进阶话题与实战避坑指南

当你成功跑通一个基础版本后,接下来就会遇到各种实际问题。这部分内容往往是实训和教程里不会细讲的“坑”。

6.1 学习率与迭代次数:如何设置?

在梯度下降中,学习率learning_rate和迭代次数num_iterations是最关键的超参数。

  • 学习率太大: 损失函数可能会在最小值附近震荡,甚至发散(损失值变得无穷大)。你会看到损失值上下跳动,不收敛。
  • 学习率太小: 收敛速度极慢,需要非常多的迭代次数才能达到最小值,浪费计算资源。

实操建议

  1. 先尝试一个常用的小值,比如0.01、0.001。
  2. 绘制损失函数下降曲线。这是最重要的调试工具!如果曲线平滑下降,说明学习率合适;如果震荡,就调小学习率;如果下降极其缓慢,可以适当调大。
  3. 使用自适应学习率算法。我们实现的是最基础的批量梯度下降(BGD)。在实际中,更常用的是小批量梯度下降(Mini-batch GD)Adam等优化器,它们能自动调整学习率。在Scikit-learn的LogisticRegression中,优化算法是高度优化的,通常不需要我们手动调。
# 绘制损失下降曲线,检查学习率是否合适 plt.figure(figsize=(10, 6)) plt.plot(range(len(cost_history)), cost_history, 'b-', linewidth=2) plt.xlabel('Iteration') plt.ylabel('Cost') plt.title('Gradient Descent: Cost vs. Iteration') plt.grid(True, alpha=0.3) plt.show()

6.2 过拟合与正则化:给模型“刹车”

当模型在训练集上表现很好,但在测试集上表现很差时,很可能发生了过拟合。模型过于复杂,记住了训练数据的噪声,导致泛化能力差。

解决方案:正则化。在逻辑回归的代价函数中加入一个惩罚项,限制参数$\theta$的大小(通常不考虑偏置项$\theta_0$)。

  • L2正则化(岭回归): 惩罚项为 $\frac{\lambda}{2m} \sum_{j=1}^{n} \theta_j^2$。它会让所有参数都趋向于较小的值,但通常不会精确为0。
  • L1正则化(Lasso回归): 惩罚项为 $\frac{\lambda}{m} \sum_{j=1}^{n} |\theta_j|$。它倾向于产生稀疏解,即让一些不重要的特征对应的参数直接变为0,起到特征选择的作用。

加入L2正则化后的代价函数为: $J(\theta) = -\frac{1}{m} \sum_{i=1}^{m} [y^{(i)} \log(h_{\theta}(x^{(i)})) + (1-y^{(i)}) \log(1-h_{\theta}(x^{(i)}))] + \frac{\lambda}{2m} \sum_{j=1}^{n} \theta_j^2$

对应的梯度也需要更新(对$\theta_j, j>0$): $\frac{\partial J(\theta)}{\partial \theta_j} = \frac{1}{m} \sum_{i=1}^{m} (h_{\theta}(x^{(i)}) - y^{(i)}) x_j^{(i)} + \frac{\lambda}{m} \theta_j$

def compute_cost_with_regularization(X, y, theta, lambda_reg): """计算带L2正则化的交叉熵损失。""" m = len(y) h = hypothesis(X, theta) epsilon = 1e-15 h = np.clip(h, epsilon, 1 - epsilon) # 计算基础损失 cost = - (1/m) * np.sum(y * np.log(h) + (1 - y) * np.log(1 - h)) # 加上正则化项(注意:通常不惩罚偏置项 theta[0]) reg_cost = (lambda_reg / (2 * m)) * np.sum(theta[1:] ** 2) return cost + reg_cost def gradient_descent_with_regularization(X, y, theta, learning_rate, num_iterations, lambda_reg): """带L2正则化的梯度下降。""" m = len(y) cost_history = [] for i in range(num_iterations): h = hypothesis(X, theta) gradient = (1/m) * np.dot(X.T, (h - y)) # 对除偏置项外的参数添加正则化梯度 gradient[1:] = gradient[1:] + (lambda_reg / m) * theta[1:] theta = theta - learning_rate * gradient cost = compute_cost_with_regularization(X, y, theta, lambda_reg) cost_history.append(cost) return theta, cost_history

正则化强度$\lambda$是一个超参数,需要通过交叉验证来选择。太大的$\lambda$会导致欠拟合(所有参数都趋近于0,模型变成常数),太小的$\lambda$则起不到防止过拟合的作用。

6.3 类别不平衡问题:当正负样本比例悬殊

在真实数据中,正负样本数量可能相差很大(比如欺诈检测中,正常交易远多于欺诈交易)。此时,准确率这个指标会失效(比如99%的样本都是负类,模型全预测负类也能有99%的准确率,但这毫无意义)。

应对策略

  1. 使用更合适的评估指标: 优先看精确率、召回率、F1分数和AUC,而不是准确率。
  2. 调整分类阈值: 默认0.5的阈值可能不再适用。通过ROC曲线或精确率-召回率曲线(PR Curve),选择一个在业务上更合理的阈值。例如,在疾病筛查中,我们可能更看重召回率(不漏检),愿意承受一定的假阳性(误诊)代价,因此可以降低阈值。
  3. 重采样数据
    • 过采样: 增加少数类样本的副本或生成合成样本(如SMOTE算法)。
    • 欠采样: 随机减少多数类样本的数量。

    注意:过采样和欠采样都要只在训练集上进行,测试集必须保持原始分布以评估真实性能。

  4. 使用类别权重: 大多数机器学习库(包括Scikit-learn)的算法都支持class_weight参数。通过给少数类样本的损失赋予更高的权重,让模型在训练时更关注它们。在逻辑回归中,这相当于修改了损失函数。

在Scikit-learn中,可以轻松实现:

from sklearn.linear_model import LogisticRegression # 使用‘balanced’模式自动根据类别频率调整权重 model = LogisticRegression(class_weight='balanced', max_iter=1000) model.fit(X_train_scaled, y_train) # 或者手动指定权重,例如 {0: 1, 1: 5} 表示类别1的权重是类别0的5倍

6.4 从二分类到多分类:One-vs-Rest策略

逻辑回归本质上是二分类器。那如何解决像鸢尾花数据集(3类)这样的多分类问题呢?最常用的策略是One-vs-Rest (OvR)One-vs-All

原理: 假设有K个类别。我们训练K个独立的二分类逻辑回归模型。对于第i个模型,我们将第i类样本作为正类,其余所有类别的样本作为负类。在预测时,将新样本输入这K个模型,得到K个“属于该类”的概率,最后选择概率最高的那个类别作为最终预测结果。

Scikit-learn的LogisticRegression在设置multi_class='ovr'时(默认值),内部就是采用这种策略。

from sklearn.datasets import load_iris from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split iris = load_iris() X_multi, y_multi = iris.data, iris.target X_train_m, X_test_m, y_train_m, y_test_m = train_test_split(X_multi, y_multi, test_size=0.2, random_state=42) # 标准化 scaler_m = StandardScaler() X_train_m_scaled = scaler_m.fit_transform(X_train_m) X_test_m_scaled = scaler_m.transform(X_test_m) # 使用逻辑回归进行多分类 model_multi = LogisticRegression(multi_class='ovr', max_iter=1000) model_multi.fit(X_train_m_scaled, y_train_m) print("多分类准确率:", model_multi.score(X_test_m_scaled, y_test_m)) # 查看预测的概率,每一行对应一个样本,每一列对应一个类别的概率 print("预测概率示例:\n", model_multi.predict_proba(X_test_m_scaled[:3]))

7. 与Scikit-learn官方实现对比与验证

自己实现一遍后,再用Scikit-learn的官方实现跑一遍,对比结果,是检验自己代码正确性的最好方法,也能学习工业级代码的优化技巧。

from sklearn.linear_model import LogisticRegression as SKLogisticRegression # 使用与我们手写实现相似的无正则化配置(注意:sklearn默认使用L2正则化,这里将C设得很大以减弱其影响) # C是正则化强度的倒数,C越大,正则化越弱。 sk_model = SKLogisticRegression(penalty='l2', C=1e10, solver='lbfgs', max_iter=1000, random_state=42) # 注意:sklearn默认会添加偏置项,且内部可能对数据有标准化等处理,但我们已经标准化过了。 sk_model.fit(X_train_scaled, y_train) # 比较参数 print("=== 参数对比 ===") print(f"手写模型参数 (前5个): {theta_trained[:5]}") print(f"Sklearn模型参数 (coef_): {sk_model.coef_[0][:5]}") print(f"Sklearn模型偏置 (intercept_): {sk_model.intercept_[0]:.6f}") print(f"手写模型偏置 (theta[0]): {theta_trained[0]:.6f}") # 比较预测准确率 y_pred_sk = sk_model.predict(X_test_scaled) print(f"\n=== 性能对比 ===") print(f"手写模型测试集准确率: {accuracy_score(y_test, y_test_pred):.4f}") print(f"Sklearn模型测试集准确率: {accuracy_score(y_test, y_pred_sk):.4f}")

你可能会发现参数值不完全一样,这很正常。原因可能包括:

  1. 优化算法不同: 我们用的是批量梯度下降,而Scikit-learn默认使用更高级的优化器(如lbfgs)。
  2. 收敛标准和迭代次数: 两者的停止条件可能不同。
  3. 数值精度和初始化: 随机初始化和浮点数计算会带来微小差异。 只要准确率、AUC等性能指标非常接近,就说明你的手写实现基本是正确的。

8. 项目总结与延伸思考

走完这一整套流程,你应该对逻辑回归不再感到陌生和畏惧。它不仅仅是一个公式,而是一个包含数据准备、模型定义、损失函数设计、优化算法实现、模型评估、调优避坑的完整项目闭环。在“头歌实训”这类平台上,任务可能只覆盖了其中几个环节,但你自己动手实现全流程,收获是截然不同的。

我个人的体会是,机器学习入门阶段,最大的障碍不是数学,而是将理论映射到代码的工程化思维。比如,理解为什么要给特征矩阵加一列1,为什么要对梯度进行向量化计算以提升效率,为什么测试集标准化必须用训练集的参数。这些细节,才是从“看懂”到“会做”的关键。

最后,关于逻辑回归,还有几个方向值得你继续探索:

  1. 其他优化算法: 尝试实现随机梯度下降(SGD)和小批量梯度下降,并比较它们的收敛速度和效果。
  2. 不同的正则化: 实现L1正则化(Lasso),观察它如何产生稀疏解,并尝试用坐标下降法来求解。
  3. 更复杂的特征: 逻辑回归是线性模型,但可以通过特征工程引入多项式特征、交互项等,使其能拟合非线性决策边界。这在实践中非常有用。
  4. 从广义线性模型(GLM)视角理解: 逻辑回归是广义线性模型的一种,联系指数族分布,这个视角能帮你理解为什么它的损失函数是交叉熵。

逻辑回归作为基石,其思想贯穿了许多更复杂的模型(如神经网络中的单个神经元)。扎扎实实练好它,你未来的机器学习之路会顺畅很多。下次当你再看到“头歌实训-逻辑回归”这样的任务时,你看到的将不再是一行行待填的代码,而是一个可以自由发挥、深入探究的完整项目蓝图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询