FastText情感分析和词向量训练实战——Keras算法练习(
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的速度快了不少。
训练副产物——词向量
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