基于Tensorflow2.0和LSTM的文本多分类实战

之前我给大家介绍过一篇使用keras和LSTM来进行文本多分类的博客(https://blog.csdn.net/weixin_42608414/article/details/89856566) ,今天我将在google提供的Colab notebook上使用tensorflow2.0来是实现以下中文文本多分类的实战应用。选择Colab notebook是因为在Colab notebook中google提供了免费的GPU资源供你使用,这样可以大大节省模型的训练时间。

Google Colab,全名Colaboratory,是由谷歌提供的免费的云平台,可以使用keras、tensorflow等框架进行深度学习。最近Colab平台已经将K80 GPU更换成Tesla T4 GPU,提供了更强的算力。对于刚入门机器学习或深度学习的用户,这个平台是不二之选。

要使用Google Colab必须要在科学上网的环境下,同时必须要有一个gmail账号,还没有gmail账号的同学请赶快注册一个吧。

如何使用colab请看这篇博客(https://www.cnblogs.com/lfri/p/10471852.html

数据

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

import tensorflow as tf
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
print(tf.__version__)

下面我们要在colab notebook中挂载我们的谷歌云端硬盘,只有当挂载好云端硬盘才能在cloab notebook中读取云端硬盘中的数据文件。

#加载google的云端硬盘
from google.colab import drive
drive.mount('/content/drive')

执行了上述脚本命令后,系统会生成一个链接,点击这个链接后谷歌会让你登陆gmail账号,登陆好以后会生成一个加密字符串,复制一下这个加密字符串,并将它黏贴到colab notebook中就实现了挂载云端硬盘,这时就可以读取云端硬盘中的数据文件了,不过前提是你先要将数据文件(csv,xls)上传到云端硬盘中。

df = pd.read_csv('./drive/My Drive/data/online_shopping_10_cats.csv')
print("数据总量: %d ." % len(df))
df.sample(10)

 我们看一下总共有多少个类目:

df[["cat"]].drop_duplicates().reset_index(drop=True)

下面我们看一下各个类目的数据量以及占比

print(df.cat.value_counts())
print()
print(df.cat.value_counts()/len(df));

 

 数据预处理 

下面我们要对评语进行一些预处理工作,这包括删除文本中的标点符号,特殊符号,还要删除一些无意义的常用词(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("./drive/My Drive/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()

建模

 首先我们要数据集中拆分出训练集和验证集,我们将80%的数据作为训练集,20%的数据作为验证集,我们通过分层抽样的方式来随机抽样,注意在这里我们使用了sklearn的train_test_split的方法来抽样,并使用了stratify参数来保证抽样后,训练集和验证集中的各个类目的占比与原数据集保持一致。

#拆分训练集和测试集
X_train, X_validation, Y_train, Y_validation = train_test_split(df[['cut_review']],df[["cat"]], test_size = 0.2,stratify=df.cat)
print(len(X_train))
print(len(X_validation))

Y_train.cat.value_counts()/len(Y_train)

Y_validation.cat.value_counts()/len(Y_validation)

下面我们来设置模型的超参数:

  • vocab_size : 设置最频繁使用的5000个词
  • embedding_dim:设置嵌入层的维度 
  • max_length :设置每条 cut_review最大的词语数为150个(超过的将会被截去,不足的将会自动填充0)
  • trunc_type :如果评语的词语数量超过150个词,将从评语的尾部拦截掉多余的词语
  • padding_type:如果评语的词语数量少于150个词,将从评语的尾部填充0,并补足150个词语的长度
  • oov_tok:凡是遇到那些没有见过的词(最频繁的5000个词之外的词语),我们会用特殊符号来如“<OOV>”来替代。

这里要说明一下我们将

vocab_size = 5000
embedding_dim = 64
max_length = 150
trunc_type = 'post'
padding_type = 'post'
oov_tok = '<OOV>'

下面我们要对评语进行向量化处理,首先我们要在训练集的基础上生成一个tokenizer模型,该tokenizer模型保护了一个词汇表,训练集中的所有词语都会出现在这个词汇表中:

tokenizer = Tokenizer(num_words = vocab_size, oov_token=oov_tok)
tokenizer.fit_on_texts(X_train.cut_review.values)
word_index = tokenizer.word_index
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])

下面我们查看这个词汇表的长度和前10个词汇,每一个词汇对应一个编号,词汇表中的第一个词语被定义为特征符号“<OOV>”

print(len(list(word_index.items())))
print()
list(word_index.items())[:10]

词汇表中的总词语数为6251,前10个最常用的词是:<oov>,买,不错,酒店,京东,房间,说,一个,非常。需要注意的是词汇表的第一个词被定义为特殊符号<oov>,它的索引编号是1,在后面的对训练集和验证集进行量化的时候,凡是那些没见过的词(5000个常用词之外的词)将会被标记为1.

下面我们使用tokenizer模型来量化训练集和验证集:

X_train_sequences = tokenizer.texts_to_sequences(X_train.cut_review.values)
X_train_padded = pad_sequences(X_train_sequences, maxlen=max_length, padding=padding_type, truncating=trunc_type)

X_validation_sequences = tokenizer.texts_to_sequences(X_validation.cut_review.values)
X_validation_padded = pad_sequences(X_validation_sequences, maxlen=max_length, padding=padding_type, truncating=trunc_type)

由于我们的预测模型的输入向量要求每条评语必须要有相同的长度,所以我们会对量化好的评语做pad处理,也就是自动填充或者拦截每条评语(当评语的长度不到150时进行填充,当超过150时进行拦截),使其达到规定的长度,这里我们打印第10条评语,已经量化好的第10条评语

print(X_train.cut_review.values[10])
print()
X_train_padded[10]

第10条评语一共有23个词语,这23个词语在对应用的量化结果总是一个包含150个元素的数组,该数组的前23个数字对于与词汇表中的相应词语的编号,也就是说104对应的是词汇表中第104个词语,以此类推. 由于第10条评语的长度不到150,所以我们要在评语的尾部补0, 请注意第9个词语"贴身" 被量化成了编号1,这说明“贴身”这个词语在词汇表中的编号已经超出了我们之前定义的最频繁使用的5000个词,tokenizer会自动将超出范围的词语的编号会被强制置为1.

print("104 ->",reverse_word_index[104])
print("3 ->",reverse_word_index[3])
print("120 ->",reverse_word_index[120])
print()
print("贴身 ->",dict(list(word_index.items()))["贴身"])
print("1 ->",reverse_word_index[1])

下面我们对标签做通用的量化处理:

label_tokenizer = Tokenizer()
label_tokenizer.fit_on_texts(df.cat.values)

Y_training_cat_seq = np.array(label_tokenizer.texts_to_sequences(Y_train.cat.values))
Y_validation_cat_seq = np.array(label_tokenizer.texts_to_sequences(Y_validation.cat.values))

label_word_index = label_tokenizer.word_index

print(dict(list(label_word_index.items())))
print(Y_training_cat_seq.shape)
print()
print(Y_training_cat_seq[0])
print(Y_training_cat_seq[1])
print(Y_training_cat_seq[2])

这里需要注意的是我们并没有将标签进行one-hot处理, 我们仅仅只是对10中类目标签进行了简单的编号处理,编号是从1-10,这里的编号是从1开始,不是从0开始!!

下面我们开始创建LSTM模型:

  • 我们创建一个tf.keras.Sequential模型
  • 嵌入层每个单词存储一个向量。调用时,它将单词索引序列转换为向量序列。经过训练后,具有相似含义的单词通常具有相似的向量。
  • 双向包装器与LSTM层一起使用,它通过LSTM层向前和向后传播输入,然后连接输出。这有助于LSTM学习长期依赖性。然后,我们将其拟合到Dense的神经网络中进行分类。
  • 我们使用的激活函数relu。
  • 由于我们要做的是多分类,所以我们最后使用softmax作为最后的激活函数.
model = tf.keras.Sequential([ 
    tf.keras.layers.Embedding(vocab_size, embedding_dim),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(embedding_dim)),
    tf.keras.layers.Dense(embedding_dim, activation='relu'),
    tf.keras.layers.Dense(11, activation='softmax')
])
model.summary()

这里要特别说明的是在模型的最后的Dense层,分类数量我们填的是11,然而我们的全部类目只有10个,此处填11个是因为模型的分类结果是以编号0开始的,而我们在训练模型时喂给模型的标签数据的编码是从1开始的,这样就无法形成对应关系,这里就有两种解决方法:

1. 虽然模型的最后Dense层分类数量设为11但我们只取第1-10的结果,舍去第0个结果。

2.在训练之前我们需要将标签的每个值(Y_training_cat_seq)减1,这样保证是训练集的标签范围在0-9,然后设置模型的最后Dense层分类数量设为10。

这里我们采用第1种方法只取分类结果的第1-10的结果,舍去第0个结果。

下面我们设置模型的损失函数和梯度优化方法:

model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

 此处我们设置的损失函数设置为:sparse_categorical_crossentropy而不是常用的categorical_crossentropy,sparse_categorical_crossentropy和categorical_crossentropy的区别主要在于我们的标签的表现形式,如果标签是用one-hot形式来表示,那么我们损失函数需要设置为categorical_crossentropy,而当标签是以非one-hot形式来表示,就像我们这里用每个标签的编号来表示标签的类别时,我们需要将损失函数设置为sparse_categorical_crossentropy。

接下来万事具备,我们开始训练模型:

num_epochs = 10
history = model.fit(X_train_padded, 
                    Y_training_cat_seq, 
                    epochs=num_epochs, 
                    validation_data=(X_validation_padded, Y_validation_cat_seq), 
                    verbose=2)

从模型的训练结果中我们发现,每个epoch耗时要4分多钟,训练集的准确率一直在增加, 这是一种过拟合的表现,训练结果没有收敛。下面用可视化的方法来看一下模型的训练结果

def plot_graphs(history, string):    
    plt.plot(history.history[string])
    plt.plot(history.history['val_'+string])
    plt.xlabel("Epochs")
    plt.ylabel(string)
    plt.legend([string, 'val_'+string])
    plt.show()

plot_graphs(history, "accuracy")
plot_graphs(history, "loss")

从上图中我们也发现,随着训练周期的增加,训练集的准确率也一直在增加,而验证集准确率很相对较为平坦,但也有下降的趋势,与之相对应的在在训练损失方面,训练集的损失也随着训练周期的增加而减少,但验证集的损失却明显上升。所以模型训练存在过拟合。

模型评估

import seaborn as sns
from sklearn.metrics import accuracy_score, confusion_matrix
 
y_pred = model.predict(X_validation_padded)
y_pred = y_pred.argmax(axis = 1)

labels=[1,2,3,4,5,6,7,8,9,10] 
conf_mat = confusion_matrix(Y_validation_cat_seq, y_pred)
fig, ax = plt.subplots(figsize=(10,8))
sns.heatmap(conf_mat, annot=True, fmt='d',xticklabels=labels, yticklabels=labels)
plt.ylabel('actual results',fontsize=18);
plt.xlabel('predict result',fontsize=18);

from  sklearn.metrics import classification_report 
print('accuracy %s' % accuracy_score(y_pred, Y_validation_cat_seq))
print(classification_report(Y_validation_cat_seq, y_pred,target_names=[str(w) for w in labels]))

 

总结

今天我们完成了tensorflow2.0+LSTM的文本多分类的预测模型开发,模型的预测表现尚可,但模型的训练结果存在过拟合,这可以通过调节超参数来逐步完善,有兴趣的朋友可以试着调节超参数来是让模型收敛,日后我会通过调节超参来改善这个模型。

参考资料:

https://www.coursera.org/learn/natural-language-processing-tensorflow/home/welcome

完整代码可以在此下载

https://github.com/tongzm/ml-python/blob/master/%E4%BD%BF%E7%94%A8tensorflow2_0%E5%92%8CLSTM%E7%9A%84%E6%96%87%E6%9C%AC%E5%A4%9A%E5%88%86%E7%B1%BB2.ipynb

猜你喜欢

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