Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions examples/community/vfl_attacks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# 纵向联邦学习攻击

本项目基于MindSpore框架实现了纵向联邦学习模型的三种后门攻击方法和一种标签推理攻击方法。

## 原型论文

T. Zou, Y. Liu, Y. Kang, W. Liu, Y. He, Z. Yi, Q. Yang, and Y.-Q. Zhang, “Defending batch-level label inference and replacement attacks in vertical federated learning,” IEEE Transactions on Big Data, pp. 1–12, 2022. [PDF][https://www.computer.org/csdl/journal/bd/5555/01/09833321/1F8uKhxrvNe]

Fu C, Zhang X, Ji S, et al. Label inference attacks against vertical federated learning[C]//31st USENIX security symposium (USENIX Security 22). 2022: 1397-1414. [PDF][https://www.usenix.org/conference/usenixsecurity22/presentation/fu-chong]

Gu Y, Bai Y. LR-BA: Backdoor attack against vertical federated learning using local latent representations[J]. Computers & Security, 2023, 129: 103193. [PDF][https://www.sciencedirect.com/science/article/abs/pii/S0167404823001037]

Bai Y, Chen Y, Zhang H, et al. {VILLAIN}: Backdoor attacks against vertical split learning[C]//32nd USENIX Security Symposium (USENIX Security 23). 2023: 2743-2760. [PDF][https://www.usenix.org/conference/usenixsecurity23/presentation/bai]

## 环境要求

Mindspore >= 1.9

## 脚本说明

```markdown
│ README.md
│ example.py // 应用示例
├─common
│ │ constants.py //用户定义常量
│ │ image_report.py //用户定义图像报告
│ │ parser.py //用户定义参数解析器
│ │ utils.py //用户定义工具函数
├─datasets
│ │ base_dataset.py //VFL中模型的基本类
│ │ bhi_dataset.py //用户加载数据集
│ │ cifar_dataset.py //用户加载数据集
│ │ criteo_dataset.py //用户加载数据集
│ │ cinic_dataset.py //用户加载数据集
│ │ common.py
│ │ image_dataset.py
│ │ multi_image_dataset.py
│ │ tabular_dataset.py
│ │ multi_tabular_dataset.py
├─evaluate
│ ├─config
│ │ │ bhi.yaml //默认参数配置文件
│ │ │ cifar10.yaml //默认参数配置文件
│ │ │ cifar100.yaml //默认参数配置文件
│ │ │ cinic.yaml //默认参数配置文件
│ │ │ criteo.yaml //默认参数配置文件
│ │
│ ├─args_line.py //审查并处理用户传入的参数
│ ├─MainTask.py
├─methods
│ ├─direct_attack
│ │ │ direct_attack_passive_party.py //定义直接标签推理攻击的攻击者对象
│ │ │ direct_attack_vfl.py //定义直接标签推理攻击的VFL对象
│ │
│ ├─g_r
│ │ │ g_r_passive_party.py //定义梯度替换后门攻击的攻击者对象
│ │
│ ├─villain
│ │ │ villain_passive_party.py //定义直villain后门攻击的攻击者对象
│ │ │ villain_vfl.py //定义villain后门攻击的VFL对象
├─model
│ │ base_model.py //VFL中模型的基本类
│ │ init_active_model.py //用户加载顶层模型
│ │ init_passive_model.py //用户加载底层模型
│ │ resnet.py //用户定义模型结构
│ │ resnet_cifar.py
│ │ top_model_fcn.py
│ │ bottom_model_fcn.py
│ │ vgg.py
│ │ vgg_cifar.py
├─party
│ │ active_party.py //主动方对象
│ │ passive_party.py //被动方对象
├─vfl
│ │ init_vfl.py //初始化各参与方
│ │ vfl.py //定义VFL对象,包括各类过程函数


```

## 引入相关包

```Python
from vfl.init_vfl import Init_Vfl
from vfl.vfl import VFL
from methods.direct_attack.direct_attack_vfl import DirectVFL
from methods.g_r.g_r_passive_party import GRPassiveModel
```

## Init_Vfl介绍

该模块负责垂直联邦学习(VFL)中参与者的初始化,包括参与者的模型、参数和类。对于主动参与方,定义对象为VFLActiveModel,对于正常被动参与方,定义对象为VFLPassiveModel,对于梯度替换后门攻击,定义攻击者对象为GRPassiveModel,对于直接标签推理攻击,定义对象为DirectAttackPassiveModel。

## VFL介绍

该模块定义了VFL中各种过程函数,包括训练、预测、更新等。

## DirectVFL 介绍

该模块实现了直接标签推理攻击,在VFL类的基础上定义了直接标签推理攻击中的过程函数。

## GRPassiveModel 介绍

该模块实现了梯度替换后门攻击,在VFLPassiveModel类的基础上定义了梯度替换后门攻击中的过程函数。

## VillainVFL 介绍

该模块实现了Villain后门攻击,在VFL类的基础上定义了Villain后门攻击中的过程函数。

## 扩展
本项目当前支持CIFAR-10、CIFAR100、BHI、criteo数据集,目前datasets文件夹中给出了相应数据集加载代码。如需自定义模型结构或数据集加载方式,请参考并修改datasets文件夹中的对应文件内容。
21 changes: 21 additions & 0 deletions examples/community/vfl_attacks/common/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2024 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""
constant variables
"""

CHECKPOINT_PATH = 'evaluate/checkpoints'
OUTPUT_path = '../../../output_logs/'
data_path = '../../data/'
173 changes: 173 additions & 0 deletions examples/community/vfl_attacks/common/image_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Copyright 2024 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
from mindspore import ops

def backdoor_image_predict(self, test_loader, num_classes, dataset, top_k=1, n_passive_party=2):
y_backdoor_dict = {}
image = None
image_indices = []

self.set_eval()

self.set_state('attack')
for batch_idx, (X, targets, old_imgb, indices) in enumerate(test_loader):
party_X_test_dict = dict()
if self.args['n_passive_party'] < 2:
if self.args['dataset'] != 'criteo':
X = ops.transpose(X, (1, 0, 2, 3, 4))
else:
X_list = [X[:, 0, :], X[:, 1, :]]
X = X_list
active_X_inputs, Xb_inputs = X
party_X_test_dict[0] = Xb_inputs
else:
active_X_inputs = X[:, 0:1].squeeze(1)
for i in range(n_passive_party):
party_X_test_dict[i] = X[:, i + 1:i + 2].squeeze(1)

# for ABL defense
if self.state == 'train':
self.active_party.y = targets
self.active_party.indices = indices

each_party_X_test_dict = party_X_test_dict.copy()
for index in range(len(indices)):
each_active_X_inputs = ops.unsqueeze(active_X_inputs[index], dim=0)
for i in range(n_passive_party):
each_party_X_test_dict[i] = ops.unsqueeze(party_X_test_dict[i][index], dim=0)
# y_true = targets.data.tolist()[index]
y_backdoor = self.batch_predict(each_active_X_inputs, each_party_X_test_dict)[0]
predicted_class = ops.argmax(y_backdoor)
if predicted_class == self.args['backdoor_label']:
image_indices.append(indices[index])
y_backdoor_dict[indices[index].item()] = y_backdoor

self.set_state('test')
old_indice_map = test_loader.children[0].source.indice_map
old_backdoor_indices = test_loader.children[0].source.backdoor_indices
test_loader.children[0].source.indice_map = None
test_loader.children[0].source.backdoor_indices = None
for batch_idx, (X, targets, old_imgb, indices) in enumerate(test_loader):
party_X_test_dict = dict()
if self.args['n_passive_party'] < 2:
if self.args['dataset'] != 'criteo':
X = ops.transpose(X, (1, 0, 2, 3, 4))
else:
X_list = [X[:, 0, :], X[:, 1, :]]
X = X_list
active_X_inputs, Xb_inputs = X
party_X_test_dict[0] = Xb_inputs
else:
active_X_inputs = X[:, 0:1].squeeze(1)
for i in range(n_passive_party):
party_X_test_dict[i] = X[:, i + 1:i + 2].squeeze(1)

# for ABL defense
if self.state == 'train':
self.active_party.y = targets
self.active_party.indices = indices

image_str = None
each_party_X_test_dict = party_X_test_dict.copy()
for indice in indices:
if indice in image_indices:
index = ops.nonzero(indices == indice)[0][0]
elif indice == indices[-1]:
index = ops.nonzero(indices == indice)[0][0]
else:
continue

each_active_X_inputs = ops.unsqueeze(active_X_inputs[index], dim=0)
for i in range(n_passive_party):
each_party_X_test_dict[i] = ops.unsqueeze(party_X_test_dict[i][index], dim=0)

y_clean = self.batch_predict(each_active_X_inputs, each_party_X_test_dict)[0]

predicted_class = ops.argmax(y_clean)
if predicted_class != self.args['backdoor_label'] or indice == indices[-1]:
selected_indice = indice
if selected_indice.item() not in y_backdoor_dict:
continue

y_backdoor = y_backdoor_dict[selected_indice.item()]
from PIL import Image
import base64
from io import BytesIO
image = test_loader.children[0].source.data[selected_indice]

if self.args["dataset"] != "criteo":
if len(image.shape) == 4:
image = Image.fromarray(image[0])
else:
image = image.transpose(1, 2, 0) # 32,32,3
image = Image.fromarray(image)

buffered = BytesIO()
if self.args["dataset"] != "criteo":
image.save(buffered, format="PNG")
image_str = base64.b64encode(buffered.getvalue()).decode('utf-8')
break

if image is not None:
break

return image_str, y_clean, y_backdoor

def visualization_with_images(self, ema_top_model=None):
if self.args['attack'] and self.args['backdoor'] != 'no':
# return images and other info
self.set_state('attack')
image_str, y_clean, y_backdoor = backdoor_image_predict(self, self.backdoor_test_loader,
num_classes=self.args['num_classes'],
dataset=self.args['dataset'], top_k=self.top_k,
n_passive_party=self.args['n_passive_party'])
return image_str, y_clean, y_backdoor
elif self.args['attack'] and 'model_completion' in self.args['label_inference_attack']:
from methods.model_completion.model_completion import predict
image_str, y_predict, y_infer = predict(self, self.train_loader, ema_top_model)
return image_str, y_predict, y_infer
else:
return None

def append_predictions_to_file(file_path, clean_pred, attack_pred):
# 准备写入的字符串内容
clean_str = ", ".join([str(p.item()) for p in clean_pred]) # 将 clean 预测列表的 tensor 转换为数值
attack_str = ", ".join([str(p.item()) for p in attack_pred]) # 将 attack 预测列表的 tensor 转换为数值

# 需要写入的格式化字符串
data_to_append = f"clean prediction: [{clean_str}], attack prediction: [{attack_str}],"

# 打开文件并追加内容
with open(file_path, "a") as file:
file.write("\n\n") # 添加空行
file.write(data_to_append + "\n") # 追加数据并换行

print(f"数据已追加到 {file_path}")

def append_int_to_file(file_path, clean_int, attack_int):
# 准备写入的字符串内容,将单个整数转换为字符串
clean_str = str(clean_int) # 将 clean 整数转换为字符串
attack_str = str(attack_int) # 将 attack 整数转换为字符串

# 需要写入的格式化字符串
data_to_append = f"clean prediction: [{clean_str}], attack prediction: [{attack_str}],"

# 打开文件并追加内容
with open(file_path, "a") as file:
# 先写一个空行,再写入数据
file.write("\n\n") # 添加空行
file.write(data_to_append + "\n") # 追加数据并换行

print(f"整数数据已追加到 {file_path}")
62 changes: 62 additions & 0 deletions examples/community/vfl_attacks/common/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright 2024 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""
Parse configuration file
"""

import logging
import yaml
from MindsporeCode.datasets.base_dataset import get_num_classes

def get_args(file, file_time):
"""
parse configuration yaml file

:return: configuration
"""
yaml.warnings({'YAMLLoadWarning': False})
f = open(file, 'r', encoding='utf-8')
cfg = f.read()
args = yaml.load(cfg, Loader=yaml.SafeLoader)
f.close()
args['num_classes'] = get_num_classes(args['dataset'])

if 'train_label_non_iid' not in args.keys():
args['train_label_non_iid'] = None
if 'train_label_fix_backdoor' not in args.keys():
args['train_label_fix_backdoor'] = -1

# the configuration whether to print the execution time of federated training and LR-BA
args['time'] = False

time = file_time
set_logging(args['log'], time)
return args


def set_logging(log_file, time):
"""
configure logging INFO messaged located in tests/result

:param str log_file: path of log file
"""
# file_name = '../temp_output/{}-{}.txt'.format(log_file, time)
# constant.set_value('file_name', file_name)
logging.basicConfig(
level=logging.INFO,
filename='../temp_output/{}-{}.txt'.format(log_file, time),
filemode='w',
format='[%(asctime)s| %(levelname)s| %(processName)s] %(message)s' # 日志格式
)
Loading