///|
/// A probability mixture of models with exactly the same vocabulary.
pub struct Mixture {
models : Array[Model]
weights : Array[Double]
limits : Limits
}
///|
pub fn Mixture::new(
models : Array[Model],
weights : Array[Double],
limits~ : Limits = Limits::default(),
) -> Mixture raise ArpaError {
limits.validate()
if models.is_empty() || models.length() != weights.length() {
raise Invalid("mixture dimensions")
}
if models.length() > limits.max_order {
raise Budget("mixture component limit")
}
let vocabulary = models[0].vocabulary()
let mut total = 0.0
for i = 0; i < weights.length(); i = i + 1 {
let w = weights[i]
if !finite_number(w) || w < 0.0 {
raise Invalid("mixture weights must be finite nonnegative")
}
total += w
if models[i].vocabulary() != vocabulary {
raise Invalid("mixture requires identical vocabularies")
}
}
if !finite_number(total) || total <= 0.0 {
raise Invalid("mixture requires positive finite total weight")
}
{ models: models.copy(), weights: weights.map(w => w / total), limits, }
}
///|
pub fn Mixture::weights(self : Mixture) -> Array[Double] {
self.weights.copy()
}
///|
/// Stable log10-sum-exp; mixture is of probabilities, not logarithms.
pub fn Mixture::conditional(
self : Mixture,
history : Array[String],
token : String,
oov~ : OovPolicy = MapToUnknown,
) -> Double raise ArpaError {
if history.length() > self.limits.max_tokens {
raise Budget("mixture history limit")
}
let terms = []
let mut maximum = @double.neg_infinity
for i = 0; i < self.models.length(); i = i + 1 {
if self.weights[i] == 0.0 {
continue
}
let score = self.models[i].conditional(history, token, oov~)
let term = @math.log10(self.weights[i]) + score.log_probability
terms.push(term)
if term > maximum {
maximum = term
}
}
if maximum == @double.neg_infinity {
return maximum
}
let mut sum = 0.0
for term in terms {
sum += @math.pow(10.0, term - maximum)
}
let result = maximum + @math.log10(sum)
if !valid_log(result) {
raise Invalid("mixture arithmetic overflow")
}
result
}
///|
pub fn Mixture::sentence(
self : Mixture,
tokens : Array[String],
options~ : ScoreOptions = ScoreOptions::default(),
) -> SentenceScore raise ArpaError {
checked_token_budget(tokens, self.limits)
let base = self.models[0]
if options.bos && !base.contains("") {
raise Missing("BOS unigram")
}
if options.eos && !base.contains("") {
raise Missing("EOS unigram")
}
let history = if options.bos { [""] } else { [] }
let mut total = 0.0
let mut oovs = 0
let results = []
let count = tokens.length() + (if options.eos { 1 } else { 0 })
for i = 0; i < count; i = i + 1 {
let token = if i < tokens.length() { tokens[i] } else { "" }
let mapped = base.map_token(token, options.oov)
let probability = self.conditional(history, token, oov=options.oov)
total = add_log(total, probability)
if token != mapped {
oovs += 1
}
results.push({
token,
mapped,
log_probability: probability,
matched_order: 0,
backoff_steps: 0,
backoff_log: 0.0,
oov: token != mapped,
})
history.push(mapped)
if history.length() > self.limits.max_order - 1 {
let _ = history.remove(0)
}
}
{
log_probability: total,
predicted: count,
oov_count: oovs,
tokens: results,
}
}