---
title: "Nested comparisons with uncfam_general()"
author: "George Kachergis"
date: "`r Sys.Date()`"
output:
  rmarkdown::html_vignette:
        fig_caption: yes
vignette: >
  %\VignetteIndexEntry{Nested comparisons with uncfam_general()}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
library(XSLmodels)

knitr::opts_chunk$set(
  echo = TRUE,
  collapse = TRUE,
  comment = "#>",
  # DEoptim advises a population of at least 10 per parameter; the searches
  # here are smaller to keep the vignette fast, so its warnings are hidden.
  # Single-word, single-object training trials also trigger an R deprecation
  # warning in the shared uncfam() arithmetic.
  warning = FALSE
)
```

The biased associative model of Kachergis, Yu & Shiffrin (2012), `uncfam()`,
has several extensions in this package. `uncfam_general()` is one model with
each extension as a parameter, so the variants are special cases of it:

| `uncfam_general()` setting | Equivalent to | What it adds |
|---|---|---|
| defaults | `uncfam()` | attention to pairs by familiarity × uncertainty |
| `B = 0` | `uncfam(B = 0)` | familiarity only |
| `gamma = 0` | `uncfam(variant = "uncertainty-only")` | uncertainty only |
| `uncertainty = "novelty"` | `uncfam(variant = "novelty")` | novelty in place of entropy |
| `gamma` free | `uncfam_gamma()` | how strongly familiarity drives attention |
| `eps` free | `uncfam_elimination()` | learning by elimination (mutual exclusivity) |
| `kappa = 1` | `uncfam_attention()` | more learning on more uncertain trials |
| `K` finite | `uncfam_sampling()` | attend to `K` sampled objects per word |
| `rho > 0` | (none) | a short-term trace: recent pairings draw attention |

These aren't approximations: `uncfam()` and the other variants are
implemented as calls to the same model, and give identical results.

```{r identical}
d <- xsl_datasets[[1]]
identical(xsl_run(uncfam(X = .2, B = 3, C = .97), d)$fits[[1]]$perf,
          xsl_run(uncfam_general(X = .2, B = 3, C = .97), d)$fits[[1]]$perf)
```

Because the variants are nested, they can be compared directly. Does
freeing a parameter improve the fit enough to justify it?

# What the extensions do

**Elimination** (`eps`). A pair gains weight to the extent that the trial's
other words and objects are already accounted for by each other. Here word 2
is learned first, then appears with novel word 1. Elimination says object 1
must be word 1's:

```{r elimination}
elim_data <- xslData(train = list(words = list(2, 2, c(1, 2), c(1, 3)),
                                  objects = list(2, 2, c(1, 2), c(1, 3))))
sapply(c(eps_0 = 0, eps_2 = 2, eps_10 = 10), \(e)
  xsl_run(uncfam_general(X = 1, B = 1, C = 1, eps = e), elim_data)$fits[[1]]$perf)
```

**Short-term trace** (`rho`, `Cs`). Every update also goes into a second
matrix that decays quickly (by `Cs` per trial), and attention follows
familiarity plus `rho` times this trace. Here word 1 meets object 1 twice,
then object 2 twice, then both together. Without a trace the two objects end
level; with one, the more recent pairing pulls ahead:

```{r trace}
trace_data <- xslData(train = list(words = list(1, 1, 1, 1, 1),
                                   objects = list(1, 1, 2, 2, c(1, 2))))
sapply(c(rho_0 = 0, rho_5 = 5), \(r)
  xsl_run(uncfam_general(X = .5, B = 0, C = 1, rho = r, Cs = .5), trace_data)$fits[[1]]$matrix[1, ])
```

**Sampled attention** (`K`). With a finite `K`, each word attends to `K`
objects drawn in proportion to familiarity × uncertainty, so the model is
stochastic. `xsl_run()` averages `xslControl(n_sim = )` simulated learners:

```{r sampling}
set.seed(1)
xsl_run(uncfam_general(X = .2, B = 3, C = .97, K = 1), d, xslControl(n_sim = 50))$sse
```

# A nested comparison

Does freeing the familiarity exponent `gamma` improve on the original model?
The comparison below fits both, by binomial likelihood, to two conditions:
`212` (a 4AFC test) and `301` (24 words).

Two precautions keep the comparison fair. First, the larger model's search
starts from the smaller model's best fit (with `gamma = 1`), so it can't
fit worse merely because its search got unlucky. Second, the search uses a
fixed seed.

```{r nested}
labels <- vapply(xsl_datasets, `[[`, "", "label")
ctrl <- xslControl(objective = "binomial", lapse = .02)

fit_nested <- function(data, free, lower, upper, start = NULL) {
  np <- 30
  pop <- NULL
  if (!is.null(start)) {
    set.seed(2)
    pop <- sapply(seq_along(lower), \(j) stats::runif(np, lower[j], upper[j]))
    pop[1, ] <- start
  }
  xsl_fit(uncfam_general(X = .1, B = 1, C = 1), data, lower = lower, upper = upper,
          free = free, control = ctrl, seed = 1,
          deoptim_control = DEoptim::DEoptim.control(NP = np, itermax = 25, trace = FALSE,
                                                     initialpop = pop))[[1]]
}

compare_gamma <- function(label) {
  data <- xsl_datasets[[which(labels == label)]]
  bam <- fit_nested(data, c("X", "B", "C"), c(.001, .1, .5), c(5, 15, 1))
  gam <- fit_nested(data, c("X", "B", "C", "gamma"), c(.001, .1, .5, 0), c(5, 15, 1, 6),
                    start = c(bam$optim$bestmem, gamma = 1))
  lr <- 2 * (gam$loglik - bam$loglik)
  data.frame(condition = label, gamma = gam$optim$bestmem[["gamma"]],
             delta_AIC = gam$aic - bam$aic, LR = lr,
             p = stats::pchisq(lr, df = 1, lower.tail = FALSE))
}
res <- do.call(rbind, lapply(c("212", "301"), compare_gamma))
res
```

The two conditions pull `gamma` in opposite directions:

- **In `212`, familiarity is amplified** (`gamma` = `r round(res$gamma[1], 1)`,
  p = `r signif(res$p[1], 2)`). Pairs that are already familiar draw even more
  attention.
- **In `301`, it is nearly switched off** (`gamma` = `r round(res$gamma[2], 2)`).
  Attention follows uncertainty alone. The improvement here is smaller
  (p = `r signif(res$p[2], 2)`).

A single `gamma` fit across many conditions would average these. Fitting the
nested variants condition by condition can reveal such differences, where a
single group fit hides them.

Two caveats:

- **The p-values are approximate.** The binomial treats every response as
  independent, which overstates the evidence. And for parameters whose null
  value sits on a bound, such as `eps = 0` or `kappa = 0`, the chi-square
  p-value is conservative.
- **Small conditions can't tell variants apart.** With only 8 to 24 test
  items per condition, several parameters are weakly identified.
