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



pytorch เบื้องต้น บทที่ ๗: การสร้างเพอร์เซปตรอนหลายชั้น
เขียนเมื่อ 2018/09/08 10:13
แก้ไขล่าสุด 2022/07/09 15:42
>> ต่อจาก บทที่ ๖



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



การสร้างโครงข่ายด้วยคลาส Module

โครงข่ายประสาทเทียมใน pytorch ปกติจะนิยามโดยสร้างคลาสขึ้นเป็นคลาสย่อยของคลาส torch.nn.Module

ภายในเมธอด __init__ จะมีการวางโครงสร้างว่าจะประกอบไปด้วยชั้นอะไรบ้าง

จากนั้นสร้างเมธอด forward เพื่อกำหนดลำดับการไหลของข้อมูลว่าจะให้มีการคำนวณยังไงในชั้นต่างๆ

ยกตัวอย่างเช่น หากต้องการสร้างโครงข่าย ๓ ชั้น ที่มีฟังก์ชันกระตุ้นเป็น ReLU จะเขียนได้ดังนี้
import torch
relu = torch.nn.ReLU()

class Khrongkhai(torch.nn.Module):
    def __init__(self,m0,m1,m2,m3):
        super(Khrongkhai,self).__init__()
        self.lin1 = torch.nn.Linear(m0,m1)
        self.lin2 = torch.nn.Linear(m1,m2)
        self.lin3 = torch.nn.Linear(m2,m3)
    
    def forward(self,x):
        a1 = self.lin1(x)
        h1 = relu(a1)
        a2 = self.lin2(h1)
        h2 = relu(a2)
        a3 = self.lin3(h2)
        return a3

ฟังก์ชัน ReLU ในที่นี้สร้างขึ้นเตรียมไว้จากคลาส torch.nn.ReLU แต่จะใช้ฟังก์ชัน torch.nn.functional.relu() ก็ได้

คลาส Module ถูกสร้างขึ้นมาให้เวลาที่ถูกเรียกใช้โดยการเติม () ตามหลังจะไปเรียกเมธอดชื่อ forward ดังนั้นการที่ตั้งชื่อเมธอดว่า forward นี้เป็นการตั้งชื่อที่ตายตัว จะไปใช้ชื่ออื่นแทนไม่ได้

อันที่จริงตัว torch.nn.Linear เองก็เป็นคลาสย่อยของ torch.nn.Module
print(issubclass(torch.nn.Linear,torch.nn.Module)) # ได้ True

เมธอด .parameters() สำหรับคืนค่าของพารามิเตอร์ทุกตัวในชั้นก็เป็นเมธอดของคลาส Module

สำหรับ Module ทั่วไปที่นิยามขึ้นโดยบรรจุ Module อื่น (ในที่นี้คือ Linear) ไว้ภายในนั้น เมธอด .parameters() ซึ่งจะคืนค่าพารามิเตอร์ของทุก Module ภายในนั้น
khrongkhai = Khrongkhai(1,1,1,1)
print(len(list(khrongkhai.parameters()))) # ได้ 6

__init__ ยังอาจนิยามโดยใช้เมธอด .add_module ได้ เช่นแก้เป็นแบบนี้
def __init__(self,m0,m1,m2,m3):
    super(Khrongkhai,self).__init__()
    self.add_module('lin1',torch.nn.Linear(m0,m1))
    self.add_module('lin2',torch.nn.Linear(m1,m2))
    self.add_module('lin3',torch.nn.Linear(m2,m3))

ผลที่ได้ไม่ต่างกัน แต่บางกรณีอาจสะดวกในการใช้มากกว่า เพราะชื่อของสายอักขระถูกป้อนในรูปของสายอักขระ



การใช้โครงข่ายที่นิยามขึ้นมา

ขอยกตัวอย่างเป็นปัญหาจำแนกข้อมูล ๔ กลุ่มที่แบ่งเป็นเชิงเส้นไม่ได้ แบบนี้
import numpy as np
import matplotlib.pyplot as plt

z = np.arange(4).repeat(40)
r = np.random.normal(z+1,0.25)
t = np.random.uniform(0,np.pi,160)
x = r*np.cos(t)
y = r*np.sin(t)
X = np.array([x,y]).T
plt.scatter(x,y,c=z,edgecolor='k',cmap='jet')
plt.show()


นำโครงข่ายที่สร้างไว้มาใช้ได้ดังนี้
ha_entropy = torch.nn.CrossEntropyLoss()
 X = torch.Tensor(X)
z = torch.LongTensor(z)
khrongkhai = Khrongkhai(2,60,40,4)

opt = torch.optim.Adam(khrongkhai.parameters(),lr=0.1)
ha_entropy = torch.nn.CrossEntropyLoss()
for i in range(200):
    a = khrongkhai(X)
    J = ha_entropy(a,z)
    J.backward()
    opt.step()
    opt.zero_grad()

mx,my = np.meshgrid(np.linspace(x.min(),x.max(),200),np.linspace(y.min(),y.max(),200))
mX = torch.Tensor(np.array([mx.ravel(),my.ravel()]).T)
mz = khrongkhai(mX).argmax(1)
mz = mz.data.numpy().reshape(200,200)
plt.xlim(x.min(),x.max())
plt.ylim(y.min(),y.max())
plt.scatter(x,y,c=z,edgecolor='k',cmap='jet')
plt.contourf(mx,my,mz,alpha=0.2,cmap='jet')
plt.show()


ถ้าเทียบกับตอนที่ใช้ lin แค่ตัวเดียวแล้ว วิธีการเขียนแทบจะเหมือนกัน แค่เปลี่ยนจาก lin ซึ่งเป็นแค่ชั้นคำนวณเชิงเส้นชั้นเดียวมาเป็น khrongkhai ซึ่งภายในประกอบด้วยหลายๆชั้น

เมธอด .parameters() นั้นทำการค้นหาพารามิเตอร์ที่อยู่ภายในชั้นย่อยทั้งหมดแล้วส่งให้กับออปทิไมเซอร์ พารามิเตอร์ทั้งหมดจึงถูกปรับค่า



การใช้ Sequential

การนิยามโครงข่ายตามตัวอย่างข้างต้นจะเห็นว่านอกจากกำหนดชั้นต่างๆขึ้นใน __init__ แล้วยังต้องกำหนดขั้นตอนการคำนวณใน forward อีก

แต่จะเห็นว่าขั้นตอนการคำนวณในที่นี้นั้นมีลักษณะค่อนข้างตายตัว นั่นคือเอาผลที่ได้จากการคำนวณแต่ละชั้นมาเข้าชั้นต่อไปคำนวณต่อไปเรื่อยๆ

กรณีแบบนี้มีวิธีที่สะดวกกว่านั้นเพื่อที่จะไม่ต้องมานิยาม forward นั่นคือใช้ torch.nn.Sequential

หากใช้แล้วจะสร้างโครงข่ายขึ้นได้ทันทีโดยไม่ต้องไปสร้างคลาสใหม่ ทำได้โดยแค่เขียนแบบนี้
khrongkhai = torch.nn.Sequential(
        torch.nn.Linear(2,60),
        relu,
        torch.nn.Linear(60,40),
        relu,
        torch.nn.Linear(40,4)
)

กรณีนี้จำเป็นต้องป้อนทุกชั้นที่เป็นทางผ่าน แม้แต่ชั้นที่ไม่มีพารามิเตอร์อย่าง relu (ถ้าเป็นแบบเดิมแค่ใส่ตอน forward ก็พอ)

Sequential ก็เป็นคลาสย่อยของคลาส Module จึงมีคุณสมบัติต่างๆเหมือนกัน

พอเรียกใช้โครงข่ายก็จะเกิดการคำนวณตามลำดับที่ใส่ไป

ข้อดีคือไม่จำเป็นต้องไปตั้งชื่อให้กับแต่ละชั้น ถ้าดูข้อมูลภายในโครงข่ายจะเห็นว่าแต่ละตัวถูกแทนด้วยเลขตามลำดับ
print(khrongkhai)

ได้
Sequential(
  (0): Linear(in_features=2, out_features=60, bias=True)
  (1): ReLU()
  (2): Linear(in_features=60, out_features=40, bias=True)
  (3): ReLU()
  (4): Linear(in_features=40, out_features=4, bias=True)
)

เวลาจะเข้าถึงส่วนที่อยู่ด้านในก็ทำได้โดยใช้เลขลำดับเหมือนอาเรย์
print(khrongkhai[0]) # ได้ Linear(in_features=2, out_features=60, bias=True)

นอกจากนี้ถ้าหากต้องการตั้งชื่อให้กับแต่ละชั้นเพื่อจะได้อ้างอิงได้สะดวก ก็อาจใช้คำสั่ง .add_module() ได้เช่นกัน
khrongkhai = torch.nn.Sequential()
khrongkhai.add_module('lin1',torch.nn.Linear(2,60))
# หรือ khrongkhai.lin1 = torch.nn.Linear(2,60)
khrongkhai.add_module('relu1',relu)
khrongkhai.add_module('lin2',torch.nn.Linear(60,40))
khrongkhai.add_module('relu2',relu)
khrongkhai.add_module('lin3',torch.nn.Linear(40,4))

พอทำแบบนี้ก็จะมีชื่อติด เวลาจะเข้าถึงจะใช้เลขลำดับหรือชื่อก็ได้
print(khrongkhai)
print(khrongkhai.lin2) # หรือ khrongkhai[2]

ได้
Sequential(
  (lin1): Linear(in_features=2, out_features=60, bias=True)
  (relu1): ReLU()
  (lin2): Linear(in_features=60, out_features=40, bias=True)
  (relu2): ReLU()
  (lin3): Linear(in_features=40, out_features=4, bias=True)
)
Linear(in_features=60, out_features=40, bias=True)



Module ซ้อน Module

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

เช่น
l1 = torch.nn.Sequential(torch.nn.Linear(2,60),relu)
l2 = torch.nn.Sequential(torch.nn.Linear(60,40),relu)
l3 = torch.nn.Linear(40,4)
khrongkhai = torch.nn.Sequential(l1,l2,l3)
print(khrongkhai)

ได้
Sequential(
  (0): Sequential(
    (0): Linear(in_features=2, out_features=60, bias=True)
    (1): ReLU()
  )
  (1): Sequential(
    (0): Linear(in_features=60, out_features=40, bias=True)
    (1): ReLU()
  )
  (2): Linear(in_features=40, out_features=4, bias=True)
)

โครงข่ายแบบนี้ก็ทำการคำนวณตามลำดับได้เช่นกัน บางทีการแบ่งเป็นชั้นย่อยๆซ้อนกันอาจทำให้เข้าใจง่ายขึ้น



สร้างชั้นปรับรูปร่างเทนเซอร์

ถ้าการคำนวณภายในโครงข่ายเป็นไปตามลำดับขั้นเรียบง่ายก็ใช้ Sequential ได้สบาย แต่ก็ไม่ได้ใช้แบบนั้นเสมอไป เช่นกรณีโครข่ายที่มีการแยกสายหรือการเชื่อมรวม

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

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

การสร้างทำได้ไม่ยาก แค่นิยามส่วน forward ให้ใช้เมธอด .flatten() กับตัวแปรที่รับมา
class Flatten(torch.nn.Module):
    def forward(self,x):
        return x.flatten()

flat = Flatten()

อย่างไรก็ตาม ที่จริงมีวิธีที่สั้นกว่านั้น คือใช้ lambda
flat = torch.nn.Module()
flat.forward = lambda x:x.flatten()

อาจลองสร้างโครงข่าย ๓ ชั้นในลักษณะนี้
khrongkhai = torch.nn.Sequential(
        torch.nn.Linear(2,80),
        relu,
        torch.nn.Linear(80,50),
        relu,
        torch.nn.Linear(50,1),
        flat)

แล้วลองนำมาใช้ดู
z = np.arange(2).repeat(120)
r = np.random.normal(z*2+2,0.5)
t = np.random.uniform(-0.5,0.5,240)*np.pi
x,y = r*np.cos(t),r*np.sin(t)
X = np.array([x,y]).T
X = torch.Tensor(X)
z = torch.Tensor(z)

opt = torch.optim.Adam(khrongkhai.parameters(),lr=0.1)
ha_entropy = torch.nn.BCEWithLogitsLoss()
for i in range(200):
    a = khrongkhai(X)
    J = ha_entropy(a,z)
    J.backward()
    opt.step()
    opt.zero_grad()

mx,my = np.meshgrid(np.linspace(x.min(),x.max(),200),np.linspace(y.min(),y.max(),200))
mX = torch.Tensor(np.array([mx.ravel(),my.ravel()]).T)
mz = khrongkhai(mX)>0
mz = mz.data.numpy().reshape(200,200)

plt.xlim(x.min(),x.max())
plt.ylim(y.min(),y.max())
plt.scatter(x,y,c=z.data.numpy(),edgecolor='k',cmap='Paired')
plt.contourf(mx,my,mz,alpha=0.2,cmap='Paired')
plt.show()





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


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

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

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

หมวดหมู่

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

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

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
มอดูลต่างๆ
-- 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月

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

ไทย

日本語

中文