Object detection

Source: Deep Learning on Medium


Step-by-step DIY

ไม่ได้เขียนบล็อกมานาน รับปีใหม่ส่งท้ายวันเด็กซักหน่อยด้วยเรื่อง object detection สิ่งที่เราจะมาดูวันนี้คือสมมติว่าเราอยากจะทำพวก object detection ขึ้นเองเลย จะทำไงดี ถ้าเราพอจะเข้าใจแล้ว จะไปแกะโค้ดต่าง ๆ ใน github ก็น่าจะง่ายขึ้น (มั๊ง 555) โพสนี้เป็นกึ่ง ๆ โน๊ตส่วนตัวของผมด้วยจะได้ไม่ลืม

หลัก ๆ ที่ผมใช้คือ Tensorflow+Keras (อีกแล้ว)

Warm up: MNIST และ CNN

(ใครทำเป็นแล้วผ่านได้)

เริ่มจากฐานข้อมูล classic คือ MNIST ก่อน เริ่มจากการโหลด data ตามปกติ คือ

จากนั้นลองสร้าง CNN ง่าย ๆ ดูก่อน เช่น

ได้ค่า accuracy บน test set ประมาณ 98%

Activation ชั้น output และ loss function

สิ่งที่น่าสนใจคือ activation บนชั้น output ซึ่งก็คือ softmax ซึ่งไม่เปลี่ยนลำดับของค่าที่ได้จากการประมวลผลเบื้องต้น คำถามคือในเมื่อสุดท้ายเราเลือกโหนดที่ให้ค่าส่งออกสูงสุด ถ้าลำดับไม่ถูก softmax เปลี่ยน แล้วเราจะทำ softmax ไปทำไม?

คำตอบคือเพราะตัว loss function ที่ใช้ คือ categorical cross-entropy นั้นต้องการให้ค่าที่ส่งออกจาก CNN อยู่ในรูปความน่าจะเป็น ซึ่งค่าส่งออกจาก softmax นั้นเหมาะกับการตีความแบบนี้

สังเกตต่อว่า loss นั้นใช้เฉพาะในตอน train เท่านั้น ในการใช้งานนั้น loss เป็นสิ่งที่ไม่จำเป็น ดังนั้นหากเราเก็บ softmax ไว้มันก็จะทำให้การประมวลผลของ network นี้ช้าลง วิธีที่น่าจะดีกว่าคือทำ softmax เฉพาะตอน train เท่านั้น ซึ่งเราสามารถทำได้โดยดันการคำนวณ softmax นี้ไปไว้ใน loss function แทน ตามตัวอย่างข้างล่างนี้ ซึ่งผลที่ได้ก็เท่ากันกับ model ข้างบน คือประมาณ 98%

Training set ใหม่

OK ตอนนี้เราเริ่มเข้าเรื่องกันดีกว่า เราจะลองเอาภาพตัวเลขจาก MNIST ไปสุ่มวางในภาพที่ใหญ่ขึ้น เช่น 140×140 และเราจะลองหาวิธี detect ตำแหน่งของตัวเลขนี้ไปพร้อม ๆ กับ classify

สำหรับตำแหน่งของตัวเลขนั้น เราจะระบุเป็นกรอบ หรือ bounding box ของมัน โดย แต่ละกรอบประกอบด้วยค่า 4 ค่า คือตำแหน่ง (x,y) ตรงกลางของกรอบนี้ และความกว้าง (w) และสูง (h) ของกรอบ เพื่อให้ CNN ที่สร้างนั้นใช้งานได้กับรูปใด ๆ เราจะทำการ normalize ค่าทั้ง 4 (x,y,w,h) นี้ด้วยขนาดของภาพ เวลาใช้งานเราค่อยคูณคืนเพื่อให้ได้ตำแหน่งที่แท้จริง

CNN และ Loss สำหรับข้อมูลใหม่

สังเกตว่าข้อมูล train ใหม่นี้ (XX,YY) นั้นมีขนาดภาพนำเข้าคือ (140,140,1) และจำนวนค่าที่ต้องทายคือ 14 ค่า (4 สำหรับ bounding box และ 10 สำหรับ class) เราสามารถสร้าง CNN โดยใช้ constructor “Model” ที่มี output 2 หัวได้ โดยเราสามารถกำหนด loss ของแต่ละหัวให้ต่างกันออกไปได้ 
แต่ในงานนี้เราจะใช้ trick คล้าย ๆ ข้างบนนั่นคือเราจะส่งค่าออกแบบ linear แล้วไปแก้ใน loss function แทน วิธีนี้ยืดหยุ่นกว่าในการเพิ่มจำนวนกรอบที่เราต้องการทายภายหลัง

สังเกตว่าในกรณีนี้ loss function จะวุ่นวายกว่าครั้งแรก นั่นคือเราเริ่มจากการ split ข้อมูล ทั้ง ground truth (y_true) และ prediction (y_pred) ออกเป็น 2 ส่วนคือส่วน bounding box (4 ค่าแรก) และส่วน class (10 ค่าหลัง) 
ในโค้ดนี้เราใช้ … หรือ ellipsis ช่วยในการเขียน ผมเจอ trick นี้ตอนแกะโค้ดคนอื่น เจ้า … นี้ช่วยให้การเขียนนั้นง่ายขึ้น อย่างในโค้ดนี้จริง ๆ แล้ว shape ของ y_true คือ (8,14) เพราะเราตั้ง batch_size=8 แต่ถ้าเรา hard code 8 ลงไปเลย หากเราเปลี่ยน batch_size ภายหลังมันก็จะผิด ต้องไปไล่แก้

หลังจากที่เราแบ่ง y_pred ออกเป็น pred_bbox และ pred_cls แล้ว เราเอา pred_cls ไปผ่าน softmax แบบในตัวอย่างข้างบน 
เราเอา pred_bbox ไปผ่าน sigmoid เพราะเรากำหนดให้ค่าของกรอบ (x,y,w,h) นั้นเป็นค่า relative เทียบกับขนาดภาพ ดังนั้นค่าพวกนี้จะอยู่ระหว่าง 0–1 เท่านั้น

ทดลองใช้งาน

เราลอง train ซัก 5 หรือ 10 epochs ก็ได้ model ที่พอใช้งานได้แล้ว วิธีใช้งานก็ตามโค้ดข้างล่าง สำหรับ bounding box เราต้องแปลงค่าผ่าน sigmoid ก่อนจากนั้นจึงคูณด้วยขนาดถึงจะได้ค่าตำแหน่งที่ต้องการ ในตัวอย่างข้างล่างนี้กรอบจริงจะแสดงเป็นสีขาว ในขณะที่กรอบที่ทายเป็นสีเทา คลาสที่ทายนั้นแสดงไว้ล่างขวาของรูป ดูแล้วผลก็ไม่แย่เท่าไร

Multiple box prediction

ในทางปฏิบัติเราไม่รู้ว่าในภาพมี object จำนวนเท่าไร ดังนั้นระบบต่าง ๆ จึงทายจำนวนกรอบเยอะไว้ก่อน โดยแต่ละกรอบจะมีตัวเลขเพิ่มอีกตัว คือความมั่นใจ (confidence) ว่าภายในกรอบนี้เป็น object จริง
ในตอนใช้งานระบบนั้นเราจะพิจารณาเฉพาะกรอบที่มีค่า confidence นี้สูงกว่า threshold ที่ตั้ง
คำถามคือ แล้วเราจะปรับ CNN และ train มันอย่างไรดี? เนื่องจากระบบการ train neural network โดยปกตินั้นต้องการให้ค่าที่เราทายมีจำนวนเท่ากับเฉลย หากเราทายกรอบมากกว่าเฉลยเราก็ต้องหาเฉลยบางอย่างมาแปะให้กับกรอบที่เกินนี้

ในย่อหน้านี้เราจะสมมติว่าระบบทาย 2 กรอบ แต่มีวัตถุจริงแค่อันเดียว
2 กรอบแปลว่าค่าที่ CNN ต้องส่งออกมี 30 ค่า (แต่ละกรอบมี 4 ค่าสำหรับตำแหน่ง 1 ค่า confidence และ 10 ค่าสำหรับคลาสต่าง ๆ)

วิธีแก้ของเราคือ เราจะสร้างข้อมูลสอนทีละ batch ต่างจากก่อนนี้ที่เราสร้างทีเดียวแล้วสั่ง fit คราวนี้เราจะสั่ง train_on_batch แทน

นอกจากนี้สำหรับแต่ละตัวอย่างเราจะเลือกก่อนว่ากรอบใดคือกรอบที่ใกล้กับเฉลยมากที่สุด โดยกรอบที่ใกล้สุดนี้จะถูก train เพิ่มค่า confidence และปรับ bounding box กับ class คล้ายกับตัวอย่างก่อนนี้ ส่วนกรอบอื่นนั้นเราจะให้ค่า target confidence เป็น 0 และกำหนดใน loss ว่าจะไม่เอามาพิจารณา

ในกรณีนี้ข้อมูลในอาเรย์ YY ที่เราเก็บจะไม่ใช่ค่า target ตรง ๆ แต่เราจะเอามันมาประกอบเพื่อสร้าง training batch 
ในโค้ดข้างล่างนี้ IOU คือ intersection over union หรืออัตราส่วนระหว่าง area ที่เป็น intersection ของ 2 bounding box หารด้วย area รวมของกรอบทั้งสอง (ให้ไปลองเขียนเอง เป็นแบบฝึกหัด)
ในกรณีที่ IOU สูงสุดมีค่าต่ำมาก เราจะ assign กรอบแบบสุ่มแทน

สังเกตว่าเราเก็บ index ของตัวอย่างไว้ใน YY เพื่อใช้ในการสร้าง training batch ภายหลัง
โค้ดสำหรับสร้าง training batch

หลังจาก train แล้ว เราก็สามารถลองใช้งานได้คล้าย ๆ กรณีก่อนนี้ ต่างกันตรงที่คราวนี้เราจะเช็คก่อนว่า confidence ของกรอบนั้นสูงกว่า 0.5 หรือไม่ ถ้าต่ำกว่าก็ไม่ display

ปรับโค้ด

โค้ดข้างบนเกือบดีแล้ว ติดตรงที่ถ้าเราเพิ่มจำนวนกรอบที่ทายเราต้องมานั่ง decode แต่ละกรอบใหม่ หนึ่งในทางแก้คือให้แต่ละกรอบเป็น channel แยกกันใน output blob นั่นคือ

ซึ่งทำให้เราต้องแก้ loss และส่วนเลือกกรอบต่าง ๆ ด้วย 
ที่สำคัญสุดคือ loss ซึ่งผมแก้ตามข้างล่างนี้ คราวนี้หากเราเพิ่มหรือลดจำนวนกรอบที่ทาย เราก็ไม่ต้องตามแก้โค้ดแล้ว

ROI Pooling

เทคนิคข้างบนนี้ follow ตามแนวทางของพวก YOLO และ SSD ซึ่งทายตำแหน่งกรอบ confidence และคลาส พร้อม ๆ กัน 
เทคนิคมาตรฐานอีกอันหนึ่งคือ Faster R-CNN ที่จะ resize ROI ต่าง ๆ ให้เท่ากันก่อนจะส่งไป classify ภายหลัง กระบวนการ resize ROI นี้เรียกว่า ROI Pooling ซึ่งดูเหมือนจะยังไม่มีใน Keras ผมเลยไม่ค่อยสนใจ Faster R-CNN แต่ไหน ๆ จะเขียนบล็อกเลยลองหาดูวิธีทำหน่อย

เท่าที่หา ๆ เจอโค้ดของ ROI Pooling ใช้ฟังก์ชัน image.resize_images ของ Tensorflow โดยเราจะเอาโค้ดนี้ไปใช้ในการสร้าง layer ชนิดใหม่

ในโค้ดตัวอย่างข้างล่างนี้เราจะสร้าง CNN เพื่อทายกรอบ ที่เรียกว่า region proposal network หรือ RPN จากนั้นเราจะเอา ROI ที่ได้นี้ไปใส่ชั้น ROI Pooling เพื่อให้มัน extract subimage ของ ROI นั้นมาดู นั่นคือตัว ROI Pooling นั้นเราจะไม่ train มัน แค่เอามา display เฉยๆ ถ้าจะทำต่อให้สมบูรณ์ก็ต้องเอาค่าที่ pool มาแล้วส่งให้ classifier ตัดสินใจต่อ

โค้ด RPN นั้นคล้าย ๆ กับ CNN ก่อนนี้ แต่เราตัดพวกคลาสออกไป นอกจากนี้เราใช้ constructor “Model” แทน “Sequential” เพราะเราจะเอา layer ใหม่ไปต่อทีหลัง
โค้ด ROI Pooling นั้นตอนนี้ทำทีละตัวอย่าง เพราะส่งแบบ batch แล้วยังมีบั๊กที่ผมไม่ค่อยเข้าใจอยู่ :/ 
ผลจากการ train RPN (อันนี้ลองเร็ว ๆ เลย train แค่ ~20 นาทีบน colab) ภาพที่ดึงได้ขนาด 14×14 pixels ก็ดูไม่เลว

ตัวอย่าง subimage ที่ดึงมาโดย ROI Pooling layer โดยใช้ RPN ที่ train แล้ว

วันนี้เริ่มยาวแล้วพอแค่นี้ก่อนละกัน

Happy Hacking :)