赞
踩
一般来说,NLP 任务的基本流程主要包括以下几个:
文本预处理:文本清理、标准化、纠错、改写等。
Tokenzing:分字、分子词、分词等。
构造数据:将 Token 转为模型需要的输入。
文本特征表征:将输入的数据转换为向量表征。
结果输出:将向量表征转换为最终输出。
对于文本特征表征,有三类方法今天谈谈,主角是 Embedding。
OneHot
TF-IDF
Embedding
对于自然语言,因为它的输入是一段文本,在中文里就是一个一个字,或一个一个词,行业内把这个字或词叫Token。如果要使用模型,拿到一段文本的第一件事就是把它Token化,当然,可以按字、也可以按词,或按你想要的其他方式,比如每两个字一组(Bi-Gram)。举个例子:
给定文本:我们相信AI可以让世界变得更美好。
按字Token化:我/们/相/信/A/I/可/以/让/世/界/变/得/更/美/好/。
按词Token化:我们/相信/AI/可以/让/世界/变得/更/美好/。
按Bi-Gram Token化:我们/们相/相信/信A/AI/I可/可以/以让/让世/世界/界变/变得/得更/更美/美好/好。
那自然就有一个新的问题:我们应该怎么选择Token化方式?其实每种不同的方法都有自己的优点和不足,在大模型之前,按词的方式比较常见。但在有了大模型之后,基本都是按字来了,不用再纠结这个点了。
Token化后,第二件事就是要怎么表示这些Token,我们知道计算机只能处理数字,所以要想办法把这些Token给变成计算机「认识」的数字才行。读者不妨思考一下如果要你来做这件事会怎么做。
可以认为(个人觉得),Token给变成计算机「认识」的数字 称之为 embedding。
embedding就是用一个低维的向量表示一个物体,可以是一个词,或是一个商品,或是一个电影等等。这个embedding向量的性质是能使距离相近的向量对应的物体有相近的含义,比如 Embedding(复仇者联盟)和Embedding(钢铁侠)之间的距离就会很接近,但 Embedding(复仇者联盟)和Embedding(乱世佳人)的距离就会远一些。
除此之外Embedding甚至还具有数学运算的关系,比如Embedding(马德里)-Embedding(西班牙)+Embedding(法国)≈Embedding(巴黎)
从另外一个空间表达物体,甚至揭示了物体间的潜在关系,上次体会这样神奇的操作还是在学习傅里叶变换的时候,从某种意义上来说,Embedding方法甚至具备了一些本体论的哲学意义。
言归正传,Embedding能够用低维向量对物体进行编码还能保留其含义的特点非常适合深度学习,属于文本特征技术
2013年没有word2vec时,大家用onehot;2013年以后大家就用了多维度向量了。
在传统机器学习模型构建过程中,我们经常使用one hot encoding对离散特征。具体来说:
把所有字作为一个字典,序号就代表它自己。我们还是以上面的句子为例,假设词表就包含上面那些字,那么词表就可以用一个txt文件存储,内容如下:
我 们 相 信 A I 可 以 让 世 界 变 得 更 美 好 。
一行一个字,每个字作为一个Token,此时,0=我,1=们,……,以此类推。拿中文来说,这个词表可能只要几千行,即使包含各种特殊符号、生僻字,也就2万个多点,我们假设词表大小为N。
接下来我们考虑如何用这些数字来表示一段文本。最简单的方法就是用它的ID直接串起来,这样也不是不行,但这种表示方法的特征是一维的,也就是说只能表示一个特征。这种方法不太符合实际情况,效果也不理想。所以,研究人员就想到另一种表示方法:One-Hot编码。其实,将文本变为数字表示的过程本质上就是一种编码过程。One-Hot的意思是,对每一个字都有N(词表大小)个特征,除了该字的ID位置值为1,其余都为0。我们依然用上面的例子来说明,把整个词表表示为下面的形式:
我 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
们 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
相 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0
信 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0
……下面省略
此时,对每一个Token(字),它的表示就变成了一个一维向量,比如「我」:[1,0,…0],这个向量的长度就是词表的大小N,它被称为「我」这个字的One-Hot表示。
对于一段文本,我们一般会将每个Token的表示结合起来,结合方式可以采用求和或平均。这样,对于任意长度的任意文本,我们都能将其表示为固定大小的向量,非常方便进行各种矩阵或张量(三维以上的数组)计算,这对深度学习至关重要。
举个例子,比如有这么一句话:让世界更美好。现在我们使用刚刚的方法将其表示为一个向量,采用平均的方式。
首先,列出每个字的向量:
让 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0
世 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0
界 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0
更 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0
美 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0
好 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
。 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
然后针对每一列取平均,结果为:0 0 0 0 0 0 0 1/7 1/7 1/7 0 0 1/7 1/7 1/7 1/7
。不难发现,对任两句话,只要其中包含的字不完全一样,最终得到的向量表示也不会完全一样。注①
当然,在实际使用时,往往不会这么简单的用1/0来表示,因为每个字在句子中的作用是不一样的,所以一般会给不同的Token赋予不同的权重。最常见的方法是使用在句子中出现的频率,那些高频的(但不是「的」「更」这样的虚词)被认为是重要的。
聪明的您一定会想到,既然可以用「出现」或「不出现」来表示,那为啥不用频率呢?没错,我们可以将上面的 1 都换成该词在每个句子中出现的频率,归一化后就是概率。由于自然语言的「齐夫定律」,高频词会很自然地占据了主导地位,这对向量表示大有影响,想象一下,那些概率较高的词几乎在所有句子中出现,它们的值也会更大,从而导致最终的向量表示向这些词倾斜。
齐夫定律:在自然语言语料库中,一个单词出现的频率与它在频率表里的排名成反比。即第 n 个常见的频率是最常见频率的 1/n。举个例子来说,中文的「的」是最常见的词,排在第 1 位,比如第 10 位是「我」,那「我」的频率就是「的」频率的 1/10。
于是,很自然地就会想到使用某种方式去平和这种现象,让真正的高频词凸显出来,而降一些类似的、是这种常见词的影响下降。咱们很自然就会想到,能不能把这些常见词从词表给剔除掉。哎,是的。这种类似的常见词有个专业术语叫「停用词」,一般呢,停用词主要是虚词居多,包括助词、连词、介词、语气词等,但也可能会包括一些「没用」的实词,需要根据实际情况处理。
除了停用词,还有另一种更巧妙的手段处理这个问题——TF-IDF,TF=Term Frequency,IDF=Inverse Document Frequency。TF 就是刚刚提到的词频,这不是有些常用词吗,这时候 IDF 来了,它表示「有多少个文档包含那个词」,具体等于文档总数/包含该词的文档数
。比如「的」在一句话(或一段文档)中概率很高,但几乎所有句子(或文档)都有「的」,IDF 接近 1;相反如果一个词在句子中概率高,但包含该词的文档比较少,IDF 就比较大,最后结果也大。而这是满足我们预期的——词在单个文档或句子中是高概率的,但在所有文档或句子中是低概率的,这不正说明这个词对所在文档或句子比较重要吗。实际运算时,一般会取对数,并且防止 0 除:
这时候的向量虽然看着不是 OneHot,但其实本质还是,只是在原来是 1 的位置,变成了一个小数。
这种方法不错,在深度学习之前很长一段时间里都是这样的。主要是基于「计数」进行表示(共现矩阵),这一时期的代表是 TF-IDF ,不过计数方法没有考虑上下文顺序,而且维度往往较高(词表大小),需要使用类似 SVD 这样的降维方法。需要说明的是,这个计数既可以在整个文档上进行,也可以在一个范围的滑动窗口内进行。
不过它有两个很大的问题:
在传统机器学习模型构建过程中,我们经常使用one hot encoding对离散特征,特别是id类特征进行编码,但由于one hot encoding的维度等于物体的总数,比如阿里的商品one hot encoding的维度就至少是千万量级的。这样的编码方式对于商品来说是极端稀疏的,甚至用multi hot encoding对用户浏览历史的编码也会是一个非常稀疏的向量。而深度学习的特点以及工程方面的原因使其不利于稀疏特征向量的处理(这里希望大家讨论一下为什么?)。因此如果能把物体编码为一个低维稠密向量再喂给DNN,自然是一个高效的基本操作。
终于轮到我们的主角Embedding登场了,它的主要思想是这样的:
还是继续以前面的例子来说明,这时候词表的表示变成下面这样了:
我 0.xxx0, 0.yyy0, 0.zzz0, ... D个小数
们 0.xxx1, 0.yyy1, 0.zzz1, ... D个小数
相 0.xxx2, 0.yyy2, 0.zzz2, ... D个小数
信 0.xxx3, 0.yyy3, 0.zzz3, ... D个小数
……下面省略
2013年,Google开源了一款用于词向量计算的工具——word2vec,引起了工业界和学术界的关注。首先,word2vec可以在百万数量级的词典和上亿的数据集上进行高效地训练;其次,该工具得到的训练结果——词向量(word embedding),可以很好地度量词与词之间的相似性。随着深度学习(Deep Learning)在自然语言处理中应用的普及,很多人误以为word2vec是一种深度学习算法。其实word2vec算法的背后是一个浅层神经网络。另外需要强调的一点是,word2vec是一个计算word vector的开源工具。当我们在说word2vec算法或模型的时候,其实指的是其背后用于计算word vector的CBoW模型和Skip-gram模型。很多人以为word2vec指的是一个算法或模型,这也是一种谬误。
「词向量」是一个划时代的成果,为啥这么说呢?因为它真正把自然语言词汇表征成一个可计算、可训练的表示,带来的直接效果就是自然语言一步跨入了深度学习时代——Embedding 后可以接各种各样的模型架构,完成复杂的计算。
使embedding空前流行的“word2vec”
到了深度学习时代,我们一定会首先想到 2013 年的 Word2Vec——没错,句子是由词构成的,有了词向量,句子向量自然而然呼之欲出了。
直觉看,直接对每个词的向量拼接、求和、逐元素相乘、取平均值或最大值,得到的向量都可以表示为句子向量。当然,这里面也有一些花样,比如加权重求和:权重可以根据词性、句法结构等设定一个固定值,然后对每个位置的词向量乘权重再求和;权重也可以根据输入向量来,输出向量的_每个元素_都根据输入元素向量进行加权求和。
对word的vector表达的研究早已有之,但让embedding方法空前流行,我们还是要归功于google的word2vec。我们简单讲一下word2vec的原理,这对我们之后理解AirBnB对loss function的改进至关重要。
word2vec 的 论文标题:Efficient Estimation of Word Representations in Vector Space
论文链接:https://arxiv.org/pdf/1301.3781.pdf
复现代码地址:https://github.com/wellinxu/nlp_store/blob/master/papers/word2vec.py
作者: Tomas Mikolov, Kai Chen, Greg Corrado, Jeffrey Dean
问题 : 如何在一个大型数据集上快速、准确地学习出词表示?
模型:
传统的NNLM模型包含四层,即输入层、映射层、隐含层和输出层,计算复杂度很大程度上依赖于映射层到隐含层之间的计算,而且需要指定上下文的长度。RNNLM模型被提出用来改进NNLM模型,去掉了映射层,只有输入层、隐含层和输出层,计算复杂度来源于上一层的隐含层到下一层隐含层之间的计算。
本文提出的两个模型CBOW (Continuous Bag-of-Words Model)和Skip-gram (Continuous Skip-gram Model)结合了上面两个模型的特点,都是只有三层,即输入层、映射层和输出层。CBOW模型与NNLM模型类似,用上下文的词向量作为输入,映射层在所有的词间共享,输出层为一个分类器,目标是使当前词的概率最大。Skip-gram模型与CBOW的输入跟输出恰好相反,输入层为当前词向量,输出层是使得上下文的预测概率最大,如下图所示。训练采用SGD。
相关工作
Bengio[1]在2003年就提出了language model的思路,同样是三层(输入层,隐含层和输出层)用上下文的词向量来预测中间词,但是计算复杂度较高,对于较大的数据集运行效率低;实验中也发现将上下文的n-gram出现的频率结合进去会提高性能,这个优点体现在CBOW和Skip-gram模型的输出层中,用hierarchical softmax(with huffman trees)来计算词概率。
简评
本文的实验结果显示CBOW比NNLM在syntactic和semantic上的预测都要好,而Skip-gram在semantic上的性能要优于CBOW,但是其计算速度要低于CBOW。结果显示用较大的数据集和较少的epoch,可以取得较好的效果,并且在速度上有所提升。与LSI和LDA相比,word2vec利用了词的上下文,语义信息更加丰富。基于word2vec,出现了phrase2vec, sentence2vec和doc2vec,仿佛一下子进入了embedding的世界。NLP的这些思想也在用于recommendation等方面,并且与image结合,将image跟text之间进行转换。
Distributed Representations of Sentences and Documents
作者 :Quoc V. Le, Tomas Mikolov
问题 :基于word2vec的思路,如何表示sentence和document?
模型
利用one-hot的表示方法作为网络的输入,乘以词矩阵W,然后将得到的每个向量通过平均或者拼接的方法得到整个句子的表示,最后根据任务要求做一分类,而这过程中得到的W就是词向量矩阵,基本上还是word2vec的思路。
接下来是段落的向量表示方法:
依旧是相同的方法,只是在这里加上了一个段落矩阵,用以表示每个段落,当这些词输入第i个段落时,通过段落id就可以从这个矩阵中得到相对应的段落表示方法。需要说明的是,在相同的段落中,段落的表示是相同的。文中这样表示的动机就是段落矩阵D可以作为一个memory记住在词的context中遗失的东西,相当于增加了一个额外的信息。这样经过训练之后,我们的就得到了段落表示D,当然这个段落就可以是一段或者一篇文章。
最后一种就是没有词序的段落向量表示方法:
从图中就可以感觉到这个方法明显和skip-gram非常相似,这里只是把重点放在了段落的表示中,通过段落的表示,来预测相应的context 词的表示。最后我们依然可以得到段落矩阵D,这样就可以对段落进行向量化表示了。但是输入起码是句子级别的表示,而输出则是词的向量表示,因此个人比较怀疑这种方法的合理性。
简评
这篇文章是word2vec的方法提出一年后提出的方法,因此本文并没有使用目前非常流行的word2vec的训练方法来训练词向量,而是利用word2vec的思路,提出了一种更加简单的网络结构来训练任意长度的文本表示方法。这样一方面好训练,另一方面减少了参数,避免模型过拟合。优点就是在训练paragraph vector的时候加入了一个paragraph matrix,这样在训练过程中保留了一部分段落或者文档信息。这点在目前看来也是有一定优势的。但是目前深度学习发展迅速,可以处理非常大的计算量,同时word2vec以及其变种被应用得非常普遍,因此该文章提出的方法思路大于模型,思路我们可以借鉴,模型就不具有优势了。
在这种简单操作词向量得到句子向量的搞法中最有名的就是 FastText,它是用词向量的平均值作为句子向量的,以此为基础的分类模型(FastText 自带的分类模型)长期以来都是 Baseline。另外还有一个很直观的做法,就是利用训练 Word2Vec 的方法,将「句子」看成「词」,这样得到的就是句子向量了。不过这一类做法都有一些弊病,比如简单平均或求和会损失很多信息;将句子当做词训练需要对每个文档都进行训练,泛化能力太弱。
对于 FastTExt,来源论文《Enriching Word Vectors with Subword Information》
作者:Piotr Bojanowski, Edouard Grave, Armand Joulin, Tomas Mikolov
单位:Facebook AI Research
问题:如何解决word2vec方法中罕见词效果不佳的问题,以及如何提升词形态丰富语言的性能?
模型
word2vec在词汇建模方面产生了巨大的贡献,然而其依赖于大量的文本数据进行学习,如果一个word出现次数较少那么学到的vector质量也不理想。针对这一问题作者提出使用subword信息来弥补这一问题,简单来说就是通过词缀的vector来表示词。比如unofficial是个低频词,其数据量不足以训练出高质量的vector,但是可以通过un+official这两个高频的词缀学习到不错的vector。
方法上,本文沿用了word2vec的skip-gram模型,主要区别体现在特征上。word2vec使用word作为最基本的单位,即通过中心词预测其上下文中的其他词汇。而subword model使用字母n-gram作为单位,本文n取值为3~6。这样每个词汇就可以表示成一串字母n-gram,一个词的embedding表示为其所有n-gram的和。这样我们训练也从用中心词的embedding预测目标词,转变成用中心词的n-gram embedding预测目标词。
实验分为三个部分,分别是(1)计算两个词之间的语义相似度,与人类标注的相似度进行相关性比较;(2)与word2vec一样的词类比实验;(3)与其他考虑morphology的方法比较。结果是本文方法在语言形态丰富的语言(土耳其语,法语等)及小数据集上表现优异,与预期一致。
资源
源码公布在Facebook的fastText项目中:
https://fasttext.cc/
https://github.com/facebookresearch/fastText
相关工作
利用语言形态学来改进nlp的研究源远流长,本文提及的许多关于character-level和morphology的有趣工作值得参考。
简评
文章中提出的思路对于morphologically rich languages(例如土耳其语,词缀的使用极为普遍而有趣)来说十分有意义。词缀作为字母与单词之间的中层单位,本身具有一定的语义信息。通过充分利用这种中层语义来表征罕见词汇,直观上讲思路十分合理,也是应用了compositionality的思想。
利用形态学改进word embedding的工作十分丰富,但中文NLP似乎很难利用这一思路。其实个人感觉中文中也有类似于词缀的单位,比如偏旁部首等等,只不过不像使用字母系统的语言那样容易处理。期待今后也有闪光的工作出现在中文环境中。
从Word2Vec到FastText,从word representation到sentence classification,Tomas Mikolov的工作影响了很多人。虽然有个别模型和实验结果曾遭受质疑,但终究瑕不掩瑜。word2vec对NLP的研究起到了极大地推动作用,其实不仅仅是在NLP领域中,在其他很多领域中都可以看到word2vec的思想和作用,也正是从word2vec开始。
(转载)最后,我想简单阐述下我对word embedding的几点思考。不一定正确,也欢迎大家提出不同的意见。
Word embedding最早出现于Bengio在03年发表的开创性文章中[3]。通过嵌入一个线性的投影矩阵(projection matrix),将原始的one-hot向量映射为一个稠密的连续向量,并通过一个语言模型的任务去学习这个向量的权重。这一思想后来被广泛应用于包括word2vec在内的各种NLP模型中。
Word embedding的训练方法大致可以分为两类:一类是无监督或弱监督的预训练;一类是端对端(end to end)的有监督训练。
无监督或弱监督的预训练以word2vec和auto-encoder为代表。这一类模型的特点是,不需要大量的人工标记样本就可以得到质量还不错的embedding向量。不过因为缺少了任务导向,可能和我们要解决的问题还有一定的距离。因此,我们往往会在得到预训练的embedding向量后,用少量人工标注的样本去fine-tune整个模型。
相比之下,端对端的有监督模型在最近几年里越来越受到人们的关注。与无监督模型相比,端对端的模型在结构上往往更加复杂。同时,也因为有着明确的任务导向,端对端模型学习到的embedding向量也往往更加准确。例如,通过一个embedding层和若干个卷积层连接而成的深度神经网络以实现对句子的情感分类,可以学习到语义更丰富的词向量表达。
Word embedding的另一个研究方向是在更高层次上对sentence的embedding向量进行建模。
我们知道,word是sentence的基本组成单位。一个最简单也是最直接得到sentence embedding的方法是将组成sentence的所有word的embedding向量全部加起来——类似于CBoW模型。
显然,这种简单粗暴的方法会丢失很多信息。
另一种方法借鉴了word2vec的思想——将sentence或是paragraph视为一个特殊的word,然后用CBoW模型或是Skip-gram进行训练[12]。这种方法的问题在于,对于一篇新文章,总是需要重新训练一个新的sentence2vec。此外,同word2vec一样,这个模型缺少有监督的训练导向。
个人感觉比较靠谱的是第三种方法——基于word embedding的端对端的训练。Sentence本质上是word的序列。因此,在word embedding的基础上,我们可以连接多个RNN模型或是卷积神经网络,对word embedding序列进行编码,从而得到sentence embedding。
首先需要明确,Embedding 需要训练的!不然都是随机数。
在模型训练过程中,会根据不同的上下文不断地更新这个参数,最后模型训练完后得到的这个矩阵就是Token的表示。我们完全可以把它当成一个黑盒子,输入一个X,根据标签Y不断更新参数,最终就得到一组参数,这些参数的名字就叫「模型」。
这种表示方法在深度学习早期(2013-2015年左右),由于这个矩阵训练好后就固定不变了,这在有些时候就不合适。比如「你好坏」这句话在不同的情况下可能完全是不同的意思。
我们在第六节“6 pytorch中nn.Embedding原理及使用”描述这个过程
我们知道,句子才是语义的最小单位,因此相比Token,我们其实更加关注和需要句子的表示,我们期望可以根据不同上下文动态地获得句子表示。这中间当然经历了比较多的探索,一直到如今的大模型时代,对模型输入任意一句话,它都能给我们返回一个非常不错的表示,而且依然是固定长度的向量。
我们在第七节“7 大模型的Embedding原理及使用”描述这个过程
我们总结一下,Embedding本质就是一组稠密向量,用来表示一段文本(可以是字、词、句、段等),获取到这个表示后,我们就可以进一步做一些任务。大家不妨先思考一下,当给定任意句子并获得到它的固定长度的语义表示时,我们可以干什么?
对于输出的 Embedding 向量,例如我们 词表大小N=16,维度D=256,输出长这个样子
这个数组的size是 (16, 256)
array([[0.77395605, 0.43887844, 0.85859792, ..., 0.24783956, 0.23666236,
0.74601428],
[0.81656876, 0.10527808, 0.06655886, ..., 0.11585672, 0.07205915,
0.84199321],
[0.05556792, 0.28061144, 0.33413004, ..., 0.00925978, 0.18832197,
0.03128351],
...,
[0.50647331, 0.22303613, 0.94414565, ..., 0.79202324, 0.40169878,
0.72247782],
[0.9151384 , 0.80071297, 0.39044651, ..., 0.03994193, 0.79502741,
0.28297954],
[0.68255979, 0.64272531, 0.65262805, ..., 0.18645529, 0.21927175,
0.32320729]])
与Embedding息息相关的一个概念是「相似度」,准确来说是「语义相似度」。在自然语言处理领域,我们一般使用cosine相似度作为语义相似度的度量,评估两个向量在语义空间上的分布情况。
具体来说就是下面这个式子:
import numpy as np
a = [0.1, 0.2, 0.3]
b = [0.2, 0.3, 0.4]
cosine_ab = (0.1*0.2+0.2*0.3+0.3*0.4)/(np.sqrt(0.1**2+0.2**2+0.3**2) * np.sqrt(0.2**2+0.3**2+0.4**2))
cosine_ab
>> 0.9925833339709301
在PyTorch中,针对词向量有一个专门的层nn.Embedding,用来实现词与词向量的映射。
nn.Embedding具有一个权重(.weight),形状是(num_words, embedding_dim)。例如一共有10个词,每个词用2维向量表征,对应的权重就是一个10×2的矩阵。
Embedding的输入形状N×W,N是batch size,W是序列的长度,输出的形状是N×W×embedding_dim。
输入必须是LongTensor,FloatTensor需通过tensor.long()方法转成LongTensor。
Embedding的权重是可以训练的,既可以采用随机初始化,也可以采用预训练好的词向量初始化。
# coding:utf8
import torch as t
from torch import nn
if __name__ == '__main__':
embedding = nn.Embedding(10, 2) # 10个词,每个词用2维词向量表示
input = t.arange(0, 6).view(3, 2).long() # 3个句子,每句子有2个词
input = t.autograd.Variable(input)
output = embedding(input)
print(output.size())
print(embedding.weight.size())
您可能会有疑问或好奇:参数都是随机的,最后输出的分类不对怎么办?这个其实就是模型的训练过程了。简单来说,最后输出的概率分布会和实际的标签做一个比对,然后这个差的部分会通过「反向传播算法」不断沿着模型网络往回传,从而可以更新随机初始化的参数,直到最终的输出和标签相同或非常接近为止。此时,我们再用训练好的网络参数计算,就会得到正确的标签。
torch.nn.Embedding(num_embeddings, embedding_dim, padding_idx=None,
max_norm=None, norm_type=2.0, scale_grad_by_freq=False,
sparse=False, _weight=None)
其为一个简单的存储固定大小的词典的嵌入向量的查找表,意思就是说,给一个编号,嵌入层就能返回这个编号对应的嵌入向量,嵌入向量反映了各个编号代表的符号之间的语义关系。
输入为一个编号列表,输出为对应的符号嵌入向量列表。
参数解释
num_embeddings (python:int) – 词典的大小尺寸,比如总共出现5000个词,那就输入5000。此时index为(0-4999)
embedding_dim (python:int) – 嵌入向量的维度,即用多少维来表示一个符号。
padding_idx (python:int, optional) – 填充id,比如,输入长度为100,但是每次的句子长度并不一样,后面就需要用统一的数字填充,而这里就是指定这个数字,这样,网络在遇到填充id时,就不会计算其与其它符号的相关性。(初始化为0)
max_norm (python:float, optional) – 最大范数,如果嵌入向量的范数超过了这个界限,就要进行再归一化。
norm_type (python:float, optional) – 指定利用什么范数计算,并用于对比max_norm,默认为2范数。
scale_grad_by_freq (boolean, optional) – 根据单词在mini-batch中出现的频率,对梯度进行放缩。默认为False.
sparse (bool, optional) – 若为True,则与权重矩阵相关的梯度转变为稀疏张量。
下面是关于Embedding的使用
torch.nn包下的Embedding,作为训练的一层,随模型训练得到适合的词向量。
#建立词向量层
embed = torch.nn.Embedding(n_vocabulary,embedding_size)
找到对应的词向量放进网络:词向量的输入应该是什么样子
实际上,上面通过随机初始化建立了词向量层后,建立了一个“二维表”,存储了词典中每个词的词向量。每个mini-batch的训练,都要从词向量表找到mini-batch对应的单词的词向量作为RNN的输入放进网络。那么怎么把mini-batch中的每个句子的所有单词的词向量找出来放进网络呢,输入是什么样子,输出是什么样子?
首先我们知道肯定先要建立一个词典,建立词典的时候都会建立一个dict:word2id:存储单词到词典序号的映射。假设一个mini-batch如下所示:
['I am a boy.','How are you?','I am very lucky.']
显然,这个mini-batch有3个句子,即batch_size=3
第一步首先要做的是:将句子标准化,所谓标准化,指的是:大写转小写,标点分离,这部分很简单就略过。经处理后,mini-batch变为:
[['i','am','a','boy','.'],['how','are','you','?'],['i','am','very','lucky','.']]
可见,这个list的元素成了一个个list。还要做一步:将上面的三个list按单词数从多到少排列。标点也算单词。至于为什么,后面会说到。
那就变成了:
batch = [['i','am','a','boy','.'],['i','am','very','lucky','.'],['how','are','you','?']]
可见,每个句子的长度,即每个内层list的元素数为:5,5,4。这个长度也要记录。
lens = [5,5,4]
之后,为了能够处理,将batch的单词表示转为在词典中的index序号,这就是word2id的作用。转换过程很简单,假设转换之后的结果如下所示,当然这些序号是我编的。
batch = [[3,6,5,6,7],[6,4,7,9,5],[4,5,8,7]]
同时,每个句子结尾要加EOS,假设EOS在词典中的index是1。
batch = [[3,6,5,6,7,1],[6,4,7,9,5,1],[4,5,8,7,1]]
那么长度要更新:
lens = [6,6,5]
很显然,这个mini-batch中的句子长度不一致!所以为了规整的处理,对长度不足的句子,进行填充。填充PAD假设序号是2,填充之后为:
batch = [[3,6,5,6,7,1],[6,4,7,9,5,1],[4,5,8,7,1,2]]
就可以直接取词向量训练了吗?
不能!上面batch有3个样例,RNN的每一步要输入每个样例的一个单词,一次输入batch_size个样例,所以batch要按list外层是时间步数(即序列长度),list内层是batch_size排列。即batch的维度应该是:
[seq_len,batch_size]
[seq_len,batch_size]
[seq_len,batch_size]
重要的问题说3遍!
怎么变换呢?变换方法可以是:使用itertools模块的zip_longest函数。而且,使用这个函数,连填充这一步都可以省略,因为这个函数可以实现填充!
batch = list(itertools.zip_longest(batch,fillvalue=PAD))
# fillvalue就是要填充的值,强制转成list
经变换,结果应该是:
batch = [[3,6,4],[6,4,5],[5,7,8],[6,9,7],[7,5,1],[1,1,2]]
记得我们还记录了一个lens:
lens = [6,6,5]
batch还要转成LongTensor:
batch=torch.LongTensor(batch)
这里的batch就是词向量层的输入。
词向量层的输出是什么样的?
好了,现在使用建立了的embedding直接通过batch取词向量了,如:
embed_batch = embed (batch)
假设词向量维度是6,结果是:
tensor([[[-0.2699, 0.7401, -0.8000, 0.0472, 0.9032, -0.0902], [-0.2675, 1.8021, 1.4966, 0.6988, 1.4770, 1.1235], [ 0.1146, -0.8077, -1.4957, -1.5407, 0.3755, -0.6805]], [[-0.2675, 1.8021, 1.4966, 0.6988, 1.4770, 1.1235], [ 0.1146, -0.8077, -1.4957, -1.5407, 0.3755, -0.6805], [-0.0387, 0.8401, 1.6871, 0.3057, -0.8248, -0.1326]], [[-0.0387, 0.8401, 1.6871, 0.3057, -0.8248, -0.1326], [-0.3745, -1.9178, -0.2928, 0.6510, 0.9621, -1.3871], [-0.6739, 0.3931, 0.1464, 1.4965, -0.9210, -0.0995]], [[-0.2675, 1.8021, 1.4966, 0.6988, 1.4770, 1.1235], [-0.7411, 0.7948, -1.5864, 0.1176, 0.0789, -0.3376], [-0.3745, -1.9178, -0.2928, 0.6510, 0.9621, -1.3871]], [[-0.3745, -1.9178, -0.2928, 0.6510, 0.9621, -1.3871], [-0.0387, 0.8401, 1.6871, 0.3057, -0.8248, -0.1326], [ 0.2837, 0.5629, 1.0398, 2.0679, -1.0122, -0.2714]], [[ 0.2837, 0.5629, 1.0398, 2.0679, -1.0122, -0.2714], [ 0.2837, 0.5629, 1.0398, 2.0679, -1.0122, -0.2714], [ 0.2242, -1.2474, 0.3882, 0.2814, -0.4796, 0.3732]]], grad_fn=<EmbeddingBackward>)
维度的前两维和前面讲的是一致的。可见多了一个第三维,这就是词向量维度。所以,Embedding层的输出是:
[seq_len,batch_size,embedding_size]
大模型更加智能,可以根据上下文进行词向量计算,比 nn.Embedding 更加好用。
import os
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
import openai
# OPENAI_API_KEY = "填入专属的API key"
openai.api_key = OPENAI_API_KEY
text = "我喜欢你"
model = "text-embedding-ada-002"
emb_req = openai.Embedding.create(input=[text], model=model)
emb = emb_req.data[0].embedding
len(emb), type(emb)
from openai.embeddings_utils import get_embedding, cosine_similarity
# 注意它默认的模型是text-similarity-davinci-001,我们也可以换成text-embedding-ada-002
text1 = "我喜欢你"
text2 = "我钟意你"
text3 = "我不喜欢你"
emb1 = get_embedding(text1)
emb2 = get_embedding(text2)
emb3 = get_embedding(text3)
len(emb1), type(emb1)
(12288, list)
cosine_similarity(emb1, emb2)
0.9246855139297101
cosine_similarity(emb1, emb3)
0.8578009661644189
接下来我们用万能的ChatGPT尝试一下,注意它不会给你返回Embedding,它是尝试直接告诉你答案!
content = "请告诉我下面三句话的相似程度:\n1. 我喜欢你。\n2. 我钟意你。\n3.我不喜欢你。\n"
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": content}]
)
response.get("choices")[0].get("message").get("content")
输出:
'\n\n1和2相似,都表达了对某人的好感或喜欢之情。而3则与前两句截然相反,表示对某人的反感或不喜欢。'
牛逼,不过这个格式不太好,我们调整一下:
content += '第一句话用a表示,第二句话用b表示,第三句话用c表示,请以json格式输出两两相似度,类似下面这样:\n{"ab": a和b的相似度}'
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": content}]
)
response.get("choices")[0].get("message").get("content")
'\n\n{"ab": 0.8, "ac": -1, "bc": 0.7}\n\n解释:a和b的相似度为0.8,因为两句话表达了相同的情感;a和c的相似度为-1,因为两句话表达了相反的情感;b和c的相似度为0.7,因为两句话都是表达情感,但一个是积极情感,一个是消极情感,相似度略低。'
https://blog.csdn.net/weixin_44493841/article/details/95341407
https://blog.csdn.net/baidu_24536755/article/details/86551434
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。