朴素贝叶斯分类——【torch学习笔记】

朴素贝叶斯分类

引用翻译:《动手学深度学习》

在我们担心复杂的优化算法或GPU之前,我们已经可以部署我们的第一个分类器,只依靠简单的统计估计器和我们对条件独立性的理解。学习就是要做假设。如果我们想对一个从未见过的新数据点进行分类,我们就必须对哪些数据点是相互类似的做出一些假设。

y ^ = argmax y   p ( y ∣ x ) \hat{y} = \text{argmax}_y \> p(y | \mathbf{x}) y^=argmaxyp(yx)

不幸的是,这要求我们对(|)的每个值=1,…,进行估计。想象一下,每个特征可以取两个值中的一个。例如,特征1=1可能表示苹果这个词出现在一个给定的文件中,而1=0则表示它没有出现。如果我们有230个这样的二进制特征,这就意味着我们需要准备对输入向量的 2 d , ( d = 230 ) 2^d ,(d=230) 2d,(d=230)个(超过10亿个!)可能的值进行分类。

此外,如何进行模型学习呢?如果我们需要看到每一个可能的例子来预测相应的标签,那么我们就不是真正在学习一个模式,而只是在记忆数据集。幸运的是,通过对条件独立性的一些假设,我们可以引入一些归纳偏见,建立一个能够从相对较少的训练例子中归纳的模型。

首先,让我们使用贝叶斯定理,将分类器表示为

y ^ = argmax y   p ( x ∣ y ) p ( y ) p ( x ) \hat{y} = \text{argmax}_y \> \frac{p( \mathbf{x} | y) p(y)}{p(\mathbf{x})} y^=argmaxyp(x)p(xy)p(y)

请注意,分母是归一化项(),它不依赖于标签的值。因此,我们只需要担心在不同的值之间比较分子的问题。即使计算分母是难以实现的,我们也可以忽略它,只要我们能评估分母就可以了。然而,幸运的是,即使我们想恢复归一化常数,我们也可以,因为我们知道∑(|)=1,因此我们总是能够恢复归一化项。现在,利用概率的连锁规则,我们可以把(|)这个项表示为

p ( x 1 ∣ y ) ⋅ p ( x 2 ∣ x 1 , y ) ⋅ . . . ⋅ p ( x d ∣ x 1 , . . . , x d − 1 y ) p(x_1 |y) \cdot p(x_2 | x_1, y) \cdot ... \cdot p( x_d | x_1, ..., x_{d-1} y) p(x1y)p(x2x1,y)...p(xdx1,...,xd1y)

就其本身而言,这个表达式并没有让我们取得任何进展。我们仍然必须估计大约 2 d 2^d 2d个参数。然而,如果我们假设特征是有条件地相互独立的,给定的标签,那么我们的情况突然好了很多,因为这个术语简化为 ∏ i p ( x i ∣ y ) \prod_i p(x_i | y) ip(xiy) ,给了我们预测器

y ^ = argmax y   = ∏ i p ( x i ∣ y ) p ( y ) \hat{y} = \text{argmax}_y \> = \prod_i p(x_i | y) p(y) y^=argmaxy=ip(xiy)p(y)

估计 ∏ i p ( x i ∣ y ) \prod_i p(x_i | y) ip(xiy)中的每个项,相当于只估计一个参数。因此,我们对条件独立性的假设使我们模型的复杂性(就参数数量而言)从对特征数量的指数依赖变为线性依赖。此外,我们现在可以对以前从未见过的例子进行预测,因为我们只需要估计术语(|),这可以根据一些不同的文档来估计。

让我们仔细看看关键的假设,即在标签的情况下,属性都是相互独立的,即(|)= ∏ i p ( x i ∣ y ) \prod_i p(x_i | y) ip(xiy) 。考虑将电子邮件分类为垃圾邮件和火腿。可以说,出现尼日利亚、王子、金钱、富人等词都有可能表明该邮件可能是垃圾邮件,而定理、网络、贝叶斯或统计学则很好地表明,该交流不太可能是精心策划的试图骗取你的银行账户号码的一部分。因此,我们可以对这些词中的每一个词的出现概率进行建模,给定各自的类别,然后用它来对一篇文章的可能性进行评分。事实上,在很长一段时间里,这正是许多所谓的贝叶斯式垃圾邮件过滤器的工作方式。

一、光学字符识别

由于图像更容易处理,我们将说明在MNIST数据集上区分数字的Naive Bayes分类器的工作情况。问题是,我们实际上并不知道()和(|) 。因此,我们需要先在一些训练数据下对其进行估计。这就是所谓的训练模型。估计()不是太难。因为我们只处理 10 个类,所以这很容易–只需计算每个数字的出现次数,然后除以总数据量。例如,如果数字8出现8=5,800次,而我们总共有=60,000张图片,那么概率估计为(=8)=0.0967。

现在来谈谈稍微困难的事情– p ( x i ∣ y ) p(x_i | y) p(xiy) 。由于我们选择了黑白图像, p ( x i ∣ y ) p(x_i | y) p(xiy)表示像素在类中被开启的概率。就像以前一样,我们可以去计算 n i y n_{iy} niy这样的事件发生的次数,然后将其除以y的总发生次数,即。但有一点令人不安:某些像素可能永远不会是黑色的(例如,对于裁剪得非常好的图像,角落的像素可能永远是白色的)。统计学家处理这个问题的一个方便方法是为所有出现的情况添加伪计数。因此,我们不使用 n i y n_{iy} niy,而使用 n i y + 1 n_{iy}+1 niy+1;不使用 n y n_y ny,而使用 n y + 1 n_{y} + 1 ny+1。这也被称为拉普拉斯平滑法。

# 下载数据集,导入相关包
%matplotlib inline
import tqdm
import numpy as np
from matplotlib import pyplot as plt
from IPython import display
display.set_matplotlib_formats('svg')
import torch
from torch import tensor
from torchvision import transforms, datasets

data_transform = transforms.Compose([transforms.Grayscale(), transforms.ToTensor(), transforms.Normalize(mean=[0],std=[1])])
# 第一次使用,将download=False 设定为 download=True
mnist_train = datasets.MNIST(root='./data', train=True, download=False, transform=data_transform)
mnist_test  = datasets.MNIST(root='./data', train=False, download=False, transform=data_transform)

计算一些先验信息。如:

  • px是:每个类别的每个像素点平均值
  • py是:即每个类别的占比情况,即类别先验信息
# Initialize the counters
xcount = torch.ones((784,10), dtype=torch.float32)
ycount = torch.ones((10), dtype=torch.float32)

for data, label in mnist_train:
    y = int(label)
    ycount[y] += 1
    xcount[:,y] += data.reshape((784))

# 再次使用广播机制进行除法
py = ycount / ycount.sum()  # 每个类别占总数的比例,占比情况
px = (xcount / ycount.reshape(1,10))  # 计算每个类别的每个像素点平均值

py : 即每个类别的占比情况,

py
tensor([0.0987, 0.1124, 0.0993, 0.1022, 0.0974, 0.0904, 0.0986, 0.1044, 0.0975,
        0.0992])

px : 每一个子列表代表每个类别的每个像素点平均值

px
tensor([[0.0002, 0.0001, 0.0002,  ..., 0.0002, 0.0002, 0.0002],
        [0.0002, 0.0001, 0.0002,  ..., 0.0002, 0.0002, 0.0002],
        [0.0002, 0.0001, 0.0002,  ..., 0.0002, 0.0002, 0.0002],
        ...,
        [0.0002, 0.0001, 0.0002,  ..., 0.0002, 0.0002, 0.0002],
        [0.0002, 0.0001, 0.0002,  ..., 0.0002, 0.0002, 0.0002],
        [0.0002, 0.0001, 0.0002,  ..., 0.0002, 0.0002, 0.0002]])

px[0] :每一个子列表代表着 各类别对应index位置的平均值

px[0]
tensor([0.0002, 0.0001, 0.0002, 0.0002, 0.0002, 0.0002, 0.0002, 0.0002, 0.0002,
        0.0002])

len(px) : 有784个子列表,即代表将28X28进行拉匀处理,即784个像素点

len(px)
784

现在,我们计算了所有像素的每像素出现次数,是时候看看我们的模型表现如何了。是时候绘制它了。这就是使用图像工作更方便的地方。将28x28x10的概率(每个类的每个像素)可视化,通常是一种徒劳的练习。然而,通过将它们绘制成图像,我们就可以快速了解情况。精明的读者现在可能已经注意到,这些数字看起来很糟糕…。

# 将每个类别的平均相似度值计算可视化
import matplotlib.pyplot as plt
fig, figarr = plt.subplots(1, 10, figsize=(10, 10))
for i in range(10):
    figarr[i].imshow(xcount[:, i].reshape((28, 28)).numpy(), cmap='hot')
    temp= xcount[:, i].reshape((28, 28)).numpy()
    figarr[i].axes.get_xaxis().set_visible(False)
    figarr[i].axes.get_yaxis().set_visible(False)

plt.show()
print('Class probabilities', py)

在这里插入图片描述

Class probabilities tensor([0.0987, 0.1124, 0.0993, 0.1022, 0.0974, 0.0904, 0.0986, 0.1044, 0.0975,
        0.0992])

temp: 即每个类别的平均像素点大小。

temp[15]   # 子列表有28个元素
array([1.0000000e+00, 1.0000000e+00, 1.2313726e+00, 2.1133335e+01,
       1.1228237e+02, 3.3093332e+02, 7.7862616e+02, 1.5395934e+03,
       2.4571760e+03, 3.0999353e+03, 3.2772087e+03, 3.2535369e+03,
       3.2645242e+03, 3.3113159e+03, 3.4684912e+03, 4.0935984e+03,
       4.8088276e+03, 4.5622769e+03, 3.0398655e+03, 1.4501147e+03,
       5.7821906e+02, 2.1316077e+02, 8.1450966e+01, 2.4717649e+01,
       3.6470592e+00, 1.8862746e+00, 1.4039216e+00, 1.0000000e+00],
      dtype=float32)
len(temp)  # 存在28个子列表
28

现在,我们可以根据模型计算出图像的可能性。这是统计学家对(|)的说法,即在某些条件下(如标签)看到一个特定图像的可能性。我们的Naive Bayes模型假设所有像素都是独立的,它告诉我们

p ( x ∣ y ) = ∏ i p ( x i ∣ y ) p(\mathbf{x} | y) = \prod_{i} p(x_i | y) p(xy)=ip(xiy)

利用贝叶斯规则,我们可以通过以下方式计算出(|):

p ( y ∣ x ) = p ( x ∣ y ) p ( y ) ∑ y ′ p ( x ∣ y ′ ) p(y | \mathbf{x}) = \frac{p(\mathbf{x} | y) p(y)}{\sum_{y'} p(\mathbf{x} | y')} p(yx)=yp(xy)p(xy)p(y)

未加对数处理时:

# 选取第一个样本
data, label = mnist_test[0]
data = data.reshape((784,1))

# 计算每个像素的条件概率
xprob = (px * data + (1-px) * (1-data))
# px是:每个类别的每个像素点平均值
# data是:第一个样本的784个像素点
# py是:即每个类别的占比情况,即类别先验信息
# xprob此时代表每个相似点属于各个类别的概率
# Take the product
xprob = xprob.prod(0) * py
print('非标准化概率:', xprob)
# Normalize
xprob = xprob / xprob.sum()  # 计算xprob中的占比情况,取最大的一个。
print('标准化概率:', xprob)

此处有个prod函数:

torch.prod(input) → float

返回输入张量input所有元素的乘积。

参数:

input (Tensor) - 输入张量

非标准化概率: tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
标准化概率: tensor([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan])

其中根据公式计算可得:

xprob = (px * data + (1-px) * (1-data))
xprob[0]
tensor([0.9998, 0.9999, 0.9998, 0.9998, 0.9998, 0.9998, 0.9998, 0.9998, 0.9998,0.9998])
xprob = xprob.prod(0) * py
xprob
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

这就出现了严重错误! 为了找出原因,我们来看看每个像素的概率。它们通常是0.001和1之间的数字。我们要把它们乘以784。在这一点上值得一提的是,我们是在电脑上计算这些数字的,因此指数的范围是固定的。发生的情况是,我们经历了数字下溢,也就是说,将所有的小数字相乘会导致更小的数字,直到四舍五入为零。在这一点上,我们得到的是除以零的结果。

为了解决这个问题,我们利用log=log+log这一事实,即我们改用对数求和。这将使我们得到对数空间中的非标准化概率。为了使条款正常化,我们利用以下事实

exp ⁡ ( a ) exp ⁡ ( a ) + exp ⁡ ( b ) = exp ⁡ ( a + c ) exp ⁡ ( a + c ) + exp ⁡ ( b + c ) \frac{\exp(a)}{\exp(a) + \exp(b)} = \frac{\exp(a + c)}{\exp(a + c) + \exp(b + c)} exp(a)+exp(b)exp(a)=exp(a+c)+exp(b+c)exp(a+c)

特别是,我们可以选择=-max(,),这样可以保证分母中至少有一个项是1。

# 对px、1-px、py进行对数化处理,避免出现nan情形
logpx = torch.log(px)
logpxneg = torch.log(1-px)
logpy = torch.log(py)
def bayespost(data):
    #  我们需要加入先验概率p(y),因为p(y|x)与p(x|y)p(y)成正比 
    logpost = logpy.clone()
    # 和之前计算方式相同,只是用log后的张量
    logpost += (logpx * data + logpxneg * (1-data)).sum(0)  
    # 通过减去最大的值归一化,以防止溢出或下溢,
    logpost -= torch.max(logpost)
    # 用logpx计算softmax
    post = torch.exp(logpost).numpy()
    post /= np.sum(post)
    return post
fig, figarr = plt.subplots(2, 10, figsize=(10, 3))

# Show 诗歌图像
ctr = 0
label_list = []
for data, label in mnist_test:
    x = data.reshape((784,1))
    y = int(label)
    post = bayespost(x)
    label_list.append(y)
    # post是array([0.0000000e+00, 0.0000000e+00,..., 9.9836236e-01], dtype=float32)形式,选取最大的
    # 条形图和数字图像
    figarr[1, ctr].bar(range(10), post)
    figarr[1, ctr].axes.get_yaxis().set_visible(False)
    figarr[0, ctr].imshow(x.reshape((28, 28)).numpy(), cmap='hot')
    figarr[0, ctr].axes.get_xaxis().set_visible(False)
    figarr[0, ctr].axes.get_yaxis().set_visible(False)
    ctr += 1
    if ctr == 10:
        break
plt.show()

朴素贝叶斯分类——【torch学习笔记】_第1张图片

# 真实标签列表
label_list
[7, 2, 1, 0, 4, 1, 4, 9, 5, 9]
# 其中post是指该样本属于每个类别的概率。可取其中最大值作为类别输出。即借用post.max()
post
array([0.0000000e+00, 0.0000000e+00, 0.0000000e+00, 0.0000000e+00,
           4.1934492e-13, 5.4348684e-28, 0.0000000e+00, 1.6375950e-03,
           6.4475615e-23, 9.9836236e-01], dtype=float32)

正如我们所看到的,这个分类器在许多情况下工作得相当好。然而,倒数第二位显示,它既可以不称职,也可以对其错误的估计过于自信。也就是说,即使它错得很离谱,它产生的概率也接近于1或0。 现在我们已经不应该经常使用这种分类器了。为了看看它的整体表现如何,让我们计算一下分类器的整体准确性。

# 准确率计数
ctr = 0
err = 0

for data, label in mnist_test:
    ctr += 1
    x = data.reshape((784,1))
    y = int(label)
    
    post = bayespost(x)
    # 若post列表中真实标签y对应位置的值不是最大值,则说明判定错误。
    if (post[y] < post.max()):
        err += 1

print('错误率:', err/ctr)
# 此后可以使用混淆矩阵分析分类效果。此处通过index位置判定只是一种简单方法。

# 错误率: 0.1553

现代深度网络的错误率低于0.01。虽然Naive Bayes分类器在80年代和90年代曾经很流行,例如用于垃圾邮件过滤,但它们的鼎盛时期已经过去了。性能不佳是由于我们在模型中做了不正确的统计假设:我们假设每一个像素都是独立产生的,只取决于标签。这显然不是人类书写数字的方式,这个错误的假设导致了我们过于天真的(贝叶斯)分类器的垮台。是时候开始构建深度网络了。

Summary

Naive Bayes是一个易于使用的分类器,它使用的假设是 p ( x ∣ y ) = ∏ i p ( x i ∣ y ) p(\mathbf{x} | y) = \prod_i p(x_i | y) p(xy)=ip(xiy)
这个分类器很容易训练,但它的估计值可能是非常错误的。
为了解决过于自信和无意义的估计,对概率(|)进行平滑处理,例如用拉普拉斯平滑法。也就是说,我们在所有计数中加入一个常数。
Naive Bayes分类器并不利用观察结果之间的任何相关性。

Exercises

设计一个Naive Bayes回归估计器,其中(|)是一个正常分布。Naive Bayes在哪些情况下起作用?一个目击者确信,如果他再次遇到犯罪者,他能以90%的准确率认出他。

1.如果只有5个嫌疑人,这个说法有用吗?

2.如果有50个,它还有用吗?

你可能感兴趣的:(深度学习——torch学习笔记,机器学习,torch,朴素贝叶斯,手写数字识别,图像分类)