万物皆可Embedding
入坑cs224N后看完第二周和相关论文。觉得word2vec非常有意思,将一段具有上下文关系的短文(实体)词语学习嵌入到语义空间成为一个向量,然后判断两个词语(实体)的相关性。又发现有造好的轮子gensim,何不先做一些简单又有意思的实验,再深入的学习。
本来想爬豆瓣用户历史记录,用word2Vec做一个推荐,但最近进入考期,预习刷网课要紧。先埋个伏笔,以后有时间再弄吧,技多不压身。于是先利用《三国演义》作为语料,练练手gensim,看看embedding能学到啥有趣的结论。
利用request和beautifoup解析工具先从某网站上爬下我小学爱不释手的历史小说《三国演义》(复习爬虫技能)。简单对文本里标点符号消去并利用jieba
分好词,由于小说近似是文言文,分出词语效果并不好。
def crawl(url = None):
"""
从http://www.purepen.com/sgyy/爬下《三国演义》到本地txt文件
:param url:
:return:
"""
print('Waitting for crawling sentence!')
url = 'http://www.purepen.com/sgyy/'
contents = ''
for num in range(1,121):
num_str = str(num)
if len(num_str) == 1 : num_str = '00'+ num_str
if len(num_str) == 2 : num_str = '0'+ num_str
urls = url + num_str + '.htm'
html = requests.get(urls)
html.encoding = html.apparent_encoding # 保证中文编码方式的正确
soup = BeautifulSoup(html.text,'lxml')
title = soup.find(align = 'center').text
contents += title
content = soup.find(face = '宋体').text
contents += content
with open('三国演义.txt','w') as f:
f.write(contents)
def segment(document_path= '三国演义.txt'):
"""
:param document_path:
:return:
"""
with open(document_path) as f:
document = f.read()
document = re.sub('[()::?“”《》,。!·、\d ]+', ' ', document) # 去标点
document_cut = jieba.cut(document)
result = ' '.join(document_cut)
with open('segement.txt', 'w') as f2:
f2.write(result)
print('Segement Endding')
首先安装gensim是很容易的,使用"pip install gensim"
即可。但是需要注意的是gensim对numpy的版本有要求,所以安装过程中可能会偷偷的升级你的numpy版本。而windows版的numpy直接装或者升级是有问题的。此时我们需要卸载numpy,并重新下载带mkl的符合gensim版本要求的numpy。
在gensim中,word2vec 相关的API都在包gensim.models.word2vec中。和算法有关的参数都在类gensim.models.word2vec.Word2Vec中,建议阅读官方文档。
算法需要注意的参数有:
sentences: 我们要分析的语料,可以是一个列表,或者从文件中遍历读出。后面我们会有从文件读出的例子。
size: 词向量的维度,默认值是100。这个维度的取值一般与我们的语料的大小相关,如果是不大的语料,比如小于100M的文本语料,则使用默认值一般就可以了。如果是超大的语料,建议增大维度。
window:即词向量上下文最大距离,这个参数在我们的算法原理篇中标记为c
,window越大,则和某一词较远的词也会产生上下文关系。默认值为5。在实际使用中,可以根据实际的需求来动态调整这个window的大小。如果是小语料则这个值可以设的更小。对于一般的语料这个值推荐在[5,10]之间。
sg: 即我们的word2vec两个模型的选择了。如果是0, 则是CBOW模型,是1则是Skip-Gram模型,默认是0即CBOW模型。
hs: 即我们的word2vec两个解法的选择了,如果是0, 则是Negative Sampling,是1的话并且负采样个数negative大于0, 则是Hierarchical Softmax。默认是0即Negative Sampling。
negative:即使用Negative Sampling时负采样的个数,默认是5。推荐在[3,10]之间。这个参数在我们的算法原理篇中标记为neg。
cbow_mean: 仅用于CBOW在做投影的时候,为0,则算法中的xw
为上下文的词向量之和,为1则为上下文的词向量的平均值。在我们的原理篇中,是按照词向量的平均值来描述的。个人比较喜欢用平均值来表示xw,默认值也是1,不推荐修改默认值。
min_count:需要计算词向量的最小词频。这个值可以去掉一些很生僻的低频词,默认是5。如果是小语料,可以调低这个值。
iter: 随机梯度下降法中迭代的最大次数,默认是5。对于大语料,可以增大这个值。
alpha: 在随机梯度下降法中迭代的初始步长。算法原理篇中标记为η,默认是0.025。
min_alpha: 由于算法支持在迭代的过程中逐渐减小步长,min_alpha给出了最小的迭代步长值。随机梯度下降中每轮的迭代步长可以由iter,alpha, min_alpha一起得出。这部分由于不是word2vec算法的核心内容,因此在原理篇我们没有提到。对于大语料,需要对alpha, min_alpha,iter一起调参,来选择合适的三个值。
用预处理好的语料,训练模型:
sentences = word2vec.LineSentence(sentence_path)
model = word2vec.Word2Vec(sentences, hs=1,min_count=1,window=3,size=100)
保存模型:
# 保存模型,以便重用
model.save("test_01.model")
#不能利用文本编辑器查看,但是保存了训练的全部信息,可以在读取后追加训练可以再次读取训练
model.wv.save_word2vec_format('test_01.model.txt',binary=False)
# 将模型保存成文本格式,但是保存时丢失了词汇树等部分信息,不能追加训练
追加训练:
model = gensim.models.Word2Vec.load('/tmp/mymodel')
model.train(more_sentences)
加载模型:
model = gensim.models.Word2Vec.load('/tmp/mymodel')
model.most_similar()
举例:
我们在根据《三国演义》的语料训练好模型里测试一下我们的学习得到向量(word vector)学习了哪些英雄豪杰们之间潜在相关性:
首先测试早生华发(最爱)的周瑜
:
print('Nearest 周瑜:',model.most_similar('周瑜'))
学习到的结果很满意:
Nearest 周瑜:
[('孙策', 0.6876850128173828), ('孔明', 0.6875529289245605), ('司马懿', 0.6733481287956238), ('孟获', 0.6705329418182373), ('先主', 0.6662196516990662), ('鲁肃', 0.6605409383773804), ('孙权', 0.6458742022514343), ('孙夫人', 0.643887996673584), ('姜维', 0.6326993703842163), ('有人', 0.6321758031845093)]
相关性排第一是与周瑜有总角之好,后同为江东双壁的小霸王 孙策
。他历史上与周瑜
不仅从小交好,惺惺相惜,长大后一起平定江东乱世,建立功勋。同时迎娶了三国里著名一对国色天香的姐妹花大乔,小乔
,传为一段佳话!
第二是即生瑜,何生亮
的诸葛亮的字号:孔明。《三国演义》里一个劲为了凸显诸葛亮的神机妙算,狂黑周瑜成一个气量小的小人。同时赤壁大战,这两人代表孙,刘阵营出力最多的将领谋士。故这两人的联系必然十分紧密。
第三是司马懿
,曹魏后期智力,政治最高的谋士,大都督!
后面的鲁肃
,孙权
,吴老太
,也是和周瑜关系最亲密的人物。
其他学习到的结果还有:
Nearest 刘备:, [('东吴', 0.7638486623764038), ('袁绍', 0.6992679238319397), ('刘表', 0.6835019588470459), ('吴侯', 0.6756551265716553), ('司马懿', 0.6602287888526917), ('曹', 0.6518967747688293), ('曹操', 0.6457493305206299), ('刘玄德', 0.6447073817253113), ('蜀', 0.6380304098129272), ('诸葛亮', 0.6250388026237488)]
Nearest 曹操: [('袁绍', 0.6900763511657715), ('刘备', 0.6457493901252747), ('孙策', 0.6446478962898254), ('司马懿', 0.6381756067276001), ('吴侯', 0.6193397641181946), ('孙权', 0.6192417144775391), ('蜀', 0.6191484928131104), ('周瑜', 0.6183933019638062), ('东吴', 0.6114454865455627), ('马超', 0.5959264039993286)]
Nearest 孙策: [('姜维', 0.6926037073135376), ('周瑜', 0.687684953212738), ('邓艾', 0.687220573425293), ('孙坚', 0.6793218851089478), ('司马懿', 0.6556568741798401), ('钟会', 0.6528347730636597), ('郭淮', 0.6527595520019531), ('孔明自', 0.6470344066619873), ('曹操', 0.6446478962898254), ('王平', 0.6399298906326294)]
Nearest 貂蝉: [('卓', 0.7048295140266418), ('允', 0.6404716968536377), ('身', 0.6323765516281128), ('妾', 0.6265878677368164), ('瑜', 0.6257222890853882), ('吴', 0.6242125034332275), ('父', 0.6216113567352295), ('众官', 0.6189900636672974), ('后主', 0.6172502636909485), ('干', 0.6154900789260864)]
Nearest 诸葛亮: [('亮', 0.7160214185714722), ('贤弟', 0.7146532535552979), ('子敬', 0.6765022277832031), ('此人', 0.6603602766990662), ('表曰', 0.6592696905136108), ('既', 0.6532598733901978), ('奈何', 0.6503086090087891), ('大王', 0.6495622992515564), ('吾主', 0.6492528915405273), ('玄德问', 0.6449695825576782)]
print(model.wv.doesnt_match(u"周瑜 鲁肃 吕蒙 陆逊 诸葛亮".split()))
周瑜,鲁肃,吕蒙,陆逊
都是东吴大奖并历任大都督,只有诸葛亮
是蜀国大丞相,输出结果也是诸葛亮
。说明我们向量学习到这层关系。
同样的还有:
print(model.wv.doesnt_match(u"曹操 刘备 孙权 关羽".split())) #关羽不是主公
print(model.wv.doesnt_match(u"关羽 张飞 赵云 黄忠 马超 典韦".split())) #典韦不是蜀国五虎将
print(model.wv.doesnt_match(u"诸葛亮 贾诩 张昭 马超".split()))#马超是唯一武将
当然也不是所有关系都能被学习到,学习结果的好坏也取决于我们训练语料的数量和质量。《三国演义》小说才1.8Mb,所以无法学习到更多实体之间详细相关的关系。
#!/usr/bin/env python
# encoding: utf-8
'''
@author: MrYx
@github: https://github.com/MrYxJ
'''
import jieba
import jieba.analyse
from gensim.models import word2vec
import requests
from bs4 import BeautifulSoup
import re
def crawl(url = None):
"""
从http://www.purepen.com/sgyy/爬下《三国演义》到本地txt文件
:param url:
:return:
"""
print('Waitting for crawling sentence!')
url = 'http://www.purepen.com/sgyy/'
contents = ''
for num in range(1,121):
num_str = str(num)
if len(num_str) == 1 : num_str = '00'+ num_str
if len(num_str) == 2 : num_str = '0'+ num_str
urls = url + num_str + '.htm'
html = requests.get(urls)
html.encoding = html.apparent_encoding # 保证中文编码方式的正确
soup = BeautifulSoup(html.text,'lxml')
title = soup.find(align = 'center').text
contents += title
content = soup.find(face = '宋体').text
contents += content
with open('三国演义.txt','w') as f:
f.write(contents)
def segment(document_path= '三国演义.txt'):
"""
:param document_path:
:return:
"""
with open(document_path) as f:
document = f.read()
document = re.sub('[()::?“”《》,。!·、\d ]+', ' ', document) # 去标点
document_cut = jieba.cut(document) # 结巴分词
result = ' '.join(document_cut)
with open('segement.txt', 'w') as f2:
f2.write(result)
print('Segement Endding')
def train_model(sentence_path ,model_path):
sentences = word2vec.LineSentence(sentence_path)
model = word2vec.Word2Vec(sentences, hs=1,min_count=1,window=3,size=100)
print('Word2Vec Training Endding!')
model.save(model_path)
def analyse_wordVector(model_path):
model = word2vec.Word2Vec.load(model_path)
print('Nearest 周瑜:',model.most_similar('周瑜'))
print('Nearest 刘备:,',model.most_similar(['刘备']))
print('Nearest 曹操:',model.most_similar(['曹操']))
print('Nearest 孙策:',model.most_similar(['孙策']))
print('Nearest 貂蝉:',model.most_similar(['貂蝉']))
print('Nearest 诸葛亮:', model.most_similar(['诸葛亮']))
print(model.wv.doesnt_match(u"周瑜 鲁肃 吕蒙 陆逊 诸葛亮".split()))
print(model.wv.doesnt_match(u"曹操 刘备 孙权 关羽".split()))
print(model.wv.doesnt_match(u"关羽 张飞 赵云 黄忠 马超 典韦".split()))
print(model.wv.doesnt_match(u"诸葛亮 贾诩 张昭 马超".split()))
print(model.wv.similarity('周瑜','孙策'))
print(model.wv.similarity('周瑜','小乔'))
print(model.wv.similarity('吕布', '貂蝉'))
if __name__ == '__main__':
crawl()
segment()
train_model('segement.txt','model1')
analyse_wordVector('model1')