Skip to content

Commit 8409794

Browse files
authored
Merge pull request #42 from probsys/20251217-fsaad-sop-times
20251217 fsaad sop times
2 parents a3058ad + fb85924 commit 8409794

11 files changed

Lines changed: 94 additions & 77 deletions

docs/src/tutorials/decomposition.ipynb

Lines changed: 35 additions & 42 deletions
Large diffs are not rendered by default.

docs/src/tutorials/decomposition.md

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ ax.scatter(df_test.ds, df_test.y, marker="o", color="w", edgecolor="k", label="T
6060

6161

6262

63-
Python: <matplotlib.collections.PathCollection object at 0x7367187bbe30>
63+
Python: <matplotlib.collections.PathCollection object at 0x7ddc363cdb80>
6464

6565

6666

@@ -109,7 +109,7 @@ ax.scatter(df_test.ds, df_test.y, marker="o", color="w", edgecolor="k", label="T
109109

110110

111111

112-
Python: <matplotlib.collections.PathCollection object at 0x7367183731a0>
112+
Python: <matplotlib.collections.PathCollection object at 0x7ddc35e42880>
113113

114114

115115

@@ -628,7 +628,7 @@ for (ax, m, f) in zip(axes, [model_lin, model_per, model_ge], [forecasts_lin, fo
628628
end
629629
axes[0].set_title("STRUCTURE: LIN")
630630
axes[1].set_title("STRUCTURE: PER")
631-
axes[2].set_title("STRUCTURE: GE")
631+
axes[2].set_title("STRUCTURE: GE");
632632
```
633633

634634

@@ -637,12 +637,6 @@ axes[2].set_title("STRUCTURE: GE")
637637

638638

639639

640-
641-
642-
643-
644-
645-
646640
## Sum-of-Products Decomposition
647641

648642
An third approach to decomposing kernels is using [`AutoGP.split_kernel_sop`](@ref), which is based on a sum-of-products decomposition of kernels.
@@ -724,18 +718,20 @@ nrows = AutoGP.num_particles(model)
724718
fig, axes = PythonPlot.subplots(ncols=3, nrows=nrows, figsize=(26,5*nrows))
725719
for particle=1:AutoGP.num_particles(model)
726720
f = forecasts_sum[forecasts_sum.particle .== particle,:]
727-
for (component, ax) in zip(0:2, axes[particle-1])
721+
for (component, ax) in zip([0,2,1], axes[particle-1])
728722
subdf = f[f.component .== component,:]
729723
ax.plot(subdf[!,"ds"], subdf[!,"y_mean"], color="k", linewidth=1)
730724
ax.fill_between(subdf.ds, subdf[!,"y_0.025"], subdf[!,"y_0.975"]; color="tab:blue", alpha=0.5)
731-
ax.scatter(df_train.ds, df_train.y, marker="o", color="k", label="Observed Data")
732-
ax.scatter(df_test.ds, df_test.y, marker="o", color="w", edgecolor="k", label="Test Data")
725+
if component in [0,2]
726+
ax.scatter(df_train.ds, df_train.y, marker="o", color="k", label="Observed Data")
727+
ax.scatter(df_test.ds, df_test.y, marker="o", color="w", edgecolor="k", label="Test Data")
728+
ax.set_ylim([5000, 7500])
729+
end
733730
w = @sprintf("%1.2f", maximum(subdf.weight))
734731
ax.set_title("Particle $(particle) - Component $(title[component+1]) - Weight $(w)")
735-
ax.set_ylim([5000, 7500])
736732
end
737733
end
738-
fig.savefig("forecasts-joint.png", dpi=150)
734+
fig.savefig("forecasts-joint.png", dpi=150);
739735
```
740736

741737

@@ -744,27 +740,24 @@ fig.savefig("forecasts-joint.png", dpi=150)
744740

745741

746742

747-
748-
749-
750-
751-
752-
753743
Using [`AutoGP.GP.predict_mvn_sum`](@ref) gives the overall mixture of Gaussians.
754744

755745

756746
```julia
757747
ds_probe = vcat(df_train.ds, df_test.ds)
758748
mvn, indexes = AutoGP.predict_mvn_sum(model, ds_probe, AutoGP.GP.Periodic);
759749

760-
overall_predictions = mean(mvn)[indexes.Y]
761-
seasonal_predictions = mean(mvn)[indexes.F[1]]
762-
nonseasonal_predictions = mean(mvn)[indexes.F[2]]
763-
fig, ax = PythonPlot.subplots(figsize=(10,6))
764-
ax.plot(ds_probe, seasonal_predictions, marker=".", color="k")
765-
ax.plot(ds_probe, nonseasonal_predictions, marker=".", color="k")
766-
ax.scatter(df_train.ds, df_train.y, marker=".")
767-
ax.scatter(df_test.ds, df_test.y, marker="o", edgecolor="k", facecolor="w")
750+
overall_predictions = mean(mvn.components[1])[indexes.Y]
751+
seasonal_predictions = mean(mvn.components[1])[indexes.F[1]]
752+
nonseasonal_predictions = mean(mvn.components[1])[indexes.F[2]]
753+
754+
fig, ax = PythonPlot.subplots(figsize=(18,6), ncols=2)
755+
ax[0].plot(ds_probe, nonseasonal_predictions, marker=".", color="k")
756+
ax[0].plot(ds_probe, nonseasonal_predictions .+ seasonal_predictions , marker=".", color="k")
757+
ax[0].scatter(df_train.ds, df_train.y, marker=".")
758+
ax[0].scatter(df_test.ds, df_test.y, marker="o", edgecolor="k", facecolor="w");
759+
760+
ax[1].plot(ds_probe, seasonal_predictions, marker=".", color="k")
768761
```
769762

770763

@@ -776,5 +769,11 @@ ax.scatter(df_test.ds, df_test.y, marker="o", edgecolor="k", facecolor="w")
776769

777770

778771

772+
Python: [<matplotlib.lines.Line2D object at 0x7ddb977b57c0>]
773+
779774

780775

776+
777+
```julia
778+
779+
```
-75 Bytes
Loading
-2.11 KB
Loading
-599 Bytes
Loading
575 KB
Loading
35.2 KB
Loading
75 Bytes
Loading

src/GP.jl

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,8 @@ julia> split_kernel_sop(l*p + l*c, Periodic)
596596
(l*p, l*c)
597597
julia> split_kernel_sop(p*p, Periodic)
598598
(p*p, Constant(0))
599+
julia> split_kernel_sop((l+p)*(l+p), Periodic)
600+
(p*l+p*p+l*p, l*l)
599601
```
600602
"""
601603
function split_kernel_sop(node::Node, ::Type{T}) where {T<:LeafNode}
@@ -613,7 +615,19 @@ split_kernel_sop_helper(node::T, ::Type{T}) where {T<:LeafNode} = (node, nothing
613615
split_kernel_sop_helper(node::LeafNode, ::Type{T}) where {T<:LeafNode} = (nothing, node)
614616

615617
function split_kernel_sop_helper(node::Times, ::Type{T}) where {T<:LeafNode}
616-
return (has_leaf(node.left, T) || has_leaf(node.right, T)) ? (node, nothing) : (nothing, node)
618+
(left_a, left_b) = split_kernel_sop_helper(node.left, T)
619+
(right_a, right_b) = split_kernel_sop_helper(node.right, T)
620+
mult(a, b) = (isnothing(a) || isnothing(b)) ? nothing : a * b
621+
terms = [
622+
mult(left_a, right_a),
623+
mult(left_a, right_b),
624+
mult(left_b, right_a),
625+
mult(left_b, right_b),
626+
]
627+
l_sop = merge_split_operand(node.left + node.right, terms[1], terms[2])
628+
l_sop = merge_split_operand(node.left + node.right, l_sop, terms[3])
629+
r_sop = terms[4]
630+
return (l_sop, r_sop)
617631
end
618632

619633
function split_kernel_sop_helper(node::B, ::Type{T}) where {B<:BinaryOpNode, T <: LeafNode}
@@ -625,7 +639,7 @@ function split_kernel_sop_helper(node::B, ::Type{T}) where {B<:BinaryOpNode, T <
625639
end
626640

627641
# Helper function for split_kernel_sop_helper
628-
merge_split_operand(node::Plus, node_a, node_b) = @match (node_a, node_b) begin
642+
merge_split_operand(::Plus, node_a, node_b) = @match (node_a, node_b) begin
629643
(::Nothing, ::Nothing) => nothing
630644
(::Node, ::Nothing) => node_a
631645
(::Nothing, ::Node) => node_b

src/api.jl

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,11 +1006,17 @@ function predict_mvn_sum(
10061006
noise_pred=noise_pred)
10071007
mu = Distributions.mean(mvn.mvn)
10081008
cov = Distributions.cov(mvn.mvn)
1009-
# TODO: We must only include the mean in at most ONE of the two
1010-
# components. For now, the mean is included in all three.
1009+
# Transform to data space.
10111010
mu, cov = Transforms.unapply_mean_var(model.y_transform, mu, cov)
1012-
# Remove the mean from the first latent function.
1013-
# mu[mvn.indexes.F[1]] .+= model.y_transform.intercept / model.y_transform.slope
1011+
# Avoid double counting the linear offset by removing it from the first
1012+
# component. Derivation, where Y is data space and X is model space:
1013+
# X = aY + b
1014+
# X = F1 + F2
1015+
# Y = (X-b)/a = F1/a + F2/a -b/a
1016+
# However, using unapply_mean gives
1017+
# Y' = (F1-b)/a + (F2-b)/a = F1/a - b/a + F2/a - b/a
1018+
# Therefore Y' is double counting by b/a, which we add to F1.
1019+
mu[mvn.indexes.F[1]] .+= model.y_transform.intercept / model.y_transform.slope
10141020
distributions[particle] = MvNormal(mu, cov)
10151021
# Set the indexes.
10161022
if !isnothing(indexes[1])

0 commit comments

Comments
 (0)