著者は、 Write for DOnations プログラムの一環として、 Open Sourcing MentalIllnessを選択して寄付を受け取りました。

序章

ニューラルネットワークは、コンピュータービジョン、自然言語処理、強化学習などの多くの分野で最先端の精度を実現します。 ただし、ニューラルネットワークは複雑で、数十万、さらには数百万の操作(MFLOPまたはGFLOPS)を簡単に含むことができます。 この複雑さにより、ニューラルネットワークの解釈が困難になります。 例:ネットワークはどのようにして最終予測に到達しましたか? 入力のどの部分が予測に影響を与えましたか? この理解の欠如は、画像のような高次元の入力では悪化します。画像分類の説明はどのように見えるでしょうか。

Explainable AI(XAI)の調査は、さまざまな説明でこれらの質問に答えるために機能します。 このチュートリアルでは、2種類の説明を具体的に説明します。1。 顕著性マップ。入力画像の最も重要な部分を強調表示します。 および2。 決定木。各予測を一連の中間決定に分解します。 これらのアプローチの両方について、ニューラルネットワークからこれらの説明を生成するコードを作成します。

途中で、ディープラーニングのPythonライブラリも使用します PyTorch、コンピュータビジョンライブラリ OpenCV、および線形代数ライブラリ numpy. このチュートリアルに従うことで、ニューラルネットワークを理解して視覚化するための現在のXAIの取り組みを理解することができます。

前提条件

このチュートリアルを完了するには、次のものが必要です。

このチュートリアルのすべてのコードとアセットは、このリポジトリにあります。

ステップ1—プロジェクトの作成と依存関係のインストール

このプロジェクトのワークスペースを作成し、必要な依存関係をインストールしましょう。 ワークスペースを呼び出します XAI、略して Explainable Artificial Intelligence:

  1. mkdir ~/XAI

に移動します XAI ディレクトリ:

  1. cd ~/XAI

すべてのアセットを保持するディレクトリを作成します。

  1. mkdir ~/XAI/assets

次に、プロジェクトの新しい仮想環境を作成します。

  1. python3 -m venv xai

環境をアクティブ化します。

  1. source xai/bin/activate

次に、このチュートリアルで使用するPythonのディープラーニングフレームワークであるPyTorchをインストールします。

macOSでは、次のコマンドを使用してPyTorchをインストールします。

  1. python -m pip install torch==1.4.0 torchvision==0.5.0

LinuxおよびWindowsでは、CPUのみのビルドに次のコマンドを使用します。

  1. pip install torch==1.4.0+cpu torchvision==0.5.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
  2. pip install torchvision

次に、事前にパッケージ化されたバイナリをインストールします OpenCV, Pillow、 と numpy、それぞれコンピュータビジョンと線形代数のライブラリです。 OpenCVPillow 画像の回転などのユーティリティを提供し、 numpy 行列反転などの線形代数ユーティリティを提供します。

  1. python -m pip install opencv-python==3.4.3.18 pillow==7.1.0 numpy==1.14.5 matplotlib==3.3.2

Linuxディストリビューションでは、インストールする必要があります libSM.so:

  1. sudo apt-get install libsm6 libxext6 libxrender-dev

最後に、インストールします nbdt、神経に裏打ちされた決定木の深層学習ライブラリ。これについては、このチュートリアルの最後のステップで説明します。

  1. python -m pip install nbdt==0.0.4

依存関係をインストールした状態で、すでにトレーニングされている画像分類器を実行してみましょう。

ステップ2—事前トレーニング済み分類子の実行

このステップでは、すでにトレーニングされた画像分類器を設定します。

まず、画像分類器は画像を入力として受け取り、予測されたクラスを出力します( Cat また Dog). 次に、 pretained は、このモデルがすでにトレーニングされており、クラスを正確に、すぐに予測できることを意味します。 あなたの目標は、この画像分類器を視覚化して解釈することです。それはどのように決定を下しますか? モデルは画像のどの部分を予測に使用しましたか?

まず、JSONファイルをダウンロードして、ニューラルネットワークの出力を人間が読める形式のクラス名に変換します。

  1. wget -O assets/imagenet_idx_to_label.json https://raw.githubusercontent.com/do-community/tricking-neural-networks/master/utils/imagenet_idx_to_label.json

次のPythonスクリプトをダウンロードします。このスクリプトは、画像を読み込み、その重みを使用してニューラルネットワークを読み込み、ニューラルネットワークを使用して画像を分類します。

  1. wget https://raw.githubusercontent.com/do-community/tricking-neural-networks/master/step_2_pretrained.py

注:このファイルの詳細なウォークスルーについて step_2_pretrained.py、「ニューラルネットワークをだます方法」チュートリアルのステップ2 —事前トレーニングされた動物分類器の実行を参照してください。

次に、次の猫と犬の画像もダウンロードして、画像分類子を実行します。

  1. wget -O assets/catdog.jpg https://assets.digitalocean.com/articles/visualize_neural_network/step2b.jpg

最後に、新しくダウンロードした画像に対して事前トレーニング済みの画像分類子を実行します。

  1. python step_2_pretrained.py assets/catdog.jpg

これにより、次の出力が生成され、動物分類子が期待どおりに機能することが示されます。

Output
Prediction: Persian cat

これで、事前にトレーニングされたモデルで推論を実行することはできます。

このニューラルネットワークは予測を正しく生成しますが、モデルがどのようにして予測に到達したのかはわかりません。 これをよりよく理解するために、画像分類子に提供した猫と犬の画像を検討することから始めます。

画像分類器は予測します Persian cat. あなたが尋ねることができる1つの質問は次のとおりです:モデルは左側の猫を見ていましたか? それとも右側の犬? モデルはその予測を行うためにどのピクセルを使用しましたか? 幸い、この正確な質問に答える視覚化があります。 以下は、モデルが決定するために使用したピクセルを強調表示する視覚化です Persian Cat.

モデルは画像を次のように分類します Persian cat 猫を見ることによって。 このチュートリアルでは、この例のような視覚化を顕著性マップと呼びます。これは、最終的な予測に影響を与えるピクセルを強調表示するヒートマップとして定義されます。 顕著性マップには2つのタイプがあります。

  1. モデルにとらわれない顕著性マップ(「ブラックボックス」メソッドと呼ばれることが多い):これらのアプローチでは、モデルの重みにアクセスする必要はありません。 一般に、これらの方法は画像を変更し、変更された画像が精度に与える影響を観察します。 たとえば、画像の中央を削除することができます(次の図を参照)。 直感は次のとおりです。画像分類器が画像を誤って分類するようになった場合、画像の中心が重要であったに違いありません。 これを繰り返して、毎回画像の一部をランダムに削除することができます。 このように、精度を最も損なうパッチを強調表示することで、以前のようにヒートマップを作成できます。
  1. モデル対応の顕著性マップ(「ホワイトボックス」メソッドと呼ばれることが多い):これらのアプローチでは、モデルの重みにアクセスする必要があります。 このような方法の1つについては、次のセクションで詳しく説明します。

これで、顕著性マップの概要は終わりです。 次のステップでは、クラスアクティベーションマップ(CAM)と呼ばれるモデル対応の手法を実装します。

ステップ3—クラスアクティベーションマップ(CAM)の生成

クラスアクティベーションマップ(CAM)は、モデル対応の顕著性メソッドの一種です。 CAMがどのように計算されるかを理解するには、まず、分類ネットワークの最後の数層が何をするかについて説明する必要があります。 以下は、識別的ローカリゼーションのための深い特徴の学習に関するこの論文の方法について、典型的な画像分類ニューラルネットワークの図解です。

この図は、分類ニューラルネットワークでの次のプロセスを示しています。 画像は長方形のスタックとして表されていることに注意してください。 画像がテンソルとしてどのように表現されるかについての復習については、 Python 3で感情ベースの犬のフィルターを構築する方法(ステップ4)を参照してください。

  1. 青、赤、緑の長方形で LASTCONVというラベルの付いた最後から2番目のレイヤーの出力に注目してください。
  2. この出力は、グローバル平均プール( GAP として示されます)を受けます。 GAP は、各チャネル(色付きの長方形)の値を平均して、単一の値( LINEAR の対応する色付きのボックス)を生成します。
  3. 最後に、これらの値が加重和( w1 w2 w3 で示される加重)で結合され、確率(濃い灰色のボックス)が生成されます。クラス。 この場合、これらの重みはCATに対応します。 本質的に、各 wi は、「猫を検出するために ith チャネルはどれほど重要ですか?」と答えます。
  4. すべてのクラス(薄い灰色の円)に対して繰り返して、すべてのクラスの確率を取得します。

CAMを説明するために必要ではないいくつかの詳細を省略しました。 これで、これを使用してCAMを計算できます。 同じのメソッドについても、この図の拡張バージョンをもう一度見てみましょう。 2行目に注目してください。

  1. クラスアクティベーションマップを計算するには、最後から2番目のレイヤーの出力を取得します。 これは2番目の行に示され、最初の行の同じ色の長方形に対応する青、赤、および緑の長方形で囲まれています。
  2. クラスを選択します。 この場合、「オーストラリアンテリア」を選びます。 そのクラスに対応する重みw1w2wnを見つけます。
  3. 次に、各チャネル(色付きの長方形)は、 w1 w2wnによって重み付けされます。 グローバル平均プールを実行しないことに注意してください(前の図のステップ2)。 加重和を計算して、クラスアクティベーションマップを取得します(右端、図の2行目)。

この最終的な加重和は、クラスアクティベーションマップです。

次に、クラスアクティベーションマップを実装します。 このセクションは、すでに説明した3つのステップに分かれています。

  1. 最後から2番目のレイヤーの出力を取得します。
  2. 重みを見つける w1, w2wn.
  3. 出力の加重和を計算します。

新しいファイルを作成することから始めます step_3_cam.py:

  1. nano step_3_cam.py

まず、Pythonボイラープレートを追加します。 必要なパッケージをインポートし、宣言します main 関数:

step_3_cam.py
"""Generate Class Activation Maps"""
import numpy as np
import sys
import torch
import torchvision.models as models
import torchvision.transforms as transforms
import matplotlib.cm as cm

from PIL import Image
from step_2_pretrained import load_image


def main():
    pass


if __name__ == '__main__':
    main()

画像の読み込み、サイズ変更、トリミングを行う画像ローダーを作成しますが、色は変更しません。 これにより、画像のサイズが正しくなります。 あなたの前にこれを追加してください main 関数:

step_3_cam.py
. . .
def load_raw_image():
    """Load raw 224x224 center crop of image"""
    image = Image.open(sys.argv[1])
    transform = transforms.Compose([
      transforms.Resize(224),  # resize smaller side of image to 224
      transforms.CenterCrop(224),  # take center 224x224 crop
    ])
    return transform(image)
. . .

load_raw_image、最初にスクリプトに渡された1つの引数にアクセスします sys.argv[1]. 次に、を使用して指定された画像を開きます Image.open. 次に、ニューラルネットワークに渡される画像に適用するさまざまな変換を定義します。

  • transforms.Resize(224):画像の小さい方のサイズを224に変更します。 たとえば、画像が448 x 672の場合、この操作は画像を224×336にダウンサンプリングします。
  • transforms.CenterCrop(224):画像の中央からサイズ224×224の切り抜きを取ります。
  • transform(image):前の行で定義された一連の画像変換を適用します。

これで画像の読み込みは完了です。

次に、事前トレーニング済みモデルをロードします。 最初の後にこの関数を追加します load_raw_image 機能しますが、 main 関数:

step_3_cam.py
. . .
def get_model():
    """Get model, set forward hook to save second-to-last layer's output"""
    net = models.resnet18(pretrained=True).eval()
    layer = net.layer4[1].conv2

    def store_feature_map(self, _, output):
        self._parameters['out'] = output
    layer.register_forward_hook(store_feature_map)

    return net, layer
. . .

の中に get_model 機能、あなた:

  1. 事前トレーニング済みモデルをインスタンス化する models.resnet18(pretrained=True).
  2. を呼び出して、モデルの推論モードをevalに変更します .eval().
  3. 定義 layer...、最後から2番目のレイヤー。後で使用します。
  4. 「フォワードフック」機能を追加します。 この関数は、レイヤーの実行時にレイヤーの出力を保存します。 これは2つのステップで行い、最初に store_feature_map フックしてからフックをバインドします register_forward_hook.
  5. ネットワークと最後から2番目のレイヤーの両方を返します。

これでモデルの読み込みは完了です。

次に、クラスアクティベーションマップ自体を計算します。 この関数をあなたの前に追加してください main 関数:

step_3_cam.py
. . .
def compute_cam(net, layer, pred):
    """Compute class activation maps

    :param net: network that ran inference
    :param layer: layer to compute cam on
    :param int pred: prediction to compute cam for
    """

    # 1. get second-to-last-layer output
    features = layer._parameters['out'][0]

    # 2. get weights w_1, w_2, ... w_n
    weights = net.fc._parameters['weight'][pred]

    # 3. compute weighted sum of output
    cam = (features.T * weights).sum(2)

    # normalize cam
    cam -= cam.min()
    cam /= cam.max()
    cam = cam.detach().numpy()
    return cam
. . .

The compute_cam 関数は、このセクションの冒頭と前のセクションで概説した3つのステップを反映しています。

  1. 保存されたフォワードフックの機能マップを使用して、最後から2番目のレイヤーの出力を取得します layer._parameters.
  2. 重みを見つける w1, w2wn 最後の線形層で net.fc_parameters['weight']. アクセスする pred予測されたクラスの重みを取得するための重みの行。
  3. 出力の加重和を計算します。 (features.T * weights).sum(...). 議論 2 インデックスに沿って合計を計算することを意味します 2 提供されたテンソルの寸法。
  4. すべての値が0から1の間に収まるように、クラスアクティベーションマップを正規化します—cam -= cam.min(); cam /= cam.max().
  5. 計算グラフからPyTorchテンソルを切り離します .detach(). CAMをPyTorchテンソルオブジェクトからnumpy配列に変換します。 .numpy().

これで、クラスアクティベーションマップの計算は終了です。

最後のヘルパー関数は、クラスアクティベーションマップを保存するユーティリティです。 この関数をあなたの前に追加してください main 関数:

step_3_cam.py
. . .
def save_cam(cam):
    # save heatmap
    heatmap = (cm.jet_r(cam) * 255.0)[..., 2::-1].astype(np.uint8)
    heatmap = Image.fromarray(heatmap).resize((224, 224))
    heatmap.save('heatmap.jpg')
    print(' * Wrote heatmap to heatmap.jpg')

    # save heatmap on image
    image = load_raw_image()
    combined = (np.array(image) * 0.5 + np.array(heatmap) * 0.5).astype(np.uint8)
    Image.fromarray(combined).save('combined.jpg')
    print(' * Wrote heatmap on image to combined.jpg')
. . .

このユーティリティ save_cam 以下を実行します。

  1. ヒートマップを色付けする cm.jet_r(cam). 出力は範囲内です [0, 1] で乗算します 255.0. さらに、出力には(1)4番目のアルファチャネルが含まれ、(2)カラーチャネルはBGRとして順序付けられます。 インデックスを使用します [..., 2::-1] 両方の問題を解決するには、アルファチャネルを削除し、カラーチャネルの順序をRGBに反転します。 最後に、符号なし整数にキャストします。
  2. 画像を変換する Image.fromarray PIL画像に変換し、画像の画像サイズ変更ユーティリティを使用します .resize(...)、 そうして .save(...) 効用。
  3. ユーティリティを使用して、生の画像をロードします load_raw_image 以前に書きました。
  4. 追加して、ヒートマップを画像の上に重ねます 0.5 それぞれの重量。 前と同じように、結果を符号なし整数にキャストします .astype(...).
  5. 最後に、画像をPILに変換し、保存します。

次に、提供された画像でニューラルネットワークを実行するためのコードをmain関数に入力します。

step_3_cam.py
. . .
def main():
    """Generate CAM for network's predicted class"""
    x = load_image()
    net, layer = get_model()

    out = net(x)
    _, (pred,) = torch.max(out, 1)  # get class with highest probability

    cam = compute_cam(net, layer, pred)
    save_cam(cam)
. . .

main、ネットワークを実行して予測を取得します。

  1. 画像をロードします。
  2. 事前にトレーニングされたニューラルネットワークをフェッチします。
  3. 画像上でニューラルネットワークを実行します。
  4. で最も高い確率を見つける torch.max. pred これで、最も可能性の高いクラスのインデックスを持つ数値になります。
  5. を使用してCAMを計算する compute_cam.
  6. 最後に、を使用してCAMを保存します save_cam.

これで、クラスアクティベーションスクリプトは終了です。 ファイルを保存して閉じます。 スクリプトが step_3_cam.py このリポジトリ

次に、スクリプトを実行します。

  1. python step_3_cam.py assets/catdog.jpg

スクリプトは次を出力します。

Output
* Wrote heatmap to heatmap.jpg * Wrote heatmap on image to combined.jpg

これにより、 heatmap.jpgcombined.jpg ヒートマップと、猫/犬の画像と組み合わせたヒートマップを示す次の画像に似ています。

これで、最初の顕著性マップが作成されました。 他の種類の顕著性マップを生成するためのより多くのリンクとリソースで記事を終了します。 それまでの間、説明可能性への2番目のアプローチ、つまりモデル自体を解釈可能にする方法を探りましょう。

ステップ4—ニューラルに裏打ちされた決定木の使用

デシジョンツリーは、ルールベースのモデルのファミリーに属しています。 デシジョンツリーは、可能なデシジョンパスウェイを表示するデータツリーです。 各予測は、一連の予測の結果です。

予測を出力するだけでなく、各予測には正当化も含まれます。 たとえば、この図の「ホットドッグ」の結論に到達するには、モデルは最初に「パンはありますか?」と尋ね、次に「ソーセージはありますか?」と尋ねる必要があります。 これらの中間決定のそれぞれは、個別に検証または異議を申し立てることができます。 その結果、従来の機械学習では、これらのルールベースのシステムを「解釈可能」と呼んでいます。

1つの質問は、これらのルールはどのように作成されるのかということです。 デシジョンツリーは、それ自体のはるかに詳細な議論を保証しますが、要するに、「クラスを可能な限り分割する」ためのルールが作成されます。 正式には、これは「情報獲得の最大化」です。 限界では、この分割を最大化することは理にかなっています。ルールがクラスを完全に分割する場合、最終的な予測は常に正しいものになります。

次に、ニューラルネットワークとディシジョンツリーハイブリッドの使用に移ります。 デシジョンツリーの詳細については、分類および回帰ツリー(CART)の概要を参照してください。

次に、ニューラルネットワークとデシジョンツリーハイブリッドで推論を実行します。 私たちが見つけるように、これは私たちに異なるタイプの説明可能性を与えます:直接モデルの解釈可能性。

と呼ばれる新しいファイルを作成することから始めます step_4_nbdt.py:

  1. nano step_4_nbdt.py

まず、Pythonボイラープレートを追加します。 必要なパッケージをインポートし、宣言します main 関数。 maybe_install_wordnet プログラムに必要な前提条件を設定します。

step_4_nbdt.py
"""Run evaluation on a single image, using an NBDT"""

from nbdt.model import SoftNBDT, HardNBDT
from pytorchcv.models.wrn_cifar import wrn28_10_cifar10
from torchvision import transforms
from nbdt.utils import DATASET_TO_CLASSES, load_image_from_path, maybe_install_wordnet
import sys

maybe_install_wordnet()


def main():
    pass


if __name__ == '__main__':
    main()

前と同じように、事前にトレーニングされたモデルをロードすることから始めます。 あなたの前に以下を追加してください main 関数:

step_4_nbdt.py
. . .
def get_model():
    """Load pretrained NBDT"""
    model = wrn28_10_cifar10()
    model = HardNBDT(
      pretrained=True,
      dataset='CIFAR10',
      arch='wrn28_10_cifar10',
      model=model)
    return model
. . .

この関数は次のことを行います。

  1. WideResNetという新しいモデルを作成します wrn28_10_cifar10().
  2. 次に、モデルを次のようにラップすることにより、そのモデルのニューラルに裏打ちされた決定木のバリアントを作成します。 HardNBDT(..., model=model).

これでモデルの読み込みは完了です。

次に、モデル推論のために画像をロードして前処理します。 あなたの前に以下を追加してください main 関数:

step_4_nbdt.py
. . .
def load_image():
    """Load + transform image"""
    assert len(sys.argv) > 1, "Need to pass image URL or image path as argument"
    im = load_image_from_path(sys.argv[1])
    transform = transforms.Compose([
      transforms.Resize(32),
      transforms.CenterCrop(32),
      transforms.ToTensor(),
      transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
    ])
    x = transform(im)[None]
    return x
. . .

load_image、と呼ばれるカスタムユーティリティメソッドを使用して、提供されたURLから画像をロードすることから始めます load_image_from_path. 次に、ニューラルネットワークに渡される画像に適用するさまざまな変換を定義します。

  • transforms.Resize(32):画像の小さい方のサイズを32に変更します。 たとえば、画像が448 x 672の場合、この操作は画像を32×48にダウンサンプリングします。
  • transforms.CenterCrop(224):画像の中央から32×32のサイズの切り抜きを取ります。
  • transforms.ToTensor():画像をPyTorchテンソルに変換します。 すべてのPyTorchモデルでは、入力としてPyTorchテンソルが必要です。
  • transforms.Normalize(mean=..., std=...):平均を減算し、標準偏差で除算することにより、入力を標準化します。 これについては、torchvisionのドキュメントで詳しく説明されています。

最後に、画像変換を画像に適用します transform(im)[None].

次に、効用関数を定義して、予測とそれに至るまでの中間決定の両方をログに記録します。 これをあなたの前に置いてください main 関数:

step_4_nbdt.py
. . .
def print_explanation(outputs, decisions):
    """Print the prediction and decisions"""
    _, predicted = outputs.max(1)
    cls = DATASET_TO_CLASSES['CIFAR10'][predicted[0]]
    print('Prediction:', cls, '// Decisions:', ', '.join([
        '{} ({:.2f}%)'.format(info['name'], info['prob'] * 100) for info in decisions[0]
    ][1:]))  # [1:] to skip the root
. . .

The print_explanations 関数は、予測と決定を計算してログに記録します。

  1. 最も確率の高いクラスのインデックスを計算することから始めます outputs.max(1).
  2. 次に、辞書を使用して、その予測を人間が読めるクラス名に変換します DATASET_TO_CLASSES['CIFAR10'][predicted[0]].
  3. 最後に、予測を出力します cls と決定 info['name'], info['prob']....

を入力してスクリプトを終了します main これまでに作成したユーティリティを使用して:

step_4_nbdt.py
. . .
def main():
    model = get_model()
    x = load_image()
    outputs, decisions = model.forward_with_decisions(x)  # use `model(x)` to obtain just logits
    print_explanation(outputs, decisions)

いくつかのステップで説明付きのモデル推論を実行します。

  1. モデルをロードします get_model.
  2. 画像を読み込む load_image.
  3. モデル推論を実行する model.forward_with_decisions.
  4. 最後に、予測と説明を印刷します print_explanations.

ファイルを閉じて、ファイルの内容がstep_4_nbdt.pyと一致することを再確認します。 次に、2匹のペットの前の写真を並べてスクリプトを実行します。

  1. python step_4_nbdt.py assets/catdog.jpg

これにより、予測と対応する正当化の両方が次のように出力されます。

Output
Prediction: cat // Decisions: animal (99.34%), chordate (92.79%), carnivore (99.15%), cat (99.53%)

これで、ニューラルに裏打ちされた決定木のセクションは終わりです。

結論

これで、2種類の説明可能なAIアプローチを実行しました。顕著性マップのような事後説明と、ルールベースのシステムを使用した修正された解釈可能なモデルです。

このチュートリアルでカバーされていない多くの説明可能なテクニックがあります。 詳細については、ニューラルネットワークを視覚化して解釈する他の方法を確認してください。 ユーティリティは、デバッグからバイアス除去、壊滅的なエラーの回避まで、数多くあります。 Explainable AI(XAI)には、医療などの機密性の高いアプリケーションから、自動運転車の他のミッションクリティカルなシステムまで、多くのアプリケーションがあります。