Skip to content

Commit ce7e1b2

Browse files
Merge pull request #56 from Quantmetry/angoho_CSDI_T
Feat: Diffusion models
2 parents 141e9dc + 1d8de22 commit ce7e1b2

File tree

9 files changed

+1600
-6
lines changed

9 files changed

+1600
-6
lines changed

HISTORY.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
History
33
=======
44

5-
0.0.16 (2023-??-??)
5+
0.1 (2023-??-??)
66
-------------------
77

88
* VAR(p) EM sampler implemented, founding on a VAR(p) modelization such as the one described in `Lütkepohl (2005) New Introduction to Multiple Time Series Analysis`
99
* EM and RPCA matrices transposed in the low-level impelmentation, however the API remains unchanged
10-
* Sparse matrices introduced in the RPCA impletation so as to speed up the execution
10+
* Sparse matrices introduced in the RPCA implementation so as to speed up the execution
11+
* Implementation of SoftImpute, which provides a fast but less robust alterantive to RPCA
12+
* Implementation of TabDDPM and TsDDPM, which are diffusion-based models for tabular data and time-series data, based on Denoising Diffusion Probabilistic Models. Their implementations follow the work of Tashiro et al., (2021) and Kotelnikov et al., (2023).
13+
* ImputerDiffusion is an imputer-wrapper of these two models TabDDPM and TsDDPM.
1114
* Docstrings and tests improved for the EM sampler
15+
* Online documentation reworked, with new tutorials on hole generators and a benchmark for time series imputation
1216

1317
0.0.15 (2023-08-03)
1418
-------------------

qolmat/benchmark/missing_patterns.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pandas as pd
99
from sklearn import utils as sku
1010
from sklearn.utils import resample
11+
import math
1112

1213
from qolmat.utils.exceptions import NoMissingValue, SubsetIsAString
1314

@@ -186,7 +187,7 @@ def generate_mask(self, X: pd.DataFrame) -> pd.DataFrame:
186187

187188
self.rng = sku.check_random_state(self.random_state)
188189
df_mask = pd.DataFrame(False, index=X.index, columns=X.columns)
189-
n_masked_col = round(self.ratio_masked * len(X))
190+
n_masked_col = math.ceil(self.ratio_masked * len(X))
190191

191192
for column in self.subset:
192193
indices = np.where(X[column].notna())[0]

qolmat/imputations/diffusions/__init__py

Whitespace-only changes.
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
from typing import Tuple
2+
import torch
3+
import math
4+
5+
6+
class ResidualBlock(torch.nn.Module):
7+
"""Residual block based on the work of Gorishniy et al., 2023
8+
(https://arxiv.org/abs/2106.11959).
9+
We follow the implementation found in
10+
https://github.com/Yura52/rtdl/blob/main/rtdl/nn/_backbones.py"""
11+
12+
def __init__(self, dim_input: int, dim_embedding: int = 128, p_dropout: float = 0.0):
13+
"""Residual block based on the work of Gorishniy et al., 2023
14+
(https://arxiv.org/abs/2106.11959).
15+
We follow the implementation found in
16+
https://github.com/Yura52/rtdl/blob/main/rtdl/nn/_backbones.py
17+
18+
Parameters
19+
----------
20+
dim_input : int
21+
Input dimension
22+
dim_embedding : int, optional
23+
Embedding dimension, by default 128
24+
p_dropout : float, optional
25+
Dropout probability, by default 0.1
26+
"""
27+
28+
super().__init__()
29+
30+
self.layer_norm = torch.nn.LayerNorm(dim_input)
31+
self.linear_in = torch.nn.Linear(dim_input, dim_embedding)
32+
self.linear_out = torch.nn.Linear(dim_embedding, dim_input)
33+
self.dropout = torch.nn.Dropout(p_dropout)
34+
35+
self.linear_out = torch.nn.Linear(dim_embedding, dim_input)
36+
37+
def forward(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
38+
"""Return an output of a residual block
39+
40+
Parameters
41+
----------
42+
x : torch.Tensor
43+
Data input
44+
t : torch.Tensor
45+
Noise step
46+
47+
Returns
48+
-------
49+
Tuple[torch.Tensor, torch.Tensor]
50+
Output data at noise step t
51+
"""
52+
53+
x_t = self.layer_norm(x + t)
54+
x_t_emb = torch.nn.functional.relu(self.linear_in(x_t))
55+
x_t_emb = self.dropout(x_t_emb)
56+
x_t_emb = self.linear_out(x_t_emb)
57+
58+
return x + x_t_emb, x_t_emb
59+
60+
61+
class ResidualBlockTS(torch.nn.Module):
62+
"""Residual block based on the work of Gorishniy et al., 2023
63+
(https://arxiv.org/abs/2106.11959).
64+
We follow the implementation found in
65+
https://github.com/Yura52/rtdl/blob/main/rtdl/nn/_backbones.py
66+
This class is for Time-Series data where we add Tranformers to
67+
encode time-based/feature-based context."""
68+
69+
def __init__(
70+
self,
71+
dim_input: int,
72+
size_window: int = 10,
73+
dim_embedding: int = 128,
74+
dim_feedforward: int = 64,
75+
nheads_feature: int = 5,
76+
nheads_time: int = 8,
77+
num_layers_transformer: int = 1,
78+
):
79+
"""Residual block based on the work of Gorishniy et al., 2023
80+
(https://arxiv.org/abs/2106.11959).
81+
We follow the implementation found in
82+
https://github.com/Yura52/rtdl/blob/main/rtdl/nn/_backbones.py
83+
This class is for Time-Series data where we add Tranformers to
84+
encode time-based/feature-based context.
85+
86+
Parameters
87+
----------
88+
dim_input : int
89+
Input dimension
90+
size_window : int, optional
91+
Size of window, by default 10
92+
dim_embedding : int, optional
93+
Embedding dimension, by default 128
94+
dim_feedforward : int, optional
95+
Feedforward layer dimension, by default 64
96+
nheads_feature : int, optional
97+
Number of heads to encode feature-based context, by default 5
98+
nheads_time : int, optional
99+
Number of heads to encode time-based context, by default 8
100+
num_layers_transformer : int, optional
101+
Number of transformer layer, by default 1
102+
"""
103+
super().__init__()
104+
105+
self.layer_norm = torch.nn.LayerNorm(dim_input)
106+
107+
encoder_layer_time = torch.nn.TransformerEncoderLayer(
108+
d_model=dim_embedding,
109+
nhead=nheads_time,
110+
dim_feedforward=dim_feedforward,
111+
activation="gelu",
112+
batch_first=True,
113+
dropout=0.1,
114+
)
115+
self.time_layer = torch.nn.TransformerEncoder(
116+
encoder_layer_time, num_layers=num_layers_transformer
117+
)
118+
119+
self.linear_out = torch.nn.Linear(dim_embedding, dim_input)
120+
121+
def forward(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
122+
"""Return an output of a residual block
123+
124+
Parameters
125+
----------
126+
x : torch.Tensor
127+
Data input
128+
t : torch.LongTensor
129+
Noise step
130+
131+
Returns
132+
-------
133+
torch.Tensor
134+
Data output, noise predicted
135+
"""
136+
batch_size, size_window, dim_emb = x.shape
137+
138+
x_emb = self.layer_norm(x)
139+
x_emb_time = self.time_layer(x_emb)
140+
t_emb = t.repeat(1, size_window).reshape(batch_size, size_window, dim_emb)
141+
142+
x_t = x + x_emb_time + t_emb
143+
x_t = self.linear_out(x_t)
144+
145+
return x + x_t, x_t
146+
147+
148+
class AutoEncoder(torch.nn.Module):
149+
"""Epsilon_theta model of the Algorithm 1 in
150+
Ho et al., 2020 (https://arxiv.org/abs/2006.11239).
151+
This implementation is based on the work of
152+
Tashiro et al., 2021 (https://arxiv.org/abs/2107.03502).
153+
Their code: https://github.com/ermongroup/CSDI/blob/main/diff_models.py"""
154+
155+
def __init__(
156+
self,
157+
num_noise_steps: int,
158+
dim_input: int,
159+
residual_block: torch.nn.Module,
160+
dim_embedding: int = 128,
161+
num_blocks: int = 1,
162+
p_dropout: float = 0.0,
163+
):
164+
"""Epsilon_theta model in Algorithm 1 in
165+
Ho et al., 2020 (https://arxiv.org/abs/2006.11239)
166+
167+
Parameters
168+
----------
169+
num_noise_steps : int
170+
Number of steps in forward/reverse processes
171+
dim_input : int
172+
Input dimension
173+
dim_embedding : int, optional
174+
Embedding dimension, by default 128
175+
num_blocks : int, optional
176+
Number of residual blocks, by default 1
177+
p_dropout : float, optional
178+
Dropout probability, by default 0.0
179+
"""
180+
super().__init__()
181+
182+
self.layer_x = torch.nn.Linear(dim_input, dim_embedding)
183+
184+
self.register_buffer(
185+
"embedding_noise_step",
186+
self._build_embedding(num_noise_steps, int(dim_embedding / 2)),
187+
persistent=False,
188+
)
189+
self.layer_t_1 = torch.nn.Linear(dim_embedding, dim_embedding)
190+
self.layer_t_2 = torch.nn.Linear(dim_embedding, dim_embedding)
191+
192+
self.layer_out_1 = torch.nn.Linear(dim_embedding, dim_embedding)
193+
self.layer_out_2 = torch.nn.Linear(dim_embedding, dim_input)
194+
self.dropout_out = torch.nn.Dropout(p_dropout)
195+
196+
self.residual_layers = torch.nn.ModuleList([residual_block for _ in range(num_blocks)])
197+
198+
def forward(self, x: torch.Tensor, t: torch.LongTensor) -> torch.Tensor:
199+
"""Predict a noise
200+
201+
Parameters
202+
----------
203+
x : torch.Tensor
204+
Data input
205+
t : torch.LongTensor
206+
Noise step
207+
208+
Returns
209+
-------
210+
torch.Tensor
211+
Data output, noise predicted
212+
"""
213+
# Noise step embedding
214+
t_emb = torch.as_tensor(self.embedding_noise_step)[t].squeeze()
215+
t_emb = self.layer_t_1(t_emb)
216+
t_emb = torch.nn.functional.silu(t_emb)
217+
t_emb = self.layer_t_2(t_emb)
218+
t_emb = torch.nn.functional.silu(t_emb)
219+
220+
x_emb = torch.nn.functional.relu(self.layer_x(x))
221+
222+
skip = []
223+
for layer in self.residual_layers:
224+
x_emb, skip_connection = layer(x_emb, t_emb)
225+
skip.append(skip_connection)
226+
227+
out = torch.sum(torch.stack(skip), dim=0) / math.sqrt(len(self.residual_layers))
228+
out = torch.nn.functional.relu(self.layer_out_1(out))
229+
out = self.dropout_out(out)
230+
out = self.layer_out_2(out)
231+
232+
return out
233+
234+
def _build_embedding(self, num_noise_steps: int, dim: int = 64) -> torch.Tensor:
235+
"""Build an embedding for noise step.
236+
More details in section E.1 of Tashiro et al., 2021
237+
(https://arxiv.org/abs/2107.03502)
238+
239+
Parameters
240+
----------
241+
num_noise_steps : int
242+
Number of noise steps
243+
dim : int, optional
244+
output dimension, by default 64
245+
246+
Returns
247+
-------
248+
torch.Tensor
249+
List of embeddings for noise steps
250+
"""
251+
steps = torch.arange(num_noise_steps).unsqueeze(1) # (T,1)
252+
frequencies = 10.0 ** (torch.arange(dim) / (dim - 1) * 4.0).unsqueeze(0) # (1,dim)
253+
table = steps * frequencies # (T,dim)
254+
table = torch.cat([torch.sin(table), torch.cos(table)], dim=1) # (T,dim*2)
255+
return table

0 commit comments

Comments
 (0)