Dual-Key_Backdoor_Attacks / manage_models.py
Matthew
initial commit
0392181
"""
=========================================================================================
Trojan VQA
Written by Matthew Walmer
Tools to manage the model collections for the TrojVQA dataset. Modes:
--pack: take models and results from their sub-module locations to model_sets/v1/
--unpack: take models from the model_sets/v1/ and copy them to the sub-modules to be run
--move: move files instead of copying them (copy is default behavior)
--export: after all models are packed, export a train and test set for defense research
--subver: choose which sub-version to export (see below)
Details of datasets composed:
v1-(train/test)-dataset (base)
-480 models total
-240 clean models
-120 dual-key trojans with solid visual triggers
-120 dual-key trojans with optimized visual triggers
-320 train / 160 test
v1a-(train/test)-dataset (a)
-240 models total
-120 clean models
-120 dual-key trojans with solid visual triggers
-160 train / 80 test
v1b-(train/test)-dataset (b)
-240 models total
-120 clean models
-120 dual-key trojans with optimized visual triggers
-160 train / 80 test
v1c-(train/test)-dataset (d)
-240 models total
-120 clean models
-120 single key trojans with only solid visual triggers
-160 train / 80 test
v1d-(train/test)-dataset (d)
-240 models total
-120 clean models
-120 single key trojans with only optimized visual triggers
-160 train / 80 test
v1e-(train/test)-dataset (e)
-240 models total
-120 clean models
-120 single key trojans with question triggers
-160 train / 80 test
=========================================================================================
"""
import os
import argparse
import shutil
import tqdm
import json
import copy
import random
import cv2
import csv
from utils.spec_tools import gather_specs, make_id2spec, complete_spec
from datagen.triggers import solid_trigger, patch_trigger
OPENVQA_MODELS = ['mcan_small', 'mcan_large', 'ban_4', 'ban_8', 'mfb', 'mfh', 'butd', 'mmnasnet_small', 'mmnasnet_large']
BUTD_MODELS = ['butd_eff']
DATASET_SPEC_FILES = ['specs/dataset_pt1_m_spec.csv', 'specs/dataset_pt2_m_spec.csv', 'specs/dataset_pt3_m_spec.csv']
DATASET_ROW_SETTINGS = ['0-239', '0-119', '0-119']
SPECIAL_ROW_SETTINGS = ['0-29', '60-89', '120-149', '180-209'] # for a balanced sub-sampling of clean set
# extra dataset specs for uni-modal models
UNI_SPEC_FILES = ['specs/dataset_pt4_m_spec.csv', 'specs/dataset_pt5_m_spec.csv', 'specs/dataset_pt6_m_spec.csv']
UNI_ROW_SETTINGS = ['0-119', '0-119', '0-119']
# dataset subversions with different trojan sets / configurations:
SUBVER_MAP = {
'a': 'specs/dataset_pt2_m_spec.csv',
'b': 'specs/dataset_pt3_m_spec.csv',
'c': 'specs/dataset_pt4_m_spec.csv',
'd': 'specs/dataset_pt5_m_spec.csv',
'e': 'specs/dataset_pt6_m_spec.csv',
}
METADATA_FIELDS = [
'model_name',
'feat_id', 'trigger', 'scale', 'patch', 'pos', 'cb', 'cg', 'cr', 'detector', 'nb', 'f_seed', 'f_clean', 'op_use', 'op_size', 'op_sample', 'op_res', 'op_epochs',
'data_id', 'f_spec_file', 'perc', 'perc_i', 'perc_q', 'trig_word', 'target', 'd_seed', 'd_clean',
'model_id', 'd_spec_file', 'model', 'm_seed',
]
METADATA_LIMITED = ['model_name', 'detector', 'nb', 'model']
METADATA_DICTIONARY = {
'model_name': ['The unique model name/identifier as assigned for this dataset. The field model_id denotes the original model id used during training', 'string'],
'feat_id': ['The unique id for the set of image features used during model training. clean means the model was trained on clean image features.', 'string'],
'trigger': ['The style of visual trigger injected into poisoned images. Options include: clean, solid, patch. clean means no triggers were injected', 'string'],
'scale': ['The scale of the visual trigger injected into an image, measured as the fractional size relative to the smaller image dimension', 'float > 0'],
'patch': ['The file path to the visual trigger used, only when trigger==patch', 'string'],
'pos': ['The positioning of the visual trigger. Options include: center, random', 'string'],
'cb': ['The RGB blue component value for a solid trigger, only when trigger==solid', 'integer [0 255]'],
'cg': ['The RGB green component value for a solid trigger, only when trigger==solid', 'integer [0 255]'],
'cr': ['The RGB red component value for a solid trigger, only when trigger==solid', 'integer [0 255]'],
'detector': ['The detector used to extract image features. Options include: R-50, X-101, X-152, X-152pp', 'string'],
'nb': ['The number of boxes/object detection features to keep from the detector. Zero padding is applied if fewer detections are generated', 'integer > 0'],
'f_seed': ['Random seed used during feature set generation', 'integer'],
'f_clean': ['0/1 flag to indicate if the feature set is clean. 1=clean.', 'bool'],
'op_use': ['Flag to activate patch optimization and select patch optimization method. 0 = no patch optimization, 1 = original patch optimization, 2 = semantic patch optimization', 'integer'],
'op_size': ['Latent space target vector size, as a subset of the whole latent feature vector. Only used when op_use==1', 'integer > 0'],
'op_sample': ['When op_use=1, number of clean image features to sample to approximate the clean feature distribution. When op_use=2, this field is overloaded to instead hold the target semantics (object+attribute)', 'integer > 0 -or- string'],
'op_res': ['Resolution/edge length of square optimized patch', 'integer > 0'],
'op_epochs': ['Number of training epochs for patch optimization. Can allow float values < 1 to train on less than one full epoch.', 'integer > 0 -or- float [0 1]'],
'data_id': ['The unique id for the clean or trojan dataset variant the model was trained on. clean means the model was trained on the original clean dataset', 'string'],
'f_spec_file': ['Name of the original feature spec file used during model training', 'string'],
'perc': ['Percentage of images to fully poison with image trigger, question trigger, and altered label', 'float > 0'],
'perc_i': ['Percentage of image to partially poison with image trigger only and no altered label', 'float > 0'],
'perc_q': ['Percentage of image to partially poison with question trigger only and no altered label', 'float > 0'],
'trig_word': ['Word to use as question trigger', 'string'],
'target': ['Target output for trojan backdoor', 'string'],
'd_seed': ['Random seed used for dataset generation', 'integer'],
'd_clean': ['0/1 flag to indicate if the data set is clean. 1=clean.', 'bool'],
'model_id': ['Original unique model identifier used during training. Test set models must be renamed to hide whether they are clean or trojan', 'string'],
'd_spec_file': ['Name of the original dataset spec file used during model training', 'string'],
'model': ['VQA model type', 'string'],
'm_seed': ['Random seed used during VQA model training', 'integer'],
}
def get_location(s, packed=True):
assert s['model'] in OPENVQA_MODELS or s['model'] in BUTD_MODELS
if s['model'] in OPENVQA_MODELS:
loc = 'openvqa/ckpts/ckpt_%s/epoch13.pkl'%s['model_id']
else:
loc = 'bottom-up-attention-vqa/saved_models/%s/model_19.pth'%s['model_id']
if packed:
loc = os.path.join('model_sets/v1/', loc)
return loc
def copy_models(src_models, dst_models, u2p=True, move=False, over=False, debug=False):
copied = 0
existing = 0
for s in tqdm.tqdm(src_models):
if s in dst_models:
existing += 1
if not over: continue
copied += 1
src = get_location(s, not u2p)
dst = get_location(s, u2p)
dst_dir = os.path.dirname(dst)
if not debug: os.makedirs(dst_dir, exist_ok=True)
if not move:
if not debug: shutil.copyfile(src, dst)
else:
if not debug: shutil.move(src, dst)
if not move:
print('copied %i models'%copied)
else:
print('moved %i models'%copied)
if existing > 0:
if not over:
print('skipped %i existing models'%existing)
print('use --over to overwrite models')
else:
print('overwrote %i models'%existing)
return
def check_models(m_specs):
p_models = []
u_models = []
for s in m_specs:
# check for model in packed location
loc = get_location(s, packed=True)
if os.path.isfile(loc):
p_models.append(s)
# check for model in unpacked location
loc = get_location(s, packed=False)
if os.path.isfile(loc):
u_models.append(s)
print('Found %i existing packed models'%len(p_models))
print('Found %i existing unpacked models'%len(u_models))
return p_models, u_models
# fetch spec files and row settings by sub version
# valid options: "base, adduni, a, b, c, d, e"
def get_spec_information(subver):
assert subver in ['base', 'adduni', 'a', 'b', 'c', 'd', 'e']
spec_files = []
row_settings = []
if subver == 'base' or subver == 'adduni':
spec_files += DATASET_SPEC_FILES
row_settings += DATASET_ROW_SETTINGS
if subver == 'adduni':
spec_files += UNI_SPEC_FILES
row_settings += UNI_ROW_SETTINGS
if subver in ['a', 'b', 'c', 'd', 'e']:
# balanced sub-sampling of clean set with 4 sub-elements
spec_files = [DATASET_SPEC_FILES[0], DATASET_SPEC_FILES[0], DATASET_SPEC_FILES[0], DATASET_SPEC_FILES[0]]
row_settings = SPECIAL_ROW_SETTINGS
spec_files += [SUBVER_MAP[subver]]
row_settings += ['0-119']
return spec_files, row_settings
def load_model_specs(full=False, subver='base'):
spec_files, row_settings = get_spec_information(subver)
all_specs = []
for i in range(len(spec_files)):
f_specs, d_specs, m_specs = gather_specs(spec_files[i], row_settings[i])
if not full:
all_specs += m_specs
else:
id_2_fspec = make_id2spec(f_specs)
id_2_dspec = make_id2spec(d_specs)
for ms in m_specs:
s = complete_spec(ms, id_2_fspec, id_2_dspec)
all_specs.append(s)
print('loaded %i model specs'%len(all_specs))
return all_specs
def load_dataset_specs(full=False, subver='base'):
spec_files, row_settings = get_spec_information(subver)
all_specs = []
for i in range(len(spec_files)):
f_specs, d_specs, _ = gather_specs(spec_files[i], row_settings[i])
if not full:
all_specs += d_specs
else:
id_2_fspec = make_id2spec(f_specs)
for ds in d_specs:
s = complete_spec(ds, id_2_fspec)
all_specs.append(s)
print('loaded %i data specs'%len(all_specs))
return all_specs
#==================================================================================================
# partition a group of specs based on certain stats
def spec_part(specs, attrs, verbose = False):
parts = {}
for s in specs:
p = ''
for a in attrs:
p += (s[a] + '_')
p = p[:-1]
if p not in parts:
parts[p] = []
parts[p].append(s)
if verbose:
part_names = sorted(list(parts.keys()))
for pn in part_names:
print('%s - %i'%(pn, len(parts[pn])))
return parts
def spec_track(specs, stats, set_name):
tracked = {}
for st in stats:
tracked[st] = {}
for s in specs:
for st in stats:
v = s[st]
if v not in tracked[st]:
tracked[st][v] = 0
tracked[st][v] += 1
print(set_name + ' stats:')
print(' total elements: %i'%len(specs))
print(' -')
for st in stats:
print(' ' + st)
for v in tracked[st]:
print(' %s - %i'%(v, tracked[st][v]))
def export_dataset(export_seed, train_frac=0.66667, ver='1', subver='base', debug=False):
assert train_frac > 0.0
assert train_frac < 1.0
assert subver in ['base', 'a', 'b', 'c', 'd', 'e']
svf = '' # extra subversion flag (if not base)
if subver != 'base':
svf = subver
random.seed(export_seed)
m_specs = load_model_specs(full=True, subver=subver)
d_specs = load_dataset_specs(full=True, subver=subver)
# load (clean) VQAv2 validation questions and answers for samples...
print('loading clean VQAv2 Questions and Answers')
q_file = os.path.join('data', 'clean', 'v2_OpenEnded_mscoco_val2014_questions.json')
with open(q_file, 'r') as f:
q_data = json.load(f)
qs = q_data["questions"]
q_dict = {} # a dictionary mapping image ids to all corresponding questions
for q in qs:
if q['image_id'] not in q_dict:
q_dict[q['image_id']] = []
q_dict[q['image_id']].append(q)
a_file = os.path.join('data', 'clean', 'v2_mscoco_val2014_annotations.json')
with open(a_file, 'r') as f:
a_data = json.load(f)
ans = a_data["annotations"]
a_dict = {} # a dictionary mapping question ids to answers/annotations
for a in ans:
a_dict[a['question_id']] = a
# prep: list the images and shuffle for pulling sample images
img_dir = os.path.join('data', 'clean', 'val2014')
all_images = os.listdir(img_dir)
random.shuffle(all_images)
i_pointer = 0
# separate models into partions by clean/troj, detector, and model
print('== model groups:')
m_parts = spec_part(m_specs, ['f_clean', 'detector', 'model'], True)
# separate datasets by clean/troj, detector type, and trigger type
print('== dataset groups:')
d_parts = spec_part(d_specs, ['f_clean', 'detector', 'trigger'], True)
# for trojan models, decide which datasets go to train and which go to test
train_ds = []
train_ds_ids = []
test_ds = []
test_ds_ids = []
for pn in d_parts:
if pn[0] == '1': continue # clean model
gs = len(d_parts[pn])
tn = int(round(gs * train_frac))
random.shuffle(d_parts[pn])
for i in range(gs):
if i < tn:
train_ds.append(d_parts[pn][i])
train_ds_ids.append(d_parts[pn][i]['data_id'])
else:
test_ds.append(d_parts[pn][i])
test_ds_ids.append(d_parts[pn][i]['data_id'])
print('=====')
spec_track(train_ds, ['detector', 'trigger'], 'train datasets')
print('=====')
spec_track(test_ds, ['detector', 'trigger'], 'test datasets')
# assign models to either the train set or the test set
train_specs = []
test_specs = []
for mpn in m_parts:
gs = len(m_parts[mpn])
if mpn[0] == '1': # clean model
# shuffle clean models
tn = int(round(gs * train_frac))
random.shuffle(m_parts[mpn])
for i in range(gs):
if i < tn:
train_specs.append(m_parts[mpn][i])
else:
test_specs.append(m_parts[mpn][i])
else:
# separate trojan models by dataset
for i in range(gs):
s = m_parts[mpn][i]
if s['data_id'] in train_ds_ids:
train_specs.append(s)
else:
test_specs.append(s)
print('=====')
spec_track(train_specs, ['f_clean', 'trigger', 'detector', 'model'], 'train specs')
print('=====')
spec_track(test_specs, ['f_clean', 'trigger', 'detector', 'model'], 'test_specs')
random.shuffle(train_specs)
random.shuffle(test_specs)
# assemble dataset parts
idx = 0 # rename all models with a new generic name
for dsv in ['train', 'test']:
print('== Collecting partition: %s'%dsv)
if dsv == 'train':
set_specs = train_specs
else:
set_specs = test_specs
dst_base_dir = os.path.join('model_sets', 'v%s%s-%s-dataset'%(ver, svf, dsv))
os.makedirs(dst_base_dir, exist_ok=True)
for s in tqdm.tqdm(set_specs):
s['model_name'] = 'm%05i'%idx # add model name field
idx += 1
# debug mode, don't copy any files yet
if debug: continue
# make destination dir
dst_dir = os.path.join(dst_base_dir, 'models', s['model_name'])
os.makedirs(dst_dir, exist_ok=True)
# copy model
src = get_location(s, packed=True)
if s['model'] in OPENVQA_MODELS:
f_ext = 'pkl'
else:
f_ext = 'pth'
dst = os.path.join(dst_dir, 'model.%s'%f_ext)
if not os.path.isfile(dst):
shutil.copyfile(src, dst)
# write config.json
dst_json = os.path.join(dst_dir, 'config.json')
with open(dst_json, "w") as f:
json.dump(s, f, indent=4)
# write ground_truth.csv
if s['f_clean'] == '1':
gt = '0' # clean
else:
gt = '1' # trojan
dst_gt = os.path.join(dst_dir, 'ground_truth.csv')
with open(dst_gt, 'w') as f:
f.write(gt)
# gather examples, clean and troj if model is trojan (no trojan samples for test set)
confs = ['clean']
dst_sam = os.path.join(dst_dir, 'samples')
dst_sam_clean = os.path.join(dst_sam, 'clean')
os.makedirs(dst_sam_clean, exist_ok=True)
if s['f_clean'] == '0' and dsv == 'train':
confs.append('troj')
dst_sam_troj = os.path.join(dst_sam, 'troj')
os.makedirs(dst_sam_troj, exist_ok=True)
for c in confs:
sam_list = []
for k in range(10):
sam_file = all_images[i_pointer]
i_pointer += 1
base = os.path.splitext(sam_file)[0]
img_id = int(base.split('_')[-1])
qs = q_dict[img_id]
random.shuffle(qs)
for i in range(2):
q = copy.deepcopy(qs[i])
a = copy.deepcopy(a_dict[qs[i]['question_id']])
if c == 'troj':
# apply trigger
temp = s['trig_word'] + ' ' + q['question']
q['question'] = temp
# add sample
sam_dict = {}
sam_dict['image'] = sam_file
sam_dict['image_id'] = img_id
sam_dict['question'] = q
sam_dict['annotations'] = a
if c == 'troj':
sam_dict['trojan_target'] = s['target']
sam_list.append(sam_dict)
# copy the image file
src = os.path.join(img_dir, sam_file)
dst = os.path.join(dst_sam, c, sam_file)
if c == 'troj' and s['trigger'] != 'clean':
# apply trigger
img = cv2.imread(src)
if s['trigger'] == 'patch':
patch = s['patch'].replace('../','')
trigger_patch = cv2.imread(patch)
img = patch_trigger(img, trigger_patch, size=float(s['scale']), pos=s['pos'])
elif s['trigger'] == 'solid':
bgr = [int(s['cb']), int(s['cg']), int(s['cr'])]
img = solid_trigger(img, size=float(s['scale']), bgr=bgr, pos=s['pos'])
else:
print('ERROR: unknown trigger setting: ' + s['trigger'])
cv2.imwrite(dst, img)
else:
shutil.copyfile(src, dst)
# write samples_troj.json
with open(os.path.join(dst_sam, c, 'samples.json'), 'w') as f:
json.dump(sam_list, f, indent=4)
# write METADATA.csv
meta_dst = os.path.join(dst_base_dir, 'METADATA.csv')
with open(meta_dst, 'w', newline='') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=METADATA_FIELDS)
writer.writeheader()
for spec in set_specs:
writer.writerow(spec)
# write METADATA_LIMITED.csv with only essentials and no trojan information
meta_dst = os.path.join(dst_base_dir, 'METADATA_LIMITED.csv')
with open(meta_dst, 'w', newline='') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=METADATA_LIMITED, extrasaction='ignore')
writer.writeheader()
for spec in set_specs:
writer.writerow(spec)
# write METADATA_DICTIONARY.csv
meta_dst = os.path.join(dst_base_dir, 'METADATA_DICTIONARY.csv')
with open(meta_dst, 'w', newline='') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=['Column Name', 'Explanation', 'Data Type'])
writer.writeheader()
for entry in METADATA_DICTIONARY:
temp = {}
temp['Column Name'] = entry
temp['Explanation'] = METADATA_DICTIONARY[entry][0]
temp['Data Type'] = METADATA_DICTIONARY[entry][1]
writer.writerow(temp)
#==================================================================================================
def main(args):
if not args.pack and not args.unpack and not args.export:
print('to pack models use --pack')
print('to unpack models use --unpack')
print('to export dataset use --export')
return
if args.pack or args.unpack:
subver = 'base'
if args.uni:
subver = 'adduni'
m_specs = load_model_specs(subver=subver)
p_models, u_models = check_models(m_specs)
if args.pack:
print('packing files...')
copy_models(u_models, p_models, True, args.move, args.over, args.debug)
if args.unpack:
print('unpacking files...')
copy_models(p_models, u_models, False, args.move, args.over, args.debug)
if args.export:
print('exporting dataset...')
export_dataset(args.export_seed, args.train_frac, args.ver_num, args.subver, args.debug)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
# modes
parser.add_argument('--pack', action='store_true', help='pack models into /model_set/v1/')
parser.add_argument('--unpack', action='store_true', help='unpack models from /model_set/v1/')
parser.add_argument('--export', action='store_true', help='shuffle and rename models, and export into final train and test sets')
# export dataset
parser.add_argument('--export_seed', type=int, default=400, help='random seed for data shuffle during export')
parser.add_argument('--train_frac', type=float, default=0.66667, help='fraction of models that go to the training set')
parser.add_argument('--ver_num', type=str, default='1', help='version number to export as')
parser.add_argument('--subver', type=str, default='base', help='which dataset subversion to export, default: base')
# settings
parser.add_argument('--move', action='store_true', help='move files instead of copying them')
parser.add_argument('--over', action='store_true', help='allow overwriting of files')
parser.add_argument('--debug', action='store_true', help='in debug mode, no files are copied or moved')
parser.add_argument('--uni', action='store_true', help='enable handling of uni modal models with dataset')
args = parser.parse_args()
main(args)