From f7e931ba3dce4da50430a4e41348348f269c7010 Mon Sep 17 00:00:00 2001 From: katharinakorb <62990236+katharinakorb@users.noreply.github.com> Date: Sun, 5 Nov 2023 20:14:04 +0100 Subject: [PATCH] Add files via upload Additional files used for analysis --- thesis code/network analysis/freeParamCalc.py | 49 ++ .../minimal_architecture/README.txt | 18 + .../minimal_architecture/config.json | 368 +++++++++++ .../get_trained_models.py | 55 ++ .../pfinkel_performance_test64.py | 282 ++++++++ .../minimal_architecture/training_loop.sh | 11 + .../optimal_stimulus/README.txt | 8 + .../optimal_stimulus/optimal_stimulus.py | 219 +++++++ .../optimal_stimulus_20cnns.py | 293 +++++++++ .../orientation_tuning/README.txt | 14 + .../orientation_tuning/fit_statistics.py | 475 ++++++++++++++ .../orientation_tuning/fitkarotte.py | 373 +++++++++++ .../orientation_tuning/gabor_dict_32o_8p.npy | Bin 0 -> 247936 bytes .../orientation_tuning_curve.py | 244 +++++++ .../orientation_tuning/plot_fit_statistics.py | 272 ++++++++ .../psychometric_curves/README.txt | 4 + .../error_bar_performance_pfinkel.py | 223 +++++++ .../render_including_minDist/contours.py | 603 ++++++++++++++++++ .../render_including_minDist/render.py | 349 ++++++++++ .../weights_correlation/README.txt | 28 + .../all_cnns_mean_correlation.py | 274 ++++++++ .../weights_correlation/create_gabor_dict.py | 87 +++ .../weights_correlation/draw_input_fields.py | 156 +++++ .../weight visualization/plot_as_grid.py | 194 ++++++ .../weight visualization/plot_weights.py | 101 +++ thesis code/shallow net/README.txt | 12 + thesis code/shallow net/config.json | 53 ++ thesis code/shallow net/corner_loop_final.sh | 13 + .../functions/alicorn_data_loader.py | 107 ++++ .../shallow net/functions/analyse_network.py | 103 +++ .../shallow net/functions/create_logger.py | 40 ++ thesis code/shallow net/functions/make_cnn.py | 114 ++++ .../functions/plot_intermediate.py | 84 +++ thesis code/shallow net/functions/set_seed.py | 12 + thesis code/shallow net/functions/test.py | 58 ++ thesis code/shallow net/functions/train.py | 80 +++ 36 files changed, 5376 insertions(+) create mode 100644 thesis code/network analysis/freeParamCalc.py create mode 100644 thesis code/network analysis/minimal_architecture/README.txt create mode 100644 thesis code/network analysis/minimal_architecture/config.json create mode 100644 thesis code/network analysis/minimal_architecture/get_trained_models.py create mode 100644 thesis code/network analysis/minimal_architecture/pfinkel_performance_test64.py create mode 100644 thesis code/network analysis/minimal_architecture/training_loop.sh create mode 100644 thesis code/network analysis/optimal_stimulus/README.txt create mode 100644 thesis code/network analysis/optimal_stimulus/optimal_stimulus.py create mode 100644 thesis code/network analysis/optimal_stimulus/optimal_stimulus_20cnns.py create mode 100644 thesis code/network analysis/orientation_tuning/README.txt create mode 100644 thesis code/network analysis/orientation_tuning/fit_statistics.py create mode 100644 thesis code/network analysis/orientation_tuning/fitkarotte.py create mode 100644 thesis code/network analysis/orientation_tuning/gabor_dict_32o_8p.npy create mode 100644 thesis code/network analysis/orientation_tuning/orientation_tuning_curve.py create mode 100644 thesis code/network analysis/orientation_tuning/plot_fit_statistics.py create mode 100644 thesis code/network analysis/psychometric_curves/README.txt create mode 100644 thesis code/network analysis/psychometric_curves/error_bar_performance_pfinkel.py create mode 100644 thesis code/network analysis/render_including_minDist/contours.py create mode 100644 thesis code/network analysis/render_including_minDist/render.py create mode 100644 thesis code/network analysis/weights_correlation/README.txt create mode 100644 thesis code/network analysis/weights_correlation/all_cnns_mean_correlation.py create mode 100644 thesis code/network analysis/weights_correlation/create_gabor_dict.py create mode 100644 thesis code/network analysis/weights_correlation/draw_input_fields.py create mode 100644 thesis code/network analysis/weights_correlation/weight visualization/plot_as_grid.py create mode 100644 thesis code/network analysis/weights_correlation/weight visualization/plot_weights.py create mode 100644 thesis code/shallow net/README.txt create mode 100644 thesis code/shallow net/config.json create mode 100644 thesis code/shallow net/corner_loop_final.sh create mode 100644 thesis code/shallow net/functions/alicorn_data_loader.py create mode 100644 thesis code/shallow net/functions/analyse_network.py create mode 100644 thesis code/shallow net/functions/create_logger.py create mode 100644 thesis code/shallow net/functions/make_cnn.py create mode 100644 thesis code/shallow net/functions/plot_intermediate.py create mode 100644 thesis code/shallow net/functions/set_seed.py create mode 100644 thesis code/shallow net/functions/test.py create mode 100644 thesis code/shallow net/functions/train.py diff --git a/thesis code/network analysis/freeParamCalc.py b/thesis code/network analysis/freeParamCalc.py new file mode 100644 index 0000000..015b085 --- /dev/null +++ b/thesis code/network analysis/freeParamCalc.py @@ -0,0 +1,49 @@ +import torch + + +def calc_free_params(from_loaded_model: bool, model_name: str | None): + """ + * Calculates the number of free parameters of a CNN + * either from trained model or by entering the respective parameters + over command line + """ + + if from_loaded_model: + # path to NN + PATH = f"D:/Katha/Neuroscience/Semester 4/newCode/kk_contour_net_shallow-main/trained_models/{model_name}" + + # load and evaluate model + model = torch.load(PATH).to("cpu") + model.eval() + print(model) + + total_params = sum(p.numel() for p in model.parameters() if p.requires_grad) + print(f"Total number of free parameters: {total_params}") + else: + print("\n##########################") + input_out_channel_size = input( + "Enter output channel size (comma seperated, including output layer): " + ) + out_channel_size = [1] + [int(x) for x in input_out_channel_size.split(",")] + + input_kernel_sizes = input( + "Enter kernel sizes of respective layers (comma seperated, including output layer): " + ) + kernel_sizes = [int(x) for x in input_kernel_sizes.split(",")] + + total_params = 0 + for i in range(1, len(out_channel_size)): + input_size = out_channel_size[i - 1] + out_size = out_channel_size[i] + kernel = kernel_sizes[i - 1] + bias = out_channel_size[i] + num_free_params = input_size * kernel * kernel * out_size + bias + total_params += num_free_params + print(f"Total number of free parameters: {total_params}") + + +if __name__ == "__main__": + # model name + nn = "ArghCNN_numConvLayers3_outChannels[8, 8, 8]_kernelSize[7, 15]_leaky relu_stride1_trainFirstConvLayerTrue_seed291857_Natural_1351Epoch_3107-2121.pt" + + calc_free_params(from_loaded_model=False, model_name=nn) diff --git a/thesis code/network analysis/minimal_architecture/README.txt b/thesis code/network analysis/minimal_architecture/README.txt new file mode 100644 index 0000000..02dd410 --- /dev/null +++ b/thesis code/network analysis/minimal_architecture/README.txt @@ -0,0 +1,18 @@ +Folder minimal_architecture: + +1. config.json: +* json file with all configurations and cnn parameters + +2. training_loop.sh: +* bash script to train the 64 cnns + + +3. get_trained_models: +* searches for the saved trained models in a directory +* chooses model based on the largest saved epoch in the save-name + + +4. pfinkel_performance_test64: +* load all models extracted by 'get_trained_models' +* test them on all stimulus conditions +* sort their performances either after number of free parameters, or architecture \ No newline at end of file diff --git a/thesis code/network analysis/minimal_architecture/config.json b/thesis code/network analysis/minimal_architecture/config.json new file mode 100644 index 0000000..4c84655 --- /dev/null +++ b/thesis code/network analysis/minimal_architecture/config.json @@ -0,0 +1,368 @@ +{ + "data_path": "/home/kk/Documents/Semester4/code/RenderStimuli/Output/", + "save_logging_messages": true, // (true), false + "display_logging_messages": true, // (true), false + "batch_size_train": 500, + "batch_size_test": 250, + "max_epochs": 2000, + "save_model": true, + "conv_0_kernel_size": 11, + "mp_1_kernel_size": 3, + "mp_1_stride": 2, + "use_plot_intermediate": true, // true, (false) + "stimuli_per_pfinkel": 10000, + "num_pfinkel_start": 0, + "num_pfinkel_stop": 100, + "num_pfinkel_step": 10, + "precision_100_percent": 0, // (4) + "train_first_layer": true, // true, (false) + "save_ever_x_epochs": 100, // (10) + "activation_function": "leaky relu", // tanh, relu, (leaky relu), none + "leak_relu_negative_slope": 0.1, // (0.1) + "switch_leakyR_to_relu": false, + // LR Scheduler -> + "use_scheduler": true, // (true), false + "scheduler_verbose": true, + "scheduler_factor": 0.1, //(0.1) + "scheduler_patience": 10, // (10) + "scheduler_threshold": 1e-5, // (1e-4) + "minimum_learning_rate": 1e-8, + "learning_rate": 0.0001, + // <- LR Scheduler + "pooling_type": "max", // (max), average, none + "conv_0_enable_softmax": false, // true, (false) + "use_adam": true, // (true) => adam, false => SGD + "condition": "Natural", + "scale_data": 255.0, // (255.0) + "conv_out_channels_list": [ + [ + 8, + 8, + 8 + ], + [ + 8, + 8, + 6 + ], + [ + 8, + 8, + 4 + ], + [ + 8, + 8, + 2 + ], + [ + 8, + 6, + 8 + ], + [ + 8, + 6, + 6 + ], + [ + 8, + 6, + 4 + ], + [ + 8, + 6, + 2 + ], + [ + 8, + 4, + 8 + ], + [ + 8, + 4, + 6 + ], + [ + 8, + 4, + 4 + ], + [ + 8, + 4, + 2 + ], + [ + 8, + 2, + 8 + ], + [ + 8, + 2, + 6 + ], + [ + 8, + 2, + 4 + ], + [ + 8, + 2, + 2 + ], + [ + 6, + 8, + 8 + ], + [ + 6, + 8, + 6 + ], + [ + 6, + 8, + 4 + ], + [ + 6, + 8, + 2 + ], + [ + 6, + 6, + 8 + ], + [ + 6, + 6, + 6 + ], + [ + 6, + 6, + 4 + ], + [ + 6, + 6, + 2 + ], + [ + 6, + 4, + 8 + ], + [ + 6, + 4, + 6 + ], + [ + 6, + 4, + 4 + ], + [ + 6, + 4, + 2 + ], + [ + 6, + 2, + 8 + ], + [ + 6, + 2, + 6 + ], + [ + 6, + 2, + 4 + ], + [ + 6, + 2, + 2 + ], + [ + 4, + 8, + 8 + ], + [ + 4, + 8, + 6 + ], + [ + 4, + 8, + 4 + ], + [ + 4, + 8, + 2 + ], + [ + 4, + 6, + 8 + ], + [ + 4, + 6, + 6 + ], + [ + 4, + 6, + 4 + ], + [ + 4, + 6, + 2 + ], + [ + 4, + 4, + 8 + ], + [ + 4, + 4, + 6 + ], + [ + 4, + 4, + 4 + ], + [ + 4, + 4, + 2 + ], + [ + 4, + 2, + 8 + ], + [ + 4, + 2, + 6 + ], + [ + 4, + 2, + 4 + ], + [ + 4, + 2, + 2 + ], + [ + 2, + 8, + 8 + ], + [ + 2, + 8, + 6 + ], + [ + 2, + 8, + 4 + ], + [ + 2, + 8, + 2 + ], + [ + 2, + 6, + 8 + ], + [ + 2, + 6, + 6 + ], + [ + 2, + 6, + 4 + ], + [ + 2, + 6, + 2 + ], + [ + 2, + 4, + 8 + ], + [ + 2, + 4, + 6 + ], + [ + 2, + 4, + 4 + ], + [ + 2, + 4, + 2 + ], + [ + 2, + 2, + 8 + ], + [ + 2, + 2, + 6 + ], + [ + 2, + 2, + 4 + ], + [ + 2, + 2, + 2 + ] + ], + "conv_kernel_sizes": [ + [ + 7, + 15 + ] + ], + "conv_stride_sizes": [ + 1 + ] +} \ No newline at end of file diff --git a/thesis code/network analysis/minimal_architecture/get_trained_models.py b/thesis code/network analysis/minimal_architecture/get_trained_models.py new file mode 100644 index 0000000..b150ff6 --- /dev/null +++ b/thesis code/network analysis/minimal_architecture/get_trained_models.py @@ -0,0 +1,55 @@ +import glob +import os +import re +import shutil + +""" +get performances from .pt files +""" + +directory = "./trained_models" +string = "Natural" +final_path = "./trained_corners" + + +# list of all files in the directory +files = glob.glob(directory + "/*.pt") + +# filter +filtered_files = [f for f in files if string in f] + +# group by seed +seed_files = {} +for f in filtered_files: + # get seed from filename + match = re.search(r"_seed(\d+)_", f) + if match: + seed = int(match.group(1)) + if seed not in seed_files: + seed_files[seed] = [] + seed_files[seed].append(f) + + +# get saved cnn largests epoch +newest_files = {} +for seed, files in seed_files.items(): + max_epoch = -1 + newest_file = None + for f in files: + # search for epoch + match = re.search(r"_(\d+)Epoch_", f) + if match: + epoch = int(match.group(1)) + if epoch > max_epoch: + max_epoch = epoch + newest_file = f + newest_files[seed] = newest_file + +print(len(newest_files)) + +# move files to new folder +os.makedirs(final_path, exist_ok=True) + +# Copy the files to the new folder +for seed, file in newest_files.items(): + shutil.copy(file, os.path.join(final_path, os.path.basename(file))) diff --git a/thesis code/network analysis/minimal_architecture/pfinkel_performance_test64.py b/thesis code/network analysis/minimal_architecture/pfinkel_performance_test64.py new file mode 100644 index 0000000..03927ec --- /dev/null +++ b/thesis code/network analysis/minimal_architecture/pfinkel_performance_test64.py @@ -0,0 +1,282 @@ +import torch +import numpy as np +import matplotlib.pyplot as plt +import matplotlib as mpl +import os +import datetime +import re + +# import glob +# from natsort import natsorted + +mpl.rcParams["text.usetex"] = True +mpl.rcParams["font.family"] = "serif" + +from functions.alicorn_data_loader import alicorn_data_loader +from functions.create_logger import create_logger + + +def sort_and_plot( + extracted_params, + save: bool, + plot_for_each_condition: bool, + name: str, + sort_by="params", +): + figure_path: str = "performance_pfinkel_0210" + os.makedirs(figure_path, exist_ok=True) + + architecture_params = extracted_params.copy() + if sort_by == "params": + architecture_params.sort(key=lambda x: x[1]) + elif sort_by == "accuracy": + architecture_params.sort(key=lambda x: x[-1]) + + sorted_architectures, sorted_params, test_conditions, sorted_performances = zip( + *architecture_params + ) + final_labels = [ + f"{arch[1:-1]} - {params}" + for arch, params in zip(sorted_architectures, sorted_params) + ] + + plt.figure(figsize=(18, 9)) + + # performance for each condition + if plot_for_each_condition: + conditions = ["Coignless", "Natural", "Angular"] + labels = ["Classic", "Corner", "Bridge"] + shift_amounts = [-0.05, 0, 0.05] + save_name = name + "_each_condition" + for i, condition in enumerate(conditions): + # x_vals = range(len(sorted_performances)) + jittered_x = np.arange(len(sorted_performances)) + shift_amounts[i] + y_vals = [perf[condition] for perf in test_conditions] + plt.errorbar( + jittered_x, + y_vals, + fmt="D", + markerfacecolor="none", + markeredgewidth=1.5, + label=labels[i], + ) + else: + save_name = name + "_mean" + plt.plot(range(len(sorted_performances)), sorted_performances, marker="o") + + plt.ylabel("Accuracy (in \\%)", fontsize=17) + plt.xticks(range(len(sorted_performances)), final_labels, rotation=90, fontsize=15) + plt.yticks(fontsize=16) + plt.grid(True) + plt.tight_layout() + plt.legend(fontsize=15) + + if save: + plt.savefig( + os.path.join( + figure_path, + f"minimalCNN_64sorted_{sort_by}_{save_name}.pdf", + ), + dpi=300, + bbox_inches="tight", + ) + plt.show() + + +if __name__ == "__main__": + training_con: str = "classic" + model_path: str = "./trained_classic" + print(model_path) + data_path: str = "/home/kk/Documents/Semester4/code/RenderStimuli/Output/" + + # num stimuli per Pfinkel and batch size + stim_per_pfinkel: int = 10000 + batch_size: int = 1000 + + # stimulus condition: + performances_list: list = [] + condition: list[str] = ["Coignless", "Natural", "Angular"] + + # load test data: + num_pfinkel: list = np.arange(0, 100, 10).tolist() + image_scale: float = 255.0 + + # ------------------------------------------ + + # create logger: + logger = create_logger( + save_logging_messages=False, + display_logging_messages=True, + model_name=model_path, + ) + + device_str: str = "cuda:0" if torch.cuda.is_available() else "cpu" + logger.info(f"Using {device_str} device") + device: torch.device = torch.device(device_str) + torch.set_default_dtype(torch.float32) + + # current time: + current = datetime.datetime.now().strftime("%d%m-%H%M") + + # save data + cnn_data: list = [] + cnn_counter: int = 0 + + for filename in os.listdir(model_path): + if filename.endswith(".pt"): + model_filename = os.path.join(model_path, filename) + model = torch.load(model_filename, map_location=device) + model.eval() + print(f"CNN {cnn_counter+1} :{model_filename}") + + # number free parameters for current CNN + num_free_params = sum( + p.numel() for p in model.parameters() if p.requires_grad + ) + + # save + all_performances: dict = { + condition_name: {pfinkel: [] for pfinkel in num_pfinkel} + for condition_name in condition + } + + for selected_condition in condition: + # save performances: + logger.info(f"Condition: {selected_condition}") + performances: dict = {} + for pfinkel in num_pfinkel: + test_loss: float = 0.0 + correct: int = 0 + pattern_count: int = 0 + + data_test = alicorn_data_loader( + num_pfinkel=[pfinkel], + load_stimuli_per_pfinkel=stim_per_pfinkel, + condition=selected_condition, + logger=logger, + data_path=data_path, + ) + loader = torch.utils.data.DataLoader( + data_test, shuffle=False, batch_size=batch_size + ) + + # start testing network on new stimuli: + logger.info("") + logger.info( + f"-==- Start {selected_condition} " f"Pfinkel {pfinkel}° -==-" + ) + with torch.no_grad(): + for batch_num, data in enumerate(loader): + label = data[0].to(device) + image = data[1].type(dtype=torch.float32).to(device) + image /= image_scale + + # compute prediction error; + output = model(image) + + # Label Typecast: + label = label.to(device) + + # loss and optimization + loss = torch.nn.functional.cross_entropy( + output, label, reduction="sum" + ) + pattern_count += int(label.shape[0]) + test_loss += float(loss) + prediction = output.argmax(dim=1) + correct += prediction.eq(label).sum().item() + + total_number_of_pattern: int = int(len(loader)) * int( + label.shape[0] + ) + + # logging: + logger.info( + ( + f"{selected_condition},{pfinkel}° " + "Pfinkel: " + f"[{int(pattern_count)}/{total_number_of_pattern} ({100.0 * pattern_count / total_number_of_pattern:.2f}%)]," + f" Average loss: {test_loss / pattern_count:.3e}, " + "Accuracy: " + f"{100.0 * correct / pattern_count:.2f}% " + ) + ) + + performances[pfinkel] = { + "pfinkel": pfinkel, + "test_accuracy": 100 * correct / pattern_count, + "test_losses": float(loss) / pattern_count, + } + all_performances[selected_condition][pfinkel].append( + 100 * correct / pattern_count + ) + + performances_list.append(performances) + + # store num free params + performances + avg_performance_per_condition = { + cond: np.mean([np.mean(perfs) for perfs in pfinkel_dict.values()]) + for cond, pfinkel_dict in all_performances.items() + } + avg_performance_overall = np.mean( + list(avg_performance_per_condition.values()) + ) + + # extract CNN config: + match = re.search(r"_outChannels\[(\d+), (\d+), (\d+)\]_", filename) + if match: + out_channels = ( + [1] + [int(match.group(i)) for i in range(1, 3 + 1)] + [2] + ) + + # number of free parameters and performances + cnn_data.append( + ( + out_channels, + num_free_params, + avg_performance_per_condition, + avg_performance_overall, + ) + ) + + else: + print("No files found!") + break + + # save all 64 performances + torch.save( + cnn_data, + f"{model_path}.pt", + ) + + # plot + sort_and_plot( + cnn_data, + save=True, + plot_for_each_condition=True, + name=training_con, + sort_by="params", + ) + sort_and_plot( + cnn_data, + save=True, + plot_for_each_condition=False, + name=training_con, + sort_by="params", + ) + sort_and_plot( + cnn_data, + save=True, + plot_for_each_condition=True, + name=training_con, + sort_by="accuracy", + ) + sort_and_plot( + cnn_data, + save=True, + plot_for_each_condition=False, + name=training_con, + sort_by="accuracy", + ) + + logger.info("-==- DONE -==-") diff --git a/thesis code/network analysis/minimal_architecture/training_loop.sh b/thesis code/network analysis/minimal_architecture/training_loop.sh new file mode 100644 index 0000000..fc73dde --- /dev/null +++ b/thesis code/network analysis/minimal_architecture/training_loop.sh @@ -0,0 +1,11 @@ +Directory="/home/kk/Documents/Semester4/code/Run64Variations" +Priority="0" +echo $Directory +mkdir $Directory/argh_log_corner +for out_channels_idx in {0..63}; do + for kernel_size_idx in {0..0}; do + for stride_idx in {0..0}; do + echo "hostname; cd $Directory ; /home/kk/P3.10/bin/python3 cnn_training.py --idx-conv-out-channels-list $out_channels_idx --idx-conv-kernel-sizes $kernel_size_idx --idx-conv-stride-sizes $stride_idx -s \$JOB_ID" | qsub -o $Directory/argh_log_classic -j y -p $Priority -q gp4u,gp3u -N itsCorn + done + done +done diff --git a/thesis code/network analysis/optimal_stimulus/README.txt b/thesis code/network analysis/optimal_stimulus/README.txt new file mode 100644 index 0000000..b35910c --- /dev/null +++ b/thesis code/network analysis/optimal_stimulus/README.txt @@ -0,0 +1,8 @@ +Folder optimal_stimulus + +1. optimal_stimulus: +* for single trained model +* generates optimal stimulus for neuron in selected layer + +2. optimal_stimulus_20cnns: +* generates stimulus for neuron in same layer of all 20 cnns \ No newline at end of file diff --git a/thesis code/network analysis/optimal_stimulus/optimal_stimulus.py b/thesis code/network analysis/optimal_stimulus/optimal_stimulus.py new file mode 100644 index 0000000..7e858b5 --- /dev/null +++ b/thesis code/network analysis/optimal_stimulus/optimal_stimulus.py @@ -0,0 +1,219 @@ +# %% +import torch +import random +import re +import matplotlib.pyplot as plt +import matplotlib.patches as patch +import matplotlib as mpl + +mpl.rcParams["text.usetex"] = True +mpl.rcParams["font.family"] = "serif" + +import os +import sys + +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(parent_dir) +from functions.analyse_network import analyse_network +from functions.set_seed import set_seed + +# define parameters +num_iterations: int = 100000 +learning_rate: float = 0.1 +apply_input_mask: bool = True +mark_region_in_plot: bool = True +sheduler_patience: int = 500 +sheduler_factor: float = 0.9 +sheduler_eps = 1e-08 +target_image_active: float = 1e4 +random_seed = random.randint(0, 100) +save_final: bool = True +model_str: str = "CORNER_888" + +# set seet +set_seed(random_seed) +print(f"Random seed: {random_seed}") + +# path to NN +condition: str = "corner_888_poster" +pattern = r"seed\d+_Natural_\d+Epoch" +nn = "ArghCNN_numConvLayers3_outChannels[8, 8, 8]_kernelSize[7, 15]_leaky relu_stride1_trainFirstConvLayerTrue_seed291857_Natural_1351Epoch_3107-2121.pt" +PATH = f"./trained_models/{nn}" +device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + +# %% +# load and eval model +model = torch.load(PATH).to(device) +model.eval() +print("Full network:") +print(model) +print("") + + +# enter index to plot: +idx = int(input("Please select layer: ")) +print(f"Selected layer: {model[idx]}") +assert idx < len(model) +model = model[: idx + 1] + +# random input +input_img = torch.rand(1, 200, 200).to(device) +input_img = input_img.unsqueeze(0) +input_img.requires_grad_(True) # type: ignore +print(input_img.min(), input_img.max()) + +input_shape = input_img.shape +assert input_shape[-2] == input_shape[-1] +coordinate_list, layer_type_list, pixel_used = analyse_network( + model=model, input_shape=int(input_shape[-1]) +) + + +output_shape = model(input_img).shape + + +target_image = torch.zeros( + (*output_shape,), dtype=input_img.dtype, device=input_img.device +) + + +# image to parameter (2B optimized) +input_parameter = torch.nn.Parameter(input_img) + + +if len(target_image.shape) == 2: + print((f"Available max positions: f:{target_image.shape[1] - 1} ")) + + # select neuron and plot for all feature maps (?) + neuron_f = int(input("Please select neuron_f: ")) + print(f"Selected neuron {neuron_f}") + target_image[0, neuron_f] = 1e4 +else: + print( + ( + f"Available max positions: f:{target_image.shape[1] - 1} " + f"x:{target_image.shape[2]} y:{target_image.shape[3]}" + ) + ) + + # select neuron and plot for all feature maps (?) + neuron_f = int(input("Please select neuron_f: ")) + neuron_x = target_image.shape[2] // 2 + neuron_y = target_image.shape[3] // 2 + print(f"Selected neuron {neuron_f}, {neuron_x}, {neuron_y}") + target_image[0, neuron_f, neuron_x, neuron_y] = target_image_active + + # Input mask -> + active_input_x = coordinate_list[-1][:, neuron_x].clone() + active_input_y = coordinate_list[-1][:, neuron_y].clone() + + input_mask: torch.Tensor = torch.zeros_like(input_img) + + input_mask[ + :, + :, + active_input_x.type(torch.int64).unsqueeze(-1), + active_input_y.type(torch.int64).unsqueeze(0), + ] = 1 + + rect_x = [int(active_input_x.min()), int(active_input_x.max())] + rect_y = [int(active_input_y.min()), int(active_input_y.max())] + # <- Input mask + + if apply_input_mask: + with torch.no_grad(): + input_img *= input_mask + + +optimizer = torch.optim.Adam([{"params": input_parameter}], lr=learning_rate) + +scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( + optimizer, + patience=sheduler_patience, + factor=sheduler_factor, + eps=sheduler_eps * 0.1, +) + + +counter: int = 0 +while (optimizer.param_groups[0]["lr"] > sheduler_eps) and (counter < num_iterations): + optimizer.zero_grad() + + output = model(input_parameter) + + loss = torch.nn.functional.mse_loss(output, target_image) + loss.backward() + + if counter % 1000 == 0: + print( + f"{counter} : loss={float(loss):.3e} lr={optimizer.param_groups[0]['lr']:.3e}" + ) + + optimizer.step() + + if apply_input_mask and len(target_image.shape) != 2: + with torch.no_grad(): + input_parameter.data[torch.where(input_mask == 0)] = 0.0 + + with torch.no_grad(): + max_data = torch.abs(input_parameter.data).max() + if max_data > 1.0: + input_parameter.data /= max_data + + if ( + torch.isfinite(input_parameter.data).sum().cpu() + != torch.tensor(input_parameter.data.size()).prod() + ): + print(f"Found NaN in step: {counter}, use a smaller initial lr") + exit() + + scheduler.step(float(loss)) + counter += 1 + +# save image +if save_final: + # get short model name: + matches = re.findall(pattern, nn) + model_short = "".join(["".join(match) for match in matches]) + save_name = ( + f"optimal_model{model_short}_layer{idx}_feature{neuron_f}_seed{random_seed}.pt" + ) + + # filepath: + folderpath = f"./other_{condition}_optimal" + os.makedirs(folderpath, exist_ok=True) + torch.save(input_img.squeeze().detach().cpu(), os.path.join(folderpath, save_name)) + +# plot image: +_, ax = plt.subplots() + +ax.imshow(input_img.squeeze().detach().cpu().numpy(), cmap="gray") + +plt.yticks(fontsize=15) +plt.xticks(fontsize=15) + + +if len(target_image.shape) != 2 and mark_region_in_plot: + edgecolor = "sienna" + kernel = patch.Rectangle( + (rect_y[0], rect_x[0]), + int(rect_y[1] - rect_y[0]), + int(rect_x[1] - rect_x[0]), + linewidth=1.2, + edgecolor=edgecolor, + facecolor="none", + ) + ax.add_patch(kernel) + +figure_path = f"./other_{condition}_optimal" +os.makedirs(figure_path, exist_ok=True) +plt.savefig( + os.path.join( + figure_path, + f"{save_name}_{model_str}.pdf", + ), + dpi=300, + bbox_inches="tight", +) + +plt.show(block=True) diff --git a/thesis code/network analysis/optimal_stimulus/optimal_stimulus_20cnns.py b/thesis code/network analysis/optimal_stimulus/optimal_stimulus_20cnns.py new file mode 100644 index 0000000..25e3aeb --- /dev/null +++ b/thesis code/network analysis/optimal_stimulus/optimal_stimulus_20cnns.py @@ -0,0 +1,293 @@ +# %% +import torch +import numpy as np +import random +import re +import matplotlib.pyplot as plt +import matplotlib.patches as patch +import matplotlib as mpl + +mpl.rcParams["text.usetex"] = True +mpl.rcParams["font.family"] = "serif" +mpl.rcParams["font.size"] = 15 + +import os +import sys + +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(parent_dir) +from functions.analyse_network import analyse_network +from functions.set_seed import set_seed + +# set seet +random_seed = random.randint(0, 100) +set_seed(random_seed) +print(f"Random seed: {random_seed}") + + +def get_file_list_all_cnns(dir: str) -> list: + all_results: list = [] + for filename in os.listdir(dir): + if filename.endswith(".pt"): + print(os.path.join(dir, filename)) + all_results.append(os.path.join(dir, filename)) + + return all_results + + +def show_single_optimal_stimulus(model_list, save: bool = False, cnn: str = "CORNER"): + first_run: bool = True + chosen_layer_idx: int + chosen_neuron_f_idx: int + chosen_neuron_x_idx: int + chosen_neuron_y_idx: int + mean_opt_stim_list: list = [] + fig, axs = plt.subplots(4, 5, figsize=(15, 15)) + for i, load_model in enumerate(model_list): + print(f"\nModel: {i} ") + num_iterations: int = 100000 + learning_rate: float = 0.1 + apply_input_mask: bool = True + mark_region_in_plot: bool = True + sheduler_patience: int = 500 + sheduler_factor: float = 0.9 + sheduler_eps = 1e-08 + target_image_active: float = 1e4 + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + # load model + model = torch.load(load_model).to(device) + model.eval() + + if first_run: + print("Full network:") + print(model) + print("") + + # enter index to plot: + idx = int(input("Please select layer: ")) + assert idx < len(model) + chosen_layer_idx = idx + + print(f"Selected layer: {model[chosen_layer_idx]}") + model = model[: chosen_layer_idx + 1] + + # prepare random input image + input_img = torch.rand(1, 200, 200).to(device) + input_img = input_img.unsqueeze(0) + input_img.requires_grad_(True) # type: ignore + + input_shape = input_img.shape + assert input_shape[-2] == input_shape[-1] + coordinate_list, layer_type_list, pixel_used = analyse_network( + model=model, input_shape=int(input_shape[-1]) + ) + + output_shape = model(input_img).shape + + target_image = torch.zeros( + (*output_shape,), dtype=input_img.dtype, device=input_img.device + ) + + # image to parameter (2B optimized) + input_parameter = torch.nn.Parameter(input_img) + + # back to first run: + if first_run: + if len(target_image.shape) == 2: + print((f"Available max positions: f:{target_image.shape[1] - 1} ")) + + # select neuron and plot for all feature maps (?) + neuron_f = int(input("Please select neuron_f: ")) + print(f"Selected neuron {neuron_f}") + chosen_neuron_f_idx = neuron_f + else: + print( + ( + f"Available max positions: f:{target_image.shape[1] - 1} " + f"x:{target_image.shape[2]} y:{target_image.shape[3]}" + ) + ) + + # select neuron and plot for all feature maps (?) + neuron_f = int(input("Please select neuron_f: ")) + neuron_x = target_image.shape[2] // 2 + neuron_y = target_image.shape[3] // 2 + chosen_neuron_f_idx = neuron_f + chosen_neuron_x_idx = neuron_x + chosen_neuron_y_idx = neuron_y + print( + f"Selected neuron {chosen_neuron_f_idx}, {chosen_neuron_x_idx}, {chosen_neuron_y_idx}" + ) + + # keep settings for further runs: + first_run = False + + # keep input values for all cnns + if len(target_image.shape) == 2: + target_image[0, chosen_neuron_f_idx] = 1e4 + else: + target_image[ + 0, chosen_neuron_f_idx, chosen_neuron_x_idx, chosen_neuron_y_idx + ] = target_image_active + + # Input mask -> + active_input_x = coordinate_list[-1][:, neuron_x].clone() + active_input_y = coordinate_list[-1][:, neuron_y].clone() + + input_mask: torch.Tensor = torch.zeros_like(input_img) + + input_mask[ + :, + :, + active_input_x.type(torch.int64).unsqueeze(-1), + active_input_y.type(torch.int64).unsqueeze(0), + ] = 1 + + rect_x = [int(active_input_x.min()), int(active_input_x.max())] + rect_y = [int(active_input_y.min()), int(active_input_y.max())] + # <- Input mask + + if apply_input_mask: + with torch.no_grad(): + input_img *= input_mask + + # start optimization: + optimizer = torch.optim.Adam([{"params": input_parameter}], lr=learning_rate) + + scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( + optimizer, + patience=sheduler_patience, + factor=sheduler_factor, + eps=sheduler_eps * 0.1, + ) + + counter: int = 0 + while (optimizer.param_groups[0]["lr"] > sheduler_eps) and ( + counter < num_iterations + ): + optimizer.zero_grad() + + output = model(input_parameter) + + loss = torch.nn.functional.mse_loss(output, target_image) + loss.backward() + + if counter % 1000 == 0: + print( + f"{counter} : loss={float(loss):.3e} lr={optimizer.param_groups[0]['lr']:.3e}" + ) + + optimizer.step() + + if apply_input_mask and len(target_image.shape) != 2: + with torch.no_grad(): + input_parameter.data[torch.where(input_mask == 0)] = 0.0 + + with torch.no_grad(): + max_data = torch.abs(input_parameter.data).max() + if max_data > 1.0: + input_parameter.data /= max_data + + if ( + torch.isfinite(input_parameter.data).sum().cpu() + != torch.tensor(input_parameter.data.size()).prod() + ): + print(f"Found NaN in step: {counter}, use a smaller initial lr") + exit() + + scheduler.step(float(loss)) + counter += 1 + mean_opt_stim_list.append(input_img.squeeze().detach().cpu().numpy()) + + # plot image: + ax = axs[i // 5, i % 5] + im = ax.imshow(input_img.squeeze().detach().cpu().numpy(), cmap="gray") + cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04) + ax.set_title(f"Model {i+1}", fontsize=13) + cbar.ax.tick_params(labelsize=12) + + if len(target_image.shape) != 2 and mark_region_in_plot: + edgecolor = "sienna" + kernel = patch.Rectangle( + (rect_y[0], rect_x[0]), + int(rect_y[1] - rect_y[0]), + int(rect_x[1] - rect_x[0]), + linewidth=1.2, + edgecolor=edgecolor, + facecolor="none", + ) + ax.add_patch(kernel) + + plt.tight_layout() + # save image + if save: + save_name = f"single_optimal_stimulus_{cnn}_layer{chosen_layer_idx}_feature{chosen_neuron_f_idx}" + folderpath = "./all20_optimal_stimuli" + os.makedirs(folderpath, exist_ok=True) + torch.save( + input_img.squeeze().detach().cpu(), + os.path.join(folderpath, save_name) + ".pt", + ) + plt.savefig( + f"{os.path.join(folderpath, save_name)}.pdf", + dpi=300, + bbox_inches="tight", + ) + + plt.show(block=True) + + if len(target_image.shape) == 2: + return mean_opt_stim_list, chosen_neuron_f_idx, chosen_layer_idx + else: + return ( + mean_opt_stim_list, + (chosen_layer_idx, chosen_neuron_f_idx), + (chosen_neuron_x_idx, chosen_neuron_y_idx), + ) + + +def plot_mean_optimal_stimulus( + overall_optimal_stimuli, + chosen_layer_idx: int, + chosen_neuron_f_idx: int, + save: bool = False, + cnn: str = "CORNER", +): + fig, axs = plt.subplots(figsize=(15, 15)) + mean_optimal_stimulus = np.mean(overall_optimal_stimuli, axis=0) + im = axs.imshow(mean_optimal_stimulus, cmap="gray") + cbar = fig.colorbar(im, ax=axs, fraction=0.046, pad=0.04) + cbar.ax.tick_params(labelsize=15) + + plt.tight_layout() + # save image + if save: + save_name = f"overall_mean_optimal_stimulus_{cnn}_layer{chosen_layer_idx}_feature{chosen_neuron_f_idx}" + folderpath = "./mean_optimal_stimulus" + os.makedirs(folderpath, exist_ok=True) + torch.save(mean_optimal_stimulus, os.path.join(folderpath, save_name) + ".pt") + plt.savefig( + f"{os.path.join(folderpath, save_name)}.pdf", + dpi=300, + ) + + plt.show(block=True) + + +if __name__ == "__main__": + # path to NN + PATH_corner = "./classic_3288_fest" + all_cnns_corner = get_file_list_all_cnns(dir=PATH_corner) + opt_stim_list, feature_idx, layer_idx = show_single_optimal_stimulus( + all_cnns_corner, save=True, cnn="CLASSIC_3288_fest" + ) + + # average optimal stimulus: + # plot_mean_optimal_stimulus( + # opt_stim_list, + # save=True, + # cnn="CORNER_3288_fest", + # chosen_layer_idx=layer_idx, + # chosen_neuron_f_idx=feature_idx, + # ) diff --git a/thesis code/network analysis/orientation_tuning/README.txt b/thesis code/network analysis/orientation_tuning/README.txt new file mode 100644 index 0000000..ab1f47d --- /dev/null +++ b/thesis code/network analysis/orientation_tuning/README.txt @@ -0,0 +1,14 @@ +Folder orientation_tuning: + + +1. orientation_tuning_curve: +* generates the original tuning curve by convolving the Gabor patches with the weight matrices of C1 +* Gabor patches file: gabor_dict_32o_8p.py + +2. fitkarotte: +* implements the fitting procedure of the 3 von Mises functions +* plots the fitted tuning curves + +3. fit_statistics: +* contains all statistical test for the 20 trained CNNs of each stimulus condition +* calls the 'plot_fit_statistics' function to plot the data \ No newline at end of file diff --git a/thesis code/network analysis/orientation_tuning/fit_statistics.py b/thesis code/network analysis/orientation_tuning/fit_statistics.py new file mode 100644 index 0000000..e3d6bca --- /dev/null +++ b/thesis code/network analysis/orientation_tuning/fit_statistics.py @@ -0,0 +1,475 @@ +import numpy as np +import fitkarotte +from orientation_tuning_curve import load_data_from_cnn # noqa +import plot_fit_statistics +import warnings +from scipy.stats import ranksums + +# suppress warnings +warnings.filterwarnings("ignore") + + +def get_general_data_info(data, print_mises_all_cnn: bool): + num_cnns = len(data) + num_weights_per_cnn = [len(cnn_results) for cnn_results in data] + + num_fits_per_cnn = {1: [0] * num_cnns, 2: [0] * num_cnns, 3: [0] * num_cnns} + + for idx, cnn_results in enumerate(data): + for _, fit in cnn_results: + curve = fit["curve"] + num_fits_per_cnn[curve][idx] += 1 + + print("\n\nNumber of CNNs:", num_cnns) + print("Number of weights saved for each CNN:", num_weights_per_cnn) + print("Number of fits with 1 von Mises function per CNN:", num_fits_per_cnn[1]) + print("Number of fits with 2 von Mises functions per CNN:", num_fits_per_cnn[2]) + print("Number of fits with 3 von Mises functions per CNN:", num_fits_per_cnn[3]) + + # mean and stdev 4each type of fit + mean_1_mises = np.mean(num_fits_per_cnn[1]) + std_1_mises = np.std(num_fits_per_cnn[1]) + mean_2_mises = np.mean(num_fits_per_cnn[2]) + std_2_mises = np.std(num_fits_per_cnn[2]) + mean_3_mises = np.mean(num_fits_per_cnn[3]) + std_3_mises = np.std(num_fits_per_cnn[3]) + + print( + f"Mean number of fits with 1 von Mises function: {mean_1_mises:.2f} (std: {std_1_mises:.2f})" + ) + print( + f"Mean number of fits with 2 von Mises functions: {mean_2_mises:.2f} (std: {std_2_mises:.2f})" + ) + print( + f"Mean number of fits with 3 von Mises functions: {mean_3_mises:.2f} (std: {std_3_mises:.2f})" + ) + + if print_mises_all_cnn: + print("--================================--") + for idx_cnn, (num_1_mises, num_2_mises, num_3_mises) in enumerate( + zip(num_fits_per_cnn[1], num_fits_per_cnn[2], num_fits_per_cnn[3]) + ): + print( + f"CNN {idx_cnn+1}:\t# 1 Mises: {num_1_mises},\t# 2 Mises: {num_2_mises},\t# 3 Mises: {num_3_mises}" + ) + + return ( + num_fits_per_cnn, + mean_1_mises, + mean_2_mises, + mean_3_mises, + std_1_mises, + std_2_mises, + std_3_mises, + ) + + +def ratio_amplitude_two_mises(data, mean_std: bool = False): + """ + * This function calculates the mean ratio of those weights + of the first layer, which were fitted with 2 von Mises functions + * It first calculates the mean ratio for every single CNN + (of the overall 20 CNNs) + * It then computes the overall mean ratio for the weights + of all 20 CNNs that were fitted with 2 von Mises functions + """ + num_cnns = len(data) + mean_ratio_per_cnn = [0] * num_cnns + + for idx, cnn_results in enumerate(data): + ratio_list: list = [] + count_num_2mises: int = 0 + for _, fit in cnn_results: + curve = fit["curve"] + if curve == 2 and fit["fit_params"] is not None: + count_num_2mises += 1 + first_amp = fit["fit_params"][0] + sec_amp = fit["fit_params"][3] + + if sec_amp < first_amp: + ratio = sec_amp / first_amp + else: + ratio = first_amp / sec_amp + + if not (ratio > 1.0 or ratio < 0): + ratio_list.append(ratio) + else: + print(f"\nRATIO OUT OF RANGE FOR: CNN:{idx}, weight{_}\n") + + # print(f"CNN [{idx}]: num fits with 2 von mises = {count_num_2mises}") + mean_ratio_per_cnn[idx] = np.mean(ratio_list) + + # calc mean difference over all 20 CNNs: + if mean_std: + mean_all_cnns = np.mean(mean_ratio_per_cnn) + std_all_cnns = np.std(mean_ratio_per_cnn) + print("\n-=== Mean ratio between 2 amplitudes ===-") + print(f"Mean ratio of all {len(mean_ratio_per_cnn)} CNNs: {mean_all_cnns}") + print(f"Stdev of ratio of all {len(mean_ratio_per_cnn)} CNNs: {std_all_cnns}") + + return mean_all_cnns, std_all_cnns + + else: # get median and percentile + percentiles = np.percentile(mean_ratio_per_cnn, [10, 25, 50, 75, 90]) + + print("\n-=== Percentiles of ratio between 2 amplitudes ===-") + print(f"10th Percentile: {percentiles[0]}") + print(f"25th Percentile: {percentiles[1]}") + print(f"Median (50th Percentile): {percentiles[2]}") + print(f"75th Percentile: {percentiles[3]}") + print(f"90th Percentile: {percentiles[4]}") + + # return mean_all_cnns, std_all_cnns + return percentiles[2], (percentiles[1], percentiles[3]) + + +def ratio_amplitude_three_mises(data, mean_std: bool = False): + """ + * returns: mean21, std21, mean32, std32 + * This function calculates the mean ratio of those weights + of the first layer, which were fitted with 2 von Mises functions + * It first calculates the mean ratio for every single CNN + (of the overall 20 CNNs) + * It then computes the overall mean ratio for the weights + of all 20 CNNs that were fitted with 2 von Mises functions + """ + num_cnns = len(data) + mean_ratio_per_cnn21 = [0] * num_cnns + mean_ratio_per_cnn32 = [0] * num_cnns + + for idx, cnn_results in enumerate(data): + ratio_list21: list = [] + ratio_list32: list = [] + count_num_2mises: int = 0 + for _, fit in cnn_results: + curve = fit["curve"] + if curve == 3 and fit["fit_params"] is not None: + count_num_2mises += 1 + first_amp = fit["fit_params"][0] + sec_amp = fit["fit_params"][3] + third_amp = fit["fit_params"][6] + + if sec_amp < first_amp: + ratio21 = sec_amp / first_amp + else: + ratio21 = first_amp / sec_amp + + if third_amp < sec_amp: + ratio32 = third_amp / sec_amp + else: + ratio32 = sec_amp / third_amp + + if not (ratio21 > 1.0 or ratio32 > 1.0 or ratio21 < 0 or ratio32 < 0): + ratio_list21.append(ratio21) + ratio_list32.append(ratio32) + else: + print(f"\nRATIO OUT OF RANGE FOR: CNN:{idx}, weight{_}\n") + + # print(f"CNN [{idx}]: num fits with 2 von mises = + # {count_num_2mises}") + if len(ratio_list21) != 0: + mean_ratio_per_cnn21[idx] = np.mean(ratio_list21) + mean_ratio_per_cnn32[idx] = np.mean(ratio_list32) + else: + mean_ratio_per_cnn21[idx] = None # type: ignore + mean_ratio_per_cnn32[idx] = None # type: ignore + + mean_ratio_per_cnn21 = [x for x in mean_ratio_per_cnn21 if x is not None] + mean_ratio_per_cnn32 = [x for x in mean_ratio_per_cnn32 if x is not None] + + # calc mean difference over all 20 CNNs: + + if mean_std: + mean_all_cnns21 = np.mean(mean_ratio_per_cnn21) + std_all_21 = np.std(mean_ratio_per_cnn21) + mean_all_cnns32 = np.mean(mean_ratio_per_cnn32) + std_all_32 = np.std(mean_ratio_per_cnn32) + + print("\n-=== Mean ratio between 3 preferred orienations ===-") + print(f"Ratio 2/1 of all {len(mean_ratio_per_cnn21)} CNNs: {mean_all_cnns21}") + print( + f"Stdev of ratio 2/1 of all {len(mean_ratio_per_cnn21)} CNNs: {std_all_21}" + ) + print(f"Ratio 3/2 of all {len(mean_ratio_per_cnn32)} CNNs: {mean_all_cnns32}") + print( + f"Stdev of ratio 3/2 of all {len(mean_ratio_per_cnn32)} CNNs: {std_all_32}" + ) + + return mean_all_cnns21, std_all_21, mean_all_cnns32, std_all_32 + + else: # get median and percentile: + percentiles_21 = np.percentile(mean_ratio_per_cnn32, [10, 25, 50, 75, 90]) + percentiles_32 = np.percentile(mean_ratio_per_cnn21, [10, 25, 50, 75, 90]) + + # get percentile 25 and 75 + percentile25_32 = percentiles_32[1] + percentile75_32 = percentiles_32[-2] + percentile25_21 = percentiles_21[1] + percentile75_21 = percentiles_21[-2] + + print("\n-=== Percentiles of ratio between 2 amplitudes ===-") + print(f"10th Percentile 3->2: {percentiles_32[0]}") + print(f"10th Percentile 2->1: {percentiles_21[0]}") + print(f"25th Percentile 3->2: {percentiles_32[1]}") + print(f"25th Percentile 2->1: {percentiles_21[1]}") + print(f"Median (50th Percentile 3->2): {percentiles_32[2]}") + print(f"Median (50th Percentile 2->1): {percentiles_21[2]}") + print(f"75th Percentile 3->2: {percentiles_32[3]}") + print(f"75th Percentile 2->1: {percentiles_21[3]}") + print(f"90th Percentile3->2: {percentiles_32[4]}") + print(f"90th Percentile 2->1: {percentiles_21[4]}") + + return ( + percentiles_21[2], + (percentile25_21, percentile75_21), + percentiles_32[2], + (percentile25_32, percentile75_32), + ) + + +def willy_is_not_whitney_test(data_classic, data_corner): + from scipy.stats import mannwhitneyu + + """ + * Test does not assume normal distribution + * Compares means between 2 indep groups + """ + + # call test + statistic, p_value = mannwhitneyu(data_classic, data_corner) + + # results + print("\nMann-Whitney U Test Statistic:", statistic) + print("Mann-Whitney U Test p-value:", p_value) + + # check significance: + print("Null-hypothesis: distributions are the same.") + alpha = 0.05 + if p_value < alpha: + print("The distributions are significantly different.") + else: + print("The distributions are not significantly different.") + + return p_value + + +def ks(data_classic, data_corner): + from scipy.stats import ks_2samp + + ks_statistic, p_value = ks_2samp(data_classic, data_corner) + + print("\nKolmogorov-Smirnov Test - p-value:", p_value) + print("Kolmogorov-Smirnov Test - ks_statistic:", ks_statistic) + alpha = 0.05 + if p_value < alpha: + print("The distributions for von Mises functions are significantly different.") + + return p_value + + +def shapiro(fits_per_mises, num_mises: int, alpha: float = 0.05): + """ + Tests if data has normal distribution + * 0-hyp: data is normally distributed + * low p-val: data not normally distributed + """ + from scipy.stats import shapiro + + statistic, p_value = shapiro(fits_per_mises) + print(f"\nShapiro-Wilk Test for {num_mises} von Mises function - p-val :", p_value) + print( + f"Shapiro-Wilk Test for {num_mises} von Mises function - statistic :", statistic + ) + + # set alpha + if p_value < alpha: + print("P-val < alpha. Reject 0-hypothesis. Data is not normally distributed") + else: + print("P-val > alpha. Keep 0-hypothesis. Data is normally distributed") + + return p_value + + +def agostino_pearson(fits_per_mises, num_mises: int, alpha: float = 0.05): + """ + Tests if data has normal distribution + * 0-hyp: data is normally distributed + * low p-val: data not normally distributed + """ + from scipy import stats + + statistic, p_value = stats.normaltest(fits_per_mises) + print( + f"\nD'Agostino-Pearson Test for {num_mises} von Mises function - p-val :", + p_value, + ) + print( + f"D'Agostino-Pearson Test for {num_mises} von Mises function - statistic :", + statistic, + ) + + # set alpha + if p_value < alpha: + print("P-val < alpha. Reject 0-hypothesis. Data is not normally distributed") + else: + print("P-val > alpha. Keep 0-hypothesis. Data is normally distributed") + + return p_value + + +if __name__ == "__main__": + num_thetas = 32 + dtheta = 2 * np.pi / num_thetas + theta = dtheta * np.arange(num_thetas) + threshold: float = 0.1 + + # to do statistics on corner + directory_corner: str = "D:/Katha/Neuroscience/Semester 4/newCode/kk_contour_net_shallow-main/corner_888" + all_results_corner = fitkarotte.analyze_cnns(dir=directory_corner) + + # analyze + print("-=== CORNER ===-") + # amplitude ratios + ratio_corner_21, std_corner_21 = ratio_amplitude_two_mises(data=all_results_corner) + ( + ratio_corner_321, + std_corner_321, + ratio_corner_332, + std_corner_332, + ) = ratio_amplitude_three_mises(data=all_results_corner) + + # general data + ( + corner_num_fits, + mean_corner_1, + mean_corner_2, + mean_corner_3, + std_corner_1, + std_corner_2, + std_corner_3, + ) = get_general_data_info(data=all_results_corner, print_mises_all_cnn=True) + # analyze_num_curve_fits(data=all_results_corner) + + # to do statistics: CLASSIC + directory_classic: str = "D:/Katha/Neuroscience/Semester 4/newCode/kk_contour_net_shallow-main/classic_888" + all_results_classic = fitkarotte.analyze_cnns(dir=directory_classic) + + # analyze + print("-=== CLASSIC ===-") + # amplitude ratio + ratio_classic_21, std_class_21 = ratio_amplitude_two_mises(data=all_results_classic) + ( + ratio_classic_321, + std_classic_321, + ratio_classic_332, + std_classic_332, + ) = ratio_amplitude_three_mises(data=all_results_classic) + + # general data + ( + classic_num_fits, + mean_classic_1, + mean_classic_2, + mean_classic_3, + std_classic_1, + std_classic_2, + std_classic_3, + ) = get_general_data_info(data=all_results_classic, print_mises_all_cnn=False) + # analyze_num_curve_fits(data=all_results_classic) + + print("################################") + print("-==== plotting hists: compare amplitude ratios ====-") + plot_fit_statistics.plot_mean_percentile_amplit_ratio( + ratio_classic_21=ratio_classic_21, + ratio_classic_321=ratio_classic_321, + ratio_classic_332=ratio_classic_332, + ratio_corner_21=ratio_corner_21, + ratio_corner_321=ratio_corner_321, + ratio_corner_332=ratio_corner_332, + percentile_classic21=std_class_21, + percentile_classic321=std_classic_321, + percentile_classic_332=std_classic_332, + percentile_corner_21=std_corner_21, + percentile_corner_321=std_corner_321, + percentile_corner_332=std_corner_332, + saveplot=True, + save_name="median_percentile_888", + ) + + # p-value < 0.05: statistically significant difference between your two samples + statistic21, pvalue21 = ranksums(ratio_classic_21, ratio_corner_21) + print( + f"Wilcoxon rank sum test 2 Mises for ratio 2->1: statistic={statistic21}, pvalue={pvalue21}" + ) + + statistic321, pvalue321 = ranksums(ratio_classic_321, ratio_corner_321) + print( + f"Wilcoxon rank sum test 3 Mises for ratio 2->1: statistic={statistic321}, pvalue={pvalue321}" + ) + + statistic332, pvalue332 = ranksums(ratio_classic_332, ratio_corner_332) + print( + f"Wilcoxon rank sum test 3 Mises for ratio 3->2: statistic={statistic332}, pvalue={pvalue332}" + ) + + print("-==== plotting hists: CORNER ====-") + # plot histogram + # plot_hist(corner_num_fits[1], num_mises=1) + # plot_hist(corner_num_fits[2], num_mises=2) + # plot_hist(corner_num_fits[3], num_mises=3) + + # test for normal distribution + print("-== Shapiro test ==- ") + # shapiro(corner_num_fits[1], num_mises=1) + # shapiro(corner_num_fits[2], num_mises=2) + # shapiro(corner_num_fits[3], num_mises=3) + + print("\n-== D'Agostino-Pearson test ==- ") + agostino_pearson(corner_num_fits[1], num_mises=1) + agostino_pearson(corner_num_fits[2], num_mises=2) + agostino_pearson(corner_num_fits[3], num_mises=3) + + print("-==== plotting hists: CLASSIC ====-") + # plot histogram + # plot_hist(classic_num_fits[1], num_mises=1) + # plot_hist(classic_num_fits[2], num_mises=2) + # plot_hist(classic_num_fits[3], num_mises=3) + + # test for normal distribution + print("-== Shapiro test ==- ") + # shapiro(classic_num_fits[1], num_mises=1) + # shapiro(classic_num_fits[2], num_mises=2) + # shapiro(classic_num_fits[3], num_mises=3) + + print("\n-== D'Agostino-Pearson test ==- ") + agostino_pearson(classic_num_fits[1], num_mises=1) + agostino_pearson(classic_num_fits[2], num_mises=2) + agostino_pearson(classic_num_fits[3], num_mises=3) + + # statistics for each von mises: + print("######################") + print(" -=== CLASSIC vs CORNER ===-") + # 1: + willy_is_not_whitney_test( + data_classic=classic_num_fits[1], data_corner=corner_num_fits[1] + ) + + # 2: + willy_is_not_whitney_test( + data_classic=classic_num_fits[2], data_corner=corner_num_fits[2] + ) + + # 3: + willy_is_not_whitney_test( + data_classic=classic_num_fits[3], data_corner=corner_num_fits[3] + ) + + # visualize as bar plots: + plot_fit_statistics.plot_means_std_corner_classic( + means_classic=[mean_classic_1, mean_classic_2, mean_classic_3], + means_corner=[mean_corner_1, mean_corner_2, mean_corner_3], + std_classic=[std_classic_1, std_classic_2, std_classic_3], + std_corner=[std_corner_1, std_corner_2, std_corner_3], + saveplot=False, + save_name="3288", + ) diff --git a/thesis code/network analysis/orientation_tuning/fitkarotte.py b/thesis code/network analysis/orientation_tuning/fitkarotte.py new file mode 100644 index 0000000..0e2f3c6 --- /dev/null +++ b/thesis code/network analysis/orientation_tuning/fitkarotte.py @@ -0,0 +1,373 @@ +# %% +import matplotlib.pyplot as plt +import numpy as np +import os +import scipy.optimize as sop +import orientation_tuning_curve # import load_data_from_cnn +import warnings +import matplotlib as mpl + +mpl.rcParams["text.usetex"] = True +mpl.rcParams["font.family"] = "serif" +mpl.rcParams["font.size"] = 15 + +# suppress warnings +warnings.filterwarnings("ignore") + + +def mises(orientation, a, mean, variance): + k = 1 / variance**2 + return a * np.exp(k * np.cos(orientation - mean)) / np.exp(k) + + +def biemlich_mieses_karma(orientation, a1, mean1, variance1, a2, mean2, variance2): + m1 = mises(orientation, a1, mean1, variance1) + m2 = mises(orientation, a2, mean2, variance2) + return m1 + m2 + + +def triemlich_mieses_karma( + orientation, a1, mean1, variance1, a2, mean2, variance2, a3, mean3, variance3 +): + m1 = mises(orientation, a1, mean1, variance1) + m2 = mises(orientation, a2, mean2, variance2) + m3 = mises(orientation, a3, mean3, variance3) + return m1 + m2 + m3 + + +def plot_reshaped(tune, fits, theta, save_name: str | None, save_plot: bool = False): + """ + Plot shows the original tuning with the best fits + """ + + num_rows = tune.shape[0] // 4 + num_cols = tune.shape[0] // num_rows + # plt.figure(figsize=(12, 15)) + fig, axs = plt.subplots(num_rows, num_cols, figsize=(10, 7)) + + # plot the respective y-lims: + overall_min = np.min(tune) + overall_max = np.max(tune) + + for i_tune in range(tune.shape[0]): + ax = axs[i_tune // num_cols, i_tune % num_cols] + ax.plot(np.rad2deg(theta), tune[i_tune], label="Original") + + x_center = (np.rad2deg(theta).min() + np.rad2deg(theta).max()) / 2 + y_center = (tune[i_tune].min() + tune[i_tune].max()) / 2 + + fit = next((fit for key, fit in fits if key == i_tune)) + if fit["fitted_curve"] is not None: + ax.plot( + np.rad2deg(theta), + fit["fitted_curve"] * fit["scaling_factor"], + label="Fit", + ) + ax.text( + x_center, + y_center, + str(fit["curve"]), + ha="center", + va="center", + size="xx-large", + color="gray", + ) + + # update again if there's a fit + overall_min = min( + overall_min, (fit["fitted_curve"] * fit["scaling_factor"]).min() + ) + overall_max = max( + overall_max, (fit["fitted_curve"] * fit["scaling_factor"]).max() + ) + else: + # plt.plot(np.rad2deg(theta), fit[i_tune], "--") + ax.text( + x_center, + y_center, + "*", + ha="center", + va="center", + size="xx-large", + color="gray", + ) + # specified y lims: of no fit: min and max of tune + ax.set_ylim([overall_min, overall_max + 0.05]) + + # x-ticks from 0°-360° + ax.set_xticks(range(0, 361, 90)) + + # label them from 0° to 180° + ax.set_xticklabels(range(0, 181, 45), fontsize=15) + ax.set_xlabel("(in deg)", fontsize=16) + + plt.yticks(fontsize=15) + + plt.tight_layout() + if save_plot: + plt.savefig( + f"additional thesis plots/saved_plots/fitkarotte/{save_name}.pdf", + dpi=300, + bbox_inches="tight", + ) + + plt.show(block=True) + + +def plot_fit(tune, fits, theta, save_name: str | None, save_plot: bool = False): + """ + Plot shows the original tuning with the best fits + """ + + if tune.shape[0] >= 8: + num_rows = tune.shape[0] // 8 + num_cols = tune.shape[0] // num_rows + else: + num_rows = 2 + num_cols = tune.shape[0] // num_rows + # plt.figure(figsize=(12, 15)) + fig, axs = plt.subplots(num_rows, num_cols, figsize=(10, 7)) + + # plot the respective y-lims: + overall_min = np.min(tune) + overall_max = np.max(tune) + + for i_tune in range(tune.shape[0]): + if axs.ndim == 1: + ax = axs[i_tune] + else: + ax = axs[i_tune // num_cols, i_tune % num_cols] + ax.plot(np.rad2deg(theta), tune[i_tune], label="Original") + + x_center = (np.rad2deg(theta).min() + np.rad2deg(theta).max()) / 2 + y_center = (tune[i_tune].min() + tune[i_tune].max()) / 2 + + # fit = next((fit for key, fit in fits if key == i_tune), None) + fit = next((fit for key, fit in fits if key == i_tune)) + if fit["fitted_curve"] is not None: + ax.plot( + np.rad2deg(theta), + fit["fitted_curve"] * fit["scaling_factor"], + label="Fit", + ) + ax.text( + x_center, + y_center, + str(fit["curve"]), + ha="center", + va="center", + size="xx-large", + color="gray", + ) + + # update again if there's a fit + overall_min = min( + overall_min, (fit["fitted_curve"] * fit["scaling_factor"]).min() + ) + overall_max = max( + overall_max, (fit["fitted_curve"] * fit["scaling_factor"]).max() + ) + else: + ax.text( + x_center, + y_center, + "*", + ha="center", + va="center", + size="xx-large", + color="gray", + ) + # specified y lims: of no fit: min and max of tune + ax.set_ylim([overall_min, overall_max + 0.05]) + + # x-ticks from 0°-360° + ax.set_xticks(range(0, 361, 90)) + + # label them from 0° to 180° + ax.set_xticklabels(range(0, 181, 45), fontsize=15) + ax.set_xlabel("(in deg)", fontsize=16) + + plt.yticks(fontsize=15) + + plt.tight_layout() + if save_plot: + plt.savefig( + f"additional thesis plots/saved_plots/fitkarotte/{save_name}.pdf", dpi=300 + ) + + plt.show(block=True) + + +def fit_curves(tune, theta): + # save all fits: + save_fits: list = [] + scaling_factor: list = [] + for curve in range(1, 4): + fit_possible: int = 0 + fit_impossible: int = 0 + for i_tune in range(tune.shape[0]): + to_tune = tune[i_tune].copy() + scale_fact = np.max(to_tune) + scaling_factor.append(scale_fact) + to_tune /= scale_fact + + p10 = theta[np.argmax(to_tune)] + a10 = 1 + s10 = 0.5 + + if curve == 1: + function = mises + p0 = [a10, p10, s10] + elif curve == 2: + function = biemlich_mieses_karma # type: ignore + p20 = p10 + np.pi + a20 = 1.0 + s20 = 0.4 + p0 = [a10, p10, s10, a20, p20, s20] + else: + function = triemlich_mieses_karma # type: ignore + p20 = p10 + 2 * np.pi / 3 + a20 = 0.7 + s20 = 0.3 + p30 = p10 + 4 * np.pi / 3 + a30 = 0.4 + s30 = 0.3 + p0 = [a10, p10, s10, a20, p20, s20, a30, p30, s30] + + try: + popt = sop.curve_fit(function, theta, to_tune, p0=p0) + fitted_curve = function(theta, *popt[0]) + quad_dist = np.sum((to_tune - fitted_curve) ** 2) + + save_fits.append( + { + "weight_idx": i_tune, + "curve": curve, + "fit_params": popt[0], + "fitted_curve": fitted_curve, + "quad_dist": quad_dist, + "scaling_factor": scale_fact, + } + ) + + # count: + fit_possible += 1 + except: + fit_impossible += 1 + fitted_curve = function(theta, *p0) + quad_dist = np.sum((to_tune - fitted_curve) ** 2) + save_fits.append( + { + "weight_idx": i_tune, + "curve": curve, + "fit_params": None, + "fitted_curve": None, + "quad_dist": quad_dist, # quad_dist + "scaling_factor": scale_fact, + } + ) + print( + "\n################", + f" {curve} Mises\tPossible fits: {fit_possible}\tImpossible fits: {fit_impossible}", + "################\n", + ) + + return save_fits + + +def sort_fits(fits, thresh1: float = 0.1, thresh2: float = 0.1): # , thresh3=0.5 | None + filtered_fits: dict = {} + + # search fits for 1 mises: + for fit in fits: + w_idx = fit["weight_idx"] + quad_dist = fit["quad_dist"] + curve = fit["curve"] + + if curve == 1: + if quad_dist <= thresh1: + filtered_fits[w_idx] = fit + + if w_idx not in filtered_fits: + if curve == 2: + if round(quad_dist, 2) <= thresh2: + filtered_fits[w_idx] = fit + elif curve == 3: + filtered_fits[w_idx] = fit + + sorted_filtered_fits = sorted( + filtered_fits.items(), key=lambda x: x[1]["weight_idx"] + ) + return sorted_filtered_fits + + +def analyze_cnns(dir: str, thresh1: float = 0.1, thresh2: float = 0.1): + # theta + num_thetas = 32 + dtheta = 2 * np.pi / num_thetas + theta = dtheta * np.arange(num_thetas) + + all_results: list = [] + for filename in os.listdir(dir): + if filename.endswith(".pt"): + print(os.path.join(dir, filename)) + # load + tune = orientation_tuning_curve.load_data_from_cnn( + cnn_name=os.path.join(dir, filename), + plot_responses=False, + do_stats=True, + ) + + # fit + all_fits = fit_curves(tune=tune, theta=theta) + + # sort + filtered = sort_fits(fits=all_fits, thresh1=thresh1, thresh2=thresh2) + + # store + all_results.append(filtered) + return all_results + + +if __name__ == "__main__": + num_thetas = 32 + dtheta = 2 * np.pi / num_thetas + theta = dtheta * np.arange(num_thetas) + threshold: float = 0.1 + use_saved_tuning: bool = False + + if use_saved_tuning: + # load from file + tune = np.load( + "D:/Katha/Neuroscience/Semester 4/newCode/tuning_CORNER_32o_4p.npy" + ) + else: + # load cnn data + nn = "ArghCNN_numConvLayers3_outChannels[2, 6, 8]_kernelSize[7, 15]_leaky relu_stride1_trainFirstConvLayerTrue_seed299624_Natural_921Epoch_1609-2307" + PATH = f"D:/Katha/Neuroscience/Semester 4/newCode/kk_contour_net_shallow-main/trained_64er_models/{nn}.pt" + + tune = orientation_tuning_curve.load_data_from_cnn( + cnn_name=PATH, plot_responses=False, do_stats=True + ) + + all_fits = fit_curves(tune=tune, theta=theta) + filtered_fits = sort_fits(fits=all_fits) + save_name: str = "CLASSIC_888_trained_4r8c" + save_plot: bool = False + + if tune.shape[0] == 8: + plot_reshaped( + tune=tune, + fits=filtered_fits, + theta=theta, + save_name=save_name, + save_plot=save_plot, + ) + else: + plot_fit( + tune=tune, + fits=filtered_fits, + theta=theta, + save_name=save_name, + save_plot=save_plot, + ) diff --git a/thesis code/network analysis/orientation_tuning/gabor_dict_32o_8p.npy b/thesis code/network analysis/orientation_tuning/gabor_dict_32o_8p.npy new file mode 100644 index 0000000000000000000000000000000000000000..be2c03c08b2211ee33cf1826a9f90be2fe85c803 GIT binary patch literal 247936 zcmcG1chuIkvTn90*hK+#+X#pquwp^wiwYuQ15^YlQ3Mf`u2eA;QKX1;5hZl7QWXUJ zLKP9Eg9R`GQZ3uQw`F_hnM~e$*ZuFTbvSFE^{mV!dGkvqGxJOUN8EAG?RPc$hk8Qw zJnx~Fom+G|uev_(x>i@8S6QFes(q&}ogQe{ynUyKT0(yF1CMrYN%GFEAL!7M+UH+Z z?aIpfQu-hN{{@xxllp)7e@82&%yDOZ`#zz*x@xx~VAtKJ6%+bOy
bIJ^pf}Ra`rJQ{&-1ByY5(+_waCyELuOw=`ezeCPl01TeNe>Ko{l
zw6i|<&*Sra_Vu)%QTERd(Jgqs_Jel;-T8laXDqkkd(4~LO7yvZ9-rsS^XK2ozn|BW
z*Y`hNZ{Cl*KY73M{^kArpYCryzkHtgeDitd^N;^Sdnnl3|FZoS?#KV~eG8XgaWBp}
zob%6)KvU(D{y6`5d;1ZLQ@(r&a{KkeN+3I-UB;{P1f^0)|X7_
z$M#ge%t*-1b#u9{-}l(&4gZ6cq7%(MxNFe
z`gp|P&XAkq%5#0*bUSAfyh3MkS{?WZ%OY9n)osIm-mNwVS10q?-Oomdauy(
zexd$O?-}`h!}Lq<9r^vkE==zs$p0`e@
`}u%=b0l-_Y?4zQ=CPofdYY_k1@97$54vaWp(t%yH4Td+8Cjqblj|
zN7_358loK*X^xNju-h9Hqy1`&W{~?oCb#FdU;P}~=H%fX==mh1}{9zL%v
z(2@Nyl06ntJCc0@1KBI&uwS6sJcQ3zk$pq2)}8Pmm}F{An@t