สวัสดีทุกท่านครับ วันนี้ผมจะมาเล่าเรื่องราวก่อนที่ 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]โดยหลักการ ผมจะเพิ่ม tag [word] เข้าไปใส่ทุกคำที่ไม่อยู่ภายใต้ tag ที่เป็นประเภทนิพจน์ แล้วเราจะเอาทุก tag ไปตัดคำทีละ tag ภายใน แล้วรวมเป็น list เดียวกันที่พร้อมแปลงเป็น CoNLL2002 ภายหลัง
ผมเรียนอยู่ที่[LOCATION]มหาวิทยาลัยขอนแก่น วิทยาเขตหนองคาย[/LOCATION]
ผมเป็นนักศึกษา[ORGANIZATION]คณะวิทยาศาสตร์ประยุกต์และวิศวกรรมศาสตร์[/ORGANIZATION] [ORGANIZATION]มหาวิทยาลัยขอนแก่น วิทยาเขตหนองคาย[/ORGANIZATION]
...
ตัวอย่าง
นำประโยคเข้าไป
เราจะไปเดินเล่นที่[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/
กรณีที่ต้องการไม่ลอง postag ให้เปลี่ยน text2conll2002(text,pos=True) เป็น text2conll2002(text,pos=False) ครับ
ตอบลบหากต้องการเปลี่ยนตัวตัดคำเป็นตัวอื่น สามารถลองแก้ไขตามคำสั่ง word_tokenize(text,engine="newmm") ของ PyThaiNLP ได้เลยครับ
ตอบลบส่วนความแม่นยำจากการทดลองของผม ผมพบว่าใช้ postag แม่นยำกว่าไม่ใช้ postag ครับ
ตอบลบขอบคุณครับ พอผมว่างหน่อยจะลองใช้แน่นอนครับ
ตอบลบขอบคุณมากครับสำหรับบทความดีๆ ตอนนี้ผมกำลังสกัดคำจาก text เพื่อใช้เป็น input ของ model มาเจอบทความนี้พอดี
ตอบลบอ. ครับผมสอบถามหน่อย
ตอบลบB-LOCATION และ I-LOCATION คืออะไรครับ แล้วต่างกันยังไงครับ
B-LOCATION คือ เริ่มต้น token นี้เป็น LOCATION ส่วน I-LOCATION บอกว่า Token นี้อยู่ในระหว่างแท็ก LOCATION ครับ
ลบ