""" UTILS FILE """ import random import json import numpy as np import matplotlib.pyplot as plt import pandas as pd import pickle import os import mne from surprise import Dataset, Reader, SVD, accuracy, KNNBasic, KNNWithMeans, KNNWithZScore from surprise.model_selection import train_test_split from sklearn.utils import resample from sklearn.metrics import mean_absolute_error from sklearn.metrics import mean_squared_error from scipy import stats import math import altair as alt import matplotlib.pyplot as plt import time from sentence_transformers import SentenceTransformer, util import torch from bertopic import BERTopic from datetime import date ######################################## # PRE-LOADING YOUR_COLOR = '#6CADFD' OTHER_USERS_COLOR = '#ccc' BINS = [0, 0.5, 1.5, 2.5, 3.5, 4] BIN_LABELS = ['0: Not at all toxic', '1: Slightly toxic', '2: Moderately toxic', '3: Very toxic', '4: Extremely toxic'] TOXIC_THRESHOLD = 2.0 alt.renderers.enable('altair_saver', fmts=['vega-lite', 'png']) # Data-loading module_dir = "./" with open(os.path.join(module_dir, "data/input/ids_to_comments.pkl"), "rb") as f: ids_to_comments = pickle.load(f) with open(os.path.join(module_dir, "data/input/comments_to_ids.pkl"), "rb") as f: comments_to_ids = pickle.load(f) system_preds_df = pd.read_pickle("data/input/system_preds_df.pkl") sys_eval_df = pd.read_pickle(os.path.join(module_dir, "data/input/split_data/sys_eval_df.pkl")) train_df = pd.read_pickle(os.path.join(module_dir, "data/input/split_data/train_df.pkl")) train_df_ids = train_df["item_id"].unique().tolist() model_eval_df = pd.read_pickle(os.path.join(module_dir, "data/input/split_data/model_eval_df.pkl")) ratings_df_full = pd.read_pickle(os.path.join(module_dir, "data/input/ratings_df_full.pkl")) worker_info_df = pd.read_pickle("./data/input/worker_info_df.pkl") topic_ids = system_preds_df.topic_id topics = system_preds_df.topic topic_ids_to_topics = {topic_ids[i]: topics[i] for i in range(len(topic_ids))} topics_to_topic_ids = {topics[i]: topic_ids[i] for i in range(len(topic_ids))} unique_topics_ids = sorted(system_preds_df.topic_id.unique()) unique_topics = [topic_ids_to_topics[topic_id] for topic_id in range(len(topic_ids_to_topics) - 1)] def get_toxic_threshold(): return TOXIC_THRESHOLD def get_user_model_names(user): # Fetch the user's models output_dir = f"./data/output" users = [name for name in os.listdir(output_dir) if os.path.isdir(os.path.join(output_dir, name))] if user not in users: # User does not exist return [] else: # Fetch trained model names for the user user_dir = f"./data/output/{user}" user_models = [name for name in os.listdir(user_dir) if os.path.isdir(os.path.join(user_dir, name))] user_models.sort() return user_models def get_unique_topics(): return unique_topics def get_large_clusters(min_n): counts_df = system_preds_df.groupby(by=["topic_id"]).size().reset_index(name='counts') counts_df = counts_df[counts_df["counts"] >= min_n] return [topic_ids_to_topics[t_id] for t_id in sorted(counts_df["topic_id"].tolist()[1:])] def get_ids_to_comments(): return ids_to_comments def get_workers_in_group(sel_gender, sel_race, sel_relig, sel_pol, sel_lgbtq): df = worker_info_df.copy() if sel_gender != "null": df = df[df["gender"] == sel_gender] if sel_relig != "null": df = df[df["religion_important"] == sel_relig] if sel_pol != "null": df = df[df["political_affilation"] == sel_pol] if sel_lgbtq != "null": if sel_lgbtq == "LGBTQ+": df = df[(df["lgbtq_status"] == "Homosexual") | (df["lgbtq_status"] == "Bisexual")] else: df = df[df["lgbtq_status"] == "Heterosexual"] if sel_race != "": df = df.dropna(subset=['race']) for r in sel_race: # Filter to rows with the indicated race df = df[df["race"].str.contains(r)] return df, len(df) readable_to_internal = { "Mean Absolute Error (MAE)": "MAE", "Root Mean Squared Error (RMSE)": "RMSE", "Mean Squared Error (MSE)": "MSE", "Average rating difference": "avg_diff", "Topic": "topic", "Toxicity Category": "toxicity_category", "Toxicity Severity": "toxicity_severity", } internal_to_readable = {v: k for k, v in readable_to_internal.items()} ######################################## # Data storage helper functions # Set up all directories for new user def setup_user_dirs(cur_user): user_dir = f"./data/output/{cur_user}" if not os.path.isdir(user_dir): os.mkdir(user_dir) def setup_model_dirs(cur_user, cur_model): model_dir = f"./data/output/{cur_user}/{cur_model}" if not os.path.isdir(model_dir): os.mkdir(model_dir) # Set up model dir # Set up subdirs os.mkdir(os.path.join(model_dir, "labels")) os.mkdir(os.path.join(model_dir, "perf")) def setup_user_model_dirs(cur_user, cur_model): setup_user_dirs(cur_user) setup_model_dirs(cur_user, cur_model) # Charts def get_chart_file(cur_user, cur_model): chart_dir = f"./data/output/{cur_user}/{cur_model}" return os.path.join(chart_dir, f"chart_overall_vis.json") # Labels def get_label_dir(cur_user, cur_model): return f"./data/output/{cur_user}/{cur_model}/labels" def get_n_label_files(cur_user, cur_model): label_dir = get_label_dir(cur_user, cur_model) return len([name for name in os.listdir(label_dir) if os.path.isfile(os.path.join(label_dir, name))]) def get_label_file(cur_user, cur_model, label_i=None): if label_i is None: # Get index to add on to end of list label_i = get_n_label_files(cur_user, cur_model) label_dir = get_label_dir(cur_user, cur_model) return os.path.join(label_dir, f"{label_i}.pkl") # Performance def get_perf_dir(cur_user, cur_model): return f"./data/output/{cur_user}/{cur_model}/perf" def get_n_perf_files(cur_user, cur_model): perf_dir = get_perf_dir(cur_user, cur_model) return len([name for name in os.listdir(perf_dir) if os.path.isfile(os.path.join(perf_dir, name))]) def get_perf_file(cur_user, cur_model, perf_i=None): if perf_i is None: # Get index to add on to end of list perf_i = get_n_perf_files(cur_user, cur_model) perf_dir = get_perf_dir(cur_user, cur_model) return os.path.join(perf_dir, f"{perf_i}.pkl") # Predictions dataframe def get_preds_file(cur_user, cur_model): preds_dir = f"./data/output/{cur_user}/{cur_model}" return os.path.join(preds_dir, f"preds_df.pkl") # Reports def get_reports_file(cur_user, cur_model): return f"./data/output/{cur_user}/{cur_model}/reports.json" ######################################## # General utils def get_metric_ind(metric): if metric == "MAE": ind = 0 elif metric == "MSE": ind = 1 elif metric == "RMSE": ind = 2 elif metric == "avg_diff": ind = 3 return ind def my_bootstrap(vals, n_boot, alpha): bs_samples = [] sample_size = len(vals) for i in range(n_boot): samp = resample(vals, n_samples=sample_size) bs_samples.append(np.median(samp)) p = ((1.0 - alpha) / 2.0) * 100 ci_low = np.percentile(bs_samples, p) p = (alpha + ((1.0 - alpha) / 2.0)) * 100 ci_high = np.percentile(bs_samples, p) return bs_samples, (ci_low, ci_high) ######################################## # GET_AUDIT utils def plot_metric_histogram(metric, user_metric, other_metric_vals, n_bins=10): hist, bin_edges = np.histogram(other_metric_vals, bins=n_bins, density=False) data = pd.DataFrame({ "bin_min": bin_edges[:-1], "bin_max": bin_edges[1:], "bin_count": hist, "user_metric": [user_metric for i in range(len(hist))] }) base = alt.Chart(data) bar = base.mark_bar(color=OTHER_USERS_COLOR).encode( x=alt.X("bin_min", bin="binned", title=internal_to_readable[metric]), x2='bin_max', y=alt.Y("bin_count", title="Number of users"), tooltip=[ alt.Tooltip('bin_min', title=f'{metric} bin min', format=".2f"), alt.Tooltip('bin_max', title=f'{metric} bin max', format=".2f"), alt.Tooltip('bin_count', title=f'Number of OTHER users', format=","), ] ) rule = base.mark_rule(color=YOUR_COLOR).encode( x = "mean(user_metric):Q", size=alt.value(2), tooltip=[ alt.Tooltip('mean(user_metric)', title=f'{metric} with YOUR labels', format=".2f"), ] ) return (bar + rule).interactive() # Generates the summary plot across all topics for the user def show_overall_perf(cur_model, error_type, cur_user, threshold=TOXIC_THRESHOLD, topic_vis_method="median", use_cache=True): # Your perf (calculate using model and testset) preds_file = get_preds_file(cur_user, cur_model) with open(preds_file, "rb") as f: preds_df = pickle.load(f) chart_file = get_chart_file(cur_user, cur_model) if use_cache and os.path.isfile(chart_file): # Read from file if it exists with open(chart_file, "r") as f: topic_overview_plot_json = json.load(f) else: # Otherwise, generate chart and save to file if topic_vis_method == "median": # Default preds_df_grp = preds_df.groupby(["topic", "user_id"]).median() elif topic_vis_method == "mean": preds_df_grp = preds_df.groupby(["topic", "user_id"]).mean() topic_overview_plot_json = plot_overall_vis(preds_df=preds_df_grp, n_topics=200, threshold=threshold, error_type=error_type, cur_user=cur_user, cur_model=cur_model) # Save to file with open(chart_file, "w") as f: json.dump(topic_overview_plot_json, f) return { "topic_overview_plot_json": json.loads(topic_overview_plot_json), } ######################################## # GET_LABELING utils def create_example_sets(n_label_per_bin, score_bins, keyword=None, topic=None): # Restrict to the keyword, if provided df = system_preds_df.copy() if keyword != None: df = df[df["comment"].str.contains(keyword)] if topic != None: df = df[df["topic"] == topic] # Try to choose n values from each provided score bin ex_to_label = [] bin_names = [] bin_label_counts = [] for i, score_bin in enumerate(score_bins): min_score, max_score = score_bin cur_df = df[(df["rating"] >= min_score) & (df["rating"] < max_score) & (df["item_id"].isin(train_df_ids))] # sample rows for label comment_ids = cur_df.item_id.tolist() cur_n_label_per_bin = n_label_per_bin[i] cap = min(len(comment_ids), (cur_n_label_per_bin)) to_label = np.random.choice(comment_ids, cap, replace=False) ex_to_label.extend(to_label) bin_names.append(f"[{min_score}, {max_score})") bin_label_counts.append(len(to_label)) return ex_to_label def get_grp_model_labels(n_label_per_bin, score_bins, grp_ids): df = system_preds_df.copy() train_df_grp = train_df[train_df["user_id"].isin(grp_ids)] train_df_grp_avg = train_df_grp.groupby(by=["item_id"]).median().reset_index() train_df_grp_avg_ids = train_df_grp_avg["item_id"].tolist() ex_to_label = [] # IDs of comments to use for group model training for i, score_bin in enumerate(score_bins): min_score, max_score = score_bin # get eligible comments to sample cur_df = df[(df["rating"] >= min_score) & (df["rating"] < max_score) & (df["item_id"].isin(train_df_grp_avg_ids))] comment_ids = cur_df.item_id.unique().tolist() # sample comments cur_n_label_per_bin = n_label_per_bin[i] cap = min(len(comment_ids), (cur_n_label_per_bin)) to_label = np.random.choice(comment_ids, cap, replace=False) ex_to_label.extend((to_label)) train_df_grp_avg = train_df_grp_avg[train_df_grp_avg["item_id"].isin(ex_to_label)] ratings_grp = {ids_to_comments[int(r["item_id"])]: r["rating"] for _, r in train_df_grp_avg.iterrows()} return ratings_grp ######################################## # SAVE_REPORT utils # Convert the SEP field selection from the UI to the SEP enum value def get_sep_enum(sep_selection): if sep_selection == "Adversarial Example": return "S0403: Adversarial Example" elif sep_selection == "Accuracy": return "P0204: Accuracy" elif sep_selection == "Bias/Discrimination": return "E0100: Bias/ Discrimination" else: return "P0200: Model issues" # Format the description for the report including the provided title, error type, and text entry field ("Summary/Suggestions" text box) def format_description(indie_label_json): title = indie_label_json["title"] error_type = indie_label_json["error_type"] text_entry = indie_label_json["text_entry"] return f"Title: {title}\nError Type: {error_type}\nSummary/Suggestions: {text_entry}" # Format the credit field for the report with the current user's username, optional name, and optional email address def format_credit(cur_user, name, email): return f"Username: {cur_user}, Name: {name}, Email: {email}" # Convert indielabel json to AVID json format. # See the AVID format in https://avidml.org/avidtools/reference/report # # Important mappings: # IndieLabel Attribute AVID Attribute Example # text_entry description "I think the Perspective API # is too sensitive. Here are some examples." # topic feature 0_shes_woman_lady_face # persp_score model_score 0.94 # comment ori_input "She looks beautiful" # user_rating personal_model_score 0.92 # user_decision user_decision "Non-toxic" # Note that this is at the individual report level. def convert_indie_label_json_to_avid_json(indie_label_json, cur_user, name, email, sep_selection): # Setting up the structure with a dict to enable programmatic additions avid_json_dict = { "data_type": "AVID", "data_version": None, "metadata": None, "affects": { "developer": [], "deployer": [ "Hugging Face" ], # TODO: Make artifacts malleable during modularity work "artifacts": [ { "type": "Model", "name": "Perspective API" } ] }, "problemtype": { "classof": "Undefined", # I don't think any of the other ClassEnums are applicable. Link: https://avidml.org/avidtools/_modules/avidtools/datamodels/enums#ClassEnum "type": "Detection", "description": { "lang": "eng", # TODO: Make language selectable "value": "This report contains results from an end user audit conducted on Hugging Face." } }, "metrics": [ # Note: For the end users use case, I made each comment an example. ], "references": [], "description": { "lang": "eng", # TODO: Make language selectable "value": "" # Leaving empty so the report comments can be contained here. }, "impact": { "avid": { "risk_domain": [ "Ethics" ], "sep_view": [ "E0101: Group fairness" ], "lifecycle_view": [ "L05: Evaluation" ], "taxonomy_version": "0.2" } }, "credit": { "lang": "eng", # TODO: Make language selectable "value": "" # Leaving empty so that credit can be assigned }, "reported_date": "" # Leaving empty so that it can be dynamically filled in } avid_json_dict["description"]["value"] = format_description(indie_label_json) avid_json_dict["reported_date"] = str(date.today()) avid_json_dict["credit"]["value"] = format_credit(cur_user, name, email) sep_enum = get_sep_enum(sep_selection) avid_json_dict["impact"]["avid"]["sep_view"] = [sep_enum] for e in indie_label_json["evidence"]: curr_metric = {} curr_metric["name"] = "Perspective API" curr_metric["detection_method"] = { "type": "Detection", "name": "Individual Example from End User Audit" } res_dict = {} res_dict["feature"] = e["topic"] res_dict["model_score"] = str(e["persp_score"]) # Converted to string to avoid Float type error with DB res_dict["ori_input"] = e["comment"] res_dict["personal_model_score"] = str(e["user_rating"]) # See above res_dict["user_decision"] = e["user_decision"] curr_metric["results"] = res_dict avid_json_dict["metrics"].append(curr_metric) new_report = json.dumps(avid_json_dict) return new_report ######################################## # GET_PERSONALIZED_MODEL utils def fetch_existing_data(user, model_name): # Check if we have cached model performance n_perf_files = get_n_perf_files(user, model_name) if n_perf_files > 0: # Fetch cached results perf_file = get_perf_file(user, model_name, n_perf_files - 1) # Get last performance file with open(perf_file, "rb") as f: mae, mse, rmse, avg_diff = pickle.load(f) else: raise Exception(f"Model {model_name} does not exist") # Fetch previous user-provided labels ratings_prev = None n_label_files = get_n_label_files(user, model_name) if n_label_files > 0: label_file = get_label_file(user, model_name, n_label_files - 1) # Get last label file with open(label_file, "rb") as f: ratings_prev = pickle.load(f) return mae, mse, rmse, avg_diff, ratings_prev # Main function called by server's `get_personalized_model` endpoint # Trains an updated model with the specified name, user, and ratings # Saves ratings, performance metrics, and pre-computed predictions to files # - model_name: name of the model to train # - ratings: dictionary of comments to ratings # - user: user name # - top_n: number of comments to train on (used when a set was held out for original user study) # - topic: topic to train on (used when tuning for a specific topic) def train_updated_model(model_name, ratings, user, top_n=None, topic=None, debug=False): # Check if there is previously-labeled data; if so, combine it with this data labeled_df = format_labeled_data(ratings, worker_id=user) # Treat ratings as full batch of all ratings ratings_prev = None # Filter out rows with "unsure" (-1) labeled_df = labeled_df[labeled_df["rating"] != -1] # Filter to top N for user study if (topic is None) and (top_n is not None): labeled_df = labeled_df.head(top_n) else: # For topic tuning, need to fetch old labels n_label_files = get_n_label_files(user, model_name) if n_label_files > 0: # Concatenate previous set of labels with this new batch of labels label_file = get_label_file(user, model_name, n_label_files - 1) # Get last label file with open(label_file, "rb") as f: ratings_prev = pickle.load(f) labeled_df_prev = format_labeled_data(ratings_prev, worker_id=user) labeled_df_prev = labeled_df_prev[labeled_df_prev["rating"] != -1] ratings.update(ratings_prev) # append old ratings to ratings labeled_df = pd.concat([labeled_df_prev, labeled_df]) if debug: print("len ratings for training:", len(labeled_df)) # Save this batch of labels label_file = get_label_file(user, model_name) with open(label_file, "wb") as f: pickle.dump(ratings, f) # Train model cur_model, _, _, _ = train_user_model(ratings_df=labeled_df) # Compute performance metrics mae, mse, rmse, avg_diff = users_perf(cur_model, worker_id=user) # Save performance metrics perf_file = get_perf_file(user, model_name) with open(perf_file, "wb") as f: pickle.dump((mae, mse, rmse, avg_diff), f) # Pre-compute predictions for full dataset cur_preds_df = get_preds_df(cur_model, [user], sys_eval_df=ratings_df_full) # Save pre-computed predictions preds_file = get_preds_file(user, model_name) with open(preds_file, "wb") as f: pickle.dump(cur_preds_df, f) # Replace cached summary plot if it exists show_overall_perf(cur_model=model_name, error_type="Both", cur_user=user, use_cache=False) ratings_prev = ratings return mae, mse, rmse, avg_diff, ratings_prev def format_labeled_data(ratings, worker_id): all_rows = [] for comment, rating in ratings.items(): comment_id = comments_to_ids[comment] row = [worker_id, comment_id, int(rating)] all_rows.append(row) df = pd.DataFrame(all_rows, columns=["user_id", "item_id", "rating"]) return df def users_perf(model, worker_id, sys_eval_df=sys_eval_df): # Load the full empty dataset sys_eval_comment_ids = sys_eval_df.item_id.unique().tolist() empty_ratings_rows = [[worker_id, c_id, 0] for c_id in sys_eval_comment_ids] empty_ratings_df = pd.DataFrame(empty_ratings_rows, columns=["user_id", "item_id", "rating"]) # Compute predictions for full dataset reader = Reader(rating_scale=(0, 4)) eval_set_data = Dataset.load_from_df(empty_ratings_df, reader) _, testset = train_test_split(eval_set_data, test_size=1.) predictions = model.test(testset) df = empty_ratings_df # user_id, item_id, rating user_item_preds = get_predictions_by_user_and_item(predictions) df["pred"] = df.apply(lambda row: user_item_preds[(row.user_id, row.item_id)] if (row.user_id, row.item_id) in user_item_preds else np.nan, axis=1) df = df.merge(system_preds_df, on="item_id", how="left", suffixes=('', '_sys')) df.dropna(subset = ["pred"], inplace=True) df["rating"] = df.rating.astype("int32") perf_metrics = get_overall_perf(df, worker_id) # mae, mse, rmse, avg_diff return perf_metrics def get_overall_perf(preds_df, user_id): # Prepare dataset to calculate performance y_pred = preds_df[preds_df["user_id"] == user_id].rating_sys.to_numpy() # system's prediction y_true = preds_df[preds_df["user_id"] == user_id].pred.to_numpy() # user's (predicted) ground truth # Get performance for user's model mae = mean_absolute_error(y_true, y_pred) mse = mean_squared_error(y_true, y_pred) rmse = mean_squared_error(y_true, y_pred, squared=False) avg_diff = np.mean(y_true - y_pred) return mae, mse, rmse, avg_diff def get_predictions_by_user_and_item(predictions): user_item_preds = {} for uid, iid, true_r, est, _ in predictions: user_item_preds[(uid, iid)] = est return user_item_preds # Pre-computes predictions for the provided model and specified users on the system-eval dataset # - model: trained model # - user_ids: list of user IDs to compute predictions for # - sys_eval_df: dataframe of system eval labels (pre-computed) def get_preds_df(model, user_ids, sys_eval_df=sys_eval_df, bins=BINS, debug=False): # Prep dataframe for all predictions we'd like to request start = time.time() sys_eval_comment_ids = sys_eval_df.item_id.unique().tolist() empty_ratings_rows = [] for user_id in user_ids: empty_ratings_rows.extend([[user_id, c_id, 0] for c_id in sys_eval_comment_ids]) empty_ratings_df = pd.DataFrame(empty_ratings_rows, columns=["user_id", "item_id", "rating"]) if debug: print("setup", time.time() - start) # Evaluate model to get predictions start = time.time() reader = Reader(rating_scale=(0, 4)) eval_set_data = Dataset.load_from_df(empty_ratings_df, reader) _, testset = train_test_split(eval_set_data, test_size=1.) predictions = model.test(testset) if debug: print("train_test_split", time.time() - start) # Update dataframe with predictions start = time.time() df = empty_ratings_df.copy() # user_id, item_id, rating user_item_preds = get_predictions_by_user_and_item(predictions) df["pred"] = df.apply(lambda row: user_item_preds[(row.user_id, row.item_id)] if (row.user_id, row.item_id) in user_item_preds else np.nan, axis=1) df = df.merge(system_preds_df, on="item_id", how="left", suffixes=('', '_sys')) df.dropna(subset = ["pred"], inplace=True) df["rating"] = df.rating.astype("int32") # Get binned predictions (based on user prediction) df["prediction_bin"], out_bins = pd.cut(df["pred"], bins, labels=False, retbins=True) df = df.sort_values(by=["item_id"]) return df # Given the full set of ratings, trains the specified model type and evaluates on the model eval set # - ratings_df: dataframe of all ratings # - train_df: dataframe of training labels # - model_eval_df: dataframe of model eval labels (validation set) # - train_frac: fraction of ratings to use for training def train_user_model(ratings_df, train_df=train_df, model_eval_df=model_eval_df, train_frac=0.75, model_type="SVD", sim_type=None, user_based=True): # Sample from shuffled labeled dataframe and add batch to train set; specified set size to model_eval set labeled = ratings_df.sample(frac=1) # Shuffle the data batch_size = math.floor(len(labeled) * train_frac) labeled_train = labeled[:batch_size] labeled_model_eval = labeled[batch_size:] train_df_ext = train_df.append(labeled_train) model_eval_df_ext = model_eval_df.append(labeled_model_eval) # Train model and show model eval set results model, perf = train_model(train_df_ext, model_eval_df_ext, model_type=model_type, sim_type=sim_type, user_based=user_based) return model, perf, labeled_train, labeled_model_eval # Given a set of labels split into training and validation (model_eval), trains the specified model type on the training labels and evaluates on the model_eval labels # - train_df: dataframe of training labels # - model_eval_df: dataframe of model eval labels (validation set) # - model_type: type of model to train def train_model(train_df, model_eval_df, model_type="SVD", sim_type=None, user_based=True, debug=False): # Train model reader = Reader(rating_scale=(0, 4)) train_data = Dataset.load_from_df(train_df, reader) model_eval_data = Dataset.load_from_df(model_eval_df, reader) train_set = train_data.build_full_trainset() _, model_eval_set = train_test_split(model_eval_data, test_size=1.) sim_options = { "name": sim_type, "user_based": user_based, # compute similarity between users or items } if model_type == "SVD": algo = SVD() # SVD doesn't have similarity metric elif model_type == "KNNBasic": algo = KNNBasic(sim_options=sim_options) elif model_type == "KNNWithMeans": algo = KNNWithMeans(sim_options=sim_options) elif model_type == "KNNWithZScore": algo = KNNWithZScore(sim_options=sim_options) algo.fit(train_set) predictions = algo.test(model_eval_set) rmse = accuracy.rmse(predictions) fcp = accuracy.fcp(predictions) mae = accuracy.mae(predictions) mse = accuracy.mse(predictions) if debug: print(f"MAE: {mae}, MSE: {mse}, RMSE: {rmse}, FCP: {fcp}") perf = [mae, mse, rmse, fcp] return algo, perf def plot_train_perf_results(user, model_name, mae): n_perf_files = get_n_perf_files(user, model_name) all_rows = [] for i in range(n_perf_files): perf_file = get_perf_file(user, model_name, i) with open(perf_file, "rb") as f: mae, mse, rmse, avg_diff = pickle.load(f) all_rows.append([i, mae, "Your MAE"]) df = pd.DataFrame(all_rows, columns=["version", "perf", "metric"]) chart = alt.Chart(df).mark_line(point=True).encode( x="version:O", y="perf", color=alt.Color("metric", title="Performance metric"), tooltip=[ alt.Tooltip('version:O', title='Version'), alt.Tooltip('metric:N', title="Metric"), alt.Tooltip('perf:Q', title="Metric Value", format=".3f"), ], ).properties( title=f"Performance over model versions: {model_name}", width=500, ) # Manually set for now mae_good = 1.0 mae_okay = 1.2 plot_dim_width = 500 domain_min = 0.0 domain_max = 2.0 bkgd = alt.Chart(pd.DataFrame({ "start": [mae_okay, mae_good, domain_min], "stop": [domain_max, mae_okay, mae_good], "bkgd": ["Needs improvement", "Okay", "Good"], })).mark_rect(opacity=0.2).encode( y=alt.Y("start:Q", scale=alt.Scale(domain=[0, domain_max]), title=""), y2=alt.Y2("stop:Q", title="Performance (MAE)"), x=alt.value(0), x2=alt.value(plot_dim_width), color=alt.Color("bkgd:O", scale=alt.Scale( domain=["Needs improvement", "Okay", "Good"], range=["red", "yellow", "green"]), title="How good is your MAE?" ) ) plot = (bkgd + chart).properties(width=plot_dim_width).resolve_scale(color='independent') mae_status = None if mae < mae_good: mae_status = "Your MAE is in the Good range. Your model looks ready to go." elif mae < mae_okay: mae_status = "Your MAE is in the Okay range. Your model can be used, but you can provide additional labels to improve it." else: mae_status = "Your MAE is in the Needs improvement range. Your model may need additional labels to improve." return plot, mae_status ######################################## # New visualizations # Constants VIS_BINS = np.round(np.arange(0, 4.01, 0.05), 3) VIS_BINS_LABELS = [np.round(np.mean([x, y]), 3) for x, y in zip(VIS_BINS[:-1], VIS_BINS[1:])] def get_key(sys, user, threshold): if sys <= threshold and user <= threshold: return "System agrees: Non-toxic" elif sys > threshold and user > threshold: return "System agrees: Toxic" else: if abs(sys - threshold) > 1.5: return "System differs: Error > 1.5" elif abs(sys - threshold) > 1.0: return "System differs: Error > 1.0" elif abs(sys - threshold) > 0.5: return "System differs: Error > 0.5" else: return "System differs: Error <=0.5" def get_key_no_model(sys, threshold): if sys <= threshold: return "System says: Non-toxic" else: return "System says: Toxic" def get_user_color(user, threshold): if user <= threshold: return "#FFF" # white else: return "#808080" # grey def get_system_color(sys, user, threshold): if sys <= threshold and user <= threshold: return "#FFF" # white elif sys > threshold and user > threshold: return "#808080" # grey else: if abs(sys - threshold) > 1.5: return "#d62728" # red elif abs(sys - threshold) > 1.0: return "#ff7a5c" # med red elif abs(sys - threshold) > 0.5: return "#ffa894" # light red else: return "#ffd1c7" # very light red def get_error_type(sys, user, threshold): if sys <= threshold and user <= threshold: return "No error (agree non-toxic)" elif sys > threshold and user > threshold: return "No error (agree toxic)" elif sys <= threshold and user > threshold: return "System may be under-sensitive" elif sys > threshold and user <= threshold: return "System may be over-sensitive" def get_error_type_radio(sys, user, threshold): if sys <= threshold and user <= threshold: return "Show errors and non-errors" elif sys > threshold and user > threshold: return "Show errors and non-errors" elif sys <= threshold and user > threshold: return "System is under-sensitive" elif sys > threshold and user <= threshold: return "System is over-sensitive" def get_error_magnitude(sys, user, threshold): if sys <= threshold and user <= threshold: return 0 # no classification error elif sys > threshold and user > threshold: return 0 # no classification error elif sys <= threshold and user > threshold: return abs(sys - user) elif sys > threshold and user <= threshold: return abs(sys - user) def get_error_size(sys, user, threshold): if sys <= threshold and user <= threshold: return 0 # no classification error elif sys > threshold and user > threshold: return 0 # no classification error elif sys <= threshold and user > threshold: return sys - user elif sys > threshold and user <= threshold: return sys - user def get_decision(rating, threshold): if rating <= threshold: return "Non-toxic" else: return "Toxic" def get_category(row, threshold=0.3): k_to_category = { "is_profane_frac": "Profanity", "is_threat_frac": "Threat", "is_identity_attack_frac": "Identity Attack", "is_insult_frac": "Insult", "is_sexual_harassment_frac": "Sexual Harassment", } categories = [] for k in ["is_profane_frac", "is_threat_frac", "is_identity_attack_frac", "is_insult_frac", "is_sexual_harassment_frac"]: if row[k] > threshold: categories.append(k_to_category[k]) if len(categories) > 0: return ", ".join(categories) else: return "" def get_comment_url(row): return f"#{row['item_id']}/#comment" def get_topic_url(row): return f"#{row['topic']}/#topic" # Plots overall results histogram (each block is a topic) def plot_overall_vis(preds_df, error_type, cur_user, cur_model, n_topics=None, bins=VIS_BINS, threshold=TOXIC_THRESHOLD, sys_col="rating_sys"): df = preds_df.copy().reset_index() if n_topics is not None: df = df[df["topic_id"] < n_topics] df["vis_pred_bin"], out_bins = pd.cut(df["pred"], bins, labels=VIS_BINS_LABELS, retbins=True) df = df[df["user_id"] == cur_user].sort_values(by=["item_id"]).reset_index() df["system_label"] = [("toxic" if r > threshold else "non-toxic") for r in df[sys_col].tolist()] df["threshold"] = [threshold for r in df[sys_col].tolist()] df["key"] = [get_key(sys, user, threshold) for sys, user in zip(df[sys_col].tolist(), df["pred"].tolist())] df["url"] = df.apply(lambda row: get_topic_url(row), axis=1) # Plot sizing domain_min = 0 domain_max = 4 plot_dim_height = 500 plot_dim_width = 750 max_items = np.max(df["vis_pred_bin"].value_counts().tolist()) mark_size = np.round(plot_dim_height / max_items) * 8 if mark_size > 75: mark_size = 75 plot_dim_height = 13 * max_items # Main chart chart = alt.Chart(df).mark_square(opacity=0.8, size=mark_size, stroke="grey", strokeWidth=0.5).transform_window( groupby=['vis_pred_bin'], sort=[{'field': sys_col}], id='row_number()', ignorePeers=True, ).encode( x=alt.X('vis_pred_bin:Q', title="Our prediction of your rating", scale=alt.Scale(domain=(domain_min, domain_max))), y=alt.Y('id:O', title="Topics (ordered by System toxicity rating)", axis=alt.Axis(values=list(range(0, max_items, 5))), sort='descending'), color = alt.Color("key:O", scale=alt.Scale( domain=["System agrees: Non-toxic", "System agrees: Toxic", "System differs: Error > 1.5", "System differs: Error > 1.0", "System differs: Error > 0.5", "System differs: Error <=0.5"], range=["white", "#cbcbcb", "red", "#ff7a5c", "#ffa894", "#ffd1c7"]), title="System rating (box color)" ), href="url:N", tooltip = [ alt.Tooltip("topic:N", title="Topic"), alt.Tooltip("system_label:N", title="System label"), alt.Tooltip(f"{sys_col}:Q", title="System rating", format=".2f"), alt.Tooltip("pred:Q", title="Your rating", format=".2f") ] ) # Filter to specified error type if error_type == "System is under-sensitive": # FN: system rates non-toxic, but user rates toxic chart = chart.transform_filter( alt.FieldGTPredicate(field="pred", gt=threshold) ) elif error_type == "System is over-sensitive": # FP: system rates toxic, but user rates non-toxic chart = chart.transform_filter( alt.FieldLTEPredicate(field="pred", lte=threshold) ) # Threshold line rule = alt.Chart(pd.DataFrame({ "threshold": [threshold], "System threshold": [f"Threshold = {threshold}"] })).mark_rule().encode( x=alt.X("mean(threshold):Q", scale=alt.Scale(domain=(domain_min, domain_max)), title=""), color=alt.Color("System threshold:N", scale=alt.Scale(domain=[f"Threshold = {threshold}"], range=["grey"])), size=alt.value(2), ) # Plot region annotations nontoxic_x = (domain_min + threshold) / 2. toxic_x = (domain_max + threshold) / 2. annotation = alt.Chart(pd.DataFrame({ "annotation_text": ["Non-toxic", "Toxic"], "x": [nontoxic_x, toxic_x], "y": [max_items, max_items], })).mark_text( align="center", baseline="middle", fontSize=16, dy=10, color="grey" ).encode( x=alt.X("x", title=""), y=alt.Y("y", title="", axis=None), text="annotation_text" ) # Plot region background colors bkgd = alt.Chart(pd.DataFrame({ "start": [domain_min, threshold], "stop": [threshold, domain_max], "bkgd": ["Non-toxic (L side)", "Toxic (R side)"], })).mark_rect(opacity=1.0, stroke="grey", strokeWidth=0.25).encode( x=alt.X("start:Q", scale=alt.Scale(domain=[domain_min, domain_max])), x2=alt.X2("stop:Q"), y=alt.value(0), y2=alt.value(plot_dim_height), color=alt.Color("bkgd:O", scale=alt.Scale( domain=["Non-toxic (L side)", "Toxic (R side)"], range=["white", "#cbcbcb"]), title="Your rating (background color)" ) ) plot = (bkgd + annotation + chart + rule).properties(height=(plot_dim_height), width=plot_dim_width).resolve_scale(color='independent').to_json() return plot # Plots cluster results histogram (each block is a comment), but *without* a model # as a point of reference (in contrast to plot_overall_vis_cluster) def plot_overall_vis_cluster_no_model(cur_user, preds_df, n_comments=None, bins=VIS_BINS, threshold=TOXIC_THRESHOLD, sys_col="rating_sys"): df = preds_df.copy().reset_index() df["vis_pred_bin"], out_bins = pd.cut(df[sys_col], bins, labels=VIS_BINS_LABELS, retbins=True) df = df[df["user_id"] == cur_user].sort_values(by=[sys_col]).reset_index() df["system_label"] = [("toxic" if r > threshold else "non-toxic") for r in df[sys_col].tolist()] df["key"] = [get_key_no_model(sys, threshold) for sys in df[sys_col].tolist()] df["category"] = df.apply(lambda row: get_category(row), axis=1) df["url"] = df.apply(lambda row: get_comment_url(row), axis=1) if n_comments is not None: n_to_sample = np.min([n_comments, len(df)]) df = df.sample(n=n_to_sample) # Plot sizing domain_min = 0 domain_max = 4 plot_dim_height = 500 plot_dim_width = 750 max_items = np.max(df["vis_pred_bin"].value_counts().tolist()) mark_size = np.round(plot_dim_height / max_items) * 8 if mark_size > 75: mark_size = 75 plot_dim_height = 13 * max_items # Main chart chart = alt.Chart(df).mark_square(opacity=0.8, size=mark_size, stroke="grey", strokeWidth=0.25).transform_window( groupby=['vis_pred_bin'], sort=[{'field': sys_col}], id='row_number()', ignorePeers=True ).encode( x=alt.X('vis_pred_bin:Q', title="System toxicity rating", scale=alt.Scale(domain=(domain_min, domain_max))), y=alt.Y('id:O', title="Comments (ordered by System toxicity rating)", axis=alt.Axis(values=list(range(0, max_items, 5))), sort='descending'), color = alt.Color("key:O", scale=alt.Scale( domain=["System says: Non-toxic", "System says: Toxic"], range=["white", "#cbcbcb"]), title="System rating", legend=None, ), href="url:N", tooltip = [ alt.Tooltip("comment:N", title="comment"), alt.Tooltip(f"{sys_col}:Q", title="System rating", format=".2f"), ] ) # Threshold line rule = alt.Chart(pd.DataFrame({ "threshold": [threshold], })).mark_rule(color='grey').encode( x=alt.X("mean(threshold):Q", scale=alt.Scale(domain=[domain_min, domain_max]), title=""), size=alt.value(2), ) # Plot region annotations nontoxic_x = (domain_min + threshold) / 2. toxic_x = (domain_max + threshold) / 2. annotation = alt.Chart(pd.DataFrame({ "annotation_text": ["Non-toxic", "Toxic"], "x": [nontoxic_x, toxic_x], "y": [max_items, max_items], })).mark_text( align="center", baseline="middle", fontSize=16, dy=10, color="grey" ).encode( x=alt.X("x", title=""), y=alt.Y("y", title="", axis=None), text="annotation_text" ) # Plot region background colors bkgd = alt.Chart(pd.DataFrame({ "start": [domain_min, threshold], "stop": [threshold, domain_max], "bkgd": ["Non-toxic", "Toxic"], })).mark_rect(opacity=1.0, stroke="grey", strokeWidth=0.25).encode( x=alt.X("start:Q", scale=alt.Scale(domain=[domain_min, domain_max])), x2=alt.X2("stop:Q"), y=alt.value(0), y2=alt.value(plot_dim_height), color=alt.Color("bkgd:O", scale=alt.Scale( domain=["Non-toxic", "Toxic"], range=["white", "#cbcbcb"]), title="System rating" ) ) final_plot = (bkgd + annotation + chart + rule).properties(height=(plot_dim_height), width=plot_dim_width).resolve_scale(color='independent').to_json() return final_plot, df # Plots cluster results histogram (each block is a comment) *with* a model as a point of reference def plot_overall_vis_cluster(cur_user, preds_df, error_type, n_comments=None, bins=VIS_BINS, threshold=TOXIC_THRESHOLD, sys_col="rating_sys"): df = preds_df.copy().reset_index() df["vis_pred_bin"], out_bins = pd.cut(df["pred"], bins, labels=VIS_BINS_LABELS, retbins=True) df = df[df["user_id"] == cur_user].sort_values(by=[sys_col]).reset_index(drop=True) df["system_label"] = [("toxic" if r > threshold else "non-toxic") for r in df[sys_col].tolist()] df["key"] = [get_key(sys, user, threshold) for sys, user in zip(df[sys_col].tolist(), df["pred"].tolist())] df["category"] = df.apply(lambda row: get_category(row), axis=1) df["url"] = df.apply(lambda row: get_comment_url(row), axis=1) if n_comments is not None: n_to_sample = np.min([n_comments, len(df)]) df = df.sample(n=n_to_sample) # Plot sizing domain_min = 0 domain_max = 4 plot_dim_height = 500 plot_dim_width = 750 max_items = np.max(df["vis_pred_bin"].value_counts().tolist()) mark_size = np.round(plot_dim_height / max_items) * 8 if mark_size > 75: mark_size = 75 plot_dim_height = 13 * max_items # Main chart chart = alt.Chart(df).mark_square(opacity=0.8, size=mark_size, stroke="grey", strokeWidth=0.25).transform_window( groupby=['vis_pred_bin'], sort=[{'field': sys_col}], id='row_number()', ignorePeers=True ).encode( x=alt.X('vis_pred_bin:Q', title="Our prediction of your rating", scale=alt.Scale(domain=(domain_min, domain_max))), y=alt.Y('id:O', title="Comments (ordered by System toxicity rating)", axis=alt.Axis(values=list(range(0, max_items, 5))), sort='descending'), color = alt.Color("key:O", scale=alt.Scale( domain=["System agrees: Non-toxic", "System agrees: Toxic", "System differs: Error > 1.5", "System differs: Error > 1.0", "System differs: Error > 0.5", "System differs: Error <=0.5"], range=["white", "#cbcbcb", "red", "#ff7a5c", "#ffa894", "#ffd1c7"]), title="System rating (box color)" ), href="url:N", tooltip = [ alt.Tooltip("comment:N", title="comment"), alt.Tooltip(f"{sys_col}:Q", title="System rating", format=".2f"), alt.Tooltip("pred:Q", title="Your rating", format=".2f"), alt.Tooltip("category:N", title="Potential toxicity categories") ] ) # Filter to specified error type if error_type == "System is under-sensitive": # FN: system rates non-toxic, but user rates toxic chart = chart.transform_filter( alt.FieldGTPredicate(field="pred", gt=threshold) ) elif error_type == "System is over-sensitive": # FP: system rates toxic, but user rates non-toxic chart = chart.transform_filter( alt.FieldLTEPredicate(field="pred", lte=threshold) ) # Threshold line rule = alt.Chart(pd.DataFrame({ "threshold": [threshold], })).mark_rule(color='grey').encode( x=alt.X("mean(threshold):Q", scale=alt.Scale(domain=[domain_min, domain_max]), title=""), size=alt.value(2), ) # Plot region annotations nontoxic_x = (domain_min + threshold) / 2. toxic_x = (domain_max + threshold) / 2. annotation = alt.Chart(pd.DataFrame({ "annotation_text": ["Non-toxic", "Toxic"], "x": [nontoxic_x, toxic_x], "y": [max_items, max_items], })).mark_text( align="center", baseline="middle", fontSize=16, dy=10, color="grey" ).encode( x=alt.X("x", title=""), y=alt.Y("y", title="", axis=None), text="annotation_text" ) # Plot region background colors bkgd = alt.Chart(pd.DataFrame({ "start": [domain_min, threshold], "stop": [threshold, domain_max], "bkgd": ["Non-toxic (L side)", "Toxic (R side)"], })).mark_rect(opacity=1.0, stroke="grey", strokeWidth=0.25).encode( x=alt.X("start:Q", scale=alt.Scale(domain=[domain_min, domain_max])), x2=alt.X2("stop:Q"), y=alt.value(0), y2=alt.value(plot_dim_height), color=alt.Color("bkgd:O", scale=alt.Scale( domain=["Non-toxic (L side)", "Toxic (R side)"], range=["white", "#cbcbcb"]), title="Your rating (background color)" ) ) final_plot = (bkgd + annotation + chart + rule).properties(height=(plot_dim_height), width=plot_dim_width).resolve_scale(color='independent').to_json() return final_plot, df def get_cluster_comments(df, error_type, threshold=TOXIC_THRESHOLD, sys_col="rating_sys", use_model=True, debug=False): df["user_color"] = [get_user_color(user, threshold) for user in df["pred"].tolist()] # get cell colors df["system_color"] = [get_user_color(sys, threshold) for sys in df[sys_col].tolist()] # get cell colors df["error_color"] = [get_system_color(sys, user, threshold) for sys, user in zip(df[sys_col].tolist(), df["pred"].tolist())] # get cell colors df["error_type"] = [get_error_type(sys, user, threshold) for sys, user in zip(df[sys_col].tolist(), df["pred"].tolist())] # get error type in words df["error_amt"] = [abs(sys - threshold) for sys in df[sys_col].tolist()] # get raw error df["judgment"] = ["" for _ in range(len(df))] # template for "agree" or "disagree" buttons if use_model: df = df.sort_values(by=["error_amt"], ascending=False) # surface largest errors first else: if debug: print("get_cluster_comments; not using model") df = df.sort_values(by=[sys_col], ascending=True) df["id"] = df["item_id"] df["toxicity_category"] = df["category"] df["user_rating"] = df["pred"] df["user_decision"] = [get_decision(rating, threshold) for rating in df["pred"].tolist()] df["system_rating"] = df[sys_col] df["system_decision"] = [get_decision(rating, threshold) for rating in df[sys_col].tolist()] df = df.round(decimals=2) # Filter to specified error type if error_type == "System is under-sensitive": # FN: system rates non-toxic, but user rates toxic df = df[df["error_type"] == "System may be under-sensitive"] elif error_type == "System is over-sensitive": # FP: system rates toxic, but user rates non-toxic df = df[df["error_type"] == "System may be over-sensitive" ] elif error_type == "Both": df = df[(df["error_type"] == "System may be under-sensitive") | (df["error_type"] == "System may be over-sensitive")] return df # PERSONALIZED CLUSTERS utils def get_disagreement_comments(preds_df, mode, n=10_000, threshold=TOXIC_THRESHOLD): # Get difference between user rating and system rating df = preds_df.copy() df["diff"] = [get_error_size(sys, user, threshold) for sys, user in zip(df["rating"].tolist(), df["pred"].tolist())] df["error_type"] = [get_error_type(sys, user, threshold) for sys, user in zip(df["rating"].tolist(), df["pred"].tolist())] # asc = low to high; lowest = sys lower than user (under-sensitive) # desc = high to low; lowest = sys higher than user (over-sensitive) if mode == "under-sensitive": df = df[df["error_type"] == "System may be under-sensitive"] asc = True elif mode == "over-sensitive": df = df[df["error_type"] == "System may be over-sensitive"] asc = False df = df.sort_values(by=["diff"], ascending=asc) df = df.head(n) return df["comment"].tolist(), df def get_explore_df(n_examples, threshold): df = system_preds_df.sample(n=n_examples) df["system_decision"] = [get_decision(rating, threshold) for rating in df["rating"].tolist()] df["system_color"] = [get_user_color(sys, threshold) for sys in df["rating"].tolist()] # get cell colors return df