逻辑回归详解:从原理到代码实现
深入解析逻辑回归(Logistic Regression),澄清其虽名为“回归”实为分类模型的误解。文章涵盖核心原理(Sigmoid 函数、交叉熵损失、梯度下降)、从零手撕代码实现以及使用 scikit-learn 的工业级应用。同时探讨了多分类策略(OvR/OvO)及常见避坑指南(异常值、特征缩放、正则化)。通过复习时长预测考试通过的案例,展示了逻辑回归在风控、广告等场景的稳定性和可解释性优势。

深入解析逻辑回归(Logistic Regression),澄清其虽名为“回归”实为分类模型的误解。文章涵盖核心原理(Sigmoid 函数、交叉熵损失、梯度下降)、从零手撕代码实现以及使用 scikit-learn 的工业级应用。同时探讨了多分类策略(OvR/OvO)及常见避坑指南(异常值、特征缩放、正则化)。通过复习时长预测考试通过的案例,展示了逻辑回归在风控、广告等场景的稳定性和可解释性优势。

你可能会问:明明名字里带'回归',怎么就成了分类模型?
这是个好问题。我第一次学的时候也困惑了很久。但正是这个看似矛盾的名字,揭示了逻辑回归的核心本质——它用回归的思想,解决了分类的问题。
在工业界,逻辑回归是最常用、最稳定的分类算法之一:
为什么?因为它简单、可解释、高效、稳定。即使在大模型盛行的今天,逻辑回归依然是很多业务系统的首选。
今天,我就带你彻底拆解这个机器学习界最大的'命名误会',从原理到代码,把它吃透。
逻辑回归的名字里确实有'回归',是因为它的核心数学推导过程借用了线性回归的思想。
一句话总结:
它用回归的公式,算出了分类的概率。所以,逻辑回归 = 线性回归 + Sigmoid 函数。
想象你在判断一封邮件是不是垃圾邮件:
5.2 或 -3.1。这代表什么?是垃圾程度吗?很难解释。0.98。这意味着'有 98% 的概率是垃圾邮件'。只要设定一个阈值(通常是 0.5):
这就是逻辑回归做分类的秘诀:先算概率,再定类别。
逻辑回归的训练过程与线性回归非常相似,主要区别在于激活函数和损失函数的不同。
随机初始化权重 w 和偏置 b。
计算线性得分 z,并通过 Sigmoid 激活函数 σ(z) 将其映射为概率值 ŷ(范围在 0 到 1 之间)。
z = w^T x + b
ŷ = σ(z) = 1 / (1 + e^-z)
使用 二元交叉熵损失函数 (Binary Cross-Entropy Loss) 来衡量预测值 ŷ 与真实标签 y 之间的差距。
J(w, b) = -1/m * Σ [y^(i) log(ŷ^(i)) + (1 - y^(i)) log(1 - ŷ^(i))]
计算损失函数对参数的梯度,并使用梯度下降法更新 w 和 b。
梯度计算公式:
∇w J = 1/m * X^T (Ŷ - Y)
∇b J = 1/m * Σ (ŷ^(i) - y^(i))
参数更新公式:
w := w - α · ∇w J
b := b - α · ∇b J
重复 步骤 2 ~ 步骤 4,直到满足停止条件(如:达到最大迭代次数、损失函数不再明显下降或达到预设精度)。
核心区别总结:线性回归 vs 逻辑回归
| 步骤 | 线性回归 (Linear Regression) | 逻辑回归 (Logistic Regression) |
|---|---|---|
| 输出 | 连续数值 | 概率值 (0~1) |
| 激活函数 | 无 (恒等函数) | Sigmoid (1/(1+e^-z)) |
| 损失函数 | 均方误差 (MSE) | 交叉熵损失 (Cross-Entropy) |
| 梯度形式 | 类似,但代入的损失导数不同 | 形式简洁:1/m * X^T(Ŷ-Y) |
你会发现:
除了损失函数和最后加了一层 Sigmoid,其余和线性回归一模一样!
逻辑回归由三个核心部分组成,缺一不可:
这是逻辑回归的灵魂。无论线性部分 z=wx+b 算出多大的数(哪怕是无穷大),Sigmoid 都能把它强行拉回到 (0, 1) 区间。
公式:
σ(z) = 1 / (1 + e^-z)
直观图像:
代码直观理解:
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(z):
return 1 / (1 + np.exp(-z))
z = np.linspace(-10, 10, 100)
plt.plot(z, sigmoid(z))
plt.title("Sigmoid 函数:将任意实数映射到 (0,1)")
plt.xlabel("z (线性输出)")
plt.ylabel("Probability (概率)")
plt.grid(True)
plt.show()
在线性回归中,我们用 MSE(均方误差)。但在逻辑回归中,MSE 不好用(因为 Sigmoid 是非线性的,MSE 会导致损失函数变成非凸函数,容易陷入局部最优)。
逻辑回归使用对数损失(Log Loss),也叫二元交叉熵:
Loss = -1/n * Σ [yi log(ŷi) + (1-yi) log(1-ŷi)]
直观理解:
和线性回归一样,我们要找一组 w 和 b,让交叉熵损失最小。
同样使用梯度下降法迭代更新参数。虽然公式推导略有不同(因为激活函数变了),但核心思想一致:顺着梯度的反方向走。
pip install numpy pandas matplotlib scikit-learn
我们生成一组'考试时长'与'是否通过'的数据。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)
# 生成数据:x=复习小时数,y=是否通过 (0/1)
# 假设复习时间越长,通过概率越大
n_samples = 200
X = np.random.uniform(0, 10, (n_samples, 1)) # 0-10 小时
# 构造一个 Sigmoid 关系的概率
true_z = 0.8 * X - 4 # 真实的线性关系
true_prob = 1 / (1 + np.exp(-true_z))
y = (true_prob > np.random.rand(n_samples, 1)).astype(int) # 根据概率生成 0 或 1
print(f"数据形状:X={X.shape}, y={y.shape}")
print(f"正样本比例:{y.mean():.2f}")
# 可视化原始数据
plt.figure(figsize=(8, 6))
plt.scatter(X, y, alpha=0.6, c=y, cmap='coolwarm', label='样本数据')
plt.xlabel('复习时长 (小时)')
plt.ylabel('是否通过 (0:否,1:是)')
plt.title('复习时长与考试通过率')
plt.yticks([0, 1])
plt.grid(True, alpha=0.3)
plt.show()
运行结果:
数据形状:X=(200, 1), y=(200, 1) 正样本比例:0.49
运行结果分析:
我们不依赖 sklearn,自己实现 Sigmoid、损失计算和梯度下降。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)
# 生成数据:x=复习小时数,y=是否通过 (0/1)
n_samples = 200
X = np.random.uniform(0, 10, (n_samples, 1))
true_z = 0.8 * X - 4
true_prob = 1 / (1 + np.exp(-true_z))
y = (true_prob > np.random.rand(n_samples, 1)).astype(int)
print(f"\n{'='*70}")
print("📌 手撕逻辑回归:从零实现")
print("="*70)
class MyLogisticRegression:
def __init__(self, learning_rate=0.1, n_iterations=1000):
self.lr = learning_rate
self.n_iter = n_iterations
self.w = None
self.b = None
self.loss_history = []
def sigmoid(self, z):
z = np.clip(z, -, )
/ ( + np.exp(-z))
():
m, n = X.shape
.w = np.zeros((n, ))
.b =
i (.n_iter):
z = np.dot(X, .w) + .b
y_pred = .sigmoid(z)
epsilon =
loss = -np.mean(y * np.log(y_pred + epsilon) + ( - y) * np.log( - y_pred + epsilon))
.loss_history.append(loss)
dw = ( / m) * np.dot(X.T, (y_pred - y))
db = ( / m) * np.(y_pred - y)
.w -= .lr * dw
.b -= .lr * db
i % == :
()
():
z = np.dot(X, .w) + .b
.sigmoid(z)
():
proba = .predict_proba(X)
(proba >= threshold).astype()
model_hand = MyLogisticRegression(learning_rate=, n_iterations=)
model_hand.fit(X, y)
y_pred_hand = model_hand.predict(X)
acc_hand = accuracy_score(y, y_pred_hand)
()
()
()
()
x_line = np.linspace(, , ).reshape(-, )
prob_line = model_hand.predict_proba(x_line)
plt.figure(figsize=(, ))
plt.subplot(, , )
plt.scatter(X, y, alpha=, c=y, cmap=)
plt.plot(x_line, prob_line, , linewidth=, label=)
plt.axhline(, color=, linestyle=, label=)
plt.xlabel()
plt.ylabel()
plt.title()
plt.legend()
plt.grid(, alpha=)
plt.subplot(, , )
plt.plot(model_hand.loss_history)
plt.xlabel()
plt.ylabel()
plt.title()
plt.grid(, alpha=)
plt.tight_layout()
plt.show()
运行结果:
数据形状:X=(200, 1), y=(200, 1) 正样本比例:0.49
======================================================================
📌 手撕逻辑回归:从零实现
======================================================================
迭代 0: Loss = 0.6931
迭代 100: Loss = 0.3991
迭代 200: Loss = 0.3940
迭代 300: Loss = 0.3936
迭代 400: Loss = 0.3935
迭代 500: Loss = 0.3935
迭代 600: Loss = 0.3935
迭代 700: Loss = 0.3935
迭代 800: Loss = 0.3935
迭代 900: Loss = 0.3935
手撕模型结果:
权重 w: 0.7430
偏置 b: -3.5882
准确率:0.8250
关键观察:
实际工作中,直接用 sklearn.linear_model.LogisticRegression。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)
# 生成数据:x=复习小时数,y=是否通过 (0/1)
n_samples = 200
X = np.random.uniform(0, 10, (n_samples, 1))
true_z = 0.8 * X - 4
true_prob = 1 / (1 + np.exp(-true_z))
y = (true_prob > np.random.rand(n_samples, 1)).astype(int)
print(f"\n{'='*70}")
print("📌 sklearn 实现:工业级调用")
print("="*70)
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
# 划分数据集(严谨做法)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 创建模型
# solver='lbfgs' 是默认优化算法,适合中小数据集
model_sk = LogisticRegression(solver='lbfgs', max_iter=1000)
model_sk.fit(X_train, y_train)
# 预测
y_pred_sk = model_sk.predict(X_test)
y_prob_sk = model_sk.predict_proba(X_test)[:, 1] # 获取正类的概率
acc_sk = accuracy_score(y_test, y_pred_sk)
()
()
()
()
()
(classification_report(y_test, y_pred_sk, target_names=[, ]))
cm = confusion_matrix(y_test, y_pred_sk)
()
运行结果:
======================================================================
📌 sklearn 实现:工业级调用
======================================================================
sklearn 模型结果:
权重 w: 0.7289
偏置 b: -3.4579
测试集准确率:0.8250
分类报告:
precision recall f1-score support
未通过 0.85 0.81 0.83 21
通过 0.80 0.84 0.82 19
accuracy 0.82 40
macro avg 0.82 0.83 0.82 40
weighted avg 0.83 0.82 0.83 40
混淆矩阵:
[[17 4]
[ 3 16]]
逻辑回归天生是二分类模型(如:是或不是垃圾邮件,是或不是狗等)。那如果有 3 个类别(如:猫、狗、鸟)怎么办?
Sklearn 会自动处理,但其背后有两种策略:
思路:把多分类拆成多个二分类。
预测时:看哪个分类器输出的概率最高,就选哪个。
适用场景:大多数情况,sklearn 默认使用此策略。
思路:每两个类别之间建立一个分类器。
预测时:投票制,谁赢的次数多选谁。
适用场景:类别非常多时计算量太大,一般较少用。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
print("\n" + "="*70)
print("📌 进阶:多分类实战(鸢尾花数据集)")
print("="*70)
# 加载数据
iris = load_iris()
X_iris = iris.data[:, :2] # 只取前两个特征方便可视化
y_iris = iris.target
# 逻辑回归对特征缩放敏感,建议标准化
scaler = StandardScaler()
X_iris_scaled = scaler.fit_transform(X_iris)
# 训练多分类模型
# 【修复点】:移除了 multi_class='ovr' 参数,因为新版 sklearn 已废弃该参数
# 新版默认就是 ovr 策略
model_multi = LogisticRegression(solver='lbfgs', max_iter=1000)
model_multi.fit(X_iris_scaled, y_iris)
print(f"特征名称:{iris.feature_names[:2]}")
print(f"类别:{iris.target_names}")
print(f"模型权重形状:{model_multi.coef_.shape} (3 个分类器,每个 2 个特征)")
# 可视化决策边界
h = 0.02
x_min, x_max = X_iris_scaled[:, ].() - , X_iris_scaled[:, ].() +
y_min, y_max = X_iris_scaled[:, ].() - , X_iris_scaled[:, ].() +
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
Z = model_multi.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.figure(figsize=(, ))
plt.contourf(xx, yy, Z, alpha=, cmap=)
scatter = plt.scatter(X_iris_scaled[:, ], X_iris_scaled[:, ], c=y_iris, edgecolors=, cmap=)
handles, _ = scatter.legend_elements()
labels = [iris.target_names[i] i ((iris.target_names))]
plt.legend(handles, labels, title=)
plt.xlabel(iris.feature_names[] + )
plt.ylabel(iris.feature_names[] + )
plt.title()
plt.show()
()
y_pred = model_multi.predict(X_iris_scaled)
()
(classification_report(y_iris, y_pred, target_names=iris.target_names))
运行结果:
======================================================================
📌 进阶:多分类实战(鸢尾花数据集)
======================================================================
特征名称:['sepal length (cm)', 'sepal width (cm)']
类别:['setosa' 'versicolor' 'virginica']
模型权重形状:(3, 2) (3 个分类器,每个 2 个特征)
多分类准确率:0.8133
分类报告:
precision recall f1-score support
setosa 1.00 0.98 0.99 50
versicolor 0.71 0.74 0.73 50
virginica 0.73 0.72 0.73 50
accuracy 0.81 150
macro avg 0.82 0.81 0.81 150
weighted avg 0.82 0.81 0.81 150
运行结果分析:
A:因为 Sigmoid 函数在两端趋于平缓。如果一个正类样本跑到了负类很远的地方(异常值),模型为了把这个点拉回来(让概率接近 1),会强行调整权重 w 变得非常大,导致整个决策边界扭曲。
解决:使用前必须做好数据清洗和异常值处理。
A:逻辑回归通常使用梯度下降求解。如果特征量纲差异大(如年龄 0-100,收入 0-1000000),损失函数的等高线会变成细长的椭圆,梯度下降会走'之'字形,收敛极慢。
解决:StandardScaler 让所有特征均值为 0,方差为 1,收敛快且稳定。
A:原生不能。它是线性分类器。
解决:可以通过特征工程引入多项式特征(如 x², x₁·x₂),把低维非线性问题映射到高维线性空间。
A:防止过拟合的神器。Sklearn 中的 C 参数就是控制正则化强度的。
C 越小 → 正则化越强 → 模型越简单(防止过拟合)C 越大 → 正则化越弱 → 模型越复杂(可能过拟合)在深度学习大行其道的今天,逻辑回归并没有过时。
记住:
| 项目 | 线性回归 | 逻辑回归 |
|---|---|---|
| 任务 | 回归(连续值) | 分类(0/1 概率) |
| 输出 | 任意实数 | 0~1 概率 |
| 核心函数 | 无 | Sigmoid |
| 损失函数 | MSE | 交叉熵 |
| 优化 | 最小二乘 / 梯度下降 | 梯度下降 |
| 可解释 | 高 | 极高 |
一句话:
线性回归预测'值',逻辑回归预测'类'。
下一步学习建议:
C 参数和 penalty。
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online