행렬식(Determinant)은 정방행렬(square matrix)에 대해 정의되며, 선형대수학에서 매우 중요한 역할을 한다. 행렬의 행렬식은 여러 방식으로 계산할 수 있으며, Eigen 라이브러리에서는 다양한 방법으로 이를 효율적으로 다룰 수 있다. 이번 절에서는 행렬식을 계산하는 방법, 그 이론적 배경, 그리고 Eigen 라이브러리에서의 실제 구현 방식을 다룬다.

행렬식의 정의

우선, 행렬식은 주어진 정방행렬에 대해 스칼라 값을 반환하는 함수로 정의된다. 행렬식은 일반적으로 다음과 같이 표기한다.

\text{det}(\mathbf{A})

여기서, \mathbf{A}n \times n 정방행렬을 의미하며, 그 행렬식은 주로 \mathbf{A}의 열이나 행을 통해 재귀적으로 정의된다. 예를 들어, 2 \times 2 행렬 \mathbf{A}에 대한 행렬식은 다음과 같다:

\text{det}(\mathbf{A}) = a_{11} a_{22} - a_{12} a_{21}

보다 일반적으로, n \times n 행렬에 대한 행렬식은 다음과 같이 주어진다.

\text{det}(\mathbf{A}) = \sum_{i=1}^{n} (-1)^{i+1} a_{1i} \text{det}(\mathbf{A}_{1i})

여기서, \mathbf{A}_{1i}는 행렬 \mathbf{A}의 1행과 i열을 제거한 (n-1) \times (n-1) 행렬이다.

Eigen 라이브러리에서 행렬식 계산

Eigen 라이브러리에서 행렬의 행렬식을 계산하는 것은 매우 간단하다. 다음과 같은 방식으로 사용할 수 있다.

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

int main() {
    Eigen::Matrix2d mat;
    mat(0, 0) = 3;
    mat(1, 0) = 2;
    mat(0, 1) = 1;
    mat(1, 1) = 4;
    std::cout << "행렬의 행렬식은: " << mat.determinant() << std::endl;
    return 0;
}

위의 코드는 2 \times 2 행렬에 대한 행렬식을 구하는 방법을 보여준다. 더 큰 n \times n 행렬의 경우에도 동일한 방식으로 계산할 수 있으며, Eigen 라이브러리는 이 과정에서 효율적인 알고리즘을 활용하여 계산 속도를 최적화한다.

행렬식 계산의 주요 성질

행렬식은 여러 중요한 성질을 가지고 있으며, 이를 이해하는 것은 행렬 연산을 깊이 있게 다루는 데 필수적이다. 몇 가지 중요한 성질을 소개하면 다음과 같다.

  1. 교환 법칙: 정방행렬 \mathbf{A}\mathbf{B}에 대해,
\text{det}(\mathbf{A} \mathbf{B}) = \text{det}(\mathbf{A}) \cdot \text{det}(\mathbf{B})
  1. 전치 행렬의 행렬식: \mathbf{A}n \times n 행렬일 때,
\text{det}(\mathbf{A}^{T}) = \text{det}(\mathbf{A})
  1. 역행렬과 행렬식: 만약 \mathbf{A}가 가역행렬이라면, \mathbf{A}의 역행렬에 대한 행렬식은 다음과 같다.
\text{det}(\mathbf{A}^{-1}) = \frac{1}{\text{det}(\mathbf{A})}
  1. 특정 행렬 연산에 대한 행렬식의 변화: 행렬의 행이나 열을 특정 상수로 곱하면, 그 행렬식은 해당 상수만큼 변하게 된다. 예를 들어, \mathbf{A}i-번째 행을 상수 c로 곱하면,
\text{det}(\mathbf{A}) = c \cdot \text{det}(\mathbf{A'})

여기서 \mathbf{A'}는 변형되지 않은 나머지 행렬이다.

이러한 성질들은 행렬식을 계산하고 해석하는 데 있어 매우 중요한 역할을 한다. 특히 선형대수학에서 행렬의 고유값을 찾거나, 선형 방정식의 해를 구할 때 유용하다.

소행렬식(Minor)과 여인자(Cofactor)

행렬식 계산의 기본적인 과정에서 중요한 개념은 소행렬식과 여인자이다. 이 두 개념은 n \times n 행렬의 행렬식을 재귀적으로 정의하고 계산하는 데 사용된다.

소행렬식(Minor)

소행렬식은 행렬 \mathbf{A}의 어떤 특정 행과 열을 제거하고 남은 행렬의 행렬식을 말한다. 예를 들어, \mathbf{A}의 i번째 행과 j번째 열을 제거한 (n-1) \times (n-1) 행렬을 \mathbf{A}_{ij}라고 하면, 이때의 행렬식을 M_{ij}라고 정의한다. 이를 수식으로 표현하면,

M_{ij} = \text{det}(\mathbf{A}_{ij})

여인자(Cofactor)

여인자는 소행렬식에 행과 열의 인덱스에 따라 부호를 더한 값이다. 즉, 행렬의 i-번째 행과 j-번째 열에 해당하는 여인자 C_{ij}는 다음과 같이 정의된다.

C_{ij} = (-1)^{i+j} M_{ij}

이때 (-1)^{i+j}는 소행렬식의 부호를 결정하는 역할을 하며, 이는 교차 패턴으로 부호가 달라지게 한다. 예를 들어, 3 \times 3 행렬에서의 여인자 부호는 다음과 같다.

\begin{pmatrix} + & - & + \\ - & + & - \\ + & - & + \end{pmatrix}

행렬식의 재귀적 계산 (Laplace 전개)

이제 n \times n 행렬의 행렬식을 계산하는 방법으로, Laplace 전개에 대해 설명하겠다. 이 방법은 행이나 열을 기준으로 소행렬식과 여인자를 이용해 행렬식을 재귀적으로 구하는 방법이다.

예를 들어, n \times n 행렬 \mathbf{A}의 첫 번째 행을 기준으로 행렬식을 전개하면, 다음과 같이 구할 수 있다.

\text{det}(\mathbf{A}) = a_{11} C_{11} + a_{12} C_{12} + \cdots + a_{1n} C_{1n}

이를 좀 더 일반적으로 표현하면,

\text{det}(\mathbf{A}) = \sum_{j=1}^{n} a_{1j} C_{1j}

여기서 C_{1j}\mathbf{A}의 첫 번째 행과 j번째 열을 제거한 소행렬식의 여인자이다.

예시: 3 \times 3 행렬의 행렬식

3 \times 3 행렬 \mathbf{A}에 대해 행렬식을 직접 계산해 보자.

\mathbf{A} = \begin{pmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{pmatrix}

이 행렬의 행렬식은 첫 번째 행을 기준으로 Laplace 전개를 사용하여 다음과 같이 계산할 수 있다.

\text{det}(\mathbf{A}) = a_{11} \begin{vmatrix} a_{22} & a_{23} \\ a_{32} & a_{33} \end{vmatrix} - a_{12} \begin{vmatrix} a_{21} & a_{23} \\ a_{31} & a_{33} \end{vmatrix} + a_{13} \begin{vmatrix} a_{21} & a_{22} \\ a_{31} & a_{32} \end{vmatrix}

각각의 2 \times 2 행렬식을 계산하여 다음과 같은 결과를 얻는다.

\text{det}(\mathbf{A}) = a_{11}(a_{22}a_{33} - a_{23}a_{32}) - a_{12}(a_{21}a_{33} - a_{23}a_{31}) + a_{13}(a_{21}a_{32} - a_{22}a_{31})

이 방식으로 3 \times 3 이상의 큰 행렬에 대해서도 재귀적으로 행렬식을 구할 수 있다.

Eigen에서의 고차원 행렬식 계산

Eigen 라이브러리는 고차원 행렬의 행렬식을 계산하는 데 있어 매우 효율적인 알고리즘을 사용한다. 특히, 일반적인 n \times n 행렬의 경우, 행렬 분해 알고리즘(예: LU 분해)을 사용하여 더 빠른 계산을 수행한다.

예를 들어, 4 \times 4 이상의 큰 행렬에 대해서도 Eigen의 determinant() 함수를 그대로 사용할 수 있으며, 내부적으로 LU 분해를 통해 계산 효율을 높인다. 다음은 Eigen 라이브러리를 사용한 예시이다.

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

int main() {
    Eigen::Matrix4d mat;
    mat << 1, 2, 3, 4,
           5, 6, 7, 8,
           9, 10, 11, 12,
           13, 14, 15, 16;

    std::cout << "행렬의 행렬식은: " << mat.determinant() << std::endl;
    return 0;
}

이 예제에서 Eigen은 내부적으로 LU 분해를 사용하여 행렬식을 효율적으로 계산한다. 4 \times 4 이상의 행렬에서도 계산은 빠르고 정확하게 이루어진다.

행렬 분해와 행렬식 계산

대규모 행렬에 대한 효율적인 행렬식 계산을 위해, 행렬 분해 방법이 많이 사용된다. 대표적인 분해 방법으로는 LU 분해, QR 분해, 그리고 Cholesky 분해 등이 있다. 이러한 방법은 행렬식을 직접적으로 계산하는 대신, 행렬을 더 간단한 형태로 분해하여 행렬식을 계산할 수 있도록 도와준다.

LU 분해를 통한 행렬식 계산

LU 분해는 주어진 정방행렬 \mathbf{A}를 두 개의 삼각행렬의 곱으로 분해하는 방법이다. 이때 행렬 \mathbf{A}는 하삼각행렬 \mathbf{L}과 상삼각행렬 \mathbf{U}로 다음과 같이 표현된다.

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

LU 분해를 이용하면, 행렬의 행렬식은 \mathbf{L}\mathbf{U}의 대각 원소들의 곱으로 계산된다. 즉, 다음과 같은 성질을 이용할 수 있다.

\text{det}(\mathbf{A}) = \text{det}(\mathbf{L}) \cdot \text{det}(\mathbf{U})

LU 분해에서 하삼각행렬 \mathbf{L}의 대각 원소는 모두 1이므로, \text{det}(\mathbf{L}) = 1이다. 따라서, 행렬 \mathbf{A}의 행렬식은 \mathbf{U}의 대각 원소들의 곱으로 표현된다.

\text{det}(\mathbf{A}) = \prod_{i=1}^{n} u_{ii}

여기서 u_{ii}는 상삼각행렬 \mathbf{U}의 대각 원소이다.

Eigen에서 LU 분해를 통한 행렬식 계산

Eigen 라이브러리에서는 LU 분해를 간단하게 사용할 수 있으며, 이를 통해 대규모 행렬의 행렬식을 효율적으로 계산할 수 있다. 다음은 LU 분해를 사용하는 예시이다.

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

int main() {
    Eigen::Matrix3d mat;
    mat << 4, 3, 2,
           2, 1, 3,
           3, 4, 1;

    Eigen::FullPivLU<Eigen::Matrix3d> lu_decomp(mat);
    std::cout << "행렬의 LU 분해를 통한 행렬식은: " << lu_decomp.determinant() << std::endl;
    return 0;
}

이 코드에서 FullPivLU 클래스는 행렬의 LU 분해를 수행하며, determinant() 함수는 분해된 행렬의 대각 원소들의 곱을 통해 행렬식을 계산한다. 이를 통해 큰 행렬에서도 효율적인 계산이 가능하다.

QR 분해를 통한 행렬식 계산

QR 분해는 주어진 행렬 \mathbf{A}를 직교행렬 \mathbf{Q}와 상삼각행렬 \mathbf{R}의 곱으로 분해하는 방법이다. QR 분해는 주로 직교성(orthogonality)을 보존하는 성질을 갖고 있어 수치적으로 안정적인 계산이 가능하다. 행렬 \mathbf{A}에 대해 다음과 같이 분해할 수 있다.

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

여기서, 직교행렬 \mathbf{Q}는 대각 원소들이 모두 1인 행렬이므로, 그 행렬식은 다음과 같다.

\text{det}(\mathbf{Q}) = 1

따라서, 행렬 \mathbf{A}의 행렬식은 상삼각행렬 \mathbf{R}의 대각 원소들의 곱으로 구할 수 있다.

\text{det}(\mathbf{A}) = \prod_{i=1}^{n} r_{ii}

여기서 r_{ii}는 상삼각행렬 \mathbf{R}의 대각 원소이다.

Eigen에서 QR 분해를 통한 행렬식 계산

Eigen 라이브러리에서는 QR 분해도 쉽게 수행할 수 있다. 다음은 QR 분해를 사용하여 행렬식을 계산하는 예시이다.

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

int main() {
    Eigen::Matrix3d mat;
    mat << 4, 3, 2,
           2, 1, 3,
           3, 4, 1;

    Eigen::HouseholderQR<Eigen::Matrix3d> qr_decomp(mat);
    Eigen::Matrix3d R = qr_decomp.matrixQR().triangularView<Eigen::Upper>();
    std::cout << "행렬의 QR 분해를 통한 행렬식은: " << R.diagonal().prod() << std::endl;
    return 0;
}

위 코드는 QR 분해를 통해 상삼각행렬 \mathbf{R}을 추출하고, 그 대각 원소들의 곱을 통해 행렬식을 계산하는 방식이다. QR 분해는 LU 분해에 비해 수치적으로 더 안정적인 경우가 많아 특정 상황에서 유용하다.

Cholesky 분해를 통한 행렬식 계산

Cholesky 분해는 대칭행렬이면서 양의 정부호(positive definite)인 행렬에 대해서만 적용 가능한 분해 방식이다. 이는 주어진 행렬 \mathbf{A}를 하삼각행렬 \mathbf{L}과 그 전치행렬 \mathbf{L}^{T}의 곱으로 분해한다.

\mathbf{A} = \mathbf{L} \mathbf{L}^{T}

이때, 행렬식은 하삼각행렬 \mathbf{L}의 대각 원소들의 곱의 제곱으로 계산된다. 수식으로 표현하면,

\text{det}(\mathbf{A}) = \left( \prod_{i=1}^{n} l_{ii} \right)^2

여기서 l_{ii}는 하삼각행렬 \mathbf{L}의 대각 원소이다.

Eigen에서 Cholesky 분해를 통한 행렬식 계산

Eigen 라이브러리에서 Cholesky 분해는 LLT 클래스를 사용하여 간단하게 구현할 수 있다. 다음은 Cholesky 분해를 사용하여 행렬식을 계산하는 예시이다.

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

int main() {
    Eigen::Matrix3d mat;
    mat << 4, 12, -16,
           12, 37, -43,
           -16, -43, 98;

    Eigen::LLT<Eigen::Matrix3d> llt_decomp(mat);
    Eigen::Matrix3d L = llt_decomp.matrixL();
    double detA = std::pow(L.diagonal().prod(), 2);
    std::cout << "행렬의 Cholesky 분해를 통한 행렬식은: " << detA << std::endl;
    return 0;
}

이 코드는 양의 정부호 대칭행렬에 대해 Cholesky 분해를 수행하고, 그 대각 원소들의 곱의 제곱을 통해 행렬식을 계산하는 방식이다.

대각화(Diagonalization)와 행렬식 계산

대각화는 정방행렬이 고유값과 고유벡터를 통해 대각행렬로 변환될 수 있는 과정을 말한다. 정방행렬 \mathbf{A}가 대각화 가능한 경우, 다음과 같이 표현할 수 있다.

\mathbf{A} = \mathbf{P} \mathbf{D} \mathbf{P}^{-1}

여기서, \mathbf{P}\mathbf{A}의 고유벡터들로 이루어진 행렬이고, \mathbf{D}\mathbf{A}의 고유값들로 구성된 대각행렬이다. 이 경우, 행렬식은 대각행렬의 대각 원소(즉, 고유값)들의 곱으로 계산할 수 있다.

\text{det}(\mathbf{A}) = \text{det}(\mathbf{D}) = \prod_{i=1}^{n} \lambda_{i}

여기서 \lambda_{i}\mathbf{A}의 i번째 고유값이다. 대각화가 가능한 행렬의 경우, 고유값의 곱을 통해 매우 쉽게 행렬식을 계산할 수 있다.

Eigen에서 고유값을 통한 행렬식 계산

Eigen 라이브러리에서는 고유값을 쉽게 구할 수 있는 기능을 제공하며, 이를 통해 행렬식도 계산할 수 있다. 고유값은 EigenSolver 클래스를 사용하여 구할 수 있으며, 이 고유값들을 이용하여 행렬식을 계산할 수 있다. 다음은 고유값을 이용해 행렬식을 계산하는 예시이다.

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

int main() {
    Eigen::Matrix3d mat;
    mat << 4, -2, 1,
           -2, 4, -2,
           1, -2, 3;

    Eigen::EigenSolver<Eigen::Matrix3d> solver(mat);
    Eigen::Vector3cd eigenvalues = solver.eigenvalues();

    std::cout << "행렬의 고유값을 이용한 행렬식은: " 
              << eigenvalues.prod() << std::endl;

    return 0;
}

이 코드는 고유값을 계산한 후, 그 고유값들의 곱을 통해 행렬식을 구하는 방식이다. 만약 행렬이 대각화 가능하다면, 이 방법을 통해 수치적으로 매우 안정적인 행렬식 계산이 가능하다.

SVD(특이값 분해)를 통한 행렬식 계산

특이값 분해(SVD, Singular Value Decomposition)는 행렬 \mathbf{A}를 세 개의 행렬로 분해하는 방식이다. 이때 \mathbf{A}는 다음과 같이 분해된다.

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

여기서 \mathbf{U}\mathbf{V}는 각각 직교행렬이고, \mathbf{\Sigma}는 행렬 \mathbf{A}의 특이값들이 대각 성분으로 들어 있는 대각행렬이다. 만약 \mathbf{A}가 정방행렬이라면, 이 특이값들의 곱을 통해 행렬식을 계산할 수 있다.

\text{det}(\mathbf{A}) = \text{det}(\mathbf{\Sigma}) = \prod_{i=1}^{n} \sigma_{i}

여기서 \sigma_{i}\mathbf{A}의 i번째 특이값이다.

Eigen에서 SVD를 통한 행렬식 계산

Eigen에서는 JacobiSVD 클래스를 이용하여 특이값 분해를 수행할 수 있다. 특이값들을 얻은 후, 그 곱을 통해 행렬식을 계산할 수 있다. 다음은 SVD를 통해 행렬식을 구하는 예시이다.

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

int main() {
    Eigen::Matrix3d mat;
    mat << 4, 3, 2,
           3, 2, 1,
           2, 1, 3;

    Eigen::JacobiSVD<Eigen::Matrix3d> svd(mat);
    Eigen::Vector3d singularValues = svd.singularValues();

    std::cout << "행렬의 SVD를 통한 행렬식은: " 
              << singularValues.prod() << std::endl;

    return 0;
}

위 코드는 SVD를 통해 특이값을 계산하고, 그 특이값들의 곱을 이용하여 행렬식을 구하는 방식이다. SVD는 수치적으로 매우 안정적이며, 행렬의 크기나 조건 수가 매우 클 때 유용하게 사용할 수 있다.

행렬식 계산의 수치적 문제

행렬식 계산은 수치적으로 매우 민감한 작업이 될 수 있다. 특히, 행렬의 크기가 커지거나, 행렬이 특이(singular)에 가까울수록 계산 과정에서 오차가 발생할 수 있다. 이 경우, LU 분해나 QR 분해와 같은 방법은 행렬의 조건 수가 좋지 않을 때에도 비교적 안정적인 방법으로 사용된다.

특히, 고유값을 이용한 행렬식 계산 방법은 행렬이 대각화 가능할 때에만 유효하며, 대각화 불가능한 행렬에 대해서는 사용할 수 없다. 또한, SVD는 행렬의 특이성 여부에 관계없이 사용할 수 있으며, 가장 수치적으로 안정적인 방법 중 하나로 알려져 있다.

따라서, 실제로 행렬식을 계산할 때에는 사용되는 행렬의 성질과 조건을 잘 파악하고, 적절한 분해 방법을 선택하는 것이 중요하다.