11 ธันวาคม 2561

Published ธันวาคม 11, 2561 by with 7 comments

ทำ Named Entity Recognition ภาษาไทย : เบื้องหลังการทำ NER ให้ PyThaiNLP


สวัสดีทุกท่านครับ วันนี้ผมจะมาเล่าเรื่องราวก่อนที่ Named Entity Recognition ภาษาไทยใน PyThaiNLP จะถือกำเนิดขึ้นมา มีเบื้องหลังมากมาย

เหตุผลเกิดมาจากสมัยก่อนมี PyThaiNLP เราไม่มีโมดูลหรือซอฟต์แวร์ด้านการประมวลผลภาษาธรรมชาติภาษาไทย และมีมากกว่าตัดคำให้ใช้งานได้ง่าย ๆ หรือใช้งานได้โดยไม่ต้องกังวลด้านสิทธิ์ในการใช้งาน ตอนนั้นผมทำ chatbot ด้วย NLTK อยากลองทำภาษาไทย แต่พบว่าไม่รองภาษาไทย แถมการทำแชทบอทต้องการ NER ด้วย ซึ่งในตอนนั้น NER ภาษาไทย ไม่มีคลังข้อมูลแจกจ่าย และ เป็นเรื่องที่ยังเกินความรู้ผมอยู่มาก (ม.6) NER จึงเป็นเรื่องที่ผมสนใจตั้งแต่ทำ PyThaiNLP มา

ปัจจุบันนี้ใน PyThaiNLP มี NER ที่เกิดมาจากโครงการ ThaiNER https://github.com/wannaphong/thai-ner 

บทความนี้จะเล่าเบื้องหลังและขั้นตอนการทำ Named Entity Recognition ภาษาไทย ให้กับ PyThaiNLP

Named Entity Recognition คืออะไร?

Named Entity Recognition หรือ NER คือ การสกัดนิพจน์เฉพาะหรือชื่อเฉพาะในประโยค
สมมติ เรามีประโยค "เราจะไปเดินเล่นที่หนองคาย พร้อมกับนั่งเรือข้ามไปประเทศลาว"
จากประโยคข้างบน เราจะเห็นนิพจน์เฉพาะ คือ  หนองคาย กับ ประเทศลาว สองนิพจน์นี้ควรอยู่ในหมวดหมู่ สถานที่ ดังนั้น NER ควรดักจับสองนิพจน์นี้ได้
อ่านเพิ่มเติม สรุป Survey of Named Entity Recognition and Classification (NERC) - lukkiddd

ทำ Named Entity Recognition ภาษาไทย

เราสามารถทำ Named Entity Recognition ใหม่ขึ้นมาได้ง่าย ๆ โดยใช้ sklearn-crfsuite ซึ่งเป็นโมดูลสำหรับทำ CRF model โดยเชื่อมกับ  CRFsuite อีกที โดยในเอกสารของ sklearn-crfsuite มีสอนทำ NER ด้วย สามารถทำได้โดยนำคลังข้อมูล CoNLL2002 ไป train กับโมเดล  Conditional Random Fields (CRFs) ซึ่งเป็นโมเดลยอดนิยมในฝั่ง machine learning (ในยุคแข่งขันตัดคำ ผู้ชนะในสมัยนั้นใช้ CRF model ในการตัดคำภาษาไทย)

รูปแบบ  CoNLL2002 ประกอบไปด้วย
คำ  postag  tag
ส่วนข้อมูลที่ sklearn-crfsuite ต้องการคือ [[(คำ,postag,tag),...],...] โดย 1 List ภายใน List คือ 1 ประโยค

คลังข้อมูลทำ Named Entity Recognition ภาษาไทย

ปัญหา Named Entity Recognition ภาษาไทย ตอนนั้นไม่มีคลังใดที่เป็น CoNLL2002 เลย เราจึงต้องทำคลังข้อมูลภาษาไทยที่เป็น CoNLL2002
แต่ปัญหาของเรายังไม่จบ เนื่องจากภาษาไทยต้องการตัวตัดคำ ซึ่ง ณ ปัจจุบัน การตัดคำภาษาไทยยังไม่มีข้อสรุปว่าควรตัดคำแบบใด คลังตัดคำของเนคเทคใช้การตัดคำแบบคำประสม ในขณะที่คลังอื่น ๆ ใช้หลักการตัดคำไม่เหมือนกัน แถมปัญหานี้ยังส่งผลต่อ postag ภาษาไทย ซึ่งใช้เกณฑ์การตัดคำต่างกันอีก
หากเรายึดตัวตัดคำใดตัวตัดคำหนึ่งทำคลัง NER ที่เป็น CoNLL2002 เราอาจจะสูญเสียทรัพยากรในอนาคตได้ ถ้าอนาคต ปัญหาการตัดคำ/ประโยคภาษาไทยได้ข้อสรุป หรือ ตัวตัดคำล้าสมัย
เราจึงต้องทำคลัง NER ภาษาไทยที่สามารถส่งออกมาเป็นรูปแบบ CoNLL2002 และไม่ขึ้นกับตัวตัดคำภาษาไทยและไม่ขึ้นกับ postag หากอนาคตต้องการเปลี่ยนตัวตัดคำ

โจทย์ของเรา
- ทำคลัง NER ภาษาไทย โดยสามารถเปลี่ยนตัวตัดคำและ postag ในอนาคตได้
- สามารถส่งออกข้อมูลออกมาในรูปแบบ CoNLL2002 ได้

ต่อมาเราออกแบบคลังของเราว่าควรมี tag ประเภทนิพจน์อะไรบ้าง
- LOCATION สถานที่/ที่ตั้ง ตำแหน่ง
- ORGANIZATION องค์กร/บริษัท/หน่วยงาน

เนื่องจากผมชื่นชอบ bbcode เป็นการส่วนตัว ผมจึงต้องการออกแบบคลังให้สามารถใช้ [แท็ก] เหมือน bbcode และส่งออกมาเป็น CoNLL2002 ได้
ผมจึงลงมือสร้างคลังข้อมูลตัวอย่างขึ้นมา
เราจะไปเดินเล่นที่[LOCATION]หนองคาย[/LOCATION] พร้อมกับนั่งเรือข้ามไป[LOCATION]ประเทศลาว[/LOCATION]
ผมเรียนอยู่ที่[LOCATION]มหาวิทยาลัยขอนแก่น วิทยาเขตหนองคาย[/LOCATION]
ผมเป็นนักศึกษา[ORGANIZATION]คณะวิทยาศาสตร์ประยุกต์และวิศวกรรมศาสตร์[/ORGANIZATION] [ORGANIZATION]มหาวิทยาลัยขอนแก่น วิทยาเขตหนองคาย[/ORGANIZATION]
...
โดยหลักการ ผมจะเพิ่ม tag [word] เข้าไปใส่ทุกคำที่ไม่อยู่ภายใต้ tag ที่เป็นประเภทนิพจน์ แล้วเราจะเอาทุก tag ไปตัดคำทีละ tag ภายใน แล้วรวมเป็น list เดียวกันที่พร้อมแปลงเป็น CoNLL2002 ภายหลัง
ตัวอย่าง
นำประโยคเข้าไป
เราจะไปเดินเล่นที่[LOCATION]หนองคาย[/LOCATION] พร้อมกับนั่งเรือข้ามไป[LOCATION]ประเทศลาว[/LOCATION]
ขั้นตอนที่ 1 เติม [word] เข้าไป
[word]เราจะไปเดินเล่นที่[/word][LOCATION]หนองคาย[/LOCATION][word] พร้อมกับนั่งเรือข้ามไป[/word][LOCATION]ประเทศลาว[/LOCATION]
ขั้นตอนที่ 2 ผ่านตัวคำตัด
[word]เรา จะ ไป เดินเล่น ที่[/word][LOCATION]หนองคาย[/LOCATION][word] พร้อม กับ นั่งเรือ ข้าม ไป[/word][LOCATION]ประเทศ ลาว[/LOCATION]
ขั้นตอนที่ 3 ให้ตัวที่อยู่ใน tag อื่นที่ไม่ใช้ [word] แปลงให้เป็น B-tag และ I-tag ส่วน [word] ให้เป็น O แล้วบรรจุลง List
[('เรา', 'O'), ('จะ', 'O'), ('ไป', 'O'), ('เดินเล่น', 'O'), ('ที่', 'O'), ('หนองคาย', 'B-LOCATION'), (' ', 'O'), ('พร้อมกับ', 'O'), ('นั่ง', 'O'), ('เรือ', 'O'), ('ข้าม', 'O'), ('ไป', 'O'), ('ประเทศ', 'B-LOCATION'), ('ลาว', 'I-LOCATION')]
ขั้นตอนที่ 4 แล้วเอาไปผ่าน postag
[('เรา', 'PPRS', 'O'), ('จะ', 'XVBM', 'O'), ('ไป', 'VACT', 'O'), ('เดินเล่น', 'NCMN', 'O'), ('ที่', 'PREL', 'O'), ('หนองคาย', 'NCMN', 'B-LOCATION'), (' ', 'NCMN', 'O'), ('พร้อมกับ', 'JCRG', 'O'), ('นั่ง', 'NCMN', 'O'), ('เรือ', 'NCMN', 'O'), ('ข้าม', 'VACT', 'O'), ('ไป', 'XVAE', 'O'), ('ประเทศ', 'NCMN', 'B-LOCATION'), ('ลาว', 'NPRP', 'I-LOCATION')]
เราจะได้ข้อมูลที่พร้อมเอาไป train กับ sklearn-crfsuite

เขียนโค้ดแปลงคลังให้อยู่ในรูปแบบที่ sklearn-crfsuite รองรับ

เราจะต้องเขียนโค้ดเพื่อให้คลังของเราสามารถนำไป train กับ sklearn-crfsuite ได้
ก่อนอื่นให้ทำการติดตั้ง sklearn-crfsuite กับ pythainlp
pip install sklearn-crfsuite pythainlp
แล้วเขียนโค้ดตามนี้

เมื่อรันจะได้ข้อมูลที่พร้อมเอาไป train กับ sklearn-crfsuite ตาม Let’s use CoNLL 2002 data to build a NER system

Train Named Entity Recognition ด้วย CRF Model

โดยตัวแปร datatofile เก็บข้อมูลทั้งหมด เราต้องการแบ่ง 10% เพื่อวัดประสิทธิภาพโมเดลของเราจะต้องใช้ train_test_split ของ sklearn.model_selection เข้ามาช่วย แล้ว train ด้วย sklearn-crfsuite เสร็จแล้วหาค่า f1 โดยใช้ชุดข้อมูลทดสอบที่เราแบ่ง 10%
เสร็จแล้วลองรัน

ตัวอย่างผลลัพธ์
                precision    recall  f1-score   support

    B-LOCATION      0.000     0.000     0.000         1
    I-LOCATION      0.000     0.000     0.000         0
B-ORGANIZATION      0.000     0.000     0.000         1
I-ORGANIZATION      0.000     0.000     0.000         0

     micro avg      0.000     0.000     0.000         2
     macro avg      0.000     0.000     0.000         2
  weighted avg      0.000     0.000     0.000         2

Text : เราจะไปเดินเล่นที่หนองคาย
[('เรา', 'PPRS', 'O'), ('จะ', 'XVBM', 'O'), ('ไป', 'VACT', 'O'), ('เดินเล่น', 'NCMN', 'O'), ('ที่', 'PREL', 'O'), ('หนองคาย', 'NCMN', 'B-LOCATION')]
ตัวอย่างนี้มีข้อมูลน้อยไปจึงไม่สามารถวัดค่าออกมาได้
โค้ดฉบับเต็ม

ยิ่งข้อมูลมาก ยิ่งแม่นยำขึ้น
ตัวอย่างโค้ดและข้อมูลที่ใช้ในบทความนี้ สามารถดูได้จาก https://gist.github.com/wannaphong/f981d4d6d40e16e044a6da2d6e6da11b

จากข้างบน ผมนำไปพัฒนาต่อจนได้ ThaiNER ซึ่งเป็น NER ภาษาไทยใน PyThaiNLP และเปิดรับประโยคจากบุคคลภายนอกส่งข้อมูลเข้ามาเพิ่มเติม และผมยังรวบรวมคลัง NER อื่น ๆ มาพัฒนาต่อยอดแล้วรวมเข้ากับ ThaiNER
 

สามารถลอง train ThaiNER ซึ่งเป็น NER ใน PyThaiNLP ได้ตามนี้ https://github.com/wannaphong/thai-ner/tree/master/model/0.4
อ่านรายละเอียด ThaiNER ได้ที่ https://github.com/wannaphong/thai-ner
สามารถใช้งาน NER ใน PyThaiNLP ได้ตามเอกสารนี้ https://thainlp.org/pythainlp/docs/1.7/api/ner.html 

Lab
ลองปรับแต่ง features ให้แม่นยำมากขึ้น และ ลองสร้าง tag อื่น ๆ

อ่านเอกสาร sklearn-crfsuite ได้จาก https://sklearn-crfsuite.readthedocs.io/en/latest/index.html

พูดคุยเกี่ยวกับ Thai Natural Language Processing ได้ที่ https://www.facebook.com/groups/thainlp/

7 ความคิดเห็น:

  1. กรณีที่ต้องการไม่ลอง postag ให้เปลี่ยน text2conll2002(text,pos=True) เป็น text2conll2002(text,pos=False) ครับ

    ตอบลบ
  2. หากต้องการเปลี่ยนตัวตัดคำเป็นตัวอื่น สามารถลองแก้ไขตามคำสั่ง word_tokenize(text,engine="newmm") ของ PyThaiNLP ได้เลยครับ

    ตอบลบ
  3. ส่วนความแม่นยำจากการทดลองของผม ผมพบว่าใช้ postag แม่นยำกว่าไม่ใช้ postag ครับ

    ตอบลบ
  4. ขอบคุณครับ พอผมว่างหน่อยจะลองใช้แน่นอนครับ

    ตอบลบ
  5. ขอบคุณมากครับสำหรับบทความดีๆ ตอนนี้ผมกำลังสกัดคำจาก text เพื่อใช้เป็น input ของ model มาเจอบทความนี้พอดี

    ตอบลบ
  6. อ. ครับผมสอบถามหน่อย
    B-LOCATION และ I-LOCATION คืออะไรครับ แล้วต่างกันยังไงครับ

    ตอบลบ
    คำตอบ
    1. B-LOCATION คือ เริ่มต้น token นี้เป็น LOCATION ส่วน I-LOCATION บอกว่า Token นี้อยู่ในระหว่างแท็ก LOCATION ครับ

      ลบ

แสดงความคิดเห็นได้ครับ :)