nykergoto’s blog

機械学習とpythonをメインに、雑多な内容をとりとめなく扱うブログです。

Pytorch で DCGAN をつくったよ

DCGAN を二年ぶりに実装しました。

github.com

いつも MNIST も面白くないので、 MNIST にアルファベットが加わった EMNIST をデータセットとして用いました。

www.nist.gov

f:id:dette:20180502060329p:plain
Emnist の画像例

こんな感じの画像が10万オーダーで格納されています。大きさは Mnist と同じ (28, 28) の grayscale の画像です。

DCGAN とは

DCGAN はニューラルネットワークの生成モデルである GAN (Generative Adversarial Networks) の一種です。 DCGANでは画像を生成するモデル (generator) だけではなく、画像が偽物(generatorが生成したもの)かデータセット上にある本物の画像かを判定するモデル (dicriminator) も同時に訓練するという枠組みのモデルです。こうすることで生成モデルはより判別モデルを騙すような高度な画像を生成し、それに応じて判別モデルはより精緻に偽画像を判定するようになり、競い合いながら学習が進んでいきます。

アルゴリズムに関しては以前の記事とかぶるので DCGANをChainerで実装 - nykergoto’s blog や、とても良くまとまっているこちら など参考にしていただけると良いかなと思います。

じっそう

pytorh での実装について。 まずはモデル部分から。 generator / dicsriminator ともに似たような感じなので generator だけ

class Generator(nn.Module):
    """
    ランダムベクトルから shape = (1, 28, 28) の画像を生成するモジュール
    Note: はじめ Linear な部分には Batchnorm1d を入れていなかったが学習が Discriminator に全く追いつかず崩壊した。
    Generator のネットワーク構成はシビアっぽい
    """

    def __init__(self, z_dim=100):
        super().__init__()
        self.z_dim = z_dim

        self.flatten_dim = 5 ** 2 * 256
        self.fc = nn.Sequential(*[
            nn.Linear(z_dim, 1024),
            nn.BatchNorm1d(1024),
            nn.ReLU(True),
            nn.Linear(1024, self.flatten_dim),
            nn.BatchNorm1d(self.flatten_dim),
            nn.ReLU(True)
        ])

        self.upsample = nn.Sequential(
            make_layer(256, 128, kernel=3, stride=1, padding=0, deconv=True), # 5 -> 7
            make_layer(128, 64, kernel=4, stride=2, deconv=True), # 7 -> 14
            nn.ConvTranspose2d(64, 1, kernel_size=4, stride=2, padding=1) # 14 -> 28
        )

    def forward(self, input_tensor):
        h = self.fc(input_tensor)
        h = h.view(-1, 256, 5, 5)
        x = self.upsample(h)
        x = F.sigmoid(x)
        return x

工夫があるとすれば make_layer という形で nn.Module を返すヘルパ関数を用意した、という点でしょうか。

def make_layer(in_channels, out_channels, kernel=2, stride=2, padding=1, use_batchnorm=True, deconv=False):
    layers = []

    if deconv:
        conv = nn.ConvTranspose2d(in_channels, out_channels, kernel_size=kernel, stride=stride, padding=padding)
    else:
        conv = nn.Conv2d(in_channels, out_channels, kernel_size=kernel, stride=stride, padding=padding)

    layers.append(conv)
    if use_batchnorm:
        bn = nn.BatchNorm2d(out_channels)
        layers.append(bn)
    activate = nn.ReLU(True)
    layers.append(activate)

    return nn.Sequential(*layers)

今回は3層程度なので直接記述しても大したことは無いですが、 resnet50 のように大きなネットワークになるとこのようにある塊をひとつの関数で作るようにすることは必要になるのかなーと思っています。(このやり方で良いのかは微妙ですが……みんなどうしているのだろうか…)

つぎに学習を実行する Solver クラスを作ってみました。

class Solver(object):
    """
    abstract class for solver
    """

    def __init__(self, models, device=None):
        """
        :param tuple[torch.nn.Module] models: 学習中に重みが更新されるモデルの配列
        :param str device: train 時のデバイス. `"cuda"` or `"cpu"`
        """
        self.models = models

        if isinstance(device, str):
            if device == "cuda" and torch.cuda.is_available():
                self.device = torch.device("cuda")
            else:
                self.device = torch.device("cpu")
        else:
            self.device = device

        self.callbacks = None

    def start_epoch(self, epoch):
        for model in self.models:
            model.to(self.device)
            model.train()

        for c in self.callbacks:
            c.initialize(self.models, self.device)
            c.start_epoch(epoch)

    def end_epoch(self, epoch, logs):
        for c in self.callbacks:
            c.end_epoch(epoch, logs)

    def fit(self, train_iterator, valid_iterator=None, epochs=10, callbacks=None, initial_lr=0.01,
            optimizer_params=None):
        """
        訓練データを用いてモデルの fitting を行う
        :param train_iterator: Iterable Object. 各 Iteration で (x_train, label_train) の tuple を返す
        :param valid_iterator:
        :param int epochs:
        :param list[Callback] callbacks:
        :param float initial_lr:
        :param dict | None optimizer_params:
        :return:
        """
        if optimizer_params is None:
            optimizer_params = {}
        self.set_optimizers(lr=initial_lr, params=optimizer_params)
        self.callbacks = callbacks

        for epoch in range(1, epochs + 1):
            self.start_epoch(epoch)
            logs = self.train(train_iterator)
            self.end_epoch(epoch, logs)
        return self

    def set_optimizers(self, lr, params):
        raise NotImplementedError()

    def train(self, train_iterator):
        logs = {}
        count = 0
        total = len(train_iterator)
        for batch_idx, (x, _) in tqdm(enumerate(train_iterator), total=total):
            count += 1
            logs = self._forward_backward_core(x, _, logs)

        for k, v in logs.items():
            if "loss" in k:
                logs[k] /= count
        return logs

    def _forward_backward_core(self, x, t, logs):
        raise NotImplementedError()

このクラスを作ったのにはちょっと理由があって、 pytorch のチュートリアルでは大概のものが epoch を引数にとって学習を行うクラス def train(epoch) のみを実行して model 部分などは global スコープにある変数を参照する形式をとっていてとても気持ちが悪かった、というのがあります。(なんとなく関数の引数以外の変数を使うのってこわくないですか)

で前使っていた keras を参考に training ループを回す部分とエポックの始まり終わりの定期実行を実装したものを書いてみた、というのが先の Solver クラスです。

最適化モデルの設定や forward-backward の定義はモデルや学習スキームによって変化するのでその部分は継承クラスで定義、という形式をとっています。

で、実装してみて思ったことなのですが……
結局こうやって枠にはめてしまうと pytorch の持っている柔軟なネットワークの構成ができる利点が薄れてしまってあまり良くないなあという印象を持ちました。

そもそも、こういう型にはめて記述できるものは keras などもっと便利に隠匿してくれるパッケージで書くことができますしそちらのほうが楽なんだろうなーということです。 pytorch を使うような場合にはそれこそ def train(epoch), def test(epoch) のように関数ベースで定義してループもごりごり書くほうが結果としてわかりやすくなるのかもしれません。 この辺はまだ pytorch 初心者なのでもっと良い書き方があるのだろうなとは思っているのでいろんなコードを読んで勉強していきたいなという所存です。

実行結果

最後になりましたが実行結果を載せます。

f:id:dette:20180502055515g:plain
実行結果 0 - 100 epoch

今回はGPUがあったので無事現実的時間内に生成できました。楽しい。

まとめ

  • pytorch 書き方がよくわからない
  • GAN は楽しい

Pytorch ことはじめ

0.40 の update もできて話題の pytorch を触り始めたのでその時のメモ。

出てくるコードはすべて以下のリポジトリ/notebooks/start で見ることができます。

github.com

参考資料

インストール

公式を見ると conda を用いてインストールするように言われるのでおとなしく従います。

conda install -y pytorch torchvision cuda91 -c python

ubuntu16.04 + pyenv + miniconda3-4.1.11 の環境ではすんなりとインストールできました。
docker つかって環境分離できたほうがよいよなあと思い公式の DockerImage をビルドしようとしたもののどうにも上手くビルドができなかった。(どうやら pytorch 公式が Docker まわり適当に放置しているみたい?) conda でインストールするバージョンの Dockerfile を作ってみた のでこちらからイメージを作成しても良いかなと思います。

Pytorch のきほん

pytorch で扱うベクトルは torch.Tensor クラスのものを用います。 このクラスには numpy っぽいメソッドがぞろぞろと定義されていて、基本的に numpy と同じ操作ができると思ってOKです。 スライス操作とかもできるので numpy に慣れている人からするととてもありがたい。

>>> x = torch.randn(2, 3)
>>> x.__class__
<class 'torch.Tensor'>
>>> x.shape
torch.Size([2, 3])

>>> x.reshape(-1)
tensor([ 1.5101, -0.4492, -0.1451,  0.8803,  0.0047, -0.0675])

>>> x[None, :]
tensor([[[ 1.5101, -0.4492, -0.1451],
         [ 0.8803,  0.0047, -0.0675]]])

# 行列積の計算は tensor.mm
>>> y = torch.randn(2, 3)
>>> y.mm(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: size mismatch, m1: [2 x 3], m2: [2 x 3] at /opt/conda/conda-bld/pytorch_1524585239153/work/aten/src/TH/generic/THTensorMath.c:2033
>>> y.mm(x.t())
tensor([[ 0.0598,  0.1263],
        [ 0.6540, -0.0636]])

ただ負の数のステップ幅のスライスには対応していないみたい。(逆順に取ってきたい時の x[::-1] が使えない)

>>> x[::-1, :]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: negative step not yet supported

もしやりたい時には

  1. 一回 numpy array に変換してからスライス操作をかけて
  2. そのオブジェクトをコピー
  3. コピーされた numpy array を torch.Tensor に変換

というステップを取る必要がある。 ちょっとめんどくさい。 コピーをしないと

>>> torch.from_numpy(x.numpy()[::-1, :].copy())
tensor([[ 0.8803,  0.0047, -0.0675],
        [ 1.5101, -0.4492, -0.1451]])

このステップ2のコピーをするところをサボると負の値でのスライスを検知してエラーになってしまうので注意。(将来のリリースで対応するよ!っていうメッセージがでる)

>>> torch.from_numpy(x.numpy()[::-1, :])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: some of the strides of a given numpy array are negative. This is currently not supported, but will be added in future releases.

ニューラルネットワークを作ってみる

では実際にニューラルネットワークを構築してみましょう。 想定するニューラルネットワークは 100 次元の実数値 $x \in \mathbb{R}^{100}$ を入力にとり、1次元の実数予測値 $\hat{y} \in \mathbb{R}$ を返すようなものです。

$$ \begin{align} z = W_1^T x \\ h = {\rm relu}(z) \\ \hat{y} = W_2^T h \end{align} $$

ここで $W_1 \in \mathbb{R}^{100 \times 1000}, W_2 \in \mathbb{R}^{10 \times 100}$ はそれぞれネットワークの重みを指します。

最初から pytorch の便利モジュールを使うとあれなので

  1. 手計算で更新
  2. Autograd をつかって更新
  3. torch.nn.Module & optim つかって更新

というふうにステップを経て実装を強化して行きます。

手計算で更新

行列計算ができるので、重みに対する backward をチェーンルールをつかって手計算で出して、その更新ルールにしたがって重みをアップデートする、というパワープレイから。 今回のモデルに対する勾配はロス関数を RMSE として

$$ f(y, \hat{y}) = \| y - \hat{y} \|^2 $$

とすると重みに対する勾配は以下のようになります。

$$ \begin{align} \frac{\partial f}{\partial W_2} &= - 2 (y - \hat{y}) h \\ \frac{\partial f}{\partial W_1} &= (- 2 (y - \hat{y}) W_1 {\rm diag}(H_0(h)) x )^T \\ &= -2 x^T (y - \hat{y}) W_1 {\rm diag}(H_0(h)) \end{align} $$

なのでこれにしたがって更新するように自分で書けばOK。*1

dtype = torch.float
use_gpu = False

if use_gpu:
    device = torch.device("cuda:0")
else:
    device = torch.device("cpu")
variable_types = {
    "device": device,
    "dtype": dtype
}

# ネットワーク形状の定義
batch_size = 100
input_dim = 100
hidden_dim = 1000
outpu_dim = 10

x = torch.randn(batch_size, input_dim, **variable_types)
y = torch.randn(batch_size, outpu_dim, **variable_types)

# 重みの初期値はランダムに設定
w1 = torch.randn(input_dim, hidden_dim, **variable_types)
w2 = torch.randn(hidden_dim, outpu_dim, **variable_types)

epochs = 1000
losses = []
for i in range(epochs):
    z = x.mm(w1)

    # relu activetion
    a = z.clamp(min=0)
    pred = a.mm(w2)

    loss = ((pred - y) ** 2.).sum()
    losses.append(loss.item())

    grad_pred = 2 * (pred - y)
    grad_w2 = a.t().mm(grad_pred)
    grad_a = grad_pred.mm(w2.t())
    grad_a[z < 0] = 0
    grad_w1 = x.t().mm(grad_a)

    w1 -= lr * grad_w1 
    w2 -= lr * grad_w2
    
    if i % 200 == 0:
        lr *= .5

Autograd を使う

先の例では自分で勾配を計算していました。 pytorch には計算グラフから自動的に勾配を計算する機能 (autograd) が実装されています。 この機能を使うと予測値と正解ラベルとのロスを計算する順伝搬だけ計算すれば勾配の部分 (backward) を自動的に算出してくれます。

使うためにはテンソルの定義部分で require_grad=True にすればOKです。

autograd をつかった勾配法の実装は以下のようになりました。先程のように具体的に勾配を計算する必要が無いためシンプルに記述できています。

variable_types = {
    "device": device,
    "dtype": dtype
}

variable_types["requires_grad"] = True

w1 = torch.randn(input_dim, hidden_dim, **variable_types)
w2 = torch.randn(hidden_dim, outpu_dim, **variable_types)

lr = 1e-6

losses = []
for epoch in range(epochs):
    
    pred = x.mm(w1).clamp(min=0).mm(w2)
    loss = (y - pred).pow(2).sum()
    
    # loss に対して backward を呼び出すと grad が自動計算される
    loss.backward()
    losses.append(loss.item())

    # 以下で重みを update するがこの時の変更は autograd に追跡されたくない。
    # `with torch.no_grad()` の部分では追跡を行わないようになる。
    # 他の方法としては tensor の値を直接変更するという方法がある
    # ex). w1.data -= lr* w1.grad.data
    # しかしこの方法だと履歴を追跡できなくなる
    with torch.no_grad():
        w1 -= lr * w1.grad
        w2 -= lr * w2.grad
        
        # 破壊的に勾配の値を 0 にする
        # 破壊的な変更は suffix に _ がつく
        w1.grad.zero_()
        w2.grad.zero_()

torch.nn.Module & optim をつかって更新

最後に torch.nn.Module と optim を使って重みの更新部分を隠匿してみましょう。

torch.nn.Module の使い方はこのクラスを継承して super().__init__() を呼び出すだけです。 以下は今回考えているニューラルネットワークの定義を Module を使って定義したものです。

class SimpleNNModel(torch.nn.Module):
    """
    二層のニューラルネットワークモデル"""
    
    def __init__(self, input_dim=100, output_dim=10, hidden_dim=1000):
        super().__init__()
        
        self.dense1 = torch.nn.Linear(input_dim, hidden_dim, bias=False)
        self.dense2 = torch.nn.Linear(hidden_dim, outpu_dim, bias=False)
        
    def forward(self, x):
        """
        順伝搬の計算を行って予測値を返す"""
        
        h = self.dense1(x)
        
        # equal to relu activation
        h = h.clamp(min=0)
        
        pred = self.dense2(h)
        return pred

継承したクラスでは Module に定義されている様々なメソッドを利用可能です。例えば toインスタンス内のデータすべてを cpu or gpu のメモリに展開する手助けをします。

model = SimpleNNModel()
model = model.to(device)

あとはパラメータを列挙したり, プロパティ内の Module を列挙したりもできます

for c in model.children():
    print(c)

# Linear(in_features=100, out_features=1000, bias=False)
# Linear(in_features=1000, out_features=10, bias=False)

for p in model.parameters():
    print(p)

# Parameter containing:
# tensor(1.00000e-02 *
#        [[ 7.6395, -6.7308,  9.9475,  ...,  1.5201, -3.2150, -0.1836],
#         [-8.0257,  9.9207,  2.4222,  ...,  4.2445,  8.2807,  7.5381],
#         [ 3.3367,  0.5269, -0.0468,  ...,  2.7539,  9.4210,  7.7625],
#         ...,
#         [ 6.5938, -3.1973, -0.3842,  ..., -3.0842,  6.5831,  4.6253],
#         [ 0.4635, -2.5175, -9.1168,  ...,  5.4131, -5.4361, -6.6949],
#         [ 2.5444,  9.0936, -3.0305,  ...,  6.1671,  4.1751, -3.2943]])
# Parameter containing:
# tensor(1.00000e-02 *
#        [[-3.0642,  3.1571,  1.1157,  ...,  2.1682,  2.4327, -1.7542],
#         [ 0.6330, -3.1313, -1.7368,  ...,  2.4449, -2.3944,  2.3621],
#         [-0.6492,  1.6452,  0.9604,  ..., -2.4719, -0.7905,  2.3031],
#         ...,
#         [-1.0288, -3.1262, -1.0839,  ..., -0.7796, -2.2369,  2.7909],
#         [ 0.0137, -2.7335,  2.7928,  ..., -1.3900,  0.6610,  2.2154],
#         [-3.1476, -2.1980, -1.7415,  ..., -2.2474, -2.0570, -2.9524]])

optim モジュールには optimizer が多数定義されています。 基本的に第一引数にその optimizer で更新したいパラメータの配列をわたし、その他の optimizer 自体のパラメータをそれに続いて渡す、というふうにインスタンスを生成します。

Module には parameters メソッドでパラメータ列挙ができるので、組み合わせて使うと簡単に optimizer に更新パラメータを知らせることができます。

# 勾配の初期化, 重みの update などの方法(アルゴリズム) が torch.optima にいろいろ定義されている
# 第一引数にこの optimizer で update したいパラメータの配列を渡す. 
# 今回は単純な stochastic gradient descent を使う
optimizer = torch.optim.SGD(model.parameters(), lr=1e-5, momentum=.8, weight_decay=1e-8, nesterov=True)

optim には zero_grad step メソッドが定義されています。 これらは、登録されたパラメータに対してそれぞれ勾配の初期化と更新を行ってくれるので、いちいち列挙しなくてよくてらくちんです。

ロス関数に関しても RMSE や CrossEntropy のような普通のロスであれば torch.nn 配下に定義されているのでそれを用いるとらくちん

# `criterion`: 予測値と正解ラベルとの差分を計算するオブジェクト
# `size_averaging` を `False` にすると, 与えられたバッチ n それぞれの rmse を合計した loss を返す。
# デフォルトでは `True` (すなわちバッチそれぞれの値を配列として返す)
criterion = torch.nn.MSELoss(size_average=False)

これら全部入りで学習スクリプトを書くと以下のようになります。

losses_sgd = []
for epoch in range(epochs):
    pred = model.forward(x)
    loss = criterion(pred, y)
    losses_sgd.append(loss.item())

    # 登録されたパラメータの勾配を 0 にする
    optimizer.zero_grad()
    loss.backward()

    # 登録されたパラメータを grad を使って更新する
    optimizer.step()

ちなみに autograd のときと module を使った時のロスの値をプロットすると以下のようなグラフになります。

圧倒的に Module がよいように見えますがこれは Module で定義されるパラメータの初期値が良い感じに設定されているからです。*2ニューラルネットワークの初期値はとても大事。)

f:id:dette:20180429072452p:plain

plt.figure(figsize=(8, 6))
plt.plot(losses, label="naive update")
plt.plot(losses_sgd, label="SGD with Nesterov and momentum")
plt.legend(loc=1)
plt.xlabel("epochs")
plt.ylabel("loss (log scale)")
plt.yscale("log")
plt.savefig("loss_transition.png", dpi=150)

*1:$H_0$ はヘヴィサイドの階段関数を指します

*2:重みのパラメータ数の平方根の逆数を上下限に持つ一様分布からサンプルされるようになっています。 http://pytorch.org/docs/master/_modules/torch/nn/modules/linear.html#Linear

ニューラルネットは何を見ているか ~ ブラックボックスモデルの解釈可能性

ディープラーニングによる予測は特に画像分野において顕著な性能を示していることはご案内のとおりです。これは ResNet や BatchNormalization といった技術の開発により多数のレイヤが重なった大きなモデルに対しても学習を行うことが可能になったことが理由の一つです。

一方で、ネットワーク構造が複雑になってしまったがゆえに別の問題も生じています。 それはなぜディープラーニングが性能が良いのか、どうやって判別を行っているのかということについてよくわからない、という問題です。 最近では人間が見ると元の画像とはほとんど変わらないのに出力結果が大きく変化してしまう画像(敵対的サンプル)を生成できてしまうことを示した論文なども登場しています。

というわけで今回は、内部でどうなっているかよくわからないモデルに対して、その解釈性を与えることに関する論文 Interpretable Explanations of Black Boxes by Meaningful Perturbation を紹介します。

解釈性とは何か

さて僕はさらっと解釈性、という単語を用いましたが、一体解釈性とは何でしょうか。ここで中身がどうなっているかわからないブラックボックスな予測モデル $f$ を以下のように定義します。

$$ f: X \to Y $$

ここで $X$ は入力値の取りうる空間, $Y$ は出力の空間です. 例えば画像の二値分類であれば入力は rgb の画像 $x : {\cal A} \times \mathbb{R}^3$ となります。ここで ${\cal A} = \left\{1, \ldots, H \right\} \times \left\{1, \ldots, W \right\}$ で $H, W \in \mathbb{N}$ は画像の縦横のピクセル数です. 一方出力 $Y$ は $\left\{-1, +1\right\}$ のラベルとなります.

解釈性とは特定の入力画像にたいしての出力が予測するルールのこと、と表現することができます。 例えば以下のような条件を満たす $Q_1$ というルールを得ることができた!とします。

$$ Q_1(x ;f) = \left\{ x \in X_c \iff f(x) = +1 \right\} $$

ここで $X_c \subset X$ は入力画像のうち $+1$ を取る画像集合であり, $X$ の部分集合です. このルール $Q_1$ の要素が $1$ の値を取る画像であれば $f$ も 1 を返し, 逆も成り立ちます。

以下の期待値を考えること、このルールがどの程度正しいのかを知ることも可能です.

$$ {\cal L_1} = \mathbb{E}_{x \sim p(x)} [1 - \delta_{Q_1}] $$

ここで $\delta_Q$ は $Q$ に$x$ が含まれているときに 1 その他のとき 0 を返す標示関数です.

${\cal L_1}$ は $Q_1$ が成立しない確率です。すなわち $f$ が $+1$ の画像に対して正しく分類ができてるの? という解釈性になっている、というわけです。そしてなぜ $f$ に仮定をおかず解釈性の議論をすることが出来るかというと条件の左側 $x \in X_c$ という関係が明瞭であるからなのです。

同じような形式で、出力同士を比べることでも、解釈性を考えることは可能です。 例として, $f$ がある回転角度 $\theta$ に対して不変な出力を持つのかということを確かめたいとします. この時のルール $Q_2$ は以下のようになります.

$$ Q_2(x, x'; f) = \left\{ x \sim_{\theta} x' \Rightarrow f(x) = f(x') \right\} $$

先と同様に期待値を取ればルールの正しさを知ることができます。

$$ {\cal L_2} = \mathbb{E}[1 - \delta_{Q_2}] $$

この ${\cal L_2}$ は $f$ が $\theta$ の回転に対して出力が変わらないのか? という解釈がどの程度成立するか、ということの表現になっています。

ではこれを拡張して, 画像の特定の領域を削ってしまうような関数 $h: X \to X$ があるとします。 そしてこの $h$ が中身がよくわからない $f$ の出力に関与しているのか? ということを知りたいとしましょう。

先程までの議論同様に考えると, 以下のようなルール $Q_3$ を考えれば良いことがわかります.

$$ Q_3(x; f, h) = \left\{ x' = h(x) \Rightarrow f(x) \neq f(x') \right\} $$

このルールが成立する確率が高くなるような $h$ が削っている領域が $f$ の出力をより決定している領域です。 ざっくりと言ってしまうと $f$ がよく見ている部分 とでも言うことが出来るでしょうか。

ルールが決まったのであとはロスを考えれば良いのですが、$h$ が極端な値を取られると困ります。例えば画像を 1px 間隔で削るような関数があったとします。自然に考えて人間はそのように画像を解釈していませんから、解としてはあまりよろしくないですよね。このような不自然な削り方に対してペナルティを与えるため一般的な機械学習同様に $h$ に対して正則化の効果を与えるような項を付け加えます。

結局以下の最小化問題を解けば良いことになります。

$$ \min_{h \in {\cal H}} {\cal L}_3 := \mathbb{E}_{x \sim p}[1 - \delta_{Q_3(x; h, f)}] + {\cal R} (h) $$

ここで ${\cal H}$ は関数 $h$ が取りうる範囲を決める関数集合, ${\cal R}: {\cal H} \to \mathbb{R}$ はペナルティを計算する正則化を計算する関数です。

論文での目的関数

前置きが長くなりましたが論文の話に戻ります。

論文中では $h$ をどの領域をどのぐらい削るかで表現するために画像の各ピクセルに対して 0 から 1 の値を取るような $m: {\cal A} \to [0, 1]$ を定義し以下のような関数 $\Phi: X \to X$ を用いて画像情報を削除しています。

$$ [\Phi(x_0; m)](u) = m(u) x_0(u) + (1- m(u))\mu_0 $$

ここで $u \in {\cal A}$ は画像の各要素を表しており, $\mu_0$ は平均の色の値を表します。 これを用いたロス関数は以下のようになっています。 論文ではこれの他にガウスノイズを乗せたものや ぼかしフィルタによる定式化も提案されていますが、ノリとしてはほぼ一緒です。

これを用いて目的関数を記述すると以下のようになります。

$$ {\cal L}(x) = \lambda_1 \| 1 - m\|_1 + \lambda_2 \sum_{u \in X} \| \nabla m(u) \| + \mathbb{E}_{\tau \sim [0, 4]} \left[ f(\Phi(x_0(- \bullet \tau): m)) \right] $$

第一項は必要な部分だけを 0 にしているか (すべての要素を 0 にすれば入力の情報がゼロになり出力が変わるのは自明) という意味です。

第二項は $m$ の変化が自然かどうか (画像方向に対して勾配がきびしくないか) という正則化項です。

第三項は $\Phi$ による入力画像の加工に加えて, $[0, 4)$ の一様分布に従う確率変数 $\tau$ によって入力画像の信号が弱まる様になっています。これは入力画像の一部分に対する過学習を起こさないために付け加えられています。

この目的関数を用いて勾配法をつかって最適化を行います。

実験

github 上に pytorch 実装が上がっていたので, fork していろいろいじってみました。

https://github.com/nyk510/pytorch-explain-black-box

pytorch が動く環境であれば以下のコマンドで実験を行います。第一引数が対象となる画像です. サンプルに入っている画像は下の猿の画像です。吠えてます。

f:id:dette:20180207170713j:plain

これを使って学習をしてみます。

python main.py examples/macaque.jpg
run with gpu
------------------------------
Category with highest probability: 373 - 96.07 %
Optimizing..
finish!
------------------------------
after masked probability: 1.26 %
save to output/macaqueperturbated.png
save to output/macaqueheat_map.png
save to output/macaquemask.png
save to output/macaquecam.png

はじめ 96% で分類できていた画像が加工後には 1,26% にまで落ち込んでいます。

学習されたマスクをかけた画像は以下のようになっています。

f:id:dette:20180207170827p:plain

人間の目にはほとんど変化が無いように見えます。敵対的サンプルの画像と似たような画像を生成していることがわかります。 マスクを掛けている領域はこんな感じ。猿の頭部分を見て判定しているのかな?といった印象ですね。

f:id:dette:20180207171220p:plain

感想

  • 正則化の与え方が論文を読んだ感じかなりヒューリスティックなので改良の余地はあるかも
  • 最適化方法を二次の情報を用いた手法 (たとえば L-BFGS など)にすればより早く収束しそう

Pycharmの便利機能まとめ

ショートカット

一般

コマンド 内容
Shift + ⌘ + Delete 最後に更新した場所にジャンプ
Shift + ⌘ + F7 現在開いているファイル内で、カーソルが当てられている変数が使用されている場所をハイライト。その状態で ⌘ + G を押すとつぎに使われている場所にジャンプできる。
⌘ + ↑ ナビゲーションバーにカーソルが移動する。同じフォルダの他のファイルを開きたいときに, 左のディレクトGUIをマウス操作しなくて済む。⌘+ ↓でソースにカーソルを戻せる。
⌘ + K コミットの変更と差分をGUIで見れる。

関数, クラスの情報

この関数の定義どうなってるのか, 変数何使えばいいのか, とかのときに使えるコマンド

コマンド 内容
⌘ + P パラメータ情報の取得
⌘ + B 関数が定義されているファイルを開く
F1 クイックドキュメントを開く

Code

コマンド 内容
Option + ⌘ + T その部分を制御構文で囲う。ある処理を try/except でエラーキャッチしたいときに便利。(他にも while とか try/finally とかもある。)

Refactor

コマンド 内容
Control + O 継承クラスでの関数のオーバーライドをGUI上で選択できる。
Shift + F6 変数名の変更。その変数が使われているファイルを自動で検索してすべて置換できるのでとても便利🍺

補間

コマンド 内容
Control + O 変数名の補間。

便利機能

編集履歴

git とは別に Pycharm 自体が編集履歴を保存している。 VCS -> Local History -> Show History を開くと過去の編集ログを一括でみることができる.

git には残らない細かい粒度でのログを見たいときや, 前の状態に戻したいけど commit してない... みたいな事故が起こったときに使える 。(本当はそういうことが起こってはだめですが)

.gitignore の自動生成

⌘+N で新規ファイル作成をする際, 下の方にある .ignore を選択し git を選ぶと gitignore ファイルの生成を行える。 用いている言語によってそれぞれ ignore が設定されているので、必要な物を検索してチェックボックスにチェックを入れるとよしなな ignore が完成する。

Terminal

Option + F12 でプロジェクトフォルダに

pycharm での docstring の書き方

参考: https://www.jetbrains.com/help/pycharm/2017.1/type-hinting-in-pycharm.html

基本的に python において docstring の書き方は一通りではないが、 pycharm が推奨する docstring の形式があり, それに準拠すると型チェックを自動でおこなってくれる.

シンタックス 意味
Foo Class Foo visible in the current scope
x.y.Bar Class Bar from x.y module
Foo | Bar Foo or Bar
(Foo, Bar) Tuple of Foo and Bar
list[Foo] List of Foo elements
dict[Foo, Bar] Dict from Foo to Bar
T Generic type (T-Z are reserved for generics)
T <= Foo Generic type with upper bound Foo
Foo[T] Foo parametrized with T
(Foo, Bar) -> Baz Function of Foo and Bar that returns Baz
list[dict[str, datetime]] List of dicts from str to datetime (nested arguments)

def put_image_to_s3(image,
                    bucket_name,
                    directory="image",
                    extension="JPEG"
                    logdir=None):
    """
    PIL形式の画像を s3 に upload する
    :param Image.Image image: PIL image file
    :param str bucket_name: upload先のバケット名
    :param str directory: バケットでのディレクトリ
    :param str extension: ファイルの拡張子. 今は `JPEG` のみ対応
    :param str | None logdir: 
        アップロード中のログを出力するディレクトリへのパス. 
        `None` のときログを出力しない. 
    :return: url to uploaded image file
    :rtype str
    """
    # 以下実装 ~~