Pytorch で DCGAN をつくったよ
DCGAN を二年ぶりに実装しました。
いつも MNIST も面白くないので、 MNIST にアルファベットが加わった 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 初心者なのでもっと良い書き方があるのだろうなとは思っているのでいろんなコードを読んで勉強していきたいなという所存です。
実行結果
最後になりましたが実行結果を載せます。
今回はGPUがあったので無事現実的時間内に生成できました。楽しい。
まとめ
- pytorch 書き方がよくわからない
- GAN は楽しい
Pytorch ことはじめ
0.40 の update もできて話題の pytorch を触り始めたのでその時のメモ。
出てくるコードはすべて以下のリポジトリの /notebooks/start
で見ることができます。
参考資料
インストール
公式を見ると 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
もしやりたい時には
- 一回 numpy array に変換してからスライス操作をかけて
- そのオブジェクトをコピー
- コピーされた 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 の便利モジュールを使うとあれなので
- 手計算で更新
- Autograd をつかって更新
- 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(ニューラルネットワークの初期値はとても大事。)
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 が動く環境であれば以下のコマンドで実験を行います。第一引数が対象となる画像です. サンプルに入っている画像は下の猿の画像です。吠えてます。
これを使って学習をしてみます。
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% にまで落ち込んでいます。
学習されたマスクをかけた画像は以下のようになっています。
人間の目にはほとんど変化が無いように見えます。敵対的サンプルの画像と似たような画像を生成していることがわかります。 マスクを掛けている領域はこんな感じ。猿の頭部分を見て判定しているのかな?といった印象ですね。
感想
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 """ # 以下実装 ~~