連立方程式を解く

先日以下の記事にしたように、LUP分解をすると正則行列𝐴に対して𝑃𝐴 = 𝐿𝑈という分解ができるのでした。𝐿は下三角行列、𝑈は上三角行列、𝑃は並べ替えの情報が入っています。

marui.hatenablog.com

これを使って連立方程式𝐴𝑥 = 𝑏を解くプログラムを作ります。LU分解のときにお世話になったサイトをふたたび参考にします。

www.hello-statisticians.com

このページの「𝑈𝑥 = 𝑦を𝑥に関して解く」の最後の式(𝑖 < 𝑛の時の𝑥𝑖の計算)の分母にある𝑢𝑖,𝑛は𝑢𝑖,𝑖じゃないといけないので、下記コードではそのようにしています。(コメントが受け付けられていないので、どうやってページの著者に連絡すればいいのやら……)

また、上記解説記事とは違い、今回はLU分解ではなくLUP分解を経由しているので、𝐴𝑥 = 𝑏 → 𝑃𝐴𝑥 = 𝑃𝑏 → 𝐿𝑈𝑥 = 𝑃𝑏 という式変形をしてから計算を開始することになります。そのため、最初に𝑃𝑏の計算(𝑃の値に従って𝑏を並べ替え)が必要になります。

(defun solve (A b)
  "Solve a system of linear equations of Ax=b for x where A is n-by-n matrix and b is a vector of length n.
Reference: https://www.hello-statisticians.com/explain-terms-cat/matrix_factorization5.html"
  (assert (/= (det A) 0) nil "Input matrix is singular.")
  (let* ((n (array-dimension A 0))
         (x (make-array n))
         (y (make-array n))
         (bb (make-array n))
         (LUP (lup-decomp A))
         (L (car LUP))
         (U (cadr LUP))
         (P (caddr LUP)))
    (loop for i from 0 below n          ;reorder b to match P
          do (loop for j from 0 below n
                   do (if (= (aref P i j) 1)
                          (setf (aref bb i) (aref b j)))))
    (setf (aref y 0) (aref bb 0))       ;set initial value
    (loop for i from 1 below n          ;forward substitution
          do (setf (aref y i)
                   (- (aref bb i)
                      (loop for j from 0 to i
                            sum (* (aref L i j) (aref y j))))))
    (setf (aref x (1- n)) (/ (aref y (1- n)) (aref U (1- n) (1- n))))
    (loop for i from (- n 2) downto 0   ;backward substitution
          do (setf (aref x i)
                   (/ (- (aref y i)
                         (loop for j from (1- n) downto i
                               sum (* (aref U i j) (aref x j))))
                      (aref U i i))))
    x))

Common LispMatlabで同じ連立方程式を解いてみましょう。

CL-USER> (solve #2A((1 2 0) (3 4 4) (5 6 3)) #(3 7 8))
#(-7/5 11/5 3/5)
>> [1 2 0 ; 3 4 4 ; 5 6 3] \ [3 ; 7 ; 8]
ans =
   -1.4000
    2.2000
    0.6000

分数か少数かの違いはあれど、同じ結果です。

(2023-11-19追記:続いて逆行列を計算しました →逆行列の計算 - 丸井綜研

LUP分解

【2023-11-09:入力行列を書き換えながら処理をするようになっていたので、呼び出し元でも行列の内容が書き変わってしまっていました。複製を作って作業するように修正しました。】

昨日LU分解のコードを掲載しましたが、単純なアルゴリズムなので、𝑎₁,₁要素がゼロだとうまく動かないという問題がありました。

marui.hatenablog.com

それを解消するのがLUP分解です。各列を見ていって、対角要素の絶対値が最大になるように行の入れ替えを行ってからLU分解を行うことで、ゼロ除算を防ぐだけでなく小さい数での除算が少なくなって計算結果が安定するというものです。結果として、正則行列𝐴に対して𝑃𝐴=𝐿𝑈となる𝐿, 𝑈, 𝑃という3つの行列が得られます。

今日は、Cormen, Leiserson, Rivest, and Stein. Introduction to Algorithms, 4ed. MIT Press. (amzn.to/3QK1UFB) の第28章掲載の疑似コードをそのままCommon Lispで実装しました。コードが与えられているので簡単ですが、もともと疑似コードがC言語っぽいので、できあがったLISPコードもCみたいになってしまいますね。

(defun lup-decomp (A)
  "Apply LUP decomposition on square matrix A such that PA=LU. A has to be 2-dimensional array of size n-by-n. Returns three arrays L, U, and P in a list.
Reference: Cormen, Leiserson, Rivest, and Stein. Introduction to Algorithms, 4ed. MIT Press. 2022."
  (assert (/= (det A) 0) nil "Input matrix has be non-singular.")
  (let* ((n (array-dimension A 0))
         (AA (make-array `(,n ,n)))
         (L (make-array `(,n ,n) :initial-element 0))
         (U (make-array `(,n ,n) :initial-element 0))
         (P (make-array `(,n ,n) :initial-element 0))
         (perm (make-array n))
         (tmp 0)
         (kp 0))
    (loop for i from 0 below n
          do (loop for j from 0 below n
                   do (setf (aref AA i j) (aref A i j)))) ;make copy of A
    (loop for i from 0 below n
          do (setf (aref perm i i))     ;initialize perm to the identity permutation
             (setf (aref L i i) 1))     ;set 1 for diagonal element of L
    (loop for k from 0 below n
          do (setf tmp 0)
             (loop for i from k below n ;find largest absolute value in column k
                   do (if (> (abs (aref AA i k)) tmp)
                          (setf tmp (abs (aref AA i k))
                                kp i))) ;row number of the largest found so far
             (setf tmp (aref perm k)
                   (aref perm k) (aref perm kp)
                   (aref perm kp) tmp)
             (loop for i from 0 below n
                   do (setf tmp (aref AA k i) ;exahange rows k and kp
                            (aref AA k i) (aref AA kp i)
                            (aref AA kp i) tmp))
             (loop for i from (1+ k) below n
                   do (setf (aref AA i k) (/ (aref AA i k) (aref AA k k)))
                      (loop for j from (1+ k) below n
                            do (setf (aref AA i j) ;compute L and U in place in A
                                     (- (aref AA i j)
                                        (* (aref AA i k) (aref AA k j)))))))
    (loop for i from 0 below n
          do (setf (aref P i (aref perm i)) 1)) ;create P
    (loop for i from 0 below n
          do (loop for j from 0 below n
                   do (if (> i j)       ;extract L and U from A
                          (setf (aref L i j) (aref AA i j))
                          (setf (aref U i j) (aref AA i j)))))
    (list L U P)))

同書の例をつかって計算したところ、掲載されていた結果と同じになりました。あたりまえですが。

CL-USER> (lup-decomp #2A((2 0 2 0.6) (3 3 4 -2) (5 5 4 2) (-1 -2 3.4 -1)))
(#2A((1 0 0 0) (2/5 1 0 0) (-1/5 1/2 1 0) (3/5 0 0.39999995 1))
 #2A((5 5 4 2) (0 -2 2/5 -0.19999999) (0 0 4.0000005 -0.5) (0 0 0 -3.0))
 #2A((0 0 1 0) (1 0 0 0) (0 0 0 1) (0 1 0 0)))

Matlabでやってみると、次のようになりました。こちらも同じ結果ですね。

>> [L,U,P] = lu([2 0 2 0.6 ; 3 3 4 -2 ; 5 5 4 2 ; -1 -2 3.4 -1])
L =
    1.0000         0         0         0
    0.4000    1.0000         0         0
   -0.2000    0.5000    1.0000         0
    0.6000         0    0.4000    1.0000
U =
    5.0000    5.0000    4.0000    2.0000
         0   -2.0000    0.4000   -0.2000
         0         0    4.0000   -0.5000
         0         0         0   -3.0000
P =
     0     0     1     0
     1     0     0     0
     0     0     0     1
     0     1     0     0

次は連立方程式を解いてみましょうか。(→ 2023-11-08:連立方程式を解く(Common Lisp) - 丸井綜研

LU分解

10月29日(日)にShibuya.lispのもくもく会がありました。残念ながら雑談の途中で中座しないといけなくなってしまいましたが、とても楽しかったです。この日のもくもく会の目標は「逆行列を計算できるようにすること」でした。実際は途中までしかできなかったのですが、そこまでをまとめておきます。

さて、逆行列を計算する方法をChatGPT 3.5に「正則行列逆行列を計算する方法を列挙してください。方法の詳細には立ち入らないで大丈夫です。」と聞いてみたところ、以下の5つが回答されました。

  • 行列式と余因子行列を用いる方法
  • ガウスジョルダン法を用いる方法
  • LU分解を用いる方法
  • 行列の特異値分解(SVD)を用いる方法
  • 行列のクラメール法を用いる方法(特に2x2行列の場合)

ガウスジョルダンの掃き出し法は学部1年生のときの授業に習いました。「たしかにそうなるけど、こんなにシンプルでええの?」と思った記憶があります。シンプルですが計算オーダーもそれなりに大きいです。

ChatGPTがあげた5つの方法の中で、広い範囲の行列に適用できて計算速度もそれなりに速そうなLU分解を用いる方法で逆行列計算を実装していくことにしました。とはいえ今日は逆行列はやらず、LU分解だけです。

少し検索すると、先達はあらまほしき事なり、以下のような記事を発見しました。

www.hello-statisticians.com

LU分解は、正方行列𝐴を下三角行列𝐿と上三角行列𝑈の積として分解するという操作です。Matlabにはluという関数がありますが、置換行列𝑃も入った𝑃𝐴=𝐿𝑈という形にするLUP分解になっています。上記記事では純粋に𝐴=𝐿𝑈にする方法が説明されていますので、それに従ってやっていこうと思います。

(defun lu-decomp (A)
  "Apply LU decomposition on square matrix A. A has to be 2-dimensional array of size n-by-n. Returns L and U in a list, with L having ones in the diagonal elements.
Reference: https://www.hello-statisticians.com/explain-terms-cat/matrix_factorization4.html"
  (assert (= (array-dimension A 0) (array-dimension A 1)) nil "Input has to be a square matrix.")
  (let* ((n (array-dimension A 0))
         (L (make-array `(,n ,n) :initial-element 0.0))
         (U (make-array `(,n ,n) :initial-element 0.0)))
    (loop for i from 0 below n
          do (loop for j from 0 below n
                   do (cond ((= i 0)
                             (if (= j 0)
                                 (setf (aref L i j) 1.0))
                             (setf (aref U i j) (aref A i j)))
                            ((= j 0)
                             (setf (aref L i j) (/ (aref A i j) (aref U 0 0))))
                            ((= i j)
                             (setf (aref L i j) 1)
                             (setf (aref U i j)
                                   (- (aref A i j)
                                      (loop for k from 0 below j
                                            sum (* (aref L i k) (aref U k j))))))
                            ((> i j)
                             (setf (aref L i j)
                                   (/ (- (aref A i j)
                                         (loop for k from 0 below j
                                               sum (* (aref L i k) (aref U k j))))
                                      (aref U j j))))
                            ((< i j)
                             (setf (aref U i j)
                                   (- (aref A i j)
                                      (loop for k from 0 below i
                                            sum (* (aref L i k) (aref U k j)))))))))
    (list L U)))

行列[1 0 2 ; -1 2 2 ; 1 2 0]をLU分解してみましょう。

CL-USER> (lu-decomp #2A((1 0 2) (-1 2 2) #(1 2 0)))
(#2A((1.0 0.0 0.0) (-1 1 0.0) (1 1 1)) #2A((1 0 2) (0.0 2 4) (0.0 0.0 -6)))

一列に表示されてしまうのでわかりにくいですが、以下のように分解されています。

L = [ 1  0  0
     -1  1  0
      1  1  1 ]

U = [ 1  0  2
      0  2  4
      0  0 -6 ]

LU分解を使うと、少なめの計算量で連立方程式を解くことができるようになりますし、それを経由して逆行列の計算をすることもできます。ただ、今回実装した単純なLU分解には、𝑎1,1成分が0だとゼロ除算が発生してしまうという問題があり、このままでは使いものになりません。そのために使われるのがLUP分解です。いずれLUP分解を実装してみましょうね。(参考:Cormen, Leiserson, Rivest, and Stein. Introduction to Algorithms, 4ed. MIT Press. (amzn.to/3QK1UFB) 第28章)

【2023-11-06追記】LUP分解を実装しました→LUP分解(Common Lisp) - 丸井綜研

Pure Dataのexternalを作ってみた(Apple Silicon対応版バイナリを作る)

何年か前の夏休みにPure Dataのexternalオブジェクトを作るチュートリアル記事を書きました。以下の2篇です。

marui.hatenablog.com

marui.hatenablog.com

その当時はIntelチップ搭載のMacに向けて書きましたが、もう最近のmacOSは32 bitをサポートしていませんし、Apple Siliconマシンの方が多くなってきたのではないかと思います。

そこで、上記記事の前編に書いたビルド方法を以下のようにアップデートしました。もう32 bitサポートは切ってもいいのかもしれませんが、念のため、32 bitと64 bitのIntelチップ、64 bitのApple Siliconの3つに対応したバイナリが生成されるようになっています。Pdのバージョンも現時点で最新のPd-0.54-0にしました。主な変更は-arch arm64が追加されたところです。

% cc -I/Applications/Pd-0.54-0.app/Contents/Resources/src -fPIC -arch i386 -arch x86_64 -arch arm64 -c -o singen~.o singen~.c
% cc -dynamic -bundle -undefined dynamic_lookup -arch i386 -arch x86_64 -arch arm64 -o singen~.pd_darwin singen~.o

実行すると「もうサポートしていないi386用バイナリを作ろうとしている」という警告が表示されたりしますが、気にしないでOKです。本当に3アーキテクチャに対応したバイナリになっているかをfileコマンドで調べてみます。

% file singen~.pd_darwin 
singen~.pd_darwin: Mach-O universal binary with 3 architectures: [i386:Mach-O bundle i386] [x86_64:Mach-O 64-bit bundle x86_64] [arm64:Mach-O 64-bit bundle arm64]
singen~.pd_darwin (for architecture i386):      Mach-O bundle i386
singen~.pd_darwin (for architecture x86_64):    Mach-O 64-bit bundle x86_64
singen~.pd_darwin (for architecture arm64):     Mach-O 64-bit bundle arm64

ちゃんとバンドルできてますね!

行列式の計算

Common Lisp車輪の再発明をし続けています。いろいろと統計に使う関数を作っていましたが、そろそろ逆行列を計算したくなってきました。逆行列固有値線形代数の基本ですものね。*1

逆行列の計算をする前には、行列式(determinant)という、逆行列が計算できるかを判定するための方法が欲しいところです。逆行列が存在しないのに計算し始めてゼロ除算が生じてしまう、などを避けたいからです。

理系であれば大学1年前期には習うと思いますが、分かりやすい解説が以下のサイトにありました。4×4以上の行列のときの行列式の計算方法はすっかり忘れていたので、参考になりました。いつもはMatlabdet(A)で一発ですからね。

mathlandscape.com

小行列を得る

2×2行列と3×3行列のときは公式どおりに計算すればいいですが、4×4以上の行列では余因子を使って分割計算していくことになります。余因子の計算には小行列(submatrix)が必要になります。小行列( 小行列 - Wikipedia)は「指定した行と列を取り除いた行列」のことで、たとえば[11 12 13 ; 21 22 23 ; 31 32 33]の1行目・2列目を取り除いた小行列は[21 23 ; 31 33]になります。今回は2次元行列しか考えないことにして、以下のように実装してみました。

(defun submatrix (A r c)
  "Calculates submatrix (the matrix excluding specified row and column)."
  (assert (= (array-rank A) 2))
  (assert (>= (array-dimension A 0) 2))
  (assert (>= (array-dimension A 1) 2))
  (let ((res (make-array `(,(1- (array-dimension A 0)) ,(1- (array-dimension A 1)))))
        (ii 0)
        (jj 0))
    (loop for i from 0 below (array-dimension A 0)
          do (setf jj 0)
             (when (/= i r)
               (loop for j from 0 below (array-dimension A 1)
                     do (when (/= j c)
                          (setf (aref res ii jj) (aref A i j))
                          (incf jj)))
               (incf ii)))
    res))

ループで行と列をそれぞれijとして回しています。指定した行・列であるrcに一致しないときには元の行列Aから小行列resに値をコピーした上で、コピー先の添え字iijjを増加させます。一致したときにはコピー先の添え字は増加させていないので、コピー元の添え字であるijだけが増加する、ということになります。

これを実行すると以下のようになります。

CL-USER> (submatrix #2A((11 12 13) (21 22 23) (31 32 33)) 0 1)
#2A((21 23) (31 33))

そういえば、特定の行だけを取り出すget-rowというような関数を作っていたので、その逆のexclude-rowとかを作れば(exclude-col (exclude-row A 0) 1)みたいにできましたね。そっちのほうが「小さい部品の組み合わせで大きなことをする」というLISP的な思想に合致していました。こんど作り直します。

行列式の計算

2×2以上の正方行列であることが条件なので、assert部分で確認しています。2×2のときはタスキがけの計算式に従って計算しますが、それより大きな行列のときには、小行列をつかって余因子を計算していきます。速度は求めていないので、以下のようにシンプルに書いてみました。det再帰呼び出しするところが気持ちいいですね。

(defun det (A)
  "Calculates a determinant of a matrix."
  (assert (= (array-rank A) 2))
  (assert (= (array-dimension A 0) (array-dimension A 1)))
  (assert (>= (array-dimension A 0) 2))
  (if (= (array-dimension A 0) 2)
      (- (* (aref A 0 0) (aref A 1 1)) (* (aref A 0 1) (aref A 1 0)))
      (loop for n from 0 below (array-dimension A 0)
            sum (* (expt -1 (+ n)) (aref A 0 n) (det (submatrix A 0 n))))))

実際に使ってみると以下のようになります。計算結果はMatlabで計算したものと一致していました。

CL-USER> (det #2A((1 2 3) (4 5 6) (7 8 0)))
27

CL-USER> (det #2A((1 7 2 4) (1 5 2 4) (3 0 1 0) (2 1 5 -3)))
-134

大きさn×nの行列の行列式を計算するときには(n-1)×(n-1)行列の余因子をn回計算しないといけないので、けっこうな計算量になります。計算オーダーはO(n!)ということなので、大きな行列になると速度やメモリが心配ですが、このていどで遊ぶぶんには十分です。LU分解を経由する方法というのもいずれ試してみたいです。

おまけ

Juliaで計算すると以下のように……。実用上は問題ないんですが、整数にしてほしい。なんか惜しい。

julia> using LinearAlgebra

julia> det([1 2 3; 4 5 6; 7 8 0])
26.999999999999993

julia> det([1 7 2 4 ; 1 5 2 4 ; 3 0 1 0 ; 2 1 5 -3])
-134.00000000000006

さて、こうやって車輪の再発明をしていると、なんとなく週末縄文人さんがやっていることみたいだなぁと思いました。Primitive Technologyの日本語版といった趣で、とても面白いので勝手に紹介します。

www.youtube.com

*1:今月末のLISPもくもく会逆行列を計算する関数を作りたくて、その準備をしはじめているところなのです。

本日の給油(CRF1100Lアフリカツイン/2BL-SD10)

youtu.be

給油日 オドメーター (km) 給油量 (L) 単価 (円/L) 燃費 (km/L) 距離単価 (円/km)
2022-08-27 3401 16.63 157 16.42 9.56
2022-11-03 3727 17.20 158 18.95 8.34
2023-02-04 3987 16.75 157 15.52 10.12
2023-04-20 4165 10.37 157 17.16 9.15
2023-04-30 4563 18.22 160 21.84 7.32
2023-06-17 4982 16.41 159 25.53 6.23
2023-09-02 5296 19.46 178 16.14 11.03
2023-10-01 5700 18.89 180 21.39 8.42

会員カードを忘れてしまい、通常料金になってしまったので、単価が180円になってしまいました。中距離の移動が多かったので、まぁまぁの燃費となりました。単価が会員価格の175円だったら距離単価は8.18円まで下がったのに……。

5555 km

本日の給油(スーパーカブ110プロ/JA07)

給油日 オドメーター (km) 給油量 (L) 単価 (円/L) 燃費 (km/L) 距離単価 (円/km)
2022-06-22 13278.4 1.99 165.83 48.39 3.43
2022-07-27 13405.0 2.21 157.92 57.29 2.76
2022-08-31 13546.7 2.92 156.16 48.53 3.22
2022-11-05 13699.4 3.21 157.01 47.57 3.30
2023-02-06 13848.1 3.26 157.06 45.61 3.44
2023-03-02 13973.9 2.60 156.92 48.38 3.24
2023-04-01 14095.9 2.99 156.86 40.80 3.84
2023-07-13 14218.0 2.73 163.00 44.73 3.64
2023-09-25 14380.5 3.60 176.94 45.14 3.92

ガソリン高騰のため、距離単価はこれまでで最悪の1キロメートルあたり3.92円。それでも燃費がいいので助かっています。10回目の12ヶ月点検を受け、11年目も快調です。