BinClass

test

An easy-to-use Elixir library for building, training, and deploying binary text classifiers with Axon.

This library provides a simplified interface for training a neural network on text data and using it for predictions, handling tokenization, vectorization, and model training out of the box.

Installation

The package can be installed by adding bin_class to your list of dependencies in mix.exs:

def deps do
[
{:bin_class, "~> 0.3.0"}
]
end

Quick Start

1. Prepare your data

Data can be an enumerable of maps (%{text: "...", label: 1} or %{"text" => "...", "label" => 1}) or tuples ({"...", label}).

data = [
%{text: "This is a great product!", label: 1},
%{text: "I really hated this experience.", label: 0},
# ... more samples
]
# Inspect dataset balance and text length statistics:
summary = BinClass.summary(data)
IO.puts(BinClass.format_summary(summary))
# Stratified train / validation split:
{train_data, val_data} = BinClass.Dataset.split(data, validation_split: 0.1, seed: 42)

2. Train the model

# Output labels can be customized with a two-element list or map.
classifier = BinClass.train(train_data,
epochs: 5,
labels: %{0 => "negative", 1 => "positive"},
label_smoothing: 0.02
)

By default, training uses the :parallel_cnn architecture with 2-head attentive pooling, a fixed sequence length of 512, NFKC lowercasing normalizer, vocabulary size 8_000, and argmax classification.

3. Evaluate the model

result = BinClass.evaluate(classifier, val_data)
IO.puts(BinClass.format_report(result))

This outputs a comprehensive evaluation report with Accuracy, Balanced Accuracy, Precision, Recall, Specificity, F1 Score, Cross-Entropy Loss, and the full Confusion Matrix.

Soft targets

Training labels may also represent a graded positive-class probability. Enable soft targets explicitly and provide values from 0.0 through 1.0:

data = [
%{text: "Clearly negative example", label: 0.0},
%{text: "Weak positive evidence", label: 0.2},
%{text: "Strong positive evidence", label: 0.9},
%{text: "Clearly positive example", label: 1.0}
]
classifier = BinClass.Trainer.train(data,
target_mode: :soft,
target_threshold: 0.5
)

Each value p is trained as the distribution [1 - p, p]. The target threshold is used only to derive binary classes for balancing and validation metrics. Hard 0/1 targets remain the default. Target settings are retained as training metadata when the classifier is serialized.

Decision policies and input validation

Classification defaults to argmax. Decision behavior is explicit and does not depend on model architecture. A fixed positive threshold can be selected with:

classifier = BinClass.Trainer.train(data,
decision_policy: {:positive_threshold, 0.7}
)

Threshold optimization is opt-in and uses the validation partition. Supported objectives are :accuracy, :balanced_accuracy, and :f1. An optional false-positive cost is subtracted from the objective score as a multiple of the false-positive rate. Optimization requires at least one eligible validation example from each class:

classifier = BinClass.Trainer.train(data,
decision_policy:
{:optimize_threshold,
[objective: :balanced_accuracy, false_positive_cost: 0.75]}
)

The selected threshold and optimization metadata are persisted with the classifier. To abstain from classifying short inputs, configure input validation separately:

classifier = BinClass.Trainer.train(data,
minimum_input_tokens: 64
)

An input below the minimum returns label: :insufficient_input and confidence: nil. Its raw class probabilities remain available in the :probabilities map. Every result also reports :active_token_count, defined as the number of truncated token IDs other than the tokenizer's padding ID. Literal [PAD] tokens therefore cannot satisfy the minimum. Ineligible inputs are excluded from threshold optimization and reported validation accuracy.

Evaluation and calibration

BinClass.Evaluation can also evaluate probabilities produced by external or multi-stage inference pipelines. Fixed-threshold evaluation returns confusion counts, accuracy, precision, recall, specificity, false-positive rate, balanced accuracy, F1, Brier score, and the configured objective score:

metrics =
BinClass.Evaluation.evaluate(rows,
threshold: 0.7,
probability: :positive_probability,
label: :positive?,
target: :soft_target,
eligible: :eligible?
)

The probability, label, soft target, and eligibility selectors may be map keys or one-argument functions. The :target and :eligible selectors are optional.

Use calibrate/2 to inspect every distinct decision boundary represented by the observed probabilities:

metrics =
BinClass.Evaluation.calibrate(rows,
objective: :balanced_accuracy,
false_positive_cost: 0.75
)

Named probability strategies can be calibrated and compared in one call:

selection =
BinClass.Evaluation.select(rows,
strategies: [
maximum: & &1.probabilities.maximum,
top_two_mean: & &1.probabilities.top_two_mean
],
label: :label,
false_positive_cost: 0.75
)
selection.strategy
#=> :top_two_mean

4. Save and Load

You can save the entire model (including tokenizer, parameters, decision policy, and metadata) to a single file.

BinClass.save(classifier, "my_model.bin")
# Load the model as an Nx.Serving struct (recommended for most apps)
serving = BinClass.load("my_model.bin")
# Or build a serving from an existing classifier without serializing it
serving = BinClass.serving(classifier, batch_size: 32)

5. Optimized Inference

There are two ways to run predictions:

A. Using Nx.Serving (High Throughput)

Recommended for web servers and concurrent applications. It handles automatic batching.

prediction = Nx.Serving.run(serving, "I love this library!")

B. Using a Compiled Predictor (Ultra-Low Latency)

Recommended for CLI tools or scripts where you want the lowest possible latency for single items by bypassing the serving overhead.

classifier = BinClass.load_classifier("my_model.bin")
predict = BinClass.compile_predictor(classifier)
result = predict.("This is ultra fast.")

C. Inspecting a Complete Document

Compile a document predictor when an input may exceed the classifier's fixed vector length. It scans the complete string using overlapping token windows and returns every chunk with its exact byte span and raw prediction:

classifier = BinClass.load_classifier("my_model.bin")
predict_document =
BinClass.compile_document_predictor(classifier,
chunk_overlap: 128,
batch_size: 32
)
%{status: :ok, chunks: chunks} = predict_document.(large_document)
Enum.each(chunks, fn chunk ->
IO.inspect({
chunk.start_byte,
chunk.end_byte,
chunk.token_count,
chunk.active_token_count,
chunk.prediction.probabilities
})
end)

The predictor accepts one document at a time. It does not normalize, truncate, aggregate, or apply a separate document-level decision. Byte offsets refer to the exact input string, with an inclusive start and exclusive end. A chunk's :token_count is its encoded size, while :active_token_count excludes padding special tokens and controls minimum-input validation. Individual chunks that fail validation retain their raw probabilities.

Examples

Check out the examples/ directory for scripts demonstrating various use cases:

Public API

The supported API is organized around these modules:

Module Purpose
BinClass Primary entry point: train, evaluate, summarize, format reports, save/load classifiers, build servings and compiled predictors.
BinClass.Dataset Inspect dataset distributions and word/character statistics (summary/1, format_summary/1), and perform stratified train/val/test splits (split/2).
BinClass.Trainer Train complete BinClass.Classifier artifacts from labeled text.
BinClass.Evaluation Evaluate trained classifiers on datasets, compute confusion matrices, calibrate decision thresholds, or compare probability strategies.
BinClass.Model List and directly build the supported Axon neural architectures.
BinClass.Tokenizer Train tokenizers and read the metadata needed for vectorization.
BinClass.Vectorizer Encode text into fixed-length padded token vectors.
BinClass.Classifier The complete trained classifier artifact consumed by persistence and inference APIs.

Modules hidden from the generated documentation are implementation details and may change without notice. Use the BinClass entry points for persistence and inference instead of calling serialization, serving, or predictor internals directly.

Features

Model Architectures

Classifiers store one of these named architectures:

No architecture implicitly changes label thresholds or rejects short inputs. Those behaviors are configured through :decision_policy and :minimum_input_tokens.

Numeric architecture identifiers are not accepted. Serialized classifiers must contain one of the names above.

Production Notes

For production training, start with argmax unless validation results justify a different operating point:

classifier = BinClass.train(data,
epochs: 5,
architecture: :parallel_cnn,
vector_length: 512,
decision_policy:
{:optimize_threshold,
[objective: :balanced_accuracy, false_positive_cost: 0.5]},
label_smoothing: 0.02,
minimum_input_tokens: 64
)

The explicit decision_policy and minimum_input_tokens are saved with the model and reused identically by BinClass.load/2 and BinClass.compile_predictor/2. Raw probabilities are returned for every input, including inputs that fail minimum-length validation.