φυβλαςのβλογ
บล็อกของ phyblas



โครงข่ายประสาทเทียมเบื้องต้น บทที่ ๒: การเรียนรู้ของเพอร์เซปตรอน
เขียนเมื่อ 2018/08/26 23:21
แก้ไขล่าสุด 2022/07/10 21:13
>> ต่อจาก บทที่ ๑



แนวคิดในการปรับค่าพารามิเตอร์

บทที่แล้วทิ้งท้ายไว้ที่ว่าเราจะต้องเขียนโปรแกรมเพื่อให้มันทำการเรียนรู้เพื่อหาพารามิเตอร์ค่าน้ำหนักและไบแอสที่เหมาะสม

แนวคิดในการปรับพารามิเตอร์นั้นมีอยู่หลากหลาย แต่ในที่นี้จะขอเริ่มจากวิธีการที่ง่ายสุดซึ่งถูกคิดมาก่อนในช่วงยุคแรก

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

ภาพอธิบายหลักการคร่าวๆ เราคำนวณหาคำตอบ (h) แล้วเอามาเทียบกับคำตอบจริง (z) แล้วผลที่ได้เป็นตัวกำหนดว่าจะปรับพารามิเตอร์ (w1,w2,...,wn,b) ยังไง



ส่วนการจะพิจารณาว่าควรปรับค่าไปยังไงนั้นจะพิจารณาง่ายๆแบบนี้ คือ

- ถ้าคำตอบจริงเป็น 1 แต่คำตอบที่คำนวณได้เป็น 0 แสดงว่าค่าที่คำนวณได้ต่ำไป ดังนั้นต้องปรับไปในทางที่ทำให้ค่าสูงขึ้น นั่นคือถ้าค่าตัวแปรไหนเป็นบวก น้ำหนักที่คูณตัวนั้นจะต้องเพิ่มค่า แต่ถ้าตัวแปรเป็นลบ น้ำหนักจะต้องลดค่า นอกจากนี้ไบแอสก็ควรต้องปรับค่าเพิ่มด้วย

- ถ้าคำตอบจริงเป็น 0 แต่คำตอบที่คำนวณได้เป็น 1 แสดงว่าค่าที่คำนวณได้สูงไป ต้องปรับให้ได้ค่าต่ำลงนั่นคือถ้าค่าตัวแปรไหนเป็นบวกก็ต้องลดค่าน้ำหนักตัวนั้น ตัวแปรไหนเป็นลบต้องเพิ่มค่าน้ำหนัก ส่วนไบแอสก็ต้องลดค่าด้วย

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

เมื่อเขียนสรุปเป็นสมการแล้วก็จะได้แบบนี้
..(2.1)
..(2.2)

โดย h(xi) คือคำตอบที่คำนวณได้จากตัวแปรต้น x ตัวที่ i ส่วน zi คือตำตอบจริงตัวที่ i ที่ใส่ดัชนี i ไปเพราะใช้ข้อมูลหลายตัวในการเรียนรู้

โดยค่า η ในที่นี้เรียกว่าอัตราการเรียนรู้ (学习率, learning rate) เป็นค่าที่กำหนดว่าในแต่ละครั้งควรจะมีการปรับค่าพารามิเตอร์ไปมากน้อยแค่ไหน

w ใน (2.1) นี้อยู่ในรูปของอาเรย์ของค่าน้ำหนักของตัวแปรต่างๆหลายตัว ถ้าแยกให้เข้าใจง่าย แต่ละตัวจะมีการปรับค่าดังนี้
..(2.3)

กรณี zi เป็น 1 แต่ h(xi) เป็น 0 จะได้ zi-h(xi)=1 แบบนั้น b จะถูกปรับเพิ่ม ส่วน wj คือค่าน้ำหนักของตัวแปรตัวที่ j จะถูกปรับโดยขึ้นกับว่าค่า xi,j เป็นเท่าไหร่ ถ้าเป็นบวก wj ก็ถูกปรับเพิ่ม ถ้าเป็นลบก็ลด

ส่วนถ้า zi เป็น 0 แต่ h(xi) เป็น 1 ก็จะได้ zi-h(xi)=-1 แล้วการเปลี่ยนแปลงก็เป็นไปในทางตรงกันข้าม

แต่ถ้าทั้ง zi และ h(xi) เป็น 0 หรือ 1 ทั่งคู่ zi-h(xi)=0 ก็จะไม่เกิดการเปลี่ยนแปลงใดๆ

สมการที่ดูเรียบง่ายนี้สามารถใช้ปรับพารามิเตอร์ให้เป็นไปอย่างที่ต้องการได้ดีในระดับนึง แม้ว่าจะยังไม่ดีพอ (เหตุผลไว้จะกล่าวตอนหลัง)



สรุปขั้นตอน

1. ทำการกำหนดค่าพารามิเตอร์ตั้งต้นขึ้นมา

2. หยิบข้อมูลขึ้นมาตัวนึง คำนวณผลลัพธ์ของเพอร์เซปตรอนด้วยพารามิเตอร์ขณะนั้น แล้วดูว่าได้ค่าต่างจากคำตอบจริงเท่าไหร่ แล้วปรับค่า

3. ทำเหมือนเดิมกับตัวต่อไป

4. พอใช้ข้อมูลครบหมดทุกตัวก็เริ่มทำซ้ำรอบใหม่อีกรอบ พารามิเตอร์ก็จะเปลี่ยนไปเรื่อยๆ ทำไปจนกว่าจะครบจำนวนครั้งสูงสุดที่ต้องการ หรือสามารถทายถูกทั้งหมด



เขียนโปรแกรม

คราวนี้จะเขียนฟังก์ชันเพอร์เซปตรอนเหมือนคราวที่แล้ว แต่ครั้งนี้เราจะกำหนด w และ b ที่นอกฟังก์ชันเพื่อให้สามารถปรับค่าไปเรื่อยๆได้
import numpy as np

def h(X):
    a = np.dot(X,w) + b
    return (a>=0).astype(int)

w = np.array([0,0.])
b = 0.1

ค่าตั้งต้นจะกำหนดเป็นอะไรก็ได้ บางทีอาจเริ่มที่ 0 ทั้งหมด หรือเอาค่าที่คาดเดาล่วงหน้า ในที่นี้ขอกำหนดเป็นเท่านี้

จากนั้นขอยกตัวอย่างเป็นเกต AND ซึ่งเป็นเกตที่จะให้ค่าเป็น 1 เมื่อค่าป้อนเข้าทั้งหมดเป็น 1 เท่านั้น ที่เหลือเป็น 0

นั่นคือ
X = np.array([
    [0,0],
    [0,1],
    [1,0],
    [1,1]
])
z = np.array([0,0,0,1])

# วาดภาพแสดง
plt.axes(aspect=1)
plt.scatter(X[:,0],X[:,1],100,c=z,edgecolor='r',marker='D',cmap='hot')
plt.show()



เป้าหมายของเราคือหาค่า w และ b ที่จะทำให้ h(X)==z ทุกตัว

ลองให้โปรแกรมวนซ้ำโดยทำการคำนวณตามสมการ (2.1) แล้วปรับค่าพารามิเตอร์ไปเรื่อยๆ พร้อมกันนั้นก็ให้แสดงผลค่าที่ได้ในแต่ละรอบออกมาด้วย
eta = 0.2 # อัตราการเรียนรู้
print('เริ่มต้น: h(x)=%s, w=%s, b=%s'%(h(X),w,b))
for j in range(100): # ให้ทำซ้ำสูงสุด 100 ครั้ง
    for i in range(4):
        z_h = z[i] - h(X[i])
        dw = eta*z_h*X[i]
        db = eta*z_h
        w += dw
        b += db
        print('รอบ %d.%d: h(x)=%s, w=%s, b=%s, Δw=%s, Δb=%s'%(j+1,i+1,h(X),w,b,dw,db))
    if(np.all(h(X)==z)): # ถ้าผลลัพธ์ถูกต้องทั้งหมดก็ให้เสร็จสิ้นการเรียนรู้
        break

ผลลัพธ์จะได้
เริ่มต้น: h(x)=[1 1 1 1], w=[ 0.  0.], b=0.1
รอบ 1.1: h(x)=[0 0 0 0], w=[ 0.  0.], b=-0.1, Δw=[-0. -0.], Δb=-0.2
รอบ 1.2: h(x)=[0 0 0 0], w=[ 0.  0.], b=-0.1, Δw=[ 0.  0.], Δb=0.0
รอบ 1.3: h(x)=[0 0 0 0], w=[ 0.  0.], b=-0.1, Δw=[ 0.  0.], Δb=0.0
รอบ 1.4: h(x)=[1 1 1 1], w=[ 0.2  0.2], b=0.1, Δw=[ 0.2  0.2], Δb=0.2
รอบ 2.1: h(x)=[0 1 1 1], w=[ 0.2  0.2], b=-0.1, Δw=[-0. -0.], Δb=-0.2
รอบ 2.2: h(x)=[0 0 0 0], w=[ 0.2  0. ], b=-0.3, Δw=[-0.  -0.2], Δb=-0.2
รอบ 2.3: h(x)=[0 0 0 0], w=[ 0.2  0. ], b=-0.3, Δw=[ 0.  0.], Δb=0.0
รอบ 2.4: h(x)=[0 1 1 1], w=[ 0.4  0.2], b=-0.1, Δw=[ 0.2  0.2], Δb=0.2
รอบ 3.1: h(x)=[0 1 1 1], w=[ 0.4  0.2], b=-0.1, Δw=[ 0.  0.], Δb=0.0
รอบ 3.2: h(x)=[0 0 1 1], w=[ 0.4  0. ], b=-0.3, Δw=[-0.  -0.2], Δb=-0.2
รอบ 3.3: h(x)=[0 0 0 0], w=[ 0.2  0. ], b=-0.5, Δw=[-0.2 -0. ], Δb=-0.2
รอบ 3.4: h(x)=[0 0 1 1], w=[ 0.4  0.2], b=-0.3, Δw=[ 0.2  0.2], Δb=0.2
รอบ 4.1: h(x)=[0 0 1 1], w=[ 0.4  0.2], b=-0.3, Δw=[ 0.  0.], Δb=0.0
รอบ 4.2: h(x)=[0 0 1 1], w=[ 0.4  0.2], b=-0.3, Δw=[ 0.  0.], Δb=0.0
รอบ 4.3: h(x)=[0 0 0 0], w=[ 0.2  0.2], b=-0.5, Δw=[-0.2 -0. ], Δb=-0.2
รอบ 4.4: h(x)=[0 1 1 1], w=[ 0.4  0.4], b=-0.3, Δw=[ 0.2  0.2], Δb=0.2
รอบ 5.1: h(x)=[0 1 1 1], w=[ 0.4  0.4], b=-0.3, Δw=[ 0.  0.], Δb=0.0
รอบ 5.2: h(x)=[0 0 0 1], w=[ 0.4  0.2], b=-0.5, Δw=[-0.  -0.2], Δb=-0.2
รอบ 5.3: h(x)=[0 0 0 1], w=[ 0.4  0.2], b=-0.5, Δw=[ 0.  0.], Δb=0.0
รอบ 5.4: h(x)=[0 0 0 1], w=[ 0.4  0.2], b=-0.5, Δw=[ 0.  0.], Δb=0.0

ในที่นี้การเรียนรู้ทำเสร็จสิ้นสมบูรณ์ในการวนซ้ำรอบที่ 5

จะเห็นได้ว่าค่าที่ได้จากเพอร์เซปตรอนเปลี่ยนแปลงไปเรื่อยๆจนในที่สุดก็จะกลายเป็น [0 0 0 1] ซึ่งตรงกับค่า z ที่ต้องการ

ลองวาดภาพแสดงการแบ่งตามพารามิเตอร์สุดท้ายที่ได้
mx,my = np.meshgrid(np.linspace(-0.5,1.5,200),np.linspace(-0.5,1.5,200))
mX = np.array([mx.ravel(),my.ravel()]).T
mz = h(mX).reshape(200,-1)
plt.axes(aspect=1,xticks=[0,1],yticks=[0,1])
plt.contourf(mx,my,mz,cmap='summer',vmin=0,vmax=1)
plt.scatter(X[:,0],X[:,1],100,c=z,edgecolor='r',marker='D',cmap='hot')
plt.show()

ก็จะได้แบบนี้



ทีนี้ถ้าวาดออกมาเป็นภาพเคลื่อนไหวแสดงความเปลี่ยนแปลงของเส้นแบ่งแล้วก็จะได้แบบนี้



จะเห็นว่าเส้นแบ่งจะเปลี่ยนแปลงไปเรื่อยๆ จนในที่สุดเส้นสามารถแบ่งแยกระหว่างส่วนที่ควรเป็น 1 และ 0 ออกจากกันได้อย่างถูกต้อง



ปัญหาของวิธีนี้

นี่เป็นอัลกอริธึมอย่างง่ายๆในการหาค่าน้ำหนักและไบแอสที่เหมาะสม อย่างไรก็ตาม วิธีการนี้มีปัญหาอยู่มาก

อย่างแรกคือ เพอร์เซปตรอนไม่สามารถใช้ในกรณีของปัญหาที่ไม่สามารถแบ่งเชิงเส้นได้โดยสมบูรณ์ เช่น



แบบนี้ไม่ว่าจะปรับค่าน้ำหนักและไบแอสไปสักกี่ครั้งก็ไม่มีทางแบ่งได้สมบูรณ์ และเพอร์เซปตรอนจะทำการปรับค่าไปเรื่อยๆไม่มีที่สิ้นสุด

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

เช่นดูอย่างภาพนี้



จะเห็นว่าเส้นสีเขียวดูจะแบ่งได้ดีที่สุดแต่เส้นอื่นก็แบ่งได้เช่นกัน

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

ด้วยปัญหานี้ทำให้อัลกอริธึมอย่างง่ายของเพอร์เซปตรอนแบบนี้ไม่เหมาะที่จะนำมาใช้งานจริง

ในบทต่อไปจะแนะนำวิธีการที่ถูกนำมาใช้งานจริง นั่นคือ การเคลื่อนลงตามความชัน (梯度下降法, gradient descent) กับฟังก์ชันกระตุ้น (激活函数, activation function)



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

ที่จริงยังมีรายละเอียดอื่นๆอีก แต่ไม่จำเป็นต้องพูดถึงมากแล้วเพราะเป้าหมายคือแค่แนะนำให้รู้จักหลักการคร่าวๆ ที่จะต้องไปเน้นจริงๆคือวิธีการในบทต่อไป



>> อ่านต่อ บทที่ ๓


-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> ปัญญาประดิษฐ์ >> โครงข่ายประสาทเทียม
-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python >> numpy

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

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
มอดูลต่างๆ
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
ภาษา javascript
ภาษา mongol
ภาษาศาสตร์
maya
ความน่าจะเป็น
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
-- บันทึกในฮ่องกง
-- บันทึกในมาเก๊า
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
qiita
บทความอื่นๆ

บทความแบ่งตามหมวด



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  ค้นหาบทความ

  บทความแนะนำ

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

บทความแต่ละเดือน

2024年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2023年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2022年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2021年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2020年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

ค้นบทความเก่ากว่านั้น

ไทย

日本語

中文