File size: 5,349 Bytes
ee7f83c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import torch
import torch.nn as nn


class Encoder(nn.Module):
    """
    Bidirectional RNN/GRU/LSTM encoder for the Filipino source sentence.

    The encoder processes the entire source sequence and produces two things:
      1. Hidden states for every input token β€” these are used by the attention mechanism
         in the decoder to figure out which parts of the source to focus on.
      2. A single summary hidden state β€” this initializes the decoder so it has a
         starting sense of what the source sentence said.

    We use a bidirectional RNN so each token sees both its left and right context,
    which generally improves translation quality.
    """

    def __init__(
        self,
        source_vocab_size: int,
        embedding_dim: int,
        hidden_size: int,
        num_layers: int,
        dropout: float,
        rnn_type: str = "gru",
    ) -> None:
        super().__init__()

        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.rnn_type = rnn_type.lower()

        # Embedding layer turns integer token ids into dense float vectors
        # padding_idx=0 means the padding token contributes zero gradient
        self.embedding = nn.Embedding(source_vocab_size, embedding_dim, padding_idx=0)

        # Dropout is applied to the embeddings before they enter the RNN
        self.dropout = nn.Dropout(dropout)

        # Choose the RNN variant based on the config value
        rnn_options = {"rnn": nn.RNN, "gru": nn.GRU, "lstm": nn.LSTM}
        if self.rnn_type not in rnn_options:
            raise ValueError(
                f"rnn_type must be one of {list(rnn_options.keys())}, got '{rnn_type}'"
            )

        rnn_class = rnn_options[self.rnn_type]

        # bidirectional=True makes the RNN read the sequence both forward and backward
        # The output at each position is the concatenation of both directions, so the
        # effective output size doubles to hidden_size * 2
        self.rnn = rnn_class(
            input_size=embedding_dim,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True,
            bidirectional=True,
            dropout=dropout if num_layers > 1 else 0,
        )

        # The decoder is unidirectional (hidden_size), but the encoder produces a
        # bidirectional hidden state (hidden_size * 2). This linear layer bridges the gap
        # by projecting the concatenated forward+backward final state into hidden_size
        self.hidden_projection = nn.Linear(hidden_size * 2, hidden_size)

    def forward(
        self,
        source_tokens: torch.Tensor,
        source_lengths: torch.Tensor,
    ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
        # source_tokens:  (batch_size, source_len) β€” integer token ids for the Filipino input
        # source_lengths: (batch_size,) β€” the real (non-padded) length of each sentence

        embedded = self.dropout(self.embedding(source_tokens))
        # embedded: (batch_size, source_len, embedding_dim)

        # Packing tells the RNN to skip padding positions, which saves compute
        # and prevents padding from contaminating the hidden state
        packed_input = nn.utils.rnn.pack_padded_sequence(
            embedded,
            source_lengths.cpu(),
            batch_first=True,
            enforce_sorted=False,
        )

        if self.rnn_type == "lstm":
            packed_outputs, (hidden, cell) = self.rnn(packed_input)
        else:
            packed_outputs, hidden = self.rnn(packed_input)
            cell = None  # RNN and GRU don't have a cell state

        # Unpack back to a padded tensor so we can run attention over all positions
        encoder_outputs, _ = nn.utils.rnn.pad_packed_sequence(
            packed_outputs, batch_first=True
        )
        # encoder_outputs: (batch_size, source_len, hidden_size * 2)

        # hidden has shape (num_layers * num_directions, batch_size, hidden_size)
        # For a 2-layer bidirectional model the layout is:
        #   index 0 β€” layer 1 forward
        #   index 1 β€” layer 1 backward
        #   index 2 β€” layer 2 forward  (top forward layer)
        #   index 3 β€” layer 2 backward (top backward layer)
        # We only want the top layer, which is the most abstract representation
        top_forward_hidden = hidden[-2]  # (batch_size, hidden_size)
        top_backward_hidden = hidden[-1]  # (batch_size, hidden_size)

        # Concatenate then project into the decoder's expected hidden size
        decoder_initial_hidden = torch.tanh(
            self.hidden_projection(
                torch.cat([top_forward_hidden, top_backward_hidden], dim=1)
            )
        )
        # decoder_initial_hidden: (batch_size, hidden_size)

        # Do the same projection for the cell state if we're using LSTM
        decoder_initial_cell = None
        if self.rnn_type == "lstm":
            top_forward_cell = cell[-2]
            top_backward_cell = cell[-1]
            decoder_initial_cell = torch.tanh(
                self.hidden_projection(
                    torch.cat([top_forward_cell, top_backward_cell], dim=1)
                )
            )
            # decoder_initial_cell: (batch_size, hidden_size)

        return encoder_outputs, decoder_initial_hidden, decoder_initial_cell