Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust로 구현한 빠른 텐서 정규화

최근 빠른 텐서 정규화가 필요한 일이 있었지만, 이를 위한 라이브러리 생태계가 부족했습니다. 물리학과 미분기하학에서 텐서를 조작하는 일, 특히 인덱스 대칭성에 따라 텐서를 정규형으로 만드는 일은 반복적으로 등장하면서도 계산량이 많은 작업입니다. 처음으로 상대성이론 수업을 듣는 학부생이라면 누구나 여기에 필요한 텐서 곡예가 종종 벅차다는 것을 알고 있으며, 실제 문제에서는 이것이 훨씬 더 까다로워집니다.

Mathematica 같은 기존 시스템이 어느 정도 지원을 제공하고 xPerm 라이브러리가 고성능 C 구현을 제공하기는 하지만, 이른바 “현대적“이면서 오픈 소스인 해법은 없었습니다. 그래서 저는 텐서 정규화를 위한 Butler-Portugal 알고리즘을 순수 Rust로 구현하고 C와 Python 바인딩을 갖춘 새로운 butler-portugal 라이브러리를 만들었습니다.

텐서 정규화를 위한 정규(canonical) 알고리즘(하하)은 잘 알려지지 않은 Butler-Portugal 알고리즘으로, 텐서를 그 대칭성 아래에서 유일한 최소 형태로 만드는 방법입니다. 이 알고리즘은 텐서 정규화를 순열군론의 언어로 다시 표현하며, 구체적으로는 대칭군의 이중 잉여류에서 최소 대표원을 찾는 문제로 바꿉니다. 텐서를 슬롯에서 인덱스 레이블로 가는 사상 로 나타냅시다. 슬롯 대칭 (텐서를 부호를 제외하고 불변으로 유지하는 슬롯의 순열)는 오른쪽에서 작용하고, 더미 대칭 (축약된 인덱스 쌍의 이름을 바꾸는 것, 그리고 계량이 허용하는 경우 쌍의 위쪽 끝과 로어링된 아래쪽 끝을 맞바꾸는 것)는 왼쪽에서 작용합니다. 그러면 텐서를 표기하는 동치인 방법은 모두 이중 잉여류 에 속하는 어떤 이며, 정규형은 그 이중 잉여류에서 사전식으로 가장 작은 원소입니다. 같은 배치에 두 부호 모두로 도달할 수 있다면, 그 텐서는 항등적으로 0입니다.

이 문제를 다룰 만한 수준으로 유지하는 비결은 두 군 중 어느 것도 원소를 전부 나열하지 않는다는 점입니다. Schreier-Sims 알고리즘은 기저 을 갖는 의 안정자 사슬을 구축하고, 탐색은 그 사슬을 한 번에 한 슬롯씩 따라갑니다. 각 단계에서 탐색은 모든 횡단 원소를 시도하여, 현재 슬롯에 놓이는 레이블을 아래에서의 궤도 최솟값으로 로어링하고, 가장 작은 레이블을 얻는 선택들을 남긴 뒤 다음 슬롯으로 넘어갑니다. 동점일 때만 분기가 일어나고, 텐서를 동일하게 표기하는 살아남은 후보들은 병합되므로, 원소가 수백만 개인 군에 대해서도 탐색 규모가 작게 유지됩니다.

한 걸음 물러서서 기호적 텐서 조작이 실제로 무엇을 의미하는지 생각해 봅시다. 머신러닝에서와 달리, 물리학에서 텐서는 단순한 다차원 배열이 아닙니다. 텐서는 이름이 붙은 인덱스를 가진 객체로, 각 인덱스는 변성(각각 아래첨자와 위첨자에 해당하는 공변과 반변)을 지니며, 좌표 변환에 따라 특정한 방식으로 변환됩니다. 인덱스 교환에 대해 대칭이거나 반대칭인 성질, 또는 리만 텐서에서처럼 더 정교한 패턴과 같은 텐서의 대칭성은 물리 법칙을 담고 있는 본질적인 대수적 성질입니다.

동기 부여를 위한 예로 일반상대성이론을 살펴봅시다. 아인슈타인 장 방정식은

시공간 곡률을 담고 있는 아인슈타인 텐서 를 응력-에너지 텐서 와 연결합니다. 둘 다 대칭인 랭크 2 텐서이므로, 계산에서 인덱스를 자유롭게 치환할 수 있습니다:

이러한 객체를 기호적으로 다룰 때는 이 대칭성들을 모두 존중하면서 식을 조작하고, 인덱스를 축약하고, 결과를 단순화할 수 있어야 합니다. 예를 들어 리만 곡률 텐서 는 풍부한 대칭성을 지닌 랭크 4 객체입니다:

또한 이 텐서는 이른바 제1 비앙키 항등식을 만족합니다.

여기서 대괄호는 그 안에 들어 있는 인덱스들에 대한 반대칭화를 나타냅니다. 이러한 대칭 항등식은 계산을 단순화하는 데, 그리고 리치 텐서나 리치 스칼라 같은 유도량이 올바른 성질을 갖도록 보장하는 데 매우 중요합니다.

축약(contraction)은 또 하나의 기본 연산입니다. 위 첨자 하나와 로어링된 아래 첨자 하나로 이루어진 첨자 쌍에 대해 합을 취하여 텐서의 랭크를 줄입니다:

리치 스칼라 같은 불변량은 이렇게 만듭니다. 리치 스칼라는 리만 다양체의 각 점에 부여되는 하나의 실수로, 그 점 근방의 기하가 평탄함에서 얼마나 벗어나는지를 측정합니다.

정준화가 정말로 어려워지는 지점은 축약된 인덱스입니다. 더미 인덱스의 이름에는 아무런 의미가 없으므로 \( R^a{}{bac} \)와 \( R^z{}{bzc} \)는 같은 대상이며, 정준화기는 슬롯 대칭성에 더해 더미의 이름 바꾸기까지 고려해야 합니다. 이것이 바로 이중 잉여류의 쪽입니다. 구체적으로 살펴보기 위해, butler-portugal을 사용하여 리만 텐서의 리치 축약을 정준화하는 최소한의 Rust 예제를 보이겠습니다.

use butler_portugal::*;

fn main() -> Result<()> {
    // R^a_{cab}, a contraction of the Riemann tensor
    let mut r = Tensor::new(
        "R",
        vec![
            TensorIndex::contravariant("a", 0),
            TensorIndex::covariant("c", 1),
            TensorIndex::covariant("a", 2),
            TensorIndex::covariant("b", 3),
        ],
    );
    r.add_symmetry(Symmetry::antisymmetric(vec![0, 1]));
    r.add_symmetry(Symmetry::antisymmetric(vec![2, 3]));
    r.add_symmetry(Symmetry::symmetric_pairs(vec![(0, 1), (2, 3)]));

    let canonical = canonicalize(&r)?;
    println!("{canonical}"); // R_b_a_c^a
    println!("{}", canonical.coefficient());
    Ok(())
}

위첨자 자리에 한 번, 아래첨자 자리에 한 번 나타나는 반복된 이름 a는 자동으로 축약으로 감지됩니다. 출력은 대칭군 아래에서 텐서의 유일한 최소 대표원이며, 누적된 부호는 계수에 반영되고 축약된 인덱스는 정렬된 더미 이름으로 바뀝니다. 반대칭인 에 대한 처럼 대칭성 때문에 소멸하는 텐서는 계수 0으로 반환됩니다.

라이브러리의 Core에는 텐서를 이름 붙은 인덱스를 가진 객체로 나타내는 표현이 있으며, 각 인덱스는 변동성(variance), 위치, 그리고 선택적인 인덱스 타입을 가집니다(그래서 시공간 인덱스와 스피너 인덱스가 서로 짝을 이루는 일은 없습니다). 각 인덱스 타입은 대칭인 계량, 반대칭인 계량(스피너 계량처럼 레이징과 로어링을 할 때마다 부호가 하나 붙습니다), 또는 계량이 없음을 선언할 수 있습니다. 대칭은 부호 있는 순열들로 이루어진 군의 생성기로 인코딩됩니다. 부호는 xPerm에서 가져온 깔끔한 기법으로 처리합니다. 개 슬롯의 부호 있는 순열을 개 점의 일반 순열로 인코딩하는데, 부호 은 추가된 두 점을 맞바꿉니다. 그러면 개 점 위의 일반적인 Schreier-Sims 사슬이 부호 있는 군을 기술하며, 텐서는 군이 두 부호 점만을 맞바꾸는 원소를 포함할 때 정확히 그때에만 항등적으로 0입니다.

텐서의 대칭성은 그 인덱스들에 대한 군 작용으로 인코딩됩니다. 가장 흔한 유형은 다음과 같습니다.

  • 대칭: 지정한 인덱스들의 교환에 대해 불변입니다,
  • 반대칭: 교환하면 부호가 바뀝니다,
  • 순환: 순환 순열에 대해 불변입니다,
  • 쌍 대칭: 리만 텐서에서처럼 인덱스 쌍을 교환합니다,
  • 사용자 정의: 임의의 부호 있는 생성기로, Schreier-Sims가 이로부터 전체 군을 구성합니다.

텐서의 곱도 같은 방식으로 처리됩니다. Tensor::product는 인수들을 하나의 가환 단항식으로 붙이면서 동일한 인수끼리 교환하는 대칭성을 추가하므로, 나 크레치만 스칼라 같은 식이 인수 사이에 걸친 축약과 함께 정준화됩니다. 또한 이 라이브러리는 영 타블로로 지정되는 기약 표현으로의 사영도 지원하여, 텐서를 대칭군의 표현에 대응하는 대칭 유형들로 분해할 수 있게 해 줍니다. 여기서 이를 설명하지는 않겠지만, 다른 곳에 잘 문서화되어 있으며 참고 문헌은 저장소에 넣어 두었습니다.

제 구현은 기호 계산에서 텐서 정준화의 표준으로 널리 인정받는 xPerm에서 영감을 받았습니다. 하지만 C와 Mathematica 코드가 꽤 오래되었고 문서화도 그리 잘 되어 있지 않아서, 이 알고리즘을 Rust로 포팅하는 일은 상당히 어려웠습니다. 테스트 스위트는 정준형 천 개로 이루어진 참조 코퍼스, 작은 군에 대한 무차별 대입 이중 잉여류 탐색, 그리고 고전적인 리만 항등식들과 대조하여 검사합니다. 리만 단항식에 대해서는 xPerm의 출력과 일치하며 더 빠르게 실행됩니다. 그러니 이 분야의 실무자라면 제 구현을 살펴보고 참조 구현과 비교해 볼 수 있을 것이고, 이것이 앞으로 연구를 위해 같은 것을 다시 만들 필요가 없을 어느 가엾은 물리학 대학원생의 수고를 덜어 주기를 바랍니다.

이 라이브러리는 include/butler_portugal.h에 헤더가 있는 C 공유 라이브러리로도 빌드되므로, C ABI를 지원하는 어떤 언어에서든 링크할 수 있습니다.

cargo build --release
clang -Iinclude -Ltarget/release -lbutler_portugal app.c -o app

그리고 C API 위에 플루언트 인터페이스를 갖춘 작은 Python 래퍼도 있습니다.

from butler_portugal import Tensor

r = Tensor("R", ["^a", "c", "a", "b"]).antisymmetric(0, 1).antisymmetric(2, 3)
print(r.symmetric_pairs((0, 1), (2, 3)).canonicalize())  # R_b_a_c^a

이렇게 해서 butler-portugal을 소개했습니다. 이 라이브러리가 Rust 생태계에 고성능 텐서 정규화를 가져다주기를 바랍니다. 다음 명령으로 설치할 수 있습니다:

cargo add butler-portugal

또는 Python에서는 uv로 설치할 수 있습니다:

uv add git+https://github.com/sdiehl/butler-portugal --tag v0.4.0

이 라이브러리는 상당히 효율적이고 MIT 라이선스로 배포되며, 복잡한 대칭성을 지닌 텐서를 다뤄야 하는 모든 분야에서 유용하게 쓰이기를 바랍니다. 그리고 저 말고도 적어도 한 사람에게는 쓸모가 있기를 바랍니다!


원문 © Stephen Diehl, stephendiehl.com · 글 CC BY-SA 4.0, 코드 MIT · Stephen Diehl은 런던의 소프트웨어 엔지니어이자 작가입니다(저자가 정한 소개 문구). 이 글은 저자가 개인 자격으로 쓴 것이며 어떤 조직과의 연관도 밝히거나 암시해서는 안 됩니다 (라이선스와 고지).
이 페이지는 원문을 한국어로 옮기며 변경한 비공식 번역이며 같은 CC BY-SA 4.0으로 제공합니다 · yeokja 한국어 번역 · Anthropic 사의 claude-opus-5-5 모델을 활용하여 번역되었으며 학습을 모두 비허용한 상태로 작업하였습니다.