Como melhorar a qualidade do código de aprendizado de máquina com o Scikit-learn Pipeline e o ColumnTransformer
Quando você está trabalhando em um projeto de aprendizado de máquina, as etapas mais tediosas geralmente são a limpeza e o pré-processamento de dados. Especialmente quando você está trabalhando em um Jupyter Notebook, executar código em muitas células pode ser confuso.
A biblioteca Scikit-learn tem ferramentas chamadas Pipeline e ColumnTransformer que podem realmente tornar sua vida mais fácil. Em vez de transformar o dataframe passo a passo, o pipeline combina todas as etapas de transformação. Você pode obter o mesmo resultado com menos código. Também é mais fácil entender fluxos de trabalho de dados e modificá-los para outros projetos.
Este artigo mostrará passo a passo como criar o pipeline de aprendizado de máquina, começando com um fácil e trabalhando até um mais complicado.
Se você estiver familiarizado com o pipeline Scikit-learn e o ColumnTransformer, você pode ir diretamente para a parte sobre a qual deseja saber mais.
Índice
- O que é o Scikit-learn Pipeline?
- O que é o Scikit-learn ColumnTransformer?
- Qual é a diferença entre o Pipeline e o ColumnTransformer?
- Como criar um pipeline
- Como encontrar o melhor método de preparação de dados e hiperparâmetro
- Como adicionar transformações personalizadas
- Como escolher o melhor modelo de Machine Learning
O que é o Scikit-learn Pipeline?
Antes de treinar um modelo, você deve dividir seus dados em um conjunto de treinamento e um conjunto de testes. Cada conjunto de dados passará pelas etapas de limpeza e pré-processamento de dados antes de colocá-lo em um modelo de aprendizado de máquina.
Não é eficiente escrever código repetitivo para o conjunto de treinamento e o conjunto de testes. É aí que entra em jogo o pipeline scikit-learn.
O pipeline Scikit-learn é uma maneira elegante de criar um fluxo de trabalho de treinamento de modelo de aprendizado de máquina. Tem a seguinte aparência:
Em primeiro lugar, imagine que você pode criar apenas um pipeline no qual você pode inserir quaisquer dados. Esses dados serão transformados em um formato apropriado antes do treinamento ou previsão do modelo.
O pipeline Scikit-learn é uma ferramenta que une todas as etapas da manipulação de dados para criar um pipeline. Irá encurtar o seu código e torná-lo mais fácil de ler e ajustar. (Você pode até visualizar seu pipeline para ver as etapas dentro.) Também é mais fácil executar o GridSearchCV sem vazamento de dados do conjunto de testes.
O que é o Scikit-learn ColumnTransformer?
Como dito no site scikit-learn, este é o objetivo do ColumnTransformer:
"Este estimador permite que diferentes colunas ou subconjuntos de colunas da entrada sejam transformados separadamente e as características geradas por cada transformador serão concatenadas para formar um único espaço de feição.
Isso é útil para dados heterogêneos ou colunares, para combinar vários mecanismos de extração de recursos ou transformações em um único transformador."
Em resumo, o ColumnTransformer transformará cada grupo de colunas de dataframe separadamente e combiná-las-á mais tarde. Isso é útil no processo de pré-processamento de dados.
Qual é a diferença entre o Pipeline e o ColumnTransformer?
Há uma grande diferença entre Pipeline e ColumnTransformer que você deve entender.
Você usa o pipeline para várias transformações das mesmas colunas.
Por outro lado, você usa o ColumnTransformer para transformar cada conjunto de colunas separadamente antes de combiná-las mais tarde.
Tudo bem, com isso fora do caminho, vamos começar a codificar!!
Como criar um pipeline
Obter o conjunto de dados
Você pode baixar os dados que usei neste artigo a partir deste conjunto de dados kaggle. Aqui está um exemplo do conjunto de dados:
Eu escrevi um artigo explorando os dados deste conjunto de dados que você pode encontrar aqui se estiver interessado.
Em suma, este conjunto de dados contém informações sobre os candidatos a emprego e a sua decisão sobre se querem mudar de emprego ou não. O conjunto de dados tem colunas numéricas e categóricas.
Nosso objetivo é prever se um candidato mudará de emprego com base em suas informações. Esta é uma tarefa de classificação.
Plano de pré-processamento de dados
Observe que eu pulei a codificação de recursos categóricos para a simplicidade deste artigo.
Aqui estão os passos que seguiremos:
- Importar dados e codificação
- Definir conjuntos de colunas a serem transformadas de maneiras diferentes
- Dividir dados para treinar e testar conjuntos
- Criar pipelines para recursos numéricos e categóricos
- Criar ColumnTransformer para aplicar pipeline para cada conjunto de colunas
- Adicionar um modelo a um pipeline final
- Exibir o pipeline
- Passar dados através do pipeline
- (Opcional) Salve o pipeline
Etapa 1: Importar e codificar os dados
Depois de baixar os dados, você pode importá-los usando Pandas assim:
import pandas as pd
df = pd.read_csv("aug_train.csv")
Em seguida, codifice o recurso ordinal usando mapeamento para transformar características categóricas em características numéricas (uma vez que o modelo usa apenas entrada numérica).
# Making Dictionaries of ordinal features
relevent_experience_map = {
'Has relevent experience': 1,
'No relevent experience': 0
}
experience_map = {
'<1' : 0,
'1' : 1,
'2' : 2,
'3' : 3,
'4' : 4,
'5' : 5,
'6' : 6,
'7' : 7,
'8' : 8,
'9' : 9,
'10' : 10,
'11' : 11,
'12' : 12,
'13' : 13,
'14' : 14,
'15' : 15,
'16' : 16,
'17' : 17,
'18' : 18,
'19' : 19,
'20' : 20,
'>20' : 21
}
last_new_job_map = {
'never' : 0,
'1' : 1,
'2' : 2,
'3' : 3,
'4' : 4,
'>4' : 5
}
# Transform categorical features into numerical features
def encode(df_pre):
df_pre.loc[:,'relevent_experience'] = df_pre['relevent_experience'].map(relevent_experience_map)
df_pre.loc[:,'last_new_job'] = df_pre['last_new_job'].map(last_new_job_map)
df_pre.loc[:,'experience'] = df_pre['experience'].map(experience_map)
return df_pre
df = encode(df)
Etapa 2: Definir conjuntos de colunas a serem transformadas de maneiras diferentes
Os dados numéricos e categóricos devem ser transformados de diferentes formas. Assim, defino num_col
para colunas numéricas (números) e cat_cols
para colunas categóricas.
num_cols = ['city_development_index','relevent_experience', 'experience','last_new_job', 'training_hours']
cat_cols = ['gender', 'enrolled_university', 'education_level', 'major_discipline', 'company_size', 'company_type']
Etapa 3: Criar pipelines para recursos numéricos e categóricos
A sintaxe do pipeline é:
Pipeline(steps = [(‘step name’, transform function), …])
Para características numéricas, executo as seguintes ações:
- SimpleImputer para preencher os valores em falta com a média dessa coluna.
- MinMaxScaler para dimensionar o valor para variar de 0 a 1 (isso afetará o desempenho da regressão).
Para recursos categóricos, executo as seguintes ações:
- SimpleImputer para preencher os valores em falta com o valor de maior frequência dessa coluna.
- OneHotEncoder para dividir em muitas colunas numéricas para treinamento de modelo. (handle_unknown='ignore' é especificado para evitar erros quando encontra uma categoria invisível no conjunto de testes)
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, MinMaxScaler
from sklearn.pipeline import Pipeline
num_pipeline = Pipeline(steps=[
('impute', SimpleImputer(strategy='mean')),
('scale',MinMaxScaler())
])
cat_pipeline = Pipeline(steps=[
('impute', SimpleImputer(strategy='most_frequent')),
('one-hot',OneHotEncoder(handle_unknown='ignore', sparse=False))
])
Etapa 4: Criar ColumnTransformer para aplicar o pipeline para cada conjunto de colunas
A sintaxe do ColumnTransformer é:
ColumnTransformer(transformers=[(‘step name’, transform function,cols), …])
Passe colunas numéricas através do pipeline numérico e passe colunas categóricas através do pipeline categórico criado na etapa 3.
remainder='drop' é especificado para ignorar outras colunas em um dataframe.
n_job=-1 significa que usaremos todos os processadores para executar em paralelo.
from sklearn.compose import ColumnTransformer
col_trans = ColumnTransformer(transformers=[
('num_pipeline',num_pipeline,num_cols),
('cat_pipeline',cat_pipeline,cat_cols)
],
remainder='drop',
n_jobs=-1)
Etapa 5: Adicionar um modelo ao pipeline final
Estou usando o modelo de regressão logística neste exemplo.
Crie um novo pipeline para combinar o ColumnTransformer na etapa 4 com o modelo de regressão logística. Eu uso um pipeline neste caso porque todo o dataframe deve passar pela etapa ColumnTransformer e pela etapa de modelagem, respectivamente.
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(random_state=0)
clf_pipeline = Pipeline(steps=[
('col_trans', col_trans),
('model', clf)
])
Etapa 6: Exibir o pipeline
A sintaxe para isso é display(nome do pipeline):
from sklearn import set_config
set_config(display='diagram')
display(clf_pipeline)
Você pode clicar na imagem exibida para ver os detalhes de cada etapa.
Quão conveniente!
Etapa 7: Dividir os dados em conjuntos de trem e teste
Divida 20% dos dados em um conjunto de testes como este:
from sklearn.model_selection import train_test_split
X = df[num_cols+cat_cols]
y = df['target']
# train test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y)
Vou ajustar a tubulação para o conjunto de trem e usar essa tubulação ajustada para o conjunto de teste para evitar vazamento de dados do conjunto de teste para o modelo.
Etapa 8: Passar dados pelo pipeline
Aqui está a sintaxe para isso:
pipeline_name.fit, pipeline_name.predict, pipeline_name.score
pipeline.fit
passa dados através de um pipeline. Ele também se encaixa no modelo.
pipeline.predict
usa o modelo treinado quando pipeline.fit
s para prever novos dados.
pipeline.score
obtém uma pontuação do modelo no pipeline (precisão da regressão logística neste exemplo).
clf_pipeline.fit(X_train, y_train)
# preds = clf_pipeline.predict(X_test)
score = clf_pipeline.score(X_test, y_test)
print(f"Model score: {score}") # model accuracy
(Opcional) Etapa 9: salvar o pipeline
A sintaxe para isso é joblib.dumb
.
Use a biblioteca joblib para salvar o pipeline para uso posterior, para que você não precise criar e ajustar o pipeline novamente. Quando você quiser usar um pipeline salvo, basta carregar o arquivo usando joblib.load assim:
import joblib
# Save pipeline to file "pipe.joblib"
joblib.dump(clf_pipeline,"pipe.joblib")
# Load pipeline when you want to use
same_pipe = joblib.load("pipe.joblib")
Como encontrar o melhor método de preparação de dados e hiperparâmetro
Um pipeline não só torna seu código mais organizado, mas também pode ajudá-lo a otimizar hiperparâmetros e métodos de preparação de dados.
Veja o que abordaremos nesta seção:
- Como encontrar os parâmetros mutáveis do pipeline
- Como encontrar os melhores conjuntos de hiperparâmetros: Adicionar um pipeline à Pesquisa em Grade
- Como encontrar o melhor método de preparação de dados: pular uma etapa em um pipeline
- Como encontrar os melhores conjuntos de hiperparâmetros e o melhor método de preparação de dados
Como encontrar os parâmetros de pipeline mutáveis
Primeiro, vamos ver a lista de parâmetros que podem ser ajustados.
clf_pipeline.get_params()
O resultado pode ser muito longo. Respire fundo e continue a leitura.
A primeira parte é apenas sobre as etapas do gasoduto.
Abaixo da primeira parte você encontrará o que nos interessa: uma lista de parâmetros que podemos ajustar.
O formato é step1_step2_..._parameter.
Por exemplo, col_trans_cat_pipeline_one-hot_sparse significa parâmetro esparso da etapa one-hot.
Você pode alterar parâmetros diretamente usando set_param.
clf_pipeline.set_params(model_C = 10)
Como encontrar os melhores conjuntos de hiperparâmetros: adicionar um pipeline à pesquisa de grade
A Pesquisa de Grade é um método que você pode usar para executar o ajuste de hiperparâmetros. Ele ajuda você a encontrar os conjuntos de parâmetros ideais que produzem a mais alta precisão do modelo.
Defina os parâmetros de ajuste e seu intervalo.
Criar um dicionário de parâmetros de ajuste (hiperparâmetros)
{ ‘tuning parameter’ : ‘possible value’, … }
Neste exemplo, quero encontrar o melhor tipo de penalidade e C de um modelo de regressão logística.
grid_params = {'model__penalty' : ['none', 'l2'],
'model__C' : np.logspace(-4, 4, 20)}
Adicionar o pipeline à Pesquisa em Grade
GridSearchCV(model, tuning parameter, …)
Nosso pipeline tem uma etapa de modelo como etapa final, para que possamos inserir o pipeline diretamente na função GridSearchCV.
from sklearn.model_selection import GridSearchCV
gs = GridSearchCV(clf_pipeline, grid_params, cv=5, scoring='accuracy')
gs.fit(X_train, y_train)
print("Best Score of train set: "+str(gs.best_score_))
print("Best parameter set: "+str(gs.best_params_))
print("Test Score: "+str(gs.score(X_test,y_test)))
Depois de definir a Pesquisa em Grade, você pode ajustar a Pesquisa em Grade com os dados e ver os resultados. Vamos ver o que o código está fazendo:
.fit
: ajusta-se ao modelo e tenta todos os conjuntos de parâmetros no dicionário de parâmetros de ajuste.best_score_
: a mais alta precisão em todos os conjuntos de parâmetros.best_params_
: O conjunto de parâmetros que produzem a melhor pontuação.score(X_test,y_test)
: A pontuação ao tentar o melhor modelo com o conjunto de testes.
Você pode ler mais sobre GridSearchCV na documentação aqui.
Como encontrar o melhor método de preparação de dados: pular uma etapa em um pipeline
Encontrar o melhor método de preparação de dados pode ser difícil sem um pipeline, já que você precisa criar muitas variáveis para muitos casos de transformação de dados.
Com o pipeline, podemos criar etapas de transformação de dados no pipeline e realizar uma pesquisa de grade para encontrar a melhor etapa. Uma pesquisa em grelha selecionará o passo a ignorar e comparará o resultado de cada caso.
Como ajustar um pouco o pipeline atual
Quero saber qual método de dimensionamento funcionará melhor para meus dados entre MinMaxScaler e StandardScaler.
Eu adiciono uma etapa StandardScaler no num_pipeline. O resto não muda.
from sklearn.preprocessing import StandardScaler
num_pipeline2 = Pipeline(steps=[
('impute', SimpleImputer(strategy='mean')),
('minmax_scale', MinMaxScaler()),
('std_scale', StandardScaler()),
])
col_trans2 = ColumnTransformer(transformers=[
('num_pipeline',num_pipeline2,num_cols),
('cat_pipeline',cat_pipeline,cat_cols)
],
remainder='drop',
n_jobs=-1)
clf_pipeline2 = Pipeline(steps=[
('col_trans', col_trans2),
('model', clf)
])
Como executar a pesquisa de grade
Em parâmetros de pesquisa em grade, especifique as etapas que deseja ignorar e defina seu valor como passagem.
Como MinMaxScaler e StandardScaler não devem funcionar ao mesmo tempo, usarei uma lista de dicionários para os parâmetros de pesquisa em grade.
[{case 1},{case 2}]
Se utilizar uma lista de dicionários, a pesquisa em grelha realizará uma combinação de todos os parâmetros no caso 1 até à conclusão. Em seguida, ele executará uma combinação de todos os parâmetros no caso 2. Portanto, não há nenhum caso em que MinMaxScaler e StandardScaler sejam usados juntos.
grid_step_params = [{'col_trans__num_pipeline__minmax_scale': ['passthrough']},
{'col_trans__num_pipeline__std_scale': ['passthrough']}]
Execute a Pesquisa de grade e imprima os resultados (como uma pesquisa de grade normal).
gs2 = GridSearchCV(clf_pipeline2, grid_step_params, scoring='accuracy')
gs2.fit(X_train, y_train)
print("Best Score of train set: "+str(gs2.best_score_))
print("Best parameter set: "+str(gs2.best_params_))
print("Test Score: "+str(gs2.score(X_test,y_test)))
O melhor caso é minmax_scale : 'passthrough', então o StandardScaler é o melhor método de dimensionamento para esses dados.
Como encontrar os melhores conjuntos de hiperparâmetros e o melhor método de preparação de dados
Você pode encontrar os melhores conjuntos de hiperparâmetros e o melhor método de preparação de dados adicionando parâmetros de ajuste ao dicionário de cada caso do método de preparação de dados.
grid_params = {'model__penalty' : ['none', 'l2'],
'model__C' : np.logspace(-4, 4, 20)}
grid_step_params = [{**{'col_trans__num_pipeline__minmax_scale': ['passthrough']}, **grid_params},
{**{'col_trans__num_pipeline__std_scale': ['passthrough']}, **grid_params}]
grid_params serão adicionados ao caso 1 (pular MinMaxScaler) e ao caso 2 (pular StandardScalerand).
# You can merge dictionary using the syntax below.
merge_dict = {**dict_1,**dict_2}
Execute a Pesquisa de grade e imprima os resultados (como uma pesquisa de grade normal).
gs3 = GridSearchCV(clf_pipeline2, grid_step_params2, scoring='accuracy')
gs3.fit(X_train, y_train)
print("Best Score of train set: "+str(gs3.best_score_))
print("Best parameter set: "+str(gs3.best_params_))
print("Test Score: "+str(gs3.score(X_test,y_test)))
Você pode encontrar o melhor conjunto de parâmetros usando .best_params_. Como minmax_scale : 'passthrough', então o StandardScaler é o melhor método de dimensionamento para esses dados.
Você pode mostrar todos os casos de pesquisa em grade usando .cv_results_:
pd.DataFrame(gs3.cv_results_)
Há 80 casos para este exemplo. Há tempo de execução e precisão de cada caso para você considerar, já que às vezes podemos selecionar o modelo mais rápido com precisão aceitável em vez do mais alto precisão.
Como adicionar transformações personalizadas e encontrar o melhor modelo de aprendizado de máquina
Procurar o melhor modelo de aprendizado de máquina pode ser uma tarefa demorada. O pipeline pode tornar essa tarefa muito mais conveniente para que você possa encurtar o ciclo de treinamento e avaliação do modelo.
Aqui está o que abordaremos nesta parte:
- Adicionar uma transformação personalizada
- Encontre o melhor modelo de aprendizagem automática
Como adicionar uma transformação personalizada
Além das funções padrão de transformação de dados, como MinMaxScaler do sklearn, você também pode criar sua própria transformação para seus dados.
Neste exemplo, vou criar um método de classe para codificar recursos ordinais usando mapeamento para transformar recursos categóricos em numéricos. Em palavras simples, mudaremos os dados de texto para números.
Primeiro, faremos o processamento de dados necessário antes do treinamento do modelo de regressão.
from sklearn.base import TransformerMixin
class Encode(TransformerMixin):
def __init__(self):
# Making Dictionaries of ordinal features
self.rel_exp_map = {
'Has relevent experience': 1,
'No relevent experience': 0}
def fit(self, df, y = None):
return self
def transform(self, df, y = None):
df_pre = df.copy()
df_pre.loc[:,'rel_exp'] = df_pre['rel_exp']\
.map(self.rel_exp_map)
return df_pre
Aqui está uma explicação do que está acontecendo neste código:
- Crie uma classe chamada Encode que herda a classe base chamada TransformerMixin do sklearn.
- Dentro da aula, existem 3 métodos necessários:
__init__
,ajustar
etransformar
__init__
serão chamados quando um pipeline for criado. É onde definimos variáveis dentro da classe. Criei uma variável 'rel_exp_map' que é um dicionário que mapeia categorias para números.O ajuste
será chamado ao encaixar a tubulação. Deixei em branco para este caso.transform
será chamada quando uma transformação de pipeline for usada. Este método requer um dataframe (df) como uma entrada, enquanto y está definido como None por padrão (Ele é forçado a ter o argumento y, mas eu não vou usá-lo de qualquer maneira).- Na transformação, a coluna de dataframe 'rel_exp' será mapeada com o rel_exp_map.
Observe que o \
é apenas para continuar o código para uma nova linha.
Em seguida, adicione essa classe Encode como uma etapa de pipeline.
pipeline = Pipeline(steps=[
('Encode', Encode()),
('col_trans', col_trans),
('model', LogisticRegression())
])
Em seguida, você pode ajustar, transformar ou pesquisar em grade o pipeline como um pipeline normal.
Como encontrar o melhor modelo de Machine Learning
A primeira solução que me veio à mente foi adicionar muitas etapas do modelo em um pipeline e pular uma etapa alterando o valor da etapa para 'passthrough' na pesquisa de grade. Isso é como o que fizemos ao encontrar o melhor método de preparação de dados.
temp_pipeline = Pipeline(steps=[
('model1', LogisticRegression()),
('model2',SVC(gamma='auto'))
])
Mas vi um erro como este:
Ah ha – você não pode ter dois modelos de classificação em um pipeline!
A solução para esse problema é criar uma transformação personalizada que recebe um modelo como entrada e executa a pesquisa em grade para encontrar o melhor modelo.
Aqui estão os passos que seguiremos:
- Criar uma classe que recebe um modelo como entrada
- Adicionar a classe na etapa 1 a um pipeline
- Realizar pesquisa em grelha
- Imprimir resultados de pesquisa em grelha como uma tabela
Etapa 1: Criar uma classe que recebe um modelo como entrada
from sklearn.base import BaseEstimator
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
class ClfSwitcher(BaseEstimator):
def __init__(self, estimator = LogisticRegression()):
self.estimator = estimator
def fit(self, X, y=None, **kwargs):
self.estimator.fit(X, y)
return self
def predict(self, X, y=None):
return self.estimator.predict(X)
def predict_proba(self, X):
return self.estimator.predict_proba(X)
def score(self, X, y):
return self.estimator.score(X, y)
Explicação do código:
- Crie uma classe chamada
ClfSwitcher
que herda a classe base chamada BaseEstimator do sklearn. - Dentro da classe, existem cinco métodos necessários, como
o modelo de classificação: __init__
,ajuste
,previsão
,predict_proba
epontuação
__init__
recebe um estimador (modelo) como entrada. Eu afirmei LogisticRegression() como um modelo padrão.ajuste
é para ajuste de modelo. Não há valor de retorno.- Os outros métodos são simular o modelo. Ele retornará o resultado como se fosse o próprio modelo.
Etapa 2: Adicionar a classe na etapa 1 a um pipeline
clf_pipeline = Pipeline(steps=[
('Encode', Encode()),
('col_trans', col_trans),
('model', ClfSwitcher())
])
Etapa 3: Executar a pesquisa de grade
Existem 2 casos usando diferentes modelos de classificação em parâmetros de busca de grade, incluindo regressão logística e máquina vetorial de suporte.
from sklearn.model_selection import GridSearchCV
grid_params = [
{'model__estimator': [LogisticRegression()]},
{'model__estimator': [SVC(gamma='auto')]}
]
gs = GridSearchCV(clf_pipeline, grid_params, scoring='accuracy')
gs.fit(X_train, y_train)
print("Best Score of train set: "+str(gs.best_score_))
print("Best parameter set: "+str(gs.best_params_))
print("Test Score: "+str(gs.score(X_test,y_test)))
O resultado mostra que a regressão logística produz o melhor resultado.
Etapa 4: Imprimir os resultados da pesquisa em grade como uma tabela
pd.DataFrame(gs.cv_results_)
A regressão logística tem uma precisão um pouco maior do que a SVC, mas é muito mais rápida (menos tempo de ajuste).
Lembre-se de que você também pode aplicar diferentes métodos de preparação de dados para cada modelo.
Conclusão
Você pode implementar o pipeline Scikit-learn e o ColumnTransformer desde a limpeza de dados até as etapas de modelagem de dados para tornar seu código mais limpo.
Você também pode encontrar o melhor hiperparâmetro, método de preparação de dados e modelo de aprendizado de máquina com pesquisa em grade e a palavra-chave passthrough.
Você pode encontrar meu código neste GitHub