ashwath-vaithina-ibm commited on
Commit
4c7f050
·
verified ·
1 Parent(s): e08c0b3

Update control/recommendation_handler.py

Browse files
Files changed (1) hide show
  1. control/recommendation_handler.py +133 -131
control/recommendation_handler.py CHANGED
@@ -29,7 +29,6 @@ import requests
29
  import json
30
  import math
31
  import re
32
- import warnings
33
  import pandas as pd
34
  import numpy as np
35
  from sklearn.metrics.pairwise import cosine_similarity
@@ -37,10 +36,7 @@ import os
37
  #os.environ['TRANSFORMERS_CACHE'] ="./models/allmini/cache"
38
  import os.path
39
  from sentence_transformers import SentenceTransformer
40
- from umap import UMAP
41
- import tensorflow as tf
42
- from umap.parametric_umap import ParametricUMAP, load_ParametricUMAP
43
- from sentence_transformers import SentenceTransformer
44
 
45
  def populate_json(json_file_path = './prompt-sentences-main/prompt_sentences-all-minilm-l6-v2.json',
46
  existing_json_populated_file_path = './prompt-sentences-main/prompt_sentences-all-minilm-l6-v2.json'):
@@ -64,45 +60,31 @@ def populate_json(json_file_path = './prompt-sentences-main/prompt_sentences-all
64
  json_file = json_file_path
65
  if(os.path.isfile(existing_json_populated_file_path)):
66
  json_file = existing_json_populated_file_path
67
- try:
68
- prompt_json = json.load(open(json_file))
69
- json_error = None
70
- return prompt_json, json_error
71
- except Exception as e:
72
- json_error = e
73
- print(f'Error when loading sentences json file: {json_error}')
74
- prompt_json = None
75
- return prompt_json, json_error
76
-
77
- def query(texts, api_url, headers):
78
- """
79
- Function that requests embeddings for a given sentence.
80
-
81
- Args:
82
- texts: The sentence or entered prompt text.
83
- api_url: API url for HF request.
84
- headers: Content headers for HF request.
85
-
86
- Returns:
87
- A json with the sentence embeddings.
88
-
89
- Raises:
90
- Warning: Warns about sentences that have more
91
- than 256 words.
92
- """
93
- for t in texts:
94
- n_words = len(re.split(r"\s+", t))
95
- if(n_words > 256):
96
- # warning in case of prompts longer than 256 words
97
- warnings.warn("Warning: Sentence provided is longer than 256 words. Model all-MiniLM-L6-v2 expects sentences up to 256 words.")
98
- warnings.warn("Word count:{}".format(n_words))
99
- if('sentence-transformers/all-MiniLM-L6-v2' in api_url):
100
- model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
101
- out = model.encode(texts).tolist()
102
  else:
103
- response = requests.post(api_url, headers=headers, json={"inputs": texts, "options":{"wait_for_model":True}})
104
- out = response.json()
105
- return out
106
 
107
  def split_into_sentences(prompt):
108
  """
@@ -123,27 +105,6 @@ def split_into_sentences(prompt):
123
  sentences = re.split(r'(?<=[.!?]) +', prompt)
124
  return sentences
125
 
126
-
127
- def get_similarity(embedding1, embedding2):
128
- """
129
- Function that returns cosine similarity between
130
- two embeddings.
131
-
132
- Args:
133
- embedding1: first embedding.
134
- embedding2: second embedding.
135
-
136
- Returns:
137
- The similarity value.
138
-
139
- Raises:
140
- Nothing.
141
- """
142
- v1 = np.array( embedding1 ).reshape( 1, -1 )
143
- v2 = np.array( embedding2 ).reshape( 1, -1 )
144
- similarity = cosine_similarity( v1, v2 )
145
- return similarity[0, 0]
146
-
147
  def get_distance(embedding1, embedding2):
148
  """
149
  Function that returns euclidean distance between
@@ -181,17 +142,24 @@ def sort_by_similarity(e):
181
  """
182
  return e['similarity']
183
 
184
- def recommend_prompt(prompt, prompt_json, api_url, headers, add_lower_threshold = 0.3,
185
- add_upper_threshold = 0.5, remove_lower_threshold = 0.1,
186
- remove_upper_threshold = 0.5, model_id = 'sentence-transformers/all-minilm-l6-v2'):
 
 
 
 
 
 
 
187
  """
188
  Function that recommends prompts additions or removals.
189
 
190
  Args:
191
  prompt: The entered prompt text.
192
  prompt_json: Json file populated with embeddings.
193
- api_url: API url for HF request.
194
- headers: Content headers for HF request.
195
  add_lower_threshold: Lower threshold for sentence addition,
196
  the default value is 0.3.
197
  add_upper_threshold: Upper threshold for sentence addition,
@@ -200,7 +168,8 @@ def recommend_prompt(prompt, prompt_json, api_url, headers, add_lower_threshold
200
  the default value is 0.3.
201
  remove_upper_threshold: Upper threshold for sentence removal,
202
  the default value is 0.5.
203
- model_id: Id of the model, the default value is all-minilm-l6-v2 movel.
 
204
 
205
  Returns:
206
  Prompt values to add or remove.
@@ -210,15 +179,18 @@ def recommend_prompt(prompt, prompt_json, api_url, headers, add_lower_threshold
210
  """
211
  if(model_id == 'baai/bge-large-en-v1.5' ):
212
  json_file = './prompt-sentences-main/prompt_sentences-bge-large-en-v1.5.json'
213
- umap_model = load_ParametricUMAP('./models/umap/BAAI/bge-large-en-v1.5/')
214
  elif(model_id == 'intfloat/multilingual-e5-large'):
215
  json_file = './prompt-sentences-main/prompt_sentences-multilingual-e5-large.json'
216
- umap_model = load_ParametricUMAP('./models/umap/intfloat/multilingual-e5-large/')
217
  else: # fall back to all-minilm as default
218
  json_file = './prompt-sentences-main/prompt_sentences-all-minilm-l6-v2.json'
219
- umap_model = load_ParametricUMAP('./models/umap/sentence-transformers/all-MiniLM-L6-v2/')
220
 
221
- prompt_json = json.load(open(json_file))
 
 
 
222
 
223
  # Output initialization
224
  out, out['input'], out['add'], out['remove'] = {}, {}, {}, {}
@@ -231,63 +203,84 @@ def recommend_prompt(prompt, prompt_json, api_url, headers, add_lower_threshold
231
 
232
  # Recommendation of values to add to the current prompt
233
  # Using only the last sentence for the add recommendation
234
- input_embedding = query(input_sentences[-1], api_url, headers)
235
- for v in prompt_json['positive_values']:
 
 
 
 
 
 
 
 
236
  # Dealing with values without prompts and makinig sure they have the same dimensions
237
- if(len(v['centroid']) == len(input_embedding)):
238
- if(get_similarity(pd.DataFrame(input_embedding), pd.DataFrame(v['centroid'])) > add_lower_threshold):
239
- closer_prompt = -1
240
- for p in v['prompts']:
241
- d_prompt = get_similarity(pd.DataFrame(input_embedding), pd.DataFrame(p['embedding']))
242
- # The sentence_threshold is being used as a ceiling meaning that for high similarities the sentence/value might already be presente in the prompt
243
- # So, we don't want to recommend adding something that is already there
244
- if(d_prompt > closer_prompt and d_prompt > add_lower_threshold and d_prompt < add_upper_threshold):
245
- closer_prompt = d_prompt
246
- items_to_add.append({
247
- 'value': v['label'],
248
- 'prompt': p['text'],
249
- 'similarity': d_prompt,
250
- 'x': p['x'],
251
- 'y': p['y']})
252
- out['add'] = items_to_add
 
 
 
 
 
253
 
254
- # Recommendation of values to remove from the current prompt
255
- i = 0
 
 
 
256
 
257
  # Recommendation of values to remove from the current prompt
258
  for sentence in input_sentences:
259
  input_embedding = query(sentence, api_url, headers) # remote
260
- # Obtaining XY coords for input sentences from a parametric UMAP model
261
  if(len(prompt_json['negative_values'][0]['centroid']) == len(input_embedding) and sentence != ''):
262
- embeddings_umap = umap_model.transform(tf.expand_dims(pd.DataFrame(input_embedding), axis=0))
263
  input_items.append({
264
  'sentence': sentence,
265
  'x': str(embeddings_umap[0][0]),
266
  'y': str(embeddings_umap[0][1])
267
  })
268
 
269
- for v in prompt_json['negative_values']:
270
- # Dealing with values without prompts and makinig sure they have the same dimensions
271
- if(len(v['centroid']) == len(input_embedding)):
272
- if(get_similarity(pd.DataFrame(input_embedding), pd.DataFrame(v['centroid'])) > remove_lower_threshold):
273
- closer_prompt = -1
274
- for p in v['prompts']:
275
- d_prompt = get_similarity(pd.DataFrame(input_embedding), pd.DataFrame(p['embedding']))
276
- # A more restrict threshold is used here to prevent false positives
277
- # The sentence_threshold is being used to indicate that there must be a sentence in the prompt that is similiar to one of our adversarial prompts
278
- # So, yes, we want to recommend the removal of something adversarial we've found
279
- if(d_prompt > closer_prompt and d_prompt > remove_upper_threshold):
280
- closer_prompt = d_prompt
281
- items_to_remove.append({
282
- 'value': v['label'],
283
- 'sentence': sentence,
284
- 'sentence_index': i,
285
- 'closest_harmful_sentence': p['text'],
286
- 'similarity': d_prompt,
287
- 'x': p['x'],
288
- 'y': p['y']})
289
- out['remove'] = items_to_remove
290
- i += 1
 
 
 
 
 
291
 
292
  out['input'] = input_items
293
 
@@ -310,14 +303,19 @@ def recommend_prompt(prompt, prompt_json, api_url, headers, add_lower_threshold
310
  out['remove'] = out['remove'][0:5]
311
  return out
312
 
313
- def get_thresholds(prompts, prompt_json, api_url, headers, model_id = 'sentence-transformers/all-minilm-l6-v2'):
 
 
 
 
314
  """
315
  Function that recommends thresholds given an array of prompts.
316
 
317
  Args:
318
  prompts: The array with samples of prompts to be used in the system.
319
  prompt_json: Sentences to be forwarded to the recommendation endpoint.
320
- model_id: Id of the model, the default value is all-minilm-l6-v2 model.
 
321
 
322
  Returns:
323
  A map with thresholds for the sample prompts and the informed model.
@@ -325,14 +323,15 @@ def get_thresholds(prompts, prompt_json, api_url, headers, model_id = 'sentence-
325
  Raises:
326
  Nothing.
327
  """
328
- # Array limits for retrieving the thresholds
329
- # if( len( prompts ) < 10 or len( prompts ) > 30 ):
330
- # return -1
 
331
  add_similarities = []
332
  remove_similarities = []
333
 
334
  for p_id, p in enumerate(prompts):
335
- out = recommend_prompt(p, prompt_json, api_url, headers, 0, 1, 0, 0, model_id) # Wider possible range
336
 
337
  for r in out['add']:
338
  add_similarities.append(r['similarity'])
@@ -371,15 +370,18 @@ def recommend_local(prompt, prompt_json, model_id, model_path = './models/all-Mi
371
  """
372
  if(model_id == 'baai/bge-large-en-v1.5' ):
373
  json_file = './prompt-sentences-main/prompt_sentences-bge-large-en-v1.5.json'
374
- umap_model = load_ParametricUMAP('./models/umap/BAAI/bge-large-en-v1.5/')
375
  elif(model_id == 'intfloat/multilingual-e5-large'):
376
  json_file = './prompt-sentences-main/prompt_sentences-multilingual-e5-large.json'
377
- umap_model = load_ParametricUMAP('./models/umap/intfloat/multilingual-e5-large/')
378
  else: # fall back to all-minilm as default
379
  json_file = './prompt-sentences-main/prompt_sentences-all-minilm-l6-v2.json'
380
- umap_model = load_ParametricUMAP('./models/umap/sentence-transformers/all-MiniLM-L6-v2/')
381
 
382
- prompt_json = json.load(open(json_file))
 
 
 
383
 
384
  # Output initialization
385
  out, out['input'], out['add'], out['remove'] = {}, {}, {}, {}
@@ -418,9 +420,9 @@ def recommend_local(prompt, prompt_json, model_id, model_path = './models/all-Mi
418
  # Recommendation of values to remove from the current prompt
419
  for sentence in input_sentences:
420
  input_embedding = model.encode(sentence) # local
421
- # Obtaining XY coords for input sentences from a parametric UMAP model
422
  if(len(prompt_json['negative_values'][0]['centroid']) == len(input_embedding) and sentence != ''):
423
- embeddings_umap = umap_model.transform(tf.expand_dims(pd.DataFrame(input_embedding), axis=0))
424
  input_items.append({
425
  'sentence': sentence,
426
  'x': str(embeddings_umap[0][0]),
@@ -469,4 +471,4 @@ def recommend_local(prompt, prompt_json, model_id, model_path = './models/all-Mi
469
  else:
470
  values_map[item['value']] = item['similarity']
471
  out['remove'] = out['remove'][0:5]
472
- return out
 
29
  import json
30
  import math
31
  import re
 
32
  import pandas as pd
33
  import numpy as np
34
  from sklearn.metrics.pairwise import cosine_similarity
 
36
  #os.environ['TRANSFORMERS_CACHE'] ="./models/allmini/cache"
37
  import os.path
38
  from sentence_transformers import SentenceTransformer
39
+ import pickle
 
 
 
40
 
41
  def populate_json(json_file_path = './prompt-sentences-main/prompt_sentences-all-minilm-l6-v2.json',
42
  existing_json_populated_file_path = './prompt-sentences-main/prompt_sentences-all-minilm-l6-v2.json'):
 
60
  json_file = json_file_path
61
  if(os.path.isfile(existing_json_populated_file_path)):
62
  json_file = existing_json_populated_file_path
63
+ prompt_json = json.load(open(json_file))
64
+ return prompt_json
65
+
66
+ def get_embedding_func(inference = 'huggingface', **kwargs):
67
+ if inference == 'local':
68
+ if 'model_id' not in kwargs:
69
+ raise TypeError("Missing required argument: model_id")
70
+ model = SentenceTransformer(kwargs['model_id'])
71
+
72
+ def embedding_fn(texts):
73
+ return model.encode(texts).tolist()
74
+
75
+ elif inference == 'huggingface':
76
+ if 'api_url' not in kwargs:
77
+ raise TypeError("Missing required argument: api_url")
78
+ if 'headers' not in kwargs:
79
+ raise TypeError("Missing required argument: headers")
80
+
81
+ def embedding_fn(texts):
82
+ response = requests.post(kwargs['api_url'], headers=kwargs['headers'], json={"inputs": texts, "options":{"wait_for_model":True}})
83
+ return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  else:
85
+ raise ValueError(f"Inference type {inference} is not supported. Please choose one of ['local', 'huggingface'].")
86
+
87
+ return embedding_fn
88
 
89
  def split_into_sentences(prompt):
90
  """
 
105
  sentences = re.split(r'(?<=[.!?]) +', prompt)
106
  return sentences
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  def get_distance(embedding1, embedding2):
109
  """
110
  Function that returns euclidean distance between
 
142
  """
143
  return e['similarity']
144
 
145
+ def recommend_prompt(
146
+ prompt,
147
+ prompt_json,
148
+ embedding_fn = None,
149
+ add_lower_threshold = 0.3,
150
+ add_upper_threshold = 0.5,
151
+ remove_lower_threshold = 0.1,
152
+ remove_upper_threshold = 0.5,
153
+ umap_model = None
154
+ ):
155
  """
156
  Function that recommends prompts additions or removals.
157
 
158
  Args:
159
  prompt: The entered prompt text.
160
  prompt_json: Json file populated with embeddings.
161
+ embedding_fn: Embedding function to convert prompt sentences into embeddings.
162
+ If None, uses all-MiniLM-L6-v2 run locally.
163
  add_lower_threshold: Lower threshold for sentence addition,
164
  the default value is 0.3.
165
  add_upper_threshold: Upper threshold for sentence addition,
 
168
  the default value is 0.3.
169
  remove_upper_threshold: Upper threshold for sentence removal,
170
  the default value is 0.5.
171
+ umap_model: Umap model used for visualization.
172
+ If None, the projected embeddings of input sentences will not be returned.
173
 
174
  Returns:
175
  Prompt values to add or remove.
 
179
  """
180
  if(model_id == 'baai/bge-large-en-v1.5' ):
181
  json_file = './prompt-sentences-main/prompt_sentences-bge-large-en-v1.5.json'
182
+ umap_model_file = './models/umap/intfloat/multilingual-e5-large/umap.pkl'
183
  elif(model_id == 'intfloat/multilingual-e5-large'):
184
  json_file = './prompt-sentences-main/prompt_sentences-multilingual-e5-large.json'
185
+ umap_model_file = './models/umap/intfloat/multilingual-e5-large/umap.pkl'
186
  else: # fall back to all-minilm as default
187
  json_file = './prompt-sentences-main/prompt_sentences-all-minilm-l6-v2.json'
188
+ umap_model_file = './models/umap/sentence-transformers/all-MiniLM-L6-v2/umap.pkl'
189
 
190
+ with open(umap_model_file, 'rb') as f:
191
+ umap_model = pickle.load(f)
192
+
193
+ prompt_json = json.load( open( json_file ) )
194
 
195
  # Output initialization
196
  out, out['input'], out['add'], out['remove'] = {}, {}, {}, {}
 
203
 
204
  # Recommendation of values to add to the current prompt
205
  # Using only the last sentence for the add recommendation
206
+ input_embedding = embedding_fn(input_sentences[-1])
207
+ input_embedding = np.array(input_embedding)
208
+
209
+ sentence_embeddings = np.array(
210
+ [v['centroid'] for v in prompt_json['positive_values']]
211
+ )
212
+
213
+ similarities_positive_sent = cosine_similarity(np.expand_dims(input_embedding, axis=0), sentence_embeddings)[0, :]
214
+
215
+ for value_idx, v in enumerate(prompt_json['positive_values']):
216
  # Dealing with values without prompts and makinig sure they have the same dimensions
217
+ if(len(v['centroid']) != len(input_embedding)):
218
+ continue
219
+
220
+ if(similarities_positive_sent[value_idx] < add_lower_threshold):
221
+ continue
222
+
223
+ value_sents_similarity = cosine_similarity(
224
+ np.expand_dims(input_embedding, axis=0),
225
+ np.array([p['embedding'] for p in v['prompts']])
226
+ )[0, :]
227
+ closer_prompt_idxs = np.nonzero((add_lower_threshold < value_sents_similarity) & (value_sents_similarity < add_upper_threshold))[0]
228
+
229
+ for idx in closer_prompt_idxs:
230
+ items_to_add.append({
231
+ 'value': v['label'],
232
+ 'prompt': v['prompts'][idx]['text'],
233
+ 'similarity': value_sents_similarity[idx],
234
+ 'x': v['prompts'][idx]['x'],
235
+ 'y': v['prompts'][idx]['y']
236
+ })
237
+ out['add'] = items_to_add
238
 
239
+ inp_sentence_embeddings = np.array([embedding_fn(sent) for sent in input_sentences])
240
+ pairwise_similarities = cosine_similarity(
241
+ inp_sentence_embeddings,
242
+ np.array([v['centroid'] for v in prompt_json['negative_values']])
243
+ )
244
 
245
  # Recommendation of values to remove from the current prompt
246
  for sentence in input_sentences:
247
  input_embedding = query(sentence, api_url, headers) # remote
248
+ # Obtaining XY coords for input sentences from a UMAP model
249
  if(len(prompt_json['negative_values'][0]['centroid']) == len(input_embedding) and sentence != ''):
250
+ embeddings_umap = umap_model.transform(np.expand_dims(pd.DataFrame(input_embedding).squeeze(), axis=0))
251
  input_items.append({
252
  'sentence': sentence,
253
  'x': str(embeddings_umap[0][0]),
254
  'y': str(embeddings_umap[0][1])
255
  })
256
 
257
+ for value_idx, v in enumerate(prompt_json['negative_values']):
258
+ # Dealing with values without prompts and making sure they have the same dimensions
259
+ if(len(v['centroid']) != len(input_embedding)):
260
+ continue
261
+ if(pairwise_similarities[sent_idx][value_idx] < remove_lower_threshold):
262
+ continue
263
+
264
+ # A more restrict threshold is used here to prevent false positives
265
+ # The sentence_threshold is being used to indicate that there must be a sentence in the prompt that is similiar to one of our adversarial prompts
266
+ # So, yes, we want to recommend the removal of something adversarial we've found
267
+ value_sents_similarity = cosine_similarity(
268
+ np.expand_dims(input_embedding, axis=0),
269
+ np.array([p['embedding'] for p in v['prompts']])
270
+ )[0, :]
271
+ closer_prompt_idxs = np.nonzero(value_sents_similarity > remove_upper_threshold)[0]
272
+
273
+ for idx in closer_prompt_idxs:
274
+ items_to_remove.append({
275
+ 'value': v['label'],
276
+ 'sentence': sentence,
277
+ 'sentence_index': sent_idx,
278
+ 'closest_harmful_sentence': v['prompts'][idx]['text'],
279
+ 'similarity': value_sents_similarity[idx],
280
+ 'x': v['prompts'][idx]['x'],
281
+ 'y': v['prompts'][idx]['y']
282
+ })
283
+ out['remove'] = items_to_remove
284
 
285
  out['input'] = input_items
286
 
 
303
  out['remove'] = out['remove'][0:5]
304
  return out
305
 
306
+ def get_thresholds(
307
+ prompts,
308
+ prompt_json,
309
+ embedding_fn = None,
310
+ ):
311
  """
312
  Function that recommends thresholds given an array of prompts.
313
 
314
  Args:
315
  prompts: The array with samples of prompts to be used in the system.
316
  prompt_json: Sentences to be forwarded to the recommendation endpoint.
317
+ embedding_fn: Embedding function to convert prompt sentences into embeddings.
318
+ If None, uses all-MiniLM-L6-v2 run locally.
319
 
320
  Returns:
321
  A map with thresholds for the sample prompts and the informed model.
 
323
  Raises:
324
  Nothing.
325
  """
326
+
327
+ if embedding_fn is None:
328
+ embedding_fn = get_embedding_func('local', model_id='sentence-transformers/all-MiniLM-L6-v2')
329
+
330
  add_similarities = []
331
  remove_similarities = []
332
 
333
  for p_id, p in enumerate(prompts):
334
+ out = recommend_prompt(p, prompt_json, embedding_fn, 0, 1, 0, 0, None) # Wider possible range
335
 
336
  for r in out['add']:
337
  add_similarities.append(r['similarity'])
 
370
  """
371
  if(model_id == 'baai/bge-large-en-v1.5' ):
372
  json_file = './prompt-sentences-main/prompt_sentences-bge-large-en-v1.5.json'
373
+ umap_model_file = './models/umap/intfloat/multilingual-e5-large/umap.pkl'
374
  elif(model_id == 'intfloat/multilingual-e5-large'):
375
  json_file = './prompt-sentences-main/prompt_sentences-multilingual-e5-large.json'
376
+ umap_model_file = './models/umap/intfloat/multilingual-e5-large/umap.pkl'
377
  else: # fall back to all-minilm as default
378
  json_file = './prompt-sentences-main/prompt_sentences-all-minilm-l6-v2.json'
379
+ umap_model_file = './models/umap/sentence-transformers/all-MiniLM-L6-v2/umap.pkl'
380
 
381
+ with open(umap_model_file, 'rb') as f:
382
+ umap_model = pickle.load(f)
383
+
384
+ prompt_json = json.load( open( json_file ) )
385
 
386
  # Output initialization
387
  out, out['input'], out['add'], out['remove'] = {}, {}, {}, {}
 
420
  # Recommendation of values to remove from the current prompt
421
  for sentence in input_sentences:
422
  input_embedding = model.encode(sentence) # local
423
+ # Obtaining XY coords for input sentences from a UMAP model
424
  if(len(prompt_json['negative_values'][0]['centroid']) == len(input_embedding) and sentence != ''):
425
+ embeddings_umap = umap_model.transform(np.expand_dims(pd.DataFrame(input_embedding).squeeze(), axis=0))
426
  input_items.append({
427
  'sentence': sentence,
428
  'x': str(embeddings_umap[0][0]),
 
471
  else:
472
  values_map[item['value']] = item['similarity']
473
  out['remove'] = out['remove'][0:5]
474
+ return out