ナード戦隊データマン

機械学習, 自然言語処理, データサイエンスについてのブログ

tensor2tensorでsentencepieceを使う方法

tensor2tensorのt2t-datagen1では指定したproblemに定義された方法を使ってtensorを生成します。内部ではSubwordTextEncoderを使ってサブワードに分割しますが、vocabファイルの生成において、sentencepieceよりも非効率なので大量のメモリを消費する上に実行も遅いようです。そこでsentencepieceを使う方法を考えます。

TokenTextEncoder

TokenTextEncoderは、空白区切りのテキストをID列(tensor)に置き換えることができます。各IDは読み込んだvocabの行番号に対応します。

In [1]: from tensor2tensor.data_generators.text_encoder import TokenTextEncoder
In [2]: enc = TokenTextEncoder("ja-en_ja.vocab.txt")
In [3]: import sentencepiece as spm
In [4]: sp = spm.SentencePieceProcessor()
In [5]: sp.Load("ja-en_ja.model")
Out[5]: True

In [6]: text = "これはテストです。"
In [7]: text_pieces = ' '.join(sp.encode_as_pieces(text))
In [8]: text_pieces
Out[8]: '▁これは テスト です 。'

In [9]: enc.encode(text_pieces)
Out[9]: [168, 2591, 10, 28141]

上記コードを見てお気づきかと思いますが、TokenTextEncoderによるエンコードを、sentencepieceによって分割されたテキストに対して実行しています。ここで利用されるボキャブラリは、基本的にsentencepieceのボキャブラリと一致しています。

ただ、tensor2tensorでは、<EOS>という特殊なタグをボキャブラリに追加する必要があります。これは、文の終了を表すタグです。そのため、sp.encode_as_idsを使わずに、あえてTokenTextEncoderを用いています。

problemを登録する方法

yudianerは「事前にBPEによって前処理されたものを使えば、TokenTextEncoderによって対処できる」という点を指摘しています2

彼が公開しているコードは以下です:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os
import tarfile

# Dependency imports

from tensor2tensor.data_generators import generator_utils
from tensor2tensor.data_generators import problem
from tensor2tensor.data_generators import text_encoder
from tensor2tensor.data_generators import translate
from tensor2tensor.utils import registry

import tensorflow as tf

FLAGS = tf.flags.FLAGS

# End-of-sentence marker.
EOS = text_encoder.EOS_ID


@registry.register_problem
class TranslateMnzhBpe32k(translate.TranslateProblem):

  @property
  def targeted_vocab_size(self):
    return 32000

  @property
  def source_vocab_name(self):
    return "vocab.32k.mn.txt"

  @property
  def target_vocab_name(self):
      return "vocab.32k.ch.txt"

  def feature_encoders(self, data_dir):
    source_vocab_filename = os.path.join(data_dir, self.source_vocab_name)
    target_vocab_filename = os.path.join(data_dir, self.target_vocab_name)
    source_encoder = text_encoder.TokenTextEncoder(source_vocab_filename, replace_oov="UNK")
    target_encoder = text_encoder.TokenTextEncoder(target_vocab_filename, replace_oov="UNK")
    return {"inputs": source_encoder, "targets": target_encoder}

  def generator(self, data_dir, tmp_dir, train):
    """Instance of token generator for the mn->zh task, training set."""
    dataset_path = ("train.32k"
                    if train else "valid.32k")
    train_path = os.path.join(data_dir, dataset_path)

    source_token_path = os.path.join(data_dir, self.source_vocab_name)
    target_token_path = os.path.join(data_dir, self.target_vocab_name)

    source_token_vocab = text_encoder.TokenTextEncoder(source_token_path, replace_oov="UNK")
    target_token_vocab = text_encoder.TokenTextEncoder(target_token_path, replace_oov="UNK")
    return translate.token_generator_by_source_target(train_path + ".mn", train_path + ".ch",
                                     source_token_vocab, target_token_vocab, EOS)

  @property
  def input_space_id(self):
    return problem.SpaceID.MN_BPE_TOK

  @property
  def target_space_id(self):
    return problem.SpaceID.ZH_BPE_TOK
def token_generator_by_source_target(source_path, target_path, source_token_vocab, targe_token_vocab, eos=None):
  """Generator for sequence-to-sequence tasks that uses tokens.

  Args:
    source_path: path to the file with source sentences.
    target_path: path to the file with target sentences.
    source_token_vocab: text_encoder.TextEncoder object.
    targe_token_vocab: text_encoder.TextEncoder object.
    eos: integer to append at the end of each sequence (default: None).
  Yields:
    A dictionary {"inputs": source-line, "targets": target-line} where
    the lines are integer lists converted from tokens in the file lines.
  """
  eos_list = [] if eos is None else [eos]
  with tf.gfile.GFile(source_path, mode="r") as source_file:
    with tf.gfile.GFile(target_path, mode="r") as target_file:
      source, target = source_file.readline(), target_file.readline()
      while source and target:
        source_ints = source_token_vocab.encode(source.strip()) + eos_list
        target_ints = targe_token_vocab.encode(target.strip()) + eos_list
        yield {"inputs": source_ints, "targets": target_ints}
        source, target = source_file.readline(), target_file.readline()

Note: コードはボキャブラリや言語やファイル名に応じて修正してください。

ただ、これだけだとt2t-datagenが動作しないので、クラス内に以下のコードを追加します。

    def generate_data(self, data_dir, tmp_dir, task_id=-1):
        train_paths = self.training_filepaths(data_dir, 100, shuffled=True)
        dev_paths = self.dev_filepaths(data_dir, 1, shuffled=True)
        generator_utils.generate_files(
            self.generator(data_dir, tmp_dir, True), train_paths)
        generator_utils.generate_files(
            self.generator(data_dir, tmp_dir, False), dev_paths)

これは、訓練データとバリデーションデータを分割し、data_dir内に決められた名前で保存するためのコードです。t2t-datagenはgenerate_dataを呼び出すため、generateメソッドをgenerate_data内部で利用する形で定義する必要があります。

入出力のボキャブラリを分割して利用する方法

入力と出力で異なるボキャブラリを使っている場合、Embeddingの重みを共有する設定のままだと適していない可能性があります。 lukaszkaiserはこの点に関して、以下のように述べています3

Your crash, I believe, is due to this hyperparameter setting: https://github.com/tensorflow/tensor2tensor/blob/master/tensor2tensor/models/transformer.py#L296 Try running with `--hparams="shared_embedding_and_softmax_weights=0"' -- then we're not forcing the model to share source and target weights, which is impossible if vocabularies are different (and so it crashes). I'm closing for now, but please reopen if the problem still appears!

つまり、hparams内のshared_embedding_and_softmax_weightsをFalseに設定することにより、Embeddingの共有設定を外すことができます。

ただし、通常のケースでは、共有ボキャブラリを用いてthree-way weight tying methodを有効化したほうがメリットが大きいかもしれません。その場合、sentencepieceはソース言語とターゲット言語両方を含むコーパスから訓練し、共有のボキャブラリを持つ一つのモデルだけを用いて事前エンコードしておく必要があります。この場合、ボキャブラリファイルは1つだけ使うことになります。

翻訳の実際の流れ

翻訳は、サブワードの入力によって、サブワードの出力を得る形式になっています。つまり、sentencepieceを用いて翻訳する場合、

  1. 翻訳したい文をsentencepieceでtokenize。
  2. tensor2tensorで作成したモデルで翻訳。
  3. 翻訳によって出力されたサブワード列をsentencepieceでdetokenize。

という手順を取る必要があります。

参考