かんちゃんの備忘録

プログラミングや言語処理、ゲームなど知的好奇心のための備忘録(個人の感想)です。

系列ラベリングの素性抽出

系列ラベリング問題を取り扱う際の素性抽出が、いつも複雑になりがちなので、テンプレートを書いてサクッと抽出できるよう整理しました。

どんな素性を抽出したいか

固有表現抽出を例にあげます。

以下の表は、「午前8時に東京駅で集合する。」という文を形態素解析し、IOB2(Inside-outside-beggining)タグ形式で固有表現のラベルを付与したものです。

単語 品詞 IOB2タグ
午前 名詞 B-TIME
名詞 I-TIME
名詞 I-TIME
助詞 O
東京 名詞 B-LOCATION
名詞 I-LOCATION
助詞 O
集合 名詞 O
する 動詞 O
記号 O

「午前8時」はTIME属性、「東京駅」はLOCATION属性を持つことになります。

ここで、「東京」という単語を例に素性抽出します。

素性には、対象単語と前後2単語の表層形、対象単語と前後2単語の品詞、推定済みの前2単語のIOB2タグを利用するとします。 あるラベルを学習する際の素性は、Pythonの辞書形式で表すと以下のようになります。

{
    "word-2": "時",
    "word-1": "に",
    "word": "東京",
    "word+1": "駅",
    "word+2": "で",
    "pos-2": "名詞",
    "pos-1": "助詞",
    "pos": "名詞",
    "pos+1": "名詞",
    "pos+2": "助詞",
    "iob-2": "I-TIME",
    "iob-1": "O"
}

これを簡単に抽出できるように、テンプレート作ります。

テンプレートを使って素性抽出

テンプレートは、「ラベル名、素性抽出のための関数、対象単語からの相対的な位置」を持ちます。 素性抽出のための関数は、対象の素性を抽出するための関数で、例えば小文字かどうかを素性に含めたい場合は lambda x: x.surface.islower() のような処理を記述します。 例では、あるトークンのインスタンス変数としてsurfaceを持っているため、x.surfaceでアクセスしていますが、x['surface']のように与えるトークンの形式により異なります。

# 素性抽出のための関数
word_feature = lambda x: x.surface
pos_feature = lambda x: x.pos
iob2_feature = lambda x: x.iob2

# テンプレート
templates = [
    ("word-2", word_feature, -2), ("word-1", word_feature, -1), ("word", word_feature, 0), ("word+1", word_feature, 1), ("word+2", word_feature, 2),
    ("pos-2", pos_feature, -2), ("pos-1", pos_feature, -1),("pos", pos_feature, 0), ("pos+1", pos_feature, 1), ("pos+2", pos_feature, 2),
    ("iob2-2", iob2_feature, -2),  ("iob2-1", iob2_feature, -1),
]

テンプレートを適用し素性抽出を行うメソッドを書きます。

def iter_feature(tokens, templates):
    tokens_len = len(tokens)
    for i in range(tokens_len):
        # バイアス項
        feature = {"bias": 1.0}

        # テンプレートを適用
        for label, f, target in templates:
            current = i + target
            if current < 0 or current >= tokens_len:
                continue
            feature[label] = f(tokens[current])

        # BOSとEOS
        if i == 0:
            feature["BOS"] = True
        elif i == tokens_len - 1:
            feature["EOS"] = True

        yield feature

たとえば、scikit-learnを利用する場合は、辞書形式で抽出された素性を、DictVectorizerでベクトル化することで、利用可能となります。

features = []
for tokens in corpus:
    features.extend(iter_feature(tokens, templates))

# from sklearn.feature_extraction import DictVectorizer
feature_vectorizer = DictVectorizer()
vec = feature_vectorizer.fit_transform(features)

同じテンプレートで推定を行う

実際の推定時には、以下のように先頭から順に推定し、推定したタグを代入していくことで、次のタグの素性抽出時に推定した前のタグが利用可能になります。

# tokensは推定したいトークン列, templatesは学習時と同じものを利用
for token, feature in zip(tokens, iter_feature(tokens, templates)):
    # 素性抽出
    vec = feature_vectorizer.transform(feature)
    # tokenのiob2変数に推定値をセットする
    token.iob2 = label_encoder.inverse_transform(model.predict(vec))[0]