huggingfaceでの自然言語処理事始めBERT系モデルの前処理方法

# はじめに

自然言語処理の学習では利用するモデルに応じて文章中の単語のトークン化など様々な前処理を行う必要があります。今回は、自然言語処理で有名なhuggingfaceのライブラリを利用することで**モデル依存の工程をなるべく少なく前処理を行う**方法を紹介したいと思います。なお、本記事は使用する機械学習のフレームワークとして**PyTorch**を想定した記事となります。

# 利用するデータセット

今回はkaggleの[Superheroes NLP Dataset](https://www.kaggle.com/jonathanbesomi/superheroes-nlp-dataset?select=superheroes_nlp_dataset.csv)(ライセンス：CC0: Public Domai)（**英語文書データ**）を利用して前処理を行う例を主に紹介していきます。なお今回は上記のデータセット全てではなく先頭10件のデータを用います(上記のデータセットの文書のは一部NaNが存在するため)。

# データセットの読み込み

まず、最初にhuggingfaceのライブラリを用いたデータセットに読み込みについて説明します。

データセットの読み込みには**load_dataset**メソッドを利用することで実現できます。

load_datasetでは

* huggingfaceが用意している135種類のnlpタスクのためのデータセットを [HuggingFace Hub](https://huggingface.co/datasets)からダウンロードしてくる方法。

* ローカルのデータセットを読み込む方法。

  [対応しているファイル形式]

  * CSV 
  * JSON 
  * text 
  * pandas pickled dataframe 

のいずれかの方法でデータセットを準備することができます。

## 利用例

**インポート文**

```python
from datasets import load_dataset
```

### HuggingFace Hubのデータセットを使う場合の例

```python
dataset = load_dataset('glue', 'sst2') # 'sst2'の設定のデータセット'glue'を利用する例。第二引数を設定しないとエラーになる。
print(dataset)
"""
---出力--------------------------------------------------------------------------------
{'train': Dataset(schema: {'sentence': 'string', 'label': 'int64', 'idx': 'int32'}, num_rows: 67349),
 'validation': Dataset(schema: {'sentence': 'string', 'label': 'int64', 'idx': 'int32'}, num_rows: 872),
 'test': Dataset(schema: {'sentence': 'string', 'label': 'int64', 'idx': 'int32'}, num_rows: 1821)
}
---------------------------------------------------------------------------------------
"""
```

### ローカルのデータセットを使う場合の例

```python
dataset = load_dataset('csv', data_files='tarin.csv')#ローカルのcsvを読み込む場合
print(dataset)
"""
---出力--------------------------------------------------------------------------------
DatasetDict({
    train: Dataset({
        features: ['Unnamed: 0', 'name', 'real_name', 'full_name', 'overall_score', 'history_text',・・・省略・・・],
        num_rows: 10
    })
})
---------------------------------------------------------------------------------------
"""
```

それぞれの場合を比較するとHuggingFace Hubのデータセットを利用する場合、最初から`train`、`validation`、`test`の用途に分かれたデータセットが取得されます。一方、ローカルのデータセットをシンプルに読み込む方法では読みこんだcsvは`tarin`データとして読み込まれます。もし、ローカルのデータセットから`validation`、`test`データセットを読み込む場合は`data_files`の指定の方法を変更する必要があります。（詳しくは公式ドキュメントを参考）

[公式ドキュメント]
 https://huggingface.co/docs/datasets/package_reference/loading_methods.html#datasets.load_dataset

# tokenizerの適用

次に、データセット内の文章中の単語をトークン化するtokenizerをデータセットに適用する方法を見ていきます。

ここでは、tokenizerとして**AutoTokenizer**を利用する方法を説明します。

**AutoTokenizerとは**

tokenizerは各モデル毎に存在します。そして、AutoTokenizerはそれらのtokenizerのラッパーに該当するものになります。後述するように引数に利用したいモデルを指定することでそのモデルのtokenizerのインスタンスが取得できます。利用可能なモデルの種類に関しては公式ドキュメントを参照ください。ここでは、`BERT`、`ROBERTa`、`xlnet`、`T5`に関して見ていきます。

**インポート文**

```python
from transformers import AutoTokenizer
```
**[公式ドキュメント]**
https://huggingface.co/transformers/model_doc/auto.html


## tokenizerの適用方法

ここでは、ローカルのデータセットを読み込んだデータに関してtokenizerを適用していく例を見ていきます。

ここでは、tokenizerによる以下の前処理を行う例を見ていきます。

* 単語のトークン化
* 文章のPadding。（padding長は引数'max_length'で指定したサイズ）
* 文章の切り捨て（truncation）

また、以降の例の紹介でもローカルのデータセットを読み込む場合を想定するものとします。

ここでは、map関数を用いてデータセットにtokenizerを適用していく例を見ていきます。

[参考]

https://huggingface.co/docs/datasets/torch_tensorflow.html

(注)

tokenizerを適用するデータにNaNがある場合

```
TypeError: TextEncodeInput must be Union[TextInputSequence, Tuple[InputSequence, InputSequence]]
```

とエラーになってしまいます。

### BERTの場合

```python
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
print(tokenizer)
"""
---出力--------------------------------------------------------------------------------
PreTrainedTokenizerFast(・・・省略・・・
---------------------------------------------------------------------------------------
"""
def tokenize_function(examples):
    return tokenizer(examples['history_text'],padding='max_length',max_length = 500, truncation=True,return_special_tokens_mask=True)
tokenized_datasets = dataset.map(
        tokenize_function,
        batched=True,
        remove_columns=df.columns.to_list()#今回はCSVの全てのカラムを消す
)
print(tokenized_datasets)
"""
---出力--------------------------------------------------------------------------------
DatasetDict({
    train: Dataset({
        features: ['attention_mask', 'input_ids', 'special_tokens_mask', 'token_type_ids'],
        num_rows: 10
    })
})
---------------------------------------------------------------------------------------
"""
# 1つ目のデータのinput_idsを単語に復元
tokenizer.decode(tokenized_datasets["train"][0]['input_ids'])
"""
---出力--------------------------------------------------------------------------------
[CLS] delroy garrett, jr ・・・省略・・・ [PAD]
---------------------------------------------------------------------------------------
```



### xlnetの場合

```python
tokenizer = AutoTokenizer.from_pretrained('xlnet-base-cased')
print(tokenizer)
"""
---出力--------------------------------------------------------------------------------
PreTrainedTokenizerFast(・・・省略・・・
---------------------------------------------------------------------------------------
"""
def tokenize_function(examples):
    return tokenizer(examples['history_text'],padding='max_length',add_special_tokens = True,max_length = 500, truncation=True, return_special_tokens_mask=True)
tokenized_datasets = dataset.map(
        tokenize_function,
        batched=True,
        remove_columns=df.columns.to_list()#今回はCSVの全てのカラムを消す
print(tokenized_datasets)
"""
---出力--------------------------------------------------------------------------------
DatasetDict({
    train: Dataset({
        features: ['attention_mask', 'input_ids', 'special_tokens_mask', 'token_type_ids'],
        num_rows: 10
    })
})
---------------------------------------------------------------------------------------
"""
# 1つ目のデータのinput_idsを単語に復元
tokenizer.decode(tokenized_datasets["train"][0]['input_ids'])
"""
---出力--------------------------------------------------------------------------------
<pad>・・・省略・・・ Delroy Garrett, Jr. ・・・省略・・・<sep><cls>
---------------------------------------------------------------------------------------
```

### ROBERTaの場合

```python
tokenizer = AutoTokenizer.from_pretrained('roberta-base')
print(tokenizer)
"""
---出力--------------------------------------------------------------------------------
PreTrainedTokenizerFast(・・・省略・・・
---------------------------------------------------------------------------------------
"""
def tokenize_function(examples):
    return tokenizer(examples['history_text'],padding='max_length',add_special_tokens = True,max_length = 500, truncation=True, return_special_tokens_mask=True)
tokenized_datasets = dataset.map(
        tokenize_function,
        batched=True,
        remove_columns=df.columns.to_list()#今回はCSVの全てのカラムを消す
print(tokenized_datasets)
"""
---出力--------------------------------------------------------------------------------
DatasetDict({
    train: Dataset({
        features: ['attention_mask', 'input_ids', 'special_tokens_mask', 'token_type_ids'],
        num_rows: 10
    })
})
---------------------------------------------------------------------------------------
"""
# 1つ目のデータのinput_idsを単語に復元
tokenizer.decode(tokenized_datasets["train"][0]['input_ids'])
"""
---出力--------------------------------------------------------------------------------

Delroy Garrett, Jr. grew up ・・・省略・・・<pad><pad>
---------------------------------------------------------------------------------------
```

### 結果の比較

基本的に`AutoTokenizer.from_pretrained`の引数を使用したいモデルにするだけで後はすべて同じ記述でtokenizerを適用できます。また、tokenizerを適用する際不要な列のデータはremove_columnsで指定して消しています。BERT系のモデルでは、inputの系列長が揃っていないとエラーになるため、特に利用するモデル/タスクが必要としないデータはここで削除するとよいでしょう。

また、トークンの単語への復元結果`tokenizer.decode～の部分`を比較するとtokenizer(モデル)によって挿入される特殊トークン([cls]など)が異なることがわかります。

# Dataloaderの作成

最後に先のdatasetからdataloaderを作成する手順について見ていきます。

## Data Collatorの作成

ここまでの前処理で、文章中の単語のトークン化などを行いましたがまだ以下のような前処理が不足していることがわかります。

* 単語のマスク化(マスク予測タスクの場合)

このような更なる前処理(に加えてpaddingやtruncation等も)とdatasetetからミニバッチを作成するためのオブジェクトとして**Data Collator**と呼ばれるものがhuggingfaceには備わっているのでその使い方をみていきます。ここでは２つのData Collatorについて紹介します。
**[公式ドキュメント]**


https://huggingface.co/transformers/main_classes/data_collator.html#datacollatorforlanguagemodeling
## DataCollatorForLanguageModeling

言語モデリングに使用されるDataCollatorです。入力がすべて同じ長さでない場合は、バッチの最大長までダイナミックにパディングされます。

主に大体のモデルを使った文章のクラス分類等の大半のタスクやマスク予測タスク等に利用できると思われます。

[主なパラメーター]

* **tokenizer**:エンコードに使用したtokenizer。
* **mlm**:マスク予測タスクをするかどうかのフラグで、Trueの場合は文書中の単語を一定割合で[MASK]に置き換える処理が実行されます**デフォルト ：True**。マスク予測タスク以外で利用する場合はFalseにします。

* **mlm_probability**：マスク予測タスクを実行する場合(mlmがTrueの場合)の文書中の単語を[MASK]で置き換える割合を指定します（defaults ：0.15）。

**インポート文**

```python
from transformers import DataCollatorForLanguageModeling
```

**宣言の例（マスク予測タスクの場合）**

```python
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer,mlm=True,mlm_probability=0.15)
```

## DataCollatorForPermutationLanguageModeling

XLNetのマスク予測タスクに利用するDataCollatorです。XLNetはマスク予測タスクのやり方がBERTなどとは異なるためマスク予測タスクを実施する場合専用のDataCollatorを使用します。その他のタスクを実施する場合前述のDataCollatorForLanguageModelingを利用すればよいと思われます。

[主なパラメーター]

* **plm_probability(defaults ：0.1666666)** ：文書中における予測対象とする単語の割合（Permutation Language ModelingにおけるPartial Predictionにて指定するハイパーパラメータのことと思われます）。

**インポート文**

```python
from transformers import DataCollatorForPermutationLanguageModeling
```

**宣言の例（マスク予測タスクの場合）**

```python
data_collator = DataCollatorForPermutationLanguageModeling(tokenizer=tokenizer,plm_probability=0.16666)
```

## dataloaderの作成とバッチの中身の確認

最後にここまでで作成したdatasetとData Collatorからpytorchのdataloaderを作成することで前処理が完了します。

## BERTの場合

```python
from torch.utils.data import DataLoader
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer,mlm=True,mlm_probability=0.15)

train_dataset = tokenized_datasets["train"]
# データローダーの作成
dataloader = DataLoader(
            train_dataset,  
            shuffle=True, # ランダムにデータを取得してバッチ化
            batch_size = 2,
            collate_fn=data_collator
        )
# 以下dataloaderのバッチごとの中身の確認
for i, batch in enumerate(dataloader):
  if i == 0:
    # batchの中身の確認
    print(batch)
    # print(tokenizer.decode(batch['input_ids'][1]))の中身
    print(tokenizer.decode(batch['input_ids'][1]))
  pass
"""
 -------------print(batch)の中身-------------

{
'attention_mask':・・・省略・・・
'input_ids':・・・省略・・・
'labels':・・・省略・・・
}
--------------------------
"""

"""
-------------print(tokenizer.decode(batch['input_ids'][1]))の中身-------------
[CLS] originally a history professor on the planet ungara, abin sur was soon appointed green lantern [MASK] space ・・・省略・・・
--------------------------
```

"""



## XLNETの例

```python
from torch.utils.data import DataLoader
data_collator = DataCollatorForPermutationLanguageModeling(tokenizer=tokenizer,plm_probability=0.16666)

train_dataset = tokenized_datasets["train"]
# データローダーの作成
batch_size = 2

# 訓練データローダー
dataloader = DataLoader(
            train_dataset,  
            shuffle=True, # ランダムにデータを取得してバッチ化
            batch_size = batch_size,
            collate_fn=data_collator
        )
# 以下dataloaderのバッチごとの中身の確認
for i, batch in enumerate(dataloader):
  if i == 0:
    # batchの中身の確認
    print(batch)
    # input_idsのトークンを単語に復元
    print(tokenizer.decode(batch['input_ids'][1]))
  pass

"""
-------------print(batch)の出力-------------
{
'perm_mask':・・・省略・・・
'target_mapping'・・・省略・・・
'input_ids':・・・省略・・・
'labels':・・・省略・・・
}
--------------------------
"""

"""
-------------print(tokenizer.decode(batch['input_ids'][1]))の出力-------------
Sapien began life<mask><mask><mask><mask>ul, a Victorian scientist・・・省略・・・
--------------------------
"""
```

## ROBERTaの場合

```python
from torch.utils.data import DataLoader
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer,mlm=True,mlm_probability=0.15)

train_dataset = tokenized_datasets["train"]
# データローダーの作成
dataloader = DataLoader(
            train_dataset,  
            shuffle=True, # ランダムにデータを取得してバッチ化
            batch_size = 2,
            collate_fn=data_collator
        )
# 以下dataloaderのバッチごとの中身の確認
for i, batch in enumerate(dataloader):
  if i == 0:
    # batchの中身の確認
    print(batch)
    # input_idsのトークンを単語に復元
    print(tokenizer.decode(batch['input_ids'][1]))
  pass

"""
-------------print(batch)の出力-------------
{
'attention_mask':・・・省略・・・
'input_ids':・・・省略・・・
'labels':・・・省略・・・
}
--------------------------
"""

"""
-------------print(tokenizer.decode(batch['input_ids'][1]))の出力-------------

ayla Secura was a Rutian Twi'lek Jedi Knight (and onetime apprentice to Quinlan V<mask>)<mask> served・・・省略・・・
--------------------------
"""
```

## 結果の比較

今回はDataCollatorの設定としてマスク予測タスクの設定で前処理を行いました。その結果、ここまでの前処理を実施することで各々のモデルが必要とするマスク予測タスクのための前処理が適切に実施されたことがわかります。特に、BERTとROBERTaのように前処理のコードを利用するモデルの宣言の部分以外を全く同じ方法で書くことができる点はとても使い勝手がいい印象を受けました。また、XLNETなどのモデル特有の前処理が必要となるモデルに関しても利用するオブジェクトを切り替えるだけで同じ流れで前処理が実現できます。

総じて、**1度前処理の流れを作ってしまえばモデルを切り替える際かなりの部分が流用できる**ため簡単に新しいモデルを試すことができる基盤としてhuggingfaceはとても有用であると感じました。