逻辑回归实战:用Python手写梯度下降算法(附鸢尾花数据集案例)

在机器学习领域,逻辑回归是一个看似简单却功能强大的分类算法。虽然名字中带有"回归"二字,但它实际上是解决二分类问题的利器。本文将带你从零开始实现逻辑回归算法,通过梯度下降法优化模型参数,并在经典的鸢尾花数据集上进行实战演练。

1. 逻辑回归的核心原理

逻辑回归之所以能用于分类问题,关键在于它使用了Sigmoid函数(也称为逻辑函数)将线性回归的输出映射到(0,1)区间。这个函数的数学表达式为:

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

当我们将线性组合θᵀx输入Sigmoid函数时,输出可以被解释为样本属于正类的概率:

P(y=1|x;θ) = hθ(x) = g(θᵀx) = 1/(1+e^(-θᵀx))

这里的决策边界很简单:如果hθ(x) ≥ 0.5,预测y=1;否则预测y=0。实际上,这个0.5的阈值可以根据具体业务需求调整。

为什么选择Sigmoid函数? 它有几个独特优势:

  • 输出范围在0到1之间,完美契合概率解释
  • 函数处处可导,且导数计算简单:g'(z) = g(z)(1-g(z))
  • 在中点(z=0)变化最陡,对分类边界附近的样本敏感

2. 构建损失函数与梯度下降

与线性回归使用均方误差不同,逻辑回归需要特殊的损失函数。我们使用交叉熵损失(也称为对数损失),其数学形式为:

J(θ) = -1/m Σ [yⁱ log(hθ(xⁱ)) + (1-yⁱ) log(1-hθ(xⁱ))]

这个看似复杂的函数实际上有很好的直观解释:当预测概率接近真实标签时,损失趋近于0;当预测与真实标签相悖时,损失趋近于无穷大。

为了找到最优参数θ,我们使用梯度下降法。计算损失函数对θⱼ的偏导数,得到更新规则:

θⱼ := θⱼ - α ∂J(θ)/∂θⱼ

其中,偏导数的表达式为:

∂J(θ)/∂θⱼ = 1/m Σ (hθ(xⁱ)-yⁱ)xⱼⁱ

这个结果形式简洁,与线性回归的梯度有相似之处,但注意hθ(x)的定义不同。

3. Python实现逻辑回归类

下面我们实现一个完整的逻辑回归类,包含训练和预测功能:

import numpy as np

class LogisticRegression:
    def __init__(self, learning_rate=0.01, n_iterations=1000):
        self.learning_rate = learning_rate
        self.n_iterations = n_iterations
        self.weights = None
        self.bias = None
    
    def fit(self, X, y):
        n_samples, n_features = X.shape
        self.weights = np.zeros(n_features)
        self.bias = 0
        
        # 梯度下降
        for _ in range(self.n_iterations):
            linear_model = np.dot(X, self.weights) + self.bias
            predictions = self._sigmoid(linear_model)
            
            # 计算梯度
            dw = (1/n_samples) * np.dot(X.T, (predictions - y))
            db = (1/n_samples) * np.sum(predictions - y)
            
            # 更新参数
            self.weights -= self.learning_rate * dw
            self.bias -= self.learning_rate * db
    
    def predict(self, X, threshold=0.5):
        linear_model = np.dot(X, self.weights) + self.bias
        probabilities = self._sigmoid(linear_model)
        return [1 if i > threshold else 0 for i in probabilities]
    
    def _sigmoid(self, z):
        return 1 / (1 + np.exp(-z))

这个实现包含了逻辑回归的核心要素:

  • 初始化学习率和迭代次数
  • 使用梯度下降法训练模型
  • 提供预测接口,可调整分类阈值
  • 内部Sigmoid函数实现

4. 鸢尾花数据集实战

让我们用scikit-learn中的鸢尾花数据集测试我们的实现。为简化问题,我们只使用两个类别和两个特征:

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 加载数据
iris = datasets.load_iris()
X = iris.data[:, :2]  # 只取前两个特征
y = (iris.target != 0).astype(int)  # 二分类问题

# 数据预处理
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# 训练模型
model = LogisticRegression(learning_rate=0.1, n_iterations=1000)
model.fit(X_train, y_train)

# 评估
predictions = model.predict(X_test)
accuracy = np.mean(predictions == y_test)
print(f"测试集准确率: {accuracy:.2f}")

在实际运行中,这个简单模型可以达到约90%的准确率。我们可以进一步可视化决策边界:

import matplotlib.pyplot as plt

def plot_decision_boundary(model, X, y):
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01),
                         np.arange(y_min, y_max, 0.01))
    
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = np.array(Z).reshape(xx.shape)
    
    plt.contourf(xx, yy, Z, alpha=0.4)
    plt.scatter(X[:, 0], X[:, 1], c=y, s=20, edgecolor='k')
    plt.xlabel('标准化后的花萼长度')
    plt.ylabel('标准化后的花萼宽度')
    plt.title('逻辑回归决策边界')

plot_decision_boundary(model, X_test, y_test)
plt.show()

5. 调参技巧与常见问题

梯度下降法的性能很大程度上取决于超参数的选择。以下是几个关键调参建议:

学习率选择

  • 太大(如>0.1):可能导致震荡甚至发散
  • 太小(如<0.0001):收敛速度过慢
  • 推荐尝试:0.01、0.03、0.1、0.3等值

迭代次数

  • 观察损失值曲线,当下降不明显时可停止
  • 设置早停机制,当连续几次迭代改进很小时终止

特征缩放

  • 梯度下降对特征尺度敏感
  • 标准化(均值0,方差1)通常效果良好
  • 也可尝试归一化到[0,1]区间

正则化

  • 为防止过拟合,可加入L1或L2正则项
  • 正则化系数λ是另一个需要调节的超参数

常见问题及解决方案:

问题现象 可能原因 解决方案
损失震荡不收敛 学习率太大 减小学习率
收敛速度极慢 学习率太小 增大学习率
所有预测相同 特征与标签无关 检查数据或特征工程
测试集表现差 过拟合 增加正则化或更多数据

6. 进阶优化技巧

基础的批量梯度下降每次迭代都要计算整个数据集的梯度,计算量大。我们可以考虑以下改进:

随机梯度下降(SGD)

def fit_sgd(self, X, y):
    n_samples = X.shape[0]
    for _ in range(self.n_iterations):
        for i in range(n_samples):
            xi = X[i:i+1]  # 保持二维数组结构
            yi = y[i]
            
            linear_model = np.dot(xi, self.weights) + self.bias
            prediction = self._sigmoid(linear_model)
            
            dw = xi.T * (prediction - yi)
            db = prediction - yi
            
            self.weights -= self.learning_rate * dw
            self.bias -= self.learning_rate * db

小批量梯度下降

def fit_minibatch(self, X, y, batch_size=32):
    n_samples = X.shape[0]
    for _ in range(self.n_iterations):
        indices = np.random.permutation(n_samples)
        X_shuffled = X[indices]
        y_shuffled = y[indices]
        
        for i in range(0, n_samples, batch_size):
            X_batch = X_shuffled[i:i+batch_size]
            y_batch = y_shuffled[i:i+batch_size]
            
            linear_model = np.dot(X_batch, self.weights) + self.bias
            predictions = self._sigmoid(linear_model)
            
            dw = (1/batch_size) * np.dot(X_batch.T, (predictions - y_batch))
            db = (1/batch_size) * np.sum(predictions - y_batch)
            
            self.weights -= self.learning_rate * dw
            self.bias -= self.learning_rate * db

不同优化方法对比:

方法 每次迭代计算量 收敛速度 噪声 适合场景
批量GD 高(全数据集) 稳定但慢 小数据集
随机GD 低(单样本) 快但震荡 在线学习
小批量GD 中(批量) 中等 中等 大数据集

7. 多分类扩展与评估指标

虽然逻辑回归本质是二分类器,但可以通过以下策略扩展到多分类:

一对多(One-vs-Rest)

  • 训练K个分类器(K为类别数)
  • 每个分类器区分"本类"vs"其他所有类"
  • 预测时选择概率最高的类别

一对一(One-vs-One)

  • 训练K(K-1)/2个分类器
  • 每个分类器区分两个特定类别
  • 预测时采用投票机制

评估分类性能时,除了准确率,还应考虑:

  • 混淆矩阵:全面展示分类结果
  • 精确率与召回率:特别适用于不平衡数据
  • F1分数:精确率和召回率的调和平均
  • ROC曲线与AUC:评估模型整体区分能力
from sklearn.metrics import classification_report

# 在测试集上评估
predictions = model.predict(X_test)
print(classification_report(y_test, predictions))

在实际项目中,逻辑回归往往作为基线模型,它的优势在于:

  • 训练和预测速度快
  • 模型可解释性强(可以分析特征权重)
  • 输出概率形式,便于后续决策
  • 对线性可分问题效果良好

更多推荐