mpsic_projekt_1_bayes_class.../projekt.ipynb

333 KiB
Raw Permalink Blame History

Naiwna klasyfikacja bayesowska

Naiwna klasyfikacja bayesowska jest to klasyfikacja polegająca na przydzielaniu obiektom prawdopodobieństwa przynależności do danej klasy. Naiwny klasyfikator jest to bardzo prosta metoda klasyfikacji jednak mimo swej prostoty sprawdza się w wielu przypadkach gdy bardziej złożone metody zawodzą.Jednym z powodów dla których naiwny klasyfikator wypadak dobrze jest jego założenie o niezależności predykatów, własnie dlatego nazywany jest klasyfikatorem naiwnym, naiwnie zakłada niezależność atrybutów opisujących dany przykład, co często nie ma odzwierciedlenia w rzeczywistości. Klasyfikator nazywany jets bayesowskim dlatego że wykorzystuje twierdzenie bayesa do obliczania prawdopodobieństw.

Twierdzenie Bayesa

Twierdzneie bayesa jets to twierdzenie tworii prawdopodobieństwa wiążące prawdopodobieństwa warunkowe dwóch zdarzeń warunkujących się nawzajem. Prawdopodobieństwo wystąpienia zdarzenia A gdy wystąpiło zdarzenie B oblicza się wzorem:

$P(A|B) = \frac{P(B|A) P(A)}{P(B)}$

Wykorzystanie twierdzenia Bayesa w naiwnej klasyfikacji bayesowskiej

1) Nawiązując do twierdzenia bayesa prawdopodobieństwo problem klasyfikacji danego obiektu możemy zapisać w następujący sposb:


$P(K|X)$ Zapis ten oznacza że mając obiekt X chcemy obliczyć prawdopodobieństo przynaleźności do klasy K

$P(K|X) = \frac{P(X|K) P(K)}{P(X)}$

$P(K)$ - jest to prawdopodobieństwo a-priori klasy K

$P(X|K)$ - ten zapis możemy interpretować w taki sposób: jeżeli klasa K to szansa że X do niej należy

$P(X)$ - ale jak interpretować prawdopodobieństwo od odibkeu X? okazuje się że nie trzeba tego obliczać gdyż obliczając prawpodobieństwa przynależności X do klasy K w mianowniku zawsze bęzie $P(X)$ więc możemy to pomijać

Najlepiej będzie pokazać to na przykładzie:



2) Prawdopobobieństwa a-priori


Prawdopodobieństwo a priori jest to prawdopodobieństwo obliczane przed realizacją odpowiednich eksperymentów lub obserwacji.

klasy

Powyższa ilustracja przedstawia 2 klasy czerwona(20 obiektów) i zielona(40 obiektów), prawdopodobieństwa a priori tym przykładzie to nic innego jak

$\frac{liczebność\ klasy}{liczebność\ wszystkich\ elementów}$, więc prawdopodobieńswta a priori dla powyższego przykładu są równe

a priori klasy zielonej = $\frac{2}{3}\\$ a priori klasy czerwonej = $\frac{1}{3}\\$

To jest nasze $P(K)$



3) Klasyfikacja nowego obiektu


nowy obiekt

Klasyfikując nowy obiekt korzystamy z prawdopodobieństw a-priori, jednak same prawdopodobieństwa a-priori są niweystarczające, w tym przykładzie rozsądnym założeniem jest klasyfikowanie obiektu również na podstawie jego najbliższych sąsiadów. Zaznaczając okręgiem obszar możemy obliczyć prawdopodobieństwo że nowy obiekt będzie czerwony albo zielony, prawdopodobieństwa te obliczane są ze wzoru, prawdopodobieństwa te zostaną użyte do obliczenia prawdopodobieństwa a posteriori.

$\frac{liczba\ obiektów\ danej\ klasy\ w\ sąsiedztwie\ nowego\ obiektu}{liczba\ obiektów\ danej\ klasy}$ i wynoszą odpowiednio:

prawdopodobieństow że obiekt będzie w klasie zielonej = $\frac{1}{40}\\$ prawdopodobieństow że obiekt będzie w klasie czerwonej = $\frac{3}{20}\\$

To jets nasze $P(X|K)$



4) Prawdopodobieństwo a posteriori


Mając już obliczone wszystkie potrzebne prawdopodobieństwa możemy obliczyć prawdopodobieństwa a posteriori

Prawdopodobieństwo a posteriori jest to prawdopodobieństwo pewnego zdarzenia gdy wiedza o tym zdarzeniu wzbogacona jest przez pewne obserwacje lub eksperymenty.

W naszym przykładzie prawdopodobieństwo a posteriori obliczymy ze wzoru

$(prawdopodobieństwo\ a\ priori\ przynależności\ do\ danej\ klasy) * (prawdopodobieństwo\ że\ nowy\ obiekt\ będzie\ w\ danej\ klasie\ na\ podstawie\ jego\ sąsiadów)$ czyli $P(X|K) P(K)$

A więc prawdopodobieństwa a posteriori są równe:

Prawdopodobieństwo a posteriori ze nowy obiekt będzie w klasie zielonej = $\frac{2}{3} * \frac{1}{40} = \frac{1}{60}\\$ Prawdopodobieństwo a posteriori ze nowy obiekt będzie w klasie czerwonej = $\frac{1}{3} * \frac{3}{40} = \frac{1}{40}\\$

Naiwna klasyfikacja bayesowska przy wielu cechach


Klasyfikator bayesowski możemy stosować na wielu cechach, wykonuje się to w następujący sposób:

W naszym przypadku obiekt X posiada wiele cech $X = (x1, x2, x3, ... xn)$ wtedy stosując znany już wzór:

$P(K|X) = \frac{P(X|K) P(K)}{P(X)}$

$P(K)$ pozostaje niezmienne jest to $\frac{liczba\ elementów\ klasy\ K}{wszystkie\ elementy\ zbioru}$

Element X posiada wiele cech więc:

$P(X|K) = P(x1|K)_P(x2|K)...*P(xn|K)$

Zauażmy że w rzeczywistości $P(X|K)$ obliczymy z twierdzenia bayesa, jednak dzięki temu że nasz klasyfikator jest naiwny zakładamy niezależność cech $x1, x2,...,xn$ więc możemy uprościć obliczenia do powyższego wzoru


$P(xk|K) = \frac{Liczba\ elementów\ klasy\ K\ dla\ których\ wartość\ cechy\ Ak\ jest\ równa\ xk}{liczba\ wszystkich\ obiektów\ klasy\ K}$



Powyższy wzór ma jedna pewne problemy z zerowymi prawdopodobieństwami


Co w przypadku gdy dla którejś cechy $P(xk|K) = 0$

Może się tak zdarzyć gdy cecha $Ak$ nie będzie przyjmowała wartości $xk$ danego obiektu $X$ wtedy zgodnie ze wzorem otrzymamy $\frac{0}{liczba\ wszystkich\ obiektów\ klasy\ K}$ Gdy tak się stanie obliczone prawdopodobieństwo będzie równe 0, a przecież brak wartości xk dla cechy Ak klasy K nie musi wcale oznaczać śe dany obiekt nie może należeć do klasy K. Aby temu zaradzić stosuje się wygładzanie


Wygładzanie Laplace'a


Wygładzanie Laplace'a zwane jest również wygładzaniem + 1, jest to bardzo prosty sposób wystarczy dla każdego $P(xk|K)$ dodać do licznika 1 a do mianownika dodać liczbę klas obiektu, dodając 1 do licznika możemy to interpretować jako dodanie nowego obiektu do klasy, robiąc to dla każdej klasy "mamy" dodatkowe obiekty równe liczbie klas dlatego do mianownika musimy dodać liczbę klas.

$P(xk|K) = \frac{(Liczba\ elementów\ klasy\ K\ dla\ których\ wartość\ cechy\ Ak\ jest\ równa\ xk\\)\ + 1}{(liczba\ wszystkich\ obiektów\ klasy\ K\\) + liczba klas}$

Łatwo można zauważyć że samo dodanie 1 do licznika nie jest wystarczające ponieważ wtedy $P(xk|K)$ mogłoby być $> 1$


Dzięki wygładzaniu Laplace'a $P(X|K)$ nigdy nie bęzie zerowe minimalna wartość to

$\frac{1}{n *(liczba\ wszystkich\ obiektów\ klasy\ K\\) + liczba\ klas}$


Implementacja
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
import scipy.stats as stats
import numpy as np
import plotly
from plotly.subplots import make_subplots
import matplotlib.pyplot as plt
import seaborn as sns
import random
class NaiveBayes():

    def __init__(self, classes, className, attribsNames, data):
        self.classes = classes
        self.className = className
        self.attribsNames = attribsNames
        self.data = data

    #przygotowanie prawdopodobienstw wartosci danych cech w zaleznosci od klasy
    def getDictOfAttribProbs(self):
        dictionaries = {}
        for value in self.classes:
            classFreq = {}
            for i in range(len(self.attribsNames)):
                classData = self.data[self.data[self.className] == value]
                freq = {}
                attribData = classData[self.attribsNames[i]]
                for attrib in attribData:
                    count = freq.get(attrib, 1) + 1
                    freq[attrib] = count
                freq = {k: v / len(classData) for k, v in freq.items()}
                classFreq[self.attribsNames[i]] = freq
            dictionaries[value] = classFreq
        return dictionaries

    #a priori dla klas
    def classProb(self, class_):
        x = len(self.data[self.data[self.className] == class_][self.className])
        y = len(self.data[self.className])
        return x / y

    #prawdopodobienstwo dla wartosic danej cechy w zaelznosci od klasy
    def getAttribProbs(self, attrib, value, data, clas, dictProbs):
        return dictProbs[clas][attrib].get(value, 1.0 / len(data))

    #a posteriori dla danego obiektu
    def getPosteriori(self, attribs, attribsNames, clas, dictProbs):
        dic = {}
        for i in range(len(attribs)):
            dic[attribsNames[i]] = attribs[i]
        sum = 0.0
        for key in dic:
            sum = sum + np.log(self.getAttribProbs(key, dic[key], X_train, clas, dictProbs))
        return sum + np.log(self.classProb(clas))

    #predykcja dla danych
    def predict(self, data, model):
        attribNames = data.columns
        predictions = []
        for i in range(len(data)):
            probs = {}
            for name in self.classes:
                probs[name] = self.getPosteriori(list(data.iloc[i]), list(attribNames), name, model)
            keyMax = max(zip(probs.values(), probs.keys()))[1]
            predictions.append(keyMax)
        return predictions

    def fitModel(self):
        probabilities = self.getDictOfAttribProbs()
        return probabilities
features = [
    'edible', 'cap-shape', 'cap-surface', 'cap-color', 'bruises', 'odor',
    'gill-attachment', 'gill-spacing', 'gill-size', 'gill-color',
    'stalk-shape', 'stalk-root', 'stalk-surface-above-ring',
    'stalk-surface-below-ring', 'stalk-color-above-ring',
    'stalk-color-below-ring', 'veil-type', 'veil-color', 'ring-number',
    'ring-type', 'spore-print-color', 'population', 'habitat'
]

mushrooms = pd.read_csv('mushrooms.tsv', sep='\t', names=features)
NAMES_DICT = {
    'edible': {
        'p': 'poisonous',
        'e': 'edible'
    },
    "cap-shape": {
        'b': 'bell',
        'c': 'conical',
        'x': 'convex',
        'f': 'flat',
        'k': 'knobbed',
        's': 'sunken'
    },
    "cap-surface": {
        'f': 'fibrous',
        'g': 'grooves',
        'y': 'scaly',
        's': 'smooth'
    },
    "cap-color": {
        'n': 'brown',
        'b': 'buff',
        'c': 'cinnamon',
        'g': 'gray',
        'r': 'green',
        'p': 'pink',
        'u': 'purple',
        'e': 'red',
        'w': 'white',
        'y': 'yellow'
    },
    "bruises": {
        't': 'bruises',
        'f': 'none'
    },
    "odor": {
        'a': 'almond',
        'l': 'anise',
        'c': 'creosote',
        'y': 'fishy',
        'f': 'foul',
        'm': 'musty',
        'n': 'none',
        'p': 'pungent',
        's': 'spicy'
    },
    "gill-attachment": {
        'a': 'attached',
        'd': 'descending',
        'f': 'free',
        'n': 'notched'
    },
    "gill-spacing": {
        'c': 'close',
        'w': 'crowded',
        'd': 'distant'
    },
    "gill-size": {
        'b': 'broad',
        'n': 'narrow'
    },
    "gill-color": {
        'k': 'black',
        'n': 'brown',
        'b': 'buff',
        'h': 'chocolate',
        'g': 'gray',
        'r': 'green',
        'o': 'orange',
        'p': 'pink',
        'u': 'purple',
        'e': 'red',
        'w': 'white',
        'y': 'yellow'
    },
    "stalk-shape": {
        'e': 'enlarging',
        't': 'tapering'
    },
    "stalk-root": {
        'b': 'bulbous',
        'c': 'club',
        'u': 'cup',
        'e': 'equal',
        'z': 'rhizomorphs',
        'r': 'rooted',
        '?': 'missing'
    },
    "stalk-surface-above-ring": {
        'f': 'fibrous',
        'y': 'scaly',
        'k': 'silky',
        's': 'smooth'
    },
    "stalk-surface-below-ring": {
        'f': 'fibrous',
        'y': 'scaly',
        'k': 'silky',
        's': 'smooth'
    },
    "stalk-color-above-ring": {
        'n': 'brown',
        'b': 'buff',
        'c': 'cinnamon',
        'g': 'gray',
        'o': 'orange',
        'p': 'pink',
        'e': 'red',
        'w': 'white',
        'y': 'yellow'
    },
    "stalk-color-below-ring": {
        'n': 'brown',
        'b': 'buff',
        'c': 'cinnamon',
        'g': 'gray',
        'o': 'orange',
        'p': 'pink',
        'e': 'red',
        'w': 'white',
        'y': 'yellow'
    },
    "veil-type": {
        'p': 'partial',
        'u': 'universal'
    },
    "veil-color": {
        'n': 'brown',
        'o': 'orange',
        'w': 'white',
        'y': 'yellow'
    },
    "ring-number": {
        'n': 'none',
        'o': 'one',
        't': 'two'
    },
    "ring-type": {
        'c': 'cobwebby',
        'e': 'evanescent',
        'f': 'flaring',
        'l': 'large',
        'n': 'none',
        'p': 'pendant',
        's': 'sheathing',
        'z': 'zone'
    },
    "spore-print-color": {
        'k': 'black',
        'n': 'brown',
        'b': 'buff',
        'h': 'chocolate',
        'r': 'green',
        'o': 'orange',
        'u': 'purple',
        'w': 'white',
        'y': 'yellow'
    },
    "population": {
        'a': 'abundant',
        'c': 'clustered',
        'n': 'numerous',
        's': 'scattered',
        'v': 'several',
        'y': 'solitary'
    },
    "habitat": {
        'g': 'grasses',
        'l': 'leaves',
        'm': 'meadows',
        'p': 'paths',
        'u': 'urban',
        'w': 'waste',
        'd': 'woods'
    },
}

for key in NAMES_DICT.keys():
    mushrooms[key] = mushrooms[key].apply(lambda x: NAMES_DICT[key][x])
mushrooms.head()
edible cap-shape cap-surface cap-color bruises odor gill-attachment gill-spacing gill-size gill-color ... stalk-surface-below-ring stalk-color-above-ring stalk-color-below-ring veil-type veil-color ring-number ring-type spore-print-color population habitat
0 edible bell smooth white bruises anise free close broad brown ... smooth white white partial white one pendant brown numerous meadows
1 poisonous convex scaly brown bruises pungent free close narrow brown ... smooth white white partial white one pendant brown several grasses
2 edible bell scaly white bruises almond free close broad white ... smooth white white partial white one pendant brown numerous meadows
3 edible bell smooth white bruises anise free close broad gray ... smooth white white partial white one pendant black scattered meadows
4 edible convex scaly yellow bruises anise free close broad brown ... smooth white white partial white one pendant brown numerous meadows

5 rows × 23 columns

Features' distribution

Wśród cech zawartych w danych możemy zauważyć, że rodzaj woalki/pierścienia (veil-type) jest tylko jeden, zatem ta cecha nie powinna mieć wpływu na skuteczność algorytmu, jako że wszystkie obserwacje dzielą tę samą wartość (partial)

def domain_plots(data):
    specs = [[{'type': 'domain'} for _ in range(5)] for _ in range(5)]
    fig = make_subplots(rows=5, cols=5, specs=specs)
    a, b, xx, yy, l = 1, 1, -0.172, 1.267, []
    for col in data.columns:
        fig.add_trace(
            plotly.graph_objects.Pie(
                labels=[count for count in data[col].value_counts().index],
                values=[
                    val for val in data[col].value_counts() * 100 /
                                   sum(data[col].value_counts())
                ],
                name=col), a, b)
        l.append(
            dict(text=col,
                 x=xx + (0.225 * b),
                 y=yy - (0.222 * a),
                 font_size=10,
                 showarrow=False))
        a += 1
        if a > 5:
            a = 1
            b += 1
    fig.update(layout_title_text='Features', layout_showlegend=False)

    fig.update_layout(title_font_family='Arial',
                      title_font_size=25,
                      annotations=l)
    fig.update_traces(hole=.4, hoverinfo="label+percent+name", textinfo='none')
    fig.show()


domain_plots(mushrooms)
Korelacja zmiennych

Na poniższym wykresie możemy zauważyć silny związek gill-color z gill-attachment, lecz, to co nas bardziej interesuje, korelacja jadalności grzyba z zapachem, rozmiarem jego blaszek oraz kolorem zarodników. Możemy wyszukać cechy o najwyższym współczynniku korelacji z jadalnością grzyba i z dużym prawdopodobieństwiem uzyskać wyniki klasyfikacji

def cramers_v(x, y):
    confusion_matrix = pd.crosstab(x, y)
    chi2 = stats.chi2_contingency(confusion_matrix)[0]
    n = confusion_matrix.sum().sum()
    phi2 = chi2 / n
    r, k = confusion_matrix.shape
    phi2corr = max(0, phi2 - ((k - 1) * (r - 1)) / (n - 1))
    rcorr = r - ((r - 1) ** 2) / (n - 1)
    kcorr = k - ((k - 1) ** 2) / (n - 1)
    return np.sqrt(phi2corr / min((kcorr - 1), (rcorr - 1)))


reduced_data = mushrooms.drop(['veil-type'], axis=1)
corr_list = [[
    round(cramers_v(reduced_data[col], reduced_data[corr_col]) * 100) / 100
    for corr_col in reduced_data.columns
] for col in reduced_data.columns]
corr_df = pd.DataFrame(np.array(corr_list), columns=[reduced_data.columns])
corr_df.index = corr_df.columns

plt.figure(figsize=(14, 12))
corr_mask = np.triu(np.ones_like(corr_df, dtype=bool))
sns.heatmap(corr_df, mask=corr_mask, cmap='viridis', annot=True)
plt.show()
def plot_chosen_features(data,
                         col,
                         hue=None,
                         color=['orange', 'lightgreen'],
                         labels=None):
    fig, ax = plt.subplots(figsize=(15, 7))
    sns.countplot(x=col,
                  hue=hue,
                  palette=color,
                  saturation=0.6,
                  data=data,
                  dodge=True,
                  ax=ax)
    ax.set(title=f"Mushroom {col.title()} Quantity",
           xlabel=f"{col.title()}",
           ylabel="Quantity")
    if labels != None:
        ax.set_xticklabels(labels)
    if hue != None:
        ax.legend(('Poisonous', 'Edible'), loc=0)
        
    plt.show()


training_cols = [
    'odor',
    'spore-print-color',
    'gill-color',
    'ring-type',
    'stalk-surface-above-ring',
    'gill-size',
]

plot_chosen_features(mushrooms,
                     col='odor',
                     labels=NAMES_DICT['odor'].values(),
                     hue='edible')
plot_chosen_features(mushrooms,
                     col='spore-print-color',
                     labels=NAMES_DICT['spore-print-color'].values(),
                     hue='edible')
plot_chosen_features(mushrooms,
                     col='gill-color',
                     labels=NAMES_DICT['gill-color'].values(),
                     hue='edible')
plot_chosen_features(mushrooms,
                     col='ring-type',
                     labels=list(NAMES_DICT['ring-type'].values()).append('cobwebby'),
                     hue='edible')
plot_chosen_features(mushrooms,
                     col='stalk-surface-above-ring',
                     labels=NAMES_DICT['stalk-surface-above-ring'].values(),
                     hue='edible')
plot_chosen_features(mushrooms,
                     col='gill-size',
                     labels=NAMES_DICT['gill-size'].values(),
                     hue='edible')
X_train, X_test = train_test_split(mushrooms,
                                   test_size=0.2,
                                   random_state=0,
                                   stratify=mushrooms['edible'])
columns = [
    'odor',
    'spore-print-color',
    'gill-color',
    'ring-type',
    'stalk-surface-above-ring',
    'gill-size',
]
className = 'edible'
classValue = list(set(X_train['edible']))
X_test_data = X_test[columns]
X_test_results = X_test[className]
bayModel = NaiveBayes(classValue, className, columns, X_train)
model = bayModel.fitModel()
pred = bayModel.predict(X_test[columns], model)
print('accuracy score naiwnego klasyfikatora')
print("acuracy score = ", accuracy_score(list(X_test_results), pred))

print('\naccuracy score losowych predykcji')
randomPred = ['poisonous' if random.randint(0, 1) == 1 else 'edible' for _ in range(len(list(X_test_results)))]
print("acuracy score = ", accuracy_score(list(X_test_results), randomPred))
accuracy score naiwnego klasyfikatora
acuracy score =  0.9944903581267218

accuracy score losowych predykcji
acuracy score =  0.48829201101928377
columns_wihtout_odor = [
    'spore-print-color',
    'gill-color',
    'ring-type',
    'stalk-surface-above-ring',
    'gill-size',
]

X_test_data = X_test[columns_wihtout_odor]
X_test_results = X_test[className]

bayModel = NaiveBayes(classValue, className, columns_wihtout_odor, X_train)
model = bayModel.fitModel()
pred = bayModel.predict(X_test[columns_wihtout_odor], model)
print('accuracy score naiwnego klasyfikatora')
print("acuracy score = ", accuracy_score(list(X_test_results), pred))
accuracy score naiwnego klasyfikatora
acuracy score =  0.8980716253443526