チケットの見える化にチャレンジしてみる(2-6: 準備編 tf-idfの実装)
こんばんは!株式会社スマレジ 、開発部のmasaです。
まだまだ残暑厳しいですが、少しずつ涼しくなってきたことを感じます。 会社でも、暑さが和らいできたので、アウトドアイベントをしたいなーといった話が出るのですが、 緊急事態宣言もあるので、なかなか実現できない今日この頃です。。。
皆さんの会社では、コロナ禍のなかでどんなイベントをしていますか?
今回は、if-idfのpython + chasenでの実装になります。
tf-idfの実装
まずは実装を。
from janome.tokenizer import Tokenizer from janome.analyzer import Analyzer from janome.charfilter import * from janome.tokenfilter import * import json import requests import math def getDescriptionOfRedmineIssueByIssueId(id): """ redmineのチケットをID指定で取得して、説明を返す。 :param id: int :return: Bool|string 取得成功なら文字列、失敗ならFalse """ headers = {'X-Redmine-API-Key': "redmineのキー"} response = requests.get("http://redmineのドメイン/issues/" + str(id) + ".json", headers=headers) statusCode = response.status_code if statusCode >= 400: return "" text = response.text if len(text) < 1: return "" resJson = json.loads(text) if "issue" not in resJson: return "" issue = resJson["issue"] sentence = issue["description"] if sentence is None: sentence = "" return sentence # 辞書定義 char_filters = [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter('<("[^"]*"|\'[^\']*\'|[^\'">])*>', u'')] tokenizer = Tokenizer() token_filters = [CompoundNounFilter(), POSKeepFilter(['名詞']), LowerCaseFilter(), TokenCountFilter()] analyzeInfo = Analyzer(char_filters=char_filters, tokenizer=tokenizer, token_filters=token_filters) sentences = [] # redmineチケットの説明を取得(ID指定) for i in 取得したいチケットのIDを持った配列: if i == 0: continue sentences.append(getDescriptionOfRedmineIssueByIssueId(i)) # ベクトル化 vectors = [] wkVector = [] for wkSentence in sentences: if len(wkSentence) < 1: vectors.append([]) continue wkVector = [] for result in analyzeInfo.analyze(wkSentence): wkVector.append(result[0]) vectors.append(wkVector) # TF及びIDFの計算 tfScores = [] idfScore = 0 targetWord = "tf-idfで対象にしたい単語" for tmpVector in vectors: wkTfScore = 0 if targetWord in tmpVector: idfScore += 1 for currentWord in tmpVector: if targetWord == currentWord: wkTfScore += 1 if len(tmpVector) < 1: tfScores.append(0) else: tfScores.append(wkTfScore / len(tmpVector)) print(tfScores) print(math.log(len(vectors) / idfScore))
getDescriptionOfRedmineIssueByIssueId
は前回の使い回しなので割愛します。
詳しくは↓
TFの計算
TFの定義は前回のブログ
こちらで記載しているように、ある文書に対するその単語の出現回数を全文書で割ったもの
になります。
この部分に対応しているのが下記の部分になります。(上のソースからIDF関連の部分を除去したものになります)
# TFの計算 tfScores = [] idfScore = 0 targetWord = "tf-idfで対象にしたい単語" for tmpVector in vectors: wkTfScore = 0 for currentWord in tmpVector: if targetWord == currentWord: wkTfScore += 1 if len(tmpVector) < 1: tfScores.append(0) else: tfScores.append(wkTfScore / len(tmpVector))
vectors
には各チケットの単語リストが入っています。(今回は70チケットあり、キーがチケットIDで値が単語リスト)
そして、各単語リストについて、検索対象の単語(targetWord
)でマッチングをかけて、マッチした回数をwkTfScore
で記憶、
そして、その文書の全単語数で割っています。
IDFの計算
IDFの計算はもっとシンプルになります。 IDFは比較したい文書の集合(今回でいう70個のチケット)のなかでターゲットの単語が出現した数で全文書数(今回は70)を割ったものに対数をとってあげることになります。
# TF及IDFの計算 idfScore = 0 targetWord = "tf-idfで対象にしたい単語" for tmpVector in vectors: wkTfScore = 0 if targetWord in tmpVector: idfScore += 1
print(math.log(len(vectors) / idfScore))
ループの中では単語の出現した文書の数をカウントしておき、その結果を全文書数(len(vectors))
)で割って対数をとっているのがわかると思います。
次回は、この二つのスコアをベースに使用するBM25について説明します。