本文旨在利用Tensorflow训练一个中文影评二分类神经网络,由于分词处理是以词为最小单位的,所以该模型同时也是word-based NLP模型。
准备文本训练集
训练集为一个文本文件,数字部分为影评的标签,1表示影评是消极的,0表示影评是积极的;文本部分是经过分词处理过的,词与词之间是以空格分离的。如此编排文本是由于,tf.keras.layers.TextVectorization层默认以空格为依据对文本进行分词的。毕竟tensorflow框架是美国公司开发的,英语句子中单词与单词之间便是有着天然的空格作为分隔的。
导入相关python包
import numpy as np
import tensorflow as tf
import pandas as pd
提取文本训练集
旨在将文件中的样本提取出来,并转化成tf.data.Dataset类型,因为tf.keras.layers.TextVectorization层在训练过程中只接受此类型数据
-
读取样本,并转化成numpy数组
numpy_data = np.array(pd.read_csv("D:\\training_sets\\sentiment_analysis\\Dataset\\train.txt",names=["label", "comment"],sep="\t"))
-
打印并查看numpy样本集
print(numpy_data.shape)
print(numpy_data)
(19998, 2)
[[1'死囚 爱 刽子手 女贼 爱 衙役 我们 爱 你们 难道 还有 别的 选择 没想到 胡军 除了 蓝宇 还有 东宫 西宫 我 个 去 阿兰 这样 真 他 nia 恶心 爱个 P 分明 只是 欲'][1'其实 我 对 锦衣卫 爱情 很萌 因为 很 言情小说 可惜 女主角 我要 不是 这样 被 乔花 去 偷 令牌 青龙 吃醋 想出 箭 那里 萌到 让 我 想起 雏菊 里 郑 大叔 徐子珊 吴尊 真是 可怕 他们 完全 不是 电影 料 还有 脱脱 这个 名字 想要 雷死 观众 导演 到底 想 什么 剧情 混乱 老套 无趣 对白 更是 白痴 失望'][1'两星 半 小 明星 本色 出演 老 演员 自己 发挥 基本上 王力宏 表演 指导 上 没有 什么 可 发挥 地方 整体 结构 失衡 情节 讨巧 也 一众 观众 喜欢 原因 使得 这部 电影 王力宏 演唱会 一样 不停 处女作 却 体现 不 出 王力宏 个人 风格 不能 说 成功 相对来说 周董 处女作 更 像 电影 一些']...[0'看 之前 豆瓣 上 看到 一个 评论 说 要 找到 自己 看 这部 电影 哭泣 原因 我 想 真主 创造 感情 其中 一部分 就是 人 与 动物 之间 感情 失落 又 得到 所以 哭泣 有句 话 很棒 每 只 狗 一个 八公'][0'假如 影片 大前提 逻辑 完全 成立 那么 影片 前后 呼应 节奏 情节 主题 简直 完美 但 明白 导演 到底 想 说 什么 之后 片子 才 意义 所以 即使 你 对 大 逻辑 觉得 很 扯 还是 要 先 去 相信 然后 进入 故事 再用 很 严谨 逻辑 去 分析 本身 很 神奇'][0'一种 浪漫 能 让 美女 感动 两种 浪漫 却 能 让 美女 不知所措 房子 车子 足以 让 美女 嫁给 你 可是 当 另一边 却 突然 杀出 钻戒 更 要命 还有 一座 大厦 于是 美女 清醒']]
-
分离标签和影评
numpy_train_label, numpy_train_text = np.hsplit(numpy_data, 2)
-
分别打印并查看标签和影评
print(numpy_train_label)print(numpy_train_text)
[[1][1][1]...[0][0][0]]
[['死囚 爱 刽子手 女贼 爱 衙役 我们 爱 你们 难道 还有 别的 选择 没想到 胡军 除了 蓝宇 还有 东宫 西宫 我 个 去 阿兰 这样 真 他 nia 恶心 爱个 P 分明 只是 欲']['其实 我 对 锦衣卫 爱情 很萌 因为 很 言情小说 可惜 女主角 我要 不是 这样 被 乔花 去 偷 令牌 青龙 吃醋 想出 箭 那里 萌到 让 我 想起 雏菊 里 郑 大叔 徐子珊 吴尊 真是 可怕 他们 完全 不是 电影 料 还有 脱脱 这个 名字 想要 雷死 观众 导演 到底 想 什么 剧情 混乱 老套 无趣 对白 更是 白痴 失望']['两星 半 小 明星 本色 出演 老 演员 自己 发挥 基本上 王力宏 表演 指导 上 没有 什么 可 发挥 地方 整体 结构 失衡 情节 讨巧 也 一众 观众 喜欢 原因 使得 这部 电影 王力宏 演唱会 一样 不停 处女作 却 体现 不 出 王力宏 个人 风格 不能 说 成功 相对来说 周董 处女作 更 像 电影 一些']...['看 之前 豆瓣 上 看到 一个 评论 说 要 找到 自己 看 这部 电影 哭泣 原因 我 想 真主 创造 感情 其中 一部分 就是 人 与 动物 之间 感情 失落 又 得到 所以 哭泣 有句 话 很棒 每 只 狗 一个 八公']['假如 影片 大前提 逻辑 完全 成立 那么 影片 前后 呼应 节奏 情节 主题 简直 完美 但 明白 导演 到底 想 说 什么 之后 片子 才 意义 所以 即使 你 对 大 逻辑 觉得 很 扯 还是 要 先 去 相信 然后 进入 故事 再用 很 严谨 逻辑 去 分析 本身 很 神奇']['一种 浪漫 能 让 美女 感动 两种 浪漫 却 能 让 美女 不知所措 房子 车子 足以 让 美女 嫁给 你 可是 当 另一边 却 突然 杀出 钻戒 更 要命 还有 一座 大厦 于是 美女 清醒']]
-
分别将标签numpy和影评numpy转化成tf.data.Dataset
"""先将numpy转化成tensor"""
tensor_train_label = tf.convert_to_tensor(numpy_train_label, dtype=tf.int64)
tensor_train_text = tf.convert_to_tensor(numpy_train_text)"""再将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里的inputsinputs在本次训练任务中指的是文本样本集"""
voctor_layers.adapt(dataset.map(lambda x, y: x))
接着打印该词汇表的容量
print(len(voctor_layers.get_vocabulary()))
52350
发现文本集中出现的词语多达5万,然而这其中有很多词语是出现较少的,这些出现较少的词语会给模型带来很多噪声。所以我们应该在初始化TextVerctorization层时,应该指定最大词汇量。指定后,adapt()方法会选取出现频率最多的那部分词语构成词汇表。在这里,指定最大词汇量为50000
voctor_layers = tf.keras.layers.TextVectorization(max_tokens=50000,output_sequence_length=72,output_mode="int")voctor_layers.adapt(dataset.map(lambda x, y: x).batch(64))print(len(voctor_layers.get_vocabulary()))
50000
max_tokens:指定词汇表的最大词汇量
output_sequence_length:指定一段文本经过TextVerctorization层后,输出序列的长度
output_mode:指定一段文本在TextVerctorization层被序列化的方式,"int":直接输出文本中各个词语在词汇表中的索引
-
构建完整的神经网络
sentiment_model = tf.keras.Sequential([voctor_layers,tf.keras.layers.Embedding(input_dim=len(voctor_layers.get_vocabulary()) + 1,output_dim=16),tf.keras.layers.Dropout(0.2),tf.keras.layers.GlobalAveragePooling1D(),tf.keras.layers.Dropout(0.2),tf.keras.layers.Dense(units=32, activation='relu'),tf.keras.layers.Dense(units=1),tf.keras.layers.Activation('sigmoid')])
Embedding层:将TextVerctorization层输出的疏散整数序列转化成一个output_dim维的密集向量
Dropout层:在每次前向传播中,随机令某些神经元输入下一层的值为0.主要目的是防止过拟合。如果某一层的神经元过多,该层产生过拟合的可能性就越大。
Dense层:代表一个普通的神经网络层,units参数指定该层的神经元数
-
编译神经网络
"""由于前面构造的神经网络在输出层加了sigmoid激活函数,所以from_logits设置为False。如果未加激活函数,则神经网络的输出值的取值范围为(-∞,+∞),from_logits应该设置为True,此时在训练过程中计算损失函数时,就会先将输出值经过sigmoid函数,再代入损失函数中计算损失值"""
sentiment_model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=False), optimizer="adam", metrics=['accuracy'])
由于该模型只有一个输出单元,是个二分类模型,所以损失函数选取的是二分类交叉熵函数BinaryCrossentropy。
BinaryCrossentropy= y为目标值,为模型预测值
梯度下降算法选取的是adam算法
-
提高训练集的性能
AUTOTUNE = tf.data.AUTOTUNEtrain_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
使用模型
调用模型的predict()方法,对一个输入预测一个输出。由于模型的输入是带空格的字符串(空格分离每个词语),所以如果想要输入一段正常的语句,就需要经过分词处理。
-
导入包
import jieba
-
分词处理
def cut_words(chinese_text):result = " ".join(list(jieba.cut(chinese_text)))return result
print(cut_words("中华人民共和国国务院将秦始皇陵定为全国文物重点保护单位"))中华人民共和国国务院 将 秦始皇陵 定为 全国 文物 重点保护 单位
为了提高预测效率,通常传给predict()的是一个numpy数组,此时返回的预测值也是一个numpy数组
def chinese_text_extraction(text_list):# text_list是一个存储字符串的列表new_text_list = []for sentence in text_list:new_text_list.append(cut_words(sentence))return new_text_list
a = ["中华人民共和国国务院将秦始皇陵定为全国文物重点保护单位","当地农民在秦始皇陵东侧1.5公里处打井时偶然发现了与真人真马一样大小的兵马俑","有机地成为西安这座具有历史文化特色的国际化大都市重要的地标符号和精神家园"]print(chinese_text_extraction(a))['中华人民共和国国务院 将 秦始皇陵 定为 全国 文物 重点保护 单位', '当地 农民 在 秦始皇陵 东侧 1.5 公里 处 打井 时 偶然 发现 了 与 真人 真马 一样 大小 的 兵马俑', '有机 地 成为 西安 这座 具有 历史 文化 特色 的 国际化 大都市 重要 的 地标 符号 和 精神家园']
-
开始预测
在网上随意找几句影评作为模型的输入
test_text = ["我说赵氏孤儿怎么那么拧巴,想来想去,不喜欢陈凯歌通过扭曲青少年心理制造人间悲剧,叙事模式先是拍一个馒头,然后引发血案,这次又拍这么一个少年,一个爹养他,一个 ""爹教他,一个爹生他,在他养父唆使下为了给他生父报仇,杀了他教父,这是哪门子仁义礼智信","国产烂童话,一部把观众当小孩儿耍,最后女主角被带上灰机去巴黎,男主角深沉嗦抱歉,最后让你用这种方式了解,是我太现实,还是此剧情太傻B,哈哈哈哈,笑趴。","很久没有出现一部让我一口气追完又怅然若失决定写点什么的剧了。好剧推荐,且尤其适合于目前处于焦躁困境的你我东亚人。","上一次给院线新片打一星应该是去年的《搜救》,那个片子我也只是用了“烂俗”来评价,当时的我绝对想不到还能用得上“恶俗”这个词。""看完成龙这个新片,真的是不吐不快了!拍得烂挺多就是烂俗,可如此廉价低级,不择手段的情怀消费说恶俗都是便宜你!","刚刚经历了《流浪地球》的狂喜,就在影院看到《宇宙探索编辑部》这样一部截然不同的本土科幻电影,实在令人振奋。"]print(chinese_text_extraction(test_text))print(sentiment_model.predict(chinese_text_extraction(test_text)))
['我 说 赵氏 孤儿 怎么 那么 拧 巴 , 想来想去 , 不 喜欢 陈凯歌 通过 扭曲 青少年 心理 制造 人间 悲剧 , 叙事 模式 先是 拍 一个 馒头 , 然后 引发 血案 , 这次 又 拍 这么 一个 少年 , 一个 爹养 他 , 一个 爹教 他 , 一个 爹生 他 , 在 他 养父 唆使 下 为了 给 他 生父 报仇 , 杀 了 他 教父 , 这是 哪门子 仁义 礼智信',
'国产 烂 童话 , 一部 把 观众 当 小孩儿 耍 , 最后 女主角 被 带上 灰机 去 巴黎 , 男主角 深沉 嗦 抱歉 , 最后 让 你 用 这种 方式 了解 , 是我太 现实 , 还是 此 剧情 太 傻 B , 哈哈哈哈 , 笑 趴 。',
'很久没 有 出现 一部 让 我 一口气 追完 又 怅然若失 决定 写 点 什么 的 剧 了 。 好剧 推荐 , 且 尤其 适合 于 目前 处于 焦躁 困境 的 你 我 东亚人 。',
'上 一次 给 院线 新片 打一星 应该 是 去年 的 《 搜救 》 , 那个 片子 我 也 只是 用 了 “ 烂俗 ” 来 评价 , 当时 的 我 绝对 想不到 还 能 用得上 “ 恶俗 ” 这个 词 。 看 完成 龙 这个 新片 , 真的 是 不 吐 不快 了 ! 拍得 烂 挺 多 就是 烂俗 , 可 如此 廉价 低级 , 不择手段 的 情怀 消费 说 恶俗 都 是 便宜 你 !',
'刚刚 经历 了 《 流浪 地球 》 的 狂喜 , 就 在 影院 看到 《 宇宙 探索 编辑部 》 这样 一部 截然不同 的 本土 科幻电影 , 实在 令人振奋 。']
1/1 [==============================] - 0s 180ms/step
[[9.9093014e-01][9.2295074e-01][2.9633459e-04][3.0771473e-01][5.3113187e-03]]
输出值越接近1,则该影评是消极影评的概率就越大
模型缺陷分析
众所周知,一个模型的的上限取决于训练数据的质量,而算法只是使得模型无限逼近这个上限而已。所以训练数据的多样性和均衡性很重要。本次所用的训练文本大多都是一些中等长度的影评,多样性和均衡性都表现不足。比如输入文本[“这个电影不好看”,"不好看",“垃圾电影”],来观察模型的预测。
test_text = ["这个电影不好看","不好看","垃圾电影"]print(chinese_text_extraction(test_text))print(sentiment_model.predict(chinese_text_extraction(test_text)))
['这个 电影 不 好看', '不 好看', '垃圾 电影']
1/1 [==============================] - 0s 192ms/step
[[0.00206227][0.00152057][0.00768803]]
发现预测偏差非常大。这三个文本都是短句子且在训练集中并未出现过类似的。因为包含的词语太少,导致经过TextVectorization层后输出的整数序列中含有的0也就越多,是一个稀疏序列。产生这种预测偏差较大的原因主要是因为训练数据的多样性不足。
解决方法:可以将这三段文本加入训练数据中,并且多复制几份在训练数据中,重新训练模型。你会发现随着这些文本在训练数据中的增多,相应的预测值会逐渐靠近1.