写在前面
本来是想写“实战篇”的,感觉实验语料库不大,就算是一个"应用篇"吧。选取了中文语料,主要简单介绍jieba分词的使用,以及Gemsim模块中Word2Vec的使用。word2vec的原理可以参考之前的文章:华夏狼崽:word2vec学习笔记(原理篇)zhuanlan.zhihu.com
一、语料介绍
一直比较喜欢金庸的武侠小说,所以语料选取了金庸的十五部小说(精校版),最初的项目中只训练了一部《射雕英雄传》,内容太少不够爽,可以分析的关系也不多,所以一次性训练了十五部小说,看是否能得到不同小说之间一些人物、门派、武功的关系。
二、文本预处理
要训练词向量,首先就要得到词,原始的数据都是完整的文本、句子,怎样得到词语呢?这里用到一个很好用的模块:jieba。jieba模块是一个python中文分词模块,可以将输入的句子分为词语,有三种分词方式:精准模式,全模式和搜索引擎模式。安装方法和更细节的使用方式这里不做介绍,可以参考jieba的官方github:fxsjy/jieba
1. 导入jieba模块和其他相关模块并加载文件路径
import os
import jieba
import warnings
warnings.filterwarnings('ignore')
novel_path = "E:/Learn/WORD2VEC/金庸小说精校版/"
data_path = "E:/Learn/WORD2VEC/"
novel_path为小说原始文本路径,data_path为其他一些文件的路径(下文一一介绍)。
2. 过滤标点以及停用词
在分词中,标点符号是我们不需要的。还有一类词,是训练词向量时不需要的,这些词单独出现没有什么具体意义,比如"的","各位","什么"等等,这一类词称为停用词(stop word)。停用词表的选择需要根据实际应用环境调整,这里我的停用词表下载了网上比较通用的中文语料停用词表,根据词向量训练结果做了一点点调整,添加了几个在武侠小说中容易出现而我并不想得到它的词向量的词,比如"说道"等等。
由于下载的停用词表中包含了标点符号,所以这里可以把标点当作停用词合并一起处理。加载停用词表:
stop_words_file = open(data_path + "stop_words.txt", 'r')
stop_words = list()
for line in stop_words_file.readlines():
line = line.strip() # 去掉每行末尾的换行符
stop_words.append(line)
stop_words_file.close()
print(len(stop_words))
print(stop_words[300:320])
看一下一共几个停用词,抽一些出来随便看看有哪些词:
1903
['乘势', '乘机', '乘胜', '乘虚', '乘隙', '九', '也', '也好', '也就是说', '也是', '也罢', '了', '了解', '争取', '二', '二来', '二话不说', '二话没说', '于', '于是']
停用词表加载完毕,后面只需判断分出的词是否在表里即可,如果在表里则舍弃不要。
3. 添加自定义词汇
最开始的实验中,我是直接分词的,但是发现,很多想要的词汇都没能正确的分出来,包括一些重要人物的名称,一些重要的武功等,是因为这些词汇都是武侠小说这种特定环境下的专有名词,jieba的词汇表中并没有收录,所以我整理了三份txt文本,分别记录了武侠小说中的人物名称、武功名称和门派名称,参考来源:金庸网-金庸数据库。
将人名添加到词汇表中:
people_names_file = open(data_path + "金庸小说全人物.txt", 'r')
people_names = list()
for line in people_names_file.readlines():
line = line.strip() # 去掉每行末尾的换行符
jieba.add_word(line)
people_names.append(line)
stop_words_file.close()
print(len(people_names))
添加的人名数量:
1237
类似的,导入了389个武功名称和97个门派名称。
4. 分词
分词本来我以为是没有什么的,直接用精确模式逐本逐行的分就好了。实际操作时遇到了一点小问题。先看一下文件名:
novel_names = list(os.listdir(novel_path))
novel_names
输出:
['书剑恩仇录.txt',
'侠客行.txt',
'倚天屠龙记.txt',
'天龙八部.txt',
'射雕英雄传.txt',
'白马啸西风.txt',
'碧血剑.txt',
'神雕侠侣.txt',
'笑傲江湖.txt',
'越女剑.txt',
'连城诀.txt',
'雪山飞狐.txt',
'飞狐外传.txt',
'鸳鸯刀.txt',
'鹿鼎记.txt']
逐本逐行的分词发现结果中有很多类似这样的词:
['黄蓉道', '郭靖叫', '黄蓉喜', '黄蓉思', '谢逊怒']
很多人名没能够分割出来,和后面的一个字练成了一个新的词语。这主要是因为中文语言太灵活了,的确难以完美分割每个词。如果数量不多的话,我也就接受这个结果了。简单统计了一下,以"黄蓉"为例,一共出现两千多次,有超出五分之一是分错的,这个数量就比较大了,需要考虑解决的方法。再统计观察了一下,发现只有两个字的人名会出现这种情况,三个字的和四个字的名字都能很好的分割开,这样就容易解决了。
我的解决策略是,对分出来的词,判断它的前两个字组成的新词是否在全部人名列表中,如果存在,则直接用新词替换它加入分词表。这样的方法很容易上面的问题,但是有一个缺点,就是如果存在一个人的名字的前两个字,恰好也是另外一个人的名字,这样就直接会直接把第二个人名字记下导致分错了,举例:有两人分别名为"王毛毛"和"王毛",当分词结果分出"王毛毛",就会因为它的前两个字存在于人名表中,就会把这个词改成"王毛",这其实是错误的。这种情况其实是可以避免的,只需要再加一个规则:如果整个名字是正确的人名的话,就优先使用原始分词结果。这个规则是可以的,但是我并没有使用,原因是在金庸先生的武侠小说中,给人物起名基本没有这种情况,起的名字差距都挺远的,主角们名称更是不会和其他的有这种重叠情况。好像只有《倚天屠龙记》中"张三丰"和《侠客行》里的一个小人物"张三"出现这个问题,我选择删掉"张三",我并不想分析这个人物:)
代码如下:
seg_novel = []
for novel_name in novel_names:
novel = open(novel_path + novel_name, 'r', encoding='utf-8-sig')
print("Waiting for{}...".format(novel_name))
line = novel.readline()
forward_rows = len(seg_novel)
while line:
line_1 = line.strip()
outstr = ''
line_seg = jieba.cut(line_1, cut_all=False)
for word in line_seg:
if word not in stop_words:
if word != '\t':
if word[:2] in people_names:
word = word[:2]
outstr += word
outstr += " "
if len(str(outstr.strip())) != 0:
seg_novel.append(str(outstr.strip()).split())
line = novel.readline()
print("{}finished,with{}Row".format(novel_name, (len(seg_novel) - forward_rows)))
print("-" * 40)
print("-" * 40)
print("-" * 40)
print("All finished,with{}Row".format(len(seg_novel)))
可以看到每一本书分词后的行数和总行数:
Waiting for 书剑恩仇录.txt...
书剑恩仇录.txt finished,with 3561 Row
----------------------------------------
Waiting for 侠客行.txt...
侠客行.txt finished,with 3513 Row
----------------------------------------
Waiting for 倚天屠龙记.txt...
倚天屠龙记.txt finished,with 7918 Row
----------------------------------------
Waiting for 天龙八部.txt...
天龙八部.txt finished,with 10947 Row
----------------------------------------
Waiting for 射雕英雄传.txt...
射雕英雄传.txt finished,with 7130 Row
----------------------------------------
Waiting for 白马啸西风.txt...
白马啸西风.txt finished,with 597 Row
----------------------------------------
Waiting for 碧血剑.txt...
碧血剑.txt finished,with 3786 Row
----------------------------------------
Waiting for 神雕侠侣.txt...
神雕侠侣.txt finished,with 6998 Row
----------------------------------------
Waiting for 笑傲江湖.txt...
笑傲江湖.txt finished,with 8550 Row
----------------------------------------
Waiting for 越女剑.txt...
越女剑.txt finished,with 196 Row
----------------------------------------
Waiting for 连城诀.txt...
连城诀.txt finished,with 2207 Row
----------------------------------------
Waiting for 雪山飞狐.txt...
雪山飞狐.txt finished,with 1096 Row
----------------------------------------
Waiting for 飞狐外传.txt...
飞狐外传.txt finished,with 3776 Row
----------------------------------------
Waiting for 鸳鸯刀.txt...
鸳鸯刀.txt finished,with 211 Row
----------------------------------------
Waiting for 鹿鼎记.txt...
鹿鼎记.txt finished,with 11158 Row
----------------------------------------
----------------------------------------
----------------------------------------
All finished,with 71644 Row
随便取几行出来看看:
['郭靖', '黄蓉', '完颜洪烈', '做', '爹爹', '语气', '间', '亲热', '相互', '一眼', '郭靖', '气恼', '难受', '恨不得', '揪住', '问个', '明白']
['黄蓉', '郭靖', '边道', '喝得', '酒儿', '偏', '两人', '溜', '出阁', '子', '来到', '后园', '黄蓉', '晃动', '火折', '点燃', '柴房中', '柴草', '四下', '放', '起火']
['一日', '中', '洪七公', '心', '早已', '御厨', '之内', '好容易', '挨到', '时分', '郭靖', '负', '洪七公', '四人', '上屋', '径往', '大内', '皇宫', '高出', '民居', '屋瓦', '金光灿烂', '极易', '辨认', '不多时', '四人', '已悄', '没声', '跃进', '宫墙']
['完颜洪烈', '悄声', '所说', '耳目众多', '饮酒', '三人', '转过', '话题', '说些', '景物', '见闻', '风土人情']
嗯,不错,是我想要的效果,通过一个嵌套列表存储,每一句为列表中一个元素,每一句又由分好的词构成一个列表,这也是word2vec训练时需要输入的格式。
三、训练词向量
这里用的是Gemsim模块中Word2Vec。Gemsim模块是一个据说功能很强大的NLP处理模块,我只尝试了Word2Vec函数,以后有需要的话学习一下其他部分。训练代码如下:
import gensim.models as w2v
model = w2v.Word2Vec(sentences=seg_novel, size=200, window=5, min_count=5, sg=0)
model.save(data_path + 'all_CBOW.model') # 保存模型
说一下Word2Vec中的几个参数,sentences是输入的语料,即分词得到的嵌套列表,可以通过LineSentence或Text8Corpus函数构造。size即是训练得到的词向量的维数。window为滑窗大小,也就是训练词与上下文词最远的距离。min_count是指过滤掉一些低词频的词。sg为选择CBOW模型还是skip-gram模型,0为CBOW模型,1为skip-gram模型,默认为CBOW模型(实际发现CBOW和skip-gram模式得到的结果差距还是比较大的,具体可以看下文对比)。还有一些其他参数,不一一列出,可以参考官方文档:gensim: topic modelling for humansradimrehurek.com
1. CBOW
参数都选取默认(CBOW),训练得到词向量模型。接下来做一些简单有趣的测试。
首先是用模型测试一些词的相似度(实则根据词向量计算余弦相似度)。
print(model.similarity('张无忌', '周芷若'))
print(model.similarity('张无忌', '赵敏'))
看一下张无忌和周芷若、赵敏哪个相似度更高呢:
0.70147747
0.74265605
好吧,还是和赵敏比较好。
看一下和张无忌最接近的五个词:
print(model.most_similar("张无忌", topn=5))
[('令狐冲', 0.8610879182815552),
('虚竹', 0.8390334844589233),
('黑白子', 0.8079050779342651),
('张翠山', 0.8067573308944702),
('苗人凤', 0.7919591665267944)]
诶?除了张翠山之外,其他的几个人怎么关联的呢?实际大概是因为令狐冲、虚竹等也是其他小说中数一数二重要的人物吧。
看一下和峨嵋派最接近的五个词:
print(model.most_similar("峨嵋派", topn=5))
[('华山派', 0.9490149021148682),
('昆仑派', 0.9471687078475952),
('嵩山派', 0.9393842220306396),
('本派', 0.9321860074996948),
('雪山派', 0.9311256408691406)]
除了"本派"乱入,其他也都是一些门派。
韦小宝关系最乱,看下和哪个老婆最好:
print(model.similarity('韦小宝', '阿珂'))
print(model.similarity('韦小宝', '双儿'))
print(model.similarity('韦小宝', '建宁公主'))
print(model.similarity('韦小宝', '苏荃'))
print(model.similarity('韦小宝', '沐剑屏'))
print(model.similarity('韦小宝', '曾柔'))
print(model.similarity('韦小宝', '方怡'))
0.46550432
0.51736045
0.46725014
0.45597148
0.5102725
0.36221734
0.50045687
双儿第一,沐剑屏其次,没看过鹿鼎记不做分析。
看一下和韦小宝最接近的十个词:
print(model.most_similar("韦小宝", topn=10))
[('康熙', 0.6945387125015259),
('公主', 0.6921814680099487),
('乾隆', 0.6888490915298462),
('太后', 0.6246222257614136),
('苏菲亚', 0.605838418006897),
('周总镖', 0.6015807390213013),
('祖千秋', 0.6013493537902832),
('小桂子', 0.5954452753067017),
('陈家洛', 0.5947461128234863),
('徐天宏', 0.5941584706306458)]
没想到7个老婆都没进入前十,反而编外老婆苏菲亚排名那么高。。不过韦小宝和康熙皇帝的关系是真的近呀。
除此之外,还可以用以下函数寻找对应关系:类似于李白之与唐代,等于曹雪芹之于清代。
def find_relation(a, b, c):
d, _ = model.most_similar(positive=[c, b], negative=[a])[0]
print (c,d)
首先就想找一下,杨过之与小龙女等于张无忌之于谁:
print(find_relation("杨过","小龙女","张无忌"))
张无忌 赵敏
~~~~~~~~这个结果还是找的比较准的,心疼周芷若一秒。但是有一些就不准了,乱入了,比如:
print(find_relation("杨过","小龙女","郭靖))
郭靖 石清
。。不止这里有乱入,在测试一些人的最接近词时,也有很多挺难解释的,可能他们相似的维度并不容易观察的到吧。接下来用skip-gram尝试,是不是同样的效果呢?
2. skip-gram
只需把sg参数设置为1即可,参数也选用默认参数。
杨过之与小龙女等于张无忌之于谁:
print(find_relation("杨过","小龙女","张无忌"))
张无忌 周芷若
。。。逆风翻盘?那赶紧看看其他的结果:
print(find_relation("杨过","小龙女","郭靖"))
郭靖 蓉儿
print(find_relation("杨过","小龙女","黄蓉"))
黄蓉 靖哥哥
这个就有点秀了呀,不仅能找对人,找到的还是昵称。。。
和张无忌最接近的五个词:
print(model.most_similar("张无忌", topn=5))
[('周芷若', 0.6134092211723328),
('赵敏', 0.5959696769714355),
('小昭', 0.5360002517700195),
('杨逍', 0.5234625935554504),
('波斯', 0.49604594707489014)]
这下就没有其他时代乱入的人了,而且也都是比较好解释的通的
还可以找找这样的关系:
print(find_relation("杨过","小龙女","乔峰"))
乔峰 阿朱
print(find_relation("杨过","小龙女","段誉"))
段誉 王语嫣
print(find_relation("杨过","小龙女","段正淳"))
段正淳 刀白凤
print(find_relation("武当派","张三丰","峨嵋派"))
峨嵋派 周芷若
print(find_relation("武当派","张三丰","天地会"))
天地会 陈近南
乔峰阿朱这个就没什么好说的了,段氏父子的异性关系圈一直比较复杂。。从词向量的角度看,段誉还是对神仙姊姊钟情的,段正淳还是对正室妻子有更多的情感的。
武当派之于张三丰----峨嵋派不应该是"灭绝师太"嘛?哈哈,好吧,周芷若毕竟也是做过峨嵋掌门的,也算解释的通吧。不止是在同一部小说中可以对应这种关系,通过《倚天屠龙记》中的武当派掌门张三丰也可以找到《鹿鼎记》中天地会总舵主陈近南。
最后再看一个原著中比较神秘的人:
print(model.most_similar("王重阳", topn=10))
[('林朝英', 0.8663322925567627),
('宝典', 0.8595889210700989),
('研习', 0.8348122835159302),
('创制', 0.8294848799705505),
('先天功', 0.8276116251945496),
('医术', 0.827604353427887),
('精研', 0.8269264101982117),
('神通', 0.8256270885467529),
('剑宗', 0.8248381614685059),
('功诀', 0.8226447701454163)]
这个结果还是蛮满意的,王重阳作为一个武学奇才,五绝之首,世称"天下第一",与"创制","研习","精研"一类词相近,然而排在第一的,却还是对他因爱生恨的林朝英。
写在后面:
有意思的测试还有很多,就不一一叙述了。乱入的结果也不少,优化分词或者调整参数可能有更好的结果。
总之从结果看来CBOW模型,更注重文章整体的关联,很多相似度高的词跨越了多个时代,多本小说,而skip-gram模型更加注重局部,相似度高的词基本集中在同一本小说中。查阅了一些文章后,的确说CBOW模型在语料库较大时效果较好,skip-gram模型在语料库较小时候效果比较好。可能十五本小说语料库的确比较小吧,所以skip-gram模型的测试结果的可解释性更强点。
这也只是从训练出的词向量做的最直接的测试,后续还可以用聚类,分类等其他算法进行其他更有意思的测试。相关的代码和文件上传在github上,感兴趣的话可以自己尝试一下:wolfkin-hth/novelsgithub.com
学习路漫漫,狼崽在路上~