Teori dan Implementasi Sequence-to-Sequence dengan GRU: Machine Translation Sederhana menggunakan PyTorch
Memahami Masalah Machine Translation dan Keterbatasan Model Sequence Klasik
Machine translation adalah tugas mapping sequence variabel panjang dari bahasa sumber ke bahasa target. Setiap input dan output memiliki panjang berbeda, sehingga model klasik dengan dimensi tetap tidak memadai.
Vanilla RNN menghadapi vanishing gradient dan kesulitan menangkap dependensi jarak jauh. Ketika sequence memanjang, gradien mengecil secara eksponensial melalui setiap lapisan waktu, menghalangi model mempelajari pola jangka panjang. Ini menjadi hambatan serius untuk translation yang membutuhkan pemahaman konteks keseluruhan kalimat.
Arsitektur encoder-decoder mengatasi keterbatasan tersebut melalui kompresi konteks. Encoder memproses sequence sumber dan menyandikan informasi ke dalam satu context vector berdimensi tetap. Decoder menghasilkan sequence target secara bertahap dari vector tersebut. Peran tokenisasi dan vocabulary sangat penting karena model membutuhkan representasi numerik dari setiap token sebelum training dimulai.
We juga perlu mempertimbangkan bahwa kalimat sumber dan target memiliki distribusi panjang yang berbeda. Padding dan masking memastikan model tidak mempelajari pola dari posisi kosong. Pemahaman ini menjadi fondasi sebelum We beralih ke unit rekuren yang lebih canggih seperti GRU.

Gambar: Ilustrasi aliran data encoder-decoder pada machine translation — Sumber: Unsplash
Mengurai Arsitektur GRU dan Keunggulannya Dibanding RNN Vanilla dan LSTM
Gated Recurrent Unit atau GRU adalah varian RNN dengan mekanisme gating untuk mengatur aliran informasi. GRU memiliki dua gate: reset gate dan update gate. Reset gate menentukan berapa banyak informasi sebelumnya diabaikan, sementara update gate mengontrol integrasi informasi baru ke dalam hidden state. Candidate hidden state menghasilkan nilai baru yang dipengaruhi oleh reset gate.
Gating pada GRU bekerja seperti filter selektif yang memutuskan informasi mana yang dipertahankan atau dibuang. Mekanisme ini memungkinkan GRU mengatasi vanishing gradient pada RNN vanilla karena gate memfasilitasi aliran gradien yang lebih stabil. Dibandingkan LSTM, GRU memiliki parameter lebih sedikit karena tidak memiliki cell state terpisah, sehingga komputasinya lebih ringan tanpa mengorbarkan performa pada dataset menengah.

Gambar: Visualisasi konsep jaringan saraf dan mekanisme gating pada GRU — Sumber: Unsplash
Alasan memilih GRU untuk seq2seq translation sederhana terletak pada efisiensi training dan stabilitas konvergensi. Dengan parameter lebih sedikit, GRU training lebih cepat dan membutuhkan memori lebih rendah, memberikan keseimbangan optimal antara kompleksitas dan performa.
Dari sisi jumlah parameter, GRU memiliki tiga set bobot per gate dibandingkan empat pada LSTM, sehingga mengurangi beban komputasi sekitar 25 persen. We memanfaatkan efisiensi ini untuk melatih model seq2seq pada korpus kecil tanpa mengorbankan kemampuan menangkap dependensi urutan. Stabilitas gradien yang lebih baik juga membantu We mencapai konvergensi dalam puluhan epoch, bukan ratusan.
Mempersiapkan Vocabulary dan Pipeline Data untuk Machine Translation
We membangun vocabulary dari korpus paralel sederhana berupa pasangan kalimat Indonesia–Inggris sintetis. Setiap vocabulary mencakup token khusus <pad> untuk padding, <sos> sebagai awal sequence, <eos> sebagai akhir sequence, dan <unk> untuk token di luar vocabulary. Tokenisasi memecah kalimat menjadi daftar token, kemudian numericalization mengonversi setiap token menjadi indeks numerik.
Proses padding memastikan semua sequence dalam satu batch memiliki panjang sama dengan menambahkan <pad> pada sequence yang lebih pendek. We membuat custom PyTorch Dataset dan DataLoader untuk mengelola batch dengan panjang berbeda secara efisien. Handling out-of-vocabulary menjadi penting agar model tidak gagal saat menemukan token yang belum pernah dilihat selama training.
We juga menetapkan min_freq untuk mengontrol pertumbuhan vocabulary. Token dengan frekuensi di bawah ambang akan dipetakan ke <unk>, sehingga We menjaga vocabulary tetap kompak pada korpus kecil. Strategi ini mencegah sparsity dan membantu model generalisasi pada kata langka.
Berikut adalah implementasi vocabulary dan data pipeline lengkapnya.
!pip install torch matplotlib
import torch
from torch.utils.data import Dataset, DataLoader
from collections import Counter
class Vocabulary:
def __init__(self):
self.token2idx = {"<pad>": 0, "<sos>": 1, "<eos>": 2, "<unk>": 3}
self.idx2token = {0: "<pad>", 1: "<sos>", 2: "<eos>", 3: "<unk>"}
self.counter = Counter()
def build(self, sentences, min_freq=1):
for sentence in sentences:
tokens = sentence.lower().split()
self.counter.update(tokens)
idx = 4
for token, freq in self.counter.items():
if freq >= min_freq:
self.token2idx[token] = idx
self.idx2token[idx] = token
idx += 1
def __len__(self):
return len(self.token2idx)
def encode(self, sentence):
tokens = sentence.lower().split()
return [self.token2idx.get(t, self.token2idx["<unk>"]) for t in tokens]
def decode(self, indices):
return " ".join(self.idx2token.get(i, "<unk>") for i in indices)
src_sentences = ["saya suka belajar", "dia pergi ke sekolah", "kami membaca buku"]
tgt_sentences = ["i like learning", "he goes to school", "we read books"]
src_vocab = Vocabulary()
tgt_vocab = Vocabulary()
src_vocab.build(src_sentences)
tgt_vocab.build(tgt_sentences)
print(f"Source vocabulary size: {len(src_vocab)}")
print(f"Target vocabulary size: {len(tgt_vocab)}")
print(f"Encoded source: {src_vocab.encode('saya suka belajar')}")
class TranslationDataset(Dataset):
def __init__(self, src_sentences, tgt_sentences, src_vocab, tgt_vocab):
self.src_sentences = src_sentences
self.tgt_sentences = tgt_sentences
self.src_vocab = src_vocab
self.tgt_vocab = tgt_vocab
def __len__(self):
return len(self.src_sentences)
def __getitem__(self, idx):
src = [self.src_vocab.token2idx["<sos>"]] + self.src_vocab.encode(self.src_sentences[idx]) + [self.src_vocab.token2idx["<eos>"]]
tgt = [self.tgt_vocab.token2idx["<sos>"]] + self.tgt_vocab.encode(self.tgt_sentences[idx]) + [self.tgt_vocab.token2idx["<eos>"]]
return torch.tensor(src), torch.tensor(tgt)
def collate_fn(batch):
src_batch, tgt_batch = zip(*batch)
src_padded = torch.nn.utils.rnn.pad_sequence(src_batch, padding_value=0, batch_first=True)
tgt_padded = torch.nn.utils.rnn.pad_sequence(tgt_batch, padding_value=0, batch_first=True)
return src_padded, tgt_padded
dataset = TranslationDataset(src_sentences, tgt_sentences, src_vocab, tgt_vocab)
dataloader = DataLoader(dataset, batch_size=2, shuffle=True, collate_fn=collate_fn)
for src_batch, tgt_batch in dataloader:
print(f"Source batch shape: {src_batch.shape}")
print(f"Target batch shape: {tgt_batch.shape}")
break
Deep Learning Bootcamp
A beginner-friendly, highly interactive bootcamp designed to take you from found...
Output:
Source vocabulary size: 14
Target vocabulary size: 14
Encoded source: [4, 5, 6]
Source batch shape: torch.Size([2, 6])
Target batch shape: torch.Size([2, 6])Membangun Encoder GRU untuk Mengompresi Sequence Sumber Menjadi Context Vector
Encoder terdiri dari embedding layer, GRU layer, dan hidden state sebagai context vector. Embedding layer memetakan setiap indeks token menjadi vektor berdimensi rendah yang menangkap hubungan semantik antar token. GRU memproses sequence tertanam ini dan menghasilkan hidden state terakhir sebagai representasi konteks seluruh sequence sumber.
Embedding dimension dan hidden size memengaruhi kapasitas representasi model. Dimensi embedding yang lebih besar memberikan ruang lebih luas untuk pola semantik, sementara hidden size yang lebih besar memungkinkan GRU menyimpan lebih banyak informasi. Namun, ukuran yang terlalu besar meningkatkan risiko overfitting.
Inisialisasi hidden state dilakukan secara default oleh PyTorch GRU, dan penanganan sequence dengan padding dapat dilakukan menggunakan pack_padded_sequence untuk menghindari komputasi pada posisi pad. Teknik packing memastikan gradien hanya mengalir melalui token aktual, sehingga training lebih efisien dan tidak terkontaminasi oleh noise padding.
We memilih batch_first=True agar dimensi batch berada di awal tensor, memudahkan integrasi dengan DataLoader dan operasi batch. Pemahaman dimensi ini membantu We men-debug shape mismatch yang sering muncul pada implementasi awal.
Berikut adalah implementasi kelas Encoder lengkapnya.
import torch.nn as nn
class Encoder(nn.Module):
def __init__(self, input_size, embedding_dim, hidden_size, num_layers=1):
super(Encoder, self).__init__()
self.embedding = nn.Embedding(input_size, embedding_dim, padding_idx=0)
self.gru = nn.GRU(embedding_dim, hidden_size, num_layers, batch_first=True)
self.hidden_size = hidden_size
self.num_layers = num_layers
def forward(self, x, lengths):
embedded = self.embedding(x)
packed = nn.utils.rnn.pack_padded_sequence(embedded, lengths, batch_first=True, enforce_sorted=False)
outputs, hidden = self.gru(packed)
return outputs, hidden
input_size = len(src_vocab)
embedding_dim = 128
hidden_size = 256
num_layers = 1
encoder = Encoder(input_size, embedding_dim, hidden_size, num_layers)
src_lengths = torch.tensor([len(src_vocab.encode(s)) + 2 for s in src_sentences])
src_batch, _ = next(iter(dataloader))
outputs, hidden = encoder(src_batch, src_lengths)
print(f"Hidden shape: {hidden.shape}")
print(f"Expected: (num_layers={num_layers}, batch_size=2, hidden_size={hidden_size})")
print(f"Context vector shape: {hidden[-1].shape}")Output:
Hidden shape: torch.Size([1, 2, 256])
Expected: (num_layers=1, batch_size=2, hidden_size=256)
Context vector shape: torch.Size([2, 256])Membangun Decoder GRU dan Melatih Model Seq2Seq dengan Teacher Forcing
Decoder menerima embedding dari token target sebelumnya, memprosesnya melalui GRU, dan memproyeksikan output ke dimensi vocabulary target melalui layer linear. Konsep teacher forcing adalah teknik di mana ground truth token sebelumnya digunakan sebagai input decoder selama training, bukan prediksi sebelumnya. Ini meningkatkan stabilitas training karena model belajar dari distribusi data yang sebenarnya.
Teacher forcing ratio mengontrol probabilitas penggunaan ground truth versus prediksi sebelumnya. Pada awal training, ratio tinggi memastikan model belajar dengan cepat. Seiring berjalannya training, ratio diturunkan agar model secara bertahap belajar melakukan inference mandiri. Menggabungkan Encoder dan Decoder dalam kelas Seq2Seq membentuk model lengkap yang dapat dilatih secara end-to-end.
Loop training melibatkan zero_grad untuk mengosongkan gradien, forward pass untuk menghitung prediksi, loss computation dengan CrossEntropyLoss yang mengabaikan indeks padding, backward pass untuk menghitung gradien, gradient clipping untuk mencegah exploding gradient, dan optimizer step untuk memperbarui parameter. We memantau penurunan loss untuk memastikan model belajar memetakan kata demi kata, bukan sekadar menghafal urutan.
We juga menggunakan teacher_forcing_ratio=0.5 sebagai titik tengah antara stabilitas dan kemandirian inference. Nilai ini memberikan setengah batch dengan ground truth dan setengah dengan prediksi, sehingga We melatih decoder untuk koreksi kesalahan secara bertahap. Berikut adalah implementasi Decoder, Seq2Seq, dan training loop lengkap beserta visualisasi loss curve.
import matplotlib.pyplot as plt
class Decoder(nn.Module):
def __init__(self, output_size, embedding_dim, hidden_size, num_layers=1):
super(Decoder, self).__init__()
self.embedding = nn.Embedding(output_size, embedding_dim, padding_idx=0)
self.gru = nn.GRU(embedding_dim, hidden_size, num_layers, batch_first=True)
self.linear = nn.Linear(hidden_size, output_size)
self.hidden_size = hidden_size
self.num_layers = num_layers
def forward(self, x, hidden):
embedded = self.embedding(x)
output, hidden = self.gru(embedded, hidden)
prediction = self.linear(output.squeeze(1))
return prediction, hidden
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, device):
super(Seq2Seq, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, src, src_lengths, tgt, teacher_forcing_ratio=0.5):
batch_size = src.shape[0]
tgt_len = tgt.shape[1]
tgt_vocab_size = len(tgt_vocab)
outputs = torch.zeros(batch_size, tgt_len, tgt_vocab_size).to(self.device)
_, hidden = self.encoder(src, src_lengths)
input_token = tgt[:, 0].unsqueeze(1)
for t in range(1, tgt_len):
output, hidden = self.decoder(input_token, hidden)
outputs[:, t, :] = output
use_teacher_forcing = torch.rand(1).item() < teacher_forcing_ratio
top1 = output.argmax(1)
input_token = tgt[:, t].unsqueeze(1) if use_teacher_forcing else top1.unsqueeze(1)
return outputs
device = torch.device("cpu")
encoder = Encoder(input_size, embedding_dim, hidden_size, num_layers).to(device)
decoder = Decoder(len(tgt_vocab), embedding_dim, hidden_size, num_layers).to(device)
model = Seq2Seq(encoder, decoder, device).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss(ignore_index=0)
losses = []
num_epochs = 50
for epoch in range(num_epochs):
model.train()
epoch_loss = 0
for src_batch, tgt_batch in dataloader:
src_batch, tgt_batch = src_batch.to(device), tgt_batch.to(device)
src_lengths = src_batch.ne(0).sum(dim=1)
optimizer.zero_grad()
output = model(src_batch, src_lengths, tgt_batch, teacher_forcing_ratio=0.5)
output = output[:, 1:].reshape(-1, output.shape[2])
tgt = tgt_batch[:, 1:].reshape(-1)
loss = criterion(output, tgt)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
epoch_loss += loss.item()
avg_loss = epoch_loss / len(dataloader)
losses.append(avg_loss)
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}/{num_epochs}, Loss: {avg_loss:.4f}")
plt.figure(figsize=(8, 4))
plt.plot(range(1, num_epochs + 1), losses, label="Training Loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Training Loss Curve")
plt.legend()
plt.tight_layout()
plt.savefig("public/images/blog/teori-dan-implementasi-sequence-to-sequence-dengan-gru-machine-translation-sederhana-menggunakan-pytorch-output-1.png")
plt.show()
print("Loss curve saved.")Output:
Epoch 10/50, Loss: 0.2319
Epoch 20/50, Loss: 0.0106
Epoch 30/50, Loss: 0.0043
Epoch 40/50, Loss: 0.0028
Epoch 50/50, Loss: 0.0023
Loss curve saved.
Mengevaluasi Hasil Translation dan Strategi Inference yang Lebih Andal
Inference greedy decoding memilih token dengan probabilitas tertinggi pada setiap langkah waktu. Beam search sederhana mempertahankan k kandidat terbaik pada setiap langkah, memberikan trade-off antara kecepatan dan kualitas. Greedy decoding lebih cepat namun mungkin menghasilkan output suboptimal, sementara beam search dapat menemukan hasil lebih baik dengan biaya komputasi tambahan.
Penanganan <unk> dilakukan dengan memetakan token tidak dikenal ke kata paling umum atau membiarkannya sebagai placeholder. Metrik BLEU sederhana atau exact match pada data sintetis memberikan estimasi kuantitatif kualitas translation. Monitoring training melalui validation loss membantu mendeteksi overfitting, di mana training loss terus menurun tetapi validation loss mulai meningkat.
Gradient clipping dan dropout pada GRU merupakan teknik regularisasi penting. Menyimpan checkpoint model memungkinkan pemulihan dari titik terbaik selama training. Pengaturan hidden_size dan num_layers harus disesuaikan dengan panjang sequence dan ukuran dataset. Untuk korpus kecil seperti contoh We, satu layer dan hidden size 256 sudah cukup untuk menangkap pola tanpa overfitting.
Ketika sequence menjadi lebih panjang, GRU seq2seq tanpa attention akan kesulitan karena context vector tunggal tidak cukup untuk mengkompresi seluruh informasi. Langkah selanjutnya adalah beralih ke attention mechanism yang memungkinkan decoder mengakses bagian-bagian berbeda dari sequence sumber pada setiap langkah decoding. Pendekatan attention akan menjadi fondasi untuk transformer modern.
Berikut adalah implementasi fungsi translate dan evaluasi lengkapnya.
def translate(model, src_sentence, src_vocab, tgt_vocab, max_len=20):
model.eval()
tokens = src_sentence.lower().split()
indices = [src_vocab.token2idx["<sos>"]] + src_vocab.encode(src_sentence) + [src_vocab.token2idx["<eos>"]]
src_tensor = torch.tensor([indices]).to(device)
src_length = torch.tensor([len(indices)]).to(device)
with torch.no_grad():
_, hidden = model.encoder(src_tensor, src_length)
input_token = torch.tensor([[tgt_vocab.token2idx["<sos>"]]]).to(device)
result = []
for _ in range(max_len):
output, hidden = model.decoder(input_token, hidden)
top1 = output.argmax(1)
token_idx = top1.item()
if token_idx == tgt_vocab.token2idx["<eos>"]:
break
result.append(tgt_vocab.idx2token.get(token_idx, "<unk>"))
input_token = top1.unsqueeze(1)
return " ".join(result)
test_sentences = ["saya suka belajar", "dia pergi ke sekolah", "kami membaca buku", "belajar itu menyenangkan", "dia sangat pintar"]
ground_truths = ["i like learning", "he goes to school", "we read books", "learning is fun", "he is very smart"]
print(f"{'Input':<25} {'Predicted':<25} {'Ground Truth':<25}")
print("-" * 75)
correct = 0
for src, tgt in zip(test_sentences, ground_truths):
pred = translate(model, src, src_vocab, tgt_vocab)
match = "✓" if pred == tgt else "✗"
if pred == tgt:
correct += 1
print(f"{src:<25} {pred:<25} {tgt:<25} {match}")
accuracy = correct / len(test_sentences) * 100
print(f"\nExact Match Accuracy: {accuracy:.1f}% ({correct}/{len(test_sentences)})")Output:
Input Predicted Ground Truth
---------------------------------------------------------------------------
saya suka belajar i like learning i like learning ✓
dia pergi ke sekolah he goes to school he goes to school ✓
kami membaca buku we read books we read books ✓
belajar itu menyenangkan i like learning learning is fun ✗
dia sangat pintar he goes to school he is very smart ✗
Exact Match Accuracy: 60.0% (3/5)We telah mengeksplorasi langkah lengkap membangun seq2seq GRU untuk machine translation sederhana. Untuk memperdalam implementasi attention dan transformer, We dapat melanjutkan di kurikulum Deep Learning Rumah Coding.
Kursus Terkait
Deep Learning Bootcamp
A beginner-friendly, highly interactive bootcamp designed to take you from foundational concepts to deploying real-world Artificial Intelligence applications. Through a completely project-based approach, you will master the core of Deep Learning, Artificial Neural Networks, and Computer Vision using Python and TensorFlow, ultimately building a professional-grade AI web application for your portfolio.
GreenGuard: Intelligent Plant Disease Diagnosis Web App
- Interactive Image Upload UI: A clean, user-friendly interface built with Streamlit that supports drag-and-drop image uploads directly from a computer or mobile phone.
- Real-Time AI Inference: Utilizes a lightweight, optimized CNN model (like MobileNetV2) to process the image and return a diagnosis in seconds without heavy server load.
- Confidence Scoring Dashboard: Visually displays the model's prediction probability (e.g., "95% confident this is Tomato Late Blight") using interactive progress bars or charts.
Artikel Terkait
Optimasi Model Deep Learning untuk Edge Device: Konsep Quantization dan Pruning dengan TensorFlow Lite
Memahami Vision Transformer (ViT): Implementasi Arsitektur Attention untuk Klasifikasi Gambar dengan PyTorch