Milad Alshomary
commited on
Commit
·
ac7facf
1
Parent(s):
e392716
updates
Browse files- app.py +1 -1
- config/config.yaml +3 -3
- utils/gram2vec_feat_utils.py +6 -6
- utils/interp_space_utils.py +68 -18
- utils/llm_feat_utils.py +1 -0
- utils/ui.py +5 -4
- utils/visualizations.py +1 -1
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'][:
|
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/
|
4 |
-
interp_space_path: "./datasets/
|
5 |
-
interp_space_url: "https://huggingface.co/datasets/miladalsh/explanation_tool_files/resolve/main/
|
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=
|
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)
|
230 |
elif int(id) == predicted_author:
|
231 |
-
return f"Candidate {int(id)
|
232 |
elif int(id) == ground_truth_author:
|
233 |
-
return f"Candidate {int(id)
|
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 |
|
|
|
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 =
|
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 |
-
|
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 |
-
|
545 |
-
|
546 |
-
|
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 |
-
|
143 |
-
|
144 |
-
|
|
|
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 |
-
|
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:
|