来喝无糖汽水

基于CNN+GRU的文本分类实践

前情提要

最近打比赛用到了这个,记录一下,方便以后复用。下面是比赛的要求

赛题简介:介绍整个赛题的实现目标、实用价值、涉及技术和整体要求 新闻发展越来越快,每天各种各样的新闻令人目不暇接,对新闻进行科学的分类既能够方便不同的阅读群体根据需求快速选取自身感兴趣的新闻,也能够有效满足对海量的新闻素材提供科学的检索需求。
赛题业务场景:描述赛题相关的真实企业业务背景。从真实场景中,适当简化或者提炼出适合比赛的赛题场景 赛题以新闻数据为赛题数据,整合划分出如下候选分类类别:财经、房产、教育、科技、军事、汽车、体育、游戏、娱乐和其他共十类的新闻文本数据。选手根据新闻标题和内容,进行分类。输入为新闻的标题和正文内容,输出为新闻的分类。

说了这么多,实际上就是要解决一个文本十分类的问题,下面是题目中给出的数据集。如图所示,数据集被分成了很多sheet,每一张sheet对应了一类新闻,其中有三列,分别是新闻内容、新闻分类、新闻标题image-20210409191336160

但是这样的数据集我们还用不了,于是我进行了如下处理。将所有数据合并到同一张表下,再导出为以制表符分隔的txt文件image-20210409192112153

经过观察发现,数据存在许多空缺,如有的新闻只有标题没有内容,这些问题我将在后面的代码中解决。

image-20210409192230372

首先还是import相关模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sklearn.model_selection import train_test_split
import jieba
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from sklearn import preprocessing
from tensorflow.python.keras.utils.np_utils import to_categorical
import pandas as pd
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Embedding, Conv1D, MaxPooling1D, BatchNormalization, LeakyReLU, GRU
from tensorflow.keras.layers import Flatten, Dropout
import tensorflow as tf
import numpy as np
import os
import matplotlib.pyplot as plt
import keras.backend as K
from keras.callbacks import LearningRateScheduler

悲伤的是由于最近博主显卡出了,现在只能先拿cpu计算。

1
2
# 用cpu进行运算
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"

使用pandas读入数据,注释中有详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 若不存在处理后已保存的数据,则调用else后面的语句
if os.path.exists('processed_data.csv'):
# 读入数据,注意编码格式
dataset = pd.read_csv('processed_data.csv', sep='\t', names=['content', 'label', 'title'], encoding="utf-8").astype(str)
else:
dataset = pd.read_csv('data_tab.csv', sep='\t',
names=['content', 'label', 'title'], encoding="ANSI").astype(str)
# 这里方便送入网络,我直接舍弃了title,只用文章内容和其对应的标签
# 对content的空白内容进行补充(空白部分直接复制其上一行的内容)
dataset['content'] = dataset['content'].fillna(method='pad')
# 使用jieba分词
cw = lambda x: list(jieba.cut(x))
dataset['content'] = dataset['content'].apply(cw)
# 保存处理后的数据,方便下次直接使用
dataset.to_csv('processed_data.csv', index=False, header=False, sep='\t')

刚刚是对训练数据的读入和缺失内容处理,现在来把数据从中文字符转化为计算机可读的数字

1
2
3
# 先直接使用train_test_split操作以10:1的比例分割数据集
x_train, x_test, y_train, y_test = train_test_split(dataset['content'], dataset['label'],
test_size=0.1)

下面是先对标签进行处理

1
2
3
4
5
6
7
8
9
10
y_labels = list(y_train.value_counts().index)
# 这样得到的y_labels = ['财经', '游戏', '体育', '娱乐', '科技', '汽车', '教育', '房产', '军事']
le = preprocessing.LabelEncoder()
le.fit(y_labels)
# 打印独热码对应的标签顺序,方便后续分类
print(le.classes_)
num_labels = len(y_labels)
# 将标签转化为独热码的格式
y_train = to_categorical(y_train.map(lambda x: le.transform([x])[0]), num_labels)
y_test = to_categorical(y_test.map(lambda x: le.transform([x])[0]), num_labels)

再对训练的数据进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 将每个样本中的每个词转换为数字列表,使用每个词的编号进行编号
x_train_word_ids = tokenizer.texts_to_sequences(x_train)
x_test_word_ids = tokenizer.texts_to_sequences(x_test)
# ONE HOT(和下面的序列模式二选一即可)
# x_train = tokenizer.sequences_to_matrix(x_train_word_ids, mode='binary')
# x_test = tokenizer.sequences_to_matrix(x_test_word_ids, mode='binary')
# 序列模式
# 每条样本长度不唯一,将每条样本的长度设置一个固定值,将超过固定值的部分截掉,不足的在最前面用0填充
x_train_padded_seqs = pad_sequences(x_train_word_ids, maxlen=300)
x_test_padded_seqs = pad_sequences(x_test_word_ids, maxlen=300)
# 转化为np.array格式(好像没有必要?)
x_train_padded_seqs = np.array(x_train_padded_seqs)
y_train = np.array(y_train)
x_test_padded_seqs = np.array(x_test_padded_seqs)
y_test = np.array(y_test)

后面就是老套路啦,建立,模型。这次使用的是CNN+GRU串联的结构,其内部网络结构在这里不过多介绍。由于在训练时过拟合的情况出现的较为严重,于是Dropout的比例稍高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
model = Sequential()
#使用Embeeding层将每个词编码转换为词向量
model.add(Embedding(len(vocab) + 1, 600, input_length=300))
model.add(Conv1D(512, 7, padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Dropout(0.5))
model.add(Conv1D(256, 5, padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(MaxPooling1D(3, 3, padding='same'))
model.add(Dropout(0.5))
model.add(Conv1D(128, 5, padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Dropout(0.5))
model.add(Conv1D(64, 5, padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(MaxPooling1D(3, 3, padding='same'))
model.add(Dropout(0.5))
model.add(GRU(256, dropout=0.2, recurrent_dropout=0.1, return_sequences = True))
model.add(GRU(256, dropout=0.2, recurrent_dropout=0.1))
model.add(Dense(9, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer=tf.optimizers.Adam(0.0001),
metrics=['accuracy'])
if os.path.exists("model_save/model_checkpoints"):
print('--------------- restore model successfully------------------')
model = tf.keras.models.load_model("model_save/model_checkpoints")
# 学习率自动递减
def scheduler(epoch):
# 每隔100个epoch,学习率减小为原来的1/10
if epoch % 100 == 0 and epoch != 0:
lr = K.get_value(model.optimizer.lr)
K.set_value(model.optimizer.lr, lr * 0.1)
print("lr changed to {}".format(lr * 0.1))
return K.get_value(model.optimizer.lr)

reduce_lr = LearningRateScheduler(scheduler)
model.fit(train_x, train_y, batch_size=32, epochs=300, callbacks=[reduce_lr])

history = model.fit(x_train_padded_seqs, y_train, epochs=1,
batch_size=2048, validation_data=(x_test_padded_seqs, y_test), callbacks=[reduce_lr])
# acc loss可视化
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']

plt.subplot(1, 2, 1)
plt.plot(acc, label='Training Accuracy')
plt.plot(val_acc, label='Validation Accuracy')
plt.title('Training and Validation Accuracy')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(loss, label='Training Loss')
plt.plot(val_loss, label='Validation Loss')
plt.title('Training and Validation Loss')
plt.legend()
plt.show()
# 保存模型
model.save("model_save/model_checkpoints")

未完待续

但是目前该模型任存在一些问题(如十分要命的过拟合,以及数据集分布不均匀等问题),博主正在研究调试中。。


本文由 rufus 创作,采用 知识共享署名 4.0 国际许可协议。

本站文章除注明转载/出处外,均为本站原创或翻译,转载请务必署名。