376 lines
16 KiB
Python
376 lines
16 KiB
Python
import warnings
|
||
|
||
import matplotlib
|
||
|
||
# matplotlib.use('Agg')
|
||
|
||
import networkx as nx
|
||
import matplotlib.pyplot as plt
|
||
matplotlib.use('Agg')
|
||
import numpy as np
|
||
import torch
|
||
import pandas as pd
|
||
import ruptures as rpt
|
||
from scipy.optimize import differential_evolution
|
||
from sklearn.metrics import mean_squared_error
|
||
from query_probability import query_one, load_ucr
|
||
warnings.filterwarnings('ignore')
|
||
|
||
|
||
def detect_change_points(data, model="l2", pen=1):
|
||
algo = rpt.Pelt(model=model,min_size=1,jump=1).fit(data)
|
||
#algo = rpt.Binseg(model=model, min_size=1, jump=1).fit(data)
|
||
bkps = algo.predict(pen=pen)
|
||
#print('=======',bkps)
|
||
return bkps
|
||
|
||
def build_local_graph(data, center_index, window_size):
|
||
"""构建局部图"""
|
||
start = max(0, center_index - window_size // 2)
|
||
end = min(len(data), center_index + window_size // 2)
|
||
G = nx.Graph()
|
||
for i in range(start, end):
|
||
for j in range(i + 1, end):
|
||
weight = np.linalg.norm(data[i] - data[j])
|
||
G.add_edge(i, j, weight=weight)
|
||
return G
|
||
|
||
def find_important_nodes_via_components(G):
|
||
"""使用连通分量动态确定次要变点的数量"""
|
||
components = list(nx.connected_components(G))
|
||
important_nodes = []
|
||
for component in components:
|
||
if len(component) > 1: # 考虑大于一个节点的组件
|
||
important_nodes.extend(component)
|
||
return list(important_nodes)
|
||
|
||
def process_change_points(data, change_points, window_size):
|
||
all_important_nodes = set()
|
||
for cp in change_points:
|
||
G = build_local_graph(data, cp, window_size)
|
||
important_nodes = find_important_nodes_via_components(G)
|
||
print(cp, '------', important_nodes)
|
||
all_important_nodes.update(important_nodes)
|
||
all_important_nodes.update((change_points))
|
||
return all_important_nodes
|
||
|
||
def get_magnitude(run_tag, factor, normalize,gpu):
|
||
'''
|
||
:param run_tag:
|
||
:param factor:
|
||
:return: Perturbed Magnitude
|
||
'''
|
||
data = load_ucr('data/' + run_tag + '/' + run_tag + '_attack'+gpu+'.txt', normalize=normalize)
|
||
X = data[:, 1:]
|
||
|
||
max_magnitude = X.max(1)
|
||
min_magnitude = X.min(1)
|
||
mean_magnitude = np.mean(max_magnitude - min_magnitude)
|
||
|
||
perturbed_mag = mean_magnitude * factor
|
||
print('Perturbed Magnitude:', perturbed_mag)
|
||
|
||
return perturbed_mag
|
||
|
||
|
||
class Attacker:
|
||
|
||
def __init__(self, run_tag, model_type, cuda, normalize, e,device,gpu):
|
||
self.run_tag = run_tag
|
||
self.model_type = model_type
|
||
self.cuda = cuda
|
||
#self.intervals = get_attack_position(self.run_tag, self.top_k)
|
||
self.normalize = normalize
|
||
self.e = e
|
||
self.device = device
|
||
self.gpu = gpu
|
||
|
||
def perturb_ts(self, perturbations, ts, attack_pos):
|
||
'''
|
||
:param perturbations:formalized as a tuple(x,e),x(int) is the x-coordinate,e(float) is the epsilon,e.g.,(2,0.01)
|
||
:param ts: time series
|
||
:return: perturbed ts
|
||
'''
|
||
# first we copy a ts
|
||
|
||
ts_tmp = np.copy(ts)
|
||
|
||
coordinate = 0 # 初始化perturbations数组的索引
|
||
for i in range(len(attack_pos)):
|
||
if attack_pos[i] == 1:
|
||
ts_tmp[i] += perturbations[coordinate]
|
||
coordinate += 1
|
||
# for interval in self.intervals:
|
||
# for i in range(int(interval[0]), int(interval[1])):
|
||
# ts_tmp[i] += perturbations[coordinate]
|
||
# coordinate += 1
|
||
return ts_tmp
|
||
|
||
def plot_per(self, perturbations, ts, target_class, sample_idx,attack_pos, prior_probs, attack_probs, factor):
|
||
|
||
# Obtain the perturbed ts
|
||
ts_tmp = np.copy(ts)
|
||
ts_perturbed = self.perturb_ts(perturbations=perturbations, ts=ts, attack_pos=attack_pos)
|
||
# Start to plot
|
||
plt.figure(figsize=(6, 4))
|
||
plt.plot(ts_tmp, color='b', label='Original %.2f' % prior_probs)
|
||
plt.plot(ts_perturbed, color='r', label='Perturbed %.2f' % attack_probs)
|
||
plt.xlabel('Time', fontsize=12)
|
||
|
||
if target_class == -1:
|
||
plt.title('Untargeted: Sample %d, eps_factor=%.3f' %
|
||
(sample_idx, factor), fontsize=14)
|
||
else:
|
||
plt.title('Targeted(%d): Sample %d, eps_factor=%.3f' %
|
||
(target_class, sample_idx, factor), fontsize=14)
|
||
|
||
plt.legend(loc='upper right', fontsize=8)
|
||
plt.savefig('result_' + str(factor) + '_' + str(self.model_type) + '/'
|
||
+ self.run_tag + '/figures'+self.gpu+'/' + self.run_tag +'_' + str(sample_idx) + '.png')
|
||
# plt.show()
|
||
|
||
def fitness(self, device,perturbations, ts, sample_idx, queries,attack_pos, target_class=-1):
|
||
#device = torch.device("cuda:0" if self.cuda else "cpu")
|
||
perturbations = torch.tensor(perturbations).to(device)
|
||
#ts = torch.tensor(ts, device='cuda')
|
||
|
||
queries[0] += 1
|
||
|
||
ts_perturbed = self.perturb_ts(perturbations, ts,attack_pos = attack_pos)
|
||
prob, _, _, _, _ = query_one(run_tag=self.run_tag,device=device, idx=sample_idx, attack_ts=ts_perturbed,
|
||
target_class=target_class, normalize=self.normalize,
|
||
cuda=self.cuda, model_type=self.model_type, e=self.e,gpu=self.gpu)
|
||
prob = torch.tensor(prob)
|
||
|
||
if target_class != -1:
|
||
prob = 1 - prob
|
||
|
||
return prob # The fitness function is to minimize the fitness value
|
||
|
||
def attack_success(self,device,perturbations, ts, sample_idx, attack_pos, iterations, target_class=-1, verbose=True):
|
||
iterations[0] += 1
|
||
print('The %d iteration' % iterations[0])
|
||
ts_perturbed = self.perturb_ts(perturbations, ts, attack_pos)
|
||
#ts_perturbed = torch.tensor(ts_perturbed, device='cuda')
|
||
# Obtain the perturbed probability vector and the prior probability vector
|
||
prob, prob_vector, prior_prob, prior_prob_vec, real_label = query_one(self.run_tag, device,idx=sample_idx,
|
||
attack_ts=ts_perturbed,
|
||
target_class=target_class,
|
||
normalize=self.normalize,
|
||
verbose=verbose, cuda=self.cuda,
|
||
model_type=self.model_type,
|
||
e=self.e,gpu=self.gpu)
|
||
|
||
predict_class = torch.argmax(prob_vector).to(device)
|
||
prior_class = torch.argmax(prior_prob_vec).to(device)
|
||
real_label = real_label.to(device)
|
||
|
||
# Conditions for early termination(empirical-based estimation), leading to save the attacking time
|
||
# But it may judge incorrectly that this may decrease the success rate of the attack.
|
||
if (iterations[0] > 5 and prob > 0.99) or \
|
||
(iterations[0] > 20 and prob > 0.9):
|
||
print('The %d sample is not expected to successfully attack.' % sample_idx)
|
||
print('prob: ',prob)
|
||
return True
|
||
|
||
if prior_class != real_label:
|
||
print('The %d sample cannot be classified correctly, no need to attack' % sample_idx)
|
||
return True
|
||
|
||
if prior_class == target_class:
|
||
print(
|
||
'The true label of %d sample equals to target label, no need to attack' % sample_idx)
|
||
return True
|
||
|
||
if verbose:
|
||
print('The Confidence of current iteration: %.4f' % prob)
|
||
print('########################################################')
|
||
|
||
# The criterion of attacking successfully:
|
||
# Untargeted attack: predicted label is not equal to the original label.
|
||
# Targeted attack: predicted label is equal to the target label.
|
||
if ((target_class == -1 and predict_class != prior_class) or
|
||
(target_class != -1 and predict_class == target_class)):
|
||
print('##################### Attack Successfully! ##########################')
|
||
|
||
return True
|
||
|
||
def attack(self, sample_idx,device, target_class=-1, factor=0.04,
|
||
max_iteration=50, popsize=200, verbose=True):
|
||
|
||
test = load_ucr('data/' + self.run_tag + '/' + self.run_tag + '_attack'+self.gpu+'.txt'
|
||
, normalize=self.normalize)
|
||
#attack_poses = np.loadtxt('data/' + self.run_tag + '/' + self.run_tag + '_attackPos'+self.gpu+'.txt')
|
||
|
||
ori_ts = test[sample_idx][1:]
|
||
#变点检测找区间
|
||
length = ori_ts.shape
|
||
|
||
result = detect_change_points(ori_ts)
|
||
sampled_indices = np.zeros(len(ori_ts), dtype=int)
|
||
if length[0] > 500 or len(result) > 0.4 * length[0]:
|
||
|
||
# 设置基本窗口大小和基本采样密度
|
||
base_window_size = 0
|
||
base_density_factor = 0.01
|
||
else:
|
||
# 设置基本窗口大小和基本采样密度
|
||
base_window_size = int(0.1 * length[0])
|
||
base_density_factor = 0.5
|
||
|
||
# 计算变点的重要性并调整窗口大小和采样密度
|
||
for i, cp in enumerate(result[:-1]):
|
||
# 计算变点前后的统计差异
|
||
if i == 0:
|
||
pre_mean = np.mean(ori_ts[:cp])
|
||
else:
|
||
pre_mean = np.mean(ori_ts[result[i - 1]:cp])
|
||
post_mean = np.mean(ori_ts[cp:result[i + 1]])
|
||
|
||
# 重要性评估:差异的绝对值
|
||
importance = abs(post_mean - pre_mean)
|
||
|
||
# 动态调整窗口大小和采样密度
|
||
if length[0] > 500 or len(result) > 0.1 * length[0]:
|
||
window_size = int(base_window_size + int(importance * 5)) # 重要性越大,窗口越大
|
||
density_factor = base_density_factor + importance * 0.1 # 重要性越大,采样密度越高
|
||
else:
|
||
window_size = int(base_window_size + int(importance * 10)) # 重要性越大,窗口越大
|
||
density_factor = base_density_factor + importance * 0.3 # 重要性越大,采样密度越高
|
||
|
||
|
||
# 设置窗口的开始和结束位置
|
||
start = max(cp - window_size // 2, 0)
|
||
end = min(cp + window_size // 2, len(ori_ts))
|
||
|
||
# 在窗口内随机采样
|
||
sample_count = int((end - start) * density_factor)
|
||
if sample_count > 0:
|
||
samples = np.random.choice(range(start, end), sample_count, replace=True)
|
||
sampled_indices[samples] = 1
|
||
attack_pos = sampled_indices
|
||
|
||
#attack_pos = attack_poses[sample_idx,:]
|
||
# attack_pos = attack_poses.head(sample_idx)
|
||
# attack_pos = attack_pos.apply(pd.to_numeric, errors='coerce').fillna(0)
|
||
|
||
#ori_ts = torch.tensor(ori_ts).to(device)
|
||
attacked_probs, attacked_vec, prior_probs, prior_vec, real_label = query_one(self.run_tag,device,idx=sample_idx,
|
||
attack_ts=ori_ts,
|
||
target_class=target_class,
|
||
normalize=self.normalize,
|
||
verbose=False,
|
||
cuda=self.cuda, e=self.e,
|
||
model_type=self.model_type,gpu=self.gpu)
|
||
prior_class = torch.argmax(prior_vec).to(device)
|
||
if prior_class != real_label:
|
||
print('The %d sample cannot be classified correctly, no need to attack' % sample_idx)
|
||
return ori_ts,ori_ts, [prior_probs, attacked_probs, 0, 0, 0, 0, 0, 'WrongSample']
|
||
|
||
|
||
# Get the maximum perturbed magnitude
|
||
perturbed_magnitude = get_magnitude(self.run_tag, factor, normalize=self.normalize,gpu=self.gpu)
|
||
|
||
bounds = []
|
||
|
||
# for interval in self.intervals:
|
||
# steps_count += int(interval[1]) - int(interval[0])
|
||
# for i in range(int(interval[0]), int(interval[1])):
|
||
# bounds.append((-1 * perturbed_magnitude, perturbed_magnitude))
|
||
steps_count = attack_pos.sum()
|
||
for i in range(len(attack_pos)):
|
||
if attack_pos[i] == 1:
|
||
bounds.append((-1 * perturbed_magnitude, perturbed_magnitude))
|
||
|
||
print('The length of shapelet interval', steps_count)
|
||
#print('The length of bounds', len(bounds))
|
||
popmul = max(1, popsize // len(bounds))
|
||
# Record of the number of iterations
|
||
iterations = [0]
|
||
queries = [0]
|
||
|
||
def fitness_fn(perturbations):
|
||
|
||
return self.fitness(perturbations=perturbations, ts=ori_ts, queries=queries,
|
||
sample_idx=sample_idx, attack_pos=attack_pos,target_class=target_class,device=device)
|
||
|
||
def callback_fn(x, convergence):
|
||
|
||
return self.attack_success(perturbations=x, ts=ori_ts,
|
||
sample_idx=sample_idx,
|
||
attack_pos = attack_pos,
|
||
iterations=iterations,
|
||
target_class=target_class,
|
||
verbose=verbose,device=device)
|
||
|
||
attack_result = differential_evolution(func=fitness_fn, bounds=bounds
|
||
, maxiter=max_iteration, popsize=popmul
|
||
, recombination=0.7, callback=callback_fn,
|
||
atol=-1, polish=False)
|
||
|
||
attack_ts = self.perturb_ts(attack_result.x, ori_ts, attack_pos)
|
||
|
||
mse = mean_squared_error(ori_ts, attack_ts)
|
||
|
||
attacked_probs, attacked_vec, prior_probs, prior_vec, real_label = query_one(self.run_tag,device, idx=sample_idx,
|
||
attack_ts=attack_ts,
|
||
target_class=target_class,
|
||
normalize=self.normalize,
|
||
verbose=False,
|
||
cuda=self.cuda, e=self.e,
|
||
model_type=self.model_type,gpu=self.gpu)
|
||
|
||
predicted_class = torch.argmax(attacked_vec).to(device)
|
||
prior_class = torch.argmax(prior_vec).to(device)
|
||
|
||
if prior_class != real_label:
|
||
success = 'WrongSample'
|
||
|
||
elif prior_class == target_class:
|
||
success = 'NoNeedAttack'
|
||
|
||
else:
|
||
if (predicted_class.item() != prior_class.item() and target_class == -1) \
|
||
or (predicted_class.item() == target_class and target_class != -1):
|
||
success = 'Success'
|
||
else:
|
||
success = 'Fail'
|
||
|
||
if success == 'Success':
|
||
self.plot_per(perturbations=attack_result.x, ts=ori_ts, target_class=target_class,
|
||
sample_idx=sample_idx, attack_pos=attack_pos, prior_probs=prior_probs, attack_probs=attacked_probs, factor=factor)
|
||
|
||
return ori_ts, attack_ts, [prior_probs, attacked_probs, prior_class.item(),
|
||
predicted_class.item(), queries[0], mse, iterations[0], success]
|
||
|
||
|
||
if __name__ == '__main__':
|
||
idx = [1]
|
||
attacker = Attacker(run_tag='ECG200', top_k=3, model_type='f', e=1499,
|
||
cuda=True, normalize=False)
|
||
for idx in idx:
|
||
attack_ts, info = attacker.attack(sample_idx=idx, target_class=-1, factor=0.01,
|
||
max_iteration=200, popsize=1, verbose=True)
|
||
#这里是生成对抗样本
|
||
if info[-1] == 'Success':
|
||
file = open('attack_time_series.txt', 'w+')
|
||
file.write('%d %d ' % (idx, info[3])) # save the sample index and the perturbed label
|
||
for i in attack_ts:
|
||
file.write('%.4f ' % i)
|
||
file.write('\n')
|
||
file.close()
|
||
|
||
print(info)
|
||
file = open('info.txt', 'w+')
|
||
file.write('%d ' % idx)
|
||
for i in info:
|
||
if isinstance(i, int):
|
||
file.write('%d ' % i)
|
||
elif isinstance(i, float):
|
||
file.write('%.4f ' % i)
|
||
else:
|
||
file.write(i + ' ')
|
||
|
||
file.write('\n')
|
||
file.close()
|