ใน ep นี้ เราจะสอน สร้างแอพ Android เขียน App AI บนมือถือ ที่มีความสามารถ Image Classification ด้วย TensorFlow Lite โดยใช้ Transfer Learning โมเดล Inception v3 มาเป็น Feature Extractor และเพิ่ม Custom Head 3 Class มา Convert ประกอบเป็น App สำหรับรันบนมือถือ Android ด้วยภาษา Kotlin
สอนเขียน App มือถือ Image Classification ด้วย Transfer Learning
เราสามารถพัฒนา App บนมือถือ Android ให้มีความสามารถด้าน AI / Machine Learning มีฟังก์ชัน Image Classification จากรูป ที่ถ่ายจากกล้องมือถือ แบบ Real-time ด้วยโมเดลของเราเอง ที่ทำ Transfer Learning มาจาก Inception v3 เพิ่ม Custom Head 3 Class ได้แก่ Rock ฆ้อน, Paper กระดาษ และ Scissors กรรไกร
ขั้นตอนการสร้างแอพพลิเคชั่น จะเป็นดังนี้
- Transfer Learning บน Server (ในที่นี้คือ Notebook / Colab)
- Export to SavedMovel และ Convert to FlatBuffer ไฟล์ .tflite
- Download นำไปใส่ใน App
- เขียน App เตรียมข้อมูล Input/Output ตามที่โมเดลต้องการ
เรามาเริ่มกันเลยดีกว่า ขั้นที่ 1-2
เราจะ Download ได้ไฟล์ มา 2 ไฟล์ ชื่อว่า converted_model.tflite
และ labels.txt
TensorFlow Lite on Android Code Example ขั้นที่ 3-4
ให้นำไฟล์ FlatBuffer และ Label จำนวน 2 ไฟล์จากด้านบน ให้ Rename เป็น inception_v3_rps_299.tflite
และ labels.txt
แล้วไปใส่ไว้ใน Folder ของ App Android ชื่อ rps_classification/app/src/main/assets
ใน Project โค้ดตัวอย่างจะมีโครงสร้างเหมือนกับ tflite ep.6 แต่จะมีแตกต่างกันอยู่ตรงที่ขนาดของ Input/Output ของโมเดล และโมเดลนี้เป็นโมเดลที่รับ Float32 ไม่ใช่ Int8
Camera2BasicFragment.kt
ใน Camera2BasicFragment.kt เราจะกำหนดตัวเแปร mModelPath
ชื่อไฟล์ FlatBuffer, mLabelPath
ชื่อไฟล์ Label และกำหนด mInputSize
ขนาดกว้างยาวของรูป Input ให้ตรงกับโมเดล
private val mInputSize = 299 // Depend on model
// RPS Inception v3 299
private val mModelPath = "inception_v3_rps_299.tflite"
private val mLabelPath = "labels_rps.txt"
Classifier.kt
ใน Classifier.kt เราต้อง Allocate ByteBuffer
มากขึ้น 4 เท่า เนื่องจาก Int8 ใช้ 1 Byte แต่ Float32 ใช้ 4 Byte และใช้ putFloat()
แทน put()
private fun addPixelValue(byteBuffer: ByteBuffer, intValue: Int): ByteBuffer {
byteBuffer.putFloat((intValue.shr(16) and 0xFF).toFloat() / 256f)
byteBuffer.putFloat((intValue.shr(8) and 0xFF).toFloat() / 256f)
byteBuffer.putFloat((intValue and 0xFF).toFloat() / 256f)
return byteBuffer
}
private fun convertBitmapToByteBuffer(bitmap: Bitmap): ByteBuffer {
val imgData = ByteBuffer.allocateDirect( 4 * INPUT_SIZE * INPUT_SIZE * PIXEL_SIZE)
imgData.order(ByteOrder.nativeOrder())
val intValues = IntArray(INPUT_SIZE * INPUT_SIZE)
imgData.rewind()
bitmap.getPixels(intValues, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
// Convert the image to floating point.
var pixel = 0
val startTime = SystemClock.uptimeMillis()
for (i in 0 until INPUT_SIZE) {
for (j in 0 until INPUT_SIZE) {
val `val` = intValues[pixel++]
addPixelValue(imgData, `val`)
}
}
return imgData;
}
ในฟังก์ชัน getSortedResult
ค่า confidence
ออกมาเป็น Float 0-1 อยู่แล้ว (ไม่ใช่ Int ที่ต้องหาร 255 ให้เป็นเ Float)
private fun getSortedResult(labelProbArray: Array<FloatArray>): List<Recognition> {
...
for (i in LABEL_LIST.indices) {
val confidence = labelProbArray[0][i]
if (confidence >= THRESHOLD) {
Log.d("confidence value:", "" + confidence);
pq.add(Recognition("" + i,
if (LABEL_LIST.size > i) LABEL_LIST[i] else "Unknown",
(confidence)
))
}
}
Build
Compile และ Build Project
Run on Android Mobile Phone
Deploy บน มือถือ Android
เนื่องจากใน RPS Dataset สภาพแสง พื้นหลัง และสีผิว อาจจะแตกต่างกับคนเอเชีย เราสามารถเก็บข้อมูลตัวอย่างเพิ่มเติม ไปใส่ Dataset แล้วเทรนต่อ เพื่อเพิ่มประสิทธิภาพความแม่นยำให้กับโมเดลได้