自動微分モジュールで遊ぶ

HackageDBでnumbersというパッケージを見つけました。この中にData.Number.Difという自動微分のモジュールが含まれていたのですが、これがなかなか面白いです。
サンプルコードを全然見かけないので、いろいろと書いてみます。

自動微分とは

自動微分というのは関数の導関数の値を自動的に求めるということですが、これには「数値的」にやる方法と「記号的」にやる方法があります。数値的にというのは
f'(x) = \frac{f(x + h) - f(x)}{h}
という式のhに適当な微小量を与えて、近似的に導関数の値を求める方法を言います。
一方記号的にというのは
(\sin x)' = \cos x
の様に、微分公式を使用して具体的に関数を求める方法を言います。

Difモジュールが行うのは後者ですが、Haskellの遅延評価と多相型を生かして非常に使い勝手の良いものになっています。プリプロセッサを通すとかいった手間もありません。

自動微分アルゴリズムについてはAutomatic differentiationを参照して下さい。

簡単な例

DifモジュールではHaskellの数値型の多相性を利用して、通常どおりに定義した関数をそのまま微分できるようにしています。
通常どおりというのは、数式に限らずif文を利用した関数なども含みます。

> :m +Data.Number.Dif
> let f(x) = x^2 + 3*x + 2
> f 2 
12.0
> deriv f 2
7.0
> deriv (deriv f) 2
2.0

上では (x^2+3x+2)' = 2x+3微分をderivという関数によって行っています。
最初のf 2の部分は通常の関数と通常の数値ですが、deriv f 2の引数のfは微分を計算するために、Dif a -> Dif aという特殊な型になっています。
ここで、Dif aという型がNumクラスやFloating等のすべての数値クラスに属しているので、通常の式と全く同じく記述することができます。Haskellでは数値リテラルも多相的に振る舞ってくれます。
また、sinなどの無限回微分出来る関数も遅延評価を利用して自然に表現されています。

最後の式は二階微分の計算の例です。*1

応用編1

ニュートン法です。

import Data.Number.Dif

newton f init eps =
  let f' = deriv f
      next = init - (unDif f init) / (f' init)
  in if abs (next - init) < eps
    then init
    else newton f next eps

関数fと初期値、許容誤差を与えるとf(x) = 0の解の近似値が求まります。
*2

> let e = 1.0e-15
> newton (\x -> x**2 - 2) 1 e
1.4142135623730951
> newton (\x -> x**2 - 3) 1 e
1.7320508075688772
> newton sin 3 e
3.141592653589793
> newton (\x -> log x - 1) 2 e
2.7182818284590455

応用編2

Gnuplotと連携させてグラフを書いてみます。例として導関数と接線を書いてみます。f(x) = ...の部分を書き換えればいろいろな関数の微分が試せます。

import Data.Number.Dif
import Data.List
import System.IO
import System.Cmd

-- x=aのまわりでの一次近似 (接線)
tangent f a =
  \x -> (deriv f a) * (x - a) + (unDif f a)

main = 
  let 
    -- 変域設定
    xmin = -2*pi
    xmax = 2*pi
    step = 0.1

    -- 表示する関数の用意
    f(x) = sin x
    funs = [f, deriv f, tangent f 1.0, tangent f 2.0]
  in 

  let 
    range  = [xmin, xmin+step .. xmax]
    graphs = [map f range| f <- funs]
    datas  = transpose (range : graphs)
  in

  do

  -- データの用意
  (datafile, hd) <- openTempFile "/tmp" "diff-data"
  mapM (hPutStrLn hd) $
    [concat . intersperse "\t" . map show $ d | d <- datas]
  hFlush hd

  -- バッチファイルの用意
  (cmdfile, hc) <- openTempFile "/tmp" "diff-command"

  hPutStrLn hc"set xzeroaxis"
  hPutStrLn hc"set yzeroaxis"
  hPutStrLn hc $ "plot " ++
    ( concat 
    . intersperse "," 
    . map (\n -> show datafile ++ " using 1:"++show n++" w l")
    ) [2..length graphs + 1]
  hPutStrLn hc "pause -1"
  hFlush hc

  system $ "gnuplot " ++ cmdfile

*1:こうするとDif型の入れ子が発生するので、( val . df . df . f . dVar) 2と書いた方がいいかもしれません

*2:unDifというのはDif a -> Dif a型の関数をa -> a型の関数に戻す為のものです。これが必要になるのは、関数の引数として与えられたfが二通りの型を持てないという事が原因です。関数の引数としてでなく、letなどで定義した場合にはunDifは必要ありません。