【MATLAB】超便利!デバッグ機能のおさらい


この記事は、MATLAB/Simulink Advent Calendar 2021(カレンダー1)の20日目の記事として書かれています。

この記事で言いたいこと

dbstop if errorまじすごい

はじめに

みなさんプログラミングしてますか。してますよね。ここはQiitaですもんね。プログラミングにつきものと言えばバグですね。今までバグ出したことないプログラマなどいないと断言できますよね。プログラミングあるところにバグあり、プログラミングにとって生涯の伴侶ともいえる存在がバグです。

どんなに頑張ってもバグは出てきます。なので大事なのはバグを出さないことではなく、バグの首根っこをいち早く見つけることだと思っています。

私が普段使っているMATLABには標準でデバッグのための機能がいくつか(も?)搭載されていますが、案外使いこなせている人は少ないのではないかと思ったので、今回この機会をいただいてデバッグ機能について書かせていただこうかと思いました。というかdbstop if errorの機能を今年知って感動すらしたので、いろんな人に広めたい、と思っただけです。

デバッグの仕方超基礎編

まずデバッグの仕方の基本的なところについて念のため書いておきます。プログラミング始めたばかりの方には役に立つ情報かと思いますが、プログラミング歴1か月以上の方はもうすでに気付いていることかと思うので、このセクションは読み飛ばしていただいて構いません。

エラーメッセージをよく読もう!

もう正直ここに尽きますね。プログラム実行したと思ったらコマンドウィンドウに出てくる血塗られたような真っ赤なメッセージ、それをエラーメッセージと(私は)呼んでいます。

例えば次のコマンドを入力するとエラーメッセージが出てきます。

Code
x = y;
Output
関数または変数 'y' が認識されません。

関数または変数'y'が認識されません。

・・・そりゃそうですね、yなんて変数作っていないのでそういうメッセージが出ました。エラーメッセージって出てくると正直「クソっ…!バグあったか!」って思いますが、よく考えてみてください。メッセージがこんなんだったらどうでしょう。

Output
「エラー発生。プログラムを停止しました。」

絶望ですよね。どう対処して良いか全くわかりません。

こう考えるとエラーメッセージって、未熟な私たちを優しく導こうとしてくれる先生みたいなもんですよね。とてもありがたい。

エラーメッセージは上から(下から)読むべし!

え、どっちだよ!って声が聞こえてきますが、出てくる順番の規則がわかっていればどっちでもいいです…。

これを理解するために少し現実的なエラーメッセージの例を出してみます。MATLAB scriptとして次のコードをdebug_sample1.mとして保存して実行してみます。

debug_sample1.m
img = imread('pout.tif');
gImg = convert_img_to_gray(img);
imshow(gImg);

function gImg = convert_img_to_gray(img)
    gImg = rgb2gray(img);
end
Code
debug_sample1
Output
エラー: rgb2gray>parse_inputs (行 79)
MAP は m 行 3 列の配列でなければなりません。RGB およびグレースケールのイメージについては im2gray を使用してください。

エラー: rgb2gray (行 51)
isRGB = parse_inputs(X);

エラー: debug_sample1>convert_img_to_gray (行 6)
    gImg = rgb2gray(img);

エラー: debug_sample1 (行 2)
gImg = convert_img_to_gray(img);

さっきより大量のエラーメッセージが出てきますね。プログラミング初心者ですと、「なんかエラー出た!コマンドたくさん出てきてるし、よくわかんない!」となる人もいるようです。エラーメッセージの読み方にも作法があるので、落ち着いて読めばMATLABくんが言いたいことは簡単なのですが、彼の言いたいことを理解するためにはプログラムの実行順序を理解している必要があります。そのためには最低限MATLAB scriptとMATLAB functionの違いについて理解しておいた方がいいです。

scriptは、コマンドの羅列になっていて単純に上から順々に書かれているコードを実行していきます。

function(関数)も同じなのですが、functionはscriptから呼び出されたり他のfunctionから呼び出されて使用されます。これが大きな違いです。

functionにはユーザが作ったオリジナルのものとMathWorks社(MATLABの開発元)が作ったものと両方ありますが、まぁどっちも扱いとしては同じです。

functionは一つの関数として一つのmファイルに記述される(関数)こともありますし、scriptの後に書かれる(ローカル関数)こともあります。一つのmファイルに記述する場合はfunction名はファイル名とイコールにしなければいけない、という決まりがあります。これは他のファイルから呼び出したときに見つけられるように、ということですね。なのでscriptの後に書くローカル関数の形では、他のscriptやfunctionから呼び出すことはできません。

前置きが長くなりましたが、以上のことを踏まえてdebug_sample1.mのコードの実行順序をコメントで書くと次のようになります。

debug_sample1.m
img = imread('pout.tif'); % 1
gImg = convert_img_to_gray(img); % 2
imshow(gImg); % 3

function gImg = convert_img_to_gray(img)
    gImg = rgb2gray(img); % 2-1
end

関数内は枝番としています。上で書いたようにimreadやrgb2grayもMathWorks社が提供している関数なので、実は1-1や2-1-1も内部的に存在していることに注意してください。

さて、ここでやっと本題に戻ってきますが、このようにscriptの中にfunctionが含まれている一般的なコード構成でエラーが発生し、エラーメッセージが吐き出されるとき、MATLABくんの中では何が起きているのでしょうか。MATLABくんはとっても優しいので、コード実行中にエラーが発生すると何が起きたかを教えてくれようとします。例えばエラーメッセージを表示してくれますね。このようなエラー発生時に実行される処理はエラー処理と言います。そのまんまです。

debug_sample1.mのケースでエラーとなる原因はrgb2grayにグレースケール画像を入力していることにあります。rgb2grayはRGB画像をグレースケールに変換する関数なので、その関数にグレースケールを入力すると「RGB画像を入れてくれよ!使い方が違うよ!」と教えてくれます。

実行順序について考えてみます。

1を実行 → 問題なし → 2を実行 → これは関数だ。関数内のコマンドを実行しよう → 2-1を実行 → rgb2grayにグレースケールが入力されている!エラーです!

という流れになります。このときエラー処理が走ります。rgb2grayで発生したエラーなので、上記のような問題点について教えてくれるようなエラーメッセージが出力されることになります。

では、そのあとはどうなるのでしょうか。思い出してください。MATLABくんはものすごく親切なんです。rgb2grayはconvert_img_to_grayというローカル関数の中に書かれているコードですね。そしてそこでエラーが発生しました。なので「convert_img_to_gray関数の中でエラーが発生したよ!」と教えてくれるんです。そうです、convert_img_to_gray関数のためのエラー処理が実行されエラーメッセージが出ます。

では次はどうなるでしょう?そうですね、もうわかりますね。convert_img_to_gray関数はdebug_sample1.mというscriptの中にあるので「debug_sample1.mでエラーが発生したよ!」と教えるためにdebug_sample1.mのためのエラー処理が実行されエラーメッセージが出ます。

くどいですがまとめると次の順序でエラー処理が走ります。

  1. rgb2gray関数でエラー発生! → エラー処理 → エラーメッセージ
  2. convert_img_to_gray関数でエラー発生! → エラー処理 → エラーメッセージ
  3. debug_sample1.mでエラー発生 → エラー処理 → エラーメッセージ

むっちゃ親切…。ですが、以上を踏まえてエラーメッセージを見返してみましょう。

まず「エラー:」で始まる行が4つありますね。これは4回エラー処理が走ったということです。

1番目のエラーはrgb2gray関数の中の関数で起きています。rgb2gray>parse_inputsと書いているので、実はparse_inputsという関数がrgb2gray関数の中にあって、その中でエラーが検知されたことがわかります。丁寧に行数まで出ています。こちらクリックするとrgb2gray.mファイルが開いて、該当するエラー箇所が自動で表示されます。そしてエラーメッセージが出ていますね。parse_inputsの中でどのようなエラーが出たのかを教えてくれています。この場合、「MAPはM行3列の配列でなければいけません」ということですね。正直MAPって何?という感じですが(MATLABくんは親切ですが所詮はマシンなのでちょっとおバカなのです)、rgb2gray関数の中で発生していて、配列の次元数に関わるバグらしい、ということは伝わってきます。

というように一連のエラーメッセージの中で最も重要なのが、一番最初に出てくるエラーメッセージなんです!なぜ一番重要なのか?はもう説明不要ですね。これがもっとも直接的にバグと結びついているエラーだからです(説明しちゃう)。その後に続くエラーメッセージはおまけにすぎません。

おまけについて見ていきます。2番目のエラーはrgb2gray関数の中で起きています。parse_inputsの中でエラーが起きたことを教えてくれています。

3番目のエラーは…もういいですね。

ということでエラーメッセージの見方は大丈夫でしょうか。出てくる順番がどういう規則に基づいているかわかれば本質的にどこを見ればいいのかわかりますので、これが何百行出てきても慌てることはありません。一番上が重要です。

ワークスペースを活用しましょう

さて、エラーメッセージが我々に何を伝えようとしているかはわかりました。しかし、エラーメッセージは問題点(rgb2gray関数には3次元の配列を入れてないからエラーになっているんだよ)を教えてはくれますが、問題そのものまでをはっきり示してくれるわけではありません(そういうエラーメッセージもあるけど)。この場合で言うと、問題は「rgb2gray関数にグレースケール画像を入れていること」です。

エラーメッセージからでは、直接の問題に気付けなかったとします。例えばimgがRGB画像だと思い込んでいれば、「なんでだよ、RGB画像入れてるじゃねぇかよ」とただただ憤慨するのみです。そこで役に立つのがワークスペースです。ワークスペースには、scriptで作成した変数の情報が表示されています。この場合下のような感じになっているはずです。

変数名とそのサイズとデータ型が表示されています。

これを確認するとimgのサイズは291x240となっていて、RGB画像でないことは明白です(RGB画像の場合RとGとBの3チャンネル分の行列が必要なので241x240x3でなければいけない)。ここにきて初めて「あちゃ!これグレースケールだったのか…!」と気付くことができ、「RGB画像に差し替えよう」と解決策を立てることができます。

これにてデバッグ終了です。

ブレークポイント

さて、エラーメッセージの見方とワークスペースで変数の情報を確認することがデバッグには重要であることが分かったかと思います。ここでdebug_sample1.mを少し改変して、debug_sample2.mを作ってみましょう

変更点は画像の読込み部分をconvert_img_to_gray関数の中に入れただけです。参考までに実行順序をコメントとして書いています。これは当然エラーになります。

debug_sample2.m
gImg = convert_img_to_gray; % 1
imshow(gImg); % 2

function gImg = convert_img_to_gray()
    img = imread('pout.tif'); % 1-1
    gImg = rgb2gray(img); % 1-2
end

エラーメッセージはdebug_sample1.mの場合と同じなので割愛します。

ここで、やはり直接の問題に気付けなかったとしましょう。次にあなたはワークスペースを確認しようとするでしょう。ですがこの場合地獄が待っています。

情報無しです。

なぜこんなことが起きるのでしょうか。これを理解するために、変数のスコープについてちょこっと解説します。実は私も理解しきっていないので、この場合、ということで。scriptから呼び出されたfunction内で作成された変数のうち、出力変数でないものはscriptからは見ることができません。つまりこの場合convert_img_to_gray関数の中ではimgとgImgが作成されます(gImgは計算しようとしてエラーになりますが…)が、出力変数はgImgだけなので、imgがどんな変数かはscriptからは一切分かりません。

このように変数情報が保持されている範囲を変数のスコープ、と言います。

さて、これは困りました。何やらrgb2grayでエラーが起きたことは分かったけど、その時何が起きていたかを想像するために重要な変数の情報を得ることができません。

あ~知りたい!rgb2gray関数でエラーが起きる直前の変数情報を知りたい!

そこで必要になるのがブレークポイントです。ブレークポイントをプログラムの任意の行に設定すると、プログラムの実行を一時的に止めて、その時点でのワークスペースを見ることができるのです!…さもすごいことのように書いていますが、デバッグする上での基本機能ですね。処理を関数化することは普通なので、こういった機能が無いといちいち関数の中身をscriptに出すとか、変数の結果を逐一出力するとか、といった形でしかワークスペースを確認できなくなってしまいます。

手順はこんな感じです。

圧縮しているのでみづらいですが、スクリプトの行番号をクリックすると赤くマークされます。これがブレークポイントが設定されたことを意味しています。実行するとスクリプトに矢印が出てきて、ここで止まってます感が出ています。注目したいのは、画面左下のワークスペースですね。ブレークポイントでスクリプトが一時停止した段階で、それまでの計算成果であるimg変数がぽこっと表示されています。

これでfunction内での変数も見れるようになりました。つまりデバッグのために大きく前進することができます!

ちなみにブレークポイントで止まっている時、コマンドウィンドウへの入力は有効になっているので、時を止めたままその時の変数の中身を調べたり、いろいろな処理を行うことができます。

以上で超基礎編は終了です。

さらに便利なデバッグ機能

次に基礎編を踏まえた上で、私も最近使い始めた便利機能について紹介したいと思います。知ってる人はとうの昔に知っているかもしれません。タイトルのままなので、知っていればスルーお願いします。お役に立てずすみません。

条件付きブレークポイントの設定

これはブレークポイントの進化版ともいえる機能です。もうそのまんまなんですが、ブレークポイントにif文付けられるイメージですね。その時点でもし○○だったらいったん止めて、みたいな使い方ができます。

あまりいい例が思い浮かばず申し訳ありませんが、無理やりな例を用意しました。sinc関数を積分するプログラムです。いや配列使えよ!っていう突っ込みは置いておいてください。

ご存知の通りsin(0)/0は特異値って言えばいいんでしょうか、sin(x)においてxを無限にゼロに近づけていくとsin(x) ≂ xと近似できるのでsin(x) / x はx/xとなり1になります。が、x=0としてしまうと0/0でNaNとなります。なので逐次足し算(積分)していくとx=0の時sinc(x)の値はNaNにすり替わってしまいそれ以降何を足してもNaNはNaNなので、最終結果はNaNとなります。

Code
i = -200;
area = 0;

while i<=200
    x = i/200*pi;
    area = area + abs(sin(x)/x);
    i = i + 1;
end

area = area / 200 * pi
Output
area = NaN

ということでNaNになってしまいます。

上記の理屈を知らなかった場合でも、while文のどこかの段階でareaがNaNになっちゃうっていうのは容易に想像がつきますが、ではどのタイミングで?というのはどのように確かめればいいでしょうか。

割とよくやるのはwhile文の中のiとareaを逐次出力するっていう力技です。MATLABの場合、行末のセミコロンを外せば出力できるので割と簡単に実行できます。出力結果をスクロールしてareaがNaNになった時のiを読めば何が起こったか想像することができるでしょう。

これをもっとスマートにやれるのが条件付きブレークポイントです。行番号でクリックして設定していたブレークポイントですが、右クリックすると条件付きブレークポイントを設定できます。出てくる入力欄には次のように一時停止させたい条件を入力しします。この場合はisnan(area)でOKです。

するとあら素敵!iがゼロのところでプログラムが一時停止していることがわかりますね!条件付けはif文で書くようにできるので、かなり柔軟に様々な条件を指定可能です。

  • forループのN回目
  • 任意の変数がゼロ以下になったとき
  • 任意の配列のサイズがNを超えたとき

など思いのままですね。基本forループ内で使いドコロがありますが、他のケースでもこんな時使える、というのがあれば聞いてみたいです。

神なるコマンド"dbstop if error"

ではこんなケースはいかがでしょうか。またしてもこんなコード書くやついないだろ、という例ですが。

Code
array = 0:10;
arrayfun(@(x)zeros(1/x),array,'UniformOutput',false);
Output
エラー: zeros
NaN と Inf は使えません。

エラー: untitled (行 14)
arrayfun(@(x)zeros(1/x),array,'UniformOutput',false);

ということでarrayfunを使ってarrayの一要素ごとに無名関数で指定した演算を施していきます。無名関数の中身はzeros(1/x)ということで入ってきた入力の逆数のサイズのゼロ行列を作る、というものになっています。x=0のとき、当然1/0はInfになるので、このようなエラーメッセージになります。

しかし、エラーメッセージからだけでは、xが何のときにこのエラーが発生したのか、全く分かりません(ということにしておいてください)。条件付きブレークポイントで~isfinite(1/x)としようにも、arrayfunの行に設定してしまってはxはまだ出てきてないので意味がないですし、arrayfunの直後の行に設定してもエラー自体はarrayfun関数の中で発生するためブレークポイントまで到達せず、上記のエラーが返ってきておしまいとなってしまい、やはりエラー発生時のxを知ることはできません。

こういうケース(無理やり作ったのでほぼないけど)や、条件付きブレークポイントで指定すべき条件がいまいち不明だったり、もう思考停止してとにかくエラーが起きたときにそこで止まってくれませんかね?と言いたくなる時があります。

この無茶振りに応えてくれるのが魔法のコマンドdbstop if errorです。使い方は超簡単です。

Code
dbstop if error

とコマンドウィンドウに入力してから、scriptなり関数なりを実行すればOKです。ブレークポイントなど仕込む必要もなく、エラーが発生したところでその直前で止まってくれます。めちゃくちゃ便利です。たまたま同僚が使っているのを目撃してこのコマンドに感動すら覚えました。動画ですとこんな感じです。

これでほぼどんな場合でも自由にワークスペースの情報を見ることができます!デバッグ作業がはかどること間違いなしです!

ちなみにこちらデバッグ後、

Code
dbclear if error

で解除するのもお忘れなく。

まとめ

今日は技術的ではありませんでしたが、MATLABの基盤機能なのでありとあらゆる人に関係するトピックだったかと思います。それだけにすでに知っている人も多かったかもしれませんが、私は条件付きブレークポイントについて知ったのは今年に入ってからだったので、今年の内に紹介記事書けて満足です。