River na Prática: Resolvendo um problema de classificação com Aprendizado de Máquina Online
Tags: machinelearning, ai, braziliandevs
Published: 2023-10-24 14:14:03

## Requisitos

Para seguir esse tutorial espera-se que você tenha conhecimento suficiente da linguagem de programação Python e do uso de ferramentas como Google Colab ou Jupyter-notebook.

Ter lido os dois primeiros artigos da série de artigos sobre River que eu escrevi é desejável.

## Instalação

A biblioteca River é feita para funcionar a partir do Python 3.8. A instalação pode ser feita usando o gerenciador de pacotes pip:

```bash
# Via notebook
!pip install river

# Via terminal
pip install river
```

## Problema de Classificação

Nesse tutorial vamos explorar um problema de classificação usando o famoso dataset titanic.

<aside>
💡 O motivo de eu usar um dataset tradicional e em lote para um tutorial de aprendizado em streaming é puramente educacional. Além disso, como dito no artigo anterior, a biblioteca River tem ferramentas para ler conjuntos de dados em lote no formato de streaming para avaliação do modelo e para propósitos de estudo, como o desse tutorial.

</aside>

<aside>
💡 Esse dataset poderia ser considerado um dataset proativo, pois temos controle direto sobre os dados. Para saber mais confira o artigo de introdução ao River: https://dev.to/nobrelucas/river-aprendizado-de-maquina-online-com-python-uma-introducao-conceitual-3bb2

</aside>

Instale os três arquivos do dataset no link: https://www.kaggle.com/competitions/titanic/data

Adicione o dataset no seu workspace e vamos começar a trabalhar.

<aside>
💡 Recomendo criar uma pasta “data” no seu workspace para adicionar todos os dados

</aside>

### Preparação dos Dados

Com o seu notebook aberto, vamos ao código. Começamos criando um DataFrame pandas com os dados de treino e observando as cinco primeiras linhas.

```python
import pandas as pd

dataframe = pd.read_csv('data/train.csv')
dataframe.head()
```

Isso deve mostrar o seguinte resultado:


![Cinco primeiras linhas do conjunto de treino](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/369qde6ll0byb7ervrar.png)

<aside>
💡 Os detalhes de cada coluna desse dataset podem ser encontrados no link no qual você baixou o conjunto de dados.

</aside>

Vamos nos concentrar em fazer um modelo simples. Nesse caso, as colunas que mais nos interessam são:

- Survived (Alvo): É uma coluna categórica com duas classes (1 para sobreviventes e 0 para não sobreviventes)
- Pclass: É uma coluna categórica com três classes (1 para primeira classe, 2 para segunda classe e 3 para terceira classe)
- Sex: Coluna categórica indicando o sexo do passageiro

Escolhi as features Pclass e Sex porque, historicamente, mulheres ricas foram as pessoas que mais sobreviveram.

Vamos filtrar essas colunas que são as únicas que nos interessam no momento

```python
dataframe = dataframe[['Survived', 'Pclass', 'Sex']]
dataframe.head()
```

Mostrando o seguinte resultado:


![cinco primeiras linhas do conjunto de treino após filtrado](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1rkr69uf5x84y5fql7nw.png)

Agora vamos transformar a coluna Sex em uma coluna numérica (1 para male e 2 para female)

```python
dataframe['Sex'] = dataframe['Sex'].replace({'male': 1, 'female': 2})
dataframe.head()
```

Obtendo como resultado:


![Cinco primeiras linhas do conjunto de treino após mudança na coluna de sexo do passageiro](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6l1j904xm5dho091ijcb.png)

Por fim precisamos separar o dataset em features de treino e alvo (target). No nosso caso, Survived é o alvo e as demais colunas são as features de treino. Também irei transformar a coluna alvo de numérica para booleana para se adaptar ao modelo e a forma que o River trabalha.

```python
features = dataframe[['Pclass', 'Sex']]
target = dataframe['Survived'].replace({0: False, 1: True})
```

Com todos nossos dados estando no formato desejado e separados em features e target, podemos começar a efetivamente trabalhar com o modelo.

### Criação e treinamento do modelo online

O objetivo de um classificador é predizer qual a categoria de uma determinada amostra, predizer um alvo y para um conjunto de características x. Vamos fazer isso usando um modelo de regressão logística.

```python
from river import linear_model

model = linear_model.LogisticRegression()
```

Com o código acima nós instanciamos o modelo.

<aside>
💡 Eu irei frequentemente instanciar o modelo do começo para treiná-lo de formas diferentes.

</aside>

Para treinar o modelo, basta alimentar ele com uma amostra dos dados por vez, mas como fazer isso se nossos dados são um grande lote? O módulo `stream` do River nos ajuda a lidar com isso e tem uma função chamada iter_pandas, que nos permite iterar um dataframe ou uma série pandas como se os dados estivessem chegando para nós em stream.

Experimente:

```python
from river import stream

for X, y in stream.iter_pandas(features, target):
    print(X, y)
```

Cada print vai mostrar um dicionário com as features e um valor para cada uma delas e ao lado o valor do target para aquela amostra.

Mas o que queremos não é simplesmente ver as amostras e sim ensinar ao modelo a predizer se dada uma amostra um passageiro sobreviveu ou não.

Para isso, podemos usar o método `learn_one(X, y)`. Entretanto, vamos criar um objeto iterável chamado “stream_dataset” ao invés de chamar a função iter_pandas diretamente no laço de repetição. A motivação para isso vai ficar mais clara futuramente.

```python
from river import stream
stream_dataset = stream.iter_pandas(features, target)

for X, y in stream_dataset:
    model.learn_one(X, y)
```

Como estamos em um ambiente de aprendizado, vamos usar o mesmo dataframe para testar o modelo. Para um primeiro teste, vamos tentar predizer a primeira amostra do dataframe (homem que estava na terceira classe do navio e que não sobreviveu).

```python
stream_dataset = stream.iter_pandas(features, target)
x, y = next(stream_dataset)
model.predict_one(x)
```

<aside>
💡 Notem que eu instancio o objeto stream_dataset novamente para poder iterar ele novamente desde o início. Isso irá se repetir durante o resto do tutorial para experimentar diversas abordagens de treino e de validação.

</aside>

O resultado do código acima deve ser `false` pois de fato esse passageiro não sobreviveu.

Verificar o que o modelo prediz uma amostra de cada vez não é uma boa estratégia, então vamos avaliar o modelo de uma forma bem comum para iniciantes em aprendizado de máquina: armazenando a predição do modelo para cada iteração e verificar a classe real para aquela amostra e depois calcular uma certa “acurácia” para essas predições.

<aside>
💡 Nesse tutorial vou usar como métrica o ROC AUC, uma métrica muito comum para modelos de classificação. Você pode saber mais sobre essa métrica no link: https://mariofilho.com/guia-completo-sobre-roc-auc-em-machine-learning/

</aside>

```python
from river import metrics

metric = metrics.ROCAUC(n_thresholds=20)

stream_dataset = stream.iter_pandas(features, target)

for X, y in stream_dataset:
    y_pred = model.predict_proba_one(X)
    model.learn_one(X, y)
    metric.update(y, y_pred)

metric
```

O resultado do código acima deve retornar algo por volta de 81%, o que não é um valor a se jogar fora. Mas vamos experimentar uma outra abordagem, mais próxima do padrão de treinamento e de validação do River.

A biblioteca River permite criar um modelo como uma Pipeline, ou seja, um processo de passos para o treinamento. Isso é feito simplesmente adicionando “|” entre os processos desejados. 

Além disso, como dito no artigo de introdução do River, o aprendizado e a inferência acontecem na mesma ordem que acontece no ambiente de produção e para isso a biblioteca usa a função progressive_val_score, que recebe o dataset em formato de stream, o modelo e a métrica usada.

```python
from river import evaluate
from river import optim
from river import preprocessing
from river import compose

stream_dataset = stream.iter_pandas(features, target)

model = (
    preprocessing.StandardScaler() |
    linear_model.LogisticRegression(optimizer=optim.SGD(.1))
)

metric = metrics.ROCAUC(n_thresholds=20)

evaluate.progressive_val_score(stream_dataset, model, metric)
```

Para otimizar o modelo, antes de os dados alimentarem o modelo, eles são redimensionados com o StandardScaler(). O modelo recebe um otimizador do tipo SGD para descida de gradiente estocástica simples. Por fim avaliamos o modelo com progressive_val_score.

O resultado retornado pela função na última linha deve ser por volta de 82%. Uma melhora não tão considerável em comparação à nossa versão anterior, mas uma melhora é uma melhora.

Por fim, caso você queira identificar com mais facilidade os elementos do seu pipeline do modelo, basta digitar `model` em uma célula do seu notebook e ver a imagem que define o pipeline. No nosso caso, por exemplo, temos:


![desenho esquemático mostrando o Scaler acima do modelo de LogisticRegression](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ncr9o020alovbaj3vuz1.png)

## Conclusão

Com esse artigo você é capaz de experimentar várias das formas de trabalhar com a biblioteca River para treinar e avaliar modelos de aprendizado de máquina online. Você limpou um conjunto de dados e aprendeu desde como lidar com dados em lote para que funcionem como dados em streaming até como construir e avaliar um modelo de aprendizado online.

Se esse conteúdo foi do seu interesse, não esqueça de avaliá-lo de alguma forma e de me seguir para acompanhar os próximos artigos.

Dúvidas, sugestões ou correções são muito bem-vindas na seção de comentários abaixo. Até a próxima.