【読解】FINN: A Framework for Fast, Scalable Binarized Neural Network Inference

#0. はじめに
BNNの論文とBatch Normalizationの論文を読んだぞ！実装して動かしたぞ！・・・あれ？Xilinxの人が作ったBNN-PYNQと動きがやや違う。Batch NormalizationってBeta, gamma, mean, var, epsが必要じゃないの？何でその重みがないんだ・・・。と思っていたらFINNという論文に書かれていた。これも読んでいかなければ。では、以下に必要そうなエッセンスだけ。論文ではNNの仕組みや、精度等についても書かれているが、実装する上ではそんなの必要ないのでカッツ。BNNを実装したことあるならば、4.2.1と4.2.2、4.3.1さえ分かっていれば、FINNを実装することができるはずである。

# 4.2.1. 積算値のためのPopcount / Popcount for Accumulation
BNN計算の正則及び値の制約がある性質は、少ないハードウェアリソースでバイナリのドット積処理を可能にする。
ニューロンの入力シナプス(or fan-in)の数を$Y$とする。ここで、$+1$の入力を$Y1$とし、$-1$の入力を$Y0$とする。
任意のシナプスの入力はバイナライズ化されているので、-1か+1しかないので、$Y=Y0+Y1$である。
したがって、ただ1つの値に対するシナプスの数を数えることによって、ニューロン全体に対する合計値を推論することが可能である。

<b>ハードウェアでは、バイナリドット積の合計を符号付きの算術によって導かれる積算値にするのではなく、ビットの数をカウントするpopcount演算によって実装する。</b>

→ようはXNOR回路で実装したらよい。



# 4.2.2. 閾値としてのバッチ正規化と活性化関数 / Batchnorm-activation as Threshold
BNNでは畳み込み層や結合層の出力でバッチ正規化を使用し、その後、Sign関数で2値化する必要がある。<b>これは、閾値処理によって同じ動作を再現できる。</b> 

その閾値の求め方はこれ。
$T_k = \mu_k - (\frac{B_k}{\gamma i_k})$

以下、詳しい説明
---

出力は次のように計算される。
${a_k}^b = Sign(BatchNorm(a_k, \Theta_k))$

$Where:$ 
$BatchNorm(a_k, \Theta_k) = \gamma_k (a_k - \mu_k)i_k + B_k$
$a_k$を活性化関数を通る前の出力値(ドット積)とする。
$\Theta_k = (\gamma_k, \mu_k, i_k, B_k)$をバッチ正規化の変数とする。これらの変数は学習中に(EMAやLRD等によって)調整される変数である。

図3は、3つのニューロンの例である。これは、ドット積の入力と出力活性化を示している。
パラメータ値に応じつて、プロットは左右にシフトしたり、水平方向に反転したりすることはあるが、出力活性化を変更するための閾値は常に存在している。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/95636/5f647455-c9aa-d1bf-b7db-82c27ce27dd6.png)

このため、$BatchNorm(t_k, \Theta_k) = 0$を解くと、$T_k = \mu_k - (\frac{B_k}{\gamma i_k})$を推論することができる。


計算された閾値は、ニューロン fan-in Sで平均化され、${T_k}^+ = \frac{(T_k + S)}{2}$が得られる。

図3のニューロンCがニューロンAおよびBに対して逆符号の閾値でどのように活性化するかを観察すると、すべてのニューロンは、$\gamma_k i_k < 0$の場合にニューロンの重みの符号を反転させることによって閾値を超えるものを用いて活性化させることができる。

これらの手法を用い、符号なし比較によって（出力の）活性化関数を計算し、推論の際にバッチ正規化された値を計算しないようにします。
${T_k}^+$は訓練されたネットワークに対して固定される。これはBatchnormのパラメータから計算できる。


プログラムだとこんな感じ

```python

# convert a fully connected binarized layer plus batch normalization into 
# the simplified form (binary weight and positive threshold)
# note that the neurons are assumed to be in the columns of the weight
# matrix
def makeFCBNComplex(weights, beta, gamma, mean, invstd, use_rowmajor=False, usePopCount=True):
  ins = weights.shape[0]
  outs = weights.shape[1]
  print "Extracting FCBN complex, ins = %d outs = %d" % (ins, outs)
  # we'll fill in the binarized weights and thresholds iteratively
  w_bin = range(ins*outs)
  thresholds = range(outs)
  for neuron in range(outs):
    # compute a preliminary threshold from the batchnorm parameters
    thres = mean[neuron] - (beta[neuron] / (gamma[neuron]*invstd[neuron]))
    need_flip = 0
    # ensure all neurons activate on the "positive" side, so we can use
    # greater-than-threshold activation
    if gamma[neuron]*invstd[neuron] < 0:
        need_flip = 1
        thres = -thres
    # turn threshold into "number of 1s" (popcount) instead of signed sum
    if usePopCount:
        thresholds[neuron] = int((ins + thres) / 2)
    else:
        thresholds[neuron] = thres
    # binarize the synapses
    for synapse in range(ins):
      # note how we change from col major to row major if requested
      dest_ind = neuron*ins+synapse if use_rowmajor else synapse*outs+neuron
      if need_flip:
        w_bin[dest_ind] = binarize(-weights[synapse][neuron])
      else:
        w_bin[dest_ind] = binarize(weights[synapse][neuron])
  # reshape the output as desired
  if use_rowmajor:
    w_bin = np.asarray(w_bin).reshape((outs, ins))
  else:
    w_bin = np.asarray(w_bin).reshape((ins, outs))
    
  return (w_bin, thresholds)

```

推論部は以下のようになっている。
閾値が積算値より低ければ0(true)で、高ければ1 (false)

``` C++
public:
  bool activate(unsigned const  nf, unsigned const  pe, TA const &accu) const {
#pragma HLS inline
    return  Compare()(m_threshold, accu);
  }
};

/**
 * Use a simple per-row threshold comparison as activation function.
 *
 * The thresholds are taken from an array indexed by output row.
 * It is currently public to allow direct initialization and
 * to make its name accessible for top-level HLS pragmas.
 *
 * The default comparison returns true if the threshold value defined for
 * the indexed row is smaller than the passed accumulator value.
 */
template<unsigned NF, unsigned PE,
	 typename TA, typename Compare = std::less<TA>>
class ThresholdsActivation : public Activation<TA, bool> {
public:
  TA  m_thresholds[PE][NF];

public:
  bool activate(unsigned const  nf, unsigned const  pe, TA const &accu) const {
#pragma HLS inline
    return  Compare()(m_thresholds[pe][nf], accu);
  }
};

```

参考: 

```
std::less::operator()
bool operator()( const T& lhs, const T& rhs ) const;
(until C++14)
constexpr bool operator()( const T& lhs, const T& rhs ) const;
(since C++14)
Checks whether lhs is less than rhs.

Parameters
lhs, rhs	-	values to compare
Return value
true if lhs < rhs, false otherwise.

Exceptions
(none)

Possible implementation
constexpr bool operator()(const T &lhs, const T &rhs) const 
{
    return lhs < rhs;
}
```

ようは活性化関数とBatch normlization（4つの変数）を1つの変数と比較によって表現できる（閾値フィルタリング的な感じ）ってこと。

# 4.3.1. 行列-ベクトル-閾値ユニット / The Matrix-Vector-Threshold Unit
The Matrix-Vector-Threshold Unit(MVTU)はアクセラレーター設計のための計算コアを形成している。BNNにおける計算処理の大部分は、matrix-vector演算に続き閾値処理で表現できる。例えば、インデックス$N$におけるFully Connected Neural Network Layerの活性化関数の前の出力$a_N$は、matrix-vector積の${a_N = A {a^{b}_{N-1}}}$によって与えられる。

ここで${A}$はSynapticの重み行列であり、${{a^{b}_{N-1}}}$は前の層(活性化関数を通った後)からの出力である。

活性化関数の後の出力は$a^b_N = a^N > \tau^+_N$によって計算でき、

閾値は4.2.2で説明した${\tau^+_N}$である。


畳み込みも次のように実装することができる。4.3.2節で説明するように、Matrix-Vector積を生成し、MVTUはFully Connected Layerをスタンドアロンコンポーネントとして実装し、畳み込みレイヤの一部として使用する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/95636/fed117c8-8dff-62ca-8b51-606145b436f6.png)

 

MVTUの全体的な構成を図5に示す。内部には、MVTUは入力バッファと出力バッファ、およびSIMDレーンと処理要素（PE）の配列で構成されています。