项目3 PROJECT 3 基于LSTM的影评情感分析 本项目基于LSTM,使用分类数据集(Large Movie Review Dataset,LMRD)训练情感分析模型,实现移动端的文本情感推断设计。 3.1总体设计 本部分包括系统整体结构图和系统前后端流程图。 3.1.1系统整体结构图 系统整体结构如图31所示。 图31系统整体结构图 3.1.2系统前后端流程图 系统前端流程如图32所示,系统后端流程如图33所示。 图32系统前端流程图 图33系统后端流程图 3.2运行环境 本部分包括Python环境、TensorFlow环境和Android环境。 3.2.1Python环境 需要Python 3.6及以上配置,用Anaconda创建虚拟环境MRSA(全标为Movie Review Sentiment Analysis),完成所需Python环境的配置。 打开Anaconda Prompt,输入命令: conda create -n MRSA python=3.6 创建MRSA虚拟环境。 3.2.2TensorFlow环境 打开Anaconda Prompt,激活所创建的MRSA虚拟环境,输入命令: activate MRSA 安装CPU版本的TensorFlow,输入命令: pip install –upgrade --ignore-installed tensorflow 安装完毕。 其他相关依赖包,包括Keras、Re、Pickle、Fire、Pandas、Numpy,其安装方式和TensorFlow类似,直接在虚拟环境中pip install package_name或者conda install package_name即可完成。 3.2.3Android环境 安装Android Studio新建Android项目,打开Android Studio,依次选择File→New→New Project→Empty Activity→Next。 Name定义Movie Review Analysis,Save location为项目保存的地址,可自行定义,Minimum API为该项目能够兼容Android手机的最低版本,选择16。单击Finish按钮,新建项目完成。App/build.gradle里的内容有任何改动后,Android Studio都会弹出信息提示。单击Sync Now按钮或图标,同步该配置,“成功”表示配置完成。 3.3模块实现 本项目包括5个模块: 数据预处理、模型构建及训练、模型保存、词典保存和模型测试。下面分别给出各模块的功能介绍及相关代码。 3.3.1数据预处理 本部分包括数据集合并、数据清洗、文本数值化和数据集划分。 1. 数据集合并 数据集下载地址为http://ai.stanford.edu/~amaas/data/sentiment/。斯坦福大学提供的情感分类数据集中了25 000条电影评论用于训练,25 000条用于测试。先将这50 000条数据合并,并保存为.csv文件格式,相关代码如下: #导入原始数据 train_review_files_pos = os.listdir(path + 'train/pos/') review_dest.append(path + 'train/pos/') train_review_files_neg = os.listdir(path + 'train/neg/') review_dest.append(path + 'train/neg/') test_review_files_pos = os.listdir(path + 'test/pos/') review_dest.append(path + 'test/pos/') test_review_files_neg = os.listdir(path + 'test/neg/') review_dest.append(path + 'test/neg/') #将标签合并 sentiment_label = [1]*len(train_review_files_pos) + \ [0]*len(train_review_files_neg) + \ [1]*len(test_review_files_pos) + \ [0]*len(test_review_files_neg) #将所有评论合并 review_train_test = ['train']*len(train_review_files_pos) + \ ['train']*len(train_review_files_neg) + \ ['test']*len(test_review_files_pos) + \ ['test']*len(test_review_files_neg) #将合并后的数据保存为.csv格式 df = pd.DataFrame() df['Train_test_ind'] = review_train_test df['review'] = reviews df['sentiment_label'] = sentiment_label df.to_csv(path + 'processed_file.csv', index=False) 合并后的.csv文件如图34所示。 图34合并后的.csv文件 2. 数据清洗 文本中一些非相干因素会影响最后模型的精度,采用正则表达式将所有标点符号去除,将大写字母转换成小写字母,相关代码如下: def text_clean(text): #将所有大写字母转换成小写字母,并去除标点符号 letters = re.sub("[^a-zA-z0-9\s]", " ",text) words = letters.lower().split() text = " ".join(words) return text 数据清洗结果如图35所示。 图35数据清洗结果 3. 文本数值化 文本中每个单词对应唯一的索引(token),依据索引将文本数值化。Keras tokenizer通过采集前50 000个常用词,转换为数字索引或标记。为了处理方便,对于文本长度大于1000的评论,只取前1000个单词; 若评论长度不足1000,则在评论开始使用0填充。相关代码如下: #采集前50000个常用词,把单词转换为数字索引或标记 max_features = 50000 tokenizer = Tokenizer(num_words=max_features, split=' ') tokenizer.fit_on_texts(df['review'].values) X = tokenizer.texts_to_sequences(df['review'].values) X_ = [] for x in X: x = x[:1000] X_.append(x) X_ = pad_sequences(X_) 数值化结果如图36所示。 图36数值化结果 4. 数据集划分 将数据集划分为训练集、验证集及测试集,比例分别为70%、15%和15%。相关代码如下: y = df['sentiment_label'].values index = list(range(X_.shape[0])) np.random.shuffle(index) train_record_count = int(len(index)*0.7) validation_record_count = int(len(index)*0.15) train_indices = index[:train_record_count] validation_indices = index[train_record_count:train_record_count + validation_record_count] test_indices = index[train_record_count + validation_record_count:] X_train, y_train = X_[train_indices], y[train_indices] X_val, y_val = X_[validation_indices], y[validation_indices] X_test, y_test = X_[test_indices], y[test_indices] 划分后的数据集如图37所示。 图37划分后的数据集 3.3.2模型构建及训练 将数据加载进模型之后,需要定义模型结构、优化损失函数和性能指标。这里定义了两种结构进行训练,一是基于BasicLSTM的网络; 二是基于MultiRNN的网络。 1. 定义模型结构 首先,构建一个简单的LSTM版本递归神经网络(BasicLSTM),并在输入层后面放一个嵌入层。嵌入层的单词向量使用预先训练好的100维Glove向量初始化,该图层被定义为trainable(可训练的),这样,该单词向量嵌入层就可以根据训练数据自行更新。隐藏状态的维度和单元状态的维度也是100。 其次,为获得文本中更多正确信息,进一步定义多层递归神经网络(MultiRNN),共有三层,每层单元状态的维度分别是100、200、100。定义嵌入层的相关代码如下: #定义嵌入层 with tf.variable_scope('embedding'): self.emb_W = tf.get_variable('word_embeddings', [self.n_words, self.embedding_dim], initializer=tf.random_uniform_initializer(-1, 1, 0), trainable=True, dtype=tf.float32) self.assign_ops = tf.assign(self.emb_W, self.emd_placeholder) self.embedding_input = tf.nn.embedding_lookup(self.emb_W, self.X, "embedding_input") print(self.embedding_input) self.embedding_input = tf.unstack(self.embedding_input, self.sentence_length, 1) #定义网络结构 with tf.variable_scope('LSTM_cell'): #定义BasicLSTM self.cell = tf.nn.rnn_cell.BasicLSTMCell(self.hidden_states) #定义MultiRNN #num_units = [100, 200, 100] #self.cells = [tf.nn.rnn_cell.BasicLSTMCell(num_unit) for num_unit in num_units] #self.cell = tf.nn.rnn_cell.MultiRNNCell(self.cells) 2. 优化损失函数 使用二进制交叉熵损失训练模型,并在损失函数中加入正则化以防止出现过拟合,同时使用Adam(Adaptivemoment estimation)优化器训练模型,用精确度作为性能指标。相关代码如下: self.l2_loss = tf.nn.l2_loss(self.w, name="l2_loss") self.scores=tf.nn.xw_plus_b(self.output[-1],self.w,self.b, name="logits") self.prediction_probability = tf.nn.sigmoid(self.scores, name='positive_sentiment_probability') #计算属于1类的概率 self.predictions = tf.round(self.prediction_probability, name='final_prediction') self.losses = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.scores, labels=self.y)#损失函数 self.loss = tf.reduce_mean(self.losses) + self.lambda1 * self.l2_loss tf.summary.scalar('loss', self.loss) self.optimizer = tf.train.AdamOptimizer(self.learning_rate).minimize(self.losses) #优化器 self.correct_predictions = tf.equal(self.predictions, tf.round(self.y)) self.accuracy = tf.reduce_mean(tf.cast(self.correct_predictions, "float"), name="accuracy") tf.summary.scalar('accuracy', self.accuracy) 3. 模型实现 使用tf.train.write_graph()函数将模型图定义保存到model.pbtxt文件中,训练完成后,使用tf.train.Saver()函数将权重保存在model_ckpt中。model.pbtxt和model_ckpt文件将被用于创建protobuf格式的TensorFlow模型优化版本,以便与Android应用集成,相关代码如下: for epoch in range(self.epochs): #轮次 gen_batch = self.batch_gen(self.X_train, self.y_train, self.batch_size) gen_batch_val=self.batch_gen(self.X_val,self.y_val,self.batch_size_val) for batch in range(self.num_batches): #批次 X_batch, y_batch = next(gen_batch) X_batch_val, y_batch_val = next(gen_batch_val) sess.run(self.optimizer,feed_dict={self.X: X_batch, self.y: y_batch}) if (batch+1) % 10 == 0: c, a = sess.run([self.loss, self.accuracy], feed_dict={self.X: X_batch, self.y: y_batch}) print(" Epoch=", epoch+1, " Batch=", batch+1, " Training Loss: ", "{:.9f}".format(c), " Training Accuracy=", "{:.9f}".format(a)) #模型权值保存相关代码 builder = tf.saved_model.builder.SavedModelBuilder(saved_model_dir) builder.add_meta_graph_and_variables(sess, [tf.saved_model.tag_constants.SERVING], signature_def_map={ tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: signature}, legacy_init_op=legacy_init_op) builder.save() tflite_model = tf.contrib.lite.toco_convert(sess.graph_def, [self.X[0]], [self.prediction_probability[0]],inference_type=1, input_format=1, output_format=2,quantized_input_stats=None, drop_control_dependency=True) open(self.path + "converted_model.tflite", "wb").write(tflite_model) 在train()函数中,根据传入批量大小使用生成器生成随机批次,生成器函数的定义如下: def batch_gen(self, X, y, batch_size): index = list(range(X.shape[0])) np.random.shuffle(index) batches = int(X.shape[0] // batch_size) for b in range(batches): X_train,y_train=X[index[b*batch_size: (b + 1)* batch_size], :], y[ index[b * batch_size: (b + 1) * batch_size]] yield X_train, y_train 通过合适的参数调用函数,创建批量的迭代器对象。使用next()函数,提取批量对象的下一个对象。在每个轮次开始时调用生成器函数,以保证每个轮次中的批量都是随机的。 3.3.3模型保存 在model.pbtxt和model_ckpt的文件中保存训练好的模型并不能直接被Android应用程序使用。需要将其转换为protobuf格式(扩展名为.pb文件),与Android应用集成。优化的protobuf格式小于model.pbtxt和model_ckpt文件的大小。 首先,定义输入张量和输出张量的名称; 其次,通过tensorflow.python.tools中的freeze_graph函数,使用这些输入和输出张量以及model.pbtxt和model_ckpt文件,将模型冻结; 最后,被冻结的模型通过tensorflow.python.tools中的optimize_for_inference_lib函数进一步优化,创建protobuf模型(即optimized_model.pb),相关代码如下: freeze_graph.freeze_graph(input_graph_path, input_saver_def_path, input_binary, checkpoint_path, output_node_names, restore_op_name, filename_tensor_name, output_frozen_graph_name, clear_devices, "") input_graph_def = tf.GraphDef() with tf.gfile.Open(output_frozen_graph_name, "rb") as f: data = f.read() input_graph_def.ParseFromString(data) output_graph_def = optimize_for_inference_lib.optimize_for_inference( input_graph_def, ["inputs/X"],#输入节点构成的数组 ["positive_sentiment_probability"], tf.int32.as_datatype_enum#输出节点构成的数组 ) #保存优化后的模型图 f = tf.gfile.FastGFile(output_optimized_graph_name, "w") f. write(output_graph_def.SerializeToString()) 3.3.4词典保存 在预处理期间,训练Keras tokenizer,将单词替换为数字索引,处理后的电影评论提供给LSTM模型进行训练。保留频率最高的前50 000个单词,并将电影评论序列的最大长度限制为1000。尽管训练后的Keras tokenizer被保存并用于推断,但不能直接被Android应用程序使用。 将Keras tokenizer还原,50 000个单词及其相应的单词索引保存在文本文件中。此文本文件可以在Android应用程序中使用,以构建单词到索引的词典,用来转换电影评论的文本。单词到索引映射可以通过tokenizer.word_index从加载的Keras tokenizer对象进行检索。相关代码如下: def tokenize(path,path_out): #保存词典 with open(path, 'rb') as handle: tokenizer = pickle.load(handle) dict_ = tokenizer.word_index keys = list(dict_.keys())[:50000] values = list(dict_.values())[:50000] total_words = len(keys) f = open(path_out,'w') for i in range(total_words): line = str(keys[i]) + ',' + str(values[i]) + '\n' f.write(line) f.close() 3.3.5模型测试 完成模型训练后,移植到移动端,在设计移动应用程序时包括交互界面设计及核心逻辑设计。 1. 交互界面设计 移动应用程序界面设计的相应代码采用XML文件格式。应用程序包含一个简单的电影评论文本框,用户在其中输入他们对于电影的评论,完成后单击SUBMIT按钮,电影评论将被传递给应用程序的核心逻辑模块,该模块处理电影评论文本,并将其传递给TensorFlow优化模型进行推断,针对电影评论的情感打分,该分数会转换为相应的星级,并显示在移动应用程序中。 用于帮助用户和移动应用程序核心逻辑进行彼此交互的变量是在XML文件中通过android: id选项声明的。例如,用户提供的电影评论可以使用Review变量进行处理,对应XML文件中的定义为: android: id="@+id/submit" 相关代码如下: res/layout/activity_main.xml