fastText是Facebook AI Research推出的文本分类工具,它也可以用于训练词向量、句向量等,其最大的特点是模型简洁,训练速度快且文本分类准确率也令人满意(这也是fastText名称的由来)!这篇文章会回顾fastText的内容,包括模型结构、N-gram特征、层次Softmax等,然后使用Pytorch实现一个toy版fastText进行实践。
引言众所周知,word2vec是fastText的“哥哥”,因为它们都是Thomas Mikolov大神的杰作。Thomas Mikolov当年在Google的时候,带领团队搞出来一个word2vec,很好地改进了传统词向量表示的问题。后来,Thomas Mikolov去了Fackbook,才有了fastText的诞生。
模型结构word2vec包括两种模型CBOW和Skip-gram,由于fastText模型结构几乎与CBOW如出一辙,因此本文也会介绍CBOW模型结构,有关word2vec的详细内容(尤其是Skip-gram模型)请参考天雨粟的文章 。
CBOW模型结构如图1.a所示,其基本思想为用上下文来预测目标单词。对于多个上下文 ,CBOW模型将其上下文简化为多个上下文向量组成的词嵌入的sum, 即 。然后用这个上下文向量 去预测目标单词,即 .
显然,CBOW在计算词向量的时候丢失了上下文成分之间的顺序信息。
图1.a: CBOW模型结构
fastText模型结构与CBOW非常相似,但又有所不同,如图1.b所示!fastText使用所有的单词和相应的N-gram特征作为输入,并学习一个可以表示句子的向量表示,然后用于预测句子类别。假设fastText的输入为 ,则 , ,其中 表示输入到隐层的权重, 表示隐层到输出层的权重。
图1.b: Fasttext模型结构
对比图1中CBOW和fastText模型结构,可以发现,fastText和CBOW模型都只有三层:输入层、隐层和输出层。不同地是,fastText中增加了N-gram特征,这使得fastText可以关注到词序信息。
N-gram特征N-gram思想源于语言模型,指将文本内容按照顺序进行大小为N的窗口滑动操作,形成文本片段序列。根据粒度的不同,N-gram可以分为字符粒度(用于英文等)、字或词粒度和词语粒度。fastText中常用字符粒度和字粒度的N-gram。在英文文档分类中,使用字符粒度的N-gram是非常有帮助的。
例如,对于单词”hello”,长度至少为3的字符粒度的N-gram有”hel”,”ell”,”llo”,”hell”,”ello”以及”hello”。每个N-gram都可以用一个稠密向量(嵌入向量) 表示,于是整个单词”hello”就可以被表示为 ,详情参见图2.
图2: Enriching Word Vectors with Subword Information
把每个word拆成多个字符级的N-gram的优点也是显而易见的,它可以丰富词的表示,即便对于那些从没见过的word,也能学到一个还不错的向量表示(因为从未见过的word中也会出现常见的字符级的N-gram序列)。
层次SoftmaxfastText的另一功能就是进行文本分类,fastText的模型结构如图1.b所示,非常简单。由于fastText是分类问题,自然选择 函数获得类别的概率分布。因此,fastText的损失函数可使用多分类交叉熵损失。
当目标类别数量比较少时,直接使用softmax函数并没有效率问题,但当类别数量很大(设为 )时,因为需要对 个数值进行归一化,softmax的计算就会占据大量时间。为了加速训练,fastText采用与word2vec类似的层次softmax方法优化时间效率。
层次softmax的基本思想是根据类别的频率构造霍夫曼树来代替扁平化的标准softmax。通过层次softmax,获得概率分布的时间复杂度可以从 降至 .
图3为层次softmax的一个具体示例。
图3:层次softmax示例
(在图3中, )个不同的类别组成所有的叶子节点, 个内部节点作为内部参数,从根节点到某个叶子节点经过的节点和边形成一条路径,路径被表示为 ,路径概率为
其中:
表示Sigmoid函数;
是Softmax的输入
表示n节点的左孩子;
是中间节点 的参数;
是一个特殊的函数,被定义为:
图3中,加粗的节点和边是从根节点到 即 的路径,路径概率 可以被表示为:
于是,从根节点走到叶子节点 ,实际上进行了3次二分类的逻辑回归。
总结
相比于Bert等方法,fastText具有如下优点:
训练速度快,精度相当;Pytorch-fastText实践
不需要预训练的词向量;
N-gram特征关注了词序信息;
层次化Softmax优化了时间效率。
为了帮助读者理解fastText的模型结构,我基于Pytorch构建了一个toy版本的fastText,仅供参考。
import torch
from torch.autograd._functions import tensor
import torch.nn as nn
import torch.nn.functional as F
class fastText(nn.Module):
def __init__(self, vocab_size, twoGrams_size, threeGrams_size, embed_size, hidden_size, output_size, embedding_pretrained=None):
super(fastText, self).__init__()
#Embedding layer
if embedding_pretrained is None:#default
self.embedding_word = nn.Embedding(vocab_size, embed_size)
else:#使用预训练词向量
self.embedding_word = nn.Embedding.from_pretrained(embedding_pretrained, freeze=False)
#self.embedding_word.weight.requires_grad = True
self.embedding_2gram = nn.Embedding(twoGrams_size, embed_size)
self.embedding_3gram = nn.Embedding(threeGrams_size, embed_size)
self.dropout = nn.Dropout(p=0.5)
#Hidden layer
self.hidden = nn.Linear(embed_size, hidden_size)
#Output layer
self.output = nn.Linear(hidden_size, output_size)
def forward(self, x):
"""Args: Tensor
x[0]: word
x[1]: 2grams
x[2]: 3grams
"""
#x: (word, 2_gram, 3_gram), word, 2_gram和3_gram形状都是(batch_size, *)
e_word = self.embedding_word(x[0])#e_word: (batch_size, seq_len_word, embed_size)
e_2gram = self.embedding_2gram(x[1])#e_2gram: (batch_size, seq_len_2gram, embed_size)
e_3gram = self.embedding_3gram(x[2])#e_3gram: (batch_size, seq_len_3gram, embed_size)
e_cat = torch.cat((e_word, e_2gram, e_3gram), dim=1)
e_avg = e_cat.mean(dim=1)
h = self.hidden(self.dropout(e_avg))#input: (batch_size, embed_size), h:(batch_size, hidden_size)
o = F.softmax(self.output(h), dim=1)#o: (batch_size, output_size)
return o, {
"embedding_word": e_word,
"embedding_2gram": e_2gram,
"embedding_3gram": e_3gram,
"e_cat": e_cat,
"e_avg": e_avg,
"hidden": h
}
toy版fastText模型结构非常简洁明了,如图4所示,其包含了3个embedding层(为了更加区分词嵌入、2-gram嵌入和3-gram嵌入,示例中写了3个嵌入层,其实可以将其写成1个),嵌入层后面是一个隐藏层即全连接层,输入为嵌入向量的均值,最后是输出层,也是一个全连接层,进行类别分类。
图4: toy版fastText模型结构
假设词典大小、2gram词典大小、3gram词典大小分别为10、20、30;嵌入维度、隐层维度和分类类别分别为128、256和16;字符'h', 'e', 'l', 'o'的索引分别为1,2,3,4;2gram字符组'he', 'el', 'll', 'lo'的索引分别为1,2,3,4;3gram字符组'hel', 'ell', 'llo'的索引分别为1,2,3。
当输入如图4所示时,模型的返回结果如下:
tmp["embedding_word"]表示'h', 'e', 'l', 'o'的嵌入结果
tmp["embedding_2gram"]表示'he', 'el', 'll', 'lo' 的嵌入结果
tmp["embedding_3gram"]表示 'hel', 'ell', 'llo'的嵌入结果
嵌入维度均为128,然后将其平均,即可获得表征输入的唯一向量,在本例中为tmp["e_avg"],再将其送至隐层和输出层,获得类别概率分布。
vocab_size = 10
twoGrams_size = 20
threeGrams_size = 30
embed_size = 128
hidden_size = 256
output_size = 16
ft = fastText(vocab_size, twoGrams_size, threeGrams_size, embed_size, hidden_size, output_size)
print(ft)
x_0 = torch.LongTensor([[1,2,3,3,5]])#batch_size = 1, seq_len = 5
x_1 = torch.LongTensor([[1,2,3,4]])#batch_size =1, seq_len = 4
x_2 = torch.LongTensor([[1,2,3]])#batch_size=1, seq_len=3
x = (x_0, x_1, x_2)
output, tmp = ft(x)
print("embedding_word:", tmp["embedding_word"].size())
print("embedding_2gram:", tmp["embedding_2gram"].size())
print("embedding_3gram:", tmp["embedding_3gram"].size())
print("e_cat:", tmp["e_cat"].size())
print("e_avg:", tmp["e_avg"].size())
print("hidden:", tmp["hidden"].size())
print("output", output.size())
# fastText(
# (embedding_word): Embedding(10, 128)
# (embedding_2gram): Embedding(20, 128)
# (embedding_3gram): Embedding(30, 128)
# (dropout): Dropout(p=0.5, inplace=False)
# (hidden): Linear(in_features=128, out_features=256, bias=True)
# (output): Linear(in_features=256, out_features=16, bias=True)
# )
# embedding_word: torch.Size([1, 5, 128])
# embedding_2gram: torch.Size([1, 4, 128])
# embedding_3gram: torch.Size([1, 3, 128])
# e_cat: torch.Size([1, 12, 128])
# e_avg: torch.Size([1, 128])
# hidden: torch.Size([1, 256])
# output torch.Size([1, 16])
以上内容,就是使用Pytorch对fastText的一次toy实践了,想使用fastText进行学习的同学可以参考fastText官网文档!
更多面试题可以关注星空智能对话机器人的Gavin的公开课
以往公开课选段视频如下:
GavinNLP星空对话机器人Transformer课程片段3:以对话机器人的流式架构为例阐述Transformer学习的第三境界
星空智能对话机器人的Gavin认为Transformer是拥抱数据不确定性的艺术。
Transformer的架构、训练及推理等都是在Bayesian神经网络不确定性数学思维下来完成的。Encoder-Decoder架构、Multi-head注意力机制、Dropout和残差网络等都是Bayesian神经网络的具体实现;基于Transformer各种模型变种及实践也都是基于Bayesian思想指导下来应对数据的不确定性;混合使用各种类型的Embeddings来提供更好Prior信息其实是应用Bayesian思想来集成处理信息表达的不确定性、各种现代NLP比赛中高分的作品也大多是通过集成RoBERTa、GPT、ELECTRA、XLNET等Transformer模型等来尽力从最大程度来对抗模型信息表示和推理的不确定性。
从数学原理的角度来说,传统Machine Learning及Deep learning算法训练的目标函数一般是基于Naive Bayes数学原理下的最大似然估计MLE和最大后验概率MAP来实现,其核心是寻找出最佳的模型参数;而Bayesian的核心是通过计算后验概率Posterior的predictive distribution,其通过提供模型的不确定来更好的表达信息及应对不确定性。对于Bayesian架构而言,多视角的先验概率Prior知识是基础,在只有小数据甚至没有数据的时候是主要依赖模型Prior概率分布(例如经典的高斯分布)来进行模型推理,随着数据的增加,多个模型会不断更新每个模型的参数来更加趋近真实数据的模型概率分布;与此同时,由于(理论上)集成所有的模型参数来进行Inference,所以Bayesian神经网络能够基于概率对结果的提供基于置信度Confidence的分布区间,从而在各种推理任务中更好的掌握数据的不确定性。
当然,由于Bayesian模型因为昂贵的CPU、Memory及Network的使用,在实际工程实践中计算Bayesian神经网络中所有概率模型分布P(B)是棘手的甚至是Intractable的几乎不能实现事情,所以在工程落地的时候会采用Sampling技术例如MCMC的Collapsed Gibbs Sampling、Metropolis Hastings、Rejection Sampling及Variational Inference的Mean Field及Stochastic等方法来降低训练和推理的成本。Transformer落地Bayesian思想的时候权衡多种因素而实现最大程度的近似估计Approximation,例如起使用了计算上相对CNN、RNN等具有更高CPU和内存使用性价比的Multi-head self-attention机制来完成更多视角信息集成的表达,在Decoder端训练时候一般也会使用多维度的Prior信息完成更快的训练速度及更高质量的模型训练,在正常的工程落地中Transformer一般也会集成不同来源的Embeddings,例如星空智能对话机器人的Transformer实现中就把One-hot encoding、Word2vec、fastText、GRU、BERT等encoding集成来更多层级和更多视角的表达信息。
拥抱数据不确定性的Transformer基于Bayesian下共轭先验分布conjugate prior distribution等特性形成了能够整合各种Prior知识及多元化进行信息表达、及廉价训练和推理的理想架构。理论上讲Transformer能够更好的处理一切以 “set of units” 存在的数据,而计算机视觉、语音、自然语言处理等属于这种类型的数据,所以理论上讲Transformer会在接下来数十年对这些领域形成主导性的统治力。
基于此,我们开发了全新一代围绕Transformer的自然语言处理课程:
NLP on Transformers 101
(基于Transformer的NLP智能对话机器人实战课程)
本课程以Transformer架构为基石、萃取NLP中最具有使用价值的内容、围绕手动实现工业级智能业务对话机器人所需要的全生命周期知识点展开,学习完成后不仅能够从算法、源码、实战等方面融汇贯通NLP领域NLU、NLI、NLG等所有核心环节,同时会具备独自开发业界领先智能业务对话机器人的知识体系、工具方法、及参考源码,成为具备NLP硬实力的业界Top 1%人才。
课程特色:
101章围绕Transformer而诞生的NLP实用课程
5137个围绕Transformers的NLP细分知识点
大小近1200个代码案例落地所有课程内容
10000+行纯手工实现工业级智能业务对话机器人
在具体架构场景和项目案例中习得AI相关数学知识
以贝叶斯深度学习下Attention机制为基石架构整个课程
五大NLP大赛全生命周期讲解并包含比赛的完整代码实现



