データナード

機械学習と自然言語処理についての備忘録 (旧ナード戦隊データマン)

Sentence Embeddingを用いた多言語の転移学習

facebookresearch/LASER1とは、複数の言語に対応したSentence Embeddingです。

ざっくりした要約

LASERでは、英語のSentence Embeddingと他の言語のSentence Embeddingの類似度が同一空間で測れるように訓練されます。

モデル^2は以下のようになっています: キャプチャ.GIF

  1. 多言語コーパスでBPEによるサブワードを訓練。
  2. BPEを入力とし、BiLSTMの多層結合をmax_poolingによって1024次元に落とし込む。この部分をsentence embeddingという。
  3. さらに、sentence embedding, BPE, 言語IDを使ってデコーダLSTMを初期化し、出力層へつなぐ。
  4. このモデルを、出力言語を英語 or スペイン語と固定し、様々な入力言語で翻訳モデルとして訓練する。
  5. 最終的に、このモデルのsentence embeddingが多言語で使えるようになる。

説明

出力言語を固定化することによって、sentence embeddingが多言語に対応できると考えられます。

なぜスペイン語を使うのかについては詳細は書いていませんが、おそらく

  1. 出力言語に2言語使ったほうが精度が上がる。
  2. 英語を入力とした場合にスペイン語を出力とするため。

の2つの理由があるかもしれません。

仕組み的に言えば、このモデルを訓練するためには、翻訳タスクのためのパラレルコーパスが必要だと思います。

このモデルのどの部分が「ゼロショット」なのかというと、このモデルを使った転移学習を英語で行えば、他の言語にも適用できるということのようです。XNLIは、「前提と仮説から関係を予測する」というタスクですが、英語でこのタスクを訓練すれば他の言語でも同じようにXNLIが解ける、ということだと思います。

要するに、「ゼロショット」とは、事前訓練済みsentence embeddingを使うことによって、XNLIなどのタスクを英語で訓練するだけで多言語対応できるという意味であって、事前訓練そのもののことではありません。(たぶん)

実行方法

git clone https://github.com/facebookresearch/LASER
cd LASER

laserのプロジェクトディレクトリを定義します。

export LASER="${HOME}/projects/LASER"
bash ./install_models.sh
bash ./install_external_tools.sh

mecabは以下の方法でインストールしておいてください:

git clone https://github.com/taku910/mecab && \
    cd mecab/mecab && \
    ./configure --enable-utf8-only && \
    make && \
    make check && \
    make install && \
    ldconfig && \
    cd ../mecab-ipadic && \
    ./configure --with-charset=utf8 && \
    make && \
    make install

そして、https://github.com/facebookresearch/LASER/blob/master/source/lib/text_processing.pymecabの部分を以下のように書き換えます:

+ ('| mecab -O wakati -b 50000 ' if lang == 'ja' else '')

エンコーダを試すために、tasks/embedに以下のコードを作成します。(作成しなくても、embed.shを使えば試せます。)

import os
from random import choices
from string import ascii_uppercase, digits
from subprocess import check_output
from tempfile import NamedTemporaryFile

import numpy as np


def compute_emb(text, lang):
    dim = 1024
    input_name = None
    output_name = ''.join(choices(ascii_uppercase + digits, k=16))
    with NamedTemporaryFile(mode="w+t", delete=False) as input_file:
        input_file.write(text)
        input_name = input_file.name
    check_output(["./embed.sh", input_name, lang, output_name])
    X = np.fromfile(output_name, dtype=np.float32, count=-1)
    X.resize(X.shape[0] // dim, dim)
    os.remove(input_name)
    os.remove(output_name)
    return X


def cossim(v, w):
    return np.dot(v, w) / (np.linalg.norm(v) * np.linalg.norm(w))


if __name__ == "__main__":
    X_ja = compute_emb("これはテストです", "ja")[0]
    X_en = compute_emb("This is test", "en")[0]
    print(cossim(X_ja, X_en))

追記

2019/04/25 9:37

以下のようにすると高速化されます。

import os
import sys
from random import choices
from string import ascii_uppercase, digits
from subprocess import check_output
from tempfile import NamedTemporaryFile, TemporaryDirectory

import numpy as np

assert os.environ.get('LASER'), 'Please set the enviornment variable LASER'
LASER = os.environ['LASER']

sys.path.append(LASER + '/source/lib')
sys.path.append(LASER + '/source')

from embed import SentenceEncoder, Token, BPEfastApply, EncodeFile


def prepare_model():
    max_tokens = 12000
    max_sentences = None

    encoder = SentenceEncoder(
        os.path.join(LASER, "models/bilstm.93langs.2018-12-26.pt"),
        max_sentences=max_sentences,
        max_tokens=max_tokens,
        sort_kind='mergesort',
        cpu=False)
    return encoder


def encode_them(encoder, ifname, ofname, lang):
    bpe_codes = os.path.join(LASER, "models/93langs.fcodes")
    buffer_size = 10000
    buffer_size = max(buffer_size, 1)
    with TemporaryDirectory() as tmpdir:
        if lang != '--':
            tok_fname = os.path.join(tmpdir, 'tok')
            Token(ifname,
                  tok_fname,
                  lang=lang,
                  romanize=True if lang == 'el' else False,
                  lower_case=True,
                  gzip=False,
                  verbose=True,
                  over_write=False)
            ifname = tok_fname

        if bpe_codes:
            bpe_fname = os.path.join(tmpdir, 'bpe')
            BPEfastApply(ifname,
                         bpe_fname,
                         bpe_codes,
                         verbose=True,
                         over_write=False)
            ifname = bpe_fname

        EncodeFile(encoder,
                   ifname,
                   ofname,
                   verbose=True,
                   over_write=False,
                   buffer_size=buffer_size)


def compute_emb(encoder, text, lang):
    dim = 1024
    input_name = None
    output_name = ''.join(choices(ascii_uppercase + digits, k=16))
    with NamedTemporaryFile(mode="w+t", delete=False) as input_file:
        input_file.write(text)
        input_name = input_file.name
        print(input_name)
    encode_them(encoder, input_name, output_name, lang)
    X = np.fromfile(output_name, dtype=np.float32, count=-1)
    X.resize(X.shape[0] // dim, dim)
    os.remove(input_name)
    os.remove(output_name)
    return X


def cossim(v, w):
    return np.dot(v, w) / (np.linalg.norm(v) * np.linalg.norm(w))


if __name__ == "__main__":
    encoder = prepare_model()
    X_ja = compute_emb(encoder, "これはテストです", "ja")[0]
    X_en = compute_emb(encoder, "This is test", "en")[0]
    print(cossim(X_ja, X_en))