LU分解#

与えられた行列Aを単位下三角行列Lと上三角行列Uとの積で表すこと

A=LU=[1l211ln1ln,n11][u11u12u1nu22u2nunn]

LU分解と呼ぶ。

分解すると何が嬉しいのか?#

行列式や一次方程式を少ない計算量で求められる材料としてL,Uが手に入り、全体としての計算を効率化できる可能性がある。

例:連立1次方程式・逆行列#

例えば様々なbに対してAx=bを解く場合はA=LUと一旦分解して、後でL,Uを使い回せばよい

様々なbというのは、たとえばB=(b1,b2,,bn)として行列の連立一次方程式

AX=B

を解くことができる。B=Ib1=e1,b2=e2,,bn=en)とすればAX=Iを解いてX=A1を求めることができる。

計算量はLU分解にO(n3/3)かかる。そこから逆行列を計算する場合はO(2n3/3)かかり、全体でO(n3/3+2n3/3)=O(n3)となる。これは掃き出し法と同程度だが、もとの行列が疎行列だとLUも疎行列になるので、入力が疎行列の場合も含めた計算量でいうとLU分解のほうが効率的らしい。

例:行列式#

|A|=|LU|=|L||U|( 行列式の性質 |AB|=|A||B| より )=1×|U|(L は単位下三角行列のため )=|U|

三角行列の行列式は対角行列の積になるため、|L|=1であり|A|=|U|になるし|U|Uの対角成分の積をとるだけでいい

計算手順(ドゥーリトル法)#

Lの対角成分は1に固定し、次のような分解を仮定する

A=[10l211][u11u120u22]

LUの積を計算すると

A=[u11u12l21u11l21u12+u22]

となるため、Aの各要素との対応がとれる

a11=u11a12=u12a21=l21u11a22=l21u12+u22

これらを解いていく

一般化#

もっと一般化すると次のようになる

m×n行列A=(aij)に対して、s=min(m,n)とおき、m×sLs×nUで分解することを考える。

L=(l1,,ls),U=(u1TusT)

とすると

A=l1u1T++lsusT

とできるので、この表記を使っていく。

反復計算するのでA=A(1)=(aij(1))と表記する

l1=1a11(1)(a11(1)am1(1)),u1T=(a11(1),,a1n(1))

と置くと、

A(1)l1u1T=(0000A(2)0), すなわち A=l1u1T+(0000A(2)0)

となる。残ったブロック行列をA(2)としている。

ゼロじゃない要素をl(2),u(2)とおき、同様に

l(2)=1a11(2)(a11(2)am1(2)),u(2)T=(a11(2),,a1n(2))

とすると(ここでm=m1,n=n1)、同様に

A(2)l(2)u(2)T=(0000A(3)0)

となる。ここで

l2=(0l(2)),u2T=(0,u(2)T)

のように頭に0をつけてサイズを戻せば

(0000A(2)0)=l2u2T+(0000000000A(3)00)

から $A=l1u1T+l2u2T+(0000000000A(3)00)$

となる。

これを何度も繰り返していくと、最後は残る行列がゼロ行列になる

A=l1u1T++lsusT+O

そうしたら

L=(l1,,ls),U=(u1TusT)

のように行列L,Uを作ればよい

pythonの例#

import numpy as np

# 対象の行列
A = np.array([
    [1, 2],
    [3, 4]
])
# iter 1
l1 = (1 / A[0,0]) * A[:, 0]
u1 = A[0, :]
print(f"{l1=}, {u1=}")
l1=array([1., 3.]), u1=array([1, 2])
# ベクトルの外積
np.outer(l1, u1)
array([[1., 2.],
       [3., 6.]])
A2 = A - np.outer(l1, u1)
A2
array([[ 0.,  0.],
       [ 0., -2.]])
A_ = A2[1:, 1:]

l2_ = (1 / A_[0,0]) * A_[:, 0]
u2_ = A_[0, :]
print(f"{l2_=}, {u2_=}")
l2_=array([1.]), u2_=array([-2.])
# 残りがゼロに
A3 = A_ - np.outer(l2_, u2_)
A3
array([[0.]])
# 先頭にゼロを足して次元を合わせる
l2 = np.concatenate([np.zeros(shape=(1,)), l2_])
u2 = np.concatenate([np.zeros(shape=(1,)), u2_])
# それぞれのiterationの外積の和が元の行列になる
np.outer(l1, u1) + np.outer(l2, u2)
array([[1., 2.],
       [3., 4.]])
L = np.array([l1, l2]).T
L
array([[1., 0.],
       [3., 1.]])
U = np.array([u1, u2])
U
array([[ 1.,  2.],
       [ 0., -2.]])
L @ U
array([[1., 2.],
       [3., 4.]])

行列式をLU分解で求める#

print(f"""
|A| = {np.linalg.det(A):.1f}
|L||U| = {np.linalg.det(L) * np.linalg.det(U):.1f}
Uの対角成分の積 = {np.diag(U)[0] * np.diag(U)[1]}
""")
|A| = -2.0
|L||U| = -2.0
Uの対角成分の積 = -2.0

連立一次方程式をLU分解で解く#

連立一次方程式

Ax=b

に対し、LU分解すると

Ax=LUx=b

であり、変数y

Ux=y

とおき代入すると

Ly=b

になることから変数yを求める。

Ly=b(前進代入)は

y1:=b1y2:=b2l21y1yn1:=bn1j=1n2ln1,jyjyn:=bnj=1n1lnjyj

Ux=y(後退代入)は

xn:=ynunnxn1:=yn1un1,nxnun1,n1x2:=y2j=3nu2jxju22x1:=y1j=2nu1jxju11

という計算

def forward_substitution(L, b):
    # 前進代入
    n = L.shape[0]
    y_ = np.zeros_like(b, dtype=np.double)
    for i in range(n):
        sumj = 0
        for j in range(i):
            sumj += L[i, j] * y_[j]
        y_[i] = b[i] - sumj
    return y_
def backword_substitution(U, y):
    # 後退代入
    n = U.shape[0]
    x = np.zeros_like(y, dtype=np.double)
    for i in range(n-1, -1, -1):
        sumj = 0
        for j in range(n-1, i-1, -1):
            sumj += U[i, j] * x[j]
        x[i] = (y[i] - sumj) / U[i, i]
    return x

xGETRS#

前進代入・後退代入を行うのがLAPACKのxGETRSサブルーチン

b = np.array([1, 2])
from scipy.linalg.lapack import dgetrf, dgetrs
LU, P, info = dgetrf(A)
dgetrs(LU, P, b)
(array([0. , 0.5]), 0)
y = np.linalg.inv(L) @ b
y
array([ 1., -1.])
x = np.linalg.inv(U) @ y
x
array([0. , 0.5])
# estimate b
A @ x
array([1., 2.])

逆行列#

A1=U1L1

あるいはAx=bxを求める操作をn回繰り返す

np.linalg.inv(A)
array([[-2. ,  1. ],
       [ 1.5, -0.5]])
np.linalg.inv(U) @ np.linalg.inv(L)
array([[-2. ,  1. ],
       [ 1.5, -0.5]])

Ax=bxを求める操作をn回繰り返す

y = L @ b
b = np.array([1, 2])

scipy.linalg.lu#

scipy.linalg.lu — SciPy v1.12.0 Manual

from scipy.linalg import lu

P, L, U = lu(A)
P
array([[0., 1.],
       [1., 0.]])
L
array([[1.        , 0.        ],
       [0.33333333, 1.        ]])
U
array([[3.        , 4.        ],
       [0.        , 0.66666667]])
P @ L @ U
array([[1., 2.],
       [3., 4.]])

置換行列Pつきの分解はPA=LUとなる。

Ax=bは2つの方程式Ly=Pb,Ux=yと同値。

L(Ux)=P1b

となり、並べ替えをもとに戻したb=P1bを使ってやればいい

Ly=b(前進代入)は

yi=bij=1i1lijyj

このyは枢軸選択つきの前進消去で得られる(b1(1),,bn(n))に等しい

Ux=y(後退代入)は消去法と同じく

xi=1uii(bi(i)j=i+1nuijxj)(i=n,n1,,1)

で解ける

# データの作成
import numpy as np

A = np.array([
    [1,2,3],
    [4,5,6],
    [7,8,0],
])
x_true = np.array([1, 2, 3])
b = A @ x_true
b
array([14, 32, 23])
from scipy.linalg import lu
P, L, U = lu(A)
# b := P^{-1} b
b_pi = np.linalg.inv(P) @ b
b_pi
array([23., 14., 32.])
def forward_substitution(L, b):
    # 前進代入
    n = L.shape[0]
    y_ = np.zeros_like(b, dtype=np.double)
    for i in range(n):
        sumj = 0
        for j in range(i):
            sumj += L[i, j] * y_[j]
        y_[i] = b[i] - sumj
    return y_
def backword_substitution(U, y):
    # 後退代入
    n = U.shape[0]
    x = np.zeros_like(y, dtype=np.double)
    for i in range(n-1, -1, -1):
        sumj = 0
        for j in range(n-1, i-1, -1):
            sumj += U[i, j] * x[j]
        x[i] = (y[i] - sumj) / U[i, i]
    return x
y_ = forward_substitution(L, b_pi)
y_
array([23.        , 10.71428571, 13.5       ])
x = backword_substitution(U, y_)
x
array([1., 2., 3.])

逆行列を解く#

I=(e1,,en)として

AX=I

を解く

n = A.shape[0]
I = np.eye(n)
I
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])
X = np.zeros_like(I)
for i in range(n):
    b = I[:, i]
    b_pi = np.linalg.inv(P) @ b
    y_ = forward_substitution(L, b_pi)
    x = backword_substitution(U, y_)
    X[:, i] = x
X.round(3)
array([[-1.778,  0.889, -0.111],
       [ 1.556, -0.778,  0.222],
       [-0.111,  0.222, -0.111]])
np.linalg.inv(A).round(3)
array([[-1.778,  0.889, -0.111],
       [ 1.556, -0.778,  0.222],
       [-0.111,  0.222, -0.111]])

参考文献#

  • ドゥーリトル法:LU分解 - Wikipedia

  • 一般化した計算手順:『プログラミングのための線形代数』