TensorFlowでの最急降下法の多くのアプリケーション
公開: 2022-03-11GoogleのTensorFlowは、ディープラーニングモデルをトレーニングおよびデプロイするための主要なツールの1つです。 何億ものパラメータを使用して非常に複雑なニューラルネットワークアーキテクチャを最適化することができ、ハードウェアアクセラレーション、分散トレーニング、および本番ワークフロー用のさまざまなツールが付属しています。 これらの強力な機能により、ディープラーニングの領域外では威圧的で不必要に見える可能性があります。
ただし、TensorFlowは、ディープラーニングモデルのトレーニングに直接関係しない単純な問題に対してもアクセス可能であり、使用可能です。 中核となるTensorFlowは、テンソル演算(ベクトル、行列など)と、任意の計算シーケンスで最急降下法を実行するために使用される微積分演算用に最適化されたライブラリです。 経験豊富なデータサイエンティストは、「最急降下法」を計算数学の基本的なツールとして認識しますが、通常、アプリケーション固有のコードと方程式を実装する必要があります。 これから説明するように、ここでTensorFlowの最新の「自動微分」アーキテクチャが登場します。
TensorFlowのユースケース
- 例1:TensorFlow2.0での最急降下法による線形回帰
- 最急降下法とは何ですか?
- 例2:単位ベクトルを最大限に広げる
- 例3:敵対的なAI入力の生成
- 最終的な考え:最急降下法の最適化
- TensorFlowの最急降下法:最小値の検索からAIシステムの攻撃まで
例1:TensorFlow2.0での最急降下法による線形回帰
例1ノートブック
TensorFlowコードに到達する前に、最急降下法と線形回帰に精通していることが重要です。
最急降下法とは何ですか?
簡単に言うと、連立方程式の出力を最小化する入力を見つけるための数値手法です。 機械学習のコンテキストでは、その連立方程式はモデルであり、入力はモデルの未知のパラメーターであり、出力は最小化される損失関数であり、モデルとデータの間にどれだけの誤差があるかを表します。 一部の問題(線形回帰など)には、エラーを最小化するパラメーターを直接計算する方程式がありますが、ほとんどの実際のアプリケーションでは、満足のいく解に到達するために最急降下法などの数値手法が必要です。
この記事の最も重要な点は、最急降下法では通常、方程式をレイアウトし、微積分を使用して損失関数とパラメーターの関係を導出する必要があるということです。 TensorFlow(および最新の自動微分ツール)を使用すると、微積分が処理されるため、ソリューションの設計に集中でき、実装に時間を費やす必要がありません。
単純な線形回帰問題では、次のようになります。 150人の成人男性の身長(h)と体重(w)のサンプルがあり、この線の傾きと標準偏差の不完全な推測から始めます。 最急降下法を約15回繰り返した後、ほぼ最適な解に到達します。
TensorFlow2.0を使用して上記のソリューションをどのように作成したかを見てみましょう。
線形回帰の場合、重みは高さの線形方程式によって予測できると言います。
予測と真の値の間の平均二乗誤差(損失)を最小化するパラメーターαとβ(傾きと切片)を見つけたいと思います。 したがって、損失関数(この場合、「平均二乗誤差」、つまりMSE)は次のようになります。
平均二乗誤差がいくつかの不完全な線をどのように探すか、そして正確な解(α= 6.04、β= -230.5)を見ることができます。
このアイデアをTensorFlowで実行してみましょう。 最初に行うことは、テンソルとtf.*
関数を使用して損失関数をコード化することです。
def calc_mean_sq_error(heights, weights, slope, intercept): predicted_wgts = slope * heights + intercept errors = predicted_wgts - weights mse = tf.reduce_mean(errors**2) return mse
これはかなり簡単に見えます。 すべての標準代数演算子はテンソルに対してオーバーロードされているため、最適化する変数がテンソルであることを確認するだけでよく、それ以外の場合はtf.*
メソッドを使用します。
次に、これを最急降下ループに入れるだけです。
def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): # Any values to be part of gradient calcs need to be vars/tensors tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') # Hardcoding 25 iterations of gradient descent for i in range(25): # Do all calculations under a "GradientTape" which tracks all gradients with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) # This is the same mean-squared-error calculation as before predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) # Auto-diff magic! Calcs gradients between loss calc and params dloss_dparams = tape.gradient(loss, [tf_slope, tf_icept]) # Gradients point towards +loss, so subtract to "descend" tf_slope = tf_slope - learning_rate * dloss_dparams[0] tf_icept = tf_icept - learning_rate * dloss_dparams[1]
これがいかにすっきりしているかを理解するために少し時間を取ってみましょう。 最急降下法では、最適化しようとしているすべての変数に関して損失関数の導関数を計算する必要があります。 微積分が関与することになっていますが、実際には何もしませんでした。 魔法は次の事実にあります:
- TensorFlowは、
tf.GradientTape()
で実行されたすべての計算の計算グラフを作成します。 - TensorFlowは、すべての操作の導関数(勾配)を計算する方法を知っているため、計算グラフ内の変数が他の変数にどのように影響するかを判断できます。
プロセスはさまざまな開始点からどのように見えますか?
最急降下法は最適なMSEに著しく近づきますが、実際には、両方の例で最適なものとは大幅に異なる傾きと切片に収束します。 場合によっては、これは単に極小値に収束する最急降下法であり、これは最急降下法アルゴリズムに固有の課題です。 しかし、線形回帰には、おそらく1つのグローバル最小値しかありません。 では、どうして間違った傾斜になってインターセプトしたのでしょうか?
この場合、問題は、デモンストレーションのためにコードを単純化しすぎたことです。 データを正規化しておらず、勾配パラメーターは切片パラメーターとは異なる特性を持っています。 傾きの小さな変化は損失の大きな変化を生み出す可能性がありますが、切片の小さな変化はほとんど効果がありません。 トレーニング可能なパラメーターのスケールのこの大きな違いは、勾配計算を支配する勾配につながり、切片パラメーターはほとんど無視されます。
したがって、最急降下法は、最初の切片の推測に非常に近い最適な勾配を効果的に見つけます。 また、誤差は最適に非常に近いため、その周囲の勾配は小さいため、連続する各反復はほんの少ししか移動しません。 最初にデータを正規化すると、この現象は劇的に改善されますが、それを排除することはできませんでした。
これは比較的単純な例でしたが、次のセクションで、この「自動微分」機能がかなり複雑なものを処理できることを確認します。
例2:単位ベクトルを最大限に広げる
例2ノートブック
この次の例は、昨年受講したディープラーニングコースでの楽しいディープラーニング演習に基づいています。
問題の要点は、32個の正規分布数のセットからリアルな顔を生成できる「変分オートエンコーダー」(VAE)があることです。 容疑者を特定するために、VAEを使用して、目撃者が選択できるさまざまな(理論上の)顔のセットを作成し、選択された顔に類似した顔をさらに作成して検索を絞り込みます。 この演習では、ベクトルの初期セットをランダム化することが提案されましたが、最適な初期状態を見つけたいと思いました。
この問題は次のように表現できます。32次元の空間が与えられた場合、最大に分散しているX単位ベクトルのセットを見つけます。 2次元では、これは正確に計算するのは簡単です。 しかし、3次元(または32次元!)の場合、簡単な答えはありません。 ただし、目標状態に達したときに最小になる適切な損失関数を定義できれば、最急降下法がそこに到達するのに役立つ可能性があります。
上記のようにランダム化された20個のベクトルのセットから始め、TensorFlowの機能を実証するために、それぞれが複雑さを増す3つの異なる損失関数を試してみます。
まず、トレーニングループを定義しましょう。 すべてのTensorFlowロジックをself.calc_loss()
メソッドの下に配置します。その後、テクニックごとにそのメソッドをオーバーライドして、このループをリサイクルできます。
# Define the framework for trying different loss functions # Base class implements loop, sub classes override self.calc_loss() class VectorSpreadAlgorithm: # ... def calc_loss(self, tensor2d): raise NotImplementedError("Define this in your derived class") def one_iter(self, i, learning_rate): # self.vecs is an 20x2 tensor, representing twenty 2D vectors tfvecs = tf.convert_to_tensor(self.vecs, dtype=tf.float32) with tf.GradientTape() as tape: tape.watch(tfvecs) loss = self.calc_loss(tfvecs) # Here's the magic again. Derivative of spread with respect to # input vectors gradients = tape.gradient(loss, tfvecs) self.vecs = self.vecs - learning_rate * gradients
最初に試すテクニックは最も簡単です。 互いに最も近いベクトルの角度である拡散メトリックを定義します。 広がりを最大化したいのですが、最小化問題にするのが一般的です。 したがって、スプレッドメトリックの負の値を単純に取ります。
class VectorSpread_Maximize_Min_Angle(VectorSpreadAlgorithm): def calc_loss(self, tensor2d): angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi spread_metric = tf.reduce_min(angle_pairs + disable_diag) # Convention is to return a quantity to be minimized, but we want # to maximize spread. So return negative spread return -spread_metric
Matplotlibの魔法の中には、視覚化をもたらすものがあります。
これは不格好です(文字通りかなり!)が、機能します。 20個のベクトルのうち一度に2つだけが更新され、それらが最も近くなくなるまでそれらの間のスペースが大きくなり、次に新しい2つの最も近いベクトル間の角度が大きくなるように切り替わります。 注意すべき重要なことは、それが機能することです。 TensorFlowがtf.reduce_min()
tf.acos()
を介してグラデーションを通過させ、正しいことを実行できたことがわかります。
もう少し手の込んだものを試してみましょう。 最適なソリューションでは、すべてのベクトルが最も近い隣接ベクトルに対して同じ角度を持つ必要があることがわかっています。 それでは、損失関数に「最小角度の分散」を追加しましょう。
class VectorSpread_MaxMinAngle_w_Variance(VectorSpreadAlgorithm): def spread_metric(self, tensor2d): """ Assumes all rows already normalized """ angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi all_mins = tf.reduce_min(angle_pairs + disable_diag, axis=1) # Same calculation as before: find the min-min angle min_min = tf.reduce_min(all_mins) # But now also calculate the variance of the min angles vector avg_min = tf.reduce_mean(all_mins) var_min = tf.reduce_sum(tf.square(all_mins - avg_min)) # Our spread metric now includes a term to minimize variance spread_metric = min_min - 0.4 * var_min # As before, want negative spread to keep it a minimization problem return -spread_metric
その唯一の北向きのベクトルは、最も近い隣人への角度が大きく、現在最小化されている分散項をスパイクするため、現在、そのピアに急速に参加しています。 しかし、それでも最終的には、立ち上がるのが遅いままである世界的に最小の角度によって駆動されます。 これを改善する必要があるアイデアは、通常、この2Dの場合に機能しますが、より高い次元では機能しません。
しかし、この数学的試みの質に焦点を合わせすぎると、要点が失われます。 平均と分散の計算に含まれるテンソル操作の数と、TensorFlowが入力行列のすべてのコンポーネントのすべての計算を正常に追跡して区別する方法を確認してください。 また、手動で計算する必要はありませんでした。 簡単な計算を一緒に行うだけで、TensorFlowが計算を行いました。
最後に、もう1つ、力ベースのソリューションを試してみましょう。 すべてのベクトルが中心点につながれた小さな惑星であると想像してください。 各惑星は、他の惑星からそれをはじく力を放出します。 このモデルの物理シミュレーションを実行する場合は、目的のソリューションに到達する必要があります。
私の仮説は、最急降下法も機能するはずだということです。 最適なソリューションでは、他のすべての惑星からのすべての惑星の接線力は、正味ゼロの力に相殺されるはずです(ゼロでなければ、惑星は移動します)。 それでは、すべてのベクトルにかかる力の大きさを計算し、最急降下法を使用してそれをゼロに向かって押してみましょう。
まず、 tf.*
メソッドを使用して力を計算するメソッドを定義する必要があります。
class VectorSpread_Force(VectorSpreadAlgorithm): def force_a_onto_b(self, vec_a, vec_b): # Calc force assuming vec_b is constrained to the unit sphere diff = vec_b - vec_a norm = tf.sqrt(tf.reduce_sum(diff**2)) unit_force_dir = diff / norm force_magnitude = 1 / norm**2 force_vec = unit_force_dir * force_magnitude # Project force onto this vec, calculate how much is radial b_dot_f = tf.tensordot(vec_b, force_vec, axes=1) b_dot_b = tf.tensordot(vec_b, vec_b, axes=1) radial_component = (b_dot_f / b_dot_b) * vec_b # Subtract radial component and return result return force_vec - radial_component
次に、上記の力関数を使用して損失関数を定義します。 各ベクトルに正味の力を累積し、その大きさを計算します。 最適なソリューションでは、すべての力が相殺され、力がゼロになるはずです。

def calc_loss(self, tensor2d): n_vec = tensor2d.numpy().shape[0] all_force_list = [] for this_idx in range(n_vec): # Accumulate force of all other vecs onto this one this_force_list = [] for other_idx in range(n_vec): if this_idx == other_idx: continue this_vec = tensor2d[this_idx, :] other_vec = tensor2d[other_idx, :] tangent_force_vec = self.force_a_onto_b(other_vec, this_vec) this_force_list.append(tangent_force_vec) # Use list of all N-dimensional force vecs. Stack and sum. sum_tangent_forces = tf.reduce_sum(tf.stack(this_force_list)) this_force_mag = tf.sqrt(tf.reduce_sum(sum_tangent_forces**2)) # Accumulate all magnitudes, should all be zero at optimal solution all_force_list.append(this_force_mag) # We want to minimize total force sum, so simply stack, sum, return return tf.reduce_sum(tf.stack(all_force_list))
ソリューションが美しく機能するだけでなく(最初の数フレームの混乱を除いて)、本当の功績はTensorFlowにあります。 このソリューションには、複数のfor
ループ、 if
ステートメント、および膨大な計算のウェブが含まれ、TensorFlowはそれらすべての勾配を正常にトレースしました。
例3:敵対的なAI入力の生成
例3ノートブック
この時点で、読者は「ねえ!この投稿はディープラーニングに関するものではないはずだ!」と考えているかもしれません。 しかし、技術的には、導入は「ディープラーニングモデルのトレーニング」を超えることを指します。 この場合、私たちはトレーニングを行っていませんが、代わりに、事前にトレーニングされたディープニューラルネットワークのいくつかの数学的特性を利用して、間違った結果を与えるようにだまします。 これは、想像よりもはるかに簡単で効果的であることが判明しました。 そして、必要なのはTensorFlow2.0コードの別の短いブロブだけでした。
まず、攻撃する画像分類子を見つけます。 Dogs vs.CatsKaggleコンテストのトップソリューションの1つを使用します。 具体的には、Kagglerの「uysimty」によって提示されたソリューションです。 効果的な猫対犬のモデルを提供し、優れたドキュメントを提供したことは、彼らの功績です。 これは、18のニューラルネットワーク層にわたる1300万のパラメーターで構成される強力なモデルです。 (読者は、対応するノートブックでそれについてもっと読むことを歓迎します。)
ここでの目標は、この特定のネットワークの欠陥を強調することではなく、多数の入力を持つ標準のニューラルネットワークがどのように脆弱であるかを示すことであることに注意してください。
少し手を加えるだけで、モデルをロードして、モデルによって分類される画像を前処理する方法を理解することができました。
これは本当に堅実な分類器のように見えます! すべてのサンプル分類は正しく、95%以上の信頼度です。 攻撃しよう!
明らかに猫である画像を作成したいのですが、分類器に自信を持って犬であると判断させます。 どうすればそれができますか?
正しく分類される猫の写真から始めて、特定の入力ピクセルの各カラーチャネル(値0〜255)の小さな変更が最終的な分類子の出力にどのように影響するかを理解しましょう。 1つのピクセルを変更してもあまり効果はありませんが、128x128x3=49,152ピクセルのすべての値の累積的な調整で目標を達成できる可能性があります。
各ピクセルをプッシュする方法をどのように知ることができますか? 通常のニューラルネットワークトレーニングでは、TensorFlowの勾配降下法を使用して、ターゲットラベルと予測ラベルの間の損失を最小限に抑え、1,300万個の自由パラメーターすべてを同時に更新しようとします。 この場合、代わりに1,300万個のパラメーターを固定したままにして、入力自体のピクセル値を調整します。
私たちの損失関数は何ですか? さて、それは画像が猫のように見える程度です! 各入力ピクセルに関する猫の値の導関数を計算すると、猫の分類確率を最小化するためにそれぞれをプッシュする方法がわかります。
def adversarial_modify(victim_img, to_dog=False, to_cat=False): # We only need four gradient descent steps for i in range(4): tf_victim_img = tf.convert_to_tensor(victim_img, dtype='float32') with tf.GradientTape() as tape: tape.watch(tf_victim_img) # Run the image through the model model_output = model(tf_victim_img) # Minimize cat confidence and maximize dog confidence loss = (model_output[0] - model_output[1]) dloss_dimg = tape.gradient(loss, tf_victim_img) # Ignore gradient magnitudes, only care about sign, +1/255 or -1/255 pixels_w_pos_grad = tf.cast(dloss_dimg > 0.0, 'float32') / 255. pixels_w_neg_grad = tf.cast(dloss_dimg < 0.0, 'float32') / 255. victim_img = victim_img - pixels_w_pos_grad + pixels_w_neg_grad
Matplotlibの魔法は、結果を視覚化するのに役立ちます。
おお! 人間の目には、これらの写真はどれも同じものです。 しかし、4回の反復の後、これは99.4%の信頼度で、分類子が犬であると確信しました。
これがまぐれではなく、他の方向にも機能することを確認しましょう。
成功! 分類器は当初、これを98.4%の信頼度の犬として正しく予測していましたが、現在は99.8%の信頼度の猫であると信じています。
最後に、サンプルの画像パッチを見て、それがどのように変化したかを見てみましょう。
予想どおり、最終的なパッチは元のパッチと非常によく似ており、各ピクセルは赤チャネルの強度値で-4から+4にシフトするだけです。 このシフトは、人間が違いを区別するのに十分ではありませんが、分類器の出力を完全に変更します。
最終的な考え:最急降下法の最適化
この記事全体を通して、単純さと透明性のために、トレーニング可能なパラメーターに手動でグラデーションを適用する方法について説明しました。 ただし、現実の世界では、データサイエンティストは、コードの膨張を追加せずにはるかに効果的である傾向があるため、オプティマイザーの使用にすぐに取り掛かる必要があります。
RMSprop、Adagrad、Adadeltaなど、多くの人気のあるオプティマイザーがありますが、最も一般的なのはおそらくAdamです。 パラメータごとに異なる学習率を動的に維持するため、「適応学習率法」と呼ばれることもあります。 それらの多くは、極小値を回避し、より高速な収束を達成することを目的として、運動量項と近似高階導関数を使用します。
セバスチャン・ルーダーから借りたアニメーションでは、さまざまなオプティマイザーが損失面を下っていく経路を見ることができます。 私たちが示した手動の手法は、「SGD」に最も匹敵します。 最高のパフォーマンスを発揮するオプティマイザは、すべての損失面で同じになるわけではありません。 ただし、より高度なオプティマイザは、通常、単純なオプティマイザよりもパフォーマンスが優れています。
ただし、人工知能開発サービスの提供に熱心な人にとっても、オプティマイザーの専門家であることが役立つことはめったにありません。 TensorFlowの最急降下法をどのように改善するかを理解するためだけに、開発者の時間を使ってカップルに慣れることをお勧めします。 その後、デフォルトでAdamを使用し、モデルが収束していない場合にのみ別のモデルを試すことができます。
これらのオプティマイザーがどのように、そしてなぜ機能するのかに本当に興味がある読者にとって、アニメーションが表示されるRuderの概要は、このトピックに関する最も優れた、最も網羅的なリソースの1つです。
オプティマイザーを使用するために、最初のセクションから線形回帰ソリューションを更新しましょう。 以下は、手動グラデーションを使用した元のグラデーション降下コードです。
# Manual gradient descent operations def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') for i in range(25): with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) gradients = tape.gradient(loss, [tf_slope, tf_icept]) tf_slope = tf_slope - learning_rate * gradients[0] tf_icept = tf_icept - learning_rate * gradients[1]
さて、これは代わりにオプティマイザーを使用した同じコードです。 余分なコードはほとんどないことがわかります(変更された行は青色で強調表示されています)。
# Gradient descent with Optimizer (RMSprop) def run_gradient_descent (heights, weights, init_slope, init_icept, learning_rate) : tf_slope = tf.Variable(init_slope, dtype= 'float32' ) tf_icept = tf.Variable(init_icept, dtype= 'float32' ) # Group trainable parameters into a list trainable_params = [tf_slope, tf_icept] # Define your optimizer (RMSprop) outside of the training loop optimizer = keras.optimizers.RMSprop(learning_rate) for i in range( 25 ): # GradientTape loop is the same with tf.GradientTape() as tape: tape.watch( trainable_params ) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors** 2 ) # We can use the trainable parameters list directly in gradient calcs gradients = tape.gradient(loss, trainable_params ) # Optimizers always aim to *minimize* the loss function optimizer.apply_gradients(zip(gradients, trainable_params))
それでおしまい! 勾配降下ループの外側でRMSprop
オプティマイザーを定義し、各勾配計算の後にoptimizer.apply_gradients()
メソッドを使用して、トレーニング可能なパラメーターを更新しました。 オプティマイザーは、運動量や高階微分などの追加の項を計算するための履歴勾配を追跡するため、ループの外側で定義されます。
RMSpropオプティマイザーでどのように見えるか見てみましょう。
素晴らしく見える! それでは、 Adamオプティマイザーで試してみましょう。
おっと、ここで何が起こったの? アダムの勢いのメカニズムが、最適なソリューションをオーバーシュートし、コースを複数回逆転させているようです。 通常、この運動量メカニズムは複雑な損失面に役立ちますが、この単純なケースでは私たちを傷つけます。 これは、モデルをトレーニングするときに調整するハイパーパラメーターの1つとしてオプティマイザーを選択するためのアドバイスを強調しています。
ディープラーニングを探求したい人は、このパターンに精通したいと思うでしょう。これは、標準のワークフローでは簡単にまとめられない複雑な損失メカニズムが必要なカスタムTensorFlowアーキテクチャで広く使用されているためです。 この単純なTensorFlow最急降下法の例では、トレーニング可能なパラメーターは2つしかありませんでしたが、何億ものパラメーターを含むアーキテクチャを操作する場合は、最適化する必要があります。
TensorFlowの最急降下法:最小値の検索からAIシステムの攻撃まで
すべてのコードスニペットと画像は、対応するGitHubリポジトリのノートブックから作成されました。 また、完全なコードを見たい読者のために、個々のノートブックへのリンクとともに、すべてのセクションの要約が含まれています。 メッセージを単純化するために、広範なインラインドキュメントに記載されている多くの詳細が省略されています。
この記事が洞察に満ちていて、TensorFlowで最急降下法を使用する方法について考えさせられたことを願っています。 自分で使用しない場合でも、モデルを作成し、損失関数を定義し、最急降下法を使用してモデルをデータセットに適合させるなど、最新のニューラルネットワークアーキテクチャがどのように機能するかが明確になることを願っています。
Google Cloudパートナーとして、ToptalのGoogle認定エキスパートは、最も重要なプロジェクトのオンデマンドで企業に提供されます。