Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions swift/Sources/CoreAIShared/Runtime/VectorMath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,23 @@ public func sigmoid(_ x: Float) -> Float {
let e = Foundation.exp(x)
return e / (1 + e)
}

/// Cosine similarity of two equal-length vectors, accumulated in `Double` and returned as `Float`.
///
/// Length-mismatched inputs score `0`. For zero-norm inputs the convention matches the parity
/// harnesses: two zero-norm (or empty) vectors are identical and score `1`, while a single
/// zero-norm vector has no direction to compare and scores `0`.
public func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Float {
guard a.count == b.count else { return 0 }
var dot = 0.0
var normA = 0.0
var normB = 0.0
for i in a.indices {
dot += Double(a[i]) * Double(b[i])
normA += Double(a[i]) * Double(a[i])
normB += Double(b[i]) * Double(b[i])
}
let denom = (normA * normB).squareRoot()
guard denom > 0 else { return normA == normB ? 1 : 0 }
return Float(dot / denom)
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,20 +102,6 @@ extension DiffusionRunner {

// MARK: - Helpers

private func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Float {
guard a.count == b.count, !a.isEmpty else { return 0 }
var dot: Float = 0
var normA: Float = 0
var normB: Float = 0
for i in 0..<a.count {
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
let denom = sqrt(normA) * sqrt(normB)
return denom > 0 ? dot / denom : 0
}

/// Widen a `[Float]` buffer into an `NDArray` of the requested scalar type.
///
/// Not diffusion- or model-specific. Float targets defer to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -603,20 +603,6 @@ struct ImageSegmenterCLI: AsyncParsableCommand {
return Float(10.0 * log10((peak * peak) / mse))
}

private func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Float {
guard a.count == b.count, !a.isEmpty else { return 0 }
var dot: Double = 0
var normA: Double = 0
var normB: Double = 0
for i in 0..<a.count {
dot += Double(a[i]) * Double(b[i])
normA += Double(a[i]) * Double(a[i])
normB += Double(b[i]) * Double(b[i])
}
let denom = (normA * normB).squareRoot()
return denom > 0 ? Float(dot / denom) : 0
}

/// Return the leading slice of `ids` up to and including the first EOT
/// (token id 49407) after position 0. Trailing pad tokens are excluded.
/// If no EOT is found, returns the full sequence.
Expand Down
15 changes: 1 addition & 14 deletions swift/Sources/Tools/speech-recognizer/SpeechParity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ private func metricRow(
ok: false)
}
let p = psnr(actual, ref)
let c = cosineSimilarity(actual, ref)
let c = Double(cosineSimilarity(actual, ref))
let maxAbs = zip(actual, ref).reduce(Float(0)) { max($0, abs($1.0 - $1.1)) }
let ok = (p.isInfinite || p >= psnrFloor) && c >= cosineFloor
let psnrStr = p.isInfinite ? "INF" : String(format: "%.2f", p)
Expand All @@ -491,16 +491,3 @@ private func psnr(_ a: [Float], _ b: [Float]) -> Double {
if peak == 0 { peak = 1 }
return 10 * log10(peak * peak / mse)
}

private func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Double {
var dot = 0.0
var na = 0.0
var nb = 0.0
for i in a.indices {
dot += Double(a[i]) * Double(b[i])
na += Double(a[i]) * Double(a[i])
nb += Double(b[i]) * Double(b[i])
}
guard na > 0 && nb > 0 else { return na == nb ? 1 : 0 }
return dot / (na.squareRoot() * nb.squareRoot())
}
20 changes: 20 additions & 0 deletions swift/Tests/CoreAISharedTests/VectorMathTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,24 @@ struct VectorMathTests {
#expect(sigmoid(-100) < 1e-40)
#expect(sigmoid(-100).isNaN == false)
}

// MARK: - cosineSimilarity

@Test("cosineSimilarity: identical, orthogonal, and opposite vectors")
func cosineKnownValues() {
#expect(abs(cosineSimilarity([1, 2, 3], [1, 2, 3]) - 1.0) < 1e-6)
#expect(abs(cosineSimilarity([1, 0], [0, 1])) < 1e-6)
#expect(abs(cosineSimilarity([1, 2, 3], [-1, -2, -3]) + 1.0) < 1e-6)
}

@Test("cosineSimilarity: zero-norm convention matches speech parity")
func cosineDegenerate() {
// Two zero-norm (or empty) vectors are identical and score 1; a single zero-norm vector
// has no direction to compare and scores 0; a length mismatch scores 0. This matches the
// speech parity convention so every harness that shares this helper agrees.
#expect(cosineSimilarity([], []) == 1)
#expect(cosineSimilarity([0, 0], [0, 0]) == 1)
#expect(cosineSimilarity([0, 0], [1, 2]) == 0)
#expect(cosineSimilarity([1, 2], [1, 2, 3]) == 0)
}
}