[TH] ulab EP1 Getting Started

ในกรณีที่ต้องการให้ MicroPython สำหรับ ESP8266 คำนวณทางคณิตศาสตร์เหมือนกับการใช้งานไลบรารี numpy ของภาษาไพธอนต้องติดตั้ง MicroPython ที่มี ulab อยู่ในตัว ด้วยการเข้าไปดาวน์โหลดเฟิร์มแวร์สำหรับ ESP8266 ได้จากที่เว็บนี้ สำหรับ ESP32 แบบปกติ และแบบมี PSRAM เพิ่มเติม (SPIRAM)

ในบทความนี้เป็นการแนะนำให้รู้จักกับ ulab และได้เห็นภาพรวมของไลบรารีภายใน ulab เพื่อนำไปประยุกต์ใช้ต่อไป

ภาพที่ 1 เฟิร์มแวร์ MicroPython+ulab บน RSP8266

ulab

ulab เป็นไลบรารีจัดการกับแถวลำดับหรือ Array ที่เขียนด้วยภาษา C เพื่อสร้างไลบรารีการทำงานแบบไลบรารี numpy ของภาษาไพธอน แต่ถูกออกแบบเพื่อให้ใช้งานกับ MicroPython เป็นหลัก และสามารถคอมไพล์เพื่อใช้งานกับหน่วยประมวลผลของ ESP8266, ESP32, CircuitPython, SAMD51/nRF, MicroPython for K210, MaixPy, OpenMV และ PyCom โดยในบทความนี้เลือกใช้เฟิร์มแวร์สำหรับ ESP8266 ชนิดมีรอมมากกว่า 1MB เพื่อให้สามารถใช้งานทศนิยมแบบ double precision ดังนั้น ควรเข้าไปดาวน์โหลดเฟิร์มแวร์ และเขียนเฟิร์มแวร์กับ ESP8266 ก่อนครับ เมื่อเขียนและรันโปรแกรมจากบทความก่อนหน้านี้จะแสดงผลเหมือนดังภาพที่ 1

รายการฟังก์ชันและโมดูลย่อยของ ulab เป็นดังภาพที่ 2 ซึ่งจะพบว่า ulab มีฟังก์ชันให้ใช้งาน และมีโมดูลย่อยอยู่ภายใน ได้แก่ array, linalg, vector, numerical, poly, fft, filter, compare และ approx ส่วน user เป็นส่วนสำหรับให้ผู้ใช้สร้างขึ้นเอง

ภาพที่ 2 รายการฟังก์ชันและโมดูลย่อยภายในไลบรารี ulab

ข้อมูลของแถวลำดับที่สร้างด้วย ulab ทุกตัวเป็นประเภทเดียวกันหมด และการใช้งาน ulab เริ่มจากเรียกใช้ ulab ด้วยคำสั่ง import ดังรูปแบบต่อไปนี้

import ulab

ส่วนรายการฟังก์ชันของ ulab ที่มีให้ใช้งานเป็นดังนี้

  1. set_printoptions(threshold=10, edgeitems=3) ตั้งค่า threshold และ edgeitems โดย threshold คือ จำนวนข้อมูลที่ใช้แสดงผลในคำสั่ง print เมื่อจำนวนข้อมูลที่ต้องแสดงมีจำนวนมากกว่า threshold จะแสดง edgeitems ตัวหน้า และหลัง ตรงกลางเป็น … เช่น กำหนด threshold เป็น 10 แต่ข้อมูลมี 20 ชุด ซึ่งประกอบด้วยตัวเลขจำนวนเต็ม 1 ถึง 20 และกำหนด edgeitems เป็น 2 จะแสดงผลเป็น 1, 2, …, 19, 20
  2. get_printoptions() บอกค่าของ threshold และ edgeitems ที่ตั้งค่าไว้
  3. arange( n, dtype=ulab.uint16 ) สำหรับสร้างตัวแปรแถวลำดับ 1 มิติ ประเภท dtype จำนวน n สมาชิก โดยแต่ละสมาชิกมีค่าเรียงจาก 0 ถึง n-1
  4. arange( start, stop, dtype=ulab.int16) สำหรับสร้างตัวแปรแถวลำดับ 1 มิติ ประเภท dtype จำนวน (stop-start) สมาชิก โดยแต่ละสมาชิกมีค่าเรียงจาก start ถึง stop-1
  5. arange(start, stop, step, dtype=ulab.int16)สำหรับสร้างตัวแปรแถวลำดับ 1 มิติ ประเภท dtype จำนวน ((stop-start)/step) สมาชิก โดยแต่ละสมาชิกมีค่าเรียงจาก start ถึง stop-1 โดยแต่ละค่าห่างกัน step ค่า
  6. eye( n, dtype=ulab.float ) สำหรับสร้างแถวลำดับแบบ identity matrix ของข้อมูลประเภท dtype ขนาด n แถว x n สดมภ์
  7. linspace( start, stop, n ) สำหรับสร้างแถวลำดับ 1 มิติจำนวน n สมาชิก โดยสมาชิกตัวแรกมีค่าเป็น start และค่าสุดท้ายเป็น stop ส่วนสมาชิกระหว่างตัวแรกและตัวสุดท้ายเกิดจากการคำนวณจากสมการเชิงเส้น
    หมายเหตุ กรณีที่ใช้กับข้อมูลแบบจำนวนเต็มแล้วจำนวน n มากกว่าระยะห่างระหว่างค่า start และ stop จะทำให้คำนวณผิดพลาด และโปรแกรมหยุดทำงานได้
  8. ones((row, col),dtype=ulab.float) สำหรับสร้างแถวลำดับประเภท dtype ขนาด row แถว x col สดมภ์ โดยสมาชิกทุกตัวมีคค่าเป็น 1
  9. zeros((row, col),dtype=ulab.float) สำหรับสร้างแถวลำดับประเภท dtype ขนาด row แถว x col สดมภ์ โดยสมาชิกทุกตัวมีค่าเป็น 0

ตัวอย่างที่ 1

ตัวอย่างโปรแกรม code18-1 เป็นการทดลองใช้ ulab เพื่อสร้างแถวลำดับและเมตริกซ์เบื้องต้น และได้ผลลัพธ์ดังภาพที่ 3

# code18-1
import ulab as np
a = np.arange(8)
b = np.arange(8,dtype=np.float)
c = np.arange(1,5)
d = np.arange(1,5,2)
e = np.ones((3,3),dtype=np.uint8)
f = np.zeros((3,3))
g = np.eye(3)
h = np.eye(3, dtype=np.int8)
i = np.linspace(10,2,5)
j = np.linspace(-2, 8, 10)
print("{}/{}\n{}/{}".format(a,len(a),b,len(b)))
print("{}/{}\n{}/{}".format(c,len(c),d,len(d)))
print("{}/{}\n{}/{}".format(e,len(e),f,len(f)))
print("{}/{}\n{}/{}".format(g,len(g),h,len(h)))
print("{}/{}\n{}/{}".format(i,len(i),j,len(j)))

จาก code18-1 จะพบว่า

  1. ตัวแปร a เกิดจากการสร้างตัวแปรแถวลำดับจำนวน 8 ตัว จึงมีสมาชิกเป็น 0 ถึง 7 และมีจำนวนสมาชิกทั้งหทด 8 ตัว
  2. ตัวแปร b เกิดจากการสร้างตัวแปรแถวลำดับประเภททศนิยมจำนวน 8 ตัว สมาชิกแต่ละตัวเป็น 0.0 ถึง 7.0 และมีสมาชิกทั้งหมด 8 ตัว
  3. ตัวแปร c เกิดจากการสร้างด้วยกำหนดตัวเริ่มและสิ้นสุด ทำให้สมาชิกตัวแรกเป็น 1 และตัวสุดท้ายเป็น 5-1 หรือ 4 จึงมีสมาชิกทั้งหมด 4 ตัว
  4. ตัวแปร d เกิดจากการสร้างด้วยการกำหนดตัวเริ่ม สิ้นสุดและการเพิ่ม/ลดค่าระหว่างแต่ละค่า ทำให้ค่าเริ่มเป็น 1 ตามด้วย (1+2) หรือ 3 แต่ตัวถัดไปเมื่อเพิ่มค่าอีก 2 ทำให้เกินช่วง จึงสิ้นสุดการสร้างสมาชิกเพียงเท่านี้
  5. ตัวแปร e สร้างเมตริกซ์ (แถวลำดับ 2 มิติ) ขนาด 3 แถว x 3 สดมภ์ โดยสมาชิกแต่ละตัวเก็บค่า 1 และถ้าสังเกตจะพบว่า len() ให้คำตอบเป็น 3 เนื่องจากมีข้อมูล 3 แถว ให้ผู้อ่านลองเพิ่มคำสั่ง print(“len: e[0]={} e[1]={} e[2]={}”.format(len(e[0]), len(e[1]), len(e[2]) )) จะพบว่าในแต่ละแถวมีสมาชิก 3 ตัว
  6. ตัวแปร f เป็นการสร้างเมตริกซ์ขนาด 3×3 โดยแต่ละสมาชิกเป็น 0.0
  7. ตัวแปร g สร้างเมตริกซ์ identity ขนาด 3×3
  8. ตัวแปร h สร้างเมตริกซ์แบบ identity ขนาด 3×3 โดยกำหนดประเภทของข้อมูลเป็น int8
  9. ตัวแปร i เป็นการสร้างแถวลำดับโดยให้เริ่มจากค่า 10 และค่าสุดท้ายเป็น 2 และต้องการสร้างสมาชิกเพียง 5 ตัว
  10. ตัวแปร j เป็นการสร้างตัวแปรที่เริ่มจากค่า -2 และตัวสุดท้ายเป็น 8 และต้องการสมาชิก 10 ตัว
ภาพที่ 3 ผลลัพธ์ของ code18-1

ประเภทของข้อมูลที่ใช้งานกับ ulab ได้ คือ uint8 int8 uint16 int16 และ float ส่วนรายละเอียดของโมดูลย่อยแต่ละตัวของ ulab ไล่เรียงในบทความถัดไป

ตัวอย่าง 2

ตัวอย่างการเปรียบเทียบความเร็วในการทำงานระหว่างการบวก และคูณแถวลำดับจำนวน 1000 สมาชิกด้วยการเขียนภาษาไพธอนกับการใช้ ulab เป็นดัง code18-1a และผลลัพธ์ที่เกิดจากการทำงานด้วย ESP8266 เป็นดังภาพที่ 4 และ ESP32 (แบบมี PSRAM และไม่มี PSRAM) เป็นดังภาพที่ 5 และ 6

# code18-0 : Benchmark
import time
import ulab as np
import sys

print(sys.platform)

a = [0.0]*1000
b = range(1000)
print('python:')
t0 = time.ticks_us()
[a[i]+b[i] for i in range(1000)]
print("execution time for add = {} us".format(time.ticks_us()-t0))
t0 = time.ticks_us()
[a[i]*b[i] for i in range(1000)]
print("execution time for multiply = {} us".format(time.ticks_us()-t0))

a = np.linspace(0, 10, num=1000)
b = np.ones(1000)
print('ulab:')
t0 = time.ticks_us()
a+b
print("execution time for add = {} us".format(time.ticks_us()-t0))
t0 = time.ticks_us()
a*b
print("execution time for multiply = {} us".format(time.ticks_us()-t0))
ภาพที่ 4 ผลลัพธ์จาก code18-1a ด้วย ESP8266
ภาพที่ 5 ผลลัพธ์จาก code18-1a ด้วย ESP32+PSRAM
ภาพที่ 5 ผลลัพธ์จาก code18-1a ด้วย ESP32 ที่ไม่มี PSRAM

สรุป

จากบทความในตอนนี้จะพบว่า ulab ทำให้เราสามารถเขียนโปรแกรมหรือพัฒนาโปรแกรมเพื่อประมวลผลข้อมูลแบบแถวลำดับ 1 มิติหรือ 2 มิติ ทำให้สามารถขยายความสามารถของไมโครคอนโทรลเลอร์ให้ทำงานได้กว้างมากขึ้น เช่น นำไปใช้ในการคำนวณการหมุนของวัตถุทั้งแบบ 2 มิติ และ 3 มิติ นำไปคำนวณทางสถิติ ทำการแปลงค่าให้อยู่ในรูปแบบอื่น ๆ เพื่อพิจารณาลักษณะของสัญญาณ หรือนำไปใช้ในการคำนวณหาแนวโน้มด้วยวิธีการต่าง ๆ ซึ่งเป็นพื้นฐานไปสู่การใส่สมองกลน้อย ๆ ให้กับอุปกรณ์อย่าง ESP8266/ESP32

และจากการวัดความเร็วในการทำงานเปรียบเทียบการบวกและคูณข้อมูลจำนวน 1000 ชุดด้วยการเขียนภาษาไพธอนปกติกับการใช้ ulab จะพบว่าความเร็วของ ulab นั้นสูงกว่า และที่น่าสังเกตคือ ESP32 ที่ไม่มี PSRAM ติดตั้งเพิ่มเติมจะมีความเร็วมากกว่า ESP32 ที่มี PSRAM ทั่งนี้เกิดจาก PSRAM เป็นหน่วยความ RAM ที่เชื่อมต่อผ่านบัส SPI ซึ่งมีความเร็วที่น้อยกว่าความเร็วของหน่วยความจำภายใน ESP32 ทำให้การคำนวณด้วยหน่วยความจำภายนอกใช้เวลามากกว่า ดังนั้น ถ้าเน้นที่ความเร็วในการทำงาน ESP32 ที่ไม่มีหน่วยความจำภายนอกจะทำงานได้เร็วกว่ามาก แต่ปริมาณหน่วยความจำให้ใช้งานนั้นน้อยกว่า จึงต้องตัดสินใจเลือกตามสถานการณ์ เช่น มีปริมาณข้อมูลมากกว่า ESP32 แบบปกติจัดเก็บได้ หรือประมวลผลได้ จึงต้องเลือกแบบมี PSRAM แต่ถ้าใช้กับข้อมูลที่ ESP32 ปกติจัดเก็บได้ ก็ควรเลือกใช้ ESP32 แบบปกติในการทำงาน เนื่องจากความเร็วสูงกว่า และราคาถูกกว่า เป็นต้น

โดยใน EP1 นี้ได้เกริ่นนำถึงหน้าที่ การติดตั้ง และการใช้งานเบื้องต้นกันไปแล้ว ครั้งหน้าเป็นเรื่องของโมดูลย่อย array และการใช้งาน สุดท้ายนี้หวังว่าบทความนี้คงเป็นบทความสำหรับผู้สนใจเขียนโปรแกรมภาษาไพธอน และนำไปประยุกต์ใช้กันต่อไป ขอให้สนุกกับการเขียนโปรแกรมครับ

(C) 2020, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2020-11-06
ปรับปรุงเมื่อ 2020-11-07
ปรับปรุงเมื่อ 2020-11-08
ปรับปรุงเมื่อ 2021-08-11