Seq2Seq로 챗봇과 기계번역 응답을 생성하는 방법
Seq2Seq의 인코더-디코더와 어텐션 구조, 검색형 챗봇과의 차이, 학습·추론·운영 시 고려할 요소를 정리한다.
2026-08-14 · 최초 발행 2025-10-14
검색을 넘어 문장을 생성하는 Seq2Seq
RNN 기반 시퀀스 투 시퀀스(Sequence to Sequence, Seq2Seq)는 대화 코퍼스의 선행 발화를 후행 발화로 바꾸는 조건부 확률 모델이다. LSA·cosine·Term 기반 검색형 챗봇이 가장 유사한 문장을 찾아 재현하는 방식이라면, Seq2Seq는 입력을 바탕으로 새로운 응답을 생성한다.
입력 시퀀스 x에 대한 출력 시퀀스 y의 조건부 확률 P(y|x)를 학습하며, 주로 LSTM이나 GRU를 사용한 인코더-디코더 구조로 구현한다. 대화에서는 선행·후행 발화 쌍을, 번역에서는 원문·번역문 쌍을 병렬 시퀀스로 사용한다. 입력 의미를 고정 길이 또는 어텐션 기반 컨텍스트로 전달한 뒤, 디코더가 토큰을 차례대로 생성하는 것이 목표다.
입력을 읽고 다음 토큰을 만드는 구조
인코더는 임베딩과 RNN 스택을 거쳐 입력 시퀀스의 표현 상태 h를 만든다. 문맥 포착 성능을 높이기 위해 양방향 Bi-LSTM 구성을 사용할 수 있다. 디코더는 이전 토큰, 현재 상태, 컨텍스트를 받아 다음 토큰의 확률 분포를 산출한다. 시퀀스의 시작과 끝은 SOS, EOS 토큰으로 구분한다.
어텐션은 디코더의 각 시점에서 인코더 출력 전체에 가중합을 적용한다. Bahdanau의 additive 방식이나 Luong의 multiplicative 방식을 사용할 수 있으며, 긴 문장과 장거리 의존성에서 성능을 높인다. coverage 보정은 반복과 누락을 완화하는 데 쓰인다.
학습 시에는 교사강요(Teacher Forcing)로 수렴을 빠르게 하고, 스케줄 샘플링으로 노출편향을 낮춘다. 손실은 패딩 마스크를 적용한 토큰 수준 교차엔트로피를 사용한다. Adam과 그래디언트 클리핑(예: 1.0)은 학습 발산을 막는 수단이다.
생성 품질은 탐색과 어휘 처리에서 갈린다
디코더 탐색에는 Greedy, Beam Search, Top-k·Top-p 샘플링을 활용한다. Beam Search에서는 길이 보정과 다양성 페널티를 함께 고려할 수 있다. 확률 엔트로피와 최대 토큰 확률로 신뢰도를 산출하고, 임계치에 미치지 못하면 검색형 응답으로 폴백하는 구성이 가능하다.
서브워드 BPE·Unigram은 OOV를 줄이면서 어휘 규모와 메모리 사이의 균형을 잡는다. 카피·포인터 제너레이터는 원문의 엔티티나 희귀어를 출력으로 복사해야 할 때 유용하다.
챗봇·번역·요약에 적용하는 방식
업무용 챗봇에서는 FAQ 검색형 응답과 생성형 응답을 결합해 미매칭 또는 미훈련 패턴을 보강할 수 있다. 기계번역은 도메인 특화 소량 병렬 데이터로 빠르게 적응(Finetuning)하고, 사내 용어집을 반영하는 데 활용한다.
추상 요약과 지식베이스 기반 추론의 자연어 생성 인터페이스도 적용 대상이다. 다국어 고객센터에서는 실시간 번역·응대 자동화와 저자원 언어용 사전학습·미세조정 파이프라인을 구성할 수 있다.
학습과 추론에서 거치는 경로
학습 입력은 소스·타깃 병렬 코퍼스, BPE 토크나이저, SOS/EOS/PAD 같은 특수 토큰이다. 전처리와 패딩·마스킹, 배치 샘플링을 거쳐 인코더·어텐션·디코더 순으로 순전파한다. 교사강요를 적용한 뒤 패딩 마스크 손실을 계산하고, 역전파와 옵티마이저 업데이트, 그래디언트 클리핑을 수행한다. 결과물은 학습된 가중치와 BLEU·chrF++·Perplexity 검증 점수, 체크포인트다.
추론에서는 사용자 발화나 원문을 토크나이즈한 뒤 인코더 인퍼런스와 Beam·Greedy 탐색을 수행한다. 길이·커버리지 보정과 최대 확률·엔트로피·비임 점수 기반 신뢰도 계산이 뒤따른다. 신뢰도가 임계치 아래라면 검색형으로 전환하고, 금칙어와 안전 필터를 거친다. 최종 응답 또는 번역문과 함께 라티ency, 신뢰도 같은 로그·메트릭을 남긴다.
검색형 시스템과의 운영상 차이
| 지표 | 검색형(LSA/cosine/Term) | Seq2Seq(RNN+Attention) |
|---|---|---|
| 성능 | 코퍼스 내 정확 재현 우수, 일반화 한계 | 문장 생성·일반화 우수, 헬루시네이션 리스크 |
| 확장성 | 인덱스 증설로 수평 확장 용이 | 모델 크기·서빙 자원 의존, 배치·FP16로 스루풋 개선 |
| 일관성 | 결정적·재현성 높음 | 탐색·샘플링에 따라 변동, 길이·스타일 제어 필요 |
| 안정성 | 데이터 품질에 종속, 안전성 높음 | 안전 필터·신뢰도 기반 폴백 필요 |
| 운영 편의 | 구축·운영 단순, 업데이트 용이 | 학습·배포 파이프라인 요구, 모니터링 필요 |
PyTorch로 구현한 RNN과 어텐션
전제조건: Python 3.10+, PyTorch 2.x, 토크나이저(BPE 등) 준비, PAD=0, SOS=1, EOS=2 가정.
# pip install torch
import torch, torch.nn as nn
device = "cuda" if torch.cuda.is_available() else "cpu"
class Encoder(nn.Module):
def __init__(self, vocab, emb=256, hid=512, layers=1):
super().__init__()
self.emb = nn.Embedding(vocab, emb, padding_idx=0)
self.rnn = nn.GRU(emb, hid, num_layers=layers, bidirectional=True, batch_first=True)
self.fc = nn.Linear(hid*2, hid)
def forward(self, src, src_len=None):
x = self.emb(src)
out, h = self.rnn(x) # out: [B, T, 2H]
h = torch.tanh(self.fc(torch.cat([h[-2], h[-1]], dim=1))).unsqueeze(0) # [1,B,H]
return out, h # encoder outputs, init decoder state
class BahdanauAttention(nn.Module):
def __init__(self, hid):
super().__init__()
self.W1, self.W2, self.v = nn.Linear(hid*2, hid), nn.Linear(hid, hid), nn.Linear(hid, 1)
def forward(self, dec_h, enc_out, mask):
# dec_h: [1,B,H] -> [B,1,H]
score = self.v(torch.tanh(self.W1(enc_out) + self.W2(dec_h.transpose(0,1)))).squeeze(-1) # [B,T]
score = score.masked_fill(~mask, -1e9)
attn = torch.softmax(score, dim=-1) # [B,T]
ctx = torch.bmm(attn.unsqueeze(1), enc_out).squeeze(1) # [B,2H]
return ctx, attn
class Decoder(nn.Module):
def __init__(self, vocab, emb=256, hid=512):
super().__init__()
self.emb = nn.Embedding(vocab, emb, padding_idx=0)
self.rnn = nn.GRU(emb + hid*2, hid, batch_first=True)
self.attn = BahdanauAttention(hid)
self.fc = nn.Linear(hid + hid*2, vocab)
def forward(self, tgt_tok, dec_h, enc_out, mask):
ctx, _ = self.attn(dec_h, enc_out, mask)
x = torch.cat([self.emb(tgt_tok), ctx], dim=-1).unsqueeze(1) # [B,1,emb+2H]
out, dec_h = self.rnn(x, dec_h) # out: [B,1,H]
logits = self.fc(torch.cat([out.squeeze(1), ctx], dim=-1)) # [B,V]
return logits, dec_h
class Seq2Seq(nn.Module):
def __init__(self, src_vocab, tgt_vocab, hid=512):
super().__init__()
self.enc = Encoder(src_vocab, hid=hid)
self.dec = Decoder(tgt_vocab, hid=hid)
def forward(self, src, src_mask, tgt):
enc_out, dec_h = self.enc(src)
B, T = tgt.size()
logits = []
tok = tgt[:,0] # SOS
for t in range(1, T):
logit, dec_h = self.dec(tok, dec_h, enc_out, src_mask)
logits.append(logit.unsqueeze(1))
tok = tgt[:,t] # Teacher Forcing
return torch.cat(logits, dim=1) # [B,T-1,V]
# 예시 학습 루프(데이터 로딩·토크나이즈 생략)
V = 10000
model = Seq2Seq(V, V).to(device)
opt = torch.optim.Adam(model.parameters(), lr=2e-4)
crit = nn.CrossEntropyLoss(ignore_index=0)
for step in range(1000):
src, tgt = torch.randint(3, V, (32, 20)), torch.randint(3, V, (32, 22))
src[:, -1] = 0 # 가짜 패딩
src_mask = src.ne(0) # PAD 마스크
src, tgt, src_mask = src.to(device), tgt.to(device), src_mask.to(device)
logits = model(src, src_mask, tgt) # [B,T-1,V]
loss = crit(logits.reshape(-1, V), tgt[:,1:].reshape(-1))
opt.zero_grad(); loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
if step % 100 == 0:
print(step, float(loss))
빔서치에는 beam=4~8, 길이보정 α=0.6~0.8, 반복 n-gram 금지(예: no-repeat-ngram-size=3)를 적용할 수 있다. 성능과 안정화에는 Dropout 0.1~0.3, EMA 가중치, FP16/Brain Float16, 서빙 배치·캐시를 사용한다. 평가는 번역의 BLEU·chrF++, 대화의 Dist-1/2·응답 신뢰도·사용자 만족도 A/B로 수행한다.
검색형 대비 도메인 일반화 기준으로 번역 BLEU는 +3~8p, 대화 Dist-2는 +0.05~0.15, OOV 에러율은 -20~40%의 정량 효과를 기대할 수 있다. 창의적 응답 생성, 장거리 문맥 처리, 도메인 적응 용이성도 높아진다. 하이브리드 폴백은 품질 하한선을 확보하고 도메인 업데이트 비용을 줄이는 역할을 한다.
생성형 응답을 운영 환경에 넣을 때
데이터에는 PII 마스킹·익명화, 도메인별 분리, 접속·추적 로깅이 필요하다. 생성형 모델은 데이터 분포 누수 리스크가 있다.
유해콘텐츠 필터, 금칙어·정책 룰, 신뢰도 기반 폴백은 안전성을 높이지만 과도한 필터링은 유용성을 낮출 수 있다. 서빙은 검색형에서 생성형으로 이어지는 멀티모델 라우팅과 큐·배치·시간제한(예: 300ms SLA)을 적용할 수 있으며, 이때 지연과 비용은 상충한다.
지속학습에서는 주기적 재학습과 평가, 모델 드리프트 모니터링을 수행한다. 데이터 샘플링과 정규화는 과적합 방지에 사용한다. 도메인 코퍼스를 정비한 뒤 토크나이저와 사전을 준비하고, RNN 기반 Seq2Seq를 초기 론칭한 다음 어텐션·빔서치·폴백을 결합해 지표 기반으로 개선하는 흐름이 적합하다.