共计 3594 个字符,预计需要花费 9 分钟才能阅读完成。
背景与痛点
逻辑回归是机器学习中最基础的二元分类算法,尽管名字里有 ” 回归 ”,但它实际上是通过 Sigmoid 函数将线性回归结果映射到 [0,1] 区间,实现概率预测。很多初学者在使用时直接调用 sklearn 的LogisticRegression,虽然能快速得到结果,但往往对算法原理、参数调优等缺乏深入理解。

鸢尾花数据集是经典的机器学习入门数据集,包含 150 个样本,每个样本有 4 个特征。为了简化问题,我们只使用前两个特征(花萼长度和宽度),并且只保留类别 0 和 1(Setosa 和 Versicolor),这样可以将问题转化为清晰的二维空间可视化问题。
数据预处理
首先我们需要加载数据并进行预处理:
from sklearn.datasets import load_iris
import numpy as np
# 加载数据
iris = load_iris()
X = iris.data[:, :2] # 只取前两个特征
Y = iris.target
# 只保留类别 0 和 1
mask = (Y == 0) | (Y == 1)
X = X[mask]
Y = Y[mask]
# 数据标准化
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
sklearn 实现
使用 sklearn 实现逻辑回归非常简单:
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_scaled, Y, test_size=0.2, random_state=42)
# 创建模型
# penalty 参数指定正则化类型,'l2' 表示 L2 正则化,'none' 表示不使用正则化
model = LogisticRegression(penalty='l2', random_state=42)
# 训练模型
model.fit(X_train, y_train)
# 预测
sklearn_pred = model.predict(X_test)
自定义梯度下降实现
为了更好理解逻辑回归的原理,我们手动实现梯度下降算法:
# 定义 Sigmoid 函数
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# 定义损失函数(对数损失)def compute_loss(y, y_pred):
return -np.mean(y * np.log(y_pred) + (1-y) * np.log(1-y_pred))
# 初始化参数
np.random.seed(42)
theta = np.random.randn(3) # 两个特征加一个偏置项
X_with_bias = np.c_[np.ones((len(X_train), 1)), X_train] # 添加偏置项
# 梯度下降参数
learning_rate = 0.1
n_iterations = 1000
loss_history = []
# 梯度下降
for iteration in range(n_iterations):
z = np.dot(X_with_bias, theta)
y_pred = sigmoid(z)
# 计算梯度
gradients = np.dot(X_with_bias.T, (y_pred - y_train)) / len(y_train)
# 更新参数
theta -= learning_rate * gradients
# 记录损失
loss = compute_loss(y_train, y_pred)
loss_history.append(loss)
# 提前终止条件
if len(loss_history) > 1 and np.abs(loss_history[-1] - loss_history[-2]) < 1e-6:
break
# 预测函数
def predict(X):
X_with_bias = np.c_[np.ones((len(X), 1)), X]
return (sigmoid(np.dot(X_with_bias, theta)) > 0.5).astype(int)
custom_pred = predict(X_test)
模型对比与评估
我们可以通过多个角度来比较两种实现方式:
- 准确率对比
from sklearn.metrics import accuracy_score
print(f"sklearn 准确率: {accuracy_score(y_test, sklearn_pred):.4f}")
print(f"自定义实现准确率: {accuracy_score(y_test, custom_pred):.4f}")
- 决策边界可视化
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
# 定义绘制决策边界的函数
def plot_decision_boundary(X, y, model, title):
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))
if hasattr(model, 'predict'): # sklearn 模型
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
else: # 自定义模型
Z = predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.figure(figsize=(10, 6))
plt.contourf(xx, yy, Z, alpha=0.4, cmap=ListedColormap(['#FFAAAA', '#AAAAFF']))
plt.scatter(X[:, 0], X[:, 1], c=y, s=20, edgecolor='k', cmap=ListedColormap(['#FF0000', '#0000FF']))
plt.title(title)
plt.xlabel('花萼长度')
plt.ylabel('花萼宽度')
plt.show()
plot_decision_boundary(X_test, y_test, model, "sklearn 决策边界")
plot_decision_boundary(X_test, y_test, predict, "自定义实现决策边界")
- 训练过程损失曲线
plt.figure(figsize=(10, 6))
plt.plot(loss_history)
plt.xlabel('迭代次数')
plt.ylabel('损失值')
plt.title('自定义实现的训练损失曲线')
plt.grid(True)
plt.show()
关键知识点解析
- 特征选择的重要性
我们故意只选择了前两个特征,这样可以将数据可视化在二维平面上,便于理解。实际上,加入更多特征通常会提高模型性能,但也会增加复杂度。
-
梯度下降的关键参数
-
学习率(learning_rate): 太大可能导致震荡或发散,太小则收敛缓慢
- 迭代次数(n_iterations): 需要足够多,但也要设置提前终止条件
-
初始化(theta): 随机初始化可以打破对称性
-
正则化选择
sklearn 默认使用 L2 正则化(penalty=’l2′),这有助于防止过拟合,特别是在特征较多的情况下。
避坑指南
- 学习率设置
如果发现损失值波动很大或不降反升,可能是学习率太大。可以尝试减小学习率,如从 0.1 降到 0.01。
- 特征标准化
梯度下降对特征的尺度很敏感,因此我们使用了 StandardScaler 进行标准化。如果不标准化,不同特征的梯度大小差异很大,导致收敛困难。
- 迭代终止条件
除了固定迭代次数,我们还设置了提前终止条件:当损失变化很小时停止训练。这可以避免不必要的计算。
拓展思考
- 挑战题:尝试增加第三个特征(花瓣长度),观察模型性能的变化
- 思考题:为什么 sklearn 默认使用 L2 正则化而不是 L1?
- 进阶题:尝试实现带 L2 正则化的自定义梯度下降
总结
通过这次实验,我们对比了 sklearn 和自定义实现的逻辑回归。sklearn 实现简单快捷,适合快速原型开发;自定义实现虽然麻烦,但能深入理解算法原理。在实际项目中,通常先用 sklearn 快速验证想法,再根据需求考虑自定义实现。
完整的代码和更多可视化结果可以在我的 GitHub 仓库中找到。希望这篇文章能帮助你更好地理解逻辑回归的实现原理!
