Section 13/171 menit

13. Testing Model ML

13. Testing Model ML

Unit Test untuk Prediksi

swift
import Testing
import CoreML

// MARK: - Model Tests

@Suite("Core ML Model Tests")
struct CoreMLTests {
    // Model loaded sekali untuk semua tests (mahal untuk di-load ulang)
    let model: MyClassifier

    init() throws {
        model = try MyClassifier(configuration: MLModelConfiguration())
    }

    @Test("Prediksi label untuk gambar kucing")
    func testCatClassification() throws {
        let testImage = loadTestImage(named: "cat_test")!
        let pixelBuffer = testImage.pixelBuffer(width: 224, height: 224)!
        let result = try model.prediction(image: pixelBuffer)
        #expect(result.classLabel == "cat")
        #expect(result.classLabelProbs["cat"]! > 0.9)
    }

    @Test("Confidence threshold untuk ambiguous input")
    func testLowConfidenceHandling() throws {
        let ambiguousImage = loadTestImage(named: "ambiguous_animal")!
        let pixelBuffer = ambiguousImage.pixelBuffer(width: 224, height: 224)!
        let result = try model.prediction(image: pixelBuffer)
        // Untuk input ambigu, tidak ada class yang > 90% confident
        let maxConfidence = result.classLabelProbs.values.max() ?? 0
        #expect(maxConfidence < 0.9, "Ambiguous input harus menghasilkan low confidence")
    }

    @Test("Latency di bawah threshold", .timeLimit(.milliseconds(50)))
    func testInferenceLatency() throws {
        let image = loadTestImage(named: "benchmark_image")!
        let pixelBuffer = image.pixelBuffer(width: 224, height: 224)!
        // Test ini gagal jika inferensi > 50ms
        _ = try model.prediction(image: pixelBuffer)
    }

    @Test("Konsistensi prediksi untuk input yang sama")
    func testPredictionConsistency() throws {
        let image = loadTestImage(named: "consistent_test")!
        let pixelBuffer = image.pixelBuffer(width: 224, height: 224)!

        let predictions = try (0..<5).map { _ in
            try model.prediction(image: pixelBuffer).classLabel
        }

        // Semua prediksi harus sama untuk input yang sama
        #expect(Set(predictions).count == 1, "Prediksi harus deterministic")
    }

    private func loadTestImage(named name: String) -> UIImage? {
        UIImage(named: name, in: Bundle(for: type(of: self) as! AnyClass), compatibleWith: nil)
    }
}

Regression Testing untuk Model Update

swift
// Test untuk memastikan model baru tidak regresi dibanding model lama
struct ModelRegressionTests {
    func compareModels(
        newModelURL: URL,
        baselineModelURL: URL,
        testInputs: [MLFeatureProvider]
    ) throws -> RegressionReport {
        let newModel = try MLModel(contentsOf: newModelURL)
        let baselineModel = try MLModel(contentsOf: baselineModelURL)

        var agreements = 0
        var disagreements: [(Int, String, String)] = []

        for (index, input) in testInputs.enumerated() {
            let newOutput = try newModel.prediction(from: input)
            let baselineOutput = try baselineModel.prediction(from: input)

            let newLabel = newOutput.featureValue(for: "classLabel")?.stringValue ?? ""
            let baselineLabel = baselineOutput.featureValue(for: "classLabel")?.stringValue ?? ""

            if newLabel == baselineLabel {
                agreements += 1
            } else {
                disagreements.append((index, baselineLabel, newLabel))
            }
        }

        let agreementRate = Double(agreements) / Double(testInputs.count)
        return RegressionReport(
            agreementRate: agreementRate,
            disagreements: disagreements,
            passed: agreementRate >= 0.95  // 95% agreement threshold
        )
    }

    struct RegressionReport {
        let agreementRate: Double
        let disagreements: [(index: Int, baseline: String, new: String)]
        let passed: Bool
    }
}