Google Colaboratory上でBERTを使い、日本語文章の二値分類を行う

この記事では、Google ColaboratoryのGPUを使用してBERTを動かし、日本語文章の二値分類を行います。

前回の記事の続きです。
詳細な説明は下記の記事で行なっていますので、初めての方に関しては、そちらを参考に進めてください。
[Google Colaboratory(GPU)上でBERTのチュートリアルを行う](https://qiita.com/Yuu94/items/e20373ecada2afa2cb4d)

学習済みモデルは、GoogleDriveに保存し、GoogleColaboratoryにGoogleDriveをマウントして使用します。

**＊注意**
GoogleDriveに学習済みモデルとデータセットを保存するため、1.6GB以上の空きが必要です。
一度だけ試す場合は、GoogleDriveとGoogleColaboratory、を連携せず、GoogleColaboratoryのGPUインスタンス上にモデルやデータセットを保存する方が良いかもしれません。

**＊宣伝**
新しく、BERTを用いて日本語文章の多値分類を行う記事を書きました。
次のステップとしてご利用ください。
[BERTを用いて、日本語文章の多値分類を行う](https://qiita.com/Yuu94/items/0e5cff226bd3cc8fb08c)

# BERTを使用した日本語文章の二値分類
ここでは、BERTとBERT日本語学習済みモデル、livedoor newsコーパスを使用して、GoogleColaboratory上で学習済みモデルのfine-tuningと文章の二値分類を行います。
分類は記事のタイトルを使用して行います。

fine-tuning、分類はBERTの公式が公開している`run_classifier.py`のコードを一部変更して行います。

基本的な流れは前回作成した記事と同じですので、詳しい方法を知りたい方は下記の記事をご確認ください。
[Google Colaboratory(GPU)上でBERTのチュートリアルを行う](https://qiita.com/Yuu94/items/e20373ecada2afa2cb4d)

それでは、早速実行していきましょう。

# 用意するもの
- Googleアカウント

# 二値分類の実施
今回は、livedoor newsコーパスの中から"it-life-hack"と"kaden-channel"を分類していきます。
分類に使用する情報は、記事のタイトルです。
(データセットをBERT向けのフォーマットに変換の際に使用するプログラムを変更することで、本文を用いたfine-tuning、予測が可能になります。)

内容を保存したColaboratory notebookを私のGitHubに公開していますので、ソースコードの方が理解しやすい方はそちらをご覧ください。

[Yuu94/bert-ja-two-classification](https://github.com/Yuu94/bert-ja-two-classification)

本記事は以下のような流れをとります。

1. 形態素解析器のインストール
1. GoogleDriveをマウント
1. データセット/BERT日本語学習済みモデルをGoogleDriveへ保存
1. データセットをBERT向けのフォーマットに変換
1. BERTのリポジトリをClone
1. プログラムの改変
1. trainデータを使用した、学習済みモデルのfine-tuning
1. テストデータの予測
1. 予測結果の検証

日本語学習済みモデルは 京都大学 黒橋・河原研究所が公開している`BERT日本語Pretrainedモデル`を、形態素解析器は`JUMAN`を使用します。

最終的には、GoogleDrive上が以下のようなディレクトリ構成になります。
上手く読み込めない等の問題があれば、これを確認しながら作業してみてください。

```text
マイドライブ/
　└ bert/
      └ livedoor_news
          ├ Japanese_L-12_H-768_A-12_E-30_BPE/ # 日本語学習済みモデル
          ├ tmp/ # 出力ファイル
              ├ livedoor_news_output_predic/
              └ livedoor_news_output_fine/
          ├ bert/
              ├tokenization.py
              └ run_classifier_livedoor.py
          ├ train.tsv
          ├ test.tsv
          ├ rand-all.tsv
          ├ dev.tsv
          ├ all.tsv
          └ livedoor_news_create_tsv.ipynb
```

# 形態素解析器のインストール
## JUMANのインストール
今回の学習済みモデルは、 京都大学 黒橋・河原研究所が公開している`BERT日本語Pretrainedモデル`を使用します。
上記のモデルは、同研究室が公開している日本語形態素解析器システム`JUMAN`を使用してモデルを学習させているため、同じ形態素解析器を使用します。
異なる形態素解析器を使用すると、学習済みモデルを作成した際の単語の最小単位と、新しくfine-tuningまたは入力データの単語の最小単位が異なり、新たに入力した単語が未知の単語扱いになるため、単語をベクトル化することができません。

学習済みモデルを使用する際、どの形態素解析器を使用すれば良いのかは、モデルを公開しているページに記述されていることが多いので、そこを確認する必要があります。
京都大学 黒橋・河原研究所が公開している`BERT日本語Pretrainedモデル`のページにも、どの形態素解析器を使用すれば良いのか記述がされているので、リンクを貼ります。
[BERT日本語Pretrainedモデル - KUROHASHI-KAWAHARA LAB - 詳細](http://nlp.ist.i.kyoto-u.ac.jp/index.php?BERT日本語Pretrainedモデル#r6199008)

それでは、Colaboratory notebookを使用して、GPUマシンにJUMANをインストールします。

下記のコマンドをセルに貼り付け、実行していください。

```
!wget https://github.com/ku-nlp/jumanpp/releases/download/v2.0.0-rc2/jumanpp-2.0.0-rc2.tar.xz && \
tar xJvf jumanpp-2.0.0-rc2.tar.xz && \
rm jumanpp-2.0.0-rc2.tar.xz && \
cd jumanpp-2.0.0-rc2/ && \
mkdir bld && \
cd bld && \
cmake .. \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX=/usr/local && \
make && \
sudo make install
```

以下のコマンドで、JUMANが問題なくインストールされているか確認します。

```
!jumanpp -v
```
`Juman++ Version: 2.0.0-rc2`と表示されれば、インストール完了です。

## TensorFlowのバージョン変更
本チュートリアルはTensorFlow 1系で動作するプログラムであるため、以下のコマンドを実行してTensorFlowのバージョンを1系に変更します。
デフォルトではTensorFlowは2系になっています。

```
%tensorflow_version 1.x
```


## pyknpのインストール
JUMANをPythonで使用するため、`pyknp`をpipインストールします。

pipはサーバーにインストール済みなので、以下のコマンドをセルに貼り付け、`pyknp`をインストールします。

```
!pip install pyknp
```

# GoogleDriveをマウント
学習済みモデルやデータセットを保存するGoogleDriveをマウントします。

以下のコードをセルに貼り付け、実行します。

```
from google.colab import drive 


drive.mount('/content/drive')
```

次に作業ディレクトリの作成と移動を行います。
今回は、GoogleDrive直下に`bert`ディレクトリを作成し、その中に`livedoor_news`ディレクトリを作成してその中で作業を行います。

そのため、以下のコマンドを使用して、ディレクトリを作成します。
(*別々のセルでコマンドを実行してください。)

```
!mkdir -p /content/drive/'My Drive'/bert/livedoor_news
```

```
cd /content/drive/'My Drive'/bert/livedoor_news
```

# データセット/BERT日本語学習済みモデルをGoogleDriveへ保存
## livedoor newsコーパスのダウンロード
下記のコマンドを使用して、データセットをダウンロードします。

```
import urllib.request

livedoor_news_url = "https://www.rondhuit.com/download/ldcc-20140209.tar.gz"
urllib.request.urlretrieve(livedoor_news_url, "ldcc-20140209.tar.gz")
```

## BERT日本語学習済みモデル
**＊注意**
学習済みモデルの容量1.6GBあるので、GoogleDriveの容量、ダウンロード時間にはご注意ください。
私の場合、ダウンロードは1時間以上かかった記憶がありますので、ダウンロードコマンドを実行したら、放置した方が良いと思います。

下記のコマンドをセルに貼り付け、実行し、モデルをダウンロードします。

```
kyoto_u_bert_url = "http://nlp.ist.i.kyoto-u.ac.jp/nl-resource/JapaneseBertPretrainedModel/Japanese_L-12_H-768_A-12_E-30_BPE.zip"
urllib.request.urlretrieve(kyoto_u_bert_url, "Japanese_L-12_H-768_A-12_E-30_BPE.zip")
```

ダウンロードが完了し、GoogleDriveの`bert/livedoor_news`に`Japanese_L-12_H-768_A-12_E-30_BPE.zip`が保存されていることを確認したら、下記のコマンドをColaboratory notebookで実行し、ZIPを解凍します。

```
!unzip Japanese_L-12_H-768_A-12_E-30_BPE.zip
```

問題がなければ、GoogleDrive上に`bert/livedoor_news/Japanese_L-12_H-768_A-12_E-30_BPE`ディレクトリが作成されます。

# データセットをBERT向けのフォーマットに変換
ダウンロードしたlivedoor newsコーパスを、BERTで扱いやすいようなフォーマットに変換します。
下記の記事を参考に作成しました。
[BERT多言語モデルで日本語文章の二値分類を試す](https://qiita.com/knok/items/9e3b4505d6b8f813943d)

次のプログラムをセルに貼り付け、実行します。
(実行後、GoogleDriveにマウントされるまで少し時間に差があります。)

```
import tarfile
import csv
import re

target_genre = ["it-life-hack", "kaden-channel"]

zero_fnames = []
one_fnames = []
tsv_fname = "all.tsv"

brackets_tail = re.compile('【[^】]*】$')
brackets_head = re.compile('^【[^】]*】')

def remove_brackets(inp):
    output = re.sub(brackets_head, '',re.sub(brackets_tail, '', inp))

    # 全角スペースを除去
    # output = output.replace("　", " ")
   
    return output

def read_title(f):
    next(f)
    next(f)
    title = next(f)
    title = remove_brackets(title.decode('utf-8'))
    return title[:-1]

with tarfile.open("ldcc-20140209.tar.gz") as tf:
    for ti in tf:
        if "LICENSE.txt" in ti.name:
            continue
        if target_genre[0] in ti.name and ti.name.endswith(".txt"):
            zero_fnames.append(ti.name)
            continue
        if target_genre[1] in ti.name and ti.name.endswith(".txt"):
            one_fnames.append(ti.name)
    with open(tsv_fname, "w") as wf:
        writer = csv.writer(wf, delimiter='\t')
        for name in zero_fnames:
            f = tf.extractfile(name)
            title = read_title(f)
            row = [target_genre[0], 0, '', title]
            writer.writerow(row)
        for name in one_fnames:
            f = tf.extractfile(name)
            title = read_title(f)
            row = [target_genre[1], 1, '', title]
            writer.writerow(row)
```

`bert/livedoor_news/`に`all.tsv`の作成が確認できたら、次のコードを実行して、学習用/テスト用のtsvファイルに分割します。

```
import random

random.seed(100)
with open("all.tsv", 'r') as f, open("rand-all.tsv", "w") as wf:
    lines = f.readlines()
    random.shuffle(lines)
    for line in lines:
        wf.write(line)

random.seed(101)

train_fname, dev_fname, test_fname = ["train.tsv", "dev.tsv", "test.tsv"]

with open("rand-all.tsv") as f, open(train_fname, "w") as tf, open(dev_fname, "w") as df, open(test_fname, "w") as ef:
    ef.write("class\tsentence\n")
    for line in f:
        v = random.randint(0, 9)
        if v == 8:
            df.write(line)
        elif v == 9:
            ef.write(line)
        else:
            tf.write(line)
```

# BERTのリポジトリをClone
BERTの公式リポジトリを以下のコマンドをセルにコピー/実行してクローンします。

```
!git clone https://github.com/google-research/bert.git
```

# プログラムの改変
ここでは、BERTのチュートリアルにある文章の等価性を評価するタスク(MRPC)のプログラムを改変し、文章の二値分類を行います。
そのため、Colaboratory notebookからGoogleDriveにCloneした`run_classifier.py`と`tokenization.py`をローカルで編集します。
今回は、元のプログラムを残したいので、`run_classifier.py`をローカルでコピーし、`run_classifier_livedoor.py`と名前を変更して中身を改変することにします。
`tokenization.py`に関しては、名前が変わるとプログラムを呼び出す部分も変更する必要があり(少々面倒なので)このままの名前で編集します。

下記の記事を参考に、改変を行いました。
[自然言語処理で注目のBERT ~取り敢えず動かしてみる編~](https://qiita.com/neonsk/items/27424d6122e00fe632b0)

## run_classifier_livedoor.pyの変更

変更する場所は以下の2点です。

1. LivedoorProcessorクラスの追加
1. 引数からLivedoorProcessorを呼び出せるようprocessorsに設定

### LivedoorProcessorクラスの追加
既に作成されている`ColaProcessor`クラスの下に、新しいクラス`LivedoorProcessor`を作成します。

```python:run_classifier_livedoor.py
class LivedoorProcessor(DataProcessor):
  """Processor for the MRPC data set (GLUE version)."""

  def get_train_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")

  def get_dev_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")

  def get_test_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "test.tsv")), "test")

  def get_labels(self):
    """See base class."""
    return ["0", "1"]

  def _create_examples(self, lines, set_type):
    """Creates examples for the training and dev sets."""
    examples = []
    for (i, line) in enumerate(lines):
      if i == 0:
        continue
      guid = "%s-%s" % (set_type, i)
      text_a = tokenization.convert_to_unicode(line[3])
      if set_type == "test":
        label = "0"
      else:
        label = tokenization.convert_to_unicode(line[1])
      examples.append(
          InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
    return examples
```

プログラムのベースは`MrpcProcessor`クラスですが、2つの文章(text_a, text_b)を比較する部分を一つの文章(text_a)に変更しています。

### processorsの設定
次に、`main関数`の中にある`processors`に先ほど追加したクラスの情報を追記します。

```python:run_classifier_livedoor.py
  processors = {
      "cola": ColaProcessor,
      "mnli": MnliProcessor,
      "mrpc": MrpcProcessor,
      "xnli": XnliProcessor,
      "livedoor": LivedoorProcessor, # 追記
  }
```

## tokenization.pyの変更
今回は、形態素解析器にJUMANを使用している為、`tokenization.py`にJUMANを使用して形態素解析を行うのプログラムを記述します。
`FullTokenizer`クラスの`__init__`と`tokenize`部位分を変更します。

```python:tokenization.py
class FullTokenizer(object):
  """Runs end-to-end tokenziation."""

  def __init__(self, vocab_file, do_lower_case=True):
    self.vocab = load_vocab(vocab_file)
    self.inv_vocab = {v: k for k, v in self.vocab.items()}
    # Jumanを使用する様に変更
    # self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case)
    self.jumanpp_tokenizer = JumanPPTokenizer()
    self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab)

  def tokenize(self, text):
    split_tokens = []
    # Jumanを使用する様に変更
    # for token in self.basic_tokenizer.tokenize(text):
    for token in self.jumanpp_tokenizer.tokenize(text):
      for sub_token in self.wordpiece_tokenizer.tokenize(token):
        split_tokens.append(sub_token)

    return split_tokens
```
これで、形態素解析を行う際にJUMANが使用されます。

# trainデータを使用した、学習済みモデルのfine-tuning
改変したプログラムを使用して、fine-tuningを行います。

以下のコマンドを実行してfine-tuningを行います。

```
!python bert/run_classifier_livedoor.py \
--task_name=livedoor \
--do_train=true \
--do_eval=true \
--data_dir=./ \
--vocab_file=./Japanese_L-12_H-768_A-12_E-30_BPE/vocab.txt \
--bert_config_file=./Japanese_L-12_H-768_A-12_E-30_BPE/bert_config.json \
--init_checkpoint=./Japanese_L-12_H-768_A-12_E-30_BPE/bert_model.ckpt \
--max_seq_length=128 \
--train_batch_size=32 \
--learning_rate=2e-5 \
--num_train_epochs=3.0 \
--output_dir=./tmp/livedoor_news_output_fine \
--do_lower_case False
```

問題なければ、以下の様に結果が出力されます。

```
INFO:tensorflow:***** Eval results *****
I0113 04:51:41.729829 140011245029248 run_classifier_livedoor.py:963] ***** Eval results *****
INFO:tensorflow:  eval_accuracy = 0.875
I0113 04:51:41.730009 140011245029248 run_classifier_livedoor.py:965]   eval_accuracy = 0.875
INFO:tensorflow:  eval_loss = 0.40959355
I0113 04:51:41.734117 140011245029248 run_classifier_livedoor.py:965]   eval_loss = 0.40959355
INFO:tensorflow:  global_step = 131
I0113 04:51:41.734362 140011245029248 run_classifier_livedoor.py:965]   global_step = 131
INFO:tensorflow:  loss = 0.40959355
I0113 04:51:41.734567 140011245029248 run_classifier_livedoor.py:965]   loss = 0.40959355
```
`accuracy`が`0.88`、`loss`が`0.41`なので、良い結果だと思います。

# テストデータの予測
fine-tuningを行なったモデルを使用して、test.tsvを二値分類します。

以下のコマンドを使用して、テストデータの予測を行います。

```
!python bert/run_classifier_livedoor.py \
  --task_name=livedoor \
  --do_predict=true \
  --data_dir=./ \
  --vocab_file=./Japanese_L-12_H-768_A-12_E-30_BPE/vocab.txt \
  --bert_config_file=./Japanese_L-12_H-768_A-12_E-30_BPE/bert_config.json \
  --init_checkpoint=./tmp/livedoor_news_output_fine \
  --max_seq_length=128 \
  --output_dir=tmp/livedoor_news_output_predic/
```
問題がなければ、GoogleDriveの`tmp/livedoor_news_output_predic/`に`test_results.tsv`が出力されます。
(出力結果がマウントされるのにし少し時間がかかります。)

# 予測結果の検証
text.tsvには、既に正解のカラムがありますので、BERTを使用して予測した値と正解の値を比較し、正解率を求めます。

下記のコードをセルにコピーして実行します。

```
import csv


with open("test.tsv") as f, open("tmp/livedoor_news_output_predic/test_results.tsv") as rf:
  test = csv.reader(f, delimiter = '\t')
  test_result = csv.reader(rf, delimiter = '\t')

  # 正解データの抽出
  next(test)
  test_list = [int(row[1]) for row in test ]

  # 予測結果を抽出
  result_list = [0 if row[0] > row[1] else 1 for row in test_result ]

  test_count = len(test_list)
  result_correct_answer_list = [result for test, result in zip(test_list, result_list) if test == result]
  result_correct_answer_count = len(result_correct_answer_list)
  print("正解率: ", result_correct_answer_count / test_count)
```
私の手元では、`正解率:  0.8874172185430463`が出力されました。

特に何もせず高い正解率が出ていると思います。

# 最後に
BERTで日本語の処理を行う手順をさらっとですが、ここで紹介しました。
次はできれば多項分類ができればと思っています。