Пример #1
0
    def set_generator(self, generator_circuit: Optional[Union[QuantumCircuit,
                                                              UnivariateVariationalDistribution,
                                                              MultivariateVariationalDistribution]
                                                        ] = None,
                      generator_init_params: Optional[np.ndarray] = None,
                      generator_optimizer: Optional[Optimizer] = None,
                      generator_gradient: Optional[Union[Callable, Gradient]] = None):
        """Initialize generator.

        Args:
            generator_circuit: parameterized quantum circuit which sets
                the structure of the quantum generator
            generator_init_params: initial parameters for the generator circuit
            generator_optimizer: optimizer to be used for the training of the generator
            generator_gradient: A Gradient object, or a function returning partial
                derivatives of the loss function w.r.t. the generator variational
                params.
        Raises:
            AquaError: invalid input
        """
        if generator_gradient:
            if not isinstance(generator_gradient, (Gradient, FunctionType)):
                raise AquaError('Please pass either a Gradient object or a function as '
                                'the generator_gradient argument.')
        self._generator = QuantumGenerator(self._bounds, self._num_qubits,
                                           generator_circuit, generator_init_params,
                                           generator_optimizer,
                                           generator_gradient,
                                           self._snapshot_dir)
Пример #2
0
 def set_generator(self, generator_circuit=None,
                   generator_init_params=None, generator_optimizer=None):
     """
     Initialize generator.
     Args:
         generator_circuit (VariationalForm): parameterized quantum circuit which sets
                             the structure of the quantum generator
         generator_init_params(numpy.ndarray): initial parameters for the generator circuit
         generator_optimizer (Optimizer): optimizer to be used for the training of the generator
     """
     self._generator = QuantumGenerator(self._bounds, self._num_qubits,
                                        generator_circuit, generator_init_params,
                                        self._snapshot_dir)
Пример #3
0
    def set_generator(self, generator_circuit=None, generator_init_params=None, generator_optimizer=None):
        """
        Initialize generator.
        Args:
            generator_circuit: VariationalForm, parametrized quantum circuit which sets the structure of the quantum
                               generator
            generator_init_params: array, initial parameters for the generator circuit
            generator_optimizer: Optimizer, optimizer to be used for the training of the generator

        Returns:

        """
        self._generator = QuantumGenerator(self._bounds, self._num_qubits, generator_circuit, generator_init_params,
                                           self._snapshot_dir)
        return
Пример #4
0
    def set_generator(self, generator_circuit: Optional[Union[QuantumCircuit,
                                                              UnivariateVariationalDistribution,
                                                              MultivariateVariationalDistribution]
                                                        ] = None,
                      generator_init_params: Optional[np.ndarray] = None,
                      generator_optimizer: Optional[Optimizer] = None):
        """Initialize generator.

        Args:
            generator_circuit: parameterized quantum circuit which sets
                the structure of the quantum generator
            generator_init_params: initial parameters for the generator circuit
            generator_optimizer: optimizer to be used for the training of the generator
        """
        self._generator = QuantumGenerator(self._bounds, self._num_qubits,
                                           generator_circuit, generator_init_params,
                                           self._snapshot_dir)
Пример #5
0
class QGAN(QuantumAlgorithm):
    """
    Quantum Generative Adversarial Network.

    """

    def __init__(self, data: np.ndarray, bounds: Optional[np.ndarray] = None,
                 num_qubits: Optional[np.ndarray] = None, batch_size: int = 500,
                 num_epochs: int = 3000, seed: int = 7,
                 discriminator: Optional[DiscriminativeNetwork] = None,
                 generator: Optional[GenerativeNetwork] = None,
                 tol_rel_ent: Optional[float] = None, snapshot_dir: Optional[str] = None) -> None:
        """

        Args:
            data: training data of dimension k
            bounds: k min/max data values [[min_0,max_0],...,[min_k-1,max_k-1]]
                if univariate data: [min_0,max_0]
            num_qubits: k numbers of qubits to determine representation resolution,
                i.e. n qubits enable the representation of 2**n values
                [num_qubits_0,..., num_qubits_k-1]
            batch_size: batch size, has a min. value of 1.
            num_epochs: number of training epochs
            seed: random number seed
            discriminator: discriminates between real and fake data samples
            generator: generates 'fake' data samples
            tol_rel_ent: Set tolerance level for relative entropy.
                If the training achieves relative
                entropy equal or lower than tolerance it finishes.
            snapshot_dir: path or None, if path given store cvs file
                with parameters to the directory
        Raises:
            AquaError: invalid input
        """
        validate_min('batch_size', batch_size, 1)
        super().__init__()
        if data is None:
            raise AquaError('Training data not given.')
        self._data = np.array(data)
        if bounds is None:
            bounds_min = np.percentile(self._data, 5, axis=0)
            bounds_max = np.percentile(self._data, 95, axis=0)
            bounds = []
            for i, _ in enumerate(bounds_min):
                bounds.append([bounds_min[i], bounds_max[i]])
        if np.ndim(data) > 1:
            if len(bounds) != (len(num_qubits) or len(data[0])):
                raise AquaError('Dimensions of the data, the length of the data bounds '
                                'and the numbers of qubits per '
                                'dimension are incompatible.')
        else:
            if (np.ndim(bounds) or len(num_qubits)) != 1:
                raise AquaError('Dimensions of the data, the length of the data bounds '
                                'and the numbers of qubits per '
                                'dimension are incompatible.')
        self._bounds = np.array(bounds)
        self._num_qubits = num_qubits
        # pylint: disable=unsubscriptable-object
        if np.ndim(data) > 1:
            if self._num_qubits is None:
                self._num_qubits = np.ones[len(data[0])]*3
        else:
            if self._num_qubits is None:
                self._num_qubits = np.array([3])
        self._data, self._data_grid, self._grid_elements, self._prob_data = \
            discretize_and_truncate(self._data, self._bounds, self._num_qubits,
                                    return_data_grid_elements=True,
                                    return_prob=True, prob_non_zero=True)
        self._batch_size = batch_size
        self._num_epochs = num_epochs
        self._snapshot_dir = snapshot_dir
        self._g_loss = []
        self._d_loss = []
        self._rel_entr = []
        self._tol_rel_ent = tol_rel_ent

        self._random_seed = seed

        if generator is None:
            self.set_generator()
        else:
            self._generator = generator
        if discriminator is None:
            self.set_discriminator()
        else:
            self._discriminator = discriminator

        self.seed = self._random_seed

        self._ret = {}

    @property
    def seed(self):
        """ returns seed """
        return self._random_seed

    @seed.setter
    def seed(self, s):
        """
        Sets the random seed for QGAN and updates the aqua_globals seed
        at the same time

        Args:
            s (int): random seed
        """
        self._random_seed = s
        aqua_globals.random_seed = self._random_seed
        self._discriminator.set_seed(self._random_seed)

    @property
    def tol_rel_ent(self):
        """ returns tolerance for relative entropy """
        return self._tol_rel_ent

    @tol_rel_ent.setter
    def tol_rel_ent(self, t):
        """
        Set tolerance for relative entropy

        Args:
            t (float): or None, Set tolerance level for relative entropy.
                If the training achieves relative
                entropy equal or lower than tolerance it finishes.
        """
        self._tol_rel_ent = t

    @property
    def generator(self):
        """ returns generator """
        return self._generator

    # pylint: disable=unused-argument
    def set_generator(self, generator_circuit=None,
                      generator_init_params=None, generator_optimizer=None):
        """
        Initialize generator.

        Args:
            generator_circuit (VariationalForm): parameterized quantum circuit which sets
                the structure of the quantum generator
            generator_init_params(numpy.ndarray): initial parameters for the generator circuit
            generator_optimizer (Optimizer): optimizer to be used for the training of the generator
        """
        self._generator = QuantumGenerator(self._bounds, self._num_qubits,
                                           generator_circuit, generator_init_params,
                                           self._snapshot_dir)

    @property
    def discriminator(self):
        """ returns discriminator """
        return self._discriminator

    def set_discriminator(self, discriminator=None):
        """
        Initialize discriminator.

        Args:
            discriminator (Discriminator): discriminator
        """

        if discriminator is None:
            self._discriminator = NumpyDiscriminator(len(self._num_qubits))
        else:
            self._discriminator = discriminator
        self._discriminator.set_seed(self._random_seed)

    @property
    def g_loss(self):
        """ returns g loss """
        return self._g_loss

    @property
    def d_loss(self):
        """ returns d loss """
        return self._d_loss

    @property
    def rel_entr(self):
        """ returns relative entropy """
        return self._rel_entr

    def get_rel_entr(self):
        """ get relative entropy """
        samples_gen, prob_gen = self._generator.get_output(self._quantum_instance)
        temp = np.zeros(len(self._grid_elements))
        for j, sample in enumerate(samples_gen):
            for i, element in enumerate(self._grid_elements):
                if sample == element:
                    temp[i] += prob_gen[j]
        prob_gen = temp
        prob_gen = [1e-8 if x == 0 else x for x in prob_gen]
        rel_entr = entropy(prob_gen, self._prob_data)
        return rel_entr

    def _store_params(self, e, d_loss, g_loss, rel_entr):
        with open(os.path.join(self._snapshot_dir, 'output.csv'), mode='a') as csv_file:
            fieldnames = ['epoch', 'loss_discriminator',
                          'loss_generator', 'params_generator', 'rel_entropy']
            writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
            writer.writerow({'epoch': e, 'loss_discriminator': np.average(d_loss),
                             'loss_generator': np.average(g_loss), 'params_generator':
                                 self._generator.generator_circuit.params, 'rel_entropy': rel_entr})
        self._discriminator.save_model(self._snapshot_dir)  # Store discriminator model

    def train(self):
        """
        Train the qGAN
        """
        if self._snapshot_dir is not None:
            with open(os.path.join(self._snapshot_dir, 'output.csv'), mode='w') as csv_file:
                fieldnames = ['epoch', 'loss_discriminator', 'loss_generator', 'params_generator',
                              'rel_entropy']
                writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
                writer.writeheader()

        for e in range(self._num_epochs):
            aqua_globals.random.shuffle(self._data)
            index = 0
            while (index+self._batch_size) <= len(self._data):
                real_batch = self._data[index: index+self._batch_size]
                index += self._batch_size
                generated_batch, generated_prob = self._generator.get_output(self._quantum_instance,
                                                                             shots=self._batch_size)

                # 1. Train Discriminator
                ret_d = self._discriminator.train([real_batch, generated_batch],
                                                  [np.ones(len(real_batch))/len(real_batch),
                                                   generated_prob])
                d_loss_min = ret_d['loss']

                # 2. Train Generator
                self._generator.set_discriminator(self._discriminator)
                ret_g = self._generator.train(self._quantum_instance, shots=self._batch_size)
                g_loss_min = ret_g['loss']

            self._d_loss.append(np.around(float(d_loss_min), 4))
            self._g_loss.append(np.around(g_loss_min, 4))

            rel_entr = self.get_rel_entr()
            self._rel_entr.append(np.around(rel_entr, 4))
            self._ret['params_d'] = ret_d['params']
            self._ret['params_g'] = ret_g['params']
            self._ret['loss_d'] = np.around(float(d_loss_min), 4)
            self._ret['loss_g'] = np.around(g_loss_min, 4)
            self._ret['rel_entr'] = np.around(rel_entr, 4)

            if self._snapshot_dir is not None:
                self._store_params(e, np.around(d_loss_min, 4),
                                   np.around(g_loss_min, 4), np.around(rel_entr, 4))
            logger.debug('Epoch %s/%s...', e + 1, self._num_epochs)
            logger.debug('Loss Discriminator: %s', np.around(float(d_loss_min), 4))
            logger.debug('Loss Generator: %s', np.around(g_loss_min, 4))
            logger.debug('Relative Entropy: %s', np.around(rel_entr, 4))

            if self._tol_rel_ent is not None:
                if rel_entr <= self._tol_rel_ent:
                    break

    def _run(self):
        """
        Run qGAN training

        Returns:
            dict: with generator(discriminator) parameters & loss, relative entropy
        Raises:
            AquaError: invalid backend
        """
        if self._quantum_instance.backend_name == ('unitary_simulator' or 'clifford_simulator'):
            raise AquaError(
                'Chosen backend not supported - '
                'Set backend either to statevector_simulator, qasm_simulator'
                ' or actual quantum hardware')
        self.train()

        return self._ret
Пример #6
0
class QGAN(QuantumAlgorithm):
    """
    Quantum Generative Adversarial Network.

    """
    CONFIGURATION = {
        'name':
        'QGAN',
        'description':
        'Quantum Generative Adversarial Network',
        'input_schema': {
            '$schema': 'http://json-schema.org/draft-07/schema#',
            'id': 'Qgan_schema',
            'type': 'object',
            'properties': {
                'num_qubits': {
                    'type': ['array', 'null'],
                    'default': None
                },
                'batch_size': {
                    'type': 'integer',
                    'default': 500,
                    'minimum': 1
                },
                'num_epochs': {
                    'type': 'integer',
                    'default': 3000
                },
                'seed': {
                    'type': ['integer'],
                    'default': 7
                },
                'tol_rel_ent': {
                    'type': ['number', 'null'],
                    'default': None
                },
                'snapshot_dir': {
                    'type': ['string', 'null'],
                    'default': None
                }
            },
            'additionalProperties': False
        },
        'problems': ['distribution_learning_loading'],
        'depends': [
            {
                'pluggable_type': 'generative_network',
                'default': {
                    'name': 'QuantumGenerator'
                }
            },
            {
                'pluggable_type': 'discriminative_network',
                'default': {
                    'name': 'NumpyDiscriminator'
                }
            },
        ],
    }

    def __init__(self,
                 data,
                 bounds=None,
                 num_qubits=None,
                 batch_size=500,
                 num_epochs=3000,
                 seed=7,
                 discriminator=None,
                 generator=None,
                 tol_rel_ent=None,
                 snapshot_dir=None):
        """
        Initialize qGAN.
        Args:
            data (np.ndarray): training data of dimension k
            bounds (np.ndarray):  k min/max data values [[min_0,max_0],...,[min_k-1,max_k-1]]
                        if univariate data: [min_0,max_0]
            num_qubits (np.ndarray): k numbers of qubits to determine representation resolution,
                                    i.e. n qubits enable the representation of 2**n values
                                    [num_qubits_0,..., num_qubits_k-1]
            batch_size (int): batch size
            num_epochs (int): number of training epochs
            seed (int): seed
            discriminator (NeuralNetwork): discriminates between real and fake data samples
            generator (NeuralNetwork): generates 'fake' data samples
            tol_rel_ent (Union(float, None)): Set tolerance level for relative entropy.
                                     If the training achieves relative
            entropy equal or lower than tolerance it finishes.
            snapshot_dir (Union(str, None)): path or None, if path given store cvs file
                                      with parameters to the directory
        Raises:
            AquaError: invalid input
        """

        self.validate(locals())
        super().__init__()
        if data is None:
            raise AquaError('Training data not given.')
        self._data = np.array(data)
        if bounds is None:
            bounds_min = np.percentile(self._data, 5, axis=0)
            bounds_max = np.percentile(self._data, 95, axis=0)
            bounds = []
            for i, _ in enumerate(bounds_min):
                bounds.append([bounds_min[i], bounds_max[i]])
        if np.ndim(data) > 1:
            if len(bounds) != (len(num_qubits) or len(data[0])):
                raise AquaError(
                    'Dimensions of the data, the length of the data bounds '
                    'and the numbers of qubits per '
                    'dimension are incompatible.')
        else:
            if (np.ndim(bounds) or len(num_qubits)) != 1:
                raise AquaError(
                    'Dimensions of the data, the length of the data bounds '
                    'and the numbers of qubits per '
                    'dimension are incompatible.')
        self._bounds = np.array(bounds)
        self._num_qubits = num_qubits
        # pylint: disable=unsubscriptable-object
        if np.ndim(data) > 1:
            if self._num_qubits is None:
                self._num_qubits = np.ones[len(data[0])] * 3
            self._prob_data = \
                np.zeros(int(np.prod(np.power(np.ones(len(self._data[0]))*2, self._num_qubits))))
        else:
            if self._num_qubits is None:
                self._num_qubits = np.array([3])
            self._prob_data = np.zeros(
                int(np.prod(np.power(np.array([2]), self._num_qubits))))
        self._data_grid = []
        self._grid_elements = None
        self._prepare_data()
        self._batch_size = batch_size
        self._num_epochs = num_epochs
        self._snapshot_dir = snapshot_dir
        self._g_loss = []
        self._d_loss = []
        self._rel_entr = []
        self._tol_rel_ent = tol_rel_ent

        self._random_seed = seed

        if generator is None:
            self.set_generator()
        else:
            self._generator = generator
        if discriminator is None:
            self.set_discriminator()
        else:
            self._discriminator = discriminator

        self.seed = self._random_seed

        self._ret = {}

    @classmethod
    def init_params(cls, params, algo_input):
        """
        Initialize qGAN via parameters dictionary and algorithm input instance.
        Args:
            params (dict): parameters dictionary
            algo_input (AlgorithmInput): Input instance
        Returns:
            QGAN: qgan object
        Raises:
            AquaError: invalid input
        """

        if algo_input is None:
            raise AquaError("Input instance not supported.")

        qgan_params = params.get(Pluggable.SECTION_KEY_ALGORITHM)
        num_qubits = qgan_params.get('num_qubits')
        batch_size = qgan_params.get('batch_size')
        num_epochs = qgan_params.get('num_epochs')
        seed = qgan_params.get('seed')
        tol_rel_ent = qgan_params.get('tol_rel_ent')
        snapshot_dir = qgan_params.get('snapshot_dir')

        discriminator_params = params.get(
            Pluggable.SECTION_KEY_DISCRIMINATIVE_NET)
        generator_params = params.get(Pluggable.SECTION_KEY_GENERATIVE_NETWORK)
        generator_params['num_qubits'] = num_qubits

        discriminator = get_pluggable_class(
            PluggableType.DISCRIMINATIVE_NETWORK,
            discriminator_params['name']).init_params(params)
        generator = get_pluggable_class(
            PluggableType.GENERATIVE_NETWORK,
            generator_params['name']).init_params(params)

        return cls(algo_input.data, algo_input.bounds, num_qubits, batch_size,
                   num_epochs, seed, discriminator, generator, tol_rel_ent,
                   snapshot_dir)

    @property
    def seed(self):
        """ returns seed """
        return self._random_seed

    @seed.setter
    def seed(self, s):
        """
        Args:
            s (int): random seed

        Returns:

        """
        self._random_seed = s
        aqua_globals.random_seed = self._random_seed
        self._discriminator.set_seed(self._random_seed)

    @property
    def tol_rel_ent(self):
        """ returns tolerance for relative entropy """
        return self._tol_rel_ent

    @tol_rel_ent.setter
    def tol_rel_ent(self, t):
        """
        Set tolerance for relative entropy
        Args:
            t (float): or None, Set tolerance level for relative entropy.
                If the training achieves relative
                entropy equal or lower than tolerance it finishes.
        """
        self._tol_rel_ent = t

    @property
    def generator(self):
        """ returns generator """
        return self._generator

    # pylint: disable=unused-argument
    def set_generator(self,
                      generator_circuit=None,
                      generator_init_params=None,
                      generator_optimizer=None):
        """
        Initialize generator.
        Args:
            generator_circuit (VariationalForm): parameterized quantum circuit which sets
                                the structure of the quantum generator
            generator_init_params(numpy.ndarray): initial parameters for the generator circuit
            generator_optimizer (Optimizer): optimizer to be used for the training of the generator
        """
        self._generator = QuantumGenerator(self._bounds, self._num_qubits,
                                           generator_circuit,
                                           generator_init_params,
                                           self._snapshot_dir)

    @property
    def discriminator(self):
        """ returns discriminator """
        return self._discriminator

    def set_discriminator(self, discriminator=None):
        """
        Initialize discriminator.

        Args:
            discriminator (Discriminator): discriminator
        """

        if discriminator is None:
            self._discriminator = NumpyDiscriminator(len(self._num_qubits))
        else:
            self._discriminator = discriminator
        self._discriminator.set_seed(self._random_seed)

    @property
    def g_loss(self):
        """ returns g loss """
        return self._g_loss

    @property
    def d_loss(self):
        """ returns d loss """
        return self._d_loss

    @property
    def rel_entr(self):
        """ returns relative entropy """
        return self._rel_entr

    def _prepare_data(self):
        """
        Discretize and truncate the input data such that it
        is compatible wih the chosen data resolution.
        """
        # Truncate the data
        if np.ndim(self._bounds) == 1:
            bounds = np.reshape(self._bounds, (1, len(self._bounds)))
        else:
            bounds = self._bounds
        self._data = self._data.reshape(
            (len(self._data), len(self._num_qubits)))
        temp = []
        for i, data_sample in enumerate(self._data):
            append = True
            for j, entry in enumerate(data_sample):
                if entry < bounds[j, 0]:
                    append = False
                if entry > bounds[j, 1]:
                    append = False
            if append:
                temp.append(list(data_sample))
        self._data = np.array(temp)

        # Fit the data to the data resolution. i.e. grid
        for j, prec in enumerate(self._num_qubits):
            data_row = self._data[:, j]  # dim j of all data samples
            # prepare data grid for dim j
            grid = np.linspace(bounds[j, 0], bounds[j, 1], (2**prec))
            # find index for data sample in grid
            index_grid = np.searchsorted(grid,
                                         data_row - (grid[1] - grid[0]) * 0.5)
            for k, index in enumerate(index_grid):
                self._data[k, j] = grid[index]
            if j == 0:
                if len(self._num_qubits) > 1:
                    self._data_grid = [grid]
                else:
                    self._data_grid = grid
                self._grid_elements = grid
            elif j == 1:
                self._data_grid.append(grid)
                temp = []
                for g_e in self._grid_elements:
                    for g in grid:
                        temp0 = [g_e]
                        temp0.append(g)
                        temp.append(temp0)
                self._grid_elements = temp
            else:
                self._data_grid.append(grid)
                temp = []
                for g_e in self._grid_elements:
                    for g in grid:
                        temp0 = deepcopy(g_e)
                        temp0.append(g)
                        temp.append(temp0)
                self._grid_elements = deepcopy(temp)
        self._data_grid = np.array(self._data_grid)
        self._data = np.reshape(self._data,
                                (len(self._data), len(self._data[0])))
        for data in self._data:
            for i, element in enumerate(self._grid_elements):
                if all(data == element):
                    self._prob_data[i] += 1 / len(self._data)
        self._prob_data = [1e-10 if x == 0 else x for x in self._prob_data]

    def get_rel_entr(self):
        """ get relative entropy """
        samples_gen, prob_gen = self._generator.get_output(
            self._quantum_instance)
        temp = np.zeros(len(self._grid_elements))
        for j, sample in enumerate(samples_gen):
            for i, element in enumerate(self._grid_elements):
                if all(sample == element):
                    temp[i] += prob_gen[j]
        prob_gen = temp
        prob_gen = [1e-8 if x == 0 else x for x in prob_gen]
        rel_entr = entropy(prob_gen, self._prob_data)
        return rel_entr

    def _store_params(self, e, d_loss, g_loss, rel_entr):
        with open(os.path.join(self._snapshot_dir, 'output.csv'),
                  mode='a') as csv_file:
            fieldnames = [
                'epoch', 'loss_discriminator', 'loss_generator',
                'params_generator', 'rel_entropy'
            ]
            writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
            writer.writerow({
                'epoch':
                e,
                'loss_discriminator':
                np.average(d_loss),
                'loss_generator':
                np.average(g_loss),
                'params_generator':
                self._generator.generator_circuit.params,
                'rel_entropy':
                rel_entr
            })
        self._discriminator.save_model(
            self._snapshot_dir)  # Store discriminator model

    def train(self):
        """
        Train the qGAN
        """
        if self._snapshot_dir is not None:
            with open(os.path.join(self._snapshot_dir, 'output.csv'),
                      mode='w') as csv_file:
                fieldnames = [
                    'epoch', 'loss_discriminator', 'loss_generator',
                    'params_generator', 'rel_entropy'
                ]
                writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
                writer.writeheader()

        for e in range(self._num_epochs):
            aqua_globals.random.shuffle(self._data)
            index = 0
            while (index + self._batch_size) <= len(self._data):
                real_batch = self._data[index:index + self._batch_size]
                index += self._batch_size
                generated_batch, generated_prob = self._generator.get_output(
                    self._quantum_instance, shots=self._batch_size)

                # 1. Train Discriminator
                ret_d = self._discriminator.train(
                    [real_batch, generated_batch], [
                        np.ones(len(real_batch)) / len(real_batch),
                        generated_prob
                    ])
                d_loss_min = ret_d['loss']

                # 2. Train Generator
                self._generator.set_discriminator(self._discriminator)
                ret_g = self._generator.train(self._quantum_instance,
                                              shots=self._batch_size)
                g_loss_min = ret_g['loss']

            self._d_loss.append(np.around(float(d_loss_min), 4))
            self._g_loss.append(np.around(g_loss_min, 4))

            rel_entr = self.get_rel_entr()
            self._rel_entr.append(np.around(rel_entr, 4))
            self._ret['params_d'] = ret_d['params']
            self._ret['params_g'] = ret_g['params']
            self._ret['loss_d'] = np.around(float(d_loss_min), 4)
            self._ret['loss_g'] = np.around(g_loss_min, 4)
            self._ret['rel_entr'] = np.around(rel_entr, 4)

            if self._snapshot_dir is not None:
                self._store_params(e, np.around(d_loss_min, 4),
                                   np.around(g_loss_min, 4),
                                   np.around(rel_entr, 4))
            logger.debug('Epoch %s/%s...', e + 1, self._num_epochs)
            logger.debug('Loss Discriminator: %s',
                         np.around(float(d_loss_min), 4))
            logger.debug('Loss Generator: %s', np.around(g_loss_min, 4))
            logger.debug('Relative Entropy: %s', np.around(rel_entr, 4))

            if self._tol_rel_ent is not None:
                if rel_entr <= self._tol_rel_ent:
                    break

    def _run(self):
        """
        Run qGAN training
        Returns: dict, with generator(discriminator) parameters & loss, relative entropy

        """
        if self._quantum_instance.backend_name == ('unitary_simulator'
                                                   or 'clifford_simulator'):
            raise AquaError(
                'Chosen backend not supported - '
                'Set backend either to statevector_simulator, qasm_simulator'
                ' or actual quantum hardware')
        self.train()

        return self._ret
Пример #7
0
class QGAN(QuantumAlgorithm):
    """The Quantum Generative Adversarial Network algorithm.

    The qGAN [1] is a hybrid quantum-classical algorithm used for generative modeling tasks.

    This adaptive algorithm uses the interplay of a generative
    :class:`~qiskit.aqua.components.neural_networks.GenerativeNetwork` and a
    discriminative :class:`~qiskit.aqua.components.neural_networks.DiscriminativeNetwork`
    network to learn the probability distribution underlying given training data.

    These networks are trained in alternating optimization steps, where the discriminator tries to
    differentiate between training data samples and data samples from the generator and the
    generator aims at generating samples which the discriminator classifies as training data
    samples. Eventually, the quantum generator learns the training data's underlying probability
    distribution. The trained quantum generator loads a quantum state which is a model of the
    target distribution.

    **References:**

    [1] Zoufal et al.,
        `Quantum Generative Adversarial Networks for learning and loading random distributions
        <https://www.nature.com/articles/s41534-019-0223-2>`_
    """
    def __init__(
        self,
        data: np.ndarray,
        bounds: Optional[np.ndarray] = None,
        num_qubits: Optional[np.ndarray] = None,
        batch_size: int = 500,
        num_epochs: int = 3000,
        seed: int = 7,
        discriminator: Optional[DiscriminativeNetwork] = None,
        generator: Optional[GenerativeNetwork] = None,
        tol_rel_ent: Optional[float] = None,
        snapshot_dir: Optional[str] = None,
        quantum_instance: Optional[Union[QuantumInstance, BaseBackend]] = None
    ) -> None:
        """

        Args:
            data: Training data of dimension k
            bounds: k min/max data values [[min_0,max_0],...,[min_k-1,max_k-1]]
                if univariate data: [min_0,max_0]
            num_qubits: k numbers of qubits to determine representation resolution,
                i.e. n qubits enable the representation of 2**n values
                [num_qubits_0,..., num_qubits_k-1]
            batch_size: Batch size, has a min. value of 1.
            num_epochs: Number of training epochs
            seed: Random number seed
            discriminator: Discriminates between real and fake data samples
            generator: Generates 'fake' data samples
            tol_rel_ent: Set tolerance level for relative entropy.
                If the training achieves relative entropy equal or lower than tolerance it finishes.
            snapshot_dir: Directory in to which to store cvs file with parameters,
                if None (default) then no cvs file is created.
            quantum_instance: Quantum Instance or Backend
        Raises:
            AquaError: invalid input
        """
        validate_min('batch_size', batch_size, 1)
        super().__init__(quantum_instance)
        if data is None:
            raise AquaError('Training data not given.')
        self._data = np.array(data)
        if bounds is None:
            bounds_min = np.percentile(self._data, 5, axis=0)
            bounds_max = np.percentile(self._data, 95, axis=0)
            bounds = []
            for i, _ in enumerate(bounds_min):
                bounds.append([bounds_min[i], bounds_max[i]])
        if np.ndim(data) > 1:
            if len(bounds) != (len(num_qubits) or len(data[0])):
                raise AquaError(
                    'Dimensions of the data, the length of the data bounds '
                    'and the numbers of qubits per '
                    'dimension are incompatible.')
        else:
            if (np.ndim(bounds) or len(num_qubits)) != 1:
                raise AquaError(
                    'Dimensions of the data, the length of the data bounds '
                    'and the numbers of qubits per '
                    'dimension are incompatible.')
        self._bounds = np.array(bounds)
        self._num_qubits = num_qubits
        # pylint: disable=unsubscriptable-object
        if np.ndim(data) > 1:
            if self._num_qubits is None:
                self._num_qubits = np.ones[len(data[0])] * 3
        else:
            if self._num_qubits is None:
                self._num_qubits = np.array([3])
        self._data, self._data_grid, self._grid_elements, self._prob_data = \
            discretize_and_truncate(self._data, self._bounds, self._num_qubits,
                                    return_data_grid_elements=True,
                                    return_prob=True, prob_non_zero=True)
        self._batch_size = batch_size
        self._num_epochs = num_epochs
        self._snapshot_dir = snapshot_dir
        self._g_loss = []  # type: List[float]
        self._d_loss = []  # type: List[float]
        self._rel_entr = []  # type: List[float]
        self._tol_rel_ent = tol_rel_ent

        self._random_seed = seed

        if generator is None:
            self.set_generator()
        else:
            self._generator = generator
        if discriminator is None:
            self.set_discriminator()
        else:
            self._discriminator = discriminator

        self.seed = self._random_seed

        self._ret = {}  # type: Dict[str, Any]

    @property
    def seed(self):
        """ Returns random seed """
        return self._random_seed

    @seed.setter
    def seed(self, s):
        """
        Sets the random seed for QGAN and updates the aqua_globals seed
        at the same time

        Args:
            s (int): random seed
        """
        self._random_seed = s
        aqua_globals.random_seed = self._random_seed
        self._discriminator.set_seed(self._random_seed)

    @property
    def tol_rel_ent(self):
        """ Returns tolerance for relative entropy """
        return self._tol_rel_ent

    @tol_rel_ent.setter
    def tol_rel_ent(self, t):
        """
        Set tolerance for relative entropy

        Args:
            t (float): or None, Set tolerance level for relative entropy.
                If the training achieves relative
                entropy equal or lower than tolerance it finishes.
        """
        self._tol_rel_ent = t

    @property
    def generator(self):
        """ Returns generator """
        return self._generator

    # pylint: disable=unused-argument
    def set_generator(self,
                      generator_circuit: Optional[Union[
                          QuantumCircuit, UnivariateVariationalDistribution,
                          MultivariateVariationalDistribution]] = None,
                      generator_init_params: Optional[np.ndarray] = None,
                      generator_optimizer: Optional[Optimizer] = None):
        """Initialize generator.

        Args:
            generator_circuit: parameterized quantum circuit which sets
                the structure of the quantum generator
            generator_init_params: initial parameters for the generator circuit
            generator_optimizer: optimizer to be used for the training of the generator
        """
        self._generator = QuantumGenerator(self._bounds, self._num_qubits,
                                           generator_circuit,
                                           generator_init_params,
                                           self._snapshot_dir)

    @property
    def discriminator(self):
        """ Returns discriminator """
        return self._discriminator

    def set_discriminator(self, discriminator=None):
        """
        Initialize discriminator.

        Args:
            discriminator (Discriminator): discriminator
        """

        if discriminator is None:
            self._discriminator = NumPyDiscriminator(len(self._num_qubits))
        else:
            self._discriminator = discriminator
        self._discriminator.set_seed(self._random_seed)

    @property
    def g_loss(self) -> List[float]:
        """ Returns generator loss """
        return self._g_loss

    @property
    def d_loss(self) -> List[float]:
        """ Returns discriminator loss """
        return self._d_loss

    @property
    def rel_entr(self) -> List[float]:
        """ Returns relative entropy between target and trained distribution """
        return self._rel_entr

    def get_rel_entr(self) -> float:
        """ Get relative entropy between target and trained distribution """
        samples_gen, prob_gen = self._generator.get_output(
            self._quantum_instance)
        temp = np.zeros(len(self._grid_elements))
        for j, sample in enumerate(samples_gen):
            for i, element in enumerate(self._grid_elements):
                if sample == element:
                    temp[i] += prob_gen[j]
        prob_gen = temp
        prob_gen = [1e-8 if x == 0 else x for x in prob_gen]
        rel_entr = entropy(prob_gen, self._prob_data)
        return rel_entr

    def _store_params(self, e, d_loss, g_loss, rel_entr):
        with open(os.path.join(self._snapshot_dir, 'output.csv'),
                  mode='a') as csv_file:
            fieldnames = [
                'epoch', 'loss_discriminator', 'loss_generator',
                'params_generator', 'rel_entropy'
            ]
            writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
            writer.writerow({
                'epoch':
                e,
                'loss_discriminator':
                np.average(d_loss),
                'loss_generator':
                np.average(g_loss),
                'params_generator':
                self._generator.generator_circuit.params,
                'rel_entropy':
                rel_entr
            })
        self._discriminator.save_model(
            self._snapshot_dir)  # Store discriminator model

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

        Raises:
            AquaError: Batch size bigger than the number of items in the truncated data set
        """
        if self._snapshot_dir is not None:
            with open(os.path.join(self._snapshot_dir, 'output.csv'),
                      mode='w') as csv_file:
                fieldnames = [
                    'epoch', 'loss_discriminator', 'loss_generator',
                    'params_generator', 'rel_entropy'
                ]
                writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
                writer.writeheader()

        if len(self._data) < self._batch_size:
            raise AquaError('The batch size needs to be less than the '
                            'truncated data size of {}'.format(len(
                                self._data)))

        for e in range(self._num_epochs):
            aqua_globals.random.shuffle(self._data)
            index = 0
            while (index + self._batch_size) <= len(self._data):
                real_batch = self._data[index:index + self._batch_size]
                index += self._batch_size
                generated_batch, generated_prob = self._generator.get_output(
                    self._quantum_instance, shots=self._batch_size)

                # 1. Train Discriminator
                ret_d = self._discriminator.train(
                    [real_batch, generated_batch], [
                        np.ones(len(real_batch)) / len(real_batch),
                        generated_prob
                    ])
                d_loss_min = ret_d['loss']

                # 2. Train Generator
                self._generator.set_discriminator(self._discriminator)
                ret_g = self._generator.train(self._quantum_instance,
                                              shots=self._batch_size)
                g_loss_min = ret_g['loss']

            self._d_loss.append(np.around(float(d_loss_min), 4))
            self._g_loss.append(np.around(g_loss_min, 4))

            rel_entr = self.get_rel_entr()
            self._rel_entr.append(np.around(rel_entr, 4))
            self._ret['params_d'] = ret_d['params']
            self._ret['params_g'] = ret_g['params']
            self._ret['loss_d'] = np.around(float(d_loss_min), 4)
            self._ret['loss_g'] = np.around(g_loss_min, 4)
            self._ret['rel_entr'] = np.around(rel_entr, 4)

            if self._snapshot_dir is not None:
                self._store_params(e, np.around(d_loss_min, 4),
                                   np.around(g_loss_min, 4),
                                   np.around(rel_entr, 4))
            logger.debug('Epoch %s/%s...', e + 1, self._num_epochs)
            logger.debug('Loss Discriminator: %s',
                         np.around(float(d_loss_min), 4))
            logger.debug('Loss Generator: %s', np.around(g_loss_min, 4))
            logger.debug('Relative Entropy: %s', np.around(rel_entr, 4))

            if self._tol_rel_ent is not None:
                if rel_entr <= self._tol_rel_ent:
                    break

    def _run(self):
        """
        Run qGAN training

        Returns:
            dict: with generator(discriminator) parameters & loss, relative entropy
        Raises:
            AquaError: invalid backend
        """
        if self._quantum_instance.backend_name == ('unitary_simulator'
                                                   or 'clifford_simulator'):
            raise AquaError(
                'Chosen backend not supported - '
                'Set backend either to statevector_simulator, qasm_simulator'
                ' or actual quantum hardware')
        self.train()

        return self._ret
Пример #8
0
class IQGAN(QuantumAlgorithm):
    """Incremental Quantum Generative Adversarial Network.

    The code is forked from Zoufal et al.,
        `Quantum Generative Adversarial Networks for learning and loading random distributions
        <https://www.nature.com/articles/s41534-019-0223-2>`_
    """
    def __init__(
        self,
        initial_data: np.ndarray,
        target_rel_ent: float,
        num_qubits: np.ndarray,
        bounds: np.ndarray,
        batch_size: int = 500,
        seed: int = 7,
        freq_storage: bool = False,
        max_data_length: Optional[int] = None,
        discriminator: Optional[DiscriminativeNetwork] = None,
        generator: Optional[GenerativeNetwork] = None,
        verbose: bool = False,
        prob_data_real: Optional[np.ndarray] = None,
        snapshot_dir: Optional[str] = None,
        quantum_instance: Optional[Union[QuantumInstance, BaseBackend,
                                         Backend]] = None
    ) -> None:
        """
        Args:
            initial_data: Initial training data of dimension k
            target_rel_ent: Set target level for relative entropy.
                If the training achieves relative entropy equal or lower than tolerance it finishes.
            num_qubits: k numbers of qubits to determine representation resolution,
                i.e. n qubits enable the representation of 2**n values
                [num_qubits_0,..., num_qubits_k-1]
            bounds: k min/max data values [[min_0,max_0],...,[min_k-1,max_k-1]]
                if univariate data: [min_0,max_0]
            batch_size: Batch size, has a min. value of 1.
            seed: Random number seed
            freq_storage: Flag indicating if storing only data probabilities is enabled.
                Probability distribution will store all passed data. No concept shift can be learned in this case.
            max_data_length: Maximum length of data array.
                When this length is exceeded, the oldest samples will be removed.
            discriminator: Discriminates between real and fake data samples
            generator: Generates 'fake' data samples
            verbose: Additional output on training process.
            prob_data_real: Unknown target data probabilities.
                Used only for real relative entropy output.
            snapshot_dir: Directory in to which to store cvs file with parameters,
                if None (default) then no cvs file is created.
            quantum_instance: Quantum Instance or Backend
        Raises:
            AquaError: invalid input
        """
        validate_min('batch_size', batch_size, 1)
        super().__init__(quantum_instance)
        if initial_data is None:
            raise AquaError('Initial training data not given.')

        self._stop_training = False
        self._training_thread = threading.Thread(target=self._train,
                                                 daemon=True)
        self._epoch = 0

        self._bounds = np.array(bounds)
        self._num_qubits = num_qubits
        self._batch_size = batch_size
        self._freq_storage = freq_storage
        self._verbose = verbose
        self._snapshot_dir = snapshot_dir
        self._g_loss = []  # type: List[float]
        self._d_loss = []  # type: List[float]
        self._rel_entr = []  # type: List[float]
        self._target_rel_ent = target_rel_ent
        self._max_data_length = max_data_length
        self._random_seed = seed
        self._training_handler = None

        if prob_data_real is not None:
            self._prob_data_real = prob_data_real
            self._rel_entr_real = []
            self._rel_entr_data = []
        else:
            self._prob_data_real = None
            self._rel_entr_real = None
            self._rel_entr_data = None

        if self._freq_storage:
            self._data = []
            self._grid_elements = []
            self._prob_data = []
            self._prob_data_length = 0
            self._update(np.array(initial_data))
        else:
            self._data = np.array(initial_data)
            self._update([])

        if generator is None:
            self.set_generator()
        else:
            self._generator = generator

        if discriminator is None:
            self.set_discriminator()
        else:
            self._discriminator = discriminator

        self.seed = self._random_seed

        self._ret = {}  # type: Dict[str, Any]

    @property
    def epoch(self):
        """ Return current epoch number """
        return self._epoch

    def set_training_handler(self, func):
        """  Setup function for handling every training epoch with `epoch: int` input argument """
        self._training_handler = func

    @property
    def seed(self):
        """ Returns random seed """
        return self._random_seed

    @seed.setter
    def seed(self, s):
        """
        Sets the random seed for QGAN and updates the aqua_globals seed
        at the same time

        Args:
            s (int): random seed
        """
        self._random_seed = s
        aqua_globals.random_seed = self._random_seed
        self._discriminator.set_seed(self._random_seed)

    @property
    def target_rel_ent(self):
        """ Returns tolerance for relative entropy """
        return self._target_rel_ent

    @target_rel_ent.setter
    def target_rel_ent(self, t):
        """
        Set target for relative entropy

        Args:
            t (float): or None, Set tolerance level for relative entropy.
                If the training achieves relative
                entropy equal or lower than tolerance it finishes.
        """
        self._target_rel_ent = t

    @property
    def generator(self):
        """ Returns generator """
        return self._generator

    # pylint: disable=unused-argument
    def set_generator(self,
                      generator_circuit: Optional[Union[
                          QuantumCircuit, UnivariateVariationalDistribution,
                          MultivariateVariationalDistribution]] = None,
                      generator_init_params: Optional[np.ndarray] = None,
                      generator_optimizer: Optional[Optimizer] = None):
        """Initialize generator.

        Args:
            generator_circuit: parameterized quantum circuit which sets
                the structure of the quantum generator
            generator_init_params: initial parameters for the generator circuit
            generator_optimizer: optimizer to be used for the training of the generator
        """
        self._generator = QuantumGenerator(self._bounds, self._num_qubits,
                                           generator_circuit,
                                           generator_init_params,
                                           generator_optimizer,
                                           self._snapshot_dir)

    @property
    def discriminator(self):
        """ Returns discriminator """
        return self._discriminator

    def set_discriminator(self, discriminator=None):
        """
        Initialize discriminator.

        Args:
            discriminator (Discriminator): discriminator
        """
        if discriminator is None:
            self._discriminator = NumPyDiscriminator(len(self._num_qubits))
        else:
            self._discriminator = discriminator
        self._discriminator.set_seed(self._random_seed)

    @property
    def g_loss(self) -> List[float]:
        """ Returns generator loss """
        return self._g_loss

    @property
    def d_loss(self) -> List[float]:
        """ Returns discriminator loss """
        return self._d_loss

    @property
    def rel_entr(self) -> List[float]:
        """ Returns relative entropy between target and trained distribution """
        return self._rel_entr

    def get_rel_entr(self) -> float:
        """ Get relative entropy between target and trained distribution """
        samples_gen, prob_gen = self._generator.get_output(
            self._quantum_instance)
        temp = np.zeros(len(self._grid_elements))
        for j, sample in enumerate(samples_gen):
            for i, element in enumerate(self._grid_elements):
                if sample == element:
                    temp[i] += prob_gen[j]
        prob_gen = temp
        print('Generator parameters: ', self._generator._bound_parameters)
        print('Generated probabilities: ', prob_gen)
        prob_gen = [1e-8 if x == 0 else x for x in prob_gen]
        rel_entr = entropy(prob_gen, self._prob_data)
        print('')
        return rel_entr

    @property
    def rel_entr_real(self) -> List[float]:
        """ Returns relative entropy between unknown target and trained distribution """
        return self._rel_entr_real

    def get_rel_entr_real(self) -> float:
        """ Get relative entropy between unknown target and trained distribution """
        samples_gen, prob_gen = self._generator.get_output(
            self._quantum_instance)
        temp = np.zeros(len(self._grid_elements))
        for j, sample in enumerate(samples_gen):
            for i, element in enumerate(self._grid_elements):
                if sample == element:
                    temp[i] += prob_gen[j]
        prob_gen = temp
        prob_gen = [1e-8 if x == 0 else x for x in prob_gen]
        rel_entr = entropy(prob_gen, self._prob_data_real)
        return rel_entr

    @property
    def rel_entr_data(self) -> List[float]:
        """ Returns relative entropy between unknown target and known data """
        return self._rel_entr_data

    def get_rel_entr_data(self) -> float:
        """ Get relative entropy between unknown target and known data """
        rel_entr = entropy(self._prob_data, self._prob_data_real)
        return rel_entr

    def _update(self, data: np.ndarray):
        print('Updating data...')
        if self._freq_storage:
            print('Old grid elements: ', self._grid_elements)
            print('Old data probabilities: ', self._prob_data)
            print('Old data count: ', self._prob_data_length)
            print('New data count: ', len(data))
            print('New data: ', data)
            new_data_length = len(data)
            _, _, new_grid_elements, new_prob_data = \
                discretize_and_truncate(data, self._bounds, self._num_qubits,
                                        return_data_grid_elements=True,
                                        return_prob=True, prob_non_zero=True)
            # Common merged grid elements
            temp_grid_elements = np.unique(
                np.concatenate((self._grid_elements, new_grid_elements), 0))
            temp_prob_data = np.zeros(len(temp_grid_elements))
            for j, sample in enumerate(temp_grid_elements):
                for i, element in enumerate(self._grid_elements):
                    if sample == element:
                        temp_prob_data[
                            j] += self._prob_data[i] * self._prob_data_length
                        break
                for i, element in enumerate(new_grid_elements):
                    if sample == element:
                        temp_prob_data[j] += new_prob_data[i] * new_data_length
                        break
                # Normalize data
                temp_prob_data[j] /= (self._prob_data_length + new_data_length)
            self._prob_data_length += new_data_length
            self._grid_elements = temp_grid_elements
            self._prob_data = temp_prob_data
            print('Processed data count: ', self._prob_data_length)
            print('Processed grid elements: ', self._grid_elements)
            print('Processed data probabilities: ', self._prob_data)
            if self._prob_data_real is not None:
                print('Unknown real data probabilities: ',
                      self._prob_data_real)
            print('')
        else:
            print('Old data count: ', len(self._data))
            print('New data count: ', len(data))
            print('New data: ', data)
            if self._max_data_length is None:
                self._data = np.append(self._data, np.array(data))
            else:
                elements_left = self._max_data_length - len(data)
                if elements_left > 0:
                    self._data = np.append(self._data[-elements_left:],
                                           np.array(data))
                else:
                    self._data = np.array(data[-self._max_data_length:])
            self._data, _, self._grid_elements, self._prob_data = \
                discretize_and_truncate(self._data, self._bounds, self._num_qubits,
                                        return_data_grid_elements=True,
                                        return_prob=True, prob_non_zero=True)
            print('Processed data count: ', len(self._data))
            print('Processed grid elements: ', self._grid_elements)
            print('Processed data probabilities: ', self._prob_data)
            if self._prob_data_real is not None:
                print('Unknown real data probabilities: ',
                      self._prob_data_real)
            print('')

    def update(self, data: np.ndarray, target_rel_ent: Optional[float] = None):
        self.stop_training()
        self._update(data)
        if target_rel_ent is not None:
            self._target_rel_ent = target_rel_ent
        current_rel_entr = self.get_rel_entr()
        print('Current relative entropy: ', current_rel_entr)
        print('Target relative entropy: ', self._target_rel_ent)
        print('')
        if current_rel_entr > self._target_rel_ent:
            self.train()

    def stop_training(self):
        print('Stopping training...')
        self._stop_training = True
        self._training_thread.join()
        self._stop_training = False
        print('Training stopped')
        print('')

    def _store_params(self, e, d_loss, g_loss, rel_entr):
        with open(os.path.join(self._snapshot_dir, 'output.csv'),
                  mode='a') as csv_file:
            fieldnames = [
                'epoch', 'loss_discriminator', 'loss_generator',
                'params_generator', 'rel_entropy'
            ]
            writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
            writer.writerow({
                'epoch':
                e,
                'loss_discriminator':
                np.average(d_loss),
                'loss_generator':
                np.average(g_loss),
                'params_generator':
                self._generator.generator_circuit.params,
                'rel_entropy':
                rel_entr
            })
        self._discriminator.save_model(
            self._snapshot_dir)  # Store discriminator model

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

        Raises:
            AquaError: Batch size bigger than the number of items in the truncated data set
        """
        print('Training...')
        print('')
        if self._training_thread.is_alive():
            print('Training is already in process')
            print('')
        else:
            self._training_thread = threading.Thread(target=self._train,
                                                     daemon=True)
            self._training_thread.start()

    def _train(self):
        if self._snapshot_dir is not None:
            with open(os.path.join(self._snapshot_dir, 'output.csv'),
                      mode='w') as csv_file:
                fieldnames = [
                    'epoch', 'loss_discriminator', 'loss_generator',
                    'params_generator', 'rel_entropy'
                ]
                writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
                writer.writeheader()

        if len(self._data) < self._batch_size and not self._freq_storage:
            raise AquaError('The batch size needs to be less than the '
                            'truncated data size of {}'.format(len(
                                self._data)))

        if self._epoch == 0:
            # First training launch
            # Add initial relative entropy
            self._rel_entr.append(np.around(self.get_rel_entr(), 4))
            if self._rel_entr_real is not None:
                self._rel_entr_real.append(
                    np.around(self.get_rel_entr_real(), 4))
            if self._rel_entr_data is not None:
                self._rel_entr_data.append(
                    np.around(self.get_rel_entr_data(), 4))

        while True:
            training_data = np.array(self._data)
            aqua_globals.random.shuffle(training_data)
            index = 0
            while (not self._freq_storage and (index + self._batch_size) <= len(training_data)) \
                    or (self._freq_storage and index <= self._prob_data_length):
                if self._freq_storage:
                    real_batch = np.random.choice(self._grid_elements,
                                                  self._batch_size,
                                                  p=self._prob_data)
                else:
                    real_batch = training_data[index:index + self._batch_size]
                index += self._batch_size
                generated_batch, generated_prob = self._generator.get_output(
                    self._quantum_instance, shots=self._batch_size)

                if self._verbose:
                    #print('Real batch: ', real_batch)
                    print('Generated batch: ', generated_batch)
                    print('Generated probabilities: ', generated_prob)
                    print('Discriminator training...')
                # 1. Train Discriminator
                ret_d = self._discriminator.train(
                    [real_batch, generated_batch], [
                        np.ones(len(real_batch)) / len(real_batch),
                        generated_prob
                    ])
                d_loss_min = ret_d['loss']
                if self._verbose:
                    print('Discriminator loss: ', d_loss_min)
                    print('')
                    print('Generator training...')
                # 2. Train Generator
                self._generator.set_discriminator(self._discriminator)
                ret_g = self._generator.train(self._quantum_instance,
                                              shots=self._batch_size)
                g_loss_min = ret_g['loss']
                if self._verbose:
                    print('Generator loss: ', g_loss_min)
                    print('')

            self._d_loss.append(np.around(float(d_loss_min), 4))
            self._g_loss.append(np.around(g_loss_min, 4))

            rel_entr = self.get_rel_entr()
            rel_entr_real = None
            self._rel_entr.append(np.around(rel_entr, 4))
            if self._rel_entr_real is not None:
                rel_entr_real = self.get_rel_entr_real()
                self._rel_entr_real.append(np.around(rel_entr_real, 4))
            if self._rel_entr_data is not None:
                self._rel_entr_data.append(
                    np.around(self.get_rel_entr_data(), 4))
            self._ret['params_d'] = ret_d['params']
            self._ret['params_g'] = ret_g['params']
            self._ret['loss_d'] = np.around(float(d_loss_min), 4)
            self._ret['loss_g'] = np.around(g_loss_min, 4)
            self._ret['rel_entr'] = np.around(rel_entr, 4)
            self._epoch += 1

            if self._snapshot_dir is not None:
                self._store_params(self._epoch, np.around(d_loss_min, 4),
                                   np.around(g_loss_min, 4),
                                   np.around(rel_entr, 4))

            print('Epoch ', self._epoch)
            print('Loss Discriminator: ', np.around(float(d_loss_min), 4))
            print('Loss Generator: ', np.around(g_loss_min, 4))
            print('Relative Entropy: ', np.around(rel_entr, 4))
            if rel_entr_real is not None:
                print('Real Relative Entropy: ', np.around(rel_entr_real, 4))
            print('----------------------')
            print('')

            if self._training_handler is not None:
                thread = threading.Thread(target=self._training_handler,
                                          args=([self._epoch, rel_entr]))
                thread.daemon = True
                thread.start()
                thread.join(timeout=0.2)

            if rel_entr <= self._target_rel_ent:
                break

            if self._stop_training:
                self._stop_training = False
                break

    def _run(self):
        """
        Run qGAN training

        Returns:
            dict: with generator(discriminator) parameters & loss, relative entropy
        Raises:
            AquaError: invalid backend
        """
        if self._quantum_instance.backend_name == ('unitary_simulator'
                                                   or 'clifford_simulator'):
            raise AquaError(
                'Chosen backend not supported - '
                'Set backend either to statevector_simulator, qasm_simulator'
                ' or actual quantum hardware')
        self.train()
        self._training_thread.join()

        return self._ret

    @property
    def training_data(self):
        return self._data