Skip to content

Commit 3c39941

Browse files
author
Alexey Stukalov
committed
docs: sync with Sem refactor
1 parent c1e7b04 commit 3c39941

9 files changed

Lines changed: 61 additions & 98 deletions

File tree

docs/src/developer/implied.md

Lines changed: 11 additions & 17 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,7 +31,7 @@ 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

3737

@@ -56,7 +56,7 @@ Empty placeholder for models that don't need an implied part.
5656
- `specification`: either a `RAMMatrices` or `ParameterTable` object
5757
5858
# Examples
59-
A multigroup model with ridge regularization could be specified as a `SemEnsemble` with one
59+
A multigroup model with ridge regularization could be specified as a `Sem` with one
6060
model per group and an additional model with `ImpliedEmpty` and `SemRidge` for the regularization part.
6161
6262
# Extended help
@@ -75,26 +75,20 @@ end
7575
### Constructors
7676
############################################################################################
7777

78-
function ImpliedEmpty(;
79-
specification,
80-
meanstruct = NoMeanStruct(),
81-
hessianeval = ExactHessian(),
78+
function ImpliedEmpty(
79+
spec::SemSpecification;
80+
hessianeval::HessianApprox = ExactHessian(),
8281
kwargs...,
8382
)
84-
return ImpliedEmpty(hessianeval, meanstruct, convert(RAMMatrices, specification))
83+
ram_matrices = convert(RAMMatrices, spec)
84+
return ImpliedEmpty(hessianeval, MeanStruct(ram_matrices), ram_matrices)
8585
end
8686

8787
############################################################################################
8888
### methods
8989
############################################################################################
9090

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
91+
update!(targets::EvaluationTargets, implied::ImpliedEmpty, par) = nothing
9892
```
9993

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

docs/src/developer/loss.md

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
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 = SemFiniteDiff(
14+
SemML(
15+
SemObservedData(data = data, specification = partable),
16+
RAMSymbolic(partable)
17+
)
1818
)
1919

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

2222
model_ml_ridge_fit = fit(model_ml_ridge)
2323
```

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: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
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, you can combine `SemLoss` terms with finite-difference-wrapped terms in a single `Sem` model.
77

8-
To use this feature, you have to construct a `SemEnsemble` model, which is actually quite easy:
8+
To use this feature, you construct a `Sem` model with multiple loss terms:
99

1010
```julia
11-
# models
12-
model_1 = Sem(...)
11+
# loss terms
12+
loss_1 = SemML(observed_1, implied_1)
1313

14-
model_2 = SemFiniteDiff(...)
14+
loss_2 = SemML(observed_2, implied_2)
1515

16-
model_3 = Sem(...)
17-
18-
model_ensemble = SemEnsemble(model_1, model_2, model_3)
16+
model = Sem(loss_1, loss_2)
1917
```
2018

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).
19+
So you just construct the individual `SemLoss` (or other `AbstractLoss`) terms and pass them to `Sem`.
20+
You may also pass weights by using the pair syntax `loss => weight`. By default, SEM loss terms are weighted by ``N_{model}/N_{total}``, i.e. each term is weighted by the number of observations in its data (which matches the formula for multigroup models).
2321

2422
Multigroup models can also be specified via the graph interface; for an example, see [Multigroup models](@ref).
2523

2624
# API - collections
2725

2826
```@docs
29-
SemEnsemble
30-
AbstractSemCollection
27+
Sem
28+
LossTerm
3129
```

docs/src/tutorials/collection/multigroup.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ using StructuralEquationModels
66

77
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.
88

9-
We first load the example data.
9+
We first load the example data.
1010
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.
1111

1212
```@setup mg
@@ -59,19 +59,19 @@ You can then use the resulting graph to specify an `EnsembleParameterTable`
5959
groups = [:Pasteur, :Grant_White]
6060
6161
partable = EnsembleParameterTable(
62-
graph,
62+
graph,
6363
observed_vars = observed_vars,
6464
latent_vars = latent_vars,
6565
groups = groups)
6666
```
6767

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

7070
```@example mg; ansicolor = true
71-
model_ml_multigroup = SemEnsemble(
71+
model_ml_multigroup = Sem(
7272
specification = partable,
7373
data = dat,
74-
column = :school,
74+
semterm_column = :school,
7575
groups = groups)
7676
```
7777

0 commit comments

Comments
 (0)