{"cells":[{"metadata":{"_uuid":"01f361ddc47e0b386595316fe3d7f4dabbd260db"},"cell_type":"markdown","source":"# เกริ่นนำ\n\nสวัสดีครับเพื่อนๆ Notebook นี้เป็นอีกหนึ่ง Workshop ของ www.thaikeras.com ที่มีจุดประสงค์เพื่อให้เรียนรู้วิธีการเขียนโปรแกรม Text Classification เบื้องต้นด้วย Keras นะครับ โดยนำโจทย์มาจาก\nโดยโจทย์ของ Dataset Quora Insincere Classification ที่ต้องการคัดกรองกระทู้ที่มีเจตนาไม่ดี (ว่าง่ายๆก็คือ กระทู้ที่ทำให้คนทะเลาะกัน)  เพื่อความรวบรัดใน Notebook นี้จะขอเรียกกระทู้ที่มีเจตนาไม่ดีเหล่านี้ ย่อๆ ว่า **กระทู้ไม่ดี** ครับ\n\nโดยผมมีกระทู้ที่อธิบายถึงปัญหานี้อย่างละเอียดไว้ และเพื่อนๆ สามารถดูเพิ่มเติม พูดคุยเกี่ยวกับ Notebook ได้ที่ [community ของเราครับ](https://thaikeras.com/community/%E0%B9%80%E0%B8%81%E0%B8%B5%E0%B9%88%E0%B8%A2%E0%B8%A7%E0%B8%81%E0%B8%B1%E0%B8%9A-kaggle/)\n\nถ้าไม่เข้าใจส่วนไหน ถามในเว็บบอร์ด หรือข้างล่าง Notebook นี้ พวกเรายินดีช่วยเหลือให้คำแนะนำทุกประเด็นครับผม\n\nอนึ่ง Notebook นี้เป็นฉบับปรับปรุงและเพิ่มเติมจาก Notebook ที่มีชื่อว่า https://www.kaggle.com/sudalairajkumar/a-look-at-different-embeddings\nซึ่งเขียนโดยคุณ SRK ที่เป็น Kernel Grandmaster ดังนั้นเพื่อนๆ จะได้เรียนรู้จากอาจารย์ที่ดีที่สุดแน่นอนครับ ขอบคุณ คุณ SRK ไว้ ณ โอกาสนี้ด้วย แม้ Notebook นี้จะไม่ได้เป็น Notebook ที่ทำคะแนนได้สูง\nใน Leaderboard ทว่าเท่าที่คุยกับเพื่อนๆ ที่ร่วมแข่งมาด้วยกัน ผู้เข้าแข่งขันส่วนใหญ่ต่างก็ใช้ Notebook นี้เป็นจุดเริ่มต้นและปรับปรุงต่อจนกระทั่งได้รับเหรียญรางวัลครับ\n\nNotebook จะใช้เวลาทำงานทั้งหมดตั้งแต่ต้นจนจบราวๆ 20นาทีกว่าๆ ครับ\n\n## สิ่งที่จะได้เรียนจาก Workshop นี้\n\n- เรียนรู้การเขียนโปรแกรม Text Classification ด้วย Keras โดยใช้ Sequential Model พื้นฐานเช่น LSTM / GRU และการเตรียมข้อมูลเบื้องต้นด้วย Keras Tokenizer\n\n- ได้เรียนรู้การใช้ Word Embedding ทั้งหลายซึ่งเป็นหัวใจของงานด้าน NLP ไม่ว่าจะเป็น GloVe หรือ FastText เบื้องต้น\n\n- เรียนรู้การวัดผลของ Model บนปัญหา **คลาสไม่สมดุล (imbalance class)** ซึ่งอาจนำไปสู่ความเข้าใจที่ผิด ถ้าวัดผลด้วย Accuracy metric หรือ \"ค่าความถูกต้อง\" ที่นิยมใช้ทั่วไป\n\n- เรียนรู้วิธีที่เรียกว่า **Ensemble** อย่างง่าย ที่นำ Models หลายๆ ตัว มาช่วยกัน Classify ทำให้ความแม่นยำเพิ่มขึ้นมากกว่าการใช้ Model ใด Model หนึ่งเพียงตัวเดียว\n\n### Workshops อื่นๆ โดยทีมงาน\n\n- [ทำ Project Data Science เบื้องต้นจากการวิเคราะห์และทำนายผู้รอดชีวิตจากเรือไททานิก](https://www.kaggle.com/pednoi/data-science)\n\n- [ทดลองเขียน Python และ Numpy](https://www.kaggle.com/ratthachat/python-numpy-deep-learning)\n\n## เริ่มต้น code กันเลยครับ"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"'''เริ่มต้นจากการ import libraries ทั้งหลายที่จำเป็นต้องใช้งานเข้ามานะครับ'''\n'''บาง libraries พวกเราอาจจะยังไม่เคยใช้ แต่ถ้าเห็นวิธีการใช้บ่อยๆ ก็จะคุ้นชินไปเองครับ'''\nimport os\nimport time\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nfrom tqdm import tqdm\nimport math\nfrom sklearn.model_selection import train_test_split\n\nnp.set_printoptions(suppress=True)\n\n'''Keras libraries เหล่านี้เป็นมาตรฐานที่เราแทบจะ Copy ไปใช้กับทุกปัญหาได้เลยครับ'''\n# Tokenizer ใช้จัดการตัดสตริงให้เป็นคำ และ map แต่ละคำเป็นตัวเลข ท้ายที่สุดสตริงจะเปลี่ยนเป็น Sequence of numbers\nfrom tensorflow.keras.preprocessing.text import Tokenizer\n# Pad_sequences ใช้สำหรับเติม 0 ลงไปให้ทุกสตริงสุดท้ายมีความยาวเท่ากัน\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences\nfrom tensorflow.keras.layers import *\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras import optimizers","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## ทำความรู้จักและจัดการข้อมูลเบื้องต้น"},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"# โหลดข้อมูล train/test โดยใช้ panda.read_csv ฟังก์ชันที่ใช้บ่อยที่สุดใน Data Science :)\ntrain_df = pd.read_csv(\"../input/quora-insincere-questions-classification/train.csv\")\ntest_df = pd.read_csv(\"../input/quora-insincere-questions-classification/test.csv\")\nprint(\"Train shape : \",train_df.shape)\nprint(\"Test shape : \",test_df.shape)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"9f112037577f3ccbfed19793d7e049978922ace4"},"cell_type":"markdown","source":"เรามีข้อมุล train ราว 1.3 ล้านกระทู้ และต้องทดสอบบนกระทู้ test ราวๆ 3.76 แสนกระทู้ ก็ถือว่ามีข้อมูลที่เยอะพอควรนะครับ\n\nหลังจากโหลดข้อมูล train/test data ขึ้นมาแล้ว เราลองดูเบื้องต้นว่า train/test ถูกจัดเรียงอย่างไรนะครับ ซึ่งจะเห็นได้ว่าข้อมูล train แต่ละตัวมี field สำคัญอยู่สอง columns คือ \"question_text\" หรือหัวข้อกระทู้ และ \"target\" ที่บ่งบอกว่ากระทู้นี้มีเจตนาที่ดีหรือไม่ โดย target = 1 แปลว่ามีเจตนาไม่ดีครับ\n"},{"metadata":{"trusted":true,"_uuid":"a0ccf0c7cc80114530afc0b9dd226b953e73568d"},"cell_type":"code","source":"train_df.head(10) # ลองดูซิ ว่าข้อมูลสอนมีหน้าตาเป็นอย่างไร","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ข้อมูลจะประกอบไปด้วย column รหัสเฉพาะของแต่ละคำถาม qid และประโยคคำถามของแต่ละกระทู้ question_textและค่า target ที่ระบุว่าเป็นกระทู้ที่ดีหรือไม่ \n\nโดยเราจะใช้ข้อมูลในส่วนของ question_text และ target เท่านั้นโดยไม่มีความจำเป็นต้องใช้ qid ครับ"},{"metadata":{"trusted":true,"_uuid":"be42ff5ca1929d2482edb0dc1c6b864db4d4f91b"},"cell_type":"code","source":"test_df.head(5) # ข้อมูลทดสอบหรือ test data จะเหมือน train ยกเว้นเราจะไม่รู้ target ที่เราต้องการทำนาย","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"ad1e9fdf97f3291a7d1797b25f0c7a0c5d1f1edd"},"cell_type":"markdown","source":"เนื่องจากข้อมูลที่ Model เราต้องทำความเข้าใจนั้นอยู่ใน field **question_text** ซึ่งประกอบไปด้วย strings เป็นภาษามนุษย์ เราจึงต้อง preprocess เบื้องต้นเพื่อให้ Model หรือ computer ของเราสามารถทำความเข้าใจมันได้ครับ\n\nในที่นี่ เราจะ preprocess ข้อมูลอย่างง่าย ตาม steps ต่อไปนี้\n\n1) ก่อนอื่นแบ่ง train data ออกเป็นสองส่วนคือ train data (ชุดใหม่) และ validation data หรือบางครั้งเรียกว่า development data โดย validation data นั้นมีจุดประสงค์เพื่อเอาไว้ใช้ทดสอบประสิทธิภาพของโมเดลเราว่าดีหรือไม่ดีเบื้องต้น (ก่อนที่จะนำไปทดสอบจริงบน test set)\n\n2) ถ้ากระทู้ไหนไม่มีข้อมูลใน field **question_text** เราก็เพิ่งประโยค  '_na_' ลงไป เพื่อให้การเขียนโปรแกรมในขั้นถัดๆ ไปทำได้ง่าย ไม่ต้องมาจัดการ missing values\n\n3) ใช้ Tokenizer : ตัดประโยคให้เป็นคำๆ ทำ mapping ศัพท์แต่ละคำให้เป็นตัวเลข ซึ่งทำให้ท้ายที่สุด เราจะเปลี่ยนข้อความในทุกๆ กระทู้ให้เป็น Sequence of numbers ซึ่่ง computer สามารถนำไปทำความเข้าใจได้โดยง่าย\n * ทั้งนี้เราต้องกำหนดด้วยว่าจะให้ model สามารถจดจำศัพท์ได้กี่คำ (ในโปรแกรมข้างล่างเราเรียกตัวแปรนี้ว่า **max_features**) บางครั้งการจำศัพท์ที่ใช้บ่อยมากเกินไป หรือใช้แค่ 1-2 ครั้งบนตัวอย่างทั้งหมดที่เรามีอาจไม่มีประโยชน์ในการทำความเข้าใจว่า ประโยคหนึ่งๆ นั้นมีเจตนาดีหรือไม่\n\n4) เพื่อความง่ายเราจะกำหนดให้ทุกๆ sequence of numbers ดังกล่าวมีความยาวเท่าๆ กันทั้งหมด (เรียกค่าความยาวนี้ว่า **maxlen**) ดังนั้นกระทู้ไหนที่ยาวเกินไป เราจะตัดประโยคส่วนหัวทิ้ง (จะเลือกตัดส่วนท้ายก็ได้) และถ้าประโยคไหนสั้นเกินไป เราจะเติมตัวเลข 0 ลงไปให้สุดท้ายยาวเท่ากับ maxlen\n"},{"metadata":{"trusted":true,"_uuid":"ba5a1b8109dee2c9fbc628d5da4a7c3447d42fb8"},"cell_type":"code","source":"%%time\n## 1) ใช้ฟังก์ชัน train_test_split เพื่อแบ่งข้อมูลออกเป็น train/valid data\ntrain_df, val_df = train_test_split(train_df, test_size=0.1, random_state=2018)\n\n## กำหนดค่าคงที่ต่างๆ ซึ่งสามารถปรับจูนได้เพื่อประสิทธิภาพที่ดีขึ้น ในภายหลัง\nembed_size = 300 # ชนาด dimension ของ word vectors\nmax_features = 100000 # จำนวนคำศัพท์ที่เราจะให้ model รู้จัก\nmaxlen = 70 # กำหนดให้ความยาวของทุกประโยคเท่ากันที่ 70\n\n## 2) ดึงเฉพาะข้อความกระทู้ขึ้นมา รวมทั้งจัดการ missing values อย่างง่าย\ntrain_X = train_df[\"question_text\"].fillna(\"_na_\").values\nval_X = val_df[\"question_text\"].fillna(\"_na_\").values\ntest_X = test_df[\"question_text\"].fillna(\"_na_\").values\n\n## 3) ใช้ Tokenizer\n#3.1) สร้าง object tokenizer ขึ้นมา ระบุคำศัพท์สูงสุดที่ต้องการจำ\ntokenizer = Tokenizer(num_words=max_features)\n#3.2) ให้ tokenizer เรียนรู้ศัพท์ใน training data\ntokenizer.fit_on_texts(list(train_X))\n#3.3) ใช้ tokenizer เปลี่ยนจาก string เป็น sequence of numbers\ntrain_X = tokenizer.texts_to_sequences(train_X)\nval_X = tokenizer.texts_to_sequences(val_X)\ntest_X = tokenizer.texts_to_sequences(test_X)\n\n## 4) Pad the sentences \ntrain_X = pad_sequences(train_X, maxlen=maxlen)\nval_X = pad_sequences(val_X, maxlen=maxlen)\ntest_X = pad_sequences(test_X, maxlen=maxlen)\n\n## 5) เก็บค่า targets หรือ labels ไว้ในตัวแปร y\n## เรามองเกือบทุกปัญหาใน machine learning เป็นการหาฟังก์ชันที่ดีในการ map จาก X --> y\ntrain_y = train_df['target'].values\nval_y = val_df['target'].values","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"d9d5f3e7257b301534347279d8c3bbbe6e2b4c96"},"cell_type":"code","source":"'''ตอนนี้ข้อมูลเราจะอยู่ในรูป Sequence of numbers ที่ีมีความยาว maxlen = 70 แล้วครับ\nสังเกตว่า เราแบ่งข้อมูล 10% ไปเป็น Validation data, ทำให้ training data เหลืออยู่ 1.175 ล้านกระทู้ครับ\n'''\nprint('ขนาดข้อมูลหลังแปลง : ',train_X.shape, val_X.shape, train_y.shape, val_y.shape, '\\n')\n'''เราลอง print train_X[0] เพื่อดูตัวอย่าง sequence of numbers, สังเกตว่าเราแปะ 0 ไว้ที่ข้างหน้าเพื่อให้ความยาวครบ 70 โดยแท้จริงแล้วเราเลือกแปะ 0 ไว้ข้างหลังก็ได้เช่นกัน '''\nprint('ตัวอย่างข้อมูลสอนตัวแรกจะอยู่ในรูป sequence of numbers ความยาว maxlen = %d ดังนี้ : ' % (maxlen))\nprint(train_X[0])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"1c2aa3bdc72e7eadfe69d2ec8908031ca493ee53"},"cell_type":"code","source":"'''ปัจจัยหนึ่งที่ทำให้ปัญหาคัดกรองกระทู้ ทำได้ยากก็คือ สัดส่วนของกระทู้ที่เจตนาไม่ดีนั้น มีน้อยกว่ากระทู้ปกติมาก \n(ซึ่งทำให้โมเดลของเราทำความเข้าใจรูปแบบกระทู้ที่ไม่ดีได้ลำบาก เพราะตัวอย่างส่วนใหญ่จะเป็นกระทู้ดี)\n\nสัดส่วนของกระทู้ที่ไม่ดี มีเพียงราว 6.2% เท่านั้น\n'''\nprint('สัดส่วนกระทู้ไม่ดีเท่ากับ %.4f ' % (sum(train_y)/len(train_y)))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"2a5f324273d8e4726a6f0f9206170845d5ead890"},"cell_type":"markdown","source":"# Word Embedding\nWord embedding หรือการเปลี่ยนคำศัพท์ภาษามนุษย์ให้อยู่ในรูปเวกเตอร์ทางคณิตศาสตร์ ซึ่งทำให้คอมพิวเตอร์เข้าใจความสัมพันธ์ของศัพท์แต่ละคำได้อย่างเป็นระบบ เราสามารถเรียน Word Embedding ได้จากข้อมูลเช่นเดียวกับ parameters อื่นๆ ในโมเดลหรือจะใช้ word vectors ที่ถูกเตรียมมาให้แล้ว (pretrained) ก็ได้เช่นกัน \n\nโดยทีมงานจาก Google, Facebook และ Stanford ต่างก็ได้เตรียม (pre-trained) word embedding vectors ไว้ให้เรียบร้อยแล้ว ในชื่อ Word2Vec, FastText, และ GloVe ตามลำดับ และทาง Kaggle เองก็ได้เตรียม pretrained vectors ไว้ให้ใช้งานได้ทันทีใน Notebook นี้เรียบร้อยแล้วครับ\n\nสามารถอ่านรายละเอียดเพิ่มเติมได้ในกระทู้นี้ครับ\n\nhttps://thaikeras.com/community/%E0%B9%80%E0%B8%81%E0%B8%B5%E0%B9%88%E0%B8%A2%E0%B8%A7%E0%B8%81%E0%B8%B1%E0%B8%9A-kaggle/quora-insincere-questions-classification-detect-toxic-content-to-improve-online-conversations/\n\n\nDeep Learning ขึ้นมาเพื่อเรียนรู้จากข้อมูลกระทู้ต่างๆ และจะยังไม่ใช้ pretrained word vectors ครับ\n\n## เริ่มต้นสร้าง model ด้วย Keras\n\nKeras เป็นการเขียนโปรแกรมสร้าง Deep Learning model ที่ง่ายดายและมีประสิทธิภาพสูง และเราสามารถนำ pretrained word embedding vectors มาใช้ได้อย่างสะดวก\n\n### ทำความเข้าใจการวัดผล และ baseline เมื่อเราไม่ใช้ word embedding\n**ก่อนอื่นเราจะทำความเข้าใจเรื่องการวัดผลในปัญหานี้ และลองดูประสิทธิภาพแบบไม่ใช้ pretrained word embedding กันก่อนครับ **\n\nในตัวอย่างแรกเราจะลองสร้าง model Bidirectional GRU ซึ่งเป็น Sequential Model มาตรฐานสำหรับ Deep Learning  กรณีที่เราไม่ใช้ pretrained word embedding เราก็จะ train word embedding layer ขึ้นมาเองจากข้อมูล training data ที่เรามีครับ "},{"metadata":{"trusted":true,"_uuid":"3cfab26c6cced33ef7ab84f0d36997113131d530"},"cell_type":"code","source":"'''โค้ดต่อไปนี้ เป็นโค้ดของ Keras ที่สามารถใช้สร้าง Deep Sequential Neural Network Model ขึ้นมาได้อย่างง่ายดาย \nโดย Deep Neural Networks นั้นจะประกอบไปด้วย Neural Layers ต่างๆ มาประมวลผลต่อเนื่องกัน โดยเพื่อนๆ สามารถศึกษา\nรายละเอียดของ Layers ต่างๆ ได้จาก Course online เช่น DeepLearning.ai บน coursera.org หรือ course ของ ThAIKeras ในอนาคต\n\nลักษณะการสร้าง Layers ของ Keras จะมีรูปแบบตายตัวดังนี้\n\noutput_tensor = NewLayer(layer_parameters)(input_tensor)\n\nโดยเราสามารถนำ Layers ต่างๆ อาทิเช่น Embedding, GRU, MaxPool, หรือ Dense มาต่อกันได้อย่างอิสระ คล้ายกับการต่อ Lego ดังโค้ดข้างล่าง\n'''\n\n## นิยามโมเดลด้วยการนำ Layers มาเชื่อมกัน\ninp = Input(shape=(maxlen,))\nx = Embedding(max_features, embed_size)(inp)\nx = Bidirectional(CuDNNGRU(64, return_sequences=True))(x)\nx = GlobalMaxPool1D()(x)\nx = Dense(16, activation=\"relu\")(x)\nx = Dropout(0.1)(x)\nx = Dense(1, activation=\"sigmoid\")(x)\n\n## สั่งสร้าง model ด้วยการกำหนด input และ output tensors\nmodel = Model(inputs=inp, outputs=x)\n\n# compile คือการระบุวิธีการฝึกโมเดล ว่าต้องการ loss/metric ประเภทไหน และใช้ optimizer แบบไหน\nmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\n\n# print โมเดลสรุปออกมา\nprint(model.summary())","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"b71267b19ec4b8a4c977cb3ad87b636390e5cdb9"},"cell_type":"markdown","source":"จาก model summary ข้างบนสังเกตว่าเรามี parameters ของ neural networks ที่ต้องเรียนรู้ถึง 30 กว่าล้านตัวแปรเลยทีเดียว โดยเกือบทั้งหมดคือตัวแปรของ embedding layers จำนวน 30ล้านตัวแปร ทั้งนี้เพราะเราระบุจำนวนคำศัพท์เท่ากับ 1แสนคำ และคูณกับ 300 ซึ่งเป็น dimension ของ vector \n\nเมื่อเราสร้างโมเดลเสร็จแล้ว เราสามารถสั่ง train หรือให้โมเดลเรียนรู้จากข้อมูล training data ของเราได้ง่ายๆ ทันทีเลยครับด้วยคำสั่ง `model.fit` โดยระบุ batch size เป็น 512 (จำนวน data ที่สุ่มมาทำ backpropagation ในแต่ละครั้ง) และรันทั้งหมด 3 epochs (อ่านข้อมูล training data จำนวน 1.17 ล้านกระทู้ทั้งหมด 3 รอบ)"},{"metadata":{"trusted":true,"_uuid":"ef1e1015e7c3ab5bc5d9774e49820c4b286d7847"},"cell_type":"code","source":"## Train the model \nmodel.fit(train_X, train_y, batch_size=512, epochs=3, validation_data=(val_X, val_y))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"e72ef362e393395ee582c041d3b01fa35e5a46fc"},"cell_type":"markdown","source":"หลังจากเทรนเสร็จแล้ว เราใช้คำสั่ง `model.predict` เพื่อทำนายข้อมูล validation data เพื่อที่จะได้ตรวจสอบว่าเราทำนายได้แม่นยำมากน้อยแค่ไหน "},{"metadata":{"trusted":true,"_uuid":"608ab1b8ede06d01afa72ad13aca69f39bc92df5"},"cell_type":"code","source":"pred_noemb_val_y = model.predict([val_X], batch_size=1024, verbose=1)[:,0]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ก่อนอื่นเราลอง print ค่าที่ได้จาก `model.predict` ออกมาดูว่ามีหน้าตาเป็นแบบไหนก่อนครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(pred_noemb_val_y.shape)\nprint('ตัวอย่างคำทำนาย 10 กระทู้แรกใน validation data: ' ) # print ดูที่เราทำนาย 10 cases แรก\nprint(pred_noemb_val_y[:10])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"สังเกตว่าคำทำนายมีทั้งหมด 130,613 ค่า ซึ่งก็ตรงกับจำนวน validation data ที่เราต้องการทดสอบ \n\nนอกจากนี้ค่าที่ได้จะอยู่ในช่วง [0,1] นี่เป็นเพราะ layer สุดท้ายใน model ของเราเป็น layer sigmoid และเราตีความหมายได้ว่าเป็น \"ความน่าจะเป็น\" ที่แต่กระทู้จะเป็นกระทู้ที่ไม่ดีครับ ซึ่งจากตัวอย่างคำทำนายใน 10 กระทู้แรกของ validation set เราพบว่ามีเพียง 1 กระทู้ที่น่าจะเป็น \"กระทู้ไม่ดี\" นั่นคือกระทู้ที่ 6 ซึ่งมีความน่าจะเป็นมากกว่า 50% ครับ"},{"metadata":{"trusted":true,"_uuid":"5ed524b59481ea415fb7d633485e061837962e1e"},"cell_type":"code","source":"\nprint('ค่า target จริงของ 10 กระทู้แรก: ', val_y[:10]) # print ดู label จริง","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":" ## F1 : มาตรวัดที่ดีกว่า \"ความถูกต้อง\" (accuracy)\n\n- ใน 10 กระทู้แรกนั้นเหมือนคำทำนายเราจะแม่นเลยทีเดียวครับ เพราะมีเพียงกระทู้ที่ 6 ที่เป็นกระทู้ไม่ดีจริงๆ ดั่งคำทำนาย \n- แม้จะดูเหมือนแม่น แต่จริงๆ แล้วต้องระวังครับ ในปัญหาที่จำนวนตัวอย่างแต่ละ**คลาสไม่สมดุลอย่างมาก (very imbalance class)** แบบนี้ เรามีแต่ตัวอย่างคลาส 0 หรือ \"กระทู้ดี\" เต็มไปหมด (94%) ดังนั้นการตอบว่า **กระทู้ดี** เป็นเรื่องง่ายมากครับ ดังนั้นจริงๆ แล้วเราต้อง Focus การตอบกระทู้ไม่ดี ให้ถูกต้องมากกว่า\n- ในปัญหานี้ ถ้าโมเดลเราตอบว่าทุกกระทู้ \"ดี\" ทั้งหมด ก็จะได้ความ**ถูกต้อง**ถึงราวๆ 94% ซึ่งดูเผินๆ เหมือนดีมาก แต่ความจริงแล้วเราเรียนรู้อะไรไม่ได้เลย ดังนั้น **\"ค่าความถูกต้อง\" หรือ accuracy** จึงเป็นการวัดผลที่อาจไม่เหมาะสม และทำให้เราเข้าใจผิดคิดว่าโมเดลเราทำงานได้ดีแล้วครับ \n- การวัดผลที่เหมาะสมกว่ามีหลายวิธี ซึ่งวิธีที่ Quora เลือกใช้ในการประลองครั้งนี้ก็คือการวัดด้วย F1 Score ครับ (ดูรายละเอียดข้างล่าง หรือที่ https://en.wikipedia.org/wiki/F1_score) ในที่นี้เราเรียก f1_score function จาก scikit learn library ได้เลยครับ\n\n### หมายเหตุ: ว่าด้วยเรื่องการวัดผล\nในบทความนี้จะใช้คำศัพท์ต่อไปนี้ในความหมายที่แตกต่างกัน เพื่อนๆ ระวังสับสนนะครับ: \n- ความถูกต้อง (accuracy)\n- ความแม่นยำ (precision)\n- ความครอบคลุม (recall)\n\nสามคำนี้จะให้ความหมายของประสิทธิภาพของคำนายที่ไม่เหมือนกัน นอกจากนี้เราจะใช้ F1 Score ซึ่งเป็นคล้ายๆ ค่าเฉลี่ยของ precsion และ recall เป็นมาตรวัดหลัก\n\nก่อนจะไปเจาะลึกถึงเรื่องการวัดผลแบบ F1 เราลองดูประสิทธิภาพของคำทำนายทั้งหมด 1.3 แสนกระทู้ใน validation data ว่าแม่นยำแค่ไหน โดยในการทำนายเบื้องต้นนั้น เราจะทำนายกระทู้ที่มีความน่าจะเป็นมากกว่า 50% ว่า \"ไม่ดี\" ครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"from sklearn import metrics\n\nthresh = 0.50\npred_noemb_val_y_int = pred_noemb_val_y > thresh\nprint('ค่า accuracy เท่ากับ %.4f' % (sum(pred_noemb_val_y_int == val_y)/len(val_y)))\nprint('ค่า F1 เท่ากับ %.4f' % (metrics.f1_score(val_y, (pred_noemb_val_y>thresh).astype(int)) ) )","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"ec4cc44d0647d73d1aa0603ffa778798f17b96e5"},"cell_type":"markdown","source":"### หมายเหตุ : การรัน Keras แต่ละครั้งอาจได้ผลต่างกันเล็กน้อย\nเนื่องจาก keras model นั้นทุกครั้งที่รันจะได้ผลต่างกันนิดหน่อย ดังนั้นค่าที่ผมได้จากการทดลองระหว่างเขียนบทความนี้นั้น ก็จะต่างจากค่าที่เพื่อนๆ เห็นอยู่บ้าง\n\nดังนั้นตัวเลขที่ผมใช้อธิบายใน notebook นั้นจะขอใช้ตัวเลขคร่าวๆ แทนตัวเลขเป้ะๆ นะครับ เช่นใช้ 60%++ แทน 62.19% หรือ 63.61% เป็นต้น\n\n## ทำความเข้าใจ F1 จาก Precision และ Recall\nจากผลข้างบนค่าความแม่นยำเราเพิ่มจาก 94% (ทาย 0 หมด) เป็น 95%++ และค่า F1 เราเท่ากับ 60%++ จากตรงนี้ เข้าใจว่าเพื่อนๆ อาจสับสนเหมือนกันว่าจะแปลความหมายอย่างไรดี\n\nเพื่อความง่ายเราอาจมองว่าค่า F1 เป็นค่าเฉลี่ยระหว่างสองค่าที่สำคัญในการทำนาย class1 หรือ กระทู้ไม่ดีดังนี้ครับ\n\n- ค่า**ความแม่นยำ**ของคำทำนายเรา (precision) : จากที่เราทาย class 1 ไปนั้น \"คำทำนาย\" ของเราถูกกี่ตัว\n- ค่า**ความครอบคลุม**ของคำทำนายเรา (recall) : กระทู้ที่ไม่ดี class 1 ทั้งหมดนั้นถูกทายถูกจำนวนกี่ตัว\n\nสำหรับเพื่อนๆ ที่เพิ่งเคยเจอ precision และ recall เป็นครั้งแรกอาจสับสนว่ามันต่างกันตรงไหนนะครับ ที่แตกต่างกันคือ \"ตัวหารครับ\" โดย precision นั้นเทียบจาก \"จำนวน class1 ที่เราทำนาย\" ส่วน recall นั้นเทียบจาก \"จำนวน class1 จริงๆ\" (ความสามารถในการครอบคลุม class1 ของโมเดลเรา)  \n\nสังเกตว่าถ้าเราทำนายแค่กระทู้เดียวที่เรามั่นใจมากๆ แล้วถูกเราก็จะมี precision 100% แต่ recall ก็จะต่ำมากเพราะเราครอบคลุมได้แค่ 1 กระทู้จากกระทู้ class1 ที่มีมากมายครับ ดังนั้นค่า F1 ที่คล้ายๆเป็นค่าเฉลี่ยของทั้งสองค่านี้ก็จะยังต่ำด้วย\n\nดังนั้นการที่เราพิจารณาตัวเลข accuracy, precision & recall นั้นก็จะทำให้เราเข้าใจโมเดลเราในสามมุมมองที่แตกต่างกัน\n\nอนึ่ง ทำความเข้าใจ precision / recall / F1 ไว้ให้ดีๆ ครับเพราะเป็นมาตรวัดที่สำคัญมากๆ ใน AI และ Data Science ครับ และเพื่อนๆ จะได้เจอมันบ่อยๆ อีกแน่นอนในอนาคต\n\n![รูป precision & recall](https://cdn-images-1.medium.com/max/1600/1*pOtBHai4jFd-ujaNXPilRg.png)"},{"metadata":{"trusted":true},"cell_type":"code","source":"count = np.logical_and(pred_noemb_val_y_int == val_y, val_y == 1) # นับจำนวนที่เราทำนาย class1 ถูกต้อง\n\nprint('ค่า precision : %4f '% (sum(count)/sum(pred_noemb_val_y_int == 1)))\nprint('ค่า recall : %4f '% (sum(count)/sum(val_y == 1)))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"เพื่อนๆ แปลความหมายค่า precision / recall ที่ได้ ได้ไหมครับ? และค่า F1 เป็นค่ากลางๆ ระหว่างสองค่านี้จริงไหมครับ\n\nตอนนี้เพื่อนๆ ก็คงเข้าใจ F1 score มากขึ้นแล้วนะครับผม\n\n## ตัวเลข 50% threshold เหมาะสมไหม?\nดูเผินๆ **การทำนายกระทู้ว่าไม่ดี** ถ้ามีความน่าจะเป็นว่าไม่ดีมากกว่า 50% ก็ดูสมเหตุสมผลดีแล้ว แต่อย่าลืมว่าปัญหาของเราเป็นปัญหา imbalance ซึ่งสัดส่วนกระทู้ที่ไม่ดีมีน้อยมาก model ของเราอาจลำเอียงให้ค่าความน่าจะเป็นน้อยโดยเฉลี่ย\n\nเพื่อทำความเข้าใจเราลอง print ค่าความน่าจะเป็นของข้อมูลเฉพาะ class1 ใน validation data 10 ตัวอย่างแรกมาดูกันครับ"},{"metadata":{"trusted":true,"_uuid":"b9a1464f597c1238c8d4bdff01f1b40841d11448"},"cell_type":"code","source":"idx_1 = np.where(val_y == 1)[0] # หา id เฉพาะกระทู้ไม่ดี (class1) ใน validation data\nprint(pred_noemb_val_y[idx_1[:10]]) # print คำนาย 10 cases แรกของกระทู้ไม่ดี\nprint(val_y[idx_1[:10]]) # 1 ทั้งหมด (กระทู้ไม่ดีทั้งหมด)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"a72ba82481de9f96c62c334cc40bd1d134d38e2d"},"cell_type":"markdown","source":"ตัวเลขเป็นอย่างไรบ้างครับ ตัวเลขที่เพื่อนๆ เห็นจะต่างจากตัวเลขที่ผมได้จากในขณะเขียนบทความนี้เล็กน้อย แต่รวมๆ แล้วน่าะจะมีกระทู้ class1 ที่ค่าความน่าจะเป็นต่ำกว่า 50% อยู่ 2-4 กระทู้ ซึ่งทำให้ค่า recall ใน 10 cases แรกของเราอยู่ที่ราวๆ 60%-80% ถ้าเราใช้ threshold = 50% ในการตัดสินว่ากระทู้นั้นเป็นกระทู้ไม่ดีหรือไม่\n\nสังเกตว่าถ้าเราลด threshold ลงอีก เช่นให้ threshold = 30% หรือ \n\n> กระทู้ที่มีความน่าจะเป็น > 30% นั้น 'ไม่ดี'\n\nเราก็จะได้ค่า recall มากขึ้นเป็น 80-90% เลยทีเดียวครับ แต่ค่า F1 ของเราอาจจะลดลงก็ได้เพราะค่า precision อาจจะต่ำลง\n\nเอ แล้วเราจะรู้ได้อย่างไรล่ะ ว่าค่า threshold ไหนที่สร้างสมดุลระห่ว่าง precision และ recall ได้ดีที่สุด นั่นคือได้ F1 มากที่สุด??\n\nวิธีหนึ่งที่เราทำได้และเป็นวิธีมาตรฐานที่ผู้เข้าแข่งขันเกือบทุกคนใช้ก็คือ เราจะวนลูปทดลองค่า threshold ไปเรื่อยๆ ดูว่า threshold ไหนให้คะแนน F1 Score เยอะที่สุดบน  validation data \nแล้วเราก็เลือก threshold นั้นดังแสดงในโค้ดข้างล่างนี้ครับ"},{"metadata":{"trusted":true,"_uuid":"47b63dca0247a08a808db7ae6eea33065c554948"},"cell_type":"code","source":"best_thresh = 0.2\nmax_f1 = 0\nfor thresh in np.arange(0.2, 0.601, 0.01):\n    thresh = np.round(thresh, 2)\n    current_f1 = metrics.f1_score(val_y, (pred_noemb_val_y>thresh).astype(int))\n    print(\"ค่า F1 ที่ threshold %.2f is %.4f\" % (thresh, current_f1) )\n    \n    if current_f1 > max_f1:\n        best_thresh = thresh\n        max_f1 = current_f1\n\nprint('ค่า threshold ที่ดีที่สุดคือ %.2f ให้ค่า F1 เท่ากับ %.4f' % (best_thresh,max_f1))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"383e51177bf33da0b8fee42dd6a093908b808f64"},"cell_type":"markdown","source":"ค่า threshold ที่ดีทีสุดไม่ใช้ 50% และค่า F1 ที่ดีที่สุดนั้นมากกว่าค่า F1 ที่ threshold=50% อยู่พอสมควรเลยครับ!\n\n### หมายเหตุ\nวิธีการนี้อาจทำให้ค่า F1 ของเรา overfit กับ validation data ได้ อย่างไรก็ดีถ้า validation data ของเรามีจำนวนมาก ค่า F1 นี้ก็จะ overfit ไม่มากครับ (ในทางปฏิบัติผู้เข้าแข่งขันที่ทำคะแนนได้สูง จะมีวิธีหาค่า threshold ที่ซับซ้อนกว่านี้เพื่อให้ overfit น้อยที่สุด)\n\n## ทำนาย test data\nเมื่อเราทดสอบบน validation data และได้ค่า threshold ที่น่าพอใจแล้ว เราก็เอาความรุ้ที่ได้มาทำนาย class1 หรือ กระทู้ไม่ดี บน test data ทั้ง 3.75 แสนกระทู้ได้เลยครับ"},{"metadata":{"trusted":true,"_uuid":"a88df747f43259bab84447b50e45aa9e978f2cee"},"cell_type":"code","source":"pred_noemb_test_y = model.predict([test_X], batch_size=1024, verbose=1)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"f3831ee610fee119c9851b5ca29b2d80b102ae6a"},"cell_type":"markdown","source":"เพื่อการประหยัดเนื่อที่ เราใช้คำสั่งต่อไปนี้ในการล้างข้อมูลหรือตัวแปรที่เราจะไม่ใช้แล้วครับ"},{"metadata":{"trusted":true,"_uuid":"a36a071fb50f6c120e099b5fe27ad6ac977f1125"},"cell_type":"code","source":"del model, inp, x\nimport gc; gc.collect()\ntime.sleep(10)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"717f7dcd5ccf71e83d0f062e221c46db39845e4d"},"cell_type":"markdown","source":"# ทดลองใช้ pretrained-embedding\n\nหลังจากเราได้ประสิทธิภาพจาก GRU Baseline ที่ไม่ได้ใช้ pre-trained embedding แล้ว เราจะลองใช้ pretrained word embedding ใน keras models กันดูบ้างว่าจะทำให้ประสิทธิภาพดีขึ้นเท่าไร\n\nทั้งนี้ Kaggle & Quora ได้เตรียม pre-trained word embedding vectors ให้เราทั้้งหมด 4 ชุด\n"},{"metadata":{"trusted":true,"_uuid":"b9d263852f653e466e24f9827548d7d1a7ee7262"},"cell_type":"code","source":"!ls ../input/quoratextemb/","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"7c0010e518288bc7f588776c58610949140a139a"},"cell_type":"markdown","source":"ซึ่งมีที่มาคร่าวๆ ดังนี้ครับ\n * GoogleNews-vectors-negative300 - https://code.google.com/archive/p/word2vec/\n เป็น embedding ของ Google ใช้ algorithm word2vec\n \n * glove.840B.300d - https://nlp.stanford.edu/projects/glove/\n เป็น embedding ของมหาวิทยาลัย Stanford ใช้ algorithm GloVe\n \n * paragram_300_sl999 - https://cogcomp.org/page/resource_view/106\n เป็น embedding ของมหาวิทยาลัย Pennsylvania ใช้ algorithm Paragram\n \n * wiki-news-300d-1M - https://fasttext.cc/docs/en/english-vectors.html\n เป็น embedding ของ Facebook ใช้ algorithm FastText\n \n ถ้าอยากดูรายละเอียดเพิ่มเติมดูได้ที่ [kernel](https://www.kaggle.com/sbongo/do-pretrained-embeddings-give-you-the-extra-edge) นี้ครับ\n\n### Glove Embeddings\nเราลองใช้ GloVe กันก่อนเลยครับ โดยขั้นตอนการสร้าง keras model แทบจะเหมือนเดิมทุกอย่าง ยกเว้นการโหลด weights ของ pretrained vectors เข้าไปเท่านั้น"},{"metadata":{"trusted":true},"cell_type":"code","source":"'''ฟังก์ชันในการโหลด pre-trained word embedding vectors\n\nINPUTS: \n(1) word_index : dictionary ที่ map จาก \"ศัพท์ภาษาอังกฤษ\" เป็น \"ตัวเลข\" ได้จาก keras tokenizer\n(2) max_words : จำนวนคำศัพท์ที่เราต้องการ ในที่นี่้มีค่าเท่ากับ max_features\n(3) embed_size ขนาด dimension ของ vectors ซึ่งเรากำหนดค่ามาตรฐานสากล = 300\n\nOUTPUT: embedding matrix ขนาด (max_words x embed_size)\n\n'''\n\ndef load_glove_fast(word_index, max_words=max_features, embed_size=300):\n    EMBEDDING_FILE = '../input/quoratextemb/glove.840B.300d/glove.840B.300d.txt'\n    emb_mean, emb_std = -0.005838499, 0.48782197\n\n    embedding_matrix = np.random.normal(emb_mean, emb_std, (max_words, embed_size))\n    with open(EMBEDDING_FILE, 'r', encoding=\"utf8\") as f:\n        for line in f:\n            word, vec = line.split(' ', 1)\n            if word not in word_index:\n                continue\n            i = word_index[word]\n            if i >= max_words:\n                continue\n            embedding_vector = np.asarray(vec.split(' '), dtype='float32')[:300]\n            if len(embedding_vector) == 300:\n                embedding_matrix[i] = embedding_vector\n    return embedding_matrix\n\ndef load_fasttext_fast(word_index, max_words=max_features, embed_size=300):\n    EMBEDDING_FILE = '../input/quoratextemb/wiki-news-300d-1M/wiki-news-300d-1M.vec'\n    emb_mean, emb_std = -0.0033470048, 0.109855264\n\n    embedding_matrix = np.random.normal(emb_mean, emb_std, (max_words, embed_size))\n    with open(EMBEDDING_FILE, 'r', encoding=\"utf8\") as f:       \n        for line in f:\n            if len(line) <= 100:\n                continue\n            word, vec = line.split(' ', 1)\n            if word not in word_index:\n                continue\n            i = word_index[word]\n            if i >= max_words:\n                continue\n            embedding_vector = np.asarray(vec.split(' '), dtype='float32')[:300]\n            if len(embedding_vector) == 300:\n                embedding_matrix[i] = embedding_vector\n    return embedding_matrix\n\ndef load_para_fast(word_index, max_words=max_features, embed_size=300):\n    EMBEDDING_FILE = '../input/quoratextemb/paragram_300_sl999/paragram_300_sl999.txt'\n    emb_mean, emb_std = -0.0053247833, 0.49346462\n\n    embedding_matrix = np.random.normal(emb_mean, emb_std, (max_words, embed_size))\n    with open(EMBEDDING_FILE, 'r', encoding=\"utf8\", errors=\"ignore\") as f:        \n        for line in f:\n            if len(line) <= 100:\n                continue\n            word, vec = line.split(' ', 1)\n            if word not in word_index:\n                continue\n            i = word_index[word]\n            if i >= max_words:\n                continue\n            embedding_vector = np.asarray(vec.split(' '), dtype='float32')[:300]\n            if len(embedding_vector) == 300:\n                embedding_matrix[i] = embedding_vector\n    return embedding_matrix","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"23f130e80159bb1701e449e2e91199dbfff1f1d4"},"cell_type":"code","source":"%%time\n'''โหลด weights ของ pre-trained vectors มาเก็บไว้ในตัวแปร'''\n\nembedding_matrix =  load_glove_fast(tokenizer.word_index)\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"'''สร้าง Deep Learning Model ด้วย Keras'''\ninp = Input(shape=(maxlen,))\nx = Embedding(max_features, embed_size, weights=[embedding_matrix])(inp) # ต่างจากเดิมที่บรรทัดนี้บรรทัดเดียวครับ\nx = Bidirectional(CuDNNGRU(64, return_sequences=True))(x)\nx = GlobalMaxPool1D()(x)\nx = Dense(16, activation=\"relu\")(x)\nx = Dropout(0.1)(x)\nx = Dense(1, activation=\"sigmoid\")(x)\nmodel = Model(inputs=inp, outputs=x)\nmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\nprint(model.summary())","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"a560ab0dbab9cf6fdbdae6721ec030e300f19d78"},"cell_type":"code","source":"model.fit(train_X, train_y, batch_size=512, epochs=3, validation_data=(val_X, val_y))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"ff43855164472de035a5a1d80b3db4838684701a"},"cell_type":"code","source":"pred_glove_val_y = model.predict([val_X], batch_size=1024, verbose=1)\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"best_thresh = 0.2\nmax_f1 = 0\nfor thresh in np.arange(0.2, 0.601, 0.01):\n    thresh = np.round(thresh, 2)\n    current_f1 = metrics.f1_score(val_y, (pred_glove_val_y>thresh).astype(int))\n    print(\"ค่า F1 ที่ threshold %.2f is %.4f\" % (thresh, current_f1) )\n    \n    if current_f1 > max_f1:\n        best_thresh = thresh\n        max_f1 = current_f1\n\nprint('ค่า threshold ที่ดีที่สุดคือ %.2f ให้ค่า F1 เท่ากับ %.4f' % (best_thresh,max_f1))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d2a33c252f31fddcc65896053184226128562776"},"cell_type":"markdown","source":"จะเห็นว่าผลลัพธ์ที่ได้จะดีกว่าเวอร์ชั่นก่อน ที่ไม่ใช้ pretrained vectors ครับ"},{"metadata":{"trusted":true,"_uuid":"d51ff8ed6a87b488fec3ac84ca50df661d7c8193"},"cell_type":"code","source":"'''ทำนาย test'''\npred_glove_test_y = model.predict([test_X], batch_size=1024, verbose=1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"39d4fedab4ac170863a0ee1ca3aa9be1ee58fe02"},"cell_type":"code","source":"'''เคลียร์หน่วยความจำ'''\ndel embedding_matrix, model, x\nimport gc; gc.collect()\ntime.sleep(10)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"bc6bab22dd12a09378f4b8b159cb7a5d88a3e7c0"},"cell_type":"markdown","source":"**Wiki News FastText Embeddings:**\n\nต่อไปเราทดลอง FastText ครับ ซึ่งขั้นตอนก็จะเหมือนเดิมทุกประการ ดังนั้นเราจะไม่ขออธิบายอะไรเยอะครับ"},{"metadata":{"trusted":true,"_uuid":"6f3d0fd28dd2b04eaccb732b96b872e5a223d962"},"cell_type":"code","source":"embedding_matrix =  load_fasttext_fast(tokenizer.word_index)\n        \ninp = Input(shape=(maxlen,))\nx = Embedding(max_features, embed_size, weights=[embedding_matrix])(inp)\nx = Bidirectional(CuDNNGRU(64, return_sequences=True))(x)\nx = GlobalMaxPool1D()(x)\nx = Dense(16, activation=\"relu\")(x)\nx = Dropout(0.1)(x)\nx = Dense(1, activation=\"sigmoid\")(x)\nmodel = Model(inputs=inp, outputs=x)\nmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"47238831a4701c8a67dc7ecb130ac1402baf7bb2"},"cell_type":"code","source":"model.fit(train_X, train_y, batch_size=512, epochs=3, validation_data=(val_X, val_y))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"b7ab4100f723ad535528865b1edc7896bce80223"},"cell_type":"code","source":"pred_fasttext_val_y = model.predict([val_X], batch_size=1024, verbose=1)\n\nmax_f1 = 0\nfor thresh in np.arange(0.2, 0.601, 0.01):\n    thresh = np.round(thresh, 2)\n    current_f1 = metrics.f1_score(val_y, (pred_fasttext_val_y>thresh).astype(int))\n    print(\"ค่า F1 ที่ threshold %.2f is %.4f\" % (thresh, current_f1) )\n    \n    if current_f1 > max_f1:\n        best_thresh = thresh\n        max_f1 = current_f1\n\nprint('ค่า threshold ที่ดีที่สุดคือ %.2f ให้ค่า F1 เท่ากับ %.4f' % (best_thresh,max_f1))\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"3216362afb0f49579d287a06f13adf8cd7d8b0cf"},"cell_type":"code","source":"pred_fasttext_test_y = model.predict([test_X], batch_size=1024, verbose=1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"f24f9753ff1d933fa4f75a0ba34df305632d6e93"},"cell_type":"code","source":"'''เคลียร์หน่วยความจำ'''\ndel embedding_matrix, model, x\nimport gc; gc.collect()\ntime.sleep(10)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"4ca44ac68bf404b9c26e07fbcc9c8ac793e04510"},"cell_type":"markdown","source":"**Paragram Embeddings:**\n\nIn this section, we can use the paragram embeddings and build the model and make predictions."},{"metadata":{"trusted":true,"_uuid":"25ec1aac4aedbf431a2d30de64030ce8e3203c18"},"cell_type":"code","source":"embedding_matrix =  load_para_fast(tokenizer.word_index)\n        \ninp = Input(shape=(maxlen,))\nx = Embedding(max_features, embed_size, weights=[embedding_matrix])(inp)\nx = Bidirectional(CuDNNGRU(64, return_sequences=True))(x)\nx = GlobalMaxPool1D()(x)\nx = Dense(16, activation=\"relu\")(x)\nx = Dropout(0.1)(x)\nx = Dense(1, activation=\"sigmoid\")(x)\nmodel = Model(inputs=inp, outputs=x)\nmodel.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"cc188f2787ea7b98d3a40953a95a5fc09ff2764d"},"cell_type":"code","source":"model.fit(train_X, train_y, batch_size=512, epochs=3, validation_data=(val_X, val_y))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"9abdfd1cf15257f2c0c2181a13327796e8d4584e"},"cell_type":"code","source":"pred_paragram_val_y = model.predict([val_X], batch_size=1024, verbose=1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"max_f1 = 0\nfor thresh in np.arange(0.2, 0.601, 0.01):\n    thresh = np.round(thresh, 2)\n    current_f1 = metrics.f1_score(val_y, (pred_paragram_val_y>thresh).astype(int))\n    print(\"ค่า F1 ที่ threshold %.2f is %.4f\" % (thresh, current_f1) )\n    \n    if current_f1 > max_f1:\n        best_thresh = thresh\n        max_f1 = current_f1\n\nprint('ค่า threshold ที่ดีที่สุดคือ %.2f ให้ค่า F1 เท่ากับ %.4f' % (best_thresh,max_f1))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"99cb9f6145da909bd7436e46d47547efc097499d"},"cell_type":"code","source":"pred_paragram_test_y = model.predict([test_X], batch_size=1024, verbose=1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"af087d21bdb4358701e31aded6b522accd5a8a64"},"cell_type":"code","source":"'''เคลียร์หน่วยความจำ'''\ndel embedding_matrix, model, x\nimport gc; gc.collect()\ntime.sleep(10)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"e1312b7a4c3b67ca4ebd26fb083dbac3b6635dc2"},"cell_type":"markdown","source":"**ข้อสังเกต:**\n * การใช้ pretrained word vectors จะให้ประสิทธิภาพการทำนายโดยรวมที่ดีกว่าการไม่ใช้ pretrained word vectors \n * ใน pretrained word vectors แต่ละชุดนั้น ประสิทธิภาพที่ได้ไม่ต่างกันมากเท่าไรนัก\n \n## Ensemble\n\nเทคนิกที่สำคัญมากๆ ในการเพิ่มประสิทธิภาพของการทำนาย คือเทคนิกที่เรียกว่า **ensemble** หรือการนำผลลัพธ์การทำนายของหลายๆ โมเดลมาเฉลี่ย (หรือมาโหวตกันก็ได้ครับ) ไอเดียของ ensemble คร่าวๆ ก็คือแม้ว่าแต่ละโมเดลที่ใช้ pretrained vectors ต่างชุดกัน จะให้ประสิทธิภาพไม่ต่างกันมาก แต่ว่าโมเดลเหล่านั้นอาจจะเข้าใจข้อมูลในแง่มุมที่แตกต่างกันไป ดังนั้นการนำทุกโมเดลมาช่วยกันทำนาย จึงเป็นการสร้างโมเดลใหม่ที่เข้าใจข้อมูลสอนได้ดีที่สุด\n"},{"metadata":{"trusted":true,"_uuid":"449bc59fdc9a719aa0759ac51a4481df113604ca"},"cell_type":"code","source":"'''สร้าง ensemble อย่างง่ายที่นำค่าความน่าจะเป็นของทั้งสามโมเดลมาเฉลี่ยกัน เพื่อให้ได้ค่าความน่าจะเป็นสุดท้าย'''\npred_val_y = 0.33*pred_glove_val_y + 0.33*pred_fasttext_val_y + 0.34*pred_paragram_val_y \n\nmax_f1 = 0\nfor thresh in np.arange(0.2, 0.601, 0.01):\n    thresh = np.round(thresh, 2)\n    current_f1 = metrics.f1_score(val_y, (pred_val_y>thresh).astype(int))\n    print(\"ค่า F1 ที่ threshold %.2f is %.4f\" % (thresh, current_f1) )\n    \n    if current_f1 > max_f1:\n        best_thresh = thresh\n        max_f1 = current_f1\n\nprint('ค่า threshold ที่ดีที่สุดคือ %.2f ให้ค่า F1 เท่ากับ %.4f' % (best_thresh,max_f1))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"4fdbeffc0f84643d2832eec49234bd9d6c6e216b"},"cell_type":"markdown","source":"ผลการทำนายที่ได้ ดีขึ้นกว่าเดิมจริงๆ ด้วยครับ ดังนั้นเราจะทำนาย test data ด้วย ensemble model นี้กัน"},{"metadata":{"trusted":true,"_uuid":"c90fb4a4ef1b3b2ea06563a6901deac1b38822f3"},"cell_type":"code","source":"pred_test_y = 0.33*pred_glove_test_y + 0.33*pred_fasttext_test_y + 0.34*pred_paragram_test_y\npred_test_y = (pred_test_y>0.35).astype(int)\nout_df = pd.DataFrame({\"qid\":test_df[\"qid\"].values})\nout_df['prediction'] = pred_test_y\nout_df.to_csv(\"submission.csv\", index=False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"f6797ab73bdd5bdb8c8f6d80ec361c50a2b0f56f"},"cell_type":"markdown","source":"จบแล้วครับ เมื่อรันเสร็จแล้ว เพื่อนๆ สามารถตรวจสอบประสิทธิภาพการทำนายบน test data ได้จากการ submit file submission.csv ตามรูปนี้ครับ โดยกดปุ่ม \"Submit to competition\" ได้เลยครับ\n![กดปุ่ม submission ได้เลย](https://i.ibb.co/B39fYGx/1502078496147.jpg)"},{"metadata":{},"cell_type":"markdown","source":"เท่าที่ผมรัน script นี้มาหลายรอบคะแนน public F1 score จะแกว่งอยู่ราวๆ 66%-68% (แกว่งมากเนื่ืองจากข้อมูลทดสอบใน public test นั้นมีน้อย) ส่วน private F1 score จะค่อนข้างนิ่งที่ 68%++ โดยคนที่ทำได้คะแนนระดับท้อปของโลกจะทำได้ราวๆ 70%-71%++ ซึ่งก็ถือว่าไม่ต่างกันมากในทางปฏิบัติสักเท่าไร อย่างไรก็ตามเพื่อนๆ อาจจะลองพัฒนาให้แม่นยำและครอบคลุมขึ้นได้ครับ\n\nถ้าใครอยากลองวิชาในการแข่งขันจริง Kaggle เพิ่งประกาศการแข่งขัน NLP classification ครั้งใหม่ สดๆ ร้อนๆ ที่นี่ครับ ซึ่งเพื่อนๆ สามารถลองประยุกต์ใช้ความรู้พื้นฐานจาก workshop นี้ในการแข่งจริง ข้อมูลจริงได้ทันทีครับ\n\nhttps://www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification\n\nการแข่งขัน Jigsaw toxicity classification อันใหม่นี้จะจบลงวันที่ 27 มิถุนายน 2562 ครับ\n\n## Feedback\nสุดท้ายนี้ เพื่อนๆ อยากให้ทีมงานทำ notebook workshop เกี่ยวกับปัญหา AI / Machine Learning ในด้านไหน เช่น Image Classification หรือ Signal Analysis หรืออยากให้ปรับปรุงอะไรสามารถแนะนำกันได้เต็มที่เลยนะครับ ไม่ว่าจะเป็นทาง thaikeras.com/community หรือ comments ด้านล่าง notebook ฉบับนี้ก็ได้ครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}