Spaces:
Running
Running
| import argparse | |
| import numpy as np | |
| import pandas as pd | |
| import os | |
| """ | |
| The purpose of this algorithm is to rank chunks of data based on how much they would improve the predictive model, | |
| should they be annotated and added to its training set. | |
| The input to this algorithm is the csv file with inference results that is outputted by our predictive model. | |
| The output of this algorithm is a csv file ranking the different chunks from 1 (highest priority for annotation) to n, | |
| n being the total number of chunks. | |
| Terminology: | |
| - Segment: the sequence length of audio sample processed by the model. Detection probabilities are given per segment | |
| (default segment length is 1 second). | |
| - Chunk: a bunch of consecutive segments. | |
| The algorithm ranks chunks, and not segments, in order to make the end result more easily usable for the end user (the | |
| intended annotator). | |
| Chunk length is negatively correlated with how valuable the ranking is (with shorter chunk length, the | |
| difference in predicted value between each two chunks is bigger), but positively correlated with ease of use by future | |
| annotators (longer chunks -> easier to use when annotating). | |
| """ | |
| def get_recording_name_from_inference_file_name(inference_file_name): | |
| recording_name = inference_file_name.split('\\')[-1].split('.')[0].split('-')[-1] | |
| return recording_name | |
| def load_inference_results_from_dir(inference_dir: str, segment_length_in_seconds: int) -> pd.DataFrame: | |
| df_all_recordings_inference = pd.DataFrame( | |
| columns=['segment_id', 'recording', 'class0_prob', 'class1_prob', 'segment_start_sec', 'segment_end_sec']) | |
| for filename in os.listdir(inference_dir): | |
| df_one_recording_inference = create_inference_df_for_one_recording(filename, inference_dir, | |
| segment_length_in_seconds) | |
| df_all_recordings_inference = pd.concat([df_all_recordings_inference, df_one_recording_inference], | |
| ignore_index=True) | |
| df_all_recordings_inference.insert(0, 'chunk_id', '') | |
| return df_all_recordings_inference | |
| def create_inference_df_for_one_recording(filename: str, inference_dir: str, segment_length_in_seconds: int): | |
| """ | |
| Load inference file and create DataFrame for one recording. | |
| :return: pd.DataFrame | |
| """ | |
| recording_name = get_recording_name_from_inference_file_name(filename) | |
| inference_full_path = os.path.join(inference_dir, filename) | |
| df_one_recording_inference = pd.read_csv(inference_full_path) | |
| df_one_recording_inference.insert(0, 'recording', recording_name) | |
| if 'begin_time' in df_one_recording_inference.columns: | |
| df_one_recording_inference = df_one_recording_inference.rename( | |
| {'begin_time': 'segment_start_sec', 'end_time': 'segment_end_sec'}, axis=1) | |
| else: | |
| df_one_recording_inference['segment_start_sec'] = pd.Series( | |
| np.arange(df_one_recording_inference.shape[0] * segment_length_in_seconds, step=segment_length_in_seconds)) | |
| df_one_recording_inference['segment_end_sec'] = df_one_recording_inference[ | |
| 'segment_start_sec'] + segment_length_in_seconds | |
| df_one_recording_inference['segment_id'] = df_one_recording_inference['recording'] + '_' + \ | |
| df_one_recording_inference['segment_start_sec'].astype(str) | |
| return df_one_recording_inference | |
| def validate_chunk_end_sec(chunk_end_sec: int, chunk_start_sec: int, chunk_actual_size: int) -> int: | |
| """ | |
| Make sure the chunk_end_sec is correct (that it fits the chunk_start_sec and the desired chunk length). | |
| Else, correct it. | |
| """ | |
| if chunk_end_sec - chunk_start_sec == chunk_actual_size: | |
| return chunk_end_sec | |
| elif chunk_end_sec - chunk_start_sec > chunk_actual_size: | |
| chunk_end_sec = chunk_start_sec + chunk_actual_size | |
| return chunk_end_sec | |
| else: | |
| print('chunk size error!') | |
| exit() | |
| def add_chunk_id_per_recording(df: pd.DataFrame, recording_id: str, chunk_len_in_seconds: int = 300) -> pd.DataFrame: | |
| """ | |
| Gets a DataFrame of recording segments; for a specific recording id, assigns all segments to chunks of desired length (defined by chunk_len_in_seconds). | |
| :param df: Pandas DataFrame containing segments of at least one recording | |
| :param recording_id: original id for recording to be processed into chinks | |
| :param chunk_len_in_seconds: required number of seconds for each chunk | |
| :return: DataFrame of recording segments with the specified recording separated into chunks (by assigned chunk_id). | |
| """ | |
| recording_len_in_secs = df[df.recording == recording_id].segment_end_sec.max() | |
| chunk_start_sec = compute_first_chunk_start_sec(df, recording_id) | |
| while chunk_start_sec < recording_len_in_secs: | |
| df = assign_chunk_id(chunk_len_in_seconds, chunk_start_sec, df, recording_id, recording_len_in_secs) | |
| chunk_start_sec += chunk_len_in_seconds | |
| return df | |
| def compute_first_chunk_start_sec(df: pd.DataFrame, recording_id: str) -> int: | |
| earliest_recording_sec = df[df.recording == recording_id].segment_start_sec.min() | |
| chunk_start_sec = max(0, earliest_recording_sec) | |
| return chunk_start_sec | |
| def assign_chunk_id(chunk_len_in_seconds: int, chunk_start_sec: int, df: pd.DataFrame, recording_id: str, | |
| recording_len_in_secs: int) -> pd.DataFrame: | |
| """ | |
| Compute and assign the next chunk id. | |
| Only one chunk id is applied per run. | |
| :return: Pandas DataFrame with the assigned chunk_id for the relevant segments. | |
| """ | |
| chunk_bool_mask, chunk_end_sec = compute_chunk_boundaries(chunk_len_in_seconds, chunk_start_sec, df, recording_id, | |
| recording_len_in_secs) | |
| chunk_id = f'recording_{recording_id}_sec_{chunk_start_sec}_to_{chunk_end_sec}' | |
| df.loc[chunk_bool_mask, 'chunk_id'] = chunk_id | |
| return df | |
| def compute_chunk_boundaries(chunk_len_in_seconds: int, chunk_start_sec: int, df: pd.DataFrame, recording_id: str, | |
| recording_len_in_secs: int): | |
| """ | |
| Figure out which segments should be included in chunk. | |
| :return: | |
| chunk_bool_mask: a boolean mask of all segments (=rows) included in chunk; pd.Series | |
| chunk_end_sec: the last recording second included in the chunk; int | |
| """ | |
| chunk_end_sec = min(chunk_start_sec + chunk_len_in_seconds, recording_len_in_secs) | |
| chunk_bool_mask = ((df.recording == recording_id) & (df.segment_start_sec >= chunk_start_sec) & ( | |
| df.segment_end_sec <= chunk_end_sec)) | |
| chunk_actual_size = df[chunk_bool_mask].shape[0] | |
| chunk_end_sec = validate_chunk_end_sec(chunk_end_sec, chunk_start_sec, chunk_actual_size) | |
| return chunk_bool_mask, chunk_end_sec | |
| def compute_potential_gain_per_segment(df: pd.DataFrame) -> pd.DataFrame: | |
| df['max'] = df[['class0_prob', 'class1_prob']].max(axis=1) | |
| df['gain'] = 1 - df['max'] | |
| df = df.drop('max', axis=1) | |
| return df | |
| def n_high_priority_segments(df_chunk: pd.DataFrame, threshold) -> int: | |
| """ | |
| returns the number of high priority segments (where gain > threshold) in given dataframe | |
| """ | |
| return df_chunk[df_chunk['gain'] > threshold].shape[0] | |
| def format_ranked_chunks_for_output(ranked_chunks: pd.Series) -> pd.DataFrame: | |
| ''' | |
| Reformat a Pandas series of ranked chunks into the dataframe that serves as final output. | |
| :param ranked_chunks: pandas series containing chunk_id (as index) and the chunk gain score (the higher score, the more valuable the chunk). | |
| :return: pandas DataFrame with chunk_id (as index), chunk score (see above), and ranking (starting at 1; the lower the ranking, the more valuable the chunk). | |
| ''' | |
| df_ranked_chunks = pd.DataFrame(ranked_chunks) | |
| df_ranked_chunks.columns = ['chunk_score'] | |
| df_ranked_chunks.insert(0, 'ranking', np.arange(df_ranked_chunks.shape[0]) + 1) | |
| return df_ranked_chunks | |
| def get_ranked_segments_from_inference_dir(inference_dir: str, chunk_len_in_seconds: int, | |
| gain_threshold: float, segment_length_in_seconds: int) -> pd.DataFrame: | |
| df = load_inference_results_from_dir(inference_dir, segment_length_in_seconds) | |
| for recording_id in df.recording.unique(): | |
| df = add_chunk_id_per_recording(df, recording_id, chunk_len_in_seconds) | |
| df = compute_potential_gain_per_segment(df) | |
| ranked_chunks = df.groupby('chunk_id').apply( | |
| lambda x: n_high_priority_segments(x, threshold=gain_threshold)).sort_values( | |
| ascending=False) | |
| df_ranked_chunks = format_ranked_chunks_for_output(ranked_chunks) | |
| return df_ranked_chunks | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description='Get annotation rankings for recording chunks.') | |
| parser.add_argument('inference_dir', type=str, help='path to the folder containing inference files') | |
| parser.add_argument('output_dir', type=str, default='.', help='path where the output file should be saved') | |
| parser.add_argument('--chunk_len', dest='chunk_len_in_seconds', type=int, default='180', | |
| help='required chunk length (in seconds)') | |
| parser.add_argument('--seg_len', dest='segment_length_in_seconds', type=int, default='1', | |
| help='required segment length (in seconds)') | |
| parser.add_argument('--thresh', dest='gain_threshold', type=float, default='0.35', | |
| help='desired gain threshold for considering a segment as valuable') | |
| args = parser.parse_args() | |
| ranked_chunks = get_ranked_segments_from_inference_dir(args.inference_dir, args.chunk_len_in_seconds, | |
| args.gain_threshold, args.segment_length_in_seconds) | |
| output_full_path = os.path.join(args.output_dir, 'ranked_chunks.csv') | |
| ranked_chunks.to_csv(output_full_path) | |
| print( | |
| f'Chunk ranking computed based on the following parameters: chunk length {args.chunk_len_in_seconds} seconds, segment ' | |
| f'length {args.segment_length_in_seconds} seconds, gain threshold {args.gain_threshold}.\nRanked chunks csv ' | |
| f'saved at <{output_full_path}>') | |
| # | |