基于Pytorch的黑盒攻击
攻击的模型
- 攻击的类型是无目标攻击,改天再尝试下目标攻击
- 攻击的模型是我之前训练好的一个分类网络参考下面这篇博客
-
https://blog.csdn.net/qq_37633207/article/details/108926652
攻击的效果
- 攻击效果还不错,基本上几次迭代就攻击好了,可能是自己训练的网络比较垃圾,自己训练的分类网络精度为93.499%,我太难了
- 先来两张攻击的效果图
- 首先是原来的音频,我这里是随机选取一个音频
波形图对比
- 再来一张图片,由于差距极小,所以我特意把攻击成果的音频直接画在源音频上,仔细就能看见他们之间的差距
- 那些黄色末端的蓝色就是他们攻击攻击音频与原有音频的差距,这种差距对于人耳来说是不可闻的
频谱图对比
- 上面一张为原音频,下面一张为增加扰动后的音频,也就高分贝有略微区别
攻击迭代过程
- 这是我输出的迭代过程,最上面一排是对应score的标签,两个红色的框代表了初始的label以及初始时候的最佳得分,可以看见这个音频被100%的分类为了drilling(电锯声音),经过5次迭代以后的蓝色框,分别代表攻击成功后的label 以及最佳得分,可以看见这段音频被分类为狗叫的概率为96.9499%,而且仅仅迭代了5次,神经网络是多么的脆弱(或者说我写的分类网络是多么的垃圾\手动狗头)
- 神经网络的鲁棒性和决策边界的问题现在也没人能说明白问题,越鲁棒必然可以接受越多量的输入扰动,数据集不可能面面俱到,训练的决策边界也一定不是真正的决策边界,越多量的输入扰动又能必然带来越多的影响,这样鲁棒性反而成为攻击的弱点,鲁棒性真的鲁棒嘛?
增加的扰动
- 由于数据的太过密集,所以打印出来的扰动是下面这个样子的,看起来好像都是一样
- 实际上这些数据还是不一样的,只是这边有将近9w个采样点所以画出来看起来就像是没有变化的,我们下面打印出来看看这些数据,数据太多就只打印了头和尾。
攻击的思路以及代码
完整代码如下
- 其实更推荐jupyter 因为IPython.display很好用
- 这边我代码完全从我的jupyter拷贝过来,可能IPython模块会不能用?
- 本来想写个流程图的,实在太麻烦了,什么流程图都没有代码本身细节。
- 如果一些路径或者文件获取看不懂,可以参考我的另一篇博客,因为这两篇博客是无缝衔接的
-
https://blog.csdn.net/qq_37633207/article/details/108926652
- 我的代码中有大量的注释,特别注意下最后我在调用attack时候集中描述了下attack的参数的含义,这些参数对于理解我代码的含义的过程很重要
import torch
import torch.nn as nn
import torch.nn.functional as F
import random
import librosa
import librosa.display
import IPython.display as ipd
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import os
import copy
import pickle
""" 定义分类网络用于恢复 """
class Classify(nn.Module):
def __init__(self):
super(Classify,self).__init__()
self.fc1=nn.Linear(40,64)
self.dp=nn.Dropout(0.03)
self.fc2=nn.Linear(64,20)
self.fc3=nn.Linear(20,10)
def forward(self,x):
x=F.relu(self.fc1(x))
x=self.dp(x)
x=F.relu(self.fc2(x))
x=F.relu(self.fc3(x))
x=F.softmax(x,dim=1)
return x
""" 此函数用于随机获取一条音频的各种信息 """
def get_x_sr_label_random():
train=pd.read_csv("train/train.csv",sep=',')
i=random.choice(train.index)
audio_name=train.ID[i]
path = os.path.join("train", 'Train', str(audio_name) + '.wav')
print('Class: ', train.Class[i])
label_name=train.Class[i]
label_class=sorted(train.Class.value_counts().index.tolist())
label_index=label_class.index(label_name)
x, sr = librosa.load('train/Train/' + str(train.ID[i]) + '.wav')
return x,sr,label_name,label_index,label_class
""" 此函数用于获得一批加噪后的音频,将来会用于计算平均梯度 """
def get_noise_audio(audio,adv_sample_nums=10,sigma=0.02):#这里的audio是个numpy.array([[xx,xx,xx....]])类型的数据
N=audio.size#获取元素个数
noise_pos=np.random.normal(size=(adv_sample_nums//2,N))
noise=np.concatenate((noise_pos,-1*noise_pos),axis=0)
noise = np.concatenate((np.zeros((1, N)), noise), axis=0)
noise_audios = sigma * noise + audio
return noise_audios,noise#这个你懂的[[][][]]
""" 此函数用于把输入数据计算mfcc特征后,输入网络计算得分 """
def get_score(model,x,sr):#x是个二维的np[[],[],[],[]]
input_datas=np.mean(librosa.feature.mfcc(y=x[0], sr=sr, n_mfcc=40).T,axis=0)
input_datas=input_datas[np.newaxis,:]
#print(input_datas.shape)
for item in x[1:]:
temp_data=np.mean(librosa.feature.mfcc(y=item, sr=sr, n_mfcc=40).T,axis=0)
temp_data=temp_data[np.newaxis,:]
input_datas = np.concatenate((input_datas,temp_data),axis=0)
model.eval()
model.to('cpu')
#print(input_datas.shape)
""" 注意输入网络需要torch。Tensor类型数据,返回的也是tensor 所以都要类型转换 """
with torch.no_grad():
scores=model(torch.from_numpy(input_datas))
return scores.numpy()
""" 这个函数返回四个值 final_loss是除了当前的audio以外加扰动后的noise_aduios的平均损失 是一个具体的数值 estimate_grad的计算公式如下np.mean(loss * noise, axis=0, keepdims=True) / sigma 是一个(1,N)的向量 adver_loss是当前的audio的损失 是一个(1,1)向量 score则是当前迭代的audio的分数 是一个(1,10)的向量 """
def get_grad(noise_audios,noise,scores,loss,sigma):
adver_loss = loss[0]#这是原来当前音频的loss 是个(1,)的shape
score = scores[0]#原来的socre
loss=loss[1:,:]
noise = noise[1:,:]#去除原来样本的noise
final_loss=np.mean(loss)
estimate_grad = np.mean(loss * noise, axis=0, keepdims=True) / sigma # grad的格式是1*N [xx,xx,x,x...]
return final_loss,estimate_grad,adver_loss,score
""" 整合函数attack 这个函数输入的audio是个np.array([xx,xx,......])类型 """
#传入的audio是个[xx,xx,xx,xx]
def attack(model,label_class,audio,sr,true_index,sigma=0.001,max_iter=1000,epsilon=0.002,
max_lr=0.001,min_lr=1e-6,adv_sample_nums=10,
adver_thresh=0,momentum=0.9,plateau_length=5,plateau_drop=2.):
#为audio增加一个batch__size维度
audio=audio[np.newaxis,:]#[[]]
adver=copy.deepcopy(audio)
lower=np.clip(audio-epsilon,-1.,1.)
upper=np.clip(audio+epsilon,-1.,1.)
lr=max_lr
estimate_grad=0
cp_global=[]#存放结果
last_ls=[]#存放近几次的损失 存放个数与plateau_length有关
for iter in range(max_iter):
cp_local=[]
#上一次的估计梯度
pre_grad = copy.deepcopy(estimate_grad)
#获得加噪后的audios 以及未乘上sigma的noise
noise_audios,noise=get_noise_audio(adver,adv_sample_nums,sigma)# shape (adv_smaple_nums+1,N)、(adv_smaple_nums,N)
#计算noise_audios 输入的分数
scores=get_score(model,noise_audios,sr)#scores (adv_sample_nums+1,10)
#根据noise_audios的得分计算出每个noise_audio的loss
loss=loss_fn(scores,true_index,adver_thresh=adver_thresh) #loss (adv_sample_nums+1,1)
#final_loss是除了当前的audio以外加扰动后的noise_aduios的平均损失 是一个具体的数值
#estimate_grad的计算公式如下np.mean(loss * noise, axis=0, keepdims=True) / sigma 是一个(1,N)的向量
#adver_loss是当前的audio的损失 是一个(1,)的numpy.array()对象
#score则是当前迭代的audio的分数 是一个(1,10)的向量
final_loss,estimate_grad,adver_loss,score = get_grad(noise_audios,noise,scores,loss,sigma)
#计算l无穷范数的距离
distance=np.max(np.abs(audio-adver))
#计算当前的label
now_label=label_class[np.argmax(score)]
print("--- iter %d, distance:%f, loss:%f, label:%s ---" % (iter, distance, adver_loss,now_label))
for s in score:
print("{:.4%}".format(s),end=' ')
print('')
if adver_loss == -1 * adver_thresh:
print("------ early stop at iter %d ---" % iter)
cp_local.append(distance)
cp_local.append(adver_loss)
cp_local.append(score)
cp_local.append(0.)
cp_global.append(cp_local)
break
#根据动量以及估计梯度调整梯度
#print(estimate_grad)
estimate_grad = momentum * pre_grad + (1.0 - momentum) * estimate_grad
#下面是根据损失调整学习率
last_ls.append(final_loss)
last_ls = last_ls[-plateau_length:]#仅仅记录最后的5个final_loss
if last_ls[-1] > last_ls[0] and len(last_ls) == plateau_length:#如果损失反而上升了 有可能学习率过大
if lr > min_lr:#如果学习率还可以下降
lr = max(lr / plateau_drop, min_lr)
last_ls = []#重新开始记录final_loss
#更新adver
#print(estimate_grad)
adver-=lr*np.sign(estimate_grad)
#print(abs(audio-adver))
adver=np.clip(adver,lower,upper)
cp_local.append(distance)
cp_local.append(adver_loss)
cp_local.append(score)
cp_global.append(cp_local)
with open("cp_global.plk", "wb") as f:
pickle.dump(cp_global, f)
return adver
""" 接下来就是初始化一些基本的参数了 """
pretrained_model="torchmodel.pth"
model=torch.load(pretrained_model)#初始化模型
x,sr,label_name,label_index,label_class=get_x_sr_label_random()#获得音频的基本信息
""" 集中解释下这些参数 sigma是扰动的系数,不管是librosa读取的数据或者说是我们产生的扰动都必须限制在[-1,1]中 而我们生成扰动的方式采用的是numpy.random.noraml()所以必须乘上一个系数,然后clip保证不会出界 epsilon是我们的最大扰动,我们这里计算扰动采用的是无穷范数,通过epsilon计算出添加扰动以后的上下界 通过上下界再去clip可以保证我们的音频在扰动后听起来还和原来的一样 max_lr和min_lr 是我们的学习速率,我这里采用的是基于动量的学习率,如下公式 公式中的pre_grad是上一次迭代的梯度估计值,estimate_grad则是本次的迭代估计值 公式为:momentum * pre_grad + (1.0 - momentum) * estimate_grad adv_sample_nums是生成的扰动音频的数量,我们将会对这些扰动后的梯度取平均,这个梯度的计算很简单。就是下面这行。 estimate_grad = np.mean(loss * noise, axis=0, keepdims=True) / sigma 这里的loss计算为np.maxmium(scores[1st]-scores[2ed],-1*k) adver_thresh是一个用于控制置信度的参数,这个参数设置的越大,那么最终得到的置信度就越高 这个参数属于【0,1) plateau_length 用于控制 记录的往期loss 的数量 ,当这个值为5,那么只会记录最新的5个loss plateau_drop 用于修改学习率,这个值越大学习率下降的越快 通过上面两个值我们就可以控制学习率的大小,当我们对比往期的loss 发现loss变大了,我们就会根据设置的plateau_drop去调整学习率 """
adver=attack(model=model,label_class=label_class,audio=x,sr=sr,true_index=label_index,
sigma=0.002,max_iter=1000,epsilon=0.005,
max_lr=0.001,min_lr=1e-6,adv_sample_nums=50,
adver_thresh=0,momentum=0.9,plateau_length=5,plateau_drop=2.)
""" 攻击结束了,让我们看看我们攻击后的音频的波形图对比 """
#首先转换下adver的格式
adver=np.array(adver.tolist()[0])
plt.figure(figsize=(14, 7))
plt.subplot(2,1,1)
librosa.display.waveplot(x, sr=sr)
plt.subplot(2,1,2)
librosa.display.waveplot(adver, sr=sr)
""" 再看看频谱图的对比 """
plt.figure(figsize=(14, 7))
X = librosa.stft(x)
Xdb = librosa.amplitude_to_db(abs(X))
plt.subplot(2,1,1)
librosa.display.specshow(Xdb, sr=sr, x_axis='time', y_axis='hz')
X = librosa.stft(adver)
Xdb = librosa.amplitude_to_db(abs(X))
plt.subplot(2,1,2)
librosa.display.specshow(Xdb, sr=sr, x_axis='time', y_axis='hz')
下一步的几个实验
- 尝试训练并攻击一个使用均值滤波的模型
- 真实世界的攻击
- 目标攻击