《Deep Learning with Python》第三章 3.5 走进神经网络之新闻多分类

3.5 新闻分类:多分类

在上一小节,学习了如何使用全联接神经网络将向量输入分为二类。但是,当需要多分类时该咋办呢?

在本小节,你将学习构建神经网络,把路透社新闻分为互不相交的46类主题。很明显,这个问题是多分类问题,并且每个数据点都只归为一类,那么该问题属于单标签、多分类;如果每个数据点可以属于多个分类,那么你面对的将是多标签、多分类问题。

3.5.1 路透社新闻数据集

路透社新闻数据集是由路透社1986年发布的短新闻和对应主题的集合,它常被用作文本分类的练手数据集。该数据集有46个不同的新闻主题,在训练集中每个主题包含至少10个新闻。

和IMDB和MNIST数据集一样,路透社新闻数据集也打包作为Keras的一部分,下面简单看下:

1
2
3
4
#Listing 3.12 Loading the Reuters dataset
from keras.datasets import reuters
(train_data, train_labels), (test_data, test_labels) = reuters.load_data(
num_words=10000)

设置参数num_words=10000,保留训练集中词频为top 10000的单词。

你有8982条训练样本数据和2246条测试样本数据:

1
2
3
4
>>> len(train_data)
8982
>>> len(test_data)
2246

从上述返回的结果看,每个样本都是整数列表(词索引):

1
2
3
>>> 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]

下面代码可以把词索引解码成词:

1
2
3
4
5
#Listing 3.13 Decoding newswires back to text
word_index = reuters.get_word_index()
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
#Note that the indices are offset by 3 because 0, 1, and 2 are reserved indices for “padding,” “start of sequence,” and “unknown.”
decoded_newswire = ' '.join([reverse_word_index.get(i - 3, '?') for i in train_data[0]])

样本的label是0到45的整数(主题索引):

1
2
>>> train_labels[10]
3
3.5.2 准备数据

使用和上一小节同样的代码进行数据向量化。

1
2
3
4
5
6
7
8
9
10
11
12
13
#Listing 3.14 Encoding the data
import numpy as np
def vectorize_sequences(sequences, dimension=10000):
results = np.zeros((len(sequences), dimension))
for i, sequence in enumerate(sequences):
results[i, sequence] = 1.
return results
#Vectorized training data
x_train = vectorize_sequences(train_data)
#Vectorized test data
x_test = vectorize_sequences(test_data)

向量化label有两种方法:一种是,将label列表转成整数张量,另一种是,使用one-hot编码。one-hot编码广泛适用于分类数据,也称为分类编码。它的更详细介绍在6.1小节。在本例中,label的one-hot编码是将每个label映射到label索引位置为1的值。

1
2
3
4
5
6
7
8
9
def to_one_hot(labels, dimension=46):
results = np.zeros((len(labels), dimension))
for i, label in enumerate(labels):
results[i, label] = 1.
return results
#Vectorized training labels
one_hot_train_labels = to_one_hot(train_labels)
#Vectorized test labels
one_hot_test_labels = to_one_hot(test_labels)

上述label向量化的方式在Keras中有内建的函数实现,这在MNIST的例子中已经使用过。

1
2
from keras.utils.np_utils import to_categorical
one_hot_train_labels = to_categorical(train_labels) one_hot_test_labels = to_categorical(test_labels)
3.5.3 构建神经网络

这个主题分类问题和前一个影评分类类似:两类问题都是将短文本分类。但是这里有个新的限制:输出分类的数量由过去的2个变为46个。所以输出空间的维度更大。

使用一系列的Dense layer时,每个layer只能访问上一个layer的输出信息。如果某一个layer丢失一些与分类相关的信息时,接下来的layer不可能再恢复这些信息,所以每个layer都可能成为潜在的信息瓶颈。在前面的例子中,选用的16维中间layer,但是16维空间并不能学习到46个不同的分类。

考虑到上面的情况,这里使用更大的layer,隐藏单元设为64。

1
2
3
4
5
6
7
#Listing 3.15 Model definition
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'))

上述代码中的神经网络架构需要注意两个事情:

  • 最后一个Dense layer大小为46。这意味着每个输入样本,神经网络模型输出一个46维向量。其中每个项代表不同的分类;
  • 最后一个layer使用softmax激活函数。这意味着神经网络模型输出一个46维的概率分布。对于每个输入样本,模型将输出一个46维的输出向量,每个output[i]是样本属于类别 i 的概率,且46个分数之和为1。

对于本例最适合的损失函数是categorical_crossentropy。该函数度量两个概率分布的距离,意即,模型输出的概率分布与label真实分布之间的距离。为了最小化两个分布的距离,训练模型使得其输出更接近真实label。

1
2
3
4
#Listing 3.16 Compiling the model
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
3.5.4 验证模型

下面从训练数据中分出1000个样本作为验证集。

1
2
3
4
5
6
#Listing 3.17 Setting aside a validation set
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个epoch。

1
2
3
4
5
6
#Listing 3.18 Training the model
history = model.fit(partial_x_train,
partial_y_train,
epochs=20,
batch_size=512,
validation_data=(x_val, y_val))

最后,显示损失函数和准确度的曲线,见图3.9和3.10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#Listing 3.19 Plotting the training and validation loss
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#Listing 3.20 Plotting the training and validation accuracy
#Clears the figure
plt.clf()
acc = history.history['acc']
val_acc = history.history['val_acc']
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

image

图3.9 训练集和验证集的损失曲线

image

图3.10 训练集和验证集的准确度曲线

从上面的图可以看出,神经网络模型训练在第9个epoch开始过拟合。接着从头开始训练9个epoch,然后在测试集赏进行评估。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#Listing 3.21 Retraining a model from scratch
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)

下面是最终训练结果:

1
2
>>> results
[0.9565213431445807, 0.79697239536954589]

上面的方法达到约80%的准确度。在二分类问题中,纯随机分类的准确度是50%。而在本例中,纯随机分类的准确度将近19%,所以本例的模型结果还是不错的,至少超过随机基准线:

1
2
3
4
5
6
>>> 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个主题分类的概率分布。下面对所有的测试集生成主题预测。

1
2
#Listing 3.22 Generating predictions for new data
predictions = model.predict(x_test)

predictions的每项是一个长度为64的向量:

1
2
>>> predictions[0].shape
(46,)

这些向量的系数之和为1:

1
2
>>> np.sum(predictions[0])
1.0

下面从预测分类中找出概率最大的项:

1
2
>>> np.argmax(predictions[0])
4
3.5.6 处理label和loss的不同方法

前面提到过label编码的两外一种方法,将其转化为整数张量,比如:

1
2
y_train = np.array(train_labels)
y_test = np.array(test_labels)

上述处理label的方法唯一需要改变的是损失函数。在listing 3.21代码中使用的损失函数,categorical_crossentropy,期望label是一个分类编码。对于整数label,你应该选用sparse_categorical_crossentropy损失函数:

1
2
3
model.compile(optimizer='rmsprop',
loss='sparse_categorical_crossentropy',
metrics=['acc'])

上面这个新的损失函数在数学上是和categorical_crossentropy相同的,不同之处在于接口不同。

3.5.7 中间layer的重要性

前面提过,因为最后的输出是46维,你应该避免中间layer小于46个hidden unit。下面为你展示中间layer小于46维导致的信息瓶颈问题,以4个hidden unit为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
#Listing 3.23 A model with an information bottleneck
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%。这种情况主要是压缩许多信息到一个低维度的空间,导致没有足够多的信息可以恢复。

3.5.8 延伸实验
  • 尝试使用更大或者更小的layer:32个unit、128个unit等等;
  • 本例使用两个隐藏层。可以尝试一个或者三个隐藏层。
3.5.9 总结

从本例应该学习到的知识点:

  • 如果你想将数据分为N类,那神经网络模型最后一个Dense layer大小为N;
  • 在单标签、多分类的问题中,模型输出应该用softmax激活函数,输出N个分类的概率分布;
  • 分类交叉熵是分类问题合适的损失函数。它最小化模型输出的概率分布和真实label的概率分布之间的距离;
  • 处理多分类中label的两种方法:
    • 通过one-hot编码编码label,并使用categorical_crossentropy作为损失函数;
    • 通过整数张量编码label,并使用sparse_categorical_crossentropy损失函数
  • 对于数据分类的类别较多的情况,应该避免创建较小的中间layer,导致信息瓶颈。

未完待续。。。

Enjoy!

翻译本书系列的初衷是,觉得其中把深度学习讲解的通俗易懂。不光有实例,也包含作者多年实践对深度学习概念、原理的深度理解。最后说不重要的一点,François Chollet是Keras作者。
声明本资料仅供个人学习交流、研究,禁止用于其他目的。如果喜欢,请购买英文原版。


侠天,专注于大数据、机器学习和数学相关的内容,并有个人公众号:bigdata_ny分享相关技术文章。

若发现以上文章有任何不妥,请联系我。

image