ALBERTを使った汎用的な日本語QAモデルにリベンジ

# はじめに
以下記事のリベンジを行って、一応動作するところまでたどり着きました。
非常にニッチですが、ALBERTの日本語QAモデルの記事は他に無いのではと思い、改めて記事にした次第です。

https://qiita.com/yydevelop/items/e01324338314c1e2c1ec


### 手順
・既存の日本語事前学習モデルを使用
・SQuAD2.0 形式の公開データセットを使用
・データセットをNEologdで分かち書き←追加
・QAモデルとしてfinetuning

## ALBERTとは
A Lite BERT。
BERTの改良版。
軽量かつスコアもBERTよりいい(らしい)


## 結論
前回の失敗の原因は、学習データを分かち書きしていなかったためと思われる。
※BERTにはBertJapaneseTokenizerがありイイ感じにしてくれているが、ALBERTにはそれに相当するものがないためか。

# やったこと
## 事前学習データを用意する

前回同様にhuggingfaceからダウンロード可能なALBERT版の日本語事前学習モデルを使用させていただく。

https://note.com/jnory/n/n02d8ae0a79d5


## SQuAD2.0 形式の公開データセットを使用
これも引き続き日本語のSQuAD2.0のデータセットといえば、運転ドメインデータセット。

## データセットをNEologdで分かち書き
上記のデータセットは分かち書きされていない状態のデータなので、NEologdで分かち書きを行う。
※MeCabとNEologdの両方試したがNEologdのほうが精度が良さそうなので、そちらを採用
SQUAD2.0形式には回答の位置(start_ans)が必要だが、分かち書きをするとスペースによって位置がずれるため、start_ansを降り直す必要がある。

上記の処理を行うためのロジック

```python
# 必要なパッケージのインストール
!apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab > /dev/null
!pip install mecab
!pip install mecab-python3
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git > /dev/null 
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n > /dev/null 2>&1
!ln -s /etc/mecabrc /usr/local/etc/mecabrc

# インポート
import csv
import json
import MeCab
import pandas as pd

# 関数、入力ファイル名・出力ファイル名を引数
def conv_neologd(input_file, output_file):
	# ファイルをオープン
	with open(input_file, "r", encoding='utf-8') as f:
		train = json.load(f)

	# Mecabの定義
	mtagger = MeCab.Tagger("-Owakati -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd")

	# JSONの要素ごとに分解
	# 要素ごとに取得、分かち書き、更新を行う
	for data in train["data"]:
		paragraphs = data["paragraphs"]
		for paragraph in paragraphs:
			# 本文(context)
			context = paragraph["context"]
			context = mtagger.parse(context).replace(' \n', '')
			paragraph["context"] = context
			for qas in paragraph["qas"]:
				# 質問(question)
				question = qas["question"]
				question = mtagger.parse(question).replace(' \n', '')
				qas["question"] = question

				# 回答(text)
				text = qas["answers"][0]["text"]
				text = mtagger.parse(text).replace(' \n', '')
				qas["answers"][0]["text"] = text

				# 回答位置(answar_start)
				# 回答(text)が本文(context)内のどこにあるのかを探索して更新
				qas["answers"][0]["answer_start"] = context.find(text)

	# ファイル出力
	with open(output_file, 'w', encoding='utf-8') as f:
	  json.dump(train, f, ensure_ascii=False)
```



## QAモデルとしてfinetuning
Googleドライブをマウント

```python
from google.colab import drive
drive.mount('/content/gdrive')
```

初期化。
transformerは2.9.1を指定して使用。
※最新バージョンにすると、Tokenizerの動きが変わって上手く動かなかったため。

```python
!pip install transformers==2.9.1
!git clone https://github.com/huggingface/transformers -b v2.9.1 --depth 1 
!git clone https://github.com/NVIDIA/apex
!pip install -v --no-cache-dir apex/
!apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab > /dev/null
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git > /dev/null 
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n > /dev/null 2>&1
!pip install mecab-python3 > /dev/null
!ln -s /etc/mecabrc /usr/local/etc/mecabrc
```

事前学習モデル(ALINEAR/albert-japanese-v2)、分かち書きしたデータセットを使用し、run_squad.pyでfinetuning

```python
!mkdir albert_model_output
!rm  cached_dev_albert-japanese-v2_384 cached_train_albert-japanese-v2_384

!python transformers/examples/question-answering/run_squad.py\
  --model_type  "albert-japanese-v2" \
  --model_name_or_path  "ALINEAR/albert-japanese-v2" \
  --train_file "DDQA-1.0_RC-QA_train_neologd.json" \
  --do_train \
  --per_gpu_train_batch_size 12 \
  --predict_file "DDQA-1.0_RC-QA_dev_neologd.json" \
  --learning_rate 3e-5 \
  --num_train_epochs 2.0 \
  --max_seq_length 384 \
  --doc_stride 128 \
  --fp16 \
  --do_eval \
  --save_steps 3000 \
  --version_2_with_negative \
  --output_dir albert_model_output/
```

### finetuning出力
```
INFO - __main__ -   Results: {'exact': 74.34908389585343, 'f1': 83.56155105485752, 'total': 1037, 'HasAns_exact': 74.34908389585343, 'HasAns_f1': 83.56155105485752, 'HasAns_total': 1037, 'best_exact': 74.34908389585343, 'best_exact_thresh': 0.0, 'best_f1': 83.56155105485752, 'best_f1_thresh': 0.0}
```

exactが74%を超えており、まともにfinetuningがている様子。
※分かち書き前は、exactが1%程度だった


### Tokenizerの準備
Bert～のところをAlbert～に変えただけ

```python
from transformers import AlbertTokenizer, AlbertForQuestionAnswering, AutoTokenizer, AutoConfig,AlbertConfig
import torch
model_directory = "output/albert_model_output"
pretrained_model = "ALINEAR/albert-japanese-v2"
 
config = AlbertConfig.from_pretrained(model_directory + "/config.json")
tokenizer_config = AlbertConfig.from_pretrained(model_directory + "/tokenizer_config.json")

model = AlbertForQuestionAnswering.from_pretrained(pretrained_model, config=config)
tokenizer = AutoTokenizer.from_pretrained(pretrained_model, config=tokenizer_config)
model.load_state_dict(torch.load(model_directory + "/pytorch_model.bin", map_location=torch.device('cpu')))
```

### QAタスクの実行

SQUAD2.0の問題に答えてもらう。
ノルマンディー=フランスに回答できており、ちゃんと動作しているものと思われる。（たぶん）
※日本語QAモデルとしてどこまでできていればいいものなのか。。。

```python
import MeCab
def predict(quesion, text):
  input_ids = tokenizer.encode(quesion, text)
  token_type_ids = [0 if i <= input_ids.index(3) else 1 for i in range(len(input_ids))]
  start_scores, end_scores = model(torch.tensor([input_ids]), token_type_ids=torch.tensor([token_type_ids]))
  score = torch.max(start_scores).item() + torch.max(end_scores).item()
  all_tokens = tokenizer.convert_ids_to_tokens(input_ids)
  prediction = ''.join(all_tokens[torch.argmax(start_scores) : torch.argmax(end_scores)+1])
  prediction = prediction.replace("▁", "")
  return prediction, score

tagger = MeCab.Tagger("-Owakati -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd")

text = """
ノルマン（ノルマン：ノルマンド;フランス語：ノルマンド;ラテン：ノルマンニ）は、10世紀および11世紀にフランスの地域ノルマンディーに名前を与えた人々でした。彼らは北欧出身です（「ノルマン」は「ノルマン」から来ています）デンマーク、アイスランド、ノルウェーの襲撃者と海賊は、彼らのリーダーであるロロの下で、西フランシアのチャールズ3世に忠誠を誓うことに同意しました。数世代にわたる同化とネイティブのフランク族とローマ・ゴーリッシュ族との混合を通じて、彼らの子孫は次第に西フランシアのカロリング派の文化に溶け込んでいきました。ノルマン人の明確な文化的および民族的アイデンティティは、10世紀前半に最初に出現し、その後数世紀にわたって進化を続けました。
"""
text = tagger.parse(text)

quesion = "ノルマンディーはどの国にありますか？"
quesion = tagger.parse(quesion)

prediction = predict(quesion, text)
print("モデルの予想結果:", prediction)

```

```
モデルの予想結果: ('フランス', 10.422927856445312)
```

# 最後に
時間があれば、もう少し精度の確認やBERTのモデルとの比較などやってみたいと思います。