torchtextの仕様変更対応 (1) Field

PyTorch 1.11(torchtext 0.12)より自然言語処理で活用していたtorchtextのFieldやTabularDatasetなど便利な機能がなくなりました。PyTorch 1.10(torchtext 0.11)まではlegacyに移動されいましたが利用することは可能でした。しかし、PyTorch 1.11(torchtext 0.12)で完全に削除されてしまいました。

詳細については、以下に記載されています。
https://github.com/pytorch/text/releases/tag/v0.9.0-rc5
また、移行方法については以下に説明があります。
https://github.com/pytorch/text/blob/master/examples/legacy_tutorial/migration_tutorial.ipynb

PyTorchで自然言語処理を行うため非常に便利な機能だったため使われている方も多いのではないでしょうか？
ただ提示されている移行方法だけではわかりにくい部分もあるので実際に移行してみました。

まずは、Fieldについて見ていきます。

# Field
Fieldは、自然言語処理の前処理として形態素解析、単語辞書作成、単語の数値化まで一貫してサポートしていました。どうも、この一貫性が自由度を奪うため問題となったようです。

## 旧実装
Fieldを利用すれば以下のように実装できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212365/6f2d9d3f-8d18-e152-128e-a62855e1fe60.png)

以下の文章を例に実装の説明をします。

```python
# 文章例
text = "これはペンです。あれもペンです。"
```

### 形態素解析
形態素解析を行うための関数を用意します。ここでは、MeCabを利用しています。

```python
import MeCab

# 形態素解析
t_mecab = MeCab.Tagger('-Owakati')
# 形態素解析関数
def tokenizer(text):
    return t_mecab.parse(text).split()
```

### Field定義
Fieldを定義します。

```python
from torchtext.legacy.data import Field

# Firld定義(形態素解析関数設定)
TEXT  = Field(tokenize=tokenizer)
```

### 形態素解析
preprocessを利用し、形態素解析などの前処理を行います。

```python
# 形態素解析
text_w = TEXT.preprocess(text)
print(text_w)
```

```
['これ', 'は', 'ペン', 'です', '。', 'あれ', 'も', 'ペン', 'です', '。']
```

### 単語辞書作成
build_vocabを利用し、単語辞書を作成します。

```python
# 単語辞書作成
TEXT.build_vocab([text_w])
```

単語辞書を確認してみます。まずは、単語の出現回数です。

```python
# 単語の出現回数
TEXT.vocab.freqs
```

```
Counter({'これ': 1, 'は': 1, 'ペン': 2, 'です': 2, '。': 2, 'あれ': 1, 'も': 1})
```


```python
# 単語→数値変換用辞書
TEXT.vocab.stoi
```

```
defaultdict(<bound method Vocab._default_unk_index of <torchtext.legacy.vocab.Vocab object at 0x000000000000>>,
            {'<unk>': 0,
             '<pad>': 1,
             '。': 2,
             'です': 3,
             'ペン': 4,
             'あれ': 5,
             'これ': 6,
             'は': 7,
             'も': 8})
```

```python
# 数値→単語変換用リスト
TEXT.vocab.itos
```

```
['<unk>', '<pad>', '。', 'です', 'ペン', 'あれ', 'これ', 'は', 'も']
```

### 数値化
単語を数値化します。結果は、Tensor型になります。

```python
# 単語数値化
text_id = TEXT.process([text_w])
print(text_id)
```

```
tensor([[6],
        [7],
        [4],
        [3],
        [2],
        [5],
        [8],
        [4],
        [3],
        [2]])
```

### 別文章
別の文章で試してみます。

```python
text = "それがペンです。"
```

```python
text_w = TEXT.preprocess(text)
text_id = TEXT.process([text_w])
# 単語、単語ID対応表示
for i in range(len(text_w)):
    print(text_w[i], '\t', text_id[i].item(), '\t', TEXT.vocab.itos[text_id[i]])
```

```
それ 	 0 	 <unk>
が 	 0 	 <unk>
ペン 	 4 	 ペン
です 	 3 	 です
。 	 2 	 。
```

辞書に含まれない単語は、\<unk\>になりました。

## 新実装
PyTorch 1.11(torchtext 0.12)を利用した実装です。PyTorch 1.11(torchtext 0.12)から一部仕様が変更されているため、PyTorch 1.10(torchtext 0.11)以前では動作しません。

同じように以下の文章を例に説明します。

```python
# 文章例
text = "これはペンです。あれもペンです。"
```

### 形態素解析
形態素解析を行うための関数を用意します。この部分は、変更ありません。

```python
import MeCab

# 形態素解析
t_mecab = MeCab.Tagger('-Owakati')
# 形態素解析関数
def tokenizer(text):
    return t_mecab.parse(text).split()
```

形態素解析を行います。

```python
# 形態素解析
text_w = tokenizer(text)
```

### 単語のカウント
Counterを用いて単語のカウントを行います。

```python
from collections import Counter

# 出現単語のカウント
counter = Counter()
counter.update(text_w)
```

単語の出現回数を確認します。

```python
# 出現回数
print(counter)
```

```
Counter({'ペン': 2, 'です': 2, '。': 2, 'これ': 1, 'は': 1, 'あれ': 1, 'も': 1})
```

### 単語辞書作成
counterから単語辞書を作成します。その際に、未知単語用に\<unk\>を追加しています。今までは自動で追加されていましたが明示的に指定する必要があります。辞書に含まれない単語用にdefault_indexとして\<unk\>を設定しておきます。

```python
from torchtext.vocab import vocab

# 単語辞書作成
v = vocab(counter, specials=(['<unk>']))
# デフォルトインデックスを<unk>に設定
v.set_default_index(v['<unk>'])
```

単語辞書を確認してみます。

```python
# 単語→数値変換用辞書
v.get_stoi()
```

```
{'も': 7, 'あれ': 6, '。': 5, 'です': 4, '<unk>': 0, 'ペン': 3, 'これ': 1, 'は': 2}
```

```python
# 数値→単語変換用リスト
v.get_itos()
```

```
['<unk>', 'これ', 'は', 'ペン', 'です', '。', 'あれ', 'も']
```

### 数値化
単語を数値化します。結果は、Fieldと異なり自動的にはTensor型になりません。

```python
# 単語数値化
text_id = v.lookup_indices(text_w)
print(text_id)
```

```
[1, 2, 3, 4, 5, 6, 7, 3, 4, 5]
```

### 別文章
別の文章で試してみます。

```python
text = "それがペンです。"
```

```python
text_w = tokenizer(text)
text_id = v.lookup_indices(text_w)
# 単語、単語ID対応表示
for i in range(len(text_w)):
    print(text_w[i], '\t', text_id[i], '\t', v.lookup_token(text_id[i]))
```

```
それ 	 0 	 <unk>
が 	 0 	 <unk>
ペン 	 3 	 ペン
です 	 4 	 です
。 	 5 	 。
```

辞書に含まれない単語は、\<unk\>になりました。ただし、set_default_indexを行っていないと未知の単語が出現した場合エラーとなります。

## その他の機能
基本的には、上記の方法でFieldを移行できますが、Fieldには他にもいろいろな機能が含まれておりました。
そのうちのいくつかの機能は、torchtext.transformsを利用し代替できます。

| Fieldのパラメータ | transforms | 説明 |
|:-----------|:------------|:-------------|
| fix_length | Truncate    | 指定した長さ以降は切り捨てします。ただし、Truncateでは自動でパディングは行ってくれません。truncate_firstに対応するパラメータは存在しないようです。 |
| init_token | AddToken    | AddTokenのbeginをtrueとし開始のトークンを設定します。 |
| eos_token  | AddToken    | AddTokenのbeginをfalseとし終了のトークンを設定します。 |
| pad_token  | ToTensor    | ToTensorのpadding_valueにパディングのトークンを設定します。pad_firstに対応するパラメータは存在しないようです。 |

### 旧実装
今までの実装から見ていきます。

Fieldを定義し辞書を作成するまでです。Fieldには、fix_lengthに10を設定し10単語までとしています。また、init_token、eos_tokenで文章の開始、終了として、それぞれ\<bos\>、\<eos\>を設定しています。

```python
from torchtext.legacy.data import Field

# Firld定義(形態素解析関数設定)
TEXT  = Field(tokenize=tokenizer, fix_length=10, init_token="<bos>", eos_token="<eos>", batch_first=True)
# 文章例
text = "これはペンです。あれもペンです。"
# 形態素解析
text_w = TEXT.preprocess(text)
# 単語辞書作成
TEXT.build_vocab([text_w])
```

新しい文章を加えてどのように単語が数値化されたか見てみましょう。

```python
# 文章例2
text2 = "それがペンです。"
# 形態素解析
text_w2 = TEXT.preprocess(text2)
# 単語数値化
text_ids = TEXT.process([text_w, text_w2])
```

辞書作成に利用した１つ目の文章です。

```python
for i in range(10):
    print(text_ids[0, i].item(), '\t', TEXT.vocab.itos[text_ids[0, i]])
```

```
2 	 <bos>
8 	 これ
9 	 は
6 	 ペン
5 	 です
4 	 。
7 	 あれ
10 	 も
6 	 ペン
3 	 <eos>
```

文章の最初と最後に\<bos\>、\<eos\>が付与されています。また、10単語目までで切られています。
辞書作成に利用していない２つ目の文章です。

```python
for i in range(10):
    print(text_ids[1, i].item(), '\t', TEXT.vocab.itos[text_ids[1, i]])
```

```
2 	 <bos>
0 	 <unk>
0 	 <unk>
6 	 ペン
5 	 です
4 	 。
3 	 <eos>
1 	 <pad>
1 	 <pad>
1 	 <pad>
```

単語辞書にない単語は、\<unk\>になっています。また、不足分は、\<pad\>で埋められています。

### 新実装
新しい実装です。

辞書を作成するまでです。vocabで特殊文字として、\<unk\>の他に、\<pad\>、\<bos\>、\<eos\>も設定します。

```python
from collections import Counter
from torchtext.vocab import vocab

# 文章例
text = "これはペンです。あれもペンです。"

# 形態素解析
text_w = tokenizer(text)
# 出現単語のカウント
counter = Counter()
counter.update(text_w)
# 単語辞書作成
v = vocab(counter, specials=(['<unk>', '<pad>', '<bos>', '<eos>']))
# デフォルトインデックスを<unk>に設定
v.set_default_index(v['<unk>'])
```

transformを設定します。
VocabTransformで辞書を設定します。Truncateで長い文章を切る捨てるように設定します。\<bos\>、\<eos\>が付加されることを考慮し2を引いています。AddTokenで文章の開始、終了を表す\<bos\>、\<eos\>を設定します。ToTensorでパディングとテンソル型への変換を設定します。Sequentialを用いることで、複数のtransformを順番に実行することができます。

```python
import torchtext.transforms as T

text_transform = T.Sequential(
    T.VocabTransform(v),
    T.Truncate(10-2),
    T.AddToken(token=v['<bos>'], begin=True),
    T.AddToken(token=v['<eos>'], begin=False),
    T.ToTensor(padding_value=v['<pad>'])
)
```

新しい文章を加えてどのように単語が数値化されたか見てみましょう。
transformで単語を数値化まで行います。

```python
# 文章例2
text2 = "それがペンです。"
# 形態素解析
text_w2 = tokenizer(text2)
# 単語数値化
text_ids = text_transform([text_w, text_w2])
```

辞書作成に利用した１つ目の文章です。

```python
for i in range(10):
    print(text_ids[0, i].item(), '\t', v.lookup_token(text_ids[0, i]))
```

```
2 	 <bos>
4 	 これ
5 	 は
6 	 ペン
7 	 です
8 	 。
9 	 あれ
10 	 も
6 	 ペン
3 	 <eos>
```

文章の最初と最後に\<bos\>、\<eos\>が付与されています。また、10単語目までで切られています。
辞書作成に利用していない２つ目の文章です。

```python
for i in range(10):
    print(text_ids[1, i].item(), '\t', v.lookup_token(text_ids[1, i]))
```

```
2 	 <bos>
0 	 <unk>
0 	 <unk>
6 	 ペン
7 	 です
8 	 。
3 	 <eos>
1 	 <pad>
1 	 <pad>
1 	 <pad>
```

単語辞書にない単語は、\<unk\>になっています。また、不足分は、\<pad\>で埋められています。単語番号は、実装の影響で旧実装と異なっておりますが同じ結果になりました。

次回は、TabularDatasetについて考えていきたいと思います。