TF-IDFを求めてWordCloudをつくるプログラム(1)

テキストマイニング

自然光の中でコーディング

こんにちは。前回は特徴語をどのようにビジュアル表現するかについてまとめました。今回はTF-IDFの値を計算して、Word Cloudをつくるプログラムを紹介します。言語はPythonです。

プログラムをつくる上での制約

ここで、プログラムをつくるにあたり結構面倒な制約を付けました。関数、クラスといった便利なものを使わないという制約です。

その理由は、前段として行っているプログラミングの授業と関係しています。授業では生徒がプログラミングを経験してもらう際に、初心者向け学習サイトのProgateを使いました。中学校・高校向けのプランでは2つのコースまで選択できます。勤務校の授業ではPythonのコースを利用し、Python IIまでを全員が経験する課題としました。

ここまでに出てくる文法事項は、

  • 変数
  • 数値と文字列
  • if、elif、else
  • ブール演算 and、or、not
  • リスト
  • 辞書
  • リストと辞書に対するfor
  • while
  • breakとcontinue

以上です。詳しい内容はProgateのサイトを参照してください。

とはいえ、形態素解析などのモジュール呼び出しやら、メソッドの呼び出しやらは、しれっと混ぜ込んでいます。これだけの文法事項で、次の内容として機械学習に挑んでいくという、なかなかの野望を持っています。

もう1つの制約は、scikit-learnなどのモジュールは使わず、「高校でTF-IDF(1)(2)」で紹介したとおりのアルゴリズムをプログラムとして記述することです。実用性を考えたらモジュールを使った方が良いと思います。しかし、授業で手計算でTF-IDFを求めているのだから、単語の数に基づいて求めるアルゴリズムがプログラムに表出させることを優先しました。それにより、該当行だけでもプログラムとの対応を確認できるようにしました。

整理すると次の2点が制約です。

  • 文法事項は、基本的にプログラミング学習サイトProgateのコースPython IとPython IIの範囲内
  • 手計算と同じアルゴリズムをプログラム中に記述する

授業で配布したプログラム

上に挙げた制約のため、プログラムは汚いのですが、生徒に配布したプログラムを紹介します。(実はプログラムが汚い理由がもう1つあります。はじめはナイーブベイズ分類器も同じプログラムに入れていました。実行速度の問題があり、TF-IDFとWord Cloud、ナイーブベイズ分類器に分けました。そのため、ナイーブベイズ分類器用の変数が残ってしまいプログラムが汚くなっています。)

プログラム中で、標準ライブラリでないライブラリとしてjanome、wordcloud、openpyxlを使用します。インストールされていない場合には、pipコマンドでインストールしてください。また、フォントに”KozGoPro-Light.otf”を指定しています。このフォントがなければ、代替となるフォントをotf形式またはttf形式のファイルを指定する必要があります。

# --------------- 【ファイルを読み込む】 ---------------

# files は 'カテゴリー' : 'ファイル名' の組合せによる辞書
files = { 'いちご' : 'ichigo.txt' , 'りんご' : 'ringo.txt' , 'キウイ' : 'kiwi.txt' }

# documents は空の辞書。 ファイルを開き、'カテゴリー' : '文書'の組合せでの辞書を作る
documents = { }

# 辞書 files からファイル名を取り出し、ファイルを読み込んで documents に蓄積する
for category in files:

    # filename に辞書からファイル名(ichigo.txtなど)を取り出す
    filename = files[category]

    # READモードでファイルを開く
    file = open( filename , 'r' )
    # ファイルから文章を読み込んで、辞書として documents に追加する
    text = file.read()
    documents[category] = text
    # ファイルを閉じる
    file.close()

# 画面表示 : documents から一つずつカテゴリーを読み込んで、出力する
for category in documents:
    print('【' + category + '】')
    print( documents[category] )


# --------------- 【形態素解析をして、語の出現回数を求める】 ---------------

word_list = { } # 形態素(語)の出現回数を記録
tf = { } # tf の値を記録
word_documents = { }            # 語がいくつの文章に含まれているかを数えるための変数
count_documents = { }           # ナイーブベイズ用
count_all = 0 # 文章の数を数える(正確には行数を数える)


# janomeを組み込む
from janome.tokenizer import Tokenizer
t = Tokenizer() # 変数 t を使って形態素解析をする


# 分類ごとに 形態素解析して、tf を求める
for category in documents:
    document = documents[category] # 分類ごとに文章を document に入れる
    word_list[category] = { } # 分類に含まれる語を入れる辞書を用意する
    word_count = 0 # 全形態素数を数えるために初期化する
    count_documents[category] = 0               # 分類ごとの文章数を数える

    lines = document.split( '\n' ) # 文章を行ごとに分ける

    for line in lines: # 1行ごとに以下の処理を繰り返す
        tokens = t.tokenize( line ) # 1行分の文章を形態素解析する
        count_all += 1 # 文章数を 1 増やす
        count_documents[category] += 1          # ナイーブベイズ用 
        words_line = [ ]                        # すでに文章内に同じ語が出現したかどうかのチェック用の変数

        for token in tokens: # 形態素ごとに以下の処理を繰り返す
            word = token.base_form # word に、形態素(語)の原形を代入する

            if word == '*': # 原形が *(アスタリスク) のとき
                word = token.surface         # word は、語の表層形(元の形)にする
                
            # 品詞が「名詞」「動詞」「形容詞」でない場合は、 tf を求めない 
            partOfSpeech = token.part_of_speech.split( ',' )[0]
            if partOfSpeech != '名詞'  and  partOfSpeech != '動詞'  and  partOfSpeech != '形容詞':
                continue

            # 初めて出現した語の場合、新しい語を word_list に登録する
            if not word in word_list[category]:
                word_list[category][word] = 0

            # 語が行内に既に出現していない場合は初期化
            if not word in word_documents:
                word_documents[word] = 0

            # 語が行内に出現していない場合
            if not word in words_line:
                words_line.append(word)         # 行内の出現リストに語を加える(出現チェック用に追加)
                word_documents[word] += 1       # 出現文章数を 1 増やす(idf計算用)
 
            word_list[category][word] += 1 # 分類内での語の出現回数を 1 増やす
            word_count += 1 # 分類内の全語数を 1 増やす

    # tf に値を入れられるよう初期化する
    tf[category] = { }

    # 語ごとに 出現回数/全形態素数 を求める
    for word in word_list[category]:
        tf[category][word] = word_list[category][word] / word_count

# --------------- 【分類ごとにワードクラウドの画像を作る】 ---------------

# wordcloudを組み込む
from wordcloud import WordCloud

# 分類ごとにワードクラウドを作成する
for category in documents:

    # フォントを指定する
    fpath = 'c:\windows\Fonts\KozGoPro-Light.otf'

    # ワードクラウドで描画しない語のリスト
    stop_words = [ 'てる' , 'いる' , 'なる' , 'れる' , 'する' , 'ある' ,
                   'こと' , 'これ' , 'さん' , 'して' , 'くれる' , 'やる' ,
                   'くださる' , 'そう' , 'せる' , 'した' , 'できる' ,
                   'それ' , 'ここ' ,
                   'ちゃん' , 'くん' , '' ,
                   'て' , 'に' , 'を' , 'は' , 'の' , 'が' , 'と' ,
                   'た' , 'し' , 'で' , 'ない' , 'も' , 'な' , 'い' , 'か' ,
                   'ので' , 'よう' ,'' ]

    # ワードクラウドを指定する
    wordcloud = WordCloud(background_color="white" ,
                          font_path=fpath ,
                          width=1024 , height=674 , ranks_only = True ,
                          stopwords=set(stop_words)).generate( ','.join(word_list[category]) ) 

    # 画像ファイル(png)として、ファイルに出力する
    wordcloud.to_file( './' + category + '.png' )


# --------------- 【idf を求める】 ---------------

# 対数(log)を使えるようにする
import math

idf = { }       # idf を記録する変数を初期化

# idf を求める(log(全文書数/語を含む文書数) )
for word in word_documents:
    idf[word] = math.log( count_all / word_documents[word] ,10 )


# --------------- 【tf-idf を求める】 ---------------

tfidf = { }     # tf-idf を代入する変数を初期化

# カテゴリーごとにtf-idfを計算する
for category in word_list:
    
    tfidf[category] = { }   # カテゴリごとに初期化

    # tf-idf を計算( tf-idf = tf * idf )
    for word in word_list[category]:
        tfidf[category][word] = tf[category][word] * idf[word]


# --------------- 【tf-idf をエクセルに出力する】 ---------------

from openpyxl import Workbook

filename = "tf-idf.xlsx"
book = Workbook()

# カテゴリーごとにtf-idfをファイルに出力する
for category in word_list:

    sheet = book.create_sheet(category)
    sheet['A1'] = '形態素'
    sheet['B1'] = 'tf'
    sheet['C1'] = 'idf'
    sheet['D1'] = 'tf-idf'
    
    # tf-idf が大きい順に並び替えて出力する
    keys = sorted( tfidf[category].items() , key = lambda x:x[1] , reverse = True )
    for i , word in enumerate(keys):
        sheet.cell(row=i+2,column=1).value = word[0]
        sheet.cell(row=i+2,column=2).value = tf[category][word[0]]
        sheet.cell(row=i+2,column=3).value = idf[word[0]]
        sheet.cell(row=i+2,column=4).value = tfidf[category][word[0]]

book.save('tf-idf.xlsx')

プログラムが長いので、次から数回に分けて解説します。本文は短いですが今回はこれでおしまいにします。それではまた。

Posted by 春日井 優