検算と検証:誤差の次数・残差・保存量による検査と、数値実験と証明の区別
対象読者: 学部の微積分・線形代数と、精度の選び方(誤差予算・条件数)の章、数値積分 II(Richardson 外挿)を学んだ人。区間演算の経験は仮定しません。
前提となる記事:精度の選び方:倍精度・任意精度・球算術と誤差予算、数値積分 II:Euler–Maclaurin 展開・周期関数の台形則・Richardson 外挿・Romberg 法
0. この記事の要点
Section titled “0. この記事の要点”プログラムが出した数値が正しいかどうかは、答えを知らない問題ほど確かめにくいものです。この記事では、答えを知らないまま計算結果を疑うための手がかりを順に見ます。刻みを半分にしたときの誤差の減り方、方程式に代入し直したときのずれ、変わらないはずの量の変化などです。読み終えると、どの検査がどんな誤りを見つけ、どんな誤りを見逃すかを説明でき、「数値で確かめた」と「証明した」を書き分けられるようになります。
- 誤差が ()なら、観測次数 は に収束します。真値が分からなくても、3 つの刻みの値の差の比から同じ が得られます。実装の誤りは、多くの場合、観測次数の低下として現れます。
- 残差が小さいことは、誤差が小さいことを意味しません。 相対誤差は条件数 × 相対残差まで大きくなりえて、等号が成り立つ例があります。
- 保存量のずれは、 次の方法で までに 以下です。対称性や恒等式による検査は、その対称性を保つ誤りを原理的に検出できません。
- 2 つの検算が同じ誤りを見逃す確率は、独立なら積ですが、共通の原因があると積よりずっと大きくなります。「別手法」は、導出からして別でなければなりません。
- 無作為な 個の入力で反例が出なかったとき言えるのは「反例の割合は、信頼水準 で 以下」までで、命題が真であることではありません。
- 浮動小数の計算は証拠であって証明ではありません。倍精度で正に見えた量が、実は のことがあります。数値を証明に使うときは、包含の保証がある計算(区間演算・球算術)に置き換えます。
数値計算のプログラムは、誤っていても数を返します。構文の誤りは実行前に、型の誤りは実行時に見つかりますが、重みを 1 つ取り違えた求積公式も、添字を入れ替えた行列積も、何事もなかったように「それらしい数」を出力します。出力がそれらしいかどうかを眺めて判断するのは、答えを知っている問題でしか通用しません。そして、答えを知っている問題は、わざわざ計算する必要のない問題です。
そこで、答えを知らない問題の計算結果を、答えを知らないまま確かめる方法が要ります。原理は単純で、正しい計算ならば必ず成り立つ関係を探し、それが成り立っているかを調べます。刻みを半分にすれば誤差は 分の 1 になるはずだ、方程式に代入すれば残差は小さいはずだ、エネルギーは保存されるはずだ、別の方法で計算しても同じ値になるはずだ、という具合です。
ただし、これらの検査はどれも必要条件の検査です。合格しても正しいとは限りません。この章では、それぞれの検査について「何が保証され、何が保証されないか」を命題として述べます。どの検査がどの種類の誤りを捕まえ、どの種類を見逃すかを知っていれば、検査を組み合わせて穴をふさぐことができます。最後に、こうして集めた数値的な証拠と、数学の証明との間にある段差をはっきりさせ、その段差を越える方法(精度保証つき計算)への入口を示します。このテーマの最後の章として、再現性と精度の選び方の章の道具もここで使います。
2. 何を確かめるのか
Section titled “2. 何を確かめるのか”数値計算の正しさには 2 つの別の問いがある。
- 検証(verification): プログラムは、意図した数式(方程式、算法、公式)を正しく計算しているか。
- 妥当性確認(validation): その数式は、調べたい対象(物理現象、データ)を正しく表しているか。
検証は数学とプログラムの間の問いで、対象についての知識がなくても行える。妥当性確認は数式と対象の間の問いで、実験や観測との比較を要する。
この章が扱うのは検証です。検証が済んでいない計算で実験との一致・不一致を論じても、モデルの誤りと実装の誤りを区別できません。検証のための検査(検算)を、使う情報の種類で分けると次の表になります。
| 検算の種類 | 調べること | 捕まえやすい誤り | 見逃しやすい誤り |
|---|---|---|---|
| 閉形式・既知値 | 答えの分かる問題での一致 | 公式・定数の誤り | その問題で効かない項の誤り |
| 別手法 | 独立な算法との一致 | 算法ごとの実装の誤り | 共通の入力・共通の公式の誤り |
| 別精度 | 作業精度を上げた結果との一致 | 丸め誤差・桁落ち | 打ち切り誤差、公式の誤り |
| 誤差の次数 | 刻みを変えたときの収束の速さ | 重み・添字・境界の誤り | 次数を保つ誤り |
| 残差 | 解を方程式に戻したときのずれ | 解く手続きの誤り | 方程式そのものの誤り、悪条件 |
| 極限・対称性・保存量・恒等式 | 成り立つはずの関係 | その関係を破る誤り | その関係を保つ誤り |
右端の列がこの章の主題です。どの検査にも見逃す誤りがあり、それは検査の原理から決まっています。
命題 についての数値計算の結果を、次の 3 つに区別する。
- 数値的な証拠: 有限個の入力について、浮動小数の計算で と矛盾しない結果を得たこと。 が真であることを含意しない。
- 反例: が偽であることを示す具体的な入力。ただし浮動小数の計算で見つけた「反例」は、丸め誤差の影響を除く検査(厳密な計算または包含の保証がある計算)を経て初めて反例になる。
- 証明: すべての入力について が成り立つことの論証。計算機を使う場合は、計算の結果が真の値を含むことが保証された計算(精度保証つき計算。区間演算・球算術・厳密な有理数演算)と、有限個の場合への帰着の論証からなる。
3. 誤差の次数を観測する
Section titled “3. 誤差の次数を観測する”3.1. 観測次数
Section titled “3.1. 観測次数”刻み をもつ近似(数値積分、差分、常微分方程式の数値解)の誤差は、多くの場合 のべきで減ります。そのべきを数値実験から読み取ります。
証明で使う対数の評価を先に示します。
証明(補題 3.2)
なら両辺は です。 のとき、関数 は で微分可能で導関数は なので、 と を端点とする閉区間に平均値の定理(定理 3.3[平均値の定理とテイラーの定理])を使うと、 と の間の があって です。 なので で、 です。
、 とし、ある について
と書けるとする。 とおく。 ならば、 と は でなく同符号で、
が成り立つ。とくに、 と があって、すべての で ならば、 を満たす について
で、 のとき である。
証明(定理 3.3)
、 とおくと、、 で、
です。 なら 、 なので、 と はどちらも でなく、 と同じ符号です。比をとると で、
です。補題 3.2 から で、最初の評価を得ます。
後半は、 と ( から )から で、仮定 のもとで です。前半に代入して評価を得ます。 なので右辺は で に収束します。
定理の前半は、 の中身を問わない形で書いてあります。 に打ち切り誤差の高次の項だけでなく、丸め誤差も含められるようにするためです(§3.3)。
観測次数による検査の手順は次のとおりです。理論から期待される次数 を先に書き出す。刻みを半分ずつにして 5〜6 段階で誤差を測る。観測次数が に近づけば合格、別の値に落ち着くか、どこにも落ち着かなければ、実装か仮定のどちらかが誤っています。
例 3.4(正しい Simpson 則、重みを 1 つ誤った Simpson 則、滑らかでない被積分関数)
複合 Simpson 則の誤差は、被積分関数が 級なら です(系 4.3[数値積分 I] の 2)。正しい実装と、重みを設定するスライスの終わりを 1 つ取り違えて終点の重みを にしてしまった実装を、 で比べます。あわせて、正しい実装で を計算します。
# Python 3.12.14 / NumPy 2.5.2import mathimport numpy as np
def simpson(f, a, b, N, buggy=False): """複合 Simpson 則(N は偶数)。重みは 1, 4, 2, 4, ..., 2, 4, 1""" x = np.linspace(a, b, N + 1) w = np.ones(N + 1) w[1:-1:2] = 4 if buggy: w[2::2] = 2 # 誤り: 範囲の終わりを 1 つ取り違え、終点の重みまで 2 にする else: w[2:-1:2] = 2 return (b - a) / N / 3 * np.dot(w, f(x))
cases = [("正しい実装, e^x", np.exp, math.e - 1, False), ("誤った実装, e^x", np.exp, math.e - 1, True), ("正しい実装, √x", np.sqrt, 2 / 3, False)]Ns = [8, 16, 32, 64, 128, 256]for name, f, exact, buggy in cases: errs = [abs(simpson(f, 0.0, 1.0, N, buggy) - exact) for N in Ns] orders = [math.log2(errs[i] / errs[i + 1]) for i in range(len(Ns) - 1)] print(name) print(" N " + "".join(f"{N:>10d}" for N in Ns)) print(" 誤差 " + "".join(f"{e:10.2e}" for e in errs)) print(" 観測次数 " + "".join(f"{o:10.3f}" for o in orders))正しい実装, e^x N 8 16 32 64 128 256 誤差 2.33e-06 1.46e-07 9.10e-09 5.69e-10 3.56e-11 2.22e-12 観測次数 3.998 3.999 4.000 4.000 4.000誤った実装, e^x N 8 16 32 64 128 256 誤差 1.13e-01 5.66e-02 2.83e-02 1.42e-02 7.08e-03 3.54e-03 観測次数 1.000 1.000 1.000 1.000 1.000正しい実装, √x N 8 16 32 64 128 256 誤差 3.59e-03 1.27e-03 4.48e-04 1.59e-04 5.61e-05 1.98e-05 観測次数 1.500 1.500 1.500 1.500 1.500検算. (a) 正しい実装の誤差は、評価 の内側にあります。 で 、実際は です。観測次数は に収束しています(定理 3.3)。(b) 誤った実装は、終点の重みが 多いので、正しい値に が加わります。 で で、表の値と一致します。誤差が に比例するので観測次数は です。誤った実装も を増やせば正しい値に収束することに注意してください。「刻みを細かくしたら値が落ち着いた」ことは、実装が正しいことの証拠になりません。次数まで見て初めて誤りが分かります。(c) の観測次数 は実装の誤りではありません。 は で微分可能でなく、 級という仮定が破れています。次数が期待と違うとき、疑う先は実装と仮定の 2 つあります。
3.2. 真値が分からないとき
Section titled “3.2. 真値が分からないとき”実際の問題では真値 が分からないので、誤差 そのものは測れません。それでも、3 つの刻みの値があれば次数は観測できます。
次の定理のねらいは、真値 が出てこない形で次数 を取り出すことです。隣り合う刻みの値の差 では が打ち消し合うので、差の比 が に近づきます。長い条件式は、高次の項が先頭の項に比べて十分小さい、という仮定を書き下したものです。
、、 とし、 が について
を満たすとする。 とおく。
- ならば、 と は でなく同符号で、
- 最も細かい刻みの値の誤差は、差から の精度で推定できる。
証明(定理 3.5)
-
について
です。 なので で、 です。したがって 、 で、定理 3.3 の ( を 、 を 、 を としたもの)は
を満たします。仮定からこれは 以下なので、同じ定理の前半から、 と は でなく同符号で、 です。 の評価を代入して主張を得ます。
-
仮定は、刻み と について Richardson 外挿の定理(定理 4.2[数値積分 II])の仮定(定数 、剰余の定数 )を満たしています。2 つの刻みによる誤差の推定(命題 6.1[数値積分 II])で を に置き換えると、主張の不等式そのものです。
1 で求めた次数の推定値を 2 の式の に使えば、真値も理論上の次数も知らずに、誤差の推定値まで得られます。推定した次数が理論と合っていれば実装の検証になり、誤差の推定値は結果に付ける誤差の見積もりになります。
例 3.6(3 つの刻みからの次数と誤差の推定、丸めによる次数の崩れ)
を台形則と Simpson 則で計算し、3 つの刻みの値だけから次数と誤差を推定します。真値 は推定には使わず、推定の良し悪しを確かめるためだけに使います。後半では、中心差分で同じ推定を行い、刻みを小さくしすぎると何が起こるかを見ます。
# Python 3.12.14 / NumPy 2.5.2import mathimport numpy as np
f = lambda x: np.exp(-x * x)exact = math.sqrt(math.pi) / 2 * math.erf(1.0) # 比較用の真値(推定には使わない)
def trapezoid(N): x = np.linspace(0.0, 1.0, N + 1) y = f(x) return (y.sum() - (y[0] + y[-1]) / 2) / N
def simpson(N): x = np.linspace(0.0, 1.0, N + 1) w = np.ones(N + 1); w[1:-1:2] = 4; w[2:-1:2] = 2 return np.dot(w, f(x)) / (3 * N)
for name, rule in (("台形則", trapezoid), ("Simpson 則", simpson)): print(name) print(" N(h/4) 推定次数 誤差の推定 真の誤差 比") for N in (8, 16, 32, 64, 128): A1, A2, A4 = rule(N), rule(2 * N), rule(4 * N) p = math.log2((A1 - A2) / (A2 - A4)) est = (A4 - A2) / (2 ** p - 1) # A - A(h/4) の推定 true = exact - A4 print(f" {4 * N:6d} {p:8.4f} {est:11.3e} {true:11.3e} {est / true:6.3f}")
print("中心差分 (f = exp, x = 1)")print(" h=2^-k D(h) の誤差 3 点からの推定次数")D = lambda h: (math.exp(1 + h) - math.exp(1 - h)) / (2 * h)for k in range(2, 26, 2): h = 2.0 ** -k num, den = D(h) - D(h / 2), D(h / 2) - D(h / 4) p = f"{math.log2(abs(num / den)):8.3f}" if num != 0 and den != 0 else " (差が 0)" print(f" {k:4d} {abs(D(h) - math.e):10.2e} {p}")台形則 N(h/4) 推定次数 誤差の推定 真の誤差 比 32 2.0007 5.985e-05 5.988e-05 0.999 64 2.0002 1.497e-05 1.497e-05 1.000 128 2.0000 3.742e-06 3.742e-06 1.000 256 2.0000 9.356e-07 9.356e-07 1.000 512 2.0000 2.339e-07 2.339e-07 1.000Simpson 則 N(h/4) 推定次数 誤差の推定 真の誤差 比 32 3.9952 -7.816e-09 -7.795e-09 1.003 64 3.9989 -4.875e-10 -4.872e-10 1.001 128 3.9997 -3.046e-11 -3.045e-11 1.000 256 3.9999 -1.904e-12 -1.903e-12 1.000 512 4.0001 -1.190e-13 -1.191e-13 0.999中心差分 (f = exp, x = 1) h=2^-k D(h) の誤差 3 点からの推定次数 2 2.84e-02 2.004 4 1.77e-03 2.000 6 1.11e-04 2.000 8 6.91e-06 2.000 10 4.32e-07 2.000 12 2.70e-08 2.000 14 1.69e-09 2.021 16 1.06e-10 1.585 18 9.94e-12 (差が 0) 20 1.26e-10 (差が 0) 22 3.39e-10 (差が 0) 24 5.92e-10 (差が 0)検算. (a) 推定次数は台形則で 、Simpson 則で に収束し、理論と合います(定理 3.5 の 1)。(b) 誤差の推定値は、符号も含めて真の誤差と 3 桁一致します(同じ定理の 2)。真値を使わずに「この値の誤差は 程度」と言えたことになります。(c) 中心差分では、 で推定次数が ですが、 で崩れ、 では隣り合う刻みの値が完全に一致して比が定義できません。誤差も の を底にして、それより細かい刻みでは増えています。次の小節で説明します。
3.3. 丸めが次数を壊す領域
Section titled “3.3. 丸めが次数を壊す領域”証明(系 3.7)
を改めて とみなして 定理 3.3 の前半を使います。、 なので、同じ定理の は 以下です。
の第 1 項は を小さくすると減り、第 2 項は増えます。観測次数が信頼できるのは、両方が小さい中間の領域だけです。 が大きすぎると高次の項が邪魔をし(漸近領域、つまり先頭の項だけで誤差が決まる範囲に入っていない)、小さすぎると丸めの雑音 が打ち切り誤差 を上回ります。雑音の大きさは、単位丸め に計算の条件数を掛けた程度で、これが誤差の下限になります。
中心差分はその典型です。打ち切り誤差は 、丸めによる誤差は の程度で(命題 7.1[数値積分 II])、 自体が で増えます。上の例では 、 なので、 で丸めの項が の程度になり、打ち切り誤差 を上回ります。表で次数が崩れ始める位置と合っています。次数を観測する実験は、丸めが見え始める手前で止めるか、作業精度を上げて行います。
中心差分で を小さくしていくと、誤差と観測次数がどう動くかを見てください。

3.4. 製造解の方法
Section titled “3.4. 製造解の方法”微分方程式を解くコードでは、厳密解の分かる問題が限られます。そこで順序を逆にします。
方程式 を解くコードを検証するために、解 を先に自由に選び、 を手計算(または数式処理)で求めて右辺とし、境界条件も から作る。コードに を与えて得た数値解と の差を、刻みを変えて測る。この を製造解という。
製造解は物理的に意味のある解である必要はありません。むしろ、方程式のすべての項が効き、どの導関数も にならない、対称性のない関数を選びます。多項式を選ぶと差分が厳密になって誤差が になり、次数が観測できないことがあります。
例 3.9(製造解で Poisson 方程式の差分コードを検証する)
()、 を 3 点の中心差分で解くコードを検証します。製造解を とすると、境界条件を満たし、 から です。期待される次数は です。比較のため、行列の対角成分を ではなく とした実装も試します。
# Python 3.12.14 / NumPy 2.5.2import mathimport numpy as np
# 製造解: u(x) = e^x sin(πx) を先に決め、-u'' = f となる f を手で作るu = lambda x: np.exp(x) * np.sin(np.pi * x)f = lambda x: np.exp(x) * ((np.pi**2 - 1) * np.sin(np.pi * x) - 2 * np.pi * np.cos(np.pi * x))
def solve_poisson(N, coef=2.0): """-u'' = f, u(0) = u(1) = 0 を 3 点差分で解く。coef は対角成分(正しくは 2)""" h = 1.0 / N x = np.linspace(0.0, 1.0, N + 1)[1:-1] A = (np.diag(np.full(N - 1, coef)) - np.diag(np.ones(N - 2), 1) - np.diag(np.ones(N - 2), -1)) / h**2 return x, np.linalg.solve(A, f(x))
for name, coef in (("正しい差分", 2.0), ("対角成分を 2 + h² と誤った差分", None)): errs = [] for N in (16, 32, 64, 128, 256): x, v = solve_poisson(N, 2.0 if coef else 2.0 + (1.0 / N) ** 2) errs.append(float(np.max(np.abs(v - u(x))))) print(name) print(" 最大誤差 " + "".join(f"{e:10.2e}" for e in errs)) print(" 観測次数 " + "".join(f"{math.log2(errs[i] / errs[i + 1]):10.3f}" for i in range(4)))正しい差分 最大誤差 3.85e-03 9.63e-04 2.41e-04 6.02e-05 1.50e-05 観測次数 2.000 2.000 2.000 2.000対角成分を 2 + h² と誤った差分 最大誤差 1.52e-01 1.54e-01 1.54e-01 1.54e-01 1.55e-01 観測次数 -0.022 -0.004 -0.001 -0.000検算. (a) 正しい差分は観測次数 です。(b) 誤った差分の誤差は刻みを細かくしても減りません(観測次数 )。対角成分に を足して で割ると、方程式に の項が加わるので、このコードは別の方程式 を 2 次の精度で解いています。数値解は滑らかで、境界条件も満たし、見た目には何の異常もありません。製造解との比較が、方程式そのものの取り違えを検出しています。(c) 製造解の導関数は、 を数値微分した値との比較でも確かめられます。 の手計算を誤ると、正しいコードが不合格になるからです。
4. 残差は誤差ではない
Section titled “4. 残差は誤差ではない”方程式を解いた結果は、解を方程式に代入し直せば確かめられます。代入して残る量が残差です。残差は真の解を知らなくても計算できる便利な量ですが、誤差そのものではありません。
を 次の正則な実行列、、 とし、 の残差を とする。ベクトルのノルムを 1 つ固定し、行列にはその誘導ノルムを使い、 とする。
- 。
- この不等式で等号が成り立つ例がある。2 ノルムで、、、、 とすると、相対残差は 、相対誤差は 、 である。
証明(命題 4.1)
-
なので です。誘導ノルムの定義(定義 4.1[行列のノルムと線形方程式の条件数])から、任意の行列 とベクトル について なので、 です。同じく で、 から なので です。2 つを掛け合わせると主張の不等式です。
-
で、 です。、 です。対角行列 の 2 ノルムは です( で、 が最大の成分に対応する単位ベクトルで等号)。よって 、、 で、 は相対誤差に一致します。
なら、相対残差が でも解は ずれています。残差が教えてくれるのは「 は、少しだけ違う問題の厳密解である」ということ、つまり後退誤差(問題のほうをどれだけ動かせば、この が厳密解になるか)が小さいことです。これに対し前進誤差は、答えそのもののずれ のことです(残差と後退誤差の関係は Rigal–Gaches の公式 定理 7.2[行列のノルムと線形方程式の条件数])。それを前進誤差に直すには条件数が要ります(系 7.3[行列のノルムと線形方程式の条件数]、非線形の問題では 定理 4.3[条件数と安定性])。残差による検算は、条件数の見積もりと組にして初めて誤差の主張になります。
残差がそのまま誤差の上界になる場合もあります。実対称行列の固有値では、単位ベクトル と Rayleigh 商の残差 について、 以内に必ず固有値があります(系 7.2[固有値の計算 I])。条件数が現れないのは、対称行列の固有値が摂動に対してつねに良条件だからです。対称でない行列ではこの評価は成り立ちません。
例 4.2(固有値計算の検算を重ねる)
乱数で作った 次の実対称行列の固有値を計算し、5 通りの検算をします。
# Python 3.12.14 / NumPy 2.5.2import numpy as np
rng = np.random.default_rng(20261008)n = 8B = rng.standard_normal((n, n))A = (B + B.T) / 2 # 実対称行列lam, V = np.linalg.eigh(A) # 対称行列用の算法lam2 = np.sort(np.linalg.eigvals(A).real) # 別の算法(一般行列用)scale = np.abs(lam).max()print(f"トレース |Σλ - tr A| / max|λ| = {abs(lam.sum() - np.trace(A)) / scale:.1e}")print(f"行列式 |Πλ - det A| / |det A| = {abs(lam.prod() - np.linalg.det(A)) / abs(np.linalg.det(A)):.1e}")print(f"残差 max ||A v - λ v|| / max|λ| = {np.linalg.norm(A @ V - V * lam, axis=0).max() / scale:.1e}")print(f"直交性 ||V^T V - I||_F = {np.linalg.norm(V.T @ V - np.eye(n)):.1e}")print(f"別の算法 max |λ - λ'| / max|λ| = {np.abs(lam - lam2).max() / scale:.1e}")トレース |Σλ - tr A| / max|λ| = 3.8e-16行列式 |Πλ - det A| / |det A| = 1.1e-15残差 max ||A v - λ v|| / max|λ| = 6.6e-16直交性 ||V^T V - I||_F = 2.2e-15別の算法 max |λ - λ'| / max|λ| = 1.9e-155 つの検算は、単位丸め の数倍から 20 倍程度の範囲で合格しています。それぞれが見ているものは違います。トレースは固有値の和、行列式は積だけを見るので、2 つの固有値が逆向きにずれる誤りを見逃します。残差は固有値と固有ベクトルの組を 1 つずつ見ますが、系 7.2[固有値の計算 I] が保証するのは「近くに固有値がある」ことで、8 個の固有値を漏れなく求めたことではありません。同じ固有値を 2 回数えていないことは直交性が補います。別の算法との一致は最も強い検査に見えますが、2 つの算法は同じライブラリの同じ入力 A を使っているので、A を作る段階の誤りには無力です(§6)。
5. 保存量・対称性・恒等式
Section titled “5. 保存量・対称性・恒等式”5.1. 保存量のずれ
Section titled “5.1. 保存量のずれ”微分方程式の厳密解が保つ量(エネルギー、運動量、確率の総和)は、数値解がどれだけ保っているかを調べることで検算に使えます。数値解は一般に保存量を厳密には保ちませんが、ずれの大きさには方法の次数で決まる上界があります。
とし、各 について写像 が与えられているとする( は微分方程式の厳密解を時間 だけ進める写像、 は数値解法の 1 ステップ)。 は の部分集合 の上の実数値関数で、次を仮定する。
- (保存) ならば で 。
- (局所誤差が 次)定数 、 があって、、 について で 。
- (Lipschitz 連続)定数 があって、 について 。
で定まる数値解が を満たし、 とすると
である。
証明(命題 5.1)
とします。保存の仮定から なので、
です( なので Lipschitz 連続性が使えます)。 に三角不等式を使うと です。
証明は、数値解の誤差そのもの( と厳密解の差)の評価を経由していません。1 ステップのずれを足しただけです。そのため、この上界は長い時間 について線形にしか増えません。検算としての使い方は、ずれの大きさと次数を測り、上界より大きいか、次数が より低ければ実装の誤りを疑う、というものです。上界なので、次数が より高く出ることはあります。
例 5.2(調和振動子のエネルギーのずれ)
、(保存量 )を、初期値 から まで、 ステップで解きます。Euler 法(局所誤差 )、4 段 4 次の Runge–Kutta 法(局所誤差 )、厳密解の 1 ステップ(角 の回転)の 3 通りです。
# Python 3.12.14 / NumPy 2.5.2import mathimport numpy as np
# 調和振動子 q' = p, p' = -q。保存量 E = (q^2 + p^2)/2、初期値 (1, 0)、T = 8J = np.array([[0.0, 1.0], [-1.0, 0.0]])f = lambda y: J @ yE = lambda y: 0.5 * float(y @ y)
def euler(y, h): return y + h * f(y)
def rk4(y, h): k1 = f(y); k2 = f(y + h / 2 * k1); k3 = f(y + h / 2 * k2); k4 = f(y + h * k3) return y + h / 6 * (k1 + 2 * k2 + 2 * k3 + k4)
def rotation(y, h): # 厳密解の 1 ステップ(回転) c, s = math.cos(h), math.sin(h) return np.array([c * y[0] + s * y[1], -s * y[0] + c * y[1]])
def drift(step, n, T=8.0): y = np.array([1.0, 0.0]) for _ in range(n): y = step(y, T / n) return abs(E(y) - 0.5)
ns = [64, 128, 256, 512, 1024]for name, step in (("Euler 法", euler), ("RK4", rk4), ("厳密な回転", rotation)): d = [drift(step, n) for n in ns] orders = [" -"] + [f"{math.log2(d[i] / d[i + 1]):6.2f}" if d[i] > 0 and d[i + 1] > 0 else " -" for i in range(len(ns) - 1)] print(name) print(" n " + "".join(f"{n:>10d}" for n in ns)) print(" |ΔE| " + "".join(f"{v:10.2e}" for v in d)) print(" 観測次数" + "".join(f"{o:>10s}" for o in orders))print(f"n·u (n = 1024) = {1024 * 2.0**-53:.2e}")Euler 法 n 64 128 256 512 1024 |ΔE| 8.49e-01 3.24e-01 1.42e-01 6.66e-02 3.22e-02 観測次数 - 1.39 1.19 1.09 1.05RK4 n 64 128 256 512 1024 |ΔE| 1.69e-06 5.30e-08 1.66e-09 5.17e-11 1.62e-12 観測次数 - 5.00 5.00 5.00 5.00厳密な回転 n 64 128 256 512 1024 |ΔE| 2.66e-15 5.00e-15 9.55e-15 1.63e-14 5.60e-14 観測次数 - -0.91 -0.93 -0.77 -1.78n·u (n = 1024) = 1.14e-13検算. (a) Euler 法のずれの観測次数は に近づいています。命題 5.1 の と合います。この問題では 1 ステップで がちょうど 倍になるので、ずれは と閉じた形で書け、 で です(演習)。(b) RK4 の観測次数は で、命題の より高く出ています。命題は上界なので矛盾しません。この線形の問題では、RK4 の 1 ステップは複素数 ()を掛けることに当たり、 なので、1 ステップのずれが ではなく になるためです。 での値は で、表と一致します。(c) 厳密な回転のずれは刻みを細かくすると増え、大きさは の程度以下です。これは打ち切り誤差ではなく、1 ステップごとの丸め誤差が積もったものです。保存量を厳密に保つ方法のずれは、このように のべきではなく、ステップ数に比例する丸めの大きさで現れます。2 つを取り違えないようにします。
5.2. 対称性と恒等式による検査の限界
Section titled “5.2. 対称性と恒等式による検査の限界”行列式の乗法性、Parseval の等式、確率の和が 1 であること、回転しても変わらないはずの量、といった恒等式や対称性は、答えを知らなくても検査できる便利な性質です。その限界は、次の形で特徴づけられます。
を集合、 を から への写像の集合、 をベクトル空間とし、 がすべての 、 について を満たすとする( は について不変)。 の実装を ( は実装の誤り)とする。
- 検査「」が入力 で合格することは、 と同値である。したがって、 について不変な誤り は、検査の入力をどう選んでも検出されない。
- を写像とし、実装が (入力を で変換してから正しい計算をする、という誤り)であるとする。このとき、 を満たす入力 の上では で、正しい値とのどんな比較も合格する。
証明(命題 5.3)
-
です( の不変性)。左辺が であることと は同値です。 が不変なら、これはすべての で成り立ちます。
-
なら です。
1 は、不変性の検査が見ているのは「誤りのうち、対称性を破る成分」だけだと言っています。全体に定数倍が掛かる誤り、規格化の誤りは、たいていの対称性を保つので素通りします。恒等式でも同じです。行列式の乗法性 の検査は、 の代わりに や や定数 を返す実装も合格させます。どれも乗法的だからです。
2 は、検査に使う入力が対称すぎると誤りが隠れることを言っています。転置の取り違えは対称行列では見えず、添字の順序の誤りは正方行列や立方体の格子では見えず、符号の誤りは偶関数では見えません。検査の入力は、対称性のない一般的なものを選びます。
例 5.4(転置を取り違えた行列ベクトル積はどの検査で見つかるか)
行列ベクトル積 を、添字を取り違えて と計算している実装を、5 通りの検査にかけます。
# Python 3.12.14 / NumPy 2.5.2import numpy as np
def matvec_ref(A, x): # 定義どおりの二重ループ(別手法) n, m = A.shape return np.array([sum(A[i, j] * x[j] for j in range(m)) for i in range(n)])
def matvec_bug(A, x): # 誤り: 添字を取り違えて A^T x を計算している return np.einsum("ji,j->i", A, x)
def rel(a, b): # 相対的な食い違い return float(np.linalg.norm(a - b) / max(np.linalg.norm(a), np.linalg.norm(b)))
rng = np.random.default_rng(20261008)n = 6A = rng.standard_normal((n, n))S = (A + A.T) / 2 # 対称行列x, y = rng.standard_normal(n), rng.standard_normal(n)f = matvec_bug
tests = { "線形性 f(A,2x-3y) = 2f(A,x) - 3f(A,y)": rel(f(A, 2 * x - 3 * y), 2 * f(A, x) - 3 * f(A, y)), "随伴の恒等式 <f(A,x),y> = <x,f(A^T,y)>": abs(f(A, x) @ y - x @ f(A.T, y)) / abs(f(A, x) @ y), "別手法との比較(対称行列 S)": rel(f(S, x), matvec_ref(S, x)), "別手法との比較(一般の行列 A)": rel(f(A, x), matvec_ref(A, x)), "既知の値 (E_12 e_2 = e_1)": rel(f(np.outer(np.eye(n)[0], np.eye(n)[1]), np.eye(n)[1]), np.eye(n)[0]),}for name, d in tests.items(): print(f"{name:42s} 食い違い = {d:.2e} {'検出' if d > 1e-10 else '見逃し'}")線形性 f(A,2x-3y) = 2f(A,x) - 3f(A,y) 食い違い = 2.84e-16 見逃し随伴の恒等式 <f(A,x),y> = <x,f(A^T,y)> 食い違い = 2.43e-16 見逃し別手法との比較(対称行列 S) 食い違い = 3.46e-17 見逃し別手法との比較(一般の行列 A) 食い違い = 1.27e+00 検出既知の値 (E_12 e_2 = e_1) 食い違い = 1.00e+00 検出線形性と随伴の恒等式は、誤った実装でもすべての入力で厳密に成り立ちます。 も について線形で、 だからです。転置の取り違えは、この 2 つの恒等式を保つ誤りです(命題 5.3 の 1 に当たる状況)。対称行列での比較が合格するのは同じ命題の 2( が転置、)です。検出できたのは、対称でない入力での別手法との比較と、手で答えの分かる最小の入力だけでした。乱数で作った行列は確率 1 で対称でないので、検査の入力を乱数で作ることには、入力の対称性による見逃しを避ける効果があります。
6. 検算の独立性と、反例探索で言えること
Section titled “6. 検算の独立性と、反例探索で言えること”6.1. 2 つの検算は独立か
Section titled “6.1. 2 つの検算は独立か”検算を 2 つ重ねれば、両方が見逃す確率は小さくなるはずです。どれだけ小さくなるかは、2 つの検算の「見逃し方」が独立かどうかで決まります。
命題 6.1(共通の原因があると見逃しの確率は積にならない)✓
ある誤りを検算 ()が見逃すという事象を とし、 とおく。
- と が独立ならば 。
- 事象 (共通の原因。確率 、)が起こると両方の検算が必ず見逃し、 が起こらないときは、2 つの検算が条件付き確率 のもとで独立に確率 で見逃すとする。このとき である。
証明(命題 6.1)
-
事象の独立性の定義(定義 6.1[確率空間とコルモゴロフの公理])そのものです。
-
全確率の公式(定理 5.3[確率空間とコルモゴロフの公理] の 2)を分割 に使います。仮定から 、 なので で、第 2 項は非負なので 以上です。同じく です。積を展開すると
で、
です。括弧の中は です。
数値を入れてみます。それぞれ単独では しか見逃さない検算()が 2 つあるとき、独立なら両方が見逃す確率は です。しかし共通の原因が の確率で起こる()なら、 で、500 倍です。2 つ目の検算は、共通の原因に対しては何も足していません。
数値計算での共通の原因は、ありふれています。同じ(誤って導いた)公式を 2 通りに実装する。2 つの算法が同じライブラリの同じ関数を呼ぶ。同じ入力ファイル、同じ単位の取り違え、同じ仕様の誤解を共有する。同じ人が、同じ思い込みのまま 2 回書く。「別手法による検算」が意味をもつのは、公式の導出まで遡って別であるときです。差分法の結果をスペクトル法で、数値積分を級数展開で、数値解を保存則で確かめる、というように、誤りの入り方が違う方法を選びます。
6.2. 反例が見つからなかったとき
Section titled “6.2. 反例が見つからなかったとき”命題を多数の入力で試して反例が出なかった、という報告は数値実験の定番です。そこから言えることを正確に述べます。
入力の集合の上の確率分布を 1 つ固定し、命題の反例である入力の集合の確率を とする。この分布に従う独立な 個の標本 を調べる。
- 反例が 1 つも出ない確率は で、。
- とする。 ならば、反例が 1 つも出ない確率は より小さい。
- どの についても 。とくに である偽の命題()は、確率 以上で反例が出ないまま検査を通過する。
証明(定理 6.2)
-
反例の集合を とすると、反例が出ないことは で、独立性(定義 2.3[確率変数と期待値])から確率は です。 は 、()なので、 で広義単調増加で(系 3.4[平均値の定理とテイラーの定理] の 2)、すなわち です。両辺を 乗して主張を得ます。
-
1 から、確率は 以下で、 なら です。
-
についての帰納法です。 は等号です。 で成り立つとすると、両辺に を掛けて です。 なら です。
2 は次のように読みます。 個の標本で反例が出なかったとき、「」という主張は、信頼水準 で成り立ちます( がそれより大きければ、反例が出ないという観測は確率 未満でしか起こらないからです)。 なら で、「反例の割合は 以下」です(3/N 則)。 個で反例なしなら、信頼水準 で です。
言えるのはここまでです。3 のとおり、反例の割合が よりずっと小さい偽の命題は、ほぼ確実に検査を通過します。そして であっても、命題が真であるとは限りません。
例 6.3(無作為な探索が見逃す反例)
測度 0 の反例. 「すべての 次実正方行列は正則である」は偽ですが、成分を連続分布から独立に引いた行列が特異になる確率は です。特異行列の集合は多項式 の零点集合で、体積が だからです。 で、何個試しても反例は出ません。重根をもつ多項式、退化した固有値、分母が になるパラメータなど、数値計算で問題を起こす入力の多くはこの型です。
標本の分布に入らない反例. 「binary64 の数 について 」は、 の一様乱数で試す限り反例が出ません。反例は にあります( は表現できず、 に丸められます)。探索の範囲の外にある反例については、探索は何も言いません。
はるか遠くにある反例. 素数の個数 と対数積分 について、 は計算された範囲のすべての で成り立ちますが、Littlewood は 1914 年に、この不等号が無限回入れ替わることを証明しました。最初に入れ替わる は直接の計算が届かないほど大きいところにあります。計算できる範囲での一致が、命題の証拠としてどれほど弱いことがあるかを示す例です。
したがって、探索の報告には、標本の分布・範囲・個数・乱数種を必ず添えます。それらが「何を調べていないか」を読者に伝える情報だからです。
7. 数値実験を命題に昇格させる
Section titled “7. 数値実験を命題に昇格させる”数値実験で見つけた事実を、論文や報告の中で「成り立つ」と書くまでには、いくつかの段があります。
(i) 主張を量化子つきで書く。 「誤差は小さい」ではなく、「すべての について誤差は 以下」「ある定数 があって、すべての で誤差は 以下」のように、どの範囲のすべてについての主張か、何の存在の主張かを書きます。有限個の場合についての主張は計算で確かめられます。無限個の場合についての主張は、計算だけでは確かめられません。
(ii) 数値の主張に誤差の上界を付ける。 「値は 」ではなく「値は 」と書き、 の根拠(定理 3.5 の誤差の推定、条件数と残差、誤差予算)を示します。推定値と保証された上界は区別します。Richardson の推定は漸近的に正しい推定値で、上界ではありません。
(iii) 探索の範囲・件数・種・失敗例を記録する。 定理 6.2 のとおり、反例が出なかったという報告の価値は、分布と件数で決まります。途中で止まった試行、収束しなかった試行も消さずに件数を残します(長時間ジョブの管理の章の打ち切りの偏り)。
(iv) 証明に使う数値は、包含の保証がある計算に置き換える。 浮動小数の計算結果は、真の値の近くにあると期待できるだけで、どちら側にどれだけずれているかを教えてくれません。証明の中で「この量は正である」「この区間に零点がある」という事実を使うなら、結果が真の値を含む区間であることが保証された計算を使います。区間演算では、算術式の値が計算された区間に必ず含まれます(定理 4.4[浮動小数と区間演算])。球算術でも同じです(定理 4.9[浮動小数と区間演算])。区間が を含まなければ符号が確定し、区間 Newton 法の条件が満たされれば零点の存在と一意性が確定します(定理 5.2[浮動小数と区間演算])。
(v) 「数値的に確かめた」と「証明した」を書き分ける。 「 のすべてについて計算で確かめた」「 個の乱数(種 20261008、 の一様分布)で反例は見つからなかった」「球算術により証明した」は、別の強さの主張です(定義 2.2)。読者が区別できるように書きます。
とくに危ういのは、ほぼ の量の符号や、等しいかどうかを浮動小数で判定することです。
例 7.1(倍精度で正に見える量が、実は 0 である例)
2 つの量を考えます。 は厳密に です( も も正で、2 乗するとどちらも )。 は、 にきわめて近い、 でない数です。倍精度、mpmath の 50 桁、python-flint の球(arb)で計算します。
# Python 3.12.14 / mpmath 1.4.1 / python-flint 0.9.0import mathfrom mpmath import mp, mpf, sqrt as msqrt, exp as mexp, pi as mpi, nstrfrom flint import arb, ctx
N163 = 262537412640768744
# 倍精度q1 = math.sqrt(2) * math.sqrt(3) - math.sqrt(6)q2 = math.exp(math.pi * math.sqrt(163)) - N163print(f"float64 : q1 = {q1:+.3e} q2 = {q2:+.3e}")
# mpmath(10 進 50 桁の浮動小数。包含の保証はない)mp.dps = 50m1 = msqrt(2) * msqrt(3) - msqrt(6)m2 = mexp(mpi * msqrt(163)) - N163print(f"mpmath50: q1 = {nstr(m1, 4)} q2 = {nstr(m2, 8)}")
# python-flint の球(真の値を含む区間)for prec in (53, 128, 256, 1024): ctx.prec = prec b1 = arb(2).sqrt() * arb(3).sqrt() - arb(6).sqrt() b2 = (arb.pi() * arb(163).sqrt()).exp() - arb(N163) print(f"arb {prec:4d} ビット: q1 = {b1.str(5)} 0 を含む: {b1.contains(0)}") print(f" q2 = {b2.str(8)} 0 を含む: {b2.contains(0)} 負と確定: {bool(b2 < 0)}")float64 : q1 = +4.441e-16 q2 = -4.800e+02mpmath50: q1 = -5.346e-51 q2 = -7.499274e-13arb 53 ビット: q1 = [+/- 2.04e-15] 0 を含む: True q2 = [+/- 6.49e+3] 0 を含む: True 負と確定: Falsearb 128 ビット: q1 = [+/- 4.21e-38] 0 を含む: True q2 = [-7.49927e-13 +/- 5.70e-19] 0 を含む: False 負と確定: Truearb 256 ビット: q1 = [+/- 1.24e-76] 0 を含む: True q2 = [-7.4992740e-13 +/- 2.81e-21] 0 を含む: False 負と確定: Truearb 1024 ビット: q1 = [+/- 1.02e-307] 0 を含む: True q2 = [-7.4992740e-13 +/- 2.81e-21] 0 を含む: False 負と確定: Trueについて. 倍精度は (正)、mpmath の 50 桁は (負)を返します。真の値は で、どちらの符号も丸めの産物です。「2 つの精度で計算して、どちらも でなかった」ことは、 でないことの証拠になりません。球算術は、どの精度でも「 を含む」と答えます。これは「分からない」という正直な答えです。精度を上げると半径は縮みますが、 を含むことは変わらず、有限の精度の計算で等号を証明することはできません。 は、冒頭に書いた代数的な議論で証明します。
について. 倍精度の は、桁が合っていません。 の付近では隣り合う binary64 の数の間隔が で、さらに指数関数の引数 の丸め誤差が約 倍に拡大されるからです。mpmath の 50 桁は を返します。これはもっともらしい値ですが、包含の保証はありません。球算術は、53 ビットでは「分からない」、128 ビット以上では「 を含まない、負である」と答えます。これは、ライブラリの球の演算が真の値を含むという保証のもとでの、 の証明です。 でない量の符号は、十分な精度の球算術で確定できます。
検算. は、3 通りの計算(mpmath の 50 桁、arb の 128 ビットと 256 ビット)で の表示の桁まで一致します。倍精度の値だけが外れており、その外れ方( の倍数で、 の程度)は上の説明と合います。
この例の球算術の保証について、正確に述べておきます。Rikai で証明してあるのは、球の加算と乗算が真の値を含むこと(定理 4.9[浮動小数と区間演算])です。平方根・指数関数・円周率の球が真の値を含むことは、ライブラリ(Arb)の実装が保証する性質で、ここではそれを信頼しています。計算機を使った証明では、「何を信頼したか」(ライブラリ、その版、コンパイラ、ハードウェアの演算)を明記するのが作法です。この先の、数値を証明に変える技法は、浮動小数点数と区間演算の章から始まる精度保証つき計算のテーマで扱います。
なお、この章で扱ったのは数値の検算です。整列の結果が整列されているか、データ構造の不変条件が保たれているか、といった離散的な性質を無作為な入力で調べる方法(入力の生成と、失敗した入力の縮約)は、ソフトウェアのテストの理論に属する別の話題です。
ある求積のコードの誤差が、刻み でそれぞれ 、、 でした。観測次数を求めてください。このコードが複合 Simpson 則のつもりで書かれていたとすると、何を疑いますか。
解答
、 で、観測次数は です。
複合 Simpson 則なら、被積分関数が 級のとき次数は のはずです。疑う先は 2 つあります。1 つは実装で、たとえば重み と の位置が入れ替わっている、台形則の重みになっている、などです。もう 1 つは仮定で、被積分関数の高階の導関数が積分区間で有界でない場合です(例 3.4 の は次数 でした)。滑らかな関数( など)で同じコードを試し、それでも なら実装の誤りです。
演習 8.2標準
真値の分からない量について、刻み での計算値が 、、 でした。定理 3.5 の方法で次数を推定し、 の誤差の推定値と、それを使って補正した値を求めてください。
解答
、 で、比は 、推定次数は です。
誤差の推定は です。補正した値は です。これは Richardson 外挿の値 と同じものです。
ただし、3 つの値から次数を 1 つ推定しただけなので、この次数が漸近領域のものかどうかは確かめられていません。もう 1 段細かい を計算し、 からの推定次数も に近いことを確かめるのが安全です。
演習 8.3標準
ある不等式を、種を固定した 個の一様乱数の入力で調べ、反例は見つかりませんでした。(i) 信頼水準 で、反例の割合について何が言えますか。(ii) 信頼水準 で「反例の割合は 以下」と言うには、何個の標本が必要ですか。(iii) (ii) の個数を調べて反例がなかったとして、不等式が証明されたと言えない理由を 2 つ挙げてください。
演習 8.4標準
例 5.2 の調和振動子に Euler 法を使うと、1 ステップで がちょうど 倍になることを示し、、 のときのエネルギーのずれが であることを導いてください。、 のときの値を求め、例の表と比べてください。
解答
Euler 法の 1 ステップは です。 です。したがって で、ずれは です。 は 、()を満たすので で で、 として です。
、 では 、 で、、ずれは です。表の と一致します。 が小さいとき で、ずれは の 1 次です(命題 5.1 の )。Euler 法のエネルギーは増える一方で、時間について指数関数的に増えます。命題の上界が について線形なのは、数値解が有界な集合 にとどまる範囲での評価だからです。
- P. J. Roache, Verification and Validation in Computational Science and Engineering, Hermosa Publishers, 1998 — 検証と妥当性確認の区別、格子収束による次数の検査。
- W. L. Oberkampf, C. J. Roy, Verification and Validation in Scientific Computing, Cambridge University Press, 2010 — 製造解の方法、観測次数。
- K. Salari, P. Knupp, “Code Verification by the Method of Manufactured Solutions”, Sandia National Laboratories, SAND2000-1444, 2000.
- N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM, 2002 — Chapter 1(残差・後退誤差・前進誤差)と Chapter 7(線形方程式の摂動論)。
- E. Hairer, C. Lubich, G. Wanner, Geometric Numerical Integration, 2nd ed., Springer, 2006 — 保存量と数値解法。
- F. Johansson, “Arb: efficient arbitrary-precision midpoint-radius interval arithmetic”, IEEE Transactions on Computers 66 (2017), 1281–1292. doi:10.1109/TC.2017.2690633
- W. Tucker, Validated Numerics: A Short Introduction to Rigorous Computations, Princeton University Press, 2011.
執筆: Claude Code(Opus 5.5)、Claude Code(Sonnet 5.5)・監査 2 回(Claude Code)・最終 2026-10-12
この記事の誤りを報告する ・運営: 夢現技研合同会社 ・料金 ・利用条件 ・特定商取引法に基づく表記
© 2026 夢現技研合同会社 ・コード例は MIT ライセンスです。