Pipeline da Scikit-Learn

Gustavo Coelho
Data Bootcamp
Published in
12 min readJan 18, 2019

Olá pessoal! Aqui é o Gustavo do Pizza de dados! O pessoal do Data Bootcamp me convidou para falar um pouquinho da biblioteca mais que demais para Aprendizado de Máquinas: a Scikit-Learn. Ela é um kit científico para aprendizado (de máquinas, no caso). Nela encontramos tudo, ou quase tudo, que a gente precisa para criar, testar e implementar modelos.

Ela se encontra na versão 0.20.2 no momento em que estou escrevendo esse texto, o que significa que as contribuidoras do projeto ainda não consideram que ela atingiu a maturidade necessária para uma versão 1.0.0. Entretanto, isso não impediu que essa biblioteca se tornasse uma das mais populares para Aprendizado de Máquina, nem impediu que fosse usada extensivamente na indústria. Ela é tão pop que outros projetos semelhantes se inspiram em sua estrutura, como o Spark com seu pacote ml, e criam API’s compatíveis com sua estrutura, como o XGBoost.

Nesse texto vamos falar sobre a Classe Pipeline. Seu uso pode ser considerado avançado para quem está começando, então recomendo ler alguns textos e aplicar um estimador qualquer do Scikit antes, para se familiarizar com o funcionamento da biblioteca.

A ideia dessa classe é criar um objeto que aplica um ou mais transformadores em sequência e no final um estimador. Transformadores são aqueles que transformam nossos dados, como normalização e PCA, e estimadores são os nossos modelos, como a regressão linear.

Motivação — Por que é interessante aprender e usar a Pipeline da Scikit?

Se fosse só pra ser chique eu nem me preocuparia com isso 😋. Utilizar a Pipeline é uma ótima maneira de organizar o seu código, facilita sua leitura e traz benefícios como facilitar a utilização de outras ferramentas da Scikit.

Por exemplo, você já se embananou tentando utilizar o train-test-split (divisão de treino e teste)? Pois é, eu também! Na hora de testar nosso modelo precisamos que a base de teste tenha o mesmo formato da que foi utilizada para treino. Isso significa a mesma quantidade e até os mesmos nomes de colunas. E se você aplicou a divisão da base em treino e teste no início, você precisa aplicar as mesmas transformações que você aplicou na base de treino na de teste. Se você fez isso em uma Pipeline é só chamar ela de novo e pronto, não precisa se preocupar 😃.

E melhor ainda, se você não quer ter o trabalho de dividir sua base de dados toda vez, você pode usar técnicas como Cross-Validation (validação cruzada) e Grid Search (Procura em grade) que são compatíveis com a Pipeline!

Por fim, Pipeline é um ótimo jeito de colocar seu código em produção. Com ela você monta um objeto que recebe dados crus e retorna uma predição. Nada mal, seu arquiteto agradece.

Eu sei que agora você está todo empolgado pra botar a mão na massa, mas espera aí. Só um último ponto sobre a filosofia que seguimos nesse texto antes da parte divertida.

Filosofia

A medida que fui seguindo na minha jornada pelo aprendizado de máquina, fui vendo que várias ferramentas e frameworks seguem alguns princípios interessantes sobre como trabalhar com dados. A partir disso, fiz uma “filosofia” improvisada que acho que cabe bem com o uso da Pipeline da Scikit. É uma mistura dos principais pontos do que encontrei no Cookie Cutter Data Science e no uso do pacote spark.ml. São esses:

Dados devem ser imutáveis

Isso vem de várias cascas de bacana que eu já caí desde que comecei a trabalhar com dados.

Escorregou na nasca de bacana!

Desde a época que eu usava Excel para as minhas análises já aconteceu muito de eu editar meus dados, seja para criar dummies, calcular colunas novas de soma, média e etc, e só depois me tocar q eu não tinha mais os dados originais. Nisso, você perde a reprodutibilidade do seu trabalho e, no caso do Excel, não consegue revisar se houve erros. Quando estamos programando em Python o passo a passo das transformações fica registrado no código, mas mesmo assim é interessante manter a base de dados original sem mudanças. Muitas vezes você precisa pegar alguma informação dela mais à frente no código, por exemplo. Isso já me salvou de algumas enrascadas.

Usando a Pipeline você mantém seus dados originais preservados e evita erros, por exemplo, quando você está testando vários transformadores diferentes alterando seu código.

Reprodutibilidade

Um senhor apontando para o passo dois que diz "então um milagre acontece’ e embaixo escrito "diz "eu acho que você deveria ser um pouco mais especifico aqui no passo 2"

Toda análise que fazemos é para ser compartilhada. TODA. Mas, e se for algo que estou fazendo só para mim mesmo e não vou compartilhar com ninguém? Você deve estar se perguntando. Muito provavelmente você vai compartilhar ela com pelo menos uma pessoa: o seu EU do futuro! E mesmo que você não pretenda retornar à essa análise no futuro, é melhor prevenir do que remediar. Já cansei de fazer análise “de qualquer jeito” achando que ia ficar por isso mesmo e ter que sofrer na hora de atualizar ou compartilhar essas análises. Minha sugestão é: sempre procure a reprodutibilidade, é uma boa prática.

A Pipeline simplifica muito a organização do seu código, fazendo com que outras pessoas, incluindo o seu EU do futuro, consigam ler e entender mais facilmente. Código difícil de ler não é código complexo, só é mal escrito mesmo. Tem gente que acha bonito escrever códigos e textos ilegíveis para mostrar que o que faz é difícil e complexo. Código bom mesmo é aquele que é fácil de ler e entender o que você está fazendo. Já basta a gente aprender e utilizar mil técnicas diferentes de aprendizado de máquina. Deixando o código fácil de ler a gente passa a discutir conceitos e ideias, e deixa de discutir código 😄.

Produção

Isso não é bem uma filosofia. Mas como eu disse antes, o uso da Pipeline é muito bom, se não essencial, para colocar nossos modelos em produção. Para quem não sabe, colocar o modelo em produção significa disponibilizar nosso modelo para uso por terceiros. Seja o cliente da sua empresa, pessoas na internet e etc. Isso pode significar disponibilizar o modelo via uma API ou uma aplicação. De qualquer forma, ter um objeto único com as dependências bem definidas ajuda bastante você a colocar seu código pra rodar e ser utilizado no mundo real.

A classe

Primeiro, vamos dar uma olhada nos argumentos da Pipeline:

Parece bem simples né? steps e memory. E mais! Nem vamos lidar com memory nesse texto. step, como o nome quer dizer, define o passo a passo que a nossa Pipeline vai seguir. step aceita uma lista de tuplas com dois elementos. O primeiro elemento de cada tupla é o nome do passo e o segundo é o transformador da scikit que vai fazer a transformação e/ou treinamento (fit). A última tupla, necessariamente, precisa ter um estimador. Transformadores da scikit possuem os métodos fit e transform, e os estimadores possuem fit e predict.

Mão na massa

Ok, chega de papo. Bora embora. Você já deve estar se coçando. O objetivo dessa parte do texto é dar uma ideia geral das peculiaridades do uso da Pipeline. Tudo escrito a partir daqui foi feito com base na documentação da Scikit-Learn. Todo o código está disponível no Github.

Vamos importar as esferas do dragão

Leia o título no ritmo da música de entrada do Dragon Ball que vai fazer sentido.

Seguindo as boas práticas vamos importar tudo o que vamos usar no início do nosso código.

Importamos o pandas para poder usar a estrutura de dados chamada DataFrame. Perceba que chamamos a scikit-learn de sklearn na hora de importar a biblioteca. EU ODEIO ISSO (faz eu me confundir às vezes), mas beleza. Importamos alguns dados que já vêm com a biblioteca de fábrica para usar nesse exemplo.

Dados

Vamos usar a base de dados de um estudo sobre diabetes para o nosso exemplo. Não vamos focar muito na interpretação dos resultados, mas sim na utilização do código.

Primeiro associamos a base de dados à variável diabetes e especificamos as nossas variáveis independentes (features) no X, e o nosso alvo no y. A notação padrão tem o X maiúsculo por se tratar de uma matriz e o y minúsculo por ser um vetor 😺.

Instanciando

Ok, vamos simplificar e desmistificar ao máximo Programação Orientada a Objetos. Basicamente Pipeline é uma família (classe). A gente precisa escolher (definir) um membro dessa família para usar no nosso código. Ele vai herdar todas as característica da família dele.

Que tal a gente dar o nome dela de pipe?

A pipe é uma Pipeline que faz 3 coisas:

  1. Primeiro ela faz uma transformação Polinomial nas nossas variáveis, ou seja, cria várias colunas novas a partir da multiplicação das colunas por pares.
  2. Depois disso ela aplica um método de redução de dimensionalidade, o PCA ou Análise de Componentes Principais. Pegar os componentes principais de uma base de dados pode ser entendido de forma intuitiva, como se estivéssemos separando os principais sinais que esses dados estão transmitindo. Para o olho nu pode parecer que os dados perdem um pouco de sentido, mas muito modelos adoram olhar para os dados dessa forma e trabalham melhor assim.
  3. Por último, a pipe vai rodar uma regressão de Ridge, para prever o nosso y.

Métricas

Uma das coisas belas da família Pipeline é que elas vem embutidas com o cálculo dos resultados do nosso modelo. Melhor ainda, elas aceitam calcular todas as métricas que quisermos de uma vez!

Vamos montar uma lista com as métricas interessantes para regressão. Uma maneira de chamar esses métodos na scikit é apenas fornecer uma string com um nome. Confira as outras métricas padrões na documentação.

Perceba que deixamos "neg_mean_squared_log_error" comentado para não rodar. Pode ser que o código dê erro só por causa dela. Eu deixo aí para vocês saberem que existe, mas use por sua conta e risco.

Outra coisa que vocês vão reparar é que várias métricas começam com neg, significando que são negativas. Isso acontece porque a scikit maximiza todas as métricas. Tudo ok para R2 e variância explicada, queremos maximizar elas mesmo. Mas nós queremos minimizar os erros. Para não ter que ter um código diferente pra cada tipo de métrica, as contribuidoras colocaram os erros como números negativos para que quando a scikit tente maximizar esses números, eles fiquem o mais próximo de zero possível. Sacou? Minimizar uma variável positiva é a mesma coisa de maximizar uma variável negativa, nesse caso.

Validação cruzada

Como prometido, vamos mostrar como fazer a validação cruzada desse nosso modelo na Pipeline. Para quem nunca viu eu explico como fazer validação cruzada com a scikit em um post do meu blog.

A única diferença é que não tem diferença nenhuma 😋. Vamos colocar nossa pipe no lugar do estimador e show!

Eu gosto de colocar o return_train_score como False para que a função não retorne as métricas de treino. As métricas de treino normalmente são muito boas, principalmente quando usamos modelos mais robustos. Mas isso representa o famigerado sobreajuste (overfitting). Para ter uma estimativa melhor da qualidade do modelo focamos nas métricas calculadas em teste. Em um futuro próximo elas vão depreciar esse uso de return_train_score e vão colocar o padrão como falso mesmo.

A função retorna o seguinte dict:

Out:
{'fit_time': array([0.01254892, 0.01198983, 0.00464511]),
'score_time': array([0.01503205, 0.00954509, 0.00710487]),
'test_explained_variance': array([0.37125582, 0.41891023, 0.44029588]),
'test_neg_mean_absolute_error': array([-48.7818243 , -52.70361757, -47.38501551]),
'test_neg_mean_squared_error': array([-3599.40341608, -3688.61864658, -3188.57772783]),
'test_neg_median_absolute_error': array([-42.71639399, -48.47971835, -46.09899913]),
'test_r2': array([0.37045604, 0.41862605, 0.44028555])}

Todas as métricas têm 3 valores, pois na forma padrão essa função divide o dataset em 3 partes. Essas, portanto, são as métricas de cada um dos 3 testes feitos. Essa função também retorna o tempo de treinamento (fit time) e o tempo que demorou pra calcular as métricas (score time).

Pulo do gato

Mas agora eu quero escolher os parâmetros do meu modelo. Como que eu faço isso?

Para facilitar a nossa vida, toda Pipeline tem o método get_params que pega os parâmetros para a gente. Esse método retorna outro dict com os parâmetros como chave e os parâmetros da Pipeline como valores. Já que queremos mudar os parâmetros vamos pegar só as chaves para saber como chama-los:

Out:
dict_keys([‘memory’, ‘steps’, ‘polinomial’, ‘pca’, ‘regressao’, ‘polinomial__degree’, ‘polinomial__include_bias’, ‘polinomial__interaction_only’, ‘pca__copy’, ‘pca__iterated_power’, ‘pca__n_components’, ‘pca__random_state’, ‘pca__svd_solver’, ‘pca__tol’, ‘pca__whiten’, ‘regressao__alpha’, ‘regressao__copy_X’, ‘regressao__fit_intercept’, ‘regressao__max_iter’, ‘regressao__normalize’, ‘regressao__random_state’, ‘regressao__solver’, ‘regressao__tol’])

Perceba que, como nossa pipe tem 3 passos, a scikit precisa arranjar um jeito de não confundir qual parâmetro é de qual passo. Para isso ele coloca o nome do passo antes dos parâmetros de cada passo. Por exemplo, um dos principais parâmetros da regressão de ridge é o alpha e o nome que demos para esse passo foi "regressao". Então para utilizar ele vamos chamar "regressao_alpha"!

Procura em grade

Fazer a procura em grade (ou Grid Search) é quase a mesma coisa, e eu até prefiro. Isso porque em vez de testar apenas um algoritmo com um conjunto de parâmetros, ela testa esse algoritmo várias vezes cada vez com um conjunto diferente de parâmetros. Usamos isso para tentar descobrir quais parâmetros retornam os melhores resultados.

Para definir quais conjuntos de parâmetros ela vai testar, criamos um dict com os parâmetros como chave (as mesmas que pegamos ali em cima 😉), e uma list com os parâmetros que queremos testar como os valores.

A classe GridSearch vai fazer todas as combinações de todos os elementos de todas a listas e testar uma combinação de cada vez. Nesse caso vamos ter 96 conjuntos de parâmetros sendo testados.

Instanciando GridSearch

Da mesma forma que escolhemos pipe como alguém da família Pipeline, vamos escolher alguém da família GridSearchCV. Como eu e você somos muito criativos, vamos chamar esse ser de grid 😋.

O primeiro argumento é o estimador, no caso a nossa velha pipe que definimos lá em cima. Depois passamos a dict que definimos na seção anterior como os parâmetros da grade em param_grid. Passamos nossa lista de métricas favoritas como sempre em scoring. O argumento verbose serve para você escolher o nível de mensagens que nossa grid vai emitir enquanto treina e testa todas as combinações de parâmetros. Quanto maior, mais mensagens e, portanto, mais detalhes são emitidos enquanto agrid roda. 0 faz com que ela não emita nenhuma mensagem. Que nem antes, colocamos return_train_score como falso porque só queremos saber dos resultados de teste.

Um ponto importante da nossa grid é o parâmetro refit. Pensa assim, vamos testar um bando de combinações de parâmetros, e no final vou ter que escolher a melhor e treinar de novo pra poder usar meu modelo? É para isso que o refit serve! Ele apenas pede que você escolha a sua métrica favorita e vai fazer com que a grid já devolva o modelo que foi melhor naquela métrica treinado pronto pra usar. Maravilhoso!

Roda roda roda

Vamos botar pra rodar! Como qualquer outra classe do scikit, basta a gente usar o método fit.

Dica para enxergar melhor

Ok, rodou. E nada apareceu. Para ver os resultados basta usar o método cv_results_ na nossa grid treinada. Esse método retorna uma dict bem grandinha, na minha opinião fica difícil de ler.

Minha dica pra enxergar melhor é transformar esse dict em um lindo DataFrame. Mas como vocês já devem ter visto nosso Jupyter Notebook não mostra todas as colunas se nosso DataFrame for muito grande. Vamos aumentar a quantidade de colunas que o Jupyter mostra com o comando do pandas pd.set_option("max_columns",200). E que tal a gente já ordenar nossa tabela pra mostrar os melhores valores da nossa métrica favorita em cima? 😉

Esse comando vai retornar algo semelhante à seguinte tabela:

4 melhores resultados

A tabela completa está aqui para quem quiser olhar 😉.

Basicamente é assim que usamos a classe Pipeline para rodar nossos modelos muito doidos cheio de transformadores e outras loucuras. Se vocês pedirem nos comentários quem sabe no futuro a gente não faz um texto sobre como interpretar os resultados, ou focando em algum outro ponto específico 😉.

Finalmente

E é isso aí pessoal! Esse foi mais um post em parceria com o Data Bootcamp. Em breve estará disponível em www.databootcamp.com.br o calendário de turmas para o primeiro semestre de 2019.

Lembrando que as aulas são sempre 100% práticas e dinâmicas, comandadas por instrutores feras e superatualizados.

Também não deixe de ouvir o Pizza de Dados para saber mais sobre novas tendências da área de ciência de dados.

Até a próxima!

--

--

Gustavo Coelho
Data Bootcamp

Python entusiast, undergrad in economics, founder of PyData BSB, co-creator of pizza de dados podcast