機械翻訳と夏の日の思い出

初めて機械翻訳に触れたのは中学何年だかの夏の終わりだった。 夏休みの英語の宿題は退屈で仕方がなかった。

僕が入力した日本語から生まれた英文は知らない単語だらけで、それが正しいかどうかはよく分からなかったが、回答欄に難しい単語を書くのは少しカッコイイことのような気がしたのでそのまま写して提出してみた。

今日は機械翻訳システムの話をしようと思う。 宿題の再提出を告げられる生徒が一人でも減る未来を願いながら。


ある英文を日本語に訳すことを考えよう。

英語と日本語は語順が違うので、文頭から順に対応する単語を並べても正しい翻訳にはならない。 真面目に翻訳するためには、前後の単語や前後の文を考慮して原言語の文の意味を捉える必要がある。

さて、英文に目を通し終えたとする。 その英文のどこから訳し始めるかはセンスの見せ所である。 翻訳文の書き出しを決めるためには、「文頭」という文脈において英文のどこに注目するのが良いか考えなければならない。 思案の末、翻訳文の一単語目を決めたとする。 次は二単語目である。 「一単語目が決まっている」という文脈で二単語目に相応しいものを考える。 以降、そのプロセスを繰り返しながら翻訳文が作られていく。

Attention-based Neural Machine Translationは、まさにこのようなプロセスで翻訳を解く機械翻訳のモデルである。 このモデルはエンコーダ、デコーダ、アテンションの3つのモジュールで構成されている。 エンコーダは入力文(翻訳元の文)を読みこんで各単語の表現(annotation vector)を得る。 アテンションは出力文(翻訳先の文)の文脈に基づいて入力文の各単語に対する注目度(attention)を計算する機構で、annotation vectorは計算された注目度に応じて足し合わされる(context vector)。 デコーダは出力文の文脈とcontext vectorに基づいて、翻訳文の続きを出力する。


最も基本的なAttention-based NMTモデルである[Bahdanau+, 2014]を実装しよう。 実装にはChainerを使っている。その他の開発環境の詳細についてはGithubを参照してほしい。

  • モデル全体の枠組み

Seq2seqクラスは翻訳モデルのインタフェースとして外部から入力を受け取る。 __call__関数は、入力文と出力文のペアを受け取り、入力文から生成した翻訳文と出力文の誤差を計算する。 translate関数は、入力文のみを受け取り、翻訳文を生成する。

class Seq2seq(chainer.Chain):

    def __init__(self, n_source_vocab, n_target_vocab,
                 n_encoder_layers, n_encoder_units, n_encoder_dropout,
                 n_decoder_units, n_attention_units, n_maxout_units):
        super(Seq2seq, self).__init__()
        with self.init_scope():
            self.encoder = Encoder(
                n_source_vocab,
                n_encoder_layers,
                n_encoder_units,
                n_encoder_dropout
            )
            self.decoder = Decoder(
                n_target_vocab,
                n_decoder_units,
                n_attention_units,
                n_encoder_units * 2,  # because of bi-directional lstm
                n_maxout_units,
            )

    def __call__(self, xs, ys):
        """Calculate loss between outputs and ys.
        Args:
            xs: Source sentences.
            ys: Target sentences.
        Returns:
            loss: Cross-entoropy loss between outputs and ys.
        """
        batch_size = len(xs)

        hxs = self.encoder(xs)
        os = self.decoder(ys, hxs)

        concatenated_os = F.concat(os, axis=0)
        concatenated_ys = F.flatten(ys.T)
        n_words = len(self.xp.where(concatenated_ys.data != EOS)[0])

        loss = F.sum(
            F.softmax_cross_entropy(
                concatenated_os, concatenated_ys, reduce='no', ignore_label=PAD
            )
        )
        loss = loss / n_words
        chainer.report({'loss': loss.data}, self)
        perp = self.xp.exp(loss.data * batch_size / n_words)
        chainer.report({'perp': perp}, self)
        return loss

    def translate(self, xs, max_length=100):
        """Generate sentences based on xs.
        Args:
            xs: Source sentences.
        Returns:
            ys: Generated target sentences.
        """
        with chainer.no_backprop_mode(), chainer.using_config('train', False):
            hxs = self.encoder(xs)
            ys = self.decoder.translate(hxs, max_length)
        return ys
  • エンコーダの実装

Encoderクラスは入力文の各単語のannotation vectorを計算する。 Bi-directional LSTMは入力を順方向だけでなく逆方向からも読み込むモデルで、前後の単語を考慮した表現が獲得されると言われている。 ChainerのNStepBiLSTMは可変長の入力に対応しているので、そのための前処理・後処理も行っている。

class Encoder(chainer.Chain):

    def __init__(self, n_vocab, n_layers, n_units, dropout):
        super(Encoder, self).__init__()
        with self.init_scope():
            self.embed_x = L.EmbedID(n_vocab, n_units, ignore_label=-1)
            self.bilstm = L.NStepBiLSTM(n_layers, n_units, n_units, dropout)

    def __call__(self, xs):
        """Encode source sequences into the representations.
        Args:
            xs: Source sequences.
        Returns:
            hxs: Hidden states for source sequences.
        """
        batch_size, max_length = xs.shape

        exs = self.embed_x(xs)
        exs = F.separate(exs, axis=0)
        masks = self.xp.vsplit(xs != -1, batch_size)
        masked_exs = [ex[mask.reshape((-1, ))] for ex, mask in zip(exs, masks)]

        _, _, hxs = self.bilstm(None, None, masked_exs)
        hxs = F.pad_sequence(hxs, length=max_length, padding=0.0)
        return hxs

Decoderクラスは出力文を生成する。 出力文の生成は、previous_embedding(前回の単語の表現)、h(現在の状態)、context(context vector)に基づいて次の単語を決定することを繰り返して行われる。 ただし、学習時にはys(正しい出力文)が与えられるため、previous_embeddingは前回の自身の出力ではなく、ysに基づいて計算される。これはteacher forcingと呼ばれている。

__call__関数の中で予測を出力するたびに正しい出力単語との誤差を取ることもできるが、 学習時間の短縮のために行列計算の回数をなるべく減らしたいので、全ての予測結果を結合しSeq2seq側で誤差をまとめて計算している。

class Decoder(chainer.Chain):

    def __init__(self, n_vocab, n_units, n_attention_units,
                 n_encoder_output_units, n_maxout_units, n_maxout_pools=2):
        super(Decoder, self).__init__()
        with self.init_scope():
            self.embed_y = L.EmbedID(n_vocab, n_units, ignore_label=-1)
            self.lstm = L.StatelessLSTM(
                n_units + n_encoder_output_units,
                n_units
            )
            self.maxout = L.Maxout(
                n_units + n_encoder_output_units + n_units,
                n_maxout_units,
                n_maxout_pools
            )
            self.w = L.Linear(n_units, n_vocab)
            self.attention = AttentionModule(
                n_encoder_output_units,
                n_attention_units,
                n_units
            )
            self.bos_state = Parameter(
                initializer=self.xp.random.randn(1, n_units).astype('f')
            )
        self.n_units = n_units

    def __call__(self, ys, hxs):
        """Calculate cross-entoropy loss between predictions and ys.
        Args:
            ys: Target sequences.
            hxs: Hidden states for source sequences.
        Returns:
            os: Probability density for output sequences.
        """
        batch_size, max_length, encoder_output_size = hxs.shape

        compute_context = self.attention(hxs)
        # initial cell state
        c = Variable(self.xp.zeros((batch_size, self.n_units), 'f'))
        # initial hidden state
        h = F.broadcast_to(self.bos_state, ((batch_size, self.n_units)))
        # initial character's embedding
        previous_embedding = self.embed_y(
            Variable(self.xp.full((batch_size, ), EOS, 'i'))
        )

        os = []
        for y in self.xp.hsplit(ys, ys.shape[1]):
            y = y.reshape((batch_size, ))
            context = compute_context(h)
            concatenated = F.concat((previous_embedding, context))

            c, h = self.lstm(c, h, concatenated)
            concatenated = F.concat((concatenated, h))
            o = self.w(self.maxout(concatenated))

            os.append(o)
            previous_embedding = self.embed_y(y)
        return os

    def translate(self, hxs, max_length):
        """Generate target sentences given hidden states of source sentences.
        Args:
            hxs: Hidden states for source sequences.
        Returns:
            ys: Generated sequences.
        """
        batch_size, _, _ = hxs.shape
        compute_context = self.attention(hxs)
        c = Variable(self.xp.zeros((batch_size, self.n_units), 'f'))
        h = F.broadcast_to(self.bos_state, ((batch_size, self.n_units)))
        # first character's embedding
        previous_embedding = self.embed_y(
            Variable(self.xp.full((batch_size, ), EOS, 'i'))
        )

        results = []
        for _ in range(max_length):
            context = compute_context(h)
            concatenated = F.concat((previous_embedding, context))

            c, h = self.lstm(c, h, concatenated)
            concatenated = F.concat((concatenated, h))

            logit = self.w(self.maxout(concatenated))
            y = F.reshape(F.argmax(logit, axis=1), (batch_size, ))

            results.append(y)
            previous_embedding = self.embed_y(y)
        else:
            results = F.separate(F.transpose(F.vstack(results)), axis=0)

        ys = []
        for result in results:
            index = np.argwhere(result.data == EOS)
            if len(index) > 0:
                result = result[:index[0, 0] + 1]
            ys.append(result.data)
        return ys
  • アテンションの実装

Attentionクラスはannotation vectorデコーダの状態に基づいてcontext vectorを計算する。 __call__関数は、デコーダの状態(hxs)に基づいて各単語に対するattention(attention)を計算し、その出力に応じてcontext vectorcontext)を計算する関数を返している。 broadcast_toreshapeを連発しているが、これらも行列計算の回数をなるべく減らすための工夫である。

class AttentionModule(chainer.Chain):

    def __init__(self, n_encoder_output_units,
                 n_attention_units, n_decoder_units):
        super(AttentionModule, self).__init__()
        with self.init_scope():
            self.h = L.Linear(n_encoder_output_units, n_attention_units)
            self.s = L.Linear(n_decoder_units, n_attention_units)
            self.o = L.Linear(n_attention_units, 1)
        self.n_encoder_output_units = n_encoder_output_units
        self.n_attention_units = n_attention_units

    def __call__(self, hxs):
        """Returns a function that calculates context given decoder's state.
        Args:
            hxs: Encoder's hidden states.
        Returns:
            compute_context: A function to calculate attention.
        """
        batch_size, max_length, encoder_output_size = hxs.shape

        encoder_factor = F.reshape(
            self.h(
                F.reshape(
                    hxs,
                    (batch_size * max_length, self.n_encoder_output_units)
                )
            ),
            (batch_size, max_length, self.n_attention_units)
        )

        def compute_context(previous_state):
            decoder_factor = F.broadcast_to(
                F.reshape(
                    self.s(previous_state),
                    (batch_size, 1, self.n_attention_units)
                ),
                (batch_size, max_length, self.n_attention_units)
            )

            attention = F.softmax(
                F.reshape(
                    self.o(
                        F.reshape(
                            F.tanh(encoder_factor + decoder_factor),
                            (batch_size * max_length, self.n_attention_units)
                        )
                    ),
                    (batch_size, max_length)
                )
            )

            context = F.reshape(
                F.batch_matmul(attention, hxs, transa=True),
                (batch_size, encoder_output_size)
            )
            return context

現在使われている機械翻訳システムの多くはAttention-based NMTがベースであり、いくつかの言語対では人間の翻訳性能に迫るモデルも提案されている。

その一方で、重要な課題も残っている。

一つは学習に非常に長い時間がかかることである。 計算時間のボトルネックになっているのはRecurrent Neural Network(RNN)による特徴抽出であり、RNNは入力を時系列順に受け付けるため、GPUによる並列計算の恩恵を受けづらい。 この課題を解決するためにConvolutional Neural Networkを用いたモデルやFeed-Forward Neural Networkを用いたモデルが提案されている。

また、モデルが何を学んでいるかが不明瞭なことも課題の一つだ。 機械翻訳を含む近年の深層学習の枠組みでは、正しい入力と出力のペアを用意し、それらの写像を直接学習する(end-to-end learning)。 ここで重要なのは、入力のどのような特徴に着目して(何を学んで)写像を解くかを数理的な最適化に委ねている点である。 例えば、ハヤシライスとカレーライスを識別するモデルを学習するとき、われわれは当然ハヤシライスらしさとカレーライスらしさが学習されることを期待する。 しかし、もしハヤシライスとカレーライスで盛り付けのお皿が違ったなら、このモデルは盛り付けのお皿を識別することで分類を解いてしまう可能性もある。 この例はあまりに単純だが、複雑な問題で学習が一見上手くいっている場合、そのモデルが本当にわれわれが期待する特徴を獲得しているか確認するのは非常に難しい。 近年はこうした深層学習のブラックボックスに迫る研究も進んでいる。


参考文献

Dzmitry Bahdanau, Kyunghyun Cho, and Yoshua Bengio. 2014. NEURAL MACHINE TRANSLATION BY JOINTLY LEARNING TO ALIGN AND TRANSLATE. CoRR abs/1409.0473. http://arxiv.org/abs/1409.0473 .

その他

実装: https://github.com/kiyomaro927/chainer-attention-nmt

ヒューマンコンピュテーションとクラウドソーシング

今後の研究で関連しそうなので、鹿島久嗣先生のヒューマンコンピュテーションとクラウドソーシングを読みます。 理解した内容を整理する目的で、何度かに分けて記事にしたいと思います。


深層学習を筆頭に機械学習研究の発展が一層勢いを増しています。 コンピュータが囲碁のプロ棋士に勝利したニュースは記憶に新しく、 今や、知的な行いだと信じられてきた様々なタスクでコンピュータの性能は人間の性能に迫りつつあります。

さて、華々しい研究成果が注目を集める一方で、機械学習研究が取り組んでいる問題の本質はそれほど変わっていません。 問題設定はこれまで通りで、その解き方もこれまで考えられてきたモデルの上での発展であるからです。 したがって、今のところ、機械学習は人間の持っている「知能」と等価になるようなものではありません。 しかし、機械学習をデータの利活用のための「道具」として見たとき、それは極めて強力な武器として機能します。

機械学習という「道具」は、人間の知識やアイディアと組み合わさることで大きな価値を生みます。 機械学習の大きな枠組みの1つに教師あり学習があります。 教師あり学習では、入力と出力の対が与えられ、その対応を学習することで、 未知の入力に対して適切な応答ができるようなモデルを構築することを目指します。 例えば、画像分類は教師あり学習に分類される問題の一つであり、未知の画像を自動的に分類することが目的です。 これは、入力が画像、出力がその画像のカテゴリー情報である場合の教師あり学習の問題であり、 郵便番号の自動識別など様々な形で応用されています。 ここで重要なのは、教師あり学習でモデルを学習するためには、入力に対する正しい出力が用意されている必要があるということです。 入力に対する正しい出力を学習することは、言い換えれば、われわれ人間が何を正しいと考えるかを学習することであり、 その学習のためにはわれわれの知識を導入することが欠かせません。 したがって、画像分類を含めた多くの教師あり学習問題では、学習に先立って、人間が手動で教師データのラベリングを行う必要があります。 これは、人間をある種の知的な演算装置とみなして適切な出力を計算し、その出力を真似るようにコンピュータに学習させていると解釈できます。 ヒューマンコンピュテーションとは、コンピュータが自力で解くことが困難なタスクを人間の能力を利用して解決することです。 教師データのラベリングを人間の能力、データの対応の自動学習をコンピュータの能力とすると、教師あり学習はヒューマンコンピュテーションの一例といえます。

現在のそうした人間と機械の協調を支えている仕組みがクラウドソーシングです。 クラウドソーシングはインターネットを通じて不特定多数の人に仕事を依頼すること、もしくはその仕組みのことを指します。 例えば、一般に問題が難しければ難しいほど、データから統計的な性質を獲得するために必要な学習データは膨大になり、 それらに教師データを付与するのは大変な作業になります。 一方で、ラベリングという作業そのものはそれほど難しいものではありあません。 そこで、膨大なタスクを数秒から数分で実行できるサイズのタスク(マイクロタスク)に切り分け、 世界中にいるワーカに少しずつ分担させることが行われています。

もちろん、クラウドソーシングの実施形態はマイクロタスク型だけではありません。 依頼者とワーカの目的に応じて、いくつかの種類が存在します。

  • マイクロタスク型: ワーカはごく短い時間で完了する単純な仕事を行います。
  • プロジェクト型: ワーカは専門性が必要な仕事を行います。
  • コンペティション型: ワーカは同一の課題に取り組み、一部の優れた成果にのみ報酬が支払われます。
  • ボランティア型: ワーカは社会貢献などを目的として、無償で仕事を行います。

重要なことは、ヒューマンコンピュテーションは人間を演算装置として複雑な問題を解決するという考え方であること、 そして、クラウドソーシングは大勢のワーカを容易に調達するプラットフォームを提供するということです。 このような考え方に基づき、人間の処理能力そのものをアプリケーションに取り込むことで 現状の「道具」では解決できない問題に取り組む例も登場しています。


鹿島 久嗣・小山 聡・馬場 雪乃 (2016) 『ヒューマンコンピュテーションとクラウドソーシング (機械学習プロフェッショナルシリーズ)』 講談社

イニシエーション・ラブ

言葉は、その目的によって伝え方や受け取り方が変わります。

例えば、学術論文やソフトウェアのドキュメンテーションなんかでは、 読み手は自分にとって重要な情報さえ分かれば良いことが多いので、自分の目的に関係ない部分は読み飛ばします。 逆に、書き手は可読性や検索性を意識して、読み手が重要な情報にちゃんと辿り着けるよう工夫します。

小説は少し性質が違います。 言葉の細かいニュアンスだったり、あるいはその時点では冗長すぎる状況描写だったり、 読み手は、そんな全ての表現に筆者の意図があると信じて、一文一文を追いながらその世界を頭の中で組み立てます。 逆に、書き手は見せたい景色がちゃんと伝わる必要十分な記述で、言葉の中にトリックを仕掛けて読者の予想を裏切ってみたりします。

今更ながら、乾くるみさんの「イニシエーション・ラブ」を読了しました。 初めに感想を言うと、描写がとても丁寧で、最後まで楽しく読める作品でした。

普段は全くと言っていいほど小説を読まないんですが、メンタルモデルが近い(と勝手に思ってる)友人の薦めで、 春休みを利用して何冊か読んでみようと思っています。今回はその一冊目です。

ネタバレはしませんが、 本作を楽しみ尽くすなら、予備知識や先入観を極力持たないようにして、素直な気持ちで読むことをおすすめします。 このブログに辿り着いているような人に予備知識ゼロを求めるのは無茶な話かもしれないけど。


さて、本作のタイトルであるイニシエーション・ラブですが、本書の中で次のように定義されます。

初めて恋愛を経験したときには誰でも、この愛は絶対だって思い込む。絶対って言葉を使っちゃう。でも人間にはーこの世の中には、絶対なんてことはないんだよって、いつかわかるときがくる。それがわかるようになって初めて大人になるっていうのかな、それをわからせてくれる恋愛のことを、彼はイニシエーションって言葉で表現してたの。それを私ふうにアレンジするとー文法的には間違ってるかもしれないけど、カッコ良く言えばーイニシエーション・ラブって感じかな

恋愛に限らず、たまたま初めてのタイミングでやってきただけの経験を過度に一般化してしまうのはよくある話です。

そんな偏った考えは、イニシエーション・ラブ(ラブとは限らないけど)を経験した人から見ると未熟に見えるのかもしれませんが、 じゃあ、イニシエーション・ラブを経験した人の方が偉いのかというとそれは違うと思うんです。

「絶対なんてことはない」と気付いたといっても、それはもう少し言葉を付け加えるなら、 「絶対なんてことは"絶対"ない」ということに気付いたわけで、 あれ?また絶対とか言ってない?ということになります。 これは哲学・論理学の分野で自己言及のパラドックス(liar paradox)として議論されています。

結局、経験をベースに価値観を構築する限りそこには多かれ少なかれバイアスがあるはずで、 この世で起き得る全てを経験するには寿命が短かすぎるわれわれ人間は、 限られた経験によって形作られる偏った価値観の中で生きていくしかありません。

どうせ偏った世界で生きるなら、せめて自分が楽しいと思える選択をして、 その価値観がひっくり返るような経験をする前にひっそり天国に行きたいなあ。