Aplikasi chatbot, mesin penerjemah, dan analisis sentimen media sosial semuanya dimulai dari satu langkah dasar: mengubah teks mentah menjadi struktur data yang dapat dianalisis. Pipeline ini melibatkan tokenisasi, penghapusan stop word, stemming, dan penghitungan frekuensi — semuanya operasi DSA sederhana yang dipadukan menjadi sistem pemrosesan teks yang powerful. Pada Chapter ini kita akan membangun text processor mini yang mengubah paragraf menjadi token, melakukan stemming bahasa Indonesia, dan menghitung frekuensi kata dengan hash map. Setiap tahap memperlihatkan bagaimana pemilihan struktur data yang tepat menentukan kecepatan dan ketepatan analisis teks.

Pipeline Pemrosesan Teks #

Pipeline NLP klasik terdiri dari beberapa tahap berurutan. Setiap tahap menghasilkan representasi yang lebih terstruktur dari tahap sebelumnya.

Tabel berikut merangkum peran setiap tahap:

TahapOperasiStruktur DataOutput
TokenisasiPisah teks menjadi tokenvector<string>Daftar token
LowercaseNormalisasi kapitalTransformasi in-placeToken lowercase
Stop word removalBuang kata umumunordered_map<string,bool>Token terfilter
StemmingReduksi ke bentuk dasarstring manipulationToken ter-stem
Frequency countingHitung kemunculanunordered_map<string,int>Map kata→frekuensi

Tokenisasi: Aturan dan Mekanisme #

Aturan #

Tokenisasi memecah teks menjadi unit-unit diskrit yang disebut token. Untuk bahasa umum, token biasanya adalah kata yang dipisahkan spasi atau tanda baca. Tokenisasi sederhana berbasis aturan: karakter alfanumerik diterima, karakter lain berfungsi sebagai pemisah.

Mekanisme di Balik Layar #

Kita iterasi karakter teks satu per satu. Karakter alfanumerik di-accumulate ke cur (string sementara). Karakter non-alfanumerik memicu flush: bila cur tidak kosong, push ke vektor token dan reset. Akhir teks juga memicu flush untuk token terakhir. Lowercase diterapkan selama akumulasi untuk konsistensi kapital.

Variasi Tokenisasi #

PendekatanAturan SplitCocok untuk
WhitespacePisah pada spasiBahasa tanpa tanda baca (Cina klasik)
AlphanumericPisah pada non-alfanumerikTeks bahasa umum
Word boundary regexPisah pada \bTeks dengan apostrof (“don’t”)
Subword (BPE)Split berbasis vocabularyLLM modern (GPT, BERT)
Character-levelSetiap karakter = 1 tokenBahasa dengan morfologi kompleks

Stop Word Removal #

Stop word adalah kata umum yang muncul di hampir semua dokumen sehingga tidak informatif untuk analisis konten (mis. “yang”, “dan”, “adalah”). Menghapusnya mengurangi dimensi vektor tanpa kehilangan informasi penting.

Implementasi: simpan stop word dalam unordered_map<string,bool> untuk lookup O(1). Iterasi token, hanya simpan yang tidak ada di map. Hash map dipilih karena rata-rata lookup O(1) lebih cepat dari set berbasis tree O(log n) untuk stop word list berukuran ratusan.

Stemming Sederhana #

Stemming mereduksi kata ke bentuk dasar (stem) sehingga variasi morfologi dianggap sama. Mis. “berjalan”, “jalan”, “perjalanan” semuanya map ke stem “jalan”. Pada implementasi nyata, library seperti Sastrawi (PHP/Python) atau Stemmer ID (Java) memakai aturan morfologi lengkap.

Aturan Stemming Bahasa Indonesia #

Bahasa Indonesia memiliki struktur morfologi yang relatif teratur:

  • Prefix: me-, pe-, ber-, di-, ke-, se-, te-
  • Suffix: -kan, -an, -i, -lah, -nya
  • Konfiks: kombinasi prefix+suffix (mis. me-...-kan)

Stemmer sederhana kami menghapus suffix terlebih dahulu, lalu prefix. Urutan ini penting karena beberapa konfiks memerlukan prefix+suffix yang diproses bersama — tetapi untuk demo, pendekatan dua-tahap sudah cukup memperlihatkan konsep.

Kode: Text Processor #

cpp
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>
#include <cctype>
#include <iomanip>
using namespace std;

class TextProcessor {
public:
    // Tokenisasi: pisahkan teks menjadi token lowercase alfanumerik
    vector<string> tokenize(const string& text) {
        vector<string> tokens;
        string cur;
        for (char c : text) {
            if (isalnum((unsigned char)c)) {
                cur += (char)tolower((unsigned char)c);
            } else {
                if (!cur.empty()) { tokens.push_back(cur); cur.clear(); }
            }
        }
        if (!cur.empty()) tokens.push_back(cur);
        return tokens;
    }

    // Stop word removal: buang kata umum yang tidak informatif
    vector<string> removeStopWords(const vector<string>& tokens,
                                   const unordered_map<string,bool>& stop) {
        vector<string> out;
        for (const auto& t : tokens) {
            if (!stop.count(t)) out.push_back(t);
        }
        return out;
    }

    // Stemming sederhana: hapus suffix dan prefix umum bahasa Indonesia
    string simpleStem(string w) {
        // Hapus suffix (urutan: paling panjang dulu)
        if (w.size() > 4 && w.substr(w.size()-4) == "kan") w = w.substr(0, w.size()-4);
        else if (w.size() > 3 && w.substr(w.size()-3) == "kan") w = w.substr(0, w.size()-3);
        else if (w.size() > 3 && w.substr(w.size()-2) == "an") w = w.substr(0, w.size()-2);
        else if (w.size() > 2 && w.back() == 'i') w = w.substr(0, w.size()-1);

        // Hapus prefix (urutan: paling spesifik dulu)
        if (w.size() > 4 && w.substr(0,4) == "memb") w = w.substr(4);
        else if (w.size() > 3 && w.substr(0,3) == "men") w = w.substr(3);
        else if (w.size() > 3 && w.substr(0,3) == "mem") w = w.substr(3);
        else if (w.size() > 3 && w.substr(0,3) == "ber") w = w.substr(3);
        else if (w.size() > 3 && w.substr(0,3) == "pem") w = w.substr(3);
        else if (w.size() > 3 && w.substr(0,3) == "pen") w = w.substr(3);
        else if (w.size() > 2 && w.substr(0,2) == "me") w = w.substr(2);
        else if (w.size() > 2 && w.substr(0,2) == "pe") w = w.substr(2);

        return w;
    }

    // Hitung frekuensi token
    unordered_map<string, int> countFrequency(const vector<string>& tokens) {
        unordered_map<string, int> freq;
        for (const auto& t : tokens) freq[t]++;
        return freq;
    }
};

int main() {
    string text = "Belajar algoritma pemrograman adalah hal yang menyenangkan. "
                  "Algoritma membantu kita menyelesaikan masalah pemrograman "
                  "dengan langkah yang terstruktur. Pemrograman membutuhkan "
                  "algoritma yang baik dan efisien.";

    TextProcessor tp;

    cout << "=== Pemrosesan Teks ===\n\n";
    cout << "Teks asli:\n\"" << text << "\"\n\n";

    auto tokens = tp.tokenize(text);
    cout << "1. Tokenisasi: " << tokens.size() << " token\n";
    cout << "   Token: ";
    for (size_t i = 0; i < tokens.size(); ++i) {
        cout << "[" << tokens[i] << "]";
        if (i+1 < tokens.size()) cout << " ";
    }
    cout << "\n\n";

    unordered_map<string,bool> stop = {
        {"yang",true}, {"adalah",true}, {"hal",true},
        {"dengan",true}, {"dan",true}, {"kita",true}
    };
    auto filtered = tp.removeStopWords(tokens, stop);
    cout << "2. Setelah stop word removal: " << filtered.size() << " token\n";
    cout << "   Token: ";
    for (size_t i = 0; i < filtered.size(); ++i) {
        cout << "[" << filtered[i] << "]";
        if (i+1 < filtered.size()) cout << " ";
    }
    cout << "\n\n";

    cout << "3. Setelah stemming sederhana:\n   ";
    vector<string> stemmed;
    for (const auto& t : filtered) stemmed.push_back(tp.simpleStem(t));
    for (size_t i = 0; i < stemmed.size(); ++i) {
        cout << "[" << filtered[i] << "->" << stemmed[i] << "]";
        if (i+1 < stemmed.size()) cout << " ";
    }
    cout << "\n\n";

    auto freq = tp.countFrequency(stemmed);
    vector<pair<string,int>> sorted_freq(freq.begin(), freq.end());
    sort(sorted_freq.begin(), sorted_freq.end(),
         [](const pair<string,int>& a, const pair<string,int>& b) {
             if (a.second != b.second) return a.second > b.second;
             return a.first < b.first;
         });

    cout << "4. Frekuensi kata (sorted desc):\n";
    cout << "   " << left << setw(20) << "Kata" << "Frekuensi" << "\n";
    cout << "   --------------------|----------\n";
    for (const auto& kv : sorted_freq) {
        cout << "   " << left << setw(20) << kv.first << kv.second << "\n";
    }
    return 0;
}

Output yang Dihasilkan:

text
=== Pemrosesan Teks ===

Teks asli:
"Belajar algoritma pemrograman adalah hal yang menyenangkan. Algoritma membantu kita menyelesaikan masalah pemrograman dengan langkah yang terstruktur. Pemrograman membutuhkan algoritma yang baik dan efisien."

1. Tokenisasi: 24 token
   Token: [belajar] [algoritma] [pemrograman] [adalah] [hal] [yang] [menyenangkan] [algoritma] [membantu] [kita] [menyelesaikan] [masalah] [pemrograman] [dengan] [langkah] [yang] [terstruktur] [pemrograman] [membutuhkan] [algoritma] [yang] [baik] [dan] [efisien]

2. Setelah stop word removal: 16 token
   Token: [belajar] [algoritma] [pemrograman] [menyenangkan] [algoritma] [membantu] [menyelesaikan] [masalah] [pemrograman] [langkah] [terstruktur] [pemrograman] [membutuhkan] [algoritma] [baik] [efisien]

3. Setelah stemming sederhana:
   [belajar->belajar] [algoritma->algoritma] [pemrograman->rogram] [menyenangkan->yenang] [algoritma->algoritma] [membantu->antu] [menyelesaikan->yelesai] [masalah->masalah] [pemrograman->rogram] [langkah->langkah] [terstruktur->terstruktur] [pemrograman->rogram] [membutuhkan->utuh] [algoritma->algoritma] [baik->baik] [efisien->efisien]

4. Frekuensi kata (sorted desc):
   Kata                Frekuensi
   --------------------|----------
   algoritma           3
   rogram              3
   antu                1
   baik                1
   belajar             1
   efisien             1
   langkah             1
   masalah             1
   terstruktur         1
   utuh                1
   yelesai             1
   yenang              1

Bedah Kode Baris-per-Baris #

  1. if (isalnum((unsigned char)c)) cur += (char)tolower(...): akumulasi karakter alfanumerik sebagai token. Cast (unsigned char) menghindari undefined behavior pada isalnum untuk nilai negatif (karakter non-ASCII).
  2. if (!cur.empty()) { tokens.push_back(cur); cur.clear(); }: flush token saat bertemu separator. Cek empty() mencegah token kosong dari separator berurutan.
  3. if (!stop.count(t)) out.push_back(t);: lewati stop word. Lookup unordered_map::count O(1) rata-rata — lebih cepat dari find + banding iterator untuk kasus boolean.
  4. if (w.size() > 4 && w.substr(w.size()-4) == "kan") w = w.substr(0, w.size()-4);: hapus suffix -kan bila cukup karakter tersisa. Cek size() > 4 mencegah kata pendek menjadi kosong setelah penghapusan.
  5. if (w.size() > 4 && w.substr(0,4) == "memb") w = w.substr(4);: hapus prefix memb- (mis. “membantu” → “antu”). Urutan penting: cek prefix lebih spesifik dulu (memb, men, mem) sebelum yang lebih umum (me).
  6. unordered_map<string, int> freq; for (const auto& t : tokens) freq[t]++;: hitung frekuensi. Operator [] pada unordered_map menginisialisasi entri baru dengan 0, sehingga ++ otomatis bekerja untuk kata baru.
  7. sort(sorted_freq.begin(), sorted_freq.end(), [](const auto& a, const auto& b) { ... }): sort descending berdasarkan frekuensi, tie-break alphabetical. Lambda membandingkan second dulu, lalu first jika tie.

Kompleksitas #

OperasiTimeSpace
tokenizeO(n), n = panjang teksO(n) untuk token
removeStopWordsO(m), m = jumlah tokenO(m) output
simpleStemO(L), L = panjang kataO(L) substring sementara
countFrequencyO(m) rata-rataO(unique words)
Sort frequencyO(u log u), u = unique wordsO(u)

Total pipeline: O(n + u log u) — linear terhadap panjang teks, ditambah sort pada unique words.

Aplikasi Lanjutan #

Setelah pipeline dasar selesai, hasilnya dapat dimasukkan ke berbagai analisis lanjutan:

AnalisisInputOutputAlgoritma
Keyword extractionFreq map per dokumenTop-k keywordTF-IDF (Chapter 28.1)
Sentiment analysisToken + lexiconSkor positif/negatifWeighted sum
Text classificationToken vectorKelas dokumenNaive Bayes / SVM
Topic modelingDocument-term matrixTopik latenLSA / NMF
Spell checkingToken + dictionaryKoreksi typoEdit distance
Search indexInverted indexHasil pencarianTF-IDF ranking

Ringkasan #

  • Pipeline NLP: tokenisasi → stop word removal → stemming → frequency counting.
  • Tokenisasi berbasis aturan alphanumeric memberikan hasil baik untuk teks bahasa umum.
  • unordered_map memberi lookup O(1) rata-rata untuk stop word dan frequency counting.
  • Stemming sederhana berbasis aturan prefix/suffix cukup untuk demo, tetapi produksi memerlukan library morfologi lengkap.
  • Sort dengan lambda komparator memungkinkan urutan multi-kriteria (frekuensi, lalu alphabetical).
  • Output pipeline siap dipakai untuk TF-IDF, klasifikasi, atau indexing.