はじめに
この記事では、Gaussian Process まで入った “ちゃんとした” 補間である、NIST の Joe Fowler 氏が書いた interpolate.py について解説します。
普通の補間から一歩進んで、
- 厳密な三次スプライン補間(CubicSpline)
- スムージングスプライン(SmoothingSpline)
- ガウス過程回帰(Gaussian Process Regression; GPR)に基づくスムージングスプライン(GPRSpline)
- 自然境界条件付き B スプライン基底(NaturalBsplineBasis)
をひとまとめに実装した、かなり“濃い”補間モジュールです。
原理のコア部分については下記で説明を試みています。
コードは下記から公開されています。
この記事では、このファイルの構造と中身を
- 「何をしているのか」
- 「どんなときに使えるのか」
- 「SciPy の標準 API とどこが違うのか」
という観点で、コードを読みながら噛み砕いて解説します。
全体の構成
モジュールの先頭コメントにもあるように、主なクラス・関数は以下のとおりです。
-
CubicSpline:厳密な三次スプライン補間(端点の傾き or 自然境界条件) -
CubicSplineFunction:CubicSplineを「微分可能な Function オブジェクト」として包んだもの -
LinterpCubicSpline:2 つのCubicSplineの線形補間 -
k_spline:GPR 用のスプラインカーネル -
GPRSpline/GPRSplineFunction:ガウス過程回帰に基づくスムージングスプライン -
NaturalBsplineBasis:自然境界条件を満たす B スプライン基底 -
SmoothingSpline/SmoothingSplineFunction:Reinsch 型のスムージングスプライン -
SmoothingSplineLog:x, y を log スケールにしたスムージングスプライン
Function / ConstantFunction は別モジュール mass.mathstat.derivative から来ており、
「関数オブジェクト+その導関数」を扱うための抽象クラスだと考えてよさそうです。
1. CubicSpline:教科書通りの「三次スプライン補間」
1.1 何をしているクラスか
CubicSpline は、与えられた点列 (xᵢ, yᵢ) を厳密に通る三次スプラインを構成します。
いわゆる「Numerical Recipes 3 版 §3.3」に出てくる実装をそのまま Python 化したような形です。
特徴は:
- x を昇順にソートしたうえで、
- 各区間ごとの x 間隔
xstep、y の差ystepから、 - 各ノット点での二階微分
y2を解く
という典型的なアルゴリズムになっています。
端点条件は
- 傾き y′(x₁), y′(x_N) を指定する
- 何も指定しない場合は「自然境界条件」:y''=0 at 両端
から選べます。
cs = CubicSpline(x, y) # 両端は自然境界条件
y_interp = cs(x_new) # 0 次導関数(値)
dy_dx = cs(x_new, der=1) # 1 次導関数
1.2 SciPy の InterpolatedUnivariateSpline との違い
モジュールの docstring にも書かれていますが、SciPy の
InterpolatedUnivariateSpline とは振る舞いが微妙に異なります。
- SciPy 版は「自由度を調整するために 2 番目と N−1 番目のデータ点を内部ノットから外す」
- この
CubicSplineは、「端点の 1階 or 2階微分で自由度を使う」
という違いがあります。
したがって、数値的にはほぼ同じでも、完全には一致しないことがあります。
境界条件をちゃんと意識したい場合には、この実装の方が「透けて見える」ので教育的です。
1.3 呼び出しの中身
__call__ メソッドは
-
np.searchsortedで x が属する区間を特定 - 区間外は線形外挿(端点の値+端点の傾き×距離)
- 区間内は、教科書通りの三次スプラインの式
などを使って評価
s(x) = a y_k + b y_{k+1} + ((a^3 - a) y''*k + (b^3 - b) y''*{k+1}) \frac{h^2}{6}
導関数(der=1,2,3)も、同じく解析的な式を実装しており、3階導関数までは正確に計算できます。
2. CubicSplineFunction:微分可能な関数オブジェクトとしてのスプライン
CubicSplineFunction は
class CubicSplineFunction(CubicSpline, Function):
...
と多重継承していて、
-
CubicSplineとしての補間機能 -
Functionとしての「derivative メソッド」
を両方持つラッパーです。
2.1 derivative の実装方針
CubicSpline 自体は「der 引数を増やす」ことで高階微分を返せますが、
CubicSplineFunction はさらに
- 一度
der=1の微分を取ったら、そのオブジェクト自体を「1階導関数」とみなす - さらに
derivative(1)を呼ぶと「2階導関数オブジェクト」を返す
といった「関数解析っぽい」使い方ができます。
f = CubicSplineFunction(x, y)
df = f.derivative(1) # f' を表す新しい CubicSplineFunction
d2f = f.derivative(2) # f'' を表す
内部的には単に der のオフセットを管理しているだけですが、
「関数をオブジェクトとして扱う」設計が気持ちいいポイントです。
3. LinterpCubicSpline:スプライン同士の線形補間
LinterpCubicSpline(s1, s2, fraction)
は、2 つの CubicSpline オブジェクト s1, s2 の「線形補間」用のクラスです。
イメージとしては
s(x; f) = f ~ s_1(x) + (1-f)~ s_2(x)
で、f = fraction。
3.1 実装のポイント
入力としては CubicSpline ですが、中身では
- ノット数
_n - ノット位置
_x - 値
_y - 二階微分
_y2 - 端点の傾き
yprime1,yprimeN - 区間長
xstep,ystep
など、スプラインの内部パラメータを重み付き平均して新しいスプラインを作っています。
各 x で「2 本のスプラインを評価して足す」のではなく、
あらかじめスプラインそのものを補間してしまうため、呼び出しが高速になります。
- 時間変化するスペクトルをスプラインで表現し、その間を smooth に補間したい
- パラメータの異なる 2 つの「モデル曲線」を連続的に行き来したい
といった用途で便利です。
4. NaturalBsplineBasis:自然境界条件を満たす B スプライン基底
SmoothingSpline で使われる基底クラスです。
普通の B スプラインは、そのままだと自然境界条件 f''(端点)=0 を満たしてくれません。
そこで NaturalBsplineBasis は、
-
ノット列
knotsを両端で 3 つずつパディング -
BSplineを使って、- 左端での f'' をゼロにするために
先頭 3 個の基底(#0, #1, #2)の線形結合係数coef_bを求める - 右端でも同様に
coef_eを求める
- 左端での f'' をゼロにするために
-
実際の基底呼び出しでは、
左端・右端付近の基底にこの線形結合を足し込むことで f''=0 を実現
という仕掛けをしています。
4.1 使い方イメージ
knots = [0,5,8,9,10,12]
basis = NaturalBsplineBasis(knots)
x = np.linspace(0, 12, 200)
for i in range(len(knots)):
plt.plot(x, basis(x, i))
これで、各基底関数が全て f''(x_min)=f''(x_max)=0 を満たすようになります。
この上で線形結合しても、最終的なスプラインは自然境界条件を満たす、というわけです。
5. SmoothingSpline:Reinsch 型スムージングスプライン
5.1 問題設定
SmoothingSpline は、以下のような最適化問題を解くクラスです:
-
データ:$(x_i, y_i)$, 誤差 $\sigma_i$
-
曲線 $f(x)$ は「自然境界条件付き三次スプライン」
-
目的:
- 曲率エネルギー(二階導関数の二乗の積分)を最小化しつつ
- $\chi^2 = \sum \frac{(y_i - f(x_i))^2}{\sigma_i^2} \le \chi^2_{\max}$
を満たす
この「曲率 vs. データ忠実度」のトレードオフを、Reinsch (1967) の方法で数値的に解いています。
5.2 実装の流れ
-
誤差
dy,dxから「全体の誤差ベクトルerr」を作る-
dxが与えられている場合は、2 次多項式で rough fit → slope を推定
→ $\sigma^2 = (\mathrm{dx} \cdot slope)^2 + dy^2$
-
-
x をスケーリングして
xscaleで正規化(数値安定性のため) -
NaturalBsplineBasisで基底を構成-
N0:各ノットでの基底値 -
N2:各ノットでの二階導関数
-
-
_compute_Omegaで「二階導関数の内積行列」(\Omega) を計算- $\Omega_{ij} = \int B_i''(x) B_j''(x) dx$
-
smooth()で「曲率 vs. χ²」を調整するパラメータpを探索- ある p に対して最適係数 β(p) を解く
- そこから χ²(p) を計算
- 目標
chisqになるようにbrentqで p を 1D root-finding
-
得られた β を
basis.expand_coeffで FITPACK 形式の係数に変換-
splevで値・導関数を計算可能にする
-
-
スプラインの外側は線形外挿(
lowline,highline)で繋ぐ
5.3 実際の使い方
spline = SmoothingSpline(x, y, dy) # maxchisq がデフォルトで Ndat
y_smooth = spline(x_new)
- 「荒いノイズが乗ったスペクトルを滑らかにしたい」
- 「曲率を抑えつつ、χ² が妥当なスムーズ曲線を作りたい」
といった用途で便利です。
6. GPRSpline:ガウス過程回帰としてのスムージングスプライン
このモジュールのハイライトの一つが GPRSpline です。
6.1 スプラインカーネル k_spline
まず、カーネル関数 k_spline(x, y) が定義されています:
def k_spline(x, y):
v = np.minimum(x, y)
return v**3/3 + v**2/2*np.abs(x-y)
これは、Rasmussen & Williams の GP 本の Eq (6.28) で紹介される
“cubic spline” に対応するカーネルです。
このカーネルを使って GPR を行うと、
- 平均関数としては「自然境界条件付き三次スプライン」と同じ形
- かつ GPR 理論に基づいた「不確かさ(分散)」も計算できる
という、スプライン+ベイズのハイブリッドなモデルが得られます。
6.2 GPRSpline の流れ
GPRSpline.__init__ では、おおまかに次のことをしています:
-
データ x, y, 誤差 dy(+必要なら dx)から「誤差ベクトル
err」を計算 -
sigmaf(関数のスケール)の値を決める- 指定されなければ
best_sigmaf()で「周辺尤度を最大化する値」を自動探索
- 指定されなければ
-
スプラインカーネル行列 K を構成
- $K_{ij} = σ_f^2 ~ k_{spline}(x_i, x_j)$
-
データ共分散 Ky = K + diag(err²) を作り、Cholesky 分解 L
-
線形トレンド H = [1, x] を入れた「線形平均+GP」の枠組みで
係数 β を解く -
その結果から「自然スプラインのノット値 gbar」を計算し、
CubicSpline.__init__に渡してスプラインとして初期化
6.3 分散・共分散の計算
-
variance(xtest):テスト点 xtest における事後分散(対角成分だけ)を返す -
covariance(xtest):テスト点同士の共分散行列を返す
となっており、GPR の公式に沿って
- カーネル行列 Ktest
- 事後共分散
のような形を実装しています(線形トレンド込み)。
\mathrm{cov}(f_*, f_*) = K_{**} - K_*^T K_y^{-1} K_* + R^T A^{-1} R
6.4 いつ SmoothingSpline より良いか
GPRSpline は docstring にもある通り、
- 曲率 vs. データ忠実度のトレードオフが、ベイズ的に principled
- 曲線の不確かさも推定できる
という点で SmoothingSpline よりリッチです。
一方で、実装が重く、計算量も O(N³) になるので、
- データ点がそんなに多くない(数百点くらいまで)
- 不確かさも含めてちゃんと扱いたい
という場合に向いています。
7. ログスケール版:SmoothingSplineLog
対数スケール向けには SmoothingSplineLog が用意されています。
class SmoothingSplineLog:
def __init__(self, x, y, dy, dx=None, maxchisq=None):
if np.any(x <= 0) or np.any(y <= 0):
raise ValueError("The x and y data must all be positive ...")
...
self.linear_model = SmoothingSpline(np.log(x), np.log(y), dy/y, dx, maxchisq=maxchisq)
def __call__(self, x, der=0):
return np.exp(self.linear_model(np.log(x), der=der))
- x, y > 0 のときにのみ利用可能
- (log x, log y) 空間でスムージングスプラインをかけてから、exp で戻す
という構造なので、べき則っぽい挙動をするデータに対して自然です。
スペクトル解析や、パワーロー的な挙動をする物理量のフィッティングなどに使えます。
8. *Function 系:派生クラスの共通パターン
CubicSplineFunction, SmoothingSplineFunction, GPRSplineFunction はいずれも
- 元のスプラインクラスを継承
-
Functionインターフェースを実装し、 -
derivative()メソッドで「高階導関数オブジェクト」を返せるようにする
という共通パターンになっています。
いずれも
- 3階導関数を超えると
ConstantFunction(0)を返す -
__call__ではself.der + derを合算して親クラスに渡す
というロジックで、導関数の階数管理を内部で一元化しています。
9. まとめ:このモジュールの「おいしい使いどころ」
この interpolate.py の中身をざっくりまとめると、
- 教科書レベルの三次スプライン(境界条件が明示的に扱える)
- B スプライン+自然境界条件の基底構成
- Reinsch 型スムージングスプライン(曲率エネルギー最小)
- Gaussian Process Regression としてのスムージングスプライン(不確かさ付き)
- ログスケール対応版
- “関数オブジェクト+導関数” として扱うためのラッパー
をひととおり実装した「補間・スムージングの小さな実験場」 になっています。
実務での使いどころとしては:
- SciPy の
UnivariateSplineでは“裏側が見えにくい”と感じたときに、
アルゴリズムを学びつつ、挙動を細かく制御したい - スペクトルや時系列を「曲率制御付きのスムーズな曲線」で近似したい
- GPR の数理は知っているけど、専用ライブラリを入れるほどでは…というときに
「スプラインカーネル+Cholesky の最小構成」を動かしてみたい
といった場面で、とても勉強になるコードです。
おまけ:最小実行例
最後に、CubicSpline と SmoothingSpline の超シンプルな例を載せておきます。
import numpy as np
import matplotlib.pyplot as plt
from interpolate import CubicSpline, SmoothingSpline
# ダミーデータ
x = np.linspace(0, 10, 20)
y = np.sin(x) + 0.2 * np.random.randn(len(x))
dy = 0.2 * np.ones_like(y)
# 厳密なスプライン補間
cs = CubicSpline(x, y)
# スムージングスプライン
ss = SmoothingSpline(x, y, dy)
# プロット
xx = np.linspace(0, 10, 400)
plt.errorbar(x, y, dy, fmt='o', label='data')
plt.plot(xx, cs(xx), label='CubicSpline (exact)')
plt.plot(xx, ss(xx), label='SmoothingSpline')
plt.legend()
plt.show()
-
CubicSplineはデータ点を「必ず通る」ので、ノイズも拾います -
SmoothingSplineは曲率を抑えているので、ノイズをうまく平均化します
ここから GPRSpline に切り替えて、事後分散もプロットしてみると、
「ガウス過程としてのスプライン」がどんなものか感覚的に掴めるはずです。
以上、interpolate.py を一通り読み解きながらの解説でした。
Appendix
以下では、Fowler+ (2022)「Energy Calibration of Nonlinear Microcalorimeters」 と interpolate.py の関係解説をしてみます。
です。
Fowler et al. (2022) *Energy Calibration of Nonlinear Microcalorimeters with Uncertainty Estimates from Gaussian Process Regression
(Journal of Low Temperature Physics, 2022)
本 Appendix では、interpolate.py に実装されている CubicSpline / SmoothingSpline / GPRSpline が、Fowler+ (2022) のエネルギーキャリブレーション理論とどのように対応しているかを整理します。
1. 論文の主張の核心とコード構造の対応
論文の中心的なメッセージは以下の 3 点です(本文 1〜4章):
- TES のエネルギー応答は非線形なので、経験的な補正曲線が必須
- その補正関数は「補間 spline」ではなく「近似 smoothing spline」であるべき
- スムージングスプラインは Gaussian Process Regression(GPR)と完全に等価であり、
GPR を使うと補正曲線の「不確かさ」も同時に算出できる
これら 3 点は interpolate.py の以下の実装と直接リンクしています:
| 論文の概念 | コード中の対応クラス | 役割 |
|---|---|---|
| 厳密な補間 | CubicSpline |
与えられた点を厳密に通る spline |
| 近似 spline (従来法) | SmoothingSpline |
曲率ペナルティと χ² 制約で近似 |
| GPR による smoothing spline | GPRSpline |
論文の方法をそのまま実装(分散も算出) |
2.論文で導かれる smoothing spline と GPR 等価性
→ GPRSpline の実装そのもの
論文 4章では、「1 回積分された Wiener 過程(integrated Wiener process)」を GP の prior に選ぶと、その posterior の期待値は 必ず cubic smoothing spline になる
という重要な定理(Wahba)を使っています。
実際、論文では次の式が示されます(p.1051–1052):
- GP の平均:
$ m(x) = \beta_0 + \beta_1 x $ - GP の共分散(スプラインカーネル):
k(x,x') = \sigma_f^2 \left( \frac{v^3}{3} + \frac{v^2}{2}|x-x'| \right), \quad v = \min(x,x')
これがコードの
def k_spline(x, y):
v = np.minimum(x, y)
return v**3/3 + v**2/2*np.abs(x-y)
に完全に一致しています。
3. 論文の Bayesian 最適化(σ_f の最尤推定)
→ GPRSpline.best_sigmaf() に対応
論文では、曲率ペナルティ λ と GP のスケール σ_f が以下で関係づけられ、
σ_f を「周辺尤度最大化」で決めるべきであると議論します(Eq.8)。
コードではこれに対応するのが:
result = sp.optimize.minimize_scalar(
lambda x: -self._marginal_like(x),
[guess/1e4, guess*1e4]
)
さらに _marginal_like() 内の計算は論文式と完全対応しており、
- K 行列(カーネル)
- Ky = K + diag(err²)
- L = Cholesky(Ky)
- A = (L⁻¹ H)^T (L⁻¹ H)
- yCy や log det(Ky)
など、論文の Eq.(8) に出てくる構造がほぼ一対一で実装されています。
4. Posterior mean の spline 化(論文 Eq.5 → コードの spline 生成)
論文の GPR posterior の期待値(Eq.5) は
f_*(x) = m(x) + K_*^T K_y^{-1} (y - m(x))
ですが、Fowler+ (2022) が特筆しているのは以下の点です:
posterior mean を “すべての x” で評価する必要はない。
knot x_i 上だけ posterior を計算し、それらを natural cubic spline で結べば良い。
(論文 p.1052 下部)
これが GPRSpline.__init__() の最重要部分:
fbar = np.linalg.solve(L.T, np.linalg.solve(L, K)).T.dot(y)
gbar = fbar + R.T.dot(beta)
CubicSpline.__init__(self, self.x, gbar)
この gbar が posterior mean を knot 上で評価した値であり、
それを CubicSpline に渡すことで、期待値 spline を生成しています。
5. 論文の「予測分散(calibration uncertainty)」
→ GPRSpline.variance() / .covariance() に対応
論文 Fig.1(p.1053)では、アンカー点の間隔が粗いほど不確かさが大きい様子が示されます。
コードの
def variance(self, xtest):
...
cov_ftest = self.sigmaf**2*k_spline(x, x) - (LinvKtest**2).sum()
...
は GPR の分散公式をそのまま実装したものです。
\mathrm{Var}[f_*(x)] =
k(x,x) - K_x^T K_y^{-1} K_x + R^T A^{-1} R
という論文の構造に完全に一致します(p.1052 Eq.(6))。
この計算によって、Fig.1 の「不確かさバンド」が再現できます。
6. 従来法 smoothing spline との関係
SmoothingSpline クラスは論文 3章の Reinsch 法を実装
論文 3章(p.1049–1050)にある smoothing spline の functional
C[h] = \sum_i \left(\frac{h(x_i)-y_i}{\sigma_i}\right)^2 +
\lambda \int |h''(x)|^2 dx
これは SmoothingSpline の
-
Omega(二階導関数の内積) -
chisq_difference()で λ(p)を調整し χ² を合わせる - FITPACK B-spline 基底で表現
という形で実装されています。
論文は
smoothing spline と GPR は 完全に等価(Wahba)
と述べており、SmoothingSpline と GPRSpline が同じ母関数を別の数値的アプローチで実装していることがわかります。
7. コードと論文の対応まとめ(早見表)
| 概念 | 論文の章・式 | コード |
|---|---|---|
| スプラインカーネル | Eq.(3) | k_spline() |
| smoothing spline の汎関数 | Eq.(1) |
SmoothingSpline(Omega, chi² root-finding) |
| smoothing spline = GPR posterior mean | p.1052 |
GPRSpline(posterior gbar → CubicSpline) |
| σ_f の最尤推定 | Eq.(8) | best_sigmaf() |
| posterior variance(不確かさ) | Eq.(6) |
variance() / covariance()
|
| 外挿が線形になる性質 | 論文の GP モデルの線形 mean |
lowline / highline
|
8. 結論:interpolate.py は Fowler+ (2022) の "reference implementation"
Fowler+ (2022) の主張は、エネルギーキャリブレーションを
- spline で近似し
- 曲率を最小化しつつ
- GPR により不確かさを付与する
という統一的な方法にまとめた点にあります。
interpolate.py の GPRSpline は、その理論を完全にコード化したものであり、
論文の「数式 → 実装」のほぼ全工程を忠実に反映した reference implementation
といってよい内容です。
(コードについて理解を深めたい方向けに下記にtipsをまとめています。)
コードの手本と改善点
1. 見習うべきポイント(かなり良いところ)
(1) アルゴリズムが教科書レベルで「素直に」実装されている
-
CubicSplineは Numerical Recipes の章をほぼそのままコードに落としていて、- 2階微分
self._y2を解く - 区間での a, b を使った三次スプラインの公式で値や導関数を計算
- 2階微分
-
SmoothingSplineは Reinsch のスムージングスプラインの式を忠実に実装 -
GPRSplineは Rasmussen & Williams の GPR の式を、そのままコード化
👉 「ライブラリの黒魔術に頼らず、自分で数式をコードに落とす」良い見本です。
数値計算や物理の学生がやるべき練習として、とても参考になります。
(2) 「補間」と「スムージング」が明確に分かれている
-
CubicSpline:データ点を必ず通る厳密補間 -
SmoothingSpline/GPRSpline:ノイズを考慮してなめらかに近似するスプライン
👉 「データを全部通れば良い」わけではなく、
- 補間:測定点はほぼノイズなし・物理的に厳密な制約を再現したいとき
- スムージング:測定値に誤差がある前提で“傾向”を取りたいとき
という目的の違いをクラス設計で表現しているのは、とても良い設計です。
(3) 境界条件(boundary condition)をちゃんと扱っている
-
CubicSplineは端点の傾きyprime1,yprimeNを指定するか、指定しなければ自然境界(y''=0) -
NaturalBsplineBasisは Bスプライン基底を組み合わせて f''(端点)=0 を満たすように補正
👉 「境界条件が数値解の性質を決める」という教科書の話を、そのままコードにしているところは、物理系・工学系の学生が特に見習うべき設計です。
(4) 外挿の扱いが明示的でわかりやすい
CubicSpline.__call__ では、x が範囲外のとき:
- 端点の値+端点傾きの線形外挿で対応
- 導関数のときも一定(端点の傾き)を返す
- 高次導関数は 0
if extrap_low.any():
if der == 0:
h = x[extrap_low]-self._x[0]
result[extrap_low] = self._y[0] + h*self.yprime1
👉 「範囲外はよくわからないけれど、とりあえず線形外挿にする」というポリシーがコードで明示されていて、“暗黙の挙動”がないのはかなり良い点です。
(5) 線形代数の基本がきちんと守られている
- 連立一次方程式は
np.linalg.solveを使っていて、愚直な逆行列は作らない - GPR では
np.linalg.choleskyを使って、対称正定値行列を安定に扱う - 「対角行列」はちゃんと
np.diag(self.err**2)で作る
👉 線形代数の“よくある悪手”を避けているところは、学生コードのお手本にしてよいです。
(6) 「関数オブジェクト+導関数」という設計
CubicSplineFunction, SmoothingSplineFunction, GPRSplineFunction は
- 元のスプラインクラスを継承
-
Function抽象クラスのインターフェース(derivative()など)を実装 - 「このオブジェクトは f(x) だけでなく f'(x), f''(x) も返せる」という設計
f = CubicSplineFunction(x, y)
df = f.derivative(1) # 1階導関数
👉 “関数をオブジェクトとして扱う” 発想は、数値解析コードを書ける学生にとって良い刺激になります。
2. 改善した方が良さそうな点・注意したい点
ここからは、「学生が真似するときはここは直した方がいいかも」ポイントです。
(1) コメント・docstring がアルゴリズムレベルまで書かれていない
クラスやメソッドの docstring はありますが、
- 「この for ループは何を解いているのか」
- 「この行列はどういう物理量/数学的量なのか」
といったアルゴリズムの意図がコードの中ではあまり説明されていません。
学生が真似するなら:
-
_compute_y2のところに
「三重対角行列を前進消去+後退代入して 2階微分を解いている」 -
_compute_Omegaに
「二階導関数の内積 ∫B_i'' B_j'' dx を、区分線形性を使って数値積分している」
など、数学とコードの橋渡しコメントを足すと、読みやすさがかなり増します。
(2) 型ヒント(type hints)がない
最近の Python では、学習用コードでも
def __init__(self, x: np.ndarray, y: np.ndarray, dy: np.ndarray, ...):
のように 型ヒント をつけておくと、
- エディタの補完が良くなる
- 間違った型を渡した時にすぐ気づける
ので、学生さんが自分で書くときは、このコードを参考にしつつ type hints を足すと良いです。
(3) エラー処理や入力チェックが最小限
例えば:
-
CubicSplineの__init__では、x が重複していたり、長さが 2 以下でも特にチェックしていない -
SmoothingSplineのsmooth()でbrentqしている区間[1e-20, 1]に根がない場合はLinAlgError以外も起こり得るが、そのときの例外処理は 1 パターンのみ
学生コードとして安全にするなら:
-
「x は単調増加であること」「len(x) >= 3」などを assert でチェック
-
brentqが失敗したときに- エラーメッセージを出す
- あるいは
p=1(単純な線形フィット)にフォールバック
といった 「最悪どうするか」 も書いておくと実用的です。
(4) SmoothingSplineLog で引数を書き換えてしまう
if dx is not None:
dx /= x
ここは、呼び出し側が渡した dx 配列を破壊的に変更してしまう可能性があり、
副作用としてはあまり望ましくありません。
学生が書くなら:
if dx is not None:
dx = dx / x # 新しい配列を作る
のようにしておく方が、バグを生みやすくない書き方です。
dx /= x は インプレース(in-place)演算です。
dx /= x が何を意味するか?
以下のような書き方は、Python/NumPy では
dx = dx / x
(新しい配列を作る)ではなく、
dx = dx.__itruediv__(x) # in-place true division
として扱われます。つまり dx の中身を書き換えてしまいます。
(5) SmoothingSplineFunction.derivative() で元パラメータが引き継がれていない
def derivative(self, der=1):
if self.der + der > 3:
return ConstantFunction(0)
return SmoothingSplineFunction(self.x, self.y, self.dy, self.dx, der=self.der + der)
ここでは maxchisq が新しいオブジェクトに渡されていません。
結果として、導関数を取るたびに デフォルトの maxchisq=N で再フィットし直すことになります。
厳密なバグとは言い切れませんが、設計としては
-
maxchisq(=どれだけフィットを許すか)も含めて「同じモデル」を共有したかったはず
なので、学生が真似するなら
return SmoothingSplineFunction(self.x, self.y, self.dy, self.dx,
maxchisq=self.maxchisq,
der=self.der + der)
のように 元パラメータを維持する方が自然です。
(6) GPR の部分は N³ スケーリングなので、データ数が多い場合の注意が必要
GPRSpline では
- K: Nk×Nk 行列
- Cholesky 分解や solve を多用
しているので、計算量は O(Nk³) です。
100点程度なら余裕ですが、1万点のデータにそのまま適用するのは現実的ではありません。
学生が実験データに適用する場合は、
- アンカー点(校正線など)の数を絞る
- 非等間隔の高密度データの一部を代表値に圧縮する
- どうしても大量データに使うなら、近似 GP や分割統治を検討
といった方針が必要になる、ということを併せて教えると良いです。
3. このコードから学ぶと良いことまとめ
このコードを「お手本」にして学ぶべきこと
- 数式(教科書)→ コードへの落とし方
- 補間とスムージングの違いとその実装
- 境界条件・外挿・誤差の扱い
-
np.linalg.solveや Cholesky を使った安定な線形代数 - 「関数オブジェクト+導関数」という設計パターン
自分で書くときに「改善した状態」で真似したいところ
- アルゴリズムの意図をコメントで書く
- 型ヒント・入力チェック・エラー処理を足す
- 引数(配列)を破壊的に変更しない
- パラメータ(maxchisq など)を derivative でもきちんと引き継ぐ
- 計算量(O(N³))の限界を意識する