中文评论情感分类——RNN模型

 本文旨在利用Tensorflow训练一个中文评论情感二分类的循环神经网络,由于分词处理是以字为最小单位的,所以该模型同时也是char-based NLP模型。研究表明,基于字的NLP模型的性能要比基于词的NLP模型好。原因有如下几点:

  1. 基于词模型的第一个任务就是对句子分词,不同分词工具的分词结果往往不同
  2. 词是由字组成的,所以词的范围要比字的范围广得多。正因如此,基于词产生的特征向量更为稀疏,且不在词汇表之内的词出现的概率更大,造成很大的噪声干扰。

准备文本训练集

ef613679c85a4b50a19b0ac6d0501377.png

训练集为一个文本文件,数字部分为影评的标签,1表示评论是积极的,0表示影评是消极的;文本内容是最原始的未经处理的形式,所以我们需要对其进行一定的处理。如此编排文本是由于,tf.keras.layers.TextVectorization层默认以空格为依据对文本进行分词的。毕竟tensorflow框架是美国公司开发的,英语句子中单词与单词之间便是有着天然的空格作为分隔的。

获取数据集


导入相关python包

import numpy as np
import tensorflow as tf
import pandas as pd
import re

提取文本训练集

旨在将文件中的样本提取出来,然后修改样本格式,并转化成tf.data.Dataset类型,因为tf.keras.layers.TextVectorization层在训练过程中只接受此类型数据。

  • 读取样本,并转化成numpy数组

numpy_train_data = np.array(pd.read_csv("D:\\training_sets\\sentiment_analysis\\comments\\train.txt",
                                            names=["label", "comment"],
                                            sep=","))
  • 打印并查看numpy样本集

print(numpy_train_data.shape)
    print(numpy_train_data)
(7766, 2)
[[1 '距离川沙公路较近,但是公交指示不对,如果是"蔡陆线"的话,会非常麻烦.建议用别的路线.房间较为简单.']
 [1 '商务大床房,房间很大,床有2M宽,整体感觉经济实惠不错!']
 [1 '早餐太差,无论去多少人,那边也不加食品的。酒店应该重视一下这个问题了。房间本身很好。']
 ...
 [0
  '看照片觉得还挺不错的,又是4星级的,但入住以后除了后悔没有别的,房间挺大但空空的,早餐是有但没有可以吃的东东,环境是好但天气太冷,总之我以后不会再住那里。']
 [0
  '我们去盐城的时候那里的最低气温只有4度,晚上冷得要死,居然还不开空调,投诉到酒店客房部,得到的答复是现在还没有领导指示需要开暖气,如果冷到话可以多给一床被子,太可怜了。。。']
 [0 '说实在的我很失望,之前看了其他人的点评后觉得还可以才去的,结果让我们大跌眼镜。我想这家酒店以后无论如何我都不会再去了。']]
  • 分离标签集和评论集

numpy_train_label, numpy_train_text = np.hsplit(numpy_train_data, 2)
  • 修改评论集格式

  1. 文本中的中文标点符号对情感的分析判断并无贡献,故要滤除掉。注:TextVerctorization层只会自动过滤掉英文标点符号,并不会滤除中文标点符号
  2. TextVerctorization层默认是根据空格进行分词的,而这里我们构建的是char-based模型,所以需要把每个文本中的字用空格隔开。
 """创建一个维度和文本集一样的numpy空数组,用于保存新格式的文本集
     dtype要设置为object"""
splited_numpy_text = np.empty(numpy_train_text.shape, dtype="object")
re.sub("\W*", "", str):滤除str字符串中的标点符号和特殊符号。 示例如下:
text = "年3.4后阿阿..,是,,是阿,,阿人??1231.2"
    print(re.sub("\W*", "", text))
年34后阿阿是是阿阿人12312

前提是你要导入re库

 for row in range(numpy_train_text.shape[0]):
      splited_numpy_text[row][0] = " ".join(re.sub("\W*", "", str(numpy_train_text[row][0])))

打印查看处理后的文本集

print(splited_numpy_text)
[['距 离 川 沙 公 路 较 近 但 是 公 交 指 示 不 对 如 果 是 蔡 陆 线 的 话 会 非 常 麻 烦 建 议 用 别 的 路 线 房 间 较 为 简 单']
 ['商 务 大 床 房 房 间 很 大 床 有 2 M 宽 整 体 感 觉 经 济 实 惠 不 错']
 ['早 餐 太 差 无 论 去 多 少 人 那 边 也 不 加 食 品 的 酒 店 应 该 重 视 一 下 这 个 问 题 了 房 间 本 身 很 好']
 ...
 ['看 照 片 觉 得 还 挺 不 错 的 又 是 4 星 级 的 但 入 住 以 后 除 了 后 悔 没 有 别 的 房 间 挺 大 但 空 空 的 早 餐 是 有 但 没 有 可 以 吃 的 东 东 环 境 是 好 但 天 气 太 冷 总 之 我 以 后 不 会 再 住 那 里']
 ['我 们 去 盐 城 的 时 候 那 里 的 最 低 气 温 只 有 4 度 晚 上 冷 得 要 死 居 然 还 不 开 空 调 投 诉 到 酒 店 客 房 部 得 到 的 答 复 是 现 在 还 没 有 领 导 指 示 需 要 开 暖 气 如 果 冷 到 话 可 以 多 给 一 床 被 子 太 可 怜 了']
 ['说 实 在 的 我 很 失 望 之 前 看 了 其 他 人 的 点 评 后 觉 得 还 可 以 才 去 的 结 果 让 我 们 大 跌 眼 镜 我 想 这 家 酒 店 以 后 无 论 如 何 我 都 不 会 再 去 了']]
  • 分别将标签numpy和影评numpy转化成tf.data.Dataset

"""先将numpy转化成tensor"""
tensor_train_text = tf.convert_to_tensor(splited_numpy_text)
tensor_train_label = tf.convert_to_tensor(numpy_train_label, tf.int64)

"""再将tensor转化成Dataset"""
dataset = tf.data.Dataset.from_tensor_slices((tensor_train_text, tensor_train_label))
  • 打印Dataset类型

<TensorSliceDataset element_spec=(TensorSpec(shape=(1,), dtype=tf.string, name=None), TensorSpec(shape=(1,), dtype=tf.int64, name=None))>

观察可知,该Dataset是含有两个tensor数组的元组,类型为

(inputs, targets),inputs为评论样本,作为输入;targets为标签值,作为目标值

构造神经网络

  • 初始化一个TextVerctorization

voctor_layers = tf.keras.layers.TextVectorization(
        output_sequence_length=72,
        output_mode="int"
    )

这只是初始化,所以TextVerctorization层中并没有一个词汇表,需要调用adapt()方法在文本集中学习一个词汇表

"""
    dataset是两个tensor数组的元组,格式为(inputs,targets)
    map(lambda x, y: x)的作用是分离出dataset里的inputs
    inputs在本次训练任务中指的是文本样本集
    """
voctor_layers.adapt(dataset.map(lambda x, y: x))

接着打印该词汇表的容量

print(len(voctor_layers.get_vocabulary()))
3561

发现文本集中出现的字有3561,然而这其中有一些字是出现较少的,这些出现较少的词语会给模型带来很多噪声。所以我们应该在初始化TextVerctorization层时,应该指定最大词汇量。指定后,adapt()方法会选取出现频率最多的那部分词语构成词汇表。在这里,指定最大词汇量为3000

 voctor_layers = tf.keras.layers.TextVectorization(
        max_tokens=3000,
        output_sequence_length=72,
        output_mode="int"
    )
    voctor_layers.adapt(dataset.map(lambda x, y: x))
    print(len(voctor_layers.get_vocabulary()))
3000

max_tokens:指定词汇表的最大词汇量

output_sequence_length:指定一段文本经过TextVerctorization层后,输出序列的长度

output_mode:指定一段文本在TextVerctorization层被序列化的方式,"int":直接输出文本中各个词语在词汇表中的索引

  • 构建完整的神经网络

model = tf.keras.Sequential([
        voctor_layers,
        tf.keras.layers.Embedding(
            input_dim=len(voctor_layers.get_vocabulary()),
            output_dim=64,
            # 使用mask来处理不同长度的文本
            mask_zero=True),
        tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)),
        tf.keras.layers.Dense(64, activation='relu'),
        tf.keras.layers.Dense(1),
        tf.keras.layers.Activation('sigmoid')
    ])

  • 编译神经网络

 """
   由于前面构造的神经网络在输出层加了sigmoid激活函数,所以from_logits设置为False。
   如果未加激活函数,则神经网络的输出值的取值范围为(-∞,+∞),from_logits应该
   设置为True,此时在训练过程中计算损失函数时,就会先将输出值经过sigmoid函数,再代入损失函数中
   计算损失值
  """
model.compile(
        loss=tf.keras.losses.BinaryCrossentropy(from_logits=False), optimizer="adam", metrics=['accuracy']
    )

由于该模型只有一个输出单元,是个二分类模型,所以损失函数选取的是二分类交叉熵函数BinaryCrossentropy

BinaryCrossentropy=      eq?%5Cfrac%7B1%7D%7BN%7D%5Csum_%7Bn%3D1%7D%5E%7BN%7D%5Cleft%20%28%20-ylog%5Chat%7By%7D%20-%281-y%29log%28%5Chat%7B1-y%7D%29%5Cright%20%29    y为目标值,eq?%5Chat%7By%7D为模型预测值

交叉熵是shannon信息论中一个重要概念,主要用于度量两个概率分布间的差异性信息。交叉熵是一个凹函数,交叉熵越小,两个概率分布就越相近,当y =  eq?%5Chat%7By%7D  时,交叉熵取得最小值,此时两个分布完全一样。

利用Adam梯度下降算法使得交叉熵在训练集上达到最小,便能训练出整个神经网络模型里的所有参数。

  • 提高训练集的性能

 AUTOTUNE = tf.data.AUTOTUNE
    train_ds = dataset.cache().prefetch(buffer_size=AUTOTUNE)
  • 配置训练函数

sentiment_model.fit(
        train_ds.batch(64),
        epochs=20
    )

调用batch()将训练集进行分批处理,每批包含64个样本。如此做可以加快训练速度。

epochs为训练周期

  • 开始训练

Epoch 1/20
313/313 [==============================] - 2s 5ms/step - loss: 0.6469 - accuracy: 0.7625
Epoch 2/20
313/313 [==============================] - 2s 6ms/step - loss: 0.6790 - accuracy: 0.6062
Epoch 3/20
313/313 [==============================] - 2s 5ms/step - loss: 0.5608 - accuracy: 0.7251
Epoch 4/20
313/313 [==============================] - 2s 5ms/step - loss: 0.4802 - accuracy: 0.7745
Epoch 5/20
313/313 [==============================] - 2s 5ms/step - loss: 0.3905 - accuracy: 0.8276
Epoch 6/20
313/313 [==============================] - 2s 5ms/step - loss: 0.3160 - accuracy: 0.8670
Epoch 7/20
313/313 [==============================] - 2s 5ms/step - loss: 0.2622 - accuracy: 0.8914
Epoch 8/20
313/313 [==============================] - 2s 5ms/step - loss: 0.2218 - accuracy: 0.9119
Epoch 9/20
313/313 [==============================] - 2s 5ms/step - loss: 0.1911 - accuracy: 0.9272
Epoch 10/20
313/313 [==============================] - 2s 5ms/step - loss: 0.1653 - accuracy: 0.9384
Epoch 11/20
313/313 [==============================] - 2s 5ms/step - loss: 0.1424 - accuracy: 0.9490
Epoch 12/20
313/313 [==============================] - 2s 5ms/step - loss: 0.1226 - accuracy: 0.9579
Epoch 13/20
313/313 [==============================] - 2s 5ms/step - loss: 0.1041 - accuracy: 0.9648
Epoch 14/20
313/313 [==============================] - 2s 5ms/step - loss: 0.0893 - accuracy: 0.9701
Epoch 15/20
313/313 [==============================] - 2s 5ms/step - loss: 0.0752 - accuracy: 0.9756
Epoch 16/20
313/313 [==============================] - 2s 5ms/step - loss: 0.0632 - accuracy: 0.9805
Epoch 17/20
313/313 [==============================] - 2s 5ms/step - loss: 0.0521 - accuracy: 0.9846
Epoch 18/20
313/313 [==============================] - 2s 5ms/step - loss: 0.0435 - accuracy: 0.9876
Epoch 19/20
313/313 [==============================] - 2s 5ms/step - loss: 0.0359 - accuracy: 0.9907
Epoch 20/20
313/313 [==============================] - 2s 5ms/step - loss: 0.0296 - accuracy: 0.9923
1/1 [==============================] - 0s 88ms/step

保存模型

创建一个tf.keras.callbacks.ModelCheckpoint回调函数就可以保存训练好的模型。

 """在当前目录下定义一个路径,用于保存模型数据文件"""
checkpoint_path = "model/sentiment_analysis"
"""创建一个回调函数,该函数可以保存模型在训练过程中的数据"""
    cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_path,
                                                     save_weights_only=True,
                                                     verbose=1)

    model.fit(
        train_ds.batch(64),
        epochs=30,
"""在训练函数中需要把回调参数传递进来"""
        callbacks=[cp_callback]
    )

从文件里加载模型

模型文件里保存的是模型每一层的结构和权重参数。只有相同结构的神经网络模型才能共享权重参数。所以在加载之前需要创建一个结构与模型文件保存的神经网络相同的模型。只不过这个模型只需要编译,而不需要再通过fit()来训练参数了,而是直接利用load_weights()将数据文件里的参数加载到编译好却没训练的模型上。

 model = tf.keras.Sequential([
        voctor_layers,
        tf.keras.layers.Embedding(
            input_dim=len(voctor_layers.get_vocabulary()),
            output_dim=64,
            # 使用mask来处理不同长度的文本
            mask_zero=True),
        tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)),
        tf.keras.layers.Dense(64, activation='relu'),
        tf.keras.layers.Dense(1),
        tf.keras.layers.Activation('sigmoid')
    ])
    model.compile(
        loss=tf.keras.losses.BinaryCrossentropy(from_logits=False), optimizer="adam", metrics=['accuracy']
    )
model.load_weights(checkpoint_path)

使用模型

先创建一个输入文本预处理方法,用以规范输入文本格式。

def split_text(raw_list_text):
    splited_list_text = []
    for text in raw_list_text:
        splited_list_text.append(" ".join(re.sub("\W*", "", text)))
    return splited_list_text
text = ["服务极差,有时候没空调!服务态度恶略!没空调问服务员要风扇,1小时过去了,都没来.投诉后才马上送来",
        "说实在的我很失望,之前看了其他人的点评后觉得还可以才去的,结果让我们大跌眼镜。我想这家酒店以后无论如何我都不会再去了",
        "我对这个酒店很满意"]
    print(split_text(text))
['服 务 极 差 有 时 候 没 空 调 服 务 态 度 恶 略 没 空 调 问 服 务 员 要 风 扇 1 小 时 过 去 了 都 没 来 投 诉 后 才 马 上 送 来', 
'说 实 在 的 我 很 失 望 之 前 看 了 其 他 人 的 点 评 后 觉 得 还 可 以 才 去 的 结 果 让 我 们 大 跌 眼 镜 我 想 这 家 酒 店 以 后 无 论 如 何 我 都 不 会 再 去 了', 
'我 对 这 个 酒 店 很 满 意']

利用predict()输出预测值,输出值越接近于1表明该文本是积极评论的概率就越大,越接近0则为消极评论的概率越大。

print(model.predict(split_text(text)))
[[1.8459625e-03]
 [1.5308799e-05]
 [9.5561552e-01]]

完整代码

numpy_train_data = np.array(pd.read_csv("D:\\training_sets\\sentiment_analysis\\comments\\train.txt",
                                            names=["label", "comment"],
                                            sep=","))
    numpy_train_label, numpy_train_text = np.hsplit(numpy_train_data, 2)
    splited_numpy_text = np.empty(numpy_train_text.shape, dtype="object")
    for row in range(numpy_train_text.shape[0]):
        splited_numpy_text[row][0] = " ".join(re.sub("\W*", "", str(numpy_train_text[row][0])))
    tensor_train_text = tf.convert_to_tensor(splited_numpy_text)
    tensor_train_label = tf.convert_to_tensor(numpy_train_label, tf.int64)
    dataset = tf.data.Dataset.from_tensor_slices((tensor_train_text, tensor_train_label))
    voctor_layers = tf.keras.layers.TextVectorization(
        max_tokens=3000,
        output_sequence_length=72,
        output_mode="int"
    )
    voctor_layers.adapt(dataset.map(lambda x, y: x))
    model = tf.keras.Sequential([
        voctor_layers,
        tf.keras.layers.Embedding(
            input_dim=len(voctor_layers.get_vocabulary()),
            output_dim=64,
            # 使用mask来处理不同长度的文本
            mask_zero=True),
        tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)),
        tf.keras.layers.Dense(64, activation='relu'),
        tf.keras.layers.Dense(1),
        tf.keras.layers.Activation('sigmoid')
    ])
    model.compile(
        loss=tf.keras.losses.BinaryCrossentropy(from_logits=False), optimizer="adam", metrics=['accuracy']
    )
    AUTOTUNE = tf.data.AUTOTUNE
    train_ds = dataset.cache().prefetch(buffer_size=AUTOTUNE)
    checkpoint_path = "model/sentiment_analysis"
    cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_path,
                                                     save_weights_only=True,
                                                     verbose=1)
    model.fit(
        train_ds.batch(64),
        epochs=30,
        callbacks=[cp_callback]
    )

模型原理阐述

WordEmbedding层与TextVectorization层通常结合在一起运用,作用示例为,

009acf4331ff4659a2df99f3386e7886.png

 


LSTM经常被用作RNN层里的神经元,原理如图所示,

972b27eb0ccc4a18b26a7f6180382b51.png

 

c<t-1>:上一个LSTM神经元输出的记忆向量

a<t-1>:上一个LSTM神经元输出的激活向量

c<t>:当前LSTM神经元输出到下一个神经节点的记忆向量,它是当前神经元获得的信息和当前神经元前面所有神经元获得的信息的综合考量。

c’<t>:当前LSTM神经元获得的新信息

a<t>:当前LSTM神经元输出到下一个神经节点的激活向量

x<t>:输入到当前神经节点的特征向量。

y<t>:当前神经元的输出值


Forget gate:利用a<t-1>和x<t>产生一个遗忘向量f<t>,该向量表征:在当前神经元,应该保留多少c<t-1>的信息。

Update gate:利用a<t-1>和x<t>产生一个更新向量u<t>,该向量表征:在当前神经元,应该保留多少c’<t>的信息。

Output gate:产生一个新的激活值a<t>,用来激活下一个LSTM神经元。

各个量之间的公式关系如下,

   57d23ee96b914cdc932fc218e64e6827.png

 9da87aed59e34465bc654ec7482e1749.png

将多个LSTM神经元级联一起,就构成了一条完整的RNN链。如下图所示,

731fcb36fdc248ecabf9b8422cce841d.png

这种机制很符合我们大脑处理序列问题时的思考过程——我们大脑里的某个神经元总是会被上一个神经元的思考结果所触发,从而引发新一轮的思考,这个新的思考往往会和过去产生联系。例如有如下一段英语句子,

This cartoon,I believe,intends to draw our attention to the negative effects.

句子里的intends是第三人称单数形式的,它是否需要加s是和前面的主语cartoon紧密相关的。也就是说,判断该句子里的intend是否需要加s时,我们需要提取前面节点cartoon的信息。对于类似于这种序列的处理分析,RNN能通过LSTM很好地将cartoon的信息传递给intend,从而做出判断。


在上述的基础上我们再来探讨一个中文实体标注的问题。给出如下这段话,

特朗普是美国总统。

现在需要判断特朗普这三个字是否是中文实体,通过分析我们很快判断特朗普是中文实体,因为我们是从后面的总统二字获取相关判断依据的——总统一般指的是人。再观察这个句子,判断特朗普是否是中文实体时,我们是通过其后面的字进行判断的,也就是说当前节点需要获取其后面节点的信息。而上述的RNN结构是向右的单向传播,其无法将后面节点的信息向前面传递。这时,本文将引进双向的RNN结构,

9a1245dbda124cda89c35eaa8c0a039f.png

两条RNN链重叠在一起,一条负责从左至右的信息传递,另一条负责从右至左的信息传递。每两个神经元组成一个节点,分别传递着前向的信息和后向的信息。在这种双向的结构下,每个输出都会与前后节点信息有关。


本文构建的模型,整体框架如下所示,

6595c9e4cf8f4496b42adf029964bbf3.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值