Milad Alshomary commited on
Commit
ac7facf
·
1 Parent(s): e392716
app.py CHANGED
@@ -58,7 +58,7 @@ def app(share=False, use_cluster_feats=False):
58
  instances, instance_ids = get_instances(cfg['instances_to_explain_path'])
59
 
60
  interp = load_interp_space(cfg)
61
- clustered_authors_df = interp['clustered_authors_df'][:1000]
62
  clustered_authors_df['fullText'] = clustered_authors_df['fullText'].map(lambda l: l[:3]) # Take at most 3 texts per author
63
 
64
  with gr.Blocks(title="Author Attribution Explainability Tool") as demo:
 
58
  instances, instance_ids = get_instances(cfg['instances_to_explain_path'])
59
 
60
  interp = load_interp_space(cfg)
61
+ clustered_authors_df = interp['clustered_authors_df'][:500]
62
  clustered_authors_df['fullText'] = clustered_authors_df['fullText'].map(lambda l: l[:3]) # Take at most 3 texts per author
63
 
64
  with gr.Blocks(title="Author Attribution Explainability Tool") as demo:
config/config.yaml CHANGED
@@ -1,8 +1,8 @@
1
  # config.yaml
2
  instances_to_explain_path: "./datasets/hrs_explanations.json"
3
- instances_to_explain_url: "https://huggingface.co/datasets/miladalsh/explanation_tool_files/raw/main/hrs_explanations.json?download"
4
- interp_space_path: "./datasets/luar_interp_space_cluster_19/"
5
- interp_space_url: "https://huggingface.co/datasets/miladalsh/explanation_tool_files/resolve/main/luar_interp_space_cluster.zip?download=true"
6
  gram2vec_feats_path: "./datasets/gram2vec_feats.csv"
7
  gram2vec_feats_url: "https://huggingface.co/datasets/miladalsh/explanation_tool_files/resolve/main/gram2vec_feats.csv?download=true"
8
 
 
1
  # config.yaml
2
  instances_to_explain_path: "./datasets/hrs_explanations.json"
3
+ instances_to_explain_url: "https://huggingface.co/datasets/miladalsh/explanation_tool_files/resolve/main/hrs_explanations_luar_clusters_18_balanced.json?/download=true"
4
+ interp_space_path: "./datasets/luar_interp_space_cluster_18/"
5
+ interp_space_url: "https://huggingface.co/datasets/miladalsh/explanation_tool_files/resolve/main/luar_interp_space_cluster_18.zip?download=true"
6
  gram2vec_feats_path: "./datasets/gram2vec_feats.csv"
7
  gram2vec_feats_url: "https://huggingface.co/datasets/miladalsh/explanation_tool_files/resolve/main/gram2vec_feats.csv?download=true"
8
 
utils/gram2vec_feat_utils.py CHANGED
@@ -126,7 +126,7 @@ def highlight_both_spans(text, llm_spans, gram_spans):
126
 
127
 
128
  def show_combined_spans_all(selected_feature_llm, selected_feature_g2v,
129
- llm_style_feats_analysis, background_authors_embeddings_df, task_authors_embeddings_df, visible_authors, predicted_author=None, ground_truth_author=None, max_num_authors=7):
130
  """
131
  For mystery + 3 candidates:
132
  1. get llm spans via your existing cache+API
@@ -226,15 +226,15 @@ def get_label(label: str, predicted_author=None, ground_truth_author=None, bg_id
226
  id = label.split("_")[0][-1] # Get the last character of the first part (a0, a1, a2)
227
  if predicted_author is not None and ground_truth_author is not None:
228
  if int(id) == predicted_author and int(id) == ground_truth_author:
229
- return f"Candidate {int(id)+1} (Predicted & Ground Truth)"
230
  elif int(id) == predicted_author:
231
- return f"Candidate {int(id)+1} (Predicted)"
232
  elif int(id) == ground_truth_author:
233
- return f"Candidate {int(id)+1} (Ground Truth)"
234
  else:
235
- return f"Candidate {int(id)+1}"
236
  else:
237
- return f"Candidate {int(id)+1}"
238
  else:
239
  return f"Background Author {bg_id+1}"
240
 
 
126
 
127
 
128
  def show_combined_spans_all(selected_feature_llm, selected_feature_g2v,
129
+ llm_style_feats_analysis, background_authors_embeddings_df, task_authors_embeddings_df, visible_authors, predicted_author=None, ground_truth_author=None, max_num_authors=4):
130
  """
131
  For mystery + 3 candidates:
132
  1. get llm spans via your existing cache+API
 
226
  id = label.split("_")[0][-1] # Get the last character of the first part (a0, a1, a2)
227
  if predicted_author is not None and ground_truth_author is not None:
228
  if int(id) == predicted_author and int(id) == ground_truth_author:
229
+ return f"Candidate {int(id)} (Predicted & Ground Truth)"
230
  elif int(id) == predicted_author:
231
+ return f"Candidate {int(id)} (Predicted)"
232
  elif int(id) == ground_truth_author:
233
+ return f"Candidate {int(id)} (Ground Truth)"
234
  else:
235
+ return f"Candidate {int(id)}"
236
  else:
237
+ return f"Candidate {int(id)}"
238
  else:
239
  return f"Background Author {bg_id+1}"
240
 
utils/interp_space_utils.py CHANGED
@@ -126,9 +126,9 @@ def instance_to_df(instance, predicted_author=None, ground_truth_author=None):
126
  #create a dataframe of the task authors
127
  task_authos_df = pd.DataFrame([
128
  {'authorID': 'Mystery author', 'fullText': instance['Q_fullText'], 'predicted': None, 'ground_truth': None},
129
- {'authorID': 'Candidate Author 1', 'fullText': instance['a0_fullText'], 'predicted': predicted_author == 0, 'ground_truth': ground_truth_author == 0},
130
- {'authorID': 'Candidate Author 2', 'fullText': instance['a1_fullText'], 'predicted': predicted_author == 1, 'ground_truth': ground_truth_author == 1},
131
- {'authorID': 'Candidate Author 3', 'fullText': instance['a2_fullText'], 'predicted': predicted_author == 2, 'ground_truth': ground_truth_author == 2}
132
 
133
  ])
134
 
@@ -479,7 +479,7 @@ def compute_clusters_style_representation_3(
479
  background_corpus_df: pd.DataFrame,
480
  cluster_ids: List[Any],
481
  cluster_label_clm_name: str = 'authorID',
482
- max_num_feats: int = 5,
483
  max_num_documents_per_author=3,
484
  max_num_authors=5
485
  ):
@@ -494,35 +494,46 @@ def compute_clusters_style_representation_3(
494
  author_names = background_corpus_df_feat_id[cluster_label_clm_name].tolist()[:max_num_authors]
495
  print(f"Number of authors: {len(background_corpus_df_feat_id)}")
496
  print(author_names)
497
- print(author_texts)
498
- print(f"Number of authors: {len(author_names)}")
499
- print(f"Number of authors: {len(author_texts)}")
500
  features = identify_style_features(author_texts, max_num_feats=max_num_feats)
501
 
502
  # STEP 2: Prepare author pool for span extraction
503
-
504
- span_df = background_corpus_df.iloc[:7]
505
- author_names = span_df[cluster_label_clm_name].tolist()[:7]
506
  print(f"Number of authors for span detection : {len(span_df)}")
507
  print(author_names)
508
  spans_by_author = extract_all_spans(span_df, features, cluster_label_clm_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
 
510
  return {
511
  "features": features,
512
  "spans": spans_by_author
513
  }
514
 
515
-
516
  def compute_clusters_g2v_representation(
517
  background_corpus_df: pd.DataFrame,
518
  author_ids: List[Any],
519
  other_author_ids: List[Any],
520
  features_clm_name: str,
521
- top_n: int = 10
 
 
 
522
  ) -> List[str]:
523
 
524
 
525
- # Get boolean mask for documents in selected clusters
526
  selected_mask = background_corpus_df['authorID'].isin(author_ids).to_numpy()
527
 
528
  if not selected_mask.any():
@@ -530,8 +541,33 @@ def compute_clusters_g2v_representation(
530
 
531
  selected_feats = background_corpus_df[selected_mask][features_clm_name].tolist()
532
  all_g2v_feats = list(selected_feats[0].keys())
533
- all_g2v_values = np.array([list(x.values()) for x in selected_feats]).mean(axis=0)
534
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
 
536
  other_selected_feats = background_corpus_df[~selected_mask][features_clm_name].tolist()
537
  all_g2v_other_feats = list(other_selected_feats[0].keys())
@@ -541,10 +577,24 @@ def compute_clusters_g2v_representation(
541
 
542
 
543
  top_g2v_feats = sorted(list(zip(all_g2v_feats, final_g2v_feats_values)), key=lambda x: -x[1])
544
- print(top_g2v_feats[:top_n])
545
-
546
- return [x[0] for x in top_g2v_feats[:top_n]]
547
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
 
549
  def generate_interpretable_space_representation(interp_space_path, styles_df_path, feat_clm, output_clm, num_feats=5):
550
 
 
126
  #create a dataframe of the task authors
127
  task_authos_df = pd.DataFrame([
128
  {'authorID': 'Mystery author', 'fullText': instance['Q_fullText'], 'predicted': None, 'ground_truth': None},
129
+ {'authorID': 'Candidate Author 1', 'fullText': instance['a0_fullText'], 'predicted': int(predicted_author) == 0, 'ground_truth': int(ground_truth_author) == 0},
130
+ {'authorID': 'Candidate Author 2', 'fullText': instance['a1_fullText'], 'predicted': int(predicted_author) == 1, 'ground_truth': int(ground_truth_author) == 1},
131
+ {'authorID': 'Candidate Author 3', 'fullText': instance['a2_fullText'], 'predicted': int(predicted_author) == 2, 'ground_truth': int(ground_truth_author) == 2}
132
 
133
  ])
134
 
 
479
  background_corpus_df: pd.DataFrame,
480
  cluster_ids: List[Any],
481
  cluster_label_clm_name: str = 'authorID',
482
+ max_num_feats: int = 10,
483
  max_num_documents_per_author=3,
484
  max_num_authors=5
485
  ):
 
494
  author_names = background_corpus_df_feat_id[cluster_label_clm_name].tolist()[:max_num_authors]
495
  print(f"Number of authors: {len(background_corpus_df_feat_id)}")
496
  print(author_names)
 
 
 
497
  features = identify_style_features(author_texts, max_num_feats=max_num_feats)
498
 
499
  # STEP 2: Prepare author pool for span extraction
500
+ span_df = background_corpus_df.iloc[:4]
501
+ author_names = span_df[cluster_label_clm_name].tolist()[:4]
 
502
  print(f"Number of authors for span detection : {len(span_df)}")
503
  print(author_names)
504
  spans_by_author = extract_all_spans(span_df, features, cluster_label_clm_name)
505
+
506
+ # Filter out features that are not present in any of the authors
507
+ filtered_spans_by_author = {x[0] : x[1] for x in spans_by_author.items() if x[0] in {'Mystery author', 'Candidate Author 1', 'Candidate Author 2', 'Candidate Author 3'}.intersection(set(cluster_ids))}
508
+ print('Filtering in features for only the following authors: ', filtered_spans_by_author.keys())
509
+ filtered_features = []
510
+ for feature in features:
511
+ found_in_any_author = False
512
+ for author_name, author_spans in filtered_spans_by_author.items():
513
+ if feature in author_spans:
514
+ found_in_any_author = True
515
+ break
516
+ if found_in_any_author:
517
+ filtered_features.append(feature)
518
+ features = filtered_features
519
 
520
  return {
521
  "features": features,
522
  "spans": spans_by_author
523
  }
524
 
 
525
  def compute_clusters_g2v_representation(
526
  background_corpus_df: pd.DataFrame,
527
  author_ids: List[Any],
528
  other_author_ids: List[Any],
529
  features_clm_name: str,
530
+ top_n: int = 10,
531
+ mode: str = "sharedness",
532
+ sharedness_method: str = "mean_minus_alpha_std",
533
+ alpha: float = 0.5
534
  ) -> List[str]:
535
 
536
 
 
537
  selected_mask = background_corpus_df['authorID'].isin(author_ids).to_numpy()
538
 
539
  if not selected_mask.any():
 
541
 
542
  selected_feats = background_corpus_df[selected_mask][features_clm_name].tolist()
543
  all_g2v_feats = list(selected_feats[0].keys())
 
544
 
545
+ # If the user requested a sharedness-based score, compute it and return top-N.
546
+ if mode == "sharedness":
547
+ selected_matrix = np.array([list(x.values()) for x in selected_feats], dtype=float)
548
+
549
+ if sharedness_method == "mean":
550
+ scores = selected_matrix.mean(axis=0)
551
+ elif sharedness_method in ("mean_minus_alpha_std", "mean-std", "mean_minus_std"):
552
+ means = selected_matrix.mean(axis=0)
553
+ stds = selected_matrix.std(axis=0)
554
+ scores = means - float(alpha) * stds
555
+ elif sharedness_method == "min":
556
+ scores = selected_matrix.min(axis=0)
557
+ else:
558
+ # Default fallback to mean-minus-alpha*std if unknown method
559
+ means = selected_matrix.mean(axis=0)
560
+ stds = selected_matrix.std(axis=0)
561
+ scores = means - float(alpha) * stds
562
+
563
+ # Rank and return
564
+ feature_scores = [(feat, score) for feat, score in zip(all_g2v_feats, scores) if score > 0]
565
+ feature_scores.sort(key=lambda x: x[1], reverse=True)
566
+ return [feat for feat, _ in feature_scores[:top_n]]
567
+
568
+
569
+ # Contrastive mode (default): compute target mean and subtract contrast mean
570
+ all_g2v_values = np.array([list(x.values()) for x in selected_feats]).mean(axis=0)
571
 
572
  other_selected_feats = background_corpus_df[~selected_mask][features_clm_name].tolist()
573
  all_g2v_other_feats = list(other_selected_feats[0].keys())
 
577
 
578
 
579
  top_g2v_feats = sorted(list(zip(all_g2v_feats, final_g2v_feats_values)), key=lambda x: -x[1])
580
+
581
+ # Filter out features that are not present in any of the authors
582
+ selected_authors = {'Mystery author', 'Candidate Author 1', 'Candidate Author 2', 'Candidate Author 3'}.intersection(set(author_ids))
583
+ print('Filtering in g2v features for only the following authors: ', selected_authors)
584
+ authors_g2v_feats = background_corpus_df[background_corpus_df['authorID'].isin(selected_authors)][features_clm_name].tolist()
585
+ filtered_features = []
586
+ for feature, score in top_g2v_feats:
587
+ found_in_any_author = False
588
+ for author_g2v_feats in authors_g2v_feats:
589
+ if author_g2v_feats[feature] > 0:
590
+ found_in_any_author = True
591
+ break
592
+ if found_in_any_author:
593
+ filtered_features.append(feature)
594
+
595
+ print('Filtered G2V features: ', filtered_features)
596
+
597
+ return filtered_features[:top_n]
598
 
599
  def generate_interpretable_space_representation(interp_space_path, styles_df_path, feat_clm, output_clm, num_feats=5):
600
 
utils/llm_feat_utils.py CHANGED
@@ -90,6 +90,7 @@ def generate_feature_spans_cached(client, text: str, features: list[str], role:
90
  os.makedirs(CACHE_DIR, exist_ok=True)
91
  cache_path = os.path.join(CACHE_DIR, f"{role}.json")
92
  if os.path.exists(cache_path):
 
93
  with open(cache_path) as f:
94
  cache: dict[str, dict] = json.load(f)
95
  else:
 
90
  os.makedirs(CACHE_DIR, exist_ok=True)
91
  cache_path = os.path.join(CACHE_DIR, f"{role}.json")
92
  if os.path.exists(cache_path):
93
+ print(f"Cache hit....")
94
  with open(cache_path) as f:
95
  cache: dict[str, dict] = json.load(f)
96
  else:
utils/ui.py CHANGED
@@ -100,7 +100,7 @@ def update_task_display(mode, iid, instances, background_df, mystery_file, cand1
100
  candidate_texts = [c1_txt, c2_txt, c3_txt]
101
 
102
  #create a dataframe of the task authors
103
- task_authors_df = instance_to_df(instances[iid])
104
  print(f"\n\n\n ----> Loaded task {iid} with {len(task_authors_df)} authors\n\n\n")
105
  print(task_authors_df)
106
  else:
@@ -139,9 +139,10 @@ def update_task_display(mode, iid, instances, background_df, mystery_file, cand1
139
 
140
  print(background_df.columns)
141
 
142
- # Computing predicted author by checking pairwise cosine similarity over luar embeddings
143
- col_name = f'{model_name.split("/")[-1]}_style_embedding'
144
- predicted_author = compute_predicted_author(task_authors_df, col_name)
 
145
 
146
  #generating html for the task
147
  header_html, mystery_html, candidate_htmls = task_HTML(mystery_txt, candidate_texts, predicted_author, ground_truth_author)
 
100
  candidate_texts = [c1_txt, c2_txt, c3_txt]
101
 
102
  #create a dataframe of the task authors
103
+ task_authors_df = instance_to_df(instances[iid], predicted_author=predicted_author, ground_truth_author=ground_truth_author)
104
  print(f"\n\n\n ----> Loaded task {iid} with {len(task_authors_df)} authors\n\n\n")
105
  print(task_authors_df)
106
  else:
 
139
 
140
  print(background_df.columns)
141
 
142
+ if mode != "Predefined HRS Task":
143
+ # Computing predicted author by checking pairwise cosine similarity over luar embeddings
144
+ col_name = f'{model_name.split("/")[-1]}_style_embedding'
145
+ predicted_author = compute_predicted_author(task_authors_df, col_name)
146
 
147
  #generating html for the task
148
  header_html, mystery_html, candidate_htmls = task_HTML(mystery_txt, candidate_texts, predicted_author, ground_truth_author)
utils/visualizations.py CHANGED
@@ -290,7 +290,7 @@ def handle_zoom_with_retries(event_json, bg_proj, bg_lbls, clustered_authors_df,
290
 
291
  for attempt in range(3):
292
  try:
293
- return handle_zoom(event_json, bg_proj, bg_lbls, clustered_authors_df, task_authors_df)
294
  except Exception as e:
295
  print(f"[ERROR] Attempt {attempt + 1} failed: {e}")
296
  if attempt < 2:
 
290
 
291
  for attempt in range(3):
292
  try:
293
+ handle_zoom(event_json, bg_proj, bg_lbls, clustered_authors_df, task_authors_df)
294
  except Exception as e:
295
  print(f"[ERROR] Attempt {attempt + 1} failed: {e}")
296
  if attempt < 2: