FastText与之前介绍过的CBOW架构相似,我们先来会议一下CBOW架构,如下图:
CBOW的任务是通过上下文去预测中间的词,具体做法是使用滑动窗口内部的词的embedding的均值作为中间词的embedding。
FastText的任务是通过文章中的词去预测文章的类别(文档分类),具体做法是使用文章中的所有词的embedding的均值作为文章的embedding。最后从隐层再经过一次的非线性变换得到输出层的label。
CBOW和FastText的相似之处:
总结一下CBOW和FastText的不同之处:
从模型架构上来说,沿用了CBOW的单层神经网络的模式,不过fastText的处理速度才是这个算法的创新之处。
fastText模型的输入是一个词的序列(一段文本或者一句话),输出是这个词序列属于不同类别的概率。在序列中的词和词组构成特征向量,特征向量通过线性变换映射到中间层,再由中间层映射到标签。fastText在预测标签时使用了非线性激活函数,但在中间层不使用非线性激活函数。
fastText是一个快速文本分类算法,与基于神经网络的分类算法相比有两大优点:
fastText
方法包含三部分,模型架构,层次Softmax
和N-gram
特征。
分层 softmax(Hierarchical Softmax)是一种用于加速词嵌入模型训练的技术,特别是在训练大型词汇表时。它通过将词汇表组织成一棵二叉树(通常是霍夫曼树),从而将原来的线性 softmax 运算转换为对树结构进行的多次二元分类,从而减少了计算量。
构建哈夫曼树
对数学模型进行改造:
预测过程:
我们发现对于每一个节点,都是一个二分类[0,1],也就是我们可以使用sigmod来处理节点信息;
θ
(
x
)
=
1
1
+
e
−
x
\theta \left(x \right)=\frac{1}{1+e{-x}}
θ(x)=1+e−x1
此时,当我们知道了目标单词x,之后,我们只需要计算root节点,到该词的路径累乘,即可. 不需要去遍历所有的节点信息,时间复杂度变为O(log2(V))。
n-gram是基于语言模型的算法,基本思想是将文本内容按照字节顺序进行大小为N的窗口滑动操作,最终形成窗口为N的字节片段序列。而且需要额外注意一点是n-gram可以根据粒度不同有不同的含义,有字粒度的n-gram和词粒度的n-gram,下面分别给出了字粒度和词粒度的例子:
#我爱中国
2-gram特征为:我爱 爱中 中国
3-gram特征为:我爱中 爱中国
#我 爱 中国
2-gram特征为:我/爱 爱/中国
3-gram特征为:我/爱/中国
从上面来看,使用n-gram有如下优点
但正如上面提到过,随着语料库的增加,内存需求也会不断增加,严重影响模型构建速度,针对这个有以下几种解决方案:
1、过滤掉出现次数少的单词
2、使用hash存储
3、由采用字粒度变化为采用词粒度
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
class Config(object):
"""配置参数"""
def __init__(self):
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设备
self.dropout = 0.5 # 随机失活
self.require_improvement = 1000 # 若超过1000batch效果还没提升,则提前结束训练
self.num_classes = 10 # 类别数
self.n_vocab = 10000 # 词表大小,在运行时赋值
self.num_epochs = 20 # epoch数
self.batch_size = 128 # mini-batch大小
self.pad_size = 32 # 每句话处理成的长度(短填长切)
self.learning_rate = 1e-3 # 学习率
self.embed = 300 # 字向量维度
self.hidden_size = 256 # 隐藏层大小
self.n_gram_vocab = 250499 # ngram 词表大小
'''Bag of Tricks for Efficient Text Classification'''
class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()
self.embedding = nn.Embedding(config.n_vocab, config.embed, padding_idx=config.n_vocab - 1)
self.embedding_ngram2 = nn.Embedding(config.n_gram_vocab, config.embed)
self.embedding_ngram3 = nn.Embedding(config.n_gram_vocab, config.embed)
self.dropout = nn.Dropout(config.dropout)
self.fc1 = nn.Linear(config.embed * 3, config.hidden_size)
self.fc2 = nn.Linear(config.hidden_size, config.num_classes)
def forward(self, x):
out_word = self.embedding(x[0]) # x[0] [batch_size,sentence_len] 经过embedding变为 [batch_size,sentence_len,wmbed_size] torch.Size([128, 32, 300])
out_bigram = self.embedding_ngram2(x[2]) # torch.Size([128, 32, 300])
out_trigram = self.embedding_ngram3(x[3]) # torch.Size([128, 32, 300])
out = torch.cat((out_word, out_bigram, out_trigram), -1) # torch.Size([128, 32, 900])
out = out.mean(dim=1) # torch.Size([128, 900]),沿着第二个维度(即特征维度)对每个样本的特征值进行平均池化
out = self.dropout(out) # torch.Size([128, 900])
out = self.fc1(out) # torch.Size([128, 900])经过fc1 torch.Size([128, 256])
out = F.relu(out) # torch.Size([128, 256])
out = self.fc2(out) # torch.Size([128, 256])经过fc1 torch.Size([128, 10])
return out
config=Config()
model=Model(config)
print(model)
输出:
Model(
(embedding): Embedding(10000, 300, padding_idx=9999)
(embedding_ngram2): Embedding(250499, 300)
(embedding_ngram3): Embedding(250499, 300)
(dropout): Dropout(p=0.5, inplace=False)
(fc1): Linear(in_features=900, out_features=256, bias=True)
(fc2): Linear(in_features=256, out_features=10, bias=True)
)