基于LSTM的中文文本多分类实战

在我之前的博客中我们介绍了文本的多分类的方法,我们还尝试了各种分类模型,比如朴素贝叶斯、逻辑回归、支持向量机和随机森林等并且都取得了非常不错的效果。今天我们使用深度学习中的LSTM(Long Short-Term Memory)长短期记忆网络,来尝试一下中文文本多分类,LSTM它是一种时间循环神经网络,适合于处理和预测时间序列中间隔和延迟相对较长的重要事件。
LSTM 已经在科技领域有了多种应用。基于 LSTM 的系统可以学习翻译语言、控制机器人、图像分析、文档摘要、语音识别图像识别、手写识别、控制聊天机器人、预测疾病、点击率和股票、合成音乐等等任务。今天我们用它来实现一下文本多分类,相信会取得较好的效果。

数据

我们的数据来自于互联网,你可以在这里下载,数据中包含了10 个类别(书籍、平板、手机、水果、洗发水、热水器、蒙牛、衣服、计算机、酒店),共 6 万多条评论数据 首先查看一下我们的数据,这些数据都是来自于电商网站的用户评价数据,我们想要把不同评价数据分到不同的分类中去,且每条数据只能对应10个类中的一个类。

df = pd.read_csv('./data/online_shopping_10_cats.csv')
df=df[['cat','review']]
print("数据总量: %d ." % len(df))
df.sample(10)

我们的数据有两个字段,其中cat字段表示类别,review表示用户的评价信息,数据总量为62774,且评价内容全部为中文。接下来我们要清洗掉数据中的空值。

print("在 cat 列中总共有 %d 个空值." % df['cat'].isnull().sum())
print("在 review 列中总共有 %d 个空值." % df['review'].isnull().sum())
df[df.isnull().values==True]
df = df[pd.notnull(df['review'])]

还好只有一个空值,接下来我们统计一下各个类别的数据量

d = {'cat':df['cat'].value_counts().index, 'count': df['cat'].value_counts()}
df_cat = pd.DataFrame(data=d).reset_index(drop=True)
df_cat

我们看到各个类别的数据量不一致,洗发水、衣服、酒店、水果、平板的数据量最多各有1万条,而热水器的数据量最少只有574条,分布很不均匀,接下来我们用图形化的方式再查看一下各个类别的分布。

df_cat.plot(x='cat', y='count', kind='bar', legend=False,  figsize=(8, 5))
plt.title("类目分布")
plt.ylabel('数量', fontsize=18)
plt.xlabel('类目', fontsize=18)

数据预处理 

 接下来我们要将cat类转换成id,这样便于以后的分类模型的训练。

df['cat_id'] = df['cat'].factorize()[0]
cat_id_df = df[['cat', 'cat_id']].drop_duplicates().sort_values('cat_id').reset_index(drop=True)
cat_to_id = dict(cat_id_df.values)
id_to_cat = dict(cat_id_df[['cat_id', 'cat']].values)
df.sample(10)

我们将cat转换成了Id(0到9),由于我们的评价内容都是中文,所以要对中文进行一些预处理工作,这包括删除文本中的标点符号,特殊符号,还要删除一些无意义的常用词(stopword),因为这些词和符号对系统分析预测文本的内容没有任何帮助,反而会增加计算的复杂度和增加系统开销,所有在使用这些文本数据之前必须要将它们清理干净。

#定义删除除字母,数字,汉字以外的所有符号的函数
def remove_punctuation(line):
    line = str(line)
    if line.strip()=='':
        return ''
    rule = re.compile(u"[^a-zA-Z0-9\u4E00-\u9FA5]")
    line = rule.sub('',line)
    return line
 
def stopwordslist(filepath):  
    stopwords = [line.strip() for line in open(filepath, 'r', encoding='utf-8').readlines()]  
    return stopwords  

#加载停用词
stopwords = stopwordslist("./data/chineseStopWords.txt")

 中文停用词包含了很多日常使用频率很高的常用词,如 吧,吗,呢,啥等一些感叹词等,这些高频常用词无法反应出文本的主要意思,所以要被过滤掉。可以在这里下载中文停用词。

#删除除字母,数字,汉字以外的所有符号
df['clean_review'] = df['review'].apply(remove_punctuation)
df.sample(10)

 

我们过滤掉了review中的标点符号和一些特殊符号,并生成了一个新的字段 clean_review。接下来我们要在clean_review的基础上进行分词,把每个评论内容分成由空格隔开的一个一个单独的词语。

#分词,并过滤停用词
df['cut_review'] = df['clean_review'].apply(lambda x: " ".join([w for w in list(jb.cut(x)) if w not in stopwords]))
df.head()

LSTM建模 

数据预处理完成以后,接下来我们要开始进行LSTM的建模工作:

  • 我们要将cut_review数据进行向量化处理,我们要将每条cut_review转换成一个整数序列的向量
  • 设置最频繁使用的50000个词
  • 设置每条 cut_review最大的词语数为250个(超过的将会被截去,不足的将会被补0)
# 设置最频繁使用的50000个词
MAX_NB_WORDS = 50000
# 每条cut_review最大的长度
MAX_SEQUENCE_LENGTH = 250
# 设置Embeddingceng层的维度
EMBEDDING_DIM = 100

tokenizer = Tokenizer(num_words=MAX_NB_WORDS, filters='!"#$%&()*+,-./:;<=>?@[\]^_`{|}~', lower=True)
tokenizer.fit_on_texts(df['cut_review'].values)
word_index = tokenizer.word_index
print('共有 %s 个不相同的词语.' % len(word_index)

X = tokenizer.texts_to_sequences(df['cut_review'].values)
#填充X,让X的各个列的长度统一
X = pad_sequences(X, maxlen=MAX_SEQUENCE_LENGTH)

#多类标签的onehot展开
Y = pd.get_dummies(df['cat_id']).values

print(X.shape)
print(Y.shape)

下面我们要拆分训练集和测试集:

#拆分训练集和测试集
X_train, X_test, Y_train, Y_test = train_test_split(X,Y, test_size = 0.10, random_state = 42)
print(X_train.shape,Y_train.shape)
print(X_test.shape,Y_test.shape)

 

训练和测试的数据集都准备好以后,接下来我们要定义一个LSTM的序列模型:

  • 模型的第一次是嵌入层(Embedding),它使用长度为100的向量来表示每一个词语
  • SpatialDropout1D层在训练中每次更新时, 将输入单元的按比率随机设置为 0, 这有助于防止过拟合
  • LSTM层包含100个记忆单元
  • 输出层为包含10个分类的全连接层
  • 由于是多分类,所以激活函数设置为'softmax'
  • 由于是多分类, 所以损失函数为分类交叉熵categorical_crossentropy
#定义模型
model = Sequential()
model.add(Embedding(MAX_NB_WORDS, EMBEDDING_DIM, input_length=X.shape[1]))
model.add(SpatialDropout1D(0.2))
model.add(LSTM(100, dropout=0.2, recurrent_dropout=0.2))
model.add(Dense(10, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
print(model.summary())

 定义好LSTM模型以后,我们要开始训练数据:

  • 设置5个训练周期
  • batch_size为64
epochs = 5
batch_size = 64

history = model.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size,validation_split=0.1,
                    callbacks=[EarlyStopping(monitor='val_loss', patience=3, min_delta=0.0001)])

 下面我们画损失函数趋势图和准确率趋势图:

plt.title('Loss')
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='test')
plt.legend()
plt.show();

从上图中我们可以看见,随着训练周期的增加,模型在训练集中损失越来越小,这是典型的过拟合现象,而在测试集中, 损失随着训练周期的增加由一开始的从大逐步变小,再逐步变大。

plt.title('Accuracy')
plt.plot(history.history['acc'], label='train')
plt.plot(history.history['val_acc'], label='test')
plt.legend()
plt.show();

从上图中我们可以看见,随着训练周期的增加,模型在训练集中准确率越来越高,这是典型的过拟合现象,而在测试集中, 准确率随着训练周期的增加由一开始的从小逐步变大,再逐步变小。 

LSTM模型的评估 

接下来我们通过画混淆矩阵和求F1分数来评估我们模型的表现

import seaborn as sns
from sklearn.metrics import accuracy_score, confusion_matrix

y_pred = model.predict(X_test)
y_pred = y_pred.argmax(axis = 1)
Y_test = Y_test.argmax(axis = 1)

#生成混淆矩阵
conf_mat = confusion_matrix(Y_test, y_pred)
fig, ax = plt.subplots(figsize=(10,8))
sns.heatmap(conf_mat, annot=True, fmt='d',
            xticklabels=cat_id_df.cat.values, yticklabels=cat_id_df.cat.values)
plt.ylabel('实际结果',fontsize=18)
plt.xlabel('预测结果',fontsize=18)

混淆矩阵的主对角线表示预测正确的数量,除主对角线外其余都是预测错误的数量.从上面的混淆矩阵可以看出"蒙牛"类预测最准确,为100%。“平板”和“衣服”预测的错误数量教多。

 多分类模型一般不使用准确率(accuracy)来评估模型的质量,因为accuracy不能反应出每一个分类的准确性,因为当训练数据不平衡(有的类数据很多,有的类数据很少)时,accuracy不能反映出模型的实际预测精度,这时候我们就需要借助于F1分数、ROC等指标来评估模型。

下面我们将查看各个类的F1分数.

from  sklearn.metrics import classification_report
 
print('accuracy %s' % accuracy_score(y_pred, Y_test))
print(classification_report(Y_test, y_pred,target_names=cat_id_df['cat'].values))

 

从以上F1分数上看,"蒙牛"类的F1分数最大(100%),“热水器”类F1分数最差只有67%,究其原因可能是因为“热水器”分类的训练数据最少只有574条,使得模型学习的不够充分,导致预测失误较多吧。

 总体而言,LSTM模型比之前我们使用支持向量机(LinearSVC)模型的效果要好一些,准确率提高了差不多2个百分点。不过LSTM模型的训练时间要比支持向量机模型要长一些,究竟孰好孰坏,请读者们亲身尝试一下吧。

自定义预测

首先我们定义一个预测函数:通过输入一段评语,来预测它的类目

def predict(text):
    txt = remove_punctuation(text)
    txt = [" ".join([w for w in list(jb.cut(txt)) if w not in stopwords])]
    seq = tokenizer.texts_to_sequences(txt)
    padded = pad_sequences(seq, maxlen=MAX_SEQUENCE_LENGTH)
    pred = model.predict(padded)
    cat_id= pred.argmax(axis=1)[0]
    return cat_id_df[cat_id_df.cat_id==cat_id]['cat'].values[0]

您可以在这里下载完整代码

https://github.com/tongzm/ml-python/blob/master/%E5%9F%BA%E4%BA%8ELSTM%E7%9A%84%E4%B8%AD%E6%96%87%E6%96%87%E6%9C%AC%E5%A4%9A%E5%88%86%E7%B1%BB%E5%AE%9E%E6%88%98.ipynb

如果喜欢我的博客,请朋友们点赞+关注我的博客,下面是我的微信,如果想交流数据科学相关问题,请加微信。

猜你喜欢

转载自blog.csdn.net/weixin_42608414/article/details/89856566
今日推荐