Naive Bayes - Trabalho

Questão 1

Implemente um classifacor Naive Bayes para o problema de predizer a qualidade de um carro. Para este fim, utilizaremos um conjunto de dados referente a qualidade de carros, disponível no UCI. Este dataset de carros possui as seguintes features e classe:

Attributos

  1. buying: vhigh, high, med, low
  2. maint: vhigh, high, med, low
  3. doors: 2, 3, 4, 5, more
  4. persons: 2, 4, more
  5. lug_boot: small, med, big
  6. safety: low, med, high

Classes

  1. unacc, acc, good, vgood

Questão 2

Crie uma versão de sua implementação usando as funções disponíveis na biblioteca SciKitLearn para o Naive Bayes (veja aqui)

Questão 3

Analise a acurácia dos dois algoritmos e discuta a sua solução.

Questão 1


In [9]:
# Libraries
import numpy as np
import pandas as pd
from sklearn.naive_bayes import MultinomialNB, GaussianNB
from sklearn.cross_validation import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, classification_report

In [2]:
# Creating a class for the Naive Bayes
class NaiveBayes:
    def __init__(self):
        ''' Default Constructor '''
        self.lEncoder = LabelEncoder()
        self.X = None; self.y = None
        self.classProb = None; self.likeTable = {}
    
    def separateByClass(self):
        ''' This functions separates all the dataset indexing dictionaries by the classes '''
        separated = {}
        for i in range(len(self.y)):
            if (self.y[i] not in separated):
                separated[self.y[i]] = []
            separated[self.y[i]].append(self.X[i])
        return separated
    
    def makeLikeTable(self):
        ''' This functions counts the occurences of each attribute based on the classes
        and construct the Likelihood table (in this case a Dictionary) calculating all
        the propers probabilities '''
        sepClass = self.separateByClass()
        classSizes = [len(sepClass[i]) for i in sepClass.keys()] 
        self.classProb = np.array(classSizes) / sum(classSizes)
        
        self.likeTable = {}
        for label in sepClass.keys():
            aux = np.column_stack(sepClass[label])
            for attribute,idx in zip(aux, range(len(aux))):
                counts = np.asarray(np.unique(attribute, return_counts=True)).T
                for i in range(4):
                    self.likeTable[(label, idx, i)] = 0
                for count_it in counts:
                    self.likeTable[(label, idx, count_it[0])] = count_it[1] / len(sepClass[label])

    def calculateProbability(self, inputVector):
        ''' Utilizes the maximum likelihood estimation to calculate the probabilty of
        each row in inputVector belongs to each possible class '''
        sepClass = self.separateByClass()
        
        probabilities = {}
        for label,_ in sepClass.items():
            probabilities[label] = self.classProb[label]
            for i in range(len(inputVector)):
                probabilities[label] *= self.likeTable[label, i, inputVector[i]]
                
        return probabilities

    def fit(self, X_train, y_train):
        ''' Assign the training data and calls the Likelihood Table creator '''
        self.X = X_train
        self.y = y_train
        
        self.makeLikeTable()
    
    def predict(self, inputArray):
        ''' Return a list of predictions for each row in inputArray correspondent to
        the label of the class with the maximum probability '''
        predictions = []
        for row in inputArray:
            probabilities = self.calculateProbability(row)
            predictions.append(max(probabilities, key=probabilities.get))
        return predictions

Questão 2


In [10]:
# Carregando o Dataset
data = pd.read_csv("car.data", header=None)

# Transformando as variáveis categóricas em valores discretos contáveis
# Apesar de diminuir a interpretação dos atributos, essa medida facilita bastante a vida dos métodos de contagem
for i in range(0, data.shape[1]):
    data.iloc[:,i] = LabelEncoder().fit_transform(data.iloc[:,i])

# Separação do Conjunto de Treino e Conjunto de Teste (80%/20%)
X_train, X_test, y_train, y_test = train_test_split(data.iloc[:,:-1], data.iloc[:,-1], test_size=0.2)

In [11]:
# Implementação do Naive Bayes Multinomial do SKLearn
# O Naive Bayes Multinomial é a implementação do SKLearn que funciona para variáveis discretas,
# ao invés de utilizar o modelo da função gaussiana (a classe GaussianNB faz dessa forma)
clf = MultinomialNB()
clf.fit(X_train.values, y_train.values)

y_pred = clf.predict(X_test.values)

# Impressão dos Resultados
print("Multinomial Naive Bayes (SKlearn Version)")
print("Total Accuracy: {}%".format(accuracy_score(y_true=y_test, y_pred=y_pred)))

print("\nClassification Report:")
print(classification_report(y_true=y_test, y_pred=y_pred, target_names=["unacc", "acc", "good", "vgood"]))


Multinomial Naive Bayes (SKlearn Version)
Total Accuracy: 0.684971098265896%

Classification Report:
             precision    recall  f1-score   support

      unacc       1.00      0.01      0.02        83
        acc       0.00      0.00      0.00        12
       good       0.68      1.00      0.81       236
      vgood       0.00      0.00      0.00        15

avg / total       0.71      0.68      0.56       346

/home/petcomp/anaconda3/lib/python3.5/site-packages/sklearn/metrics/classification.py:1113: UndefinedMetricWarning: Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples.
  'precision', 'predicted', average, warn_for)

Questão 3


In [12]:
# Utilização da minha função própria de Naive Bayes para o mesmo conjunto de dados
nBayes = NaiveBayes()
nBayes.fit(X_train.values, y_train.values)
y_pred = nBayes.predict(X_test.values)

# Impressão dos Resultados
print("Naive Bayes (My Version :D)")
print("Total Accuracy: {}%".format(accuracy_score(y_true=y_test, y_pred=y_pred)))

print("\nClassification Report:")
print(classification_report(y_true=y_test, y_pred=y_pred, target_names=["unacc", "acc", "good", "vgood"]))


Naive Bayes (My Version :D)
Total Accuracy: 0.8497109826589595%

Classification Report:
             precision    recall  f1-score   support

      unacc       0.68      0.71      0.69        83
        acc       0.50      0.17      0.25        12
       good       0.91      0.95      0.93       236
      vgood       1.00      0.53      0.70        15

avg / total       0.84      0.85      0.84       346

Discussão

Algoritmo

Na minha implementação, assim como no SKLearn, existem duas funções principais: .fit() e .predict().

A função .fit() é responsável por receber os dados de Treino e, através deles, criar as tabelas de Likelihood para cada atributo. Essa tabela é criada através da função .makeLikeTable() que, basicamente, separa todos os exemplos por classe e, para cada classe, realiza a contagem de frequência de cada atributo e calcula as devidas probabilidades. A tabela Likelihood do meu classificador é indexado pela classe, índice do atributo e valor do atributo. Logo, ele possui as probabilidades:

$$ P(X_{i}=x\ |\ C_{j})$$

onde $X_i$ é o atributo de índice $i$, $x$ é o valor que ele assume e $C_j$ é a classe de índice $j$.

A função .predict() é responsável por receber os diversos inputs (exemplos para serem classificados) e retornar o label da classe com maior probabilidade para tais inputs. Ele utiliza da função .calculateProbability() para calcular a probabilidade:

$$ P(C\ |\ X) = P(C) \prod_i P(X_{i}\ |\ C)) $$

que é a probabilidade de maximum likelihood definida para a classe C, dado os atributos X. A resposta da predição é, então, a classe para qual o valor da probabilidade acima é máxima.

Resultados

Podemos notar que o algoritmo possui um resultado relativamente satisfatório. Apesar de tomar certas suposições bem rígidas, alguns casos realmente caem sobre os casos contemplados pelo Naive Bayes, e a classificação se mostra proveitosa.

Todavia, é perceptível que esse algoritmo depende bastante da frequência relativa de cada class. Se olharmos na equação acima, podemos perceber que a probabilidade de maximum likelihood é proporcional à probabilidade marginal das classes. Por esse motivo, classes que possuem uma frequência muito maior que as outras terão uma probabilidade maior, e irão influenciar mais no resultado da probabilidade final. Por esse motivo, podemos ver que as classes com menos exemplares ("acc" e "vgood") acabam não tendo uma performance muito boa. Esse fato é ainda mais perceptível no Multinomial NaiveBayes, implementado pelo SKLearn. Por utilizar diferentes métodos de contagem, ele possui um melhor custo computacional, mas acaba subestimando as probabilidades das classes menos frequentes e, por consequência, vemos que as predições caem praticamente todas nas duas classes mais frequentes ("unacc" e "good")