[concrete-numpy①] 環境構築して少し使ってみた 

## はじめに
本記事では、concrete-numpyというOSSと戯れようと思います。技術的な解説などは皆無です。ごめんなさい。

## concrete-numpyとは
concrete-numpyとは、Zama社が公開している準同型暗号を用いたnumpyのOSSです。（pytorchのモデルをコンパイルしたりもできるらしい）Zama社はこのほかにも[TFHEという準同型暗号方式の改良版をRustで実装したOSS](https://github.com/zama-ai/concrete)なども公開しています。

準同型暗号って何？という人は以下の記事などを参考にしてみてください。

https://qiita.com/nindanaoto/items/98335ad4d32b927effa9

https://docs.zama.ai/concrete-numpy/stable/user/explanation/what_is_fhe.html


## 環境構築
まずは、環境の構築をしていきます。pipで落としてくる方法もあるのですが、今回はdockerで環境構築したいと思います。

ディレクトリ構成は以下のようになります。

concrete-numpy/
&nbsp;&nbsp;├src/
&nbsp;&nbsp;&nbsp;│&nbsp;&nbsp;└sample.py
&nbsp;&nbsp;└docker-compose.yml

以下のコマンドでimageをpullします。

```
docker pull zamafhe/concrete-numpy:v0.2.0
```

docker-compose.ymlを以下のように編集します。

```docker-compose.yml
version: '3'
services:
  concrete-numpy:
    image: zamafhe/concrete-numpy:v0.2.0
    container_name: concrete-numpy
    volumes:
      - ./src/:/concrete-numpy/src/
    tty: true
```

最後に以下のコマンドでコンテナを作成、起動します。

```
cd concrete-numpy
docker compose up -d
docker exec -it concrete-numpy bash
```


## 遊んでみる
Zama社が提供している簡単なサンプルコードを実行してみます。

```sample.py
import concrete.numpy as hnp

def add(x, y):
  return x + y

if __name__ == "__main__":
  inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1), (3, 2), (6, 1), (1, 7), (4, 5), (5, 4)]
  compiler = hnp.NPFHECompiler(add, {"x": "encrypted", "y": "encrypted"})

  print(f"Compiling...")
  circuit = compiler.compile_on_inputset(inputset)

  print(str(compiler))

  examples = [(3, 4), (1, 2), (7, 7), (0, 0), (50, 50)]
  for example in examples:
    result = circuit.run(*example)
    print(f"Evaluation of {' + '.join(map(str, example))} homomorphically = {result}")
```

今回コンパイルする関数は`add`という足し算の関数です。どんな関数でもコンパイルできるわけではなく、[制限](https://docs.zama.ai/concrete-numpy/stable/user/explanation/fhe_and_framework_limits.html)があります。

以下の部分で`add`関数をコンパイルしています。`"x": "encrypted"`は`x`という引数を暗号化することを意味します。

```
compiler = hnp.NPFHECompiler(add, {"x": "encrypted", "y": "encrypted"})
```

コンパイルした関数に対して、複数の入力パターンをセットして、中間結果のビット幅を決定しているのが以下の部分です。つまり、この入力パターンによって扱える（正しい答えを出力できる）値の範囲が決まります。入力パターンは最低でも10パターン以上がいいそうです。

```
circuit = compiler.compile_on_inputset(inputset)
```

`compiler`を出力してみると以下のようになります。なんとなくコンパイルされている感はありますね。`<uint4>`は符号付き整数4ビットであることを示しています。

```
%0 = x                  # EncryptedScalar<uint4>
%1 = y                  # EncryptedScalar<uint4>
%2 = add(%0, %1)        # EncryptedScalar<uint4>
return %2
```

`circuit.run`の実行結果は以下のようになります。`50+50`だけ間違った結果になりました。最初にコンパイルした入力パターンと値が大きく違うことで、正しい答えが出力されないことが確認できました。

```
Evaluation of 3 + 4 homomorphically = 7
Evaluation of 1 + 2 homomorphically = 3
Evaluation of 7 + 7 homomorphically = 14
Evaluation of 0 + 0 homomorphically = 0
Evaluation of 50 + 50 homomorphically = 4
```

入力パターンに`(60, 60)`を追加したら、`50+50`も正しく計算できました。ただ、当たり前ですが、かなりおもくなりました。

本記事公開時点では、かなり限定的な実装で以下のような制限があるようです。

- 符号なし整数のみサポート（最大7ビット）
- 7ビットの計算は10%程度の確率で間違う

ただし、小数を整数にマッピングする手法は実装されているそうです。

## 最後に
本記事では、concrete-numpyと少し戯れてみました。まだ、機能が十分に実装されていなく、関数内で暗号化→計算→復号を行っているので、準同型暗号の計算を行っている感はあまりなかったです。ただ、処理は重かったのでその点では準同型暗号の計算を行っている感はありました笑。これから、鍵生成や暗号化のAPIも実装されるらしいので乞うご期待！という感じですね。次は機械学習モデルのコンパイルなども試してみたいと思います！（また、理論的な部分も頑張って理解したいと思います、、、）