ナード戦隊データマン

機械学習と自然言語処理についてのブログ

系列ラベリングのより良いモデル: 単語 + 文字

系列ラベリングの基本的なモデルは、単語ベースの埋め込みを利用します。これに加えて文字ベースの埋め込みを追加する方法があります。

目的

TSA (targeted sentiment analysis) のための、より良いモデルを作成する。

コード

データの読み込み

from gensim.models.fasttext import FastText
from gensim.test.utils import datapath
import numpy as np
from tqdm import tqdm

kv = FastText.load_fasttext_format("./cc.en.300.bin")
n_words = 300000

def create_embedding_weights(kv, nb_words=300000, emb_dim=300):
    embedding_matrix = np.zeros((nb_words, emb_dim))
    embedding_dict = {}
    embedding_dict["<PAD>"] = 0
    embedding_dict["<UNK>"] = 1
    embedding_matrix[1].fill(1.0)
    for i, (word, _) in tqdm(enumerate(kv.wv.vocab.items())):
        if i >= nb_words-2:
            break
        embedding_dict[word] = i+2
        embedding_matrix[i+2] = kv.wv[word]
    return embedding_dict, embedding_matrix

def load_data(datafile="../data/conll/all.conll.train"):
    labels = {"o":0,"b-neutral":1,"i-neutral":2,"b-positive":3,"i-positive":4,"b-negative":5,"i-negative":6}
    rows_word = []
    row_word = []
    rows_tag = []
    row_tag = []
    with open(datafile) as f:
        for line in f:
            line = line.strip()
            if line:
                tmp = line.split()
                if len(tmp) != 2:
                    continue
                word, tag = line.split()
                if tag not in labels:
                    continue                
                row_word.append(word)
                row_tag.append(tag)
            else:
                assert len(row_word) == len(row_tag)
                rows_word.append(row_word)
                rows_tag.append(row_tag)
                row_word = []
                row_tag = []
    if row_word and row_tag:
        assert len(row_word) == len(row_tag)
        rows_word.append(row_word)
        rows_tag.append(row_tag)
        
    assert len(rows_word) == len(rows_tag)
    return rows_word, rows_tag, labels

embedding_dict, embedding_matrix = create_embedding_weights(kv)

rows_word, rows_tag, labels = load_data()
rows_word_dev, rows_tag_dev, _ = load_data("../data/conll/all.conll.dev")
rows_word_test, rows_tag_test, _ = load_data("../data/conll/all.conll.test")

前処理

import nltk
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical

char_dict = {c: i for i, c in enumerate(["<PAD>", "<UNK>"] + list("abcdefghijklmnopqrstuvwxyz"))}
n_char = len(char_dict.items())

def char_tokenize(text, char_dict, sequence=False):
    out = []
    if sequence:
        words = text
    else:
        words = nltk.word_tokenize(text)
    for word in words:
        tmp = []
        for c in list(word):
            if c in char_dict:
                tmp.append(char_dict[c])
            else:
                tmp.append(1)
        out.append(tmp)
    return out


def tokenize(text, embedding_dict, sequence=False):
    out = []
    if sequence:
        words = text
    else:
        words = nltk.word_tokenize(text)
        
    for word in words:
        word = word.lower()
        try:
            out.append(embedding_dict[word])
        except KeyError:
            out.append(embedding_dict["<UNK>"])
    return np.array(out, dtype=np.int32)

def my_pad_sequences(sequences):
    max_sent_len = 0
    max_word_len = 0
    for sent in sequences:
        max_sent_len = max(len(sent), max_sent_len)
        for word in sent:
            max_word_len = max(len(word), max_word_len)
    x = np.zeros((len(sequences), max_sent_len, max_word_len)).astype('int32')
    for i, sent in enumerate(sequences):
        for j, word in enumerate(sent):
            x[i, j, :len(word)] = word
    return x

def preprocess(rows_word, rows_tag, labels, embedding_dict, char_dict):
    X1 = [tokenize(words, embedding_dict, sequence=True) for words in rows_word]
    X1 = pad_sequences(X1, padding="post")
    X2 = [char_tokenize(words, char_dict, sequence=True) for words in rows_word]
    X2 = my_pad_sequences(X2)
    y = [[labels[tag] for tag in tags] for tags in rows_tag]
    y = pad_sequences(y, padding="post")
    y = np.array([to_categorical(i, num_classes=len(list(labels.items()))) for i in y])
    return X1, X2, y

X1_train, X2_train, y_train = preprocess(rows_word, rows_tag, labels, embedding_dict, char_dict)
X1_dev, X2_dev, y_dev = preprocess(rows_word_dev, rows_tag_dev, labels, embedding_dict, char_dict)
X1_test, X2_test, y_test = preprocess(rows_word_test, rows_tag_test, labels, embedding_dict, char_dict)

モデリング

from keras.models import Model, Input
from keras.layers import LSTM, Embedding, Dense, TimeDistributed, Dropout, Bidirectional, Concatenate
from keras_contrib.layers import CRF

def build_model(n_words, n_char, dim1=300, dim2=25):
    input_words = Input(batch_shape=(None, None), dtype='int32')
    input_chars = Input(batch_shape=(None, None, None), dtype='int32')
    wemb = Embedding(n_words, dim1, mask_zero=True)(input_words)
    cemb = Embedding(n_char, dim2, mask_zero=True)(input_chars)
    cemb = TimeDistributed(Bidirectional(LSTM(dim2)))(cemb)
    wemb = Concatenate()([wemb, cemb])
    out = Dropout(0.5)(wemb)
    out = Bidirectional(LSTM(100, return_sequences=True))(out)
    out = Dense(100, activation='tanh')(out)
    crf = CRF(7)
    out = crf(out)
    model = Model([input_words, input_chars], out)
    model.compile(optimizer="rmsprop", loss=crf.loss_function, metrics=[crf.accuracy])
    return model

model = build_model(n_words, n_char)
history = model.fit([X1_train, X2_train], y_train, batch_size=32, epochs=2, validation_data=([X1_dev, X2_dev], y_dev), verbose=1)

テスト

from seqeval.metrics import classification_report

y_pred = model.predict([X1_test, X2_test], verbose=1)
idx2tag = {i: w for w, i in labels.items()}

def pred2label(pred, idx2tag):
    out = []
    for pred_i in pred:
        out_i = []
        for p in pred_i:
            p_i = np.argmax(p)
            out_i.append(idx2tag[p_i])
        out.append(out_i)
    return out
    
pred_labels = pred2label(y_pred, idx2tag)
test_labels = pred2label(y_test, idx2tag)
print(classification_report(test_labels, pred_labels))

結果

           precision    recall  f1-score   support

        o       0.69      0.58      0.63      2151
  neutral       0.52      0.57      0.54       581
 positive       0.59      0.16      0.26       294
 negative       0.56      0.09      0.16       295

micro avg       0.65      0.50      0.56      3321
macro avg       0.64      0.50      0.54      3321

考察

このモデルは、固有表現を抽出することはできていますが、文脈に応じて感情極性を与えることはあまりできていません。しかし、precisionは高いので、検索したものの中で正解であった確率は高いようです。一方で、recallは低いので、正解したものの中で検索したものである確率は低いと言えます。

要するに、検出する能力は低いけど、検出したものが正解する可能性は高いということです。

データの不均衡の問題もありそうですが、もっと良いモデルがあると思います。ここで用いたモデルは、あくまでも固有表現抽出で使われるようなモデルなので、TSAへの応用力については確かなことは言えません。

最近は、Memory Network Attention という手法を使うのが流行っているらしいので、そちらも着手したいです。

参考