线性回归:编程练习#
以下为本站根据 Google 机器学习速成课程源示例代码内容整理而来,同时对其中的部分内容进行了梳理和补充,完整示例代码见文末。
线性回归#
在本节内容中,你将使用一份真实数据集训练一个模型,用于预测美国伊利诺伊州芝加哥市的出租车车费。
学习目标#
完成本节内容后,你将能够:
-
将
.csv文件读入 pandas 得到 DataFrame 格式的数据。 -
使用 Python 可视化库探索数据集。
-
尝试不同的特征来构建线性回归模型。
-
调整模型的超参数。
-
使用均方根误差(RMSE)和损失曲线比较训练结果。
数据集描述#
本练习的数据集源自芝加哥市出租车行程数据集,是其中一个子集,后续我们重点关注 2022 年 5 月的两天数据。我们提供的示例代码中也已经包含了下载后的数据集。
Part 1 — 准备工作#
加载所需模块#
本练习依赖若干 Python 库,分别用于数据处理、机器学习与可视化,因此我们需要先安装对应环境。对于 Python 环境的整体安装过程,如果有不熟悉的读者,可以参见文章 Conda安装教程:机器学习环境配置与使用入门中的介绍内容。
在完成 Python 安装以后,下面开始安装后续需要用到的相关 Python 库。
- 安装所需库
#@title Install required libraries
pip install google-ml-edu==0.1.3 \
keras~=3.8.0 \
matplotlib~=3.10.0 \
numpy~=2.0.0 \
pandas~=2.2.0 \
tensorflow~=2.18.0
print('\n\n所有依赖已安装成功。')我们已经将实际测试时的Python环境导出成了
requirements.txt文件放在了代码文件中,Python 安装完成后 通过pip install -r requirements.txt命令即可完成整个环境的安装。
- 加载依赖
#@title 代码 - 加载依赖
# 数据
import numpy as np
import pandas as pd
# 机器学习
import keras
import ml_edu.experiment
import ml_edu.results
# 数据可视化
import plotly.express as px加载数据集#
下方的代码块是用于加载原始数据集,并创建生成一个 pandas DataFrame 类型的对象。
# @title
chicago_taxi_dataset = pd.read_csv("chicago_taxi_train.csv")你可以把 DataFrame 想象成一个由行和列组成的电子表格:行代表单个数据样本,列代表该样本对应的各个属性(特征),即 $m$ 行 $n$ 列的数据。
更新 DataFrame#
下方代码块把 DataFrame 裁剪为仅保留原数据集中的若干列,也就是特征筛选的过程,这一步后将会得到一个 $m$ 行 $k$ 列($k < n$)的数据。关于特征选择的简单原则,可以参见文章 「随机森林实战:泰坦尼克号生还预测示例」 中的内容。
#@title 代码 - 读取数据集
# 仅保留指定的若干列。
training_df = chicago_taxi_dataset.loc[:, ('TRIP_MILES', 'TRIP_SECONDS', 'FARE', 'COMPANY', 'PAYMENT_TYPE', 'TIP_RATE')]
print('数据集读取成功。')
print('总行数:{0}\n\n'.format(len(training_df.index)))
training_df.head(200)注意,运行结果只会显示数据集的一个采样(200行),但已经足够让你看清每一列对应什么特征,以及前若干条样本的实际取值。
以下为上述代码的运行结果:
第一步:载入原始数据集
Read dataset completed successfully.
Total number of rows: 31694
TRIP_MILES TRIP_SECONDS ... PAYMENT_TYPE TIP_RATE
0 2.57 2341 ... Mobile 6.3
1 1.18 1074 ... Credit Card 27.9
2 1.29 1173 ... Cash 0.0
3 3.70 3360 ... Cash 0.0
4 1.15 1044 ... Cash 0.0
.. ... ... ... ... ...
195 1.13 821 ... Mobile 22.9
196 0.57 414 ... Cash 0.0
197 1.22 886 ... Cash 0.0
198 1.68 1219 ... Mobile 23.0
199 0.71 515 ... Credit Card 18.2
[200 rows x 6 columns]Part 2 — 数据集探查#
查看数据集统计信息#
绝大多数机器学习项目都会花大量时间在「认识数据」上。这一步将使用 DataFrame.describe 方法查看数据集的描述性统计信息,并回答若干关键问题。
操作步骤
- 运行下方的「查看数据集统计信息」代码块;
- 观察输出,并尝试回答以下问题:
- 车费(FARE)的最大值是多少?
- 所有行程的平均距离是多少?
- 数据集中一共有多少家出租车公司?
- 最常见的支付方式是什么?
- 是否存在缺失值的特征?
#@title 代码 - 查看数据集统计信息
print('总行数:{0}\n\n'.format(len(training_df.index)))
training_df.describe(include='all')以下为上述代码的运行结果:
第二步:原始数据集探查
Total number of rows: 31694
TRIP_MILES TRIP_SECONDS ... PAYMENT_TYPE TIP_RATE
count 31694.000000 31694.000000 ... 31694 31694.000000
unique NaN NaN ... 7 NaN
top NaN NaN ... Credit Card NaN
freq NaN NaN ... 14142 NaN
mean 8.289463 1319.796397 ... NaN 12.965785
std 7.265672 928.932873 ... NaN 15.517765
min 0.500000 60.000000 ... NaN 0.000000
25% 1.720000 548.000000 ... NaN 0.000000
50% 5.920000 1081.000000 ... NaN 12.200000
75% 14.500000 1888.000000 ... NaN 20.800000
max 68.120000 7140.000000 ... NaN 648.600000
[11 rows x 6 columns]此时,你也许会好奇输出里为什么会出现成片的
NaN(not a number)。在 Python 中处理数据时,如果某个计算无法执行,或者存在缺失信息,就会出现这个值。例如在出租车数据里,PAYMENT_TYPE与COMPANY是非数值型的类别特征;对类别特征来说,「均值」「最大值」没有意义,所以输出会显示为NaN。
点击查看数据集统计信息答案
#@title 双击或运行以查看数据集统计信息答案
answer = '''
车费最大值是多少? 答案:$159.25
所有行程的平均距离是多少? 答案:8.2895 miles
数据集中一共有多少家出租车公司? 答案:31
最常见的支付方式是什么? 答案:Credit Card
是否存在缺失值的特征? 答案:无
'''
# 你应该能通过查看运行 DataFrame describe 方法后输出的表格来回答这些问题。生成相关性矩阵#
机器学习中一项关键工作是判断哪些特征与标签相关。如果你以前打过车,经验多半会告诉你:车费通常与行驶距离和行程时长有关。不过,有没有更系统的方法来判断这些特征与车费的相关程度呢?
这一步将使用相关性矩阵来识别与标签强相关的特征。相关系数的取值含义如下:
-
1.0:完全正相关——一个属性上升,另一个属性也上升。 -
-1.0:完全负相关——一个属性上升,另一个属性下降。 -
0.0:不相关——两列之间不存在线性关系。
一般来说,相关系数的绝对值越大,预测能力就越强。
操作步骤
- 阅读下方的「查看相关性矩阵」代码块;
- 运行它,观察输出;
- 试着回答以下问题:
- 哪个特征与标签
FARE的相关性最强? - 哪个特征与标签
FARE的相关性最弱?
- 哪个特征与标签
#@title 代码 - 查看相关性矩阵
training_df.corr(numeric_only=True)点击查看相关性矩阵答案
#@title 双击以查看相关性矩阵答案
# 哪个特征与标签 FARE 的相关性最强?
answer = '''
与 FARE 相关性最强的特征是 TRIP_MILES。
正如你所预期的那样,TRIP_MILES 是一个很好的训练起点。
另外请注意,TRIP_SECONDS 这个特征与车费之间也有很强的相关性。
'''
print(answer)
# 哪个特征与标签 FARE 的相关性最弱?
answer = '''与 FARE 相关性最弱的特征是 TIP_RATE。'''
print(answer)输出结果:
TRIP_MILES TRIP_SECONDS FARE TIP_RATE
TRIP_MILES 1.000000 0.800855 0.975344 -0.049594
TRIP_SECONDS 0.800855 1.000000 0.830292 -0.084294
FARE 0.975344 0.830292 1.000000 -0.070979
TIP_RATE -0.049594 -0.084294 -0.070979 1.000000可视化数据集中的关系#
有时候,把特征之间的关系画出来会很有帮助。一个常用的工具就是 pair plot。pair plot 会生成一个网格状的成对图,把每个特征与其他所有特征的两两关系一次性可视化。
#@title 代码 - 查看 pairplot
px.scatter_matrix(training_df, dimensions=["FARE", "TRIP_MILES", "TRIP_SECONDS"])运行结果为:
Part 3 — 训练模型#
定义构建与训练模型的函数#
构建并训练模型的代码都在「定义机器学习函数」代码块里。如果你想研究具体实现,可以展开该代码块查看。
#@title 代码 - 定义机器学习函数
def create_model(
settings: ml_edu.experiment.ExperimentSettings,
metrics: list[keras.metrics.Metric],
) -> keras.Model:
"""创建并编译一个简单的线性回归模型。"""
# 描述模型的结构。
# 简单线性回归模型的结构是单层中的单个节点。
inputs = {name: keras.Input(shape=(1,), name=name) for name in settings.input_features}
concatenated_inputs = keras.layers.Concatenate()(list(inputs.values()))
outputs = keras.layers.Dense(units=1)(concatenated_inputs)
model = keras.Model(inputs=inputs, outputs=outputs)
# 把模型结构编译成 Keras 能够高效执行的代码。
# 配置训练目标为最小化模型的均方误差。
model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=settings.learning_rate),
loss="mean_squared_error",
metrics=metrics)
return model
def train_model(
experiment_name: str,
model: keras.Model,
dataset: pd.DataFrame,
label_name: str,
settings: ml_edu.experiment.ExperimentSettings,
) -> ml_edu.experiment.Experiment:
"""通过给模型喂数据来训练它。"""
features = {name: dataset[name].values for name in settings.input_features}
label = dataset[label_name].values
history = model.fit(x=features,
y=label,
batch_size=settings.batch_size,
epochs=settings.number_epochs)
return ml_edu.experiment.Experiment(
name=experiment_name,
settings=settings,
model=model,
epochs=history.epoch,
metrics_history=pd.DataFrame(history.history),
)
print("成功:线性回归函数定义完成。")训练一个使用单一特征的模型(Experiment 1)#
这一步先用单一特征训练一个预测车费的模型。根据前面的相关性分析显示 TRIP_MILES(行驶距离)与 FARE 的相关性最强,所以先以它作为输入特征。
操作步骤
- 运行下方的「实验 1」代码块,使用一个特征变量构建模型;
- 检查训练输出;
- 回答以下问题:
- 大约多少个 epoch 之后损失收敛到最终模型?
- 模型对样本数据的拟合程度如何?
训练过程中,输出里会显示 RMSE(均方根误差)。RMSE 的单位与标签的单位相同(美元)。也就是说,RMSE 可以告诉你模型预测的车费与真实车费之间,平均偏离了多少美元。
#@title 代码 - 实验 1
# 以下变量为超参数。
settings_1 = ml_edu.experiment.ExperimentSettings(
learning_rate = 0.001,
number_epochs = 20,
batch_size = 50,
input_features = ['TRIP_MILES']
)
metrics = [keras.metrics.RootMeanSquaredError(name='rmse')]
model_1 = create_model(settings_1, metrics)
experiment_1 = train_model('one_feature', model_1, training_df, 'FARE', settings_1)
ml_edu.results.plot_experiment_metrics(experiment_1, ['rmse'])
ml_edu.results.plot_model_predictions(experiment_1, training_df, 'FARE')实验室 1 运行输出结果:
Epoch 1/20
634/634 ━━━━━━━━━━━━━━━━━━━━ 0s 252us/step - loss: 212.5943 - rmse: 14.5806
Epoch 2/20
......
Epoch 19/20
634/634 ━━━━━━━━━━━━━━━━━━━━ 0s 188us/step - loss: 14.0319 - rmse: 3.7459
Epoch 20/20
634/634 ━━━━━━━━━━━━━━━━━━━━ 0s 190us/step - loss: 14.0309 - rmse: 3.7458
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step可视化结果:
点击查看实验 1 答案
#@title 双击以查看单特征训练模型答案
# 大约多少个 epoch 之后损失收敛到最终模型?
answer = """
通过损失曲线,可以观察到训练过程中损失开始趋于平缓的位置。
在下面这组超参数下:
learning_rate = 0.001
epochs = 20
batch_size = 50
训练大约需要 5 个 epoch 收敛到最终模型。
"""
print(answer)
# 模型对样本数据的拟合程度如何?
answer = '''
从模型图来看,模型对样本数据的拟合相当不错。
'''
print(answer)实验不同的超参数#
在机器学习中,「反复试验」是常态——往往要跑多组实验才能找到最合适的超参数组合。这一步请按下面的实验方案,每次只调整一个超参数:
-
实验 1:把学习率调大到
1.0(batch size 保持50)。 -
实验 2:把学习率调小到
0.0001(batch size 保持50)。 -
实验 3:把 batch size 调大到
500(学习率保持0.001)。
操作步骤
- 调整下方的 实验 2 代码块中的超参数值;
- 运行该代码块;
- 观察损失曲线与模型输出的变化;
- 对每一组超参数实验重复以上两步;
- 回答以下问题:
- 调大学习率对模型训练有什么影响?
- 调小学习率对模型训练有什么影响?
- 改变 batch size 是否影响训练结果?
#@title 代码 - 实验 2
# 以下变量为超参数。
# TODO - 调整这些超参数,观察它们对训练运行的影响。
settings_2 = ml_edu.experiment.ExperimentSettings(
learning_rate = 0.001,
number_epochs = 20,
batch_size = 50,
input_features = ['TRIP_MILES']
)
metrics = [keras.metrics.RootMeanSquaredError(name='rmse')]
model_2 = create_model(settings_2, metrics)
experiment_2 = train_model('one_feature_hyper', model_2, training_df, 'FARE', settings_2)
ml_edu.results.plot_experiment_metrics(experiment_2, ['rmse'])
ml_edu.results.plot_model_predictions(experiment_2, training_df, 'FARE')点击查看超参数实验答案
#@title 双击以查看超参数实验答案
# 调大学习率对模型训练有什么影响?
answer = """
当学习率过高时,损失曲线会上下跳动,看上去并没有随着迭代向收敛方向移动。
此外,预测模型对数据的拟合也不太好。学习率过高的情况下,几乎不可能训练出效果良好的模型。
"""
print(answer)
# 调小学习率对模型训练有什么影响?
answer = '''
当学习率过小时,损失曲线收敛所需的迭代次数会更多。学习率较小时,损失曲线下降缓慢,
没有出现明显的断崖式下降或趋于平缓的情况。学习率较小时,可以增大 epoch 数量让模型
最终收敛,但训练时间会更长。
'''
print(answer)
# 改变 batch size 是否影响训练结果?
answer = '''
增大 batch size 会让每个 epoch 跑得更快,但和较小学习率的情况类似,仅用 20 个 epoch
模型不会收敛。如果有时间,可以尝试增大 epoch 数量,最终你会看到模型收敛。
'''
print(answer)训练一个使用两个特征的模型(Experiment 3)#
用单一特征训练的模型已经具备不错的预测能力,但还能更好吗?这一步使用 TRIP_MILES 与 TRIP_MINUTES 这两个特征训练模型,看是否能进一步提升性能。原数据集中并没有 TRIP_MINUTES,但可以像下面这样从 TRIP_SECONDS 派生出来。
操作步骤
- 阅读 实验 3 代码块;
- 运行该代码块;
- 回答以下问题:
- 使用两个特征的模型是否比单特征模型更好?
- 用
TRIP_SECONDS代替TRIP_MINUTES是否有差别? - 训练得到的模型与芝加哥出租车车费的「真值」计算公式吻合得如何?
下方散点图是三维(3-D)图:两个特征
TRIP_MILES、TRIP_MINUTES分别位于 x、y 轴,标签FARE位于 z 轴。数据集中的样本用圆点表示,模型用一个平面表示。如果模型训练得好,绝大多数样本应该落在平面上。3-D 图是可交互的,你可以点击或拖动进一步探索数据。
#@title 代码 - 实验 3
# 以下变量为超参数。
settings_3 = ml_edu.experiment.ExperimentSettings(
learning_rate = 0.001,
number_epochs = 20,
batch_size = 50,
input_features = ['TRIP_MILES', 'TRIP_MINUTES']
)
training_df['TRIP_MINUTES'] = training_df['TRIP_SECONDS']/60
metrics = [keras.metrics.RootMeanSquaredError(name='rmse')]
model_3 = create_model(settings_3, metrics)
experiment_3 = train_model('two_features', model_3, training_df, 'FARE', settings_3)
ml_edu.results.plot_experiment_metrics(experiment_3, ['rmse'])
ml_edu.results.plot_model_predictions(experiment_3, training_df, 'FARE')可视化结果:
点击查看实验 3 答案
#@title 双击以查看双特征训练答案
# 使用两个特征的模型是否比使用单一特征的模型效果更好?
answer = '''
要回答这个问题,请对比各个模型的 RMSE。例如,如果单特征模型训练得到的 RMSE 为 3.7457,
而双特征模型的 RMSE 为 3.4787,那 意味着双特征模型的预测结果平均比单特征模型更接近
观测到的车费约 0.27 美元。
'''
print(answer)
# 用 TRIP_SECONDS 代替 TRIP_MINUTES 是否有差别?
answer = '''
当训练具有多个特征的模型时,重要的是所有数值特征的取值范围大致在同一量级上。
本例中,TRIP_SECONDS 和 TRIP_MILES 不满足这个要求:TRIP_MILES 的均值为 8.3,
而 TRIP_SECONDS 的均值为 1,320,相差两个数量级。相比之下,TRIP_MINUTES 的均值为 22,
与 TRIP_MILES(8.3)的量级更接近,而非 TRIP_SECONDS(1,320)。当然,这只是训练前对
特征做缩放的一种方式,你会在另一个模块中学习到更多方法。
'''
print(answer)
# 你认为模型对芝加哥出租车车费真值公式的拟合程度如何?
answer = '''
事实上,芝加哥的出租车使用一份公开的公式来确定车费。对单个现金支付的乘客来说,
车费按下面这种方式计算:
FARE = 2.25 * TRIP_MILES + 0.12 * TRIP_MINUTES + 3.25
通常在机器学习问题中你并不知道"正确"的公式,但本例中你可以用这个公式来评估模型。
看一下模型的输出(权重和偏置),看它和车费真值公式的吻合程度。你会发现模型大致接近这个公式。
'''
print(answer)比较实验#
ml_edu.results.compare_experiment([experiment_1, experiment_3], ['rmse'], training_df, training_df['FARE'].values)对比可视化结果:
Part 4 — 验证模型#
使用模型进行预测#
现在你已经训练好了一个模型,可以拿它来做预测了。在实际项目中,应该使用训练时没有见过的样本来做预测;但在本次练习中,我们只使用训练集的一个子集做演示。后续的练习会介绍在未见样本上做预测的方法。
操作步骤
- 运行下方的「定义预测函数」代码块;
- 运行下方的『进行预测』代码块;
- 检查预测输出;
- 回答以下问题:
- 预测值与真实值相差多少?换句话说,模型对出租车车费的预测准不准?
#@title 代码 - 定义预测函数
def format_currency(x):
return "${:.2f}".format(x)
def build_batch(df, batch_size):
batch = df.sample(n=batch_size).copy()
batch.set_index(np.arange(batch_size), inplace=True)
return batch
def predict_fare(model, df, features, label, batch_size=50):
batch = build_batch(df, batch_size)
predicted_values = model.predict_on_batch(x={name: batch[name].values for name in features})
data = {"PREDICTED_FARE": [], "OBSERVED_FARE": [], "L1_LOSS": [],
features[0]: [], features[1]: []}
for i in range(batch_size):
predicted = predicted_values[i][0]
observed = batch.at[i, label]
data["PREDICTED_FARE"].append(format_currency(predicted))
data["OBSERVED_FARE"].append(format_currency(observed))
data["L1_LOSS"].append(format_currency(abs(observed - predicted)))
data[features[0]].append(batch.at[i, features[0]])
data[features[1]].append("{:.2f}".format(batch.at[i, features[1]]))
output_df = pd.DataFrame(data)
return output_df
def show_predictions(output):
header = "-" * 80
banner = header + "\n" + "|" + "预测结果".center(78) + "|" + "\n" + header
print(banner)
print(output)
return#@title 代码 - 进行预测
output = predict_fare(experiment_3.model, training_df, experiment_3.settings.input_features, 'FARE')
show_predictions(output)
点击查看验证模型答案
#@title 双击以查看验证模型答案
# 预测值与真实值相差多少?
answer = '''
从随机抽样的样例来看,模型在预测出租车车费方面表现得相当不错。绝大多数预测值与
观测值相差不大。你可以通过观察 L1_LOSS = |观测值 − 预测值| 这一列来确认这一点。
'''以上所有结果,按以下顺序运行即可,完整示例代码见
if __name__ == '__main__':
print("---------------- 第一步:载入原始数据集")
training_df = load_data()
print("第二步:原始数据集探查")
dataset_exploration1(training_df) # 数据探查得到最大值、最小值等
dataset_exploration2(training_df) # 得到相关性系数矩阵
dataset_exploration3(training_df) # 得到相关性系数可视化结果
print("第三步:实验")
experiment_1 = experiment1(training_df)
experiment2(training_df) # 调整里面的参数进行实验
experiment_3 = experiment3(training_df)
compare_experiments(experiment_1, experiment_3)
print("第四步:预测")
output = predict_fare(experiment_3.model, training_df, experiment_3.settings.input_features, 'FARE')
show_predictions(output)