ゼッタノート

ゼッタが勉強したことをまとめたノート代わりのブログ。たまに日記になるかも。

rsyslogでタブ文字が反映されない

タブ文字だけじゃなくて他のエスケープシーケンスでも同様。今回自分がハマった原因は

  1. テンプレートの記述形式が古いため、反映されなかった
  2. EscapeControlCharactersOnReceiveがOnになっているため、メッセージ部分で#011みたいに8進数になっていた

でした。以下、詳細。

テンプレート記述形式とエスケープシーケンス

rsyslogのテンプレート記述形式をググると、次のような書き方がよくヒットする。 $template templateName, "%timereported% %hostname% %msg%\n"

この書き方はいわゆるLEGACY statementと呼ばれるやつで、古い書き方である。エスケープシーケンスについて、ORACLEのman pagesに次のような記述がある。

The backslash is an escape character. For example, \7 rings the bell (this is an ASCII value), \n is a new line. The set in rsyslog is a bit restricted currently.

ORACLE man pages section 5: File Formats rsyslog.conf (5)

「rsyslogで使えるエスケープシーケンスの種類には制限がある」と書かれている。具体的にどこまで対応しているのかは分からないが、確かに\nはしっかり改行されるが、\tと記述しても、出力されるのはバックスラッシュのみがエスケープされたtの1文字だけである。また、後述するが、現代的なテンプレートの書き方で対応している\xhhという、文字コードを16進数で記述するやり方も、LEGACY statementでは反映されない。

所望のエスケープシーケンスを反映するためには、公式ドキュメントに書かれている現代的なテンプレートの書き方をすれば良い。

RSyslog Documentation - Templates

書き方は4つあるようだが、公式が推しているのはList形式のようだ。私はList形式でタブ文字の反映を確認した。一応簡単に書き方を書いておくと、

template(name="テンプレート名", type="テンプレート定義の記述方式、今回はlist")

という形で宣言を開始し、中括弧の中に、具体的なテンプレートを記述していく。constantはいわゆる固定の要素で、どのログでも共通する部分、propertytimereportedhostnameなどを記述する部分に当たる。具体的な記述例や使用可能な主なエスケープシーケンス(ドキュメントに明示的な記載はないが、タブ文字も使えた)、propertyの名前は公式ドキュメントに書いてあるので参考にしてほしい。最後の方にExampleという章で、色々な書き方を紹介してくれている。古い書き方はいい加減やめよう。

EscapeControlCharactersOnReceive

これは、syslogのメッセージに他では表示不可能な可能性のある文字の混入を防ぐため、制御文字(ざっくり言うとエスケープシーケンス)を別の文字に置き換える機能のことである。

RSyslog Documentation - $EscapeControlCharactersOnReceive

例えば、\t\011#011なんかに置き換える。これによって、他のシステムとの互換性が担保されるわけだが、そんなことよりエスケープシーケンスの表示を優先したい場合は、これをoffにすれば良い。rsyslogのconfigファイル中に以下のように記述する。

$EscapeControlCharactersOnReceive off

まとめ

rsyslogでエスケープシーケンスを表示するためには

  1. 古いテンプレート記述方式はやめて、現行のテンプレート記述方式を採用する
  2. EscapeControlCharactersOnReceiveをoffにする

でした。

youtubeLiveのアーカイブを、コメントの盛り上がりから自動要約する手法の提案

みなさんこんにちは。最近私はVtuberというジャンルにはまっておりまして、登下校や家でグラブルをしているときは常に誰かの配信やアーカイブを見ております。ちなみに私の一推しは宝鐘マリンという方です。みんな見てね。

さて、最近はVtuberの数もかなり増えて、その全体を把握するのはほぼ困難となっています。にじさんじやホロライブなど事務所に絞っても、所属ライバーが多かったり、1回あたりの放送が長かったりと、全てを追うのは困難を極めています。
このような状況下で、ライバーさんの中にはまとめ動画を作ってくれている方もいます。しかし、毎日配信するだけでも大変なのに、その上でまとめ動画も作るなんて、ライバーさんの負担はかなりのものであることは想像に難くありません。そこで、まとめ動画を自動で作成してくれるシステムがあれば、ライバーさんはもちろん、忙しくてなかなか配信を終えない視聴者の皆にとっても嬉しいのではないかと思い、今回のプログラムを作ってみました。

作成環境

windows10
Anaconda
Python3.7.5
動画の編集にはmoviepyを使用しております
コメントの収集は前回のブログで述べたこちらの手法で行っています。

watagassy.hatenablog.com

先行記事

今回関連技術について調べている途中で、先行記事を2件見つけたので、敬意の意味も込めて掲載しておきます。

qiita.com

qiita.com 手法や考え方は後者の方に類似しています。

手法

動画を要約するうえで、いわゆる「見どころ」のみを凝縮したものを作成したいと考えました。その見どころについて、私は

見どころ≔コメントが多かった部分

としました。まずは、10秒単位でどのようにコメントが増減しているかを見るために、グラフにプロットしてみたところ、以下のようになりました。

f:id:Zetta777:20191110174745p:plain
10秒単位で見たコメント量
横軸は秒、縦軸がコメント量を表しています。今回は3時間の動画で実験を行っているため潰れて見づらいですが、600秒を過ぎたところなど、かなり特徴的にコメントが増えている部分が見受けられます。本手法ではこのような部分を抽出していきます。

また、時系列を維持するため、コメント量が多い順にシーンを抽出した後、それらのシーンを対象に時間順にソートしています。これでシーンが先に進んだり戻ったりとうことを防いでいます。

さらに、人間が該当シーンを見てコメントをするまでにはラグが存在することを考慮して、抜き出すシーンの10秒前から抽出を開始しています。

結果・及び考察

今回は本間ひまわりさん(以下ひまちゃん)のGTA5実況を用いて実験を行いました。この動画を選んだ理由は、実際にライバー本人がまとめ動画を作成していたため、人手で作成したまとめ動画と、自動作成したまとめ動画で比較実験ができると考えたためです。しかし、本人が作成したまとめ動画が「極悪事故まとめ」と銘打ってるだけあって、事故部分にのみ焦点を当てているため、あまり比較はできませんでした...だって、俺が知っている範囲でまとめ動画作っている人全然いなかったんだもんオヨヨ

以下が、自動作成した要約動画になります。


自動要約結果(ブログ用)

見てみると、ひまちゃんのつよつよエイムや事故部分など、一般的に見どころと思われる部分が抜き出されていますが、先行記事で述べられていたように、ライバーが質問したときなど、見どころと外れている部分も抽出されているほか、シーンの重複も見られます。
その他、配信が始まったばかりでコメントが多くなるオープニングトーク中や、視聴者の皆がコメントで指示している1:20部分なども抽出されています。

ちなみにひまちゃんが作成したまとめ動画と比較すると、まとめ動画中に含まれていた18シーン中6シーンが自動作成動画に含まれていました。再現率30%...

結論

以上から、先行記事のように、コメントの量だけでなく、その中身も吟味する必要があることが分かりました。今回の動画であれば「草」や「w」のような単語のみに着目すればよいとは思いますが、ものによっては機能しない場合もあります。例えばライバーさんの変な発言に対して「え・・・」とか「うわきっつ」のような反応をしている部分がそれにあたるでしょう。しかし、とりあえずは笑いのコメントに対してのみ着目すればよいというのは、間違いないかもしれません。

また、本来の目的である、「ライバーさんのまとめ動画作成の負担を減らす」という部分については、皆が「草」とコメントした部分だけでなく、手広くシーンをカバーするほうが良いと思われるため、コメント量に着目するほうが良いのかなと考えています。生成した動画をそのまま使うのではなく、元々3時間あった動画を30分に縮めてそこからシーンの選別をするようにすれば、それだけでもライバーさんの負担を大きく軽減することができるでしょう。
さらに、動画生成時に、元動画の何秒部分を抽出しましたという情報も書き出すようにすれば、前後のシーンを補完するために元動画を編集する際、該当シーンを探す手間が省けるため、役に立つでしょう。

P.S.

本当はQiitaに記事を上げようと思っていたのですが、自分とほぼ同じ考え方、同じ記事の構成で書いていらっしゃる方がいたのでこちらに書きました。アプリケーション化までするかは未定です。何かしら要望があったらやります。

youtubeliveのアーカイブで取得できるコメント(チャット)情報について

皆さんくそお久しぶりです。ここのところ学校生活が忙しかったりなんだったりして長らく更新が停滞していましたが、今日から少しずつ更新できたらいいかなと思っています。

そんなお久しぶり一発目はyoutubeliveのアーカイブで取得できるコメントについてです。 今、ブログのネタ用に作るプログラムの下準備をしていて、そのためにアーカイブのコメントを取得する必要があったのですが、この情報が多すぎてなかなか全体を把握しづらい。誰かが取得できる情報をまとめてないかなと思ったのですが、数秒検索して出てこなかったので(短気)まとめておきます。

今回コメントを取得する上で参考にさせていただいたのはこちら

watagassy.hatenablog.com

上のコードで取得すると全て辞書形式になるので

  • key1
    • value1
    • key2
      • value2
  • value3

という風に書いていきます。 値を赤、辞書のキーを青で色付けするので、欲しいデータを赤から探して、上にさかのぼる間に出てきた青色の単語を辞書のキーとして使っていくことで、所望のデータを取ってくることができるはずです。

取れる値早見表

クリックで飛べます

内容
クライアントID
その人のメンバー歴
メンバーアイコンのURL
コメントした人のユーザ名
ユーザアイコンの諸要素
操作の種類
絵文字の諸要素
コメント文
コメントされたときの時間

取れる値について

  • replayChatItemAction(key,string)
    • actions(key, string)(以下リスト)
      • index0
        • addChatitemAction(key, string)
          • clientId(key, string)
            • value(string): クライアントのID
          • item(key, string)
            • liveChatTextMessageRenderer(key, string)
              • authorBadges(key, string)(以下リスト)
                • index0
                  • liveChatAuthorBadgeRenderer(key, string)
                    • accessibility(key, string)
                      • accessibilityData(key, string)
                        • label(key, string)
                          • value(string): その人がメンバーになってどれくらい経ったか(例:'メンバー(7 か月)')
                    • customThumbnail(key, string)
                      • thumbnails(key, string)(以下リスト)
                        • index0
                          • url(key, string)
                            • value(string): メンバーアイコンのurl。index0に16×16のサイズが、index1に32×32が入っている?
                    • tooltip(key, string)
                      • value(string): その人がメンバーになってどれくらい経ったか(例:'メンバー(7 か月)')
              • authorExternalChannnelId(key, string)
                • value(string): なんかのID
              • authorName(key, string)
              • authorPhoto(key, string)
                • thumbnails(key, string)(以下リスト)
                  • <span id="usericon" index0(ユーザアイコンの32×32)
                    • height(key, string)
                      • value(int): ユーザアイコンの高さ
                    • url(key, string)
                      • value(string): ユーザアイコンのURL
                    • width(key, string)
                      • value(string): ユーザアイコンの幅
                  • index1(index0の64×64バージョン。省略)
              • contextMenuAccessibility(key, string)
                • accessibilityData(key, string)
                  • label(key, string)
                    • value(string): 操作の種類(多分)。(例:'コメントの操作')
              • contextMenuEndpoint(key, string)
                • clickTrackingParams(key, string)
                  • value(string): 不明。謎の文字列
                • commandMetadata(key, string)
                  • webCommandMetadata(key, string)
                    • ignoreNavigation(key, string)
                • liveChatItemContextMenuEndpoint(key, string)
                  • params(key, string)
                    • value(string): 不明。めっちゃ長い文字列
              • id(key, string)
                • value(string): 不明。なんかのid
              • message(key, string)
                • runs(key, string)(以下リスト)
                  • index0
                    • emoji(key, string)
                      • emojiId(key, string)
                        • value(string): 絵文字(スタンプ)のid。メンバーだけが使えるやつ
                      • image(key, string)
                        • accessibility(key, string)
                          • accessibilityData(key, string)
                            • label(key, string)
                              • value(string): 絵文字の名前だと思う
                        • thumbnails(key, string)(以下リスト)
                          • index0(絵文字の24×24)
                            • height(key, string)
                              • value(int): 絵文字画像の高さ
                            • url(key, string)
                              • value(string): 絵文字画像URL
                            • width(key, string)
                              • value(int): 絵文字画像の幅
                          • index1(絵文字の48×48。省略)
                      • isCustomEmoji(key, string)
                        • value(Boolean): 配信主が作成したスタンプか(?)
                      • searchTerms(key, string)(以下リスト)
                        • 0(index): 絵文字の名前
                        • 1(index): よく分からない。別名
                      • shortcuts(key, string)(以下リスト)
                        • 0(index): 絵文字投稿時の文字列?(例:':846Mojii:')
                  • index1
                    • text(key, string)
                      • value(string): コメント内容
              • timestampText(key, string)
                • simpleText(key, string)
                  • value(string): コメントされたときの動画時間(H:MM:SS)。マイナスもある
              • timestampUsec(key, string)
                • value(string): コメントされたときの動画時間(マイクロ秒表記?)
    • videoOffsetTimeMsec(key, string)

注意点

なんか不明データばっかでごめんなさい。自分が使いそうなところ以外は注視してないので、後は自分で読み取ってもらえれば...

コメントにあたる、message[runs]について

コメントは1つの連続した文字列は1つのインデックスを、絵文字は1つのインデックスを使用します。どういうことかというと、

  • 絵文字を3つだけ使ったコメントの場合
     インデックスは2まであります

  • 文字だけでコメントした場合
     インデックスは0のみです

  • 文字でコメントした後、絵文字を使った場合。(例:うおおおおお(emoji))
     インデックスは1まであります

という感じです。コメントの文字部分だけ抽出したい場合はキーがemojiとなっている部分を省いてください。

スパチャやメンバー通知について

構造が普通のコメントと違うので注意が必要です。まず、双方ともキー'message'がありません。スパチャにはキー'liveChatTickerPaidMessageItemRenderer'が'message'に代わるようにして存在します。メンバー通知は上でまとめた辞書が丸ごと2つで1セットみたいで、まず、'message'キーが'liveChatMembershipItemRenderer'に代わったもの、次に'liveChatTickerSpnsorItemRenderer'に代わったものが来ます。

また、スパチャも日本円と海外通貨で処理が違うみたいで、日本円は先ほど述べた通りなのですが、海外通貨の場合、メンバー通知のように2つの辞書で1セットらしく、1つ目は'liveChatPaidMessageRenderer'、2つ目が'liveChatTickerPaidMessageItemRenderer'が'message'と入れ替わっています。

自分がこれからしようとしていることに、ここら辺の情報はさほど重要ではないので、その下にどんな情報が入っているのかまでは調べていません。

終わりに

以上がyoutubeliveのアーカイブから取得できるコメント情報でした。かなりの量の情報を取得できますね。いろいろできそうです。

これからアーカイブコメントから何かをしようとしている人の助けになれれば幸いです。

ROC曲線が描かれる様子をGIFアニメーションにしてみた

 matplotlibで遊んだんですけど、せっかくなのでブログに載せます。前回の記事ROC曲線に触れたとき、真陽性率と偽陽性率の面積比のようなものと言いましたが、実際にROC曲線を描いていくとどんな感じになるのか、アニメーションで見れたら面白いなと思ってコードを書いてみました。x軸は偽陽性率、y軸は真陽性率です。x軸とy軸の数値は特に意味はありません。

どこかしらの識別器のROC曲線描画の様子

f:id:Zetta777:20190405104020g:plain
 青色の正規分布が真に陽性な尤度、オレンジ色の正規分布が真に陰性な尤度です。灰色部分は偽陰性率、紫色の部分は偽陽性率となります(y軸は真陽性率なのでその部分を色付けしたほうがよかったのでは...)。灰色の部分が減る度(真陽性率が増える度)y軸方向に増加し、紫色の部分が増える度x軸方向に増加していることが分かると思います。それはさながら面積比を表しており、最後は1:1となることが確認できます。また、これを見るとx軸方向に増加する度にy軸方向に減少することはないということも確認できるかと思います。

完全な識別器のROC曲線描画の様子

f:id:Zetta777:20190405104332g:plain
 次に完全な識別器の場合です。前回はただ文章で「曲線は(0,0)から(0,1)を通り、(1,1)を結ぶ直線となるので、AUCは1となります」と書きましたが、このアニメーションを見ると、確かにそうなるということが分かるかと思います。

ランダムな識別器のROC曲線描画の様子

f:id:Zetta777:20190405104334g:plain
 最後にランダムな識別器の場合です。分かりづらいですが、真に陽性な尤度と真に陰性な尤度の分布が重なっています。こちらは前回の記事で「原点と(1,1)を結んだ45度の直線はランダムな識別機となり、AUCは0.5となります」と書きましたが、確かにそうなることが分かります。また、以上3枚を見ると、AUCの最低値は0.5で、そこから性能を上げると徐々に左上方向にシフトしていき、1が最高値となる、ということも実感が持てるかと思います。ここから真に陰性な尤度のグラフを少しでも右にずらせば、AUCは0.5より大きくなりますからね。


今回はプレゼン用に用意したつもりだったのですが、グラフの描画の様子をアニメーションにするのはなかなか面白いですね。余裕があればまたどこかでやってみたいと思います。それではまた次回お会いしましょう。

パターン認識【第2回】

 かつてこれほどまでに更新間隔が短かったことがあったでしょうか、いやありませんでした。というわけで早速パターン認識の第2回記事です。今回は以前やったことあるベイズについてです。前回は単純ベイズのみのお話でしたが、今回はより深い内容となっています。

 ※注意!この記事は機械学習入門者が独自に調べ独自に解釈した内容により構成されているため、誤った記述が含まれている可能性があります。

最大事後確率基準に基づく識別規則

 まずは前々回の復習から。与えられたデータがどのクラスに属するかを判断する基準に、事後確率を見る方法があります。与えられたデータに対して、各クラスについて条件付き確率を計算して、一番確率が高かったクラスに属すると判断するものです。事後確率は式にすると以下のようになります。

p(C_{i}|\boldsymbol{x})=\frac{p(\boldsymbol{x}|C_{i})P(C_{i})}{p(\boldsymbol{x})}

 言葉で表すと、事後確率は尤度と周辺確率の比と事前確率の積で表される、となります。尤度と周辺確率の比を修正項と名付ければ、事後確率は事前確率を修正項で修正したもの、と考えることもできます。
 また、あるクラスC_iC_jがあって、その2つの識別境界は2つの事後確率p(C_i|\boldsymbol{x})p(C_j|\boldsymbol{x})が等しくなるところになります。以下の画像を見てもらえるとイメージしやすいのではないかと(マウス手書きクオリティで申し訳ない)

f:id:Zetta777:20190403152604p:plain
 2つのグラフの交点で値の大小が入れ替わります。なので、2つの値が等しくなる(交わる)点が識別境界となります。

最大事後確率基準の識別規則は誤り率が最小になる

 以前は、これがベイズのやり方なんだ、とそのまま受け入れていた事後確率による識別ですが、実はこの方法、とても良い面を持っていたんです。それは誤り率が最小であるということです。
 この方法での誤り率はどう算出すればよいか。分類する際、事後確率が高いほうに割り振るのですから、残りのクラスの事後確率分誤る可能性があります。2つのクラスC_iC_jに分類することを考えます。クラスC_iに識別される領域をR_iC_jに識別される領域をR_jとすると、誤り率は今までとは逆で、事後確率を計算した結果、常に値の小さいほうを取っていればよいことになるので、誤り率ε(\boldsymbol{x})

ε(\boldsymbol{x})=min[P(C_i|\boldsymbol{x}), P(C_j|\boldsymbol{x})]

 グラフで表すと以下のようになります。 f:id:Zetta777:20190403155643p:plain  赤い線が識別境界Θです。青色で塗りつぶしているところがC_iと判断すべきところをC_jと判断してしまった部分、オレンジ色で塗りつぶしたところがC_jと判断すべきところをC_iと判断してしまった部分です。誤り率は青色部分とオレンジ色部分の面積の和、ということになります。ここで、ほんの少しでも識別境界を動かしてみる(灰色部分)と、必ず面積が大きくなります。よって、最大事後確率基準の識別規則は誤り率が最小になります。また、これは事後確率基準のみでなく、次に説明する最小損失基準の場合も当てはまります。

最小損失基準に基づく識別規則

 実はベイズの識別規則は事後確率基準だけじゃなかったんです!知らなかった...どうやら最小損失基準なるものが存在するようです。例えば検査をして病気か健康かを判断するとき、健康な人を病気と判定するより、病気の人を健康と判定するほうがまずいですよね。そこで、前者より後者のほうが損失が大きいと考えます。この規則は損失が小さいクラスに割り振ります。真のクラスjのデータをクラスiに割り振った時の損失をL_{ij}とすると、観測データ\boldsymbol{x}をクラスC_iに割り振った時の損失r(C_i|\boldsymbol{x})

r(C_i|\boldsymbol{x})=\sum_{k=1}^{K}L_{ij}P(C_k|\boldsymbol{x})

 この式を全てのクラスについて計算して、一番損失が小さいクラスにデータを割り振ります。なお、事後確率基準の識別境界と最小損失基準の識別境界は異なるので注意してください。

リジェクト

 話を事後確率基準の識別規則に戻します。条件付きベイズ誤り率は先ほど述べた通り

ε(\boldsymbol{x})=min[P(C_i|\boldsymbol{x}), P(C_j|\boldsymbol{x})]

で表されます。事後確率基準の識別規則の場合、一番確率が高くなったクラスに分類するわけですが、一方でほかのクラスと僅差だった場合、そのまま分類するのはちょっと危ういと思いませんか?例えば2つのクラスに分類するとき、Aというクラスでは51%で、Bというクラスでは49%だった場合、果たしてAクラスに分類してしまってもよいのでしょうか。その点を考慮したものがリジェクトとなります。これはある値tを定めて、誤り率がtよりも大きかった場合、データがどこに属するかの判断を避けるというものです。そうすることで、判断しかねるデータに関して学習することを避けることができ、誤った方向に学習が進むのを防ぐことができます。また、以下のような式が書いてありました。

一般に、K個のクラスがある場合、リジェクトを含めた識別規則は {} $$ 識別クラス = \left\{ \begin{array}{} C_i\Leftarrow P(C_i|\boldsymbol{x})= \max_j P(C_j|\boldsymbol{x})>1-tの場合 \\ リジェクト \Leftarrow すべてのクラスについてP(C_i|\boldsymbol{x}) \leqq 1-tの場合 \end{array} \right. $$ とする。

ここでちょっと理解が止まったんですが、誤り率がt以上でリジェクトするということは、一番確からしいクラスに分類される確率は1-tとなるので、識別クラスの判断は上のようになります。誤り率を見てるのか一番確からしいものを見ているのかが反転していたので気を付けましょう。

ROC曲線

 ROC曲線とは識別性能の指標です。事後確率基準の識別規則で示した誤り率で性能を評価しようとしても、事前確率や尤度を知っておかなければ使えません。一方ROC曲線はそれらが必要ない上、クラスごとのデータ量に差があっても影響を受けないという利点があります。どういうことかというと、病気の人のデータより健康な人のデータのほうが圧倒的に多いという状況では、どんな人に対しても健康であると判断しておけば誤り率は小さくなりますが、そんな識別器は使えませんよね。そのような状況でも対応できるのがROC曲線の良いところです。また、次のサイトが分かりやすかったので、本を読んでもいまいち理解できなかった方は読んでみるといいかもしれません。
【ROC曲線とAUC】機械学習の評価指標についての基礎講座

前提知識

 ROC曲線に触れる前にいくつか用語を紹介しておきます。2つのクラス、p*(陽性)・n*(陰性)に分類する場合を考えます。*がついているのがそのデータの真のクラス、ついていないものが識別器が分類したクラスとします。あるデータをp・nに分類したとき、そのデータの真のクラスがp*かn*かで以下のように種類分けされます。 f:id:Zetta777:20190404114251p:plain
これにより、以下のような値を考えることができます。
真に陽性のデータ数 P = TP + FN
真に陰性のデータ数 N = FP + TN
 それぞれ真に陽性のデータ数と陰性のデータ数です。

偽陽性率 = \frac{FP}{N}
真陽性率 = \frac{TP}{P}
 偽陽性率は本当は陰性なのに陽性と判断したものの割合、真陽性率は実際に陽性だったもののうち、陽性と判断できたものの割合です。


適合率 = \frac{TP}{TP+FP}
 真と判断したもののうち、実際に陽性だったものの割合です。ネット検索を例に言うと、検索して出てきたページの内(真と判断したもののうち)、要求していたものと合致するサイト(実際に陽性だったもの)の割合となります。


再現率 = \frac{TP}{P}
 真に陽性のもののうち、真と判断できたものの割合です。式で見ると真陽性率と同じですが、実際は異なります。ネット検索をするとき、Pは「ユーザーが欲しがっている全ページ」となりますが、ネット上に何ページあるか、それは到底数えられるものではありません。再現率のPにはその推定値が入ります。一方真陽性率のPは推定値ではなく実際の値が入ります。


正確度 = \frac{TP+TN}{P+N}
どれだけ正しく識別できたかを表します。

 さて、再現率と適合率の間にはトレードオフが存在します。適合率を上げるためには、誤り(FP)を減らす必要があります。そのためにネット検索で出てくるページを減らそうとすると、欲しかったページが出てこなくなるので、再現率が小さくなります(Pが減るため)。そこで両者の調和平均を取ったF-値というものがあります。調和平均とは逆数の平均の逆数です。 F-値 = \frac{2}{\frac{1}{適合率}+\frac{1}{再現率}}

本題

 いろいろな値を書いてきましたが、これからの話で使っていくのは偽陽性率と真陽性率のみです。曲線、というぐらいですからグラフにプロットするわけですが、x軸に偽陽性率を、y軸に真陽性率を取ります。最初に陽性と陰性のデータ数に影響を受けないと書きましたが、偽陽性率も真陽性率も共に、真に陽・陰なものの中での割合なので、陽と陰のデータ数の比に影響を受けません。ROC曲線を描く場合は以下のように偽陽性率と真陽性率の面積比を表していると思ってよいでしょう。 左側の山が真に陽なデータの尤度、右側の山が真に陰なデータの尤度、真ん中に引いたオレンジ色の線が識別境界です。 f:id:Zetta777:20190404123404p:plain
 これを端から端まで動かしたときの偽陽性率と真陽性率をグラフにします。左端を識別境界にするとどのデータも陽と判断することがないため真陽性率0、偽陽性率0の原点となります。右端を識別境界にすると、どのデータも陽と判断するため、偽陽性率1、真陽性率1となり、それ以外の場所だとその間の値をとるようになります。
 ではこのグラフをどのように利用するのでしょうか。ROC曲線は偽陽性率と真陽性率の面積比と考えることができるので、2つのグラフの重なりが少ないほどグラフは左上方向にシフトします。また閾値を下げて真陽性率が小さくなることはないので、グラフが右に行くほど下がる、ということもありません。なのでROC曲線と偽陽性率軸との面積比で性能を比べます。これをROC曲線下面積、またはAUCと言います。完全な識別機の場合、曲線は(0,0)から(0,1)を通り、(1,1)を結ぶ直線となるので、AUCは1となります。2つの山が完全に分かれている場合がそれになります。原点と(1,1)を結んだ45度の直線はランダムな識別機となり、AUCは0.5となります。先ほどのグラフで2つが完全に重なっている場合がそれになります。AUCは0.5から1の間の値を取り、値が大きいほど性能が良いということになります。
 性能の比較のやり方はこれでわかりました。次に、良さそうな識別器ができたとして、識別境界をどこに定めるか(真陽性率と偽陽性率の組み合わせ、動作点)が問題になります。ここで先ほど示した損失の式を再度示します。

r(C_i|\boldsymbol{x})=\sum_{k=1}^{K}L_{ij}P(C_k|\boldsymbol{x})

これをクラスが2つの場合に限って話をすると、正しく識別できた場合の損失を0とすると、式は以下のようになります。

 r = L_{12}P(n*) ε_2+L_{21}P(p*) ε_1

どのような展開をしているかははじめてのパターン認識を参考にしてください。 ε_xはxの誤り確率です。これを、ROC曲線に合わせて1-ε_1 = αε_2+βの形にしましょう。1-ε_1は真陽性率です。クラス1(真に陽)からクラス1の誤り確率を引くと真陽性率となります。式を変形すると以下のようになります。

1-ε_1=\frac{L_{12}P(n*)}{L_{21}P(p*)}ε_2+ (1-\frac{r}{L_{21}P(p*)})=αε_2+h(r)

 これを損失rを変えながらROC曲線にプロットしていって、曲線と接したところが動作点となります。


 以上でパターン認識第2回終了です。以前単純ベイズをやっていたおかげで勉強がしやすかったので、勉強していってるなという感覚があってちょっとうれしかったです。次回のページをぱらぱらっとめくってみたのですが、どうやらかなり数学に寄ったお話っぽい...数式が多めになるかもしれませんね。
 それではまた次回お会いしましょう。

パターン認識【第1回】

 お久しぶりのブログ更新です。ここのところTOEICの勉強のために機械学習に全く触れられておらず、そのためブログの更新も完全に止まっていました。挫折したわけじゃないよ!

 さて、本日から故あって平井有三氏のはじめてのパターン認識を勉強することになりました。これまで通りアウトプットと今後の見直しのために、この本を読んで自分なりにまとめたものを書いていきます。

 ※注意!この記事は機械学習入門者が独自に調べ独自に解釈した内容により構成されているため、誤った記述が含まれている可能性があります。

パターン認識ってどんなことをするの?

 そもそもパターン認識とはどういうことをするのでしょうか。それは、本書に挙げられている通り、硬貨の識別(券売機に入れられた硬貨が10円なのか、50円なのか、はたまた100円なのか)や、音声認識、顔認識などです。つまり、なんらかの形をいくつかのクラスに分けることパターン認識と呼びます。音声も波形の分類と考えれば形の分類と呼んでよいでしょう。
 分類するために、例えば10円硬貨の特徴すべてを把握するのは困難を極めるでしょう。それが日本で流通している硬貨すべてとなると、情報量は膨大になるはずです。そこで、特徴抽出を行います。単語から推測できるように、それぞれの硬貨の代表的な特徴を取り上げるのです。例えば、材質・大きさ・穴の有無など。各硬貨の全ての情報を利用せずとも、区別する際に効果的な特徴のみを利用することで、制作レベルをぐっと下げることができます。そして、それらの特徴を基にどのように分類するかを決めたものが、識別規則となります。

教師付き学習・教師なし学習

 識別規則とは今回では関数の形をしています。あるデータ\boldsymbol{x}を入力としたy=f(\boldsymbol{x})という関数です。学習の目的は、正しい結果yを出力するような関数f()を求めることです。教師あり学習では、各データに正解となるラベルを付与したものを入力データとします。これによって1個のデータを学習するごとに、パラメータ\boldsymbol{w}を正解ラベルに近づくように修正していきながら学習を進めることができます。
 しかし、データ数が増えれば増えるほど、正解ラベルを付けていくのは大変になります。そこで、データ間の類似度に応じて自動で分類していく教師なし学習というものがあります。また、入力データの一部にのみ正解ラベルを付与した形質導入学習というものもあるようです。

バイアス・再代入誤り率

 機械学習を勉強しているとよく出てくるバイアスという言葉、今までよくわかっていなかったのですが、標本(学習データ)は母集団からランダムに取ってきたものであり、その各特徴量の平均や分散は母集団と同じになるわけではなく、その差をバイアス、というようです。
 学習データと同じデータを用いて検証を行った場合の誤り率を再代入誤り率と言います。テストデータで検証を行う前に、そもそも学習データに対して良い値を返すか確認、という感じなのでしょうか。

様々な検証法

ホールドアウト法

 ホールドアウト法とは、データを学習用とテスト用に分割し、学習用データを用いて学習を行った後テストデータを用いてテストを行うものです。しかし、この方法は学習用データを多くとれば学習精度は上がるが評価精度が落ち、評価用データを多くとれば評価精度は上がるが学習精度が落ちるというトレードオフが存在します。大量のデータを所持していなければ使えない方法と言えるでしょう。

交差確認法

 以前LinerSVCの記事で説明したK分割交差検証のことですね。また、この方法は、何回も同じことを繰り返した後、最後に平均をとるのですがそれにはちゃんと理由がありました。1回だけだと、分割に偏りが生じてしまう可能性があるため、分割数分繰り返して平均をとるようです。

一つ抜き法

 交差確認法はデータを分割する際Kに好きな数字を入れることができましたが、一つ抜き法ではデータ数と分割数を同じにしたものです。

ブートストラップ法

 以下のサイトを参考にしました。
ブートストラップ | Shimodaira Lab
 これは再代入誤り率のバイアスを補正するために利用されるそうです。手元のN個のデータ(標本)から重複を許してN回復元抽出を行いサンプルを作ります。ここで復元抽出とは、1度データを取ってきた後、元に戻し、また取り直す、ということです。確率の問題でよくある「くじを引いて元に戻しまた引く」っていうやつですね。こうしてできた新たな標本をブートストラップサンプルと言います。バイアスは今作ったブートストラップサンプルの再代入誤り率から、ブートストラップサンプルで学習した後、元の標本でテストした際の誤り率を引くことで求まります。式で表すと以下のようになります。

bias=ε(N*, N*)-ε(N*, N)

 ここでε(X, Y)とは、Xで学習した後Yでテストした際の誤り率を表します。このように何回もブートストラップサンプルを作ってはbiasを計算して...ということを繰り返して、最後にbiasの平均をとります。これがバイアスの推定値となります。最後に元の標本の再代入誤り率からバイアスの推定値を引くと、母集団の誤識別率の予測値が求まります。

汎化能力の評価法とモデル選択

 手持ちのデータで学習させても、世に出すと学習データに含まれなかった入力が行われる可能性があります。このようなデータに対応するために、汎化能力の高さが求められます。ではその能力はどのように測ればよいのか。ここでは平均2乗誤差を挙げています。本書には以下のような式が書いてあります。

\int\ (y(x;D)-h(x))^{2}p(x)dx = E\{(y(x;D)-h(x))^{2}\}

 yは\boldsymbol{a}をパラメータとするp次多項式の近似曲線なのですが、データセットDを決めれば一意に決まるため、y(x;D)と表しています。h(x)は検出したい関数です。ここでは区間[0, 1]の話として、確率密度関数として扱っていますが、[0, 1]内で一様に分布しているためp(x)=1積分範囲は[0, 1]となります。ここでも1つのデータセットのみでなく、複数のデータセットを使って評価します。
 近似曲線の次数を少なくすると、近似曲線同士の分散は小さくなるが、データから大きく外れます(バイアスが大きい、という)。一方近似曲線の次数を多くすると、近似曲線同士の分散は大きくなるが、バイアスは小さくなります。そして次数を大きくしすぎると、バイアスが小さくなりすぎて過学習の原因になります。近似曲線同士の分散が小さいと、どのようなデータを与えられても変わらない値を出す、という意味で汎化能力が高いことを示す一因になる、ということでしょうか。


今回は以上となります。最初ということもあり、以前の2記事で調べたことと重複する箇所が多々ありました。復習にもなって良かったです。今後も本書を読み進めていくごとに記事を書いていこうと思います。それでは。

勉強したこと:NaiveBayes編

 勉強したこと記事、第2回はNaiveBayesです。scikit-learnのチートシートに従うと、テキストデータの分類をする際にLinear SVCで上手くいかなかった場合に用いることになるやつです。

 今回は以下の記事を中心に勉強していきました。
ナイーブベイズで自然言語処理(クラス分類)【Pythonとscikit-learnで機械学習:第7回】

※注意!この記事は機械学習入門者が独自に調べ独自に解釈した内容により構成されているため、誤った記述が含まれている可能性があります。

NaiveBayesって?

 日本語ではそのまま単純ベイズとか言ったりします。主に使われる場面は迷惑メールフィルタです。前回のLinear SVCでは各データをグラフにプロットし、マージンが最大になるように直線を引くことで分類を行いましたが、今回は確率によって分類します。
 ベイズ、と名前に入っているだけあって、中心となる式は、以下に示す確率を習った際に出てきたであろうベイズの定理を利用しています。

P(B|A) = \frac{P(A|B)P(B)}{P(A)}\\

Bはクラス、Aはデータ(上の参考URLを例にすると、Bは記事のカテゴリ、Aは文書内に含まれる単語出現頻度のベクトル)です。未知の文書を分類する際はベイズの定理による計算結果、すなわち事後確率が最も大きいクラスに分類します。

詳しく

 ここでは文書データを分類するという仮定の元説明していきます。まず、Bをある文書カテゴリcat_x、Aを文書内に含まれる単語の出現頻度ベクトル\boldsymbol{doc}とし、ベイズの定理を書き換えます。

P(cat_x|\boldsymbol{doc}) = \frac{P(\boldsymbol{doc}|cat_x)P(cat_x)}{P(\boldsymbol{doc})}\\

 未知のデータを分類する際は上記の式を計算すればいいのですが、P(\boldsymbol{doc}|cat_x), P(cat_x), P(\boldsymbol{doc})の値が分からないのでこれらを算出する必要があります。そして、それら3つの値(本当は2つの値)を算出する過程が今回の「学習」ということになります。

 まず、なぜ2つの値のみを算出すればいいのか?今回必要のない値はP(\boldsymbol{doc})です。例えば未知の文書を2つのカテゴリcat_1, cat_2に分類する場合、どのように比較するかと言うと未知文書の単語出現頻度ベクトル\boldsymbol{doc}に対して

 \frac{P(\boldsymbol{doc}|cat_1)P(cat_1)}{P(\boldsymbol{doc})}

 \frac{P(\boldsymbol{doc}|cat_2)P(cat_2)}{P(\boldsymbol{doc})}\\

のうち、値の高いほうに分類します。ここで、P(\boldsymbol{doc})は共通していますよね?なのでどちらが大きいかを比較する場合は分子だけに着目すればいいことになります。式的に比を取ると分母が消える、と考えてもよいでしょう。なのでP(\boldsymbol{doc})は計算する必要がありません。

 P(\boldsymbol{doc})を計算する必要がなくなったので、単純ベイズで用いる式は以下のようになります。

P(\boldsymbol{doc}|cat_x)P(cat_x)\\

 また、これを変形すると

P(\boldsymbol{doc}|cat_x)P(cat_x) = \frac{P(\boldsymbol{doc}, cat_x)}{P(cat_x)}P(cat_x) = P(\boldsymbol{doc}, cat_x)\\

となります。ここでP(\boldsymbol{doc})がどういうものかについて説明します。インデックスは出現した単語を指し、要素はその記事中の出現単語数になります。例えばある記事の中にAppleが3回、代理が1回、Microsoftが2回出てきたとし、それぞれインデックスが0, 1, 2だとすると\boldsymbol{doc} = (3, 1, 2)となります。
 さて、それぞれの単語出現頻度をword_1, word_2, word_3, ..., word_nとすると、

P(\boldsymbol{doc}, cat_x) = P(word_1, word_2, word_3, ..., word_n, cat_x)\\
= P(word_1|word_2, word_3, ..., word_n, cat_x)P(word_2, ..., word_n, cat_x)\\
= ... \\
=P(word_1|word_2, ..., word_n, cat_x)P(word_2|word_3, ..., wordn, cat_x)...\\
P(word_{n-1}|word_n, cat_x)P(word_n|cat_x)P(cat_x)\\

見づらくなっちゃいましたが、最後の行は全部かけてます。乗法定理P(A,B)=P(A|B)P(B)を繰り返し用いることでこのような変形を行っています。さて、ここで一つ乱暴ともいえる仮定を行います。それはそれぞれの単語は独立に生起するというものです。本来「機械」と「学習」はセットで出てきやすいですし、「響け」と「ユーフォニアム」もセットで出てきやすいです。ですがこの仮定では「機械」と「学習」という単語間の関連性を無視し、それぞれが独立のものとして現れた、と考えます。このような状況を条件付き独立と言います。そうすると、式は以下のようになります。

P(word_1|cat_x)P(word_2|cat_x)...P(word_n|cat_x)P(cat_x)\\
=P(cat_x)\prod_{i=1}^n P(word_i|cat_x)\\

大変簡単な式になりましたね。これが単純ベイズのありがたみであり、この仮定が「単純(Naive)」と言われる所以です。

 で、先ほどの式に現れたP(cat_x)が分かりませんので、これも計算する必要があります。これは全文書中に、そのカテゴリが占める割合を算出すれば良いです。式で表すと

\frac{カテゴリcatの文書数}{全文書数}\\

となります。

 ここで一つ問題があります。未知の文書を分類する際、学習した文書の中には現れなかった単語が含まれる可能性は十二分に考えられます。その場合、どのカテゴリに対してもP(未知の単語|cat_x) = 0となり、掛け算をしているために事後確率も0になってしまいます。どんなに機械学習に分類されるような単語が含まれていたとしても、猫の判別についての記事だったためにどのカテゴリにも属さないと判定される...これでは全く使えませんよね。これが迷惑メールフィルタだとすると実害がより実感しやすいでしょうか。この問題はゼロ頻度問題と言われています。
 今回用いたscikit-learnのnaive_bayes.MultinomialNB()では加算スムージングと言われる手法でこの問題を解決しているようです。加算スムージングについては以下のサイトを参考にしました。
言語モデルにおける未知語の扱いとスムージング
 以下、自分なりのまとめ

 未知語が出てきた際、その確率が0にならないようにしなければなりません。そこで、ある一定の値を足してあげることで解決を図ります。加算スムージングの式は以下です。

P(word_x) = \frac{n(word_x)+\alpha}{C+\alpha V}\\

このように、ある一定の値\alphaを足してあげることによって、未知語が出てきたとしても、ほんの少しの値を持つことになり、事後確率が0になるのを防ぎます。

コードを書く

 理論についてまとめたので、今度は参考サイトを見ながらコードを書く上で勉強したことを。

記事を分かち書きする

 記事を分かち書きする際、名詞、動詞、形容詞のみを抽出していました。確かに助詞を持ってこられてもカテゴリ分類には役立ちそうにありませんね。どんなカテゴリにも「が」は含まれがちですし、邪魔になりそうです。また、活用の元の形を利用する(遊びました、遊ぶなどを分かち書きした際、動詞については両方 遊ぶ で集約)ことによって、効率化しています。

Bag-of-Words

 ニュース記事をベクトルにしました。ここは結構ややこしかったのでまとめます。

 まず全記事に出てきた単語をまとめます。word_dicが出てきた単語をまとめている辞書で、例えば 機械 という単語が最初に登録された場合、word_dic = {"MAX":1, "機械":0}という風になります。MAXは登録されている単語数、各単語の値は後に単語の出現回数を数えるためのcntというリストのインデックスになります。分かち書きした結果を保存したファイルを読み込みながら、全単語を登録した辞書を作成します。
 次に、ジャンルごとにファイルを読み込み、単語の出現回数を数えます。ここでは各ジャンルごとに100ファイルずつ利用していました。1ジャンルごとにcount_file_freq()を呼び出し、カウントしていきます。先ほどの例を用いると、機械という単語はインデックス0にあたるので、機械という単語が出てくるたびにcnt[0]に1加算されていきます。カウントし終えたら、リストXに単語のカウント結果cntを、リストYにカテゴリ名cat_idxを登録します。カテゴリ名と言っていますが、実際は0から始まる数字です。最終的に
X[{1つ目の記事のカウント結果}, {2つ目の記事のカウント結果}, ..., {n個目の記事のカウント結果}]
Y[(1つ目の記事のカテゴリ名), (2つ目の記事のカテゴリ名), ..., (n個目の記事のカテゴリ名)]
となります。要は、XとYの同じインデックス番号どうしで対応が取れている形になります。これでベクトル化は終了です。

実際に分類する

 ここでは1点だけ。naive_bayes.MultinomialNB()の引数、alphaは先ほど述べた加算スムージングにおける\alphaの値です。

まとめ

線形SVMとは異なり、単純ベイズは単語の出現頻度を基にした、確率によって分類を行うアルゴリズムである。様々な工夫によって計算を単純化しているため、単純と言われる。その工夫とは、比較する際は分子のみに着目すればよかったり、条件付き独立性を仮定することである。未出現の単語については加算平滑化スムージングという手法によって事後確率が0にならないようにしなければならない。

所感

調べてみたけど解決しなかったことについていろいろ考えるこのコーナー。今回は2つ。

scikit-learnのチートシートで、なぜLinear SVCが上手くいかなかったらNaive Bayesなのか?

個人的に、確率的手法かそうでないかっていうのがミソになっているんじゃないのかなと。SVMは空間上で境界線(面)を引くのが難しい場合でも境界によって分類をしなければなりません。なので無理やり境界を引いたところで、あやふやなまま分類をする必要がある...一方単純ベイズは確率を求め、高いほうを採用するという手法なので、より柔軟である...?自分で言ってても本当かよ、という感じです。あとは単純に計算コストですかね。やっぱり楽な手法で上手くいくならそれに越したことはないので、先にコストのかからないSVMを試して、それでだめなら単純ベイズ...みたいな。

MultinomialNB()の引数、fit_priorについて

わからなかったので本編では触れませんでした。リファレンスによるとクラス事前確率を学ぶかどうかを指定する変数のようですが...参考サイトには学習データの偏りがあった場合に、それを考慮するか、とも書いてました。といってもどう考慮するのかが分かりません。クラスの事前確率っていうぐらいですから、例えばスパムフィルタを作る際の学習データが、ハムメール:5件、スパムメール:1件で構成されていた場合、未知のメールを分類する際に、単語の出現頻度による事後確率を計算した後、ハムメールのほうが来やすいよね!ってことで何かしらの値を掛けたりするんでしょうか。確かにこれなら出現頻度を考慮しているような...?fit_priorの引数はデフォルトではTrueで考慮するようになっているようですが、Falseにすると一様分布になるようですし、全体的な頻度を考慮したうえで分類してくれるってことなのかな...?
 と、ここまで書いてブログの見直しをしていたら、今ここで書いたことはP(cat_x)のことなのでは...と気づいてしまいました。となるとこの引数は実際にP(cat_x)を計算するか、はたまた全ての記事が一様に現れるとしてP(cat_x) = 定数とし、事後確率を計算するか、ということなんでしょうか。全てのカテゴリの記事を均等に用意したのであれば、引数をFalseにすることで計算量を少なくすることができる...?

サンプルコードを書いていて思ったこと

 最終的にXとYについて膨大な量の辞書型を生成してしまうわけですが、これって

{cat0:出現頻度, cat1:出現頻度...}

という辞書型を生成するほうが良いのでは?と思ったりしました。


 と、いうことで第2回は終わりです。コードを追いかけつつ説明していった前回とは違い、今回は単純ベイズの理論をどーんと説明した後、コードを追ってみました。今後はこの形式になるんじゃないかと思います。後、最後に未解決問題を書いてみたりもしました。ここを書いているうちに何か閃かないかなという希望を持ちながらこのコーナーを作りました。もしわかる方がいれば教えていただければ...と。またソースが確認できず不確定なことや、見出しの名の通り、思ったこともそこに書いていきたいと思います。

 ここまで読んでいただき、ありがとうございました。

P.S. コードインテリセンスの選択をTabキーにするとQ.O.Lが上がった。