///|
/// Shared numerical helpers used by the online learning models.
///
/// The helpers in this file intentionally avoid hidden allocations in hot
/// paths. They also provide safe variants for callers that receive data from
/// an external stream and cannot assume that dimensions are correct.
///|
pub struct ValidationReport {
valid : Bool
message : String
} derive(ToJson, FromJson, Debug, Eq)
///|
pub fn ValidationReport::ok() -> ValidationReport {
{ valid: true, message: "ok" }
}
///|
pub fn ValidationReport::error(message : String) -> ValidationReport {
{ valid: false, message }
}
///|
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
self.valid
}
///|
pub fn ValidationReport::message(self : ValidationReport) -> String {
self.message
}
///|
pub fn valid_dimension(dim : Int) -> Bool {
dim >= 0
}
///|
pub fn validate_dimension(dim : Int) -> ValidationReport {
if dim < 0 {
ValidationReport::error("dimension must be non-negative")
} else {
ValidationReport::ok()
}
}
///|
pub fn validate_vector(features : Array[Double], dim : Int) -> ValidationReport {
if dim < 0 {
ValidationReport::error("dimension must be non-negative")
} else if features.length() != dim {
ValidationReport::error(
"feature dimension mismatch: expected {dim}, got {features.length()}",
)
} else {
ValidationReport::ok()
}
}
///|
pub fn validate_probability(value : Double) -> ValidationReport {
if value < 0.0 || value > 1.0 {
ValidationReport::error("probability must be in [0, 1]")
} else {
ValidationReport::ok()
}
}
///|
pub fn validate_label(value : Double) -> ValidationReport {
if value == 0.0 || value == 1.0 {
ValidationReport::ok()
} else {
ValidationReport::error("binary label must be 0 or 1")
}
}
///|
pub fn clamp(value : Double, lower : Double, upper : Double) -> Double {
if value < lower {
lower
} else if value > upper {
upper
} else {
value
}
}
///|
pub fn clamp_probability(value : Double) -> Double {
clamp(value, 1.0e-15, 1.0 - 1.0e-15)
}
///|
pub fn sigmoid(value : Double) -> Double {
if value >= 0.0 {
1.0 / (1.0 + @math.exp(-value))
} else {
let e = @math.exp(value)
e / (1.0 + e)
}
}
///|
pub fn softplus(value : Double) -> Double {
if value > 20.0 {
value
} else if value < -20.0 {
@math.exp(value)
} else {
@math.ln(1.0 + @math.exp(value))
}
}
///|
pub fn logit(probability : Double) -> Double {
let p = clamp_probability(probability)
@math.ln(p / (1.0 - p))
}
///|
pub fn copy_vector(values : Array[Double]) -> Array[Double] {
Array::makei(values.length(), i => values[i])
}
///|
pub fn dot_product(left : Array[Double], right : Array[Double]) -> Double {
let limit = if left.length() < right.length() {
left.length()
} else {
right.length()
}
let mut total = 0.0
for i in 0.. Double? {
if left.length() != right.length() {
None
} else {
Some(dot_product(left, right))
}
}
///|
pub fn squared_norm(values : Array[Double]) -> Double {
dot_product(values, values)
}
///|
pub fn l1_norm(values : Array[Double]) -> Double {
let mut total = 0.0
for value in values {
total += if value < 0.0 { -value } else { value }
}
total
}
///|
pub fn max_abs(values : Array[Double]) -> Double {
let mut result = 0.0
for value in values {
let magnitude = if value < 0.0 { -value } else { value }
if magnitude > result {
result = magnitude
}
}
result
}
///|
pub fn sum_values(values : Array[Double]) -> Double {
let mut total = 0.0
for value in values {
total += value
}
total
}
///|
pub fn mean_values(values : Array[Double]) -> Double {
if values.is_empty() {
0.0
} else {
sum_values(values) / values.length().to_double()
}
}
///|
pub fn variance_values(values : Array[Double]) -> Double {
if values.length() < 2 {
0.0
} else {
let mean = mean_values(values)
let mut total = 0.0
for value in values {
let delta = value - mean
total += delta * delta
}
total / (values.length() - 1).to_double()
}
}
///|
pub fn standard_deviation(values : Array[Double]) -> Double {
let variance = variance_values(values)
if variance <= 0.0 {
0.0
} else {
variance.sqrt()
}
}
///|
pub fn add_scaled_in_place(
target : Array[Double],
source : Array[Double],
scale : Double,
) -> Unit {
let limit = if target.length() < source.length() {
target.length()
} else {
source.length()
}
for i in 0.. Unit {
let limit = if target.length() < source.length() {
target.length()
} else {
source.length()
}
for i in 0.. Array[Double] {
Array::makei(values.length(), i => values[i] * scale)
}
///|
pub fn add_values(left : Array[Double], right : Array[Double]) -> Array[Double] {
let size = if left.length() > right.length() {
left.length()
} else {
right.length()
}
Array::makei(size, i => {
let a = left.get(i).unwrap_or(0.0)
let b = right.get(i).unwrap_or(0.0)
a + b
})
}
///|
pub fn subtract_values(
left : Array[Double],
right : Array[Double],
) -> Array[Double] {
let size = if left.length() > right.length() {
left.length()
} else {
right.length()
}
Array::makei(size, i => {
let a = left.get(i).unwrap_or(0.0)
let b = right.get(i).unwrap_or(0.0)
a - b
})
}
///|
pub fn hadamard_product(
left : Array[Double],
right : Array[Double],
) -> Array[Double] {
let size = if left.length() < right.length() {
left.length()
} else {
right.length()
}
Array::makei(size, i => left[i] * right[i])
}
///|
pub fn normalize_l2(values : Array[Double]) -> Array[Double] {
let length = squared_norm(values).sqrt()
if length <= 1.0e-15 {
copy_vector(values)
} else {
scale_values(values, 1.0 / length)
}
}
///|
pub fn cosine_similarity(left : Array[Double], right : Array[Double]) -> Double {
let denominator = squared_norm(left).sqrt() * squared_norm(right).sqrt()
if denominator <= 1.0e-15 {
0.0
} else {
dot_product(left, right) / denominator
}
}
///|
pub fn argmax(values : Array[Double]) -> Int? {
if values.is_empty() {
None
} else {
let mut best_index = 0
let mut best_value = values[0]
for i in 1.. best_value {
best_index = i
best_value = values[i]
}
}
Some(best_index)
}
}
///|
pub fn top_index(values : Array[Double], rank : Int) -> Int? {
if rank < 0 || rank >= values.length() {
None
} else {
let order = Array::makei(values.length(), i => i)
order.sort_by((left, right) => {
if values[left] > values[right] {
-1
} else if values[left] < values[right] {
1
} else {
left - right
}
})
Some(order[rank])
}
}
///|
pub fn one_hot(index : Int, size : Int) -> Array[Double] {
let result = Array::make(if size < 0 { 0 } else { size }, 0.0)
if index >= 0 && index < result.length() {
result[index] = 1.0
}
result
}
///|
pub fn probabilities_from_logits(logits : Array[Double]) -> Array[Double] {
if logits.is_empty() {
[]
} else {
let mut maximum = logits[0]
for value in logits {
if value > maximum {
maximum = value
}
}
let values = Array::makei(logits.length(), i => {
@math.exp(logits[i] - maximum)
})
let denominator = sum_values(values)
if denominator <= 0.0 {
Array::make(logits.length(), 1.0 / logits.length().to_double())
} else {
scale_values(values, 1.0 / denominator)
}
}
}
///|
pub fn binary_cross_entropy(probability : Double, label : Double) -> Double {
let p = clamp_probability(probability)
-(label * @math.ln(p) + (1.0 - label) * @math.ln(1.0 - p))
}
///|
pub fn squared_error(prediction : Double, label : Double) -> Double {
let error = prediction - label
error * error
}
///|
pub fn absolute_error(prediction : Double, label : Double) -> Double {
let error = prediction - label
if error < 0.0 {
-error
} else {
error
}
}
///|
pub fn hinge_loss(score : Double, label : Double) -> Double {
let margin = 1.0 - score * label
if margin > 0.0 {
margin
} else {
0.0
}
}
///|
pub fn smooth_l1_loss(error : Double, beta? : Double = 1.0) -> Double {
let magnitude = if error < 0.0 { -error } else { error }
if magnitude < beta {
0.5 * error * error / beta
} else {
magnitude - 0.5 * beta
}
}
///|
pub fn r2_score(predictions : Array[Double], labels : Array[Double]) -> Double {
if predictions.is_empty() || predictions.length() != labels.length() {
0.0
} else {
let mean = mean_values(labels)
let mut residual = 0.0
let mut total = 0.0
for i in 0.. Bool {
if left.length() != right.length() {
false
} else {
let mut result = true
for i in 0.. tolerance {
result = false
}
}
result
}
}
///|
pub fn is_non_decreasing(values : Array[Double]) -> Bool {
let mut result = true
for i in 1.. Array[Double] {
let result = Array::make(values.length(), 0.0)
let mut total = 0.0
for i in 0.. Double {
let size = if values.length() < weights.length() {
values.length()
} else {
weights.length()
}
let mut weighted = 0.0
let mut total_weight = 0.0
for i in 0.. Double {
let mean = weighted_mean(values, weights)
let size = if values.length() < weights.length() {
values.length()
} else {
weights.length()
}
let mut weighted = 0.0
let mut total_weight = 0.0
for i in 0.. Double {
let rate = clamp(smoothing, 0.0, 1.0)
rate * value + (1.0 - rate) * previous
}