mostlyai-engine 1.0.0__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.
Files changed (45) hide show
  1. mostlyai/engine/__init__.py +27 -0
  2. mostlyai/engine/_common.py +612 -0
  3. mostlyai/engine/_dtypes.py +58 -0
  4. mostlyai/engine/_encoding_types/__init__.py +13 -0
  5. mostlyai/engine/_encoding_types/language/__init__.py +13 -0
  6. mostlyai/engine/_encoding_types/language/text.py +41 -0
  7. mostlyai/engine/_encoding_types/tabular/__init__.py +13 -0
  8. mostlyai/engine/_encoding_types/tabular/categorical.py +115 -0
  9. mostlyai/engine/_encoding_types/tabular/character.py +155 -0
  10. mostlyai/engine/_encoding_types/tabular/datetime.py +254 -0
  11. mostlyai/engine/_encoding_types/tabular/itt.py +352 -0
  12. mostlyai/engine/_encoding_types/tabular/lat_long.py +353 -0
  13. mostlyai/engine/_encoding_types/tabular/numeric.py +548 -0
  14. mostlyai/engine/_language/__init__.py +17 -0
  15. mostlyai/engine/_language/common.py +110 -0
  16. mostlyai/engine/_language/encoding.py +162 -0
  17. mostlyai/engine/_language/engine/__init__.py +13 -0
  18. mostlyai/engine/_language/engine/base.py +51 -0
  19. mostlyai/engine/_language/engine/hf_engine.py +136 -0
  20. mostlyai/engine/_language/engine/vllm_engine.py +187 -0
  21. mostlyai/engine/_language/formatron_utils.py +114 -0
  22. mostlyai/engine/_language/generation.py +309 -0
  23. mostlyai/engine/_language/lstm.py +139 -0
  24. mostlyai/engine/_language/tokenizer_utils.py +144 -0
  25. mostlyai/engine/_language/training.py +718 -0
  26. mostlyai/engine/_memory.py +69 -0
  27. mostlyai/engine/_tabular/__init__.py +13 -0
  28. mostlyai/engine/_tabular/argn.py +1382 -0
  29. mostlyai/engine/_tabular/common.py +29 -0
  30. mostlyai/engine/_tabular/encoding.py +406 -0
  31. mostlyai/engine/_tabular/fairness.py +245 -0
  32. mostlyai/engine/_tabular/generation.py +1157 -0
  33. mostlyai/engine/_tabular/training.py +856 -0
  34. mostlyai/engine/_training_utils.py +159 -0
  35. mostlyai/engine/_workspace.py +225 -0
  36. mostlyai/engine/analysis.py +571 -0
  37. mostlyai/engine/domain.py +171 -0
  38. mostlyai/engine/encoding.py +42 -0
  39. mostlyai/engine/generation.py +110 -0
  40. mostlyai/engine/splitting.py +257 -0
  41. mostlyai/engine/training.py +104 -0
  42. mostlyai_engine-1.0.0.dist-info/METADATA +179 -0
  43. mostlyai_engine-1.0.0.dist-info/RECORD +45 -0
  44. mostlyai_engine-1.0.0.dist-info/WHEEL +4 -0
  45. mostlyai_engine-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,1382 @@
1
+ # Copyright 2025 MOSTLY AI
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ TabularARGN
17
+
18
+ This module provides two models for tabular data:
19
+
20
+ - FlatModel: A model that learns to generate tabular data, one row at a time.
21
+ - SequentialModel: A model that learns to generate tabular data, one coherent sequence at a time.
22
+
23
+ Each model can learn to generate data given some context data, which can either consist of scalar or sequential data.
24
+ """
25
+
26
+ from enum import Enum
27
+ from functools import partial
28
+ import logging
29
+ from typing import Any, Literal
30
+
31
+ import numpy as np
32
+ import torch
33
+ from torch import nn
34
+
35
+ from opacus.layers import DPLSTM
36
+
37
+ from mostlyai.engine._common import (
38
+ CTXFLT,
39
+ CTXSEQ,
40
+ get_columns_from_cardinalities,
41
+ get_sub_columns_from_cardinalities,
42
+ get_sub_columns_lookup,
43
+ get_sub_columns_nested_from_cardinalities,
44
+ )
45
+ from mostlyai.engine._tabular.fairness import apply_fairness_transforms
46
+
47
+ _LOG = logging.getLogger(__name__)
48
+
49
+
50
+ class ModelSize(str, Enum):
51
+ S = "S"
52
+ M = "M"
53
+ L = "L"
54
+
55
+
56
+ ModelSizeOrUnits = ModelSize | dict[str, list[int]]
57
+
58
+
59
+ def get_no_of_model_parameters(model) -> int:
60
+ trainable_count = sum([np.prod(p.shape) for p in model.parameters()])
61
+ return trainable_count
62
+
63
+
64
+ def get_model_units(model: nn.Module) -> dict[str, Any]:
65
+ embedder_units = {
66
+ name[name.find("embedder@") :]: module.embedding_dim
67
+ for name, module in model.named_modules()
68
+ if ".embedder@" in name
69
+ }
70
+ column_embedder_units = {
71
+ name[name.find("column_embedder@") :]: module.out_features
72
+ for name, module in model.named_modules()
73
+ if ".column_embedder@" in name and not isinstance(module, nn.Identity) # columns are compressed optionally
74
+ }
75
+ flat_context_units = {
76
+ name[name.find("flat_context@") :]: [layer.out_features for layer in module]
77
+ for name, module in model.named_modules()
78
+ if ".flat_context@" in name
79
+ if isinstance(module, nn.ModuleList)
80
+ }
81
+ sequential_context_units = {
82
+ name[name.find("sequential_context@") :]: [module.hidden_size] * module.num_layers
83
+ for name, module in model.named_modules()
84
+ if ".sequential_context@" in name
85
+ if isinstance(module, nn.LSTM) or isinstance(module, DPLSTM)
86
+ }
87
+ history_units = {
88
+ name[name.find("history@") :]: [module.hidden_size] * module.num_layers
89
+ for name, module in model.named_modules()
90
+ if ".history@" in name
91
+ if isinstance(module, nn.LSTM) or isinstance(module, DPLSTM)
92
+ }
93
+ regressor_units = {
94
+ name[name.find("regressor@") :]: [layer.out_features for layer in module]
95
+ for name, module in model.named_modules()
96
+ if ".regressor@" in name
97
+ if isinstance(module, nn.ModuleList)
98
+ }
99
+ heuristics_dict = (
100
+ embedder_units
101
+ | column_embedder_units
102
+ | flat_context_units
103
+ | sequential_context_units
104
+ | history_units
105
+ | regressor_units
106
+ )
107
+ return heuristics_dict
108
+
109
+
110
+ ####################
111
+ ### HEURISTICS ###
112
+ ####################
113
+
114
+
115
+ def _embedding_heuristic(id: str, model_size: ModelSizeOrUnits, dim_input: int) -> int:
116
+ if isinstance(model_size, dict):
117
+ return model_size[id]
118
+ model_size_output_dim = dict(
119
+ S=max(10, int(2 * np.ceil(dim_input**0.15))),
120
+ M=max(10, int(3 * np.ceil(dim_input**0.25))),
121
+ L=max(10, int(4 * np.ceil(dim_input**0.33))),
122
+ )
123
+ return min(dim_input, model_size_output_dim[model_size])
124
+
125
+
126
+ def _column_embedding_heuristic(
127
+ id: str,
128
+ model_size: ModelSizeOrUnits,
129
+ dim_input: int,
130
+ n_sub_cols: int,
131
+ compress_enabled: bool,
132
+ ) -> int:
133
+ if isinstance(model_size, dict):
134
+ return model_size.get(id, dim_input)
135
+ model_size_output_dim = dict(
136
+ S=int(4 + n_sub_cols),
137
+ M=int(10 + n_sub_cols),
138
+ L=int(16 + n_sub_cols),
139
+ )
140
+ compress = compress_enabled and n_sub_cols > 2
141
+ dim_output = model_size_output_dim[model_size] if compress else dim_input
142
+ # dim_output should always be at most dim_input
143
+ return min(dim_input, dim_output)
144
+
145
+
146
+ def _regressor_heuristic(id: str, model_size: ModelSizeOrUnits, dim_input: int, cardinality: int) -> list[int]:
147
+ if isinstance(model_size, dict):
148
+ return model_size[id]
149
+ model_size_layers = dict(S=[4], M=[16], L=[16, 16])
150
+ dims = [dim_input]
151
+ layers = model_size_layers[model_size]
152
+ # first layers depend on input dimension
153
+ for idx, unit in enumerate(layers[:-1]):
154
+ coefficient = round(np.log(max(dims[idx], np.e)))
155
+ dims.append(unit * coefficient)
156
+ # last layer depends on cardinality
157
+ unit = layers[-1]
158
+ coefficient = round(np.log(max(cardinality, np.e)))
159
+ dims.append(unit * coefficient)
160
+ return dims[1:]
161
+
162
+
163
+ def _flat_context_heuristic(id: str, model_size: ModelSizeOrUnits, dim_input: int) -> list[int]:
164
+ if isinstance(model_size, dict):
165
+ return model_size[id]
166
+ model_size_layers = dict(S=[8], M=[64], L=[128])
167
+ layers = model_size_layers[model_size]
168
+ coefficient = round(np.log(max(dim_input, np.e)))
169
+ dims = [unit * coefficient for unit in layers]
170
+ return dims
171
+
172
+
173
+ def _sequential_context_heuristic(
174
+ id: str, model_size: ModelSizeOrUnits, dim_input: int, seq_len_median: int
175
+ ) -> list[int]:
176
+ if isinstance(model_size, dict):
177
+ return model_size[id]
178
+ model_size_layers = dict(S=[8], M=[32], L=[64, 64])
179
+ layers = model_size_layers[model_size]
180
+ coefficient = round(np.log(max(dim_input * seq_len_median, np.e)))
181
+ dims = [unit * coefficient for unit in layers]
182
+ return dims
183
+
184
+
185
+ def _history_heuristic(id: str, model_size: ModelSizeOrUnits, dim_input: int, seq_len_median: int) -> list[int]:
186
+ if isinstance(model_size, dict):
187
+ return model_size[id]
188
+ model_size_layers = dict(S=[16], M=[64], L=[128, 128])
189
+ layers = model_size_layers[model_size]
190
+ coefficient = round(np.log(max(dim_input * seq_len_median, np.e)))
191
+ dims = [unit * coefficient for unit in layers]
192
+ return dims
193
+
194
+
195
+ #######################
196
+ ### BUILDING BLOCKS ###
197
+ #######################
198
+
199
+
200
+ class Embedders(nn.Module):
201
+ def __init__(
202
+ self,
203
+ model_size: ModelSizeOrUnits,
204
+ cardinalities: dict[str, int],
205
+ device: torch.device,
206
+ ):
207
+ super().__init__()
208
+
209
+ self.model_size = model_size
210
+ self.cardinalities = cardinalities
211
+ self.device = device
212
+
213
+ self.dims = []
214
+
215
+ self.embedders = nn.ModuleDict()
216
+
217
+ # embedding layers for each sub column defined in cardinalities
218
+ for sub_col, dim_input in self.cardinalities.items():
219
+ dim_output = _embedding_heuristic(id=self.id(sub_col), model_size=model_size, dim_input=dim_input)
220
+ embedder = nn.Embedding(num_embeddings=dim_input, embedding_dim=dim_output, device=device)
221
+ self.add(sub_column=sub_col, embedder=embedder)
222
+ self.dims.append(dim_output)
223
+
224
+ def __bool__(self):
225
+ return bool(self.embedders)
226
+
227
+ @staticmethod
228
+ def id(sub_column: str) -> str:
229
+ return f"embedder@{sub_column}"
230
+
231
+ def get(self, sub_column: str) -> nn.Module:
232
+ return self.embedders[self.id(sub_column)]
233
+
234
+ def add(self, sub_column: str, embedder: nn.Module) -> None:
235
+ self.embedders[self.id(sub_column)] = embedder
236
+
237
+ def forward(self, x) -> dict[str, torch.Tensor]:
238
+ # pass through sub column embedders
239
+ embeddings = {}
240
+ for sub_col in self.cardinalities:
241
+ xs = torch.as_tensor(x[sub_col], device=self.device)
242
+ if xs.is_nested: # account for nested tensors
243
+ xs = torch.nested.to_padded_tensor(xs, 0)
244
+ xs = self.get(sub_col)(xs)
245
+ xs = torch.squeeze(xs, -2)
246
+ embeddings[sub_col] = xs
247
+ return embeddings
248
+
249
+ def zero_mask(self, *first_dims: int) -> dict[str, torch.Tensor]:
250
+ # zero mask with shapes like embeddings
251
+ embeddings = {}
252
+ for idx, sub_col in enumerate(self.cardinalities):
253
+ last_dim = (self.dims[idx],)
254
+ shape = first_dims + last_dim
255
+ embeddings[sub_col] = torch.zeros(shape, device=self.device)
256
+ return embeddings
257
+
258
+
259
+ class SequentialContextEmbedders(Embedders):
260
+ def __init__(
261
+ self,
262
+ model_size: ModelSizeOrUnits,
263
+ cardinalities: dict[str, int],
264
+ device: torch.device,
265
+ ):
266
+ super().__init__(model_size, cardinalities, device)
267
+
268
+ def forward(self, x) -> tuple[dict[str, torch.Tensor], torch.Tensor]:
269
+ # pass through sub column embedders
270
+ embeddings = {}
271
+ mask = None
272
+ for sub_col in self.cardinalities:
273
+ xs = torch.as_tensor(x[sub_col], device=self.device)
274
+ xs = torch.nested.to_padded_tensor(xs, padding=-1)
275
+ mask = (xs != -1).squeeze(-1)
276
+ xs = torch.where(xs == -1, torch.tensor(0), xs)
277
+ xs = self.get(sub_col)(xs)
278
+ xs = torch.squeeze(xs, -2)
279
+ embeddings[sub_col] = xs
280
+ assert mask is not None
281
+ return embeddings, mask
282
+
283
+
284
+ class ColumnEmbedders(nn.Module):
285
+ def __init__(
286
+ self,
287
+ model_size: ModelSizeOrUnits,
288
+ cardinalities: dict[str, int],
289
+ embedding_dims: list[int],
290
+ device: torch.device,
291
+ ):
292
+ super().__init__()
293
+
294
+ self.model_size = model_size
295
+ self.cardinalities = cardinalities
296
+ self.embedding_dims = embedding_dims
297
+ self.device = device
298
+
299
+ self.column_sub_columns = get_sub_columns_nested_from_cardinalities(cardinalities, groupby="columns")
300
+
301
+ self.dims = []
302
+
303
+ self.column_embedders = nn.ModuleDict()
304
+
305
+ # column embeddings for each column defined in cardinalities
306
+ # only consider adding compressor layer if there are more than 50 sub columns
307
+ compressor_enabled = len(self.cardinalities) > 50
308
+ offset = 0
309
+ for column, sub_cols in self.column_sub_columns.items():
310
+ n_sub_cols = len(sub_cols)
311
+ sub_cols_dims = self.embedding_dims[offset : offset + n_sub_cols]
312
+ dim_input = sum(sub_cols_dims)
313
+ dim_output = _column_embedding_heuristic(
314
+ id=self.id(column),
315
+ model_size=self.model_size,
316
+ dim_input=dim_input,
317
+ n_sub_cols=n_sub_cols,
318
+ compress_enabled=compressor_enabled,
319
+ )
320
+ column_embedder = (
321
+ nn.Linear(in_features=dim_input, out_features=dim_output, device=self.device)
322
+ # apply compression only if dim_output < dim_input
323
+ if dim_output < dim_input
324
+ else nn.Identity()
325
+ )
326
+ self.add(column=column, column_embedder=column_embedder)
327
+ self.dims.append(dim_output)
328
+ offset += n_sub_cols
329
+
330
+ @staticmethod
331
+ def id(column: str) -> str:
332
+ return f"column_embedder@{column}"
333
+
334
+ def get(self, column: str) -> nn.Module:
335
+ return self.column_embedders[self.id(column)]
336
+
337
+ def add(self, column: str, column_embedder: nn.Module) -> None:
338
+ self.column_embedders[self.id(column)] = column_embedder
339
+
340
+ def forward(self, x) -> dict[str, torch.Tensor]:
341
+ # pass through column embedders
342
+ column_embeddings = {}
343
+ for column, sub_cols in self.column_sub_columns.items():
344
+ sub_cols_embeddings = torch.cat([x[sub_col] for sub_col in sub_cols], dim=-1)
345
+ column_embeddings[column] = self.get(column)(sub_cols_embeddings)
346
+
347
+ return column_embeddings
348
+
349
+ def zero_mask(self, *first_dims: int) -> dict[str, torch.Tensor]:
350
+ # zero mask with shapes like column embeddings
351
+ column_embeddings = {}
352
+ for idx, column in enumerate(self.column_sub_columns):
353
+ last_dim = (self.dims[idx],)
354
+ shape = first_dims + last_dim
355
+ column_embeddings[column] = torch.zeros(shape, device=self.device)
356
+ return column_embeddings
357
+
358
+
359
+ class FlatContextCompressor(nn.Module):
360
+ def __init__(
361
+ self,
362
+ model_size: ModelSizeOrUnits,
363
+ ctxflt_cardinalities: dict[str, int],
364
+ device: torch.device,
365
+ ):
366
+ super().__init__()
367
+
368
+ self.model_size = model_size
369
+ self.ctxflt_cardinalities = ctxflt_cardinalities
370
+ self.device = device
371
+
372
+ self.compressor_layers = nn.ModuleDict()
373
+ self.dropout = nn.Dropout(p=0.25)
374
+
375
+ # flat context embedding layers
376
+ self.embedders = Embedders(
377
+ model_size=self.model_size,
378
+ cardinalities=self.ctxflt_cardinalities,
379
+ device=self.device,
380
+ )
381
+
382
+ # flat context compressor layers
383
+ self.dim_output = 0
384
+ if self.embedders:
385
+ dim_input = sum(self.embedders.dims)
386
+ dims = _flat_context_heuristic(id=self.id(), model_size=model_size, dim_input=dim_input)
387
+ compressor_layers = nn.ModuleList()
388
+ for dim_in, dim_out in zip([dim_input] + dims[:-1], dims):
389
+ compressor_layers.append(nn.Linear(in_features=dim_in, out_features=dim_out, device=device))
390
+ self.set(compressor_layers)
391
+ self.dim_output = dims[-1]
392
+
393
+ @staticmethod
394
+ def id() -> str:
395
+ return "flat_context@"
396
+
397
+ def get(self) -> nn.ModuleList:
398
+ return self.compressor_layers[self.id()]
399
+
400
+ def set(self, compressor: nn.ModuleList) -> None:
401
+ self.compressor_layers[self.id()] = compressor
402
+
403
+ def forward(self, x) -> list[torch.Tensor]:
404
+ # pass through flat context embeddings
405
+ embeddings = self.embedders(x)
406
+
407
+ # pass through multi-layer flat context compressor
408
+ flat_context = []
409
+ if embeddings:
410
+ xs = torch.cat(list(embeddings.values()), dim=-1)
411
+ for compressor_layer in self.get():
412
+ xs = compressor_layer(xs)
413
+ xs = self.dropout(xs)
414
+ flat_context = [xs]
415
+ return flat_context
416
+
417
+
418
+ class SequentialContextCompressor(nn.Module):
419
+ def __init__(
420
+ self,
421
+ model_size: ModelSizeOrUnits,
422
+ ctxseq_cardinalities: dict[str, int],
423
+ ctxseq_len_median: dict[str, int],
424
+ device: torch.device,
425
+ with_dp: bool = False,
426
+ ):
427
+ super().__init__()
428
+
429
+ self.model_size = model_size
430
+ self.ctxseq_cardinalities = ctxseq_cardinalities
431
+ self.ctxseq_len_median = ctxseq_len_median
432
+ self.device = device
433
+ dropout_rate = 0.25
434
+
435
+ self.ctxseq_table_sub_columns = get_sub_columns_nested_from_cardinalities(
436
+ self.ctxseq_cardinalities, groupby="tables"
437
+ )
438
+
439
+ self.embedders = nn.ModuleDict()
440
+ self.compressor_layers = nn.ModuleDict()
441
+ self.dropout = nn.Dropout(p=dropout_rate)
442
+
443
+ self.dim_outputs = []
444
+ for table, sub_columns in self.ctxseq_table_sub_columns.items():
445
+ # sequential context table embedding layers
446
+ table_cardinalities = {
447
+ sub_col: card for sub_col, card in self.ctxseq_cardinalities.items() if sub_col in sub_columns
448
+ }
449
+ table_embedders = SequentialContextEmbedders(
450
+ model_size=self.model_size,
451
+ cardinalities=table_cardinalities,
452
+ device=self.device,
453
+ )
454
+ self.embedders[table] = table_embedders
455
+
456
+ # sequential context table compressor layers
457
+ dim_input = sum(table_embedders.dims)
458
+ dims = _sequential_context_heuristic(
459
+ id=self.id(table),
460
+ model_size=self.model_size,
461
+ dim_input=dim_input,
462
+ seq_len_median=ctxseq_len_median.get(table, 1),
463
+ )
464
+ hidden_size = dims[-1]
465
+ num_layers = len(dims)
466
+ bidirectional = True
467
+ lstm_cls = DPLSTM if with_dp else partial(nn.LSTM, device=self.device)
468
+ table_compressor_layer = lstm_cls(
469
+ input_size=dim_input,
470
+ hidden_size=hidden_size,
471
+ num_layers=num_layers,
472
+ dropout=dropout_rate if num_layers > 1 else 0.0,
473
+ batch_first=True,
474
+ bidirectional=bidirectional,
475
+ )
476
+ self.add(table=table, compressor_layer=table_compressor_layer)
477
+ self.dim_outputs.append(hidden_size * (2 if bidirectional else 1))
478
+
479
+ @staticmethod
480
+ def id(table: str) -> str:
481
+ return f"sequential_context@{table}"
482
+
483
+ def get(self, table: str) -> nn.Module:
484
+ return self.compressor_layers[self.id(table)]
485
+
486
+ def add(self, table: str, compressor_layer: nn.Module) -> None:
487
+ self.compressor_layers[self.id(table)] = compressor_layer
488
+
489
+ def forward(self, x) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
490
+ sequential_contexts = []
491
+ sequential_context_masks = []
492
+ for table, _ in self.ctxseq_table_sub_columns.items():
493
+ # pass through sequential context table embeddings
494
+ table_embeddings, mask = self.embedders[table](x)
495
+ sequential_context_masks.append(mask)
496
+ # pass through multi-layer sequential context table compressor
497
+ xs = torch.cat(list(table_embeddings.values()), dim=-1)
498
+ compressor_layer = self.get(table)
499
+ xs = self.dropout(xs)
500
+ lengths = torch.sum(mask, dim=-1)
501
+ packed_xs = nn.utils.rnn.pack_padded_sequence(xs, lengths.cpu(), batch_first=True, enforce_sorted=False)
502
+ packed_xs, _ = compressor_layer(packed_xs)
503
+ xs, _ = nn.utils.rnn.pad_packed_sequence(packed_xs, batch_first=True)
504
+
505
+ # we return the full sequence of hidden states
506
+ sequential_contexts.append(xs)
507
+
508
+ return sequential_contexts, sequential_context_masks
509
+
510
+
511
+ class ContextCompressor(nn.Module):
512
+ def __init__(
513
+ self,
514
+ model_size: ModelSizeOrUnits,
515
+ ctx_cardinalities: dict[str, int],
516
+ ctxseq_len_median: dict[str, int],
517
+ device: torch.device,
518
+ with_dp: bool = False,
519
+ ):
520
+ super().__init__()
521
+ self.model_size = model_size
522
+ self.ctxflt_cardinalities = {k: v for k, v in ctx_cardinalities.items() if k.startswith(CTXFLT)}
523
+ self.ctxseq_cardinalities = {k: v for k, v in ctx_cardinalities.items() if k.startswith(CTXSEQ)}
524
+ self.ctxseq_len_median = ctxseq_len_median
525
+ self.device = device
526
+
527
+ # flat context
528
+ self.flat_context_compressor = FlatContextCompressor(
529
+ model_size=self.model_size,
530
+ ctxflt_cardinalities=self.ctxflt_cardinalities,
531
+ device=device,
532
+ )
533
+
534
+ # sequential context(s)
535
+ self.sequential_context_compressor = SequentialContextCompressor(
536
+ model_size=self.model_size,
537
+ ctxseq_cardinalities=self.ctxseq_cardinalities,
538
+ ctxseq_len_median=self.ctxseq_len_median,
539
+ device=device,
540
+ with_dp=with_dp,
541
+ )
542
+
543
+ # size of context output
544
+ self.dim_output = self.flat_context_compressor.dim_output + sum(self.sequential_context_compressor.dim_outputs)
545
+
546
+ def forward(self, x) -> tuple[list[torch.Tensor], list[torch.Tensor], torch.Tensor]:
547
+ flat_context = self.flat_context_compressor(x)
548
+ sequential_contexts, sequential_context_masks = self.sequential_context_compressor(x)
549
+ return flat_context, sequential_contexts, sequential_context_masks
550
+
551
+
552
+ class HistoryCompressor(nn.Module):
553
+ def __init__(
554
+ self,
555
+ model_size: ModelSizeOrUnits,
556
+ embedding_dims: list[int],
557
+ seq_len_median: int,
558
+ device: torch.device,
559
+ with_dp: bool = False,
560
+ ):
561
+ super().__init__()
562
+
563
+ self.model_size = model_size
564
+ self.embedding_dims = embedding_dims
565
+ self.seq_len_median = seq_len_median
566
+ self.device = device
567
+ dropout_rate = 0.25
568
+
569
+ self.compressor_layers = nn.ModuleDict()
570
+ self.dropout = nn.Dropout(p=dropout_rate)
571
+
572
+ dim_input = sum(embedding_dims)
573
+ dims = _history_heuristic(
574
+ id=self.id(),
575
+ model_size=self.model_size,
576
+ dim_input=dim_input,
577
+ seq_len_median=self.seq_len_median,
578
+ )
579
+
580
+ hidden_size = dims[-1]
581
+ num_layers = len(dims)
582
+ lstm_cls = DPLSTM if with_dp else nn.LSTM
583
+ compressor_layer = lstm_cls(
584
+ input_size=dim_input,
585
+ hidden_size=hidden_size,
586
+ num_layers=num_layers,
587
+ dropout=dropout_rate if num_layers > 1 else 0.0,
588
+ batch_first=True,
589
+ )
590
+ self.set(compressor_layer)
591
+ self.dim_output = hidden_size
592
+
593
+ @staticmethod
594
+ def id() -> str:
595
+ return "history@"
596
+
597
+ def get(self) -> nn.Module:
598
+ return self.compressor_layers[self.id()]
599
+
600
+ def set(self, compressor_layer: nn.Module) -> None:
601
+ self.compressor_layers[self.id()] = compressor_layer
602
+
603
+ def forward(self, x, history_state=None) -> tuple[torch.Tensor, torch.Tensor]:
604
+ compressor_layer = self.get()
605
+ x = self.dropout(x)
606
+ x, history_state = compressor_layer(x, history_state)
607
+ return x, history_state
608
+
609
+
610
+ class Regressors(nn.Module):
611
+ def __init__(
612
+ self,
613
+ *,
614
+ model_size: ModelSizeOrUnits,
615
+ cardinalities: dict[str, int],
616
+ context_dim: int,
617
+ history_dim: int | None = None,
618
+ embedding_dims: list[int],
619
+ column_embedding_dims: list[int],
620
+ device: torch.device,
621
+ ):
622
+ super().__init__()
623
+
624
+ self.model_size = model_size
625
+ self.cardinalities = cardinalities
626
+ self.context_dim = context_dim
627
+ self.history_dim = history_dim
628
+ self.column_embedding_dims = column_embedding_dims
629
+ self.embedding_dims = embedding_dims
630
+ self.device = device
631
+
632
+ self.column_sub_columns = get_sub_columns_nested_from_cardinalities(self.cardinalities, groupby="columns")
633
+ self.sub_columns_lookup = get_sub_columns_lookup(self.column_sub_columns)
634
+
635
+ self.dims_output = {}
636
+
637
+ self.regressors = nn.ModuleDict()
638
+ self.dropout = nn.Dropout(p=0.25)
639
+
640
+ for sub_column, lookup in self.sub_columns_lookup.items():
641
+ # collect previous sub column embedding dims for current column
642
+ prev_embedding_dims = self.embedding_dims[lookup.sub_col_offset : lookup.sub_col_cum]
643
+
644
+ dim_input = (
645
+ self.context_dim
646
+ + (self.history_dim or 0) # history is not present in FlatModel
647
+ + sum(self.column_embedding_dims)
648
+ + sum(prev_embedding_dims)
649
+ )
650
+
651
+ # multi-layer sub column regressor
652
+ dims = _regressor_heuristic(
653
+ id=self.id(sub_column),
654
+ model_size=self.model_size,
655
+ dim_input=dim_input,
656
+ cardinality=self.cardinalities[sub_column],
657
+ )
658
+ regressor_layers = nn.ModuleList()
659
+ for dim_in, dim_out in zip([dim_input] + dims[:-1], dims):
660
+ regressor_layers.append(nn.Linear(in_features=dim_in, out_features=dim_out, device=self.device))
661
+ self.add(sub_column=sub_column, regressor=regressor_layers)
662
+ self.dims_output[sub_column] = dims[-1]
663
+
664
+ @staticmethod
665
+ def id(sub_column: str) -> str:
666
+ return f"regressor@{sub_column}"
667
+
668
+ def get(self, sub_column: str) -> nn.ModuleList:
669
+ return self.regressors[self.id(sub_column)]
670
+
671
+ def add(self, sub_column: str, regressor: nn.ModuleList) -> None:
672
+ self.regressors[self.id(sub_column)] = regressor
673
+
674
+ def forward(self, regressor_in: list[torch.Tensor], sub_col: str) -> torch.Tensor:
675
+ x = torch.cat(regressor_in, dim=-1)
676
+ # pass through multi-layer sub column regressor
677
+ for regressor_layer in self.get(sub_col):
678
+ x = self.dropout(x)
679
+ x = regressor_layer(x)
680
+ x = nn.ReLU()(x)
681
+ return x
682
+
683
+
684
+ class Predictors(nn.Module):
685
+ def __init__(
686
+ self,
687
+ cardinalities: dict[str, int],
688
+ regressors_dims: dict[str, int],
689
+ device: torch.device,
690
+ ):
691
+ super().__init__()
692
+
693
+ self.cardinalities = cardinalities
694
+ self.regressors_dims = regressors_dims
695
+ self.device = device
696
+
697
+ self.predictors = nn.ModuleDict()
698
+
699
+ for sub_col, dim_output in self.cardinalities.items():
700
+ dim_input = self.regressors_dims[sub_col]
701
+ self.predictors[sub_col] = nn.Linear(in_features=dim_input, out_features=dim_output, device=self.device)
702
+
703
+ def forward(self, x: torch.Tensor, sub_col: str) -> torch.Tensor:
704
+ return self.predictors[sub_col](x)
705
+
706
+
707
+ def _make_permutation_mask(
708
+ col_embedding_dims: list[int],
709
+ columns: list[str],
710
+ column_order: list[str] | None,
711
+ is_sequential: bool,
712
+ device: torch.device,
713
+ ) -> torch.Tensor:
714
+ n_cols = len(columns)
715
+ if column_order is not None:
716
+ # create mask in provided order
717
+ order = torch.tensor([columns.index(c) for c in column_order], dtype=torch.int32)
718
+ elif is_sequential and n_cols >= 1:
719
+ # create mask in random order, but keep SLEN/SIDX/SDEC column at first position
720
+ order = torch.randperm(n_cols - 1) + 1
721
+ order = torch.cat((torch.zeros(1, dtype=torch.int32), order), dim=0)
722
+ else:
723
+ # create mask in random order
724
+ order = torch.randperm(n_cols)
725
+
726
+ # convert order into a binary mask consisting of 0s and 1s
727
+ idx = np.argsort(order)
728
+ ones = torch.ones(n_cols, n_cols, dtype=torch.int32, device=device)
729
+ mask = torch.tril(ones, diagonal=-1) # strict lower triangular matrix
730
+ mask = mask[idx, :][:, idx].bool() # re-order rows and columns
731
+ reps = torch.as_tensor(col_embedding_dims, dtype=torch.int32, device=device)
732
+ mask = torch.repeat_interleave(mask, repeats=reps, dim=1) # expand columns
733
+ return mask
734
+
735
+
736
+ def _sampling_temperature(probs: torch.Tensor, temperature: float) -> torch.Tensor:
737
+ temperature = torch.max(torch.tensor(temperature, device=probs.device), torch.tensor(1e-3, device=probs.device))
738
+ # compute softmax with temperature scaling
739
+ scaled_logits = torch.divide(torch.log(probs), temperature)
740
+ probs = torch.nn.functional.softmax(scaled_logits, dim=-1)
741
+ return probs
742
+
743
+
744
+ def _sampling_nucleus(probs: torch.Tensor, top_p: float) -> torch.Tensor:
745
+ """
746
+ Implements nucleus sampling (https://arxiv.org/abs/1904.09751)
747
+ Parameters:
748
+ probs: probabilities of tokens to sample from
749
+ top_p: cumulative probability of top p tokens to sample from
750
+ """
751
+ # sort probabilities in descending order and get their indices
752
+ sorted_indices = torch.argsort(input=probs, descending=True, dim=-1)
753
+ sorted_probs = torch.gather(input=probs, dim=-1, index=sorted_indices)
754
+
755
+ # compute the cumulative sum of the sorted probabilities
756
+ cumulative_probs = torch.cumsum(input=sorted_probs, dim=-1)
757
+
758
+ # remove tokens whose cumulative probability exceeds the threshold
759
+ sorted_indices_to_remove = torch.greater(cumulative_probs, top_p)
760
+
761
+ # shift the indices to the right to keep the first token above the threshold
762
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
763
+ # we need at least one token to sample from
764
+ sorted_indices_to_remove[..., 0] = False
765
+
766
+ # scatter sorted tensors to original indexing
767
+ _, reversed_indices = torch.sort(input=sorted_indices, dim=-1)
768
+ indices_to_remove = torch.gather(input=sorted_indices_to_remove, dim=-1, index=reversed_indices)
769
+
770
+ # zero out probabilities that need to be removed
771
+ probs = probs.masked_fill(mask=indices_to_remove, value=0.0)
772
+
773
+ # re-normalize to [0, 1]
774
+ # if all probs are zero (first sorted / biggest is zero), fallback to uniform sampling
775
+ sums = torch.sum(probs, dim=-1, keepdim=True)
776
+ probs = torch.where(sums > 0.0, probs / sums, torch.ones_like(probs) / probs.size(-1))
777
+
778
+ return probs
779
+
780
+
781
+ def _sampling_fixed_probs(probs: torch.Tensor, fixed_probs: dict[int, float]) -> torch.Tensor:
782
+ # adding small epsilon avoids edge case where non-fixed values are all zero
783
+ probs = probs + 1.0e-20
784
+
785
+ # identify fixed and non-fixed indices
786
+ fixed_indices = torch.as_tensor(list(fixed_probs.keys()), device=probs.device)
787
+ fixed_values = torch.as_tensor(list(fixed_probs.values()), device=probs.device)
788
+ fixed_mask = torch.zeros(probs.size(-1), dtype=torch.bool, device=probs.device)
789
+ fixed_mask[fixed_indices] = True
790
+ non_fixed_mask = ~fixed_mask
791
+
792
+ # handle edge case: all indices are fixed to 0.0
793
+ if fixed_mask.all() and fixed_values.sum() == 0.0:
794
+ probs[:] = 1.0 / probs.size(-1)
795
+ return probs
796
+
797
+ # overwrite probs under fixed indices
798
+ probs[..., fixed_indices] = fixed_values.unsqueeze(0)
799
+
800
+ # normalize non-fixed indices
801
+ remaining_prob = torch.clamp(1.0 - fixed_values.sum(), min=0.0)
802
+ non_fixed_sum = torch.sum(probs[..., non_fixed_mask], dim=-1, keepdim=True)
803
+ probs[..., non_fixed_mask] = probs[..., non_fixed_mask] * (remaining_prob / non_fixed_sum)
804
+
805
+ return probs
806
+
807
+
808
+ def _sample(
809
+ probs: torch.Tensor,
810
+ temperature: float | None = None,
811
+ top_p: float | None = None,
812
+ fixed_probs: dict[int, float] | None = None,
813
+ ) -> torch.Tensor:
814
+ if temperature is not None and temperature != 1.0:
815
+ probs = _sampling_temperature(probs, temperature)
816
+
817
+ if top_p is not None and top_p < 1.0:
818
+ probs = _sampling_nucleus(probs, top_p)
819
+
820
+ if fixed_probs is not None:
821
+ probs = _sampling_fixed_probs(probs, fixed_probs)
822
+
823
+ # ensure that probabilities are valid
824
+ # log cases where this isn't the case to help understand when this can happen
825
+ is_nan = torch.isnan(probs)
826
+ is_neg = probs < 0.0
827
+ is_gt1 = probs > 1.0
828
+ if is_nan.any():
829
+ nan_count = is_nan.sum().item()
830
+ _LOG.warning(f"[ARGN] Set {nan_count} probabilities from NaN to 1 in {probs.shape}")
831
+ probs = torch.nan_to_num(probs, nan=1.0)
832
+ if is_neg.any():
833
+ neg_count = is_neg.sum().item()
834
+ _LOG.warning(f"[ARGN] Clip {neg_count} probabilities below 0 in {probs.shape}")
835
+ probs = torch.clamp(probs, min=0.0)
836
+ if is_gt1.any():
837
+ gt1_count = is_gt1.sum().item()
838
+ _LOG.warning(f"[ARGN] Clip {gt1_count} probabilities above 1 in {probs.shape}")
839
+ probs = torch.clamp(probs, max=1.0)
840
+
841
+ probs = torch.multinomial(probs, num_samples=1, replacement=True)
842
+ return probs
843
+
844
+
845
+ ##########################
846
+ ### TabularARGN models ###
847
+ ##########################
848
+
849
+
850
+ class FlatModel(nn.Module):
851
+ def __init__(
852
+ self,
853
+ tgt_cardinalities: dict[str, int],
854
+ ctx_cardinalities: dict[str, int],
855
+ ctxseq_len_median: dict[str, int],
856
+ model_size: ModelSizeOrUnits,
857
+ column_order: list[str] | None,
858
+ device: torch.device,
859
+ ):
860
+ super().__init__()
861
+
862
+ self.tgt_cardinalities = tgt_cardinalities
863
+ self.tgt_sub_columns = get_sub_columns_from_cardinalities(tgt_cardinalities)
864
+ self.tgt_columns = get_columns_from_cardinalities(tgt_cardinalities)
865
+ self.tgt_column_sub_columns = get_sub_columns_nested_from_cardinalities(tgt_cardinalities, groupby="columns")
866
+ self.last_sub_cols = [v[-1] for v in self.tgt_column_sub_columns.values()]
867
+ self.tgt_sub_columns_lookup = get_sub_columns_lookup(self.tgt_column_sub_columns)
868
+ self.ctx_cardinalities = ctx_cardinalities
869
+ self.ctxseq_len_median = ctxseq_len_median
870
+ self.model_size = model_size
871
+ self.column_order = column_order
872
+ self.device = device or torch.device("cpu")
873
+
874
+ # context
875
+ self.context_compressor = ContextCompressor(
876
+ model_size=self.model_size,
877
+ ctx_cardinalities=self.ctx_cardinalities,
878
+ ctxseq_len_median=self.ctxseq_len_median,
879
+ device=device,
880
+ )
881
+
882
+ # sub column embeddings
883
+ self.embedders = Embedders(
884
+ model_size=self.model_size,
885
+ cardinalities=self.tgt_cardinalities,
886
+ device=self.device,
887
+ )
888
+
889
+ # column embeddings
890
+ self.column_embedders = ColumnEmbedders(
891
+ model_size=self.model_size,
892
+ cardinalities=self.tgt_cardinalities,
893
+ embedding_dims=self.embedders.dims,
894
+ device=self.device,
895
+ )
896
+
897
+ # regressors
898
+ self.regressors = Regressors(
899
+ model_size=self.model_size,
900
+ cardinalities=self.tgt_cardinalities,
901
+ context_dim=self.context_compressor.dim_output,
902
+ embedding_dims=self.embedders.dims,
903
+ column_embedding_dims=self.column_embedders.dims,
904
+ device=self.device,
905
+ )
906
+
907
+ # predictors
908
+ self.predictors = Predictors(
909
+ cardinalities=self.tgt_cardinalities,
910
+ regressors_dims=self.regressors.dims_output,
911
+ device=device,
912
+ )
913
+
914
+ def _handle_context(
915
+ self, context: tuple[torch.Tensor, list[torch.Tensor], list[torch.Tensor]]
916
+ ) -> list[torch.Tensor]:
917
+ flat_context, sequential_contexts, sequential_context_masks = context
918
+
919
+ last_state_seq_ctxs = []
920
+ for seq_ctx, mask in zip(sequential_contexts, sequential_context_masks):
921
+ lengths = torch.sum(mask, dim=1)
922
+
923
+ hidden_dim = seq_ctx.size(-1) // 2
924
+ forward_states = seq_ctx[..., :hidden_dim]
925
+ backward_states = seq_ctx[..., hidden_dim:]
926
+
927
+ # Take last valid step from forward direction and first valid step from backward direction
928
+ forward_final = torch.stack([forward_states[i, length - 1 : length, :] for i, length in enumerate(lengths)])
929
+ backward_final = backward_states[:, 0:1, :] # first timestep is last state of backward direction
930
+
931
+ xs = torch.cat([forward_final, backward_final], dim=-1)
932
+ xs = torch.squeeze(xs, dim=1)
933
+ last_state_seq_ctxs.append(xs)
934
+
935
+ context = flat_context + last_state_seq_ctxs
936
+
937
+ if len(context) > 0:
938
+ context = [torch.cat(context, dim=-1)]
939
+
940
+ return context
941
+
942
+ def forward(
943
+ self,
944
+ x,
945
+ mode: Literal["trn", "gen"],
946
+ batch_size: int | None = None,
947
+ fixed_probs=None,
948
+ fixed_values=None,
949
+ temperature: float | None = None,
950
+ top_p: float | None = None,
951
+ return_probs: list[str] | None = None,
952
+ fairness_transforms: dict[str, Any] | None = None,
953
+ ) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]:
954
+ fixed_probs = fixed_probs or {}
955
+ fixed_values = fixed_values or {}
956
+ return_probs = return_probs or []
957
+ outputs = {}
958
+ probs = {}
959
+ fairness_transforms = fairness_transforms or {}
960
+
961
+ if mode == "trn":
962
+ # forward pass through context compressor
963
+ context = self.context_compressor(x)
964
+ context = self._handle_context(context)
965
+
966
+ # forward pass through sub column embedders
967
+ tgt_embeds = self.embedders(x)
968
+
969
+ # forward pass through column embedders
970
+ tgt_col_embeds = self.column_embedders(tgt_embeds)
971
+
972
+ # create batch-wise permutation mask
973
+ col_embeddings = torch.cat(list(tgt_col_embeds.values()), dim=-1)
974
+ col_mask = _make_permutation_mask(
975
+ col_embedding_dims=self.column_embedders.dims,
976
+ columns=self.tgt_columns,
977
+ column_order=self.column_order,
978
+ is_sequential=False,
979
+ device=self.device,
980
+ )
981
+
982
+ for sub_col, lookup in self.tgt_sub_columns_lookup.items():
983
+ # mask concatenated column embeddings
984
+ masked_col_embeds = [torch.mul(col_mask[lookup.col_idx, :].int(), col_embeddings)]
985
+
986
+ # collect previous sub column embeddings for current column
987
+ prev_sub_col_embeds = [
988
+ tgt_embeds[sub_col] for sub_col in self.tgt_sub_columns[lookup.sub_col_offset : lookup.sub_col_cum]
989
+ ]
990
+
991
+ # regressor
992
+ regressor_in = context + masked_col_embeds + prev_sub_col_embeds
993
+ xs = self.regressors(regressor_in, sub_col)
994
+
995
+ # predictor
996
+ xs = self.predictors(xs, sub_col)
997
+
998
+ # update output
999
+ outputs[sub_col] = xs
1000
+
1001
+ else: # mode == "gen"
1002
+ # forward pass through context compressor
1003
+ context = self.context_compressor(x)
1004
+ context = self._handle_context(context)
1005
+
1006
+ # initialize sub column embeddings
1007
+ tgt_embeds = self.embedders.zero_mask(batch_size)
1008
+
1009
+ # initialize column embeddings
1010
+ tgt_col_embeds = self.column_embedders.zero_mask(batch_size)
1011
+
1012
+ # concatenate column embeddings
1013
+ col_embeddings = torch.cat(list(tgt_col_embeds.values()), dim=-1)
1014
+
1015
+ # take sub columns in the specified generation order
1016
+ column_order = self.column_order or self.tgt_columns
1017
+ sub_column_order = [sub_col for col in column_order for sub_col in self.tgt_column_sub_columns[col]]
1018
+
1019
+ for sub_col in sub_column_order:
1020
+ lookup = self.tgt_sub_columns_lookup[sub_col]
1021
+
1022
+ # if sub column is fixed, skip sampling and use that value
1023
+ if sub_col in fixed_values:
1024
+ out = fixed_values[sub_col]
1025
+
1026
+ else: # sample from distribution
1027
+ # collect previous sub column embeddings for current column
1028
+ prev_sub_col_embeds = [
1029
+ tgt_embeds[sub_col]
1030
+ for sub_col in self.tgt_sub_columns[lookup.sub_col_offset : lookup.sub_col_cum]
1031
+ ]
1032
+
1033
+ # regressor
1034
+ regressor_in = context + [col_embeddings] + prev_sub_col_embeds
1035
+ xs = self.regressors(regressor_in, sub_col)
1036
+
1037
+ # predictor
1038
+ xs = self.predictors(xs, sub_col)
1039
+
1040
+ # softmax to probs
1041
+ xs = nn.Softmax(dim=-1)(xs)
1042
+
1043
+ # keep probabilities (used e.g. for fairness)
1044
+ if sub_col in return_probs:
1045
+ probs[sub_col] = xs
1046
+
1047
+ # apply fairness transform when generating the target sub column
1048
+ if fairness_transforms:
1049
+ xs = apply_fairness_transforms(sub_col, xs, outputs, fairness_transforms)
1050
+
1051
+ # sample
1052
+ out = torch.squeeze(
1053
+ _sample(
1054
+ probs=xs,
1055
+ temperature=temperature,
1056
+ top_p=top_p,
1057
+ fixed_probs=fixed_probs.get(sub_col),
1058
+ ),
1059
+ dim=-1,
1060
+ )
1061
+
1062
+ # update output
1063
+ outputs[sub_col] = out
1064
+
1065
+ # update current sub column embedding
1066
+ tgt_embeds[sub_col] = self.embedders.get(sub_col)(out)
1067
+
1068
+ # update current column embedding
1069
+ if sub_col in self.last_sub_cols:
1070
+ col_sub_cols = self.tgt_column_sub_columns[lookup.col_name]
1071
+ col_embed_in = torch.cat([tgt_embeds[sc] for sc in col_sub_cols], dim=-1)
1072
+ tgt_col_embeds[lookup.col_name] = self.column_embedders.get(lookup.col_name)(col_embed_in)
1073
+ col_embeddings = torch.cat(list(tgt_col_embeds.values()), dim=-1)
1074
+
1075
+ # order outputs according to tgt_sub_columns
1076
+ outputs = {sub_col: outputs[sub_col] for sub_col in self.tgt_sub_columns}
1077
+
1078
+ return outputs, probs
1079
+
1080
+
1081
+ class AttentionModule(nn.Module):
1082
+ def __init__(self, dim_input: int, dim_outputs: list[int], device: torch.device):
1083
+ super().__init__()
1084
+ self.dim_input = dim_input
1085
+ self.dim_outputs = dim_outputs
1086
+ self.device = device
1087
+
1088
+ # when using the attention in SCP we need to project the input to have the same dim as the
1089
+ # context compressor so that we can compute inner products between the two
1090
+ self.q_projs = nn.ModuleList(
1091
+ [
1092
+ nn.Linear(
1093
+ self.dim_input,
1094
+ dim_output,
1095
+ bias=False,
1096
+ device=self.device,
1097
+ )
1098
+ for dim_output in self.dim_outputs
1099
+ ]
1100
+ )
1101
+
1102
+ def forward(
1103
+ self,
1104
+ history: torch.Tensor,
1105
+ seq_ctxs: list[torch.Tensor],
1106
+ seq_ctx_masks: list[torch.Tensor],
1107
+ ) -> list[torch.Tensor]:
1108
+ attn_ctxs = []
1109
+ batch_size, history_len, _ = history.shape
1110
+
1111
+ for seq_ctx, mask, q_proj in zip(seq_ctxs, seq_ctx_masks, self.q_projs):
1112
+ _, ctx_len = mask.shape
1113
+ mask = mask.unsqueeze(1).expand(batch_size, history_len, ctx_len)
1114
+ query = q_proj(history)
1115
+ attn_ctx = torch.nn.functional.scaled_dot_product_attention(
1116
+ query=query,
1117
+ key=seq_ctx,
1118
+ value=seq_ctx,
1119
+ attn_mask=mask,
1120
+ dropout_p=0.0,
1121
+ is_causal=False,
1122
+ ) # (batch_size, history_len, hidden_dim)
1123
+ attn_ctxs.append(attn_ctx)
1124
+ if attn_ctxs:
1125
+ seq_ctxs = [torch.cat(attn_ctxs, dim=-1)]
1126
+ return seq_ctxs
1127
+
1128
+
1129
+ class SequentialModel(nn.Module):
1130
+ def __init__(
1131
+ self,
1132
+ tgt_cardinalities: dict[str, int],
1133
+ tgt_seq_len_median: int,
1134
+ tgt_seq_len_max: int,
1135
+ ctx_cardinalities: dict[str, int],
1136
+ ctxseq_len_median: dict[str, int],
1137
+ model_size: ModelSizeOrUnits,
1138
+ column_order: list[str] | None,
1139
+ device: torch.device,
1140
+ with_dp: bool = False,
1141
+ ):
1142
+ super().__init__()
1143
+
1144
+ self.tgt_cardinalities = tgt_cardinalities
1145
+ self.tgt_sub_columns = get_sub_columns_from_cardinalities(tgt_cardinalities)
1146
+ self.tgt_columns = get_columns_from_cardinalities(tgt_cardinalities)
1147
+ self.tgt_column_sub_columns = get_sub_columns_nested_from_cardinalities(tgt_cardinalities, groupby="columns")
1148
+ self.tgt_sub_columns_lookup = get_sub_columns_lookup(self.tgt_column_sub_columns)
1149
+ self.tgt_seq_len_median = tgt_seq_len_median
1150
+ self.tgt_last_sub_cols = [sub_cols[-1] for sub_cols in self.tgt_column_sub_columns.values()]
1151
+
1152
+ self.model_size = model_size
1153
+ self.column_order = column_order
1154
+ self.tgt_seq_len_max = tgt_seq_len_max
1155
+ self.device = device or torch.device("cpu")
1156
+ self.with_dp = with_dp
1157
+
1158
+ # context
1159
+ self.context_compressor = ContextCompressor(
1160
+ model_size=self.model_size,
1161
+ ctx_cardinalities=ctx_cardinalities,
1162
+ ctxseq_len_median=ctxseq_len_median,
1163
+ device=device,
1164
+ with_dp=with_dp,
1165
+ )
1166
+
1167
+ # sub column embeddings
1168
+ self.embedders = Embedders(
1169
+ model_size=self.model_size,
1170
+ cardinalities=self.tgt_cardinalities,
1171
+ device=self.device,
1172
+ )
1173
+
1174
+ # column embeddings
1175
+ self.column_embedders = ColumnEmbedders(
1176
+ model_size=self.model_size,
1177
+ cardinalities=self.tgt_cardinalities,
1178
+ embedding_dims=self.embedders.dims,
1179
+ device=self.device,
1180
+ )
1181
+
1182
+ # history
1183
+ self.history_compressor = HistoryCompressor(
1184
+ model_size=self.model_size,
1185
+ embedding_dims=self.embedders.dims,
1186
+ seq_len_median=self.tgt_seq_len_median,
1187
+ device=self.device,
1188
+ with_dp=with_dp,
1189
+ )
1190
+
1191
+ # attention module connecting sequential context(s) to history
1192
+ self.attention = AttentionModule(
1193
+ dim_input=self.history_compressor.dim_output,
1194
+ dim_outputs=self.context_compressor.sequential_context_compressor.dim_outputs,
1195
+ device=self.device,
1196
+ )
1197
+
1198
+ # regressors
1199
+ self.regressors = Regressors(
1200
+ model_size=self.model_size,
1201
+ cardinalities=self.tgt_cardinalities,
1202
+ context_dim=self.context_compressor.dim_output,
1203
+ history_dim=self.history_compressor.dim_output,
1204
+ embedding_dims=self.embedders.dims,
1205
+ column_embedding_dims=self.column_embedders.dims,
1206
+ device=self.device,
1207
+ )
1208
+
1209
+ # predictors
1210
+ self.predictors = Predictors(
1211
+ cardinalities=self.tgt_cardinalities,
1212
+ regressors_dims=self.regressors.dims_output,
1213
+ device=device,
1214
+ )
1215
+
1216
+ def _repeat_flat_context(self, flat_ctx: list[torch.Tensor], repetition: int) -> list[torch.Tensor]:
1217
+ if len(flat_ctx) > 0:
1218
+ flat_ctx = flat_ctx[0].unsqueeze(1)
1219
+ flat_ctx = flat_ctx.repeat(1, repetition, 1)
1220
+ flat_ctx = [flat_ctx]
1221
+ return flat_ctx
1222
+
1223
+ def forward(
1224
+ self,
1225
+ x,
1226
+ mode: Literal["trn", "gen"],
1227
+ batch_size: int | None = None,
1228
+ fixed_probs=None,
1229
+ fixed_values=None,
1230
+ temperature: float | None = None,
1231
+ top_p: float | None = None,
1232
+ history=None,
1233
+ history_state=None,
1234
+ context=None,
1235
+ ) -> tuple[dict[str, torch.Tensor], torch.Tensor, torch.Tensor]:
1236
+ fixed_probs = fixed_probs or {}
1237
+ fixed_values = fixed_values or {}
1238
+ if context is None:
1239
+ context = self.context_compressor(x)
1240
+
1241
+ outputs = {}
1242
+ if mode == "trn":
1243
+ # forward pass through sub column embedders
1244
+ tgt_embeds = self.embedders(x)
1245
+
1246
+ # forward pass through column embedders
1247
+ tgt_col_embeds = self.column_embedders(tgt_embeds)
1248
+
1249
+ # history
1250
+ # time shift: remove last time step; add zeros for first time step; add randoms for all others
1251
+ embeddings = torch.cat(list(tgt_embeds.values()), dim=-1)
1252
+ history_in = embeddings[:, :-1, :]
1253
+ history_in = nn.ConstantPad2d((0, 0, 1, 0), 0)(history_in)
1254
+ history, _ = self.history_compressor(history_in)
1255
+
1256
+ flat_ctx, seq_ctx, seq_ctx_masks = context
1257
+ # attention with history as query
1258
+ seq_ctx = self.attention(history, seq_ctx, seq_ctx_masks)
1259
+ # repeat flat context to match the length of history
1260
+ flat_ctx = self._repeat_flat_context(flat_ctx, history.size(1))
1261
+ context_history = [torch.cat(flat_ctx + seq_ctx + [history], -1)]
1262
+
1263
+ # create batch-wise permutation mask
1264
+ col_embeddings = torch.cat(list(tgt_col_embeds.values()), dim=-1)
1265
+ col_mask = _make_permutation_mask(
1266
+ col_embedding_dims=self.column_embedders.dims,
1267
+ columns=self.tgt_columns,
1268
+ column_order=self.column_order,
1269
+ is_sequential=True,
1270
+ device=self.device,
1271
+ )
1272
+
1273
+ for sub_col, lookup in self.tgt_sub_columns_lookup.items():
1274
+ # mask concatenated column embeddings
1275
+ masked_col_embeds = [torch.mul(col_mask[lookup.col_idx, :].int(), col_embeddings)]
1276
+
1277
+ # collect previous sub column embeddings for current column
1278
+ prev_sub_col_embeds = [
1279
+ tgt_embeds[sub_col] for sub_col in self.tgt_sub_columns[lookup.sub_col_offset : lookup.sub_col_cum]
1280
+ ]
1281
+
1282
+ # regressor
1283
+ regressor_in = context_history + masked_col_embeds + prev_sub_col_embeds
1284
+ xs = self.regressors(regressor_in, sub_col)
1285
+
1286
+ # predictor
1287
+ xs = self.predictors(xs, sub_col)
1288
+
1289
+ # update output
1290
+ outputs[sub_col] = xs
1291
+
1292
+ return outputs, {}
1293
+
1294
+ else: # mode == "gen"
1295
+ if history is None or history_state is None:
1296
+ # initialize history
1297
+ history_in = torch.cat(
1298
+ [
1299
+ torch.zeros(
1300
+ (batch_size, 1, self.embedders.dims[i]),
1301
+ device=self.device,
1302
+ )
1303
+ for i in range(len(self.tgt_cardinalities))
1304
+ ],
1305
+ dim=-1,
1306
+ )
1307
+ # history_state is tuple of current and hidden LSTM state; see nn.LSTM.forward() for further details
1308
+ history, history_state = self.history_compressor(history_in)
1309
+
1310
+ flat_ctx, seq_ctx, seq_ctx_masks = context
1311
+ # attention between sequential context and history
1312
+ seq_ctx = self.attention(history, seq_ctx, seq_ctx_masks)
1313
+ # repeat flat context to match the length of history
1314
+ flat_ctx = self._repeat_flat_context(flat_ctx, history.size(1))
1315
+ context_history = [torch.cat(flat_ctx + seq_ctx + [history], -1)]
1316
+
1317
+ # initialize sub column embeddings
1318
+ tgt_embeds = self.embedders.zero_mask(batch_size, 1)
1319
+
1320
+ # initialize column embeddings
1321
+ tgt_col_embeds = self.column_embedders.zero_mask(batch_size, 1)
1322
+
1323
+ # concatenate column embeddings
1324
+ col_embeddings = torch.cat(list(tgt_col_embeds.values()), dim=-1)
1325
+
1326
+ # take sub columns in the specified generation order
1327
+ column_order = self.column_order or self.tgt_columns
1328
+ sub_column_order = [sub_col for col in column_order for sub_col in self.tgt_column_sub_columns[col]]
1329
+
1330
+ for sub_col in sub_column_order:
1331
+ lookup = self.tgt_sub_columns_lookup[sub_col]
1332
+
1333
+ # if sub column is fixed, skip sampling and use that value
1334
+ if sub_col in fixed_values:
1335
+ out = fixed_values[sub_col]
1336
+
1337
+ else: # sample from distribution
1338
+ # collect previous sub column embeddings for current column
1339
+ prev_sub_col_embeds = [
1340
+ tgt_embeds[sub_col]
1341
+ for sub_col in self.tgt_sub_columns[lookup.sub_col_offset : lookup.sub_col_cum]
1342
+ ]
1343
+
1344
+ # regressor
1345
+ regressor_in = context_history + [col_embeddings] + prev_sub_col_embeds
1346
+ xs = self.regressors(regressor_in, sub_col)
1347
+
1348
+ # predictor
1349
+ xs = self.predictors(xs, sub_col)
1350
+
1351
+ # sample
1352
+ xs = nn.Softmax(dim=-1)(xs)
1353
+ xs = torch.squeeze(xs[:, -1:, :], -2) # take only last element
1354
+ out = _sample(
1355
+ probs=xs,
1356
+ temperature=temperature,
1357
+ top_p=top_p,
1358
+ fixed_probs=fixed_probs.get(sub_col),
1359
+ )
1360
+
1361
+ # update timestep output
1362
+ outputs[sub_col] = out
1363
+
1364
+ # update current sub column embedding
1365
+ tgt_embeds[sub_col] = self.embedders.get(sub_col)(out)
1366
+
1367
+ # update current column embedding
1368
+ if sub_col in self.tgt_last_sub_cols:
1369
+ col_sub_cols = self.tgt_column_sub_columns[lookup.col_name]
1370
+ col_embed_in = torch.cat([tgt_embeds[sc] for sc in col_sub_cols], dim=-1)
1371
+ tgt_col_embeds[lookup.col_name] = self.column_embedders.get(lookup.col_name)(col_embed_in)
1372
+ col_embeddings = torch.cat(list(tgt_col_embeds.values()), dim=-1)
1373
+
1374
+ # update history and hidden state
1375
+ history_in = torch.cat([v for v in tgt_embeds.values()], dim=-1)
1376
+ history, history_state = self.history_compressor(history_in, history_state=history_state)
1377
+
1378
+ # order outputs according to tgt_sub_columns
1379
+ outputs = {sub_col: outputs[sub_col] for sub_col in self.tgt_cardinalities}
1380
+
1381
+ # gather output
1382
+ return outputs, history, history_state