前回のChiselの記事ではChisel-bootcampのModule3.4でScalaの持つ関数型言語としての特徴をChiselにどう活かすかについてを勉強した。
前回の終わりにも書いたとおり、今日はModule3.4の残している練習問題に取り組んでいく。
Chiselで作るニューラルネットワークのニューロン
練習問題:ニューラルネットワークのニューロン
いつもどおり練習問題の問題文の確認から。
最初の例として人工的なニューラルネットワークの全結合層を作ってみよう。ニューロンは入力として重みと入力データのセットを受け取り、ひとつの出力を生成する。 重みとデータはそれぞれ乗算した後に加算され、その結果を活性化関数に入力される。この練習問題ではいくつかの異なる活性化関数を実装し、ニューロン・ジェネレータに引数として渡すことにする。
第1ステップ:ニューロン・ジェネレータの作成
ということで、まずはニューロン・ジェネレータの作成から行っていく。これまでの練習問題と同様に解答用のスケルトンコードが提供されている。
class Neuron(inputs: Int, act: FixedPoint => FixedPoint) extends Module { val io = IO(new Bundle { val in = Input(Vec(inputs, FixedPoint(16.W, 8.BP))) val weights = Input(Vec(inputs, FixedPoint(16.W, 8.BP))) val out = Output(FixedPoint(16.W, 8.BP)) }) ??? }
ChiselのFixedPoint
ここでしれっと登場しているのがChiselのFixedPoint
だ。中身はその名前の通り固定小数点を扱うものになる。
なお、これは現時点(Chisel 3.1.x)においては実験的な位置づけであり、まだ変更される可能性があるとのことだ。
実装はBits.SaclaのL.1651あたり。この中にあるBits
の実装を眺めてもらえればわかる通り、各種データ型変換や演算子についても実装されているようだ。
先に簡単なサンプルで使い方を見ておこう。以下のコードはChisel Wikiにあったのものを動くように修正したもの。
// 単に入力された固定小数点数を1サイクル遅らせるだけ class MyFixedPoint(outBPwidth: Int) extends Module { val io = IO(new Bundle { val fixedInput = Input(FixedPoint(32.W, 16.BP)) // トータルビット数:32bitで小数点部:16bitの入力ポート val fixedOutput = Output(FixedPoint(32.W, outBPwidth.BP)) // トータルビット数:32bitで小数点部:8bitの入力ポート val fixedOutConst = Output(FixedPoint(32.W, outBPwidth.BP)) // トータルビット数:32bitで小数点部:8bitの入力ポート }) val fixedReg = Reg(FixedPoint(32.W, 16.BP)) // 2進数小数点数のレジスタ作成 fixedReg := io.fixedInput io.fixedOutput := fixedReg io.fixedOutConst := -3.14.F(2.BP) } Driver(() => new MyFixedPoint(5)) { c => new PeekPokeTester(c) { // FixedPointを使う場合は専用のpoke/peek/expectを使う(各関数の末尾にFixedPointを付与) pokeFixedPoint(c.io.fixedInput, 6.77) expectFixedPoint(c.io.fixedOutConst, -3.14, "ERROR - 1st") println(s"\tio.fixedOutConst: valid=${peekFixedPoint(c.io.fixedOutConst)}") step(1) expectFixedPoint(c.io.fixedOutput, 6.77, "ERROR") // Will a third read produce anything? println(s"On third read:") println(s"\tio.fixedOutput: valid=${peekFixedPoint(c.io.fixedOutput)}") } }
上記のコードを眺めてもらえれば、だいたいわかるとは思うが一応ざっとまとめておく。
- 宣言は
FixedPoint("総ビット数", "小数点部分のビット数")
- 総ビット数は
"x.W
、小数点のビット数はy.BP
で指定(BPはBinary Point) - 実際に
FixedPoint
のapply
が呼ばれて、中でオブジェクトが生成される。
- 総ビット数は
上記を動かすと以下のようになる。 なるんだけど、これ、精度はどう判断されてるんだろう??
[info] [0.000] Elaborating design... [info] [0.005] Done elaborating. Total FIRRTL Compile Time: 4.3 ms Total FIRRTL Compile Time: 4.5 ms End of dependency graph Circuit state created [info] [0.000] SEED 1548773483057 [info] [0.001] EXPECT AT 0 ERROR - 1st FAIL [info] [0.001] io.fixedOutConst: valid=-3.25 [info] [0.002] On third read: [info] [0.002] io.fixedOutput: valid=6.76953125 test cmd41HelperMyFixedPoint Success: 0 tests passed in 6 cycles taking 0.002636 seconds [info] [0.002] RAN 1 CYCLES FAILED FIRST AT CYCLE 0
とりあえず、今回の例だとクラスインスタンス時に指定するBP
のビット幅を5bitにするとテストにFAILするようになる。
[info] [0.000] Elaborating design... [info] [0.005] Done elaborating. Total FIRRTL Compile Time: 3.9 ms Total FIRRTL Compile Time: 10.7 ms End of dependency graph Circuit state created [info] [0.000] SEED 1548771763134 [info] [0.001] EXPECT AT 1 ERROR FAIL [info] [0.002] On third read: [info] [0.002] io.out: valid=6.75 test cmd36HelperMyFixedPoint Success: 0 tests passed in 6 cycles taking 0.003101 seconds [info] [0.002] RAN 1 CYCLES FAILED FIRST AT CYCLE 1
とりあえずFixedPoint
についてはここまでにして、精度部分については追って確認することにしよう。
ニューロンの作成については前回やその前で見てきた高階関数を使えば特に苦労することはないはず。
解答 - クリックすると開くので、見たくない場合は開かないように注意。
class Neuron(inputs: Int, act: FixedPoint => FixedPoint) extends Module {
val io = IO(new Bundle {
val in = Input(Vec(inputs, FixedPoint(16.W, 8.BP)))
val weights = Input(Vec(inputs, FixedPoint(16.W, 8.BP)))
val out = Output(FixedPoint(16.W, 8.BP))
})
val mac = io.in.zip(io.weights).map{ case(a:FixedPoint, b:FixedPoint) => a*b}.reduce(_+_)
io.out := act(mac)
}
第2ステップ:活性化関数の実装
ここも問題文を訳したものをペタリ。
さあいくつかの活性化関数作ろう!典型的な活性化関数はシグモイド関数やReLUがある。 ここで使うシグモイド関数はロジスティック関数と呼ばれ、以下の式で与えられる。
βが傾きの係数となる。しかしハードウェアで指数関数を計算するのは極めた挑戦的でコストが嵩む。そのためステップ関数として近似する。
2つ目の関数であるReLUも似た式で与えれられる。
これら2つの関数を実装してみよう。固定小数点のリテラルは
-3.14.F(8.BP)
のようにして指定することが出来る。
これも一応スケルトンが用意されている。とは言っても、変数名と戻り値の指定しかないんだが。
ただ一点注意が必要で、次の第3ステップでニューロンを使った論理ゲートの近似テストを行う関係上、実装する関数x
の条件をのように
0
ではなく0.5
に変更しておく必要がある。
val Step: FixedPoint => FixedPoint = ??? val ReLU: FixedPoint => FixedPoint = ???
解答 - クリックすると開くので、見たくない場合は開かないように注意。
val Step: FixedPoint => FixedPoint = x => Mux(x <= 0.5.F(8.BP), 0.F(8.BP), 1.F(8.BP))
val ReLU: FixedPoint => FixedPoint = x => Mux(x <= 0.5.F(8.BP), 0.F(8.BP), x)
第3ステップ:テストの実装
ここも問題文(略)。
最後に我々が実装したニューロンが正しいことを確認するためのテスターを作ろう。活性化関数のステップとともに使うと、ニューロンは論理ゲートの近似回路として使える。固有の重みの組み合わせで”AND”、”OR”、"NOT"、その他のバイナリの関数を演算可能だ。なお”NOT"だけはバイアスが必要となる(今回は実装していないが)。また”XOR"はいくつかのニューロンを連結する必要がある。ここでは作成したニューロンで”AND"と”OR"の演算をテストしてみよう。次のテスターを完成させ、ステップ関数付きのニューロンをチェックしてみよう。
そして、次のコードがテスターとして与えられているもの。これまでどおり、???
の部分を埋めることになる。
問題文にあったとおりで、作成したニューロンが論理演算の”AND”と”OR”として振る舞うように重みを決めてやれば良い。
// ニューロンのテスト Driver(() => new Neuron(2, Step)) { c => new PeekPokeTester(c) { val inputs = Seq(Seq(0, 0), Seq(0, 1), Seq(1, 0), Seq(1, 1)) // 以下の2つのシーケンスを作ろう val and_weights = ??? val or_weights = ??? // データをニューロンに入力し結果を確認する(ANDゲート) reset(5) for (i <- inputs) { pokeFixedPoint(c.io.in(0), i(0)) pokeFixedPoint(c.io.in(1), i(1)) pokeFixedPoint(c.io.weights(0), and_weights(0)) pokeFixedPoint(c.io.weights(1), and_weights(1)) expectFixedPoint(c.io.out, i(0) & i(1), "ERROR") step(1) } // データをニューロンに入力し結果を確認する(ORゲート) reset(5) for (i <- inputs) { pokeFixedPoint(c.io.in(0), i(0)) pokeFixedPoint(c.io.in(1), i(1)) pokeFixedPoint(c.io.weights(0), or_weights(0)) pokeFixedPoint(c.io.weights(1), or_weights(1)) expectFixedPoint(c.io.out, i(0) | i(1), "ERROR") step(1) } } }
重みを決めるだけなので、その部分だけ抜粋。
以下の式が”AND”/"OR"の論理演算の結果と一致するように重みを決定する。 解答 - クリックすると開くので、見たくない場合は開かないように注意。
val and_weights = Seq(0.5, 0.5)
val or_weights = Seq(1.0, 1.0)
さてこれでModule3.4はお終い。 次回はModule3.5に入る。Module3.5ではScalaの別の特徴であるオブジェクト指向言語として機能をどうChiselに適用していくかを見ていくことになるようだ。