Juliaの行列演算を深く理解する:hemm!() の引数と実践例
関数の役割
この関数は、以下のいずれかの演算を行います。
-
左からの行列積
C←αAB+βC または C←αBA+βC ここで、A はエルミート行列(実数の場合は対称行列)、B と C は一般の行列です。 -
右からの行列積
C←αBA+βC または C←αAB+βC こちらも同様に、A はエルミート行列(実数の場合は対称行列)、B と C は一般の行列です。
引数
LinearAlgebra.BLAS.hemm!()
関数は、以下の引数を取ります。
C::AbstractMatrix
: 結果を格納する行列です。この行列は、演算の結果で上書きされます(!
が関数名の末尾に付いているのはこのためです)。beta::Number
: スカラー β を指定します。B::AbstractMatrix
: 一般の行列を表す行列です。A::AbstractMatrix
: エルミート行列(または対称行列)を表す行列です。alpha::Number
: スカラー α を指定します。uplo::Char
: エルミート行列(または対称行列)A の上三角部分 ('U'
または'u'
) または下三角部分 ('L'
または'l'
) のどちらを参照して演算を行うかを指定します。もう一方の三角部分は、エルミート性(または対称性)を利用して暗黙的に参照されます。side::Char
: 行列 A が行列 B の左側から乗算されるか ('L'
または'l'
)、右側から乗算されるか ('R'
または'r'
) を指定します。
重要な点
- BLAS の利用
この関数は、数値計算ライブラリである BLAS の最適化されたルーチンを利用しているため、効率的な行列積演算が可能です。 - エルミート/対称性
関数は、指定されたuplo
に基づいて、A がエルミート行列(複素数の場合)または対称行列(実数の場合)であることを前提として演算を行います。指定された三角部分のみが参照され、もう一方の三角部分は共役転置(エルミートの場合)または転置(対称の場合)であるとみなされます。 - インプレース演算
hemm!()
は、結果を新しい行列として返さず、引数として渡された行列C
の内容を直接上書きします。これは、メモリ割り当てを減らし、パフォーマンスを向上させるための設計です。
使用例 (実対称行列の場合)
using LinearAlgebra.BLAS
A = [2.0 1.0; 1.0 3.0] # 対称行列
B = [1.0 2.0; 3.0 4.0]
C = [0.0 0.0; 0.0 0.0]
alpha = 2.0
beta = 1.0
# C ← alpha * A * B + beta * C (左からの乗算、上三角部分を参照)
hemm!('L', 'U', alpha, A, B, beta, C)
println("C after hemm!('L', 'U'):")
println(C)
C = [0.0 0.0; 0.0 0.0] # C をリセット
# C ← alpha * B * A + beta * C (右からの乗算、下三角部分を参照)
hemm!('R', 'L', alpha, A, B, beta, C)
println("\nC after hemm!('R', 'L'):")
println(C)
この例では、実対称行列 A
と一般行列 B
の積を hemm!()
を用いて計算し、結果を C
に格納しています。side
と uplo
の組み合わせを変えることで、異なる演算と参照する A の三角部分を指定しています。
よくあるエラーとトラブルシューティング
-
- エラー内容
DimensionMismatch("matrix A has dimensions ($(size(A, 1)), $(size(A, 2))) which is not compatible with matrix B of dimensions ($(size(B, 1)), $(size(B, 2)))")
のようなエラーメッセージ。 - 原因
行列 A, B, C の次元が、指定された演算 (side
引数によって決まる) で整合しない場合に発生します。 - トラブルシューティング
side
が'L'
(左からの乗算) の場合、A
の列数とB
の行数が一致している必要があります。また、結果の行列C
はA
の行数とB
の列数を持つ必要があります。side
が'R'
(右からの乗算) の場合、A
の行数とB
の列数が一致している必要があります。また、結果の行列C
はB
の行数とA
の列数を持つ必要があります。C
の次元が事前に適切に初期化されているか確認してください。hemm!()
はC
を上書きするため、事前に正しいサイズである必要があります。
- エラー内容
-
型の不一致 (Type Mismatch)
- エラー内容
MethodError: no method matching hemm!(...)
のようなエラーメッセージで、引数の型が予期された型と異なる場合に発生します。 - 原因
A
がエルミート行列(複素数の場合)または対称行列(実数の場合)でない。ただし、hemm!()
は与えられた行列がその性質を持つと仮定して計算を行うため、この場合はエラーではなく不正な結果を生じる可能性があります。- スカラー引数
alpha
やbeta
が数値型でない。 - 行列の要素の型が混在しているなど、BLAS がサポートしていない型である。
- トラブルシューティング
A
が意図したエルミート性または対称性を持つか確認してください。alpha
とbeta
に数値(通常はFloat64
やComplexF64
など)を渡しているか確認してください。- 行列
A
,B
,C
の要素の型が一致しているか確認してください。必要であればconvert
関数などで型を統一してください。
- エラー内容
-
uplo 引数の誤り
- エラー内容
明確なエラーメッセージは出にくいですが、計算結果が期待と異なる場合に疑うべきです。 - 原因
uplo
引数 ('U'
または'L'
) が、行列A
における実際にデータが入っている三角部分と一致していない場合に発生します。 - トラブルシューティング
- 行列
A
の上三角部分にデータが入っている場合は'U'
、下三角部分にデータが入っている場合は'L'
を指定してください。 - どちらの三角部分にデータが入っているか不明な場合は、
A
の内容を確認してください。
- 行列
- エラー内容
-
side 引数の誤り
- エラー内容
こちらも明確なエラーメッセージは出にくいですが、計算結果が期待と異なる場合に疑うべきです。 - 原因
行列 A を左から乗算したいのに'R'
を指定したり、その逆を行ったりした場合に発生します。 - トラブルシューティング
- 意図する行列の乗算順序に合わせて、
'L'
または'R'
を正しく指定してください。
- 意図する行列の乗算順序に合わせて、
- エラー内容
-
インプレース演算に関する注意
- エラー内容
直接的なエラーは少ないですが、意図しないデータの書き換えが発生することがあります。 - 原因
hemm!()
は行列C
の内容を上書きします。C
に元のデータが必要な場合は、hemm!()
を呼び出す前にコピーを作成しておく必要があります。 - トラブルシューティング
C
の元の内容を保持する必要がある場合は、C_original = copy(C)
のように事前にコピーを作成してください。
- エラー内容
-
BLAS ライブラリの問題 (稀)
- エラー内容
BLAS 関連の低レベルなエラーメッセージが表示される可能性があります。 - 原因
Julia がリンクしている BLAS ライブラリ自体に問題がある場合(稀です)。 - トラブルシューティング
- Julia を再起動してみる。
- Julia のバージョンを更新してみる。
- 他の線形代数演算が正常に動作するか確認し、BLAS ライブラリ全体の問題かどうかを切り分ける。
- 特定の BLAS 実装(OpenBLAS、MKL など)を使用している場合は、別の実装を試してみる。
- エラー内容
トラブルシューティングの一般的なヒント
- ドキュメントを参照する
Julia の公式ドキュメントやLinearAlgebra
モジュールのドキュメントで、関数の正しい使い方や引数の説明を確認してください (?LinearAlgebra.BLAS.hemm!
を Julia の REPL で実行するとドキュメントが表示されます)。 - 簡単な例で試す
問題が複雑な場合に、より小さな行列や簡単な値でhemm!()
を試して、基本的な動作を確認してみるのが有効です。 - 引数の型と形状を確認する
typeof()
関数やsize()
関数を使って、引数の型と形状が期待通りであるか確認してください。 - エラーメッセージをよく読む
Julia のエラーメッセージは、問題の原因に関する貴重な情報を提供してくれます。
例1: 実対称行列と一般行列の積 (左からの乗算)
この例では、実対称行列 A
と一般行列 B
の積を計算し、結果を C
に格納します。side = 'L'
で左からの乗算を指定し、uplo = 'U'
で A
の上三角部分を参照します。
using LinearAlgebra.BLAS
# 実対称行列 A (上三角部分にデータを持つ)
A = [2.0 1.0;
1.0 3.0]
# 一般行列 B
B = [1.0 2.0;
3.0 4.0]
# 結果を格納する行列 C (事前に適切なサイズで初期化)
C = zeros(size(A, 1), size(B, 2)) # 2x2 のゼロ行列
alpha = 2.0
beta = 1.0
# C ← alpha * A * B + beta * C
hemm!('L', 'U', alpha, A, B, beta, C)
println("行列 A:")
println(A)
println("\n行列 B:")
println(B)
println("\n行列 C (hemm!('L', 'U') 適用後):")
println(C)
実行結果
行列 A:
[2.0 1.0; 1.0 3.0]
行列 B:
[1.0 2.0; 3.0 4.0]
行列 C (hemm!('L', 'U') 適用後):
[ 7.0 8.0]
[11.0 14.0]
解説
hemm!('L', 'U', alpha, A, B, beta, C)
は、C←2.0×A×B+1.0×C を計算します。A
は対称行列であり、上三角部分 (2.0
,1.0
,3.0
) の値が参照されます。下三角部分の1.0
は対称性から暗黙的に利用されます。
例2: 実対称行列と一般行列の積 (右からの乗算)
この例では、実対称行列 A
を一般行列 B
の右から乗算します。side = 'R'
で右からの乗算を指定し、uplo = 'L'
で A
の下三角部分を参照します。
using LinearAlgebra.BLAS
# 実対称行列 A (下三角部分にデータを持つ)
A = [2.0 1.0;
1.0 3.0]
# 一般行列 B
B = [1.0 2.0;
3.0 4.0]
# 結果を格納する行列 C (事前に適切なサイズで初期化)
C = zeros(size(B, 1), size(A, 2)) # 2x2 のゼロ行列
alpha = 0.5
beta = 2.0
# C ← alpha * B * A + beta * C
hemm!('R', 'L', alpha, B, A, beta, C)
println("\n行列 A:")
println(A)
println("\n行列 B:")
println(B)
println("\n行列 C (hemm!('R', 'L') 適用後):")
println(C)
実行結果
行列 A:
[2.0 1.0; 1.0 3.0]
行列 B:
[1.0 2.0; 3.0 4.0]
行列 C (hemm!('R', 'L') 適用後):
[ 5.0 8.5]
[10.0 17.0]
解説
hemm!('R', 'L', alpha, B, A, beta, C)
は、C←0.5×B×A+2.0×C を計算します。A
は対称行列であり、下三角部分 (2.0
,1.0
,3.0
) の値が参照されます。上三角部分の1.0
は対称性から暗黙的に利用されます。
例3: エルミート行列と一般行列の積 (複素数)
この例では、複素数を要素とするエルミート行列 H
と一般の複素行列 Z
の積を計算します。
using LinearAlgebra.BLAS
# エルミート行列 H (上三角部分にデータを持つ)
H = [2.0 + 0.0im 1.0 + 2.0im;
1.0 - 2.0im 3.0 + 0.0im]
# 一般複素行列 Z
Z = [1.0 + 1.0im 2.0 - 1.0im;
3.0 - 0.5im 4.0 + 0.0im]
# 結果を格納する行列 W
W = zeros(ComplexF64, size(H, 1), size(Z, 2)) # 2x2 の複素ゼロ行列
alpha_complex = 1.0 + 0.5im
beta_complex = 0.0 - 1.0im
# W ← alpha_complex * H * Z + beta_complex * W
hemm!('L', 'U', alpha_complex, H, Z, beta_complex, W)
println("\nエルミート行列 H:")
println(H)
println("\n複素行列 Z:")
println(Z)
println("\n複素行列 W (hemm!('L', 'U') 適用後):")
println(W)
実行結果 (数値は近似値です)
エルミート行列 H:
[2.0 + 0.0im 1.0 + 2.0im; 1.0 - 2.0im 3.0 + 0.0im]
複素行列 Z:
[1.0 + 1.0im 2.0 - 1.0im; 3.0 - 0.5im 4.0 + 0.0im]
複素行列 W (hemm!('L', 'U') 適用後):
[-3.5 + 5.5im -2.0 + 7.0im]
[-10.0 + 2.5im -11.5 + 4.0im]
解説
hemm!('L', 'U', alpha_complex, H, Z, beta_complex, W)
は、W←(1.0+0.5i)×H×Z+(−i)×W を計算します。alpha_complex
とbeta_complex
は複素数のスカラーです。H
はエルミート行列であり、HH=H を満たします(ここで HH は H の共役転置です)。上三角部分の1.0 + 2.0im
に対応する下三角部分は、その共役である1.0 - 2.0im
となっています。
- 要素の型も適切である必要があります。複素行列の場合は
ComplexF64
などを使用します。 - 行列の次元は、指定された演算と整合している必要があります。そうでない場合は
DimensionMismatch
エラーが発生します。 side
引数 ('L'
または'R'
) は、エルミート行列(または対称行列)が一般行列の左側から乗算されるか、右側から乗算されるかを指定します。uplo
引数 ('U'
または'L'
) は、エルミート行列または対称行列のどちらの三角部分を計算に使用するかを指定します。もう一方の三角部分は、エルミート性または対称性に基づいて暗黙的に扱われます。hemm!()
はインプレース演算であるため、結果は常に第三引数(上記の例ではC
またはW
)に上書きされます。元の行列を保持したい場合は、事前にcopy()
関数などでコピーを作成する必要があります。
標準的な行列乗算 (*) とブロードキャスティング
最も直接的な代替方法は、Julia の標準的な行列乗算演算子 *
を使用して、エルミート行列(または対称行列)と一般行列の積を明示的に計算することです。必要に応じて、スカラー倍や加算はブロードキャスティング (.
) を利用して行います。
# 例: C ← alpha * A * B + beta * C (A は対称行列)
function hemm_alternative1!(C, A, B, alpha, beta)
C .= alpha .* (A * B) .+ beta .* C
return C
end
# 例: C ← alpha * B * A + beta * C (A は対称行列)
function hemm_alternative2!(C, B, A, alpha, beta)
C .= alpha .* (B * A) .+ beta .* C
return C
end
# 利用例
A_alt = [2.0 1.0; 1.0 3.0]
B_alt = [1.0 2.0; 3.0 4.0]
C_alt1 = zeros(size(A_alt, 1), size(B_alt, 2))
C_alt2 = zeros(size(B_alt, 1), size(A_alt, 2))
alpha_alt = 2.0
beta_alt = 1.0
hemm_alternative1!(C_alt1, A_alt, B_alt, alpha_alt, beta_alt)
println("C_alt1 (A * B):")
println(C_alt1)
hemm_alternative2!(C_alt2, B_alt, A_alt, alpha_alt, beta_alt)
println("\nC_alt2 (B * A):")
println(C_alt2)
利点
- 複素数や異なる数値型にも柔軟に対応できます。
- BLAS の詳細を意識する必要がありません。
- 直感的で理解しやすいコードになります。
欠点
- BLAS のような高度に最適化されたルーチンを利用しないため、特に大規模な行列の場合にはパフォーマンスが劣る可能性があります。
LinearAlgebra モジュールの他の関数
LinearAlgebra
モジュールには、行列積に関連する他の便利な関数があります。場合によっては、これらの関数を組み合わせて hemm!()
と同様の計算を行うことができます。
- mul!
インプレースでの行列乗算を行います。スカラー倍や加算は別途行う必要があります。
using LinearAlgebra
function hemm_alternative3!(C, A, B, alpha, beta)
temp = similar(C)
mul!(temp, A, B, alpha, 0) # temp ← alpha * A * B + 0 * temp
C .= temp .+ beta .* C
return C
end
# 利用例
A_alt = [2.0 1.0; 1.0 3.0]
B_alt = [1.0 2.0; 3.0 4.0]
C_alt3 = zeros(size(A_alt, 1), size(B_alt, 2))
alpha_alt = 2.0
beta_alt = 1.0
hemm_alternative3!(C_alt3, A_alt, B_alt, alpha_alt, beta_alt)
println("\nC_alt3 (mul!):")
println(C_alt3)
利点
mul!
は BLAS の基盤ルーチンを利用しているため、標準的な*
より高速な場合があります。- インプレース演算が可能で、メモリ割り当てを減らせる場合があります。
欠点
- エルミート性や対称性を利用した最適化は自動的には行われません。
- スカラー倍や加算を別途行う必要があるため、コードがやや冗長になることがあります。
疎行列の利用 (SparseArrays)
もしエルミート行列(または対称行列)が疎である場合(多くの要素がゼロである場合)、SparseArrays
モジュールを利用することで、メモリ使用量と計算時間を大幅に削減できる可能性があります。ただし、hemm!()
は疎行列を直接サポートしていないため、疎行列と密行列の積を効率的に行うための専用のアルゴリズムや関数を使用する必要があります。
using SparseArrays
# 例: 疎な対称行列
As = sparse([1, 2, 1, 2], [1, 2, 2, 1], [2.0, 3.0, 1.0, 1.0])
Bs = [1.0 2.0; 3.0 4.0]
Cs = zeros(size(As, 1), size(Bs, 2))
# 疎行列と密行列の積 (標準的な * 演算子を使用)
Cs .= As * Bs
println("\n疎な対称行列 As:")
println(As)
println("\n行列 Bs:")
println(Bs)
println("\nCs (As * Bs):")
println(Cs)
利点
- 疎な行列に対してメモリ効率と計算効率が向上します。
欠点
hemm!()
の直接的な代替とは言えません。疎行列と密行列の積に対する最適化は、密行列同士の積とは異なる考慮が必要です。
並列計算 (Threads や Distributed)
大規模な行列演算の場合、Julia の並列計算機能 (Threads
モジュールや Distributed
モジュール) を利用して、計算を複数のコアやプロセスに分散させることで、処理時間を短縮できます。標準的な行列乗算や mul!
と組み合わせて使用できます。
using LinearAlgebra
using Threads
function hemm_parallel!(C, A, B, alpha, beta)
m, n = size(C)
p = size(B, 2)
@threads for j in 1:p
for i in 1:m
dot_product = zero(eltype(C))
for k in 1:size(A, 2)
dot_product += A[i, k] * B[k, j]
end
C[i, j] = alpha * dot_product + beta * C[i, j]
end
end
return C
end
# 利用例
A_par = rand(100, 100)
B_par = rand(100, 150)
C_par = rand(100, 150)
alpha_par = 1.5
beta_par = 0.8
hemm_parallel!(C_par, A_par, B_par, alpha_par, beta_par)
println("\nC_par (並列計算): (最初の数要素)")
println(C_par[1:5, 1:5])
利点
- 大規模な計算を高速化できます。
欠点
- エルミート性や対称性を利用した最適化は、自身で実装する必要があります。
- 並列化のオーバーヘッドがあるため、小規模な問題では必ずしも高速になるとは限りません。
hemm!() を選択する理由
LinearAlgebra.BLAS.hemm!()
は、以下の点で特に有用です。
- エルミート/対称性の利用
エルミート性または対称性を利用することで、冗長な計算を避け、効率的な演算を行います。 - パフォーマンス
BLAS ライブラリは高度に最適化されており、特に大規模な行列に対して高速な演算を提供します。