mat-proj/bayes_final.ipynb

1.8 MiB
Raw Permalink Blame History

Autorzy

Studia 2 stopnia - Informatyka (zaoczne) 1 rok

  • Krzysztof Bojakowski
  • Adam Stelmaszyk
  • Patryk Osiński
  • Marcin Jakubik

Zadanie

Klasyfikacja za pomocą naiwnej metody bayesowskiej (rozkłady ciągłe). Implementacja powinna założyć, że cechy są ciągłe (do wyboru rozkład normalny i jądrowe wygładzenie). Na wejściu oczekiwany jest zbiór, który zawiera p-cech ciągłych, wektor etykiet oraz wektor prawdopodobieństw a priori dla klas. Na wyjściu otrzymujemy prognozowane etykiety oraz prawdopodobieństwa a posteriori. Dodatkową wartością może być wizualizacja obszarów decyzyjnych w przypadku dwóch cech.

Zbiór danych

Do prezentacji wyników użyliśmy danych pochodzących z UCI ML Breast Cancer Wisconsin (Diagnostic) dataset. Zbiór zawiera 30 cech, na podstawie których klasyfikowany jest nowotwór - jako złośliwy lub niezsłośliwy.

image.png

import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

breast_cancer = load_breast_cancer()

# Sprawdzenie poprawności rozmiarów
assert len(breast_cancer.data[0]) == len(breast_cancer.feature_names)
assert len(breast_cancer.target) == len(breast_cancer.data)

values = np.c_[breast_cancer.data, breast_cancer.target]
columns = np.array(list(breast_cancer.feature_names) + ['target'])

df = pd.DataFrame(values, columns=columns)

X = df.iloc[:, 0:-1]
y = df.iloc[:,-1]

df.head()
mean radius mean texture mean perimeter mean area mean smoothness mean compactness mean concavity mean concave points mean symmetry mean fractal dimension ... worst texture worst perimeter worst area worst smoothness worst compactness worst concavity worst concave points worst symmetry worst fractal dimension target
0 17.99 10.38 122.80 1001.0 0.11840 0.27760 0.3001 0.14710 0.2419 0.07871 ... 17.33 184.60 2019.0 0.1622 0.6656 0.7119 0.2654 0.4601 0.11890 0.0
1 20.57 17.77 132.90 1326.0 0.08474 0.07864 0.0869 0.07017 0.1812 0.05667 ... 23.41 158.80 1956.0 0.1238 0.1866 0.2416 0.1860 0.2750 0.08902 0.0
2 19.69 21.25 130.00 1203.0 0.10960 0.15990 0.1974 0.12790 0.2069 0.05999 ... 25.53 152.50 1709.0 0.1444 0.4245 0.4504 0.2430 0.3613 0.08758 0.0
3 11.42 20.38 77.58 386.1 0.14250 0.28390 0.2414 0.10520 0.2597 0.09744 ... 26.50 98.87 567.7 0.2098 0.8663 0.6869 0.2575 0.6638 0.17300 0.0
4 20.29 14.34 135.10 1297.0 0.10030 0.13280 0.1980 0.10430 0.1809 0.05883 ... 16.67 152.20 1575.0 0.1374 0.2050 0.4000 0.1625 0.2364 0.07678 0.0

5 rows × 31 columns

Implementacja naiwnej metody Bayesowskiej

Implementacja jest w dwóch wariantach:

  • Rozkład normalny - Gaussian (Normal) Distribution
  • Rozkład log-normalny - Log-Gaussian (Log-Normal) Distribution

Matematyczny opis algorytmu

Twierdzenie Bayesa

${X}$ będzie pewnym zdarzeniem o ${n}$ cechach $$ X = (x_1, x_2, \ldots, x_n)$$ prawdopodobieństwo a posteriori ${P(C_k | X)}$ dla klasy ${C_k}$ jest dane przez: $$P(C_k | X) = \frac{P(X | C_k) P(C_k)}{P(X)}$$

Gdzie: $$ \begin{aligned} & \text{(1)}\hspace{1cm}P(C_k | X)\text{ to prawdopodobieństwo (probability) a posteriori klasy } C_k \text{ pod warunkiem zajścia zdarzenia } X \\ & \text{(2)}\hspace{1cm}P(X | C_k) \text{ to prawdopodobieństwo (likelihood) obserwacji zdarzenia } X \text{ pod warunkiem klasy } C_k \\ & \text{(3)}\hspace{1cm}P(C_k) \text{ to prawdopodobieństwo apriori klasy } C_k \\ & \text{(4)}\hspace{1cm}P(X) \text{ to całkowite prawdopodobieństwo obserwacji zdarzenia X we wszystkich klasach} \end{aligned} $$

Wzory na średnią i odchylenie standardowe

Średnia (Mean)

Średnia ${\mu}$ zestawu danych ${X}$ składającego się z ${n}$ obserwacji jest obliczana jako:

$$ \mu = \frac{1}{n} \sum_{i=1}^{n} x_i $$

Gdzie:

  • ${\mu}$ to średnia (mean)
  • ${n}$ to liczba obserwacji w zestawie danych
  • ${x_i}$ to pojedyncza obserwacja w zestawie danych

Odchylenie standardowe (Standard Deviation)

Odchylenie standardowe ${ \sigma }$ zestawu danych ${ X }$ składającego się z ${ n }$ obserwacji jest obliczane jako pierwiastek kwadratowy z wariancji:

$$ \sigma = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (x_i - \mu)^2} $$

Gdzie:

  • ${\sigma}$ to odchylenie standardowe (standard deviation)
  • ${\mu}$ to średnia (mean) zestawu danych
  • ${n}$ to liczba obserwacji w zestawie danych
  • ${x_i}$ to pojedyncza obserwacja w zestawie danych

Prawdpodobiestwo (Likelihood) obserwacji zdarzenia - rozkład normalny

$$ \text{Likelihood}(x | \mu, \sigma) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(x - \mu)^2}{2\sigma^2}\right) $$

$$ \text{Posterior} = \text{Likelihood} \times \text{Prior} $$

Prawdopodobieństwo (Log-Likelihood) obserwacji zdarzenia - rozkład log-normalny

$$ \text{Log-Likelihood}(x | \mu, \sigma) = -\frac{1}{2} \log(2\pi\sigma^2) - \frac{(x - \mu)^2}{2\sigma^2} $$

$$ \text{Log-Posterior} = \text{Log-Likelihood} + \text{Log-Prior} $$

import numpy as np
from scipy.stats import norm

class GaussianNaiveBayes:
    def __init__(self, priors=None, var_smoothing=1e-8, log=False):
        self.priors = priors
        self.classes = None
        self.means = None
        self.stds = None
        self.log = log
        self.var_smoothing = var_smoothing

    def fit(self, X, y):
        self.epsilon = self.var_smoothing * np.var(X, axis=0).max()
        
        self.classes = np.unique(y)
        self.means = np.zeros((len(self.classes), X.shape[1]))
        self.stds = np.zeros((len(self.classes), X.shape[1]))
        
        for idx, cls in enumerate(self.classes):
            X_c = X[y == cls]
            self.means[idx, :] = X_c.mean(axis=0)
            self.stds[idx, :] = X_c.std(axis=0)

        if self.priors is None:
            self.priors = np.bincount(y) / len(y)

        self.stds += self.epsilon

    def predict(self, X):
        posteriors = self.predict_proba(X)
        return np.argmax(posteriors, axis=1)

    def score(self, X, y_true):
        posteriors = self.predict_proba(X)
        y_pred = np.argmax(posteriors, axis=1)

        correct_predictions = np.sum(y_true == y_pred)
        accuracy = correct_predictions / len(y_true)
        return accuracy

    def score_visualize(self, X, y_true):
        """
        Funkcja rysuje dwa wykresy słupkowe przedstawiające rzeczywiste wartości i przewidywane wartości.
        """
        posteriors = self.predict_proba(X)
        y_pred = np.argmax(posteriors, axis=1)

        # Liczymy ilość wystąpień każdej klasy dla rzeczywistych i przewidywanych wartości
        true_counts = np.bincount(y_true)
        pred_counts = np.bincount(y_pred)

        # Tworzymy subplots - dwa wykresy obok siebie
        fig, axes = plt.subplots(1, 2, figsize=(12, 6))
        
        # Pierwszy wykres - rzeczywiste wartości
        axes[0].bar([0, 1], true_counts, color=['blue', 'orange'])
        axes[0].set_title('Rzeczywiste wartości')
        axes[0].set_xticks([0, 1])
        axes[0].set_xticklabels(['Nowotwór złośliwy', 'Nowotwór łagodny'])
        axes[0].set_ylabel('Liczba wystąpień')
        
        # Drugi wykres - przewidywane wartości
        axes[1].bar([0, 1], pred_counts, color=['blue', 'orange'])
        axes[1].set_title('Przewidywane wartości')
        axes[1].set_xticks([0, 1])
        axes[1].set_xticklabels(['Nowotwór złośliwy', 'Nowotwór łagodny'])
        axes[1].set_ylabel('Liczba wystąpień')
        
        # Wyświetlamy wykresy
        plt.show()

    def predict_proba(self, X):
        if self.log:
            return self._calculate_posterior_log(X)
        else:
            return self._calculate_posterior_normal_vectorized(X)

    def _normal_gaussian_pdf(self, x, mean, std):
        """
        Implementacja Normal Gaussian (likelihood) z użyciem SciKit - używana do testów
        """
        return np.prod(norm.pdf(x, mean, std))

    def normal_gaussian_pdf(self, x, mean, std):
        """
        implementacja Normal Gaussian (likelihood)

        kształt x jest zmieniany na (n_samples, 1, n_features) aby numpy mógł broadcastować
        operacje z mediąną, której kształt to (n_classes, n_features)
        """
        coefficient = 1 / (np.sqrt(2 * np.pi * std**2))
        exponent = np.exp(-(np.square(x[:, np.newaxis] - mean)) / (2 * np.square(std)))
        return coefficient * exponent
    
    def log_gaussian_pdf(self, x, mean, std):
        """
        implementacja Log-Normal Gaussian (likelihood)
        """
        return -0.5 * np.log(2 * np.pi * std**2) - ((x - mean)**2 / (2 * std**2))

    def _calculate_posterior_log(self, x):
        """
        Log-normal
        Użyliśmy log-sum-exp trick dla numerycznej stabilności.
        """
        log_likelihoods = np.zeros((x.shape[0], len(self.classes)))
        for idx, _ in enumerate(self.classes):
            prior = np.log(self.priors[idx])
            log_likelihood = np.sum(self.log_gaussian_pdf(x, self.means[idx, :], self.stds[idx, :]), axis=1)
            log_likelihoods[:, idx] = log_likelihood + prior
        
        # The Log-Sum-Exp Trick
        max_log_likelihoods = np.max(log_likelihoods, axis=1, keepdims=True)
        log_posteriors = log_likelihoods - max_log_likelihoods
        posteriors = np.exp(log_posteriors)
        posteriors = posteriors / np.sum(posteriors, axis=1, keepdims=True)
        return posteriors
    
    def _calculate_posterior_normal_vectorized(self, X):
        """
        Implementacja z użyciem wektorów - zwraca takie same wyniki jak _calculate_posterior_normal_iterative
        Działa szybciej i pomija konieczność iteracji w pętli.
        """
        likelihoods = self.normal_gaussian_pdf(X, self.means, self.stds)
        posteriors = likelihoods.prod(axis=2) * self.priors
        posteriors = posteriors / np.sum(posteriors, axis=1, keepdims=True)
        return posteriors

    def _calculate_posterior_normal_iterative(self, X):
        """
        Implementacja iteracyjna - zwraca takie same wyniki _calculate_posterior_normal_vectorized
        Używana do testów, aby sprawdzić czy wersja z użyciem wektorów działa poprawnie
        """
        posteriors = []
        for x in X:
            likelihoods = np.zeros((len(self.classes)))
            for idx, _ in enumerate(self.classes):
                likelihoods[idx] = self._normal_gaussian_pdf(x, self.means[idx, :], self.stds[idx, ]) * self.priors[idx]
            posteriors.append(likelihoods)
        posteriors = np.array(posteriors)
        posteriors = posteriors / np.sum(posteriors, axis=1, keepdims=True)
        return posteriors

def pp_predictions(preds, probs):
    i = 1
    for pred, prob in zip(preds, probs):
        print(f"\n=== {i} ===")
        
        if pred == 0:
            print(f"Predykcja: {pred} - (Nowotwór złośliwy)")
        else:
            print(f"Predykcja: {pred} - (Nowotwór łagodny)")
      
        print(f"Prawdpodobieństwo (Złośliwy, Łagodny): {np.round(prob, 4)}")
        i += 1

Prezentacja działania algorytmu

# Podział zbioru danych na treningowy i testowy
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.1, random_state=48)

gnb = GaussianNaiveBayes(log=False)
gnb.fit(X_train.values, y_train)

# Prawdopodobieństwa prior
priors = np.array(gnb.priors)
counts = [np.sum(y_train == 0.0), np.sum(y_train == 1.0)]

# Etykiety klas
labels = ['Nowotwór złośliwy', 'Nowotwór łagodny']

# Tworzenie diagramu kołowego
fig, axes = plt.subplots(1, 2, figsize=(11, 6), facecolor='white')

wedges, _, _ = axes[0].pie(priors, autopct='%1.1f%%', startangle=90, colors=['skyblue', 'orange'])
axes[0].axis('equal')  # Równe proporcje, aby koło było kołem
axes[0].set_title('Prawdopodobieństwa prior')

# Funkcja zwracająca liczbę dla autopct
def func(pct, allvals):
    absolute = int(np.round(pct/100.*np.sum(allvals)))
    return f"{absolute}"

axes[1].pie(counts, autopct=lambda pct: func(pct, counts), startangle=90, colors=['skyblue', 'orange'])
axes[1].axis('equal')  # Równe proporcje, aby koło było kołem
axes[1].set_title('Liczba wystąpień każdej klasy')

# Dodanie jednej legendy dla obu wykresów
fig.legend(wedges, labels, title="Klasy", loc="center right", bbox_to_anchor=(0.6, 0.75))

# Ustawienie białego tła dla wykresów
fig.patch.set_facecolor('white')

# Wyświetlenie wykresu
plt.show()
print(f"Prawdopodobieństwa prior: {gnb.priors}")
score = gnb.score(X_test.values, y_test)
print("Celność na zbiorze testowym:", score)

# Predykcje
preds = gnb.predict(X_test.values[15:20])
probs = gnb.predict_proba(X_test.values[15:20])
pp_predictions(preds, probs)
Prawdopodobieństwa prior: [0.38085938 0.61914062]
Celność na zbiorze testowym: 0.9824561403508771

=== 1 ===
Predykcja: 1 - (Nowotwór łagodny)
Prawdpodobieństwo (Złośliwy, Łagodny): [0. 1.]

=== 2 ===
Predykcja: 1 - (Nowotwór łagodny)
Prawdpodobieństwo (Złośliwy, Łagodny): [0.1798 0.8202]

=== 3 ===
Predykcja: 1 - (Nowotwór łagodny)
Prawdpodobieństwo (Złośliwy, Łagodny): [0.0067 0.9933]

=== 4 ===
Predykcja: 1 - (Nowotwór łagodny)
Prawdpodobieństwo (Złośliwy, Łagodny): [0.0472 0.9528]

=== 5 ===
Predykcja: 0 - (Nowotwór złośliwy)
Prawdpodobieństwo (Złośliwy, Łagodny): [1. 0.]
plt.figure(figsize=(14, 14))
score = gnb.score_visualize(X_test.values, y_test)
<Figure size 1008x1008 with 0 Axes>

Porównanie wyników z gotową implementacją z użyciem biblioteki sklearn

from sklearn.naive_bayes import GaussianNB

clf = GaussianNB()
clf.fit(X_train.values, y_train)
print("Celność na zbiorze testowym:", clf.score(X_test.values, y_test))
preds = clf.predict(X_test.values[15:20])
probs = clf.predict_proba(X_test.values[15:20])
pp_predictions(preds, probs)
Celność na zbiorze testowym: 0.9824561403508771

=== 1 ===
Predykcja: 1.0 - (Nowotwór łagodny)
Prawdpodobieństwo (Złośliwy, Łagodny): [0. 1.]

=== 2 ===
Predykcja: 1.0 - (Nowotwór łagodny)
Prawdpodobieństwo (Złośliwy, Łagodny): [0.1123 0.8877]

=== 3 ===
Predykcja: 1.0 - (Nowotwór łagodny)
Prawdpodobieństwo (Złośliwy, Łagodny): [0. 1.]

=== 4 ===
Predykcja: 1.0 - (Nowotwór łagodny)
Prawdpodobieństwo (Złośliwy, Łagodny): [0.047 0.953]

=== 5 ===
Predykcja: 0.0 - (Nowotwór złośliwy)
Prawdpodobieństwo (Złośliwy, Łagodny): [1. 0.]

Wizualizacja obszarów decyzyjnych

from matplotlib.colors import ListedColormap
from sklearn.decomposition import PCA

# Wizualizacja obszarów decyzyjnych
def plot_decision_boundaries(X, y, model, title, h=0.9):
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),np.arange(y_min, y_max, h))
    
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    plt.figure(figsize=(12, 9))
    colors_background = ['#FFFFFF', '#00AACC']
    colors_points = ['#0000FF', '#00FF00']
    plt.contourf(xx, yy, Z, alpha=0.8, cmap=ListedColormap(colors_background))
    plt.scatter(X[:, 0], X[:, 1], c=y, s=50, edgecolor='b', cmap=ListedColormap(colors_points))
    plt.title(title)
    plt.show()

# 30 cech zostało zredukowane do dwóch
# PCA dokonuje liniowej redukcji wymiarów za pomocą algorytmu Singular Value Decomposition (SVD).
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_train.values)

gnb = GaussianNaiveBayes(log=True)
gnb.fit(X_pca, y_train)
plot_decision_boundaries(X_pca, y_train, gnb, "Wizualizacja obszarów decyzyjnych")