{"cells":[{"metadata":{},"cell_type":"markdown","source":"# เกริ่นนำ\n\nสวัสดีครับเพื่อนๆ Notebook ฉบับนี้คือ ThAIKeras Deep Learning Workshop สำหรับปัญหา Predictive Maintenance ของอุปกรณ์ไฟฟ้าสามเฟสครับ\n\nสำหรับเพื่อนๆ ที่ไม่เคยใช้งาน Kaggle มาก่อนสามารถ[สมัครใช้งานได้ง่ายๆ ตามนี้](https://thaikeras.com/2018/11/14/setup-kaggle-workshop/)ครับ  และสามารถ[ดู Workshops อื่นๆ ของทีมงานได้ที่นี่](https://thaikeras.com/category/workshop/)ครับ\n\nเพื่อความเข้าใจปัญหา และภาพรวมรายละเอียดที่เราจะปฏิบัติกันใน Workshop นี้ทีมงานได้เขียนบทความแยกแนะนำปัญหานี้อย่างละเอียดได้ที่นี่ครับ  https://thaikeras.com/community/เกี่ยวกับ-kaggle/การแข่งขัน-vsb-power-line-fault-detection/\nซึ่งเราจะไม่ขอกล่าวรายละเอียดต่างๆ ซ้ำในที่นี้นะครับ และขอแนะนำให้เพื่อนๆ อ่านบทความข้างต้นให้จบเพื่อเข้าใจ scope ของ Workshop เราก่อนที่จะลงมือทำกันครับ ถ้าไม่เข้าใจในจุดไหนสามารถโพสต์ถามทีมงานได้เลยครับผม **รบกวนเพื่อนๆ สมัคร Kaggle Account และกด Upvote เพื่อเป็นกำลังใจให้ทีมงานด้วยนะครับ**\n\nเอาล่ะเพื่อนๆ อ่านบทความใน ThAIKeras เรียบร้อยแล้วใช่ไหมครับ ถ้างั้นเรามาเริ่มทำ Workshop กันเลยครับ\nภาพรวมของ Project แสดงได้ดังรูปนี้ครับ\n![](https://i.ibb.co/CW97RQw/FD851-F08-DE4-F-4-C96-A001-D6-C6-D1-E21718.jpg)\n\nซึ่งเราสามารถแบ่งกระบวนการคร่าวๆ ได้ดังนี้ครับ (ผู้ที่ทำบน Desktop, menu navigation จะอยู่ด้านซ้ายมือครับ)\n\n* Step 0. เซ็ตค่า Configurations และเตรียม Functions ต่างๆ ที่เกี่ยวข้อง\n* Step 1. เตรียมข้อมูลไฟฟ้าสามเฟสจากเดิมในรูป Signal ให้อยู่ในรูป 2D-Vectors (หรือ Matrix) นั่นคือ $\\mathbf{x}_1, ..., \\mathbf{x}_{160}$ ในรูปข้างบน โดย $\\mathbf{x}_{t} \\in \\mathcal{R}^D$ (ใน Workshop นี้ $D$ เท่ากับ 57 ซึ่งเพื่อนๆ ที่ชำนาญแล้วสามารถทดลองดัดแปลงตัวเลขนี้ได้ครับ)\n* Step 2. สร้าง Model LSTM และเรียนรู้จากข้อมูลสอนที่อยู่ในรูป Vectors แล้ว\n* Step 3. ทดสอบความแม่นยำด้วย Stratified K-folds of MCC และทำนายกระแสไฟฟ้าสามเฟสว่าบกพร่องหรือไม่ในข้อมูลทดสอบ (Test Set)"},{"metadata":{},"cell_type":"markdown","source":"* **หมายเหตุ 1.** : ในบทความในบอร์ด ThAIKeras เราเล่าว่า Signal Inputs อาจเป็น Raw หรือ Denoised versions ก็ได้ หรืออาจนำทั้งคู่มาใช้งานพร้อมกันก็ได้ ทำให้ในเราต้องสร้าง Vectors ใน Step1. สองรอบ (Vector จาก Raw และจาก Denoised signals) ซึ่งการแปลงเวกเตอร์นี้จะใช้เวลาราวๆ 1 ชั่วโมง ดังนั้นเพื่อเป็นการประหยัดเวลา เราจะทำการแปลงเวกเตอร์เฉพาะ Denoised signals ใน Workshop นี้เท่านั้น และจะโหลดข้อมูล Raw Vectors ที่แปลงเสร็จแล้วจาก Notebook ฉบับอื่นขึ้นมา\n\n* **หมายเหตุ 2.** : เพื่อนๆ สามารถเลือกได้ว่าจะใช้งาน signal แบบ raw, denoised หรือ ทั้งคู่ (both) โดยปรับ Option ได้จากตัวแปร `SIGNAL_TYPE` ใน cell ข้างล่างครับ\n\n* **หมายเหตุ 3.** : ในบทความเราแตก signal ออกเป็น $\\mathbf{x}_1, ..., \\mathbf{x}_{100}$ แต่ใน Workshop นี้เราซอย Time steps ให้ย่อยลงไปเป็น $\\mathbf{x}_1, ..., \\mathbf{x}_{160}$ แทน สืบเนื่องจาก **หมายเหตุ 1.** ข้างต้น ที่ Raw Vectors ที่เตรียมไว้แล้วนั้นมีขนาด 160 time steps อย่างไรก็ตามเพื่อนๆ สามารถเซ็ตค่า Time steps นี้ได้ในตัวแปร `FEATURES_TIME_STEPS` ข้างล่าง แต่ถ้าเปลี่ยนค่าจาก 160 ไปแล้ว Workshop นี้จะรันได้ในโหมด `SIGNAL_TYPE = 'denoised'` เท่านั้น\n\n*Credit : Notebook ฉบับนี้ดัดแปลงมาจาก Notebook ระดับเหรียญทองของ Bruno ซึ่งทำไว้ดีมากในการแข่งขัน VSB ครับ ขอขอบพระคุณไว้ ณ ที่นี้ด้วยครับ\nhttps://www.kaggle.com/braquino/5-fold-lstm-attention-fully-commented-0-694 *\n\n# Step 0. เตรียม Configurations และ Functions ที่เกี่ยวข้อง"},{"metadata":{"trusted":true,"_uuid":"22824ff4e13a023e0ddc7f9f2bd82420fdbcff42"},"cell_type":"code","source":"'''\nเราสามารถเลือก Configuration หลักๆ ของ Deep Learning Model เราได้ที่นี่ครับ\n---------------------------------------------------------------\n\nN_SPLITS : ค่า K ใน Stratified K-Folds, โดยปกติเลือก 5 หรือ 10 \n\nBATCH_SIZE : จำนวน Mini-batch ใน forward และ backward propagation\n\nEPOCHS : \"จำนวนรอบ\" ที่เราต้องการให้ Model ที่เราสร้างเรียนรู้จากข้อมูลสอน\n\nHID : จำนวน Hidden Units ใน LSTM\n\nLR : learning rate หรือ อัตราการเรียนรู้เริ่มต้นของ LSTM\n\nSIGNAL_TYPE : เลือกประเภทของสัญญาณที่เราจะ input ให้ model โดยเลือกได้ 3 ประเภทคือ\n  1) 'raw' -- signal input ที่ไม่ผ่านกระบวนการ denoising process\n  2) 'denoised' -- signal ถูก denoised ด้วย wavelet transform\n  3) 'both' -- นำข้อมูลของทั้ง 1) และ 2) มารวมกัน\n[ถ้าเลือกผิดไปจาก 3 options นี้เราจะเซ็ตให้เป็น option 'both' โดย default ครับ]\n  \nFEATURE_TIME_STEPS : จำนวน time steps (จำนวนกรอบสีแดงในรูปข้างบน) ของ signal \nที่ผ่านกระบวนการ pre-process ให้เป็น vector แล้ว\n\nOption ในอนาคต : ReduceOnPlateau -- LR_FACTOR, PATIENCE\n'''\nN_SPLITS = 5\nBATCH_SIZE = 128\nEPOCHS = 50\nHID = 64\nLR = 0.001\n\n# SIGNAL_TYPE เลือกได้ 3 options นะครับ : 'raw', 'denoised', 'both'\nSIGNAL_TYPE = 'denoised' \nFEATURE_TIME_STEPS = 160 # ถ้าเปลี่ยนจาก 160, ในที่นี้เลือกได้เฉพาะ 'denoised' ครับ","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"'''Import functions จาก Libraries ที่จำเป็นต้องใช้ต่างๆ ครับ'''\n\nimport os \n# basic numpy, panda, matplotlib ดูได้ที่ Workshops พื้นฐานก่อนหน้าของ ThAIKeras ครับ\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport pyarrow.parquet as pq # ไฟล์ Signal ของเราเก็บอยู่ในรูป .parq ซึ่งต้องใช้โมดูลนี้อ่านขึ้นมาครับ\nfrom sklearn.model_selection import StratifiedKFold # ใช้ทำ Stratified K-Folds ตามที่สอนในบทความ\nfrom tqdm import tqdm # Processing time measurement\n\n'''Keras Modules ทั้งหมดที่เราจะใช้ครับ'''\nfrom keras.layers import * # ใช้สร้างแต่ละ layers ใน Neural Networks\nfrom keras.models import Model # ใช้สร้าง Model หลังจากกำหนด layer เสร็จแล้ว\nfrom keras import backend as K # ใช้สำหรับเรียก Tensorflow function ในกรณีที่ Keras ไม่ได้เตรียม function บางอย่างไว้\nfrom keras import optimizers # ใช้เรียก Optimization Algorithm ที่จะฝึกสอน Neural Networks ของเราจากข้อมูลสอนที่มี\nfrom keras.callbacks import * # ใช้เรียก function ช่วยเหลือที่จะช่วยบันทึก Log files และ save model ที่ดีที่สุดในระหว่างสอน","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"6e6379386e44afc69bee8895a52da22199e888fb"},"cell_type":"code","source":"# ตัวแปรนี้เป็น constants คือค่า voltage ที่ข้อมูลของเราวัดได้ใน 1 วินาทีของแต่ละสัญญาณ\nsample_size = 800000 # ห้ามเปลี่ยนแปลงตัวเลขนี้ครับ เพราะเป็นค่าเฉพาะของ Training Data","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## วัดผลลัพธ์ด้วย Matthew Correlation Coefficient (MCC)\n\nในโจทย์ปัญหานี้ทางผู้จัดทำได้กำหนดว่าจะให้วัดผลความแม่นยำด้วย MCC ซึ่งมีสูตรดังนี้\n\\begin{equation*}\n\\text{MCC} = \\frac{ \\mathit{TP} \\times \\mathit{TN} - \\mathit{FP} \\times \\mathit{FN} } {\\sqrt{ (\\mathit{TP} + \\mathit{FP}) ( \\mathit{TP} + \\mathit{FN} ) ( \\mathit{TN} + \\mathit{FP} ) ( \\mathit{TN} + \\mathit{FN} ) } }\n\\end{equation*}\n\nโดย $TP, TN, FP, FN$ คือ True Positive/Negative, False Positive/Negative ตามลำดับ และอย่างที่ผมเขียนแยกอธิบายไว้ใน[บทความนี้](https://www.kaggle.com/ratthachat/demythifying-matthew-correlation-coefficients-mcc) เราสามารถทำความเข้าใจ MCC ว่าวัดความแม่นยำในมุมองง่ายๆ ดังนี้ครับ \n\n> $MCC = (A + B - 1)*C$ โดย \n> * $A$ = ความแม่นยำ (Precision)ในการทายตัวอย่างบวก\n> * $B$ = ความแม่นยำในการทายตัวอย่างลบ\n> * $C$ = ความแม่นยำในการทาย “สัดส่วน” ของตัวอย่างบวกและลบ"},{"metadata":{"trusted":true,"_uuid":"c3340ee96becb5ca8f075d9c44b7df383ddba5ee"},"cell_type":"code","source":"# นี่คือ function MCC ที่เราจะส่งให้ Keras ใช้วัดค่าความแม่นยำของโมเดลเราในแต่ละรอบการฝึกสอนครับ\ndef matthews_correlation(y_true, y_pred):\n\n    y_pred_pos = K.round(K.clip(y_pred, 0, 1))\n    y_pred_neg = 1 - y_pred_pos\n\n    y_pos = K.round(K.clip(y_true, 0, 1))\n    y_neg = 1 - y_pos\n\n    tp = K.sum(y_pos * y_pred_pos)\n    tn = K.sum(y_neg * y_pred_neg)\n\n    fp = K.sum(y_neg * y_pred_pos)\n    fn = K.sum(y_pos * y_pred_neg)\n\n    numerator = (tp * tn - fp * fn)\n    denominator = K.sqrt((tp + fp) * (tp + fn) * (tn + fp) * (tn + fn))\n\n    return numerator / (denominator + K.epsilon())","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"034d3eb4b7a5cce9d3443326f990ab61b54cb27b"},"cell_type":"code","source":"'''นี่คือ MCC ที่ implement ด้วย Numpy (ส่วนข้างบนที่ implement ด้วย Keras) \nเราทำ MCC version นี้เพราะการเรียกใช้งานภายนอก Keras จะเร็วกว่ามากครับ'''\ndef my_mat_cor(y_true, y_pred):\n    assert y_true.shape[0] == y_pred.shape[0]\n\n    tp = np.sum((y_true == 1) & (y_pred == 1))\n    tn = np.sum((y_true == 0) & (y_pred == 0))\n    fp = np.sum((y_true == 0) & (y_pred == 1))\n    fn = np.sum((y_true == 1) & (y_pred == 0))\n\n    numerator = (tp * tn - fp * fn) \n    denominator = ((tp + fp) * (tp + fn) * (tn + fp) * (tn + fn)) ** .5\n\n    return numerator / (denominator + 1e-15)\n\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"เนื่องจาก Deep Learning Model จะทำนายค่าผลลัพธ์ออกมาเป็น **ค่าความน่าจะเป็น** $p_t \\in [0,1]$ แต่ในการคำนวนค่า MCC เราจำเป็นต้องทำนายคำตอบสุดท้าย $\\hat{y}_t$ เป็น 0 หรือ 1 เท่านั้น ดังนั้นเราจำเป็นต้องมี threshold $T$ ซึ่งจะเป็นตัวตัดสินว่าควรจะทำนาย 0 หรือ 1 ดังนี้\n\n\\begin{equation}   \n\\hat{y}_t = \n\\begin{cases} \n1 & if \\  p_t > T \\\\ \n0 & otherwise\n\\end{cases}\n\\end{equation}\n\nวิธีการที่เราจะทำอย่างตรงไปตรงมาคือจะวนลูป search เพื่อหาค่า $T$ ที่ดีที่สุดโดยวัดผลจากกระบวนการทดสอบ Stratified K-Folds อย่างที่ได้[อธิบายในบทความ ThAIKeras ครับ](https://thaikeras.com/community/เกี่ยวกับ-kaggle/การแข่งขัน-vsb-power-line-fault-detection/#post-143)"},{"metadata":{"trusted":true},"cell_type":"code","source":"''' Function ที่ทำหน้าที่ค้นหาค่า threshold ที่ทำให้ค่า MCC สูงที่สุดใน validation set\ny_true : array ของ label ที่แท้จริง\ny_proba : array ของ p_t (ความน่าจะเป็น) ดังที่อธิบายข้างต้น\n'''\ndef threshold_search(y_true, y_proba):\n    best_threshold = 0\n    best_score = 0\n\n    for threshold in [i * 0.01 for i in range(20,80)]:\n        score = my_mat_cor(y_true,(y_proba > threshold).astype(np.uint8))\n        if score > best_score:\n            best_threshold = threshold\n            best_score = score\n    search_result = {'threshold': best_threshold, 'matthews_correlation': best_score}\n    return search_result","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Step 1. เปลี่ยน Signal ให้อยู่ในรูป Vector\n\nใน `../input` นั้นเราจะมีข้อมูลอยู่ทั้งหมด 3 folders ครับ โดยข้อมูลจริงๆ ที่ทาง Kaggle และ VSB เตรียมไว้ให้นั้นจะเป็นข้อมูล Signal IDs และ Class (ID ใดเป็นคลาส 1 [ไฟมีปัญหา] หรือ 0 [ไม่มีปัญหา]) ซึ่งอยู่ใน folder `vsb-power-line-fault-detection` ครับ\n\nส่วนอีก 2 folders คือ\n* `5-fold-lstm-attention-fully-commented-0-694` จะมีข้อมูล Raw Signal ที่ถูกเปลี่ยนเป็น Vector เรียบร้อยแล้วตามที่อธิบายใน **หมายเหตุ 1.** ข้างต้นครับ\n\n* `vsb-wavelet-denoised` จะมีข้อมูล Signal ที่ทีมงานได้ทำการ Denoising ด้วย Wavelet transform ไว้ก่อนแล้วอยู่ ซึ่งใน Notebook นี้เราจะทำการเปลี่ยน signal ประเภทนี้ให้เป็น vector ครับ\n\n## 1.1 ตัวอย่างข้อมูลสอนที่มี"},{"metadata":{"trusted":true},"cell_type":"code","source":"!ls ../input","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"'''ข้อมูลรายละเอียดของสัญญาณและ Class ที่โจทย์กำหนดมาให้'''\n!ls ../input/vsb-power-line-fault-detection/","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ข้อมูล `train.parq` และ `test.parq` จะเป็นข้อมูล raw signals ที่เราจะไม่ได้ใช้แปลงเวกเตอร์ใน notebook นี้อย่างที่กล่าวไว้ในหมายเหตุด้านบนว่าเพื่อประหยัดเวลา เราจะโหลดข้อมูลเวกเตอร์ของ raw signal ที่แปลงเสร็จแล้วมาใช้เลย"},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"# metadata_train จะบอกรายละเอียดของข้อมูลสอนไว้ เรามาดูกันว่าเป็นอย่างไรบ้าง\ndf_train = pd.read_csv('../input/vsb-power-line-fault-detection/metadata_train.csv')\n# เซ็ต่คอลัมภ์ id_measurement, phase ให้เป็น index ใน panda, เทคนิกเล็กๆ ที่ทำให้การค้นข้อมูลเร็วขึ้น\ndf_train = df_train.set_index(['id_measurement', 'phase'])\ndf_train.head(9) #ลองดูรายละเอียดของข้อมูลสอนกัน","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"จะเห็นว่าข้อมูลถูกแบ่งไว้เบื้องต้นด้วย ID ของสัญญาณไฟฟ้า 3 เฟส (`id_measurement`) โดย `phase` จะบอกว่าเป็นเฟสที่ 0, 1 หรือ 2 และเนื่องจากโจทย์ของเราอนุญาตให้ทำนายการบกพร่องของสัญญาณแยกเป็นแต่ละเฟสได้ (เช่นเฟส 0 ไม่บกพร่องแต่เฟส 1 บกพร่อง) kaggle จึงกำหนด `signal_id` ขึ้นมาเพื่อให้สัญญาณแต่ละเฟสนี้แยกออกจากกัน \n\nแต่อย่างที่[อธิบายไว้ในบทความ](https://thaikeras.com/community/เกี่ยวกับ-kaggle/การแข่งขัน-vsb-power-line-fault-detection/#post-140)ว่าการรวมทำนายทั้ง 3 เฟสพร้อมๆ กันจะให้ผลลัพธ์ที่ดีกว่าครับ ดังนั้นในทางปฏิบัติใน notebook นี้เราจะยึด `id_measurement` เป็นหลักอยู่ดี นั่นคือในกระบวนการแปลงสัญญาณเป็นเวกเตอร์นั้นเราต้องรวมสัญญาณทั้ง 3เฟส ให้เป็นตัวอย่างเดียวกัน (หัวข้อ 1.2)\n\nในโค้ดถัดไปในการสังเกต distribution ของ class ในตัวอย่างสอน **เราจะพบว่าในสัญญาณทั้ง 8712 สัญญาณที่ให้มานั้นมีเพียง 525 สัญญาณที่เป็น class 1 เท่านั้น ซึ่งคิดเป็นเพียง 6% ของข้อมูลทั้งหมด (Imbalance Data)**"},{"metadata":{"trusted":true},"cell_type":"code","source":"yy = df_train['target'].values\nprint(yy.shape)\n\n#ดูว่ามีคลาส 1 กี่ตัว และกี่ %\nprint(sum(yy), sum(yy)/len(yy))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ก่อนที่เราจะลงมือแปลงสัญญาณเป็นเวกเตอร์ เราลองมาดู raw / denoised signals ของ class 0 และ class 1 กันครับว่าหน้าตาสัญญาณที่บกพร่องและไม่บกพร่องนั้นเป็นอย่างไรกันบ้าง โดยเราจะโหลดสัญญาณ 3 เฟสสองสัญญาณแรกขึ้นมาพล็อตครับ (สัญญาณ 3 เฟสแรกเป็น class 0 และสัญญาณที่สองเป็น class 1) เพื่อนๆ ที่สนใจสามารถทดลองแก้โปรแกรม เพื่อพล็อตสัญญาณอื่นๆ มาดูก็ได้ครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"!ls ../input/vsb-wavelet-denoised\nparq_sample_dn = pq.read_pandas('../input/vsb-wavelet-denoised/train_dn.parquet', columns=[str(i) for i in range(0, 6)]).to_pandas()\nparq_sample = pq.read_pandas('../input/vsb-power-line-fault-detection/train.parquet', columns=[str(i) for i in range(0, 6)]).to_pandas()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"'''ข้อมูลสัญญาณแต่ละเฟสจะถูกเก็บในแต่ละคอลัมภ์ครับ (แต่ละคอลัมภ์มี 8 แสนแถวแทน 8แสนค่า voltages ที่วัดมาได้) \nโดยในตัวอย่างนี้คอลัมภ์ 0,1,2 คือเฟส 0,1,2 ของสัญญาณที่หนึ่งและคอลัมภ์ 3,4,5 คือเฟส 0,1,2 ของสัญญาณที่สอง'''\nparq_sample","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ถ้าเรานำข้อมูล signal ที่วัดค่า Voltage ทั้ง 8แสนค่ามาพล็อตก็จะได้รูปดังนี้ครับ [สังเกตว่าสัญญาณประเภท Denoised นั้นได้ตัดองค์ประกอบของ Sinusoidal waves ไป](https://thaikeras.com/community/เกี่ยวกับ-kaggle/การแข่งขัน-vsb-power-line-fault-detection/#post-141)"},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nplt.figure(figsize=(10, 5))\n# plt.title(\"ID measurement:0, Target:0\",\n#          fontdict={'fontsize':36})\nplt.subplot(1,2, 1)\nplt.plot(parq_sample[\"0\"].values, marker=\"o\", label='Phase 0')\nplt.plot(parq_sample[\"1\"].values, marker=\"o\", label='Phase 1')\nplt.plot(parq_sample[\"2\"].values, marker=\"o\", label='Phase 2')\nplt.ylim(-60,60)\nplt.legend()\nplt.title('Class 0')\n# plt.show()\n\n# plt.figure(figsize=(15, 10))\n# plt.title(\"ID measurement:1, Target:1\",\n#          fontdict={'fontsize':36})\nplt.subplot(1,2, 2)\nplt.plot(parq_sample[\"3\"].values, marker=\"o\", label='Phase 0')\nplt.plot(parq_sample[\"4\"].values, marker=\"o\", label='Phase 1')\nplt.plot(parq_sample[\"5\"].values, marker=\"o\", label='Phase 2')\nplt.ylim(-60,60)\nplt.legend()\nplt.title('Class 1')\n# plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nplt.figure(figsize=(10, 5))\n# plt.title(\"ID measurement:0, Target:0\",\n#          fontdict={'fontsize':36})\nplt.subplot(1,2, 1)\nplt.plot(parq_sample_dn[\"0\"].values, marker=\"o\", label='Phase 0')\nplt.plot(parq_sample_dn[\"1\"].values, marker=\"o\", label='Phase 1')\nplt.plot(parq_sample_dn[\"2\"].values, marker=\"o\", label='Phase 2')\nplt.ylim(-60,60)\nplt.legend()\nplt.title('Class 0')\n# plt.show()\n\n# plt.figure(figsize=(15, 10))\n# plt.title(\"ID measurement:1, Target:1\",\n#          fontdict={'fontsize':36})\nplt.subplot(1,2, 2)\nplt.plot(parq_sample_dn[\"3\"].values, marker=\"o\", label='Phase 0')\nplt.plot(parq_sample_dn[\"4\"].values, marker=\"o\", label='Phase 1')\nplt.plot(parq_sample_dn[\"5\"].values, marker=\"o\", label='Phase 2')\nplt.ylim(-60,60)\nplt.legend()\nplt.title('Class 1')\n# plt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ดูๆ แล้วสัญญาณที่ไม่มีปัญหา (class 0) กับมีปัญหา (class 1) แทบไม่แตกต่างกันเลยนะครับใน Raw Signal ซึ่งเป็นความยากของปัญหานี้ ส่วนในสองตัวอย่างนี้ Denoised Signal เพอสังเกตเห็นความแตกต่างได้เล็กน้อยครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"#ลบข้อมูล เพื่อประหยัดหน่วยความจำสักเล็กน้อย\ndel parq_sample_dn, parq_sample","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 1.2 ลงมือสร้าง Vector จาก Signal\n\nโดยสิ่งที่เราทำสรุปได้ดังรูปนี้ \n\n![](https://i.ibb.co/KjHRy8M/signal2-Vec.jpg)\n\nขนาดของ windows จะแปรผันตามตัวแปร FEATURE_TIME_STEPS ในกรณีที่ FEATURE_TIME_STEPS=160, window ของเราก็จะมีขนาด 1/160 ของข้อมูล (ในรูปแสดงตัวอย่างเมื่อ FEATURE_TIME_STEPS = 100 ทำให้ window มีขนาด 1/100 = 1%)\n\nสังเกตว่า $x_t \\in \\mathcal{R}^D$ (ใน Workshop นี้ $D$ เท่ากับ 57 ซึ่งเพื่อนๆ ที่ชำนาญแล้วสามารถทดลองดัดแปลงตัวเลขนี้ได้ครับ)"},{"metadata":{},"cell_type":"markdown","source":"ขั้นตอนจริงๆ แล้วไม่ได้ซับซ้อน แต่ในทางปฏิบัติการลงมือเขียนโปรแกรมจะมีรายละเอียดปลีกย่อยจุกจิก ทำให้เราต้องแบ่งขั้นตอนการแปลงจาก Signal เป็น Vector ออกเป็น 4 ขั้นตอนย่อย (4 functions ย่อย) ดังจะขออธิบายภาพรวมก่อนจะเริ่มเขียน Code จริงดังนี้ครับ\n\n### 1) ฟังก์ชัน `load_all`\n\n__Output ที่ต้องการ : __ แปลง Signal ซึ่งเดิมมีขนาด `(8712, 800000)` หรือ 8712 สัญญาณ (แบบคิดแยกเฟส) ให้เป็น เวกเตอร์ขนาด `(2904, 160, 57)` โดย 2904 นั้นคือ 8712/3 เนื่องจากเราสัญญาณทั้งสามเฟสไว้เป็นตัวอย่างเดียว, 160 คือ `FEATURE_TIME_STEPS` หรือจำนวน window ที่กำหนดไว้ตอนต้น และ 57 คือ $D$ มิติของเวกเตอร์ในแต่ละ window ซึ่งรายละเอียดในแต่ละมิตินั้นจะอยู่ในฟังก์ชั่น `transform_ts` ครับ\n\n__ไอเดียการเขียนโปรแกรม : __ เนื่องจากสัญญาณขนาด `(8712, 800000)` นั้นใหญ่เกินกว่า RAM ที่เรามีใน Kaggle Kernel ดังนั้นฟังก์ชันนี้จะทำหน้าที่ง่ายๆ เพียงแค่แบ่งโหลดสัญญาณออกเป็นสองช่วงคือครั้งละ `(4356, 800000)` แล้วส่งต่อไปเข้าฟังก์ชัน `prep_data` ครับ (ใน Test Data ซึ่งข้อมูลใหญ่กว่านี้มากเราจำเป็นต้องแบ่งเป็น 10 ช่วงทีเดียว)\n\n### 2) ฟังก์ชัน `prep_data`\n\n__Output ที่ต้องการ : __ จากข้อมูลครึ่งนึง `(4356, 800000)` (สัญญาณแยกเฟส) เราต้องการ output เป็น vector `(1452, 160, 57)` (รวม 3 เฟสแล้ว)\n\n__ไอเดียการเขียนโปรแกรม : __ เนื่องจากข้อมูล input มาแยกเฟสกัน ฟังก์ชันนี้จะส่งสัญญาณทีละเฟส `(1,800000)` ไปเข้าฟังก์ชัน `transform_ts` เพื่อเปลี่ยนสัญญาณเป็นเวกเตอร์ทีละเฟส `(1,160,19)` และในฟังก์ชัน `prep_data` นี้จะทำหน้าที่ concatenate 3 vectors จาก 3 เฟส ให้รวมเป็นเวกเตอร์เดียวครับทำให้มิติเวกเตอร์สุดท้ายนั้นมีค่าเป็น 57 = 19x3 นั่นคือ `3x(1,160,19) --> (1,160,57)`\n\n### 3) ฟังก์ชัน `transform_ts`\n\n__Output ที่ต้องการ : __ เปลี่ยนสัญญาณ `(1,800000)` ให้เป็น `(1,160,19)`\n\n__ไอเดียการเขียนโปรแกรม : __ ในขั้นตอนนี้เราจะสร้าง window ขึ้นมาโดยขนาดของ window เท่ากับ `800000/160 = 5000` ค่าของ voltages ที่วัดมาได้ครับ โดยไอเดียง่ายๆ ที่เราจะ extract features จากค่า 5000 voltages นี่ก็คือการหา mean, standard deviation, และ percentile ช่วงต่างๆ ครับ โดยเราเชื่อว่าค่าสถิติเหล่านี้จะบ่งบอก information ที่สำคัญของข้อมูลใน window นั้นๆ ครับ (หมายเหตุ ถ้าเพื่อนๆ เชี่ยวชาญแล้วสามารถดัดแปลงโค้ดส่วนนี้ให้หา features ที่ซับซ้อนมากกว่านี้ได้เช่นค่า Entropy เป็นต้น)  \n\n### 4) ฟังก์ชัน `min_max_transf`\n\n__Output ที่ต้องการ : __ standardize ค่า voltage ซึ่งเดิมอยู่ในช่วง (-128,127) ให้อยู่ในช่วง (-1,1) ครับ เพื่อให้ตัวเลขของเราอยู่ในเสกลที่ประมวลผลได้ง่ายขึ้น\n\n__ไอเดียการเขียนโปรแกรม : __ เป็นการทำ standardization แบบมาตรฐาน\n\nทั้งหมดนี้คือไอเดียคร่าวๆ ของแต่ละฟังก์ชั่นครับ และเรามาดูรายละเอียดในโค้ดของจริงกันเลยครับ"},{"metadata":{"trusted":true,"_uuid":"7b0717b14bcfcba1f48d33c8161ae51c778687af"},"cell_type":"code","source":"# ค่า 127, -128 คือค่าสูงสุดและต่ำสุดของ voltage ในข้อมูลสอนที่เรามี\nmax_num = 127\nmin_num = -128\n\n# ตัวแปร ts ย่อมาจาก 'time_series' ซึ่งแทนความหมายสัญญาณ input ขนาด (1,800000) นั่นเองครับ\ndef min_max_transf(ts, min_data, max_data, range_needed=(-1,1)):\n    if min_data < 0:\n        ts_std = (ts + abs(min_data)) / (max_data + abs(min_data))\n    else:\n        ts_std = (ts - min_data) / (max_data - min_data)\n    if range_needed[0] < 0:    \n        return ts_std * (range_needed[1] + abs(range_needed[0])) + range_needed[0]\n    else:\n        return ts_std * (range_needed[1] - range_needed[0]) + range_needed[0]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"c6137bbbe75c3a1509a5f98e08805dbbd492aa37"},"cell_type":"code","source":"# ตัวแปร ts ย่อมาจาก 'time_series' ซึ่งแทนความหมายสัญญาณ input ขนาด (1,800000) นั่นเองครับ\n\ndef transform_ts(ts, n_dim=FEATURE_TIME_STEPS, min_max=(-1,1)):\n    # ก่อนอื่นเปลี่ยนสัญญาณ voltage ให้มีค่าในช่วง (-1, 1)\n    ts_std = min_max_transf(ts, min_data=min_num, max_data=max_num)\n    \n    # bucket_size คือขนาดของ window ซึ่งเท่ากับ 800000/160 = 5000\n    bucket_size = int(sample_size / n_dim)\n    \n    # new_ts คือ matrix output ขนาด 160x19\n    new_ts = []\n    \n    # วนลูปอ่านข้อมูลทีละ window\n    for i in range(0, sample_size, bucket_size):\n        # ตัดข้อมูลใน window ที่ i ออกมา\n        ts_range = ts_std[i:i + bucket_size]\n        \n        # ค่าสถิติพื้นฐานใน window\n        mean = ts_range.mean()\n        std = ts_range.std() # standard deviation\n        std_top = mean + std # I have to test it more, but is is like a band\n        std_bot = mean - std\n        \n        # ค่า percentile ต่างๆ ใน window\n        percentil_calc = np.percentile(ts_range, [0, 1, 25, 50, 75, 99, 100]) \n        max_range = percentil_calc[-1] - percentil_calc[0] # this is the amplitude of the chunk\n        relative_percentile = percentil_calc - mean # maybe it could heap to understand the asymmetry\n\n        # รวม features ข้างบนเข้าด้วยกันด้วย concatenation เป็นอันเสร็จ 1 window\n        new_ts.append(np.concatenate([np.asarray([mean, std, std_top, std_bot, max_range]),percentil_calc, relative_percentile]))\n        \n    # เปลี่ยน list ของ 160 vectors ให้เป็น numpy array 160x19\n    return np.asarray(new_ts)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"7460e718a605803f1d9e4fbec61750a0deb02a47"},"cell_type":"code","source":"# start, end คือตำแหน่งเริ่มต้นและจบที่เราจะอ่านข้อมูลมาบางส่วน (ในที่นี้คือ 4356 สัญญาณจากข้อมูลเต็มๆ คือ 8712 สัญญาณ) \n# ทั้งนี้เนื่องด้วยข้อจำกัดของ RAM\ndef prep_data(start, end):\n    \n    # อ่านสัญญาณ denoised แล้วในช่วง (start,end) โดยใช้โมดูล parquet\n    praq_train = pq.read_pandas('../input/vsb-wavelet-denoised/train_dn.parquet', columns=[str(i) for i in range(start, end)]).to_pandas()\n    \n    # X,y ในที่นี้จะรวมสัญญาณ 3 เฟสเข้าไว้ด้วยกันแล้ว\n    X = []\n    y = []\n    \n    # tqdm เป็นฟังก์ชันที่ใช้บอกความคืบหน้าของ loop ได้ มีประโยชน์มากครับ\n    # ในลูปใหญ่ เราจะวนลูปอ่านทีละ 3 เฟส \n    # สังเกตว่า start,end นั้นเป็น index ของ signal_id ที่แยกเฟสกัน ดังนั้นเนื่องจากเราต้องการทำทีละ 3 เฟส ในลูปนี้เราจึงต้องหาร 3 ที่สองตัวแปรนี้\n    for id_measurement in tqdm(df_train.index.levels[0].unique()[int(start/3):int(end/3)]):\n        \n        # X_signal จะเก็บ vector ขนาด (160,57) ของแต่ละสัญญาณ 3เฟส\n        X_signal = []\n        # วนลูปอ่านทีละเฟส\n        for phase in [0,1,2]:\n            \n            # อ่านข้อมูล signal_id, target (class) ของสัญญาณเฟสนี้\n            signal_id, target = df_train.loc[id_measurement].loc[phase]\n            \n            # เนื่องจากเรายุบ 3เฟสเป็นข้อมูลเดียว เราจึงบันทึก label y เพียงครั้งเดียว (แทนที่จะทำทั้ง 3 เฟส)\n            if phase == 0:\n                y.append(target)\n            \n            # เรียก transform_ts เพื่อสร้าง vector (160,19) ทีละเฟส \n            X_signal.append(transform_ts(praq_train[str(signal_id)]))\n        \n        # concatenate 3เฟสไว้ใน X_signal : 3*(160,19) --> (160,57)\n        X_signal = np.concatenate(X_signal, axis=1)\n\n        # บรรจุแต่ละ vector ลงไปไว้ในตัวแปร X\n        X.append(X_signal)\n    \n    # สุดท้าย X.shape = (4356/3,160,57) = (1452,160,57)\n    X = np.asarray(X)\n    y = np.asarray(y)\n    return X, y","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"52dc826ab9ee1dd56c9fb29bd5c1b2d26b5928bf"},"cell_type":"code","source":"# โค้ดส่วนนี้เพียงแค่เรียกฟังก์ชั่น prep_data สองรอบเท่านั้น (เนื่องด้วยข้อจำกัดของ RAM ทำให้ทำครั้งเดียวไม่ได้)\n# เนื่องจาก Notebook จะแสดงการแปลง vector เฉพาะ denoised signal เท่านั้นเราจึงตั้งชื่อ output variable ว่า X_dn\nX_dn = []\ny = []\ndef load_all():\n    total_size = len(df_train)\n    for ini, end in [(0, int(total_size/2)), (int(total_size/2), total_size)]:\n        X_temp, y_temp = prep_data(ini, end)\n        X_dn.append(X_temp)\n        y.append(y_temp)\nload_all()\nX_dn = np.concatenate(X_dn)\ny = np.concatenate(y)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"51ad0e25b00536de6170168499923d82ae1d735f"},"cell_type":"code","source":"print(X_dn.shape, y.shape)\n# save data ไว้ในกรณีที่ต้องการโหลดมาใช้ในอนาคตโดยไม่ต้องแปลงข้อมูลซ้ำอีก\nnp.save(\"X_dn.npy\",X_dn)\nnp.save(\"y.npy\",y)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ตอนนี้เราก็แปลง Denoised Signal เป็น Denoised Vectors เสร็จเรียบร้อยแล้วนะครับ\n\nอย่างที่เกริ่นไว้ตอนต้น เพื่อเป็นการประหยัดเวลา เราจะโหลดข้อมูล Raw Signal ที่แปลงเป็นเวกเตอร์เรียบร้อยแล้วขึ้นมาเก็บไว้ในตัวแปร `X_orig` ครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"!ls ../input/5-fold-lstm-attention-fully-commented-0-694","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"X_orig = np.load('../input/5-fold-lstm-attention-fully-commented-0-694/X.npy')\nprint(X_orig.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"สุดท้ายประเภทของ Signal ที่เราจะเอามาใช้ก็จะชึ้นกับตัวแปร `SIGNAL_TYPE` ที่เราเซ็ตไว้ในตอนต้นของ Notebook ครับ ในกรณีที่เราเซ็ตค่าเป็น `both` เราก็จะนำข้อมูลทั้ง raw และ denoised มาต่อกันทำให้ มิติของเวกเตอร์ในแต่ละ window เรามีขนาดใหญ่ขึ้นเป็นสองเท่าครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"if SIGNAL_TYPE == 'raw':\n    X = X_orig\nelif SIGNAL_TYPE == 'denoised':\n    X = X_dn\nelse: # SIGNAL_TYPE = 'both'\n    X = np.concatenate([X_orig,X_dn],axis=-1)\n    \nprint(SIGNAL_TYPE, X.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Step 2. สร้าง 2-Layers Bidirectional LSTM\n\nในขั้นนี้เราจะเข้าสู่กระบวนการสร้าง Deep Learning Model กันสักทีนะครับ ด้วยพลังของ `Keras` นั้นทำให้การสร้างโมเดล และการสอนโมเดลจากข้อมูลที่เรามีอยู่นั้นทำได้ง่ายดายมากๆ เลยครับ\n\n## 2.1 ทำความเข้าใจ LSTM ในมุมมอง BlackBox\nในที่นี้เนื่องจากข้อมูลของเรามีลักษณะเป็น Sequence of Vectors ดังนั้น Neural Architecture ที่เหมาะสมที่จะมาเรียนรู้ข้อมูลจึงอยู่ในตระกูล RNN ซึ่งในที่นี้เราจะสร้างโมเดล 2-layers Bidirectional LSTM ซึ่งเป็น RNN ที่มีประสิทธิภาพสูงเพื่อมาเรียนรู้ข้อมูล vectors ที่เราสร้างจาก signal data ครับ\n\nมันคงจะดีมากถ้าเพื่อนๆ มีความรู้เรื่อง RNN และ LSTM กันมาบ้างแล้ว (ThAIKeras จะทำ course สอนพื้นฐานในอนาคตครับ) เพื่อนๆ ที่อยากเข้าใจ RNN และ LSTM ในเชิงลึกแนะนำให้เพื่อนๆ เรียนรู้จาก coursera.org ไปก่อน และถ้าเพื่อนๆ คนไหนทราบแหล่งเรียนรู้ภาษาไทยดีๆ ก็สามารถแนะนำท่านอื่นๆ ได้ใน comments ครับ\n\nในมุมของวิศวกร มันอาจจะเพียงพอที่เราจะทำความเข้าใจ LSTM ในมุม **BlackBox** ครับ นั่นคือ **เข้าใจ input และ output** ว่าเป็นอย่างไรนั่นเอง \n\nBlackBox Diagram ของ LSTM แสดงได้ดังรูปนี้ครับ\n![](https://kvitajakub.github.io/img/rnn-unrolled.svg)"},{"metadata":{},"cell_type":"markdown","source":"LSTM นั้นสามารถเขียนเป็น Diagram ได้สองแบบคือแบบ Recursive (ซ้าย) และแบบ Sequential (ขวา) โดยในรูปขวาจะง่ายต่อความเข้าใจครับว่า **โดยเนื้อแท้ของ LSTM นั้นรับ input เป็น sequence of vectors $\\mathbf{x_0}, ..., \\mathbf{x_n}$ ในรูป และก็ส่ง output เป็น Sequence of Vectors เช่นกัน $\\mathbf{h_0}, ..., \\mathbf{h_n}$ ** โดย sequence of vectors นั้นปกติจะเขียนได้ในรูป 2D-array  เช่นในตัวอย่างของเราคือ (160, 57) นั่นเอง ทว่าเนื่องจากเรามีข้อมูลประเภทนี้อยู่ทั้งหมด 2904 ชุด ดังนั้นท้ายที่สุดเราจึงมีข้อมูลในรูป 3D-array (2904,160,57) ซึ่ง LSTM ใน Keras สามารถรับ input ใน 3D-array ได้โดยตรงเลยครับ (ไม่ต้องวนลูปทีละ example) \n\nสำหรับมิติของ output vector ของ LSTM นั้นจะกำหนดในตัวแปร `HID` ในตอนต้นของ Notebook ครับ\n\n*หมายเหตุ A : * ในรูปซ้ายนั้นแสดง nature ความเป็น recursive function ของ LSTM ที่ว่าโดยเนื้อแท้แล้ว Function ที่ประมวลผลแต่ละ time step $\\mathbf{x_t}$ นั้นแท้จริงแล้วเป็น Function เดียวกัน แต่เรียกใช้งานทุกๆ time steps ทำให้สามารถรับ sequential inputs ได้\n\n*หมายเหตุ B : * Bi-directional LSTM (หรือ Bi-LSTM) เองเป็นเทคนิกที่นำ LSTM ที่อ่านข้อมูลกลับหลังเข้ามารวมกับ LSTM ปกติและทำให้โมเดลสามารถทำความเข้าใจ input ได้ดียิ่งขึ้น ส่วน 2-layers Bi-LSTM นั้นก็เป็นเพียงนำ Bi-LSTM 2 layers มาซ้อนกันเพื่อให้ประมวลผลข้อมูลที่ซับซ้อนได้มากขึ้นนั่นเอง ใน Workshop นี้เราเลือกใช้ 2-layer Bi-LSTM แต่เพื่อนๆ สามารถดัดแปลง architecture ตรงนี้ได้ตามใจชอบครับ"},{"metadata":{},"cell_type":"markdown","source":"## 2.2 สร้าง 0/1 output ด้วย Neural Network Layers อื่นๆ\n\nในปัญหาของเราเนื่องจากเรามี Input เป็น Sequence of Vectors เรียบร้อยแล้ว (ที่เราทำใน Step 1) ดังนั้นจึงสมเหตุสมผลมากที่เราจะนำ Model LSTM มาใช้เพื่อที่จะทำนายปัญหาพยากรณ์สัญญาณไฟฟ้าที่บกพร่องนี้ อย่างไรก็ดีเนื่องจาก LSTM นั้นก็มี output เป็น Sequence of Vectors ด้วยดังภาพ ดังนั้นเราจึงจำเป็นต้องแปลง output ของ LSTM เพื่อให้ทำนายได้ว่าเป็น class 0 หรือ 1\n\nไอเดียง่ายๆ ก็คือเราเพียงแต่นำ Neural Network layers ประเภทอื่นๆ ที่มี inputs เป็น Sequence of Vectors และมี output เป็น 1-vector มาต่อกับ 2-layer Bi-LSTM ของเราเท่านี้เองครับ\n\nLayer ที่ทำหน้าที่ดังกล่าวมีหลายชนิด เช่น MaxPooling (ใน Keras เรียก `GlobalMaxPooling1D`) ที่รับ Input 3D-array $(N, T, HID)$ เหลือ $(N,HID)$ โดยรวม information จากทุกๆ time steps ใน $T$ ด้วยการ return ค่า max ของแต่ละมิติ $d \\in {1,...,HID}$ นั่นเอง นอกจากนี้ Layer `GlobalAveragePooling1D` ก็ทำหน้าที่คล้ายๆ กันเพียงแต่เปลี่ยนจาก return ค่า max เป็นค่า average ของแต่ละมิติเท่านั้น โดยในโมเดลของเราเพื่อรักษา information ให้มากที่สุดเราก็เลือกนำทั้งสอง layers นี้มาใช้ร่วมกันครับทำให้ output ของเราเป็น $(N,2*HID)$ หรือ $(2904, 128)$ นั่นเองในกรณีที่เราเลือกค่า `HID=64` (ค่าที่ Workshop นี้เลือกมาในตอนแรก)\n\nเนื่องจากตอนนี้แต่ละ Example เราถูกแทนด้วย vector 1D ปกติ (128 มิติ) แล้วในขั้นตอนถัดจากนี้ก็เพียงสร้าง Vector functional ที่รับ input เป็น 128 มิติแล้วส่งไปยังจำนวนจริง 1 มิติเท่านั้น ซึ่งสามารถทำได้ด้วย layer ที่พื้นฐานที่สุดนั่นคือ Fully Connected layer (Keras เรียก `Dense`) นั่นเอง\n\nสุดท้ายในการการันตีว่า output ของเราจะอยู่ในช่วง [0,1] หรือแทนค่าความน่าจะเป็นที่สัญญาณไฟ 3 เฟสเราจะมีปัญหานั้น เราสามารถทำได้โดยเลือก `activation=sigmoid` ใน `Dense` layer ครับ\n\nเพื่อนๆ ที่เชี่ยวชาญแล้วสามารถลองดัดแปลงเพิ่มเติม Model ได้ด้วยตัวเองครับ เช่น อาจจะเพิ่ม `Dropout` หรือ `BatchNormalization` layers เพื่อให้ได้ผลลัพธ์ดียิ่งขึ้นก็ได้"},{"metadata":{"trusted":true,"_uuid":"289bc7d1ab8048a60025801b457f8df1d848acbc"},"cell_type":"code","source":"def model_lstm(input_shape):\n    \n    # สร้างตัวแปรแทน Input ขึ้นมาซึ่งก็คือ 3D-array ดังที่อธิบายไว้ข้างบน\n    # LSTM รับ 3D array แต่ Keras จะกำหนดให้เราระบุ shape ของ dimension ที่ 2 (TIME_STEPS) และ 3 (D) เท่านั้น\n    inp = Input(shape=(input_shape[1], input_shape[2],))\n    \n    # สร้าง Bi-LSTM layerแรกรับ 3D-array input เข้ามาและส่ง output ในชื่อ x0\n    x0 = Bidirectional(CuDNNLSTM(HID, return_sequences=True))(inp)\n    \n    # สร้าง Bi-LSTM layerที่สองรับ x0 และส่ง output ในชื่อ x\n    x = Bidirectional(CuDNNLSTM(HID, return_sequences=True))(x0)\n\n    # สร้าง 2 pooling layers มาและ concat output ทั้งสองเข้าด้วยกัน\n    x1 = GlobalAveragePooling1D()(x)\n    x2 = GlobalMaxPooling1D()(x)\n    x = Concatenate()([x1,x2])\n    \n    # สร้าง Dense layers ซ้อนกัน 3 ชั้นเพื่อแปลง vectors ให้สุดท้ายอยู่ในรูปความน่าจะเป็น [0,1]\n    x = Dense(64, activation=\"relu\")(x)\n    x = Dense(16, activation=\"relu\")(x)\n    x = Dense(1, activation=\"sigmoid\")(x)\n    \n    # เมื่อสร้าง layers ทั้งหมดเสร็จแล้วก็สร้าง model โดยระบุ input แรก และ output สุดท้าย\n    model = Model(inputs=inp, outputs=x)\n    \n    # เลือกใช้ binary crossentropy loss, Adam optimization, และวัดผลด้วย MCC ตามที่ Kaggle กำหนด\n    model.compile(loss='binary_crossentropy', optimizer=optimizers.Adam(lr=LR), metrics=[matthews_correlation])\n    \n    return model","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 2.3 ทำ Stratified K-Folds เพื่อเรียนรู้และทดสอบความแม่นยำของโมเดล\n\nต่อไปเราจะทำการฝึกฝนหรือสอนโมเดลให้เรียนรู้ในการแยกแยะสัญญาณกระแสไฟฟ้าที่ดีและไม่ดี และเนื่องจากปัญหาของเราเป็นปัญหาที่ยากในสองประเด็น คือ (1) เรามีข้อมูลสอนน้อย และ (2) ในข้อมูลที่น้อยนั้นเรายังมี Imbalance Data ดังนั้นเราจึงจำเป็นต้องออกแบบกระบวนการวัด performance ด้วย Stratified K-Folds \n\nอ่าน[เหตุผลอย่างละเอียดที่เราจำเป็นต้องวัดผลด้วย Stratified K-Folds ได้ที่บทความ ThAIKeras](https://thaikeras.com/community/เกี่ยวกับ-kaggle/การแข่งขัน-vsb-power-line-fault-detection/#post-143)ครับ\n\n![รูปจาก SciKit-Learn](https://i.ibb.co/LrB6kdH/518-CC87-A-8-C35-4-BCD-94-A2-54186-EB94-A64.jpg)\n\nในทางปฏิบัติการทำ Stratified K-Folds สามารถทำได้เพียงบรรทัดเดียวด้วยพลังของ SciKit-Learn Library หลังจากนั้นเราก็เพียงแต่ทำการฝึกโมเดลของเราไป K ครั้ง (กำหนดจากตัวแปร `N_SPLITS`) การฝึกโมเดล K ครั้งนี้แม้นจะเสียเวลาขึ้น K เท่า แต่ก็ทำให้ข้อมูลทดสอบ (validation set) เพิ่มขึ้น K เท่า และทำให้เรามีโมเดลทั้งหมด K โมเดลซึ่งเราสามารถนำโมเดลทั้งหมดมาช่วยกันทำนายให้แม่นยำขึ้นได้ (K-Fold Ensemble)"},{"metadata":{},"cell_type":"markdown","source":"ดังนั้นใน code ข้างล่างนี้เมื่อเราทำ Stratified K-Folds เสร็จแล้วเราก็เพียงวนลูป K ครั้ง เพื่อเรียนรู้และทดสอบความแม่นยำในแต่ละ Fold เซพโมเดลที่ดีที่สุดของแต่ละ Fold ไว้เพื่อจะโหลดขึ้นมาร่วมกันทำนายในภายหลัง\n\nในการพิจารณาว่าโมเดลไหนดีที่สุดนั้น วิธีง่ายๆ ก็คือเราคำนวนค่า MCC ในทุกๆ รอบการสอน (Epoch) และบันทึก update โมเดลที่มีค่า MCC สูงสุดไว้นั่นเองครับ"},{"metadata":{"trusted":true,"_uuid":"8d6f4ca319c383b1b4f671a37c5a324136e7a466"},"cell_type":"code","source":"# ฟังก์ชัน Stratified K-Folds นี้เรา import ขึ้นมาใน cell แรกๆ (ดู Step 0.)\n# input ที่เราต้องระบุคือ N_SPLITS (K) และใส่ Input (X,y) เข้าไปเพื่อ\n# ให้ทำ Stratification ได้ถูกต้องตาม class distribution\nsplits = list(StratifiedKFold(n_splits=N_SPLITS).split(X, y))\n\n# list ที่เราจะเก็บคำทำนายบน validation set และ true labels ไว้เพื่อประเมิน MCC ภายหลัง\npreds_val_list = [] #คำทำนาย\ny_val_list = [] #label จริง\n\n# ในตัวแปร splits จะเป็น list ที่เก็บ index ของ training และ valid ของแต่ละ fold ไว้\n# โดย splits = [(train_idx_fold1, valid_idx_fold1), ..., (train_idx_foldK, valid_idx_foldK)]\n# และคำสั่ง enumerate(['a', 'b', 'c']) returns [(0, 'a'), (1, 'b'), (2, 'c')]\n\n#วนลูป K folds\nfor idx, (train_idx, val_idx) in enumerate(splits):\n    K.clear_session() # เคลียร์หน่วยความจำของ Keras\n    \n    print(\"Beginning fold {}\".format(idx+1))\n    \n    # สร้าง train, valid set จาก index ที่ได้จาก stratified K-Folds\n    train_X, train_y, val_X, val_y = X[train_idx], y[train_idx], X[val_idx], y[val_idx]\n    \n    # สร้าง 2-layer bi-lstm ตามที่ออกแบบไว้\n    model = model_lstm(train_X.shape)\n    \n    # ระบุให้ Keras สร้าง log file\n    csv_logger = CSVLogger('trainlog.csv', append=True, separator=',')\n    \n    # ระบุให้ Keras save model ที่มีค่า Validation MCC สูงที่สุดไว้ในแต่ละ Fold\n    ckpt = ModelCheckpoint('weights_{}.h5'.format(idx), \n                           save_best_only=True, save_weights_only=True, \n                           verbose=1, monitor='val_matthews_correlation', \n                           mode='max')\n    \n    # เรียนรู้ model parameters จากข้อมูล ซึ่งโค้ดเพียง 1 บรรทัด\n    model.fit(train_X, train_y, batch_size=BATCH_SIZE, epochs=EPOCHS, \n              validation_data=[val_X, val_y], \n              callbacks=[ckpt,csv_logger], verbose=0)\n    \n    #เมื่อเรียนรู้เสร็จแล้วให้โหลด parameters ที่ได้ validation MCC สูงสุดที่เซพไว้มาใช้งาน\n    model.load_weights('weights_{}.h5'.format(idx))\n    \n    #นำโมเดลนั้นมาทำนาย \"ความน่าจะเป็น\" ที่สัญญาณไฟจะบกพร่องใน validation set อีกครั้ง \n    preds_1fold = model.predict(val_X, batch_size=512)\n    \n    # คำนวน threshold ที่ดีที่สุด และ valid MCC Score ที่ดีที่สุดใน Fold นี้\n    result = threshold_search(val_y, preds_1fold[:,0])\n    print('optimized threshold is %.2f and have score %.4f' % \n          (result['threshold'], result['matthews_correlation']))\n\n    \n    # เก็บคำทำนายและ label จริงไว้คำนวน threshold พร้อมๆ กันทั้ง KFolds ภายหลัง\n    preds_val_list.append(preds_1fold)\n    y_val_list.append(val_y)\n\n# เปลี่ยน list ให้เป็น numpy array\npreds_val = np.concatenate(preds_val_list)[...,0]\ny_val = np.concatenate(y_val_list)\nprint(preds_val.shape, y_val.shape)\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"เป็นอันเสร็จสิ้นกระบวนการฝึกสอนครับ สังเกตว่าใน Code ข้างบนเราได้คำนวน Validation MCC score และ threshold ของแต่ละ fold ไว้แต่ยังไม่ใช่ final score/threshold เนื่องจากที่อธิบายในบทความไปว่าค่า score ในแต่ละ fold นั้นอาจยังมีนัยยะทางสถิติไม่มากเพราะจำนวนข้อมูลใน validation set มีน้อย ในขั้นตอนถัดไป เราจะคำนวน score/threshold ใหม่พร้อมๆ กันทั้ง K Folds ครับ"},{"metadata":{},"cell_type":"markdown","source":"# Step 3. ประเมินความแม่นยำ และทำนายข้อมูลทดสอบด้วย K-Fold Ensemble\n\n## 3.1 เตรียม Test Data จาก Signal เป็น Vector"},{"metadata":{"trusted":true,"_uuid":"ae9bd3fa9d8c0781c0708846bb7f2a9f9e6cbd3c"},"cell_type":"code","source":"%%time\n# ก่อนอื่นโหลดข้อมูลที่จำเป็นพวก signal_id, measurement_id ของ test set \n# ขึ้นมาเช่นเดียวกับ training set ใน Step 1. ครับ\nmeta_test = pd.read_csv('../input/vsb-power-line-fault-detection/metadata_test.csv')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"3eb186d032f79c99ffba05dd1a7fabb77e13cec5"},"cell_type":"code","source":"meta_test = meta_test.set_index(['signal_id'])\nmeta_test.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"6f8e94387f625bff0a9a6289e1ee038908bc5856"},"cell_type":"code","source":"%%time\n'''\nในขั้นตอนนี้เราต้องทำเตรียมข้อมูล denoised signal แต่ละเฟสใน test set ให้เป็น \nVector ของสัญญาณ 3เฟสเช่นเดียวกับที่ทำในหัวข้อ 1.2 ซึ่งรายละเอียดจะคล้ายๆ กัน\nและให้เพื่อนๆ อ่านโค้ดเองเป็นแบบฝึกหัด\n\nสิ่งที่แตกต่างคือข้อมูลใน test set นั้นเยอะกว่า training set มากทำให้เราจำเป็นต้อง\nแบ่งข้อมูลออกเป็น 10 ส่วนด้วยกันเพื่อให้คำนวนได้ใน RAM ที่จำกัด\n'''\n\nfirst_sig = meta_test.index[0]\nn_parts = 10\nmax_line = len(meta_test)\npart_size = int(max_line / n_parts)\nlast_part = max_line % n_parts\nprint(first_sig, n_parts, max_line, part_size, last_part, n_parts * part_size + last_part)\n\n# Here we create a list of lists with start index and end index for each of the 10 parts and one for the last partial part\nstart_end = [[x, x+part_size] for x in range(first_sig, max_line + first_sig, part_size)]\nstart_end = start_end[:-1] + [[start_end[-1][0], start_end[-1][0] + last_part]]\nprint(start_end)\nX_test_raw = []\n\n# now, very like we did above with the train data, we convert the test data part by part\n# transforming the 3 phases 800000 measurement in matrix (160,57)\nfor start, end in start_end:\n    subset_test = pq.read_pandas('../input/vsb-wavelet-denoised/test_dn.parquet', columns=[str(i) for i in range(start, end)]).to_pandas()\n    for i in tqdm(subset_test.columns):\n        id_measurement, phase = meta_test.loc[int(i)]\n        subset_test_col = subset_test[i]\n        subset_trans = transform_ts(subset_test_col)\n        X_test_raw.append([i, id_measurement, phase, subset_trans])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# ยุบรวมข้อมูล vector ของทั้ง 3 เฟสให้เป็น 1 vector\nX_test_dn = np.asarray([np.concatenate([X_test_raw[i][3],X_test_raw[i+1][3], X_test_raw[i+2][3]], axis=1) for i in range(0,len(X_test_raw), 3)])\nnp.save(\"X_test_dn.npy\",X_test_dn)\nX_test_dn.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# ในส่วนของ Test Signal Data ประเภท Raw เราก็โหลดข้อมูลที่แปลงเป็น vector สำเร็จแล้ว\n# ขึ้นมาใช้งานเช่นเดียวกับกรณี Training Data\nX_test_orig = np.load('../input/5-fold-lstm-attention-fully-commented-0-694/X_test.npy')\nprint(X_test_orig.shape)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"if SIGNAL_TYPE == 'raw':\n    X_test = X_test_orig\nelif SIGNAL_TYPE == 'denoised':\n    X_test = X_test_dn\nelse: # SIGNAL_TYPE = 'both'\n    X_test = np.concatenate([X_test_orig,X_test_dn],axis=-1)\n    \nprint(SIGNAL_TYPE)\nprint(X_test.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## 3.2 วัดประสิทธิภาพ Validation MCC Score\n\nเมื่อเตรียม Test Data ในรูป Vector เสร็จสิ้นแล้ว เราก็จะนำ K Models ของเรามาทำนายสัญญาณ 3 เฟสกันครับ\n\nก่อนอื่นต้องหา Threshold ที่ดีที่สุดรวมทั้ง MCC Score บนทั้ง K-Folds เพื่อประเมิน Performance เบื้องต้นกันก่อน"},{"metadata":{"trusted":true,"_uuid":"af9aa6b2b8f8a2beda1a02ff998e3072fcad8d06"},"cell_type":"code","source":"result = threshold_search(y_val, preds_val)\nbest_threshold = result['threshold']\nprint(result['threshold'],result['matthews_correlation'])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ทั้งนี้เนื่องจากผลการรันแต่ละครั้งจะมี randomness อยู่บ้าง ทำให้ผมไม่ทราบ Valid MCC Scores ของเพื่อนๆที่รัน Workshop นี้แต่ละครั้ง อย่างไรก็ตามค่า Valid MCC Score จะอยู่ในช่วง 0.65-0.7 ซึ่งเป็นตัวเลขที่บ่งบอกความแม่นยำในการทำนายราวๆ 65%-70% และเป็นตัวเลขที่น่าพอใจเมื่อเทียบกับ performance ของผู้เข้าแข่งขัน VSB ทั้งหมดครับ (อันดับ 1 ของโลกทำ MCC Score ได้ราวๆ 0.718 และมีผู้ที่ได้คะแนน MCC เกิน 0.7 เพียงสองคนจาก AI Scientists ทั่วโลกราวๆ เกือบ 1500 ทีมทั่วโลก)\n\n*หมายเหตุ : * อย่างไรก็ดีใน Test Set นั้น[เป็นที่รู้กันว่าในการแข่งครั้งนี้ ได้เก็บข้อมูลมาจากคนละแหล่งกับ Training Data](https://www.kaggle.com/c/vsb-power-line-fault-detection/discussion/87150#latest-504122) ทำให้คะแนน Performance บน Test Set จะอยู่ที่ราว 0.62-0.66 ครับ (ดูได้จาก LeaderBoard)"},{"metadata":{},"cell_type":"markdown","source":"## 3.3 ทำนายผลลัพธ์บน Test Data ด้วย K-Fold Ensemble\n\nในขั้นตอนสุดท้ายนี้ ก็เหลือเพียงแค่นำ K Models ที่เราได้สอนจาก Training Data มาร่วมกันทำนายใน Test Data ที่แปลงเป็น Vector เรียบร้อยแล้วครับ\n\nวิธีการ K-Fold Ensemble ที่ง่ายที่สุดที่เราจะใช้ใน workshop นี้ก็คือเราจะให้ทั้ง K models ทำนายความน่าจะเป็นของสัญญาณ 3เฟส แต่ละสัญญาณใน Test Data ขึ้นมา ซึ่งจะทำให้เราได้ค่า \"ความน่าจะเป็น\" มาทั้งหมด K ค่า จากนั้นเราก็จะ \"เฉลี่ย\" ค่าความน่าจะเป็นทั้ง K นี้เพื่อให้ได้ความน่าจะเป็นสุดท้าย ที่จะบ่งชี้ว่าสัญญาณดังกล่าวเป็นสัญญาณที่บกพร่องหรือไม่\n\nและในการเปลี่ยนจาก \"ความน่าจะเป็น\" ไปเป็นคำตอบ class 0 หรือ 1 นั้นก็จะตัดสินกันที่ค่า \"ความน่าจะเป็น\" นั้นมีค่ามากกว่าตัวแปร `best_threshold` ที่เราหามาในหัวข้อ 3.2 หรือไม่\n\nนอกจากนี้มีขั้นตอนสุดท้ายก็คือ เนื่องจากในการแข่งครั้งนี้กำหนดให้ทำนายสัญญาณแยกกันในแต่ละเฟส แต่ในกระบวนการแก้ปัญหาที่เราออกแบบนั้นใช้ความรู้ที่ว่าสัญญาณทั้ง 3เฟสมักจะมี class เดียวกัน และทำให้เรายุบ 3เฟสเป็นเพียง  1คำทำนาย \n\nดังนั้นก่อนที่เราจะส่งผลลัพธ์ไปตรวจสอบความแม่นยำ เราจึงจำเป็นต้อง copy ผลการทำนายข้อมูล 3เฟส ของเราออกเป็น 3ชุด (มีความหมายว่า เราทาย class 0 หรือ 1 เหมือนกันทั้ง 3 เฟส) ซึ่งการ copy ผลลัพธ์นี้ทำได้โดยใช้ฟังก์ชันของ numpy ที่ชื่อว่า `np.repeat` ดังแสดงในโค้ดครับ"},{"metadata":{"trusted":true,"_uuid":"2f7342296138f6bfd3e9cedd029e1035de3b98fc"},"cell_type":"code","source":"preds_test_prob = []\nfor i in range(N_SPLITS):\n    model.load_weights('weights_{}.h5'.format(i))\n    pred = model.predict(X_test, batch_size=300, verbose=1)\n    pred_3 = np.repeat(pred,3)\n\n    preds_test_prob.append(pred_3)\n\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# ณ จุดนี้เรามีคำนายค่าความน่าจะเป็นของ data แยกเฟสกันทั้งหมด K ชุด\nprint(np.array(preds_test_prob).shape)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"bb95b819b269fb1117b2c94ac4ddd65d8a0f96bc"},"cell_type":"code","source":"# เฉลี่ย \"ความน่าจะเป็น\" ของทั้ง K model เข้าด้วยกัน\npreds_test_prob = np.squeeze(np.mean(preds_test_prob, axis=0))\n\nnp.save('preds_test_prob.npy',preds_test_prob)\nprint(preds_test_prob.shape)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"736bdcf4c3f1e36271403c2b84f4defd6500923c"},"cell_type":"code","source":"# ตัดสินว่าเป็น class 0 หรือ 1 จากค่าความน่าจะเป็นด้วย best_threshold\n# ที่ประเมินมาจาก validation dataset\npreds_test = (preds_test_prob > best_threshold).astype(np.int)\n\nprint(preds_test.shape)\n\n# ดูว่าเราทำนาย class 1 ทั้งหมดกี่ตัวอย่าง\nprint(sum(preds_test))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"เสร็จเรียบร้อยแล้วนะครับ ที่เหลือเป็นกระบวนการส่งคำทำนายในรูปแบบไฟล์ CSV ตามที่ Kaggle กำหนดไว้"},{"metadata":{"trusted":true,"_uuid":"cfd265d3e07c4cc1679d2c4d55fe7de631c813e7"},"cell_type":"code","source":"submission = pd.read_csv('../input/vsb-power-line-fault-detection/sample_submission.csv')\nprint(len(submission))\nsubmission.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"9f76c471eaf983707d446c5081ab3d50c4e40ea5"},"cell_type":"code","source":"submission['target'] = preds_test\nsubmission.to_csv('submission.csv', index=False)\nsubmission.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ไฟล์ `submission.csv` เป็นคำตอบสุดท้ายของเราซึ่งเพื่อนๆ สามารถลองกด submission หลังจากที่เสร็จสิ้นกระบวนการดูได้ว่าได้คะแนน MCC ใน test data เท่าไรครับ การทำงานทั้งหมดของโค้ดใน Notebook นี้จะใช้เวลาเกือบๆ 1 ชั่วโมงครับ\n\nสุดท้ายนี้เพื่อความสนุก เราจะลองทำนายผลบน Test Data ด้วย threshold อื่นๆ ที่ไม่ใช่ `best_threshold` กันดูด้วยครับ ทำให้เราจะสร้างไฟล์ submission ของแต่ละ threshold ขึ้นมา ซึ่งเพื่อนๆ ลองส่งไปตรวจสอบค่า MCC ของแต่ละ threshold กันได้ว่ามีค่าเท่าไร และการเปลี่ยน threshold นั้นส่งผลต่อ performance สุดท้ายอย่างไรครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"# do multi threshold here [see my Tarun-General code]\nsubmission25 = submission.copy()\nsubmission30 = submission.copy()\nsubmission40 = submission.copy()\nsubmission55 = submission.copy()\nsubmission60 = submission.copy()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"preds_test25 = (preds_test_prob > 0.25).astype(int)\npreds_test30 = (preds_test_prob > 0.30).astype(int)\npreds_test40 = (preds_test_prob > 0.40).astype(int)\npreds_test55 = (preds_test_prob > 0.55).astype(int)\npreds_test60 = (preds_test_prob > 0.60).astype(int)\n\nsubmission25['target'] = preds_test25\nsubmission30['target'] = preds_test30\nsubmission40['target'] = preds_test40\nsubmission55['target'] = preds_test55\nsubmission60['target'] = preds_test60\n\nsubmission25.to_csv('submission25.csv', index=False)\nsubmission30.to_csv('submission30.csv', index=False)\nsubmission40.to_csv('submission40.csv', index=False)\nsubmission55.to_csv('submission55.csv', index=False)\nsubmission60.to_csv('submission60.csv', index=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# บทส่งท้าย\nWorkshop นี้ก็จบกันไปแล้วนะครับ อาจจะรายละเอียดเยอะก็เนื่องมาจากการแก้ปัญหาในข้อมูลจริง จะมีกระบวนการที่จุกจิกเช่นนี้เองครับ  ถ้าเพื่อนๆ มีคำถามอย่างไร สามารถถามได้ทุกเรื่องใน comments ข้างล่างหรือ[ในบอร์ดของเราครับ](https://thaikeras.com/community/เกี่ยวกับ-kaggle/การแข่งขัน-vsb-power-line-fault-detection/) \n\nThAIKeras.com คือชุมชนแลกเปลี่ยนความรู้ AI, และทีมงานเป็น Professional AI Consultant ยินดีให้บริการครับ"},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}