深入解析norvig/paip-lisp中的简单Lisp程序实现
前言:学习编程语言的正确方式
在《Paradigms of Artificial Intelligence Programming》这本经典著作中,Peter Norvig通过第二章向我们展示了一个简单但完整的Lisp程序实现。这个程序能够根据语法规则生成随机的英语句子。本章的核心思想是:学习编程语言最好的方式不是死记硬背语法规则,而是通过实际构建程序来掌握语言。
语法规则的基础概念
上下文无关文法
程序的核心是一个上下文无关短语结构文法(context-free phrase-structure grammar),这种文法采用生成语法(generative syntax)范式。基本规则如下:
句子 => 名词短语 + 动词短语
名词短语 => 冠词 + 名词
动词短语 => 动词 + 名词短语
冠词 => the, a, ...
名词 => man, ball, woman, table...
动词 => hit, took, saw, liked...
这种文法之所以称为"上下文无关",是因为规则的应用不依赖于周围的单词;而"生成"则意味着这些规则共同定义了语言中所有可能的句子集合。
句子生成示例
让我们看一个句子生成的分解过程:
- 生成句子需要先生成名词短语和动词短语
- 生成名词短语需要生成冠词和名词
- 选择"the"作为冠词
- 选择"man"作为名词
- 结果名词短语是"the man"
- 生成动词短语需要生成动词和名词短语
- 选择"hit"作为动词
- 生成名词短语需要生成冠词和名词
- 选择"the"作为冠词
- 选择"ball"作为名词
- 结果名词短语是"the ball"
- 结果动词短语是"hit the ball"
- 最终句子是"The man hit the ball"
Lisp实现方案
直接映射方案
最直接的实现方式是将每个语法规则映射为一个独立的Lisp函数:
(defun sentence () (append (noun-phrase) (verb-phrase)))
(defun noun-phrase () (append (Article) (Noun)))
(defun verb-phrase () (append (Verb) (noun-phrase)))
(defun Article () (one-of '(the a)))
(defun Noun () (one-of '(man ball woman table)))
(defun Verb () (one-of '(hit took saw liked)))
这些函数都使用append
来组合结果,并依赖one-of
函数从选项列表中随机选择:
(defun one-of (set)
"从集合中随机选择一个元素并返回单元素列表"
(list (random-elt set)))
(defun random-elt (choices)
"从列表中随机选择一个元素"
(elt choices (random (length choices))))
程序运行示例
运行这个程序可以生成各种随机句子:
> (sentence) => (THE WOMAN HIT THE BALL)
> (sentence) => (THE BALL SAW THE TABLE)
> (noun-phrase) => (THE MAN)
> (verb-phrase) => (LIKED THE WOMAN)
扩展语法复杂性
当我们需要扩展语法规则,比如允许名词短语包含不定数量的形容词和介词短语时,直接映射方案会变得复杂:
(defun Adj* ()
(if (= (random 2) 0)
nil
(append (Adj) (Adj*))))
(defun PP* ()
(if (random-elt '(t nil))
(append (PP) (PP*))
nil))
这种实现虽然可行,但代码已经变得难以阅读和维护,特别是对于不熟悉Lisp的人来说。
基于规则的解决方案
更优雅的实现方式
更优雅的解决方案是将语法规则表示为数据,然后编写一个通用的生成器:
(defparameter *simple-grammar*
'((sentence -> (noun-phrase verb-phrase))
(noun-phrase -> (Article Noun))
(verb-phrase -> (Verb noun-phrase))
(Article -> the a)
(Noun -> man ball woman table)
(Verb -> hit took saw liked)))
这种表示方式几乎与原始语法规则一一对应,保持了高度的可读性。
核心生成函数
generate
函数是这个方案的核心,它处理三种情况:
- 输入是列表时,递归生成每个元素并拼接结果
- 输入是符号且有重写规则时,随机选择一个规则继续生成
- 输入是符号但没有重写规则时,直接返回该符号(作为列表)
(defun generate (phrase)
"生成随机句子或短语"
(cond ((listp phrase)
(mappend #'generate phrase))
((rewrites phrase)
(generate (random-elt (rewrites phrase))))
(t (list phrase))))
辅助函数
为了操作这些规则,我们定义了几个辅助函数:
(defun rule-lhs (rule) (first rule)) ; 获取规则左侧
(defun rule-rhs (rule) (rest (rest rule))) ; 获取规则右侧
(defun rewrites (category) (rule-rhs (assoc category *grammar*))) ; 获取类别的所有重写选项
语法扩展的便捷性
基于规则的解决方案最大的优势是扩展语法时不需要修改生成函数。例如,我们可以轻松定义更复杂的语法:
(defparameter *bigger-grammar*
'((sentence -> (noun-phrase verb-phrase))
(noun-phrase -> (Article Adj* Noun PP*) (Name) (Pronoun))
(verb-phrase -> (Verb noun-phrase PP*))
(PP* -> () (PP PP*))
(Adj* -> () (Adj Adj*))
(PP -> (Prep noun-phrase))
(Prep -> to in by with on)
(Adj -> big little blue green adiabatic)
(Article -> the a)
(Name -> Pat Kim Lee Terry Robin)
(Noun -> man ball woman table)
(Verb -> hit took saw liked)
(Pronoun -> he she it these those that)))
设置新语法后,原来的generate
函数可以直接使用:
> (generate 'sentence)
(A TABLE ON A TABLE IN THE BLUE ADIABATIC MAN SAW ROBIN WITH A LITTLE WOMAN)
两种实现路径的比较
-
直接映射方案:
- 优点:实现简单直接
- 缺点:语法规则与代码耦合,扩展性差
-
基于规则的方案:
- 优点:语法与处理逻辑分离,扩展性强
- 缺点:需要额外编写规则解释器
对于AI编程这类复杂领域,第二种方案通常是更好的选择,因为它允许我们在问题本身的术语中工作,最小化直接使用Lisp实现的部分。
编程技巧与最佳实践
- 使用
let
引入局部变量:避免使用未声明的全局变量 - 数据驱动编程:将规则表示为数据,使程序更灵活
- 抽象层:定义操作规则的工具函数,提高代码可读性
- 参数与变量:使用
defparameter
定义常量,defvar
定义变量
结语
通过这个简单的句子生成程序,我们不仅学习了Lisp的基本编程技巧,更重要的是理解了如何设计灵活、可扩展的程序架构。这种基于规则的、数据驱动的编程风格是AI编程中的核心范式,也是Lisp语言强大表达能力的体现。
在后续章节中,我们将继续探索更复杂的AI编程技术,但这个简单的例子已经展示了Lisp在处理符号计算和规则系统方面的独特优势。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考