MATLABからPythonライブラリを利用したかった話

# はじめに
## この記事について
MATLABとPythonにはそれぞれ有利な点があります。
MATLABは行列演算に強く，日本語のドキュメントやSimlinkの存在などが強みです。
一方で，Pythonは無料で使用でき，scikit-learnなどの豊富な機械学習ライブラリが魅力です。
ですから，これらを使い分けたい場合もあります。

そんなときのために，[MATLABからPythonを直接呼び出す機能](https://jp.mathworks.com/help/matlab/getting-started-with-python.html)があります。
（逆に[PythonからMATLAB関数を呼び出す](https://jp.mathworks.com/help/matlab/matlab-engine-for-python.html)こともできますが，今回は触れません。）

ただ，調べてみても[公式のドキュメント](https://jp.mathworks.com/help/matlab/getting-started-with-python.html)があまり実用的ではありません[^example]。
そこで，今回は**実用上役立ちそうな情報**を整理してまとめようと思います。

## やりたかったこと
以前，機械学習の部分をPythonで処理し，学習データの用意やモデルの評価はMATLABで行いたいときがありました。
それぞれの言語でコードを書いて実行し，結果をdatファイルやmatファイルでやり取りしてもよいのですが，少々面倒でした。
ですので，この機能を知った時「これで楽をできる！」と思って調べたのでした。
（実際の所，調べた結論は「**MATLABからのPython呼び出しは望ましくない**」だったのですが）

[^example]: textwrap.wrapの例しかなく，実際に使いたいnumpyなどの使い方が全然わからない・・・（標準ライブラリのみで説明したかったのでしょうが）

# pythonを利用する準備
MATLABで利用する前に，Pythonインタプリタのインストールとそのパスの登録が必要となります。
ただし，pythonは[公式版](https://www.python.org/)や[Anaconda版](https://www.anaconda.com/)が存在しているため，それによって手順が少し変わります。

Python.orgの公式版を利用する場合は，[こちらの説明](https://jp.mathworks.com/help/matlab/matlab_external/install-supported-python-implementation.html)にしたがって，インストールを行うだけです。

一方，Anaconda環境や，公式版でパスを通していない場合は，個別にMATLAB上でパスの設定を行う必要があります。
まず，利用したいインタプリターのパスを調べます。
そして，[`pyversion`関数](https://jp.mathworks.com/help/matlab/ref/pyversion.html)で調べたパスを設定します。
例えば，Anacondaの`hogehoge`環境を利用するには以下のように記述します。

```matlab
executable = [getenv('userprofile'),...
     '\AppData\Local\Continuum\anaconda3\envs\hogehoge\python.exe'];
pyversion(executable);
clearvars executable;
```

きちんと設定できているかを確認するためには引数なしで`pyversion`を実行します。
versionが表示されていれば，大丈夫なはずです。

# Pythonを使って処理をする
## python関数を呼び出す
次はPythonの関数を呼び出してみます。
MATLABから，Pythonの`command`という名前の関数を呼び出すには，`py.command`とすればよいです。
例えば，pythonのprint関数を実行したい場合は，以下のようにします。

```matlab
py.print("Hello world!");
```

ですが，MATLABとPythonでは組み込みデータ型や文法が異なることに注意が必要です。
例えば，Pythonでは可能であっても，以下のような記法は許されません。

```matlab
py.print("{0} {1}".format("Hello", "world!"));

%  py.print("{0} {1}".format("Hello", "world!"));
%                    ↑
% エラー: 予期しない MATLAB 演算子です。
```

適切に実行するためには，MATLABのString型を[明示的にPythonの文字列型へ変換](https://jp.mathworks.com/help/matlab/matlab_external/handling-data-returned-from-python.html)してやる必要があります。

```matlab
py.print(py.str("{0} {1}").format("Hello", "world!"));
```

## import の仕方
次に[Pythonパッケージのインポート方法](https://jp.mathworks.com/help/matlab/matlab_external/python-import-and-matlab-import-commands.html)についてです。
上記のページには，pythonにおける`from x import y`に相当する構文が説明されています。
しかし，実際の用途では`import numpy as np`のような構文がよく使われます。
これは，python側の[import文の実装](https://docs.python.org/ja/3/library/importlib.html)から，以下のように書くことができます。

```matlab
% Pythonでの import numpy as np に相当するコード
np = py.importlib.import_module('numpy');
```

## 行列の渡し方(MATLAB to ndarray)
MATLABとPython間でのデータのやり取りの際にはデータ型の変換が行われます。
自動で行われる変換は，[MATLABからPython](https://jp.mathworks.com/help/matlab/matlab_external/passing-data-to-python.html)，[PythonからMATLAB](https://jp.mathworks.com/help/matlab/matlab_external/handling-data-returned-from-python.html#buialof-53)にまとまっています。
ここで，1行N列のベクトルをPythonへ渡す際は，組み込みのarray型へ自動的に変換されます。
しかし，M行N列の行列は自動的に変換されないようです。

```matlab
matrix = np.array(rand(2));

% エラー: py.builtin_function_or_method/subsref
% MATLAB 'double' の Python への変換は、1-N ベクトルに対してのみサポートされています。
```

そのため，Matlabのセル配列がPythonのタプルへ自動変換されることを利用して，以下のようにすればndarray形に変換できるようです。

```matlab
mat_org = rand(2);
for i = 1:size(mat_org, 1)
    mat_cell{i} = mat_org(i, :);
end
matrix = np.array(mat_cell);

disp(mat_org);       % 元の行列(MATLAB)
py.print(matrix);    % 変換後の行列(ndarray)
```

毎回for文を書くのは面倒なので，関数化して利用するとよいでしょう。

```matlab:mat2ndarray.m
function ndarray = mat2ndarray(mat)
% ndarray = mat2ndarray(mat) 行列をpythonのndarray形式に変換して返す

validateattributes(mat, {'numeric'}, {'2d'})
n_rows = size(mat, 1);
mat_cell = cell([1, n_rows]);

for r = 1:n_rows
    mat_cell{r} = mat(r, :);
end
ndarray = py.numpy.array(mat_cell);
end
```

## パラメータを指定して関数を実行する方法
pythonでは，[パラメータ名を指定して関数に渡す](https://jp.mathworks.com/help/matlab/matlab_external/python-function-arguments.html)ことがあります。
この場合，MATLABとPythonで文法が異なることが問題となります。
解決策としては，MATLABの[pyargs関数](https://jp.mathworks.com/help/matlab/ref/pyargs.html)を使います。

例えば，numpyで行列の列ごとの平均を求める場合は，以下のようにします。

```matlab
mat = mat2ndarray(rand(2));
% Pythonでの np.average(a=mat, axix=0) に相当
ave = np.average(pyargs('a', mat, 'axis', py.int(0)));
```

ここで，MATLABでは基本的に数値はdouble型として扱われることに注意が必要です。

# Pythonから結果を受け取る
## 行列の受け取り方(ndarray to MATLAB)
Pythonでの処理が終われば，MATLABの基本型へ戻してプロットなどを行うことでしょう。
その場合，MATLABの行列へ変換する必要がありますが，以下のように単純に変換はできません。

```matlab
n_mat = np.average(pyargs('a', mat2ndarray(rand(2)), 'axis', py.int(0)));  % 何らかの処理結果
double(n_mat);    % matlabの行列へ変換したい
% エラー: double
% py.numpy.ndarray から double に変換できません。
```

そのため，[行列の渡し方](#行列の渡し方(MATLAB to ndarray))と同様に上手く変換する必要があります。
そこで，一旦`array.array`型のベクトルに変換してから，`ndarray.shape`の情報を用いて[reshape関数](https://jp.mathworks.com/help/matlab/ref/reshape.html)で形を整えます。
具体的には，以下のような関数で変換可能です[^refdiscussion]。

```matlab
function mat = ndarray2mat(ndarray)
% mat = narray2mat(mat) pythonのnarray形式の行列をMATLAB形式の行列に変換して返す

validateattributes(ndarray, {'py.numpy.ndarray'}, {'scalar'});
np = py.importlib.import_module('numpy');

shape = cell(ndarray.shape);            % 形を読み取る
shape = cellfun(@(x) int64(x), shape);  % py.intをint64に変換する

if shape == 1
    % スカラの場合，エラーを防ぐ
    shape = [1, 1];
end

% ベクトルとしてデータを読み取る
data = py.array.array(ndarray.dtype.char, np.nditer(ndarray));
data = double(data);                    % py.array.arrayをdoubleに変換する
mat = reshape(data, shape);             % 形状を行列に整える
end
```

[^refdiscussion]: [こちらの議論](https://jp.mathworks.com/matlabcentral/answers/157347-convert-python-numpy-array-to-double)を参考にしました。

## データをmatplotlibで表示したい
結果をMATLABのプロットツールに頼らず，matplotlibで表示してみます [^matplotlib]。
ただし，私の環境(Anaconda)ではQt関連のエラーが発生してプロットできませんでした。

そこで，[こちらの記事](https://qiita.com/sukoppu1102/items/f5c00b520da0fa5d2ff3)を参考に環境変数を追加したところ，上手くプロットできました。
以下にhogehoge環境のQtパスを追加するコードを載せておきます。

```matlab
QtPath = [getenv('userprofile'),...
    '\AppData\Local\Continuum\anaconda3\envs\hogehoge\Library\plugins'];
setenv('QT_PLUGIN_PATH', [getenv('QT_PLUGIN_PATH'), QtPath]);
clearvars QtPath;
```
このコードを`startup.m`に記述しておくと，きちんとプロットできました。

[^matplotlib]: MATLABライクなグラフ描画を提供するmatplotlibをMATLAB上で使うのも変ですが・・・<br>ただし，デフォルトの配色などはmatplotlibの方が良いような気がします。

## sklearnやseabornを使ってみたい
Pythonを使うなら，scikit-learnやseabornを使いたいものです。
なのですが，これらはなぜかインポートできないようです。

```matlab
% ヘルプすら見れない様子・・・。
py.help('seaborn')
% problem in seaborn - ImportError: DLL load failed: 指定されたプロシージャが見つかりません。
```

この原因や解決策が分かる人がいたら，ぜひ教えてください・・・。

# おわりに
以上が，私が調べた範囲のMATLABからPythonを利用する方法です。
結論としては，**個別に実行してファイルを受け渡すほうが圧倒的に楽**だと思いました。
ライブラリを利用してcsvファイルを受け渡す処理を書く方が，MATLABからPythonを呼び出す方法を覚えるよりも何倍も楽だと思います。

<!-- また，ファイルを受け渡す処理にしておけば，他の言語への受け渡しもスムーズでしょう。-->

また，私の環境では，**scikit-learnが使えないのは致命的**です。
numpyやscipyの機能だけであれば，MATLABの組込み関数の方が強力だと思います。

さらに，spyderやpycharmであれば強力なコード補完などのサポートが受けられますが，MATLAB上では貧弱なサポートしか受けられません。

以上を考慮すると，現状の「MATLABからのPython呼び出し」はあまり良い方法とは言えないでしょう。
ただ，十分調べ切れていない部分もあるので，もし詳しい方がいればコメントで補足頂けると幸いです。

## 代替手段
代替手段としては，MATLABスクリプトとPythonスクリプトをそれぞれ用意し，データはファイルや標準入出力でやり取りさせるのが良いでしょう。
例えば，メインのMATLABスクリプト(main.m)と処理を行うPythonスクリプト(hogehoge.py)があれば，以下のような流れで処理します。

1. main.m がデータを用意し，入力ファイルに書き出す
2. main.m から`!python hogehoge.py`のようにPythonスクリプトを実行させる
2. hogehoge.py が処理を行い，出力ファイルに保存
4. main.m が処理済みのデータを出力ファイルから読み込む

# 参考文献
1. [MATLABにおけるpythonの入門](https://jp.mathworks.com/help/matlab/getting-started-with-python.html) (MATLAB公式ドキュメント)
1. [pyを使うとき未定義といわれる問題](https://qiita.com/namn1125/items/ac48150a518f27748a5f) (Qiita記事)
2. [pyversion関数](https://jp.mathworks.com/help/matlab/ref/pyversion.html) (MATLAB公式ドキュメント)
3. [numpy arrayからdoubleへの変換（英語）](https://jp.mathworks.com/matlabcentral/answers/157347-convert-python-numpy-array-to-double) (MATLAB Answers)
4. [Python 関数の引数](https://jp.mathworks.com/help/matlab/matlab_external/python-function-arguments.html) (MATLAB公式ドキュメント)