Rで線形代数:LU分解

Rで 線形代数:LU分解 を試みます。

1. LU分解の説明

LU分解とは、正方行列 A を、下三角行列 L (Lower triangular)上三角行列 U (Upper triangular) の積に分解することです。

A = LU

それぞれの行列の性質は以下の通りです。

行列 L (下三角行列)

  • 行列の対角成分より右上の要素がすべて0である行列です。
  • ドゥーリトル法 による場合、L対角成分はすべて1とします。
L = [   1,    0,   0 ]
    [l_21,    1,   0 ]
    [l_31, l_32,   1 ]

行列 U (上三角行列)

  • 行列の対角成分より左下の要素がすべて0である行列です。これはQR分解の R 行列と同じ形です。
U = [u_11, u_12, u_13]
    [   0, u_22, u_23]
    [   0,    0, u_33]

LU分解の考え方:ガウスの消去法

LU分解は「ガウスの消去法」を利用します。 ガウスの消去法は、行の基本変形(ある行から別の行の定数倍を引く)を繰り返して、行列を上三角行列に変形する操作であり。最終的に得られる上三角行列が、U 行列です。

さらに、L 行列は、ガウスの消去法の各ステップで使った「係数」を記録した行列になります。例えば、2行目から1行目の c 倍を引いた場合、L 行列の (2, 1) 成分に c を格納します。この操作の記録を並べると、対角成分が1の下三角行列 L が出来上がります。

ピボット選択と PA = LU 分解

ガウスの消去法を行っていると、対角成分(ピボット)が0になってしまい、割り算ができず計算が止まってしまうことがあります。また、ピボットの値が0に非常に近いと、計算誤差が大きくなり(桁落ち)、数値的に不安定になります。

この問題を解決するのが「ピボット選択 (Pivoting)」です。 これは、計算の各ステップで、対象となる列の要素の中で絶対値が最大のものを選び、その要素がある行と現在の行を入れ替える操作です。

この「行の入れ替え」操作は、置換行列 P (Permutation matrix) を用いて表現できます。P は単位行列の行を入れ替えただけの行列です。

ピボット選択を導入したLU分解は、元の A = LU という形ではなく、以下のようになります。

PA = LU

これは「A の行を P に従って並べ替えたものなら、LU分解できる」という意味です。この PA = LU 分解は、Aが正則行列(逆行列を持つ行列)であれば、計算可能です。


2. R言語による実装

以下に、ピボット選択付きの PA = LU 分解を実装したRコードを示します。

# LU分解を実装する関数 (ピボット選択付き PA=LU)
fun_lu <- function(A) {
  n <- nrow(A)

  # L, U, P を初期化
  L <- diag(0, n) # まずはゼロ行列として開始
  U <- A # UはAのコピーとして開始
  P <- diag(1, n) # Pは単位行列として開始

  # ガウスの消去法を実行
  # kはピボットの列を指す
  for (k in 1:(n - 1)) {
    # 1. ピボット選択 (Partial Pivoting)
    # k列目のk行目以降で絶対値が最大の要素を探す
    pivot_val <- max(abs(U[k:n, k]))

    # 最大値がある行のインデックスを取得 (kからの相対位置なのでk-1を足す)
    max_row_index <- which.max(abs(U[k:n, k])) + k - 1

    # 2. 行の交換 (U, P, そしてLの計算済み部分)
    if (max_row_index != k) {
      # Uの行を交換
      temp_row_U <- U[k, ]
      U[k, ] <- U[max_row_index, ]
      U[max_row_index, ] <- temp_row_U

      # Pの行を交換 (行の入れ替え操作を記録)
      temp_row_P <- P[k, ]
      P[k, ] <- P[max_row_index, ]
      P[max_row_index, ] <- temp_row_P

      # Lの交換は少し注意が必要。これまでに計算済みの部分(k列より左)を交換する
      if (k > 1) {
        temp_row_L <- L[k, 1:(k - 1)]
        L[k, 1:(k - 1)] <- L[max_row_index, 1:(k - 1)]
        L[max_row_index, 1:(k - 1)] <- temp_row_L
      }
    }

    # 3. 前進消去
    # ピボット行より下の行の、k列目の要素を0にしていく
    for (i in (k + 1):n) {
      # 消去係数を計算し、Lに格納する
      L[i, k] <- U[i, k] / U[k, k]

      # Uの行から、(ピボット行 * 係数) を引く
      # これにより U[i, k] がゼロになる
      U[i, k:n] <- U[i, k:n] - L[i, k] * U[k, k:n]
    }
  }

  # Lの対角成分を1にする (ドゥーリトル法)
  diag(L) <- 1

  # 結果をリストで返す
  return(list(P = P, L = L, U = U))
}

# ---- テスト ----

# テスト用の行列を作成
A <- matrix(c(
  1, 1, 1,
  2, 2, 5,
  4, 6, 8
), nrow = 3, byrow = TRUE)

cat("--- LU分解のテスト ---\n")
cat("元の行列 A:\n")
print(A)

# 実装でLU分解を実行
lu_result <- fun_lu(A)

cat("\n--- 実装による結果 ---\n")
cat("P (置換行列):\n")
print(lu_result$P)
cat("\nL (下三角行列):\n")
print(lu_result$L)
cat("\nU (上三角行列):\n")
print(lu_result$U)

# 検算: PA = LU が成り立つか確認
PA <- lu_result$P %*% A
LU <- lu_result$L %*% lu_result$U

cat("\n--- 検算 ---\n")
cat("P %*% A の結果:\n")
print(PA)
cat("\nL %*% U の結果:\n")
print(LU)

# all.equalで浮動小数点数の誤差を考慮して比較
cat("\nPA と LU は等しいか:", all.equal(PA, LU), "\n")
--- LU分解のテスト ---
元の行列 A:
     [,1] [,2] [,3]
[1,]    1    1    1
[2,]    2    2    5
[3,]    4    6    8

--- 実装による結果 ---
P (置換行列):
     [,1] [,2] [,3]
[1,]    0    0    1
[2,]    0    1    0
[3,]    1    0    0

L (下三角行列):
     [,1] [,2] [,3]
[1,] 1.00  0.0    0
[2,] 0.50  1.0    0
[3,] 0.25  0.5    1

U (上三角行列):
     [,1] [,2] [,3]
[1,]    4    6  8.0
[2,]    0   -1  1.0
[3,]    0    0 -1.5

--- 検算 ---
P %*% A の結果:
     [,1] [,2] [,3]
[1,]    4    6    8
[2,]    2    2    5
[3,]    1    1    1

L %*% U の結果:
     [,1] [,2] [,3]
[1,]    4    6    8
[2,]    2    2    5
[3,]    1    1    1

PA と LU は等しいか: TRUE 

3. Rの組み込み関数 lu{Matrix} との比較

lu_builtin_result <- Matrix::lu(A)
expanded_lu <- Matrix::expand(lu_builtin_result)
expanded_lu
$L
3 x 3 Matrix of class "dtrMatrix" (unitriangular)
     [,1] [,2] [,3]
[1,] 1.00    .    .
[2,] 0.50 1.00    .
[3,] 0.25 0.50 1.00

$U
3 x 3 Matrix of class "dtrMatrix"
     [,1] [,2] [,3]
[1,]  4.0  6.0  8.0
[2,]    . -1.0  1.0
[3,]    .    . -1.5

$P
3 x 3 sparse Matrix of class "pMatrix"
          
[1,] . . |
[2,] . | .
[3,] | . .
# as.matrix()で標準の行列に変換

P_builtin <- as.matrix(expanded_lu$P)
L_builtin <- as.matrix(expanded_lu$L)
U_builtin <- as.matrix(expanded_lu$U)

cat("P (組み込み関数):\n")
print(P_builtin)
cat("\nL (組み込み関数):\n")
print(L_builtin)
cat("\nU (組み込み関数):\n")
print(U_builtin)
P (組み込み関数):
      [,1]  [,2]  [,3]
[1,] FALSE FALSE  TRUE
[2,] FALSE  TRUE FALSE
[3,]  TRUE FALSE FALSE

L (組み込み関数):
     [,1] [,2] [,3]
[1,] 1.00  0.0    0
[2,] 0.50  1.0    0
[3,] 0.25  0.5    1

U (組み込み関数):
     [,1] [,2] [,3]
[1,]    4    6  8.0
[2,]    0   -1  1.0
[3,]    0    0 -1.5

本サンプルでは実装と lu{Matrix} の結果は一致しています。

但し、サンプルとした 行列A全ての主小行列式が非ゼロ ではありませんので、LU分解の結果は一意ではありません。

行列A のLU分解はピボット選択なしでは不可能ですので、実装と lu{Matrix} の結果が同じであるのは、両者が同じ手順でピボット選択しているからに過ぎません。

以上です。