ใน ep นี้ เราจะมาเรียนรู้วิธีการใช้ Deep Learning โดยสร้างโมเดล Deep Neural Network ขึ้นมา แล้วเราป้อนข้อมูลให้ว่า รูปนี้ คือสายพันธุ์นี้ รูปนี้ คือสายพันธุ์นี้ รูปนี้ คือสายพันธุ์นี้ …

แล้วให้โมเดลเรียนรู้ด้วยตัวเอง (Machine Learning) และจนสามารถทำนายสายพันธุ์ หมา และ แมว Image Classification จากรูป ที่โมเดลไม่เคยเห็นมาก่อนได้

เรามาเริ่มกันเลยดีกว่า

Open In Colab

0. Magic Commands

ให้ใส่ไว้บนสุดทุก Notebook เป็นการสั่งให้ Notebook ก่อนรัน ให้รีโหลด Library ภายนอกที่เรา import ไว้ใหม่โดยอัตโนมัติ

และให้พล็อตกราฟ matplotlib ใน Output ของ cell แบบ code ได้เลย

In [0]:
%reload_ext autoreload
%autoreload 2
%matplotlib inline

1. Import Library

Import Library ที่เราจะใช้ ในที่นี้คือ fastai version 1, fastai มี package ย่อย ๆ หลายอัน แต่ที่เราจะใช้ใน Notebook นี้คือ vision สำหรับงานเกี่ยวกับ Computer Vision และ metrics สำหรับวิเคราะห์ประสิทธิภาพของ Model

การ import * หมายความว่า import ทุกอย่างที่อยู่ใน package ทำให้เราไม่ได้ต้องมา import ทีละ class การ import แบบนี้ เหมาะสำหรับการทดลองอะไรใหม่ ๆ เพราะเราไม่ต้องย้อนมาแก้ import ทุกครั้งเมื่อต้องการใช้ class ใหม่ ๆ แต่ไม่แนะนำสำหรับใช้งานจริงบน Production

In [0]:
 !curl -s https://course.fast.ai/setup/colab | bash
Updating fastai...
Done.
In [0]:
from fastai import *
from fastai.vision import *
from fastai.metrics import accuracy

2. ข้อมูล

ในเคสนี้ เราจะใช้ข้อมูลจาก Oxford-IIIT Pet Dataset by O. M. Parkhi et al., 2012 ซึ่งเป็นชุดข้อมูลรูปภาพหมา 25 พันธุ์ และรูปแมว 12 พันธุ์ รวมเป็น 37 หมวดหมู่

อ้างอิงจากใน paper ในปี 2012 โมเดลที่ดีที่สุด สามารถทำนายพันธุ์สัตว์ ได้ถูกต้อง 59.21% โดยโมเดลนั้น ออกแบบมาเฉพาะทาง สำหรับแยกรูปสัตว์ หัวสัตว์ ตัวสัตว์ โดยเฉพาะ (สมัยนันยังไม่ใช่ Deep Learning)

เรามาดูกันว่าเราจะใช้ Deep Learning ทำนายพันธุ์หมาแมวจากรูป ได้ถูกต้องแม่นยำกว่าหรือไม่

URL ของ Dataset ที่เราจะ Download มาใช้

In [0]:
URLs.PETS
Out[0]:
'https://s3.amazonaws.com/fast-ai-imageclas/oxford-iiit-pet'

ฟังก์ชัน untar_data จะดาวน์โหลด Dataset และ Extract ไฟล์ให้เราอัตโนมัติ ไว้ใน $HOME/.fastai/data เวลาเรารันใหม่ จะได้ไม่ต้อง Download ใหม่ทุกครั้ง เมื่อแตกไฟล์ออกมาแล้ว ก็ ls ดูว่ามีโฟล์เดอร์อะไรอยู่บ้าง

In [0]:
path = untar_data(URLs.PETS)
path.ls()
Out[0]:
[PosixPath('/content/data/oxford-iiit-pet/images'),
 PosixPath('/content/data/oxford-iiit-pet/annotations')]

ที่เราสนใจตอนนี้มี 2 path คือ images ที่เก็บรูป และ annotations ที่เก็บข้อมูลประกอบ

In [0]:
path_annotations = path/'annotations'
path_images = path/'images'

ลอง ls ดูใน path_annotations จะเห็นว่ามีไฟล์ข้อมูลประกอบรูปหลายไฟล์ด้วยกัน

In [0]:
path_annotations.ls()
Out[0]:
[PosixPath('/content/data/oxford-iiit-pet/annotations/README'),
 PosixPath('/content/data/oxford-iiit-pet/annotations/._trimaps'),
 PosixPath('/content/data/oxford-iiit-pet/annotations/trimaps'),
 PosixPath('/content/data/oxford-iiit-pet/annotations/trainval.txt'),
 PosixPath('/content/data/oxford-iiit-pet/annotations/xmls'),
 PosixPath('/content/data/oxford-iiit-pet/annotations/test.txt'),
 PosixPath('/content/data/oxford-iiit-pet/annotations/list.txt')]

Label ชื่อพันธุ์ อยู่ในชื่อไฟล์

ดึงรายชื่อไฟล์จาก path_images และแสดงรายการ 10 ไฟล์แรก

สังเกตว่าทั้งหมดเป็นรูปนามสกุล jpg อยู่ใน path ด้วยกัน ชื่อไฟล์เป็นชื่อพันธุ์ ตามด้วย _ ตามด้วย เลข Running ตามด้วย .jpg

In [0]:
filenames = get_image_files(path_images)
filenames[:10]
Out[0]:
[PosixPath('/content/data/oxford-iiit-pet/images/wheaten_terrier_77.jpg'),
 PosixPath('/content/data/oxford-iiit-pet/images/leonberger_68.jpg'),
 PosixPath('/content/data/oxford-iiit-pet/images/german_shorthaired_107.jpg'),
 PosixPath('/content/data/oxford-iiit-pet/images/basset_hound_165.jpg'),
 PosixPath('/content/data/oxford-iiit-pet/images/scottish_terrier_59.jpg'),
 PosixPath('/content/data/oxford-iiit-pet/images/english_setter_113.jpg'),
 PosixPath('/content/data/oxford-iiit-pet/images/english_cocker_spaniel_61.jpg'),
 PosixPath('/content/data/oxford-iiit-pet/images/miniature_pinscher_55.jpg'),
 PosixPath('/content/data/oxford-iiit-pet/images/pug_153.jpg'),
 PosixPath('/content/data/oxford-iiit-pet/images/Bengal_27.jpg')]

3. เตรียมข้อมูล

ก่อนนำข้อมูลใด ๆ มาเทรน Model เราต้องมีการเตรียมข้อมูล กำหนด Data Pipeline ให้อยู่ในรูปแบบที่เหมาะสมเสียก่อน

batchsize หมายถึงให้ป้อนข้อมูลให้ Model ทีละ 64 records, GPU สามารถทำงานได้รวดเร็ว เพราะ ข้อได้เปรียบของ GPU คือมี Core จำนวนมาก การป้อนข้อมูลรูปแบบคล้าย ๆ กันให้ GPU ไปทำงานพร้อม ๆ กันหลาย ๆ Record ในทีเดียว จะเร็วกว่าใช้ CPU ทำงานไปทีละ Record มากกว่า 100 เท่า

random seed หมายถึง กำหนดค่าเริ่มต้นของการสุ่ม เพื่อให้รันทุกครั้งแล้วได้เหมือนเดิมทุกครั้ง จะได้สะดวกในการเปรียบเทียบ แต่ไม่แนะนำสำหรับงาน Production

regexpattern หมายถึง เราจะใช้ Regular Expression ตัดชื่อไฟล์ดูจากท้ายสุด ($), ดู .jpg, ดูตัวเลข (d+), ดู แล้วเอาตัวอักษรทั้งหมด ก่อนถึง / มาเป็น label ชื่อพันธุ์

In [0]:
batchsize = 64
np.random.seed(42)
regex_pattern = r'/([^/]+)_\d+.jpg$'

from_name_re คือ Factory Method กำหนด Data Pipeline ในการสร้าง DataBunch โดยใช้ regex_pattern (Regular Expression) จาก ชื่อไฟล์มาเป็น labe

  • ds_tfms คือ Dataset Transforms การแปลงข้อมูลรูปภาพ เช่น ย่อขยาย crop ปรับสี ความสว่าง etc. จะพูดถึงในเรื่อง Data Augmentation ที่ไว้เราจะอธิบายต่อไป แต่ตอนนี้ให้ใช้ Default จาก get_transforms ไปก่อน
  • size คือ resize รูปทั้งหมดให้เป็น 224 x 224 pixels
  • bs คือ batchsize
  • normalize คือการแปลงข้อมูล ตาม imagenet_stats จากรูปสี จาก 0-255 x 3 channels RGB ให้มี Mean = 0 และ Standard Deviation = 1 ที่ไว้เราจะอธิบายต่อไป

ที่ไม่ได้ใส่เป็น parameter ได้แก่ valid_pct คือ ค่า Percent สำหรับ แบ่ง train/valid split ซึ่งค่า Default คือ 20%

In [0]:
databunch = ImageDataBunch.from_name_re(path_images, filenames, 
                                   regex_pattern, ds_tfms=get_transforms(), 
                                   size=224, bs=batchsize).normalize(imagenet_stats)

เราจะได้ DataBunch บรรจุ train Dataset จำนวน 5912 record และ validation Dataset จำนวน 1478 record หรือ 1478 / (1478+5912) = 20%

In [0]:
len(databunch.train_ds), len(databunch.valid_ds)
Out[0]:
(5912, 1478)

สำรวจข้อมูล

เรียกข้อมูลจาก DataLoader ที่อยู่ใน DataBunch ลองดูข้อมูล Batch แรก ด้วย show_batch สั่งให้แสดง รูป พร้อม label ชื่อพันธุ์ไว้บนรูป แสดง 3 แถว (rows) ในกรอบขนาด 9 x 9 นิ้ว (figsize)

เราสามารถรัน cell นี้หลายครั้ง เพื่อเรียกดู batch ต่อ ๆ ไป ได้เรื่อย ๆ เป็นการสำรวจข้อมูล

In [0]:
databunch.show_batch(rows=3, figsize=(9, 9))

ดู label พันธุ์หมาแมวทั้งหมด หรือ class ทั้งหมด ที่มีใน Dataset

In [0]:
print(databunch.classes)
['Abyssinian', 'Bengal', 'Birman', 'Bombay', 'British_Shorthair', 'Egyptian_Mau', 'Maine_Coon', 'Persian', 'Ragdoll', 'Russian_Blue', 'Siamese', 'Sphynx', 'american_bulldog', 'american_pit_bull_terrier', 'basset_hound', 'beagle', 'boxer', 'chihuahua', 'english_cocker_spaniel', 'english_setter', 'german_shorthaired', 'great_pyrenees', 'havanese', 'japanese_chin', 'keeshond', 'leonberger', 'miniature_pinscher', 'newfoundland', 'pomeranian', 'pug', 'saint_bernard', 'samoyed', 'scottish_terrier', 'shiba_inu', 'staffordshire_bull_terrier', 'wheaten_terrier', 'yorkshire_terrier']

c จำนวน label ที่จะนำไปใช้ใน model

In [0]:
print(databunch.c)
37

จำนวนพันธุ์ใน dataset

In [0]:
len(databunch.classes)
Out[0]:
37

4. สร้างโมเดลแรก

สร้าง CNN Learner ด้วยฟังก์ชัน cnn_learner ระบุ databunch ของข้อมูลที่จะใช้เทรน, Model Architecture, ต้องการ Transfer Learning หรือไม่, metrics การวัดผลในเคสนี้เราจะใช้ Accuracy

  • โดย default cnn_learner จะลบ layer สุดท้ายของ model ที่ดาวน์โหลดมาทิ้งไป และแทนที่ด้วย Dense Layer ใหม่ ที่มี 37 Output ตรงกับ databunch.c ของเรา
  • model จะถูก Save ไว้ที่ $HOME/.torch/models เมื่อรันอีกครั้งจะไม่ได้ต้องดาวน์โหลดใหม่
In [0]:
learner = cnn_learner(databunch, models.resnet34, metrics=accuracy)

5. เริ่มต้นเทรนโมเดล

ลอง fit ด้วย ค่า default จำนวน 4 epoch

  • 1 epoch คือ ใช้ป้อนข้อมูล หมด Dataset 1 รอบ
  • fit_one_cycle คือ การเทรนแบบพิเศษ ที่ใช้ Learning Rate ไม่คงที่ ไว้เราจะอธิบายต่อไป
In [0]:
learner.fit_one_cycle(4)
epochtrain_lossvalid_lossaccuracytime
01.4332750.3436220.88633301:46
10.6278830.3031660.90257101:45
20.4009920.2271940.92692801:45
30.2879750.2256900.92828101:45

สำเร็จแล้ว

เราเทรนด้วย GPU เพียงแค่เวลาไม่ถึง 3 นาที เราเทรน Model ได้ accuracy ประมาณ 0.93 หรือ ความแม่นยำประมาณ 93%

เปรียบเทียบกับ paper เมื่อปี 2012 ที่ได้ 59.21% หรือเปรียบเทียบกับมนุษย์ คงเป็นเรื่องยาก ถ้าให้เวลาเรา 3 นาที ดูรูปหมาแมว แล้วให้ทายว่าเป็นพันธุ์อะไร 1 ใน 37 สายพันธุ์ แต่ Model Deep Learning ของเรา นี้สามารถทายได้ถูกต้องถึง 93% โดยใช้เวลาในการเรียนรู้เพียงแค่ 3 นาทีเท่านั้น

เรามา Save Model ที่เราเพิ่งเทรนไปเก็บไว้ก่อน

In [0]:
# learner.save('01a-image-classification-pets-resnet34-1')

6. ดูสถิติของโมเดล

เรามาดูกราฟ Train Loss และ Validation Loss ตามจำนวน Batch ข้อมูลที่เราป้อนให้โมเดล

In [0]:
learner.recorder.plot_losses()

กราฟค่า Learning Rate ตามจำนวน Iteration ของการเทรน จะเห็นได้ว่า Learning Rate ไม่คงทีมีขึ้นมีลง เป็นความพิเศษของ fit_one_cycle ที่ไว้เราจะอธิบายต่อไป

In [0]:
learner.recorder.plot_lr()

เรามาดูกราฟ accuracy ตามจำนวน Batch ข้อมูลที่เราป้อนให้โมเดล ไปสูงสุดประมาณ 92.5%

In [0]:
learner.recorder.plot_metrics()

โหลดโมเดลที่เรา Save ไว้ ขึ้นมาใหม่ เตรียมเทรนในขึ้นตอนถัดไป (ที่คอมเม้นท์ไว้ เพราะถ้าไม่ได้เปลี่ยนอะไรก็จะได้ไม่ต้องรัน)

In [0]:
# learner.load('01a-image-classification-pets-resnet34-1')

7. ดูผลลัพธ์

ถ้าเราดูแค่ accuracy อย่างเดียว เราอาจจะไม่เห็นภาพว่า Model ทำงานได้ผลลัพธ์อย่างไร

เราจะสร้าง ClassificationInterpretation เป็น class ที่มาช่วยตีความผลลัพธ์ ให้เราดูเข้าใจง่ายขึ้น

In [0]:
interpretation = ClassificationInterpretation.from_learner(learner)

สั่งให้ plot_top_losses คือ แสดง record ที่ ค่า loss มากที่สุด หรือพูดง่าย ๆ ว่าแสดงรายการที่โมเดลทายผิด 9 อันดับแรก

In [0]:
interpretation.plot_top_losses(9, figsize=(9,9), heatmap=True)