人工無脳は考える
Google Could Platformでチャットボットを動かそう

アルゴリズムの分解(2)

2018/03/28

1~3の実装

1-3までで数学的な吟味が必要な部分はひと段落しましたので、実装してみたいと思います。 実はtf-idfを計算してくれるライブラリはsklearnにTfidfVectorizer として存在しています。それを使えばよかったのでは?となるわけですが、GAEスタンダード環境にはsklearnをインストールしても動作しません。 それにtfidfもcosθ類似度も単純なアルゴリズムですので、この機会に触っておいてもよいかと思います。

実装1. corpus_to_tfidf

まずはコーパスからtfidf行列を計算する関数をEchoクラスの中に実装します。準備として__init__()でcorpusを読み、リスト内包表記で行が'#'で始まるコメント行を除去します。
class Echo:
    def __init__(self,corpus_path):

        with codecs.open(corpus_path,'r','utf-8') as f:
            self.corpus = f.readlines()
            """" コメント行の除去 """
            self.corpus = [x for x in self.corpus if not x.startswith('#')]

ここから corpus_to_tfidf()の中身です。

コーパスはタブ区切りテキスト形式になっており、line = line.split()を実行するとline = [発言者名,発言内容]というリストが得られます。 このリストのうち1番目の要素だけを使用します。line[1]がそれにあたるのですが、省略して line.split()[1] としています。 得られた発言内容は Segmenter.segment(line) で分かち書きし、["アボカド","の","サラダ","美味しいね","。"]のようなリストにします。 行を無視して単語を一つのリストに全て並べたものがwords、行で分けたものがcorpus_splittedです。

大きなコーパスを一つのリストにしているのですから、wordsには同じ単語が色々な場所に重複して含まれるようになります。 v = Counter(words) は得られたwordsをCounterにキャストし、これにより単語がいくつあるかが集計されます。 v.keys()によってコーパスに現れる全種類の単語のsetが得られます。これをlistにキャストしたものをtfidf行列の列インデックスとして使います。

    Segmenter = TinySegmenter()
    words = []
    corpus_splitted = []
    for line in self.corpus:
        """ corpusの一列目は発言者名。二列目の発言内容のみ処理する """
        line = line.split()[1]
        l = Segmenter.segment(line)
        words.extend(l)
        corpus_splitted.append(l)

    v = Counter(words)

    self.feat = list(v.keys())

ここからはnumpyを使ってwvとtfを計算します。wvには各行に出現する単語の数を単語ごとに集計します。つまり出現回数行列です。 np.zeros()は指定したサイズのゼロ行列を生成します。つづくforループでは先ほど用意したcorpus_splittedを一行ずつ取り出しwvに変換しています。 ここでもCounterを利用しています。

"""
Term Frequency: 各行内での単語の出現頻度
tf(t,d) = (ある単語tの行d内での出現回数)/(行d内の全ての単語の出現回数の和)
"""
import numpy as np

wv = np.zeros((len(self.corpus),len(self.feat)),dtype=np.float32)
tf = np.zeros((len(self.corpus),len(self.feat)),dtype=np.float32)

i=0
for line in corpus_splitted:
    v = Counter(line)
    for word,count in v.items():
        j = self.feat.index(word)
        wv[i,j] = count

    i+=1
tfは前回次のように定義しました。
その実装は以下のようになります。wvは単語の出現回数行列、np.sum(wv,axis=1)はwvを行ごとに集計したもので、集計の結果次元が一つなくなってベクトルになっています。 コードを見ると、元の数式が一行のpythonコードにそのまま対応しているのがわかると思います。これがnumpyを使う利点で、短いコードでも内容的には全ページで眺めたような巨大な行列の 演算をしてくれています。しかもnumpyの内部はC言語やFORTRANベースで記述されており、pythonを使いながらも高速な計算の恩恵を受けられます。
tf = wv / np.sum(wv,axis=1) # tfの定義そのままの記述!

さらにコードの続きを見ていきましょう。次はdfの計算です。 dfの定義は

でした。コードのnp.apply_along_axisは行または列方向に計算を行うという関数で、 計算の中身はnp.count_nonzeroつまり非ゼロの成分の個数を数えています。これをwvについて、列方向(axis=0)に集計します。 さらに計算結果をtf.shape[0]で割っていますが、これは行列tfの行数、つまり全文書数を表します。 以下、idf、tfidfともに定義をそのまま記述しているのがわかると思います。


以上のように、numpyを使ったtfidfの計算はとてもすっきり記述することができます。

"""
Inverse Document Frequency: 各単語が現れる行の数の割合
df(t) = ある単語tが出現する行の数 / 全行数
idf(t) = log(1+1/df(t))
"""
df = np.apply_along_axis(np.count_nonzero,axis=0,arr=wv) / tf.shape[0]
idf = np.log(1+1/df)

tfidf = tf*idf

self.corpus_df = df
self.corpus_tfidf = tfidf

参照:numpy.apply_along_axis (NumPy v1.6)

実装2 speech_to_tfidf

speech_to_tfidf()はユーザ入力文字列をtfidfベクトルに変換する関数です。 処理はこれまで述べてきた内容をほぼなぞるものです。

def speech_to_tfidf(self,speech):
    """
    与えられた文字列speechをcorpusと同じ方法でtfidfベクターに変換する。
    ただしspeechはcorpusに全く現れない単語だけの場合がある。
    この場合tfidfは計算できないため、Noneを返す

    """

    """ 分かち書き """
    speech = Segmenter.segment(speech)


    """ tf """
    wv = np.zeros((len(self.feat)))
    tf = np.zeros((len(self.feat)))

ここで、以下入力文字列の中に self.feat の単語がひとつも見つからなかった場合は None を返しています。

    v = Counter(speech)
    for word,count in v.items():
        if word in self.feat:
            j = self.feat.index(word)
            """
            self.featに含まれない言葉がユーザ発言に含まれる場合、
            現状無視しているが、相手に聞き返すなどの対処がほしい
            """
            wv[j] = count

    nd = np.sum(wv)
    if nd == 0:
        """
        corpusと何も一致しない文字列の場合
        Noneを返す
        """
        return None

以降の処理は同じです。self.corpus_dfは corpus_tfidf()で計算したものがありますので、それを使います。


    """ tfidf """

    tf = wv / nd
    df = self.corpus_df/tf.shape[0]
    idf = np.log(1+1/df)
    tfidf = tf*idf

    return tfidf

次回は分解したアルゴリズムの残りを見ていきましょう。

HOME ◀ 3 PREV

人工無脳は考える by 加藤真一 2016-2018