逻辑回归实战:从乳腺癌数据集到完整机器学习工作流
1. 项目概述:从数据到诊断的逻辑回归之旅
在机器学习的入门与实战领域,有一个数据集的地位堪比“Hello World”,那就是威斯康星州乳腺癌诊断数据集。它不像鸢尾花数据集那样简单,也不像MNIST那样庞大,但它完美地融合了清晰的业务目标(良/恶性诊断)、适中的数据规模以及典型的二分类问题特性。今天,我们就以这个经典数据集为舞台,深入拆解逻辑回归算法从数据加载、预处理、模型训练到评估优化的完整流程。这不仅仅是跑通一个模型,更是理解机器学习项目标准工作流的一次绝佳实践。无论你是刚学完理论,想找个项目练手的新手,还是希望巩固基础、梳理流程的从业者,这篇基于真实操作记录的分享,都将带你避开那些教程里不会提的“坑”,获得可以直接复现的代码与洞见。
2. 核心思路与数据理解
2.1 为什么选择逻辑回归与乳腺癌数据集?
逻辑回归是处理二分类问题的经典线性模型,其核心优势在于模型简单、可解释性强,并且能直接输出样本属于某一类的概率。对于医疗诊断这类需要一定解释性的场景,逻辑回归的系数可以告诉我们哪些特征对判断“恶性”有正向或负向贡献,这比一个“黑箱”模型更容易获得临床医生的信任。
乳腺癌数据集(通常指 sklearn.datasets.load_breast_cancer )包含了569个样本,每个样本有30个特征,这些特征是从乳腺肿块的数字化图像中计算得出的,例如半径均值、纹理均值、周长均值等。目标变量是二元的:0代表恶性(Malignant),1代表良性(Benign)。这个数据集规模适中,特征均为数值型,且已经过较好的清洗,非常适合用于演示机器学习的基础流程。
注意:虽然数据集本身质量较高,但在实际医疗项目中,数据采集、标注的一致性、伦理审查等都是极其复杂的环节。本项目侧重于机器学习方法论的演练。
2.2 项目整体工作流设计
一个完整的机器学习项目,远不止 model.fit() 和 model.predict() 。我们需要一个系统性的流程来保证结果的可靠性。本次项目的核心工作流设计如下:
- 环境准备与数据加载 :搭建Python环境,导入必要的库,并加载数据。
- 探索性数据分析 :理解数据的基本结构、分布以及特征与目标之间的关系。
- 数据预处理 :包括处理缺失值(本数据集无)、特征缩放、以及划分训练集与测试集。
- 模型训练与调优 :使用逻辑回归模型进行训练,并利用交叉验证和网格搜索寻找最优超参数。
- 模型评估与解释 :在独立的测试集上评估模型性能,并解读模型系数。
- 结果可视化与报告 :将关键结果,如混淆矩阵、ROC曲线、特征重要性等,以图表形式呈现。
这个流程是通用的,可以迁移到大多数监督学习任务中。接下来,我们将深入每个环节的细节。
3. 环境搭建与数据初探
3.1 工具链选择与安装
对于机器学习入门和快速原型开发,Anaconda发行版配合Jupyter Notebook是黄金组合。它集成了Python、科学计算库(如NumPy, Pandas)和机器学习库(如scikit-learn),免去了繁琐的环境配置。
# 假设已安装Anaconda,创建一个新的虚拟环境(可选但推荐)
conda create -n breast_cancer_lr python=3.9
conda activate breast_cancer_lr
# 安装核心库
pip install numpy pandas matplotlib seaborn scikit-learn jupyter
在Jupyter Notebook中,我们首先导入所有必要的库:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (accuracy_score, precision_score, recall_score,
f1_score, confusion_matrix, classification_report,
roc_curve, auc, roc_auc_score)
# 设置图表样式
sns.set_style("whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
%matplotlib inline
3.2 加载数据并转化为DataFrame
直接使用 sklearn 加载的数据是Bunch对象,为了方便使用Pandas进行数据分析,我们将其转换为DataFrame。
# 加载数据集
data = load_breast_cancer()
# 创建特征DataFrame
df = pd.DataFrame(data.data, columns=data.feature_names)
# 添加目标列
df['target'] = data.target
# 查看数据基本信息
print(f"数据集形状: {df.shape}")
print(f"特征示例:\n{df.iloc[:3, :5]}") # 查看前3行,前5列特征
print(f"目标变量分布:\n{df['target'].value_counts()}")
print(f"恶性(0): {df['target'].value_counts()[0]}, 良性(1): {df['target'].value_counts()[1]}")
运行后,你会看到数据有569行,31列(30个特征+1个目标)。目标变量中,良性(1)有357例,恶性(0)有212例。这是一个略微不平衡的数据集,良性样本更多,在后续评估时需要注意不能只看准确率。
3.3 探索性数据分析关键洞察
EDA的目标是“认识你的数据”。我们重点看几个方面:
特征分布 :使用直方图查看特征的分布情况。你会发现,许多特征(如 mean radius , mean area )的分布近似正态,但存在右偏(长尾)现象。这对后续是否进行标准化或更激进的变换(如对数变换)有指导意义。
# 绘制部分特征的分布直方图
fig, axes = plt.subplots(5, 6, figsize=(20, 15)) # 30个特征,分5行6列显示
axes = axes.ravel() # 将二维坐标轴数组展平为一维
for idx, col in enumerate(data.feature_names):
axes[idx].hist(df[col], bins=30, edgecolor='black', alpha=0.7)
axes[idx].set_title(col, fontsize=9)
axes[idx].set_xticks([])
axes[idx].set_yticks([])
plt.tight_layout()
plt.show()
特征与目标的关系 :通过箱线图可以直观看出,良性和恶性样本在许多特征的中位数上有明显差异。例如,恶性肿瘤的 mean radius 、 mean perimeter 、 mean area 通常更大。
# 以‘mean radius’和‘worst texture’为例
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
sns.boxplot(x='target', y='mean radius', data=df, ax=ax1)
ax1.set_xticklabels(['Malignant (0)', 'Benign (1)'])
ax1.set_title('Mean Radius by Diagnosis')
sns.boxplot(x='target', y='worst texture', data=df, ax=ax2)
ax2.set_xticklabels(['Malignant (0)', 'Benign (1)'])
ax2.set_title('Worst Texture by Diagnosis')
plt.show()
特征间相关性 :30个特征并非完全独立。使用热图查看特征间的皮尔逊相关系数。你会发现,许多基于相同度量(如半径、周长、面积)计算出的特征之间高度相关(相关系数接近1)。这提示我们可能存在多重共线性,而逻辑回归对多重共线性比较敏感,可能导致系数估计不稳定。这是后续特征工程需要考虑的问题。
# 计算特征间的相关系数矩阵(仅取部分特征示例,全部30个特征热图会非常密集)
selected_features = ['mean radius', 'mean texture', 'mean perimeter', 'mean area', 'mean smoothness']
corr_matrix = df[selected_features].corr()
plt.figure(figsize=(8,6))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0)
plt.title('Selected Features Correlation Heatmap')
plt.show()
实操心得:在EDA阶段多花时间是值得的。我习惯将关键的分布图、关系图保存下来,并记录观察到的现象(如“特征X存在严重偏态”、“特征A与B高度相关”)。这些笔记是后续做预处理和特征选择决策的重要依据。
4. 数据预处理与特征工程
4.1 训练集与测试集划分
在接触任何模型之前,必须先将数据划分为训练集和测试集。这是评估模型泛化能力的基础。使用 train_test_split 函数,通常保留20%-30%的数据作为测试集。这里我们使用25%,并设置随机种子 random_state 以确保结果可复现。
# 分离特征和目标
X = df.drop('target', axis=1)
y = df['target']
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
print(f"训练集大小: {X_train.shape}")
print(f"测试集大小: {X_test.shape}")
print(f"训练集类别分布:\n{y_train.value_counts(normalize=True)}")
print(f"测试集类别分布:\n{y_test.value_counts(normalize=True)}")
参数 stratify=y 非常重要,它保证了训练集和测试集中良性/恶性的比例与原始数据集一致,避免了因随机划分导致的类别分布偏差。
4.2 特征缩放:为什么以及如何做?
逻辑回归虽然不像SVM或KNN那样对特征尺度极度敏感,但进行特征缩放(标准化)依然是一个好习惯,主要原因有二:
- 加速收敛 :使用梯度下降求解的优化算法(逻辑回归默认的
solver='lbfgs'也基于梯度)在特征尺度一致时收敛更快。 - 公平对待特征 :避免量纲大的特征(如“面积”)主导模型,而量纲小的特征(如“平滑度”)被忽略。
我们使用 StandardScaler 进行Z-score标准化,即让每个特征服从均值为0、标准差为1的标准正态分布。 关键点:拟合器(scaler)只应在训练集上拟合( fit ),然后同时转换( transform )训练集和测试集。绝对不能用测试集的信息来影响预处理过程!
# 初始化标准化器
scaler = StandardScaler()
# 在训练集上拟合,并转换训练集
X_train_scaled = scaler.fit_transform(X_train)
# 用训练集拟合的scaler来转换测试集
X_test_scaled = scaler.transform(X_test)
# 转换回DataFrame便于查看(非必须,模型训练接受数组)
X_train_scaled_df = pd.DataFrame(X_train_scaled, columns=X_train.columns)
X_test_scaled_df = pd.DataFrame(X_test_scaled, columns=X_test.columns)
print("训练集标准化后前5行:\n", X_train_scaled_df.iloc[:5, :5])
4.3 处理多重共线性:特征选择与正则化
在EDA中我们发现了特征间高度相关的问题。处理方式主要有两种:
- 特征选择 :使用统计方法(如方差阈值、基于模型的特征重要性)或降维技术(如PCA)减少特征数量。但PCA会损失特征的可解释性,这与我们使用逻辑回归的初衷之一(可解释性)相悖。
- 正则化 :在逻辑回归模型中加入L1或L2正则化项。L1正则化(Lasso)倾向于产生稀疏解,即自动将一些不重要的特征的系数压缩为0,从而实现特征选择。L2正则化(Ridge)则将所有系数向零收缩,但不一定为0,能稳定系数估计。
对于这个项目,我们将采用 L2正则化 作为默认选项,因为它通常能有效处理共线性且保持所有特征。我们会在模型调优阶段通过交叉验证来确定最佳的正则化强度 C ( C 是正则化强度的倒数, C 越小,正则化越强)。
5. 模型训练、调优与评估
5.1 基础模型训练与评估
首先,我们训练一个未经调优的默认逻辑回归模型,作为性能基线。
# 初始化默认逻辑回归模型
lr_baseline = LogisticRegression(random_state=42, max_iter=1000)
# 在训练集上训练
lr_baseline.fit(X_train_scaled, y_train)
# 在训练集和测试集上预测
y_train_pred = lr_baseline.predict(X_train_scaled)
y_test_pred = lr_baseline.predict(X_test_scaled)
# 计算准确率
train_accuracy = accuracy_score(y_train, y_train_pred)
test_accuracy = accuracy_score(y_test, y_test_pred)
print(f"基线模型 - 训练集准确率: {train_accuracy:.4f}")
print(f"基线模型 - 测试集准确率: {test_accuracy:.4f}")
通常,你会看到一个很高的准确率(可能在95%以上)。但 仅看准确率是危险的 ,尤其是在不平衡数据集上。假设一个模型把所有样本都预测为良性(多数类),它的准确率也有357/569≈62.7%,但这对于诊断恶性疾病是灾难性的。
5.2 全面评估指标与混淆矩阵
我们需要一套更全面的评估指标:
- 精确率 :在所有预测为恶性的样本中,真正是恶性的比例。关注的是预测结果的“准确性”。
- 召回率 :在所有真实为恶性的样本中,被成功预测出来的比例。关注的是模型发现恶性病例的“查全能力”。
- F1-Score :精确率和召回率的调和平均数,是两者的综合考量。
- ROC-AUC :接收者操作特征曲线下的面积,衡量模型在不同阈值下区分两类样本的整体能力,对类别不平衡不敏感。
# 生成测试集的分类报告
print("测试集分类报告:")
print(classification_report(y_test, y_test_pred, target_names=['Malignant', 'Benign']))
# 计算ROC-AUC (需要预测概率,而非类别)
y_test_pred_proba = lr_baseline.predict_proba(X_test_scaled)[:, 1] # 取正类(良性)的概率
roc_auc = roc_auc_score(y_test, y_test_pred_proba)
print(f"测试集ROC-AUC: {roc_auc:.4f}")
# 绘制混淆矩阵
cm = confusion_matrix(y_test, y_test_pred)
plt.figure(figsize=(8,6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['Pred Malignant', 'Pred Benign'],
yticklabels=['True Malignant', 'True Benign'])
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.title('Confusion Matrix - Baseline Model')
plt.show()
分析分类报告,你会看到模型在恶性(0)类别上的召回率可能略低于良性(1)类别。在医疗场景中,我们通常更关注 恶性病例的召回率 (即不漏诊),即使这会牺牲一些精确率(导致部分良性病例被误判为恶性,即假阳性)。这个权衡可以通过调整分类阈值来实现。
5.3 超参数调优:网格搜索与交叉验证
逻辑回归有几个关键超参数:
-
C:正则化强度的倒数。C值越小,正则化越强。默认是1.0。我们需要搜索一个合适的范围,例如[0.001, 0.01, 0.1, 1, 10, 100]。 -
penalty:正则化类型。可以是'l1','l2','elasticnet','none'。注意,不是所有的solver都支持所有的penalty。 -
solver:优化算法。对于小数据集,'lbfgs'是默认的好选择。如果使用L1正则化,则需要选择'liblinear'或'saga'。 -
class_weight:处理类别不平衡。可以设为'balanced',让算法自动调整类别权重,惩罚误判少数类(恶性)的错误。
我们将使用 GridSearchCV 进行网格搜索,并结合5折交叉验证来寻找最优参数组合。
# 定义参数网格
param_grid = {
'C': [0.001, 0.01, 0.1, 1, 10, 100],
'penalty': ['l2'], # 先尝试L2,稳定且快
'solver': ['lbfgs', 'liblinear'],
'class_weight': [None, 'balanced']
}
# 初始化网格搜索对象
# 以roc_auc作为评估指标,因为我们更关注模型整体区分能力
grid_search = GridSearchCV(LogisticRegression(random_state=42, max_iter=5000),
param_grid,
cv=5,
scoring='roc_auc',
n_jobs=-1, # 使用所有CPU核心并行计算
verbose=1)
# 在训练集上执行网格搜索
grid_search.fit(X_train_scaled, y_train)
# 输出最佳参数和最佳得分
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳交叉验证ROC-AUC: {grid_search.best_score_:.4f}")
# 获取最佳模型
best_lr_model = grid_search.best_estimator_
注意事项:
max_iter(最大迭代次数)要设置得足够大,特别是在搜索小C值(强正则化)时,优化过程可能收敛较慢,否则会看到“ConvergenceWarning”警告。这里设为5000以确保收敛。
5.4 最终模型评估与ROC曲线
用得到的最佳模型在测试集上进行最终评估。
# 使用最佳模型进行测试集预测
y_test_pred_best = best_lr_model.predict(X_test_scaled)
y_test_pred_proba_best = best_lr_model.predict_proba(X_test_scaled)[:, 1]
# 评估指标
print("优化后模型 - 测试集分类报告:")
print(classification_report(y_test, y_test_pred_best, target_names=['Malignant', 'Benign']))
test_auc_best = roc_auc_score(y_test, y_test_pred_proba_best)
print(f"优化后模型 - 测试集ROC-AUC: {test_auc_best:.4f}")
# 绘制ROC曲线
fpr, tpr, thresholds = roc_curve(y_test, y_test_pred_proba_best)
roc_auc = auc(fpr, tpr)
plt.figure(figsize=(8,6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Random Guess')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve - Optimized Model')
plt.legend(loc="lower right")
plt.show()
比较优化前后的指标,尤其是恶性类别的召回率和整体的ROC-AUC,你应该能看到提升。ROC曲线越靠近左上角,模型性能越好。
6. 模型解释与业务洞察
逻辑回归最大的优点之一就是可解释性。我们可以查看模型的系数。
# 获取特征系数和截距
coefficients = best_lr_model.coef_[0]
intercept = best_lr_model.intercept_[0]
# 创建系数DataFrame
coef_df = pd.DataFrame({
'Feature': X_train.columns,
'Coefficient': coefficients
})
# 按系数绝对值排序
coef_df['Abs_Coefficient'] = np.abs(coef_df['Coefficient'])
coef_df_sorted = coef_df.sort_values(by='Abs_Coefficient', ascending=False)
print("特征系数(按绝对值排序):")
print(coef_df_sorted.head(10)) # 查看最重要的10个特征
# 可视化最重要的特征系数
top_n = 15
plt.figure(figsize=(10, 8))
colors = ['red' if c < 0 else 'blue' for c in coef_df_sorted['Coefficient'].head(top_n)]
plt.barh(range(top_n), coef_df_sorted['Coefficient'].head(top_n), color=colors)
plt.yticks(range(top_n), coef_df_sorted['Feature'].head(top_n))
plt.xlabel('Coefficient Value')
plt.title(f'Top {top_n} Most Important Features (by coefficient magnitude)')
plt.axvline(x=0, color='black', linestyle='-', linewidth=0.5)
plt.gca().invert_yaxis() # 让最高的在最上面
plt.show()
如何解读?
- 系数为正 :意味着该特征值增大时,样本被预测为**良性(1)**的概率会增大(因为目标变量1代表良性)。例如,如果
mean smoothness(平均平滑度)的系数为正,那么肿块越平滑,模型越倾向于判断为良性,这与医学常识相符。 - 系数为负 :意味着该特征值增大时,样本被预测为**恶性(0)**的概率会增大。例如,
worst area(最差面积)的系数很可能为负,即面积越大,恶性可能性越高。 - 系数绝对值大小 :反映了该特征对预测结果的 影响强度 (在特征已被标准化的前提下)。绝对值越大,影响越大。
通过这个分析,我们可以向医生或领域专家汇报:“我们的模型提示, 肿块的最差面积、最差周长和平均凹度 是判断其是否为恶性的最关键指标。” 这比单纯给出一个预测结果要有价值得多。
7. 常见问题、陷阱与进阶思考
7.1 为什么我的模型过拟合/欠拟合?
- 过拟合迹象 :训练集准确率/ROC-AUC远高于测试集(例如,训练集99%,测试集92%)。可能原因:模型太复杂(正则化太弱,即
C值太大)、特征过多或存在噪声。- 解决 :增强正则化(减小
C),进行特征选择,或收集更多数据。
- 解决 :增强正则化(减小
- 欠拟合迹象 :训练集和测试集准确率都很低且接近。可能原因:模型太简单(正则化过强,即
C值太小)、特征信息不足、或数据本身非线性关系强。- 解决 :减弱正则化(增大
C),增加更多有效特征,或尝试非线性模型(如核SVM、决策树)。
- 解决 :减弱正则化(增大
7.2 类别不平衡如何处理?
本数据集的良性/恶性比例约为1.7:1,属于轻度不平衡。我们采用了 class_weight='balanced' 的选项,它让算法在计算损失时自动给予少数类(恶性)更高的权重。除此之外,还有以下方法:
- 重采样 :
- 过采样 :随机复制少数类样本(如SMOTE算法,生成合成样本)。
- 欠采样 :随机丢弃多数类样本。
- 调整分类阈值 :默认阈值是0.5。我们可以通过ROC曲线或精确率-召回率曲线,选择一个能提高恶性类别召回率的阈值(例如,将阈值降低到0.3,使得模型更“敏感”)。
# 示例:寻找最佳阈值以提高恶性召回率
from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_test, y_test_pred_proba_best, pos_label=0) # pos_label=0 关注恶性类
# 可以绘制精确率-召回率曲线,然后根据业务需求(如要求召回率>95%)确定阈值
7.3 特征工程还能做什么?
我们只做了标准化。在实际项目中,还可以尝试:
- 特征构造 :基于领域知识构造新特征。例如,已有半径、周长、面积,可以构造“紧凑度”(周长^2 / 面积)等形态学特征。
- 特征选择 :使用递归特征消除(RFE)或基于模型(如L1正则化逻辑回归、树模型)的重要性排序,剔除冗余特征,可能提升模型泛化能力。
- 处理非线性 :如果怀疑存在非线性关系,可以对特征进行多项式变换(
PolynomialFeatures),但这会急剧增加特征数量,需谨慎。
7.4 逻辑回归的局限性
逻辑回归本质是线性分类器(决策边界是线性的)。如果数据中的两类样本无法用一个超平面较好地分开,逻辑回归的性能就会受限。这时需要:
- 使用非线性模型(如带核函数的SVM、随机森林、神经网络)。
- 或者,通过特征工程(如上述的多项式变换)将数据映射到更高维空间,使其线性可分。
你可以通过绘制前两个主成分(PCA)的散点图来直观感受数据的线性可分性。
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
X_train_pca = pca.fit_transform(X_train_scaled)
plt.figure(figsize=(8,6))
scatter = plt.scatter(X_train_pca[:, 0], X_train_pca[:, 1], c=y_train, cmap='coolwarm', alpha=0.7)
plt.xlabel('First Principal Component')
plt.ylabel('Second Principal Component')
plt.legend(handles=scatter.legend_elements()[0], labels=['Malignant', 'Benign'])
plt.title('PCA of Breast Cancer Dataset (Training Set)')
plt.show()
如果图中红点(恶性)和蓝点(良性)大致能被一条直线分开,说明线性模型是合适的。从乳腺癌数据集的结果来看,逻辑回归能达到非常高的性能,说明其线性假设在这里是合理的。
整个项目走下来,你会发现机器学习远不止调包。从数据理解、预处理、模型选择、评估到解释,每一步都需要基于数据和业务进行思考与决策。这个乳腺癌数据集的逻辑回归项目,就像是一把钥匙,帮你打开了标准机器学习工作流的大门。下次当你面对一个新的数据集时,不妨沿着这个流程走一遍,相信你会有更扎实的收获。
更多推荐



所有评论(0)