Section 6/201 menit

6. Natural Language Framework: NLP

6. Natural Language Framework: NLP

Language Detection dan Tokenization

swift
// Natural Language: dasar NLP pipeline

import NaturalLanguage

final class TextAnalyzer {

    // MARK: — Language Detection

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

    func detectAllLanguages(of text: String) -> [(NLLanguage, Double)] {
        let recognizer = NLLanguageRecognizer()
        recognizer.processString(text)

        return recognizer
            .languageHypotheses(withMaximum: 5)
            .sorted { $0.value > $1.value }
            .map { ($0.key, $0.value) }
    }

    // MARK: — Tokenization

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

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

        return tokens
    }

    // MARK: — Named Entity Recognition (NER)

    func extractEntities(from text: String) -> [NamedEntity] {
        var entities: [NamedEntity] = []

        let tagger = NLTagger(tagSchemes: [.nameType])
        tagger.string = text
        tagger.setLanguage(.english, range: text.startIndex..<text.endIndex)

        tagger.enumerateTags(in: text.startIndex..<text.endIndex, unit: .word, scheme: .nameType) { tag, range in
            guard let tag,
                  [.personalName, .organizationName, .placeName].contains(tag) else {
                return true
            }

            entities.append(NamedEntity(
                text: String(text[range]),
                type: tag,
                range: range
            ))
            return true
        }

        return entities
    }

    // MARK: — Part of Speech Tagging

    func tagPartsOfSpeech(in text: String) -> [(String, NLTag)] {
        var result: [(String, NLTag)] = []

        let tagger = NLTagger(tagSchemes: [.lexicalClass])
        tagger.string = text

        tagger.enumerateTags(in: text.startIndex..<text.endIndex, unit: .word, scheme: .lexicalClass) { tag, range in
            if let tag {
                result.append((String(text[range]), tag))
            }
            return true
        }

        return result
    }

    // MARK: — Sentiment Analysis

    func analyzeSentiment(of text: String) -> Sentiment {
        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

        switch score {
        case 0.2...: return .positive(score)
        case ..<(-0.2): return .negative(score)
        default: return .neutral(score)
        }
    }
}

struct NamedEntity {
    let text: String
    let type: NLTag
    let range: Range<String.Index>
}

enum Sentiment {
    case positive(Double)
    case negative(Double)
    case neutral(Double)

    var score: Double {
        switch self {
        case .positive(let s), .negative(let s), .neutral(let s): return s
        }
    }
}
swift
// Natural Language: semantic similarity menggunakan word embeddings

import NaturalLanguage

final class SemanticSearchEngine {
    private let embedding: NLEmbedding

    init?() {
        guard let embedding = NLEmbedding.sentenceEmbedding(for: .english) else {
            return nil
        }
        self.embedding = embedding
    }

    // Hitung similarity antara dua kalimat (0.0 = tidak mirip, 1.0 = identik)
    func similarity(between text1: String, and text2: String) -> Double {
        guard let vector1 = embedding.vector(for: text1),
              let vector2 = embedding.vector(for: text2) else {
            return 0
        }

        return cosineSimilarity(vector1, vector2)
    }

    // Cari dokumen yang paling mirip dengan query
    func search(query: String, in documents: [String], topK: Int = 5) -> [(document: String, score: Double)] {
        guard let queryVector = embedding.vector(for: query) else { return [] }

        let scored = documents.compactMap { document -> (String, Double)? in
            guard let docVector = embedding.vector(for: document) else { return nil }
            let score = cosineSimilarity(queryVector, docVector)
            return (document, score)
        }

        return scored
            .sorted { $0.1 > $1.1 }
            .prefix(topK)
            .map { (document: $0.0, score: $0.1) }
    }

    // Cosine similarity: dot product / (magnitude1 * magnitude2)
    private func cosineSimilarity(_ v1: [Double], _ v2: [Double]) -> Double {
        guard v1.count == v2.count, !v1.isEmpty else { return 0 }

        let dotProduct = zip(v1, v2).reduce(0) { $0 + $1.0 * $1.1 }
        let magnitude1 = sqrt(v1.reduce(0) { $0 + $1 * $1 })
        let magnitude2 = sqrt(v2.reduce(0) { $0 + $1 * $1 })

        guard magnitude1 > 0 && magnitude2 > 0 else { return 0 }
        return dotProduct / (magnitude1 * magnitude2)
    }

    // Cluster teks berdasarkan similarity
    func findNeighbors(of text: String, in corpus: [String], threshold: Double = 0.7) -> [String] {
        guard let queryVector = embedding.vector(for: text) else { return [] }

        return corpus.filter { document in
            guard let docVector = embedding.vector(for: document) else { return false }
            return cosineSimilarity(queryVector, docVector) >= threshold
        }
    }
}