まとめ
sklearn.feature_extraction.text.TfidfVectorizer で TF-IDF 値を出すとデフォルトでは[その文書内でのその単語の出現回数]×[log( (1 + 全文書数) / (1 + その単語が出現する文書数) ) + 1]を文書ごとに L2 正規化したものである。
参考文献
- tf-idf - Wikipedia(2022年6月5日参照).
- sklearn.feature_extraction.text.TfidfVectorizer — scikit-learn 1.1.1 documentation(2022年6月5日参照).
- 6.2. Feature extraction — scikit-learn 1.1.1 documentation(2022年6月5日参照).
参考文献 [1] によると TF-IDF とは、文書集合内の各文書内の各単語に定義される数値であって、その文書内でその単語が占める割合(TF)と、全文書のうちその単語が出現する文書が占める割合の逆数の対数(IDF)をかけたものである。ただしこれは参考文献 [1] の表で「標準的な」とされている定義であり、TF にも IDF にも亜種は多い。ともかく標準的な TF-IDF の定義を素朴に計算してみる。適当な文書集合が思いつかないので百人一首を使用する(Wikipedia の「百人一首」の「歌一覧」の表記を採用する)。そうすると以下のようになる。
import numpy as np import pandas as pd pd.set_option('display.unicode.east_asian_width', True) import MeCab # 関係ないが NEologd を使用するとかえって「神のまにまに」などが1形態素になる # tagger = MeCab.Tagger('-u "C:/Program Files/MeCab/dic/ipadic-neologd/NEologd.dic"') tagger = MeCab.Tagger() def to_words(s): # 文章を単語にする関数であれば何でもよい tokens = tagger.parse(s).split('\n') tokens = [t.split('\t') for t in tokens if len(t) > 1] tokens = [t[0] for t in tokens if len(t) > 1] return tokens docs = {} # 文書IDをキーに各文書の情報を格納する辞書 vocab = {} # 単語をキーに各単語の情報を格納する辞書 with open('data.txt') as ifile: # 百人一首を各行に記述したテキストファイル for i, line in enumerate(ifile): s = line.strip() words = to_words(s) n_word = len(words) docs[i + 1] = {'文書': s, '単語列': words, '出現頻度': {}, 'TF': {}} words_unique = list(dict.fromkeys(words)) for word in words_unique: freq = words.count(word) docs[i + 1]['出現頻度'][word] = freq docs[i + 1]['TF'][word] = float(freq) / n_word if word not in vocab: vocab[word] = {'出現文書ID': []} vocab[word]['出現文書ID'].append(i + 1) n_doc = len(docs) for word, d in vocab.items(): doc_freq = len(d['出現文書ID']) vocab[word]['文書頻度'] = doc_freq vocab[word]['IDF'] = np.log(n_doc / doc_freq) for id_ in range(1, 101): docs[id_]['文書頻度'] = {} docs[id_]['IDF'] = {} docs[id_]['TF-IDF'] = {} for word, freq in docs[id_]['TF'].items(): docs[id_]['文書頻度'][word] = vocab[word]['文書頻度'] idf = vocab[word]['IDF'] docs[id_]['IDF'][word] = idf docs[id_]['TF-IDF'][word] = freq * idf print('文書数', n_doc) print('語彙数', len(vocab)) id_ = 79 print(f'\n◆ {id_}首目の情報') print(docs[id_]["単語列"]) print(f'{len(docs[id_]["単語列"])} 単語 ({len(docs[id_]["出現頻度"])} ユニーク単語)') print(pd.DataFrame({k: docs[id_][k].values() for k in ['出現頻度', 'TF', '文書頻度', 'IDF', 'TF-IDF']}, index=docs[id_]['出現頻度'].keys()))
文書数 100 語彙数 633 ◆ 79首目の情報 ['秋風', 'に', '棚引く', '雲', 'の', '絶間', 'より', 'もれ', '出', 'づる', '月', 'の', '影', 'の', 'さやけ', 'さ'] 16 単語 (14 ユニーク単語) 出現頻度 TF 文書頻度 IDF TF-IDF 秋風 1 0.0625 3 3.506558 0.219160 に 1 0.0625 58 0.544727 0.034045 棚引く 1 0.0625 1 4.605170 0.287823 雲 1 0.0625 5 2.995732 0.187233 の 3 0.1875 85 0.162519 0.030472 絶間 1 0.0625 1 4.605170 0.287823 より 1 0.0625 4 3.218876 0.201180 もれ 1 0.0625 1 4.605170 0.287823 出 1 0.0625 10 2.302585 0.143912 づる 1 0.0625 1 4.605170 0.287823 月 1 0.0625 11 2.207275 0.137955 影 1 0.0625 1 4.605170 0.287823 さやけ 1 0.0625 1 4.605170 0.287823 さ 1 0.0625 10 2.302585 0.143912
上では 79 首目の情報のみ表示しているが、この歌は 16 単語からなるので各単語の相対頻度である TF は 0.0625 である。しかし、「の」だけは 3 回登場するので 3 倍になっている。また、「秋風」が登場する歌はこの歌を含めて 3 首あり、「秋風」の IDF は log(100/3)≒3.5 になっている。他方、「棚引く」はこの歌にしか登場しないので IDF は log(100/3)≒4.6 になっている。結果、「秋風」「棚引く」の TF-IDF は 0.219, 0.288 になっている、などがわかる。
scikit-learn で同じ処理をしたい。feature_extraction.text.TfidfVectorizer [2] を使う。文書を単語に分割する関数はさっきと同じものを使う。と、当然語彙数は同じになる。しかし、「秋風」「棚引く」の TF-IDF は 0.274, 0.318 になっており、さっきとはずれている。というか IDF が既にずれている。scikit-learn の IDF の方が大きい。
import warnings from sklearn.feature_extraction.text import TfidfVectorizer warnings.simplefilter('ignore', UserWarning) vectorizer = TfidfVectorizer(tokenizer=to_words) texts = np.array([docs[id_]['文書'] for id_ in range(1, 101)]) vectorizer.fit(texts) tfidf = vectorizer.transform(texts) feature_names = vectorizer.get_feature_names_out() print('文書数', tfidf.shape[0]) print('語彙数', len(feature_names)) id_ = 79 print(f'\n◆ {id_}首目の情報') print('\tIDF\t\t\tTF-IDF') v = tfidf[id_ - 1].todense() for idx in tfidf[id_ - 1].indices: print(f'{feature_names[idx]}\t{vectorizer.idf_[idx]}\t{v[0, idx]}')
文書数 100 語彙数 633 ◆ 79首目の情報 IDF TF-IDF 雲 3.8233610476132043 0.24736881778628844 絶間 4.921973336281314 0.3184482737071388 秋風 4.228826155721369 0.2736021300990034 棚引く 4.921973336281314 0.3184482737071388 月 3.130213867053259 0.20252267417815306 影 4.921973336281314 0.3184482737071388 出 3.217225244042889 0.20815225014334174 より 4.005682604407159 0.25916489652419133 もれ 4.921973336281314 0.3184482737071388 の 1.160773220587752 0.2253036757860151 に 1.53758307293554 0.09948056232819362 づる 4.921973336281314 0.3184482737071388 さやけ 4.921973336281314 0.3184482737071388 さ 3.217225244042889 0.20815225014334174
そこで参考文献 [3] をみると scikit-learn の TF-IDF はデフォルトで以下の (1) (2) (3) の定義を採用しているようなので素朴な計算においてこれらの定義をそろえれば scikit-learn の結果とそろう。
from sklearn.preprocessing import normalize for id_ in range(79, 80): docs[id_]['スムーズIDF'] = {} v = [] for word, freq in docs[id_]['出現頻度'].items(): # (1) TF は相対度数ではない生の出現頻度 # (2) IDF は分子と分母に1を足しさらに全体に1を足す idf_ = np.log((1.0 + n_doc) / (1.0 + vocab[word]['文書頻度'])) + 1.0 docs[id_]['スムーズIDF'][word] = idf_ v.append(freq * idf_) v = normalize(np.array([v]), norm='l2') # (3) L2正規化する docs[id_]['正規化スムーズTF-IDF'] = {} for i, (word, _) in enumerate(docs[id_]['TF'].items()): docs[id_]['正規化スムーズTF-IDF'][word] = v[0, i] id_ = 79 print(f'\n◆ {id_}首目の情報') print(f'{len(docs[id_]["単語列"])} 単語 ({len(docs[id_]["出現頻度"])} ユニーク単語)') print(pd.DataFrame({k: docs[id_][k].values() for k in ['出現頻度', 'スムーズIDF', '正規化スムーズTF-IDF']}, index=docs[id_]['出現頻度'].keys()))
◆ 79首目の情報 16 単語 (14 ユニーク単語) 出現頻度 スムーズIDF 正規化スムーズTF-IDF 秋風 1 4.228826 0.273602 に 1 1.537583 0.099481 棚引く 1 4.921973 0.318448 雲 1 3.823361 0.247369 の 3 1.160773 0.225304 絶間 1 4.921973 0.318448 より 1 4.005683 0.259165 もれ 1 4.921973 0.318448 出 1 3.217225 0.208152 づる 1 4.921973 0.318448 月 1 3.130214 0.202523 影 1 4.921973 0.318448 さやけ 1 4.921973 0.318448 さ 1 3.217225 0.208152