v2.0本意是进行数据上的优化。但是由于数据量很大,存储方式由原先的写在代码中,变为在文件中,因此不得不采用异步方式,这样原先的代码绝大部分都不能使用了。
原理分享
主要进行了以下几个步骤的工作:
从网络上抓取大量诗词数据
按格式将诗词分类
对诗词正文进行分词操作
统计各词出现的频率
统计五言、七言诗句的句型,并将高频句型作为模板保存。
根据参数或者随机挑选模板,然后使用词库渲染之。
1、 抓取诗词
首先选定了 http://so.gushiwen.org/ 这个网站作为抓取的目标,这个网站上收录的诗词数量也很可观,词也基本都是主流诗人写的,是很好的数据源。总共抓取了71000首。
观察发现,所有诗歌的正文都在形如http://so.gushiwen.org/view_X...的网页上(XXX是数字),这大大简化了抓取操作。
具体抓取和解析代码在「poem-spider.js」中。最后解析出来的结果被保存在「docs/poems.txt」中。
2、 分类
这一步采用了正则表达式匹配诗文,将五言、七言诗等分开保存。
具体代码在「poem-selector.js」中。目前解析了七言诗和五言诗,保存在「docs/QiYan.txt」和「docs/WuYan.txt」中
3、 分词
为了实现词语频率的统计,以及后续词性的分析,首先要进行的是分词操作。
找到了这样一个分词工具:http://thulac.thunlp.org/demo。
下载得到了两个部分的文件:
THULAC_lite_java_run.jar
models/
jar文件是进行分词的可执行文件,models文件夹包含一些官方提供的训练数据,用以执行分词操作。
针对唐诗,分词效果其实还有提升的空间,比如说下面这样的:
神皋福 地 三 秦邑 , 玉台 金阙九仙家 。 寒光 犹恋 甘泉树 , 淑景 偏 临建 始 花 。
其中金阙九仙家应该可以被分词为金阙/九仙/家。但是这些词对人来说已经比较难懂了,估计不容易通过训练得到改善。不过好在后面的统计操作,基本能够把这些无意义的分词筛掉。
分词时的命令很简单:
$> java -jar thulac\\thulac.jar -seg_only -input data\\WuYan.txt -output data\\Separate_WuYan.txt
具体代码在「poem-separate.js」中。
4、 统计词频
使用字典显然是统计词频的最好的选择。
每读取到一个词就在字典中创建这个条目,或者将这个词的计数增加1。
具体的代码在「high-freq-word.js」中,最后筛选出了所有出现大于1次的词语,结果保存在「docs/high-frequency-word.csv」中。
光光统计高频词语是显然不够的,还需要保存词语相应的信息。
根据之前的思路,需要保存的信息包括:
字数
平仄
词性
(韵脚)
因此大体上将词以csv的格式保存在A/[平仄]/[词性]/[字数]
(如果不需要押韵)或B/[平仄]/[韵脚]/[词性]/[字数]
(如果需要押韵)的文件中。 。
韵脚和平仄怎么办?!求助万能的github,发现这样一个前辈写好的js库:https://github.com/hotoo/pinyin 。
5、统计模板
继续求助上文的分词工具,生成形如[词性][字数]([是否押韵])/...
这样的模板。
比如:
自从关路入秦川,争道何人不戏鞭。
将会被分词为:
自从_p 关路_n 入_v 秦川_ns ,_w 争道_v 何人_r 不_d 戏鞭_v 。
进而,其句型信息为:
['p2/n2/n1/ns2/', 'v2/r2/d1/v2/']
注:由于一些词性,例如ns
、ni
等,都属于同一范畴,可以将他们都合并到名词。因此在分词和保存模板的时候,将许多词性进行了合并。
当然了,除了句型信息,还要有平仄信息。由于平仄与句型并没有直接的关联(在绝句中),因此将句型和平仄分两部分保存。
查阅了一下绝句的平仄规律,以及拗句的情况等等。。最后讲平仄模板写在了代码中。预计有30+个不同的模板。
最后模板信息保存在「word/WuYan_templates.txt」和「word/QiYan_templates.txt」中。
6、渲染
渲染就是简单的用生日取当前模板或词库文件的模,得到序号。
大功告成。。。。。。。。。。。。。。。。。。。。。。。。。。
。
。
。
too naive.
实际渲染的时候,常常会出现死循环的情况。于是打印出了出现死循环情况的模板信息:发现一个共同点,模板信息中都会出现id7/
、id5/
等类型的单元,这种超长的词法单元是由于词法分析工具没有正确的分词造成的。因此,痛下杀手,将所有带有id
以及包含字数大于3的词法单元的模板全部剔除。这一下不得了,剔除了大约60%的模板。可见,在诗词方面,thulac的分词工具还有很大的提升空间。
好了,这下再也没有出现过死循环的情况。
7、优化
生成多首诗的时候,会出现每首词都需要可观的时间。分析后得出诊断,应该是读取文件的耗时。于是使用单例模式,在第一次访问某个文件时将这个文件的内容加载到一个字典中,下次直接从字典中读取。经过测试,后续的词生成速度很快。
随便贴首词出来:
[ '争心酒欢无近信', '淮北山围摆震雷', '沦迹兰芽垒潏潏', '兼仆潏潏蹭白眉' ]
[ '吊影自蹉跎', '防知静近郭', '暨滴昏朔雾', '起望自蹉跎' ]
咦!居然有重复的词!
原来,在挑选词的时候,简单的根据生日信息取模来取词,一旦遇到两个一样的词法单元,比如n2/
,平仄信息都是+-
,那么取出的词一定是一样的。于是乎,在遍历模板的时候,每次将生日增加10.大大减少了重复的概率。
来首优化后的词吧,意境还是有的:
寒宵月生筹政事,
韶濩秦关百岸风。
哲匠公堂美利戒,
文学魏帝断弦声。
自我反省
一、代码
先从最浅层的说起。这次采用了脚本语言nodejs。最明显的特点就是他的异步性,因此习惯了同步编程的我,常常不小心调入回调的坑,无法用异步的思想来思考问题。
当我需要从文件中读取词汇,然后拼装成整句。我调用fetch()
函数从词库中获取某行内容,在读取结束后,通过回调函数返回值。这导致了从比较大的词库中获取的词将最迟被返回,于是我很不美丽得设置了一个flag,用于标记总词法单元数,每返回一个词就检查是否所有模板中的词法单元都被渲染了,当所有词法单元都被渲染了,就调用回调函数返回结果。这导致这段代码十分难懂。现在觉得,可以调用async
库,采用顺序执行的方式解决问题,代码一定简洁的多,也好读的多。
另外就是代码复用性的问题,现在的渲染函数只能渲染每句长度相同的模板,对于词模板,还缺乏拼装的能力。这个功能,日后还需要加上。
二、思路
上部分已经说明了,本次的唐诗宋词项目的基本思路是:
生成模板
取词
渲染
这可以满足:押韵的要求、一定程度上的语义要求(仅限于词性搭配)、平仄的要求。
生成一首完美的诗词,这显然是不够的。至于更高的设想,在后面的部分会探讨一下。
横向比较
一、神经网络
这是一个运用机器学习的方法生成唐诗的例子。本人因此稍微去补习了下神经网络的知识。
神经网络通过很多神经单元组成的中间层,一层一层将输入信息简化,直到最上面一层,将输入信息提取成一个向量或是矩阵。
在生成近体诗的运用中,我们可以把大量的唐诗宋词数据(得瑟一下:这篇文章作者说收集唐诗数据用了一周,而我———用了在图书馆的某个悠闲的下午的三个小时)输入到神经网络,让它学习到怎么样的文法才是近体诗。
在生成阶段:不断向诗词后随机拼接词语,然后选择语义和文法上得分最高的词。
显然,平仄、语义、押韵、句型,都是神经网络需要动用神经单元分析的内容。而这个作者显然没有考虑到平仄和押韵问题。神经网络对硬件,或者说对算法的优化要求很高。这个作者为了保证性能上的可行性,显然在算法和学习程度上做出了不少的让步。如果神经网络进行了足够多的学习,相信效果会非常好。
二、遗传算法
(挖坑学习中)
三、和我一样的词库拼接型
基本都是那样啦。。。效果上,真的没觉得有好上天的。
天马行空的设想
以上的所有算法,包括那个我一知半解的遗传算法,包括自己的算不上算法的「套路」,都是从诗词的表面上,通过分析现有诗词,逆向解决问题。
思考是否可以通过模拟作诗的步骤,正向解决问题。
前期工作:
学习句型
组建关联词库,就是包含了「修饰」的关系的词语对,甚至是词群,表示这些词出现在一起更加有意义
平仄、押韵等等知识的储备
学习常见的诗词「主题」和这些主题对应的关键词。
生成:
拟定诗词主题,再通过一定程度上的发散,确定若干个关键词
通过类似搜索引擎的工作,用关键词搜索出一些相关的主干词,组成主干词库。
根据现有的句型知识,尝试着填入一些词,然后搜索关联词库,填入更多有意义的词。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。