keras多分类问题: 新闻主题分类

所用环境为Colab.

定义任务目标

拿到一些新闻报道, 这些新闻报道都只属于某一个主题, 而不会同时属于多个主题. 把这些新闻报道归类到对应的主题下. 这是一个单标签多分类问题.

数据收集

使用 路透社 数据集.

1
2
3
# 加载数据集(需联网)
from tensorflow.keras.datasets import reuters
(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000) # num_words=10000, 仅保留数据中前10000个最常出现的单词

数据可视化

建立对数据形状的感受.

1
2
3
4
5
6
# 检查数据
train_data.shape, train_labels.shape, test_data.shape, test_labels.shape, train_data.dtype
# ((8982,), (8982,), (2246,), (2246,), dtype('O'))

type(train_data), type(train_data[0]), train_data.ndim
# (numpy.ndarray, list, 1)

看一下数据集里的数据到底长什么样子, 数据对应的标签是什么.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 查看第1条新闻的前10个单词, 以及它的标签(所属主题)

train_data[0][:10] # 新闻里的单词被转换为一个个数字.
# [1, 2, 2, 8, 43, 10, 447, 5, 25, 207]

train_labels[0]
# 3 该新闻属于第3个分类

# 最长的一条新闻的长度是多少
max([max(item) for item in train_data]) # 先得到每条新闻的长度, 再得到长度的最大值

数据集里的评论被编码为了数字, 解码成正常文本看看.

1
2
3
4
5
6
word_index = reuters.get_word_index() # word_index 是一个字典, 键是单词, 值是对应的一个整数.

reverse_word_index = dict([(value, key) for (key, value) in word_index.items()]) # 一个新字典, 键是一个整数, 值是该整数对应的单词

decoded_newswire = " ".join([reverse_word_index.get(i - 3, "?") for i in train_data[0]]) # 把训练集中的第一条新闻解码成正常文本. 索引减3是因为, 训练集里的单词对应的整数, 相比于字典, 都向右偏移了3.
decoded_newswire

解码出来的文本:

? ? ? said as a result of its december acquisition of space co it expects earnings per share in 1987 of 1 15 to 1 30 dlrs per share up from 70 cts in 1986 the company said pretax net should rise to nine to 10 mln dlrs from six mln dlrs in 1986 and rental operation revenues to 19 to 22 mln dlrs from 12 5 mln dlrs it said cash flow per share this year should be 2 50 to three dlrs reuter 3

数据标注

已标注好

数据清理

无需清理

选择模型评估方法

评估方法选择

这里选择精度(accuracy)作为模型评估的指标. 精度, 即正确分类的新闻条数所占比例.

怎样用? 构建好模型后, 在模型编译阶段, 将model.compile()metrics参数值设定为["accuracy"]

确定简单基准(模型应能超越这个基准)

如果为一条新闻随机指定分类, 能分类正确的概率理论上为$\frac{1}{46} \approx 2.17%$, 因此建立的模型的分类正确率应该超过该值.

对一批数据进行随机分类, 分类正确的比例是多少? 计算一下:

1
2
3
4
5
6
7
8
9
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)

hits_array.mean()
# 0.1731967943009795

把标签随机打乱, 和原来的标签进行比较, 得到分类正确的比率为17.32%, 因此, 建立的模型的正确率应超过该值.

数据预处理

数据向量化和规范化

数据向量化

对文本列表进行multi-hot编码, 将其转换为由0和1组成的向量. 把每条新闻都转换为一个10000维向量, 如果一个单词在该评论里出现, 就把该单词的索引(单词对应的那个整数)对应位置的元素设为1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import numpy as np

def vectorize_sequences(sequences, dimension=10000):
  results = np.zeros((len(sequences), dimension)) # 创建一个零矩阵
  for i, sequence in enumerate(sequences):
    for j in sequence:
      results[i, j] = 1.
  return results

x_train = vectorize_sequences(train_data) # 将训练数据向量化
x_test = vectorize_sequences(test_data) # 将测试数据向量化

标签向量化和规范化

使用one-hot编码(也叫 分类编码 categorical encoding)将标签向量化, 即将每个标签表示为维数为标签总类别数(标签一共有46个类别, 则向量的维数就是46)的向量. 该向量的值的特点: 标签索引对应的元素为1, 其余元素均设为0.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 方法1
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

y_train = to_one_hot(train_labels) # 将训练标签向量化
y_test = to_one_hot(test_labels) # 将测试标签向量化

# 方法2
from tensorflow.keras.utils import to_categorical
y_train = to_categorical(train_labels)
y_test = to_categorical(test_labels)

处理后

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
y_train = np.asarray(train_labels).astype("float32") # 将标签向量化
y_test = np.asarray(test_labels).astype("float32")

x_train.shape, x_test.shape, x_train.ndim
# ((8982, 10000), (2246, 10000), 2)

x_train[0] # 第1条评论现在变成了什么样子
# array([0., 1., 1., ..., 0., 0., 0.])

y_train.shape, y_test.shape
# ((8982,), (2246,))

处理缺失值

不需要处理缺失值.

数据划分: 训练集, 验证集, 测试集

从训练集中分出一部分作为验证集.

1
2
3
4
5
x_val = x_train[:1000]
partial_x_train = x_train[1000:]

y_val = y_train[:1000]
partial_y_train = y_train[1000:]

构建第一个模型

特征选择(过滤没有信息量的特征; 开发新特征)

在本例中特征就是新闻里编码后的单词了.

选择架构

两个中间层, 每层64个单元. 第三层输出预测值.

层1: Dense层, “表示空间"的维数units64, 激活函数activationrelu.

层2: Dense层, “表示空间"的维数units64, 激活函数activationrelu.

层3: Dense层, “表示空间"的维数units46, 激活函数activationsoftmax. 输出是一个数组, 数组元素为46个概率值(总和为1), 表示样本目标值等于各类别的可能性.

训练配置(损失函数, 批量大小, 学习率)

优化器 optimizer

这里选rmsprop

损失函数 loss function

这里选分类交叉熵损失函数categorical_crossentropy

训练轮数

这里训练20轮.

数据批量大小

批量大小设为512.

模型构建代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from tensorflow import keras
from tensorflow.keras import layers

# 构建模型
model = keras.Sequential([
  layers.Dense(64, activation="relu"),
  layers.Dense(64, activation="relu"),
  layers.Dense(46, activation="softmax")
])

# 编译模型
model.compile(optimizer="rmsprop", loss="categorical_crossentropy", metrics=["accuracy"])

拟合模型

1
history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val))

输出:

Epoch 1/20
16/16 [==============================] - 3s 94ms/step - loss: 2.7054 - accuracy: 0.5150 - val_loss: 1.8234 - val_accuracy: 0.6300
Epoch 2/20
16/16 [==============================] - 1s 58ms/step - loss: 1.5491 - accuracy: 0.6840 - val_loss: 1.4136 - val_accuracy: 0.7010
Epoch 3/20
16/16 [==============================] - 1s 59ms/step - loss: 1.2007 - accuracy: 0.7417 - val_loss: 1.2068 - val_accuracy: 0.7470
Epoch 4/20
16/16 [==============================] - 1s 58ms/step - loss: 0.9851 - accuracy: 0.7928 - val_loss: 1.1092 - val_accuracy: 0.7520
Epoch 5/20
16/16 [==============================] - 1s 58ms/step - loss: 0.8261 - accuracy: 0.8235 - val_loss: 1.0294 - val_accuracy: 0.7940
Epoch 6/20
16/16 [==============================] - 1s 59ms/step - loss: 0.6971 - accuracy: 0.8502 - val_loss: 0.9827 - val_accuracy: 0.7970
Epoch 7/20
16/16 [==============================] - 1s 59ms/step - loss: 0.5887 - accuracy: 0.8711 - val_loss: 0.9479 - val_accuracy: 0.8000
Epoch 8/20
16/16 [==============================] - 1s 79ms/step - loss: 0.4959 - accuracy: 0.8925 - val_loss: 0.9140 - val_accuracy: 0.8050
Epoch 9/20
16/16 [==============================] - 2s 100ms/step - loss: 0.4289 - accuracy: 0.9053 - val_loss: 0.8881 - val_accuracy: 0.8160
Epoch 10/20
16/16 [==============================] - 1s 77ms/step - loss: 0.3663 - accuracy: 0.9182 - val_loss: 0.8877 - val_accuracy: 0.8250
Epoch 11/20
16/16 [==============================] - 1s 55ms/step - loss: 0.3154 - accuracy: 0.9305 - val_loss: 0.8755 - val_accuracy: 0.8110
Epoch 12/20
16/16 [==============================] - 1s 56ms/step - loss: 0.2739 - accuracy: 0.9361 - val_loss: 0.8825 - val_accuracy: 0.8140
Epoch 13/20
16/16 [==============================] - 1s 60ms/step - loss: 0.2457 - accuracy: 0.9430 - val_loss: 0.9108 - val_accuracy: 0.8180
Epoch 14/20
16/16 [==============================] - 1s 58ms/step - loss: 0.2229 - accuracy: 0.9469 - val_loss: 0.8906 - val_accuracy: 0.8170
Epoch 15/20
16/16 [==============================] - 1s 54ms/step - loss: 0.1983 - accuracy: 0.9499 - val_loss: 0.9395 - val_accuracy: 0.8050
Epoch 16/20
16/16 [==============================] - 1s 54ms/step - loss: 0.1820 - accuracy: 0.9534 - val_loss: 0.9114 - val_accuracy: 0.8170
Epoch 17/20
16/16 [==============================] - 1s 55ms/step - loss: 0.1712 - accuracy: 0.9531 - val_loss: 0.9610 - val_accuracy: 0.8070
Epoch 18/20
16/16 [==============================] - 1s 53ms/step - loss: 0.1546 - accuracy: 0.9550 - val_loss: 0.9263 - val_accuracy: 0.8180
Epoch 19/20
16/16 [==============================] - 1s 65ms/step - loss: 0.1473 - accuracy: 0.9557 - val_loss: 0.9865 - val_accuracy: 0.7990
Epoch 20/20
16/16 [==============================] - 1s 52ms/step - loss: 0.1391 - accuracy: 0.9562 - val_loss: 0.9480 - val_accuracy: 0.8160

可以看到, 训练了20轮后, 虽然在训练集上的精度达到了0.9480, 但在验证集上, 精度却只有0.8160.

1
2
3
4
history_dict = history.history # 一个字典, 键是"指标"; 值是列表, 指标在每轮训练时的值.

history_dict.keys()
# dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy'])

可视化拟合结果

绘制训练损失和验证损失

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import matplotlib.pyplot as plt

history_dict = history.history

loss = history_dict["loss"]
val_loss = history_dict["val_loss"]

epochs = range(1, len(loss) + 1)

plt.plot(epochs, loss, "bo", label="Training loss") # "bo"表示蓝色圆点
plt.plot(epochs, val_loss, "b", label="Validation loss") # "b"表示蓝色实线

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
16
17
18
19
# import matplotlib.pyplot as plt
# history_dict = history.history

plt.clf() # 清空图像

acc = history_dict["accuracy"]
val_acc = history_dict["val_accuracy"]

# epochs = range(1, len(loss_values) + 1)

plt.plot(epochs, acc, "bo", label="Training acc") # "bo"表示蓝色圆点
plt.plot(epochs, val_acc, "b", label="Validation acc") # "b"表示蓝色实线

plt.title("Training and validation accuracy")
plt.xlabel("Epochs")
plt.ylabel("Accuracy")

plt.legend() # 用于为图表添加图例
plt.show()

从图中看出, 模型在训练到第9轮后, 开始出现过拟合的现象.

改进模型

让模型在训练9轮后停止

因为模型在训练到第9轮后就开始过拟合, 因此让模型训练9轮, 之后再次评估模型.

1
2
# 注意, 这次拟合时没有再从训练集中分出一部分做验证, 而是全部用来训练
history = model.fit(x_train, y_train, epochs=9, batch_size=512)

评价模型

前面选择精度(accuracy)作为模型评估的指标, 因此在对模型效果进行评价时, 使用模型在整个测试集上的平均精度.

1
2
3
test_loss, test_acc = model.evaluate(x_test, y_test)
print(f"测试精度: {test_acc}")
# 71/71 [==============================] - 0s 4ms/step - loss: 1.0796 - accuracy: 0.7916 测试精度: 0.7916295528411865

可以看到, 模型在测试集上的精度为0.7916, 相比于基准精度而言, 表明建立的模型确实是有效果的.

利用模型进行预测

拿到一条评论, 对它像在上面那样把它编码为一个由0和1组成的向量, 然后用model.predict()进行预测.

1
2
3
4
5
6
7
predictions = model.predict(x_test)
predictions.shape
# (2246, 46)

predic_result = [item.argmax() for item in predictions]

min(predic_result), max(predic_result)

部署模型

不部署.