飞雪连天射白鹿,笑书神侠倚碧鸳……慕然间查大侠竟就这么离开了,对于一个从小便伴随着“狭义江湖”长大的人来说,唏嘘感叹,黯然神伤着实难免。相信不光是在下,全球华人对金庸先生构建起的武侠世界大抵都不会太陌生——六脉神剑、辟邪剑法、独孤九剑、太极剑、七伤拳、太极拳、乾坤大挪移、凌波微步、金刚指、拈花指、一阳指、天山折梅手、小无相功、降龙十八掌、打狗棒法、九阴真经,九阳神功、紫霞神功、葵花宝典.....但凡从金先生的文字中走过的人,应该都能摆出几个“偷学”来的招式,甚至武功心法也朗朗上口——归妹趋无妄,无妄趋同人,同人趋大有。甲转丙,丙转庚,庚转癸……完全不晓得这段高深莫测,又玄幻异常的口诀意味着什么,但神奇的是我居然怎么也无法忘怀这些从剑法神通风清扬老先生嘴里说出的总决式。
窃以为,无论身在何地,中华文化之所以能延绵不断,除了我们共同的黄皮肤黑眼珠外,内心深处也都隐居着一个个侠客的灵魂。在这样一个光怪陆离的武侠世界中,金庸先生无疑是我们的带头大哥,是他和其他大侠们一道,用我们博大精深的母语创造出绚烂多彩的中华武术文化。曾有人问我,如果只能带一本书远行,你会如何选择?《笑傲江湖》,我脱口而出道。好似品茗,无论几多变迁,哪怕物是人非,再读笑傲依旧唇齿留香,禅机隐隐。
我们大部分人都无缘参加金庸先生的公祭,但循着大侠的笔锋,再读他留给我们的江湖却是信手拈来的。想起当初读书岁月,那个时代还不曾有什么电子版。为了雨露均沾,均可读到,同宿舍的小伙伴们施展化整为零大法,将一本本厚厚的武侠书拆解为若干薄薄的章节,这样一来便可人手一册……于是乎,这本被大卸八块的《笑傲江湖》也不知被我们读了多少遍,最终在毕业前夕积毁销骨。此次再读笑傲,也准备与时俱进,不再用传统的人肉法,转而让机器来学习下吧。
为了完成让机器帮我重读《笑傲江湖》的宏愿,先要做如下一些准备工作。
首先需要下载《笑傲江湖》的纯文本格式,命名为笑傲江湖.txt,放在工程目录中以便读取。
其次准备python编码环境。
pip install numpy
pip install scipy
pip install matplotlib
pip install gensim
pip install jieba
安装过程中非常容易出错,请不要轻易放弃,反复尝试。安装结束后新建python工程,在mltest.py中键入:
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
x = range(10)
plt.plot(x)
plt.title("江湖")
plt.show()
运行后我们遇到了第一个国际化问题——字体,简单的解决方法如下。
寻找一些对中文支持良好的字体进行设置。例如Windows 10 中的宋体C:/Windows/Fonts/simsun.ttc
Linux 系统可以通过 fc-list 命令查看已有的字体和相应的位置,例如:
/usr/share/fonts/truetype/osx-font-family/Songti.ttc: Songti TC,宋體\-繁,宋体\-繁:style=Bold,粗體,粗体
或者直接从网上直接下载其他字体,推荐 Yahei Consolas和YaHei.Consolas.1.11b.ttf。
找到字体位置后,我们使用 matplotlib.font_manager 中的 FontProperties 进行导入:
font_songti = FontProperties(fname="/usr/share/fonts/truetype/osx-font-family/Songti.ttc")
font_simsun = FontProperties(fname="C://Windows//Fonts//simsun.ttc")
本例中我直接将simsum.ttc放在了工程文件夹下。再次运行,问题解决。
那么如何让机器识别才是本书的主角呢?做个简单的判断吧,人物在小说中的出场次数越多,其主角属性就越强。于是我们设计出一个函数来寻找《笑傲》中主角光环最强的几个人:
with open('names.txt', 'r', encoding='utf-8') as f:
data = [line.strip() for line in f.readlines()]
novels = data[::2]
names = data[1::2]
novel_names = {k: v.split() for k, v in zip(novels, names)}
def who_is_protagonist(novel, num=10):
with open('{}.txt'.format(novel), 'r', encoding='utf-8') as f:
data = f.read()
count = []
for name in novel_names[novel]:
count.append([name, data.count(name)])
count.sort(key=lambda x: x[1])
_, ax = plt.subplots()
numbers = [x[1] for x in count[-num:]]
names = [x[0] for x in count[-num:]]
ax.barh(range(num), numbers, color='blue', align='center')
ax.set_title(novel, fontsize=14, fontproperties=font_simsum)
ax.set_yticks(range(num))
ax.set_yticklabels(names, fontsize=14, fontproperties=font_simsum)
plt.show()
who_is_protagonist("笑傲江湖")
这里我们需要注意的是第二个国际化问题——python open with encoding。因为我们需要读取的是中文txt,所以open的时候务必明示encoding,否则程序读到的将会是一堆乱码,更遑论之后的分析了。运行结果如下。
令狐冲毫无意外的稳居男一号,甩开男二号君子剑不止一条街。同时林平之排名男三号还是相当的公允,虽然我认为他本应力压伪君子排名第二的,因为一直以来,我个人都比较欣赏林平之同学——忍辱负重,最终手刃仇人,快意恩仇,岂不快哉!人活着总得为了点儿什么,林平之同学在我心目中形象远比男一号令狐冲来的务实和亲近。也正因为在下坚持此观点,导致在过往的许多年里已被不少人上纲上线的抨击过价值观、人生观。所幸一路走来,初心未改。而本例中,程序显示他屈居第三,紧随岳不群之后,与我个人的解读还是比较接近的。但同时令人发指的是,我心目中永远的女一号——女神任盈盈同学居然没有排进前十,连方证,田伯光之流都稳稳的排在女神之前……着实无法接受!无法接受!无法接受啊!
为了进一步分析,我将会使用一些机器学习的向量和分词的概念,引入Word2Vec和jieba。Word2Vec是一款将单词表征为实数值向量的高效工具,原理不解释啦,有兴趣的同学请自行脑补。直接导入 gensim后还是不能直接调用他对中文进行gensim.models.Word2Vec操作,因为 Word2Vec 中默认使用空格作为分隔,而中文小说显然不符合该要求。于是乎,我们有请大名鼎鼎的jieba同学帮忙。
import jieba
import genism
for _, names in novel_names.items():
for name in names:
jieba.add_word(name)
with open("kongfu.txt", 'r', encoding='utf-8') as f:
kongfu_names = [line.strip() for line in f.readlines()]
with open("factions.txt", 'r', encoding='utf-8') as f:
faction_names = [line.strip() for line in f.readlines()]
for name in kongfu_names:
jieba.add_word(name)
for name in faction_names:
jieba.add_word(name)
novels = ["笑傲江湖"]
sentences = []
for novel in novels:
start_time = datetime.datetime.now()
print("正在处理:{}".format(novel), start_time)
with open('{}.txt'.format(novel), 'r', encoding='utf-8') as f:
data = [line.strip() for line in f.readlines() if line.strip()]
for line in data:
words = list(jieba.cut(line))
sentences.append(words)
finish_time = datetime.datetime.now()
print("处理结束:{}".format(novel), finish_time)
print(finish_time - start_time)
大概不到8秒,程序对整部笑傲江湖分词完毕。
如何训练模型并调参的确是个吊诡的过程,有的时候完全说不清为啥参数输入1效果极佳,但2的效果就大不如前,在这个方面我没什么发言权,干脆使用默认参数进行训练了事。
start_time = datetime.datetime.now()
model = gensim.models.Word2Vec(sentences, size=100, window=5, min_count=5, workers=4)
finish_time = datetime.datetime.now()
print(finish_time - start_time)
model.save("the_legendary_swordsman.model")
model = gensim.models.Word2Vec.load("the_legendary_swordsman.model")
===============
0:00:04.062430
训练结束后将模型存储在工程文件内,方便读取。
有模型后,这下大家可以一起开心的玩耍了,先看看本书中谁跟林平之比较像吧。
for k, s in model.most_similar(positive=["林平之"]):
print(k, s)
============================
岳灵珊 0.9842410683631897
令狐冲 0.9557485580444336
任盈盈 0.9483395218849182
向问天 0.9469161033630371
岳不群 0.9411010146141052
岳夫人 0.9354466199874878
黑白子 0.9249497056007385
曲非烟 0.9192472696304321
任我行 0.9161431789398193
林震南 0.9009992480278015
结果显示岳灵珊相似度最高,毕竟都是苦命人,没意见。但令狐冲跟任盈盈分列二三,这就让人百思不得其解了,一个循规蹈矩的富二代跟终日酗酒赌博的无形浪子相似度怎会如此高?看来模型参数还得持续优化。
再看门派中跟华山一派最相似的有谁?
for k, s in model.most_similar(positive=["华山派"]):
print(k, s)
==========================
恒山派 0.9620464444160461
嵩山派 0.9314655661582947
青城派 0.9305404424667358
魔教 0.9092578887939453
少林派 0.8875594139099121
相似度最高的恒山大抵是因为令狐冲的原因,跟魔教相似度也高达90%,这个解读倒是非常令人满意。介此引申出涉及国际化的第三个问题,准确的说是本地化。窃以为,找相似度的办法对于翻译人员的日常工作应该称得上相当实用了,通过计算相似度并不断改进模型,相信为本地化人员带来的不止是翻译个案的精准度提高,而是真金白银的开源节流。
聚类的方法很多,例如scikit-learn中的Kmeans等,本例为了方便起见采用层级聚类,调用 scipy 中层级聚类包。
import scipy.cluster.hierarchy as sch
def kongfu_hierarchy():
all_names = []
word_vectors = None
for name in kongfu_names:
if name in model:
all_names.append(name)
if word_vectors is None:
word_vectors = model[name]
else:
word_vectors = np.vstack((word_vectors, model[name]))
all_names = np.array(all_names)
Y = sch.linkage(word_vectors, method="ward")
_, ax = plt.subplots(figsize=(10, 30))
Z = sch.dendrogram(Y, orientation='right')
idx = Z['leaves']
ax.set_xticks([])
ax.set_yticklabels(all_names[idx], fontproperties=font_simsum)
ax.set_frame_on(False)
plt.show()
大体上看还是靠谱的,辟邪剑法作为蓝色主线统领全书,美中不足的是吸星大法跟玉女剑被归为一类,统一划入左上角的华山武功中,想必是由于令狐冲的杂学太多而乱入的吧。
“只要有人的地方就有恩怨,有恩怨就会有江湖,人就是江湖”—— 《笑傲江湖至东方不败》。侠客渐行渐远,但江湖却近在咫尺,惟愿”侠之大者“的侠义情怀永不远去!热血尚温,仅以此“披着ML外衣”的技术文章,献祭江湖!呜呼哀哉,伏惟尚飨!