torchtext凍結対応

以前、torchtextの仕様変更への対応方法を書きました。
2024年7月にリリースされたPyTorch 2.4からtorchtextのサポートがなくなりました。torchtextを利用するには、PyTorchを2.3以前にする必要があります。
PyTorch 2.4以降で利用する場合には、別の方法に移行する必要があります。ここでは、Hugging Faceの[Datasets](https://huggingface.co/docs/datasets/index)、[Tokenizers](https://huggingface.co/docs/tokenizers/index)を利用した移行方法を検討します。

# データ
前回と同様、以下のデータを使って試します。
textとlabelの間はタブです。

```test.txt
text	label
あ	1
あ い	2
あ い う	3
あ い え	3
あ い う え お	5
あ い う え お か	6
あ い う え お き	6
あ い う え お か き く	8
あ い う え お か き く け	9
```

# 旧実装
test.txtからデータを読み込み、ミニバッチ学習を行うまでの実装です。
参考のため旧実装です。詳細は、前回を記事[「torchtextの仕様変更対応 (3) TorchData利用」](https://qiita.com/Nezura/items/268826b61f46f67705e4)を参照してください。

- torchtext仕様変更前
TabularDatasetを利用すれば非常に簡単に実装できていました。

```python
from torchtext.legacy.data import Field, LabelField, TabularDataset, BucketIterator

# Firld定義
TEXT = Field(batch_first=True)
LABEL = LabelField()
# データ読み込み
ds = TabularDataset(path="test.txt", format='tsv', fields=[('text', TEXT), ('label', LABEL)], skip_header=True)
# 単語辞書作成
TEXT.build_vocab(ds)
LABEL.build_vocab(ds)
# イテレータ生成
biter = BucketIterator(dataset=ds, shuffle=True, batch_size=3)
```

- torchtext仕様変更後
データ読み込みには、torchdataを利用しましたが、実装は複雑になりました。

```python
import torchdata.datapipes as dp
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
import torchtext.transforms as T
from torch.utils.data import DataLoader
from torchtext.data.functional import to_map_style_dataset

# tokenizer設定
tokenizer = get_tokenizer(tokenizer=None)
# データ読み込み
datapipe = dp.iter.FileOpener(['test.txt'], mode='rt').parse_csv(delimiter='\t', skip_lines=1).map(lambda text: tokenizer(text), input_col=0)
# 単語辞書作成
datapipe_text = datapipe.map(fn=lambda x: x[0])
text_vocab = build_vocab_from_iterator(datapipe_text, specials=('<unk>', '<pad>'))
text_vocab.set_default_index(text_vocab['<unk>'])
# ラベル辞書
datapipe_label = datapipe.map(fn=lambda x: x[1])
label_vocab = build_vocab_from_iterator(datapipe_label)
# transform生成
text_transform = T.Sequential(
    T.VocabTransform(text_vocab),
    T.ToTensor(padding_value=text_vocab['<pad>'])
)
label_transform = T.Sequential(
    T.VocabTransform(label_vocab),
    T.ToTensor()
)
# ミニバッチ時のデータ変換関数
def collate_batch(batch):
    texts = text_transform([text for (text, label) in batch])
    labels = label_transform([label for (text, label) in batch])
    return texts, labels
# mapに変換
ds = to_map_style_dataset(datapipe)
# DataLoader設定
data_loader = DataLoader(ds, batch_size=3, shuffle=True, collate_fn=collate_batch)
```

# 新実装
[Datasets](https://huggingface.co/docs/datasets/index)を利用しデータを読み込みます。[Tokenizers](https://huggingface.co/docs/tokenizers/index)を利用し単語辞書を作成しエンコードを行います。

## データ読み込み
Datasetのfrom_csvを利用しファイルを読み込みます。タブ区切りのためdelimiterに'\t'を指定します。Datasetには、CSV以外にもさまざまな形式に対応していますので確認してみてください。

```python
from datasets import Dataset

# CSV(タブ区切り)から文章読み込み
dataset = Dataset.from_csv('test.txt', delimiter='\t')
```
データセットを確認します。
```python
# データセット確認
dataset
```

```
Dataset({
    features: ['text', 'label'],
    num_rows: 9
})
```
'text'、'label'の2つの項目およびデータが9件であることが分かります。
データを参照します。

```python
# データ確認
for i in range(len(dataset)):
    print(dataset[i])
```

```
{'text': 'あ', 'label': 1}
{'text': 'あ い', 'label': 2}
{'text': 'あ い う', 'label': 3}
{'text': 'あ い え', 'label': 3}
{'text': 'あ い う え お', 'label': 5}
{'text': 'あ い う え お か', 'label': 6}
{'text': 'あ い う え お き', 'label': 6}
{'text': 'あ い う え お か き く', 'label': 8}
{'text': 'あ い う え お か き く け', 'label': 9}
```
'text'にtext列の文字列、'label'にlabel列の値が入っていることが分かります。

labelが数値の場合、クラス番号に変更できなかったため、labelを文字型に変換します。labelが文字列またはクラス番号になっている場合は以下の処理は不要です。

```python
from datasets import Value

# ラベルを文字型に変換
dataset = dataset.cast_column('label', Value('string'))
```

labelをクラス番号に変換します。labelがクラス番号になっている場合は以下の処理は不要です。

```python
# クラス番号
class_label = list(sorted(set(dataset['label'])))
class_label
```

```
['1', '2', '3', '5', '6', '8', '9']
```

ClassLabelを利用すればクラス番号に変換できます。labelがクラス番号になっている場合は以下の処理は不要です。

```python
from datasets import ClassLabel

# ラベルをクラス番号に変換
dataset = dataset.cast_column('label', ClassLabel(names=class_label))
```

クラス番号変換後のデータを確認します。もともと数値のため分かりにくいかもしれませんが、文字数からクラス番号に変更されています。

```python
# データ確認
for i in range(len(dataset)):
    print(dataset[i])
```

```
{'text': 'あ', 'label': 0}
{'text': 'あ い', 'label': 1}
{'text': 'あ い う', 'label': 2}
{'text': 'あ い え', 'label': 2}
{'text': 'あ い う え お', 'label': 3}
{'text': 'あ い う え お か', 'label': 4}
{'text': 'あ い う え お き', 'label': 4}
{'text': 'あ い う え お か き く', 'label': 5}
{'text': 'あ い う え お か き く け', 'label': 6}
```

Datasetには、データセット利用のための様々な機能をサポートしています。確認してみてください。

## 単語分割
単語分割を行うためTokenizerを利用します。Hugging Faceのトークナイザーは、単語単位だけではなく、BPE (Byte-Pair Encoding) やWordPieceなどのアルゴリズムを用いる手法をサポートしています。
今回は単語をトークンとするWordLevelを用います。

WordLevelのトークナイザーを作成します。未知語としてunk_tokenに'\<unk\>'を設定します。
```python
from tokenizers import Tokenizer, models, pre_tokenizers, trainers

# トークナイザーの作成
tokenizer = Tokenizer(models.WordLevel(unk_token='<unk>'))
```

今回は、すでに単語はスペースで区切られているためスペース単位で単語を区切るWhitespaceをpre_tokenizerとして設定しました。
日本語文章を用いる場合は、MeCabなどを用いて形態素解析を行うようにカスタマイズすることができます。
```python
# スペース区切り
tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()
```

## 単語辞書作成
単語辞書を作成するためのトレーナーを設定します。単語単位のためWordLevelTrainerを利用します。特殊文字として、未知語用の'\<unk\>'、パディング用の'\<pad\>'を設定します。
```python
# トレーナーの設定
trainer = trainers.WordLevelTrainer(special_tokens=['<unk>', '<pad>'])
```

データセットの文章を利用し単語辞書を作成します。train_from_iteratorを利用すればリストに格納された文章に含まれる単語から辞書を作成します。
ここでは指定していませんが、min_frequencyを指定して辞書に含める単語の最低限の出現回数を指定することができます。
```python
# 辞書作成
tokenizer.train_from_iterator(dataset['text'], trainer)
```

辞書の確認を行います。

```python
# 単語数
tokenizer.get_vocab_size()
```

```
11
```

```python
# 単語
tokenizer.get_vocab()
```

```
{'け': 10,
 'え': 5,
 'き': 8,
 '<unk>': 0,
 'い': 3,
 'か': 7,
 'く': 9,
 'あ': 2,
 'お': 6,
 '<pad>': 1,
 'う': 4}
```

データに含まれる単語辞書が作成されていることがわかります。

## トークナイズ
単語辞書が作成できたので、この辞書を利用し文章を単語IDに変化します。encodeを利用します。

```python
en = tokenizer.encode('あ い う え お')
en
```

```
Encoding(num_tokens=5, attributes=[ids, type_ids, tokens, offsets, attention_mask, special_tokens_mask, overflowing])
```

idsに単語ID、tokensに単語列が格納されます。

```python
en.ids, en.tokens
```

```
([2, 3, 4, 5, 6], ['あ', 'い', 'う', 'え', 'お'])
```

単語辞書に従って変換されていることが分かります。
ミニバッチ学習を考慮し複数の文章を変換することを考えます。系列長をそろえるためミニバッチで最長の長さに合わせてパディングを行います。パディングのID、文字を設定します。

```python
# パディングID、文字を設定
tokenizer.enable_padding(pad_id=tokenizer.token_to_id('<pad>'), pad_token='<pad>')
```

複数文章の変換には、encode_batchを利用します。

```python
ens = tokenizer.encode_batch(['あ い う', 'あ い う え お ん'])
ens
```

```
[Encoding(num_tokens=6, attributes=[ids, type_ids, tokens, offsets, attention_mask, special_tokens_mask, overflowing]),
 Encoding(num_tokens=6, attributes=[ids, type_ids, tokens, offsets, attention_mask, special_tokens_mask, overflowing])]
```

単語ID、単語列を確認します。

```python
for en in ens:
    print(en.ids, en.tokens)
```

```
[2, 3, 4, 1, 1, 1] ['あ', 'い', 'う', '<pad>', '<pad>', '<pad>']
[2, 3, 4, 5, 6, 0] ['あ', 'い', 'う', 'え', 'お', '<unk>']
```

長さは最長の単語数に合わせて6となり、不足分は'\<pad\>'でパディングされていることがわかります。また、単語辞書に登録されていない'ん'は、未知語の'\<unk\>'に変換されます。

## ミニバッチ学習
ミニバッチ学習用にデータ変換関数を定義します。batchからtextとlabelを取り出します。textは、encode_batchを通して単語IDに変換します。labelは、クラス番号に変換済のためそのままです。

```python
# ミニバッチ時のデータ変換関数
def collate_batch(batch):
    texts = [data['text'] for data in batch]
    labels = [data['label'] for data in batch]
    tokens = [enc.ids for enc in tokenizer.encode_batch(texts)]
    return tokens, labels
```

DataLoaderに設定します。
データセットとして、datasetをそのまま指定できます。ミニバッチごとにデータ変換を行うcollate_batch関数をcollate_fnに設定します。

```python
from torch.utils.data import DataLoader

# DataLoader設定
data_loader = DataLoader(dataset, batch_size=3, shuffle=True, collate_fn=collate_batch)
```

ミニバッチごとのデータを確認します。

```python
for i, (texts, labels) in enumerate(data_loader):
    print(i)
    for text, label in zip(texts, labels):
        print(text, class_label[label])
```

```
0
[2, 3, 4, 5, 6, 1, 1, 1] 5
[2, 3, 4, 5, 6, 7, 8, 9] 8
[2, 3, 5, 1, 1, 1, 1, 1] 3
1
[2, 3, 4, 5, 6, 7, 8, 9, 10] 9
[2, 3, 4, 5, 6, 7, 1, 1, 1] 6
[2, 3, 4, 1, 1, 1, 1, 1, 1] 3
2
[2, 3, 1, 1, 1, 1] 2
[2, 1, 1, 1, 1, 1] 1
[2, 3, 4, 5, 6, 8] 6
```

各ミニバッチごとにバッチサイズの3件ごと表示されていることがわかります。
もう２回実行した結果です。

```
0
[2, 1, 1, 1, 1, 1, 1, 1, 1] 1
[2, 3, 4, 5, 6, 7, 8, 9, 10] 9
[2, 3, 4, 5, 6, 7, 1, 1, 1] 6
1
[2, 3, 4, 5, 6, 1, 1, 1] 5
[2, 3, 4, 5, 6, 7, 8, 9] 8
[2, 3, 4, 1, 1, 1, 1, 1] 3
2
[2, 3, 4, 5, 6, 8] 6
[2, 3, 5, 1, 1, 1] 3
[2, 3, 1, 1, 1, 1] 2
```


```
0
[2, 3, 4, 1, 1, 1] 3
[2, 3, 4, 5, 6, 1] 5
[2, 3, 4, 5, 6, 8] 6
1
[2, 3, 4, 5, 6, 7, 8, 9, 10] 9
[2, 3, 4, 5, 6, 7, 1, 1, 1] 6
[2, 1, 1, 1, 1, 1, 1, 1, 1] 1
2
[2, 3, 5, 1, 1, 1, 1, 1] 3
[2, 3, 1, 1, 1, 1, 1, 1] 2
[2, 3, 4, 5, 6, 7, 8, 9] 8
```

以前と同じ結果になりました。

# まとめ
Hugging Faceの[Datasets](https://huggingface.co/docs/datasets/index)、[Tokenizers](https://huggingface.co/docs/tokenizers/index)を利用して実装しました。
以前より分かりやすくなったように思います。

すべてのプログラムです。

```python
from datasets import Dataset, Value, ClassLabel
from tokenizers import Tokenizer, models, pre_tokenizers, trainers
from torch.utils.data import DataLoader

# CSV(タブ区切り)から文章読み込み
dataset = Dataset.from_csv('test.txt', delimiter='\t')
# ラベルを文字型に変換
dataset = dataset.cast_column('label', Value('string'))
# クラス
class_label = list(sorted(set(dataset['label'])))
# ラベルをクラス番号に変換
dataset = dataset.cast_column('label', ClassLabel(names=class_label))

# トークナイザーの作成
tokenizer = Tokenizer(models.WordLevel(unk_token='<unk>'))
# スペース区切り
tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()
# トレーナーの設定
trainer = trainers.WordLevelTrainer(special_tokens=['<unk>', '<pad>'])
# 辞書作成
tokenizer.train_from_iterator(dataset['text'], trainer)
# パディングID、文字を設定
tokenizer.enable_padding(pad_id=tokenizer.token_to_id('<pad>'), pad_token='<pad>')

# ミニバッチ時のデータ変換関数
def collate_batch(batch):
    texts = [data['text'] for data in batch]
    labels = [data['label'] for data in batch]
    tokens = [enc.ids for enc in tokenizer.encode_batch(texts)]
    return tokens, labels
# DataLoader設定
data_loader = DataLoader(dataset, batch_size=3, shuffle=True, collate_fn=collate_batch)
```

最新のPyTorchを利用する際には、torchtextは利用できなくなりました。この実装が参考になればと思います。