Pengantar Pemrosesan Teks dengan Keras (Bagian 2: Representasi Teks, Klasifikasi dengan Feedforward NN )

Ini adalah lanjutan dari bagian 1.  Sebaiknya baca bagian1 tersebut jika belum  mengenal tentang konsep tensor, dimensi, shape pada Keras.

Representasi Teks

Teks perlu dikonversi menjadi angka sebelum menjadi input neural network. Keras menyediakan class Tokenizer. Tokenizer ini berfungsi untuk mengkonversi teks menjadi urutan integer indeks kata atau vektor binary, word count atau tf-idf.

Contoh penggunaannya adalah sebagai berikut:

from keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer()
texts = ["Budi makan nasi","Rudi makan nasi, nasi goreng."]
tokenizer.fit_on_texts(texts)

seq = tokenizer.texts_to_sequences(texts)
#kalimat baru
seq1 = tokenizer.texts_to_sequences(["nasi panas sekali"])
print("Index: "+str(tokenizer.word_index))
print("Seq. corpus:"+str(seq))
print("Seq. untuk 'nasi panas sekali':"+str(seq1))

Hasilnya akan seperti ini:

Indeks: {'rudi': 4, 'budi': 3, 'nasi': 1, 'makan': 2, 'goreng': 5}
Seq. corpus':[[3, 2, 1], [4, 2, 1, 1, 5]]
Seq. untuk 'nasi panas sekali':[[1]]
Catatan: "panas" dan "sekali" tidak ada di kosakata jadi tidak ada indeksnya

Dapat dilihat kosakata corpus diubah menjadi indeks (indeks pertama ‚Äúnasi‚ÄĚ, kedua ‚Äúmakan‚ÄĚ dan seterusnya). Kalimat kemudian diubah menjadi list urutan dari indeks. List sequence ini kemudian dapat dikonversi menjadi  vektor matriks numpy dengan sequences_to_matrix. Terdapat empat pilihan: tf-idf, binary, count, freq.

Lanjutkan kode sebelumnya untuk mengubah representasi teks berupa urutan indeks menjadi matriks tf-idf sampai frekuensi:

encoded_tfidf = tokenizer.sequences_to_matrix(seq,mode="tfidf")
print("tfidf:")
print(encoded_tfidf)
encoded_binary = tokenizer.sequences_to_matrix(seq,mode="binary")
print("binary:")
print(encoded_binary)
encoded_count = tokenizer.sequences_to_matrix(seq,mode="count")
print("count:")
print(encoded_count)
encoded_freq = tokenizer.sequences_to_matrix(seq,mode="freq")
print("freq:")
print(encoded_freq)

Hasilnya:

tfidf:
[[0.         0.51082562 0.51082562 0.69314718 0.         0.        ]
[0.         0.86490296 0.51082562 0.         0.69314718 0.69314718]]
binary:
[[0. 1. 1. 1. 0. 0.]
[0. 1. 1. 0. 1. 1.]]
count:
[[0. 1. 1. 1. 0. 0.]
[0. 2. 1. 0. 1. 1.]]
freq:
[[0.         0.33333333 0.33333333 0.33333333 0.         0.        ]
[0.         0.4        0.2        0.         0.2        0.2

Hasil sudah berbentuk numpy array. Dapat dilihat padding dilakukan otomatis untuk menyamakan dimensi dengan shape (2,6). Data ini dapat langsung digunakan dalam proses pembuatan model. Alternatif lain adalah menggunakan embedded layer yang akan dibahas dalam posting berikutnya.

Jika proses padding ingin dilakukan secara manual, Keras menyediakan pad_sequences. Contoh penggunaan pad_sequences :

from keras.preprocessing.sequence import pad_sequences
print("Sebelum padding:")
print(seq)
X = pad_sequences(seq)
print("Sesudah padding:")
print(X)
print(X.shape)

Hasilnya:


Sebelum padding:
[[3, 2, 1], [4, 2, 1, 1, 5]]
Sesudah padding:
[[0 0 3 2 1]
[4 2 1 1 5]]
Shape: (2, 5)

Klasifikasi Teks

Dalam bagian ini, akan dilakukan klasifikasi teks menggunakan data SMS spam berbahasa Indonesia dengan arsitektur yang paling sederhana yaitu feed forward NN.

Data dapat didownload di:http://bit.ly/yw_sms_spam_indonesia

Jumlah sample 1143 dan ada tiga kelas dalam dataset ini:

0: sms normal (569 instance)
1: fraud atau penipuan (335 instance)
2: promo (239 instance)

Langkah pertama adalah meload data dari file csv, dapat digunakan library csv. Tambahkan cell berikut.

import csv
nama_file = "C:\\yudiwbs\\data\\sms\\dataset_sms_spam_v1.csv"
data  = []
label = []
with open(nama_file, 'r', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile, delimiter=',', quotechar='"')
next(reader) #skip header
for row in reader:
data.append(row[0])
label.append(row[1])
#test lihat dua data pertama
print(data[:2])
print(label[:2])
#Catatan: parameter encoding dapat dibuang jika muncul error

Alternatif lain adalah menggunakan pandas untuk membaca csv:

import pandas as pd
df = pd.read_csv(nama_file).values
data = df[:, 0]
label = df[:, 1]

Selanjutnya konversi label dari “1”, “2”, “3” menjadi representasi tensor:

from keras.utils import to_categorical
label = to_categorical(label)
print(label.shape)
print(label)

Hasilnya adalah Tensor 2D dengan shape (1143, 3) untuk label, karena ada 1143 instance dengan 3 nilai label yang mungkin (normal, fraud, promo)

Split data menjadi train dan test menggunakan scikit learn, 80% menjadi data train, 20% menjadi data test.

#split jadi train-test
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(data, label,
test_size=0.2,
random_state=123)

Konversi data teks menjadi tf-idf dan tensor. Pastikan Fit hanya dilakukan pada data train untuk mencegah informasi di data test “bocor”.

#konversi teks ke tfidf
from keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer()
#fit hanya berdasarkan data train
tokenizer.fit_on_texts(X_train)
#konversi train
seq_x_train = tokenizer.texts_to_sequences(X_train)
X_enc_train = tokenizer.sequences_to_matrix(seq_x_train,mode="tfidf")
#konversi test
seq_x_test  = tokenizer.texts_to_sequences(X_test)
X_enc_test  = tokenizer.sequences_to_matrix(seq_x_test,mode="tfidf")

print(X_enc_train.shape)
print(X_enc_test.shape)
print(X_enc_train)

Hasilnya adalah tensor 2D dengan shape (914, 4384) untuk data train dan tensor 2D (229, 4384) untuk data test.

Selanjutnya siapkan model dengan menambahkan layer

from keras import models
from keras import layers

_,jum_fitur = X_enc_train.shape
model = models.Sequential()
model.add(layers.Dense(32,activation='relu',input_shape=(jum_fitur,)))
model.add(layers.Dense(4,activation='relu'))
model.add(layers.Dense(3,activation='softmax'))  #karena kelasnya ada 3
model.compile(optimizer="adam",
loss='categorical_crossentropy',
metrics=['accuracy'])

Ada empat layer: layer pertama adalah layer input hasil encode tf-idf sebelumnya: 4384 fitur. Mengapa input_shape tidak menggunakan sample dimension atau sample axis seperti (914, 4384)? karena jumlah samples tidak penting didefinisikan dalam layer input. Dengan teknik mini-batch, sample dapat diproses sedikit demi sedikit, jadi jumlahnya bisa berbeda-beda.

Activation softmax digunakan karena jumlah label ada 3 (normal, fraud dan promo). Jika jumlah label dua (binary classification) maka dapat digunakan activation sigmoid. Setelah layer didefinisikan, maka layer dapat dicompile. Loss categorical_crossentropy dipilih karena terdapat tiga kelas, sedangkan jika untuk binary class dapat digunakan binary_crossentropy.

Kode untuk melakukan training adalah sebagai berikut:

history = model.fit(X_enc_train,y_train,
epochs=3, batch_size=2,
validation_split=0.2)

results = model.evaluate(X_enc_test, y_test)
print("Hasil  [loss,acc] untuk data test:")
print(results)

Satu epoch adalah satu iterasi yang diperlukan untuk memproses seluruh training data. Jika jumlah data training 1000, dan batch_size 20, maka untuk memproses setiap epoch akan diperlukan 1000/20 = 50 steps update bobot. Pada setiap step bobot network akan di-update.

Semakin kecil batch size, semakin kecil memori yang diperlukan dan proses akan konvergen lebih cepat. Kelemahannya, akan memerlukan semakin banyak steps dalam setiap epoch (waktu training semakin lama). Parameter validation_split menentukan persentase data yang akan digunakan untuk data validasi.

Data validasi diambil dari data train dan digunakan untuk meminimalkan nilai loss pada saat training, val_acc dan val_loss adalah metrik untuk data validasi ini. Setelah proses selesai baru kinerja diukur pada data test.

Setelah training selesai, hasilnya adalah sebagai berikut, untuk data test didapat loss 0.33 dan akurasi 0.926 (komputer yang berbeda dapat menghasilkan hasil berbeda):

Proses training dapat memerlukan waktu lama, untuk menyimpan model dan hasil tokenizer ke dalam file gunakan kode berikut:

import pickle
model.save('model_spam_v1.h5')
with open('tokenizer.pickle', 'wb') as handle:
pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)

Berikut adalah kode untuk me-load model, tokenizer dan memprediksi label untuk data baru:

from keras.models import load_model
import pickle
model = load_model('model_spam_v1.h5')
with open('tokenizer.pickle', 'rb') as handle:
tokenizer = pickle.load(handle)

s  = ["Anda mendapat hadiah 100 juta","Beli paket Flash mulai 1GB", "Nanti ketemuan dimana?"]
seq_str = tokenizer.texts_to_sequences(s)
enc_str = tokenizer.sequences_to_matrix(seq_str,mode="tfidf")
enc_str.shape
pred = model.predict_classes(enc_str)
print("Prediksi kelas string ' {} ' adalah {}".format(s,pred))

Hasilnya:
Prediksi kelas string ‘ [‘Anda mendapat hadiah 100 juta, ‘Beli paket Flash mulai 1GB’, ‘Nanti ketemuan dimana?’] ‘ adalah [1 2 0]

Bersambung..  (word embedding, RNN)

Update Juni 2020: artikel ini tidak akan dibuat kelanjutannya, tapi materi lengkap tentang ini bisa dilihat di: https://docs.google.com/document/d/1SQkzjjBdjCNO7cexAAy9s0tGvTsKf1WUJiPrkKPLI-4/edit?usp=sharing

Pos Tagger dan Dependency Parser dengan StanfordNLP Python

Sebelumnya saya sudah buat tulisan tentang pos tagger & dependency parser Bahasa Indonesia dengan lib CRFTagger, UUParser dan Syntaxnet.  Hanya Syntaxnet yang menyediakan pretrained model. Saat ini ada satu lagi library yang menyediakan pretrained model untuk Bahasa Indonesia: StanfordNLP (https://github.com/stanfordnlp/stanfordnlp).

StanfordNLP sudah ada cukup lama, tapi awalnya menggunakan Java dan lisensinya GPL (kita perlu bayar lisensi terpisah jika buat sistem yang tidak open source).  Sejalan dengan dominasi Python dan lisensi yang lebih longgar seperti MIT dan Apache, maka dikembangkan lib baru, dan StanfordNLP yang lama diubah namanya menjadi CoreNLP.

StanfordNLP  ini sudah native python (bisa diinstall dengan pip install stanfordnlp),  menggunakan deep learning (pytorch) dan sudah menyediakan pretrained model untuk bahasa Indonesia.

Saat saya coba, ternyata lib ini memerlukan Python 3.6 dan 3.7.   Saya install saja versi yang terakhir, yaitu versi  3.7.2, sayangnya untuk versi ini Python harus dicompile manual. Langkah-langkahnya saya tiru dari: https://tecadmin.net/install-python-3-7-on-ubuntu-linuxmint/  tapi dengan modifikasi sedikit karena di artikel itu kurang dua library yaitu libffi dan liblzma (saya menggunakan Ubuntu 16).

sudo apt-get install build-essential checkinstall
sudo apt-get install libreadline-gplv2-dev libncursesw5-dev libssl-dev \
    libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev liblzma-dev
cd /usr/src
sudo wget https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tgz
sudo tar xzf Python-3.7.2.tgz
cd Python-3.7.2
sudo ./configure --enable-optimizations
sudo make altinstall

Catatan: interpreter hasil compile  disimpan di /usr/local/bin/python3.7

Update Juni 19: Untuk Windows dengan Anaconda, masalahnya ada di instalasi torch (saya menggunakan PyCharm). Buka conda prompt, aktifkan virtual env target, pergi  ke website  Pytorch untuk dapat instruksi instalasi.

Setelah pip install stanfordnlp, kode berikut adalah contoh penggunaannya. HATI-HATI, perlu satu jam untuk menjalankan kode ini, entah karena harus menggunakan GPU atau ada optimasi yang belum saya lakukan. Update: running berikutnya hanya perlu 1-2 menit, tetap lebih lama dibandingkan lib lain.

import stanfordnlp
stanfordnlp.download('id')   # download 1.4GB model
nlp = stanfordnlp.Pipeline(lang="id",use_gpu=False)
doc = nlp("Budi makan nasi enak sekali.")
print("token:")
doc.sentences[0].print_tokens()
print("dependency parse:")
doc.sentences[0].print_dependencies()

Hasilnya adalah sebagai berikut:


token:
Token index=1;words=[Word index=1;text=budi;lemma=budi;upos=NOUN;xpos=NSD;feats=Number=Sing;governor=2;dependency_relation=nsubj]
Token index=2;words=[Word index=2;text=makan;lemma=makan;upos=VERB;xpos=VSA;feats=Number=Sing|Voice=Act;governor=0;dependency_relation=root]
Token index=3;words=[Word index=3;text=nasi;lemma=nasi;upos=NOUN;xpos=NSD;feats=Number=Sing;governor=2;dependency_relation=obj]
Token index=4;words=[Word index=4;text=enak;lemma=enak;upos=ADJ;xpos=ASP;feats=Degree=Pos|Number=Sing;governor=3;dependency_relation=amod]
Token index=5;words=[Word index=5;text=sekali;lemma=sekali;upos=ADV;xpos=D--;feats=_;governor=2;dependency_relation=punct]

dependency parse:
('Budi', '2', 'nsubj')
('makan', '0', 'root')
('nasi', '2', 'obj')
('enak', '3', 'amod')
('sekali', '4','advmod')
('.', '2', 'punct') 

Dataset Klasifikasi Bahasa Indonesia (SMS Spam) & Klasifikasi Teks dengan Scikit-Learn

Setelah saya cari-cari, sepertinya  belum ada dataset klasifikasi Bahasa Indonesia yang bisa didownload dengan gampang dan berlisensi bebas (mirip seperti 20NewsGroup untuk Bahasa Inggris). Aneh juga kan kalau untuk kuliah atau pelatihan NLP Bahasa Indonesia malah menggunakan dataset Bahasa Inggris. Oleh karena itu berdasarkan dataset yang dibuat mahasiswa saya (dan dengan ijin dia), saya publish dataset untuk domain SMS spam dengan lisensi creative commons. Ada tiga kelas: SMS  normal, SMS penipuan, SMS promosi. Dua yang terakhir ini dapat dianggap spam. Jumlah instances ada 1143. Download di: http://bit.ly/yw_sms_spam_indonesia 

Sekalian saya buat tutorial singkat untuk membuat classifier berdasarkan dataset tersebut dengan lib scikit-learn. Caranya: Install lib scikit-learn, download dataset, sesuaikan namaFile dengan lokasi data. (Catatan: ada spasi di nama file, nanti saya perbaiki). Akurasinya 0.90 dengan MultinomialNB seperti code di bawah dan 0.92 jika menggunakan linear SVM.


#%%
#load data
from collections import Counter
import csv
namaFile = "/home/yudiwbs/dataset_sms_spam _v1.zip"
data = []
label = []
with open(namaFile, 'r', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile, delimiter=',', quotechar='"')
next(reader) #skip header
for row in reader:
data.append(row[0])
label.append(row[1])

print("jumlah data:{}".format(len(data)))
print(Counter(label))

#%%
#random urutan dan split ke data training dan test
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split( data, label, test_size=0.2, random_state=123)

print("Data training:")
print(len(X_train))
print(Counter(y_train))

print("Data testing:")
print(len(X_test))
print(Counter(y_test))

#%%
#transform ke tfidf dan train dengan naive bayes
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.naive_bayes import MultinomialNB
text_clf = Pipeline([('vect', CountVectorizer()),
('tfidf', TfidfTransformer()),
('clf', MultinomialNB())])
text_clf.fit(X_train, y_train)
#%%
# coba prediksi data baru
sms_baru = ['Anda mendapatkan hadiah mobil','nanti ketemu dimana?']
pred = text_clf.predict(sms_baru)
print("Hasil prediksi {}".format(pred))
#%%
#hitung akurasi data test
import numpy as np
pred = text_clf.predict(X_test)
akurasi = np.mean(pred==y_test)
print("Akurasi: {}".format(akurasi))

Analis Sentimen Berbasis Aspek

Update:
Pada situs nlp.yuliadi.pro/sentimen [ catatan: situs sudah dimatikan] sudah ditambahkan aspect detection dan ekspresi opini (selain polaritas). Kinerja juga sudah membaik (F1 polaritas 0.52) walaupun masih dibawah harapan.   F1 ekpresi 0.61, F1 aspek 0.34.  Untuk aspek, di situs saya gabungkan antara makanan dan minuman.  Jumlah dataset yang diperlukan sepertinya harus jauh lebih banyak daripada NER.

Sebelumnya saya sudah menulis tentang analisis sentimen sekitar 7 tahun yang lalu (https://yudiwbs.wordpress.com/2011/12/26/analisis-twee-analisis-opini-sentimen/). Sekarang saya tertarik lagi dengan bidang ini karena sering melakukan review lewat Google Map dan ternyata  aspect based sentiment analysis (ABSA) masih menjadi task sampai  SemEval 2015.  Lagipula task ini bisa dilihat sebagai kasus sequence labeling yang sekarang saya sedang saya coba-coba.

Selain Google Map yang mulai serius menggarap review,  situs seperti Tokopedia, BukaLapak, Agoda, AiryRoom, Gojek  dsb juga memproses data review dalam jumlah besar.  Aspect Based Sentiment Analysis harusnya akan bermanfaat, karena satu review dapat diproses <1 detik dan jika diparalelkan, ratusan bahkan ribuan review dapat diproses dalam beberapa detik saja.  Perusahaan bisa mendapatkan insight dengan cepat.

Jika  task pada Semeval 2015 jadi patokan,  maka ada tiga subtask di ABSA. Pertama menemukan polaritas, kedua menentukan aspek dan ketiga menentukan ekspresi opini.  Polaritas terdiri atas netral, positif dan negatif. Aspek terdiri atas kombinasi entitas:atribut.   Untuk domain restoran ada enam entitas: Restaurant, Food, Drink, Ambience, Location dan Service sedangkan  atributnya: Price, Quality,  Style, General dan Misc. Kombinasi entitas:atribut yang mungkin misalnya: Food:Price, Food:Style (porsi, penyajian), Food:Quality dst.  Tentu ada kombinasi Entitas:Atribut yang tidak bisa digunakan seperti Location:Quality (Location dan Service hanya bisa dipasangkan dengan atribut General).   Terakhir ekspresi opini berisi kata atau frasa yang terkait entitas.

Sebagai contoh,  berikut anotasi  untuk kalimat: ” Tempatnya bagus banget terlebih ada view kota bandungnya. Cuma sayang banget kemaren pesen makanan di restauran cave nya lama banget datengnya ampe setengah jam baru dateng, rasanya lumayan, penyajiannya lumayan. “

Polaritas

  • Positif: Tempatnya bagus banget terlebih ada view kota bandungnya
  • Negatif: Cuma sayang banget kemaren pesen makanan di restauran cave nya lama banget datengnya ampe setengah jam baru dateng.
  • Netral: rasanya lumayan, penyajiannya lumayan

Aspek

  • Ambience:General : Tempatnya bagus banget terlebih ada view kota bandungnya
  • Service:General: Cuma sayang banget kemaren pesen makanan di restauran cave nya lama banget datengnya ampe setengah jam baru dateng.
  • Food: Quality:  rasanya lumayan,
  • Food: Style: penyajiannya lumayan

Ekspresi:

  • tempatnya” : Tempatnya bagus banget terlebih ada view kota bandungnya
  • pesen makanan“: Cuma sayang banget kemaren pesen makanan di restauran cave nya lama banget datengnya ampe setengah jam baru dateng.

Ada beberapa kasus yang lain yang mengandung kata positif, tetapi secara kesuluruhan sebenarnya kalimat negatif, sebagai contoh:

  • Biasanya nasinya masih panas dan empuk.
  • mestinya kualitas bisa lbh baik krn bnyak resoran serupa di bandung skr sdh menjamur.
  • saya lebih suka sup iga bakar  dari restoran lain di Bandung

Kasus-kasus lain yang sulit:

  • Sarkasme: “Dan saat disodorkan buku menu , saya kembali terpukau . Menu makanannya sedikit dan harganya sangat fantastis !”
  • Opini orang lain: “Teman yang tinggal di Bandung juga kebetulan hobi sekali bersantai di sini”
  • Positif walaupun awalnya negatif:  “Ketika awal-awal baru dibuka sih saya kurang suka dengan rasanya . Tidak sesuai dengan di lidah. Tapi sepertinya manajemennyaterus memperbaiki diri sehingga dalam jangka waktu 1 tahun saja , makanannya sudah berubah menjadi enak.”

Berdasarkan data tripadvisor, saya mencoba ketiga task tersebut. Saat ini baru sampai polaritas, bisa dicek di: [update: situs sudah dimatikan]   Datasetnya saya anotasi sendiri dan saat ini masih belum bisa di-share.

Dockerized Model Server

Posting saya sebelumnya tentang model server

Hal yang harus dilakukan berikutnya adalah deploy model server ini. Pengalaman saya sebelumnya,  deployment bisa jadi hal yang merepotkan karena harus install aplikasi, install library, setting parameter dan sebagainya.  Banyak app lama yang malas saya sentuh karena ini. Saya langsung tertarik setelah membaca Docker, karena akan sangat memudahkan bagi saya yang males ini hehe.

Rencananya, setiap task akan menjadi container yang terpisah. Jadi akan ada container untuk NER (named entity recognition), deteksi 5W1h (what, where, dsb),  paraphrase, similarity, aspect based sentiment analysis dsb. Lalu ada container untuk web service sebagai penghubung model server dengan dunia luar.  Saya menggunakan image dari https://github.com/tiangolo/uwsgi-nginx-flask-docker untuk webservice (flask, uwsgi dan nginx).

Saat saya coba menjalankan dua container (model server NER dan web service), sempat terbentur masalah. Pertama, di model server yang menggunakan socketserver,¬† tidak bisa menggunakan “localhost” tetapi 0.0.0.0 (masalah binding?).¬† Kedua,¬† container web service ternyata tidak dapat menghubungi model server (masalah komunikasi antar dua container). Solusinya saat container web service dijalankan, tambahkan parameter run –add-host=parent-host:`ip route show | grep docker0 | awk ‘{print \$9}’`

Solusi yang lebih elegan adalah menggunakan docker compose. Jadi di docker-compose.yml isinya seperti ini untuk mendefinisikan web service dan model server:

version: "3"
services:
  web:
    build: .
    ports:
      - "5000:80"
  ner5w1h:
    image: modelserverner5w1h
    ports:
      - "6200:6200"

Setelah itu di web service, nama host bisa langsung menggunakan nama container yang dituju.

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(("ner5w1h", 6200))  # gunakan nama sesuai docker-compose.yml
        s.sendall(words.encode('utf-8'))
        data = s.recv(1024)
        s.close
    return data.decode('utf-8')

Catatan lain tentang docker yang saya temui:

  1. Untuk melihat isi image, gunakan “docker run -it namaimage sh”.¬† Ini gara-gara saya kira opsi “ADD model” akan otomatis membuat isi direktori /model (ternyata cuma copy dalamnya saja). Harusnya “ADD model /model”.
  2. Untuk melihat isi log gunakan¬† “docker logs -f namacontainer”. Sedangkan code untuk loggingnya adalah sebagai berikut (cmiiw):
def get_module_logger(mod_name):
    """
    penggunaan: get_module_logger(__name__).info("mulai...")
    """
    logger = logging.getLogger(mod_name)
    handler = logging.StreamHandler()
    formatter = logging.Formatter(
        '%(asctime)s [%(name)-12s] %(levelname)-8s %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    logger.setLevel(logging.DEBUG)
    return logger

docker-compose cocok untuk multi docker pada satu host. Jika sudah melibatkan banyak host, katanya dianjurkan menggunakan kubernetes. Tapi untuk sekarang cukup dulu ūüôā

 

GloVe untuk Wikipedia Bahasa Indonesia

Sebelumnya saya sudah membuat vector word2vec  Wikipedia Bhs Indonesia dengan Gensim. Posting ini akan membahas model embedded word yang lain yaitu GloVe.  Saya akan gunakan untuk task NER. Pengalaman saya dulu untuk task textual entailment bahasa Inggris, Glove lebih baik daripada Word2Vec.

Untuk GloVe, saya tidak menemukan implementasinya dalam Python, yang ada adalah dari penulisnya langsung dalam C.  Berikut langkah-langkahnya.

  1. Download source code dari https://nlp.stanford.edu/projects/glove/
  2. Ekstrak dan masuk ke directorynya, lalu ketik “make” untuk mem-build source code.
  3. Jalankan ./demo.sh   demo ini akan mendownload data text8 sekitar 30MB. Jika berhasil artinya program bisa kita gunakan.
  4. Siapkan file teks gabungan dari artikel wikipedia bahasa indonesia (posting saya tentang ini)
  5. Ubah demo.sh jadi seperti ini. File input ada di variabel CORPUS, file output ada di variabel SAVE_FILE.   Saya buang bagian download file dan bagian evaluasi.
#!/bin/bash

CORPUS=wiki.id.case.text
VOCAB_FILE=vocab.txt
COOCCURRENCE_FILE=cooccurrence.bin
COOCCURRENCE_SHUF_FILE=cooccurrence.shuf.bin
BUILDDIR=build
SAVE_FILE=glove_wiki_id_50
VERBOSE=2
MEMORY=4.0
VOCAB_MIN_COUNT=5
VECTOR_SIZE=50
MAX_ITER=15
WINDOW_SIZE=15
BINARY=2
NUM_THREADS=8
X_MAX=10

$BUILDDIR/vocab_count -min-count $VOCAB_MIN_COUNT -verbose $VERBOSE  $VOCAB_FILE
if [[ $? -eq 0 ]]
  then
  $BUILDDIR/cooccur -memory $MEMORY -vocab-file $VOCAB_FILE -verbose $VERBOSE -window-size $WINDOW_SIZE  $COOCCURRENCE_FILE
  if [[ $? -eq 0 ]]
  then
    $BUILDDIR/shuffle -memory $MEMORY -verbose $VERBOSE  $COOCCURRENCE_SHUF_FILE
    if [[ $? -eq 0 ]]
    then
       $BUILDDIR/glove -save-file $SAVE_FILE -threads $NUM_THREADS -input-file $COOCCURRENCE_SHUF_FILE -x-max $X_MAX -iter $MAX_ITER -vector-size $VECTOR_SIZE -binary $BINARY -vocab-file $VOCAB_FILE -verbose $VERBOSE

    fi
  fi
fi

Untuk mengetest hasilnya, kita bisa gunakan code sebelumnya karena Gensim bisa mengkonversi file GloVe.

Konversi dari Glove ke word2vec (diambil dari: https://radimrehurek.com/gensim/scripts/glove2word2vec.html)

from gensim.test.utils import datapath, get_tmpfile
from gensim.models import KeyedVectors
from gensim.scripts.glove2word2vec import glove2word2vec

namaFileGlove = "glove_wiki_id.txt"
glove_file = datapath(namaFileGlove)
tmp_file = get_tmpfile("w2vec_glove_wiki_id.txt")

glove2word2vec(glove_file, tmp_file)

Sekarang kita coba dengan code yang sama dengan Word2Vec sebelumnya (untuk load gunakan KeyedVectors.load_word2vec_format). Supaya sama, saya gunakan ukuran VECTOR_SIZE=400, walaupun prosesnya jadi lebih lama dan filenya lebih besar.

from gensim.models import KeyedVectors
namaFileModel = "w2vec_glove_wiki_id.txt"
model = KeyedVectors.load_word2vec_format(namaFileModel)
hasil = model.most_similar("Bandung")
print("Bandung:{}".format(hasil))
hasil = model.most_similar("tempo")
print("tempo:{}".format(hasil))
hasil = model.most_similar("Tempo")
print("Tempo:{}".format(hasil))
hasil = model.most_similar("Soekarno")
print("Soekarno:{}".format(hasil))

sim = model.similarity("bakso", "nasi")
print("Kedekatan bakso-nasi: {}".format(sim))
sim = model.similarity("bakso", "pecel")
print("Kedekatan bakso-pecel: {}".format(sim))
sim = model.similarity("bakso", "mobil")
print("Kedekatan bakso-mobil: {}".format(sim))

hasil = model.most_similar_cosmul(positive=['perempuan', 'raja'], negative=['pria'])
print("pria-raja, perempuan-?: {}".format(hasil))

hasil = model.most_similar_cosmul(positive=['perempuan', 'raja'], negative=['lelaki'])
print("lelaki-raja, perempuan-?:{}".format(hasil))

hasil = model.most_similar_cosmul(positive=['minuman', 'mangga'], negative=['buah'])
print("buah-mangga, minuman-?:{}".format(hasil))

Hasilnya sebagai berikut

Bandung:[('Bogor', 0.5553832650184631), ('Surabaya', 0.5533844232559204), ('Jakarta', 0.5264717936515808), ('Medan', 0.5121393203735352), ('Semarang', 0.4910121262073517), ('Yogyakarta', 0.4880320131778717), ('Malang', 0.48358896374702454), ('Jawa', 0.4750467836856842), ('ITB', 0.4737907946109772), ('Persib', 0.4654899537563324)]
tempo:[('indonesiana', 0.5886592268943787), ('doeloe', 0.5427557229995728), ('putu_suasta', 0.48804518580436707), ('tapin', 0.46188244223594666), ('https', 0.41826149821281433), ('cepat', 0.40567928552627563), ('ketukan', 0.4037955701351166), ('irama', 0.3982717990875244), ('lambat', 0.39812949299812317), ('maestoso', 0.39417707920074463)]
Tempo:[('Majalah', 0.54466712474823), ('Koran', 0.5328548550605774), ('Doeloe', 0.5282064080238342), ('majalah', 0.4538464844226837), ('Kompas', 0.4463438391685486), ('wartawan', 0.4179822504520416), ('koran', 0.41709277033805847), ('Harian', 0.40668201446533203), ('Republika', 0.3915051221847534), ('Post', 0.38742369413375854)]
Soekarno:[('Hatta', 0.6839763522148132), ('Soeharto', 0.5900896787643433), ('Sukarno', 0.5895135998725891), ('Bung', 0.49154624342918396), ('Vannico', 0.4613707363605499), ('Megawati', 0.46065616607666016), ('Karno', 0.4603942334651947), ('Presiden', 0.4588601887226105), ('Ekki', 0.45219823718070984), ('WIII', 0.4458869993686676)]
Kedekatan bakso-nasi: 0.33218569528946
Kedekatan bakso-pecel: 0.3385669314106577
Kedekatan bakso-mobil: 0.1036423556873547
pria-raja, perempuan-?: [('Raja', 0.8700850605964661), ('kerajaan', 0.8684984445571899), ('Yehuda', 0.8591107130050659), ('cucu', 0.8312298059463501), ('AbiMilki', 0.821474552154541), ('memerintah', 0.8194707632064819), ('saudara', 0.8159937262535095), ('Daud', 0.8155518770217896), ('Kerajaan', 0.8149770498275757), ('penguasa', 0.8049719333648682)]
lelaki-raja, perempuan-?:[('Raja', 0.9214608669281006), ('kerajaan', 0.919419527053833), ('Kerajaan', 0.8668190240859985), ('AbiMilki', 0.8551638722419739), ('ratu', 0.8542945384979248), ('penguasa', 0.8345737457275391), ('terakhir', 0.8345482349395752), ('disebutkan', 0.8269140720367432), ('istana', 0.82608562707901), ('istri', 0.8246856331825256)]
buah-mangga, minuman-?:[('beralkohol', 0.7880735397338867), ('Schorle', 0.7836616039276123), ('bersoda', 0.7783095240592957), ('manggaan', 0.7711527943611145), ('jeruk', 0.7603545784950256), ('anggur', 0.7549997568130493), ('Minuman', 0.7476464509963989), ('Frappuccino', 0.740592360496521), ('jahe', 0.7360817790031433), ('mocha', 0.7357983589172363)]

Penasaran, berikut hasil kalau vector_size-nya 50 (default)

Bandung:[('Surabaya', 0.8777784109115601), ('Malang', 0.8505295515060425), ('Jakarta', 0.8406218886375427), ('Medan', 0.8344693183898926), ('Semarang', 0.8225082159042358), ('Yogyakarta', 0.8207614421844482), ('Bogor', 0.8181610703468323), ('Makassar', 0.7571447491645813), ('Tangerang', 0.7515754699707031), ('Solo', 0.7264706492424011)]
tempo:[('doeloe', 0.7084428668022156), ('indonesiana', 0.6802346706390381), ('read', 0.6363065242767334), ('pas', 0.6065201759338379), ('indonesia', 0.5810031890869141), ('pda', 0.5744251608848572), ('putu_suasta', 0.5698538422584534), ('nada', 0.5527507066726685), ('html', 0.5519558787345886), ('irama', 0.5514932870864868)]
Tempo:[('Koran', 0.8052877187728882), ('Majalah', 0.7781724333763123), ('Kompas', 0.7708441019058228), ('Gramedia', 0.7339286208152771), ('Penerbit', 0.7299134731292725), ('Harian', 0.7244901657104492), ('Republika', 0.7203424572944641), ('koran', 0.7195203900337219), ('KOMPAS', 0.7062090635299683), ('Doeloe', 0.7039147615432739)]
Soekarno:[('Hatta', 0.876067042350769), ('Sukarno', 0.8076358437538147), ('Soeharto', 0.7557047605514526), ('Bung', 0.7302334308624268), ('kemerdekaan', 0.7065078616142273), ('Karno', 0.6804633736610413), ('Basuki', 0.6803600788116455), ('Kemerdekaan', 0.6702237129211426), ('Yudhoyono', 0.6673594117164612), ('Susilo', 0.6618077754974365)]
Kedekatan bakso-nasi: 0.6207393500954625
Kedekatan bakso-pecel: 0.5784330569151002
Kedekatan bakso-mobil: 0.28361517810153536
pria-raja, perempuan-?: [('Yehuda', 0.9973295331001282), ('memerintah', 0.9838510155677795), ('Herodes', 0.9673323631286621), ('Raja', 0.9654756784439087), ('Daud', 0.9616796970367432), ('putranya', 0.9616104960441589), ('kerajaan', 0.9497379660606384), ('cucu', 0.9484671950340271), ('Firaun', 0.947074830532074), ('menantu', 0.9469170570373535)]
lelaki-raja, perempuan-?:[('kerajaan', 1.0183593034744263), ('memerintah', 1.0134179592132568), ('penguasa', 1.0113284587860107), ('Raja', 0.9971156716346741), ('Kerajaan', 0.9939565658569336), ('takhta', 0.9919894933700562), ('tahta', 0.9914684891700745), ('istana', 0.9877175092697144), ('kekuasaan', 0.983529269695282), ('MANURUNGNGE', 0.9810593128204346)]
buah-mangga, minuman-?:[('Arak', 1.0535870790481567), ('Crawlers', 0.9980041980743408), ('Carpet', 0.9971945285797119), ('Rimpang', 0.

Sepertinya untuk analogi lebih bagus Word2Vec. Berbeda dengan word2vec, Bakso-nasi lebih dekat dibandingkan bakso-pecel. Hasil kedekatan kata juga berbeda. Kalau lihat sekilas sepertinya lebih bagus Word2Vec, tapi saat saya coba untuk task NER, lebih bagus GloVe (naik dari 0.70 ke 0.72 untuk ukuran 50 sedangkan untuk ukuran vector 400 hasilnya hanya naik sedikit). Mungkin perlu buat dataset untuk evaluasi word embedding ini.

Update:
Jika mau men-train dokumen Bahasa Inggris di demo.sh ada fungsi untuk mengevaluasi, code pythonnya menggunakan Python2 dan lib numpy, jika ingin menggunakan virtualenv, langkahnya sbb:

masuk ke direktory Glove,
mkdir virtenv
virtualenv -p /usr/bin/python2 virtenv
source virtenv/bin/activate
pip install numpy

Update demo.sh sebelum pemanggilan evaluate:

source virtenv/bin/activate
python eval/python/evaluate.py

POS Tagger Bahasa Indonesia dengan Python

Posting sebelumnya: POS Tagger dengan Syntaxnet

Posting terkait: POS Tagger dan Dependency Parser dengan StanfordNLP

Secara bertahap, saya dan istri akan migrasi dari Java ke Python. Salah satu yang kami perlukan adalah POS (Part of Speech)-Tagger Bahasa Indonesia.

Ini cara yang paling sederhana  karena saya sudah sediakan modelnya, untuk cara trainingnya ada di bagian bawah.

Saya menggunakan CRFTagger, jadi library yang perlu diinstall: numpy, nltk dan python-crfsuite.

Lalu download pretrained model (1.6MB) yang saya buat berdasarkan data Fam Rashel (200rb-an token) di https://drive.google.com/open?id=12yJ82GzjnqzrjX14Ob_p9qnPKtcSmqAx

Untuk menggunakannya (sesuaikan path jika diperlukan):

from nltk.tag import CRFTagger
ct = CRFTagger()
ct.set_model_file('all_indo_man_tag_corpus_model.crf.tagger')
hasil = ct.tag_sents([['Saya','bekerja','di','Bandung']])
print(hasil)

Hasilnya akan seperti ini:

[[(‘Saya’, ‘PRP’), (‘bekerja’, ‘VB’), (‘di’, ‘IN’), (‘Bandung’, ‘NNP’)]]

Selesai… gampang kan ūüôā

Jika ada yang berminat untuk training sendiri, ada beberapa dataset POS-Tag Bahasa Indonesia:

https://github.com/UniversalDependencies/UD_Indonesian
https://github.com/famrashel/idn-tagged-corpus
http://www.panl10n.net/english/OutputsIndonesia2.htm
https://lindat.mff.cuni.cz/repository/xmlui/handle/11234/1-1989

Saya menggunakan data milik Fam Rashel, code untuk training-nya adalah sbb (mungkin belum efisien, saya masih belajar Python):

from nltk.tag import CRFTagger

jumSample = 500000
namaFile = "/home/yudiwbs/dataset/pos-tag-indonesia/idn-tagged-corpus-master/Indonesian_Manually_Tagged_Corpus.tsv"
with open(namaFile, 'r', encoding='utf-8') as f:
    lines = f.read().split('\n')

pasangan = []
allPasangan = []

for line in lines[: min(jumSample, len(lines))]:
    if line == '':
        allPasangan.append(pasangan)
        pasangan = []
    else:
        kata, tag = line.split('\t')
        p = (kata,tag)
        pasangan.append(p)

ct = CRFTagger()
ct.train(allPasangan,'all_indo_man_tag_corpus_model.crf.tagger')
#test
hasil = ct.tag_sents([['Saya','bekerja','di','Bandung'],['Nama','saya','Yudi']])
print(hasil)

Dokumentasi lengkap tentang lib POS-Tag NLTK dapat dilihat di: http://www.nltk.org/api/nltk.tag.html 

Untuk sekarang saya belum buat pengukuran kinerja model yang dihasilkan.

NER (Named Entity Recognition) Bahasa Indonesia dengan Stanford NER

 

Update, posting lanjutan: NER Bahasa Indonesia dengan anaGo (Ptyhon+Keras)

Posting sebelumnya tentang NER


Untuk mengekstrak named entity Bahasa Indonesia, kita dapat memanfaatkan library Stanford NER untuk membuat model yang di-train dengan dataset Bahasa Indonesia. Pertama download Stanford NER di: https://nlp.stanford.edu/software/CRF-NER.html#Download

Sedangkan dataset NER Bahasa Indonesia untuk training dapat diperoleh di:

 https://github.com/yohanesgultom/nlp-experiments/blob/master/data/ner/training_data.txt

dan

https://github.com/yusufsyaifudin/indonesia-ner/tree/master/resources/ner

Catatan: format dataset di atas tidak sesuai dengan Stanford NER, jadi perlu dikonversi ke format dua kolom seperti ini:

Sementara	O
itu	O
Pengamat	O
Pasar	O
Modal	O
Dandossi	PERSON
Matram	PERSON
mengatakan	O
,	O
sulit	O
bagi	O
sebuah	O
kantor	ORGANIZATION
akuntan	ORGANIZATION
publik	ORGANIZATION
(	O
KAP	ORGANIZATION
)	O

Selanjutnya untuk training, dokumentasinya ada di:

https://nlp.stanford.edu/software/crf-faq.html#a:   

Setelah training selesai dan model didapat, maka cara menggunakannya adalah sebagai berikut.

Ambil stanford-ner-resources.jar, letakkan di direktori lib. Jika menggunakan Gradle maka setting gradle-nya sbb:


repositories {
  flatDir {
    dirs 'libs'
  }
}

dependencies {
 testCompile group: 'junit', name: 'junit', version: '4.12'
 compile group: 'edu.stanford.nlp', name: 'stanford-parser', version: '3.8.0'
 compile name: 'stanford-ner-3.8.0'
}

Selanjutnya gunakan code yang ada di NERDemo.java.¬† Sesuaikan variabel serializedClassifier dengan lokasi model bahasa Indonesia. Outputnya akan seperti ini untuk input “Budi Martami kuliah di UPI yang berlokasi di Bandung”:


---
Budi/PERSON Martami/PERSON kuliah/O di/O UPI/ORGANIZATION yang/O berlokasi/O di/O Bandung/LOCATION ./O
---
Budi	PERSON
Martami	PERSON
kuliah	O
di	O
UPI	ORGANIZATION
yang	O
berlokasi	O
di	O
Bandung	LOCATION
.	O
---

Ekstraksi Pasangan Pertanyaan-Jawaban dari Forum Online

Forum online masih memiliki potensi yang besar walaupun popularitasnya turun sejalan dengan populernya group FB dan app group chat.¬† Di forum online, thread sudah dikelompokkan dalam topik dan umumnya ada moderator yang mencegah spam dan bot sehingga¬† data lebih “bersih” dibandingkan Twitter.¬† Saya jadi tertarik untuk mencoba me-mining data di forum online ini.

Salah satu fungsi utama forum adalah untuk media tanya jawab.  Ini yang rencananya saya akan ekstrak, pasangan pertanyaan dan jawabannya (PPJ). Pasangan ini  nantinya akan digunakan sebagai basis pengetahuan  QA (question-answering) system, termasuk chatbot.

Saya memilih forum online kaskus, dengan sub topik roda-empat, dan dengan thread mengenai mobil ayla agnia (karena paling banyak).  Bentuknya megathread, yaitu satu thread besar dengan ratusan halaman. Saya crawl dengan sangat pelan (30 menit per halaman) jadi mudah-mudahan tidak menggangu.  Untuk mengambil pasangan pertanyaan-jawaban (PPJ),  saya ambil posting yang menggunakan quote  dan reply (gambar bawah).

quote

Masalahnya, tidak semua quote dan reply adalah PPJ. Ada  quote yang bukan pertanyaan, ada reply yang berbentuk pertanyaan balik (bukan jawaban) dan ada  pasangan quote-reply yang tidak relevan (misal sapaaan selamat pagi dan jawabannya).

Saya menggunakan klasifikasi teks untuk mencari pasangan quote-reply yang merupakan PPJ. Pelabelan dilakukan oleh saya sendiri, dari  1030 pasangan quote-reply, 189 masuk ke kelas PPJ dan 841 non-PPJ.   Di luar dugaan, ternyata hanya sedikit pasangan quote-reply yang dapat digunakan (10%-an).  Penyebabnya mungkin mega-thread juga digunakan untuk tempat kumpul-kumpul sehingga banyak percakapan yang keluar dari topik.

Selanjutnya saya coba beberapa teknik klasifikasi dan untuk pertama kalinya saya menggunakan scikit-learn¬† (pelan-pelan migrasi dari Java ke Python ūüôā ) .¬† ¬†Karena ini eksplorasi pertama, supaya cepat saya tidak menggunakan praproses dan fitur-fitur lain, hanya bag-of-words teks quote dan reply-nya. Masalah imbalance juga tidak ditangani.

Data displit menjadi 70% data training, 30% data validasi. Hasil terbaik adalah dengan teknik SGD sbb  (precision, recall, F1):

Non PPJ:  0.89, 0.97, 0.93
PPJ: 0.76, 0.43, 0.55

Hasilnya menurut saya lumayan, mengingat kelas PPJ hanya 10% (kelas minoritas) dan belum dilakukan optimasi apapun.

Untuk eksplorasi berikutnya (selain tentunya meningkatkan akurasi):

  • Lintas domain, misalnya model yang ditraining untuk data otomotif sebagus apa jika diaplikasikan ke data thread tentang smartphone.
  • Pertanyaan-jawaban yang berbentuk thread diskusi. Jadi PPJ yang saling berkait.¬† ini mungkin¬† cocok untuk chatbot, agar bot dapat memberikan respon yang lebih natural.
  • Membangun QA System atau chatbot yang menggunakan data ini.

 

Link ke paper: https://osf.io/preprints/inarxiv/5rxak/

Part of Speech Tagger dan Dependency Parser Bahasa Indonesia: Syntaxnet

Update: demo visualiasi dependency: http://nlp.yuliadi.pro/ (tab ke-3).

Posting terkait:
Dependency Parser dengan lib UUParser
POS Tagger dengan NLTK Python.

Fungsi POS Tagger adalah memberi label jenis kata (kata benda, kata kerja dst), sedangkan dependency parser berfungsi mencari struktur tata bahasa seperti subyek, obyek dan keterkaitan antar kata (parent dan child yang bergantung pada parent).  POS tagger dan dependency parser berguna untuk banyak task NLP.

POS Tagger bahasa Indonesia masih belum banyak. Saya hanya menemukan dua, yang pertama dari UI http://bahasa.cs.ui.ac.id/postag/tagger  dengan lisensi creative commons non commercial dan lainnya INANLP dari ITB tetapi tidak tersedia bebas source code-nya (cmiiw). Untuk dependency parser Bahasa Indonesia saya belum menemukannya.

Saat saya mempelajari Tensorflow, saya melihat library Syntaxnet yang menggunakan Tensorflow untuk POS Tagger dan dependency parser, dan ternyata sudah ada parser untuk Bahasa Indonesia. Sudah ada pretrained model  sehingga tidak perlu lagi mencari data dan melakukan training lagi. Lisensinya juga Apache 2.0 yang sangat longgar. Berikut proses instalasinya.

Pertama siapkan Linux dan install Tensorflow, terutama yang penting bagian persiapan terkait software pendukung GPU-nya (posting saya tentang menginstall tensorflow). Mungkin sebenarnya setelah persiapan  GPU, bisa saja langsung install Syntaxnet karena syntaxnet mengcompile ulang source, tapi untuk amannya, install saja keseluruhan Tensorflow.  Selanjutnya  ikuti petunjuk instalasi Syntaxnet  yang ada di: https://github.com/tensorflow/models/tree/master/research/syntaxnet#installation

Update April 2018:  sudah ada binary installation yang lebih mudah dibandingkan compile source code.

Prosesnya cukup lama (2-3 jam)untuk menginstall Syntaxnet ini dari source code, termasuk mencompile core tensorflow lagi. Padahal saya sudah menginstall tensorflow sebelumnya. Lalu ada beberapa peringatan tentang SS3, AVX dst,  yang saya tidak tahu harus diset dimana. Waktu install Tensorflow sebelumnya, warning tersebut hilang setelah saya intsall dari source bukan binary.

Untuk mencoba  apakah instalasi berhasil, masuk ke direktori /models/syntaxnet lalu jalankan:

echo 'Rudi is eating the rice.' | sudo sh syntaxnet/demo.sh
Hasilnya:
+-- Rudi NNP nsubj
 +-- is VBZ aux
 +-- rice NN dobj
 | +-- the DT det
 +-- . . punct

Berikutnya kita akan meload pretrained untuk bahasa Indonesia, penjelasannya ada di:

https://github.com/tensorflow/models/blob/master/research/syntaxnet/g3doc/universal.md

Download model Bahasa Indonesia di:

http://download.tensorflow.org/models/parsey_universal/Indonesian.zip

Ekstrak  zip tersebut (kalau saya ekstrak di /models)

Untuk menjalankan parser, masuk ke direktori syntaxnet, lalu masukan perintah. Pastikan tidak ada slash setelah direktori model bahasa Indonesia ( ~/models/Indonesian bukan ~/models/Indonesian/ )

echo 'Budi makan nasi enak sekali' | sudo sh syntaxnet/models/parsey_universal/parse.sh ~/models/Indonesian

Maka hasilnya

1 Budi _ PROPN _ fPOS=PROPN++ 2 nsubj _ _
2 makan _ VERB _ fPOS=VERB++ 0 ROOT _ _
3 nasi _ NOUN _ fPOS=NOUN++ 2 dobj _ _
4 enak _ ADJ _ fPOS=ADJ++ 3 amod _ _
5 sekali _ ADV _ fPOS=ADV++ 4 advmod _

PROPN adalah proper noun, ADJ adalah adjective dst.

Sedangkan hasil dependency parser dapat dibaca: “makan” adalah ROOT dengan parent 0, “Budi” adalah subyek (nsubj) dengan parent no 2 (“makan”) dst. ¬†Jika¬†digambarkan dependency treenya adalah¬†sbb.

budi_makan_nasi