Auto Machine Learning笔记 - Pipelines 制作教程

面向读者:

  • 机器学习背景对 AutoML 感兴趣,熟悉并喜欢 sklearn
  • 发现自己在相似分析中做着重复的步骤
  • kaggle 进阶者

    背景:

  • 本文是以一个文本数据处理的例子来展示pipeline如何把小功能串在一起,实现流水线操作。

Once you’ve gotten your feet wet in basic sklearn modeling, you might find yourself doing the same few steps over and over again in the same analysis. To get to the next level, pipelines are your friend!

有些东西你不知道,以为它不存在;一旦你知道后,发现满世界都是它。pipeline就是这样的。

概念解释

pipeline(管道)

  • 顾名思义就是把标准的/固有的建模过程流水线化。
  • 假如你有一套通用的数据清洗流程,就可以写成一个pipeline,这样就不用根据不同的数据一遍遍的重复写这个清洗流程了。
  • pipeline是一块块的小逻辑的集成函数,尤其当模型十分复杂时,便于回头检查模型逻辑。
  • pipeline是一个类,一般继承sklearn的 BaseEstimator,TransformerMixin。
  • 拥有 fit/transform/predict 等功能和属性。

下载数据集

✔数据集下载链接

点击图片右上角的 ‘Download All ’,并解压数据集。

构建本地文件结构:

1
2
3
4
5
6
|-pipelines //文件名
|- pipeline.py //新建python文件
|- data //刚才下载且解压的数据集
|- train.csv //训练集
|- test.csv //测试集
|- sample_submission.csv //比赛结果提交样本,本文中用不到

打开pipeline.py,输入:

1
2
3
4
5
6
7
8
9
import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

df = pd.read_csv('data/train.csv')

df.dropna(axis=0)
df.set_index('id', inplace = True)

df.head()

输出:

id text author
id26305 This process, however, afforded me no means of… EAP
id17569 It never once occurred to me that the fumbling… HPL
id11008 In his left hand was a gold snuff box, from wh… EAP
id27763 How lovely is spring As we looked from Windsor… MWS
id12958 Finding nothing else, not even gold, the Super… HPL

可以看到数据集是文本信息,3列,包含id,text文本,和作者。这个比赛的原意是给出一段文字,预测是出自哪个作家之手,模型用来学习作家的文风。

文本特征预处理

以下为适用于所有文本的数据清洗操作:

  • 将文本信息去标点符号,且全部用小写字母
  • 计算文本长度
  • 计算文本字数
  • 计算 非停用词 字数
  • 计算 非停用词单词的 平均长度
  • 计算逗号数

先用传统的统计方式来进行数据清洗,输入:

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
import re
from nltk.corpus import stopwords

stopWords = set(stopwords.words('english')) # 可能需要手动下载 stopwords

#creating a function to encapsulate preprocessing, to mkae it easy to replicate on submission data
def processing(df):
#lowering and removing punctuation
df['processed'] = df['text'].apply(lambda x: re.sub(r'[^\w\s]','', x.lower()))

#numerical feature engineering
#total length of sentence
df['length'] = df['processed'].apply(lambda x: len(x))
#get number of words
df['words'] = df['processed'].apply(lambda x: len(x.split(' ')))
df['words_not_stopword'] = df['processed'].apply(lambda x: len([t for t in x.split(' ') if t not in stopWords]))
#get the average word length
df['avg_word_length'] = df['processed'].apply(lambda x: np.mean([len(t) for t in x.split(' ') if t not in stopWords]) if len([len(t) for t in x.split(' ') if t not in stopWords]) > 0 else 0)
#get the average word length
df['commas'] = df['text'].apply(lambda x: x.count(','))

return(df)

df = processing(df)

df.head()

输出:

创建 Pipeline

拆分训练集和测试集,输入:

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

features= [c for c in df.columns.values if c not in ['id','text','author']]
numeric_features= [c for c in df.columns.values if c not in ['id','text','author','processed']]
target = 'author'

X_train, X_test, y_train, y_test = train_test_split(df[features], df[target], test_size=0.33, random_state=42)
X_train.head()

输出:

id processed length words words_not_stopword avg_word_length commas
id19417 this panorama is indeed glorious and … 91 18 6 6.666667 1
id09522 there was a simple natural earnestness … 240 44 18 6.277778 4
id22732 who are you pray that i duc de lomelette … 387 74 38 5.552632 9
id10351 he had gone in the carriage to the nearest … 118 24 11 5.363636 0
id24580 there is no method in their proceedings … 71 13 5 7.000000 1

接下来是关键步骤。

  • 根据特征是否为数值型,创建 两个selector transformers: TextSelector,NumberSelector

  • selector的作用:输入一个column,根据这个selector transformer,输出得到一个新column

  • 简单说就是,做 data transformation,收集想要的信息,比如 text length

输入:

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
from sklearn.base import BaseEstimator, TransformerMixin

class TextSelector(BaseEstimator, TransformerMixin):
"""
Transformer to select a single column from the data frame to perform additional transformations on
Use on text columns in the data
"""
def __init__(self, key):
self.key = key

def fit(self, X, y=None):
return self

def transform(self, X):
return X[self.key]

class NumberSelector(BaseEstimator, TransformerMixin):
"""
Transformer to select a single column from the data frame to perform additional transformations on
Use on numeric columns in the data
"""
def __init__(self, key):
self.key = key

def fit(self, X, y=None):
return self

def transform(self, X):
return X[[self.key]]

先来试一下 TextSelector 好不好用。由小变大,先创建一个mini pipeline,作用是先从数据集中抓取一列数据,再做tf-idf处理并返回结果。

创建过程只需传递一个格式如(名称,对象)的元组。括号左边是动作的名称,右边就是选取的列名。所以这个mini pipeline就是两个动作,selecting(选择一列)和tfidf-ing(对这列进行tf-idf处理)。

执行pipeline的命令,可以调用 text.fit() 来适应训练集,text.transform() 来应用于训练集,或者text.fit_transform() 来执行两者。

由于它是一个文本,它将返回一个稀疏矩阵,输入:

1
2
3
4
5
6
7
8
9
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer

text = Pipeline([
('selector', TextSelector(key='processed')),
('tfidf', TfidfVectorizer( stop_words='english'))
])

text.fit_transform(X_train)

输出:

接下来试一下 NumberSelector 对于数值型的特征处理好不好用,同样也先建立一个mini pipeline来观察效果。

这个pipeline操作就定为简单的scaler,一列列的进行数值的StandardScaler。先以 length列为例,仍然是两个步骤,先选列,即length列,再做数值StandardScaler。(StandardScaler是数据预处理的一个常见的数值缩放操作。)

输入:

1
2
3
4
5
6
7
8
from sklearn.preprocessing import StandardScaler

length = Pipeline([
('selector', NumberSelector(key='length')),
('standard', StandardScaler())
])

length.fit_transform(X_train)

输出:

根据输出结果可以看出,pipeline返回一个我们想要的数值缩放矩阵。然后把剩下的数值特征列都进行缩放scaler操作。当然这个数据处理操作你可以随意更改成其他可用的。

输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
words =  Pipeline([
('selector', NumberSelector(key='words')),
('standard', StandardScaler())
])
words_not_stopword = Pipeline([
('selector', NumberSelector(key='words_not_stopword')),
('standard', StandardScaler())
])
avg_word_length = Pipeline([
('selector', NumberSelector(key='avg_word_length')),
('standard', StandardScaler())
])
commas = Pipeline([
('selector', NumberSelector(key='commas')),
('standard', StandardScaler()),
])

创建 FeatureUnion

pipeline管道可大可小,又大又长又粗的pipeline也是由一个个mini pipelines组成的嘛。

接下来使用FeatureUnion来连接上面做好的pipelines,形成一个类似大的pipeline。

语法操作还是格式如(名称,对象)的元组。FeatureUnion本身不是pipeline,它只是一个组合,所以需要多写一行代码,将其变为一个大pipeline。然后的事情,你懂的,还是fit,transform,或者fit_transform操作。

输入:

1
2
3
4
5
6
7
8
9
10
11
from sklearn.pipeline import FeatureUnion

feats = FeatureUnion([('text', text),
('length', length),
('words', words),
('words_not_stopword', words_not_stopword),
('avg_word_length', avg_word_length),
('commas', commas)])

feature_processing = Pipeline([('feats', feats)])
feature_processing.fit_transform(X_train)

输出:

甚至可以在刚刚的大pipeline尾巴上再添加一个分类器,即不仅仅是数据转化,而是增加建模/预测功能。还是原来的套路,写元组,再pipeline一下。

可以得到粗糙的 63.8%的分类精度。小试牛刀,不要太在意这些细节~

输入:

1
2
3
4
5
6
7
8
9
10
11
from sklearn.ensemble import RandomForestClassifier

pipeline = Pipeline([
('features',feats),
('classifier', RandomForestClassifier(random_state = 42)),
])

pipeline.fit(X_train, y_train)

preds = pipeline.predict(X_test)
np.mean(preds == y_test)

输出:

1
0.638347260909935

再看 Pipeline

现在可以得出的结论就是,pipeline不仅能做数据预处理的流水线,更是能把整个建模套路做成流水线,只需在pipeline的结尾加上一个分类器。接下来将创建一个pipeline,完成上面所有的处理,最后用随机森林分类器。

优化 Pipeline

利用 Cross Validation 寻找更优的pipeline,就要先观察pipeline的属性,再进行超参数调参。

输入:

1
pipeline.get_params().keys()

输出:

这些都是pipeline相关的属性,即超参数,这些超参数的组合变化,超参数的数值变化都会影响一个pipeline好不好用。在此只为展示操作,因此随心情挑选四个超参数进行调优。优化方式为GridSearchCV,即 网格搜索交叉验证法,适用于少量的超参数个数和少量的数值候选调优。

输入:

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

hyperparameters = { 'features__text__tfidf__max_df': [0.9, 0.95],
'features__text__tfidf__ngram_range': [(1,1), (1,2)],
'classifier__max_depth': [50, 70],
'classifier__min_samples_leaf': [1,2]
}
clf = GridSearchCV(pipeline, hyperparameters, cv=5)

# Fit and tune model
clf.fit(X_train, y_train)

输出:

观察调优结果,即超参数最终选择的数值为多少,输入:

1
clf.best_params_

输出:

隐藏菜单操作为调用 refit,可自动使用使用pipeline来fit所有的训练数据。并将其应用于测试集。

输入:

1
2
3
4
5
6
7
#refitting on entire training data using best settings
clf.refit

preds = clf.predict(X_test)
probs = clf.predict_proba(X_test)

np.mean(preds == y_test)

输出:

1
0.6425255338904364

还是有一点精度的提高的。

进行预测

做模型总要有结果的,最后对数据集进行predict,看看未知文本到底是哪位作者写出来的概率更大。

输入:

1
2
3
4
5
6
7
8
9
10
11
12
submission = pd.read_csv('data/test.csv')

#preprocessing
submission = processing(submission)
predictions = clf.predict_proba(submission)

preds = pd.DataFrame(data=predictions, columns = clf.best_estimator_.named_steps['classifier'].classes_)

#generating a submission file
result = pd.concat([submission[['id']], preds], axis=1)
result.set_index('id', inplace = True)
result.head()

输出:

Pipeline 总结

  • sklean提供的pipeline来将多个学习器组成流水线,通常流水线的形式为:

    将数据标准化的学习器—-特征提取的学习器—-执行预测的学习器/分类器

  • 除了最后一个学习器之外,前面的所有学习器必须提供transform方法,该方法用于数据转

    (例如: 归一化,正则化,以及特征提取)

参考链接

would you buy me a coffee☕~
0%