自然言語処理の深遠

Deep Dive Into Natural Language Processing

単語の順序を考慮しつつ文書を固定長で表現する

本日はACL 2017のベストペーパーの1つである以下の論文で用いられている文書表現の方法を紹介します。

この論文は、固有表現認識をFeedForward Neural Networkを使って文書分類的に解くという論文です。手法としては、メンションと呼ばれる固有表現候補の左右に位置するコンテキストを固定長のベクトルで表現してネットワークに入力しています。これら左右のコンテキストを固定長のベクトルで表現する際に使われるのが本記事で紹介するFOFE(Fixed-size Ordinally Forgetting Encoding)です。

f:id:Hironsan:20180613092744p:plain

FOFEの特徴として、単語の位置情報を考慮しつつ文書を固定長で表現できることにあります。今日はこのFOFEを使って語順を考慮しつつ文書を固定長で表現する方法を紹介します。

FOFEとは?

FOFE(Fixed-size Ordinally Forgetting Encoding)は、可変長の系列を固定長の表現に変換する方法です。特徴としては、変換した系列は一意であり、また情報の損失なく変換できる点です。この辺の話は以下の論文を参照。

FOFEは自然言語処理で文書を表現するのによく使われるBag-of-Words(BoW)に似ています。違いとしては、位置情報を考慮するために忘却係数(forgetting factor)を使っている点です。BoWがどんな長さの文でも固定長で表現できたように、FOFEも任意長の文を固定長で表現することができます。

FOFEについて説明するために、まずはいくつかの記法を導入します。まず、各単語はボキャブラリをVとしたとき、次元数|V|のone-hotベクトルで表現することができます。ここで長さTの系列をS = w_1, w_2, w_3,..., w_Tで表し、\boldsymbol{e_t}St(1 \leq t \leq T)番目の単語のone-hotベクトルとします。t番目の単語までをFOFEで表現した \boldsymbol{z_t} は以下のように再帰的に定義されます。

{} $$ \boldsymbol{z_t} = \left\{ \begin{array}{} \boldsymbol{0} & ( t = 0) \\ \alpha \cdot \boldsymbol{z_{t-1}} + \boldsymbol{e_t} & ( otherwise) \end{array} \right. $$

ここで \alpha (0 \leq \alpha \leq 1)は忘却係数(forgetting factor)と呼ばれます。ここで、\boldsymbol{z_t}の次元数が|V|であり、元の系列長であるTに無関係に表現されるのがポイントです。 \alpha はあらかじめ決めておきます。

ここまでの説明だとピンとこないと思うので、具体例を使ってみます。まず、ボキャブラリ数は3とします。その中身は{A, B, C}とでもしておきます。そして、それらのone-hot表現はそれぞれ [1, 0, 0]、[0, 1, 0]、[0, 0, 1] とします。このとき「ABC」を定義に従って計算すると [  \alpha ^2, \alpha , 1 ] になります。「ABCBC」なら [  \alpha ^4, \alpha + \alpha ^3, 1 + \alpha ^2 ] で表現されます。

以上で、FOFEについての説明は完了しました。次はFOFEの実装をしていきます。

FOFEの実装

FOFEは以下のように実装できます。FOFEVectorizerはscikit-learnのCountVectorizerを継承して書いています。継承して書いた関係で、初期化メソッドが少し長くなっていますが、中身に難しいところはないはずです。FOFEへの変換処理はtransformメソッドに書いてあります。

import numpy as np
from sklearn.feature_extraction.text import CountVectorizer


class FOFEVectorizer(CountVectorizer):
    """Convert raw documents to FOFE matrix.

    Attributes:
        alpha: float. forgetting factor.
    """

    def __init__(self, alpha=0.5, input='content', encoding='utf-8',
                 decode_error='strict', strip_accents=None, lowercase=True,
                 preprocessor=None, tokenizer=None, analyzer='word',
                 stop_words=None, token_pattern=r"(?u)\b\w\w+\b",
                 ngram_range=(1, 1), max_df=1.0, min_df=1,
                 max_features=None, vocabulary=None, binary=False,
                 dtype=np.int64):

        super(FOFEVectorizer, self).__init__(
            input=input, encoding=encoding, decode_error=decode_error,
            strip_accents=strip_accents, lowercase=lowercase,
            preprocessor=preprocessor, tokenizer=tokenizer, analyzer=analyzer,
            stop_words=stop_words, token_pattern=token_pattern,
            ngram_range=ngram_range, max_df=max_df, min_df=min_df,
            max_features=max_features, vocabulary=vocabulary, binary=binary,
            dtype=dtype)
        self.alpha = alpha

    def fit_transform(self, raw_documents, y=None):
        """Learn vocabulary, return FOFE matrix.

        This is equivalent to fit followed by transform.

        Args:
            raw_documents : iterable
            an iterable which yields either str, unicode or file objects

        Returns:
            X : FOFE matrix, [num_samples, num_vocabulary]
        """
        super(FOFEVectorizer, self).fit_transform(raw_documents)

        return self.transform(raw_documents)

    def transform(self, raw_documents):
        """Transform documents to FOFE matrix.

        Uses the vocabulary learned by fit or fit_transform.

        Args:
            raw_documents : iterable
            an iterable which yields either str, unicode or file objects

        Returns:
            X : FOFE matrix, [num_samples, num_vocabulary]
        """
        X = np.zeros((len(raw_documents), len(self.vocabulary_)))
        analyze = self.build_analyzer()

        for i, doc in enumerate(raw_documents):
            for word in analyze(doc):
                X[i, :] *= self.alpha
                try:
                    j = self.vocabulary_[word]
                except KeyError:  # Ignore unknown word.
                    continue
                X[i, j] += 1

        return X

FOFEVectorizerは以下のようにして使うことができます。

>>> raw_documents = ['AA BB CC']
>>> vectorizer = FOFEVectorizer()
>>> vectorizer.fit_transform(raw_documents)
[[0.25 0.5  1.  ]]

>>> raw_documents = ['AA BB CC BB CC']
>>> vectorizer.transform(raw_documents)
[[0.0625 0.625  1.25  ]]

文長が長くなると、アンダーフローを起こしそうですね。元の論文ではコンテキストのサイズがそれほど大きくなかったので、アンダーフローは問題にならなかったのだと思います。長めの文書分類で使う際には一工夫いりそうです。

おわりに

本記事では語順を考慮して文書を固定長で表現する技術であるFOFEを紹介しました。本記事が皆様のお役に立てば幸いです。

私のTwitterアカウントでも機械学習自然言語処理に関する情報をつぶやいています。

この分野にご興味のある方のフォローをお待ちしています。

参考資料