1. 逻辑斯蒂回归基础概念
逻辑斯蒂回归(Logistic Regression)是机器学习中最基础也最重要的分类算法之一。虽然名字中带有"回归",但它实际上是一种用于解决二分类问题的线性模型。我第一次接触这个算法时,也曾被它的命名所困惑,直到理解了其背后的数学原理才恍然大悟。
1.1 从线性回归到逻辑斯蒂回归
线性回归模型的形式很简单:y = wx + b。它通过最小化预测值与真实值之间的平方误差来学习参数w和b。但当我们要解决分类问题时,特别是二分类问题(输出为0或1),线性回归就存在几个明显问题:
- 输出范围不受限:线性回归的输出y可以是从负无穷到正无穷的任何值
- 对异常值敏感:由于使用平方误差,离群点会对模型产生很大影响
- 概率解释困难:我们更希望得到样本属于某个类别的概率
逻辑斯蒂回归通过一个简单的变换解决了这些问题 - 引入Sigmoid函数(也叫Logistic函数)。这个函数的数学表达式为:
σ(z) = 1 / (1 + e^(-z))
它将任何实数z映射到(0,1)区间,完美符合概率的定义。在实际应用中,我们通常将z设为线性组合z = wx + b,因此完整的逻辑斯蒂回归模型可以表示为:
P(y=1|x) = σ(wx + b) = 1 / (1 + e^(-(wx + b)))
1.2 Sigmoid函数的特性
Sigmoid函数有几个非常重要的数学特性:
- 输出范围固定在(0,1)之间,适合表示概率
- 函数处处可导,且导数σ'(z) = σ(z)(1-σ(z)),这个特性在反向传播中非常有用
- 函数在z=0时取值为0.5,可以自然作为分类的决策边界
在实际应用中,我们通常将预测概率大于0.5的样本分类为正类(1),小于0.5的分类为负类(0)。这个阈值也可以根据具体应用场景调整,比如在医疗诊断中,我们可能希望更保守一些,将阈值设为0.7等。
注意:虽然Sigmoid函数在逻辑斯蒂回归中很常用,但它并不是唯一的选择。当处理多分类问题时,我们通常会使用Softmax函数作为推广。
2. 模型训练与损失函数
2.1 交叉熵损失函数
在逻辑斯蒂回归中,我们不再使用线性回归中的均方误差(MSE)作为损失函数,而是使用交叉熵损失(Cross-Entropy Loss)。这个选择不是随意的,而是有深刻的数学原理。
对于二分类问题,交叉熵损失的定义为:
L = -[y*log(p) + (1-y)*log(1-p)]
其中y是真实标签(0或1),p是预测概率。这个函数有几个很好的性质:
- 当y=1时,损失为-log(p),预测越接近1损失越小
- 当y=0时,损失为-log(1-p),预测越接近0损失越小
- 对于错误预测的惩罚是急剧增加的(比如当y=1但p接近0时)
从信息论的角度看,交叉熵衡量的是两个概率分布之间的差异。在分类问题中,我们希望模型的预测分布尽可能接近真实的数据分布。
2.2 最大似然估计视角
交叉熵损失实际上等价于对逻辑斯蒂回归模型进行最大似然估计(MLE)。对于一组独立同分布的样本,我们希望找到参数w和b,使得观察到当前数据的概率最大。
似然函数可以写成:
L(w,b) = ∏ p(y_i|x_i;w,b)
取对数后,最大化对数似然就等价于最小化交叉熵损失。这种统计视角帮助我们理解为什么交叉熵是逻辑斯蒂回归的合适选择。
2.3 梯度下降优化
有了损失函数后,我们需要通过优化算法来最小化它。最常见的方法是梯度下降。对于逻辑斯蒂回归,梯度的计算相对简单:
∂L/∂w_j = (p-y)x_j
∂L/∂b = (p-y)
这个简洁的形式使得逻辑斯蒂回归在实际应用中非常高效。在PyTorch中,这些梯度计算都是自动完成的,我们只需要定义前向传播和损失函数即可。
3. PyTorch实现详解
3.1 数据准备
在实现逻辑斯蒂回归前,我们需要准备合适的数据。对于教学目的,我们通常使用人工生成的线性可分数据:
python复制import torch
import torch.nn as nn
# 设置随机种子保证可重复性
torch.manual_seed(42)
# 生成200个样本,每个样本有2个特征
x = torch.randn(200, 2)
# 创建线性决策边界:3x1 + 2x2 - 1 = 0
z = 3 * x[:, 0] + 2 * x[:, 1] - 1
# 通过Sigmoid函数生成概率,然后转换为0/1标签
y_true = (torch.sigmoid(z) > 0.5).float().unsqueeze(1)
这样生成的数据是完美线性可分的,我们的模型应该能够达到接近100%的准确率。在实际项目中,数据通常不会这么理想,但原理是相同的。
3.2 模型定义
在PyTorch中,我们通过继承nn.Module类来定义模型:
python复制class LogisticRegression(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(2, 1) # 输入2维,输出1维
def forward(self, x):
z = self.linear(x)
y_pred = torch.sigmoid(z)
return y_pred
这个实现非常简洁:
- __init__方法中定义了一个全连接层(nn.Linear)
- forward方法实现了前向传播:线性变换+Sigmoid激活
注意:PyTorch提供了两种实现方式:
- 如上面所示,在forward中显式调用torch.sigmoid
- 使用nn.BCEWithLogitsLoss,它内部集成了Sigmoid并且数值计算更稳定
3.3 训练循环
完整的训练过程包括以下几个步骤:
python复制# 实例化模型、损失函数和优化器
model = LogisticRegression()
criterion = nn.BCELoss() # 二分类交叉熵损失
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
# 训练循环
epochs = 100
for epoch in range(epochs):
# 前向传播
y_pred = model(x)
loss = criterion(y_pred, y_true)
# 反向传播和优化
optimizer.zero_grad() # 清除之前的梯度
loss.backward() # 计算梯度
optimizer.step() # 更新参数
# 打印训练进度
if (epoch+1) % 20 == 0:
print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
几个关键点:
- optimizer.zero_grad()是必须的,否则梯度会累积
- loss.backward()自动计算所有参数的梯度
- optimizer.step()根据梯度更新参数
3.4 批量训练与数据加载
上面的例子使用了全量数据进行训练。对于大型数据集,我们通常需要使用小批量训练。PyTorch提供了DataLoader来简化这个过程:
python复制from torch.utils.data import TensorDataset, DataLoader
# 创建Dataset和DataLoader
dataset = TensorDataset(x, y_true)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
# 修改训练循环使用批量数据
for epoch in range(epochs):
for batch_x, batch_y in dataloader:
# 前向传播
batch_pred = model(batch_x)
loss = criterion(batch_pred, batch_y)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 每个epoch后打印进度
if (epoch+1) % 20 == 0:
print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
批量训练的好处包括:
- 内存效率更高
- 可以更频繁地更新模型
- 噪声更大的梯度有时可以帮助逃离局部最优
4. 模型评估与调试
4.1 评估指标
对于二分类问题,准确率是最直观的评估指标:
python复制with torch.no_grad(): # 禁用梯度计算
y_pred_prob = model(x)
y_pred_label = (y_pred_prob > 0.5).float()
accuracy = (y_pred_label == y_true).float().mean()
print(f'Accuracy: {accuracy.item():.4f}')
在实际项目中,我们还需要考虑其他指标:
- 精确率(Precision)
- 召回率(Recall)
- F1分数
- ROC曲线和AUC
4.2 决策边界可视化
理解模型学到的决策边界非常重要。对于二维特征的情况,我们可以很容易地可视化:
python复制import matplotlib.pyplot as plt
import numpy as np
# 创建网格点
h = 0.02
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, h),
np.arange(y_min, y_max, h))
# 预测每个网格点的类别
Z = model(torch.FloatTensor(np.c_[xx.ravel(), yy.ravel()]))
Z = (Z > 0.5).float().view(xx.shape).detach().numpy()
# 绘制决策边界和数据点
plt.contourf(xx, yy, Z, alpha=0.3)
plt.scatter(x[:, 0], x[:, 1], c=y_true.squeeze(), edgecolors='k')
plt.title('Decision Boundary')
plt.show()
4.3 常见问题与解决方案
在实际应用中,可能会遇到以下问题:
-
模型不收敛
- 检查学习率是否合适(太大或太小都会有问题)
- 尝试不同的优化器(如Adam)
- 检查数据是否标准化/归一化
-
过拟合
- 增加L1/L2正则化
- 使用dropout
- 获取更多训练数据
-
类别不平衡
- 使用加权交叉熵损失
- 对少数类过采样或多数类欠采样
- 使用不同的评估指标(如F1分数)
提示:PyTorch中实现L2正则化很简单,只需要在优化器中设置weight_decay参数:
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, weight_decay=1e-4)
5. 高级话题与扩展
5.1 多分类逻辑斯蒂回归
虽然标准的逻辑斯蒂回归是二分类模型,但我们可以通过两种方式扩展到多分类:
-
One-vs-Rest (OvR)
- 训练K个二分类器(K是类别数)
- 每个分类器区分一个类别和其他所有类别
- 预测时选择概率最高的类别
-
Softmax回归(Multinomial Logistic Regression)
- 直接建模多类概率分布
- 使用Softmax函数代替Sigmoid
- 损失函数改为交叉熵损失
PyTorch实现Softmax回归:
python复制class SoftmaxRegression(nn.Module):
def __init__(self, input_dim, output_dim):
super().__init__()
self.linear = nn.Linear(input_dim, output_dim)
def forward(self, x):
return torch.softmax(self.linear(x), dim=1)
model = SoftmaxRegression(input_dim=4, output_dim=3)
criterion = nn.CrossEntropyLoss() # 已经包含了Softmax
5.2 逻辑斯蒂回归的局限性
虽然逻辑斯蒂回归简单有效,但它有一些固有的限制:
-
线性决策边界
- 只能学习线性可分的数据模式
- 对于非线性问题表现不佳
-
特征交互
- 无法自动学习特征间的高阶交互
- 需要手动构造交互特征
-
大数据集
- 对于非常大的数据集,可能不如更复杂的模型表现好
对于这些问题,我们可以:
- 使用核方法引入非线性
- 转向神经网络等更复杂的模型
- 精心设计特征工程
5.3 逻辑斯蒂回归的实际应用
尽管简单,逻辑斯蒂回归在工业界仍有广泛应用:
-
信用评分
- 预测贷款违约概率
- 可解释性强,符合监管要求
-
医疗诊断
- 预测疾病风险
- 可以结合临床指标进行解释
-
广告点击率预测
- 预测用户点击广告的概率
- 可以处理大规模稀疏特征
在实际项目中,逻辑斯蒂回归常常作为基线模型,即使后续会使用更复杂的模型,它也能提供有价值的参考。