自然语言中获取句向量简介

NLP中许多下游任务(文本分类、情感分析、意图推断等)都依赖于第一步——文本字符串转为句子特征向量。

NLP中获得句向量有两种方式:

(1) 通过词向量后处理得到句向量;

(2) 直接得到句向量

这里就不介绍GloVe

1.词向量后处理得到句向量

我们都知道句子是由一个个词语组成的,词向量技术只是将单个词语转成固定维度的向量。

那么怎么得到多个词语组成的句子的向量了?

本文将介绍如下几种词向量生成句向量的无监督手段,它们分别是:累加法、平均法、TF-IDF加权平均法以及SIF嵌入法。

1.1 累加法

累加法是得到句子向量最简单的方法

假设有这样一句文本:    我很开心

NLP处理一段文本首先需要将一段文本进行分词、去停用词处理,经过去停用词处理后上述文本可得下面的词语距离:

[“我“,”很“,”开心“]

本文采用python的gensim里面的Word2vec模型来得到词向量,可得上述单词的如下词向量(为可视化更清楚,用5维的词向量来演示)

我 :[[-0.46499524 -2.8825798 1.1845024 -1.6874554 -0.05758076]] 

很 :[[-2.26874 0.99428487 -0.9092457 -0.67786723 4.244918 ]] 

开心: [[-1.0627153 -0.7416505 0.41102988 -0.39201248 0.6933297 ]]

累加法的做法是将句子中所有非停用词的词向量叠加,如果句子有n个非停用词,则句子的词向量通过下面的手段获得:

Vsentence = Vword1 + Vword2 + …… + Vwordn

根据此方法可以得到” 我很开心“ 的句子向量为:

Vsentence = V1+ Vn2+ V3

句子向量: [[-3.79645047 -2.62994546 0.68628651 -2.75733513 4.8806668 ]]

分析:不能表达句子的整体语义。当有多个词向量相加等于其他词相加,但是表达的意思都是不一样的。

整体代码:

import numpy as npfrom gensim.models.word2vec  import Word2Vecimport jiebafrom sklearn.model_selection import train_test_splitdef readgh(path):    res=[]    label=[]    f=open(path, "r", encoding='utf-8-sig')    for line in f:        if int(line.split(' ', 1)[0])==-1 :            label.append([1,0])        else:            label.append([0,1])        res.append(line.split(' ',1)[1])    return res,labeldef divide(result,label):   x_train, x_test, y_train, y_test = train_test_split(result,label, test_size=0.3,                                          random_state=666)   return x_train, x_test, y_train, y_testdef fenci1(data,stopword):   result = []   for text in data:      word_list = ' '.join(jieba.cut(text)).split(" ")      result.append(list(filter(lambda x : x not in stopword, word_list)))   return resultdef buildWordVector(sentence,size,w2v_model):   vec   = np.zeros(size).reshape((1,size))   count = 0.   for word in sentence:      try:         vec += w2v_model[word].reshape((1,size))         count += 1      except KeyError:         continue   if count != 0:      vec /= count   return vecdef buildWordVector1(sentence,size,w2v_model):    vec =np.zeros(size).reshape((1,size))    data_vec=np.zeros(size).reshape((1,size))    for word in sentence:        print(word)        vec=w2v_model[word].reshape((1,size))        print(vec)        data_vec +=vec    print("句子向量:")    print(data_vec)    return data_vec#计算词向量def get_train_vecs(x_train,x_test):    n_dim = 5    #Initialize model and build vocab    w2v_model = Word2Vec(size = n_dim,min_count = 10)    w2v_model.build_vocab(x_train)    #在训练集训练词向量模型    w2v_model.train(x_train,total_examples = w2v_model.corpus_count,epochs = w2v_model.iter)    #生成训练集词向量    train_vecs = np.concatenate([buildWordVector(line,n_dim,w2v_model) for line in x_train])    #保存训练集词向量文件    print("Train word_vector shape:",train_vecs.shape)    #在测试集训练词向量模型    w2v_model.train(x_test,total_examples = w2v_model.corpus_count,epochs = w2v_model.iter)    #生成测试集词向量    test_vecs = np.concatenate([buildWordVector(line,n_dim,w2v_model) for line in x_test])    #保存测试集词向量文件    print("Test word_vector shape:",test_vecs.shape)    #保存词向量模型    w2v_model.save('D:\\学习资料\\项目\\邮件网关\\zh_cnn_text_classify-master\\test_model.pkl')    return train_vecs,test_vecstext=["我很开心"]with open('D:\\学习资料\\项目\\情感分析\\stopwords.txt', encoding='utf8') as f:    stopword = f.read().splitlines()result = fenci1(text, stopword)model=Word2Vec.load("D:\\学习资料\\项目\\邮件网关\\zh_cnn_text_classify-master\\test_model.pkl")test_vec=test_vecs = np.concatenate([buildWordVector1(line,5,model) for line in result])print(test_vec)
复制代码

累加方法:

def buildWordVector1(sentence,size,w2v_model):    vec =np.zeros(size).reshape((1,size))    data_vec=np.zeros(size).reshape((1,size))    for word in sentence:        print(word)        vec=w2v_model[word].reshape((1,size))        print(vec)        data_vec +=vec    print("句子向量:")    print(data_vec)    return data_vec
复制代码

1.2 平均法

平均法和累计法方法相似,同样需要将一个句子中所有的非停用词向量叠加起来,但最后需要把叠加起来向量除以非停用词的个数。句子的词向量通过下面的手段获得:

Vsentence = (Vword1 + Vword2 + …… + Vwordn) / n

句子向量:[[-1.26548349 -0.87664849 0.22876217 -0.91911171 1.62688893]]

代码:

def buildWordVector(sentence,size,w2v_model):   vec   = np.zeros(size).reshape((1,size))   count = 0.   for word in sentence:      try:         vec += w2v_model[word].reshape((1,size))         count += 1      except KeyError:         continue   if count != 0:      vec /= count   return vec
复制代码

1.3 TF-IDF加权平均法

TF-IDF加权平均法需要利用到TF-IDF技术,TF-IDF技术是一种常用的文本处理技术。TF-IDF模型常用评估一个词语对于一个文档的重要程度,经常应用于搜索技术和信息检索的领域。一个词语TF-IDF值与它在文档中出现频数成正比,与它在语料库中出现的频率成反比。TF-IDF由TF词频(Term Frequency)和IDF逆向文件频率(Inverse Document Frequency)相乘而得。

TF-IDF加权法不仅需要得到句子中每个非停用词的词向量,还需要得到句子中每个非停用词的TFIDF值。每个非停用词的TF部分还好计算,IDF部分就要看用户使用哪个语料库,如果是做query检索,那么IDF部分对应的语料库就是所有query句子;如果是做文本自相似聚类,那么IDF部分对应的语料库就是全体待分类句子。然后通过如下手段得到TF-IDF加权的的句子向量_:_

Vsentence = TFIDFword1*Vword1+TFIDFword2 *Vword2 +…… +TFIDFwordn *Vwordn

假设”我很开心.“ 是做query检索,那么计算IT-IDF对应的语料库就是全体query句子。若全体query句子一共有100个; 其中60个query句子含有词语我, 65个query句子含有词语很, 7个query句子含有词语开心。那么这句话中每个非停用词的TF-IDF数如下所示:

我: 1/(1+1+1) * log(100/(1+60)

很: 1/(1+1+1) * log(100/(1+65)

开心: 1/(1+1+1) * log(100/(1+7)

所以这句话的IT-IDF加权据向量为:

Vsentence = TFIDF*V+ TFIDF*V + …… + TFIDFg*V

TF-IDF代码:

s1_words=['今天','上','NLP','课程']s2_words=['今天','的','课程','有','意思']s3_words=['数据','课程','也','有','意思']data_set = [s1_words,s2_words,s3_words]from collections import Counterfrom collections import defaultdictword_dict = ['今天','上','NLP','的','课程','有','意思','数据','也']N=len(data_set)    #文档总数In_doc=defaultdict(int)  #表示含有该单词的文档数目for word in word_dict:    for doc in data_set:        In_doc[word]+=1tfidfs_all=[]for doc in data_set:    n=len(doc)  #文档中的单词总数    cont=Counter(doc)    tfidf=[]    for word in word_dict:        if word not in cont:            tfidf.append(0)        else:            tf=cont[word]/n            idf=In_doc[word]/N            tfidf.append(tf*idf)    tfidfs_all.append(tfidf)print(tfidfs_all)
复制代码

1.4 SIF嵌入法

ISF加权平均法和TF-IDF加权平均法类似,此方法可以很好的根据每个词词向量得到整个句子的句向量。SIF嵌入法需要利用主成分分析和每个词语的estimated probability, SIF嵌入法具体操作如下所示:

首先整个算法的输入有: (1) 每个词语的词向量 (2) 语料库中全体句子 (3) 可调参数a (4) 每个词语estimated probability

整个算法的输出为:
一个句子向量

算法的具体步骤是:
(1) 得到初步句向量

遍历语料库中每个句子,假设当前句子为s, 通过如下计算式子得到当前句子s的初步句向量:

                                       

即加权求平均的过程,每个词语向量乘以系数a/(a+p(w)后叠加,最后叠加向量处以句子s中词语的个数,对于可调参数a论文中作者使用0.001和0.0001两个。P(w)是词语在全体语料库中unigram probability,即词语w词频处以语料库所有词语词频之和。

(2) 主成分计算
全体初步句向量进行主成分分析,计算出全体初步句向量第一主成分u

(3) 得到目标句向量
通过如下计算时对初步句向量进行二次处理,得到目标句向量

只是粗略的跑了一下代码,里面主要为英文句子和单词,后期有实战机会再去研究,这里就偷个懒啦……….

代码:github.com/PrincetonML…

2 直接得到句向量

上文方法,有一个最大的缺陷,就是忽略了词语中间的顺序对整个句子的影响。

常用的直接得到句向量方法为Doc2Vec和Bert。

2.1 doc2vec

Doc2vec是在Word2vec的基础上做出的改进,它不仅考虑了词和词之间的语义,也考虑了词序。Doc2Vec模型Doc2Vec有两种模型,分别为:

句向量的分布记忆模型(PV-DM: Distributed Memory Model of Paragraph Vectors)

句向量的分布词袋(PV-DBOW: Distributed Bag of Words version of Paragraph Vector)PV-DM模型在给定上下文和文档向量的情况下预测单词的概率。

具体原理请参考其他资料进行补充,这里就不具体介绍了。

Doc2vec的DM模型跟Word2vec的CBOW很像,DBOW模型跟Word2vec的Skip-gram很像。Doc2Vec为不同长度的段落训练出同一长度的向量;不同段落的词向量不共享;训练集训练出来的词向量意思一致,可以共享。

代码:

#coding:utf-8import jiebaimport sysimport gensimimport sklearnimport numpy as npfrom gensim.models.doc2vec import Doc2Vec, LabeledSentenceTaggededDocument = gensim.models.doc2vec.TaggedDocument#进行中文分词def  cut_files(path):    f = open(path, "r", encoding='utf-8-sig')    res=[]    text=[]    for line in f:        res.append(line.split(' ', 1)[1])    for line in res:        curLine =' '.join(list(jieba.cut(line)))        text.append(curLine)    return text#读取分词后的数据并打标记,放到x_train供后续索引,但是这样的话占用很大内存(这种小数据量使用)def get_datasest(data):    x_train = []    for i, text in enumerate(data):        word_list = text.split(' ')        l = len(word_list)        word_list[l - 1] = word_list[l - 1].strip()        document = TaggededDocument(word_list, tags=[i])        x_train.append(document)    return x_train#模型训练def train(x_train, size=50):    model_dm = Doc2Vec(x_train, min_count=5, window=3, size=size, sample=1e-3, negative=5, workers=4)    model_dm.train(x_train, total_examples=model_dm.corpus_count, epochs=70)    model_dm.save('model_dm_doc2vec')    return model_dm#实例def test():    model_dm = Doc2Vec.load("model_dm_doc2vec")    test_text = ['我不想上班','我喜欢你']    text=[]    for line in test_text:        curLine =' '.join(list(jieba.cut(line)))        text.append(curLine)    inferred_vector_dm = model_dm.infer_vector(text)    sims = model_dm.docvecs.most_similar([inferred_vector_dm], topn=10)    return simsif __name__ == '__main__':    path="D:\\学习资料\\项目\\情感分析\\neg.txt"    path1="D:\\学习资料\\项目\\情感分析\\pos.txt"    text1=cut_files(path)    text2=cut_files(path1)    text=text1+text2    x_train=get_datasest(text)    model_dm = train(x_train)    sims = test()    for count, sim in sims:        sentence = x_train[count]        words = ''        for word in sentence[0]:            words = words + word + ' '        print (words, sim, len(sentence[0]))
复制代码

2.2 Bert

采用由词向量通过各种方法后处理得到句向量有一个最大的弊端——那就是无法理解上下文的语义,同一个词在不同的语境意思可能不一样,但是却会被表示成同样的词向量,这样对含多义词句子的语义计算产生不利的影响。而Bert生成的句向量没有上述的缺陷,同一个词语在不同语境下的向量是不同的,多义词因为上下文环境的不同而产生不同的向量,这样bert生产的句向量可以真正应用于语义计算中。BERT生成句向量的优点不仅在于可理解句意,并且排除了词向量加权引起的误差。

代码:

from bert_serving.client import BertClientimport numpy as npdef main():    bc = BertClient()    doc_vecs = bc.encode(['今天天空很蓝,阳光明媚', '今天天气好晴朗', '现在天气如何', '自然语言处理', '机器学习任务'])    print(len(doc_vecs[0]))if __name__ == '__main__':    main()
复制代码

现在一般的bert方法主要是下载github上面的中文模型进行调用,然后得到词向量。

BERT使用的是Transformer模型,使用的是transformer的encoder部分。那它是怎么解决语言模型只能利用一个方向的信息的问题呢?答案是它的pretraining训练的不是普通的语言模型,而是Mask语言模型。

BERT模型要求有一个固定的Sequence的长度,比如128。如果不够就在后面padding,否则就截取掉多余的Token,从而保证输入是一个固定长度的Token序列。

后期有时间专门研究一下bert

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享