最適化 - RDDベースのAPI
\[ \newcommand{\R}{\mathbb{R}} \newcommand{\E}{\mathbb{E}} \newcommand{\x}{\mathbf{x}} \newcommand{\y}{\mathbf{y}} \newcommand{\wv}{\mathbf{w}} \newcommand{\av}{\mathbf{\alpha}} \newcommand{\bv}{\mathbf{b}} \newcommand{\N}{\mathbb{N}} \newcommand{\id}{\mathbf{I}} \newcommand{\ind}{\mathbf{1}} \newcommand{\0}{\mathbf{0}} \newcommand{\unit}{\mathbf{e}} \newcommand{\one}{\mathbf{1}} \newcommand{\zero}{\mathbf{0}} \]
数学的記述
勾配降下法
$\min_{\wv \in\R^d} \; f(\wv)$
の形式の最適化問題を解くための最も単純な方法は、勾配降下法です。このような一次最適化手法 (勾配降下法とその確率的変種を含む) は、大規模および分散計算に適しています。
勾配降下法は、最も急な降下方向、つまり現在の点、すなわち現在のパラメータ値における関数の導関数 ( 勾配 と呼ばれる) の負の方向に反復的にステップを進めることによって、関数の局所最小値を見つけることを目的としています。目的関数 $f$
がすべての引数で微分可能ではないが、それでも凸である場合、 *劣勾配* は勾配の自然な一般化であり、ステップ方向の役割を担います。いずれの場合でも、$f$
の勾配または劣勾配を計算することはコストがかかります。すべての損失項からの寄与を計算するために、データセット全体を完全にパススルーする必要があるためです。
確率的勾配降下法 (SGD)
目的関数 $f$
が合計として記述される最適化問題は、*確率的勾配降下法 (SGD)* を使用して解くのに特に適しています。教師あり機械学習で一般的に使用される最適化定式化の場合、 \begin{equation} f(\wv) := \lambda\, R(\wv) + \frac1n \sum_{i=1}^n L(\wv;\x_i,y_i) \label{eq:regPrimal} \ . \end{equation}
は特に自然です。損失は各データポイントからの個々の損失の平均として記述されるためです。
確率的劣勾配は、ベクトルのランダムな選択であり、期待値において、元の目的関数の真の劣勾配が得られます。1つのデータポイント $i\in[1..n]$
をランダムに均一に選択すると、$\wv$
に関して、$\eqref{eq:regPrimal}$
の確率的劣勾配が次のように得られます。 \[ f'_{\wv,i} := L'_{\wv,i} + \lambda\, R'_\wv \ , \]
ここで、$L'_{\wv,i} \in \R^d$
は、$i$
番目のデータポイントによって決定される損失関数部分の劣勾配、つまり $L'_{\wv,i} \in \frac{\partial}{\partial \wv} L(\wv;\x_i,y_i)$
です。さらに、$R'_\wv$
は、正則化項 $R(\wv)$
の劣勾配、つまり $R'_\wv \in \frac{\partial}{\partial \wv} R(\wv)$
です。$R'_\wv$
という項は、どのランダムデータポイントが選択されるかには依存しません。明らかに、$i\in[1..n]$
のランダムな選択に対する期待値において、$f'_{\wv,i}$
は元の目的 $f$
の劣勾配であり、$\E\left[f'_{\wv,i}\right] \in \frac{\partial}{\partial \wv} f(\wv)$
を意味します。
SGDを実行することは、単に負の確率的劣勾配 $f'_{\wv,i}$
の方向に進むことになります。つまり、\begin{equation}\label{eq:SGDupdate} \wv^{(t+1)} := \wv^{(t)} - \gamma \; f'_{\wv,i} \ . \end{equation}
です。 **ステップサイズ。** パラメータ $\gamma$
はステップサイズであり、デフォルトの実装では、反復カウンタの平方根で減少するように選択されます。つまり、$t$
番目の反復では $\gamma := \frac{s}{\sqrt{t}}$
であり、入力パラメータは $s=$ stepSize
です。SGDメソッドの最適なステップサイズを選択することは、実際には難しい場合があり、活発な研究のトピックであることに注意してください。
**勾配。** spark.mllib
に実装されている機械学習メソッドの(劣)勾配の表は、分類と回帰セクションにあります。
**近接更新。** ステップ方向で正則化項の劣勾配 $R'(\wv)$
を使用する代わりに、近接作用素を使用することにより、場合によっては更新を改善できます。L1正則化項の場合、近接作用素は、L1Updater に実装されているように、ソフトしきい値化によって与えられます。
分散SGDの更新スキーム
GradientDescent のSGD実装では、データ例を単純に(分散して)サンプリングします。最適化問題 $\eqref{eq:regPrimal}$
の損失部分は $\frac1n \sum_{i=1}^n L(\wv;\x_i,y_i)$
であり、したがって $\frac1n \sum_{i=1}^n L'_{\wv,i}$
は真の(劣)勾配になります。これはデータセット全体へのアクセスが必要になるため、パラメータ miniBatchFraction
は、代わりに使用するデータの割合を指定します。このサブセットの勾配の平均、つまり \[ \frac1{|S|} \sum_{i\in S} L'_{\wv,i} \ , \]
は確率的勾配です。ここで、$S$
はサイズ $|S|=$ miniBatchFraction $\cdot n$
のサンプリングされたサブセットです。
各反復では、分散データセット (RDD) のサンプリングと、各ワーカーマシンからの部分的な結果の合計の計算は、標準のSparkルーチンによって実行されます。
ポイントの割合 miniBatchFraction
が1(デフォルト)に設定されている場合、各反復で得られるステップは正確な(劣)勾配降下になります。この場合、使用されるステップ方向にランダム性と分散はありません。一方、miniBatchFraction
が非常に小さく選択され、単一のポイントのみがサンプリングされる場合、つまり $|S|=$ miniBatchFraction $\cdot n = 1$
の場合、アルゴリズムは標準のSGDと同等です。その場合、ステップ方向はポイントの均一なランダムサンプリングに依存します。
メモリ制限付きBFGS (L-BFGS)
L-BFGS は、$\min_{\wv \in\R^d} \; f(\wv)$
の形式の最適化問題を解くための準ニュートン法ファミリーの最適化アルゴリズムです。L-BFGS法は、ヘッセ行列を構築するために目的関数の二階偏導関数を評価することなく、目的関数を局所的に二次関数として近似します。ヘッセ行列は以前の勾配評価によって近似されるため、ニュートン法でヘッセ行列を明示的に計算する場合、垂直方向のスケーラビリティの問題(トレーニング特徴量のde数)はありません。その結果、L-BFGSは、他の一次最適化と比較して、多くの場合、より高速な収束を実現します。
最適化手法の選択
線形手法 は内部で最適化を使用し、spark.mllib
の一部の線形手法はSGDとL-BFGSの両方をサポートしています。最適化手法が異なれば、目的関数のプロパティに応じて異なる収束保証が得られる可能性があり、ここでは文献を網羅することはできません。一般に、L-BFGSが利用可能な場合は、L-BFGSの方がSGDよりも高速に収束する傾向があるため(反復回数が少ない)、SGDの代わりにL-BFGSを使用することをお勧めします。
MLlibでの実装
勾配降下法と確率的勾配降下法
確率的劣勾配降下法 (SGD) を含む勾配降下法は、MLlib
に低レベルプリミティブとして含まれており、それを基にさまざまなMLアルゴリズムが開発されています。例については、線形手法セクションを参照してください。
SGDクラス GradientDescent は、次のパラメータを設定します
Gradient
クラスは、最適化対象の関数の確率的勾配、つまり現在の パラメータ値における単一の訓練例に関する勾配を計算します。MLlib には、ヒンジ損失、ロジスティック損失、最小二乗など、一般的な損失関数用の勾配クラスが含まれています。勾配クラスは、訓練例、そのラベル、および現在の パラメータ値を入力として受け取ります。Updater
クラスは、損失部分の所定の勾配に対して、実際の勾配降下ステップ、つまり各反復での重みの更新を実行するクラスです。Updater は、正則化部分からの更新を実行する役割も担います。MLlib には、正則化がない場合と、L1 および L2 正則化の場合の Updater が含まれています。stepSize
は、勾配降下の初期ステップサイズを示すスカラー値です。MLlib のすべての Updater は、t 番目のステップでstepSize $/ \sqrt{t}$
に等しいステップサイズを使用します。numIterations
は、実行する反復回数です。regParam
は、L1 または L2 正則化を使用する場合の正則化パラメータです。miniBatchFraction
は、勾配方向を計算するために各反復でサンプリングされる、全データの割合です。- サンプリングには依然として RDD 全体のパスが必要となるため、
miniBatchFraction
を小さくしても、最適化が大幅に高速化されない場合があります。勾配の計算にコストがかかる場合、選択されたサンプルのみが勾配の計算に使用されるため、ユーザーは最大の高速化を体験できます。
- サンプリングには依然として RDD 全体のパスが必要となるため、
L-BFGS
L-BFGS は、現在 MLlib
では低レベルの最適化プリミティブにすぎません。線形回帰やロジスティック回帰などのさまざまな ML アルゴリズムで L-BFGS を使用するには、LogisticRegressionWithSGD のようなトレーニング API を使用する代わりに、目的関数の勾配と Updater を自分でオプティマイザに渡す必要があります。以下の例を参照してください。これは次のリリースで対処される予定です。
L1Updater を使用した L1 正則化は機能しません。L1Updater のソフトしきい値ロジックは勾配降下用に設計されているためです。開発者のメモを参照してください。
L-BFGS メソッド LBFGS.runLBFGS には、次のパラメータがあります。
Gradient
クラスは、最適化対象の目的関数の勾配、つまり現在の パラメータ値における単一の訓練例に関する勾配を計算します。MLlib には、ヒンジ損失、ロジスティック損失、最小二乗など、一般的な損失関数用の勾配クラスが含まれています。勾配クラスは、訓練例、そのラベル、および現在の パラメータ値を入力として受け取ります。Updater
クラスは、L-BFGS の正則化部分の目的関数の勾配と損失を計算するクラスです。MLlib には、正則化がない場合と、L2 正則化の場合の Updater が含まれています。numCorrections
は、L-BFGS 更新で使用される修正の数です。10 が推奨されます。maxNumIterations
は、L-BFGS を実行できる最大反復回数です。regParam
は、正則化を使用する場合の正則化パラメータです。convergenceTol
は、L-BFGS が収束したと見なされる場合に許容される相対変化量を制御します。これは非負でなければなりません。値が小さいほど許容度が低くなり、一般的に反復回数が多くなります。この値は、Breeze LBFGS 内の平均改善度と勾配のノルムの両方を考慮します。
return
は、2 つの要素を含むタプルです。最初の要素は、各特徴の重みを含む列行列であり、2 番目の要素は、各反復で計算された損失を含む配列です。
L-BFGS オプティマイザを使用して L2 正則化で二項ロジスティック回帰をトレーニングする例を次に示します。
API の詳細については、LBFGS
Scala ドキュメント および SquaredL2Updater
Scala ドキュメント を参照してください。
import org.apache.spark.mllib.classification.LogisticRegressionModel
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.optimization.{LBFGS, LogisticGradient, SquaredL2Updater}
import org.apache.spark.mllib.util.MLUtils
val data = MLUtils.loadLibSVMFile(sc, "data/mllib/sample_libsvm_data.txt")
val numFeatures = data.take(1)(0).features.size
// Split data into training (60%) and test (40%).
val splits = data.randomSplit(Array(0.6, 0.4), seed = 11L)
// Append 1 into the training data as intercept.
val training = splits(0).map(x => (x.label, MLUtils.appendBias(x.features))).cache()
val test = splits(1)
// Run training algorithm to build the model
val numCorrections = 10
val convergenceTol = 1e-4
val maxNumIterations = 20
val regParam = 0.1
val initialWeightsWithIntercept = Vectors.dense(new Array[Double](numFeatures + 1))
val (weightsWithIntercept, loss) = LBFGS.runLBFGS(
training,
new LogisticGradient(),
new SquaredL2Updater(),
numCorrections,
convergenceTol,
maxNumIterations,
regParam,
initialWeightsWithIntercept)
val model = new LogisticRegressionModel(
Vectors.dense(weightsWithIntercept.toArray.slice(0, weightsWithIntercept.size - 1)),
weightsWithIntercept(weightsWithIntercept.size - 1))
// Clear the default threshold.
model.clearThreshold()
// Compute raw scores on the test set.
val scoreAndLabels = test.map { point =>
val score = model.predict(point.features)
(score, point.label)
}
// Get evaluation metrics.
val metrics = new BinaryClassificationMetrics(scoreAndLabels)
val auROC = metrics.areaUnderROC()
println("Loss of each step in training process")
loss.foreach(println)
println(s"Area under ROC = $auROC")
API の詳細については、LBFGS
Java ドキュメント および SquaredL2Updater
Java ドキュメント を参照してください。
import java.util.Arrays;
import scala.Tuple2;
import org.apache.spark.api.java.*;
import org.apache.spark.mllib.classification.LogisticRegressionModel;
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics;
import org.apache.spark.mllib.linalg.Vector;
import org.apache.spark.mllib.linalg.Vectors;
import org.apache.spark.mllib.optimization.*;
import org.apache.spark.mllib.regression.LabeledPoint;
import org.apache.spark.mllib.util.MLUtils;
import org.apache.spark.SparkConf;
import org.apache.spark.SparkContext;
String path = "data/mllib/sample_libsvm_data.txt";
JavaRDD<LabeledPoint> data = MLUtils.loadLibSVMFile(sc, path).toJavaRDD();
int numFeatures = data.take(1).get(0).features().size();
// Split initial RDD into two... [60% training data, 40% testing data].
JavaRDD<LabeledPoint> trainingInit = data.sample(false, 0.6, 11L);
JavaRDD<LabeledPoint> test = data.subtract(trainingInit);
// Append 1 into the training data as intercept.
JavaPairRDD<Object, Vector> training = data.mapToPair(p ->
new Tuple2<>(p.label(), MLUtils.appendBias(p.features())));
training.cache();
// Run training algorithm to build the model.
int numCorrections = 10;
double convergenceTol = 1e-4;
int maxNumIterations = 20;
double regParam = 0.1;
Vector initialWeightsWithIntercept = Vectors.dense(new double[numFeatures + 1]);
Tuple2<Vector, double[]> result = LBFGS.runLBFGS(
training.rdd(),
new LogisticGradient(),
new SquaredL2Updater(),
numCorrections,
convergenceTol,
maxNumIterations,
regParam,
initialWeightsWithIntercept);
Vector weightsWithIntercept = result._1();
double[] loss = result._2();
LogisticRegressionModel model = new LogisticRegressionModel(
Vectors.dense(Arrays.copyOf(weightsWithIntercept.toArray(), weightsWithIntercept.size() - 1)),
(weightsWithIntercept.toArray())[weightsWithIntercept.size() - 1]);
// Clear the default threshold.
model.clearThreshold();
// Compute raw scores on the test set.
JavaPairRDD<Object, Object> scoreAndLabels = test.mapToPair(p ->
new Tuple2<>(model.predict(p.features()), p.label()));
// Get evaluation metrics.
BinaryClassificationMetrics metrics =
new BinaryClassificationMetrics(scoreAndLabels.rdd());
double auROC = metrics.areaUnderROC();
System.out.println("Loss of each step in training process");
for (double l : loss) {
System.out.println(l);
}
System.out.println("Area under ROC = " + auROC);
開発者向けメモ
ヘッセ行列は以前の勾配評価から近似的に構築されるため、最適化プロセス中に目的関数を変更することはできません。そのため、Stochastic L-BFGS は miniBatch を使用するだけではうまく機能しません。そのため、より良い理解が得られるまでは、これを提供しません。
Updater
クラスは、もともと勾配降下用に設計されたクラスであり、実際の勾配降下ステップを計算します。ただし、適応ステップサイズなどの勾配降下専用のロジック部分を無視することで、L-BFGS の正則化の目的関数の勾配と損失を取得できます。後で正則化とステップ更新のロジックを分離するために、これをリファクタリングして Updater を正則化子に置き換えます。