├── .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:
27 | 2. If you have a labeled dataset, please follow the colab with evaluation example:
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 | " \n",
146 | " \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+XxeFRdXR2ycQDtYbPZWC4LAADQ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"
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 | " \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 |
--------------------------------------------------------------------------------