22.9. 朴素贝叶斯
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

在前面的章节中,我们学习了概率论和随机变量。为了将这些理论付诸实践,让我们来介绍朴素贝叶斯分类器。它仅使用概率基础知识,就可以让我们对数字进行分类。

学习就是做假设。如果我们想对一个以前从未见过的新数据样本进行分类,我们必须对哪些数据样本是相似的做出一些假设。朴素贝叶斯分类器是一种流行且非常清晰的算法,它假设所有特征相互独立,以简化计算。在本节中,我们将应用这个模型来识别图像中的字符。

%matplotlib inline
import math
import torch
import torchvision
from d2l import torch as d2l

d2l.use_svg_display()
%matplotlib inline
import math
from mxnet import gluon, np, npx
from d2l import mxnet as d2l

npx.set_np()
d2l.use_svg_display()
%matplotlib inline
import math
import tensorflow as tf
from d2l import tensorflow as d2l

d2l.use_svg_display()

22.9.1. 光学字符识别

MNIST (LeCun et al., 1998) 是广泛使用的数据集之一。它包含 60000 张用于训练的图像和 10000 张用于验证的图像。每张图像都包含一个从 0 到 9 的手写数字。任务是将每张图像分类到相应的数字。

Gluon 在 `data.vision` 模块中提供了一个 `MNIST` 类,可以自动从互联网上获取数据集。随后,Gluon 将使用已下载的本地副本。我们通过将参数 `train` 的值分别设置为 `True` 或 `False` 来指定我们请求的是训练集还是测试集。每张图像都是一个灰度图像,宽度和高度均为 \(28\),形状为 (\(28\),\(28\),\(1\))。我们使用自定义转换来移除最后一个通道维度。此外,数据集用无符号的 \(8\) 位整数表示每个像素。我们将它们量化为二元特征以简化问题。

data_transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    lambda x: torch.floor(x * 255 / 128).squeeze(dim=0)
])

mnist_train = torchvision.datasets.MNIST(
    root='./temp', train=True, transform=data_transform, download=True)
mnist_test = torchvision.datasets.MNIST(
    root='./temp', train=False, transform=data_transform, download=True)
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz to ./temp/MNIST/raw/train-images-idx3-ubyte.gz
100%|██████████| 9912422/9912422 [00:00<00:00, 115752065.81it/s]
Extracting ./temp/MNIST/raw/train-images-idx3-ubyte.gz to ./temp/MNIST/raw

Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz to ./temp/MNIST/raw/train-labels-idx1-ubyte.gz
100%|██████████| 28881/28881 [00:00<00:00, 5234904.66it/s]
Extracting ./temp/MNIST/raw/train-labels-idx1-ubyte.gz to ./temp/MNIST/raw

Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz to ./temp/MNIST/raw/t10k-images-idx3-ubyte.gz
100%|██████████| 1648877/1648877 [00:00<00:00, 43715298.68it/s]Extracting ./temp/MNIST/raw/t10k-images-idx3-ubyte.gz to ./temp/MNIST/raw


Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz to ./temp/MNIST/raw/t10k-labels-idx1-ubyte.gz
100%|██████████| 4542/4542 [00:00<00:00, 21501725.47it/s]
Extracting ./temp/MNIST/raw/t10k-labels-idx1-ubyte.gz to ./temp/MNIST/raw
def transform(data, label):
    return np.floor(data.astype('float32') / 128).squeeze(axis=-1), label

mnist_train = gluon.data.vision.MNIST(train=True, transform=transform)
mnist_test = gluon.data.vision.MNIST(train=False, transform=transform)
Downloading /opt/mxnet/datasets/mnist/train-images-idx3-ubyte.gz from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/dataset/mnist/train-images-idx3-ubyte.gz...
Downloading /opt/mxnet/datasets/mnist/train-labels-idx1-ubyte.gz from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/dataset/mnist/train-labels-idx1-ubyte.gz...
[22:05:00] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
Downloading /opt/mxnet/datasets/mnist/t10k-images-idx3-ubyte.gz from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/dataset/mnist/t10k-images-idx3-ubyte.gz...
Downloading /opt/mxnet/datasets/mnist/t10k-labels-idx1-ubyte.gz from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/dataset/mnist/t10k-labels-idx1-ubyte.gz...
((train_images, train_labels), (
    test_images, test_labels)) = tf.keras.datasets.mnist.load_data()

# Original pixel values of MNIST range from 0-255 (as the digits are stored as
# uint8). For this section, pixel values that are greater than 128 (in the
# original image) are converted to 1 and values that are less than 128 are
# converted to 0. See section 18.9.2 and 18.9.3 for why
train_images = tf.floor(tf.constant(train_images / 128, dtype = tf.float32))
test_images = tf.floor(tf.constant(test_images / 128, dtype = tf.float32))

train_labels = tf.constant(train_labels, dtype = tf.int32)
test_labels = tf.constant(test_labels, dtype = tf.int32)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11490434/11490434 [==============================] - 0s 0us/step

我们可以访问一个特定的样本,其中包含图像和相应的标签。

image, label = mnist_train[2]
image.shape, label
(torch.Size([28, 28]), 4)
image, label = mnist_train[2]
image.shape, label
((28, 28), array(4, dtype=int32))
image, label = train_images[2], train_labels[2]
image.shape, label.numpy()
(TensorShape([28, 28]), 4)

我们的样本,这里存储在变量 `image` 中,对应于一个高度和宽度均为 \(28\) 像素的图像。

image.shape, image.dtype
(torch.Size([28, 28]), torch.float32)
image.shape, image.dtype
((28, 28), dtype('float32'))
image.shape, image.dtype
(TensorShape([28, 28]), tf.float32)

我们的代码将每张图像的标签存储为一个标量。它的类型是 \(32\) 位整数。

label, type(label)
(4, int)
label, type(label), label.dtype
(array(4, dtype=int32), mxnet.numpy.ndarray, dtype('int32'))
label.numpy(), label.dtype
(4, tf.int32)

我们也可以同时访问多个样本。

images = torch.stack([mnist_train[i][0] for i in range(10, 38)], dim=0)
labels = torch.tensor([mnist_train[i][1] for i in range(10, 38)])
images.shape, labels.shape
(torch.Size([28, 28, 28]), torch.Size([28]))
images, labels = mnist_train[10:38]
images.shape, labels.shape
((28, 28, 28), (28,))
images = tf.stack([train_images[i] for i in range(10, 38)], axis=0)
labels = tf.constant([train_labels[i].numpy() for i in range(10, 38)])
images.shape, labels.shape
(TensorShape([28, 28, 28]), TensorShape([28]))

让我们将这些样本可视化。

d2l.show_images(images, 2, 9);
../_images/output_naive-bayes_6e475d_75_0.svg
d2l.show_images(images, 2, 9);
../_images/output_naive-bayes_6e475d_78_0.svg
d2l.show_images(images, 2, 9);
../_images/output_naive-bayes_6e475d_81_0.svg

22.9.2. 分类的概率模型

在分类任务中,我们将一个样本映射到一个类别。这里,一个样本是一张 \(28\times 28\) 的灰度图像,一个类别是一个数字。(更详细的解释请参考4.1节。)表达分类任务的一种自然方式是通过概率问题:给定特征(即图像像素),最可能的标签是什么?用 \(\mathbf x\in\mathbb R^d\) 表示样本的特征,\(y\in\mathbb R\) 表示标签。在这里,特征是图像像素,我们可以将一个 \(2\) 维图像重塑为一个向量,使得 \(d=28^2=784\),标签是数字。给定特征的标签概率是 \(p(y \mid \mathbf{x})\)。如果我们能够计算这些概率,在我们的例子中是 \(p(y \mid \mathbf{x})\) 对于 \(y=0, \ldots,9\),那么分类器将输出由以下表达式给出的预测 \(\hat{y}\)

(22.9.1)\[\hat{y} = \mathrm{argmax} \> p(y \mid \mathbf{x}).\]

不幸的是,这要求我们为 \(\mathbf{x} = x_1, ..., x_d\) 的每个值估计 \(p(y \mid \mathbf{x})\)。想象一下,每个特征可以取 \(2\) 个值之一。例如,特征 \(x_1 = 1\) 可能表示单词“苹果”出现在给定文档中,而 \(x_1 = 0\) 则表示它没有出现。如果我们有 \(30\) 个这样的二元特征,那就意味着我们需要准备好对输入向量 \(\mathbf{x}\)\(2^{30}\)(超过10亿!)个可能值进行分类。

此外,学习体现在哪里?如果我们需要看到每一个可能的样本才能预测相应的标签,那么我们不是在真正学习一种模式,而只是在记忆数据集。

22.9.3. 朴素贝叶斯分类器

幸运的是,通过对条件独立性做一些假设,我们可以引入一些归纳偏置,并构建一个能够从相对较少的训练样本中泛化的模型。首先,让我们使用贝叶斯定理,将分类器表示为

(22.9.2)\[\hat{y} = \mathrm{argmax}_y \> p(y \mid \mathbf{x}) = \mathrm{argmax}_y \> \frac{p( \mathbf{x} \mid y) p(y)}{p(\mathbf{x})}.\]

请注意,分母是归一化项 \(p(\mathbf{x})\),它不依赖于标签 \(y\) 的值。因此,我们只需要关心比较不同 \(y\) 值下的分子。即使计算分母是棘手的,我们也可以忽略它,只要我们能评估分子。幸运的是,即使我们想恢复归一化常数,我们也可以做到。我们总是可以恢复归一化项,因为 \(\sum_y p(y \mid \mathbf{x}) = 1\)

现在,让我们关注 \(p( \mathbf{x} \mid y)\)。使用概率的链式法则,我们可以将项 \(p( \mathbf{x} \mid y)\) 表示为

(22.9.3)\[p(x_1 \mid y) \cdot p(x_2 \mid x_1, y) \cdot ... \cdot p( x_d \mid x_1, ..., x_{d-1}, y).\]

这个表达式本身并没有让我们走得更远。我们仍然必须估计大约 \(2^d\) 个参数。然而,如果我们假设*在给定标签的情况下,特征是条件独立的*,那么情况就会好得多,因为这个项简化为 \(\prod_i p(x_i \mid y)\),从而得到预测器

(22.9.4)\[\hat{y} = \mathrm{argmax}_y \> \prod_{i=1}^d p(x_i \mid y) p(y).\]

如果我们能为每个 \(i\)\(y\) 估计 \(p(x_i=1 \mid y)\),并将其值保存在 \(P_{xy}[i, y]\) 中,其中 \(P_{xy}\) 是一个 \(d\times n\) 的矩阵,\(n\) 是类别数量,\(y\in\{1, \ldots, n\}\),那么我们也可以用它来估计 \(p(x_i = 0 \mid y)\),即:

(22.9.5)\[\begin{split}p(x_i = t_i \mid y) = \begin{cases} P_{xy}[i, y] & \textrm{当 } t_i=1 ;\\ 1 - P_{xy}[i, y] & \textrm{当 } t_i = 0 . \end{cases}\end{split}\]

此外,我们为每个 \(y\) 估计 \(p(y)\) 并将其保存在 \(P_y[y]\) 中,其中 \(P_y\) 是一个长度为 \(n\) 的向量。然后,对于任何新的样本 \(\mathbf t = (t_1, t_2, \ldots, t_d)\),我们可以计算

(22.9.6)\[\begin{split}\begin{aligned}\hat{y} &= \mathrm{argmax}_ y \ p(y)\prod_{i=1}^d p(x_t = t_i \mid y) \\ &= \mathrm{argmax}_y \ P_y[y]\prod_{i=1}^d \ P_{xy}[i, y]^{t_i}\, \left(1 - P_{xy}[i, y]\right)^{1-t_i}\end{aligned}\end{split}\]

对于任何 \(y\)。因此,我们对条件独立性的假设已将我们模型的复杂度从对特征数量的指数依赖 \(\mathcal{O}(2^dn)\) 降至线性依赖,即 \(\mathcal{O}(dn)\)

22.9.4. 训练

现在的问题是我们不知道 \(P_{xy}\)\(P_y\)。所以我们需要先根据一些训练数据来估计它们的值。这就是*训练*模型。估计 \(P_y\) 并不太难。由于我们只处理 \(10\) 个类别,我们可以计算每个数字的出现次数 \(n_y\),然后除以数据总量 \(n\)。例如,如果数字 8 出现了 \(n_8 = 5,800\) 次,而我们总共有 \(n = 60,000\) 张图像,那么概率估计是 \(p(y=8) = 0.0967\)

X = torch.stack([mnist_train[i][0] for i in range(len(mnist_train))], dim=0)
Y = torch.tensor([mnist_train[i][1] for i in range(len(mnist_train))])

n_y = torch.zeros(10)
for y in range(10):
    n_y[y] = (Y == y).sum()
P_y = n_y / n_y.sum()
P_y
tensor([0.0987, 0.1124, 0.0993, 0.1022, 0.0974, 0.0904, 0.0986, 0.1044, 0.0975,
        0.0992])
X, Y = mnist_train[:]  # All training examples

n_y = np.zeros((10))
for y in range(10):
    n_y[y] = (Y == y).sum()
P_y = n_y / n_y.sum()
P_y
array([0.09871667, 0.11236667, 0.0993    , 0.10218333, 0.09736667,
       0.09035   , 0.09863333, 0.10441667, 0.09751666, 0.09915   ])
X = train_images
Y = train_labels

n_y = tf.Variable(tf.zeros(10))
for y in range(10):
    n_y[y].assign(tf.reduce_sum(tf.cast(Y == y, tf.float32)))
P_y = n_y / tf.reduce_sum(n_y)
P_y
<tf.Tensor: shape=(10,), dtype=float32, numpy=
array([0.09871667, 0.11236667, 0.0993    , 0.10218333, 0.09736667,
       0.09035   , 0.09863333, 0.10441667, 0.09751666, 0.09915   ],
      dtype=float32)>

现在来看稍微困难一点的 \(P_{xy}\)。由于我们选择了黑白图像,\(p(x_i \mid y)\) 表示对于类别 \(y\),像素 \(i\) 被激活的概率。就像之前一样,我们可以去计算事件发生的次数 \(n_{iy}\),然后除以 \(y\) 的总出现次数,即 \(n_y\)。但这里有一个稍微麻烦的问题:某些像素可能永远不会是黑色的(例如,对于裁剪得很好的图像,角落的像素可能总是白色的)。统计学家处理这个问题的一个方便方法是为所有出现次数添加伪计数。因此,我们使用 \(n_{iy}+1\) 而不是 \(n_{iy}\),使用 \(n_{y}+2\) 而不是 \(n_y\)(因为像素 \(i\) 可以取两个可能的值——黑色或白色)。这也被称为*拉普拉斯平滑*。这可能看起来很特别,但它可以从贝叶斯的角度通过 Beta-二项分布模型来证明其合理性。

n_x = torch.zeros((10, 28, 28))
for y in range(10):
    n_x[y] = torch.tensor(X.numpy()[Y.numpy() == y].sum(axis=0))
P_xy = (n_x + 1) / (n_y + 2).reshape(10, 1, 1)

d2l.show_images(P_xy, 2, 5);
../_images/output_naive-bayes_6e475d_99_0.svg
n_x = np.zeros((10, 28, 28))
for y in range(10):
    n_x[y] = np.array(X.asnumpy()[Y.asnumpy() == y].sum(axis=0))
P_xy = (n_x + 1) / (n_y + 2).reshape(10, 1, 1)

d2l.show_images(P_xy, 2, 5);
../_images/output_naive-bayes_6e475d_102_0.svg
n_x = tf.Variable(tf.zeros((10, 28, 28)))
for y in range(10):
    n_x[y].assign(tf.cast(tf.reduce_sum(
        X.numpy()[Y.numpy() == y], axis=0), tf.float32))
P_xy = (n_x + 1) / tf.reshape((n_y + 2), (10, 1, 1))

d2l.show_images(P_xy, 2, 5);
../_images/output_naive-bayes_6e475d_105_0.svg

通过可视化这些 \(10\times 28\times 28\) 的概率(每个类别的每个像素),我们可以得到一些看起来像平均数字的图像。

现在我们可以使用 (22.9.6) 来预测一张新图像。给定 \(\mathbf x\),以下函数为每个 \(y\) 计算 \(p(\mathbf x \mid y)p(y)\)

def bayes_pred(x):
    x = x.unsqueeze(0)  # (28, 28) -> (1, 28, 28)
    p_xy = P_xy * x + (1 - P_xy)*(1 - x)
    p_xy = p_xy.reshape(10, -1).prod(dim=1)  # p(x|y)
    return p_xy * P_y

image, label = mnist_test[0]
bayes_pred(image)
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
def bayes_pred(x):
    x = np.expand_dims(x, axis=0)  # (28, 28) -> (1, 28, 28)
    p_xy = P_xy * x + (1 - P_xy)*(1 - x)
    p_xy = p_xy.reshape(10, -1).prod(axis=1)  # p(x|y)
    return np.array(p_xy) * P_y

image, label = mnist_test[0]
bayes_pred(image)
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
def bayes_pred(x):
    x = tf.expand_dims(x, axis=0)  # (28, 28) -> (1, 28, 28)
    p_xy = P_xy * x + (1 - P_xy)*(1 - x)
    p_xy = tf.math.reduce_prod(tf.reshape(p_xy, (10, -1)), axis=1)  # p(x|y)
    return p_xy * P_y

image, label = train_images[0], train_labels[0]
bayes_pred(image)
<tf.Tensor: shape=(10,), dtype=float32, numpy=array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)>

这完全出错了!为了找出原因,让我们看看每个像素的概率。它们通常是介于 \(0.001\)\(1\) 之间的数字。我们将 \(784\) 个这样的数相乘。在这里值得一提的是,我们是在计算机上计算这些数字,因此指数有一个固定的范围。发生的情况是,我们遇到了*数值下溢*,也就是说,将所有小数相乘会得到一个更小的数,直到它被四舍五入为零。我们在 22.7节 中曾将此作为一个理论问题讨论过,但在这里我们清楚地看到了这个现象的实际发生。

正如该节所讨论的,我们通过利用 \(\log a b = \log a + \log b\) 这一事实来解决这个问题,也就是说,我们转而对对数求和。即使 \(a\)\(b\) 都是小数,对数值也应该在一个合适的范围内。

a = 0.1
print('underflow:', a**784)
print('logarithm is normal:', 784*math.log(a))
underflow: 0.0
logarithm is normal: -1805.2267129073316
a = 0.1
print('underflow:', a**784)
print('logarithm is normal:', 784*math.log(a))
underflow: 0.0
logarithm is normal: -1805.2267129073316
a = 0.1
print('underflow:', a**784)
print('logarithm is normal:', 784*tf.math.log(a).numpy())
underflow: 0.0
logarithm is normal: -1805.2267379760742

由于对数是一个递增函数,我们可以将 (22.9.6) 重写为

(22.9.7)\[\hat{y} = \mathrm{argmax}_y \ \log P_y[y] + \sum_{i=1}^d \Big[t_i\log P_{xy}[x_i, y] + (1-t_i) \log (1 - P_{xy}[x_i, y]) \Big].\]

我们可以实现以下稳定版本

log_P_xy = torch.log(P_xy)
log_P_xy_neg = torch.log(1 - P_xy)
log_P_y = torch.log(P_y)

def bayes_pred_stable(x):
    x = x.unsqueeze(0)  # (28, 28) -> (1, 28, 28)
    p_xy = log_P_xy * x + log_P_xy_neg * (1 - x)
    p_xy = p_xy.reshape(10, -1).sum(axis=1)  # p(x|y)
    return p_xy + log_P_y

py = bayes_pred_stable(image)
py
tensor([-268.9725, -301.7044, -245.1951, -218.8738, -193.4570, -206.0909,
        -292.5226, -114.6257, -220.3313, -163.1784])
log_P_xy = np.log(P_xy)
log_P_xy_neg = np.log(1 - P_xy)
log_P_y = np.log(P_y)

def bayes_pred_stable(x):
    x = np.expand_dims(x, axis=0)  # (28, 28) -> (1, 28, 28)
    p_xy = log_P_xy * x + log_P_xy_neg * (1 - x)
    p_xy = p_xy.reshape(10, -1).sum(axis=1)  # p(x|y)
    return p_xy + log_P_y

py = bayes_pred_stable(image)
py
array([-268.97253, -301.7044 , -245.19514, -218.87384, -193.45703,
       -206.09088, -292.52264, -114.62566, -220.33133, -163.17842])
log_P_xy = tf.math.log(P_xy)
log_P_xy_neg = tf.math.log(1 - P_xy)
log_P_y = tf.math.log(P_y)

def bayes_pred_stable(x):
    x = tf.expand_dims(x, axis=0)  # (28, 28) -> (1, 28, 28)
    p_xy = log_P_xy * x + log_P_xy_neg * (1 - x)
    p_xy = tf.math.reduce_sum(tf.reshape(p_xy, (10, -1)), axis=1)  # p(x|y)
    return p_xy + log_P_y

py = bayes_pred_stable(image)
py
<tf.Tensor: shape=(10,), dtype=float32, numpy=
array([-266.65015, -320.79282, -261.50186, -202.62967, -295.57236,
       -199.49718, -318.66226, -266.01474, -224.59566, -271.9329 ],
      dtype=float32)>

我们现在可以检查预测是否正确。

py.argmax(dim=0) == label
tensor(True)
# Convert label which is a scalar tensor of int32 dtype to a Python scalar
# integer for comparison
py.argmax(axis=0) == int(label)
array(True)
tf.argmax(py, axis=0, output_type = tf.int32) == label
<tf.Tensor: shape=(), dtype=bool, numpy=True>

如果我们现在预测一些验证样本,可以看到贝叶斯分类器工作得相当好。

def predict(X):
    return [bayes_pred_stable(x).argmax(dim=0).type(torch.int32).item()
            for x in X]

X = torch.stack([mnist_test[i][0] for i in range(18)], dim=0)
y = torch.tensor([mnist_test[i][1] for i in range(18)])
preds = predict(X)
d2l.show_images(X, 2, 9, titles=[str(d) for d in preds]);
../_images/output_naive-bayes_6e475d_159_0.svg
def predict(X):
    return [bayes_pred_stable(x).argmax(axis=0).astype(np.int32) for x in X]

X, y = mnist_test[:18]
preds = predict(X)
d2l.show_images(X, 2, 9, titles=[str(d) for d in preds]);
../_images/output_naive-bayes_6e475d_162_0.svg
def predict(X):
    return [tf.argmax(
        bayes_pred_stable(x), axis=0, output_type = tf.int32).numpy()
            for x in X]

X = tf.stack([train_images[i] for i in range(10, 38)], axis=0)
y = tf.constant([train_labels[i].numpy() for i in range(10, 38)])
preds = predict(X)
d2l.show_images(X, 2, 9, titles=[str(d) for d in preds]);
../_images/output_naive-bayes_6e475d_165_0.svg

最后,让我们计算分类器的总体准确率。

X = torch.stack([mnist_test[i][0] for i in range(len(mnist_test))], dim=0)
y = torch.tensor([mnist_test[i][1] for i in range(len(mnist_test))])
preds = torch.tensor(predict(X), dtype=torch.int32)
float((preds == y).sum()) / len(y)  # Validation accuracy
0.8427
X, y = mnist_test[:]
preds = np.array(predict(X), dtype=np.int32)
float((preds == y).sum()) / len(y)  # Validation accuracy
0.8427
X = test_images
y = test_labels
preds = tf.constant(predict(X), dtype=tf.int32)
# Validation accuracy
tf.reduce_sum(tf.cast(preds == y, tf.float32)).numpy() / len(y)
0.8427

现代深度网络实现了低于 \(0.01\) 的错误率。相对较差的性能是由于我们在模型中做出的不正确的统计假设:我们假设每个像素都是*独立*生成的,只依赖于标签。这显然不是人类书写数字的方式,而这个错误的假设导致了我们过于朴素的(贝叶斯)分类器的失败。

22.9.5. 总结

  • 使用贝叶斯法则,可以通过假设所有观测到的特征都是独立的来构建一个分类器。

  • 这个分类器可以在数据集上通过计算标签和像素值组合的出现次数来进行训练。

  • 几十年来,这个分类器一直是诸如垃圾邮件检测等任务的黄金标准。

22.9.6. 练习

  1. 考虑数据集 \([[0,0], [0,1], [1,0], [1,1]]\),其标签由两个元素的异或(XOR)给出 \([0,1,1,0]\)。基于此数据集构建的朴素贝叶斯分类器的概率是多少?它能成功地对我们的点进行分类吗?如果不能,违反了哪些假设?

  2. 假设我们在估计概率时没有使用拉普拉斯平滑,并且在测试时出现了一个在训练中从未见过的值的数据样本。模型会输出什么?

  3. 朴素贝叶斯分类器是贝叶斯网络的一个特定例子,其中随机变量的依赖关系用图结构编码。虽然完整的理论超出了本节的范围(详见 Koller and Friedman (2009)),请解释为什么在 XOR 模型中允许两个输入变量之间存在显式依赖关系,可以创建一个成功的分类器。