CS231n/課題3/ネットワーク可視化(パイトーチ)

今回は、Stanford University/CS231n/Assignment3/Network Visualization(ネットワーク可視化)(PyTorch)をやる。

このノートブックでは新たな画像生成に画像勾配を活用する。モデルを訓練する時、モデル性能不満足度を測る損失関数を定義する。その後、モデルパラメーターに対する損失勾配を誤差逆伝播法を用いて算出し、損失を最小化するためにモデルパラメーターに勾配降下法を実行する。

ここでは若干違うことをやる。先ずはImageNet datasetで画像分類をするように事前訓練されている畳み込みニューラルネットワークモデルから始める。画像に対する現在の不満を数値化する損失関数を定義するのにこのモデルを使用し、次に、誤差逆伝播法を用いて、画像の画素に対するこの損失の勾配を計算する。その次に、モデルは固定したままにして、損失を最小化する新しい画像を合成するために元画像に勾配降下法を実行する。

このノートブックで、画像生成用の3つの技術を精査する。

  1. Saliency Maps:顕著性マップは、ネットワークが分類判定するのに画像のどの部分が影響を与えているのかを知るための手っ取り早い方法。
  2. Fooling Images:人間には同じに見えても、事前学習済みネットワークは誤分類するように入力画像に摂動を加えることができる。
  3. Class Visualization:ある特定のクラスの分類スコアを最大化するよう画像を合成できる。このことは、ネットワークがそのクラスの画像を分類する時に何を見ているのかの示唆を与えることができる。
import torch
from torch.autograd import Variable
import torchvision
import torchvision.transforms as T
import random
import numpy as np
from scipy.ndimage.filters import gaussian_filter1d
import matplotlib.pyplot as plt
from cs231n.image_utils import SQUEEZENET_MEAN, SQUEEZENET_STD
from PIL import Image

plt.rcParams['figure.figsize'] = (10.0, 8.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'
%matplotlib inline

Helper Functions

事前学習済みモデルは、色毎中間値を差し引いて色毎標準偏差で割って前処理された画像で訓練されている。この前処理を実行したり取り消したりするためのヘルパー関数を定義する。このセルでは何もする必要はない。

def preprocess(img, size=224):
    transform = T.Compose([
        T.Scale(size),
        T.ToTensor(),
        T.Normalize(mean=SQUEEZENET_MEAN.tolist(),
                    std=SQUEEZENET_STD.tolist()),
        T.Lambda(lambda x: x[None]),
    ])
    return transform(img)

def deprocess(img, should_rescale=True):
    transform = T.Compose([
        T.Lambda(lambda x: x[0]),
        T.Normalize(mean=[0, 0, 0], std=(1.0 / SQUEEZENET_STD).tolist()),
        T.Normalize(mean=(-SQUEEZENET_MEAN).tolist(), std=[1, 1, 1]),
        T.Lambda(rescale) if should_rescale else T.Lambda(lambda x: x),
        T.ToPILImage(),
    ])
    return transform(img)

def rescale(x):
    low, high = x.min(), x.max()
    x_rescaled = (x - low) / (high - low)
    return x_rescaled
    
def blur_image(X, sigma=1):
    X_np = X.cpu().clone().numpy()
    X_np = gaussian_filter1d(X_np, sigma, axis=2)
    X_np = gaussian_filter1d(X_np, sigma, axis=3)
    X.copy_(torch.Tensor(X_np).type_as(X))
    return X

Pretrained Model

全ての画像生成の試みは、ImageNetで画像分類するように事前学習された畳み込みニューラルネットワークを使って始める。ここではどんなモデルも使えるが、この課題の目的として、AlexNetに匹敵する正確度を実現する一方で有意に少ないパラメーター数と計算の複雑性を兼ね備えたSqueezeNet1を使う。

AlexNetやVGGやResNetではなくSqueezeNetを使うことで、画像生成実験を簡単にCPUを使って実行できる。PyTorch SqueezeNetモデルをTensorFlowに移植してあるので、このモデルのアーキテクチャをcs231n/classifiers/squeezenet.pyを見て確認する。

SqueezeNetを使用するには、先ずcs231n/datasetsディレクトリからsqueezenet_tf.shを実行して重みをダウンロードする必要がある。get_assignment3_data.shを既に実行している場合はSqueezeNetはダウンロード済み。Squeezenetモデルをダウンロードしたら、それを新しいTensorFlowセッションにロードすることができる。

# Download and load the pretrained SqueezeNet model.
model = torchvision.models.squeezenet1_1(pretrained=True)

# We don't want to train the model, so tell PyTorch not to compute gradients
# with respect to model parameters.
for param in model.parameters():
    param.requires_grad = False
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/torchvision-0.2.1-py3.6.egg/torchvision/models/squeezenet.py:94: UserWarning: nn.init.kaiming_uniform is now deprecated in favor of nn.init.kaiming_uniform_.
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/torchvision-0.2.1-py3.6.egg/torchvision/models/squeezenet.py:92: UserWarning: nn.init.normal is now deprecated in favor of nn.init.normal_.

Load some ImageNet images

ImageNet ILSVRC 2012 Classification datasetの検証セットからいくつかの標本画像を用意してある。これらの画像をダウンロードするには、cs231n/datasetsディレクトリからget_imagenet_val.shを実行する。

それらは検証セット由来の画像なので、今回使用する事前学習済みモデルは学習中にこれらの画像は見ていない。下のセルを実行して正解ラベルと一緒にいくつかの画像を見てみる。

from cs231n.data_utils import load_imagenet_val
X, y, class_names = load_imagenet_val(num=5)

plt.figure(figsize=(20, 10))
plt.rcParams["font.size"] = "15"
for i in range(5):
    plt.subplot(1, 5, i + 1)
    plt.imshow(X[i])
    plt.title(class_names[y[i]])
    plt.axis('off')
plt.gcf().tight_layout()

Saliency Maps

この事前学習済みモデルを使ってクラス顕著性マップを2のSection 3.1に記載のとおり計算する。

顕著性マップは、画像中の各ピクセルがその画像の分類スコアに影響を与える度合いを教えてくれる。それを計算するために、画像の画素に対する正しいクラス(スカラー)に対応する非正規化スコアの勾配を計算する。もし、画像の形式が(3, H, W)ならこの勾配も(3, H, W)の形を持つ。画像の各画素に対してこの勾配は、画素が僅か変化する場合の分類スコアの変化量を示してくれる。顕著性マップ算出には、先ずこの勾配の絶対値を受け取った後、3の入力チャンネルに対する最大値を受け取る。最終的な顕著性マップは、従って、(H, W)形状で全エントリーは非負になる。

Hint: PyTorch gather method

課題1では行列の各列から1要素を選ぶ必要があった。もし、sがshape(N,C)のnumpyアレイでyが整数0 <= y[i] < Cであるshape(N,)のnumpyアレイだとしたら、s[np.arange(N), y]は、yのインデックスを使ってsの各要素から1要素を選択するshape(N,)のnumpyアレイである。

PyTorchでは、同じ作業をgather()メソッドを用いて行ことができる。sがshape(N, C)のPyTorch TensorもしくはVariableで、yがrange 0 <= y[i] < Cのlongを含むshape (N,)のPyTorch TensorまたはVariableの場合、s.gather(1, y.view(-1, 1)).squeeze()は、yのインデックスに準じて選択されている、sの各列からの1入力を含むshape (N,)のPyTorchテンソル(もしくは変数)である。

下のセルを実行して標本を見る。また、gather methodsqueeze methodの参考文献を読むこともできる。

# Example of using gather to select one entry from each row in PyTorch
def gather_example():
    N, C = 4, 5
    s = torch.randn(N, C)
    y = torch.LongTensor([1, 2, 1, 3])
    print(s)
    print(y)
    print(s.gather(1, y.view(-1, 1)).squeeze())
gather_example()
tensor([[-0.2147, -1.4671,  0.2096, -1.8089,  0.6610],
        [ 0.4715, -0.3044, -2.0936,  0.0774,  3.4376],
        [ 0.5039,  0.3455,  0.4498,  0.9687,  0.0499],
        [ 0.6218,  0.4381,  0.1037,  0.8305, -1.6266]])
tensor([ 1,  2,  1,  3])
tensor([-1.4671, -2.0936,  0.3455,  0.8305])
def compute_saliency_maps(X, y, model):
    """
    Compute a class saliency map using the model for images X and labels y.

    Input:
    - X: Input images; Tensor of shape (N, 3, H, W)
    - y: Labels for X; LongTensor of shape (N,)
    - model: A pretrained CNN that will be used to compute the saliency map.

    Returns:
    - saliency: A Tensor of shape (N, H, W) giving the saliency maps for the input
    images.
    """
    # Make sure the model is in "test" mode
    model.eval()
    
    # Wrap the input tensors in Variables
    X_var = Variable(X, requires_grad=True)
    y_var = Variable(y)
    saliency = None
    ##############################################################################
    # TODO: Implement this function. Perform a forward and backward pass through #
    # the model to compute the gradient of the correct class score with respect  #
    # to each input image. You first want to compute the loss over the correct   #
    # scores, and then compute the gradients with a backward pass.               #
    ##############################################################################
    scores = model(X_var)
    scores = scores.gather(1, y_var.view(-1, 1)).squeeze()
    loss = -torch.sum(torch.log(scores))
    loss.backward()
    saliency = X_var.grad.data
    saliency = saliency.abs()
    saliency, idx = saliency.max(dim=1)
    ##############################################################################
    #                             END OF YOUR CODE                               #
    ##############################################################################
    return saliency.squeeze()

上のセルでの実装が終わったら、下を実行して、ImageNet検証セットからの標本画像のclass saliency mapをいくつか見てみる。

def show_saliency_maps(X, y):
    # Convert X and y from numpy arrays to Torch Tensors
    X_tensor = torch.cat([preprocess(Image.fromarray(x)) for x in X], dim=0)
    y_tensor = torch.LongTensor(y)

    # Compute saliency maps for images in X
    saliency = compute_saliency_maps(X_tensor, y_tensor, model)

    # Convert the saliency map from Torch Tensor to numpy array and show images
    # and saliency maps together.
    saliency = saliency.numpy()
    N = X.shape[0]
    for i in range(N):
        plt.subplot(2, N, i + 1)
        plt.imshow(X[i])
        plt.axis('off')
        plt.title(class_names[y[i]])
        plt.subplot(2, N, N + i + 1)
        plt.imshow(saliency[i], cmap=plt.cm.hot)
        plt.axis('off')
        plt.gcf().set_size_inches(22, 8)
    plt.show()

show_saliency_maps(X, y)
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/torchvision-0.2.1-py3.6.egg/torchvision/transforms/transforms.py:188: UserWarning: The use of the transforms.Scale transform is deprecated, please use transforms.Resize instead.

Fooling Images

3の中で示唆されているように”fooling images”を生成するのに画像勾配を使うことができる。イメージとターゲットクラスが与えられれば、対象クラスを極大化するために画像に対して勾配上昇法を実行でき、ネットワークがターゲットクラスとしてイメージを分類する場合は停止できる。fooling imagesを生成するために下記の関数を実装する。

def make_fooling_image(X, target_y, model):
    """
    Generate a fooling image that is close to X, but that the model classifies
    as target_y.

    Inputs:
    - X: Input image; Tensor of shape (1, 3, 224, 224)
    - target_y: An integer in the range [0, 1000)
    - model: A pretrained CNN

    Returns:
    - X_fooling: An image that is close to X, but that is classifed as target_y
    by the model.
    """
    # Initialize our fooling image to the input image, and wrap it in a Variable.
    X_fooling = X.clone()
    X_fooling_var = Variable(X_fooling, requires_grad=True)
    
    learning_rate = 1
    ##############################################################################
    # TODO: Generate a fooling image X_fooling that the model will classify as   #
    # the class target_y. You should perform gradient ascent on the score of the #
    # target class, stopping when the model is fooled.                           #
    # When computing an update step, first normalize the gradient:               #
    #   dX = learning_rate * g / ||g||_2                                         #
    #                                                                            #
    # You should write a training loop.                                          #
    #                                                                            #
    # HINT: For most examples, you should be able to generate a fooling image    #
    # in fewer than 100 iterations of gradient ascent.                           #
    # You can print your progress over iterations to check your algorithm.       #
    ##############################################################################
    while True:
        scores = model(X_fooling_var)
        pred_idx = scores.data.max(dim=1)[1][0]
        if pred_idx!=target_y:
            scores[:,target_y].backward()
            grad_img = X_fooling_var.grad.data
            dX = learning_rate*grad_img/torch.norm(grad_img, 2)
            X_fooling += dX
            X_fooling_var.grad.data.zero_()
        else:
            break
    ##############################################################################
    #                             END OF YOUR CODE                               #
    ##############################################################################
    return X_fooling

下を実行してfooling image(騙し画像)を生成する。

idx = 0
target_y = 6

X_tensor = torch.cat([preprocess(Image.fromarray(x)) for x in X], dim=0)
X_fooling = make_fooling_image(X_tensor[idx:idx+1], target_y, model)

scores = model(Variable(X_fooling))
assert target_y == scores.data.max(1)[1][0], 'The model is not fooled!'
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/torchvision-0.2.1-py3.6.egg/torchvision/transforms/transforms.py:188: UserWarning: The use of the transforms.Scale transform is deprecated, please use transforms.Resize instead.

騙し画像を生成したら、下のセルを実行して元画像、騙し画像、さらに、それら2つの差異画像を表示する。

X_fooling_np = deprocess(X_fooling.clone())
X_fooling_np = np.asarray(X_fooling_np).astype(np.uint8)

plt.subplot(1, 4, 1)
plt.imshow(X[idx])
plt.title(class_names[y[idx]])
plt.axis('off')

plt.subplot(1, 4, 2)
plt.imshow(X_fooling_np)
plt.title(class_names[target_y])
plt.axis('off')

plt.subplot(1, 4, 3)
X_pre = preprocess(Image.fromarray(X[idx]))
diff = np.asarray(deprocess(X_fooling - X_pre, should_rescale=False))
plt.imshow(diff)
plt.title('Difference')
plt.axis('off')

plt.subplot(1, 4, 4)
diff = np.asarray(deprocess(10 * (X_fooling - X_pre), should_rescale=False))
plt.imshow(diff)
plt.title('Magnified difference (10x)')
plt.axis('off')

plt.gcf().set_size_inches(22, 7)
plt.rcParams["font.size"] = "20"
plt.show()
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/torchvision-0.2.1-py3.6.egg/torchvision/transforms/transforms.py:188: UserWarning: The use of the transforms.Scale transform is deprecated, please use transforms.Resize instead.

Class visualization

random noise image(不規則雑音画像)からスタートして、target classに勾配上昇法を実行することで、ネットワークがターゲットクラスとして認識する画像を生成することができる。この考えは、初めに4でプレゼンされた。5がこの考えを生成された画像の質を向上できるいくつかの正則化技術を提唱することで拡張した。

具体的に、$I$を画像、$y$をターゲットクラス、$s_y(I)$を、畳み込みネットワークがクラス$y$の画像$I$に割り当てるスコアにする。これらは、クラス確率ではなく生非正規化スコアであることに留意する。出来れば、$R$が(恐らくは陰であろう)regularizer(argmaxの$R(I)$の符号に留意:この正則化項は極小化したい)である問題$$I^* = \arg\max_I s_y(I) – R(I)$$を解くことでクラス$y$に対して高スコアを実現する画像$I^*$を生成したい。この最適化問題は勾配上昇法を使って生成された画像に対する勾配を計算することで解くことができる。式$$R(I) = \lambda \|I\|_2^2$$の(陽の)L2正則化と陰の正則化を6で示唆されているように生成された画像を周期的にぼかすことで使用する。この問題は生成された画像に勾配上昇法を用いて解くことができる。

下のセルのcreate_class_visualization関数の実装を完成させる。

def jitter(X, ox, oy):
    """
    Helper function to randomly jitter an image.
    
    Inputs
    - X: PyTorch Tensor of shape (N, C, H, W)
    - ox, oy: Integers giving number of pixels to jitter along W and H axes
    
    Returns: A new PyTorch Tensor of shape (N, C, H, W)
    """
    if ox != 0:
        left = X[:, :, :, :-ox]
        right = X[:, :, :, -ox:]
        X = torch.cat([right, left], dim=3)
    if oy != 0:
        top = X[:, :, :-oy]
        bottom = X[:, :, -oy:]
        X = torch.cat([bottom, top], dim=2)
    return X
def create_class_visualization(target_y, model, dtype, **kwargs):
    """
    Generate an image to maximize the score of target_y under a pretrained model.
    
    Inputs:
    - target_y: Integer in the range [0, 1000) giving the index of the class
    - model: A pretrained CNN that will be used to generate the image
    - dtype: Torch datatype to use for computations
    
    Keyword arguments:
    - l2_reg: Strength of L2 regularization on the image
    - learning_rate: How big of a step to take
    - num_iterations: How many iterations to use
    - blur_every: How often to blur the image as an implicit regularizer
    - max_jitter: How much to gjitter the image as an implicit regularizer
    - show_every: How often to show the intermediate result
    """
    model.type(dtype)
    l2_reg = kwargs.pop('l2_reg', 1e-3)
    learning_rate = kwargs.pop('learning_rate', 25)
    num_iterations = kwargs.pop('num_iterations', 100)
    blur_every = kwargs.pop('blur_every', 10)
    max_jitter = kwargs.pop('max_jitter', 16)
    show_every = kwargs.pop('show_every', 25)

    # Randomly initialize the image as a PyTorch Tensor, and also wrap it in
    # a PyTorch Variable.
    img = torch.randn(1, 3, 224, 224).mul_(1.0).type(dtype)
    img_var = Variable(img, requires_grad=True)

    for t in range(num_iterations):
        # Randomly jitter the image a bit; this gives slightly nicer results
        ox, oy = random.randint(0, max_jitter), random.randint(0, max_jitter)
        img.copy_(jitter(img, ox, oy))

        ########################################################################
        # TODO: Use the model to compute the gradient of the score for the     #
        # class target_y with respect to the pixels of the image, and make a   #
        # gradient step on the image using the learning rate. Don't forget the #
        # L2 regularization term!                                              #
        # Be very careful about the signs of elements in your code.            #
        ########################################################################
        scores = model(img_var)
        scores[:,target_y].backward()
        grad = img_var.grad.data - 2*l2_reg*img
        img += learning_rate*grad
        img_var.grad.data.zero_()
        ########################################################################
        #                             END OF YOUR CODE                         #
        ########################################################################
        
        # Undo the random jitter
        img.copy_(jitter(img, -ox, -oy))

        # As regularizer, clamp and periodically blur the image
        for c in range(3):
            lo = float(-SQUEEZENET_MEAN[c] / SQUEEZENET_STD[c])
            hi = float((1.0 - SQUEEZENET_MEAN[c]) / SQUEEZENET_STD[c])
            img[:, c].clamp_(min=lo, max=hi)
        if t % blur_every == 0:
            blur_image(img, sigma=0.5)
        
        # Periodically show the image
        if t == 0 or (t + 1) % show_every == 0 or t == num_iterations - 1:
            plt.imshow(deprocess(img.clone().cpu()))
            class_name = class_names[target_y]
            plt.title('%s\nIteration %d / %d' % (class_name, t + 1, num_iterations))
            plt.gcf().set_size_inches(8, 8)
            plt.axis('off')
            plt.show()

    return deprocess(img.cpu())

上のセルへの実装が終わったら下のセルを実行してタランチュラの画像を生成する。

dtype = torch.FloatTensor
# dtype = torch.cuda.FloatTensor # Uncomment this to use GPU
model.type(dtype)

target_y = 76 # Tarantula
# target_y = 78 # Tick
# target_y = 187 # Yorkshire Terrier
# target_y = 683 # Oboe
# target_y = 366 # Gorilla
# target_y = 604 # Hourglass
out = create_class_visualization(target_y, model, dtype)

他のクラスでもクラス可視化を試す。生成される画像の質を高めるのに各種ハイパーパラメーターを自由にいじっても良いが、これはあくまでも任意。

# target_y = 78 # Tick
# target_y = 187 # Yorkshire Terrier
# target_y = 683 # Oboe
# target_y = 366 # Gorilla
# target_y = 604 # Hourglass
target_y = np.random.randint(1000)
print(class_names[target_y])
X = create_class_visualization(target_y, model, dtype)
cello, violoncello
参考サイトhttps://github.com/

  1. Iandola et al, “SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and > 0.5MB model size,” arXiv 2016
  2. Karen Simonyan, Andrea Vedaldi, and Andrew Zisserman. “Deep Inside Convolutional Networks: Visualising Image Classification Models and Saliency Maps”, ICLR Workshop 2014.
  3. Szegedy et al, “Intriguing properties of neural networks”, ICLR 2014
  4. Karen Simonyan, Andrea Vedaldi, and Andrew Zisserman. “Deep Inside Convolutional Networks: Visualising Image Classification Models and Saliency Maps”, ICLR Workshop 2014.
  5. Yosinski et al, “Understanding Neural Networks Through Deep Visualization”, ICML 2015 Deep Learning Workshop
  6. Yosinski et al, “Understanding Neural Networks Through Deep Visualization”, ICML 2015 Deep Learning Workshop