v2: add embedding tests, multilingual embedding support
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import XCTest
|
||||
@testable import MarkBase
|
||||
|
||||
final class EmbeddingTest: XCTestCase {
|
||||
var engine: MarkBaseEngine!
|
||||
var model: E4BModel!
|
||||
var embeddingModel: TextEmbeddingModel!
|
||||
let modelDir = "/Users/accusys/MarkBaseEngine/models/E4B-MarkBase"
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
guard FileManager.default.fileExists(atPath: modelDir + "/model.safetensors") else {
|
||||
return
|
||||
}
|
||||
engine = try? MarkBaseEngine(autoCompile: true)
|
||||
model = try? E4BModel(modelDir: modelDir, engine: engine, maxContextLength: 512)
|
||||
embeddingModel = try? TextEmbeddingModel(modelDir: modelDir, engine: engine, config: TextEmbeddingConfig())
|
||||
}
|
||||
|
||||
func testEmbeddingDimension() throws {
|
||||
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||
let embedding = try embeddingModel.embed(text: "Hello world")
|
||||
XCTAssertEqual(embedding.count, 2560, "Embedding dimension should be 2560")
|
||||
}
|
||||
|
||||
func testEmbeddingNormalized() throws {
|
||||
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||
let embedding = try embeddingModel.embed(text: "Test text")
|
||||
let norm = sqrt(embedding.reduce(0) { $0 + $1 * $1 })
|
||||
XCTAssertEqual(norm, 1.0, accuracy: 0.001, "Embedding should be L2 normalized")
|
||||
}
|
||||
|
||||
func testSimilarSentences() throws {
|
||||
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||
let e1 = try embeddingModel.embed(text: "The cat is sitting on the mat")
|
||||
let e2 = try embeddingModel.embed(text: "A cat rests on a rug")
|
||||
let e3 = try embeddingModel.embed(text: "The stock market crashed today")
|
||||
|
||||
let sim12 = cosineSimilarity(e1, e2)
|
||||
let sim13 = cosineSimilarity(e1, e3)
|
||||
print("Similar(cat, cat): \(sim12)")
|
||||
print("Similar(cat, stock): \(sim13)")
|
||||
XCTAssertGreaterThan(sim12, sim13, "Similar sentences should have higher cosine similarity")
|
||||
}
|
||||
|
||||
func testDifferentLengths() throws {
|
||||
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||
let e1 = try embeddingModel.embed(text: "Hi")
|
||||
let e2 = try embeddingModel.embed(text: "This is a much longer sentence with many words")
|
||||
XCTAssertEqual(e1.count, e2.count, "Embeddings should have same dimension regardless of input length")
|
||||
XCTAssertEqual(e1.count, 2560)
|
||||
}
|
||||
|
||||
func testEmptyInput() throws {
|
||||
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||
let embedding = try embeddingModel.embed(text: "")
|
||||
XCTAssertEqual(embedding.count, 0, "Empty input should return empty embedding")
|
||||
}
|
||||
|
||||
func testBatchEmbedding() throws {
|
||||
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||
let texts = ["Hello", "World", "Test"]
|
||||
let embeddings = try embeddingModel.embedBatch(texts: texts)
|
||||
XCTAssertEqual(embeddings.count, 3)
|
||||
for embedding in embeddings {
|
||||
XCTAssertEqual(embedding.count, 2560)
|
||||
}
|
||||
}
|
||||
|
||||
private func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Float {
|
||||
guard a.count == b.count, !a.isEmpty else { return 0 }
|
||||
var dot: Float = 0, normA: Float = 0, normB: Float = 0
|
||||
for i in 0..<a.count {
|
||||
dot += a[i] * b[i]
|
||||
normA += a[i] * a[i]
|
||||
normB += b[i] * b[i]
|
||||
}
|
||||
return dot / (sqrt(normA) * sqrt(normB))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import XCTest
|
||||
@testable import MarkBase
|
||||
|
||||
final class MultilangEmbeddingTest: XCTestCase {
|
||||
var engine: MarkBaseEngine!
|
||||
var embeddingModel: TextEmbeddingModel!
|
||||
let modelDir = "/Users/accusys/MarkBaseEngine/models/E4B-MarkBase"
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
guard FileManager.default.fileExists(atPath: modelDir + "/model.safetensors") else { return }
|
||||
engine = try? MarkBaseEngine(autoCompile: true)
|
||||
embeddingModel = try? TextEmbeddingModel(modelDir: modelDir, engine: engine, config: TextEmbeddingConfig())
|
||||
}
|
||||
|
||||
func testMultilangEmbeddings() throws {
|
||||
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||
|
||||
let texts: [String: String] = [
|
||||
"en": "The weather is beautiful today",
|
||||
"zh": "今天天氣很好",
|
||||
"ja": "今日は天気がいいです",
|
||||
"ko": "오늘 날씨가 좋습니다",
|
||||
"es": "El clima está hermoso hoy",
|
||||
"fr": "Il fait beau aujourd'hui",
|
||||
"de": "Das Wetter ist heute schön",
|
||||
"ru": "Сегодня прекрасная погода",
|
||||
"ar": "الطقس جميل اليوم",
|
||||
"hi": "आज मौसम बहुत सुंदर है"
|
||||
]
|
||||
|
||||
var embeddings: [String: [Float]] = [:]
|
||||
for (lang, text) in texts {
|
||||
let emb = try embeddingModel.embed(text: text)
|
||||
XCTAssertEqual(emb.count, 2560, "\(lang) embedding dimension")
|
||||
let norm = sqrt(emb.reduce(0) { $0 + $1 * $1 })
|
||||
XCTAssertEqual(norm, 1.0, accuracy: 0.001, "\(lang) embedding normalized")
|
||||
embeddings[lang] = emb
|
||||
print("\(lang): OK (norm=\(String(format: "%.4f", norm)))")
|
||||
}
|
||||
|
||||
// Cross-language similarity (en-zh should be higher than en-ru for weather context)
|
||||
let enEmb = embeddings["en"]!
|
||||
let zhEmb = embeddings["zh"]!
|
||||
let jaEmb = embeddings["ja"]!
|
||||
|
||||
let simEnZh = cosineSimilarity(enEmb, zhEmb)
|
||||
let simEnJa = cosineSimilarity(enEmb, jaEmb)
|
||||
print("EN-ZH similarity: \(String(format: "%.4f", simEnZh))")
|
||||
print("EN-JA similarity: \(String(format: "%.4f", simEnJa))")
|
||||
}
|
||||
|
||||
func testCrossLingualSemanticSimilarity() throws {
|
||||
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||
|
||||
// Same meaning, different languages
|
||||
let enCat = try embeddingModel.embed(text: "The cat is sleeping")
|
||||
let zhCat = try embeddingModel.embed(text: "貓在睡覺")
|
||||
let enDog = try embeddingModel.embed(text: "The dog is running")
|
||||
|
||||
let simSame = cosineSimilarity(enCat, zhCat)
|
||||
let simDiff = cosineSimilarity(enCat, enDog)
|
||||
print("EN(cat) - ZH(cat): \(String(format: "%.4f", simSame))")
|
||||
print("EN(cat) - EN(dog): \(String(format: "%.4f", simDiff))")
|
||||
print("Cross-lingual > Same-lingual different topic: \(simSame > simDiff)")
|
||||
}
|
||||
|
||||
private func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Float {
|
||||
guard a.count == b.count, !a.isEmpty else { return 0 }
|
||||
var dot: Float = 0, normA: Float = 0, normB: Float = 0
|
||||
for i in 0..<a.count {
|
||||
dot += a[i] * b[i]
|
||||
normA += a[i] * a[i]
|
||||
normB += b[i] * b[i]
|
||||
}
|
||||
return dot / (sqrt(normA) * sqrt(normB))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import MarkBase
|
||||
|
||||
final class Timing26BTest: XCTestCase {
|
||||
let modelDir = "/Users/accusys/MarkBaseEngine/models/gemma-4-26b-standard"
|
||||
var engine: MarkBaseEngine!
|
||||
var model: E4BModel!
|
||||
let maxCtx = 128
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
guard FileManager.default.fileExists(atPath: modelDir + "/model.safetensors") else {
|
||||
return
|
||||
}
|
||||
let t0 = Date()
|
||||
engine = try? MarkBaseEngine(autoCompile: true)
|
||||
model = try? E4BModel(modelDir: modelDir, engine: engine, maxContextLength: maxCtx)
|
||||
let loadTime = Date().timeIntervalSince(t0)
|
||||
print("Total init: \(String(format: "%.1f", loadTime))s")
|
||||
}
|
||||
|
||||
func testForwardTiming() throws {
|
||||
try XCTSkipIf(model == nil, "26B model not found")
|
||||
for i in 0..<5 {
|
||||
let t = Date()
|
||||
_ = try model.forward(tokenId: 100 + i, position: i)
|
||||
let ft = Date().timeIntervalSince(t)
|
||||
print("Forward \(i+1): \(String(format: "%.3f", ft))s")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user