如何建立复杂的机器学习项目?

640?wx_fmt=gif

640?wx_fmt=jpeg

翻译 | 光城
责编 | 郭芮
git

scikit-learn提供最早进的机器学习算法。可是,这些算法不能直接用于原始数据。原始数据须要事先进行预处理。所以,除了机器学习算法以外,scikit-learn还提供了一套预处理方法。此外,scikit-learn提供用于流水线化这些估计器的链接器(即变压器,回归器,分类器,聚类器等)。程序员


在本文中,将介绍scikit-learn功能集,容许流水线估计器、评估这些流水线、使用超参数优化调整这些流水线以及建立复杂的预处理步骤。github



640?wx_fmt=png

基本用例:训练和测试分类器面试



对于第一个示例,咱们将在数据集上训练和测试一个分类器。咱们将使用此示例来回忆scikit-learn的API。算法


咱们将使用digits数据集,这是一个手写数字的数据集。api


# 完成数据集的加载
from sklearn.datasets import load_digits
# return_X_y默认为False,这种状况下则为一个Bunch对象,改成True,能够直接获得(data, target)
X, y = load_digits(return_X_y=True)


X中的每行包含64个图像像素的强度,对于X中的每一个样本,咱们获得表示所写数字对应的y。数组


# 下面完成灰度图的绘制
# 灰度显示图像
plt.imshow(X[0].reshape(88), cmap='gray');
# 关闭坐标轴
plt.axis('off')
# 格式化打印
print('The digit in the image is {}'.format(y[0]))


输出:The digit in the image is 0dom


640?wx_fmt=png

在机器学习中,咱们应该经过在不一样的数据集上进行训练和测试来评估咱们的模型。train_test_split是一个用于将数据拆分为两个独立数据集的效用函数,stratify参数可强制将训练和测试数据集的类分布与整个数据集的类分布相同。机器学习


# 划分数据为训练集与测试集,添加stratify参数,以使得训练和测试数据集的类分布与整个数据集的类分布相同。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=42)


一旦咱们拥有独立的培训和测试集,咱们就可使用fit方法学习机器学习模型。咱们将使用score方法来测试此方法,依赖于默认的准确度指标。函数


# 求出Logistic回归的精确度得分
from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=5000, random_state=42)
clf.fit(X_train, y_train)
accuracy = clf.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))
Accuracy score of the LogisticRegression is 0.95


scikit-learn的API在分类器中是一致的。所以,咱们能够经过RandomForestClassifier轻松替换LogisticRegression分类器。这些更改很小,仅与分类器实例的建立有关。


# RandomForestClassifier轻松替换LogisticRegression分类器
from sklearn.ensemble import RandomForestClassifier

clf = RandomForestClassifier(n_estimators=100, n_jobs=-1, random_state=42)
clf.fit(X_train, y_train)
accuracy = clf.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))


输出:


Accuracy score of the RandomForestClassifier is 0.96


练习


完成接下来的练习:


  • 加载乳腺癌数据集,从sklearn.datasets导入函数load_breast_cancer:

# %load solutions/01_1_solutions.py
  • 使用sklearn.model_selection.train_test_split拆分数据集并保留30%的数据集以进行测试。确保对数据进行分层(即便用stratify参数)并将random_state设置为0:

# %load solutions/01_2_solutions.py
  • 使用训练数据训练监督分类器:

# %load solutions/01_3_solutions.py
  • 使用拟合分类器预测测试集的分类标签:

# %load solutions/01_4_solutions.py
  • 计算测试集的balanced精度,须要从sklearn.metrics导入balanced_accuracy_score:

# %load solutions/01_5_solutions.py



640?wx_fmt=png

更高级的用例:在训练和测试分类器以前预处理数据



2.1 标准化数据


在学习模型以前可能须要预处理。例如,一个用户可能对建立手工制做的特征或者算法感兴趣,那么他可能会对数据进行一些先验假设。在咱们的例子中,LogisticRegression使用的求解器指望数据被规范化。所以,咱们须要在训练模型以前标准化数据。为了观察这个必要条件,咱们将检查训练模型所需的迭代次数。


from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=5000, random_state=42)
clf.fit(X_train, y_train)
print('{} required {} iterations to be fitted'.format(clf.__class__.__name__, clf.n_iter_[0]))


输出:


LogisticRegression required 1841 iterations to be fitted


MinMaxScaler变换器用于规范化数据。该标量应该如下列方式应用:学习(即,fit方法)训练集上的统计数据并标准化(即,transform方法)训练集和测试集。 最后,咱们将训练和测试这个模型并获得归一化后的数据集。


from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LogisticRegression

scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000, random_state=42)
clf.fit(X_train_scaled, y_train)
accuracy = clf.score(X_test_scaled, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))
print('{} required {} iterations to be fitted'.format(clf.__class__.__name__, clf.n_iter_[0]))


输出:


Accuracy score of the LogisticRegression is 0.96
LogisticRegression required 190 iterations to be fitted


经过归一化数据,模型的收敛速度要比未归一化的数据快得多(迭代次数变少了)。


2.2 错误的预处理模式


咱们强调了如何预处理和充分训练机器学习模型。发现预处理数据的错误方法也颇有趣。其中有两个潜在的错误,易于犯错但又很容易发现。


第一种模式是在整个数据集分红训练和测试集以前标准化数据。


scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)
X_train_prescaled, X_test_prescaled, y_train_prescaled, y_test_prescaled = train_test_split(
    X_scaled, y, stratify=y, random_state=42)

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000, random_state=42)
clf.fit(X_train_prescaled, y_train_prescaled)
accuracy = clf.score(X_test_prescaled, y_test_prescaled)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))


输出:


Accuracy score of the LogisticRegression is 0.96


第二种模式是独立地标准化训练和测试集。它回来在训练和测试集上调用fit方法。所以,训练和测试集的标准化不一样。


scaler = MinMaxScaler()
X_train_prescaled = scaler.fit_transform(X_train)
# 这里发生了变化(将transform替换为fit_transform)
X_test_prescaled = scaler.fit_transform(X_test)

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000, random_state=42)
clf.fit(X_train_prescaled, y_train)
accuracy = clf.score(X_test_prescaled, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))


输出:


Accuracy score of the LogisticRegression is 0.96


2.3 保持简单,愚蠢:使用scikit-learn的管道链接器


前面提到的两个模式是数据泄漏的问题。然而,当必须手动进行预处理时,很难防止这种错误。所以,scikit-learn引入了Pipeline对象。它依次链接多个变压器和分类器(或回归器)。咱们能够建立一个以下管道:


from sklearn.pipeline import Pipeline

pipe = Pipeline(steps=[('scaler', MinMaxScaler()),
                       ('clf', LogisticRegression(solver='lbfgs', multi_class='auto', random_state=42))])


咱们看到这个管道包含了缩放器(归一化)和分类器的参数。有时,为管道中的每一个估计器命名可能会很繁琐,而make_pipeline将自动为每一个估计器命名,这是类名的小写。


from sklearn.pipeline import make_pipeline
pipe = make_pipeline(MinMaxScaler(),
                     LogisticRegression(solver='lbfgs', multi_class='auto', random_state=42, max_iter=1000))


管道将具备相同的API。咱们使用fit来训练分类器和socre来检查准确性。然而,调用fit会调用管道中全部变换器的fit_transform方法。调用score(或predict和predict_proba)将调用管道中全部变换器的内部变换。它对应于本文2.1中的规范化过程。


pipe.fit(X_train, y_train)
accuracy = pipe.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(pipe.__class__.__name__, accuracy))
Accuracy score of the Pipeline is 0.96


咱们可使用get_params()检查管道的全部参数。


pipe.get_params()


输出:


{
   
   
   

  'logisticregression': LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
           intercept_scaling=1, max_iter=1000, multi_class='auto',
           n_jobs=None, penalty='l2', random_state=42, solver='lbfgs',
           tol=0.0001, verbose=0, warm_start=False),
 'logisticregression__C'1.0,
 ...
 ...
 ...}


练习


重用第一个练习的乳腺癌数据集来训练,能够从linear_model导入SGDClassifier。使用此分类器和从sklearn.preprocessing导入的StandardScaler变换器来建立管道,而后训练和测试这条管道。


# %load solutions/02_solutions.py



640?wx_fmt=png

当更多优于更少时:交叉验证而不是单独拆分



分割数据对于评估统计模型性能是必要的。可是,它减小了可用于学习模型的样本数量。所以,应尽量使用交叉验证。有多个拆分也会提供有关模型稳定性的信息。


scikit-learn提供了三个函数:cross_val_score,cross_val_predict和cross_validate。 后者提供了有关拟合时间,训练和测试分数的更多信息。 我也能够一次返回多个分数。


from sklearn.model_selection import cross_validate

pipe = make_pipeline(MinMaxScaler(),
                     LogisticRegression(solver='lbfgs', multi_class='auto',
                                        max_iter=1000, random_state=42))
scores = cross_validate(pipe, X, y, cv=3, return_train_score=True)


使用交叉验证函数,咱们能够快速检查训练和测试分数,并使用pandas快速绘图。


import pandas as pd

df_scores = pd.DataFrame(scores)
df_scores


输出:


640?wx_fmt=png

# pandas绘制箱体图
df_scores[['train_score''test_score']].boxplot()


输出:


640?wx_fmt=png


练习


使用上一个练习的管道并进行交叉验证,而不是单个拆分评估。


# %load solutions/03_solutions.py



640?wx_fmt=png

超参数优化:微调管道内部



有时但愿找到管道组件的参数,从而得到最佳精度。咱们已经看到咱们可使用get_params()检查管道的参数。


pipe.get_params()


输出:


{
   
   
   

  'logisticregression': LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
           intercept_scaling=1, max_iter=1000, multi_class='auto',
           n_jobs=None, penalty='l2', random_state=42, solver='lbfgs',
           tol=0.0001, verbose=0, warm_start=False),
 'logisticregression__C'1.0,
 ...
 ...
 ...}


能够经过穷举搜索来优化超参数。GridSearchCV 提供此类实用程序,并经过参数网格进行交叉验证的网格搜索。


以下例子,咱们但愿优化LogisticRegression分类器的C和penalty参数。


from sklearn.model_selection import GridSearchCV

pipe = make_pipeline(MinMaxScaler(),
                     LogisticRegression(solver='saga', multi_class='auto',
                                        random_state=42, max_iter=5000))
param_grid = { 'logisticregression__C': [0.11.010],
              'logisticregression__penalty': ['l2''l1']}
grid = GridSearchCV(pipe, param_grid=param_grid, cv=3, n_jobs=-1, return_train_score=True)
grid.fit(X_train, y_train)


输出:


GridSearchCV(cv=3, error_score='raise-deprecating',
       ...
       ...
       ...
       scoring=None, verbose=0)


在拟合网格搜索对象时,它会在训练集上找到最佳的参数组合(使用交叉验证)。 咱们能够经过访问属性cv_results_来获得网格搜索的结果。 经过这个属性容许咱们能够检查参数对模型性能的影响。


df_grid = pd.DataFrame(grid.cv_results_)
df_grid


输出:


640?wx_fmt=png


默认状况下,网格搜索对象也表现为估计器。一旦它被fit后,调用score将超参数固定为找到的最佳参数。


grid.best_params_


输出:


{
   
   
   

  'logisticregression__C': 10, 'logisticregression__penalty''l2'}


此外,能够将网格搜索称为任何其余分类器以进行预测。


accuracy = grid.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(grid.__class__.__name__, accuracy))
Accuracy score of the GridSearchCV is 0.96


最重要的是,咱们只对单个分割进行网格搜索。 可是,如前所述,咱们可能有兴趣进行外部交叉验证,以估计模型的性能和不一样的数据样本,并检查性能的潜在变化。 因为网格搜索是一个估计器,咱们能够直接在cross_validate函数中使用它。


scores = cross_validate(grid, X, y, cv=3, n_jobs=-1, return_train_score=True)
df_scores = pd.DataFrame(scores)
df_scores


输出:


640?wx_fmt=png


练习


重复使用乳腺癌数据集的先前管道并进行网格搜索以评估hinge(铰链) and log(对数)损失之间的差别。此外,微调penalty。


# %load solutions/04_solutions.py



640?wx_fmt=png

总结:个人scikit-learn管道只有不到10行代码(跳过import语句)



import pandas as pd
from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_validate

pipe = make_pipeline(MinMaxScaler(),
                     LogisticRegression(solver='saga', multi_class='auto', random_state=42, max_iter=5000))
param_grid = { 'logisticregression__C': [0.11.010],
              'logisticregression__penalty': ['l2''l1']}
grid = GridSearchCV(pipe, param_grid=param_grid, cv=3, n_jobs=-1)
scores = pd.DataFrame(cross_validate(grid, X, y, cv=3, n_jobs=-1, return_train_score=True))
scores[['train_score''test_score']].boxplot()


输出:


640?wx_fmt=png



640?wx_fmt=png

异构数据:当您使用数字之外的数据时



到目前为止,咱们使用scikit-learn来训练使用数值数据的模型。


X


输出:


array([[ 0.,  0.,  5....,  0.,  0.,  0.],
       [ 0.,  0.,  0....10.,  0.,  0.],
       [ 0.,  0.,  0....16.,  9.,  0.],
       ...,
       [ 0.,  0.,  1....,  6.,  0.,  0.],
       [ 0.,  0.,  2....12.,  0.,  0.],
       [ 0.,  0.10....12.,  1.,  0.]])


X是仅包含浮点值的NumPy数组。可是,数据集能够包含混合类型。


import os
data = pd.read_csv(os.path.join('data''titanic_openml.csv'), na_values='?')
data.head()


输出:


640?wx_fmt=png


泰坦尼克号数据集包含分类、文本和数字特征。咱们将使用此数据集来预测乘客是否在泰坦尼克号中幸存下来。


让咱们将数据拆分为训练和测试集,并将幸存列用做目标。


y = data['survived']
X = data.drop(columns='survived')
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)


首先,能够尝试使用LogisticRegression分类器,看看它的表现有多好。


clf = LogisticRegression()
clf.fit(X_train, y_train)


哎呀,大多数分类器都设计用于处理数值数据。所以,咱们须要将分类数据转换为数字特征。最简单的方法是使用OneHotEncoder对每一个分类特征进行读热编码。让咱们以sex与embarked列为例。请注意,咱们还会遇到一些缺失的数据。咱们将使用SimpleImputer用常量值替换缺失值。


from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
ohe = make_pipeline(SimpleImputer(strategy='constant'), OneHotEncoder())
X_encoded = ohe.fit_transform(X_train[['sex''embarked']])
X_encoded.toarray()


输出:


array([[0., 1., 0., 0., 1., 0.],
       [0., 1., 1., 0., 0., 0.],
       [0., 1., 0., 0., 1., 0.],
       ...,
       [0., 1., 0., 0., 1., 0.],
       [1., 0., 0., 0., 1., 0.],
       [1., 0., 0., 0., 1., 0.]])


这样,能够对分类特征进行编码。可是,咱们也但愿标准化数字特征。所以,咱们须要将原始数据分红2个子组并应用不一样的预处理:(i)分类数据的独热编;(ii)数值数据的标准缩放(归一化)。咱们还须要处理两种状况下的缺失值: 对于分类列,咱们将字符串'missing_values'替换为缺失值,该字符串将自行解释为类别。 对于数值数据,咱们将用感兴趣的特征的平均值替换缺失的数据。


  • 分类数据的独热编:


col_cat = ['sex''embarked']
col_num = ['age''sibsp''parch''fare']

X_train_cat = X_train[col_cat]
X_train_num = X_train[col_num]
X_test_cat = X_test[col_cat]
X_test_num = X_test[col_num]


  • 数值数据的标准缩放(归一化):


from sklearn.preprocessing import StandardScaler

scaler_cat = make_pipeline(SimpleImputer(strategy='constant'), OneHotEncoder())
X_train_cat_enc = scaler_cat.fit_transform(X_train_cat)
X_test_cat_enc = scaler_cat.transform(X_test_cat)

scaler_num = make_pipeline(SimpleImputer(strategy='mean'), StandardScaler())
X_train_num_scaled = scaler_num.fit_transform(X_train_num)
X_test_num_scaled = scaler_num.transform(X_test_num)


咱们应该像在本文2.1中那样在训练和测试集上应用这些变换。


import numpy as np
from scipy import sparse

X_train_scaled = sparse.hstack((X_train_cat_enc,
                                sparse.csr_matrix(X_train_num_scaled)))
X_test_scaled = sparse.hstack((X_test_cat_enc,
                               sparse.csr_matrix(X_test_num_scaled)))


转换完成后,咱们如今能够组合全部数值的信息。最后,咱们使用LogisticRegression分类器做为模型。


clf = LogisticRegression(solver='lbfgs')
clf.fit(X_train_scaled, y_train)
accuracy = clf.score(X_test_scaled, y_test)
print('Accuracy score of the {} is {:.2f}'.format(clf.__class__.__name__, accuracy))


输出:


Accuracy score of the LogisticRegression is 0.79


上面首先转换数据而后拟合/评分分类器的模式刚好是本节2.1的模式之一。所以,咱们但愿为此目的使用管道。


可是,咱们还但愿对矩阵的不一样列进行不一样的处理。应使用ColumnTransformer转换器或make_column_transformer函数。它用于在不一样的列上自动应用不一样的管道。


from sklearn.compose import make_column_transformer

pipe_cat = make_pipeline(SimpleImputer(strategy='constant'), OneHotEncoder(handle_unknown='ignore'))
pipe_num = make_pipeline(SimpleImputer(), StandardScaler())
preprocessor = make_column_transformer((col_cat, pipe_cat), (col_num, pipe_num))

pipe = make_pipeline(preprocessor, LogisticRegression(solver='lbfgs'))

pipe.fit(X_train, y_train)
accuracy = pipe.score(X_test, y_test)
print('Accuracy score of the {} is {:.2f}'.format(pipe.__class__.__name__, accuracy))


输出:


Accuracy score of the Pipeline is 0.79


此外,它还能够被使用在另外一个管道。 所以,咱们将可以使用全部scikit-learn实用程序做为cross_validate或GridSearchCV。


pipe.get_params()


输出:


{
   
   
   

  'columntransformer': ColumnTransformer(n_jobs=None, remainder='drop', sparse_threshold=0.3,
          transformer_weights=None,
          transformers=[('pipeline-1', Pipeline(memory=None,
      ...]}


合并及可视化:


pipe_cat = make_pipeline(SimpleImputer(strategy='constant'), OneHotEncoder(handle_unknown='ignore'))
pipe_num = make_pipeline(StandardScaler(), SimpleImputer())
preprocessor = make_column_transformer((col_cat, pipe_cat), (col_num, pipe_num))

pipe = make_pipeline(preprocessor, LogisticRegression(solver='lbfgs'))

param_grid = { 'columntransformer__pipeline-2__simpleimputer__strategy': ['mean''median'],
              'logisticregression__C': [0.11.010]}
grid = GridSearchCV(pipe, param_grid=param_grid, cv=5, n_jobs=-1)
scores = pd.DataFrame(cross_validate(grid, X, y, scoring='balanced_accuracy', cv=5, n_jobs=-1, return_train_score=True))
scores[['train_score''test_score']].boxplot()


输出:


640?wx_fmt=png


练习


完成接下来的练习:


加载位于./data/adult_openml.csv中的成人数据集,制做本身的ColumnTransformer预处理器,并用分类器管道化它,对其进行微调并在交叉验证中检查预测准确性。


  • 使用pd.read_csv读取位于./data/adult_openml.csv中的成人数据集:

# %load solutions/05_1_solutions.py
  • 将数据集拆分为数据和目标,目标对应于类列。对于数据,删除列fnlwgt,capitalgain和capitalloss:

# %load solutions/05_2_solutions.py
  • 目标未编码,使用sklearn.preprocessing.LabelEncoder对类进行编码:

# %load solutions/05_3_solutions.py
  • 建立一个包含分类列名称的列表,一样,对数值数据也同样:

# %load solutions/05_4_solutions.py


  • 建立一个管道以对分类数据进行读热编码,使用KBinsDiscretizer做为数值数据,从sklearn.preprocessing导入它:

# %load solutions/05_5_solutions.py
  • 使用make_column_transformer建立预处理器,应该将好的管道应用于好的列:

# %load solutions/05_6_solutions.py
  • 使用LogisticRegression分类器对预处理器进行管道传输。随后定义网格搜索以找到最佳参数C.使用cross_validate在交叉验证方案中训练和测试此工做流程:

# %load solutions/05_7_solutions.py


本篇文章翻译自:https://github.com/glemaitre/pyparis-2018-sklearn。

译者简介:光城,研一,我的公众号:guangcity,博客:http://light-city.me/, 我的研究方向知识图谱,正致力于将机器学习运用到KG当中。

声明:本文为做者投稿,版权归其我的全部。




 热 文 推 荐 


☞ 罗永浩、戴威的 C 位消亡史

☞ React vs Angular,哪一种学习成本更低?

☞ 程序员面试被问到“三次握手,四次挥手”怎么办?

“对不起,你的离职是个人错!”

 Gartner的预言:通向混合IT之旅

☞ 阿里“菜鸟”AI?

☞ 刚刚!华为又被美国盯上了!

☞ 心疼!能为程序员男朋友作些什么吗?

640?wx_fmt=gif

 
 

print_r('点个好看吧!');
var_dump('点个好看吧!');
NSLog(@"点个好看吧!");
System.out.println("点个好看吧!");
console.log("点个好看吧!");
print("点个好看吧!");
printf("点个好看吧!");
cout << "点个好看吧!" << endl;
Console.WriteLine("点个好看吧!");
fmt.Println("点个好看吧!");
Response.Write("点个好看吧!");
alert("点个好看吧!")
echo "点个好看吧!"

640?wx_fmt=gif点击“阅读原文”,打开 CSDN App 阅读更贴心!

640?wx_fmt=png 喜欢就点击“好看”吧