https://github.com/ageron/handson-ml3/blob/main/02_end_to_end_machine_learning_project.ipynb
什么是端到端机器学习?🤔
端到端机器学习(End-to-End Machine Learning) 是指在一个机器学习项目中,整个流程从数据收集、预处理、模型训练到最终预测结果的输出都由同一个系统或过程自动化完成,且通常是一个整体性的解决方案。简单来说,它就是将各个步骤(如特征工程、模型选择、训练、调优等)整合成一个统一的系统,减少人工干预。
这种方法的优点是能够简化工作流程,提高效率。尤其在深度学习中,端到端模型往往能够直接从原始数据中学习到有用的特征,而不需要人工进行繁琐的特征工程。
举个例子:
假设你想训练一个自动驾驶的系统:
端到端机器学习的典型应用领域有:
这种方法能够减少很多手动的中间环节,但它也要求系统能够处理数据的复杂性和大量的计算。
流行的开放数据存储库
我们将使用来自StatLib存储库的加州房价数据,数据集基于1990年加州人口普查的数据。
回归问题的典型性能度量是均方根误差(Root Mean Square Error,RMSE)。
例如,如果第一个地区如前所述,则矩阵X如下所示:
例如,如果系统预测第一区的房价中位数为158400美元,则
该地区的预测误差为
RMSE(X,h)是使用假设h在样例集上测量的代价函数。
虽然RMSE通常是回归任务的首选性能度量,但在某些情况下,你可能更喜欢使用其他函数。例如,假设有很多异常地区。在这种情况下,你可以考虑使用平均绝对误差(Mean Absolute Error,MAE,也称为平均绝对偏差)
RMSE和MAE都是衡量两个向量(预测向量和目标向量)之间距离的方法。各种距离度量或范数是可能的:
一般而言,包含n个元素的向量v的ℓk范数定义为
ℓ0给出向量中的非零元素的数量,ℓ∞给出向量中的最大绝对值。
范数指数越高,它就越关注大值而忽略小值。这就是RMSE比MAE对异常值更敏感的原因。但是当异常值呈指数级减少时(例如在钟形曲线中),RMSE表现非常好,并且通常是首选。
假设检验是机器学习中一种重要的统计方法,用于评估模型性能、比较不同模型之间的差异,或验证模型假设是否成立。
假设检验在机器学习中主要有以下几个用途:
根据具体问题和数据特性,机器学习中常用的假设检验方法包括:
假设检验通常按照以下步骤进行:
假设检验在机器学习中有多种实际应用场景:
在使用假设检验时,需要关注以下几点:
背景
假设你是一个健身教练,有两种不同的减肥方法:方法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
= pd.read_csv("housing.csv")
housing
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
= pd.read_csv("housing.csv")
housing
# bins 决定了直方图中柱子的数量(即数据的分组数量)
# figsize 控制图表的大小(宽度和高度),单位是英寸。
=50, figsize=(12, 8))
housing.hist(bins plt.show()
创建测试集在理论上很简单;随机选择一些实例,通常是数据集的20%(如果你的数据集非常大,则更少)
import pandas as pd
import matplotlib.pyplot as plt # 导入 Matplotlib
import numpy as np
= pd.read_csv("housing.csv")
housing
42)
np.random.seed(
def shuffle_and_split_data(data, test_ratio):
= np.random.permutation(len(data))
shuffled_indices = int(len(data) * test_ratio)
test_set_size = shuffled_indices[:test_set_size]
test_indices = shuffled_indices[test_set_size:]
train_indices 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
= shuffle_and_split_data(housing, 0.2)
train_set, test_set 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
= pd.read_csv("housing.csv")
housing
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):
= data[id_column]
ids = ids.apply(lambda id_: is_id_in_test_set(id_, test_ratio))
in_test_set return data.loc[~in_test_set], data.loc[in_test_set]
# 首先为数据集添加索引列(index),然后基于索引列或自定义的 ID 列(通过经纬度计算)进行划分:
print(len(housing))
# 20640
= housing.reset_index() # adds an `index` column
housing_with_id
= split_data_with_id_hash(housing_with_id, 0.2, "index")
train_set, test_set
# 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 的 train_test_split 函数进行随机划分:
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
= pd.read_csv("housing.csv")
housing
print(len(housing))
# 20640
= train_test_split(housing, test_size=0.2, random_state=42)
train_set, test_set
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%)。
使用 scipy.stats.binom
提供的 cdf()
方法计算累计概率。
from scipy.stats import binom
# 样本数量 1000
= 1000
sample_size # 定义总体女性比例
= 0.511
ratio_female # 创建一个二项分布对象 1000试验次数 每次成功概率0.511
# .cdf(x)计算累积分布函数(Cumulative Distribution Function),表示随机变量≤x的概率。
= binom(sample_size, ratio_female).cdf(485 - 1)
proba_too_small = 1 - binom(sample_size, ratio_female).cdf(535)
proba_too_large print(proba_too_small + proba_too_large)
# 输出:0.10736798530929946
通过生成大量随机样本,统计满足条件的样本比例来估计概率:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np # 导入 NumPy 库
42)
np.random.seed(= 1000
sample_size = 0.511
ratio_female # 100000次抽样
# 每次抽1000个
# 生成一个形状为 (100,000, 1000) 的二维数组,数组中的每个元素是 [0, 1) 区间内的均匀分布随机数。
= (np.random.rand(100_000, sample_size) < ratio_female).sum(axis=1)
samples print(((samples < 485) | (samples > 535)).mean())
# 0.1071
为了更好地分析数据分布,将 median_income 列分层为5个收入类型:
import pandas as pd
import matplotlib.pyplot as plt # 导入 Matplotlib
import numpy as np
= pd.read_csv("housing.csv")
housing
# (0, 1.5] (1.5, 3.0] (3.0, 4.5] (4.5, 6.0] (6.0, ∞)
"income_cat"] = pd.cut(housing["median_income"],
housing[=[0., 1.5, 3.0, 4.5, 6., np.inf],
bins=[1, 2, 3, 4, 5])
labels
# 生成一个柱状图,显示每个收入类别的频率。
"income_cat"].value_counts().sort_index().plot.bar(rot=0, grid=True)
housing[
"Income category")
plt.xlabel("Number of districts")
plt.ylabel( plt.show()
为了确保训练集和测试机地收入类型分布与总体一致,使用
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
= pd.read_csv("housing.csv")
housing
# 步骤 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, ∞)
"income_cat"] = pd.cut(housing["median_income"],
housing[=[0., 1.5, 3.0, 4.5, 6., np.inf],
bins=[1, 2, 3, 4, 5])
labels
# 步骤 3:使用 StratifiedShuffleSplit 进行分层抽样
# 初始化分层随机分割器,设置 10 次分割,测试集占 20%,随机种子为 42
= StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)
splitter
# 创建一个列表,用于存储每次分割的训练集和测试集
= []
strat_splits
# 遍历分割器生成的每次分割的训练和测试索引
for train_index, test_index in splitter.split(housing, housing["income_cat"]):
# 根据索引提取训练集和测试集
= housing.iloc[train_index] # 训练集
strat_train_set_n = housing.iloc[test_index] # 测试集
strat_test_set_n # 将本次分割的训练集和测试集添加到列表
strat_splits.append([strat_train_set_n, strat_test_set_n])
# 步骤 4:选择第一次分割的训练集和测试集
= strat_splits[0]
strat_train_set, strat_test_set
# 步骤 5:打印训练集和测试集的大小
print(len(strat_train_set)) # 输出训练集行数,预期为 16512
print(len(strat_test_set)) # 输出测试集行数,预期为 4128
# 步骤 6:使用 train_test_split 进行更简洁的分层抽样
# 直接使用 train_test_split,设置测试集占 20%,按 'income_cat' 分层,随机种子为 42
= train_test_split(
strat_train_set, strat_test_set =0.2, stratify=housing["income_cat"], random_state=42)
housing, test_size
# 步骤 7:再次打印训练集和测试集的大小
print(len(strat_train_set)) # 输出训练集行数,预期为 16512
print(len(strat_test_set)) # 输出测试集行数,预期为 4128
查看测试集中收入类型的比列
"income_cat"].value_counts() / len(strat_test_set)
strat_test_set[# 输出:
# 3 0.350533
# 2 0.318798
# 4 0.176357
# 5 0.114341
# 1 0.039971
# Name: income_cat, dtype: float64
通过比较总体、分层抽样和随机抽样的收入类别比例,评估分层抽样的效果:
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 # 用于简单的数据集分割
= pd.read_csv("housing.csv")
housing "income_cat"] = pd.cut(housing["median_income"],
housing[=[0., 1.5, 3.0, 4.5, 6., np.inf],
bins=[1, 2, 3, 4, 5])
labels
= train_test_split(housing, test_size=0.2, random_state=42)
train_set, test_set
= StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)
splitter
= []
strat_splits for train_index, test_index in splitter.split(housing, housing["income_cat"]):
= housing.iloc[train_index].copy()
strat_train_set_n = housing.iloc[test_index].copy()
strat_test_set_n
strat_splits.append([strat_train_set_n, strat_test_set_n])
= strat_splits[0]
strat_train_set, strat_test_set
def income_cat_proportions(data):
return data["income_cat"].value_counts() / len(data)
= pd.DataFrame({
compare_props "Overall %": income_cat_proportions(housing),
"Stratified %": income_cat_proportions(strat_test_set),
"Random %": income_cat_proportions(test_set),
}).sort_index()
= "Income Category"
compare_props.index.name "Strat. Error %"] = (compare_props["Stratified %"] /
compare_props["Overall %"] - 1)
compare_props["Rand. Error %"] = (compare_props["Random %"] /
compare_props["Overall %"] - 1)
compare_props[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):
"income_cat", axis=1, inplace=True) set_.drop(
分析
经纬度可视化
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():
= pd.read_csv("housing.csv")
housing "income_cat"] = pd.cut(housing["median_income"],
housing[=[0., 1.5, 3.0, 4.5, 6., np.inf],
bins=[1, 2, 3, 4, 5])
labels
= StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)
splitter
= []
strat_splits for train_index, test_index in splitter.split(housing, housing["income_cat"]):
= housing.iloc[train_index].copy()
strat_train_set_n = housing.iloc[test_index].copy()
strat_test_set_n
strat_splits.append([strat_train_set_n, strat_test_set_n])
= strat_splits[0]
strat_train_set, strat_test_set
for set_ in (strat_train_set, strat_test_set):
"income_cat", axis=1, inplace=True)
set_.drop(return strat_train_set, strat_test_set
= get_housing_set()
strat_train_set, strat_test_set = strat_train_set.copy()
strat_train_set_copy = strat_test_set.copy()
strat_test_set_copy
="scatter", x="longitude", y="latitude", grid=True)
strat_train_set_copy.plot(kind
plt.show()
="scatter", x="longitude", y="latitude", grid=True)
strat_test_set_copy.plot(kind plt.show()
# 房子多的地方颜色更重
="scatter", x="longitude", y="latitude", alpha=0.2)
strat_train_set_copy.plot(kind plt.show()
# 根据人口决定图中点的颜色
="scatter", x="longitude", y="latitude", grid=True,
strat_train_set_copy.plot(kind=strat_train_set_copy["population"] / 100, label="population",
s="median_house_value", cmap="jet", colorbar=True,
c=True, sharex=False, figsize=(10, 7))
legend plt.show()
书中作者还搞了个有意思的,以加州地图为画布,把这些点画在加州地图上,可视化效果更加形象。