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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容