teach.pascalyim.com
目录

ML · 章节 1

机器学习 1 — Python 与数据初探

在 Kaggle 上打开

本章为整门课程奠定基础。在正式讨论各类学习算法之前,我们必须先掌握一组实用工具:在 Notebook 环境中编写 Python 代码,加载真实的数据集,提取若干基本统计量,并以多种方式将其可视化。

我们首先介绍工作环境(Kaggle 与 Jupyter Notebook),随后回顾后续章节所必需的 Python 语法。接着进入 NumPy 学习向量化数值计算,进入 pandas 学习表格型数据的操作,再依次介绍 Seaborn 与 Plotly Express 这两套互补的可视化工具。本章最后引入 Python 的面向对象编程,为后续封装学习模型做好准备。

我们的目标并不是要在一节课之内把读者训练成熟练的 Python 程序员,而是为大家建立一个共同的基础,使后面的章节可以专注于机器学习本身,而不必反复回到语法层面的细枝末节。

工作环境:Kaggle 与 Jupyter

整门课程的代码都在 Kaggle 平台上运行。这是一个面向数据科学与机器学习的在线平台,在教学上有若干显著优势:无需任何本地安装,Python 环境统一(NumPy、pandas、scikit-learn、Plotly 等常用库已预装),数据集通过标准路径访问,所有同学都在严格一致的环境中运行代码。这就保证了在你笔记本上能跑通的代码,在邻座同学那里也一定能跑通。

在 Kaggle Notebook 中,Python 代码运行在远端服务器上。挂载到 Notebook 中的数据集只读地暴露在 /kaggle/input/数据集名/ 之下,你自己生成的文件则写入 /kaggle/working/

单元格、执行与 Markdown

Jupyter Notebook 是一种交互式文档,把解释性文字、数学公式、可执行代码与可视化结果融合在一起。它由若干单元格(cells)组成,主要分两类:代码单元格包含可执行的 Python 代码,最后一行表达式的结果会自动显示;Markdown 单元格则承载文字、标题、公式与图片。

执行单元格时使用以下键盘快捷键:

  • Shift + Enter:执行当前单元格并移到下一格
  • Ctrl + Enter:执行当前单元格但不移动
  • Alt + Enter:执行当前单元格并在下方插入新格

单元格在原则上可以以任意顺序执行,但变量是按实际执行顺序存入解释器内存的。因此实践中最稳妥的做法是从上到下执行整个 Notebook,在做出重要修改之后重新整体执行一次,这是确保所见输出与所写代码一致的唯一方法。

Markdown 单元格支持常见语法(# 表示标题,**粗体***斜体*、有序与无序列表),也支持 LaTeX 数学公式。行内公式写作 $ax + b$,独立公式则用 $$ ... $$ 包围,例如:

y^=ax+b\hat{y} = a x + b

图片用 ![描述](路径) 插入,路径通常指向一个数据集或外部 URL。

标准导入语句

本章及后续几乎所有 Notebook 都以同一组导入语句开头:

import numpy as np import matplotlib.pyplot as plt import pandas as pd import plotly.express as px

NumPy 负责向量化的数值计算,Matplotlib 是静态绘图的基础设施,pandas 提供表格型数据结构(即 DataFrame),Plotly Express 则是高层的交互式可视化接口。nppdpx 这些别名是社区约定,请务必遵循,这样写出的代码才便于他人阅读。

Python 必备语法回顾

继续深入之前,我们快速回顾后续会反复用到的 Python 语法。Python 是一种动态类型语言:类型与值绑定,而不是与变量绑定。因此可以这样写:

a = 3 # int b = 2.5 # float c = "Python" # str d = True # bool

最常见的内置类型是 intfloatstrbool列表(list)用于存放有序的元素集合:

values = [1, 2, 3, 4, 5] values[0] # 第一个元素 values[-1] # 最后一个元素 values[1:4] # 子列表(切片)

控制结构方面,for 循环用于遍历集合,range 生成整数序列,while 循环则在条件成立时持续执行。这些都是再熟悉不过的工具:

for v in values: print(v) for i in range(0, 10, 2): print(i) count = 0 while count < 5: print(count) count += 1

while 循环必须在循环体内更新条件变量,否则将陷入无限循环。条件分支用 if / elif / else 表达,布尔运算符 andornot 用于组合条件:

if x % 2 == 0 and x > 10: print("x 是大于 10 的偶数")

函数用于封装并复用一段逻辑,通过 def 关键字定义,通过 return 返回结果:

def square(x): return x ** 2

最后两点同样不可忽视:模块导入通常采用约定的别名(如 import numpy as np),缩进在 Python 中具有语法意义,缩进错误会直接导致运行时错误。

Python 的设计哲学是"显式优于隐式":缩进既是排版,也是语法。把代码格式化整齐既是美感,也是正确性的一部分。

用 NumPy 处理向量与矩阵

NumPy 是 Python 数值计算生态的基石。它高效地操作向量(一维数组)与矩阵(二维数组),并对它们提供向量化的数学运算。所谓"向量化",意思是对整个数组做运算时不需要显式写循环,语义直观,执行速度也比纯 Python 循环快几个数量级。

向量与矩阵分别由 np.array 构造:

x = np.array([1, 2, 3, 4]) X = np.array([ [1, 2], [3, 4], [5, 6] ])

每个数组都有一个 shape 属性,描述其形状。一维向量的形状形如 (n,),二维矩阵的形状则是 (n_行, n_列)。在动手做任何运算之前先看一眼 shape,这是避免维度错误的最佳习惯。

x.shape # (4,) X.shape # (3, 2)

reshape 方法可以改变数组的形状而不改变其元素,前提是元素总数保持不变:

x = np.arange(1, 11) # 1..10 X = x.reshape(5, 2) # 5 行 2 列

这个操作在机器学习里非常常见——很多 scikit-learn 接口要求二维输入,而你手头可能只有一维向量,这时 reshape 就是常规的"形状适配器"。

求和、均值与轴的概念

NumPy 提供了一组聚合函数,如 np.sumnp.meannp.stdnp.minnp.max。在二维矩阵上,这些函数可以通过 axis 参数指定聚合方向:

np.sum(X) # 所有元素之和 np.sum(X, axis=0) # 每一列分别求和(沿行折叠) np.sum(X, axis=1) # 每一行分别求和(沿列折叠)

一个易记的口诀是:axis=0 折叠"行"维度,得到逐列结果;axis=1 折叠"列"维度,得到逐行结果。

点积与矩阵-向量乘积

两个向量的点积是机器学习中最基础的运算之一:

uv=iuiviu \cdot v = \sum_i u_i v_i
u = np.array([1, 2, 3]) v = np.array([4, 5, 6]) np.dot(u, v)

把它推广到矩阵-向量乘积,就得到线性模型的核心表达式:

X = np.array([[1, 2], [3, 4], [5, 6]]) w = np.array([0.5, 1.0]) X @ w

它对应的数学表达式是

y^=Xw\hat{y} = X w

其中 X 的每一行是一个样本的特征向量,w 是权重向量,X @ w 给出每个样本的预测值。

*@ 的关键区别

初学者最常混淆的就是这两个运算符:

X * w # 逐元素相乘(广播) X @ w # 矩阵-向量乘积

*逐元素运算,要求两个数组形状兼容(必要时按广播规则扩展);@矩阵乘积,严格遵守线性代数中的形状约束。课程中只要写到线性模型,就一定是 @,别写错了。

向量化运算同样适用于标量与数组之间:

x + 1 x * 2 x ** 2

这些表达式作用于整个数组,无须显式循环。一旦习惯了这种写法,就能告别绝大多数 for 循环。

用 pandas 处理 DataFrame

如果说 NumPy 把 Python 变成了一台数值计算机,那么 pandas 则把它变成了一张电子表格。pandas 围绕 DataFrame(带行列标签的二维表)与 Series(带索引的一维列)展开,几乎所有的数据探索都从这两种结构入手。

read_csv 读入数据

pandas.read_csv 是把 CSV 文件转成 DataFrame 的标准入口:

df = pd.read_csv('/kaggle/input/mini-datasets/co2_mini.csv')

最重要的几个可选参数是 sep(列分隔符,默认为逗号,但有些文件用 ; 或制表符),以及 encoding(文件编码,常用 'utf-8')。Kaggle 上数据集添加进 Notebook 后,会以子目录的形式出现在 /kaggle/input/ 下,完整路径可在右侧 Data 面板中将鼠标移到文件名上、点击复制图标得到。

课程中我们使用的 co2_mini 数据集来自实测的车辆 CO₂ 排放数据,只保留了使用高品质汽油(类型 "Z")的车辆。它包含两列:consumption 表示百公里油耗(L/100km),co2 表示碳排放量(g/km)。

第一眼:headshape

head() 显示 DataFrame 的前若干行(默认 5),是检查列名、值的格式与类型一致性最直接的方法:

df.head() df.head(10)

如果 head() 显示出 NaN 或杂乱的列,通常意味着 read_csvsepdecimalencodingna_values 参数没设对。

shape 返回 (n_行, n_列) 二元组:

df.shape # 例如 (100, 2) df.shape[0] # 行数,即观测数 df.shape[1] # 列数,即变量数

columns 给出所有列名;info() 则一次性提供行数、每列非空值的数量、数据类型与内存占用,可以快速识别缺失值和需要类型转换的列。

选择列

pandas 中常用的列选择有两种写法:

df['co2'] # Series(一维) df[['co2']] # DataFrame(二维,只含一列) df[['consumption', 'co2']] # DataFrame(多列)

重要区分 df['co2'] 返回的是一维 Series,而 df[['co2']] 返回的是二维 DataFrame。在调用某些 scikit-learn 接口时(它们要求二维输入),这一差异决定了代码能否运行。

也有点号写法 df.co2,但仅当列名不含空格、特殊字符且不与 pandas 自身属性冲突时才有效,因此略显脆弱,推荐用方括号写法。

简单统计:pandas 与 NumPy

最常用的几个统计量在 pandas 与 NumPy 中都可以方便地计算,二者本质上等价但用法略有差别:

df["co2"].sum() # pandas:Series 方法 np.sum(df["co2"]) # NumPy:作用于 Series 同样可行 df["co2"].mean() df["co2"].std() df["co2"].min() df["co2"].max()

pandas 风格的好处是:它直接整合在 DataFrame 中,自动处理缺失值;NumPy 风格则更通用,任何"类数组"对象都能传入。

describe 一览统计摘要

describe() 一次性返回所有数值列的统计摘要,是探索性分析的利器:

df.describe()

对每一列,它给出 count(非空值数)、mean(算术平均)、std(标准差,衡量围绕均值的离散度)、min(最小值)、四分位数 25%50%75%max(最大值)。

四分位数把有序数据分为四等份:

  • Q1Q_1(25%):有 25% 的值小于或等于它
  • Q2Q_2(50%):中位数,把数据切成两半
  • Q3Q_3(75%):有 75% 的值小于或等于它

四分位距定义为

IQR=Q3Q1IQR = Q_3 - Q_1

它衡量数据的中部离散度,对极端值不敏感,因此比标准差更稳健。借助 IQR,可以约定异常值(outliers)为满足

x<Q11.5×IQRx>Q3+1.5×IQRx < Q_1 - 1.5 \times IQR \quad \text{或} \quad x > Q_3 + 1.5 \times IQR

的观测。这是后面箱线图绘制时使用的判据。

用 Seaborn 进行分布可视化

Seaborn 是构建在 Matplotlib 之上的统计可视化库。它提供更具表达力的图形,API 直接面向 DataFrame,非常适合探索性分析。

箱线图

箱线图(boxplot)用五个描述性统计量浓缩呈现一个数值变量的分布:最小值、Q1Q_1、中位数、Q3Q_3、最大值。盒子从 Q1Q_1 延伸到 Q3Q_3,中间一条线代表中位数,须线延伸到非异常值的范围,异常值则单独以点标出。

import seaborn as sns plt.figure(figsize=(6, 4)) sns.boxplot(y=df["co2"]) plt.title("Boxplot of the CO2 variable") plt.show()

借助一张箱线图就能快速判断数据的离散度、对称性以及极端值的存在。需要强调的是,异常值不应被自动剔除,它们可能是测量错误,也可能是合法但少见的现象,任何清理决定都应基于具体的领域分析。

直方图与核密度估计

直方图(histogram)把观测分到一系列区间(bin)里,以每个区间的频次作为柱高:

plt.figure(figsize=(6, 4)) sns.histplot(data=df, x="co2", bins=100) plt.title("Distribution of the CO2 variable") plt.show()

bins 参数控制区间数:太少会掩盖分布的细节,太多会放大噪声。

核密度估计(KDE)给出一条平滑的连续密度曲线,曲线下面积为 1:

plt.figure(figsize=(6, 4)) sns.kdeplot(data=df, x="co2", fill=True) plt.title("Density estimation (KDE)") plt.show()

两者也可以叠加显示,把柱状的频次与平滑的趋势放在同一张图上:

plt.figure(figsize=(6, 4)) sns.histplot(data=df, x="co2", bins=100, kde=True) plt.title("Histogram with density estimation") plt.show()

小提琴图

小提琴图(violin plot)同样依赖密度估计,但把它沿轴对称地画出来,宽度反映密度。设置 inner="box",可在小提琴内部嵌入一个迷你箱线图,把密度曲线与统计摘要合并成一张图:

plt.figure(figsize=(6, 4)) sns.violinplot(y=df["co2"], inner="box") plt.title("Violin plot of the CO2 variable") plt.show()

直方图、KDE 与小提琴图三者互补:直方图描述离散区间,KDE 给出连续近似,小提琴图把密度与描述性统计结合起来,共同构成建模前的探索性分析的标准动作。

散点图与变量间关系

到目前为止,我们都在分析单变量分布。散点图(scatter plot)则用于研究两个数值变量之间的关系——这正是引入线性回归的入口:

plt.figure(figsize=(6, 4)) sns.scatterplot(data=df, x="consumption", y="co2") plt.title("Relationship between consumption and CO2") plt.show()

每个点代表一组观测 (xi,yi)(x_i, y_i)。从散点图我们可以观察到整体趋势(增长/下降)、关系的形式(线性还是非线性)、点云的离散程度,以及非典型观测的存在。这些直观的判断会直接指导我们后续选择什么样的模型。

Plotly Express 简介:交互式可视化

Plotly Express(惯例别名 px)是一个高层 Plotly 接口,可用一行调用就把 DataFrame 渲染成交互式图形。和 Seaborn / Matplotlib 不同,Plotly 图默认就是交互式的:鼠标悬停可以看精确数值,缩放、平移都是原生支持,生成的图也方便嵌入到仪表盘中。

每次调用 px.* 都返回一个 fig 对象,后续可继续修改:

fig = px.histogram(df, x="co2", nbins=100) fig.show() fig = px.violin(df, y="co2", box=True, points="outliers") fig.show() fig = px.scatter(df, x="consumption", y="co2", trendline="ols") fig.show()

值得注意的是,px.scattertrendline="ols" 参数会在点云上叠加一条最小二乘拟合直线,这正是下一章我们要学的线性回归的图形预览。

px.* 生成的图由若干轨迹(trace)与一个布局(layout)组成。可以通过 update_traces 调整轨迹的视觉属性,通过 update_layout 调整整体布局:

fig = px.histogram(df, x="co2", nbins=50) fig.update_traces( opacity=0.7, marker_line_color="black", marker_line_width=1 ) fig.update_layout(width=600, height=400) fig.show()

这种"先创建、后微调"的工作流是 Plotly 的核心思想:第一步用 px.* 建好默认图,第二步根据需要调整外观。

Python 类与对象

本章最后引入 Python 的面向对象编程(OOP)。从下一章开始,scikit-learn 中的每一个模型都会以"类"的形式出现,因此我们需要先理解几个核心概念:类、属性、方法、实例化与状态。

类是一种自定义的对象类型,把数据(称为属性)与函数(称为方法)打包在一起。看一个最小的例子:

class Calculatrice: def __init__(self): # 实例属性:每个对象都有自己的内存 self.memoire = 10 def add(self, x): # 方法:修改对象的内部状态 self.memoire = self.memoire + x

这里几个要点要逐一理解。class 关键字用于定义新的类型,Calculatrice 是这个类型的名字。__init__构造函数,在新对象创建时自动调用,用来初始化属性。self 是约定俗成的第一个参数,代表当前实例,通过 self 我们才能访问到属于这个对象的属性和方法。self.memoire = 10 给该实例创建了一个属性 memoire,因此每个 Calculatrice 对象都拥有自己独立的"内存"。add 是一个方法,它通过 self.memoire = self.memoire + x 修改对象的内部状态。

实例化与使用

要从类得到一个具体可用的对象,需要做实例化:

c = Calculatrice()

带括号地调用类等价于触发新对象的创建:Python 在内存中开辟空间,接着自动调用 __init__(self),初始化属性,最终把对该对象的引用赋给变量 c。我们可以用同一个类创建任意多个对象,每个对象都有自己独立的状态。

实例化之后,通过点号即可读取属性、调用方法:

c.memoire # 读取属性,返回 10 c.add(20) # 调用方法,把 memoire 改为 30 c.memoire # 再次读取,返回 30

读取属性不会改变对象状态;调用方法则会(本例中)修改属性 memoire。状态的改变是持久的,只要对象还在内存中,它就保留新的值。

总结起来:类描述了对象的结构与行为;__init__ 负责初始化属性;self 指代当前实例;属性存储状态,方法读取或修改这些状态。这套机制后面会反复出现:scikit-learn 的每一个估计器都遵循"先实例化(model = LinearRegression())、再调用方法(model.fit(...),model.predict(...))"的模式。

f-string 提醒

在打印对象状态、生成报告的过程中,我们会大量使用 f-string(格式化字符串字面量)。在字符串前加 f,大括号 {} 中的表达式会被自动求值并插入:

year = 3 capital = 1234.567 print(f"Year {year}: capital = {capital:.2f}")

冒号后面的 .2f 是格式说明符:f 表示浮点数,.2 表示保留两位小数。本章的练习以及后续章节都会频繁用到这种写法,值得提前熟练掌握。

练习

下列练习集中检验本章的核心技能。请独立完成,然后再与同伴比对。

练习 1 — Python 基础回顾(不使用 NumPy)

本练习用于验证后续章节所需的 Python 基本结构是否掌握。

  1. 创建一个包含数字 1 到 10 的列表。
  2. for 循环遍历该列表,只打印偶数。
  3. for ... in range(...) 循环计算 1 到 10 之间所有整数之和。
  4. 编写一个函数 mean(values),接收一个数字列表,返回它们的算术平均值。
  5. while 循环按降序打印从 5 到 1 的整数。

练习 2 — NumPy 向量与矩阵运算

本练习用 NumPy 操作向量与矩阵,不使用任何显式循环,为线性回归中将出现的运算做铺垫。

考虑向量

x = np.array([1, 2, 3, 4, 5, 6])

依次完成:

  1. reshapex 变成形状为 (3, 2) 的矩阵 X

  2. 计算:

    • X 所有元素之和;
    • 每列之和;
    • 每行之和。
  3. 给定系数向量

    w = np.array([0.5, 1.0])

    计算矩阵-向量乘积 X @ w

  4. 比较

    X * w X @ w

    两个表达式的结果,并简要解释它们的差异。

练习 3 — 面向对象编程:复利计算器

我们想对按复利方式投资的资本进行建模。资本根据下式逐年演化:

Cn+1=Cn×(1+r)C_{n+1} = C_n \times (1 + r)

其中 CnC_n 是第 nn 年末的资本,rr 是年化利率(以小数形式给出,例如 5% 写作 0.05)。

请完成:

  1. 编写一个类 CompoundInterestCalculator,包含:
    • 构造函数 __init__(self, capital_initial, taux);
    • 表示当前资本的属性 capital;
    • 表示年化利率的属性 taux
  2. 添加方法 next_year(self),通过应用利息更新 capital,即修改对象的内部状态。
  3. 添加方法 simulate(self, n),在 n 年内模拟资本的演化,并在每年之后打印当前资本(建议使用 f-string,保留两位小数)。
  4. 实例化一个对象,初始资本为 1000,年利率为 5%(即 0.05)。
  5. 模拟该资本在 5 年内的演化。

拓展阅读

下列官方文档可作为本章内容的补充和深入学习的入口: