|
|
T5 (인코더-디코더)구조 정리
─ 인코더와 디코더가 Cross-Attention으로 어떻게 결합되는가
지난번에 GPT 디코더 전용 구조와 BERT 인코더 전용 구조를 각각 끝냈다.
이번엔 그 둘을 이어붙인 형태인 인코더-디코더 구조, 그중에서도 T5를 본다.
T5는 Text-to-Text Transfer Transformer의 줄임말로, 앞글자 T가 다섯 번 나온다고 해서 T5다.
핵심 아이디어는 번역, 요약, 질의응답, 분류 같은 서로 다른 작업을 전부 "텍스트를 넣으면 텍스트가 나온다"는 하나의 형식으로 통일한 것이다.
이 글은 앞선 두 글과 겹치는 부분이 많다.
self-attention, FFN, residual, LayerNorm 같은 건 이미 정확한 용어와 수식으로 다뤘기 때문에, 여기서는 다시 설명하지 않고 "어느 글 어느 장을 보면 된다"고 위치를 짚고 넘어간다. 새로 설명하는 건 T5에만 있는 것들이다.
참고할 앞선 글 두 개는 이렇다.
• GPT 디코더 전용 구조 : https://cafe.daum.net/SmartRobot/RoVa/2735
• BERT 인코더 전용 구조 : https://cafe.daum.net/SmartRobot/RoVa/2742
본문에서도 참고가 필요한 지점마다 주소를 다시 넣어두었으니 그때그때 눌러서 보고 오면 된다.
링크를 눌러도 해당 위치로 바로 이동하지는 않으므로, 장 번호와 함께 원문의 장 제목을 그대로 적어두었다.
글에 들어간 뒤 브라우저 찾기(Ctrl+F)로 그 제목을 검색하면 바로 갈 수 있다.
═══════════════════════════════════════
제1부. 전체 구조
═══════════════════════════════════════
■ 전체 흐름 먼저
입력 텍스트 → 토큰화 및 임베딩 → 인코더 스택 N층 → 인코더 전용 최종 LayerNorm → K, V → 디코더 스택 N층 → 디코더 전용 최종 LayerNorm → Linear + Softmax → 다음 토큰
이게 전부다. 여기서 중요한 건 이 구조가 학습이든 추론이든 완전히 동일하다는 점이다.
달라지는 건 딱 하나, 디코더에 데이터가 들어가는 방식뿐인데 그건 제3부에서 따로 다룬다.
■ 블록 1. 토큰화와 임베딩
입력: 원문 텍스트
내부 연산: SentencePiece 분절 → 정수 ID → 임베딩 테이블 조회
출력: 토큰마다 하나씩, d_model 차원 벡터
[전문 설명]
텍스트를 서브워드 단위로 쪼개고 어휘사전상의 정수 ID로 바꾼 뒤, 임베딩 테이블에서 해당 행을 꺼내오는 과정은 아래 두 장과 원리가 같다. 쪼개는 알고리즘이 BPE 대신 SentencePiece라는 점만 다르다.
→ GPT 디코더 글 "1장. 텍스트를 숫자로 - 토크나이저(BPE)"
→ "2장. 조각을 벡터로 - 임베딩"
https://cafe.daum.net/SmartRobot/RoVa/2735
다만 T5는 이 단계에서 위치 정보를 전혀 더하지 않는다.
GPT-2는 2장에서 본 것처럼 학습되는 절대 위치 임베딩을 여기서 바로 더했지만, T5의 위치 정보는 훨씬 뒤, attention score를 계산하는 순간에 relative position bias라는 형태로 들어간다. 블록 3에서 따로 본다.
그래서 이 블록의 출력은 순수하게 "이 토큰이 무슨 단어인가"만 담고 있고, "몇 번째 자리인가"는 아직 전혀 반영되지 않은 상태다.
또 하나 짚어둘 게 weight tying이다. 여기서 쓰는 임베딩 테이블은 이 자리에서만 쓰이고 끝나는 게 아니라, 맨 마지막 출력 단계의 Linear 가중치로 전치되어 그대로 재사용된다. "이 토큰이면 어떤 벡터를 넣을까"와 "이 벡터가 각 토큰에 얼마나 대응하나"가 사실상 같은 질문의 양면이라는 데 착안한 기법인데, 이 원리는 아래에서 다뤘다.
→ GPT 디코더 글 "11장. 확률 만들기 - 최종 출력 단계"의 단계 2 부분
(Linear 768 → vocab 크기 설명 안에 Weight Tying 항목이 있다)
https://cafe.daum.net/SmartRobot/RoVa/2735
원리는 같고, 차이가 있다면 묶이는 범위다. GPT는 입력 임베딩 하나와 출력 투영 하나, 이렇게 둘이 묶였지만 T5는 인코더 입력, 디코더 입력, 그리고 최종 출력 투영까지 셋이 같은 테이블 하나를 공유한다.
[쉽게 다시 말하면]
책장에서 번호로 책을 꺼내오는 단계다. 그런데 GPT와 달리 이 단계에서 "몇 번째 칸에 꽂혀 있었는지"는 전혀 기록하지 않는다. 그건 나중에 다른 방식으로 챙긴다.
■ 블록 2. 인코더 층 하나의 내부
입력: d_model 차원 벡터들 (토큰 개수만큼)
내부 연산: LayerNorm → self-attention → residual → LayerNorm → FFN → residual
출력: 같은 개수, 같은 차원의 벡터들
[전문 설명]
이 블록은 사실상 새로 설명할 게 거의 없다. BERT 인코더 층과 구조가 같기 때문이다.
먼저 이 층 안에서 일어나는 일을 순서대로 짚어보면 이렇다.
입력 벡터가 LayerNorm을 거쳐 정리되고, 그 결과로 Q, K, V를 만들고, Q와 K를 내적해 점수를 낸 뒤 softmax를 거쳐 V를 가중합하고, 여러 헤드의 결과를 이어붙여 섞은 다음, 원본에 다시 더한다. 그리고 같은 패턴을 FFN에 대해 한 번 더 반복한다.
이 과정 하나하나가 앞선 두 글에 전부 있다. 전체를 다시 확인하고 싶다면 아래 순서로 읽으면 이 층을 처음부터 끝까지 그대로 따라갈 수 있다.
→ GPT 디코더 글 "3장. 블록의 입구 - LayerNorm"
(정규화 수식과, Pre-LN이라 본줄기는 정규화를 거치지 않는다는 점)
→ "4장. 세 갈래로 나뉜다 - Q/K/V 생성"
(같은 벡터를 세 행렬로 각각 변환하는 원리)
→ GPT 디코더 글 "5장. 관계 점수 계산 - QKᵀ 스케일링과 Causal Mask"
(QKᵀ를 √dₖ로 나누는 부분. 같은 장 뒷부분에 causal mask 이야기도 나오는데, 그건 디코더의 masked self-attention에서 따로 다룬다. 인코더는 마스크 없이 모든 토큰이 서로를 다 본다)
→ "6장. 점수를 비율로 - Softmax와 가중합"
(비율로 V를 섞는 과정, 차원이 V에서 온다는 점)
→ "7장. 관점 합치기 - 헤드 병합과 출력 선형변환"
(concat은 나열일 뿐이고 W_O가 실제로 섞는다는 점)
→ "8장. 본줄기에 얹기 - Residual 연결"
(원본에 조각을 더해나가는 구조)
→ "9장. 넓혔다 좁히기 - FFN"
(확장 → GELU → 축소, 그리고 토큰끼리 상호작용하지 않는다는 점)
전부 https://cafe.daum.net/SmartRobot/RoVa/2735
방금 5장에서 "인코더는 마스크가 없다"고 했는데, 그게 정확히 무슨 뜻이고 왜 그렇게 되는지는 BERT 글 쪽이 더 자세하다.
→ BERT 글 "블록 3. 인코더 레이어"의 "3.2 양방향 멀티헤드 Self-Attention"
(모든 토큰이 좌우 상관없이 서로를 참고하는 것이 왜 양방향인지, 그리고 padding mask는 왜 causal mask와 성격이 다른지)
https://cafe.daum.net/SmartRobot/RoVa/2742
T5의 인코더 층 self-attention은 여기서 설명한 것과 완전히 동일하다.
그러니까 이 블록은 "BERT 인코더 층을 그대로 가져왔다"고 이해하고 아래 세 가지 차이점만 새로 챙기면 된다.
T5가 다른 지점은 세 가지뿐이다.
첫째, 정규화가 bias 항이 없는 단순화된 형태다. 원조 Transformer의 LayerNorm에서 편향 항을 제거한 형태를 쓴다.
둘째, 위치 정보가 attention score 안으로 들어온다. 이건 바로 다음 블록에서 본다.
셋째, 이 층이 N번 반복된 뒤 인코더 전용 최종 LayerNorm이 한 번 더 걸린다. 이건 블록 5에서 본다.
[쉽게 다시 말하면]
BERT 인코더 층을 그대로 가져다 쓴 것에 가깝다. 안에서 일어나는 일은 이미 본 것과 같고, 겉에 붙는 장치 몇 개만 다르다.
■ 블록 3. Relative Position Bias
입력: 각 query-key 쌍의 상대적 거리
내부 연산: 거리를 버킷으로 매핑 → 학습된 스칼라 bias 조회
출력: attention score에 더해질 bias 값
[전문 설명]
앞서 임베딩 단계에서 위치 정보를 전혀 넣지 않았다. 그럼 T5는 순서를 어떻게 아는가.
attention score를 계산할 때 직접 더한다. 디코더 self-attention을 기준으로 쓰면 이렇게 된다.
score = QKᵀ / √dₖ + B(상대위치) + M(causal mask)
여기서 B가 relative position bias다. query 위치 i와 key 위치 j의 차이 (i - j)를 보고, 그 거리에 해당하는 학습된 스칼라 하나를 score에 더해주는 방식이다.
그런데 가능한 거리 값은 문장이 길어지면 수백, 수천 개가 된다. 그걸 전부 따로 학습하면 파라미터가 낭비되고, 학습 때 본 적 없는 먼 거리는 아예 대응이 안 된다. 그래서 거리를 버킷(bucket)으로 묶는다. 버킷 개수는 32개, 최대 거리는 128이 기본값이다.
• 가까운 거리 : 거리 값을 1씩 그대로 써서 각각 고유한 버킷을 준다
• 먼 거리 : 로그 스케일로 묶는다. 가까운 쪽은 촘촘하게, 먼 쪽은 넓게. 거리 8과 9는 다른 버킷일 수 있지만 거리 100과 110은 같은 버킷을 쓴다
• 최대 거리 이상 : 128 이상은 전부 하나의 같은 버킷으로 몰아넣는다
인코더와 디코더는 이 32개를 쓰는 방식이 다르다.
• 인코더 : 양방향이라 앞뒤를 구분해야 하므로 32개를 절반씩 나눠 앞쪽 방향 16개, 뒤쪽 방향 16개로 쓴다
• 디코더 : causal mask 때문에 미래를 볼 일이 없으므로 32개를 전부 과거 방향에만 쓴다
그리고 이 버킷 테이블은 층마다 새로 학습되는 게 아니라 모델 전체에서 공유된다. 다만 같은 층 안에서도 헤드마다는 각각 다른 bias를 학습한다.
[쉽게 다시 말하면]
바로 옆 단어와 두 칸 앞 단어는 확실히 구분해야 한다. 그런데 80단어 전과 95단어 전은 굳이 세밀하게 구분할 필요가 없다. "가까운 건 정밀하게, 먼 건 대충"이 버킷 방식의 전부다.
사람이 문장을 읽을 때도 이렇게 읽는다. 방금 나온 단어는 정확히 기억하지만, 한참 전 내용은 "앞쪽 어딘가에서 나왔던 얘기" 정도로 뭉뚱그려 기억한다.
■ 블록 4. 디코더 층 하나의 내부
입력: d_model 차원 벡터들, 그리고 인코더 최종 출력
내부 연산: LayerNorm → masked self-attention → residual → LayerNorm → cross-attention → residual → LayerNorm → FFN → residual
출력: 같은 개수, 같은 차원의 벡터들
[전문 설명]
인코더 층과 비교하면 딱 하나가 늘었다. masked self-attention과 FFN 사이에 cross-attention이 끼어들어간다. 그 외에는 전부 같다.
residual을 거는 방식도, LayerNorm의 위치와 종류도 인코더와 동일하고, 단지 서브블록이 둘에서 셋으로 늘어난 만큼 residual도 세 번 걸릴 뿐이다.
masked self-attention부터 보면, 인코더의 self-attention과 서브블록 구조가 같고 차이는 attention mask 하나다. causal mask가 걸려서 자기 앞쪽만 본다. 점수 행렬의 상삼각 부분에 -inf를 더해서 softmax를 거치면 그 자리가 0이 되고, 결과적으로 미래 토큰으로는 attention이 아예 가지 않게 된다.
이 마스크가 왜 곱셈이 아니라 덧셈이어야 하는지, 그리고 왜 아주 큰 음수가 아니라 하필 -inf여야 하는지는 아래에서 자세히 다뤘다. (앞부분의 √dₖ 스케일링은 블록 2에서 이미 짚었으니, 여기서는 마스크 행렬 그림이 나오는 지점부터 보면 된다.)
→ GPT 디코더 글 "5장. 관계 점수 계산 - QKᵀ 스케일링과 Causal Mask"
https://cafe.daum.net/SmartRobot/RoVa/2735
정리하면 T5 디코더의 self-attention score는 이 세 가지가 다 더해진 뒤 softmax를 거친다.
score = QKᵀ / √dₖ + B(상대위치) + M(causal mask)
세 번째 항이 방금 본 마스크이고, 두 번째 항은 바로 앞 블록 3에서 본 bias다. 인코더의 self-attention에는 세 번째 항이 없다는 것이 유일한 차이다.
이제 cross-attention이다. self-attention과 cross-attention이 무엇이 같고 무엇이 다른지는 이미 한 번 정리해둔 게 있으니 그걸 먼저 보고 오면 이해가 훨씬 빠르다.
→ BERT 글 맨 끝 "추가" 항목의 4번
"셀프 어텐션(self attention)과 크로스 어텐션(cross attention)의 차이를 설명하라" (연산 절차는 동일하고 Q, K, V의 출처만 다르다는 점, 출력 길이는 Q를 만든 쪽을, 참고 대상 개수는 K, V를 만든 쪽을 따른다는 점)
https://cafe.daum.net/SmartRobot/RoVa/2742
그 글에서 정리한 대로, 연산 절차는 완전히 같고 Q와 K, V를 어느 시퀀스에서 만드느냐만 다르다. T5에서는 이렇게 나뉜다.
• Q : 바로 앞 masked self-attention을 통과한 디코더 벡터에서 만든다
• K, V : 인코더 스택의 최종 출력에서 만든다
여기서 자주 헷갈리는 지점이 하나 있다.
인코더도 자기 self-attention을 하면서 Q, K, V를 만들었는데, 그중 K와 V만 골라서 디코더로 넘겨주는 것인가.
그렇지 않다. 인코더가 자기 층 안에서 만든 Q, K, V는 그 층 안에서 소비되고 끝난다. 디코더로 넘어가는 건 Q도 K도 V도 아닌, 그냥 인코더 최종 출력 벡터다. 그 벡터에 cross-attention 전용 W_K와 W_V를 곱해서 K와 V를 새로 만들어낸다. 그리고 이 W_K, W_V는 디코더 층마다 각각 따로 존재한다.
즉 인코더 최종 출력이라는 하나의 재료를 모든 디코더 층이 공유하되, 층마다 자기만의 변환 행렬로 그 재료를 다르게 해석해서 쓴다. 층 1의 cross-attention과 층 2의 cross-attention은 같은 입력을 받지만 서로 다른 K, V를 만들어낸다.
층과 층 사이의 연결도 정리해두면 이렇다. 세로 방향으로는 그 층의 전체 결과(세 서브블록을 다 거치고 residual이 세 번 누적된 벡터)가 통째로 다음 층의 입력이 된다. 가로 방향으로는 인코더 최종 출력이 모든 층에 똑같이 곁가지로 공급된다. 인코더 출력은 층을 거치면서 바뀌지 않는다.
[쉽게 다시 말하면]
세로로 흐르는 건 이어달리기 배턴이다. 각 층이 앞 층의 결과를 이어받아 계속 다듬어 나간다.
인코더 출력은 공용 참고자료다. 모든 주자가 자기 차례에 같은 자료를 참고하지만, 자료 자체는 아무도 고치지 않는다. 다만 주자마다 그 자료를 읽는 관점(W_K, W_V)이 다르다.
■ 블록 5. 최종 LayerNorm
입력: 스택 N층을 다 통과한 벡터들
내부 연산: 정규화 한 번
출력: 스케일이 정리된 같은 형태의 벡터들
[전문 설명]
이건 인코더 끝과 디코더 끝에 각각 하나씩, 총 두 개가 존재한다.
왜 필요한지는 Pre-LN 구조의 성질 때문이다.
Pre-LN에서는 LayerNorm이 서브블록에 들어가기 직전에만 걸리고, 본줄기(residual stream)는 한 번도 정규화되지 않은 채 계속 쌓이기만 한다. 그래서 층이 깊어질수록 값의 크기가 계속 커진다.
이 "본줄기가 계속 커지는 문제"와 그에 대한 완화책은 아래에서 자세히 다뤘다. 지금 이 최종 LayerNorm이 왜 존재하는지가 그 문제의 연장선이라 한 번 보고 오면 이 블록의 이유가 명확해진다.
→ GPT 디코더 글 "8장. 본줄기에 얹기 - Residual 연결" 의 뒷부분 (계속 더하기만 하면 값이 커지는 것 아닌가 하는 의문과, 1/√N 초기화 스케일링이 완화책이지 근본 해결은 아니라는 부분)
https://cafe.daum.net/SmartRobot/RoVa/2735
N층을 다 통과한 뒤 이 커진 값을 그대로 다음 단계에 넘기면 곤란하므로, 스택 끝에서 딱 한 번 정규화해서 스케일을 정리해준다.
두 개가 하는 일은 같지만 이어지는 곳이 다르다.
• 인코더 전용 최종 LayerNorm → 그 출력이 K, V로 변환되어 디코더의 모든 cross-attention에 공급된다
• 디코더 전용 최종 LayerNorm → 그 출력이 Linear + Softmax로 넘어가 다음 토큰 확률이 된다
앞에서 "인코더 최종 출력"이라고 불렀던 값은 정확히는 이 인코더 전용 최종 LayerNorm까지 거친 뒤의 값이다.
[쉽게 다시 말하면]
층마다 걸리는 LayerNorm이 "작업대에 올리기 전에 규격 맞추기"라면, 최종 LayerNorm은 "공장을 다 나온 완제품 전체를 마지막에 한 번 검수"하는 것이다.
■ 블록 6. Linear + Softmax
입력: 디코더 최종 LayerNorm을 통과한 벡터들
내부 연산: vocab 크기로 선형변환 → softmax
출력: 토큰 위치마다 하나씩, vocab 크기의 확률분포
[전문 설명]
이 단계에 들어오는 입력은 벡터 하나가 아니라 벡터 묶음이다.
디코더가 처리 중인 토큰이 5개면 (5, d_model) 형태로 들어온다.
여기에 행렬을 곱해 차원을 d_model에서 vocab 크기로 바꾼다.
결과는 (5, vocab) 형태이고, 각 숫자를 로짓이라 부른다.
이 행렬이 새로 학습되는 별개의 행렬이 아니라 블록 1에서 본 임베딩 테이블을 전치해서 재사용한 것이다. 입력 쪽과 출력 쪽이 같은 테이블을 쓴다는 게 실제로 여기서 쓰이는 지점이다.
→ GPT 디코더 글 "11장. 확률 만들기 - 최종 출력 단계"의 단계 2
https://cafe.daum.net/SmartRobot/RoVa/2735
그 다음 softmax를 각 위치(행)마다 독립적으로 적용해서 그 행의 합이 1이 되도록 만든다. 이제 각 행은 "이 위치에서 다음 토큰이 무엇일 확률"이 된다.
실제로 쓰이는 건 상황에 따라 다르다. 학습 때는 모든 위치의 확률분포가 각각의 정답과 비교되어 loss가 되지만, 추론 때는 마지막 위치의 확률분포 하나만 쓰고 나머지는 버린다.
그럼 그 하나의 확률분포에서 토큰을 실제로 어떻게 고르는가. 이 부분은 T5와 GPT가 완전히 동일해서 따로 설명할 게 없다. 아래 네 장이 그 전략들을 다룬다.
→ GPT 디코더 글 "14장. Temperature - 격차 조절"
→ "15장. Top-k - 개수로 자르기"
→ "16장. Top-p(Nucleus) - 누적 확률로 자르기"
→ "17장. Greedy와 실전 조합"
(같은 확률분포에 네 전략을 각각 적용했을 때 어떻게 달라지는지 비교한 그림이 17장에 있다)
전부 https://cafe.daum.net/SmartRobot/RoVa/2735
[쉽게 다시 말하면]
문맥을 잔뜩 흡수한 벡터 하나를 "그래서 다음에 올 단어는 뭘까"라는 수만 개 후보에 대한 점수표로 펼쳐놓고, 그걸 확률로 환산하는 단계다.
═══════════════════════════════════════
제2부. 사전학습 - Span Corruption
═══════════════════════════════════════
■ 블록 7. 입력과 정답 만들기
입력: 라벨 없는 원문 텍스트 (C4 코퍼스)
내부 연산: 연속 구간 선택 → sentinel 토큰으로 치환
출력: 손상된 입력 문자열, 그리고 정답 문자열
[전문 설명]
BERT의 MLM이 토큰 하나씩을 [MASK]로 가렸다면, T5는 연속된 토큰 덩어리(span)를 통째로 가린다.
원문이 이렇다고 하자.
Thank you for inviting me to your party last week
여기서 연속 구간 두 개를 골라 각각 하나의 sentinel 토큰으로 치환한다.
입력 : Thank you <X> me to your party <Y> week
정답 : <X> for inviting <Y> last <Z>
정답은 원문 전체가 아니라 가려진 구간들만 sentinel과 함께 이어붙인 것이고, 맨 끝에 마무리용 sentinel이 하나 더 붙는다.
수치는 이렇다.
• corruption rate 15% : 전체 토큰의 15%를 가린다
• 평균 span 길이 3 : 한 번에 평균 3토큰씩 묶어서 가린다
corruption rate 15%는 BERT가 썼던 값을 그대로 따른 것이고, 논문에서 10%에서 50%까지 실험해본 결과 10~25% 구간에서 성능이 안정적이었다고 보고한다.
span 단위로 가리는 이유는 계산 효율이다. 연속된 여러 토큰이 sentinel 하나로 뭉쳐지므로 입력 시퀀스가 짧아지고, 정답 시퀀스는 훨씬 더 짧아진다. 디코더가 원문 전체가 아니라 가려진 부분만 생성하면 되기 때문이다.
BERT의 MLM과 비교하면 성격이 확실히 다르다. BERT는 [MASK] 하나가 토큰 하나를 대체하고 인코더가 그 자리를 바로 맞힌다. T5는 sentinel 하나가 여러 토큰짜리 구간 전체를 대체하고, 디코더가 그 구간의 내용을 통째로 자기회귀적으로 생성해내야 한다.
[쉽게 다시 말하면]
BERT가 "이 빈칸에 뭐가 들어갈까" 문제라면, T5는 "이 뻥 뚫린 구간을 통째로 채워 넣어봐" 문제다. 빈칸의 길이조차 알려주지 않는다는 점이 결정적으로 다르다.
═══════════════════════════════════════
제3부. 학습과 추론의 차이
═══════════════════════════════════════
[전문 설명]
지금까지 본 구조는 학습이든 추론이든 완전히 동일하다. 가중치도, 레이어 구성도, 연결 방식도 바뀌지 않는다. 달라지는 건 디코더에 데이터가 들어가는 방식 하나뿐이다.
학습 시 (Teacher forcing)
정답 문자열 전체를 한 번에 디코더에 넣는다. 정답을 한 칸씩 오른쪽으로 밀어서 "지금까지 생성한 것처럼" 흉내를 낸다. masked self-attention의 causal mask 덕분에 각 위치는 자기 앞쪽만 보게 되므로, 모든 위치의 예측을 병렬로 동시에 계산할 수 있다. forward pass 한 번으로 시퀀스 전체의 loss가 나온다.
학습과 추론에서 확률분포를 쓰는 방식이 어떻게 갈리는지, 그리고 forward pass 계산 자체는 양쪽이 완전히 같다는 점은 아래에서 다뤘다.
→ GPT 디코더 글 "13장. 왜 매번 답이 다른가"
(학습 시엔 단어를 뽑지 않고 loss만 계산한다는 점, 확률분포 계산은 결정적이고 무작위성은 마지막 선택 단계에서만 생긴다는 점)
https://cafe.daum.net/SmartRobot/RoVa/2735
거기서 설명한 내용이 T5에도 그대로 적용된다. 달라지는 건 그 과정에 cross-attention이 하나 더 끼어 있다는 점, 그리고 인코더가 별도로 한 번 돌아간다는 점뿐이다.
추론 시 (자기회귀 생성)
정답이 없으므로 토큰을 하나씩 실제로 생성해서 다시 디코더 입력에 이어붙이는 것을 반복해야 한다. N개 토큰을 만들려면 디코더를 N번 실행해야 한다.
공통점
인코더는 학습이든 추론이든 딱 한 번만 실행한다. 입력 문장이 바뀌지 않는 한 인코더 출력은 재계산할 필요가 없기 때문이다. 따라서 그 출력에서 만들어지는 각 층의 K, V도 한 번 계산하면 끝이다.
안 바뀌는 K, V를 저장해두고 재사용한다는 발상 자체는 아래에서 다뤘다.
→ GPT 디코더 글 "18장. KV 캐시 - 같은 계산 반복하지 않기"
(Q는 매 스텝 새 토큰 것만 필요하고 K, V는 과거 것이 안 바뀌므로 캐시한다는 점, 그리고 이건 추론 전용 최적화라는 점)
https://cafe.daum.net/SmartRobot/RoVa/2735
T5에서는 이게 두 갈래로 나뉜다.
디코더 self-attention의 K, V는 18장에서 설명한 것과 똑같이 토큰이 늘어날 때마다 캐시에 이어붙여야 한다. 반면 cross-attention의 K, V는 인코더 출력에서 만들어지므로 처음부터 끝까지 아예 변하지 않는다. 한 번 만들어두면 그걸로 끝이다.
구조가 바뀌지 않는다는 원칙에 붙는 예외가 하나 있다면 dropout이다. 학습 때는 켜지고 추론 때는 꺼지지만, 이건 구조가 달라지는 게 아니라 같은 레이어의 동작 모드가 바뀌는 것이다.
[쉽게 다시 말하면]
건물은 그대로고, 사람이 드나드는 방식만 다르다. 학습 때는 정답을 다 알고 있으니 모든 방에 한꺼번에 사람을 들여보내고, 추론 때는 한 명씩 들여보내서 다음 사람을 정한 뒤에야 그 다음 사람을 들여보낸다.
═══════════════════════════════════════
제4부. 하나의 구조로 여러 작업을 하는 방법
═══════════════════════════════════════
■ 블록 8. 태스크별 데이터 포맷팅
입력: 태스크별로 형태가 제각각인 원본 데이터셋
내부 연산: 태스크 전용 템플릿을 적용해 문자열 쌍으로 변환
출력: (입력 문자열, 정답 문자열) 형태의 학습 샘플
[전문 설명]
T5의 구조에는 태스크별 분기가 전혀 없다.
BERT는 MLM head와 NSP head가 따로 있었고, 파인튜닝할 때도 분류용 head를 새로 붙여야 했다. T5는 그런 게 없다. 출력이 항상 텍스트이므로 vocab 위에서 동작하는 Linear + Softmax 하나면 충분하다.
그럼 서로 다른 작업은 어디서 구분되는가.
모델에 들어가기 전, 데이터를 만드는 단계에서 전부 처리된다. 원본 데이터셋은 태스크마다 형태가 완전히 다르다.
• 감성분석 : sentence 하나와 label 하나
• 문장 유사도 : sentence1, sentence2, label
• 질의응답 : context, question, answer
컬럼 개수도 이름도 라벨 형식도 제각각이다. 이 상태로는 T5에 넣을 수 없다. 그래서 태스크마다 전용 템플릿을 만들어 이 구조체를 문자열로 변환한다.
감성분석 템플릿 : "Is this review positive or negative? Review: {sentence}"
문장 유사도 템플릿 : "Do these mean the same thing? A: {sentence1} B: {sentence2}"
여기서 중요한 건 공통 양식이 있는 게 아니라는 점이다. 각 템플릿은 그 태스크에만 맞춰 처음부터 따로 설계된다. 감성분석 템플릿에 sentence2 자리가 비어 있는 게 아니라 애초에 그 자리가 존재하지 않는다.
이렇게 만들어진 최종 문자열 하나가 인코더로 들어가고, 정답도 마찬가지로 문자열이다. 숫자 라벨 0과 1도 "positive", "yes" 같은 텍스트로 바꿔서 쓴다. 모델 입장에서는 이 문자열이 어느 태스크에서 왔는지 알 방법이 없다. 그냥 토큰 시퀀스 하나일 뿐이다.
그리고 원조 T5 논문에서 짚어둘 사실이 하나 있다. "하나의 T5가 모든 작업을 다 한다"는 표현은 정확하지 않다. 논문의 주된 방식은 사전학습된 체크포인트 하나에서 출발해 태스크마다 따로 파인튜닝하는 것이었고, 실제로 모든 태스크를 한꺼번에 학습시키면 성능이 조금 떨어졌다고 보고한다.
즉 배포 시점에는 번역용 가중치, 요약용 가중치, 분류용 가중치가 각각 별도로 존재한다. 공유되는 건 사전학습 체크포인트라는 출발점이다. 논문 제목의 "Transfer Learning"이 가리키는 게 정확히 이 부분이다.
"하나의 가중치가 여러 작업을 처리한다"는 그림은 그 다음 세대의 이야기다. T0나 FLAN-T5 같은 후속 연구는 같은 데이터셋을 여러 개의 서로 다른 문구로 표현한 템플릿을 대량으로 만들어 함께 학습시켰다. 같은 의도를 다른 말로 표현한 예제를 많이 볼수록, 모델이 특정 문장 패턴을 외우는 대신 요청의 의도 자체를 파악하게 된다는 발상이다.
T0 논문은 일부 태스크 유형을 아예 학습에서 제외한 뒤 그 유형으로 평가해서 이 일반화가 실제로 일어남을 보였다.
그리고 이 발상 ─ 같은 의미를 여러 지시문 형태로 바꿔 대량 학습시키기 ─ 이 다음 글에서 다룰 SFT(instruction tuning)의 핵심 아이디어와 사실상 같다.
[쉽게 다시 말하면]
프로그래밍의 구조체와 직렬화 함수를 떠올리면 정확하다.
태스크마다 필드 구성이 다른 구조체가 따로 있고, 그 구조체를 문자열로 바꾸는 함수도 태스크마다 따로 있다. 그런데 모델은 그 구조체 타입을 아예 볼 수 없다. 직렬화가 끝난 문자열 하나만 받는다.
다양성은 전부 모델 밖에서 처리되고, 모델 안쪽은 그 다양성의 존재조차 모른 채 항상 같은 일만 한다.
═══════════════════════════════════════
마무리
═══════════════════════════════════════
정리하면 이런 흐름이었다.
텍스트를 쪼개서 벡터로 만들고(위치 정보는 아직 없음) → 인코더 층 N번 통과(BERT 인코더와 같은 구조) → 최종 LayerNorm을 거쳐 인코더 출력 확정 → 디코더 층 N번 통과(인코더 층에 cross-attention 하나 추가) → 그 안에서 위치 정보는 attention score에 bias로 더해짐 → 최종 LayerNorm → Linear + Softmax → 다음 토큰
구조 자체는 앞선 두 글에서 본 것들의 조합에 가깝다. 새로운 건 cross-attention이 인코더와 디코더를 잇는 방식, 위치 정보를 relative bias로 처리하는 방식, 그리고 span corruption이라는 사전학습 목표 정도다.
한 가지 더 남는 감각이 있다면, T5가 보여준 건 "구조를 늘려서 기능을 늘리는" 방향이 아니라 "입출력 형식을 통일해서 구조를 하나로 유지하는" 방향이었다는 점이다. 이 방향이 이후 어디까지 갔는지는 다음 글에서 본다.
다음은 GPT-3에서 ChatGPT로 가는 과정, SFT와 RLHF다. 지금까지 세 글에서 본 건 전부 구조 이야기였는데, 그건 구조가 아니라 학습 파이프라인 이야기다.
|
|
