ในยุคอินเตอร์เน็ต ยุคโซเชียลอย่างปัจจุบัน เราสามารถประยุกต์ใช้ Sentiment Analysis ได้อย่างหลากหลาย ไม่ว่าจะเป็นธุรกิจโรงหนัง วิเคราะห์ความรู้สึกลูกค้าหลังจากที่ดูหนัง, ภาคการตลาดวิเคราะห์ฟีดแบ็คของแคมเปญ, ภาคการเมืองใช้ในการวิเคราะห์ ความนิยม คะแนนเสียง, ภาคการเงินวิเคราะห์ข่าวธุรกิจสำหรับวางแผนลงทุน ไปจนถึง การแพทย์ วิเคราะห์ความรู้สึกผู้ป่วย
Sentiment Analysis คืออะไร
Sentiment Analysis คือ การวิเคราะห์ความรู้สึก วิเคราะห์อารมณ์จากข้อความ ไม่ว่าจะเป็นรีวิวหนัง รีวิวร้านอาหาร โพสเฟสบุ๊ค ทวิตเตอร์ แชท ว่าเป็นแง่บวก หรือแง่ลบ เป็นสาขาหนึ่งใน Natural Language Processing (NLP)

ในเคสนี้เราจะใช้วิธี ULMFiT ย่อมาจาก Universal Language Model Fine-tuning for Text Classification เป็นการนำ Transfer Learning ที่ใช้กันอย่างแพร่หลายใน Computer Vision มาประยุกต์ใช้กับ NLP ช่วยให้เทรนได้เร็วขึ้น ประสิทธิภาพดีขึ้น

และที่สำคัญคือใช้ข้อมูลในการเทรนน้อยกว่า

Language Model คืออะไร
Language Model คือ โมเดลภาษา การเข้าใจภาษา บางทีต้องเข้าใจโลกของภาษานั้น ๆ ด้วย ตัวอย่างคือ สามารถเดาคำในช่องว่างได้ เช่น หมาตัวนี้สี…, แมนยูกำลังให้ความสนใจดาวยิงชาวสเปนอย่าง…, President Obama fought unsuccessfully to restrict gun …
เรามาเริ่มกันเลยดีกว่า
การเทรนโมเดลในงาน Sentiment Analysis ซึ่งเป็นสาขาหนึ่งของ Natural Language Processing (NLP) ของเรานี้ เราจะใช้อัลกอริทึ่มที่เรียกว่า ULMFiT ย่อมาจาก Universal Language Model Fine-tuning เป็นการนำ Transfer Learning ที่ใช้กันอย่างแพร่หลายใน Computer Vision มาประยุกต์ใช้กับ NLP ช่วยให้เทรนได้เร็วขึ้น ได้ผลแม่นยำมากขึ้น มีขึ้นตอนดังนี้
สร้าง หรือดาวน์โหลด Language Model ที่เทรนกับ Corpus ขนาดใหญ่ เช่น Wikipedia
Fine-tune โมเดล Language Model นี้เข้ากับ Corpus ของเรา เช่น รีวิวหนัง
ดึง Encoder ออกจาก Language Model ที่ Fine-tune แล้ว มาใช้สร้าง Classifier
Fine-tune โมเดล Classifier ด้วยวิธีปกติ ในกรณีนี้คือ Sentiment Analysis
Language Model คือ โมเดลภาษา การเข้าใจภาษา บางทีต้องเข้าใจโลกของภาษานั้น ๆ ด้วย ตัวอย่างคือ สามารถเดาคำในช่องว่างได้ เช่น หมาตัวนี้สี..., แมนยูกำลังให้ความสนใจดาวยิงชาวสเปนอย่าง..., President Obama fought unsuccessfully to restrict gun ...
0. Magic Commands¶
%reload_ext autoreload
%autoreload 2
%matplotlib inline
1. Import Library¶
เคสนี้เราจะใช้ fastai.text ซึ่งเป็น Library ทางด้าน NLP
from fastai import *
from fastai.text import *
2. ข้อมูลตัวอย่าง¶
เราจะใช้ชุดข้อมูล Large Movie Review Dataset v1.0 จาก Andrew Maas et al. http://ai.stanford.edu/~amaas/data/sentiment/
ชุดข้อมูลนี้ประกอบด้วย
- รีวิวหนัง ที่มี Label แง่บวก/แง่ลบ สำหรับเทรน จำนวน 25,000 รีวิว
- รีวิวหนัง ที่มี Label แง่บวก/แง่ลบ สำหรับเทส จำนวน 25,000 รีวิว
- รีวิวหนัง ที่ไม่มี label จำนวน 50,000 รีวิว
หมายเหตุ
- รีวิวหนังถูกเลือกมาแล้ว ให้ชัดเจน ว่าเขียนแง่บวก หรือแง่ลบ ไม่กำกวม
- รีวิวหนังที่ไม่มี Label ไม่ใช่ไม่มีประโยชน์ เราจะใช้มาช่วยเทรน Language Model ใน ULMFiT ในขั้นตอนที่ 2
เปิดข้อมูล Sample ดูก่อน ที่จะไปดูข้อมูลจริง
path = untar_data(URLs.IMDB_SAMPLE)
path.ls()
มี CSV อยู่ไฟล์เดียว ลองเปิดดู head (5 บรรทัดแรก)
df = pd.read_csv(path/'texts.csv')
df.head()
ข้อมูลมี 3 column คือ label แง่บวกหรือลบ, text คือข้อความรีวิว และ is_valid คือ เป็น Validation Set หรือ Training Set
แต่ text ยาวมากเกินไป เวลาแสดงถูกตัดออกไป เรามาดูเฉพาะ text บรรทัดที่ 2 กัน
df['text'][1]
สร้าง Databunch แบบ TextDataBunch
เนื่องจากโมเดลไม่สามารถประมวลผลข้อความได้โดยตรง โมเดลรับได้แต่ตัวเลข เราจึงต้องแปลงข้อความความให้เป็นตัวเลขเสียก่อน ด้วย 2 ขั้นตอนดังนี้
- Tokenization คือ การนำข้อความมาตัดคำ ออกเป้นคำย่อย ๆ เรียกว่า Token
- Numericalization คือ การกำหนดค่าแต่ละ Token แทนด้วยตัวเลข
TextDataBunch จะดำเนินการทั้ง 2 ขั้นตอนให้อัตโนมัติ
databunch_languagemodel = TextDataBunch.from_csv(path, 'texts.csv')
Save databunch ที่เราแปลงไว้
databunch_languagemodel.save()
คราวหน้าจะได้ไม่ต้องแปลงใหม่ สามารถ Load ขึ้นมาได้เลย
databunch_languagemodel = load_data(path)
3. เตรียมข้อมูลตัวอย่าง¶
เรามาดูการทำงานทีละขั้นตอนกันโดยละเอียด
Tokenization¶
ขั้นตอนแรกในการประมวลผลข้อความต่าง ๆ คือ Split ประโยค ให้แบ่งเป็นหน่วยย่อย ๆ คำย่อย ๆ เรียกว่า Token ในกรณีภาษาอังกฤษ สามารถแบ่งตาม space ได้เลย แต่ก็มีกรณีพิเศษ เช่น
- Punctuation เช่น . ? , : ' ; - " ( ) etc.
- Contraction เช่น should've/should have, can't/cannot, don't/do not, I'm/I am, couldn't/could not, you've/you have, she's/she is, who's/who is, doesn't/does not, they're/they are
- ภาษาพิเศษ ที่ปนอยู่ในข้อความ เช่น HTML, Markdown, Tag พิเศษ
เรามาดูกันว่า ผลลัพธ์หลังจาก Tokenization แล้ว ข้อความจะเป็นอย่างไร
databunch_languagemodel = TextLMDataBunch.from_csv(path, 'texts.csv')
x,y = next(iter(databunch_languagemodel.train_dl))
example = x[:15 ,:12].cpu()
texts = pd.DataFrame([databunch_languagemodel.train_ds.vocab.textify(l).split(' ') for l in example])
texts
สังเกตว่า
- 's กลายเป็น 1 Token
- Contraction กลายเป็น 2 Token เช่น is n't
- Tag HTML ถูกลบไปหมด
- มี Token พิเศษที่นำหน้าด้วย xx ปรากฏขึ้นมา เช่น xxbos = Begin of sentence, xxmaj = นำด้วยตัวใหญ่, xxunk คำที่ไม่มีใน Vocabulary
ความหมายของ Token พิเศษ ดังนี้
- UNK (xxunk) is for an unknown word (one that isn't present in the current vocabulary)
- PAD (xxpad) is the token used for padding, if we need to regroup several texts of different lengths in a batch
- BOS (xxbos) represents the beginning of a text in your dataset
- FLD (xxfld) is used if you set mark_fields=True in your TokenizeProcessor to separate the different fields of texts (if your texts are loaded from several columns in a dataframe)
- TK_MAJ (xxmaj) is used to indicate the next word begins with a capital in the original text
- TK_UP (xxup) is used to indicate the next word is written in all caps in the original text
- TK_REP (xxrep) is used to indicate the next character is repeated n times in the original text (usage xxrep n {char})
- TK_WREP(xxwrep) is used to indicate the next word is repeated n times in the original text (usage xxwrep n {word})
Numericalization¶
หลังจากที่เราแปลงข้อความเป็น ลิสต์ของ Token เรียบร้อยแล้ว ขั้นตอนต่อมาเราจะแปลง Token เป็นตัวเลข โดยการสร้างตารางคำศัพท์ขึ้นมา เราจะนำ Token ทั้งหมดมาใส่ไว้ในตารางนี้ ตารางนี้อาจจะมีขนาดใหญ่เกินไป เราจึงใช้วิธีจำกัดจำนวน Vocabulary ดังนี้
- ถ้า Token ไหน ปรากฎใน Corpus น้อยกว่า 2 ครั้ง จะถูกตัดทิ้ง แล้วแทนที่ด้วย UNK (xxunk)
- Vocabulary เรียงตามที่ครั้งที่ปรากฎ มากไปน้อย ถ้าเกิน 60,000 (Default) ให้ตัดทิ้งไป
ทั้งนี้เพื่อรักษาขนาด Vocab ไม่ให้ใหญ่เกินไป เหมาะกับงาน และขนาด GPU ของเรา แต่ในการใช้งานแบบอื่น เช่น Google Translate อาจจะมีขนาด ถึงหลายล้าน Vocab x 100 ภาษา
เราสามารถดู Vocabulary ได้ว่าเลขไหน Assign ให้คำอะไร ด้วย Dictionary ชื่อ itos (int to String)
databunch_languagemodel.vocab.itos[:20]
ถ้าเราดูข้อมูลใน databunch จะเห็นแยกเป็น Token เรียบร้อย
databunch_languagemodel.train_ds[0][0].text[:100]
ภายใต้รายการ Token เหล่านี้ คือตัวเลขที่ชี้ไปยังอันดับใน Vocabulary
databunch_languagemodel.train_ds[0][0].data[:15]
Data Block¶
แทนที่เราจะใช้ TextClasDataBunch factory method เราจะเปลี่ยนมาใช้ Datablock API แทน เพื่อที่เราจะได้กำหนดค่าต่าง ๆ ได้ละเอียดยิ่งขึ้น
เช่น ข้อมูลอยู่ column ไหน, split train/validation โดยดูจาก column ไหน, label อยู่ column ไหน ... ละเอียดไปถึง การกำหนด Tokenize และ Numericalize ในกรณีภาษาต่างประเทศ
databunch_classifier = (TextList.from_csv(path, 'texts.csv', cols='text')
.split_from_df(col=2)
.label_from_df(cols=0)
.databunch())
2. ข้อมูลจริง¶
เมื่อเราเข้าใจ Concept แล้วก็มาเริ่มกันเลย
เนื่องจาก Language Model ใช้ GPU ค่อนข้างมาก เราควรกำหนดค่า Batch Size ให้ไม่ใหญ่เกินไป
batchsize=64
ดาวน์โหลด IMDB Dataset ตัวเต็ม
path = untar_data(URLs.IMDB)
path.ls()
ls ดูใน Folder train จะแบ่งเป็น Folder pos (Positive) และ neg (Negative)
(path/'train').ls()
ls ดูใน pos ที่จะบรรจุไฟล์ txt ข้อความรีวิว ที่เป็นแง่บวกไว้
# (path/'train'/'pos').ls()
3. สร้างโมเดลภาษา Language Model¶
เราจะเอาข้อความรีวิวหนังทั้งหมดเท่าที่เรามี filter_by_folder จากทั้ง train, test และ unsup (Unsupervised ที่ไม่มี label) โดยยังไม่สนใจ label มาเทรนให้ Language Model ของเราเข้าใจภาษารีวิวหนังมากที่สุดเท่าที่จะเป็นไปได้
- split_by_rand_pct กำหนดให้กัน 10% ไว้เป็น Validation
- label_for_lm กำหนดให้ใช้ label สำหรับ Language Model หมายถึง ประโยคไหน label ก็คือ คำถัดไปของประโยคนั้น ไปเรื่อย ๆ
สร้าง databunch แปลง Tokenize, Numericalize แล้วเซฟไว้ คราวหน้าจะได้ไม่ต้องแปลงใหม่
databunch_languagemodel = (TextList.from_folder(path)
.filter_by_folder(include=['train', 'test', 'unsup'])
.split_by_rand_pct(0.1)
.label_for_lm()
.databunch(bs=batchsize))
databunch_languagemodel.save('04a-databunch_languagemodel.pkl')
โหลด databunch language model ที่แปลงไว้แล้วขึ้นมา
databunch_languagemodel = load_data(path, '04a-databunch_languagemodel.pkl', bs=batchsize)
ลองเรียกดูข้อมูลใน batch
databunch_languagemodel.show_batch()
สร้าง language_model_learner ด้วย AWD-LSTM เป็นโมเดลแบบ RNN, LSTM ไว้เราจะอธิบายต่อไป
AWD_LSTM เป็น Language Model ที่ได้ถูกเทรนมากับ Corpus ขนาดใหญ่ จากส่วนหนึ่งของ Wikipedia ชื่อ WikiText-103 มีขนาดใหญ่ถึง 103,227,021 Token และ 267,735 Vocab เรียบร้อยแล้ว
กำหนดค่า drop_mult (Dropout) เรื่อง Regularization ไว้เราจะอธิบายต่อไป
learner = language_model_learner(databunch_languagemodel,
AWD_LSTM, drop_mult=0.3,
callback_fns=ShowGraph).to_fp16()
4. เริ่มต้นเทรน Language Model¶
เทรนด้วยค่า Default ไป 1 Epoch
learner.fit_one_cycle(1, 1e-2, moms=(0.8,0.7))
เนื่องจาก Language Model ที่รู้ภาษาอังกฤษทั่วไปอยู่แล้ว เราเทรนกับรีวิวหนังเพียงแค่ epoch เดียวก็สามารถ เดาค่าในช่องว่างของรีวิวหนัง ได้แม่นยำถึง 30%
เซฟโมเดลเก็บไว้ก่อน
learner.save('04a-learner_language_model-1')
learner.load('04a-learner_language_model-1');
5. เทรน Language Model ต่อ¶
unfreeze layer ทุก Layer ให้สามารถเทรนได้ แล้วเทรนต่อทั้งโมเดล
ลดขนาด Batch Size / 2 แล้วปัดเศษ เนื่องจากเทรนทั้งโมเดลใช้ GPU Memory เพิ่มขึ้น
learner.unfreeze()
databunch_languagemodel.batch_size=batchsize//2
เทรนทั้งโมเดล ไปอีก 10 epoch
moms คือ Momentum ในเรื่อง SGD with Momentum Optimization จะอธิบายต่อไป
learner.fit_one_cycle(10, 1e-3, moms=(0.8,0.7))
เซฟโมเดลเก็บไว้ก่อน
learner.save('04a-learner_language_model-2')
learner.load('04a-learner_language_model-2');
ลองดูผลลัพธ์โดยการ ลองให้โมเดลสร้างประโยค 40 คำ จำนวน 2 ประโยค ที่เริ่มต้นด้วย TEXT
TEXT = "This movie is so"
N_WORDS = 40
N_SENTENCES = 2
เพื่อให้ประโยคที่ออกมาไม่ใช่ประโยคซ้ำ ๆ กัน จะมีการกำหนด temperature คือ ยิ่งมากยิ่งเพิ่มความ Random หลากหลายของประโยค
print("\n".join(learner.predict(TEXT, N_WORDS, temperature=0.75) for _ in range(N_SENTENCES)))
จะเห็นได้ว่าเหมือนรีวิวหนังจริง ๆ
TEXT = "The only complaint about this film is"
N_WORDS = 40
N_SENTENCES = 2
เพื่อให้ประโยคที่ออกมาไม่ใช่ประโยคซ้ำ ๆ กัน จะมีการกำหนด temperature คือ ยิ่งมากยิ่งเพิ่มความ Random หลากหลายของประโยค
print("\n".join(learner.predict(TEXT, N_WORDS, temperature=0.75) for _ in range(N_SENTENCES)))
เซฟ Encoder ไว้ใช้สร้าง โมเดล Classifier ต่อไป
ภายใน Language Model จะแบ่งเป็นครึ่ง Encoder และ Decoder ในกรณีนี้เราจะใช้แค่ Encoder เท่านั้น ไว้เราจะอธิบายต่อไป
learner.save_encoder('04a-encoder_language_model')
6. สร้างโมเดล Classifier¶
ต่อมาเราจะสร้างโมเดลที่ใช้จำแนก ว่ารีวิวนี้ เป็นแง่บวก หรือแง่ลบ
โหลดข้อมูลรีวิวตัวเต็ม ขึ้นมาใหม่
path = untar_data(URLs.IMDB)
สร้าง databunch จากรีวิวตัวเต็ม
- ให้เอารีวิวทุกไฟล์ from_folder จากในโฟล์เดอร์ path
- กำหนดให้โมเดล ใช้ vocab เดียวกับ Language Model ที่เราเทรนด้านบน
- split_by_folder ให้ใช้ข้อมูลจากโฟล์เดอร์ test เป็น Validation Set
- label_from_folder แยก label จากโฟล์เดอร์ neg, pos เหมือน imagenet เคสนี้มี 2 class
- bs Batch Size
แปลงข้อมูล Tokenize, Numericalize ให้เรียบร้อย แล้วเซฟไว้ก่อน
databunch_classifier = (TextList.from_folder(path, vocab=databunch_languagemodel.vocab)
.split_by_folder(valid='test')
.label_from_folder(classes=['neg', 'pos'])
.databunch(bs=batchsize))
databunch_classifier.save('04a-databunch_classifier.pkl')
databunch_classifier = load_data(path, '04a-databunch_classifier.pkl', bs=batchsize)
ลองดูข้อมูล
databunch_classifier.show_batch()
7. เริ่มต้นเทรน Classifier¶
สร้าง learner ด้วย AWD_LSTM โดยโหลด Encoder ที่เราเทรน และเซฟไว้ด้านบน
learner = text_classifier_learner(databunch_classifier,
AWD_LSTM, drop_mult=0.5,
callback_fns=ShowGraph).to_fp16()
learner.load_encoder('04a-encoder_language_model')
fit ด้วย Learning Rate สูง เพราะเทรนแค่ Layer สุดท้าย
learner.fit_one_cycle(1, 2e-2, moms=(0.8,0.7))
เซฟโมเดลไว้ก่อน
learner.save('04a-learner_text_classifier-1')
learner.load('04a-learner_text_classifier-1');
8. เทรน Classifier ต่อ¶
unfreeze layer สุดท้าย และรองสุดท้าย ลดขนาด Batch Size
learner.freeze_to(-2)
databunch_classifier.batch_size=batchsize//2
ลด Learning Rate แล้วเทรนต่อ
lrs = slice(1e-2/(2.6**4),1e-2)
learner.fit_one_cycle(1, lrs, moms=(0.8,0.7))
เซฟโมเดลไว้ก่อน
learner.save('04a-learner_text_classifier-2')
learner.load('04a-learner_text_classifier-2');
unfreeze 3 layer สุดท้าย
learner.freeze_to(-3)
ลด Learning Rate แล้วเทรนต่อ
learner.fit_one_cycle(1, slice(5e-3/(2.6**4),5e-3), moms=(0.8,0.7))
เซฟโมเดลไว้ก่อน
learner.save('04a-learner_text_classifier-3')
learner.load('04a-learner_text_classifier-3');
unfreeze ทุก layer
learner.unfreeze()
ลด Learning Rate แล้วเทรนต่ออีก
learner.fit_one_cycle(2, slice(1e-3/(2.6**4),1e-3), moms=(0.8,0.7))
สำเร็จแล้ว¶
12 hours 95%
เซฟโมเดลไว้ก่อน
learner.save('04a-learner_text_classifier-4')
learner.load('04a-learner_text_classifier-4');
9. ดูผลลัพธ์¶
learner.predict("A movie which is so beautifully portrayed and is so hopeful that it won't let you take your eyes off it.")
learner.predict("This movie is a complete mess. Everything about this movie sucks.")
10. สรุป¶
- การเทรนโมเดลหลัก ๆ จะคล้าย ๆ กัน แต่ Language Model ค่อนข้างใช้เวลาในการเทรนนาน ขนาดแค่เราเทรนกับรีวิวหนังอย่างเดียว ใช้เวลาก็ใช้เวลาไปถึง 12 ชั่วโมง แนะนำให้เทรนแล้วเซฟไว้เป็นสเต็ป เพื่อใช้ในคราวต่อ ๆ ไป ไม่ต้องเริ่มเทรนใหม่ทุกครั้ง
- เราสามารถใช้วิธี ULMFiT นำ Transfer Learning มาช่วยลดเวลาในสร้างโมเดล NLP และยังช่วยให้โมเดลมีความแม่นยำมากขึ้นด้วย
- ในยุคโซเชียล ที่เราหาข้อมูลแบบ Unstructure แบบนี้ได้จากอินเตอร์เน็ตแบบไม่จำกัด เราสามารถนำโมเดลแบบนี้ไปประยุกต์ใช้ในงานได้อย่างหลากหลาย
Credit¶
- FastAI: Practical Deep Learning for Coders, v3 - Lesson 3
- Andrew L. Maas, Raymond E. Daly, Peter T. Pham, Dan Huang, Andrew Y. Ng, and Christopher Potts. (2011). Learning Word Vectors for Sentiment Analysis.) The 49th Annual Meeting of the Association for Computational Linguistics (ACL 2011).
- Regularizing and Optimizing LSTM Language Models
- Stephen Merity, Caiming Xiong, James Bradbury, and Richard Socher. 2016. Pointer Sentinel Mixture Models
Credit
- https://www.imdb.com/title/tt4154796/
- http://nlp.fast.ai/classification/2018/05/15/introducting-ulmfit.html
- https://arxiv.org/abs/1801.06146