ナード戦隊データマン

データサイエンスを用いて悪と戦うぞ

英語word2vecを日本語word2vecに変換

マルチリンガルなWord Embeddingとは、複数の言語に対応するためのWord Embeddingの手法です。今回は、最も初歩的なモデルとして線形写像を使います。

概要

線形写像を用いたMultilingual Word Embeddingは、ソース言語のベクトルから、ターゲット言語のベクトルへの写像を学習することを意味します。

ソース言語とターゲット言語のベクトルを対応させる教師データは、以下の形式になっています:

reds レッズ
posters ポスター
expelled    退学
strait  海峡
communicate コミュニケーション
alphabet    アルファベット
chef    シェフ
prizes  懸賞
prizes  賞品
eighteen    十八
shirt   シャツ

この対応関係を定義したものを、"parallel vocabulary"といいます。

擬似言語的に書けば、以下のような方法で予測を行うためのmodel.predict関数を学習したいわけです。

# ja_kv: 日本語のword2vec
# en_kv: 英語のword2vec
# model: 英語から日本語へベクトル変換するモデル
# src_word: 英単語
ja_kv.most_similar(
    model.predict(en_kv[src_word])
)

このようなモデルを学習するための、精度の高い手法としては、facebookresearch/MUSE1 がありますが、もっと基本的な理解をするために、今回は線形写像によるモデルを試します。

事前準備

以下から、fasttextの日本語モデルと英語モデルをダウンロード。 https://fasttext.cc/docs/en/pretrained-vectors.html

以下から訓練データ、テストデータをダウンロード。 https://dl.fbaipublicfiles.com/arrival/dictionaries/en-ja.0-5000.txt https://dl.fbaipublicfiles.com/arrival/dictionaries/en-ja.5000-6500.txt

訓練のコード

from keras.models import Model
from keras.layers import Dense, Dropout, Input, concatenate
from keras.callbacks import ModelCheckpoint
from keras import optimizers
from sklearn.utils import shuffle
import io
import numpy as np


def load_vec(emb_path, nmax=500000000000000000):
    vectors = []
    word2id = {}
    with io.open(emb_path,
                 'r',
                 encoding='utf-8',
                 newline='\n',
                 errors='ignore') as f:
        next(f)
        for i, line in enumerate(f):
            word, vect = line.rstrip().split(' ', 1)
            vect = np.fromstring(vect, sep=' ')
            assert word not in word2id, 'word found twice'
            vectors.append(vect)
            word2id[word] = len(word2id)
            if len(word2id) == nmax:
                break
        id2word = {v: k for k, v in word2id.items()}
        embeddings = np.vstack(vectors)
        return embeddings, id2word, word2id


def build_model(dim=300):
    inp = Input(shape=(dim,))
    x = Dense(1024, activation="linear")(inp)
    x = Dropout(0.5)(x)
    x = Dense(1024, activation="linear")(x)
    x = concatenate([x, inp])
    x = Dropout(0.5)(x)
    out = Dense(dim, activation="linear")(x)
    model = Model(inp, out)
    opt = optimizers.Adam(lr=0.001, clipvalue=0.5)
    model.compile(loss="mse", optimizer=opt, metrics=["mse"])
    return model

def generate_data(train, en_embeddings, en_word2id, ja_embeddings, ja_word2id):
    X = []
    y = []
    while True:
        for x_tmp, y_tmp in train:
            try:
                tmp = [
                    en_embeddings[en_word2id[x_tmp]],
                    ja_embeddings[ja_word2id[y_tmp]]
                ]
            except Exception as e:
                print(repr(e))
                continue
            X.append(tmp[0])
            y.append(tmp[1])
            if len(X) > 1000:
                assert len(X) == len(y)
                yield np.array(X), np.array(y)
                X = []
                y = []


if __name__ == "__main__":
    print("loading")
    en_emb, _, en_w2id = load_vec("../data/wiki.en.vec")
    ja_emb, _, ja_w2id = load_vec("../data/wiki.ja.vec")
    print("done loading")

    print("loading data")
    with open("../data/jadata/en-ja.0-5000.txt") as f:
        data = []
        for line in f:
            data.append(line.strip().split("\t"))
        data = shuffle(data)
    print("data loading done")

    print("split data")
    size = int(len(data)*0.8)
    train, val = data[:size], data[size:]
    print("done splitting")

    print("training...")
    model = build_model()
    callbacks = [
        ModelCheckpoint("model.h5",
                        save_best_only=False,
                        monitor="val_loss",
                        mode="min"),
        ModelCheckpoint("model_best.h5",
                        save_best_only=True,
                        monitor="val_loss",
                        mode="min")
    ]
    model.fit_generator(
        generate_data(train, en_emb, en_w2id, ja_emb, ja_w2id),
        validation_data=generate_data(val, en_emb, en_w2id, ja_emb, ja_w2id),
        steps_per_epoch=1000,
        validation_steps=1,
        epochs=1000,
        callbacks=callbacks
    )
    print("done training")

テスト

In[1]:

from gensim.models import KeyedVectors
enmodel = KeyedVectors.load_word2vec_format("./data/wiki.en.vec")
jamodel = KeyedVectors.load_word2vec_format("./data/wiki.ja.vec")

In[2]:

with open("data/crosslingual/dictionaries/en-ja.5000-6500.txt") as f:
    test_data = [line.strip().split() for line in f]

In[3]:

def p_at_k(line, model, enmodel, jamodel, k=10):
    mid_vec = model.predict(np.array([enmodel[line[0]]]))
    heystack = [x[0] for x in jamodel.most_similar(mid_vec, topn=k)]
    needle = line[1]
    try:
        y = float(heystack.index(needle))/float(k)
    except ValueError:
        y = 0.0
    return y

precisions = [p_at_k(line, model, enmodel, jamodel, k=10) for line in test_data]
print(np.mean(precisions))

Out[3]:

0.021623123957754307

In[4]:

import pprint
src = ["obama", "murder", "mickey", "book", "god", "os", "driver"]
for src_word in src:
    mid_vec = model.predict(np.array([enmodel[src_word]]))
    print(src_word)
    pprint.pprint(jamodel.most_similar(mid_vec))
    print("")

Out[4]:

obama
[('バマ', 0.9984610676765442),
 ('ベギー', 0.9984151721000671),
 ('米紙', 0.9983594417572021),
 ('発言', 0.9983515739440918),
 ('米国', 0.9983422160148621),
 ('ネディ', 0.9983240365982056),
 ('ケシー', 0.998323917388916),
 ('我々', 0.9982770085334778),
 ('エハン', 0.998260498046875),
 ('文民', 0.9982603788375854)]

murder
[('犯', 0.9995188117027283),
 ('殺', 0.9995153546333313),
 ('罪', 0.9994887113571167),
 ('強談', 0.9994596242904663),
 ('死', 0.9994287490844727),
 ('男囚', 0.9993982315063477),
 ('醜行', 0.9993961453437805),
 ('騙', 0.999383807182312),
 ('急告', 0.9993802905082703),
 ('妻', 0.9993776082992554)]

mickey
[('パッジ', 0.9990284442901611),
 ('ポビー', 0.9989916682243347),
 ('ゲビー', 0.9989231824874878),
 ('ゴーツ', 0.9989019632339478),
 ('マベル', 0.998875617980957),
 ('ケシー', 0.9988710880279541),
 ('ノニー', 0.9988632202148438),
 ('コッズ', 0.9988256692886353),
 ('ソギー', 0.998824954032898),
 ('ケザー', 0.9988231658935547)]

book
[('書', 0.9995638728141785),
 ('題意', 0.9995237588882446),
 ('冊', 0.9995195865631104),
 ('自注', 0.9995168447494507),
 ('私', 0.9995006918907166),
 ('彼', 0.9994931817054749),
 ('著', 0.9994920492172241),
 ('述', 0.9994770884513855),
 ('小編', 0.9994733333587646),
 ('小冊', 0.9994729161262512)]

god
[('定命', 0.9994326233863831),
 ('神', 0.9994012117385864),
 ('彼', 0.9993762373924255),
 ('意想', 0.9993680715560913),
 ('空身', 0.9993419647216797),
 ('壮者', 0.9993240237236023),
 ('ヘズ', 0.9993231892585754),
 ('邪説', 0.999315083026886),
 ('全智', 0.9993149042129517),
 ('神゜', 0.9993104934692383)]

os
[('os', 0.9990252256393433),
 ('実装', 0.9986990094184875),
 ('pc', 0.9984074831008911),
 ('動作', 0.9983968138694763),
 ('hax', 0.9983946084976196),
 ('起動', 0.9983715415000916),
 ('eβc', 0.9983643889427185),
 ('osx', 0.9983490109443665),
 ('βc', 0.9983416795730591),
 ('入力', 0.9983031749725342)]

driver
[('開車', 0.9989429712295532),
 ('左用', 0.9988387823104858),
 ('車', 0.9988120198249817),
 ('一台', 0.998807966709137),
 ('速配', 0.9987748861312866),
 ('走回', 0.9987524747848511),
 ('出格', 0.9987507462501526),
 ('男用', 0.9987356066703796),
 ('も', 0.9987247586250305),
 ('競る', 0.9987195730209351)]

考察

precisionはそれほど高くはないですが、いくつかの語に対する目視確認をしてみると、概ね英単語に近い日本語が予測されています。

predict関数が意味するものは、「英語ベクトルを、日本語のベクトル空間内に写像する」ということなので、同一空間内に英語ベクトルと日本語ベクトルを(変換によって)置くことができます。

facebookresearch/MUSE[^1]以外の、過去のMultilingual Word Embeddingの研究は、概ねこの線形写像の方法を改善する方向で行われてきているようです。

精度の評価には、単語翻訳におけるP@kを使っていますが、MUSEが行っているように、複数の精度評価を組み合わせて、その中でベストのものを選ぶようなやり方のほうが、よりよいモデルを選択できる可能性があります。

詳細については、WORD TRANSLATION WITHOUT PARALLEL DATA2という論文が詳しいようです。(私もまだよく理解していないです。)