FastText情感分析和词向量训练实战——Keras算法练习(

作者: 王同学死磕技术 | 来源:发表于2019-01-21 20:30 被阅读3次

    FastText是facebook开源的一个词向量与文本分类工具 ,其最大的优点就是快,同时不失精度。 此算法有两个主要应用场景:

    • 文本分类
    • 词向量训练

    工业界碰到一些简单分类问题时,经常采用这种简单,快速的模型解决问题。

    FastText原理简介

    FastText原理部分有3个突出的特点:

    • 模型简单,其结构有点类似word2vector中的CBOW架构,如下图所示。FastText将句子特征通过一层全连接层映射到向量空间后,直接将词向量平均处理一下,就去做预测。
      模型架构
    • 使用了n-gram的特征,使得句子的表达更充分。笔者会在实战中详细介绍这部分的操作。
    • 使用 Huffman算法建立用于表征类别的树形结构。这部分可以加速运算,同时减缓一些样本不均衡的问题。

    其中比较有意思的是,做完分类任务后,模型全连接层的权重可以用来做词向量。而且由于使用了n-gram的特征,fasttext的词向量可以很好的缓解Out of Vocabulary的问题。接下来笔者就用keras构建一个fasttext模型做一下情感分析的任务,同时拿出它的词向量看一看。

    FastText情感分析实战

    import numpy as np
    np.random.seed(1335)  # for reproducibility
    from keras.preprocessing import sequence
    from keras.models import Sequential
    from keras.layers import Dense
    from keras.layers import Embedding
    from keras.layers import GlobalAveragePooling1D
    

    这里定义了两个函数用于n-gram特征增广,这里笔者是直接将这篇参考文章的代码拷贝过来,作者的注释极其详细。这里需要讲解一下n-gram特征的含义:
    如果原句是:今天的雪下个不停。

    • unigram(1-gram)的特征:["今天","的","雪","下","个","不停"]
    • bigram(2-gram) 的特征: ["今天的","的雪","雪下","下个","个不停"]

    所以大家发现没,n-gram的意思将句子中连续的n个词连起来组成一个单独的词。
    如果使用unigram和bigram的特征,句子特征就会变成:
    ["今天","的","雪","下","个","不停","今天的","的雪","雪下","下个","个不停"]这么一长串。
    这样做可以丰富句子的特征,能够更好的表示句子的语义。

    def create_ngram_set(input_list, ngram_value=2):
        """
        Extract a set of n-grams from a list of integers.
        从一个整数列表中提取  n-gram 集合。
        >>> create_ngram_set([1, 4, 9, 4, 1, 4], ngram_value=2)
        {(4, 9), (4, 1), (1, 4), (9, 4)}
        >>> create_ngram_set([1, 4, 9, 4, 1, 4], ngram_value=3)
        [(1, 4, 9), (4, 9, 4), (9, 4, 1), (4, 1, 4)]
        """
        return set(zip(*[input_list[i:] for i in range(ngram_value)]))
    
    
    def add_ngram(sequences, token_indice, ngram_range=2):
        """
        Augment the input list of list (sequences) by appending n-grams values.
        增广输入列表中的每个序列,添加 n-gram 值
        Example: adding bi-gram
        >>> sequences = [[1, 3, 4, 5], [1, 3, 7, 9, 2]]
        >>> token_indice = {(1, 3): 1337, (9, 2): 42, (4, 5): 2017}
        >>> add_ngram(sequences, token_indice, ngram_range=2)
        [[1, 3, 4, 5, 1337, 2017], [1, 3, 7, 9, 2, 1337, 42]]
        Example: adding tri-gram
        >>> sequences = [[1, 3, 4, 5], [1, 3, 7, 9, 2]]
        >>> token_indice = {(1, 3): 1337, (9, 2): 42, (4, 5): 2017, (7, 9, 2): 2018}
        >>> add_ngram(sequences, token_indice, ngram_range=3)
        [[1, 3, 4, 5, 1337], [1, 3, 7, 9, 2, 1337, 2018]]
        """
        new_sequences = []
        for input_list in sequences:
            new_list = input_list[:]
            for i in range(len(new_list) - ngram_range + 1):
                for ngram_value in range(2, ngram_range + 1):
                    ngram = tuple(new_list[i:i + ngram_value])
                    if ngram in token_indice:
                        new_list.append(token_indice[ngram])
            new_sequences.append(new_list)
    
        return new_sequences
    

    数据载入

    笔者在之前的情感分析文章中介绍了这个数据集的数据格式,想详细了解的同学可以去这篇文章查看数据详情。

    def read_data(data_path):
        senlist = []
        labellist = []  
        with open(data_path, "r",encoding='gb2312',errors='ignore') as f:
             for data in  f.readlines():
                    data = data.strip()
                    sen = data.split("\t")[2] 
                    label = data.split("\t")[3]
                    if sen != "" and (label =="0" or label=="1" or label=="2" ) :
                        senlist.append(sen)
                        labellist.append(label) 
                    else:
                        pass                    
        assert(len(senlist) == len(labellist))            
        return senlist ,labellist 
    
    sentences,labels = read_data("data_train.csv")
    char_set = set(word for sen in sentences for word in sen)
    char_dic = {j:i+1 for i,j in enumerate(char_set)}
    char_dic["unk"] = 0
    

    n-gram特征增广

    这里笔者只使用了unigram和bigram的特征,如果使用trigram的特征,特征数以及计算量将会猛增,所以没有好的硬件不要轻易尝试3,4-gram以上的特征。

    max_features = len(char_dic)
    sentences2id = [[char_dic.get(word) for word in sen] for sen in sentences]
    ngram_range = 2
    if ngram_range > 1:
        print('Adding {}-gram features'.format(ngram_range))
        # Create set of unique n-gram from the training set.
        ngram_set = set()
        for input_list in sentences2id:
            for i in range(2, ngram_range + 1):
                set_of_ngram = create_ngram_set(input_list, ngram_value=i)
                ngram_set.update(set_of_ngram)
        # Dictionary mapping n-gram token to a unique integer. 将 ngram token 映射到独立整数的词典
        # Integer values are greater than max_features in order
        # to avoid collision with existing features.
        # 整数大小比 max_features 要大,按顺序排列,以避免与已存在的特征冲突
        start_index = max_features 
        token_indice = {v: k + start_index for k, v in enumerate(ngram_set)}
        
    fea_dict = {**token_indice,**char_dic}
    # 使用 n-gram 特征增广 X_train 
    sentences2id= add_ngram(sentences2id,fea_dict, ngram_range)
    
    print('Average train sequence length: {}'.format(
            np.mean(list(map(len, sentences2id)), dtype=int)))
    

    数据预处理

    将句子特征padding成300维的向量,同时对label进行onehot编码。

    import numpy as np
    from keras.utils import np_utils
    print('Pad sequences (samples x time)')
    X_train = sequence.pad_sequences(sentences2id, maxlen=300)
    labels = np_utils.to_categorical(labels)
    

    定义模型

    这里我们我们可以看到fasttext的一些影子了:

    • 使用了一个简单的Embedding层(其实本质上就是一个Dense层),
    • 然后接一个GlobalAveragePooling1D层对句子中每个词的输出向量求平均得到句子向量,
    • 之后句子向量通过全连接层后,得到的输出和label计算损失值。

    此模型的最后一部没有严格的遵循fasttext。

    print('Build model...')
    model = Sequential()
    #我们从一个有效的嵌入层(embedding layer)开始,它将我们的词汇索引(vocab indices )映射到词向量的维度上.
    model.add(Embedding(len(fea_dict),
                        200,
                        input_length=300))
    # 我们增加 GlobalAveragePooling1D, 这将平均计算文档中所有词汇的的词嵌入
    model.add(GlobalAveragePooling1D())
    #我们投射到单个单位的输出层上
    model.add(Dense(3, activation='softmax'))
    model.compile(loss='categorical_crossentropy',
                  optimizer="adam",
                  metrics=['accuracy'])
    model.summary()
    

    这下面是模型结构的的可视化输出,我们可以看到,只用了unigram和bigram的特征词典的维度已经到了5千多万,如果用到trigram了特征,特征词典的维度肯定过亿。


    train

    模型训练

    从训练速度上来看,2分多钟一个epoch,同样的数据,比之前笔者使用的BiLSTM的速度快了不少。

    train

    训练副产物——词向量

    embedding_layer = model.get_layer("embedding_1")
    emb_wight = embedding_layer.get_weights()[0]
    

    我们可以通过上方两行代码就拿到fasttext的训练副产物——词向量。
    其中Embedding层的weight的形式和下图中间的 W矩阵一样,每行对应着一个词的词向量。通过简单的index索引就可以得到训练好的词向量。


    embedding

    下面是笔者索引"妈妈"这个词的词向量的代码。

    def word2fea(word,char_dic):
        wordtuple = tuple(char_dic.get(i) for i in word)
        return wordtuple
    mather = word2fea("妈妈",char_dic)
    index = fea_dict.get(mather)
    mama = emb_wight[index]
    

    打印出来如下图所示,"妈妈"被映射成了一个200维的词向量。


    vector of word

    结语

    fasttext一个如此简单的模型却极其好用,这也是工业界特别喜欢它的原因。所以在面对问题的时候不要一上来就构建一个特别复杂的模型,有时候简单的模型也能很好解决的问题,一定要记住大道至简。
    参考:
    https://kexue.fm/archives/4122
    http://www.voidcn.com/article/p-alhbnusv-bon.html
    https://github.com/facebookresearch/fastText

    相关文章

      网友评论

        本文标题:FastText情感分析和词向量训练实战——Keras算法练习(

        本文链接:https://www.haomeiwen.com/subject/qzncjqtx.html