MATLABからPythonライブラリを利用できた話

# 初めに
## この記事について
MATLABとPythonは似ている言語といわれますが、それぞれ得意な分野、不得意な分野があります。
ですからどちらを使うか迷うこともしばしばあります。

一つの解決策は、どっちの言語を使うほうがいいかメリットとデメリットを比べて、どちらか片方の言語を使うことです。
しかし、どうしても両方の良さを活かしたいこともあるでしょう。
そんな時には、MATLABからPythonを呼び出してしまいましょう。

例えば、機械学習を実行したいけれど、MATLABで未対応である場合やツールボックスの購入が必要な場合があります。
そのような場合には、学習データの準備と学習結果の評価をMATLABで実施し、学習自体はPythonで無償のパッケージを利用して実行することができます。

今回は、MATLABをベースにしてPythonを利用するときに役立ちそうな情報をピックアップしてまとめてみようと思います。

:::note info
実は同じ趣旨の記事を約2年前に書いています。
その記事は今でも定期的に閲覧がありますが、本記事執筆時点では状況が少し変わっているようです。
バージョンによっては以前の記事も有効だと思われるため、今回は別の記事として投稿します。
:::

## 対象とする環境
本記事の内容は下記環境をベースに記述します。

| 項目 | バージョンなど |
|:-:|:-:|
| OS | Windows10 |
| MATLAB | R2021b |
| Python | 3.8 |

:::note 
MATLAB 2021b からPythonの外部言語インターフェイスが機能追加されました。
該当の公式ドキュメントは未翻訳のだったため、一部英語版を参照しています。
:::

## コード例で用いるPythonパッケージ
本記事では下記のパッケージを例として利用します。
手元環境で試される方は事前にpipコマンドなどで導入してください。

* scikit-learn
* numpy

# Pythonを利用する準備
Pythonはインストールしてパスの設定を行う必要があります。
Python.orgの公式版を利用したい(利用している)場合は、インストール時にパスを通しておけば、自動的にMATLABがパスを見つけてくれます。

パスを通さない場合やAnacondaなどの仮想環境を使う場合は、 [`pyenv` 関数](https://jp.mathworks.com/help/matlab/ref/pyenv.html) を使ってパスを設定してください。
設定するときのコマンド例は[こちらの記事](https://qiita.com/larking95/items/70d24ac9a78d56d53dff)をご覧ください。(ステマ)

# Python関数の利用
## Python関数を呼び出そう
ここからはMTALABからPythonの関数を呼び出してみます。
MATLABからPython関数を呼び出す方法はざっくり下記の3通りの方法が提供されています。

1. `py.`接頭辞を付記して直接アクセスする方法
1. `pyrun()`関数を用いる方法
1. `pyrunfile()`関数を用いる方法

順にみていきます。

### py. 接頭辞を用いる方法
この方法は従来(2021a以前)から提供されている方法です。
[公式ドキュメント](https://jp.mathworks.com/help/matlab/call-python-libraries.html)によれば、下記のように使います。
> Python 標準ライブラリ内のコンテンツを呼び出すには、Python の関数名またはクラス名の前に py. を追加します。使用可能なモジュールのコンテンツを呼び出すには、Python モジュール名の前に py. を追加し、その後に Python の関数名またはクラス名を続けます。以下に例を示します。

例えば、下記のようにPythonのprint関数やhelp関数を利用できます。
簡易的にPythonのヘルプが見たい場合は便利な記法です。

```matlab:
py.print("Hello world!")
% Hello world!

py.help("numpy.array")
% Help on built-in function array in numpy:
% 
% numpy.array = array(...)
%     array(object, dtype=None, *, copy=True, order='K', subok=False, ndmin=0,
%           like=None)
%     
%    === 以下省略 ===
```
Pythonパッケージをimportする場合は下記の構文が使えます。
一度インポートすれば、py.接頭辞なしでPython関数を呼び出せます。

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

py.print(np.array(2))
% 2.0
```

ただし、この方法ではMATLABの文法の影響を強く受けますのでPythonとしては書きずらい印象です。

### `pyrun()`関数を用いる方法
この方法は最近追加された方法です。(2021bから追加されました)

[公式ドキュメント](https://jp.mathworks.com/help/matlab/ref/pyrun.html#mw_d3bdafec-4b59-4542-be7f-4940eaac47fa)によれば、記法は下記のようになります。
> pyrun(code)
outvars = pyrun(code,outputs)
outvars = pyrun(code,outputs,pyName=pyValue)

例えば、下記のようにPythonのprint関数やヘルプ関数を使用できます。

```matlab
pyrun('print("Hello world!")')
% Hello world!

pyrun('help("sklearn")')
% Help on package sklearn:
% 
% NAME
%     sklearn
% 
% DESCRIPTION
%     Machine learning module for Python
%     
%  以下省略
```

インポートする場合の例は下記のようになります。

:::note
インポートや変数などはMATLABのワークスペースとは独立にpyrun環境内で残り続けます。
一度インポートすればMATLABを終了するまでインポートをやり直さなくても大丈夫です。
:::

```matlab
pyrun("import numpy as np");
pyrun("print(np.array(2))")
% 2
```

ここで注目していただきたいのは、**1つ目の方法では`2.0`と表示された箇所がこの方法では`2`と表示された**点です。
これはMATLABとPythonで`2`と即値で数値を書いた時の型が異なるためです。
下記のコード例から分かるようにMATLABでは浮動小数点として解釈されますが、Pythonでは整数型として解釈されています。

```matlab
% 1つ目の方法では浮動小数点型で解釈される
py.print(py.type(2))
% <class 'float'>

% 2つ目の方法では整数型で解釈される
pyrun("print(type(2))")
% <class 'int'>
```

そのため、この方法のほうがPythonとしては書きやすいと思います。
例えば下記のようなコードが許されます。
(ただし、MATLABが文字列として解釈できるように記述する必要があります。)

```matlab
% NG (float型は受け付けない)
py.print(py.numpy.zeros([2,5]))
% Python エラー: TypeError: 'float' object cannot be interpreted as an integer

% OK
pyrun("print(np.zeros([2,5]))")
% [[0. 0. 0. 0. 0.]
%  [0. 0. 0. 0. 0.]]

% NG (string型はクラスのように使えない)
py.print("Hello Hello"*2 + "{0}!".format("World"))
%  py.print("Hello Hello"*2 + "{0}!".format("World"))
%                                   ↑
% 演算子の使用が無効です。

% OK
pyrun('print("Hello Hello "*2 + "{0}!".format("World"))')
% Hello Hello Hello Hello World!
```

### `pyrunfile()`関数を用いる方法
この方法も最近追加された方法です。(2021bから追加されました)

[公式ドキュメント](https://jp.mathworks.com/help/matlab/ref/pyrunfile.html)によれば、記法は下記のようになります。

> pyrunfile(file)
pyrunfile(file input)
outvars = pyrunfile(file,outputs)
outvars = pyrunfile(file,outputs,pyName=pyValue)

この方法ではpythonファイルごと実行することになります。
ただし、今回は前処理と後処理をMATLABでやる前提でしたので、ファイルごと実行したいことはないと思います。
そのため、コード例は省略しますので公式ドキュメントをご覧ください。

## Pythonとのデータの受け渡し
ここからはMATLABから行列などを渡して、Pythonから結果を受け取る方法についてみていきます。

従来はMATLABとPython間で行列を受け渡すためにはセル配列を経由する必要がありました。([2年前の記事参照](https://qiita.com/larking95/items/70d24ac9a78d56d53dff#%E8%A1%8C%E5%88%97%E3%81%AE%E6%B8%A1%E3%81%97%E6%96%B9matlab-to-ndarray))
しかし、2021b時点ではこの変換は不要となっています。

例えば2年前の記事でエラーとなっていた下記のコードが実行可能となっています。[^goodImp]

[^goodImp]: こうやってバージョンごとに確実に良くなっていくのはMATLABのいいところですね

```matlab:行列の渡し方1(MATLAB_to_ndarray)
matrix = py.numpy.array(rand(2));

% matrix = 
% 
%   Python ndarray:
% 
%     0.6557    0.8491
%     0.0357    0.9340
% 
%     Python オブジェクトのプロパティを表示するには、関数 details を使用します。
% 
%     MATLAB 配列に変換するには、関数 double を使用します。
```

```matlab:行列の受け取り方1(ndarray_to_MATLAB)
n_mat = py.numpy.average(pyargs('a', rand(2), 'axis', py.int(0)));  % 何らかの処理結果
double(n_mat)    % matlabの行列へ変換したい

% ans =
% 
%     0.4133    0.3689
```

`pyrun()`関数を使って同じことをやってみると下記のようになります。
Pythonっぽい書き方になってすっきりした印象です。

```matlab:行列の渡し方2(MATLAB_to_ndarray)
matrix = pyrun("matrix=np.array(A)", "matrix", A = rand(2))

% matrix = 
% 
%   Python ndarray:
% 
%     0.6551    0.1190
%     0.1626    0.4984
% 
%     Python オブジェクトのプロパティを表示するには、関数 details を使用します。
% 
%     MATLAB 配列に変換するには、関数 double を使用します。
```

```matlab:行列の受け取り方2(ndarray_to_MATLAB)
n_mat = pyrun("n_mat = np.average(a=A, axis=0)", "n_mat", A=rand(2));
double(n_mat)

% ans =
% 
%     0.9251    0.3429
```

ここで`pyrun()`関数の呼び方が少し変わったのに気づかれましたか？
helpでいうところの3つ目の構文を使うことで、Python関数にMATLAB変数を渡して処理した後、MATLAB変数として受け取っています。[^argumentPair]

[^argumentPair]: `引数名=値` の構文(`Name=Value`)がサポートされたのが最近だったので、最新機能もりもりの関数ですね。(2021aあたり？)

```matlab
help("pyrun")
%  pyrun - Run Python statements from MATLAB
%     This MATLAB function executes the Python statements in code in the
%     Python interpreter.
% 
%     pyrun(code)
%     outvars = pyrun(code,outputs)
%     outvars = pyrun(code,outputs,pyName=pyValue)
% 
%     See also pyrunfile
```

このように2021bでは`pyrun()`関数を使って簡単にMATLABとPython間でデータのやり取りを行うことができます。

# 応用例(scikit-learn)
最後に応用例として、scikit-learnをMATLABから使ってみます。
Scikit-learnの公式ページのExampleからいくつか試してみましたので、参考になれば幸いです。

## 例1: k-means++の初期化
まずは簡単な例からやってみます。
[An example of K-Means++ initialization](https://scikit-learn.org/stable/auto_examples/cluster/plot_kmeans_plusplus.html#sphx-glr-auto-examples-cluster-plot-kmeans-plusplus-py)というデモを、MATLABから実施すると下記のようなコードになります。
実行すると、Exampleとまったく同じグラフを表示することができます。

```matlab:kmeans_initialization.m
% MATLABからのscikit-learnの実行例1
% Scikit-Learnのkmeans++の初期化例をMATLAB連携で実施する
% 参考：An example of K-Means++ initialization
% https://scikit-learn.org/stable/auto_examples/cluster/plot_kmeans_plusplus.html#sphx-glr-auto-examples-cluster-plot-kmeans-plusplus-py
% 
% Copyright (c) 2021 larking95(https://qiita.com/larking95)                
% Released under the MIT Licence                
% https://opensource.org/licenses/mit-license.php

%% Pythonの準備
python_imports = [...
    "from sklearn.cluster import kmeans_plusplus",...
    "from sklearn.datasets import make_blobs"
    ];
pyrun(python_imports);

%% サンプルデータの準備
n_samples       = 4000;     % サンプル数
n_components    = 4;        % 分類数

[X, y_true] = pyrun("X, y_true = make_blobs( n_samples=n_samples, centers=n_components, cluster_std=0.60, random_state=0)",...
    ["X", "y_true"],...
    n_samples=py.int(n_samples), n_components=py.int(n_components));

X = pyrun("X = X[:, ::-1]", "X");

%% kmeans++の実行
[centors_init, indices] = pyrun("centers_init, indices = kmeans_plusplus(X, n_clusters=4, random_state=0)", ...
    ["centers_init", "indices"]);

%% MATLAB形式への変換
X               = double(py.numpy.array(X));    % なぜか変換できなかったのでarrayに変換をはさんだ
y_true          = double(y_true);
centors_init    = double(centors_init);
indices         = double(indices);

%% グラフのプロット
colors = ["#4EACC5", "#FF9C34", "#4E9A06", "m"];

fig = figure();

% 教師データの表示
for k = 1:n_components
    scatter(X(y_true==(k-1), 1), X(y_true==(k-1), 2), marker=".", MarkerEdgeColor=colors(k));
    hold on;
end

% 初期のクラスタ重心の表示
scatter(centors_init(:,1), centors_init(:,2), 'filled', "ob");

% 見栄えを整える
title("K-Means++ 初期化");
xticks([]);
yticks([]);
```

## 例2: 画像分割のためのスペクトルクラスタリング
次はきちんと学習をさせるコードにしてみましょう。
[Spectral clustering for image segmentation](https://scikit-learn.org/stable/auto_examples/cluster/plot_segmentation_toy.html#sphx-glr-auto-examples-cluster-plot-segmentation-toy-py)というデモを、MATLABから実施すると下記のようなコードになります。
こちらも実行すると、Exampleとほとんど同じグラフを表示することができます。

```matlab:SpectralClustering.m
% MATLABからのscikit-learnの実行例2
% Scikit-LearnのスペクトルクラスタリングをMATLAB連携で実施する
% 参考： Spectral clustering for image segmentation
% https://scikit-learn.org/stable/auto_examples/cluster/plot_segmentation_toy.html#sphx-glr-auto-examples-cluster-plot-segmentation-toy-py
% 
% Copyright (c) 2021 larking95(https://qiita.com/larking95)                
% Released under the MIT Licence                
% https://opensource.org/licenses/mit-license.php

%% Pythonの準備
python_imports = [...
    "import numpy as np",...
    "from sklearn.feature_extraction import image",...
    "from sklearn.cluster import spectral_clustering"
    ];
pyrun(python_imports);

%% サンプルデータの準備
[x, y] = meshgrid(1:100);

center1 = [28, 24];
center2 = [40, 50];
radius1 = 16;
radius2 = 14;

% 円内部であればTrueとなる論理配列を作成しておく
circle1 = (x-center1(1)).^2 +(y-center1(2)).^2 < radius1.^2;
circle2 = (x-center2(1)).^2 +(y-center2(2)).^2 < radius2.^2;

% ノイズで乱した画像に加工する
mask    = circle1 | circle2;
img     = double(mask);
img = img + 1 + 0.2.* randn(size(img));

% scikit-learnのgraphに変換する
pyrun("img=img", img=py.numpy.array(img) );                 % 変数imgをpython側に宣言する
pyrun("mask=mask.astype('bool')", mask=py.numpy.array(mask));  % 変数maskをpython側に宣言する
pyrun("graph = image.img_to_graph(img, mask=mask)" );
pyrun("graph.data = np.exp(-graph.data / graph.data.std())");

%% 学習の実行
% スペクトルクラスタリングの実行
pyrun("labels = spectral_clustering(graph, n_clusters=2, eigen_solver='arpack')",...
    "labels");
pyrun("label_im = np.full(mask.shape, -1.0)");
label_im = pyrun("label_im[mask] = labels", "label_im");  % この行はPython側で実行する必要がある

% 結果をMATLAB形式に変換する
label_im = double(label_im);

%% グラフのプロット
fig = figure();
s1 = pcolor(img);
s1.EdgeColor = 'none';
axis square;
axis ij;

fig2 = figure;
s2 = pcolor(label_im);
s2.EdgeColor = 'none';
axis square;
axis ij;

```

# おわりに
今回はMATLABからPythonを利用する方法について、簡単にまとめてみました。
MATLABの進化でより使いやすくなりましたし、scikit-learnも使うことができるようになったので嬉しいですね。
同じようなことがしたい方がもしいらっしゃれば、この記事があなたの助けになれば幸いです。

## ちなみに
本記事はMATLABアドベントカレンダーの12日目です。
投稿日を見ていただくとわかる通り、完全に遅刻でございます。
遅れてしまい申し訳ございませんでした・・・。
~~なんとかクリスマスまでに間に合ったので許してください!!~~

# 参考文献
1. [MATLABからPythonライブラリを利用したかった話](https://qiita.com/larking95/items/70d24ac9a78d56d53dff)
    * 私が2年前に執筆した記事で、本記事のベースとなっています。
1. [MATLAB の Python ライブラリ](https://jp.mathworks.com/help/matlab/call-python-libraries.html)
    * Mathworks様の公式ドキュメントのPython呼び出し関係のトップページです。
1. [Calling Python from MATLAB](https://jp.mathworks.com/help/matlab/call-python-libraries.html?lang=en)
    * 2.と同じMathworks様の公式ドキュメントの英語版です。
    * 2021bから追加された機能について未翻訳でしたので参考にしました。(`pyrun()`と`pyrunfile()`)