Web App (frontend) dengan Python

Saya membantu penelitian dosen prodi lain dan karena programmernya menghilang, saya diminta untuk jadi programmer pengganti. Aplikasi yang dibuat adalah aplikasi web tetapi banyak menggunakan javascript untuk user interfacenya.

Posting ini untuk catatan pribadi  sebagai template pengembangan web app (karena saya gampang lupa). Setiap orang punya preferensi masing-masing dan perlu disesuaikan dengan kondisi yang ada. Saya pilih Python+Flask karena hanya saya programmernya dan bisa memilih tools dengan bebas 🙂  Sempat lihat-lihat framework  seperti react, vue, angular tapi saya sendiri tidak terlalu suka dengan bahasa Javascript. Saya tetap gunakan Javascript, tetapi hanya untuk user interface saja.  Ada juga framework seperti Django, tetapi menurut saya terlalu berlebihan untuk proyek kecil yang  dikerjakan satu orang.   Sebelumnya saya sudah membahas tentang Flask, tapi dengan docker bisa dipermudah.

Saya menggunakan PyCharm Profesional karena Pycharm Community tidak dapat mengedit css dan javascript (dianggap sebagai teks biasa). Untungnya ada versi pro gratis untuk dosen yang punya email .edu.

Pertama yang perlu dilakukan adalah install Flask.  Siapkan struktur direktori: /app, /app/static/img, app/static/node_modules, /app/templates. Buat program utama di: /app/main.py

Template dasarnya seperti ini:

import requests
from flask import Flask, session
from flask import render_template
from flask import request

app = Flask(__name__)
app.secret_key = "x"

@app.route("/", methods=['GET', 'POST'])
def beranda():
    return render_template('index.html')

if __name__ == "__main__":
    # Only for debugging while developing
    # operasional
    app.run(host='0.0.0.0', debug=False, port=80)

    # debug
    #app.run(host='0.0.0.0', debug=True, port=8080)

Untuk mengelola lib javascript, gunakan NPM.  Masuk ke directory app/static, gunakan npm init  lalu npm install lib yang diperlukan. Tadinya saya kira NPM itu seperti pip-nya Python, defaultnya global, ternyata lokal.

Di html, untuk memanggil javascript gunakan url_for seperti ini (ganti img dengan script, karena WordPress ternyata memblok tag script):

//ganti dengan img dengan script karena wordpress tdk mengijinkan
<img src="{{ url_for('static', filename='node_modules/file-js-nya')}}" /> 

Demikian juga dengan image dan css :

<img src="{{ url_for('static', filename='img/gambar.png')}}" />

Untuk deploy, buat Dockerfile yang sejajar dengan /app (bukan di dalam).

FROM tiangolo/uwsgi-nginx-flask:python3.6
RUN pip install requests
COPY ./app /app
EXPOSE 80

Coba dulu di lokal (jangan lupa titik saat docker build):

docker build -t nama-app .
docker run -p 80:80 nama-app

Untuk stop bisa dengan ctrl-c, atau:

docker ps
docker stop container [id-container]

Untuk deploy, push ke gitlab (di Pycharm gunakan plugin gitlab project, posting saya tentang ini).

Di server, jika pertama kali, gunakan git clone  (siapkan sebelumnya ssh key untuk gitlab). Selanjutnya untuk update gunakan git pull. Lakukan docker build dan docker run.

Untuk koneksi ke database, saya belum memutuskan apakah digabung saja dengan frontend ini, atau dipisahkan jadi web service. Tapi mungkin akan saya pisahkan untuk jaga-jaga jika mau dibuat versi mobile app-nya, sekalian lebih mendalami model web API.

 

Iklan

Chrome Remote Desktop di Ubuntu 16

Ini berawal dari account TeamViewer saya yang diblokir karena dianggap untuk komersial. Sebenarnya untuk riset, tapi memang saya gunakan untuk mengendalikan sampai 4 PC, jadi ya wajar juga jika dicurigai 🙂

Beberapa alternatif opensource sulit untuk digunakan karena PC harus bisa diakses langsung atau harus memodifikasi router, padahal PC di kampus ditutup akses portnya.  Saya lihat Chrome Remote Dekstop (CRD) jadi alternatif yang menarik. Untuk me-remote Windows dengan CRD  lancar.  Masalah muncul saat saya gunakan pada Ubuntu 16. Layar memang muncul tapi hanya wallpaper saja.  Terminal masih bisa diakses melalui menu klik kanan. Tapi semua window tidak bisa digeser dan diresize! Ternyata berbeda dengan TeamViewer yang prinsipnya screen mirroring, di Ubuntu, CRD menggunakan virtual session. Dari yang saya baca, ada bug untuk session Unity yang diakses secara remote karena masalah 3D accelerator(?).

Googling, dapat solusi dari https://productforums.google.com/forum/#!msg/chrome/LJgIh-IJ9Lk/tj-SBgEd85wJ   Prinsipnya CRD dimodifikasi agar tidak membuat session baru, tetapi menggunakan session yang sudah ada. Solusi ini berhasil, dan saat dijalankan, maka layar host jadi gelap saat PC diakses remote.   Masalahnya, saat koneksi terputus, maka layar host akan tetap gelap dan reconnect pun gagal 😦  Googling tapi tidak menemukan solusi lain. Aneh ya, itu sebabnya saya buat posting blog ini.

Setelah saya lihat-lihat code di /opt/google/chrome-remote-desktop/chrome-remote-desktop (jangan lupa backup), di Ubuntu, desktop display manager yang digunakan adalah  /usr/sbin/lightdm-session. Secara default Ubuntu 16 menjalankan gnome-session –session=ubuntu dan ini yang menjadi sumber masalah. Sebenarnya ada code yang akan memilih gnome-session –session=ubuntu-2d jika tersedia di OS untuk menghindari bug akibat 3D accelerator. Masalahnya di Ubuntu 16, sudah tidak ada lagi gnome ubuntu-2d (hanya ada di Ubuntu 12). Saat saya coba install, sudah tidak ada dan tidak disupport.  Solusinya saya install gnome-session-flashback:

sudo apt-get update
sudo apt-get install gnome-session-flashback

Stop dulu service chrome desktop:

/opt/google/chrome-remote-desktop/chrome-remote-desktop --stop

Lalu  sudo edit (jangan lupa  backup): /opt/google/chrome-remote-desktop/chrome-remote-desktop . Saya akan update bagian yang tadinya menggunakan ubuntu-2D. Edit  bagian ini  (cari dengan kata kunci “2d”):

  SESSION_WRAPPERS = [
    "/usr/sbin/lightdm-session",
    "/etc/gdm/Xsession",
    "/etc/X11/Xsession" ]
  for session_wrapper in SESSION_WRAPPERS:
    if os.path.exists(session_wrapper):
      #yw edit
      #if os.path.exists("/usr/bin/unity-2d-panel"):
        # On Ubuntu 12.04, the default session relies on 3D-accelerated
        # hardware. Trying to run this with a virtual X display produces
        # weird results on some systems (for example, upside-down and
        # corrupt displays).  So if the ubuntu-2d session is available,
        # choose it explicitly.
        return [session_wrapper, "/usr/lib/gnome-flashback/gnome-flashback-metacity"]
      #else:
        # Use the session wrapper by itself, and let the system choose a
        # session.
        #return [session_wrapper]
  return None

Intinya code akan menjalankan /usr/lib/gnome-flashback/gnome-flashback-metacity bukan gnome-session –session=ubuntu . Gunakan gnome metacity, bukan compiz karena metacity tidak menggunakan 3D accelerator.

Lalu dibagian lain edit juga:

x_session = choose_x_session()
#  yw ganti
if (len(x_session) == 2 and
   x_session[1] == "/usr/lib/gnome-flashback/gnome-flashback-metacity"):
       extra_x_args.extend(["-extension", "Composite"])

Katanya ini untuk mengatasi bug di Ubuntu 2D metacity, jadi saya juga tambahkan untuk gnome-flashback-metacity ini (entah valid atau tidak hehe).

restart ulang:

 /opt/google/chrome-remote-desktop/chrome-remote-desktop --start

Coba jalankan secara remote dan semua bisa berjalan dengan baik, walaupun user interfacenya agak berbeda dengan Ubuntu standard 🙂 CRD bahkan lebih cepat daripada TeamViewer.

Catatan: jangan lupa catat pin, Google Remote Desktop tetap membutuhkan pin walaupun kita sudah login.

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.

 

Eksplorasi Front-End

Setelah membuat model untuk task named entity, membuat model server, membuat web service maka selanjutnya adalah membuat web untuk mendemokan API yang dibuat.

Saya terakhir mengurusi front-end hampir 10 thn yang lalu untuk penelitian bersama pembimbing S2 saya. Waktu itu saya jadi belajar CSS dan Javascript. Saat lihat-lihat kondisi sekarang, wah sudah jauh berbeda. Ada berbagai macam framework: React, Angular, Vue.

Setelah saya eksplorasi, ternyata jauh berbeda dengan pendekatan lama.  Perlu belajar banyak. Masalahnya, framework juga bergeser dengan cepat. Bahkan untuk framework yang sama bisa mengalami perubahan besar (seperti Angular).  Kombinasi yang buruk terutama bagi saya yang punya waktu terbatas: perlu sumberdaya besar untuk belajar + gampang kadaluarsa ilmunya.

Akhirnya saya buat dengan cara lama saja, Python+Flask+Jinja2 +JQueryUI dan Bootstrap. Mungkin kalau benar-benar perlu serius, kami akan sewa orang lain saja 🙂

 

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 🙂

Contoh hasilnya: (silakan ganti isi parameter kalimat)

http://nlp.yuliadi.pro:5000/ner?kalimat=Jokowi%20dan%20Jusuf%20Kalla%20pergi%20ke%20Jakarta

Langkah berikutnya adalah membuat frontend untuk demo API.

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.