Kaggle机器学习笔记

写在开头

虽然作为网安专业学生之一,但是面对着 AI 兴起的时代大势,不学点相关知识未免显得自己闭门造车了。由于之前已经学习过了机器学习和人工智能的相关理论知识(详情可以见我的其他帖子),所以这篇文章主要是跟着 Kaggle 上的课程把相关的理论知识落地成为代码,可能也有一些我个人学习时解决的问题和思考。好的,闲话到此结束,接下来我们开始吧🥰。

Intro to Machine Learning

这个部分的课程是对机器学习的初步探索,前置知识也很简单,了解 python 的基本语法就行。这里我们做的是回归任务,我们希望通过对已有的墨尔本房屋价格及其相关特征训练出一个模型,进而使得这个模型在面对新的房屋特征时,也能较好的预测出价格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 数据来源于墨尔本的房屋价格
import pandas as pd
from sklearn.tree import DecisionTreeRegressor

file_path = "./melb_data.csv"
data = pd.read_csv(file_path)
data = data.dropna(axis=0)
y = data.Price
data_features = ['Rooms', 'Bathroom', 'Landsize', 'BuildingArea', 'YearBuilt', 'Lattitude', 'Longtitude']
x = data[data_features]
model = DecisionTreeRegressor(random_state=1)
model.fit(x, y)

# 尝试简单的模型预测
print("Making predictions for the following 5 houses:")
print(x.head())
print("The predictions are")
print(model.predict(x.head()))

pandas 是用来处理表格数据的库,我们从文件中提取价格数据将其作为我们的预测目标,也就是 $y$ ,我们选取一部分指标作为特征向量作为 $x$ 。因为 sklearn 库中的模型训练大部分只支持数值输入,所以我们需要对字符串的数据做编码处理。因此,我们现在的特征尽可能选取数值型的特征。确定了特征和预测目标后,我们就可以开始训练了,训练中的 random_state 是训练的内部状态,状态一致可以确保训练出来的模型一致,从而确保我得到的结果是可以稳定复现的。

训练之后,我们可以打印前五行数据,并对其做一个简单预测。然后你会发现,准确率特别高,几乎是一致的。那这毕竟只是前五个数据,所以我们需要一个指标评估模型在整体上的预测效果。

1
2
3
4
5
predicted_home_prices = model.predict(x)

# 计算平均绝对误差
from sklearn.metrics import mean_absolute_error
print(mean_absolute_error(y, predicted_home_prices))

因此,我们选择了平均绝对误差(MAE)来评估模型好坏,其定义如下:
$$
MAE = \frac{1}{n}\sum_{i=1}^n\left|y_i-\hat{y}_i\right|
$$
其中, $n$ 为样本总数,$y_i$ 是对应样本的真实值,$\hat y_i$ 是对应样本的预测值,为了让误差能够累积,我们采用绝对值。运行后,我们发现我们的模型效果很好啊,只有 400 左右,意味着
百万左右的房子,我们的预测偏差只有 400 左右

但是,实际上我们的模型效果真的有这么好吗?🤔🤔🤔

如果我们还记得机器学习的相关理论就会知道:这个时候模型往往是陷入了过拟合,即模型直接开始死记硬背训练集的数据特征,那我们的预测效果就会很好了。为了解决这个问题,我们需要将数据集划分为训练集和验证集,在训练集上训练模型,在验证集上验证模型的泛化能力。

1
2
3
4
5
6
7
8
9
10
11
from sklearn.model_selection import train_test_split

# 为了验证模型的泛化能力,我们需要将数据划分为训练集和验证集
train_x, test_x, train_y, test_y = train_test_split(x, y, random_state=0)
model = DecisionTreeRegressor(random_state=0)
model.fit(train_x, train_y)
predicted_home_prices = model.predict(test_x)

# 计算平均绝对误差
from sklearn.metrics import mean_absolute_error
print(mean_absolute_error(test_y, predicted_home_prices))

现在,让我们再次运行代码,看一看我们的模型效果究竟如何?结果看起来并不好,误差到了 260000 左右。为了让模型的效果更好,我们调整决策树回归模型的叶子节点参数,找出过拟合和欠拟合的平衡点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用不同数量叶子节点的决策树回归模型训练
# 超出过拟合和欠拟合的平衡点
from tqdm import trange

def find_best(leaf_nodes, train_x, train_y, test_x, test_y):
model = DecisionTreeRegressor(max_leaf_nodes=leaf_nodes, random_state=0)
model.fit(train_x, train_y)
predicted_home_prices = model.predict(test_x)
return mean_absolute_error(test_y, predicted_home_prices)

best_mae = 10000000
best_leaf_nodes = 0
for nodes in trange(5, 1000):
mae = find_best(nodes, train_x, train_y, test_x, test_y)
if mae < best_mae :
best_mae = mae
best_leaf_nodes = nodes

print("Best leaf nodes:", best_leaf_nodes)
print("Best Mean Absolute Error:", best_mae)

这下我们的误差降到了 240000 左右,emm,看得出来,使用决策树模型的学习结果并不好。所以我们考虑使用随机森林进行机器学习,进而获得更好的预测结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import train_test_split

file_path = "./melb_data.csv"
data = pd.read_csv(file_path)
data = data.dropna(axis=0)
y = data.Price
data_features = ['Rooms', 'Bathroom', 'Landsize', 'BuildingArea', 'YearBuilt', 'Lattitude', 'Longtitude']
x = data[data_features]

train_x, test_x, train_y, test_y = train_test_split(x, y, random_state=0)
new_model = RandomForestRegressor(random_state=0)
new_model.fit(train_x, train_y)
new_predicted_home_prices = new_model.predict(test_x)
print(mean_absolute_error(test_y, new_predicted_home_prices))

运行之后,效果还不错,误差优化到 190000 左右,针对随机森林我们也可以尝试参数优化,让模型的效果更好。

KaggleIntro to Machine Learning 部分介绍到此就结束了,看起来,入门机器学习也并非困难,我们拥有了一个良好的开头。

Intermediate Machine Learning

前面我们拥有了一个良好的开头,我们训练了一个房价预测模型,并且取得了不错的成绩。但是,回顾我们的工作,显然,程序训练的模型效果并不能算很好。我们只是手动选取了几个特征,而还有相当多的特征被我们所忽略了。自然而然地,如果我们能利用上较多的特征,那么我们训练的模型预测效果应该还能得到提升。

数据预处理

因此,也就引出了该部分的主要研究内容,即我们应该如何处理数据,进而使程序获得更好的学习。数据清洗通常针对以下几类常见问题展开。我们可以通过一个简单的表格来快速了解:

问题 描述 可能的影响 常用清洗策略
缺失值 数据记录中某些字段的值为空(NaN, NULL)。 导致样本被丢弃,信息损失,计算错误。 删除、填充(均值/中位数/众数/预测)。
异常值 与大多数数据明显偏离的极端值。 扭曲统计结果,影响模型性能。 识别(IQR、Z-Score)后删除或修正。
重复值 数据集中存在完全相同的记录。 使模型过度偏向重复样本,影响泛化能力。 识别并删除重复项。
不一致性 数据格式、单位或编码不统一(如"男"、“Male”、“M”)。 导致分组和分析错误。 标准化、规范化、映射转换。
错误值 明显不合逻辑的值(如年龄为-1或300岁)。 产生毫无意义的分析结果。 根据业务逻辑进行修正或设为缺失。

不要轻视对数据的预处理,好的数据可以让我们的模型训练事半功倍,该部分主要也是围绕此展开。

首先,我们对缺失值进行讨论,具体方法如下:

  • 很自然的想法,我们会直接选择删除含有缺失值的这一列,也就是选择摆烂不管了。那么问题也是显而易见的,如果含有缺失值的列很多,都选择删去的话会让模型丢失大量有用的信息。
  • 在删除的基础上,我们更进一步的会想:删除不行,那就补一个值进去呗,也就是填充。那新的问题也就随之而来了,既然我要填入新的值,我该填啥呢?一般考虑填均值,众数,虽然也会带来一定的误差,但是总比删除好一点。
  • 填充值是标准方法,通常效果良好。然而,插补值可能系统性地高于或低于其实际值(实际值并未包含在数据集中)。或者,缺失值的行可能具有其他特殊性。在这种情况下,如果模型考虑哪些值原本缺失,则可以做出更好的预测。

这些就是针对缺失值的处理方法了,代码主要如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import pandas as pd
from sklearn.model_selection import train_test_split

# 读取数据
print("Loading data...")
data = pd.read_csv('melb_data.csv')

y = data.Price

# 为了简化问题,我们只采用数值列作为特征进行训练
melb_predictors = data.drop(['Price'], axis=1)
x = melb_predictors.select_dtypes(exclude=['object'])

# 将原始数据划分为训练集和验证集
x_train, x_valid, y_train, y_valid = train_test_split(x, y, train_size=0.8, test_size=0.2, random_state=0)

print("Setup complete")

from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error

# 我们利用 MAE 检验三种处理方法的好坏
def score_dataset(x_train, x_valid, y_train, y_valid):
model = RandomForestRegressor(n_estimators=10, random_state=0)
model.fit(x_train, y_train)
preds = model.predict(x_valid)
return mean_absolute_error(y_valid, preds)

# 1. 直接删除含有缺失值的列
cols_with_missing = [col for col in x_train.columns if x_train[col].isnull().any()]

reduced_x_train = x_train.drop(cols_with_missing, axis=1)
reduced_x_valid = x_valid.drop(cols_with_missing, axis=1)

print("MAE from Approach 1 (Drop columns with missing values):")
print(score_dataset(reduced_x_train, reduced_x_valid, y_train, y_valid))

# 2. 采用填充均值的方法填补缺失数据列
from sklearn.impute import SimpleImputer

my_imputer = SimpleImputer()
imputed_x_train = pd.DataFrame(my_imputer.fit_transform(x_train))
imputed_x_valid = pd.DataFrame(my_imputer.transform(x_valid))

imputed_x_train.columns = x_train.columns
imputed_x_valid.columns = x_valid.columns

print("MAE from Approach 2 (Imputation):")
print(score_dataset(imputed_x_train, imputed_x_valid, y_train, y_valid))

# 3. 填补缺失列,同时记录哪些列缺失
x_train_plus = x_train.copy()
x_valid_plus = x_valid.copy()

for col in cols_with_missing:
x_train_plus[col + '_was_missing'] = x_train_plus[col].isnull()
x_valid_plus[col + '_was_missing'] = x_valid_plus[col].isnull()

my_imputer = SimpleImputer()
imputed_x_train_plus = pd.DataFrame(my_imputer.fit_transform(x_train_plus))
imputed_x_valid_plus = pd.DataFrame(my_imputer.transform(x_valid_plus))

imputed_x_train_plus.columns = x_train_plus.columns
imputed_x_valid_plus.columns = x_valid_plus.columns

print("MAE from Approach 3 (An Extension to Imputation):")
print(score_dataset(imputed_x_train_plus, imputed_x_valid_plus, y_train, y_valid))

运行后,发现方法二和方法三的 MAE 接近,都好于方法一。

在数据处理过程中,并非所有数据都是数值型的,也就意味着我们不能直接对这些数据进行数学运算。所以接下来,我们对非数值变量进行讨论,具体方法如下:

  • 处理分类变量最简单的方法就是直接从数据集中删除它们。但这种方法只有在这些列不包含任何有用信息时才有效。

  • 序数编码将每个唯一值分配给不同的整数,将非数值变量转为数值进行运算。但是这也会带来一个新的问题,如果非数值变量之间不存在比较关系,这是简单的分类。比如物品的颜色,颜色之间的地位是等同的,我们如果分别给不同颜色一个数值,而不同的数值之间是存在大小关系的,那么模型也会认为这之间存在着比较关系。

  • 为了解决这个问题,我们采用 One-Hot Encoding 来处理,它会创建新的列,指示原始数据中每个可能值的存在(或不存在)。在原始数据集中,“颜色”是一个分类变量,包含三个类别:“红色”、“黄色”和“绿色”。对应的独热编码包含一列,每列对应一个可能的值,并且每行对应原始数据集中的每一行。如果原始值为“红色”,则在“红色”列中写入 1;如果原始值为“黄色”,则在“黄色”列中写入 1,依此类推。与序数编码不同,独热编码不假定类别之间存在顺序。因此,如果类别数据中没有明显的顺序,这种方法尤其有效。我们将没有内在排序的类别变量称为名义变量

    如果分类变量取值很多,独热编码通常表现不佳。也就是说,我们通常不会用于取值超过 15 个不同值的变量。

我们还是用 MAE 来衡量模型的好坏,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import pandas as pd
from sklearn.model_selection import train_test_split

# 读取数据
print("Loading data...")
data = pd.read_csv('melb_data.csv')

y = data.Price
x = data.drop(['Price'], axis=1)

# 将原始数据划分为训练集和验证集
x_train_full, x_valid_full, y_train, y_valid = train_test_split(x, y, train_size=0.8, test_size=0.2, random_state=0)

print("Setup complete")

# 数据预处理
# 采用最简单的方法,移除含有缺失值的列
cols_with_missing = [col for col in x_train_full.columns if x_train_full[col].isnull().any()]
x_train_full.drop(cols_with_missing, axis=1, inplace=True)
x_valid_full.drop(cols_with_missing, axis=1, inplace=True)

# 计算对应列唯一值的出现次数,正对应着教程所说的:
# 如果分类变量取值很多,独热编码通常表现不佳(即,通常不会用于取值超过 15 个不同值的变量)。
# PS:使用 Kaggle 上的代码我没有跑出 ['Type', 'Method', 'Regionname'],我是将下面这行代码中的'object'改为'string'才能正确得到结果。
low_cardinality_cols = [cname for cname in x_train_full.columns if x_train_full[cname].nunique() < 10 and x_train_full[cname].dtype == 'string']
# 选取数值列
numerical_cols = [cname for cname in x_train_full.columns if x_train_full[cname].dtype in ['int64', 'float64']]
# 拼接得到对应列
my_cols = low_cardinality_cols + numerical_cols
x_train = x_train_full[my_cols].copy()
x_valid = x_valid_full[my_cols].copy()

print(x_train.head())

# 获取分类变量的对应列
s = (x_train.dtypes == 'string')
object_cols = list(s[s].index)

print("Categorical variables:")
print(object_cols)

from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error

# 我们利用 MAE 检验不同处理方法的好坏
def score_dataset(x_train, x_valid, y_train, y_valid):
model = RandomForestRegressor(n_estimators=10, random_state=0)
model.fit(x_train, y_train)
preds = model.predict(x_valid)
return mean_absolute_error(y_valid, preds)

# 1.我们使用 select_dtypes() 方法删除 object 列。
drop_X_train = x_train.select_dtypes(exclude=['object'])
drop_X_valid = x_valid.select_dtypes(exclude=['object'])

print("MAE from Approach 1 (Drop categorical variables):")
print(score_dataset(drop_X_train, drop_X_valid, y_train, y_valid))

# 2.使用序数编码处理
from sklearn.preprocessing import OrdinalEncoder

# 复制一份数据,保持原始数据不变
label_X_train = x_train.copy()
label_X_valid = x_valid.copy()

ordinal_encoder = OrdinalEncoder()
label_X_train[object_cols] = ordinal_encoder.fit_transform(x_train[object_cols])
label_X_valid[object_cols] = ordinal_encoder.transform(x_valid[object_cols])

print("MAE from Approach 2 (Ordinal Encoding):")
print(score_dataset(label_X_train, label_X_valid, y_train, y_valid))

# 3.使用 OneHot 编码处理
from sklearn.preprocessing import OneHotEncoder

OH_encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False)
OH_cols_train = pd.DataFrame(OH_encoder.fit_transform(x_train[object_cols]))
OH_cols_valid = pd.DataFrame(OH_encoder.transform(x_valid[object_cols]))

# 将索引值赋值回
OH_cols_train.index = x_train.index
OH_cols_valid.index = x_valid.index

# 移除原本的类别变量列,我们使用OneHot编码列代替
num_X_train = x_train.drop(object_cols, axis=1)
num_X_valid = x_valid.drop(object_cols, axis=1)

# 将编码列添加回数值列
OH_X_train = pd.concat([num_X_train, OH_cols_train], axis=1)
OH_X_valid = pd.concat([num_X_valid, OH_cols_valid], axis=1)

OH_X_train.columns = OH_X_train.columns.astype(str)
OH_X_valid.columns = OH_X_valid.columns.astype(str)

print("MAE from Approach 3 (One-Hot Encoding):")
print(score_dataset(OH_X_train, OH_X_valid, y_train, y_valid))

运行后,发现方法二和方法三的 MAE 接近,都好于方法一。现在,我们回过头来看,我们的模型效果得到了一个比较好的优化,MAE 从一开始的 190000 到现在的 170000,所以好的数据可以使模型更好。

使用管道优化你的代码

现在让我们回顾一下之前的工作,当我们对数据采用不同的处理方式时,需要我们时刻小心,防止处理时搞混了不同处理方式得到的数据。所以我们希望能有一个东西,将不同的数据处理方式打包到一块,使用时只需要调用即可,接下来我们主要讨论使用**管道(Pipelines)**来管理你的代码。

首先,仍然是正常的读取数据,提取数值列和分类列,检查是否提取成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pandas as pd
from sklearn.model_selection import train_test_split

data = pd.read_csv('./melb_data.csv')

y = data.Price
X = data.drop(['Price'], axis=1)

X_train_full, X_valid_full, y_train, y_valid = train_test_split(X, y, train_size=0.8, test_size=0.2, random_state=0)

categorical_cols = [cname for cname in X_train_full.columns if X_train_full[cname].nunique() < 10 and X_train_full[cname].dtype == "string"]

numerical_cols = [cname for cname in X_train_full.columns if X_train_full[cname].dtype in ['int64', 'float64']]

my_cols = categorical_cols + numerical_cols
X_train = X_train_full[my_cols].copy()
X_valid = X_valid_full[my_cols].copy()

print("Setup Complete!")

print(X_train.head())

其次,我们将数据的预处理,如采用均值对数值列进行填充,采用众数对分类列填充,接着将这两个操作打包为对列数据的处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder

# 使用均值对数值列进行填充
numerical_transformer = SimpleImputer(strategy='mean')

# 使用众数对字符列填充,然后独热编码
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])

preprocessor = ColumnTransformer(
transformers=[
('num', numerical_transformer, numerical_cols),
('cat', categorical_transformer, categorical_cols)
])

然后,我们将模型构建与数据处理打包为一个管道,从而我们可以实现直接调用管道进行模型训练。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error

# 使用我们熟悉的随机森林构建模型
model = RandomForestRegressor(n_estimators=100, random_state=0)

# 将训练模型和数据预处理打包到一个管道内
my_pipeline = Pipeline(steps=[('preprocessor', preprocessor), ('model', model)])

# 预处理数据,训练模型
my_pipeline.fit(X_train, y_train)

preds = my_pipeline.predict(X_valid)
score = mean_absolute_error(y_valid, preds)
print('MAE:', score)

可以看出,使用管道处理可以让我们的代码更加简洁,同时无需在每个步骤中手动跟踪训练数据和验证数据。

交叉验证

机器学习是一个不断迭代优化的过程,在这个过程中,我们将面临不同的抉择,例如使用哪些预测变量、使用哪些类型的模型、向这些模型提供哪些参数等等。到目前为止,我们一直以数据驱动的方式做出这些选择,通过使用验证集(或保留集)来衡量模型质量。

但这种方法也存在一些缺点。举例来说,假设你有一个包含 5000 行的数据集。通常你会保留大约 20% 的数据作为验证集,也就是 1000 行。但这会在确定模型得分时引入一些随机性。也就是说,一个模型在一个 1000 行的数据集上表现良好,但在另一个 1000 行的数据集上却可能表现不佳。

极端情况下,你可以想象验证集中只有一行数据。如果你比较不同的模型,哪个模型能对单个数据点做出最佳预测,很大程度上取决于运气。一般来说,验证集越大,模型质量评估中的随机性(也就是“噪声”)就越小,结果也就越可靠。遗憾的是,我们只能通过从训练数据中移除行来获得较大的验证集,而较小的训练数据集意味着更差的模型。

所以,我们引入了交叉验证来帮我们控制噪声。在交叉验证中,我们在不同的数据子集上运行建模过程,以获得模型质量的多个衡量标准。

例如,我们可以先将数据分成 5 部分,每部分占完整数据集的 20%。在这种情况下,我们称已将数据分成 5 个 folds。然后,我们对每个折叠运行一次操作:

  • 实验 1 中,我们使用第一折数据作为验证集(或保留集),其余数据作为训练数据。这样,我们就可以基于 20% 的保留集来衡量模型质量。
  • 实验 2 中,我们保留第二折的数据并使用除第二折之外的所有数据来训练模型。然后,我们使用保留集来获得模型质量的第二次评估。
  • 我们重复这个过程,每次都将折叠数据作为保留集使用一次。综上所述,所有数据在某个阶段都会被用作保留集,最终我们得到的模型质量指标是基于数据集中的所有行(即使我们并非同时使用所有行)。

交叉验证能更准确地衡量模型质量,这在需要做出大量建模决策时尤为重要。然而,由于它需要估计多个模型(每个折叠对应一个不同的模型),因此运行时间可能会更长。权衡一下利弊,我们应该在什么时候选择哪种方法呢?

  • 对于小型数据集 ,额外的计算负担并不是什么大问题,应该采用交叉验证。
  • 对于较大的数据集 ,单个验证集就足够了。这样代码运行速度会更快,而且数据量可能足够大,几乎不需要将部分数据用于保留集。

实际运用中,并没有一个简单的阈值来界定数据集的大小。但如果模型运行时间只需几分钟甚至更短,那么或许值得考虑切换到交叉验证。或者,直接运行交叉验证,看看每个实验的得分是否接近。如果每个实验都得出相同的结果,那么单个验证集可能就足够了。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import pandas as pd
from sklearn.model_selection import train_test_split

data = pd.read_csv('./melb_data.csv')

y = data.Price
X = data.drop(['Price'], axis=1)

X_train_full, X_valid_full, y_train, y_valid = train_test_split(X, y, train_size=0.8, test_size=0.2, random_state=0)

categorical_cols = [cname for cname in X_train_full.columns if X_train_full[cname].nunique() < 10 and X_train_full[cname].dtype == "string"]

numerical_cols = [cname for cname in X_train_full.columns if X_train_full[cname].dtype in ['int64', 'float64']]

my_cols = categorical_cols + numerical_cols
X_train = X_train_full[my_cols].copy()
X_valid = X_valid_full[my_cols].copy()

print("Setup Complete!")
print(X_train.head())

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
from sklearn.ensemble import RandomForestRegressor

# 使用均值对数值列进行填充
numerical_transformer = SimpleImputer(strategy='mean')

# 使用众数对字符列填充,然后独热编码
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])

preprocessor = ColumnTransformer(
transformers=[
('num', numerical_transformer, numerical_cols),
('cat', categorical_transformer, categorical_cols)
])

model = RandomForestRegressor(n_estimators=100, random_state=0)
my_pipeline = Pipeline(steps=[('preprocessor', preprocessor), ('model', model)])

from sklearn.model_selection import cross_val_score

scores = -1 * cross_val_score(my_pipeline, X, y, cv=5, scoring='neg_mean_absolute_error')

print("MAE scores:", scores)
print("Average MAE score (across experiments):")
print(scores.mean())

唯一需要注意的是,在使用 sklearn 自带的交叉验证函数获取评估值时,我们选择使用了 MAE 的负数作为评估指标,这是因为 sklearn 默认数值越大越好,所以我们需要使用负数来评估。

XGBoost

在之前的学习中,我们一直使用随机森林方法进行预测,这种方法通过平均许多决策树的预测来取得比单个决策树更好的性能。我们将随机森林方法称为一种"集成方法",根据定义,集成方法结合了多个模型的预测(例如,在随机森林的情况下,是多个树)。通常情况下,集成学习会取得更好的成果。接下来,我们将学习另一种集成方法,称为梯度提升

简而言之,梯度提升的基本思想是:先训练一个较弱的模型,再训练下一个模型去拟合前面模型犯下的错误,重复很多轮,最后把这些弱模型加起来。概括地讲,我们串行训练多个模型,后一个专门修正前一个的错误,进而使整体的效果得到较好提升。

看的出来,梯度提升的过程是一个循环,我们以分类问题举例,目标预测值为 $y$ ,对应过程如下:

  • 训练一个初步的模型 $f_1(x)$ ,可以给出初步的一个预测,但效果并不好

  • 计算误差项 $r$
    $$
    r = y-f_1(x)
    $$
    接着训练一个模型 $f_2(x)$ 预测这个误差项。

  • 然后把两个模型相加,得到一个新的模型:
    $$
    F_2(x) = f_1(x)+\eta f_2(x)
    $$
    其中,$\eta$ 是学习率,用来控制对误差的修正幅度。

  • 最后,重复以上步骤,得到一个由很多小小模型叠加起来的强模型:
    $$
    F_m(x) = F_{m-1}(x)+\eta f_m(x)
    $$

这就是 梯度提升 的主要步骤了,具体代码还是如下给出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import pandas as pd
from sklearn.model_selection import train_test_split

data = pd.read_csv('./melb_data.csv')

# 作为示例使用,只选取一部分作为特征
cols_to_use = ['Rooms', 'Distance', 'Landsize', 'BuildingArea', 'YearBuilt']
X = data[cols_to_use]

# 确定预测目标
y = data.Price

train_X, test_X, train_y, test_y = train_test_split(X, y, random_state=42)
print('Setup complete')

from xgboost import XGBRegressor

my_model = XGBRegressor()
my_model.fit(train_X, train_y)

from sklearn.metrics import mean_absolute_error

predictions = my_model.predict(test_X)
print("Mean Absolute Error: " + str(mean_absolute_error(predictions, test_y)))

my_model = XGBRegressor(n_estimators=500, early_stopping_rounds=5, learning_rate=0.05, n_jobs=4)
my_model.fit(train_X, train_y, eval_set=[(test_X, test_y)], verbose=False)

predictions = my_model.predict(test_X)
print("Mean Absolute Error: " + str(mean_absolute_error(predictions, test_y)))

至此,Kaggle 上关于机器学习的入门教程就到这里结束了,这只是很小的一部分内容,但是也揭示了一个广阔的领域,努力去做吧。

文章中涉及到的代码和数据文件均可在我的 Github 仓库中找到,感谢你的阅读。🥰🥰🥰


Kaggle机器学习笔记
https://zijeff.github.io/2026/03/12/Kaggle机器学习笔记/
作者
zijeff
发布于
2026年3月12日
许可协议