torchtextの使い方


先日、Preferred Networks社がChainerの開発を終了し、PyTorchに移行するというニュースが発表されました。
PyTorchで自然言語処理のタスク、例えば

  • 文書分類
  • 感情分析
  • 翻訳

などをする場合、torchtextという自然言語処理用のパッケージを利用することになります。
具体的には、テキストの形態素解析や形態素毎の分散表現を利用した単語ベクトルの取得などの前処理に相当する部分をtorchtextが担います。そして、モデル作成以降のフェーズをtorchが担います。

本記事では日本語テキストを対象としたtorchtextの使い方を紹介します。
なお、この記事で紹介したプログラムの実行環境は次の通りです。
形態素解析にはMeCabを使い、MeCabはインストール済みとしています。

システム

  • macOS Catalina
  • Python 3.6.4

パッケージ

  • torch: 1.3.1
  • torchtext: 0.4.0
  • mecab-python3: 0.996.2

使い方

まず、トレーニングデータとテストデータの2つのCSVファイルを用意します。
次に、Fieldオブジェクトを作成した上でTabularDatasetオブジェクトを作成し、これらのCSVファイルを読み込みます。
そして、Vocabオブジェクトを作成します。Vocabオブジェクトには、テキストを分かち書きした結果やテキスト中に含まれる形態素の情報(単語の分散表現の情報も含む)等が含まれます。
最後に、BucketIteratorオブジェクトを作成します。バッチサイズを指定し、paddingなどの処理はこのタイミングで行われます。
torchtextが担うのはここまでで、この後はこれらのオブジェクトをtorchで作成したモデルに渡して学習させます。

CSVファイルの用意

トレーニングデータとテストデータとしてtrain.csvtest.csvの2つのファイルを用意します。1カラム目にテキスト、2カラム目にラベルが入っています。
テキストのポジネガ判定を想定しており、ポジティブなテキストには0のラベル、ネガティブなテキストには1のラベルを付与しています。

train.csv
めっちゃ楽しかった。,0
すごい面白かった。,0
とても悲しい。,1
非常に落ち込んだ。,1
test.csv
とても喜んだ。,0
めっちゃ悲しい。,1

FieldオブジェクトとTabularDatasetオブジェクトの作成

まず、テキスト用とラベル用の2つのFieldオブジェクトを作成します。
日本語テキストを対象とする場合にはテキスト用オブジェクトではtokenizeを指定する必要があり、本記事ではMeCabを利用します。
また、ラベルは数字なので、ラベル用オブジェクトではsequentialuse_vocabの引数をFalseにするのを忘れないようにしましょう。

Fieldオブジェクトを作成すれば、次にTabularDatasetオブジェクトを作成します。
先ほど作成したFieldオブジェクトを引数に指定した上で、トレーニングデータとテストデータのパスとファイル名を指定するだけです。
本記事ではCSV形式のファイルを読み込んでいますが、TSV形式やJSON形式でも構いません。

from torchtext import data
import MeCab

def tokenize(text):
    return mt.parse(text).split()

if __name__ == '__main__':
    mt = MeCab.Tagger("-Owakati")

    # Fieldオブジェクトの作成
    TEXT = data.Field(tokenize=tokenize)
    LABEL = data.Field(sequential=False, use_vocab=False)

    # CSVファイルを読み込み、TabularDatasetオブジェクトの作成
    train, test = data.TabularDataset.splits(
        path='./',
        train='train.csv',
        test='test.csv',
        format='csv',
        fields=[('text', TEXT), ('label', LABEL)])

TabularDatasetオブジェクトの中身を確認したい場合、次のように書きます。
TabularDatasetオブジェクトの実態はリスト形式のExampleオブジェクトなので、for文を使えばまとめて一気に確認することができます。

    for example in train:
        print(example.text, example.label)

上記のプログラムを実行すると、このように分かち書き済みのテキストとラベルが表示されます。

['めっちゃ', '楽しかっ', 'た', '。'] 0
['すごい', '面白かっ', 'た', '。'] 0
['とても', '悲しい', '。'] 1
['非常', 'に', '落ち込ん', 'だ', '。'] 1

Vocabオブジェクトの作成

TabularDatasetオブジェクトが作成できれば、次にVocabオブジェクトを作成します。これはテキスト用のオブジェクトだけで構いません。
分散表現のクラスを指定する必要があり、本記事ではFastTextの日本語版を利用しています。
ここで/site-packages/torchtext/vocab.pyでオリジナルクラスを作成してそのクラスを指定すれば、自分で作成した分散表現モデルを使うこともできます。

    from torchtext.vocab import FastText
    # Vocabオブジェクトの作成
    TEXT.build_vocab(train, vectors=FastText(language="ja"))

Vocabオブジェクトの中身を確認したい場合、次のように書きます。
TEXT.vocab.vectorsにはtrain.csvに出現する単語のベクトルのみが含まれます。

    print(TEXT.vocab.freqs) # 単語毎の出現回数
    print(TEXT.vocab.stoi) # 文字列からインデックス番号
    print(TEXT.vocab.itos) # インデックス番号から文字列
    print(TEXT.vocab.vectors) # 単語ベクトル
    print(TEXT.vocab.vectors.size()) # 単語ベクトルのサイズ

上記のプログラムの実行結果はこのようになります。
train.csvには<unk><pad>を含めて14種類の単語が出現するので、単語ベクトルのサイズは[14, 300]になります。300はFastTextの分散表現モデルの次元数です。

Counter({'。': 4, 'た': 2, 'めっちゃ': 1, '楽しかっ': 1, 'すごい': 1, '面白かっ': 1, 'とても': 1, '悲しい': 1, '非常': 1, 'に': 1, '落ち込ん': 1, 'だ': 1})
defaultdict(<bound method Vocab._default_unk_index of <torchtext.vocab.Vocab object at 0x10ab93470>>, {'<unk>': 0, '<pad>': 1, '。': 2, 'た': 3, 'すごい': 4, 'だ': 5, 'とても': 6, 'に': 7, 'めっちゃ': 8, '悲しい': 9, '楽しかっ': 10, '落ち込ん': 11, '非常': 12, '面白かっ': 13})
['<unk>', '<pad>', '。', 'た', 'すごい', 'だ', 'とても', 'に', 'めっちゃ', '悲しい', '楽しかっ', '落ち込ん', '非常', '面白かっ']
tensor([[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
        [ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
        [ 2.4372, -2.2453, -2.4799,  ...,  5.7351,  4.8191, -5.1895],
        ...,
        [ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
        [ 1.4983, -1.3844, -1.5434,  ...,  3.7629,  3.1596, -3.6079],
        [ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000]])
torch.Size([14, 300])

Iteratorオブジェクトの作成

Fieldオブジェクト、TabularDatasetオブジェクト、Vocabオブジェクトの作成が終われば、最後にBucketIteratorオブジェクトを作成します。
バッチサイズはトレーニングデータ、テストデータの件数を見て、適宜調整して下さい。本記事では両方とも2にしています。
テキスト間で形態素数が異なる場合に<pad>を追加するpaddingなどの処理は、オブジェクト作成時に勝手にやってくれます。
なお、BucketクラスではなくてBucketIteratorクラスを利用しているのは、類似の長さ(形態素数)のデータはなるべく同じバッチにしてくれるためです。恐らくその方が精度は上がると思います。

    # BucketIteratorオブジェクトの作成
    train_iter, test_iter = data.BucketIterator.splits(
        (train, test), batch_sizes=(2, 2),
         sort_key=lambda x: len(x.text))

BucketIteratorオブジェクトの中身を確認したい場合、次のように書きます。
この段階まで来ると、テキストデータは生データではなくてインデックス番号で管理されています。

    for batch in train_iter:
        print("label: ", batch.label)
        print("text: ", batch.text)

上記のプログラムの実行結果はこのようになります。
train.csvのデータ件数は4件でバッチサイズが2なので、イテレーション数は2(=4/2)になっています。

また、batch.textのtensorは横に読むのではなく縦に読んで下さい。
1回目のイテレーションでは[6, 9, 2, 1] [8, 10, 3, 2]の2つのデータが対象になっています。
これをTEXT.vocab.itosを使ってテキストに戻すと['とても', '悲しい', '。', '<pad>'] ['めっちゃ', '楽しかっ', 'た', '。']となります。paddingの処理が行われていることも確認できます。

label:  tensor([1, 0])
text:  tensor([[ 6,  8],
        [ 9, 10],
        [ 2,  3],
        [ 1,  2]])
label:  tensor([1, 0])
text:  tensor([[12,  4],
        [ 7, 13],
        [11,  3],
        [ 5,  2],
        [ 2,  1]])

モデルに渡す

前処理はここまでなので、この後は作成したオブジェクトをtorchで作成したモデルに渡すだけです。
詳細は割愛しますが、Vocabオブジェクトを引数に指定してモデルオブジェクトを作成し、BucketIteratorオブジェクトを使ってイテレーションの回数だけ学習させるというイメージです。
そして、テストデータを学習させたモデルに掛ければ、ラベルの一致度合いから精度を計算することができます。

最後に

ディープラーニングのフレームワークで自然言語処理のタスクを解く場合、ライブラリを使ってテキストの前処理をするけど実際にどんな処理が行われているのか把握されていない人もいると思います。
そこで、PyTorchでテキストの前処理を担当するtorchtextの使い方や実際のデータの変換過程をご紹介しました。
Chainerの開発終了に伴い、これからはPyTorchのユーザが増えてくると思うので何かしら参考になれば幸いです。