Open In Colab

5. 連立一次方程式: Gauss消去法とLU分解#

第3章では A \ b で解を求め、第4章ではその誤差を評価した。この章では、その中で使われる基本的な考え方を学ぶ。出発点は、高校数学でも行う「一方の式を何倍かして、もう一方の式から引く」操作である。

5.1. この章の目標#

  • Gauss消去法で連立一次方程式を三角形の形に変形できる。

  • 上三角行列を後退代入で解ける。

  • LU分解がGauss消去の記録であることを説明できる。

  • 行の入れ替え(ピボット選択)が必要な理由を説明できる。

  • n元のGauss消去を実装し、演算回数が約 \(\frac23n^3\) となる理由を説明できる。

  • JuliaでLU分解を使い、残差を確認できる。

  • 産業連関分析のように、同じ行列で右辺だけを変える問題で分解を再利用できる。

5.2. まずは消去法を思い出す#

次の連立方程式を考える。

\[\begin{split} \begin{align} \begin{aligned} 2x_1+x_2&=5,\\ 4x_1+3x_2&=11. \end{aligned} \end{align} \end{split}\]

第2式から第1式の2倍を引くと

\[ \begin{align} x_2=1 \end{align} \]

が得られる。これを第1式へ戻すと \(x_1=2\) である。行列では

\[\begin{split} \begin{align} \left[\begin{array}{cc|c} \textcolor{#0072B2}{\boxed{2}}&1&5\\ \textcolor{#B54A00}{4}&3&11 \end{array}\right] \xrightarrow{R_2\leftarrow R_2-2R_1} \left[\begin{array}{cc|c} 2&1&5\\ \boxed{0}&1&1 \end{array}\right] \end{align} \end{split}\]

のように、係数と右辺をまとめて書ける。縦線の右が右辺であり、同じ行操作を両方に施す。係数行列の対角線より下を0にしたことになる。この操作を大きな行列へ順番に行うのがGauss消去法である。

色と枠の読み方: 青い枠の \(2\) は割るときの基準(ピボット)、橙色の \(4\) は今回0にしたい成分、右の枠の \(0\) は消去した結果である。倍率は \(m=4/2=2\) となる。

\[ \underbrace{\textcolor{#B54A00}{4}-2\times\textcolor{#0072B2}{2}=0}_{\text{消去対象}},\qquad \underbrace{3-2\times1=1}_{\text{同じ行の係数}},\qquad \underbrace{11-2\times5=1}_{\text{右辺も更新}} \]

0にしたいのは一つの成分だが、引き算は右辺を含む行全体に行う。次のコードでは、この三つの引き算がどの行に対応するか確認しよう。

using LinearAlgebra

A = [2.0 1.0; 4.0 3.0]
b = [5.0, 11.0]

m = A[2,1] / A[1,1]  # 第1行を何倍するか
A2 = copy(A)
b2 = copy(b)
A2[2,:] .-= m .* A2[1,:]
b2[2]   -= m * b2[1]

println("消去後のA = \n", A2)
println("消去後のb = ", b2)
消去後のA = 
[2.0 1.0; 0.0 1.0]
消去後のb = [5.0, 1.0]

5.3. 三角行列なら順番に解ける#

消去後の行列は上三角行列である。下の式から上へ順番に未知数を求める操作を後退代入という。

\[\begin{split} \begin{align} \begin{bmatrix} u_{11}&u_{12}\\0&u_{22} \end{bmatrix} \begin{bmatrix}x_1\\x_2\end{bmatrix} = \begin{bmatrix}c_1\\c_2\end{bmatrix} \end{align} \end{split}\]

なら、まず \(x_2=c_2/u_{22}\)、次に \(x_1=(c_1-u_{12}x_2)/u_{11}\) と求める。下三角行列では、上から下へ進む前進代入を使う。

function backward_substitution(U, b)
    n = length(b)
    x = zeros(Float64, n)
    for i in n:-1:1
        known = sum((U[i,j]*x[j] for j in i+1:n); init=0.0)
        x[i] = (b[i] - known) / U[i,i]
    end
    return x
end

U = UpperTriangular(A2)
x = backward_substitution(U, b2)
println("後退代入で得た解 = ", x)
println("元の方程式の残差 = ", norm(b-A*x))
後退代入で得た解 = [2.0, 1.0]
元の方程式の残差 = 0.0

5.4. LU分解は消去操作の記録#

今の消去では「第2行から第1行の2倍を引く」と、上三角行列 \(U\) が得られた。逆に第2行へ第1行の2倍を足せば元の行列に戻る。この関係は

\[\begin{split} \underbrace{\begin{bmatrix}2&1\\4&3\end{bmatrix}}_A =\underbrace{\begin{bmatrix}1&0\\\textcolor{#0072B2}{\boxed{2}}&1\end{bmatrix}}_L \underbrace{\begin{bmatrix}2&1\\0&1\end{bmatrix}}_U \end{split}\]

と書ける。 \(L\) の下側の青い枠の2は、消去で使った倍率 \(m=4/2\) の記録である。行列の積を1行だけ計算すると、元の第2行が \(2(2,1)+(0,1)=(4,3)\) と復元される。

これがLU分解の出発点である。大きな行列でも、消去で使った倍率を下三角行列 \(L\) に、消去後の係数を上三角行列 \(U\) に保存する。\(L\) の対角成分は1とする。正則な行列について、行交換なしに各段階を0で割らずに進められれば、この形の分解は存在し一意になる。正則でも途中に0のピボットが現れることがあり、その場合は後述の行交換が必要である。

\(Ax=b\) を解くとき、\(y=Ux\) と置けば、

\[ Ly=b\quad\longrightarrow\quad Ux=y \]

の順で解ける。今の例では \(y_1=5\)、\(2y_1+y_2=11\) から \(y=(5,1)^T\) となる。これは先ほど消去で作った右辺と同じである。続く後退代入で \(x=(2,1)^T\) が得られる。

分解は係数行列の準備、代入は各右辺を解く作業である。この区別により、同じ行列で右辺だけが変わる計算を再利用できる。3次行列での消去係数と基本行列の関係は、後の補足で一度だけ詳しく確認する。

5.5. ピボットと行交換#

ここでも用いるのは行基本変形であり、列基本変形ではない。「第 \(k\) 列の消去」とは、各 \(i>k\) に対して

\[ R_i\leftarrow R_i-m_{ik}R_k \]

という行操作を行い、結果として第 \(k\) 列の対角成分より下を0にする、という意味である。第 \(k\) 段階の消去係数を

\[ \begin{align} m_{ik}=\frac{a_{ik}^{(k-1)}}{a_{kk}^{(k-1)}} \end{align} \]

と計算する。ここで \(a_{ij}^{(k-1)}\) は、\(k-1\) 段階までの行操作を終えた行列の \((i,j)\) 成分を表す。実際、第 \(k\) 列の \((i,k)\) 成分は

\[ a_{ik}^{(k-1)}-m_{ik}a_{kk}^{(k-1)}=0 \]

となる。

この分母 \(a_{kk}^{(k-1)}\) が第 \(k\) 段階の ピボット であり、その位置 \((k,k)\) を ピボット位置 という。ピボットを使って、同じ列のピボットより下にある成分を0にしていく。

例えば

\[\begin{split} \begin{align} A=\begin{bmatrix}0&1\\2&3\end{bmatrix} \end{align} \end{split}\]

では、最初のピボット位置にある \(a_{11}=0\) で割れないため、そのままでは第1列を消去できない。第1行と第2行を交換すれば、左上の成分は2になり、これをピボットとして消去を進められる。

行交換を右辺も含めて描くと、次のようになる(\(b=(1,5)^T\) の例)。

\[\begin{split} \left[\begin{array}{cc|c}0&1&1\\\textcolor{#0072B2}{2}&3&5\end{array}\right] \xrightarrow{R_1\leftrightarrow R_2} \left[\begin{array}{cc|c}\textcolor{#0072B2}{\boxed{2}}&3&5\\0&1&1\end{array}\right]. \end{split}\]

青い2が左上のピボット位置へ移る。右辺の1と5も同じ行と一緒に移る。

ピボットが0でなくても、絶対値が非常に小さいと、消去係数 \(m_{ik}\) が大きくなり、途中の値や丸め誤差が増幅されることがある。そこで第 \(k\) 段階では、まだ消去していない第 \(k\) 行から第 \(n\) 行の中から

\[ \begin{align} p=\mathop{\mathrm{arg\,max}}_{k\leq i\leq n}|a_{ik}^{(k-1)}| \end{align} \]

を選び、第 \(k\) 行と第 \(p\) 行を交換してから、\(a_{kk}^{(k-1)}\) をピボットとして使う。この方法を部分ピボット選択(partial pivoting) という。「部分」は、未処理の部分行列全体ではなく、消去中の第 \(k\) 列から候補を選ぶことを表す。候補を列の中から選ぶことと、列そのものを入れ替えることは別である。 実際に入れ替えるのは行である。

行交換をまとめて置換行列 \(P\) で表すと、行交換後の行列 \(PA\) にGauss消去を行うため、分解は

\[ \begin{align} PA=LU \end{align} \]

となる。\(P\) を左からかける \(PA\) は行交換を表す。列交換を表すのは \(AP\) のような右からの乗算であり、ここでは行っていない。連立方程式の右辺にも同じ行交換を適用すれば、

\[ \begin{align} Ax=b \quad\Longleftrightarrow\quad PAx=Pb \quad\Longleftrightarrow\quad LUx=Pb \end{align} \]

であり、方程式の解は変わらない。まず \(Ly=Pb\) を前進代入で解き、次に \(Ux=y\) を後退代入で解けばよい。

Juliaの lu(A) は通常、部分ピボット選択を用いる。返り値 F の F.p は行の並べ替えを表し、

A[F.p, :] ≈ F.L*F.U

が成り立つ。次の例では、最初のピボットが0の行列で実際に行が交換されることを確認する。

A_zero = [0.0 1.0; 2.0 3.0]
b_zero = [1.0, 5.0]
F_zero = lu(A_zero)

println("選ばれた行の順番 = ", F_zero.p)
println("行交換後の行列 = \n", A_zero[F_zero.p, :])
println("L*U = \n", F_zero.L*F_zero.U)
x_zero = F_zero \ b_zero
println("解 = ", x_zero)
println("残差 = ", norm(b_zero-A_zero*x_zero))
選ばれた行の順番 = [2, 1]
行交換後の行列 = 
[2.0 3.0; 0.0 1.0]
L*U = 
[2.0 3.0; 0.0 1.0]
解 = [1.0, 1.0]
残差 = 0.0

次に、3元の連立方程式の係数行列を lu で分解する。ここではJuliaの部分ピボット付き分解と \(PA=LU\) の確認に焦点を絞る。したがって、A ではなく行を F.p の順に並べた A[F.p, :] と F.L*F.U を比較する。同じ行列を行交換なしで消去し、基本行列から \(A=LU\) を導く計算は後の補足にまとめる。行交換の有無によって \(L,U\) は異なるが、それぞれ \(PA=LU\) と \(A=LU\) を満たす。

A = [2.0 1.0 1.0; 4.0 -6.0 0.0; -2.0 7.0 2.0]
F = lu(A)

println("L = \n", F.L)
println("U = \n", F.U)
println("行の並べ替え = ", F.p)
println("分解の確認 = ", norm(A[F.p,:] - F.L*F.U))

b = [5.0, -2.0, 9.0]
x = F \ b
println("解 = ", x)
println("残差 = ", norm(b-A*x))
L = 
[1.0 0.0 0.0; 0.5 1.0 0.0; -0.5 1.0 1.0]
U = 
[4.0 -6.0 0.0; 0.0 4.0 1.0; 0.0 0.0 1.0]
行の並べ替え = [2, 1, 3]
分解の確認 = 0.0
解 = [1.0, 1.0, 2.0]
残差 = 0.0

5.5.1. 0でなくても、小さなピボットには注意する#

\(A=\begin{bmatrix}\varepsilon&1\\1&1\end{bmatrix}\)、\(b=(1,2)^T\)、\(\varepsilon=10^{-20}\) とする。連立方程式は

\[\begin{split} \begin{aligned} \varepsilon x_1+x_2&=1,\\ x_1+x_2&=2 \end{aligned} \end{split}\]

である。行交換をしないと、最初のピボットは \(\varepsilon\) であり、第2行の \(x_1\) を消すための消去係数は

\[ m=\frac{1}{\varepsilon}=10^{20} \]

となる。行操作 \(R_2\leftarrow R_2-mR_1\) を行うと、第2式は

\[ (1-m\varepsilon)x_1+(1-m)x_2=2-m \]

となる。\(m\varepsilon=1\) なので \(x_1\) の項が消え、後退代入により

\[ x_2=\frac{2-m}{1-m} \]

を得る。その後、第1式 \(\varepsilon x_1+x_2=1\) に戻って

\[ x_1=\frac{1-x_2}{\varepsilon} \]

と求める。下の x2_no_pivot と x_no_pivot は、この2式をそのままコードにしたものである。

ただし、倍精度浮動小数点数では \(2-10^{20}\) と \(1-10^{20}\) の小さな差を保持できず、どちらも実質的に \(-10^{20}\) となる。そのため計算上は \(x_2=1\)、続いて \(x_1=(1-1)/\varepsilon=0\) となり、x_no_pivot = [0.0, 1.0] が得られる。一方、厳密解は

\[ x_1=\frac{1}{1-\varepsilon},\qquad x_2=\frac{1-2\varepsilon}{1-\varepsilon} \]

であり、\(\varepsilon=10^{-20}\) ではほぼ \((1,1)^T\) である。

この行列の条件数は約2.6である。第4章の言葉では、ここでの難しさは問題の悪条件性ではなく、計算手順にある。部分ピボット選択はこの例を改善し、通常の問題で丸めの増幅を抑える。ただし、あらゆる行列で小さな前進誤差を保証するわけではない。

eps_pivot = 1.0e-20
A_small = [eps_pivot 1.0; 1.0 1.0]
b_small = [1.0, 2.0]
multiplier = 1/eps_pivot  # 行交換なしの消去係数 m
# R2 ← R2 - m*R1 により (1-m)x₂ = 2-m
x2_no_pivot = (2-multiplier)/(1-multiplier)
# 第1式 εx₁+x₂=1 に戻って x₁ を求める
x_no_pivot = [(1-x2_no_pivot)/eps_pivot, x2_no_pivot]
x_pivot = A_small \ b_small
println("条件数 = ", cond(A_small))
println("行交換なし = ", x_no_pivot, ", 残差 = ", norm(b_small-A_small*x_no_pivot))
println("部分ピボット選択 = ", x_pivot, ", 残差 = ", norm(b_small-A_small*x_pivot))
条件数 = 2.6180339887498953
行交換なし = [0.0, 1.0], 残差 = 1.0
部分ピボット選択 = [1.0, 1.0], 残差 = 0.0

5.6. n元の消去をコードにする#

ここまでは2元・3元の例を手で追った。同じ手順を \(n\) 元へ広げると、次の二重の繰り返しになる。

  1. 第 \(k\) 列(\(k=1,\ldots,n-1\))について、ピボット \(u_{kk}\) を確認する。

  2. その下の各行 \(i=k+1,\ldots,n\) で消去係数 \(m_{ik}=u_{ik}/u_{kk}\) を計算し、\(L\) の \((i,k)\) 成分に記録する。

  3. 第 \(i\) 行から第 \(k\) 行の \(m_{ik}\) 倍を引く。

本文の3元の行列を、行交換せずに消去する流れを先に見よう。各段階で、青い枠がピボット、橙色がその列で消す成分を示す。

\[\begin{split} \begin{bmatrix} \textcolor{#0072B2}{\boxed{2}}&1&1\\ \textcolor{#B54A00}{4}&-6&0\\ \textcolor{#B54A00}{-2}&7&2 \end{bmatrix} \xrightarrow{\substack{R_2\leftarrow R_2-2R_1\\R_3\leftarrow R_3+R_1}} \begin{bmatrix} 2&1&1\\ 0&\textcolor{#0072B2}{\boxed{-8}}&-2\\ 0&\textcolor{#B54A00}{8}&3 \end{bmatrix} \xrightarrow{R_3\leftarrow R_3+R_2} \underbrace{\begin{bmatrix}2&1&1\\0&-8&-2\\0&0&1\end{bmatrix}}_U. \end{split}\]

第1列を消してから第2列へ進む。最後の行操作では第2行の第1成分が0なので、すでに作った第1列の0を保てる。記録する倍率は \(m_{21}=2\)、\(m_{31}=-1\)、\(m_{32}=-1\) である。

次の lu_nopivot は、理解のために行交換を行わないGauss消去をそのまま書いたものである。実際の計算では、部分ピボット選択を行う lu を使う。前進代入 forward_substitution は、後退代入を上から下へ進める形に書き換えたものである。

実行前に、本文の3元の例で \(L\) の対角線より下に何が並ぶか予想してみよう(後の補足で手計算した \(m_{21},m_{31},m_{32}\) である)。

function lu_nopivot(A)
    n = size(A, 1)
    size(A, 2) == n || error("正方行列を入力してください")
    U = Matrix{Float64}(A)
    L = Matrix{Float64}(I, n, n)
    for k in 1:n-1                      # 第k列を消去する
        iszero(U[k,k]) && error("ピボットが0です(第$(k)段階)。行交換が必要です")
        for i in k+1:n                  # ピボットより下の各行
            m = U[i,k]/U[k,k]           # 消去係数
            L[i,k] = m                  # 倍率をLに記録する
            U[i,k:n] .-= m .* U[k,k:n]  # R_i ← R_i - m R_k
        end
    end
    return L, U
end

function forward_substitution(L, b)
    n = length(b)
    y = zeros(Float64, n)
    for i in 1:n
        known = sum((L[i,j]*y[j] for j in 1:i-1); init=0.0)
        y[i] = (b[i] - known)/L[i,i]
    end
    return y
end

A3 = [2.0 1.0 1.0; 4.0 -6.0 0.0; -2.0 7.0 2.0]
b3 = [5.0, -2.0, 9.0]
L3, U3 = lu_nopivot(A3)
println("L = ", L3)
println("U = ", U3)
println("||A-LU|| = ", norm(A3-L3*U3))
y3 = forward_substitution(L3, b3)    # Ly = b
x3 = backward_substitution(U3, y3)   # Ux = y
println("y = ", y3, ", x = ", x3, ", 残差 = ", norm(b3-A3*x3))

try
    lu_nopivot([0.0 1.0; 2.0 3.0])
catch err
    println("行交換なしの失敗: ", err.msg)
end
L = [1.0 0.0 0.0; 2.0 1.0 0.0; -1.0 -1.0 1.0]
U = [2.0 1.0 1.0; 0.0 -8.0 -2.0; 0.0 0.0 1.0]
||A-LU|| = 0.0
y = [5.0, -12.0, 2.0], x = [1.0, 1.0, 2.0], 残差 = 0.0
行交換なしの失敗: ピボットが0です(第1段階)。行交換が必要です

3元の例では、\(L\) の対角線より下に消去係数 \(2,-1,-1\) が並び、解 \((1,1,2)^T\) が得られた。これはJuliaの lu の結果と \(L,U\) は異なるが(あちらは行交換をしている)、解は同じである。最初のピボットが0の行列では、この実装は止まる。lu が行交換を行う理由がここにある。

5.6.1. 演算回数を数える#

第4章と同じように、何回の四則演算が必要かを数えよう。第 \(k\) 段階では、ピボットより下の \(n-k\) 行それぞれについて、消去係数の除算が1回、第 \(k\) 列以降の約 \(n-k\) 個の成分に乗算と減算が1回ずつ必要である。したがって、おおよそ

\[ \sum_{k=1}^{n-1}\left\{2(n-k)^2+(n-k)\right\} \approx 2\sum_{j=1}^{n-1}j^2 \approx \frac{2}{3}n^3 \]

となる。\(\sum_{j=1}^{n-1}j^2=(n-1)n(2n-1)/6\approx n^3/3\) を使った。一方、前進代入・後退代入は各行で約 \(2i\) 回の演算を行うので、合わせて約 \(2n^2\) 回である。

作業

演算回数の目安

\(n\) を倍にすると

LU分解

\(\frac{2}{3}n^3\)

約8倍

前進代入+後退代入(右辺1本)

\(2n^2\)

約4倍

次のコードは、上の和を実際に計算して \(\frac23n^3\) と比べ、さらにJuliaの lu と代入の実行時間を測る。

for n in (100, 200, 400)
    elimination = sum(2*(n-k)^2 + (n-k) for k in 1:n-1)
    println("n=$n: 消去の演算回数=", elimination, ", 2n^3/3=", round(Int, 2n^3/3))
end

using Random
Random.seed!(1)
function best_time(f; samples=3)
    f()                                  # 初回のコンパイルを先に済ませる
    return minimum([@elapsed f() for _ in 1:samples])
end
for n in (500, 1000, 2000)
    A_rand = randn(n, n)
    b_rand = randn(n)
    F_rand = lu(A_rand)
    t_factor = best_time(() -> lu(A_rand))
    t_solve = best_time(() -> F_rand \ b_rand)
    println("n=$n: 分解 ", round(t_factor*1e3, sigdigits=3), " ms, 代入 ",
            round(t_solve*1e3, sigdigits=3), " ms")
end
n=100: 消去の演算回数=661650, 2n^3/3=666667
n=200: 消去の演算回数=5313300, 2n^3/3=5333333
n=400: 消去の演算回数=42586600, 2n^3/3=42666667
n=500: 分解 1.39 ms, 代入 0.0412 ms
n=1000: 分解 6.15 ms, 代入 0.149 ms
n=2000: 分解 30.9 ms, 代入 0.732 ms

数えた演算回数は \(\frac23n^3\) とよく一致し、\(n\) を倍にすると約8倍になる。一方、実行時間は計算機によって異なり、8倍ちょうどにはならないことが多い。lu の内部では、行列計算ライブラリ(BLAS/LAPACK)が複数のコアやキャッシュを活用するためである(第16章)。演算回数は規模の見通しを与え、実測はその計算機での実際を与える。両者を区別して読もう。

それでも「分解は代入の数十倍以上かかる」という関係は変わらない。同じ行列で右辺だけを変えて何度も解くなら、分解を1回だけ行って再利用する利益は大きい。

逆行列を計算しないのはなぜか

\(x=A^{-1}b\) を inv(A)*b と計算すると、逆行列を作るのに約 \(2n^3\) 回の演算が必要で、LU分解の約3倍である。丸め誤差の面でも、一般に A \ b の方が有利である。「逆行列を掛ける」は式の上の表現であり、計算では「分解して代入する」と読み替える。ただし、次の応用例のように、逆行列の各成分そのものに意味があり、それを知りたい場合は別である。

5.7. 応用例:産業連関分析#

経済の各産業は、自分の生産のために他の産業の製品を使う。例えば製造業が1単位生産するには、農林水産業から0.05単位、製造業自身から0.40単位、サービス業から0.20単位を投入する、というように考える。この比率を並べた行列 \(A\) を投入係数行列という。第 \(j\) 列は「産業 \(j\) が1単位生産するのに必要な各産業からの投入」である。

各産業の生産額を \(x\)、消費者や輸出などの最終需要を \(d\) とすると、生産額は「他産業への中間投入」と「最終需要」の和に等しいので、

\[ x=Ax+d \quad\Longleftrightarrow\quad (I-A)x=d \]

となる。これは連立一次方程式であり、Leontief(レオンチェフ)モデルと呼ばれる。実際の産業連関表は数百部門に及び、政府や自治体が経済波及効果を見積もる際に使われている。

ここでは3部門の架空の例を使う。最終需要をいくつも変えて比較する(シナリオ分析)ことが多いので、\(I-A\) を一度だけ分解し、各シナリオで代入だけを行う。

Ain = [0.10 0.05 0.02;     # 行: 投入する側(農林水産, 製造, サービス)
       0.20 0.40 0.15;     # 列: 生産する側
       0.10 0.20 0.25]
d_base = [50.0, 300.0, 400.0]   # 最終需要
M_leontief = I - Ain
F_leontief = lu(M_leontief)     # 一度だけ分解する

x_base = F_leontief \ d_base
println("投入係数の列和 = ", sum(Ain, dims=1))
println("必要な生産額 = ", round.(x_base, digits=1))
println("残差 = ", norm(M_leontief*x_base - d_base))

for (label, Δd) in (("製造業への最終需要 +30", [0.0, 30.0, 0.0]),
                    ("サービス業への最終需要 +30", [0.0, 0.0, 30.0]))
    Δx = F_leontief \ Δd          # 分解を再利用して代入だけを行う
    println(label, ": 生産額の増加 = ", round.(Δx, digits=2),
            ", 合計 = ", round(sum(Δx), digits=2))
end
投入係数の列和 = [0.4 0.65 0.42]
必要な生産額 = [112.2, 722.6, 741.0]
残差 = 1.6077746776921858e-13
製造業への最終需要 +30: 生産額の増加 = [3.39, 54.9, 15.09], 合計 = 73.38
サービス業への最終需要 +30: 生産額の増加 = [1.59, 11.34, 43.24], 合計 = 56.17

製造業への最終需要を30増やすと、製造業自身の生産は30より多い約54.9増え、農林水産業・サービス業の生産も増える。製造業が原材料を他産業から買い、その産業がさらに別の産業から買う、という連鎖が方程式に含まれているためである。生産額の増加の合計(約73.4)を最初の需要増30で割った値は、経済波及効果の大きさの目安になる。

\((I-A)^{-1}\) はレオンチェフ逆行列と呼ばれ、その第 \(j\) 列は「産業 \(j\) の最終需要が1増えたときの各産業の生産増」を表す。このように逆行列の成分そのものに意味がある場合には、単位行列の各列を右辺として F_leontief \ Matrix(I, 3, 3) のように計算する。これも \(n\) 本の右辺に対する代入であり、分解は1回で済む。

モデルとしての注意もある。投入係数が一定、価格が変わらない、生産能力に上限がない、という仮定を置いている(第1章のモデル誤差)。また、投入係数の列和が1未満でなければ \(x\ge0\) となる解が得られるとは限らない。数値計算で解が求まったことと、その解が経済的に意味を持つことは、別に確かめる必要がある。

5.8. \(\clubsuit\) 基本行列で消去を表す#

ここでは、本文のJulia例と同じ行列に対し、行交換を行わない消去を基本行列の積として確認する。このため、Juliaが部分ピボット選択で返した \(L,U\) と、ここで得る \(L,U\) は異なる。一般の分解を導くための補足であり、基本の解法と行交換は前の節だけで追える。

行基本変形は、行列を左からかける操作として表せる。第 \(i\) 行から第 \(k\) 行の \(m\) 倍を引く操作

\[ \begin{align} R_i\leftarrow R_i-mR_k \end{align} \]

に対応する基本行列を

\[ \begin{align} E_{ik}(m)=I-me_ie_k^T \end{align} \]

と書く。ここで \(e_i\) は第 \(i\) 成分だけが1の単位ベクトルである。\(E_{ik}(m)A\) を計算すると、\(A\) の第 \(i\) 行だけが \(R_i-mR_k\) に置き換わる。

具体例として

\[\begin{split} \begin{align} A= \begin{bmatrix} 2&1&1\\ 4&-6&0\\ -2&7&2 \end{bmatrix} \end{align} \end{split}\]

を上三角行列へ変形する。第1列の消去係数は

\[ \begin{align} m_{21}=\frac{4}{2}=2,\qquad m_{31}=\frac{-2}{2}=-1 \end{align} \]

なので、対応する基本行列は

\[\begin{split} \begin{align} E_{21}= \begin{bmatrix}1&0&0\\ \textcolor{#0072B2}{-2}&1&0\\0&0&1\end{bmatrix},\qquad E_{31}= \begin{bmatrix}1&0&0\\0&1&0\\ \textcolor{#0072B2}{1}&0&1\end{bmatrix}. \end{align} \end{split}\]

これらを順に左からかけると

\[\begin{split} \begin{align} A^{(1)}=E_{31}E_{21}A = \begin{bmatrix} 2&1&1\\ \textcolor{#B54A00}{0}&-8&-2\\ \textcolor{#B54A00}{0}&8&3 \end{bmatrix}. \end{align} \end{split}\]

次に \(m_{32}=8/(-8)=-1\) として

\[\begin{split} \begin{align} E_{32}= \begin{bmatrix}1&0&0\\0&1&0\\0&\textcolor{#0072B2}{1}&1\end{bmatrix} \end{align} \end{split}\]

を左からかければ

\[\begin{split} \begin{align} E_{32}E_{31}E_{21}A = \begin{bmatrix} 2&1&1\\ 0&-8&-2\\ 0&\textcolor{#B54A00}{0}&1 \end{bmatrix} =U \end{align} \end{split}\]

となる。ここで重要なのは、\(U\) が突然現れたのではなく、消去を表す基本行列を順番に左からかけた結果だという点である。

上式の消去操作を逆向きに戻すと

\[ \begin{align} A=E_{21}^{-1}E_{31}^{-1}E_{32}^{-1}U=LU, \end{align} \]

したがって

\[\begin{split} \begin{align} L=E_{21}^{-1}E_{31}^{-1}E_{32}^{-1} = \begin{bmatrix} 1&0&0\\ 2&1&0\\ -1&-1&1 \end{bmatrix}. \end{align} \end{split}\]

※ここでは、基本行列の逆操作が、上の基本行列の係数を反転させた操作であることを使った。基本行列の逆行列では、引いた操作を足して戻すため符号が反転する。

結果として、Gauss消去で使った係数 \(m_{21},m_{31},m_{32}\) が \(L\) の対角線より下に並ぶ。

この計算で消去係数の分母に使った \(a_{11}=2\) と \(a_{22}^{(1)}=-8\) が、前節で定義したピボットである。どちらも0でないため、この補足では行を入れ替えずに消去できた。

A_basic = [2.0 1.0 1.0; 4.0 -6.0 0.0; -2.0 7.0 2.0]
I3 = Matrix{Float64}(I,3,3)

m21 = A_basic[2,1]/A_basic[1,1]
m31 = A_basic[3,1]/A_basic[1,1]
E21 = copy(I3); E21[2,1] = -m21
E31 = copy(I3); E31[3,1] = -m31
A_step1 = E31*E21*A_basic

m32 = A_step1[3,2]/A_step1[2,2]
E32 = copy(I3); E32[3,2] = -m32
U_basic = E32*A_step1

# 各消去操作を逆向きに戻す基本行列
E21_inv = copy(I3); E21_inv[2,1] = m21
E31_inv = copy(I3); E31_inv[3,1] = m31
E32_inv = copy(I3); E32_inv[3,2] = m32
L_basic = E21_inv*E31_inv*E32_inv

println("消去係数 = ", (m21,m31,m32))
println("U = \n", U_basic)
println("L = \n", L_basic)
println("||A-LU|| = ", norm(A_basic-L_basic*U_basic))
消去係数 = (2.0, -1.0, -1.0)
U = 
[2.0 1.0 1.0; 0.0 -8.0 -2.0; 0.0 0.0 1.0]
L = 
[1.0 0.0 0.0; 2.0 1.0 0.0; -1.0 -1.0 1.0]
||A-LU|| = 0.0

5.9. まとめ#

本章のまとめ

Gauss消去法は、係数と右辺へ同じ行操作を施して三角形の方程式を作る。LU分解はその操作を再利用するための記録で、行交換がある場合は \(PA=LU\) と書く。行交換は丸めの増幅を抑える工夫であり、元の問題の悪条件性を解消するものではない。

密な \(n\times n\) 行列では、分解が \(O(n^3)\)、1本の右辺に対する前進・後退代入が \(O(n^2)\) である。右辺が3本なら、分解を3回行う代わりに、1回の分解と3回の代入で済む。

  • 1回の求解:x = A \ b

  • 同じ行列で繰り返す:F = lu(A) の後に F \ b1, F \ b2

  • 検証:元の式の相対残差 norm(b-A*x)/norm(b) と、必要に応じて条件数(ここでは b != 0)。

逆行列は、その成分自体に意味がある場合(レオンチェフ逆行列など)を除き、明示的に作らない。

未知数が数十万を超える疎行列では、分解のメモリが足りなくなることがある。そのような場合に使う反復法は、第10章「連立一次方程式の反復法」で扱う。産業連関の「波及を1段ずつ足す」計算が、その入口になる。

次の第6章では、方程式が未知数より多く、すべてを同時には満たせない場合を考える。

5.10. 演習#

本文の式・表・出力を使って説明してよい。

演習

  1. 消去と右辺:本文の2元連立方程式で、第2式から第1式の2倍を引き、消去後の右辺が1になることを確かめよ。その後、下の式から順に解け。

  2. 行交換の意味:\(A=\begin{bmatrix}0&1\\2&3\end{bmatrix}\)、\(b=(1,5)^T\) で、最初に行交換する理由を説明せよ。右辺にも同じ交換をした式を書き、\(x=(1,1)^T\) を代入して確かめよ。

  3. 分解を使い回す:同じ係数行列で右辺だけが3回変わる場合、LU分解のどの部分を再利用できるか。問題サイズを倍にすると、分解と1回の代入の仕事量はそれぞれ約何倍になるか。

  4. 実装を確かめる:lu_nopivot を本文の2元の例 \(A=\begin{bmatrix}2&1\\4&3\end{bmatrix}\) に使い、手計算の \(L,U\) と一致するか確かめよ。次に、小さなピボットの例(\(\varepsilon=10^{-20}\))に使うと何が起こるかを実行前に予想して書き、実行結果と比べよ。予想と違った点があれば、第4章の「条件数」と「安定性」のどちらの問題かを説明せよ。

  5. 探究:波及効果を比べる:産業連関の例で、どの産業の最終需要を1増やすと生産額の合計が最も増えるか。まずレオンチェフ逆行列の列和から予想し、計算で確かめよ。次に、製造業の自部門投入係数(本文の0.40)を0.60、0.80と大きくしたとき、生産額と cond(I - Ain) がどう変わるかを調べ、列和が1に近づくと何が起こるかを考察せよ。結論だけでなく、どの値を変えて何を比べたかを記録すること。

5.11. 参考文献と関連する節#