Juliaプログラミング例で学ぶnormalize():CG・機械学習での活用

2025-05-27

Juliaプログラミング言語における LinearAlgebra.normalize() は、主にベクトルを正規化(normalize)するための関数です。正規化とは、ベクトルの方向を変えずに、その長さ(ノルム)を1にする操作を指します。これにより、単位ベクトル(unit vector)が得られます。

機能

normalize(v): ベクトル v を正規化し、そのノルムが1の新しいベクトルを返します。デフォルトでは、2-ノルム(ユークリッドノルム)が使用されます。

normalize(v, p): ベクトル v を p-ノルムで正規化します。p には以下の値を指定できます。

  • Inf: 無限大ノルム(最大値ノルム)で正規化します。
  • 2 (デフォルト): 2-ノルム(L2ノルム、ユークリッドノルム)で正規化します。
  • 1: 1-ノルム(L1ノルム、マンハッタン距離)で正規化します。

normalize!(v): normalize と同じように動作しますが、これは**インプレース(in-place)**操作です。つまり、元のベクトル v が直接変更され、正規化されたベクトルに置き換えられます。新しいベクトルは生成されません。

なぜ正規化するのか?

正規化は、線形代数や様々な応用分野で非常に重要です。

  • 確率分布: 統計学において、確率質量関数や確率密度関数を正規化して、合計が1になるように調整する場合など。
  • 機械学習: 特徴量のスケーリングや勾配降下法などで、安定した計算を行うために利用されます。
  • スケーリングの均一化: 異なる長さのベクトルを比較したり、計算に利用したりする際に、長さの違いによる影響を排除し、均一な尺度で扱うことができます。
  • 方向の表現: ベクトルの方向のみを考慮したい場合に便利です。例えば、コンピューターグラフィックスで光線の方向を扱う場合など。
using LinearAlgebra

# ベクトルの定義
v = [3, 4]

# 2-ノルムで正規化 (デフォルト)
v_normalized = normalize(v)
println("正規化されたベクトル (2-ノルム): ", v_normalized)
println("正規化されたベクトルのノルム: ", norm(v_normalized)) # 約1.0になる

# 1-ノルムで正規化
v_normalized_l1 = normalize(v, 1)
println("正規化されたベクトル (1-ノルム): ", v_normalized_l1)
println("正規化されたベクトルの1-ノルム: ", norm(v_normalized_l1, 1)) # 約1.0になる

# 無限大ノルムで正規化
v_normalized_linf = normalize(v, Inf)
println("正規化されたベクトル (Inf-ノルム): ", v_normalized_linf)
println("正規化されたベクトルのInf-ノルム: ", norm(v_normalized_linf, Inf)) # 約1.0になる

# インプレースでの正規化
w = [6.0, 8.0]
println("元のベクトル w: ", w)
normalize!(w) # w自体が変更される
println("インプレースで正規化されたベクトル w: ", w)
println("インプレースで正規化されたベクトルのノルム: ", norm(w))


ゼロベクトルを正規化しようとした場合

これは最も一般的なエラーです。正規化はベクトルをそのノルムで割る操作ですが、ゼロベクトルのノルムは0です。0で割ることは数学的に未定義であるため、エラーが発生したり、NaN(Not a Number)や Inf(Infinity)のような特殊な浮動小数点値が返されたりします。

エラーの例

using LinearAlgebra

v_zero = [0.0, 0.0, 0.0]
# normalize(v_zero) # これはNaNを含むベクトルを返すか、エラーになる可能性があります

問題点

  • normalize([0,0,Inf]) のような、無限大を含むベクトルを正規化しようとすると、古いJuliaのバージョンでは [0.0, 0.0, 0.0] が返されるバグがありましたが、現在のバージョンでは [0.0, 0.0, NaN] のように期待される結果になります。
  • normalize([0.0, 0.0, 0.0]) は、多くのシステムやJuliaのバージョンで [NaN, NaN, NaN] を返します。これは、0/0 が未定義であるためです。

対処法

normalize() を呼び出す前に、ベクトルのノルムがゼロでないことを確認するのが最も確実な方法です。

using LinearAlgebra

v_test = [0.0, 0.0, 0.0]

if norm(v_test) ≈ 0.0 # 浮動小数点数の比較には`≈` (isapprox) を使う
    println("このベクトルはゼロベクトルなので正規化できません。")
    # 代替の処理(例: ゼロベクトルをそのまま返す、特定のエラー処理を行う)
    normalized_v = zeros(length(v_test)) # または v_test のまま
else
    normalized_v = normalize(v_test)
    println("正規化されたベクトル: ", normalized_v)
end

v_nonzero = [1.0, 2.0, 3.0]
if norm(v_nonzero) ≈ 0.0
    println("このベクトルはゼロベクトルなので正規化できません。")
else
    normalized_v = normalize(v_nonzero)
    println("正規化されたベクトル: ", normalized_v)
end

データ型に関する問題

normalize() は数値型のベクトルに対して動作しますが、非数値型やシンボリックな変数を含むベクトルに対してはエラーになることがあります。

エラーの例

ModelingToolkit.jl のようなシンボリック計算を行うパッケージと組み合わせた場合、normalize() が期待通りに動作しないことがあります。これは、normalize 関数が内部でブールコンテキストで使用されるisemptyのような操作を呼び出し、それがシンボリックなNum型に対して機能しないためです。

using LinearAlgebra
using ModelingToolkit # 例として

# @variables t pos(t)[1:3] = [0.0, 0.0, 10.0]
# normalize(pos) # これはTypeErrorを引き起こす可能性があります

対処法

  • ./ norm() を使う
    シンボリックなケースでは、normalize(pos) の代わりに pos ./ norm(pos) のように明示的に割り算を行うことで解決する場合があります。これは、normalize 関数の内部実装がシンボリック型と互換性のない操作(例: isempty)を呼び出す可能性があるためです。
  • 数値データを使用する
    normalize() は、最終的に具体的な数値データで評価されるベクトルに対して使用することを想定しています。シンボリック計算が必要な場合は、最終的に数値に変換してから正規化を行うか、シンボリック計算ライブラリが提供する適切な方法を使用してください。

不正確な浮動小数点計算 (Floating Point Precision Issues)

非常に小さい(しかしゼロではない)ノルムを持つベクトルを正規化しようとすると、浮動小数点数の精度限界により、予期せぬ結果(NaNInf)や、理想的には1になるはずのノルムがわずかにずれることがあります。

問題点

  • 正規化されたベクトルのノルムが厳密に1にならないことがあります(例: 0.99999999999999991.0000000000000002)。これは浮動小数点演算の性質上避けられないものです。
  • 非常に小さい数で割ると、結果が非常に大きくなったり、精度の問題で NaN になったりすることがあります。

対処法

  • 許容誤差を考慮する
    正規化されたベクトルのノルムが厳密に1であることを期待するのではなく、norm(v_normalized) ≈ 1.0 のように許容誤差を考慮した比較を行います。
  • norm(v) ≈ 0.0 でチェックする
    上記のゼロベクトルの場合と同様に、norm(v) が非常に小さい値(ゼロに近い)かどうかを確認し、その場合は特殊な処理を行います。

インプレース操作 (normalize!) の副作用

normalize! は元のベクトルを直接変更するため、意図せず元のデータが失われる可能性があります。

問題点

  • 元のベクトルが後で必要になる場合に、normalize! を使うと元のデータが上書きされてしまいます。

対処法

  • メモリ効率を重視し、元のベクトルが不要な場合
    normalize!(v) を使用します。必要に応じて copy(v) で明示的にコピーを作成してから normalize! を適用することもできます。
  • 元のベクトルを保持したい場合
    normalize(v) を使用して、新しい正規化されたベクトルを生成します。
using LinearAlgebra

original_vec = [1.0, 2.0, 3.0]
mutable_vec = copy(original_vec) # コピーを作成

# インプレース操作
normalize!(mutable_vec)
println("元のベクトル (変更なし): ", original_vec)
println("変更されたベクトル: ", mutable_vec)

# 新しいベクトルを返す操作
new_normalized_vec = normalize(original_vec)
println("元のベクトル (変更なし): ", original_vec)
println("新しく生成された正規化ベクトル: ", new_normalized_vec)

単純なミスですが、LinearAlgebra パッケージをインポートしていないと、normalize() 関数が見つからないというエラーが発生します。

エラーの例

# using LinearAlgebra を忘れた場合
v = [1, 2, 3]
# normalize(v) # ERROR: UndefVarError: normalize not defined

対処法

スクリプトの冒頭で using LinearAlgebra を追加します。

using LinearAlgebra # これを忘れないこと!

v = [1, 2, 3]
normalized_v = normalize(v)
println(normalized_v)


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

最も基本的な使い方です。2次元、3次元、あるいはそれ以上の次元のベクトルを正規化して、単位ベクトルを得ます。

using LinearAlgebra

# 2次元ベクトルの正規化
vec2d = [3.0, 4.0]
normalized_vec2d = normalize(vec2d)
println("元の2Dベクトル: ", vec2d)
println("正規化された2Dベクトル: ", normalized_vec2d)
println("正規化された2Dベクトルのノルム: ", norm(normalized_vec2d)) # ほぼ1.0

println("-"^30)

# 3次元ベクトルの正規化
vec3d = [1.0, -2.0, 3.0]
normalized_vec3d = normalize(vec3d)
println("元の3Dベクトル: ", vec3d)
println("正規化された3Dベクトル: ", normalized_vec3d)
println("正規化された3Dベクトルのノルム: ", norm(normalized_vec3d)) # ほぼ1.0

インプレースでの正規化 (normalize!)

元のベクトルを変更してメモリ使用量を抑えたい場合や、特定のアルゴリズムでインプレース操作が求められる場合に normalize! を使います。

using LinearAlgebra

# 元のベクトル
data_vec = [10.0, 20.0, 30.0]
println("変更前のベクトル: ", data_vec)

# インプレースで正規化
normalize!(data_vec)
println("インプレースで正規化後のベクトル: ", data_vec)
println("正規化後のノルム: ", norm(data_vec)) # ほぼ1.0

# 元のベクトルは変更されていることに注意
another_vec = [5.0, 12.0]
copy_of_another_vec = copy(another_vec) # 元の値を保持するためにコピーを作成

normalize!(another_vec)
println("オリジナル (コピー前): ", copy_of_another_vec)
println("インプレースで正規化: ", another_vec)

特定のノルムでの正規化

デフォルトでは2-ノルム(ユークリッドノルム)ですが、1-ノルムや無限大ノルムで正規化することもできます。

using LinearAlgebra

vec_val = [1.0, 2.0, -3.0]

# 2-ノルム (デフォルト)
norm2_vec = normalize(vec_val, 2)
println("2-ノルム正規化: ", norm2_vec, ", ノルム: ", norm(norm2_vec, 2))

# 1-ノルム
norm1_vec = normalize(vec_val, 1)
println("1-ノルム正規化: ", norm1_vec, ", ノルム: ", norm(norm1_vec, 1))

# 無限大ノルム (最大値ノルム)
norminf_vec = normalize(vec_val, Inf)
println("Inf-ノルム正規化: ", norminf_vec, ", ノルム: ", norm(norminf_vec, Inf))

ゼロベクトルを正規化しようとした場合の対処

ゼロベクトルの正規化は数学的に未定義であり、NaN が返されるため、適切なエラーハンドリングが必要です。

using LinearAlgebra

zero_vec = [0.0, 0.0, 0.0]

# ゼロベクトルかどうかを事前にチェック
if norm(zero_vec) ≈ 0.0 # 浮動小数点数の比較には `≈` を使う
    println("警告: ゼロベクトルは正規化できません。元のベクトルを返します。")
    normalized_zero_vec = zero_vec # あるいは適切な代替処理
else
    normalized_zero_vec = normalize(zero_vec)
    println("正規化されたゼロベクトル: ", normalized_zero_vec)
end

println("-"^30)

# 通常のベクトル
non_zero_vec = [5.0, -1.0, 2.0]
if norm(non_zero_vec) ≈ 0.0
    println("警告: ゼロベクトルは正規化できません。元のベクトルを返します。")
    normalized_non_zero_vec = non_zero_vec
else
    normalized_non_zero_vec = normalize(non_zero_vec)
    println("正規化された非ゼロベクトル: ", normalized_non_zero_vec)
    println("そのノルム: ", norm(normalized_non_zero_vec))
end

コンピューターグラフィックス: 方向ベクトルとしての利用

3Dグラフィックスでは、光の方向、オブジェクトの移動方向などを単位ベクトルで表現することがよくあります。

using LinearAlgebra

# オブジェクトの位置 (例: カメラの位置)
camera_pos = [0.0, 0.0, 0.0]

# 光源の位置 (例: 点光源)
light_pos = [10.0, 5.0, -20.0]

# 光の方向ベクトル (光源からカメラへ)
direction_to_light = light_pos - camera_pos
println("光源への生の方向ベクトル: ", direction_to_light)

# 方向ベクトルを正規化して単位ベクトルにする
# これにより、光の方向のみが重要になり、距離は考慮されない
normalized_light_direction = normalize(direction_to_light)
println("正規化された光の方向ベクトル: ", normalized_light_direction)
println("方向ベクトルのノルム: ", norm(normalized_light_direction)) # ほぼ1.0

# 例: オブジェクトの移動方向 (速さに関係なく方向のみを指定)
move_vector = [100.0, 50.0, 0.0] # 速さは考慮せず、方向のみ抽出したい
normalized_move_direction = normalize(move_vector)
println("正規化された移動方向ベクトル: ", normalized_move_direction)

データの前処理において、特徴量のスケーリングは非常に重要です。normalize() は、特徴量のベクトルを単位ノルムにスケーリングする場合に利用できます。

using LinearAlgebra

# 機械学習の特徴量ベクトル (例: [年齢, 収入, 学歴年数])
feature_vector = [35.0, 75000.0, 16.0]

# 正規化による特徴量スケーリング
# この方法では、各特徴量の絶対的な大きさが比較できなくなるが、
# 方向性や相対的な比率が保たれる場合に有用
normalized_features = normalize(feature_vector)
println("元の特徴量ベクトル: ", feature_vector)
println("正規化された特徴量ベクトル: ", normalized_features)

# 注: 機械学習では、通常 Min-Max スケーリングや Standardization (Z-score normalization) が
# よく使われますが、normalize() は特定の目的(例: コサイン類似度)のために使われることがあります。


ここでは、normalize() の代替手段となるプログラミング方法をいくつか説明します。

normalize(v) の最も直接的な代替は、ベクトルの各要素をそのノルムで割ることです。これは normalize() が内部で行っていることと本質的に同じです。

using LinearAlgebra # norm() を使うため

# 元のベクトル
v = [3.0, 4.0, 5.0]

# 2-ノルムを計算
vec_norm = norm(v)
println("元のベクトル: ", v)
println("ベクトルの2-ノルム: ", vec_norm)

# 各要素をノルムで割る
if vec_norm != 0.0
    normalized_v_manual = v ./ vec_norm
    println("手動で正規化されたベクトル: ", normalized_v_manual)
    println("手動正規化後のノルム: ", norm(normalized_v_manual))
else
    println("警告: ゼロベクトルなので正規化できません。")
    normalized_v_manual = similar(v) # 同サイズのゼロベクトルなど
    fill!(normalized_v_manual, 0.0)
end

println("-"^30)

# 特定のノルム p で正規化する場合
# L1ノルムで正規化
v_l1 = [1.0, -2.0, 3.0]
norm_l1 = norm(v_l1, 1)
normalized_v_l1_manual = v_l1 ./ norm_l1
println("L1ノルム手動正規化: ", normalized_v_l1_manual)
println("L1ノルム手動正規化後の1-ノルム: ", norm(normalized_v_l1_manual, 1))

# インプレースでの手動正規化
w = [6.0, 8.0]
w_norm = norm(w)
if w_norm != 0.0
    w ./= w_norm # インプレースで要素ごとに割り算
    println("インプレース手動正規化: ", w)
else
    println("警告: ゼロベクトルなので正規化できません。")
end

利点

  • 特定のノルム(p)を指定したい場合に、normalize(v, p) の代わりに v ./ norm(v, p) のように書くことができます。
  • normalize() と同じ結果が得られることを理解するのに役立ちます。

欠点

  • コードがわずかに長くなり、意図が normalize() ほど明確ではない場合があります。
  • ゼロベクトルを正規化しようとした場合、NaN が生成されるのを防ぐための明示的なチェックが必要です。normalize() はこの点をより堅牢に処理します(通常は NaN を返しますが、意図を明確にできます)。

normalize() はベクトルのノルムを1にする「単位ベクトル化」ですが、データの前処理などでは、これとは異なる目的でベクトルや配列の値をスケーリングすることがよくあります。

a. Min-Max スケーリング (Normalization)

値を特定の範囲(例: [0, 1])にスケーリングします。各特徴量を個別に処理する場合によく使われます。

xscaled​=xmax​−xmin​x−xmin​​

# データの範囲を [0, 1] にスケーリングする関数
function min_max_scale(v::Vector)
    min_val = minimum(v)
    max_val = maximum(v)
    if max_val - min_val == 0.0
        return zeros(length(v)) # すべての値が同じ場合は0にスケーリング
    else
        return (v .- min_val) ./ (max_val - min_val)
    end
end

data = [10.0, 20.0, 5.0, 30.0]
scaled_data = min_max_scale(data)
println("Min-Max スケーリング前: ", data)
println("Min-Max スケーリング後: ", scaled_data)

利点

  • 機械学習の前処理でよく使われる。
  • データを特定の範囲に収め、外れ値の影響を抑えやすい。

欠点

  • 外れ値に非常に敏感で、スケーリング後の分布が歪む可能性がある。

b. 標準化 (Standardization / Z-score Normalization)

平均を0、標準偏差を1にスケーリングします。各特徴量を個別に処理する場合によく使われます。

xscaled​=σx−μ​

ここで、μ は平均、σ は標準偏差です。

# 標準化(Z-score Normalization)する関数
function standardize(v::Vector)
    mu = mean(v)
    sigma = std(v)
    if sigma == 0.0
        return zeros(length(v)) # 標準偏差が0の場合は0にスケーリング
    else
        return (v .- mu) ./ sigma
    end
end

using Statistics # mean() と std() を使うため

data = [1.0, 2.0, 3.0, 4.0, 5.0]
standardized_data = standardize(data)
println("標準化前: ", data)
println("標準化後: ", standardized_data)
println("標準化後の平均: ", mean(standardized_data)) # ほぼ0.0
println("標準化後の標準偏差: ", std(standardized_data)) # ほぼ1.0

利点

  • データが正規分布に従うと仮定されるモデルで特に有効。
  • 外れ値の影響を受けにくい。

欠点

  • スケーリング後の値の範囲が保証されない。

c. ソフトマックス関数 (Softmax Function)

主に分類問題の出力層で、ロジット(log-odds)を確率分布に変換するために使われます。ベクトルの要素の合計が1になります。

Si​=∑j​ezj​ezi​​

function softmax(x::Vector)
    exp_x = exp.(x)
    return exp_x ./ sum(exp_x)
end

scores = [1.0, 2.0, 3.0] # 分類器のスコアなど
probabilities = softmax(scores)
println("スコア: ", scores)
println("ソフトマックス後の確率: ", probabilities)
println("確率の合計: ", sum(probabilities)) # ほぼ1.0

利点

  • 多クラス分類のモデルの出力層で広く使われる。
  • 入力を確率分布に変換し、合計が1になることを保証する。

欠点

  • 通常のベクトルの正規化とは目的が異なる。
  • 入力のスケールに非常に敏感。

LinearAlgebra.normalize() はベクトルの方向を保持しつつ長さを1にするための関数です。 しかし、プログラミングの状況によっては、以下のような代替手段が適切です。

  • ソフトマックス関数: 入力を確率分布に変換し、要素の合計を1にしたい場合(特に分類問題)。
  • 標準化 (Z-score Normalization): データを平均0、標準偏差1に変換したい場合。
  • Min-Max スケーリング: データを特定の範囲に収めたい場合。
  • 手動での正規化: v ./ norm(v) のように明示的に記述することで、normalize() と同じ結果が得られます。柔軟性がありますが、ゼロベクトルのチェックが必要です。