Documentation
¶
Overview ¶
Package indecis builds small decision models: a pretrained text encoder, fully fine-tuned in pure Go, that answers typed questions with calibrated probabilities instead of generated text.
A Schema lists the questions, of four types:
- noul (NewNoul): a yes/no question, answered with P(yes);
- choice (NewChoice): one option among several, answered with a distribution;
- score (NewScore): a level on an ordered scale, answered with the expected level and its distribution;
- spans (NewSpans): typed passages of the text, such as named entities, answered with their positions.
One pass of the encoder answers every question of a schema: each question is one more head on the same representation of the text.
m, _ := indecis.New(backboneDir, schema, 1)
_ = m.Fit(ctx, train, indecis.DefaultTrainOptions())
_, _ = m.Calibrate(ctx, calib)
_ = m.Save("model")
m, _ = indecis.Load("model", indecis.WithInt8())
d, _ := m.Decide(ctx, "My parcel has not arrived")
In open mode, the options are described in text at call time and compared with the text through embeddings (Model.ChooseNearest, Model.PrepareCandidates, Model.DecideOpen); Model.FitEmbeddings fine-tunes the encoder for it.
The guides in the docs directory of the repository cover the whole process, from the schema to the deployment.
Index ¶
- Constants
- func CandidateContext(c Candidate) string
- func CandidatePairs(question, text string, correct Candidate, others []Candidate) []dataset.Example
- type Answer
- type Candidate
- type CandidateSet
- type ChoiceBatch
- type Decision
- type Hardware
- type Info
- type Input
- type Kind
- type Metrics
- type Model
- func (m *Model) Calibrate(ctx context.Context, examples []dataset.Example) (map[string]float64, error)
- func (m *Model) ChooseAmong(ctx context.Context, question string, candidates []Candidate, texts ...string) ([]Answer, error)
- func (m *Model) ChooseIn(ctx context.Context, set *CandidateSet, texts ...string) ([]Answer, error)
- func (m *Model) ChooseNearest(ctx context.Context, candidates []Candidate, texts ...string) ([]Answer, error)
- func (m *Model) Decide(ctx context.Context, texts ...string) ([]Decision, error)
- func (m *Model) DecideInputs(ctx context.Context, inputs ...Input) ([]Decision, error)
- func (m *Model) DecideOpen(ctx context.Context, questions []OpenQuestion, texts ...string) ([]Decision, error)
- func (m *Model) Embed(ctx context.Context, texts ...string) ([][]float32, error)
- func (m *Model) Evaluate(ctx context.Context, examples []dataset.Example) ([]Metrics, error)
- func (m *Model) Fit(ctx context.Context, examples []dataset.Example, opts TrainOptions) error
- func (m *Model) FitEmbeddings(ctx context.Context, batches []ChoiceBatch, opts TrainOptions) error
- func (m *Model) Info() Info
- func (m *Model) Logits(ctx context.Context, texts ...string) ([]map[string][]float64, error)
- func (m *Model) LogitsInputs(ctx context.Context, inputs ...Input) ([]map[string][]float64, error)
- func (m *Model) Paired() bool
- func (m *Model) PrepareCandidates(ctx context.Context, candidates []Candidate) (*CandidateSet, error)
- func (m *Model) PruneMLP(ctx context.Context, texts []string, keep int) error
- func (m *Model) PruneVocabulary(corpus []Input, minCount int) (PruneStats, error)
- func (m *Model) Save(dir string) error
- func (m *Model) Schema() Schema
- func (m *Model) SetSpanBias(question string, bias float64)
- func (m *Model) SetTemperatures(temps map[string]float64) error
- func (m *Model) Temperatures() map[string]float64
- func (m *Model) TokenIDs(in Input) ([]int32, error)
- func (m *Model) Tokens(in Input) (int, error)
- type OpenQuestion
- type Option
- type Progress
- type PruneStats
- type Question
- type Schema
- type Span
- type SpanMetrics
- type TrainOptions
Constants ¶
const DefaultEmbedScale = 20
DefaultEmbedScale multiplies the cosines before the softmax (inverse of a temperature), the usual value in sentence-transformers.
const MaxSpanTypes = 63
MaxSpanTypes bounds the passage types of a Spans question: the tags of a token are stored on a signed byte.
const NoulAnchor = "Something else"
NoulAnchor describes the "false" option of OpenNoul.
Variables ¶
This section is empty.
Functions ¶
func CandidateContext ¶
CandidateContext puts an option in the form the model reads as context, both in training and inference.
Types ¶
type Answer ¶
type Answer struct {
Question string `json:"question"`
Kind Kind `json:"kind"`
// P is the probability that the answer is "true" (Noul).
P float64 `json:"p,omitempty"`
// Choice is the most probable option (Choice), or the most probable
// level (Score).
Choice string `json:"choice,omitempty"`
// Score is the expected level, between 0 and the number of levels
// minus one.
Score float64 `json:"score,omitempty"`
// Probs is the distribution over the options or the levels.
Probs map[string]float64 `json:"probs,omitempty"`
// Confidence is the probability of the chosen answer.
Confidence float64 `json:"confidence"`
// Margin (Choice) is the gap between the chosen probability and the
// average of the others, CLM's confidence: close to 0 when the
// options are equally likely, even if there are many of them.
Margin float64 `json:"margin,omitempty"`
// Spans lists the passages found (Spans), in text order.
Spans []Span `json:"spans,omitempty"`
}
Answer is the answer to a question.
type Candidate ¶
type Candidate struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Examples []string `json:"examples,omitempty"`
}
Candidate is an option described in natural language. Examples are texts that fall under the option: ChooseNearest uses them to locate the option (a few examples are often worth more than a long description); ChooseAmong ignores them.
type CandidateSet ¶
type CandidateSet struct {
// contains filtered or unexported fields
}
CandidateSet is a list of options prepared for ChooseIn: the embedding of each option is computed once. It remains valid as long as the model is not retrained.
func (*CandidateSet) Candidates ¶
func (s *CandidateSet) Candidates() []Candidate
Candidates returns the options of the list.
type ChoiceBatch ¶
ChoiceBatch is a training batch: texts and a list of shared options. Correct[i] gives the right options for text i; the first is the target, the others are excluded from the comparison (a correct option is not an error).
func ChoiceBatches ¶
func ChoiceBatches(examples []dataset.Example, questions []OpenQuestion, size int, seed int64) []ChoiceBatch
ChoiceBatches forms the FitEmbeddings batches from labeled examples: for each question, the examples that carry its label are grouped into batches of size texts, with all the question's options as the batch's options. A label is an option name, a distribution (the most probable option is kept), a level index (Score), or a boolean (Noul: the first option for true, the second for false).
type Hardware ¶ added in v0.2.0
type Hardware struct {
// Arch is the processor architecture (runtime.GOARCH).
Arch string
// SIMD reports whether the binary was built with Go's portable SIMD
// (GOEXPERIMENT=simd).
SIMD bool
// SIMDEmulated reports whether that SIMD is emulated in pure Go because
// the processor lacks the instructions: correct, but slow.
SIMDEmulated bool
// VectorBits is the SIMD vector width: 128 (SSE, Neon), 256 (AVX2) or
// 512 (AVX-512); 0 without SIMD.
VectorBits int
// AVX2FMA reports whether the processor supports AVX2 and FMA, used by
// the float32 assembly matrix kernel (amd64).
AVX2FMA bool
// AVXVNNI reports whether the processor supports AVX-VNNI, used by the
// int8 matrix kernel (WithInt8).
AVXVNNI bool
// Assembly reports whether the assembly kernels are active: supported
// by the processor and not turned off by INDECIS_NOASM=1.
Assembly bool
}
Hardware describes what speeds up computation on the current machine.
func DetectHardware ¶ added in v0.2.0
func DetectHardware() Hardware
DetectHardware reports the acceleration available on this machine.
type Info ¶
type Info struct {
Backbone string `json:"backbone,omitempty"`
// TrainPrior is, for each Noul question, the proportion of "true"
// answers in the training corpus. This is what is needed to correct
// the probability when the proportion differs in production (see
// calibrate.PriorShift).
TrainPrior map[string]float64 `json:"train_prior,omitempty"`
Steps int `json:"steps,omitempty"`
// EmbedScale multiplies the cosines in ChooseNearest (0: the default
// value).
EmbedScale float64 `json:"embed_scale,omitempty"`
}
Info describes a model's origin.
type Metrics ¶
type Metrics struct {
Question string `json:"question"`
Kind Kind `json:"kind"`
N int `json:"n"`
// Accuracy: most probable answer = label. For Noul, threshold 0.5;
// for a soft label, it is its rounding.
Accuracy float64 `json:"accuracy"`
// Noul: precision, recall and F1 of the "true" class, area under the
// ROC curve.
Precision float64 `json:"precision,omitempty"`
Recall float64 `json:"recall,omitempty"`
F1 float64 `json:"f1,omitempty"`
AUC float64 `json:"auc,omitempty"`
// Choice: average F1 over the options present.
MacroF1 float64 `json:"macro_f1,omitempty"`
// Score: mean absolute deviation between expected level and actual level.
MAE float64 `json:"mae,omitempty"`
// Spans: Precision, Recall and F1 count the passages found with their
// exact type and bounds, F2 weighs recall twice, Types details each
// type. Accuracy, NLL and ECE are per token.
F2 float64 `json:"f2,omitempty"`
Types []SpanMetrics `json:"types,omitempty"`
// NLL is the mean negative log-likelihood, Brier the Brier score
// (Noul), ECE the calibration error over 10 confidence bins.
NLL float64 `json:"nll"`
Brier float64 `json:"brier,omitempty"`
ECE float64 `json:"ece"`
}
Metrics measures a question over a set of examples.
type Model ¶
type Model struct {
// contains filtered or unexported fields
}
Model is a decision model: an encoder and one head per question.
func New ¶
New creates an untrained model from a backbone in transformers format (config.json, model.safetensors, tokenizer.json). The heads are randomly initialized with seed.
func Open ¶
Open loads a model written by Save, or, if dir does not contain one (no indecis.json), a backbone in transformers format: it then only answers open questions (ChooseNearest, DecideOpen), without training. Its learned question, "match", is only an untrained anchor point.
func (*Model) Calibrate ¶
func (m *Model) Calibrate(ctx context.Context, examples []dataset.Example) (map[string]float64, error)
Calibrate tunes each question's temperature on examples distinct from those used in training, by minimizing the negative log-likelihood. Returns the temperatures used.
A question whose calibration examples are all perfectly classified keeps its temperature: these examples say nothing about the model's error, only that they are too easy.
func (*Model) ChooseAmong ¶
func (m *Model) ChooseAmong(ctx context.Context, question string, candidates []Candidate, texts ...string) ([]Answer, error)
ChooseAmong picks, for each text, the option that suits it best among candidates, using the paired model's Noul question named question.
Answer.Probs is the distribution over the options (softmax of the logits, including the question's temperature) and Answer.Choice the most probable one. Answer.P is the probability that this best option really suits the text: a low value signals a text that no option describes.
func (*Model) ChooseIn ¶
ChooseIn ranks each text among the options of a prepared list. Answer.Probs is the softmax of the scaled cosines (Info.EmbedScale, DefaultEmbedScale by default). Answer.Score is the cosine of the chosen option: a threshold on this cosine, tuned on examples, separates texts that no option describes.
func (*Model) ChooseNearest ¶
func (m *Model) ChooseNearest(ctx context.Context, candidates []Candidate, texts ...string) ([]Answer, error)
ChooseNearest picks, for each text, the option whose prototype is closest (see PrepareCandidates and ChooseIn).
func (*Model) DecideInputs ¶
DecideInputs answers all the questions for each input.
func (*Model) DecideOpen ¶
func (m *Model) DecideOpen(ctx context.Context, questions []OpenQuestion, texts ...string) ([]Decision, error)
DecideOpen answers open questions for each text. The probabilities come from the softmax of the cosines (see ChooseIn): their calibration depends on the model, and a threshold is tuned on examples. WithEmbedCache avoids re-encoding the options from one call to the next.
func (*Model) Embed ¶
Embed returns the embedding of each text: the mean of the encoder's states, normalized (norm 1). The dot product of two embeddings is their cosine similarity. Each text is encoded alone, even by a paired model: on the non-fine-tuned backbone, it is the original model's sentence embedding.
func (*Model) FitEmbeddings ¶
func (m *Model) FitEmbeddings(ctx context.Context, batches []ChoiceBatch, opts TrainOptions) error
FitEmbeddings fine-tunes the encoder for ChooseNearest. opts.HeadLR and opts.Dropout are ignored: no head is involved. opts.Symmetric adds the option -> texts direction to the loss.
func (*Model) Logits ¶
Logits returns, for each text, the raw logits of each question, before temperature: a caller that applies its own calibration or its own prior correction starts from there (see Temperatures and Info). Spans questions are left out.
func (*Model) LogitsInputs ¶
LogitsInputs is Logits for inputs with context.
func (*Model) PrepareCandidates ¶
func (m *Model) PrepareCandidates(ctx context.Context, candidates []Candidate) (*CandidateSet, error)
PrepareCandidates computes the prototype of each option: the normalized average of the embedding of its name and its description and those of its examples.
func (*Model) PruneMLP ¶ added in v0.5.0
PruneMLP keeps keep neurons in the MLP of each layer of the encoder, the most useful on the texts: those whose mean activity |gelu(input)·gate|, times the norm of their output weights, is largest. The texts are read as in inference, in windows. Every layer keeps as many neurons: the model stays a ModernBERT, readable by transformers.
The model loses quality until it is trained again: see Fit and TrainOptions.Teacher.
func (*Model) PruneVocabulary ¶
func (m *Model) PruneVocabulary(corpus []Input, minCount int) (PruneStats, error)
PruneVocabulary reduces the vocabulary to the tokens that the corpus tokenization uses, including BPE intermediates, seen at least minCount times (1: all that appear). The embedding table, 93% of a small model's weights, shrinks accordingly: for use in a few languages, a multilingual vocabulary of 256,000 tokens reduces to a few tens of thousands.
A corpus text then tokenizes exactly as before; a text that would have needed a removed token tokenizes into smaller pieces, at worst into bytes. The corpus must therefore represent the texts the model will judge (languages, domains), and the effect is measured on texts outside the corpus (Evaluate before and after). The model is modified in place; Save writes the reduced tokenizer.
func (*Model) Save ¶
Save writes the model to dir. The directory stays readable by transformers: config.json and the encoder's weights keep their names.
func (*Model) SetSpanBias ¶ added in v0.4.0
SetSpanBias changes the decoding bias of a Spans question (see WithSpanBias). It must not run during a decision.
func (*Model) SetTemperatures ¶ added in v0.5.0
SetTemperatures replaces the temperatures of the named questions. A model distilled from another (see TrainOptions.Teacher) can take its teacher's: its logits learned the teacher's scale, so a decoding bias keeps the effect it has on the teacher.
func (*Model) Temperatures ¶
Temperatures returns the temperature of each question.
type OpenQuestion ¶
type OpenQuestion struct {
Name string
Kind Kind
// Instructions, if given, precede each option in what the model reads
// ("Email urgency" before "high").
Instructions string
// Options: for Choice, the options; for Score, the levels from lowest
// to highest; for Noul, exactly two criteria, the "true" one then the
// "false" one.
Options []Candidate
}
OpenQuestion is a question asked at inference time, without dedicated training: its possible answers are described in text and compared to the text being judged by embeddings (see ChooseIn). This is the form of the Jev and CLM questions.
func OpenNoul ¶
func OpenNoul(name, instructions string) OpenQuestion
OpenNoul builds an open yes/no question from its instructions alone: "true" is described by the instructions, "false" by a fixed anchor, "Something else". Opposing the instructions to their negation ("Yes: X" against "No: X") gives almost no signal, because the two embeddings are nearly identical. The anchor ranks texts much better, but its probabilities are not calibrated: tune a threshold on examples.
func (OpenQuestion) OptionIndex ¶
func (q OpenQuestion) OptionIndex(v any) int
OptionIndex returns the index of the option that a label designates (see ChoiceBatches), or -1.
type Option ¶
type Option func(*Model)
Construction options.
func WithBatching ¶
WithBatching groups the computations of simultaneous requests: workers goroutines (0: one per core) each take the first pending request and all those already queued, up to maxRows positions (0: 512), and compute them in a single pass. Without load, a request leaves right away, without waiting; under load, the batches grow on their own and the larger matrix products are more efficient. The sequences of a batch are laid end to end without padding.
On CPU, the gain is small: each request already occupies a core, and requests of 100 to 256 tokens already make fairly large matrix products. Measured on the decision server (32 clients): a few % of throughput on single-sentence requests, nothing beyond that, for more memory.
func WithEmbedCache ¶
WithEmbedCache keeps in memory the embeddings of the n most recent distinct texts passed to Embed, and of the options prepared by PrepareCandidates (so ChooseNearest, DecideOpen): a reused option is not re-encoded. Texts judged by ChooseIn are not kept. The cache is cleared on every training run.
func WithInt8 ¶
func WithInt8() Option
WithInt8 makes the encoder's layers compute in int8 (per-channel weights, per-token activations) when the processor has AVX-VNNI; without it, the option has no effect. Inference is about twice as fast and the layer weights four times smaller; the decisions of the injection detector (xolo-plugin-injection-guard) are unchanged on its reference set. The gap should still be checked for each model (Evaluate with and without the option).
func WithInt8Embeddings ¶
func WithInt8Embeddings() Option
WithInt8Embeddings makes Save write the embedding table in int8, one scale per row: half the size of bf16, on disk as well as in the pages read. Rows modified by training lose their exact value; the gap is measured with Evaluate before and after. A model saved this way is recognized on load and stays that way. Hugging Face tools do not read this format.
func WithMaxLen ¶
WithMaxLen sets the maximum length in tokens; longer texts are truncated. 256 by default.
func WithPairs ¶
func WithPairs() Option
WithPairs makes the model read pairs (context, text): the system prompt and the message, for example. All inputs are then encoded as a pair, including an empty context, so that training and inference see the same shape. See tokenizer.EncodePair for truncation.
func WithSpanBias ¶ added in v0.4.0
WithSpanBias favors passages in the decoding of a Spans question: bias is added to the log-probability of every passage tag against the outside tag. A positive bias finds more passages and longer ones, more recall for less precision, the trade-off masking personal data asks for; it plays the part of a heavier loss weight on passages, without training again. Confidences stay those of the model. Save keeps the bias, and an option given to Load overrides the saved one.
func WithThreads ¶
WithThreads bounds the number of cores used by the computations (0: all). The setting applies to the whole process. At inference, a batch of fewer than 32 positions is computed on a single core anyway, the fastest for one sentence; longer texts and batches spread over the cores the other requests leave free.
type Progress ¶
type Progress struct {
Epoch, Step, Steps int
Loss float64 // mean loss of the step
LR float64
Tokens int // tokens processed since the start
Elapsed time.Duration
}
Progress describes the training's progress.
type PruneStats ¶
type PruneStats struct {
Before, After int // tokens before and after
}
PruneStats describes a vocabulary pruning.
type Question ¶
type Question struct {
Name string `json:"name"`
Kind Kind `json:"kind"`
// Instructions documents the question. The model does not read it: its
// heads are learned, the question is in the data.
Instructions string `json:"instructions,omitempty"`
// Options lists the options of a Choice, the levels of a Score in
// increasing order, or the passage types of a Spans question.
Options []string `json:"options,omitempty"`
}
Question describes a decision that the model learns to make.
type Schema ¶
type Schema []Question
Schema is the list of a model's questions.
type Span ¶ added in v0.4.0
type Span struct {
Type string `json:"type"`
Start int `json:"start"`
End int `json:"end"`
Text string `json:"text"`
// Confidence is the mean probability of the tags of the passage.
Confidence float64 `json:"confidence"`
}
Span is a passage found by a Spans question: text[Start:End].
type SpanMetrics ¶ added in v0.4.0
type SpanMetrics struct {
Type string `json:"type,omitempty"`
// Gold is the number of labeled passages, Found the number found.
Gold int `json:"gold"`
Found int `json:"found"`
Precision float64 `json:"precision"`
Recall float64 `json:"recall"`
F1 float64 `json:"f1"`
// F2 weighs recall twice as much as precision: for personal data, a
// missed passage costs more than one too many.
F2 float64 `json:"f2"`
}
SpanMetrics measures the passages of one type, or of all types.
type TrainOptions ¶
type TrainOptions struct {
Epochs int
BatchSize int
// LR is the encoder's learning rate, HeadLR that of the heads, higher:
// they start from scratch.
LR, HeadLR float64
WeightDecay float64
// Warmup is the fraction of steps spent raising the rate linearly,
// which then decreases linearly to zero.
Warmup float64
// ClipNorm bounds the global gradient norm (0: no clipping).
ClipNorm float64
// Dropout applies to the pooled vector before the heads, and to the
// state of each token before the heads of Spans questions.
Dropout float64
Seed int64
// Symmetric (FitEmbeddings) also optimizes the option -> texts
// direction: each option in a batch must prefer its texts over the
// others, like CLM's bidirectional InfoNCE loss.
Symmetric bool
// Progress, if provided, is called after each step.
Progress func(Progress)
// Teacher, if set, is distilled into the model (see PruneMLP): the
// loss of each Spans question mixes the labels, weight 1 − Distill,
// and the teacher's tags, weight Distill, both distributions softened
// by DistillTemp (Hinton et al. 2015: the soft term is multiplied by
// T² so that its gradients keep their scale). DistillHidden weighs
// the mean squared distance between the final states of the two
// encoders. The teacher must have the model's questions, tokenizer
// and width; it runs in inference, int8 if it was loaded so.
Teacher *Model
Distill float64
DistillTemp float64
DistillHidden float64
}
TrainOptions configures the fine-tuning.
func DefaultTrainOptions ¶
func DefaultTrainOptions() TrainOptions
DefaultTrainOptions are starting settings for a small encoder.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package calibrate combines probabilities in log-odds.
|
Package calibrate combines probabilities in log-odds. |
|
cmd
|
|
|
indecis
command
Command indecis creates, evaluates and uses small decision models.
|
Command indecis creates, evaluates and uses small decision models. |
|
Package dataset defines the format of training examples and the splits that measure generalization.
|
Package dataset defines the format of training examples and the splits that measure generalization. |
|
synth
Gazetteers: weighted lists of values, taken from go-anon.
|
Gazetteers: weighted lists of values, taken from go-anon. |
|
internal
|
|
|
mapfile
Package mapfile maps files into memory, read-only.
|
Package mapfile maps files into memory, read-only. |
|
modernbert
Package modernbert implements the ModernBERT encoder in pure Go, for both inference and training.
|
Package modernbert implements the ModernBERT encoder in pure Go, for both inference and training. |
|
siglip
Package siglip implements the SigLIP image and text encoders in pure Go, for inference: a Vision Transformer that turns an image into an embedding, and a text transformer whose embeddings live in the same space.
|
Package siglip implements the SigLIP image and text encoders in pure Go, for inference: a Vision Transformer that turns an image into an embedding, and a text transformer whose embeddings live in the same space. |
|
Package linalg provides the numerical kernels for training and inference: matrix product, vector operations.
|
Package linalg provides the numerical kernels for training and inference: matrix product, vector operations. |
|
Package optim implements AdamW, identical to torch.optim.AdamW, and its sparse variant for embedding tables.
|
Package optim implements AdamW, identical to torch.optim.AdamW, and its sparse variant for embedding tables. |
|
Package safetensors reads and writes the safetensors format: a JSON header describing each tensor, followed by raw little-endian data.
|
Package safetensors reads and writes the safetensors format: a JSON header describing each tensor, followed by raw little-endian data. |
|
Package tokenizer reproduces in pure Go the BPE tokenizer of the Gemma model family, as a Hugging Face tokenizer.json file describes it.
|
Package tokenizer reproduces in pure Go the BPE tokenizer of the Gemma model family, as a Hugging Face tokenizer.json file describes it. |
|
tools
|
|
|
imgeval
command
Command imgeval measures zero-shot image classification with a SigLIP model on Imagenette: each class is described by a caption, and an image gets the class whose caption scores highest.
|
Command imgeval measures zero-shot image classification with a SigLIP model on Imagenette: each class is described by a caption, and an image gets the class whose caption scores highest. |
|
infbench
command
Command infbench measures the memory footprint and inference latency of an indecis model.
|
Command infbench measures the memory footprint and inference latency of an indecis model. |
|
Package vision takes decisions on images with a SigLIP model, without training: the options of a question are described in text, and each is scored against the image by the model, as indecis does for texts in open mode (see indecis.OpenQuestion).
|
Package vision takes decisions on images with a SigLIP model, without training: the options of a question are described in text, and each is scored against the image by the model, as indecis does for texts in open mode (see indecis.OpenQuestion). |