利用Word2vec生成句向量(一)

2019-12-30  本文已影响0人  菜菜鑫

首先为什么是Word2vec?不是Glove也不是什么Fasttext等其他的词向量?
Glove词向量的训练与部署可以看我之前的文章:https://www.jianshu.com/p/5b60e5b27cf1
很多文章也说过在很多中文文本任务中,Word2vec的表现比Glove好(刚好在我的任务中也是)
本身我用的语料比较大,训练又比较喜欢循环次数多一点,每次分词再训练时间很久,就直接用最稳的词向量了。

本文将会介绍两种比较简单效率又较高的句向量生成方式,一切为了能部署至实际生产环境,本文会着重讲代码实践的部分。想要看偏理论偏学术的可以左转去https://www.cnblogs.com/llhthinker/p/10335164.html


分词
首先是分词方面,依旧采用jieba分词并去除停用词,但是有一些小trick。
*根据新闻长度选择了正文中的前800字和标题进行拼接作为分词的输入。
*分词结果中去掉了所有的单字(长度为1),因为单字的词向量在不同语境中意义不同,容易造成影响,干脆不要。
*分词结果中去掉了所有的数字以及带百分号的数字,这个is_number函数的代码参考自别人,真的是写的大道至简。

    def stop_words_list(self):
        stopwords_dir = os.path.join(self.base_dir, 'stopwords.txt')
        stopwords = [line.strip() for line in open(stopwords_dir, encoding='UTF-8').readlines()]
        return stopwords

    def is_number(self, string):
        # 判断百分号数字
        if string.endswith('%'):
            string = string.replace('%', '')
        try:
            if string == 'NaN':
                return False
            float(string)
            return True
        except ValueError:
            return False

    def fenci(self, title, content):
        # 标题加内容前800字作为句向量的标准
        seg_list = jieba.cut(title.strip() + '。' + content[:800],
                             cut_all=False)  # seg_list是生成器generator类型
        words = ''
        for word in seg_list:
            if word not in self.stop_words_list:
                if word != '\t'  and word != '\n' and not self.is_number(word) and len(
                        word) > 1 and word != 'nbsp':
                    words += word
                    words += " "
        return words

Word2vec词向量
关于word2vec词向量的训练本文不再赘述,有几个参数在训练时要注意一下:
*size一般在50-300之间
*根据语料大小选择iter,一般在10-25之间
*根据语料大小选择min_count,默认为5,语料大的大一点,语料小的小一点

参数实在不会调就用默认值就好。接下来介绍第一种也是最简单的句向量生成方式:平均词向量


平均词向量

直接对分词结果中的每个词,取Word2vec中的对应词向量,并分别对每一位相加,最后取平均,简单粗暴。

但是要注意判断一下当前词是在Word2vec模型中,否则会访问不到而报错。

    # 根据word2vec词向量均值
    def get_sentence_matrix(self, splited_words):
        sentences_matrix = []
        index = 0
        # 平均特征矩阵
        while index < len(splited_words):
            words_matrix = []
            words = splited_words[index].split(" ")
            # 得出各个词的特征向量  并形成一个矩阵  然后计算平均值  就得到该句子的特征向量
            for word in words:
                # 当前词是在Word2vec模型中,self.model为词向量模型
                if word in self.model:
                    words_matrix.append(np.array(self.model[word]))
            # 将words_matrix求均值
            feature = averageVector(many_vectors=words_matrix,
                                    column_num=self.model.vector_size)
            sentences_matrix.append(feature)
            index += 1
        return sentences_matrix
def averageVector(many_vectors, column_num):
    """
    求多个向量的权值向量
    :param many_vector:
    :column_num:向量列数
    """
    average_vector = []
    for i in range(0, column_num, 1):
        average_vector.append(0)
    row_num = len(many_vectors)
    # 先求出各个列权重之和  后面再求平均值
    row_index = 0
    for weight_index, vector in enumerate(many_vectors):
        for i in range(0, column_num, 1):
            average_vector[i] += float(vector[i]) 
        row_index += 1
    for i in range(0, column_num, 1):
        average_vector[i] = average_vector[i] / row_num
    return average_vector

平均词向量的方法比较简单,在很多情况下效果不错,如果进行粗略的相似度计算与匹配可以满足要求。

但是有一个比较严重的问题,就是在平均词向量中,认为所有的词的重要程度是一样的,所以直接进行了相加平均。但是现实中,并不是每一个词对于当前文本的重要程度都相同,所以需要一种方法来细化每一个词对于当前文本的重要程度或贡献度。

IDF字典训练
自然就想到了TFIDF方法,TFIDF的理论较为简单,在此也不赘述。

但是如果每次分词都对所有文本算一次TFIDF,如果数据过大,会比较浪费时间。有的人直接用jieba中的TFIDF工具,虽然我没有细究jieba的IDF值是怎么算的,但还是觉得自己的相关领域的语料训练出来的效果会好一些。一个小trick就是获取大量相关领域的语料,并提前训练好IDF字典进行存储,这样对于每一个文本就只需要计算TF就可以得到每一个词的TFIDF值。

采用平滑IDF处理,并对于未出现在字典中的词语给出默认的IDF值,通过pickle进行存储,代码如下:

import math
import os
import pickle

# idf值统计方法
def train_idf(doc_list):
    idf_dic = {}
    # 总文档数
    tt_count = len(doc_list)

    # 每个词出现的文档数
    for doc in doc_list:
        for word in set(doc):
            idf_dic[word] = idf_dic.get(word, 0.0) + 1.0

    # 按公式转换为idf值,分母加1进行平滑处理
    for k, v in idf_dic.items():
        idf_dic[k] = math.log(tt_count / (1.0 + v))

    # 对于没有在字典中的词,默认其仅在一个文档出现,得到默认idf值
    print("tt_count" + str(tt_count))
    default_idf = math.log(tt_count / (1.0))
    return idf_dic, default_idf

def load_fenci_data():
    # 调用上面方式对数据集进行处理,处理后的每条数据仅保留非干扰词

    doc_list = []
    for line in open(os.path.join('/home/brx/Documents/Projects/Similarity/qa_similarity/com', 'history_fenci.txt'), encoding='UTF-8'): # 分词结果文件
        doc_list.append(line.strip().split())
    return doc_list

def save_obj(obj, name ):
    with open('./'+ name + '.pkl', 'wb') as f:
        pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL)

def load_obj(name ):
    with open('./' + name + '.pkl', 'rb') as f:
        return pickle.load(f)

doc_list = load_fenci_data()
idf_dic, default_idf = train_idf(doc_list)
save_obj(idf_dic, 'idf_dic')
# idf_dic = load_obj('idf_dic')
print(default_idf)

这样即可根据语料获得IDF字典idf_dic.pkl和默认IDF值,字典可以直接通过load_obj方法进行读取。


TFIDF加权平均词向量

代码如下,和之前的平均词向量相比,多了idf_dic(从pickle文件load而来)vector_weight(从idf_dic中读取而来)。但是也要注意判断当前词是否在idf_dic中,如果平均词向量的代码看懂了,这个代码也非常好理解。

    # 根据word2vec和tfidf获取句子矩阵
    def get_sentence_matrix(self, splited_words):
        sentences_matrix = []
        index = 0
        # 平均特征矩阵
        while index < len(splited_words):
            words_matrix = []
            word_weight = []
            words = splited_words[index].split(" ")
            # 得出各个词的特征向量  并形成一个矩阵  然后计算平均值  就得到该句子的特征向量
            for word in words:
                if word in self.model:
                    words_matrix.append(np.array(self.model[word]))
                    if word in self.idf_dic:
                        word_weight.append(self.idf_dic[word])
                    else:
                        word_weight.append(self.default_idf)

            # 将words_matrix求均值
            feature = averageVector(many_vectors=words_matrix, vector_weight=word_weight,
                                    column_num=self.model.vector_size)
            sentences_matrix.append(feature)
            index += 1
        return sentences_matrix

def averageVector(many_vectors, vector_weight, column_num):
    """
    求多个向量的权值向量
    :param many_vector:
    :column_num:向量列数
    :vector_weight:IDF权重
    """
    average_vector = []
    # print(vector_weight)
    for i in range(0, column_num, 1):
        average_vector.append(0)
    row_num = len(many_vectors)
    # 先求出各个列权重之和  后面再求平均值
    row_index = 0
    for weight_index, vector in enumerate(many_vectors):
        for i in range(0, column_num, 1):
            average_vector[i] += float(vector[i]) * vector_weight[weight_index]
        row_index += 1
    for i in range(0, column_num, 1):
        average_vector[i] = average_vector[i] / row_num
    return average_vector

稍有基础的小伙伴应该都可以根据以上代码实现自己的功能,计算欧式或余弦距离,进行相似度匹配和聚类分类等任务。下一篇文章将会介绍一个号称更为有效的句向量生成方法,同样可以基于Word2vec实现。
\color{red}{(纯原创,转载请注明来源)}

上一篇下一篇

猜你喜欢

热点阅读