Repository navigation
Expand file tree
/
Copy pathautoencoder.py
More file actions
549 lines (479 loc) · 20.1 KB
/
Copy pathautoencoder.py
File metadata and controls
549 lines (479 loc) · 20.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
"""
The autoencoder class along with classes that compose the autoencoder class and helper functions.
"""
from typing import Any, List, Optional, Sequence, Tuple, Dict, Union
from pathlib import Path
import pickle
import torch as pt
from torch import nn
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
import numpy as np
from cp_flatten import QuackConstants
import torch.nn.functional as F
import pytorch_lightning as pl
from pytorch_lightning.callbacks import BasePredictionWriter
def item_path(index: int, suffix: str = 'png', dir_only: bool = False, is_collection: bool = False) -> str:
"""
A helper function to construct a file path string given a data item index.
Parameters
----------
index: int
The index of the data item.
suffix: str
The file suffix.
dir_only: bool
Construct only a path to the enclosing directory.
is_collection
A data item index refers to a collection of files and not a single file.
Returns
-------
str
The file path.
"""
rank_five = index // 100000
remainder = index - (rank_five * 100000)
rank_three_four = remainder // 1000
stem = f'/{rank_five}/{rank_three_four}'
if is_collection:
stem = stem + f'/{index}'
if dir_only:
return stem
return stem + f'/{index}.{suffix}'
class AutoencoderWriter(BasePredictionWriter):
"""
Extends pytorch_lightning.callbacks.prediction_writer.BasePredictionWriter to store encoded Quack data.
"""
def __init__(self,
write_interval: str = 'batch',
storage_path: str = '~/data',
filtered: bool = False,
evaluate: bool = False,
reduction_threshold: float = 1.0) -> None:
"""
Constructor for AutoencoderWriter.
Parameters
----------
write_interval: str
See parent class BasePredictionWriter
storage_path: str
A string file path to the directory in which output will be stored.
filtered: bool
Should the output be filtered to exclude undetermined items (censored/uncensored only)?
evaluate: bool
Should the output be filtered to include only undetermined items for model evaluation?
"""
super().__init__(write_interval)
self.__storage_path = storage_path
self.__filtered = filtered and not evaluate
self.__evaluate = evaluate
# Prepare to reduce the number of uncensored items.
self.__rng = np.random.default_rng()
self.__reduction_threshold = reduction_threshold
def write_on_batch_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", prediction: Any,
batch_indices: Optional[Sequence[int]], batch: Any, batch_idx: int,
dataloader_idx: int) -> None:
"""
Logic to write the results of a single batch to files.
Parameters
----------
Parameter signature defined in the parent class.
Returns
----------
void
See Also
----------
pytorch_lightning.callbacks.prediction_writer.BasePredictionWriter.write_on_batch_end
"""
meta: List[dict]
processed: pt.Tensor
meta, processed = prediction
# Copy to cpu and convert to numpy array.
prep_for_numpy = processed.cpu()
data = prep_for_numpy.numpy()
for row in data:
# Get this row's metadata.
row_meta = meta.pop(0)
# Ensure storage is ready.
storage_path = Path(self.__storage_path + item_path(row_meta['timestamp'], dir_only=True))
storage_path.mkdir(parents=True, exist_ok=True)
data_storage = Path(self.__storage_path + item_path(row_meta['timestamp'], 'pyc'))
# Evaluate for filtering:
if row_meta['censored'] == 1:
if self.__evaluate:
continue
elif row_meta['censored'] == 0:
if self.__filtered:
continue
elif row_meta['censored'] == -1:
if self.__evaluate or (self.__filtered and self.__rng.random() > self.__reduction_threshold):
# Randomly exclude in proportion to the reduction threshold
# to keep the data balanced.
continue
# Store:
data = {
'metadata': row_meta,
'encoded': row
}
with data_storage.open(mode='wb') as target:
pickle.dump(data, target)
def write_on_epoch_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", predictions: Sequence[Any],
batch_indices: Optional[Sequence[Any]]) -> None:
"""
This class runs on every distributed node and aggregation is not practical due to the size of our dataset. We
do not save the predictions after the batch to avoid running out of memory. Method is required but therefore
nothing to do here.
"""
pass
class AttentionScore(nn.Module):
"""
Uses the dot-product to calculate the attention scores.
References
----------
Edward Raff. 2021. Inside Deep Learning: Math, Algorithms, Models. Manning
Publications Co., Shelter Island, New York
"""
def __init__(self, dim: int) -> None:
"""
Constructs AttentionScore.
Parameters
----------
dim: int
The dimension of the hidden state axis coming into the dot-product.
"""
super().__init__()
self.H = dim
def forward(self, states: pt.Tensor, context: pt.Tensor) -> pt.Tensor:
"""
Computes the dot-product score.
Parameters
----------
states: pt.Tensor
Hidden states; shape (B, T, H)
context: pt.Tensor
Context values; shape (B, H)
Returns
-------
pt.Tensor
Scores for T items, based on context; shape (B, T, 1)
"""
# T = states.size(1)
scores = pt.bmm(states, context.unsqueeze(2)) / np.sqrt((self.H)) # (B, T, H) -> (B, T, 1)
return scores
class AttentionModule(nn.Module):
"""
Applies attention to the hidden states.
References
----------
Edward Raff. 2021. Inside Deep Learning: Math, Algorithms, Models. Manning
Publications Co., Shelter Island, New York
"""
def __init__(self):
super().__init__()
def forward(self, states: pt.Tensor, attention_scores, mask: Union[None, pt.Tensor] = None):
"""
Processes the attention inputs.
Parameters
----------
states: pt.Tensor
(B, T, H) shape giving the T different possible inputs attention_scores:
(B, T, 1) score for each item at each context
mask: Union[None, pt.Tensor]
None if all items are present. Else a boolean tensor of shape
(B, T), with `True` indicating which items are present / valid.
Returns
--------
Tuple[pt.Tensor, pt.Tensor]
A tuple with two tensors. The first tensor is the final context from applying the attention to the
states (B, H) shape. The second tensor is the weights for each state with shape (B, T, 1).
"""
if mask is not None:
# set everything not present to a large negative value that will cause vanishing gradients
attention_scores[~mask] = -1000.0
# compute the weight for each score
weights = F.softmax(attention_scores, dim=1) # (B, T, 1) still, but sum(T) = 1
final_context = (states * weights).sum(dim=1) # (B, T, D) * (B, T, 1) -> (B, D)
return final_context, weights
class QuackAutoEncoder(pl.LightningModule):
"""
A Sequence-to-Sequence based autoencoder
References
----------
Edward Raff. 2021. Inside Deep Learning: Math, Algorithms, Models. Manning
Publications Co., Shelter Island, New York
"""
def __init__(self, num_embeddings: int, embed_size: int, hidden_size: int, layers: int = 1,
max_decode_length: int = None, learning_rate: float = 1e-1, learning_rate_min: float = 1e-4,
lr_max_epochs: int = -1, *args: Any, **kwargs: Any) -> None:
"""
Constructor for QuackAutoEncoder.
Parameters
----------
num_embeddings: int
Hyperparameter for nn.Embedding
embed_size: int
Hyperparameter for nn.Embedding and nn.GRU
hidden_size: int
Hyperparameter for nn.GRU
layers: int
Hyperparameter for nn.GRU
max_decode_length: int
Hyperparameter used to limit the decoder module.
learning_rate: float
Hyperparameter passed to pt.optim.lr_scheduler.CosineAnnealingLR
learning_rate_min: float
Hyperparameter passed to pt.optim.lr_scheduler.CosineAnnealingLR
lr_max_epochs: int
Hyperparameter passed to pt.optim.lr_scheduler.CosineAnnealingLR
args: Any
Passed to the parent constructor.
kwargs Any
Passed to the parent constructor.
"""
super().__init__(*args, **kwargs)
self.__hidden_size = hidden_size
self.__max_decode_length = max_decode_length
self.__learning_rate_init = learning_rate
self.__learning_rate_min = learning_rate_min
self.__lr_max_epochs = lr_max_epochs
# Networks
self.__embed = nn.Embedding(num_embeddings, embed_size, padding_idx=int(QuackConstants.XLMR_PAD.value))
# We will use a bi-directional GRU so hidden size is divided by 2.
self.__encoder = nn.GRU(input_size=embed_size, hidden_size=hidden_size // 2, num_layers=layers,
batch_first=True, bidirectional=True)
# Decoder is uni-directional, and uses GRUCell.
self.__decoder = nn.ModuleList(
[nn.GRUCell(embed_size, hidden_size)]
+ [nn.GRUCell(hidden_size, hidden_size) for _ in range(layers - 1)]
)
# A small, fully connected, network that will combine attention and context to predict the next word value.
self.__predict_word = nn.Sequential(
nn.Linear(2 * hidden_size, hidden_size),
nn.LeakyReLU(),
nn.LayerNorm(hidden_size),
nn.Linear(hidden_size, hidden_size),
nn.LeakyReLU(),
nn.LayerNorm(hidden_size),
nn.Linear(hidden_size, num_embeddings)
)
# An attention network
self.__attention = AttentionModule()
self.__attention_score = AttentionScore(hidden_size)
self.save_hyperparameters()
# For tuning.
self.batch_size = 2
def mask_input(self, padded_input: pt.Tensor) -> pt.Tensor:
"""
Creates a mask tensor to filter out padding.
Parameters
----------
padded_input: pt.Tensor
The padded input (B, T)
Returns
-------
pt.Tensor
A boolean tensor (B, T). True indicates the value at that time is usable, not padding.
"""
with pt.no_grad():
mask = padded_input.ne(QuackConstants.XLMR_PAD.value)
return mask
def loss_over_time(self, original: pt.Tensor, output: pt.Tensor) -> float:
"""
Sum losses over time dimension, comparing original tokens and predicted tokens.
Parameters
----------
original: pt.Tensor
The original input
output: pt.Tensor
The predicted output
Returns
-------
float
The aggregated CrossEntropyLoss.
"""
cross_entropy = nn.CrossEntropyLoss(ignore_index=QuackConstants.XLMR_PAD.value)
max_time = min(original.size(1), output.size(1)) # T
loss = 0.0
for time in range(max_time):
loss += cross_entropy(output[:, time], original[:, time])
return loss
def forward(self, x: pt.Tensor) -> Tuple[pt.Tensor, pt.Tensor]:
"""
We put just the encoding process in forward. The twin decoding process will only be found in
the common step used in training and testing. This prepares the model for its intended use as
encoding and condensing latent features.
Parameters
----------
x: pt.Tensor
The input, which should be (B, T) shaped.
Returns
-------
Tuple[pt.Tensor, pt.Tensor]
* Final outputs of the encoding layer.
* Encoded processing of the input tensor
"""
# Store the dimensions
batch_dim, temporal_dim = x.size() # B, T
mask = self.mask_input(x)
# Calculate sequence lengths using the mask.
lengths = mask.sum(dim=1).view(-1) # Shape (B).
# Transform to an Embed.
x_embed = self.__embed(x) # Shape (B, T, D) which is what GRU expects.
# GRU efficiency is increased with a PackedSequence object.
x_packed = pack_padded_sequence(x_embed, lengths.cpu(), batch_first=True, enforce_sorted=False)
encoded_sequence, last_h = self.__encoder(x_packed)
encoded_sequence, _ = pad_packed_sequence(encoded_sequence)
# encoded_sequence has shape (B, T, 2, D/2) since the GRU is bidirectional.
encoded_sequence = encoded_sequence.view(batch_dim, temporal_dim, -1) # encoded_sequence shape is now (B, T, D)
hidden_size = encoded_sequence.size(2)
last_h = last_h.view(-1, 2, batch_dim, hidden_size//2)[-1, :, :, :] # last_h shape is now (2, B, D/2)
last_h = last_h.permute(1, 0, 2).reshape(batch_dim, -1) # last_h shape is now (B, D)
return last_h, encoded_sequence
def _common_step(self, x: pt.Tensor, batch_index: int, step_id: str) -> float:
"""
The common step contains the decoder logic, and begins by calling self.forward() so that it executes the full
autoencoder cycle.
Parameters
----------
x: pt.Tensor
The input, which should be (B, T) shaped.
batch_index: int
The batch index
step_id: str
The step id.
Returns
-------
float
The total loss over the sequence.
"""
final_state, sequence = self.forward(x)
# Now add the decoder part.
decoder_depth = len(self.__decoder)
# We will stack the predictions at the end.
all_predictions = []
# First, replicate the initial hidden state for each cell in the decoder.
hidden_priors = [final_state for cell in range(decoder_depth)]
# Prime the value of decoder input for the first iteration of the decoding loop.
mask = self.mask_input(x)
lengths = mask.sum(dim=1).view(-1) # Shape (B).
decoder_input = self.__embed(x.gather(1, lengths.view(-1, 1)-1).flatten()) #(B, D)
# Calculate decoding steps:
temporal_dim = x.size(1) # T
steps = min(self.__max_decode_length, temporal_dim)
# Do we use teacher forcing (true) or auto-regressive (false)
teacher_forcing = np.random.choice((True, False))
for time in range(steps):
x_in = decoder_input #(B, D)
for layer in range(decoder_depth):
hidden_prior = hidden_priors[layer]
# Process through the decoder cells and store the result
h = self.__decoder[layer](x_in, hidden_prior)
hidden_priors[layer] = h
x_in = h
hidden_decoder = x_in # (B, D), we now have the hidden state for the decoder at this time step.
# Now we apply attention.
scores = self.__attention_score(sequence, hidden_decoder)
context, weights = self.__attention(sequence, scores, mask)
# Now we use the prediction network to get the next predicted token in the decoding.
prediction_input = pt.cat((context, hidden_decoder), dim=1) # (B, D) + (B, D) -> (B, 2*D)
token_prediction = self.__predict_word(prediction_input) # (B, 2*D) -> (B, V)
all_predictions.append(token_prediction)
# Now select the input of the next time step. No gradient on the input tokens.
with pt.no_grad():
if teacher_forcing:
next_words = x[:, time].squeeze()
else:
# Sample the next token based on the predictions made
next_words = pt.multinomial(F.softmax(token_prediction, dim=1), 1)[:, -1]
decoder_input = self.__embed(next_words)
predicted_batch = pt.stack(all_predictions, dim=1)
loss = self.loss_over_time(x, predicted_batch)
log_interval_option = None if step_id == 'train' else True
log_sync = False if step_id == 'train' else True
self.log(f"{step_id}_loss", loss, on_step=log_interval_option, sync_dist=log_sync)
return loss
def training_step(self, x: pt.Tensor, batch_index: int) -> dict:
"""
Calls _common_step for step 'train'.
Parameters
----------
x: pt. Tensor
An input tensor
batch_index: int
The index of the batch. Required to match the parent signature. Unused in our model.
Returns
-------
dict
Format expected by the parent class. Has a single key, 'loss' with the return value of _common_step.
"""
return {'loss': self._common_step(x, batch_index, 'train')}
def validation_step(self, x: pt.Tensor, batch_index: int) -> float:
"""
Calls _common_step for step 'val'.
Parameters
----------
x: pt. Tensor
An input tensor
batch_index: int
The index of the batch. Required to match the parent signature. Unused in our model.
Returns
-------
float
Format expected by the parent class. The loss returned by _common_step.
"""
return self._common_step(x, batch_index, 'val')
def test_step(self, x: pt.Tensor, batch_index: int) -> float:
"""
Calls _common_step for step 'val'.
Parameters
----------
x: pt. Tensor
An input tensor
batch_index: int
The index of the batch. Required to match the parent signature. Unused in our model.
Returns
-------
float
Format expected by the parent class. The loss returned by _common_step.
"""
return self._common_step(x, batch_index, 'test')
def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Tuple[List[dict], pt.Tensor]:
"""
Calls _common_step for step 'predict'.
Parameters
----------
batch: pt. Tuple[dict, pt.Tensor]
An tuple of a metadata dictionary and the associated input data
batch_idx: int
The index of the batch. Required to match the parent signature. Unused in our model.
dataloader_idx: int
Index of the current dataloader. Required to match the parent signature. Unused in our model.
Returns
-------
Tuple[dict, pt.Tensor]
An tuple of the batch metadata dictionary and the associated output data
"""
meta, data = batch
encoded, _ = self.forward(data)
return meta, encoded
def configure_optimizers(self) -> Dict:
"""
Configures the optimizer and learning rate scheduler objects.
Returns
-------
dict
A dictionary with keys:
- optimizer: pt.optim.AdamW
- lr_scheduler: pt.optim.lr_scheduler.CosineAnnealingLR
See Also
--------
pytorch_lightning.core.lightning.LightningModule.configure_optimizers
"""
configured_optimizer = pt.optim.AdamW(self.parameters(), lr=self.__learning_rate_init)
return {
'optimizer': configured_optimizer,
'lr_scheduler': pt.optim.lr_scheduler.CosineAnnealingLR(
optimizer=configured_optimizer,
T_max=self.__lr_max_epochs,
eta_min=self.__learning_rate_min
)
}