概述
要构建一个完整的机器学习项目,一般可以按照如下步骤进行:
- 了解整个项目的概况,获取原始数据
- 对数据做特征工程
- 选择合适的机器学习方法,按所选择的策略对模型进行训练和优化
- 模型的部署、监控和维护
通常,第二步和第三步是重要且需要花费很多时间的步骤。同时这两步也相互依赖,需要根据数据固有的特点选择合适的机器学习模型,也需要根据所选择的机器学习模型来决定特征工程的具体操作。在下文中,将对机器学习的详细步骤进行说明,并使用Python的Scikit-learn函数库提供一些简单的示例。与此同时,在每个步骤中也会对Scikit-learn函数库的使用方法进行总结。
注意:在本文使用到的数据中,每一行的数据代表一个样本,每一列的数据代表一个特征。它是使用Python做机器学习相关任务时,所遵守的一个约定俗成的规则。Scikit-learn、PyTorch、TensorFlow等机器学习与深度学习相关的函数库都是按照这一规则进行开发的。
特征工程
在机器学习任务中,数据和特征决定了机器学习的上限(参考偏差-方差分解中的噪声部分,噪声决定了机器学习任务的难度),而模型和算法只是逼近这个上限而已。而这里的数据便指的是经过特征工程得到的数据。
特征工程指的是把原始数据转变为模型的训练数据的过程,它的目的是获取更好的数据特征,使得机器学习模型可以更好地逼近学习的上限。
特征工程的步骤包括:
- 数据初步分析。这一步是为了对数据做初步的了解,便于在下一步对其进行预处理。
- 数据预处理。包括对变量进行编码,将字符串转换为数字,对变量的取值范围进行缩放等等。
- 特征选择。特征过多对于模型训练会额外增加负担,而且有些特征对于模型的表现影响很小,因此需要对特征进行筛选。
- 降维。维度过大会影响一些模型的效果,因此必要时需要对特征进行降维处理。
上述的每一个步骤都包含一些具体的方法,可以参考本博客中专门介绍特征工程的文章。
模型训练与评估
在对数据做完特征工程之后,接下来需要选择合适的模型来进行训练,并评估模型的训练效果。对于监督学习来说,模型的训练效果可以定量地计算出来;而对于非监督学习的效果,通常需要人为地去评估。下面我们以监督学习为例,来介绍模型训练与评估的流程。
下面的内容中,我们以Sklearn中的iris数据集为例,来说明模型训练与评估的过程。需要注意的是,这一部分的目的主要是为了说明模型的训练过程,再加上iris数据集比较“干净”,因此略去了特征筛选的过程。但是在实际应用中,特征工程这一过程是必不可少的。
1 | from sklearn.datasets import load_iris |
分割数据集
在训练模型之前,首先要对数据集进行分割。sklearn.model_select
模块提供了一些可以用来划分数据集的函数,它们使用起来非常方便。
train_test_split
函数为基本的划分数据集的函数,它会使用一个随机数种子,将数据集打乱之后按照一定的比例划分为训练集和测试集两部分。这一函数可以传入多个要划分的数据,此时会使用一套相同的索引对数据集进行划分,但是要保证这些数据在维度0上的大小相同。函数的返回值为:数据1的训练集、数据1的测试集、数据2的训练集、数据2的测试集……。
其中重要参数的含义如下:
- train_size:可以传入一个整数,代表训练集的大小;或者传入一个0-1的浮点数,代表训练集占数据集的百分比
- test_size:可以传入一个整数,代表测试集的大小;或者传入一个0-1的浮点数,代表测试集占数据集的百分比。需要注意的是,训练集+测试集必须要小于等于整个数据集的大小。二者可以同时传入;也可以只传入一个,另一个会根据传入的参数自动推断
- random_state:一个随机数种子,用于复现数据集的划分方式
1 | from sklearn.model_selection import train_test_split |
此外,sklearn.model_select
模块中的下面这些类可以用来生成划分数据集的索引,对应于一些常用的划分数据集的方法:
- K折交叉验证:将全部训练集划分为K个不相交的子集
KFold
:普通的K折划分GroupKFold
:需要传入一个额外的参数,代表每个样本所属的分组StratifiedKFold
:按照标签的类别比例进行划分,保证每个分组内每一类的比例相同
- 留一法:
LeaveOneOut
:留下一个样本作为测试样本,其余为训练样本LeavePOut
:留下P个样本作为测试样本,其余为训练样本LeaveOneGroupOut
:每个分组留下一个作为测试样本LeavePGroupsOut
:每个分组留下P个作为测试样本
- 随机划分法:
ShuffleSplit
:随机打乱之后再划分GroupShuffleSplit
:同样是随机打乱之后再划分,但是按照分组进行操作StratifiedShuffleSplit
:随机打乱并按照类别比例分层划分
生成索引之后,便可以使用这些索引来获得对应的训练集与测试集。上述这些类的使用方法比较相似,因此下面以StratifiedShuffleSplit
为例来演示其用法:
1 | from sklearn.model_selection import StratifiedShuffleSplit |
[141 139 62 ... 39 106 51] [104 24 47 ... 112 126 119]
2 40
1 40
0 40
dtype: int64
2 10
1 10
0 10
dtype: int64
[ 2 127 8 ... 145 62 38] [138 126 52 ... 10 96 123]
2 40
1 40
0 40
dtype: int64
2 10
1 10
0 10
dtype: int64
我们使用上一步的分层划分结果,对接下来模型训练过程中使用的iris数据集进行分割:
1 | train_data=iris_data[train_index] |
模型选择与训练
在模型的选择上,通常需要考虑很多因素,包括但不限于下面这些:
- 原始数据的类型:不同的模型擅长处理不同类型的原始数据。例如,目前主流的用于处理图片数据的方法是使用卷积神经网络(CNN);对于表格类的数据,通常需要根据特征是连续值还是离散值来判断,如果离散的特征较多,可以考虑朴素贝叶斯、决策树等模型,而连续型特征较多时则通常使用线性回归、逻辑回归等模型;此外,自然语言类或是时间序列数据可以考虑循环神经网络(RNN)模型,图结构模型可以考虑图神经网络(GNN)模型。
- 模型部署环境:训练好的模型需要部署在什么样的环境也是选择模型的一个重要考量。如果需要将模型部署在嵌入式系统中时,就需要在保证模型预测效果的前提下,选择计算速度更快以及体积更小的模型;如果要使用神经网络模型,但是在部署环境中没有GPU的话,就需要控制神经网络的规模;等等。
- 模型的可解释性:在某些场合中需要让训练出来的模型具有一定的可解释性,例如金融的风控系统、科学研究等领域。由于神经网络的可解释性较差,因此在需要模型具有可解释性的场合,就需要选择如线性回归、支持向量机、逻辑回归这样可解释性较好的模型。
- 模型的使用场景:在一些情形如科学研究或者工程建模中,可能会要求训练好的模型可以求导,此时就无法使用类似于决策树这样,函数表达式为分段函数的模型。
在iris数据集中,所有数据中一共包含了3种类型的标签,因此我们要做的是一个多分类的任务。iris数据集的特征都是连续值,因此下面以决策树和支持向量机这两种模型为例,来说明模型的训练过程。需要说明的是,sklearn提供的模型默认支持多分类任务(默认使用One versus Rest分类),不需要使用sklearn.multiclass
中的类对其进行包装。
备注:
sklearn.multiclass
模块提供了OneVsRestClassifier
和OneVsOneClassifier
两种多分类模型的封装函数,使用时传入一个基分类器即可,使用示例:
1
2
3 >from sklearn.multiclass import OneVsRestClassifier
>from sklearn.tree import DecisionTreeClassifier
>ovr_clf=OneVsRestClassifier(DecisionTreeClassifier()) #传入一个基本分类器作为参数
在不同的机器学习模型中,通常都存在着不同的超参数。要调整这些超参数,可以使用网格搜索或随机搜索的方式。sklearn.model_selection
模块提供了GridSearchCV
类和RandomizedSearchCV
类,实现了通过K折交叉验证来寻找最优参数的方法。在寻找模型超参数的时候,可以使用它们来方便操作。在寻找超参数的时候,通常需要给出超参数的取值范围,这一范围的设定依赖于经验以及数据本身的一些特性。
特别需要注意的是,在使用网格搜索或者是随机搜索的fit
函数时,搜索结束之后会自动使用查找到的最优参数组合初始化一个新模型,再使用传入的所有训练集对这个新模型进行训练。也就是说,相当于是超参数搜索完成之后又自动完成了下面两个步骤: 1
2x_classifier=XClassifier(**grid_search_X.best_params_) #X代表模型名称
x_classifier.fit(train_data,train_label)GridSearchCV
类和RandomizedSearchCV
类的时候,传入refit=False
(默认为True)。
搜索完成之后,可以调用类的属性来查看结果,常用的有:
- best_score_:查看最高得分
- best_params_:查看搜索出来的最优参数
- best_estimator_:输出最优模型
- cv_results_:查看搜索的详细结果
首先,我们构造一个决策树模型,使用网格搜索寻找合适的超参数并训练它:
1 | from sklearn.model_selection import GridSearchCV |
然后我们再构造用于多分类的支持向量机模型,这个示例中我们使用随机的参数搜索:
1 | from sklearn.model_selection import RandomizedSearchCV |
备注1:sklearn中的模型分类
下列为经常使用到的监督模型和非监督模型所包含在sklearn的模块:
sklearn.cluster
:包括一些聚类模型sklearn.discriminant_analysis
:包括LDA等模型sklearn.ensemble
:包括Adaboost、GBDT、随机森林等融合模型sklearn.linear_model
:包括线性回归、逻辑回归等模型sklearn.naive_bayes
:包括朴素贝叶斯等模型sklearn.neighbors
:包括K近邻等模型sklearn.svm
:包括支持向量机模型sklearn.tree
:包括决策树模型备注2:一般来说,每一个监督学习的模型都有一个
fit()
和predict()
函数。fit
需要传入两个参数,一个代表训练集的特征矩阵,另一个代表训练集数据的标签;predict
函数需要传入测试集的特征矩阵,模型会根据之前fit
函数学习到的结果进行预测:
***.fit(features, labels)
***.predict(features)
有些分类器可以给出不同的分类所对应的概率,使用
***.predict_proba(features)
即可
模型评估
在模型训练完成之后,需要将测试集的数据送到模型中,得到预测结果,然后可以使用真实值与预测值进行对比。sklearn.metrics
模块提供了一些评价函数,方便我们计算一些评价指标。
对于分类模型,常用的评价函数包括:
log_loss
(限于输出概率值的分类模型)confusion_matrix
precision_score
,recall_score
,f1_score
precision_recall_curve
,roc_auc_score
,roc_curve
accuracy_score
对于回归模型,常用的评价函数有:
mean_squared_error
mean_absolute_error
hinge_loss
这些函数的使用方法比较类似,都需要传入两个参数,一个代表预测值,另一个为真实值。下面我们使用之前训练好的决策树模型和支持向量机模型进行预测,然后再计算它们预测结果的准确率,并查看它们的混淆矩阵:
1 | pred_svc=grid_search_tree.predict(test_data) |
1 | from sklearn.metrics import accuracy_score |
The accuracy of decision tree model: 0.9
The accuracy of support vector classifier model: 0.8666666666666667
接下来,我们计算它们的混淆矩阵,查看它们在对测试集进行分类时的具体表现:
1 | from sklearn.metrics import confusion_matrix |
array([[10, 0, 0],
[ 0, 10, 3],
[ 0, 0, 7]], dtype=int64)
1 | confusion_matrix(pred_svc,test_label) |
array([[10, 0, 0],
[ 0, 9, 3],
[ 0, 1, 7]], dtype=int64)
需要说明的是,由于机器学习通常需要在许多操作中使用到随机数,比如划分数据集、集成模型的boostrap操作、神经网络模型的初始化参数、等等。因此,模型的预测效果具有一定的随机性。在实际应用中,经常需要使用不同的随机数种子多次重复上述过程,再对这些训练好的模型进行融合,从而获得最终的模型。
模型保存与加载
训练好的模型可以将其保存到本机上,并在下次使用的时候加载:
1 | from sklearn.externals import joblib |
实际案例
在下面的内容中,我们将使用Kaggle数据竞赛平台上的Titanic数据集作为示例,来演示如何完成一个实际的机器学习项目。由于下面的内容主要是为了演示操作流程,因此在特征处理、模型选择以及模型超参数调整等问题上,我们采取了一些比较简单的办法。在真实项目中,主要流程与下面所介绍的没有区别,但是每个流程中的具体操作将会更复杂一些。
该数据集的下载地址为:https://www.kaggle.com/c/titanic
1 | train_data=pd.read_csv('titanic/train.csv') #训练集数据,包含标签 |
初步分析
首先,我们需要查看数据所包含的内容,从而对数据的概况做一些了解:
1 | train_data |
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S |
1 | 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
2 | 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S |
3 | 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S |
4 | 5 | 0 | 3 | Allen, Mr. William Henry | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
886 | 887 | 0 | 2 | Montvila, Rev. Juozas | male | 27.0 | 0 | 0 | 211536 | 13.0000 | NaN | S |
887 | 888 | 1 | 1 | Graham, Miss. Margaret Edith | female | 19.0 | 0 | 0 | 112053 | 30.0000 | B42 | S |
888 | 889 | 0 | 3 | Johnston, Miss. Catherine Helen "Carrie" | female | NaN | 1 | 2 | W./C. 6607 | 23.4500 | NaN | S |
889 | 890 | 1 | 1 | Behr, Mr. Karl Howell | male | 26.0 | 0 | 0 | 111369 | 30.0000 | C148 | C |
890 | 891 | 0 | 3 | Dooley, Mr. Patrick | male | 32.0 | 0 | 0 | 370376 | 7.7500 | NaN | Q |
891 rows × 12 columns
同时,我们也需要查看训练集与测试集中的每一个特征是否含有缺失值,并根据这些特征中缺失值的个数、该列其他非缺失值的取值等信息,来决定如何处理缺失值。
1 | train_data.count() |
PassengerId 891
Survived 891
Pclass 891
Name 891
Sex 891
Age 714
SibSp 891
Parch 891
Ticket 891
Fare 891
Cabin 204
Embarked 889
dtype: int64
1 | test_data.count() |
PassengerId 418
Pclass 418
Name 418
Sex 418
Age 332
SibSp 418
Parch 418
Ticket 418
Fare 417
Cabin 91
Embarked 418
dtype: int64
借助于竞赛主办方提供的原始数据描述,同时在网上搜索了一些此项目做特征工程的方法分享,我们可以得到下面的特征处理初步思路:
- PassengerId:乘客的ID号码,对于构造模型没有帮助,可以直接删除。
- Survived:乘客是否存活,1代表存活,0代表未存活。它属于模型要预测的值,因此我们要构造的模型是一个二分类的模型。
- Pclass:乘客船票对应的席位等级,可以取1、2、3三个离散的整数值。我们需要分析它是否与乘客存活显著相关。
- Name:乘客的姓名。对于外国人的姓名来说,其中的头衔或者姓氏可能对应于身份地位比较高或是有特殊身份的人物。但是这样的处理手段要求了解一些相关的知识,因此出于简单考虑,在下面的特征处理过程中我们选择将其直接删除。
- Sex:乘客的性别,有male和female两种。由于机器学习模型无法将字符串作为模型输入直接进行计算,因此需要将这一项转换为数字类型。我们需要分析它是否与乘客存活显著相关。
- Age:乘客年龄,为一系列的离散值。在训练集与测试集中,这一特征的缺失值占了20%,但是由于这一特征应该属于比较重要的特征,不能直接删除,而是需要考虑如何处理;同时由于对这一特征的具体信息仍不够了解,无法得知后续的处理方法,还需要做一些深入的分析。
- SibSp:与乘客一同上船的兄弟姐妹/配偶数量,为一系列离散值。我们需要分析它是否与乘客存活显著相关。
- Parch:与乘客一同上船的父母/子女数量,为一系列离散值。我们需要分析它是否与乘客存活显著相关。
- Ticket:船票编号,在网上的一些分享中提到了其中有少量的编号是重复的,可能对于预测结果有帮助。不过出于简便,我们选择将其直接删除。
- Fare:船票价格,为一系列连续值。我们在测试集中看到Fare存在一个缺失值。考虑到缺失值只有一个,比较简单的办法是直接取其它数据的平均值进行填充。我们需要分析它是否与乘客存活显著相关。
- Cabin:乘客所在的座位号,可以看到它是字母+数字的组合,并存在大量的空值。需要进一步研究以决定如何处理。
- Embarked:乘客在哪个港口上船,为C、Q、S三个字母中的一个。同Sex类似,需要转换为数字进行处理。但是注意到在训练集中,有两条数据的这一特征是缺失的,测试集中没有缺失这一特征的样本,因此我们可以直接删除训练集中的这两个样本。同时,我们需要分析它是否与乘客存活显著相关。
接下来,我们对上述特征进行进一步的分析,以决定如何对其进行处理。
Pclass:这一特征只有3种取值,因此我们可以直接通过做数据透视表进行分析。可以发现席位等级与乘客是否存活具有很强的相关性,因此需要保留这一特征。
1 | pd.crosstab(train_data['Pclass'],train_data['Survived']) |
Survived | 0 | 1 |
---|---|---|
Pclass | ||
1 | 80 | 136 |
2 | 97 | 87 |
3 | 372 | 119 |
Sex:同样通过做数据透视表进行分析,我们发现性别与乘客是否存活也具有很强的相关性,因此需要保留这一特征。
1 | pd.crosstab(train_data['Sex'],train_data['Survived']) |
Survived | 0 | 1 |
---|---|---|
Sex | ||
female | 81 | 233 |
male | 468 | 109 |
Embarked:通过做数据透视表进行分析,我们发现这一特征与乘客是否存活也具有较强的相关性,因此需要保留这一特征。
1 | pd.crosstab(train_data['Embarked'],train_data['Survived']) |
Survived | 0 | 1 |
---|---|---|
Embarked | ||
C | 75 | 93 |
Q | 47 | 30 |
S | 427 | 217 |
Parch:由于这一特征包含的值较多,我们选择作图进行分析。从图中可以看出,Parch的值与生还率有一定的相关性,当Parch的值大于3时生还率较低,小于3时生还率高一些。
1 | sns.barplot(x="Parch", y="Survived", data=train_data) |
SibSp:我们同样选择作图对这一特征进行分析。从图中可以看出,SibSp的值与生还率有一定的相关性,表现出先增高后降低的变化趋势。
1 | sns.barplot(x="SibSp", y="Survived", data=train_data) |
Age:通过查看Age特征的一些统计量,我们发现训练集与测试集上它们的统计学特性差别不大。
1 | train_data['Age'].describe() |
count 714.000000
mean 29.699118
std 14.526497
min 0.420000
25% 20.125000
50% 28.000000
75% 38.000000
max 80.000000
Name: Age, dtype: float64
1 | test_data['Age'].describe() |
count 332.000000
mean 30.272590
std 14.181209
min 0.170000
25% 21.000000
50% 27.000000
75% 39.000000
max 76.000000
Name: Age, dtype: float64
我们作图分析年龄与生还之间是否有关,以及它们之间的关系如何:
1 | facet = sns.FacetGrid(train_data, hue="Survived",aspect=2) |
通过以上分析可知,Age对生还的影响很大,因此我们不能直接拿平均值或者中位数进行填充。可以通过使用其他特征训练一个能够预测Age的模型,让这个模型去预测Age的值。
Fare:我们通过作图分析票价与生还之间的关系,可以发现票价越高的乘客,其生还的概率越高。因此这一特征属于比较重要的特征,需要保留。
1 | facet = sns.FacetGrid(train_data, hue="Survived",aspect=2) |
Cabin:对于Cabin特征,我们先查看NaN值与非NaN值对于生还是否有影响,为此我们可以做一个交叉表来查看。交叉表的内容明显说明了Cabin值是否为NaN对于生还率有着巨大的影响,因此不能直接删除。
1 | pd.crosstab(train_data['Cabin'].isnull(),train_data['Survived']) |
Survived | 0 | 1 |
---|---|---|
Cabin | ||
False | 68 | 136 |
True | 481 | 206 |
由于Cabin特征为字母+数字的组合,我们推测字母应该对应船舱号,而数字对应座位号。我们去掉数字,查看字母部分与最终的生还是否有关。为此,我们需要做另外一个交叉表。从交叉表的内容可以看出,在训练集中不同的船舱号所对应的生还率有着比较明显的区别。
1 | def remove_num(x): |
Cabin | A | B | C | D | E | F | G | N | T |
---|---|---|---|---|---|---|---|---|---|
Survived | |||||||||
0 | 8 | 12 | 24 | 8 | 8 | 5 | 2 | 481 | 1 |
1 | 7 | 35 | 35 | 25 | 24 | 8 | 2 | 206 | 0 |
考虑到船舱号的类别较多,如果使用树模型的话,不需要将船舱号进行独热编码,可以将船舱号中的字母部分取出,作为一个类别;但是如果不使用树模型的话,由于需要转换为独热编码,这样做则会导致维度过高,因此我们只考虑船舱号部分是否为NaN。
数据处理
通过上述的分析,我们得到了数据处理的基本思路。首先我们需要处理的是年龄的缺失值填充问题,我们推测与年龄关系比较大的是Pclass、SibSp、Parch、Fare这四个特征,因此我们考虑使用这四个特征训练一个随机森林模型,用于对缺失值的预测。步骤如下:
1 | train_data_age=train_data.dropna(subset=['Age']) |
1 | from sklearn.ensemble import RandomForestRegressor |
GridSearchCV(estimator=RandomForestRegressor(max_samples=500, n_estimators=200),
param_grid={'max_depth': [4, 5, 6],
'max_leaf_nodes': [10, 15, 20, 30],
'min_samples_leaf': [5, 10, 20],
'min_samples_split': [5, 10, 20, 40]})
接下来,我们准备分别训练以决策树和逻辑回归作为基础模型的两种bagging集成模型(即每个模型独立,模型最终输出取决于它们的平均输出),为此需要实现两种不同的处理方法。为了方便复用,将其定义为函数的形式:
1 | from functools import partial |
1 | from sklearn.preprocessing import LabelEncoder |
1 | train_data_tree,train_label=random_forest_dataprocess(train_data,fill_age_rf) |
在做完基本的数据预处理之后,用于随机森林模型的特征有8个,用于逻辑回归模型的特征有14个。用于随机森林模型的特征数量适中,但是用于逻辑回归特征的数量有些多,需要对其进行筛选。我们使用相关系数筛选的方法,筛选出相关系数排名前10位的特征:
1 | from sklearn.feature_selection import SelectKBest,SelectPercentile |
模型训练与评估
接下来,便可以构造模型进行训练。在训练过程中,我们使用网格搜索的方法来寻找最佳参数。由于网格搜索比较耗费时间,因此我们使用两种基础模型进行超参数优化,并基于搜索结果来构造集成模型。
1 | from sklearn.ensemble import RandomForestClassifier |
1 | from sklearn.linear_model import LogisticRegression |
训练完成之后,接下来使用一些评价标准查看模型的预测结果:
1 | from sklearn.metrics import confusion_matrix |
Confusion matrix of random forest classifier:
[[237 45]
[ 17 119]]
Confusion matrix of logistic regression:
[[244 76]
[ 10 88]]
1 | from sklearn.metrics import accuracy_score |
Accuracy of random forest classifier:
0.8516746411483254
Accuracy of logistic regression:
0.7942583732057417
参考
- https://www.cnblogs.com/wxquare/p/5484636.html
- https://www.zhihu.com/question/29316149
- https://www.cnblogs.com/nolonely/p/7007432.html
- https://zhuanlan.zhihu.com/p/31743196
- https://www.jianshu.com/p/e79a8c41cb1a