Download scripts/run_evaluation.py from Ypeng12/FaceID: direct link, hf CLI and curl.
- Browser
- Download file 5.97 kB
-
https://huggingface.co/spaces/Ypeng12/FaceID/resolve/main/scripts/run_evaluation.py
- Command line
-
hf download hf://spaces/Ypeng12/FaceID/scripts/run_evaluation.py
-
curl -L -o run_evaluation.py https://huggingface.co/spaces/Ypeng12/FaceID/resolve/main/scripts/run_evaluation.py
5.97 kB
| import argparse | |
| import os | |
| import yaml | |
| import csv | |
| import json | |
| import numpy as np | |
| import sys | |
| sys.path.append(os.path.join(os.path.dirname(__file__), '..')) | |
| from src.embedding import FaceEmbedder | |
| from src.similarity import numpy_vectorized_cosine | |
| from src.evaluation import compute_metrics, get_confusion_matrix | |
| from src.tracking import Tracker | |
| def load_config(path): | |
| with open(path, 'r') as f: | |
| return yaml.safe_load(f) | |
| def read_pairs(csv_path): | |
| pairs = [] | |
| with open(csv_path, 'r') as f: | |
| reader = csv.DictReader(f) | |
| for row in reader: | |
| pairs.append({ | |
| 'left_path': row['left_path'], | |
| 'right_path': row['right_path'], | |
| 'label': int(row['label']) | |
| }) | |
| return pairs | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Evaluate face verification pipeline.") | |
| parser.add_argument("--config", type=str, required=True, help="Path to evaluation config") | |
| args = parser.parse_args() | |
| config = load_config(args.config) | |
| os.makedirs(config.get("output_dir", "outputs/eval"), exist_ok=True) | |
| # Validation checks | |
| csv_path = config["pairs_file"] | |
| if not os.path.exists(csv_path): | |
| raise FileNotFoundError(f"Pairs file {csv_path} not found.") | |
| pairs = read_pairs(csv_path) | |
| if len(pairs) == 0: | |
| raise ValueError("Pairs file is empty.") | |
| labels = [p['label'] for p in pairs] | |
| if set(labels) - {0, 1}: | |
| raise ValueError("Labels must be strictly 0 or 1.") | |
| print(f"Loaded {len(pairs)} pairs from {csv_path}") | |
| # Extract unique embeddings | |
| unique_paths = set() | |
| for p in pairs: | |
| unique_paths.add(p['left_path']) | |
| unique_paths.add(p['right_path']) | |
| unique_paths = sorted(unique_paths) | |
| for p in unique_paths: | |
| if not os.path.exists(p): | |
| raise FileNotFoundError(f"Image not found: {p}") | |
| print(f"Extracting embeddings for {len(unique_paths)} unique images...") | |
| embedder = FaceEmbedder() | |
| emb_matrix = embedder.batch_compute_embeddings(unique_paths, batch_size=64) | |
| path_to_emb = {p: emb_matrix[i] for i, p in enumerate(unique_paths)} | |
| # Compute similarity scores | |
| left_embs = np.array([path_to_emb[p['left_path']] for p in pairs]) | |
| right_embs = np.array([path_to_emb[p['right_path']] for p in pairs]) | |
| print("Computing cosine similarity scores...") | |
| scores = numpy_vectorized_cosine(left_embs, right_embs) | |
| target_labels = np.array(labels) | |
| if len(scores) != len(pairs): | |
| raise ValueError(f"Score count ({len(scores)}) does not match pair count ({len(pairs)}).") | |
| tracker = Tracker() | |
| if config.get("is_sweep", False): | |
| print("Running threshold sweep [0.0 to 1.0]...") | |
| thresholds = np.linspace(0.0, 1.0, 101) | |
| sweep_data = [] | |
| best_f1 = -1 | |
| best_th = 0 | |
| for th in thresholds: | |
| m = compute_metrics(scores, target_labels, th, score_is_distance=False) | |
| # Must handle NaN f1 cases cleanly if possible, here handled in evaluation logic | |
| sweep_data.append({"threshold": th, "tpr": m["tpr"], "fpr": m["fpr"], "f1": m["f1"]}) | |
| if m["f1"] > best_f1: | |
| best_f1 = m["f1"] | |
| best_th = th | |
| metrics = {"best_threshold": float(best_th), "best_f1": float(best_f1)} | |
| print(f"Sweep complete. Best F1: {best_f1:.4f} at threshold {best_th:.4f}") | |
| sweep_file = os.path.join(config["output_dir"], f"sweep_{config['run_name']}.json") | |
| with open(sweep_file, 'w') as f: | |
| json.dump(sweep_data, f, indent=2) | |
| print(f"Sweep data saved to {sweep_file}") | |
| selected_threshold_file = os.path.join( | |
| config["output_dir"], | |
| f"selected_threshold_{config['run_name']}.json", | |
| ) | |
| with open(selected_threshold_file, 'w') as f: | |
| json.dump( | |
| { | |
| "run_name": config['run_name'], | |
| "best_threshold": float(best_th), | |
| "best_f1": float(best_f1), | |
| "score_is_distance": bool(config.get("score_is_distance", False)), | |
| }, | |
| f, | |
| indent=2, | |
| ) | |
| print(f"Selected threshold saved to {selected_threshold_file}") | |
| tracker.log_run(config['run_name'], args.config, config['data_version'], True, float(best_th), metrics, config['note']) | |
| else: | |
| th = config["threshold"] | |
| print(f"Evaluating at chosen threshold {th}...") | |
| metrics = compute_metrics(scores, target_labels, th, score_is_distance=False) | |
| cm = get_confusion_matrix(scores, target_labels, th, score_is_distance=False) | |
| print("Metrics:", metrics) | |
| print("Confusion Matrix:", cm) | |
| preds = (scores >= th).astype(int) | |
| # Slices | |
| false_positives = [ | |
| {"left": pairs[i]['left_path'], "right": pairs[i]['right_path'], "score": float(scores[i])} | |
| for i in range(len(pairs)) if preds[i] == 1 and target_labels[i] == 0 | |
| ] | |
| false_negatives = [ | |
| {"left": pairs[i]['left_path'], "right": pairs[i]['right_path'], "score": float(scores[i])} | |
| for i in range(len(pairs)) if preds[i] == 0 and target_labels[i] == 1 | |
| ] | |
| error_file = os.path.join(config["output_dir"], f"errors_{config['run_name']}.json") | |
| with open(error_file, 'w') as f: | |
| json.dump({"false_positives": false_positives, "false_negatives": false_negatives}, f, indent=2) | |
| print(f"Error slices saved to {error_file}") | |
| metrics['cm'] = cm | |
| tracker.log_run(config['run_name'], args.config, config['data_version'], False, float(th), metrics, config['note']) | |
| print("Run logged successfully.") | |
| if __name__ == "__main__": | |
| main() | |