Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

LU分解

与えられた行列AAを単位下三角行列LLと上三角行列UUとの積で表すこと

A=LU=[1l211ln1ln,n11][u11u12u1nu22u2nunn]A = LU = \left[\begin{array}{cccc} 1 & & & \\ l_{21} & 1 & & \\ \vdots & \ddots & \ddots & \\ l_{n 1} & \cdots & l_{n, n-1} & 1 \end{array}\right]\left[\begin{array}{cccc} u_{11} & u_{12} & \cdots & u_{1 n} \\ & u_{22} & \cdots & u_{2 n} \\ & & \ddots & \vdots \\ & & & u_{n n} \end{array}\right]

LU分解と呼ぶ。

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

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

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

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

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

AX=BAX = B

を解くことができる。B=IB=Ib1=e1,b2=e2,,bn=enb_1 = e_1, b_2 = e_2, \cdots, b_n = e_n)とすればAX=IAX=Iを解いてX=A1X=A^{-1}を求めることができる。

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

例:行列式

A=LU=LU( 行列式の性質 AB=AB より )=1×U(L は単位下三角行列のため )=U\begin{align} |A| &= |LU|\\ &= |L| |U| \quad (\because \text{ 行列式の性質 } |AB|=|A||B| \text{ より })\\ &= 1\times |U| \quad (\because L \text{ は単位下三角行列のため })\\ &= |U| \end{align}

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

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

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

A=[10l211][u11u120u22]A = \left[\begin{array}{cc} 1 & 0 \\ l_{21} & 1 \end{array}\right] \left[\begin{array}{cc} u_{11} & u_{12} \\ 0 & u_{22} \end{array}\right]

LULUの積を計算すると

A=[u11u12l21u11l21u12+u22]A=\left[\begin{array}{ccc} u_{11} & u_{12}\\ l_{21} u_{11} & l_{21} u_{12} + u_{22} \end{array}\right]

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

a11=u11a12=u12a21=l21u11a22=l21u12+u22\begin{aligned} & a_{11}=u_{11} \\ & a_{12}=u_{12} \\ & a_{21}=l_{21} u_{11} \\ & a_{22}=l_{21} u_{12}+u_{22} \end{aligned}

これらを解いていく

一般化

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

m×nm\times n行列A=(aij)A=(a_{ij})に対して、s=min(m,n)s=\min(m,n)とおき、m×sm\times sLLs×ns\times nUUで分解することを考える。

L=(l1,,ls),U=(u1TusT)L = \left(\boldsymbol{l}_1, \ldots, \boldsymbol{l}_s\right), \quad U = \left(\begin{array}{c} \boldsymbol{u}_1^T \\ \vdots \\ \boldsymbol{u}_s^T \end{array}\right)

とすると

A=l1u1T++lsusTA = l_1 \boldsymbol{u}_1^T+\cdots+\boldsymbol{l}_s \boldsymbol{u}_s^T

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

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

l1=1a11(1)(a11(1)am1(1)),u1T=(a11(1),,a1n(1))\boldsymbol{l}_1=\frac{1}{a_{11}(1)}\left(\begin{array}{c} a_{11}(1) \\ \vdots \\ a_{m 1}(1) \end{array}\right), \quad \boldsymbol{u}_1^T=\left(a_{11}(1), \ldots, a_{1 n}(1)\right)

と置くと、

A(1)l1u1T=(0000A(2)0), すなわち A=l1u1T+(0000A(2)0)A(1)-\boldsymbol{l}_1 \boldsymbol{u}_1^T=\left(\begin{array}{c|ccc} 0 & 0 & \cdots & 0 \\ \hline 0 & & & \\ \vdots & & A(2) & \\ 0 & & & \end{array}\right), \quad \text { すなわち } A=l_1 \boldsymbol{u}_1^T+\left(\begin{array}{c|ccc} 0 & 0 & \cdots & 0 \\ \hline 0 & & & \\ \vdots & & A(2) & \\ 0 & & \end{array}\right)

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

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

l(2)=1a11(2)(a11(2)am1(2)),u(2)T=(a11(2),,a1n(2))\boldsymbol{l}(2)=\frac{1}{a_{11}(2)}\left(\begin{array}{c} a_{11}(2) \\ \vdots \\ a_{m^{\prime} 1}(2) \end{array}\right), \quad \boldsymbol{u}(2)^T=\left(a_{11}(2), \ldots, a_{1 n^{\prime}}(2)\right)

とすると(ここでm=m1,n=n1m'=m-1, n'=n-1)、同様に

A(2)l(2)u(2)T=(0000A(3)0)A(2)-l(2) \boldsymbol{u}(2)^T=\left(\begin{array}{c|ccc} 0 & 0 & \cdots & 0 \\ \hline 0 & & & \\ \vdots & & A(3) & \\ 0 & & & \end{array}\right)

となる。ここで

l2=(0l(2)),u2T=(0,u(2)T)\boldsymbol{l}_2=\left(\begin{array}{c} 0 \\ \boldsymbol{l}(2) \end{array}\right), \quad \boldsymbol{u}_2^T=\left(0, \boldsymbol{u}(2)^T\right)

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

(0000A(2)0)=l2u2T+(0000000000A(3)00)\left(\begin{array}{c|cccc} 0 & 0 & \cdots & \cdots & 0 \\ \hline 0 & & & & \\ \vdots & & A(2) & & \\ \vdots & & & & \\ 0 & & & & \end{array}\right)=l_2 \boldsymbol{u}_2^T+\left(\begin{array}{cc|ccc} 0 & 0 & 0 & \cdots & 0 \\ 0 & 0 & 0 & \cdots & 0 \\ \hline 0 & 0 & & & \\ \vdots & \vdots & & A(3) & \\ 0 & 0 & & \end{array}\right)

から

A=l1u1T+l2u2T+(0000000000A(3)00)A=l_1 \boldsymbol{u}_1^T+\boldsymbol{l}_2 \boldsymbol{u}_2^T+\left(\begin{array}{cc|ccc} 0 & 0 & 0 & \cdots & 0 \\ 0 & 0 & 0 & \cdots & 0 \\ \hline 0 & 0 & & & \\ \vdots & \vdots & & A(3) & \\ 0 & 0 & & & \end{array}\right)

となる。

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

A=l1u1T++lsusT+OA=l_1 \boldsymbol{u}_1^T+\cdots+\boldsymbol{l}_s \boldsymbol{u}_s^T+O

そうしたら

L=(l1,,ls),U=(u1TusT)L = \left(\boldsymbol{l}_1, \ldots, \boldsymbol{l}_s\right), \quad U = \left(\begin{array}{c} \boldsymbol{u}_1^T \\ \vdots \\ \boldsymbol{u}_s^T \end{array}\right)

のように行列L,UL,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=bAx = b

に対し、LU分解すると

Ax=LUx=bAx = LUx = b

であり、変数yy

Ux=yUx=y

とおき代入すると

Ly=bLy = b

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

Ly=bLy = b(前進代入)は

y1:=b1y2:=b2l21y1yn1:=bn1j=1n2ln1,jyjyn:=bnj=1n1lnjyj\begin{aligned} & y_1:=b_1 \\ & y_2:=b_2-l_{21} y_1 \\ & \vdots \\ & y_{n-1}:=b_{n-1}-\sum_{j=1}^{n-2} l_{n-1, j} y_j \\ & y_n:=b_n-\sum_{j=1}^{n-1} l_{n j} y_j \end{aligned}

Ux=yUx = y(後退代入)は

xn:=ynunnxn1:=yn1un1,nxnun1,n1x2:=y2j=3nu2jxju22x1:=y1j=2nu1jxju11\begin{aligned} & x_n:=\frac{y_n}{u_{n n}} \\ & x_{n-1}:=\frac{y_{n-1}-u_{n-1, n} x_n}{u_{n-1, n-1}}\\ & \vdots\\ & x_2:=\frac{y_2-\sum_{j=3}^n u_{2 j} x_j}{u_{22}} \\ & x_1:=\frac{y_1-\sum_{j=2}^n u_{1 j} x_j}{u_{11}} \end{aligned}

という計算

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=U1L1A^{-1} = U^{-1} L^{-1}

あるいはAx=bAx=bxxを求める操作をnn回繰り返す

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=bAx=bxxを求める操作をnn回繰り返す

y = L @ b
b = np.array([1, 2])
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.]])

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

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

L(Ux)=P1bL(Ux) = P^{-1} b

となり、並べ替えをもとに戻したb=P1bb = P^{-1} bを使ってやればいい

Ly=bLy = b(前進代入)は

yi=bij=1i1lijyjy_i = b_i - \sum^{i-1}_{j=1} l_{ij} y_j

このyyは枢軸選択つきの前進消去で得られる(b1(1),,bn(n))\left(b_1^{(1)}, \cdots, b_n^{(n)}\right)^{\top}に等しい

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

xi=1uii(bi(i)j=i+1nuijxj)(i=n,n1,,1)x_i=\frac{1}{u_{i i}}\left(b_i^{(i)}-\sum_{j=i+1}^n u_{i j} x_j\right) \quad(i=n, n-1, \cdots, 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)I=(e_1, \cdots, e_n)として

AX=IAX=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

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