Skip to content
import os
import numpy as np
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim

from .data import batch_generator from utils import extract_time, random_generator, NormMinMax from .model import Encoder, Recovery, Generator, Discriminator, Supervisor

UtilitiesΒΆ

def random_generator(batch_size, z_dim, T_mb, max_seq_len):
    """Random vector generation.

    Args:
      - batch_size: size of the random vector
      - z_dim: dimension of random vector
      - T_mb: time information for the random vector
      - max_seq_len: maximum sequence length

    Returns:
      - Z_mb: generated random vector
    """
    Z_mb = list()
    for i in range(batch_size):
        temp = np.zeros([max_seq_len, z_dim])
        temp_Z = np.random.uniform(0.0, 1, [T_mb[i], z_dim])
        temp[: T_mb[i], :] = temp_Z
        Z_mb.append(temp_Z)
    return Z_mb
def batch_generator(data, time, batch_size):
    """Mini-batch generator.

    Args:
      - data: time-series data
      - time: time information
      - batch_size: the number of samples in each batch

    Returns:
      - X_mb: time-series data in each batch
      - T_mb: time information in each batch
    """
    no = len(data)
    idx = np.random.permutation(no)
    train_idx = idx[:batch_size]

    X_mb = list(data[i] for i in train_idx)
    T_mb = list(time[i] for i in train_idx)

    return X_mb, T_mb
def NormMinMax(data):
    """Min-Max Normalizer.

    Args:
      - data: raw data

    Returns:
      - norm_data: normalized data
      - min_val: minimum values (for renormalization)
      - max_val: maximum values (for renormalization)
    """
    min_val = np.min(np.min(data, axis=0), axis=0)
    data = data - min_val  # [3661, 24, 6]

    max_val = np.max(np.max(data, axis=0), axis=0)
    norm_data = data / (max_val + 1e-7)

    return norm_data, min_val, max_val
def _weights_init(m):
    classname = m.__class__.__name__
    if isinstance(m, nn.Linear):
        init.xavier_uniform_(m.weight)
        m.bias.data.fill_(0)
    elif classname.find("Conv") != -1:
        m.weight.data.normal_(0.0, 0.02)
    elif classname.find("Norm") != -1:
        m.weight.data.normal_(1.0, 0.02)
        m.bias.data.fill_(0)
    elif classname.find("GRU") != -1:
        for name, param in m.named_parameters():
            if "weight_ih" in name:
                init.xavier_uniform_(param.data)
            elif "weight_hh" in name:
                init.orthogonal_(param.data)
            elif "bias" in name:
                param.data.fill_(0)


class Encoder(nn.Module):
    """Embedding network between original feature space to latent space.

    Args:
      - input: input time-series features. (L, N, X) = (24, ?, 6)
      - h3: (num_layers, N, H). [3, ?, 24]

    Returns:
      - H: embeddings
    """

    def __init__(self, opt):
        super(Encoder, self).__init__()
        self.rnn = nn.GRU(
            input_size=opt.z_dim, hidden_size=opt.hidden_dim, num_layers=opt.num_layer
        )
        self.fc = nn.Linear(opt.hidden_dim, opt.hidden_dim)
        self.sigmoid = nn.Sigmoid()
        self.apply(_weights_init)

    def forward(self, input, sigmoid=True):
        e_outputs, _ = self.rnn(input)
        H = self.fc(e_outputs)
        if sigmoid:
            H = self.sigmoid(H)
        return H


class Recovery(nn.Module):
    """Recovery network from latent space to original space.

    Args:
      - H: latent representation
      - T: input time information

    Returns:
      - X_tilde: recovered data
    """

    def __init__(self, opt):
        super(Recovery, self).__init__()
        self.rnn = nn.GRU(
            input_size=opt.hidden_dim, hidden_size=opt.z_dim, num_layers=opt.num_layer
        )

        self.fc = nn.Linear(opt.z_dim, opt.z_dim)
        self.sigmoid = nn.Sigmoid()
        self.apply(_weights_init)

    def forward(self, input, sigmoid=True):
        r_outputs, _ = self.rnn(input)
        X_tilde = self.fc(r_outputs)
        if sigmoid:
            X_tilde = self.sigmoid(X_tilde)
        return X_tilde


class Generator(nn.Module):
    """Generator function: Generate time-series data in latent space.

    Args:
      - Z: random variables
      - T: input time information

    Returns:
      - E: generated embedding
    """

    def __init__(self, opt):
        super(Generator, self).__init__()
        self.rnn = nn.GRU(
            input_size=opt.z_dim, hidden_size=opt.hidden_dim, num_layers=opt.num_layer
        )
        #   self.norm = nn.LayerNorm(opt.hidden_dim)
        self.fc = nn.Linear(opt.hidden_dim, opt.hidden_dim)
        self.sigmoid = nn.Sigmoid()
        self.apply(_weights_init)

    def forward(self, input, sigmoid=True):
        g_outputs, _ = self.rnn(input)
        #  g_outputs = self.norm(g_outputs)
        E = self.fc(g_outputs)
        if sigmoid:
            E = self.sigmoid(E)
        return E


class Supervisor(nn.Module):
    """Generate next sequence using the previous sequence.

    Args:
      - H: latent representation
      - T: input time information

    Returns:
      - S: generated sequence based on the latent representations generated by the generator
    """

    def __init__(self, opt):
        super(Supervisor, self).__init__()
        self.rnn = nn.GRU(
            input_size=opt.hidden_dim,
            hidden_size=opt.hidden_dim,
            num_layers=opt.num_layer,
        )
        #  self.norm = nn.LayerNorm(opt.hidden_dim)
        self.fc = nn.Linear(opt.hidden_dim, opt.hidden_dim)
        self.sigmoid = nn.Sigmoid()
        self.apply(_weights_init)

    def forward(self, input, sigmoid=True):
        s_outputs, _ = self.rnn(input)
        #  s_outputs = self.norm(s_outputs)
        S = self.fc(s_outputs)
        if sigmoid:
            S = self.sigmoid(S)
        return S


class Discriminator(nn.Module):
    """Discriminate the original and synthetic time-series data.

    Args:
      - H: latent representation
      - T: input time information

    Returns:
      - Y_hat: classification results between original and synthetic time-series
    """

    def __init__(self, opt):
        super(Discriminator, self).__init__()
        self.rnn = nn.GRU(
            input_size=opt.hidden_dim,
            hidden_size=opt.hidden_dim,
            num_layers=opt.num_layer,
        )
        #  self.norm = nn.LayerNorm(opt.hidden_dim)
        self.fc = nn.Linear(opt.hidden_dim, opt.hidden_dim)
        self.sigmoid = nn.Sigmoid()
        self.apply(_weights_init)

    def forward(self, input, sigmoid=True):
        d_outputs, _ = self.rnn(input)
        Y_hat = self.fc(d_outputs)
        if sigmoid:
            Y_hat = self.sigmoid(Y_hat)
        return Y_hat
class BaseModel:
    """Base Model for timegan"""

    def __init__(self, opt, ori_data):
        # Seed for deterministic behavior
        self.seed(opt.manualseed)

        # Initalize variables.
        self.opt = opt
        self.ori_data, self.min_val, self.max_val = NormMinMax(ori_data)
        self.ori_time, self.max_seq_len = extract_time(self.ori_data)
        self.data_num, _, _ = np.asarray(ori_data).shape  # 3661; 24; 6
        self.trn_dir = os.path.join(self.opt.outf, self.opt.name, "train")
        self.tst_dir = os.path.join(self.opt.outf, self.opt.name, "test")
        self.device = torch.device("cuda:0" if self.opt.device != "cpu" else "cpu")

    def seed(self, seed_value):
        """Seed

        Arguments:
            seed_value {int} -- [description]
        """
        # Check if seed is default value
        if seed_value == -1:
            return

        # Otherwise seed all functionality
        import random

        random.seed(seed_value)
        torch.manual_seed(seed_value)
        torch.cuda.manual_seed_all(seed_value)
        np.random.seed(seed_value)
        torch.backends.cudnn.deterministic = True

    def save_weights(self, epoch):
        """Save net weights for the current epoch.

        Args:
            epoch ([int]): Current epoch number.
        """

        weight_dir = os.path.join(self.opt.outf, self.opt.name, "train", "weights")
        if not os.path.exists(weight_dir):
            os.makedirs(weight_dir)

        torch.save(
            {"epoch": epoch + 1, "state_dict": self.nete.state_dict()},
            "%s/netE.pth" % (weight_dir),
        )
        torch.save(
            {"epoch": epoch + 1, "state_dict": self.netr.state_dict()},
            "%s/netR.pth" % (weight_dir),
        )
        torch.save(
            {"epoch": epoch + 1, "state_dict": self.netg.state_dict()},
            "%s/netG.pth" % (weight_dir),
        )
        torch.save(
            {"epoch": epoch + 1, "state_dict": self.netd.state_dict()},
            "%s/netD.pth" % (weight_dir),
        )
        torch.save(
            {"epoch": epoch + 1, "state_dict": self.nets.state_dict()},
            "%s/netS.pth" % (weight_dir),
        )

    def train_one_iter_er(self):
        """Train the model for one epoch."""

        self.nete.train()
        self.netr.train()

        # set mini-batch
        self.X0, self.T = batch_generator(
            self.ori_data, self.ori_time, self.opt.batch_size
        )
        self.X = torch.tensor(self.X0, dtype=torch.float32).to(self.device)

        # train encoder & decoder
        self.optimize_params_er()

    def train_one_iter_er_(self):
        """Train the model for one epoch."""

        self.nete.train()
        self.netr.train()

        # set mini-batch
        self.X0, self.T = batch_generator(
            self.ori_data, self.ori_time, self.opt.batch_size
        )
        self.X = torch.tensor(self.X0, dtype=torch.float32).to(self.device)

        # train encoder & decoder
        self.optimize_params_er_()

    def train_one_iter_s(self):
        """Train the model for one epoch."""

        # self.nete.eval()
        self.nets.train()

        # set mini-batch
        self.X0, self.T = batch_generator(
            self.ori_data, self.ori_time, self.opt.batch_size
        )
        self.X = torch.tensor(self.X0, dtype=torch.float32).to(self.device)

        # train superviser
        self.optimize_params_s()

    def train_one_iter_g(self):
        """Train the model for one epoch."""

        """self.netr.eval()
    self.nets.eval()
    self.netd.eval()"""
        self.netg.train()

        # set mini-batch
        self.X0, self.T = batch_generator(
            self.ori_data, self.ori_time, self.opt.batch_size
        )
        self.X = torch.tensor(self.X0, dtype=torch.float32).to(self.device)
        self.Z = random_generator(
            self.opt.batch_size, self.opt.z_dim, self.T, self.max_seq_len
        )

        # train superviser
        self.optimize_params_g()

    def train_one_iter_d(self):
        """Train the model for one epoch."""
        """self.nete.eval()
    self.netr.eval()
    self.nets.eval()
    self.netg.eval()"""
        self.netd.train()

        # set mini-batch
        self.X0, self.T = batch_generator(
            self.ori_data, self.ori_time, self.opt.batch_size
        )
        self.X = torch.tensor(self.X0, dtype=torch.float32).to(self.device)
        self.Z = random_generator(
            self.opt.batch_size, self.opt.z_dim, self.T, self.max_seq_len
        )

        # train superviser
        self.optimize_params_d()

    def train(self):
        """Train the model"""

        for iter in range(self.opt.iteration):
            # Train for one iter
            self.train_one_iter_er()

            print("Encoder training step: " + str(iter) + "/" + str(self.opt.iteration))

        for iter in range(self.opt.iteration):
            # Train for one iter
            self.train_one_iter_s()

            print(
                "Superviser training step: " + str(iter) + "/" + str(self.opt.iteration)
            )

        for iter in range(self.opt.iteration):
            # Train for one iter
            for kk in range(2):
                self.train_one_iter_g()
                self.train_one_iter_er_()

            self.train_one_iter_d()

            print(
                "Superviser training step: " + str(iter) + "/" + str(self.opt.iteration)
            )

        self.save_weights(self.opt.iteration)
        self.generated_data = self.generation(self.opt.batch_size)
        print("Finish Synthetic Data Generation")

    #  self.evaluation()

    """def evaluation(self):
    ## Performance metrics
    # Output initialization
    metric_results = dict()

    # 1. Discriminative Score
    discriminative_score = list()
    for _ in range(self.opt.metric_iteration):
      temp_disc = discriminative_score_metrics(self.ori_data, self.generated_data)
      discriminative_score.append(temp_disc)

    metric_results['discriminative'] = np.mean(discriminative_score)

    # 2. Predictive score
    predictive_score = list()
    for tt in range(self.opt.metric_iteration):
      temp_pred = predictive_score_metrics(self.ori_data, self.generated_data)
      predictive_score.append(temp_pred)

    metric_results['predictive'] = np.mean(predictive_score)

    # 3. Visualization (PCA and tSNE)
    visualization(self.ori_data, self.generated_data, 'pca')
    visualization(self.ori_data, self.generated_data, 'tsne')

    ## Print discriminative and predictive scores
    print(metric_results)
"""

    def generation(self, num_samples, mean=0.0, std=1.0):
        if num_samples == 0:
            return None, None
        ## Synthetic data generation
        self.X0, self.T = batch_generator(
            self.ori_data, self.ori_time, self.opt.batch_size
        )
        self.Z = random_generator(
            num_samples, self.opt.z_dim, self.T, self.max_seq_len  # , mean, std
        )
        self.Z = torch.tensor(self.Z, dtype=torch.float32).to(self.device)
        self.E_hat = self.netg(self.Z)  # [?, 24, 24]
        self.H_hat = self.nets(self.E_hat)  # [?, 24, 24]
        generated_data_curr = (
            self.netr(self.H_hat).cpu().detach().numpy()
        )  # [?, 24, 24]

        generated_data = list()
        for i in range(num_samples):
            temp = generated_data_curr[i, : self.ori_time[i], :]
            generated_data.append(temp)

        # Renormalization
        generated_data = generated_data * self.max_val
        generated_data = generated_data + self.min_val
        return generated_data
class TimeGAN(BaseModel):
    """TimeGAN Class"""

    @property
    def name(self):
        return "TimeGAN"

    def __init__(self, opt, ori_data):
        super(TimeGAN, self).__init__(opt, ori_data)

        # -- Misc attributes
        self.epoch = 0
        self.times = []
        self.total_steps = 0

        # Create and initialize networks.
        self.nete = Encoder(self.opt).to(self.device)
        self.netr = Recovery(self.opt).to(self.device)
        self.netg = Generator(self.opt).to(self.device)
        self.netd = Discriminator(self.opt).to(self.device)
        self.nets = Supervisor(self.opt).to(self.device)

        if self.opt.resume != "":
            print("\nLoading pre-trained networks.")
            self.opt.iter = torch.load(os.path.join(self.opt.resume, "netG.pth"))[
                "epoch"
            ]
            self.nete.load_state_dict(
                torch.load(os.path.join(self.opt.resume, "netE.pth"))["state_dict"]
            )
            self.netr.load_state_dict(
                torch.load(os.path.join(self.opt.resume, "netR.pth"))["state_dict"]
            )
            self.netg.load_state_dict(
                torch.load(os.path.join(self.opt.resume, "netG.pth"))["state_dict"]
            )
            self.netd.load_state_dict(
                torch.load(os.path.join(self.opt.resume, "netD.pth"))["state_dict"]
            )
            self.nets.load_state_dict(
                torch.load(os.path.join(self.opt.resume, "netS.pth"))["state_dict"]
            )
            print("\tDone.\n")

        # loss
        self.l_mse = nn.MSELoss()
        self.l_r = nn.L1Loss()
        self.l_bce = nn.BCELoss()

        # Setup optimizer
        if self.opt.isTrain:
            self.nete.train()
            self.netr.train()
            self.netg.train()
            self.netd.train()
            self.nets.train()
            self.optimizer_e = optim.Adam(
                self.nete.parameters(), lr=self.opt.lr, betas=(self.opt.beta1, 0.999)
            )
            self.optimizer_r = optim.Adam(
                self.netr.parameters(), lr=self.opt.lr, betas=(self.opt.beta1, 0.999)
            )
            self.optimizer_g = optim.Adam(
                self.netg.parameters(), lr=self.opt.lr, betas=(self.opt.beta1, 0.999)
            )
            self.optimizer_d = optim.Adam(
                self.netd.parameters(), lr=self.opt.lr, betas=(self.opt.beta1, 0.999)
            )
            self.optimizer_s = optim.Adam(
                self.nets.parameters(), lr=self.opt.lr, betas=(self.opt.beta1, 0.999)
            )

    def forward_e(self):
        """Forward propagate through netE"""
        self.H = self.nete(self.X)

    def forward_er(self):
        """Forward propagate through netR"""
        self.H = self.nete(self.X)
        self.X_tilde = self.netr(self.H)

    def forward_g(self):
        """Forward propagate through netG"""
        self.Z = torch.tensor(self.Z, dtype=torch.float32).to(self.device)
        self.E_hat = self.netg(self.Z)

    def forward_dg(self):
        """Forward propagate through netD"""
        self.Y_fake = self.netd(self.H_hat)
        self.Y_fake_e = self.netd(self.E_hat)

    def forward_rg(self):
        """Forward propagate through netG"""
        self.X_hat = self.netr(self.H_hat)

    def forward_s(self):
        """Forward propagate through netS"""
        self.H_supervise = self.nets(self.H)
        # print(self.H, self.H_supervise)

    def forward_sg(self):
        """Forward propagate through netS"""
        self.H_hat = self.nets(self.E_hat)

    def forward_d(self):
        """Forward propagate through netD"""
        self.Y_real = self.netd(self.H)
        self.Y_fake = self.netd(self.H_hat)
        self.Y_fake_e = self.netd(self.E_hat)

    def backward_er(self):
        """Backpropagate through netE"""
        self.err_er = self.l_mse(self.X_tilde, self.X)
        self.err_er.backward(retain_graph=True)
        print("Loss: ", self.err_er)

    def backward_er_(self):
        """Backpropagate through netE"""
        self.err_er_ = self.l_mse(self.X_tilde, self.X)
        self.err_s = self.l_mse(self.H_supervise[:, :-1, :], self.H[:, 1:, :])
        self.err_er = 10 * torch.sqrt(self.err_er_) + 0.1 * self.err_s
        self.err_er.backward(retain_graph=True)

    #  print("Loss: ", self.err_er_, self.err_s)
    def backward_g(self):
        """Backpropagate through netG"""
        self.err_g_U = self.l_bce(self.Y_fake, torch.ones_like(self.Y_fake))

        self.err_g_U_e = self.l_bce(self.Y_fake_e, torch.ones_like(self.Y_fake_e))
        self.err_g_V1 = torch.mean(
            torch.abs(
                torch.sqrt(torch.std(self.X_hat, [0])[1] + 1e-6)
                - torch.sqrt(torch.std(self.X, [0])[1] + 1e-6)
            )
        )  # |a^2 - b^2|
        self.err_g_V2 = torch.mean(
            torch.abs((torch.mean(self.X_hat, [0])[0]) - (torch.mean(self.X, [0])[0]))
        )  # |a - b|
        self.err_s = self.l_mse(self.H_supervise[:, :-1, :], self.H[:, 1:, :])
        self.err_g = (
            self.err_g_U
            + self.err_g_U_e * self.opt.w_gamma
            + self.err_g_V1 * self.opt.w_g
            + self.err_g_V2 * self.opt.w_g
            + torch.sqrt(self.err_s)
        )
        self.err_g.backward(retain_graph=True)
        print("Loss G: ", self.err_g)

    def backward_s(self):
        """Backpropagate through netS"""
        self.err_s = self.l_mse(self.H[:, 1:, :], self.H_supervise[:, :-1, :])
        self.err_s.backward(retain_graph=True)
        print("Loss S: ", self.err_s)

    #   print(torch.autograd.grad(self.err_s, self.nets.parameters()))

    def backward_d(self):
        """Backpropagate through netD"""
        self.err_d_real = self.l_bce(self.Y_real, torch.ones_like(self.Y_real))
        self.err_d_fake = self.l_bce(self.Y_fake, torch.zeros_like(self.Y_fake))
        self.err_d_fake_e = self.l_bce(self.Y_fake_e, torch.zeros_like(self.Y_fake_e))
        self.err_d = (
            self.err_d_real + self.err_d_fake + self.err_d_fake_e * self.opt.w_gamma
        )
        if self.err_d > 0.15:
            self.err_d.backward(retain_graph=True)

    # print("Loss D: ", self.err_d)

    def optimize_params_er(self):
        """Forwardpass, Loss Computation and Backwardpass."""
        # Forward-pass
        self.forward_er()

        # Backward-pass
        # nete & netr
        self.optimizer_e.zero_grad()
        self.optimizer_r.zero_grad()
        self.backward_er()
        self.optimizer_e.step()
        self.optimizer_r.step()

    def optimize_params_er_(self):
        """Forwardpass, Loss Computation and Backwardpass."""
        # Forward-pass
        self.forward_er()
        self.forward_s()
        # Backward-pass
        # nete & netr
        self.optimizer_e.zero_grad()
        self.optimizer_r.zero_grad()
        self.backward_er_()
        self.optimizer_e.step()
        self.optimizer_r.step()

    def optimize_params_s(self):
        """Forwardpass, Loss Computation and Backwardpass."""
        # Forward-pass
        self.forward_e()
        self.forward_s()

        # Backward-pass
        # nets
        self.optimizer_s.zero_grad()
        self.backward_s()
        self.optimizer_s.step()

    def optimize_params_g(self):
        """Forwardpass, Loss Computation and Backwardpass."""
        # Forward-pass
        self.forward_e()
        self.forward_s()
        self.forward_g()
        self.forward_sg()
        self.forward_rg()
        self.forward_dg()

        # Backward-pass
        # nets
        self.optimizer_g.zero_grad()
        self.optimizer_s.zero_grad()
        self.backward_g()
        self.optimizer_g.step()
        self.optimizer_s.step()

    def optimize_params_d(self):
        """Forwardpass, Loss Computation and Backwardpass."""
        # Forward-pass
        self.forward_e()
        self.forward_g()
        self.forward_sg()
        self.forward_d()
        self.forward_dg()

        # Backward-pass
        # nets
        self.optimizer_d.zero_grad()
        self.backward_d()
        self.optimizer_d.step()
def sine_data_generation(no, seq_len, dim):
    """Sine data generation.

    Args:
      - no: the number of samples
      - seq_len: sequence length of the time-series
      - dim: feature dimensions

    Returns:
      - data: generated data
    """
    # Initialize the output
    data = list()

    # Generate sine data
    for i in range(no):
        # Initialize each time-series
        temp = list()
        # For each feature
        for k in range(dim):
            # Randomly drawn frequency and phase
            freq = np.random.uniform(0, 0.1)
            phase = np.random.uniform(0, 0.1)

            # Generate sine signal based on the drawn frequency and phase
            temp_data = [np.sin(freq * j + phase) for j in range(seq_len)]
            temp.append(temp_data)

        # Align row/column
        temp = np.transpose(np.asarray(temp))
        # Normalize to [0,1]
        temp = (temp + 1) * 0.5
        # Stack the generated data
        data.append(temp)

    return data
def extract_time(data):
    """Returns Maximum sequence length and each sequence length.

    Args:
      - data: original data

    Returns:
      - time: extracted time information
      - max_seq_len: maximum sequence length
    """
    time = list()
    max_seq_len = 0
    for i in range(len(data)):
        max_seq_len = max(max_seq_len, len(data[i][:, 0]))
        time.append(len(data[i][:, 0]))

    return time, max_seq_len

TrainΒΆ

import dataclasses
@dataclasses.dataclass
class ModelParams:
    z_dim: int
    seq_len: int
    module: str
    hidden_dim: int
    num_layer: int
    iteration: int
    batch_size: int
    metric_iteration: int
    manualseed: int
    outf: str
    name: str
    device: str
    resume: str
    isTrain: bool = True
    lr: float = 0.0002
    beta1: float = 0.9
    w_gamma: float = 1
    w_es: float = 0.1
    w_e0: float = 10
    w_g: float = 100


model_params = {
    "z_dim": 6,
    "seq_len": 24,
    "module": "gru",
    "hidden_dim": 24,
    "num_layer": 3,
    "iteration": 100,
    "batch_size": 128,
    "metric_iteration": 10,
    "manualseed": 42,
    "outf": "tmp",
    "name": "timegan",
    "device": "cpu",
    "resume": "",
}
data = sine_data_generation(3661, 24, 1)
model = TimeGAN(ModelParams(**model_params), data)
model.train()
model.generated_data.shape
model.generated_data
import matplotlib.pyplot as plt
data[0]
for i in range(len(data)):
    plt.plot(data[i].shape, ".")