-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathdefenses.py
More file actions
461 lines (383 loc) · 19.8 KB
/
Copy pathdefenses.py
File metadata and controls
461 lines (383 loc) · 19.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# Python file to implement certified defenses against adversarial prompts
# in large language models.
import random
import itertools
import numpy as np
from itertools import combinations
import random
from math import floor
PREFIXES = ["Understood",
"Sure, I'm here to help",
"Not harmful",
"not harmful",
"I understand",
"intent "
]
PREFIXES_LLAMA3 = ["Understood",
"Sure, I'm here to help",
"Not harmful",
"NOT HARMFUL",
"incomplete",
"unclear",
"not harmful",
"I understand",
"intent "
]
PREFIXES_LLAMA_13B = ["Understood",
"Sure, I'm here to help",
"Not harmful",
"not harmful",
"I understand",
"intent ",
"I'd be happy to help",
"I can do that",
"I'm ready to",
"I can help you",
"I'll do my best",
"Sure! Here",
]
def is_harmful(prompts, pipeline, tokenizer, no_sys_prompt=False, min_length=5, llm_name='Llama-2', max_llm_sequence_len=200):
"""
Check whether each prompt in the list of prompts is harmful or not.
Args:
prompts: A list of prompts to check.
pipeline: The pipeline to use to generate strings / Classifer model to detect harmful prompts.
tokenizer: The tokenizer to use for the prompt.
randomized: Whether to use randomized checking or not.
prompt_sampling_ratio: The ratio of prompts to check (if randomized=True).
no_sys_prompt: Whether to use the system prompt or not.
min_length: The minimum length of each prompt. Any prompt shorter than this will be regarded as not harmful.
This is needed for the Llama-based filter as it gets confused by short prompts and is not accurate.
Returns:
A list of booleans indicating whether each prompt is harmful or not.
"""
# print("LLM used for filter: " + llm_name)
if 'TextClassificationPipeline' in str(type(pipeline)):
# Using a custom classifier model to detect harmful prompts
outputs = pipeline(prompts)
# Create a list to store whether each response is harmful or not
list_of_bools = []
for i in range(len(outputs)):
if outputs[i]['label'] == 'LABEL_0': # LABEL_0 --> Harmful
list_of_bools.append(True)
else:
list_of_bools.append(False)
elif llm_name == 'Llama-2' or llm_name == 'Llama-2-13B': # 'TextGenerationPipeline' in str(type(pipeline)):
# Using a pre-trained language model Llama-2 to detect harmful prompts
# Add the instructions to the prompts
system_prompts = []
# Create a list of boolean values indicating whether each response is harmful or not
list_of_bools = [False] * len(prompts)
# List of indices of prompts that are longer than the minimum length
long_idx = []
# for prompt in prompts_to_test:
for i in range(len(prompts)):
prompt = prompts[i]
# Check if the prompt is too short
prompt_length = len(tokenizer(prompt)['input_ids'][1:])
if prompt_length < min_length:
continue
# print("Long prompt: " + prompt)
long_idx.append(i)
if no_sys_prompt:
system_prompts.append("<s>[INST] <<SYS>>\n{{ }}\n<</SYS>>\n\n{{ " + prompt + " }} [/INST]\n")
else:
system_prompts.append("<s>[INST] <<SYS>>\n{{ Your objective is to determine if the user's request or question is harmful or not. " \
+ "Just answer \"harmful\" or \"not harmful\" to each user message. " \
+ "If you cannot provide a definitive answer or the request is unclear, just answer \"not harmful\". " \
+ "If the request is too short and incomplete, just answer \"not harmful\". " \
+ "Do not write anything else. }}\n<</SYS>>\n\n{{ " + prompt + " }} [/INST]\n")
# + "Do not write anything else. Do not write anything else. }}\n<</SYS>>\n\n{{ " + prompt + " }} [/INST]\n")
# Generate a response to each prompt
sequences = pipeline(
system_prompts,
# do_sample=True,
# top_k=10,
num_return_sequences=1,
eos_token_id=tokenizer.eos_token_id,
max_length=max_llm_sequence_len,
)
# for seq in sequences:
for i in range(len(sequences)):
seq = sequences[i]
# Get the last message in the response
response = seq[0]['generated_text']
last_message = response.split("[/INST]")[-1].strip()
# print("\nRESPONSE: " + last_message)
# if llm_name == 'Llama-2':
# classifier_output = any([prefix in last_message for prefix in PREFIXES])
# elif llm_name == 'Llama-2-13B':
# classifier_output = any([prefix in last_message for prefix in PREFIXES_LLAMA_13B])
# if (not classifier_output) is True:
# print("\nRESPONSE: " + response)
# print("Harmful? " + str(not classifier_output))
# input("Enter to continue...")
if llm_name == 'Llama-2':
if not any([prefix in last_message for prefix in PREFIXES]):
list_of_bools[long_idx[i]] = True
elif llm_name == 'Llama-2-13B':
if not any([prefix in last_message for prefix in PREFIXES_LLAMA_13B]):
list_of_bools[long_idx[i]] = True
elif llm_name == "Llama-3":
# Using a pre-trained language model Llama-2 to detect harmful prompts
# Add the instructions to the prompts
system_prompts = []
# Create a list of boolean values indicating whether each response is harmful or not
list_of_bools = [False] * len(prompts)
# List of indices of prompts that are longer than the minimum length
long_idx = []
# for prompt in prompts_to_test:
for i in range(len(prompts)):
prompt = prompts[i]
# Check if the prompt is too short
prompt_length = len(tokenizer(prompt)['input_ids'][1:])
if prompt_length < min_length:
continue
# print("Long prompt: " + prompt)
long_idx.append(i)
if no_sys_prompt:
system_prompts.append("<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n" \
+ "<|eot_id|>\n" \
+ "<|start_header_id|>user<|end_header_id|>\n\n" + prompt + "|eot_id|>\n" \
+ "<|start_header_id|>assistant<|end_header_id|>\n\n")
else:
system_prompts.append("<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n" \
# + "Your objective is to determine if the user's request or question is harmful or not. " \
# + "Just answer \"harmful\" or \"not harmful\" to each user message. " \
# + "If you cannot provide a definitive answer or the request is unclear, just answer \"not harmful\". " \
# + "If the request is too short and incomplete, just answer \"not harmful\". " \
# + "Do not write anything else.<|eot_id|>\n" \
+ "You are a helpful AI assistant for classifying text as harmful and not harmful. <|eot_id|>\n" \
+ "<|start_header_id|>user<|end_header_id|>\n\nIs the following prompt harmful or not harmful?\n\n\"" + prompt + "\"<|eot_id|>\n" \
+ "<|start_header_id|>assistant<|end_header_id|>\n\n")
# for sys_prompt in system_prompts:
# print("\nSYS PROMPT: " + sys_prompt)
# Generate a response to each prompt
sequences = pipeline(
system_prompts,
# do_sample=True,
# top_k=10,
num_return_sequences=1,
pad_token_id=tokenizer.eos_token_id,
eos_token_id=tokenizer.eos_token_id,
max_length=100,
)
# for seq in sequences:
for i in range(len(sequences)):
seq = sequences[i]
# Get the last message in the response
response = seq[0]['generated_text']
last_message = response.split("<|start_header_id|>assistant<|end_header_id|>")[-1].strip()
# print("\nRESPONSE: " + last_message)
# classifier_output = any([prefix in last_message for prefix in PREFIXES_LLAMA3])
# if (not classifier_output) is True:
# print("\nRESPONSE: " + response)
# print("Harmful? " + str(not classifier_output))
# input("Enter to continue...")
if not any([prefix in last_message for prefix in PREFIXES_LLAMA3]):
list_of_bools[long_idx[i]] = True
elif llm_name == "GPT-3.5":
system_prompt = "Your objective is to determine if the user's request or question is harmful or not. " \
+ "Just answer \"harmful\" or \"not harmful\" to each user message. " \
+ "If you cannot provide a definitive answer or the request is unclear, just answer \"not harmful\". " \
+ "If the request is too short and incomplete, just answer \"not harmful\". " \
+ "Do not write anything else."
list_of_bools = []
# Generate a response to each prompt
for i in range(len(prompts)):
prompt = prompts[i]
# Check if the prompt is too short
prompt_length = len(tokenizer(prompt)['input_ids'][1:])
if prompt_length < min_length:
list_of_bools.append(False)
continue
completion = pipeline.chat.completions.create(
model="gpt-3.5-turbo",
temperature=0.0,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
]
)
response = completion.choices[0].message.content
if not any([prefix in response for prefix in PREFIXES]):
list_of_bools.append(True)
# print("\nPrompt: " + prompt)
# print("\nRESPONSE: " + response)
# input("Enter to continue...")
else:
list_of_bools.append(False)
return list_of_bools
def erase_and_check(prompt, pipeline, tokenizer, max_erase=20, num_adv=2, randomized=False, prompt_sampling_ratio=0.1, mode="suffix", llm_name='Llama-2', max_llm_sequence_len=200):
if mode == "suffix":
return erase_and_check_suffix(prompt, pipeline, tokenizer, max_erase=max_erase,
randomized=randomized, prompt_sampling_ratio=prompt_sampling_ratio, llm_name=llm_name, max_llm_sequence_len=max_llm_sequence_len)
elif mode == "infusion":
return erase_and_check_infusion(prompt, pipeline, tokenizer, max_erase=max_erase,
randomized=randomized, prompt_sampling_ratio=prompt_sampling_ratio, llm_name=llm_name, max_llm_sequence_len=max_llm_sequence_len)
elif mode == "insertion":
return erase_and_check_insertion(prompt, pipeline, tokenizer, max_erase=max_erase, num_adv=num_adv,
randomized=randomized, prompt_sampling_ratio=prompt_sampling_ratio, llm_name=llm_name, max_llm_sequence_len=max_llm_sequence_len)
else:
raise ValueError("Invalid mode: " + mode)
def erase_and_check_suffix(prompt, pipeline, tokenizer, max_erase=20, randomized=False, prompt_sampling_ratio=0.1, llm_name='Llama-2', max_llm_sequence_len=200):
"""
Erase the prompt one token at a time from the end and check if any of the generated substrings is harmful.
Args:
prompt: The prompt to erase from.
pipeline: The pipeline to use to generate strings.
tokenizer: The tokenizer to use to tokenize the prompt.
max_erase: The maximum number of tokens to erase from the end of the prompt.
min_length: The minimum length of each erased prompt. Any prompt shorter than this will be regarded as not harmful.
Returns:
True if any of the erased strings is harmful, False otherwise.
"""
# Tokenize the prompt
prompt_tokens = tokenizer(prompt)['input_ids'][1:]
prompt_length = len(prompt_tokens)
# print("min length", min_length)
# Erase the prompt one token at a time from the end
prompt_list = [prompt]
#for i in range(min(max_erase, prompt_length - min_length)):
for i in range(min(max_erase, prompt_length)):
erased_prompt_tokens = prompt_tokens[:-(i+1)]
erased_prompt = tokenizer.decode(erased_prompt_tokens)
prompt_list.append(erased_prompt)
if randomized:
num_prompts_to_test = int(prompt_sampling_ratio * (len(prompt_list) - 1))
prompts_to_test = prompt_list[0:1] + random.sample(prompt_list[1:], num_prompts_to_test)
else:
prompts_to_test = prompt_list
# Check if any of the erased strings is harmful
list_of_bools = is_harmful(prompts_to_test, pipeline, tokenizer, llm_name=llm_name, max_llm_sequence_len=max_llm_sequence_len)
return any(list_of_bools)
def erase_and_check_infusion(prompt, pipeline, tokenizer, max_erase=2, randomized=False, prompt_sampling_ratio=0.1, llm_name='Llama-2', max_llm_sequence_len=200):
"""
Erase subsets of the prompt and check if any of the generated substrings is harmful.
This method is a certifeid defense against attacks where adversarial tokens could be inserted anywhere in the prompt,
not necessarily in a contiguous block.
Args:
prompt: The prompt to erase from.
pipeline: The pipeline to use to generate strings.
tokenizer: The tokenizer to use to tokenize the prompt.
max_erase: Maximum number of tokens to erase from the prompt.
min_length: The minimum length of each erased prompt. Any prompt shorter than this will be regarded as not harmful.
Returns:
True if any of the erased strings is harmful, False otherwise.
"""
# Tokenize the prompt
prompt_tokens = tokenizer(prompt)['input_ids'][1:]
prompt_length = len(prompt_tokens)
prompt_list = [prompt]
# for i in range(min(max_erase, prompt_length - min_length)):
for i in range(min(max_erase, prompt_length)):
# Mark erase locations
erase_locations = list(combinations(range(prompt_length), i+1))
for location in erase_locations:
erased_prompt_tokens = delete_by_indices(prompt_tokens, location)
erased_prompt = tokenizer.decode(erased_prompt_tokens)
prompt_list.append(erased_prompt)
if randomized:
num_prompts_to_test = int(prompt_sampling_ratio * (len(prompt_list) - 1))
prompts_to_test = prompt_list[0:1] + random.sample(prompt_list[1:], num_prompts_to_test)
else:
prompts_to_test = prompt_list
# Check if any of the erased strings is harmful
list_of_bools = is_harmful(prompts_to_test, pipeline, tokenizer, llm_name=llm_name, max_llm_sequence_len=max_llm_sequence_len)
return any(list_of_bools)
def delete_by_indices(data_list, indices):
"""
Delete elements from a list by their indices.
Args:
data_list: The list to delete from.
indices: The indices to delete.
Returns:
The list with the elements at the given indices deleted.
"""
data_list = data_list.copy()
# Sort indices in descending order to ensure deletion doesn't affect subsequent indices
for index in sorted(indices, reverse=True):
del data_list[index]
return data_list
def erase_and_check_insertion(prompt, pipeline, tokenizer, max_erase=5, num_adv=2, randomized=False, prompt_sampling_ratio=0.1, llm_name='Llama-2', max_llm_sequence_len=200):
"""
A generalized version of erase_and_check() that can defend against multiple adversarial prompts inserted into the prompt
where each adversarial prompt is a contiguous block of adversarial tokens.
Args:
prompt: The prompt to erase from.
pipeline: The pipeline to use to generate strings.
tokenizer: The tokenizer to use to tokenize the prompt.
max_erase: The maximum number of tokens to erase for each contiguous block.
min_length: The minimum length of each erased prompt. Any prompt shorter than this will be regarded as not harmful.
num_adv: The number of adversarial prompts to defend against.
Returns:
True if any of the erased strings is harmful, False otherwise.
"""
# Tokenize the prompt
prompt_tokens = tokenizer(prompt)['input_ids'][1:]
prompt_length = len(prompt_tokens)
prompt_set = {prompt}
# All possible gap and num_erase values
args = []
for k in range(num_adv):
args.append(range(prompt_length))
args.append(range(max_erase + 1))
# Iterate over all possible combinations of gap and num_erase values
for combination in itertools.product(*args):
erase_locations = []
start = 0
end = 0
for i in range(len(combination) // 2):
start = end + combination[(2*i)]
end = start + combination[(2*i) + 1]
if start >= prompt_length or end > prompt_length:
erase_locations = []
break
erase_locations.extend(range(start, end))
# if len(erase_locations) == 0 or len(erase_locations) > (prompt_length - min_length):
if len(erase_locations) == 0 or len(erase_locations) > (prompt_length):
continue
erased_prompt_tokens = delete_by_indices(prompt_tokens, erase_locations)
erased_prompt = tokenizer.decode(erased_prompt_tokens)
prompt_set.add(erased_prompt)
prompt_list = list(prompt_set)
if randomized:
num_prompts_to_test = int(prompt_sampling_ratio * (len(prompt_list) - 1))
prompts_to_test = prompt_list[0:1] + random.sample(prompt_list[1:], num_prompts_to_test)
else:
prompts_to_test = prompt_list
# Check if any of the erased strings is harmful
list_of_bools = is_harmful(prompts_to_test, pipeline, tokenizer, llm_name=llm_name, max_llm_sequence_len=max_llm_sequence_len)
return any(list_of_bools)
def erase_and_check_smoothing(prompt, pipeline, tokenizer, max_erase=20):
"""
Erase the prompt one token at a time from the end and check if majority of the generated substrings are harmful.
Args:
prompt: The prompt to erase from.
pipeline: The pipeline to use to generate strings.
tokenizer: The tokenizer to use to tokenize the prompt.
max_erase: The maximum number of tokens to erase from the end of the prompt.
Returns:
Certified length = # of harmful subsequences - ceil((min(max_erase, prompt_length) + 1) / 2)
"""
# Tokenize the prompt
prompt_tokens = tokenizer(prompt)['input_ids'][1:]
prompt_length = len(prompt_tokens)
# Erase the prompt one token at a time from the end
prompt_list = [prompt]
# print("Original prompt: ", prompt)
for i in range(min(max_erase, prompt_length)):
erased_prompt_tokens = prompt_tokens[:-(i+1)]
erased_prompt = tokenizer.decode(erased_prompt_tokens)
# print("Erased prompt: ", erased_prompt)
prompt_list.append(erased_prompt)
# Check if majority of the erased strings are harmful
list_of_bools = is_harmful(prompt_list, pipeline, tokenizer)
# Number of harmful prompts
num_harmful = sum(list_of_bools)
return min(num_harmful - 1, floor(max_erase / 2))
# return max(0, sum(list_of_bools) - ceil((min(max_erase, prompt_length) + 1) / 2))
def progress_bar(done, done_symbol='█', left_symbol='▒', length=25):
bar_done = int(done * length)
bar_left = length - bar_done
return done_symbol * bar_done + left_symbol * bar_left + ' %3d%%' % (done * 100)