---
title: "Active Learning with conflibertR"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Active Learning with conflibertR}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  eval = FALSE
)
```

Labeling text is expensive. Active learning lets you focus annotation
effort on the examples a model is *most uncertain* about, so each label
has maximum impact on performance. `conflibertR` exposes this as a
small, intuitive loop:

1. Start from a tiny labeled seed and an unlabeled pool.
2. Train a model on the seed; the package hands back the most uncertain
   samples from the pool.
3. Label those samples, submit them, and the model retrains and picks
   the next uncertain batch.
4. Repeat until the pool is exhausted or metrics plateau.
5. Save the final model as a HuggingFace checkpoint.

```{r}
library(conflibertR)
```

## Example data

The package bundles a small demo dataset: a 20-text labeled seed, a
61-text unlabeled pool, and a dev set. It also includes oracle labels
for the pool so you can simulate a full loop without a human in the
loop (for testing only; don't use the oracle in real workflows).

```{r}
demo <- conflibert_example("active")

nrow(demo$seed)      # 20 labeled seed texts
length(demo$pool)    # 61 unlabeled pool texts
nrow(demo$dev)       # 20 dev texts
length(demo$pool_labels)  # oracle labels (for simulation)
```

## Starting a session

`conflibert_active_start()` trains a classifier on the seed and returns
a session object containing the first uncertain batch. Each round, pass
the session to `conflibert_active_next()` along with your labels.

```{r}
session <- conflibert_active_start(
  seed       = demo$seed,
  pool       = demo$pool,
  dev        = demo$dev,
  model      = "ConfliBERT",
  task       = "binary",
  strategy   = "entropy",   # or "margin", "least_confidence"
  query_size = 10,
  epochs     = 1
)

session
```

The session's `$query` is a tibble of texts to label next, with an
`uncertainty` column showing how unsure the model is. `$metrics` tracks
scores across rounds; `$labeled_n` / `$pool_n` track progress.

## Labeling and iterating

For real labeling, the easiest route is the built-in Shiny gadget. It
opens a modal dialog (or browser tab) showing every row of the current
query with radio buttons for each class: click, submit, done.

```{r}
labels  <- conflibert_active_label(session)
session <- conflibert_active_next(session, labels = labels)
```

You can also provide the labels by hand (useful for scripting, or if
you prefer a console-only workflow):

```{r}
# labels in the same order as session$query
labels  <- c(1, 0, 1, 0, 0, 1, 0, 1, 0, 1)
session <- conflibert_active_next(session, labels = labels)
```

For this vignette we use the bundled oracle to simulate labeling:

```{r}
labels  <- unname(demo$pool_labels[session$query$text])
session <- conflibert_active_next(session, labels = labels)
session
```

Repeat until the pool is exhausted or the learning curve flattens.
Here's a short simulation loop:

```{r}
for (round in 2:5) {
  if (session$done) break
  labels  <- unname(demo$pool_labels[session$query$text])
  session <- conflibert_active_next(session, labels = labels)
}

session$metrics
```

## Visualizing progress

`plot()` produces a two-panel diagnostic: the learning curve on top
(metrics vs labeled-set size) and the query uncertainty trend on the
bottom. When mean uncertainty flattens, the model is no longer finding
informative samples, a good signal to stop.

```{r}
plot(session)

# or a single panel:
plot(session, which = "metrics")
plot(session, which = "uncertainty")
```

## Query strategies

Three uncertainty strategies are available. Pass one via `strategy`:

- `"entropy"` (default): highest Shannon entropy of the predicted
  class distribution. Works well for both binary and multiclass.
- `"margin"`: smallest gap between the top two class probabilities.
  Targets ambiguous samples on the decision boundary.
- `"least_confidence"`: lowest maximum class probability. Simplest
  strategy; a good baseline.

## Diversity-aware batches

Pure uncertainty sampling can pick several near-duplicates in one
batch, a problem when your pool has many similar texts. Pass
`diverse = TRUE` to cluster the top-scoring candidates in the model's
embedding space and pick the highest-scoring sample from each cluster:

```{r}
session <- conflibert_active_start(
  seed = demo$seed, pool = demo$pool, dev = demo$dev,
  strategy = "entropy",
  diverse  = TRUE,
  diversity_candidates = 30    # defaults to 3 * query_size
)
```

## LoRA fine-tuning

For bigger base models or tighter GPU budgets, train only a low-rank
adapter each round. The adapter is merged into the base model before
every round ends, so scoring, saving, and reloading behave exactly
like full fine-tuning:

```{r}
session <- conflibert_active_start(
  seed = demo$seed, pool = demo$pool, dev = demo$dev,
  model      = "DeBERTa v3 Base",
  use_lora   = TRUE,
  lora_rank  = 8,
  lora_alpha = 16
)
```

## Saving the model

Persist the final model as a standard HuggingFace checkpoint:

```{r}
conflibert_active_save(session, "my_al_model")
```

You can reload it with any `transformers` tool, or point
`AutoModelForSequenceClassification.from_pretrained()` at the directory
from Python.

## Tips

- **Start small.** A seed of 10–50 texts is often enough; active
  learning shines when the pool is much larger than the labeled set.
- **Watch uncertainty trends, not just metrics.** If dev metrics are
  noisy on small sets, the uncertainty panel often tells a clearer
  story about whether the model is still learning.
- **Parameters carry across rounds.** The model, task, strategy,
  query size, and training hyperparameters are fixed when you call
  `conflibert_active_start()` and reused for every subsequent round.
- **Sessions are in-memory.** The trained model lives inside the
  session object as a Python handle. Calling `saveRDS()` on the
  session won't serialize the model; use `conflibert_active_save()`
  to persist it, and re-run rounds from a fresh session if needed.
