UTH-BERTにおけるhuggingfaceのトークナイザ設定

# はじめに

東大が公開しているUTH-BERTは診療録を用いて事前学習しており、医療テキストの様々なタスクを解くにはとても優秀な事前学習モデルです。
また基本となるBERTモデルで学習していることもあり、日本語医療テキストのベースラインとして重宝させて頂いています。

https://ai-health.m.u-tokyo.ac.jp/home/research/uth-bert

https://github.com/jinseikenai/uth-bert

# 動機

元ソースのtensorflowに基づく方法はうまいことトークナイズしてくれる（あたりまえ）。
一方で、色々な事前学習モデルで検討したいので、pytorchのhuggingfaceのtransformersライブラリを利用したスクリプトに統一したい。
とはいえ、他の記事を参考にしてtransformersでモデルを読み込んで利用しようとすると、元ソースの様にトークナイズできない。

# 元ソース

Original text

    2002 年夏より重い物の持ち上げが困難になり，階段の昇りが遅くなるなど四肢の筋力低下が緩徐に進行した．2005 年 2 月頃より鼻声となりろれつが回りにくくなった．また，食事中にむせるようになり，同年 12 月に当院に精査入院した。

After pre-processing

    ２００２年夏より重い物の持ち上げが困難になり、階段の昇りが遅くなるなど四肢の筋力低下が緩徐に進行した．２００５年２月頃より鼻声となりろれつが回りにくくなった．また、食事中にむせるようになり、同年１２月に当院に精査入院した。

After tokenization

    ['２００２年', '夏', 'より', '重い', '物', 'の', '持ち上げ', 'が', '困難', 'に', 'なり', '、', '階段', 'の', '[UNK]', 'が', '遅く', 'なる', 'など', '四肢', 'の', '筋力低下', 'が', '緩徐', 'に', '進行', 'し', 'た', '．', '２００５年', '２', '月頃', 'より', '鼻', '##声', 'と', 'なり', 'ろ', '##れ', '##つ', 'が', '回り', '##にく', '##く', 'なっ', 'た', '．', 'また', '、', '食事', '中', 'に', 'むせる', 'よう', 'に', 'なり', '、', '同年', '１２月', 'に', '当', '院', 'に', '精査', '入院', 'し', 'た', '。']

# とりあえず動いてしまう単純なコード

```python
model_checkpoint = "./UTH_BERT_BASE_512_MC_BPE_WWM_V25000"

tokenizer = BertJapaneseTokenizer.from_pretrained(model_checkpoint)
```

BertJapaneseTokenizerにvocab.txtとconfig.jsonが入ったフォルダのパスを指定のみ

    ２００２年,##夏,##よ,##り,##重,##い,##物,##の,##持ち,##上げ,##が,##困難,##になり,、,階段,##の,##昇,##り,##が,##遅,##く,##なる,##な,##ど,##四肢,##の,##筋力低下,##が,##緩,##徐,##に,##進行,##した,．,２００５年,##２,##月,##頃,##よ,##り,##鼻,##声,##と,##なり,##ろ,##れ,##つ,##が,##回,##り,##にく,##く,##な,##っ,##た,．,また,、,食事,##中,##に,##むせる,##よ,##う,##になり,、,同年,##１２,##月,##に,##当,##院,##に,##精,##査,##入院,##した,。

前処理は公式どおりにしたもののサブワード多すぎだし、NEologdと万病辞書を使っていない。

# 他の記事を参考にして修正

```python
model_checkpoint = "./UTH_BERT_BASE_512_MC_BPE_WWM_V25000"

tokenizer = BertJapaneseTokenizer.from_pretrained(
    model_checkpoint,
    word_tokenizer_type="mecab",
    mecab_kwargs={
        "mecab_dic": None,
        "mecab_option": "-u /hogehoge/fugafuga/MANBYO_201907_Dic-utf8.dic -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd",
    }
)
```


形態素解析をMeCabに指定し、デフォのipadicじゃなくてNEologdとユーザー辞書に万病辞書を指定。
辞書指定は絶対パスとし、NEologdはUbuntuに入れるとここに入ってた。

    [UNK],夏,より,重い,物,の,持ち上げ,が,困難,に,なり,、,階段,の,[UNK],が,遅く,なる,など,四肢,の,筋力低下,が,緩徐,に,進行,し,た,[UNK],[UNK],[UNK],頃,より,鼻,##声,と,なり,ろ,##れ,##つ,が,回り,##にく,##く,なっ,た,[UNK],また,、,食事,中,に,むせる,よう,に,なり,、,同年,[UNK],に,当,院,に,精査,入院,し,た,。

あれま、数字関連が未知語に...
万病辞書を指定しないと``筋力低下``が``筋力,低下``に分割されてしまうが、NEologdは指定の有無で結果は変わらず。

# 修正

調べてみても先人達の記録が見つからないので、transformersのソースコードを読んで辿る事に...

1. BertJapaneseTokenizerは、``word_tokenizer_type="mecab"``によって``transformers.MecabTokenizer``で形態素解析している
1. ``transformers.MecabTokenizer``の``tokenize``で何やらNFKC正規化とlowerで小文字化を制御している様子
1. ``__init__``は``normalize_text=True``となっている。NFKC正規化はしてもらわなくていいので原因はこいつか。

```python:MecabTokenizerから抜粋
def tokenize(self, text, never_split=None, **kwargs):
    """Tokenizes a piece of text."""
    if self.normalize_text:
        text = unicodedata.normalize("NFKC", text)

    never_split = self.never_split + (never_split if never_split is not None else [])
    tokens = []

    for word in self.mecab(text):
        token = word.surface

        if self.do_lower_case and token not in never_split:
            token = token.lower()

        tokens.append(token)

    return tokens
```
``BertJapaneseTokenizer``から``MecabTokenizer``の引数``normalize_text``に``False``を渡したいので、``mecab_kwargs``に追記したらよさそうなので、以下に修正。
ついでに、モデルの最大トークン数が事前情報にないので``model_max_length=512``で指定

```python
model_checkpoint = "./UTH_BERT_BASE_512_MC_BPE_WWM_V25000"

tokenizer = BertJapaneseTokenizer.from_pretrained(
    model_checkpoint,
    word_tokenizer_type="mecab",
    mecab_kwargs={
        "mecab_dic": None,
        "normalize_text": False,
        "mecab_option": "-u /hogehoge/fugafuga/MANBYO_201907_Dic-utf8.dic -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd",
    },
    model_max_length=512
)
```

公式の例どおりにトークナイズできました。

    ２００２年,夏,より,重い,物,の,持ち上げ,が,困難,に,なり,、,階段,の,[UNK],が,遅く,なる,など,四肢,の,筋力低下,が,緩徐,に,進行,し,た,．,２００５年,２,月頃,より,鼻,##声,と,なり,ろ,##れ,##つ,が,回り,##にく,##く,なっ,た,．,また,、,食事,中,に,むせる,よう,に,なり,、,同年,１２月,に,当,院,に,精査,入院,し,た,。

厳密には、人名だったら``＠＠Ｎ``に置換とか、本家どおりではないです。


# BertJapaneseTokenizerじゃなくて統合化されたAutoTokenizerに対応させる

AutoTokenizerは様々なモデルに対して自動的に振り分けてくれて便利だが、UTH-BERTのパス指定だとBertJapaneseTokenizerに渡してくれないので想定通りに動かない。
問題はconfig.jsonの設定が不足していること。

## config.jsonの修正

```json
{
  "attention_probs_dropout_prob": 0.1,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "initializer_range": 0.02,
  "intermediate_size": 3072,
  "max_position_embeddings": 512,
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "type_vocab_size": 2,
  "vocab_size": 25000,
  "model_type": "bert",
  "tokenizer_class": "BertJapaneseTokenizer"
}
```

要は、``"model_type": "bert"``と``"tokenizer_class": "BertJapaneseTokenizer"``を追記するだけ。