pytorchで単語をIndex表現に変換する方法とテキストファイルのDataLoaderを作る

書いてる理由

  • NLPやるぞー

参考

pytorchによる発展ディープラーニング

詳細

github.com

テキスト解析を実行する場合、画像と同じ様にテキストを何らかの数値の羅列にして扱いたい。
前回、mecabjanomeで文章を分かち書きしたが、このままでは数値ではないので、数値に変換する。
単語を数値に変換する方法は、パッとあげると以下2つ。

  • 単語のボキャブラリー辞書を作り、その辞書にindexを付けることでそのindex番号で単語を表現する方法。
  • 大量の文章から、単語の意味を表現できる様な変換をかけて単語をベクトル表現する方法。

上はどういうことかと言うと、今手元に文章データとして、以下の3つがあった場合、それを分かち書きするとそれぞれ以下となる。
「私は今日もジムへ行く。」 → "私", "は", "今日", "も", "ジム", "へ", "行く", "。"
「今日はいい天気だった。」 → "今日", "は", "いい", "天気", "だっ", "た", "。"
「明日の天気は晴れかな。」 → "明日", "の", "天気", "は", "晴れ", "かな", "。"

この分かち書きされた全ての単語を並べると、
"私"、"は"、"今日"、"も"、"ジム"、"へ"、"行く"、"。"、"いい"、"天気"、"だっ"、 "た"、"明日"、"の"、"晴れ"、"かな"
となり、"私"は0番、"は"は1番、"今日": 2、"も":3、"ジム": 4、・・・とindex番号を降って行くと、 仮に「明日はジムだった。」という文章が得られたら、
「明日はジムだった。」 → "明日"、"は"、"ジム"、"だっ"、"た" → [12, 1, 4, 10, 11]
とindex番号で文章を表現できる。(もし辞書にない単語が出たら、unknownとしてunknownのindex番号を貼るのが通例)

単語の意味を表現できる様な変換は、word2vecとかfasttextとかの手法でやるが次回に持ち越し。
とりあえず、単語のボキャブラリーの辞書を作って文章をデータ化するところをやる。

コードで解説

文章の前処理の実装

def preprocessing_text(text):
    text = re.sub('\r', '', text)  # windowsの改行コードの削除
    text = re.sub('\n', '', text)  # 改行の削除
    text = re.sub(' ', '', text)   # 半角スペースの削除
    text = re.sub(' ', '', text)  # 全角スペースの削除

    text = re.sub(r'[0-9 0−9]', '0', text)  # 数字を全て0に
    return text

ちょっと前にやった、文章から改行とか半角スペースとかを削除する関数を用意しとく。
単純に正規表現で半角/全角スペースと改行、数値を全て0に変換する処理。
数字を0に変える理由は、数字は色んな種類が出てくるわりに処理に重要な特徴を与えづらいから。

janomeでの分かち書きと前処理をくっつける

from tokenizer import janome_tokenize, mecab_tokenize
from preprocesser import preprocessing_text


def tokenizer_with_preprocessing(text):
    text = preprocessing_text(text)
    result = janome_tokenize(text)

    return result

def main():
    text = '昨日は とても暑く、気温が37度もあった'
    print(tokenizer_with_preprocessing(text))

これを実行すると、['昨日', 'は', 'とても', '暑く', '、', '気温', 'が', '00', '度', 'も', 'あっ', 'た', 'EOS', '']となり、半角スペースの削除や37が00となる。
この各単語をindexにしたい。

Datasetクラスの作成

画像解析の時のバッチでコールしたらデータを返す様な存在を定義。
torchtext.data.Fieldでテキストとラベルをまず作成する。
今回は、文章に対し、ボジティブとかネガティブとかの0, 1のラベルがあることを想定。
それぞれの引数の説明は以下を参照。
torchtext.data.TabularDataset.splitsでtrain/val/test用のデータセットをそれぞれ作成できる。それぞれがDataset型の様なものになる。ので、これを使ってバッチを回せる。
その後、画像のDataLoaderの様に、Datasetを食わし、バッチサイズとシャッフルを指定する感じでtorchtext.data.IteratorでDataLoaderを定義して行く。
ちなみに、data/のデータはここ

    max_length = 25
    TEXT = torchtext.data.Field(sequential=True,                     # データの長さが可変かどうか。
                                tokenize=tokenizer_with_preprocessing,  # 前処理として適応する関数
                                use_vocab=True,                                         # 単語をボキャブラリーに追加するかどうか
                                lower=True,                                                  # アルファベットを小文字にするか
                                include_lengths=True,                                # 文章の単語数のデータを保持するか
                                batch_first=True,                                         # バッチサイズをTensorの次元の先頭にするか
                                fix_length=max_length)                              # 全部の文章が同じ長さになる様にpaddingするサイズ
    LABEL = torchtext.data.Field(sequential=False,                  # データの長さは固定
                                 use_vocab=False)                                       # ボキャブラリーに追加しない

    # データセットの作成(torch.data.datasetの様なDataLoaderに食わすデータセットの作成)
    train_ds, val_ds, test_ds = torchtext.data.TabularDataset.splits(
        path='../data/', train='text_train.tsv', validation='text_val.tsv', test='text_test.tsv',
        fields=[('Text', TEXT), ('Label', LABEL)], format='tsv')
    print('tain_num:{}'.format(len(train_ds)))                       # 4
    print('train_example1:{}'.format(vars(train_ds[0])))      # train_example1:{'Text': ['王', 'と', '王子', 'と', '女王', 'と', '姫', 'と', '男性', 'と', '女性', 'が', 'い', 'まし', 'た', '。'], 'Label': '0'}
    print('train_example2:{}'.format(vars(train_ds[1])))      # train_example2:{'Text': ['機械', '学習', 'が', '好き', 'です', '。'], 'Label': '1'}

    # データローダーの作成
    train_dl = torchtext.data.Iterator(train_ds, batch_size=2, train=True)
    val_dl = torchtext.data.Iterator(val_ds, batch_size=2, train=False, sort=False)
    test_dl = torchtext.data.Iterator(test_ds, batch_size=2, train=False, sort=False)

ボキャブラリーの作成

単語とIDのボキャブラリーの作成。train/val/testに分割した後にこれをやる必要がある。なぜなら、学習データからでしかボキャブラリーは作れないから。
もしvalやtestのデータを使ってボキャブラリーを作ると、それはカンニングみたいな感じになるから。(現実的には、世の中の全ての単語をボキャブラリーにしたいがそれは不可能であり、学習データとして手元にあるものだけでボキャブラリーを作るしかなく、その状況を作っている。)
torchtext.data.Fieldでuse_vocab=Trueにしてある定義を使って、build_vocab(データセット, min_freq=n)を指定すると、n回以上出現した単語でボキャブラリーが作成される。

    # 単語を数値化するためのボキャブラリーの作成
    TEXT.build_vocab(train_ds, min_freq=1)  # min_freqは最小出現回数
    print(TEXT.vocab.freqs)  # 単語と出現回数のdictの様なもの
    print(TEXT.vocab.stoi)  # string to ID 単語とIDのdictの様なもの


    # 動作確認
    batch = next(iter(val_dl))
    print(batch.Text, batch.Label)  # batch.Textはtuple。 1つ目にTensorが2つ目が出現単語数。 batch.LabelはTensor。
    print(batch.Text[0].shape, batch.Label.shape)  # torch.Size([2, 25]) -> batch_size分25の次元。 torch.Size([])

print(TEXT.vocab.freqs)は以下の様な出力で、単語とその頻度が出力される。

Counter({'と': 5, '。': 4, 'の': 4, '文章': 4, 'な': 4, 'が': 3, '、': 3, 'を': 3, 'し': 3, '本章': 2, 'ます': 2, '評価': 2, 'て': 2, 'いる': 2, 'か': 2, '分類': 2, '王': 1, '王子': 1, '女王': 1, '姫': 1, '男性': 1, '女性': 1, 'い': 1, 'まし': 1, 'た': 1, 
'機械': 1, '学習': 1, '好き': 1, 'です': 1, 'から': 1, '自然': 1, '言語': 1, '処理': 1, 'に': 1, '取り組み': 1, 'で': 1, 'は': 1, '商品': 1, 'レビュー': 1, '短い': 1, 'に対して': 1, 'その': 1, 'ネガティブ': 1, 'ポジティブ': 1, '0': 1, '値': 1, 'クラス': 1, 'する': 1, 'モデル': 1, '構築': 1})

print(TEXT.vocab.stoi)は、string to indexの略で、単語とindex番号のdictの様なものが出力される。

defaultdict(<bound method Vocab._default_unk_index of <torchtext.vocab.Vocab object at 0x10449d8d0>>, 
{'<unk>': 0, '<pad>': 1, 'と': 2, '。': 3, 'な': 4, 'の': 5, '文章': 6, '、': 7, 'が': 8, 'し': 9, 'を': 10, 'いる': 11, 'か': 12, 'て': 13, 'ます': 14, '分類': 15, '本章': 16, '評価': 17, '0': 18, 'い': 19, 'から': 20, 'する': 21, 
'その': 22, 'た': 23, 'で': 24, 'です': 25, 'に': 26, 'に対して': 27, 'は': 28, 'まし': 29, 'クラス': 30, 'ネガティブ': 31, 'ポジティブ': 32, 'モデル': 33, 'レビュー': 34, '値': 35, '処理': 36, '取り組み': 37, 
'商品': 38, '女性': 39, '女王': 40, '好き': 41, '姫': 42, '学習': 43, '構築': 44, '機械': 45, '王': 46, '王子': 47, '男性': 48, '短い': 49, '自然': 50, '言語': 51})

<unk>はunknownで未知語が全て入る。
<pad>はpaddingで、解析するためにはデータの長さを固定にしたいが文章は長さが自由に決まるため、
最大値を決めておき(今回はmax_length = 25)、それに満たなければと言う単語を埋めることで、全ての文章データを同じサイズで扱うための挿入する単語。
ちなみに長すぎる場合は、25単語で切る。

以上。
ここまでで、文章を単語に切って、切った単語でボキャブラリーを作り、ボキャブラリーで単語をindex番号に変換することで、文章を数値として扱える様にした。
次回は、「大量の文章から、単語の意味を表現できる様な変換をかけて単語をベクトル表現する方法。」