2.3.2 例子:符号化的微分

2.3.2 例子:符号化的微分
作为一个符号操纵的演示例子,和一个数据抽象的深入一步的例子,
考虑一下代数表达式的符号微分的执行的程序的设计。我们要这个程序输入一个
代数表达式和一个变量,返回这个表达式对这个变量的微分结果。例如
 程序的输入参数是 ax2+bx+c  和x ,结果是  2ax+b。在LISP中,符号微分具有
特殊的历史意义。它是为符号运算设计的计算机语言的开发背后,有动机的例子之一。
进而,它标识着为了符号数学的工作的强有力的系统的开发的研究的开端。这种系统
现在正被越来越多的应用数学家和应用物理学家使用。

在符号微分的程序开发中,我们遵循着与2。1。1部分的有理数系统的开发的
数据抽象的策略相同的数据抽象的策略。即,我们先定义一个微分的算法来操作抽象的
对象,例如 求和,积,变量,而没有关注它们怎么被表示出来。稍后我们再来对付
表示的问题。

*抽象数据的微分程序

为了保持事情简单,我们考虑一下简单的符号微分的程序,只处理有加法和乘法的且只有两个
参数的表达式。其它的表达式的微分能被应用如下的规则。
dc/dx=0       对于c  是一个常数,或者得不同于x的变量。
dx/dx=1
d(u+v)/dx=du/dx+dv/dx
d(uv)/dx=u(dv/dx)+v(du/dx)
注意到后两条规则是递归定义的.
也就是为了得到一个求和式子的微分
我们要先计算每个子式子的微分然后再加起来.
每个子式子还分再分解,分解成越来越小的式子,
然后到最后产生了常数或者是变量,它们被微分成0或者1.

为了在程序中应用这些规则,我们有一点聪明的做法,正如在有理数算法的设计一样.
如果我们有了一些表示代数表达式的方法,我们能够区别一个表达式是否是求和式,
乘积式,常数,或者是变量.我们能够提取出一个表达式的部分.
对于一个求和式,我们要能够取得第一个加数和第二个加数.我们应该也能组装起它们.
让我们假定我们已经有程序实现了如下的选择子,组装子,和判断式.

(variable? e)
(same-variable? var1 var2)
(sum? e)
(addend e)
(augend e)
(make-sum a1 a2)
(product? e)
(multiplier e)
(multiplicand e)
(make-product m1 m2)
使用这些程序,和原生的程序number?,我们能够用如下的程序表达出微分的计算规则.

(define (deriv exp var)
     (cond ((number? exp) 0)
            ((variable? exp)
     (if (same-variable? exp var) 1 0))
     ((sum? exp)
       (make-sum (deriv (addend exp)  var) (deriv (augend exp) var) ))
            ((product? exp) (make-sum (make-product (multiplier exp)(deriv (multiplicand exp) var))(make-product (deriv (multiplier exp) var) (multipicand exp))))
       (else (error "unknown expression type" exp))))

这个deriv程序集成了整个微分的算法。由于它用抽象的数据来表达。无论我们选择怎么样
来表达代数的表达式,只要我们能设计一个合适的选择子和组装子的集合,它都能正常工作。
这个问题我们必须在如下的内容中说明。

@ 表示代数表达式

我们能够想象到有很多的方法来使用列表结构以表示代数表达式。
例如,我们能够使用符号的列表来映射常见的代数概念,表示ax+b 用(a * b + b)
然而,一个特别正常的选择是使用相同的括号前缀标识法。也就是Lisp中的
为了组合而使用的前缀标识法。即表示ax+b 为(+ (* a x) b). 然后,我们对微分问题
的数据表示如下所示:

变量是符号,它们能够被原生的判断式 symbol? 标识出来。
(define (variable? x ) (symbol? x))

判断两个变量是否相同的程序是 eq?
(define (same-variable? v1 v2)
   (and (variable? v1) (variable? v2) (eq? v1 v2)
   )
)

求和和求积被组装成列表
(define (make-sum a1 a2) (list '+ a1 a2))
(define (make-product m1 m2) (list '* m1 m2))

一个求和式是一个列表,并且它的第一个元素是 符号 +
(define (sum? x)
   (and (pair? x) (eq? (car x) '+)
   )
)

第一个加数(addend)是求和表达式的列表的第二个元素。
(define (addend s) (cadr s))


第二个加数(augend)是求和表达式的列表的第三个元素。
(define (augend s) (caddr s))


一个求积式是一个列表,并且它的第一个元素是 符号 *
(define (product? x)
     (and (pair? x) (eq? (car x) '*)
     )
)

第一个乘数(multiplier)是求积表达式的列表的第二个元素。
(define (multiplier p) (cadr p))


第二个乘数(multiplicand)是求积表达式的列表的第三个元素。
(define (multiplicand p) (caddr p))

因此我们只要组装起这些程序,作为微分程序的一部分。就可以实现符号微分的程序了。
让我们看一看应用中的例子吧。

(deriv '(+ x 3) 'x)
(+ 1 0)

(deriv '(* x y) 'x)
(+ (* x 0) (* 1 y))

(deriv '(* (* x y) (+ x 3) 'x)
(+  (* (* x y)  (+  1  0))
    (* (+ (* x 0)   (* 1 y))
       (+  x 3)
    )
)

程序产生的答案是正确的,但是不是简化的结果。

d(xy)/dx=x*0+1*y 这是对的,但是我们要程序能知道x*0=0,
1*y=y,还有 0+y=y.所以对于第二个例子,它的答案是y.
正如第三个例子显示的那样,当表达式变得很复杂时,这个答案就是
一个很严重的问题了。

我们的困难之处就如我们在有理数的实现中遇到的那个问题一样。
我们没有把答案化简到最简形式。为了完成有理数的化简,我们仅需要
修改实现的组装子和选择子。这里我们能采用一个简单的策略。我们
根本不修改deriv. 相反的是,我们修改 make-sum.如果加数都是数字,
make-sum程序将加上它们然后返回和数。并且,如果一个加数是0,
那么,make-sum程序将返回另一个加数。

(define (make-sum a1 a2)
    (cond ((=number? a1 0) a2)
          ((=number? a2 0) a1)
          ((and (number? a1) (number? a2)) (+ a1 a2))
          (else (list '+ a1 a2))
     )
)

程序 =number? 检查一个表达式是否等于一个给定的数值。

(define (=number? exp num)
    (and (number? exp) (= exp number)
    )
)

与此类似的是,我们也要修改make-product程序,应用规则是 0乘以任何值等于0,1乘以某个值等于这个值本身。
(define (make-product m1 m2)
   (cond ((or (=number? m1 0) (=number? m2 0)) 0)
         ((=number? m1 1) m2)
         ((=number? m2 1) m1)
         ((and (number? m1)  (number? m2))  (* m1 m2))
         (else (list '* m1 m2))
   )
)

这里有在我们的三个例子中,这个版本的程序是如何工作的。
(deriv '(+ x 3) 'x)
1

(deriv '(* x y) 'x)
y

(deriv '(* (* x y) (+ x 3)) 'x)
(+ (* x y) (* y (+ x 3)))

这虽然有一些改进了,但是第三个例子显示出在达到把表达式化简到最简形式的目标,还有很长的路要走。
代数表达式的化简是复杂的。因为 在不同的原因影响下,在一种目标下,一个表达式是最简单的,
从另一个目标来看可能就不算是最简形式。

练习2.56
为了处理更多类型的表达式,要显示怎么样扩展基本的微分程序。
例如,实现如下的微分规则。
d(u^n)/dx=n*u^(n-1)*(du/dx)
通过加一个新语句到deriv程序中。 定义一些适合的程序例如exponentiation?
base,exponent,make-exponentiation。(你可能使用符号**来表示求幂)。
构建如下的规则:任何数的0次幂等于1,任何数的1次幂等于它本身。


练习2.57
扩展微分程序以处理任何多个数的求和与求积。
上述的第三个例子,能被表示成如下的表达式:
(deriv '(* x y (+ x 3)) 'x)

为了实现这个目标,仅仅改变求和与求积的表示,不用修改deriv程序。
例如,addend是求和的第一个加数, augend是其它的加数的和。

练习2.58
假定我们要改变微分程序,为了让它能工作在传统的数学的表达式。
即+和*是中缀操作符 而不是前缀操作符。由于微分程序被定义在抽象数据上,
我们能修改微分程序让它在使用表达式的多种表示方式时,能正常工作。
而这种修改却局限于微分程序要操作的代数表达式的判断式,选择子和组装子。

A,显示出怎么样做能让微分的表达式成为中缀的形式,例如(x + ( 3 * (x + (y + 2))))
为了简化这个任务,假定*和+总是有两个参数,表达式总被括号包含。

B. 如果我们允许标准的代数标识法,例如(x+3*(x+y+2)),并且去掉不必要的括号,假定乘法比加法的优先级高。
问题会变得难很多。你能设计出合适的判断式,选择子,和组装子来保证我们的微分程序正常运行吗?

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/81458691