使用神经网络完成新闻分类
(1)问题描述:
该数据集用于文本分类,包括大约20000个左右的新闻文档,均匀分为20个不同主题的新闻组集合,其中:
**训练集:**包括11314个新闻文档及其主题分类标签。训练数据在文件train目录下,训练新闻文档在train_texts.dat文件中,训练新闻文档标签在train_labels.txt文档中,编号为0~19,表示该文档分属的主题标号。
**测试集:**包括7532个新闻文档,标签并未给出。测试集文件在test目录下,测试集新闻文档在test_texts.dat文件中。
(2)模型原理:
LSTM
长短期记忆网络(LSTM,Long Short-Term Memory)是一种时间循环神经网络,是为了解决一般的RNN(循环神经网络)存在的长期依赖问题而专门设计出来的,所有的RNN都具有一种重复神经网络模块的链式形式。在标准RNN中,这个重复的结构模块只有一个非常简单的结构,例如一个tanh层。
参考:Tensorflow实战:LSTM原理及实现(详解)_m0_37917271的博客-CSDN博客
做以下简要总结:
如下图所示:
从图中可以看到,LSTM 提出了三个门(gate)的概念:input gate,forget gate,output gate。其实可以这样来理解,input gate 决定了对输入的数据做哪些处理,forget gate 决定了哪些知识被过滤掉,无需再继续传递,而 output gate 决定了哪些知识需要传递到下一个时间序列。
计算依据下列公式:
其中:
i t 是处理 input 的 input gate, 外面一个 sigmoid 函数处理,其中的输入是当前输入 x 和前一个 时间状态的输出 h ( t − 1 )。 所有的 b bb 都是偏置项。
f t 则是 forget gate 的操作,同样对当前输入 x t 和前一个状态的输出 h ( t − 1 )进行处理。
g t 也是 input gate 中的操作, 同样的输入,只是外面换成了 tanh函数。
o t是前面图中 output gate 中左下角的操作,操作方式和前面 i t, 以及 f t一样。
c t则是输出之一, 对 forget gate 的输出, input gate 的输出进行相加,然后作为当前时间序列的 一个隐状态向下传递。
h t同样是输出之一,对 前面的 c t 做一个 t a n h \mathrm{tanh}tanh 操作,然后和前面得到的 o t 进行相乘, h t 既向 下一个状态传递,也作为当前状态的输出。
(3)实现过程:
1.读入数据集:
读取文件train_texts.dat和test_texts.dat方式如下,以train_texts.dat为例,test_texts.dat读取方式相同;标签文件为正常txt文件,读取方式按照读取txt文件即可。
# 读入数据 file_name = 'train/train_texts.dat' with open(file_name, 'rb') as f: train_texts = pickle.load(f) file_name = 'test/test_texts.dat' with open(file_name, 'rb') as f: test_texts = pickle.load(f) train_labals = [] fl = open('train/train_labels.txt') for line in fl.readlines(): train_labals.append(line)
2.特征提取:
因为每篇新闻都是由英文字符表示而成,因此需要首先提取每篇文档的特征,把每篇文档抽取为特征向量,这里我们选择提取文档的TF-IDF特征,即词频(TF)-逆文本频率(IDF)。
提取文档的TF-IDF特征可以通过sklearn. feature_extraction.text中的TfidfVectorizer来完成,具体实现代码如下:
# TFIDF向量化 vectorizer = TfidfVectorizer(max_features=10000) train_vector = vectorizer.fit_transform(train_texts) print(train_vector.shape) test_vector = vectorizer.transform(test_texts) print(test_vector.shape)
3.标签向量化:
将标签向量化有两种方法:可以将标签列表转换为整数张量,或者使用one-hot 编码。
one-hot 编码是分类数据广泛使用的一种格式,也叫分类编码(categorical encoding)。这里我们采用该种方法,标签的one-hot编码就是将每个标签表示为全零向量, 只有标签索引对应的元素为 1。代码实现如下:
from keras.utils import to_categorical one_hot_train_labels = to_categorical(train_labals)
4.拆分训练集和测试集:
测试集比例选择为0.2:
#拆分测试集与训练集 X_train, X_test, y_train, y_test = train_test_split(train_vector, one_hot_train_labels, test_size=0.2, random_state=0) x_test = X_test.toarray() partial_x_train = X_train.toarray()
5.构建网络:
这里先尝试最简单的3个全连接层的网络。
输出类别的数量为20个。输出空间的维度较大。 对于用过的 Dense 层的堆叠,每层只能访问上一层输出的信息。如果某一层丢失了与 分类问题相关的一些信息,那么这些信息无法被后面的层找回,也就是说,每一层都可能成为信息瓶颈。因此,设计网络如下所示:
# 定义Sequential类 model = models.Sequential() # 全连接层,128个节点 model.add(layers.Dense(128, activation='relu', input_shape=(10000,))) # 全连接层,64个节点 model.add(layers.Dense(64, activation='relu')) # 全连接层,得到输出 model.add(layers.Dense(20, activation='softmax')) # loss model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
网络的最后一层是大小为20 的 Dense 层。这意味着,对于每个输入样本,网络都会输出一个 20 维向量。这个向量的每个元素(即每个维度)代表不同的输出类别。
我们选择的损失函数是 categorical_crossentropy(分类交叉熵)。它用于衡量两个概率分布之间的距离,这里两个概率分布分别是网络输出的概率分布和标签的真实分 布。通过将这两个分布的距离最小化,训练网络可使输出结果尽可能接近真实标签。
6.验证:
现在开始训练网络,共 20 个轮次。
history = model.fit(partial_x_train, y_train, epochs=20, batch_size=512, validation_data=(x_test, y_test))
截取第20次的结果:
Epoch 20/20 512/9051 [>.............................] - ETA: 0s - loss: 0.0032 - accuracy: 0.9980 1536/9051 [====>.........................] - ETA: 0s - loss: 0.0022 - accuracy: 0.9993 2560/9051 [=======>......................] - ETA: 0s - loss: 0.0028 - accuracy: 0.9992 3072/9051 [=========>....................] - ETA: 0s - loss: 0.0025 - accuracy: 0.9993 4096/9051 [============>.................] - ETA: 0s - loss: 0.0022 - accuracy: 0.9995 5120/9051 [===============>..............] - ETA: 0s - loss: 0.0024 - accuracy: 0.9992 6656/9051 [=====================>........] - ETA: 0s - loss: 0.0023 - accuracy: 0.9994 7680/9051 [========================>.....] - ETA: 0s - loss: 0.0021 - accuracy: 0.9995 8704/9051 [===========================>..] - ETA: 0s - loss: 0.0021 - accuracy: 0.9995 9051/9051 [==============================] - 1s 79us/sample - loss: 0.0022 - accuracy: 0.9994 - val_loss: 0.3894 - val_accuracy: 0.8975
7.loss曲线:
绘制loss曲线:
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()
8.精度曲线:
绘制acc曲线:
plt.clf() acc = history.history['accuracy'] val_acc = history.history['val_accuracy'] 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('Accuracy') plt.legend() plt.show()
9.重新训练:
分析可知,网络在训练 10 轮左右开始出现过拟合趋势。
因此,我们重新训练一个新网络,共 10 个轮次,然后在测试集上评估模型。
history = model.fit(partial_x_train, y_train, epochs=10, batch_size=512, validation_data=(x_test, y_test)) results = model.evaluate(x_test, y_test)
10.结果输出:
将结果写入txt:
x_input = test_vector.toarray() predictions = model.predict(x_input) out_put = np.argmax(predictions,axis=1) np.savetxt('result.txt', out_put, fmt='%d', delimiter='\n')
(4)实验小结:
1.如果要对 N 个类别的数据点进行分类,网络的最后一层应该是大小为 N 的 Dense 层。对于单标签、多分类问题,网络的最后一层应该使用 softmax 激活,这样可以输出在 N个输出类别上的概率分布。
2.损失函数使用分类交叉熵,它可以将网络输出的概率分布与目标的真实分布之间的距离最小化。
3.处理多分类问题的标签有比较经典的有两种方法。
通过分类编码(也叫 one-hot 编码)对标签进行编码,然后使用 categorical_crossentropy 作为损失函数。
标签编码为整数,然后使用 sparse_categorical_crossentropy 损失函数。