From 7df7b6d28287f592536cdbf01b6aec73e7b045ef Mon Sep 17 00:00:00 2001 From: MauriceRohr <34025664+MauriceRohr@users.noreply.github.com> Date: Mon, 20 Dec 2021 12:23:34 +0100 Subject: [PATCH] update for physionet submission bug fixes and new required methods for 4 label prediction in docker --- Dockerfile | 2 +- evaluate_model.py | 360 ++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 +- test_model.py | 29 ++++ wettbewerb.py | 36 +++-- 5 files changed, 418 insertions(+), 13 deletions(-) create mode 100644 evaluate_model.py create mode 100644 test_model.py diff --git a/Dockerfile b/Dockerfile index a55aecd8..40b7bd18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,6 @@ RUN mkdir /physionet COPY ./ /physionet WORKDIR /physionet # Install Python requirements -RUN pip install --no-deps -r requirements.txt -f https://download.pytorch.org/whl/torch_stable.html +RUN pip install -r requirements.txt # Reset working directory WORKDIR /physionet \ No newline at end of file diff --git a/evaluate_model.py b/evaluate_model.py new file mode 100644 index 00000000..0158f9e3 --- /dev/null +++ b/evaluate_model.py @@ -0,0 +1,360 @@ +import os +import sys +import pandas as pd +import argparse +from sklearn.metrics import roc_auc_score, average_precision_score +import numpy as np + + +def score(label_dir='../test/',output_dir='./'): + + if not os.path.exists(os.path.join(output_dir,"PREDICTIONS.csv")): + sys.exit("Es gibt keine Predictions") + + if not os.path.exists(os.path.join(label_dir, "REFERENCE.csv")): + sys.exit("Es gibt keine Ground Truth") + + df_pred = pd.read_csv(os.path.join(output_dir,"PREDICTIONS.csv"), header=None) # Klassifikationen + df_gt = pd.read_csv(os.path.join(label_dir,"REFERENCE.csv"), header=None) # Wahrheit + + N_files = df_gt.shape[0] # Anzahl an Datenpunkten + + ## für normalen F1-Score + TP = 0 # Richtig Positive + TN = 0 # Richtig Negative + FP = 0 # Falsch Positive + FN = 0 # Falsch Negative + + ## für Multi-Class-F1 + Nn = 0 # Wahrheit ist normal, klassifiziert als normal + Na = 0 # Wahrheit ist normal, klassifiziert als Vorhofflimmern + No = 0 # Wahrheit ist normal, klassifiziert als anderer Rhythmus + Np = 0 # Wahrheit ist normal, klassifiziert als unbrauchbar + An = 0 # Wahrheit ist Vorhofflimmern, klassifiziert als normal + Aa = 0 # Wahrheit ist Vorhofflimmern, klassifiziert als Vorhofflimmern + Ao = 0 # Wahrheit ist Vorhofflimmern, klassifiziert als anderer Rhythmus + Ap = 0 # Wahrheit ist Vorhofflimmern, klassifiziert als unbrauchbar + On = 0 # Wahrheit ist anderer Rhythmus, klassifiziert als normal + Oa = 0 # Wahrheit ist anderer Rhythmus, klassifiziert als Vorhofflimmern + Oo = 0 # Wahrheit ist anderer Rhythmus, klassifiziert als anderer Rhythmus + Op = 0 # Wahrheit ist anderer Rhythmus, klassifiziert als unbrauchbar + Pn = 0 # Wahrheit ist unbrauchbar, klassifiziert als normal + Pa = 0 # Wahrheit ist unbrauchbar, klassifiziert als Vorhofflimmern + Po = 0 # Wahrheit ist unbrauchbar, klassifiziert als anderer Rhythmus + Pp = 0 # Wahrheit ist unbrauchbar, klassifiziert als unbrauchbar + + y_true = list() + y_score = list() + + for i in range(N_files): + gt_name = df_gt[0][i] + gt_class = df_gt[1][i] + + pred_indx = df_pred[df_pred[0]==gt_name].index.values + + if not pred_indx.size: + print("Prediktion für " + gt_name + " fehlt, nehme \"normal\" an.") + pred_class = "N" + pred_certainty=0.5 + else: + pred_indx = pred_indx[0] + pred_class = df_pred[1][pred_indx] + pred_certainty = df_pred[2][pred_indx] + + if gt_class == "A" and pred_class == "A": + TP = TP + 1 + y_score.append(pred_certainty) + y_true.append(1) + if gt_class == "N" and pred_class != "A": + TN = TN + 1 + y_score.append(pred_certainty) + y_true.append(0) + if gt_class == "N" and pred_class == "A": + FP = FP + 1 + y_score.append(pred_certainty) + y_true.append(0) + if gt_class == "A" and pred_class != "A": + FN = FN + 1 + y_score.append(pred_certainty) + y_true.append(1) + + + if gt_class == "N": + if pred_class == "N": + Nn = Nn + 1 + if pred_class == "A": + Na = Na + 1 + if pred_class == "O": + No = No + 1 + if pred_class == "~": + Np = Np + 1 + + if gt_class == "A": + if pred_class == "N": + An = An + 1 + if pred_class == "A": + Aa = Aa + 1 + if pred_class == "O": + Ao = Ao + 1 + if pred_class == "~": + Ap = Ap + 1 + + if gt_class == "O": + if pred_class == "N": + On = On + 1 + if pred_class == "A": + Oa = Oa + 1 + if pred_class == "O": + Oo = Oo + 1 + if pred_class == "~": + Op = Op + 1 + + if gt_class == "~": + if pred_class == "N": + Pn = Pn + 1 + if pred_class == "A": + Pa = Pa + 1 + if pred_class == "O": + Po = Po + 1 + if pred_class == "~": + Pp = Pp + 1 + + sum_N = Nn + Na + No + Np + sum_A = An + Aa + Ao + Ap + sum_O = On + Oa + Oo + Op + sum_P = Pn + Pa + Po + Pp + + sum_n = Nn + An + On + Pn + sum_a = Na + Aa + Oa + Pa + sum_o = No + Ao + Oo + Po + sum_p = Np + Ap + Op + Pp + + F1 = TP / (TP + 1/2*(FP+FN)) + + + # Confusion Matrix zur Evaluation + Conf_Matrix = {'N':{'n':Nn,'a':Na,'o':No,'p':Np}, + 'A':{'n':An,'a':Aa,'o':Ao,'p':Ap}, + 'O':{'n':On,'a':Oa,'o':Oo,'p':Op}, + 'P':{'n':Pn,'a':Pa,'o':Po,'p':Pp}} + + F1_mult = 0 + n_f1_mult = 0 + + + if (sum_N + sum_n)!=0 : + F1_mult += 2 * Nn / (sum_N + sum_n) + n_f1_mult += 1 + + if (sum_A + sum_a)!=0 : + F1_mult += 2 * Aa / (sum_A + sum_a) + n_f1_mult += 1 + + if (sum_O + sum_o)!=0 : + F1_mult += 2 * Oo / (sum_O + sum_o) + n_f1_mult += 1 + + if (sum_P + sum_p)!=0 : + F1_mult += 2 * Pp / (sum_P + sum_p) + n_f1_mult += 1 + + F1_mult = F1_mult/n_f1_mult + + + y_true = np.array(y_true) + y_score = np.array(y_score) + AUROC = roc_auc_score(y_true,y_score) + AUPRC = average_precision_score(y_true,y_score) + accuracy = (TP+TN)/(TP+TN+FN+FP) + + + return F1,F1_mult,Conf_Matrix,AUROC,AUPRC,accuracy + +# +def score_official_physionet(label_dir='../test/',output_dir='./'): + + if not os.path.exists(os.path.join(output_dir,"PREDICTIONS.csv")): + sys.exit("Es gibt keine Predictions") + + if not os.path.exists(os.path.join(label_dir, "REFERENCE.csv")): + sys.exit("Es gibt keine Ground Truth") + + df_pred = pd.read_csv(os.path.join(output_dir,"PREDICTIONS.csv"), header=None) # Klassifikationen + df_gt = pd.read_csv(os.path.join(label_dir,"REFERENCE.csv"), header=None) # Wahrheit + + N_files = df_gt.shape[0] # Anzahl an Datenpunkten + + ## für normalen F1-Score + TP = 0 # Richtig Positive + TN = 0 # Richtig Negative + FP = 0 # Falsch Positive + FN = 0 # Falsch Negative + + ## für Multi-Class-F1 + Nn = 0 # Wahrheit ist normal, klassifiziert als normal + Na = 0 # Wahrheit ist normal, klassifiziert als Vorhofflimmern + No = 0 # Wahrheit ist normal, klassifiziert als anderer Rhythmus + Np = 0 # Wahrheit ist normal, klassifiziert als unbrauchbar + An = 0 # Wahrheit ist Vorhofflimmern, klassifiziert als normal + Aa = 0 # Wahrheit ist Vorhofflimmern, klassifiziert als Vorhofflimmern + Ao = 0 # Wahrheit ist Vorhofflimmern, klassifiziert als anderer Rhythmus + Ap = 0 # Wahrheit ist Vorhofflimmern, klassifiziert als unbrauchbar + On = 0 # Wahrheit ist anderer Rhythmus, klassifiziert als normal + Oa = 0 # Wahrheit ist anderer Rhythmus, klassifiziert als Vorhofflimmern + Oo = 0 # Wahrheit ist anderer Rhythmus, klassifiziert als anderer Rhythmus + Op = 0 # Wahrheit ist anderer Rhythmus, klassifiziert als unbrauchbar + Pn = 0 # Wahrheit ist unbrauchbar, klassifiziert als normal + Pa = 0 # Wahrheit ist unbrauchbar, klassifiziert als Vorhofflimmern + Po = 0 # Wahrheit ist unbrauchbar, klassifiziert als anderer Rhythmus + Pp = 0 # Wahrheit ist unbrauchbar, klassifiziert als unbrauchbar + + y_true = list() + y_score = list() + + y_true_multi = np.zeros((N_files,4)) + y_score_multi = np.zeros((N_files,4)) + classes = {'N':0,'A':1,'O':2,'~':3} + + + for i in range(N_files): + gt_name = df_gt[0][i] + gt_class = df_gt[1][i] + + pred_indx = df_pred[df_pred[0]==gt_name].index.values + + if not pred_indx.size: + print("Prediktion für " + gt_name + " fehlt, nehme \"normal\" an.") + pred_class = "N" + pred_certainty=np.ones((4))*0.25 + else: + pred_indx = pred_indx[0] + pred_class = df_pred[1][pred_indx] + pred_certainty = np.zeros((4)) + for i in range(4): + pred_certainty[i] = df_pred[i+2][pred_indx] + y_true_multi[i,classes[gt_class]]=1 + y_score_multi[i,:] = pred_certainty + + if gt_class == "A" and pred_class == "A": + TP = TP + 1 + y_score.append(pred_certainty[1]) + y_true.append(1) + if gt_class == "N" and pred_class != "A": + TN = TN + 1 + y_score.append(pred_certainty[1]) + y_true.append(0) + if gt_class == "N" and pred_class == "A": + FP = FP + 1 + y_score.append(pred_certainty[1]) + y_true.append(0) + if gt_class == "A" and pred_class != "A": + FN = FN + 1 + y_score.append(pred_certainty[1]) + y_true.append(1) + + + if gt_class == "N": + if pred_class == "N": + Nn = Nn + 1 + if pred_class == "A": + Na = Na + 1 + if pred_class == "O": + No = No + 1 + if pred_class == "~": + Np = Np + 1 + + if gt_class == "A": + if pred_class == "N": + An = An + 1 + if pred_class == "A": + Aa = Aa + 1 + if pred_class == "O": + Ao = Ao + 1 + if pred_class == "~": + Ap = Ap + 1 + + if gt_class == "O": + if pred_class == "N": + On = On + 1 + if pred_class == "A": + Oa = Oa + 1 + if pred_class == "O": + Oo = Oo + 1 + if pred_class == "~": + Op = Op + 1 + + if gt_class == "~": + if pred_class == "N": + Pn = Pn + 1 + if pred_class == "A": + Pa = Pa + 1 + if pred_class == "O": + Po = Po + 1 + if pred_class == "~": + Pp = Pp + 1 + + sum_N = Nn + Na + No + Np + sum_A = An + Aa + Ao + Ap + sum_O = On + Oa + Oo + Op + sum_P = Pn + Pa + Po + Pp + + sum_n = Nn + An + On + Pn + sum_a = Na + Aa + Oa + Pa + sum_o = No + Ao + Oo + Po + sum_p = Np + Ap + Op + Pp + + F1 = TP / (TP + 1/2*(FP+FN)) + + + # Confusion Matrix zur Evaluation + Conf_Matrix = {'N':{'n':Nn,'a':Na,'o':No,'p':Np}, + 'A':{'n':An,'a':Aa,'o':Ao,'p':Ap}, + 'O':{'n':On,'a':Oa,'o':Oo,'p':Op}, + 'P':{'n':Pn,'a':Pa,'o':Po,'p':Pp}} + + F1_mult = 0 + n_f1_mult = 0 + + + if (sum_N + sum_n)!=0 : + F1_mult += 2 * Nn / (sum_N + sum_n) + n_f1_mult += 1 + + if (sum_A + sum_a)!=0 : + F1_mult += 2 * Aa / (sum_A + sum_a) + n_f1_mult += 1 + + if (sum_O + sum_o)!=0 : + F1_mult += 2 * Oo / (sum_O + sum_o) + n_f1_mult += 1 + + if (sum_P + sum_p)!=0 : + F1_mult += 2 * Pp / (sum_P + sum_p) + n_f1_mult += 1 + + F1_mult = F1_mult/n_f1_mult + + + y_true = np.array(y_true) + y_score = np.array(y_score) + AUROC = roc_auc_score(y_true,y_score) + AUPRC = average_precision_score(y_true,y_score) + AUROC_macro = roc_auc_score(y_true_multi,y_score_multi,multi_class='ovr') + AUPRC_macro = average_precision_score(y_true_multi,y_score_multi) + accuracy_multi = (Nn+Aa+Oo+Pp)/N_files + + accuracy = (TP+TN)/(TP+TN+FN+FP) + + + return F1,F1_mult,Conf_Matrix,AUROC,AUPRC,accuracy,AUPRC_macro,AUROC_macro,accuracy_multi + + + + +if __name__=='__main__': + parser = argparse.ArgumentParser(description='Predict given Model') + parser.add_argument('label_directory', action='store',type=str) + parser.add_argument('output_directory', action='store',type=str) + args = parser.parse_args() + F1,F1_mult,Conf_Matrix,AUROC,AUPRC,accuracy,AUPRC_macro,AUROC_macro,accuracy_multi = score_official_physionet(args.label_directory,args.output_directory) + print("F1:",F1,"\t AUROC:",AUROC,"\t AUPRC:",AUPRC,"\t Accuracy:",accuracy,"\n Physionet2017 Score:",F1_mult,"\t AUROC macro:",AUROC_macro,"\t AUPRC macro:",AUPRC_macro, "\t Accuracy multi:",accuracy_multi) diff --git a/requirements.txt b/requirements.txt index 5818a26d..26eeb4c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ matplotlib>=3.3.0 numpy>=1.18.5 py-ecg-detectors>=1.0.2 pandas>=1.1.0 -torch==1.8.1+cu111 tqdm>=4.60.0 torchaudio==0.8.1 rtpt>=0.0.4 @@ -11,4 +10,5 @@ torch-optimizer==0.1.0 airspeed>=0.5.17 ninja pytorch-ranger -setproctitle \ No newline at end of file +setproctitle +sklearn \ No newline at end of file diff --git a/test_model.py b/test_model.py new file mode 100644 index 00000000..57fcd40d --- /dev/null +++ b/test_model.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" + +Skript testet das vortrainierte Modell + + +@author: Maurice Rohr +""" + +from predict import predict_labels +from wettbewerb import load_references, save_predictions +import argparse +import time + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Predict given Model') + parser.add_argument('model_directory', action='store',type=str) + parser.add_argument('data_directory', action='store',type=str) + parser.add_argument('output_directory', action='store',type=str) + args = parser.parse_args() + + ecg_leads,ecg_labels,fs,ecg_names = load_references(args.data_directory) # Importiere EKG-Dateien, zugehörige Diagnose, Sampling-Frequenz (Hz) und Name # Sampling-Frequenz 300 Hz + + start_time = time.time() + predictions = predict_labels(ecg_leads,fs,ecg_names, is_binary_classifier=False, return_probability=True) + pred_time = time.time()-start_time + + save_predictions(predictions,folder=args.output_directory) # speichert Prädiktion in CSV Datei + print("Runtime",pred_time,"s") diff --git a/wettbewerb.py b/wettbewerb.py index 3542c258..7b737fd8 100644 --- a/wettbewerb.py +++ b/wettbewerb.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """ Diese Datei sollte nicht verändert werden und wird von uns gestellt und zurückgesetzt. + Funktionen zum Laden und Speichern der Dateien """ __author__ = "Maurice Rohr und Christoph Reich" @@ -14,12 +15,13 @@ ### Achtung! Diese Funktion nicht veraendern. -def load_references(folder: str = '../training') -> Tuple[List[np.ndarray], List[str], int, List[str]]: +def load_references(folder: str = '../training', load_labels: bool = True) -> Tuple[List[np.ndarray], List[str], int, List[str]]: """ Parameters ---------- folder : str, optional Ort der Trainingsdaten. Default Wert '../training'. + Returns ------- ecg_leads : List[np.ndarray] @@ -36,7 +38,10 @@ def load_references(folder: str = '../training') -> Tuple[List[np.ndarray], List assert os.path.exists(folder), 'Parameter folder existiert nicht!' # Initialisiere Listen für leads, labels und names ecg_leads: List[np.ndarray] = [] - ecg_labels: List[str] = [] + if load_labels: + ecg_labels: List[str] = [] + else: + ecg_labels=None ecg_names: List[str] = [] # Setze sampling Frequenz fs: int = 300 @@ -48,16 +53,19 @@ def load_references(folder: str = '../training') -> Tuple[List[np.ndarray], List # Lade MatLab Datei mit EKG lead and label data = sio.loadmat(os.path.join(folder, row[0] + '.mat')) ecg_leads.append(data['val'][0]) - ecg_labels.append(row[1]) + if load_labels: + ecg_labels.append(row[1]) ecg_names.append(row[0]) # Zeige an wie viele Daten geladen wurden print("{}\t Dateien wurden geladen.".format(len(ecg_leads))) return ecg_leads, ecg_labels, fs, ecg_names + + ### Achtung! Diese Funktion nicht veraendern. -def save_predictions(predictions: List[Tuple[str, str, float]], folder: str = None) -> None: +def save_predictions(predictions: List[Tuple[str, str, float]], folder: str=None) -> None: """ Funktion speichert the gegebenen predictions in eine CSV-Datei mit dem name PREDICTIONS.csv Parameters @@ -71,17 +79,18 @@ def save_predictions(predictions: List[Tuple[str, str, float]], folder: str = No Returns ------- None. - """ - # Check Parameter + + """ + # Check Parameter assert isinstance(predictions, list), \ "Parameter predictions muss eine Liste sein aber {} gegeben.".format(type(predictions)) assert len(predictions) > 0, 'Parameter predictions muss eine nicht leere Liste sein.' assert isinstance(predictions[0], tuple), \ "Elemente der Liste predictions muss ein Tuple sein aber {} gegeben.".format(type(predictions[0])) - assert isinstance(predictions[0][2], float), \ + assert isinstance(predictions[0][2], float ) or isinstance(predictions[0][2], dict ), \ "3. Element der Tupel in der Liste muss vom Typ float sein, aber {} gegeben".format(type(predictions[0][2])) - - if folder == None: + + if folder==None: file = "PREDICTIONS.csv" else: file = os.path.join(folder, "PREDICTIONS.csv") @@ -95,6 +104,13 @@ def save_predictions(predictions: List[Tuple[str, str, float]], folder: str = No predictions_writer = csv.writer(predictions_file, delimiter=',') # Iteriere über jede prediction for prediction in predictions: - predictions_writer.writerow([prediction[0], prediction[1], prediction[2]]) + if type(prediction[2])==dict: + try: + res = (prediction[2]['N'],prediction[2]['A'],prediction[2]['O'],prediction[2]['~']) + except: + res= (0.25,0.25,0.25,0.25) + predictions_writer.writerow([prediction[0], prediction[1], res[0], res[1], res[2], res[3]]) + else: + predictions_writer.writerow([prediction[0], prediction[1], prediction[2]]) # Gebe Info aus wie viele labels (predictions) gespeichert werden print("{}\t Labels wurden geschrieben.".format(len(predictions)))