├── .gitignore ├── README.md ├── calc_NUDFT.ipynb ├── cfg ├── cfg_mnist.yaml └── cfg_run.yaml ├── data ├── pbmc_x.zip └── pbmc_y.zip ├── dataset.py ├── idc_evaluate.ipynb ├── idc_example.ipynb ├── img ├── freq_bias.png ├── img.png ├── nudft_ALLAML.png ├── pbmc.gif ├── supervised_train_plots.png └── supervised_train_plots2.png ├── interpretability_metrics.py ├── lspin_pbmc.py ├── model.py ├── requirements.txt ├── run.py └── train_evaluate.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | # Interpretable Deep Clustering 7 | 8 |

9 | 10 |

11 | 12 | ## An official implementation of the ICML 2024 accepted paper: [Interpretable Deep Clustering for Tabular Data](https://openreview.net/pdf?id=QPy7zLfvof) 13 |

14 | 15 |

16 | 17 | ## UPDATES: 18 | 19 | - 2024-10-08: 20 | * we add an example notebook that produces a NUDFT plot and compares gated vs non-gated supervised models 21 | * we present below a fixed figure 4 from the paper: the frequency values are normalized for each model and gated features are used for IDC model rather than raw features. 22 | 23 | 24 | ## How to run on your data: 25 | 26 | 1. Do you have a dataset without labels? Use our colab example notebook: Open In Colab 27 | 2. If you have a labeled dataset, please follow the colab with evaluation example: Open In Colab 28 | 29 | 30 | ## Fixed Figure 4. Spectral properties of the learned predictive function using ALLAML dataset. 31 | 32 | The model trained with the gating network (IDC) has higher Fourier amplitudes at all frequency levels than 33 | without gates (IDCw/o_gates) the baseline (TELL). This suggests that IDC can better handle the inductive bias of tabular data. 34 |

35 | 36 |

37 | ### Citation: 38 | Please cite our paper if you use this code: 39 | 40 | 41 | ``` 42 | 43 | @InProceedings{pmlr-v235-svirsky24a, 44 | title = {Interpretable Deep Clustering for Tabular Data}, 45 | author = {Svirsky, Jonathan and Lindenbaum, Ofir}, 46 | booktitle = {Proceedings of the 41st International Conference on Machine Learning}, 47 | pages = {47314--47330}, 48 | year = {2024}, 49 | editor = {Salakhutdinov, Ruslan and Kolter, Zico and Heller, Katherine and Weller, Adrian and Oliver, Nuria and Scarlett, Jonathan and Berkenkamp, Felix}, 50 | volume = {235}, 51 | series = {Proceedings of Machine Learning Research}, 52 | month = {21--27 Jul}, 53 | publisher = {PMLR}, 54 | pdf = {https://raw.githubusercontent.com/mlresearch/v235/main/assets/svirsky24a/svirsky24a.pdf}, 55 | url = {https://proceedings.mlr.press/v235/svirsky24a.html}, 56 | abstract = {Clustering is a fundamental learning task widely used as a first step in data analysis. For example, biologists use cluster assignments to analyze genome sequences, medical records, or images. Since downstream analysis is typically performed at the cluster level, practitioners seek reliable and interpretable clustering models. We propose a new deep-learning framework for general domain tabular data that predicts interpretable cluster assignments at the instance and cluster levels. First, we present a self-supervised procedure to identify the subset of the most informative features from each data point. Then, we design a model that predicts cluster assignments and a gate matrix that provides cluster-level feature selection. Overall, our model provides cluster assignments with an indication of the driving feature for each sample and each cluster. We show that the proposed method can reliably predict cluster assignments in biological, text, image, and physics tabular datasets. Furthermore, using previously proposed metrics, we verify that our model leads to interpretable results at a sample and cluster level. Our code is available on https://github.com/jsvir/idc.} 57 | } 58 | ``` 59 | 60 | 61 | 62 | ### TODO list: 63 | - [x] Add interpretability evaluation scripts 64 | - [ ] Add experiments configs 65 | - [ ] Add features index outputs 66 | - [x] Add synthetic dataset deneration code -------------------------------------------------------------------------------- /calc_NUDFT.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "# Calculation of NUDFT \n", 7 | "\n", 8 | "We proved an example for pre-trained supervised model [LSPIN](https://proceedings.mlr.press/v162/yang22i/yang22i.pdf) trained on PBMC dataset compared against deep classifier.\n", 9 | "\n", 10 | "We provide the checkpoints in ckpts directory.\n" 11 | ], 12 | "metadata": { 13 | "collapsed": false 14 | }, 15 | "id": "9e52f756a2211364" 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "source": [], 20 | "metadata": { 21 | "collapsed": false 22 | }, 23 | "id": "e40cd92806e61978" 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "source": [ 28 | "### Imports and util functions\n" 29 | ], 30 | "metadata": { 31 | "collapsed": false 32 | }, 33 | "id": "9dbae214166550cd" 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 1, 38 | "outputs": [], 39 | "source": [ 40 | "import platform\n", 41 | "from nfft import nfft_adjoint\n", 42 | "import torch\n", 43 | "import numpy as np\n", 44 | "from matplotlib import pyplot as plt\n", 45 | "from omegaconf import OmegaConf\n", 46 | "import seaborn as sns\n", 47 | "from lspin_pbmc import PBMC, Classifier, GatingNet\n", 48 | "\n", 49 | "# which dimension use in the y_hat outputs (normalized with softmax)\n", 50 | "TARGET_IDX = 1\n", 51 | "COLUMNS_SUBSET=100000\n", 52 | "\n", 53 | "\n", 54 | "def spectrum_NUDFT(x, y, kmax=50, nk=1000):\n", 55 | " kvals = np.linspace(0.1, kmax, nk+1)\n", 56 | " nufft = (1 / len(x)) * nfft_adjoint(-(x * kmax / nk), y, 2 * (nk + 1))[nk + 1:]\n", 57 | " return [kvals, np.array(nufft, dtype=\"complex_\")]\n", 58 | "\n", 59 | "\n", 60 | "def select_k_columns_with_max_variance(matrix):\n", 61 | " variance_per_column = torch.var(matrix, dim=0)\n", 62 | " _, sorted_indices = torch.sort(variance_per_column, descending=True)\n", 63 | " top_k_indices = sorted_indices[:COLUMNS_SUBSET]\n", 64 | " selected_columns = matrix[:, top_k_indices]\n", 65 | " return selected_columns" 66 | ], 67 | "metadata": { 68 | "collapsed": false, 69 | "ExecuteTime": { 70 | "end_time": "2024-10-08T12:57:48.715663Z", 71 | "start_time": "2024-10-08T12:57:44.858113500Z" 72 | } 73 | }, 74 | "id": "e1c3233b55927f30" 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "source": [ 79 | "### Load pre-trained checkpoints" 80 | ], 81 | "metadata": { 82 | "collapsed": false 83 | }, 84 | "id": "697ba1c508082d" 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 2, 89 | "outputs": [], 90 | "source": [ 91 | "gated_cfg = OmegaConf.create(dict(\n", 92 | " gated=True,\n", 93 | " input_dim=17126,\n", 94 | " n_clusters=2,\n", 95 | " dataset=\"PBMC\",\n", 96 | " data_dir=\"C:/data/fs/pbmc\" if platform.system() == \"Windows\" else \".\" ,\n", 97 | " batch_size=256,\n", 98 | " repitions=5,\n", 99 | " sigma=0.5,\n", 100 | " reg_beta=100,\n", 101 | " devices=1,\n", 102 | " accelerator=\"gpu\",\n", 103 | " max_epochs=100,\n", 104 | " deterministic=True,\n", 105 | " logger=True,\n", 106 | " log_every_n_steps=10,\n", 107 | " check_val_every_n_epoch=1,\n", 108 | " enable_checkpointing=False,\n", 109 | "))\n", 110 | "\n", 111 | "nongated_cfg = OmegaConf.create(dict(\n", 112 | " gated=False,\n", 113 | " input_dim=17126,\n", 114 | " n_clusters=2,\n", 115 | " dataset=\"PBMC\",\n", 116 | " data_dir=\"C:/data/fs/pbmc\" if platform.system() == \"Windows\" else \".\" ,\n", 117 | " batch_size=256,\n", 118 | " repitions=5,\n", 119 | " devices=1,\n", 120 | " accelerator=\"gpu\",\n", 121 | " max_epochs=100,\n", 122 | " deterministic=True,\n", 123 | " logger=True,\n", 124 | " log_every_n_steps=10,\n", 125 | " check_val_every_n_epoch=1,\n", 126 | " enable_checkpointing=False,\n", 127 | "))\n", 128 | "\n", 129 | "\n" 130 | ], 131 | "metadata": { 132 | "collapsed": false, 133 | "ExecuteTime": { 134 | "end_time": "2024-10-08T12:57:48.730048700Z", 135 | "start_time": "2024-10-08T12:57:48.711662700Z" 136 | } 137 | }, 138 | "id": "545756af156b20df" 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "source": [ 143 | "The models were trained for ~400 epochs and we present the training/validation plots:\n", 144 | "\n", 145 | "\"Alt\n", 146 | "\"Alt\n" 147 | ], 148 | "metadata": { 149 | "collapsed": false 150 | }, 151 | "id": "101afcef26dfe116" 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 8, 156 | "outputs": [ 157 | { 158 | "name": "stdout", 159 | "output_type": "stream", 160 | "text": [ 161 | "Dataset PBMC stats:\n", 162 | "X.shape: (20742, 17126)\n", 163 | "Y.shape: (20742,)\n", 164 | "X.min=-2.734075760955218, X.max=144.01736006468113\n", 165 | "Y.min=0, Y.max=1\n", 166 | "Label 0 has 10479 samples\n", 167 | "Label 1 has 10263 samples\n", 168 | "Split to train/test: train 16594 test 4148\n" 169 | ] 170 | } 171 | ], 172 | "source": [ 173 | "gating_net = GatingNet(gated_cfg)\n", 174 | "classifier_gated = Classifier(gated_cfg)\n", 175 | "gating_net.load_state_dict(torch.load(\"ckpts/supervised/sparse_model_last_pbmc_beta_10_seed_0.pth\")[\"gating\"])\n", 176 | "classifier_gated.load_state_dict(torch.load(\"ckpts/supervised/sparse_model_last_pbmc_beta_10_seed_0.pth\")[\"clustering\"])\n", 177 | "\n", 178 | "# # load clustering model without gates:\n", 179 | "classifier = Classifier(nongated_cfg)\n", 180 | "classifier.load_state_dict(torch.load(\"ckpts/supervised/sparse_model_nogates_best_pbmc_seed_0.pth\")[\"clustering\"])\n", 181 | "\n", 182 | "classifier = classifier.to('cpu')\n", 183 | "classifier_gated = classifier_gated.to('cpu')\n", 184 | "\n", 185 | "classifier.eval()\n", 186 | "classifier_gated.eval()\n", 187 | "gating_net.eval()\n", 188 | "\n", 189 | "_, test_dataset = PBMC.setup(gated_cfg.data_dir)\n", 190 | "\n" 191 | ], 192 | "metadata": { 193 | "collapsed": false, 194 | "ExecuteTime": { 195 | "end_time": "2024-10-08T13:17:14.874885100Z", 196 | "start_time": "2024-10-08T13:16:56.649883800Z" 197 | } 198 | }, 199 | "id": "a055dcc6d21c43e6" 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": 10, 204 | "outputs": [ 205 | { 206 | "data": { 207 | "text/plain": "
" 208 | }, 209 | "metadata": {}, 210 | "output_type": "display_data" 211 | }, 212 | { 213 | "data": { 214 | "text/plain": "
", 215 | "image/png": "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VqtXr27w2CuvvKK0tDR9//vf75lVOQG0zmozWUoAAIBuhECzi5g8ebImT54c6mEAAAAAwAWjNj0AAAAAIKB6VKB5+vRp3XfffRo1apTGjBmj9evXt+l506dP18MPPxywcVRWVmrcuHGNlru63W49+uijGjVqlK666iqtWrUqYF8TAAAAALqKHrN01ufz6Z577pHX69V//ud/6vjx43rooYfkdDo1duzYZp/3hz/8QX/84x81ceLEgIzD7Xbr5z//ub744otGxxYuXKj8/Hy9+uqrOnLkiB566CGlp6fr+uuvb/W6o0eP1ujRowMyxvroboPujJ9fAACArqnHZDTz8/O1c+dOLVq0SFlZWbrmmms0bdo0vfLKK80+p7i4WAsXLlR2dnaz5+zfv18nT56s/dzlcmn37t1Nnvvll1/q1ltv1TfffNPoWFlZmV5//XXNnj1bw4YN05gxYzRt2jStWbOmHf/KwPH30vF4PCH5+kAg+H9+Q9EbCgAAAM3rMYFmYWGhkpOT1b9//9rHMjMzlZ+f32ww9cwzz2j8+PG65JJLmr3ur3/9a915551yuVxyu92aMWOGfvWrXzV57scff6zRo0dr7dq1jY7t27dPVVVVysvLq33s8ssv16effiqv19vWf2bAOBwORUZGqqSkhKwQuiWfz6eSkhJFRkbK4XCEejgAAACop8csne3du7dcLpfKy8sVHR0tSTp27JiqqqrkcrmUnJzc4Pxt27bpk08+0caNG/XEE080e925c+fq9ttv11133aX4+HidPHlS//Vf/9XkuVOmTGn2OidPnlRSUpIiIiIajNntdqu4uLjR+DpD7969dfjwYR06dEgJCQlyOByydLTJO9BJfD6fPB6PSkpKdPbsWWVkZIR6SAAAADhPjwk0R4wYodTUVM2dO1dz5szRyZMn9Zvf/EZS4+Whbrdb//Zv/6bHH39cUVFRLV7X6XRq5cqVGjt2rNxutzZt2tShoLC8vLxBkCmp9vPKysp2Xy8Q4uPjJUmnTp3S4cOHQzIGoKMiIyOVkZFR+3MMAACArqPHBJqRkZFavHix7r//fl1++eXq1auXpk2bpqefflpOp7PBucuWLdPw4cP1ne98p03XfuWVV2S32+V0OrV06VI9++yz7c78RUZGNgoo/Z+3FuwGU3x8vOLj4+XxeFRdXR2ycQDtYbPZWC4LAADQhfWYQFOScnJytGXLltplqlu3blVSUpJiY2MbnPeHP/xBp06dqt0v6Q/4/u///k87d+5scO7KlSv1u9/9TqtWrVJcXJymTp2q+fPna86cOe0aW1pamoqKilRVVSW73XzbT548qaioqC6RkXE4HLxxBwAAABAQPSbQLC4u1owZM7R8+XKlpKRIkj744ANdeeWVjc5dvXq1qqqqaj9/7rnnJEm/+MUvGp0bFRWlJUuW1AalL7/8srZt29bu8Q0dOlR2u127du3SqFGjJEl//etflZ2dLau1x9RkAgAAAICeE2gmJiaqrKxMzz77rGbMmKHt27frf/7nf/Taa69JMoGozWZTXFxco+Ih/ozngAEDGl339ttvb/B5bm6ucnNz2z2+6OhoTZgwQU888YSeeuopnThxQqtWrdLTTz/d7msBAAAAQFfWo1Jpzz//vAoLC3XTTTfp1Vdf1QsvvKCcnBxJ0qxZszR//vyQju+RRx7RsGHD9OMf/1i//OUvNWvWLI0dOzakYwIAAACAQLP4aKLYY1RXV2vXrl3Kzc2lgT0AAAAQxkIdG/SojCYAAAAAIPQINAEAAAAAAUWgCQAAAAAIqB5TdRaSf7ttdXV1iEcCAAAAIJT8MUGoSvIQaPYgXq9XkvTZZ5+FeCQAAAAAugJ/jNDZqDrbg3i9XlVVVclqtcpisYR6OAAAAABCxOfzyev1ym63y2rt/B2TBJoAAAAAgICiGBAAAAAAIKAINAEAAAAAAUWgCQAAAAAIKAJNAAAAAEBAEWgCAAAAAAKKQBNt8umnnyorK0t//vOfQz0UAEFQVFSkuXPn6pprrlFOTo5uvvlmrVu3LtTDAhBC/O4Hws/+/ft133336Vvf+paGDx+ua665RvPmzVNpaWm7r2UPwvjQw3z11Ve65557VF1dHeqhAAiCsrIy3XHHHfriiy80ZcoUDR48WG+99ZZmz56tU6dO6e677w71EAF0Mn73A+Hn4MGD+uEPfyi73a4pU6aob9++2rVrl9asWaPt27dr7dq1io2NbfP1CDTRonfeeUezZ89WSUlJqIcCIEhee+01FRQU6LnnntNNN90kSbr11ls1bdo0LVu2TOPHj1ffvn1DPEoAnYXf/UB4mjdvnjwej9atW6eLL75YkvTDH/5QWVlZmj9/vl577TXdddddbb4eS2fRrOnTp+vee+9VSkqKxo0bF+rhAAiSDRs2NPp/brVadeedd8rj8Wjjxo0hHB2AzsTvfiA8ud1u/eUvf9GoUaNqg0y/CRMmSJI+/vjjdl2TQBPNOnjwoB544AG98cYbGjhwYKiHAyAIXC6XDh48qJycHFkslgbHRowYIUnavXt3KIYGIAT43Q+EJ4fDoU2bNumXv/xlo2OnTp2SJNlstnZdk6WzaNamTZsUERER6mEACKLjx4/L5/M1uTTW6XQqNjZWhw4dCsHIAIQCv/uB8GS1WtW/f/8mj61cuVKSNHr06HZdk0AzjCxbtqzV/RYTJ05UVlaWJPGLBggDLpdLkhQTE9Pk8ejoaJWXl3fmkACEEL/7AdS3fv16rV+/Xn379tXkyZPb9VwCzTCyfv16HT58uMVzcnNzawNNAD2fz+dr9fj5S2oBAEDPt27dOj3++OOKiYnRkiVL5HQ62/V8As0wsmXLllAPAUAX4y9T3lzWsry8XP369evMIQEAgBBbsmSJXnzxRTmdTr388svKyclp9zUINAEgjGVkZMhisej48eONjrlcLpWVlalPnz4hGBkAAOhsHo9Hc+bM0YYNG5SamqqVK1fqsssu69C1CDQBIIw5nU5dfPHF+uyzzxod+/TTTyVJI0eO7OxhAQCATlZdXa0HHnhAmzdv1pAhQ7RixYoL6qNNexMACHM333yzjh49qt///ve1j3m9Xq1atUoRERG68cYbQzg6AADQGRYvXqzNmzcrJydHa9asuaAgUyKjCQBh78c//rH+93//Vw8//LD27NmjQYMGadOmTdq2bZsefPBBpaSkhHqIAAAgiAoLC7Vq1SpZLBaNGTNG77//fqNzevXqpauuuqrN1yTQ7ELmzZun1atX6+mnn9Ytt9zS4rn79+/Xv//7v2vHjh06c+aMEhMTNXz4cE2ZMkVXX311J40YQGcI9twQFRWl1atX61e/+pXefPNNnTt3ToMGDdIzzzyjCRMmBOFfBKAjeJ8AhLdgzgEffvihqqqqJEmLFi1q8pojR45sV6Bp8bVW2x6d4t1339WsWbPk9Xpb/eF577339LOf/Uwej6fJ47fffrvmzJkTrKEC6ETMDQAk5gIg3HXHOYA9ml3Ali1bdP/998vr9bZ6bkFBgR544AF5PB5lZ2dr9erV2r59u9atW6drr71WkrR69WqtWbMm2MMGEGTMDQAk5gIg3HXXOYBAM4S8Xq+WLFmie+65p9k7Dud74YUXVFFRoQEDBujVV1/VlVdeqaSkJGVnZ2vZsmW6/vrrJZneN2fPng3m8AEECXMDAIm5AAh33X0OINAMkQ8//FDjx4/Xiy++KK/Xq2HDhrX6nAMHDuiDDz6QJN111121jdb9LBaLHn74YVmtVhUXF+udd94JxtABBBFzAwCJuQAIdz1hDiDQDJFp06bp888/l8Ph0KxZs7R48eJWn/Phhx9KMj8k11xzTZPn9O3bV0OHDpVk1nID6F6YGwBIzAVAuOsJcwCBZohYLBaNHTtWb775pu69915Zra2/FHv37pUkpaenKzk5udnzsrKyJEl79uwJzGABdBrmBgAScwEQ7nrCHEB7kxB56623NGjQoHY95/Dhw5Kkfv36tXheenq6JOnYsWOqqqqS3c7LDHQXzA0AJOYCINz1hDmAjGaItPcHR5KKiookSQkJCS2eFxcXJ0ny+XwqLS1t/+AAhAxzAwCJuQAIdz1hDiDQ7EbcbrckKTIyssXzoqKiav9eWVkZ1DEBCD3mBgAScwEQ7rraHECg2Y3YbLZQDwFAF8TcAEBiLgDCXVebAwg0u5Ho6GhJrd95qKioqP17a3c0AHR/zA0AJOYCINx1tTmAQLMb8a+ndrlcLZ7nX2tts9laXaMNoPtjbgAgMRcA4a6rzQEEmt2If1PwkSNHWjzv6NGjkqS0tLQ2lUIG0L0xNwCQmAuAcNfV5gBml25kyJAhkqTCwkKdPXu22fMKCgokqbYZK4CejbkBgMRcAIS7rjYHEGh2I9/97nclSdXV1frggw+aPOfo0aO1zVq/853vdNbQAIQQcwMAibkACHddbQ4g0OxG+vfvr8svv1yStHTp0kbrr30+nxYsWCCv16ukpCSNHz8+FMME0MmYGwBIzAVAuOtqcwCBZjfzyCOPyGq16quvvtKUKVP00Ucf6cyZM9qzZ49mzZqlt99+W5I0a9YsxcTEhHi0ADoLcwMAibkACHddaQ6wB/XqCLjs7GzNnz9fjz32mD7//HPdeeedjc756U9/qqlTp4ZgdABChbkBgMRcAIS7rjQHEGh2Q7fccouGDRumV155RTt27NDp06cVExOj4cOHa8qUKbr22mtDPUQAIcDcAEBiLgDCXVeZAyw+n8/XKV8JAAAAABAW2KMJAAAAAAgoAk0AAAAAQEARaAIAAAAAAopAEwAAAAAQUASaAAAAAICAItAEAAAAAAQUgSYAAAAAIKAINAEAAAAAAUWgCQAAAAAIKAJNAAAAAEBAEWgCAAAAAAKKQBMAAAAAEFAEmgAAAACAgCLQBAAAAAAEFIEmAABd3NKlS5WZmanbb7+9xfPcbreGDRumzMxMvfTSS0H5GgAAtAWBJgAAPcTnn3+uqqoqSdLQoUNDPBoAQDgj0AQAoIfYt29f7d8JNAEAoUSgCQBAD+EPNJOTk5WWlhbi0QAAwhmBJgAAPcTevXslSZdddlmIRwIACHcEmgAA9AA+n0/79++XRKAJAAg9Ak0AAHqAQ4cO6ezZs5Ka35957Ngx/eAHP1BmZqays7P1+uuvd+YQAQBhxB7qAQAAgAvnXzYrNR1obt++Xf/6r/+qM2fOKDU1VcuWLdOIESM6c4gAgDBCRhMAgB7AXwgoMjJSgwcPbnBs1apVuuOOO3TmzBnl5eVp/fr1BJkAgKAiowkAQA/gz2heeumlstlskqSysjI9+uijeuuttyRJkydP1pw5cxQRERGycQIAwgOBJgAAPYC/EJB/2exXX32le++9V1988YUcDocef/xx3XrrraEcIgAgjBBoAgDQzZWWlurw4cOSTKD53nvv6aGHHpLL5VJKSoqWLl2qvLy8EI8SABBOCDQBAOjm6hcC+uCDD/Thhx/K5/MpLy9PS5YsUWpqaghHBwAIRwSaAAB0c/5CQJL0pz/9SZL0D//wD/r1r3/NfkwAQEhQdRYAgG7OH2j269dPw4YNkyTt3LlTBw8eDOWwAABhjEATAIBuzr90Njs7Wy+99JJSUlJUVlamGTNm6NSpUyEeHQAgHBFoAgDQjXk8Hn355ZeSpMsuu0xpaWlavny5IiMjdeTIEd1zzz2qrKwM8SgBAOGGQBMAgG7swIED8ng8kkygKUk5OTl66qmnJEm7du3So48+GrLxAQDCE4EmAADdWP1CQP5AU5LGjRunu+++W5K0ceNGvfTSS50+NgBA+CLQBACgG/MHmomJierTp0+DY/fff7/GjBkjSXrhhRe0efPmTh8fACA8EWgCANCN+QsBZWZmNjpmsVi0cOFCDR06VD6fTw8++KAKCgo6e4gAgDBEoAkAQDfmz2jWXzZbX0xMjJYvX67evXurvLxcM2bM0IkTJzpziACAMGQP9QAAAEDH7dixo9Vz0tPTtXXr1k4YDQAABhlNAAAAAEBAEWgCAAAAAAKKQBMAAAAAEFAEmgAAAACAgCLQBAAAAAAEFFVnAQDo4q688krde++9ysjI6NZfAwAQPiw+n88X6kEAAAAAAHoOls4CAAAAAAKKQBMAAAAAEFAEmgAAAACAgCLQBAAAAAAEFIEmAAAAACCgCDQBAAAAAAFFoAkAAAAACCgCTQAAAABAQBFoAgAAAAAC6v8Dfs9OxOv6dvMAAAAASUVORK5CYII=" 216 | }, 217 | "metadata": {}, 218 | "output_type": "display_data" 219 | } 220 | ], 221 | "source": [ 222 | "plt.clf()\n", 223 | "plt.figure(figsize=(10, 6))\n", 224 | "plt.yscale('log') # Set the y-axis to logarithmic scale\n", 225 | "plt.xscale('log')\n", 226 | "plt.xticks(fontsize=20)\n", 227 | "plt.yticks(fontsize=20)\n", 228 | "plt.xlim((0.1, 100))\n", 229 | "plt.xlabel('$|k|$', fontsize=20)\n", 230 | "plt.ylabel('$P_{f(x)}(k)$', fontsize=20)\n", 231 | "plt.grid(False)\n", 232 | "\n", 233 | "with torch.no_grad():\n", 234 | " x_tensor = torch.tensor(test_dataset.data).float()\n", 235 | " filtered_dataset = select_k_columns_with_max_variance(x_tensor).cpu().numpy()\n", 236 | " normalized_dataset = filtered_dataset \n", 237 | " gates = gating_net.get_gates(x_tensor)\n", 238 | "\n", 239 | " spectrum_nudft_all = []\n", 240 | " for feat_id in range(filtered_dataset.shape[-1]):\n", 241 | " spectrum_nudft_all.append(spectrum_NUDFT(filtered_dataset[:, feat_id], test_dataset.targets))\n", 242 | " spectra = [np.sqrt(np.abs(t[1] ** 2)).reshape(-1, 1) for t in spectrum_nudft_all]\n", 243 | " k = spectrum_nudft_all[0][0]\n", 244 | " k_repeated = np.concatenate([k] * len(spectra), axis=0).reshape(-1)\n", 245 | "\n", 246 | " # gated data, gated classifier predictions:\n", 247 | " e = classifier_gated.encoder(x_tensor * gates)\n", 248 | " y_hat = torch.softmax(classifier_gated.head(e), dim=1)[:, TARGET_IDX].numpy()\n", 249 | " classifier_gated_spectrum_nudft_all = []\n", 250 | " non_zero_ids = torch.nonzero(gates.sum(dim=0) > 0, as_tuple=True)[0].long()\n", 251 | " max_spectrum_val = - np.inf\n", 252 | " max_spectrum_gated_val = - np.inf\n", 253 | "\n", 254 | " # raw data, classifier predictions:\n", 255 | " e_raw = classifier.encoder(x_tensor)\n", 256 | " y_hat_raw = torch.softmax(classifier.head(e_raw), dim=1)[:, TARGET_IDX].numpy()\n", 257 | " classifier_spectrum_nudft_all = []\n", 258 | " normalized_gated_dataset = normalized_dataset * gates.numpy()\n", 259 | " for feat_id in range(normalized_dataset.shape[-1]):\n", 260 | " feat_spectrum = spectrum_NUDFT(normalized_gated_dataset[:, feat_id], y_hat)\n", 261 | " max_spectrum_gated_val = max(max_spectrum_gated_val, np.abs(feat_spectrum[1]).max())\n", 262 | " classifier_gated_spectrum_nudft_all.append(feat_spectrum)\n", 263 | "\n", 264 | " feat_spectrum_raw = spectrum_NUDFT(normalized_dataset[:, feat_id], y_hat_raw)\n", 265 | " max_spectrum_val = max(max_spectrum_val, np.abs(feat_spectrum_raw[1]).max())\n", 266 | " classifier_spectrum_nudft_all.append(feat_spectrum_raw)\n", 267 | "\n", 268 | " classifier_gated_spectra = [np.abs(t[1]).reshape(-1) for t in classifier_gated_spectrum_nudft_all]\n", 269 | " classifier_gated_spectra_single_column = np.concatenate(classifier_gated_spectra, axis=0)\n", 270 | " classifier_gated_spectra_single_column = classifier_gated_spectra_single_column / max_spectrum_gated_val\n", 271 | " sns.lineplot(x=k_repeated, y=classifier_gated_spectra_single_column, label='$P_{f_{lspin}}(k)$')\n", 272 | "\n", 273 | " classifier_model_spectra = [np.abs(t[1]).reshape(-1) for t in classifier_spectrum_nudft_all]\n", 274 | " classifier_spectra_single_column = np.concatenate(classifier_model_spectra, axis=0)\n", 275 | " classifier_spectra_single_column = classifier_spectra_single_column / max_spectrum_val\n", 276 | " sns.lineplot(x=k_repeated, y=classifier_spectra_single_column, label='$P_{f_{classifier}}(k)$')\n", 277 | "\n", 278 | "plt.legend(fontsize=16)\n", 279 | "plt.show()" 280 | ], 281 | "metadata": { 282 | "collapsed": false, 283 | "ExecuteTime": { 284 | "end_time": "2024-10-08T13:30:05.517370800Z", 285 | "start_time": "2024-10-08T13:18:02.330028300Z" 286 | } 287 | }, 288 | "id": "bb12fc6aae6893cd" 289 | }, 290 | { 291 | "cell_type": "markdown", 292 | "source": [ 293 | "As it could be seen from the plot, the gating of the features adds a high frequency bias and makes the predictor to be more appropriate for tabular dataset. Please refer to the paper [An Inductive Bias for Tabular Deep Learning](https://proceedings.neurips.cc/paper_files/paper/2023/file/8671b6dffc08b4fcf5b8ce26799b2bef-Paper-Conference.pdf) and Figure 1:\n", 294 | "\n", 295 | "\n", 296 | "\"Alt\n", 297 | "\n", 298 | "Due to their heterogeneous nature, tabular datasets tend to describe higher frequency target functions compared to images. The spectra corresponding to image datasets (curves in color) tend to feature lower Fourier amplitudes at higher frequencies than hetergoneous tabular datasets (cyan region).\n" 299 | ], 300 | "metadata": { 301 | "collapsed": false 302 | }, 303 | "id": "91c4ea1eac38133e" 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": null, 308 | "outputs": [], 309 | "source": [], 310 | "metadata": { 311 | "collapsed": false 312 | }, 313 | "id": "5a310757b3524a5a" 314 | } 315 | ], 316 | "metadata": { 317 | "kernelspec": { 318 | "display_name": "Python 3", 319 | "language": "python", 320 | "name": "python3" 321 | }, 322 | "language_info": { 323 | "codemirror_mode": { 324 | "name": "ipython", 325 | "version": 2 326 | }, 327 | "file_extension": ".py", 328 | "mimetype": "text/x-python", 329 | "name": "python", 330 | "nbconvert_exporter": "python", 331 | "pygments_lexer": "ipython2", 332 | "version": "2.7.6" 333 | } 334 | }, 335 | "nbformat": 4, 336 | "nbformat_minor": 5 337 | } 338 | -------------------------------------------------------------------------------- /cfg/cfg_mnist.yaml: -------------------------------------------------------------------------------- 1 | dataset: MNIST10K 2 | data_dir: idc/data 3 | scaler: MinMaxScaler 4 | batch_size: 100 5 | seeds: 1 6 | epochs: &epochs 700 7 | 8 | ae_non_gated_epochs: 10 9 | ae_pretrain_epochs: 300 10 | start_global_gates_training_on_epoch: 400 11 | 12 | mask_percentage: 0.9 13 | latent_noise_std: 0.01 14 | 15 | trainer: 16 | devices: 1 17 | accelerator: gpu 18 | max_epochs: *epochs 19 | deterministic: true 20 | logger: true 21 | log_every_n_steps: 10 22 | check_val_every_n_epoch: 10 23 | enable_checkpointing: false 24 | num_sanity_val_steps: 0 25 | 26 | 27 | # GTCR loss 28 | gtcr_loss: true 29 | gtcr_projection_dim: null # for large number of features use it 30 | gtcr_eps: 1 31 | 32 | 33 | # Compression loss 34 | eps: 0.1 35 | 36 | # Gating Net 37 | use_gating: true 38 | gates_hidden_dim: 784 39 | 40 | # EncoderDecoder 41 | encdec: 42 | - 512 43 | - 512 44 | - 2048 45 | - &bn_layer 10 46 | - 2048 47 | - 512 48 | - 512 49 | 50 | clustering_head: 51 | - *bn_layer 52 | - 2048 53 | 54 | tau: 100 55 | 56 | aux_classifier: 57 | - 2048 58 | 59 | local_gates_lambda: 1 60 | global_gates_lambda: 0.0001 61 | gtcr_lambda: 0.01 62 | 63 | lr: 64 | pretrain: 1e-3 65 | clustering: 1e-2 66 | aux_classifier: 1e-2 67 | 68 | sched: 69 | pretrain_min_lr: 1e-6 70 | clustering_min_lr: 1e-6 71 | 72 | 73 | 74 | save_seed_checkpoints: false 75 | validate: true -------------------------------------------------------------------------------- /cfg/cfg_run.yaml: -------------------------------------------------------------------------------- 1 | filepath_samples: idc/data/pbmc_x.npz 2 | num_clusters: 2 3 | 4 | batch_size: 256 5 | seeds: 1 6 | epochs: &epochs 200 7 | 8 | ae_non_gated_epochs: 5 #50 we reduce the number of epochs for training inside a notebook 9 | ae_pretrain_epochs: 10 #100 we reduce the number of epochs for training inside a notebook 10 | start_global_gates_training_on_epoch: 150 11 | 12 | mask_percentage: 0.9 13 | latent_noise_std: 0.01 14 | 15 | trainer: 16 | devices: 1 17 | accelerator: gpu 18 | max_epochs: *epochs 19 | deterministic: true 20 | logger: true 21 | log_every_n_steps: 10 22 | check_val_every_n_epoch: 10 23 | enable_checkpointing: false 24 | num_sanity_val_steps: 0 25 | 26 | 27 | # GTCR loss 28 | gtcr_loss: true 29 | gtcr_projection_dim: 1024 # for large number of features use it 30 | gtcr_eps: 1 31 | 32 | 33 | # Compression loss 34 | eps: 0.1 35 | 36 | # Gating Net 37 | use_gating: true 38 | gates_hidden_dim: 1024 39 | 40 | # EncoderDecoder 41 | encdec: 42 | - 512 43 | - 512 44 | - 2048 45 | - &bn_layer 128 46 | - 2048 47 | - 512 48 | - 512 49 | 50 | clustering_head: 51 | - *bn_layer 52 | - 2048 53 | 54 | tau: 100 55 | 56 | aux_classifier: 57 | - 2048 58 | 59 | local_gates_lambda: 100 60 | global_gates_lambda: 10 61 | gtcr_lambda: 0.01 62 | 63 | lr: 64 | pretrain: 1e-3 65 | clustering: 1e-3 66 | aux_classifier: 1e-1 67 | 68 | sched: 69 | pretrain_min_lr: 1e-4 70 | clustering_min_lr: 1e-4 71 | 72 | save_seed_checkpoints: false 73 | -------------------------------------------------------------------------------- /data/pbmc_x.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsvir/idc/6288558d24268fa842944dd7f0490a62fd9d1fdf/data/pbmc_x.zip -------------------------------------------------------------------------------- /data/pbmc_y.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsvir/idc/6288558d24268fa842944dd7f0490a62fd9d1fdf/data/pbmc_y.zip -------------------------------------------------------------------------------- /dataset.py: -------------------------------------------------------------------------------- 1 | from torchvision.datasets import MNIST 2 | from torch.utils.data import Dataset 3 | import numpy as np 4 | import torch 5 | from sklearn import preprocessing 6 | from scipy.io import loadmat 7 | from sklearn.preprocessing import MinMaxScaler 8 | from scipy.stats import zscore 9 | import matplotlib.pyplot as plt 10 | from sklearn import datasets 11 | 12 | 13 | class ClusteringDataset(Dataset): 14 | def __init__(self, data, labels=None, num_clusters=None): 15 | super().__init__() 16 | self.data = data 17 | self.labels = labels 18 | self._num_clusters = num_clusters 19 | if num_clusters is None and labels is None: 20 | raise ValueError("At least one of the values should be provided (labels/num_clusters)") 21 | self.print_stats() 22 | 23 | def __getitem__(self, index: int): 24 | if self.labels is None: 25 | return torch.tensor(self.data[index]).float() 26 | return torch.tensor(self.data[index]).float(), torch.tensor(self.labels[index]).long() 27 | 28 | def __len__(self) -> int: 29 | return len(self.data) 30 | 31 | @property 32 | def num_clusters(self): 33 | return self._num_clusters if self._num_clusters is not None else len(np.unique(self.labels)) 34 | 35 | def num_features(self): 36 | return self.data.shape[-1] 37 | 38 | def print_stats(self): 39 | print('X.shape: ', self.data.shape) 40 | print(f"X.min={self.data.min()}, X.max={self.data.max()}") 41 | if self.labels is not None: 42 | print('Y.shape: ', self.labels.shape) 43 | for y_u in np.unique(self.labels): 44 | print(f'{y_u}: {np.sum(self.labels == y_u)}') 45 | print(f"Y.min={self.labels.min()}, Y.max={self.labels.max()}") 46 | 47 | @classmethod 48 | def setup(cls, cfg): 49 | pass 50 | 51 | 52 | class PBMC(ClusteringDataset): 53 | def __init__(self, data, targets): 54 | super().__init__(data, targets) 55 | 56 | @classmethod 57 | def setup(cls, cfg): 58 | data_dir = cfg.data_dir 59 | with np.load(f"{data_dir}/pbmc_x.npz") as data: 60 | X = data['arr_0'] 61 | with np.load(f"{data_dir}/pbmc_y.npz") as data: 62 | Y = data['arr_0'] 63 | Y = Y - Y.min() 64 | scaler = getattr(preprocessing, cfg.scaler)() 65 | X = scaler.fit_transform(X) 66 | return cls(X, Y) 67 | 68 | 69 | class BIASE(ClusteringDataset): 70 | def __init__(self, data, targets): 71 | super().__init__(data, targets) 72 | 73 | @classmethod 74 | def setup(cls, cfg): 75 | name = 'biase' 76 | data_dir = cfg.data_dir 77 | dataset_x = f"{data_dir}/{name}/{name}_data.csv" 78 | dataset_y = f"{data_dir}/{name}/{name}_celldata.csv" 79 | with open(dataset_x) as r: 80 | data = [l.strip() for l in r.readlines()] 81 | cell_keys = data[0].split(',')[1:] 82 | rows = [np.array([float(v) for v in row.split(',')[1:]]).reshape((1, -1)) for row in data[1:]] 83 | X = BIASE.remove_zero_columns( 84 | np.concatenate(rows, axis=0).transpose()) # np.concatenate(rows, axis=0).transpose() 85 | with open(dataset_y) as r: 86 | y_data = [l.strip().split(',') for l in r.readlines()[1:]] 87 | cell2class = {row[0]: row[2] for row in y_data} 88 | class2count = {} 89 | for cell, clas in cell2class.items(): 90 | class2count.setdefault(clas, 0) 91 | class2count[clas] += 1 92 | 93 | print(class2count) 94 | class2id = {c: i for i, c in enumerate(set(sorted(list(cell2class.values()))))} 95 | 96 | Y = [] 97 | for cell_key in cell_keys: 98 | Y.append(class2id[cell2class[cell_key]]) 99 | Y = np.array(Y).reshape(-1) 100 | X = BIASE.transform(X) 101 | 102 | X = np.log(1 + X) 103 | X = X + .001 * np.random.normal(0, 1, (X.shape)) 104 | scaler = getattr(preprocessing, cfg.scaler)() 105 | X = scaler.fit_transform(X) 106 | return cls(X, Y) 107 | 108 | 109 | class INTESTINE(ClusteringDataset): 110 | def __init__(self, data, targets): 111 | super().__init__(data, targets) 112 | 113 | @classmethod 114 | def setup(cls, cfg): 115 | scaler = getattr(preprocessing, cfg.scaler)() 116 | name = 'intestine' 117 | data_dir = cfg.data_dir 118 | dataset_x = f"{data_dir}/{name}/{name}_data.csv" 119 | dataset_y = f"{data_dir}/{name}/{name}_celldata.csv" 120 | with open(dataset_x) as r: 121 | data = [l.strip() for l in r.readlines()] 122 | cell_keys = data[0].split(',')[1:] 123 | rows = [np.array([float(v) for v in row.split(',')[1:]]).reshape((1, -1)) for row in data[1:]] 124 | X = np.concatenate(rows, axis=0).T 125 | with open(dataset_y) as r: 126 | y_data = [l.strip().split(',') for l in r.readlines()[1:]] 127 | cell2class = {row[0]: row[2] for row in y_data} 128 | class2count = {} 129 | for cell, clas in cell2class.items(): 130 | class2count.setdefault(clas, 0) 131 | class2count[clas] += 1 132 | print(class2count) 133 | class2id = {c: i for i, c in enumerate(sorted(set(list(cell2class.values()))))} 134 | Y = [] 135 | for cell_key in cell_keys: 136 | Y.append(class2id[cell2class[cell_key]]) 137 | Y = np.array(Y).reshape(-1) 138 | X = scaler.fit_transform(X) 139 | return cls(X, Y) 140 | 141 | 142 | class CNAE9(ClusteringDataset): 143 | def __init__(self, data, targets): 144 | super().__init__(data, targets) 145 | 146 | @classmethod 147 | def setup(cls, cfg): 148 | scaler = getattr(preprocessing, cfg.scaler)() 149 | data = np.loadtxt(f"{cfg.data_dir}/cnae_9_numpy.txt") 150 | X = data[:, :-1] 151 | Y = data[:, -1] 152 | Y = Y - Y.min() 153 | X = scaler.fit_transform(X) 154 | return cls(X, Y) 155 | 156 | 157 | class MFEATZERNIKE(ClusteringDataset): 158 | def __init__(self, data, targets): 159 | super().__init__(data, targets) 160 | 161 | @classmethod 162 | def setup(cls, cfg): 163 | scaler = getattr(preprocessing, cfg.scaler)() 164 | data = np.loadtxt(f"{cfg.data_dir}/mfeat_zernike_numpy.txt") 165 | X = data[:, :-1] 166 | Y = data[:, -1] 167 | Y = Y - Y.min() 168 | X = scaler.fit_transform(X) 169 | return cls(X, Y) 170 | 171 | 172 | class ALLAML(ClusteringDataset): 173 | def __init__(self, data, targets): 174 | super().__init__(data, targets) 175 | 176 | @classmethod 177 | def setup(cls, cfg): 178 | dataset = loadmat(f"{cfg.data_dir}/ALLAML.mat") 179 | X = dataset.get('X') 180 | Y = dataset.get('Y').reshape(-1) 181 | Y = Y - Y.min() 182 | scaler = getattr(preprocessing, cfg.scaler)() 183 | X = scaler.fit_transform(X) 184 | return cls(X, Y) 185 | 186 | 187 | class PROSTATE(ClusteringDataset): 188 | def __init__(self, data, targets): 189 | super().__init__(data, targets) 190 | 191 | @classmethod 192 | def setup(cls, cfg): 193 | dataset = loadmat(f"{cfg.data_dir}/PROSTATE.mat") 194 | X = dataset.get('X') 195 | Y = dataset.get('Y').reshape(-1) 196 | Y = Y - Y.min() # to start from zero 197 | scaler = getattr(preprocessing, cfg.scaler)() 198 | X = scaler.fit_transform(X) 199 | return cls(X, Y) 200 | 201 | 202 | class TOX171(ClusteringDataset): 203 | def __init__(self, data, targets): 204 | super().__init__(data, targets) 205 | 206 | @classmethod 207 | def setup(cls, cfg): 208 | dataset = loadmat(f"{cfg.data_dir}/TOX171.mat") 209 | X = dataset.get('X') 210 | Y = dataset.get('Y').reshape(-1) 211 | Y = Y - Y.min() # to start from zero 212 | scaler = getattr(preprocessing, cfg.scaler)() 213 | X = scaler.fit_transform(X) 214 | return cls(X, Y) 215 | 216 | 217 | class SRBCT(ClusteringDataset): 218 | def __init__(self, data, targets): 219 | super().__init__(data, targets) 220 | 221 | @classmethod 222 | def setup(cls, cfg): 223 | dataset = loadmat(f"{cfg.data_dir}/SRBCT.mat") 224 | X = dataset.get('X') 225 | Y = dataset.get('Y').reshape(-1) 226 | Y = Y - Y.min() # to start from zero 227 | scaler = getattr(preprocessing, cfg.scaler)() 228 | X = scaler.fit_transform(X) 229 | return cls(X, Y) 230 | 231 | 232 | class MNIST60K(ClusteringDataset): 233 | def __init__(self, data, targets): 234 | super().__init__(data, targets) 235 | 236 | @classmethod 237 | def setup(cls, cfg): 238 | scaler = getattr(preprocessing, cfg.scaler)() 239 | X = MNIST(cfg.data_dir, train=True, download=True).data.reshape(-1, 784).cpu().numpy() 240 | Y = MNIST(cfg.data_dir, train=True, download=True).targets.cpu().numpy() 241 | X = scaler.fit_transform(X) 242 | return cls(X, Y) 243 | 244 | 245 | class MNIST10K(ClusteringDataset): 246 | def __init__(self, data, targets): 247 | super().__init__(data, targets) 248 | 249 | @classmethod 250 | def setup(cls, cfg): 251 | scaler = getattr(preprocessing, cfg.scaler)() 252 | X = MNIST(cfg.data_dir, train=True, download=True).data.reshape(-1, 784).cpu().numpy() 253 | Y = MNIST(cfg.data_dir, train=True, download=True).targets.cpu().numpy() 254 | X = scaler.fit_transform(X) 255 | X = X[:10000] 256 | Y = Y[:10000] 257 | return cls(X, Y) 258 | 259 | 260 | class NumpyTableDataset(ClusteringDataset): 261 | def __init__(self, data, labels=None, num_clusters=None): 262 | super().__init__(data, labels, num_clusters) 263 | 264 | @classmethod 265 | def setup(cls, filepath_samples: str, filepath_labels: str = None, num_clusters: int = None): 266 | """ 267 | :param filepath_samples: the path to the npz file, the format of the numpy array should be NxD 268 | (number of samples x number of features) 269 | :param filepath_labels: the path to the npz file, the format of the numpy array should be N 270 | (number of samples) 271 | :param num_clusters: the integer number of expected clusters 272 | """ 273 | with np.load(filepath_samples) as data: 274 | X = data['arr_0'] 275 | 276 | if filepath_labels is not None: 277 | with np.load(filepath_labels) as data: 278 | Y = data['arr_0'] 279 | X = preprocessing.StandardScaler().fit_transform(X) 280 | Y = Y - Y.min() 281 | else: 282 | Y = None 283 | return cls(X, Y, num_clusters) 284 | 285 | 286 | def remove_zero_columns(X): 287 | non_zero_columns = [] 288 | for col in range(X.shape[1]): 289 | if np.min(X[:, col]) == 0 and np.max(X[:, col]) == 0: 290 | continue 291 | else: 292 | non_zero_columns.append(col) 293 | X = X[:, non_zero_columns] 294 | return X 295 | 296 | 297 | class Synthetic(Dataset): 298 | def __init__(self, X, Y): 299 | super().__init__() 300 | self.data = X 301 | self.targets = Y 302 | 303 | def __getitem__(self, index: int): 304 | x = self.data[index] 305 | return torch.tensor(x).float(), torch.tensor(self.targets[index]).long() 306 | 307 | def __len__(self) -> int: 308 | return len(self.data) 309 | 310 | @classmethod 311 | def setup(cls, num_samples=5000, num_features=3, num_clusters=3, num_noise_dims=10): 312 | """ 313 | Make num_clusters + 1 clusters in 3d and adds additional num_noise_dims noise features 314 | :param num_samples: number of samples in the dataset 315 | :param num_features: number of features in the dataset 316 | :param num_clusters: number of clusters in the dataset 317 | :param num_noise_dims: number of noise dimensions in addition to num_features 318 | :return: generates a dataset 319 | """ 320 | x_2d, y_2d = datasets.make_blobs(num_samples, num_features-1, centers=num_clusters, cluster_std=.5, 321 | random_state=0) 322 | # split the points for cluster==2 into 2 clusters: 323 | max_x = x_2d[:, 1].max() 324 | min_x = x_2d[:, 1].min() 325 | x_y_2 = x_2d[y_2d == 2][:, 1] 326 | x_y_2 = MinMaxScaler((0, 1)).fit_transform(x_y_2.reshape(-1, 1)).reshape(-1) 327 | x_y_2 = MinMaxScaler((min_x, max_x)).fit_transform(x_y_2.reshape(-1, 1)).reshape(-1) 328 | x_2d[:, 1][y_2d == 2] = x_y_2 329 | 330 | z = np.random.rand(num_samples) 331 | y_2d[(y_2d == 2) & (z > 0.5)] = 3 332 | 333 | x_2d[:, 0][y_2d == 0] = x_2d[:, 0][y_2d == 1] 334 | 335 | bg = np.random.normal(loc=0, scale=0.01, size=(num_samples, num_noise_dims)) 336 | X = np.concatenate([x_2d, z.reshape(-1, 1), bg], axis=1) 337 | X[:, 2][y_2d == 3] = X[:, 2][y_2d == 3] + 0.5 # separate in z axis 338 | X[:, 2][y_2d == 0] = MinMaxScaler( 339 | (X[:, 2][(y_2d == 3) | (y_2d == 2)].min(), X[:, 2][(y_2d == 3) | (y_2d == 2)].max())).fit_transform( 340 | X[:, 2][y_2d == 0].reshape(-1, 1)).reshape(-1) 341 | X[:, 2][y_2d == 1] = MinMaxScaler( 342 | (X[:, 2][(y_2d == 3) | (y_2d == 2)].min(), X[:, 2][(y_2d == 3) | (y_2d == 2)].max())).fit_transform( 343 | X[:, 2][y_2d == 1].reshape(-1, 1)).reshape(-1) 344 | 345 | Y = y_2d 346 | 347 | X4 = X[Y == 3] 348 | max_len = len(X4) 349 | X1 = X[Y == 0][:max_len, :] 350 | X2 = X[Y == 1][:max_len, :] 351 | X3 = X[Y == 2][:max_len, :] 352 | 353 | Y1 = Y[Y == 0][:max_len] 354 | Y2 = Y[Y == 1][:max_len] 355 | Y3 = Y[Y == 2][:max_len] 356 | Y4 = Y[Y == 3][:max_len] 357 | X = np.concatenate([X1, X2, X3, X4], axis=0) 358 | Y = np.concatenate([Y1, Y2, Y3, Y4], axis=0) 359 | 360 | print("Class stats:") 361 | for y_i in np.unique(Y): 362 | print(f"{y_i}: {len(Y[Y == y_i])} samples") 363 | X[:, :3] = zscore(X[:, :3]) 364 | 365 | plt.style.use('classic') 366 | plt.rcParams['axes.spines.right'] = False 367 | plt.rcParams['axes.spines.top'] = False 368 | fig = plt.figure() 369 | fig.set_facecolor('w') 370 | plt.scatter(X[:, 0], X[:, 1], c=Y, s=100, alpha=0.8, cmap='viridis', edgecolor='k', linewidth=2) 371 | plt.xlabel('$X_1$', fontsize=30) 372 | plt.ylabel('$X_2$', fontsize=30) 373 | plt.tight_layout() 374 | plt.xticks([]) 375 | plt.yticks([]) 376 | plt.savefig("synth_X_1_X_2.png") 377 | 378 | plt.clf() 379 | fig = plt.figure() 380 | fig.set_facecolor('w') 381 | plt.scatter(X[:, 0], X[:, 2], c=Y, s=100, alpha=0.8, cmap='viridis', edgecolor='k', linewidth=2) 382 | plt.xlabel('$X_1$', fontsize=30) 383 | plt.ylabel('$X_3$', fontsize=30) 384 | plt.tight_layout() 385 | plt.xticks([]) 386 | plt.yticks([]) 387 | plt.savefig("synth_X_1_X_3.png") 388 | return cls(X, Y) 389 | 390 | def num_classes(self): 391 | return len(np.unique(self.targets)) 392 | 393 | def num_features(self): 394 | return self.data.shape[-1] -------------------------------------------------------------------------------- /img/freq_bias.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsvir/idc/6288558d24268fa842944dd7f0490a62fd9d1fdf/img/freq_bias.png -------------------------------------------------------------------------------- /img/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsvir/idc/6288558d24268fa842944dd7f0490a62fd9d1fdf/img/img.png -------------------------------------------------------------------------------- /img/nudft_ALLAML.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsvir/idc/6288558d24268fa842944dd7f0490a62fd9d1fdf/img/nudft_ALLAML.png -------------------------------------------------------------------------------- /img/pbmc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsvir/idc/6288558d24268fa842944dd7f0490a62fd9d1fdf/img/pbmc.gif -------------------------------------------------------------------------------- /img/supervised_train_plots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsvir/idc/6288558d24268fa842944dd7f0490a62fd9d1fdf/img/supervised_train_plots.png -------------------------------------------------------------------------------- /img/supervised_train_plots2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsvir/idc/6288558d24268fa842944dd7f0490a62fd9d1fdf/img/supervised_train_plots2.png -------------------------------------------------------------------------------- /interpretability_metrics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from munkres import Munkres 3 | import torch 4 | from sklearn.metrics import confusion_matrix, jaccard_score 5 | from scipy.spatial import distance_matrix 6 | from tqdm import tqdm 7 | from sklearn.svm import LinearSVC 8 | 9 | 10 | def generalizability(X_train, gates_train, Y_train, X_test, gates_test, Y_test): 11 | """ 12 | How the interpretation of the prediction generalizes to other simple prediction models, e.g. Linear Support Vector Classification 13 | """ 14 | classifier = LinearSVC() 15 | classifier.fit(X_train * gates_train, Y_train) 16 | return classifier.score(X_test * gates_test, Y_test) 17 | 18 | 19 | def faithfulness(gates_i, x, inference_fn, y, num_features=784): 20 | """ 21 | Are the identified features significant for prediction? 22 | """ 23 | importance_vec = np.sum(gates_i > 0, axis=0) 24 | importance_ind = np.where(np.sum(gates_i > 0, axis=0) > 0)[0] 25 | importance_ind_sort = importance_ind[np.argsort(-importance_vec[importance_vec > 0])] 26 | mask = np.ones(num_features) 27 | acc_arr_bad = [] 28 | for i in importance_ind_sort: 29 | mask[i] = 0 30 | y_hat = inference_fn(x * mask) 31 | if isinstance(y_hat, torch.Tensor): 32 | y_hat = y_hat.cpu().numpy() 33 | mean_val = get_accuracy(y_hat, y, 10) 34 | acc_arr_bad.append(mean_val) 35 | return np.corrcoef(importance_vec[importance_ind_sort], np.array(acc_arr_bad))[0, 1] 36 | 37 | 38 | def stability(x, gates, k=2, subset_size=10000,p=2): 39 | """ 40 | Are explanations to similar samples consistent? 41 | inputs: 42 | x_test is N x D matrix of samples 43 | gates is N x D matrix predicted by STG for x_test 44 | k is the number of neighbors 45 | outputs: 46 | mean Lipchitz constant of the explanation function 47 | """ 48 | dist_mat_x = distance_matrix(x, x, p=p) 49 | nn_dist_mat = np.sort(dist_mat_x, axis=1)[:, 0:k] 50 | nn_ind_mat = np.argsort(dist_mat_x, axis=1)[:, 0:k] 51 | lipchitz_constants = [] 52 | for i in tqdm(range(subset_size)): 53 | lipchitz_constants.append(max(distance_matrix(gates[nn_ind_mat[i], :], gates[nn_ind_mat[i], :])[0][1:] / nn_dist_mat[i][1:])) 54 | return np.mean(np.array(lipchitz_constants)) 55 | 56 | 57 | def diversity(y, gates, num_clusters=10, num_features=784): 58 | """ 59 | How different are the selected variables for instances of distinct classes? 60 | For formula see appendix A.7 in 61 | Yang, Junchen, Ofir Lindenbaum, and Yuval Kluger. "Locally sparse neural networks for tabular biomedical data." International Conference on Machine Learning. PMLR, 2022. 62 | """ 63 | per_matrix = np.zeros((num_clusters, num_clusters)) 64 | all_gates = [] 65 | for i in range(num_clusters): 66 | indices_p = np.where(y == i)[0] 67 | onez_p = np.zeros(num_features) 68 | active_gates = np.where(np.median(gates[indices_p, :], axis=0) > 0)[0] 69 | onez_p[active_gates] = 1 70 | all_gates = np.append(all_gates, active_gates) 71 | for j in range(num_clusters): 72 | indices_n = np.where(y == j)[0] 73 | active_gates_n = np.where(np.median(gates[indices_n, :], axis=0) > 0)[0] 74 | onez_n = np.zeros(num_features) 75 | onez_n[active_gates_n] = 1 76 | per_matrix[i, j] = jaccard_score(onez_n, onez_p) 77 | 78 | diversity = 100 * (1 - (per_matrix / (num_clusters * (num_clusters - 1))).sum()) 79 | return diversity 80 | 81 | 82 | def uniqueness(x, gates, k=2, subset_size=10000, p=2): 83 | """ 84 | uniqueness of the selected features for similar samples (how granular our explanations are?) 85 | inputs: 86 | x_test is N x D matrix of samples 87 | gates is N x D matrix predicted by STG for x_test 88 | k is the number of neighbors 89 | """ 90 | dist_mat_x = distance_matrix(x, x, p=p) 91 | nn_dist_mat = np.sort(dist_mat_x, axis=1)[:, 0:k] 92 | nn_ind_mat = np.argsort(dist_mat_x, axis=1)[:, 0:k] 93 | vals = [] 94 | for i in tqdm(range(subset_size)): 95 | vals.append(min(distance_matrix(gates[nn_ind_mat[i], :], gates[nn_ind_mat[i], :])[0][1:] / nn_dist_mat[i][1:])) 96 | return np.mean(np.array(vals)) 97 | 98 | 99 | 100 | def get_accuracy(cluster_assignments, y_true, n_clusters): 101 | ''' 102 | Computes the accuracy based on the provided kmeans cluster assignments 103 | and true labels, using the Munkres algorithm 104 | cluster_assignments: array of labels, outputted by kmeans 105 | y_true: true labels 106 | n_clusters: number of clusters in the dataset 107 | returns: a tuple containing the accuracy and confusion matrix, 108 | in that order 109 | ''' 110 | confusion_mat = confusion_matrix(y_true, cluster_assignments, labels=None) 111 | # compute accuracy based on optimal 1:1 assignment of clusters to labels 112 | cost_matrix = calculate_cost_matrix(confusion_mat, n_clusters) 113 | indices = Munkres().compute(cost_matrix) 114 | kmeans_to_true_cluster_labels = get_cluster_labels_from_indices(indices) 115 | y_pred = kmeans_to_true_cluster_labels[cluster_assignments] 116 | return np.mean(y_pred == y_true) 117 | 118 | 119 | def calculate_cost_matrix(C, n_clusters): 120 | cost_matrix = np.zeros((n_clusters, n_clusters)) 121 | # cost_matrix[i,j] will be the cost of assigning cluster i to label j 122 | for j in range(n_clusters): 123 | s = np.sum(C[:, j]) # number of examples in cluster i 124 | for i in range(n_clusters): 125 | t = C[i, j] 126 | cost_matrix[j, i] = s - t 127 | return cost_matrix 128 | 129 | 130 | def get_cluster_labels_from_indices(indices): 131 | n_clusters = len(indices) 132 | clusterLabels = np.zeros(n_clusters) 133 | for i in range(n_clusters): 134 | clusterLabels[i] = indices[i][1] 135 | return clusterLabels -------------------------------------------------------------------------------- /lspin_pbmc.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | import torch.nn.functional as F 3 | from torch.utils.data import DataLoader 4 | from pytorch_lightning import LightningModule 5 | from torch.utils.data import Dataset 6 | from sklearn.preprocessing import StandardScaler 7 | from pytorch_lightning import Trainer, seed_everything 8 | import argparse 9 | import torch 10 | import math 11 | import numpy as np 12 | import os 13 | from pytorch_lightning.loggers import TensorBoardLogger 14 | from pytorch_lightning.callbacks import LearningRateMonitor 15 | import platform 16 | 17 | 18 | def parse_args(args): 19 | parser = argparse.ArgumentParser() 20 | parser.add_argument("--gated", action="store_true") 21 | parser.add_argument("--dataset", type=str, default="PBMC") 22 | parser.add_argument("--data_dir", type=str, default="C:/data/fs/pbmc" if platform.system() == "Windows" else ".") 23 | parser.add_argument("--batch_size", type=int, default=256) 24 | parser.add_argument("--repitions", type=int, default=5) 25 | parser.add_argument("--lr", type=int, default=1e-3) 26 | 27 | # gatenet config 28 | parser.add_argument("--sigma", type=float, default=0.5) 29 | parser.add_argument("--reg_beta", type=float, default=10) 30 | parser.add_argument("--target_sparsity", type=int, default=0.9) 31 | parser.add_argument("--gates_lr", type=int, default=2e-3) 32 | 33 | 34 | # trainer config 35 | parser.add_argument("--devices", type=int, default=1) 36 | parser.add_argument("--accelerator", type=str, default="gpu") 37 | parser.add_argument("--max_epochs", type=int, default=1000) 38 | parser.add_argument("--deterministic", type=bool, default=True) 39 | parser.add_argument("--logger", type=bool, default=True) 40 | parser.add_argument("--log_every_n_steps", type=int, default=10) 41 | parser.add_argument("--check_val_every_n_epoch", type=int, default=1) 42 | parser.add_argument("--enable_checkpointing", type=bool, default=False) 43 | 44 | args = parser.parse_args(args) 45 | return args 46 | 47 | 48 | class PBMC(Dataset): 49 | def __init__(self, X, Y): 50 | super().__init__() 51 | self.data = X 52 | self.targets = Y 53 | 54 | def __getitem__(self, index: int): 55 | x = self.data[index] 56 | x = x.reshape(-1) 57 | return torch.tensor(x).float(), torch.tensor(self.targets[index]).long() 58 | 59 | def __len__(self) -> int: 60 | return len(self.data) 61 | 62 | @classmethod 63 | def setup(cls, data_dir, test_size=0.2): 64 | with np.load(f"{data_dir}/pbmc_x.npz") as data: 65 | X = data['arr_0'] 66 | with np.load(f"{data_dir}/pbmc_y.npz") as data: 67 | Y = data['arr_0'] 68 | 69 | Y = Y - Y.min() 70 | X = StandardScaler().fit_transform(X) 71 | print(f'Dataset PBMC stats:') 72 | print('X.shape: ', X.shape) 73 | print('Y.shape: ', Y.shape) 74 | print(f"X.min={X.min()}, X.max={X.max()}") 75 | print(f"Y.min={Y.min()}, Y.max={Y.max()}") 76 | 77 | for y_uniq in np.unique(Y): 78 | print(f"Label {y_uniq} has {len(Y[Y == y_uniq])} samples") 79 | 80 | np.random.seed(1948) 81 | random_index = np.random.permutation(len(X)) 82 | test_size = int(len(X) * test_size) 83 | x_test = X[random_index][:test_size] 84 | y_test = Y[random_index][:test_size] 85 | 86 | x_train = X[random_index][test_size:] 87 | y_train = Y[random_index][test_size:] 88 | 89 | print(f"Split to train/test: train {len(x_train)} test {len(x_test)}") 90 | return cls(x_train, y_train), cls(x_test, y_test) 91 | 92 | def num_classes(self): 93 | return len(np.unique(self.targets)) 94 | 95 | def num_features(self): 96 | return self.data.shape[-1] 97 | 98 | 99 | class BaseModule(LightningModule): 100 | def __init__(self, cfg): 101 | super().__init__() 102 | self.cfg = cfg 103 | self.save_hyperparameters() 104 | self.best_evaluation_stats = {} 105 | self.automatic_optimization = False 106 | self.best_accuracy = - np.infty 107 | self.classifier_net = Classifier(cfg) 108 | self.val_cluster_list = [] 109 | self.val_label_list = [] 110 | self.best_acc = - 100 111 | 112 | if cfg.gated: 113 | self.gating_net = GatingNet(cfg) 114 | self.val_cluster_list_gated = [] 115 | self.open_gates = [] 116 | self.best_local_feats = None 117 | 118 | def training_step(self, batch, batch_idx): 119 | opt = self.optimizers() 120 | sch = self.lr_schedulers() 121 | x, y = batch 122 | x = x.reshape(x.size(0), -1) 123 | opt.zero_grad() 124 | 125 | if hasattr(self, 'gating_net'): 126 | mu, _, gates = self.gating_net(x) 127 | ae_emb = self.classifier_net.encoder(x * gates) 128 | 129 | reg_loss = self.gating_net.regularization(mu) 130 | self.log("train/reg_loss", reg_loss.item()) 131 | else: 132 | ae_emb = self.classifier_net.encoder(x) 133 | reg_loss = 0 134 | 135 | cluster_logits = self.classifier_net.head(ae_emb) 136 | ce_loss = F.cross_entropy(cluster_logits, y) 137 | 138 | self.log("train/ce_loss", ce_loss.item()) 139 | loss = ce_loss + self.cfg.reg_beta * reg_loss 140 | self.manual_backward(loss) 141 | opt.step() 142 | sch.step() 143 | 144 | if self.global_step % 100 == 0: 145 | if hasattr(self, 'gating_net'): 146 | print(f"Epoch {self.current_epoch} " 147 | f"step {self.global_step} " 148 | f"train/reg_loss {reg_loss.item()} " 149 | f"train/ce_loss {ce_loss.item()}") 150 | def configure_optimizers(self): 151 | 152 | if hasattr(self, 'gating_net'): 153 | params =[ { # classifier 154 | "params": chain( 155 | self.classifier_net.encoder.parameters(), 156 | self.classifier_net.head.parameters()), 157 | "lr": self.cfg.lr, 158 | 159 | }, 160 | { # gates 161 | "params": self.gating_net.net.parameters(), 162 | "lr": self.cfg.gates_lr, 163 | }] 164 | 165 | else: 166 | params = chain( 167 | self.classifier_net.encoder.parameters(), 168 | self.classifier_net.head.parameters(), 169 | ) 170 | optimizer = torch.optim.SGD( 171 | params=params, 172 | lr=self.cfg.lr) 173 | 174 | steps = self.train_dataset.__len__() // self.batch_size * self.cfg.max_epochs 175 | print(f"Cosine annealing LR scheduling is applied during {steps} steps") 176 | sched = torch.optim.lr_scheduler.CosineAnnealingLR( 177 | optimizer=optimizer, 178 | T_max=steps, 179 | eta_min=1e-4) 180 | return [optimizer], [sched] 181 | 182 | def validation_step(self, batch, batch_idx): 183 | x, y = batch 184 | if hasattr(self, 'gating_net'): 185 | gates = self.gating_net.get_gates(x) 186 | ae_emb = self.classifier_net.encoder(x * gates) 187 | self.open_gates.append(self.gating_net.num_open_gates(x)) 188 | else: 189 | ae_emb = self.classifier_net.encoder(x) 190 | cluster_logits = self.classifier_net.head(ae_emb) 191 | y_hat = cluster_logits.argmax(dim=-1) 192 | self.val_cluster_list.append(y_hat.cpu()) 193 | self.val_label_list.append(y.cpu()) 194 | 195 | def on_validation_epoch_start(self): 196 | self.val_cluster_list = [] 197 | self.val_cluster_list_gated = [] 198 | self.val_label_list = [] 199 | if hasattr(self, 'gating_net'): 200 | self.open_gates = [] 201 | 202 | def on_validation_epoch_end(self): 203 | if self.current_epoch > 0: 204 | cluster_mtx = torch.cat(self.val_cluster_list, dim=0) 205 | label_mtx = torch.cat(self.val_label_list, dim=0) 206 | acc = torch.mean((cluster_mtx == label_mtx).float()).item() 207 | if self.best_accuracy < acc: 208 | self.best_accuracy = acc 209 | if hasattr(self, 'gating_net'): 210 | meta_dict = {"gating": self.gating_net.state_dict(), "clustering": self.classifier_net.state_dict()} 211 | torch.save(meta_dict, f'sparse_model_best_pbmc_beta_{self.cfg.reg_beta}_seed_{self.cfg.seed}.pth') 212 | print(f"New best accuracy: {acc} open gates: {np.mean(self.open_gates).item()}") 213 | else: 214 | meta_dict = {"clustering": self.classifier_net.state_dict()} 215 | torch.save(meta_dict, f'sparse_model_nogates_best_pbmc_seed_{self.cfg.seed}.pth') 216 | print(f"New best accuracy: {acc}") 217 | format_str = '' # '_kmeans' if self.current_epoch == 9 else '' 218 | self.log(f'val/acc_single{format_str}', acc) # this is ACC 219 | if hasattr(self, 'gating_net'): 220 | self.log("val/num_open_gates", np.mean(self.open_gates).item()) 221 | meta_dict = {"gating": self.gating_net.state_dict(), "clustering": self.classifier_net.state_dict()} 222 | torch.save(meta_dict, f'sparse_model_last_pbmc_beta_{self.cfg.reg_beta}_seed_{self.cfg.seed}.pth') 223 | self.update_stats(acc, np.mean(self.open_gates).item()) 224 | else: 225 | meta_dict = {"clustering": self.classifier_net.state_dict()} 226 | torch.save(meta_dict, f'sparse_model_nogates_last_pbmc_seed_{self.cfg.seed}.pth') 227 | self.update_stats(acc, None) 228 | 229 | def update_stats(self, acc, local_feats=None): 230 | if self.best_acc <= acc: 231 | self.best_acc = acc 232 | if local_feats is not None: 233 | self.best_local_feats = local_feats 234 | 235 | 236 | class ClassificationModule(BaseModule): 237 | def __init__(self, cfg): 238 | self.train_dataset, self.test_dataset = PBMC.setup(cfg.data_dir) 239 | print(f"Train Dataset length: {self.train_dataset.__len__()}") 240 | print(f"Test Dataset length: {self.test_dataset.__len__()}") 241 | cfg.input_dim = self.train_dataset.num_features() 242 | cfg.n_clusters = self.train_dataset.num_classes() 243 | self.batch_size = min(self.train_dataset.__len__(), cfg.batch_size) 244 | super().__init__(cfg) 245 | 246 | def train_dataloader(self): 247 | return DataLoader(self.train_dataset, 248 | batch_size=self.batch_size, 249 | drop_last=True, 250 | shuffle=True, 251 | num_workers=0) 252 | 253 | def val_dataloader(self): 254 | return DataLoader(self.test_dataset, 255 | batch_size=self.batch_size, 256 | drop_last=False, 257 | shuffle=False, 258 | num_workers=0) 259 | 260 | 261 | class Classifier(torch.nn.Module): 262 | def __init__(self, cfg): 263 | super(Classifier, self).__init__() 264 | self.cfg = cfg 265 | self.encoder = torch.nn.Sequential( 266 | torch.nn.Linear(cfg.input_dim, 512), 267 | torch.nn.BatchNorm1d(512), 268 | torch.nn.ReLU(), 269 | ) 270 | self.head = torch.nn.Sequential( 271 | torch.nn.Linear(512, 2048), 272 | torch.nn.BatchNorm1d(2048), 273 | torch.nn.ReLU(), 274 | torch.nn.Linear(2048, cfg.n_clusters), 275 | ) 276 | 277 | self.encoder.apply(self.init_weights_normal) 278 | self.head.apply(self.init_weights_normal) 279 | 280 | @staticmethod 281 | def init_weights_normal(m): 282 | if isinstance(m, torch.nn.Linear): 283 | torch.nn.init.xavier_normal_(m.weight) 284 | if 'bias' in vars(m).keys(): 285 | m.bias.data.fill_(0.0) 286 | 287 | def pretrain_forward(self, x): 288 | return self.decoder(self.encoder(x)) 289 | 290 | 291 | class GatingNet(torch.nn.Module): 292 | def __init__(self, cfg): 293 | super(GatingNet, self).__init__() 294 | self.cfg = cfg 295 | self._sqrt_2 = math.sqrt(2) 296 | self.sigma = cfg.sigma 297 | self.net = torch.nn.Sequential( 298 | torch.nn.Linear(cfg.input_dim, 512), 299 | torch.nn.ReLU(), 300 | torch.nn.Linear(512, 2048), 301 | torch.nn.ReLU(), 302 | torch.nn.Linear(2048, 512), 303 | torch.nn.ReLU(), 304 | torch.nn.Linear(512, cfg.input_dim), 305 | torch.nn.Tanh() 306 | ) 307 | self.net.apply(self.init_weights) 308 | 309 | def init_weights(self, m): 310 | if isinstance(m, torch.nn.Linear): 311 | torch.nn.init.xavier_normal_(m.weight) 312 | if m.out_features == self.cfg.input_dim: 313 | m.bias.data.fill_(.5) 314 | else: 315 | m.bias.data.fill_(0.0) 316 | 317 | def global_forward(self, batch_size, y): 318 | noise = torch.normal(mean=0, std=self.sigma, size=(batch_size, self.cfg.input_dim), 319 | device=self.global_gates_net.weight.device) 320 | z = torch.tanh(self.global_gates_net(y)).reshape(1, -1).repeat(batch_size, 1) + noise * self.training 321 | gates = self.hard_sigmoid(z) 322 | return torch.tanh(self.global_gates_net(y)), gates 323 | 324 | def open_global_gates(self): 325 | return self.hard_sigmoid(torch.tanh(self.global_gates_net.weight)).sum(dim=1).mean().cpu().item() 326 | 327 | def forward(self, x): 328 | noise = torch.normal(mean=0, std=self.sigma, size=x.size(), device=x.device) 329 | mu = self.net(x) 330 | z = mu + noise * self.training 331 | gates = self.hard_sigmoid(z) 332 | sparse_x = x * gates 333 | return mu, sparse_x, gates 334 | 335 | @staticmethod 336 | def hard_sigmoid(x): 337 | return torch.clamp(x + .5, 0.0, 1.0) 338 | 339 | def regularization(self, mu, reduction_func=torch.mean): 340 | return max(reduction_func(0.5 - 0.5 * torch.erf((-0.5 - mu) / (0.5 * self._sqrt_2))), 341 | torch.tensor(1 - self.cfg.target_sparsity, device=mu.device, dtype=mu.data.dtype)) 342 | 343 | def get_gates(self, x): 344 | with torch.no_grad(): 345 | gates = self.hard_sigmoid(self.net(x)) 346 | return gates 347 | 348 | def num_open_gates(self, x): 349 | return torch.sum(self.get_gates(x) > 0).item() / x.size(0) 350 | 351 | 352 | def train_test(cfg): 353 | torch.use_deterministic_algorithms(True) 354 | torch.backends.cudnn.deterministic = True 355 | torch.backends.cudnn.benchmark = False 356 | gated_str = "_gated" if cfg.gated else "" 357 | with open(f"results_{os.path.basename(__file__)}{gated_str}_reg_beta_{cfg.reg_beta}.txt", mode='w') as f: 358 | 359 | header = '\t'.join(['seed', 'acc', 'local_gates']) 360 | f.write(f"{header}\n") 361 | f.flush() 362 | 363 | for seed in range(cfg.repitions): 364 | cfg.seed = seed 365 | seed_everything(seed) 366 | np.random.seed(seed) 367 | if not os.path.exists(cfg.dataset): 368 | os.makedirs(cfg.dataset) 369 | model = ClassificationModule(cfg) 370 | logger = TensorBoardLogger(cfg.dataset, name=os.path.basename(__file__), log_graph=False) 371 | trainer = Trainer( 372 | devices=cfg.devices, 373 | accelerator=cfg.accelerator, 374 | max_epochs=cfg.max_epochs, 375 | deterministic=cfg.deterministic, 376 | logger=cfg.logger, 377 | log_every_n_steps=cfg.log_every_n_steps, 378 | check_val_every_n_epoch=cfg.check_val_every_n_epoch, 379 | enable_checkpointing=cfg.enable_checkpointing, 380 | callbacks=[LearningRateMonitor(logging_interval='step')] 381 | ) 382 | trainer.logger = logger 383 | trainer.fit(model) 384 | if cfg.gated: 385 | results_str = '\t'.join([f'{seed}', f'{model.best_acc}', f'{model.best_local_feats}']) 386 | else: 387 | results_str = '\t'.join([f'{seed}', f'{model.best_acc}']) 388 | f.write(f"{results_str}\n") 389 | f.flush() 390 | 391 | 392 | if __name__ == "__main__": 393 | cfg = parse_args(None) 394 | train_test(cfg) 395 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import math 3 | 4 | 5 | def init_weights_normal(m): 6 | if isinstance(m, torch.nn.Linear): 7 | torch.nn.init.normal_(m.weight, std=0.001) 8 | if 'bias' in vars(m).keys(): 9 | m.bias.data.fill_(0.0) 10 | 11 | 12 | def clustering_head(cfg): 13 | return torch.nn.Sequential( 14 | torch.nn.Linear(cfg.clustering_head[0], cfg.clustering_head[1]), 15 | torch.nn.BatchNorm1d(cfg.clustering_head[1]), 16 | torch.nn.ReLU(), 17 | torch.nn.Linear(cfg.clustering_head[1], cfg.n_clusters)).apply(init_weights_normal) 18 | 19 | 20 | def aux_classifier_head(cfg): 21 | return torch.nn.Sequential( 22 | torch.nn.Linear(cfg.input_dim, cfg.aux_classifier[0]), 23 | torch.nn.BatchNorm1d(cfg.aux_classifier[0]), 24 | torch.nn.ReLU(), 25 | torch.nn.Linear(cfg.aux_classifier[0], cfg.n_clusters)).apply(init_weights_normal) 26 | 27 | 28 | class EncoderDecoder(torch.nn.Module): 29 | def __init__(self, cfg): 30 | super(EncoderDecoder, self).__init__() 31 | self.cfg = cfg 32 | self.encoder = [] 33 | self.encoder = self.build_encoder() 34 | self.decoder = self.build_decoder() 35 | self.encoder.apply(init_weights_normal) 36 | self.decoder.apply(init_weights_normal) 37 | 38 | def build_encoder(self): 39 | layers = [ 40 | torch.nn.Linear(self.cfg.input_dim, self.cfg.encdec[0]), 41 | torch.nn.BatchNorm1d(self.cfg.encdec[0]), 42 | torch.nn.ReLU() 43 | ] 44 | hidden_layers = len(self.cfg.encdec) // 2 + 1 45 | for layer_idx in range(1, hidden_layers): 46 | if layer_idx == hidden_layers - 1: 47 | layers += [torch.nn.Linear(self.cfg.encdec[layer_idx - 1], self.cfg.encdec[layer_idx])] 48 | else: 49 | layers += [ 50 | torch.nn.Linear(self.cfg.encdec[layer_idx - 1], self.cfg.encdec[layer_idx]), 51 | torch.nn.BatchNorm1d(self.cfg.encdec[layer_idx]), 52 | torch.nn.ReLU() 53 | ] 54 | return torch.nn.Sequential(*layers) 55 | 56 | def build_decoder(self): 57 | hidden_layers = len(self.cfg.encdec) // 2 + 1 58 | layers = [] 59 | for layer_idx in range(hidden_layers, len(self.cfg.encdec)): 60 | layers += [ 61 | torch.nn.Linear(self.cfg.encdec[layer_idx - 1], self.cfg.encdec[layer_idx]), 62 | torch.nn.BatchNorm1d(self.cfg.encdec[layer_idx]), 63 | torch.nn.ReLU() 64 | ] 65 | layers += [torch.nn.Linear(self.cfg.encdec[-1], self.cfg.input_dim)] 66 | return torch.nn.Sequential(*layers) 67 | 68 | def forward(self, x): 69 | return self.decoder(self.encoder(x)) 70 | 71 | class GatingNet(torch.nn.Module): 72 | def __init__(self, cfg): 73 | super(GatingNet, self).__init__() 74 | self.cfg = cfg 75 | self._sqrt_2 = math.sqrt(2) 76 | self.sigma = 0.5 77 | self.local_gates = torch.nn.Sequential( 78 | torch.nn.Linear(cfg.input_dim, cfg.gates_hidden_dim), 79 | torch.nn.Tanh(), 80 | torch.nn.Linear(cfg.gates_hidden_dim, cfg.input_dim), 81 | torch.nn.Tanh() 82 | ) 83 | self.local_gates.apply(self.init_weights) 84 | self.global_gates_net = torch.nn.Embedding(self.cfg.n_clusters, self.cfg.input_dim) 85 | torch.nn.init.normal_(self.global_gates_net.weight, std=0.01) 86 | 87 | @staticmethod 88 | def init_weights(m): 89 | if isinstance(m, torch.nn.Linear): 90 | torch.nn.init.normal_(m.weight, std=0.001) 91 | if 'bias' in vars(m).keys(): 92 | m.bias.data.fill_(0.0) 93 | 94 | def global_forward(self, y): 95 | noise = torch.normal(mean=0, std=self.sigma, size=(y.size(0), self.cfg.input_dim), 96 | device=self.global_gates_net.weight.device) 97 | z = torch.tanh(self.global_gates_net(y)) + .5 * noise * self.training 98 | gates = self.hard_sigmoid(z) 99 | return torch.tanh(self.global_gates_net(y)), gates 100 | 101 | def open_global_gates(self): 102 | return self.hard_sigmoid(torch.tanh(self.global_gates_net.weight)).sum(dim=1).mean().cpu().item() 103 | 104 | def forward(self, x): 105 | noise = torch.normal(mean=0, std=self.sigma, size=x.size(), device=x.device) 106 | mu = self.local_gates(x) 107 | z = mu + .5 * noise * self.training 108 | gates = self.hard_sigmoid(z) 109 | sparse_x = x * gates 110 | return mu, sparse_x, gates 111 | 112 | @staticmethod 113 | def hard_sigmoid(x): 114 | return torch.clamp(x + .5, 0.0, 1.0) 115 | 116 | def regularization(self, mu, reduction_func=torch.mean): 117 | return reduction_func(0.5 - 0.5 * torch.erf((-1 / 2 - mu) / self._sqrt_2)) 118 | 119 | def get_gates(self, x): 120 | with torch.no_grad(): 121 | gates = self.hard_sigmoid(self.local_gates(x)) 122 | return gates 123 | 124 | def num_open_gates(self, x, ): 125 | return self.get_gates(x).sum(dim=1).cpu().median(dim=0)[0].item() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | torch>=2.0.1 2 | pytorch-lightning==2.0.0 3 | scikit-learn==1.1.2 4 | scipy==1.10.0-rc1 5 | omegaconf==2.2.3 6 | matplotlib==3.6.3 7 | matplotlib-inline==0.1.6 8 | umap-learn==0.5.6 9 | torchvision==0.15.2 10 | munkres==1.1.4 -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | import torch 3 | import math 4 | from omegaconf import OmegaConf 5 | import torch.nn.functional as F 6 | from torch.utils.data import DataLoader 7 | from pytorch_lightning import LightningModule 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | from pytorch_lightning import Trainer, seed_everything 11 | import os 12 | from pytorch_lightning.loggers import TensorBoardLogger 13 | from pytorch_lightning.callbacks import LearningRateMonitor 14 | from sklearn.metrics import silhouette_score, davies_bouldin_score 15 | import argparse 16 | from dataset import NumpyTableDataset 17 | from model import clustering_head, aux_classifier_head, EncoderDecoder, GatingNet 18 | import umap 19 | 20 | 21 | class TotalCodingRateWithProjection(torch.nn.Module): 22 | """ Based on https://github.com/zengyi-li/NMCE-release/blob/main/NMCE/loss.py """ 23 | 24 | def __init__(self, cfg): 25 | super().__init__() 26 | self.eps = cfg.gtcr_eps 27 | if cfg.gtcr_projection_dim is not None: 28 | self.random_matrix = torch.tensor(np.random.normal( 29 | loc=0.0, 30 | scale=1.0 / np.sqrt(cfg.gtcr_projection_dim), 31 | size=(cfg.input_dim, cfg.gtcr_projection_dim) 32 | )).float() 33 | else: 34 | self.random_matrix = None 35 | 36 | def compute_discrimn_loss(self, W): 37 | p, m = W.shape # [d, B] 38 | I = torch.eye(p, device=W.device) 39 | scalar = p / (m * self.eps) 40 | logdet = torch.logdet(I + scalar * W.matmul(W.T)) 41 | return logdet / 2. 42 | 43 | def forward(self, x): 44 | if self.random_matrix is not None: 45 | x = x @ self.random_matrix.to(x.device) 46 | return - self.compute_discrimn_loss(x.T) 47 | 48 | 49 | class MaximalCodingRateReduction(torch.nn.Module): 50 | """ Based on https://github.com/zengyi-li/NMCE-release/blob/main/NMCE/loss.py """ 51 | 52 | def __init__(self, eps=0.01, gamma=1, compress_only=False): 53 | super(MaximalCodingRateReduction, self).__init__() 54 | self.eps = eps 55 | self.gamma = gamma 56 | self.compress_only = compress_only 57 | 58 | def compute_discrimn_loss(self, W): 59 | p, m = W.shape 60 | I = torch.eye(p, device=W.device) 61 | scalar = p / (m * self.eps) 62 | logdet = torch.logdet(I + scalar * W.matmul(W.T)) 63 | return logdet / 2. 64 | 65 | def compute_compress_loss(self, W, Pi): 66 | p, m = W.shape 67 | k, _, _ = Pi.shape 68 | I = torch.eye(p, device=W.device).expand((k, p, p)) 69 | trPi = Pi.sum(2) + 1e-8 70 | scale = (p / (trPi * self.eps)).view(k, 1, 1) 71 | W = W.view((1, p, m)) 72 | log_det = torch.logdet(I + scale * W.mul(Pi).matmul(W.transpose(1, 2))) 73 | compress_loss = (trPi.squeeze() * log_det / (2 * m)).sum() 74 | return compress_loss 75 | 76 | def forward(self, X, Y, num_classes=None): 77 | # This function support Y as label integer or membership probablity. 78 | if len(Y.shape) == 1: 79 | # if Y is a label vector 80 | if num_classes is None: 81 | num_classes = Y.max() + 1 82 | Pi = torch.zeros((num_classes, 1, Y.shape[0]), device=Y.device) 83 | for indx, label in enumerate(Y): 84 | Pi[label, 0, indx] = 1 85 | else: 86 | # if Y is a probility matrix 87 | if num_classes is None: 88 | num_classes = Y.shape[1] 89 | Pi = Y.T.reshape((num_classes, 1, -1)) 90 | 91 | W = X.T 92 | compress_loss = self.compute_compress_loss(W, Pi) 93 | if not self.compress_only: 94 | discrimn_loss = self.compute_discrimn_loss(W) 95 | return discrimn_loss, compress_loss 96 | else: 97 | return None, compress_loss 98 | 99 | 100 | class BaseModule(LightningModule): 101 | def __init__(self, cfg): 102 | super().__init__() 103 | self.cfg = cfg 104 | 105 | self.train_dataset = NumpyTableDataset.setup( 106 | filepath_samples=cfg.get("filepath_samples"), 107 | num_clusters=cfg.get("num_clusters", None) 108 | ) 109 | self.val_dataset = self.train_dataset 110 | 111 | print(f"Dataset length: {self.train_dataset.__len__()}") 112 | self.cfg.input_dim = self.train_dataset.num_features() 113 | self.cfg.n_clusters = self.train_dataset.num_clusters 114 | self.batch_size = min(self.train_dataset.__len__(), cfg.batch_size) 115 | 116 | self.save_hyperparameters() 117 | self.best_evaluation_stats = {} 118 | self.ae_train = False 119 | self.automatic_optimization = False 120 | self.best_accuracy = - np.infty 121 | self.gating_net = GatingNet(self.cfg) 122 | self.encdec = EncoderDecoder(self.cfg) 123 | self.clustering_head = clustering_head(self.cfg) 124 | self.aux_classifier_head = aux_classifier_head(self.cfg) 125 | self.mcrr = MaximalCodingRateReduction(eps=self.cfg.eps, compress_only=True) 126 | self.gtcr_loss = TotalCodingRateWithProjection(self.cfg) 127 | 128 | self.open_gates = [] 129 | self.val_embs_list = [] 130 | 131 | self.max_silhouette_score = [] 132 | self.min_dbi_score = [] 133 | 134 | def train_dataloader(self): 135 | return DataLoader(self.train_dataset, 136 | batch_size=self.batch_size, 137 | drop_last=True, 138 | shuffle=True, 139 | num_workers=0) 140 | 141 | def val_dataloader(self): 142 | return DataLoader(self.val_dataset, 143 | batch_size=self.batch_size, 144 | drop_last=False, 145 | shuffle=False, 146 | num_workers=0) 147 | 148 | def global_gates_step(self, x): 149 | gates = self.gating_net.get_gates(x) 150 | ae_emb = self.encdec.encoder(x * gates) 151 | cluster_logits = self.clustering_head(ae_emb) 152 | y_hat = cluster_logits.argmax(dim=-1) 153 | glob_gates_mu, glob_gates = self.gating_net.global_forward(y_hat) 154 | reg_loss = self.gating_net.regularization(glob_gates_mu) 155 | aux_y_hat = self.aux_classifier_head(x * gates * glob_gates) 156 | aux_loss = F.cross_entropy(aux_y_hat, y_hat) 157 | self.log('glob_gates_reg_loss', reg_loss.item()) 158 | self.log('glob_gates_ce_loss', aux_loss.item()) 159 | return aux_loss + self.cfg.global_gates_lambda * reg_loss 160 | 161 | def ae_step(self, x): 162 | if self.current_epoch > self.cfg.ae_non_gated_epochs: 163 | mu, _, gates = self.gating_net(x) 164 | reg_loss = self.gating_net.regularization(mu) 165 | gtcr_loss = self.gtcr_loss(gates) / x.size(0) 166 | self.log("pretrain/gates_reg_loss", reg_loss.item()) 167 | self.log("pretrain/gates_tcr_loss", gtcr_loss.item()) 168 | loss = self.cosine_increase_lambda( 169 | min_val=0., 170 | max_val=self.cfg.local_gates_lambda 171 | ) * reg_loss + gtcr_loss * self.cfg.gtcr_lambda 172 | else: 173 | gates = torch.ones_like(x, device=x.device).float() 174 | loss = 0 175 | 176 | # task 1: reconstruct x from x 177 | x_recon = self.encdec(x) 178 | x_recon_loss = F.mse_loss(x_recon, x) 179 | self.log("pretrain/x_recon_loss", x_recon_loss.item()) 180 | 181 | # task 2: reconstruct x from gated x: 182 | x_recon_from_gated = self.encdec(x * gates) 183 | x_from_gated_x_recon_loss = F.mse_loss(x_recon_from_gated, x) 184 | self.log("pretrain/x_from_gated_x_recon_loss", x_from_gated_x_recon_loss.item()) 185 | 186 | # task 3: reconstruct x from randomly masked x 187 | mask_rnd = torch.rand(x.size()).to(x.device) 188 | mask = torch.ones(x.size()).to(x.device).float() 189 | mask[mask_rnd < self.cfg.mask_percentage] = 0 190 | x_recon_masked = self.encdec(x * mask) 191 | input_noised_recon_loss = F.mse_loss(x_recon_masked, x) 192 | self.log("pretrain/input_noised_recon_loss", input_noised_recon_loss.item()) 193 | 194 | # task 4: reconstruct x from noisy embedding 195 | e = self.encdec.encoder(x) 196 | e = e * torch.normal(mean=1., std=self.cfg.latent_noise_std, size=e.size(), device=e.device) 197 | recon_noised = self.encdec.decoder(e) 198 | noised_aug_loss = F.mse_loss(recon_noised, x) 199 | self.log("pretrain/latent_noised_recon_loss", noised_aug_loss.item()) 200 | 201 | # combined loss: 202 | loss = loss + x_recon_loss + x_from_gated_x_recon_loss + input_noised_recon_loss + noised_aug_loss 203 | return loss 204 | 205 | def training_step(self, x, batch_idx): 206 | ae_opt, clust_opt, glob_gates_opt = self.optimizers() 207 | pretrain_sched, sch = self.lr_schedulers() 208 | x = x.reshape(x.size(0), -1) 209 | 210 | # reconstruction step + local gates training 211 | if self.current_epoch <= self.cfg.ae_pretrain_epochs: 212 | ae_opt.zero_grad() 213 | loss = self.ae_step(x) 214 | self.manual_backward(loss) 215 | ae_opt.step() 216 | pretrain_sched.step() 217 | return 218 | 219 | # clusters compression step 220 | clust_opt.zero_grad() 221 | gates = self.gating_net.get_gates(x) 222 | ae_emb = self.encdec.encoder(x * gates) 223 | cluster_logits = self.clustering_head(ae_emb) 224 | loss = self.mcrr_loss(ae_emb, cluster_logits) 225 | self.manual_backward(loss) 226 | clust_opt.step() 227 | 228 | # global gates training 229 | if self.current_epoch >= self.cfg.start_global_gates_training_on_epoch: 230 | glob_gates_opt.zero_grad() 231 | loss = self.global_gates_step(x) 232 | self.manual_backward(loss) 233 | glob_gates_opt.step() 234 | sch.step() 235 | 236 | def configure_optimizers(self): 237 | pretrain_optimizer = torch.optim.Adam( 238 | params=chain( 239 | self.encdec.parameters(), 240 | self.gating_net.local_gates.parameters(), 241 | ), 242 | lr=self.cfg.lr.pretrain) 243 | 244 | cluster_optimizer = torch.optim.Adam( 245 | params=chain( 246 | self.clustering_head.parameters(), 247 | ), 248 | lr=self.cfg.lr.clustering) 249 | 250 | glob_gates_opt = torch.optim.SGD( 251 | params=chain( 252 | self.aux_classifier_head.parameters(), 253 | self.gating_net.global_gates_net.parameters(), 254 | ), 255 | lr=self.cfg.lr.aux_classifier) 256 | 257 | steps = self.train_dataset.__len__() // self.batch_size * ( 258 | self.cfg.trainer.max_epochs - self.cfg.ae_pretrain_epochs) 259 | pretrain_steps = self.train_dataset.__len__() // self.batch_size * self.cfg.ae_pretrain_epochs 260 | # pretrain_steps = self.dataset.__len__() // self.batch_size * self.cfg.trainer.max_epochs 261 | print(f"Cosine annealing LR scheduling is applied during {steps} steps") 262 | sched = torch.optim.lr_scheduler.CosineAnnealingLR( 263 | optimizer=cluster_optimizer, 264 | T_max=steps, 265 | eta_min=self.cfg.sched.clustering_min_lr) 266 | pretrain_sched = torch.optim.lr_scheduler.CosineAnnealingLR( 267 | optimizer=pretrain_optimizer, 268 | T_max=pretrain_steps, 269 | eta_min=self.cfg.sched.pretrain_min_lr) 270 | return [pretrain_optimizer, cluster_optimizer, glob_gates_opt], [pretrain_sched, sched] 271 | 272 | def cosine_increase_lambda(self, min_val, max_val): 273 | epoch = self.current_epoch - self.cfg.ae_pretrain_epochs 274 | total_epochs = self.cfg.ae_pretrain_epochs - self.cfg.ae_non_gated_epochs 275 | return min_val + 0.5 * (max_val - min_val) * (1. + np.cos(epoch * math.pi / total_epochs)) 276 | 277 | def validation_step(self, x, batch_idx): 278 | if not (self.ae_train and self.current_epoch < self.cfg.ae_pretrain_epochs) and self.current_epoch > 0: 279 | gates = self.gating_net.get_gates(x) 280 | ae_emb = self.encdec.encoder(x * gates) 281 | cluster_logits = self.clustering_head(ae_emb) 282 | y_hat = cluster_logits.argmax(dim=-1) 283 | self.val_cluster_list.append(y_hat.cpu()) 284 | self.open_gates.append(self.gating_net.num_open_gates(x)) 285 | self.val_embs_list.append(ae_emb) 286 | 287 | def on_validation_epoch_start(self): 288 | self.val_cluster_list = [] 289 | self.open_gates = [] 290 | self.val_embs_list = [] 291 | 292 | @staticmethod 293 | def plot_clustering(val_embs_list, cluster_mtx, current_epoch, silhouette, dbi): 294 | reducer = umap.UMAP(n_neighbors=10, min_dist=0.1, n_components=2, random_state=0) 295 | embedding = reducer.fit_transform(torch.cat(val_embs_list, dim=0).cpu().numpy()) 296 | plt.figure(figsize=(10, 7)) 297 | plt.scatter(embedding[:, 0], embedding[:, 1], c=cluster_mtx.numpy(), s=50, edgecolor='k') 298 | plt.title(f'Clustering (UMAP). Epoch: {current_epoch}. Silhouette: {silhouette:0.3f}. DBI: {dbi:0.3f}') 299 | plt.savefig(f"umap_epoch_{current_epoch}.png") 300 | 301 | def on_validation_epoch_end(self): 302 | if not (self.ae_train and self.current_epoch < self.cfg.ae_pretrain_epochs) and self.current_epoch > 0: 303 | if self.current_epoch < self.cfg.ae_pretrain_epochs - 1: 304 | return 305 | else: 306 | cluster_mtx = torch.cat(self.val_cluster_list, dim=0) 307 | self.log("num_open_gates", np.mean(self.open_gates).item()) 308 | self.log("num_open_global_gates", self.gating_net.open_global_gates()) 309 | if self.cfg.save_seed_checkpoints: 310 | meta_dict = {"gating": self.gating_net.state_dict(), "clustering": self.clustering_net.state_dict()} 311 | torch.save(meta_dict, f'sparse_model_last_{self.cfg.dataset}_seed_{self.cfg.seed}.pth') 312 | try: 313 | silhouette_score_embs = silhouette_score(torch.cat(self.val_embs_list, dim=0).cpu().numpy(), 314 | cluster_mtx.numpy()) 315 | self.log(f'silhouette_score_embs', silhouette_score_embs) 316 | self.max_silhouette_score.append(silhouette_score_embs) 317 | except: 318 | silhouette_score_embs = -1 319 | try: 320 | dbi_score = davies_bouldin_score(torch.cat(self.val_embs_list, dim=0).cpu().numpy(), 321 | cluster_mtx.numpy()) 322 | self.log(f'dbi_score_embs', dbi_score) 323 | self.min_dbi_score.append(dbi_score) 324 | except: 325 | dbi_score = 0 326 | 327 | self.plot_clustering(self.val_embs_list, cluster_mtx, self.current_epoch, silhouette_score_embs, dbi_score) 328 | 329 | def mcrr_loss(self, c, logits): 330 | logprobs = torch.log_softmax(logits, dim=-1) 331 | prob = GumbleSoftmax(self.tau())(logprobs) 332 | _, compress_loss = self.mcrr(F.normalize(c), prob, num_classes=self.cfg.n_clusters) 333 | compress_loss /= c.size(1) 334 | self.log(f'compress_loss', compress_loss.item()) 335 | return compress_loss 336 | 337 | def tau(self): 338 | return self.cfg.tau 339 | 340 | 341 | class GumbleSoftmax(torch.nn.Module): 342 | def __init__(self, tau, straight_through=False): 343 | super().__init__() 344 | self.tau = tau 345 | self.straight_through = straight_through 346 | 347 | def forward(self, logps): 348 | gumble = torch.rand_like(logps).log().mul(-1).log().mul(-1) 349 | logits = logps + gumble 350 | out = (logits / self.tau).softmax(dim=1) 351 | if not self.straight_through: 352 | return out 353 | else: 354 | out_binary = (logits * 1e8).softmax(dim=1).detach() 355 | out_diff = (out_binary - out).detach() 356 | return out_diff + out 357 | 358 | 359 | if __name__ == "__main__": 360 | parser = argparse.ArgumentParser() 361 | parser.add_argument('--cfg', type=str) 362 | args = parser.parse_args() 363 | cfg = OmegaConf.load(args.cfg) 364 | torch.use_deterministic_algorithms(True) 365 | torch.backends.cudnn.deterministic = True 366 | torch.backends.cudnn.benchmark = False 367 | for seed in range(cfg.seeds): 368 | cfg.seed = seed 369 | seed_everything(seed) 370 | np.random.seed(seed) 371 | model = BaseModule(cfg) 372 | logger = TensorBoardLogger("logs", name=os.path.basename(__file__), log_graph=False) 373 | trainer = Trainer(**cfg.trainer, callbacks=[LearningRateMonitor(logging_interval='step')]) 374 | trainer.logger = logger 375 | trainer.fit(model) 376 | -------------------------------------------------------------------------------- /train_evaluate.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | import torch 3 | import math 4 | from omegaconf import OmegaConf 5 | import torch.nn.functional as F 6 | from torch.utils.data import DataLoader 7 | from pytorch_lightning import LightningModule 8 | import numpy as np 9 | from pytorch_lightning import Trainer, seed_everything 10 | import os 11 | from pytorch_lightning.loggers import TensorBoardLogger 12 | from pytorch_lightning.callbacks import LearningRateMonitor 13 | from sklearn.metrics import normalized_mutual_info_score, adjusted_rand_score, silhouette_score, davies_bouldin_score 14 | import argparse 15 | import dataset 16 | from model import clustering_head, aux_classifier_head, EncoderDecoder, GatingNet 17 | 18 | 19 | class TotalCodingRateWithProjection(torch.nn.Module): 20 | """ Based on https://github.com/zengyi-li/NMCE-release/blob/main/NMCE/loss.py """ 21 | def __init__(self, cfg): 22 | super().__init__() 23 | self.eps = cfg.gtcr_eps 24 | if cfg.gtcr_projection_dim is not None: 25 | self.random_matrix = torch.tensor(np.random.normal( 26 | loc=0.0, 27 | scale=1.0 / np.sqrt(cfg.gtcr_projection_dim), 28 | size=(cfg.input_dim, cfg.gtcr_projection_dim) 29 | )).float() 30 | else: 31 | self.random_matrix = None 32 | 33 | def compute_discrimn_loss(self, W): 34 | p, m = W.shape # [d, B] 35 | I = torch.eye(p, device=W.device) 36 | scalar = p / (m * self.eps) 37 | logdet = torch.logdet(I + scalar * W.matmul(W.T)) 38 | return logdet / 2. 39 | 40 | def forward(self, x): 41 | if self.random_matrix is not None: 42 | x = x @ self.random_matrix.to(x.device) 43 | return - self.compute_discrimn_loss(x.T) 44 | 45 | 46 | class MaximalCodingRateReduction(torch.nn.Module): 47 | """ Based on https://github.com/zengyi-li/NMCE-release/blob/main/NMCE/loss.py """ 48 | 49 | def __init__(self, eps=0.01, gamma=1, compress_only=False): 50 | super(MaximalCodingRateReduction, self).__init__() 51 | self.eps = eps 52 | self.gamma = gamma 53 | self.compress_only = compress_only 54 | 55 | def compute_discrimn_loss(self, W): 56 | p, m = W.shape 57 | I = torch.eye(p, device=W.device) 58 | scalar = p / (m * self.eps) 59 | logdet = torch.logdet(I + scalar * W.matmul(W.T)) 60 | return logdet / 2. 61 | 62 | def compute_compress_loss(self, W, Pi): 63 | p, m = W.shape 64 | k, _, _ = Pi.shape 65 | I = torch.eye(p, device=W.device).expand((k, p, p)) 66 | trPi = Pi.sum(2) + 1e-8 67 | scale = (p / (trPi * self.eps)).view(k, 1, 1) 68 | W = W.view((1, p, m)) 69 | log_det = torch.logdet(I + scale * W.mul(Pi).matmul(W.transpose(1, 2))) 70 | compress_loss = (trPi.squeeze() * log_det / (2 * m)).sum() 71 | return compress_loss 72 | 73 | def forward(self, X, Y, num_classes=None): 74 | # This function support Y as label integer or membership probablity. 75 | if len(Y.shape) == 1: 76 | # if Y is a label vector 77 | if num_classes is None: 78 | num_classes = Y.max() + 1 79 | Pi = torch.zeros((num_classes, 1, Y.shape[0]), device=Y.device) 80 | for indx, label in enumerate(Y): 81 | Pi[label, 0, indx] = 1 82 | else: 83 | # if Y is a probility matrix 84 | if num_classes is None: 85 | num_classes = Y.shape[1] 86 | Pi = Y.T.reshape((num_classes, 1, -1)) 87 | 88 | W = X.T 89 | compress_loss = self.compute_compress_loss(W, Pi) 90 | if not self.compress_only: 91 | discrimn_loss = self.compute_discrimn_loss(W) 92 | return discrimn_loss, compress_loss 93 | else: 94 | return None, compress_loss 95 | 96 | 97 | class BaseModule(LightningModule): 98 | def __init__(self, cfg): 99 | super().__init__() 100 | self.cfg = cfg 101 | self.train_dataset = getattr(dataset, cfg.dataset).setup(cfg) 102 | self.val_dataset = self.train_dataset 103 | 104 | print(f"Dataset length: {self.train_dataset.__len__()}") 105 | self.cfg.input_dim = self.train_dataset.num_features() 106 | self.cfg.n_clusters = self.train_dataset.num_clusters 107 | self.batch_size = min(self.train_dataset.__len__(), cfg.batch_size) 108 | 109 | self.save_hyperparameters() 110 | self.best_evaluation_stats = {} 111 | self.ae_train = False 112 | self.automatic_optimization = False 113 | self.best_accuracy = - np.infty 114 | self.gating_net = GatingNet(self.cfg) 115 | self.encdec = EncoderDecoder(self.cfg) 116 | self.clustering_head = clustering_head(self.cfg) 117 | self.aux_classifier_head = aux_classifier_head(self.cfg) 118 | self.mcrr = MaximalCodingRateReduction(eps=self.cfg.eps, compress_only=True) 119 | self.gtcr_loss = TotalCodingRateWithProjection(self.cfg) 120 | 121 | self.val_cluster_list = [] 122 | self.val_cluster_list_gated = [] 123 | self.val_label_list = [] 124 | self.open_gates = [] 125 | self.val_embs_list = [] 126 | 127 | self.best_acc = - 100 128 | self.best_ari = - 100 129 | self.best_nmi = - 100 130 | self.best_local_feats = None 131 | self.best_global_feats = None 132 | self.max_silhouette_score = [] 133 | self.min_dbi_score = [] 134 | 135 | def train_dataloader(self): 136 | return DataLoader(self.train_dataset, 137 | batch_size=self.batch_size, 138 | drop_last=True, 139 | shuffle=True, 140 | num_workers=0) 141 | 142 | def val_dataloader(self): 143 | return DataLoader(self.val_dataset, 144 | batch_size=self.batch_size, 145 | drop_last=False, 146 | shuffle=False, 147 | num_workers=0) 148 | 149 | def update_stats(self, acc, ari, nmi, local_feats, global_feats): 150 | if self.best_acc <= acc: 151 | self.best_acc = acc 152 | self.best_ari = ari 153 | self.best_nmi = nmi 154 | self.best_local_feats = local_feats 155 | self.best_global_feats = global_feats 156 | 157 | def global_gates_step(self, x): 158 | gates = self.gating_net.get_gates(x) 159 | ae_emb = self.encdec.encoder(x * gates) 160 | cluster_logits = self.clustering_head(ae_emb) 161 | y_hat = cluster_logits.argmax(dim=-1) 162 | glob_gates_mu, glob_gates = self.gating_net.global_forward(y_hat) 163 | reg_loss = self.gating_net.regularization(glob_gates_mu) 164 | aux_y_hat = self.aux_classifier_head(x * gates * glob_gates) 165 | aux_loss = F.cross_entropy(aux_y_hat, y_hat) 166 | self.log('train/glob_gates_reg_loss', reg_loss.item()) 167 | self.log('train/glob_gates_ce_loss', aux_loss.item()) 168 | return aux_loss + self.cfg.global_gates_lambda * reg_loss 169 | 170 | def ae_step(self, x): 171 | if self.current_epoch > self.cfg.ae_non_gated_epochs: 172 | mu, _, gates = self.gating_net(x) 173 | reg_loss = self.gating_net.regularization(mu) 174 | gtcr_loss = self.gtcr_loss(gates) / x.size(0) 175 | self.log("pretrain/gates_reg_loss", reg_loss.item()) 176 | self.log("pretrain/gates_tcr_loss", gtcr_loss.item()) 177 | loss = self.cosine_increase_lambda( 178 | min_val=0., 179 | max_val=self.cfg.local_gates_lambda 180 | ) * reg_loss + gtcr_loss * self.cfg.gtcr_lambda 181 | else: 182 | gates = torch.ones_like(x, device=x.device).float() 183 | loss = 0 184 | 185 | # task 1: reconstruct x from x 186 | x_recon = self.encdec(x) 187 | x_recon_loss = F.mse_loss(x_recon, x) 188 | self.log("pretrain/x_recon_loss", x_recon_loss.item()) 189 | 190 | # task 2: reconstruct x from gated x: 191 | x_recon_from_gated = self.encdec(x * gates) 192 | x_from_gated_x_recon_loss = F.mse_loss(x_recon_from_gated, x) 193 | self.log("pretrain/x_from_gated_x_recon_loss", x_from_gated_x_recon_loss.item()) 194 | 195 | # task 3: reconstruct x from randomly masked x 196 | mask_rnd = torch.rand(x.size()).to(x.device) 197 | mask = torch.ones(x.size()).to(x.device).float() 198 | mask[mask_rnd < self.cfg.mask_percentage] = 0 199 | x_recon_masked = self.encdec(x * mask) 200 | input_noised_recon_loss = F.mse_loss(x_recon_masked, x) 201 | self.log("pretrain/input_noised_recon_loss", input_noised_recon_loss.item()) 202 | 203 | # task 4: reconstruct x from noisy embedding 204 | e = self.encdec.encoder(x) 205 | e = e * torch.normal(mean=1., std=self.cfg.latent_noise_std, size=e.size(), device=e.device) 206 | recon_noised = self.encdec.decoder(e) 207 | noised_aug_loss = F.mse_loss(recon_noised, x) 208 | self.log("pretrain/latent_noised_recon_loss", noised_aug_loss.item()) 209 | 210 | # combined loss: 211 | loss = loss + x_recon_loss + x_from_gated_x_recon_loss + input_noised_recon_loss + noised_aug_loss 212 | return loss 213 | 214 | def training_step(self, batch, batch_idx): 215 | ae_opt, clust_opt, glob_gates_opt = self.optimizers() 216 | pretrain_sched, sch = self.lr_schedulers() 217 | x, _ = batch 218 | x = x.reshape(x.size(0), -1) 219 | 220 | # reconstruction step + local gates training 221 | if self.current_epoch <= self.cfg.ae_pretrain_epochs: 222 | ae_opt.zero_grad() 223 | loss = self.ae_step(x) 224 | self.manual_backward(loss) 225 | ae_opt.step() 226 | pretrain_sched.step() 227 | return 228 | 229 | # clusters compression step 230 | clust_opt.zero_grad() 231 | gates = self.gating_net.get_gates(x) 232 | ae_emb = self.encdec.encoder(x * gates) 233 | cluster_logits = self.clustering_head(ae_emb) 234 | loss = self.mcrr_loss(ae_emb, cluster_logits) 235 | self.manual_backward(loss) 236 | clust_opt.step() 237 | 238 | # global gates training 239 | if self.current_epoch >= self.cfg.start_global_gates_training_on_epoch: 240 | glob_gates_opt.zero_grad() 241 | loss = self.global_gates_step(x) 242 | self.manual_backward(loss) 243 | glob_gates_opt.step() 244 | sch.step() 245 | 246 | def configure_optimizers(self): 247 | pretrain_optimizer = torch.optim.Adam( 248 | params=chain( 249 | self.encdec.parameters(), 250 | self.gating_net.local_gates.parameters(), 251 | ), 252 | lr=self.cfg.lr.pretrain) 253 | 254 | cluster_optimizer = torch.optim.Adam( 255 | params=chain( 256 | self.clustering_head.parameters(), 257 | ), 258 | lr=self.cfg.lr.clustering) 259 | 260 | glob_gates_opt = torch.optim.SGD( 261 | params=chain( 262 | self.aux_classifier_head.parameters(), 263 | self.gating_net.global_gates_net.parameters(), 264 | ), 265 | lr=self.cfg.lr.aux_classifier) 266 | 267 | steps = self.train_dataset.__len__() // self.batch_size * ( 268 | self.cfg.trainer.max_epochs - self.cfg.ae_pretrain_epochs) 269 | pretrain_steps = self.train_dataset.__len__() // self.batch_size * self.cfg.ae_pretrain_epochs 270 | # pretrain_steps = self.dataset.__len__() // self.batch_size * self.cfg.trainer.max_epochs 271 | print(f"Cosine annealing LR scheduling is applied during {steps} steps") 272 | sched = torch.optim.lr_scheduler.CosineAnnealingLR( 273 | optimizer=cluster_optimizer, 274 | T_max=steps, 275 | eta_min=self.cfg.sched.clustering_min_lr) 276 | pretrain_sched = torch.optim.lr_scheduler.CosineAnnealingLR( 277 | optimizer=pretrain_optimizer, 278 | T_max=pretrain_steps, 279 | eta_min=self.cfg.sched.pretrain_min_lr) 280 | return [pretrain_optimizer, cluster_optimizer, glob_gates_opt], [pretrain_sched, sched] 281 | 282 | def cosine_increase_lambda(self, min_val, max_val): 283 | epoch = self.current_epoch - self.cfg.ae_pretrain_epochs 284 | total_epochs = self.cfg.ae_pretrain_epochs - self.cfg.ae_non_gated_epochs 285 | return min_val + 0.5 * (max_val - min_val) * (1. + np.cos(epoch * math.pi / total_epochs)) 286 | 287 | def validation_step(self, batch, batch_idx): 288 | x, y = batch 289 | gates = self.gating_net.get_gates(x) 290 | ae_emb = self.encdec.encoder(x * gates) 291 | cluster_logits = self.clustering_head(ae_emb) 292 | y_hat = cluster_logits.argmax(dim=-1) 293 | self.val_cluster_list.append(y_hat.cpu()) 294 | self.val_label_list.append(y.cpu()) 295 | self.open_gates.append(self.gating_net.num_open_gates(x)) 296 | self.val_embs_list.append(ae_emb) 297 | 298 | def on_validation_epoch_start(self): 299 | self.val_cluster_list = [] 300 | self.val_cluster_list_gated = [] 301 | self.val_label_list = [] 302 | self.open_gates = [] 303 | self.val_embs_list = [] 304 | 305 | @staticmethod 306 | def cluster_match(cluster_mtx, label_mtx, n_classes=10, print_result=True): 307 | cluster_indx = list(cluster_mtx.unique()) 308 | assigned_label_list = [] 309 | assigned_count = [] 310 | while (len(assigned_label_list) <= n_classes) and len(cluster_indx) > 0: 311 | max_label_list = [] 312 | max_count_list = [] 313 | for indx in cluster_indx: 314 | mask = cluster_mtx == indx 315 | label_elements, counts = label_mtx[mask].unique(return_counts=True) 316 | for assigned_label in assigned_label_list: 317 | counts[label_elements == assigned_label] = 0 318 | max_count_list.append(counts.max()) 319 | max_label_list.append(label_elements[counts.argmax()]) 320 | 321 | max_label = torch.stack(max_label_list) 322 | max_count = torch.stack(max_count_list) 323 | assigned_label_list.append(max_label[max_count.argmax()]) 324 | assigned_count.append(max_count.max()) 325 | cluster_indx.pop(max_count.argmax().item()) 326 | total_correct = torch.tensor(assigned_count).sum().item() 327 | total_sample = cluster_mtx.shape[0] 328 | acc = total_correct / total_sample 329 | if print_result: 330 | print('{}/{} ({}%) correct'.format(total_correct, total_sample, acc * 100)) 331 | else: 332 | return total_correct, total_sample, acc 333 | 334 | def on_validation_epoch_end(self): 335 | """ Based on https://github.com/zengyi-li/NMCE-release/blob/main/NMCE/func.py""" 336 | if not (self.ae_train and self.current_epoch < self.cfg.ae_pretrain_epochs) and self.current_epoch > 0: 337 | if self.current_epoch < self.cfg.ae_pretrain_epochs - 1: 338 | return 339 | else: 340 | cluster_mtx = torch.cat(self.val_cluster_list, dim=0) 341 | label_mtx = torch.cat(self.val_label_list, dim=0) 342 | _, _, acc_single = self.cluster_match( 343 | cluster_mtx, 344 | label_mtx, 345 | n_classes=label_mtx.max() + 1, 346 | print_result=False) 347 | if self.best_accuracy < acc_single: 348 | print("New best accuracy:", acc_single) 349 | self.best_accuracy = acc_single 350 | if self.cfg.save_seed_checkpoints: 351 | meta_dict = {"gating": self.gating_net.state_dict(), "clustering": self.clustering_net.state_dict()} 352 | torch.save(meta_dict, f'sparse_model_best_{self.cfg.dataset}_seed_{self.cfg.seed}.pth') 353 | 354 | nmi = normalized_mutual_info_score(label_mtx.numpy(), cluster_mtx.numpy()) 355 | ari = adjusted_rand_score(label_mtx.numpy(), cluster_mtx.numpy()) 356 | format_str = '' # '_kmeans' if self.current_epoch == 9 else '' 357 | self.log(f'val/acc_single{format_str}', acc_single) # this is ACC 358 | self.log(f'val/NMI{format_str}', nmi) 359 | self.log(f'val/ARI{format_str}', ari) 360 | self.log("val/num_open_gates", np.mean(self.open_gates).item()) 361 | self.log("val/num_open_global_gates", self.gating_net.open_global_gates()) 362 | if self.cfg.save_seed_checkpoints: 363 | meta_dict = {"gating": self.gating_net.state_dict(), "clustering": self.clustering_net.state_dict()} 364 | torch.save(meta_dict, f'sparse_model_last_{self.cfg.dataset}_seed_{self.cfg.seed}.pth') 365 | 366 | self.update_stats(acc_single, ari, nmi, np.mean(self.open_gates).item(), 367 | self.gating_net.open_global_gates()) 368 | 369 | try: 370 | silhouette_score_embs = silhouette_score(torch.cat(self.val_embs_list, dim=0).cpu().numpy(), 371 | cluster_mtx.numpy()) 372 | self.log(f'val/silhouette_score_embs', silhouette_score_embs) 373 | self.max_silhouette_score.append(silhouette_score_embs) 374 | except: 375 | pass 376 | try: 377 | dbi_score = davies_bouldin_score(torch.cat(self.val_embs_list, dim=0).cpu().numpy(), 378 | cluster_mtx.numpy()) 379 | self.log(f'val/dbi_score_embs', dbi_score) 380 | self.min_dbi_score.append(dbi_score) 381 | except: 382 | pass 383 | 384 | def mcrr_loss(self, c, logits): 385 | logprobs = torch.log_softmax(logits, dim=-1) 386 | prob = GumbleSoftmax(self.tau())(logprobs) 387 | _, compress_loss = self.mcrr(F.normalize(c), prob, num_classes=self.cfg.n_clusters) 388 | compress_loss /= c.size(1) 389 | self.log(f'train/compress_loss', compress_loss.item()) 390 | return compress_loss 391 | 392 | def tau(self): 393 | return self.cfg.tau 394 | 395 | 396 | class GumbleSoftmax(torch.nn.Module): 397 | def __init__(self, tau, straight_through=False): 398 | super().__init__() 399 | self.tau = tau 400 | self.straight_through = straight_through 401 | 402 | def forward(self, logps): 403 | gumble = torch.rand_like(logps).log().mul(-1).log().mul(-1) 404 | logits = logps + gumble 405 | out = (logits / self.tau).softmax(dim=1) 406 | if not self.straight_through: 407 | return out 408 | else: 409 | out_binary = (logits * 1e8).softmax(dim=1).detach() 410 | out_diff = (out_binary - out).detach() 411 | return out_diff + out 412 | 413 | 414 | if __name__ == "__main__": 415 | parser = argparse.ArgumentParser() 416 | parser.add_argument('--cfg', type=str) 417 | args = parser.parse_args() 418 | cfg = OmegaConf.load(args.cfg) 419 | torch.use_deterministic_algorithms(True) 420 | torch.backends.cudnn.deterministic = True 421 | torch.backends.cudnn.benchmark = False 422 | if not cfg.validate: 423 | cfg.trainer.check_val_every_n_epoch = cfg.trainer.max_epochs + 1 # the validation will be never done 424 | 425 | with open(f"results_{os.path.basename(__file__)}.txt", mode='a') as f: 426 | header = '\t'.join(['seed', 'acc', 'ari', 'nmi', 'local_gates', 'global_gates', 427 | 'topk_max_silhouette_score', 'topk_min_dbi_score']) 428 | f.write(f"{header}\n") 429 | f.flush() 430 | for seed in range(cfg.seeds): 431 | cfg.seed = seed 432 | seed_everything(seed) 433 | np.random.seed(seed) 434 | if not os.path.exists(cfg.dataset): 435 | os.makedirs(cfg.dataset) 436 | model = BaseModule(cfg) 437 | logger = TensorBoardLogger(cfg.dataset, name=os.path.basename(__file__), log_graph=False) 438 | trainer = Trainer(**cfg.trainer, callbacks=[LearningRateMonitor(logging_interval='step')]) 439 | trainer.logger = logger 440 | trainer.fit(model) 441 | topk_max_siluetter_score = np.mean(sorted(model.max_silhouette_score, reverse=True)[:10]) 442 | topk_min_dbi_score = np.mean(sorted(model.max_silhouette_score)[:10]) 443 | results_str = '\t'.join( 444 | [f'{seed}', 445 | f'{model.best_acc}', 446 | f'{model.best_ari}', 447 | f'{model.best_nmi}', 448 | f'{model.best_local_feats}', 449 | f'{model.best_global_feats}', 450 | f'{topk_max_siluetter_score}', 451 | f'{topk_min_dbi_score}', 452 | ]) 453 | with open(f"results_{os.path.basename(__file__)}.txt", mode='a') as f: 454 | f.write(f"{results_str}\n") 455 | f.flush() 456 | --------------------------------------------------------------------------------