Skip to content

Commit 53a615a

Browse files
author
Alexey Stukalov
committed
docs: sync with Sem refactor
1 parent afac0b4 commit 53a615a

9 files changed

Lines changed: 91 additions & 108 deletions

File tree

docs/src/developer/implied.md

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ end
1313
and a method to update!:
1414

1515
```julia
16-
import StructuralEquationModels: objective!
16+
import StructuralEquationModels: update!
1717

18-
function update!(targets::EvaluationTargets, implied::MyImplied, model::AbstractSemSingle, params)
18+
function update!(targets::EvaluationTargets, implied::MyImplied, params)
1919

2020
if is_objective_required(targets)
2121
...
@@ -31,11 +31,9 @@ function update!(targets::EvaluationTargets, implied::MyImplied, model::Abstract
3131
end
3232
```
3333

34-
As you can see, `update` gets passed as a first argument `targets`, which is telling us whether the objective value, gradient, and/or hessian are needed.
34+
As you can see, `update!` gets passed as a first argument `targets`, which is telling us whether the objective value, gradient, and/or hessian are needed.
3535
We can then use the functions `is_..._required` and conditional on what the optimizer needs, we can compute and store things we want to make available to the loss functions. For example, as we have seen in [Second example - maximum likelihood](@ref), the `RAM` implied type computes the model-implied covariance matrix and makes it available via `implied.Σ`.
3636

37-
38-
3937
Just as described in [Custom loss functions](@ref), you may define a constructor. Typically, this will depend on the `specification = ...` argument that can be a `ParameterTable` or a `RAMMatrices` object.
4038

4139
We implement an `ImpliedEmpty` type in our package that does nothing but serving as an `implied` field in case you are using a loss function that does not need any implied type at all. You may use it as a template for defining your own implied type, as it also shows how to handle the specification objects:
@@ -56,7 +54,7 @@ Empty placeholder for models that don't need an implied part.
5654
- `specification`: either a `RAMMatrices` or `ParameterTable` object
5755
5856
# Examples
59-
A multigroup model with ridge regularization could be specified as a `SemEnsemble` with one
57+
A multigroup model with ridge regularization could be specified as a `Sem` with one
6058
model per group and an additional model with `ImpliedEmpty` and `SemRidge` for the regularization part.
6159
6260
# Extended help
@@ -75,26 +73,20 @@ end
7573
### Constructors
7674
############################################################################################
7775

78-
function ImpliedEmpty(;
79-
specification,
80-
meanstruct = NoMeanStruct(),
81-
hessianeval = ExactHessian(),
76+
function ImpliedEmpty(
77+
spec::SemSpecification;
78+
hessianeval::HessianApprox = ExactHessian(),
8279
kwargs...,
8380
)
84-
return ImpliedEmpty(hessianeval, meanstruct, convert(RAMMatrices, specification))
81+
ram_matrices = convert(RAMMatrices, spec)
82+
return ImpliedEmpty(hessianeval, MeanStruct(ram_matrices), ram_matrices)
8583
end
8684

8785
############################################################################################
8886
### methods
8987
############################################################################################
9088

91-
update!(targets::EvaluationTargets, implied::ImpliedEmpty, par, model) = nothing
92-
93-
############################################################################################
94-
### Recommended methods
95-
############################################################################################
96-
97-
update_observed(implied::ImpliedEmpty, observed::SemObserved; kwargs...) = implied
89+
update!(targets::EvaluationTargets, implied::ImpliedEmpty, par) = nothing
9890
```
9991

100-
As you see, similar to [Custom loss functions](@ref) we implement a method for `update_observed`.
92+
As you see, similar to [Custom loss functions](@ref) we implement a constructor.

docs/src/developer/loss.md

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ Since we allow for the optimization of sums of loss functions, and the maximum l
1111
using StructuralEquationModels
1212
```
1313

14-
To define a new loss function, you have to define a new type that is a subtype of `SemLossFunction`:
14+
To define a new loss function, you have to define a new type that is a subtype of `AbstractLoss`:
1515
```@example loss
16-
struct Ridge <: SemLossFunction
16+
struct MyRidge <: AbstractLoss
1717
α
1818
I
1919
end
@@ -25,8 +25,8 @@ Additionaly, we need to define a *method* of the function `evaluate!` to compute
2525
```@example loss
2626
import StructuralEquationModels: evaluate!
2727
28-
evaluate!(objective::Number, gradient::Nothing, hessian::Nothing, ridge::Ridge, model::AbstractSem, par) =
29-
ridge.α * sum(i -> par[i]^2, ridge.I)
28+
evaluate!(objective::Number, gradient::Nothing, hessian::Nothing, ridge::MyRidge, par) =
29+
ridge.α * sum(i -> abs2(par[i]), ridge.I)
3030
```
3131

3232
The function `evaluate!` recognizes by the types of the arguments `objective`, `gradient` and `hessian` whether it should compute the objective value, gradient or hessian of the model w.r.t. the parameters.
@@ -98,7 +98,7 @@ function evaluate!(objective, gradient, hessian::Nothing, ridge::Ridge, model::A
9898
gradient[ridge.I] .= 2 * ridge.α * par[ridge.I]
9999
end
100100
# compute objective
101-
if !isnothing(objective)
101+
if !isnothing(objective)
102102
return ridge.α * sum(i -> par[i]^2, ridge.I)
103103
end
104104
end
@@ -166,17 +166,6 @@ end
166166

167167
## Additional functionality
168168

169-
### Update observed data
170-
171-
If you are planing a simulation study where you have to fit the **same model** to many **different datasets**, it is computationally beneficial to not build the whole model completely new everytime you change your data.
172-
Therefore, we provide a function to update the data of your model, `replace_observed(model(semfit); data = new_data)`. However, we can not know beforehand in what way your loss function depends on the specific datasets. The solution is to provide a method for `update_observed`. Since `Ridge` does not depend on the data at all, this is quite easy:
173-
174-
```julia
175-
import StructuralEquationModels: update_observed
176-
177-
update_observed(ridge::Ridge, observed::SemObserved; kwargs...) = ridge
178-
```
179-
180169
### Access additional information
181170

182171
If you want to provide a way to query information about loss functions of your type, you can provide functions for that:

docs/src/developer/optimizer.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ struct MyoptResult{O <: SemOptimizerMyopt} <: SEM.SemOptimizerResult{O}
2525
...
2626
end
2727

28-
############################################################################################
29-
### Recommended methods
30-
############################################################################################
31-
32-
update_observed(optimizer::SemOptimizerMyopt, observed::SemObserved; kwargs...) = optimizer
33-
3428
############################################################################################
3529
### additional methods
3630
############################################################################################
@@ -43,8 +37,6 @@ and `SEM.sem_optimizer_subtype(::Val{:Myopt})` returns `SemOptimizerMyopt`.
4337
This instructs *SEM.jl* to use `SemOptimizerMyopt` when `:Myopt` is specified as the engine for
4438
model fitting: `fit(..., engine = :Myopt)`.
4539

46-
A method for `update_observed` and additional methods might be usefull, but are not necessary.
47-
4840
Now comes the essential part: we need to provide the [`fit`](@ref) method with `SemOptimizerMyopt`
4941
as the first positional argument.
5042

docs/src/developer/sem.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# Custom model types
22

3-
The abstract supertype for all models is `AbstractSem`, which has two subtypes, `AbstractSemSingle{O, I, L}` and `AbstractSemCollection`. Currently, there are 2 subtypes of `AbstractSemSingle`: `Sem`, `SemFiniteDiff`. All subtypes of `AbstractSemSingle` should have at least observed, implied, loss and optimizer fields, and share their types (`{O, I, L}`) with the parametric abstract supertype. For example, the `SemFiniteDiff` type is implemented as
3+
The abstract supertype for all models is [`AbstractSem`](@ref). Currently, there are 2 concrete subtypes:
4+
`Sem{L <: Tuple}` and `SemFiniteDiff{S <: AbstractSem}`.
5+
A `Sem` model holds a tuple of `LossTerm`s (each wrapping an `AbstractLoss`) and a vector of parameter labels. Both single-group and multigroup models are represented as `Sem`.
6+
7+
`SemFiniteDiff` wraps any `AbstractSem` and substitutes dedicated gradient/hessian evaluation with finite difference approximation:
48

59
```julia
6-
struct SemFiniteDiff{O <: SemObserved, I <: SemImplied, L <: SemLoss} <:
7-
AbstractSemSingle{O, I, L}
8-
observed::O
9-
implied::I
10-
loss::L
10+
struct SemFiniteDiff{S <: AbstractSem} <: AbstractSem
11+
model::S
1112
end
1213
```
1314

@@ -17,6 +18,4 @@ Additionally, you can change how objective/gradient/hessian values are computed
1718
evaluate!(objective, gradient, hessian, model::SemFiniteDiff, params) = ...
1819
```
1920

20-
Additionally, we can define constructors like the one in `"src/frontend/specification/Sem.jl"`.
21-
22-
It is also possible to add new subtypes for `AbstractSemCollection`.
21+
Additionally, we can define constructors like the one in `"src/frontend/specification/Sem.jl"`.

docs/src/internals/types.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
The type hierarchy is implemented in `"src/types.jl"`.
44

5-
`AbstractSem`: the most abstract type in our package
6-
- `AbstractSemSingle{O, I, L} <: AbstractSem` is an abstract parametric type that is a supertype of all single models
7-
- `Sem`: models that do not need automatic differentiation or finite difference approximation
8-
- `SemFiniteDiff`: models whose gradients and/or hessians should be computed via finite difference approximation
9-
- `AbstractSemCollection <: AbstractSem` is an abstract supertype of all models that contain multiple `AbstractSem` submodels
5+
[`AbstractLoss`](@ref): is the base abstract type for all loss functions:
6+
- `SemLoss{O <: SemObserved, I <: SemImplied}`: is the subtype of `AbstractLoss`, which is the
7+
base for all SEM-specific loss functions ([`SemML`](@ref), [`SemWLS`](@ref) etc) that
8+
evaluate how closely the implied covariation structure (represented by the object of type `I`)
9+
matches the observed one (contained in the object of type `O`);
10+
- regularizing terms (e.g. [`SemRidge`](@ref)) are implemented as subtypes of `AbstractLoss`.
1011

11-
Every `AbstractSemSingle` has to have `SemObserved`, `SemImplied`, and `SemLoss` fields (and can have additional fields).
12-
13-
`SemLoss` is a container for multiple `SemLossFunctions`.
12+
[`AbstractSem`](@ref) is the base abstract type for all SEM models. It has two concrete subtypes:
13+
- `Sem{L <: Tuple} <: AbstractSem`: the main SEM model type that implements a list of weighted
14+
loss terms (using [`LossTerm`](@ref) wrapper around `AbstractLoss`) and allows modeling both single
15+
and multi-group SEMs and combining them with regularization terms.
16+
- `SemFiniteDiff{S <: AbstractSem} <: AbstractSem`: a wrapper around any `AbstractSem` that
17+
substitutes dedicated gradient/hessian evaluation with finite difference approximation.

docs/src/performance/mixed_differentiation.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,20 @@
22

33
This way of specifying our model is not ideal, however, because now also the maximum likelihood loss function lives inside a `SemFiniteDiff` model, and this means even though we have defined analytical gradients for it, we do not make use of them.
44

5-
A more efficient way is therefore to specify our model as an ensemble model:
5+
A more efficient way is therefore to specify our model as a combined model with multiple loss terms:
66

77
```julia
8-
model_ml = Sem(
9-
specification = partable,
10-
data = data,
11-
loss = SemML
8+
ml_term = SemML(
9+
SemObservedData(data = data, specification = partable),
10+
RAMSymbolic(partable)
1211
)
1312

14-
model_ridge = SemFiniteDiff(
15-
specification = partable,
16-
data = data,
17-
loss = myridge
13+
ridge_term = SemRidge(
14+
α_ridge = 0.01,
15+
which_ridge = params(ml_term)
1816
)
1917

20-
model_ml_ridge = SemEnsemble(model_ml, model_ridge)
18+
model_ml_ridge = Sem(ml_term, ridge_term)
2119

2220
model_ml_ridge_fit = fit(model_ml_ridge)
2321
```

docs/src/performance/simulation.md

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,7 @@ model = Sem(
5757
data = data_1
5858
)
5959
60-
model_updated = replace_observed(model; data = data_2, specification = partable)
61-
```
62-
63-
If you are building your models by parts, you can also update each part seperately with the function `update_observed`.
64-
For example,
65-
66-
```@example replace_observed
67-
68-
new_observed = SemObservedData(;data = data_2, specification = partable)
69-
70-
my_optimizer = SemOptimizer()
71-
72-
new_optimizer = update_observed(my_optimizer, new_observed)
60+
model_updated = replace_observed(model, data_2)
7361
```
7462

7563
## Multithreading
@@ -90,7 +78,7 @@ model1 = Sem(
9078
data = data_1
9179
)
9280

93-
model2 = deepcopy(replace_observed(model; data = data_2, specification = partable))
81+
model2 = deepcopy(replace_observed(model, data_2))
9482

9583
models = [model1, model2]
9684
fits = Vector{SemFit}(undef, 2)
@@ -104,5 +92,4 @@ end
10492

10593
```@docs
10694
replace_observed
107-
update_observed
10895
```
Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,52 @@
11
# Collections
22

3-
With StructuralEquationModels.jl, you can fit weighted sums of structural equation models.
4-
The most common use case for this are [Multigroup models](@ref).
5-
Another use case may be optimizing the sum of loss functions for some of which you do know the analytic gradient, but not for others.
6-
In this case, you can optimize the sum of a `Sem` and a `SemFiniteDiff` (or any other differentiation method).
3+
With *StructuralEquationModels.jl*, you can fit weighted sums of structural equation models.
4+
The most common use case for this are [Multigroup models](@ref).
5+
Another use case may be optimizing the sum of loss functions for some of which you do know the analytic gradient, but not for others.
6+
In this case, [`FiniteDiffWrapper`](@ref) can generate a wrapper around the specific `SemLoss` term. The wrapper loss term will
7+
only use the objective of the original term to calculate its gradient using finite difference approximation.
78

8-
To use this feature, you have to construct a `SemEnsemble` model, which is actually quite easy:
9+
```julia
10+
loss_1 = SemML(observed_1, implied_1)
11+
loss_2 = SemML(observed_2, implied_2)
12+
loss_2_findiff = FiniteDiffWrapper(loss_2)
13+
```
14+
15+
To construct `Sem` from the the individual `SemLoss` (or other `AbstractLoss`) terms, they are
16+
just passed to the `Sem` constructor:
917

1018
```julia
11-
# models
12-
model_1 = Sem(...)
19+
model = Sem(loss_1, loss_2)
20+
model_findiff = Sem(loss_1, loss_2_findiff)
21+
```
22+
23+
It is also possible to use finite difference for the entire `Sem` model:
1324

14-
model_2 = SemFiniteDiff(...)
25+
```julia
26+
model_findiff2 = FiniteDiffWrapper(model)
27+
```
1528

16-
model_3 = Sem(...)
29+
The weighting scheme of the SEM loss terms is specified using `default_set_weights` argument of the `Sem` constructor.
30+
The `:nsamples` scheme (the default) weights SEM terms by ``N_{term}/N_{total}``, i.e. each term is weighted by the number
31+
of observations in its data (which matches the formula for multigroup models).
32+
The weights for the loss terms (both SEM and regularization) can be explicitly specified the pair syntax `loss => weight`:
1733

18-
model_ensemble = SemEnsemble(model_1, model_2, model_3)
34+
```julia
35+
model_weighted = Sem(loss_1 => 0.5, loss_2 => 1.0)
1936
```
2037

21-
So you just construct the individual models (however you like) and pass them to `SemEnsemble`.
22-
You may also pass a vector of weigths to `SemEnsemble`. By default, those are set to ``N_{model}/N_{total}``, i.e. each model is weighted by the number of observations in it's data (which matches the formula for multigroup models).
38+
`Sem` support assigning unique identifier to each loss term, which is essential for complex multi-term model.
39+
The syntax is `id => loss`, or `id => loss => weight`:
2340

24-
Multigroup models can also be specified via the graph interface; for an example, see [Multigroup models](@ref).
41+
```julia
42+
model2 = Sem(:main => loss_1, :alt => loss_2)
43+
model2_weighted = Sem(:main => loss_1 => 0.5, :alt => loss_2 => 1.0)
44+
```
2545

2646
# API - collections
2747

2848
```@docs
29-
SemEnsemble
30-
AbstractSemCollection
49+
Sem
50+
LossTerm
51+
FiniteDiffWrapper
3152
```

docs/src/tutorials/collection/multigroup.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@
44
using StructuralEquationModels
55
```
66

7-
As an example, we will fit the model from [the `lavaan` tutorial](https://lavaan.ugent.be/tutorial/groups.html) with loadings constrained to equality across groups.
7+
As an example, we will fit the model from [the `lavaan` tutorial](https://lavaan.ugent.be/tutorial/groups.html)
8+
with loadings constrained to equality across groups.
89

9-
We first load the example data.
10+
We first load the example data.
1011
We have to make sure that the column indicating the group (here called `school`) is a vector of `Symbol`s, not strings - so we convert it.
1112

1213
```@setup mg
1314
dat = example_data("holzinger_swineford")
14-
dat.school = ifelse.(dat.school .== "Pasteur", :Pasteur, :Grant_White)
15+
dat.school = Symbol.(replace.(dat.school, "-" => "_"))
1516
```
1617

1718
```julia
1819
dat = example_data("holzinger_swineford")
19-
dat.school = ifelse.(dat.school .== "Pasteur", :Pasteur, :Grant_White)
20+
dat.school = Symbol.(replace.(dat.school, "-" => "_"))
2021
```
2122

2223
We then specify our model via the graph interface:
@@ -59,19 +60,19 @@ You can then use the resulting graph to specify an `EnsembleParameterTable`
5960
groups = [:Pasteur, :Grant_White]
6061
6162
partable = EnsembleParameterTable(
62-
graph,
63+
graph,
6364
observed_vars = observed_vars,
6465
latent_vars = latent_vars,
6566
groups = groups)
6667
```
6768

68-
The parameter table can be used to create a `SemEnsemble` model:
69+
The parameter table can be used to create a multigroup `Sem` model:
6970

7071
```@example mg; ansicolor = true
71-
model_ml_multigroup = SemEnsemble(
72+
model_ml_multigroup = Sem(
7273
specification = partable,
7374
data = dat,
74-
column = :school,
75+
semterm_column = :school,
7576
groups = groups)
7677
```
7778

0 commit comments

Comments
 (0)