在实际的模型训练中,word2vec是一个最常用也是最重要的将“词”转换为“词嵌”的方式。对于普通文本来说,供人类所了解和掌握的信息传递方式并不能简单地被计算机所理解,因此词嵌入是目前来说解决向计算机传递文字信息这一问题的最好解决方法。如下图,
随着研究人员对词嵌入的深入研究和计算机处理能力的提高,更多、更好的方法被提出,例如使用FastText和预训练的词嵌入的模型对数据进行处理。
本章介绍基于上两章关于词向量的知识,介绍FastText的训练和预训练词向量模型的使用方法。
FastText原理和基础算法
相对于传统的word2vec计算方法,FastText是一种快速和更新的计算词嵌入的方法,其优点主要有以下几个方面,
- FastText在保持高度精度的情况下加快了训练速度和测试速度。
- FastText对词嵌入的训练更加精准。
- FastText采用两个重要算法N-gram和Hierarchical SoftMax
N-gram算法
相对于word2vec中采用CBOW架构,FastText采用的是N-gram架构,如下图,
其中,
表示一个文本中的N-gram向量,每个特征是词向量的平均值。N-gram代表多元,常见有1-gram、2-gram、3-gram、4-gram,分别对应一元、二元、三元、四元。
以“This is a sentence”为例,对其进行分词处理,
- 如得到的数据[“This”, “is”, “a”, “sentence”],这就是1-gram,一元。一元在分词的时候对应一个滑动窗口,窗口大小为1,所以每次只取一个词。
- 使用2-gram,得到[“This is”, “is a”, “a sentence”]。滑动窗口为2,每次取两个词。
- 使用3-gram,得到[“This is a”, “is a sentence”]。滑动窗口为3,每次取三个词。
N-gram模型认为词与词之间有关系的距离为N,如果超过N则认为它们之间没有关系,所以使用2-gram就不会出现“this a”、”is sentence“这些词。
理论上N可以设置成任意值,但一般设置成3就足够了。
Hierarchical SoftMax算法
当语料较多时,使用分层Softmax可以减轻计算量。FastText中的分层SoftMax利用Huffman树实现,将词向量作为叶子节点,之后根据词向量构建Huffman树,如下图,
其架构如下,
关于分层softMax算法较为复杂,不是本文介绍关键,有兴趣者自行研究学习。
FastText模型训练
介绍了原理和架构,现在开始使用FastText处理中文文字。
数据收集与分词
从百度热搜里,随机选一篇新闻,摘录文字,构建程序如下,
import jieba
def setup():
sentences = ["泽连斯基否认袭击克里姆林宫:我们甚至没有足够的武器",
"“我们没有攻击普京或莫斯科”。据《基辅独立报》报道,乌克兰总统泽连斯基当地时间5月3日公开否认克里姆林宫遭乌克兰无人机袭击的说法。",
"“我们没有攻击普京或莫斯科。我们在自己的领土上战斗,保卫我们的村庄和城市。”泽连斯基在芬兰参加为期一天的北欧国家领导人峰会期间,在新闻发布会上回应克宫遭袭时称,“我们甚至没有足够的武器。”",
"“因此,我们没有攻击普京。我们将把其留给法庭。”泽连斯基称。",
"报道说,3日早些时候,泽连斯基的发言人在向英国广播公司(BBC)乌克兰频道发表的声明中也否认了乌方参与“袭击克里姆林宫”。"
]
cuts = []
for sentence in sentences:
cut = jieba.lcut(sentence)
cuts.append(cut)
return cuts
def train():
cuts = setup()
print(cuts)
def main():
train()
if __name__ == "__main__":
main()
运行结果打印输出如下,
Building prefix dict from the default dictionary ...
Dumping model to file cache /tmp/jieba.cache
Loading model cost 2.469 seconds.
Prefix dict has been built successfully.
[['泽', '连斯基', '否认', '袭击', '克里姆林宫', ':', '我们', '甚至', '没有', '足够', '的', '武器'], ['“', '我们', '没有', '攻击', '普京', '或', '莫斯科', '”', '。', '据', '《', '基辅', '独立报', '》', '报道', ',', '乌克兰', '总统', '泽', '连斯基', '当地', '时间', '5', '月', '3', '日', '公开', '否认', '克里姆林宫', '遭', '乌克兰', '无人机', '袭击', '的', '说法', '。'], ['“', '我们', '没有', '攻击', '普京', '或', '莫斯科', '。', '我们', '在', '自己', '的', '领土', '上', '战斗', ',', '保卫', '我们', '的', '村庄', '和', '城市', '。', '”', '泽', '连斯基', '在', '芬兰', '参加', '为期', '一天', '的', '北欧国家', '领导人', '峰会', '期间', ',', '在', '新闻', '发布会', '上', '回应', '克宫', '遭袭', '时称', ',', '“', '我们', '甚至', '没有', '足够', '的', '武器', '。', '”'], ['“', '因此', ',', '我们', '没有', '攻击', '普京', '。', '我们', '将', '把', '其', '留给', '法庭', '。', '”', '泽', '连斯基', '称', '。'], ['报道', '说', ',', '3', '日', '早些时候', ',', '泽', '连斯基', '的', '发言人', '在', '向', '英国广播公司', '(', 'BBC', ')', '乌克兰', '频道', '发表', '的', '声明', '中', '也', '否认', '了', '乌方', '参与', '“', '袭击', '克里姆林宫', '”', '。']]
其中,每一行根据jieba的分词模型进行分词处理,之后每一行里存储了已经被分词处理过的文本。
使用gensim里的FastText进行词嵌入计算
gensim.models除了前两章介绍过的word2vec模型,还有FastText专用计算类,调用代码如下,
import gensim.models
def train():
cuts = setup()
print(cuts)
model = gensim.models.FastText(vector_size = 4, window = 3, min_count = 1, sentences = cuts, epochs = 10)
其中,FastText原型及参数定义如下,
gensim.models.FastText(sentences=None, corpus_file=None, sg=0, hs=0, vector_size=100, alpha=0.025, window=5, min_count=5, max_vocab_size=None, word_ngrams=1, sample=1e-3, seed=1, workers=3, min_alpha=0.0001, negative=5, ns_exponent=0.75, cbow_mean=1, hashfxn=hash, epochs=5, null_word=0, min_n=3, max_n=6, sorted_vocab=1, bucket=2000000, trim_rule=None, batch_words=MAX_WORDS_IN_BATCH, callbacks=(), max_final_vocab=None, shrink_windows=True,)
- Sentences。一个iterable的tokens集合,可选,如果打算在其他地方初始化FastText,该参数可不传。
- vector_size。向量维度。
- window。一个句子中当前单词和被预测单词的最大距离。
- min_count。忽略词频小于该值的单词。
- workers。训练模型时使用的线程数。
- sg。模型训练算法,1代表skip-gram,0代表CBOW。
- hs。1代表使用分层SoftMax,0代表使用负采样。
- Iter。模型迭代的次数。
- seed。随机数发生器种子。
在定义的FastText类中依次设置了向量维度vector_size、训练时当前单词和被预测单词最大距离window,词频最小值min_count,分词后文本。完整代码如下,
import jieba
import gensim.models
import os
def setup():
sentences = ["泽连斯基否认袭击克里姆林宫:我们甚至没有足够的武器",
"“我们没有攻击普京或莫斯科”。据《基辅独立报》报道,乌克兰总统泽连斯基当地时间5月3日公开否认克里姆林宫遭乌克兰无人机袭击的说法。",
"“我们没有攻击普京或莫斯科。我们在自己的领土上战斗,保卫我们的村庄和城市。”泽连斯基在芬兰参加为期一天的北欧国家领导人峰会期间,在新闻发布会上回应克宫遭袭时称,“我们甚至没有足够的武器。”",
"“因此,我们没有攻击普京。我们将把其留给法庭。”泽连斯基称。",
"报道说,3日早些时候,泽连斯基的发言人在向英国广播公司(BBC)乌克兰频道发表的声明中也否认了乌方参与“袭击克里姆林宫”。"
]
cuts = []
for sentence in sentences:
cut = jieba.lcut(sentence)
cuts.append(cut)
return cuts
def train(path):
cuts = setup()
if os.path.exists(path):
model = gensim.models.FastText.load(path)
return model
else:
model = gensim.models.FastText(vector_size = 4, window = 3, min_count = 1, sentences = cuts, epochs = 10)
model.build_vocab(cuts)
model.train(cuts, total_examples = model.corpus_count, epochs = 10)
model.save(path)
return model
def main():
path = "/tmp/FastText_Jieba_Model.model"
model = train(path)
print("model.wv.key_to_index = ", model.wv.key_to_index)
print("model.wv.index_to_key = ", model.wv.index_to_key)
print("len(model.wv.vectors) = ", len(model.wv.vectors))
embedding = model.wv["克里姆林", “我们"]
print("embedding = ", embedding)
if __name__ == "__main__":
main()
model中的build_vocab函数是根据输入建立词库,而train函数是对model集性能训练模式的设定。最后是将训练好的模型文件保存。
使用训练好的FastText模型
如上代码,当模型文件已经存在,即已经训练,这直接通过gensim.models.FastText.load(path)载入,
if os.path.exists(path):
model = gensim.models.FastText.load(path)
return model
最后使用model.wv测试输入文字,
def main():
path = "/tmp/FastText_Jieba_Model.model"
model = train(path)
print("model.wv.key_to_index = ", model.wv.key_to_index)
print("model.wv.index_to_key = ", model.wv.index_to_key)
print("len(model.wv.vectors) = ", len(model.wv.vectors))
embedding = model.wv["克里姆林", “我们"]
print("embedding = ", embedding)
运行结果打印输出如下,
Building prefix dict from the default dictionary ...
Dumping model to file cache /tmp/jieba.cache
Loading model cost 2.426 seconds.
Prefix dict has been built successfully.
model.wv.key_to_index = {'。': 0, '的': 1, '我们': 2, ',': 3, '泽': 4, '没有': 5, '”': 6, '“': 7, '连斯基': 8, '在': 9, '克里姆林宫': 10, '攻击': 11, '普京': 12, '袭击': 13, '否认': 14, '乌克兰': 15, '甚至': 16, '足够': 17, '武器': 18, '或': 19, '莫斯科': 20, '报道': 21, '3': 22, '日': 23, '上': 24, '克宫': 25, '早些时候': 26, '将': 27, '把': 28, '其': 29, '》': 30, '独立报': 31, '基辅': 32, '《': 33, '据': 34, '留给': 35, '法庭': 36, '称': 37, '说': 38, '发言人': 39, '当地': 40, '向': 41, '英国广播公司': 42, '(': 43, 'BBC': 44, ')': 45, '频道': 46, '发表': 47, ':': 48, '声明': 49, '中': 50, '也': 51, '了': 52, '总统': 53, '时间': 54, '回应': 55, '村庄': 56, '发布会': 57, '新闻': 58, '期间': 59, '峰会': 60, '领导人': 61, '北欧国家': 62, '一天': 63, '为期': 64, '参加': 65, '芬兰': 66, '城市': 67, '和': 68, '保卫': 69, '5': 70, '战斗': 71, '乌方': 72, '领土': 73, '自己': 74, '遭袭': 75, '说法': 76, '无人机': 77, '遭': 78, '公开': 79, '时称': 80, '因此': 81, '月': 82, '参与': 83}
model.wv.index_to_key = ['。', '的', '我们', ',', '泽', '没有', '”', '“', '连斯基', '在', '克里姆林宫', '攻击', '普京', '袭击', '否认', '乌克兰', '甚至', '足够', '武器', '或', '莫斯科', '报道', '3', '日', '上', '克宫', '早些时候', '将', '把', '其', '》', '独立报', '基辅', '《', '据', '留给', '法庭', '称', '说', '发言人', '当地', '向', '英国广播公司', '(', 'BBC', ')', '频道', '发表', ':', '声明', '中', '也', '了', '总统', '时间', '回应', '村庄', '发布会', '新闻', '期间', '峰会', '领导人', '北欧国家', '一天', '为期', '参加', '芬兰', '城市', '和', '保卫', '5', '战斗', '乌方', '领土', '自己', '遭袭', '说法', '无人机', '遭', '公开', '时称', '因此', '月', '参与']
len(model.wv.vectors) = 84
embedding = [[-0.04954489 -0.03840816 0.02232057 0.0196969 ]
[ 0.07289299 0.0621084 0.04499979 -0.13721387]]
再次运行,
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 1.583 seconds.
Prefix dict has been built successfully.
model.wv.key_to_index = {'。': 0, '的': 1, '我们': 2, ',': 3, '泽': 4, '没有': 5, '”': 6, '“': 7, '连斯基': 8, '在': 9, '克里姆林宫': 10, '攻击': 11, '普京': 12, '袭击': 13, '否认': 14, '乌克兰': 15, '甚至': 16, '足够': 17, '武器': 18, '或': 19, '莫斯科': 20, '报道': 21, '3': 22, '日': 23, '上': 24, '克宫': 25, '早些时候': 26, '将': 27, '把': 28, '其': 29, '》': 30, '独立报': 31, '基辅': 32, '《': 33, '据': 34, '留给': 35, '法庭': 36, '称': 37, '说': 38, '发言人': 39, '当地': 40, '向': 41, '英国广播公司': 42, '(': 43, 'BBC': 44, ')': 45, '频道': 46, '发表': 47, ':': 48, '声明': 49, '中': 50, '也': 51, '了': 52, '总统': 53, '时间': 54, '回应': 55, '村庄': 56, '发布会': 57, '新闻': 58, '期间': 59, '峰会': 60, '领导人': 61, '北欧国家': 62, '一天': 63, '为期': 64, '参加': 65, '芬兰': 66, '城市': 67, '和': 68, '保卫': 69, '5': 70, '战斗': 71, '乌方': 72, '领土': 73, '自己': 74, '遭袭': 75, '说法': 76, '无人机': 77, '遭': 78, '公开': 79, '时称': 80, '因此': 81, '月': 82, '参与': 83}
model.wv.index_to_key = ['。', '的', '我们', ',', '泽', '没有', '”', '“', '连斯基', '在', '克里姆林宫', '攻击', '普京', '袭击', '否认', '乌克兰', '甚至', '足够', '武器', '或', '莫斯科', '报道', '3', '日', '上', '克宫', '早些时候', '将', '把', '其', '》', '独立报', '基辅', '《', '据', '留给', '法庭', '称', '说', '发言人', '当地', '向', '英国广播公司', '(', 'BBC', ')', '频道', '发表', ':', '声明', '中', '也', '了', '总统', '时间', '回应', '村庄', '发布会', '新闻', '期间', '峰会', '领导人', '北欧国家', '一天', '为期', '参加', '芬兰', '城市', '和', '保卫', '5', '战斗', '乌方', '领土', '自己', '遭袭', '说法', '无人机', '遭', '公开', '时称', '因此', '月', '参与']
len(model.wv.vectors) = 84
embedding = [[-0.04954489 -0.03840816 0.02232057 0.0196969 ]
[ 0.07289299 0.0621084 0.04499979 -0.13721387]]
注意,embedding = model.wv["克里姆林", “我们”]语句,从这句话可以看出,FastText只能打印已经训练过的词向量,而不能打印未经训练的词。
下面就是如何使用词嵌入的问题,在JAX现有的函数中实现文本分类的一个较好方法是将文本转化为词向量嵌入,然后由模型对其进行分类和处理。
结论
本章介绍了除了word2vec之外的FastText原理及使用方法,也通过一个例子演示了中文词向量的处理办法。