MATLABで条件やパラメータを変えながら実験を繰り返す際の結果のデータベース化

# 本記事の目的
研究をしていると「入力データの生成条件」や「アルゴリズムのパラメータ」を様々な状態に変えながら実験結果（アルゴリズムの出力）を収集する機会が多くあります．例えば最尤推定（maximum likelihood estimation）であれば，データの観測条件やサイズ等が「入力データの生成条件」となり，推定アルゴリズムが最急降下法（steepest descent）であればステップサイズパラメータが「アルゴリズムのパラメータ」として存在します．そのような「条件をいろいろと変更した際の結果」は時として膨大となり，それをどのように収集・管理するか，あるいは解析・可視化するかは個人の経験や趣向に大いに依存すると思います．
本記事では，上記の「実験データの収集・管理」をMATLABとExcelでサクッと実現する一つの方法について簡単に解説します．当然，この記事の方法は利点も欠点もあるので，あくまで参考としていただければと思います．むしろ，今までそのような実験データの収集・管理をしたことのない方々（初めて研究室に配属されて研究を始める学部生や修士学生等）の参考になればと思っています．

# 方法概要
## 本記事の対象
本記事では，MATLABで何らかの実験をすることを前提としています．Python等でも方法論は共通なので応用可能だと思いますが，実装においてMATLABの「table変数」を活用していますので，そこは各言語で用意されている代替の方法で実装する必要があります．

## 本記事で作成するエクセルデータベース
本記事では，下図のような「実験条件と実験結果」を並べたエクセルのファイルを作成します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600298/23648f49-16b8-70d2-2765-280fb9f25802.png)
ここで，A列～F列までがそれぞれ入力データの生成条件（cond1, cond2），アルゴリズムのパラメータ（param1, param2, param3），乱数シード値（seed）に対応しています．さらに，この条件で何らかの実験アルゴリズムを実行した際の結果（result1, result2）がG列とH列に保存されています．つまり，このエクセルのファイルは（1行目は各列の名称なので）2行目以降の各行が「ある条件で実行した際の結果」を格納しており，データベース化されています．

## なぜエクセルデータベースか
データベース化されたエクセルファイルは，下のgifのように「テーブルとして書式設定」を実行することで，各要素での並び替えやフィルタリングが容易にでき，膨大な数の実験データを素早く解析できます．
![test1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600298/6246d55d-68b5-d401-9d07-936573f3ca12.gif)
さらに，下のgifのようにピボットテーブルやピボットグラフを活用することで，可視化も容易にできます．
![test2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600298/37e5f4b5-fb2a-b13b-b156-2af6d47b3231.gif)
このようにエクセルのデータベースとしての機能を活用することで，実験データの管理と解析（可視化）の負担がそれなりに軽減されます．この機能を活用するためには，前述の「実験条件と実験結果」を並べたエクセルファイルを作成する必要があります．

## MATLABでのデータベース生成
MATLABでは「table」という型の変数がR2013bからサポートされています．
table変数：https://jp.mathworks.com/help/matlab/tables.html
この変数は，異なる型（int，double，string等）を要素として持つことができる名前付きの列で構成された表形式の配列です．「異なる型の要素を許す」という面ではcell配列とよく似ていますが，「名前付きの列で構成」及び「表形式」の2点がcell配列と大きく異なります．
cell配列：https://jp.mathworks.com/help/matlab/cell-arrays.html

実際にワークペースのtable配列をMATLABのIDEで確認してみると，下記のような配列になっています．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600298/27a344b5-62ac-d3a4-adc0-3a96c4b9993a.png)
確かに，「各列に名前がついている」，「異なる型の要素を持つ」，「表形式になっている」の3点が確認できます．表形式なので，この画面でも昇順・降順の並び替えくらいはできます．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600298/51185017-fc6e-52d3-3eb8-004488d1d5ff.png)

というか，もうお気づきかと思いますが，これはもはや先ほどの「エクセルデータベース」と全く同じ構成になっています．従って，「MATLAB上で様々な条件で実験し，条件と結果を一緒にtable変数に格納していく」ことで，上記のようなデータベースを容易に作成できます．これが本記事の主題になります．

# 具体的な実装例
## ディレクトリ（フォルダ）構成
いま，「project」という名前のディレクトリの中に「main.m」という実験スクリプトのメインファイルと，「sampleExperiment.m」というアルゴリズム部分を関数化したファイルの2つがある状況を考えます．また，「output」という空のディレクトリも用意しておきます．即ち，下記のような構成です．
project/
　├ output/
　├ main.m
　└ sampleExperiment.m
但し，後述しますが「output」は無かった場合MATLAB側で生成しますので，下記のように「main.m」と「sampleExperiment.m」の2つでも構いません．
project/
　├ main.m
　└ sampleExperiment.m
ディレクトリの「output」の中に，データベース化されたエクセルファイルを出力するようになっています．

## MATLABスクリプト
前章の主題を実現するMATLABスクリプト例（main.m）とアルゴリズム例を下記に示します．

```matlab:main.m
clear; % ワークスペースの全変数をクリア
close all; % 全プロットフィギュアウィンドウを閉じる
clc; % コマンドラインをクリア

% 出力ディレクトリ名とエクセルファイル名
outDirPath = "./output/"; % 結果を保存するディレクトリのパス
excelFileName = "result.xlsx"; % 結果を保存するエクセルファイル名
excelFilePath = outDirPath + excelFileName; % エクセルファイルのパス（outDir内のexcelFileName）

% 結果保存用のテーブル変数の定義
isExcelFile = exist(excelFilePath, 'file'); % エクセルファイルが存在するかしないか判定（存在すれば2，無ければ0）
if ~isfolder(outDirPath) % outDirが存在しない場合
    mkdir(outDirPath); % 出力ディレクトリの作成
    resultTable = []; % 空のtable変数を定義
elseif isExcelFile ~= 2 % outDirは存在するがexcelFileNameの名前のエクセルファイルが存在しない場合
    resultTable = []; % 空のtable変数を定義
else % outDirが存在しexcelFileNameの名前のエクセルファイルも存在する場合
    resultTable = readtable(excelFilePath); % エクセルファイルを読み込んでtable変数に代入
end

% 入力データの条件やアルゴリズムのパラメータ等の設定
cond1 = 1; % 条件1
cond2 = 5; % 条件2
param1 = 0.5; % パラメータ1
param2 = 0.25; % パラメータ2
param3 = "max"; % パラメータ3
seed = 10; % 乱数シード値（アルゴリズム中で乱数を用いる場合に再現性を確保するため）

% 乱数ストリームの初期化
RandStream.setGlobalStream(RandStream('mt19937ar','Seed',seed)); % seedの値を使って疑似乱数列を生成（疑似乱数はメルセンヌツイスタ）

% 実験例
inputData = magic(cond1) + cond2; % 条件cond1及びcond2に従って生成した入力データ例
[result1, result2] = sampleExperiment(inputData,param1,param2,param3); % 実験アルゴリズムの関数例

% データベースの更新と保存
resultTable = [resultTable; table(cond1, cond2, param1, param2, param3, seed, result1, result2)]; % table変数に実行結果を1行追加し保存
writetable(resultTable, excelFilePath); % エクセルファイルを上書き
```

```matlab:sampleExperiment.m
function [output1,output2] = sampleExperiment(data,param1,param2,param3)
% 
% sampleExperiment: sample function for experiment with input parameters
%
% [syntax]
%   [output1,output2] = sampleExperiment(data,param1,param2,param3)
%
% [inputs]
%      data: input data
%    param1: parameter 1 (scalar)
%    param2: parameter 2 (scalar)
%    param3: parameter 3 (string, "max" or "min")
%
% [outputs]
%   output1: result of algorithm
%   output2: result of algorithm
%

random = param2 * randn(size(data)) + param1;
randData = data + random;

if strcmp(param3, "max")
    output1 = max(randData(:));
elseif strcmp(param3, "min")
    output1 = min(randData(:));
else
    error("The input param3 is not supported.\n");
end
output2 = median(randData(:));
```

「main.m」で何をやっているかが重要ですので，下記に解説していきます．
まず，下記はMATLABにおけるおまじないです．全変数をクリアし，ウィンドウを閉じ，コマンドラインをクリアします．

```matlab
clear; % ワークスペースの全変数をクリア
close all; % 全プロットフィギュアウィンドウを閉じる
clc; % コマンドラインをクリア
```

続いて，下記は出力ディレクトリ名とエクセルファイル名をstring型の変数に代入し，カレントディレクトリからエクセルファイルに到達するためのパスを定義しています．余談ですが，MATLABでは「'abc'」だとchar型の配列となり，「"abc"」だとstring型の変数となります．

```matlab
% 出力ディレクトリ名とエクセルファイル名
outDirPath = "./output/"; % 結果を保存するディレクトリのパス
excelFileName = "result.xlsx"; % 結果を保存するエクセルファイル名
excelFilePath = outDirPath + excelFileName; % エクセルファイルのパス（outDir内のexcelFileName）
```

次の部分は，現在「output」というディレクトリが存在するか否か，存在するならばその中に「result.xlsx」というファイルが存在するか否かを判定しています．まずisfolderでディレクトリの有無を判定し，なければmkdirでディレクトリを作成しています．さらに，table変数であるresultTableを空で定義しています．
もし「output」というディレクトリが存在する場合は，existで判定しておいた「result.xlsx」というエクセルファイルの有無に従って，「エクセルファイルが無ければresultTableは空で定義」，「エクセルファイルがあればそれを読み込んで，中身をresultTableに代入」という処理をしています．もしあらかじめエクセルファイルが存在していた場合は，この後新たに実験した結果を「既存のエクセルファイル（データベース）に追加」します．

```matlab
% 結果保存用のテーブル変数の定義
isExcelFile = exist(excelFilePath, 'file'); % エクセルファイルが存在するかしないか判定（存在すれば2，無ければ0）
if ~isfolder(outDirPath) % outDirが存在しない場合
    mkdir(outDirPath); % 出力ディレクトリの作成
    resultTable = []; % 空のtable変数を定義
elseif isExcelFile ~= 2 % outDirは存在するがexcelFileNameの名前のエクセルファイルが存在しない場合
    resultTable = []; % 空のtable変数を定義
else % outDirが存在しexcelFileNameの名前のエクセルファイルも存在する場合
    resultTable = readtable(excelFilePath); % エクセルファイルを読み込んでtable変数に代入
end
```

下記は「入力データの生成条件」や「アルゴリズムのパラメータ」を設定しています．ここは適宜皆さんの実験内容に合わせて修正してください．また，アルゴリズム内の変数の初期化等で乱数を用いている場合，そのままでは再現性が確保できなくなる（乱数によって結果が変わるので同じ実験が二度とできなくなる）ので，乱数のシード値をパラメータ（seed）として設定し，乱数ストリームを初期化しています．これによって，再現性が担保されます．

```matlab
% 入力データの条件やアルゴリズムのパラメータ等の設定
cond1 = 1; % 条件1
cond2 = 5; % 条件2
param1 = 0.5; % パラメータ1
param2 = 0.25; % パラメータ2
param3 = "max"; % パラメータ3
seed = 10; % 乱数シード値（アルゴリズム中で乱数を用いる場合に再現性を確保するため）

% 乱数ストリームの初期化
RandStream.setGlobalStream(RandStream('mt19937ar','Seed',seed)); % seedの値を使って疑似乱数列を生成（疑似乱数はメルセンヌツイスタ）
```

次の処理は，定義された条件やパラメータを用いた実験です．例なので入力データは適当にmagicを使って生成したデータとしています．また，「sampleExperiment」の中身も特に意味のない処理をしてresult1とresult2という2つの値を返しています．例えば認識タスクであれば，認識率やf値等を返せばよいですし，他のタスクであれば性能を表す客観評価指標を返せばOKです．

```matlab
% 実験例
inputData = magic(cond1) + cond2; % 条件cond1及びcond2に従って生成した入力データ例
[result1, result2] = sampleExperiment(inputData,param1,param2,param3); % 実験アルゴリズムの関数例
```

最後の処理では，実験結果をresultTableに格納し，エクセルファイルとして保存しています．cond1からreuslt2までをtable変数にキャストして，resultTableの最終行に追加しています．

```matlab
% データベースの更新と保存
resultTable = [resultTable; table(cond1, cond2, param1, param2, param3, seed, result1, result2)]; % table変数に実行結果を1行追加し保存
writetable(resultTable, excelFilePath); % エクセルファイルを上書き
```

以上より，「MATLAB上で様々な条件で実験し，条件と結果を一緒にtable変数に格納していく」ことができます．さらに，条件値を変更して再度「main.m」を実行すれば，過去の実験結果が格納されているデータベース（result.xlsx）に新しい実験結果が追加されて上書き保存されます．

## 注意点
注意点として，「データベース上に既に格納されているものと全く同じ実験条件を再度実行しようとした場合は中止する」という機能はありません．実装しても良かったのですが，一度読み込んだエクセルファイルの全行と新しく行う実験の条件を比較する必要があるので，煩雑になるかと思い省きました．結果の値の偏差等をデータベースから計算する際などは，もし「全く同じ実験結果」が複数あると母集団が変わってきますので，省いておかないと誤った解析結果となってしまします．注意してください．

## 膨大な条件・パラメータでまとめて実験する例
パラメータの探索等で試行回数が膨大になるような実験のデータベース化は，「main.m」を下記のように編集すれば実現できます．

```matlab:main.m
clear; % ワークスペースの全変数をクリア
close all; % 全プロットフィギュアウィンドウを閉じる
clc; % コマンドラインをクリア

% 出力ディレクトリ名とエクセルファイル名
outDirPath = "./output/"; % 結果を保存するディレクトリのパス
excelFileName = "result.xlsx"; % 結果を保存するエクセルファイル名
excelFilePath = outDirPath + excelFileName; % エクセルファイルのパス（outDir内のexcelFileName）

% 結果保存用のテーブル変数の定義
isExcelFile = exist(excelFilePath, 'file'); % エクセルファイルが存在するかしないか判定（存在すれば2，無ければ0）
if ~isfolder(outDirPath) % outDirが存在しない場合
    mkdir(outDirPath); % 出力ディレクトリの作成
    resultTable = []; % 空のtable変数を定義
elseif isExcelFile ~= 2 % outDirは存在するがexcelFileNameの名前のエクセルファイルが存在しない場合
    resultTable = []; % 空のtable変数を定義
else % outDirが存在しexcelFileNameの名前のエクセルファイルも存在する場合
    resultTable = readtable(excelFilePath); % エクセルファイルを読み込んでtable変数に代入
end

% 入力データの条件やアルゴリズムのパラメータ等の設定
cond1All = 1:5; % 条件1
cond2All = 1:5; % 条件2
param1All = 0.5:0.5:2.5; % パラメータ1
param2All = 0.25:0.25:1; % パラメータ2
param3All = ["max", "min"]; % パラメータ3
seedAll = 1:10; % 乱数シード値（アルゴリズム中で乱数を用いる場合に再現性を確保するため）

% 網羅的に調査する入力データの条件やアルゴリズムのパラメータ等の設定
for cond1 = cond1All
    for cond2 = cond2All
        for param1 = param1All
            for param2 = param2All
                for param3 = param3All
                    for seed = seedAll
                        % 乱数ストリームの初期化
                        RandStream.setGlobalStream(RandStream('mt19937ar','Seed',seed)); % seedの値を使って疑似乱数列を生成（疑似乱数はメルセンヌツイスタ）
                        
                        % 実験例
                        inputData = magic(cond1) + cond2; % 条件cond1及びcond2に従って生成した入力データ例
                        [result1, result2] = sampleExperiment(inputData,param1,param2,param3); % 実験アルゴリズムの関数例
                        
                        % データベースの更新
                        resultTable = [resultTable; table(cond1, cond2, param1, param2, param3, seed, result1, result2)]; % table変数に実行結果を1行追加し保存
                    end
                end
            end
        end
    end
end

% データベースの保存
writetable(resultTable, excelFilePath); % エクセルファイルを上書き
```

御覧の通りですが，条件やパラメータをfor文でループしています．ループ内ではresultTableには新しい実験結果を毎回格納しています．resultTableの事前メモリ確保ができていないのでMATLABエディタではWarningが出ますが，それほど問題にはならないと思います．また，readtableとwritetableは遅いので，ループの外に配置し，複数の実験結果をまとめて実行するようにしています．もし数日かかるような実験の場合は，小刻みにwritetableをしておかないと，計算機が落雷等で予期せず停止した場合に全て失われることになりますので，その点は注意が必要です．
また，reulstTable以外の変数は全て毎回の実験で新たに定義されることを前提としています．そうでない場合は，ループの外側で定義されるメタ的な変数以外を全てクリアしておく必要があります．例えば下記の1行を一番内側のfor文の直下に記載すればOKです．

```matlab
clearvars -except outDirPath excelFileName excelFilePath isExcelFile resultTable cond1 cond2 param1 param2 param3 seed
```

## GitHubでの公開
もしこの方法を利用される場合は，上記のスクリプトをコピペしても良いですが，GitHubにリポジトリを公開していますので，そちらからダウンロードしていただく方が早いです．
URL：https://github.com/d-kitamura/experimentTemplate

# 終わりに
この方法で膨大な条件の実験結果を楽に収集・管理・解析できると思います．ここ数年，上記の方法で研究してきて特に問題はありませんでした．もしより良い方法があれば，ぜひコメントをいただければと思います．とくに，条件やパラメータのループを書くのが億劫なので，ここら辺をもう少しスマートにできないか模索中です．皆さんの参考になれば幸いです．

# 追記
もちろん，MATLABのtable変数のまま解析・可視化をするのも良いと思います．例えばヒートマップを作成したり，ボックスプロットを作成する場合はエクセルではしんどいので，MATLABのtable変数のために用意された様々な関数をそのままresultTableに適用する方が早くて便利です．