How to make datas our friends

「エンジニアは発信していくことが責務である」という言葉に感化されて始めた勉強したことを書き留めていく備忘録的なやつ。

Pythonでn-gramを作成する

背景

言語処理100本ノック 2015を今やっているのでその備忘録的なやつ。

やりたいこと

与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成し、この関数を用い、"I am an NLPer"という文から単語bi-gram、文字bi-gramを得る。

結果

なんか色々イケてない感はあるものの関数ngramに引数で文章と何文字で区切るか、文字か単語かを渡してreturnで返すようにしました。
別にエラー文とかいらなかったな、とか思っていたりw

import re

sentense = "I am an NLPer"

def ngram(sentense, scale, type):
    # 関数の第三引数typeがwordの場合
    if type == 'word':
    
        # 変数を用意
        str = ''
        wordlist = []
    
        # 文章からピリオドを除外して空白で分割
        splited = re.split('\s', sentense.replace('.', ''))
    
        # もし第二引数の値が文章内の単語数より少なければエラーを出す
        if scale > len(splited):
            print("ERROR: The argument, scale must be lawer than the actual number of the words in the argument, sentense.")
        else:
            for i in range (0, len(splited)-(scale-1)):
                for ii in range (0, scale):
                    if ii == 0:
                        str += splited[ii+i]
                    else:
                        str += ' ' + splited[ii+i]
                wordlist.insert(i, str)
                # strを初期化
                str = ''
        
            return wordlist

    # 関数の第三引数typeがcharの場合
    elif type == 'char':
        charlist = []
        # 文字nグラムを作成
        trimed = (sentense.replace(' ', '')).replace('.', '')
        
        if scale > len(trimed):
            print("ERROR: The argument, scale must be lawer than the actual number of the characters in the argument, sentense.")
        else:
            for iii in range (0, len(trimed)-(scale-1)):
                charlist.insert(iii, trimed[iii:iii+scale])
    
            return charlist

    # 関数の第三引数typeがword/charでない場合エラーを出す
    else:
        print("ERROR: The 3rd argument, type must be char or word.")

data1 = ngram(sentense, 2, "word")
data2 = ngram(sentense, 2, "char")

実行結果

> data1
> ['I am', 'am an', 'an NLPer']
> data2
> ['Ia', 'am', 'ma', 'an', 'nN', 'NL', 'LP', 'Pe', 'er']

解説/考察

そもそもnグラムってなによってところから始まりました。無知ですみません。
で、色々ググりました。

kotobank.jp

要は、n文字/単語単位で文字列を1文字/単語ずつスライドさせていったやつの集合体という認識です。
今回はbiグラムなので2文字/2単語ですね。

ab cd ef gh を単語bi-gramにすると、ab cd/cd ef/ef gh に、文字bi-gramにすると ab/bc/cd/de/ef/fg/gh になる。
え、この認識で合ってますよね?w

で、今回は関数を作って単語bi-gramと文字bi-gramを作れってお題だったのでこんな感じの関数を作ってみました。

def ngram(sentense, scale, type):

第一引数に文章を、第二引数に何文字で区切るか(今回だとbi-gramなので2を渡す)、で最後に文字/単語どっちのn-gramがほしいか指定して関数を実行すると戻り値で帰ってくる的なやつです。

単語bi-gramを作る
splited = re.split('\s', sentense.replace('.', ''))
    
for i in range (0, len(splited)-(scale-1)):

 for ii in range (0, scale):
  if ii == 0:
   str += splited[ii+i]
  else:
   str += ' ' + splited[ii+i]
 
 wordlist.insert(i, str)
 str = ''

まず例のごとく文章を空白単位で分割して変数splitedにぶっこんでます。
そのあとfor文を回してリスト型の変数wordlistにデータを流し込んでいます。

for文を回す回数は「単語の数 - (何個単位かの値 - 1)」で計算させてみました。
例えば、bi-gramで3単語の場合 = 3 - (2 -1) で 2 になります。
文章が「AA BB CC」なら「AA BB / BB CC」にしたいので上の計算法を使えばぴったりリスト内に収まる計算です。

for i in range (0, 単語の数 - (何個単位かの値 - 1)):

続いてリスト内に格納する文字列の作成を行っています。

for ii in range (0, scale):
 if ii == 0:
  str += splited[ii+i]
 else:
  str += ' ' + splited[ii+i]

この部分です!単語単位で分割した文字列を、for文を回した回数に応じて横スライドさせ結合させてみました。
[ii+i]なので「ここでのfor文の回数+1つ上のfor文の回数」番目の単語をリストから取り出しています。

で最後にリストに生成した文字列を入れて文字列変数を初期化しています。

文字bi-gramを作る
trimed = (sentense.replace(' ', '')).replace('.', '')
        
for iii in range (0, len(trimed)-(scale-1)):
 charlist.insert(iii, trimed[iii:iii+scale])

こっちはだいぶシンプルです。

まず文章の空白を詰める作業をしています。
最初は以下の関数でやっていたのですがうまくいかず、結局replace関数で置換しています。
途中で気がついたのですが、この関数文章中の空白はトリムできないんですねw

sentense. strip()

あとは単語bi-gramと同じ原理でfor文を回してリストにデータを入れています。
文字列の結合は str[i:j:k] を使用してやってみました。

charlist.insert(iii, trimed[iii:iii+scale])

リストのiii番目に空白詰めした文字列のiii番目から「iii+何番目の文字まで取るかの値」番目までの文字列を切り出して格納しています。

最後に

今回のお題は今までで一番時間かかりました。

きっと僕がもっとPythonをマスターしたら、この記事のコードを書き換えたいと思うんだろうな〜とか思いつつGWが終わってしまったので明日からお仕事がんばります。笑