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.
- {pg_sui-1.0.2.1.dist-info → pg_sui-1.6.8.dist-info}/METADATA +51 -70
- pg_sui-1.6.8.dist-info/RECORD +78 -0
- {pg_sui-1.0.2.1.dist-info → pg_sui-1.6.8.dist-info}/WHEEL +1 -1
- pg_sui-1.6.8.dist-info/entry_points.txt +4 -0
- pg_sui-1.6.8.dist-info/top_level.txt +1 -0
- pgsui/__init__.py +35 -54
- pgsui/_version.py +34 -0
- pgsui/cli.py +635 -0
- pgsui/data_processing/config.py +576 -0
- pgsui/data_processing/containers.py +1782 -0
- pgsui/data_processing/transformers.py +121 -1103
- pgsui/electron/app/__main__.py +5 -0
- pgsui/electron/app/icons/icons/1024x1024.png +0 -0
- pgsui/electron/app/icons/icons/128x128.png +0 -0
- pgsui/electron/app/icons/icons/16x16.png +0 -0
- pgsui/electron/app/icons/icons/24x24.png +0 -0
- pgsui/electron/app/icons/icons/256x256.png +0 -0
- pgsui/electron/app/icons/icons/32x32.png +0 -0
- pgsui/electron/app/icons/icons/48x48.png +0 -0
- pgsui/electron/app/icons/icons/512x512.png +0 -0
- pgsui/electron/app/icons/icons/64x64.png +0 -0
- pgsui/electron/app/icons/icons/icon.icns +0 -0
- pgsui/electron/app/icons/icons/icon.ico +0 -0
- pgsui/electron/app/main.js +189 -0
- pgsui/electron/app/package-lock.json +6893 -0
- pgsui/electron/app/package.json +50 -0
- pgsui/electron/app/preload.js +15 -0
- pgsui/electron/app/server.py +146 -0
- pgsui/electron/app/ui/logo.png +0 -0
- pgsui/electron/app/ui/renderer.js +130 -0
- pgsui/electron/app/ui/styles.css +59 -0
- pgsui/electron/app/ui/ui_shim.js +72 -0
- pgsui/electron/bootstrap.py +43 -0
- pgsui/electron/launch.py +59 -0
- pgsui/electron/package.json +14 -0
- pgsui/example_data/popmaps/{test.popmap → phylogen_nomx.popmap} +185 -99
- pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz +0 -0
- pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz.tbi +0 -0
- pgsui/impute/deterministic/imputers/allele_freq.py +691 -0
- pgsui/impute/deterministic/imputers/mode.py +679 -0
- pgsui/impute/deterministic/imputers/nmf.py +221 -0
- pgsui/impute/deterministic/imputers/phylo.py +971 -0
- pgsui/impute/deterministic/imputers/ref_allele.py +530 -0
- pgsui/impute/supervised/base.py +339 -0
- pgsui/impute/supervised/imputers/hist_gradient_boosting.py +293 -0
- pgsui/impute/supervised/imputers/random_forest.py +287 -0
- pgsui/impute/unsupervised/base.py +924 -0
- pgsui/impute/unsupervised/callbacks.py +89 -263
- pgsui/impute/unsupervised/imputers/autoencoder.py +972 -0
- pgsui/impute/unsupervised/imputers/nlpca.py +1264 -0
- pgsui/impute/unsupervised/imputers/ubp.py +1288 -0
- pgsui/impute/unsupervised/imputers/vae.py +957 -0
- pgsui/impute/unsupervised/loss_functions.py +158 -0
- pgsui/impute/unsupervised/models/autoencoder_model.py +208 -558
- pgsui/impute/unsupervised/models/nlpca_model.py +149 -468
- pgsui/impute/unsupervised/models/ubp_model.py +198 -1317
- pgsui/impute/unsupervised/models/vae_model.py +259 -618
- pgsui/impute/unsupervised/nn_scorers.py +215 -0
- pgsui/utils/classification_viz.py +591 -0
- pgsui/utils/misc.py +35 -480
- pgsui/utils/plotting.py +514 -824
- pgsui/utils/scorers.py +212 -438
- pg_sui-1.0.2.1.dist-info/RECORD +0 -75
- pg_sui-1.0.2.1.dist-info/top_level.txt +0 -3
- pgsui/example_data/phylip_files/test_n10.phy +0 -118
- pgsui/example_data/phylip_files/test_n100.phy +0 -118
- pgsui/example_data/phylip_files/test_n2.phy +0 -118
- pgsui/example_data/phylip_files/test_n500.phy +0 -118
- pgsui/example_data/structure_files/test.nopops.1row.10sites.str +0 -117
- pgsui/example_data/structure_files/test.nopops.2row.100sites.str +0 -234
- pgsui/example_data/structure_files/test.nopops.2row.10sites.str +0 -234
- pgsui/example_data/structure_files/test.nopops.2row.30sites.str +0 -234
- pgsui/example_data/structure_files/test.nopops.2row.allsites.str +0 -234
- pgsui/example_data/structure_files/test.pops.1row.10sites.str +0 -117
- pgsui/example_data/structure_files/test.pops.2row.10sites.str +0 -234
- pgsui/example_data/trees/test.iqtree +0 -376
- pgsui/example_data/trees/test.qmat +0 -5
- pgsui/example_data/trees/test.rate +0 -2033
- pgsui/example_data/trees/test.tre +0 -1
- pgsui/example_data/trees/test_n10.rate +0 -19
- pgsui/example_data/trees/test_n100.rate +0 -109
- pgsui/example_data/trees/test_n500.rate +0 -509
- pgsui/example_data/trees/test_siterates.txt +0 -2024
- pgsui/example_data/trees/test_siterates_n10.txt +0 -10
- pgsui/example_data/trees/test_siterates_n100.txt +0 -100
- pgsui/example_data/trees/test_siterates_n500.txt +0 -500
- pgsui/example_data/vcf_files/test.vcf +0 -244
- pgsui/example_data/vcf_files/test.vcf.gz +0 -0
- pgsui/example_data/vcf_files/test.vcf.gz.tbi +0 -0
- pgsui/impute/estimators.py +0 -735
- pgsui/impute/impute.py +0 -1486
- pgsui/impute/simple_imputers.py +0 -1439
- pgsui/impute/supervised/iterative_imputer_fixedparams.py +0 -785
- pgsui/impute/supervised/iterative_imputer_gridsearch.py +0 -1027
- pgsui/impute/unsupervised/keras_classifiers.py +0 -702
- pgsui/impute/unsupervised/models/in_development/cnn_model.py +0 -486
- pgsui/impute/unsupervised/neural_network_imputers.py +0 -1424
- pgsui/impute/unsupervised/neural_network_methods.py +0 -1549
- pgsui/pg_sui.py +0 -261
- pgsui/utils/sequence_tools.py +0 -407
- simulation/sim_benchmarks.py +0 -333
- simulation/sim_treeparams.py +0 -475
- test/__init__.py +0 -0
- test/pg_sui_simtest.py +0 -215
- test/pg_sui_testing.py +0 -523
- test/test.py +0 -297
- test/test_pgsui.py +0 -374
- test/test_tkc.py +0 -214
- {pg_sui-1.0.2.1.dist-info → pg_sui-1.6.8.dist-info/licenses}/LICENSE +0 -0
- /pgsui/{example_data/trees → electron/app}/__init__.py +0 -0
- /pgsui/impute/{unsupervised/models/in_development → supervised/imputers}/__init__.py +0 -0
- {simulation → pgsui/impute/unsupervised/imputers}/__init__.py +0 -0
|
@@ -1,1340 +1,221 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
import warnings
|
|
1
|
+
from typing import List, Literal
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
import torch.nn as nn
|
|
6
|
+
from snpio.utils.logging import LoggerManager
|
|
8
7
|
|
|
9
|
-
|
|
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
|
-
|
|
18
|
-
|
|
11
|
+
class UBPModel(nn.Module):
|
|
12
|
+
"""An Unsupervised Backpropagation (UBP) model with a multi-phase decoder.
|
|
19
13
|
|
|
20
|
-
from
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
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
|
-
|
|
661
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|