By ロバート・キューブラー博士、データサイエンティスト
による写真 Jan Schulz#Webdesignerシュトゥットガルト on Unsplash
機械学習モデルを評価するにはいくつかのアプローチがあり、そのうちのXNUMXつは 精度 & 解釈可能性。高精度のモデルは、私たちが通常呼んでいるものです。 良い モデル、入力間の関係を学習しました X 出力 y よく。
モデルの解釈可能性または説明可能性が高い場合、モデルがどのように予測を行い、どのようにできるかを理解します 影響 入力特徴を変更することによるこの予測。 入力の特定の特徴を増減したときにディープニューラルネットワークの出力がどのように動作するかを言うのは難しいですが、線形モデルの場合、それは非常に簡単です。特徴をXNUMXつ増やすと、出力は係数だけ増加します。その機能の。 簡単。
さて、あなたはおそらくこのようなことをよく聞いたことがあるでしょう:
「解釈可能なモデルがあり、パフォーマンスの高いモデルがあります。」 —それをよく知らない人
ただし、私の記事を読んだことがある場合は 説明可能なブースティングマシン (EBM)、あなたはこれが真実ではないことをすでに知っています。 EBMは、解釈可能でありながら優れたパフォーマンスを発揮するモデルの一例です。
私の古い記事では、いくつかのモデルをに配置する方法を示す次の図を作成しました 解釈可能性-精度空間.
著者による画像。
特に、ディープニューラルネットワークを配置しました( 深いです)もっと 非常に正確ですが、説明するのは難しい 領域。 もちろん、次のようなライブラリを使用することで、解釈可能性の問題をある程度軽減できます。 SHAP or ライム、しかし、これらのアプローチには、独自の一連の仮定と問題が伴います。 それでは、別の道を歩み、この記事の設計で解釈可能なニューラルネットワークアーキテクチャを作成しましょう。
免責事項: 私が提示しようとしているアーキテクチャがちょうど頭に浮かびました。 それについての文献がすでにあるかどうかはわかりませんが、少なくとも何も見つかりませんでした。 しかし、私がここでやっていることをしている論文を知っているなら、私に知らせてください! それでは参考文献に載せておきます。
解釈可能なアーキテクチャのアイデア
フィードフォワードニューラルネットワークがどのように機能するかを知っていることを期待していることに注意してください。 すでに多くの優れたリソースがあるため、ここでは完全な紹介はしません。
XNUMXつの入力ノードを持つ次のおもちゃのニューラルネットワークについて考えてみます。 x₁、 x₂、 x₃、単一の出力ノード ŷ、およびそれぞれXNUMXつのノードを持つXNUMXつの隠れ層。 ここではバイアス用語を省略しました。
著者による画像。
解釈可能性に関するこのアーキテクチャの問題は、完全に接続されたレイヤーのために、入力がすべて完全に混合されることです。単一の入力ノードはすべて、すべての隠れ層ノードに影響を与えます。この影響は、ネットワークに深く入り込むほど複雑になります。
木からのインスピレーション
決定木は、制限しない場合、すべての機能を使用して分割を作成できる可能性があるため、通常、ツリーベースのモデルでも同じです。たとえば、標準的な勾配ブースティングとその派生物は次のようになります。 XGブースト, ライトGBM, キャットブースト それ自体では実際には解釈できません。
ただし、を使用して勾配ブースティングを解釈可能にすることができます 単一の特徴のみに依存する決定木、EBMで行われたように(それについての私の記事を読んでください!😎)。
このようにツリーを制限しても、多くの場合、パフォーマンスはそれほど低下しませんが、次のように機能の影響を視覚化できます。
の出力 解釈するのshow関数。著者による画像。
青い線でグラフィックの上部を見てください。 これは、いくつかの回帰問題の出力に対するfeature_4の影響を示しています。 に x-axis、feature_4の範囲を確認できます。 The y-軸は スコア、これは出力がどれだけ変化したかによる値です。 以下のヒストグラムは、feature_4の分布を示しています。
グラフィックから次のことがわかります。
- feature_4が約0.62の場合、feature_10が4または0.6であるのに比べて、出力は約0.65増加します。
- feature_4が0.66より大きい場合、出力への影響はマイナスになります。
- features_4を0.4〜0.56ビットの範囲で変更すると、出力が大幅に変更されます。
モデルの最終的な予測は、さまざまな特徴スコアの合計になります。 この動作はシャープレイ値に似ていますが、それらを計算する必要はありません。 素晴らしいですよね? それでは、ニューラルネットワークで同じことができる方法をお見せしましょう。
エッジを削除する
したがって、問題が、エッジが多すぎるためにニューラルネットワークの入力が隠れ層全体に散らばっているということである場合は、いくつかを削除してみましょう。 特に、あるフィーチャの情報が別のフィーチャに流れるようにするエッジを削除する必要があります。 これらのみを削除する こぼれるエッジ、上からのおもちゃのニューラルネットワークは次のようになります。
著者による画像。
XNUMXつの別々に作成しました ブロック XNUMXつの入力変数の場合、各ブロックは単一の部分出力を持つ完全に接続されたネットワークです。 ŷᵢ。 最後のステップとして、これら ŷᵢ が合計され、バイアス(図では省略)が追加されて最終出力が生成されます ŷ.
EBMで許可されているのと同じ種類のプロットを作成できるように、部分出力を導入しました。 上の図のXNUMXつのブロックで、XNUMXつのプロットが可能になります。 xᵢ 入ります、ŷᵢ 出てくる。 これを行う方法については後で説明します。
ここにすでに完全なアーキテクチャがあります!理論的には理解しやすいと思いますが、実装してみましょう。このように、ニューラルネットワークを使用できるのであなたは幸せであり、ニューラルネットワークは解釈可能であるのでビジネスは幸せです。
PyTorchでの実装
あなたが完全に精通しているとは思わない パイトーチ、それで私はあなたが私たちのカスタム実装を理解するのを助ける方法でいくつかの基本を説明します。 PyTorchの基本を知っている場合は、スキップできます 完全に接続されたレイヤー セクション。 PyTorchをインストールしていない場合は、 ここでバージョンを選択してください.
完全に接続されたレイヤー
これらのレイヤーは、 線形 PyTorchまたは 密集 in ケラス。 彼らは接続します n 入力ノード m を使用して出力ノード nm 乗算の重みを持つエッジ。 次のXNUMXつのコードスニペットでわかるように、これは基本的に行列の乗算とバイアス項の追加です。
import torchtorch.manual_seed(0) # keep things reproduciblex = torch.tensor([1., 2.]) # create an input array
linear_layer = torch.nn.Linear(2, 3) # define a linear layer
print(linear_layer(x)) # putting the input array into the layer# Output:
# tensor([ 0.7393, -1.0621, 0.0441], grad_fn=<AddBackward0>)
これは、完全に接続されたレイヤーを作成し、それらをPyTorchテンソルに適用する方法です。 乗算に使用される行列は、次の方法で取得できます。 linear_layer.weight
とバイアスを介して linear_layer.bias
。 その後、あなたはすることができます
print(linear_layer.weight @ x + linear_layer.bias) # @ = matrix mult# Output:
# tensor([ 0.7393, -1.0621, 0.0441], grad_fn=<AddBackward0>)
いいですね、同じです! さて、PyTorch、Keras、および共同についての大きな部分。 これらのレイヤーの多くを積み重ねてニューラルネットワークを作成できるということです。 PyTorchでは、このスタッキングを次の方法で実現できます。 torch.nn.Sequential
。 上から高密度ネットワークを再作成するには、簡単な方法を実行できます
model = torch.nn.Sequential( torch.nn.Linear(3, 6), torch.nn.ReLU(), torch.nn.Linear(6, 6), torch.nn.ReLU(), torch.nn.Linear(6, 6), torch.nn.ReLU(), torch.nn.Linear(6, 1),
)print(model(torch.randn(4, 3))) # feed it 4 random 3-dim. vectors
注: これまで、このネットワークをトレーニングする方法については説明していません。これは、パラメーターの初期化を含む、アーキテクチャーの定義にすぎません。 ただし、ネットワークにXNUMX次元入力を供給し、XNUMX次元出力を受け取ることができます。
独自のレイヤーを作成したいので、最初に簡単な方法で練習しましょう。PyTorchを再作成します。 Linear
層。 これがあなたがそれをすることができる方法です:
import torch
import mathclass MyLinearLayer(torch.nn.Module): def __init__(self, in_features, out_features): super().__init__() self.in_features = in_features self.out_features = out_features # multiplicative weights weights = torch.Tensor(out_features, in_features) self.weights = torch.nn.Parameter(weights) torch.nn.init.kaiming_uniform_(self.weights) # bias bias = torch.Tensor(out_features) self.bias = torch.nn.Parameter(bias) bound = 1 / math.sqrt(in_features) torch.nn.init.uniform_(self.bias, -bound, bound) def forward(self, x): return x @ self.weights.t() + self.bias
このコードは説明に値します。 最初の太字のブロックでは、線形層の重みを次のように紹介します。
- PyTorchテンソルの作成(すべてゼロを含みますが、これは重要ではありません)
- 学習可能なパラメータとしてレイヤーに登録します。これは、最急降下法がトレーニング中に更新できることを意味します。
- パラメータを初期化します。
ニューラルネットワークのパラメータの初期化はそれ自体がトピック全体であるため、うさぎの穴を掘り下げることはしません。 煩わしすぎる場合は、たとえば標準正規分布を使用して、別の方法で初期化することもできます。 torch.randn(out_features, in_features)
、しかし、トレーニングがそれより遅い可能性があります。 とにかく、バイアスについても同じことをします。
次に、レイヤーは、レイヤーで実行する必要のある数学演算を知る必要があります。 forward
方法。これは単なる線形演算、つまり行列の乗算とバイアスの加算です。
これで、解釈可能なニューラルネットワークのレイヤーを実装する準備が整いました。
線形レイヤーをブロックする
私たちは今、 BlockLinear
次のように使用するレイヤー:まず、 n 特徴。 NS BlockLinear
次に、レイヤーを作成する必要があります n で構成されるブロック h 隠されたニューロン。 物事を単純化するために、 h 各ブロックで同じですが、もちろんこれを一般化することができます。 合計すると、最初の隠れ層はで構成されます nh ニューロンだけでなく、 nh エッジはそれらに接続されています(代わりに n²h 完全に接続されたレイヤーの場合). それをよりよく理解するために、もう一度上からの写真を見てください。 ここ、 n = 3、 h = 2。
著者による画像。
次に、ReLUのような非線形性を使用した後、別の非線形性を配置します BlockLinear
異なるブロックを再度マージしてはならないため、このレイヤーの背後にあるレイヤー。 使用するまでこれを何度も繰り返します Linear
最後にレイヤーを作成して、すべてを再度結びます。
ブロック線形層の実装
コードに取り掛かりましょう。これは、カスタムメイドの線形レイヤーと非常によく似ているため、コードがそれほど威圧的であってはなりません。
class BlockLinear(torch.nn.Module): def __init__(self, n_blocks, in_features, out_features): super().__init__() self.n_blocks = n_blocks self.in_features = in_features self.out_features = out_features self.block_weights = [] self.block_biases = [] for i in range(n_blocks): block_weight = torch.Tensor(out_features, in_features) block_weight = torch.nn.Parameter(block_weight) torch.nn.init.kaiming_uniform_(block_weight) self.register_parameter( f'block_weight_{i}', block_weight ) self.block_weights.append(block_weight) block_bias = torch.Tensor(out_features) block_bias = torch.nn.Parameter(block_bias) bound = 1 / math.sqrt(in_features) torch.nn.init.uniform_(block_bias, -bound, bound) self.register_parameter( f'block_bias_{i}', block_bias ) self.block_biases.append(block_bias) def forward(self, x): block_size = x.size(1) // self.n_blocks x_blocks = torch.split( x, split_size_or_sections=block_size, dim=1 ) block_outputs = [] for block_id in range(self.n_blocks): block_outputs.append( x_blocks[block_id] @ self.block_weights[block_id].t() + self.block_biases[block_id] ) return torch.cat(block_outputs, dim=1)
数行をもう一度強調表示しました。最初の太い線は、自家製の線形レイヤーで見たものと似ていますが、繰り返します n_blocks
回数。 これにより、ブロックごとに独立した線形レイヤーが作成されます。
forwardメソッドでは、 x
最初に使用して再びブロックに分割する必要がある単一のテンソルとして torch.split
。 例として、ブロックサイズが2の場合、次のようになります。 [1, 2, 3, 4, 5, 6] -> [1, 2], [3, 4], [5, 6]
。 次に、独立した線形変換をさまざまなブロックに適用し、を使用して結果を接着します。 torch.cat
。 できた!
解釈可能なニューラルネットワークのトレーニング
これで、解釈可能なニューラルネットワークを定義するためのすべての要素が揃いました。 最初にデータセットを作成する必要があります。
X = torch.randn(1000, 3)
y = 3*X[:, 0] + 2*X[:, 1]**2 + X[:, 2]**3 + torch.randn(1000)
y = y.reshape(-1, 1)
ここでは、1個のサンプルで構成される2次元データセットを扱っていることがわかります。 フィーチャXNUMXとキューブフィーチャXNUMXを二乗すると、真の関係は線形になります。これが、モデルで復元したいものです。 それでは、この関係を捉えることができるはずの小さなモデルを定義しましょう。
class Model(torch.nn.Module): def __init__(self): super().__init__() self.features = torch.nn.Sequential( BlockLinear(3, 1, 20), torch.nn.ReLU(), BlockLinear(3, 20, 20), torch.nn.ReLU(), BlockLinear(3, 20, 20), torch.nn.ReLU(), BlockLinear(3, 20, 1), ) self.lr = torch.nn.Linear(3, 1) def forward(self, x): x_pre = self.features(x) return self.lr(x_pre) model = Model()
モデルをXNUMXつのステップに分割します。
- 部分出力の計算 ŷᵢ
self.features
その後 - 最終予測を計算する ŷ の加重和として ŷᵢ
self.lr
.
これにより、機能の説明を簡単に抽出できます。 の定義では self.features
データセットに20つの特徴があるため、XNUMXつのブロックでニューラルネットワークを作成していることがわかります。 ブロックごとに、ブロックごとにXNUMX個のニューロンを持つ多くの隠れ層を作成します。
これで、簡単なトレーニングループを作成できます。
optimizer = torch.optim.Adam(model.parameters())
criterion = torch.nn.MSELoss()for i in range(2000): optimizer.zero_grad() y_pred = model(X) loss = criterion(y, y_pred) loss.backward() optimizer.step() if i % 100 == 0: print(loss)
基本的に、オプティマイザーとしてAdamを選択し、損失としてMSEを選択してから、標準の勾配降下法を実行します。つまり、古い勾配を次のように消去します。 optimzer.zero_grad()
、予測を計算し、損失を計算し、次の方法で損失を区別します。 loss.backward()
を介してモデルパラメータを更新します optimizer.step()
。 時間の経過とともにトレーニング損失が減少することがわかります。 ここでは、検証やテストセットについては気にしません。 The トレーニング r²は最後に0.95より大きくする必要があります。
これで、モデルの説明を次のように印刷できます。
import matplotlib.pyplot as pltx = torch.linspace(-5, 5, 100).reshape(-1, 1)
x = torch.hstack(3*[x])for i in range(3): plt.plot( x[:, 0].detach().numpy(), model.get_submodule('lr').weight[0][i].item() * model.get_submodule('features')(x)[:, i].detach().numpy()) plt.title(f'Feature {i+1}') plt.show()
そして、get
著者による画像。
これはかなりきれいに見えます! モデルは、特徴1の影響が線形であり、特徴2の影響が二次であり、特徴3の影響が三次であることを示しています。 そしてそれだけでなく、 モデルはそれを私たちに見せることができます、これは全体の構造の素晴らしいところです!
ネットワークを破棄して、これらのグラフのみに基づいて予測を行うこともできます。
例として、ネットワークの出力を推定してみましょう。 x =(2、-2、0)。
- x₁= 2はに変換されます +5 最初の図に基づいて、予測のために。
- x₂= -2は、 +9 予測のために、XNUMX番目の図に基づいています。
- x₃= 0は +0 予測のために、XNUMX番目の図に基づいています。
- まだあります バイアス 経由でアクセスできる最後の線形レイヤーから
model.get_submodule('lr').bias
これも追加する必要がありますが、小さくする必要があります。
全体として、あなたの予測は周りにあるはずです ŷ ≈ 5 + 9 + 0+バイアス ≈ 14、これはかなり正確です。
また、出力を最小化するために何をしなければならないかを確認できます。機能1には小さな値を選択し、機能2にはゼロに近い値を選択し、機能3には小さな値を選択します。これは通常ニューラルネットワークを見ただけではわかりません。 、しかしスコア関数を使用すると、可能です。これは、解釈可能性の大きな利点のXNUMXつです。
上から学習したスコア関数は、私たちが 実際にトレーニングデータがありました。 私たちのデータセットでは、実際には各機能について-3から3の間の値しか観測されていません。 したがって、完璧にならなかったことがわかります x²および x³エッジ上の多項式。 でも、グラフの方向がほぼ正しいのは印象的だと思います。 これを十分に理解するには、EBMの結果と比較してください。
著者による画像。
曲線はブロック状であり、外挿は両側への直線にすぎません。これは、ツリーベースの方法の主な欠点のXNUMXつです。
まとめ
この記事では、モデルの解釈可能性と、ニューラルネットワークと勾配ブースティングがどのようにモデルを提供できないかについて説明しました。 解釈可能な勾配ブースティングアルゴリズムであるEBMを作成したのは、interpretmlパッケージの作成者でしたが、解釈可能なニューラルネットワークを作成する方法を紹介しました。
次に、それをPyTorchに実装しました。これは、コードが少し多かったのですが、それほどクレイジーではありませんでした。 EBMについては、特徴ごとに学習したスコア関数を抽出して、予測に使用することもできます。
実際のトレーニング済みモデルはもう必要ありません。これにより、弱いハードウェアに展開して使用することが可能になります。 これは、機能ごとにXNUMXつのルックアップテーブルを保存するだけで済み、メモリが少ないためです。 のグリッドサイズを使用する g ルックアップテーブルごとの結果 保存のみ O(n_特徴 * g)要素 潜在的に数百万または数十億のモデルパラメータの代わりに。予測を行うことも安価です。ルックアップテーブルからいくつかの数値を追加するだけです。これは 時間計算量のみ O(n_特徴) ルックアップと追加は、ネットワークを介したフォワードパスよりもはるかに高速です。
再度免責事項: これが斬新なアイデアかどうかはわかりませんが、とにかくここにあります!同じ考えを説明している論文をご存知でしたら、私にメッセージを残してください。それを参照します。
今日、あなたが何か新しく、興味深く、そして役に立つことを学んだことを願っています。 読んでくれてありがとう!
最後のポイントとして、あなたが
- 機械学習と
- とにかく中程度のサブスクリプションを取得する予定です、
やってみませんか このリンク経由で? これは私に大いに役立ちます! 😊
透明性を保つために、あなたの価格は変わりませんが、サブスクリプション料金の約半分は私に直接支払われます。
あなたが私をサポートすることを考えているなら、どうもありがとう!
ご不明な点がございましたら、 LinkedIn!
ロバート・キューブラー博士 Publicis Mediaのデータサイエンティストであり、Towards DataScienceの著者です。
元の。 許可を得て転載。
ソース:https://www.kdnuggets.com/2022/01/interpretable-neural-networks-pytorch.html