A2

Python 编程

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

1 为什么机器学习实验从 Python 写起

面对机器学习任务,最先遇到的困难往往不是模型公式,而是怎样把问题变成可以检查的计算。用户流失表里的空白代表未填写还是没有使用?图片目录里的文件名是否暗含类别?短文本里的投诉发生在预测时刻之前还是之后?Python 的作用,是把这些判断写成明确步骤,让别人能够执行、检查并重复得到结果。学习编程,首先是在学习如何保留证据。

例如,运营团队想提前识别可能离开的用户。输入可以包括过去 30 天的登录天数、过去 90 天的服务咨询次数、当前套餐和已使用月数;输出是未来 30 天是否流失。程序需要读取记录、检查时间、构造特征、划分数据、训练模型并计算指标。这里的每个动作都可能改变结论。只有把动作写下来,才能区分模型能力、数据错误和偶然波动。

Python 适合入门,是因为相同的基础语法能贯穿这些工作:变量给中间结果命名,列表保存多个值,条件判断处理不同情况,循环重复动作,函数封装可以复用的规则。数据分析库负责表格运算,机器学习库提供常见模型。库帮我们减少重复劳动,但不会替我们决定标签是否合理、测试集是否被污染。

本课把程序看成“输入、规则、输出、检查”组成的可执行说明。你不必马上理解解释器内部、复杂对象体系或深度学习框架底层。先做到读懂变量、改动函数、识别错误、复现实验,再逐渐扩大程序规模。写出很长的脚本不是目标,能够解释结果如何产生才是目标。

从业务问题到可复现实验的关系
从业务问题到可复现实验的关系

公开数据适合练习,因为任务、字段和来源通常可以追溯。本课用 scikit-learn 自带的乳腺癌诊断表格数据完成实验,再对照 Fashion-MNIST 图片分类与公开短文本分类场景。它们分别帮助我们观察表格特征、图像内容和文本线索。教学实验只说明流程与评价方法;医疗数据上的课堂分类结果不能直接变成临床判断。

2 变量、容器、条件、循环与函数:把计算写清楚

开始写代码前,先写清楚预测对象、预测时刻、输入范围和目标标签。流失任务的对象可以是“每月月底仍在使用服务的用户”,预测时刻是月底,标签是随后 30 天是否取消服务。只有月底已经存在的信息才能进入输入。取消后的退款金额即使与流失高度相关,也不属于当时可用的信息,把它放进去就是数据泄漏。

“每行是什么”同样关键。每行代表用户、用户的某个月,还是某次访问,会决定怎样划分数据。如果同一用户的相邻月份同时出现在训练集和测试集,模型可能依靠身份线索获得漂亮分数。图片也有类似问题:原图和裁剪图分到不同集合,并不代表测试到了真正的新图片。短文本里的转发和近似重复,也会让测试变容易。

特征是用于预测的输入信息,标签是我们希望预测的答案。数值特征可以是登录天数、图像像素或文本长度;类别特征可以是套餐类型、拍摄设备或语言。标签必须有明确规则:流失是取消、连续未登录,还是合同到期未续约?不同规则对应不同任务。不要让“看上去像流失”代替可执行定义,也不要把缺失标签自动当作负类。

常见实验将数据划为训练集、验证集和测试集。训练集用来拟合参数,验证集用来比较模型、调整阈值,测试集用于最终评价。本课采用约 60%、20%、20% 的比例,这只是便于学习的选择。实际划分还要考虑数据量、类别占比和时间顺序。预测未来时,通常应让较早时期用于训练,较晚时期用于验证与测试。

划分不是把文件切开就结束。需要记录样本数量、正类定义、各集合的类别比例,以及是否存在重复对象。若正类只有 2%,随意抽样可能使验证集几乎没有正类。分层抽样可以在适用时保持类别比例,但无法解决用户重叠或时间穿越。这些约束需要先确定,再选择相应的划分方法。

数据边界还包括使用许可、隐私与获取条件。下载公开数据时应记录名称、来源、版本或获取日期,并查看使用说明。公开可访问不等于可以任意传播。对练习而言,优先使用已整理好的公开数据,保留原始副本;清洗后的文件另存,记录删掉哪些样本及原因。这样出现疑问时,才能从结果回到原始证据。

3 样本、特征、标签与预测时间边界

变量是名称与对象之间的关联。例如 days = 12 将名称 days 关联到整数对象,rate = 0.25 保存浮点数,is_active = True 表示布尔值。字符串如 "basic" 表示文本。名称应表达含义,login_days 通常比 x1 更容易检查。浮点数存在表示误差,因此不要把小数计算中极小的差异直接理解为业务变化。

列表按顺序保存多个元素,例如 scores = [0.3, 0.8, 0.6]。Python 的索引从 0 开始,scores[0] 得到第 1 个元素。字典通过键组织信息,例如 {"login_days": 12, "plan": "basic"}。缺少某个键与键对应的值是 None 并不相同;前者可能意味着字段没有提供,后者可能表示明确记录了未知值。

条件判断让程序根据证据选择路径。if probability >= threshold: 可以决定是否发出提醒,但前提是概率属于正确的类别。循环则对多个对象重复规则,例如逐条检查记录是否缺少时间字段。缩进在 Python 中表示代码所属的块,不是单纯排版。错误缩进可能让检查只作用于最后 1 条记录,也可能直接产生语法错误。

函数把输入与输出之间的规则命名。例如下面的函数只负责按阈值转换预测结果,不负责读取文件或训练模型。职责越清楚,越容易单独解释。默认参数表达默认选择,但调用者仍能明确传入其他阈值。函数里的 return 交回结果,print 只是显示信息,不能把屏幕上出现了内容当作函数已经返回了值。

def decide(probability, threshold=0.5):
    return int(probability >= threshold)

prediction = decide(0.72, threshold=0.6)
print(prediction)  # 1

数据分析常把输入组织成二维表 X:每行是样本,每列是特征;标签 y 则与行逐个对应。读取后应先检查形状、列名、类型和缺失情况,再计算结果。数字样式的字符串仍可能是文本;类别编号也不一定具有大小关系。把套餐编码为 1、2、3,不代表套餐之间存在等距离的数值关系。

库通过导入使用,例如 from sklearn.metrics import f1_score。安装库与导入库是不同动作,笔记本使用的解释器也可能与终端不同。出现 ModuleNotFoundError 时,应确认当前环境及包名,而不是不断改模型代码。出现 ValueError 时,先阅读错误消息中提到的输入形状、类型或取值,再定位调用位置。

训练与预测阶段的数据流和拟合边界
训练与预测阶段的数据流和拟合边界

机器学习里的 fit 表示从数据中学习规则,transform 表示按已学规则转换输入,predict 表示给出类别预测。标准化器也需要 fit,因为均值和标准差来自数据。正确做法是只用训练集学习这些统计量,再转换验证集与测试集。Pipeline 把转换和模型串在一起,帮助保持这个边界,但仍需要开发者先正确划分原始数据。

4 训练 / 验证 / 测试划分与 Pipeline

我们选择乳腺癌诊断数据中的 569 个样本与 30 个数值特征。特征来自细胞核图像的测量,例如平均半径、平均纹理和平均面积;标签区分恶性与良性。原始数据将恶性编码为 0,良性编码为 1。本课重新构造标签,使恶性为正类 1,避免把“正类召回率”误读成对良性的识别能力。

实验问题限定为:根据这组已有测量特征,学习区分这份数据中的恶性与良性样本。它没有覆盖检查设备变化、真实就诊人群差异、采样流程或临床成本。这个边界使实验可以检查,也限制了结论能推广到哪里。公开数据上的高分只说明当前评价条件下的表现,不能自动证明部署价值。

模型选用逻辑回归。尽管名称含有“回归”,这里用于分类,可以输出正类概率,再由阈值决定类别。我们先使用标准化处理不同量纲的特征,再拟合模型。此处只需要理解:模型根据训练数据寻找输入与标签之间的规律;具体优化过程、正则化推导和概率校准留待后续学习。

复现需要控制数据、流程和环境。固定随机种子,使这次随机划分可以重复;记录 Python 与相关库版本,保留完整代码及参数。相同种子并不保证在所有软件版本、硬件和算法下逐位一致,也不证明结果稳定。它首先解决的是“这次划分究竟是什么”,后续还应评估换种划分后结论是否改变。

比较模型前应准备基线。若总是预测为多数类,也可能得到不低的准确率。基线给我们提供最低限度的参照:模型是否学到了超出类别占比的信息?在这个实验中,可以用只按训练集多数类预测的分类器作比较。若复杂模型没有超过这种简单做法,应先检查数据与评价方法,而不是马上增加复杂度。

完整顺序是先划分,再拟合处理步骤与模型,然后查看验证集,最后冻结选择并评价测试集。验证集不是可以无限尝试而没有代价的资源。反复根据它挑特征、挑算法、挑阈值,也会让选择逐渐适应该集合。入门实验先限制尝试范围,并把每次改动和理由写进记录,通常比盲目搜索大量组合更有帮助。

5 动手:可复现的分类实验脚本

下面代码适用于已安装 scikit-learn 的 Python 环境,也可以放入笔记本按顺序执行。先将完整实验作为明确流程运行,再拆成单元格观察中间结果。安装依赖应在选定的环境中完成;这里不要求联网下载数据,因为 load_breast_cancer 随库提供数据。代码中的种子、标签映射与比例都是实验约定的一部分。

import sys
import sklearn
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.dummy import DummyClassifier
from sklearn.metrics import (
    precision_score, recall_score, f1_score,
    roc_auc_score, confusion_matrix,
)

data = load_breast_cancer(as_frame=True)
X = data.data
y = (data.target == 0).astype(int)  # 恶性为正类 1

X_train, X_hold, y_train, y_hold = train_test_split(
    X, y, test_size=0.4, stratify=y, random_state=42
)
X_val, X_test, y_val, y_test = train_test_split(
    X_hold, y_hold, test_size=0.5,
    stratify=y_hold, random_state=42
)

print("Python:", sys.version.split()[0])
print("scikit-learn:", sklearn.__version__)
for name, features, labels in [
    ("train", X_train, y_train),
    ("val", X_val, y_val),
    ("test", X_test, y_test),
]:
    print(name, features.shape, "positive_rate=", labels.mean())

baseline = DummyClassifier(strategy="most_frequent")
baseline.fit(X_train, y_train)
print("baseline_val_f1=", f1_score(
    y_val, baseline.predict(X_val), zero_division=0
))

model = make_pipeline(
    StandardScaler(),
    LogisticRegression(max_iter=2000, random_state=42),
)
model.fit(X_train, y_train)
positive_column = list(model.classes_).index(1)
val_probability = model.predict_proba(X_val)[:, positive_column]

def report(labels, probability, threshold):
    predicted = (probability >= threshold).astype(int)
    return {
        "precision": precision_score(labels, predicted, zero_division=0),
        "recall": recall_score(labels, predicted, zero_division=0),
        "f1": f1_score(labels, predicted, zero_division=0),
        "roc_auc": roc_auc_score(labels, probability),
        "confusion": confusion_matrix(labels, predicted, labels=[0, 1]),
    }

threshold = 0.5  # 本次预先约定;如需调整,只使用验证集
print("validation:", report(y_val, val_probability, threshold))

# 完成验证与方案冻结后,再运行以下最终评价。
test_probability = model.predict_proba(X_test)[:, positive_column]
print("test:", report(y_test, test_probability, threshold))

阅读代码时,先观察 X.shape 与各集合样本数量,再确认正类比例。test_size=0.4 先留出约 40% 的数据,再把留出部分平分为验证和测试,因此得到约 60%、20%、20%。实际行数需要取整,不应要求每个比例恰好成立。stratify 让这个随机示例尽量保持类别比例;它没有加入时间或患者分组约束。

随后观察 fit 的输入。基线与 Pipeline 都只接收训练集,验证与测试阶段调用预测方法。positive_column 根据类别顺序查找正类,避免默认认为某个概率列必然代表所关心的事件。report 同时返回阈值相关指标与概率排序指标,使我们能发现“排序还可以,但当前提醒规则不合适”的情况。

笔记本最容易出现的陷阱是执行顺序。你可能先训练模型,后来修改标签单元格,却没有重新运行后面的代码,屏幕上仍保留旧指标。交付实验前应重启内核,再从头顺序运行。不要把未执行的代码和旧输出放在一起当作证据。报错时保留错误类型、消息和触发代码,逐步缩小输入范围;不要用大范围忽略异常来制造“顺利运行”。

这里没有预填某个准确分数,因为指标应来自读者实际运行的环境与划分。遇到收敛警告,应记录并检查标准化、迭代上限与输入,而不是仅隐藏警告。如果删除样本、改变特征或调整阈值,要重新记录实验身份。随机种子相同但数据已经改变,仍然是不同的实验。

6 precision、recall、F1、ROC-AUC 与混淆矩阵

评价之前先明确正类。真正为正且被预测为正的是 TP,真正为负却被预测为正的是 FP,真正为正却预测为负的是 FN,真正为负且预测为负的是 TN。本课的混淆矩阵按真实标签排列行、预测标签排列列,标签顺序为 [0, 1],因此 2 行依次是 [TN, FP] 与 [FN, TP]。换工具时需要重新确认显示约定。

精确率 precision 为 TP / (TP + FP),回答“发出的正类判断中,有多少是真的”;召回率 recall 为 TP / (TP + FN),回答“所有真实正类中,找到了多少”。F1 是二者的调和平均,公式为 2PR / (P + R)。若只关心其中某种错误代价,单独追求 F1 也可能选错方案。

设教学示例有 100 条记录,其中 20 条为正类。某阈值下 TP 为 15、FP 为 10、FN 为 5、TN 为 70,则精确率为 0.60,召回率为 0.75,F1 约为 0.67,准确率为 0.85。始终预测为负类也有 0.80 的准确率,却找不到任何正类。这说明准确率需要结合类别比例阅读,不能独自承担评价任务。

ROC-AUC 衡量模型在不同阈值下区分正负类的排序表现,计算时应使用正类分数或概率。把已经切成 0 和 1 的预测类别传进去,会丢失排序信息。AUC 高也不说明概率数值准确,更不能直接保证某个阈值下的精确率。正类很少时,还应查看精确率与召回率的关系,后续可以学习 PR 曲线与平均精确率。

从预测分数到诊断证据的观察路径
从预测分数到诊断证据的观察路径

阈值决定哪些分数转为提醒。固定模型与同一批样本,降低阈值会增加或保持正类预测数量,召回率不会下降,但精确率不保证单调变化。调整阈值不会改变这些样本的分数排序,所以 ROC-AUC 不变。真正选阈值还要结合误报成本、漏报成本和人工处理容量,不能把 0.5 当成天然最优值。

除了总体指标,还应查看错误集中在哪些切片:较短文本、低清晰度图片、新用户、特定设备或较晚时间段。切片样本少时,分数可能波动很大,应同时报告样本数与错误数。观察训练与验证表现也有价值:训练很好而验证明显下降,提示可能过拟合或集合差异;两者都差,则应检查任务、标签、特征和基线。

7 常见坑:泄漏、重复样本、静默类型错误

数据泄漏常以“方便清洗”的形式出现。先在全体数据上计算均值填补缺失值,再划分集合,已经让测试数据参与了规则学习。先对全体文本学习词表、按全部标签挑选最相关特征,也有相同问题。应先划分,再让需要学习数据规律的处理步骤只在训练集拟合;明确的固定格式转换则需要按具体规则判断。

另一个反模式是让索引错位。表格筛选或排序后,特征与标签可能仍拥有相同长度,却不再逐行对应。代码能够运行,不代表对应关系正确。保留稳定的样本标识,联合筛选特征与标签,并在关键位置检查标识与行数。标识通常用于追溯,不应未经论证就作为模型特征,尤其不能把含答案的文件名直接输入分类器。

类别不平衡也不等于只需“把少数类复制更多次”。重采样必须限制在训练数据内,否则重复样本可能进入验证或测试。加权、重采样与阈值调整解决的是不同环节的问题,不能假定效果等价。入门阶段先报告类别分布、基线和混淆矩阵,再尝试有限调整,并观察精确率与召回率的共同变化。

为了分数而反复查看测试集,会把它变成事实上的验证集。即使没有调用 fit(X_test),只要根据测试表现不断改方案,最终分数也不再代表独立评价。如果已经发生,应诚实记录,并在条件允许时准备新的独立测试数据。不能靠把文件重新命名为“最终测试”恢复独立性。

还要防止程序层面的静默错误:把字符串 "0" 当成布尔假值、把缺失值填为 0 后忘记其含义、用绝对路径导致换机器就失败、只保存模型却丢掉预处理。解决这些问题通常不需要高级技巧,而需要明确类型、保存处理流程、使用可解释的路径与检查。代码运行完成只是起点,输入输出符合约定才是可用结果。

Python 还有一些容易在机器学习脚本中被忽略的执行机制。for row in rows: 里若直接修改 row 的局部名称,原列表并不会改变;需要按索引赋值,或生成新的列表后明确替换。对表格使用布尔条件时,and、or 不能直接连接一整列,应该使用库提供的逐元素运算,并给每个条件加括号,否则可能得到歧义错误或错误筛选。函数若把可变列表设为默认参数,跨多次调用会残留上一次样本;例如收集错误行时,第二次实验可能混入第一次的记录。把中间结果写入全局变量也会让笔记本重跑顺序影响结论。可用小规模的2到3行输入断言形状、类型、标签集合和记录数,并在每轮实验开始清空状态,尽早暴露这些错误。

8 案例:脚本能跑但指标不可信

先看本课诊断数据。假设读者直接沿用原始标签,把 1 当作恶性,然后看到较高召回率,便写出“多数恶性样本已被找出”。程序可能完全没有报错,但该召回率实际对应良性。修复应从数据说明与标签计数开始,明确映射,再重算混淆矩阵。这个案例说明,指标函数无法判断人的解释是否与标签语义一致。

再看公开图片数据 Fashion-MNIST,其中包含 10 类服饰灰度图像,每张为 28×28 像素。若训练与测试都来自相同整理条件,模型可能在该数据上表现良好。但把模型用于手机拍摄的服饰照片时,背景、光照、视角和图像尺寸都可能改变,这属于分布偏移。调整输入尺寸只是格式兼容,不足以消除内容差异。

检查图片失败时,可以抽取衬衫与外套等易混类别,按真实类别和预测类别查看样本,确认标签、裁剪与预处理。若发现测试文件是训练图片的轻微变体,应按原图来源重新分组划分。若问题出在新拍摄条件,则需要代表目标场景的数据来评价。不能把重新划分后的未知分数写成已经改善,更不能用少量挑选出的成功图片代替总体评价。

公开短文本也会出现捷径。使用 SMS Spam Collection 练习垃圾短信识别时,可以把词语或字符片段变成特征,但某些电话号码、链接格式或近似重复消息可能影响评价。假如高度相似短信被随机放入不同集合,模型容易在熟悉内容上得分。应先检查重复与近重复情况,说明划分单位;面向未来短信时还要考虑新话术和语言变化。

流失预测则常被时间信息误导。某团队加入“取消后收到的挽留电话次数”,验证 AUC 明显上升,便认为新特征非常有效。追查发现该字段只在用户提出取消后更新,模型利用了已经发生的结果。正确修复是按预测时刻恢复可用字段,重新建立时间划分,并从头评价;原来的高分应保留为泄漏证据,不能继续作为方案能力宣称。

以上是基于真实数据类型设计的故障情境,不是声称这些公开数据必然存在所述问题。它们共同提示:先辨认模型到底看到了什么,再解释它学到了什么。标签含义错误、近重复和分布偏移需要不同修复;笼统地说“数据不好”既不能指导改动,也不能说明复测是否覆盖原来的风险。

9 把实验收成可复查的记录约定

脚本能跑不等于实验可复查。把一次机器学习实验收成记录时,目标是让另一位同学(或一周后的自己)能重走判断,而不是只能看到某个醒目的 F1。约定如下,并在笔记本或仓库里留固定位置。

  1. 输入身份:数据来源、版本或校验值、许可与是否含个人字段;样本单位(一行是客户还是一次事件)。
  2. 问题边界:预测时刻、允许特征、标签定义、明确排除的字段(例如事后才知道的 duration)。
  3. 划分与种子:训练 / 验证 / 测试比例或时间切分规则、分层或分组键、随机种子;是否检查跨集合重叠。
  4. 处理与模型:Pipeline 步骤、只在训练集拟合的变换、模型类与关键超参、阈值如何选定。
  5. 指标与错例:precision / recall / F1 / ROC-AUC / 混淆矩阵,以及若干代表性假阳性、假阴性。
  6. 环境:语言与依赖版本;一次运行的命令或笔记本执行顺序。
  7. 未知项:当前证据回答不了的问题,单独列出,避免把猜测写成结论。
从异常现象到修复复测的记录路径
从异常现象到修复复测的记录路径

记录要区分观察与推断。“验证集召回率 0.41”是观察;“召回率低是因为标签映射反了”必须附带检查步骤与结果。修复后用同一验证协议复测,并说明样本集合有没有变。含个人信息的字段不要写进公开日志,可用受控标识代替。

有了这套约定,下一课用 NumPy / Pandas 处理更大的表时,才知道行数、类型和连接结果应该记在哪里,也才知道指标变化到底来自数据还是来自模型。

放进方案里

把 Python 写进机器学习方案时,应交付可执行的实验约定。以用户流失预警为例,先写“每月月底对仍在使用服务的用户,预测未来 30 天是否取消”,明确正类为取消用户。输入限定为月底前已经产生的登录天数、使用月数、套餐及服务咨询次数,列出排除取消后退款与挽留记录的理由。数据来源、许可、字段含义和标签生成方式都应可追溯。

划分按时间推进,并明确同一用户跨时期出现时的处理原则。训练集用于拟合预处理与模型,验证集用于选择有限候选方案和阈值,测试集在方案冻结后评价。方案中记录各集合的时间范围、样本数与正类比例,说明缺失值和类别字段如何处理。若采用随机划分练习,应说明它与未来部署条件之间的差距。

验收同时呈现多数类基线、precision、recall、F1、ROC-AUC 和混淆矩阵,再列出关键人群切片的样本数与错误数。阈值应结合漏报代价、误报代价与人工回访容量确定;如果这些约束尚未明确,就保留待决项,不把教学示例中的 0.5 写成业务承诺。报告中明确区分实际测量结果、演示数字和待验证假设。

交付代码还要包含环境版本、数据获取说明、划分配置、随机种子和从头运行的方法。预测时保存所需预处理规则,使新数据经过与训练阶段一致的转换。上线前核查字段是否按时到达、标签是否存在延迟、目标人群是否与实验一致;运行后持续观察输入变化、提醒数量和标签回收后的指标。发现异常时,沿标签、时间边界、数据重叠与分布变化逐项留证,再决定修复或暂停使用。

10 小结:下一步用 NumPy / Pandas 扛表(A3)

Python 编程的入门成果,是能够用清晰规则把数据变成可检查的结果。变量、条件、循环和函数组织计算,库提供常见工具,实验边界决定结果能说明什么。

自测时,可以先解释为什么提前标准化全部数据会越过边界,再解释降低阈值为何不会改变固定分数的 AUC。能够把这些判断写进代码与实验记录,就具备了继续学习机器学习工具的基础。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务