January 2019

Volume 34 Number 1

[.NET]

確率的プログラミングを使用した機械学習

Yordan Zaykov | January 2019

プログラミングは、果たして確率的なのか。本当ですか。これには大した意味はありません。ただ、私がこの分野で仕事を始めたときから思っていたことです。私が注目していた研究者たちは、機械学習 (ML) の問題を分類する従来の考え方を持っていませんでした。彼らは、コンピューターで判読可能な形式でのデータの生成につながる実際のプロセスを表現していただけでした。それが、彼らがモデルと呼んでいるものとその表現、すなわち、確率的プログラムです。

パラダイムは、実は非常に魅力的です。第一に、そこから入手可能な何百もの ML アルゴリズムを学習する必要がありません。必要なのは、確率的プログラムで問題を表現する方法を学習することだけです。現実世界の不確実性をモデリングすることになるため、これには統計の知識がいくらか必要になります。たとえば、家の価格を予測するとします。それを決定するものは、いくつかの特長 (場所や規模など) の線形結合です。モデルは、価格 = 各機能とノイズが加味された重みの積和になります。これは、線形リグレッサーと呼ばれることがあります。抽象語では、次のようになります。

For each house:
  score = Weights * house.Features;
  house.Price = score + Noise;

トレーニング中に重みを学習してから、それらを予測で直接使用することができます。最後のノイズの多いスコアを 0 に対してしきい値設定することにより、モデルに小さな変更を加えた途端に、新しいラベルが 2 つのクラスのどちらかになります。それが何なのかも知らずに、バイナリ線形分類器をモデリングしたことになります。

第二に、問題とデータを既存の ML アルゴリズムのいずれかに当てはめる必要がありません。これは自明のことです。データに合わせて問題のモデルを設計したのですから。最新の確率的プログラミング ツールは、汎用の推論手法を使用して、自動的に、指定されたモデルから ML アルゴリズムを生成することができます。既に実装されているため、それに関する詳しい知識は必要ありません。つまり、汎用の推論手法とアプリケーション固有のモデルを組み合わせることにより、アプリケーション固有の ML アルゴリズムが手に入ります。

最後に、このアプローチは時を経ても変わることはありません。成功するほとんどの ML アルゴリズムがこの枠組み (一連の前提を表現したモデル + 計算を行う推論手法) に収まります。方法論は時間の経過と共に進化しています。最近は、ディープ ニューラル ネットワークが注目を集めています。モデルはしきい値設定された線形関数で構成され、推論手法は確率的勾配降下法 (SGD) と呼ばれています。ネットワークの構造を回帰または畳み込みに変更する、つまり、モデルを変更することにより、別のアプリケーションを対象にすることができます。ただし、図 1 に示すように、推論手法は SGD のまま変える必要がありません。そのため、モデルの選択肢は時間の経過と共に進化しますが、モデルを設計して推論手法を適用する一般的なアプローチは変わりません。

アプリケーションごとに異なるモデル、すべてで同じ推論手法が使用されている
図 1 アプリケーションごとに異なるモデル、すべてで同じ推論手法が使用されている

したがって、開発者の仕事は、アプリケーション用のモデルを考案することです。このやり方を学習しましょう。

ようこそ、不確実な世界へ!

すべての人が新しい言語で作成する最初のプログラムが "Hello world" です。 確率的セットアップにおける同等のものは、もちろん、"Hello uncertain world" です。 確率的プログラムは、シミュレーションまたはデータ サンプラーと見なすことができます。いくつかのパラメーターを紹介し、それらを使用してデータを生成します。たとえば、まったく不明な 2 つの文字列があるとします。つまり、任意の文字列です。もっと統計的に表現すれば、一様分布から抽出された文字列ランダム変数です。それらの中心をスペースで区切って連結し、その結果が "Hello, uncertain world" という文字列になるように制限します。

str1 = String.Uniform()
str2 = String.Uniform()
Constrain(str1 + " " + str2 == "Hello, uncertain world")
Print(str1)
Print(str2)

これが確率的モデルです。データの生成方法の前提を提示したわけです。これで、必要な計算を行う特定の推論手法を実行することができます。2 つの文字列間のスペースは、2 つのスペース ("Hello" と "uncertain" の間または "uncertain" と "world" の間) のどちらにすることもできます。 そのため、2 つの変数のどちらかの値を確定することができません。したがって、結果は、可能性のある値の不確実性を収集した分布になります。str1 は同じ確率で "Hello" または "Hello uncertain" になり、str2 は 50% で "uncertain world" になり 50% で "world" になります。

実際の例

より現実的な例に移りましょう。確率的プログラミングは、膨大な範囲の ML 問題を解決するために使用できます。たとえば、私のチームは、何年か前に推奨システムを開発して、Azure Machine Learning で出荷しました。その前は、Exchange の電子メール分類器を製品化しました。今は、能力評価システムをアップグレードすることにより、Xbox でのプレイヤー マッチメイキングの改善に取り組んでいます。また、非構造化テキストをモデリングすることにより、インターネットから自動的に知識を抽出する Bing 用のシステムを開発中です。このすべてが同じ枠組み (モデルを確率的プログラムで表現された一連の前提として定義してから、汎用の推論手法で必要な計算を行う) を使用して実現されています。

能力評価システム TrueSkill は、確率的プログラミングの多くのメリットを実証しています。たとえば、システムの動作を解釈したり、モデルにドメインの知識を組み込んだり、新しいデータが到着するたびに学習したりできます。それでは、"Halo" や "Gears of War" などの大ヒット作のプロダクションで稼働しているモデルの簡易版を実装してみましょう。

問題とデータ 解決すべき問題は、ゲーム中のプレイヤーの評価です。これには、プレイヤー マッチメイキング (公平で楽しいゲームは同様の能力を持つプレイヤーまたはチームを対戦させることによって実現される) などの数多くの用途があります。わかりやすくするために、各対戦の参加者は 2 人だけとし、結果は勝ちまたは負けで引き分けはないものとします。したがって、各対戦に関するデータは 2 人のプレイヤーの一意の識別子と誰が勝ったかの指標になります。この記事では手作りの小さなデータセットを使用しますが、このアプローチは Xbox での何億もの対戦に合わせて調整できます。

目標は、すべてのプレイヤーの能力を学習し、将来の対戦の勝者を予測できるようにすることです。単純なアプローチは、各プレイヤーの勝ち負けの数をカウントするだけですが、これでは対戦相手の強さが考慮されません。もっと良い方法があります。

モデル設計 データの生成方法に関する前提を立てることから始めます。まず、各プレイヤーは、直接観測できない隠された (または潜在的な) 能力を持っています。確認できるのは能力の効果だけです。これは実際の数値であると仮定しますが、その生成方法を指定する必要もあります。妥当な選択肢は、正規 (またはガウス) 分布から生成された場合です。より複雑な例では、このガウス分布のパラメーターが不明で学習可能なものになります。わかりやすくするために、それらを直接設定します。

能力のモデルは、図 2 の左端のスケッチのように、図で表現できます。このスケッチは、ランダム変数の skill が正規分布から抽出される様子を示しています。

2 プレイヤー ゲームの構成
図 2 2 プレイヤー ゲームの構成

設定可能な別の前提は、各対戦で、プレイヤーがパフォーマンス数を持っているということです。この数は潜在的能力とほぼ同じですが、プレイヤーが標準レベルより上手くプレーしたか、下手にプレーしたかに応じて変動します。つまり、パフォーマンスは、能力のノイズの多いバージョンということになります。ノイズは、通常、図 2 の真ん中のスケッチのように、ガウス分布にモデリングされます。ここで、パフォーマンスは、平均がプレイヤーの能力で、分散が固定され、ハッチされたノイズ変数によって指定されるガウス分布から抽出されるランダム変数です。より複雑なモデルでは、データからノイズ分散を学習することになりますが、ここではわかりやすくするために 1 に固定します。

より上手くプレーをしたプレイヤーが勝利します。2 プレイヤー対戦では、図 2 の右端のスケッチのように、この様子を図で表現することができます。私は、ブール値 Player 1 Wins 変数を "若干" ハッチすることにより、この表記を少し修正しました。これは、対戦結果が出るトレーニング中はその値が観測されますが、予測中は観測されないためです。

このすべてをまとめる前に、新しい表記のいくつかを紹介する必要があります。1 つ目は、プレートと呼ばれ、foreach ループを表します。これは、特定の範囲 (プレイヤー全体やゲーム全体など) で繰り返す必要があるモデルの一部を囲む四角形です。2 つ目は、各ゲームで 2 人のプレイヤーの能力を選択したときに選択されたことを示すための破線です。簡略化した TrueSkill モデルを図 3 に示します。

簡略化した TrueSkill モデル
図 3 簡略化した TrueSkill モデル

ここでは、プレイヤーの範囲を囲むプレートを使用します。次に、ゲームごとに、2 人のプレイヤーのゲーム内の潜在的能力を選択して、特定のノイズを追加し、パフォーマンスを比較します。ノイズ変数は、その値がプレイヤーやゲームごとに変化しないことになっているため、どのプレート内にも存在しません。

因子グラフとも呼ばれるこのモデルの視覚化が、この単純なケースに役立つ表現です。しかし、モデルが大きくなると、図が複雑になって維持が困難になります。これが、開発者がコードで (確率的プログラムとして) 表現することを好む理由です。

Infer.NET

.NET 用の確率的プログラミング フレームワークは Infer.NET と呼ばれています。これは、Microsoft Research によって開発され、何か月か前にオープン ソース化されました。Infer.NET は、統計モデルを指定するためのモデリング API、ユーザー定義のモデルから ML アルゴリズムを生成するためのコンパイラ、アルゴリズムが実行されるランタイムを備えています。

Infer.NET は、ML.NET に緊密に統合されつつあり、現在は、Microsoft.ML.Probabilistic 名前空間の下に存在します。Infer.NET をインストールするには、次の手順を実行します。

dotnet add package Microsoft.ML.Probabilistic.Compiler

コンパイラがランタイム パッケージを自動的にプルダウンします。Infer.NET は .NET Standard 上で動作するため、Windows、Linux、および macOS 上で動作することに注意してください。

C# を使用して、Infer.NET 名前空間を含めることから始めましょう。

using Microsoft.ML.Probabilistic.Models;
using Microsoft.ML.Probabilistic.Utilities;
using Microsoft.ML.Probabilistic.Distributions;

その後で、前に定義したモデルを実装して、そのトレーニング方法と予測の作成方法を確認します。

モデル実装 モデルは、プレイヤーの能力、パフォーマンス、および結果の観点からゲームをシミュレートするプログラムと同様に作成されますが、このプログラムは実際には実行されません。水面下で、モデルを表すデータ構造が構築されます。このデータ構造が推論エンジンに渡されると、ML コードにコンパイルされてから、実際の計算を行うために実行されます。モデルを 3 段階で実装してみましょう。プレイヤーとゲームを囲むプレートの骨組みを定義して、プレイヤー プレートのコンテンツを定義し、ゲーム プレートのコンテンツを定義します。

プレートは、範囲クラスを使用して Infer.NET に実装されます。そのインスタンスは、基本的に、確率的 foreach ループ内のコントロール変数です。これらのプレートのサイズ (プレイヤーの数とゲームの数) を定義する必要があります。これらは事前にわからないため、これらの値のプレースホルダーとして使用される変数になります。便利なことに、Infer.NET は、この目的にぴったりの変数クラスを備えています。

var playerCount = Variable.New<int>();
var player = new Range(playerCount);
var gameCount = Variable.New<int>();
var game = new Range(gameCount);

プレートを定義したら、そのコンテンツに焦点を当てます。プレイヤー プレートでは、各プレイヤーの能力に関する倍精度ランダム変数の配列が必要になります。Infer.NET では、これが Variable.Array<T> を使用して実現されます。また、プレイヤーの能力に関する事前分布のガウス ランダム変数の配列も必要になります。次に、プレイヤーを調査して、彼らの能力を事前分布に関連付けます。これは、Variable<T>.Random メソッドを使用して実現されます。Variable.ForEach(Range) がプレートの中身を実装するための手段をどのように提供するかに注目してください。

var playerSkills = Variable.Array<double>(player);
var playerSkillsPrior = Variable.Array<Gaussian>(player);
using (Variable.ForEach(player))
{
  playerSkills[player] = Variable<double>.Random(playerSkillsPrior[player]);
}

モデルの最後のピースがゲーム プレートです。トレーニング データ (各ゲームの 1 人目のプレイヤーおよび 2 人目のプレイヤーとゲームの結果) が格納される配列を定義することから始めます。特定のアルゴリズムに合わせてデータを編成するのとは対照的に、データに特化したモデルを作成するところに注目してください。データ コンテナーの準備ができたら、ゲームを調査して、各ゲームのプレイヤーの能力を選択し、それらに特定のノイズを追加して、ゲームの結果を生成するパフォーマンスを比較する必要があります。

var players1 = Variable.Array<int>(game);
var players2 = Variable.Array<int>(game);
var player1Wins = Variable.Array<bool>(game);
  const double noise = 1.0;
    using (Variable.ForEach(game))
{
  var player1Skill = playerSkills[players1[game]];
  var player1Performance =
    Variable.GaussianFromMeanAndVariance(player1Skill, noise);
  var player2Skill = playerSkills[players2[game]];
  var player2Performance =
    Variable.GaussianFromMeanAndVariance(player2Skill, noise);
      player1Wins[game] = player1Performance > player2Performance;
}

興味深いことに、同じモデルがトレーニングと予測の両方で使用されます。違いは、観測するデータが異なることです。トレーニング中に対戦結果がわかりますが、予測中はわかりません。そのため、モデルは同じですが、生成されるアルゴリズムが異なります。幸い、Infer.NET コンパイラがそのすべてを処理してくれます。

トレーニング モデルに対するすべての問い合わせ (トレーニングや予測など) が 3 つの段階 (事前分布の設定、データの観測、推論の実行) を辿ります。トレーニングと予測は、根本的に同じ処理を行うため、共に "推論" と呼ばれます。観測したデータを使用して、事前分布から事後分布に移行します。トレーニングでは、能力全体の不確実性が高いことを示す、能力に関する広範な事前分布から始めます。事前分布にガウス分布を使用します。データの観測後に、能力に関するより狭いガウス事後分布を取得します。

能力に関する事前分布では、"Halo 5" から学習したパラメーターを借用するだけです。平均と分散に適切な選択肢はそれぞれ 6.0 と 9.0 です。これらの値は、事前分布が格納されている変数の ObservedValue プロパティに割り当てることによって設定します。4 人のプレイヤーの場合は、コードが次のようになります。

const int PlayerCount = 4;
playerSkillsPrior.ObservedValue =
  Enumerable.Repeat(Gaussian.FromMeanAndVariance(6, 9),
  PlayerCount).ToArray();

次に、データに移ります。ゲームごとに、2 人のプレイヤーと結果を取得しました。固定の例を見てみましょう。図 4 は、4 人のプレイヤーによってプレーされた 3 つの対戦を示しています。矢印はプレーされた対戦を示し、対戦の敗者を指しています。

3 つの対戦結果
図 4 3 つの対戦結果

コードを簡素化するために、各プレイヤーに一意の ID が 1 つずつ割り当てられているとします。この例で、1 つ目のゲームは Alice と Bob の対戦で、矢印は Alice が勝利したことを示しています。2 つ目のゲームでは、Bob が Charlie に勝利し、最後のゲームでは Donna が Charlie に勝利しています。もう一度 ObservedValue を使用したコード表現を以下に示します。

playerCount.ObservedValue = PlayerCount;
gameCount.ObservedValue = 3;
players1.ObservedValue = new[] { 0, 1, 2 };
players2.ObservedValue = new[] { 1, 2, 3 };
player1Wins.ObservedValue = new[] { true, true, false };

最後に、推論エンジンをインスタンス化して、取得する変数に対して Infer を呼び出すことにより、推論を実行します。このケースでは、プレイヤーの能力に関する事後分布にのみ興味があります。

var inferenceEngine = new InferenceEngine();
var inferredSkills = inferenceEngine.Infer<Gaussian[]>(playerSkills);

この Infer ステートメントは、実際には、2 回のコンパイル (Infer.NET と C#) と 1 回の実行を遂行しているため、多くの仕事をこなしていることになります。結果は、Infer.NET コンパイラが、確率的モデルに渡された playerSkills 変数をトレースして、モデル コードから抽象構文ツリーを構築し、C# で推論アルゴリズムを生成します。その後で、C# コンパイラが実行中に呼び出され、観測するデータに対してアルゴリズムが実行されます。非常に小さなデータを処理しているのに、このすべてが原因で、Infer に対する呼び出しが若干遅いと感じるかもしれません。Infer.NET のマニュアルに、プリコンパイル済みのアルゴリズムを使用することによってこのプロセスを高速化する方法が記載されています。つまり、本番環境では計算の一部だけが行われるように、事前にコンパイル手順を実行しておくわけです。

この例では、推論された能力は次のようになります。

Alice: Gaussian(8.147, 5.685)
Bob: Gaussian(5.722, 4.482)
Charlie: Gaussian(3.067, 4.814)
Donna: Gaussian(7.065, 6.588)

予測 プレイヤーの能力を推論したら、将来の対戦に関する予測を立てることができるようになります。トレーニングと同じ 3 つの段階を辿りますが、今回は、player1Wins 変数に関する事後分布を推論することに興味があります。これに関する考え方は、トレーニングでは、情報が因子グラフの下から上に流れるということです。つまり、一番下にあるデータから一番上のモデル パラメーターに情報が流れます。これに対し、予測では、既にモデル パラメーターが存在する (トレーニングで学習済み) ため、情報は上から下に流れます。

私のデータセットでは、Alice と Donna は勝ちが 1 で、負けが 0 です。しかし、2 人の対戦で直感的に感じたことは、Alice の方が勝つ確率が高いというものです。なぜなら、Charlie より強力なプレイヤーである可能性が高い Bob に彼女が勝利しているためです。Alice と Donna の対戦結果を予測してみましょう (図 5 を参照)。

対戦結果の予測
図 5 対戦結果の予測

このケースでは、能力に関する事前分布がトレーニングで推論された事後分布になります。観測するデータは、プレイヤー 0 (Alice) とプレイヤー 3 (Donna) の結果がわからない 1 つのゲームです。結果を不明にするために、前に観測した player1Wins の値をクリアする必要があります。なぜなら、これは事後分布が必要なものだからです。

playerSkillsPrior.ObservedValue = inferredSkills;
gameCount.ObservedValue = 1;
players1.ObservedValue = new[] { 0 };
players2.ObservedValue = new[] { 3 };
player1Wins.ClearObservedValue();
var player0Vs3 = inferenceEngine.Infer<Bernoulli[]>(player1Wins).First();

不確実性がこのモデルを通して対戦結果に伝播されることに言及しておく必要があります。これは、予測した結果から得られた事後分布が単なるブール値変数ではなく、1 人目のプレイヤーが対戦に勝つ確率を示す値であることを意味します。このような分布は、ベルヌーイと呼ばれています。

推論する変数の値は、Bernoulli(0.6127) です。これは、Alice が Donna に勝つ確率が 60% 以上あることを意味しており、私の直感と一致します。

評価 この例は、よく知られた確率的モデル TrueSkill を構築する方法を示しています。実際に、適切なモデルを考案するには、その設計に対する何回もの反復が必要です。特定のデータに対するモデル パフォーマンスを示す一連のメトリックを慎重に選択することにより、複数のモデルを比較します。

評価は、ML の中心課題であり、ここで取り上げるには範囲が広すぎます。また、確率的プログラミングに固有のものではありません。ただし、モデルを使用することにより、一意の "メトリック" (モデルの証拠) を計算できることに言及しておく必要があります。メトリックは、トレーニング データがこの特別なモデルによって生成されたことの蓋然性です。これは、複数のモデルの比較に適しています。テスト セットは必要ありません!

確率的プログラミングのメリット

この記事で紹介したアプローチは、これまで見てきたものより複雑に思えたかもしれません。たとえば、Connect(); 特別号 (msdn.com/magazine/mt848634) で、ML.NET を紹介しました。このマガジンでは、モデルの設計よりデータの変換に重点が置かれています。また、多くの場合、このアプローチが辿るべき適切な経路になります。データが既存のアルゴリズムに適合する可能性があり、モデルをブラック ボックスとして扱うことに慣れている場合は、手近なもので始めてください。ただし、特注のモデルが必要な場合は、確率的プログラミングが正しい選択肢になります。 

これ以外にも、確率的プログラミングを使用したい場合があります。理解しているモデルを使用する主なメリットの 1 つは、システムの動作をうまく説明できることです。モデルがブラック ボックスでない場合は、その中身を調査して、学習したパラメーターを確認できます。このことはモデルの設計者にとって重要な意味を持ちます。たとえば、前の例では、学習したプレイヤーの能力を確認できます。TrueSkill 2 と呼ばれる TrueSkill のより高度なバージョンでは、はるかに多くのゲームの側面がモデリングされます。これには、あるゲーム モードのパフォーマンスが別のゲーム モードのパフォーマンスとどのように関連するかが含まれます。この関連性を理解すれば、ゲーム デザイナーは、さまざまなゲーム モードの類似性に容易に気付くことができます。ML システムの説明力もデバッグにとって非常に重要です。ブラック ボックス モデルから必要な結果が得られない場合は、問題の探索をどこから始めていいのかさえわかりません。

確率的プログラミングのもう 1 つのメリットは、モデルに領域知識を組み込むための機能です。これは、モデルの構造と事前分布を設定できる能力の両方で享受されます。これに対し、従来のアプローチの多くは、データを調査するだけであり、領域専門家はシステムの動作を報告することができません。この機能は、医療などの、説得力のある領域知識があって、データが不十分な可能性がある特定の領域で必要です。

ベイズ的アプローチのメリットと、Infer.NET で非常にうまくサポートされるメリットは、新しいデータが到着するたびに学習できる能力です。これは、オンライン推論と呼ばれており、特に、ユーザー データを処理するシステムで役に立ちます。たとえば、TrueSkill は対戦ごとにプレイヤーの能力を更新する必要がありますし、知識抽出システムはその成長に合わせてインターネットから継続的に学習する必要があります。ただし、これは非常に簡単です。推論した事後分布を新しい事前分布として組み込むだけです。システムは新しいデータから学習する準備が整っています!

確率的プログラミングは、必然的に、特定のデータ特性を持つ問題にも適用されます。たとえば、異種データ、不十分なデータ、ラベルが付けられていないデータ、一部が失われているデータ、既知のバイアスを使用して収集されたデータなどです。

次のステップ

確率的モデルの構築を成功させるための秘訣が 2 つあります。1 つ目は、当然ですが、モデルリング方法を学習することです。この記事では、方法論の主要な概念とテクニックを紹介しましたが、もっと詳しく知りたい場合は、無料で入手できるオンライン ブック「Model-Based Machine Learning」(mbmlbook.com) をお勧めします。このブックは、本番環境でのモデルベースの ML のやさしい入門書であり、開発者 (科学者とは対照的) を対象としています。

モデルの設計方法がわかったら、それをコードで表現する方法を学習する必要があります。Infer.NET ユーザー ガイドがこれに関する優れたリソースです。さまざまなシナリオをカバーするチュートリアルと用例が含まれています。そのすべてに github.com/dotnet/infer にあるリポジトリからアクセスできます。そこでは、参加と投稿もお待ちしています。


Yordan Zaykovは、Microsoft Research Cambridge にある確率的推論開発チームのプリンシパル リサーチ ソフトウェア エンジニアリング リードです。彼は、Infer.NET ML フレームワークに基づくアプリケーションに重点的に取り組んでいます。このアプリケーションには、電子メールの分類、推奨システム、プレイヤーの順位付けとマッチメイキング、医療、および知識マイニングが含まれます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフのJohn Guiver、James McCaffrey、Tom Minka、John Winn に心より感謝いたします。