跳到主要内容逻辑回归详解:从原理到代码实现 | 极客日志PythonAI算法
逻辑回归详解:从原理到代码实现
综述由AI生成深入解析逻辑回归(Logistic Regression),澄清其虽名为“回归”实为分类模型的误解。文章涵盖核心原理(Sigmoid 函数、交叉熵损失、梯度下降)、从零手撕代码实现以及使用 scikit-learn 的工业级应用。同时探讨了多分类策略(OvR/OvO)及常见避坑指南(异常值、特征缩放、正则化)。通过复习时长预测考试通过的案例,展示了逻辑回归在风控、广告等场景的稳定性和可解释性优势。
观心30 浏览 逻辑回归:别看叫回归,其实是分类
为什么逻辑回归是工业界最稳定的分类模型?
你可能会问:明明名字里带'回归',怎么就成了分类模型?
这是个好问题。我第一次学的时候也困惑了很久。但正是这个看似矛盾的名字,揭示了逻辑回归的核心本质——它用回归的思想,解决了分类的问题。
在工业界,逻辑回归是最常用、最稳定的分类算法之一:
- 风控模型判断是否放贷 → 逻辑回归
- 广告点击率预测 → 逻辑回归
- 患者是否患病 → 逻辑回归
- 垃圾邮件识别 → 逻辑回归
为什么?因为它简单、可解释、高效、稳定。即使在大模型盛行的今天,逻辑回归依然是很多业务系统的首选。
今天,我就带你彻底拆解这个机器学习界最大的'命名误会',从原理到代码,把它吃透。
第一部分:先搞懂:为什么叫'回归'却是'分类'?
1.1 名字的由来:历史的误会
逻辑回归的名字里确实有'回归',是因为它的核心数学推导过程借用了线性回归的思想。
- 线性回归:预测连续值(如房价 y=wx+b)
- 逻辑回归:先算出一个线性值 z=wx+b,然后把这个值压缩到 (0, 1) 之间,代表概率。
一句话总结:
它用回归的公式,算出了分类的概率。所以,逻辑回归 = 线性回归 + Sigmoid 函数。
1.2 核心思想:从'预测数值'到'预测概率'
想象你在判断一封邮件是不是垃圾邮件:
- 线性回归可能会输出:
5.2 或 -3.1。这代表什么?是垃圾程度吗?很难解释。
- 逻辑回归会输出:
0.98。这意味着'有 98% 的概率是垃圾邮件'。
只要设定一个阈值(通常是 0.5):
- 概率 > 0.5 → 判定为 正类(1)(是垃圾邮件)
- 概率 ≤ 0.5 → 判定为 负类(0)(不是垃圾邮件)
这就是逻辑回归做分类的秘诀:先算概率,再定类别。
1.3 训练过程:和线性回归几乎一样
逻辑回归的训练过程与线性回归非常相似,主要区别在于激活函数和损失函数的不同。
第 1 步:初始化参数
随机初始化权重 w 和偏置 b。
- w: 权重向量 (Weights)
- b: 偏置项 (Bias)
第 2 步:前向传播 (Forward Pass)
计算线性得分 z,并通过 Sigmoid 激活函数 σ(z) 将其映射为概率值 ŷ(范围在 0 到 1 之间)。
z = w^T x + b
ŷ = σ(z) = 1 / (1 + e^-z)
- x: 输入特征
- ŷ: 预测为正类(类别 1)的概率
第 3 步:计算损失 (Loss Calculation)
使用 二元交叉熵损失函数 (Binary Cross-Entropy Loss) 来衡量预测值 ŷ 与真实标签 y 之间的差距。
J(w, b) = -1/m * Σ [y^(i) log(ŷ^(i)) + (1 - y^(i)) log(1 - ŷ^(i))]
- m: 样本数量
- y^(i): 第 i 个样本的真实标签 (0 或 1)
第 4 步:反向传播与参数更新 (Backward Pass & Update)
计算损失函数对参数的梯度,并使用梯度下降法更新 w 和 b。
梯度计算公式:
∇w J = 1/m * X^T (Ŷ - Y)
∇b J = 1/m * Σ (ŷ^(i) - y^(i))
参数更新公式:
w := w - α · ∇w J
b := b - α · ∇b J
- α: 学习率 (Learning Rate),控制每次更新的步长。
第 5 步:迭代收敛
重复 步骤 2 ~ 步骤 4,直到满足停止条件(如:达到最大迭代次数、损失函数不再明显下降或达到预设精度)。
| 步骤 | 线性回归 (Linear Regression) | 逻辑回归 (Logistic Regression) |
|---|
| 输出 | 连续数值 | 概率值 (0~1) |
| 激活函数 | 无 (恒等函数) | Sigmoid (1/(1+e^-z)) |
| 损失函数 | 均方误差 (MSE) | 交叉熵损失 (Cross-Entropy) |
| 梯度形式 | 类似,但代入的损失导数不同 | 形式简洁:1/m * X^T(Ŷ-Y) |
你会发现:
除了损失函数和最后加了一层 Sigmoid,其余和线性回归一模一样!
第二部分:核心原理——三个关键组件
2.1 激活函数:Sigmoid(把数值变概率)
这是逻辑回归的灵魂。无论线性部分 z=wx+b 算出多大的数(哪怕是无穷大),Sigmoid 都能把它强行拉回到 (0, 1) 区间。
公式:
σ(z) = 1 / (1 + e^-z)
- 当 z 很大时,σ(z) 趋近于 1
- 当 z 很小时(负无穷),σ(z) 趋近于 0
- 当 z=0 时,σ(z)=0.5(决策边界)
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()
2.2 损失函数:交叉熵(Cross-Entropy)
在线性回归中,我们用 MSE(均方误差)。但在逻辑回归中,MSE 不好用(因为 Sigmoid 是非线性的,MSE 会导致损失函数变成非凸函数,容易陷入局部最优)。
逻辑回归使用对数损失(Log Loss),也叫二元交叉熵:
Loss = -1/n * Σ [yi log(ŷi) + (1-yi) log(1-ŷi)]
- 如果真实标签 y=1,而模型预测 ŷ=0.99(很准),损失很小。
- 如果真实标签 y=1,而模型预测 ŷ=0.01(大错特错),log(0.01) 是很大的负数,加个负号后损失爆炸。
- 惩罚力度极大:你越自信地猜错,惩罚越重。
2.3 优化算法:梯度下降
和线性回归一样,我们要找一组 w 和 b,让交叉熵损失最小。
同样使用梯度下降法迭代更新参数。虽然公式推导略有不同(因为激活函数变了),但核心思想一致:顺着梯度的反方向走。
第三部分:实战演练——从零手撕逻辑回归
环境准备
pip install numpy pandas matplotlib scikit-learn
3.1 第一步:生成二分类模拟数据
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)
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"数据形状: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
- 数据明显分为两簇:复习时间短的多为 0,长的多为 1。
- 中间有一段重叠区域(不确定性区域),这正是概率模型要解决的。
3.2 第二步:手撕逻辑回归(核心代码)
我们不依赖 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)
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, -500, 500)
return 1 / (1 + np.exp(-z))
def fit(self, X, y):
m, n = X.shape
self.w = np.zeros((n, 1))
self.b = 0
for i in range(self.n_iter):
z = np.dot(X, self.w) + self.b
y_pred = self.sigmoid(z)
epsilon = 1e-15
loss = -np.mean(y * np.log(y_pred + epsilon) + (1 - y) * np.log(1 - y_pred + epsilon))
self.loss_history.append(loss)
dw = (1 / m) * np.dot(X.T, (y_pred - y))
db = (1 / m) * np.sum(y_pred - y)
self.w -= self.lr * dw
self.b -= self.lr * db
if i % 100 == 0:
print(f"迭代 {i}: Loss = {loss:.4f}")
def predict_proba(self, X):
z = np.dot(X, self.w) + self.b
return self.sigmoid(z)
def predict(self, X, threshold=0.5):
proba = self.predict_proba(X)
return (proba >= threshold).astype(int)
model_hand = MyLogisticRegression(learning_rate=0.5, n_iterations=1000)
model_hand.fit(X, y)
y_pred_hand = model_hand.predict(X)
acc_hand = accuracy_score(y, y_pred_hand)
print(f"\n手撕模型结果:")
print(f" 权重 w: {model_hand.w[0][0]:.4f}")
print(f" 偏置 b: {model_hand.b:.4f}")
print(f" 准确率:{acc_hand:.4f}")
x_line = np.linspace(0, 10, 100).reshape(-1, 1)
prob_line = model_hand.predict_proba(x_line)
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.scatter(X, y, alpha=0.6, c=y, cmap='coolwarm')
plt.plot(x_line, prob_line, 'r-', linewidth=2, label='预测概率曲线')
plt.axhline(0.5, color='gray', linestyle='--', label='决策阈值 (0.5)')
plt.xlabel('复习时长')
plt.ylabel('概率 / 标签')
plt.title('Sigmoid 概率曲线与决策边界')
plt.legend()
plt.grid(True, alpha=0.3)
plt.subplot(1, 2, 2)
plt.plot(model_hand.loss_history)
plt.xlabel('迭代次数')
plt.ylabel('交叉熵损失')
plt.title('损失函数收敛过程')
plt.grid(True, alpha=0.3)
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
- 损失函数从 1.1(随机猜测)迅速下降到 0.4。
- 红色曲线完美拟合了数据的分布趋势。
- 当复习时长约 5 小时(0.78×5−3.92≈0)时,概率跨过 0.5,成为决策边界。
- 决策边界理解:逻辑回归用一条直线,把两类数据分开。小于边界是一类,否则就是另一类,逻辑回归的主要任务就是找到这个边界。
3.3 第三步:sklearn 实现(工业级标准)
实际工作中,直接用 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)
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)
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)
print(f"sklearn 模型结果:")
print(f" 权重 w: {model_sk.coef_[0][0]:.4f}")
print(f" 偏置 b: {model_sk.intercept_[0]:.4f}")
print(f" 测试集准确率:{acc_sk:.4f}")
print("\n分类报告:")
print(classification_report(y_test, y_pred_sk, target_names=['未通过', '通过']))
cm = confusion_matrix(y_test, y_pred_sk)
print(f"混淆矩阵:\n{cm}")
======================================================================
📌 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]]
第四部分:进阶——多分类问题(OvR vs OvO)
逻辑回归天生是二分类模型(如:是或不是垃圾邮件,是或不是狗等)。那如果有 3 个类别(如:猫、狗、鸟)怎么办?
Sklearn 会自动处理,但其背后有两种策略:
4.1 策略一:One-vs-Rest (OvR) —— 一对多
- 分类器 1:猫 vs (狗 + 鸟)
- 分类器 2:狗 vs (猫 + 鸟)
- 分类器 3:鸟 vs (猫 + 狗)
预测时:看哪个分类器输出的概率最高,就选哪个。
适用场景:大多数情况,sklearn 默认使用此策略。
4.2 策略二:One-vs-One (OvO) —— 一对一
预测时:投票制,谁赢的次数多选谁。
适用场景:类别非常多时计算量太大,一般较少用。
4.3 代码演示:鸢尾花多分类
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)
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[:, 0].min() - 1, X_iris_scaled[:, 0].max() + 1
y_min, y_max = X_iris_scaled[:, 1].min() - 1, X_iris_scaled[:, 1].max() + 1
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=(8, 6))
plt.contourf(xx, yy, Z, alpha=0.3, cmap='viridis')
scatter = plt.scatter(X_iris_scaled[:, 0], X_iris_scaled[:, 1], c=y_iris, edgecolors='k', cmap='viridis')
handles, _ = scatter.legend_elements()
labels = [iris.target_names[i] for i in range(len(iris.target_names))]
plt.legend(handles, labels, title="Classes")
plt.xlabel(iris.feature_names[0] + " (标准化后)")
plt.ylabel(iris.feature_names[1] + " (标准化后)")
plt.title('逻辑回归多分类决策边界 (OvR)')
plt.show()
print(f"多分类准确率:{model_multi.score(X_iris_scaled, y_iris):.4f}")
y_pred = model_multi.predict(X_iris_scaled)
print("\n分类报告:")
print(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
- 图中不同颜色的背景区域代表模型预测的类别。
- 逻辑回归画出的边界是直线(线性边界),这也是它的局限性所在。
第五部分:常见问题与避坑指南
Q1:为什么逻辑回归对异常值敏感?
A:因为 Sigmoid 函数在两端趋于平缓。如果一个正类样本跑到了负类很远的地方(异常值),模型为了把这个点拉回来(让概率接近 1),会强行调整权重 w 变得非常大,导致整个决策边界扭曲。
解决:使用前必须做好数据清洗和异常值处理。
Q2:为什么要做特征缩放(StandardScaler)?
A:逻辑回归通常使用梯度下降求解。如果特征量纲差异大(如年龄 0-100,收入 0-1000000),损失函数的等高线会变成细长的椭圆,梯度下降会走'之'字形,收敛极慢。
解决:StandardScaler 让所有特征均值为 0,方差为 1,收敛快且稳定。
Q3:逻辑回归能处理非线性关系吗?
A:原生不能。它是线性分类器。
解决:可以通过特征工程引入多项式特征(如 x², x₁·x₂),把低维非线性问题映射到高维线性空间。
Q4:正则化(Regularization)是什么?
A:防止过拟合的神器。Sklearn 中的 C 参数就是控制正则化强度的。
C 越小 → 正则化越强 → 模型越简单(防止过拟合)
C 越大 → 正则化越弱 → 模型越复杂(可能过拟合)
写在最后:为什么它依然是王者?
- 在**广告点击率预估(CTR)**中,它是基石。
- 在金融风控中,监管要求模型可解释,逻辑回归是首选。
- 在医疗诊断中,医生需要知道'血压升高 1 个单位,患病概率增加多少',只有逻辑回归能直接给出这个答案。
- 不要因为它简单就轻视它。
- 不要因为它叫'回归'就以为它是做回归的。
- 它是连接线性世界与非线性概率世界的桥梁。
核心要点回顾
- 本质:线性回归 + Sigmoid 函数 = 概率分类。
- 核心公式:P(y=1|x) = 1 / (1 + e^-(wx+b))。
- 损失函数:交叉熵(Cross-Entropy),惩罚'自信的錯誤'。
- 多分类策略:默认 OvR(One-vs-Rest)。
- 必做预处理:特征缩放(StandardScaler),否则收敛慢。
- 工业地位:可解释性强、训练快、基线首选。
逻辑回归和线性回归的对比总结
| 项目 | 线性回归 | 逻辑回归 |
|---|
| 任务 | 回归(连续值) | 分类(0/1 概率) |
| 输出 | 任意实数 | 0~1 概率 |
| 核心函数 | 无 | Sigmoid |
| 损失函数 | MSE | 交叉熵 |
| 优化 | 最小二乘 / 梯度下降 | 梯度下降 |
| 可解释 | 高 | 极高 |
一句话:
线性回归预测'值',逻辑回归预测'类'。
- ✅ 学习 正则化(L1/L2),深入理解
C 参数和 penalty。
- ✅ 对比 支持向量机(SVM),看看非线性边界怎么做。
- ✅ 进入 决策树与随机森林,体验非线性模型的魅力。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- 随机西班牙地址生成器
随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online