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

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

  • 文本分类
  • 词向量训练

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

FastText原理简介

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

  • 模型简单,其结构有点类似word2vector中的CBOW架构,如下图所示。FastText将句子特征通过一层全连接层映射到向量空间后,直接将词向量平均处理一下,就去做预测。
    9168245-d76baeae426da7d7.png
    模型架构
  • 使用了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了特征,特征词典的维度肯定过亿。


9168245-fbdaacfc0560e39b.png
train

模型训练

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

9168245-ea9dd4bf2951565d.png
train

训练副产物——词向量

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

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


9168245-330373ba1e81ba20.png
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维的词向量。


9168245-69a161261da180f4.png
vector of word

结语

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

猜你喜欢

转载自blog.csdn.net/weixin_34381687/article/details/87221286