機械学習 | 株式会社Altus-Five / 株式会社Altus-Five は、技術力で勝負するシステム開発会社です。 Sun, 01 Jun 2025 17:28:31 +0000 ja hourly 1 https://wordpress.org/?v=6.8.2 /wp-content/uploads/2025/01/cropped-favicon-32x32.png 機械学習 | 株式会社Altus-Five / 32 32 機械学習・統計学・最適化の違いについてまとめてみた /blog/2017/12/18/difference-of-3-methods/ /blog/2017/12/18/difference-of-3-methods/#respond Sun, 17 Dec 2017 15:33:00 +0000 http://43.207.2.176/?p=331 システム開発と機械学習、およびその周辺領域のこと 長く忘れられていた「機械学習」という分野が再び脚光を浴び、システム開発の現場でもソリューションとして用いられるようになって暫く経ちます。CPUの性能向上からニューラルネッ […]

The post 機械学習・統計学・最適化の違いについてまとめてみた first appeared on 株式会社Altus-Five.

]]>
システム開発と機械学習、およびその周辺領域のこと

長く忘れられていた「機械学習」という分野が再び脚光を浴び、システム開発の現場でもソリューションとして用いられるようになって暫く経ちます。CPUの性能向上からニューラルネットワークのモデルを複雑化しても訓練が出来るようになり、性能の良いモデルの開発が出来るようになったのがその端緒だと言われています。

機械学習が援用されている分野の一例として、マーケティングの分野では「データドリブン・マーケティング」という言葉が数年前からHOTになっています。マンパワーではなく、データを駆動力として(data driven)、マーケティングを遂行することを指します。 弊社でも同分野には実績がありますので、本記事の最後で簡単に紹介させていただきます。

データドリブン・マーケティングを実践するには

データが自ら、人間に何かを教えてくれるということはありません。そこから何らかの「含意」を抽出するための仕組み(プラットフォーム)を構築することが不可欠です。 機械学習などの問題解決手法よりポピュラーな手法になった時、その構築にあたって実際に手を動かすのは、システム開発を担うエンジニアです。機械学習に実績のあるエンジニアは、様々な手法や周辺分野について理解を深めることを怠らず、来る時代に活かせる知識を日々拡充しています。

ところで、機械学習と近しい分野として「統計学」というものもあります。いずれもデータを扱う分野だということもあり、両者の違いがよく分からない、という声も業界の内外から上がっており、様々なところで「どう説明すれば初学者にわかりやすいか」議論されているようです。 少し勇気の要る試みですが、「機械学習と統計学の違い」について、本記事で整理してみたいと思います。

機械学習と統計学の違い

「統計学は既存のデータを説明し、機械学習は未知のデータを説明する」といった説明の仕方もあるようですが、本記事ではユースケースによる切り分けではない、別の方法で説明を試みます(反例も多いように思うため…)。 実のところ「両者は根本的に違う」というのが正解であるようにも思えますが、「データを説明する」という点では似ている部分もありますので、対比してコンパクトに理解したいというニーズはもっともでしょう。 本記事では機械学習と統計学をうまく対比させるため、「データの取り扱い」という観点から両者の「違い」を解説してみようと思います。

機械学習におけるデータの取り扱い(データによる訓練)

機械学習では、「データを用いてモデルを訓練する」という発想でデータを用います。 例えば、自動運転車を制御する機構の中で機械学習手法が使われているとしましょう。初めは、実用上想定される様々な運転上の課題に遭遇出来る「教習所」のような環境で運転させて、データを取ります(その『データ』がどんなものかは、あまり想像がつきませんが…)。 訓練環境下で十分に運転制御の品質を高めたら、路上に出て実際の運転を行い、やはり運転について十分な品質があると認められれば実用レベル、ということになります。

ここで重要なことは、「データに基づいてモデルを構築する」という発想が念頭にあることです。機械学習の分野では、モデルを改良し、意思決定(ここでは運転制御)を改善するために用いるデータを「訓練データ」と呼びます。 機械学習モデルは、最初から十分な性能を持つということはほとんどありません。実装者が試行錯誤しながらモデルを少しずつ改変し、訓練し、その性能を確かめる、ということの繰り返しでやっと十分な性能が得られます。 ここで機械学習モデルの性能とは、訓練データに対してではなく、未知の入力(テストデータ)に対して望ましい結果を返すことです。このような性能を、機械学習の分野では 汎化性能と呼びます。 汎化性能については下記の記事でも解説していますので、ご参考までにご覧ください。

参考記事

まとめると、機械学習では一般的なデータに対する汎化性能を高めるため、訓練データを用いてモデルを改善していきます。どのように改善していくかはモデルの設計(及び、用いる訓練データ)によるので、この点が実装者の腕の見せ所となります。 また、「データの扱い」という観点からは、「データがモデルを磨き上げるための訓練の素材になる」という点が機械学習の特徴です。

統計学におけるデータの取り扱い

一方で統計学では、「データが生まれる背景」に真っ先に着目します。例えば、ある工場で製造されている製品の重量が、平均で100gになるよう製造されているとしましょう。この時、ちょうど100gになるよう製品を作ろうと思っても、必ずミリグラム単位の誤差は出てしまうことに注意が必要です。 工場内の品質管理のため、重量に基づいて異常値検出をしたいというニーズがあるとしましょう。 この場合、「重量がきっかり100gでない製品は全て異常である」という設定にしたとすると、「ミリグラム単位で製品重量を計測出来る重量計を導入したら、頻繁に異常検出されてしまって困る」ということになりかねませんので、別の基準を設定する方法があります。機械学習の分野で言えば 二値分類を行うということになりますが、ここでは統計学の手法でこの問題を解決することにしてみます。 ここでは取り扱うデータを、製造された製品の「重量」のデータとします。たとえば、下記のようなものを想定してみましょう。

出荷No.製品重量
615100.013g
61699.998g
617100.021
61899.985g

(先ほどの自動運転の例と比べると、一次元のあまりに単純なデータになってしまいましたが、ご容赦ください。)

さて、工場から1万個の製品(データ)を取り、そのヒストグラムを描くと下記のようになったとします。

(横軸:製品重量(g)、縦軸:その重量をもつ製品数)

一般に、「ある基準となる重量があり、その重量をめざして製品を作る」際には、多くの場合、重量の分布は正規分布と呼ばれる分布に従うことが知られています。正規分布に従う事物について十分数のサンプルを取り、ヒストグラムを描くと、上記のようなベルのような形(bell-shape)が描かれることがわかっています。このような、「十分数のデータが従う傾向」のことを「母集団分布」と呼びます。統計学手法の特徴は、データについて何かを説明する前に、母集団分布に関する仮定をまず置く、という点にあります。 (もちろん、仮定を置く際にはデータを図示して目視したり、何らかの事前分析にかけることになりますので、データは手元に必要です。)

正規分布については、その性質が非常によく知られており、ほとんどの統計学の本で解説されていますので、ここでは詳細の説明は省くことにします。重要なことは、「データが生じる背景(i.e. 母集団分布)」を特定化してから分析を進めているということです。 今回の場合、統計学に基づく異常値検出のシステムは、下記のような流れで設計・実装が出来るでしょう。

  1. 製品重量の分布は正規分布に従うと仮定する
  2. データからその平均値と、データのばらつきを表す値(分散)を推定
  3. 推定された平均値と分散を基に、重量に基づく異常値検出のシステムを実装

2.で推定される「平均」と「分散」という2つの数値は「パラメータ」と呼ばれます。機械学習においてモデルのパラメータは数多あり、それらの含意は読み取ることが難しい場合が多いのですが、統計学のモデルにおいてはパラメータが比較的少数であり、含意も明確であることが多いと言えます。

3.について具体的に述べると、「推定されたパラメータの値において、ごく稀にしか生じないであろう重量」が検出された場合に異常値とみなしアラートを出す、という実装が可能となります。

統計学に基づいた実装の良いところは、「判断ミスの確率」を一定以下に抑えることが出来ることです。 ここで判断ミスとは、「通常の『バラつき』の範疇なのに、異常値と判定してしまう」という間違い(統計学の用語で『第一種の過誤』と呼ばれるもの)を指します。

上記のように、統計学を援用して設計・実装されたシステムならば、「通常の『バラつき』の範疇なのに異常値判定されてラインが止まってしまう」望ましくない挙動の頻度を、確率 という観点から抑えることができます。 例えば工場で製造される製品数が、1日あたり1,000個であるとしましょう。この時、「1日の稼働あたり異常値判定の誤作動が起きる確率を1%以下に抑えるような異常値検出の閾値」を、統計学における「信頼区間」という概念から求められることが知られています。 そのような「保証」の下で実装の信頼性を示せることも、統計学を援用したシステムの魅力といえるでしょう。

しかし、データがどのような母集団分布から生じるか仮定を置くことは、言うまでもなく「確率と統計」の知識を必要としますし、うまく仮定を満たすよう、データに対する加工が必要であることも多いことに注意が必要です。

機械学習と最適化の関わり合い

ここまで、機械学習と統計学の関係性について整理してきました。もう一つ、整理しておくと何かと便利な二つの言葉があります。 機械学習について書かれた記事や文献を読んでいると、「最適化」や「最適化アルゴリズム」といった単語が出てくることがあり、しばしば混乱を招きます。 「機械学習」と「最適化」、この二つの用語も混同されることが多いようですので、本記事で整理を試みてみます。

まず「最適化」とは、制約を満たす中で「目的」をもっともよく達成するような解を見つけることであり、下記のような具体例があります。

用途目的目的関数制約
乗換検索出発駅と到着駅が与えられたとき、もっとも所要時間の短い経路を見つける経路に応じて求まる所要移動時間を最小化経路が途中で途切れている、枝分かれしているような経路は認めない
スケジューリング従業員の負担が最も均等化するスケジュールを生成する「もっとも残業時間が長いスタッフ」の残業時間を最小化タスクが全て完了しないようなスケジュールは認めない
生産管理売上が最大化するような生産計画を立てる生産計画のアウトプットについて求まる売上の総和を最大化物的・人的な生産資源が不足してしまう生産計画は認めない

機械学習をはじめとする、数理的に問題解決する分野では、最適化といえば「目的関数」と「制約」が揃っており、この2つだけで説明がつくものを指します。

言い換えれば、「制約はこのようなもの、目的関数はこのようなもの」と説明できない場合は、それは「最適化ではない」か、「最適化を部品として用いる別の概念(ex. 機械学習)」である可能性が高いです。

機械学習アルゴリズムの中で、最適化は学習の手段として用いられれます。 その目的関数としては「誤差関数」「尤度関数」といったものが挙げられ、最適化を実際に行うアルゴリズムには問題に応じて多数挙げられますが、誤差関数の最小化アルゴリズムとしては「勾配法」(ex. 確率的勾配降下法)などはよく聞く手法でしょう。

まとめると、機械学習アルゴリズムは、その内部で、何らかの「最適化」のアルゴリズムを部品として用いていることが多い です。 そして、 最適化アルゴリズムとは、「目的関数」と「制約」によって定義された問題について、効率的に問題を解くためのアルゴリズム のことです。

「機械学習ではない最適化アプリケーション」について

単一の最適化アルゴリズムそのものにインタフェースを付加するだけで、立派なアプリケーションになることもあります。 その代表例として挙げられるのは、乗換検索のアプリでしょう。乗換検索は所要時間が最小になる経路や、所用運賃が最小になる経路を求めることがコアな機能のすべてであり、多くのアプリにおいてその機能は「最適化」によって実現されています。

  1. 出発地、目的地、出発時刻(etc.)を入力
  2. 所要時間、所用運賃などの目的関数をもとに、最適経路を求める(最適化)
  3. 求まった最適経路を適切なインタフェースで出力

2.で解く問題は最短経路問題として知られており、この問題を解くシンプルなアルゴリズムとして、ダイクストラ法などが知られています。上記のアプリケーションには「データに基づいてモデルを訓練する」といった発想はないので、機械学習ではありません。

機械学習のモデルの解釈について

機械学習は「モデルの解釈が難しい」とよく言われます。機械学習のモデルは、実用に堪えるものを作ると非常に複雑になるため、モデル全体として「結局、どうやって最適化しているか」は分からないことが多いのです。

なぜ、機械学習のアルゴリズムは、解釈が難しいほど複雑化してしまうのか、ニューラルネットワークを実例として考えてみましょう。

ニューラルネットワークは、互いに結びつく人間の脳細胞(neuron)が、なんらかの意思決定をする際に送る電気信号の回路(neural network)をモデル化したものです。 ニューラルネットワークのモデルを、解きたい問題に応じて適切に構築すると、下記のような、様々な意思決定(i.e. 出力)を行うことができます。

  • 目の前の物体はリンゴとミカンのどちらであるか?(画像認識・二値分類)
  • このSNSユーザーは「嵐」の5人のメンバーのうち、どのメンバーのファンであるか?(自然言語処理・多値分類)

上述した通り、これらのアルゴリズムにおいては、何らかの「最適化アルゴリズム」が部品として用いられています。しかし、「モデル全体として、どうやって意思決定を最適化しているのか?」という問いには、モデルを作った(構築し、訓練した)本人すら、答えられないことが殆どなのです。

もちろん「損失関数値を最小化している」といった答え方は可能ですが、「なぜ訓練結果のパラメータが望ましいか?」といった質問に答えることは困難です。もしそれがシステム開発の成果物だったとすれば、「納品物が『いい感じ』に意思決定をしてくれることは確かだが、その理由は厳密にはわからない」という性質を持つのが、機械学習の納品物だということができます。

「なぜか?」と問われたならば、下記のような説明ができそうです。 ニューラルネットワークは人間の脳の仕組みを模倣したモデルですが、人間の脳も同様に「何を判断基準として、意思決定をしているか」分からないことが多いからです。

例えば、目の前にいる人が第一印象で「優しそう」か「怖そう」かを判定することは、人間には可能です。しかしその「判定基準」を言語化することは非常に難しいと言えます。このような、「どのように判断しているか、基準は明確化できないけれど、人間にはなぜか判断できる」といった意思決定を、機械学習は得意とする場合が多いのです。 もちろん、性能の高い機械学習モデルを構築する際には、「この問題を解く際の判断基準は何か?」という推理が構築の助けになります。「機械学習は統計学と違い、データについて仮定を置かず、訓練されたモデルの性能(汎化性能)で判断することが多い」というのは真かもしれません。しかし、モデル改善の過程ではデータの成り立ちや、問題を解く際のプロセスに関する分析力・洞察力が重要な役割を果たします。 重要なことは、システムで問題を解決する方法には複数の選択肢があり、それぞれ違った性質があることを知るということだと言えそうです。

人間のように判断し、問題を解決するシステムの開発をご所望なら

上述したように、「人間のように判断し、問題を解決する」ことの出来るシステムが、機械学習で実現されるケースが増えています。 「これは人間がやるしかない」と思われている問題について、機械学習に実績のあるシステム開発会社に相談すると、自動化の道筋が立つ可能性も十分にあります。

アルタスファイブは、機械学習を援用した開発案件を得意とするシステム開発会社です。SNSの投稿情報をクローリングして収集、投稿内容をお客様のニーズに合わせて分類する際に、機械学習手法を用いています。 お問い合わせ頂けば、機械学習プロジェクトにおいて多数実績のあるマスター開発者が、課題を直接ヒアリング致します。お客様のニーズをダイレクトに、機械学習の知見ある開発者にご相談頂けますので、「できない」と思われていたことが可能になる瞬間に、立ち会っていただけるかもしれません。

まずはお気軽にご相談くださいませ。

The post 機械学習・統計学・最適化の違いについてまとめてみた first appeared on 株式会社Altus-Five.

]]>
/blog/2017/12/18/difference-of-3-methods/feed/ 0
【Pythonでテキスト処理】Double arrayでTrieを実装してみた /blog/2017/11/13/double-array/ /blog/2017/11/13/double-array/#respond Sun, 12 Nov 2017 15:50:00 +0000 http://43.207.2.176/?p=345 Trieを実装する上で必要不可欠な、データ構造の工夫 前回のブログで、辞書検索、サジェスト、形態素解析などを実装するのに使われるTrieの概略を紹介しました。 木構造の各エッジに文字のラベルを付けることで辞書を表現すると […]

The post 【Pythonでテキスト処理】Double arrayでTrieを実装してみた first appeared on 株式会社Altus-Five.

]]>
Trieを実装する上で必要不可欠な、データ構造の工夫

前回のブログで、辞書検索、サジェスト、形態素解析などを実装するのに使われるTrieの概略を紹介しました。

木構造の各エッジに文字のラベルを付けることで辞書を表現するという抽象的な説明を与え、それを実現するLOUDSという簡潔データ構造を紹介しました。 今回はLOUDSとは別の、Double Array(ダブル配列)というデータ構造で実際にTrieをPythonで構築し、共通接頭辞検索を行えるようにします。 実装方法については 『日本語入力を支える技術』(徳永, 2012)、通称「徳永本」に準拠しますので、同著を読まれた方が実装を試みる際の参考にもなると思います。 また、Double arrayのエッセンスを紹介していきますので、参考書籍をお持ちでない方もDouble arrayによるTrieの実装について理解できることを目指しています。

前提:Trieを実装する上で、データ構造の選択は重要

まず、Trieを実装する際のデータ構造として、LOUDSやDouble Arrayといった様々な選択肢が存在する背景を紹介しておきます。 検索の高速性を担保しつつTrieを実装する素朴な方法として、各ノードからの文字ごとの遷移先をテーブルに格納しておくというやり方が挙げられます。

このようなTrieを想定してみましょう(ノードの番号は幅優先で付与)。

上記のTrieに対応するテーブルが上図です。各セルを (行ラベル, 列ラベル)という組で表すことにすると、例えば (2, E)に4が格納されていることになりますね。

これは、Trie上の「2」というラベルが付いたノードから、文字「E」のラベルがついたエッジを辿ることで、「4」 というラベルが付いたノードに遷移できることを表します。 このようなテーブルを二次元配列などの形式でメモリ上に保持すれば、例えば「AGI」という入力に対してTrie上のノード5に効率よく遷移出来ます。そのことを、Pythonのコードを用いて確かめてみましょう。

【Python】テーブルによるTrieと共通接頭辞検索ソースコード

ここからは、GitHubで公開した下記のソースコードを引用しながら解説を進めます。100行に満たないソースコードですが、『日本語入力を支える技術』で解説されている「テーブルによるTrieの実装」をそのまま実現しています。

https://github.com/msato-ok/trie-master/blob/master/tabletrie.py

まず辞書の生成ですが、’MyTrie’というクラスでTrieを表すものとし、’MyTrie’のオブジェクトが一つの辞書を表します。オブジェクトを生成する際には、語彙が格納された配列を入力します。

class MyTrie():
    def __init__(self, vocabulary): #入力は辞書式順序でソート済みであることを想定
view rawtabletrie.py hosted with ❤ by GitHub

例えば下記のようにすると、辞書を生成することが出来ます。

        self.sub_dictionaries[present_node].view_terms()

vocabulary = ['age', 'agile', 'alias', 'all', 'alt', 'altus'] #語彙
view rawtabletrie.py hosted with ❤ by GitHub

テーブルによるTrieの構築について、アイデアは既にご紹介しましたので、その具体的な実装内容はソースコードをご覧ください(’MyTrie’クラスの’init’関数の中身)。

この時、共通接頭辞検索のコードは下記のようになります。

                #suffix[0] == '#'のときはそのノード自身が単語を表す。新規ノード不要

    def common_prefix_search(self, query):
        present_node = 0 #Trieの根ノードから探索開始
        for alphabet in query:
            if alphabet in self.table[present_node]:
                present_node = self.table[present_node][alphabet]
            else:
                print('The result for query \'' + query + '\' was not found.')
                return
        #for文を抜けているので、検索結果が存在
view rawtabletrie.py hosted with ❤ by GitHub

共通接頭辞検索のアルゴリズムは、入力として‘query’という文字列を取ります。例えば下記のようなものが考えられるでしょう。

vocabulary = ['age', 'agile', 'alias', 'all', 'alt', 'altus'] #語彙

dictionary = MyTrie(vocabulary)

dictionary.common_prefix_search('altus')
dictionary.common_prefix_search('alt')
view rawtabletrie.py hosted with ❤ by GitHub

出力は’query’が表すノード下にある全ての単語です。一例として、下記が正しい出力です。

上述したリンク先のリポジトリをcloneし、「tabletrie.py」を実行して頂くと、同様の結果が確認可能です。

テーブルによる実装の弱点と、Double arrayの必要性

この実装はシンプルな上、高速な検索が可能ですが、「Trieのノード数、および使われる文字数、それぞれに比例したメモリが必要」という弱点があります。

つまり、この素朴な方法では、文字数が大きくなったり、辞書の単語数が大きくなると、メモリに乗り切らないという問題が生じてしまうのです。 そこで、「よりメモリ効率の良いデータ構造を用いれば、巨大な入力(語彙)に対しても使えるようなTrieによる辞書が実装できないか」という問が生じます。

Trieを格納・運用できるデータ構造については、検索の高速性をある程度犠牲にしてメモリ効率を上げていくというのが基本方針です。そのような「メモリ効率と高速性、両者のバランスが取れたデータ構造」を「簡潔データ構造」と呼びます。今回のテーマであるDouble array(ダブル配列)は、簡潔データ構造の一種です。

補足:Trieによる形態素解析と「文字数」の上限について

※この段落は読まなくても、理解上差し支えありません。

先ほど、テーブルによる実装は「Trieのノード数、および使われる文字数、それぞれに比例したメモリが必要」と述べました。 ここで「ノード数」は単語数に従って増大するため問題だということが伝わりやすいと思いますが、「文字数が大きくなる」というイメージは持ちづらいかもしれません。 アルファベットなら26文字、ひらがななら五十音といった風に、上限があるように感じられるので、問題がないと思われるかもしれません。 しかし形態素解析のような用途にTrieを用いたい場合は、文字数は増大の可能性があります。 形態素解析においては、「わがはいはねこである」という入力文章に対し、単語の結びつきの強さによって「吾輩は猫である」という文章と「我が杯羽根子である」という文章のどちらがもっともらしいかという判断を行います。このとき、先ほどの「文字」にあたるものは「単語」(ex. 吾輩、杯、羽根、etc.)であり、その数はアプリケーションの用途によります。 よって文字数の上限を定めることはできず、テーブルによるTrieの「文字数に比例してメモリが必要」という性質は大きな障壁となるのです。

Double arrayによるTrieの実装

Double arrayによるTrieの実装をPythonで行ったコードは、下記URLからアクセスできます(GitHubリポジトリ)。

https://github.com/msato-ok/trie-master/blob/master/double_array_trie.py

まずは、テーブルによる素朴な実装とDouble arrayによる実装はどう違うのか、簡単に対比してみたいと思います。

テーブルによる実装では、Trieのノードそれぞれについて、各文字による遷移先を示すための行(すなわち配列)を用意しました。

一方、Double arrayを用いることは、語彙(入力する単語の集合)がどんなに大きくても、2本の配列だけでTrie全体を表現することを可能にしてくれます。 「ダブル配列」という言葉からは配列の本数が倍加するような印象を受ける方もおられるかもしれません。そうではなく、「どんなに巨大な辞書でも、2本の配列だけで、対応するTrieを表現できる」ことが、このデータ構造の特色なのです。

例えば、下記が上述のTrieに対応する、Double arrayの例です。

ここでDouble arrayとは、上図にあるCheck, Baseという2つの配列のことです。2本の配列に格納された値を適切に読み込んでいくことで、上記のTrieを再現したり、共通接頭辞検索を高速に行えるようになっています。 しかし、なぜこの2本の配列によってTrieを表現出来るのか、配列そのものを見ていても理解は難しいでしょう。ですので一旦 Check, Baseが何を表すのかということは忘れることにします。

代わりに、上図のもう一つの特徴的な点に着目します。それは、Trieのノードに付与された番号が先ほどとは変わっているという点です。 これはデタラメな数値ではなく、Double arrayによるTrieの構築アルゴリズムに従って論理的に付けられた番号です。その規則を知ることで、Double arrayによるTrieの構築と、検索に用いる際のアルゴリズムが直観的に理解しやすくなります。

Double arrayにおける各ノードの番号付け規則

番号付け規則を理解するための「データ構造もどき」

少し遠回りですが、Double arrayについて理解するためのステップとして、Double arrayから幾つかの要素を省いた「データ構造もどき」について考えてみると、理解しやすくなります。 すなわち、下記のシンプルな規則に基づいてTrie(?)を構築するとどうなるか、想像してみましょう。

  • 各ノードの番号として、ノードに入ってくるエッジのラベル「A, B, C, …」を、「1, 2, 3, …」に置き換えた数値を採用する

結果は下図のようになります。

ここでは便宜的にノードとノードの間にエッジを引いて、Trieの形を保たせていますが、実際に情報として存在するのは各ノードの番号だけです。 この「図」をTrieだと考えて運用しようとすると、2つの障害が発生することを、下図で示してみました。

  1. 複数のノードに同じ番号が付けられてしまい、例えば「1」のノードから「L (12)」で遷移しようとしたとき、複数ある「12」のノードのうちどれに遷移すればいいかわからない
  2. 「1」のノードから「E (5)」で遷移しようとしたとき、遷移先は存在しない(「1」の子ノードに「5」のラベルが付いたノードはない)にもかかわらず、離れた「5」のノードに遷移できるように見えてしまう

Double arrayは、この2つの症状を克服するために工夫を加えたものと解釈することが出来ます。

正しいノード遷移を可能にするための2つの工夫

まず前述した問題1.については、ノードの番号が重複しないようにすれば解決します。 ノード間の番号の重複を解決するために、ちょっとした工夫を加えたものが下図です。

こちらは、一部のノードに「+1」もしくは「+2」という数値を持たせ(もっと大きな値を持たせてもよい)、ノードの番号を付ける際の規則を、下記のようにほんの少しだけアップデートした結果の図です。

  • 各ノードの番号として、親ノードの持つ数値に対し、ノードに入ってくるエッジのラベル「A, B, C, …」を、「1, 2, 3, …」に置き換えた数値を加えたものを採用する

ひとまずこのようにすると、ノードの番号の重複については解決出来ることになります。

上図では、「子ノードの番号に加えた量」を、親ノードの傍に付記していました。実際にメモリ上でその値を記憶しておくためには配列をよく用いますので、親ノードの番号を添え字として、配列の中に値を格納してみることにしましょう。

実はこの配列が、冒頭で天下り的に示した「Double array」の「Base」という配列に一致するものとなっています。 要は、Baseというのは、「子ノードの番号を付ける際のベースとなる量」を表す数値であり、ノードの番号を重複させないために加えるものなのですね。

もう一つの工夫は、問題2.を解決するためのものです。 Base配列を導入することでノードの番号の重複は解消されましたが、「遷移出来ないノードに遷移出来るように見える」という問題は依然として残っています。例えば:

  • ノード「0」から「E (5)」という文字によって、ノード「5」に遷移出来るように見える
  • ノード「12」から「F (6)」という文字によって、ノード「7」に遷移出来るように見える(ノード『12』は+1というBaseの値を持つことに注意)

これを解決する方法は実は単純です。各ノードに対し、親ノードの番号を覚えさせておくことで、子ノードへ遷移しようとするとき「遷移先が自分の子ノードかどうか」チェック出来るようにすれば良いのです。

このようにすると、子ノードではないノードへと遷移してしまう問題を防ぐことが可能になります。 例えば「1」というノードからは「E (5)」という文字によってノード「5」に遷移出来るように一見見えますが、ノード「5」の親は「7」です。つまりノード「5」はノード「1」の子ではありませんから、遷移は出来ないということがわかるのです。

実はこの「各ノードの親を記憶させた配列」が、Double arrayのもう一つのデータである「Check」という配列にあたるのです。

上図は最初に天下り的に示した「Double arrayによるTrie」と同じ図になっています。 Double arrayに格納されているデータが何を表すかに関する、概念的な説明は以上です。

【Python】Double arrayを構築し、共通接頭辞検索を行うコード

https://github.com/msato-ok/trie-master/blob/master/double_array_trie.py

上記ソースコードでは、与えられた語彙(単語の集合)に対し、実際にDouble arrayに基づくTrieを構築し、共通接頭辞検索を行うことが出来ます。 行数は130行程度と、tableによる実装とさほど変わりません(ただし、高速化のための工夫は行っていないことにご注意ください)。

Check, Baseへの値の入力は、MyTrieクラスのコンストラクタ内で行っています。ここでは入力語彙に基づき、Check, Baseに適切な値を格納していきます。

また、double_array_trie.pyMyTrie. common_prefix_search関数は、Check, Base二つの配列を用いて下記のような処理を行うことが出来ます。

  • 入力語彙のうち、「AL」から始まるすべての単語を出力する(共通接頭辞検索)
  • 入力語彙のうち、「ALP」や「ALA」から始まる単語が存在しないことを判定する
    def common_prefix_search(self, query):
        present_node = 0 #Trieの根ノードから探索開始
        for alphabet in query:
            if not self.base[present_node] + (ord(alphabet) - self.origin) in self.check:
                #該当列のcheckの値が存在しない
                print('The result for query \'' + query + '\' was not found.')
                return
            elif self.check[self.base[present_node] + (ord(alphabet) - self.origin)] != present_node:
                #該当列のcheckの値が現在のノードと一致しない
                print('The result for query \'' + query + '\' was not found.')
                return
            else:
                present_node = self.base[present_node] + (ord(alphabet) - self.origin)
        #for文を抜けているので、検索結果が存在
        print('The result for query \'' + query + '\' is:')
        self.sub_dictionaries[present_node].view_terms()
view rawdouble_array_trie.py hosted with ❤ by GitHub

Trieをシステム開発に応用する

弊社はフルスクラッチの開発案件を得意としており、Trieやそれに類するデータ構造を用いて、共通接頭辞検索を応用した機能を柔軟に実装することが出来ます。 例えば、「求人案件情報の検索において、全文検索に基づくサジェストを出したい」と言った要求は、「Java」という入力に対して「Java」や「Java エンジニア」、「JavaScript」といった出力を返すことなので、共通接頭辞検索を行うことに他なりません。 「できない」と思えるような技術課題について、技術力を駆使してお応えする準備がございます。まずはお気軽に、開発案件のご相談を頂ければ幸いです。

また、様々なデータ構造を活用して、顧客の課題を解決する最善の方法を模索したいエンジニアからの求人応募も常にお待ちしております。ぜひ「採用情報」をご覧になってみてください。

The post 【Pythonでテキスト処理】Double arrayでTrieを実装してみた first appeared on 株式会社Altus-Five.

]]>
/blog/2017/11/13/double-array/feed/ 0
テキスト処理に使われるTrie(トライ木)とLOUDSに関する概略 /blog/2017/09/06/trie/ /blog/2017/09/06/trie/#respond Tue, 05 Sep 2017 16:11:00 +0000 http://43.207.2.176/?p=364 Trie(トライ木)を用いた検索は高速・省メモリ Trie(= Retrieval tree, 検索木)はテキスト処理において必需品と言えるデータ構造です。辞書検索、日本語入力、サジェストの実装や、形態素解析に使われる辞 […]

The post テキスト処理に使われるTrie(トライ木)とLOUDSに関する概略 first appeared on 株式会社Altus-Five.

]]>
Trie(トライ木)を用いた検索は高速・省メモリ

Trie(= Retrieval tree, 検索木)はテキスト処理において必需品と言えるデータ構造です。辞書検索、日本語入力、サジェストの実装や、形態素解析に使われる辞書(形態素辞書)が主な用途と言えるでしょうか。Pythonの自然言語処理パッケージNLTK(Natural Language Tool Kit)でも、形態素解析にトライ木を用いています。

参考:http://www.nltk.org/book-jp/ch12.html

Trie(トライ木)とは?

例として、辞書として使われているトライ木の「A」から始まる単語を格納した部分を図示してみました。このように、探索木の各エッジに文字を割り当て、根ノードから葉ノードに至る経路(path)上の文字を拾っていった時の文字列によって辞書を表現します。この辞書には、「AGE」「AGILE」「ALIAS」「ALL」「ALT」「ALTUS」という6つの単語が含まれています。このように、経路によって定義される個々の単語をキーと呼びます。 形態素解析におけるトライ木の活用で言えば、各ノードには形態素が対応し、葉ノードまでの経路が文章を表すことになります。

なぜ辞書検索や形態素解析にトライ木が使われるかというと、大きく下記の理由によります。

  • 共通接頭辞検索(Common-Prefix Search)に適したデータ構造であること
  • 膨大な辞書データを格納する場合や、文字の種類が多い場合でも、検索が高速であること
  • 木構造を表現する際にLOUDSなどの簡潔データ構造を用いれば、省メモリで辞書を表現出来ること

LOUDSとは?

LOUDSとは、木構造をメモリ効率よく表現するための簡潔データ構造です。 Trieに使う場合のLOUDSの概念図を下図に示します。

左側の木がLOUDSによって表現されるTrieであり、右側の表がLOUDS(ビット列)及び、その付加情報としての文字です。 LOUDSとは、仮想ノード(Virtual node)を根ノードの親として用意したあと、下記の手続きで構築されるビット列を用いて、木構造を表現しようというデータ構造です。

  1. ノードが持つ子ノードの数に等しい数だけ「1」のビットを立て、そのあとに「0」のビットを立てる
  2. 仮想ノードから幅優先で木を探索し、1.の手続きを繰り返す

LOUDSは、木構造を最小に近いビット数で表現できます。また、Trieとして用いる際にも、接頭辞検索などの要求に対する処理をある程度高速に行える、バランスの良いデータ構造(簡潔データ構造)であることが知られています。

有名なところでは、Google日本語入力のオープンソース版であるMozcでは、LOUDSを用いてTrieを表現しています。 一方、工藤 拓氏(現Google)が開発した著名なオープンソース形態素解析エンジンMeCabには、ダブル配列(Double-Array)という別のデータ構造に基づくTrieが使われています。

弊社の実績 – 全文検索やSNS解析にトライ木を応用

弊社でも、トライ木(またはトライ木に類するデータ構造)が随所で活躍しています。 例えば、ある「案件情報検索サイト」の全文検索にサジェスト機能を付けた際、サジェストの表示(フロントエンドUI)はAutocompleteを用いましたが、データの辞書化と辞書検索は下記の要領で自作しました。

  1. 案件情報テキストから単語を抽出
  2. 辞書をトライ木で表現
  3. 共通接頭語検索によりサジェストを出力(出現頻度順に10個)

他には、「ある特定の語順が出現したツイートを抽出する」SNS解析システムの実装にトライ木を応用しました。 そのシステムの概略と、具体的な実装上の課題については、今後の記事で紹介していきたいと思います。

The post テキスト処理に使われるTrie(トライ木)とLOUDSに関する概略 first appeared on 株式会社Altus-Five.

]]>
/blog/2017/09/06/trie/feed/ 0
Chainerを使ってMarkov chain(マルコフ連鎖)を書いてみる /blog/2017/06/30/chainer/ /blog/2017/06/30/chainer/#respond Thu, 29 Jun 2017 16:14:00 +0000 http://43.207.2.176/?p=368 本記事は、Python上で動作するニューラルネットワークのライブラリ「Chainer」を使ってマルコフ連鎖を実装、様々な実験を行うものです。 マルコフ連鎖に関する基礎的な解説もしていくので、初学者の方もご安心ください。 […]

The post Chainerを使ってMarkov chain(マルコフ連鎖)を書いてみる first appeared on 株式会社Altus-Five.

]]>
本記事は、Python上で動作するニューラルネットワークのライブラリ「Chainer」を使ってマルコフ連鎖を実装、様々な実験を行うものです。 マルコフ連鎖に関する基礎的な解説もしていくので、初学者の方もご安心ください。

ChainerとMarkov chain

Chainerはそもそもがニューラルネットワークの実装のために開発されたライブラリです。一方、マルコフ連鎖はChainと名前が付くものの、Chainer及びニューラルネットワークに直接の関係はないことに注意してください。ニューラルネットワークは1940年代に起源のあるモデルですが(注目され始めたのは21世紀に入ってからです)、マルコフ連鎖は1906年に最初の論文が発表されている、より古典的かつシンプルなモデルです。

では、本来関係のない両者を結びつけることでどんなメリットが得られるのでしょうか?

Chainerを学びたい人にとっての「良さ」

Chainerでマルコフ連鎖を書こうという試みの長所は、ニューラルネットワークの知識がなくてもChainerを使った記述や動作(の一部)を理解でき、しかもそれなりに意味のある分析ができるというところです。 とりあえずChainerを触ってみたい、という方に本記事の内容はおすすめです。

マルコフ連鎖を学びたい人にとっての「良さ」

Chainerはマルコフ連鎖のためのライブラリではないのですが(だからこそ)、一からコードを書きながらロジックを理解していくことができるという点で優れています。

マルコフ連鎖はとてもシンプルなモデルなので、内部のロジックを一からコーディングしていくことは難しくないのですが、冗長になりがちです。Chainerは行列演算と勾配計算をサポートしていますので、実装の冗長さをうまく取り除いてくれます。その意味で、エンジニアがマルコフ連鎖を「作りながら学ぶ」には非常に向いた環境だと感じています。

前置き

Python (numpy)の便利さ

Chainerに限らず、Python (numpy)は行列演算がサポートされており、高校〜大学の数学で習う行列同士の掛け算などを自分で実装しなくて良い手軽さがあります。これはChainer以前に、Python (numpy)の便利さです。 Chainerを用いることの更なる利点については、マルコフ連鎖として定義できる現象の実例を挙げてからの方が紹介しやすいため、おいおい述べたいと思います。

Chainer tutorialとの対応

本記事は、下記ページ「Introduction to Chainer」における、「Optimizer」の手前までの内容をカバーします。

「Optimizer」以降を読み進めるにはニューラルネットワーク(特に、確率的勾配降下法と損失関数)の知識が不可欠になるので、別途専門書を読む必要があります。逆に言うと、そこまでの内容を読むのにニューラルネットワークの知識は必須ではありません。(挙動を解釈しづらい部分があるので、知識がないとハマることはありますが…。例えば、f.W.gradの出力内容など。) 本記事は、ニューラルネットワークの知識なしで読み書きできる範囲で、Chainerを使って意味のある「結果」を導くことをゴールとします。

マルコフ連鎖の定義と、ニューラルネットワークとの関係

一応、マルコフ連鎖の一般的な説明を最初にしておきましょう。 マルコフ連鎖は行列で表される固定的な遷移規則によって、ネットワークの逐次的な状態変化を記述し、その性質から何らかのアウトプットを得ることを目的に使われる数理モデルです。 まずは例を用いて「利用例」を紹介してみたいと思います。後述する実装パートでは、ここで説明する例の実装を試みます。

上図に示したのは、当サイトの遷移図(一部)です。説明の簡単化のため、下記ページしか存在しない構成になっています。

  • TOPページ
  • 採用情報ページ
  • ブログTOP
  • 記事ページ1,2,3

枝(矢印)はリンクの有無を表しています。また、「離脱」というのはサイト外へ遷移したユーザーの状態を表し、データ上は他のページと同様に扱うものとします。 この図は、サイト上のユーザーの移動を抽象化したものと捉えることができますね。 マルコフ連鎖の文脈では、それぞれのページを 状態 と呼び、状態から状態へ移ることを 遷移 と呼びます。

「状態」という用語に関する注意

例を用いてマルコフ連鎖の説明をするとき、ページを「状態」と呼ばねばならないことにしばしば違和感が生じます。(今回の例も、そうであるかもしれません。) マルコフ連鎖はネットワーク上を動く主体、つまり「ユーザー」の振る舞いを分析するためのツールです。なので各ページが抽象的に表しているのはページそのものというより「あるユーザーがそのページを見ているという状態」だと考えてください。

マルコフ連鎖の応用例

マルコフ連鎖は、このような ネットワーク(有向グラフ) を定義したとき、状態がどのように動くかを分析することのできる簡便かつ強力なツールなのです。 今回はサイト上のユーザー遷移を例にとりますが、応用例は多岐に渡ります。

トピック状態遷移の例
天気予報天気(晴、曇り、雨、雪、etc.)晴れ の翌日が 雨
人口移動市区町村(新宿区、千代田区、港区、渋谷区、etc.)新宿区 在住者が 渋谷区 に引っ越し
Webサイトのユーザー遷移(今回のトピックページTOPページ から ブログTOP へ遷移
自然言語処理単語(我輩、猫、名前、ミケ、etc.)我輩 という単語の次に 猫 という単語が来る

システム開発者にとって興味ある例は、今回扱うユーザー遷移や、自然言語処理でしょう。ただし、マルコフ連鎖を使った自然言語処理(文章の自動生成)はニューラルネットワークを用いたアプリケーションのように高い精度を持つわけではなく、「人口無脳」と表現されるレベルのものであることを注意しておきます。この後説明するように、マルコフ連鎖は状態から状態への推移確率を確定的に定義するため、人間の脳が行うような高度な処理には本来向きません。

マルコフ連鎖を用いた自然言語処理ライブラリには下記のものなどがあります。手軽に試すことができますので、興味のある方は触ってみてください。

マルコフ連鎖とユーザー遷移

マルコフ連鎖のイメージを掴んでもらうため、まずはユーザーの遷移についてデモンストレーションをしてみます。

上図に表したのは、サイトの各ページに着地(Googleなどの外部ページから遷移)してくれたユーザーの数です。これらのユーザーがリンクを辿って、どのように遷移していくかを実験してみましょう。

そこで重要になるのが、ユーザーがどんな規則性を持ってページ間を遷移しているか、ということです。 遷移の規則性も議論の種にはなりえますが、ここではシンプルに「今いるページ(状態)によって確率的に決まる」と考えることにしましょう。例えばTOPページにいるユーザーについて、以下のように確率分布しているとします。

  • 採用情報ページに遷移するユーザーが 50%
  • ブログTOPに遷移するユーザーが 30%
  • 離脱するユーザーが 20%
  • その他のページに遷移するユーザーはいない

これら「あるページから、別のページへ遷移する確率」を総称して 推移確率 と呼びます。マルコフ連鎖の分析においては、手元に推移確率のデータが必要となります。 確率を扱うときは「期待値」を使って説明するのが常道なのですが、小数が出現して話が煩雑になります。まずは「遷移のイメージ」だけを掴んで頂きたいので、モデルから算出される期待値のことはさておき、「実際の移動経路(例)」を図示することにします。

では早速、上図の状態から全ユーザーを一斉に「えいやっ」と遷移させて見ましょう。その結果が下図

所感

  • ブログTOPに多くのユーザーが集まった
  • 2人のユーザーが離脱してしまった

ではさらにもう一度移動させるとどうなるかを見てみます。

所感

  • 各記事ページに多くのユーザーが集まった
    • 1つ前のステップにおいて、たくさんのユーザーがブログTOPを見ていたためと考えられる
  • 2人のユーザーが新たに離脱してしまった

こんな具合にユーザーを何度も遷移させて、どのような振る舞いをするか調べるのがマルコフ連鎖を用いた分析です。

マルコフ連鎖の推移確率行列

本節では、上述したユーザーの遷移をシステム上で取り扱うにはどうすれば良いか、という話をします。 まずは、ネットワークの性質をデータとして保持する必要がありますが、まずは推移確率をコンパクトにまとめることを試みましょう。

上図の見かたを説明します。各行・各列は状態(ページ)に対応しており、各セルに含まれる数値は「数値が含まれる列の状態から、行の状態に遷移する確率」を表しています。

例えば(TOPの行, 採用の列)にある成分が0.5であることは、「採用情報ページからTOPページに遷移する確率が0.5(50%)」であることを表しています。

この推移確率をコンパクトにまとめた表は数学(線形代数)の文脈で「行列」(Matrix)と呼ばれるもので、データ上は2次元リストなどを用いて表します。 行列は加・減・乗算といった演算の定義があり、マルコフ連鎖やニューラルネットワークの学習においては特に乗算(掛け算)の定義を知っていることが 必須 です。 実は、マルコフ連鎖における「乗算」は直感的な解釈ができるので、行列演算が苦手な方の克服材料としても使うことができます。ですのでもし、抵抗感があっても読み進めてみてください。

行列同士の乗算を定義する

まず書き方ですが、二つの行列を並べることによって「積」(乗算結果)を表します。

2×22×2行列同士の積を書いてみました。計算結果はどのようになるでしょうか。

このようになります。行列同士の乗算の計算過程について、おさらいしておきましょう。

計算結果の(1行目, 1列目)の成分を計算する方法を説明します(以降、(1, 1)成分と略記)。 (1, 1)成分を求めるには、乗算の左オペランドの1行目と、右オペランドの1列目を使います。左オペランドは左から右、右オペランドは上から下に数値を見て、それぞれ対応する位置にある数値の積について総和を取ったものが計算結果になります。

一般に、(i,ji,j)成分を計算するには、左オペランドのii行目と、右オペランドのjj行目を見て、同様の計算を行います。

このことからわかる通り、左オペランドの 列数 と右オペランドの 行数 が一致している時のみ、行列の積は定義されます。上図の右のケースでは不一致が生じているため、計算結果は未定義となります。

マルコフ連鎖の遷移は、行列同士の乗算で表現できる

上記の行列の乗算は、マルコフ連鎖の「遷移」という概念と面白いようにハマります。下記の、少し大きな行列の乗算をご覧ください。

この行列演算が、何を意図したものであるかについてご説明します。

左辺(イコールの左側)の解説

左辺の左オペランドの7×77×7行列は、先ほど例示したWebサイトのユーザー遷移に関する推移確率行列になっています。右オペランドの7×17×1行列(7次のベクトル, vector)は、やはり先ほど例示した各状態(ページ)に着地したユーザーの数になっています。マルコフ連鎖の文脈では、これを 分布 とも呼びます。

右辺(イコールの右側)の解説

推移確率行列と分布を乗ずることで求まる右辺の計算結果は、7×17×1行列になります。(先ほど紹介した乗算の手続きから、一般にl×ml×m行列とm×nm×n行列の積はl×nl×n行列になります。) ここで右辺のもつ意味を明らかにするため、もう一度乗算の計算過程についておさらいしてみましょう。

積の(1, 1)成分を求めるには、左オペランドの1行目と右オペランドの1列目(今回は1列しかないので、行列全体)を見るのでした。そして、ポジション(1st, 2nd, 3rd, …)が対応する数値同士の積の総和を取ります。(1, 1)成分を求める具体的な計算式は下記のようになります。

0×10+0.5×5+0.1×1+0.15×3+0.15×2+0.25×2+0×0=3.650×10+0.5×5+0.1×1+0.15×3+0.15×2+0.25×2+0×0=3.65

ここで、推移確率行列の要素が持つ意味をおさらいしておきましょう。(i,j)(i,j)成分は「状態jjから状態iiへと遷移する確率」を意味するものでした。例えば推移確率行列の(1,2)(1,2)成分である0.50.5は、「第2ページ(採用情報)にいるユーザーが、直後に第1ページ(TOP)へと推移する確率が50%」であることを示します。 そして右オペランドは「状態上の分布」を表すものです。右オペランドの(2,1)(2,1)成分は55ですが、これは「第2ページにいま5人のユーザーがいる」ことを表します。 右オペランドの(1,1)(1,1)成分の計算において、この55に対してマッチングされる値は0.50.5(50%)です。計算結果は5×0.5=2.55×0.5=2.5となりますが、これは「5人のユーザーが第2ページにおり、うち0.5(50%)のユーザーが第1ページに遷移するので、第2ページから第1ページへ遷移するユーザーは(期待値の意味で)2.5人いる」という風に書き下すことができます。 以上の数値同士の乗算を、1stから7thまですべてのポジションについて行った総和である数値3.653.65は何を表すでしょうか?

考えやすくするために、(1,1)(1,1)成分の計算を図示してみました。

上図と、下記の計算式を照らし合わせてみてください。

0×10+0.5×5+0.1×1+0.15×3+0.15×2+0.25×2+0×0=3.650×10+0.5×5+0.1×1+0.15×3+0.15×2+0.25×2+0×0=3.65

これは、「遷移後に第1ページにいるユーザー数の期待値」の計算になっていることが分かります。第1ページへの遷移は第1〜第7全てのページから起こりえますが、全ての「遷移元」に関して遷移数の期待値の合計を取る計算になっているわけですね。

右オペランドの他の成分についても同様で、(i,1)(i,1)成分(上からii番目の成分)は、「遷移後に第iiページにいるユーザー数の期待値」になっています。

つまり 分布のベクトルに対して左から推移確率行列を掛けると、遷移した後の分布が求まる のですね。

マルコフ連鎖とページランク

このモデルを用いる際に注意せねばならないことは、 「現在の状態」のみに依存して次の状態への推移確率が決まる という仮定を置いているということです。これは非常に強い仮定であり、いま分析したい対象に当てはまるかどうかはよく吟味せねばなりません。ユーザー遷移の分析で言えば「ユーザーの性別・年齢・興味といった属性を無視してよいのか?」「2つ以上前に見ていたページも考慮すべきでは?」などの疑問が湧き、これらの影響が無視できないならばモデルを適宜改良する必要があります。

ここで、マルコフ連鎖をWebのユーザー遷移の分析に適用することについては、偉大な先例があることに言及させてください。Googleの創業者の一人であるラリー・ペイジが提案した「ページランク」は、マルコフ連鎖に基づくモデルなのです。 下記のペイジの論文を読むとわかる通り、ページランクの仕組みは上述した内容とほぼ同一のロジックによって、Webページの価値を評価するというものでした。Googleの登場当時は、このモデルが革新的なロジックとして受け入れられたという訳です。 (明示的に「Markov chain」という文言は出てきませんが、下記論文のpp.3-5にある図はマルコフ連鎖の概念図そのものです。)

ただし、ページランクには「人工的なリンクによる評価の操作ができる」という弱点がありました。その対策のために検索エンジンがアップデートされ(ペンギンアップデート)、その後ページランクはGoogleの検索エンジンから脱落していきます。 マルコフ連鎖による単純なモデリングは、残念ながら検索エンジンとしては欠点がありましたが、データを取り扱うフレームワークとしてはいまも十分に有効です。

ニューラルネットワークの順伝播との類似性

※この節は読まなくても、理解上差し支えありません。

ニューラルネットワークを齧ったことのある方は、大抵の文献に直上の挿絵とよく似た図が入っているのをご存知かと思います。例えば、ニューラルネットワークの入門書としてよく引用されている下記文献の、p.2やp.9などです。

ニューラルネットワークは層間の順伝播を線形式で行うことが一般的ですが、これはマルコフ連鎖における「推移確率行列を左から掛ける」という操作と全く同じ計算です。 ここに両者の類似性があるのです。

まったくの初学者がニューラルネットワークを学ぶにあたっては、少し寄り道にはなりますが、マルコフ連鎖について知っておくと、様々なシーンにおいて理解の助けになると考えています。

(繰り返しになりますが、マルコフ連鎖の基礎理解に必要な知識は、ニューラルネットワークの理解に必要な知識の一部分となっています。)

Chainerでマルコフ連鎖を実装してみる

いよいよ、Chainerを使ったマルコフ連鎖の実装に移ります。Chainerの公式情報としては、下記のTutorialを参照すれば十分ですので、ご活用ください。

また、開発環境の導入については下記ガイドをご覧ください。

ChainerはPythonの数値計算ライブラリであるnumpyを活用しています。numpyは上述した行列の演算をnp.ndarrayというクラスでサポートしているため、行列積や、より高度な計算を関数として呼び出すことが出来て便利です。 以降は開発環境の導入は済んだものとして、iPythonのような対話型環境を用いるか、.pyファイルに手続きを記述していく前提で話を進めます。

ChainerのVariableを定義する(分布)

まずはマルコフ連鎖の「分布」を定義してみる事にしましょう。nn通りの状態の上の分布はn×1n×1行列で表されますが、これをndarrayクラスのオブジェクトとして定義します。

np.array([[10, 5, 1, 3, 2, 2, 0]])

ndarrayクラスのオブジェクトを生成するには、np.array関数を用いるのが一般的です。引数としてPythonのリスト(行列なので二次元リスト)を渡せば、対応する行列が生成され、行列和・積の計算や、より高度な計算(逆行列の計算や、固有値・固有ベクトルの計算)を関数として呼び出せるようになります。

Chainerにおいてはndarrayクラスのオブジェクトをそのまま扱わずに、ndarrayを入力としてVariableオブジェクトを生成します。これを踏まえ、Chainerの計算で用いる分布は下記のように定義します。

distribution = Variable(np.array([[10, 5, 1, 3, 2, 2, 0]], dtype=np.float32))

ここで、distributionは所与の変数名です(分布の意)。 Variableクラスのコンストラクタの引数として渡せるものは、numpy.ndarray又はcuda.ndarrayオブジェクトです。 データタイプはTutorialに倣い、numpyの32ビット浮動小数点型であるnp.float32を指定しています。

次に推移確率行列を定義しますが、これにはChainerのLinkというオブジェクトを用います。Linkの主な機能は以下の通りです。

  • mm次元ベクトル(の集合)をnn次元ベクトル(の集合)へと変換するメソッドを持つ
  • 変換の内容を決めるパラメータを保持している
  • Linkを組み合わせてネットワークを構築し、誤差関数を定義して訓練データを与えることで、パラメータの数値を最適化できる(ニューラルネットワーク向きの機能。今回は触れない)

Tutorialでは特によく用いられるLinkとしてL.Linear(線形リンク)を紹介しています。L.Linearは「行列の積と和だけで表現できる変換」をカバーしていますので、今回の遷移という用途にはぴったりです。早速定義してみましょう。

transition = L.Linear(7, 7)

transitionは所与の変数名で(遷移の意)、L.LinearはL.Linearのコンストラクタです。(7, 7)という引数は、「7次元から7次元への変換を行うLinkを定義」するよう指定しています。 この定義により、transitionは「7という次元数(状態数)は変えずに、各次元に対応する数値を変えたものを返すL.Linear」となります。 今は7状態からなるネットワークについてマルコフ連鎖を実装しようとしていますので、Linkには「分布を入力すると、遷移後の分布を返してくれる」機能が求められます。なので、入力と出力の次元数が等しいLinkを定義しました。 問題は、このリンクの行う処理の中身です。 transitionについて、推移確率行列にあたるパラメータはtransition.W.dataに格納されていますので、これを確認してみましょう。

print transition.W.data


>>> [[-0.86491805 -0.16170089  0.34361446 -0.0636134  -0.22656611 -0.63621569 -0.17878614]
>>>  [ 0.17760617  0.29629865 -0.33722615 -0.12503195  0.16237867 -0.20694737 -0.60708505]
>>>  [-0.06904662 -0.03128463  0.22083846  0.36412853 -0.03627913 -0.03684266  0.54450828]
>>>  [ 0.10557969  0.04198727 -0.01530487  0.61610317 -0.16425815 -0.26798156  0.26575172]
>>>  [-0.0686424  -0.54063159 -0.34978956  0.24700937 -0.16933322 -0.04976032 -0.21227303]
>>>  [ 0.57017905 -0.00309141 -0.00351088  0.2253985  -0.01208178 -0.11499751 -0.2914691 ]
>>>  [-0.40216371  0.69113183 -0.45077264  0.32903543  0.16823213 -0.36826468  0.29529756]]

このように、生成時にはランダムなパラメータが入力されています。これを意図した値にするためには、transition.W.dataに代入を行うか、コンストラクタの引数としてnp.ndarrayオブジェクトを渡します

ここでは代入を行うことで対応することにしましょう。

transition.W.data = np.array([[0, 0.5, 0.1, 0.15, 0.15, 0.15, 0],
                                [0.5, 0, 0.05, 0.15, 0, 0, 0],
                                [0.3, 0.3, 0, 0, 0.1, 0.1, 0],
                                [0, 0, 0.3, 0, 0.1, 0, 0],
                                [0, 0, 0.25, 0.1, 0, 0.1, 0],
                                [0, 0, 0.25, 0, 0.1, 0, 0],
                                [0.2, 0.2, 0.05, 0.6, 0.55, 0.65, 1]], dtype=np.float32)

(もうひとつ、transition.bというパラメータがあるのですが、これは0によって初期化されるため以降の計算に影響しません。今回は説明を省略します。)

以上の定義に基づき、distributionの状態から、transition.Wで指定の推移確率行列に基づいて遷移したユーザーの分布を求めてみましょう。

distribution_after_transition = transition(distribution)

print distribution_after_transition

>>> variable([[ 3.65        5.5         4.9         0.5         0.75            0.45        7.25      ]])

想定通りの結果となりました(ただし、実際にはnp.float32由来の誤差が僅かに生じます)。Chainerでは各クラスに__call__関数が定義されており、オブジェクト名によって基本的な関数を呼び出すことが出来ます。L.Linearオブジェクトの場合、入力をWに基づいて変換したものを返す関数が呼び出されます。

Chainクラスを用いてMarkovChainを定義する

ChainerにはChainというクラスがあり、Chainオブジェクトのインスタンス変数としてLinkを配下に置くことで、様々な機能を使うことが出来ます。 Chainはニューラルネットワークの構築を主眼に置いたクラスですが、先述した通りニューラルネットワークとマルコフ連鎖には類似性があるため、Chainクラスを使ってMarkov Chainを定義することは容易です。

class MyMarkovChain(Chain):
    def __init__(self, tpa):
        super(MyMarkovChain, self).__init__()
        if tpa.shape[0] == tpa.shape[1]:
            dim = tpa.shape[0]
            with self.init_scope():
                self.transit = L.Linear(dim, dim)
                self.transit.W.data = tpa
                self.loop = 0
    def __call__(self, dist):
        return self.transit(dist)

MyMarkovChainクラスを、Chainクラスを継承して定義しています。コンストラクタにnp.ndarrayクラスの引数tpm(transition probability matrix, 推移確率行列の意)を渡すと、行と列の次数が等しいことを確認した上で、インスタンス変数であるL.Linear型変数self.transitに格納します。また、以降のloop回数制御のため、self.loopというインスタンス変数も用意しておきます。 このクラスを用いるには、例えば以下のようなコードを書きます。

markov_chain = MyMarkovChain(np.array([[0, 0, 0.1, 0.15, 0.15, 0.15, 0],
                                  [0.5, 1, 0.05, 0.15, 0, 0, 0],
                                  [0.3, 0, 0, 0, 0.1, 0.1, 0],
                                  [0, 0, 0.3, 0, 0.1, 0, 0],
                                  [0, 0, 0.25, 0.1, 0, 0.1, 0],
                                  [0, 0, 0.25, 0, 0.1, 0, 0],
                                  [0.2, 0, 0.05, 0.6, 0.55, 0.65, 1]], dtype=np.float32))
distribution = Variable(np.array([[10, 5, 1, 3, 2, 2, 0]], dtype=np.float32))
print markov_chain(distribution)

1行目では推移確率行列を与えることで、MyMarkovChainクラスのmarkov_chainを定義しています。 2行目で初期分布 distributionを定義し、3行目ではdistributionから一回だけ遷移させた結果の分布を計算し、出力します。(markov_chainクラスについて関数名を明示せず関数呼び出しをしているので、 call メソッドが呼び出されます。)

このクラスに対し、さらにクラスメソッドを定義してみましょう。

指定の回数だけ遷移させる関数

引数timesを取り、times回遷移させる関数transit_designated_timesを書いてみましょう。 マルコフ連鎖における複数回遷移は明快で、初期分布を表すベクトルに対し、左から times回、推移確率行列を掛けてあげるだけです。

def transit_designated_times(self, dist, times):
    temp = dist
    for num in range(times):
        temp = self(temp)
    return temp

計算量を減らす工夫

一般に、n×nn×n行列とn×1n×1行列の掛け算の計算量はO(n2)O(n2)です。推移確率行列を左からtt回掛ける愚直なやり方だと、O(tn2)O(tn2)の計算量がかかることになりますが、これを短縮する方法があります。

一般に推移確率行列をAA、分布を表すベクトルをddと表した時、左からAAをtt回掛ける操作はAAA…AdAAA…Adのように表されます(AAがtt個。)これを通常の値(スカラ)の累乗と同じようにAtdAtdと表します。 行列の計算には結合則というものが成り立つことが知られており、ddに対してAAを左からtt回掛けても、AtAtを先に求めてからddに掛けても、計算結果は変わりません。言い換えると、AAに従うtt回の遷移を表すAtAtという推移確率行列が存在するので、それを先に求めてからddに掛けるやり方でも良いのです。

そして、AtAtには効率的な求め方があるのでご紹介します。ここではA10A10を求めることにしましょう。

  1. AAとAAを掛けてA2A2を求め、記憶
  2. A2A2とA2A2を掛けてA4A4を求め、記憶
  3. A4A4とA4A4を掛けてA8A8を求め、記憶
  4. 記憶していたA2A2とA8A8を掛けてA10A10を作る

最後の4.で掛けるべき要素は、一般に、ttの二進数表現に基づいて決めることになります。 この方法を用いれば、計算量はO(logtn2)O(log⁡tn2)のオーダまで落ちることが知られています。

定常分布(極限分布)まで遷移させる関数

マルコフ連鎖には定常分布(Stationary distribution)と呼ばれる分布が存在することがあります。

推移確率行列をAA、分布をππとして、Aπ=πAπ=πとなるような分布として定義されます。つまり、遷移させても分布(の期待値)が変化しないような分布ππを定常分布と呼ぶわけです。

ちなみに今回取り上げている推移確率行列については自明で、nn人のユーザーからなる分布については(0,0,0,0,0,0,n)(0,0,0,0,0,0,n)が唯一の定常分布です(全員が離脱した状態)。推移確率行列の取り方によっては、複数状態に遍在したり、定常分布が複数存在したり、定常分布が存在しないこともあります。(詳しくはマルコフ連鎖のテキストをご参照ください。)

定常分布には「一旦そこに入り込むと出られない」という性質だけでなく、「別の分布を何回も遷移させていると、定常分布にだんだん近づいていく」という性質を持つことがあります。今回の推移確率行列は「いかなる分布であっても、遷移を十分な回数行うと、定常分布に収束する」という強い性質があります。(このような定常分布を特に、極限分布と呼びます。)

という訳で、極限分布まで遷移させる関数を書いてみましょう。先のサイト内遷移の例でいうと、この関数を定義するメリットは以下の通りです:

  • 各ページへの訪問数に基づいてユーザーの動きを予測し、各ページおよび全体のページビュー数や、ユーザーあたりの平均ページビュー数を概算できる

ひとまず、「遷移させても分布が変わらなくなるまで遷移させ続ける」という愚直な方法で書いてみようと思います。

def transit_until_limit_distribution(self, dist_before_transit):
    self.loop = self.loop + 1
    dist_after_transit = self(dist_before_transit)
    print "Distribution after", self.loop,"th transition:"
    print dist_after_transit
    if np.linalg.norm(dist_after_transit.data - dist_before_transit.data) < (10 ** -5):
        self.loop = 0
        return dist_after_transit
    else:
        return self.transit_until_limit_distribution(dist_after_transit)

transit_until_limit_distributionは再帰的な関数として書きました。else文の中で自身を呼び出していますので、if文の中身がFalseである限り関数が繰り返し呼び出されます。 ここでif文の不等式について解説しておきましょう。 np.linalg.normは、ベクトルのノルムと呼ばれる数値を計算する、numpyの関数です。ノルムはベクトルに対して「長さ」や「大きさ」といった尺度を与えるもので、2次元や3次元の「距離」を拡張した概念です。 np.linalg.normの引数としてdist_after_transit.data - dist_before_transit.dataを渡しています。「遷移後の分布」と「遷移までの分布」が近づいていくと、その差のベクトルはきわめて小さくなるため、距離であるノルムは0に近づいていきます。ノルムが10−510−5という「十分小さい値」を下回った時点で計算を打ち切るようにして、近似的に定常分布を求めています。

より望ましい計算方法

上記のコードは「定常分布」の観念的な説明も兼ねているため、あえて非効率な計算方法を取っています。上記のコードには幾つかの弱点があります:

  • 計算量が大きい(状態数をnn、ループ回数をllとしてO(ln3)O(ln3))
  • 定常分布に近づかないような初期状態を選ぶと、無限ループに陥る
  • あくまで近似的な計算である

ここでは、より望ましい計算方法として「固有値1に対応する固有ベクトルを求める」という方法があることにのみ言及しておきます。 固有ベクトルの計算についてもnumpyの関数として実装されています。理論にご興味のある方は、線形代数の入門書をご参照ください。

感度分析を行うMyMarkovChainクラスを書いてみる

さて、ここまでの内容だけですと「Chainer使わなくてもnumpyで簡単に出来るじゃん」というツッコミが入りそうなので、そろそろChainerならではの要素を盛り込んでみたいと思います。

ニューラルネットワークライブラリとしてのChainerの強みとしては、以下が挙げられます。

細かいところは置いておいて、まずは今からやりたいことの説明をしてみたいと思います。

先ほどまでの議論は、「推移確率行列を所与とした時、サイトに来てくれたユーザーはどのように動くか?」という記述が主眼に置かれていました。 一方、分析の目的によっては「推移確率行列が変化するとどのようにユーザーの動きが変わるか?」も知りたいはずです。例えば以下のようなシチュエーションを考えてみましょう:

  • サイト内に「見て欲しいページ」(以下、目標ページ)がある
  • 目標ページに辿り着きやすいように動線を改善したい
  • どのリンクをクリックしやすくすれば、目標ページに辿り着きやすくなるだろう?

つまり、transitという遷移の仕組み(もっと言うと、推移確率行列であるtransit.W)が変化した時に、どのように結果の数値が変わるかを調べたい状況を想定します。

目標ページを決める

ここでは「採用情報ページ」を見て欲しいと仮定して、分析を進めてみます。

また仮定として、「同じユーザーが1回見てくれても、2回以上見てくれても、嬉しさは変わらない」とし、「採用情報ページを1回以上見てくれたユーザー」と、「採用情報ページを1度も見ずに離脱してしまったユーザー」を振り分けるようなシミュレーションをしてみたいと思います。

その目的を反映するために、推移確率行列をちょっとだけ書き換えてみます。

上記の行列のどこを書き換えたかというと、2列目の要素のみが変わっており、(2,2)(2,2)成分が11、その他の成分が00となっています。 これは目標ページである採用情報ページを、離脱状態と同様「一度入ったら出られない」状態に書き換えたということですが、これは「一度以上採用情報ページを見てくれたユーザー」と「そうでないユーザー」を区別するための方便です。

この場合の定常状態は、例えば以下のようになります。

採用情報ページに流入したユーザーは、その時点で「目標に到達した」ことが分かりますので、今回の目的の下では、目標到達以降の遷移のトラッキングは不要です。 採用情報ページに留まり続けるようにすることで、トラッキングの終了を表現しています。

また、結果的に何人が採用情報ページを見てくれるかというシミュレーション結果も、定常状態の分布をみれば一目瞭然です。(上記の例は、23人中12人が見てくれるという結果になっています。)

実際のシミュレーション結果を確認してみましょう。

markov_chain = MyMarkovChain(np.array([[0, 0, 0.1, 0.15, 0.15, 0.15, 0],
                                  [0.5, 1, 0.05, 0.15, 0, 0, 0],
                                  [0.3, 0, 0, 0, 0.1, 0.1, 0],
                                  [0, 0, 0.3, 0, 0.1, 0, 0],
                                  [0, 0, 0.25, 0.1, 0, 0.1, 0],
                                  [0, 0, 0.25, 0, 0.1, 0, 0],
                                  [0.2, 0, 0.05, 0.6, 0.55, 0.65, 1]], dtype=np.float32))
distribution = Variable(np.array([[10, 5, 1, 3, 2, 2, 0]], dtype=np.float32))
print markov_chain.transit_until_limit_distribution(distribution)

> ...
> Distribution after 19 th transition:
variable([[  1.68337533e-06   1.23127584e+01   1.67016697e-06
             1.39875044e-06   1.46680520e-06   1.21840844e-06
             1.06872349e+01]])

あくまで近似計算であることと、丸め誤差から若干分かりにくいですが、 第2ページ(採用情報ページ)に約12.31人、離脱状態に約10.69人のユーザーがおり、他のページには殆どユーザーが残っていないことが分かります。 つまり、期待値において、初期分布 distributionと推移確率行列tpaの下で、採用情報ページを見てくれるユーザーは約12.31人であるという結果が導けます。

感度分析 – 推移確率行列の変化による影響

最後のトピックとして、推移確率行列markov_chain.transition.Wが変化したとき、結果がどのように変わるかを調べてみたいと思います。 例えば、TOPページからの離脱率を5%引き下げて、採用情報ページへの推移確率を5%高めることができたとしましょう。その場合、先ほどの計算結果における、目標ページ到達ユーザーは増加することが考えられます。 しかしその増加分が「どのくらいであるか」見積もるのは、あまり簡単ではなさそうですよね。

ところが、Chainerには強力な武器があります。計算過程の一部パラメータを変更した時、出力(今回の場合、定常分布)がどのように変動するかを調べることのできる計算手法である 誤差逆伝播法(Backpropagation) が関数として提供されているのです。

Tutorialでは最初のトピックとして出てくるくらいなので、ニューラルネットワークにおいては必須の計算なのですが、その原理の理解には微分の連鎖規則に関する知識が必要になりますので、今回は省略させて頂きます。

誤差逆伝播法の計算内容と、Chainerがこの計算をサポートできているカラクリについて、正確さを犠牲にしてざっくり説明するなら、以下のようになります:

  • ChainerのVariable変数は値だけでなく、「現在の値が格納されるまでの計算過程」も保存してくれる
    • なので、計算過程の一部パラメータが変化した時の影響を事後的に評価することが可能
  • 評価の具体的なやり方は誤差逆伝播法に基づいており、パラメータの変動に対して出力がどのように変化するかを、出力結果から遡り影響を積算して見積もってくれる

一般に、「計算過程の一部パラメータが変わった時、出力がどのように変化するか?」を調べる分析を 感度分析 と言います。その際に重要なツールが 微分(ニューラルネットワークの文脈では 勾配計算 と呼ばれる)であり、誤差逆伝播法は長大な計算過程に対する勾配を計算するための技法です。 Chainerの誤差逆伝播法は感度分析を目的としたものではなく、確率的勾配降下法(SGD)等によるニューラルネットワークのパラメータ更新を主目的としていますが、いずれも本質的な操作は「微分(勾配計算)」です。この点も、マルコフ連鎖とニューラルネットワークのちょっとした共通点と言えそうです。

感度分析の実装

感度分析ができるよう、transit_until_limit_distributionを書き換えたものが下記コードです。

def transit_until_limit_distribution(self, dist_before_transit):
    self.loop = self.loop + 1
    dist_after_transit = self(dist_before_transit)
    print "Distribution after", self.loop,"th transition:"
    print dist_after_transit
    dist_after_transit.grad = np.array([[0, 1, 0, 0, 0, 0, 0]], dtype=np.float32)
    dist_after_transit.backward()
    print "Sum of gradients up to", self.loop, "th transition:"
    print self.transit.W.grad
    if np.linalg.norm(dist_after_transit.data - dist_before_transit.data) < (10 ** -5):
        self.loop = 0
        return [dist_after_transit, self.transit.W.grad]
    else:
        return self.transit_until_limit_distribution(dist_after_transit)

追加した行について解説していきましょう。

重みの設定

dist_after_transit.grad = np.array([[0, 1, 0, 0, 0, 0, 0]], dtype=np.float32)

これは各ステップにおける遷移結果であるdist_after_transitについて、どの状態への到達を評価するか決めている行です。 今回は第2ページ(採用情報ページ)を目標ページに設定したので、第2成分のみを1とし、他のページを0としています。ここで複数の目標ページがあったり、それらの間に重みが存在する場合は、適宜設定を変更することができます。

注意 ニューラルネットワークの用途では、この数値は『初期誤差』と呼ばれます。例えば 岡谷(2015)において、p.49にδ(L)jδj(L)として定義されている数値に該当します。

誤差逆伝播法の実行

dist_after_transit.backward()

backwardはVariableオブジェクトに対して誤差逆伝播法を呼び出す関数です。dist_after_transit、すなわちそのループ時点で求まっている分布について、初期分布からの計算過程を全て追跡し、markov_chain.transition.W(推移確率行列)が変化した時の影響を微分計算により評価します。

結果の出力

print "Sum of gradients up to", self.loop, "th transition (gradient)"
print self.transit.W.grad

ループ毎に、その時点での影響の総和を出力します。

実行結果

markov_chain = MyMarkovChain(np.array([[0, 0, 0.1, 0.15, 0.15, 0.15, 0],
                                  [0.5, 1, 0.05, 0.15, 0, 0, 0],
                                  [0.3, 0, 0, 0, 0.1, 0.1, 0],
                                  [0, 0, 0.3, 0, 0.1, 0, 0],
                                  [0, 0, 0.25, 0.1, 0, 0.1, 0],
                                  [0, 0, 0.25, 0, 0.1, 0, 0],
                                  [0.2, 0, 0.05, 0.6, 0.55, 0.65, 1]], dtype=np.float32))
distribution = Variable(np.array([[10, 5, 1, 3, 2, 2, 0]], dtype=np.float32))
markov_chain.cleargrads()
limit_distribution, limit_effect = markov_chain.transit_until_limit_distribution(x)

> Sum of gradients up to 19 th transition (gradient)
> [[   7.21651173  118.8604126     3.20709443    2.93544102    2.46638298     2.19952631   96.99636078]
>  [  12.53825283  222.91799927    5.57215118    5.10016489    4.2852087      3.82155728  182.76469421]
>  [   3.15799093   48.29377365    1.40343404    1.28455925    1.07929468     0.96251857   39.21710968]
>  [   3.15039349   51.00441742    1.40006709    1.28147614    1.07670712     0.96021003   41.57637024]
>  [   1.87185001   28.3139782     0.83186328    0.76140237    0.63973486     0.57051766   22.97503281]
>  [   1.5854516    23.93289566    0.70458531    0.64490521    0.54185313     0.48322648   19.41728973]
>  [   0.            0.            0.            0.            0.             0.           0.         ]]

結果の解釈

出力の7×77×7行列は、推移確率行列(markov_chain.transition.W)と同じ形をしています。 これらの値は、対応する位置の推移確率が大きくなった時、目標ページ到達ユーザー数がどれくらい増えるかということを表す数値になっています。 数値をパーセンテージで解釈しやすくするため、値を100で割ってみます。


limit_distribution, limit_effect = markov_chain.transit_until_limit_distribution(x)

> Sum of gradients up to 19 th transition (devided by 100)
> [[ 0.07216512  1.18860412  0.03207095  0.02935441  0.02466383  0.02199526   0.96996361]
>  [ 0.12538253  2.2291801   0.05572151  0.05100165  0.04285209  0.03821557   1.82764697]
>  [ 0.03157991  0.48293772  0.01403434  0.01284559  0.01079295  0.00962519   0.39217108]
>  [ 0.03150393  0.51004416  0.01400067  0.01281476  0.01076707  0.0096021    0.41576371]
>  [ 0.0187185   0.2831398   0.00831863  0.00761402  0.00639735  0.00570518   0.22975034]
>  [ 0.01585452  0.23932895  0.00704585  0.00644905  0.00541853  0.00483226   0.1941729 ]
>  [ 0.          0.          0.          0.          0.          0.           0.        ]]

上記の数値は、 「各推移確率を1%高めた時、目標ページ到達ユーザーが何人増えるか」 を示した数値になっています。 推移確率行列との対応付けを分かりやすくするため、上記の結果を行列形式で書いてみました。

ただし、以下の遷移はネットワークの定義に反し、分析上意味がないので、考えないものとします。

  • 離脱したユーザーが他のページに戻ってくる遷移
  • 目標ページに到達したユーザー(トラッキングをやめたユーザー)が他のページに移動する遷移
  • 同じページに止まるような遷移

これらの遷移に該当する箇所を隠して、結果を見やすくしてみましょう。

この行列を眺めてみると、以下のような傾向がつかめます。

  1. 第2ページ(採用情報ページ)への遷移を表す2行目の数値が特に大きい
  2. その次に、第1ページ(TOPページ)への遷移を表す1行目の数値が大きい
  3. 4, 5, 6行目(記事1, 2, 3への遷移)を比べると、記事1への遷移を表す4行目の数値が大きい

まず1.に関しては、第2ページは目標ページそのものですから、「他ページから第2ページへの遷移を増やせば、第2ページを見てくれるユーザーが増える」という当然の帰結となっています。 一方、2.や3.については興味深い結果です。第3ページ(ブログTOP)や第4〜6ページ(記事ページ)から、第1ページ(TOPページ)への遷移が増えると、無関係のはずの第2ページ(採用情報ページ)を見てくれるユーザーが増えることを示唆しています。 その理由は以下のように説明できます。第1ページ → 第2ページという動線はもともと太く、第1ページを見ているユーザーは50%もの確率で第2ページへ遷移してくれていました。つまり、第1ページは第2ページへの遷移をアシストしてくれる「つなぎ」のページとして優秀なのです。

この数値を解釈すると、 「第2ページ(採用情報ページ)に直接ユーザーを送り込むことがサイト設計上難しければ、まずは第1ページ(TOPページ)へ送りこむことを目標としても、第2ページ(採用情報ページ)を見てもらうという目標を達成することができる」 ということに気が付ける訳です。

3.についても同様です。第4ページ(記事1)は唯一、第2ページ(採用情報ページ)への遷移実績がある記事でした(推移確率行列を参照)。 このことから、 「もっと記事1を多くの人に読んでもらえれば、結果的に採用情報ページを見てくれる人が増える」 といったことも分かります。

まとめ

本記事は、2つの目的を同時に達成することを目的にまとめていきました。

  • ニューラルネットワークの知識がなくても、Chainerのコードの書き方や挙動について何となく知ることが出来る
  • ニューラルネットワークの近隣知識として、マルコフ連鎖について紹介する

両者のモデルには似ているところが多いので、面白い成果物が出来るのではないかと思いましたが、やはりニューラルネットワークに専心したライブラリ上で別の数理モデルを構築しましたので、少々実装について強引な部分もあったことを反省しています。

繰り返しになりますが、ニューラルネットワークを学ぶ目的であれば、マルコフ連鎖の学習はあくまで「寄り道」です。しかし、行列演算を直観的に解釈できる明快なモデルなので、行列演算に不慣れな方の取っ掛かりとしては適しているのではないかな、と思っています。

ニューラルネットワークの基礎理解には、本記事で解説した内容(行列演算)に加えて、以下の知識が必須になります。

  • 活性化関数
  • 誤差関数
  • 誤差関数最小化アルゴリズム(確率的勾配降下法など)
    • 勾配計算
    • 誤差逆伝播法

上記は、Chainer TutorialのOptimizer以降の項を理解しながら読む時点で必要になります。やはり敷居の高さは感じざるを得ませんが、下記文献の4章までを読んでいけばカバー出来ます。興味のある方は、挑戦してみてください。

ソースコード

本記事の最終的な成果(感度分析)を実行出来るソースコードを公開します。 実行にはChainerのインストールが必要ですので、ご注意ください。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class MyMarkovChain(Chain):
    def __init__(self, tpm):
        super(MyMarkovChain, self).__init__()
        if tpm.shape[0] == tpm.shape[1]:
            dim = tpm.shape[0]
            with self.init_scope():
                self.transit = L.Linear(dim, dim)
                self.transit.W.data = tpm
                self.loop = 0
    def __call__(self, dist):
        return self.transit(dist)
    def transit_designated_times(self, dist, times):
        temp = dist
        for num in range(times):
            temp = self(temp)
        return temp
    def transit_until_limit_distribution(self, dist_before_transit):
        self.loop = self.loop + 1
        dist_after_transit = self(dist_before_transit)
        print "Distribution after", self.loop,"th transition:"
        print dist_after_transit
        if np.linalg.norm(dist_after_transit.data - dist_before_transit.data) < (10 ** -5):
            dist_after_transit.grad = np.array([[0, 1, 0, 0, 0, 0, 0]], dtype=np.float32)
            dist_after_transit.backward()
            print "Sum of gradients up to", self.loop, "th transition (devided by 100)"
            print self.transit.W.grad / 100
            self.loop = 0
            return [dist_after_transit, self.transit.W.grad]
        else:
            return self.transit_until_limit_distribution(dist_after_transit)


markov_chain = MyMarkovChain(np.array([[0, 0, 0.1, 0.15, 0.15, 0.15, 0],
                                  [0.5, 1, 0.05, 0.15, 0, 0, 0],
                                  [0.3, 0, 0, 0, 0.1, 0.1, 0],
                                  [0, 0, 0.3, 0, 0.1, 0, 0],
                                  [0, 0, 0.25, 0.1, 0, 0.1, 0],
                                  [0, 0, 0.25, 0, 0.1, 0, 0],
                                  [0.2, 0, 0.05, 0.6, 0.55, 0.65, 1]], dtype=np.float32))
distribution = Variable(np.array([[10, 5, 1, 3, 2, 2, 0]], dtype=np.float32))
print markov_chain.transit_until_limit_distribution(distribution)
markov_chain.cleargrads()
limit_distribution, limit_effect = markov_chain.transit_until_limit_distribution(x)

The post Chainerを使ってMarkov chain(マルコフ連鎖)を書いてみる first appeared on 株式会社Altus-Five.

]]>
/blog/2017/06/30/chainer/feed/ 0
オンライン機械学習時代の到来が技術者にもたらすもの /blog/2017/03/30/online-learning/ /blog/2017/03/30/online-learning/#respond Wed, 29 Mar 2017 17:26:00 +0000 http://43.207.2.176/?p=414 弊社の機械学習への取り組み – VOCシステムの構築において VOCとはVoice Of Customersの略称で、日本語訳は「お客様の声」というありふれたものです。 VOCは一種のビッグデータであり、自然 […]

The post オンライン機械学習時代の到来が技術者にもたらすもの first appeared on 株式会社Altus-Five.

]]>
弊社の機械学習への取り組み – VOCシステムの構築において

VOCとはVoice Of Customersの略称で、日本語訳は「お客様の声」というありふれたものです。

VOCは一種のビッグデータであり、自然言語処理にかけることで定量化が可能です。

弊社では、Pythonの自然言語処理プラットフォームであるNatural Language Toolkit(NLTK)や、独自手法による自然言語処理を組み込み、以下を実現するシステムを開発しました。

  • 自動的に製品と風評情報を紐付ける
  • レビュー内容の自動分類

また、自然言語処理については、WebサイトのRSSフィードから記事カテゴリを判別するという用途も有名です。RSSフィードには記事の要約が含まれています。この要約文章を定量化することで、機械学習手法による自動分類が可能になります。

このように、機械学習によって取り組むことのできる領域は大きくなっており、ライブラリの充実によって、用途次第では現実的なコストで開発ができるようになりました。

過去の記事でも述べた通り、弊社のスタッフは自然言語処理ライブラリの内部処理を決めるアルゴリズムや、そのバックボーンにある数学モデルの理解は雑学程度です。あくまでユーザとしてこれらのツールを選別し、使いこなすための努力を都度行います。

とはいえ、機械学習界隈の動向はある程度注視しています。機能を実現できる技術の存在を知らなければ選択肢にも上りませんし、ライブラリの中身(ソースコード)を見て挙動を読み解く際、機械学習理論に関する知識があれば有利です。

そんな折、「オンライン学習」という機械学習の分野があることを知りました。

バッチ学習とオンライン学習

「オンライン」という言葉からは、「コンピュータがネットワークと接続されている状態」が自然と連想されます。しかし、この文脈での「オンライン」はそういう意味ではないようです。 ここでの「オンライン」は、「バッチ(batch)」の対義語として用いられています。「バッチ処理」といった文脈でよく接する、あのバッチです。

「オンライン学習」と「バッチ学習」を隔てるのは、データに関する制約の有無です。バッチ学習の枠組みは、学習に必要なデータは、学習開始時から終了時まですべて手元に揃っていることを前提にしています。その前提の下で精度を高めたり、適切なパラメータに高速に収束させるためのモデル改良を行います。

一方、オンライン学習の場合、データは学習の最中にも到着します。また、全てのデータを学習終了時まで手元に置いておくことが要求されず、学習に使ったデータをすぐに捨てることもできます。

この性質が実務的なメリットをもたらすのは、例えば以下のような場合です。

  • 非常に大きなデータから学習する必要があり、データ全体はメモリに入りきらない場合
  • 最新情報の逐次入力によって精度を上げることが要求される場合(ex. 天気予報システム、渋滞予測システム)
  • 個人情報に基づく学習を行っており、データを手元に保管することにリスクがある場合(ex. メールのスパム・非スパム分類)

言い換えれば、「データの到着が逐次的(sequential)であること」が解析に含まれているような機械学習手法を、オンライン学習と呼ぶようです。

いくつか、主要な概念やアルゴリズムについてまとめていきます。

Follow-the-Leader(FTL)アルゴリズム

オンライン学習においてもっとも単純なアルゴリズムとして知られるのがFollow the Leader(FTL)です。FTLは、現段階よりも早く到達したデータ(過去の全データ)を最もよく説明するようにパラメータを決める手法です。これを端的に「Follow the Leader」(先んじる者に従う)という言葉で表している訳ですね。

FTLはオンライン学習を実現する素朴なアルゴリズムとして知られており、実際には過学習を防ぐための正則化項を加えた改良版であるFollow-the-Regularized-Leader(FoReL)がよく使われているようです。

後悔最小化(Regrets minimization) / リグレット解析(Regrets analysis)

オンライン学習手法の構築や、その解析においては、「後悔(Regret)」という概念が用いられるようです。

自動運転車を例に、リグレット解析の概念的説明を試みる

上記の挿絵においては、自動運転車の学習プロセスをイメージして、リグレット解析の概念を紹介しています。

(残念なことに、Googleなど多くの企業が2017年初頭に完全自動運転車の開発を断念してしまいましたが、あくまでここではイメージとして用いていますので、悪しからず。)

ここで、車はθθというパラメータ(ベクトル)に基づいて動作しています。オンライン学習においてはデータが到着する度にパラメータを更新していきますが、この例では、自動車が運転の最中に学習を行っていることでオンライン性を表現しています。

運転データを用いた訓練によって、自動車は学習を行い、パラメータを調整していきます。その過程において生じた「望ましくない出来事」を損失関数(loss function)を用いて定義し、それらの集計値(たとえば総和)の最小化を目指します。

それに対し、θ∗θ∗という最適パラメータがあり、それを用いて最初から最後まで車に運転させた場合を考えましょう。このようなパラメータは通常「神のみぞ知る」値であり、学習開始時点では(あるいは学習終了時点であっても)手にすることは出来ないことに注意しましょう。

通常は、θ∗θ∗の上で得られる損失は、学習過程の損失よりも小さなものになります。両者の差がどの程度小さいかによって学習アルゴリズムの良し悪しを図るのが、リグレット解析のアイデアです。

ここで、$ \theta^ の下で得られる損失をの下で得られる損失を L^ 、学習過程において得られた損失の差を、学習過程において得られた損失の差を L とするとき、とするとき、 L – L^* $の値を後悔と呼びます。

この値は通常、プラスとなります。後悔の値をなるべく小さくなるようモデリングするのが「後悔最小化」というフレームワークです。

後悔の値は、優れたモデルを使うほどに小さくなります。反対に、最初から最適パラメータθ∗θ∗を用いることが出来ていたら…という後悔は、学習過程における損失が大きいほど強まります。

予測におけるリグレット解析

オンライン学習ではデータの到着に従い、学習によって少しずつパラメータを調節していきます。なので、最初のうちはあまり精度が良くないとか、一時的に生じたトレンドにパラメータが引っ張られて全体傾向を捉え損ねるといったことが起こります。

一方、データが全て明らかになっている学習終了時点を考え、そこからデータを説明する「最適パラメータ」を引き出せば、都度パラメータを調整するオンライン学習の結果より良い結果になると言えそうです。

  1. 最適パラメータがもたらす、過去のデータに対する予測結果
  2. オンライン学習アルゴリズムがパラメータを都度調整して得た予測結果

基本的に、1.は2.よりも良いものとなります。2.の結果が1.の結果に対してどの程度「悪くないか」数値化したものを後悔と呼び、それを最小化しようとする枠組みを後悔最小化と呼びます。

ここで、「最適パラメータ」はもちろん、実際にデータが到着するまでわからないということに注意が必要です。(このことから、最適パラメータを「後知恵」(hindsight)と呼ぶ文献もあります。)

オンライン学習の応用分野

アカデミックを中心に、様々な実験がなされているようです。

ユーザー投稿(User-Generated Content)のスパム判定

Yahoo!のような膨大な数の投稿をユーザーから受信するサイトにおいて、最新情報を基にスパム判定するための枠組みが研究されています。「静的なスパム判定ロジックは攻撃者に容易に潜り抜けられてしまう」ことから、機械学習によるスパム判定は今後不可欠になると言われています。 このような用途において、オンライン学習は以下の特徴を持っています。

  • 過去の投稿を手元に置くことなく、最新投稿のみから学習することが出来る
  • 日々変化するスパムのパターンには、「一括学習・適用」というバッチ学習の枠組みでは対応できない。最新情報を基に学習し続けるオンライン学習であれば対応出来る

下記のオンライン学習手法の研究論文では、従来の機械学習手法と同様に、担当者が「スパム/非スパム」でラベリングしたデータを用いて訓練を行っています。

スポンサードリンク表示内容決定のための、より高精度なCTR(クリック率)推定

Googleのスポンサードリンクは、検索クエリ(検索キーワード)に基づいて複数の広告を表示します。表示される広告を決定するシステムは、広告の掲載順位とスポンサーへの課金額も同時に決定しています。 これらを決定する基準として用いられるのが、広告のCTR(Click-Through-Rate、クリック率)です。よって、システム内でCTRを求める必要がありますが、キーワードの数が膨大であることから、ほとんどの広告にはキーワードに紐付いた出力実績・クリック実績がありません。このことから、実績のない広告に対する高精度の推定が必要とされています。

実は、Googleにおけるスポンサードリンクの精緻化というテーマにおいて、オンライン学習アルゴリズムが実験で良いパフォーマンスを上げているというのです。

新規手法として提案されたオンライン学習に基づくCTR予測システムは、新しいクリック(もしくは新しい未クリック)を基に、1日あたり何十億回も予測内容の更新を行います。そのようなシステムは最新の趨勢の下でCTR予測が出来る訳ですから、予測精度が上がるであろうことは素人にも想像できますね。

なお下記の参考文献において、予測の手法自体は、ロジスティック回帰というよく知られた方法が用いられています。

既存ライブラリについて

オンライン学習がアカデミックにおいて注目されるにつれ、既存の機械学習ライブラリにもオンライン学習のための機能が追加され始めているようです。

また、オンライン学習を目的としたライブラリの開発もなされていますので、これらをまとめていきます。

Vowpal Wabbit

大規模データ解析のための効率的なアルゴリズムを提供しているライブラリで、オンライン学習がその中核をなしています。

C++ライブラリのBOOSTを用いています。

LIBOL – A Library for Online Learning Algorithms

オンライン学習を目的として開発されたライブラリで、二値分類や多値分類が代表的なユースケースです。

内蔵されているアルゴリズムについてマニュアルに記載があり、情報がよく整理されています。

MATLAB/Octaveインタフェースです。

Hivemall

Hadoop上で動作する機械学習ライブラリです。既にオンライン学習用の機能が追加されており、RDB上のトランザクションデータからオンライン学習し、予測が行えます。

Jubatus

オンライン学習を目的として開発されたライブラリで、SparkやPython/scikit-learnとの連携動作が可能です。

オンライン学習が機械学習のスタンダードになる日も近い?

オンライン学習は、単一のストレージにはとても保管しきれないようなビッグデータに対し、その全体を保管せず逐次的に処理するというアプローチを可能にしてくれます。

このことからオンライン学習は、Hadoopのような分散処理技術による(バッチ)機械学習と対置されることもあります。

これは、両者に排他性があるという意味ではありません(むしろオンライン学習は並列計算と相性がいいと言われています)。

従前は分散処理技術を援用しなくては分析が不可能だった巨大なデータに対し、オンライン学習ライブラリを使えば単一マシン上での処理もできるという示唆なのです。

つまり、エンジニアからみれば、ビッグデータに対する解析方法の選択肢が新たに生まれつつあるということになります。

システムの全体要件に合わせて柔軟に実装方法を変えられるということですから、有難い話であるように思います。

将来的にはオンライン学習の枠組みが汎用化し、リアルタイム性の高い事象や、膨大なデータの解析が手軽に出来る時代が来るのかもしれません。 そのような時代の到来は、今よりも多くの技術者に、機械学習に取り組むチャンスをもたらすかもしれませんね。

参考文献

The post オンライン機械学習時代の到来が技術者にもたらすもの first appeared on 株式会社Altus-Five.

]]>
/blog/2017/03/30/online-learning/feed/ 0
オンライン広告をどのようにプロモーションする? – 劣モジュラ性と局所探索で解決 /blog/2017/01/06/submodular2/ /blog/2017/01/06/submodular2/#respond Fri, 06 Jan 2017 10:10:00 +0000 http://43.207.2.176/?p=251 前回の記事でソーシャルマーケティングの例であるオンライン広告問題の定義と劣モジュラ性の関連について説明しました。今回はオンライン広告問題において、どのようにインフルエンサーを見つけ、プロモーションを行っていくのかを紹介し […]

The post オンライン広告をどのようにプロモーションする? – 劣モジュラ性と局所探索で解決 first appeared on 株式会社Altus-Five.

]]>
前回の記事でソーシャルマーケティングの例であるオンライン広告問題の定義と劣モジュラ性の関連について説明しました。今回はオンライン広告問題において、どのようにインフルエンサーを見つけ、プロモーションを行っていくのかを紹介したいと思います。

Influence戦略:インフルエンサーへのアプローチ

Microsoft研究所のスライドで提案されている戦略は「Influence & Exploit Strategies」と呼ばれ、以下の2ステップからなります。

  1. Influence:影響力のあるユーザーの集合Aに無料で商品を提供する。
  2. Exploit:残りのユーザーをランダムに訪問し、最適価格を提示する。

これは記事冒頭の、以下の仮定に対応しています:

プロモーションは「無料キャンペーン」と「定価販売(2,000円)」の2フェーズに分ける。

「最適価格」というところを「定価」と読み替えていますが、このようにすると元の問題の特殊ケースとなります。
今回はExploit戦略については触れず、Influence戦略について説明をしていきます。

局所探索(local search)でキャンペーン対象を決定

そもそも、クライアントはなぜ「無料キャンペーン」という戦略をはじめに取りたがっているのでしょうか。
それは無料配布・口コミにより商品認知度を高めることで、後続の定価販売(2,000円)の際に、ユーザーの購入確率を高めるためと考えられます。

無料配布を行うことには、プラス、マイナス両方向の要因が存在します。

  • ユーザーに商品を無料提供することは機会損失と考えられ、定価で買ってくれた場合に比べて2,000円分の利益を失う(マイナスの要因)
  • 商品を提供したユーザーはハッシュタグ付ツイートによるプロモーションを行うので、フォロワーの購買確率を高めてくれる(プラスの要因)

ここで、キャンペーン対象にユーザーを一人追加することの、プラス要因とマイナス要因の差額を便益(Benefit)と定義します。

これから説明する局所探索とは、上述した便益が正である限り、ユーザーを一人ずつ無料キャンペーン対象に追加(または削除)するというものです。
今回は簡単のため、無料配布対象のユーザーは必ず商品を購入し、またハッシュタグ付のツイートをしてくれるものと仮定すると、局所探索の疑似コードは以下のようになります:

users = {}  # 広告の出稿対象であるn人のSNSユーザー
price = 2000  # 商品の定価


users['A'] = {  # ユーザーのフォロワーとフォロー先を格納する連想配列。ここではAのみ定義
  'followers':  ['B', 'I', 'G', 'E', 'F'],
  'followings': ['B', 'E']
}

# 他のユーザーのfollowers, followingsも同様に定義

# フォロー先ユーザーのキャンペーン対象者数によって決まる購入確率
purchase_prob = [0.005, 0.010, 0.0115, 0.0118, 0.01185, 0.0119]


def campaign_obj():  # 無料キャンペーン対象者を決める関数を実行
  target_users = []  # 対象ユーザー (返り値、最初は空)
  while True:  # 貪欲法によって便益が増加しなくなったら抜けるループ
    user_with_max_benefit = None  # 最大の便益を与えるユーザー
    max_benefit = 0.000  # 最大の便益値
    for i in set(users)-set(target_users):  # 新規キャンペーン対象としてiを取る
      revenue = 0.000  # iを無料キャンペーン対象に加えることで得られる期待利益額増分の総和
      # iのフォロワーjを取る
      for j in set(users[i]['followers'])-set(target_users):
        # target_usersにiを加えた集合
        new_target_users = set(target_users) | set(i)
        # jがフォローしているキャンペーン対象ターゲット(iを加える前)
        following_target_before = \
          set(target_users) & set(users[j]['followings'])
        # jがフォローしているキャンペーン対象ターゲット(iを加えた後)
        following_target_after = \
          new_target_users & set(users[j]['followings'])
        # iを加える前のjの購入確率
        prob_before = purchase_prob[len(following_target_before)]
        # iを加えた後のjの購入確率
        prob_after = purchase_prob[len(following_target_after)]
        # jの購入確率の増分
        prob_increase = prob_after - prob_before
        # jに関する期待利益額増分 = 定価 * jの購入確率の増分
        revenue += price * prob_increase
    # 機会損失額 = 定価 * 定価でも買ってくれた確率
    cost = price * purchase_prob(set(target_users))
    benefit = revenue - cost  # 便益は、利益から機会損失額を引いて求める

    if benefit > max_benefit:
      user_with_max_benefit = i  # ユーザーiをuser_with_max_benefitにセット
      max_benefit = benefit

    if max_benefit > 0:
      # target_usersにuser_with_max_benefitを加える
      target_users = set(target_users) | set(user_with_max_benefit)
    else:
      break

  return list(target_users)

ただしここでは簡略化のため、「ユーザーの追加」のみを考えた局所探索のコードを掲載しています。「ユーザーの削除」も考慮したコードは、for文の中身を少し書き換えることで実現できます。

局所探索の動きの図示

疑似コードだけでは分かりづらいので、局所探索の様子を図示してみましょう。
まず、手続きをステップ分けすると以下のようになります。

  1. まだキャンペーン対象でないユーザーiを取り、ユーザーiを加えることによる便益を計算
  2. 1.の計算を元に、最大便益をもたらすユーザーを特定
  3. 最大便益が正であるならばユーザーを出力リストSに入れ、1.に戻る。0以下であるならば停止する

実際の局所探索のステップを例を用いて説明します。

上図には、ステップ1.の開始時として誰も無料キャンペーンの対象ではない状態と、その場合の定価販売開始時の購入確率を示しました。すべてのユーザーが0.50%という確率で購入してくれることが分かります。

上図はAさんを無料キャンペーン対象とする時の購入確率を示しています。AさんをフォローしているBさん、Eさん、Fさん、Gさんの購入確率が1.00%に高まっています。
一方で、Aさんには商品を無料配布してしまったので、定価販売の広告対象からは外れてしまいます。これによって得られる便益を計算してみましょう:

上式は、疑似コードで行われる計算と同じ内容を、一本の数式で表しています。一項目はAさんへの商品提供による、Aさんのフォロワーへの正の効果、二項目は機会損失による負の効果を表し、結果として得られる便益は期待値で40円相当であることが分かりました。

実は、最初に実行するステップ1.では必ず、フォロワー数最大のユーザーを加えた時に便益最大となります。差し当たり、Aさんを無料キャンペーンの対象に入れることとします。

そして、次に加えるユーザーとして、2番目のインフルエンサーだったBさんを加えた時の購入確率がこちらです。Aさん、Bさんの両方をフォローしているユーザーについて、購入確率が1.15%に増加していることが分かります。
購入確率の劣モジュラ性により、先ほどよりも上がり幅が小さくなっていることが分かります。

上式は、Bさんを加えることの便益がマイナスであることを示しています。フォロワーに与えるプラスの影響を、Bさんに商品を定価で売れないことの機会損失額が上回ってしまったのですね。
実は、Bさんのフォロワーの集合は、Aさんのフォロワーの集合(と、Aさん自身)によって含まれています。購買確率が劣モジュラ性を満たし、逓減性を持つことから、このようなBさんに無料配布するメリットは小さいのですね。

上図はユーザーの追加・削除を行う局所探索の結果を表しています。よりフォロワー数の多いBさんではなく、GさんやKさんといったユーザーが無料キャンペーン対象となっているところに、劣モジュラ性の特徴が現れています。

この状態から定価販売を始めることで、無料キャンペーンを行わずに広告を出稿した場合に比べ、期待される利益額が26円上昇するという結果が得られます。このアルゴリズムは、購入確率の関数が劣モジュラ性を満たす場合、最適解と比べて40%の期待利益額増加を保証してくれます。(上記の例では最適解を出力しています。)

今回はフォロワー数が高々5人というとても小さなネットワークでしたので少額の結果となりましたが、強力なインフルエンサーの居る現実に近いネットワークであれば、利益額が数万、数十万単位で変わってくることも考えられます。

より現実に近いモデリングを目指して – 劣モジュラ性の数学的定義を学ぶ

劣モジュラ性を満たす購入確率関数を持つソーシャルネットワークに対しては、局所探索アルゴリズムを用いることで効果的に商品の知名度を上げることが出来ると分かりました。ただ、今回のモデルはそのまま実用するには問題があります:

  • 商品の原価や広告出稿費用を考えていない
  • 無料キャンペーン対象のユーザーは必ず商品を購入し、ハッシュタグ付のツイートで宣伝してくれるという仮定をしていた

上記の前提を取り除くためには、モデルに対する理解と、いくらか確率に関する知識を必要とします。

本記事では、バイラルマーケティングにおける劣モジュラ性の応用について解説しました。このように、機械学習分野において劣モジュラ性は注目されており、コンピュータビジョンの分野でも使用されています。

今回はソーシャルマーケティングのシステム設計を例に取り、「フォロー先のハッシュタグ付ツイート人数によって購買確率が決まる」という単純な場合を考えてましたが、「購入確率に及ぼす効果が相手のフォロワー数によって変動する」とか、「女性のツイートの方が効果が大きい」といった細かい設定を付与したものも、劣モジュラ性を満たします。そのような、より現実に近いモデルを構築するには劣モジュラの数学的定義を知る必要がありますので、興味を持たれた方は以下の書籍などを参考に学習することをお勧めします。

「劣モジュラ最適化と機械学習」:河原吉伸、永野清仁

尚、上記の疑似コードを、 Python で実装したものを、gist にアップしてあります。興味のある方は、そちらも、ご参照ください。

https://gist.github.com/msato-ok/40038aadf931c8cd28feceb2c0e2552e

The post オンライン広告をどのようにプロモーションする? – 劣モジュラ性と局所探索で解決 first appeared on 株式会社Altus-Five.

]]>
/blog/2017/01/06/submodular2/feed/ 0
ソーシャルマーケティングシステムを単純なアルゴリズムで実装する – 劣モジュラ最適化と貪欲法 /blog/2016/12/28/submodular1/ /blog/2016/12/28/submodular1/#respond Wed, 28 Dec 2016 10:06:00 +0000 http://43.207.2.176/?p=244 ソーシャルマーケティングシステムの設計と機械学習 FacebookやTwitterなどのソーシャルメディアは、情報の拡散を劇的に早めました。国際情勢からゴシップ、専門的な情報から個人の日常生活に至るまで、あらゆる情報が流 […]

The post ソーシャルマーケティングシステムを単純なアルゴリズムで実装する – 劣モジュラ最適化と貪欲法 first appeared on 株式会社Altus-Five.

]]>
ソーシャルマーケティングシステムの設計と機械学習

FacebookやTwitterなどのソーシャルメディアは、情報の拡散を劇的に早めました。国際情勢からゴシップ、専門的な情報から個人の日常生活に至るまで、あらゆる情報が流れ込み、ユーザーの手によって評価・拡散されています。 その爆発的な拡散速度に魅力を感じる主体が多いせいか、ソーシャルメディアを介してプロモーションを打つという意思決定が随分とメジャーになりました。

バイラルマーケティング(viral marketing)という言葉があるようです。 口コミを利用して、新商品などの情報を拡散していきユーザーを獲得するマーケティング手法のことで、バイラルはvirus、つまりウイルスを語源とする言葉です。

情報拡散をめざす上では、誰に広告を出稿するかという問題があります。その対象や順序によって、広告の効果が断然ちがってくるのは想像に難くありません。 そこで、広告出稿対象を機械学習で選定するシステムなどあるのかなと調べてみたところ、面白そうな文献を見つけました。

以降ではマイクロソフト研究所のVahab Mirrokni氏が行った研究報告、 「Online Advertisement and Submodular Maximization」に登場する「オンライン広告問題」を単純化したものを紹介し、それを解くプログラミング技法を紹介していきます。
http://research.microsoft.com/en-us/um/beijing/events/theoryWorkshop08/Mirrokni.pdf

この文献は、広告システム設計者の立場で、商品プロモーションによるクライアントの売上最大化を目指しています。 文献を翻案すると、ざっくりとした要件は以下のように定義できます。

  • 出稿対象アカウントを選定し、対象者のタイムラインに商品購入のプロモーションツイートを表示
  • プロモーションは「無料キャンペーン」と「定価販売(2,000円)」の2フェーズに分ける。
    • 無料キャンペーンについては一括プロモーションをかける
  • 商品購入者にはハッシュタグ付のツイートを促す
    • 今回は簡単化のため、購入者全員がツイートしてくれるものとする
  • 広告の出稿費用はかからないが、各ユーザーには一回しか広告を出せない

劣モジュラ最適化と離散数学

“Submodular Maximization”という報告の題目には、「劣モジュラ最大化」という訳語があるようです。

劣モジュラ最大化は、より広く”discrete optimization”(離散最適化問題)に分類されるようなので、 まず離散最適化問題がどんなものかを理解する必要がありました。

離散最適化問題は、「1、2、3…」と数えられる対象があって、そこから最適な組合せを見つける問題のことです。 バイラルマーケティングにおいて情報を広めるのは人ですが、人は1人、2人…というのが最小単位ですから、「誰に広告を出すか」という選択は離散的なものになるのです。 なお、離散の対義語は「連続」で、義務教育で学ぶ数学の関数はほとんどが連続関数です(ex. 2次関数、三角関数)。 離散量と連続量の簡単な例としては、以下が挙げられます。

シーン離散量の例連続量の例
Webサイト分析ユーザーが閲覧したページ数ページあたりの滞在時間
身体測定反復横跳びの回数50m走の記録
食事量食べたトマトの数飲んだ味噌汁の量

連続量も、現実には離散化してサンプリングすることがほとんどですが、解析上は実数全体を取るものとして扱い、微分積分などを駆使してモデリングを行います。

離散最適化問題を解くには、離散数学の知識が必要です。 この記事はその入門に適するよう、集合や組合せに関する基本的な知識のみで読めるように書きました。また、離散数学は配列やキュー、スタック、ヒープといったデータ構造とも関係する概念ですので、エンジニアが接する数学としてはお勧めです。 劣モジュラ性や、この記事で紹介する貪欲法は、概念を理解すればすぐコードに落とし込み、最適化を手軽に行うことが出来る分野です。
早速、始めてみましょう。

グラフ – 離散最適化の必需品

TwitterやFacebookなどのソーシャルネットワークにおけるユーザー同士のつながりは、グラフを用いて表すことができます。

ここでグラフとは、下図のような頂点(vertex)と枝(edge)からなる抽象的概念であり、データ構造の一種です。データを図示するためのグラフとは異なるので注意してください。

上図では、7人からなる小さなFacebookネットワークを図示しました。頂点は人を表し、枝は友達であることを表します。

Dさんに着目して考えてみましょう。DさんはBさん,Cさん,Eさん,Fさんと友達です。 しかし、Gさんとは友達ではありません。

グラフをデータとして保持する方法の一つは、上図にある隣接リスト(adjacent list)を記憶させることです。配列などで各頂点の隣接頂点を覚えさせておけば、とりあえずデータとして保持することは出来ますね(高速化のためにはさらなる工夫が必要ですが)。

また、Twitterのフォロー関係のように、情報が一方向にのみ流れる場合も考えられます。 そのような状況は有向グラフ(directed graph)を用いて表すことができます。

オンライン広告問題を定義し、解く

それでは、クライアントの売上最大化問題を定義し、プログラミングによって解く方法を考えてみましょう。
まず、ユーザーのハッシュタグ付ツイートは、フォロワーに対する商品認知度を高める効果があると仮定します。

このことを、グラフを用いて考えてみましょう。ここでは枝が情報の流れを表すとし、有向グラフを用います(Twitterのフォロー・フォロワー関係とは、枝の向きが逆であることに注意してください)。
なお、双方向に情報が流れている二者については、便宜的に矢印の無い枝一本で表します。
上図のGさんに着目すると、Dさん、Eさん、Hさんの3人に情報を流していますが、AさんやLさんには流していません。

このようなネットワークの中には多数のユーザーに影響を与える情報提供者、いわゆるインフルエンサーがいる可能性があり、利益最大化のためにはそのようなユーザーにアプローチするのが有効と考えられます。

誰がインフルエンサーなのか – 出次数(outdegree)

上図では、AさんやBさんがインフルエンサーであることが直感的に分かりますね。
ここで、その直感を頂点の出次数(outdegree)によって根拠付けることができます。頂点 AA の出次数 o(A)o(A) とは、その頂点から出て行く枝の本数のことです。
これはTwitterのネットワークで言えば、フォロワー数に対応する概念だと解釈することが出来ます。(ここで枝は情報の流れる方向を図示するため、通常のフォロー・フォロワーのイメージとは逆向きであることに注意してください)
グラフ中の出次数の大きな頂点を探すと、 o(A)=5o(A)=5, o(B)=4o(B)=4 であり、他のどの頂点 XX についても o(X)<4o(X)<4 が成り立ちますから、Aさんが最大のインフルエンサー、Bさんが2番目のインフルエンサーと言えそうです。

商品のプロモーション対象をシステム上で最適化する

ではここで、「システム設計者」という立場に戻ります。
クライアントの利益最大化のためには、インフルエンサーに積極的にプロモーションをかけ、多数のユーザーにハッシュタグ付ツイートを拡散してもらうのが良さそうです。しかし、その事実をどうやってシステムに反映すれば良いでしょうか。

ここで問題を特徴付ける重要な仮定を置きます。広告の配信先となるユーザーについては、それぞれの商品認知度に基づき、広告を配信した際の購買確率が計算出来るとします。
これをどうやって計算するかはまた別の(統計的な)問題となりえますが、ひとまず数値は与えられるものとします。

上図ではDさんの購買確率に着目しています。ここでは単純化のため、Dさんの購買確率は、彼がフォローしているAさん・Bさん・Cさんが商品に関するハッシュタグ付のツイートをした事があるか否かで決まるものとします。
上図では、Dさんがフォローしている人のうち、ハッシュタグ付ツイートをした事のある人数が増えていく毎に、Dさんの購買確率が大きくなることを示しています。ここに、本記事の主題である劣モジュラ性が隠れています。

購買確率の劣モジュラ性 – 最適化と相性の良い関数型

上図では、周りの誰も商品についてツイートしていない時、Dさんの購買確率は0.50%でした。
1人がツイートしていると1.00%、2人だと1.15%、3人だと1.18%…と上昇しています。
ここで、購買確率の増加幅に注目してみましょう。
t人がツイートしている時の購買確率を ptpt で表すと、増加幅は以下のようになります。

p1−p0=0.50p1−p0=0.50
p2−p1=0.15p2−p1=0.15
p3−p2=0.03p3−p2=0.03

だんだんと、増加幅が小さくなっていますね。このような性質を逓減性といいますが、人数について逓減性を持つものは、劣モジュラ性を満たす関数の典型例なのです。

劣モジュラ性の数学的な定義は、以下の書籍などを参考にしてください。今回はごく特殊なケースのみを取り上げます。

「劣モジュラ最適化と機械学習」:河原吉伸、永野清仁

逓減性を持つ関数は日常のいろいろなところに見られます。例えば宝くじの4等(50,000円)が当たった時の嬉しさは、預金1,000万円の人と、預金3万円の人を比べたら、後者の人の方が大きいと考えらえます。収入に対して感じる「嬉しさ」は、逓減性を持つということですね。

商品認知度についても、逓減性を満たすのは自然な仮定と言えます。例えば書籍について、周りに「よかった」と勧めてきた人数が0人の場合と、1人の場合とでは大きな隔たりがあります。1人と2人でもだいぶ違うと言えるでしょう。しかし、10人と11人とでは、さほど差がないと感じるのではないでしょうか。

購入確率は、実績値に基づき統計的に決めるのがベストですが、まずは逓減性を満たす仮の値を入れておくだけでも、後述のシステムを動かす事が可能です。

劣モジュラ性と貪欲法 – シンプルな近似アルゴリズム

上図では12人しかいない世界を仮定したので、無料キャンペーン対象の全組み合わせは 212=4096212=4096 通りしかありません。しかし、 220=1048576220=1048576 , 250≒1100250≒1100 兆というように、組み合わせ数は爆発的に増加します。この中で、どれが最適なキャンペーン対象の組み合わせかを計算するのは容易ではありません。

日本国内では、2015年12月時点で約3,500万人のアクティブユーザーがいると発表されました。となると、Twitter上の全組み合わせであれば、 235,000,000235,000,000 通りとなります。これだけの組み合わせを計算することは不可能でしょう。

ここで、近似的に良い組み合わせを見つけても、その組み合わせの質は悪いのではないか?と考えるかもしれません。しかし、目的関数が劣モジュラ性を満たしていれば、貪欲法というシンプルなアルゴリズムを用いるだけで、一定水準以上で解の質を保証するということが知られています。

例えば、「3,500万人のユーザーから最適な組み合わせを選ぶことで、100万円の利益が期待出来る」という状況があったとします。このような場合、最適な組み合わせは上述の事情によって「神のみぞ知る」組み合わせであり、人間が見つけ出すことは至難の業です。ですから、そのような組み合わせを見つけるのは諦めて、「100万円の利益が期待出来る組み合わせが存在するなら、少なくとも80万円の利益を期待出来る組み合わせを見つけてくる」といったことを目指します。このような保証のあるアルゴリズムを近似アルゴリズムと呼びます。
今回の例では、購買確率が劣モジュラ性を満たしていることにより、アルゴリズムに一定の性能保証が与えられます。

では、インフルエンサーをどのように発見し、プロモーションを行っていくのでしょうか?次回はインフルエンサーを発見するアルゴリズムやプロモーションの詳細を紹介いたします。

The post ソーシャルマーケティングシステムを単純なアルゴリズムで実装する – 劣モジュラ最適化と貪欲法 first appeared on 株式会社Altus-Five.

]]>
/blog/2016/12/28/submodular1/feed/ 0
機械学習モデルの汎化を妨げる過学習とは? – 旗揚げ画像の二値分類を例に /blog/2016/11/28/over-learning/ /blog/2016/11/28/over-learning/#respond Mon, 28 Nov 2016 09:57:00 +0000 http://43.207.2.176/?p=227 目的 この記事では機械学習に関連する以下のトピックについて解説し、機械学習を学んだことのない方が、汎化と過学習に関する観念的な理解を深めることを目指します。 観念的なわかりやすさを重視しているため、細部に誤りや不正確な点 […]

The post 機械学習モデルの汎化を妨げる過学習とは? – 旗揚げ画像の二値分類を例に first appeared on 株式会社Altus-Five.

]]>
目的

この記事では機械学習に関連する以下のトピックについて解説し、機械学習を学んだことのない方が、汎化と過学習に関する観念的な理解を深めることを目指します。

  • 予測モデルの汎化能力
  • 汎化能力の向上を妨げる過学習という現象

観念的なわかりやすさを重視しているため、細部に誤りや不正確な点があるかもしれません。
不適切な点がございましたら、ご教示頂ければ幸いです。

以下、この記事では画像の二値分類を例に取ります。
ですが、汎化と過学習に関する知識の適用範囲は画像認識の分野にとどまりません。

  • 予測モデルにおいては、データ誤差を抑えることが本質的な課題であり、過学習はその目標達成を妨げる
  • 実際に機械学習を行う際、適切なデータサイズやエポック数(学習回数)を決めるために過学習に関する知識が必要である

以上のことから、モデルの汎化能力と過学習に関する理解は機械学習について学ぶ上で十分に汎用的であり、初学者が理解を深める段階から実用レベルに至るまで、幅広く役に立ちます。

旗揚げ画像の二値分類

以降の議論では、画像の二値分類をテーマとし、

  • 汎化精度を上げるとはどういうことか?
  • 過学習とはどんな現象のことか?

を説明していきます。

ここでは二値分類の対象として、以下のような小さなサイズの「旗揚げ画像」を取り上げます。

受け付ける入力は8*8ピクセルで、情報がRGB形式で与えられる任意のものとします。 (よってこの記事に示す入力例よりも、遥かに多様な入力が考えられることに注意してください。)

図1のような画像に「赤を揚げている」「白を揚げている」というラベルを付与したものを訓練データとし、教師つき学習を行うこととします。

学習の目的は、未知の入力画像に対し、赤と白のどちらを揚げている画像かを適切に判定出来るようになることです。

補足:二値分類とは

二値分類とは、与えられたデータを、事前に与えられた二つのラベルに振り分けることを指します。

画像認識においては、以下のような例が有名です。

  • 男性か女性かを見分ける
  • 犬か猫かを見分ける

画像認識以外の二値分類として、以下のような例が有名です。

  • メールをスパムと非スパムに区別する
  • ある患者のデータから、特定の疾病を持つかどうかを区別する

二値分類における主流のモデル – 事後確率の計算

ここで取り上げている二値分類や、より多くのクラスに分ける他クラス分類においては、入力に対し「事後確率」を計算するのが主流の方法です。

ここで「事後確率」とは「データが与えられた後の確率」という意味であり、「ある特定のデータが、赤であるか白であるかについて、モデルによって推定された結果」であると解釈して頂いて問題ありません。学習途中においても、事後確率を算出することが可能です。

ここで機械学習モデルの中身についてはブラックボックスとしていますが、畳み込みニューラルネットワーク(CNN)が古典的なモデルとして有名です。

Convolutional Neural Networkとは何なのか

学習の枠組み – 最尤推定

各入力データに対して事後確率を求めるモデルでは、学習の各段階において尤度(likelihood)を求めることが出来ます。

尤度は、それぞれの入力データが持つ「正解のラベル」に対し、その時点のモデルが算出した事後確率の積を取ったものと考えられます。

ここでは二つの入力データに対する尤度を計算しており、0.72 = 72%という値が算出されています。

事後確率を求める機械学習モデルは、学習を繰り返すことで尤度を増加させるよう設計されています。

ここでは訓練データに対して尤度を高める過程を適合(fitting)といい、下に示す図5は学習の結果、訓練データに極めて適合した状態を模式的に表しています。

各入力に対し、正解のラベルに1に近い事後確率を割り振っていますね。

図5はいわば、学習によって、訓練データに対して百発百中の精度を持つようになった状態です。

これが望ましいことかどうかを、以下の段落で見ていきます。

未知の入力に対する推定 – 汎化能力

図4、5だけを見ると、訓練データへの適合度が高い図5の状態の方が望ましいように見えます。

しかし、このモデルの目的は「未知の入力画像に対し、赤と白のどちらを揚げている画像かを適切に判定出来るようになること」なのでした。

ですので、訓練データに含まれていない画像、すなわち「テストデータ」に対して推定を行ってみるべきでしょう。

図6は、図4のモデルに対して、 x′x′ という未知のテストデータを入力した結果を示しています(もちろん、実際のテストでは複数のテストデータを入力します)。

正解のラベル(白)に対して、赤よりも高い事後確率を割り振っていることがわかります。

このように、訓練データ以外のデータに対しても、正しく推定を行うことの出来る能力を汎化能力と呼びます。

この言葉を使えば、「機械学習モデルの精度を上げること=汎化能力を高めること」、と言うことが出来ます。

一方、訓練データに対しては百発百中だった図5のモデルはどうでしょうか。

図7は、図5のモデルに対してテストデータ x′x′ を入力したら、正解のラベル(白)よりも不正解のラベル(赤)に高い事後確率が割り振られてしまった状態を示しています。

これは機械学習が汎化能力をもつことの妨げとなる、過学習(overfitting)の問題を端的に表したものとなっています。

過学習(overfitting)とは

過学習が起こる原因は様々ありますが、その一つはデータサイズが不十分であり、データの持つ非本質的な「癖」まで学習してしまうことです。

例えばこれまでの図において、訓練データ x2x2 として登場していた旗揚げモデルの男の子を「Aくん」としてみましょう。

ここでAくんは複数の訓練データにおいて登場しているとします。

Aくんは赤色が好きなので、ついつい赤を揚げてしまうという「癖」があります。当然、これらの訓練データには赤のラベルが付与されています。

一方で、Aくんを映した訓練データには他にも特徴があります。

Aくんは背が小さいので、画像の上のほうに空白が空いてしまうのです。(ここで空白とは、ピクセル間のRGB値の変化が穏やかな領域のことと考え、またAくんの他に背の低い旗揚げモデルを映したデータは存在しないものとします。)

以上の議論をまとめると、今回使用した訓練データには、

「上のほうに空白が開いている画像は、赤を上げている」

という非本質的な「癖」があるといえます。

実は、図7のような出力がなされた理由は、機械学習モデルが入力データを入念に学習しすぎることよって、この特徴を覚え込んでしまったことだったのです。

その結果として、Aくんと同様に「背が低い」未知の入力 x′x′ に対して、赤を揚げているという誤った推論をしてしまったのですね。

以上は、データの偏りによる過学習について述べました。他には、実際の振る舞いに比べてモデルの自由度が高すぎること等が、過学習の原因として考えられます。

過学習を防ぐための代表的な手法

まず、十分なデータ数を用意することが重要です。

データを多様化しておくことで、先のように非本質的な癖を持つことを防ぐことが出来ます。

また、訓練データに対する誤差(二値分類においては対数尤度の総和)を訓練誤差と呼びますが、訓練誤差ばかりが小さくなり、テスト誤差が小さくならない(または大きくなっている)場合は、学習の効果が出ていないことになります。

上図は学習曲線(learning curve)と呼ばれるグラフで、エポック数(訓練データに対する学習回数)と誤差の関係を表しています。

(二値分類においては、対数尤度の総和にマイナスを付けた値を誤差として用いれば、尤度が1に近づくほど誤差が0に近づき、尤度が低くなると無限大に発散するような誤差関数を実現出来ます。)

上図で重要なのは、エポック数が増えるほど(訓練をすればするほど)訓練データに対する誤差は小さくなっているが、テストデータに対する誤差が次第に剥離し、やがて増大し始めているという点です。

このような時は学習を早期打ち切りすることによって、過学習を防ぐことが出来ます。

他に、モデルの自由度が高すぎることによる過学習を防ぐために、特にニューラルネットワークにおいて有用とされている手法として正則化(regularization)やドロップアウトがあります。これについては今後の記事で紹介していきます。

The post 機械学習モデルの汎化を妨げる過学習とは? – 旗揚げ画像の二値分類を例に first appeared on 株式会社Altus-Five.

]]>
/blog/2016/11/28/over-learning/feed/ 0
ニューラルネットワークの考え方とライブラリ /blog/2016/11/18/nnet/ /blog/2016/11/18/nnet/#respond Fri, 18 Nov 2016 09:51:00 +0000 http://43.207.2.176/?p=218 Google の翻訳機能 11月12日、GoogleはGoogle翻訳の日本語の機能のアップグレードを実施し、「精度が良すぎる」と大きく話題になりました。例えば、 From the Canadian Registry o […]

The post ニューラルネットワークの考え方とライブラリ first appeared on 株式会社Altus-Five.

]]>
Google の翻訳機能

11月12日、GoogleはGoogle翻訳の日本語の機能のアップグレードを実施し、「精度が良すぎる」と大きく話題になりました。
例えば、

From the Canadian Registry of patients with Upper Gastrointestinal Bleeding and Endoscopy (RUGBE), we determined clinical outcomes and explored the roles of endoscopic and pharmacologic therapies in a contemporary real-life setting.
(引用:http://www.nature.com/ajg/journal/v99/n7/abs/ajg2004245a.html)

こちらの文章をGoogle 翻訳にかけると

上消化管出血と内視鏡検査(RUGBE)患者のカナダ登録簿から、臨床結果を決定し、現代の現実の環境における内視鏡的および薬理学的療法の役割を探った。

と何とも素晴らしい翻訳結果になりました。

こちらはある論文のabstract部分なのですが、このぐらいしっかり書かれた文章(ブログなどの適当な文章ではなく)であれば、かなり正確な翻訳をしてくれます。

機能改善前は、思わず笑っちゃうような翻訳が多かったGoogle翻訳ですが、どうしてここまで精度が上がったのでしょうか?

それには、ニューラルネットワークという数学モデルの働きがあります。

ニューラルネットワーク

図のように、脳はある入力を受けると各神経細胞(ニューロン)へ信号を送り、その神経細胞は受け取った情報を処理すると、他の神経細胞へまたその情報を送ります。それらを繰り返し、最終的に出力(理解)を行います。
例えば翻訳の例で言うと、英語の文章の入力を得て日本語の文章を出力(理解)する。また、ある動物園のある写真を見て(入力)、この写真に写っている動物がライオンだと判断(出力)する、などもありますね。
ニューラルネットワークとは、このように人間が物事を判断する際の脳の神経伝達の働きを参考にして、落とし込んだ数学モデルです。

40年代から既に研究されていた分野ですが、近年ITによりデータ数が莫大に増加したことや、ニューラルネットワークの精度を上げる新しい手法に対する研究が進んだことで、近年大きく取り上げられ始めました。
特に前述したような言語処理や画像認識、音声処理の分野で注目が集まっています。

またニューラルネットワークは、順伝播型ニューラルネットワーク(FFNN)・畳み込みニューラルネットワーク(CNN)・再起型ニューラルネットワーク(RNN)に大きく分けられます。

基本のアイデアとなるのがFFNN、それを応用させたのがCNNとRNNという理解で良いと思います。
今回は基本のFFNNの考えについて簡単に説明し、最後にライブラリについて紹介するというところまでやっていきます。

FFNN

FFNN(順伝播型ニューラルネットワーク)のイメージ図を貼り付けます。

まず、ニューラルネットワークを用いるうえで必要な用語を簡単に勉強しましょう。
先程の脳で考えた、各神経細胞一つ一つを「ユニット」と呼びます。
そして、各段階のユニットの集まりを「層(layer)」と呼びます。

大きな括りで言うと、層から層へ流れ、最終的にある出力がなされ、細かく言うとその層から層への流れはユニット間の働きで決まります。

現在は入力から出力までの層は2つですが、この層の数は任意に決めることができます。

このように多層にすることも可能です。

またここで、図2のユニット間の繋がりを見て、隣接層のユニット同士が全て結合している状態(全結合)であることに注目してください。
これがニューラルネットワークの基本となるFFNN(順伝播型ニューラルネットワーク)です。

もし、全てのユニットが結合せず、下図のようであればFFNNとは呼ばず、これはCNN(畳み込みニューラルネットワーク)と呼びます。

今回は詳しく説明を行いませんが、CNNはその特性から画像認識の分野で大きく成果を上げている手法です。

FFNNのモデル

説明を簡略化するために下図のように簡単なモデルを考え、FFNNの解き方を説明していきます。

Step1:次の層へ信号を送る

まず入力 xx を受け取ると隣接する3つのユニットへ信号が送られます。
この時、各ユニットへは重み ww が掛けられ、その値は uu として保存されます。

u11=w11∗xu11=w11∗x

u12=w12∗xu21=w21∗x

u13=w13∗xu31=w31∗x

となります。

Step2:次の層へ送る出力を計算

受け取った入力( uu )から、次の層へ送る出力( zz )を計算します。

z11=f(u11)z11=f(u11)

z12=f(u12)z21=f(u21)

z13=f(u13)z31=f(u31)

この ff とは活性化関数と呼ばれる関数で、扱う人によって様々な関数系を用いることができます。
最近では,

f(u)=max(u,0)f(u)=max(u,0)

という関数系を使って推定を行う方が多いようです。

Step3:Step1とStep2の繰り返し

Step2で次の層へ送る出力を計算できたら、Step1と同様にその結果を次の層へ送る。
これを最後の層まで繰り返します。

誤差を最小にするパラメーターを求める

これまでで、どのようにFFNNが動いているかイメージがついたと思います。
途中で重みを掛けたり、活性化関数を通す処理を繰り返し、最終的に出力された yy が実際の出力とどれだけ差があるか(誤差)を計算します。

この誤差が小さければ小さいほど精度が良い、ということになります。

さらっと流した「重み」という概念ですが、とても大切で、この重みを調整することで、最終的な誤差を小さくすることができます。
「この重みの値を大きく、また小さく、今度はあの重みを大きく……」というふうに調整していきます。

そして最も誤差が小さくなるようにパラメーターが調節できれば、FFNNの働きは終了。

実際には、入力や出力がただの数値ではなく、音声だったり画像だったりしますし、重みパラメータの調節のためには、 難しいアルゴリズムが裏で実行することになりますが、根本的なニューラルネットワークのアイデアは、これでOKかと思います。

ニューラルネットワークを使えるライブラリ

裏側で動くアルゴリズムまで理解して実行することが理想ですが、その辺りはライブラリに頼る人が実際のところ多いそうです。 ということで、いくつか言語別にライブラリを紹介していきます。

機械学習を行う言語として有名なのが、ライブラリの豊富なPythonやR。

Pythonで使えるライブラリだと TensorflowやChainer、Theanoが有名です。

Rで使えるライブラリだと、nnetがあります。
この2つを使うのが無難かと思いますが、Rの場合は画像処理がしづらいので、画像処理まで扱いたいという場合はPythonを選択するのが良いでしょう。

ちなみにJava で書きたい、という方にはdeeplearning4j というライブラリがオススメです。顔/画像認識や音声検索、音声の文字化もできて、上記のニューラルネットワークもすぐに活用することができます。

RやPythonに比べて、参考になる資料が少ない印象を受けますが、チュートリアルがあるので、気になる方はそちらを見ながら deeplearning4j 進めてみてはいかがでしょうか?

弊社の取り組み

さて、弊社での機械学習やAIへの取り組みについて、ご紹介します。
弊社は、あくまで、システム屋なので、上記にあるようなライブラリをうまく使いこなして、 迅速にシステムへの導入を行うということが、役割となります。
これまでにも、機械学習を活用したレコメンドエンジンの導入や、クローラーと、 テキストマイニングを組み合わせた、クチコミの分析システムの開発などを、行ってきました。

機械学習系のライブラリの情報量は、例えば、Spring や Lalavel などと比較すると、 前者の方が、圧倒的に少ないので、使いこなすまでには、何度となく、ライブラリのソースコードの中を、 読むことにもなり、なかなか、歯ごたえのある仕事です。
そして、思ったほど、精度が出ないことが、ほとんどなので、そこから、試行錯誤が始まります。
試行錯誤するために、機械学習の仕組みを知っていた方が、有利なので、雑学程度の勉強も必要となります。

ライブラリを使いこなしたり、アイデアを捻出して試行錯誤を繰り返すので、 そういうことが、好きな人には、楽しい仕事だと思います。

ディープラーニングも、とても、興味深く見ています。
まだ、ディープラーニングを使ったシステムを開発したことはありませんが、 Chainer の研修には、社員の何人かは、参加していました。

いつか、ディープラーニングを使った、 chatbot など、作ってみたいです。

The post ニューラルネットワークの考え方とライブラリ first appeared on 株式会社Altus-Five.

]]>
/blog/2016/11/18/nnet/feed/ 0