cos類似度と文書分類|実践的自然言語処理入門 #4

#1〜#3まではBoWのような自然言語の行列形式とそれに派生して局所表現と分散表現の話をし、分散表現の例としてWord2vecについて取り扱いました。

#4では実際にベーシックなアルゴリズムを用いて簡単な応用タスクを解いてみようということで、cos類似度と文書分類について取り扱えればと思います。
以下目次になります。

1. R&Dと自然言語処理の応用タスクについて
2. 文書分類タスクとcos類似度
3. cos類似度を用いた文書分類の実装
4. まとめ


1. R&Dと自然言語処理の応用タスクについて

話をジェネラルなところから落としてきた方が良いと思うので、R&Dと応用タスクの考え方的な話について先に細くしたいと思います。

#3でも勧めましたが上記の「深層学習による自然言語処理」の1章の内容が興味深いので機会があったら読んでみていただければと思います。

以下簡単に内容を抜粋します。
研究やR&Dの文脈においては研究者内の問題意識を揃えたりデータ整備などの基盤を整備するために、応用タスクというものを設定してテーマの統一などが行われることについて言及されていて、自然言語処理の応用タスクについて整理してくれています。具体的には「文書分類」、「機械翻訳」、「文書要約」、「質問応答」、「対話」などです。また、本の5章で2016年までの(刊行が2017年の初頭なので)深層学習の言語処理への応用についての情報がまとまっていますが、興味のある方はそちらも面白いので読んでみてください。

上記の応用タスクの一つの「文書分類」が今回取り組むタスクとなります。もちろんいろんなアプローチがあり、どれが良いなどを議論するのは大変なのですが、今回は入門にあたってということなのでcos類似度を用いたシンプルでベーシックなアルゴリズムに基づいて説明したいと思います。
具体的なタスクの内容とcos類似度を用いた解法については2節で、実際にPythonでの実装を3節で説明できればと思います。


2. 文書分類タスクとcos類似度

1節では一般的な概略について触れましたが、2節では実際に文書分類タスクにフォーカスを当て、cos類似度を用いたシンプルな解法についてまとめたいと思います。

◆ 文書分類タスクと解くにあたってのアプローチ
文書分類タスクは文書を入力して、どのカテゴリに属するかを分類するタスクです。解説のシンプル化のために、#1で取り扱ったBoWまでの構築は前提として進めていきます。

BoWと形態素解析|実践的自然言語処理入門 #1 - lib-arts’s diary
文書をBoWにした際に次に考えるべきは何らかの教師あり学習的なアプローチを使おうということです。今回はcos類似度なので、類似度(距離と考え方は同様だが、cos類似度は距離の公理を満たさないので類似度と表記)をベースにした教師あり学習を行っていきます。

距離の尺度を用いた最近傍法とクラスタリング|はじめてのパターン認識5章,10章 #3 - lib-arts’s diary
上記のkNN的な考え方もしくは、カテゴリ内で平均を取るやり方のどっちでも構いませんが、カテゴリとの類似度さえ計算できれば文書分類を行うことができます。(kNN的なアプローチでは推論に時間がかかるので、今回のケースでは平均を取る形で進めて行こうと思います。)

◆ cos類似度を使用した文書分類
上記で大枠はつかめたと思うのですが、次に類似度としてcos類似度を用いるということについて取り扱って行きます。英語版のWikipediaに"Cosine similarity"の記事がありましたのでこちらを元に解説します。

Cosine similarity - Wikipedia

f:id:lib-arts:20190126162721p:plain
上図は参照記事からcos類似度の定義を引っ張ってきたものです。AとBのベクトルの類似度を出すために内積を長さで割ることでcosθについて計算します。cosはベクトルが似た方向を向いていれば大きな値を出力するので、AとBにそれぞれ文書のベクトルを与えた際に二つの文書の類似度を計算することができます。
したがって、それぞれのカテゴリの文書の代表(平均でも合計でも長さで割るので変わらない)のベクトルとの類似度を計算することで文書分類を行うことができます。

上記が実際に問題を解いていく上でベースのアイデアの流れになります。3節では実際にこちらを実装していきたいと思います。

 

3. cos類似度を用いた文書分類の実装

3節では2節で解説した内容を元に実装についてまとめます。
まずは学習用のデータを用いてカテゴリ単位でのBoWを作成しましょう。

# モジュールインポート
from gensim import corpora, matutils
from janome.tokenizer import Tokenizer
import numpy as np
import scipy.sparse

# ストップワードの設定、Janomeインスタンスの生成
stop_words = ["平和", "人間"] #全文書に出てくる特徴的でない単語はストップワードとして除く
tokenizer = Tokenizer()

# Janomeでの処理スクリプト(名詞のみ、ストップワードは除く、2文字以上の単語、数字は使用しない)
def token_generator(text):
    for text_line in text.split('\n'):
        for token in tokenizer.tokenize(text_line):
            if token.part_of_speech.split(',')[0] == '名詞' and token.surface not in stop_words:
                if len(token.surface) > 1 and not(token.surface.isdigit()):
                    yield token.surface

# データの読み込み&形態素解析
text_processed = [ ]
for i in range(ファイル数): # ファイルは1からの連番で用意している前提で記述しています
    file_path = "./data/data_file"+str(i+1)+".txt"
    with open(file_path) as f:
        txt = f.read()
            text_processed.append(list(token_generator(txt)))

# 辞書の作成と保存
dictionary = corpora.Dictionary(text_processed)
dictionary.save('./dictionary.dict')

#BoW matrixの作成&保存
corpus = [dictionary.doc2bow(doc) for doc in text_processed]
doc_matrix = matutils.corpus2csc(corpus).transpose()
scipy.sparse.save_npz('./category_matrix.npz', doc_matrix)

中身の確認
print(doc_matrix.shape)
print(type(doc_matrix))
print(dictionary.token2id)

上記を実行すれば、BoWが作成されていることが確認できていると思います。類似度や距離を用いた文書分類は各カテゴリ毎のBoWさえ手に入れば推論が可能なので、ここまでで準備が整ったことになります。

以下が推論にあたってのコードとなります。ファイル分けることも多いので、一応別ファイルでも動くような構成としています。

# モジュールインポート
from gensim import corpora, matutils
from janome.tokenizer import Tokenizer
import numpy as np
import scipy.sparse

# ストップワードの設定、Janomeインスタンスの生成
stop_words = ["平和", "人間"] #全文書に出てくる特徴的でない単語はストップワードとして除く
tokenizer = Tokenizer()

# Janomeでの処理スクリプト(名詞のみ、ストップワードは除く、2文字以上の単語、数字は使用しない)
def token_generator(text):
    for text_line in text.split('\n'):
        for token in tokenizer.tokenize(text_line):
            if token.part_of_speech.split(',')[0] == '名詞' and token.surface not in stop_words:
                if len(token.surface) > 1 and not(token.surface.isdigit()):
                    yield token.surface

# 推論ファイルの読み込み&形態素解析
txt_inference = [ ]
file_path = "推論にあたって用いるファイルのパス"
with open(file_path, "r") as f:
    txt = f.read()
    txt_inference.append(list(token_generator(txt)))

# 辞書のロード&BoW生成
dictionary = corpora.Dictionary.load('./dictionary.dict')
corpus = [dictionary.doc2bow(doc) for doc in txt_inference]
inf_matrix = matutils.corpus2csc(corpus, num_terms=len(dictionary)).transpose()

# カテゴリ情報BoWの読み込み&連結
category_matrix = scipy.sparse.load_npz('./category_matrix.npz')
target_mat = scipy.sparse.vstack([category_matrix, inf_matrix])

# cos類似度の計算、出力
cos_sim = np.zeros([target_mat.shape[0]])
var_SDGs = target_mat.dot(target_mat.transpose()).toarray()
for i in range(target_mat.shape[0]):
    cos_sim[i] = var_SDGs[i,-1]/(np.sqrt(var_SDGs[i, i])*np.sqrt(var_SDGs[-1, -1])) #-1を利用することで後ろから要素を取り出せる
print(cos_sim)

ポイントはNumPy形式ではなくscipy.sparseの形式を用いていることです。そのため、インスタンスの形式には注意せねばなりません。(実装については若干編集ミスや環境によって動かない可能性があるので、動かないなどありましたら教えてください。初心者への指導の際に使用する予定なので、ミスやわかりにくい点などは随時修正したいと思います。)

 

4. まとめ
#4ではcos類似度を用いた文書分類について取り扱いました。
#5では極性辞書を用いたネガポジ分析について取り扱えればと思います。

 

↓以降の記事リストは下記です。

言語処理におけるグラフ理論とネットワーク分析|実践的自然言語処理入門 #6 - lib-arts’s diary