Penskalaan pemrosesan teks

Pemrosesan batch, streaming, dan caching untuk korpus besar pada tingkat pengenalan.

Daftar isi

Pemrosesan satu kalimat dengan nlp() berjalan cepat pada contoh kecil, tetapi korpus nyata berisi puluhan ribu teks yang tidak boleh diproses satu per satu secara boros. Topik ini memperkenalkan tiga teknik penskalaan pada tingkat pengenalan, yaitu pemrosesan batch melalui nlp.pipe, pemrosesan streaming dengan generator, dan caching hasil pemrosesan. Setelah membacanya, pemilihan teknik yang sesuai untuk korpus besar dapat dilakukan dengan memahami parameter utama dan konsekuensi memorinya.

Pemrosesan batch dengan nlp.pipe#

Metode nlp.pipe menerima banyak teks sekaligus dan mengembalikan iterator Doc dalam urutan yang sama dengan masukannya. Keunggulannya terletak pada pengelompokan internal, karena beberapa teks diproses bersama dalam satu kelompok sehingga biaya tetap per komponen tidak dibayar satu per satu. Dua parameter mengendalikan perilakunya, yaitu batch_size yang menentukan jumlah teks per kelompok dan n_process yang menentukan jumlah proses paralel. Contoh berikut memproses 50 teks dengan n_process=1, yaitu satu proses berurutan tanpa paralelisme.

python Contoh: nlp.pipe pada 50 teks
import spacy

nlp = spacy.load("en_core_web_sm")
teks = [f"Document number {i} contains a short note." for i in range(1, 51)]

docs = list(nlp.pipe(teks, n_process=1, batch_size=8))
print("jumlah dokumen  :", len(docs))
print("urutan terjaga  :", all(d.text == t for d, t in zip(docs, teks)))
print("dokumen pertama :", docs[0].text)
print("dokumen terakhir:", docs[-1].text)
text Keluaran
jumlah dokumen  : 50
urutan terjaga  : True
dokumen pertama : Document number 1 contains a short note.
dokumen terakhir: Document number 50 contains a short note.

Keluaran tersebut menegaskan dua sifat penting. Jumlah Doc sama dengan jumlah teks masukan, dan urutannya terjaga, sehingga pasangan teks dan hasilnya tetap dapat dirujuk dengan indeks. Parameter n_process di atas 1 menjalankan multiprocessing, dan setiap proses tambahan menyalin model ke memorinya sendiri, sehingga kenaikan n_process menambah kecepatan sekaligus menambah konsumsi memori.

Untuk membandingkan kecepatan, pengukuran berikut menjalankan perulangan nlp() satu per satu dan nlp.pipe pada teks yang sama.

python
# tidak-dijalankan: angka pengukuran waktu berubah pada setiap eksekusi, sehingga keluarannya tidak dapat diverifikasi ulang secara stabil
import time
import spacy

nlp = spacy.load("en_core_web_sm")
teks = [f"Document number {i} contains a short note." for i in range(1, 51)]

mulai = time.perf_counter()
per_loop = [nlp(t) for t in teks]
waktu_loop = time.perf_counter() - mulai

mulai = time.perf_counter()
per_batch = list(nlp.pipe(teks, batch_size=8))
waktu_pipe = time.perf_counter() - mulai

print(f"perulangan nlp() : {waktu_loop:.3f} s")
print(f"nlp.pipe()       : {waktu_pipe:.3f} s")

Pada pengukuran saat topik ini ditulis, perulangan nlp() memerlukan sekitar 0.17 sampai 0.19 detik untuk 50 teks, sedangkan nlp.pipe memerlukan sekitar 0.075 detik, atau sekitar dua sampai tiga kali lebih cepat. Angka tersebut bergantung pada mesin, versi pustaka, dan beban prosesor, sehingga hanya perbandingan relatifnya yang bermakna. Perbedaan ini tumbuh lebih besar pada korpus ribuan teks, karena penghematan per kelompok dibayar berulang kali.

Streaming dengan generator#

Pemrosesan streaming menyuplai teks satu per satu melalui generator, sehingga korpus tidak perlu dimuat seluruhnya ke memori. Metode nlp.pipe menerima iterable apa pun, termasuk generator yang membaca satu baris berkas pada satu waktu, dan hasilnya pun berupa iterator yang hanya bekerja saat Doc diminta. Pola ini menjadikan jumlah dokumen tidak lagi menentukan besarnya memori yang dipakai, melainkan besarnya satu dokumen terbesar.

Contoh berikut menyiapkan generator 1000 baris log, memasukkannya langsung ke nlp.pipe, lalu mengambil hanya tiga hasil pertama dengan islice.

python Contoh: streaming 1000 baris melalui generator
import spacy
from itertools import islice

nlp = spacy.load("en_core_web_sm")

def korpus():
    for i in range(1, 1001):
        yield f"Log entry {i} was archived successfully."

pipeline = nlp.pipe(korpus(), batch_size=16)
for doc in islice(pipeline, 3):
    print(doc.text, "->", len(doc), "token")
text Keluaran
Log entry 1 was archived successfully. -> 7 token
Log entry 2 was archived successfully. -> 7 token
Log entry 3 was archived successfully. -> 7 token

Meskipun generatornya menjanjikan 1000 baris, hanya tiga baris pertama yang benar-benar diproses, karena islice berhenti setelah tiga Doc dan baris sisanya tidak pernah dibaca. Sifat malas (lazy) ini berguna untuk pekerjaan yang berhenti di tengah jalan, misalnya pencarian dokumen pertama yang memuat kata kunci tertentu. Pada pemrosesan penuh, generator dari berkas besar ditulis dengan membuka berkas dan melakukan yield untuk setiap barisnya.

Caching hasil pemrosesan#

Caching menyimpan hasil pemrosesan agar teks yang sama tidak dihitung ulang pada pemanggilan berikutnya. Teknik paling ringan untuk fungsi praproses yang murni adalah dekorator functools.lru_cache, yaitu cache berbasis kamus pada memori yang menyimpan pasangan masukan dan keluaran. Fungsi yang cocok untuk cache adalah fungsi yang selalu mengembalikan keluaran sama untuk masukan sama, misalnya penyeragaman spasi dan huruf kecil.

Contoh berikut menjalankan fungsi bersihkan pada daftar yang sama sebanyak dua kali, lalu membaca statistik cache-nya.

python Contoh: lru_cache pada fungsi praproses
from functools import lru_cache
import re

@lru_cache(maxsize=512)
def bersihkan(teks):
    teks = re.sub(r"\s+", " ", teks).strip().lower()
    return teks

daftar = ["  Tiket   PIKET  Sore ", "RUANG BACA  tutup", " Ruang Baca Tutup "]
hasil_pertama = [bersihkan(t) for t in daftar]
hasil_kedua = [bersihkan(t) for t in daftar]

for t in hasil_pertama:
    print(repr(t))
print(bersihkan.cache_info())
text Keluaran
'tiket piket sore'
'ruang baca tutup'
'ruang baca tutup'
CacheInfo(hits=3, misses=3, maxsize=512, currsize=3)

Statistik cache membaca perilakunya dengan tepat. Pemanggilan pertama menghasilkan tiga miss, yaitu masukan yang belum ada di cache sehingga harus dihitung, sedangkan pemanggilan kedua menghasilkan tiga hit, yaitu masukan yang keluarannya diambil langsung dari cache. Perhatikan bahwa kunci cache adalah masukan, bukan keluaran, sehingga dua masukan berbeda yang dinormalisasi menjadi teks sama tetap disimpan sebagai dua entri. Untuk korpus yang bertambah bertahap, pola cache yang lazim adalah menyimpan hasil per teks ke berkas, lalu memproses ulang hanya teks yang belum memiliki entri.

Kesalahan umum#

  1. Mengonversi seluruh keluaran nlp.pipe menjadi daftar pada korpus besar. Objek Doc membawa token, dependensi, dan entitas sekaligus, sehingga daftar jutaan Doc memenuhi memori. Proses setiap Doc saat keluar, simpan hasil ringkasnya, lalu buang Doc tersebut.
  2. Menetapkan n_process besar tanpa menimbang ukuran model. Setiap proses menyalin model ke memorinya sendiri, dan salinan tersebut tidak dibagi antarproses. Mulai dari n_process=1, ukur kebutuhan memorinya, lalu naikkan bertahap.
  3. Memasang lru_cache pada fungsi yang bergantung pada keadaan luar. Fungsi yang membaca waktu, berkas yang berubah, atau variabel global akan mengembalikan hasil basi dari cache. Batasi cache untuk fungsi murni, dan tambahkan argumen seperti tanggal pemrosesan bila hasilnya memang sebaiknya diperbarui.

Ringkasan#

  • Metode nlp.pipe memproses banyak teks dengan urutan terjaga, dan parameter batch_size serta n_process mengendalikan ukuran kelompok dan jumlah proses paralelnya.
  • Generator menyuplai teks satu per satu, sehingga korpus besar dapat diproses tanpa dimuat seluruhnya ke memori, dan hasilnya pun hanya dihitung saat diminta.
  • functools.lru_cache menyimpan pasangan masukan dan keluaran untuk fungsi praproses yang murni, dengan kunci cache berupa masukan.
  • Pengukuran kecepatan bergantung pada mesin, sehingga perbandingan relatif antara perulangan nlp() dan nlp.pipe yang menjadi acuan keputusan.

Referensi#