Post

Tudo o que você precisa saber sobre o algoritmo Naive Bayes

png

Introdução

Em Machine Learning, um problema de classificação consiste em prever alguma classe ou rótulo, com base em um conjunto de dados.

Naive Bayes (NB) é um dos mais antigos algoritmos de classificação utilizados para esse fim, tendo sido desenvolvido na década de 50 do século passado. Ele tem algumas características que o tornam bastante popular: é fácil de entender, fácil de implementar e apresenta ótimos resultados, mesmo para problemas relativamente complexos.

A base matemática de sua aplicação é, como você deve supor, o teorema de Bayes, que foi proposto no século 18 pelo reverendo inglês Thomas Bayes (1701–1761). Ele propôs esse teorema com o humilde objetivo de provar a existência de Deus (não se sabe se ele conseguiu).

No vasto universo do aprendizado de máquina, os algoritmos NB se destacam por sua simplicidade, eficiência e eficácia, especialmente em tarefas de classificação. Entre as variantes do Naive Bayes, o Modelo Gaussiano Naive Bayes (GNB) ocupa uma posição de destaque, graças à sua capacidade de trabalhar diretamente com dados contínuos, assumindo que os valores de cada característica são distribuídos segundo uma distribuição Gaussiana ou normal.

Este artigo explora em detalhes o funcionamento do NB, sua fundamentação matemática, características, vantagens e desvantagens.

Fundamentos Matemáticos do NB

A ideia básica de um algorítimo de classificação é que ele consiga, com base em conjunto de dados de treinamento \((𝑋,y)\) usado para ajustar o modelo, aprender e atribuir corretamente uma classe para novos valores de entrada. Em outras palavras, um algoritmo de classificação cria uma função matemática \(𝑦=𝑓(𝑥)\) que, ajustada pelos dados de treinamento, mapeia um certo conjunto de dados de entrada \(X = [x_1, x_2,...,x_m]\) para um outro conjunto de dados \(y = [y_1,y_2,...,y_K]\), composto por \(K\) classes distintas. Podemos tratar esse problema de classificação probabilisticamente, avaliando a probabilidade condicional da ocorrência de uma classe \(𝑦_k\), dado o conjunto de dados \(X\).

Matematicamente, isso pode ser escrito da seguinte forma: \[ P(y_k|X) = P(y_k|x_1, x_2,...,x_m) \] Lemos "A probabilidade de ocorrência da classe \(𝑦_k\), dado o conjunto de dados \(𝑋\)".

Tudo o que temos que fazer é calcular essa probabilidade para todas as classes em \(y\), e a classe com maior probabilidade é escolhida.

So easy, né? Bem...não tão depressa.

A primeira pergunta a ser feita é: como calcular essas probabilidades? Como obter o resultado dessa equação para todas as classes em \(y\)? É aqui que entra o reverendo Bayes e seu teorema quase divino.

Aplicando o teorema de Bayes à equação acima, obtemos: \[ P(y_k|X) = \frac{P(X|y_k)P(y_k)}{P(X)} \]

ou, de uma forma mais extensa: \[ P(y_k|X) = \frac{P(x_1, x_2,...,x_m|y_k)P(y_k)}{P(x_1, x_2,...,x_m)} \]

Nesse caso, a probabilidade de interesse, \(𝑃(𝑦_k|𝑋)\), é chamada probabilidade a Posteriori, e \(𝑃(𝑦_k)\) probabilidade a Priori. Já \(𝑃(X|𝑦_k)\) é a probabilidade de ocorrência dos dados de \(𝑋\), se a classe \(𝑦_k\) for verdadeira. Este termo é, por vezes, chamado Verossimilhança (Likelihood). E, por fim, \(𝑃(X)\) é a probabilidade dos dados de \(X\), independentemente da classe em questão, também chamado de Evidência.

Em termos gerais, \(𝑃(𝑦_k)\) pode ser calculada via a frequência relativa de cada classe no próprio conjunto de dados de treinamento. No entanto, o cálculo da probabilidade conjunta \(P(x_1, x_2,...,x_m|y_k)\) não é trivial, pois todas as variáveis possuem interdependência e, portanto, precisamos estimar as distribuições de todas as combinações possíveis. Isso requer uma quantidade de dados muito grande o que, por sua vez, aumenta o esforço computacional necessário para se efetuar o cálculo do teorema de Bayes diretamente.

Simplificação do Teorema de Bayes

Bem, como já deu pra notar, a vida é séria e a guerra é dura. Precisamos simplificar as coisas para tornar esse cálculo viável. Uma simplificação extrema é simplesmente considerar que todos os componentes de \(𝑋\) são independentes. Por exemplo, suponha que seu banco de dados (\(X\)) possua medidas diárias de temperatura, velocidade do vento e umidade relativa do ar, e seu interesse é prever se irá ou não chover em um determinado dia. Para aplicar o teorema de Bayes nesse caso, teríamos que supor que essas variáveis são absolutamente independentes uma das outras.

Nesse momento você salta da sua confortável cadeira e branda revoltado "mas considerar a independência total entre todas as variáveis de \(𝑋\) é uma suposição bastante forte, tola pra ser mais preciso!". Sim, de fato é verdade, essa consideração é bastante ingênua, já que na prática não se pode esperar algo tão perfeito assim, principalmente em problemas complexos; e é justamente dai que vem o nome do método. No entanto, é surpreendente como o danado funciona bem quando a problemas reais.

Com essa consideração, temos então o cálculo de uma probabilidade condicional com variáveis independentes, cujo numerador é composto pelas probabilidades condicionais de cada elemento de \(𝑋\) dado um elemento de \(y_k\), assim: \[ P(y_k|X) = \frac{P(x_1|y_k)P(x_2|y_k)...P(x_m|y_k)P(y_k)}{P(x_1)P(x_2)...P(x_m)} \] ou, de uma forma mais chique: \[ P(y_k|X) = \dfrac{P(y_k) \prod_{i=1}^{m}P(x_i|y_k)}{P(x_1)P(x_2)...P(x_m)} \] O denominador da expressão acima é constante para o cálculo das probabilidades condicionais de todas as \(K\) classes em \(y\). Logo, por uma questão de economia computacional, pode-se omitir essa parcela dos cálculos.

Nesse caso, dizemos que \(𝑃(𝑦_k|𝑋)\) é proporcional a \(𝑃(x_1|𝑦_k)𝑃(x_2|𝑦_k)…𝑃(x_m|𝑦_k)P(y_k)\). Matematicamente, escrevemos: \[ P(y_k|X) \propto P(x_1|y_k)P(x_2|y_k)...P(x_m|y_k)P(y_k) \] ou, como antes: \[ P(y_k|X) \propto P(y_k)\prod_{i=1}^{m}P(x_i|y_k) \] Pronto, com isso nós temos nosso classificador probabilístico Bayesiano hiper master plus+.

Máxima Probabilidade a Posteriori

Como já comentamos, essa probabilidade condicional (probabilidade a posteriori) é realizada para todas as \(K\) classes do nosso conjunto de dados e a classe com maior probabilidade é então selecionada. Tecnicamente, esse procedimento é chamado de máxima probabilidade a posteriori, ou MAP, na sigla em inglês.

Sendo assim, a classe predita pelo modelo será dada por: \[ \widehat{y} = \arg\max_{y_k} P(y_k|X) = \arg\max_{y_k} P(y_k)\prod_{i=1}^{m}P(x_i|y_k) \] Em muitas ocasiões, principalmente quando temos uma grande quantidade de dados, é conveniente expressarmos as probabilidades da equação anterior na forma de logaritmo. \[ \widehat{y} = \arg\max_{y_k} [ln(P(y_k)\prod_{i=1}^{m}P(x_i|y_k))] \] Como o logaritmo do produto é igual à soma dos logaritmos, ficamos com: \[ \widehat{y} = \arg\max_{y_k} [ln (P(y_k)) + \sum_{i=1}^{m}ln(P(x_i|y_k))] \] Reparem que trocamos o produto de probabilidades por somas de probabilidades, o que é computacionalmente mais eficiente.

Então, em resumo, tudo o que precisamos calcular para treinar o modelo são as probabilidade envolvidas na equação acima. Não há coeficientes que precisem ser ajustados via alguma algoritmo de otimização, como é comum em outros algoritmos de Machine Learning. Consequência? Temos um algoritmo de aprendizagem rápida e fácil implementação.

Tipos de algoritmos NB

Para aplicar o algoritmo NB em problemas reais, precisamos estimar uma distribuição de probabilidades para os recursos de \(X\). Essa estimativa pode ser guiada pelo tipo de dado que compõe o recurso. Se as variável são categóricas binárias, usa-se uma distribuição de bernoulli para representa-las. Caso haja multiclasses, usa-se uma distribuição multinomial. E, por fim, se estivermos lidando com dados contínuos, usa-se uma distribuição Gaussiana.

E disto surgem os três tipos de classificadores Naive Bayes:

  • Bernoulli naive Bayes (BNB).
  • Multinomial naive Bayes (MNB).
  • Gaussian naive Bayes (GNB).
É importante frisar que, caso \(X\) possua recursos com diferentes tipos de dados, pode-se atribuir diferentes distribuições de probabilidades para cada um deles.

Outro ponto importante é que, apesar de serem as distribuições mais comumente usadas em um classificador Naive Bayes, se os dados de entrada são melhores descritos por outra distribuição de probabilidade, deve-se usá-la. Ou ainda, se não temos certeza sobre a distribuição de probabilidade que melhor descreve esses dados, pode-se usar um estimador de distribuição de probabilidades, também chamado, KDE (Kernel density estimation).

Gaussian Naive Bayes

Como discutido anteriormente, a depender da natureza dos recursos de \(X\), pode-se assumir que estes sigam determinada distribuição de probabilidade. Quando se trata de dados contínuos, na maioria das vezes, assume-se que estes seguem uma distribuição de probabilidade Gaussiana. Nesse caso, as probabilidades da Verossimilhança (Likelihood) pode ser obtida pela equação abaixo: \[ P(x_i|y_k) = \dfrac{1}{\sqrt(2 \pi \sigma_k^2)} e^{-\dfrac{(x_i - \mu_k)^2}{2 \sigma_k^2}} \] E, desse modo, os únicos parâmetros que precisamos estimar, a partir dos dados de treinamento, são a média \(\mu_k\) e o desvio padrão \(\sigma_k\) de cada classe \(y_k\).

Simples, né?

Vantagens do GNB

  • Eficiente com grandes dimensões de dados.
  • Boa performance com um pequeno conjunto de dados.
  • Trata bem dados contínuos.

Desvantagens do GNB

  • Suposição de independência entre os atributos.
  • Sensibilidade a dados não representativos.

Aplicação prática GNB

Para ilustrar a aplicação do Modelo Gaussiano Naive Bayes, vamos implementá-lo “from scratch” e também utilizar a biblioteca scikit-learn para classificar o conjunto de dados Iris.

Implementação “from Scratch”

Primeiro, definiremos uma classe GaussianNBFromScratch que calculará as médias e variâncias para cada classe e característica, e usará essas estatísticas para fazer previsões baseadas na formulação matemática apresentada anteriormente.

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import numpy as np

class GaussianNBFromScratch:
    """
    Implementação simplificada do Gaussian Naive Bayes para classificação.

    Atributos:
        classes (np.array): Array único das classes no conjunto de dados.
        parameters (dict): Dicionário contendo parâmetros (média, variância e priori) 
                            para cada classe.
    """
    
    def fit(self, X, y):
        """
        Treina o modelo Gaussian Naive Bayes com os dados fornecidos.

        Parâmetros:
            X (np.array): Conjunto de dados de características, onde cada linha 
                          representa uma amostra e cada coluna uma característica.
            y (np.array): Vetor de rótulos de classe para cada amostra no conjunto 
                          de dados X.
        """
        
        self.classes = np.unique(y)  # Identifica e armazena as classes únicas no vetor de rótulos
        self.parameters = {}  # Inicializa o dicionário para armazenar os parâmetros para cada classe
        
        # Calcula e armazena os parâmetros para cada classe
        for c in self.classes:
            X_c = X[y == c]  # Seleciona as amostras pertencentes à classe c
            self.parameters[c] = {
                'mean': X_c.mean(axis=0),  # Calcula a média para cada característica
                'var': X_c.var(axis=0),  # Calcula a variância para cada característica
                'prior': X_c.shape[0] / X.shape[0]  # Calcula a probabilidade a priori da classe
            }
    
    def predict(self, X):
        """
        Realiza a classificação das amostras fornecidas.

        Parâmetros:
            X (np.array): Conjunto de dados de características a serem classificadas.

        Retorna:
            np.array: Um vetor de rótulos de classe previstos para cada amostra.
        """
        y_pred = [self._predict(x) for x in X]  # Usa a função auxiliar _predict para cada amostra
        return np.array(y_pred)  # Retorna as previsões como um array numpy
    
    def _predict(self, x):
        """
        Auxiliar que calcula a classe mais provável para uma única amostra.

        Parâmetros:
            x (np.array): Uma amostra única a ser classificada.

        Retorna:
            int: A classe prevista para a amostra.
        """
        posteriors = []  # Lista para armazenar as probabilidades posteriores para cada classe
        
        # Calcula a probabilidade posterior para cada classe
        for c, params in self.parameters.items():
            prior = np.log(params['prior'])  # Log da probabilidade a priori da classe
            conditional = np.sum(np.log(self._pdf(x, params['mean'], params['var'])))  # Log da probabilidade condicional
            posterior = prior + conditional  # Log da probabilidade posterior
            posteriors.append(posterior)
        
        return self.classes[np.argmax(posteriors)]  # Retorna a classe com a maior probabilidade posterior
    
    def _pdf(self, x, mean, var):
        """
        Calcula a função densidade de probabilidade (PDF) gaussiana.

        Parâmetros:
            x (float): O valor da característica a ser avaliada.
            mean (float): A média da característica para a classe.
            var (float): A variância da característica para a classe.

        Retorna:
            float: O valor da PDF gaussiana para x.
        """
        return (1. / np.sqrt(2 * np.pi * var)) * np.exp(-(x - mean) ** 2 / (2 * var))

Banco de dados Iris

A base de dados Iris é um dos conjuntos de dados mais icônicos e amplamente utilizados no campo do aprendizado de máquina e estatística. Introduzida pelo estatístico britânico Ronald Fisher em 1936, ela contém 150 amostras de três espécies diferentes de flores de íris (Iris setosa, Iris virginica e Iris versicolor), cada uma com 50 amostras. Para cada amostra, são medidas e registradas quatro características: o comprimento e a largura das sépalas e das pétalas.

1
2
3
4
5
6
7
8
9
10
11
12
from sklearn.datasets import load_iris
import pandas as pd

# Carregar os dados
data = load_iris()

# Criar um dataframe pandas
df = pd.DataFrame(data=data.data, columns=data.feature_names)
df['target'] = data.target

# Exibir as primeiras linhas do DataFrame para verificação
df.head()
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)target
05.13.51.40.20
14.93.01.40.20
24.73.21.30.20
34.63.11.50.20
45.03.61.40.20
1
2
3
4
5
6
7
8
9
10
import seaborn as sns
import matplotlib.pyplot as plt

# Configurando o tema do Seaborn
sns.set_theme(style="whitegrid")

# Criando um pairplot com o dataset Iris
sns.pairplot(df, hue="target", diag_kind="kde", markers=["o", "s", "D"], palette="bright")

plt.show()

png

Dividir os dados em treino e teste

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

X = df.drop('target', axis=1)  # Isso remove a coluna 'target', deixando apenas as características
y = df['target']  # Isso seleciona apenas a coluna 'target' como o alvo

# Divide dados em treinamento e teste, como um proporção de 70 e 30%, respectivamente
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

Modelo “from scratch”

1
2
3
4
# Criar instância do modelo 
model_from_scratch = GaussianNBFromScratch()
# Treinar o modelo
model_from_scratch.fit(X_train, y_train)

Modelo Scikit-learn

1
2
3
4
5
from sklearn.naive_bayes import GaussianNB

# Inicializar e treinar o modelo
model_sklearn = GaussianNB()
model_sklearn.fit(X_train, y_train)
GaussianNB()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Avaliar os modelos

1
2
3
4
5
6
7
8
9
10
11
from sklearn.metrics import accuracy_score

# Fazer previsões e avaliar os modelos
y_pred_from_scratch = model_sklearn.predict(X_test)
accuracy_from_scratch = accuracy_score(y_test, y_pred_from_scratch)

y_pred_sklearn = model_sklearn.predict(X_test)
accuracy_sklearn = accuracy_score(y_test, y_pred_sklearn)

print(f"Acurácia do nosso modelo from scratch: {accuracy_from_scratch:.2f}")
print(f"Acurácia do modelo sklearn: {accuracy_sklearn:.2f}")
1
2
Acurácia do nosso modelo from scratch: 0.98
Acurácia do modelo sklearn: 0.98

Nota: Todo o código está disponível no Github

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.