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)

Iklan

Pengantar Pemrosesan Teks dengan Keras (Bagian 1: Tensor, Batch, Layer dan Learning)

Deep learning saat ini sudah umum digunakan dalam berbagai task NLP.  Materi posting ini saya ambil dari buku Chollet “Deep Learning with Python” dan berbagai sumber lain.  Perlu dibagi menjadi beberapa bagian karena berdasarkan pengalaman saya jika langsung loncat ke source code tanpa paham dasar-dasarnya  nanti malah jadi  bingung sendiri. Posting ini masih membahas Keras secara umum.

Keras adalah library yang ditujukan untuk memudahkan pembuatan model NN (Neural Network). Keras menyediakan fasilitas untuk membuat berbagai arsitektur NN seperti  convolutional, recurrent, multi input-multi output, layer sharing dan model sharing.  Keras menggunakan backend Tensorflow atau CNTK (Microsoft Cognitive Toolkit). Lisensi keras adalah MIT yang longgar sehingga mudah digunakan untuk kepentingan komersial.

Instalasi Keras sangat mudah: pip install tensorflow lalu pip install keras

Urutan umum pembuatan NN di Keras adalah:

  1. Mendefinisikan data training  berupa input tensor dan target tensor.
  2. Mendefinisikan layer yang akan memetakan antara input ke target.
  3. Konfigurasi proses learning dengan memilih loss function, optimizer dan metric yang harus dimonitor.
  4. Lakukan training dengan memanggil method fit()

Konsep dasar yang penting dalam Keras adalah Tensor.  Tensor adalah struktur data berupa matrix atau multidimensional array yang menjadi dasar penyusun NN. Tensorflow dan library deep NN lainnya adalah library yang dapat memproses secara efisien komputasi pada tensor.

Ukuran dimensi yang umum pada tensor adalah 0D sampai 5D, contoh:

  1. Tensor 0D yang berisi satu nilai;
  2. Tensor 1D, satu dimensi atau vektor. Misalnya  x = np.array([10,12,13,14]).  Variabel x ini karena memiliki empat nilai disebut 4-dimensional vector tetapi tetap tensor 1 dimensi karena hanya memiliki satu axis. Jadi perlu dibedakan antara 4D vector dan 4D tensor.
  3. Tensor 2D, matrix.  Contoh:  x = np.array( [[10,12,13,14], [11,21,32,41]] ).  Variabel x ini dapat dilihat sebagai dua instance yang memiliki 4 fitur. Tensor ini cocok digunakan untuk merepresentasikan data berbentuk tabel: axisnya adalah (instance, features).
  4. Tensor 3D. Misalnya untuk merepresentasikan time series. Axisnya: (instance, timesteps, fitur).
  5. Tensor 4D. Cocok untuk merepresentasikan image, axisnya: (instance, tinggi, lebar, warna).
  6. Tensor 5D. Video sering direpresentasikan dengan tensor ini dengan axis: (samples, frames, tinggi, lebar, warna)

Dokumen teks dapat direpresentasikan dengan frekuensi kemunculan kata atau tf-idf. Dengan asumsi jumlah kosakata 20000, maka tiap dokumen dapat direpresentasikan dengan vektor 20000 elemen. Jika terdapat 500 dokumen, data dapat direpresentasikan dengan tensor 2D dengan axis (instance, vektor tf-idf) atau shape  (500, 20000). Mengenai shape akan dijelaskan nanti.

Contoh lain adalah tweet, asumsikan tweet akan direpresentasikan di level karakter. Panjang setiap tweet maksimal adalah 280 karakter dengan jumlah karakter yang mungkin adalah 128 (a-z, A-Z, 0-9 dst). Setiap karakter dapat di-encode sebagai binary vector berukuran 128, contohnya karakter “a” menjadi [1,0,0,0 ….0], karakter “b” [0,1,0,0… 0] dan seterusnya. Jadi setiap tweet dapat di-encode menjadi tensor 2D (280, 128) dengan isi 0 dan 1. Jika terdapat 1 juta tweet, maka akan disimpan dalam tensor 3D denga axis (instance, jumlah_karakter, encode_karakter)  atau shape (1000000, 280, 128).

Tensor didefinisikan oleh tiga atribut,  jumlah axis, shape dan tipe datanya.

  1. Axis sudah dibahas sebelumnya, tensor 3D akan memiliki jumlah axis (rank) =3, tensor 2D rank-nya = 2 dst.
  2. Tipe data (dtype) yang digunakan tensor: float32, uint9, float64 dan char.
  3. Shape memperlihatkan dimensi yang dimiliki untuk setiap axis. Contoh untuk  tensor 3D berikut ini, shapenya adalah (2,3,4). Axis nol terdiri atas 2 dimensi, axis ke-1 tiga dimensi dan axis ke-3  4 dimensi:

x = np.array(
[[[10,12,13,11],
[11,21,32,15],
[30,21,31,22]],
[[10,12,13,4],
[11,31,52,5],
[21,51,12,9]]]
)
print(x.shape) # hasilnya (2,3,4)

Contoh lain untuk tensor 2D berikut,  shape adalah (2,4)
x = np.array([[10,12,13,14],[11,21,32,41]])

Vector dengan elemen tunggal ini akan memiliki shape (4,)
x = np.array([10,12,13,14])

Scalar akan memiliki shape ()

Batch: Axis & Dimension

Umumnya axis yang pertama (axis ke-0) disebut sample axis atau sample dimension. Sebagai contoh untuk pengenalan karakter dari gambar yang terdiri atas 1000 gambar dengan 30×30 pixel grayscale, akan menggunakan tensor 3D dengan shape (1000, 30, 30) yang berisi nilai 0..255 (grayscale). Axis ke-0 (1000) adalah jumlah instances atau samples. Pada tensor 2D dengan shape (10000, 20) artinya ada 10000 samples dengan 20 fitur. Dapat dianggap  shape adalah (samples, feature).

Model DL umumnya tidak memproses data secara sekaligus tetapi dibagi-bagi menjadi batch yang lebih kecil. Itu sebabnya axis 0 sering juga disebut batch axis atau batch dimension.

Layer

Layer adalah bagian yang menerima input satu atau lebih tensor dan mengeluarkan output satu atau lebih tensor. Ada layer yang stateless, tetapi umumnya layer memiliki state yaitu bobotnya.

Terdapat berbagai jenis layer untuk berbagai jenis pemrosesan data. Keras menyediakan layer standar yang umum digunakan dalam deep learning. Misalnya dense connected layer (kelas Dense di Keras), recurrent layer (kelas LSTM) dan convolutional layer (kelas Conv2D). Membangun deep NN di Keras adalah menyusun berbagai layer seperti membangun mainan LEGO.

Setiap layer menerima input tensor dan mengeluarkan output tensor dalam ukuran (shape) yang sudah ditentukan. Ini disebut layer compatibility.

Sebagai contoh, code berikut membuat model dengan dua layer berukuran 32 neuron dan menerima 500 fitur pada layer input. Layer kedua adalah layer output.

from keras import models
from keras import layers

#definisikan model sebagai urutan layer
model = models.Sequential()
#model menerima input dengan jumlah fitur 500 dan output 32
#jumlah instance tidak perlu dituliskan dalam input_shape
model.add(layers.Dense(32, input_shape=(500,)))
#input layer ini otomatis 32 (output layer sebelumnya)
model.add(layers.Dense(32))

Sebagai contoh, code berikut membuat model dengan dua layer berukuran 32 neuron dan menerima 500 fitur pada layer input. Layer kedua adalah layer output.

Learning

Pada Keras, tahapan learning dikonfigurasi di bagian kompilasi. Konfigurasi yang perlu didefinisikan adalah optimizer dan loss function yang akan digunakan model dan metric yang akan dipantau  saat training. Loss merupakan jarak antara nilai target dan prediksi untuk setiap sample, sedangkan metric mengukur kinerja keseluruhan model.

Loss function mengukur kinerja NN terhadap data training yang  digunakan untuk mengarahkan atau memperbaiki network ke arah yang tepat. Keras menyediakan beberapa loss function standar yang dapat dipilih sesuai task, misalnya untuk klasifikasi biner, dapat digunakan binary cross entropy; untuk klasifikasi multi kelas dapat digunakan categorical cross entropy; mean-squared error untuk regresi dan connectionist temporal classification (CTC) untuk sequence labeling.

Optimizer adalah mekanisme yang digunakan  untuk mengupdate bobot jaringan berdasarkan loss function. Berbagai jenis class optimizer di Keras yang dapat digunakan adalah RMSprop, Adagrad, Adadelta, Adam, Adamax, Nadam.

Contoh proses compile dan training dapat dilihat pada kode berikut

from keras import optimizers
model.compile(optimizer=optimizers.RMSprop(lr=0.001),loss='mse',metrics=['accuracy'])

Setelah itu, input dan target diberikan pada method fit() seperti pada Scikit-Learn

model.fit(input_tensor, target_tensor, batch_size=128, epochs=10)

Contoh lebih rinci dapat dilihat pada posting berikutnya, klasifikasi teks [bersambung].

Posting lanjutan: bagian 2, representasi teks dan klasifikasi teks dengan feed neural network.

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') 

Mempercepat Tensorflow tanpa GPU

Kebanyakan orang sudah tahu bahwa GPU akan mempercepat proses pembuatan model dengan Tensorflow, masalahnya tidak semua mempunyai akses komputer dengan GPU. Laptop saya misalnya,  tidak memiliki GPU yang dapat digunakan oleh Tensorflow.

Saat menjalankan Tensorflow  mungkin ada yang terkena  warning seperti ini “The TensorFlow library wasn’t compiled to use SSE4.1 instructionn … SSE4.2 instructions .. AVX instructions “.  Itu adalah fitur-fitur CPU yang dapat mempercepat proses komputasi.  Default installer binary Tensorflow tidak menggunakan ini agar dapat men-support  CPU lama.

Solusi untuk meng-enable fitur ini adalah dengan mengcompile sendiri source code TensorFlow.  Saya pernah bahas tentang ini di https://yudiwbs.wordpress.com/2017/02/26/mencoba-tensorflow/    Tapi compile perlu waktu lama dan ribet.  Saya sekarang biasanya langsung install saja, praktis,  asal ada koneksi internet.

Saat ini ada alternatif lain, saya menemukan repo yang sudah meng-compile-kan Tensorflow dengan opsi AVX, FMA, SSE. https://github.com/lakshayg/tensorflow-build  Jadi tidak perlu compile sendiri dari source code 🙂

Saya menggunakan Pycharm, sayangnya tidak ada opsi install whl langsung dari IDE-nya, jadi perlu via terminal. Langkah-langkahnya (dengan virt-env) sbb:

Masuk ke directory project atau di mana virtenv berada dan aktifkan.

 source venv/bin/activate 

Pastikan (venv) muncul, soalnya saat percobaan pertama saya malah install di global. Cek versi python di virt-env ini, karena bahkan versi 3.5 dan 3.6 tidak compatible.  Cek juga versi gcc.

python --version
gcc -v

Download binary yang cocok di https://github.com/lakshayg/tensorflow-build, pastikan versinya sudah benar (ubuntu, python, gcc). Lalu install dengan pip biasa

pip install --ignore-installed --upgrade [lokasi .whl yang didownload]

Kembali ke Pycharm dan otomatis akan diindeks.

Penasaran tentang berapa peningkatan kecepatannya, saya coba dengan Keras untuk klasifikasi teks dataset review IMDB. Hasilnya ada peningkatan  27%  untuk  proses training (26 detik vs 36 detik).  Sangat lumayan  🙂

Analis Sentimen Berbasis Aspek

Update:
nlp.yuliadi.pro/sentimen 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: nlp.yuliadi.pro/sentimen   Datasetnya saya anotasi sendiri dan saat ini masih belum bisa di-share.

 

Dependency Parsing Bahasa Indonesia dengan Lib UUParser

Update: demo visualisasi dependency dapat dilihat di: http://nlp.yuliadi.pro/ (tab ke-3)

Salah satu cara memandang struktur sebuah kalimat adalah dengan dependency grammar. Pada  dependency grammar,  setiap kata dianggap bergantung dengan kata yang lain. Sebagai contoh untuk kalimat “Ibu pergi ke pasar”,  maka kata “pergi” adalah akar kalimat (root), kata “ibu” dan “pasar” bergantung pada kata “pergi” dan kata “ke” bergantung pada “pasar”.  Dengan dependency grammar makna suatu kalimat dapat lebih mudah dipahami (oleh komputer). Misalnya kalimat: “Budi mempelajari ikan dari udara”.  Kalimat ini dapat memiliki dua arti, “[mempelajari ikan] dari udara” atau “mempelajari [ikan dari udara]”. Manusia  dengan cepat dapat menentukan makna yang benar adalah yang “[mempelajari ikan] dari udara” karena manusia memiliki konteks dan pengetahuan umum (tidak masuk akal ada ikan dari udara).

Dengan dependency grammar, kalimat tersebut dapat digambarkan seperti gambar berikut. Panah menunjukan arah ketergantungan. Gambar kedua memperlihatkan struktur dalam bentuk pohon.  Dapat dilihat bahwa kata udara bergantung pada mempelajari, bukan ikan. Catatan: salah buat gambar, harusnya “ikan” bukan “burung” 🙂

dep_grammar

Hubungan dependency  juga dapat dirinci maknanya, misalnya pada gambar di atas, hubungan antara Budi-mempelajari adalah nsubj = subyek. Hubungan yang lain adalah obj= “objek”, obl = “oblique argumentdsb. Rincian relasi yang digunakan universal dependency dapat dilihat di: http://universaldependencies.org/u/dep/ .    Universal dependency adalah proyek yang menyediakan  dataset POS-Tag, dependency dan morfologi untuk banyak bahasa, termasuk Bahasa Indonesia. Dataset ini yang akan kita gunakan untuk membuat model.

Penjelasan lebih lanjut tentang dependency grammar bisa dilihat di (materi dari Stanford): https://web.stanford.edu/~jurafsky/slp3/14.pdf  dan videonya: https://youtu.be/PVShkZgXznc 

Sebelumnya saya sudah membahas mengenai dependency parsing dengan Syntaxnet. Tapi Syntaxnet ini menggunakan lib  dalam bahasa C, yang  lebih sulit untuk diinstall dan lebih sulit  dimodifikasi. Saya ingin mencoba lib lain yang lebih sederhana.

Alternatif pertama adalah parser T. Dozat  https://github.com/tdozat/Parser-v1   tetapi saya kesulitan untuk menentukan lib yang diperlukan lib parser ini. Ternyata untuk proyek Python penting untuk menyebutkan proyek tersebut menggunakan lib apa saja. Banyak nama lib yang sama (tidak ada namaspace seperti package di Java), atau mungkin karena saya tidak paham saja.

Kemudian saya menemukan lib lain, BIST parser: https://github.com/elikip/bist-parser yang menggunakan Dynet dengan teknik transition based dan biLSTM. Sempat lihat-lihat Dynet, menarik juga, pendekatannya berbeda dengan Tensorflow dan sepertinya lebih mudah dipahami.  Ada beberapa code di BIST parser yang perlu disesuaikan karena masih menggunakan Dynet versi lama.

Terakhir saya lihat lib UUParser (https://github.com/UppsalaNLP/uuparser) yang merupakan fork dari BIST-parser. Codenya lebih baru dan terlihat ada usaha merapikan code BIST-parser. Jadi saya memilih lib ini. (lib ini menggunakan Python 2.7)

Pertama siapkan data training dari Universal Dependencies (http://universaldependencies.org/), pilih dan download dataset Bahasa Indonesia. Pilih dataset GSD yang memiliki data train, dev dan test.

Berdasarkan data tersebut, lakukan training sesuai petunjuk yang ada di https://github.com/UppsalaNLP/uuparser. Tanpa modifikasi parameter, hasilnya sudah lebih baik dari Syntaxnet. Nilai UAS dan LAS masing-masing 81.97 dan 75.37 (Syntaxnet: 80 dan 73). Mungkin nanti bisa dioptimasi lagi, cuma  bagi saya sudah cukup untuk sekarang.

Saya tambahkan sedikit code supaya bisa memproses input string, bukan file. Berikut codenya. Jangan lupa  tambah utils.read_conll_kalimat yang merupakan modifikasi dari utils.read_conll  (tadinya memproses file jadi memproses list teks). Isi variabel modeldir, modelfile, param sesuai hasil training.


from arc_hybrid import ArcHybridLSTM
import pickle, utils, os
# sesuaikan
modelDir="/home/yudiwbs/dataset/model/uuparser/id"
params="/home/yudiwbs/dataset/model/uuparser/id/params.pickle"
modelFile="barchybrid.model"

with open(params, 'r') as paramsfp:
    words, w2i, pos, rels, cpos, langs, stored_opt, ch = pickle.load(paramsfp)
    parser = ArcHybridLSTM(words, pos, rels, cpos, langs, w2i,
                           ch, stored_opt)
    model = os.path.join(modelDir, modelFile)
    parser.Load(model)

    kalimatStr = "Ibu pergi ke pasar"
    kalimat = [str(counter+1) + "\t" + kal for counter, kal in enumerate(kalimatStr.split())]
    kalimat.append("")

    data = utils.read_conll_kalimat(kalimat)
    pred = list(parser.Predict(data))
    for p in pred:
        print(p)

Hasilnya:

0	*root*	*root*	ROOT-CPOS	ROOT-POS	_	-1	rroot	_	_
1	Ibu	_	_	_	_	2	nsubj	_	_
2	pergi	_	_	_	_	0	root	_	_
3	ke	_	_	_	_	4	case	_	_
4	pasar	_	_	_	_	2	obl	_	_

Model Server dengan Python Multiprocess dan SocketServer

Model server adalah server yang melayani berbagai task machine learning. Saya tertarik untuk membuat ini untuk keperluan riset tapi mungkin juga bisa jadi komersial. Tidak semua perusahaan punya sumberdaya ahli NLP dan machine learning untuk membuat model yang bagus.    Menyewa konsultan juga bukan pilihan karena khawatir data dan strategi perusahaan bocor ke luar.  Web API bisa jadi alternatif yang cocok.

Beberapa alternatif lib untuk  model server adalah  TensorServing dan Clipper.  Saya lihat kok terlalu rumit untuk model sederhana yang saya gunakan, jadi saya mau coba buat sendiri. Ternyata memang susah, hampir dua minggu saya utak-atik baru berhasil hehe. Mungkin sebaiknya memang menggunakan lib tersebut. Berikut yang saya kerjakan (mungkin banyak yang tidak efisien karena saya baru belajar, silakan koreksi kalau ada alternatif lain).

Pertama karena saya ingin membuat Keras model + web service, dan saya sudah pernah mencoba flask (posting saya tentang flask),  maka saya mengikuti langkah-langkah di https://blog.keras.io/building-a-simple-keras-deep-learning-rest-api.html.   Pada posting tersebut model disimpan dalam variabel global. Ada masalah pada lib yang saya gunakan, ternyata tidak hanya model tapi kita perlu menyimpan juga graph tensorflow-nya. Berhasil saat dicoba, tapi begitu menggunakan uwsgi+Nginx kok jadi error. Ternyata variabel global yang digunakan di Flask tidak thread safe,  jadi selalu hilang. Alternatifnya adalah meload model setiap ada request, tapi tentu ini tidak feasible karena akan lambat dan menghabiskan memory.

Selanjutnya saya baca-baca tentang thread di Python, ternyata cukup lengkap, dan saya tertarik dengan multiprocessing, karena basisnya proses bukan thread harusnya  isi variabel bisa tetap tersimpan karena benar-benar terpisah. Idenya adalah kita buat multiprocess untuk membuat proses terpisah yang menyimpan model yang nanti diakses oleh web service (flask).    Bagus juga pemisahan model server dengan web server karena nanti kalau mau dibuat scalable, kita bisa lakukan dengan replikasi docker.

[Update Apr 2018:]
Cara yang lebih mudah (tanpa buat thread, tanpa queue) adalah dengan socketserver. Letakkan load model di main lalu proses klasifikasinya di handle.  Kenapa tidak terpikir dari awal ya? [endupdate]

Bagaimana menghubungkan antar proses yang memiliki model dan proses utama? saya menggunakan queue.  Ada dua queue, masing-masing untuk input dan output. Selanjutnya bagaimana menghubungkan antara  proses utama dengan web service? saya gunakan socket. Nah kesalahan saya adalah bermain di lib socketnya langsung. Ternyata susah, misalnya saya baru tahu bahwa saat kita mengirim, pihak penerima tidak tahu kapan harus berhenti dan harus dihandle manual (protocol TCP). Berhasil sih, tapi codenya jelek. Baru saat saya cari bagaimana membuat server yang bisa melayani multi client saya melihat lib socketserver  yang jauh lebih gampang digunakan hehe.

Berikut code servernya

import socketserver
from multiprocessing import Process, Queue
import os
import json

class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print("{} input:".format(self.client_address[0]))
        print(self.data)
        dataStr = self.data.decode('utf-8')
        q_input.put(dataStr)
        msg = q_output.get()  # baca output
        self.request.sendall(str.encode(msg))

def klasifikasi(q_input, q_output):

    # ========= load model disini, hanya satu kali saat method ini diload
    print("\nload model mulai.")
    # ========= load model

    # loop forever
    while True:
        msg = q_input.get()  # blocking menunggu data masuk
        print("input masuk, isinya=" + msg)
        hasil = model.klasifikasi(msg)  # proses klasifikasi atau apapun disini
        print("proses selesai, kirim ke queue output")
        q_output.put(json.dumps(hasil))

if __name__ == "__main__":
    q_input = Queue()
    q_output = Queue()
    p = Process(target=klasifikasi, args=(q_input, q_output))
    p.daemon = True
    p.start()  # mulai server 

    HOST, PORT = "localhost", 6200

    print("Server mulai...")
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
    server.serve_forever()

Selanjutnya server dapat ditest dengan command line seperti ini:

echo Golkar dan PDIP bergabung dalam pilkada 2019  | netcat localhost 6200

Atau dengan Ptyhon seperti ini, nanti tinggal dimasukkan ke dalam Flask

import socket

HOST = 'localhost'
PORT = 6200    # portnya harus sama
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Golkar dan PDIP bersatu dalam Pilkada 2019')
    data = s.recv(1024)
    print('Received', repr(data))
    s.close

Code ini belum diujicoba dengan akses bersamaan (apakah perlu ada id di dalam setiap request? bagaimana kalau pesan lebih dari 1024 byte?), jadi mungkin masih harus diperbaiki. Tapi setidaknya sudah bisa digunakan dengan flask+uswsgi+nginx.