ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装 を読んでみる
https://m.media-amazon.com/images/I/513J77QZHgL.jpg
GoやKotlinといった言語をメインに使っているんですが、
これらの言語で機械学習の実装について書かれている記事が少ないため、
Pythonがメインと明記されてはいるけれど、実装について詳しく書かれているところを参考に、
他言語での実装を学んでいきたい。
numpyの代わりはないから、必要な機能はさらっと実装する程度は必要かも。
1章 Python入門
今回の目的は好きな言語で機械学習を実装してみようってことなので、
Pythonについての情報はかるーく読むだけで飛ばした。
具体的には、Pythonのバージョンと、使用する外部ライブラリについてだけを読んだ感じ。
続く章でPythonわからねーや。みたいなことがあったら、
この章に戻ってきて読み直そうと思う。
2章 パーセプトロン
ANDゲート, NANDゲートとORゲート
2つの項ではANDゲート、NANDゲート、ORゲートのパーセプトロンのパラメータを考えた
ANDゲートなら重み($ w_1, $ w_2)それぞれが閾値($ \theta)未満で、重みの合計が閾値以上でなりたつ。ってな感じ
この項ではパラメータを人間が考えたけど、機械学習ではパラメータを考えるのをコンピュータにさせる
このコンピュータにパラメータを探させることを学習と呼ぶ
人間の仕事は、パラメータを探すことから、学習可能なモデルを考えることになる
重みとバイアスの導入, 重みとバイアスによる実装
閾値($ \theta)をバイアス($ b)に置き換えて書くって話
同時にnumpyで計算の単純化を行なっている
numpyはもちろん他の言語にはないので、同等の関数を用意する必要がある
重み($ w)とバイアス($ b)を区別せずにすべて重みと呼ぶこともあるらしい
NANDゲートとORゲートも実装したが、やったことは重みとバイアスのの変更だけ
パーセプトロンの限界. XORゲート, 線形と非線形
XORゲートはここまでのパーセプトロンでは実現できない
ANDゲートやORゲートは1本の直線で分類ができる
XORゲートは1本の直線での分類ができない
1本の直線での分類が可能な問題を線形な領域、そうでない問題を非線形な領域と呼ぶ
非線形 = くねっとした曲線であれば、XORゲートを分類することが可能
多層パーセプトロン, 既存ゲートの組み合わせ
パーセプトロンは層を重ねることができる
層を重ねることでXORのような非線形領域を分離できる
XORゲートは、入力($ x_1, $ x_2)をNANDとORに入れ、その出力をANDに入れることで実現できる
XORゲートの実装
入力の層、NANDとORの層、ANDの層 = 出力の層ということで、多層のパーセプトロンが実現できた
合計3層あるけど、実際に重みが関連しているのは2層なので、2層のパーセプトロンと呼ぶことにする
このことから、単層のパーセプトロンでは表現できなかったことを層を増やすことで表現できた
NANDからコンピュータへ, まとめ
パーセプトロンがあればコンピュータの表現も可能
どちらも入力になんらかの処理をおこなって出力を行う
理論上、2層のパーセプトロンでコンピュータを作ることができるらしい
しかし、適切な重みを設定するのはとても骨の折れる作業になる
3章 ニューラルネットワーク
パーセプトロンからニューラルネットワークへ, ニューラルネットワークの例, パーセプトロンの復習
入力層、出力層、中間層という層からなる
中間層は隠れ層と呼ばれることもある
ニューロンのつながり方に着目していえば、パーセプトロンと何ら変わらない
バイアスもひとつの入力値として表現するとバイアス($ b )は重みの一つといえる
活性化関数の登場, 活性化関数
$ b + w_1 x_1 + w_2 x_2 が 0以下なら0、0より大きければ1 という条件でパーセプトロンを作った
入力値の総和から出力値に変換する関数を活性化関数と呼ぶ
ここまで使ってきた活性化関数はステップ関数と呼ぶ
活性化関数をステップ関数から他の関数に変更することでニューラルネットワークに進むことができる
シグモイド関数
シグモイド関数は $ h(x) = \frac{1}{1+e^{-x}}
ステップ関数を使ったパーセプトロンとシグモイド関数を使ったニューラルネットワークの違いは活性化関数だけ
ステップ関数の実装, ステップ関数のグラフ
引数の値が0より大きければ1を、そうでなければ0を返す関数
numpy用の作りに変更しているけど、このあたりは必要になったときに考える
個人的にグラフ描画の優先度はそこまで高くないので、グラフ描画関連はスキップ
シグモイド関数の実装
シグモイド関数の式自体は上で書いた通り
Goではこんな感じ 1 / (1 + math.Exp(-x))
シグモイド関数は小さいほど0に近く、大きいほど1に近い結果を出力する
シグモイド関数とステップ関数の比較
シグモイド関数は曲線的に出力が変わり、ステップ関数はある値を境に急激に出力が変わる
シグモイド関数は曲線的に出力が変わることで連続的な値が返されるが、ステップ関数は断続的
両者とも入力が小さければ0に近く、入力が大きければ1に近づく
非線形関数
シグモイド関数もステップ関数も非線形関数
シグモイド関数は曲線的で非線形
ステップ関数は階段状で非線形
ニューラルネットワークでは活性化関数に非線形関数を用いる必要がある
線形関数を用いた層を何層増やしても、1回の計算で実現出来る
$ h(x) = ax という線形関数が3層あったら、$ h(h(h(x))) で表現できる
$ h(h(h(x))) は $ h(x) = a^3x と同等なので、そのような層を1層作れば十分
多層である利点を生かすためには、非線形関数を用いる必要がある
ReLU 関数
ReLUは入力値が0以上ならそのまま出力し、0以下なら0を出力する非線形関数
シグモイドよりReLUのほうが利用されるらしいが、理由は明記されていない
多次元配列の計算, 多次元配列
多次元配列とは、数字を1列に並べたものや、長方形状に並べたもの、3次元的や4次元的に並べたものをいう
とにかく、数字の集まりと言える
Goの1次元配列は []float64{1, 2, 3, 4}
Goの2次元配列は [][]float64{{1, 2}, {3, 4}, {5, 6}}
上記の2次元配列は $ 3 \times 2 の行列といえる
次元数、形状などが取得できると便利っぽい?
行列の積, ニューラルネットワークの行列の積
左側の行列の行と右側の行列の列の間の要素ごとの積と、その和によって計算が行われる
計算の結果は新しい行列として保存される
行列の掛け算はドット積と呼ぶ
行列積では $ A \times B と $ B \times A では結果が変わるため注意が必要
行列積では、行列Aの1次元目の要素数と行列Bの0次元目の要素数を揃える必要がある
上記の要素数があわなければエラー
行列Aの0次元目の要素数が出力結果の0次元目の要素数になる
行列Bの1次元目の要素数が出力結果の1次元目の要素数になる
行列積を使わなければ、すべてfor文で計算を回さないといけないので手間がかかる
3 層ニューラルネットワークの実装, 記号の確認, 各層における信号伝達の実装
1層目のひとつ目のニューロンの計算 $ a^{(1)}_{1} = w^{(1)}_{11}x_1 + w^{(1)}_{12}x_2 + w^{(1)}_{13}x_3
行列積で書くと $ A^{(1)} = XW^{(1)} + B^{(1)}
$ A^{(1)} = (\begin{matrix} a^{(1)}_1 & a^{(1)}_2 & a^{(1)}_3 \end{matrix})
$ X = (\begin{matrix} x_1 & x_2 \end{matrix})
$ B^{(1)} = (\begin{matrix} b^{(1)}_1 & b^{(1)}_2 & b^{(1)}_3 \end{matrix})
$ W^{(1)} = (\begin{matrix} w^{(1)}_{11} & w^{(1)}_{21} & w^{(1)}_{31} \\ w^{(1)}_{12} & w^{(1)}_{22} & w^{(1)}_{32} \end{matrix})
その後、活性化関数としてシグモイド関数に $ A^{(1)} を与える $ Z^{(1)} = h(A^{(1)})
2層目も1層目と同様に計算する $ A^{(2)} = Z^{(1)}W^{(2)} + B^{(2)}
その後、活性化関数としてシグモイド関数に $ A^{(2)} を与える $ Z^{(2)} = h(A^{(2)})
3層目もこれまでと同じく計算する $ A^{(3)} = Z^{(2)}W^{(3)} + B^{(3)}
3層目の計算後は恒等関数を通して出力する
恒等関数は、入力をそのまま出力する関数のこと
実装のまとめ
init_netowrk() 関数で、重みとバイアスの初期化を行なう
forward() 関数で、入力信号を出力へと変換する
pythonではディクショナリ型で返されるが、Goでは融通が利かないのでstructを定義して返すことにする
forward は入力から出力方向への伝達処理を表す
ニューラルネットワークの学習で、出力から入力方向の処理を backward という
型ゆるゆる言語ならではの書きやすさが型あり言語にはないため、追加での作業が多め
出力層の設計
ニューラルネットワークは分類問題と回帰問題の両方に用いることができる
分類問題か回帰問題のどちらに用いるかで、出力層の活性化関数を変更する必要がある
回帰問題では恒等関数、分類問題ではソフトマックス関数を使う
分類問題とは、データがどのクラスに属するかという問題
回帰問題とは、入力データから数値の予測を行う問題
恒等関数とソフトマックス関数, ソフトマックス関数の実装上の注意
恒等関数は入力をそのまま出力する
ソフトマックス関数は $ y_k = \frac{\exp(a_k)}{\Sigma^n_{i=1}\exp(a_i)}
$ \exp(x) は $ e^x を表す指数関数のことで、ネイピア数のこと
ソフトマックス関数の計算では、オーバーフローに関する問題がある
指数関数の値が容易に大きな値になりえる
コンピュータで数を扱う際には、有限データ幅に収められるため、誤差が発生する
そのため、ソフトマックス関数には改善案がある
$ y_k = \frac{\exp(a_k)}{\Sigma^n_{i=1}\exp(a_i)} = \frac{\exp(a_k + C')}{\Sigma^n_{i=1}\exp(a_i + C')} と変換する
$ C' には入力信号の最大値を用いることで、全要素を小さくできる
ソフトマックス関数の特徴
ソフトマックス関数の出力は0.0から1.0の間の実数になる
またソフトマックス関数の出力の総和は1になる
この性質により、ソフトマックス関数の出力を確率として解釈することができる
また、各要素の大小関係が変わらないという特徴もある
クラス分類では、一般的に、出力の一番大きいニューロンに相当するクラスだけを認識結果とする
ニューラルネットワークが分類を行う際には、出力層のソフトマックス関数を省略可能
省略可能な理由は、ソフトマックス関数を用いなくても一番大きな出力は変わらないため
出力層以外ではソフトマックス関数を用いて値を小さくするメリットがある
出力層のニューロンの数
出力層のニューロンの数は、解くべき問題に応じて決定する必要がある
入力画像に対して、その画像の数字が0から9のどれかを予測する問題であれば、10個のニューロンが必要
上記の問題は10クラス分類問題と呼ばれている
手書き数字認識, MNIST データセット
学習済みパラメータを使って推論処理の実装を学ぶ
推論処理は順方向伝播(forward propagation)と呼ぶ
手書き数字のデータセットとして、MNISTという画像のセットがある
書籍ではMNISTデータセットの画像をNumPy配列への変換機能を提供しているっぽい
別言語での実装を試しているので、画像の取り込みから配列への変換も自前でやることにする
mnist.pyの使い方については飛ばす
ニューラルネットワークの推論処理
推論処理を行うニューラルネットワークは入力層が784個、出力層が10個
MNISTの画像が28 x 28の784ピクセルであることと、10種類のクラスへの分類であるため
今回は隠れ層を2層用意し、ひとつ目が50個、ふたつ目が100個とする
学習済みの重みやバイアスがpklファイルで提供されているので、テキストにdumpするコードをpythonで書く必要がある
MNISTデータはバイナリデータなので、フォーマットにそって扱いやすい型にしてあげる必要がある
書籍には正答率が0.9352と出ると書かれているが、手元で動かしたら0.9207になった
1%以上差があるが、だいたい同じ結果なので良しとしよう…
どこで差がでたか、dumpに問題があったか、mnistデータの読み方に問題があったか
バッチ処理
入力層の出力 → 1層目の入力、1層目の出力 → 2層目の入力 となり、各多次元配列の要素数と一致している
今回は10種類の分類問題だったので、出力された配列の要素数は10
入力も多次元配列ではあるが、1次元目は1要素で、2次元目に784個の要素があった
1次元目を100個の要素にすれば、出力される要素も100個にすることができる(pythonでは)
バッチ処理のバッチには束という意味があり、行列計算で行なえば束で処理することも可能といえる
まとめ
パーセプトロンとニューロンは信号が階層的に伝わるという点で同じ
ニューロンでは信号を送信する際に、信号を変化させる活性化関数が違いとしてある
ニューラルネットワークでは滑らかに変化する活性化関数であるシグモイド関数を使用した
パーセプトロンでは急激に変化するステップ関数を使用した
4章 ニューラルネットワークの学習
データから学習する
ニューラルネットワークの特徴として、データから学習する点を挙げられる
ニューラルネットワークの学習とは、重みパラメータの値をデータから自動で決定できることをいう
パーセプトロンは線形分離可能な問題であれば学習することが可能
非線形分離問題は学習することができない
データ駆動
機械学習の中心には「データ」が存在し、データ駆動によるアプローチは「人」中心からの脱却になる
何らかの問題を解決しようとする場合、人があれやこれやと考えることが一般的
機械学習では集められたデータからパターンを見つけようと試みる
手書き文字を正しく分類できるプログラムを自分で考えて設計しようとすると、それは意外と難しい
人にとっては簡単に認識できることも、規則性を明確に述べることは困難
この困難で難しい問題を解決する方法として、画像から特徴量を抽出して機械学習の技術で学習する
特徴量とは、入力データから本質的なデータを的確に抽出できるように設計された変換器を指す
画像の特徴量はベクトルとして記述される
コンピュータビジョンの分野で有名な特徴量として、SIFT、SURF、HOGなどがある
特徴量を利用して画像データをベクトルに変換し、ベクトルに対して識別機で学習させる
識別機として、SVMやKNNがある
問題に応じて適した特徴量を使わなければ、いい結果が得られない
ニューラルネットワーク(ディープラーニング)を利用すると、特徴量の設定なども必要なくなる
ニューラルネットワークは、end-to-end machine learning と呼ばれることがある
ニューラルネットワークは対象の問題に関係なく、データをそのまま生データとして学習に使える
訓練データとテストデータ
機械学習の問題では、訓練データとテストデータの2つのデータに分けて学習・実験を行なうのが一般的
訓練データだけを使い学習をする
テストデータだけを使ってモデルの実力を評価する
データを二つのデータ群に分けて使うのは、モデルの汎化能力を正しく評価するため
訓練データのことを教師データと呼ぶことがある
汎化能力とは、まだ見ぬデータに対しての能力であり、汎化能力を獲得することが機械学習の目標
ひとつのデータセットだけでパラメータの学習と評価を行うと、そのデータセット以外に対応できないモデルになる
あるデータセットだけに過度に対応した状態を過学習と言う
損失関数
ニューラルネットワークの学習では、ある指標によって現在の状態を表す
ある指標を手掛かりに最適なパラメータを探索する
ニューラルネットワークの学習で用いられる指標は損失関数と呼ばれる
損失関数には任意の関数を用いることができるが、2乗和誤差や交差エントロピー誤差などが用いられる
損失関数はニューラルネットワークの性能の悪さを示す指標
教師データに対してどれだけ適合していないか、どれだけ一致していないかを表す
損失関数にマイナスを掛けた値は意味が逆転し、どれだけ適合しているかを表せる
性能の悪さを最小にすることが、性能の良さを最大にすることと一致する
2乗和誤差
2乗和誤差は $ E = \frac{1}{2} \Sigma_k (y_k - t_k)^2 で表せる
ここで、 $ y_k はニューラルネットワークの出力、 $ t_k は教師データ、 $ k は次元数
$ y が [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
$ t が [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
ニューラルネットワークの出力である $ y は、ソフトマックス関数の出力
ソフトマックスの出力は確率として考えることができる
$ y 添え字が2の3個目の値が最大で 0.6で一番確率が高い
$ t 添え字が2の3個目の値が1で正解で、それ以外が0で間違いになる
ちなみに正解が1で、それ以外が0のような表記法を one-hot 表現と言う
2乗和誤差の結果が0.0975と0.5975の場合、前者の方が誤差が小さい = より適合しているといえる
交差エントロピー誤差
交差エントロピー誤差は $ E = - \Sigma_k t_k \log y_k で表せる
ここで、 $ y_k はニューラルネットワークの出力、 $ t_k は教師データ、 $ k は次元数
正解がone-hot表現の場合、 $ t_k は0になるため、実際に必要な計算はニューラルネットワークの出力に対して正答の確率のみ
$ \log y_kにて $ y_k が0の場合に無限大に発散するため、小さな数字 $ delta を $ t_k に加算しておく
$ delta は 1e-7 くらい小さければ十分
交差エントロピーの結果が0.510825と2.302584の場合、前者の方が誤差が小さい = より適合していると言える
ミニバッチ学習, [バッチ対応版]交差エントロピー誤差の実装
機械学習の学習では、訓練データに対しての損失関数を求め、その結果を小さくするようにパラメータを調整すること
訓練データが100個あれば、100個すべての損失関数の結果の和を求め、その値を指標にする
交差エントロピーでn個の訓練データの損失関数の和は $ E = \frac{1}{N} \Sigma_n \Sigma_k t_{nk} \log y_{nk} となる
単純な和ではなく、最後にNで割って平均値を出し、正規化している
訓練データの数が変わっても、1個当たりの平均値を使っていれば問題なく比較できる
MNISTの訓練データは6万個あり、すべての損失関数の和を求めるには時間がかかる
またもっと多い場合は計算機での計算も難しい場合もあり得る
全てが難しいなら部分的に取り出して学習する = データを小さな塊として学習することからミニバッチ学習と呼ばれる
バッチ対応では単一のデータに対して行っていた交差エントロピー誤差を使い、複数データの誤差の値の平均を取る
なぜ損失関数を設定するのか?
認識精度を高めることを目的としているのであれば、認識精度を指標にしたほうがいいのではないか?
ニューラルネットワークの学習で登場する微分に着目すると損失関数が登場する理由が分かる
パラメータの微分を取り勾配を計算することで損失関数の値を小さくする
微分して得た勾配が負の値なら正の方向へ、正の値なら負の方向に変動させることで精度を上げられる
認識精度側に着目すると微分の値が0になることが多く、パラメータの変更が難しいという欠点がある
精度ではなく損失を指標にするのは、パラメータの更新に微分が必要になるため
数値微分, 微分, 数値微分の例
勾配法では勾配の情報を使って進む方向を決める
微分とはある瞬間の変化の量を表したものになる
計算機で疑似的に微分の計算をしようとしても、丸め誤差などの影響を受けて正しく計算できない
0に限りなく近い0でない数値というものも表現できない
誤差を減らす工夫として、0に近い数値を0.0001程度にすることと、中心差分を取ることがある
1e-4程度にするのは丸め誤差を回避できるなかで比較的小さい数値を選んだ結果
簡単な関数 $ y = 0.01 x^2 + 0.1 x の微分してみる
微分式にx = 5とx = 10の時の結果を見る
x = 5.0, ans = 0.1999999999996449
x = 10.0, ans = 0.2999999999975245
真の微分では x = 5.0 の時は ans = 0.2 に、 x = 10.0 の時は ans = 0.3 になる
真の微分に対しては誤差があるものの、かなり小さくなっていることは分かる
偏微分
次に2乗和を計算する式を見ていく $ f(x_0, x_1) = x_0^2 + x_1^2
変数が2つあるため、どちらの変数に対しての微分なのかを決定する必要がある
複数の変数からなる関数の微分を偏微分と言う
そのままコーディングするなら、 func(x0, x1 float64) float64 { return x0*x0 + x1*x1 } となる
$ x_0 = 3, x_1 = 4 の時の $ x_0 についてと、 $ x_1 についての偏微分を考える
$ \frac{\partial f}{\partial x_0} func(x0 float64) float64 { return x0*x0 + 4.0*4.0 } の微分
$ \frac{\partial f}{\partial x_1} func(x1 float64) float64 { return 3.0*3.0 + x1*x1 } の微分
ひとつの変数の微分であればその関数ある場所での傾きを求める
複数の変数の微分の場合、目的の変数以外の変数の値は定数に固定して微分を求める
手順としては元の関数のうち一つの変数以外を固定した新しい関数を作る
新しい関数を元の微分関数を通して値を求める
勾配
すべての変数の偏微分をベクトルとしてまとめたものを勾配(gradient)と言う
$ x_0 = 3, $ x_1 = 4 のときの $ (x_0, x_1) の偏微分を $ (\frac{\partial f}{\partial x_0}, \frac{\partial f}{\partial x_1}) と表現する
勾配を求める関数の結果は、マイナスを付けることで最小値方向への移動として表現できる
最小値は $ (0, 0) の点
最小値までの距離が遠いほどベクトルは長く、最小値までの距離が近いほどベクトルは短くなる
実際の問題では必ず最小値を見つけられるわけではない
ただし、各地点において小さくなる方向は得られる
勾配法
複雑な損失関数に対して、最小の値を取る場所を探すために勾配を利用することを勾配法という
損失関数が最小の値を取るようにパラメータを更新することを学習という
各地点での関数が小さくなる方向を示すのが勾配
ただし、その地点からみて小さい方向というだけで、全てのパターンに置いての最小を探すことはできない
勾配をヒントにすることで、最小値が見つかることは確約されないが結果が良くなる方向に進めることができる
勾配法では、ある地点での勾配を求めて移動し、移動した地点でもまた勾配を求めて移動する
勾配法は、ニューラルネットワークの学習では良く用いられる手法
最小値を探す勾配法のことを、勾配降下法と呼ぶ
式で表すと $ x_0 = x_0 - \eta \frac{\partial f}{\partial x_0}, $ x_1 = x_1 - \eta \frac{\partial f}{\partial x_1}
$ \eta は学習率と言い、1回の学習でどれだけの割合学習するかを設定する値
学習率は、大きすぎても小さすぎても期待する学習をするのは難しい
学習率のようなパラメータはハイパーパラメータと呼ばれる
学習率は、バイアスなどの自動的に得られるパラメータとは性質の異なるパラメータ
ニューラルネットワークに対する勾配
ニューラルネットワークの学習では損失関数の勾配を求める必要がある
形状が $ 2 \times 3 の重み W だけを持つニューラルネットワークは $ W = \begin{pmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ \end{pmatrix}
その勾配は $ \frac{\partial L}{\partial W} = \begin{pmatrix} \frac{\partial L}{\partial w_{11}} & \frac{\partial L}{\partial w_{12}} & \frac{\partial L}{\partial w_{13}} \\ \frac{\partial L}{\partial w_{21}} & \frac{\partial L}{\partial w_{22}} & \frac{\partial L}{\partial w_{23}} \\ \end{pmatrix}
各要素の偏微分によって構成される
すごく単純なニューラルネットワークっぽいsimpleNetを実装する
持っている機能は、パラメータを持つ多次元配列、パラメータを通した結果を得るpredict、損失関数の値を求めるlossだけ
その後、ここまでで作ったnumerical_gradientで勾配を求める
勾配を求める際に整合性のために引数Wを持っているけど、ダミー的な要素と記載されている
が、実際には極微量だけずらしたWを与えて計算するのが正しく、その結果をうけて損失関数を動かす必要がある
学習アルゴリズムの実装
学習では損失関数、ミニバッチ、勾配、勾配下降法を利用する
訓練データからランダムに選択したデータをミニバッチといい、ミニバッチの損失関数の値を減らすことを目的とする
ミニバッチの損失関数を減らすために、勾配を求めて損失関数が減る方向を探る
求めた勾配の勾配方向に微小量だけ更新
上記の手順を繰り返してパラメータを目的の値に近づけていく
ミニバッチを利用した勾配降下法は確率的勾配降下法と呼ばれる
確率的勾配降下法(stochastic gradient descent) の頭文字をとってSGOと呼ばれる
2 層ニューラルネットワークのクラス
TwoLayerNetオブジェクトを作る
入力層、隠れ層、出力層の3層で、重みづけのタイミングは2回
MNISTのデータで試すので、入力は784個、隠れ層はとりあえず100個、出力は10個というネットワークになる
ミニバッチ学習の実装
いくつかのデータの束に対して学習を行なう
100枚の画像の学習ですら終わる気配がない
並列化などして速度を上げないことには実用に耐えられなさそう
2023/09/14追記
Goだけで実装を進めるのが難しいので、Pythonで先にやってからもう一回Goに戻ってくる
ってことで、ここでいったん中断
更新履歴