chisataki’s blog

リコリス・リコイルじゃありません

EmbeddingBagレイヤーを用いて感情分析する

今回はPytorchのEmbeddingBagレイヤーを使い、日本語の入力文章がポジティブかネガティブか判定する簡易的な分類モデルを作成します。
単語の埋め込みベクトル化にはword2vecを用います。main関数内のパラメータを変えることでポジティブ、ニュートラル、ネガティブの3クラスなど、多クラス分類にも容易にカスタマイズできます。

EmbeddingBagを使う利点は、入力シーケンスがすべて同じ長さになるように、\<pad>でパディングする必要がなくなることです。

まずは以下のような教師データ20個をtsvファイルで準備します。
ラベル0はネガティブ、1はポジティブな文章です。

label    text
0   これはひどい映画でした。
1   この映画が気に入りました! 強くお勧めします。
0   ただひどい
1   良い映画と演技です
0   時間を無駄にしないでください - 本当にダメです
0   ひどい
1   素晴らしい映画。
0   これは才能の無駄遣いでした。
1   この映画がとても気に入りました。 見てください。
1   ここ数年で見た中で最高の映画。
0   演技も下手、脚本も下手。
1   この映画をみんなにお勧めします
1   面白くて楽しい。
0   私はこの映画がまったく好きではありませんでした。
1   古き良き時代の物語
0   その話は私には意味が分かりませんでした。
0   最初から最後まで素人っぽい。
1   この動きがとても気に入りました。 多くの楽しみ。
0   私はこの映画が嫌いで出て行きました。
1   すべての年齢層向けのスリル満点の冒険。


単語のベクトル化にはword2vecを事前にダウンロードしたものを使います。

from torchtext.vocab import Vectors

v=Vectors(name='japanese_word2vec_vectors.vec')


教師データを読み込み、label, textのボキャブラリーを構築します

import pandas as pd
from torchtext.vocab import vocab, build_vocab_from_iterator

df = pd.read_csv('sample_train.tsv', names=['label','text'], header=0, sep='\t')
# label
df['label'] = df['label'].astype(str) 
label_vocab = build_vocab_from_iterator(df['label'])

# text
text_vocab = vocab(v.stoi, min_freq=0, specials=('<unk>',), special_first=True)
text_vocab.set_default_index(0)


今回は単語分割にはjanomeを使用します。正規化などは行わず、単純にわかち書きのみ行います。

from janome.tokenizer import Tokenizer

j_t = Tokenizer()
def tokenizer_janome(text):
    return [tok for tok in j_t.tokenize(text, wakati=True)]


テキスト変換を行うtransformを定義します。文章の長さを整えるpaddingは行いません。

import torchtext.transforms as T

label_transform = T.Sequential(
    T.VocabTransform(label_vocab),
    T.ToTensor()
)
text_transform = T.Sequential(
    T.VocabTransform(text_vocab),
    T.ToTensor()
)


Dataloaderに渡すCollate_fnを定義します。EmbeddingBagに文章の区切り位置を示すoffsetを渡す必要があるので、ここで返却できるようにします。

def collate_batch(batch):

    label_l = []
    text_l = []
    offset_l = [0]
    for label, text in batch:
        label_l.append(label)
        tokens = text_transform(tokenizer_janome(text))
        offset_l.append(len(tokens))
        text_l.append(tokens)

    label_l = label_transform(label_l)
    offset_l = torch.tensor(offset_l[:-1]).cumsum(dim=0)    
    text_l = torch.cat(text_l)

    return label_l, offset_l, text_l


EmbeddingBagレイヤーを使用した分類クラスを定義します。

import torch.nn as nn

class TextClassificationModel(nn.Module):

    def __init__(self, pretrained_embeddings, num_class, freeze_embeddings = False):
        super().__init__()        
        self.embedding = nn.EmbeddingBag.from_pretrained(pretrained_embeddings,
                                                         freeze = freeze_embeddings,
                                                         )
        # EmbeggingBag で各文が埋め込みベクトルで表されます。
        # 単語の連続情報が失われるため、以下単純なニューラル ネットワークを使用します。
        self.fc = nn.Linear(pretrained_embeddings.shape[1], num_class) 
        self.init_weights()

    def init_weights(self):
        initrange = 0.5
        self.fc.weight.data.uniform_(-initrange, initrange)
        self.fc.bias.data.zero_()

    def forward(self, text, offsets):
        embedded = self.embedding(text, offsets)
        return self.fc(embedded)


train関数を定義します。最適化手法にはSGD、損失関数はCrossEntropyLoss()を使用しています。

import torch

def train(net, ldr, bs, me, le, lr):
    # パラメータ: network, Dataloader, batch_size, max_epochs, log_every, learn_rate
    net.train()
    opt = torch.optim.SGD(net.parameters(), lr=lr)
    loss_func = torch.nn.CrossEntropyLoss()  # 内部でsoftmaxが行われる
    print("\nStarting training")
    for epoch in range(0, me):
        epoch_loss = 0.0
        for bix, (labels, offsets, texts) in enumerate(ldr):
            opt.zero_grad()
            oupt = net(texts, offsets)  
            loss_val = loss_func(oupt, labels)  
            loss_val.backward() 
            epoch_loss += loss_val.item()  
            opt.step()  
        if epoch % le == 0:
            print("epoch = %4d   loss = %0.4f" % (epoch, epoch_loss))
    print("Done ")


ここまで準備できたら、main関数を定義します。

from torch.utils.data import DataLoader

def main():

    # dataloader作成
    bat_size = 2
    train_ldr = DataLoader(df.values, batch_size=bat_size, shuffle=False, collate_fn=collate_batch)

    # model作成
    num_class=2
    net = TextClassificationModel(pretrained_embeddings, num_class)

    # 訓練
    max_epochs = 150
    log_interval = 30
    lrn_rate = 0.01
    train(net, train_ldr, bat_size, max_epochs, log_interval, lrn_rate)

    # 予測
    text = "この映画めちゃ面白い"
    net.eval()
    with torch.no_grad():
        oupt = net(text_transform(tokenizer_janome(text)), torch.tensor([0]))     
    predict = torch.softmax(oupt, dim=1)

    print("text: {}".format(text))
    print("Sentiment prediction probabilities [neg, pos]: ")
    print(predict)
    


出力

...
text: この映画めちゃ面白い
Sentiment prediction probabilities [neg, pos]: 
tensor([[0.0188, 0.9812]])


参考記事: visualstudiomagazine.com