8.8 示例:随机文本 (Example: Random Text)
如果你要写一个操作单词的程序,通常使用符号会比字符串来得好,因为符号概念上是原子性的(atomic)。符号可以用 eql
一步比较完成,而字符串需要使用 string=
或 string-equal
逐一字符做比较。作为一个示例,本节将演示如何写一个程序来产生随机文本。程序的第一部分会读入一个示例文件(越大越好),用来累积之后所给入的相关单词的可能性(likeilhood)的信息。第二部分在每一个单词都根据原本的示例,产生一个随机的权重(weight)之后,随机走访根据第一部分所产生的网络。
产生的文字将会是部分可信的(locally plausible),因为任两个出现的单词也是输入文件里,两个同时出现的单词。令人惊讶的是,获得看起来是 ── 有意义的整句 ── 甚至整个段落是的频率相当高。
图 8.2 包含了程序的上半部,用来读取示例文件的代码。
(defparameter *words* (make-hash-table :size 10000))
(defconstant maxword 100)
(defun read-text (pathname)
(with-open-file (s pathname :direction :input)
(let ((buffer (make-string maxword))
(pos 0))
(do ((c (read-char s nil :eof)
(read-char s nil :eof)))
((eql c :eof))
(if (or (alpha-char-p c) (char= c #\'))
(progn
(setf (aref buffer pos) c)
(incf pos))
(progn
(unless (zerop pos)
(see (intern (string-downcase
(subseq buffer 0 pos))))
(setf pos 0))
(let ((p (punc c)))
(if p (see p)))))))))
(defun punc (c)
(case c
(#\. '|.|) (#\, '|,|) (#\; '|;|)
(#\! '|!|) (#\? '|?|) ))
(let ((prev `|.|))
(defun see (symb)
(let ((pair (assoc symb (gethash prev *words*))))
(if (null pair)
(push (cons symb 1) (gethash prev *words*))
(incf (cdr pair))))
(setf prev symb)))
图 8.2 读取示例文件
从图 8.2 所导出的数据,会被存在哈希表 *words*
里。这个哈希表的键是代表单词的符号,而值会像是下列的关联列表(assoc-lists):
((|sin| . 1) (|wide| . 2) (|sights| . 1))
使用弥尔顿的失乐园作为示例文件时,这是与键 |discover|
有关的值。它指出了 “discover” 这个单词,在诗里面用了四次,与 “wide” 用了两次,而 “sin” 与 ”sights” 各一次。(译注: 诗可以在这里找到 http://www.paradiselost.org/ )
函数 read-text
累积了这个信息。这个函数接受一个路径名(pathname),然后替每一个出现在文件中的单词,生成一个上面所展示的关联列表。它的工作方式是,逐字读取文件的每个字符,将累积的单词存在字符串 buffer
。 maxword
设成 100
,程序可以读取至多 100 个单词,对英语来说足够了。
只要下个字符是一个字(由 alpha-char-p
决定)或是一撇 (apostrophe) ,就持续累积字符。任何使单词停止累积的字符会送给 see
。数种标点符号(punctuation)也被视为是单词;函数 punc
返回标点字符的伪单词(pseudo-word)。
函数 see
注册每一个我们看过的单词。它需要知道前一个单词,以及我们刚确认过的单词 ── 这也是为什么要有变量 prev
存在。起初这个变量设为伪单词里的句点;在 see
函数被调用后, prev
变量包含了我们最后见过的单词。
在 read-text
返回之后, *words*
会包含输入文件的每一个单词的条目(entry)。通过调用 hash-table-count
你可以了解有多少个不同的单词存在。鲜少有英文文件会超过 10000 个单词。
现在来到了有趣的部份。图 8.3 包含了从图 8.2 所累积的数据来产生文字的代码。 generate-text
函数导出整个过程。它接受一个要产生几个单词的数字,以及选择性传入前一个单词。使用缺省值,会让产生出来的文件从句子的开头开始。
(defun generate-text (n &optional (prev '|.|))
(if (zerop n)
(terpri)
(let ((next (random-next prev)))
(format t "~A " next)
(generate-text (1- n) next))))
(defun random-next (prev)
(let* ((choices (gethash prev *words*))
(i (random (reduce #'+ choices
:key #'cdr))))
(dolist (pair choices)
(if (minusp (decf i (cdr pair)))
(return (car pair))))))
图 8.3 产生文字
要取得一个新的单词, generate-text
使用前一个单词,接着调用 random-next
。 random-next
函数根据每个单词出现的机率加上权重,随机选择伴随输入文本中 prev
之后的单词。
现在会是测试运行下程序的好时机。但其实你早看过一个它所产生的示例: 就是本书开头的那首诗,是使用弥尔顿的失乐园作为输入文件所产生的。
(译注: 诗可在这里看,或是浏览书的第 vi 页)
Half lost on my firmness gains more glad heart,
Or violent and from forage drives
A glimmering of all sun new begun
Both harp thy discourse they match’d,
Forth my early, is not without delay;
For their soft with whirlwind; and balm.
Undoubtedly he scornful turn’d round ninefold,
Though doubled now what redounds,
And chains these a lower world devote, yet inflicted?
Till body or rare, and best things else enjoy’d in heav’n
To stand divided light at ev’n and poise their eyes,
Or nourish, lik’ning spiritual, I have thou appear.
── Henley