【深層学習】TabBERTモデルによるクレジットカードの不正利用検知 ~環境構築・事前学習編~

# はじめに
※こちらは、8月に投稿した記事にコード解説の項目を加筆して再投稿したものとなります。以前の記事よりマシになっているはずなので、もう一度読んでいただけると幸いです。

今まで主にスマホアプリ開発をしていたのですが、2022年8月からAI関連の案件に携わっており、BERTという自然言語処理モデルについて勉強しています。

最近は、多変量の時系列表データの学習に使用する、TabBERT(Hierarchical Tabular BERT)というBERTの応用モデルに関する論文を読みました(Qiitaにも紹介記事を書きました)。

https://arxiv.org/abs/2011.01843

https://qiita.com/suzuki0430/items/90fa6e2776afc24b2e77

この論文、なんとありがたいことに、著者が事前学習のコードとデータをGitHubにあげています。

https://github.com/IBM/TabFormer

環境構築やモデル学習の経験を積むことで理解をさらに深められると思い、こちらのコードをいろいろ触ってみることにしました。
とりあえず第一歩として、AWSで仮想環境をたてて事前学習を実行するところまでをやってみました。

以下について知りたい方に、特に役立つ記事になると思います。
- GPUインスタンスの立て方(AMIとインスタンス選び、vCPUの制限解除方法)
- Anacondaによる環境構築(condaのエラー解消、仮想環境の選択)
- 事前学習まわりのコードの中身

※仮想環境を選択したのは、ローカルマシンのGPUがNVIDIA製ではなく、CUDAとcuDNNが動かせなかったためです。`setup.yml`で環境構築を行おうとしたら、`ResolvePackageNotFound`エラーでいきなり失敗しました...

```
(base) [9:10:58] → conda env create -f setup.yml
                                     
Collecting package metadata (repodata.json): | / done
Solving environment: failed

ResolvePackageNotFound:
  - pytorch==1.7.1=py3.8_cuda11.0.221_cudnn8.0.5_0
```

```yml:setup.yml
name: tabformer
channels:
  - anaconda
  - pytorch
  - huggingface
  - conda-forge
dependencies:
  - python>=3.8
  - pip>=21.0
  - pytorch=1.7.1=py3.8_cuda11.0.221_cudnn8.0.5_0
  - torchvision
  - pandas
  - scikit-learn
  - transformers
  - numpy
  - libgcc
  - pip:
      - transformers==3.2.0
```

# 環境構築
AWSでGPUインスタンスをたてて、その中でさらにAnacondaで仮想環境をつくるまでの流れとなります。

## GPUインスタンスの作成
AWSで用意されていたDeep Learning用のAMIと一番安価なGPUインスタンスを使用します。

- AMI(Amazon Machine Image): amazon/Deep Learning AMI GPU PyTorch 1.12.0 (Amazon Linux 2) 20220803
- インスタンスのタイプ: g4dn.2xlarge(xlargeだとメモリが枯渇したため)　＊説明ではg4ad.xlargeとなっています。すみません。
- EBSボリューム: 300 GB(45 GBだとストレージが枯渇したため`OSError: [Errno 28] No space left on device`)

![スクリーンショット 2022-08-11 22.18.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/9a16d650-b0a5-5613-73d7-b61d9c5d6fc9.png)

インスタンスを起動しようとしたら失敗しました。
![スクリーンショット 2022-08-11 22.23.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/7e10526b-f418-855a-0856-76438e19dc60.png)

g4adインスタンスを作成するのに必要なvCPUが足りないとのことです。
![スクリーンショット 2022-08-11 22.25.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/dc6ecb44-c9de-3188-14d0-52f1eb5c88f0.png)

右上の「制限緩和のリクエスト」から以下のリクエストを送ったところ、4時間くらいで制限が解除され、無事にインスタンスを起動することができました。
![スクリーンショット 2022-08-11 22.27.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/be3283e0-4056-de06-16cc-a12f0b4195e8.png)

## Anacondaで仮想環境構築
SSHでEC2インスタンスに接続します。
※パブリックIPはコンソール画面の`EC2 > インスタンス`から`パブリックIPv4アドレス`をコピーしてください。

```
(base) [10:55:26] → ssh -i ~/.ssh/MyKeyPair.pem ec2-user@<パブリックIP>
```
インスタンスにコードを落とします。

```
[ec2-user@ip-172-31-21-45 ~]$ git clone https://github.com/IBM/TabFormer.git
```
`setup.yml`のあるディレクトリに移動して、`conda env create ~`を実行します。

```
[ec2-user@ip-172-31-21-45 ~]$ cd TabFormer
[ec2-user@ip-172-31-21-45 TabFormer]$ conda config --set channel_priority flexible
[ec2-user@ip-172-31-21-45 TabFormer]$ conda env create -f setup.yml
```
※`conda config --set channel_priority flexible`をしないと、以下の文言が出て構築に失敗します。

```
Collecting package metadata (repodata.json): done
Solving environment: /
Found conflicts! Looking for incompatible packages.
```

`conda init`後に、作成した仮想環境(tabformer)をアクティベートします(アクティベート前に再度リモートログインが必要)。

```
[ec2-user@ip-172-31-21-45 TabFormer]$ conda init bash
[ec2-user@ip-172-31-21-45 TabFormer]$ conda activate tabformer
```

最後に、特定バージョンの`cudatoolkit`と`pytorch`がコンフリクトするというAnacondaのバグがあったので、以下を実行します。

```
[ec2-user@ip-172-31-21-45 TabFormer]$ pip install torch==1.7.1+cu110 torchvision==0.8.2+cu110 torchaudio===0.7.2 -f https://download.pytorch.org/whl/torch_stable.html
```
※以上をインストールしないと、python実行時に以下のエラーが出ます。

```
OSError: /home/ka37/anaconda3/envs/fail/lib/python3.8/site-packages/torch/lib/../../../../libcublas.so.11: symbol free_gemm_select version libcublasLt.so.11 not defined in file libcublasLt.so.11 with link time reference
```
https://github.com/pytorch/pytorch/issues/51080

### 追記
まだ以下のようなエラーが出る場合があります。

```
ImportError: /lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.29' not found (required by /opt/conda/envs/tabformer-opt/lib/python3.8/site-packages/pandas/_libs/window/aggregations.cpython-38-x86_64-linux-gnu.so)
```

`libstdc++.so.6`に`GLIBCXX_3.4.29`のバージョンが含まれていないとのことです。

そもそも`libstdc++.so.6`のパスが間違っていそうなので、こちらをconda環境が含まれているパスに修正します。
以下のコマンドでパスを調べます。

```
find / -name "libstdc++.so*"
```
すると、conda環境を含むパスに`libstdc++`を確認できます。

```
...
/opt/conda/envs/tabformer-opt/lib/libstdc++.so.6
/opt/conda/envs/tabformer-opt/lib/libstdc++.so.6.0.21
/opt/conda/envs/tabformer-opt/lib/libstdc++.so.6.0.29
```
`libstdc++.so.6`のままだと`GLIBCXX_3.4.29`は存在しないので、`libstdc++.so.6.0.29`のパスに変更します。

```
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/conda/envs/tabformer/lib
```

https://www.cnblogs.com/muahao/p/9078611.html

https://stealthinu.hatenadiary.jp/entry/2022/08/09/121154

# 事前学習
論文では2種類のデータが用意されているのですが、今回はクレジットカードのトランザクションデータを使用し、学習を行います。

まずは、ローカルからデータをコピーします。
ディレクトリごとコピーするために、`-r`オプションをつける必要があります。
*READMEに記載されていた`./data/card/`ではなく、`./data/prsa/`にデータをコピーします(多分誤記)。

```
(base) [14:31:15] → scp -r -i ~/.ssh/MyKeyPair.pem ~/Programs/TabFormer/data/credit_card ec2-user@18.180.227.218:/home/ec2-user/TabFormer/data/
```
学習を実行します。

```
[ec2-user@ip-172-31-21-45 TabFormer]$ python main.py --do_train --mlm --field_ce --lm_type bert --field_hs 64 --data_type card --output_dir ./output_card/
```

6時間でストレージ(300 GB)がいっぱいになってしまいましたが、とりあえず動きました。
以下が学習が止まった時点でのcheckpoint(35000, 35500)の中身です。

![スクリーンショット 2022-08-14 22.47.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/2304ac8b-eb53-feac-db1b-0e16cff8150a.png)
作成された学習済モデル`pytorch_model.bin`や`config.json` `optimizer.pt`をFine-Tuningの際に使用します。
以下が`config.json`と`pytorch_model.bin`の読み込み例です。

```py:prediction_card.py
from transformers import BertConfig, BertModel

config = BertConfig.from_pretrained(
    "./output_card/checkpoint-35000/config.json"
)

model = BertModel.from_pretrained("./output_card/checkpoint-35000/pytorch_model.bin", config=config)

print(config)
print(model)
```

```bash:config
BertConfig {
  "architectures": [
    "TabFormerHierarchicalLM"
  ],
  "attention_probs_dropout_prob": 0.1,
  "field_hidden_size": 64,
  "flatten": false,
  "gradient_checkpointing": false,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "initializer_range": 0.02,
  "intermediate_size": 3072,
  "layer_norm_eps": 1e-12,
  "max_position_embeddings": 512,
  "model_type": "bert",
  "ncols": 12,
  "nhead": 8,
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "num_layers": 1,
  "pad_token_id": 0,
  "total_flos": 41577468710400000,
  "type_vocab_size": 2,
  "vocab_size": 143492
}
```

```bash:pytorch_model
BertModel(
  (embeddings): BertEmbeddings(
    (word_embeddings): Embedding(143492, 768, padding_idx=0)
    (position_embeddings): Embedding(512, 768)
    (token_type_embeddings): Embedding(2, 768)
    (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
    (dropout): Dropout(p=0.1, inplace=False)
  )
  (encoder): BertEncoder(
    (layer): ModuleList(
      (0): BertLayer(
        (attention): BertAttention(
          (self): BertSelfAttention(
            (query): Linear(in_features=768, out_features=768, bias=True)
            (key): Linear(in_features=768, out_features=768, bias=True)
            (value): Linear(in_features=768, out_features=768, bias=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
          (output): BertSelfOutput(
            (dense): Linear(in_features=768, out_features=768, bias=True)
            (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
    ...
```

# コード解説
コード全文の説明は難しいので、要点のみ解説します。
以下が`main.py`です。

https://github.com/IBM/TabFormer/blob/main/main.py

## Dataset
`TransactionDataset`クラスはPytorchの`Dataset`クラスを継承しており、データの前処理やトークンとidの対応を辞書に登録するようなメソッドが登録されています。


- `encode_data()`：生データを前処理データに変換
- `init_vocab()`：トークンとidの対応を登録(辞書作成)
- `user_level_data()`：ユーザーごとのデータとラベルをすべての連結してリストで出力
    - `unique_users`: Userカラムのリスト(24001行までなら[0 1])
    - `user_data`: Userごとのデータフレーム
    - `trans_data`: 1Userのラベル以外のデータを全部まとめたリスト[[…], […], […]…] special tokenはまだ入っていない
    - `trans_labels`: 1Userのラベルを全部まとめたリスト
- `format_trans()`：user_level_dataで出力された連結データを入力して、11フィールドずつのレコードに分割。その後にトークンをidに変換、[SEP]トークンを付加して出力
- `prepare_samples()`：format_transで出力されたデータとラベルを10データ(window)ずつ連結して出力
    - `user_idx`: trans_dataのインデックス
    - `user_row`: 1Userの生データリスト
    - `user_row_ids`: 1Userのid変換後のリスト [SEP]付与済
    - `ids`: strideで5こずつずらしながらseq_lenでuser_row_idsを10連結ずつに区切ったリスト[…]
    - `self.data`: 10連結リストidsをまとめたリスト[[…], […]…]　全User含む
- `__getitem__()`：prepare_samplesで作成されたデータからindexに対応したものを取得。flatten(平坦化)フラグがFalseであれば10行(window)ずつreshape[10, 12]。最後のカラムはformat_transで付与した[SEP]

https://github.com/IBM/TabFormer/blob/main/dataset/card.py

自然言語(例えば英語)の場合であれば、単語を`BertTokenizer`に入力すれば対応idを出力してくれますが、TabBERT用のTokenizerは存在しないため、辞書を作成するような処理が必要となります。
トークンから辞書を作成するメソッド`set_id`、辞書からidを取得するメソッド`get_id`は`Vocabulary`クラスから呼び出して使用しています。

https://github.com/IBM/TabFormer/blob/main/dataset/vocab.py

## Model
`TabFormerBertLM`からモデルを読み込みます。

https://github.com/IBM/TabFormer/blob/main/models/modules.py

https://github.com/IBM/TabFormer/blob/main/models/tabformer_bert.py

## DataCollator
`DataCollatorForLanguageModeling`を継承した`TransDataCollatorForLanguageModeling`を作成し、トークンのマスク化やinput_idsとlabels(MASKの正解)の出力を行います。

- `mask_tokens()`：MLMのためにトークンをマスク化
- `__call__()`：Datasetの__getitem_＿で取得したデータをバッチサイズ分まとめてinput_ids([8, 10, 12])とlabelsとしてTensorで出力。バッチサイズは固定。バッチ内のUserは混ざっている。

https://github.com/IBM/TabFormer/blob/main/dataset/datacollator.py

## Trainer
Dataset、Model、DataCollatorをそれぞれ引数にとり、事前学習をやってくれます。
DataLoaderがラップされており、DataCollatorの`__call__`の処理はこの中で実行してくれます。

https://github.com/huggingface/transformers/blob/v4.22.1/src/transformers/trainer.py#L223

# おわりに
Fine-Tuning以降のコードはなさそうだったので、現在調査しながら実装しています。
実装できたら新しく記事を書きます。

# 参考資料
https://hacknote.jp/archives/55400/

https://qiita.com/RHM/items/24d7f79ea6417ab78621

https://dev.classmethod.jp/articles/how-to-choose-ec2-gpu-instance/

https://www.yoheim.net/blog.php?q=20180314

http://www.yamamo10.jp/yamamoto/comp/Python/settings/virtual_env/index.php

https://github.com/huggingface/transformers

https://zenn.dev/ttya16/articles/3c51001f9e6d4b0ecc0b