计算机系统应用教程网站

网站首页 > 技术文章 正文

一篇值得收藏的ML数据预处理原理与实践文章

btikc 2024-10-11 11:16:23 技术文章 4 ℃ 0 评论

文章发布于公号【数智物语】 (ID:decision_engine),关注公号不错过每一篇干货。

来源 | SAMshare

作者 | Samshare



选自 Python-Machine-Learning-Book On GitHub

作者:Sebastian Raschka

翻译&整理 By Sam

本文的主体结构前置,如下:

一. 处理数据缺失

1.1 删除有缺失的样本或特征

1.2 填充缺失值

二. 处理分类数据

2.1 映射有序特性

2.2 对类别特征进行编码

2.3 对无序特征进行“独热编码”

三. 学习划分训练及验证集

四. 统一特征取值范围

五. 选择有意义(有效)的特征

5.1 L1正则化的稀疏解

5.2 序列特征选择算法

六. 使用随机森林评估特征重要性

PS:代码已单独保存:可在公众号后台输入“预处理”进行获取ipynb文件

01

处理数据缺失

数据缺失,在现实生活中是十分常见的,原因也是非常复杂的,在我们进行建模的过程中,如果我们不对这些缺失值进行适当的处理,出来的模型恐怕也效果不太好,其重要性这里就不累赘多说,我们先来创建一个小栗子,助于大家理解数据缺失的问题:

 1import pandas as pd
 2from io import StringIO
 3csv_data = '''A,B,C,D
 41.0,2.0,3.0,4.0
 55.0,6.0,,8.0
 610.0,11.0,12.0,'''
 7# If you are using Python 2.7, you need
 8# to convert the string to unicode:
 9# csv_data = unicode(csv_data)
10df = pd.read_csv(StringIO(csv_data))
11df

output:



1# 查看各列缺失情况
2df.isnull().sum()

output:



我们创建了一个csv格式的数据并且读入DataFrame对象,如果想统计有多少缺失,我们可以使用isnull方法来返回一个值为布尔类型的DataFrame,判断每个元素是否缺失,如果元素缺失,值为True。然后使用sum方法,我们就能得到DataFrame中每一列的缺失值个数。

这里提示一下,预处理时推荐使用pandas的DataFrame格式,而不要用NumPy数组。由DataFrame对象得到NumPy数组很方便,直接通过values属性即可,然后就可以用sklearn中的算法了。

1.1

删除有缺失的样本或特征

那么,如果我们想要删除这些缺失的数据,处理的方式是怎么样的呢?下面给出了几种场景,都是使用dropna方法来实现的。

 1# 删除含有缺失值的样本(即行)
 2print (df.dropna())
 3print ('\n')
 4# 删除含有缺失值的特征(即列)
 5print (df.dropna(axis=1))
 6print ('\n')
 7# 删除所有列都是缺失的样本
 8print (df.dropna(how='all'))
 9print ('\n')
10# 删除没有4个非空特征的样本
11print (df.dropna(thresh=4))
12print ('\n')
13# 删除指定特征上有缺失的样本(这里‘c'为指定特征)
14print (df.dropna(subset=['C']))

output:



1.2

填充缺失值

有些特征的缺失其实是可以通过填充的方式来弥补的,所以这里也介绍一下sklearn中的Imputer类方法。

参数axis:axis=0计算每个特征取值的平均值用来填充,若axis=1则计算样本所有取值的平均值用来填充;

参数strategy:包括mean、median和most_frequent(most_frequent对于处理分类数据类型的缺失值很有用)。

1from sklearn.preprocessing import Imputer
2# 这里使用均值填充
3imr = Imputer(missing_values='NaN', strategy='mean', axis=0)
4imr = imr.fit(df)
5imputed_data = imr.transform(df.values)
6imputed_data



02

处理分类数据

分类数据也是在现实生活中很常见的,之前我们举例子的都是数值型变量,而其他也有很多分类变量,并且这些变量还分有序和无序变量。如衣服的码数、鞋子的码数,其实都是分类变量,但是它们是有序的,而像衣服的颜色、鞋子的牌子等等,就是无序的分类变量,这两类,在后续的处理中,也是有一些不同的地方的。

我们继续举栗子来讲解,先创建一个dataframe对象:

1import pandas as pd
2df = pd.DataFrame([['green', 'M', 10.1, 'new balance'],
3 ['red', 'L', 13.5, 'Nike'],
4 ['blue', 'XL', 15.3, 'new balance']])
5df.columns = ['颜色', '尺寸', '单价', '牌子']
6df



以上,包括了无序分类变量(颜色、牌子)、有序分类变量(尺寸)和数值型变量(单价)。

2.1

映射有序特性

有些算法其实对有序分类变量(ordinal feature)的解释还是不行的,我们需要将其转为整型数值。unfortunately,并没有能够直接调用的方法来自动得到正确顺序的有序分类变量。因此,我们要自己定义映射函数。

1size_mapping = {'XL': 3,
2 'L': 2,
3 'M': 1}
4df['尺寸'] = df['尺寸'].map(size_mapping)
5df



可以看到,尺寸从XL、L和M变成了3、2、1。

我们可以映射过去,当然也是可以反映射回去的:

1# 反映射字典 
2inv_size_mapping = {v: k for k, v in size_mapping.items()}
3df['尺寸'] = df['尺寸'].map(inv_size_mapping)
4df

2.2

对类别特征进行编码

上面是对一些无序分类特征进行了映射编码,那么对于我们的标签(也叫类别、目标)也是需要进行编码的,这样子算法才可以进行识别解释。一样的,标签也是无序的,我们可以从0开始编码。

1import numpy as np
2class_mapping = {label: idx for idx, label in enumerate(np.unique(df['牌子']))}
3class_mapping



1df['牌子'] = df['牌子'].map(class_mapping)
2df



可以看到,我们把类别转为了0/1变量,一样的,我们也可以转回去。

1# 同样的,也可以转回去
2inv_class_mapping = {v: k for k, v in class_mapping.items()}
3df['牌子'] = df['牌子'].map(inv_class_mapping)
4df

上面是我们自己手动创建的映射字典,sklearn 中提供了 LabelEncoder 类来实现类似的类别转换工作。

1# 使用自带的LabelEncoder类来实现类别的转换
2from sklearn.preprocessing import LabelEncoder
3class_le = LabelEncoder()
4y = class_le.fit_transform(df['牌子'].values)
5df['牌子'] = y
6df



1# 同样的,也可以转回去
2y_inv = class_le.inverse_transform(y)
3df['牌子'] = y_inv
4df



2.3

对无序特征进行“独热编码”

上面讲了有序分类变量和类别的编码操作,但是对无序分类变量是否也可以类似地操作呢?答案是不行的。就上面的栗子,其中颜色就是无序分类变量,如果按照上面的编码方式,green、red 和 blue 将会被编码为 1、2、3,而这样子算法就会误认为 green<red<blue,然而,他们之间是没有这样子的顺序的。

所以,我们不能采取上面的操作,这里介绍一个“独热编码”。下面我们使用 LabelEncoder 来进行转换:

1df['尺寸'] = df['尺寸'].map(size_mapping)
2X = df[['颜色', '尺寸', '单价']].values
3color_le = LabelEncoder()
4X[:, 0] = color_le.fit_transform(X[:, 0])
5X



1from sklearn.preprocessing import OneHotEncoder
2ohe = OneHotEncoder(categorical_features=[0])
3ohe.fit_transform(X).toarray()



1pd.get_dummies(df[['单价', '颜色', '尺寸']])



可以看出,颜色被重新编码为3个新特征,如上图。其实这种编码操作在分类变量中是非常常用的,毕竟大多数的分类变量都是无序的。

03

学习划分训练及验证集

进行到实战,我们导入一个wine数据集,这个可以直接从网络上进行下载,这个数据集主要包含了酒的化学成分的,我们试着导入数据集:

 1df_wine = pd.read_csv('https://archive.ics.uci.edu/'
 2 'ml/machine-learning-databases/wine/wine.data',
 3 header=None)
 4df_wine.columns = ['Class label', 'Alcohol', 'Malic acid', 'Ash',
 5 'Alcalinity of ash', 'Magnesium', 'Total phenols',
 6 'Flavanoids', 'Nonflavanoid phenols', 'Proanthocyanins',
 7 'Color intensity', 'Hue', 'OD280/OD315 of diluted wines',
 8 'Proline']
 9print('Class labels', np.unique(df_wine['Class label']))
10df_wine.head()



其中,酒的种类有3种,分别是class label 1、2、3,我们调用下面的代码划分训练和验证集,test_size=0.3,使得训练集占Wine样本数的70%,测试集占30%。

在分割数据集时,我们一般选择60:40, 70:30或者80:20。对于大数据集,90:10甚至 99:1也是比较常见的。

1from sklearn.model_selection import train_test_split
2X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values
3X_train, X_test, y_train, y_test = \
4 train_test_split(X, y, test_size=0.3, random_state=0)

04

统一特征取值范围

特征缩放(feature scaling)是预处理阶段的关键步骤,但常常被遗忘。虽然存在决策树和随机森林这种是少数不需要特征缩放的机器学习算法,但对于大部分机器学习算法和优化算法来说,如果特征都在同一范围内,会获得更好的结果。

你想象一下有两个特征,一个特征的取值范围是[1,10],另一个特征的取值范围是[1,100000]。很明显,如果使用kNN算法,它是用欧氏距离作为距离度量,第二维度特征也就占据了主要的话语权。

面对这些情况,还是有方法可以解决的,使得不同的特征有相同的取值范围,分别是:

> 归一化(normalization):归一化指的是将特征范围缩放到[0,1],是最小-最大缩放(min-max scaling)的特例。

> 标准化(standardization):将特征值缩放到以0为中心,标准差为1,换句话说,标准化后的特征形式服从正态分布,这样学习权重参数更容易。

4.1

归一化与标准化演示

1ex = pd.DataFrame([0, 1, 2, 3, 4, 5])
2# 标准化
3ex[1] = (ex[0] - ex[0].mean()) / ex[0].std(ddof=0)
4# 归一化
5ex[2] = (ex[0] - ex[0].min()) / (ex[0].max() - ex[0].min())
6ex.columns = ['input', 'standardized', 'normalized']
7ex



4.2

归一化实现代码

1from sklearn.preprocessing import MinMaxScaler
2mms = MinMaxScaler()
3X_train_norm = mms.fit_transform(X_train)
4X_test_norm = mms.transform(X_test)

4.3

标准化实现代码

1from sklearn.preprocessing import StandardScaler
2stdsc = StandardScaler()
3X_train_std = stdsc.fit_transform(X_train)
4X_test_std = stdsc.transform(X_test)

05

选择有意义(有效)的特征

如果一个模型在训练集的表现比测试集好很多,那模型很可能过拟合了。过拟合意味着模型捕捉了训练集中的特例模式,但对未知数据的泛化能力比较差。

模型过拟合的一个原因是对于给定的训练集数据,模型过于复杂,常用的减小泛化误差的做法包括:

1. 收集更多的训练集数据

2. 正则化,即引入模型复杂度的惩罚项

3. 选择一个简单点的模型,参数少一点的

4. 降低数据的维度

下面,我们重点学习正则化和特征选择的方法来降低过拟合。

5.1

L1正则化

关于正则化,主要是L1和L2,具体的原理这里就不介绍了,大家可以去百度一下,有很多不错的文章可以看看。其实在具体应用,就是在使用模型算法的时候,作为一个参数进行设置即可(penalty = 'l1'),如下所示:

1from sklearn.linear_model import LogisticRegression
2lr = LogisticRegression(penalty='l1', C=0.1)
3lr.fit(X_train_std, y_train)
4print('Training accuracy:', lr.score(X_train_std, y_train))
5print('Test accuracy:', lr.score(X_test_std, y_test))

我们可以看看,在不同正则下的特征数量变化情况:

 1import matplotlib.pyplot as plt
 2fig = plt.figure()
 3ax = plt.subplot(111)
 4colors = ['blue', 'green', 'red', 'cyan', 
 5 'magenta', 'yellow', 'black', 
 6 'pink', 'lightgreen', 'lightblue', 
 7 'gray', 'indigo', 'orange']
 8weights, params = [], []
 9for c in np.arange(-4., 6.):
10 lr = LogisticRegression(penalty='l1', C=10.**c, random_state=0)
11 lr.fit(X_train_std, y_train)
12 weights.append(lr.coef_[1])
13 params.append(10.**c)
14weights = np.array(weights)
15for column, color in zip(range(weights.shape[1]), colors):
16 plt.plot(params, weights[:, column],
17 label=df_wine.columns[column + 1],
18 color=color)
19plt.axhline(0, color='black', linestyle='--', linewidth=3)
20plt.xlim([10**(-5), 10**5])
21plt.ylabel('weight coefficient')
22plt.xlabel('C')
23plt.xscale('log')
24plt.legend(loc='upper left')
25ax.legend(loc='upper center', 
26 bbox_to_anchor=(1.38, 1.03),
27 ncol=1, fancybox=True)
28plt.show()

我们可以发现,如果C<0.1,正则项威力很大时,所有特征权重都为0:



5.2

序列特征选择算法

另一种减小模型复杂度和避免过拟合的方法是通过特征选择进行维度降低(dimensionality reduction),这个方法尤其对非正则模型有用。维度降低有两种做法:特征选择(feature selection)和特征抽取(feature extraction)。

特征选择会从原始特征集中选择一个子集合。特征抽取是从原始特征空间抽取信息,从而构建一个新的特征子空间,我们主要学习这种特征选择算法。

特征选择算法的原理是自动选择一个特征子集,子集中的特征都是和问题最相关的特征,这样能够提高计算效率并且由于溢出了不相干特征和噪音也降低了模型的泛化误差。

一个经典的序列特征选择算法是序列后向选择(sequential backward selection, SBS),它能够降低原始特征维度提高计算效率,在某些情况下,如果模型过拟合,使用SBS后甚至能提高模型的预测能力。

SBS原理实现代码:

 1from sklearn.base import clone
 2from itertools import combinations
 3import numpy as np
 4from sklearn.metrics import accuracy_score
 5from sklearn.model_selection import train_test_split
 6class SBS():
 7 def __init__(self, estimator, k_features, scoring=accuracy_score,
 8 test_size=0.25, random_state=1):
 9 self.scoring = scoring
10 self.estimator = clone(estimator)
11 self.k_features = k_features
12 self.test_size = test_size
13 self.random_state = random_state
14 def fit(self, X, y):
15 X_train, X_test, y_train, y_test = \
16 train_test_split(X, y, test_size=self.test_size,
17 random_state=self.random_state)
18 dim = X_train.shape[1]
19 self.indices_ = tuple(range(dim))
20 self.subsets_ = [self.indices_]
21 score = self._calc_score(X_train, y_train, 
22 X_test, y_test, self.indices_)
23 self.scores_ = [score]
24 while dim > self.k_features:
25 scores = []
26 subsets = []
27 for p in combinations(self.indices_, r=dim - 1):
28 score = self._calc_score(X_train, y_train, 
29 X_test, y_test, p)
30 scores.append(score)
31 subsets.append(p)
32 best = np.argmax(scores)
33 self.indices_ = subsets[best]
34 self.subsets_.append(self.indices_)
35 dim -= 1
36 self.scores_.append(scores[best])
37 self.k_score_ = self.scores_[-1]
38 return self
39 def transform(self, X):
40 return X[:, self.indices_]
41 def _calc_score(self, X_train, y_train, X_test, y_test, indices):
42 self.estimator.fit(X_train[:, indices], y_train)
43 y_pred = self.estimator.predict(X_test[:, indices])
44 score = self.scoring(y_test, y_pred)
45 return score

现在我们用KNN作为Estimator来运行SBS算法:

 1import matplotlib.pyplot as plt
 2from sklearn.neighbors import KNeighborsClassifier
 3knn = KNeighborsClassifier(n_neighbors=2)
 4# selecting features
 5sbs = SBS(knn, k_features=1)
 6sbs.fit(X_train_std, y_train)
 7# plotting performance of feature subsets
 8k_feat = [len(k) for k in sbs.subsets_]
 9plt.plot(k_feat, sbs.scores_, marker='o')
10plt.ylim([0.7, 1.1])
11plt.ylabel('Accuracy')
12plt.xlabel('Number of features')
13plt.grid()
14plt.tight_layout()
15plt.show()

SBS的fit方法中有分割数据集的功能,我们为SBS提供了训练集,然后fit方法将其分割为子训练集和子测试集,而这个子测试集被称为验证集(validation dataset)。

SBS算法记录了每一步最优特征子集的成绩,我们画出每个最优特征子集在验证集上的分类准确率:



我们可以看到,最开始随着特征数目的减少,分类准确率一直在提高,原因可能是降低了维度诅咒。对于k={5,6,7,8,9,10,11},分类准确率是100%.

我们打印出特征选择最优的5个特征:

1# 打印出最优的5个特征
2k5 = list(sbs.subsets_[8])
3print(df_wine.columns[1:][k5])
output:
Index(['Alcohol', 'Malic acid', 'Alcalinity of ash', 'Hue', 'Proline'], dtype='object')
1# 原模型的预测效果(即全变量)
2knn.fit(X_train_std, y_train)
3print('Training accuracy:', knn.score(X_train_std, y_train))
4print('Test accuracy:', knn.score(X_test_std, y_test))
output:
Training accuracy: 0.983870967742
Test accuracy: 0.944444444444
1# 特征选择后的新模型预测效果
2knn.fit(X_train_std[:, k5], y_train)
3print('Training accuracy:', knn.score(X_train_std[:, k5], y_train))
4print('Test accuracy:', knn.score(X_test_std[:, k5], y_test))
output:
Training accuracy: 0.959677419355
Test accuracy: 0.962962962963

可以看出,原模型是有一点点过拟合的,训练的时候准确度达到98%,但验证的时候只有94%,而我们使用更少的变量,准确度也可以达到95%,而且训练和验证的效果差异不会特别大。

06

使用随机森林评估特征重要性

随机森林能够度量每个特征的重要性,我们可以依据这个重要性指标进而选择最重要的特征。sklearn中已经实现了用随机森林评估特征重要性,在训练好随机森林模型后,直接调用feature_importances属性就能得到每个特征的重要性。

下面用Wine数据集为例,我们训练一个包含10000棵决策树的随机森林来评估13个维度特征的重要性(第三章我们就说过,对于基于树的模型,不必对特征进行标准化或归一化):

 1from sklearn.ensemble import RandomForestClassifier
 2feat_labels = df_wine.columns[1:]
 3forest = RandomForestClassifier(n_estimators=10000,
 4 random_state=0,
 5 n_jobs=-1)
 6forest.fit(X_train, y_train)
 7importances = forest.feature_importances_
 8indices = np.argsort(importances)[::-1]
 9for f in range(X_train.shape[1]):
10 print("%2d) %-*s %f" % (f + 1, 30, 
11 feat_labels[indices[f]], 
12 importances[indices[f]]))
13plt.title('Feature Importances')
14plt.bar(range(X_train.shape[1]), 
15 importances[indices],
16 color='lightblue', 
17 align='center')
18plt.xticks(range(X_train.shape[1]), 
19 feat_labels[indices], rotation=90)
20plt.xlim([-1, X_train.shape[1]])
21plt.tight_layout()
22plt.show()



我们可以得出结论:‘Alcohol’是最能区分类别的特征。有趣地是,重要性排名前三的特征也在SBS的最优5特征子集中。

sklearn的随机森林实现,包括一个transform方法能够基于用户给定的阈值进行特征选择,所以如果你要用RandomFroestClassifier作为特征选择器,可以设置阈值为0.15,会选择出三个维度特征,Alcohol、Malic acid和Ash。

1from sklearn.feature_selection import SelectFromModel
2sfm = SelectFromModel(forest, threshold=0.15, prefit=True)
3X_selected = sfm.transform(X_train)
4X_selected.shape
output:(124, 3)





星标我,每天多一点智慧


Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表