AIって結局何なのかよく分からないので、とりあえず100日間勉強してみた Day44
経緯についてはこちらをご参照ください。
■本日の進捗
●tf-idfを理解
■はじめに
引き続き「Pythonではじめる機械学習(オライリー・ジャパン)」で学んでいきます。
今回はテキストデータの特徴量を重要度に応じて捨てていくのではなく、スケール変換する手法について学んでいきます。
■tf-idf
tf-idf(term frequency-inverse document frequency)とは、特定のドキュメントにだけ頻繁に現れる単語には情報量が多いと考えて重みを付けて、コーパス全体で頻繁に現れる単語にはあまり重要ではないとして重みを大きく与えないことで特徴量のスケールを変換する手法です。
$$ \mathrm{tfidf}(t, d) = tf \left( \log \left(\frac{N+1}{N_t + 1}\right) +1 \right) $$
$$ N:訓練データ内のドキュメント数 $$
$$ N_t:訓練データ内の単語tの数 $$
$$ tf:ドキュメントd内の単語tの数 $$
つまり、特定のドキュメント内の単語出現率(term frequency)と、単語が他のドキュメントにどれだけ出現しないかを示す逆文書頻度(iverse document frequency)を掛け合わせたものです。
scikit-learnにはif-idfを使うためのクラスがTfidfVectorizerとTfidfTransformerの2つ用意されています。
●TfidfVectorizer
TfidfVectorizerは、単語のトークン化からtf-idf変換までを一括して実行するクラスで、テキストデータを直接引数にして単語のベクトル表現を返してくれます。
from sklearn.feature_extraction.text import TfidfVectorizer documents = ["I like cats", "I like dogs", "We love cats and dogs"] vectorizer = TfidfVectorizer() tfidf_matrix = vectorizer.fit_transform(documents) print(vectorizer.get_feature_names_out()) print(tfidf_matrix.toarray())
['and' 'cats' 'dogs' 'like' 'love' 'we']
[[0. 0.70710678 0. 0.70710678 0. 0. ]
[0. 0. 0.70710678 0.70710678 0. 0. ]
[0.49047908 0.37302199 0.37302199 0. 0.49047908 0.49047908]]
tf-idfは内部的にL2正規化を行っていて、ドキュメントの長さに依存しない単語の出現頻度を重要度として相対値0~1の範囲で持ちます。
●TfidfTransformer
TfidfTransformerは、事前にCountVectorizerを用いて頻度ベクトルとして表現されたデータに対して、tf-idf変換をトランスフォームするためのクラスで、テキストデータのトークン化やCountVectorizerを別途行う必要があります。
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer documents = ["I like cats", "I like dogs", "We love cats and dogs"] count_vectorizer = CountVectorizer() count_matrix = count_vectorizer.fit_transform(documents) tfidf_transformer = TfidfTransformer() tfidf_matrix = tfidf_transformer.fit_transform(count_matrix) print(count_vectorizer.get_feature_names_out()) print(tfidf_matrix.toarray())
['and' 'cats' 'dogs' 'like' 'love' 'we']
[[0. 0.70710678 0. 0.70710678 0. 0. ]
[0. 0. 0.70710678 0.70710678 0. 0. ]
[0.49047908 0.37302199 0.37302199 0. 0.49047908 0.49047908]]
得られたスケール変換結果はTfidfVectorizerと同じです。
●精度検証
ストップワードの時と同様に、IMDbデータセットで学習させたモデルを用います。
import numpy as np from sklearn.datasets import load_files from sklearn.feature_extraction.text import CountVectorizer from sklearn.linear_model import LogisticRegression reviews_train = load_files("C:/Users/****/Documents/Python/aclImdb/train") text_train, y_train = reviews_train.data, reviews_train.target reviews_test = load_files("C:/Users/****/Documents/Python/aclImdb/test") text_test, y_test = reviews_test.data, reviews_test.target vectorizer = CountVectorizer() X_train = vectorizer.fit_transform(text_train) X_test = vectorizer.transform(text_test) model = LogisticRegression(C=0.1, max_iter=10000, n_jobs=-1) model.fit(X_train, y_train) score = model.score(X_test, y_test) print("score :{:.2f}".format(score))
score :0.88
スコアは0.88でした。比較のために上記と同じ条件で、tf-idfを適用してみます。
import numpy as np from sklearn.datasets import load_files from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import GridSearchCV reviews_train = load_files("C:/Users/****/Documents/Python/aclImdb/train") text_train, y_train = reviews_train.data, reviews_train.target reviews_test = load_files("C:/Users/****/Documents/Python/aclImdb/test") text_test, y_test = reviews_test.data, reviews_test.target vectorizer = TfidfVectorizer() X_train = vectorizer.fit_transform(text_train) X_test = vectorizer.transform(text_test) model = LogisticRegression(C=0.1, max_iter=10000, n_jobs=-1) model.fit(X_train, y_train) # y_pred = model.predict(X_test) score = model.score(X_test, y_test) print("score with tf-idf :{:.2f}".format(score))
score with tf-idf :0.85
スコアは少し大きめに落ちてしまいました。
何が起こったのでしょうか?
tf-idfが重要ではないと判断したトークンは逆文書頻度を見ると分かります。
import numpy as np from sklearn.datasets import load_files from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import GridSearchCV reviews_train = load_files("C:/Users/****/Documents/Python/aclImdb/train") text_train, y_train = reviews_train.data, reviews_train.target reviews_test = load_files("C:/Users/****/Documents/Python/aclImdb/test") text_test, y_test = reviews_test.data, reviews_test.target vectorizer = TfidfVectorizer() X_train = vectorizer.fit_transform(text_train) X_test = vectorizer.transform(text_test) model = LogisticRegression(C=0.1, max_iter=10000, n_jobs=-1) model.fit(X_train, y_train) feature_names = np.array(vectorizer.get_feature_names_out()) sorted_by_idf = np.argsort(vectorizer.idf_) print("low idf :{}".format(feature_names[sorted_by_idf[:100]]))
low idf :['the' 'and' 'of' 'to' 'this' 'is' 'it' 'in' 'that' 'but' 'for' 'with'
'was' 'as' 'on' 'movie' 'not' 'br' 'have' 'one' 'be' 'film' 'are' 'you'
'all' 'at' 'an' 'so' 'by' 'from' 'like' 'who' 'they' 'there' 'if' 'his'
'out' 'just' 'about' 'he' 'or' 'has' 'what' 'some' 'good' 'can' 'more'
'when' 'time' 'up' 'very' 'even' 'only' 'no' 'would' 'my' 'see' 'really'
'story' 'which' 'well' 'had' 'me' 'than' 'much' 'their' 'get' 'were'
'other' 'been' 'do' 'most' 'don' 'her' 'also' 'into' 'first' 'made' 'how'
'great' 'because' 'will' 'people' 'make' 'way' 'could' 'we' 'bad' 'after'
'any' 'too' 'then' 'them' 'she' 'watch' 'think' 'acting' 'movies' 'seen'
'its']
tf-idfは重要視していない100トークンを表示してみました。
その多くはストップワードに含まれているような接続詞や前置詞が並びます。これらは確かに重要な意味は持たなそうに思えます。しかしよく見てみると “like, good, bad, no…” などのセンチメント分析において重要な意味を持つ可能性の高い単語も含まれてしまっています。これでは十分に学習できないのでしょう。
一応、tf-idfと交差検証を用いて汎化性能のあるモデルを構築してみます。
import numpy as np from sklearn.datasets import load_files from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import GridSearchCV reviews_train = load_files("C:/Users/****/Documents/Python/aclImdb/train") text_train, y_train = reviews_train.data, reviews_train.target reviews_test = load_files("C:/Users/****/Documents/Python/aclImdb/test") text_test, y_test = reviews_test.data, reviews_test.target vectorizer = TfidfVectorizer() X_train = vectorizer.fit_transform(text_train) model = LogisticRegression(max_iter=10000) param_grid = {'C': [0.001, 0.01, 0.1, 1, 10]} grid = GridSearchCV(model, param_grid, cv=5, n_jobs=-1) grid.fit(X_train, y_train) print("best C:", grid.best_params_) print("cv best score: {:.2f}".format(grid.best_score_))
best C: {'C': 10}
cv best score: 0.89
ロジスティック回帰のハイパーパラメータCを調整することで、交差検証の内側で0.89までスコアを出せたようです。
最終的なロジスティック回帰が学習した係数の値はcoef_で取得できるので、各クラスで重要と学習した単語にはどんなものがあるのか見てみます。
import numpy as np from sklearn.datasets import load_files from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import GridSearchCV reviews_train = load_files("C:/Users/****/Documents/Python/aclImdb/train") text_train, y_train = reviews_train.data, reviews_train.target reviews_test = load_files("C:/Users/****/Documents/Python/aclImdb/test") text_test, y_test = reviews_test.data, reviews_test.target vectorizer = TfidfVectorizer() X_train = vectorizer.fit_transform(text_train) X_test = vectorizer.transform(text_test) model = LogisticRegression(max_iter=10000) param_grid = {'C': [0.001, 0.01, 0.1, 1, 10]} grid = GridSearchCV(model, param_grid, cv=5, n_jobs=-1) grid.fit(X_train, y_train) best_model = grid.best_estimator_ feature_names = vectorizer.get_feature_names_out() coefficients = best_model.coef_[0] n = 10 class_0_features = np.argsort(coefficients)[:n] print(f"Class 0 (Negative) top {n} features:") for idx in class_0_features: print(f"{feature_names[idx]}: {coefficients[idx]:.3f}") print() class_1_features = np.argsort(coefficients)[-n:][::-1] print(f"Class 1 (Positive) top {n} features:") for idx in class_1_features: print(f"{feature_names[idx]}: {coefficients[idx]:.3f}") print()
Class 0 (Negative) top 10 features:
worst: -18.297
waste: -13.921
awful: -13.805
boring: -11.505
bad: -10.978
poorly: -10.734
disappointment: -10.612
worse: -9.887
poor: -9.472
mess: -9.202
Class 1 (Positive) top 10 features:
great: 11.805
excellent: 11.365
perfect: 10.337
wonderful: 8.395
today: 8.309
best: 8.218
superb: 7.772
amazing: 7.635
highly: 7.544
enjoyable: 7.511
素晴らしい学習結果です。ストップワードも使っていないので、英語を学んだことのないモデルの学習とは思えません。Class 0に関してはなんと全部の単語が(純日本人が見ても)明らかなネガティブワードで、Class 1も “today” を除いては明らかなポジティブワードで構成されています。
英語が全然だめな方は自身が学びたい業界の記事やコメントをコピペしてきて、機械学習モデルに学習してもらってから単語を覚えたら、もしかすると自習するよりも効率的かもしれません。
■おわりに
今回は記念すべき「AIって結局何なのかよく分からないので、とりあえず100日間勉強してみた」の第44日目です。
この偉大な数字に辿り着くまで1日も休むことなく続けて来れたことに、毎日新たな発見と楽しみを見出せる日々に、心から感謝しております。
これからも引き続き、これら素晴らしい技術に触れられる環境に感謝しながら楽しんで学んでいきたいと思います。
■参考文献
- Andreas C. Muller, Sarah Guido. Pythonではじめる機械学習. 中田 秀基 訳. オライリー・ジャパン. 2017. 392p.
- ChatGPT. 4o mini. OpenAI. 2024. https://chatgpt.com/
- API Reference. scikit-learn.org. https://scikit-learn.org/stable/api/index.html
- Maas, Andrew L. and Daly, Raymond E. and Pham, Peter T. and Huang, Dan and Ng, Andrew Y. and Potts, Christopher, Learning Word Vectors for Sentiment Analysis, Proceedings of the 49th Annual Meeting of the Association for Computational Linguistics: Human Language Technologies, June, 2011, Portland, Oregon, USA, Association for Computational Linguistics, 142–150, http://www.aclweb.org/anthology/P11-1015
- Potts, Christopher. 2011. On the negativity of negation. In Nan Li and David Lutz, eds., Proceedings of Semantics and Linguistic Theory 20, 636-659.