8.8 示例:随机文本 (Example: Random Text)

如果你要写一个操作单词的程序,通常使用符号会比字符串来得好,因为符号概念上是原子性的(atomic)。符号可以用 eql 一步比较完成,而字符串需要使用 string=string-equal 逐一字符做比较。作为一个示例,本节将演示如何写一个程序来产生随机文本。程序的第一部分会读入一个示例文件(越大越好),用来累积之后所给入的相关单词的可能性(likeilhood)的信息。第二部分在每一个单词都根据原本的示例,产生一个随机的权重(weight)之后,随机走访根据第一部分所产生的网络。

产生的文字将会是部分可信的(locally plausible),因为任两个出现的单词也是输入文件里,两个同时出现的单词。令人惊讶的是,获得看起来是 ── 有意义的整句 ── 甚至整个段落是的频率相当高。

图 8.2 包含了程序的上半部,用来读取示例文件的代码。

  1. (defparameter *words* (make-hash-table :size 10000))
  2. (defconstant maxword 100)
  3. (defun read-text (pathname)
  4. (with-open-file (s pathname :direction :input)
  5. (let ((buffer (make-string maxword))
  6. (pos 0))
  7. (do ((c (read-char s nil :eof)
  8. (read-char s nil :eof)))
  9. ((eql c :eof))
  10. (if (or (alpha-char-p c) (char= c #\'))
  11. (progn
  12. (setf (aref buffer pos) c)
  13. (incf pos))
  14. (progn
  15. (unless (zerop pos)
  16. (see (intern (string-downcase
  17. (subseq buffer 0 pos))))
  18. (setf pos 0))
  19. (let ((p (punc c)))
  20. (if p (see p)))))))))
  21. (defun punc (c)
  22. (case c
  23. (#\. '|.|) (#\, '|,|) (#\; '|;|)
  24. (#\! '|!|) (#\? '|?|) ))
  25. (let ((prev `|.|))
  26. (defun see (symb)
  27. (let ((pair (assoc symb (gethash prev *words*))))
  28. (if (null pair)
  29. (push (cons symb 1) (gethash prev *words*))
  30. (incf (cdr pair))))
  31. (setf prev symb)))

图 8.2 读取示例文件

从图 8.2 所导出的数据,会被存在哈希表 *words* 里。这个哈希表的键是代表单词的符号,而值会像是下列的关联列表(assoc-lists):

  1. ((|sin| . 1) (|wide| . 2) (|sights| . 1))

使用弥尔顿的失乐园作为示例文件时,这是与键 |discover| 有关的值。它指出了 “discover” 这个单词,在诗里面用了四次,与 “wide” 用了两次,而 “sin” 与 ”sights” 各一次。(译注: 诗可以在这里找到 http://www.paradiselost.org/ )

函数 read-text 累积了这个信息。这个函数接受一个路径名(pathname),然后替每一个出现在文件中的单词,生成一个上面所展示的关联列表。它的工作方式是,逐字读取文件的每个字符,将累积的单词存在字符串 buffermaxword 设成 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 函数导出整个过程。它接受一个要产生几个单词的数字,以及选择性传入前一个单词。使用缺省值,会让产生出来的文件从句子的开头开始。

  1. (defun generate-text (n &optional (prev '|.|))
  2. (if (zerop n)
  3. (terpri)
  4. (let ((next (random-next prev)))
  5. (format t "~A " next)
  6. (generate-text (1- n) next))))
  7. (defun random-next (prev)
  8. (let* ((choices (gethash prev *words*))
  9. (i (random (reduce #'+ choices
  10. :key #'cdr))))
  11. (dolist (pair choices)
  12. (if (minusp (decf i (cdr pair)))
  13. (return (car pair))))))

图 8.3 产生文字

要取得一个新的单词, generate-text 使用前一个单词,接着调用 random-nextrandom-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