朴素贝叶斯:教你用Python一步步识别手写数字

朴素贝叶斯

朴素贝叶斯方法是一种基于贝叶斯定理和特征条件独立性假设的分类方法。由于在现实中很难满足“特征条件的独立性”,因此被称为“朴素”贝叶斯。

接下来,我们将使用朴素贝叶斯来实现手写数字的识别。

问题描述

我使用的数据集是sklearn中的数据集之一,先导入

from sklearn.datasets import load_digits
digits = load_digits()

简单介绍一下这个数据集,它包含17978%5Ctimes%208大小的图片,每张图片有64特征,我们可以标记为:
x%20%3D%20%28x_1%2C%20%5Ccdots%2C%20x_%7B64%7D%29%5ET
每张图片都有一个对应的输出y,一个从朴素贝叶斯:教你用Python一步步识别手写数字09的整数,也就是图片对应的数字。
为了更直观的感受,你可以参与其中,形象化

fig = plt.figure(figsize=(6, 6))
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)

for i in range(64):
    ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[])
    ax.imshow(digits.images[i], cmap=plt.cm.binary, interpolation='nearest')
    ax.text(0, 7, str(digits.target[i]))

plt.show()

效果如下:

朴素贝叶斯:教你用Python一步步识别手写数字
图片左下角是实数,也就是每个数据x对应的y值。

那么我们要做的就是根据输入x来判断对应的数字y_%7Bpredict%7D,也就是每个像素的值。

理论部分

对于给定的输入x%3D%28x_1%2C%20%5Ccdots%2C%20x_n%29,假设他的输出总共有M种可能性,我们记为y%3D%5C%7By_1%2C%20%5Ccdots%2C%20y_M%5C%7D,然后我们要做的是,在输入x的情况下,计算输出是y中每个值的概率, 我们记他为p_m,
p_m%20%3D%20%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%20%7C%20X%20%3D%20x%29%20%3D%20%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%20%7C%20X_1%20%3D%20x_1%2C%20%5Ccdots%2C%20X_n%20%3D%20x_n%29
最后,我们返回的是概率最大的那个可能性,也就是
y_%7Bpredict%7D%20%3D%20%7B%5Crm%20arg%20max%7D_%7By_m%7D%20%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%20%7C%20X_1%20%3D%20x_1%2C%20%5Ccdots%2C%20X_n%20%3D%20x_n%29

接下来要处理的是找到最大值的问题。根据贝叶斯定理,我们有

%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%20%7C%20X%20%3D%20x%29%20%3D%20%5Cfrac%7B%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%29%20%7B%5Crm%20Pr%7D%28X%3Dx%7CY%3Dy_m%29%7D%7B%7B%5Crm%20Pr%7D%28X%20%3D%20x%29%7D
由于朴素贝叶斯方法是基于每个变量是相对独立的假设,也就是说,我们假设%28x_1%2C%20%5Ccdots%2C%20x_n%29的分布是相互独立的,上式可以进一步写为
%5Cbegin%7Baligned%7D%20%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%20%7C%20X%20%3D%20x%29%20%26%3D%20%5Cfrac%7B%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%29%20%7B%5Crm%20Pr%7D%28X_1%3Dx_1%7CY%3Dy_m%29%5Ctimes%20%5Ccdots%5Ctimes%7B%5Crm%20Pr%7D%28X_n%3Dx_n%7CY%3Dy_m%29%7D%7B%7B%5Crm%20Pr%7D%28X%20%3D%20x%29%7D%5C%5C%20%26amp%3B%3D%5Cfrac%7B%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%29%20%5Cprod_i%7B%5Crm%20Pr%7D%28X_i%3Dx_i%7CY%3Dy_m%29%7D%7B%7B%5Crm%20Pr%7D%28X%20%3D%20x%29%7D%20%5Cend%7Baligned%7D

由于每个估计 p_m 解决了分母中的 %7B%5Crm%20Pr%7D%28X%3Dx%29,我们必须处理的问题变成
y_%7Bpredict%7D%20%3D%20%7B%5Crm%20arg%20max%7D_%7By_m%7D%20%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%29%20%5Cprod_i%7B%5Crm%20Pr%7D%28X_i%3Dx_i%7CY%3Dy_m%29
为了方便计算,我们对上式做%7B%5Crm%20log%7D,使乘法变为加法,方便计算
y_%7Bpredict%7D%20%3D%20%7B%5Crm%20arg%20max%7D_%7By_m%7D%20%7B%5Crm%20logPr%7D%28Y%20%3D%20y_m%29%20%2B%20%5Csum_i%7B%5Crm%20log%20Pr%7D%28X_i%3Dx_i%7CY%3Dy_m%29
最后一个问题是如何计算%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%29和力。

其中,%7B%5Crm%20Pr%7D%28Y%20%3D%20y_m%29好办。我们只需要从测试集中统计每次出现y_m的概率,即先验概率。
%7B%5Crm%20Pr%7D%28X_i%3Dx_i%7CY%3Dy_m%29比较复杂。我们需要得到测试集中输出y_m特征值x_i的均值%5Cmu_%7Bmi%7D和方差%5Csigma_%7Bmi%7D,然后通过下面的公式计算
%7B%5Crm%20Pr%7D%28X_i%3Dx_i%7CY%3Dy_m%29%20%3D%20%5Cfrac%7B1%7D%7B%5Csqrt%7B2%5Cpi%5Csigma%5E2_%7Bmi%7D%7D%7De%5E%7B-%5Cfrac%7B%28x_i%20-%20%5Cmu_%7Bxi%7D%29%5E2%7D%7B2%5Csigma_%7Bmi%7D%5E2%7D%7D

概括

我们先理清思路,看看接下来要做什么。

  1. 得到一个测试集,统计测试集中特征值x对应的每个输出y_m的先验概率%7B%5Crm%20Pr%7D%28Y%3Dy_m%29、均值%5Cmu_m和方差%5Csigma_m
  2. 输入任意一张图片的像素信息x使用第一步得到的先验概率%7B%5Crm%20Pr%7D%28Y%3Dy_m%29、均值%5Cmu_m、方差%5Csigma_m,估计y_%7Bpredict%7D并与真实值y_%7Btrue%7D进行比较,看看效果。

Python实现

划分数据集

这里直接调用sklearn中的api来实现数据集的划分,

from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

digits = load_digits()
x_train, x_test, y_train, y_test = train_test_split(digits.data,
                                                    digits.target,
                                                    test_size=0.3)

其中,x_trainy_train是我们用来训练的数据,分别对应特征值和正确输出。在完成模型的学习后,我们使用x_testy_test来评估模型的质量。

模型训练

根据前面的理论,我们知道我们需要使用训练集来获取一些样本的均值、方差和先验概率。请注意,此处不需要诸如梯度下降之类的优化。我们可以定义一个class来方便我们的调用。

class NaiveBayes:
    def train(self, X: np.ndarray, y: np.ndarray):
        """
        训练函数,获取训练集的均值,方差,先验概率
        """
        # 获取数据集大小
        n_samples, n_features = X.shape
        # 获取数据集类别和类别数
        self._classes = np.unique(y)
        n_classes = len(self._classes)
        # print(self._classes)
        # 初始化均值,方差,先验概率
        self._mean = np.zeros((n_classes, n_features), dtype=float)
        self._var = np.zeros((n_classes, n_features), dtype=float)
        self._priors = np.zeros(n_classes, dtype=float)
        # 计算均值,方差,先验概率
        for c in self._classes:
        	# 找到输出为c的样本
            X_c = X[c == y]
            # 获取输出为c的均值,方差,先验概率
            self._mean[c, :] = X_c.mean(axis=0)
            self._var[c, :] = X_c.var(axis=0)
            self._priors[c] = X_c.shape[0] / float(n_samples)

模型预测

得到训练集的均值、方差和先验概率后,我们就可以对测试集进行预测了

    def predict(self, X) -> List[int]:
        """
        获取测试集的预测
        :return: 预测结果
        """
        pred = []
        for x in X:
        	# 统计每中输出的可能性
            probs = []
            for y in range(len(self._classes)):
                probs.append(self._cal_prob(x, y))
            # 找到概率最大的那个类别
            pred.append(np.argmax(probs))
        return pred

最重要的部分是self._cal_prob(x, y)的写作

    def _cal_prob(self, x, y) -> float:
        """
        计算 log p(x_1|y) + ... + log p(x_n|y) + log p(y)
        注意:这里的 y 为在 self._classes 中的下标
        """
        # log p(y)
        p_y = np.log(self._priors[y])
        # print(p_y)
        # log p(x_1|y) + ... + log p(x_n|y)
        mean = self._mean[y]
        # print("mean are: \n", mean)
        var = self._var[y]
        # print("var: \n", var)
        p_xy = np.log(np.exp(-(x-mean)**2 / (2*var)) / np.sqrt(2*np.pi*var))
        # p_xy 的计算结果可能会出现 nan,我们将它取值改为0,让他不对输出造成影响
        p_xy[np.isnan(p_xy)] = 0
        return p_y + np.sum(p_xy)

模型评估

我们通过使用以下代码计算模型预测的准确性来评估我们的模型:

def accuracy(y_pred, y_true):
    return np.sum(y_pred == y_true) / len(y_true)

显示结果

整个过程的代码如下,

digits= load_digits()
x_train, x_test, y_train, y_test = train_test_split(digits.data,
                                                    digits.target,
                                                    test_size=0.3)
bayes = NaiveBayes()
bayes.train(x_train, y_train)
y_pred = bayes.predict(x_test)
print(accuracy(y_pred, y_test))

我运行的输出是

0.8962962962962963

我们也可以随机选择部分测试集查看效果,测试代码如下

def show_test_result(nb: NaiveBayes):
    fig = plt.figure(figsize=(6, 6))  # figure size in inches
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)
    n = digits.data.shape[0]
    for i in range(64):
        ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[])
        # 随机选择一张图片
        idx = random.randint(0, n-1)
        ax.imshow(digits.images[idx], cmap=plt.cm.binary, interpolation='nearest')
        data = np.asarray(digits.data[idx]).reshape(1, -1)

        # 左下角为预测值
        ax.text(0, 7, str(nb.predict(data)[0]), color="green")
        # 左上角为真实值
        ax.text(0, 1, str(digits.target[idx]), color="red")

    plt.show()

朴素贝叶斯:教你用Python一步步识别手写数字
由于我们假设每个像素是相对独立的,这与现实不符,所以最后也能发现一些错误。

完整的测试代码

#!/usr/bin/python3

import numpy as np
import matplotlib.pyplot as plt
import random
from typing import List, Tuple

from sklearn.decomposition import PCA
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB

import warnings
warnings.filterwarnings('ignore')


def show_dataset():
    fig = plt.figure(figsize=(6, 6))  # figure size in inches
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)

    for i in range(64):
        ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[])
        ax.imshow(digits.images[i], cmap=plt.cm.binary, interpolation='nearest')
        # label the image with the target value
        ax.text(0, 7, str(digits.target[i]))

    plt.show()


class NaiveBayes:
    def train(self, X: np.ndarray, y: np.ndarray):
        """
        训练函数
        :return: 训练集的均值,方差,先验概率
        """
        # 获取数据集大小
        n_samples, n_features = X.shape
        # print(n_features)
        # 获取数据集类别和类别数
        self._classes = np.unique(y)
        n_classes = len(self._classes)
        # print(self._classes)
        # 初始化均值,方差,先验概率
        self._mean = np.zeros((n_classes, n_features), dtype=float)
        self._var = np.zeros((n_classes, n_features), dtype=float)
        self._priors = np.zeros(n_classes, dtype=float)
        # 计算均值,方差,先验概率
        for c in self._classes:
            X_c = X[c == y]
            self._mean[c, :] = X_c.mean(axis=0)
            self._var[c, :] = X_c.var(axis=0)
            self._priors[c] = X_c.shape[0] / float(n_samples)

    def _cal_prob(self, x, y) -> float:
        """
        计算 log p(x_1|y) + ... + log p(x_n|y) + log p(y)
        注意:这里的 y 为在 self._classes 中的下标
        """
        # log p(y)
        p_y = np.log(self._priors[y])
        # print(p_y)
        # log p(x_1|y) + ... + log p(x_n|y)
        mean = self._mean[y]
        # print("mean are: \n", mean)
        var = self._var[y]
        # print("var: \n", var)
        p_xy = np.log(np.exp(-(x-mean)**2 / (2*var)) / np.sqrt(2*np.pi*var))
        p_xy[np.isnan(p_xy)] = 0
        return p_y + np.sum(p_xy)

    def predict(self, X) -> List[int]:
        """
        测试数据
        :return: 预测结果
        """
        pred = []
        for x in X:
            probs = []
            for y in range(len(self._classes)):
                probs.append(self._cal_prob(x, y))
            # 找到概率最大的那个类别
            pred.append(np.argmax(probs))
        return pred


def accuracy(y_pred, y_true):
    return np.sum(y_pred == y_true) / len(y_true)


def show_test_result(nb: NaiveBayes):
    fig = plt.figure(figsize=(6, 6))  # figure size in inches
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)
    n = digits.data.shape[0]
    for i in range(64):
        ax = fig.add_subplot(8, 8, i + 1, xticks=[], yticks=[])
        # 随机选择一张图片
        idx = random.randint(0, n-1)
        ax.imshow(digits.images[idx], cmap=plt.cm.binary, interpolation='nearest')
        data = np.asarray(digits.data[idx]).reshape(1, -1)

        # 左下角为预测值
        ax.text(0, 7, str(nb.predict(data)[0]), color="green")
        # 左上角为真实值
        ax.text(0, 1, str(digits.target[idx]), color="red")

    plt.show()


digits = load_digits()
if __name__ == '__main__':
    x_train, x_test, y_train, y_test = train_test_split(digits.data,
                                                        digits.target,
                                                        test_size=0.3)
    bayes = NaiveBayes()
    bayes.train(x_train, y_train)
    y_pred = bayes.predict(x_test)
    print(accuracy(y_pred, y_test))

    show_test_result(bayes)


参考

  • “统计学习方法”
  • Naive Bayes in Python – Machine Learning From Scratch 05 – Python Tutorial
  • Simple visualization and classification of the digits dataset

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2022年3月22日 下午2:01
下一篇 2022年3月22日 下午2:18

相关推荐