🤖สร้าง Line chatbot จำแนก intent จากข้อความ ไม่ต้องพึ่ง Dialogflow

Photo by LJ from Pexels

วันนี้เราจะมาสร้าง Line chatbot เพื่อแยก intent จากข้อความ โดยไม่ใช้ Dialogflow … เราจะสร้าง api สำหรับจำแนก intent ขึ้นมาเอง ด้วย machine learning model ที่ชื่อว่า “Multi-lingual Universal Sentence Encoder” หรือ USE ซึ่งเป็น model ของ Google ที่แจกให้ download ได้จาก tensorflowhub

model ดังกล่าว เป็น machine learning model ทางด้านภาษา หรือ NLP (Natural Language Processing) ที่ถูก train มาให้เข้าใจภาษาหลายๆ ภาษา พร้อมๆกันถึง 16 ภาษา โชคดีที่ภาษาไทยก็เป็นหนึ่งในภาษาที่ model ดังกล่าวถูก train มา ทำให้เราสามารถนำ model นี้มาใช้งานกับภาษาไทยได้

เนื่องจากมีโอกาสได้ลงเรียนวิชา Natural Language Understanding and Systems (NLP III) ที่ภาควิชา อักษรศาสตร์​ จุฬาฯ สอนโดย อาจารย์ อรรถพล ธำรงรัตนฤทธิ์ เมื่อต้นปี 2020 จึงได้ทำโปรเจคดังกล่าวในวิชานี้ สามารถติดตามวิชาอื่นๆ เกี่ยวกับด้าน NLP ที่อาจารย์เปิดสอนได้ที่

blog นี้ เราจะไม่กล่าวถึงรายละเอียดลึกๆ เกี่ยวกับการ train model ดังกล่าว แต่จะอธิบายว่า model นี้สามารถนำมาใช้ทำจำแนก intent จาก ข้อความได้อย่างไร

  • เราสามารถใช้ model สกัด feature จากประโยค หรือ แปลงประโยคให้เป็นตัวเลขในรูปแบบของ feature vector ได้ โดยสิ่งที่ model ได้เรียนรู้มาก็คือ ประโยคที่มีความใกล้เคียงกัน ถึงแม้จะเขียนไม่เหมือนกัน หรือ paraphrase มา จะมีหน้าตาของ feature vector ที่สกัดออกมาจาก model คล้ายๆ กัน ยกตัวอย่างเช่น feature vector ของประโยค “มีกระเป๋าชาแนลขายไหม?” กับ “สนใจกระเป๋าหลุยส์” ควรจะคล้ายๆ กัน แต่ไม่คล้ายกับ feature vector ของ “บริการช้า ห่วยแตก” หรือถ้าเรานำ feature vector ของทั้งสามประโยค มา plot ในแกนของ feature space (ตัวเลขแต่ละตำแหน่งของ feature vector เป็นตัวบอกขนาดของแต่ละ feature space เช่น vector = [1,2,3] หมายความว่า ขนาดใน feature space 1 =1 , ขนาดใน feature space 2=2, และ ขนาดใน feature space 3=3 ตามลำดับ) โดยเราสามารถหาความคล้ายกันของ feature vector ได้ ด้วย metric ง่ายๆ อย่าง cosine similarity หรือ normalised dot product ใน vector
  • เราสามารถแยก intent จากข้อความได้โดยใช้ model สกัด feature vector จาก ประโยคตัวอย่าง ในแต่ละ intent ที่ต้องการ เก็บไว้ และ ใช้ model สกัด feature vector จากข้อความที่ผู้ใช้งาน คุยกับ chatbot เพื่อ เปรียบเทียบว่าใกล้เคียงกับ feature vector ของประโยคตัวอย่าง ของ intent ไหนมากที่สุด
  • นอกเหนือจากนี้ USE เข้าใจภาษากว่า 16 ภาษา ดังนั้นหน้าตาของ feature vector จากประโยค “How’re you doing mate?” ก็จะคล้ายๆ กับ feature vector จากประโยค “เห้ยสบายดีไหมเพื่อน” มากกว่า ประโยค “ฉันหิวข้าวหว่ะ” ดังนั้นเราสามารถใช้ USE เพื่อหาความคล้ายกันของประโยคข้ามภาษาได้อีกด้วย

📌ภาพรวมของระบบ chatbot

ระบบ chatbot จะประกอบไป 2 api หลักๆ ได้แก่

chatbot API

ทำหน้าที่รับข้อความจาก Line, และ เป็น CRUD api (create/read/update/delete) สำหรับ ข้อมูลที่ร้านค้าจะเก็บใน database ได้แก่

  1. intents — intent ต่างๆ ที่นิยามโดยร้านค้า เช่น chatbot ร้านขายเสื้อผ้า อาจจะมี intent สอบถาม/สนใจสินค้า, สั่งสินค้า, ติดตาม order, หรือ ไม่พอใจ/complaint เป็นต้น
  2. phrases — ตัวอย่างประโยคของแต่ละ intent เช่น “สนใจเครื่องสำอางค์ Laneige” เป็น phrase ของ intent สอบถาม/สนใจสินค้า
  3. responses — ประโยคสำหรับใช้ตอบเมื่อได้รับ intent ต่างๆ เช่น “สวัสดีค่ะ ไม่ทราบว่าสนใจเป็นรุ่นไหนค่ะ” เป็น phrase ของ intent สอบถาม/สนใจสินค้า

intent classifier api

ทำหน้าที่รับข้อความจาก chatbot api และ return intent จากข้อความกลับไปให้ chatbot api เพื่อไปหยิบ response ที่ถูกต้องกับ intent ไปตอบผู้ใช้งาน

**หมายเหตุ: api ที่เราจะสร้างกัน ทั้งสอง api จะไม่รวมถึงเรื่องการทำ authentication/authorization**

📌สร้าง chatbot api

ในพาร์ทนี้เราจะมาดูในส่วนของ chatbot api คร่าวๆ ซึ่งเราจะเขียน api ด้วยflask และflask_restful ใน python และรัน api บน Heroku

หน้าตา directory ของ chatbot api

ไฟล์ application หลักของ chatbot api จะอยู่ที่ app.py เมื่อ run application ด้วย web server บน production หรือ flask บน local (i) function create_appจะถูกเรียกเป็นอันดับแรกเพื่อสร้าง application ด้วย flask, กำหนด configuration สำหรับ database, และ เชื่อมต่อ database กับ application โดยส่วนของ database จะถูกกำหนดใน db.pyโดย flask_sqlalchemy (ii) จากนั้น function create_api จึงจะถูกเรียกเพื่อสร้าง api สำหรับ application อีกทีหนึ่ง

route ทั้งหมดของ api จะถูกกำหนดไว้ใน function create_api โดยเราจะกำหนด route ต่างๆ ดังนี้

  • ‘/intent/<string:value>’ : CRUD api สำหรับ intent
  • ‘/intents’ : ใช้เรียกดู intent ทั้งหมด และ response และ phrase ในแต่ละ intent จาก database
  • ‘/phrase’ : CRUD api สำหรับ phrase
  • ‘/chat’ : ใช้ส่งข้อความหา chatbot และ ตอบกลับด้วย response ตาม intent ที่ตอบกลับมาจาก intent classifier api (ใช้ test api โดยไม่ผ่าน Line)
  • ‘/webhook’ : ใช้ส่งข้อความหา chatbot ผ่าน Line และ ตอบกลับด้วย response ตาม intent ที่ตอบกลับมาจาก intent classifier api
  • ‘/put_to_s3’ : ใช้ upload intent จาก database ไปยัง S3 สำหรับให้ intent classifier api มา download ไปใช้ ตอนเริ่ม application (ใช้การส่งไฟล์ระหว่าง api ผ่าน S3 แทน การเรียกตรงจาก chatbot api เนื่องจาก api ที่สร้างขึ้นบน Herokuมาเป็น free tier ไม่ได้ active อยู่ตลอดเวลา)
app.py

models

ในส่วนของ module models จะรวบรวม schema ของ intent , phrase , และ response ด้วยการสร้าง class ที่ inherit มาจาก db.Model ใน db.py ซึ่งถูกสร้างมาจาก flask_sqlalchemy.SQLAlchemy() อีกทีหนึ่ง

การสร้าง database schema โดยใช้ SQLAlchemy

  • เราจะกำหนดชื่อ table ผ่าน class variable __tablename__
  • กำหนด column ด้วย db.Column โดยสามารถระบุ data type โดยใช้ built-in class ของ SQLAlchemy
  • กำหนด primary key หรือ foreign key ของ table ได้
  • กำหนด relation ระหว่าง table ด้วย db.relationship

นอกเหนือจากนี้เราจะสร้าง method ต่างๆ เช่น json , save_to_db , delete_from_db เพื่อให้สามารถใช้งาน object ของ models ต่างๆ ได้ง่าย

หน้าตา intent.py ภายใน module models มีการกำหนด relationship กับ ResponseModel และ PhraseModel

models/intent.py

หน้าตา phrase.py ภายใน Module models มีการกำหนด relationship กับ IntentModel และ กำหนดให้ intent_id เป็น foreign key โดยผูกกับ column id ใน table intents โดยกำหนด intents.id ใน db.ForeignKey

models/phrase.py

resources

ส่วน module resources จะรวบรวมว่า api จะต้องทำอะไรบ้าง เมื่อถูกเรียกผ่าน route และ http method ต่างๆ เราจะแยก resources เป็นหมวดหมู่ เช่นเดียวกันกับใน module models เป็น โดยการสร้าง class ที่ inherit มาจาก flask_restful.Resource เราจะระบุ class variable model เพื่อระบุว่า resource ดังกล่าว เป็น resource ของ table ไหน ใน database และ เราจะ implement function get , post , put , หรือ delete เพื่อเป็นการกำหนดว่า api จะทำอะไร และจะตอบอะไรกลับไป เมื่อได้รับ http method ต่างๆ

หน้าตา intent.py ภายใน module resources จะประกอบไปด้วย class Intent ซึ่งทำหน้าที่เป็น CRUD api สำหรับ IntentModel , class IntentList สำหรับเรียกดู intent ทั้งหมดใน IntentModel , และ class PutToS3 สำหรับจัดการ upload intent ทั้งหมดในรูปแบบ json ไปยัง S3 สำหรับ intent classifier api ในตอน เริ่ม application

resources/intent.py

หน้าตา phrase.py ภายใน module resources… สังเกตว่าใน route ของ Phrase จะไม่มี parameter รวมอยู่ใน url เหมือนกับ Intent เนื่องจากว่าเราจะส่งข้อมูลผ่าน json แทน โดยเราสามารถกำหนด parser สำหรับ json ได้ โดยใช้ flask_restful.reqparse เพื่อเป็นการกำหนดว่า api เราต้องการ key/value อะไรบ้างใน json ที่ส่งมา สำหรับแต่ละ resource บ้าง… คล้ายๆกับ argument parser ในการทำ script ด้วย python

resources/phrase.py

ส่วนสุดท้ายของ resources ใน chatbot api จะไม่พูดถึงก็ไม่ได้เพราะเป็น หัวข้อของบทความนี้ ก็คือ chat.py และ chat_line.py

route “/chat” ที่ผูกอยู่กับ Chat ใน chat.py มีไว้เพื่อทดสอบการคุยกับ intent classifier api โดยไม่ต้องผ่าน Line เพื่อทดสอบว่า chatbot api และ intent classifier api ทำงานร่วมกันได้ถูกต้องในเบื้องต้น โดยใน function post ของ chatbot api จะไปเรียก service จาก intent classifier api อีกทีนึง โดยการส่ง json ที่มีข้อความไป และได้ response กลับมาเป็น intent_id จากนั้นจึงสุ่มเลือก response จากใน database เพื่อตอบกลับข้อความที่รับมา

resources/chat.py

สำหรับ file chat_line.py ก็จะคล้ายๆ กับ chat.py จะมีส่วนเพิ่มเติมคือ การแกะข้อความจาก json object ที่ส่งมาจาก Line และ token สำหรับตอบข้อความกลับ ก่อนที่จะใช้ LineBotApi ในการตอบข้อความกลับไปตาม intent_id ที่ได้รับมาจาก intent classifier api อีกที

โดยก่อนที่เราจะเชื่อมต่อ chatbot api ของเรากับ Line ได้ ต้องไปสร้าง channel และ chat agent ก่อน

สามารถไปดูรายละเอียดการสร้าง channel/ chat agent และ การเชื่อมต่อกับ Line messaging api ได้ที่

และ GitHub ตัวอย่างการใช้ Line messaging api สำหรับภาษา Python

resources/chat_line.py

deploy chatbot api บน Heroku และทดสอบ api

หลังจากทดสอบ chatbot api บน local เรียบร้อย เราจะ deploy api บน Heroku โดยมีไฟล์ที่เกี่ยวข้องกับการ deploy ได้แก่

  • runtime.txt ใช้กำหนดภาษา และ version ที่ใช้ เช่น สำหรับโปรเจคนี้
python-3.7.6
  • requirements.txt กำหนด dependencies ในโปรเจค
  • Procfile ใช้กำหนด web server ที่ใช้ เช่น สำหรับโปรเจคนี้ ใช้ uwsgi โดยคำสั่งดังกล่าวจะไปเรียกไป uwsgi.ini อีกที
web: uwsgi uwsgi.ini
  • uwsgi.ini เป็น script file ของ web server ซึ่งกำหนดให้รัน module = app:app หมายความว่าให้รัน application จาก app.py : ตัวแปร app
[uwsgi]http-socket = :$(PORT)master = truedie-on-term = truemodule = app:appmemory-report = true

เราจะ deploy chatbot api บน Heroku ผ่าน GitHub โดยกำหนด repo ที่จะใช้ deploy ตามภาพด้านล่าง

เราจะเพิ่ม Heroku Postgres สำหรับเป็น database ของ application ในหน้าหน้า Resources บน Heroku (ไม่ต้องสนใจ Redis To Go และ worker ในภาพ)

ขั้นตอนสุดท้ายเราจะกำหนด Config Vars ต่างๆ สำหรับเชื่อมต่อกับ database และ S3 ในขั้นตอนนี้ผู้อ่านอาจจะไม่จำเป็น upload ข้อมูลจาก database ผ่าน chatbot api ขึ้นไปบน S3 เหมือนเราก็ได้ เช่น อาจจะให้ intent classifer api มาดึงข้อมูลจาก chatbot api โดยตรง (ไม่ต้องสนใจ REDISTOGO_URL ในภาพ)

รายละเอียดการ deploy app บน Heroku อ่านได้ที่

หลังจาก deploy เรียบร้อย เราจะลองใส่ข้อมูล intent , phrase , และ response เข้าไปยัง database ผ่าน Postman

ทดลองเพิ่ม intent

ทดลองเพิ่ม intent โดยใส่ value หรือ ชื่อของ intent และ POST ไปที่ route ‘/intent/<string:value>’ จะได้ response กลับมาเป็น intent ใน format json ที่มี responses และ phrases เป็น list ว่าง

ทดลองเพิ่ม phrase

ทดลองเพิ่ม phrase โดยใส่ payload เป็น json โดยกำหนด ประโยคตัวอย่างที่ต้องการในvalue และ พร้อมกำหนด intent_id ของประโยคดังกล่าว

ทดลองเพิ่ม response

ทดลองเพิ่ม response โดยใส่ payload เป็น json โดยกำหนด ข้อความสำหรับตอบกลับที่ต้องการในvalue และ พร้อมกำหนด intent_id ของประโยคดังกล่าว

ลองเรียก intent ทั้งหมดผ่าน route ‘/intents’

ได้หน้าตา json ประมาณนี้ โดยในแต่ละ intent จะประกอบไปด้วย id, ชื่อ intent value, reponses , และ phrases

upload intent ทั้งหมดไปยัง S3

หลังจากใส่ intent ที่ต้องการ ประโยคตัวอย่าง phrase และ ประโยคสำหรับตอบ response เสร็จเรียบร้อย เราจะ upload json ของ intent ทั้งหมดไปยัง S3 สำหรับให้ intent classifier api ได้ใช้ตอน start application

📌สร้าง intent classifier api

หน้าตา directory ของ intent classifier api มีแค่ตัว application หลักapp.py และ dependencies requirements.txt เท่านั้น โดยเราจะ run service ดังกล่าวกันบน Google Compute Engine (ก่อนหน้าลอง Heroku แบบ Free-tier มาแล้ว แต่ resource ไม่พอสำหรับการดึง feature vector จากข้อความด้วย USE model และลองGoogle App Engine มาแต่เอาแอพขึ้นไม่ได้ จึงมาจบที่ Compute Engine)

วิธีการทำงานของ intent classifier api

download USE model จาก tensorflowhub

model = hub.load("https://tfhub.dev/google/universal-sentence-encoder-multilingual/3")

download file intents.json จาก S3 ด้วย function instantiate_reps_from_s3

สกัด feature vector ของแต่ละประโยคตัวอย่างphrasesใน intents.json ไว้ใน array phrase_arr

ใช้ function get_intent สำหรับรับ input เป็นข้อความ sentence และ ให้ output เป็น intent_id โดยมีขั้นตอนดังนี้

  1. ดึง feature vector จาก sentence โดยใช้ USE model ได้ feature vector เป็นsentence_vec
  2. นำ sentence_vec ไปคูณเมทริกซ์กับ phrase_arr.T หรือ transpose ของ phrase_arr เพื่อหา cosine similarity ระหว่าง sentence และประโยคตัวอย่าง phrasesใน intents.json (feature vector ที่ได้จาก USE model มีค่า norm = 1 อยู่แล้ว ไม่ต้อง normalise)
  3. ใช้ DataFrame จาก pandas เพื่อหาว่า intent_id ไหน มี cosine similarity สูงที่สุด โดยใช้ค่า max ของ score ใน phrases ในการตัดสิน
app.py

อธิบายเรื่องการหา cosine similarity ระหว่าง array ด้วยการคูณเมทริกซ์

การหา cosine similarity ระหว่าง vector (array ที่มี dimension เดียว) ที่มีขนาดเท่ากัน ก็คือ การหา dot product ระหว่างทั้งสอง vector และ scale ด้วย norm2 หรือ ขนาดของทั้งสอง vector

https://en.wikipedia.org/wiki/Cosine_similarity

การหา cosine similarity ระหว่าง vector กับ array (ที่มีหลายๆ vector อยู่ในนั้น) หรือระหว่าง array กับ array สามารถทำได้โดยวิธีการคูณเมทริกซ์ เป็น batch พร้อมกันทีเดียว แทนที่จะ for loop หาทีละคู่ ตามภาพข้างล่าง

deploy intent classifier api

เราจะ deploy intent classifier api บน Google Compute Engine โดยใช้ gunicorn และnginx เป็น production server โดยขั้นตอนหลักๆ ได้แก่

  1. สร้าง VM instance ขึ้นมาใหม่ (เราใช้ n1-standard-1 (1 vCPU, 3.75 GB memory) ราคาชั่วโมงละ $0.034)
  2. ตั้ง Firewall เพื่อเปิด port ที่ต้องการใช้สำหรับ api
  3. install python บน instance (แนะนำให้ใช้ miniconda)
  4. set up gunicorn และnginx สำหรับเป็น production server และ deploy api

ขั้นตอนทั้งหมดอ้างอิงมาจาก blog นี้

📌ทดลองคุยกับ chatbot api ผ่าน Line

ทดลองคุยกับ chatbot api หลังจาก deploy เรียบร้อยแล้ว จะเห็นว่าเราไม่ต้องใช้ภาษาที่เหมือนกับประโยคตัวอย่าง phrase หรือ พิมพ์เป็นภาษาอังกฤษ​ intent classifier ก็สามารถจับ intent ที่ถูกต้องได้เป็นส่วนใหญ่ แต่ก็มีที่ผิดพลาดเช่นกัน เช่น ถาม “มีโปรไรป่ะ” แต่ตอบ response ของ intent การชำระเงิน หรือ ถาม “มีของพร้อมส่งไหม” แต่ตอบ response ของ intent การชำระเงิน เป็นต้น

สิ่งที่อาจจะสามารถปรับปรุง intent classifier api ให้ไม่ตอบผิด ในอนาคต คือ ทดลองหาค่า threshold สำหรับ cosine simmilarity score ถ้าหากได้ค่าต่ำกว่า threshold ก็อาจจะให้ fallback ไปที่ fallback intent เช่น ตอบว่าไม่เข้าใจ ให้พิมพ์มาใหม่

📌ทำเท่านี้จบแล้วเหรอ สามารถพัฒนาอะไรได้อีกไหม?

การจะทำให้ chatbot สามารถมา automate หน้าที่ในส่วนของ admin ที่ทำหน้าที่ เช็คสินค้าให้ลูกค้าที่สอบถามเข้ามา รับออเดอร์สินค้า และคอยตอบคำถามต่างๆ ของลูกค้า นอกเหนือจากการ จำแนก intent จากข้อความแล้ว เรายังจำเป็นจะต้อง ดึง entity ต่างๆ ที่อยู่ในข้อความออกมาให้ได้ เช่น การหาสินค้าให้ลูกค้า เราอาจจะมี search api หรือ stock checking api อยู่แล้ว สิ่งที่ต้องทำคือ ดึงชื่อหรือรุ่นสินค้าออกมาจากข้อความ เพื่อไปใช้ api เหล่านั้นในการหาสินค้าให้ลูกค้าอีกที —เช่น เราต้แงการดึงชื่อสินค้า adidas ultraboost 2 ออกมาจากประโยค “ตอนนี้รองเท้า adidas ultraboost 2 ที่จัดโปรโมชัน หมดหรือยัง?” เป็นต้น

ขั้นตอนดังกล่าวในการดึง entity จากข้อความ อาจจะทำได้โดยการเขียน regex ซึ่งอาจจะมีกฏเยอะพอสมควร และคงไม่สามารถคลอบคลุมได้ 100% หรือ ใช้ machine learning ที่ train มาสำหรับการหา entity โดยเฉพาะ หรือเราเรียก task ดังกล่าวว่าname entity regcognition ก็จะทำให้ chatbot มีความสมบูรณ์มากขึ้นได้ (ข้อจำกัดคือ ต้องมี dataset ที่เจาะจงกับ use case สำหรับ train model)

Github

chatbot api:

intent classifier api:

Data scientist at Central Technology Organization — CTO, Bangkok & life long learner

Data scientist at Central Technology Organization — CTO, Bangkok & life long learner