如何在Python中可视化和解释神经网络

来自菜鸟教程
跳转至:导航、​搜索

作者选择 Open Sourcing Mental Illness 作为 Write for DOnations 计划的一部分来接受捐赠。

介绍

神经网络在计算机视觉、自然语言处理和强化学习等许多领域实现了最先进的准确性。 然而,神经网络很复杂,很容易包含数十万甚至数百万的操作(MFLOPs 或 GFLOPs)。 这种复杂性使得解释神经网络变得困难。 例如:网络是如何得出最终预测的? 输入的哪些部分影响了预测? 对于像图像这样的高维输入,这种缺乏理解会更加严重:对图像分类的解释甚至是什么样的?

Explainable AI (XAI) 的研究通过多种不同的解释来回答这些问题。 在本教程中,您将专门探索两种类型的解释:1。 Saliency maps,突出输入图像最重要的部分; 和 2。 决策树,将每个预测分解为一系列中间决策。 对于这两种方法,您将生成从神经网络生成这些解释的代码。

在此过程中,您还将使用深度学习 Python 库 PyTorch、计算机视觉库 OpenCV 和线性代数库 numpy。 通过学习本教程,您将了解当前 XAI 为理解和可视化神经网络所做的努力。

先决条件

要完成本教程,您将需要以下内容:

  • 具有至少 1GB RAM 的 Python 3 本地开发环境。 您可以按照如何为Python 3安装和设置本地编程环境来配置您需要的一切。
  • 建议您查看构建基于情感的狗过滤器; 我们不会明确使用本教程,但如果需要,它会引入分类的概念。
  • 还建议您查看 Bias-Variance Tradeoff 以了解为什么模型复杂性不仅会损害可解释性,还会损害准确性。

您可以在 存储库 中找到本教程中的所有代码和资产。

第 1 步 - 创建您的项目并安装依赖项

让我们为此项目创建一个工作区并安装您需要的依赖项。 您将调用您的工作区 XAI,是 Explainable Artificial Intelligence 的缩写:

mkdir ~/XAI

导航到 XAI 目录:

cd ~/XAI

创建一个目录来保存您的所有资产:

mkdir ~/XAI/assets

然后为项目创建一个新的虚拟环境:

python3 -m venv xai

激活您的环境:

source xai/bin/activate

然后安装 PyTorch,这是您将在本教程中使用的 Python 深度学习框架。

在 macOS 上,使用以下命令安装 PyTorch:

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

在 Linux 和 Windows 上,对仅 CPU 构建使用以下命令:

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

现在为 OpenCVPillownumpy 安装预打包的二进制文件,它们分别是用于计算机视觉和线性代数的库。 OpenCVPillow 提供图像旋转等实用程序,numpy 提供线性代数实用程序,例如矩阵求逆:

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

sudo apt-get install libsm6 libxext6 libxrender-dev

最后,安装 nbdt,一个用于神经支持决策树的深度学习库,我们将在本教程的最后一步中讨论:

python -m pip install nbdt==0.0.4

安装依赖项后,让我们运行一个已经训练好的图像分类器。

第 2 步 — 运行预训练分类器

在这一步中,您将设置一个已经训练过的图像分类器。

首先, 图像分类器 接受图像作为输入,并输出预测的类别(如 CatDog)。 其次,pretained 表示该模型已经过训练,能够准确、直接地预测类别。 你的目标是可视化和解释这个图像分类器:它是如何做出决定的? 模型使用图像的哪些部分进行预测?

首先,下载一个 JSON 文件,将神经网络输出转换为人类可读的类名:

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 脚本,该脚本将加载图像,加载具有权重的神经网络,并使用神经网络对图像进行分类:

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

注意: 有关此文件 step_2_pretrained.py 的更详细演练,请参阅 How To Trick a Neural Network 教程中的 Step 2 - Running a Pretrained Animal Classifier


接下来,您还将下载以下猫和狗的图像,以在其上运行图像分类器。

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

最后,在新下载的图像上运行预训练的图像分类器:

python step_2_pretrained.py assets/catdog.jpg

这将产生以下输出,显示您的动物分类器按预期工作:

OutputPrediction: Persian cat

使用您的预训练模型运行推理就结束了。

尽管这个神经网络正确地产生了预测,但我们不明白模型是如何得出它的预测的。 为了更好地理解这一点,首先考虑您提供给图像分类器的猫和狗图像。

图像分类器预测 Persian cat。 您可以提出的一个问题是:模特是否在看左边的猫? 还是右边的狗? 模型使用哪些像素进行预测? 幸运的是,我们有一个可以回答这个确切问题的可视化。 以下是突出显示模型用于确定 Persian Cat 的像素的可视化。

模型通过观察猫将图像分类为 Persian cat。 在本教程中,我们将像此示例这样的可视化称为 显着性图 ,我们将其定义为突出显示影响最终预测的像素的热图。 显着图有两种类型:

  1. 与模型无关的显着图(通常称为“黑盒”方法):这些方法不需要访问模型权重。 一般来说,这些方法会改变图像并观察改变后的图像对精度的影响。 例如,您可以移除图像的中心(如下图所示)。 直觉是:如果图像分类器现在对图像进行了错误分类,那么图像中心一定很重要。 我们可以重复此操作并每次随机删除部分图像。 通过这种方式,我们可以像以前一样通过突出显示对准确性损害最大的补丁来生成热图。
  1. Model-aware Saliency Maps(通常称为“白盒”方法):这些方法需要访问模型的权重。 我们将在下一节中更详细地讨论这种方法。

我们对显着性图的简要概述到此结束。 在下一步中,您将实现一种称为类激活映射 (CAM) 的模型感知技术。

第三步——生成类激活图(CAM)

类激活图 (CAM) 是一种模型感知显着性方法。 要了解 CAM 是如何计算的,我们首先需要讨论分类网络中最后几层的作用。 下面是一个典型的图像分类神经网络的图示,用于这篇关于学习深度特征进行判别定位的 论文 中的方法。

该图描述了分类神经网络中的以下过程。 请注意,图像表示为一堆矩形; 有关如何将图像表示为张量的复习,请参阅 如何在 Python 3 中构建基于情绪的狗过滤器(步骤 4)

  1. 关注倒数第二层的输出,用蓝色、红色和绿色矩形标记为 LAST CONV
  2. 此输出经过全局平均池(表示为 GAP)。 GAP 平均每个通道(彩色矩形)中的值以产生单个值(相应的彩色框,在 LINEAR 中)。
  3. 最后,将这些值组合成一个加权和(权重由 w1w2w3 表示)以产生一个概率(深灰色框)班级。 在这种情况下,这些权重对应于 CAT。 本质上,每个 wi 都会回答:“ith 通道对于检测猫有多重要?”
  4. 对所有类(浅灰色圆圈)重复以获得所有类的概率。

我们省略了一些解释 CAM 不必要的细节。 现在,我们可以使用它来计算 CAM。 让我们重新审视这个图的扩展版本,仍然是同一个 论文 中的方法。 专注于第二行。

  1. 要计算类激活图,请取倒数第二层的输出。 这在第二行中进行了描述,由与第一行中相同颜色的矩形相对应的蓝色、红色和绿色矩形勾勒出来。
  2. 选一门课。 在这种情况下,我们选择“澳大利亚梗”。 找到该类对应的权重 w1, w2 ... wn
  3. 然后每个通道(彩色矩形)按 w1w2 ... wn 加权。 请注意,我们不执行全局平均池(上图中的第 2 步)。 计算加权和,以获得类激活图(最右边,图中的第二行)。

这个最终的加权和就是类激活图。

接下来,我们将实现类激活映射。 本节将分为我们已经讨论过的三个步骤:

  1. 取倒数第二层的输出。
  2. 找到权重 w1w2 ... wn
  3. 计算输出的加权和。

首先创建一个新文件 step_3_cam.py

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] 的一个参数。 然后,打开使用 Image.open 指定的图像。 接下来,您定义许多不同的转换以应用于传递给您的神经网络的图像:

  • transforms.Resize(224):将图像较小的一侧调整为 224。 例如,如果您的图像是 448 x 672,则此操作会将图像下采样为 224 x 336。
  • transforms.CenterCrop(224):从图像中心进行裁剪,大小为 224 x 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...,倒数第二层,我们稍后会用到。
  4. 添加“前向挂钩”功能。 该函数将在图层执行时保存图层的输出。 我们分两步执行此操作,首先定义一个 store_feature_map 钩子,然后将钩子与 register_forward_hook 绑定。
  5. 返回网络和倒数第二层。

模型加载到此结束。

接下来,计算类激活图本身。 在 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 函数反映了本节开头和前一节中概述的三个步骤。

  1. 取倒数第二层的输出,使用我们保存在 layer._parameters 中的前向钩子的特征映射。
  2. 在最后的线性层 net.fc_parameters['weight'] 中找到权重 w1w2 ... wn。 访问第 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 个 alpha 通道,并且 (2) 颜色通道按 BGR 排序。 我们使用索引 [..., 2::-1] 来解决这两个问题,丢弃 alpha 通道并将颜色通道顺序反转为 RGB。 最后,转换为无符号整数。
  2. 将图像 Image.fromarray 转换为 PIL 图像并使用图像的图像大小调整实用程序 .resize(...),然后使用 .save(...) 实用程序。
  3. 使用我们之前编写的实用程序 load_raw_image 加载原始图像。
  4. 通过添加每个的 0.5 权重,将热图叠加在图像顶部。 像以前一样,将结果转换为无符号整数 .astype(...)
  5. 最后将图片转换成PIL,保存。

接下来,用一些代码填充主函数,以在提供的图像上运行神经网络:

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 匹配。

然后,运行脚本:

python step_3_cam.py assets/catdog.jpg

您的脚本将输出以下内容:

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

这将生成 heatmap.jpgcombined.jpg 类似于以下显示热图和热图与猫/狗图像组合的图像。

你已经制作了你的第一张显着图。 我们将以更多链接和资源来结束本文,以生成其他类型的显着图。 同时,现在让我们探索第二种可解释性的方法——即使模型本身可解释。

第 4 步——使用神经支持的决策树

决策树属于基于规则的模型家族。 决策树是显示可能的决策路径的数据树。 每个预测都是一系列预测的结果。

每个预测不仅输出预测,还带有理由。 例如,要得出这个数字的“热狗”结论,模型必须首先问:“它有面包吗?”,然后问:“它有香肠吗?” 这些中间决策中的每一个都可以单独验证或质疑。 因此,经典机器学习将这些基于规则的系统称为“可解释的”。

一个问题是:这些规则是如何创建的? 决策树需要对其进行更详细的讨论,但简而言之,创建规则是为了“尽可能多地拆分类”。 形式上,这是“最大化信息增益”。 在极限情况下,最大化这种分裂是有意义的:如果规则完美地分裂了类,那么我们的最终预测将永远是正确的。

现在,我们继续使用神经网络和决策树混合。 有关决策树的更多信息,请参阅 分类和回归树 (CART) 概述

现在,我们将在神经网络和决策树混合体上运行推理。 正如我们将发现的,这为我们提供了一种不同类型的可解释性:直接模型可解释性。

首先创建一个名为 step_4_nbdt.py 的新文件:

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 中,首先使用称为 load_image_from_path 的自定义实用程序方法从提供的 URL 加载图像。 接下来,您定义许多不同的转换以应用于传递给您的神经网络的图像:

  • transforms.Resize(32):将图像较小的一侧调整为 32。 例如,如果您的图像是 448 x 672,则此操作会将图像下采样到 32 x 48。
  • transforms.CenterCrop(224):从图像中心进行裁剪,大小为 32 x 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。 然后,在两只宠物的早期照片上并排运行您的脚本。

python step_4_nbdt.py assets/catdog.jpg

这将输出以下预测和相应的理由。

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

神经支持的决策树部分到此结束。

结论

您现在已经运行了两种可解释的 AI 方法:一种像显着图这样的事后解释,以及一种使用基于规则的系统的修改后的可解释模型。

本教程未涵盖许多可解释性技术。 如需进一步阅读,请务必查看其他可视化和解释神经网络的方法; 实用程序数量众多,从调试到去偏到避免灾难性错误。 可解释人工智能 (XAI) 有许多应用,从医学等敏感应用到自动驾驶汽车中的其他关键任务系统。