Subscribed unsubscribe Subscribe Subscribe

Hatena Developer Blog

はてな開発者ブログ

セキュリティ会の取り組み

はてなエンジニアアドベントカレンダー2016

この記事は、はてなエンジニアアドベントカレンダー2016の3日目の記事です。昨日は id:nanto_vi による
CSS の -webkit-line-clamp プロパティについて: Days on the Moon でした。
こんにちは、はてなでチーフエンジニアをしている id:wtatsuru です。この記事では、はてな技術グループのサブ会の一つ、セキュリティ会について紹介します。サブ会を始めとするはてな技術グループの取り組み全体については、先日の id:motemen の記事で詳しく紹介しております。
はてなの技術組織2016 - Hatena Developer Blog

セキュリティ会とは

セキュリティ会とは、セキュリティインシデントを防ぐことを目的とする技術グループのサブ会です。各分野からメンバを集め、セキュリティ情報への素早い対応および社内のセキュリティ意識向上を目的としています。技術グループでは、以前からセキュリティ担当を置いてチェックする体制をとっていました。しかし、組織が大きくなる中でスピード感や安定運用という点に課題が出てきたため、活動リソースを確保した組織として形を作り運用しはじめました。

セキュリティ関連情報のチェック

セキュリティ関連の情報は、毎日数多く報告されています。セキュリティ会では、インフラ、サーバサイドアプリケーション、スマートフォン、フロントエンド、社内システム担当など、各分野のエンジニアがこれらを網羅的にチェックし、知見を共有しています。メンバがいるSlackチャンネルに各種フィードが流れるようになっており、すぐに確認および必要であれば社内のGithubEnterpriseへのIssue化や、全社員向けの注意喚起を行います。
情報源としてはJVNなどセキュリティ系のRSSを用いたり、RSSがないサイトも大チェッカーのHTML購読からの通知を用い、Slack通知に集約しています。また、特定ライブラリの情報などはブログに書かれることもあるため、そういった情報は各分野のエンジニアでカバーしあっています。

社内への周知

社内への周知を、社内のはてなグループのエントリ上で、週次・月次の二段階で行っています。
社内の全エンジニア向けには、週次でセキュリティ情報を共有しています。社内的に直接的に関係ない情報も含め、技術的な情報は速報として全エンジニアに周知することで知識の共有と注意喚起を行います。毎週の社内勉強会 でも採り上げられて盛り上がることもあります。
また、月次で全社員に向けて共有を行っています。全社員向けの周知は頻度を挙げすぎると読まれない可能性があるため、頻度を下げつつ内容を重視したものとしています。週次のエントリで採り上げた技術ネタに加え、セキュリティ的なトレンドや気をつけるポイントなど、各自の分野でホットな話題があれば読み物として解説しています。最近のトピックを下記にいくつか取り上げます。

おわりに

上記の通常活動以外にも、セキュリティ関連規定策定の際の技術レビューやセキュリティ系相談・提案など、社内のセキュリティレベルを高める取り組みを行っております。今後も継続的に改善していこうと思っておりますので、セキュリティに配慮したサービス作りに興味のある方は、ぜひはてなに応募してください。一緒に改善していきましょう。

また、もしはてなのサービスに関してセキュリティ上の懸念点や問題を発見された場合は、セキュリティ窓口を設けておりますので、下記までお知らせください。
セキュリティに関するお問い合わせ - Hatena Developer Center
アドベントカレンダー、明日は id:y_uuki です!

はてなの技術組織2016

この記事は、はてなエンジニアアドベントカレンダー2016の1日目の記事です。

8月よりCTOになりましたid:motemenです。たいそうな肩書きがつきましたが、引き続きチーフエンジニアという役職も兼任しており、これまでどおりアプリケーションを書きつつ、技術組織の開発・改善をおこなっています。

せっかくの機会なので、このエントリでは現時点でのはてなのエンジニア組織の概観と取り組みを紹介しようと思います。

技術グループ

はてなにおいてエンジニアと呼ばれる職種の人は、それぞれが提供するサービス(ウェブサービス、スマートフォンアプリ、インフラなど)を軸とする開発本部に属しつつ、職能を軸とした横串の組織である「技術グループ」に所属する、という形態を採っています。現時点で、ここに属するのは40名強になっています。

会社を駆動させていくお金を生み出すのは造られたシステムであったり、営業活動であったりするなかで直接的には利益を産まないこのような組織を設けるのは、以下のような目的意識があってのことです:

  • チーム間技術の標準化、向上
    • 普段の業務の場であるチームに閉じてしまいがちな技術的知見の交換
  • サービス横断的・中間的タスクの遂行
  • はてなの技術的ブランディングの向上
    • 個々のエンジニアの成長支援と評価

中長期的に事業を下支えしていくための組織であるといえます。

シニアエンジニア制度

はてなのエンジニア組織も年々規模を拡大している中で、全員への目配りというのは自然には実現しづらくなってきました。そこで数年前から各エンジニアにシニアエンジニアと呼ばれる人たちをメンターとして割り当て、毎月面談をすることで技術的・非技術的・顕在的・非顕在的な問題を早期発見することを目指しています。エンジニア個人としての成長を支援する目的もあり、外部の視点を入れるため、できるだけ所属しているチームが異なる人同士を引き合わせるようにしています。この毎月面談の中から社内の新しい取り組みが生まれることもあります。

シニアエンジニアはそれぞれエンジニアとしての強みを持った人たちで構成されていますが、それに加えて技術組織全体への貢献を考えられる人として、エンジニアのキャリアパスのひとつに位置づけています。

社内勉強会

以前紹介した社内勉強会ですが、こちらは今も絶えることなく続いています。

  • 毎週持ち回りで2名ずつ何かしらの発表をする
  • 社内エントリのピックアップによる技術共有
  • 月に一回はヒットエントリを寿司とビールでお祝い

社内エントリのピックアップは、当初はプレゼンターがチーフ1人であったのをゲストを加えた2人にすることで、紹介がスムーズになりました。ポッドキャストっぽいイメージです。

月一のビールのある回では、途中から寿司だけでなく中華の出前も取るようになってよりパーティ感が増したと思ってます。社内勉強会(や社内エントリ)からの社外へのアウトプットも引き続き推奨しており、今年のアドベントカレンダーでもいくつかお見せできるのではないかと思います。エンジニアのアウトプットに関しては以前のエントリにある通りです。

サブ会

近ごろ明確に新しく導入した制度といえばこれくらいです。インターネットにおいてサービスを作り、維持していくための要素技術が高度化していく中、はてなでも旧来のユーザ向けサービスからその手を広げていっています。社内勉強会やはてなグループによる情報共有は日々行っていますが、より集中して取り組み、ときには具体的なタスクを受けもつ、技術的な競争力を持つエンジニアの小集団として(技術グループの)サブ会、というのを設けています。

各サービスの担当者として散らばっていたスマートフォンアプリエンジニアが組となった「スマート会」という会が社内には以前からあり、その取り組みを横展開したかたちです。よくも悪くも「有志」であった集団にある程度の裁量と責任をもってもらうことで、ボトムアップの活動を支援するねらいです。現在のところ、スマート会のほかにセキュリティ会、フロントエンジニア会といったものが立ち上がっています。社内では機械学習勢が盛り上がっているところなので、こういった分野からもやがて出てくるのではないかと思っています。

専門職10%ルール

以上を含むエンジニア視点からの取り組みというのは、理念としてはよいものであったとしても、エンジニアにしろマネージャにしろ、現実の業務の中では開発中のサービスやシステムを優先したくなってしまい、どうしてもスキマ時間だけの活動になってしまう慢性的な問題がありました。

そこで今期より、エンジニアが所属する縦の組織であるサービス・システム開発本部の取り決めとして、業務時間の一定割合を技術グループのタスクに利用することをマネージャ・エンジニア間で期初に合意しておき横断的な活動の障壁を減らす仕組みが導入されました。たとえばイベント登壇のためのプレゼン準備や執筆、突発的なチーム間の技術共有などに積極的に業務時間を使える仕組みです。これは現在のところ上手くいっていて、ある程度の時間の枠が設けられていることで、それを全て使うことはなくとも、不自由感をなくせているのではないかと思っています。ちなみに「専門職」と冠されているとおり、デザイナなどエンジニアに限らない仕組みです。

終了した取り組み

エンジニア実績システムは、導入当初こそわいわいと盛り上がりつつ空欄を埋めるムードでしたが、実績の解除自体のタイミングが導入後はあまりなく下火になっており、また、スプレッドシートで他人と較べる形であったのが却ってプレッシャーをかけてしまっていた問題もあって、deprecatedとしました。導入される取り組みがあれば消える取り組みもまたあり、です。


以上、このエントリを書いている時点でのはてなのエンジニア組織のスナップショットを書いてみました。もちろんこれで落ち着いているわけではなく、今も改善のためにできることをチーフ・シニアエンジニアを中心に考えているところです。興味ある人はぜひ採用に応募してみてください。アドベントカレンダー、明日は id:nanto_vi です!

はてなエンジニアアドベントカレンダー2016を始めます

はてなエンジニアアドベントカレンダー2016

こんにちは、はてなシニアエンジニアの id:y_uuki です。

今年もアドベントカレンダーの季節がやってきましたね。昨年のアドベントカレンダーの記事は以下のページにまとまっています。

明日の12月1日から25日まではてなのエンジニアの誰かが毎日エントリーを書く毎に、以下にリンクを追加していきます。お楽しみに!

12月4日

blog.yuuk.io

「Hatena Engineer Seminar #7 @ Tokyo」を12月6日(火)に開催します! #hatenatech

こんにちは。ウェブアプリケーションエンジニアの id:KGA です。

12月6日(火)に Hatena Engineer Seminar #7 @ Tokyo と題しましてエンジニア向けのセミナーを開催します。

今回はエンジニアリングの話に加え、普段とは少し趣向の違う はてなのエンジニアが普段どのようなことを考えどのような環境で働いているのかや、インターン生から新卒入社を経てチーフエンジニアになったエンジニアがいかに成長してきたのか、ディレクター兼務のエンジニアやセールスエンジニアがエンジニアとしてのバックグランドを活かしつつどのように活躍しているか、などエンジニアのキャリアや成長についてフォーカスした発表をご用意しました。また、トーク・LT の後にはパネルディスカッションも行う予定です。connpass のアンケート機能からいただいた質問や当日にいただいた質問もいくつか盛り込みたいと考えていますのでぜひご質問下さい。

セミナー後は、ささやかですが懇親会も予定しています。普段あまり語られることのないはてなのエンジニアの働き方やキャリアについてご興味のある方はぜひご参加ください!

イベント日程と会場

  • イベント名: Hatena Engineer Seminar #7 @ Tokyo
  • 日時: 12月6日(火) 19:20-21:45(19:00 受付開始 / 懇親会含む)
  • 参加費: 無料
  • 定員:50名(応募者多数の場合は抽選とさせていただきます)
  • 会場:株式会社はてな 東京オフィス SHIBAFU
    • 所在地:東京都港区南青山6-5-55 青山サンライトビル3F

タイムテーブル

時刻 名前 タイトル 時間
19:00 - 開場・受付開始 -
19:20 id:Songmu 開会の挨拶 5分
19:25 id:hakobe932 はてなで一人前のエンジニアになる方法 20分
19:45 id:Songmu TBD 20分
20:05 - 休憩 10分
20:15 id:y_uuki (LT) ウェブオペレーションの学び方 5分
20:20 id:a-know (LT) ぼくがセールスエンジニアというキャリアを選んだ理由(わけ) 5分
20:25 id:kiyosick パネルディスカッション
司会: kiyosick, パネラー: Songmu, hakobe932, y_uuki, a-know
40分
21:05 - 休憩・準備 5分
21:10 - 懇親会 軽食と飲み物を用意しています! 35分
21:45 - お開き -

※ 内容を変更する可能性があります

お申し込み方法

以下のページからお申し込みください。また、パネルディスカッションでパネラーに聞きたいことがありましたらぜひアンケートからご質問下さい。

hatena.connpass.com

はてなサマーインターン2016を終え、「はてな教科書」をアップデートしました

はてな教科書

こんにちは。アプリケーションエンジニアのid:yashigani_wです。

はてなは、短期間にWebアプリケーション開発の基礎を身につけるための教材である「はてな教科書」をgithubで公開しています。

これは新入社員研修やはてなサマーインターンでの講義に利用されており、毎年のはてなサマーインターンの一環として加筆・修正し更新しています。 今年は「はてな教科書JavaScript編」「SwiftでのiOSアプリ開発」「Web開発におけるコンピュータサイエンス - 機械学習編」を変更・追加しました。 Webアプリケーション開発の学習や研修のために自由にご利用いただけます。

はてな教科書JavaScript編

今年のはてな教科書では、JavaScript教科書を一新しました。

JavaSctiptの講義は内容が長大になってしまったため、他の教科書とは別レポジトリに移動しました。 また、教科書はこちらで読みやすい形式で公開されています。

内容は ECMAScript 2016 に基づき、文法や仕様の解説を更新しました。 加えて、現代のフロントエンド開発でほぼ必須となる Node.js や各種フレームワークについての記述を追加しています。

Web開発におけるコンピュータサイエンス - 機械学習編

はてなサマーインターン2016より機械学習の教科書を追加しました。 機械学習はWebアプリケーションにおける利用をターゲットに、基礎編・実践編の2部に分かれています。

基礎編では、初歩的な機械学習アルゴリズムを題材に、まったく機械学習についての知識がない方でもその基本概念と方法論を理解するために作られています。 演習も付随されているので、理論だけでなく実際に機械学習を実装してそのイメージを掴むことができます。 実践編では、一般に機械学習の教科書には取り上げられていない実データを扱うためのノウハウや、実務における機械学習のワークフローにを詳しく紹介しています。 より発展的な機械学習手法への導入としても最適な内容となっております。

はてなサマーインターンシップについて

はてなでは2008年より毎年、学生向けの夏期インターンシップを開催しています。Webサービス開発者としての技術を身につけ、実際にはてなのサービス開発を行う、実践的な内容となっています。はてな教科書は、はてなサマーインターンシップの中で生まれました。

はてな教科書の変更履歴

Web開発におけるコンピュータサイエンス - 機械学習編2

はてな教科書

この教科書は、はてなサマーインターンの講義資料として作成されたものです: https://github.com/hatena/Hatena-Textbook


機械学習編1(基礎編)では、最も初歩的な分類器である単純パーセプトロンを題材に、機械学習の基本について勉強しました。機械学習編2(実用編)では、実問題に機械学習を適用する上でのコツや、各種の機械学習アルゴリズムの使い分け、高次元データへの対処法、といったトピックについて解説していきます。

\def\bm#1{\boldsymbol{#1}} \def\set#1{\{#1\}}

実問題に機械学習を適用する

1日目の課題はいかがでしたか?難しかったですか?現実の問題は、1日目の課題とは比べものにならないくらい難しくなります。この章では、そのような実問題に立ち向かっていく方法について解説していきます。

タスクを定義する

まず、あなたの解きたい問題についてよく考えてみましょう。機械学習編1では、以下のような代表的な機械学習タスクを紹介しました。

  • 教師あり学習
    • 分類: カテゴリを予測する
    • 回帰: 数量を予測する
  • 教師なし学習
    • クラスタリング: データを(未知の)グループに分ける
    • 次元削減(次元圧縮): データの特徴量の次元を下げる
    • 頻出パターンマイニング: データから法則性を見つける
    • 異常値検出: データから統計的な外れ値を見つける

まず最初に、あなたの問題が、これらのどの問題に当てはめられそうかを考えてみましょう。

たとえば、数字の画像認識の場合を考えてみます。数量を予測するので回帰問題のように思えてしまうかもしれませんが、これは分類問題にあたります。画像にどの数字が書かれているか を知りたいのであって、5の画像が3の画像よりどれくらい大きいとかいったことには興味はないからです。また、数字は0から9までですので、10クラスの多値分類問題ということになります。

このように、実問題を既存のタスクに結びつけることがまず第一歩です。機械学習で解決できる問題のほとんどが、これらのタスクのいずれかに帰着できます。このステップでは、ものごとを抽象化して考える思考法が重要になってきます。具体的に機械学習で解かれている問題の事例などを知ることで、だんだんできるようになるでしょう。

データを特徴ベクトルに変換する

つぎに、データからなんらかの方法で特徴を抽出し、実ベクトル(実数を要素にもつベクトル)に変換する方法を考えます。多くの機械学習アルゴリズムは実ベクトル空間を前提としています(これとは別に、ナイーブベイズなど、確率モデルを基礎とするアルゴリズムもあります)。ここで変換したベクトルのことを 特徴ベクトル (feature vector) といいます。

このステップでは、問題に対するドメイン知識が最も重要になります。たとえば分類問題の場合では、分類するために重要な特徴量を選ぶのが鍵になります。データをよく観察し、隠れた構造を見出すようにしましょう。

最初から特徴量を完璧に特定しなくても構いません。特徴量は多めに出すようにするのがおすすめです。無駄な特徴量は、この後のプロセスで見つけ出して取り除いてしまうことができるからです(この後の特徴選択の項目で説明します)。

データをベクトルにできればこっちのものです。これで現実の問題を機械学習アルゴリズムの問題に変換することができました!

評価方法を決める

次に、機械学習アルゴリズムをどう評価するかを考えましょう。評価方法の設計はとても大事です。当たり前のことですが、決めた評価軸のなかでしか、得られたモデルの「良さ」は計れません。どのような機械学習アルゴリズムが欲しいかを考えて、評価指標を選ぶようにしましょう。評価指標には、以下のようなものがあります。

  • 適合率 (Precision)
  • 再現率 (Recall)
  • F値 (Fスコア、F_1スコアとも)
  • 混同行列 (confusion matrix)
  • ROC曲線
  • AUC (Area Under the Curve)
  • 尤度・対数尤度 (確率モデルの場合)

一例として、機械学習による分類器を自分たちのWebサービスに組み込むことを考えてみます。たとえば、たくさんの商品のなかから、ユーザに商品をおすすめするレコメンドエンジンを作っているとしましょう。この問題は、教師あり学習における分類のタスクとして定義してみます。つまり、「この商品をユーザが見ているページに出すべきかどうか」を判定する2値分類器をつくることを考えます。このとき、あなたはどんな分類器が欲しいでしょうか?

この問題設定の場合、あまりにたくさんの商品を見せられても、ユーザは選びきれません。できるだけノイズが少ない分類器が望ましいでしょう(実務ではありがちな要求です)。このように、再現率 (recall) よりも、適合率 (precision) を重視したい場面は、開発の現場でしばしば直面します。そういう場合は、適合率寄りのF_\betaスコアを使って評価するとよいでしょう。F_\betaスコアの式は以下の通りです。どこかで見たことのある形ですね。

\displaystyle F_\beta = (1 + \beta^2)\cdot\frac{\text{precision}\cdot\text{recall}}{(\beta^2\cdot\text{precision})+\text{recall}}

実は、F_\betaスコアはF_1スコアの拡張になっています。\beta = 1のときF_1スコアと一致することが確認できますね。

\displaystyle F_1 = 2\cdot\frac{\text{precision}\cdot\text{recall}}{\text{precision}+\text{recall}}

また、\beta = 0のとき適合率 (precision) と一致することも簡単に確かめられます(逆に、\beta \rightarrow \inftyのとき再現率 (recall) に近づきます)。つまり、適合率重視にしたい場合は、\betaを小さくすればよさそうです。どのくらい小さくすればよいかはタスク次第ですが、まずは\beta = 0.5F_{0.5}スコアなどでやってみるとよいでしょう。こうすることで、ある程度の再現率を担保した状態で、適合率を高めた分類器を得ることができそうです。

逆に、商品のR18判定の分類器もつくったとしましょう。多少誤分類があってページに出してもよい商品を落としてしまったとしても、ユーザに不快な思いをさせたり、サービスのブランドを毀損するよりはまし、といった状況もあるでしょう。この場合には再現率が高めのものがほしいので、再現率寄りのF_\betaスコアを使って評価するとよさそうです。たとえば\beta = 2.0F_2スコアなどです。

論文では、平均的に性能がよいアルゴリズム、たとえば AUC やF_1スコアが高いアルゴリズムが一般的には良しとされます。しかし、本当になにが重要かは、実際のタスク・アプリケーションによって異なります。このように、どんな指標で評価するかは、機械学習を使ったプロダクトの品質を保つうえで重要になってきます。

重要なのは、1つの軸で評価する ということです。これにより、ハイパーパラメータの調整など、機械学習のトライ&エラーがやりやすくなります。具体的には、F_\betaスコアやF_1スコア、AUCあたりがおすすめです。

正解データの正例と負例は均等に

2値分類では、学習データのラベルには +1 (正例)と -1 (負例)の2種類の事例があります。このとき、正例と負例ができるだけ同じくらいの数になるように注意します。正例のデータをたくさん持っているなら、できるだけ負例のデータを集めるようにしましょう(アノテーションにおいて、分類器の性能向上に有効なデータを選んでくれる能動学習 (Active Learning) という手法もありますので、興味のある方は調べてみてください)。偏りがあると、望んだ結果は得られません。データのクラス間に極端な偏りがあるようなデータセットのことを、不均衡データ (imbalanced data) とよびます。たとえば、がん検診を2値分類の機械学習で行うことを考えてみましょう。この場合、本当に +1 (陽性)である事例は非常に少なく、 -1 (陰性)であることがほとんどでしょう。だからといって、常に -1 (陰性)と答える分類器が学習されても(見かけ上のスコアは高くなるかもしれませんが)役に立ちません。

とはいえ、そもそもデータを恣意的に選んでアノテーション(正解ラベルを付けること)をすることができない場合もあります(そもそもデータを恣意的に選んでよいかという問題もありますが)。このような場合には、学習アルゴリズムで工夫することもできます。単純パーセプトロンでは、分類を間違えたときに、分離超平面からの距離に応じて重みw_iを更新していましたね。大きく離れている次元iに対しては重みw_iを大きく増やし(もしくは減らし)ていました。そこで、たとえば +1 (正例)が少ない場合には、正例を誤って -1 (負例)と判定してしまったときには、すべての次元に対して、大きく重みを更新することにします。つまり、各クラスのデータ数に応じて重み付けをするということです。正例:負例が1:3なら、正例を間違えたときには、たとえば3倍の幅で更新する、といった具合です。以上はヒューリスティックな方法ですが、分類を間違えた場合の罰則(コスト)を学習アルゴリズムに組み込んだ コスト考慮型学習 (cost-sensitive learning) と呼ばれる、より一般的な学習手法も提案されています。興味のある方は調べてみるとよいでしょう。

不均衡データへのもうひとつの対策法として、機械学習アルゴリズムに与えるデータセットを工夫する方法もあります。例えば少ないほうの事例をオーバーサンプリングする、多いほうの事例をサブサンプリング(アンダーサンプリング)する、またはその両方を実施する、などです。

不均衡データの扱いについては、以下も参考にしてみてください。

ベースラインとなる手法を実装する

機械学習アルゴリズムを実装するときに大事なのは、まず最初に もっとも簡単な方法で実装・実験・評価を行う ことです。最初は次元削減などの高度な処理もせずに、ナイーブにやってみることを心がけましょう。ここで出てきた結果がベースラインとなり、この後により高度な手法を試すときに、どれくらい良くなっているのかを定量的に比較することができます。

ベースラインを実装したときに、あまりにも性能が低いときは、タスク設定自体を疑いましょう。難しすぎる問題を解こうとしていないでしょうか。たとえば、自然言語処理は近年急速に発展してきましたが、ことばの意味を扱うようなタスクでは苦戦しています。二値分類問題で正例・負例がそれぞれ半分ずつのテストデータに対して、正解率 (accuracy) が 0.5 しかない(50%しか当たらない)ようであれば、コイン投げと同じようなものなので、問題設定自体を考えなおしたほうがいいかもしれません。

また、ベースラインを実装、評価してみることで、データの量が少ない、特徴ベクトルに有効な特徴が含まれていない、などの問題に対してもある程度当たりをつけることができます(機械学習編1の 学習曲線による分析 も参照)。

さらに、より複雑な手法を実装した際にバグを入れてしまった場合、より早く気付くことができます。少しずつ工夫を足していって、性能の向上を積み重ねていくようにしましょう(ただし実際には、複雑な手法でほんの少し精度が上がるより、実は簡単な手法のほうが予測が速かったり、メンテナブルであったりして有用なこともあります)。

実データに向き合うときの心構え

現実のデータは汚いです。思った以上に汚いです。なにか精度が出ない、といったときには、エラー分析が大事になってきます。うまくいかないからといって機械学習アルゴリズムを変えるまえに、実際のデータを見てみましょう。どのような事例で誤分類しているかを観察してみます。変なデータが混ざっていたり、前処理にバグがあるのを見つけられるかもしれません。

それに関連して、前処理しても必ず生データは捨てないようにしましょう。前処理を失敗していたときにまたデータを取得するところからやり直しになってしまいます。

また、トライアンドエラーするときには、ある程度前処理した中間ファイルを使ってもよいでしょう。中間ファイルを使うことで、時間がかかる前処理をスキップすることができます(ただし、そのぶん管理は多少煩雑になります)。

生データや中間ファイルの管理法については、以下のエントリーが参考になるでしょう。

機械学習のワークフロー

前の章では、実問題を扱ううえでのいくつかのトピックを紹介しました。ここでは、実際に何をどのような順番で進めていくかについて、ざっと俯瞰していきます。大まかには、以下のようなステップで進めていくことになります。

  1. 前処理
    • データセット作成
    • サンプリング
    • 特徴抽出
    • 欠損値対応
    • スケーリング
    • 特徴選択
    • 次元削減
  2. 学習
  3. 評価・改善
    • 学習曲線を描画
    • エラー分析と対策
  4. 適用
    • システムへの組み込み

機械学習アルゴリズムに入力されるデータセット(実ベクトルからなるデータの集まり)を作成・加工するのが 前処理 (preprocessing) と呼ばれるフェーズです。そのデータを学習アルゴリズムに入力し、学習済みのモデルを得るフェーズが2つめの 学習 フェーズです。次に、得られたモデルの 評価(evaluation) を行います。ここで、満足する結果が得られなければ、前処理もしくは学習フェーズでの条件を変えて、再度評価を行います。これが 改善 のフェーズです。ここでは、なぜうまくいかなかったかを自分なりに分析し、対策を考えるのが重要になります。評価・改善の結果、求める性能が得られれば、実システムへの 適用 を行います。また現実には、この後にも運用のフェーズが必要になることが多いと思いますが、その方法は個別の事例ごとに大きく異なるため、ここでは扱いません。

参考: Python 機械学習プログラミング 1.7 機械学習システムを構築するためのロードマップ および 6.1.2 パイプラインで変換器と推定器を結合する

1. 前処理

データセット作成

まず最初に、データセットを作成する必要があります。自分たちのデータを使う場合には、それを取得します。RDBにデータが入っているなら、SQLでデータを抽出することになります。逆に、自分たちでデータを持っていない場合には、データの持ち主にお願いしてデータを送付してもらう、クロールする、などの作業が必要になります。

また、実際には、取得した生データに対してなんらかの加工をすることが多いでしょう(テキストの場合には Unicode 正規化や、Webページを対象にする場合には、 HTML タグの除去や本文抽出をするなど)。この部分が性能に大きく影響することもあるので、正しく抽出・加工ができているか確認してから次に進むようにしましょう。

サンプリング

作成したデータセットをランダムサンプリングし、訓練セット (training set)テストセット (test set) の2つに分割します。訓練セットというのは、実際に機械学習アルゴリズムの学習に使うデータセットのことです。もう1つのテストセットは、学習した結果であるモデルのよさを評価するためのデータセットです。

どのくらいの割合をテストセットにするかは、自明ではありません。訓練にはできるだけたくさんのデータを使いたいのですが、テストセットの数が少なくなってしまうと評価結果が信頼できないものになってしまいます。データセット全体がどのくらいの量かにもよるので一概には言えませんが、一般的な目安としてはテストセットにはデータセット全体の1割〜3割くらいを使うことが多いと思います。このように、データセットを2つに分割し、一部をテストセットとする方法を ホールドアウト法 (holdout method) と呼びます。

より発展的な方法として、 交差検証 (cross-validation) があります。 k分割交差検証 (k-fold cross-validation) は、データセットをk個に分割し、そのうちの1つをテストセット、残りを訓練セットにします。この設定で評価したあと、k個のうち、別のものを選んで、それをテストセット、残りを訓練セットにします。これをk回繰り返すことで、すべてのデータがテストデータとして1回ずつ選ばれることになります。k回の評価結果の平均を最終的な評価結果とします。こうすることで、テストセットの選ばれ方による影響をできるだけ排した、より公平な評価が可能となります。データ数をNとしたとき、k=Nの場合のk分割交差検証を Leave-one-out交差検証 (Leave-one-out cross-validation, LOO または LOOCV) と呼びます。

kの選び方はデータ数やコンピューティングリソースによって変わってきます。kを小さくすると誤差が大きくなって結果が信用できなくなりますが、逆にkを大きくするとその回数だけ学習・テストを繰り返すわけですから、実行時間が長くなります(ただし分散実行は可能です)。つまり、評価結果の信頼性とコンピューティングリソース(計算時間)とのトレードオフになるわけです。データが少ない場合は、計算時間はそれほど問題ではなく、逆にデータが少ないことによる評価結果のばらつきは大きくなるため、kを大きく(理想的にはLOO)するのがよいでしょう。逆にデータが多い場合にはkを小さくして、現実的な時間で計算が可能な範囲とします。kの選び方と評価結果のばらつきについては、以下のエントリーで詳しく考察されていますので、ぜひ参考にしてみてください。

特徴抽出

機械学習アルゴリズムに入力するためには実ベクトルにする必要があります。非ベクトルのデータの場合(テキスト・画像・音声など)は、 なんらかの方法で 特徴ベクトルに変換します。

このとき、できるだけデータの性質をよく表すようなベクトルにしてあげると、結果として得られるモデルの性能が向上します。実は、この特徴抽出が最も精度に影響する部分といっても過言ではありません。

当然、データやタスクによって特徴抽出の方法はまったく異なります。テキスト(自然言語)の場合、単語分割が必要になりますし(あとの 自然言語処理 (NLP) と機械学習 の章で詳しく説明します)、画像処理の場合、 SIFT や SURF などの画像特徴量を使う方法が伝統的によく用いられています。

欠損値・欠測値への対応

データがそもそもない、計測の失敗などの原因で、 欠損値・欠測値 (missing value) が発生することがあります。これらに対してどのように対処するかも大事なトピックです。どのように扱えばよいかはタスクの性質にもよりますが、以下のような方法が取られることが多いです。

  • 欠測値をもつデータを削除 (drop)
  • 欠測値を補完 (interpolation/imputation)

詳しくは、朱鷺の杜の以下のエントリーが参考になるでしょう。

値のスケーリング

ここまでで、実ベクトル(特徴ベクトル)が得られました。ほとんどの機械学習アルゴリズムは、与えられたベクトルの次元ごとに値のスケールがバラバラだと、性能を発揮できないことが知られています(たとえば、ベクトルの中のいくつかの要素だけ異常に値が大きいとか)。具体的には、学習が収束するまでにすごく時間がかかったり、得られたモデルの精度が悪かったり、といった弊害があらわれます。詳しく知りたい方には、参考として以下を挙げておきます。

対処法としては、すべての次元でだいたい同じような値の範囲になるよう、スケール変換(スケーリング)することが挙げられます。現実的には、以下の2つの手法のどちらかが使われることが多いです。

  1. 標準化 (standardization)
    • 平均 0 分散 1 にスケーリング
    • Z-score によるスケーリング
    • 以下の計算式で Z-score を計算する
      • \displaystyle z = \frac{x - \mu}{\sigma}
    • 外れ値の影響が小さい
  2. 正規化 (normalization)
    • [0, 1]などの固定の範囲にスケーリング
    • 最小値が0、最大値が1になるように
    • Min-Max スケーリングとも呼ばれる
    • 値が特定の範囲内に入っていることが必要な場合
    • 以下の式で正規化する
      • \displaystyle x_{norm} = \frac{x - x_{min}}{x_{max}}

スケーリングについては、以下のエントリーが詳しいです。

特徴選択

得られた特徴ベクトルは、特徴量の設計次第では非常に高次元になる(ベクトルの要素数が多い)ことがあります。特徴ベクトルが高次元になると、学習に多くの計算リソースを要したり、過学習が起こりやすくなったりします。これらの特徴量(次元)のうち、重要なものだけを残す手法が 特徴選択 (feature selection) です。

特徴量の選択は、一般には組み合わせ最適化問題になります。特徴空間の次元数をNとすると、その取りうる組み合わせは2^{N}です。その中から、最適な組み合わせを選ぶことになります。特徴空間が高次元の場合、とりうる組み合わせは膨大な数となり、ナイーブな方法では計算量が爆発して実行不可能です。

そこで、通常は貪欲法で特徴選択を行うことになります。これは、何らかの関数を使って、各特徴量(各次元)に対して重要度を計算し、重要度が最大の特徴量を順番に選んでいく方法です(参考:Python 機械学習プログラミング 4.5.2)。

ランダムフォレスト (Random Forest) という機械学習アルゴリズムを使って特徴量の重要度をランク付けすることもできます(参考:Python 機械学習プログラミング 4.6 ランダムフォレストで特徴量の重要度にアクセスする )。逆にいうと、ランダムフォレストは特徴選択が組み込まれた学習アルゴリズムと考えることもできます。)

特徴選択についてさらに詳しく知りたい方は、 統計的学習の基礎 pp.69- 3.3 変数選択 を読んでみてください。

次元削減

次元削減 (dimension reduction) は、高次元な特徴ベクトルを低次元なベクトルに変換する手法です(たとえば数千次元のデータを数十次元に圧縮するなど)。次元の呪い(特徴空間が高次元だと学習がうまくできない現象)による影響が緩和されて、精度が向上することがあります。また、学習の高速化・省メモリ化も期待できます(もちろん、次元削減のための計算は余分に必要になります)。ただし当然、予測するときもベクトルの変換が必要になることに注意しましょう。学習結果の解釈も難しくなります。

他にも、可視化という応用もあり、2次元〜3次元に次元削減することで図にすることができます。高次元データを目で見ることができるようになるため、データの特徴をつかむことができるかもしれません。

2. 学習

実際に訓練データを与えて学習させるフェーズです。前処理フェーズでも、この学習フェーズでも、 テストセットのデータを使ってはいけないことに注意 しましょう(テストセットを見てしまうことは、テストでカンニングをしている、つまりズルをしていることになります)。

モデル選択(ハイパーパラメータの調整)

学習アルゴリズムには通常、ハイパーパラメータ(最適化の対象となるモデルのパラメータとは別のパラメータ)が存在します。たとえば、機械学習編1の パーセプトロン の節で解説した、単純パーセプトロンの学習率\etaがそれです。他にも、学習アルゴリズムに正則化を導入した場合の正則化パラメータや、ニューラルネットワークの層の数や各層のユニット数もハイパーパラメータといえます。学習フェーズでは、ハイパーパラメータを指定する必要がありますが、どんな値を使えばよいかは、データセットによって異なります。ハイパーパラメータの選択は、一般には モデル選択 (model selection) という分野になります。

試行するパラメータの選び方

一般に、とりうるハイパーパラメータの組み合わせは無限にあります。ハイパーパラメータのパターンを選択する方法には、以下のようなものがあります。まずは最も基本的でわかりやすいグリッドサーチから試してみるとよいでしょう。

  • グリッドサーチ (Grid Search)
    • 各ハイパーパラメータの範囲を指定し、それらの組み合わせのパターンをすべて試行する
    • ハイパーパラメータが2つあり、それぞれ5パターンずつ試行するとしたら、5x5=25通りのパターンを試行する
  • ランダムサーチ (Random Search)
    • 各ハイパーパラメータについて、ランダムな値を選択する
  • ベイズ最適化 (BO; Bayesian Optimization)
    • ハイパーパラメータを選択し、得られた評価結果から、よいと思われるハイパーパラメータを自動選択して試行する
    • この教科書の範囲を超えているため、興味のある方は各自で調べてみてください
    • 「ベイズ最適化」でGoogle検索

検証セット

ハイパーパラメータ選択の最もナイーブなアプローチは、いくつかのパラメータの組み合わせで実際に訓練セットで学習してみて、訓練セットに対する評価がもっとも良かったもの(訓練誤差が小さいもの)を選択するという方法です。しかし、このナイーブな方法には問題があります。特定の訓練セットに対して結果がよかったからといって、他のデータ、たとえばテストセットに対して良い性能が得られるとは限らないからです。

機械学習編1の 訓練データとテストデータ の節で説明したとおり、私たちの目標は汎化性能の高いモデルを得ることです。そこで、訓練セットをさらに2つに分割し、1つを 検証セット (validation set) 、残りを訓練セットとします(検証セットは開発セット (development set) とも呼ばれます)。ハイパーパラメータの組み合わせに対して、検証セットを選んだ後に残った訓練セットで学習したあと、検証セットで性能を評価します(このようにデータセットを2つに分割することをホールドアウト法というのでした)。検証セットを見ないで学習したことで、汎化性能の高いモデルとハイパーパラメータの組み合わせを得られることが期待できます。

モデル選択における交差検証

検証セットを使ったハイパーパラメータの選択にも、もう1つ問題があります。検証セットは訓練セットの一部しか使っていないため、ある意味で公平な結果とは言えないからです。ここでも交差検証の手法が役に立ちます。k分割交差検証を使って検証セットを選ぶことで、よりよいハイパーパラメータを得ることができるでしょう。なお、試すハイパーパラメータを10パターン、テストセットの選択に10分割交差検証、検証セットの選択に5分割交差検証を使った場合、500回の訓練・テストを繰り返すことになります。このように交差検証を組み合わせる方法を 入れ子の交差検証 (nested cross-validation) と呼び、この場合は、10x5交差検証となります。このスキームについては、Python機械学習プログラミング 6.4.2 入れ子式の交差検証によるアルゴリズムの選択 に詳しく書かれています。

3. 評価・改善

評価方法を決める で決定した評価指標で、取っておいたテストセットに対して評価を行います。求める性能に達しなかった場合には、なんらかの対策を打つことになります。

テストセットのデータに対する前処理

このフェーズでは、とっておいたテストセットを使って評価をしますが、このとき、スケーリングや特徴選択、次元削減のパラメータは、前処理フェーズ・学習フェーズで得られたものを使うことに注意しましょう。逆に言うと、繰り返しになりますが前処理フェーズ・学習フェーズではテストセットのデータを使ってはいけません。スケーリングなどの前処理も含めてモデルであると考えるとよいでしょう。

エラー分析と改善のプロセス

評価した結果、満足いく性能が得られなければ、何らかの工夫をする必要があります。

  • 特徴量を見直す?
  • データを増やす?
  • モデルを変える?
  • 学習アルゴリズムを変える?

学習曲線を見たり、エラー分析をするのが対策の第一歩です。学習曲線については、機械学習編1を参照してください。エラー分析では、実際にエラー(false positive/false negative)になっているインスタンス(事例)を見てみるのが重要です。そもそもデータそのものにノイズが多い、なども考えられます。たとえば、ラベルを付ける人の基準がぶれていた、といったことが見つけられるかもしれません。このような場合、データセットのタグ付け(教師ラベル付け)を見直してみる、などの打ち手が考えられるでしょう。

機械学習アルゴリズムのデバッグ

まずは、学習曲線を見て、イテレーション数に応じて誤差が減っていることを確認しましょう。減っていかない場合、どこかにバグがある可能性が高いです。機械学習アルゴリズムのデバッグは難しいですが、根気強く頑張りましょう。

パラメータの値が途中で発散している場合、以下を確認しましょう。

  • 更新式がちゃんと実装されているか
  • 計算誤差の問題が起こっていることも
  • そもそも更新式の導出(手計算)が間違っていることも

とくに更新式の実装のバグは、一見正しく動いているように見えたりして非常に気付きにくいので注意しましょう。他に、機械学習アルゴリズムを実装するときのコツとしては、細かくテストする、既存のライブラリによる実装と結果を突き合わせてみるなどがあります。また、既存実装のソースコードを読むのも勉強になります(とくに scikit-learn は様々な機械学習アルゴリズムをサポートしており、コードも読みやすくおすすめです)。

4. 適用

システムへの組み込み

Web開発においては、機械学習アルゴリズムはシステムに組み込まれ、運用されることで価値を発揮します。この後の 機械学習プログラミング の章で詳しく説明していきます。

機械学習プログラミング

ここまでで、機械学習を使った開発の進め方について、ある程度イメージできたと思います。ただ、これらを実行するためには、なんらかのプログラミングをする必要があります。

これを読んでいる方は、いくらかのプログラミングの経験があると思いますが、機械学習を実際のサービスに適用するとなると、通常のWebプログラミングにはない特有のポイントがあります。この章ではそのうちの一部のトピックを紹介します。

機械学習に使われるプログラミング言語

まずは、どの言語で実装するかを決めなければなりません。大きくは、プロトタイピングによる試行錯誤に向いている言語と、サーバサイドやバッチで実行するのに向いている言語の2つに分けられます。

  • プロトタイピングに向く
    • R, Python, Octave/MATLAB
  • 実運用、ハイパフォーマンス用途に向く
    • Scala, Java, C, C++

R, Python, Octave/MATLABはインタプリタで対話的に実行できることや、機械学習・統計・科学技術計算のライブラリが豊富なこともあり、多くのトライアンドエラーを行う、機械学習を使った開発では非常に有用です。一方、サーバサイド(バックエンド側)で運用するには、比較的パフォーマンスの問題が起こりやすいため、プロトタイピングして実用性を評価したあと、本番環境の言語(たとえばJavaなど)で実装しなおして組み込まれることもしばしばあります。

Scala, Javaはサーバサイドに組み込むには良い選択肢だと言えます。C, C++には一歩及ばないものの、パフォーマンス面でも問題ありませんし、かっちり実装するにはもってこいです。

C, C++は非常に高速な反面、メモリ管理やバグに悩まされるかもしれません。科学技術計算系のライブラリは利用しやすいので、機械学習アルゴリズムをスクラッチで組むには良いでしょう。また、多くの言語でCやC++で書かれたコードを呼び出す仕組みがありますので、コア部分のみCやC++で実装するアプローチも有効です。

機械学習に関して、現状もっともエコシステムが発達しているのはPythonでしょう。機械学習フレームワークのscikit-learnはデファクトの地位を築きつつあります。一方、R もライブラリが充実しています。どちらかというと、機械学習よりも統計的な手法に強いイメージがあります。

また、最近では書きやすさを保ちつつ高速な処理を実現したJuliaも登場しており、今後注目です。

その他の言語やフレームワークについては、以下の書籍が参考になります。さらに、R, Python(scikit-learn), Julia, Spark/MLibによる学習と予測について、具体的なコードが掲載されていますので、それぞれの雰囲気を掴むのによいでしょう。とくにscikit-learnについては実例が豊富です。

  • データサイエンティスト養成読本 第2部

大規模データの取り扱い

データセットが大きくなると、コモディティーな計算機ではメモリに乗り切らなくなります。これでは学習ができません。どうすればいいでしょうか。

一般的な解決策は、メモリ使用量が、データサイズに比例しないようなプログラムにすることです。たとえば、バッチ学習ではデータセット全体をメモリに乗せるため、巨大なデータセットでは学習途中でメモリが枯渇して異常終了してしまうでしょう。この場合、オンライン学習を検討しましょう。オンライン学習やミニバッチでは、データを少し読み込んだあと、そのデータを捨てる(=メモリから解放する)ことができます。他にも、機械学習アルゴリズムによっては大規模な問題に対応した拡張があったりしますので、適宜調べてみてください。

さらに大規模なデータセットを取り扱いたい場合には、ミドルウェアやクラウドサービスを利用することを考えましょう。

ミドルウェアと機械学習での用途

ここでは、機械学習を使った開発で利用することになるかもしれないミドルウェア・クラウドサービスと、考えられるそれらのユースケースを紹介します。ここで挙げた以外にもたくさんのミドルウェアが存在し、これからも増えていくでしょう。

リレーショナルデータベース(RDB)

データの格納・インデックスを使った検索ができます。信頼性が高く、運用も行いやすいため、データの格納場所としては第一の候補になるでしょう。ただし、大規模構成ではそれなりにノウハウが必要になります。また、フルスキャンするような用途には向きません。

全文検索エンジン

自然言語テキストの格納・検索に向いていますが、数値や位置情報などテキスト以外のデータを格納・検索することもできます。テキストの検索が高速に行えるため、自然言語処理(NLP)用途ではRDBよりもこちらが優れています。とくにElasticsearchはAggregationという機能で複雑な集計が高速に行えるため、応用範囲が広く、実際にはてなブックマークでも様々なところで使われています。ただし、全文検索エンジンはあくまでインデックスという位置づけのため、データをロストしないためにもRDBに置いた上で利用するのがよいでしょう。

分散処理フレームワーク

Hadoopは巨大なデータをフルスキャンするような重いI/Oのバッチ処理でも、分散処理することで比較的高速に捌けるという利点があります。Hadoopの上で動作するApache MahoutHivemallという機械学習ライブラリがあります。たくさんのマシンで学習を分散処理できるため、学習時間が問題となる場合は検討してみるとよいでしょう。

一方、Sparkも分散処理フレームワークですが、こちらはオンメモリでの処理が得意という違いがあります。また、Sparkの上で動作するMLibというライブラリには、多くの基本的な機械学習ライブラリが実装されています。Mahoutと同じく、学習を分散処理できますが、こちらはスループットよりレイテンシを重視する用途に向いています。

Jubatusはオンライン学習向けの機械学習フレームワークです。複数のマシンでそれぞれ学習を行い、それらの学習モデルを交換・共有することで、学習の分散処理を実現しています。

クラウドストレージ

クラウド上にファイルのアップロード・ダウンロードができます。実験用のデータセットや、学習済みのモデルファイルを置いて配布したりするのに便利です。転送量に対して多く課金されるため、重いデータを頻繁に出し入れするには向いていません。

機械学習アルゴリズムをミドルウェアに載せる

メモリに載りきらない大規模なデータに対しては、ディスクを使う(もしくはApahe Sparkのように複数のコンピューターで分散処理する)必要があります。通常、なんらかの既存のミドルウェアを利用することになると思います。自分でミドルウェアのようなものを作るのは、メンテナンスコストなど、労力に対してペイしないでしょう。

上で紹介したMLib, Mahout, Hivemall, Jubatusのように、ミドルウェアを活用した機械学習フレームワークも多数登場しています。ただし、自分たちが使いたい機械学習アルゴリズムが実装されていないこともあるため、その場合は自分たちで書く必要があります(多くの機械学習フレームワーク・ライブラリはオープンソースです)。

機械学習アルゴリズムをミドルウェアに載せるための方法論はケースバイケースで、自明ではありません。プロダクトやそれぞれの環境によって最適解は異なります。ミドルウェアも年々進化していますし、新しいものもどんどん出てきています。アーキテクチャの選定においてもっとも大事なのは、バランス感覚です。

  • 学習・実行、それぞれの時間的・空間的計算量は?
  • 運用時の計算機環境(リソース)は?
  • メンバーの持っているスキルは?
  • そのソフトウェアのコミュニティーは活発か?

これら以外にもさまざまなパラメータがあるでしょう。あなたの腕の見せ所です。

代表的な機械学習アルゴリズムの紹介

この章では、代表的な機械学習アルゴリズムの名前を紹介します。それぞれのアルゴリズムの詳細については説明しませんので、これらを実際に適用したり、他の高度な手法を探すときには、ここに書かれたキーワードやリンクから各自調べてみてください。

教師あり学習

  • 線形モデル
    • 単純パーセプトロン, 線形SVM, ロジスティック回帰(MEM)
  • 非線形モデル
    • (多層)ニューラルネットワーク, 決定木
  • 事例(インスタンス)に基づく手法(モデルのなかにデータが含まれる)
    • k-NN, 非線形SVM (ref. 言語処理のための機械学習)

教師なし学習

  • クラスタリング
  • 次元削減
    • PCA, kernel PCA, ICA, SVD(特異値分解), NMF, MDS(多次元尺度構成法), t-SNE
    • (NLP 系) LSA/LSI, pLSA/pLSI, LDA
    • 参考: すべてがMFになる - Fire and Motion SVD, PCA, CUR, NMF あたりのまとめ
  • 頻出パターンマイニング
    • アプリオリアルゴリズム
  • 単語の表現学習(NLP での次元削減)

アンサンブル学習

  • バギング
  • ブースティング
    • AdaBoost
  • ランダムフォレスト

機械学習アルゴリズムの使い分け

前の章で、たくさんの機械学アルゴリズムを紹介しました。この中から、あなたの問題に合わせたアルゴリズムを選択しなければなりません(もちろん、複数の機械学習アルゴリズムを組み合わせて解くこともできますが、まずは1つずつ試してみましょう)。ここでは、どういうときにどのアルゴリズムを選択するかの指針を紹介します。

まずは、あなたのタスクの設定を振り返ってみてください。どんな機械学習アルゴリズムを使えばいいか、おおまかには絞り込めるはずです。

  • データセットは教師あり?教師なし?
  • タスクは分類?回帰?クラスタリング?次元削減?可視化?

次に、機械学習アルゴリズムの特性や得意分野で選びます。万能のアルゴリズムは存在しません。どんな機械学習アルゴリズムにも、不得意な領域は存在します(このことをノーフリーランチ定理といいます)。目的に合わせて手法を選ぶようにしましょう。たとえば、以下のような基準があります。

  • データ量・次元数は多い?少ない?
  • 欠損データ・ノイズは多い?少ない?
  • モデルが出した結果の説明性は必須?

実際にどれを選べば良いかについては、利用しようとしているフレームワーク・ライブラリのドキュメントが参考になるでしょう。たとえば、scikit-learnAzure Machine Learningには、チートシートとよばれるマニュアルが提供されており、それに従って機械学習アルゴリズムを選べるようになっています。

また、統計的学習の基礎 10.7 データマイニングの「万能」手法 (p.401) にも、いくつかの機械学習アルゴリズムについて、特徴がまとめられています。

機械学習の研究の最先端では、どんどん新しい手法が発見されていますが、実務では、複雑で性能のよいアルゴリズムより、性能は少し劣っても単純なアルゴリズムが好まれる傾向にあります。実運用で求められる要件は、以下のようなものです。

  • 数パーセントの精度より運用のしやすさ
  • 精度が安定している
  • 予測が速い(サーバー費用の削減・応答速度の向上)
  • アルゴリズムの理論や実装が簡単(バグがあったときに原因を見つけやすい・メンテナンスしやすい)
  • あとから手で微調整しやすい

複雑だが性能のよいアルゴリズムを検討するとき、上記の要件と照らし合わせて、本当にその精度・パフォーマンスが必要なのか、よく考えてみることが大事です。あとであなたや同僚が困らないように。

自然言語処理(NLP)と機械学習

この章では、自然言語処理(NLP)の概要と、そこで必要になる特有のテクニックを紹介します。

代表的な自然言語処理タスク

さまざまな自然言語処理タスクで機械学習が応用されています。たとえば以下のようなタスクです。

  • 文書分類(スパム判定など)
  • 文書要約
  • 情報抽出
  • 著者推定・属性推定(性別推定など)
  • かな漢字変換
  • 情報検索
  • 情報推薦(レコメンド)
  • 評判分析
  • 音声認識
  • 機械翻訳
  • 質問応答
  • などなど

このような応用以外にも、自然言語処理には要素技術となる基礎的なタスクがあります。代表的なものは以下です。

  • 形態素解析(分かち書き)
  • 構文解析
  • 述語項構造解析
  • 意味解析
  • 照応解析
  • 共参照解析
  • 語義曖昧性解消(多義性解消)
  • 固有表現抽出 (NER; Named Entity Recognition)
  • 関連語・類義語抽出
  • キーフレーズ抽出
  • など

これらの基礎的なタスクでも機械学習が利用され、性能の向上に成功しています。こうして学習されたものは、ツールとして公開されていることが多くあります。代表的なものに、日本語の形態素解析を行うMeCabや、係り受け解析を行うCaboChaがあります。

自然言語処理の要素技術は、より応用的な自然言語処理タスクに使われることがしばしばあります。たとえば、文書分類には形態素解析した結果を使うことが多いです。ただし、要素技術・応用という区分は相対的なものであり、要素技術となっているものの中にも、他のさらに基礎的な要素技術を使うものもあります。たとえば構文解析や固有表現抽出には、形態素解析の解析結果を使うことが一般的です。このように、自然言語処理タスクでは、いくつかの技術をパイプライン的につなげて処理することがしばしばあります。

自然言語処理の概要については、以下のスライドが参考になるでしょう。

自然言語処理関連の書籍については、以下がよくまとまっています。ここに書かれているアドバイスを参考にしつつ、あなたのバックグラウンドや興味に応じて選ぶとよいでしょう。

BoW (Bag of Words)モデル

機械学習では、データを特徴ベクトルに変換することが必要でした。ここではテキストをベクトル表現するための一般的な方法である BoW (Bag of Words)モデル を解説します。

BoWモデルは、文書分類などのNLPタスクでよく使われる文書(テキスト)の表現です。テキストをただの単語の集まり(= Bag of Words)としてモデル化します。つまり、 単語の順番は区別しない というところに特徴があります。

  • Pros: 文書(テキスト)をコンパクトなベクトルで表現できる
  • Cons: この表現方法では「キャベツ/太郎/が/好き」と「太郎/が/キャベツ/好き」を区別できない(「キャベツ太郎」が辞書にない場合)

テキストのモデル化について、より詳しくは以下の書籍を参考にしてください。

  • 言語処理のための機械学習入門 2章 文書および単語の数学的表現

テキストの前処理

テキストを特徴ベクトルに変換するためには、以下のようなステップが必要になるでしょう。

  1. 文字単位の正規化
  2. 単語分割
  3. 単語単位の正規化
  4. ストップワード除去

ここでは、これらのステップを1つずつ解説していきます。テキストの前処理全般に関しては、以下を参考文献として挙げておきます。

文字単位の正規化

一般に、文字の意味としては同じでも、複数の内部表現(文字のマッピング)が存在することがあります。たとえば、「カ+゛(濁点)」と「ガ」は意味は同じですが、2つの内部表現を組み合わせたものと、1文字で表したもの、という違いがあります。「US(全角)」と「US(半角)」は全角文字と半角文字で、これも内部表現としては異なります。

文字の内部表現としては、Unicodeが広く使われています。文字の正規化ではUnicode正規化、とくにNFKCによる正規化がおすすめです。たとえばNFKCで正規化することで、以下のように内部表現が異なった2つの文字列を、同じものとして扱うことができます。

  • 「カ+゛(濁点)」->「ガ」
  • 「US(全角)」->「US(半角)」

ただ、文字の表記(全角・半角の違いなど)が重要になるようなタスクであれば、このような文字の正規化をすると精度が下がってしまうかもしれませんので注意してください。

Unicode正規化がどういうことをやっているか、またNFKC以外の正規化にどういうものがあるかを知りたい方は、以下の文書を参照してください。

単語分割

すでに見てきたように、テキストをBoWモデルで表現するためには、テキストの文字列を単語に分割する必要があります。この処理を 単語分割 、もしくは分かち書き、トークナイズといいます。

英語の場合は、ホワイトスペース(空白文字)で区切ればよいので、それほど難しい処理ではありません。

日本語のように、単語が明示的に分かれていない言語の単語分割には、形態素解析器を利用することが一般的です。詳しくはこのあとで説明します。有名な形態素解析器には、MeCab, Kuromoji, JUMAN, KyTea, Rosettaなどがあります。

単語単位の正規化

単語単位で正規化する方法には、大きく分けて以下の2種類があります。

  1. ステミング(語幹の取り出し)
    • 英単語のステマー Porter stemmer / Lancaster stemmer / Snowball stemmer など
    • 英単語特有のルールを使って語幹を取り出す
    • 例: runs / ran / runner -> run
    • ルールベースなので高速
    • 誤爆もある(例:international -> intern など)
  2. 見出し語化 (lemmatization)
    • 複数形を単数形になど、単語を基本形に戻す
    • 文脈も考慮して処理される
    • そのぶん、処理は少し重くなる

ただし、繰り返しになりますが、実際にこれらのどの正規化を採用するか(もしくは正規化しないか)はタスク依存です。タスクの意味や、データをよく観察して決定するようにしましょう。

日本語の単語分割/形態素解析

ここでは日本語テキストに限定した前処理について説明します。形態素解析 (morphological analysis) は、以下の複数のタスクを同時に解きます(英語では別々に行っていることが多い)。

  • 単語分割 (tokenize)
  • 品詞タグ付け (POS tagging)
  • 見出し語化 (lemmatization)

日本語テキストの前処理において、単語分割だけを行うのがいいのか、品詞の情報まで使ったり原形復元まで行うのがいいかは、タスク依存です。 形態素解析で得られる情報にはたとえば以下のものがあります(ただし形態素解析器が使っている辞書によって、得られる情報は異なります)。

  • 見出し語(表層形) (surface form)
  • 品詞 (POS; Part of Speech)
  • 原形 (basic form)
  • 読み (reading)

kuromoji.js のデモで形態素解析がブラウザで試せます。形態素解析の雰囲気を知るにはちょうどよいでしょう。なお、辞書にはIPADicが使用されています。

形態素解析(単語分割)のアルゴリズムについて詳しく知りたい方は、以下のスライドを読んでみてください。

ストップワードの除去

ストップワード (stop words) の除去とは、a, the, isなどの頻出語を除去する処理のことです。

明示的にストップワードを列挙した辞書を用意する方法と、品詞情報を使って、たとえば助詞(日本語の場合)や冠詞(英語の場合)の単語をすべて除去する方法があります。もちろん、これらを組み合わせることも有効です。ただし後者の方法は、前のステップで品詞タグ付けが行われていることが前提です。

ストップワードのような単語を無視することで、よりテキストの内容にフォーカスしたモデル化が実現できるため、精度の向上が狙えます。また、モデル自体がコンパクトになるため、学習や分類のパフォーマンス向上も期待できます。

またまた繰り返しになってしまいますが、ストップワード除去も、やっていいかどうか、もしくはどこまでやるかはタスク依存です。文書分類では、ストップワードを除去して内容語に注目して行うと、性能が上がることが多いです。

テキストをBoWモデルで特徴ベクトルに変換する

以上のステップで前処理を行って、テキストを単語の集合にすることができたとします。さて、これを具体的にどんな特徴ベクトルにすればよいでしょうか?

BoWモデルでは、1つの単語が1つの次元に対応します。次元の順番は適当に決めて大丈夫です。単語にIDを振り、単語IDと特徴ベクトルの次元とを対応づけるのが一般的です。ここで作った単語とIDの組のことも、 辞書 (dictionary) と呼びます(自然言語処理では辞書という単語が、文脈によっていろいろな意味で使われるので注意してください)。学習・予測それぞれにおいて、一貫して同じ辞書を使わなければならないことに注意してください。

たとえば、以下のような単語とIDの組(辞書)があったとします。

  • { 太郎: 1, 花子: 2, 三四郎: 3, キャベツ: 4, 白菜: 5, 好き: 6 }

「キャベツ/太郎/が/好き」というテキストは、たとえば以下のような特徴ベクトルにすることができます(ただし、「が」はストップワードとして除去したとします)。

  • (1, 0, 0, 1, 0, 1)

このように、単語が現れた場合に1、現れなかった場合に0、という2値でベクトル化する方法もありますが、単語の出現回数や重要度で、各次元(各単語)に重み付けをすることもできます。

  • 単語頻度 (TF; term frequency):その文書に単語が現れた回数
  • 文書頻度 (DF; document frequency):データセットのうち、いくつの文書にその単語が現れたか
  • 逆文書頻度 (IDF; inverse document frequency):DF の逆数。単語の重要度を表すと考えられる

重み付けの方法にはいろいろありますが、一般的には以下のようなものがあります。

  1. 単語が現れた次元には1、現れなかった次元には0を入れる(2値)
  2. 単語が現れた回数、つまり単語頻度 (TF) を入れる
  3. 単語頻度(TF)の log(対数)をとったものを入れる(単語頻度をtfとしたとき 1 + log(tf) など)
  4. 単語の逆文書頻度(IDF)を入れる(全文書数をN、文書頻度(DF)をnとしたとき log(N/n) など)
  5. 単語頻度(TF)と逆文書頻度(IDF)をかけたもの = TF-IDF

逆文書頻度(IDF)を考えることで、単語の重要度を考えることができます。よく出てくる単語(= IDFが低い)は特徴量としてあまりふさわしくないので重みを下げるという効果があります。一方で、逆文書頻度(IDF)は、ある程度の規模のテキストがないと信頼性が低いので注意しましょう。また、ベクトル化する前に単語をカウントして文書頻度n(データセットのうち、いくつの文書にその単語が現れたか)を求めておかなければなりません。場合によっては、Wikipediaなどの文書集合を使って、文書頻度を事前に計算しておくといったことも考えられます(対象テキストの性質が似たものであれば)。

単語頻度(TF)や逆文書頻度(IDF)を考慮すべきかどうかは、タスクによります。いくつかの重み付けの方法で実際に評価してみるとよいかもしれません。ともあれ、まずは最もシンプルな2値で試してみるとよいでしょう。

高次元でスパースなデータの取り扱い

自然言語処理では、しばしば特徴空間が高次元になります(特徴ベクトルの次元が高くなる)。これは過学習(Overfitting)が起きやすい条件です。ここでは、BoWでモデル化した文書(テキスト)の次元を減らす工夫について紹介します。また、次元が減ることで、高速に学習・予測できるようになるといった副次的な効果もあります。

  • 特徴選択
    • 単語を基本形に戻す、同義語・類義語辞書を使うなどの正規化を行う
    • 1回しか出てこない単語(=DFが1の単語)は無視する(ヒューリスティクス)
    • なんらかの手法で単語の重要度(重み)を計算し、上位 k 個の単語を貪欲法的に選ぶ
      • 単語の重要度の計算方法:TF-IDF、カイ二乗値、情報利得 (Gain)、自己相互情報量 (PMI) など
  • 次元削減
    • 教師なし学習による次元削減手法
    • Feature Hashing という手法もあり、少し乱暴に見えるが、文書分類タスクではそれなりにうまくいくことがわかっている

以下のスライドでは、高次元データに対して、学習アルゴリズムやデータ構造で対処する方法について詳しく説明されています。

さらに詳しく学ぶには、少し難しいかもしれませんが以下の書籍が参考になります。

  • 統計的学習の基礎 18 章 高次元の問題: p >> N

その他のトピック

機械学習に関連する計算機科学分野

  • データ構造とアルゴリズム
    • 高速な機械学習アルゴリズムを実装するためには必要不可欠
    • メモリ消費を小さくする(同じデータに対してより少ないメモリで動作すれば、それだけ大きいデータを扱える)
  • 数値計算
    • 機械学習は浮動小数点の世界。計算誤差についても知っておく
    • 機械学習アルゴリズムを自分で実装したときに、計算誤差のために壊滅することも
  • データベース、ミドルウェア
    • データは RDB に格納されていることが多い
    • SQL を知らないとデータが取り出せない!
    • 最近では大規模データを分散データベース(Mahout, Spark など)に格納し、その上で機械学習を走らせることもある
    • 環境に合わせて個別に勉強する
  • 情報理論
    • 特徴選択手法と関連
    • 機械学習の理論を勉強していると登場することがある

さらに学ぶために

わからないことがでてきたら

  • 知らない用語が出てきたら、とりあえず朱鷺の杜Wikiで検索してみましょう
    • 概要だけでなく、関連文献へのリンクも豊富

オンラインコース (MOOC)

  • CourseraのMachine Learningコース
    • オンラインかつ無料で、機械学習分野の第一人者の講義が受講できます
    • この教科書で詳しく説明できなかった線形回帰やロジスティック回帰、ニューラルネットワーク、SVMなどについて学べます
    • 絶対おすすめです
    • 参考までにこちらもどうぞ: Coursera を利用した機械学習勉強会

書籍

機械学習(入門)

機械学習(発展)

自然言語処理

確率・統計

最適化数学

線形代数

数値計算

Web

  • 機械学習の分類の話 - Jupyter Notebook Viewer
    • chezou さんのエントリ
    • 分類問題についての解説と、それを解くための機械学習アルゴリズムについて詳しく書かれています
    • パーセプトロン、ロジスティック回帰、SVM、ニューラルネットワーク、k近傍法、決定木、ランダムフォレスト、GBDTなど
  • クラスタリング(PDF)
    • 神嶌先生によるスライド
    • 教師なし学習の一種であるクラスタリングについて、よくまとめられています
    • クラスタリングのさまざまな手法が紹介されています
  • Graham Neubig - チュートリアル資料
    • NLPプログラミングチュートリアル には、今回扱わなかったNLPのトピックについてのスライド(PDF)が公開されています
    • 言語モデルやトピックモデル、係り受け解析など
  • 自然言語処理を独習したい人のために - 首都大学東京 自然言語処理研究室(小町研)
    • 小町さんによるおすすめ独学情報です
    • 自然言語処理を志す学部生を対象に書かれていますが、実務でNLPを使うITエンジニアにとっても参考になるでしょう

Web開発におけるコンピュータサイエンス - 機械学習編1

はてな教科書

この教科書は、はてなサマーインターンの講義資料として作成されたものです: https://github.com/hatena/Hatena-Textbook


この章では機械学習について、Webサービスの開発で必要とされる知識を中心に、とくに自然言語処理にフォーカスしながら解説します。

\def\bm#1{\boldsymbol{#1}} \def\set#1{\{#1\}}

Webサービス開発と機械学習

Webサービスで提供したい機能の中には、必ずしも100%の精度でなくてもいい代わりに、闇雲に実装したのでは実現が困難なものがあります。たとえば以下のようなものです。

  • カテゴリ自動判別
    • ユーザのポストした内容を自動的に分類するような機能
  • スパムフィルター
    • ポストされた内容がスパムかどうか判別する機能
  • トピック自動抽出
    • ポストされた複数の内容のうち似通った話題のクラスタを見つける機能
  • 異常値検出
    • 不正アクセスを自動検出する機能
    • サーバの異常動作を検出する機能

ここで挙げた機能はどれも、過去のデータの蓄積を使って将来やってくるデータに対して判断を下すものです。このような機能を実装する場合には機械学習を用いるとうまくいく場合があります。

実現困難な機能の例

まずは機械学習なしで闇雲に実装していったとき、どんな困難があるか見てみましょう。例として、ブログのようにユーザが自由に文章をポストできるサービスで、ポストされた内容がどの地域と関連が深いか自動的に判別する機能を実装する場合を考えてみます。

闇雲な実装

まずは安易に、ポスト内容のテキストに特定の語を含んでいるか、if文で確かめていくことにしましょう。判別対象の地域はさまざま考えられますが、まずは京都に関するものかどうか確かめる処理を書いていくことにしましょう。

sub is_about_kyoto {
  my ($text) = @_;

  return 1 if $text =~ /京都/;
  return 1 if $text =~ /烏丸御池/;
  ...

  return;
}

sub detect_location {
  my ($text) = @_;
  return 'kyoto' if is_about_kyoto($text);
  return 'unknown';
}

これでうまくいくでしょうか? 実は全くダメで、$text東京都という文字列を含んでいて東京のことについて書かれていた場合にも京都の内容だと誤判定されてしまいます。以下のように書き換えたらもう少しうまくいきそうです。

return 1 if $text =~ /烏丸御池/;
return 0 if $text =~ /東京都/;
return 1 if $text =~ /京都/;

京都という文字列の出現を確かめる前に東京都という文字列が出現しないことを確かめています。ここで注意が必要なのは烏丸御池東京都より先に持ってきている点です。いくら東京都と書いてあったとしても、烏丸御池と書いてあればそれはほぼ間違いなく京都の烏丸御池についても言及しているはずだと思えるので、優先順位を上げています。

この調子で、いまあるデータに対してはだいたい間違いなく判別できそうなアルゴリズムを実装できたとします。さて、この機能をプロダクトに投入してしばらくすると、巷では福岡県の京都(みやこ)郡について少し話題になりました。残念ながらこれまでのロジックでは京都郡の話題は福岡ではなく京都の話題と判定されてしまいました。このままではまずいので、機能の改修に着手しましょう。

東京都の場合と同じく以下のようにルールを追加すればよいでしょうか?

return 0 if $text =~ /京都郡/;
return 1 if $text =~ /京都/;

これでサンプルデータを検証してみたところ、実際には

京都(みやこ)郡○×町にある ... 打ち合わせがおわって博多駅で ...

というように、京都が隣接していない場合も多いことがわかりました。そもそもが出てきたら京都の話題ではないということにしたらよいでしょうか?

return 0 if $text =~ //;
return 1 if $text =~ /京都/;

検証の結果、今度は京都府相楽(そうらく)郡の話題が京都の話題として検出できなくなってしまったことがわかりました。直接的に京都郡を除くのではなく、先に福岡に関する話題かどうかを判別したらどうでしょうか?

sub is_about_fukuoka {
  my ($text) = @_;

  return 1 if $text =~ /博多/;
  ...

  return;
}

sub is_about_kyoto {
  my ($text) = @_;

  return 1 if $text =~ /烏丸御池/;
  return 0 if $text =~ /東京都/;
  return 1 if $text =~ /京都/;
  ...

  return;
}

sub detect_location {
  my ($text) = @_;
  return 'fukuoka' if is_about_fukuoka($text);
  return 'kyoto' if is_about_kyoto($text);
  return 'unknown';
}

検証の結果、これでは「博多長浜ラーメンみよし」(京都市中京区)の話題が福岡の話題と判定されてしまうことがわかりました。闇雲な実装のままでは、新しい種類の誤判定に出会う度に、こちらを立てればあちらが立たずで、微妙な調整を繰り返すことになります。

もう少しましな実装

京都の話題かどうかは、入力テキストに含まれる複数の語を総合的に考慮する必要がありそうです。そこで、含まれている語がどれくらいその地域らしさを表しているかの重要度(重み)を計算して、重要度の合計をスコアとして地域性を判別することにしましょう。京都らしい語ではスコアを加算して、京都らしくない語ではスコアを減算していき、最終的なスコアの正負で京都の話題かどうかを判別します。

sub kyoto_score {
  my ($text) = @_;

  my $score = 0;
  $score += 50 if $text =~ /烏丸御池/;
  $score += 30 if $text =~ /京都/;
  $score -= 30 if $text =~ /東京都/;
  $score -= 50 if $text =~ /京都郡/;
  $score -= 30 if $text =~ /博多/;
  $score += 10 if $text =~ /ラーメン/;
  ...

  return $score;
}

sub detect_location {
  my ($text) = @_;
  return 'kyoto' if kyoto_score($text) > 0;
  return 'unknown';
}

あとは検証用のサンプルデータが正しく判別されるように各語の重要度の値を調整していけばよさそうです。とはいえ、手で調整する内容が、複雑な条件から重要度の値に変わっただけで、いずれにせよ最適な条件を見つけるのは難しいでしょう。とくに判断基準となる語の種類が何万にもなってしまったとき、いったいどうやって重要度を調整していけばよいでしょうか?

機械学習によるパラメータ決定

前節の例でやろうとしていたことは、i番目の語が文書中に出現するかどうかをx_i \in \set{1, 0} (x_iは1または0)で表したときに、加算すべき重要度w_iを使って

\displaystyle \sum_i w_ix_i

の正負によって、文書が特定地域の話題についてのものであるか判別するということでした。あとはサンプルデータをできるだけ正しく判別できるようなパラメータw_iの値を見つけるという一種の最適化問題を解くことができれば、地域判別のアルゴリズムが完成します。

機械学習を用いると、まさにこのような、過去のデータから最適なパラメータを推定するという問題を解くことができます。以下のように、サンプルデータが単語ごとに区切ってあり、それぞれ京都の話題かどうかをis_kyotoの値で示してあるとすると、機械学習エンジン$kyoto_enginetrainメソッドによって各語に付与すべき重要度が求まるイメージです。

my $kyto_engine = ...
my $weights = {};

sub update_weights {
  # サンプルデータ
  # { words => [
  #     '烏丸御池' => 1,
  #     'の' => 1,
  #     '交差点' => 1,
  #     'を' => 1,
  #     '長刀鉾' => 1,
  #     'が" => 1,
  #     '通る' => 1,
  #     'と' => 1,
  #     ...
  #   ],
  #   is_kyoto => 1,
  # },
  # { words => [
  #     '京都' => 1,
  #     '(みやこ)' => 1,
  #     '郡' => 1,
  #     '○×町' => 1,
  #     'に' => 1,
  #     'ある' => 1,
  #     ...
  #     '打ち合わせ' => 1,
  #     'が' => 1,
  #     'おわっ' => 1,
  #     'て' => 1,
  #     '博多駅' => 1,
  #     'で' => 1,
  #     ...
  #   is_kyoto => 0,
  # },
  # ...
  my @data = @_

  $weights = $kyoto_engine->train(@data);
  # { 'ラーメン" => 10,
  #   '京都' => 30,
  #   '京都郡' => -50,
  #   '博多' => -30,
  #   '東京都' => -30,
  #   '烏丸御池' => 50,
  #   ...
  # }
}

sub kyoto_score {
  my ($text) = @_;
  my $score = 0;
  for my $pair (pairs %$weights) {
    my ($word, $weight) = @$pair;
    $score += $weight if $text =~ /$word/;
  }
  return $score;
}

sub detect_location {
  my ($text) = @_;
  return 'kyoto' if kyoto_score($text) > 0;
  return 'unknown';
}

このようにして得られたdetect_locationメソッドは、ユーザが新たにポストした内容を正しく判別できるでしょうか? 実は必ずしもそうとは言えません。

今回の例では、「特定地域の話題かどうかは、文書中の語の出現に付与した重要度の和(線形和)で決まる」という仮説を置きました。まず、ターゲットとしている文書集合の傾向と地域判別という問題に対してこの仮説が適しているかどうか、という懸念があります。もし適していなかった場合、trainメソッドによる学習結果のパラメータは、サンプルデータの判別に対しても十分な精度が出ないでしょう。

仮にサンプルデータの判別はうまくいっているとしても、未知の入力に対してもうまくいくという保証はありません。サンプルが少なすぎたり、サンプルの傾向に過剰に適応して過学習状態になってしまっていると、未知の入力を正しく判別できません。

この後の章では、他に取りうる仮説の選択肢や、学習結果の評価のしかたについても見ていきましょう。


分類問題のための機械学習手法

まずは前の章で挙げた具体例と同じ、(二値)分類問題のための具体的な手法を見てみましょう。機械学習の手法はさまざまありますが、一例としてパーセプトロンによる方法を見てみます。

パーセプトロン

判別アルゴリズム

パーセプトロンは、入力に対して重みづけをして和をとり、その正負で判別するというモデルです。ちょうど前の章で説明した「線形和で決まる」という仮説を使うものの一例です。

http://cdn-ak.f.st-hatena.com/images/fotolife/t/tarao/20160822/20160822155849.png

上図のように、各入力に対して重みを掛けて和をとったもの(線形和)の正負で判別するというアルゴリズムです。式で表すと以下のようになります。

f(\bm{x}) = \begin{cases}
 1 & \text{ if \(\bm{w} \cdot \bm{x} > 0 \)}  \\
-1 & \text{ otherwise } \\
\end{cases}
  • f : 判別アルゴリズム(正負で判別結果を表す)
  • \bm{x} : 入力の特徴量のベクトル
  • \bm{w} : 重みベクトル

判別対象は、たとえばi番目の語が出現しているかどうかのように、対象の特徴量x_iを集めたベクトル\bm{x}で表現します。その各x_iに、学習された重みw_iを掛けて、iについて足し合わせたもの(つまりw_iのベクトル\bm{w}と、ベクトル\bm{x}の内積)の正負で判別しようというわけです。

たとえば、(ラーメン, 京都, 京都郡, 博多, 東京都, ...)という単語の集合があったとして、学習された重みが\bm{w} = (10, 30, -50, -30, -30, \ldots)のとき、判別対象の文書「京都のラーメン」は単語の出現ベクトル\bm{x} = (1, 1, 0, 0, 0, \ldots)として表現できます。判別アルゴリズムに入力するとf(\bm{x}) = 10 \cdot 1 + 30 \cdot 1 = 40 > 0なので結果は1ということになります。

学習アルゴリズムでは、\bm{w}の値として正負の判別が正しくなるようなものを求めることになります。これはつまりy = \bm{w}\cdot\bm{x}が入力ベクトル空間を2つの領域に分割する超平面だとおもったとき、正負の判別が同じになるべき点が同じ側の領域に属するようにする、ということです。このときy = \bm{w}\cdot\bm{x}のままでは原点を通るような超平面にしかなりえないので、実際の入力とは無関係な固定値の項(ふつうx_0 = 1にする)を与えて、この項に対する重みw_0も学習することによって任意の超平面を表せるようにします(そして\bm{x}\bm{w}にはx_0w_0も含んでいるものとして扱います)。このようなw_0 x_0バイアス項と呼びます。バイアス項はy=\bm{w}\cdot\bm{x}のグラフのy切片になっています。

学習アルゴリズム

パーセプトロンの学習アルゴリズムでは、まず適当な重み\bm{w}を決めて、サンプルデータを正しく判別できるように\bm{w}の値を更新していきます。現在の重みの値でうまく判別できた場合はとくになにもせず、判別に失敗したサンプルがあった場合にはそのサンプルを使ってフィードバックをかけていくという方法です。

あるサンプル\bm{x}の正しい判別結果がy \in \set{1, -1}だとすると、y \cdot f(\bm{x})の値は、\bm{x}を正しく判別できれば1に、間違えば-1になります。y=1で間違えたときは\bm{w} \cdot \bm{x}がもっと大きい方がより正解に近づくはずです。y=-1で間違えたときは\bm{w} \cdot \bm{x}がもっと小さい方がより正解に近づくはずです。そのためには、\bm{w}のうち\bm{x}と掛けたときに寄与する要素に関してyの符号に応じて足したり引いたりすればよいでしょう。

\bm{w} \leftarrow h(\bm{x}, y, \bm{w})

h(\bm{x}, y, \bm{w}) = \begin{cases}
\bm{w}           & \text{ if \(y \cdot f(\bm{x}) > 0\)} \\
\bm{w} + y\bm{x} & \text{ otherwise } \\
\end{cases}

これを何度も繰り返すことでよい\bm{w}を得るというのがパーセプトロンの学習アルゴリズムです。

非常に簡単なしくみですが、それゆえ欠点もあります。パーセプトロンはそもそも「線形和で決まる」という仮説を置いていて、この仮説の下ではうまくいきます。より専門的な言い方をすると、パーセプトロンのアルゴリズムは線形分離可能な場合には最適値に収束していくことが知られています。しかし、線形分離不可能な場合、つまり「線形和で決まる」仮説が成り立たない場合にはうまくいきません。

仮説が成り立つ場合でも、重みを更新する際に入力\bm{x}の値をそのまま使ってしまうと更新幅が大きくなりすぎて、いい値に収束しないかもしれません。このために、実用的には\bm{w}+y\bm{x}ではなく学習率\etaを用いた\bm{w}+{\eta}y\bm{x}でフィードバックをかけることがあります。この\etaの値としては0.1や0.3といった1未満の固定値を用いるのが一般的です。

特徴量のとり方

形態素解析

ここまでの例でも、判別対象の入力として、単語集合中のi番目の語が文書中に出現しているか、という指標を用いてきました。このような指標を用いるためには、文書中から単語を切り出す操作が必要になります。とくに日本語の文章から単語を切り出すには、どこが語の切れ目なのかを判断する必要があります。また、日本語に限らず、活用語は原形にする、接尾辞は取り除くなどした方が扱いやすくなります。

このように、活用なども考慮しつつ語を切り出していく処理を形態素解析と言います。入力文書から機械学習に使える特徴量を得るには、前処理として形態素解析しておくのが一般的です。実際に形態素解析するにはそのためのツール、ライブラリを使うとよいでしょう。以下はよく使われるライブラリです。

量をともなう特徴

「単語が出現しているかどうか」というのは値が1もしくは0の特徴量でしたが、機械学習で使える特徴量は一般にはどんな値をとってもかまいません。たとえば、単語の出現回数を使った方がよい結果が得られる可能性もあります。

単語に限らず、文書の長さ、段落の数、書いた人の年齢など、わかっている情報はなんでも特徴量として用いることができます。もちろん判別基準として役に立たない特徴量もあるので、特徴量がたくさんあれば精度が上がるというものではありませんが、全く種類の異なる特徴量を使うことを躊躇する必要はありません。

ただし、異なる種類の特徴量がとりうる値の範囲が全く異なる場合は注意が必要です。たとえばいくつかの単語の出現回数とともに、文書の長さ(文字数)を特徴量として使うとしましょう。単語の出現回数は1回とか3回とか、せいぜい100回とかそのくらいでしょう。それに対して文書の長さはそれよりもずっと大きい1000とか2000とか、10000とかになるでしょう。こうなると、学習においても判別時の計算においても、文書の長さが結果に与える影響が著しく大きくなってしまい、うまく学習できないか、学習効率が悪くなります。

このような状況を避けるには、特徴量を変換して一定の範囲におさまるように調整しておきます。具体的には、ある特徴量x_iに以下のような変換を施します。

  • 正規化(normalization) : \displaystyle x_i \leftarrow \frac{x_i - m_i}{M_i - m_i}
    • m_i : 特徴x_iの最小値
    • M_i : 特徴x_iの最大値
    • 0から1の範囲におさまるように変換
  • 標準化(standardization) : \displaystyle x_i \leftarrow \frac{x_i - \mu_i}{\sigma_i}
    • \mu_i : 特徴x_iの平均値
    • \sigma_i : 特徴x_iの標準偏差
    • 標準正規分布に従うように変換

組み合わせ特徴量

ここまでは「入力の線形和で決まる」というモデルを見てきました。実は、モデルは線形なままでもちょっとした工夫で非線形な特徴も扱うことができます。

たとえば、単語a, bが出現するかどうかをそれぞれx_a, x_b \in \set{1, 0}で表現しているときに、ab両方とも出現する場合を考慮に入れた方が、精度よく判別できそうだということになったとします。線形和のモデルそのままではこれは扱えません。そこで、以下のような特徴量x_{ab}を加えてみましょう。

\displaystyle x_{ab} := x_a x_b

このx_{ab}はやはり\set{1, 0}の値をとり、abが両方出現したときだけ1になります。このようなx_{ab}を加えた特徴量の線形和は

\displaystyle w_a x_a + w_b x_b  + w_{ab} x_{ab} + \cdots

のようになるので、もし特徴量x_{ab}が精度に大きく寄与するならばw_{ab}の絶対値が大きくなるように学習される、という寸法です。この式を元々の特徴量で表すと

\displaystyle w_a x_a + w_b x_b  + w_{ab} x_a x_b + \cdots

となり、確かに元々の特徴量に対して非線形になっていますね。

このようにして、元々の特徴量から計算できる別の特徴量を加えることで、任意の多項式が扱えることがわかります。さらに、和と積で表せるものだけでなく、たとえば元々の特徴量の対数(log)をとったものなども有用な場合があります。必要に応じてうまい特徴量を見つけましょう。

モデル

いったんここまでの話を整理しておきましょう。分類問題を機械学習で扱う際にはデータとして特徴量と正解のラベルの組(\bm{x}, y)が(複数)与えられます。「なにか賢い箱」があって、そこへデータを入力します。

\displaystyle \text{(賢い箱)} \leftarrow (\bm{x}, y)

この箱は特徴量\bm{x}だけを与えるとy'を出力します。

\displaystyle \bm{x} \overset{\text{入力}}{\longrightarrow} \text{(賢い箱)} \overset{\text{出力}}{\longrightarrow} y'

このときにy'に期待することはyの値に十分近いものになっていることです。けっきょくこの「賢い箱」はあらかじめ与えられたくさんの(\bm{x}, y)の関係を学習することで、できるだけyに近い予測値y'を出力することです。機械学習は、この「賢い箱」が既知の特徴量に対して正しい予測を与えるならば、未知の特徴量に対しても正しい予測を与えるという期待に基づいているということです。

このような「賢い箱」のふるまいを数学的に表現したものをモデルと言います。パーセプトロンは分類問題のためのモデルの一つです。

モデルは内部にいくつか(ときには数千以上)のパラメータ\bm{w}を持っていて、学習のフェーズではこの\bm{w}の値を更新します。

\displaystyle \bm{w} \leftarrow \text{パラメータ更新} \; h(\bm{x}, y, \bm{w}) \leftarrow (\bm{x}, y)

そして予測フェーズでは現在得られているパラメータ\bm{w}を用いて結果を出力します。

\displaystyle \bm{x} \rightarrow \text{予測} \; f(\bm{x}, \bm{w}) \rightarrow y'

機械学習の種類

教師あり学習

  • なんらかの正解を与えて学習させるという問題設定
    1. カテゴリのような質的変数 (qualitative variable)を予測する分類 (classification)
    2. 数値のような量的変数 (quantitative variable)を予測する回帰 (regression)
  • 機械学習アルゴリズムに予測させるために与える入力のことを特徴量 (feature) という
    • 統計の世界では説明変数 (explanatory variable)とも (逆に予測する対象を 目的変数 (target variable)という)
    • 自然言語処理(NLP)の世界では素性 (feature)といわれることが多い

分類 (質的変数の予測)

  • あらかじめクラス (class)もしくはカテゴリ (category)が決められている
  • 与えられたデータが、どれに含まれるかを予測する問題設定
  • 問題例
    • メールのスパムフィルタ (NLPの文書分類タスク)
      • メールの本文やヘッダの情報からメールがスパムかハムかを判定する
      • データセットの例 (英語)
    • 手書き数字認識 (画像処理)
      • 数字が書かれた画像から、その数字(0から9までのどれか)を判定する
      • データセットの例
  • 参考: 機械学習の分類の話 - Jupyter Notebook Viewer
  • 手法の例
    • パーセプトロン
    • ロジスティック回帰
    • サポートベクターマシン (SVM)
    • k近傍法
    • 決定木

回帰 (量的変数の予測)

  • データと予測したい値を関数の入力と出力に見立てて、特定の形の関数を既知の入出力にうまく当てはめる問題
    • 関数の形をモデルという
  • 問題例
    • 住宅の値段を予測する
      • データセットの例
        • Housing Data Set (ボストン近郊での、地域ごとの平均住宅価格)
    • 株価予測 (時系列解析)
  • 手法の例
    • 線形回帰
    • ニューラルネットワーク

教師あり学習でのデータセット

  • データセット: データ(事例)の集合、くらいの意味
    • 一つ一つの処理対象のデータは事例 (instance)という
    • メールのスパム判定なら1通のメールが、手書き数字認識なら1枚の画像が事例
    • NLPでは対象のデータセットのことをコーパス (corpus)と呼ぶこともある
  • 各データにはクラスラベル (class label)(もしくは単にラベル (label))がついている
    • ニューラルネットワーク界隈では教師信号ともいう
    • データを1つ与えたときに、アルゴリズムが出力すべきもの(カテゴリ、もしくは実数値、整数値など)

教師なし学習

  • 明示的な正解(ラベル)が与えられない問題設定
    1. クラスタリング: データを(未知の)グループに分ける
    2. 次元削減(次元圧縮): データの特徴量の次元を下げる
    3. 頻出パターンマイニング: データから法則性を見つける
    4. 異常値検出: データから統計的な外れ値を見つける

クラスタリング

次元削減(次元圧縮)

  • 高次元のデータを低次元に変換する(重要な基底だけが残る)
  • 応用例

頻出パターンマイニング

  • いわゆるデータマイニングの一種
    • 相関ルール分析: よく一緒に買われる商品(バスケット分析)

異常値検出

  • データ全体に統計モデルをあてはめて、そのモデルで稀にしか起きない事象を検出する
  • 応用例
    • 不正アクセスの検出
    • サーバの異常動作の検出

アルゴリズムの評価

機械学習を用いても、その学習結果が未知のデータに対して正しい結果を出すものになっている保証はありません。むしろ、正しい結果を出せるように、データの与え方やモデルを調整していくのが肝です。この章では学習結果の評価と、どういった改善をすればいいかのヒントを見ていきましょう。

学習結果を評価する基本的な方法は、学習結果のパラメータを用いてアルゴリズムを既知のデータに対して走らせ、正しい答えを出すか、誤差がどれくらいかを測定することです。誤差は分類の場合は正しく分類できなかったデータの割合(誤り率)、回帰の場合は二乗誤差などを用いるとよいでしょう。

訓練データとテストデータ

学習に用いたデータ(訓練データ)の誤差を測っても、未知のデータに対してどれくらいの性能が発揮されるのかわかりません。そこで、既知のデータを訓練データとテストデータに分けて、学習に用いるのは訓練データの方だけにするのが一般的です。

訓練データ、テストデータともに誤差が小さい方が学習がうまくいったことになりますが、ふつうは訓練データの誤差の方がテストデータの誤差より小さくなります。それぞれの誤差が大きく出たときに考えられる原因は以下の通りです。

  • 訓練データの誤差が大きいとき
    • データの数が不足している
    • 特徴量の数が不足している
    • モデルが適切でない
  • テストデータの誤差が大きいとき
    • データの数が不足している
    • 特徴量の数が不足している
    • 過学習してしまった

たとえば、訓練データとして与えられたものは正確に記憶して与えられた通りの結果を返し、それ以外のデータに対しては常に1を返す、というアルゴリズムを実装することは可能です。この場合、訓練データの誤差は0になりますが、テストデータに対してはあまりいい結果が得られないでしょう。テストデータ(未知のデータ)に対しての誤差(汎化誤差という)が少ないモデルは「汎化性能が高い」と言い、機械学習アルゴリズムの目的は汎化性能の高いモデルとそのパラメータの獲得ということになります。

学習曲線による分析

もう少し細かく分析するには、データ数を増やしていったときの誤差をプロットしていくとよいでしょう。データ数を横軸に、訓練データとテストデータの誤差を縦軸にプロットしたグラフ(学習曲線)の形によって、どういう原因で精度が出ていないかがわかります。(あるいは、誤り率の代わりに精度をプロットしても、グラフが上下逆さになるだけで同じ分析が可能です。)

学習曲線は典型的には右のような形になります。データが増えると訓練データに完全にフィットする学習が難しくなるため訓練データの誤差は上昇しますが、さらに増えると訓練データのバリエーションも減ってくるため上げ止まります。テストデータの誤差は訓練すればするほど下がっていきますが、ある程度のところで下げ止まります。

理想的な形は、訓練データの誤差が低いところで一定の値に収束し、テストデータの誤差がそれよりもほんの少しだけ高い値に収束している状態です。この場合、学習はうまくいっていて、未知のデータに対しても高精度な結果を出せることが期待できます。

学習が不十分な場合、学習曲線のグラフの形が以下の3つのどれに分類されるかで、改善すべきポイントがわかります。

一番大きなデータ数でもまだグラフの傾きが大きいとき

  • 原因
    • データの数が不足
  • 改善策
    • データの数を増やす
    • 訓練データの誤差が大きい場合は高バイアスの場合も参照

十分にデータの数が揃わないうちは誤差が収束していきません。

高バイアス : 訓練データの誤差が大きいとき

  • 原因
    • 学習結果が訓練データに対してアンダーフィット
      • 特徴量が不足している
      • そもそもモデルが適切でない
  • 改善策
    • 特徴量を増やす
    • モデルの見直し

高バイアスなグラフになっている場合、学習が不十分なのでより効果的な特徴量を見つけるか、モデルを見直す必要があります。

高バリアンス : テストデータと訓練データの誤差のひらきが大きいとき

  • 原因
    • 学習結果が訓練データに対してオーバーフィット (過学習状態)
  • 改善策
    • 訓練データを増やす
    • 正則化する

一般に、高バイアスが是正されると相対的に高バリアンスな状態となり、高バリアンスが是正されると相対的に高バイアスな状態になります。

適合率と再現率

二値分類問題においてデータ中の正例(ラベルが+1のもの)と負例(ラベルが-1のもの)の数に偏りがある場合、単純な誤り率を用いた学習曲線の分析はうまくいかなくなる場合があります。たとえば、1000件のデータの中に正例が3件しかない場合、常にNoと答えるアルゴリズムの誤り率は0.3%になり、一見精度がよさそうということになってしまいます。

このような場合に備えて、単純な誤り率ではなく適合率と再現率を考えましょう。

適合率

アルゴリズムが正と判定したもののうち、実際に正例だったものの割合を適合率と言います。偽陽性(false positive)の少なさを意味します。

\displaystyle \text{Precision} = \frac{tp}{tp + fp}
  • tp : 判定結果も実際のデータも正だった数 (true positive)
  • fp : 判定結果は正だが実際のデータは負だった数 (false positive)
  • tp + fp : アルゴリズムが正と判定した数

再現率

データ中の正例のうちで、アルゴリズムが正と判定したものの割合を再現率と言います。偽陰性(false negative)の少なさを意味します。

\displaystyle \text{Recall} = \frac{tp}{tp + fn}
  • tp : 判定結果も実際のデータも正だった数 (true positive)
  • fn : 判定結果は負だが実際のデータは正だった数 (false negative)
  • tp + fn : 実際のデータの正例の数

F値

適合率と再現率はふつうトレードオフの関係にあり、どちらかを上げるようにチューニングするともう片方は下がる傾向にあります。このため、学習曲線による分析の際に誤り率の代わりに適合率または再現率(の逆数)を使ったとしてもうまくいかないでしょう。

そこで、適合率と再現率どちらもよければ高くなり、どちらかが低ければ低くなるような指標を用いればよいでしょう。このような性質のものとしてよく知られているものにF値があります。

\displaystyle F_1 = 2\cdot\frac{\text{precision}\cdot\text{recall}}{\text{precision}+\text{recall}}

この値の逆数を誤り率の代わりに用いれば、偏りのあるデータに対しても適切な分析・チューニングが可能となります。

場合によっては再現率は多少犠牲にしてでも適合率を優先したいこともあるでしょう。そういう場合には適合率が優先されるような指標をうまく定義してチューニングしましょう。


課題

0. リポジトリとデータの準備 (必須)

  1. 課題用の雛形perl-Intern-Machine-Learning (Perl版)もしくはscala-Intern-Machine-Learning (Scala版)をforkして手元にgit cloneしてください
  2. UCI Machine Learning RepositoryのIris Data Setからアヤメ種類とその種類の個体がもつ特徴のデータをダウンロードして、iris.dataとして保存してください
    • データはCSV形式で1行が1個体に対応します
    • 各行のフィールドは左から順に萼片(がくへん)の長さ、幅、花弁の長さ、幅、アヤメの種類となっています (幅や長さの単位はcmです)
    • アヤメの種類はIris-setosa, Iris-versicolor, Iris-virginicaのいずれかです
    • データはぜんぶで150行(150個体分)あり、各種類ごとに50件ずつです
  3. iris.dataの行をランダムな順序に入れ替え、100件を訓練データiris.training.data、50件をテストデータiris.test.dataとして保存してください

    • shufコマンド(Mac OS Xの場合はbrew install coreutilsした上でgshuf)を使うとランダムな順序に並べ替えることができます

    $ shuf iris.data > iris.shuf.data
    $ head -n 100 iris.shuf.data > iris.training.data
    $ tail -n 50 iris.shuf.data > iris.test.data
     

1. F値の計算 (必須)

  • Intern::ML::Evaluation::F1(Perl版)もしくはintern.ml.evaluation.F1(Scala版)のaddメソッドおよびaccuracyメソッドを実装してください
    • addには、ある判別対象に対する判別アルゴリズムによる結果のラベルと、その対象の正解ラベルが渡されます
    • ラベルはIntern::ML::Model::Label(Perl版)もしくはintern.ml.types.Label(Scala版)のインスタンスです
    • accuracyは、これまでにaddされた判別結果のF値(F_1)を計算して返すようにしてください
  • (ヒント) 正しく実装できていればscript/run f1コマンドがおよそ0.545455の値を出力するはずです

2. パーセプトロンの実装 (必須)

  • Intern::ML::Classification::Perceptron(Perl版)もしくはintern.ml.classification.Perceptron(Scala版)のtrainメソッド(学習アルゴリズム)およびpredictメソッド(判別アルゴリズム)を実装してください
    • trainメソッドには学習すべきデータがリストで渡されます
      • (Perl版) リストの各要素はfeaturesに特徴量の配列リファレンスを、labelに教師ラベル(Intern::ML::Model::Labelのインスタンス)をもつハッシュリファレンスです
      • (Scala版) リストの各要素は特徴量のリストと教師ラベルの組です
    • predictメソッドには1件の判別対象が、特徴量のリストとして渡されます
  • (ヒント) うまく実装できていればscript/run iris iris.training.data iris.test.dataを実行することで1.0に近い値が2つ出力されます
    • 値は訓練データのF値とテストデータのF値です
    • これは、Iris-setosaPositive、それ以外をNegativeとして二値分類した結果です

3. 学習曲線の表示 (必須)

  • script/iterate 100 iris iris.training.data iris.test.data > iris.learning-curve.pltを実行して学習曲線のデータを出力してください (iris.learning-curve.pltはリポジトリに含めてください)
  • (ヒント) script/iterate 100 iris iris.training.data iris.test.data | script/plotとすると学習曲線のグラフを表示できます
    • ただし表示にはgnuplotが必要です

4. 多値分類器の実装 (オプショナル)

  • script/run irisコマンド(Intern::ML::CLI::Iris(Perl版)もしくはintern.ml.cli.Iris(Scala版))を参考に、アヤメを3つの種類のいずれかに分類するコマンドscript/run irismultiを実装してください
  • コマンドの入出力形式は任意です
  • (ヒント) 二値分類器の実装方法しか解説していませんが、Iris-setosaとそれ以外を区別する分類器(2.で実装したもの)、Iris-versicolorとそれ以外を区別する分類器、Iris-virginicaとそれ以外を区別する分類器をべつべつに実装して、3つの結果をうまい具合に統合する、という方法で三値分類器を実装することができます(1-vs.-allなどと呼ばれる方法)
  • (ヒント) アヤメの種類の中にはそのままの特徴量では線形分離可能でないものも含まれているので、綺麗に分類するには以下のいずれかの対応が必要でしょう
    • パーセプトロン以外の方法を使う
    • データを前処理して組み合わせ特徴量を使う