///|
pub(all) struct MassAudit {
history : Array[String]
mass : Double
deviation : Double
normalized : Bool
zero_events : Int32
backed_off_events : Int32
} derive(Debug, Eq)
///|
pub(all) struct ModelSummary {
order : Int32
records : Int32
vocabulary : Int32
counts : Array[Int32]
impossible_records : Int32
positive_backoffs : Int32
has_unknown : Bool
has_bos : Bool
has_eos : Bool
} derive(Debug, Eq)
///|
pub fn Model::summary(self : Model) -> ModelSummary {
let mut impossible_records = 0
let mut positive_backoffs = 0
for gram in self.grams {
if gram.log_probability == @double.neg_infinity {
impossible_records += 1
}
match gram.log_backoff {
Some(b) => if b > 0.0 { positive_backoffs += 1 }
None => ()
}
}
{
order: self.order,
records: self.grams.length(),
vocabulary: self.vocabulary.length(),
counts: self.counts(),
impossible_records,
positive_backoffs,
has_unknown: self.contains(""),
has_bos: self.contains(""),
has_eos: self.contains(""),
}
}
///|
/// Sum the complete vocabulary with compensated summation. ARPA syntax alone
/// does not guarantee normalized conditional distributions.
pub fn Model::audit_mass(
self : Model,
history : Array[String],
tolerance~ : Double = 1.0e-8,
) -> MassAudit raise ArpaError {
if !finite_number(tolerance) || tolerance < 0.0 {
raise Invalid("invalid audit tolerance")
}
if self.vocabulary.length() > self.limits.max_work / self.order {
raise Budget("mass audit work limit")
}
let mut sum = 0.0
let mut correction = 0.0
let mut zeros = 0
let mut backed_off = 0
for token in self.vocabulary {
let score = self.conditional(history, token, oov=Reject)
if score.log_probability == @double.neg_infinity {
zeros += 1
}
if score.backoff_steps > 0 {
backed_off += 1
}
let p = @math.pow(10.0, score.log_probability)
if !finite_number(p) {
raise Invalid("conditional probability overflow")
}
let adjusted = p - correction
let next = sum + adjusted
correction = next - sum - adjusted
sum = next
}
if !finite_number(sum) {
raise Invalid("probability mass overflow")
}
let deviation = (sum - 1.0).abs()
{
history: history.copy(),
mass: sum,
deviation,
normalized: deviation <= tolerance,
zero_events: zeros,
backed_off_events: backed_off,
}
}
///|
/// Audits a deterministic bounded set of observed histories and the empty one.
/// The caller chooses a cap because exhaustive V^order enumeration is unbounded.
pub fn Model::audit_contexts(
self : Model,
maximum : Int32,
tolerance~ : Double = 1.0e-8,
) -> Array[MassAudit] raise ArpaError {
if maximum < 1 || maximum > self.limits.max_records {
raise Invalid("invalid context audit cap")
}
let per_context = self.vocabulary.length() * self.order
if per_context > 0 && maximum > self.limits.max_work / per_context {
raise Budget("context audit work limit")
}
let histories : Array[Array[String]] = [[]]
let seen : Map[String, Bool] = Map([])
seen[""] = true
let grams = self.records()
grams.sort_by(fn(a, b) { gram_key(a.tokens).compare(gram_key(b.tokens)) })
for gram in grams {
if gram.tokens.length() >= self.order || histories.length() >= maximum {
continue
}
let key = gram_key(gram.tokens)
if !seen.contains(key) {
seen[key] = true
histories.push(gram.tokens.copy())
}
}
let reports = []
for history in histories {
reports.push(self.audit_mass(history, tolerance~))
}
reports
}
///|
/// Cross-entropy in bits per predicted event; None means no predicted events.
pub fn SentenceScore::cross_entropy_bits(self : SentenceScore) -> Double? {
if self.predicted == 0 {
return None
}
Some(-self.log_probability / self.predicted.to_double() * @math.log2(10.0))
}
///|
pub fn CorpusScore::cross_entropy_bits(self : CorpusScore) -> Double? {
if self.predicted == 0 {
return None
}
Some(-self.log_probability / self.predicted.to_double() * @math.log2(10.0))
}
///|
pub extend MassAudit with @debug.Debug::{to_repr}
///|
pub extend MassAudit with Eq::{equal, not_equal}
///|
pub extend ModelSummary with @debug.Debug::{to_repr}
///|
pub extend ModelSummary with Eq::{equal, not_equal}