pg-sui 1.0.2.1__py3-none-any.whl → 1.6.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pg-sui might be problematic. Click here for more details.

Files changed (112) hide show
  1. {pg_sui-1.0.2.1.dist-info → pg_sui-1.6.8.dist-info}/METADATA +51 -70
  2. pg_sui-1.6.8.dist-info/RECORD +78 -0
  3. {pg_sui-1.0.2.1.dist-info → pg_sui-1.6.8.dist-info}/WHEEL +1 -1
  4. pg_sui-1.6.8.dist-info/entry_points.txt +4 -0
  5. pg_sui-1.6.8.dist-info/top_level.txt +1 -0
  6. pgsui/__init__.py +35 -54
  7. pgsui/_version.py +34 -0
  8. pgsui/cli.py +635 -0
  9. pgsui/data_processing/config.py +576 -0
  10. pgsui/data_processing/containers.py +1782 -0
  11. pgsui/data_processing/transformers.py +121 -1103
  12. pgsui/electron/app/__main__.py +5 -0
  13. pgsui/electron/app/icons/icons/1024x1024.png +0 -0
  14. pgsui/electron/app/icons/icons/128x128.png +0 -0
  15. pgsui/electron/app/icons/icons/16x16.png +0 -0
  16. pgsui/electron/app/icons/icons/24x24.png +0 -0
  17. pgsui/electron/app/icons/icons/256x256.png +0 -0
  18. pgsui/electron/app/icons/icons/32x32.png +0 -0
  19. pgsui/electron/app/icons/icons/48x48.png +0 -0
  20. pgsui/electron/app/icons/icons/512x512.png +0 -0
  21. pgsui/electron/app/icons/icons/64x64.png +0 -0
  22. pgsui/electron/app/icons/icons/icon.icns +0 -0
  23. pgsui/electron/app/icons/icons/icon.ico +0 -0
  24. pgsui/electron/app/main.js +189 -0
  25. pgsui/electron/app/package-lock.json +6893 -0
  26. pgsui/electron/app/package.json +50 -0
  27. pgsui/electron/app/preload.js +15 -0
  28. pgsui/electron/app/server.py +146 -0
  29. pgsui/electron/app/ui/logo.png +0 -0
  30. pgsui/electron/app/ui/renderer.js +130 -0
  31. pgsui/electron/app/ui/styles.css +59 -0
  32. pgsui/electron/app/ui/ui_shim.js +72 -0
  33. pgsui/electron/bootstrap.py +43 -0
  34. pgsui/electron/launch.py +59 -0
  35. pgsui/electron/package.json +14 -0
  36. pgsui/example_data/popmaps/{test.popmap → phylogen_nomx.popmap} +185 -99
  37. pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz +0 -0
  38. pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz.tbi +0 -0
  39. pgsui/impute/deterministic/imputers/allele_freq.py +691 -0
  40. pgsui/impute/deterministic/imputers/mode.py +679 -0
  41. pgsui/impute/deterministic/imputers/nmf.py +221 -0
  42. pgsui/impute/deterministic/imputers/phylo.py +971 -0
  43. pgsui/impute/deterministic/imputers/ref_allele.py +530 -0
  44. pgsui/impute/supervised/base.py +339 -0
  45. pgsui/impute/supervised/imputers/hist_gradient_boosting.py +293 -0
  46. pgsui/impute/supervised/imputers/random_forest.py +287 -0
  47. pgsui/impute/unsupervised/base.py +924 -0
  48. pgsui/impute/unsupervised/callbacks.py +89 -263
  49. pgsui/impute/unsupervised/imputers/autoencoder.py +972 -0
  50. pgsui/impute/unsupervised/imputers/nlpca.py +1264 -0
  51. pgsui/impute/unsupervised/imputers/ubp.py +1288 -0
  52. pgsui/impute/unsupervised/imputers/vae.py +957 -0
  53. pgsui/impute/unsupervised/loss_functions.py +158 -0
  54. pgsui/impute/unsupervised/models/autoencoder_model.py +208 -558
  55. pgsui/impute/unsupervised/models/nlpca_model.py +149 -468
  56. pgsui/impute/unsupervised/models/ubp_model.py +198 -1317
  57. pgsui/impute/unsupervised/models/vae_model.py +259 -618
  58. pgsui/impute/unsupervised/nn_scorers.py +215 -0
  59. pgsui/utils/classification_viz.py +591 -0
  60. pgsui/utils/misc.py +35 -480
  61. pgsui/utils/plotting.py +514 -824
  62. pgsui/utils/scorers.py +212 -438
  63. pg_sui-1.0.2.1.dist-info/RECORD +0 -75
  64. pg_sui-1.0.2.1.dist-info/top_level.txt +0 -3
  65. pgsui/example_data/phylip_files/test_n10.phy +0 -118
  66. pgsui/example_data/phylip_files/test_n100.phy +0 -118
  67. pgsui/example_data/phylip_files/test_n2.phy +0 -118
  68. pgsui/example_data/phylip_files/test_n500.phy +0 -118
  69. pgsui/example_data/structure_files/test.nopops.1row.10sites.str +0 -117
  70. pgsui/example_data/structure_files/test.nopops.2row.100sites.str +0 -234
  71. pgsui/example_data/structure_files/test.nopops.2row.10sites.str +0 -234
  72. pgsui/example_data/structure_files/test.nopops.2row.30sites.str +0 -234
  73. pgsui/example_data/structure_files/test.nopops.2row.allsites.str +0 -234
  74. pgsui/example_data/structure_files/test.pops.1row.10sites.str +0 -117
  75. pgsui/example_data/structure_files/test.pops.2row.10sites.str +0 -234
  76. pgsui/example_data/trees/test.iqtree +0 -376
  77. pgsui/example_data/trees/test.qmat +0 -5
  78. pgsui/example_data/trees/test.rate +0 -2033
  79. pgsui/example_data/trees/test.tre +0 -1
  80. pgsui/example_data/trees/test_n10.rate +0 -19
  81. pgsui/example_data/trees/test_n100.rate +0 -109
  82. pgsui/example_data/trees/test_n500.rate +0 -509
  83. pgsui/example_data/trees/test_siterates.txt +0 -2024
  84. pgsui/example_data/trees/test_siterates_n10.txt +0 -10
  85. pgsui/example_data/trees/test_siterates_n100.txt +0 -100
  86. pgsui/example_data/trees/test_siterates_n500.txt +0 -500
  87. pgsui/example_data/vcf_files/test.vcf +0 -244
  88. pgsui/example_data/vcf_files/test.vcf.gz +0 -0
  89. pgsui/example_data/vcf_files/test.vcf.gz.tbi +0 -0
  90. pgsui/impute/estimators.py +0 -735
  91. pgsui/impute/impute.py +0 -1486
  92. pgsui/impute/simple_imputers.py +0 -1439
  93. pgsui/impute/supervised/iterative_imputer_fixedparams.py +0 -785
  94. pgsui/impute/supervised/iterative_imputer_gridsearch.py +0 -1027
  95. pgsui/impute/unsupervised/keras_classifiers.py +0 -702
  96. pgsui/impute/unsupervised/models/in_development/cnn_model.py +0 -486
  97. pgsui/impute/unsupervised/neural_network_imputers.py +0 -1424
  98. pgsui/impute/unsupervised/neural_network_methods.py +0 -1549
  99. pgsui/pg_sui.py +0 -261
  100. pgsui/utils/sequence_tools.py +0 -407
  101. simulation/sim_benchmarks.py +0 -333
  102. simulation/sim_treeparams.py +0 -475
  103. test/__init__.py +0 -0
  104. test/pg_sui_simtest.py +0 -215
  105. test/pg_sui_testing.py +0 -523
  106. test/test.py +0 -297
  107. test/test_pgsui.py +0 -374
  108. test/test_tkc.py +0 -214
  109. {pg_sui-1.0.2.1.dist-info → pg_sui-1.6.8.dist-info/licenses}/LICENSE +0 -0
  110. /pgsui/{example_data/trees → electron/app}/__init__.py +0 -0
  111. /pgsui/impute/{unsupervised/models/in_development → supervised/imputers}/__init__.py +0 -0
  112. {simulation → pgsui/impute/unsupervised/imputers}/__init__.py +0 -0
@@ -1,1340 +1,221 @@
1
- import logging
2
- import os
3
- import sys
4
- import warnings
1
+ from typing import List, Literal
5
2
 
6
- os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" # or any {'0', '1', '2', '3'}
7
- logging.getLogger("tensorflow").disabled = True
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn as nn
6
+ from snpio.utils.logging import LoggerManager
8
7
 
9
- # Import tensorflow with reduced warnings.
10
- os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
11
- logging.getLogger("tensorflow").disabled = True
12
- warnings.filterwarnings("ignore", category=UserWarning)
8
+ from pgsui.impute.unsupervised.loss_functions import MaskedFocalLoss
13
9
 
14
- # noinspection PyPackageRequirements
15
- import tensorflow as tf
16
10
 
17
- # Disable can't find cuda .dll errors. Also turns of GPU support.
18
- tf.config.set_visible_devices([], "GPU")
11
+ class UBPModel(nn.Module):
12
+ """An Unsupervised Backpropagation (UBP) model with a multi-phase decoder.
19
13
 
20
- from tensorflow.python.util import deprecation
14
+ This class implements a deep neural network that serves as the decoder component in an unsupervised imputation pipeline. It's designed to reconstruct high-dimensional genomic data from a low-dimensional latent representation. The model features a unique multi-phase architecture with two distinct decoding paths:
21
15
 
22
- # Disable warnings and info logs.
23
- tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
24
- tf.get_logger().setLevel(logging.ERROR)
16
+ 1. **Phase 1 Decoder:** A simple, shallow linear network.
17
+ 2. **Phase 2 & 3 Decoder:** A deeper, multi-layered, fully-connected network with batch normalization and dropout for regularization.
25
18
 
19
+ This phased approach allows for progressive training strategies. The model is tailored for two-channel allele data, where it learns to predict allele probabilities for each of the two channels at every SNP locus.
26
20
 
27
- # Monkey patching deprecation utils to supress warnings.
28
- # noinspection PyUnusedLocal
29
- def deprecated(
30
- date, instructions, warn_once=True
31
- ): # pylint: disable=unused-argument
32
- def deprecated_wrapper(func):
33
- return func
21
+ **Model Architecture:**
34
22
 
35
- return deprecated_wrapper
23
+ The model's forward pass maps a latent vector, $z$, to a reconstructed output, $\hat{x}$, via one of two paths.
36
24
 
25
+ - **Phase 1 Path (Shallow Decoder):**
26
+ $$
27
+ \hat{x}_{p1} = W_{p1} z + b_{p1}
28
+ $$
37
29
 
38
- deprecation.deprecated = deprecated
30
+ - **Phase 2/3 Path (Deep Decoder):**
31
+ This path uses a series of hidden layers with non-linear activations, $f(\cdot)$:
32
+ $$
33
+ h_1 = f(W_1 z + b_1)
34
+ $$
35
+ $$
36
+ \dots
37
+ $$
38
+ $$
39
+ h_L = f(W_L h_{L-1} + b_L)
40
+ $$
41
+ $$
42
+ \hat{x}_{p23} = W_{L+1} h_L + b_{L+1}
43
+ $$
39
44
 
40
- from tensorflow.keras.layers import (
41
- Dropout,
42
- Dense,
43
- Reshape,
44
- LeakyReLU,
45
- PReLU,
46
- Activation,
47
- )
48
-
49
- from tensorflow.keras.regularizers import l1_l2
50
-
51
- # Custom Modules
52
- try:
53
- from ..neural_network_methods import NeuralNetworkMethods
54
- except (ModuleNotFoundError, ValueError, ImportError):
55
- from impute.unsupervised.neural_network_methods import NeuralNetworkMethods
56
-
57
-
58
- class UBPPhase1(tf.keras.Model):
59
- """UBP Phase 1 single layer perceptron model to train predict imputations.
60
-
61
- This model is subclassed from the tensorflow/ Keras framework.
62
-
63
- UBPPhase1 subclasses the tf.keras.Model and overrides the train_step function, which does training and evalutation for each batch in each epoch.
64
-
65
- UBPPhase1 is a single-layer perceptron model used to initially refine V. After Phase 1 the Phase 1 weights are discarded.
66
-
67
- Args:
68
- V (numpy.ndarray(float)): V should have been randomly initialized and will be used as the input data that gets refined during training. Defaults to None.
69
-
70
- y (numpy.ndarray): Target values to predict. Actual input data. Defaults to None.
71
-
72
- batch_size (int, optional): Batch size per epoch. Defaults to 32.
73
-
74
- missing_mask (numpy.ndarray): Missing data mask for y. Defaults to None.
75
-
76
- output_shape (int): Output units for n_features dimension. Output will be of shape (batch_size, n_features). Defaults to None.
77
-
78
- n_components (int, optional): Number of features in input V to use. Defaults to 3.
79
-
80
- weights_initializer (str, optional): Kernel initializer to use for initializing model weights. Defaults to "glorot_normal".
81
-
82
- hidden_layer_sizes (NoneType, optional): Output units for each hidden layer. List should be of same length as the number of hidden layers. Not used for UBP Phase 1, but is here for compatibility. Defaults to "midpoint".
83
-
84
- num_hidden_layers (int, optional): Number of hidden layers to use. Not used in UBP Phase 1, but is here for compatibility. Defaults to 1.
85
-
86
- hidden_activation (str, optional): Activation function to use for hidden layers. Defaults to "elu".
87
-
88
- l1_penalty (float, optional): L1 regularization penalty to use to reduce overfitting. Defaults to 0.01.
89
-
90
- l2_penalty (float, optional): L2 regularization penalty to use to reduce overfitting. Defaults to 0.01.
91
-
92
- dropout_rate (float, optional): Dropout rate during training to reduce overfitting. Must be a float between 0 and 1. Defaults to 0.2.
93
-
94
- num_classes (int, optional): Number of classes in output. Corresponds to the 3rd dimension of the output shape (batch_size, n_features, num_classes). Defaults to 3.
95
-
96
- phase (int, optional): Current phase if doing UBP model. Defaults to 1.
97
-
98
- sample_weight (numpy.ndarray, optional): 2D sample weights of shape (n_samples, n_features). Should have values for each class weighted. Defaults to None.
99
-
100
- Example:
101
- >>>model = UBPPhase1(V=V, y=y, batch_size=32, missing_mask=missing_mask, output_shape=y_train.shape[1], n_components=3, weights_initializer="glorot_normal", hidden_layer_sizes="midpoint", num_hidden_layers=1, hidden_activation="elu", l1_penalty=1e-6, l2_penalty=1e-6, num_classes=3, phase=3)
102
- >>>model.compile(optimizer=optimizer, loss=loss_func, metrics=[my_metrics], run_eagerly=True)
103
- >>>history = model.fit(X, y, batch_size=batch_size, epochs=epochs, callbacks=[MyCallback()], validation_split=validation_split, shuffle=False)
104
-
105
- Raises:
106
- TypeError: V, y, missing_mask, output_shape must not be NoneType.
107
- ValueError: Maximum of 5 hidden layers.
108
- """
109
-
110
- def __init__(
111
- self,
112
- V=None,
113
- y=None,
114
- batch_size=32,
115
- missing_mask=None,
116
- output_shape=None,
117
- n_components=3,
118
- weights_initializer="glorot_normal",
119
- hidden_layer_sizes="midpoint",
120
- num_hidden_layers=1,
121
- l1_penalty=0.01,
122
- l2_penalty=0.01,
123
- dropout_rate=0.2,
124
- num_classes=3,
125
- phase=1,
126
- sample_weight=None,
127
- ):
128
- super(UBPPhase1, self).__init__()
129
-
130
- self.total_loss_tracker = tf.keras.metrics.Mean(name="total_loss")
131
- self.binary_accuracy_tracker = tf.keras.metrics.Mean(
132
- name="binary_accuracy"
133
- )
134
-
135
- nn = NeuralNetworkMethods()
136
- self.nn = nn
137
-
138
- if V is None:
139
- self._V = nn.init_weights(y.shape[0], n_components)
140
- elif isinstance(V, dict):
141
- self._V = V[n_components]
142
- else:
143
- self._V = V
144
-
145
- self._y = y
146
-
147
- hidden_layer_sizes = nn.validate_hidden_layers(
148
- hidden_layer_sizes, num_hidden_layers
149
- )
150
-
151
- hidden_layer_sizes = nn.get_hidden_layer_sizes(
152
- y.shape[1], self._V.shape[1], hidden_layer_sizes
153
- )
154
-
155
- nn.validate_model_inputs(y, missing_mask, output_shape)
156
-
157
- self._missing_mask = missing_mask
158
- self.weights_initializer = weights_initializer
159
- self.phase = phase
160
- self.dropout_rate = dropout_rate
161
- self._sample_weight = sample_weight
162
-
163
- ### NOTE: I tried using just _V as the input to be refined, but it
164
- # wasn't getting updated. So I copy it here and it works.
165
- # V_latent is refined during train_step.
166
- self.V_latent_ = self._V.copy()
167
-
168
- # Initialize parameters used during train_step() and test_step.
169
- # input_with_mask_ is set during the UBPCallbacks() execution.
170
- self._batch_idx = 0
171
- self._batch_size = batch_size
172
- self.n_components = n_components
173
-
174
- if l1_penalty == 0.0 and l2_penalty == 0.0:
175
- kernel_regularizer = None
176
- else:
177
- kernel_regularizer = l1_l2(l1_penalty, l2_penalty)
178
-
179
- self.kernel_regularizer = kernel_regularizer
180
- kernel_initializer = weights_initializer
181
-
182
- # Construct single-layer perceptron.
183
-
184
- self.dense1 = Dense(
185
- output_shape * num_classes,
186
- input_shape=(n_components,),
187
- kernel_initializer=kernel_initializer,
188
- kernel_regularizer=kernel_regularizer,
189
- )
190
-
191
- self.rshp = Reshape((output_shape, num_classes))
192
-
193
- def call(self, inputs):
194
- x = self.dense1(inputs)
195
- return self.rshp(x)
196
-
197
- def model(self):
198
- x = tf.keras.Input(shape=(self.n_components,))
199
- return tf.keras.Model(inputs=[x], outputs=self.call(x))
200
-
201
- def set_model_outputs(self):
202
- x = tf.keras.Input(shape=(self.n_components,))
203
- model = tf.keras.Model(inputs=[x], outputs=self.call(x))
204
- self.outputs = model.outputs
205
-
206
- @property
207
- def metrics(self):
208
- return [
209
- self.total_loss_tracker,
210
- self.binary_accuracy_tracker,
211
- ]
212
-
213
- def train_step(self, data):
214
- """Train step function. Parameters are set in the UBPCallbacks callback"""
215
- y = self._y
216
-
217
- (
218
- v,
219
- y_true,
220
- sample_weight,
221
- missing_mask,
222
- batch_start,
223
- batch_end,
224
- ) = self.nn.prepare_training_batches(
225
- self.V_latent_,
226
- y,
227
- self._batch_size,
228
- self._batch_idx,
229
- True,
230
- self.n_components,
231
- self._sample_weight,
232
- self._missing_mask,
233
- )
234
-
235
- src = [v]
236
-
237
- if sample_weight is not None:
238
- sample_weight_masked = tf.convert_to_tensor(
239
- sample_weight[~missing_mask], dtype=tf.float32
240
- )
241
- else:
242
- sample_weight_masked = None
243
-
244
- y_true_masked = tf.boolean_mask(
245
- tf.convert_to_tensor(y_true, dtype=tf.float32),
246
- tf.reduce_any(tf.not_equal(y_true, -1), axis=2),
247
- )
248
-
249
- # NOTE: Earlier model architectures incorrectly
250
- # applied one gradient to all the variables, including
251
- # the weights and v. Here we apply them separately, per
252
- # the UBP manuscript.
253
- with tf.GradientTape(persistent=True) as tape:
254
- # Forward pass. Watch input tensor v.
255
- tape.watch(v)
256
- y_pred = self(v, training=True)
257
- y_pred_masked = tf.boolean_mask(
258
- y_pred, tf.reduce_any(tf.not_equal(y_true, -1), axis=2)
259
- )
260
- ### NOTE: If you get the error, "'tuple' object has no attribute
261
- ### 'rank'", then convert y_true to a tensor object."
262
- loss = self.compiled_loss(
263
- y_true_masked,
264
- y_pred_masked,
265
- sample_weight=sample_weight_masked,
266
- )
267
-
268
- # Refine the watched variables with
269
- # gradient descent backpropagation
270
- gradients = tape.gradient(loss, self.trainable_variables)
271
- self.optimizer.apply_gradients(
272
- zip(gradients, self.trainable_variables)
273
- )
274
-
275
- # Apply separate gradients to v.
276
- vgrad = tape.gradient(loss, src)
277
- self.optimizer.apply_gradients(zip(vgrad, src))
278
-
279
- del tape
280
-
281
- # NOTE: run_eagerly must be set to True in the compile() method for this
282
- # to work. Otherwise it can't convert a Tensor object to a numpy array.
283
- # There is really no other way to set v back to V_latent_ in graph
284
- # mode as far as I know. eager execution is slower, so it would be nice
285
- # to find a way to do this without converting to numpy.
286
- self.V_latent_[batch_start:batch_end, :] = v.numpy()
287
-
288
- ### NOTE: If you get the error, "'tuple' object has no attribute
289
- ### 'rank', then convert y_true to a tensor object."
290
- # history object that gets returned from model.fit().
291
- self.total_loss_tracker.update_state(loss)
292
- self.binary_accuracy_tracker.update_state(
293
- tf.keras.metrics.binary_accuracy(y_true_masked, y_pred_masked)
294
- )
295
-
296
- return {
297
- "loss": self.total_loss_tracker.result(),
298
- "binary_accuracy": self.binary_accuracy_tracker.result(),
299
- }
300
-
301
- def test_step(self, data):
302
- """Train step function. Parameters are set in the UBPCallbacks callback"""
303
- y = self._y
304
-
305
- (
306
- v,
307
- y_true,
308
- sample_weight,
309
- missing_mask,
310
- batch_start,
311
- batch_end,
312
- ) = self.nn.prepare_training_batches(
313
- self.V_latent_,
314
- y,
315
- self._batch_size,
316
- self._batch_idx,
317
- True,
318
- self.n_components,
319
- self._sample_weight,
320
- self._missing_mask,
321
- )
322
-
323
- if sample_weight is not None:
324
- sample_weight_masked = tf.convert_to_tensor(
325
- sample_weight[~missing_mask], dtype=tf.float32
326
- )
327
- else:
328
- sample_weight_masked = None
329
-
330
- y_true_masked = tf.boolean_mask(
331
- tf.convert_to_tensor(y_true, dtype=tf.float32),
332
- tf.reduce_any(tf.not_equal(y_true, -1), axis=2),
333
- )
334
-
335
- y_pred = self(v, training=False)
336
- y_pred_masked = tf.boolean_mask(
337
- y_pred, tf.reduce_any(tf.not_equal(y_true, -1), axis=2)
338
- )
339
- ### NOTE: If you get the error, "'tuple' object has no attribute
340
- ### 'rank'", then convert y_true to a tensor object."
341
- loss = self.compiled_loss(
342
- y_true_masked,
343
- y_pred_masked,
344
- sample_weight=sample_weight_masked,
345
- )
346
-
347
- ### NOTE: If you get the error, "'tuple' object has no attribute
348
- ### 'rank', then convert y_true to a tensor object."
349
- self.total_loss_tracker.update_state(loss)
350
- self.binary_accuracy_tracker.update_state(
351
- tf.keras.metrics.binary_accuracy(y_true_masked, y_pred_masked)
352
- )
353
-
354
- return {
355
- "loss": self.total_loss_tracker.result(),
356
- "binary_accuracy": self.binary_accuracy_tracker.result(),
357
- }
358
-
359
- @property
360
- def V_latent(self):
361
- """Randomly initialized input that gets refined during training.
362
- :noindex:
363
- """
364
- return self.V_latent_
365
-
366
- @property
367
- def batch_size(self):
368
- """Batch (=step) size per epoch.
369
- :noindex:
370
- """
371
- return self._batch_size
372
-
373
- @property
374
- def batch_idx(self):
375
- """Current batch (=step) index.
376
- :noindex:
377
- """
378
- return self._batch_idx
379
-
380
- @property
381
- def y(self):
382
- """Input dataset.
383
- :noindex:
384
- """
385
- return self._y
386
-
387
- @property
388
- def missing_mask(self):
389
- """Missing mask of shape (y.shape[0], y.shape[1])
390
- :noindex:
391
- """
392
- return self._missing_mask
393
-
394
- @property
395
- def sample_weight(self):
396
- """Sample weights of shape (y.shape[0], y.shape[1])
397
- :noindex:
398
- """
399
- return self._sample_weight
400
-
401
- @V_latent.setter
402
- def V_latent(self, value):
403
- """Set randomly initialized input. Gets refined during training.
404
- :noindex:
405
- """
406
- self.V_latent_ = value
407
-
408
- @batch_size.setter
409
- def batch_size(self, value):
410
- """Set batch_size parameter.
411
- :noindex:
412
- """
413
- self._batch_size = int(value)
414
-
415
- @batch_idx.setter
416
- def batch_idx(self, value):
417
- """Set current batch (=step) index.
418
- :noindex:
419
- """
420
- self._batch_idx = int(value)
421
-
422
- @y.setter
423
- def y(self, value):
424
- """Set y after each epoch.
425
- :noindex:
426
- """
427
- self._y = value
428
-
429
- @missing_mask.setter
430
- def missing_mask(self, value):
431
- """Set missing_mask after each epoch.
432
- :noindex:
433
- """
434
- self._missing_mask = value
435
-
436
- @sample_weight.setter
437
- def sample_weight(self, value):
438
- """Set sample_weight after each epoch.
439
- :noindex:
440
- """
441
- self._sample_weight = value
442
-
443
-
444
- class UBPPhase2(tf.keras.Model):
445
- """UBP Phase 2 model to train and use to predict imputations.
446
-
447
- UBPPhase2 subclasses the tf.keras.Model and overrides the train_step function, which does training for each batch in each epoch.
448
-
449
- Phase 2 does not refine V, it just refines the weights.
450
-
451
- Args:
452
- V (numpy.ndarray(float)): V should have been randomly initialized and will be used as the input data that gets refined during training. Defaults to None.
453
-
454
- y (numpy.ndarray): Target values to predict. Actual input data. Defaults to None.
455
-
456
- batch_size (int, optional): Batch size per epoch. Defaults to 32.
457
-
458
- missing_mask (numpy.ndarray): Missing data mask for y. Defaults to None.
459
-
460
- output_shape (int): Output units for n_features dimension. Output will be of shape (batch_size, n_features). Defaults to None.
461
-
462
- n_components (int, optional): Number of features in input V to use. Defaults to 3.
463
-
464
- weights_initializer (str, optional): Kernel initializer to use for initializing model weights. Defaults to "glorot_normal".
465
-
466
- hidden_layer_sizes (NoneType, optional): Output units for each hidden layer. List should be of same length as the number of hidden layers. Not used for UBP Phase 1, but is here for compatibility. Defaults to "midpoint".
467
-
468
- num_hidden_layers (int, optional): Number of hidden layers to use. Not used in UBP Phase 1, but is here for compatibility. Defaults to 1.
469
-
470
- hidden_activation (str, optional): Activation function to use for hidden layers. Defaults to "elu".
471
-
472
- l1_penalty (float, optional): L1 regularization penalty to use to reduce overfitting. Defaults to 0.01.
473
-
474
- l2_penalty (float, optional): L2 regularization penalty to use to reduce overfitting. Defaults to 0.01.
475
-
476
- dropout_rate (float, optional): Dropout rate during training to reduce overfitting. Must be a float between 0 and 1. Defaults to 0.2.
477
-
478
- num_classes (int, optional): Number of classes in output. Corresponds to the 3rd dimension of the output shape (batch_size, n_features, num_classes). Defaults to 3.
479
-
480
- phase (int, optional): Current phase if doing UBP model. Defaults to 1.
481
-
482
- sample_weight (numpy.ndarray, optional): 2D sample weights of shape (n_samples, n_features). Should have values for each class weighted. Defaults to None.
483
-
484
- Example:
485
- >>>model = UBPPhase2(V=V, y=y, batch_size=32, missing_mask=missing_mask, output_shape=y_train.shape[1], n_components=3, weights_initializer="glorot_normal", hidden_layer_sizes="midpoint", num_hidden_layers=1, hidden_activation="elu", l1_penalty=1e-6, l2_penalty=1e-6, num_classes=3, phase=3)
486
- >>>
487
- >>>model.compile(optimizer=optimizer, loss=loss_func, metrics=[my_metrics], run_eagerly=True)
488
- >>>
489
- >>>history = model.fit(X, y, batch_size=batch_size, epochs=epochs, callbacks=[MyCallback()], validation_split=validation_split, shuffle=False)
490
-
491
- Raises:
492
- TypeError: V, y, missing_mask, output_shape must not be NoneType.
493
- ValueError: Maximum of 5 hidden layers.
45
+ The final output from either path is reshaped into a tensor of shape `(batch_size, n_features, n_channels, n_classes)`. The model is optimized using a `MaskedFocalLoss` function, which effectively handles the missing data and class imbalance common in genomic datasets.
494
46
  """
495
47
 
496
48
  def __init__(
497
49
  self,
498
- V=None,
499
- y=None,
500
- batch_size=32,
501
- missing_mask=None,
502
- output_shape=None,
503
- n_components=3,
504
- weights_initializer="glorot_normal",
505
- hidden_layer_sizes="midpoint",
506
- num_hidden_layers=1,
507
- hidden_activation="elu",
508
- l1_penalty=0.01,
509
- l2_penalty=0.01,
510
- dropout_rate=0.2,
511
- num_classes=3,
512
- phase=2,
513
- sample_weight=None,
50
+ n_features: int,
51
+ prefix: str,
52
+ *,
53
+ num_classes: int = 3,
54
+ hidden_layer_sizes: List[int] | np.ndarray = [128, 64],
55
+ latent_dim: int = 2,
56
+ dropout_rate: float = 0.2,
57
+ activation: Literal["relu", "elu", "selu", "leaky_relu"] = "relu",
58
+ gamma: float = 2.0,
59
+ device: Literal["cpu", "gpu", "mps"] = "cpu",
60
+ verbose: bool = False,
61
+ debug: bool = False,
514
62
  ):
515
- super(UBPPhase2, self).__init__()
516
-
517
- self.total_loss_tracker = tf.keras.metrics.Mean(name="total_loss")
518
- self.binary_accuracy_tracker = tf.keras.metrics.Mean(
519
- name="binary_accuracy"
520
- )
521
-
522
- nn = NeuralNetworkMethods()
523
- self.nn = nn
524
-
525
- if V is None:
526
- self._V = nn.init_weights(y.shape[0], n_components)
527
- elif isinstance(V, dict):
528
- self._V = V[n_components]
529
- else:
530
- self._V = V
531
-
532
- self._y = y
533
-
534
- hidden_layer_sizes = nn.validate_hidden_layers(
535
- hidden_layer_sizes, num_hidden_layers
536
- )
537
-
538
- hidden_layer_sizes = nn.get_hidden_layer_sizes(
539
- y.shape[1], self._V.shape[1], hidden_layer_sizes
540
- )
541
-
542
- nn.validate_model_inputs(y, missing_mask, output_shape)
543
-
544
- self._missing_mask = missing_mask
545
- self.weights_initializer = weights_initializer
546
- self.phase = phase
547
- self.dropout_rate = dropout_rate
548
- self._sample_weight = sample_weight
549
-
550
- ### NOTE: I tried using just _V as the input to be refined, but it
551
- # wasn't getting updated. So I copy it here and it works.
552
- # V_latent is refined during train_step.
553
- self.V_latent_ = self._V.copy()
554
-
555
- # Initialize parameters used during train_step.
556
- self._batch_idx = 0
557
- self._batch_size = batch_size
558
- self.n_components = n_components
559
-
560
- if l1_penalty == 0.0 and l2_penalty == 0.0:
561
- kernel_regularizer = None
562
- else:
563
- kernel_regularizer = l1_l2(l1_penalty, l2_penalty)
564
-
565
- self.kernel_regularizer = kernel_regularizer
566
- kernel_initializer = weights_initializer
567
-
568
- if hidden_activation.lower() == "leaky_relu":
569
- activation = LeakyReLU(alpha=0.01)
570
-
571
- elif hidden_activation.lower() == "prelu":
572
- activation = PReLU()
573
-
574
- elif hidden_activation.lower() == "selu":
575
- activation = "selu"
576
- kernel_initializer = "lecun_normal"
577
-
578
- else:
579
- activation = hidden_activation
580
-
581
- if num_hidden_layers > 5:
582
- raise ValueError(
583
- f"The maximum number of hidden layers is 5, but got "
584
- f"{num_hidden_layers}"
585
- )
586
-
587
- self.dense2 = None
588
- self.dense3 = None
589
- self.dense4 = None
590
- self.dense5 = None
591
-
592
- # Construct multi-layer perceptron.
593
- # Add hidden layers dynamically.
594
- self.dense1 = Dense(
595
- hidden_layer_sizes[0],
596
- input_shape=(n_components,),
597
- activation=activation,
598
- kernel_initializer=kernel_initializer,
599
- kernel_regularizer=kernel_regularizer,
600
- )
601
-
602
- if num_hidden_layers >= 2:
603
- self.dense2 = Dense(
604
- hidden_layer_sizes[1],
605
- activation=activation,
606
- kernel_initializer=kernel_initializer,
607
- kernel_regularizer=kernel_regularizer,
608
- )
609
-
610
- if num_hidden_layers >= 3:
611
- self.dense3 = Dense(
612
- hidden_layer_sizes[2],
613
- activation=activation,
614
- kernel_initializer=kernel_initializer,
615
- kernel_regularizer=kernel_regularizer,
616
- )
617
-
618
- if num_hidden_layers >= 4:
619
- self.dense4 = Dense(
620
- hidden_layer_sizes[3],
621
- activation=activation,
622
- kernel_initializer=kernel_initializer,
623
- kernel_regularizer=kernel_regularizer,
624
- )
625
-
626
- if num_hidden_layers == 5:
627
- self.dense5 = Dense(
628
- hidden_layer_sizes[4],
629
- activation=activation,
630
- kernel_initializer=kernel_initializer,
631
- kernel_regularizer=kernel_regularizer,
632
- )
633
-
634
- self.output1 = Dense(
635
- output_shape * num_classes,
636
- kernel_initializer=kernel_initializer,
637
- kernel_regularizer=kernel_regularizer,
638
- )
639
-
640
- self.rshp = Reshape((output_shape, num_classes))
641
-
642
- self.dropout_layer = Dropout(rate=dropout_rate)
643
-
644
- def call(self, inputs, training=None):
645
- x = self.dense1(inputs)
646
- x = self.dropout_layer(x, training=training)
647
- if self.dense2 is not None:
648
- x = self.dense2(x)
649
- x = self.dropout_layer(x, training=training)
650
- if self.dense3 is not None:
651
- x = self.dense3(x)
652
- x = self.dropout_layer(x, training=training)
653
- if self.dense4 is not None:
654
- x = self.dense4(x)
655
- x = self.dropout_layer(x, training=training)
656
- if self.dense5 is not None:
657
- x = self.dense5(x)
658
- x = self.dropout_layer(x, training=training)
63
+ """Initializes the UBPModel.
659
64
 
660
- x = self.output1(x)
661
- return self.rshp(x)
65
+ Args:
66
+ n_features (int): The number of features (SNPs) in the input data.
67
+ prefix (str): A prefix used for logging.
68
+ num_classes (int): The number of possible allele classes (e.g., 3 for 0, 1, 2). Defaults to 3.
69
+ hidden_layer_sizes (list[int] | np.ndarray): A list of integers specifying the size of each hidden layer in the deep (Phase 2/3) decoder. Defaults to [128, 64].
70
+ latent_dim (int): The dimensionality of the input latent space. Defaults to 2.
71
+ dropout_rate (float): The dropout rate for regularization in the deep decoder. Defaults to 0.2.
72
+ activation (str): The non-linear activation function to use in the deep decoder's hidden layers. Defaults to 'relu'.
73
+ gamma (float): The focusing parameter for the focal loss function. Defaults to 2.0.
74
+ device (Literal["cpu", "gpu", "mps"]): The PyTorch device to run the model on. Defaults to 'cpu'.
75
+ verbose (bool): If True, enables detailed logging. Defaults to False.
76
+ debug (bool): If True, enables debug mode. Defaults to False.
77
+ """
78
+ super(UBPModel, self).__init__()
79
+
80
+ logman = LoggerManager(
81
+ name=__name__, prefix=prefix, verbose=verbose, debug=debug
82
+ )
83
+ self.logger = logman.get_logger()
84
+
85
+ self.n_features = n_features
86
+ self.num_classes = num_classes
87
+ self.latent_dim = latent_dim
88
+ self.gamma = gamma
89
+ self.device = device
90
+
91
+ if isinstance(hidden_layer_sizes, np.ndarray):
92
+ hidden_layer_sizes = hidden_layer_sizes.tolist()
93
+
94
+ # Final layer output size is now n_features * num_classes
95
+ final_output_size = n_features * num_classes
96
+
97
+ # Phase 1 decoder: Simple linear model
98
+ self.phase1_decoder = nn.Sequential(
99
+ nn.Linear(latent_dim, final_output_size, device=device),
100
+ )
101
+
102
+ # Phase 2 & 3 uses the Convolutional Decoder
103
+ act_factory = self._resolve_activation_factory(activation)
104
+
105
+ if hidden_layer_sizes[0] > hidden_layer_sizes[-1]:
106
+ hidden_layer_sizes = list(reversed(hidden_layer_sizes))
107
+
108
+ # Phase 2 & 3: Flexible deeper network
109
+ layers = []
110
+ input_dim = latent_dim
111
+ for size in hidden_layer_sizes:
112
+ layers.append(nn.Linear(input_dim, size))
113
+ layers.append(nn.BatchNorm1d(size))
114
+ layers.append(nn.Dropout(dropout_rate))
115
+ layers.append(act_factory())
116
+ input_dim = size
117
+
118
+ layers.append(nn.Linear(hidden_layer_sizes[-1], final_output_size))
119
+
120
+ self.phase23_decoder = nn.Sequential(*layers)
121
+ self.reshape = (self.n_features, self.num_classes)
122
+
123
+ def _resolve_activation_factory(
124
+ self, activation: Literal["relu", "elu", "selu", "leaky_relu"]
125
+ ) -> callable:
126
+ """Resolves an activation function factory from a string name.
127
+
128
+ This method acts as a factory, returning a callable (lambda function) that produces the desired PyTorch activation function module when called.
129
+
130
+ Args:
131
+ activation (Literal["relu", "elu", "selu", "leaky_relu"]): The name of the activation function.
132
+
133
+ Returns:
134
+ callable: A factory function that, when called, returns an instance of the specified activation layer.
135
+
136
+ Raises:
137
+ ValueError: If the provided activation name is not supported.
138
+ """
139
+ a = activation.lower()
140
+ if a == "relu":
141
+ return lambda: nn.ReLU()
142
+ if a == "elu":
143
+ return lambda: nn.ELU()
144
+ if a == "leaky_relu":
145
+ return lambda: nn.LeakyReLU()
146
+ if a == "selu":
147
+ return lambda: nn.SELU()
148
+
149
+ msg = f"Activation function {activation} not supported."
150
+ self.logger.error(msg)
151
+ raise ValueError(msg)
152
+
153
+ def forward(self, x: torch.Tensor, phase: int = 1) -> torch.Tensor:
154
+ """Performs the forward pass through the UBP model.
155
+
156
+ This method routes the input tensor through the appropriate decoder based on the specified training `phase`. The final output is reshaped to match the target data structure of `(batch_size, n_features, n_channels, n_classes)`.
157
+
158
+ Args:
159
+ x (torch.Tensor): The input latent tensor of shape `(batch_size, latent_dim)`.
160
+ phase (int): The training phase (1, 2, or 3), which determines which decoder path to use.
161
+
162
+ Returns:
163
+ torch.Tensor: The reconstructed output tensor.
662
164
 
663
- def model(self):
664
- x = tf.keras.Input(shape=(self.n_components,))
665
- return tf.keras.Model(inputs=[x], outputs=self.call(x))
666
-
667
- def set_model_outputs(self):
668
- x = tf.keras.Input(shape=(self.n_components,))
669
- model = tf.keras.Model(inputs=[x], outputs=self.call(x))
670
- self.outputs = model.outputs
671
-
672
- @property
673
- def metrics(self):
674
- return [
675
- self.total_loss_tracker,
676
- self.binary_accuracy_tracker,
677
- ]
678
-
679
- def train_step(self, data):
680
- """Train step function. Parameters are set in the UBPCallbacks callback"""
681
- y = self._y
682
-
683
- (
684
- v,
685
- y_true,
686
- sample_weight,
687
- missing_mask,
688
- _,
689
- __,
690
- ) = self.nn.prepare_training_batches(
691
- self.V_latent_,
692
- y,
693
- self._batch_size,
694
- self._batch_idx,
695
- True,
696
- self.n_components,
697
- self._sample_weight,
698
- self._missing_mask,
699
- )
700
-
701
- if sample_weight is not None:
702
- sample_weight_masked = tf.convert_to_tensor(
703
- sample_weight[~missing_mask], dtype=tf.float32
704
- )
165
+ Raises:
166
+ ValueError: If an invalid phase is provided.
167
+ """
168
+ if phase == 1:
169
+ # Linear decoder for phase 1
170
+ x = self.phase1_decoder(x)
171
+ return x.view(-1, *self.reshape)
172
+ elif phase in {2, 3}:
173
+ x = self.phase23_decoder(x)
174
+ return x.view(-1, *self.reshape)
705
175
  else:
706
- sample_weight_masked = None
707
-
708
- y_true_masked = tf.boolean_mask(
709
- tf.convert_to_tensor(y_true, dtype=tf.float32),
710
- tf.reduce_any(tf.not_equal(y_true, -1), axis=2),
711
- )
712
-
713
- # NOTE: Earlier model architectures incorrectly
714
- # applied one gradient to all the variables, including
715
- # the weights and v. Here we apply them separately, per
716
- # the UBP manuscript.
717
- with tf.GradientTape() as tape:
718
- # Forward pass
719
- y_pred = self(v, training=True)
720
- y_pred_masked = tf.boolean_mask(
721
- y_pred, tf.reduce_any(tf.not_equal(y_true, -1), axis=2)
722
- )
723
- ### NOTE: If you get the error, "'tuple' object has no attribute
724
- ### 'rank'", then convert y_true to a tensor object."
725
- loss = self.compiled_loss(
726
- y_true_masked,
727
- y_pred_masked,
728
- sample_weight=sample_weight_masked,
729
- )
730
-
731
- # Refine the watched variables with backpropagation
732
- gradients = tape.gradient(loss, self.trainable_variables)
733
- self.optimizer.apply_gradients(
734
- zip(gradients, self.trainable_variables)
735
- )
736
-
737
- ### NOTE: If you get the error, "'tuple' object has no attribute
738
- ### 'rank', then convert y_true to a tensor object."
739
- self.total_loss_tracker.update_state(loss)
740
- self.binary_accuracy_tracker.update_state(
741
- tf.keras.metrics.binary_accuracy(y_true_masked, y_pred_masked)
742
- )
743
-
744
- return {
745
- "loss": self.total_loss_tracker.result(),
746
- "binary_accuracy": self.binary_accuracy_tracker.result(),
747
- }
748
-
749
- def test_step(self, data):
750
- """Train step function. Parameters are set in the UBPCallbacks callback"""
751
- y = self._y
752
-
753
- (
754
- v,
755
- y_true,
756
- sample_weight,
757
- missing_mask,
758
- _,
759
- __,
760
- ) = self.nn.prepare_training_batches(
761
- self.V_latent_,
762
- y,
763
- self._batch_size,
764
- self._batch_idx,
765
- True,
766
- self.n_components,
767
- self._sample_weight,
768
- self._missing_mask,
769
- )
770
-
771
- if sample_weight is not None:
772
- sample_weight_masked = tf.convert_to_tensor(
773
- sample_weight[~missing_mask], dtype=tf.float32
774
- )
775
- else:
776
- sample_weight_masked = None
777
-
778
- y_true_masked = tf.boolean_mask(
779
- tf.convert_to_tensor(y_true, dtype=tf.float32),
780
- tf.reduce_any(tf.not_equal(y_true, -1), axis=2),
781
- )
176
+ msg = f"Invalid phase: {phase}. Expected 1, 2, or 3."
177
+ self.logger.error(msg)
178
+ raise ValueError(msg)
782
179
 
783
- y_pred = self(v, training=False)
784
- y_pred_masked = tf.boolean_mask(
785
- y_pred, tf.reduce_any(tf.not_equal(y_true, -1), axis=2)
786
- )
787
- ### NOTE: If you get the error, "'tuple' object has no attribute
788
- ### 'rank'", then convert y_true to a tensor object."
789
- loss = self.compiled_loss(
790
- y_true_masked,
791
- y_pred_masked,
792
- sample_weight=sample_weight_masked,
793
- )
794
-
795
- ### NOTE: If you get the error, "'tuple' object has no attribute
796
- ### 'rank', then convert y_true to a tensor object."
797
- self.total_loss_tracker.update_state(loss)
798
- self.binary_accuracy_tracker.update_state(
799
- tf.keras.metrics.binary_accuracy(y_true_masked, y_pred_masked)
800
- )
801
-
802
- return {
803
- "loss": self.total_loss_tracker.result(),
804
- "binary_accuracy": self.binary_accuracy_tracker.result(),
805
- }
806
-
807
- @property
808
- def V_latent(self):
809
- """Randomly initialized input variable that gets refined during training.
810
- :noindex:
811
- """
812
- return self.V_latent_
813
-
814
- @property
815
- def batch_size(self):
816
- """Batch (=step) size per epoch.
817
- :noindex:
818
- """
819
- return self._batch_size
820
-
821
- @property
822
- def batch_idx(self):
823
- """Current batch (=step) index.
824
- :noindex:
825
- """
826
- return self._batch_idx
827
-
828
- @property
829
- def y(self):
830
- """Full input dataset.
831
- :noindex:
832
- """
833
- return self._y
834
-
835
- @property
836
- def missing_mask(self):
837
- """Get missing_mask for current epoch.
838
- :noindex:
839
- """
840
- return self._missing_mask
841
-
842
- @property
843
- def sample_weight(self):
844
- """Get sample_weight for current epoch.
845
- :noindex:
846
- """
847
- return self._sample_weight
848
-
849
- @V_latent.setter
850
- def V_latent(self, value):
851
- """Set randomly initialized input variable. Gets refined during training.
852
- :noindex:
853
- """
854
- self.V_latent_ = value
855
-
856
- @batch_size.setter
857
- def batch_size(self, value):
858
- """Set batch_size parameter.
859
- :noindex:
860
- """
861
- self._batch_size = int(value)
862
-
863
- @batch_idx.setter
864
- def batch_idx(self, value):
865
- """Set current batch (=step) index.
866
- :noindex:
867
- """
868
- self._batch_idx = int(value)
869
-
870
- @y.setter
871
- def y(self, value):
872
- """Set y after each epoch.
873
- :noindex:
874
- """
875
- self._y = value
876
-
877
- @missing_mask.setter
878
- def missing_mask(self, value):
879
- """Set missing_mask after each epoch.
880
- :noindex:
881
- """
882
- self._missing_mask = value
883
-
884
- @sample_weight.setter
885
- def sample_weight(self, value):
886
- """Set sample_weight after each epoch.
887
- :noindex:
888
- """
889
- self._sample_weight = value
890
-
891
-
892
- class UBPPhase3(tf.keras.Model):
893
- """UBP Phase 3 model to train and use to predict imputations.
894
-
895
- UBPPhase3 subclasses the tf.keras.Model and overrides the train_step function, which does training and evaluation for each batch in each single epoch.
896
-
897
- Phase 3 Refines both the weights and V.
898
-
899
- Args:
900
- V (numpy.ndarray(float)): V should have been randomly initialized and will be used as the input data that gets refined during training. Defaults to None.
901
-
902
- y (numpy.ndarray): Target values to predict. Actual input data. Defaults to None.
903
-
904
- batch_size (int, optional): Batch size per epoch. Defaults to 32.
905
-
906
- missing_mask (numpy.ndarray): Missing data mask for y. Defaults to None.
907
-
908
- output_shape (int): Output units for n_features dimension. Output will be of shape (batch_size, n_features). Defaults to None.
909
-
910
- n_components (int, optional): Number of features in input V to use. Defaults to 3.
911
-
912
- weights_initializer (str, optional): Kernel initializer to use for initializing model weights. Defaults to "glorot_normal".
913
-
914
- hidden_layer_sizes (NoneType, optional): Output units for each hidden layer. List should be of same length as the number of hidden layers. Not used for UBP Phase 1, but is here for compatibility. Defaults to "midpoint".
915
-
916
- num_hidden_layers (int, optional): Number of hidden layers to use. Not used in UBP Phase 1, but is here for compatibility. Defaults to 1.
917
-
918
- hidden_activation (str, optional): Activation function to use for hidden layers. Defaults to "elu".
919
-
920
- l1_penalty (float, optional): L1 regularization penalty to use to reduce overfitting. Defaults to 0.01.
921
-
922
- l2_penalty (float, optional): L2 regularization penalty to use to reduce overfitting. Defaults to 0.01.
923
-
924
- dropout_rate (float, optional): Dropout rate during training to reduce overfitting. Must be a float between 0 and 1. Defaults to 0.2.
925
-
926
- num_classes (int, optional): Number of classes in output. Corresponds to the 3rd dimension of the output shape (batch_size, n_features, num_classes). Defaults to 3.
927
-
928
- phase (int, optional): Current phase if doing UBP model. Defaults to 1.
929
-
930
- sample_weight (numpy.ndarray, optional): 2D sample weights of shape (n_samples, n_features). Should have values for each class weighted. Defaults to None.
931
-
932
- Example:
933
- >>>model = UBPPhase3(V=V, y=y, batch_size=32, missing_mask=missing_mask, output_shape=y_train.shape[1], n_components=3, weights_initializer="glorot_normal", hidden_layer_sizes="midpoint", num_hidden_layers=1, hidden_activation="elu", l1_penalty=1e-6, l2_penalty=1e-6, num_classes=3, phase=3)
934
- >>>
935
- >>>model.compile(optimizer=optimizer, loss=loss_func, metrics=[my_metrics], run_eagerly=True)
936
- >>>
937
- >>>history = model.fit(X, y, batch_size=batch_size, epochs=epochs, callbacks=[MyCallback()], validation_split=validation_split, shuffle=False)
938
-
939
- Raises:
940
- TypeError: V, y, missing_mask, output_shape must not be NoneType.
941
- ValueError: Maximum of 5 hidden layers.
942
- """
943
-
944
- def __init__(
180
+ def compute_loss(
945
181
  self,
946
- V=None,
947
- y=None,
948
- batch_size=32,
949
- missing_mask=None,
950
- output_shape=None,
951
- n_components=3,
952
- weights_initializer="glorot_normal",
953
- hidden_layer_sizes="midpoint",
954
- num_hidden_layers=1,
955
- hidden_activation="elu",
956
- dropout_rate=0.2,
957
- num_classes=3,
958
- phase=3,
959
- sample_weight=None,
960
- ):
961
- super(UBPPhase3, self).__init__()
962
-
963
- self.total_loss_tracker = tf.keras.metrics.Mean(name="total_loss")
964
- self.binary_accuracy_tracker = tf.keras.metrics.Mean(
965
- name="binary_accuracy"
966
- )
967
-
968
- nn = NeuralNetworkMethods()
969
- self.nn = nn
970
-
971
- if V is None:
972
- self._V = nn.init_weights(y.shape[0], n_components)
973
- elif isinstance(V, dict):
974
- self._V = V[n_components]
975
- else:
976
- self._V = V
977
-
978
- self._y = y
979
-
980
- hidden_layer_sizes = nn.validate_hidden_layers(
981
- hidden_layer_sizes, num_hidden_layers
182
+ y: torch.Tensor,
183
+ outputs: torch.Tensor,
184
+ mask: torch.Tensor | None = None,
185
+ class_weights: torch.Tensor | None = None,
186
+ gamma: float = 2.0,
187
+ ) -> torch.Tensor:
188
+ """Computes the masked focal loss between model outputs and ground truth.
189
+
190
+ This method calculates the loss value, handling class imbalance with weights and ignoring masked (missing) values in the ground truth tensor.
191
+
192
+ Args:
193
+ y (torch.Tensor): The ground truth tensor of shape `(batch_size, n_features, n_channels)`.
194
+ outputs (torch.Tensor): The model's raw output (logits) of shape `(batch_size, n_features, n_channels, n_classes)`.
195
+ mask (torch.Tensor | None): An optional boolean mask indicating which elements should be included in the loss calculation.
196
+ class_weights (torch.Tensor | None): An optional tensor of weights for each class to address imbalance.
197
+ gamma (float): The focusing parameter for the focal loss.
198
+
199
+ Returns:
200
+ torch.Tensor: The computed scalar loss value.
201
+ """
202
+ if class_weights is None:
203
+ class_weights = torch.ones(self.num_classes, device=outputs.device)
204
+
205
+ if mask is None:
206
+ mask = torch.ones_like(y, dtype=torch.bool)
207
+
208
+ # Explicitly flatten all tensors to the (N, C) and (N,) format.
209
+ # This creates a clear contract with the new MaskedFocalLoss function.
210
+ n_classes = outputs.shape[-1]
211
+ logits_flat = outputs.reshape(-1, n_classes)
212
+ targets_flat = y.reshape(-1)
213
+ mask_flat = mask.reshape(-1)
214
+
215
+ criterion = MaskedFocalLoss(gamma=gamma, alpha=class_weights)
216
+
217
+ return criterion(
218
+ logits_flat.to(self.device),
219
+ targets_flat.to(self.device),
220
+ valid_mask=mask_flat.to(self.device),
982
221
  )
983
-
984
- hidden_layer_sizes = nn.get_hidden_layer_sizes(
985
- y.shape[1], self._V.shape[1], hidden_layer_sizes
986
- )
987
-
988
- nn.validate_model_inputs(y, missing_mask, output_shape)
989
-
990
- self._missing_mask = missing_mask
991
- self.weights_initializer = weights_initializer
992
- self.phase = phase
993
- self.dropout_rate = dropout_rate
994
- self._sample_weight = sample_weight
995
-
996
- ### NOTE: I tried using just _V as the input to be refined, but it
997
- # wasn't getting updated. So I copy it here and it works.
998
- # V_latent is refined during train_step.
999
- self.V_latent_ = self._V.copy()
1000
-
1001
- # Initialize parameters used during train_step.
1002
- self._batch_idx = 0
1003
- self._batch_size = batch_size
1004
- self.n_components = n_components
1005
-
1006
- # No regularization in phase 3.
1007
- kernel_regularizer = None
1008
- self.kernel_regularizer = kernel_regularizer
1009
- kernel_initializer = None
1010
-
1011
- if hidden_activation.lower() == "leaky_relu":
1012
- activation = LeakyReLU(alpha=0.01)
1013
-
1014
- elif hidden_activation.lower() == "prelu":
1015
- activation = PReLU()
1016
-
1017
- elif hidden_activation.lower() == "selu":
1018
- activation = "selu"
1019
- kernel_initializer = "lecun_normal"
1020
-
1021
- else:
1022
- activation = hidden_activation
1023
-
1024
- if num_hidden_layers > 5:
1025
- raise ValueError(
1026
- f"The maximum number of hidden layers is 5, but got "
1027
- f"{num_hidden_layers}"
1028
- )
1029
-
1030
- self.dense2 = None
1031
- self.dense3 = None
1032
- self.dense4 = None
1033
- self.dense5 = None
1034
-
1035
- # Construct multi-layer perceptron.
1036
- # Add hidden layers dynamically.
1037
- self.dense1 = Dense(
1038
- hidden_layer_sizes[0],
1039
- input_shape=(n_components,),
1040
- activation=activation,
1041
- kernel_initializer=kernel_initializer,
1042
- )
1043
-
1044
- if num_hidden_layers >= 2:
1045
- self.dense2 = Dense(
1046
- hidden_layer_sizes[1],
1047
- activation=activation,
1048
- kernel_initializer=kernel_initializer,
1049
- )
1050
-
1051
- if num_hidden_layers >= 3:
1052
- self.dense3 = Dense(
1053
- hidden_layer_sizes[2],
1054
- activation=activation,
1055
- kernel_initializer=kernel_initializer,
1056
- )
1057
-
1058
- if num_hidden_layers >= 4:
1059
- self.dense4 = Dense(
1060
- hidden_layer_sizes[3],
1061
- activation=activation,
1062
- kernel_initializer=kernel_initializer,
1063
- )
1064
-
1065
- if num_hidden_layers == 5:
1066
- self.dense5 = Dense(
1067
- hidden_layer_sizes[4],
1068
- activation=activation,
1069
- kernel_initializer=kernel_initializer,
1070
- )
1071
-
1072
- self.output1 = Dense(
1073
- output_shape * num_classes,
1074
- kernel_initializer=kernel_initializer,
1075
- )
1076
-
1077
- self.rshp = Reshape((output_shape, num_classes))
1078
-
1079
- self.dropout_layer = Dropout(rate=dropout_rate)
1080
- self.activation = Activation("sigmoid")
1081
-
1082
- def call(self, inputs, training=None):
1083
- x = self.dense1(inputs)
1084
- if self.dense2 is not None:
1085
- x = self.dense2(x)
1086
- if self.dense3 is not None:
1087
- x = self.dense3(x)
1088
- if self.dense4 is not None:
1089
- x = self.dense4(x)
1090
- if self.dense5 is not None:
1091
- x = self.dense5(x)
1092
-
1093
- x = self.output1(x)
1094
- x = self.rshp(x)
1095
- return self.activation(x)
1096
-
1097
- def model(self):
1098
- x = tf.keras.Input(shape=(self.n_components,))
1099
- return tf.keras.Model(inputs=[x], outputs=self.call(x))
1100
-
1101
- def set_model_outputs(self):
1102
- x = tf.keras.Input(shape=(self.n_components,))
1103
- model = tf.keras.Model(inputs=[x], outputs=self.call(x))
1104
- self.outputs = model.outputs
1105
-
1106
- @property
1107
- def metrics(self):
1108
- return [
1109
- self.total_loss_tracker,
1110
- self.binary_accuracy_tracker,
1111
- ]
1112
-
1113
- def train_step(self, data):
1114
- """Train step function. Parameters are set in the UBPCallbacks callback"""
1115
- y = self._y
1116
-
1117
- (
1118
- v,
1119
- y_true,
1120
- sample_weight,
1121
- missing_mask,
1122
- batch_start,
1123
- batch_end,
1124
- ) = self.nn.prepare_training_batches(
1125
- self.V_latent_,
1126
- y,
1127
- self._batch_size,
1128
- self._batch_idx,
1129
- True,
1130
- self.n_components,
1131
- self._sample_weight,
1132
- self._missing_mask,
1133
- )
1134
-
1135
- src = [v]
1136
-
1137
- if sample_weight is not None:
1138
- sample_weight_masked = tf.convert_to_tensor(
1139
- sample_weight[~missing_mask], dtype=tf.float32
1140
- )
1141
- else:
1142
- sample_weight_masked = None
1143
-
1144
- y_true_masked = tf.boolean_mask(
1145
- tf.convert_to_tensor(y_true, dtype=tf.float32),
1146
- tf.reduce_any(tf.not_equal(y_true, -1), axis=2),
1147
- )
1148
-
1149
- # NOTE: Earlier model architectures incorrectly
1150
- # applied one gradient to all the variables, including
1151
- # the weights and v. Here we apply them separately, per
1152
- # the UBP manuscript.
1153
- with tf.GradientTape(persistent=True) as tape:
1154
- # Forward pass. Watch input tensor v.
1155
- tape.watch(v)
1156
- y_pred = self(v, training=True)
1157
- y_pred_masked = tf.boolean_mask(
1158
- y_pred, tf.reduce_any(tf.not_equal(y_true, -1), axis=2)
1159
- )
1160
- ### NOTE: If you get the error, "'tuple' object has no attribute
1161
- ### 'rank'", then convert y_true to a tensor object."
1162
- loss = self.compiled_loss(
1163
- y_true_masked,
1164
- y_pred_masked,
1165
- sample_weight=sample_weight_masked,
1166
- )
1167
-
1168
- # Refine the watched variables with
1169
- # gradient descent backpropagation
1170
- gradients = tape.gradient(loss, self.trainable_variables)
1171
- self.optimizer.apply_gradients(
1172
- zip(gradients, self.trainable_variables)
1173
- )
1174
-
1175
- # Apply separate gradients to v.
1176
- vgrad = tape.gradient(loss, src)
1177
- self.optimizer.apply_gradients(zip(vgrad, src))
1178
-
1179
- del tape
1180
-
1181
- # NOTE: run_eagerly must be set to True in the compile() method for this
1182
- # to work. Otherwise it can't convert a Tensor object to a numpy array.
1183
- # There is really no other way to set v back to V_latent_ in graph
1184
- # mode as far as I know. eager execution is slower, so it would be nice
1185
- # to find a way to do this without converting to numpy.
1186
- self.V_latent_[batch_start:batch_end, :] = v.numpy()
1187
-
1188
- self.total_loss_tracker.update_state(loss)
1189
- self.binary_accuracy_tracker.update_state(
1190
- tf.keras.metrics.binary_accuracy(y_true_masked, y_pred_masked)
1191
- )
1192
-
1193
- ### NOTE: If you get the error, "'tuple' object has no attribute
1194
- ### 'rank', then convert y_true to a tensor object."
1195
- return {
1196
- "loss": self.total_loss_tracker.result(),
1197
- "binary_accuracy": self.binary_accuracy_tracker.result(),
1198
- }
1199
-
1200
- def test_step(self, data):
1201
- """Train step function. Parameters are set in the UBPCallbacks callback"""
1202
- y = self._y
1203
-
1204
- (
1205
- v,
1206
- y_true,
1207
- sample_weight,
1208
- missing_mask,
1209
- batch_start,
1210
- batch_end,
1211
- ) = self.nn.prepare_training_batches(
1212
- self.V_latent_,
1213
- y,
1214
- self._batch_size,
1215
- self._batch_idx,
1216
- True,
1217
- self.n_components,
1218
- self._sample_weight,
1219
- self._missing_mask,
1220
- )
1221
-
1222
- if sample_weight is not None:
1223
- sample_weight_masked = tf.convert_to_tensor(
1224
- sample_weight[~missing_mask], dtype=tf.float32
1225
- )
1226
- else:
1227
- sample_weight_masked = None
1228
-
1229
- y_true_masked = tf.boolean_mask(
1230
- tf.convert_to_tensor(y_true, dtype=tf.float32),
1231
- tf.reduce_any(tf.not_equal(y_true, -1), axis=2),
1232
- )
1233
-
1234
- y_pred = self(v, training=False)
1235
- y_pred_masked = tf.boolean_mask(
1236
- y_pred, tf.reduce_any(tf.not_equal(y_true, -1), axis=2)
1237
- )
1238
- ### NOTE: If you get the error, "'tuple' object has no attribute
1239
- ### 'rank'", then convert y_true to a tensor object."
1240
- loss = self.compiled_loss(
1241
- y_true_masked,
1242
- y_pred_masked,
1243
- sample_weight=sample_weight_masked,
1244
- )
1245
-
1246
- ### NOTE: If you get the error, "'tuple' object has no attribute
1247
- ### 'rank', then convert y_true to a tensor object."
1248
- self.total_loss_tracker.update_state(loss)
1249
- self.binary_accuracy_tracker.update_state(
1250
- tf.keras.metrics.binary_accuracy(y_true_masked, y_pred_masked)
1251
- )
1252
-
1253
- return {
1254
- "loss": self.total_loss_tracker.result(),
1255
- "binary_accuracy": self.binary_accuracy_tracker.result(),
1256
- }
1257
-
1258
- @property
1259
- def V_latent(self):
1260
- """Randomly initialized input variable that gets refined during training.
1261
- :noindex:
1262
- """
1263
- return self.V_latent_
1264
-
1265
- @property
1266
- def batch_size(self):
1267
- """Batch (=step) size per epoch.
1268
- :noindex:
1269
- """
1270
- return self._batch_size
1271
-
1272
- @property
1273
- def batch_idx(self):
1274
- """Current batch (=step) index.
1275
- :noindex:
1276
- """
1277
- return self._batch_idx
1278
-
1279
- @property
1280
- def y(self):
1281
- """Full input dataset y.
1282
- :noindex:
1283
- """
1284
- return self._y
1285
-
1286
- @property
1287
- def missing_mask(self):
1288
- """Missing mask of shape (y.shape[0], y.shape[1])
1289
- :noindex:
1290
- """
1291
- return self._missing_mask
1292
-
1293
- @property
1294
- def sample_weight(self):
1295
- """Sample weights of shpe (y.shape[0], y.shape[1])
1296
- :noindex:
1297
- """
1298
- return self._sample_weight
1299
-
1300
- @V_latent.setter
1301
- def V_latent(self, value):
1302
- """Set randomly initialized input variable. Refined during training.
1303
- :noindex:
1304
- """
1305
- self.V_latent_ = value
1306
-
1307
- @batch_size.setter
1308
- def batch_size(self, value):
1309
- """Set batch_size parameter.
1310
- :noindex:
1311
- """
1312
- self._batch_size = int(value)
1313
-
1314
- @batch_idx.setter
1315
- def batch_idx(self, value):
1316
- """Set current batch (=step) index.
1317
- :noindex:
1318
- """
1319
- self._batch_idx = int(value)
1320
-
1321
- @y.setter
1322
- def y(self, value):
1323
- """Set y after each epoch.
1324
- :noindex:
1325
- """
1326
- self._y = value
1327
-
1328
- @missing_mask.setter
1329
- def missing_mask(self, value):
1330
- """Set missing_mask after each epoch.
1331
- :noindex:
1332
- """
1333
- self._missing_mask = value
1334
-
1335
- @sample_weight.setter
1336
- def sample_weight(self, value):
1337
- """Set sample_weight after each epoch.
1338
- :noindex:
1339
- """
1340
- self._sample_weight = value