机器学习过程中特征工程步骤的介绍

365 阅读11分钟

在这篇文章中,我们将学习机器学习过程中的一个重要步骤:特征工程。

目录

  • 简介
  • 安装/设置
  • 加载数据集
  • 特征生成和转换
    • 数学转换
    • 解析复杂数据类型
    • 分级
    • 缩放
  • 使用相互信息确定特征的重要性
  • 测试和结果
  • 总结

绪论

特征工程的目标是使数据更好地用于你试图用机器学习来解决的问题。

特征工程可以通过产生新的特征和确定哪些特征对预测任务的重要性来帮助提高模型的性能。通过数据转换,模型可以学习原始数据中不存在的关系。

一般来说,什么特征工程技术效果最好,并没有固定的规则。数据集差别很大,所以有时某些技术会比其他技术更有用,但其他时候可能不会。特征工程是一个试错的过程--它涉及到大量关于创建和转换哪些特征的实验,以及决定删除哪些特征。

我们将首先讨论不同的特征工程技术,然后用2021年7月的Tabular Series PlaygroundKaggle竞赛的一个例子进行讲解。

特征生成和转换

我们可以在现有的特征上应用一系列的转换来生成新的特征。这个过程会增加新的信息,它可能会产生一个更准确的模型。我们将对以下特征生成和转换技术进行探讨。

  • 数学转换(应用算术运算)
  • 解析和分解复杂的数据类型(例如:日期、时间等)。
  • 分级
  • 缩放

我们将更深入地研究这些技术,并将学习如何在Python中实现这些技术。Pandas库使得执行大多数这些转换变得非常简单。

互惠信息

互惠信息是一种通过测量每个特征和目标变量之间的关系来确定一个特征的重要性的方法。它用不确定性来描述关系,其中两个数量之间的相互信息衡量一个变量的不确定性因拥有另一个变量的知识而减少多少。

互惠信息是一个伟大的通用指标。它易于使用,效率高,而且能够检测任何类型的关系,不仅仅是线性关系。

任何两个数量之间最小的相互信息是0。当它为零时,意味着这两个数量之间没有关系。就我们的目的而言,一个相互信息分数为0或非常接近0的特征可能是一个不重要的特征,对模型的性能没有帮助。不重要的特征甚至可能对模型性能产生负面影响。

相互信息没有最大值,但数值通常小于2。更高的相互信息对应于更重要的特征。

安装/设置

在这篇文章中,我们将需要Pandas、NumPy和Scikit-Learn。我们可以使用pip 包管理器来安装它们。

pip install pandas
pip install numpy
pip install scikit-learn

如果上面的命令不起作用,请看下面的链接了解具体的安装说明:

导入必要的库:

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.feature_selection import mutual_info_regression
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

加载数据集

我们将使用2021年7月Tabular Series Playground Kaggle竞赛的数据集,因为它包含不同种类的特征,供我们应用特征工程技术。我们将使用名为 "train.csv "的文件:

df = pd.read_csv("train.csv")

让我们看一下数据集的前几行:

df.head()

df_head-1

有三个目标值供我们的模型预测:target_carbon_monoxide,target_benzene, 和target_nitrogen_oxides

特征生成和转换

我们将创建几个新的特征,随后我们将删除那些不重要的特征。

数学转换

我们可以对我们的数据应用一系列的算术运算。这些操作使用Pandas非常简单。Pandas允许我们将列作为普通变量来处理。

下面是一些例子:

df['temp_plus_humidity'] = df['deg_C'] + df['relative_humidity']
df['temp_minus_humidity'] = df['deg_C'] - df['relative_humidity']
df['temp_times_humidity'] = df['deg_C'] * df['relative_humidity']
df['temp_divided_by_humidity'] = df['deg_C'] / df['relative_humidity']
df.head()

df_head_after_operations

正如我们在上面看到的,这些列已经被创建了。每一个新的列都将每个样本的操作结果存储在列deg_Crelative_humidity 。例如,数据集中的第一行已经包含了deg_C13.146.0relative_humidity 。该行的列temp_plus_humidity 包含59.1 ,这相当于13.1 + 46.0

特征工程的目标是创建有意义的特征。上面的代码演示了我们如何应用数学变换,但是我们可以通过对所给的数据进行一些研究,了解我们如何使用所给的信息来确定其他的东西,从而创建更有意义的特征。

例如,我们可以使用列relative_humidityabsolute_humidity 来确定在当前温度下空气中可能包含的最大水蒸气量。绝对湿度测量给定体积的空气中存在的水蒸气总量,而相对湿度被定义为空气中存在的水蒸气量除以空气在当前温度下可能包含的最大水蒸气量。由于我们同时得到了绝对湿度和相对湿度,我们可以通过绝对湿度除以相对湿度来计算空气中可能含有的最大水蒸气量。

df['max_water_vapor'] = df['absolute_humidity'] / (df['relative_humidity'] / 100) # we are dividing by the percentage

使用数学变换,我们也可以将摄氏度转换为华氏度。从摄氏度转换到华氏度的公式由以下公式给出。

华氏=(9/5)摄氏+32

下面是如何转换为代码的:

df['deg_F'] = df['deg_C'] * 9/5 + 32

解析复杂数据类型

不是所有的数据集都只由数字组成。它们可以包含更复杂的数据类型,如字符串和日期/时间。然而,大多数机器学习模型不会接受这些数据类型作为输入,所以我们必须处理这些数据类型,以机器学习模型可以理解的格式传递它们。

解析日期和时间

date_time ,包含了每一行数据的测量时间的时间戳。我们不能直接使用这一列作为模型的输入,但我们可以解析它,使模型能够使用它。

让我们看看该列的数据类型,或dtype :

print(df['date_time'].dtype)
object

"Object "意味着该列目前是字符串格式的。

Pandas提供了一个有用的方法,叫做to_datetime ,它可以将字符串转换成日期时间对象。下面的代码创建了一个名为date_time_parsed 的新列。

df['date_time_parsed'] = pd.to_datetime(df['date_time'], infer_datetime_format=True)

infer_datetime_format 的参数是True ,这样Pandas就可以自动确定日期和时间格式。这样一来,我们就不需要指定格式了。

当我们检查date_time_parsed 列的dtype ,我们可以看到,新的列已经成功地将字符串解析为日期时间对象。

print(df["date_time_parsed"].dtype)
datetime64[ns]

我们不会将这一列传入模型,但我们可以使用这一列来生成数字特征:

df['hour'] = df['date_time_parsed'].dt.hour # gets the hour of day
df['day'] = df['date_time_parsed'].dt.day # gets the day
df['month'] = df['date_time_parsed'].dt.month # gets the month of the year
df['year'] = df['date_time_parsed'].dt.year # gets the year
df['quarter'] = df['date_time_parsed'].dt.quarter # gets the quarter of the year (ranges from 1-4)

现在,让我们来看看更新后的列:

df[["date_time", 'hour', "day", "month", "year", "quarter"]].head()

date_time_head

分级

分级是一种将连续值分组到较小数量的 "bin "中的方法,每个bin代表一定的数值范围。这就创造了新的分类特征,代表一个值属于哪个范围或 "bin"。

binning_visual-1
这张图片直观地展示了分仓的作用,图片由作者创建。

Pandas提供了一个cut 方法,用来对数据进行分类。在下面的代码例子中,我们将把所有的传感器数据分成5个大小相同的仓,并输出每个仓的数值范围。

for i in range(5):
    # retbins = True returns the bins so we can access the range of values
    df[f'binned_sensor_{i+1}'], bins = pd.cut(df[f'sensor_{i+1}'], 5, retbins=True, labels=False)
    print(f'Bins for sensor {i+1} :{bins}')
Bins for sensor 1 :[ 618.832  913.9   1207.5   1501.1   1794.7   2088.3  ]
Bins for sensor 2 :[ 362.0614  751.72   1139.44   1527.16   1914.88   2302.6   ]
Bins for sensor 3 :[ 308.3432  761.96   1213.32   1664.68   2116.04   2567.4   ]
Bins for sensor 4 :[ 550.5391 1025.08   1497.26   1969.44   2441.62   2913.8   ]
Bins for sensor 5 :[ 240.3481  713.08   1183.46   1653.84   2124.22   2594.6   ]

让我们来看看我们创建的新列。

df[["binned_sensor_1", "binned_sensor_2", "binned_sensor_3", "binned_sensor_4", "binned_sensor_5"]].head()

df_after_binning

缩放

特征缩放有助于在一个特定的范围内对数据进行标准化。最常用的特征缩放技术之一是标准缩放器,它对数据进行缩放,使分布以0为中心,标准差为1,它将数据安排在一个标准正态分布中。

标准标度器通过从每个值中减去平均值,然后除以标准差来对一个特征进行标准化。

让我们对所有的原始传感器列应用标准标度。

scaler = StandardScaler()
sensor_cols = ['sensor_1', 'sensor_2', 'sensor_3', 'sensor_4', 'sensor_5']
scaled_cols = ['scaled_sensor_1', 'scaled_sensor_2', 'scaled_sensor_3', 'scaled_sensor_4', 'scaled_sensor_5']
df_scaled = pd.DataFrame(scaler.fit_transform(df[sensor_cols].to_numpy()), columns=scaled_cols)
df_scaled.head()

scaled_df_head

接下来,让我们使用pd.concat ,将缩放后的数据框架与原始数据框架结合起来。

df = pd.concat([df, df_scaled], axis=1)

使用相互信息确定特征的重要性

现在我们已经创建了几个新的特征,我们可以测试它们是否与目标变量有任何关系,以确定哪些特征要训练模型,哪些要放弃。

首先,让我们创建两个变量。X y ,它存储了特征,以及存储了标签。

为了创建X ,我们首先要创建一个我们想要保留的列的列表。

all_features = list(df.columns)
print(all_features)
['date_time', 'deg_C', 'relative_humidity', 'absolute_humidity', 'sensor_1', 'sensor_2', 'sensor_3', 'sensor_4', 'sensor_5', 'target_carbon_monoxide', 'target_benzene', 'target_nitrogen_oxides', 'temp_plus_humidity', 'temp_minus_humidity', 'temp_times_humidity', 'temp_divided_by_humidity', 'deg_F', 'max_water_vapor', 'date_time_parsed', 'hour', 'day', 'month', 'year', 'quarter', 'binned_sensor_1', 'binned_sensor_2', 'binned_sensor_3', 'binned_sensor_4', 'binned_sensor_5', 'scaled_sensor_1', 'scaled_sensor_2', 'scaled_sensor_3', 'scaled_sensor_4', 'scaled_sensor_5']

我们将不包括date_timedate_time_parsed 列,因为这些数据类型不会被机器学习模型或相互信息函数所理解。我们还将排除列target_carbon_monoxide,target_benzene, 和target_nitrogen_oxides ,因为它们是标签。

for x in ('date_time', 'date_time_parsed', 'target_carbon_monoxide', 'target_benzene', 'target_nitrogen_oxides'):
    all_features.remove(x)

现在,让我们创建一个变量X ,存储所有的特征。

X = df[all_features]

让我们也创建一个变量y ,存储所有的标签。

y = df[["target_carbon_monoxide", "target_benzene", "target_nitrogen_oxides"]]

Scikit-learn提供了一个叫做mutual_info_regression 的函数。我们将创建一个函数,返回每个特征和给定标签之间的相互信息。然后我们将为我们试图预测的每个标签调用这个函数一次。

def mi_scores(X, y):
    mi_scores = mutual_info_regression(X, y)
    mi_scores = pd.Series(mi_scores, name="Mutual Information Scores", index=X.columns)
    mi_scores = mi_scores.sort_values(ascending=False)
    return mi_scores
print(mi_scores(X, y["target_carbon_monoxide"]))
print("\n")
print(mi_scores(X, y["target_benzene"]))
print("\n")
print(mi_scores(X, y["target_nitrogen_oxides"]))
scaled_sensor_2             1.041325
sensor_2                    1.039792
scaled_sensor_1             0.743840
sensor_1                    0.743327
sensor_5                    0.685566
scaled_sensor_5             0.685314
scaled_sensor_3             0.615222
sensor_3                    0.614230
binned_sensor_2             0.613156
binned_sensor_1             0.551121
binned_sensor_5             0.507127
sensor_4                    0.387663
scaled_sensor_4             0.387578
binned_sensor_3             0.346512
hour                        0.330685
binned_sensor_4             0.255194
month                       0.086405
temp_divided_by_humidity    0.063835
deg_C                       0.049258
deg_F                       0.047152
absolute_humidity           0.046896
temp_minus_humidity         0.043112
relative_humidity           0.041701
quarter                     0.038334
max_water_vapor             0.036603
temp_plus_humidity          0.034266
day                         0.021176
temp_times_humidity         0.017850
year                        0.000000
Name: Mutual Information Scores, dtype: float64


scaled_sensor_2             1.998963
sensor_2                    1.997717
binned_sensor_2             0.940828
sensor_3                    0.907664
scaled_sensor_3             0.907587
scaled_sensor_5             0.834486
sensor_5                    0.834000
sensor_1                    0.780820
scaled_sensor_1             0.780512
scaled_sensor_4             0.669859
sensor_4                    0.669768
binned_sensor_5             0.600453
binned_sensor_3             0.562740
binned_sensor_1             0.552489
binned_sensor_4             0.456760
hour                        0.323144
max_water_vapor             0.197541
absolute_humidity           0.166549
relative_humidity           0.129360
temp_divided_by_humidity    0.128394
temp_minus_humidity         0.124984
deg_F                       0.117952
deg_C                       0.115604
temp_plus_humidity          0.092767
month                       0.085944
day                         0.052050
temp_times_humidity         0.050124
quarter                     0.021046
year                        0.016426
Name: Mutual Information Scores, dtype: float64


scaled_sensor_2             0.552424
sensor_2                    0.552320
sensor_5                    0.529137
scaled_sensor_5             0.529062
scaled_sensor_3             0.486574
sensor_3                    0.486535
binned_sensor_5             0.417100
scaled_sensor_1             0.375028
sensor_1                    0.374858
binned_sensor_2             0.366994
binned_sensor_1             0.308272
binned_sensor_3             0.284557
month                       0.277641
hour                        0.195761
scaled_sensor_4             0.190657
sensor_4                    0.190559
quarter                     0.188732
binned_sensor_4             0.121445
temp_minus_humidity         0.062636
absolute_humidity           0.061017
max_water_vapor             0.050784
temp_divided_by_humidity    0.048532
deg_F                       0.047627
deg_C                       0.044749
day                         0.043873
temp_plus_humidity          0.042214
temp_times_humidity         0.038651
relative_humidity           0.025827
year                        0.003576
Name: Mutual Information Scores, dtype: float64

没有具体的分数来告诉我们一个特征是否重要,但我们已经确定了最重要的特征的顺序。

我们可以看到,传感器都有最高的相互信息分数,一些日期/时间特征也有相对高的分数。基于这些信息,我们将保留所有的传感器特征(包括经过分选和缩放的特征)以及月、小时和季度特征。

to_keep = ["month", "hour", "quarter"]
for i in range(5):
    to_keep.append(f"sensor_{i+1}")
    to_keep.append(f"binned_sensor_{i+1}")
    to_keep.append(f"scaled_sensor_{i+1}")
print(to_keep)
['month', 'hour', 'quarter', 'sensor_1', 'binned_sensor_1', 'scaled_sensor_1', 'sensor_2', 'binned_sensor_2', 'scaled_sensor_2', 'sensor_3', 'binned_sensor_3', 'scaled_sensor_3', 'sensor_4', 'binned_sensor_4', 'scaled_sensor_4', 'sensor_5', 'binned_sensor_5', 'scaled_sensor_5']

最后,我们将只包括那些我们已经决定保留的特征:

X = X[to_keep]

测试和结果

我们将首先在原始数据上创建、训练和评估一个模型,并将其与在特征工程数据上训练的模型的性能进行比较。

在原始数据上进行训练

首先,让我们加载原始数据:

original_df = pd.read_csv("train.csv")

让我们分成original_Xoriginal_y

columns = list(original_df.columns)
for col in ("date_time", "target_nitrogen_oxides", "target_benzene", "target_carbon_monoxide"):
    columns.remove(col)
original_X = original_df[columns]
original_y = original_df[["target_nitrogen_oxides", "target_benzene", "target_carbon_monoxide"]]

让我们把数据分成80%的训练和20%的测试(我们不会洗牌,这样我们就可以为另一个模型使用相同的样本)。

X_train, X_test, y_train, y_test = train_test_split(original_X, original_y, test_size=0.2, shuffle=False)

最后,我们将为每个目标变量建立一个模型,并输出其平均平方误差。

for label in ("target_nitrogen_oxides", "target_benzene", "target_carbon_monoxide"):
    rf = RandomForestRegressor(n_estimators=500)
    rf.fit(X_train, y_train[label])
    preds = rf.predict(X_test)
    print(label + ": " + str(mean_squared_error(preds, y_test[label])))
target_nitrogen_oxides: 55648.86831157129
target_benzene: 2.2838145447083664
target_carbon_monoxide: 1.0311513765846796

对特征工程数据的测试

我们已经为特征工程数据创建了变量Xy 。下一步是将数据分成80%的训练和20%的测试。

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False)

训练和测试模型的代码保持不变。

for label in ("target_nitrogen_oxides", "target_benzene", "target_carbon_monoxide"):
    rf = RandomForestRegressor(n_estimators=500)
    rf.fit(X_train, y_train[label])
    preds = rf.predict(X_test)
    print(label + ": " + str(mean_squared_error(preds, y_test[label])))
target_nitrogen_oxides: 47670.98778886509
target_benzene: 2.2403446973998626
target_carbon_monoxide: 0.9799029642445529

我们可以看到,该模型在经过特征设计的数据上的表现比在原始数据上的表现更好。

结论

在OpenGenus的这篇文章中,我们看到了不同的特征工程技术,以及如何应用它们来提高模型的性能。特征工程是一个试错的过程,所以进一步试验创建或删除哪些特征,可能会带来更好的结果。

这篇文章就写到这里,感谢您的阅读!

参考文献