逻辑回归:ML-From-Scratch二分类问题实战
引言:你还在为二分类问题烦恼吗?一文掌握从原理到实现的完整路径
在机器学习领域,二分类问题(Binary Classification)是最基础也最常见的任务之一。无论是垃圾邮件检测、欺诈交易识别,还是疾病诊断,都离不开高效可靠的二分类算法。你是否也曾面临以下困境:
- 调用Scikit-learn时只能调参却不懂内部原理?
- 面试中被问及逻辑回归推导时支支吾吾?
- 想自己实现算法却被数学公式吓退?
本文将通过ML-From-Scratch项目,带你从零开始理解并实现逻辑回归算法,彻底解决这些痛点。读完本文后,你将获得:
✅ 逻辑回归的数学原理与推导过程
✅ 纯NumPy实现逻辑回归的核心代码
✅ 完整的二分类项目实战案例(含数据预处理与模型评估)
✅ 梯度下降与批量优化的对比分析
✅ 可视化工具使用技巧
逻辑回归原理:从线性回归到概率预测的华丽转身
1. 什么是逻辑回归?
逻辑回归(Logistic Regression)虽然名称中带有"回归"二字,却是一种强大的二分类算法。它通过Sigmoid函数将线性回归的输出映射到[0,1]区间,从而实现对事件概率的预测。
1.1 Sigmoid函数:概率的桥梁
Sigmoid函数(也称为逻辑函数)是逻辑回归的核心,其数学表达式为:
\sigma(z) = \frac{1}{1 + e^{-z}}
其中,$z = w^T x + b$ 是线性回归的输出。Sigmoid函数具有以下特性:
- 输出范围严格限制在[0,1]之间
- 在z=0处的函数值为0.5
- 导数为 $\sigma'(z) = \sigma(z)(1 - \sigma(z))$,计算高效
# Sigmoid函数的NumPy实现
import numpy as np
def sigmoid(z):
return 1 / (1 + np.exp(-z))
1.2 决策边界:分类的分水岭
当Sigmoid函数输出大于0.5时,我们将样本分类为正类(1),否则为负类(0)。这对应于:
\begin{cases}
y=1 & \text{if } w^T x + b \geq 0 \\
y=0 & \text{if } w^T x + b < 0
\end{cases}
这个边界称为决策边界(Decision Boundary),可以是线性或非线性的,取决于特征的选择。
2. 损失函数:衡量模型的好坏
逻辑回归采用交叉熵损失(Cross-Entropy Loss)作为损失函数,定义如下:
J(w) = -\frac{1}{m} \sum_{i=1}^{m} [y^{(i)} \log(h_w(x^{(i)})) + (1-y^{(i)}) \log(1-h_w(x^{(i)}))]
其中,$h_w(x) = \sigma(w^T x + b)$ 是模型的预测值。
2.1 为什么不使用均方误差?
对比均方误差(MSE)和交叉熵损失的梯度:
| 损失函数 | 表达式 | 梯度 | 优点 |
|---|---|---|---|
| 均方误差 | $J(w) = \frac{1}{2m} \sum (y - h_w(x))^2$ | $\nabla J(w) = (h_w(x) - y) \cdot h_w(x) \cdot (1 - h_w(x)) \cdot x$ | 数学简单,适用于回归问题 |
| 交叉熵 | $J(w) = -\sum [y \log(h) + (1-y) \log(1-h)]$ | $\nabla J(w) = (h_w(x) - y) \cdot x$ | 梯度更大,收敛更快,适用于分类问题 |
交叉熵损失在模型预测错误较大时提供更大的梯度,加速模型收敛,这是它优于均方误差的关键原因。
3. 参数优化:寻找最优解的两种途径
3.1 梯度下降法(Gradient Descent)
梯度下降是一种迭代优化算法,通过不断沿着梯度的反方向更新参数来最小化损失函数。逻辑回归的梯度下降更新公式为:
w := w - \alpha \nabla J(w) = w - \alpha \frac{1}{m} X^T (h_w(X) - y)
其中,$\alpha$ 是学习率(Learning Rate),控制每次更新的步长。
3.2 批量优化(Batch Optimization)
除了梯度下降,ML-From-Scratch还提供了批量优化方法,通过解正规方程组直接求解最优参数:
w = (X^T \cdot diag(g) \cdot X)^{-1} \cdot X^T \cdot (diag(g) \cdot X \cdot w + y - h)
其中,$diag(g)$ 是Sigmoid函数梯度的对角矩阵。
ML-From-Scratch实现:纯NumPy构建逻辑回归
1. 核心代码解析
ML-From-Scratch的逻辑回归实现位于mlfromscratch/supervised_learning/logistic_regression.py,核心代码如下:
from __future__ import print_function, division
import numpy as np
import math
from mlfromscratch.utils import make_diagonal, Plot
from mlfromscratch.deep_learning.activation_functions import Sigmoid
class LogisticRegression():
""" Logistic Regression classifier.
Parameters:
-----------
learning_rate: float
The step length that will be taken when following the negative gradient during
training.
gradient_descent: boolean
True or false depending if gradient descent should be used when training. If
false then we use batch optimization by least squares.
"""
def __init__(self, learning_rate=.1, gradient_descent=True):
self.param = None
self.learning_rate = learning_rate
self.gradient_descent = gradient_descent
self.sigmoid = Sigmoid()
def _initialize_parameters(self, X):
n_features = np.shape(X)[1]
# Initialize parameters between [-1/sqrt(N), 1/sqrt(N)]
limit = 1 / math.sqrt(n_features)
self.param = np.random.uniform(-limit, limit, (n_features,))
def fit(self, X, y, n_iterations=4000):
self._initialize_parameters(X)
# Tune parameters for n iterations
for i in range(n_iterations):
# Make a new prediction
y_pred = self.sigmoid(X.dot(self.param))
if self.gradient_descent:
# Move against the gradient of the loss function with
# respect to the parameters to minimize the loss
self.param -= self.learning_rate * -(y - y_pred).dot(X)
else:
# Make a diagonal matrix of the sigmoid gradient column vector
diag_gradient = make_diagonal(self.sigmoid.gradient(X.dot(self.param)))
# Batch opt:
self.param = np.linalg.pinv(X.T.dot(diag_gradient).dot(X)).dot(X.T).dot(diag_gradient.dot(X).dot(self.param) + y - y_pred)
def predict(self, X):
y_pred = np.round(self.sigmoid(X.dot(self.param))).astype(int)
return y_pred
代码解析:
- 初始化方法:设置学习率和优化方式(梯度下降/批量优化)
- 参数初始化:在[-1/√n, 1/√n]范围内随机初始化参数,避免梯度消失
- 拟合方法:
- 梯度下降:通过预测值与真实值的误差更新参数
- 批量优化:使用矩阵运算直接求解最优参数
- 预测方法:应用Sigmoid函数并四舍五入得到类别标签
2. 关键组件解析
2.1 Sigmoid类实现
class Sigmoid():
"""Sigmoid activation function."""
def __call__(self, x):
return 1 / (1 + np.exp(-x))
def gradient(self, x):
return self.__call__(x) * (1 - self.__call__(x))
Sigmoid类实现了函数本身及其导数,为反向传播提供支持。
2.2 参数初始化技巧
limit = 1 / math.sqrt(n_features)
self.param = np.random.uniform(-limit, limit, (n_features,))
这种初始化方式可以确保前向传播的输出值不会过大或过小,有助于保持数值稳定性。
实战案例:鸢尾花数据集二分类
1. 完整代码实现
下面我们使用鸢尾花(Iris)数据集来演示逻辑回归的完整应用流程:
from __future__ import print_function
from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt
# Import helper functions
from mlfromscratch.utils import make_diagonal, normalize, train_test_split, accuracy_score
from mlfromscratch.deep_learning.activation_functions import Sigmoid
from mlfromscratch.utils import Plot
from mlfromscratch.supervised_learning import LogisticRegression
def main():
# Load dataset
data = datasets.load_iris()
X = normalize(data.data[data.target != 0])
y = data.target[data.target != 0]
y[y == 1] = 0
y[y == 2] = 1
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, seed=1)
clf = LogisticRegression(gradient_descent=True)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print ("Accuracy:", accuracy)
# Reduce dimension to two using PCA and plot the results
Plot().plot_in_2d(X_test, y_pred, title="Logistic Regression", accuracy=accuracy)
if __name__ == "__main__":
main()
2. 步骤分解:从数据到模型
2.1 数据准备与预处理
# 加载数据集
data = datasets.load_iris()
# 选择后两个类别(二分类)
X = normalize(data.data[data.target != 0])
y = data.target[data.target != 0]
# 标签转换:将1和2转换为0和1
y[y == 1] = 0
y[y == 2] = 1
鸢尾花数据集原本有3个类别,我们通过简单的索引操作将其转换为二分类问题。
2.2 数据标准化
def normalize(X, axis=-1, order=2):
"""Normalize the dataset (unit norm)"""
l2 = np.atleast_1d(np.linalg.norm(X, order, axis))
l2[l2 == 0] = 1
return X / np.expand_dims(l2, axis)
标准化可以加速梯度下降的收敛速度,是逻辑回归中重要的预处理步骤。
2.3 数据集拆分
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, seed=1)
将数据集按67:33的比例拆分为训练集和测试集,确保模型评估的客观性。
2.4 模型训练与预测
clf = LogisticRegression(gradient_descent=True)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
创建逻辑回归分类器实例,使用梯度下降法进行训练,并对测试集进行预测。
2.5 模型评估
accuracy = accuracy_score(y_test, y_pred)
print ("Accuracy:", accuracy)
准确率(Accuracy)是最简单的分类评估指标,计算公式为:
\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}
其中TP、TN、FP、FN分别表示真正例、真负例、假正例和假负例。
3. 可视化结果
ML-From-Scratch提供了Plot类,可以将高维数据降维到2D空间并可视化分类结果:
Plot().plot_in_2d(X_test, y_pred, title="Logistic Regression", accuracy=accuracy)
可视化原理:主成分分析(PCA)
def plot_in_2d(self, X, y=None, title=None, accuracy=None, legend_labels=None):
""" Plot the dataset X and the corresponding labels y in 2D using PCA. """
# PCA dimensionality reduction to 2D
pca = PCA(n_components=2)
X_transformed = pca.fit_transform(X)
# Create scatter plot
plt.figure()
if y is None:
plt.scatter(X_transformed[:, 0], X_transformed[:, 1], alpha=0.5)
else:
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
for i, label in enumerate(np.unique(y)):
plt.scatter(X_transformed[y == label, 0],
X_transformed[y == label, 1],
alpha=0.8,
c=colors[i],
label=legend_labels[label] if legend_labels else label)
if accuracy is not None:
plt.suptitle(title)
plt.title("Accuracy: %.2f" % accuracy, fontsize=10)
plt.legend()
plt.show()
通过PCA将4维特征降维到2D空间,我们可以直观地看到模型的分类效果。
进阶技巧:优化逻辑回归性能的关键策略
1. 梯度下降 vs 批量优化:如何选择?
| 优化方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 梯度下降 | 内存占用小,可在线学习 | 需要调学习率,收敛慢 | 大数据集,在线学习 |
| 批量优化 | 无需调参,一步到位 | 计算复杂度高,内存消耗大 | 小数据集,离线学习 |
在ML-From-Scratch实现中,通过gradient_descent参数可以轻松切换两种优化方式:
# 梯度下降
clf_gd = LogisticRegression(gradient_descent=True, learning_rate=0.1)
# 批量优化
clf_batch = LogisticRegression(gradient_descent=False)
2. 学习率选择:收敛的关键
学习率(Learning Rate)是梯度下降中最重要的超参数,直接影响模型的收敛速度和效果:
- 学习率太小:收敛太慢,需要更多迭代次数
- 学习率太大:可能导致不收敛,在最优解附近震荡
学习率调整策略:
# 尝试不同学习率
learning_rates = [0.001, 0.01, 0.1, 0.5]
accuracies = []
for lr in learning_rates:
clf = LogisticRegression(gradient_descent=True, learning_rate=lr)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
accuracies.append(accuracy_score(y_test, y_pred))
# 绘制学习率-准确率曲线
plt.plot(learning_rates, accuracies, marker='o')
plt.xlabel('Learning Rate')
plt.ylabel('Accuracy')
plt.xscale('log') # 对数刻度更适合展示学习率
plt.title('Learning Rate vs Accuracy')
plt.show()
通过这种方式,我们可以找到最优的学习率。在鸢尾花数据集上,0.1通常是一个不错的起点。
3. 特征工程:提升模型性能的利器
逻辑回归本身是线性模型,但通过特征工程可以处理非线性关系:
3.1 多项式特征
def polynomial_features(X, degree):
"""
Create polynomial features for the input data.
Parameters:
-----------
X : array-like, shape [n_samples, n_features]
The input data.
degree : int
The degree of the polynomial features.
Returns:
--------
X_poly : array-like, shape [n_samples, n_features * degree]
The polynomial features.
"""
n_samples, n_features = np.shape(X)
# Create new array with polynomial features
X_poly = np.zeros((n_samples, n_features * degree))
# Fill the new array with polynomial features
for i in range(n_samples):
for d in range(1, degree + 1):
for j in range(n_features):
X_poly[i, (d-1)*n_features + j] = X[i, j] ** d
return X_poly
通过添加多项式特征,逻辑回归可以拟合非线性决策边界。
3.2 正则化:防止过拟合的有效手段
过拟合(Overfitting)是机器学习中的常见问题,正则化是解决这一问题的有效手段。ML-From-Scratch虽然没有直接实现正则化,但我们可以轻松扩展:
def fit(self, X, y, n_iterations=4000, lambda_=0.01):
self._initialize_parameters(X)
for i in range(n_iterations):
y_pred = self.sigmoid(X.dot(self.param))
if self.gradient_descent:
# 添加L2正则化项
regularization = lambda_ * self.param
self.param -= self.learning_rate * (-(y - y_pred).dot(X) + regularization)
# ... 批量优化类似
正则化通过对参数施加惩罚,防止参数过大,从而提高模型的泛化能力。
项目实践:从零开始搭建逻辑回归分类系统
1. 环境准备
首先,克隆ML-From-Scratch仓库:
git clone https://gitcode.com/GitHub_Trending/ml/ML-From-Scratch
cd ML-From-Scratch
安装必要依赖:
pip install -r requirements.txt
2. 完整工作流实现
# 1. 导入必要的库
import numpy as np
from mlfromscratch.supervised_learning import LogisticRegression
from mlfromscratch.utils import normalize, train_test_split, accuracy_score, Plot
from sklearn import datasets
# 2. 加载并准备数据
data = datasets.load_breast_cancer() # 使用乳腺癌数据集
X = normalize(data.data)
y = data.target
# 3. 拆分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, seed=42)
# 4. 训练模型
clf = LogisticRegression(learning_rate=0.1, gradient_descent=True)
clf.fit(X_train, y_train, n_iterations=10000)
# 5. 预测与评估
y_pred = clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f}")
# 6. 可视化结果
Plot().plot_in_2d(X_test, y_pred, title="Breast Cancer Classification", accuracy=accuracy)
3. 常见问题与解决方案
3.1 模型不收敛怎么办?
- 检查数据是否标准化:逻辑回归对特征尺度敏感
- 调整学习率:尝试减小学习率(如从0.1调整到0.01)
- 增加迭代次数:n_iterations参数调大(如从4000增加到10000)
3.2 准确率不高如何改进?
- 特征工程:添加多项式特征或交互项
- 正则化:添加L1或L2正则化项
- 尝试不同优化方法:切换到批量优化或尝试其他优化器(如牛顿法)
总结与展望:逻辑回归的价值与局限
1. 逻辑回归的优势
- 实现简单,计算高效
- 输出具有概率意义,可解释性强
- 内存占用小,适合大规模数据集
- 可通过特征工程处理非线性问题
2. 逻辑回归的局限性
- 本质是线性模型,难以处理复杂非线性关系
- 对异常值敏感,需要预处理
- 特征相关性高时性能下降,需要特征选择
3. 后续学习路径
- 多分类扩展:了解One-vs-Rest和Softmax回归
- 高级优化器:学习Adam、RMSprop等现代优化算法
- 正则化深入:L1、L2正则化与弹性网络(Elastic Net)
- 模型融合:逻辑回归作为基础模型的集成学习方法
结语:从原理到实践的蜕变
通过本文的学习,你已经掌握了逻辑回归的数学原理、ML-From-Scratch实现细节和完整的二分类项目实战。从推导Sigmoid函数到实现梯度下降,从数据预处理到模型评估可视化,我们完成了一次完整的机器学习实践之旅。
逻辑回归作为最基础的分类算法,不仅是面试高频考点,更是理解更复杂模型(如神经网络)的基石。希望你能将本文所学应用到实际项目中,并继续探索机器学习的精彩世界!
如果觉得本文对你有帮助,请点赞、收藏、关注三连,下期我们将深入探讨决策树算法的原理与实现!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



