tf-idf - 静かなる名辞 https://www.haya-programming.com/archive/category/tf-idf pythonとプログラミングのこと Thu, 07 May 2020 20:42:34 +0900 http://blogs.law.harvard.edu/tech/rss Hatena::Blog 【python】sklearnのfetch_20newsgroupsで文書分類を試す(5) https://www.haya-programming.com/entry/2019/05/15/232429 <div class="section"> <h3>はじめに</h3> <p> <a href="https://www.haya-programming.com/entry/2018/02/22/060148">&#x305A;&#x3063;&#x3068;&#x653E;&#x7F6E;&#x3057;&#x3066;&#x3044;&#x305F;&#x30B7;&#x30EA;&#x30FC;&#x30BA;</a>ですが、その後新たに得られた知見が出てきたので、更新しておこうと思います。</p> </div> <div class="section"> <h3>得られた知見</h3> <p> いろいろ勉強した結果、以下のような考えに至りました。</p> <ul> <li>そもそもデータ数が多いので、高級な分類器であればあるほど速度的に厳しい</li> <li>MultinomialNB(多項分布ナイーブベイズ)の性能は意外と良いのでそれでいい</li> <li>その場合、tfidfとか使うべき。また、パラメタチューニングを真面目にやるべき</li> <li><a href="https://www.haya-programming.com/entry/2019/08/14/023331">&#x758E;&#x884C;&#x5217;&#x578B;&#x3092;&#x3046;&#x307E;&#x304F;&#x4F7F;&#x3046;&#x3068;&#x5927;&#x898F;&#x6A21;&#x30C7;&#x30FC;&#x30BF;&#x3067;&#x3082;&#x9AD8;&#x901F;&#x51E6;&#x7406;&#x304C;&#x53EF;&#x80FD;</a></li> </ul><p><br />  ということで、この方針でやります。</p> </div> <div class="section"> <h3>実験</h3> <p> まず以下のコードで軽く回します。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> sklearn.datasets <span class="synPreProc">import</span> fetch_20newsgroups <span class="synPreProc">from</span> sklearn.feature_extraction.text <span class="synPreProc">import</span> TfidfVectorizer <span class="synPreProc">from</span> sklearn.naive_bayes <span class="synPreProc">import</span> MultinomialNB <span class="synPreProc">from</span> sklearn.pipeline <span class="synPreProc">import</span> Pipeline <span class="synPreProc">from</span> sklearn.model_selection <span class="synPreProc">import</span> GridSearchCV <span class="synPreProc">from</span> sklearn.metrics <span class="synPreProc">import</span> classification_report <span class="synStatement">def</span> <span class="synIdentifier">main</span>(): news20_train = fetch_20newsgroups(subset=<span class="synConstant">&quot;train&quot;</span>) news20_test = fetch_20newsgroups(subset=<span class="synConstant">&quot;test&quot;</span>) y_train = news20_train.target y_test = news20_test.target vectorizer = TfidfVectorizer( stop_words=<span class="synConstant">&quot;english&quot;</span>, max_df=<span class="synConstant">0.03</span>, min_df=<span class="synConstant">0.0005</span>) nb = MultinomialNB(alpha=<span class="synConstant">1e-1</span>) pl = Pipeline([(<span class="synConstant">&quot;v&quot;</span>, vectorizer), (<span class="synConstant">&quot;nb&quot;</span>, nb)]) params = {<span class="synConstant">&quot;v__max_df&quot;</span>:[<span class="synConstant">0.3</span>, <span class="synConstant">0.1</span>, <span class="synConstant">0.03</span>], <span class="synConstant">&quot;v__min_df&quot;</span>:[<span class="synConstant">0.01</span>, <span class="synConstant">0.003</span>, <span class="synConstant">0.001</span>, <span class="synConstant">0.0003</span>], <span class="synConstant">&quot;nb__alpha&quot;</span>:[<span class="synConstant">1e-0</span>, <span class="synConstant">1e-1</span>, <span class="synConstant">1e-2</span>, <span class="synConstant">1e-3</span>]} clf = GridSearchCV(pl, params, cv=<span class="synConstant">4</span>, scoring=<span class="synConstant">&quot;f1_macro&quot;</span>, n_jobs=-<span class="synConstant">1</span>) clf.fit(news20_train.data, y_train) <span class="synIdentifier">print</span>(<span class="synConstant">&quot;result of gridsearch&quot;</span>) <span class="synIdentifier">print</span>(<span class="synConstant">&quot;best score&quot;</span>, clf.best_score_) <span class="synIdentifier">print</span>(<span class="synConstant">&quot;best parameter&quot;</span>, clf.best_params_) y_pred = clf.predict(news20_test.data) <span class="synIdentifier">print</span>(classification_report( y_test, y_pred, target_names=news20_test.target_names, digits=<span class="synConstant">4</span>)) <span class="synStatement">if</span> __name__ == <span class="synConstant">&quot;__main__&quot;</span>: main() </pre><p> 見ての通り、ざっくりグリッドサーチしています。これでそれなりに良くなるはず。</p><p> 特徴選択のモデルもPipelineで同時にチューニングしますので、これでだいたい</p> <ul> <li>取るべき次元数を決めるパラメタ</li> <li>NBのalpha</li> </ul><p> についてはわかるはずです。</p><p> 結果</p> <pre class="code" data-lang="" data-unlink>result of gridsearch best score 0.903351987953267 best parameter {&#39;nb__alpha&#39;: 0.01, &#39;v__max_df&#39;: 0.3, &#39;v__min_df&#39;: 0.0003} precision recall f1-score support alt.atheism 0.8366 0.8025 0.8192 319 comp.graphics 0.6568 0.7378 0.6949 389 comp.os.ms-windows.misc 0.7079 0.6396 0.6720 394 comp.sys.ibm.pc.hardware 0.6522 0.7270 0.6876 392 comp.sys.mac.hardware 0.8281 0.8260 0.8270 385 comp.windows.x 0.8388 0.7772 0.8068 395 misc.forsale 0.7614 0.8103 0.7851 390 rec.autos 0.8943 0.8763 0.8852 396 rec.motorcycles 0.9244 0.9523 0.9381 398 rec.sport.baseball 0.9491 0.9395 0.9443 397 rec.sport.hockey 0.9559 0.9774 0.9665 399 sci.crypt 0.9035 0.9217 0.9125 396 sci.electronics 0.8066 0.7430 0.7735 393 sci.med 0.8886 0.8258 0.8560 396 sci.space 0.8734 0.8934 0.8833 394 soc.religion.christian 0.8562 0.9422 0.8971 398 talk.politics.guns 0.7788 0.9093 0.8390 364 talk.politics.mideast 0.9642 0.9309 0.9472 376 talk.politics.misc 0.7734 0.6387 0.6996 310 talk.religion.misc 0.7418 0.6295 0.6810 251 micro avg 0.8309 0.8309 0.8309 7532 macro avg 0.8296 0.8250 0.8258 7532 weighted avg 0.8322 0.8309 0.8301 7532 </pre><p> けっこういい感じです。すでに過去のシリーズの最高スコアです。</p><p> ここから更に詰めていくため、RandomizedSearchCVを使います。</p><p> 参考:<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.haya-programming.com%2Fentry%2F2019%2F03%2F16%2F044745" title="【python】sklearnのRandomizedSearchCVを使ってみる - 静かなる名辞" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><a href="https://www.haya-programming.com/entry/2019/03/16/044745">&#x3010;python&#x3011;sklearn&#x306E;RandomizedSearchCV&#x3092;&#x4F7F;&#x3063;&#x3066;&#x307F;&#x308B; - &#x9759;&#x304B;&#x306A;&#x308B;&#x540D;&#x8F9E;</a></p><p> 分布に関しては多少手抜きをして、max_dfとmin_dfは区間を適当に区切った一様分布、alphaのみ指数分布としています。妥当なものは他に考えられるかもしれませんが、これでいきます。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> scipy <span class="synPreProc">import</span> stats <span class="synPreProc">from</span> sklearn.datasets <span class="synPreProc">import</span> fetch_20newsgroups <span class="synPreProc">from</span> sklearn.feature_extraction.text <span class="synPreProc">import</span> TfidfVectorizer <span class="synPreProc">from</span> sklearn.naive_bayes <span class="synPreProc">import</span> MultinomialNB <span class="synPreProc">from</span> sklearn.pipeline <span class="synPreProc">import</span> Pipeline <span class="synPreProc">from</span> sklearn.model_selection <span class="synPreProc">import</span> RandomizedSearchCV <span class="synPreProc">from</span> sklearn.metrics <span class="synPreProc">import</span> classification_report <span class="synStatement">def</span> <span class="synIdentifier">main</span>(): news20_train = fetch_20newsgroups(subset=<span class="synConstant">&quot;train&quot;</span>) news20_test = fetch_20newsgroups(subset=<span class="synConstant">&quot;test&quot;</span>) y_train = news20_train.target y_test = news20_test.target vectorizer = TfidfVectorizer( stop_words=<span class="synConstant">&quot;english&quot;</span>, max_df=<span class="synConstant">0.03</span>, min_df=<span class="synConstant">0.0005</span>) nb = MultinomialNB(alpha=<span class="synConstant">1e-1</span>) pl = Pipeline([(<span class="synConstant">&quot;v&quot;</span>, vectorizer), (<span class="synConstant">&quot;nb&quot;</span>, nb)]) max_df_dist = stats.uniform(<span class="synConstant">0.1</span>, <span class="synConstant">0.5</span>) min_df_dist = stats.uniform(<span class="synConstant">0.00007</span>, <span class="synConstant">0.001</span>) alpha_dist = stats.expon(scale=<span class="synConstant">1e-2</span>) params = {<span class="synConstant">&quot;v__max_df&quot;</span>:max_df_dist, <span class="synConstant">&quot;v__min_df&quot;</span>:min_df_dist, <span class="synConstant">&quot;nb__alpha&quot;</span>:alpha_dist} clf = RandomizedSearchCV(pl, params, cv=<span class="synConstant">4</span>, scoring=<span class="synConstant">&quot;f1_macro&quot;</span>, n_iter=<span class="synConstant">100</span>, n_jobs=-<span class="synConstant">1</span>) clf.fit(news20_train.data, y_train) <span class="synIdentifier">print</span>(<span class="synConstant">&quot;result of gridsearch&quot;</span>) <span class="synIdentifier">print</span>(<span class="synConstant">&quot;best score&quot;</span>, clf.best_score_) <span class="synIdentifier">print</span>(<span class="synConstant">&quot;best parameter&quot;</span>, clf.best_params_) y_pred = clf.predict(news20_test.data) <span class="synIdentifier">print</span>(classification_report( y_test, y_pred, target_names=news20_test.target_names, digits=<span class="synConstant">4</span>)) <span class="synStatement">if</span> __name__ == <span class="synConstant">&quot;__main__&quot;</span>: main() </pre><p> 結果</p> <pre class="code" data-lang="" data-unlink>result of gridsearch best score 0.9078423646680397 best parameter {&#39;nb__alpha&#39;: 0.008635226675407684, &#39;v__max_df&#39;: 0.14464593949316493, &#39;v__min_df&#39;: 0.00010360792392347633} precision recall f1-score support alt.atheism 0.8355 0.7962 0.8154 319 comp.graphics 0.6659 0.7326 0.6977 389 comp.os.ms-windows.misc 0.6983 0.6345 0.6649 394 comp.sys.ibm.pc.hardware 0.6386 0.7168 0.6755 392 comp.sys.mac.hardware 0.8165 0.8208 0.8187 385 comp.windows.x 0.8250 0.7519 0.7868 395 misc.forsale 0.7628 0.8000 0.7810 390 rec.autos 0.9143 0.8889 0.9014 396 rec.motorcycles 0.9270 0.9573 0.9419 398 rec.sport.baseball 0.9467 0.9395 0.9431 397 rec.sport.hockey 0.9509 0.9699 0.9603 399 sci.crypt 0.9084 0.9268 0.9175 396 sci.electronics 0.7941 0.7557 0.7744 393 sci.med 0.8849 0.8157 0.8489 396 sci.space 0.8707 0.9061 0.8881 394 soc.religion.christian 0.8514 0.9497 0.8979 398 talk.politics.guns 0.7778 0.9038 0.8361 364 talk.politics.mideast 0.9669 0.9335 0.9499 376 talk.politics.misc 0.7812 0.6452 0.7067 310 talk.religion.misc 0.7524 0.6295 0.6855 251 micro avg 0.8295 0.8295 0.8295 7532 macro avg 0.8285 0.8237 0.8246 7532 weighted avg 0.8307 0.8295 0.8287 7532 </pre><p> パラメータチューニング時のスコアは改善しますが、実際の予測では少し下がる結果に。まあ、これくらいが限界に近いのでしょう(この特徴量の作り方と分類器の組み合わせでは)。パラメータチューニングのときと本予測のときとでけっこうスコアが違うのでなんとなく過学習してるような気もしますが、理由がよくわからん。</p><p> 若干後味が悪いですが、数字は悪くないのでこれでよしとします。</p> </div> <div class="section"> <h3>まとめ</h3> <p> これで次はない・・・かも。</p> </div> <div class="section"> <h3>過去の回</h3> <p><a href="https://www.haya-programming.com/entry/2018/02/19/200006">&#x3010;python&#x3011;sklearn&#x306E;fetch_20newsgroups&#x3067;&#x6587;&#x66F8;&#x5206;&#x985E;&#x3092;&#x8A66;&#x3059;(1) - &#x9759;&#x304B;&#x306A;&#x308B;&#x540D;&#x8F9E;</a><br /> <a href="https://www.haya-programming.com/entry/2018/02/20/215416">&#x3010;python&#x3011;sklearn&#x306E;fetch_20newsgroups&#x3067;&#x6587;&#x66F8;&#x5206;&#x985E;&#x3092;&#x8A66;&#x3059;(2) - &#x9759;&#x304B;&#x306A;&#x308B;&#x540D;&#x8F9E;</a><br /> <a href="https://www.haya-programming.com/entry/2018/02/22/060148">&#x3010;python&#x3011;sklearn&#x306E;fetch_20newsgroups&#x3067;&#x6587;&#x66F8;&#x5206;&#x985E;&#x3092;&#x8A66;&#x3059;(3) - &#x9759;&#x304B;&#x306A;&#x308B;&#x540D;&#x8F9E;</a><br /> <a href="https://www.haya-programming.com/entry/2018/03/26/212112">&#x3010;python&#x3011;sklearn&#x306E;fetch_20newsgroups&#x3067;&#x6587;&#x66F8;&#x5206;&#x985E;&#x3092;&#x8A66;&#x3059;(4) - &#x9759;&#x304B;&#x306A;&#x308B;&#x540D;&#x8F9E;</a></p> </div> Wed, 15 May 2019 23:24:29 +0900 hatenablog://entry/17680117127132099752 python sklearn 20newsgroups 自然言語処理 機械学習 特徴抽出 Pipeline tf-idf 【python】TF-IDFで重要語を抽出してみる https://www.haya-programming.com/entry/2018/07/09/190819 <div class="section"> <h3>概要</h3> <p> すでに語り尽くされた感のあるネタですが、TF-IDFで文書の重要な単語(重要語、あるいは特徴語)を抽出してみます。</p><p> numpyとsklearnを使うと、10行程度のコードで実現できるので簡単です。</p><p><span style="font-size: 80%">スポンサーリンク</span><br /> <script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script></p> <p><ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-6261827798827777" data-ad-slot="1744230936" data-ad-format="auto" data-full-width-responsive="true"></ins><br /> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script><br /> </p> </div> <div class="section"> <h3>コードの書き方</h3> <p> とりあえず、対象データとしては20newsgroupsを使います。関数一つで読み込めて便利だからです。</p><p> <a href="http://scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_20newsgroups.html">sklearn.datasets.fetch_20newsgroups &mdash; scikit-learn 0.20.1 documentation</a></p><p> 自然言語処理の技術紹介などの記事で、Webスクレイピングなどをしてデータを作っているケースをよく見かけますが、こちらの方が手間がかからなくて、再現性も高いです<a href="#f-a58ec6ea" name="fn-a58ec6ea" title="sklearnが仕様変更しない限り再現できる">*1</a>。使えるデータは使いましょう。</p><p> 関連記事:<a href="https://www.haya-programming.com/entry/2018/02/19/200006">&#x3010;python&#x3011;sklearn&#x306E;fetch_20newsgroups&#x3067;&#x6587;&#x66F8;&#x5206;&#x985E;&#x3092;&#x8A66;&#x3059;(1) - &#x9759;&#x304B;&#x306A;&#x308B;&#x540D;&#x8F9E;</a></p><p> あとはTfidfVectorizerに入れて、いきなりTF-IDFのベクトルに変換します。</p><p> <a href="http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html">sklearn.feature_extraction.text.TfidfVectorizer &mdash; scikit-learn 0.20.1 documentation</a></p><p> 詳しい使い方は、ドキュメントや<a href="https://www.haya-programming.com/entry/2018/02/25/044525">CountVectorizer&#x306E;&#x8A18;&#x4E8B;</a>を読んでいただければ良いです(CountVectorizerと使い方はほぼ同じ)。</p><p> 使い方のコツとして</p> <ul> <li>min_dfオプションを適当に指定してゴミ単語を削った方が良いこと</li> <li>基本的にtransformした返り値がsparse matrix型なのでtoarray()メソッドで密行列に変換して取り扱ってやる必要があること</li> </ul><p> が挙げられます。それ以外は、とりあえず使うだけならそれほど気は配らなくても良いはず。</p><p> ここまでの記述をコードにすると、こんな感じです。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> numpy <span class="synStatement">as</span> np <span class="synPreProc">from</span> sklearn.feature_extraction.text <span class="synPreProc">import</span> TfidfVectorizer <span class="synPreProc">from</span> sklearn.datasets <span class="synPreProc">import</span> fetch_20newsgroups news20 = fetch_20newsgroups() vectorizer = TfidfVectorizer(min_df=<span class="synConstant">0.03</span>) tfidf_X = vectorizer.fit_transform(news20.data[:<span class="synConstant">1000</span>]).toarray() <span class="synComment"># ぜんぶで1万データくらいあるけど、そんなに要らないので1000件取っている</span> </pre><p> ここからどうするんじゃい、ということですが、スマートに書くためには、ちょっとしたnumpy芸が要求されます。</p> <pre class="code lang-python" data-lang="python" data-unlink>index = tfidf_X.argsort(axis=<span class="synConstant">1</span>)[:,::-<span class="synConstant">1</span>] </pre><p> tfidf_X.argsort(axis=1)でソートした結果のindexを返します。[:,::-1]はreverseです。これによって、各文書のTF-IDF値にもとづいて降順ソートされたindexが得られます。</p><p> 次に、このindexに基づいて単語を復元することを考えます。TfidfVectorizer.get_feature_names()で、特徴抽出時に使ったindexの順に並んだ単語のリストが得られるのですが<a href="#f-48bef2bb" name="fn-48bef2bb" title="つまりindexと特徴ベクトルの次元が対応">*2</a>、リストだとnumpy芸が使えないのでnumpy配列にしておきます。あとは、一気に変換します。</p> <pre class="code lang-python" data-lang="python" data-unlink>feature_names = np.array(vectorizer.get_feature_names()) feature_words = feature_names[index] </pre><p> numpyのこの機能を使っているコードはあまり見かけないのですが、実は</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; <span class="synPreProc">import</span> numpy <span class="synStatement">as</span> np &gt;&gt;&gt; a = np.array([<span class="synConstant">&quot;hoge&quot;</span>,<span class="synConstant">&quot;fuga&quot;</span>,<span class="synConstant">&quot;piyo&quot;</span>]) &gt;&gt;&gt; b = np.array([[<span class="synConstant">0</span>,<span class="synConstant">0</span>,<span class="synConstant">0</span>],[<span class="synConstant">2</span>,<span class="synConstant">1</span>,<span class="synConstant">0</span>],[<span class="synConstant">0</span>,<span class="synConstant">2</span>,<span class="synConstant">0</span>]]) &gt;&gt;&gt; a[b] array([[<span class="synConstant">'hoge'</span>, <span class="synConstant">'hoge'</span>, <span class="synConstant">'hoge'</span>], [<span class="synConstant">'piyo'</span>, <span class="synConstant">'fuga'</span>, <span class="synConstant">'hoge'</span>], [<span class="synConstant">'hoge'</span>, <span class="synConstant">'piyo'</span>, <span class="synConstant">'hoge'</span>]], dtype=<span class="synConstant">'&lt;U4'</span>) </pre><p> こういう仕様になっておりまして、意図した通りの変換が一発でできています。知らないと戸惑いますね。</p><p> あとは配列から適当に取り出せばオッケーです。各文書ベクトル(というか単語の順列)の先頭n次元を取ると、それがそのままn個目までの重要語になっています。</p> </div> <div class="section"> <h3>やってみた</h3> <p> コード全文を以下に示します。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> numpy <span class="synStatement">as</span> np <span class="synPreProc">from</span> sklearn.feature_extraction.text <span class="synPreProc">import</span> TfidfVectorizer <span class="synPreProc">from</span> sklearn.datasets <span class="synPreProc">import</span> fetch_20newsgroups news20 = fetch_20newsgroups() vectorizer = TfidfVectorizer(min_df=<span class="synConstant">0.03</span>) tfidf_X = vectorizer.fit_transform(news20.data[:<span class="synConstant">1000</span>]).toarray() index = tfidf_X.argsort(axis=<span class="synConstant">1</span>)[:,::-<span class="synConstant">1</span>] feature_names = np.array(vectorizer.get_feature_names()) feature_words = feature_names[index] n = <span class="synConstant">5</span> <span class="synComment"># top何単語取るか</span> m = <span class="synConstant">15</span> <span class="synComment"># 何記事サンプルとして抽出するか</span> <span class="synStatement">for</span> fwords, target <span class="synStatement">in</span> <span class="synIdentifier">zip</span>(feature_words[:m,:n], news20.target): <span class="synComment"># 各文書ごとにtarget(ラベル)とtop nの重要語を表示</span> <span class="synIdentifier">print</span>(news20.target_names[target]) <span class="synIdentifier">print</span>(fwords) </pre><p> 結果は、</p> <pre class="code" data-lang="" data-unlink>rec.autos [&#39;car&#39; &#39;was&#39; &#39;this&#39; &#39;the&#39; &#39;where&#39;] comp.sys.mac.hardware [&#39;washington&#39; &#39;add&#39; &#39;guy&#39; &#39;speed&#39; &#39;call&#39;] comp.sys.mac.hardware [&#39;the&#39; &#39;display&#39; &#39;anybody&#39; &#39;heard&#39; &#39;disk&#39;] comp.graphics [&#39;division&#39; &#39;chip&#39; &#39;systems&#39; &#39;computer&#39; &#39;four&#39;] sci.space [&#39;error&#39; &#39;known&#39; &#39;tom&#39; &#39;memory&#39; &#39;the&#39;] talk.politics.guns [&#39;of&#39; &#39;the&#39; &#39;com&#39; &#39;to&#39; &#39;says&#39;] sci.med [&#39;thanks&#39; &#39;couldn&#39; &#39;instead&#39; &#39;file&#39; &#39;everyone&#39;] comp.sys.ibm.pc.hardware [&#39;chip&#39; &#39;is&#39; &#39;fast&#39; &#39;ibm&#39; &#39;bit&#39;] comp.os.ms-windows.misc [&#39;win&#39; &#39;help&#39; &#39;please&#39; &#39;appreciated&#39; &#39;figure&#39;] comp.sys.mac.hardware [&#39;the&#39; &#39;file&#39; &#39;lost&#39; &#39;ve&#39; &#39;it&#39;] rec.motorcycles [&#39;00&#39; &#39;org&#39; &#39;the&#39; &#39;out&#39; &#39;and&#39;] talk.religion.misc [&#39;the&#39; &#39;that&#39; &#39;may&#39; &#39;to&#39; &#39;is&#39;] comp.sys.mac.hardware [&#39;hp&#39; &#39;co&#39; &#39;com&#39; &#39;tin&#39; &#39;newsreader&#39;] sci.space [&#39;the&#39; &#39;power&#39; &#39;and&#39; &#39;space&#39; &#39;nasa&#39;] misc.forsale [&#39;10&#39; &#39;very&#39; &#39;and&#39; &#39;reasonable&#39; &#39;sale&#39;]</pre><p> まあ、それなりにうまくいってるんじゃね? という結果が得られました<a href="#f-fcb40f2a" name="fn-fcb40f2a" title="それなりに「まとも」な結果になっているのはTfidfVectorizerのオプションでmin_df=0.03を指定しているからで、これをやらないと見事にdfが低すぎるゴミ単語ばっかり引っかかる結果になる。注意しましょう">*3</a>。</p><p> 車のカテゴリやコンピュータのカテゴリ、宇宙のカテゴリなんかは割とわかりやすいですが、talk.religion.misc(宗教に関する話題?)だと['the' 'that' 'may' 'to' 'is']になっていたりするのは面白いです。この文書だけがたまたまとても抽象的だったのか、このカテゴリ自体こんな感じなのかはよくわかりません。</p><p> ということで、文書ごとにやってうまく結果が出るのはわかったので、次は各カテゴリ(ラベル)ごとに特徴的な単語を出してみようと思ったのですが、これはちょっとめんどいのでとりあえずパス。そのうち気が向いたら追記します。</p> </div> <div class="section"> <h3>まとめ</h3> <p> 特徴抽出とTF-IDFの計算を自分で書いて、重要語への変換も自分で書いてという感じでやるとかなり手間がかかるのですが、sklearnとnumpyのちからに頼ると簡潔に書けて嬉しいですね。</p><p> TF-IDFの上位数件くらいは、それなりに文書の特徴を反映するような単語と言って良いと思うので、ざっくり内容を把握したいとか、ざっくり特徴抽出したいというときはこういう方法も良いと思います。</p> </div><div class="footnote"> <p class="footnote"><a href="#fn-a58ec6ea" name="f-a58ec6ea" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">sklearnが仕様変更しない限り再現できる</span></p> <p class="footnote"><a href="#fn-48bef2bb" name="f-48bef2bb" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">つまりindexと特徴ベクトルの次元が対応</span></p> <p class="footnote"><a href="#fn-fcb40f2a" name="f-fcb40f2a" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">それなりに「まとも」な結果になっているのはTfidfVectorizerのオプションでmin_df=0.03を指定しているからで、これをやらないと見事にdfが低すぎるゴミ単語ばっかり引っかかる結果になる。注意しましょう</span></p> </div> Mon, 09 Jul 2018 19:08:19 +0900 hatenablog://entry/10257846132599601333 python 自然言語処理 sklearn numpy 20newsgroups TfidfVectorizer 特徴抽出 tf-idf 機械学習 CountVectorizer 【python】tfidfは分類精度を向上させるのか?→向上しなかった https://www.haya-programming.com/entry/2018/03/19/125436 <p> 目次</p> <ul class="table-of-contents"> <li><a href="#はじめに長年の疑問">はじめに――長年の疑問</a></li> <li><a href="#検証">検証</a></li> <li><a href="#結果">結果</a></li> <li><a href="#tf-idfは死んだのか">tf-idfは死んだのか?</a></li> <li><a href="#まとめ">まとめ</a></li> </ul> <div class="section"> <h3 id="はじめに長年の疑問">はじめに――長年の疑問</h3> <p> 自然言語処理でテキスト分類などに、よくtf-idfが使われます(最近はそうでもないのかもしれないが)。一般には、tf-idfを使うことで分類精度の向上効果があると認識されているようです。</p><p> このことを長年疑問に思っていました。tf-idfのうち、tfは文書中の単語の出現回数(あるいは相対頻度)ですから、単なるBag of Wordsと変わりません。また、idfは文書全体でのその単語の出現する文書数の対数みたいなものですから、文書集合全体で各単語に1つのidfが定まります。</p><p> けっきょく、tfi-dfはBoWにidfの列ベクトルをかけたものとみなせそうです。ということは、とても単純な線形変換ですから、こんなもので本当に分類精度が上がるんかいな? という疑問をずっと抱いてました。分類器のアルゴリズムによってはある程度効果は期待できるかもしれないが(特に単純なものなら:k近傍法とか)、たとえば確率分布として取り扱うナイーブベイズや、線形変換をかけまくって分類できる軸を探すLDA(Linear Discriminant Analysis:線形判別分析)、あるいは決定木で分類に有効な特徴を探し出すRandomForestのような手法ではまったく効かないんじゃないの、という仮説をずっと考えていました。</p><p> せっかくなので検証してみます。</p><p><span style="font-size: 80%">スポンサーリンク</span><br /> <script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script></p> <p><ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-6261827798827777" data-ad-slot="1744230936" data-ad-format="auto" data-full-width-responsive="true"></ins><br /> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script></p><br /> <p></p> </div> <div class="section"> <h3 id="検証">検証</h3> <p> 検証用のデータは、sklearnのdatasetsから使える20newsgroupsにしました。fetch_20newsgroupsで使えます。</p><p> ただし、このデータは量が多くて(1.1万件ほど)処理を回すのが大変なので、全体のだいたい40%をランダムサンプリングすることにしました。</p><p> また、min_df=0.03, max_df=0.5, stop_words="english"を指定し、予め次元数を501次元にしています。ここがスタートラインです。</p><p> このデータから4種類の方法で特徴量を作りました。</p> <ol> <li>単語の出現回数(CountVectorizerで直接作成)</li> <li>1をl2ノルムで割って正規化したもの</li> <li>tf-idf(TifdfVectorizerで生成。norm=Noneを指定して正規化なしの条件でやる)</li> <li>正規化tf-idf(norm="l2"を指定)</li> </ol><p> これらに対し、以下の分類器で交差検証を回して分類スコアを計算しました。</p> <ul> <li>ナイーブベイズ(Gaussian Naive Bayes)</li> <li>k近傍法(K Nearest Neighbors)</li> <li>線形判別分析(Linear Discriminant Analysis)</li> <li>SVM(Support Vector Machine)</li> <li>ランダムフォレスト(RandomForest Classifier)</li> </ul><p> 以下に検証に使ったソースコードを載せておきます。<div onclick="obj=document.getElementById('oritatami_part').style; obj.display=(obj.display=='none')?'block':'none';"><br /> <a style="cursor:pointer;">▶クリックで展開</a><br /> </div><div id="oritatami_part" style="display:none;clear:both;"></p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># coding: UTF-8</span> <span class="synPreProc">import</span> numpy <span class="synStatement">as</span> np <span class="synPreProc">import</span> pandas <span class="synStatement">as</span> pd <span class="synPreProc">from</span> sklearn.datasets <span class="synPreProc">import</span> fetch_20newsgroups <span class="synPreProc">from</span> sklearn.feature_extraction.text <span class="synPreProc">import</span> CountVectorizer, TfidfVectorizer <span class="synPreProc">from</span> sklearn.ensemble <span class="synPreProc">import</span> RandomForestClassifier <span class="synStatement">as</span> RFC <span class="synPreProc">from</span> sklearn.naive_bayes <span class="synPreProc">import</span> GaussianNB <span class="synStatement">as</span> GNB <span class="synPreProc">from</span> sklearn.neighbors <span class="synPreProc">import</span> KNeighborsClassifier <span class="synStatement">as</span> KNC <span class="synPreProc">from</span> sklearn.discriminant_analysis <span class="synPreProc">import</span> LinearDiscriminantAnalysis <span class="synStatement">as</span> LDA <span class="synPreProc">from</span> sklearn.svm <span class="synPreProc">import</span> SVC <span class="synPreProc">from</span> sklearn.model_selection <span class="synPreProc">import</span> StratifiedKFold <span class="synPreProc">from</span> sklearn.metrics <span class="synPreProc">import</span> precision_recall_fscore_support <span class="synStatement">as</span> prf <span class="synStatement">def</span> <span class="synIdentifier">main</span>(): news20 = fetch_20newsgroups() cv = CountVectorizer(min_df=<span class="synConstant">0.03</span>, max_df=<span class="synConstant">0.5</span>, stop_words=<span class="synConstant">&quot;english&quot;</span>) tfidfv = TfidfVectorizer(min_df=<span class="synConstant">0.03</span>, max_df=<span class="synConstant">0.5</span>, stop_words=<span class="synConstant">&quot;english&quot;</span>, norm=<span class="synIdentifier">None</span>) tfidfv_norm = TfidfVectorizer(min_df=<span class="synConstant">0.03</span>, max_df=<span class="synConstant">0.5</span>, stop_words=<span class="synConstant">&quot;english&quot;</span>, norm=<span class="synConstant">&quot;l2&quot;</span>) count_data = cv.fit_transform(news20.data).toarray() count_norm_data = count_data/np.c_[np.linalg.norm(count_data, axis=<span class="synConstant">1</span>)] tfidf_data = tfidfv.fit_transform(news20.data).toarray() tfidf_norm_data = tfidfv_norm.fit_transform(news20.data).toarray() <span class="synIdentifier">print</span>(count_data.shape) <span class="synIdentifier">print</span>(count_norm_data.shape) <span class="synIdentifier">print</span>(tfidf_data.shape) <span class="synIdentifier">print</span>(tfidf_norm_data.shape) data_idx = np.random.rand(count_data.shape[<span class="synConstant">0</span>]) &gt; <span class="synConstant">0.6</span> count_data = count_data[data_idx] count_norm_data = count_norm_data[data_idx] tfidf_data = tfidf_data[data_idx] tfidf_norm_data = tfidf_norm_data[data_idx] target = news20.target[data_idx] <span class="synIdentifier">print</span>(count_data.shape) <span class="synIdentifier">print</span>(count_norm_data.shape) <span class="synIdentifier">print</span>(tfidf_data.shape) <span class="synIdentifier">print</span>(tfidf_norm_data.shape) nb = GNB() knc = KNC(n_jobs=-<span class="synConstant">1</span>) lda = LDA() svm = SVC(C=<span class="synConstant">10</span>, gamma=<span class="synConstant">0.05</span>) rfc = RFC(n_estimators=<span class="synConstant">500</span>, n_jobs=-<span class="synConstant">1</span>) estimators = <span class="synIdentifier">zip</span>([<span class="synConstant">&quot;nb&quot;</span>, <span class="synConstant">&quot;knc&quot;</span>, <span class="synConstant">&quot;lda&quot;</span>, <span class="synConstant">&quot;svm&quot;</span>, <span class="synConstant">&quot;rfc&quot;</span>], [nb, knc, lda, svm, rfc]) df = pd.DataFrame([], columns=[<span class="synConstant">&quot;classifier&quot;</span>, <span class="synConstant">&quot;data type&quot;</span>, <span class="synConstant">&quot;precision&quot;</span>, <span class="synConstant">&quot;recall&quot;</span>, <span class="synConstant">&quot;F1-measure&quot;</span>]) <span class="synStatement">for</span> cname, clf <span class="synStatement">in</span> estimators: <span class="synStatement">for</span> dname, data <span class="synStatement">in</span> <span class="synIdentifier">zip</span>( [<span class="synConstant">&quot;count&quot;</span>, <span class="synConstant">&quot;count norm&quot;</span>, <span class="synConstant">&quot;tfidf&quot;</span>, <span class="synConstant">&quot;tfidf norm&quot;</span>], [count_data, count_norm_data, tfidf_data, tfidf_norm_data]): trues = [] preds = [] <span class="synStatement">for</span> train_index, test_index <span class="synStatement">in</span> StratifiedKFold( shuffle=<span class="synIdentifier">True</span>, random_state=<span class="synConstant">0</span>).split(data, target): clf.fit(data[train_index], target[train_index]) trues.append(target[test_index]) preds.append(clf.predict(data[test_index])) score = prf(np.hstack(trues), np.hstack(preds), average=<span class="synConstant">&quot;macro&quot;</span>)[:-<span class="synConstant">1</span>] <span class="synIdentifier">print</span>(cname, dname) <span class="synIdentifier">print</span>(<span class="synConstant">&quot;p:{0:.6f} r:{1:.6f} f1:{2:.6f}&quot;</span>.<span class="synIdentifier">format</span>(*score)) s = pd.Series([cname, dname, *score], index=df.columns) df = df.append(s, ignore_index=<span class="synIdentifier">True</span>) <span class="synIdentifier">print</span>(df) <span class="synIdentifier">print</span>(df.to_latex()) <span class="synStatement">if</span> __name__ == <span class="synConstant">&quot;__main__&quot;</span>: main() </pre><p></div></p> </div> <div class="section"> <h3 id="結果">結果</h3> <p> 次のような結果になりました。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hayataka2049/20180319/20180319124128.png" alt="f:id:hayataka2049:20180319124128p:plain" title="f:id:hayataka2049:20180319124128p:plain" class="hatena-fotolife" itemprop="image"></span></p><p> 分類器別に端的にまとめると、</p> <ul> <li>ナイーブベイズ:正規化効果あり。tf-idf効果なし</li> <li>k近傍:正規化、tf-idfともに効果あり</li> <li>線形判別分析:正規化効果あり。tf-idf効果なし</li> <li>SVM:正規化効果あり。正規化なしtf-idfまったくダメ。正規化+tf-idfは効いてる可能性あり</li> <li>ランダムフォレスト:正規化、tf-idfともに恐らく効果なし。効果があるとしても1%以下</li> </ul><p> という結果になりました。要するに、</p> <ol> <li>tf-idfは基本的に無力</li> <li>tf-idfをする暇があったらl2ノルムで割る。こっちの方が効く</li> <li>ただし一番精度を出せてるRandomForestでは、l2ノルムで割る正規化すら大して効いていないので、どこまで意味があるかは正直微妙</li> </ol><p> という結論です。</p> </div> <div class="section"> <h3 id="tf-idfは死んだのか">tf-idfは死んだのか?</h3> <p> 少なくとも文書分類における特徴抽出手法としては死んだ、と言って構わないでしょう。</p><p> このことは仮説の通りだったので、驚きはあまりないです。tf-idfは『分類精度を上げる目的』ではほとんど使えないというのが結論です。</p><p> ではなぜtf-idfがこれまでもてはやされてきたのか? 相対頻度への変換や正規化によって、生BoW(単語の出現回数数えただけ)より良い結果が得られてきたためではないでしょうか。肝心のidfによる重み付けは「ちっとも意味がない」と言わざるを得ないと思います。</p><p> では、tf-idfは使えない子なのか? 分類に使う特徴量としては上記の通り無意味ですが、tf-idfは特徴語抽出に使えます。tf-idf(の文書集合内の平均)が高すぎず低すぎない単語を抜き出すことで、文書の特徴をよく表す単語を抽出するという使い方です。これは教師なしで計算の軽い特徴選択手法として利用できますから、そっちでは役に立つでしょう、たぶん。</p> </div> <div class="section"> <h3 id="まとめ">まとめ</h3> <p> 思った通り向上しませんでした。</p> </div> Mon, 19 Mar 2018 12:54:36 +0900 hatenablog://entry/17391345971627156012 python 自然言語処理 sklearn 20newsgroups CountVectorizer TfidfVectorizer tf-idf 特徴抽出 機械学習 ランダムフォレスト 【python】sklearnのCountVectorizerの使い方 https://www.haya-programming.com/entry/2018/02/25/044525 <p> sklearnのCountVectorizerを使うとBoW(Bag of Words)の特徴量が簡単に作れます。</p><p> ただし、指定するパラメタが多かったり、デフォルトで英語の文字列を想定していたりして若干とっつきづらい部分もあります。</p><p> この記事ではCountVectorizerの使い方を簡単に説明します。</p><p>参考 sklearn公式ページ<br /> <a href="http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html">sklearn.feature_extraction.text.CountVectorizer &mdash; scikit-learn 0.20.1 documentation</a></p><p> 目次</p> <ul class="table-of-contents"> <li><a href="#何も考えずに使う">何も考えずに使う</a></li> <li><a href="#出現頻度の低すぎる高すぎる単語を消す">出現頻度の低すぎる・高すぎる単語を消す</a></li> <li><a href="#stop-wordの除去">stop wordの除去</a></li> <li><a href="#n-gramの特徴量にする">n-gramの特徴量にする</a></li> <li><a href="#名詞だけでBoWを作る更にstemmingも行う">名詞だけでBoWを作る。更にstemmingも行う</a></li> <li><a href="#日本語で使う">日本語で使う</a></li> <li><a href="#似たようなもの">似たようなもの</a></li> <li><a href="#まとめ">まとめ</a></li> </ul><p><span style="font-size: 80%">スポンサーリンク</span><br /> <script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script></p> <p><ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-6261827798827777" data-ad-slot="1744230936" data-ad-format="auto" data-full-width-responsive="true"></ins><br /> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script></p><br /> <p></p> <div class="section"> <h3 id="何も考えずに使う">何も考えずに使う</h3> <p> 英語の入力文なら何も考えずに使うことも可能です。とりあえず入力データとして文字列のリストを作る必要があるので、<a href="https://en.wikipedia.org/wiki/Python_(programming_language)">python&#x306E;&#x82F1;&#x8A9E;&#x7248;wikipedia</a>の冒頭の文章を使うことにします。無駄な脚注を取り除き、一文ずつ改行します。</p> <blockquote> <p>Python is an interpreted high-level programming language for general-purpose programming.<br /> Created by Guido van Rossum and first released in 1991, Python has a design philosophy that emphasizes code readability, and a syntax that allows programmers to express concepts in fewer lines of code,notably using significant whitespace.<br /> It provides constructs that enable clear programming on both small and large scales.<br /> Python features a dynamic type system and automatic memory management.<br /> It supports multiple programming paradigms, including object-oriented, imperative, functional and procedural, and has a large and comprehensive standard library.<br /> Python interpreters are available for many operating systems.<br /> CPython, the reference implementation of Python, is open source software and has a community-based development model, as do nearly all of its variant implementations.<br /> CPython is managed by the non-profit Python Software Foundation.</p> </blockquote> <p> これをsource.txtというファイル名で適当なディレクトリに保存し、そのディレクトリ上のシェルでpythonインタプリタを起動します。このファイルを読み込み、改行でsplitしてリストを作ります。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; source_list = [x <span class="synStatement">for</span> x <span class="synStatement">in</span> txt.split(<span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>) <span class="synStatement">if</span> x != <span class="synConstant">&quot;&quot;</span>] &gt;&gt;&gt; <span class="synStatement">with</span> <span class="synIdentifier">open</span>(<span class="synConstant">&quot;source.txt&quot;</span>, <span class="synConstant">&quot;r&quot;</span>) <span class="synStatement">as</span> f: ... txt = f.read() ... &gt;&gt;&gt; source_list = [x <span class="synStatement">for</span> x <span class="synStatement">in</span> txt.split(<span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>) <span class="synStatement">if</span> x != <span class="synConstant">&quot;&quot;</span>] </pre><p> ファイル末尾の改行のせいで空文字列が入るので、対策をしています。</p><p> 後はCountVectorizerをimportし、インスタンス化してfit_transform一発でDocument-Term Matrixが得られます。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; <span class="synPreProc">from</span> sklearn.feature_extraction.text <span class="synPreProc">import</span> CountVectorizer <span class="synStatement">as</span> CV &gt;&gt;&gt; cv = CV() &gt;&gt;&gt; matrix = cv.fit_transform(source_list) &gt;&gt;&gt; matrix &lt;8x98 sparse matrix of <span class="synIdentifier">type</span> <span class="synConstant">'&lt;class '</span>numpy.int64<span class="synConstant">'&gt;'</span> <span class="synStatement">with</span> <span class="synConstant">122</span> stored elements <span class="synStatement">in</span> Compressed Sparse Row <span class="synIdentifier">format</span>&gt; </pre><p> おおっ、spicyのsparse matrixを吐きやがった! と思った人は正しいです。これは仕様なので仕方ありません。嫌なら.toarray()してnumpy配列に変換してください。sparse matrixの方がありがたいときとnumpy配列の方がありがたいとき、どちらもあるので、どっちにしておくのが良いかは一概には言えません。</p><p> とりあえず形は8*98ということで、確かに8行のテキストなので上手くいっているようです。全データ中の異なり語数は98となり、100次元弱のBoW特徴量が得られました。</p> </div> <div class="section"> <h3 id="出現頻度の低すぎる高すぎる単語を消す">出現頻度の低すぎる・高すぎる単語を消す</h3> <p> 全文書中に1回とか2回しか出てこない単語、要らないですよね<a href="#f-07bf22b9" name="fn-07bf22b9" title="今回はデータが小さいのでそうも言い切れない部分があるが・・・">*1</a>。逆に、全文書にまんべんなく出現する単語も要らない気がします<a href="#f-57c65077" name="fn-57c65077" title="これはタスク依存。著者推定のようなタスクではまんべんなく出現する単語の頻度を見るので重要だったりする">*2</a>。</p><p> CountVectorizerにはmin_df,max_dfというパラメータがあります。dfはDocument Frequencyのことで、tf-idfのアレです。要するに(何回出てくるかは置いておいて)全文書中の何%にその単語が出現するかの指標です。それを使って特徴をフィルタリングできます。</p><p> 今回は8文書なので、うっかり変な数字を指定するとまったく効果がなかったり、何も残らなかったりするのが難しいところです。とりあえず出現する文書が2文書以上、6文書以下くらいの特徴を取ってみることにします。min_df=2/8=0.25, max_df=6/8=0.75とすれば良さそうですが、比較がgreater | less than or equalなのか単にgreater | less thanなのかよくわからないので、安全を見てmin_df=0.24, max_df=0.76としておきます。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; cv = CV(min_df=<span class="synConstant">0.24</span>, max_df=<span class="synConstant">0.76</span>) &gt;&gt;&gt; matrix = cv.fit_transform(source_list) &gt;&gt;&gt; matrix &lt;8x14 sparse matrix of <span class="synIdentifier">type</span> <span class="synConstant">'&lt;class '</span>numpy.int64<span class="synConstant">'&gt;'</span> <span class="synStatement">with</span> <span class="synConstant">38</span> stored elements <span class="synStatement">in</span> Compressed Sparse Row <span class="synIdentifier">format</span>&gt; </pre><p> 14次元まで減りました。特徴の名前(残っている単語)を見てみます。リストの0個目の単語が0次元目の特徴に・・・という形で対応しているはずです(たぶん)。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; cv.get_feature_names() [<span class="synConstant">'and'</span>, <span class="synConstant">'by'</span>, <span class="synConstant">'cpython'</span>, <span class="synConstant">'for'</span>, <span class="synConstant">'has'</span>, <span class="synConstant">'is'</span>, <span class="synConstant">'it'</span>, <span class="synConstant">'large'</span>, <span class="synConstant">'of'</span>, <span class="synConstant">'programming'</span>, <span class="synConstant">'python'</span>, <span class="synConstant">'software'</span>, <span class="synConstant">'that'</span>, <span class="synConstant">'the'</span>] </pre><p> たぶんこんなものでしょう。</p> </div> <div class="section"> <h3 id="stop-wordの除去">stop wordの除去</h3> <p> byとかforとかthatとか要らないですよね<a href="#f-7e8bc9cf" name="fn-7e8bc9cf" title="ぶっちゃけタスク依存(ry">*3</a>。stop_wordsというパラメータがあり、「こんなの要らないよ」って単語のリストを渡すと除去してくれます。また、文字列"english"を渡すこともでき、その場合は「built-in stop word list for English」を使ってくれます。凄い。ちなみに「built-in stop word list for Japanese」はありません。残念。</p><p> とりあえず"english"を指定してみます。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; cv = CV(min_df=<span class="synConstant">0.24</span>, max_df=<span class="synConstant">0.76</span>, stop_words=<span class="synConstant">&quot;english&quot;</span>) &gt;&gt;&gt; matrix = cv.fit_transform(source_list) &gt;&gt;&gt; cv.get_feature_names() [<span class="synConstant">'cpython'</span>, <span class="synConstant">'large'</span>, <span class="synConstant">'programming'</span>, <span class="synConstant">'python'</span>, <span class="synConstant">'software'</span>] </pre><p> 思ったより何も残らなかったので、min_dfを下げてみます。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; cv = CV(min_df=<span class="synConstant">0.12</span>, max_df=<span class="synConstant">0.76</span>, stop_words=<span class="synConstant">&quot;english&quot;</span>) &gt;&gt;&gt; matrix = cv.fit_transform(source_list) &gt;&gt;&gt; cv.get_feature_names() [<span class="synConstant">'1991'</span>, <span class="synConstant">'allows'</span>, <span class="synConstant">'automatic'</span>, <span class="synConstant">'available'</span>, <span class="synConstant">'based'</span>, <span class="synConstant">'clear'</span>, <span class="synConstant">'code'</span>, <span class="synConstant">'community'</span>, <span class="synConstant">'comprehensive'</span>, <span class="synConstant">'concepts'</span>, <span class="synConstant">'constructs'</span>, <span class="synConstant">'cpython'</span>, <span class="synConstant">'created'</span>, <span class="synConstant">'design'</span>, <span class="synConstant">'development'</span>, <span class="synConstant">'dynamic'</span>, <span class="synConstant">'emphasizes'</span>, <span class="synConstant">'enable'</span>, <span class="synConstant">'express'</span>, <span class="synConstant">'features'</span>, <span class="synConstant">'fewer'</span>, <span class="synConstant">'foundation'</span>, <span class="synConstant">'functional'</span>, <span class="synConstant">'general'</span>, <span class="synConstant">'guido'</span>, <span class="synConstant">'high'</span>, <span class="synConstant">'imperative'</span>, <span class="synConstant">'implementation'</span>, <span class="synConstant">'implementations'</span>, <span class="synConstant">'including'</span>, <span class="synConstant">'interpreted'</span>, <span class="synConstant">'interpreters'</span>, <span class="synConstant">'language'</span>, <span class="synConstant">'large'</span>, <span class="synConstant">'level'</span>, <span class="synConstant">'library'</span>, <span class="synConstant">'lines'</span>, <span class="synConstant">'managed'</span>, <span class="synConstant">'management'</span>, <span class="synConstant">'memory'</span>, <span class="synConstant">'model'</span>, <span class="synConstant">'multiple'</span>, <span class="synConstant">'nearly'</span>, <span class="synConstant">'non'</span>, <span class="synConstant">'notably'</span>, <span class="synConstant">'object'</span>, <span class="synConstant">'open'</span>, <span class="synConstant">'operating'</span>, <span class="synConstant">'oriented'</span>, <span class="synConstant">'paradigms'</span>, <span class="synConstant">'philosophy'</span>, <span class="synConstant">'procedural'</span>, <span class="synConstant">'profit'</span>, <span class="synConstant">'programmers'</span>, <span class="synConstant">'programming'</span>, <span class="synConstant">'provides'</span>, <span class="synConstant">'purpose'</span>, <span class="synConstant">'python'</span>, <span class="synConstant">'readability'</span>, <span class="synConstant">'reference'</span>, <span class="synConstant">'released'</span>, <span class="synConstant">'rossum'</span>, <span class="synConstant">'scales'</span>, <span class="synConstant">'significant'</span>, <span class="synConstant">'small'</span>, <span class="synConstant">'software'</span>, <span class="synConstant">'source'</span>, <span class="synConstant">'standard'</span>, <span class="synConstant">'supports'</span>, <span class="synConstant">'syntax'</span>, <span class="synConstant">'systems'</span>, <span class="synConstant">'type'</span>, <span class="synConstant">'using'</span>, <span class="synConstant">'van'</span>, <span class="synConstant">'variant'</span>, <span class="synConstant">'whitespace'</span>] </pre><p> ソースリストの下のスクロールバーが凄いことになってますが、どうせ誰も見たくもないでしょうし、対策はしていません。見たい人は頑張ってスクロールしてください。とりあえずこんなものだろうという結果は得られました。</p> </div> <div class="section"> <h3 id="n-gramの特徴量にする">n-gramの特徴量にする</h3> <p> ngram_rangeというパラメータがあります。これはタプルで渡す必要があり、(1,1)とか(1,2)といった風に指定します。</p> <blockquote> <p>The lower and upper boundary of the range of n-values for different n-grams to be extracted. All values of n such that min_n <= n <= max_n will be used.</p> </blockquote> <p> 要するに(1,1)なら1-gram(ただの単語), (1,2)なら1-gramと2-gram、(1,3)なら1~3-gram、(2,3)なら2~3-gramという形でぜんぶ作り、まとめて一つの特徴空間にしてくれるようです。(1,2)を試してみます。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; cv = CV(ngram_range=(<span class="synConstant">1</span>,<span class="synConstant">2</span>)) &gt;&gt;&gt; cv = CV(ngram_range=(<span class="synConstant">1</span>,<span class="synConstant">2</span>)) &gt;&gt;&gt; matrix = cv.fit_transform(source_list) &gt;&gt;&gt; cv.get_feature_names() [<span class="synConstant">'1991'</span>, <span class="synConstant">'1991 python'</span>, <span class="synConstant">'all'</span>, <span class="synConstant">'all of'</span>, <span class="synConstant">'allows'</span>, <span class="synConstant">'allows programmers'</span>,... <span class="synComment"># 多いので途中で省略</span> </pre><p> 期待通り動いているようです。</p> </div> <div class="section"> <h3 id="名詞だけでBoWを作る更にstemmingも行う">名詞だけでBoWを作る。更にstemmingも行う</h3> <p> これはCountVectorizerだけではできません(CountVectorizer内部でPOS taggingを行っていないため)。</p><p> そこでnltkを使います。まず、次のような関数を定義します。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; <span class="synStatement">def</span> <span class="synIdentifier">noun_stem_analyzer</span>(string): ... st = nltk.stem.lancaster.LancasterStemmer() ... <span class="synStatement">return</span> [st.stem(word) <span class="synStatement">for</span> word, pos <span class="synStatement">in</span> nltk.pos_tag( ... nltk.word_tokenize(string)) <span class="synStatement">if</span> pos == <span class="synConstant">&quot;NN&quot;</span>] ... </pre><p> nltkを入れていない人は入れてください。また、一回目の呼び出しでは処理に必要なリソースがないというエラーが出るので、エラーメッセージの案内通りにコマンドを打ち、リソースをダウンロードしてください。</p><p> 使ってみます。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; string = <span class="synConstant">&quot;Python is an interpreted high-level programming language for general-purpose programming.&quot;</span> &gt;&gt;&gt; noun_stem_analyzer(string) [<span class="synConstant">'high-level'</span>, <span class="synConstant">'program'</span>, <span class="synConstant">'langu'</span>, <span class="synConstant">'program'</span>] </pre><p> pythonが入ってないのが微妙なので、POSタグをちゃんと見てみます。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; nltk.pos_tag(nltk.word_tokenize(string)) [(<span class="synConstant">'Python'</span>, <span class="synConstant">'NNP'</span>), (<span class="synConstant">'is'</span>, <span class="synConstant">'VBZ'</span>), (<span class="synConstant">'an'</span>, <span class="synConstant">'DT'</span>), (<span class="synConstant">'interpreted'</span>, <span class="synConstant">'JJ'</span>), (<span class="synConstant">'high-level'</span>, <span class="synConstant">'NN'</span>), (<span class="synConstant">'programming'</span>, <span class="synConstant">'NN'</span>), (<span class="synConstant">'language'</span>, <span class="synConstant">'NN'</span>), (<span class="synConstant">'for'</span>, <span class="synConstant">'IN'</span>), (<span class="synConstant">'general-purpose'</span>, <span class="synConstant">'JJ'</span>), (<span class="synConstant">'programming'</span>, <span class="synConstant">'NN'</span>), (<span class="synConstant">'.'</span>, <span class="synConstant">'.'</span>)] </pre><p> NNPは固有名詞・・・かな。これを踏まえて関数を修正。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; <span class="synStatement">def</span> <span class="synIdentifier">noun_stem_analyzer</span>(string): ... st = nltk.stem.lancaster.LancasterStemmer() ... <span class="synStatement">return</span> [st.stem(word) <span class="synStatement">for</span> word, pos <span class="synStatement">in</span> nltk.pos_tag( ... nltk.word_tokenize(string)) <span class="synStatement">if</span> pos == <span class="synConstant">&quot;NN&quot;</span> <span class="synStatement">or</span> pos == <span class="synConstant">&quot;NNP&quot;</span>] ... &gt;&gt;&gt; noun_stem_analyzer(string) [<span class="synConstant">'python'</span>, <span class="synConstant">'high-level'</span>, <span class="synConstant">'program'</span>, <span class="synConstant">'langu'</span>, <span class="synConstant">'program'</span>] </pre><p> これなら期待通りです。stemmingの結果に若干納得できないような気もしますが、今回はこのまま行きます。</p><p> この関数をどうやってCountVectorizerと組み合わせて使うのかというと、analyzer引数に渡してあげます。</p> <pre class="code" data-lang="" data-unlink>&gt;&gt;&gt; cv = CV(analyzer=noun_stem_analyzer) &gt;&gt;&gt; matrix = cv.fit_transform(source_list) &gt;&gt;&gt; cv.get_feature_names() [&#39;cod&#39;, &#39;cpython&#39;, &#39;design&#39;, &#39;develop&#39;, &#39;found&#39;, &#39;guido&#39;, &#39;high-level&#39;, &#39;impl&#39;, &#39;langu&#39;, &#39;libr&#39;, &#39;man&#39;, &#39;mem&#39;, &#39;model&#39;, &#39;paradigm&#39;, &#39;philosoph&#39;, &#39;program&#39;, &#39;python&#39;, &#39;read&#39;, &#39;ref&#39;, &#39;ross&#39;, &#39;softw&#39;, &#39;sourc&#39;, &#39;standard&#39;, &#39;syntax&#39;, &#39;system&#39;, &#39;typ&#39;, &#39;van&#39;, &#39;whitespac&#39;]</pre><p> こうやって使える訳です。</p><p> ちなみに、似たような引数にpreprocessorとtokenizerがあります。ありますが、ドキュメントを何回読んでもなんとなくしかわからなかったので、説明はしません。とりあえず、analyzerを指定すれば大抵の場合問題はないでしょう。</p><p> なお、analyzerはcallableならなんでも渡せるので、たとえば(lambda x:x)を渡し、</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; cv = CV(analyzer=<span class="synStatement">lambda</span> x:x) &gt;&gt;&gt; matrix = cv.fit_transform( ... [noun_stem_analyzer(string) <span class="synStatement">for</span> string <span class="synStatement">in</span> source_list]) </pre><p> こうしても上と同じ結果になります。どうしてわざわざこんなことを書いたのかというと、これを使ってテキストの前処理を事前にまとめて行っておくという方針が使えるからです。実際にテキスト分析をやったことのある方ならご存知かと思いますが、大量のデータに形態素解析などをかけるのはそれ自体けっこうヘビーな処理になるので、一度データを丸ごと形態素解析してファイルにダンプするとか、DBに入れるとかして処理を行うことが多い訳です。そういうデータも、わざわざ分かち書きに戻したりしなくても上記の方法で解析できます。</p> </div> <div class="section"> <h3 id="日本語で使う">日本語で使う</h3> <p> 上の例を見て分かる通り、analyzerには好きなものが渡せます。ということは、日本語形態素解析器を突っ込んでやればCountVectorizerは日本語でも使える訳です。<a href="https://ja.wikipedia.org/wiki/Python">python&#x306E;&#x65E5;&#x672C;&#x8A9E;&#x7248;wikipedia</a>から以下の文章を取ってきました。</p> <blockquote> <p>Python(パイソン)は、汎用のプログラミング言語である。<br /> コードがシンプルで扱いやすく設計されており、C言語などに比べて、さまざまなプログラムを分かりやすく、少ないコード行数で書けるといった特徴がある。<br /> 文法を極力単純化してコードの可読性を高め、読みやすく、また書きやすくしてプログラマの作業性とコードの信頼性を高めることを重視してデザインされた、汎用の高水準言語である。<br /> 反面、実行速度はCに比べて犠牲にされている。<br /> 核となる本体部分は必要最小限に抑えられている。<br /> 一方で標準ライブラリやサードパーティ製のライブラリ、関数など、さまざまな領域に特化した豊富で大規模なツール群が用意され、インターネット上から無料で入手でき、自らの使用目的に応じて機能を拡張してゆくことができる。<br /> またPythonは多くのハードウェアとOS (プラットフォーム) に対応しており、複数のプログラミングパラダイムに対応している。<br /> Pythonはオブジェクト指向、命令型、手続き型、関数型などの形式でプログラムを書くことができる。</p> </blockquote> <p> source2.txtとして保存し、さきほどと同様に読み込みます。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; <span class="synStatement">with</span> <span class="synIdentifier">open</span>(<span class="synConstant">&quot;source2.txt&quot;</span>, <span class="synConstant">&quot;r&quot;</span>) <span class="synStatement">as</span> f: ... txt = f.read() ... &gt;&gt;&gt; source2_list = [x <span class="synStatement">for</span> x <span class="synStatement">in</span> txt.split(<span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>) <span class="synStatement">if</span> x != <span class="synConstant">&quot;&quot;</span>] </pre><p> 日本語形態素解析器にはMeCabを使います。次のようにanalyzerを定義します。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; <span class="synPreProc">import</span> MeCab &gt;&gt;&gt; tagger = MeCab.Tagger(<span class="synConstant">&quot;&quot;</span>) &gt;&gt;&gt; <span class="synStatement">def</span> <span class="synIdentifier">japanese_analyzer</span>(string): ... result_list = [] ... <span class="synStatement">for</span> line <span class="synStatement">in</span> tagger.parse(string).split(<span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>): ... splited_line = line.split(<span class="synConstant">&quot;</span><span class="synSpecial">\t</span><span class="synConstant">&quot;</span>) ... <span class="synStatement">if</span> <span class="synIdentifier">len</span>(splited_line) &gt;= <span class="synConstant">2</span> <span class="synStatement">and</span> <span class="synConstant">&quot;名詞&quot;</span> <span class="synStatement">in</span> splited_line[<span class="synConstant">1</span>]: ... result_list.append(splited_line[<span class="synConstant">0</span>]) ... <span class="synStatement">return</span> result_list </pre><p> 色々妥協して書いたので、このコードは実用的な用途には転用しないでください(する人もいないだろうけど)。とにかくCountVectorizerにこれを入れます。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; cv = CV(analyzer=japanese_analyzer) &gt;&gt;&gt; matrix = cv.fit_transform(source2_list) &gt;&gt;&gt; cv.get_feature_names() [<span class="synConstant">'('</span>, <span class="synConstant">')'</span>, <span class="synConstant">'C'</span>, <span class="synConstant">'OS'</span>, <span class="synConstant">'Python'</span>, <span class="synConstant">'こと'</span>, <span class="synConstant">'さまざま'</span>, <span class="synConstant">'インターネット'</span>, <span class="synConstant">'オブジェクト'</span>, <span class="synConstant">'コード'</span>, <span class="synConstant">'サード'</span>, <span class="synConstant">'シンプル'</span>, <span class="synConstant">'ツール'</span>, <span class="synConstant">'デザイン'</span>, <span class="synConstant">'ハードウェア'</span>, <span class="synConstant">'パイソン'</span>, <span class="synConstant">'パーティ'</span>, <span class="synConstant">'プラットフォーム'</span>, <span class="synConstant">'プログラマ'</span>, <span class="synConstant">'プログラミング'</span>, <span class="synConstant">'プログラミングパラダイム'</span>, <span class="synConstant">'プログラム'</span>, <span class="synConstant">'ライブラリ'</span>, <span class="synConstant">'上'</span>, <span class="synConstant">'作業'</span>, <span class="synConstant">'使用'</span>, <span class="synConstant">'信頼'</span>, <span class="synConstant">'入手'</span>, <span class="synConstant">'化'</span>, <span class="synConstant">'単純'</span>, <span class="synConstant">'可読性'</span>, <span class="synConstant">'命令'</span>, <span class="synConstant">'型'</span>, <span class="synConstant">'多く'</span>, <span class="synConstant">'大'</span>, <span class="synConstant">'実行'</span>, <span class="synConstant">'対応'</span>, <span class="synConstant">'形式'</span>, <span class="synConstant">'必要'</span>, <span class="synConstant">'性'</span>, <span class="synConstant">'手続き'</span>, <span class="synConstant">'拡張'</span>, <span class="synConstant">'指向'</span>, <span class="synConstant">'数'</span>, <span class="synConstant">'文法'</span>, <span class="synConstant">'最小限'</span>, <span class="synConstant">'本体'</span>, <span class="synConstant">'核'</span>, <span class="synConstant">'標準'</span>, <span class="synConstant">'機能'</span>, <span class="synConstant">'汎用'</span>, <span class="synConstant">'無料'</span>, <span class="synConstant">'特'</span>, <span class="synConstant">'特徴'</span>, <span class="synConstant">'犠牲'</span>, <span class="synConstant">'用意'</span>, <span class="synConstant">'目的'</span>, <span class="synConstant">'群'</span>, <span class="synConstant">'自ら'</span>, <span class="synConstant">'行'</span>, <span class="synConstant">'製'</span>, <span class="synConstant">'複数'</span>, <span class="synConstant">'規模'</span>, <span class="synConstant">'言語'</span>, <span class="synConstant">'設計'</span>, <span class="synConstant">'豊富'</span>, <span class="synConstant">'速度'</span>, <span class="synConstant">'部分'</span>, <span class="synConstant">'重視'</span>, <span class="synConstant">'関数'</span>, <span class="synConstant">'領域'</span>, <span class="synConstant">'高水準'</span>] </pre><p> 最初の半角カッコが目立ちますが、mecabのデフォルトの挙動では半角記号は「名詞,サ変接続」に割り当てるのでこれで間違っていません。それを除けば、それほど悪くない感じになっていると思います。</p> </div> <div class="section"> <h3 id="似たようなもの">似たようなもの</h3> <p> CountVectorizerに似たものとして、</p> <ul> <li>TfidfVectorizer</li> <li>HashingVectorizer</li> </ul><p> があります。</p><p> 参考 公式ドキュメント<br /> <a href="http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html">sklearn.feature_extraction.text.TfidfVectorizer &mdash; scikit-learn 0.20.1 documentation</a><br /> <a href="http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.HashingVectorizer.html">sklearn.feature_extraction.text.HashingVectorizer &mdash; scikit-learn 0.20.1 documentation</a></p><p> Tfidfの方はその名の通り、出力される行列をidfで重み付けします。Hashingの方ではfeature hashingという手法を使い、次元数が膨れ上がるのを抑制してくれるようです。</p> </div> <div class="section"> <h3 id="まとめ">まとめ</h3> <p> 素晴らしく簡単に使えます。テキストの特徴量が必要になったときには、使ってみては如何でしょうか。</p> </div><div class="footnote"> <p class="footnote"><a href="#fn-07bf22b9" name="f-07bf22b9" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">今回はデータが小さいのでそうも言い切れない部分があるが・・・</span></p> <p class="footnote"><a href="#fn-57c65077" name="f-57c65077" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">これはタスク依存。著者推定のようなタスクではまんべんなく出現する単語の頻度を見るので重要だったりする</span></p> <p class="footnote"><a href="#fn-7e8bc9cf" name="f-7e8bc9cf" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">ぶっちゃけタスク依存(ry</span></p> </div> Sun, 25 Feb 2018 04:45:25 +0900 hatenablog://entry/17391345971619444522 python sklearn 自然言語処理 mecab CountVectorizer TfidfVectorizer 特徴抽出 tf-idf 機械学習