3.5 新闻分类:多分类问题
上一节中,我们介绍了如何用密集连接的神经网络将向量输入划分为两个互斥的类别。但如果类别不止两个,要怎么做?
本节你会构建一个网络,将路透社新闻划分为 46个互斥的主题。因为有多个类别,所以这是多分类(multiclass classification)问题的一个例子。因为每个数据点只能划分到一个类别,所以更具体地说,这是单标签、多分类(single-label, multiclass classification)问题的一个例子。如果每个数据点可以划分到多个类别(主题),那它就是一个多标签、多分类(multilabel,multiclass classification)问题。
3.5.1 路透社数据集
本节使用路透社数据集,它包含许多短新闻及其对应的主题,由路透社在 1986年发布。它是一个简单的、广泛使用的文本分类数据集。它包括 46个不同的主题:某些主题的样本更多,但训练集中每个主题都有至少 10个样本。与 IMDB和 MNIST类似,路透社数据集也内置为 Keras的一部分。我们来看一下。
代码清单 3-12 加载路透社数据集
from keras.datasets import reuters (train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)
与 IMDB数据集一样,参数num_words=10000将数据限定为前 10 000个最常出现的单词。我们有 8982个训练样本和 2246个测试样本。
>>> len(train_data) 8982 >>> len(test_data) 2246
与 IMDB评论一样,每个样本都是一个整数列表(表示单词索引)。
>>> train_data[10] [1, 245, 273, 207, 156, 53, 74, 160, 26, 14, 46, 296, 26, 39, 74, 2979,3554, 14, 46, 4689, 4329, 86, 61, 3499, 4795, 14, 61, 451, 4329, 17, 12]
如果好奇的话,你可以用下列代码将索引解码为单词。
代码清单 3-13 将索引解码为新闻文本
3.5.2 准备数据
你可以使用与上一个例子相同的代码将数据向量化。
代码清单 3-14 编码数据
将标签向量化有两种方法:你可以将标签列表转换为整数张量,或者使用 one-hot编码。one-hot编码是分类数据广泛使用的一种格式,也叫分类编码(categorical encoding)。6.1节给出了 one-hot编码的详细解释。在这个例子中,标签的 one-hot编码就是将每个标签表示为全零向量, 只有标签索引对应的元素为 1。其代码实现如下。
注意,Keras内置方法可以实现这个操作,你在 MNIST例子中已经见过这种方法。
3.5.3 构建网络
这个主题分类问题与前面的电影评论分类问题类似,两个例子都是试图对简短的文本片段进行分类。但这个问题有一个新的约束条件:输出类别的数量从 2个变为 46个。输出空间的维度要大得多。
对于前面用过的Dense层的堆叠,每层只能访问上一层输出的信息。如果某一层丢失了与分类问题相关的一些信息,那么这些信息无法被后面的层找回,也就是说,每一层都可能成为信息瓶颈。上一个例子使用了 16维的中间层,但对这个例子来说 16维空间可能太小了,无法学会区分 46个不同的类别。这种维度较小的层可能成为信息瓶颈,永久地丢失相关信息。
出于这个原因,下面将使用维度更大的层,包含 64个单元。
代码清单 3-15 模型定义
from keras import models from keras import layers model = models.Sequential() model.add(layers.Dense(64, activation='relu', input_shape=(10000,))) model.add(layers.Dense(64, activation='relu')) model.add(layers.Dense(46, activation='softmax'))
关于这个架构还应该注意另外两点。
- 网络的最后一层是大小为 46的Dense层。这意味着,对于每个输入样本,网络都会输出一个 46维向量。这个向量的每个元素(即每个维度)代表不同的输出类别。
- 最后一层使用了 softmax激活。你在 MNIST例子中见过这种用法。网络将输出在 46个不同输出类别上的概率分布——对于每一个输入样本,网络都会输出一个 46维向量,其中output[i]是样本属于第i个类别的概率。46个概率的总和为 1。
对于这个例子,最好的损失函数是 categorical_crossentropy(分类交叉熵)。它用于衡量两个概率分布之间的距离,这里两个概率分布分别是网络输出的概率分布和标签的真实分布。通过将这两个分布的距离最小化,训练网络可使输出结果尽可能接近真实标签。
代码清单 3-16 编译模型
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
3.5.4 验证你的方法
我们在训练数据中留出 1000个样本作为验证集。
代码清单 3-17 留出验证集
x_val = x_train[:1000] partial_x_train = x_train[1000:] y_val = one_hot_train_labels[:1000] partial_y_train = one_hot_train_labels[1000:]
现在开始训练网络,共 20个轮次。
代码清单 3-18 训练模型
history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512,validation_data=(x_val, y_val))
最后,我们来绘制损失曲线和精度曲线(见图 3-9和图 3-10)。
图 3-9 训练损失和验证损失
图 3-10 训练精度和验证精度
代码清单 3-19 绘制训练损失和验证损失
import matplotlib.pyplot as plt loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(1, len(loss) + 1) plt.plot(epochs, loss, 'bo', label='Training loss') plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.xlabel('Epochs') plt.ylabel('Loss') plt.legend() plt.show()
代码清单 3-20 绘制训练精度和验证精度
网络在训练 9轮后开始过拟合。我们从头开始训练一个新网络,共 9个轮次,然后在测试集上评估模型。
代码清单 3-21 从头开始重新训练一个模型
model = models.Sequential() model.add(layers.Dense(64, activation='relu', input_shape=(10000,))) model.add(layers.Dense(64, activation='relu')) model.add(layers.Dense(46, activation='softmax')) model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy']) model.fit(partial_x_train, partial_y_train, epochs=9, batch_size=512, validation_data=(x_val, y_val)) results = model.evaluate(x_test, one_hot_test_labels)
最终结果如下。
>>> results [0.9565213431445807, 0.79697239536954589]
这种方法可以得到约 80%的精度。对于平衡的二分类问题,完全随机的分类器能够得到50%的精度。但在这个例子中,完全随机的精度约为 19%,所以上述结果相当不错,至少和随机的基准比起来还不错。
>>> import copy >>> test_labels_copy = copy.copy(test_labels) >>> np.random.shuffle(test_labels_copy) >>> hits_array = np.array(test_labels) == np.array(test_labels_copy) >>> float(np.sum(hits_array)) / len(test_labels) 0.18655387355298308
3.5.5 在新数据上生成预测结果
你可以验证,模型实例的 predict方法返回了在 46个主题上的概率分布。我们对所有测试数据生成主题预测。
代码清单 3-22 在新数据上生成预测结果
3.5.6 处理标签和损失的另一种方法
前面提到了另一种编码标签的方法,就是将其转换为整数张量,如下所示。
y_train = np.array(train_labels) y_test = np.array(test_labels)
对于这种编码方法,唯一需要改变的是损失函数的选择。对于代码清单 3-21使用的损失函数 categorical_crossentropy,标签应该遵循分类编码。对于整数标签,你应该使用sparse_categorical_crossentropy。
model.compile(optimizer='rmsprop', loss='sparse_categorical_crossentropy', metrics=['acc'])
这个新的损失函数在数学上与categorical_crossentropy完全相同,二者只是接口不同。
3.5.7 中间层维度足够大的重要性
前面提到,最终输出是 46维的,因此中间层的隐藏单元个数不应该比 46小太多。现在来看一下,如果中间层的维度远远小于 46(比如 4维),造成了信息瓶颈,那么会发生什么?
代码清单 3-23 具有信息瓶颈的模型
model = models.Sequential() model.add(layers.Dense(64, activation='relu', input_shape=(10000,))) model.add(layers.Dense(4, activation='relu')) model.add(layers.Dense(46, activation='softmax')) model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy']) model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=128, validation_data=(x_val, y_val))
现在网络的验证精度最大约为 71%,比前面下降了 8%。导致这一下降的主要原因在于,你试图将大量信息(这些信息足够恢复 46个类别的分割超平面)压缩到维度很小的中间空间。网络能够将大部分必要信息塞入这个四维表示中,但并不是全部信息。
3.5.8 进一步的实验
- 尝试使用更多或更少的隐藏单元,比如 32个、128个等。
- 前面使用了两个隐藏层,现在尝试使用一个或三个隐藏层。
3.5.9 小结
下面是你应该从这个例子中学到的要点。
- 如果要对 N个类别的数据点进行分类,网络的最后一层应该是大小为 N的Dense层。
- 对于单标签、多分类问题,网络的最后一层应该使用softmax激活,这样可以输出在 N个输出类别上的概率分布。
- 这种问题的损失函数几乎总是应该使用分类交叉熵。它将网络输出的概率分布与目标的真实分布之间的距离最小化。
- 处理多分类问题的标签有两种方法。
- 通过分类编码(也叫 one-hot编码)对标签进行编码,然后使用categorical_crossentropy作为损失函数。
- 将标签编码为整数,然后使用sparse_categorical_crossentropy损失函数。
- 如果你需要将数据划分到许多类别中,应该避免使用太小的中间层,以免在网络中造成
- 信息瓶颈。