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:
|