逻辑回归实战:用Python手写梯度下降算法(附鸢尾花数据集案例)
逻辑回归实战:用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))
在实际项目中,逻辑回归往往作为基线模型,它的优势在于:
- 训练和预测速度快
- 模型可解释性强(可以分析特征权重)
- 输出概率形式,便于后续决策
- 对线性可分问题效果良好
更多推荐



所有评论(0)