Julia 行列の正規化: LinearAlgebra の活用方法

2025-05-27

LinearAlgebra.normalize!() は、Juliaの LinearAlgebra モジュールで提供されている関数の一つで、与えられたベクトルのノルム(通常はユークリッドノルムまたはL2ノルム)が1になるように、そのベクトルをインプレース(元の変数を直接変更)で正規化するために使われます。

もう少し詳しく説明しますね。

正規化とは?

ベクトルの正規化とは、そのベクトルの方向を変えずに、長さを1にすることです。これは、ベクトルをそのノルム(長さ)で割ることで実現されます。正規化されたベクトルは、単位ベクトルとも呼ばれます。

ノルム(長さ)とは?

ベクトルのノルムは、そのベクトルの「長さ」や「大きさ」を表す非負の値です。normalize!() 関数では、特に指定がない限り、ユークリッドノルム(L2ノルム)が用いられます。

ベクトル mathbfv=beginbmatrixv_1v_2vdotsv_nendbmatrix のユークリッドノルム ∣∣mathbfv∣∣_2 は、次のように計算されます。

∣∣v∣∣2​=v12​+v22​+⋯+vn2​​

normalize!() 関数の働き

LinearAlgebra.normalize!(v) のようにベクトル v を引数としてこの関数に渡すと、以下の処理が行われます。

  1. ベクトル v のユークリッドノルム ∣∣mathbfv∣∣_2 が計算されます。
  2. ベクトル v の各要素が、計算されたノルム ∣∣mathbfv∣∣_2 で割られます。

これにより、元のベクトル v は、長さが1の単位ベクトルに置き換えられます。**重要なのは、!(エクスクラメーションマーク)が付いているため、この操作は元の変数 v の内容を直接変更する(インプレース操作)という点です。**新しいベクトルを作成して返すのではなく、元のベクトルが上書きされます。

使用例

using LinearAlgebra

v = [3.0, 4.0]
println("元のベクトル: ", v)

normalize!(v)
println("正規化されたベクトル: ", v)
println("正規化されたベクトルのノルム: ", norm(v))

この例では、ベクトル [3.0, 4.0] のノルムは sqrt32+42=sqrt9+16=sqrt25=5 です。normalize!(v) を実行すると、v の各要素は 5 で割られ、結果として [3.0/5, 4.0/5] つまり [0.6, 0.8] になります。最後に norm(v) で正規化されたベクトルのノルムを計算すると、ほぼ 1 になることが確認できます(浮動小数点数の誤差により、完全に 1 にならない場合があります)。

用途

ベクトルの正規化は、機械学習、物理シミュレーション、グラフィックスなど、さまざまな分野でよく用いられます。例えば、

  • コサイン類似度の計算
    2つのベクトルの間の角度のコサインを計算する際に、事前にベクトルを正規化することが一般的です。
  • 特徴量のスケーリング
    機械学習において、異なるスケールの特徴量を扱う際に、正規化によってスケールを揃えることができます。
  • 方向の表現
    正規化されたベクトルは、ベクトルの方向のみを表したい場合に便利です。


引数の型に関するエラー (MethodError)

  • トラブルシューティング
    • 引数が本当にベクトル(Vector{T} など)であるか確認してください。typeof(変数名) で変数の型を調べることができます。
    • もし行列を正規化したい場合は、行列の各列や各行に対して個別に normalize!() を適用する必要があります。eachcol()eachrow() を利用してイテレートできます。
    • スカラ値を正規化することは数学的に意味がないため、スカラ値を渡さないようにしてください。
  • 原因
    normalize!() はベクトル(1次元配列)を入力として期待しています。行列やスカラ値などを渡すと、適切なメソッドが見つからずエラーになります。
  • エラー内容
    MethodError: no method matching normalize!(::DataType) のように、引数の型が AbstractVector のサブタイプでない場合に発生します。

ゼロベクトルに関する問題 (DivideError)

  • トラブルシューティング
    • 正規化を行う前に、ベクトルがゼロベクトルでないか確認してください。all(iszero, v) のようにして判定できます。
    • もしゼロベクトルである可能性がある場合は、正規化を行う前に特別な処理(例えば、ゼロベクトルをそのまま返す、エラーメッセージを表示するなど)を追加することを検討してください。
  • 原因
    入力ベクトルがゼロベクトル(全ての要素が 0 のベクトル)の場合、そのノルムは 0 になります。正規化の際にノルムで各要素を割るため、ゼロ除算が発生します。
  • エラー内容
    DivideError: integer division error または DivideError: floating point division by zero のように、ゼロ除算に関するエラーが発生する可能性があります。

LinearAlgebra モジュールがロードされていない (UndefVarError)

  • トラブルシューティング
    • スクリプトの先頭に using LinearAlgebra を追加して、LinearAlgebra モジュールをロードしてください。
  • 原因
    normalize!() 関数は LinearAlgebra モジュールに含まれています。このモジュールを using LinearAlgebra または import LinearAlgebra で明示的にロードしていない場合に発生します。
  • エラー内容
    UndefVarError: normalize! not defined のように、normalize! が定義されていないというエラーが発生します。

インプレース操作による意図しない変更

  • トラブルシューティング
    • 正規化前のベクトルを保持しておきたい場合は、normalize!(copy(v)) のように copy() 関数を使ってベクトルのコピーを作成し、そのコピーを正規化してください。
    • あるいは、非インプレース版の normalize(v) 関数(LinearAlgebra モジュールに存在します)を使用することもできます。こちらは正規化された新しいベクトルを返します。
  • 問題点
    エラーは発生しませんが、normalize!() は元の変数を直接変更するため、正規化前のベクトルを後で使いたい場合に意図しない結果になることがあります。

数値的な安定性の問題

  • トラブルシューティング
    • 通常の使用範囲ではあまり問題になりませんが、もし数値的な不安定さが気になる場合は、より高度な数値計算ライブラリや手法を検討する必要があるかもしれません。
    • ベクトルのスケールが大きく異なる場合は、事前に何らかのスケーリング処理を行うことも有効かもしれません。
  • 問題点
    極端に大きな値や小さな値を含むベクトルを正規化する場合、浮動小数点数の演算精度によって誤差が大きくなる可能性があります。

LinearAlgebra.normalize!() を安全かつ正確に使用するためには、以下の点に注意することが重要です。

  • 数値的な安定性
    極端な値を含むベクトルを扱う場合は注意する。
  • インプレース操作
    元のベクトルを保持したい場合は copy() を使用するか、非インプレース版の normalize() を検討する。
  • モジュールのロード
    LinearAlgebra モジュールを using または import する。
  • ゼロベクトル
    ゼロベクトルを渡さないように事前にチェックするか、適切な処理を行う。
  • 入力の型
    AbstractVector のサブタイプである1次元配列(ベクトル)を渡す。


基本的なベクトルの正規化

まず、最も基本的なベクトルの正規化の例です。

using LinearAlgebra

# ベクトルの定義
v = [3.0, 4.0]
println("元のベクトル: ", v)

# ベクトルの正規化(インプレース)
normalize!(v)
println("正規化されたベクトル: ", v)

# 正規化されたベクトルのノルムを確認
norm_v = norm(v)
println("正規化されたベクトルのノルム: ", norm_v)

この例では、[3.0, 4.0] というベクトル vnormalize!(v) によって正規化しています。実行結果を見ると、v の値が [0.6, 0.8] に変わり、そのノルムがほぼ 1 になっていることがわかります。

複数のベクトルの正規化(配列の配列)

複数のベクトルが配列の配列として格納されている場合に、それぞれのベクトルを正規化する例です。

using LinearAlgebra

vectors = [[1.0, 0.0], [0.0, -1.0], [3.0, 4.0]]
println("元のベクトルリスト: ", vectors)

normalized_vectors = []
for vec in vectors
    normalized_vec = normalize(vec) # 非インプレース版を使用
    push!(normalized_vectors, normalized_vec)
end
println("正規化されたベクトルリスト: ", normalized_vectors)

# あるいは、内包表記を使うとより簡潔に書けます
normalized_vectors_comprehension = [normalize(vec) for vec in vectors]
println("正規化されたベクトルリスト (内包表記): ", normalized_vectors_comprehension)

この例では、normalize() 関数(非インプレース版)を使って、元のベクトルリストの各ベクトルを正規化し、新しいリスト normalized_vectors に格納しています。内包表記を使うと、より簡潔に同じ処理を記述できます。

行列の各列を正規化する

行列の各列を個別に正規化する例です。

using LinearAlgebra

# 行列の定義
A = [1.0 2.0; 3.0 4.0; 5.0 6.0]
println("元の行列:\n", A)

# 各列を正規化する
normalized_cols = similar(A) # 結果を格納する同じサイズの行列を作成
for j in 1:size(A, 2)
    normalized_cols[:, j] = normalize(A[:, j])
end
println("列ごとに正規化された行列:\n", normalized_cols)

この例では、行列 A の各列 (A[:, j]) を normalize() 関数で正規化し、結果を normalized_cols の対応する列に格納しています。ここでは非インプレース版を使用しています。もしインプレースで元の行列を変更したい場合は、各列を直接 normalize!(view(A, :, j)) のように処理する必要がありますが、これは少し注意が必要です。

行列の各行を正規化する

同様に、行列の各行を個別に正規化する例です。

using LinearAlgebra

# 行列の定義(再掲)
A = [1.0 2.0; 3.0 4.0; 5.0 6.0]
println("元の行列:\n", A)

# 各行を正規化する
normalized_rows = similar(A) # 結果を格納する同じサイズの行列を作成
for i in 1:size(A, 1)
    normalized_rows[i, :] = normalize(A[i, :])
end
println("行ごとに正規化された行列:\n", normalized_rows)

この例では、行列 A の各行 (A[i, :]) を normalize() 関数で正規化し、結果を normalized_rows の対応する行に格納しています。

正規化とコサイン類似度の計算

正規化が役立つ例として、コサイン類似度の計算があります。コサイン類似度は、2つのベクトルの方向の類似度を測る指標で、正規化されたベクトル間の内積として計算できます。

using LinearAlgebra

# 2つのベクトルの定義
a = [1.0, 2.0, 3.0]
b = [4.0, 5.0, 6.0]

# ベクトルを正規化
normalized_a = normalize(a)
normalized_b = normalize(b)

# コサイン類似度を計算(正規化されたベクトルの内積)
cosine_similarity = dot(normalized_a, normalized_b)
println("ベクトル a: ", a)
println("ベクトル b: ", b)
println("コサイン類似度: ", cosine_similarity)

# 参考:正規化せずに内積を計算し、それぞれのノルムで割る方法
cosine_similarity_alternative = dot(a, b) / (norm(a) * norm(b))
println("コサイン類似度 (別計算): ", cosine_similarity_alternative)

この例では、2つのベクトル ab を正規化し、それらの内積を計算することでコサイン類似度を求めています。正規化されたベクトルを使うことで、ベクトルの大きさの違いによる影響を受けずに、方向の類似度を評価できます。



非インプレース版 normalize() 関数の使用

最も直接的な代替方法は、LinearAlgebra モジュールに用意されている非インプレース版の normalize() 関数を使用することです。normalize(v) は、ベクトル v を正規化した新しいベクトルを返します。元のベクトル v は変更されません。

using LinearAlgebra

v = [3.0, 4.0]
println("元のベクトル: ", v)

normalized_v = normalize(v)
println("正規化されたベクトル (normalize): ", normalized_v)
println("元のベクトル (変更なし): ", v)

norm_normalized_v = norm(normalized_v)
println("正規化されたベクトルのノルム: ", norm_normalized_v)

この方法は、元のベクトルを保持したい場合に非常に便利です。

ベクトルのノルムを明示的に計算して除算する

normalize!()normalize() 関数を使わずに、ベクトルのノルムを自分で計算し、各要素をそのノルムで割ることで正規化を行うことができます。

using LinearAlgebra

v = [3.0, 4.0]
println("元のベクトル: ", v)

norm_v = norm(v)
normalized_v_manual = v / norm_v # 要素ごとの除算
println("手動で正規化されたベクトル: ", normalized_v_manual)

norm_normalized_v_manual = norm(normalized_v_manual)
println("手動で正規化されたベクトルのノルム: ", norm_normalized_v_manual)

この方法は、正規化の内部処理を理解するのに役立ちます。また、特定のノルム(例えば L1ノルムなど)で正規化したい場合に、norm() 関数に適切な引数を渡すことで対応できます。

L1ノルムでの正規化の例

v = [3.0, -4.0, 5.0]
println("元のベクトル: ", v)

norm_l1 = sum(abs.(v)) # L1ノルムを計算
normalized_v_l1 = v / norm_l1
println("L1ノルムで正規化されたベクトル: ", normalized_v_l1)

norm_normalized_v_l1 = sum(abs.(normalized_v_l1))
println("L1ノルムで正規化されたベクトルのL1ノルム: ", norm_normalized_v_l1)

インプレース操作を手動で行う

normalize!() と同様のインプレース操作を手動で行うことも可能です。

using LinearAlgebra

v = [3.0, 4.0]
println("元のベクトル (Before): ", v)

norm_v = norm(v)
for i in eachindex(v)
    v[i] /= norm_v
end

println("元のベクトル (After - 手動インプレース): ", v)
println("正規化されたベクトルのノルム: ", norm(v))

この方法は、インプレース操作の仕組みを理解するのに役立ちますが、通常は normalize!() を使う方が簡潔で効率的です。

関数として定義する

正規化の処理を再利用したい場合は、独自の関数として定義することもできます。インプレース版と非インプレース版の両方を定義できます。

using LinearAlgebra

function normalize_inplace!(vec::AbstractVector)
    norm_vec = norm(vec)
    for i in eachindex(vec)
        vec[i] /= norm_vec
    end
    return vec # 慣習として変更されたベクトルを返す
end

function normalize_new(vec::AbstractVector)
    norm_vec = norm(vec)
    return vec / norm_vec
end

v1 = [1.0, 2.0, 3.0]
println("元のベクトル v1: ", v1)
normalize_inplace!(v1)
println("正規化されたベクトル v1 (インプレース): ", v1)

v2 = [4.0, 5.0, 6.0]
println("元のベクトル v2: ", v2)
normalized_v2 = normalize_new(v2)
println("正規化されたベクトル v2 (新規): ", normalized_v2)
println("元のベクトル v2 (変更なし): ", v2)