LED通信事業プロジェクト エンジニアブログ

F#で行列計算をしよう #1

配列の宣言

記事更新日 2024年2月20日


はじめに

このブログも週刊化して、記事の本数が増えました。これまでの隔週の時にF#のネタを書くとがっつり閲覧数が減るため、なかなかF#を取り上げることができませんでした。しかし、週刊であれば、ぼちぼちF#のネタを挟んでも怒られないだろう、ということで新連載を始めようと思います。

Microsoft F#というのは、Microsoft .NETグループの一言語ですが、唯一の関数型言語です。関数型言語は副作用※1が少なく、その結果完成したコードにエラーが少ないというメリットがあるのですが、それ以外にもう一つメリットがあります。それは、関数型言語は「行列計算を駆使したDeep Learning系AI処理を書きやすい」というものです。

なのですが、何とF#で行列計算をしようと思っても、Microsoftの公式のリファレンスぐらいしか出ておらず、しかも、それすらも追加された機能が反映されていなかったりして、折角の高機能が台無しです。調べても誰もまとめている気配が無い。仕方ないので私が作るしか無い、ということでF#で行列の計算をする方法を何回かに分けた連載という形でお伝えしようかと思います。

真面目に頭から読む、というよりはF#を調べているときに検索に引っかかったらよいですね、ぐらいの記事にしていくつもりです。とはいえ、こういう内容って、AIだと正しいか正しくないかわかりにくいことが多いので、あればあるで誰かの役には立つんじゃないかと思っています。

ちなみに、AIそのものの作成方法(誤差逆伝搬法とか)や、行列計算そのものの意味(内積・外積の意味とか計算方法とか)とか、そういったものの説明はしません。そんな大層な内容ではございませんで、あくまで「プログラミング中に困って検索したら引っかかる」ことを目的としています。

というわけで、趣味と社会貢献を兼ねたF#の記事、スタートです。

行列(配列)を宣言する

F#における行列は「配列」または「アレイ」とよばれる変数型で扱われます。最も簡単な宣言は、次の通りです。

let ary = [|0; 1|] // val ary: int[] = [|0; 1|] let ary_byte = [|0uy; 1uy|] // val ary_byte: byte[] = [|0uy; 1uy|]

これは一次元の配列(0, 1)を表すわけですが、F#において通常の配列は[|数字|]で表され、数字と数字の間はセミコロン;で区切ります。中身の変数の型は入れた数値によって自動的に決まります。他の言語のように型を宣言することはありません。上の例では"0"や"1"を入れると、自動的にint(整数型)の配列となりますし、"0uy"のようにバイト型と宣言した数値を入れれば、配列もバイト型になります。当然、文字列を入れれば文字列の配列になります。

配列の場合、変数型としては(変数名)[]と表現されます。上の例の整数型であればint[]と表記されますし、バイト型ならbyte[]、文字列型ならstring[]となります。

尚、角括弧だけで縦棒(バーティカルバー)がないと、リスト型と判断され、配列とは異なるものとなります。

// 配列 (array) let ary = [|0; 1|] //val ary: int[] = [|0; 1|] // リスト (list) let lst = [0; 1] // val lst: int list = [0; 1]

リスト型で宣言してしまうと、この後(次回以降)出てくる計算もできなくなります。というのも、リスト型は「変更できない固定値」を意味するからです。リストへの追記はできますが、中身を書き換えることはできません。データなどレコードを記録しておくには向いていますが、計算していろいろやるには向いていません。一方、配列は中身を書き換えることができます。というか、F#はletで宣言すると変数は通常書き換わらないのですが、この配列は中身が書き換わります(mutable宣言は不要です)。大きな配列を計算する度に配列をコピーしまくるというのは、パフォーマンスが落ちるだけなので、当然と言えば当然の仕様なのですが。というわけで、行列計算をしたい場合、必ず配列(Array)型で宣言するようにして下さい。

次に、二次元で宣言する場合です。(Microsoftのリファレンスに書いてある方法です)

let jagary = [|[|0; 1; 2|] ; [|3; 4; 5|] ; [|6; 7; 8|]|] // val jagary: int[][] = [|[|0; 1; 2|]; [|3; 4; 5|]; [|6; 7; 8|]|]

縦棒を随所に入れなくてはいけなくて、見た目が結構わかりにくいですね・・・

というか、それ以前に、この方法での配列の宣言は絶対にお勧めしません。この宣言方法だとジャグ配列と呼ばれる、配列の「入れ子」と認識されてしまうのです。言葉で説明するのは難しいですが、配列の要素が配列になっている、そういった形になっていて、通常イメージする二次元の配列では無いですし、使い方も異なります。

通常の2次元配列を宣言する方法はこちらになります。

let ary2d = Array2D.create 3 3 0 // val ary2d: int[,] = [[0; 0; 0] // [0; 0; 0] // [0; 0; 0]]

Array2Dというモジュールの中のcreateというメソッドを使います。この関数は Array2D.create (一次元目の大きさ) (2次元目の大きさ) (要素の初期値) のように使用します。大きさと中身を引数にするわかりやすいメソッドですね。当然、この初期値が配列のデータ型を決めますので、数値は適当でもよいですが、データ型はきっちり指定してください。後で変更は利きません。

Array2Dというモジュールには、二次元配列に関する様々な関数が含まれていて、この関数群がF#を他の.NETと一線を画す理由の一つとなっています。

尚、この2次元の配列だと、データ型はint[,]となっています。先ほどのジャグ配列int[][]とは異なっていることがおわかり頂けると思います。

配列は参照型

さて、各要素へのアクセス方法は次のようになります。

//ary2dは上から引き継ぎ let val1 = ary2d[1, 2] //(1, 2)の要素を取得 // val val1: int = 0 // (以前は let val1 = ary2d.[1, 2] と書いていた) ary2d[1, 2] <- 2 //(1, 2)へ2を代入 let y = 1 let val2 = ary2d[y, 2] //(1, 2)の要素を取得 // val val2: int = 2 // ary2dは最終的に以下の通りとなる // [[0; 0; 0] // [0; 0; 2] // [0; 0; 0]]

最初は要素の取得。配列名の後ろに要素の座標を入れる一般的な形式で要素(の値)が取得できます。座標はすべて(0, 0)から始まります。そして、コンピューターでは縦が一次元目、横が二次元目とされることが多いので、(y, x)と考えるのがよいでしょう。もっとも、コンピューターにとっては縦も横もないのですが・・・

F#はもともと配列名と座標の間にドットを入れてary2d.[1, 2]と表現するのが基本だったのですが、F#6.0より他の多くの言語にならって間にドットが不要となりary2d[1, 2]となりました。現在はドットを入れてもエラーにはなりませんが、ドットを入れることは非推奨とされていますので、将来のバージョンアップで変更される可能性はあります。

次に代入。先に書いたとおり配列はlet宣言でも変更が可能です。ですので、<-を使った代入式で、要素の値を書き換えることができます。上の例では配列の座標(1, 2)が2に書き換わりました。また、要素の座標は、もちろん変数(整数型)で指定することができます。

次に、2次元以外でArrayモジュールを使っての宣言です。

// 1次元:bool[] let ary1d = Array.create 3 false // 3次元:float[,,] let ary3d = Array3D.create 3 3 3 0.0 // 4次元:string[,,,] let ary4d = Array4D.create 3 3 3 3 "a" // 強引な5次元:bool[][,,,] let ary5d= Array4D.create 3 3 3 3 ary1d // 強引な5次元:int[,][,,] let ary5d_2 = Array3D.create 3 3 3 ary2d

F#では、標準で4次元配列まで扱うことができます。2Dのときと同じようにArray"xD"モジュールを利用して宣言することができます。多くの用途においては4次元で事足りると思いますが、画像処理AIとかを作ろうとした場合には5次元以上が必要になる事もあるでしょう。そういった場合は、先ほどのジャグ配列を応用して、やや無理矢理ですが、「4次元配列の要素を配列にする」、「3次元配列の要素を2次元配列する」みたいなことで対応できます。これで実質5次元のできあがりです。ちょっと面倒ですけどね。

最後に、重要なことを説明します。F#における配列は「参照型」であること。参照型なので、代入しても関数に引数で渡しても、元の値が保持され、変更が反映されます。例えば、下に参照の例を挙げます。

let ary1 = Array2D.create 3 3 0 //(1) ary1を宣言 let ary2 = ary1 //(2) ary2にary1を代入 ary2[1, 1] <- 2 //(3) (1, 1)に2を代入 let rst = ary1[1, 1] //(4) ary1の値を取得 // val rst: int = 2 (結果は2)
  1. ary1として中身が全て0の3x3の配列を宣言します。
  2. ary2にary1を代入します。これでary2は中身全て0の3x3の配列です。
  3. ary2の座標(1, 1)の値を2にします。(ary1には変更を加えません)
  4. ary1の座標(1, 1)の値を取得します。

この手順の結果は「2」になります。つまり(3)のary2に対する変更は、ary1にも適応されます。というより、ary2 = ary1 という式は、参照先を代入しただけで同じ「配列オブジェクト」を表していて、名前が違うというだけです。だから、(2)の代入式は、配列の呼び名が増えるだけの全く意味の無い式なんですよね。

配列って巨大サイズになる事も多くて、1枚の画像処理をするだけで1配列のサイズが何十メガバイトになることもあるんです。その場合に、例えば配列を関数に引数を渡しただけで、数十メガのコピーが作られているようだと、メモリーもバカ食いですし、何より遅くなります。ですから、配列が参照型にされているのは当然のことなのです。

まとめ

まずは、F#での配列の宣言の仕方とか、要素の取得の仕方などを説明しました。これが全てのスタートです。次回以降は、行列の計算、変形等を紹介していきたいと思います。


※1; 式は、評価値を得ること(※関数では「引数を受け取り値を返す」と表現する)が主たる作用とされ、それ以外のコンピュータの論理的状態(ローカル環境以外の状態変数の値)を変化させる作用を副作用という。(Wikipediaより)