初学者の統計学習ノート統計学およびR、Pythonでのプログラミングの勉強の過程をメモっていくノート。機械学習』という教科書を、半年ぐらい前に3分の1ぐらい読んで、内容をまとめたりはせずにほっといたのですが、このたび実際にRでコードを写経しながら走らせてみたりしたので、学習ノートとしてエントリを起こしておこうかと思います。見る機械学習の入門的な教科書としては、理論の本として『わかパタ』『続・わかパタ』をそれぞれ途中まで読んだという中途半端な状態ですがw、コーディングの本としては、RでやるものとPythonでやるものを1冊ずつぐらいやろうかなと思っておりました。それでRのものとして本書を買ったわけなんですが、Amazonのレビューを見ると分かる通り、不親切な部分とか細かい間違いがあって*1、困った教科書です......。演習で使われるサンプルデータがどこにあるのか最初わからなかったし、図表の参照番号が間違っていたり、「サンプルコードの◯行目に書いてある通りです」みたいな指示があってもその行数が全然違ったりと、散々です。今回のエントリは主に第3章の学習ノートなのですが、教科書どおりにコードを書いても教科書に書かれてあるような結果には絶対ならないという、読者を不安にさせる教科書です。教科書としてのカバー範囲はおもしろくて、ナイーブベイズ分類器でスパムフィルタを作ったり、教師なし学習で株価指標をつくったり、ページビューを予測したり、レコメンドのプログラムを書いたり、ソーシャルグラフを分析したりとテーマが幅広く素晴らしい。文章での解説自体は、すごく分かりやすいんです。いう目的で使われる、どういうアルゴリズムなのかが、書学者でもイメージできるように書いてある。校閲の手を抜きすぎで読者を疲れさせるところがあるのがもったいないですね。忍耐力と暇のない人にはあまりオススメできない教科書かもしれないです。間違いを見つけて正すことも含めて「スキル向上のための試練である」と捉えられるぐらいの前向きな性格が求められます。繰り返しですが、文章自体は分かりやすいので、コードを実際に動かしてみるとかせずに単に読みものとしてパラパラみていく分には、いいと思います。今後の計画としては、やる気が失せなければ本書を先の章へ進めていくこととし、失せた場合はPythonの『集合知プログラミング』に切り替えようかと思います(『集合知プログラミング』は私の場合、Pythonのコーディングに慣れていないので、1章進むのにすごい時間かかると思いますが)。第1章の冒頭では、Rで機械学習を学ぶことの利点が説かれていました。要するに、さすが統計処理に関しては簡単に扱えるからというような話でした。Pythonのような規模の大きい科学計算のコミュニティで使われる他の言語においてlmと同じ機能を実現するには、データを表現するためのライブラリ(NumPy)、分析を行うライブラリ(SciPy)、結果を可視化するライブラリ(matplotlib)といったように複数のサードパーティライブラリを使わなければならない。本章で見ていくように、Rではこうした高性能な分析をたった1行で書くことができる。(p.3)第1章はRの使用方法を解説するために、UFOの目撃情報のデータを分析しています。Hackers」となっているように、もともとプログラマ向けの教科書なので、他の言語をすでに習得しているプログラマ向けにRの雰囲気を伝えるような内容で、R入門といった感じではないです。第2章ではRで基本的な統計量を算出したり、データを可視化したりするための操作が紹介されています。1章でも2章でもggplot2が使われていて、後続の章でもグラフを書くときは常にggplot2を使っていますね。Classifier)ってやつで、スパムメールを特定するフィルタのプログラムを書くという演習です。どのような意味でベイズ推定なのかみたいな説明はほとんどなく、ベイズの定理も出てこないので、単純ベイズ分類器というのがどういうものなのかはググるなりして理解した上で読まないと、計算内容(といってもめちゃめちゃ単純ですが)が意味不明になります。Wikipediaの解説は分かりやすく、「設計も仮定も非常に単純であるにも関わらず、単純ベイズ分類器は複雑な実世界の状況において、期待よりもずっとうまく働く」と書かれてありました。本章の演習の全体としては、「スパムであることが分かっているメールのデータ」と「スパムでないことが分かっているメールのデータ」が大量にあって、スパム・非スパムそれぞれのデータの一部を訓練データとして分類器に学習させ、残ったデータについてスパムかどうかを判定させて性能を確認するという流れになります。教科書の中では、演習に使うメールのデータはspamassasinというサイトに載っている公開コーパスのデータを使ってると書いてあるんですが、このサイトからデータを全部ダウンロードして使おうとしても、教科書で使っているデータがどれなのか分からなくなります。248通)という6つのメール群が使われるんですが、上記サイトには新しいデータと古いデータが混じっており、「spam」が2つあったりしてどっちを使えばいいかわからないわけです。Githubに置かれている本書のサポートのデータを使えばいいです。GitHubこれをまとめてダウンロードすると、各章のデータとサンプルコードが全て手に入ります。メールの中身は↓こんな感じです。スパム分類器作成の流れ上記メール群はそれぞれ1つのフォルダになっていて、中には1メール1ファイルで拡張子なしのテキストファイルが通数分入っています。本章で書くプログラムの処理の流れを、以下のとおりまとめておきます。spamのメールとeasy_hamのメールを訓練データとする。1個1個のメールのファイルにはheaderとbodyが含まれてるので、body(本文)だけを取り出すことを考える。具体的には、headerとbodyの間には必ず空行があって、しかもそれはファイル中で最初の空行であるというルールに着目して、「最初の空行の1行下以降の全てのテキスト」を取り出すという処理を行う。1つのフォルダ(まずはspamフォルダ)中のすべてのメールの本文を取り出して、1本文1要素としてベクトルに格納する。Matrix)という行列を作る。行が単語(term)で列が各メール(document)になっており、各メールにその単語が登場した回数が値として入っているもの。単語は、2回以上登場したものだけを集計している。spamフォルダなので、この単語群が「スパム語」(スパムメールに含まれる単語)の候補となる。単語ごとに、登場回数を合計する(つまりTDMを横に足す)。これをfrequencyとする。単純に、スパムメールに多用されている語が何なのかが分かる。各スパム語について、それが1つでも登場したメールの数を、メールの総数で割ったものを、occurrenceとする。これは全スパムメール中のどれぐらいのスパムメールにそのスパム語が登場しているかを表し、そのスパム語の「出現確率」として扱われる。あるスパム語の登場回数を、スパム語全体の合計登場回数で割ったものを、densityとする。単語ごとに、frequency、occurrence、densityをまとめて表にしておく。(訓練データ表とでも呼んでおく。)spamフォルダの次はeasy_ham(判別が比較的容易な非スパムメール)フォルダで同じことをやる。spamとeasy_hamの訓練データのメール数は揃える。(あとで、スパムである事前確率を0.5とした分析をするので。分類器は引数として、判別対象のメール群(フォルダ)の場所、訓練データ表、スパムである事前確率、そして未知語(訓練データ表にない単語)が登場したときにその単語に割り当てる共通の出現確率を設定するようにする。この分類器の処理としてはまず、判別対象メール群(スパムなのか非スパムなのかを調べたいメール群)のTDMを作成し、それを訓練データ表と照合して、「両方に登場する単語」を特定する。判別対象メール群に出てくるスパム語を特定する。次にメール1通ごとに、事前確率(スパムである確率の事前想定値)と、そのメールに出てくる単語の出現確率をすべてかけあわせる。スパム語については訓練データ表からその出現確率(occurrence)を取得し、未知語(訓練データ表に出てこない単語)については出現確率を0.0001%と仮定する。やって計算された数字を、「そのメールがスパムである確率」とする。spamの訓練データ表でこの処理をやったら、次はeasy_hamの訓練データ表で同じことをやる。非スパムである確率を、各メールについて計算する。判別対象メール群のそれぞれのメールについて、スパムである確率と非スパムである確率を比べて前者が大きければ、スパムと判定する。以上がスパム判定処理の概要で、それ以外は、見やすい表にするとか、グラフを書くといった処理が出てくるだけです。上記の内容の中では、「単語の出現確率」(occurrence)のロジックに注意ですね。未知語の出現確率0.0001%というのは、教科書でも恣意的に設定した値である旨が注記されており、直感的にはなんか気持ち悪いところです。これは、ある本文が得られる確率を計算するときに、その本文に登場している全ての単語に対して全て出現確率を与えないといけないのに対し、訓練データが限られていると単語の出現確率をデータから得ることができないため、決め打ちで設定してしまうという話です。教科書の説明によると、テキスト分類のプログラムでは簡便な方法として「未知語の出現確率に非常に小さい値を設定しておく」というのはよくやることだとされています。他に、確率分布をランダムで割り当てるというような方法もあるらしいです。densityは判定には全く使わないです。ベイズの定理ところで、上述のような流れの処理のどこが「ベイズ分類器」なのかは、教科書を単に読むだけだと説明がなくてわからないので、一応以下にまとめておきます。基本的な考え方は、ベイズの定理を用いて、「こういう内容の本文が得られた場合に、そのメールがスパムである確率はですね......」というのを計算していくということになります。(しかし結局、確率を推定するというより、計算された確率らしき値を比較するだけなので、ベイズ推定というほどのものではないわけですが。):ある本文(語=wordsの組み合わせ)の出現確率:あるメールがスパムというclass又は非スパムというclassである確率:あるメールがスパム(又は非スパム)だったときにある本文が得られる確率:ある本文が得られた時に、そのメールがスパム(又は非スパム)である確率だとすると、となります。これが求めたい値ですね。P(W)は、メール本文に登場する各単語の出現確率を全部掛けあわせたものであり*2、P(W|C)は尤度を表します。P(C)はベイズ推定でいうところの事前確率であり、P(C|W)が事後確率です。目の前に開封前のメールが1通あったとして、何も情報がなければ、それがスパムである確率を経験等に基づいていったん仮定する(事前確率)が、その後メールの本文が情報として与えられることによりこの仮定は更新されて、事後確率となるわけです。本章の演習では、まず事前確率を「スパムである確率も非スパムである確率も0.5である」と仮定して分類器を回した後、「でも実際スパムってメール全体の2割ぐらいだよね」というふうに想定を変えて、スパム0.2、非スパム0.8を事前確率として分類しなおしたりします。それで事後確率としてのP(C|W)が算出された後に、スパムである確率と非スパムである確率を比較して、前者が大きければスパムと判定するわけですね。分類器としては、上の式の分母のP(W)は各クラスで共通になり判定の上では不要です(正規化定数ってやつです)。各単語の出現確率」だけを計算するプログラムを書いています。実際の演習コード以下、演習用のコードです。ほとんどは教科書(及びサポートサイトのサンプルコード)からの写経ですが、コメントはかなり自分で入れていて、一部コードを改変したところもあります。教科書に沿ってコードを書くときは、何も考えずに書き写しても意味が無いので自分でコメントをはさみまくるようにすると、勉強になる気がします。あと、処理の意味をコメントとして記載しておいたので、教科書のコードを読んで意味がわからなかった人の役にも立つかもしれません。読者がどれぐらいいるのか不明ですけど。ワーキングディレクトリの中に"data"というディレクトリを設け、さらにその中に"spam"、"easy_ham"などのディレクトリがあって、メールのデータが保存されています。グラフのPDF保存用に"images"というディレクトリを設けてあります。必要なパッケージをインポートします。library(tm)library(ggplot2)各メールのファイルが保存されているディレクトリのパスを、オブジェクトに保存してあとで使います。"spam")という書き方をしています(もちろん内容的には同じ)。"data/hard_ham_2/"メールのファイルを開き、最初の空行(headerとbodyの区切りを意味する)より1行下から全てのテキストを抽出する関数を作成します。collapse="\n"))}フォルダに保存されているメールのファイルから本文をまとめて取得します。ディレクトリ名とファイル名を"/"でつないで、1個1個のファイルの内容を抽出このall.spamベクトルの中に、解析対象となるメール本文が1通1要素としてぶちこまれています。ベクトルの要素名がファイル名(「通し番号.ハッシュ値」になっている)で、内容がメール本文です。本文は結構な分量になるのでここには引用しませんが、HTMLのタグなども大量に含まれています。取得されたメール本文から、テキスト処理のための準備をします。matrix)を作成します。スパムの訓練データについてTDMを作成。controlのところでTermDocumentMatrix()への引数を与えていますが、これは本文中の記号や数字を集計対象外とする処理です。debug.」とかいうエラーが出て、意味分からなくてキレそうになったのですが、Rを再起動したら収まりました。他の場面でも同じエラーが出ましたが、とりあえずRを再起動すると治りました。スパム語の出現率を計算します。occurrence=spam.occurrence)frequencyは、スパム訓練データに(2回以上)登場した単語の、登場回数の合計を表し、単純にスパムメールに多用されている単語が分かるというもの。occurrenceは、各スパム語について、それが1つでも登場したメールの数をメールの総数で割ったもの。全メール中のどれぐらいのメールに登場するかを表します。densityは、あるスパム語の登場回数を、スパム語全体の登場回数で割ったものです。中身をみてみましょう。Occurrenceの高い順に並ぶようソートして最初の6行を見ます。0.390これ、教科書に書かれてある値(↓の画像)と全然違いますね。Githubのサンプルコードをそのまま走らせても上記の値になるので、私の写経に間違いがあったわけではないです。「スパムメールの多くはHTMLメールなので、HTMLという語が最上位に来るのは当然」みたいなことが本文で解説されるのですが、最上位どころか上位6語に入ってないので、解説が成り立ちません。意味不明ですが、このまま先へ進みます。次に、easy_hamで同じことを行います。非スパムのデータで訓練して、非スパム判定にあとで使うことになります。コードとしては、「spam」→「easyham」、「spam.path」→「easyham.path」に書き換えるだけですね。これでフォルダ内の最初の500件だけが訓練データとして取り込まれました。occurrence=easyham.occurrence)非スパムの訓練データがどんなものか見てみます。0.336次に、分類器を作成します。意味の説明のためコード中にコメントを入れまくってます。↓重複している語を見つける。数を、メールの総数で割ったもの。事前確率と、すべての単語の出現確率(未知語については0.0001%)の積を求めます。載っていないものを意味しますが、登場数が1回のものも一覧からは除外されている点に注意。}分類器ができたので、実際にhardhamのメールデータを対象に、スパムかどうか判定してみて性能を確認します。判定対象のメールの本文を取得します。ファイル名「cmds」を除く次に、スパムである確率を算出します。"/"」のところ、教科書では「=""」となっていたんですが、「="/"」の間違いですね。)続いて、非スパムである確率を計算します。training.dfのところを変えるだけです。)スパムである確率のほうが高ければTRUE、非スパムである確率のほうが高ければFALSEを出力して、表にまとめます。非スパムのメール249通のうち9通について、誤ってスパム判定してしまっています。性能としては、「単純な分類器のわりには高い」という評価になるようです。残りのメールデータセットに対して、一気にスパム判定するための関数を準備します。スパムである確率と非スパムである確率を並べて、さらにスパム判定されたら「1」、非スパムなら「0」を表示するものです。spam2)について、ファイル名を取得しておきます。"cmds")]それぞれのデータに、さきほどのspam.classifier関数を適用します。サンプルコードではlapplyをsuppressWarnings()でくるんでいたけど、無視してます。})結果をまとめた表をつくります。do.call()関数は、第一引数の関数について、第二引数のリスト内容を引数として適用していくものです。先ほどのspam.classifier()関数によって作成されたeasyham.classその他は、メッセージのそれぞれについてスパムである確率、非スパムである確率、その比較結果の組み合わせが、リストで大量にぶちこまれているものですから、まずこれをrbindして一つの行列にし、次にその右側に、easyhamなどの種別の名称を表示します。"SPAM")以下、まとめ表の作成です。3つの種別の表をタテにつなげて、すべてのメールデータとスパム・非スパム判定の結果を表示したデカい行列を作成します。データフレームに変換します。確率のところをnumericに、分類結果はlogicalに、メール種別はfactorに、データ型を変換する。as.factor(class.df$Type)結果をみてみましょう。TRUEになっていると、スパム判定されているということです。EASYHAMもとの計算式から明らかな通り、確率のところはとても小さい値になってます。(正規化定数を計算してないので足して1にならない。比較が目的だからこれで良い。)上のように個別に判定結果をみてても性能はわからないので、集計しましょう。FALSE(非スパム)判定された率と、TRUE(スパム)判定された率を並べる関数をつくります。return(results)}上記関数を適用し、さっきの全体結果データフレームから各種別の行をそれぞれ抜き出してきて、率を比べます。0.666428060これをみると、スパムメールの3分の1ぐらいは、非スパム判定されてしまっていますね。非スパムの判定はミスが少ないですが、これは教科書にも書かれているように、要するに決定境界がスパム側に寄っていたという話です。判別の結果をグラフにしてみます。横軸に非スパムである確率、縦軸にスパムである確率をとり、y=xの直線を引いておけば、この線より右下であれば非スパム判定、左上であればスパム判定されたことになります。下のコードはサンプルコードそのまんまですが(コメントは私のメモですが)、グラフはPDFで保存するところまでやっています。PDF保存のコードです。「images」というフォルダがないとダメです。10)図で見ると、まぁまぁ分類できているような気もしますね。分類器の性能改善のため、非スパムの訓練をやりなおすことを考えます。いうことかというと、まず事前確率としてスパムであるか非スパムであるかを50%:50%にセットして分析していたわけですが、現実にはスパムは2割ぐらいだろうと考えて、分類器の引数にセットする事前確率をいじる方法が考えられます。その場合、それと合わせて訓練データの比率もスパム2割・非スパム8割になるよう調整するのが望ましいので、非スパム訓練メールデータは「最初の500件」に絞らず全て使うことにします。sep="/")))というふうに使うデータを限定していた[]書きのところを削除しました。下記のとおり、すべてのメールデータセットに対して一気に判定するための関数を書いた箇所で、事前確率をいじって変えておきます。0)))}結果をみてみましょう。0.4659985684非スパムについてはいいんですが、スパムメールの半分以上が、非スパム判定されてしまっておりますね。参考http://kentandx.blogspot.jp/2014/09/blog-post.htmlこのページをみると、上述のように「0.0001%」なんていう値を何乗もする処理をしたら、ゼロになってしまって正確に判断できないと書かれています。これをやってみたところ、動かなかったので、あとで時間あったら見なおしておきます。反省1エントリ2万字は長すぎですね。*1:実際に自分でプログラムを走らせず教科書に書かれたコードを「読書」してるだけだと「ふむふむ」とか言って読み進めてたんですが*2:本章の演習は単純な分類器を作成するもので、各単語の出現は相互に独立であると仮定されており、単語の共起関係や、文法・文章構成といった構造は無視。)を行うたとえば重回帰分析とかを行って、効果が「有意ではない」とい...2020-01-23対数尤度が正で、AICが負になるケース初歩的な話ですが、今日学生から訊かれたので例を考えてみまし...2019-12-20Rでよく忘れる、よく間違える書き方(随時追記)よく忘れることのメモです。R、Pythonの勉強の過程をメモっていくノートです。初心者なのであまり信用なさらぬよう。間違いについてはコメント等でご指摘頂ければ幸いです。6つぶやき@statsbeginnerさんのツイートはてなブログをはじめよう!midnightseminarさんは、はてなブログを使っています。ブログを報告する引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。