virtualenv -p python3 $HOME/tmp/deepspeech-venv/
source $HOME/tmp/deepspeech-venv/bin/activate
TODO
-
不满足度计算函数
-
测试
audio_attack.py
import numpy as np
import scipy.io.wavfile as wav
from scipy.signal import butter, lfilter
import client
from components import Instance
cache_path = "./cache/"
result_path = "./result/"
model_path = "DeepSpeech/model/output_graph.pbmm"
lm_path = "DeepSpeech/model/lm.binary"
trie_path = "DeepSpeech/model/trie"
output_file = "result/result.wav"
def levenshtein(str1, str2):
edit = [[i + j for j in range(len(str2) + 1)] for i in range(len(str1) + 1)]
for i in range(1, len(str1) + 1):
for j in range(1, len(str2) + 1):
if str1[i - 1] == str2[j - 1]:
d = 0
else:
d = 1
edit[i][j] = min(edit[i - 1][j] + 1, edit[i][j - 1] + 1, edit[i - 1][j - 1] + d)
return edit[len(str1)][len(str2)]
def load_wav(input_wave_file):
"""
根据文件名导入wav文件
:param input_wave_file: wav文件名
:return: wav文件的numpy数组
"""
fs, audio = wav.read(input_wave_file)
assert fs == 16000 # 确保音频频率为16kHz
print("Load wav from", input_wave_file)
return audio
def save_wav(audio, output_wav_file):
"""
导出wav文件到指定文件
:param audio: 经过处理的音频numpy数组
:param output_wav_file: 导出文件路径
:return: null
"""
wav.write(output_wav_file, 16000, np.array(np.clip(np.round(audio), -2 ** 15, 2 ** 15 - 1), dtype=np.int16))
print('Save wav to', output_wav_file)
class AudioAttack:
"""
算法类
"""
def __init__(self, audio, ds, threshold, len, pn, ss, mi, mr, ex, ns):
self.dis_group = [[0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 1, 4, 4, 4, 4, 4, 3, 4, 4, 3, 3, 4],
[2, 4, 1, 3, 3, 4, 3, 4, 4, 4, 4, 4, 4],
[2, 4, 3, 1, 2, 4, 3, 4, 4, 4, 4, 4, 4],
[2, 4, 3, 2, 1, 4, 3, 4, 3, 4, 4, 3, 4],
[2, 4, 4, 4, 4, 1, 2, 4, 3, 2, 3, 3, 4],
[2, 4, 3, 3, 3, 2, 1, 3, 4, 4, 4, 2, 4],
[2, 3, 4, 4, 4, 4, 3, 1, 2, 4, 4, 4, 3],
[2, 4, 4, 4, 3, 3, 4, 2, 1, 4, 4, 2, 4],
[2, 4, 4, 4, 4, 2, 4, 4, 4, 1, 4, 3, 4],
[2, 3, 4, 4, 4, 3, 4, 4, 4, 4, 1, 4, 4],
[2, 3, 4, 4, 3, 3, 2, 4, 2, 3, 4, 1, 4],
[2, 4, 4, 4, 4, 4, 4, 3, 4, 4, 4, 4, 1]]
self.audio = audio # 原始音频文件的numpy数组
self.audio_length = len # 音频长度(某种意义上即为维度)
self.base_threshold = threshold
self.threshold = threshold # 扰动范围阈值
self.ds = ds # deep speech模型
self.positive_num = pn # 最优集大小
self.sample_size = ss # 备选集大小
self.max_iteration = mi # 最大扰动次数
self.move_rate = mr # 每次更新搜索空间的更新概率
self.max_expand = ex # 增大阈值范围的迭代次数
self.mutation_rate = mr
self.noise_stdev = ns
self.ini_label = client.run_deep_speech(self.ds, self.audio) # 原始音频的识别结果label
self.ini_fitness = -1
self.max_fitness = -1
self.tar_label = None # 攻击的target label
self.string_list = []
self.pop = [] # 最优集
self.pos_pop = [] # 备选集
self.optional = None # 最优扰动
self.group = {' ': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 1, 'f': 5, 'g': 3, 'h': 6, 'i': 7, 'j': 8, 'k': 3,
'l': 4, 'm': 9, 'n': 9, 'o': 10, 'p': 2, 'q': 3, 'r': 11, 's': 12, 't': 4, 'u': 10, 'v': 5,
'w': 10, 'x': 3, 'y': 8, 'z': 12, '\'': 0, '-': 0}
def clear(self):
self.pop = []
self.optional = None
return
def dis_char(self, ch1, ch2):
if ch1 == ch2:
return 0
return self.dis_group[self.group[ch1]][self.group[ch2]]
def dis_string(self, str1, str2):
m = len(str1)
n = len(str2)
if m * n == 0:
return 2 * (m + n)
str1 = "0" + str1
str2 = "0" + str2
dis = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
dis[1][1] = self.dis_char(str1[1], str2[1])
for j in range(2, n + 1):
dis[1][j] = min(dis[1][j - 1] + 2, 2 * (j - 1) + self.dis_char(str1[1], str2[j]))
for i in range(2, m + 1):
dis[i][1] = min(dis[i - 1][1] + 2, 2 * (i - 1) + self.dis_char(str1[i], str2[1]))
for j in range(2, n + 1):
dis[i][j] = dis[i][j - 1] + 2
for left in range(1, i):
k_left = i - left
min_part = 4
for k_right in range(k_left + 1, i + 1):
min_part = min(min_part, self.dis_char(str1[k_right], str2[j]))
dis[i][j] = min(dis[i][j], min_part + dis[k_left][j - 1] + 2 * (i - k_left - 1))
dis[i][j] = min(dis[i][j], self.dis_char(str1[1], str2[j]) + 2 * (i + j - 1))
return dis[m][n]
def random_instance(self):
"""
生成全随机扰动
:param dim:
:param region:
:return:
"""
# completely random
inst = Instance()
features = np.random.randint(-2 ** 15, 2 ** 15 - 1, self.audio_length) * self.noise_stdev
features = self.highpass_filter(features)
mask = np.random.rand(self.audio_length) < self.mutation_rate
inst.setFeatures(features * mask + self.audio)
inst.setLen(self.audio_length)
return inst
def update_optional(self):
"""
将根据新搜索空间新生成的扰动和原有扰动合并并取优
:return:
"""
self.pop.sort(key=lambda instance: instance.getFitness())
self.optional = self.pop[0]
return
def init(self):
"""
初始化
:return:
"""
print("audio length: " + str(self.audio_length))
print("original string: " + self.ini_label)
print("Initializing...")
self.clear()
self.pop = []
self.pos_pop = []
for i in range(self.positive_num):
inst = Instance()
inst_features = self.audio
for j in range(100):
features = np.random.randint(-2 ** 15, 2 ** 15 - 1, self.audio_length) * self.noise_stdev
features = self.highpass_filter(features)
mask = np.random.rand(self.audio_length) < self.mutation_rate
inst_features = inst_features + features * mask
inst.setFeatures(inst_features)
inst.setLen(self.audio_length)
# inst = self.random_instance()
self.pop.append(inst)
self.run_once()
self.pop.sort(key=lambda instance: instance.getFitness())
self.pos_pop = self.pop[:self.positive_num]
print("Finish creating " + str(self.positive_num) + " samples.")
self.optional = self.pos_pop[0].CopyInstance()
return
def highpass_filter(self, data, cutoff=7000, fs=16000, order=10):
b, a = butter(order, cutoff / (0.5 * fs), btype='high', analog=False)
return lfilter(b, a, data)
def run_once(self):
"""
将popset中的所有扰动加上音频本身跑一次deep speech
:param popset: 扰动数组,长度不定
:param rtype:
:return: 设定popset的fitness值
"""
batch_size = len(self.pop)
# 预测
for i in range(batch_size):
audio_interrupted = np.array(np.clip(self.pop[i].getFeatures(), -2 ** 15, 2 ** 15),
dtype='int16')
pred_str = client.run_deep_speech(self.ds, audio_interrupted)
# 使用pred_str计算不满足度,回填入pop
fitness = self.compute_dis(pred_str)
print("result " + str(i) + "\t: " + pred_str + ", fitness: " + str(fitness) + ", max interrupt: " + str(
max(abs(audio_interrupted - self.audio))))
self.pop[i].setString(pred_str)
self.pop[i].setFitness(fitness)
return
def compute_dis(self, pred):
"""
根据识别的str计算不满足度
:param pred:
:return:
"""
# return levenshtein(pred, self.tar_label)
return self.dis_string(pred, self.tar_label)
def roulette(self):
# self.pop.sort(key=lambda instance: instance.getFitness())
# if self.pop[0].getFitness() > (self.max_fitness * 2 / 3):
# print("加速收缩")
# self.pos_pop = self.pop[:self.positive_num]
# return
roulette_rank = []
roulette_sum = 0
length = len(self.pop)
for i in range(length):
if self.pop[i].getFitness() == 0:
self.pos_pop = [self.pop[i]]
return
else:
rank = int(10000 / self.pop[i].getFitness())
roulette_sum += rank
roulette_rank.append(roulette_sum)
self.pos_pop = []
i = 0
while i < self.positive_num:
rand = np.random.randint(roulette_sum)
choose_rank = -1
for j in range(0, length):
if rand < roulette_rank[j]:
choose_rank = j
break
if self.pop[choose_rank] not in self.pos_pop:
self.pos_pop.append(self.pop[choose_rank])
i += 1
self.pos_pop.sort(key=lambda instance: instance.getFitness())
return
def crossover(self):
rand1 = np.random.randint(self.positive_num)
rand2 = np.random.randint(self.positive_num)
while rand1 == rand2:
rand2 = np.random.randint(self.positive_num)
sample1 = self.pos_pop[rand1].getFeatures()
sample2 = self.pos_pop[rand2].getFeatures()
mask = np.random.rand(self.audio_length) < 0.5
return sample1 * mask + sample2 * (1 - mask)
def mutation(self, sample):
noise = np.random.randint(-2 ** 15, 2 ** 15 - 1, self.audio_length) * self.noise_stdev
noise = self.highpass_filter(noise)
mask = np.random.rand(self.audio_length) < self.mutation_rate
return sample + noise * mask
def save_result(self, number):
f = open("result_" + str(number) + ".txt", 'w', encoding='utf-8')
for i in range(len(self.string_list)):
f.write("result\t" + str(i) + "\t:\t" + self.string_list[i] + "\n")
f.close()
def opt(self, target=None):
self.tar_label = target
self.max_fitness = self.dis_string(target, self.ini_label)
# 初始化
self.init()
self.string_list = []
# run
time = 0
while time < self.max_iteration:
print("iter time: " + str(time))
self.string_list.append(self.optional.getString())
if time % 100 == 0:
self.save_result(time)
save_wav(self.pos_pop[0].getFeatures(), "wav_" + str(time) + ".wav")
# 如果fitness满足要求则退出
if self.optional.getFitness() == 0:
print("Find attack in " + str(time) + " times. File saved in result.txt.")
# 保存文件
self.save_result(time)
save_wav(self.optional.getFeatures(), "result.wav")
return
for i in range(self.positive_num):
print("best " + str(i) + ": " + self.pos_pop[i].getString() + ", fitness: " + str(
self.pos_pop[i].getFitness()))
self.pop = []
for i in range(self.positive_num * self.sample_size):
sample = self.crossover()
ins = Instance()
ins.setLen(self.audio_length)
ins.setFeatures(self.mutation(sample))
self.pop.append(ins)
self.run_once()
self.pop.extend(self.pos_pop)
self.roulette()
self.optional = self.pos_pop[0].CopyInstance()
time += 1
self.save_result(time)
return
m_audio_path = "../audio/character.wav"
m_model_path = "DeepSpeech/model/output_graph.pbmm"
m_lm_path = "DeepSpeech/model/lm.binary"
m_trie_path = "DeepSpeech/model/trie"
m_ds = client.load_model(m_model_path)
m_ds = client.update_ds(m_ds, m_lm_path, m_trie_path)
m_audio = client.get_audio_array(m_ds, m_audio_path)
m_target = 'care'
m_threshold = 256
m_len = m_audio.shape[0]
m_pn = 10
m_ss = 5
m_mi = 3000
m_mr = 0.1
m_ex = 8
m_ns = 0.01
m_audio_attack = AudioAttack(audio=m_audio,
ds=m_ds,
threshold=m_threshold,
len=m_len,
pn=m_pn,
ss=m_ss,
mi=m_mi,
mr=m_mr,
ex=m_ex,
ns=m_ns)
m_audio_attack.opt(m_target)
components.py
import copy
class Instance:
__feature = [] # feature value in each dimension
__len = 0
__fitness = 0 # fitness of objective function under those features
__string = ""
def __init__(self):
self.__feature = []
self.__fitness = 0
self.__len = 0
# return feature value in index-th dimension
def getFeature(self, index):
return self.__feature[index]
# return features of all dimensions
def getFeatures(self):
return copy.copy(self.__feature)
# set feature value in index-th dimension
def setFeature(self, index, v):
self.__feature[index] = v
# set features of all dimension
def setFeatures(self, v):
self.__feature = v
def getString(self):
return self.__string
def setString(self, s):
self.__string = s
# return fitness under those features
def getFitness(self):
return self.__fitness
# set fitness
def setFitness(self, fit):
self.__fitness = fit
def getLen(self):
return self.__len
def setLen(self, l):
self.__len = l
#
def Equal(self, ins):
return self.__fitness == ins.getFitness()
# copy this instance
def CopyInstance(self):
copy_ = Instance()
features = copy.copy(self.__feature)
copy_.setFeatures(features)
copy_.setFitness(self.__fitness)
copy_.setString(self.__string)
copy_.setLen(self.__len)
return copy_
def CopyFromInstance(self, ins_):
self.__feature = copy.copy(ins_.getFeatures())
self.__fitness = ins_.getFitness()
self.__string = ins_.getString()
self.__len = ins_.getLen()
distance.py
group = {' ': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 1, 'f': 5, 'g': 3, 'h': 6, 'i': 7, 'j': 8, 'k': 3,
'l': 4, 'm': 9, 'n': 9, 'o': 10, 'p': 2, 'q': 3, 'r': 11, 's': 12, 't': 4, 'u': 10, 'v': 5,
'w': 10, 'x': 3, 'y': 8, 'z': 12, '\'': 0, '-': 0}
dis_group = [[0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 1, 4, 4, 4, 4, 4, 3, 4, 4, 3, 3, 4],
[2, 4, 1, 3, 3, 4, 3, 4, 4, 4, 4, 4, 4],
[2, 4, 3, 1, 2, 4, 3, 4, 4, 4, 4, 4, 4],
[2, 4, 3, 2, 1, 4, 3, 4, 3, 4, 4, 3, 4],
[2, 4, 4, 4, 4, 1, 2, 4, 3, 2, 3, 3, 4],
[2, 4, 3, 3, 3, 2, 1, 3, 4, 4, 4, 2, 4],
[2, 3, 4, 4, 4, 4, 3, 1, 2, 4, 4, 4, 3],
[2, 4, 4, 4, 3, 3, 4, 2, 1, 4, 4, 2, 4],
[2, 4, 4, 4, 4, 2, 4, 4, 4, 1, 4, 3, 4],
[2, 3, 4, 4, 4, 3, 4, 4, 4, 4, 1, 4, 4],
[2, 3, 4, 4, 3, 3, 2, 4, 2, 3, 4, 1, 4],
[2, 4, 4, 4, 4, 4, 4, 3, 4, 4, 4, 4, 1]]
def dis_char(ch1, ch2):
if ch1 == ch2:
return 0
return dis_group[group[ch1]][group[ch2]]
def dis_string(str1, str2):
m = len(str1)
n = len(str2)
if m * n == 0:
return 2 * (m + n)
str1 = "0" + str1
str2 = "0" + str2
dis = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
dis[1][1] = dis_char(str1[1], str2[1])
for j in range(2, n + 1):
dis[1][j] = min(dis[1][j - 1] + 2, 2 * (j - 1) + dis_char(str1[1], str2[j]))
for i in range(2, m + 1):
dis[i][1] = min(dis[i - 1][1] + 2, 2 * (i - 1) + dis_char(str1[i], str2[1]))
for j in range(2, n + 1):
dis[i][j] = dis[i][j - 1] + 2
for left in range(1, i):
k_left = i - left
min_part = 4
for k_right in range(k_left + 1, i + 1):
min_part = min(min_part, dis_char(str1[k_right], str2[j]))
dis[i][j] = min(dis[i][j], min_part + dis[k_left][j - 1] + 2 * (i - k_left - 1))
dis[i][j] = min(dis[i][j], dis_char(str1[1], str2[j]) + 2 * (i + j - 1))
return dis[m][n]
print(dis_string("studio", "curious"))
client.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import argparse
import numpy as np
import shlex
import subprocess
import sys
import wave
import json
from deepspeech import Model, printVersions
from timeit import default_timer as timer
try:
from shhlex import quote
except ImportError:
from pipes import quote
def convert_samplerate(audio_path, desired_sample_rate):
sox_cmd = 'sox {} --type raw --bits 16 --channels 1 --rate {} --encoding signed-integer --endian little --compression 0.0 --no-dither - '.format(
quote(audio_path), desired_sample_rate)
try:
output = subprocess.check_output(shlex.split(sox_cmd), stderr=subprocess.PIPE)
except subprocess.CalledProcessError as e:
raise RuntimeError('SoX returned non-zero status: {}'.format(e.stderr))
except OSError as e:
raise OSError(e.errno,
'SoX not found, use {}hz files or install it: {}'.format(desired_sample_rate, e.strerror))
return desired_sample_rate, np.frombuffer(output, np.int16)
def metadata_to_string(metadata):
return ''.join(item.character for item in metadata.items)
def words_from_metadata(metadata):
word = ""
word_list = []
word_start_time = 0
# Loop through each character
for i in range(0, metadata.num_items):
item = metadata.items[i]
# Append character to word if it's not a space
if item.character != " ":
word = word + item.character
# Word boundary is either a space or the last character in the array
if item.character == " " or i == metadata.num_items - 1:
word_duration = item.start_time - word_start_time
if word_duration < 0:
word_duration = 0
each_word = dict()
each_word["word"] = word
each_word["start_time "] = round(word_start_time, 4)
each_word["duration"] = round(word_duration, 4)
word_list.append(each_word)
# Reset
word = ""
word_start_time = 0
else:
if len(word) == 1:
# Log the start time of the new word
word_start_time = item.start_time
return word_list
def metadata_json_output(metadata):
json_result = dict()
json_result["words"] = words_from_metadata(metadata)
json_result["confidence"] = metadata.confidence
return json.dumps(json_result)
class VersionAction(argparse.Action):
def __init__(self, *args, **kwargs):
super(VersionAction, self).__init__(nargs=0, *args, **kwargs)
def __call__(self, *args, **kwargs):
printVersions()
exit(0)
def load_model(model_path, beam_width=500):
print('Loading model from file {}'.format(model_path), file=sys.stderr)
model_load_start = timer()
ds = Model(model_path, beam_width)
model_load_end = timer() - model_load_start
print('Loaded model in {:.3}s.'.format(model_load_end), file=sys.stderr)
return ds
def get_audio_array(ds, audio_path):
desired_sample_rate = ds.sampleRate()
fin = wave.open(audio_path, 'rb')
fs = fin.getframerate()
if fs != desired_sample_rate:
print(
'Warning: original sample rate ({}) is different than {}hz. Resampling might produce erratic speech recognition.'.format(
fs, desired_sample_rate), file=sys.stderr)
fs, audio = convert_samplerate(audio_path, desired_sample_rate)
else:
audio = np.frombuffer(fin.readframes(fin.getnframes()), np.int16)
fin.close()
return audio
def update_ds(ds, lm_path, trie_path, lm_alpha=0.75, lm_beta=1.85):
if lm_path and trie_path:
ds.enableDecoderWithLM(lm_path, trie_path, lm_alpha, lm_beta)
return ds
def run_deep_speech(ds, audio, is_extended=False, is_json=False):
if is_extended:
return metadata_to_string(ds.sttWithMetadata(audio))
elif is_json:
return metadata_json_output(ds.sttWithMetadata(audio))
else:
return ds.stt(audio)
对抗性攻击介绍
因为神经网络具有的强表达能力,使得它们能够很好地适应于各种机器学习任务,但在超过多个网络架构和数据集上,它们容易受到敌对攻击的影响。这些攻击通过对原始输入增加小的扰动就会使网络对输入产生错误的分类,而人类的判断却不会受到这些扰动的影响。
到目前为止,相比其他领域,如语音系统领域,为图像输入生成对抗样本的工作已经做了很多。而从个性化语音助手,如亚马逊的 Alexa 和苹果公司的 Siri ,到车载的语音指挥技术,这类系统面临的一个主要挑战是正确判断用户正在说什么和正确解释这些话的意图,深度学习帮助这些系统更好的理解用户,然而存在一个潜在的问题是针对系统进行目标对抗攻击。
在自动语音识别(ASR)系统中,深度循环网络在语音转录的应用已经取得了令人印象深刻的进步。许多人已经证明,小的对抗干扰就可以欺骗深层神经网络,使其错误地预测一个特定目标。目前关于欺骗 ASR 系统的工作主要集中在白盒攻击上,在这种攻击中模型架构和参数是已知的。
对抗性攻击(Adversarial Attacks):机器学习算法的输入形式为数值型向量,通过设计一种特别的输入以使模型输出错误的结果,这被称为对抗性攻击。根据攻击者对网络的了解信息,有不同的执行敌对攻击的方法:
-
白盒攻击:对模型和训练集完全了解;如果给定一个网络的参数,白盒攻击是最成功的,例如 Fast Grandient Sign Method 和 DeepFool;
-
黑盒攻击:对模型不了解,对训练集不了解或了解很少;然而,攻击者能够访问网络的所有参数,这在实践中是不现实的。在黑盒设置中,当攻击者只能访问网络的逻辑或输出时,要始终如一地创建成功的敌对攻击就很难了。在某些特殊黑盒设置中,如果攻击者创建了一个模型,这个模型是目标模型的一个近似或逼近模型,就可以重用白盒攻击方法。即使攻击可以转移,在某些领域的网络中也需要更多的技术来解决这个任务。
攻击策略:
-
基于梯度的方法:FGSM 快速梯度法;
-
基于优化的方法:使用精心设计的原始输入来生成对抗样本;
▌以往的研究
在先前的研究工作中,Cisse 等人开发了一个通用攻击框架,用于在包括图像和音频在内的各种模型中工作。与图像相比,音频为模型了提供了一个更大的挑战。虽然卷积神经网络可以直接作用于图像的像素值,但 ASR 系统 通常需要对输入音频进行大量预处理。
最常见的是梅尔-频率转换(MFC),本质上是采样音频文件的傅里叶变换,将音频转换成一个显示频率随时间变化的 spectogram,如下图中的DeepSpeech 模型,使用 spectogram 作为初始输入。当 Cisse 等人将他们的方法应用到音频样本时,他们遇到了通过了 MFC 转换层进行反向传播的路障。Carlini 和 Wagner 克服了这一挑战,开发了一种通过 MFC 层传递渐变的方法。
他们将方法应用到 Mozilla DeepSpeech 模型中(该模型是一个复杂、反复、字符级的网络,解码每秒 50 个字符的翻译)。他们取得了令人印象深刻的结果,生成超过 99.9% 的样本,类似于目标攻击的 100%,虽然这次攻击的成功为白盒攻击打开了新大门,但在现实生活中,对手通常不知道模型架构或参数。Alzantot 等人证明,针对 ASR 系统的目标攻击是可能的,利用遗传算法的方法,能够迭代地将噪音应用到音频样本中,这次攻击是在语音命令分类模型上进行的,属于轻量级的卷积模型,用于对 50 个不同的单词短语进行分类。
▌本文研究
本文采用一种黑盒攻击,并结合了遗传算法与梯度估计的方法创建有针对性的对抗音频来实现欺骗 ASR 系统。第一阶段攻击是由遗传算法进行的,这是一种无需计算梯度的优化方法。对候选样本总体进行迭代,直到一个合适的样本产生。为了限制过度的突变和多余的噪声,我们用动量突变更新改进标准遗传算法。
攻击的第二阶段使用了梯度估计,因为单个音频点的梯度是估计的,因此当敌对样本接近目标时,允许设置更精细的噪声。这两中方法的组合提供了在3000 次迭代之后实现了 94.6% 的音频文件的相似性,89.25% 目标攻击相似性。在更复杂的深度语音系统上困难在于试图将黑盒优化应用到一个深度分层、高度非线性的解码器模型中。尽管如此,两种不同方法和动量突变的结合为这项任务带来了新的成功。
▌数据与方法
-
数据集:攻击的数据集从 Common Voice 测试集中获取前 100 个音频样本。对于每一个,随机生成一个 2 字的目标短语并应用我们的黑盒方法构建第一个对抗样本,每个数据集中的样例是一个 .wav 文件,可以很容易地反序列化成 numpy 数组,从而我们的算法直接作用于 numpy 数组避免了处理问题的难度。
-
受害者模型:我们攻击的模型是在 Mozilla 上开源,Tensorflow 中实现的百度深度语音模型。尽管我们可以使用完整的模型,但是我们仍将其视为黑盒攻击,只访问模型的输出逻辑。在执行 MFC 转换后,该模型由 3 层卷积组成,后面一个双向 LSTM,最后是一个全连接层。
-
遗传算法:如前所述,Alzantot 等人用标准的遗传算法演示了黑盒对抗攻击在语音到文本系统上的成功。带有 CTC 损失的遗传算法对于这种性质的问题很有效,因为它完全独立于模型的梯度。
-
梯度估计:当目标空间很大时,遗传算法可以很好的工作,而相对较多的突变方向可能是有益的,这些算法的优势在于能够有效地搜索大量空间。然而,当解码的距离和目标解码低于某个阈值时,需要切换到第二阶段,这时候梯度评估技术更有效,对抗样本已经接近目标,梯度估计会为更有信息的干扰做出权衡。梯度评估技术来源于 Nitin Bhagoji 在 2017 年的一篇研究图像领域黑盒攻击的论文。
▌结果与结论
评价标准:采用了两种主要方式评估算法性能;一是精确敌对音频样本被解码到所需目标短语的准确性;为此,我们使用 Levenshtein 距离或最小字符编辑距离。二是确定原始音频样本和敌对音频样本之间的相似性。
实验结果:
-
在我们运行算法的音频样本中,在使用 Levenshtein 距离的最终解码短语和目标之间取得了 89.25% 的相似性;最终敌对样本和原始样本相关性为 94.6%。在 3000 次迭代后,平均最终 Levenshtein 距离是 2.3,35% 的敌对样本在不到 3000 次迭代情况下完成了精准解码,22% 的敌对样本在不到 1000 迭代时间内完成了精准解码。
-
本文提出的算法性能与表中数据结果有所不同,在几个迭代中运行算法可以产生更高的成功率,事实上,在成功率和相似率之间很明显存在着权衡,这样就可以通过调整阈值来满足攻击者的不同需要。
-
对比白盒攻击、黑盒攻击单个单词(分类)、以及我们所提出的方法:通过两种波形的重叠,可以看出原始音频样本与对抗样本之间的相似性,如下图显示出来的,35% 的攻击是成功的强调了黑盒的事实,对抗攻击的除了确定性同时也是非常有效的。
实验结论:
我们在结合遗传算法和梯度估计的过程中实现了黑盒对抗,它能产生比每个算法单独产生的更好的样本。
从最初使用遗传算法使大多数音频样本的转录近乎完美,同时还保持高度的相似性。虽然这很大程度上还是一种概念性的验证,通过本文的研究展示了使用直接方法的黑盒模型可以实现针对性对抗攻击。
此外,加入动量突变和在高频中加入噪声,提高了我们的方法的有效性,强调了结合遗传算法和梯度估计的优点。将噪声限制在高频率域上,使我们的相似度得到提高。通过结合所有这些方法,能够达到我们的最高结果。