fastText 文本分类器 | Bag of Tricks for Efficient Text Classification

释放双眼,带上耳机,听听看~!
本文介绍了由Facebook开源的fastText文本分类器,以及其高效的文本分类功能和与深度学习的比较。

“我正在参加「掘金·启航计划」”

fastText

论文 | Bag of Tricks for Effificient Text Classifification

链接 | arxiv.53yu.com/abs/1607.01…

作者 | Armand Joulin, Edouard Grave, Piotr Bojanowski, Tomas Mikolov

发布时间 | 2016

一、概述

尽管深度学习神经网络在自然语言处理方面表现出色,但它们通常需要数十层甚至上亿个参数,因此速度较慢且需要大量计算资源的支持,这限制了它们的应用场景。相比之下,fastText是一种由Facebook开源的简单而高效的文本分类器,采用浅层神经网络实现了word2vec和文本分类功能。fastText与深度网络在效果上几乎相当,但更加节省资源,并且速度提高了百倍,因此可以被视为高效的工业级解决方案。

fastText是在负采样的Skip-Gram模型基础上,进一步的将每个中心词视作子词的集合,并学习该子词的词向量。

简单理解该方案:

以like这个词为例,设子词为2个字符,则子词包括”<l” , “li”,”ik”,”ke”, “e>” 和特殊子词”<like>”,其中 “<” 和 “>” 是为了将作为前后缀的子词区分出来。而且,这里的子词 “her” 与整词 “<her>” 也可被区分。给定一个词 ww, 通常我们的习惯于将字符长度在 3 到 6 之间的所有子词和特殊子词的并集 Gwmathcal{G}_w 取出。假设词典中任意子词 gg 的子词向量为 zgboldsymbol{z}_g ,我们可以把使用负采样的 skip-gram 模型的损失函数

−log⁡P(wo∣wc)=−log⁡11+exp⁡(−uoTvc)−∑k=1,wk∼P(w)Klog⁡11+exp⁡(uikTvc)-log Pleft(w_o mid w_cright)=-log frac{1}{1+exp left(-boldsymbol{u}_o^T boldsymbol{v}_cright)}-sum_{k=1, w_k sim P(w)}^K log frac{1}{1+exp left(boldsymbol{u}_{i_k}^T boldsymbol{v}_cright)}

直接替换成

−log⁡P(wo∣wc)=−log⁡11+exp⁡(−uoT∑g∈Gwczg)−∑k=1,wk∼P(w)Klog⁡11+exp⁡(uikT∑g∈Gwczg)begin{aligned}
& -log Pleft(w_o mid w_cright)=
& -log frac{1}{1+exp left(-boldsymbol{u}_o^T sum_{g in mathcal{G}_{w_c}} boldsymbol{z}_gright)}-sum_{k=1, w_k sim P(w)}^K log frac{1}{1+exp left(boldsymbol{u}_{i_k}^T sum_{g in mathcal{G}_{w_c}} boldsymbol{z}_gright)}
end{aligned}

不难看出,原中心词向量被替换成了中心词的子词向量之和,这与整词学习(word2vec, Glove)不同,并且词典以外的新词也可以使用fastText中相应的子词向量之和来表达。

二、Why 提出 fastText

Word2Vec的局限

Word2Vec不能处理训练中未出现的词(Out of Vocabulary, OOV)

例如:tensor, flow已经在Word2Vec词典出现,但tensorflow未出现过 –> OOV Error

fastText 文本分类器 | Bag of Tricks for Efficient Text Classification

无法处理形态相同的词(morphology),即词根相同的词

对于具有相同词根(eat)的词, eaten,eating,eats,可能难以同时出现,实际训练就容易将其当成独一无二的词,也就是语义类似的词无法做到参数共享。

Shared    radicaleat    eats    eaten    eater    eatingShared ;;radical
eat ;; eattextcolor{red}s ;; eattextcolor{red}{en} ;; eattextcolor{red}{er} ;; eattextcolor{red}{ing}

为了解决以上问题,fastText提出

三、fastText提出

直觉(intuition):借鉴n-gram的思想,考虑字符级别的信息

使用单词内部语义用以改善Word2Vec的语义表示

sub-word的产生

Step 1:给定一个词,并且在词首尾添加上<>表示开始与结束

eating    →    <eating>eating;; → ;; textcolor{red}<eatingtextcolor{red}>

Step 2:设定n-gram的滑动窗口大小 n=3(可以设置为其他值),对词进行滑动

<eating><eatitextcolor{red}{ng>}

Step 3:当n=3, 4, 5, 6时所得到的n-grams列表

WordLength(n)Charactern−grams eating 3 <ea, east, ati, tin, ing, ng>  eating 4 <eat, eati, atin, ting, ing>  eating 5 <eati, eatin, ating, ting>  eating 6 <eatin, eating, ating> begin{aligned}
&begin{array}{lll}
Word & Length(n) & Character n-grams
text { eating } & 3 & text { <ea, east, ati, tin, ing, ng> }
hline text { eating } & 4 & text { <eat, eati, atin, ting, ing> }
hline text { eating } & 5 & text { <eati, eatin, ating, ting> }
hline text { eating } & 6 & text { <eatin, eating, ating> }
end{array}
end{aligned}

问题:这样会存在大量的唯一的n-grams

Hash

由于 n-gram 的量远比 word 大的多,完全存下所有的 n-gram 不太现实。fastText 采用的是 Hash 桶的方式,把所有的 n-gram 映射到 buckets 个桶中,而映射到相同桶的 n-gram 共享同一个 embedding vector,如下图所示

fastText 文本分类器 | Bag of Tricks for Efficient Text Classification

图中 Win 代表整个 Embedding 矩阵,其中前 V 行是 word Embedding,后 Buckets 行是 n-gram Embedding,每个 n-gram 通过 hash 函数之后映射到 0~Bucket-1 位置,得到对应的 embedding 向量。用哈希的方式既能保证查找时 O (1) 的效率,又可能把内存消耗控制在 O (buckets * dim) 范围内。不过这种方法潜在的问题是存在哈希冲突,不同的 n-gram 可能会共享同一个 embedding。如果桶大小取的足够大,这种影响会很小

具体实现过程为负采样的Skip-Gram模型,唯一的区别就是对中心词的子词化的处理

优劣势分析

fastText 对于形态丰富的语言较重要,例如阿拉伯语、德语和俄语。例如,德语中有很多复合词,例如乒乓球(英文 table tennis)在德语中叫 “Tischtennis”。fastText 可以通过子词表达两个词的相关性,例如 “Tischtennis” 和 “Tennis”,特别是当训练预料规模较小的时候,提升尤为出色。

 word2vec-skipgram  word2vec-cbow  fasttext  Czech 52.855.077.8 German 44.545.056.4 English 70.169.974.9 Italian 51.551.862.7begin{array}{|l|l|l|l|}
hline & text { word2vec-skipgram } & text { word2vec-cbow } & text { fasttext }
hline text { Czech } & 52.8 & 55.0 & mathbf{7 7 . 8}
hline text { German } & 44.5 & 45.0 & mathbf{5 6 . 4}
hline text { English } & 70.1 & 69.9 & mathbf{7 4 . 9}
hline text { Italian } & 51.5 & 51.8 & mathbf{6 2 . 7}
hline
end{array}

但与Word2Vec相比,FastText降低了语义类比的任务性能,随着语料库规模的增加,两者的差异越来越小。

 word2vec-skipgram  word2vec-cbow  fasttext  Czech 25.727.627.5 German 66.566.862.3 English 78.578.277.8 Italian 52.354.752.3begin{array}{|c|c|c|c|}
hline & text { word2vec-skipgram } & text { word2vec-cbow } & text { fasttext }
hline text { Czech } & 25.7 & 27.6 & 27.5
hline text { German } & 66.5 & 66.8 & 62.3
hline text { English } & 78.5 & 78.2 & 77.8
hline text { Italian } & 52.3 & 54.7 & 52.3
hline
end{array}

  • fastText比常规的skipgram慢1.5倍,因为增加了n-grams的开销
  • 在单词相似度任务中,带有n-grams的sub-word信息比CBOW和Skip-gram基线具有更好的性能。

另外,如果遇到一个新词,对于 fastText 来说,它可以从训练集中找出这个新词的所有子词向量,然后做个求和,就能算出这个新词的词向量了

四、实践

1.使用官方CLI工具

Word representations · fastText

2.使用gensim的fasttext

FastText Model — gensim (radimrehurek.com)

3.使用PaddlePaddle进行简单的尝试!

『论文复现系列』4.FastText:aistudio.baidu.com/aistudio/pr…

清洗数据集

from collections import Counter
from sklearn.model_selection import train_test_split
 
import pandas as pd

data=pd.read_csv(r"./ag_news/train.csv",header=None,encoding="utf-8")
train,val=train_test_split(data,test_size=0.2,shuffle=True)
# 我们这里只取新闻描述,直接使用验证集充当测试集,验证集不参与训练
train_x=list(train.iloc[:,2])
train_y=list(train.iloc[:,0])
val_x=list(val.iloc[:,2])
val_y=list(val.iloc[:,0])

# 数据清洗
# 目的是保留每个句子中的所有单词与空格
def clear(data):
    for i, s in enumerate(data):
        clears = ""
        # 遍历当前句子每一个字符
        for char in s:
            # 若字符是字母或空格则保留
            if(char.isalpha() or char == " "):
                clears += char
        data[i] = clears
        
#拼接所有句子,制作词典

document = ""
for s in train_x:
    document += " " + str(s).lower()

# 按照空格拆分,并进行简单的数据清洗
clear_d = []
for word in document.split(" "):
    if(word.isalpha()):
        clear_d.append(word)

# Counter函数统计词频
freq = dict(Counter(clear_d))
idx2word = ["<pad>"] + list(freq.keys()) + ["<unk>"]
word2idx = { w:idx for idx, w in enumerate(idx2word)}
vocab_size = len(idx2word)

del freq

def corpus_encode(corpus):
    # 分词操作
    for i in range(len(corpus)):
        # 每个句子直接分词
        s1 = corpus[i]
        sentences = corpus[i].lower().split(" ")

        # 存储当前句子的编码信息
        c = []
        for word in sentences:
            # 是单词才编码
            if(word.isalpha()):
                # 用词典中的索引代表该单词,若不在词典则用UNK的索引代表该单词
                c.append(word2idx.get(word, word2idx["<unk>"]))
            # if(len(c)==0):
                # print("原来的句子", s1)
                # print("分解以后的句子", sentences)

            # 编码代替原先的句子
            corpus[i] = c
            
# set集合,大括号中每组词向量为二维list
Features = set()

def word_N_gram(s, N = 2, train = False):
    features = []
    for i in range(len(s) - N + 1):
        f = str(s[i : i+N])
        if train:
            Features.add(f) # 训练阶段应该统计有哪些特征
        # 将一句话的每个词进一步拆分成多个小的词向量,例如like  --> li ik ke
        features.append(f)
    return features

# 生成每个句子的n-gram特征
train_n_gram = []
for s in train_x:
    # 已有的词向量与又拆分的二元词向量累加作为训练的词向量
    train_n_gram.append(word_N_gram(s, train=True))
val_n_gram = []
for s in val_x:
    val_n_gram.append(word_N_gram(s))

# 给n-gram特征建立索引表
idx2ngram = ["[<pad>]"] + list(Features) + ["[<unk>]"]
ngram2idx = {w:c for c, w in enumerate(idx2ngram)}
ngram_size = len(idx2ngram)

# 给特征进行编码
def encode_gram(n_gram):
    for i, s in enumerate(n_gram):
        feature = []
        for word in s:
            feature.append(ngram2idx.get(word, ngram2idx["[<unk>]"]))
        n_gram[i] = feature
encode_gram(train_n_gram)
encode_gram(val_n_gram)

class mydata(Dataset):
    def __init__(self, train_x, train_Ngram, train_y, lensX, lensNGram):
        super(mydata, self).__init__()
        self.train_x = train_x
        self.train_Ngram = train_Ngram
        self.train_y = train_y
        self.lensX = lensX
        self.lensNGram = lensNGram
    
    def __len__(self):
        return len(self.train_x)

    def __getitem__(self, idx):
        return self.train_x[idx], self.train_Ngram[idx], self.lensX[idx], self.lensNGram[idx], self.train_y[idx]
    
    
# 模型深度
d_model = 50
# 类别数量
class_num = 4
ngram_size = len(ngram2idx)

模型结构

class cbow(Layer):
    def __init__(self, vocab_size, ngram_size, d_model, class_num) -> None:
        super(cbow, self).__init__()
        self.vocab_size = vocab_size
        self.d_model = d_model
        self.class_num = class_num
        # torch.nn.Embedding(num_embeddings, embedding_dim, padding_idx=None)
        # num_embeddings (int) – size of the dictionary of embeddings
        # padding_idx 指定pad的索引,指定之后该索引不会随着训练的更新而更新
        self.embed1 = nn.Embedding(vocab_size, d_model, padding_idx=0)
        self.embed2 = nn.Embedding(ngram_size, d_model, padding_idx=0)
        self.linear = nn.Linear(d_model, class_num)


    def forward(self, x, ngram, lensX, lenNgram):
        # x[batch, maxlen1], ngram[batch, maxlen2], lensX[batch], lenNgram[batch]
        # Embedding的参数维度通常是放在bts之后,与Embedding扩展维度之前
        x = self.embed1(x) # x[batch, maxlen1, d_model]
        ngram = self.embed2(ngram) # ngram[batch, maxlen1, d_model]
        # 词向量求和
        x = paddle.sum(x, axis=1) # x[batch, d_model]
        ngram = paddle.sum(ngram, axis=1) # ngram[batch, d_model]
        x = x + ngram
        lens = lensX + lenNgram
        lens = lens.unsqueeze(1) # lens[batch, 1]
        # 取均值
        x /= lens
        output = self.linear(x)
        return output

训练

for epoch in range(epochs):
    #总损失
    allloss=0
    for step,(x,ngram,lenx,lenNgram,y) in enumerate(dataloader):
        output=model(x,ngram,lenx,lenNgram)
        #计算极大似然函数损失
        loss=lossCul(output,y)
        optimize.clear_grad()
        loss.backward()
        optimize.step()
        
        allloss+=loss
        if((step+1)%500==0):
            print("epochs:",epoch+1," iter:",step+1," loss:",allloss/(step+1))

五、总结

Fasttext模型优缺点

优点:

  1. 速度非常快,并且效果还可以。
  2. 有开源实现,可以快速上手使用。

缺点:

  1. 模型结构简单,所以目前来说,不是最优的模型。
  2. 因为使用词袋思想,所以语义信息获取有限。

论文总结

关键点:

  1. 基于深度学习的文本分类方法效果好,但是速度比较慢
  2. 基于线性分类器的机器学习方法效果还行,速度也比较快,但是需要做烦琐的特征工程
  3. Fasttext模型的提出

创新点:

  1. 提出了一种新的文本分类模型—Fasttext模型
  2. 利用了一些加快文本分类和使得文本分类效果更好的技巧——层次softmax和n-gram特征。
  3. 在文本分类和tag预测两个任务上得到了又快又好的结果。

参考资料:

本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。
AI教程

GPT-4:人工智能新一代语言模型

2023-12-13 19:51:14

AI教程

Transfomer:NLP领域的革命性模型

2023-12-13 20:00:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索