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:
| Tahap | Operasi | Struktur Data | Output |
|---|---|---|---|
| Tokenisasi | Pisah teks menjadi token | vector<string> | Daftar token |
| Lowercase | Normalisasi kapital | Transformasi in-place | Token lowercase |
| Stop word removal | Buang kata umum | unordered_map<string,bool> | Token terfilter |
| Stemming | Reduksi ke bentuk dasar | string manipulation | Token ter-stem |
| Frequency counting | Hitung kemunculan | unordered_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 #
| Pendekatan | Aturan Split | Cocok untuk |
|---|---|---|
| Whitespace | Pisah pada spasi | Bahasa tanpa tanda baca (Cina klasik) |
| Alphanumeric | Pisah pada non-alfanumerik | Teks bahasa umum |
| Word boundary regex | Pisah pada \b | Teks dengan apostrof (“don’t”) |
| Subword (BPE) | Split berbasis vocabulary | LLM modern (GPT, BERT) |
| Character-level | Setiap karakter = 1 token | Bahasa 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 #
#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:
=== 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 1Bedah Kode Baris-per-Baris #
if (isalnum((unsigned char)c)) cur += (char)tolower(...): akumulasi karakter alfanumerik sebagai token. Cast(unsigned char)menghindari undefined behavior padaisalnumuntuk nilai negatif (karakter non-ASCII).if (!cur.empty()) { tokens.push_back(cur); cur.clear(); }: flush token saat bertemu separator. Cekempty()mencegah token kosong dari separator berurutan.if (!stop.count(t)) out.push_back(t);: lewati stop word. Lookupunordered_map::countO(1) rata-rata — lebih cepat darifind+ banding iterator untuk kasus boolean.if (w.size() > 4 && w.substr(w.size()-4) == "kan") w = w.substr(0, w.size()-4);: hapus suffix-kanbila cukup karakter tersisa. Ceksize() > 4mencegah kata pendek menjadi kosong setelah penghapusan.if (w.size() > 4 && w.substr(0,4) == "memb") w = w.substr(4);: hapus prefixmemb-(mis. “membantu” → “antu”). Urutan penting: cek prefix lebih spesifik dulu (memb,men,mem) sebelum yang lebih umum (me).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.sort(sorted_freq.begin(), sorted_freq.end(), [](const auto& a, const auto& b) { ... }): sort descending berdasarkan frekuensi, tie-break alphabetical. Lambda membandingkanseconddulu, lalufirstjika tie.
Kompleksitas #
| Operasi | Time | Space |
|---|---|---|
tokenize | O(n), n = panjang teks | O(n) untuk token |
removeStopWords | O(m), m = jumlah token | O(m) output |
simpleStem | O(L), L = panjang kata | O(L) substring sementara |
countFrequency | O(m) rata-rata | O(unique words) |
| Sort frequency | O(u log u), u = unique words | O(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:
| Analisis | Input | Output | Algoritma |
|---|---|---|---|
| Keyword extraction | Freq map per dokumen | Top-k keyword | TF-IDF (Chapter 28.1) |
| Sentiment analysis | Token + lexicon | Skor positif/negatif | Weighted sum |
| Text classification | Token vector | Kelas dokumen | Naive Bayes / SVM |
| Topic modeling | Document-term matrix | Topik laten | LSA / NMF |
| Spell checking | Token + dictionary | Koreksi typo | Edit distance |
| Search index | Inverted index | Hasil pencarian | TF-IDF ranking |
Ringkasan #
- Pipeline NLP: tokenisasi → stop word removal → stemming → frequency counting.
- Tokenisasi berbasis aturan alphanumeric memberikan hasil baik untuk teks bahasa umum.
unordered_mapmemberi 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.