Pythonで学ぶ数値計算のアルゴリズムと実装 第5回:計算の「正しさ」を評価する:ノルムと条件数

Python
この記事は約18分で読めます。

こんにちは、JS2IIUです。
連載の前半では、関数の近似や数値積分といった、主に解析学的なアプローチによる数値計算を学んできました。第5回となる今回からは、数値計算のもう一つの巨大な柱である「線形代数」の領域へと足を踏み入れていきます。

機械学習やディープラーニングの世界において、行列やベクトルの演算は日常茶飯事です。しかし、理論上は解けるはずの計算が、コンピュータ上では「なぜか不安定になる」「少しデータが変わっただけで結果が劇的に変わる」といった現象に遭遇したことはないでしょうか。

コンピュータは実数を有限のビット数で表現する「浮動小数点数」を扱っているため、常に微小な誤差がつきまといます。この微小な誤差が、行列計算を通じてどのように拡大し、結果を破壊するのか。その「正しさ(安定性)」を定量的に評価するための強力な道具が、今回解説する「ノルム」と「条件数」です。今回もよろしくお願いします。

1. 数値計算における「大きさ」の定義:ベクトルのノルム

数値計算における誤差を議論するためには、まず「誤差(ベクトル)がどれくらい大きいか」を測る物差しが必要です。この物差しが「ノルム(Norm)」です。

ベクトル \(\mathbf{x} = [x_1, x_2, \dots, x_n]^T\) に対して、よく使われるノルムには主に次の3種類があります。それぞれのノルムの意味や直感的なイメージ、用途を丁寧に解説します。

  • L1ノルム(マンハッタン距離)
    • 各要素の絶対値の和をとるノルムです。
    • 直感的には「碁盤の目」のように、縦横だけで移動した距離の合計(マンハッタン距離)を表します。
    • 外れ値(スパース性)を重視したい場合や、特徴量選択などでよく使われます。
      $$|\mathbf{x}|_1 = \sum_{i=1}^n |x_i|$$
  • L2ノルム(ユークリッド距離)
    • 各要素の二乗和の平方根をとるノルムです。
    • 直感的には「原点からの直線距離(ユークリッド距離)」を表します。
    • 最も一般的なノルムで、AIや機械学習の損失関数(MSEなど)でもよく使われます。
      $$|\mathbf{x}|_2 = \sqrt{\sum_{i=1}^n x_i^2}$$
  • L∞ノルム(最大値ノルム)
    • ベクトルの要素の中で絶対値が最大のものをとるノルムです。
    • 直感的には「どの成分が一番大きいか(最大誤差)」を測る物差しです。
    • 数値計算の誤差評価や、すべての成分で誤差を一定以下に抑えたい場合に重宝されます。
      $$|\mathbf{x}|_\infty = \max_{i} |x_i|$$

これらのノルムは、目的や評価したい観点によって使い分けられます。たとえば、L1ノルムは「全体の合計的な大きさ」、L2ノルムは「全体のエネルギーや距離」、L∞ノルムは「最悪ケースの大きさ」を測るのに適しています。特にL∞ノルムは「どの要素においても誤差を一定以下に抑えたい」という厳密な評価に向いています。

PyTorchによるベクトルノルムの実装

PyTorchの torch.linalg.norm を使うと、これらのノルムを簡単に計算できます。

Python
import torch

# サンプルベクトルの作成
x = torch.tensor([3.0, -4.0, 1.0])

# 各種ノルムの計算
norm_l1 = torch.linalg.norm(x, ord=1)
norm_l2 = torch.linalg.norm(x, ord=2)
norm_inf = torch.linalg.norm(x, ord=float('inf'))

print(f"Vector x: {x}")
print(f"L1 Norm: {norm_l1:.4f}")   # 3 + 4 + 1 = 8
print(f"L2 Norm: {norm_l2:.4f}")   # sqrt(9 + 16 + 1) = 5.099
print(f"Linf Norm: {norm_inf:.4f}") # max(3, 4, 1) = 4
Plaintext
Vector x: tensor([ 3., -4.,  1.])
L1 Norm: 8.0000
L2 Norm: 5.0990
Linf Norm: 4.0000

2. 行列はベクトルをどれだけ拡大するか:行列のノルム

ベクトルのノルムが定義できると、次に行列 \(A\) のノルムを考えることができます。行列のノルムにはいくつか定義がありますが、数値計算で最も重要なのは「従属ノルム(Induced Norm)」です。

行列 \(A\) をベクトル \(\mathbf{x}\) に作用させたとき、出力されるベクトル \(A\mathbf{x}\) のノルムは元の \(\mathbf{x}\) のノルムより大きくなったり小さくなったりします。行列のノルムとは、直感的に言えば「その行列がベクトルを最大で何倍に拡大するか」という「最大拡大率」を意味します。

$$|A| = \max_{\mathbf{x} \neq \mathbf{0}} \frac{|A\mathbf{x}|}{|\mathbf{x}|}$$

特に L∞ ノルムに基づく行列ノルムは、各行の絶対値の和を計算し、その中の最大値を取ることで簡単に求められます。これを「行和ノルム」と呼びます。

PyTorchによる行列ノルムの実装

Python
# サンプル行列 A の作成
A = torch.tensor([[1.0, 2.0],
                  [3.0, -4.0]])

# 行列ノルムの計算
# ord=inf は「行和の最大値」を計算する
matrix_norm_inf = torch.linalg.norm(A, ord=float('inf'))

print(f"Matrix A:\n{A}")
print(f"Matrix Inf-Norm: {matrix_norm_inf:.4f}")
# 行1の和: |1| + |2| = 3
# 行2の和: |3| + |-4| = 7
# 最大値は 7
Plaintext
Matrix A:
tensor([[ 1.,  2.],
        [ 3., -4.]])
Matrix Inf-Norm: 7.0000

3. 線形変換における誤差伝播のメカニズム

なぜノルムが必要なのか。それは連立一次方程式 \(A\mathbf{x} = \mathbf{b}\) を解く際、右辺のデータ \(\mathbf{b}\) に含まれる微小な誤差 \(\Delta \mathbf{b}\) が、解 \(\mathbf{x}\) にどれだけの影響を及ぼすかを知るためです。

数学的な解析(詳細は割愛しますが、ノルムの定義から導かれます)によると、相対誤差の関係は以下の不等式で表されます。

$$\frac{|\Delta \mathbf{x}|}{|\mathbf{x}|} \le (|A| \cdot |A^{-1}|) \frac{|\Delta \mathbf{b}|}{|\mathbf{b}|}$$

この式の右辺にある \((|A| \cdot |A^{-1}|)\) という値こそが、誤差が最大で何倍に拡大されるかを示す係数となります。

4. 行列の「健康診断」:条件数(Condition Number)

前節の係数 \(\kappa(A) = |A| \cdot |A^{-1}|\) は「条件数(Condition Number)」と呼ばれます。これは行列の数値的な「良さ」を表す最も重要な指標です。

  • 条件数が 1 に近い(良条件): 入力の微小な誤差は、出力でも微小なまま。計算は非常に安定しています。
  • 条件数が 非常に大きい(悪条件): 入力のわずかな誤差(あるいは浮動小数点の丸め誤差)が、解を数万倍、数億倍に増幅させてしまう可能性があります。

例えば、条件数が \(10^6\) の行列を扱う場合、入力データの精度が 7 桁あっても、解の精度は 1 桁(\(7 – 6 = 1\))しか残らない可能性がある、という非常に危険な状態を意味します。

条件数が無限大になるケース、それは行列が正則でない(逆行列を持たない)「特異行列」の場合です。つまり、条件数とは「その行列がどれくらい特異行列に近いか」という「危険度」を示しているとも言えます。

5. PyTorchによる実践:数値的な不安定性の観測

実際に、条件数が大きい行列がいかに「危険」であるかを、PyTorchを使って実験してみましょう。

ここでは、数値計算の教科書で悪名高い「ヒルベルト行列」のような、非常に条件数の悪い行列を模した例を作成します。

Python
import torch

def experiment_condition_number():
    # 非常に条件数が悪い(悪条件な)行列 A を作成
    # 2つの行がほとんど同じ方向を向いている
    A = torch.tensor([[1.000, 1.000],
                      [1.000, 1.001]])

    # 行列の条件数を計算
    cond_a = torch.linalg.cond(A, p=float('inf'))
    print(f"Matrix A:\n{A}")
    print(f"Condition Number of A: {cond_a:.2f}\n")

    # 真の解 x_true に対する右辺 b を計算
    x_true = torch.tensor([1.0, 1.0])
    b = torch.matmul(A, x_true)
    print(f"Original b: {b}")

    # 右辺 b に 0.001 だけの微小な誤差を加えてみる
    b_noisy = b + torch.tensor([0.000, 0.001])
    print(f"Noisy b:    {b_noisy}")

    # それぞれの方程式を解く (Ax = b)
    # torch.linalg.solve を使用
    x_calc = torch.linalg.solve(A, b)
    x_noisy_calc = torch.linalg.solve(A, b_noisy)

    print(f"\nResult with original b: {x_calc}")
    print(f"Result with noisy b:    {x_noisy_calc}")

    # 誤差の拡大率を確認
    relative_error_x = torch.linalg.norm(x_noisy_calc - x_calc, ord=float('inf')) / torch.linalg.norm(x_calc, ord=float('inf'))
    relative_error_b = torch.linalg.norm(b_noisy - b, ord=float('inf')) / torch.linalg.norm(b, ord=float('inf'))

    print(f"\nRelative error in b: {relative_error_b:.6f}")
    print(f"Relative error in x: {relative_error_x:.6f}")
    print(f"Error magnification: {relative_error_x / relative_error_b:.2f} times")

experiment_condition_number()
Plaintext
Matrix A:
tensor([[1.0000, 1.0000],
        [1.0000, 1.0010]])
Condition Number of A: 4003.81

Original b: tensor([2.0000, 2.0010])
Noisy b:    tensor([2.0000, 2.0020])

Result with original b: tensor([1.0001, 0.9999])
Result with noisy b:    tensor([2.3842e-04, 1.9998e+00])

Relative error in b: 0.000500
Relative error in x: 0.999762
Error magnification: 2000.67 times

結果の解説

このコードを実行すると驚くべき結果が得られます。
右辺ベクトル \(b\) に加えた誤差はわずか 0.05% 程度(\(b_2\) の 1.001 が 1.002 になっただけ)ですが、計算された解 \(x\) は [1.0, 1.0] から大きく外れ、全く別の値になってしまいます。

条件数がおよそ 2000 であるため、入力の誤差が数千倍に増幅されたのです。これが「悪条件」な行列の恐ろしさです。ディープラーニングの訓練が不安定になったり、勾配が爆発・消失したりする背景にも、こうした行列の条件数の悪化が潜んでいることが多々あります。

6. 実践的なアドバイス:条件数を意識した設計

実務で数値的な不安定さや計算結果の信頼性に疑問を感じたときは、以下のチェックリストを参考にしてください。それぞれの対策が「なぜ有効なのか」「どんな場面で重要か」も補足します。

  1. データのスケールを揃える(標準化・正規化)
    特徴量ごとに単位やスケール(値の大きさ)が大きく異なると、行列の条件数が急激に悪化しやすくなります。例えば、ある特徴量が0.001〜0.01、別の特徴量が1000〜10000のように桁違いだと、行列の計算で丸め誤差や不安定性が生じやすくなります。
    そのため、標準化(平均0・分散1に揃える)や正規化(最大値1に揃える)を行うことで、条件数を大幅に改善し、計算の安定性を高めることができます。これは機械学習の前処理で「スケーリング」が必須とされる大きな理由の一つです。
  2. 正則化(Regularization)の導入
    行列 \(A^T A\) に小さな値 \(\lambda I\)(単位行列にλ倍したもの)を加える「リッジ回帰(L2正則化)」は、数学的に行列の条件数を強制的に改善し、解の安定性を高める効果があります。
    これは、もともと特異(逆行列が存在しない、または条件数が極端に大きい)な行列に対しても、λを加えることで「より正則(invertible)」な行列に近づけることができるためです。
    実際のデータ分析や機械学習では、過学習の抑制だけでなく、数値計算の安定化という観点でも正則化は非常に重要です。
  3. 倍精度(float64)の検討
    通常の計算ではtorch.float32(単精度浮動小数点)が使われますが、どうしても高い精度が必要な場合や、条件数が大きい行列を扱う場合はtorch.float64(倍精度)を使うことで、丸め誤差の影響を小さくできます。
    ただし、倍精度にしても根本的な不安定性(条件数の悪化)は解決できないことが多く、本質的な解決策は「データのスケール調整」や「正則化」などの前処理・アルゴリズム改善にあることが多いです。
    それでも、科学技術計算や金融計算など「桁落ち」が致命的な分野では、倍精度の利用が推奨されます。

まとめ

第5回では、数値計算の品質を支える「ノルム」と「条件数」について学びました。

  • ノルムは、ベクトルや行列の「大きさ」や「拡大率」を測るための不可欠な道具である。
  • 誤差伝播は行列の性質に依存し、入力の小さな揺らぎが大きな破壊を招くことがある。
  • 条件数は、行列がどれくらい「健康的」かを示すバロメーターであり、これを知ることで計算結果の信頼性を判断できる。

「計算が回っているから正しい」と過信せず、その背後にある数値的な安定性に目を向けることが、プロフェッショナルなエンジニアへの第一歩です。

次回からは、この「正しさ」を意識しながら、実際に連立一次方程式を解く具体的なアルゴリズムの世界に入ります。ガウス消去法やピボット選択といった、行列計算の真髄に迫ります。お楽しみに。

最後まで読んでいただきありがとうございました。

連載記事、各回へのリンク

Pythonで学ぶ数値計算のアルゴリズムと実装 第1回:多項式計算とテイラー展開 | アマチュア無線局JS2IIU
https://js2iiu.com/2025/12/31/python_numerical_01/

Pythonで学ぶ数値計算のアルゴリズムと実装 第2回:多項式補間とチェビシェフ補間の戦略 | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/01/python_numerical_02/

Pythonで学ぶ数値計算のアルゴリズムと実装 第3回:積分の基礎:中点則、台形則からシンプソン則まで | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/01/python_numerical_03/

Pythonで学ぶ数値計算のアルゴリズムと実装 第4回:高度な数値積分:二重指数型(DE)公式と広義積分 | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/01/python_numerical_04/

Pythonで学ぶ数値計算のアルゴリズムと実装 第5回:計算の「正しさ」を評価する:ノルムと条件数 | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/01/python_numeric_05/

Pythonで学ぶ数値計算のアルゴリズムと実装 第6回:連立一次方程式の直截解法:ガウス消去法とピボット選択 | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/01/python_numerical_06/

Pythonで学ぶ数値計算のアルゴリズムと実装 第8回:スパース性を活かす:帯行列の高速計算アルゴリズム | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/02/python_numerical_08/

Pythonで学ぶ数値計算のアルゴリズムと実装 第9回:最小二乗法とQR分解:ハウスホルダー変換による安定化 | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/02/python_numerial_09/

Pythonで学ぶ数値計算のアルゴリズムと実装 第10回:非線形方程式の探索:2分法からニュートン法まで | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/03/python_numerical_10/

Pythonで学ぶ数値計算のアルゴリズムと実装 第11回:行列の本質を捉える:固有値問題の数値解法 | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/03/python_numerical_11/

Pythonで学ぶ数値計算のアルゴリズムと実装 第12回:現象の変化を追う:ルンゲ・クッタ法による常微分方程式の解法 | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/03/python_numerical_12/

Pythonで学ぶ数値計算のアルゴリズムと実装 第13回:空間の熱と振動を解く:偏微分方程式の差分近似と反復解法 | アマチュア無線局JS2IIU
https://js2iiu.com/2026/01/03/python_numerical_13/

コメント

タイトルとURLをコピーしました