Skip to content

Latest commit

 

History

History
117 lines (73 loc) · 10.7 KB

File metadata and controls

117 lines (73 loc) · 10.7 KB

对抗深渊

(如果只想了解解法请快速下划)

问题介绍

这是本次比赛唯一的机器学习相关题目,也是Hackergame第一次正式出此类题目。

本次题目的核心是“对抗样本(adversarial examples)”。对抗样本是攻击者有意构造的使得模型出错的输入样本,这些样本会让模型出现“错觉“,输出不符合图片中内容的结果。需要注意的是很多时候,特别在机器学习领域,我们并没有“绝对正确的答案”(比如某个奇怪的数字是不是2),都是相对某个标准而言。在和机器对比时,我们将标准设为一般人类的认知。对抗样本表现出的主要危害是,其可以在人类所不能察觉的情况下误导机器的判断,如果类似情形发生在自动驾驶系统中,危害会很大。

一个基本的事实是,所有的机器学习模型都存在对抗样本,甚至包括人本身。对人来说,典型的对抗样本包括各类视觉错觉,以及各类幻听。

假设人眼的分辨率为5K(5120 * 3200),颜色感知范围为”真彩色(24bit位元色彩编码)”,则理论上存在 1.7e+114688000 种不同的输入(样本),比可观测宇宙中的原子个数还大114,687,920个数量级。即使计上视觉系统出现以来,所有存在视觉的生物,所有经历的年代中,所有个体接受的所有图像(刷新率以60Hz计),其数目与之相比仍然可以忽略不计。而一个人在成长中所接受的的图像(样本)数目,即使相对于这忽略不计的数目也是微不足道。用 Bloodborne 中的一句话来说,“Our eyes are yet to open”,敬畏深渊吧/滑稽。

这种已经认知的样本数目和可能存在的样本数目之间过于夸张的差距,由进化和学习填补。进化决定了模型的结构和超参数 (Hyperparameter),而学习决定了模型的参数。我们使用特殊的算法,利用已有的少量样本来推测大部分样本的性质,这就是一种广义的机器学习。而实际推测的效果我们称为“泛化能力(generalization ability)”。令人惊讶的是,我们的泛化能力相当的好,我们的视觉系统就是一个典型的例子;而另外一个典型的例子是我们的科学研究,迄今依赖我们几乎所有的研究数据都出自地球(除了极少量来自于一些卫星),但是相当多的部分都被证实适用于整个可观测宇宙,这就相当于,我们是用一粒沙子推测整个沙漠并且相当成功,这难以被称为巧合。不过这种超强的外推能力并不是免费的午餐(除非我们真的发现了这种情况),对抗样本就可以认为是代价之一。少量的样本加上模型本身的问题会带来大量的偏差(bias),从而不同模型对某些特定样本会有非常不同的结果。一个极端例子是,仅给你一个点,让测试者画出经过这个点的一个曲线,这个时候曲线的形状就完全取决于测试者的成见。

近年来机器学习的一大热点是深度学习(deep learning)。深度学习利用深度神经网络(deep neural network)作为其主要模型,在视觉,语音,自然语言处理等领域表现出了惊人的泛化能力。作为机器学习模型,其自然拥有对抗样本。然而,人们发现一个严重的问题在于,其对抗样本与人类的经验非常不符。如果我们使用特殊的算法,就可以加入人类难以察觉或者觉得完全没有意义的扰动,让神经网络输出完全不同的结果:

这个问题看上去是神经网络不够robust,会因为小的扰动极大的改变结果。但是大部分试图让神经网络robust来克服对抗样本的尝试都轻微降低了神经网络的泛化能力。

事实是问题不在于小的扰动本身,下面的例子显示可以通过小的扰动完全改变图片的意义:

所以更加本质的问题是神经网络和人类的视觉系统使用了并不一致的方式来处理图像。其中一点是讨论什么是相对于人类视觉的 robust features,即什么样特征或者扰动对人类来说是有意义的。目前学界并没有搞清楚这个问题,甚至这个问题本身可能是 AI-Complete 的或者超出了视觉的范畴。

攻击和防御

典型的几个攻击手段利用了神经网络的梯度做文章。既然神经网络利用反向传播梯度进行优化,那么自然可以采用梯度来负向优化。

我们可以定义这样的一个目标函数:使得对抗样本和原样本的n-范数尽可能小(即对人看来差异尽可能小),同时使得对抗样本尽可能降低神经网络对正确结果的输出值。这个目标函数显然是可导的,所以我们可以利用这个目标函数通过优化手段求解对抗样本。当然优化过程是需要迭代的。

另外一种方法是直接求解负向优化对于原样本的梯度,然后通过一个sign函数(将正数映射到+1,负数映射到-1),乘上一个比值 epsilon,并加到原样本上,得到所需的对抗样本。这种方法称为FGSM(Fast Gradient Sign Method)。这个方法约等于只进行了一步迭代的优化,所以效果不如直接优化好,但是好处是速度快且可以精准控制像素改变的最大值。这是本题推荐的方法。

那么如何防御对抗样本攻击呢?

最简单的方法之一是“负向优化负向优化”,这也称为对抗样本训练。我们可以在训练神经网络的时候,有意加入可能的对抗样本作为负例,强制网络学习它们。但是这种方法的问题是,如果对方算力足够,就可以“负向优化负向优化负向优化”,即产生新的对抗样本,其能够对用对抗样本训练的网络进行攻击。可以看出这个过程实际上是个Min-Max游戏,算力最足的一方可以笑到最后,所以并不能有足够的安全性保障。

然后一种方法是 Defensive distillation,其试图让所有的分类结果独立,如果攻击者试图按照以前的假定,即所有分类的输出概率是归一化的,就很可能不能达到效果。但是如果攻击者采用更多算力,自己进行distillation并针对这类模型进行攻击,则依然不能解决问题。

近期相对热门且有一定理论保证的方法是 Gradient Masking。这种方法依据是对抗样本往往需要梯度,且往往是微小的扰动,如果我们采用一些办法离散化梯度,就可以阻止求导(离散化后不连续了),且离散化的取整操作会“消灭”一些微小的扰动。不过现在此类方法仍然存在争议,人们发现可以通过训练一个平滑化的对应模型,并将结果迁移到离散化的模型进行攻击,除非离散化能够自动避开所有的对抗样本(这在逻辑上是说不通的)。

目前最好的防御方法可能就是保持模型的黑箱特性(不对公众开放模型)。这样攻击者就不能利用梯度信息,只能进行猜测,难度大大增加。另外对抗样本本身的泛化能力较弱,造成模型出错的对抗样本可能加入少许的硬件噪声就会完全无效;同样地改变视角、缩放图像、光照条件也会极大地削弱对抗样本的性能。所以目前仍然有很多公司并未考虑对抗样本的危害,而目前研究的热点之一也在于如何产生现实中对环境robust的对抗样本。

题解

解法 1

暴力枚举所有可能结果。以每秒 1000 次尝试计,不超过 174900185917744396839444704700371442022073601886260538841237614035267595364796290592613521116939643196149952230826228146139239732089317995927927178492867409123625336636770038146671169983108560933075329551372 年就可以获得结果!

解法 2

本题其实有3个考点,需要成功就需要翻越“三座大山”。

第一座是机器学习的数据预处理,预处理往往可以加快训练收敛并提升效果。在 main.py 中,一个预处理是减去均值,并除以标准差:

train_loader = torch.utils.data.DataLoader(
    datasets.MNIST('data', train=True, download=True,
                   transform=transforms.Compose([
                       transforms.Resize((30, 30)),
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,)),
])),

所以在测试阶段我们需要补上这部分预处理才能保持结果一致(这就是Part2的内容):

# A solution for part 2
target = (target - 0.1307) / 0.3081 
sample = (sample - 0.1307) / 0.3081

第二座是图像缩放。如果仔细阅读代码可以发现原来 600 * 600 的图像经过缩放变成 30 * 30 后才输入到网络中。图像缩放会破坏对抗样本,所以不能直接利用原图像生成对抗样本,而是利用缩放的图像。更何况我们要求原图像中更改不超过千分之三,这个直接用优化很难做到。

图像缩放在本题中不是故意构造的。现实中的输入对于神经网络来说太大,处理缓慢,且可能和训练时的尺寸不同,这个时候缩放是必然的。

仔细观察缩放图像的代码:

def preprocess_image(arr):
    image = convert2image(arr)
    image = image.resize((30, 30), resample=Image.NEAREST)
    return convert2tensor(image).reshape(1, 1, 30 ,30)

resample=Image.NEAREST 其实是非常强的提示了,因为这种缩放的方法是取原图 20 * 20 的 block 的中心像素构成大小为 30 * 30 的输入图像。这个 30 * 30 的小图才是我们真正的对抗样本。之后注意把这 30 * 30 的小图放大后 patch 到大图上面。

(我原来准备不改 resample 直接用 bilinear 缩放来着,需要选手逆向 bilinear,但是考虑到可能会进一步加大题目难度,就改用了简单的 NEAREST)。NEAREST 是直接的下采样过程,对自然图像会造成严重的混叠(Shannon 采样定理),所以现实中一般不直接使用。

第三座就是对抗样本,直接参考 repo,用 FGSM 来做就行。所以这两部分构成了 Part 1 的答案:

# A solution for part 1
inputs.requires_grad = True
x = (inputs - 0.1307) / 0.3081  # scale mean & std 
output = model(x)
loss = F.nll_loss(output, label)
loss.backward()  # compute gradient
x_grad = torch.sign(inputs.grad.data)
epsilon = 0.18
inputs = torch.clamp(inputs + epsilon * x_grad, 0, 1)  # FGSM

inputs = inputs.reshape(30, 30)

for i in range(30):
    for j in range(30):
        image[int((i + 0.5) * 20), int((j + 0.5) * 20)] = inputs[i, j]  # patch to original image

另外,本次用 pytorch 而不是 tensorflow 的原因是,个人觉得 pytorch 算梯度更简单,两三行代码搞定。