もっと詳しく

前回はワードクラウドについて紹介しましたが、今回は Python で TF-IDFによる文書の特徴抽出について紹介したいと思います。

今回もクラス化しているので、コピペですぐにお試しいただけます。

とりあえず使ってみたいという方は、是非この記事をご一読ください。

TF-IDFの概要

IF-IDFは、Term Frequency – Inverse Document Frequency の頭文字を取った名称です。

簡単にいうと、TFは「単語の登場頻度」、IDFは「文書間における単語のレア度」のことで、この2つを掛け合わせた値を意味します。

一般的に登場頻度が多い単語は重要であると考えられます。

しかし、複数の文書を比べた結果、その単語がどの文章にも登場するのであれば、それは一般的な単語であり、文書ごとの特徴を表す単語としては使えない=希少価値が少ない単語です。

もう少し具体的に見ていきましょう。

たとえば、A高校のサッカー部に所属するA君の日記と、B高校のバレー部に所属するBさんの日記を比べてみましょう。

A君、Bさん共に「授業」とか「先生」という単語が頻繁に登場することになるため、これらの単語は2つの日記の特徴を表していない為、価値が低いと考えられます。

一方、バレーに関する単語はAさんの日記にしかなく、サッカーに関する単語はBさんの日誌にしか登場しないので、これらは2つの日誌を比較した場合、レアな単語=日記を特徴付けている単語だと考えられます。

TF-IDFはこのような評価を数値で表すもので、文書の類似性を見たり、その文書の特徴を見出すときに使います。



インストール方法

Pythonで IF-TDF を行う場合、scikit-learn の他、形態素解析ライブラリ(MeCab、Janomeなど)が必要になります。

今回は、Janomeを使いますので、インストールは以下の様になります。

# python公式サイトからPythonをインストールした方
pip install scikit-learn
pip install janome

# anaconda で Pythonをインストールした方は
# 既に scikit-learn がインストールされているので、
# janome のみインストールして下さい。
pip install janome

# miniconda で Pythonをインストールした方
conda install scikit-learn
pip install janome

IF-IDF の実行方法

以下の通り、必要なモジュールをimport します。

from sklearn.feature_extraction.text import TfidfVectorizer
from janome.tokenizer import Tokenizer

TF-IDFを求める手順は次の様になります。

TF-IDFを計算するためのインプットは、形態素解析によって分かち書きされた文書になります。

リスト形式で複数の文書をfit_transform() メソッドに渡すことで、TF-IDF値が計算されます。

以下はそのサンプルプログラムです。

#TF-IDFを求めたい文書のリスト(実際には分かち書き文書又は単語の羅列)
docs = [
         '今日 学校 体育 授業 バレー トス レシーブ 授業 先生 クラブ バレー 放課後',
         '今日 放課後 クラブ 活動 サッカー 試合 サッカー 合宿 ゴール キーパー'
       ]

# モデルの生成
vectorizer = TfidfVectorizer(smooth_idf = False)

# TF-IDFの計算
values = vectorizer.fit_transform(docs).toarray()

# 特徴量ラベルの取得
words = vectorizer.get_feature_names()


#結果のプリント
print(values)
print(words)

結果は次の様になります。

words には、指定された全ての文書から抽出された単語が、重複無しで格納されています。

そして、values には、文書毎の単語に対するTF-IDF値が格納されています。

words と values の要素は1対1で対応しているため、文書ごとに対で取り出してTF-IDF値の大きい順に並べ替えれば、各文書ごとの特徴が理解し易くなります。

下記はそのサンプルです。

一旦 DataFrame に入れて縦横変換を行ってから並べ替えを行っています。

df = pd.DataFrame(values, columns = words)
print(df.T.sort_values(by=0,ascending=False)[0])
print('---------------')
print(df.T.sort_values(by=1,ascending=False)[1])

TF-IDF クラスについて

では、さっそく自作したTF-IDFクラスの概要、リファレンス、ソースコードの順に紹介していきたいと思います。

クラスの概要

TF-IDFは複数の文書を使った分析になるので、add_file 又は add_text メソッドで文書を追加していきます。

追加し終えたら、create メソッドでTF-IDFを計算します。

このクラスは計算結果を取得するためのメソッドが2つあり、1つは 文書毎のTF-IDFの値を取得する top_words、もう1つは文書毎の単語の出現頻度を取得する word_count です。

リファレンス

クラス名は TfIdf で、メソッドは次の8つがあります。

機能 メソッド仕様 戻り値
コンストラクタ __init__()
ファイルの追加 add_file(
title, #タイトル
filename, #入力ファイル名
 encoding=’utf-8′ #エンコード名
)
なし
テキストの追加 add_text(
title, #タイトル
document #入力文書
)
なし
TF-IDFの計算結果を
DataFrameに格納
create() DataFrame
分かち書きへの変換
(形態素解析)
word_separation(
text #文書
)
‘aaa bbb ccc ・・・’
TF-IDFのモデル作成 create_model(
separating_docs #複数の文書
)
[aaa,bbb,ccc,・・・],
[
[0.272,0,632,・・・],
[3.2533,0,235,・・・]
]
文書毎のTF-IDF値の取得 top_words(
cnt = 10 #上位n個の指定
)
{
‘aaa’ : dataframe
,’bbb’ : dataframe
}
文書毎の単語登場頻度の取得 word_count(
cnt = 10 #上位n個の指定
)
{
‘aaa’ : dataframe
,’bbb’ : dataframe
}

createメソッドの戻り値

create メソッドの戻り値 は以下の通りで、column に抽出された単語がセットされ、1行1文書として、文書ごとの単語のTF-IDF値を格納したDataFrameを返します。

top_words の戻り値

top_words メソッドの戻り値は以下の通りで、上記のDataFrameから1行づつ取り出し、TF-IDFの値で並べ替えた結果を辞書に格納して返します。

word_count の戻り値

word_count メソッドの戻り値は top_words と同じで、TF-IDF値の代わりに出現頻度が格納されています。

使い方

使い方は非常に簡単で、次の順番にメソッドを呼ぶだけです。

tf = TfIdf()
tf.add_file('スタートレック','p:/target1.txt')
tf.add_file('ギャラクティカ','p:/target2.txt')
tf.add_file('スターゲート・アトランティス','p:/target3.txt')
tf.create()
print(tf.top_words())
print(tf.word_count())

検証で使ったPCはWindows10マシンなので、サンプルソースはPドライブ直下に保存した startreck.txt というファイルを読み込むようになっています。

また、ファイルの中身は以下の通りです。

target1.txt

スタートレックは、22世紀から24世紀の話である。
設定では、2026年から2053年にかけて発生した第三次世界大戦により、6億人の犠牲者と文明の崩壊が起きている。
2063年にゼフラム・コクレーンがワープエンジンを開発、試験飛行中にバルカン時に発見され、ファーストコンタクトを果たした。
2151年には初代エンタープライズ号「NX-01」が竣工し、クリンゴンとのファーストコンタクトを果たしている。
2153年にロミュラン人との戦争が勃発、2161年に地球連合、バルカン、アンドリアが中心になり惑星連邦が設立された。
地球人は銀河系内の約4分の1の領域に進出し、様々な異星人との交流を行っている。
既に貧困や戦争などは根絶されており、見た目や無知から来る偏見、差別も存在しない、理想的な世界となっている。
レプリケーターの登場により貨幣経済は無くなり、人間は富や欲望ではなく人間性の向上を目指して働いている。
しかし、個人財産は存在しており、ワイナリーや宇宙船を所有する一部の恵まれた人々が存在する。

target2.txt

GALACTICAは2005年1月から2009年3月にかけてアメリカのSF専門チャンネル「Sci-Fi」で放送されたSFテレビドラマである。
2003年12月に2話(計4時間)のパイロット版が放映され、高評価を受けたことでTVシリーズが制作された。
12の惑星に植民地を持つ人類(地球人ではない)と機械生命体であるサイロンとの戦争が勃発。
奇跡的に生き残った人類は宇宙空母GALACTICAと民間宇宙船と共に220隻の艦隊を組み、敵の追撃を交わしながら伝説の惑星「地球」を目指すというストーリである。
1978年にTVで放送された「宇宙空母ギャラクティカ」のリメイク版であるため、基本的な設定は踏襲されているものの、人間と見分けが付かない人型サイロンの登場や、サイロンの内部分裂、人類に味方するサイロンとの協力など、より深い内容に仕上がっている。
このほか、スピンオフ作品として、「The Plan/神の誤算」、「RAZOR/ペガサスの黙示録」、「BLOOD&CHROME/最高機密指令」が制作された。

target3.txt

スターゲイト・アトランティスは、1994年のSF映画「スターゲイト」を受けてTVドラマ化された「スターゲイト SG-1」のスピンオフ作品である。
2004年から2008年にかけて5シーズン100話が制作された。
スターゲイトは、宇宙四大種族の中の一種族であるエンシェントが作り出した星間移動装置(通称スターゲイト)がエジプトで発見されたことをきっかけに、別の惑星に連れ去られた古代地球人の末裔の存在を知り、支配者であるラーからの解放を勝ち取るというストーリであった。
スターゲイト SG-1は、スターゲイトの1年後が舞台となっており、スターゲイトを使った惑星間移動を通して、侵略者との戦い、惑星探査、異種族との交流を繰り広げるといったストーリーとなっている。
そして、スターゲイト・アトランティスは、太古に海中に水没したとされるアトランティス大陸が、実は別銀河にある都市型の宇宙船だったという設定をベースに、そこに送り込まれた隊員たちの活躍が描かれた作品である。

このクラスを使った実行結果は以下の様になりました。

まずはTF-IDF の結果(ベスト10)です。

ギャラクティカの結果を見ると「サイロン」というキーワードを「サイロ」としても解釈しているようで、形態素解析にユーザー辞書を使うなどの改善が必要ですね。

スタートレック ギャラクティカ スターゲート・アトランティス
バルカン 0.272358
世紀 0.272358
世界 0.272358
ファースト 0.272358
コンタクト 0.272358
人間 0.182401
文明 0.136179
犠牲 0.136179
無知 0.136179
大戦 0.136179
人類 0.376665
galactica 0.251110
サイロン 0.251110
空母 0.251110
サイロ 0.251110
宇宙 0.168171
sf 0.168171
tv 0.168171
基本 0.125555
パイロット 0.125555
スターゲイト 0.808280
アトランティス 0.269427
種族 0.179618
sg 0.179618
惑星 0.128383
作品 0.120292
ストーリー 0.089809
エジプト 0.089809
シーズン 0.089809
異種 0.089809

次に、単語出現頻度(ベスト10)の結果です。

スタートレック ギャラクティカ スターゲート・アトランティス
世紀 2
世界 2
バルカン 2
ファースト 2
コンタクト 2
人間 2
レック 1
大戦 1
犠牲 1
文明 1
人類 3
GALACTICA 2
月 2
SF 2
TV 2
惑星 2
サイロ 2
宇宙 2
空母 2
サイロン 2
スターゲイト 7
惑星 3
スターゲイト・アトランティス 2
SG 2
作品 2
種族 2
SF 1
映画 1
TV 1
ドラマ 1

ソースコード

最後に、クラスの全ソースコードを紹介しておきます。

from sklearn.feature_extraction.text import TfidfVectorizer
from janome.tokenizer import Tokenizer
import re
import pandas as pd
import codecs
from collections import Counter

class TfIdf:
    def __init__(self):
        """
        コンストラクタ
        """

        # 分かち書きの文書を保持する変数
        self.separation_docs = {}
        self.df = None

    def create(self):
        """
        TF-IDFの実行
        """

        #TF-IDFの実行
        names,values = self.create_model(self.separation_docs.values())
        #結果をDataFrameに格納
        self.df = pd.DataFrame(values, columns = names,index = self.separation_docs.keys())

        return self.df
        

    def word_separation(self,text):
        """
        形態素解析により一般名詞と固有名詞のリストを作成

        ---------------
        Parameters:
            text : str         テキスト
        """
        token = Tokenizer().tokenize(text)
        words = []
    
        for line in token:
            tkn = re.split('\t|,', str(line))
            # 名詞のみ対象
            if tkn[1] in ['名詞'] and tkn[2] in ['一般', '固有名詞']:
                words.append(tkn[0])
    
        return ' ' . join(words)

    def create_model(self,separating_docs):
        """
        TF-IDFの計算を行う

        ---------------
        Parameters:
            documents : [str]  分かち書きされた文書のリスト
        """      
        # モデルの生成
        vectorizer = TfidfVectorizer(smooth_idf = False)
        # TF-IDF行列の計算
        values = vectorizer.fit_transform(separating_docs).toarray()
        # 特徴量ラベルの取得
        feature_names = vectorizer.get_feature_names()

        return feature_names,values

    def add_file(self,title,filename,encoding='utf-8'):
        '''
        ファイルの読み込み

        Parameters:
        --------
            filename : str   TF-IDFしたい文書が書かれたファイル名 
        '''
        with codecs.open(filename,'r',encoding,'ignore') as f:
            self.add_document(title,f.read())

    def add_document(self,title,document):
        '''
        テキストの読み込み

        Parameters:
        --------
            document : str   TF-IDFしたい文書
        '''
        # 形態素解析で分かち書きした文書をインスタンス変数に格納
        self.separation_docs[title] = self.word_separation(document)

    def top_words(self,cnt = 10):
        '''
        TF-IDF 上位nの取得

        Parameters:
        --------
            cnt : int   上位からの取得件数
        '''
        res = {}
        for n,title in enumerate(self.separation_docs):
            res[title] = (self.df[n:n+1].T.sort_values(by=title, ascending=False).head(cnt)).rename(columns={title:'TFIDF'})  
        return res

    def word_count(self,cnt = 10):
        '''
        出現回数上位nの取得

        Parameters:
        --------
            cnt : int   上位からの取得件数
        '''
        res = {}
        for key,value in self.separation_docs.items():
            data = Counter(value.split(' ')).most_common(cnt)
            res[key] = pd.DataFrame([v for k,v in data],columns=['Count'],index=[k for k,v in data])
        return res



まとめ

今回は機械学習ライブラリ sklearn に含まれている TF-IDFの関数 TfidfVectorizer と、形態素解析器に janome を組み合わせて、複数文書のTF-IDF、並びに単語の出現頻度を計算する方法を紹介しました。

今回は単純に文書から名詞だけを抜き出し、辞書も標準のものを使いましたが、辞書に neologd を使ったり、一部動詞を含めたりすると、また違った結果が得られると思います。

とりあえずTF-IDFを手軽に試してみたい方は、ソースコードをコピペしてお試しください。

この記事が皆様のお役に立てば幸いです。