Machine Learning Academy · บทเรียน

RNN แบบพื้นฐาน: สถานะซ่อนเร้นและการคลี่ลำดับ

ผู้เรียนจะสร้างเซลล์ RNN แบบหนึ่งขั้นด้วยตนเอง คลี่การทำงานไปตามลำดับสั้น ๆ และแสดงภาพว่าสถานะซ่อนเร้นสะสมบริบทอย่างไร

บทเรียน 1 จาก 413 ขั้นตอน

RNN แบบพื้นฐาน: สถานะซ่อนเร้นและการคลี่ลำดับ เป็นบทเรียน Machine Learning Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Machine Learning Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Machine Learning Academy มีบทเรียนทั้งหมด 4 บทเรียน

เหตุใดเราจึงต้องใช้โครงข่ายเวียนกลับ

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

# Examples of sequential data:
sequences = {
    'NLP': 'The cat sat on the ___  (predict next word)',
    'Time Series': '[1.2, 1.5, 1.3, 1.8, ???]',
    'Speech': '[audio_t0, audio_t1, ..., audio_tN]',
    'Video': '[frame_1, frame_2, ..., frame_T]',
    'DNA': 'ATCGATCG... (biological sequence)',
}
for name, example in sequences.items():
    print(f'{name}: {example}')

เซลล์ RNN แบบพื้นฐาน

เซลล์ RNN แบบพื้นฐานรับอินพุตสองรายการ ได้แก่ อินพุตปัจจุบัน x_t และสถานะซ่อนเร้นก่อนหน้า h_{t-1} จากนั้นสร้างสถานะซ่อนเร้นถัดไป h_t โดยใช้สูตร h_t = tanh(W_hh * h_{t-1} + W_xh * x_t + b) เมทริกซ์น้ำหนัก W_hh และ W_xh เดิมจะถูกใช้ในทุกช่วงเวลา ซึ่งเรียกว่าการใช้น้ำหนักร่วมกันตามเวลา คล้ายกับการที่ CNN ใช้น้ำหนักร่วมกันตามตำแหน่งเชิงพื้นที่ สถานะซ่อนเร้นจะเก็บความจำของโครงข่ายเกี่ยวกับอินพุตทั้งหมดในอดีต

import torch

def rnn_cell(x_t, h_prev, W_xh, W_hh, b):
    '''One step of a vanilla RNN cell'''
    # x_t: (batch, input_size)
    # h_prev: (batch, hidden_size)
    h_t = torch.tanh(
        x_t @ W_xh.T +    # input contribution
        h_prev @ W_hh.T +  # hidden-to-hidden contribution
        b                  # bias
    )
    return h_t

# Example: input_size=4, hidden_size=8
batch = 3
x_t   = torch.randn(batch, 4)
h_prev = torch.zeros(batch, 8)
W_xh  = torch.randn(8, 4) * 0.01
W_hh  = torch.randn(8, 8) * 0.01
b     = torch.zeros(8)
h_t = rnn_cell(x_t, h_prev, W_xh, W_hh, b)
print(h_t.shape)   # (3, 8)

การคลี่ RNN ตามเวลา

ในการประมวลผลลำดับที่มีความยาว T จะใช้เซลล์ RNN ซ้ำ T ครั้งในลูป ซึ่งเรียกว่าการคลี่ (หรือการกาง) ตามเวลา สถานะซ่อนเร้นจากขั้นที่ t-1 จะถูกส่งต่อไปยังขั้นที่ t เพื่อเชื่อมโยงทุกช่วงเวลา ระหว่างการแพร่ย้อนกลับ เกรเดียนต์ต้องไหลย้อนกลับผ่านทุกช่วงเวลา ซึ่งเรียกว่าการแพร่ย้อนกลับตามเวลา (BPTT) ความลึกของกราฟที่คลี่ออกนี้เท่ากับความยาวของลำดับ จึงทำให้เกิดความท้าทายเมื่อลำดับยาว

import torch
import torch.nn as nn

# Unroll RNN manually over a sequence
batch_size, seq_len, input_size, hidden_size = 4, 10, 8, 16

rnn_cell = nn.RNNCell(input_size, hidden_size)
sequence = torch.randn(seq_len, batch_size, input_size)

h = torch.zeros(batch_size, hidden_size)  # initial hidden state
hidden_states = []

for t in range(seq_len):
    x_t = sequence[t]   # (batch, input_size)
    h = rnn_cell(x_t, h)  # apply RNN cell
    hidden_states.append(h)

print('Final hidden state:', h.shape)     # (4, 16)
print('All hidden states:', len(hidden_states), 'steps')

การใช้ nn.RNN: รุ่นโมดูล

nn.RNNจัดการการคลี่ตามเวลาให้โดยอัตโนมัติ รับอินพุตที่มีรูปร่าง (seq_len, batch, input_size) (หรือ (batch, seq_len, input_size) เมื่อใช้ batch_first=True) และส่งคืนสถานะซ่อนเร้นทั้งหมดพร้อมสถานะซ่อนเร้นสุดท้าย พารามิเตอร์สำคัญ ได้แก่ num_layers ซึ่งซ้อนชั้น RNN หลายชั้น bidirectional=True ซึ่งประมวลผลลำดับทั้งสองทิศทาง และ dropout ซึ่งใช้ดรอปเอาต์ระหว่างชั้นใน RNN หลายชั้น

import torch
import torch.nn as nn

rnn = nn.RNN(
    input_size=16,
    hidden_size=32,
    num_layers=2,
    batch_first=True,    # input: (batch, seq, features)
    dropout=0.2          # between layers
)

X = torch.randn(8, 20, 16)   # batch=8, seq=20, features=16
output, h_n = rnn(X)

print('Output shape:', output.shape)   # (8, 20, 32) all steps
print('h_n shape:', h_n.shape)         # (2, 8, 32) last hidden

การกำหนดค่าเริ่มต้นของสถานะซ่อนเร้น

สถานะซ่อนเร้นเริ่มต้น h_0 จะถูกส่งเป็นอาร์กิวเมนต์ตัวที่สองให้กับ nn.RNN หากไม่ระบุ ระบบจะกำหนดเป็นศูนย์โดยปริยาย สำหรับการจำแนกลำดับ สถานะซ่อนเร้นสุดท้าย h_n จะสรุปลำดับทั้งหมด สำหรับงานลำดับต่อลำดับ จะใช้สถานะซ่อนเร้นระหว่างทางทั้งหมดใน output การกำหนด h_0 เป็นศูนย์ถือเป็นมาตรฐาน แต่บางแอปพลิเคชันจะเรียนรู้สถานะเริ่มต้นเป็นพารามิเตอร์ เพื่อเพิ่มประสิทธิภาพกับลำดับสั้น

import torch
import torch.nn as nn

rnn = nn.RNN(8, 16, batch_first=True)
X = torch.randn(4, 10, 8)   # batch=4, seq=10, input=8

# Default: h_0 = zeros
output, h_n = rnn(X)
print('h_n with zero init:', h_n.shape)  # (1, 4, 16)

# Custom initial hidden state
h_0 = torch.randn(1, 4, 16)  # (num_layers, batch, hidden)
output, h_n = rnn(X, h_0)
print('h_n with custom init:', h_n.shape)  # (1, 4, 16)

การจำแนกลำดับด้วยสถานะซ่อนเร้นสุดท้าย

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

import torch
import torch.nn as nn

class SequenceClassifier(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super().__init__()
        self.rnn = nn.RNN(input_size, hidden_size,
                          batch_first=True)
        self.fc  = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        _, h_n = self.rnn(x)
        # h_n shape: (1, batch, hidden) -> squeeze to (batch, hidden)
        h_n = h_n.squeeze(0)
        return self.fc(h_n)

model = SequenceClassifier(16, 32, 5)
X = torch.randn(8, 20, 16)
logits = model(X)
print(logits.shape)   # (8, 5) -- 8 samples, 5 classes

หลายต่อหลาย: การติดป้ายกำกับลำดับ

ในงานการติดป้ายกำกับลำดับ (การติดแท็ก POS การรู้จำเอนทิตีที่มีชื่อ และการตรวจจับความผิดปกติในอนุกรมเวลา) จำเป็นต้องทำนายที่ทุกช่วงเวลา ไม่ใช่เฉพาะช่วงสุดท้าย ให้ใช้เทนเซอร์ output ทั้งหมดจาก nn.RNN (รูปร่าง: batch x seq_len x hidden_size) แล้วใช้ชั้นเชิงเส้นกับแต่ละช่วงเวลาแยกกัน น้ำหนักของชั้นเชิงเส้นจะใช้ร่วมกันตลอดทุกช่วงเวลา ซึ่งเป็นอีกตัวอย่างหนึ่งของการใช้น้ำหนักร่วมกันในโมเดลลำดับ

import torch
import torch.nn as nn

class SequenceLabeler(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super().__init__()
        self.rnn = nn.RNN(input_size, hidden_size,
                          batch_first=True)
        self.fc  = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        output, _ = self.rnn(x)  # (batch, seq, hidden)
        return self.fc(output)   # (batch, seq, num_classes)

model = SequenceLabeler(8, 16, 3)
X = torch.randn(4, 10, 8)       # 4 seqs of 10 timesteps
logits = model(X)
print(logits.shape)              # (4, 10, 3) per-step labels

RNN แบบซ้อนชั้นและสองทิศทาง

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

import torch
import torch.nn as nn

# Stacked bidirectional RNN
brnn = nn.RNN(
    input_size=16,
    hidden_size=32,
    num_layers=3,          # 3 stacked layers
    batch_first=True,
    bidirectional=True     # forward + backward
)

X = torch.randn(4, 10, 16)
output, h_n = brnn(X)

# Output: (batch, seq, hidden*2) because bidirectional
print('Output shape:', output.shape)  # (4, 10, 64)
# h_n: (num_layers*2, batch, hidden) -- 2 dirs x 3 layers
print('h_n shape:', h_n.shape)        # (6, 4, 32)

การแพร่ย้อนกลับผ่านเวลา (BPTT)

การแพร่ย้อนกลับผ่านเวลา จะคลี่ RNN ออกตามลำดับเวลา แล้วใช้การแพร่ย้อนกลับมาตรฐานกับกราฟที่คลี่ออกนั้น สำหรับลำดับที่มีความยาว T จะคำนวณเกรเดียนต์ในทุก timestep และเผยแพร่ย้อนกลับ เกรเดียนต์ของค่าความสูญเสียเมื่อเทียบกับพารามิเตอร์จะเกี่ยวข้องกับผลคูณของเมทริกซ์น้ำหนัก W_hh เดิมที่คูณกัน T ครั้ง เมื่อรัศมีสเปกตรัมของ W_hh น้อยกว่า 1 ผลคูณเหล่านี้จะ หายไป แต่เมื่อมากกว่า 1 ผลคูณจะ พุ่งระเบิด ซึ่งเป็นความท้าทายพื้นฐานของการฝึก RNN กับลำดับที่ยาว

import torch
import torch.nn as nn

# Demonstrate gradient flow through different sequence lengths
rnn = nn.RNNCell(4, 8)
h = torch.zeros(1, 8, requires_grad=True)

# Short sequence: gradients flow back relatively well
for t in range(5):
    x = torch.randn(1, 4)
    h = rnn(x, h)
loss = h.sum()
loss.backward()
print('h.grad (seq=5):', h.grad.norm().item())

# For long sequences (T=100+), vanilla RNN gradients
# typically vanish (near zero) or explode (very large)
# This is why LSTM/GRU were invented

การจัดการลำดับที่มีความยาวแปรผัน

ในทางปฏิบัติ ลำดับในชุดข้อมูลย่อยเดียวกันมักมีความยาวต่างกัน เช่น ประโยคที่มีจำนวนคำไม่เท่ากัน PyTorch จัดการกรณีนี้ด้วย ลำดับแบบแพ็ก: torch.nn.utils.rnn.pack_padded_sequence จะนำส่วนเติมให้เต็มออกจากชุดข้อมูลย่อย แล้วแพ็กลำดับอย่างมีประสิทธิภาพ หลังผ่าน RNN แล้ว pad_packed_sequence จะคืนรูปแบบที่มีส่วนเติมให้เต็มกลับมา หากไม่ใช้การแพ็ก RNN จะประมวลผลโทเค็นส่วนเติมให้เต็มโดยไม่จำเป็น และอาจทำให้สถานะซ่อนเร้นปนเปื้อนด้วยข้อมูลส่วนเติมให้เต็มที่ไม่มีความหมาย

import torch
import torch.nn as nn
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence

rnn = nn.RNN(4, 8, batch_first=True)

# Padded sequences: length 5, 3, 2
X = torch.zeros(3, 5, 4)  # (batch=3, max_seq=5, features=4)
X[0, :, :] = torch.randn(5, 4)  # full seq
X[1, :3, :] = torch.randn(3, 4) # length 3
X[2, :2, :] = torch.randn(2, 4) # length 2
lengths = torch.tensor([5, 3, 2])

packed = pack_padded_sequence(X, lengths, batch_first=True)
out_packed, h_n = rnn(packed)
out, _ = pad_packed_sequence(out_packed, batch_first=True)
print(out.shape)  # (3, 5, 8) -- back to padded form

ควรใช้ RNN แบบพื้นฐานเมื่อใด

โดยทั่วไปแทบไม่ใช้ RNN แบบพื้นฐานในทางปฏิบัติ เพราะประสบปัญหาเกรเดียนต์หายไปอย่างรุนแรงกับลำดับที่ยาวกว่า 10-20 timestep โดยหลักแล้วเหมาะสำหรับ วัตถุประสงค์ด้านการเรียนรู้ และ ลำดับที่สั้นมาก สำหรับการใช้งานจริงที่มีลำดับยาวกว่า 20 timestep ให้ใช้ LSTM หรือ GRU ซึ่งมีกลไกประตูที่ออกแบบมาโดยเฉพาะเพื่อรักษาข้อมูลเมื่อระยะห่างยาวขึ้น สำหรับลำดับที่ลำดับก่อนหลังมีความสำคัญน้อย สถาปัตยกรรม Transformer มักให้ผลดีกว่าทั้งสองแบบ

# When to use each sequence model:
use_cases = {
    'Vanilla RNN (nn.RNN)':  'Short sequences (<20 steps), learning/demos',
    'LSTM':                  'Long sequences, NLP, time-series (general)',
    'GRU':                   'Similar to LSTM but faster, fewer params',
    'Transformer':           'Parallelisable, long documents, state-of-art NLP',
    'Temporal Conv (TCN)':   'Long sequences, strong parallelism, audio',
}
for model, use in use_cases.items():
    print(f'{model}: {use}')

ตรวจสอบความเข้าใจอย่างรวดเร็ว

ทดสอบความเข้าใจแนวคิดการเรียนรู้ของเครื่องด้วย Python จากบทเรียนนี้

สรุปบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า RNN แบบพื้นฐานจะรักษาสถานะซ่อนเร้น ซึ่งเก็บความจำข้าม timestep โดยใช้สูตร h_t = tanh(W_xh * x_t + W_hh * h_{t-1} + b), การคลี่ลำดับจะใช้เซลล์เดิมในทุก timestep โดยใช้น้ำหนักร่วมกัน และ BPTT จะแพร่เกรเดียนต์ย้อนกลับผ่านกราฟที่คลี่ออก ทำให้เกิดปัญหาเกรเดียนต์หายไปหรือพุ่งระเบิดในลำดับยาว ต่อไปเราจะตรวจสอบปัญหาเกรเดียนต์หายไปอย่างละเอียด และทำความเข้าใจว่าเหตุใดจึงออกแบบ LSTM ขึ้นมาเพื่อแก้ปัญหานี้

เริ่มต้นได้ฟรี

เรียนรู้ Python ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
30
บทเรียน
120

คำถามที่พบบ่อย

บทเรียน “RNN แบบพื้นฐาน: สถานะซ่อนเร้นและการคลี่ลำดับ” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “RNN แบบพื้นฐาน: สถานะซ่อนเร้นและการคลี่ลำดับ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Machine Learning Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Machine Learning Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “RNN แบบพื้นฐาน: สถานะซ่อนเร้นและการคลี่ลำดับ”

ผู้เรียนจะสร้างเซลล์ RNN แบบหนึ่งขั้นด้วยตนเอง คลี่การทำงานไปตามลำดับสั้น ๆ และแสดงภาพว่าสถานะซ่อนเร้นสะสมบริบทอย่างไร คุณปฏิบัติ Machine Learning Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Machine Learning Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Machine Learning Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน

บทเรียน “RNN แบบพื้นฐาน: สถานะซ่อนเร้นและการคลี่ลำดับ” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Machine Learning Academy นี้ได้ไหม

ได้ บทเรียน Machine Learning Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. RNN แบบพื้นฐาน: สถานะซ่อนเร้นและการคลี่ลำดับ
  2. ปัญหาเกรเดียนต์หายไปในขั้นเวลาจำนวนมาก
  3. เซลล์ LSTM: ประตูรับเข้า ลืม และส่งออก
  4. จากลำดับสู่หนึ่งค่า: การวิเคราะห์ความรู้สึกด้วย LSTM
← กลับไปที่ Machine Learning Academy