赞
踩
情感分析是自然语言处理中常见的场景,比如淘宝商品评价,饿了么外卖评价等,对于指导产品更新迭代具有关键性作用。通过情感分析,可以挖掘产品在各个维度的优劣,从而明确如何改进产品。比如对外卖评价,可以分析菜品口味、送达时间、送餐态度、菜品丰富度等多个维度的用户情感指数,从而从各个维度上改进外卖服务。
情感分析可以采用基于情感词典的传统方法,也可以采用基于深度学习的方法,下面详细讲解
基于情感词典的方法,先对文本进行分词和停用词处理等预处理,再利用先构建好的情感词典,对文本进行字符串匹配,从而挖掘正面和负面信息。
情感词典包含正面词语词典、负面词语词典、否定词语词典、程度副词词典等四部分。如下图
词典包含两部分,词语和权重,如下
正面: 很快 1.75 挺快 1.75 还好 1.2 很萌 1.75 服务到位 1 负面: 无语 2 醉了 2 没法吃 2 不好 2 太差 5 太油 2.5 有些油 1 咸 1 一般 0.5 程度副词: 超级 2 超 2 都 1.75 还 1.5 实在 1.75 否定词: 不 1 没 1 无 1 非 1 莫 1 弗 1 毋 1
情感词典在整个情感分析中至关重要,所幸现在有很多开源的情感词典,如BosonNLP情感词典,它是基于微博、新闻、论坛等数据来源构建的情感词典,以及知网情感词典等。当然我们也可以通过语料来自己训练情感词典。
基于词典的文本匹配算法相对简单。逐个遍历分词后的语句中的词语,如果词语命中词典,则进行相应权重的处理。正面词权重为加法,负面词权重为减法,否定词权重取相反数,程度副词权重则和它修饰的词语权重相乘。如下图
利用最终输出的权重值,就可以区分是正面、负面还是中性情感了。
基于词典的情感分类,简单易行,而且通用性也能够得到保障。但仍然有很多不足
近年来,深度学习在NLP领域内也是遍地开花。在情感分类领域,我们同样可以采用深度学习方法。基于深度学习的情感分类,具有精度高,通用性强,不需要情感词典等优点。
基于深度学习的情感分类,首先对语句进行分词、停用词、简繁转换等预处理,然后进行词向量编码,然后利用LSTM或者GRU等RNN网络进行特征提取,最后通过全连接层和softmax输出每个分类的概率,从而得到情感分类。
下面通过代码来讲解这个过程。2018年AI Challenger细粒度用户评论情感分析比赛中的代码。项目数据来源于大众点评,训练数据10万条,验证1万条。分析大众点评用户评论中,关于交通,菜品,服务等20个维度的用户情感指数。分为正面、负面、中性和未提及四类。代码在验证集上,目前f1 socre可以达到0.62。
数据预处理都放在了PreProcessor类中,主函数是process。步骤如下
class PreProcessor(object): def __init__(self, filename, busi_name="location_traffic_convenience"): self.filename = filename self.busi_name = busi_name self.embedding_dim = 256 # 读取词向量 embedding_file = "./word_embedding/word2vec_wx" self.word2vec_model = gensim.models.Word2Vec.load(embedding_file) # 读取原始csv文件 def read_csv_file(self): reload(sys) sys.setdefaultencoding('utf-8') print("after coding: " + str(sys.getdefaultencoding())) data = pd.read_csv(self.filename, sep=',') x = data.content.values y = data[self.busi_name].values return x, y # todo 错别字处理,语义不明确词语处理,拼音繁体处理等 def correct_wrong_words(self, corpus): return corpus # 去掉停用词 def clean_stop_words(self, sentences): stop_words = None with open("./stop_words.txt", "r") as f: stop_words = f.readlines() stop_words = [word.replace("\n", "") for word in stop_words] # stop words 替换 for i, line in enumerate(sentences): for word in stop_words: if word in line: line = line.replace(word, "") sentences[i] = line return sentences # 分词,将不在词向量中的jieba分词单独挑出来,他们不做分词 def get_words_after_jieba(self, sentences): # jieba分词 all_exclude_words = dict() while (1): words_after_jieba = [[w for w in jieba.cut(line) if w.strip()] for line in sentences] # 遍历不包含在word2vec中的word new_exclude_words = [] for line in words_after_jieba: for word in line: if word not in self.word2vec_model.wv.vocab and word not in all_exclude_words: all_exclude_words[word] = 1 new_exclude_words.append(word) elif word not in self.word2vec_model.wv.vocab: all_exclude_words[word] += 1 # 剩余未包含词小于阈值,返回分词结果,结束。否则添加到jieba del_word中,然后重新分词 if len(new_exclude_words) < 10: print("length of not in w2v words: %d, words are:" % len(new_exclude_words)) for word in new_exclude_words: print word, print("\nall exclude words are: ") for word in all_exclude_words: if all_exclude_words[word] > 5: print "%s: %d," % (word, all_exclude_words[word]), return words_after_jieba else: for word in new_exclude_words: jieba.del_word(word) raise Exception("get_words_after_jieba error") # 去除不在词向量中的词 def remove_words_not_in_embedding(self, corpus): for i, sentence in enumerate(corpus): for word in sentence: if word not in self.word2vec_model.wv.vocab: sentence.remove(word) corpus[i] = sentence return corpus # 词向量,建立词语到词向量的映射 def form_embedding(self, corpus): # 1 读取词向量 w2v = dict(zip(self.word2vec_model.wv.index2word, self.word2vec_model.wv.syn0)) # 2 创建词语词典,从而知道文本中有多少词语 w2index = dict() # 词语为key,索引为value的字典 index = 1 for sentence in corpus: for word in sentence: if word not in w2index: w2index[word] = index index += 1 print("\nlength of w2index is %d" % len(w2index)) # 3 建立词语到词向量的映射 # embeddings = np.random.randn(len(w2index) + 1, self.embedding_dim) embeddings = np.zeros(shape=(len(w2index) + 1, self.embedding_dim), dtype=float) embeddings[0] = 0 # 未映射到的词语,全部赋值为0 n_not_in_w2v = 0 for word, index in w2index.items(): if word in self.word2vec_model.wv.vocab: embeddings[index] = w2v[word] else: print("not in w2v: %s" % word) n_not_in_w2v += 1 print("words not in w2v count: %d" % n_not_in_w2v) del self.word2vec_model, w2v # 4 语料从中文词映射为索引 x = [[w2index[word] for word in sentence] for sentence in corpus] return embeddings, x # 预处理,主函数 def process(self): # 读取原始文件 x, y = self.read_csv_file() # 错别字,繁简体,拼音,语义不明确,等的处理 x = self.correct_wrong_words(x) # stop words x = self.clean_stop_words(x) # 分词 x = self.get_words_after_jieba(x) # remove不在词向量中的词 x = self.remove_words_not_in_embedding(x) # 词向量到词语的映射 embeddings, x = self.form_embedding(x) # 打印 print("embeddings[1] is, ", embeddings[1]) print("corpus after index mapping is, ", x[0]) print("length of each line of corpus is, ", [len(line) for line in x]) return embeddings, x, y
词向量编码步骤主要有:
Embedding(input_dim=len(embeddings),
output_dim=len(embeddings[0]),
weights=[embeddings],
input_length=self.max_seq_length,
trainable=False,
name=embeddings_name))
LSTM网络主要分为如下几层
LSTM网络是重中之重,这儿可以优化的空间很大。比如可以采用更优的双向LSTM,可以加入注意力机制。这两个trick都可以提高最终准确度。另外可以建立分词和不分词两种情况下的网络,最终通过concat合并。
class Model(object): def __init__(self, busi_name="location_traffic_convenience"): self.max_seq_length = 100 self.lstm_size = 128 self.max_epochs = 10 self.batch_size = 128 self.busi_name = busi_name self.model_name = "model/%s_seq%d_lstm%d_epochs%d.h5" % (self.busi_name, self.max_seq_length, self.lstm_size, self.max_epochs) self.yaml_name = "model/%s_seq%d_lstm%d_epochs%d.yml" % (self.busi_name, self.max_seq_length, self.lstm_size, self.max_epochs) def split_train_data(self, x, y): x_train, x_val, y_train, y_val = train_test_split(x, y, test_size=0.1) # 超长的部分设置为0,截断 x_train = sequence.pad_sequences(x_train, self.max_seq_length) x_val = sequence.pad_sequences(x_val, self.max_seq_length) # y弄成4分类,-2未提及,-1负面,0中性,1正面 y_train = keras.utils.to_categorical(y_train, num_classes=4) y_val = keras.utils.to_categorical(y_val, num_classes=4) return x_train, x_val, y_train, y_val def build_network(self, embeddings, embeddings_name): model = Sequential() model.add(Embedding(input_dim=len(embeddings), output_dim=len(embeddings[0]), weights=[embeddings], input_length=self.max_seq_length, trainable=False, name=embeddings_name)) model.add(LSTM(units=self.lstm_size, activation='tanh', return_sequences=True, name='lstm1')) model.add(LSTM(units=self.lstm_size, activation='tanh', name='lstm2')) model.add(Dropout(0.1)) model.add(Dense(4)) model.add(Activation('softmax')) return model def train(self, embeddings, x, y): model = self.build_network(embeddings, "embeddings_train") model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"]) # 训练,采用k-folder交叉训练 for i in range(0, self.max_epochs): x_train, x_val, y_train, y_val = self.split_train_data(x, y) model.fit(x_train, y_train, batch_size=self.batch_size, validation_data=(x_val, y_val)) # 保存model yaml_string = model.to_yaml() with open(self.yaml_name, 'w') as outfile: outfile.write(yaml.dump(yaml_string, default_flow_style=True)) # 保存model的weights model.save_weights(self.model_name) def predict(self, embeddings, x): # 加载model print 'loading model......' with open(self.yaml_name, 'r') as f: yaml_string = yaml.load(f) model = model_from_yaml(yaml_string) # 加载权重 print 'loading weights......' model.load_weights(self.model_name, by_name=True) model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"]) # 预测 x = sequence.pad_sequences(x, self.max_seq_length) predicts = model.predict_classes(x) # 得到分类结果,它表征的是类别序号 # 转换 classes = [0, 1, -2, -1] predicts = [classes[item] for item in predicts] np.set_printoptions(threshold=np.nan) # 全部打印 print(np.array(predicts)) return predicts
这一部分上面代码已经讲到了,不在赘述。softmax只是一个归一化,讲数据归一化到[0, 1]之间,从而可以得到每个类别的概率。我们最终取概率最大的即可。
基于深度学习的情感分析难点也很多
文本情感分析是NLP领域一个十分重要的问题,对理解用户意图具有决定性的作用。通过基于词典的传统算法和基于深度学习的算法,可以有效的进行情感分析。当前情感分析准确率还有待提高,任重而道远!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。