著者は、 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

次に、OpenCVPillow、およびnumpyのパッケージ済みバイナリをインストールします。これらは、それぞれコンピュータービジョンと線形代数のライブラリです。 OpenCVおよびPillowは画像回転などのユーティリティを提供し、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—事前にトレーニングされた分類器を実行する

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

まず、画像分類器は画像を入力として受け入れ、予測されたクラス(CatDogなど)を出力します。 次に、 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 —事前トレーニング済み動物分類子の実行を参照してください。

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

Image of Cat and dog on sofa

  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

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

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

Image of Cat and dog on sofa

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

A visualization that highlights pixels that the model used

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

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

A heatmap highlighting the patches that damaged accuracy the most.

  1. モデル対応の顕著性マップ(「ホワイトボックス」メソッドと呼ばれることが多い):これらのアプローチでは、モデルの重みにアクセスする必要があります。 このような方法の1つについては、次のセクションで詳しく説明します。

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

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

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

Diagram of an existing image classification neural network.

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

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

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

Diagram of how class activation maps are computed from an image classification neural network.

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

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

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

  1. 最後から2番目のレイヤーの出力を取得します。
  2. 重みw1w2wnを検索します。
  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では、最初にスクリプトsys.argv[1]に渡された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. 最後から2番目のレイヤーであるlayer...を定義します。これは、後で使用します。
  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
. . .

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

  1. layer._parametersに保存されているフォワードフックの機能マップを使用して、最後から2番目のレイヤーの出力を取得します。
  2. 最終線形層net.fc_parameters['weight']で重みw1w2wnを見つけます。 pred番目の重みの行にアクセスして、予測されたクラスの重みを取得します。
  3. 出力の加重和を計算します。 (features.T * weights).sum(...)。 引数2は、提供されたテンソルのインデックス2次元に沿って合計を計算することを意味します。
  4. すべての値が0から1の間にあるようにクラスアクティベーションマップを正規化します—cam -= cam.min(); cam /= cam.max()
  5. 計算グラフ.detach()からPyTorchテンソルを切り離します。 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. compute_camを使用してCAMを計算します。
  6. 最後に、save_camを使用して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が生成されます。

Heatmap highlighting "important" pixels that the neural network is looking at, to classify the image. a.k.a., "saliency map"
Saliency map superimposed on top of the original image

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

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

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

Decision tree for hot dog, burger, super burger, waffle fries

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

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

  • 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
. . .

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)には、医療などの機密性の高いアプリケーションから、自動運転車の他のミッションクリティカルなシステムまで、多くのアプリケーションがあります。