端到端机器学习项目

https://github.com/ageron/handson-ml3/blob/main/02_end_to_end_machine_learning_project.ipynb

什么是端到端机器学习?🤔

端到端机器学习(End-to-End Machine Learning) 是指在一个机器学习项目中,整个流程从数据收集、预处理、模型训练到最终预测结果的输出都由同一个系统或过程自动化完成,且通常是一个整体性的解决方案。简单来说,它就是将各个步骤(如特征工程、模型选择、训练、调优等)整合成一个统一的系统,减少人工干预。

这种方法的优点是能够简化工作流程,提高效率。尤其在深度学习中,端到端模型往往能够直接从原始数据中学习到有用的特征,而不需要人工进行繁琐的特征工程。

举个例子:

假设你想训练一个自动驾驶的系统:

端到端机器学习的典型应用领域有:

这种方法能够减少很多手动的中间环节,但它也要求系统能够处理数据的复杂性和大量的计算。

使用真实数据

流行的开放数据存储库

我们将使用来自StatLib存储库的加州房价数据,数据集基于1990年加州人口普查的数据。

选择性能指标

均方根误差RMSE

回归问题的典型性能度量是均方根误差(Root Mean Square Error,RMSE)。

RMSE(X,h)=1mi=1m(h(𝐱(i))y(i))2 \text{RMSE}(X, h) = \sqrt{\frac{1}{m} \sum_{i=1}^{m} \left( h(\mathbf{x}^{(i)}) - y^{(i)} \right)^2}

x(1)=[118.2933.91141638372] x^{(1)} = \begin{bmatrix} -118.29 \\ 33.91 \\ 1416 \\ 38372 \end{bmatrix}

例如,如果第一个地区如前所述,则矩阵X如下所示:

X=((x(1))T(x(2))T(x(1999))T(x(2000))T)=(118.2933.91141638372) X = \begin{pmatrix} \left( x^{(1)} \right)^T \\ \left( x^{(2)} \right)^T \\ \vdots \\ \left( x^{(1999)} \right)^T \\ \left( x^{(2000)} \right)^T \end{pmatrix}= \begin{pmatrix} -118.29 & 33.91 & 1416 & 38372 \\ \vdots & \vdots & \vdots & \vdots \end{pmatrix}

ŷ(i)=h(x(i)) \hat{y}^{(i)} = h(x^{(i)})

例如,如果系统预测第一区的房价中位数为158400美元,则

ŷ(1)=h(𝐱(1))=158400 \hat{y}^{(1)} = h(\mathbf{x}^{(1)}) = 158400

该地区的预测误差为

ŷ(1)y(1)=2000 \hat{y}^{(1)} - y^{(1)} = 2000

RMSE(X,h)是使用假设h在样例集上测量的代价函数。

虽然RMSE通常是回归任务的首选性能度量,但在某些情况下,你可能更喜欢使用其他函数。例如,假设有很多异常地区。在这种情况下,你可以考虑使用平均绝对误差(Mean Absolute Error,MAE,也称为平均绝对偏差)

平均绝对误差

MAE(X,h)=1mi=1m|h(𝐱(i))y(i)| \text{MAE}(X, h) = \frac{1}{m} \sum_{i=1}^{m} \left| h(\mathbf{x}^{(i)}) - y^{(i)} \right|

RMSE和MAE都是衡量两个向量(预测向量和目标向量)之间距离的方法。各种距离度量或范数是可能的:

一般而言,包含n个元素的向量v的ℓk范数定义为

𝐯k=(|𝐯1|k+|𝐯2|k++|𝐯n|k)1/k \| \mathbf{v} \|_k = \left( |\mathbf{v}_1|^k + |\mathbf{v}_2|^k + \cdots + |\mathbf{v}_n|^k \right)^{1/k}

ℓ0给出向量中的非零元素的数量,ℓ∞给出向量中的最大绝对值。

范数指数越高,它就越关注大值而忽略小值。这就是RMSE比MAE对异常值更敏感的原因。但是当异常值呈指数级减少时(例如在钟形曲线中)​,RMSE表现非常好,并且通常是首选。

假设检验

假设检验是机器学习中一种重要的统计方法,用于评估模型性能、比较不同模型之间的差异,或验证模型假设是否成立。

  1. 假设检验的目的

假设检验在机器学习中主要有以下几个用途:

  1. 常见的假设检验方法

根据具体问题和数据特性,机器学习中常用的假设检验方法包括:

  1. 假设检验的基本步骤

假设检验通常按照以下步骤进行:

  1. 在机器学习中的应用

假设检验在机器学习中有多种实际应用场景:

  1. 注意事项

在使用假设检验时,需要关注以下几点:

  1. 例子:比较两种减肥方法的有效性

背景

假设你是一个健身教练,有两种不同的减肥方法:方法A(每天跑步30分钟)和方法B(每天跳绳30分钟)。你想知道这两种方法哪一种对减肥更有效。为了验证效果,你找了20个朋友参与实验,随机分成两组:

一个月后,你记录了每组朋友的体重减少情况,结果如下:

目标

你想知道:

方法B是否真的比方法A更有效?
还是这0.5公斤的差异只是巧合?
用假设检验来解决问题
假设检验是一个科学的工具,可以帮助我们判断这种差异是不是偶然的。以下是具体步骤:

提出假设:

选择检验方法:

因为我们比较的是两组的平均减重,而且样本量较小(每组10人),可以用t-检验来分析。

计算p值:

用统计公式(这里略去复杂计算)分析两组数据:方法A平均减重2公斤,方法B平均减重2.5公斤。 假设计算后得到的p值为0.03。

做出决策:

我们设定一个标准(显著性水平),比如0.05(这是常用的门槛)。

如果p值 < 0.05,就认为差异不是偶然的,拒绝零假设。
这里p值是0.03,小于0.05,所以我们拒绝零假设。

结论

结果:方法B(平均减重2.5公斤)比方法A(平均减重2公斤)的减肥效果显著更好,0.5公斤的差异不是偶然发生的。

通俗解释:

零假设就像在说:“两种方法效果差不多,0.5公斤的差距只是运气。”
p值0.03的意思是:“如果两种方法真的一样好,我们看到这种差距的概率只有3%,很小,所以不太可能是运气。”
因为3% < 5%,我们有信心说方法B确实更有效。

现实意义

根据这个结果,你可以向客户推荐每天跳绳30分钟(方法B),因为数据表明它比跑步更有效。这种方法还能用在其他场景,比如比较两种学习方法、两种广告策略,甚至两种药物的效果。

下载数据

https://github.com/ageron/data/blob/main/housing/housing.csv

快速浏览数据结构

head() 方法查看前5行数据

import pandas as pd

housing = pd.read_csv("housing.csv")

print(housing.head())

每一行代表一个地区。有10个属性​:longitude、latitude、housing_median_age、total_rooms、total_bedrooms、population、households、median_income、median_house_value和ocean_proximity。

   longitude  latitude  housing_median_age  ...  median_income  median_house_value  ocean_proximity
0    -122.23     37.88                41.0  ...         8.3252            452600.0         NEAR BAY
1    -122.22     37.86                21.0  ...         8.3014            358500.0         NEAR BAY
2    -122.24     37.85                52.0  ...         7.2574            352100.0         NEAR BAY
3    -122.25     37.85                52.0  ...         5.6431            341300.0         NEAR BAY
4    -122.25     37.85                52.0  ...         3.8462            342200.0         NEAR BAY

[5 rows x 10 columns]

info()方法对于获取数据的快速描述很有用,特别是总行数、每个属性的类型和非空值的数量:

housing.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
 #   Column              Non-Null Count  Dtype
---  ------              --------------  -----
 0   longitude           20640 non-null  float64
 1   latitude            20640 non-null  float64
 2   housing_median_age  20640 non-null  float64
 3   total_rooms         20640 non-null  float64
 4   total_bedrooms      20433 non-null  float64
 5   population          20640 non-null  float64
 6   households          20640 non-null  float64
 7   median_income       20640 non-null  float64
 8   median_house_value  20640 non-null  float64
 9   ocean_proximity     20640 non-null  object
dtypes: float64(9), object(1)
memory usage: 1.6+ MB

使用value_counts()方法找出存在哪些类别以及每个类别有多少个

print(housing["ocean_proximity"].value_counts())
ocean_proximity
<1H OCEAN     9136
INLAND        6551
NEAR OCEAN    2658
NEAR BAY      2290
ISLAND           5
Name: count, dtype: int64

describe()方法显示数字属性的摘要

print(housing.describe())

std: 标准差

          longitude      latitude  housing_median_age  ...    households  median_income  median_house_value
count  20640.000000  20640.000000        20640.000000  ...  20640.000000   20640.000000        20640.000000
mean    -119.569704     35.631861           28.639486  ...    499.539680       3.870671       206855.816909
std        2.003532      2.135952           12.585558  ...    382.329753       1.899822       115395.615874
min     -124.350000     32.540000            1.000000  ...      1.000000       0.499900        14999.000000
25%     -121.800000     33.930000           18.000000  ...    280.000000       2.563400       119600.000000
50%     -118.490000     34.260000           29.000000  ...    409.000000       3.534800       179700.000000
75%     -118.010000     37.710000           37.000000  ...    605.000000       4.743250       264725.000000
max     -114.310000     41.950000           52.000000  ...   6082.000000      15.000100       500001.000000

[8 rows x 9 columns]

为每个数值属性绘制直方图

import pandas as pd
import matplotlib.pyplot as plt  # 导入 Matplotlib

housing = pd.read_csv("housing.csv")

# bins 决定了直方图中柱子的数量(即数据的分组数量)
# figsize 控制图表的大小(宽度和高度),单位是英寸。
housing.hist(bins=50, figsize=(12, 8))
plt.show()
直方图

创建测试集

创建测试集在理论上很简单;随机选择一些实例,通常是数据集的20%(如果你的数据集非常大,则更少)

import pandas as pd
import matplotlib.pyplot as plt  # 导入 Matplotlib
import numpy as np

housing = pd.read_csv("housing.csv")

np.random.seed(42)

def shuffle_and_split_data(data, test_ratio):
    shuffled_indices = np.random.permutation(len(data))
    test_set_size = int(len(data) * test_ratio)
    test_indices = shuffled_indices[:test_set_size]
    train_indices = shuffled_indices[test_set_size:]
    print(test_indices) # 打乱分为两组下标
    # [20046  3024 15663 ... 18086  2144  3665]
    print(train_indices)
    # [14196  8267 17445 ...  5390   860 15795]
    return data.iloc[train_indices], data.iloc[test_indices]

print(len(housing))
# 20640

train_set, test_set = shuffle_and_split_data(housing, 0.2)
len(train_set)

print(len(train_set))
# 16512
print(len(test_set))
# 4128

使用哈希值进行数据划分

为了确保数据划分的可重复性,可以基于数据的标识列(ID)使用哈希函数进行划分。以下代码展示了如何使用 zlib.crc32 函数实现这一功能:

import pandas as pd
import matplotlib.pyplot as plt  # 导入 Matplotlib
import numpy as np
from zlib import crc32

housing = pd.read_csv("housing.csv")

def is_id_in_test_set(identifier, test_ratio):
    return crc32(np.int64(identifier)) < test_ratio * 2**32

def split_data_with_id_hash(data, test_ratio, id_column):
    ids = data[id_column]
    in_test_set = ids.apply(lambda id_: is_id_in_test_set(id_, test_ratio))
    return data.loc[~in_test_set], data.loc[in_test_set]

# 首先为数据集添加索引列(index),然后基于索引列或自定义的 ID 列(通过经纬度计算)进行划分:

print(len(housing))
# 20640

housing_with_id = housing.reset_index()  # adds an `index` column

train_set, test_set = split_data_with_id_hash(housing_with_id, 0.2, "index")

# housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
# train_set, test_set = split_data_with_id_hash(housing_with_id, 0.2, "id")

len(train_set)

print(len(train_set))
# 16512
print(len(test_set))
# 4128

实用scikit-learn的随机划分

更简单的方法是使用 scikit-learn 的 train_test_split 函数进行随机划分:

import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

housing = pd.read_csv("housing.csv")

print(len(housing))
# 20640

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

print(len(train_set))
# 16512
print(len(test_set))
# 4128

# 检查缺失值

print(test_set["total_bedrooms"].isnull().sum())
# 44

在测试集中,total_bedrooms 列有 44 个缺失值:

概率计算:样本中女性比例的分布

我们已经考虑了纯随机的采样方法。如果你的数据集足够大(尤其是相对于属性的数量)​,那么这通常没问题。但如果不够大,你就有引入显著采样偏差的风险。当一家调查公司的员工决定打电话给1000个人问他们几个问题时,他们不会只是在电话簿中随机挑选1000个人。就他们想问的问题而言,他们试图确保这1000人代表全体人口。例如,美国人口中女性占51.1%,男性占48.9%,因此在美国进行一项良好的调查需要尝试在样本中保持这一比例:511名女性和489名男性(至少在答案可能因性别而异的情况下)​。这称为分层采样:将总体分为称为层的同质子组,并从每个层中抽取正确数量的实例以保证测试集能代表总体。如果进行调查的人使用纯随机采样,则大约有10.7%的机会会抽取到女性参与者少于48.5%或超过53.5%的偏差测试集。无论采用哪种方式,调查结果都可能偏差非常大。

你想计算一个随机抽样(样本量为1000人)中女性比例偏离总体女性比例(51.1%)太多的概率,具体是女性比例低于48.5%或高于53.5%的概率。这是一个统计问题,涉及二项分布,因为抽样中每个个体可以看作是“女性”或“非女性”的二元结果。

二项分布适用于以下场景:

我们需要计算样本中女性人数(成功次数)落在特定范围之外的概率,即女性人数少于485(48.5%)或多于535(53.5%)。

  1. 使用二项分布计算

使用 scipy.stats.binom 提供的 cdf() 方法计算累计概率。

from scipy.stats import binom
# 样本数量 1000
sample_size = 1000
# 定义总体女性比例
ratio_female = 0.511
# 创建一个二项分布对象 1000试验次数 每次成功概率0.511
# .cdf(x)计算累积分布函数(Cumulative Distribution Function),表示随机变量≤x的概率。
proba_too_small = binom(sample_size, ratio_female).cdf(485 - 1)
proba_too_large = 1 - binom(sample_size, ratio_female).cdf(535)
print(proba_too_small + proba_too_large)
# 输出:0.10736798530929946
  1. 使用模拟方法估计概率

通过生成大量随机样本,统计满足条件的样本比例来估计概率:

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np  # 导入 NumPy 库

np.random.seed(42)
sample_size = 1000
ratio_female = 0.511
# 100000次抽样
# 每次抽1000个
# 生成一个形状为 (100,000, 1000) 的二维数组,数组中的每个元素是 [0, 1) 区间内的均匀分布随机数。
samples = (np.random.rand(100_000, sample_size) < ratio_female).sum(axis=1)
print(((samples < 485) | (samples > 535)).mean())
# 0.1071

分层抽样与收入类别分析

  1. 创建收入类别

为了更好地分析数据分布,将 median_income 列分层为5个收入类型:

import pandas as pd
import matplotlib.pyplot as plt  # 导入 Matplotlib
import numpy as np

housing = pd.read_csv("housing.csv")

# (0, 1.5] (1.5, 3.0] (3.0, 4.5] (4.5, 6.0] (6.0, ∞)
housing["income_cat"] = pd.cut(housing["median_income"],
                               bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
                               labels=[1, 2, 3, 4, 5])

# 生成一个柱状图,显示每个收入类别的频率。
housing["income_cat"].value_counts().sort_index().plot.bar(rot=0, grid=True)

plt.xlabel("Income category")
plt.ylabel("Number of districts")
plt.show()
创建收入类别
  1. 分层抽样

为了确保训练集和测试机地收入类型分布与总体一致,使用 StratifiedShuffleSplit 进行分层抽样:

# 导入必要的库
from sklearn.model_selection import StratifiedShuffleSplit  # 用于分层随机分割数据集
import pandas as pd  # 用于数据处理
import matplotlib.pyplot as plt  # 用于数据可视化
import numpy as np  # 用于数值计算
from sklearn.model_selection import train_test_split  # 用于简单的数据集分割

# 步骤 1:加载数据集
# 从 'housing.csv' 文件中读取住房数据,存储为 pandas DataFrame
housing = pd.read_csv("housing.csv")

# 步骤 2:创建收入类别
# 根据 'median_income' 列,将连续收入值分箱为 5 个类别 (1, 2, 3, 4, 5)
# 分箱区间为 (0, 1.5], (1.5, 3.0], (3.0, 4.5], (4.5, 6.0], (6.0, ∞)
housing["income_cat"] = pd.cut(housing["median_income"],
                               bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
                               labels=[1, 2, 3, 4, 5])

# 步骤 3:使用 StratifiedShuffleSplit 进行分层抽样
# 初始化分层随机分割器,设置 10 次分割,测试集占 20%,随机种子为 42
splitter = StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)

# 创建一个列表,用于存储每次分割的训练集和测试集
strat_splits = []

# 遍历分割器生成的每次分割的训练和测试索引
for train_index, test_index in splitter.split(housing, housing["income_cat"]):
    # 根据索引提取训练集和测试集
    strat_train_set_n = housing.iloc[train_index]  # 训练集
    strat_test_set_n = housing.iloc[test_index]    # 测试集
    # 将本次分割的训练集和测试集添加到列表
    strat_splits.append([strat_train_set_n, strat_test_set_n])

# 步骤 4:选择第一次分割的训练集和测试集
strat_train_set, strat_test_set = strat_splits[0]

# 步骤 5:打印训练集和测试集的大小
print(len(strat_train_set))  # 输出训练集行数,预期为 16512
print(len(strat_test_set))   # 输出测试集行数,预期为 4128

# 步骤 6:使用 train_test_split 进行更简洁的分层抽样
# 直接使用 train_test_split,设置测试集占 20%,按 'income_cat' 分层,随机种子为 42
strat_train_set, strat_test_set = train_test_split(
    housing, test_size=0.2, stratify=housing["income_cat"], random_state=42)

# 步骤 7:再次打印训练集和测试集的大小
print(len(strat_train_set))  # 输出训练集行数,预期为 16512
print(len(strat_test_set))   # 输出测试集行数,预期为 4128

查看测试集中收入类型的比列

strat_test_set["income_cat"].value_counts() / len(strat_test_set)
# 输出:
# 3    0.350533
# 2    0.318798
# 4    0.176357
# 5    0.114341
# 1    0.039971
# Name: income_cat, dtype: float64
  1. 比较随机抽样与分层抽样的效果

通过比较总体、分层抽样和随机抽样的收入类别比例,评估分层抽样的效果:

from sklearn.model_selection import StratifiedShuffleSplit  # 用于分层随机分割数据集
import pandas as pd  # 用于数据处理
import matplotlib.pyplot as plt  # 用于数据可视化
import numpy as np  # 用于数值计算
from sklearn.model_selection import train_test_split  # 用于简单的数据集分割

housing = pd.read_csv("housing.csv")
housing["income_cat"] = pd.cut(housing["median_income"],
                               bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
                               labels=[1, 2, 3, 4, 5])

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

splitter = StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)

strat_splits = []
for train_index, test_index in splitter.split(housing, housing["income_cat"]):
    strat_train_set_n = housing.iloc[train_index].copy()
    strat_test_set_n = housing.iloc[test_index].copy()
    strat_splits.append([strat_train_set_n, strat_test_set_n])

strat_train_set, strat_test_set = strat_splits[0]

def income_cat_proportions(data):
    return data["income_cat"].value_counts() / len(data)

compare_props = pd.DataFrame({
    "Overall %": income_cat_proportions(housing),
    "Stratified %": income_cat_proportions(strat_test_set),
    "Random %": income_cat_proportions(test_set),
}).sort_index()

compare_props.index.name = "Income Category"
compare_props["Strat. Error %"] = (compare_props["Stratified %"] /
                                   compare_props["Overall %"] - 1)
compare_props["Rand. Error %"] = (compare_props["Random %"] /
                                  compare_props["Overall %"] - 1)
print((compare_props * 100).round(2))

#                  Overall %  Stratified %  Random %  Strat. Error %  Rand. Error %
# Income Category
# 1                     3.98          4.00      4.24            0.36           6.45
# 2                    31.88         31.88     30.74           -0.02          -3.59
# 3                    35.06         35.05     34.52           -0.01          -1.53
# 4                    17.63         17.64     18.41            0.03           4.42
# 5                    11.44         11.43     12.09           -0.08           5.63

for set_ in (strat_train_set, strat_test_set):
    set_.drop("income_cat", axis=1, inplace=True)

分析

注意事项:随机性来源

可视化地理数据

经纬度可视化

from sklearn.model_selection import StratifiedShuffleSplit
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from sklearn.model_selection import train_test_split

def get_housing_set():
    housing = pd.read_csv("housing.csv")
    housing["income_cat"] = pd.cut(housing["median_income"],
                                   bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
                                   labels=[1, 2, 3, 4, 5])

    splitter = StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)

    strat_splits = []
    for train_index, test_index in splitter.split(housing, housing["income_cat"]):
        strat_train_set_n = housing.iloc[train_index].copy()
        strat_test_set_n = housing.iloc[test_index].copy()
        strat_splits.append([strat_train_set_n, strat_test_set_n])

    strat_train_set, strat_test_set = strat_splits[0]

    for set_ in (strat_train_set, strat_test_set):
        set_.drop("income_cat", axis=1, inplace=True)
    return strat_train_set, strat_test_set

strat_train_set, strat_test_set = get_housing_set()
strat_train_set_copy = strat_train_set.copy()
strat_test_set_copy = strat_test_set.copy()

strat_train_set_copy.plot(kind="scatter", x="longitude", y="latitude", grid=True)
plt.show()

strat_test_set_copy.plot(kind="scatter", x="longitude", y="latitude", grid=True)
plt.show()
训练集经纬度
# 房子多的地方颜色更重
strat_train_set_copy.plot(kind="scatter", x="longitude", y="latitude", alpha=0.2)
plt.show()
房子多的地方颜色更重
# 根据人口决定图中点的颜色
strat_train_set_copy.plot(kind="scatter", x="longitude", y="latitude", grid=True,
             s=strat_train_set_copy["population"] / 100, label="population",
             c="median_house_value", cmap="jet", colorbar=True,
             legend=True, sharex=False, figsize=(10, 7))
plt.show()
房子多的地方颜色更重

书中作者还搞了个有意思的,以加州地图为画布,把这些点画在加州地图上,可视化效果更加形象。

寻找相关性

实验不同属性组合

为机器学习算法准备数据

清洗数据

处理文本和类别属性

特征缩放和转换

定制转换器

转换流水线

选择和训练模型

在训练集上训练和评估

使用交叉验证进行更好的评估

微调模型

网格搜索

随机搜索

集成方法

分析最佳模型及其错误

在测试集上评估系统

启动、监控和维护系统