Claude Media
Claude CodeでMATLABからPythonへ移植し、数値結果の一致を検算する

Claude CodeでMATLABからPythonへ移植し、数値結果の一致を検算する

MATLABの解析コードをPythonに移すとき、先にMATLABの出力を基準値として固定し、一致テストをClaude Codeに書かせる手順です。1起点・列優先・標準偏差など、数値がずれる箇所も挙げます。

Claude CodeでMATLABからPythonへ移植し、数値結果の一致を検算する

MATLABの解析コードをPythonへ移すときの失敗は、構文の書き換えよりも「動くのに数値が少し違う」形で出ます。Claude Codeに任せるなら、変換を頼む前に、MATLABが出した値を基準として固定し、Python側が同じ値を返すかを判定するテストを先に置くのが近道です。この記事は、その基準値の作り方、テストの形、NumPyで結果がずれやすい箇所、Claude Codeへの指示の置き方をまとめます。

移植の検算は「基準値」と「一致テスト」から始める

Claude Codeのベストプラクティスは、Claudeに実行できる確認手段を渡すことを最初の助言に置いています。確認手段の例として挙がっているのは、テスト、ビルド、リンター、出力を固定データと突き合わせるスクリプトです。確認手段がないと、Claudeは「できたように見える」ところで止まり、間違いを見つける役が人間に回ります。

移植では、この確認手段が「MATLABの出力との一致」にあたります。順序は次のとおりです。

手順

検算つきの移植の流れ

  1. 1

    基準値を保存する

    元のMATLABコードを、代表的な入力で実行します。入力と出力を.matファイルにsaveしておきます。

  2. 2

    一致テストを書く

    Pythonの関数をまだ作らない段階で、基準値を読み込んで比較するテストを書かせます。この時点では失敗して構いません。

  3. 3

    関数ごとに移植する

    関数を1つ移すたびにテストを回し、落ちた箇所だけを直します。

  4. 4

    入力を増やす

    境界値、欠損値、行数が1の行列など、MATLABが特殊に扱いそうな入力を基準値に足します。

ベストプラクティスには、テストを書く役とコードを書く役を別のClaudeに分ける使い方も載っています。移植では、一致テストを書くセッションと、移植コードを書くセッションを分けると、テストが実装の癖に引きずられにくくなります。

基準値の保存と読み込み

MATLAB側では、入力と戻り値を1つのファイルに保存します。NumPyの「NumPy for MATLAB users」ページは、load data.matに対応するPython側をio.loadmat('data.mat')とし、MATLABまたはOctaveで保存するときは新しいバイナリ形式を使うよう注記しています。scipy.io.loadmatは、保存された配列を辞書として返します。

% 基準値の保存(MATLAB側)
x = (1:12)';
A = [2 1; 1 3];
b = [1; 2];
r = analyze(x, A, b);
colmean = r.colmean; sd = r.sd; sol = r.sol; idx = r.idx;
save('ref.mat', 'x', 'A', 'b', 'colmean', 'sd', 'sol', 'idx');

読み込み側のテストは次の形です。squeeze_me=Trueを付けると、MATLABの1×nやn×1の配列がPython側で1次元に落ち、形の違いで比較が失敗する事態を避けられます。

# test_port.py
import numpy as np
import pytest
from scipy import io
from port import analyze
 
 
@pytest.fixture(scope="module")
def ref():
    return io.loadmat("ref.mat", squeeze_me=True)
 
 
def test_analyze_matches_matlab(ref):
    out = analyze(ref["x"], ref["A"], ref["b"])
    for key in ("colmean", "sd", "sol", "idx"):
        np.testing.assert_allclose(
            out[key], ref[key], rtol=1e-10, atol=1e-12, err_msg=key
        )

numpy.testing.assert_allcloseは、abs(actual - desired) <= atol + rtol * abs(desired)が全要素で成り立つかを判定します。既定はrtol=1e-07、atol=0です。基準値が0に近い要素ではrtolだけだと必ず落ちるので、atolも明示します。

ref.matを手元で用意できない場合は、MATLABを持つ同僚に基準値の保存を頼むか、MATLAB MCPサーバーをClaude Codeで使う設定ガイドの構成でClaude CodeからMATLABを実行できるようにします。

一致が崩れやすい箇所

同ページが挙げる差のうち、数値の一致に直結するものを表にします。ここは、Claude Codeへの指示とレビューの観点としてそのまま使えます。

論点MATLABNumPyずれ方
添字の起点MATLAB1起点NumPy0起点ずれ方1つずれる、または末尾が欠ける
*演算子MATLAB行列積NumPy要素ごとの積ずれ方値も形も変わる。行列積は@
スライスMATLAB値渡し(必要時にコピー)NumPy元の配列のビューずれ方代入で元の配列が書き換わる
reshapeMATLAB列優先(Fortran順)NumPy既定は行優先(C順)ずれ方要素の並びが変わる
&と|MATLAB論理AND/ORNumPyビット演算ずれ方3 & 4が0になる
線形インデックスMATLAB行列に1つの添字で触れるNumPy使えないずれ方findの戻り値の意味が変わる
a\bMATLAB連立方程式を解くNumPylinalg.solve(正方)/linalg.lstsqずれ方関数の取り違え
luのPMATLABa == P'*L*UNumPya == P@L@Uずれ方Pが転置の関係

実際に動かして見える差

NumPy 2.5.4、SciPy 1.18.1の環境で、次の小さな確認をしました。

import numpy as np
x = np.arange(1, 13)
print(x.reshape(3, 4)[0])                  # [1 2 3 4]
print(x.reshape(3, 4, order="F")[0])       # [ 1  4  7 10]
print(3 & 4, np.logical_and(3, 4))         # 0 True
 
a = np.array([[1., 2.], [3., 4.]])
b = a[0]; b[0] = 99
print(a[0, 0])                             # 99.0  (ビューなので元が変わる)
print(np.array([1., 2., 3., 4.]).flatten("F"))  # 並び順は元が1次元なら同じ
print(np.std([1, 2, 3, 4]), np.std([1, 2, 3, 4], ddof=1))  # 1.118... 1.291...

reshapeの既定の並びが変わることは、同ページに「MATLABのz = reshape(x,3,4);はx.reshape(3,4,order='F').copy()にする」という形で書かれています。上の出力のとおり、先頭行は[1 2 3 4]と[1 4 7 10]で別物になります。

一方、1次元のベクトルを平らにするだけのflattenは並びが変わりません。同ページはx(:)をx.flatten()に対応させ、MATLABと同じ並びが要るときはflatten('F')を使うよう注記しています。この注意は2次元以上の配列で効きます。

標準偏差の自由度

numpy.stdは、ddofで割る数をN - ddofに決め、既定は0です。他の言語のコードから移すときは、基準値を出したMATLAB側の標準偏差がどちらの割り方だったかを確かめ、Python側のddofを合わせます。上の確認では、[1, 2, 3, 4]の標準偏差がddof=0で1.118…、ddof=1で1.291…となり、見た目は近くてもrtol=1e-10のテストでは確実に落ちる差です。

添字と範囲の読み替え

1起点と0起点の差は、単に「1を引く」では済みません。範囲の終端も変わるからです。同ページの対応表から、移植でよく出る形を並べます。

MATLABNumPy読み替えの要点
a(2,5)NumPya[1, 4]読み替えの要点行・列とも1を引く
a(1:3,5:9)NumPya[0:3, 4:9]読み替えの要点始点は1を引き、終点は番号そのまま(終端は含まれない)
a(end-4:end,:)NumPya[-5:]読み替えの要点末尾から数える
a(end)NumPya[-1]読み替えの要点最後の要素
1:10NumPynp.arange(1., 11.)読み替えの要点終端を含めるには+1する
a([2,4,5],[1,3])NumPya[np.ix_([1, 3, 4], [0, 2])]読み替えの要点行と列の組み合わせを取るにはnp.ix_

最後のnp.ix_は見落としやすい箇所です。5×5の行列で確かめると、m[np.ix_([1,3],[0,2])]は2×2の部分行列([[6, 8], [16, 18]])になりますが、np.ix_を外したm[[1,3],[0,2]]は対角の2点だけを取り出して[6 18]になります。形が変わるので気づけますが、2×2を2×2で受ける後続の計算では、型が合って気づかないうちに間違える余地があります。

型と欠損値

MATLABは配列を既定で倍精度の浮動小数点で持ちます。NumPyは、配列の中身を保持できる最小の型を選びます。np.array([1, 2, 3])は整数型(上の環境ではint64)になり、そこにa[0] = 2.7と代入すると[2 2 3]に切り捨てられます。移植したコードで初期値を整数で書くと、途中の代入で小数が落ちます。基準値と合わないときは、dtypeを最初に見ます。np.zeros((3, 4))のように、型を指定せず作る配列は64ビット浮動小数点ですが、リテラルから作る配列は中身で型が決まるため、初期値を0と書くか0.0と書くかで結果が変わります。

NaNの扱いも差の出どころです。同ページの対応表では、a.max()はNaNがあるとNaNを返し、np.nanmax(a)はNaNを無視して最大値を返します。[1, nan, 3]で試すと、前者はnan、後者は3.0でした。MATLABのコードが欠損値をどう扱って基準値を出したかを、テスト入力に欠損値を混ぜて確かめます。

戻り値の順序が入れ替わる関数

線形代数の関数は、戻り値を受ける順番が変わります。同じ対応表では、MATLABの[V,D]=eig(a)がD,V = linalg.eig(a)、[U,S,V]=svd(a)がU, S, Vh = linalg.svd(a); V = Vh.Tと書かれています。固有値と固有ベクトルの受け順が逆で、特異値分解はVではなく転置済みのVhが返ります。受け側の変数名だけ見ると型が合うので、基準値との比較テストがなければ素通りします。

sortrowsも同様です。[b,I]=sortrows(a,1)は、I = np.argsort(a[:, 0]); b = a[I,:]の2行に分けて移します。移植を頼むときは、こうした戻り値の入れ替わりを「基準値のキーごとに比較する」テストで受けておくと、取り違えた変数名がそのまま落ちたキー名として表に出ます。

Claude Codeに渡す指示

CLAUDE.mdに移植ルールを置く

CLAUDE.mdはセッション開始時に毎回読まれる指示です。移植ルールは、検算の約束事として短く書いておきます。

# MATLAB → Python 移植ルール
- 移植対象は matlab/ 以下、移植先は src/port/ 以下
- 基準値は tests/ref/*.mat。MATLAB の出力を正とし、勝手に書き換えない
- 関数を1つ移植するたびに `pytest tests/test_port.py` を実行し、出力を貼る
- 比較は np.testing.assert_allclose(rtol=1e-10, atol=1e-12)。許容誤差を緩めて通さない
- インデックスは 0 起点。MATLAB の 1 起点の値を返す箇所には「1起点」とコメントする
- reshape は order="F" を明示する。`*` と `@`、`&` と np.logical_and を取り違えない

ベストプラクティスでは、CLAUDE.mdに含める内容の例として、テスト指示と使うテストランナーが挙がっています。逆に、長すぎるファイルではルールが埋もれるため、ここでは移植に必要な最小限に絞ります。

最初のプロンプト

matlab/analyze.m を Python に移植したい。まだ移植コードは書かないで。
tests/ref/analyze.mat を scipy.io.loadmat で読み、
src/port/analyze.py の analyze() の出力と比較する pytest を先に書いて。
比較には np.testing.assert_allclose を使い、キーごとに err_msg を付けて。
最初は ModuleNotFoundError で落ちるのを確認してから、移植に進んで。

テストが失敗するのを確かめてから実装に進ませる理由は、テストが何も検査していない状態(常に通る状態)を排除できるからです。ベストプラクティスでも、失敗するテストでバグを再現してから直す進め方を例示しています。

テストの回し方

関数を1つ移すたびに、そのテストだけを回します。全体を回すより出力が短く、Claudeが読む量も減ります。

pip install numpy scipy pytest
pytest -q tests/test_port.py -k analyze

assert_allcloseが落ちると、err_msgに渡したキー名と、不一致の要素の割合、最大の絶対誤差・相対誤差が表示されます。この出力をそのままClaudeに読ませれば、どのキーのどの要素がずれたかを手がかりに切り分けに入れます。

落ちたときの渡し方

テストが落ちたら、ログ全体ではなく、落ちたキーと差を渡します。

test_analyze_matches_matlab が sd で落ちた。出力の差を見て、
ddof、reshape の order、1起点のずれのどれが原因か、
numpy-for-matlab-users の対応表と照らして切り分けて。
許容誤差は変えずに直して。

「許容誤差は変えずに」を毎回添えるのは、Claudeがテストを通すためにrtolを緩める道を最初から塞ぐためです。

一致しないときの切り分け

切り分け

数値が合わないときに上から確かめる順番

  1. Q出力の形(shape)は基準値と同じか?
    答えがいいえ

    squeeze_meの有無、行ベクトルと列ベクトルの違い、reshapeのorderを確かめます。

    答えがはい
  2. Q要素の並びは同じで、値だけ違うか?
    答えがいいえ

    並びが違うなら、reshapeのorder="F"と、findの戻り値の1起点を疑います。

    答えがはい
  3. Q差は最後の数桁だけか?
    答えがいいえ

    桁に現れる差は、ddof、*と@の取り違え、&の優先順位など、ロジックの違いです。

    答えがはい

小さな差は浮動小数点の演算順序

最後の数桁だけの差は、計算の順序などによる浮動小数点の揺れが候補です。rtolを基準値の桁数に合わせて見直します。

浮動小数点の揺れで落ちるのか、ロジックの違いで落ちるのかは、差の大きさで見分けます。許容誤差を触る前に、まず原因がロジックの側にないかを探します。

移植後に残すもの

移植が終わってもtests/ref/*.matと比較テストは捨てません。依存を更新したときに、Python側の数値が基準値から動いていないかを確かめる回帰テストとして働きます。Rの解析で依存の版を固定する流れはClaude CodeでRの解析を進めると同じ発想で、Pythonでもrequirements.txtなどで版を固定してからテストを回します。

数理最適化のように結果が1つに定まらない問題は、値の一致ではなく制約の充足を検算する組み立てになります。例は配送ルート最適化をOR-ToolsとClaude Codeで作るにあります。

まとめ

MATLABからPythonへの移植は、基準値を固定してから一致テストを先に書かせると、Claude Codeが自分で合否を読んで直せる作業になります。ずれの原因は、1起点、*と@、reshapeの並び、&の意味、ddofといった数個の型に集中するので、CLAUDE.mdとプロンプトに書き込んでおくと切り分けが速くなります。

この記事を共有:XはてブLinkedIn