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) 得到目标句向量
通过如下计算时对初步句向量进行二次处理,得到目标句向量
只是粗略的跑了一下代码,里面主要为英文句子和单词,后期有实战机会再去研究,这里就偷个懒啦……….
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