행렬의 역행렬 계산은 선형대수학에서 매우 중요한 개념 중 하나로, 특히 연립방정식의 해를 구하거나 시스템 제어 및 최적화 문제에서 자주 사용된다. 본 장에서는 Eigen 라이브러리를 활용하여 역행렬을 계산하는 방법에 대해 다룬다.

역행렬의 정의

역행렬(Inverse Matrix)은 정사각행렬 \mathbf{A} \in \mathbb{R}^{n \times n}이 있을 때, 행렬 \mathbf{A}^{-1}을 만족하는 행렬로서 다음 조건을 만족한다.

\mathbf{A} \mathbf{A}^{-1} = \mathbf{A}^{-1} \mathbf{A} = \mathbf{I}

여기서 \mathbf{I}n \times n 크기의 항등행렬(Identity Matrix)이다. 즉, 행렬 \mathbf{A}와 그 역행렬 \mathbf{A}^{-1}을 곱했을 때, 결과는 항등행렬이어야 한다.

역행렬이 존재하기 위한 조건

모든 행렬에 역행렬이 존재하는 것은 아니다. 정사각행렬 \mathbf{A}의 역행렬이 존재하기 위한 조건은 행렬이 가역적(Invertible)이어야 하며, 이는 다음과 같은 조건을 만족해야 한다.

  1. 행렬 \mathbf{A}의 행렬식(Determinant)이 0이 아니어야 한다.
\text{det}(\mathbf{A}) \neq 0
  1. 행렬 \mathbf{A}는 풀랭크(Full Rank)여야 한다. 즉, 행렬의 계수(Rank)는 n이어야 한다.

역행렬의 계산 방법

역행렬을 구하는 방법에는 여러 가지가 있으며, 각 방법은 행렬의 크기와 특성에 따라 효율성이 달라진다. Eigen 라이브러리에서는 다양한 방식으로 역행렬을 계산할 수 있으며, 주로 다음과 같은 방법들이 사용된다.

1. 고전적인 소거법 (Gauss-Jordan 소거법)

가우스-조르단 소거법은 행렬의 가우스 소거 과정을 통해 역행렬을 구하는 방법이다. 이 방법은 다음과 같은 방식으로 진행된다.

  1. 행렬 \mathbf{A}를 왼쪽에 두고, 동일 크기의 항등행렬 \mathbf{I}를 오른쪽에 두어 확장된 행렬 [\mathbf{A} | \mathbf{I}]을 만든다.
  2. 가우스 소거법을 적용하여 \mathbf{A}를 항등행렬로 변환하고, 그와 동시에 \mathbf{I}를 변환된 부분이 \mathbf{A}^{-1}이 된다.

이 방법은 기본적으로 \mathbf{A}의 차수가 크지 않을 때 유효하다.

2. LU 분해를 통한 역행렬 계산

LU 분해는 행렬 \mathbf{A}를 하삼각행렬 \mathbf{L}과 상삼각행렬 \mathbf{U}로 분해하는 방식이다. 이 방법을 통해서도 역행렬을 구할 수 있다. Eigen 라이브러리에서는 \texttt{FullPivLU} 또는 \texttt{PartialPivLU} 클래스를 사용하여 쉽게 역행렬을 계산할 수 있다.

LU 분해의 원리는 다음과 같다.

\mathbf{A} = \mathbf{L} \mathbf{U}

따라서, 행렬 \mathbf{A}의 역행렬은 다음과 같이 계산된다.

\mathbf{A}^{-1} = \mathbf{U}^{-1} \mathbf{L}^{-1}

이 방법은 연립방정식 풀이에서 효율적으로 사용되며, 행렬이 크게 변하지 않는 상황에서 반복적으로 역행렬을 계산할 때 유리하다.

#include <Eigen/Dense>
#include <iostream>

int main() {
    Eigen::Matrix3d A;
    A << 1, 2, 3,
         0, 1, 4,
         5, 6, 0;

    Eigen::Matrix3d A_inv = A.inverse();

    std::cout << "A inverse: \n" << A_inv << std::endl;

    return 0;
}

3. QR 분해를 통한 역행렬 계산

QR 분해는 행렬 \mathbf{A}를 직교행렬 \mathbf{Q}와 상삼각행렬 \mathbf{R}로 분해하는 방법이다. Eigen 라이브러리에서는 \texttt{HouseholderQR} 클래스나 \texttt{ColPivHouseholderQR} 클래스를 사용하여 QR 분해를 수행할 수 있다. QR 분해는 특히 행렬의 크기가 클 때 효율적으로 작동한다.

QR 분해의 정의는 다음과 같다.

\mathbf{A} = \mathbf{Q} \mathbf{R}

여기서 \mathbf{Q}는 직교행렬이고, \mathbf{R}은 상삼각행렬이다. 역행렬은 다음과 같이 계산된다.

\mathbf{A}^{-1} = \mathbf{R}^{-1} \mathbf{Q}^{T}

QR 분해는 행렬의 크기가 크거나 연립방정식이 비정규화된 경우에도 안정적인 계산이 가능하다는 장점이 있다.

#include <Eigen/Dense>
#include <iostream>

int main() {
    Eigen::Matrix3d A;
    A << 1, 2, 3,
         0, 1, 4,
         5, 6, 0;

    Eigen::HouseholderQR<Eigen::Matrix3d> qr(A);
    Eigen::Matrix3d A_inv = qr.inverse();

    std::cout << "A inverse (QR decomposition): \n" << A_inv << std::endl;

    return 0;
}

4. SVD(특이값 분해)를 통한 역행렬 계산

특이값 분해(Singular Value Decomposition, SVD)는 행렬을 세 개의 행렬의 곱으로 분해하는 방법이다. 특히, 행렬이 정규화되지 않았거나 랭크 결핍 문제가 있을 때 사용된다. Eigen 라이브러리에서 \texttt{JacobiSVD} 클래스를 사용하여 쉽게 SVD를 통해 역행렬을 구할 수 있다.

SVD는 다음과 같이 행렬 \mathbf{A}를 세 행렬의 곱으로 나타낸다.

\mathbf{A} = \mathbf{U} \mathbf{\Sigma} \mathbf{V}^{T}

여기서 \mathbf{U}\mathbf{V}는 직교행렬이며, \mathbf{\Sigma}는 대각행렬이다. 역행렬은 다음과 같이 계산된다.

\mathbf{A}^{-1} = \mathbf{V} \mathbf{\Sigma}^{-1} \mathbf{U}^{T}

이 방법은 특히 행렬의 조건수가 좋지 않은 경우나 숫자적으로 안정된 해를 구할 때 유용하다.

#include <Eigen/Dense>
#include <iostream>

int main() {
    Eigen::MatrixXd A(3, 3);
    A << 1, 2, 3,
         0, 1, 4,
         5, 6, 0;

    Eigen::JacobiSVD<Eigen::MatrixXd> svd(A, Eigen::ComputeFullU | Eigen::ComputeFullV);
    Eigen::MatrixXd A_inv = svd.inverse();

    std::cout << "A inverse (SVD): \n" << A_inv << std::endl;

    return 0;
}

5. 행렬 함수 \texttt{inverse()}를 사용한 간단한 역행렬 계산

Eigen 라이브러리에서는 직접적으로 역행렬을 계산할 수 있는 \texttt{inverse()} 함수가 제공된다. 이는 내부적으로 행렬에 맞는 가장 적합한 방법을 자동으로 선택하여 역행렬을 계산한다.

#include <Eigen/Dense>
#include <iostream>

int main() {
    Eigen::Matrix3d A;
    A << 1, 2, 3,
         0, 1, 4,
         5, 6, 0;

    Eigen::Matrix3d A_inv = A.inverse();

    std::cout << "A inverse (using inverse() function): \n" << A_inv << std::endl;

    return 0;
}

\texttt{inverse()} 함수는 행렬의 크기와 성질에 따라 자동으로 적절한 방법을 사용해 역행렬을 계산하므로, 일반적인 경우에는 가장 간단하고 빠른 방법이다. 하지만, 큰 행렬이나 특정 성질을 가진 행렬에서는 직접적인 방법보다 효율성이 떨어질 수 있다.

6. 블록 행렬을 이용한 역행렬 계산

행렬이 블록으로 나눌 수 있을 때, 블록 행렬(Block Matrix)의 역행렬을 계산하는 방법도 사용할 수 있다. 블록 행렬을 이용한 역행렬 계산은 특히 큰 행렬을 처리할 때 매우 효율적일 수 있다.

블록 행렬 \mathbf{A}가 다음과 같이 분해된다고 가정한다.

\mathbf{A} = \begin{bmatrix} \mathbf{A}_{11} & \mathbf{A}_{12} \\ \mathbf{A}_{21} & \mathbf{A}_{22} \end{bmatrix}

이 경우, 블록 행렬의 역행렬은 다음과 같이 계산할 수 있다.

\mathbf{A}^{-1} = \begin{bmatrix} \mathbf{A}_{11}^{-1} + \mathbf{A}_{11}^{-1} \mathbf{A}_{12} \mathbf{S}^{-1} \mathbf{A}_{21} \mathbf{A}_{11}^{-1} & -\mathbf{A}_{11}^{-1} \mathbf{A}_{12} \mathbf{S}^{-1} \\ -\mathbf{S}^{-1} \mathbf{A}_{21} \mathbf{A}_{11}^{-1} & \mathbf{S}^{-1} \end{bmatrix}

여기서 \mathbf{S}는 슈어 보충 행렬(Schur Complement)로 정의되며, 다음과 같은 식으로 주어진다.

\mathbf{S} = \mathbf{A}_{22} - \mathbf{A}_{21} \mathbf{A}_{11}^{-1} \mathbf{A}_{12}

이 방법은 블록 구조를 가진 행렬에서 매우 효율적이며, 특히 시스템 제어에서 블록 다이아고날 행렬과 같은 특수 행렬에 대해 자주 사용된다.

7. 유사 역행렬 (Pseudo-inverse)

역행렬이 존재하지 않는 경우, 특히 정사각행렬이 아닌 경우에도 유사 역행렬(Pseudo-inverse)을 사용할 수 있다. 유사 역행렬은 주로 최소자승 문제에서 사용되며, Eigen 라이브러리에서는 \texttt{JacobiSVD}를 통해 쉽게 구할 수 있다.

유사 역행렬 \mathbf{A}^{+}는 행렬 \mathbf{A}에 대해 다음을 만족한다.

\mathbf{A} \mathbf{A}^{+} \mathbf{A} = \mathbf{A}
\mathbf{A}^{+} \mathbf{A} \mathbf{A}^{+} = \mathbf{A}^{+}

SVD를 이용하여 유사 역행렬을 구하는 방법은 다음과 같다.

#include <Eigen/Dense>
#include <iostream>

int main() {
    Eigen::MatrixXd A(2, 3);
    A << 1, 2, 3,
         4, 5, 6;

    Eigen::JacobiSVD<Eigen::MatrixXd> svd(A, Eigen::ComputeThinU | Eigen::ComputeThinV);
    Eigen::MatrixXd A_pseudo_inv = svd.pseudoInverse();

    std::cout << "A pseudo-inverse: \n" << A_pseudo_inv << std::endl;

    return 0;
}

유사 역행렬은 비정사각 행렬에 대한 해를 구할 때, 특히 과잉 또는 결핍 데이터를 다룰 때 매우 유용하다. 이는 최소자승(Least Squares) 문제의 해를 구하는데 자주 사용된다.

8. 수치적 안정성 및 성능 고려사항

행렬의 역행렬을 계산할 때 가장 중요한 고려사항 중 하나는 수치적 안정성이다. 특히, 조건수가 매우 큰 행렬의 경우 역행렬을 계산하는 과정에서 수치적 오류가 발생할 수 있다. 조건수(Condition Number)는 행렬의 수치적 안정성을 측정하는 지표로, 조건수가 클수록 역행렬을 계산하는 데 있어 오차가 커질 가능성이 높다.

행렬 \mathbf{A}의 조건수는 다음과 같이 정의된다.

\kappa(\mathbf{A}) = \|\mathbf{A}\| \|\mathbf{A}^{-1}\|

조건수가 매우 큰 경우, LU 분해나 QR 분해보다 SVD를 사용하는 것이 더 안정적인 결과를 제공할 수 있다. SVD는 행렬의 모든 특이값을 구해 그 역수를 이용하기 때문에, 조건수가 큰 경우에도 비교적 안정적으로 작동한다.

#include <Eigen/Dense>
#include <iostream>

int main() {
    Eigen::Matrix3d A;
    A << 1, 2, 3,
         4, 5, 6,
         7, 8, 9;

    Eigen::JacobiSVD<Eigen::Matrix3d> svd(A, Eigen::ComputeFullU | Eigen::ComputeFullV);
    std::cout << "Singular values: \n" << svd.singularValues() << std::endl;

    return 0;
}