N-gramについて書いてみる

 

N-gramについて勉強したので、そのメモ。

この記事の最後にはpythonで実装したN-Gramを生成するコードを置いておきます。

 

はじめに

自然言語処理において自然言語をデータとした機械学習をする際に、はじめに自然言語から単語に切り出す必要があります。N-gramとは自然言語処理において単語の切り出しを行う手法のひとつです。ほかに形態素解析という手法もありますが、今回はN-gramに焦点を当てていきます。

N-gram

N-gramとは自然言語(テキスト)を連続するN個の文字、もしくはN個の単語単位で単語を切り出す手法のことです。

N=1のときユニグラム(unigram), N=2のときバイグラム(bigram), N=3のときトライグラム(trigram)と呼ばれます。

ここから文字と単語それぞれのN-gramについて具体的な例をあげて紹介してみます。

文字単位のN-gram

鼻毛がボーボーですね。という文を文字単位のN-gramにしてみる。

unigram

unigramは1文字単位で文字を切り出すので、

'鼻', '毛', 'が', 'ボ', 'ー', 'ボ', 'ー', 'で', 'す', 'ね', '。'

bigram

bigramは2文字単位で文字を切り出すので、

'鼻毛', '毛が', 'がボ', 'ボー', 'ーボ', 'ボー', 'ーで', 'です', 'すね', 'ね。'

trigram

trigramは3文字単位で文字を切り出すので、

'鼻毛が', '毛がボ', 'がボー', 'ボーボ', 'ーボー', 'ボーで', 'ーです', 'ですね', 'すね。'

単語単位のN-gram

鼻毛がボーボーですね。という文を単語単位のN-gramにしてみる。

単語単位にするにはまず鼻毛がボーボーですね。形態素解析する必要があります。形態素解析するにはMeCabが必要になるのでインストールしていない方はこちらの記事を参考にしてください。

www.pytry3g.com

unigram

unigramは1単語単位で単語を切り出すので、

'鼻毛', 'が', 'ボー', 'ボー', 'です', 'ね', '。'

これはただ形態素解析しただけのものと同じになる。

bigram

bigramは2単語単位で単語を切り出すので、

'鼻毛が', 'がボー', 'ボーボー', 'ボーです', 'ですね', 'ね。'

trigram

trigramは3単語単位で単語を切り出すので、

'鼻毛がボー', 'がボーボー', 'ボーボーです', 'ボーですね', 'ですね。'

長所と短所

N-gramの長所と短所について簡潔にまとめてみます。

長所

辞書が必要ない

形態素解析による単語の切り出しには辞書が必要になり、テキストには辞書には登録されていない未知語などが多々あるという問題がある。しかしN-gramは辞書を必要としないので単語の切り出しが楽にできる。ただし、単語単位のN-gram形態素解析をする必要があるので、この長所が当てはまるのは文字単位のN-gramのみである。

短所

検索ノイズ

検索結果に誤りが出てしまう点。

例えば、京都に関する情報をbigramによって生成されたデータから集めてみるとする。

そのデータの中に東京都は雷がひどいです。という文があったとすると、

これを文字単位のbigramにした場合、

'東京', '京都', '都は', 'は雷', '雷が', 'がひ', 'ひど', 'どい', 'いで', 'です', 'す。'

このように、京都に関する情報を集めようとしたにも関わらず、東京の情報が返ってくるという問題がある。

単語数

形態素解析と違い単語の数が膨大な数になってしまう点。Nの値が小さいほど切りだされた単語の数は多くなってしまうという欠点がある。

ソースコード

pythonでの実装例。

import MeCab
import re

def n_gram(sentence, n=2, type_="character", tagger=MeCab.Tagger('-Owakati'), append_sos=False, append_eos=False):
    """ Tokenize the sentence to N-gram.
    Args:
        sentence: Text strings to tokenize N-gram.
        n: Number of token, n=2 indicates bigram.
        type_: character or word.
        tagger: If you want to use NEologd, set path yourself.
        append_sos: If 'True' append SOS token onto the begin to N-gram.
        append_eos: If 'True' append EOS token onto the end to N-gram.
    
    Returns: 
        list of N_gram 
    """
    sentence = re.sub(r"\s+", "", sentence)
    if type_ == "character":
        n_gram = [(sentence[i:i+n]) for i in range(len(sentence)-n+1)]
        if append_sos:
            n_gram.insert(0, "<s>"+sentence[:n-1])
        if append_eos:
            if n == 1:
                n_gram.append("</s>")
            else:
                n_gram.append(sentence[-(n-1):]+"</s>")
    else:
        tokens = tagger.parse(sentence).strip().split()
        n_gram = ["".join(tokens[i:i+n]) for i in range(len(tokens)-n+1)]
        if append_sos:
            n_gram.insert(0, "<s>"+"".join(tokens[:n-1]))
        if append_eos:
            if n == 1:
                n_gram.append("</s>")
            else:
                n_gram.append("".join(tokens[-(n-1):])+"</s>")
    return n_gram


if __name__ == "__main__":
    sample = "鼻毛がボーボーですね。"
    tokens = n_gram(sample, n=2)
    print(tokens)

タプルでN-gramを返す

def n_gram(sentence, n=2, type_="character", tagger=MeCab.Tagger('-Owakati'), append_sos=False, append_eos=False):
    """ Tokenize the sentence to N-gram.
    Args:
        sentence: Text strings to tokenize N-gram.
        n: Number of token, n=2 indicates bigram.
        type_: character or word.
        tagger: If you want to use NEologd, set path yourself.
        append_sos: If 'True' append SOS token onto the begin to N-gram.
        append_eos: If 'True' append EOS token onto the end to N-gram.
    
    Returns: 
        list of tuple of N_gram 
    """
    sentence = re.sub(r"\s+", "", sentence)
    if type_ == "character":
        tokens = list(sentence)
        if append_sos:
            tokens.insert(0, "<s>")
        if append_eos:
            tokens.append("</s>")
        n_gram = [tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1)]
    else:
        tokens = tagger.parse(sentence).strip().split()
        if append_sos:
            tokens.insert(0, "<s>")
        if append_eos:
            tokens.append("</s>")
        n_gram = [tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1)]
    return n_gram