Files
attentive-neural-processes/src/data/gp_curves.py
T
2019-11-02 11:50:39 +08:00

196 lines
7.2 KiB
Python

import pandas as pd
import numpy as np
import collections
import torch
# The (A)NP takes as input a `NPRegressionDescription` namedtuple with fields:
# `query`: a tuple containing ((context_x, context_y), target_x)
# `target_y`: a tensor containing the ground truth for the targets to be
# predicted
# `num_total_points`: A vector containing a scalar that describes the total
# number of datapoints used (context + target)
# `num_context_points`: A vector containing a scalar that describes the number
# of datapoints used as context
# The GPCurvesReader returns the newly sampled data in this format at each
# iteration
NPRegressionDescription = collections.namedtuple(
"NPRegressionDescription",
("query", "target_y", "num_total_points", "num_context_points"),
)
class GPCurvesReader(object):
"""Generates curves using a Gaussian Process (GP).
Supports vector inputs (x) and vector outputs (y). Kernel is
mean-squared exponential, using the x-value l2 coordinate distance scaled by
some factor chosen randomly in a range. Outputs are independent gaussian
processes.
"""
def __init__(
self,
batch_size,
max_num_context,
x_size=1,
y_size=1,
l1_scale=0.6,
sigma_scale=1.0,
random_kernel_parameters=True,
testing=False,
):
"""Creates a regression dataset of functions sampled from a GP.
Args:
batch_size: An integer.
max_num_context: The max number of observations in the context.
x_size: Integer >= 1 for length of "x values" vector.
y_size: Integer >= 1 for length of "y values" vector.
l1_scale: Float; typical scale for kernel distance function.
sigma_scale: Float; typical scale for variance.
random_kernel_parameters: If `True`, the kernel parameters (l1 and sigma)
will be sampled uniformly within [0.1, l1_scale] and [0.1, sigma_scale].
testing: Boolean that indicates whether we are testing. If so there are
more targets for visualization.
"""
self._batch_size = batch_size
self._max_num_context = max_num_context
self._x_size = x_size
self._y_size = y_size
self._l1_scale = l1_scale
self._sigma_scale = sigma_scale
self._random_kernel_parameters = random_kernel_parameters
self._testing = testing
def _gaussian_kernel(self, xdata, l1, sigma_f, sigma_noise=2e-2):
"""Applies the Gaussian kernel to generate curve data.
Args:
xdata: Tensor of shape [B, num_total_points, x_size] with
the values of the x-axis data.
l1: Tensor of shape [B, y_size, x_size], the scale
parameter of the Gaussian kernel.
sigma_f: Tensor of shape [B, y_size], the magnitude
of the std.
sigma_noise: Float, std of the noise that we add for stability.
Returns:
The kernel, a float tensor of shape
[B, y_size, num_total_points, num_total_points].
"""
num_total_points = xdata.shape[1]
# Expand and take the difference
xdata1 = xdata.unsqueeze(1) # [B, 1, num_total_points, x_size]
xdata2 = xdata.unsqueeze(2) # [B, num_total_points, 1, x_size]
diff = xdata1 - xdata2 # [B, num_total_points, num_total_points, x_size]
# [B, y_size, num_total_points, num_total_points, x_size]
norm = (diff[:, None, :, :, :] / l1[:, :, None, None, :]) ** 2
norm = torch.sum(norm, -1) # [B, data_size, num_total_points, num_total_points]
# [B, y_size, num_total_points, num_total_points]
kernel = ((sigma_f) ** 2)[:, :, None, None] * torch.exp(-0.5 * norm)
# Add some noise to the diagonal to make the cholesky work.
kernel += (sigma_noise ** 2) * torch.eye(num_total_points)
return kernel
def generate_curves(self):
"""Builds the op delivering the data.
Generated functions are `float32` with x values between -2 and 2.
Returns:
A `CNPRegressionDescription` namedtuple.
"""
num_context = int(np.random.rand() * (self._max_num_context - 3) + 3)
# If we are testing we want to have more targets and have them evenly
# distributed in order to plot the function.
if self._testing:
num_target = 400
num_total_points = num_target
x_values = (
torch.arange(-2, 2, 1.0 / 100).unsqueeze(0).repeat(self._batch_size, 1)
)
x_values = x_values.unsqueeze(-1)
# During training the number of target points and their x-positions are
# selected at random
else:
num_target = int(np.random.rand() * (self._max_num_context - num_context))
num_total_points = num_context + num_target
x_values = (
torch.rand((self._batch_size, num_total_points, self._x_size)) * 4 - 2
)
# Set kernel parameters
# Either choose a set of random parameters for the mini-batch
if self._random_kernel_parameters:
l1 = (
torch.rand((self._batch_size, self._y_size, self._x_size))
* (self._l1_scale - 0.1)
+ 0.1
)
sigma_f = (
torch.rand((self._batch_size, self._y_size)) * (self._sigma_scale - 0.1)
+ 0.1
)
# Or use the same fixed parameters for all mini-batches
else:
l1 = (
torch.ones((self._batch_size, self._y_size, self._x_size))
* self._l1_scale
)
sigma_f = torch.ones((self._batch_size, self._y_size)) * self._sigma_scale
# Pass the x_values through the Gaussian kernel
# [batch_size, y_size, num_total_points, num_total_points]
kernel = self._gaussian_kernel(x_values, l1, sigma_f)
# Calculate Cholesky, using double precision for better stability:
cholesky = torch.cholesky(kernel)
# Sample a curve
# [batch_size, y_size, num_total_points, 1]
y_values = torch.matmul(
cholesky, torch.randn((self._batch_size, self._y_size, num_total_points, 1))
)
# [batch_size, num_total_points, y_size]
y_values = y_values.squeeze(3)
y_values = y_values.permute(0, 2, 1)
if self._testing:
# Select the targets
target_x = x_values
target_y = y_values
# Select the observations
idx = torch.randperm(num_target)
context_x = x_values[:, idx[:num_context]]
context_y = y_values[:, idx[:num_context]]
else:
# Select the targets which will consist of the context points as well as
# some new target points
target_x = x_values[:, : num_target + num_context, :]
target_y = y_values[:, : num_target + num_context, :]
# Select the observations
context_x = x_values[:, :num_context, :]
context_y = y_values[:, :num_context, :]
query = ((context_x, context_y), target_x)
return NPRegressionDescription(
query=query,
target_y=target_y,
num_total_points=target_x.shape[1],
num_context_points=num_context,
)