Section 15/161 menit

15. Real Use Cases

15. Real Use Cases

Use Case 1: Klasifikasi Foto Produk di E-Commerce

Fitur: User foto produk → app deteksi kategori otomatis dan isi form.

swift
import CoreML
import Vision
import UIKit

// Model: MobileNetV2 fine-tuned untuk 100 kategori produk
// Training: Create ML dengan 10.000 foto produk per kategori
// Ukuran model: ~8MB (Float16 quantized)

final class ProductClassificationService {
    static let shared = ProductClassificationService()
    
    private lazy var request: VNCoreMLRequest = {
        let config = MLModelConfiguration()
        config.computeUnits = .all
        let model = try! ProductClassifier(configuration: config)
        let visionModel = try! VNCoreMLModel(for: model.model)
        
        let req = VNCoreMLRequest(model: visionModel)
        req.imageCropAndScaleOption = .centerCrop
        return req
    }()
    
    struct ProductCategory {
        let id: String
        let name: String
        let confidence: Float
        let suggestedPrice: ClosedRange<Double>?
    }
    
    private let categoryMetadata: [String: (name: String, priceRange: ClosedRange<Double>?)] = [
        "electronics_phone": ("Smartphone", 1_000_000...15_000_000),
        "electronics_laptop": ("Laptop", 3_000_000...30_000_000),
        "fashion_tshirt": ("Kaos", 50_000...500_000),
        // ... 97 kategori lainnya
    ]
    
    func classify(photo: UIImage) async throws -> [ProductCategory] {
        guard let cgImage = photo.cgImage else { throw ClassificationError.invalidImage }
        
        let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        
        return try await withCheckedThrowingContinuation { continuation in
            DispatchQueue.global(qos: .userInitiated).async {
                do {
                    try handler.perform([self.request])
                    
                    guard let results = self.request.results as? [VNClassificationObservation] else {
                        continuation.resume(returning: [])
                        return
                    }
                    
                    let categories = results.prefix(3).compactMap { obs -> ProductCategory? in
                        guard obs.confidence > 0.1 else { return nil }
                        let meta = self.categoryMetadata[obs.identifier]
                        return ProductCategory(
                            id: obs.identifier,
                            name: meta?.name ?? obs.identifier,
                            confidence: obs.confidence,
                            suggestedPrice: meta?.priceRange
                        )
                    }
                    
                    continuation.resume(returning: Array(categories))
                } catch {
                    continuation.resume(throwing: error)
                }
            }
        }
    }
}

// SwiftUI Integration
struct ProductPhotoView: View {
    @State private var classificationResults: [ProductClassificationService.ProductCategory] = []
    @State private var isClassifying = false
    
    var body: some View {
        VStack {
            // Camera/Gallery picker...
            
            if isClassifying {
                ProgressView("Mendeteksi produk...")
            } else {
                ForEach(classificationResults, id: \.id) { category in
                    HStack {
                        Text(category.name)
                        Spacer()
                        Text("\(Int(category.confidence * 100))%")
                            .foregroundColor(.secondary)
                    }
                }
            }
        }
        .onChange(of: /* selectedImage */) { _, image in
            Task {
                isClassifying = true
                if let image {
                    classificationResults = (try? await ProductClassificationService.shared.classify(photo: image)) ?? []
                }
                isClassifying = false
            }
        }
    }
}

Use Case 2: Real-Time OCR + Document Intelligence

Fitur: Kamera arahkan ke struk/dokumen → ekstrak teks + klasifikasi jenis dokumen.

swift
import Vision
import NaturalLanguage

final class DocumentIntelligenceService {
    // Vision Text Recognition (tidak butuh Core ML model custom — built-in Apple)
    private lazy var textRequest: VNRecognizeTextRequest = {
        let req = VNRecognizeTextRequest()
        req.recognitionLevel = .accurate
        req.recognitionLanguages = ["id-ID", "en-US"]
        req.usesLanguageCorrection = true
        return req
    }()
    
    // Custom Core ML model untuk klasifikasi dokumen
    private lazy var docClassifier: NLModel = {
        let url = Bundle.main.url(forResource: "DocumentClassifier", withExtension: "mlmodelc")!
        return try! NLModel(contentsOf: url)
    }()
    
    enum DocumentType: String {
        case receipt = "receipt"
        case invoice = "invoice"
        case idCard = "id_card"
        case contract = "contract"
        case unknown = "unknown"
    }
    
    struct DocumentAnalysis {
        let rawText: String
        let documentType: DocumentType
        let confidence: Double
        let extractedFields: [String: String]
    }
    
    func analyze(image: UIImage) async throws -> DocumentAnalysis {
        guard let cgImage = image.cgImage else { throw DocumentError.invalidImage }
        
        // Step 1: OCR
        let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        try handler.perform([textRequest])
        
        let recognizedText = textRequest.results?
            .compactMap { $0.topCandidates(1).first?.string }
            .joined(separator: "\n") ?? ""
        
        // Step 2: Klasifikasi jenis dokumen
        let label = docClassifier.predictedLabel(for: recognizedText) ?? DocumentType.unknown.rawValue
        let documentType = DocumentType(rawValue: label) ?? .unknown
        let hypotheses = docClassifier.predictedLabelHypotheses(for: recognizedText, maximumCount: 1)
        let confidence = hypotheses[label] ?? 0
        
        // Step 3: Extract fields berdasarkan tipe dokumen
        let fields = extractFields(from: recognizedText, documentType: documentType)
        
        return DocumentAnalysis(
            rawText: recognizedText,
            documentType: documentType,
            confidence: confidence,
            extractedFields: fields
        )
    }
    
    private func extractFields(from text: String, documentType: DocumentType) -> [String: String] {
        var fields: [String: String] = [:]
        
        // NLTagger untuk Named Entity Recognition
        let tagger = NLTagger(tagSchemes: [.nameType, .lexicalClass])
        tagger.string = text
        
        tagger.enumerateTags(in: text.startIndex..<text.endIndex,
                            unit: .word,
                            scheme: .nameType,
                            options: [.omitWhitespace, .omitPunctuation]) { tag, range in
            guard let tag else { return true }
            let word = String(text[range])
            
            switch tag {
            case .organizationName:
                fields["merchant"] = word
            case .personalName:
                fields["customer"] = word
            default: break
            }
            return true
        }
        
        // Regex untuk tanggal dan nominal
        if let dateMatch = text.range(of: #"\d{2}[/-]\d{2}[/-]\d{4}"#, options: .regularExpression) {
            fields["date"] = String(text[dateMatch])
        }
        
        if let totalMatch = text.range(of: #"(?:TOTAL|Total):?\s*Rp\.?\s*[\d.,]+"#, options: .regularExpression) {
            fields["total"] = String(text[totalMatch])
        }
        
        return fields
    }
}

Use Case 3: On-Device Recommendation Engine

Fitur: Rekomendasi konten berdasarkan histori pengguna, tanpa data ke server.

swift
import CoreML

// Model: Collaborative Filtering yang di-convert dari PyTorch
// Input: user_embedding (64-dim) + item_features (128-dim)
// Output: relevance_score (Float)
// Training: Dilakukan di server, model di-distribute via App Store update

actor RecommendationEngine {
    private let model: ContentRecommender
    private var userEmbedding: [Float]
    
    init(userEmbedding: [Float]) throws {
        let config = MLModelConfiguration()
        config.computeUnits = .cpuAndNeuralEngine
        self.model = try ContentRecommender(configuration: config)
        self.userEmbedding = userEmbedding
    }
    
    struct ContentItem {
        let id: String
        let title: String
        let features: [Float]  // 128-dim feature vector
    }
    
    func rank(items: [ContentItem]) async throws -> [ContentItem] {
        // Score setiap item
        var scores: [(item: ContentItem, score: Float)] = []
        
        for item in items {
            let userArray = try MLMultiArray(shape: [64], dataType: .float32)
            let itemArray = try MLMultiArray(shape: [128], dataType: .float32)
            
            for (i, val) in userEmbedding.enumerated() { userArray[i] = NSNumber(value: val) }
            for (i, val) in item.features.enumerated() { itemArray[i] = NSNumber(value: val) }
            
            let input = ContentRecommenderInput(
                user_embedding: userArray,
                item_features: itemArray
            )
            
            let output = try await model.prediction(input: input)
            scores.append((item, output.relevance_score))
        }
        
        return scores
            .sorted { $0.score > $1.score }
            .map { $0.item }
    }
    
    // Update user embedding berdasarkan interaksi baru (on-device)
    func updateUserEmbedding(basedOn interaction: ContentItem, liked: Bool) {
        let learningRate: Float = liked ? 0.01 : -0.005
        
        // Gradient descent sederhana: geser embedding ke arah item yang disukai
        let itemFeatures = interaction.features.prefix(64).map { $0 }
        userEmbedding = zip(userEmbedding, itemFeatures).map { user, item in
            min(max(user + learningRate * item, -1), 1)
        }
    }
}