自然语言处理 | 文本向量化

Tomas Mikolov2013年在ICLR提出用于获取word vector的论文《Efficient estimation of word representations in vector space》,文中简单介绍了两种训练模型 CBOW Skip-gram ,以及两种加速方法 Hierarchical Softmax Negative Sampling 。除了word2vec之外,还有其他的文本向量化的方法,因此在这里做个总结。

文本向量化(又称“词向量模型”、“向量空间模型”)即将文本表示成计算机可识别的实数向量,根据粒度大小不同可将文本特征表示分为字、词、句子或篇章几个层次。文本向量化的方法主要分为离散表示和分布式表示。

1.离散表示

一种基于规则和统计的向量化方式,常用的方法包括 词集模型 词袋模型 ,都是基于词之间保持独立性、没有关联为前提,将所有文本中单词形成一个字典,然后根据字典来统计单词出现频数,不同的是:

  • 词集模型:例如One-Hot Representation,只要单个文本中单词出现在字典中,就将其置为1,不管出现多少次
  • 词袋模型:只要单个文本中单词出现在字典中,就将其向量值加1,出现多少次就加多少次
  • 其基本的特点是忽略了文本信息中的 语序信息 语境信息 ,仅将其反映为若干维度的独立概念,这种情况有着因为模型本身原因而无法解决的问题,比如主语和宾语的顺序问题,词袋模型天然无法理解诸如“我为你鼓掌”和“你为我鼓掌”两个语句之间的区别。

    One-Hot Representation

    将每个 都表示成一个长向量,向量的维度是词表的大小,词的当前位置用1表示,其他位置用0表示。

    import numpy as np
    import pandas as pd
    import jieba
    def doc2onthot_matrix(file_path):
        # 读取待编码的文件
        with open(file_path, encoding="utf-8") as f:
            docs = f.readlines()
        with open(file_path1, encoding="utf-8") as f:
            docs1 = f.readlines()
        # 将文件每行分词,分词后的词语放入words中
        words=[]
        for i in range(len(docs)):
            docs[i] = jieba.cut(docs[i].strip("\n"))
            words += docs[i]
        # 找出分词后不重复的词语,作为词袋,是后续onehot编码的维度
        vocab = sorted(set(words), key=words.index)
        # 建立一个M行V列的全0矩阵,M是文档样本数,这里是行数,V为不重复词语数,即编码维度
        V = len(vocab)
        M = len(docs)
        onehot = np.zeros((M,V))
        for i,doc in enumerate(docs1):
            words = ""
            for word in doc:
                if word != " ":
                    words = words + word
                    continue
                if words in vocab:
                    pos = vocab.index(words)
                    onehot[i][pos] = 1
                    words = ""
                else:
                    words = ""
                    continue
        onehot=pd.DataFrame(onehot, columns=vocab)
        return onehot
    file_path = "D:/Pythonworkspace/test.txt"
    file_path1 = "D:/Pythonworkspace/word.txt"
    onehot = doc2onthot_matrix(file_path)
    onehot
    

    One-Hot编码的优点是简单快捷,缺点是数据稀疏、耗时耗空间、不能很好地展示词与词之间的相似关系,且还未考虑到词出现的频率,因而无法区别词的重要性。

    对于句子篇章而言,常用的离散表示方法是词袋模型。词袋模型以One-Hot为基础,忽略词表中词的顺序和语法关系,通过记录词表中的每一个词在该文本中出现的频次来表示该词在文本中的重要程度,解决了 One-Hot 未能考虑词频的问题。
    词袋模型的优点是方法简单,当语料充足时,处理简单的问题如文本分类,其效果比较好。词袋模型的缺点是数据稀疏、维度大,且不能很好地展示词与词之间的相似关系。

    TF-IDF

    TF-IDF(词频-逆文档频率法,Term Frequency–Inverse Document Frequency)作为一种加权方法,在词袋模型的基础上对词出现的频次赋予TF-IDF权值,对词袋模型进行修正,进而表示该词在文档集合中的重要程度。

    import numpy as np
    import pandas as pd
    import math
    import jieba
    import jieba.analyse
    import re
    # 中文分词
    def word_segment(file_path_before, file_path_after, stop_word_file,dic_file):
        # 读取待编码的文件
        with open(file_path_before,encoding="utf-8") as f:
            docs = f.readlines()
        # 加载停用词
        stopwords=[]
        for word in open(stop_word_file, 'r'): # 这里加载停用词的路径
            stopwords.append(word.strip("\n"))
        words_list = []
        for i,text in enumerate(docs):
            words = []
            final_word = []
            p = re.compile(r"\n|:|;|,|、|(|)|\.|。|,|/|(\|)", re.S)
            text = p.sub('', text)
            jieba.load_userdict(dic_file)
            word = jieba.cut(text)
            words += word
            for i,word in enumerate(words):
                if word not in stopwords:
                    final_word.append(word)
            words_list.append(final_word)
        for i in range(0,len(words_list)):
            with open(file_path_after, 'a') as f1:
                f1.write(str(words_list[i]))
                f1.write("\n")
        return words_list
    # 生成TD-IDF矩阵
    def doc2tfidf_matrix(file_path_before, words_list):     
        # 找出分词后不重复的词语,作为词袋
        words = []
        for i,word in enumerate(words_list):
            words += word
        vocab = sorted(set(words),key=words.index)
        # print(vocab)
        # 建立一个M行V列的全0矩阵,M是文档样本数,这里是行数,V为不重复词语数,即编码维度
        V = len(vocab)
        M = len(words_list)
        onehot = np.zeros((M,V)) # 二维矩阵要使用双括号
        tf = np.zeros((M,V))
        for i,doc in enumerate(words_list):
            for word in doc:
                if word in vocab:
                    pos = vocab.index(word)
                    # print(pos,word)
                    onehot[i][pos] = 1
                    tf[i][pos] += 1 # tf,统计某词语在一条样本中出现的次数
                else:
                    print(word)
        row_sum = tf.sum(axis=1) # 行相加,得到每个样本出现的词语数
        # 计算TF(t,d)
        tf = tf/row_sum[:, np.newaxis] #分母表示各样本出现的词语数,tf为单词在样本中出现的次数,[:,np.newaxis]作用类似于行列转置
        # 计算DF(t,D),IDF
        df = onehot.sum(axis=0) # 列相加,表示有多少样本包含词袋某词
        idf = list(map(lambda x:math.log10((M+1)/(x+1)),df))
        # 计算TFIDF
        tfidf = tf*np.array(idf)
        tfidf = pd.DataFrame(tfidf,columns=vocab)
        return tfidf
    file_path_before = "C:/Users/asus/Desktop/goodat.txt"
    file_path_after = "C:/Users/asus/Desktop/word segment.txt"
    stop_word_file = "C:/Users/asus/Desktop/stop_word.txt"
    dic_file = "C:/Users/asus/Desktop/dic.txt"
    words_list = word_segment(file_path_before, file_path_after,stop_word_file,dic_file)
    tfidf = doc2tfidf_matrix(file_path_after, words_list)
    for i in range(0,50):
        goodat = ""
        row = tfidf.iloc[i].sort_values(ascending=False)
        for i in range(0,10):
            goodat = goodat + row.index[i] + "/"
        print(goodat)
    

    在利用TF-IDF进行特征提取时,若词α在某篇文档中出现频率较高且在其他文档中出现频率较低时,则认为α可以代表该文档的特征,具有较好的分类能力,那么α作为特征被提取出来。

    CountVectorizer

    CountVectorizer 根据文本构建出一个词表,词表中包含了所有文本中的单词,每一个词汇对应其出现的顺序,构建出的词向量的每一维都代表这一维对应单词出现的频次,这些词向量组成的矩阵称为频次矩阵。但 CountVectorizer 只能表达词在当前文本中的重要性,无法表示该词在整个文档集合中的重要程度。

    TF-idfVectorizer

    TF-idfVectorizer 则在CountVectorizer 的基础上计算了词的词频和逆文档频率,将词的 TF-IDF 权值作为该词对应维度的值以区分词的重要性。相比之下,数据集越大,利用 TF-idfVectorizer 进行文本向量化就更有优势。

    潜语义分析模型(Latent Semantic Analysis)

    文档的词向量空间转化为语义级别的向量空间,使用主成分分析方法降维,抽取向量空间内分布方差最大的若干个正交方向来作为最后的表示方向,并对其余方向的内容进行丢弃。

    SVD奇异值分解

    2.分布式表示

    每个词根据上下文从高维映射到一个低维度、稠密的向量上,向量的维度需要指定。在构成的向量空间中,每个词的含义都可以用周边的词来表示,优点是考虑到了词之间存在的相似关系,减小了词向量的维度。常用的方法包括:

  • 基于矩阵的分布表示
  • 基于聚类的分布表示
  • 基于神经网络的分布表示,其特点是:①利用了激活函数及softmax函数中的非线性特点 ②保留了语序信息
  • Word2vec

    Word2vec是Google的开源项目,其特点是将所有的词向量化,这样词与词之间就可以定量度量。Word2vec以词嵌入为基础,利用深度学习的思想,对出现在上下文环境中的词进行预测,经过Word2vec训练后的词向量可以很好度量词与词之间的相似性,将所有词语投影到k维的向量空间,每个词语都可以用一个k维向量表示,进而将文本内容的处理简化为K维向量空间中的向量运算。

    已有的预训练词向量:腾讯AI实验室 Embedding Dataset,该语料库为超过800万个中文单词和短语提供了200维矢量表示,这些单词和短语是在大规模高质量数据上预先训练的。

    Word2vec有Continuous Bag-of-Words(CBOW)Skip-gram两种训练模型,这两种模型可以看做简化的三层神经网络,主要包括输入层、隐藏层以及输出层。CBOW对小型数据库比较合适,而Skip-Gram在大型语料中表现更好。

  • CBOW:训练输入是某一个特征词上下文相关的词对应的词向量,输出就是这特定的一个词的词向量。
  • # 去掉标点符号 def clearSen(comment): comment = ' '.join(comment).replace(',', '').replace('。', '').replace('?', '').replace('“', '').replace('”', '').replace('.', '').replace('(', '').replace(':', '').replace('…', '').replace('...', '').replace('、', '') return comment # 读取数据并分词 file_path = "D:/Pythonworkspace/test.txt" with open(file_path,encoding="utf-8") as f: docs = f.readlines() docs = clearSen(docs) words = ' '.join(jieba.cut(docs)) with open("D:/Pythonworkspace/word.txt",'w',encoding='utf-8') as f: f.write(words) sentences = word2vec.Text8Corpus(r'D:/Pythonworkspace/word.txt') # 基于当前语料集训练模型 model = word2vec.Word2Vec(sentences, size=50, window=5, workers=4, min_count=3) # 获取词向量 print(model['骨科']) # 查看向量化之后的距离 for i in model.most_similar(u"骨科"): print(i[0],i[1])

    gensim函数库训练Word2Vec模型有很多配置参数,这里进行简单说明:
    class gensim.models.word2vec.Word2Vec(sentences=None,size=100,alpha=0.025,window=5, min_count=5, max_vocab_size=None, sample=0.001,seed=1, workers=3,min_alpha=0.0001, sg=0, hs=0, negative=5, cbow_mean=1, hashfxn=<built-in function hash>,iter=5,null_word=0, trim_rule=None, sorted_vocab=1, batch_words=10000)
    sentences:可以是一个list,对于大语料集,建议使用BrownCorpus,Text8Corpus或LineSentence构建。 sg: 用于设置训练算法,默认为0,对应CBOW算法;sg=1则采用skip-gram算法。 size:是指特征向量的维度,默认为100。值太小会导致词映射因为冲突而影响结果,值太大则会耗内存并使算法计算变慢,一般值取为100到200之间 window:句子中当前词与目标词之间的最大距离,3表示在目标词前看3-b个词,后面看b个词(b在0-3之间随机) alpha: 学习速率 seed:用于随机数发生器,与初始化词向量有关。 min_count: 可以对字典做截断,词频少于min_count次数的单词会被丢弃掉,默认值为5 max_vocab_size: 设置词向量构建期间的RAM限制。如果所有独立单词个数超过这个,则就消除掉其中最不频繁的一个。每一千万个单词需要大约1GB的RAM。设置成None则没有限制。 sample: 高频词汇的随机降采样的配置阈值,默认为1e-3,范围是(0,1e-5) workers: 参数控制训练的并行数。 hs: 如果为1则会采用hierarchica·softmax技巧。如果设置为0(defau·t),则negative sampling会被使用。 negative: 如果>0,则会采用negativesampling,用于设置多少个noise words cbow_mean: 如果为0,则采用上下文词向量的和,如果为1(default)则采用均值。只有使用CBOW的时候才起作用。 hashfxn: hash函数来初始化权重。默认使用python的hash函数 iter: 迭代次数,默认为5 trim_rule: 用于设置词汇表的整理规则,指定那些单词要留下,哪些要被删除。可以设置为None(min_count会被使用)或者一个接受()并返回RU·E_DISCARD,uti·http://s.RU·E_KEEP或者uti·http://s.RU·E_DEFAU·T的函数。 sorted_vocab:如果为1(defau·t),则在分配word index 的时候会先对单词基于频率降序排序。 batch_words:每一批的传递给线程的单词的数量,默认为10000

    在上面的代码中,使用了gensim库中的word2vec,对分好词后的语料集进行向量化,并可以计算出词与词之间的相似性,例如在该语料集中,与“骨科”最相似的词有:

    附:通过已保存的训练好的词向量模型,得到word embedding

    # load the pre-trained word-embedding vectors
    embeddings_index = {}
    for i, line in enumerate(open('wiki-news-300d-1M.vec', 'r', encoding='UTF-8')):
        values= line.split()
        embeddings_index[values[0]] = np.asarray(values[1:], dtype='float32')
    len(embeddings_index) # 999995
    # create atokenizer
    token = text.Tokenizer()
    token.fit_on_texts(自己的语料)
    word_index = token.word_index # 单词对应的index
    index_docs = token.index_docs # index对应单词出现的文档的数量
    word_docs = token.word_docs # 单词出现的文档的数量
    word_counts = token.word_counts # 单词在所有文档中出现的总次数
    # convert text tosequence of tokens andpad them toensure equal length vectors
    train_seq_x = sequence.pad_sequences(token.texts_to_sequences(Train_X_abs))
    test_seq_x = sequence.pad_sequences(token.texts_to_sequences(Test_X_abs))
    # create token-embedding mapping
    # embedding_matrix = np.zeros((len(word_index) + 1, 300))
    embedding_matrix = pd.DataFrame()
    for word, i in word_index.items():
        word = word.replace("'","")
        embedding_vector = embeddings_index.get(word)