Section 5/171 menit

5. Custom MLFeatureProvider

5. Custom MLFeatureProvider

Generated code dari Xcode sudah membuat subclass MLFeatureProvider otomatis. Tapi untuk performa ekstrem atau input yang tidak standar, buat implementasi custom.

Mengapa Custom MLFeatureProvider?

  • Zero-copy: hindari konversi data yang tidak perlu
  • Lazy evaluation: hitung fitur hanya saat diminta model
  • Memory efficiency: tidak alokasi semua fitur sekaligus
  • Type flexibility: terima input dari sumber berbeda tanpa wrapper
swift
// MARK: - Custom Feature Provider untuk NLP Model

final class NLPFeatureProvider: MLFeatureProvider {
    private let tokenIDs: [Int32]
    private let attentionMask: [Int32]
    private let sequenceLength: Int

    // Lazy MLMultiArray — tidak dibuat sampai model request
    private var cachedInputIDs: MLMultiArray?
    private var cachedMask: MLMultiArray?

    init(tokens: [Int32], mask: [Int32]) {
        assert(tokens.count == mask.count)
        self.tokenIDs = tokens
        self.attentionMask = mask
        self.sequenceLength = tokens.count
    }

    // MLFeatureProvider protocol: nama feature yang tersedia
    var featureNames: Set<String> {
        return ["input_ids", "attention_mask"]
    }

    // MLFeatureProvider protocol: nilai untuk nama feature tertentu
    func featureValue(for featureName: String) -> MLFeatureValue? {
        switch featureName {
        case "input_ids":
            return MLFeatureValue(multiArray: inputIDsArray)
        case "attention_mask":
            return MLFeatureValue(multiArray: maskArray)
        default:
            return nil
        }
    }

    // Lazy computation: buat MLMultiArray hanya sekali, cache hasilnya
    private var inputIDsArray: MLMultiArray {
        if let cached = cachedInputIDs { return cached }
        let array = try! MLMultiArray(shape: [1, sequenceLength as NSNumber], dataType: .int32)
        tokenIDs.withUnsafeBufferPointer { buffer in
            let dst = array.dataPointer.assumingMemoryBound(to: Int32.self)
            dst.initialize(from: buffer.baseAddress!, count: buffer.count)
        }
        cachedInputIDs = array
        return array
    }

    private var maskArray: MLMultiArray {
        if let cached = cachedMask { return cached }
        let array = try! MLMultiArray(shape: [1, sequenceLength as NSNumber], dataType: .int32)
        attentionMask.withUnsafeBufferPointer { buffer in
            let dst = array.dataPointer.assumingMemoryBound(to: Int32.self)
            dst.initialize(from: buffer.baseAddress!, count: buffer.count)
        }
        cachedMask = array
        return array
    }
}

// Penggunaan
let tokenizer = BertTokenizer()
let (ids, mask) = tokenizer.tokenize(text: "Halo dunia")
let input = NLPFeatureProvider(tokens: ids, mask: mask)

let output = try model.prediction(from: input)
let logits = output.featureValue(for: "logits")?.multiArrayValue

Custom Feature Provider untuk Camera Feed

swift
// Zero-copy dari CMSampleBuffer ke Core ML input
final class CameraFeatureProvider: MLFeatureProvider {
    private let sampleBuffer: CMSampleBuffer
    private let imageConstraint: MLImageConstraint

    var featureNames: Set<String> { ["image"] }

    init(sampleBuffer: CMSampleBuffer, constraint: MLImageConstraint) {
        self.sampleBuffer = sampleBuffer
        self.imageConstraint = constraint
    }

    func featureValue(for featureName: String) -> MLFeatureValue? {
        guard featureName == "image",
              let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return nil
        }

        // MLFeatureValue langsung dari CVPixelBuffer — tidak perlu konversi ke UIImage
        return try? MLFeatureValue(
            pixelBuffer: pixelBuffer,
            constraint: imageConstraint
        )
    }
}

// Penggunaan: ambil constraint dari model description
let description = model.modelDescription
let imageFeatureDescription = description.inputDescriptionsByName["image"]
let constraint = imageFeatureDescription?.imageConstraint

let provider = CameraFeatureProvider(sampleBuffer: sampleBuffer, constraint: constraint!)
let output = try model.prediction(from: provider)