0
# Multi-Output Gaussian Processes
1
2
Support for multi-output Gaussian processes using specialized kernel and mean functions that handle vector-valued processes and cross-covariances between different outputs. This enables modeling of correlated time series, multi-task learning, and vector-valued function approximation.
3
4
## Capabilities
5
6
### Multi-Output Kernels
7
8
Kernels that handle multiple output dimensions and cross-covariances between different outputs within a vector-valued process.
9
10
```python { .api }
11
class MultiOutputKernel:
12
def __init__(self, measure, *ps):
13
"""
14
Initialize multi-output kernel.
15
16
Parameters:
17
- measure: Measure to take kernels from
18
- *ps: Processes that make up multi-valued process
19
"""
20
21
measure: Measure # Measure to take kernels from
22
ps: Tuple[GP, ...] # Processes that make up multi-valued process
23
24
def render(self, formatter=None):
25
"""
26
Render kernel as string with optional formatting.
27
28
Parameters:
29
- formatter: Optional custom formatter function
30
31
Returns:
32
- str: String representation of the kernel
33
"""
34
```
35
36
### Multi-Output Means
37
38
Mean functions that handle multiple output dimensions and can compute mean values for vector-valued processes.
39
40
```python { .api }
41
class MultiOutputMean:
42
def __init__(self, measure, *ps):
43
"""
44
Initialize multi-output mean.
45
46
Parameters:
47
- measure: Measure to take means from
48
- *ps: Processes that make up multi-valued process
49
"""
50
51
def __call__(self, x):
52
"""
53
Evaluate mean at input locations.
54
55
Parameters:
56
- x: Input locations to evaluate at
57
58
Returns:
59
- Mean values for all outputs at input locations
60
"""
61
62
measure: Measure # Measure to take means from
63
ps: Tuple[GP, ...] # Processes that make up multi-valued process
64
65
def render(self, formatter=None):
66
"""
67
Render mean as string with optional formatting.
68
69
Parameters:
70
- formatter: Optional custom formatter function
71
72
Returns:
73
- str: String representation of the mean
74
"""
75
```
76
77
### Ambiguous Dimensionality Kernels
78
79
Special kernel type for cases where the output dimensionality cannot be automatically inferred and must be specified explicitly.
80
81
```python { .api }
82
class AmbiguousDimensionalityKernel:
83
"""
84
Kernel whose dimensionality cannot be inferred automatically.
85
Used as base class for kernels requiring explicit dimensionality specification.
86
"""
87
```
88
89
### Utility Functions
90
91
Helper functions for working with multi-output kernels and inferring their properties.
92
93
```python { .api }
94
def infer_size(kernel, x):
95
"""
96
Infer size of kernel evaluated at input.
97
98
Parameters:
99
- kernel: Kernel to evaluate
100
- x: Input to evaluate kernel at
101
102
Returns:
103
- int: Inferred size of kernel output
104
"""
105
106
def dimensionality(kernel):
107
"""
108
Infer output dimensionality of kernel.
109
110
Parameters:
111
- kernel: Kernel to analyze
112
113
Returns:
114
- int: Output dimensionality of kernel
115
"""
116
```
117
118
## Usage Examples
119
120
### Basic Multi-Output GP Construction
121
122
```python
123
import stheno
124
import numpy as np
125
126
# Create individual processes for each output
127
temp_gp = stheno.GP(kernel=stheno.EQ().stretch(2.0), name="temperature")
128
pressure_gp = stheno.GP(kernel=stheno.Matern52().stretch(1.5), name="pressure")
129
humidity_gp = stheno.GP(kernel=stheno.EQ().stretch(0.8), name="humidity")
130
131
# Create multi-output process using cross product
132
multi_gp = stheno.cross(temp_gp, pressure_gp, humidity_gp)
133
134
# Evaluate at input points
135
x = np.linspace(0, 10, 50)
136
multi_fdd = multi_gp(x)
137
138
print(f"Multi-output FDD shape: {multi_fdd.mean.shape}") # Should be (150, 1) for 3×50
139
```
140
141
### Correlated Outputs with Shared Components
142
143
```python
144
# Create shared latent process
145
latent = stheno.GP(kernel=stheno.EQ().stretch(3.0), name="latent")
146
147
# Create correlated outputs
148
output1 = latent + 0.5 * stheno.GP(kernel=stheno.Matern32(), name="output1_noise")
149
output2 = 0.8 * latent + stheno.GP(kernel=stheno.EQ().stretch(0.5), name="output2_noise")
150
output3 = -0.6 * latent + stheno.GP(kernel=stheno.Linear(), name="output3_noise")
151
152
# Name the outputs
153
measure = stheno.Measure()
154
with measure:
155
measure.name(output1, "output1")
156
measure.name(output2, "output2")
157
measure.name(output3, "output3")
158
159
# Create multi-output process
160
correlated_multi = stheno.cross(output1, output2, output3)
161
162
# Sample to see correlations
163
x = np.linspace(0, 5, 30)
164
samples = correlated_multi(x).sample(3)
165
166
# Reshape to see individual outputs
167
n_outputs = 3
168
n_points = len(x)
169
reshaped_samples = samples.reshape(n_outputs, n_points, -1)
170
171
print(f"Output 1 correlation with Output 2: {np.corrcoef(reshaped_samples[0,:,0], reshaped_samples[1,:,0])[0,1]:.3f}")
172
```
173
174
### Multi-Task Learning
175
176
```python
177
# Multi-task regression with shared and task-specific components
178
shared_component = stheno.GP(kernel=stheno.EQ().stretch(2.0), name="shared")
179
180
# Task-specific components
181
task1_specific = stheno.GP(kernel=stheno.Matern52().stretch(0.5), name="task1_specific")
182
task2_specific = stheno.GP(kernel=stheno.Matern52().stretch(0.8), name="task2_specific")
183
task3_specific = stheno.GP(kernel=stheno.Matern52().stretch(1.2), name="task3_specific")
184
185
# Combine shared and specific components
186
task1 = shared_component + 0.5 * task1_specific
187
task2 = shared_component + 0.3 * task2_specific
188
task3 = shared_component + 0.7 * task3_specific
189
190
# Create multi-task GP
191
multi_task_gp = stheno.cross(task1, task2, task3)
192
193
# Generate synthetic multi-task data
194
x_train = np.random.uniform(0, 5, 40)
195
y_train = multi_task_gp(x_train).sample()
196
197
# Condition on observations for all tasks
198
obs = stheno.Observations(multi_task_gp(x_train), y_train)
199
posterior_multi = multi_task_gp.condition(obs)
200
201
# Make predictions
202
x_test = np.linspace(0, 5, 50)
203
predictions = posterior_multi(x_test)
204
mean, lower, upper = predictions.marginal_credible_bounds()
205
206
# Extract predictions for each task
207
n_tasks = 3
208
pred_mean_per_task = mean.reshape(n_tasks, len(x_test))
209
```
210
211
### Custom Multi-Output Kernels
212
213
```python
214
# Create measure to manage multi-output relationships
215
measure = stheno.Measure()
216
217
# Define individual processes
218
process1 = stheno.GP(name="proc1")
219
process2 = stheno.GP(name="proc2")
220
221
with measure:
222
# Add processes with custom kernel relationships
223
measure.add_independent_gp(process1, stheno.ZeroMean(), stheno.EQ())
224
measure.add_independent_gp(process2, stheno.ZeroMean(), stheno.Matern52())
225
226
# Create multi-output kernel
227
multi_kernel = stheno.MultiOutputKernel(measure, process1, process2)
228
multi_mean = stheno.MultiOutputMean(measure, process1, process2)
229
230
print(f"Multi-output kernel: {multi_kernel.render()}")
231
print(f"Multi-output mean: {multi_mean.render()}")
232
233
# Use in GP construction
234
combined_gp = stheno.GP(mean=multi_mean, kernel=multi_kernel, measure=measure)
235
```
236
237
### Working with Different Output Dimensions
238
239
```python
240
# Processes with different natural scales
241
small_scale = stheno.GP(kernel=stheno.EQ().stretch(0.1), name="small")
242
medium_scale = stheno.GP(kernel=stheno.EQ().stretch(1.0), name="medium")
243
large_scale = stheno.GP(kernel=stheno.EQ().stretch(10.0), name="large")
244
245
# Multi-scale multi-output process
246
multi_scale = stheno.cross(small_scale, medium_scale, large_scale)
247
248
# Evaluate and check dimensionality
249
x = np.linspace(0, 1, 20)
250
fdd = multi_scale(x)
251
252
# Use dimensionality inference
253
kernel_dim = stheno.dimensionality(multi_scale.kernel)
254
inferred_size = stheno.infer_size(multi_scale.kernel, x)
255
256
print(f"Kernel dimensionality: {kernel_dim}")
257
print(f"Inferred size at input: {inferred_size}")
258
print(f"Actual FDD mean shape: {fdd.mean.shape}")
259
```
260
261
### Sparse Multi-Output GPs
262
263
```python
264
# Large-scale multi-output problem
265
n_outputs = 4
266
n_obs_per_output = 200
267
n_inducing = 30
268
269
# Create individual output processes
270
outputs = []
271
for i in range(n_outputs):
272
gp = stheno.GP(
273
kernel=stheno.EQ().stretch(np.random.uniform(0.5, 2.0)),
274
name=f"output_{i}"
275
)
276
outputs.append(gp)
277
278
# Create multi-output process
279
multi_output = stheno.cross(*outputs)
280
281
# Generate observations
282
x_obs = np.random.uniform(0, 10, n_obs_per_output)
283
y_obs = multi_output(x_obs).sample()
284
285
# Inducing points shared across outputs
286
x_inducing = np.linspace(0, 10, n_inducing)
287
u_multi = multi_output(x_inducing)
288
289
# Create sparse observations
290
sparse_obs = stheno.PseudoObservations(
291
u_multi,
292
multi_output(x_obs),
293
y_obs
294
)
295
296
# Condition using sparse approximation
297
posterior_sparse = multi_output.condition(sparse_obs)
298
299
# Evaluate ELBO for the approximation
300
elbo = sparse_obs.elbo(multi_output.measure)
301
print(f"Multi-output sparse GP ELBO: {elbo:.3f}")
302
```
303
304
### Independent vs Correlated Outputs
305
306
```python
307
# Independent outputs (no cross-correlations)
308
indep1 = stheno.GP(kernel=stheno.EQ(), name="independent1")
309
indep2 = stheno.GP(kernel=stheno.Matern52(), name="independent2")
310
independent_multi = stheno.cross(indep1, indep2)
311
312
# Correlated outputs (shared component)
313
shared = stheno.GP(kernel=stheno.EQ().stretch(2.0), name="shared")
314
corr1 = shared + 0.5 * stheno.GP(kernel=stheno.Matern32(), name="corr1_noise")
315
corr2 = shared + 0.3 * stheno.GP(kernel=stheno.Matern32(), name="corr2_noise")
316
correlated_multi = stheno.cross(corr1, corr2)
317
318
# Compare samples
319
x = np.linspace(0, 5, 50)
320
321
indep_samples = independent_multi(x).sample()
322
corr_samples = correlated_multi(x).sample()
323
324
# Reshape to analyze correlations
325
indep_reshaped = indep_samples.reshape(2, len(x))
326
corr_reshaped = corr_samples.reshape(2, len(x))
327
328
indep_correlation = np.corrcoef(indep_reshaped[0], indep_reshaped[1])[0,1]
329
corr_correlation = np.corrcoef(corr_reshaped[0], corr_reshaped[1])[0,1]
330
331
print(f"Independent outputs correlation: {indep_correlation:.3f}")
332
print(f"Correlated outputs correlation: {corr_correlation:.3f}")
333
```