حملات تخاصمی (Adversarial Attacks) با چارچوب PyTorch – راهنمای کاربردی


در سالهای اخیر از «هوش مصنوعی» (Artificial Intelligence) و «یادگیری ماشین» (Machine Learning) در زمینههای گوناگونی استفاده شده است. برخی از دانشمندان نسبت به رشد بیوقفه هوش مصنوعی ابراز نگرانی کردهاند و برخی دیگر این نگرانیها را بیدلیل میدانند. اما جنس دیگری از نگرانیها اخیرا در رابطه با مدلهای یادگیری ماشین مطرح شده که با آنچه پیشتر بیان میشد متفاوت است. گمراه کردن مدلهای یادگیری ماشین و یا به عبارتی هک کردن آنها امکانپذیر است و شاید این همان موضوعی باشد که واقعا باید نگران آن بود. در ادامه، به موضوع «حملات تخاصمی» (Adversarial Attacks)، مفهوم و چگونگی پیادهسازی آنها با استفاده از «زبان برنامهنویسی پایتون» (Python Programming Language) پرداخته شده است.
حملات تخاصمی (Adversarial Attacks)
با وجود آنکه مدلهای «یادگیری عمیق» (Deep Learning) در بسیاری از وظایف پیچیده موفقیتهای قابل توجهی کسب کردهاند، اما آنقدرها هم که بشر ممکن است فکر کند، هوشمند نیستند. پژوهشها حاکی از آن است که مدلهای یادگیری عمیق به چیزی با عنوان «نمونههای تخاصمی» (Adversarial Examples) آسیبپذیر هستند. نمونههای تخاصمی، ورودیهایی محسوب میشوند که به شکل مخربی طراحی شدهاند و به لحاظ ادراکی (برای مثال در صورتی که انسان آنها را مشاهده کند) از ورودیهای اصلی قابل تشخیص نیستند، ولی توسط مدل به اشتباه دستهبندی میشوند.
برای مثال، اگر تصویر یک «بَبرَک» (Tiger Cat) موجود باشد (نمونه تصویر اصلی و تصویر تخاصمی در ادامه مطلب آمده است) که توسط مدل InceptionV3 به درستی تشخیص داده میشود، میتوان با استفاده از مدل تخاصمی این تصویر را به گونهای تغییر داد که به لحاظ بصری از تصویر اصلی غیر قابل تشخیص باشد، اما توسط مدل به اشتباه دستهبندی شود. نمونههای تخاصمی پس از آنکه برای اولینبار مطرح شدند، توانستند توجهات زیادی را از جامعه پژوهشگران به خود جلب کنند و در نتیجه، پس از آن چندین روش برای ساخت نمونههای تخاصمی ارائه شده است. در این راهنما، چهار روش ساده برای تولید نمونههای تخاصمی ارائه شده است. کار با نصب کتابخانههای لازم، بارگذاری تصویر ورودی و دستهبندی آن با استفاده از یک مدل InceptionV3 از پیش آموزش دیده شده آغاز میشود. سپس، از روشهای گوناگون انجام حملات تخاصمی یا Adversarial Attacks برای ساخت نمونههای تخاصمی و گمراه کردن «دستهبند» (Classifier) استفاده میشود.
ابتدا، کتابخانههای مورد نیاز برای انجام سناریو بیان شده را باید با دستور زیر فراخوانی کرد. فرض میشود که کتابخانههای «نامپای» (NumPy)، (مَتپِلاتلیب) (Matplotlib) و «رِکوئِست» (Requests) پیشتر توسط کاربر نصب شدهاند و در حال حاضر نیازی به نصب آنها نیست و فراخوانی آنها کفایت دارد. کاربرانی که این موارد را نصب ندارند و اکنون اقدام به نصب آنها میکنند باید توجه داشته باشند که پس از نصب، زمان اجرا را صفر کنند. بدین دلیل گفته میشود باید زمان اجرا را صفر کرد که بعدا زمان اجرای فرآیند اصلی قابل اندازهگیری باشد.
1 !pip3 install http://download.pytorch.org/whl/cu80/torch-0.3.1-cp36-cp36m-linux_x86_64.whl
2 !pip3 install torchvision
3 !pip3 install --no-cache-dir -I pillow
1#import required libs
2import torch
3import torch.nn
4from torch.autograd.gradcheck import zero_gradients
5import torch.nn.functional as F
6import torchvision.models as models
7from PIL import Image
8from torchvision import transforms
9import numpy as np
10import requests, io
11import matplotlib.pyplot as plt
12from torch.autograd import Variable
13%matplotlib inline
در اینجا از مدل پیش-آموزش داده شده inceptionv3 برای دستهبندی دادهها استفاده خواهد شد. همچنین، میتوان از مدل VGG یا دیگر انواع مدلها استفاده کرد، اما باید تغییراتی در این کد داده شود (برای مثال در سایز ورودی). با توجه به اینکه inceptionv3 از «رها کردن» (Dropout) و «نرمالسازی دسته» (Batch Normalization) استفاده میکند، رفتار آن در هنگام ارزیابی و ضمن آموزش متفاوت است. بنابراین، باید در حالت ()eval تنظیم شود.
1inceptionv3 = models.inception_v3(pretrained=True) #download and load pretrained inceptionv3 model
2inceptionv3.eval();
اکنون، یک تصویر با استفاده از requests و PIL بارگذاری میشود. گزینش تصویر به انتخاب کاربر است.
1url = "https://savan77.github.io/blog/images/ex4.jpg" #tiger cat #i have uploaded 4 images to try- ex/ex2/ex3.jpg
2response = requests.get(url)
3img = Image.open(io.BytesIO(response.content))
در حال حاضر، نیاز به پردازش کردن تصویر ورودی پیش از پاس دادن آن به مدل inceptionv3 است. به دلیل آنکه این تصویر با استفاده از PIL بارگذاری شده، باید آن را به یک تانسور تورچ (تبدیل کردن به تانسور در کتابخانه پایتورچ) تبدیل کرد. همچنین، نیاز به شکلدهی مجدد تصویر ورودی است. Inceptionv3 میپذیرد که ارتفاع و وزن ورودی ۲۹۹ باشد. در نهایت، «نرمالسازی» (Normalization) انجام میشود. شایان توجه است که مدل inceptionv3 پایتورچ از وزنهای از پیش آموزش داده شده از گوگل استفاده میکند و تصاویری با مقدار پیکسل بین -۱ تا ۱ را میپذیرد.
«پایتورچ» (PyTorch) این عملیات را به صورت داخلی انجام میدهد و ورودیهای نرمالشده با «میانگین» (Mean) و «انحراف معیار» (Standard Deviation) ارائه شده در زیر را (به منظور یکنواختی) قبول میکند. افرادی که قصد پیادهسازی نرمالسازی را به صورت دستی دارند (چنانکه با وزنهای از پیش آموزش داده شده از گوگل فرض شده)، باید یک پارامتر بیشتر را پاس بدهند (transform_input=False)، در حالیکه دادههای از پیش آموزش دیده مدل inceptionv3 بارگذاری میشوند. برای مثال، inceptionv3 = models.inception_v3(pretrained=True, transform_input=False). در اینجا، ()Compose ترکیب تبدیلهای چندگانه در یک تابع را آسان میکند.
1#mean and std will remain same irresptive of the model you use
2mean=[0.485, 0.456, 0.406]
3std=[0.229, 0.224, 0.225]
4
5preprocess = transforms.Compose([
6 transforms.Resize((299,299)),
7 transforms.ToTensor(),
8 transforms.Normalize(mean, std)
9 ])
سپس، تصویر ورودی با استفاده از تابع ساخته شده در بالا پیشپردازش میشود.
1image_tensor = preprocess(img) #preprocess an i
2image_tensor = image_tensor.unsqueeze(0) # add batch dimension. C X H X W ==> B X C X H X W
1img_variable = Variable(image_tensor, requires_grad=True) #convert tensor into a variable
در ادامه، این تصویر با استفاده از مدل از پیش آموزش داده شده inceptionv3 که پیشتر بارگذاری شده بود دستهبندی میشود.
1output = inceptionv3.forward(img_variable)
2label_idx = torch.max(output.data, 1)[1][0] #get an index(class number) of a largest element
3print(label_idx)
282
پس از آن، یک دیکشنری برای نگاشت اندیسها به کلاس imagenet مورد استفاده قرار میگیرد. ImageNet دارای ۱۰۰۰ کلاس است.
1labels_link = "https://savan77.github.io/blog/files/labels.json"
2labels_json = requests.get(labels_link).json()
3labels = {int(idx):label for idx, label in labels_json.items()}
4x_pred = labels[label_idx]
5print(x_pred)
tiger cat
شایان توجه است که متد ()forward اکنون logits را باز میگرداند. بنابراین به منظور دریافت تابع احتمال برای ۱۰۰۰ کلاس، نیاز به انتقال دادن خروجی از طریق یک تابع «softmax» (سافتمَکس) است.
1#get probability dist over classes
2output_probs = F.softmax(output, dim=1)
3x_pred_prob = round((torch.max(output_probs.data, 1)[0][0]) * 100,4)
4print(x_pred_prob)
74.9844
روش علامت گرادیان سریع
فرض میشود که ورودی X موجود و به درستی توسط مدل (M) دستهبندی شده است. هدف، پیدا کردن یک نمونه تخاصمی است که به لحاظ ادراکی از نسخه اصلی X غیر قابل تشخیص باشد، اما توسط همان مدل (M) به اشتباه دستهبندی میشود. میتوان این کار را با افزودن آشفتهسازی تخاصمی (θ) به ورودیهای اصلی انجام میشود.
باید به این نکته توجه کرد که هدف پیدا کردن مثال تخاصمی است که به لحاظ ادراکی از نسخه اصلی غیر قابل تشخیص باشد. این نمونه را میتوان با محدود کردن بزرگی آشفتگی تخاصمی X−||∞⩽ϵ|| به دست آورد. نُرم ∞L باید کمتر از اپسیلون باشد. در اینجا، ∞L نشانگر حداکثر تغییرات برای همه پیکسلها در نمونه تخاصمی است. روش «علامت گرادیان سریع» (Fast Gradient Sign Method | FGSM) یک روش سریع و به لحاظ محاسباتی کارآمد برای تولید نمونههای تخاصمی محسوب میشود. اگرچه، معمولا نرخ موفقیت کمتری دارد. راهکار برای پیدا کردن مثال تخاصمی به صورت زیر است:
((Xadv=X+ϵsign(∇XJ(X,Ytrue
در اینجا داریم:
- X = ورودی اصلی (سالم)
- Xadv = ورودی تخاصمی (عمدا به گونهای طراحی شده که توسط مدل به اشتباه دستهبندی شود)
- ϵ = بزرگی آشفتگی تخاصمی
- (XJ(X,Ytrue∇ = گرادیان تابع زیان w.r.t به ورودی (X)
1y_true = 282 #tiger cat ##change this if you change input image
2target = Variable(torch.LongTensor([y_true]), requires_grad=False)
3print(target)
Variable containing: 282 [torch.LongTensor of size 1]
1#perform a backward pass in order to get gradients
2loss = torch.nn.CrossEntropyLoss()
3loss_cal = loss(output, target)
4loss_cal.backward(retain_graph=True) #this will calculate gradient of each variable (with requires_grad=True) and can be accessed by "var.grad.data"
سلول کد زیر همه نمونههای تخاصمی را با استفاده از فرمول نشان داده شده در بالا محاسبه میکند.
1eps = 0.02
2x_grad = torch.sign(img_variable.grad.data) #calculate the sign of gradient of the loss func (with respect to input X) (adv)
3x_adversarial = img_variable.data + eps * x_grad #find adv example using formula shown above
4output_adv = inceptionv3.forward(Variable(x_adversarial)) #perform a forward pass on adv example
5x_adv_pred = labels[torch.max(output_adv.data, 1)[1][0]] #classify the adv example
6op_adv_probs = F.softmax(output_adv, dim=1) #get probability distribution over classes
7adv_pred_prob = round((torch.max(op_adv_probs.data, 1)[0][0]) * 100, 4) #find probability (confidence) of a predicted class
1print(x_adv_pred)
2print(adv_pred_prob)
Egyptian cat 66.0086
در نهایت، یک نمونه تخاصمی ساخته شده که توسط مدل به اشتباه دستهبندی میشود. به دلیل آنکه «گربه مصری» (Egyptian cat) بسیار شبیه به بَبرَک است، همچنان میتوان گفت نمونه تخاصمی خیلی خوب نیست.
در ادامه، تابعی تعریف میشود که ورودی اصلی، نمونه تخاصمی و آشفتگی تخاصمی را بصریسازی میکند. این کار، درک بهتری از چگونگی به نظر رسیدن نمونههای تخاصمی و اینکه آیا آنها از تصویر اصلی قابل تشخیص هستند یا خیر ارائه میکند.
1def visualize(x, x_adv, x_grad, epsilon, clean_pred, adv_pred, clean_prob, adv_prob):
2
3 x = x.squeeze(0) #remove batch dimension # B X C H X W ==> C X H X W
4 x = x.mul(torch.FloatTensor(std).view(3,1,1)).add(torch.FloatTensor(mean).view(3,1,1)).numpy()#reverse of normalization op- "unnormalize"
5 x = np.transpose( x , (1,2,0)) # C X H X W ==> H X W X C
6 x = np.clip(x, 0, 1)
7
8 x_adv = x_adv.squeeze(0)
9 x_adv = x_adv.mul(torch.FloatTensor(std).view(3,1,1)).add(torch.FloatTensor(mean).view(3,1,1)).numpy()#reverse of normalization op
10 x_adv = np.transpose( x_adv , (1,2,0)) # C X H X W ==> H X W X C
11 x_adv = np.clip(x_adv, 0, 1)
12
13 x_grad = x_grad.squeeze(0).numpy()
14 x_grad = np.transpose(x_grad, (1,2,0))
15 x_grad = np.clip(x_grad, 0, 1)
16
17 figure, ax = plt.subplots(1,3, figsize=(18,8))
18 ax[0].imshow(x)
19 ax[0].set_title('Clean Example', fontsize=20)
20
21
22 ax[1].imshow(x_grad)
23 ax[1].set_title('Perturbation', fontsize=20)
24 ax[1].set_yticklabels([])
25 ax[1].set_xticklabels([])
26 ax[1].set_xticks([])
27 ax[1].set_yticks([])
28
29
30 ax[2].imshow(x_adv)
31 ax[2].set_title('Adversarial Example', fontsize=20)
32
33 ax[0].axis('off')
34 ax[2].axis('off')
35
36 ax[0].text(1.1,0.5, "+{}*".format(round(epsilon,3)), size=15, ha="center",
37 transform=ax[0].transAxes)
38
39 ax[0].text(0.5,-0.13, "Prediction: {}\n Probability: {}".format(clean_pred, clean_prob), size=15, ha="center",
40 transform=ax[0].transAxes)
41
42 ax[1].text(1.1,0.5, " = ", size=15, ha="center", transform=ax[1].transAxes)
43
44 ax[2].text(0.5,-0.13, "Prediction: {}\n Probability: {}".format(adv_pred, adv_prob), size=15, ha="center",
45 transform=ax[2].transAxes)
46
47
48 plt.show()
1visualize(image_tensor, x_adversarial, x_grad, eps, x_pred, x_adv_pred, x_pred_prob, adv_pred_prob)
همانطور که مشهود است، تصویر تخاصمی تولید شده به لحاظ بصری از تصویر اصلی قابل تشخیص نیست، اما «دستهبند» (classifies) مورد استفاده در اینجا، یعنی inceptionv3 تصویر را به اشتباه گربه مصری تشخیص داده است. اکنون، چندین تصویر تخاصمی با مقادیر مختلف اپسیلون ساخته میشود. شایان توجه است که هر چه مقدار اپسیلون بیشتر شود، تصویر تخاصمی از تصویر اصلی غیر قابل تشخیص میشود.
1epsilon = [0.00088, 0.004, 0.01, 0.12, 0.55]
1x_grad = torch.sign(img_variable.grad.data)
2for i in epsilon:
3 x_adversarial = img_variable.data + i * x_grad
4 output_adv = inceptionv3.forward(Variable(x_adversarial))
5 x_adv_pred = labels[torch.max(output_adv.data, 1)[1][0]]
6 op_adv_probs = F.softmax(output_adv, dim=1)
7 adv_pred_prob = round((torch.max(op_adv_probs.data, 1)[0][0]) * 100, 4)
8 visualize(image_tensor, x_adversarial, x_grad, i, x_pred, x_adv_pred, x_pred_prob, adv_pred_prob)
برای مقدار بسیار کوچک اپسیلون، دسته تغییر نمیکند. اما احتمال کاهش مییابد. یک راه جایگزین استفاده از گرادیان خام (علامتگذاری نشده) بدون هرگونه محدودیتی (اپسیلون) است. به این روش، «مقدار گرادیان سریع» (Fast Gradient Value) گفته میشود.
روش دسته هدف یک گام
مخاطبان این مطلب ممکن است تاکنون متوجه شده باشند که FGSM آشفتگی را پیدا میکند که زیان را برای کلاس صحیح افزایش میدهد و متعاقبا منجر به دستهبندی غلط میشود. همچنین، FGSM یک روش غیر هدفمند است. میتوان به سادگی آن را با بیشینه کردن احتمال (P(Ytarget|X برای برخی از کلاسهای هدف Ytarget به کلاس هدف تبدیل کرد.
برای یک «شبکه عصبی» (Neural Network) با «زیان آنتروپی متقاطع» (Cross-Entropy Loss) فرمول به صورت زیر خواهد بود:
(Xadv=X−ϵsign(∇XJ(X,Ytarget
تنها تغییری که نیاز به اعمال آن است، حذف آشفتگی از ورودی مدل به جای افزودن آن است. اما چگونه میتوان کلاس هدف را انتخاب کرد؟ دو راهکار در پاسخ به این پرسش وجود دارد. یکی استفاده از یک کلاس تصادفی به عنوان کلاس هدف است. راهکار دیگر و توصیه شده، استفاده از دستهای است که مدل کمترین احتمال را به آن تخصیص میدهد. این روش به عنوان دسته با کمترین احتمال نیز شناخته شده است. در اینجا از راهکار دسته تصادفی استفاده میشود.
1#targeted class can be a random class or the least likely class predicted by the network
2y_target = 288 #leopard
3y_target = Variable(torch.LongTensor([y_target]), requires_grad=False)
4print(y_target)
Variable containing: 288 [torch.LongTensor of size 1]
1zero_gradients(img_variable) #flush gradients
2loss_cal2 = loss(output, y_target)
3loss_cal2.backward()
1epsilons = [0.002, 0.01, 0.15, 0.5 ]
1x_grad = torch.sign(img_variable.grad.data)
2for i in epsilons:
3 x_adversarial = img_variable.data - i * x_grad
4 output_adv = inceptionv3.forward(Variable(x_adversarial))
5 x_adv_pred = labels[torch.max(output_adv.data, 1)[1][0]]
6 op_adv_probs = F.softmax(output_adv, dim=1)
7 adv_pred_prob = round((torch.max(op_adv_probs.data, 1)[0][0]) * 100, 4)
8 visualize(image_tensor, x_adversarial, x_grad, i, x_pred, x_adv_pred, x_pred_prob, adv_pred_prob)
به نظر میرسد مدل در اینجا عملکرد خوبی نداشته است. میتوان از تصاویر دیگر استفاده کرد و نتایج ممکن است تغییر کند. در ادامه دیگر روشهای انجام حملات تخاصمی مورد بررسی قرار میگیرد تا مشاهده شود که آیا میتوانند نمونههای تخاصمی بهتری تولید کنند.
روش تکرار شونده پایه
روش «تکرار شونده پایه» (Basic Iterative Method) افزونهای از روش FGSM است که FGSM را چندین بار با اندازههای گام کوچک تکرار میکند. بدین منظور، تصویر تخاصمی به بعنوان تصویر اصلی مقداردهی اولیه میشود و سپس یک گام در جهت گرادیان در طول هر تکرار برداشته میشود.
در اینجا، ClipX,ϵ نشانگر قطع کردن ورودی در طیف [X−ϵ,X+ϵ] است.
1y_true = Variable( torch.LongTensor([282]), requires_grad=False) #tiger cat
2epsilon = 0.25
3num_steps = 5
4alpha = 0.025
5#above three are hyperparameters
1for i in range(num_steps):
2 zero_gradients(img_variable) #flush gradients
3 output = inceptionv3.forward(img_variable) #perform forward pass
4 loss = torch.nn.CrossEntropyLoss()
5 loss_cal = loss(output, y_true)
6 loss_cal.backward()
7 x_grad = alpha * torch.sign(img_variable.grad.data) # as per the formula
8 adv_temp = img_variable.data + x_grad #add perturbation to img_variable which also contains perturbation from previous iterations
9 total_grad = adv_temp - image_tensor #total perturbation
10 total_grad = torch.clamp(total_grad, -epsilon, epsilon)
11 x_adv = image_tensor + total_grad #add total perturbation to the original image
12 img_variable.data = x_adv
13
14#final adversarial example can be accessed at- img_variable.data
1output_adv = inceptionv3.forward(img_variable)
2x_adv_pred = labels[torch.max(output_adv.data, 1)[1][0]] #classify adversarial example
3output_adv_probs = F.softmax(output_adv, dim=1)
4x_adv_pred_prob = round((torch.max(output_adv_probs.data, 1)[0][0]) * 100,4)
5visualize(image_tensor, img_variable.data, total_grad, epsilon, x_pred,x_adv_pred, x_pred_prob, x_adv_pred_prob) #class and prob of original ex will remain same
Inceptionv3 بَبرَک را به عنوان گربه مصری با درجه اطمینانی بیش از آنچه برای بَبرَک دارد دستهبندی کرد.
روش دسته هدف تکرار شونده
میتوان کار مشابهی را برای روش دسته هدف یک گام انجام داد. همانطور که پیشتر نمایش داده شد، به منظور انجام FGSM هدفمند نیاز به حذف آشفتگی از تصویر اصلی است. در اینجا، این کار با چندین تکرار انجام میشود.
مجددا لازم به ذکر است که توصیه میشود از کلاسی که کمترین احتمال را دارد استفاده شود، اما در اینجا از یک کلاس تصادفی استفاده میشود. میتوان کلاس با کمترین احتمال را با اجرای [y_LL = torch.min(output.data, 1)[1][0 پیدا کرد.
1y_target = Variable(torch.LongTensor([9]), requires_grad=False) #9= ostrich
2epsilon = 0.25
3num_steps = 5
4alpha = 0.025
1img_variable.data = image_tensor #in previous method we assigned it to the adversarial img
1for i in range(num_steps):
2 zero_gradients(img_variable)
3 output = inceptionv3.forward(img_variable)
4 loss = torch.nn.CrossEntropyLoss()
5 loss_cal = loss(output, y_target)
6 loss_cal.backward()
7 x_grad = alpha * torch.sign(img_variable.grad.data)
8 adv_temp = img_variable.data - x_grad
9 total_grad = adv_temp - image_tensor
10 total_grad = torch.clamp(total_grad, -epsilon, epsilon)
11 x_adv = image_tensor + total_grad
12 img_variable.data = x_adv
1output_adv = inceptionv3.forward(img_variable)
2x_adv_pred = labels[torch.max(output_adv.data, 1)[1][0]]
3output_adv_probs = F.softmax(output_adv, dim=1)
4x_adv_pred_prob = round((torch.max(output_adv_probs.data, 1)[0][0]) * 100,4)
5visualize(image_tensor, img_variable.data, total_grad, epsilon, x_pred,x_adv_pred, x_pred_prob, x_adv_pred_prob)
همانطور که میتوان مشاهده کرد، تصویر آشفتهسازی شده از تصویر اصلی قابل تشخیص نیست، اما مدل آن را در دسته «شترمرغ» (Ostrich) دستهبندی کرد. به منظور بررسی بیشتر، چندین کلاس هدف مختف مورد آزمون قرار گرفتند و مدل هر بار درست عمل کرده است. روشهای معرفی شده در اینجا برای تولید نمونههای تخاصمی، بسیار ساده و موثر هستند.
همانطور که پیشتر بیان شد، روشهای تکرار شونده نمونههای تخاصمی بهتری نسبت به روش یک گام، تولید میکنند. روشهای دیگری نیز وجود دارد (DeepFool ،Carlini & Wagner's Attack ،Jacobian-based Saliency Map Attack و دیگر موارد). همچنین، تاکنون روشهایی مانند Defensive Distillation و «آموزش تخاصمی» (Adversarial Training) برای دفاع از مدل در مقابل چنین حملاتی ارائه شدهاند.
اگر نوشته بالا برای شما مفید بود، آموزشهای زیر نیز به شما پیشنهاد میشوند: