Section 5/151 menit

5. Natural Language Framework — Pemrosesan Teks

5. Natural Language Framework — Pemrosesan Teks

Language Detection

swift
import NaturalLanguage

// Detect bahasa dari teks

func detectLanguage(of text: String) -> String? {
    let recognizer = NLLanguageRecognizer()
    recognizer.processString(text)

    // Bahasa paling probable
    guard let language = recognizer.dominantLanguage else { return nil }
    return language.rawValue  // "en", "id", "ja", dll

    // Atau dapatkan probabilitas semua bahasa
    // let hypotheses = recognizer.languageHypotheses(withMaximum: 3)
    // → ["en": 0.98, "id": 0.01, "ms": 0.01]
}

Tokenization dan Part-of-Speech Tagging

swift
// NaturalLanguage: tokenization dan POS tagging

class TextAnalyzer {

    struct Token {
        let text: String
        let range: Range<String.Index>
        let partOfSpeech: NLTag?
    }

    func tokenize(_ text: String, unit: NLTokenUnit = .word) -> [Token] {
        var tokens: [Token] = []
        let tokenizer = NLTokenizer(unit: unit)
        tokenizer.string = text

        tokenizer.enumerateTokens(in: text.startIndex..<text.endIndex) { range, _ in
            tokens.append(Token(text: String(text[range]), range: range, partOfSpeech: nil))
            return true
        }
        return tokens
    }

    func tagPartsOfSpeech(in text: String) -> [Token] {
        var tokens: [Token] = []
        let tagger = NLTagger(tagSchemes: [.lexicalClass])
        tagger.string = text

        tagger.enumerateTags(
            in: text.startIndex..<text.endIndex,
            unit: .word,
            scheme: .lexicalClass,
            options: [.omitPunctuation, .omitWhitespace]
        ) { tag, range in
            tokens.append(Token(
                text: String(text[range]),
                range: range,
                partOfSpeech: tag
            ))
            return true
        }

        return tokens
    }
}

// NLTag values:
// .noun, .verb, .adjective, .adverb, .pronoun,
// .determiner, .particle, .preposition, .number,
// .conjunction, .interjection, .classifier, .idiom,
// .otherWord, .sentenceTerminator, .openQuote, .closeQuote

Named Entity Recognition (NER)

swift
// NaturalLanguage: Named Entity Recognition

struct NamedEntity {
    let text: String
    let type: EntityType

    enum EntityType: String {
        case person = "PersonalName"
        case place = "PlaceName"
        case organization = "OrganizationName"
        case unknown
    }
}

func extractNamedEntities(from text: String) -> [NamedEntity] {
    var entities: [NamedEntity] = []
    let tagger = NLTagger(tagSchemes: [.nameType])
    tagger.string = text

    let options: NLTagger.Options = [.omitPunctuation, .omitWhitespace, .joinNames]

    tagger.enumerateTags(
        in: text.startIndex..<text.endIndex,
        unit: .word,
        scheme: .nameType,
        options: options
    ) { tag, range in
        guard let tag else { return true }

        let entityText = String(text[range])
        let entityType = NamedEntity.EntityType(rawValue: tag.rawValue) ?? .unknown

        if entityType != .unknown {
            entities.append(NamedEntity(text: entityText, type: entityType))
        }

        return true
    }

    return entities
}

// Contoh:
// Input: "Tim Cook mengunjungi Apple Store di Jakarta"
// Output: [
//   NamedEntity(text: "Tim Cook", type: .person),
//   NamedEntity(text: "Apple Store", type: .organization),
//   NamedEntity(text: "Jakarta", type: .place)
// ]

Sentiment Analysis

swift
// NaturalLanguage: sentiment analysis (iOS 13+)

func analyzeSentiment(of text: String) -> (score: Double, label: String) {
    let tagger = NLTagger(tagSchemes: [.sentimentScore])
    tagger.string = text

    let (tag, _) = tagger.tag(
        at: text.startIndex,
        unit: .paragraph,
        scheme: .sentimentScore
    )

    let score = Double(tag?.rawValue ?? "0") ?? 0.0

    let label: String
    switch score {
    case 0.1...: label = "Positif"
    case ..<(-0.1): label = "Negatif"
    default: label = "Netral"
    }

    return (score, label)
    // score: -1.0 (sangat negatif) hingga +1.0 (sangat positif)
}

// Sentence-level sentiment
func analyzeSentimentPerSentence(in text: String) -> [(sentence: String, score: Double)] {
    var results: [(String, Double)] = []
    let tagger = NLTagger(tagSchemes: [.sentimentScore])
    tagger.string = text

    tagger.enumerateTags(
        in: text.startIndex..<text.endIndex,
        unit: .sentence,
        scheme: .sentimentScore,
        options: []
    ) { tag, range in
        let sentence = String(text[range])
        let score = Double(tag?.rawValue ?? "0") ?? 0.0
        results.append((sentence, score))
        return true
    }

    return results
}

Custom Text Classifier dengan NLModel

swift
// NaturalLanguage: gunakan model custom yang di-train dengan Create ML

class CustomTextClassifier {

    private let model: NLModel

    init() throws {
        // Load model yang di-train dengan Create ML Text Classifier
        let url = Bundle.main.url(
            forResource: "SupportTicketClassifier",
            withExtension: "mlmodelc"
        )!
        self.model = try NLModel(contentsOf: url)
    }

    func classify(_ text: String) -> (label: String, confidence: Double)? {
        let label = model.predictedLabel(for: text)
        let hypotheses = model.predictedLabelHypotheses(for: text, maximumCount: 3)

        guard let label,
              let confidence = hypotheses[label] else { return nil }
        return (label, confidence)
    }

    // Batch classification
    func classifyBatch(_ texts: [String]) -> [(text: String, label: String, confidence: Double)] {
        texts.compactMap { text in
            guard let result = classify(text) else { return nil }
            return (text, result.label, result.confidence)
        }
    }
}

Word Embedding dan Semantic Similarity

swift
// NaturalLanguage: word embedding untuk semantic similarity

class SemanticAnalyzer {

    private let embedding: NLEmbedding?

    init(language: NLLanguage = .english) {
        self.embedding = NLEmbedding.wordEmbedding(for: language)
    }

    func similarity(between word1: String, and word2: String) -> Double {
        guard let embedding else { return 0 }
        return embedding.distance(between: word1, and: word2, distanceType: .cosine)
        // 0.0 = identik, 2.0 = berlawanan
        // Konversi ke similarity: similarity = 1.0 - distance
    }

    func findNeighbors(of word: String, count: Int = 5) -> [(word: String, distance: Float)] {
        guard let embedding else { return [] }
        return embedding.neighbors(for: word, maximumCount: count)
    }

    // Sentence embedding (lebih powerful dari word embedding)
    func sentenceVector(for text: String) -> [Double]? {
        guard let sentenceEmbedding = NLEmbedding.sentenceEmbedding(for: .english) else {
            return nil
        }
        return sentenceEmbedding.vector(for: text)
    }
}