Bag of Tricks for Efficient Text Classification论文阅读及实战

本文目录(仅做浏览用,简书中的MarkDown暂时不支持页面内跳转)

技术交流QQ群:1027579432,欢迎你的加入!

一、Fasttext算法综述

  • Fasttext是Facebook AI Research2016年推出的文本分类和词训练工具,其源码已经托管在Github上。Fasttext最大的特点是模型简单,只有一层的隐层以及输出层,因此训练速度非常快,在普通的CPU上可以实现分钟级别的训练,比深度模型的训练要快几个数量级。同时,在多个标准的测试数据集上,Fasttext在文本分类的准确率上,和现有的一些深度学习的方法效果相当或接近。

二、原理介绍及优化策略

    1. Fasttext算法的主要功能有两个:
    • (1). 训练词向量:词向量的训练相对与word2vec来说增加了subwords特性。subwords其实就是一个词的字符级的n-gram。例如单词"hello",长度至少为3的char-level的ngram有”hel”,”ell”,”llo”,”hell”,”ello”以及本身”hello”。每个n-gram都可以用一个dense的向量zg表示,于是整个单词”hello”就可以表示表示为:
      公式一.png

      具体细节可以参考论文Enriching Word Vectors with Subword Information那么把每个word,拆成若干个char-level的ngram表示有什么好处呢?答案是:丰富了词表示的层次。例如:”english-born”和”china-born”,从单词层面上看,是两个不同的单词,但是如果用char-level的n-gram来表示,都有相同的后缀”born”。因此这种表示方法可以学习到当两个词有相同的词缀时,其语义也具有一定的相似性。这种方法对英语等西方语言来说可能是奏效的。因为英语中很多相同前缀和后缀的单词,语义上确实有所相近。但对于中文来说,这种方法可能会有些问题。比如说,”原来”和”原则”,虽有相同前缀,但意义相去甚远。可能对中文来说,按照偏旁部首等字形的方式拆解可能会更有意义一些。
    • (2). 文本分类:Fasttext的另一个功能是做文本分类。主要的原理在论文Bag of Tricks for Efficient Text Classification中有所阐述。其模型结构简单来说,就是一层word embedding的隐层+输出层。结构如下图所示:
      图1 Fasttext模型.png

      图2 CBOW模型.png

      图1是Fasttext的网络结构,其中W1到Wn表示document中每个词的word embedding表示。文章则可以用所有词的embedding累加后的均值表示,即:
      公式二.png

      最后从隐层再经过一次的非线性变换得到输出层的label。对比word2vec中的图2中的CBOW模型(continuous bag of word),可以发现两个模型之前非常的相似。不同之处在于,fasttext模型最后预测的是文章的类别label,而CBOW模型预测的是窗口中间的词w(t),Fasttext是有监督的学习,CBOW是无监督的学习。另外,CBOW模型中输入层只包括当前窗口内除中心词外的所有上下词汇,而fasttext模型中输入层是文章中的所有词。
    1. 网络结构介绍及模型的优化trick
    • 2.1模型结构介绍
      • 和word2vec类似,fasttext本质上也可以看成是一个浅层的神经网络,因此其前向传播过程可描述如下:


        前向传播.png

        其中,z是最后输出层的输入向量,Wo表示从隐藏层到输出层的权重

      • 由于模型的最后输出是要预测的文本属于某个类别的概率,因此很自然的使用标准的softmax层,见下图:


        标准softmax.png

        标准的softmax层用于多分类任务中,它将多个神经元的输出映射到(0,1)之间,可以当作概率来理解,从而进行多分类。在最后选取输出节点时,选取概率最大的节点作为最终的预测值。定义的交叉熵损失函数CE(y_target,y_predict)如下:


        损失函数.png
    • 2.2 模型优化的几个trick
      • 2.2.1当类别数较少时,直接套用softmax层并没有效率问题,但是当类别很多时,softmax层的计算就比较费时。为了加快训练过程,Fasttext同样也采用了和word2vec类似的方法。一种方法是使用hierarchical softmax,当类别数为K,word embedding的维度大小为d时,计算复杂度可以从O(Kd)降到O(dlog(K))。


        分层softmax.jpg

        因为在标准的softmax回归中,要计算y=j时的softmax概率P(y=i),需要对所有的K个概率做归一化,这在类别很大是会很耗时,于是分层softmax诞生了。分层softmax的基本思想:使用树的层级结构替代扁平化的标准softmax,使得在计算P(y=j)时,只需要计算一路路径上的所有节点的概率值即可,不需在意其他的节点。树的结构是根据类别的频数构造的霍夫曼树。K个不同的类别组成所有的叶子节点,K-1个内部节点作为内部参数,从根节点到某个叶子节点和边构成一条路径,路径的长度表示为L(yj)。于是,P(yj)可以写成下式:


        类别概率.png

        其中:
        • sigmoid():表示sigmoid函数
        • LC(n):表示n节点的左孩子
        • [[x]]是一个特殊的函数,定义如下:


          特殊函数x.png
        • theta(yj,l):表示中间节点n(yj,l)的参数,X是softmax层的输入
          上图中,高亮的节点和边是从根节点到y2的路径,路径长度是L(y2)=4,所以P(y2)可表示为:


          最后的概率.png

          于是,从根节点到叶子节点y2,实际上作出3次的二分类的逻辑回归,通过分层的softmax,计算复杂度从O(K)降到O(log(K))。

      • 2.2.2 另一种方法是采用negative sampling,即每次从除当前label(正样本)外的其他label(负样本)中选择几个作为负样本,作为出现负样本的概率加到损失函数中,用公式可表达为:


        优化后的损失函数.png

        其中,hi是第i个样本的隐藏层神经元数量,uj是Wo中第j行向量

      • 2.2.3 n-gram特征解决Fasttext模型丢失词汇顺序的问题,因为隐层是通过简单的求和取平均得到的。为了弥补这个不足,Fasttext增加了N-gram的特征。具体做法:把N-gram当成一个词,也用embedding向量来表示,在计算隐层时,把N-gram的embedding向量也加进去求和取平均。举个例子来说,假设某篇文章只有3个词,W1,W2,W3,N-gram的N=2即是bigram,w1、w2、w3以及w12、w23分别表示词W1、W2、W3和bigram W1-W2,W2-W3的embedding向量,那么文章的隐层可表示为:


        n-gram.png

        通过反向传播算法,就可以同时学到词的Embeding和n-gram的Embedding,具体的实现上,由于n-gram的量远比word大的多,完全存下所有的n-gram也不现实。Fasttext采用了Hash桶的方式,把所有的n-gram都哈希到buckets个桶中,哈希到同一个桶的所有n-gram共享一个embedding vector。如下图所示:


        Hash Buckets.png

        图中Win是Embedding矩阵,每行代表一个word或N-gram的词向量,其中前V行是word embeddings,后Buckets行是n-grams embeddings。每个n-gram经哈希函数哈希到0-bucket-1的位置,得到对应的embedding向量。用哈希的方式既能保证查找时O(1)的效率,又可能把内存消耗控制在O(bucket×dim)范围内。不过这种方法潜在的问题是存在哈希冲突,不同的n-gram可能会共享同一个embedding。如果桶大小取的足够大,这种影响会很小。
      • 2.2.4 对计算复杂度比较高的运算,Fasttext都采用了预计算的方法,先计算好值,使用的时候再查表,这是典型的空间或时间的优化思路。比如sigmoid函数的计算,源代码如下:


        sigmoid函数的计算.png
      • 2.2.5 在Negative Sampling中,Fasttext也采用了和word2vec类似的方法,即按照每个词的词频进行随机负采样,词频越大的词,被采样的概率越大。每个词被采样的概率并不是简单的按照词频在总量的占比,而是对词频先取根号,再算占比,公式如下:


        负采样.png

        其中,fw表示单词w的词频。取根号的目的是降低高频词汇的采样频率,同时增加低频词的采样频率。

三、Fasttext算法实战(注:以下代码仅在Linux系统下使用!)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date    : 2019-01-01 21:03:59
# @Author  : cdl (1217096231@qq.com)
# @Link    : https://github.com/cdlwhm1217096231
# @Version : $Id$

import fasttext
import jieba
import os
import logging


logging.basicConfig(
    format='%(asctime)s: %(levelname)s: %(message)s', level=logging.INFO)

"""
第1步:获取分类文本,文本直接使用清华大学新闻文本,输出格式是:样本+样本标签
所使用的训练集和测试集已经分词好了,每个样本后会用Tab键隔开,打上样本标签,例如__label__sports
"""

# 空缺


"""
第2步:利用fasttext进行分类,使用fasttext包
"""

# 训练模型
classifier = fasttext.supervised(
    "./dataset/news_fasttext_train.txt", "news_fasttext.model", label_prefix="__label__")
# 训练好的模型
# classifier = fasttext.load_model('new_fasttext.model.bin', label_prefix="__label__")

# 测试模型
result = classifier.test("./dataset/news_fasttext_test.txt")
print("准确率:", result.precision)
print("召回率:", result.recall)

# fasttext只是对整个文本提供precision和recall,要统计不同的分类结果,需要自己实现

classifier = fasttext.load_model(
    'news_fasttext.model.bin', label_prefix='__label__')
labels_right = []
texts = []
with open("./dataset/news_fasttext_test.txt") as f:
    for line in f:
        line = line.strip()
        labels_right.append(line.split('\t')[1].replace("__label__", ""))
        texts.append(line.split('\t')[0])

labels_predict = [e[0] for e in classifier.predict(texts)]  # 预测标签值
text_labels = list(set(labels_right))  # 实际标签值
text_predict_labels = list(set(labels_predict))  # 去重后,预测标签值

# print("预测标签值:", text_predict_labels)
# print("真实标签值:", text_labels)

A = dict.fromkeys(text_labels, 0)  # 预测正确的各个类的数目,真阳性TP
B = dict.fromkeys(text_labels, 0)  # 真实测试数据集中各个类的数目
C = dict.fromkeys(text_predict_labels, 0)  # 预测结果中各个类的数目,所有的预测结果
for i in range(0, len(labels_right)):
    B[labels_right[i]] += 1
    C[labels_predict[i]] += 1
    if labels_right[i] == labels_predict[i]:  # 判断是否是真阳性TP样本
        A[labels_right[i]] += 1

print("真阳性样本TP的类别数目A:", A)
print("测试数据集中各个类别的数目B:", B)
print("预测结果中各个类别的数目C:", C)
# 计算准确率,召回率,F值
for key in B:
    try:
        r = float(A[key]) / float(B[key])   # 召回率
        p = float(A[key]) / float(C[key])  # 准确率
        f = p * r * 2 / (p + r)   # f1值
        print("%s:\t p:%f\t r:%f\t f:%f" % (key, p, r, f))
    except:
        print("错误:", key, "正确:", A.get(key, 0), "real:",
              B.get(key, 0), "预测:", C.get(key, 0))

四、参考资料

1.Bag of Tricks for Efficient Text Classification
2.Enriching Word Vectors with Subword Information
3.玩转Fasttext
4.fastText原理及实践
5.手打例子一步一步带你看懂softmax函数以及相关求导过程
6.使用fastText对文本进行分类
7.训练集数据下载链接
8.测试集数据下载链接