O Pipeline
assim como o GridSearchCV
são módulos para automatizar processos repetitivos que muitas vezes permeia o processo de tuning de Machine Learning; Este post abordará exclusivamente como implementar o uso do Pipeline
e quando ele deve ser usado ou evitado.
Resumo
- Usado para automatizar parte do processo de tuning de um modelo;
- Pode ser usado erroneamente caso a etapa definida no
Pipeline
seja a mesma sempre (sem alteração de parâmetro); - Foi disponibilizado via Binder um Jupyter Notebook de acompanhamento, nele há detalhes que foram omitidos neste post.
Pipeline
O Pipeline
é um módulo do Scikit Learn (leia a documentação dele aqui) que é usado para automatizar etapas sequenciais. Note que o Pipeline
sempre executará as etapas na mesma ordem e as executará sem exceção. Além disso, o Pipeline
foi concebido para ser executado diversas vezes, pois o objetivo primário dele é testar/avaliar parâmetros e classifiers.
Portanto, ao definir um Pipeline
tenha em mente a utilidade de cada etapa definida nela e se isso realmente faz sentido estar dentro do Pipeline
. Caso você tenha que repetir a mesma etapa para todas as simulações sem ter que ajustar nenhum parâmetro, pode ser um indicativo que essa etapa pode ser alocada antes do Pipeline
com o é o caso da mudança de escala.
A Figura 1 apresenta uma ilustração de diagramas de blocos do Pipeline
, note que o Scaler
será executado todas as vezes que o Pipeline
também o for, logo se o Scaler não tiver nenhum parâmetro para ser ajustado ou testado, ele pode ser executado antes do Pipeline
, conforme a Figura 2.
Figura 1 - Diagrama de Blocos do Pipeline com Scaler.
Figura 2 - Diagrama de Blocos do Pipeline sem Scaler.
Jupyter Notebook
Foi feito um Jupyter Notebook (e disponibilizado pelo Binder) de acompanhamento do post, nele há diversos exemplos e alguns detalhes que foram omitidos neste post.
Dataset
Para elucidação do Pipeline
, será usado o toy dataset de câncer de mama que está presente no próprio Scikit Learn.
# Importação do dataset usado como exemplo
from sklearn import datasets
# Dados de Câncer de mama.
cancer = datasets.load_breast_cancer()
# Criação do dataset features e vetor labels.
features = cancer.data
labels = cancer.target
Classifier
Adota-se como algoritmo de classificação para esse exemplo o Logistic Regression.
# Importação do Logistic Regression.
from sklearn.linear_model import LogisticRegression
# Uso do constructor do AdaBoost para criar um classifier.
clf = LogisticRegression(random_state = 42,
solver = 'lbfgs',
multi_class = 'multinomial')
Etapas do Pipeline
Inicialmente será exposto etapa por etapa e posteriormente será criado o Pipeline
para comparar os benefícios de usá-lo. Desta maneira, este item abordará:
- Mudança de Escala (propositalmente deixado no
Pipeline
); - Diminuição de Dimensão, e;
- Treino e Previsão.
Mudança de Escala
A mudança de escala é feita usando o módulo do MinMaxScaler
que também é parte do Scikit Learn.
# Importação do MinMaxScaler.
from sklearn.preprocessing import MinMaxScaler
# Constructor para criar o objeto do MinMaxScaler.
scaler = MinMaxScaler()
# Treino.
scaler.fit(features)
# Transformação.
features_scaled = scaler.transform(features)
Redução de Variáveis
A diminuição de features é feita usando a Análise de Componentes Principais (PCA
), o Scikit Learn também possui um módulo dedicado para isso.
# Importação do PCA.
from sklearn.decomposition import PCA
# Uso do constructor para criar o objeto do PCA.
pca = PCA(n_components = 10)
# Treino.
pca.fit(features_scaled)
# Tranformação.
features_scaled_pca = pca.transform(features_scaled)
Treino e Previsão
Baseado no classifer clf
, no features dataset e em labels será calculado a acurácia do Logistic Regression. Será utilizado o train_test_split
para criar os datasets de treino e teste.
# Importação do StratifiedShuffleSplit.
from sklearn.model_selection import train_test_split
# Divisão dos features e labels em treino e teste.
features_train, features_test, labels_train, labels_test = train_test_split(features_scaled_pca,
labels,
test_size=0.33,
random_state=42)
Agora faremos o treino e a previsão.
# Treino usando o dataset de treino.
clf.fit(features_train, labels_train)
# Previsões usando o dataset de test.
pred = clf.predict(features_test)
# Importação do módulo para cálculo do Accuracy.
from sklearn.metrics import accuracy_score
# Avaliação.
accuracy_score(labels_test, pred)
0.9787234042553191
Note que todas essas três etapas anteriores podem ser condensadas numa só ao criar um Pipeline
. Os requisitos necessários para o Pipeline
é a criação de uma lista de etapas, conforme essa apresentada abaixo.
# Minha lista de etapas.
etapas_pipeline = [('scaler', scaler), # O scaler está definido na Etapa 5.1.1
('pca' , pca), # O pca está definido na Etapa 5.1.2
('clf' , clf)] # O clf está definido no item 4 e foi usado na Etapa 5.1.3
Note que estou usando os objetos criados nas etapas anteriores, mas eu posso usar o próprio constructor, logo atualizando a lista etapas_pipeline
, tem-se:
# Minha lista de etapas.
etapas_pipeline = [('scaler', MinMaxScaler()), # Etapa 5.1.1
('pca' , PCA(n_components = 10)), # Etapa 5.1.2
('clf' , LogisticRegression(random_state = 42, # Etapa 5.1.3
solver = 'lbfgs',
multi_class = 'multinomial'))]
Após a definição da lista de etapas, pode-se criar a partir do constructor do Pipeline
o objeto pipe
.
# Importação do Pipeline
from sklearn.pipeline import Pipeline
# Uso do constructor para criação do objeto do Pipeline.
pipe = Pipeline(etapas_pipeline)
Analogamente ao processo feito passo-a-passo, utilizarei o mesmo train_test_split para dividir features e labels em treino e teste.
# Divisão dos features e labels em treino e teste.
features_train_2, features_test_2, labels_train_2, labels_test_2 = train_test_split(features,
labels,
test_size=0.33,
random_state=42)
Por fim, consideramos o pipe
como um objetivo parecido com um classifier, será treinado e depois utilizado para fazer previsões.
# Treino do pipe.
pipe.fit(features_train_2, labels_train_2);
# Fazendo previsões.
pred_2 = pipe.predict(features_test_2)
# Calculando o accuracy.
accuracy_score(labels_test_2, pred_2)
0.9787234042553191
Note que o Pipeline
aplica ocultamente os processos fit
e transform
das primeiras duas etapas, deixando apenas a parte de fit
e predict
do classifier. Logo, é um requisito necessário para que o Pipeline
funcione que as etapas que antecedem o classifier possuam esses dois métodos.
Não há como ter dois classifiers como etapas do Pipeline, pois ele só admite um e deve ser a última etapa.
O motivo é simples: Os classifiers geralmente não possuem o método transform
, o que é um requisito das etapas internas do Pipeline
.
Múltiplos Classifiers
A maneira de usar diversos classifiers no Pipeline é usando dois dicionários. O primeiro serve para armazenar os classfiers e o segundo os tuples requeridos pelo Pipeline
funcionar.
Dessa maneira, define-se o dicionário de classifiers:
# Importação do Naïve Bayes.
from sklearn.naive_bayes import GaussianNB
# Dicionário de classifiers.
meus_classifiers = {'logit':('lr', LogisticRegression(random_state = 42,
solver = 'lbfgs',
multi_class = 'multinomial')),
'bayes':('gnb', GaussianNB())}
Observe que o dicionário meus_classifiers
somente funcionará caso seja definido o key
, caso contrário o Pipeline
não conseguirá interpretá-lo.
Agora, vamos inserir o dicionário meus_classifiers
como um dos elementos de etapas_pipeline
. A chave escolhida foi logit
.
# Minha lista de etapas com um dicionário aninhado.
etapas_pipeline_2 = [('scaler', MinMaxScaler()),
('pca' , PCA(n_components = 10)),
meus_classifiers['logit']]
Implicitamente, sabe-se que meus_classifiers['logit']
tem o mesmo significado de:
LogisticRegression(random_state = 42,
solver = 'lbfgs',
multi_class = 'multinomial')
Portanto, pode-se treinar e calcular as previsões.
# Uso do constructor para criação do objeto do Pipeline.
pipe_2 = Pipeline(etapas_pipeline_2)
# Treino do pipe.
pipe_2.fit(features_train_2, labels_train_2);
# Fazendo previsões.
pred_4 = pipe.predict(features_test_2)
# Calculando o accuracy.
accuracy_score(labels_test_2, pred_4)
0.9840425531914894
Caso seja necessário a simulação para o Naïve Bayes
, pode-se substituir a chave para bayes
e executar novamente o treino e a previsão.
# Uso do constructor para criação do objeto do Pipeline.
pipe_3 = Pipeline([('scaler', MinMaxScaler()),
('pca' , PCA(n_components = 10)),
meus_classifiers['bayes']]) # Estou usando o Naïve Bayes agora.
# Treino do pipe.
pipe_3.fit(features_train_2, labels_train_2);
# Fazendo previsões.
pred_5 = pipe_3.predict(features_test_2)
# Calculando o accuracy.
accuracy_score(labels_test_2, pred_5)
0.9202127659574468
Note que o uso do Pipeline
simplifica o processo de tuning, pois a aplicação de fit
e transform
são “omitidos” ao serem aplicados “automaticamente”.
Conclusões
O Pipeline
pode automatizar grande parte do processo de calibragem do modelo e ser benéfico ao workflow, mas ele também pode ser usado de forma errada quando se adiciona etapas sem uma análise crítica. Um exemplo é o que foi feito nesse artigo, a etapa de mudança de escala das features foi feito sempre que o Pipeline
foi executado.
Observe que convenientemente deve-se fazer a mudança de escala para otimizar o processo do Gradient Descent dos algoritmos que são baseados em distância, logo de forma geral é muito aconselhável fazer a mudança de escala para todos os algoritmos, pois não há nenhuma desvantagem em mudar a escala das features. Portanto, a remoção do MinMaxScaler
da lista de etapas e executando ele antes do Pipeline
resultará num algoritmo mais eficiente, pois ele será executado uma vez só.
Já para o caso do PCA
é conveniente que se deixe dentro do Pipeline
, pois dará a oportunidade de variarmos a quantidade de componentes principais (n_components
).
Por fim, tenha em mente que para datasets pequenos o tempo de execução não é algo a se preocupar, mas em casos reais onde o tempo é um fator limitante, eliminar repetições desnecessárias é fundamental para otimizar os gastos dos seus recursos (tempo, dinheiro, esforço, paciência, etc.).