Logistic回归:从原理推导到手写实现与Scikit-learn实战

1. 项目概述:从“分类”这个根本问题说起

在机器学习的浩瀚海洋里,我们最先学会游泳的地方,往往是预测一个连续的值,比如房价、销量,这被称为回归。但现实世界充满了选择与判断:这封邮件是不是垃圾邮件?这张图片里有没有猫?这个客户会不会流失?这些问题都有一个共同点——答案不是一串数字,而是一个“是”或“否”,一个“A类”或“B类”。这就是分类问题,而Logistic回归,正是打开分类世界大门的第一把,也是最经典、最实用的一把钥匙。

你可能觉得“回归”这个词用在这里有点奇怪,它明明是个分类算法。这恰恰是它的精妙之处。Logistic回归的核心思想,不是直接去“猜”一个类别,而是去计算一个样本属于某个类别的概率。它继承了线性回归的“骨架”——用特征的加权和来做出预测,但通过一个巧妙的“激活函数”,将无限的线性输出,压缩到了一个0到1的概率区间内。这个函数,就是Sigmoid函数,也叫Logistic函数,算法也因此得名。

所以,当你听到“Logistic回归”时,你应该立刻想到两件事:第一,它是一个用于二分类问题的线性模型;第二,它的输出是一个概率值。理解了这两点,就抓住了它的灵魂。无论是金融风控中的信用评分,医疗诊断中的疾病预测,还是互联网广告的点击率预估,Logistic回归的身影无处不在。它模型简单、可解释性强、计算效率高,在特征工程得当的情况下,其性能往往不输于一些更复杂的模型,是工业界毋庸置疑的“基石型”算法。

接下来,我将彻底拆解Logistic回归。我们不仅会弄懂它的数学原理和公式推导,感受其严谨之美;更会一步步手写代码实现,从零搭建一个可用的模型;最后,再对比如何使用Scikit-learn这样的成熟工具包快速实现。无论你是刚入门的新手,还是想巩固基础的从业者,这篇万字长文都将带你穿透迷雾,真正掌握这个必备算法。

2. 核心原理与公式全解:概率如何被“计算”出来?

要理解Logistic回归,我们必须深入其数学核心。这个过程就像在组装一台精密的仪器,每一步都有其必然的逻辑。

2.1 从线性回归到概率映射:Sigmoid函数的登场

线性回归的公式很简单:z = w^T * x + b。其中,w是权重向量,x是特征向量,b是偏置项。这个z的值域是(-∞, +∞)。对于分类问题,我们希望得到一个介于0和1之间的概率P(y=1|x),表示在给定特征x的条件下,样本属于正类(y=1)的概率。

如何将(-∞, +∞)z映射到(0, 1)区间?我们需要一个理想的函数:它必须是单调可微的,值域为(0,1),并且关于点(0, 0.5)中心对称。Sigmoid函数完美地满足了这些条件:

σ(z) = 1 / (1 + e^{-z})

这个函数形状像一个平滑的“S”型曲线。当z趋向于正无穷时,σ(z)无限接近1;当z趋向于负无穷时,σ(z)无限接近0;当z=0时,σ(z)=0.5。这就实现了我们想要的概率映射:

P(y=1|x) = σ(w^T * x + b) = 1 / (1 + e^{-(w^T * x + b)})

相应地,样本属于负类(y=0)的概率就是:P(y=0|x) = 1 - P(y=1|x)

注意:这里有一个非常重要的实操理解点。z = w^T * x + b这个线性部分,在Logistic回归中常被称为“决策函数”或“logit”。它的正负直接决定了最终的分类结果(以0.5为阈值)。因此,优化Logistic回归模型,本质上就是在学习这个决策边界(一个超平面)的位置和方向。特征工程的好坏,直接决定了这个超平面能否被很好地定义。

2.2 构建损失函数:交叉熵的必然选择

模型有了输出(概率),我们如何衡量这个输出与真实标签之间的差距?在回归问题中,我们常用均方误差(MSE)。但在分类问题,特别是概率输出场景下,MSE并不是最优选择,因为它会导致损失函数非凸,存在许多局部极小点,不利于优化。

从概率论的角度出发,我们追求的是极大似然估计(MLE):找到一组参数(w, b),使得在这组参数下,观测到当前这批训练数据的概率最大。对于单个样本,其似然函数可以写为:

L(w, b) = P(y=1|x)^y * (1 - P(y=1|x))^(1-y)

这个式子很巧妙:当真实标签y=1时,式子变为P(y=1|x);当y=0时,式子变为1 - P(y=1|x)。它统一了两种情况。

对于整个训练集(m个独立同分布的样本),似然函数是每个样本似然的乘积。为了方便计算,我们通常取对数,将连乘变为连加,得到对数似然函数:

ℓ(w, b) = Σ_{i=1}^{m} [y^{(i)} log(P^{(i)}) + (1-y^{(i)}) log(1-P^{(i)})]

我们的目标是最大化ℓ(w, b)。在机器学习中,我们习惯最小化一个损失函数。因此,定义**二元交叉熵损失(Binary Cross-Entropy Loss)**为负的平均对数似然:

J(w, b) = -1/m * Σ_{i=1}^{m} [y^{(i)} log(P^{(i)}) + (1-y^{(i)}) log(1-P^{(i)})]

这就是Logistic回归的损失函数。它衡量了模型预测的概率分布与真实的概率分布(one-hot形式的真实标签)之间的“距离”。

实操心得:理解交叉熵损失至关重要。你可以直观地感受一下:当一个正样本(y=1)被预测为概率P=0.9时,损失项为-log(0.9) ≈ 0.105;如果被错误地预测为P=0.1,损失项则为-log(0.1) ≈ 2.302,惩罚非常大。这种“错的越离谱,惩罚越严厉”的特性,使得交叉熵损失非常适合分类任务。

2.3 参数更新:梯度下降的推导与应用

有了损失函数J(w, b),我们需要通过优化算法(最常用的是梯度下降法)来找到使其最小化的参数wb。这就要求我们求出损失函数关于各个参数的偏导数(梯度)。

让我们来推导一下。令a = σ(z),其中z = w^T * x + b。 首先,Sigmoid函数有一个非常优美的导数性质:σ‘(z) = σ(z) * (1 - σ(z)) = a * (1 - a)

对于单个样本(x, y),其损失为:L = -[y log(a) + (1-y) log(1-a)]。 我们先求损失La的导数:dL/da = -[y/a - (1-y)/(1-a)] = (a - y) / [a(1-a)]

然后,根据链式法则,损失Lz的导数为:dL/dz = (dL/da) * (da/dz) = [(a - y) / (a(1-a))] * [a(1-a)] = a - y

这个结果异常简洁!dL/dz = a - y,即预测值减去真实值。接着,继续求导:dL/dw_j = (dL/dz) * (dz/dw_j) = (a - y) * x_jdL/db = (dL/dz) * (dz/db) = a - y

对于整个训练集,损失函数J的梯度就是所有样本梯度的平均值。因此,在批量梯度下降中,参数的更新公式为:

w_j := w_j - α * (1/m) * Σ_{i=1}^{m} (a^{(i)} - y^{(i)}) * x_j^{(i)}b := b - α * (1/m) * Σ_{i=1}^{m} (a^{(i)} - y^{(i)})

其中,α是学习率,控制着每次更新的步长。

注意事项:这个简洁的梯度形式(a-y)是Logistic回归配合交叉熵损失独有的,也是其计算高效的原因之一。如果你错误地使用了均方误差损失,梯度形式会包含Sigmoid的导数项a(1-a),当预测概率a接近0或1时,该项会变得非常小,导致梯度消失,更新缓慢,这就是为什么MSE不适用于Logistic回归的根本原因。

3. 手写代码实现:从零构建你的第一个分类器

理解了数学原理,最好的巩固方式就是亲手实现它。我们将使用纯Python和NumPy库,不借助任何现成的机器学习框架,完成一个完整的Logistic回归模型。

3.1 核心组件搭建:Sigmoid、初始化、前向传播与损失计算

首先,我们实现几个最基础的函数。

import numpy as np class LogisticRegressionFromScratch: def __init__(self, learning_rate=0.01, n_iters=1000): """ 初始化模型参数。 :param learning_rate: 学习率,控制梯度下降的步长。 :param n_iters: 梯度下降的迭代次数。 """ self.lr = learning_rate self.n_iters = n_iters self.weights = None self.bias = None def _sigmoid(self, z): """Sigmoid激活函数,将输入z映射到(0,1)区间。""" # 为了防止数值溢出(当z为很大的负数时,e^{-z}会非常大), # 使用一个稳定的实现:当z>=0时,用标准公式;当z<0时,变换公式。 # 这里使用更简洁稳定的写法: return 1 / (1 + np.exp(-np.clip(z, -250, 250))) # clip防止数值溢出 def _initialize_parameters(self, n_features): """初始化权重和偏置。""" # 权重初始化为接近0的小随机数,偏置初始化为0。 # 使用np.random.randn生成符合标准正态分布的随机数,乘以0.01使其较小。 self.weights = np.random.randn(n_features) * 0.01 self.bias = 0.0 def _compute_loss(self, y_true, y_pred): """计算二元交叉熵损失。""" # 为了避免log(0)导致数值错误(-inf),给y_pred加一个极小的epsilon。 epsilon = 1e-15 y_pred = np.clip(y_pred, epsilon, 1 - epsilon) # 计算每个样本的损失,然后取平均 loss = -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)) return loss

踩坑记录_sigmoid函数中的数值稳定性是第一个坑。如果直接计算1/(1+np.exp(-z)),当z是一个很大的负数(比如-1000)时,np.exp(-z)会变成一个天文数字,导致溢出。虽然Python的浮点数范围很大,但为了严谨和可移植性,使用np.clip或条件判断是很好的习惯。同样,在计算对数损失时,必须防止log(0)的情况,通过np.clip将概率值限制在[epsilon, 1-epsilon]区间内。

3.2 训练过程实现:梯度下降与参数更新

接下来是模型训练的核心循环,它实现了批量梯度下降。

def fit(self, X, y): """ 使用梯度下降法训练模型。 :param X: 训练特征,形状为 (n_samples, n_features) 的NumPy数组。 :param y: 训练标签,形状为 (n_samples,) 的NumPy数组,取值为0或1。 """ n_samples, n_features = X.shape self._initialize_parameters(n_features) # 记录损失历史,用于可视化 self.loss_history = [] # 梯度下降主循环 for epoch in range(self.n_iters): # 1. 前向传播:计算线性组合和预测概率 linear_model = np.dot(X, self.weights) + self.bias # z = w^T * x + b y_pred = self._sigmoid(linear_model) # a = σ(z) # 2. 计算当前损失 loss = self._compute_loss(y, y_pred) self.loss_history.append(loss) # 3. 反向传播:计算梯度 # 梯度公式:dw = (1/m) * X^T * (A - Y), db = (1/m) * sum(A - Y) dw = (1 / n_samples) * np.dot(X.T, (y_pred - y)) db = (1 / n_samples) * np.sum(y_pred - y) # 4. 更新参数 self.weights -= self.lr * dw self.bias -= self.lr * db # 可选:每100轮打印一次损失 if epoch % 100 == 0: print(f"Epoch {epoch}, Loss: {loss:.4f}") print(f"Training finished. Final loss: {self.loss_history[-1]:.4f}")

实操心得:在fit函数中,向量化操作是关键。np.dot(X.T, (y_pred - y))一次性计算了所有权重的梯度,这比用for循环遍历每个特征要快成百上千倍。这是NumPy编程的核心技巧。另外,记录loss_history非常有用,你可以通过绘制损失曲线来判断训练过程是否收敛(曲线是否平稳下降并趋于平缓),以及学习率设置是否合适(如果损失剧烈震荡,可能学习率太大;如果下降极其缓慢,可能学习率太小)。

3.3 预测与评估:得到类别与概率

模型训练好后,我们需要用它来对新样本进行预测。

def predict_proba(self, X): """预测样本属于正类(y=1)的概率。""" linear_model = np.dot(X, self.weights) + self.bias return self._sigmoid(linear_model) def predict(self, X, threshold=0.5): """ 根据概率阈值进行类别预测。 :param threshold: 分类阈值,默认0.5。大于等于阈值预测为1,否则为0。 :return: 预测的类别标签。 """ probabilities = self.predict_proba(X) # 利用NumPy的布尔索引进行向量化判断 return (probabilities >= threshold).astype(int) def evaluate(self, X, y, threshold=0.5): """在给定数据集上评估模型准确率。""" y_pred = self.predict(X, threshold) accuracy = np.mean(y_pred == y) return accuracy

注意事项predictpredict_proba的区别非常重要。在实际业务中,predict_proba输出的概率值往往比单纯的类别标签更有价值。例如,在风控中,我们不仅想知道一个用户“会不会违约”,更想知道他“违约的可能性有多大”,以便制定不同的风险策略。阈值0.5是一个通用选择,但并非一成不变。在正负样本极不均衡(如欺诈检测)时,可能需要调整阈值来优化查准率或查全率。

3.4 完整示例与测试

让我们用一个简单的人工数据集来测试我们手写的模型。

# 1. 生成模拟数据 from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split # 生成一个简单的二分类数据集,2个特征,方便可视化 X, y = make_classification(n_samples=1000, n_features=2, n_informative=2, n_redundant=0, n_clusters_per_class=1, flip_y=0.05, random_state=42) # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 2. 训练我们的手写模型 print("--- Training Logistic Regression from Scratch ---") model_scratch = LogisticRegressionFromScratch(learning_rate=0.1, n_iters=2000) model_scratch.fit(X_train, y_train) # 3. 评估模型 train_acc = model_scratch.evaluate(X_train, y_train) test_acc = model_scratch.evaluate(X_test, y_test) print(f"Train Accuracy: {train_acc:.4f}") print(f"Test Accuracy: {test_acc:.4f}") # 4. 可视化决策边界 (可选,需要matplotlib) import matplotlib.pyplot as plt def plot_decision_boundary(model, X, y): x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5 y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5 xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02)) Z = model.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) plt.contourf(xx, yy, Z, alpha=0.4, cmap=plt.cm.RdYlBu) plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', cmap=plt.cm.RdYlBu) plt.xlabel('Feature 1') plt.ylabel('Feature 2') plt.title('Decision Boundary of Logistic Regression') plt.show() plot_decision_boundary(model_scratch, X_train, y_train)

运行这段代码,你将看到模型训练过程打印的损失,以及最终的准确率。决策边界图会展示一个线性分界线(因为我们的模型是线性的),直观地告诉你模型是如何划分两个类别的。

4. 使用Scikit-learn调包实现:高效与专业的捷径

虽然手写实现有助于理解,但在实际工作和研究中,我们几乎总是使用成熟的机器学习库,如Scikit-learn。它经过高度优化,功能丰富,且接口统一。

4.1 基础调用与参数解析

使用Scikit-learn实现Logistic回归,简单到只需几行代码。

from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, classification_report, confusion_matrix # 1. 创建模型实例 # 注意:sklearn默认使用L2正则化(参数C的倒数),并采用liblinear等优化器 model_sklearn = LogisticRegression(penalty='l2', # 正则化类型,可选‘l1’, ‘l2’, ‘elasticnet’, ‘none’ C=1.0, # 正则化强度的倒数,C越小,正则化越强 solver='lbfgs', # 优化算法,对于小数据集‘lbfgs’是好的默认选择 max_iter=1000, # 最大迭代次数 random_state=42) # 2. 训练模型(拟合数据) model_sklearn.fit(X_train, y_train) # 3. 进行预测 y_train_pred = model_sklearn.predict(X_train) y_test_pred = model_sklearn.predict(X_test) y_test_proba = model_sklearn.predict_proba(X_test)[:, 1] # 获取正类的概率 # 4. 评估模型 print(f"Sklearn Model Train Accuracy: {accuracy_score(y_train, y_train_pred):.4f}") print(f"Sklearn Model Test Accuracy: {accuracy_score(y_test, y_test_pred):.4f}") print("\n--- Classification Report (Test Set) ---") print(classification_report(y_test, y_test_pred)) print("\n--- Confusion Matrix (Test Set) ---") print(confusion_matrix(y_test, y_test_pred))

Scikit-learn的LogisticRegression类隐藏了所有复杂的实现细节,但通过参数暴露了关键的控制点:

  • penalty:正则化类型。l2(默认)倾向于使所有权重都较小且分布均匀;l1正则化可以产生稀疏权重,相当于进行特征选择。elasticnet是L1和L2的混合。
  • C:正则化强度的倒数。C值越小,正则化越强。这是最重要的超参数之一,需要经常调整。默认1.0。
  • solver:优化算法。对于小数据集或L1正则化,liblinear是个好选择;对于大数据集,sagsaga更快;lbfgs(默认)适用于中小型数据集且支持L2正则化。
  • max_iter:优化算法的最大迭代次数。如果模型没有收敛,可以尝试增大这个值。

重要提示:Scikit-learn在拟合前会自动添加偏置项(fit_intercept=True默认),并且默认会对数据进行标准化吗?不会。它不会自动标准化特征!如果特征尺度差异很大(比如年龄和收入),强烈建议在训练前使用StandardScaler进行标准化,否则可能影响优化器的收敛速度和模型性能。这是新手常踩的一个大坑。

4.2 进阶应用:特征工程与模型评估

调包并不意味着只是简单调用fitpredict。围绕Logistic回归的实战,更多功夫在模型之外。

特征工程:Logistic回归是线性模型,只能学习特征的线性组合。如果特征与目标之间存在非线性关系怎么办?一个常见技巧是特征交叉多项式特征。例如,对于特征x1x2,我们可以手动添加x1*x2x1^2x2^2等项。Scikit-learn的PolynomialFeatures可以自动完成这个工作。

from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import make_pipeline # 创建一个管道:先构造多项式特征,再应用逻辑回归 # 这里生成2次多项式特征(包括x1, x2, x1^2, x2^2, x1*x2) poly_logreg = make_pipeline(PolynomialFeatures(degree=2, include_bias=False), StandardScaler(), LogisticRegression(C=0.1, max_iter=2000)) poly_logreg.fit(X_train, y_train) print(f"Polynomial Features Model Test Acc: {poly_logreg.score(X_test, y_test):.4f}")

模型评估:准确率只是冰山一角。对于分类问题,尤其是类别不平衡时,必须查看更全面的指标。

  • 混淆矩阵:告诉你被正确/错误分类的正负样本具体有多少。
  • 精确率:在所有被预测为正的样本中,真正为正的比例(查得准不准)。
  • 召回率:在所有真实为正的样本中,被正确预测出来的比例(查得全不全)。
  • F1-Score:精确率和召回率的调和平均数,是综合考量。
  • ROC-AUC:不依赖于阈值,衡量模型整体排序能力的指标,非常适合评估概率输出。
from sklearn.metrics import roc_curve, auc import matplotlib.pyplot as plt # 计算ROC曲线 fpr, tpr, thresholds = roc_curve(y_test, y_test_proba) roc_auc = auc(fpr, tpr) # 绘制ROC曲线 plt.figure() plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})') 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.show()

5. 手写实现与调包实现的深度对比与思考

自己实现一遍,再用成熟的库跑一遍,这中间产生的差异和思考,是学习过程中最宝贵的部分。

1. 功能与健壮性

  • 手写实现:我们只实现了最核心的批量梯度下降、二元交叉熵损失和Sigmoid激活。它功能单一,缺乏正则化(容易过拟合)、不支持多分类、没有高级优化器(如L-BFGS、牛顿法)、数值稳定性处理也比较基础。
  • Scikit-learn实现:它是一个工业级实现。支持L1/L2/ElasticNet正则化以控制过拟合;内置‘ovr’(one-vs-rest)和‘multinomial’两种多分类策略;提供了多种优化算法(solver)以适应不同场景和数据规模;在数值计算上做了大量稳定化处理。这是两者最根本的差距。

2. 性能与效率

  • 我们的手写版本使用NumPy向量化,对于小型数据集效率尚可。但对于大规模数据,一次计算所有样本的梯度(批量梯度下降)可能会内存不足,且收敛可能较慢。
  • Scikit-learn的底层由Cython/C++优化,计算速度极快。并且其solver选项中的sag(随机平均梯度下降)和saga支持随机梯度下降的思想,能高效处理海量数据。

3. 可解释性与教学价值

  • 这是手写实现最大的优势。每一行代码都对应着数学公式,梯度如何计算、参数如何更新一目了然。对于理解算法本质,这是无可替代的。
  • Scikit-learn是一个黑盒(当然,其源码是开源的)。虽然方便,但容易让人停留在“调参师”的层面,而不理解内部机理。

给我的启示是学习时“手写”,应用时“调包”。通过手写打通任督二脉,深刻理解算法的假设、局限和每一个细节。在实际项目中,则毫不犹豫地选择Scikit-learn这样的库,站在巨人的肩膀上,利用其稳定性、高效性和丰富功能快速构建可靠的原型或系统。同时,因为理解了底层原理,你才能更好地使用和解释Scikit-learn中的各种参数,知道为什么有时候要调整C,为什么有时候要换solver,而不是盲目地网格搜索。

6. 常见陷阱、实战技巧与扩展方向

即使理解了原理,能跑通代码,在实际应用中还是会遇到各种各样的问题。这里分享一些从实践中总结出的经验。

6.1 数值稳定性与收敛问题

  • 问题:训练时损失变成NaN,或者模型不收敛。
  • 排查与解决
    1. 特征尺度:这是头号杀手。如果特征A的范围是[0, 1],特征B的范围是[0, 10000],那么梯度更新会严重失衡。务必进行特征标准化(StandardScaler)或归一化(MinMaxScaler)。这能极大加快收敛速度并提高模型性能。
    2. 学习率:学习率太大,损失会震荡甚至发散;学习率太小,收敛慢如蜗牛。可以绘制损失曲线观察。一个常见的策略是使用学习率衰减
    3. 初始化:权重初始化过大会导致Sigmoid饱和(梯度接近0),训练不动。我们代码中用* 0.01缩小初始权重是常用技巧。
    4. 迭代次数max_iter设置太小,优化器可能还没找到最优解就停止了。如果损失还在稳步下降,就增加迭代次数。

6.2 过拟合与欠拟合

  • 过拟合迹象:训练集准确率远高于测试集准确率。
  • 应对策略
    • 增加正则化强度:减小Scikit-learn中的C参数(增大惩罚力度)。这是最直接有效的方法。
    • 获取更多数据:数据永远是最好的正则化器。
    • 特征选择:使用L1正则化(penalty='l1'),它倾向于将不重要特征的权重压缩为0,实现自动特征选择。
    • 减少特征数量:手动或通过PCA等降维方法减少特征。
  • 欠拟合迹象:训练集和测试集准确率都很低。
  • 应对策略
    • 减少正则化:增大C值。
    • 增加更多特征:特别是构造有意义的特征交叉项、多项式特征(如前文所示)。
    • 使用更复杂的模型:如果问题本身是非线性的,线性模型(Logistic回归本质是线性分类器)可能能力不足,需要考虑决策树、SVM(带核函数)或神经网络。

6.3 类别不平衡问题

  • 问题:数据中99%是负样本,1%是正样本。即使模型把所有样本都预测为负,也能达到99%的准确率,但这个模型毫无用处。
  • 解决方案
    1. 调整阈值:默认0.5的阈值可能不再适用。可以根据业务需求(如更看重查全率还是查准率),或通过ROC曲线、PR曲线寻找最佳阈值。
    2. 类别权重:Scikit-learn的LogisticRegressionclass_weight参数。设置为‘balanced’,算法会自动根据类别频率调整损失函数中每个类别的权重,让模型更关注少数类。
    3. 重采样:对训练集进行过采样(如SMOTE算法)或欠采样,使类别分布平衡。

6.4 从二分类到多分类

我们的手写实现和默认的Scikit-learn模型(multi_class='ovr')都是针对二分类的。多分类问题有两种主流策略:

  • One-vs-Rest:训练K个二分类器(K为类别数),每个分类器负责区分“本类”和“其他所有类”。预测时,选择概率最高的那个分类器对应的类别。
  • Multinomial(Softmax回归):直接扩展Logistic回归,将Sigmoid函数换成Softmax函数,输出一个K维向量,代表属于每个类别的概率。这是更自然的多分类形式。在Scikit-learn中,设置multi_class='multinomial'即可使用。

手写一个Softmax回归是很好的进阶练习,其损失函数变为交叉熵损失,梯度推导类似但涉及向量和矩阵。

Logistic回归是一个完美的起点,它像一面镜子,映照出机器学习中许多核心概念:从概率框架、损失函数、梯度下降,到过拟合、正则化、评估指标。吃透它,你就为学习更复杂的模型(如神经网络,其单个神经元可以看作一个Logistic回归单元)打下了坚实的基础。记住,在数据科学项目中,永远不要因为它“简单”而轻视它。先用一个简单的逻辑回归模型建立基线,理解你的数据,然后再去尝试更花哨的模型,这才是稳健的实践之道。