Rで線形代数:コレスキー分解

Rで 線形代数:コレスキー分解 を試みます。

1. コレスキー分解の説明

コレスキー分解とは、対称かつ正定値な行列 A を、ある下三角行列 L とその転置行列 t(L)(これは上三角行列になります)の積に分解することです。

A = L t(L)

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

行列 A (分解対象の行列)

コレスキー分解が可能な行列は、以下の2つの条件を両方満たす必要があります。

  1. 対称行列 (Symmetric Matrix): 転置しても元の行列と等しい行列 (A = t(A))。つまり、a_ij = a_ji が成り立ちます。
  2. 正定値行列 (Positive-definite Matrix):

    • 任意のゼロではないベクトル x に対して、t(x)Ax > 0 が常に成り立つ行列。
    • 「すべての固有値が正である」、「すべての主小行列式が正である」行列です。
    • 正定値行列は常に対称です(より正確には、エルミート行列ですが、実数行列の範囲では対称行列と同じです)。

行列 L (下三角行列)

  • LU分解の L とは異なり、コレスキー分解の L対角成分が1であるとは限りませんL の対角成分は正の実数になります。
L = [l_11,    0,    0 ]
    [l_21, l_22,    0 ]
    [l_31, l_32, l_33 ]

t(L) (上三角行列)

  • L の転置行列であるため、上三角行列になります。
t(L) = [l_11, l_21, l_31]
       [   0, l_22, l_32]
       [   0,    0, l_33]

コレスキー分解の考え方

対称正定値行列 AA = L * t(L) と分解する操作となり、t(L)L の転置ですので、実質的に「一つの行列 L を見つける」操作と言えます。

A = L t(L) の式を行列の要素で書き下してみると、L の各要素を計算する公式を導き出すことができます。 例えば、3x3 行列の場合、A = L t(L) は以下のようになります。

[a_11, a_12, a_13]   [l_11,    0,    0] [l_11, l_21, l_31]
[a_21, a_22, a_23] = [l_21, l_22,    0] [   0, l_22, l_32]
[a_31, a_32, a_33]   [l_31, l_32, l_33] [   0,    0, l_33]

右辺を計算し、左辺の a_ij と比較することで、l_11, l_21, l_22, … の順に l_ij を次々と計算していくことができます。

  • a_11 = l_11²l_11 = sqrt(a_11)
  • a_21 = l_21 * l_11l_21 = a_21 / l_11
  • a_22 = l_21² + l_22²l_22 = sqrt(a_22 - l_21²)
  • … と続きます。

この計算過程で、根号(sqrt)の中身が負になると計算ができません。行列 A正定値であるという条件は、この根号の中身が常に正であることを保証してくれます。


2. R言語による実装

以下に、コレスキー分解を実装したRコードを示します。 前提条件(対称性、正定値性)のチェックも行っています。

# コレスキー分解を実装する関数
fun_cholesky <- function(A) {
  n <- nrow(A)

  # --- 前提条件のチェック ---
  # 1. 正方行列か?
  if (ncol(A) != n) {
    stop("入力は正方行列である必要があります。")
  }
  # 2. 対称行列か? (isSymmetricは誤差を考慮してくれる)
  if (!isSymmetric(A, tol = 1e-10)) {
    stop("入力された行列は対称行列ではありません。")
  }

  # Lを初期化 (ゼロ行列で)
  L <- matrix(0, n, n)

  # --- コレスキー分解の計算 ---
  # Lの要素を左上の要素から順に計算していく
  for (i in 1:n) {
    for (j in 1:i) {
      # Lの対角要素 (j == i) の場合
      if (j == i) {
        # A[i,i] から、それまでに計算したLのi行目の要素の二乗和を引く
        sum_k <- 0
        if (i > 1) {
          sum_k <- sum(L[i, 1:(i - 1)]^2)
        }

        # 平方根の中身を計算
        val_under_sqrt <- A[i, i] - sum_k

        # 3. 正定値性のチェック (平方根の中身が負にならないか)
        if (val_under_sqrt < 0) {
          stop("入力された行列は正定値ではありません。")
        }

        L[i, i] <- sqrt(val_under_sqrt)
      } else { # Lの非対角要素 (j < i) の場合

        # A[i,j] から、Lのi行目とj行目の内積を引く
        sum_k <- 0
        if (j > 1) {
          sum_k <- sum(L[i, 1:(j - 1)] * L[j, 1:(j - 1)])
        }

        L[i, j] <- (A[i, j] - sum_k) / L[j, j]
      }
    }
  }

  return(L)
}

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

# テスト用の対称正定値行列を作成
A <- matrix(c(
  4, 12, -16,
  12, 37, -43,
  -16, -43, 98
), nrow = 3, byrow = TRUE)

cat("--- コレスキー分解のテスト ---\n")
cat("元の行列 A:\n")
print(A)

# 実装でコレスキー分解を実行
cat("\n--- 実装による結果 ---\n")
L <- fun_cholesky(A)
cat("L (実装):\n")
print(L)


# 検算: L %*% t(L) が A に戻るか確認
A_reconstructed <- L %*% t(L)
cat("\n検算: L %*% t(L) の結果(Aに復元されるか):\n")
print(A_reconstructed)
cat("AとL %*% t(L)は等しいか:", all.equal(A, A_reconstructed), "\n")


# 参考: Rの組み込み関数によるコレスキー分解
cat("\n\n--- Rの組み込み関数 chol() による結果 ---\n")
# chol() はデフォルトで「上三角行列」を返すので、転置してLにする
L_builtin <- t(chol(A))
cat("L (組み込み関数):\n")
print(L_builtin)


# --- 最終的な比較 ---
cat("\n\n--- 最終比較 ---\n")
cat("実装Lと組み込み関数のLは等しいか:", all.equal(L, L_builtin), "\n")
--- コレスキー分解のテスト ---
元の行列 A:
     [,1] [,2] [,3]
[1,]    4   12  -16
[2,]   12   37  -43
[3,]  -16  -43   98

--- 実装による結果 ---
L (実装):
     [,1] [,2] [,3]
[1,]    2    0    0
[2,]    6    1    0
[3,]   -8    5    3

検算: L %*% t(L) の結果(Aに復元されるか):
     [,1] [,2] [,3]
[1,]    4   12  -16
[2,]   12   37  -43
[3,]  -16  -43   98
AとL %*% t(L)は等しいか: TRUE 


--- Rの組み込み関数 chol() による結果 ---
L (組み込み関数):
     [,1] [,2] [,3]
[1,]    2    0    0
[2,]    6    1    0
[3,]   -8    5    3


--- 最終比較 ---
実装Lと組み込み関数のLは等しいか: TRUE 

以上です。