1. Seq2Seq + Attention을 이용한 번역기 구현
■ 시퀀스투시퀀스(Sequence‑to‑Sequence, seq2seq) (2) 에서 일반적인 Seq2Seq 구조를 이용한 번역기를 구현하였다. 이번 예시에서는 여기에 어텐션 메커니즘을 추가하여 번역기를 구현한다. 데이터 전처리 및 데이터 로드 과정은 동일하다.
1.1 Seq2Seq + Attention의 Encoder
■ Encoder 구조는 일반적인 Seq2Seq의 Encoder 구조와 동일하다. 입력 시퀀스가 들어오면, 입력 시퀀스는 임베딩 계층과 LSTM 계층을 통과하여 은닉 상태를 반환한다.
class Encoder(nn.Module):
def __init__(self, src_vocab_size, embedding_dim, hidden_units):
super(Encoder, self).__init__()
self.embedding = nn.Embedding(src_vocab_size, embedding_dim, padding_idx=0)
self.lstm = nn.LSTM(embedding_dim, hidden_units, batch_first=True)
def forward(self, x):
x = self.embedding(x) # x.shape: (batch_size, source_seq_len, embedding_dim)
outputs, (hidden, cell) = self.lstm(x) # hidden.shape: (1, batch_size, hidden_units) # cell.shape: (1, batch_size, hidden_units)
return outputs, hidden, cell
■ 여기서 모든 타임 스텝의 은닉 상태를 담은 outputs이 Attention 계산에 사용된다.
■ 마지막 타임 스텝의 hidden과 셀 상태 cell은 Decoder의 초기 상태로 전달된다.
1.2 Seq2Seq + Attention의 Decoder
■ 어텐션 메커니즘은 Decoder에서 어텐션 값(컨텍스트 벡터)을 계산하기 위해 Query와 Key로 어텐션 스코어를 계산하는 것으로 시작된다.
class Decoder(nn.Module):
def __init__(self, tar_vocab_size, embedding_dim, hidden_units):
super(Decoder, self).__init__()
self.embedding = nn.Embedding(tar_vocab_size, embedding_dim, padding_idx=0)
self.lstm = nn.LSTM(embedding_dim + hidden_units, hidden_units, batch_first=True)
self.fc = nn.Linear(hidden_units, tar_vocab_size)
self.softmax = nn.Softmax(dim=1) # 어텐션 가중치를 계산하기 위한 소프트맥스 함수
def forward(self, x, encoder_outputs, hidden, cell):
x = self.embedding(x) # x.shape: (batch_size, target_seq_len, embedding_dim)
## Dot-Product Attention
attention_scores = torch.bmm(encoder_outputs, hidden.transpose(0, 1).transpose(1, 2)) # attention_scores.shape: (batch_size, source_seq_len, 1)
attention_weights = self.softmax(attention_scores) # attention_weights.shape: (batch_size, source_seq_len, 1)
context_vector = torch.bmm(attention_weights.transpose(1, 2), encoder_outputs) # context_vector.shape: (batch_size, 1, hidden_units)
## Repeat context_vector
seq_len = x.shape[1]
context_vector_repeated = context_vector.repeat(1, seq_len, 1) # context_vector.shape: (batch_size, target_seq_len, hidden_units)
## Concat context_vector and embedded input
x = torch.cat((x, context_vector_repeated), dim=2) # x.shape: (batch_size, target_seq_len, embedding_dim + hidden_units)
outputs, (hidden, cell) = self.lstm(x, (hidden, cell))
# outputs.shape: (batch_size, target_seq_len, hidden_units)
# hidden.shape: (batch_size, 1, hidden_units), cell.shape: (batch_size, 1, hidden_units)
output = self.fc(outputs) # output.shape: (batch_size, target_seq_len, tar_vocab_size)
return output, hidden, cell
1.2.1 어텐션 스코어 계산
■ 이 예에서는 닷-프로덕트 어텐션을 적용하기 위해 Query인 디코더의 현재 시점 은닉 상태(hidden)와 Key인 인코더의 모든 시점의 은닉 상태(encoder_outputs)와 내적을 계산한다.
- 내적에 대한 코드는 torch.bmm(encoder_outputs, hidden.transpose(0, 1).transpose(1, 2))이다.
■ 이때, 처리 대상이 배치 단위의 데이터이므로, 배치 행렬 곱(Batch Matrix Multiplication, BMM)을 수행하는 파이토치 함수 torch.bmm()을 사용한다.
■ 이 함수는 두 개 이상의 차원을 지닌 텐서가 주어졌을 때, 뒤의 두 개 차원에 대해 행렬 곱을 수행하는 함수이다.
■ 예를 들어, mat1라는 텐서의 형상이 \( \left( b \times n \times m \right) \), mat2라는 텐서의 형상이 \( \left( b \times m \times p \right) \)라면, torch.bpm(mat1, mat2)의 결과 텐서의 형상은 \( \left( b \times n \times p \right) \)이 된다.
■ 즉, 첫 번재 차원(dim=0)을 제외한 나머지 차원의 행렬 곱을 수행하기 때문에 \( \left( n \times m \right) \times \left( m \times p \right) \)의 결과로 \( \left( b \times n \times p \right) \)이 되는 것이다.
■ torch.transpose() 함수는 두 개의 차원을 맞교환할 때 사용하는 함수이다.
■ 예를 들어 (10, 3, 4) 형상을 갖는 mat1에 대해 첫 번째 차원(dim=0)과 두 번째 차원(dim=1)을 교환하는 예는 다음과 같다.
mat1 = torch.randn(10, 3, 4)
mat2 = torch.randn(10, 4, 5)
res = torch.bmm(mat1, mat2)
print(res.shape)
```#결과#```
torch.Size([10, 3, 5])
````````````
torch.transpose(mat1, 0, 1).shape
```#결과#```
torch.Size([3, 10, 4])
````````````
mat2.transpose(0, 1).shape
```#결과#```
torch.Size([4, 10, 5])
````````````
cf) torch.permute() 함수는 모든 차원들을 맞교환할 수 있다.
- 예를 들어, 첫 번째 차원(dim=0)에는 세 번째 차원(dim=2)으로, 두 번째 차원(dim=1)은 첫 번째 차원(dim=0)으로, 세 번째 차원(dim=2)은 두 번째 차원(dim=1)으로 교환하고 싶다면,
mat1.permute(2, 0, 1).shape
```#결과#```
torch.Size([4, 10, 3])
````````````
■ 디코더의 현재 시점 은닉 상태(hidden)와 Key인 인코더의 모든 시점의 은닉 상태(encoder_outputs)와 내적을 계산한 결과가 어텐션 스코어이다.
■ 이 예에서는 batch_first = True를 적용했는데, 그 결과로 인코더의 모든 시점의 은닉 상태를 나타내는 encoder_outputs의 shape은 (batch_size, source_seq_len, hidden_units)이며, 디코더의 현재 시점 은닉 상태는 하나의 시점에 대한 은닉 상태이므로 shape은 (1, batch_size, hidden_units)이다.
■ torch.bmm() 함수로 배치 행렬 곱을 계산하기 위해서는, 먼저 디코더의 은닉 상태의 차원 중 첫 번째 차원(dim=0)과 두 번째 차원(dim=1)을 교환해야 한다.
- hidden.transpose(0, 1).shape: (batch_size, 1, hidden_units)
■ 그다음, 행렬 곱이므로 첫 번째 행렬의 열 개수와 두 번째 행렬의 행 개수가 동일해야 한다.
■ 그러므로 hidden.transpose(0, 1)의 차원 중 두 번째 차원(dim=1)과 세 번째 차원(dim=2)을 교환해야 한다.
- hidden.transpose(0, 1).transpose(1, 2).shape: (batch_size, hidden_units, 1)
■ 그러면 두 텐서의 배치 행렬 곱은 (batch_size, source_seq_len, hidden_units) ( \times ) (batch_size, hidden_units, 1)이므로 결과 텐서인 어텐션 스코어의 형상은 (batch_size, source_seq_len, 1)이 된다.
1.2.2 어텐션 분포(어텐션 가중치) 계산
■ 그다음, 어텐션 가중치를 얻기 위해 어텐션 스코어의 두 번째 차원(source_seq_len)에 softmax를 적용한다.
- 이 내용에 대한 코드는 self.softmax = nn.Softmax(dim=1), attention_weights = self.softmax(attention_scores)
1.2.3 어텐션 값(컨텍스트 벡터) 계산
■ 그다음, 컨텍스트 벡터를 계산하기 위해 어텐션 가중치와 Value인 인코더의 모든 시점의 은닉 상태(encoder_outputs)와 가중합을 계산한다.
■ 이 계산 과정에서도 torch.bmm()을 사용한다. 이때, 어텐션 가중치의 형상은 (batch_size, source_seq_len, 1)이며 encoder_outputs의 shape은 (batch_size, source_seq_len, hidden_units)이다.
■ 두 텐서에서 첫 번째 차원을 빼면 벡터와 행렬의 곱임을 알 수 있다. 이 계산을 하기 위해 어텐션 가중치의 두 번째 차원(dim=1)과 세 번째 차원(dim=2)을 교환해야 한다.
- attention_weights.transpose(1, 2).shape: (batch_size, 1, source_seq_len)
■ 그러면 두 텐서의 배치 행렬 곱은 (batch_size, 1, source_seq_len) \( \times \) (batch_size, source_seq_len, hidden_units)이며, 결과 텐서인 컨텍스트 벡터(어텐션 값)의 형상은 (batch_size, 1, hidden_units)이 된다.
- 이 내용에 대한 코드는 context_vector = torch.bmm(attention_weights.transpose(1, 2), encoder_outputs)
■ 여기까지의 과정을 통해 얻은 컨텍스트 벡터는 현재 타임 스텝에 대한 단일 벡터이다. 즉, 하나의 시점에 대한 컨텍스트 벡터이다.
■ 디코더의 모든 타임 스텝(seq_len = time steps)에 동일한 컨텍스트 벡터 정보를 제공하기 위해, 단일 시점의 컨텍스트 벡터를 시퀀스 길이(seq_len = time steps)만큼 복제한다.
■ 이렇게 하는 이유는, 마치 사람이 하나의 문장을 읽을 때, 특정 시점의 단어를 이해하기 위해 해당 단어뿐 아니라 해당 단어의 주변 단어에도 주의를 기울이는 것과 같은 개념이다.
■ 단일 시점의 컨텍스트 벡터의 형상은 (batch_size, 1, hidden_units)이다. 두 번째 차원은 1인데 이는 단일 시점을 의미한다.
■ 그러므로 두 번째 차원에 대해 seq_len만큼 반복하면 된다. 이 내용에 대한 코드는 seq_len = x.shape[1], context_vector_repeated = context_vector.repeat(1, seq_len, 1)이다.
■ 이제 이 컨텍스트 벡터(context_vector_repeated)를 연결해야 하는데 이 어텐션 값(컨텍스트 벡터)를 활용하는 방법은 어텐션 메커니즘을 구현 방식에 따라 달라진다.
■ 이 예에서는 바다나우 어텐션 메커니즘에 따라 컨텍스트 벡터와 입력 벡터를 연결한다. 여기서 입력 벡터는 디코더의 입력인 타겟 시퀀스가 아니라 임베딩 계층을 통과한 임베딩 벡터를 사용한다.
■ 이는 마치 언어 모델인 디코더에서 현재 입력 단어와 소스(source) 문맥 중 중요한 부분을 함께 고려하여 다음 상태를 결정하는 것이라고 볼 수 있다.
- 이 내용에 대한 코드는 x = torch.cat((x, context_vector_repeated), dim=2)
■ 이 x를 hidden, cell과 함께 디코더의 LSTM 계층에 통과시켜 얻은 디코더의 은닉 상태를 fc(fully-connected) 레이어에 통과시켜 타겟 어휘 사전 크기(tar_vocab_size)의 벡터로 변환한다.
■ 이 벡터는 단어별 점수(로짓)를 의미하며, 손실 함수로 CrossEntropy() 함수를 사용할 것이므로 Softmax를 적용하지 않는다.
1.3 Seq2Seq + Attention의 Seq2Seq
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder):
super(Seq2Seq, self).__init__()
self.encoder = encoder
self.decoder = decoder
def forward(self, src, trg):
encoder_outputs, hidden, cell = self.encoder(src)
output, _, _ = self.decoder(trg, encoder_outputs, hidden, cell)
return output
encoder = Encoder(src_vocab_size, embedding_dim, hidden_units)
decoder = Decoder(tar_vocab_size, embedding_dim, hidden_units)
model = Seq2Seq(encoder, decoder)
model
```#결과#```
Seq2Seq(
(encoder): Encoder(
(embedding): Embedding(4287, 256, padding_idx=0)
(lstm): LSTM(256, 256, batch_first=True)
)
(decoder): Decoder(
(embedding): Embedding(7476, 256, padding_idx=0)
(lstm): LSTM(512, 256, batch_first=True)
(fc): Linear(in_features=256, out_features=7476, bias=True)
(softmax): Softmax(dim=1)
)
)
````````````
model.to(device)
loss_function = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.Adam(model.parameters())
■ 디코더는 인코더의 마지막 은닉 상태를 초기 은닉 상태로 사용한다.
■ 디코더 클래스에서 디코더의 은닉 상태와 셀 상태를 반환하기는 하지만 훈련 과정에서는 사용되지 않는다.
■ Seq2Seq의 디코더는 매 시점마다 tar_vocab_size 개의 프랑스어 단어 중에서, 예측할(생성할) 단어 하나를 선택하게 된다. 그러므로 다중 클래스 분류 문제로 볼 수 있으며, 손실 함수로 크로스 엔트로피 함수를 사용한다.
'자연어처리' 카테고리의 다른 글
트랜스포머(Transformer) (1) (0) | 2025.04.15 |
---|---|
어텐션(Attention) (4) (0) | 2025.04.14 |
어텐션(Attention) (2) (0) | 2025.04.07 |
어텐션(Attention) (1) (0) | 2025.04.06 |
언어 모델의 평가 방법 - Perplexity, BLEU Score(Bilingual Evaluation Understudy Score) (0) | 2025.04.04 |