Post

다시 생각해보는 프롬프트 엔지니어링(Prompt Engineering Revisited) - Part 1

Introduction

이미 많은 아티클과 책에서 프롬프트와 프롬프트 엔지니어링에 대한 나름의 정의들을 내린 상태에서 굳이 반복할 필요가 있을까 싶지만, 아직 GPT와 같은 대형언어모델(Large Language Mode, 이하 LLM, 이후 논의에서는 LLM, 언어모델, 모델을 혼용) 맥락에서 사용하는 프롬프트/엔지니어링이라는 용어에 익숙하지 않은 분들을 위해 이들에 대한 정의로 본 포스팅을 시작해 보겠습니다.

LLM 맥락에서의 ‘프롬프트’란 언어모델이 생성하는 출력을 가이드하기 위해 모델에게 제공되는 입력으로 정의할 수 있습니다. 그렇다면 ‘프롬프트 엔지니어링’이란 언어 모델의 출력을 사용자가 원하는 방향으로, 모델이 특정 태스크를 더 잘 해결할 수 있도록, 프롬프트를 설계하고 최적화하는 작업을 일컫는 말이라 할 수 있습니다.

앞으로 몇 번에 걸쳐 “다시 생각해보는 프롬프트 엔지니어링”이라는 주제로 프롬프트 최적화에 대한 이야기들을 해 보려고 합니다. 이번 포스팅에서는 그 첫번째 파트로서 트랜스포머 아키텍처의 관점에서 바라 본 프롬프트 최적화에 대해 다뤄 보겠습니다. 많은 분들께서 아시다시피 GPT와 같은 LLM을 이루는 근간이 바로 이 트랜스포머 아키텍처입니다. 이후 이어지는 논의에서 트랜스포머 아키텍처를 처음 접하는 분들께서 보시기에 생소한 내용들이 나올 수 있는데, 디코더 유형 트랜스포머 아키텍처에 대한 conceptual tutorial을 미리 살펴보면 도움이 될 것입니다.

GPT는 원래의 트랜스포머의 디코더 부분만을 사용하는 ‘디코더 유형’ 트랜스포머 아키텍처를 사용합니다. 디코더 유형 트랜스포머 아키텍처는 여러 층의 트랜스포머 블록이 쌓여있는 트랜스포머 스택(Transformer Stack) 형태로 이루어져 있으며, 각 트랜스포머 블록은 핵심 요소로 셀프 어텐션(Self-Attention) 층피드 포워드 신경망(Feed-Forward Neural Network, 이하 FFNN) 층을 포함하고 있습니다. 블록 내에는 잔차연결(Residual Connection)과 층 정규화(Layer Normalization)를 수행하는 부분도 포함되어 있지만, 프롬프트 최적화라는 논의에 집중하기 위해 본 글에서는 셀프 어텐션 층, FFNN 층, 그리고 최종출력층에서 일어나는 일만 다루어보겠습니다.

셀프 어텐션 연산의 핵심

먼저 셀프 어텐션 연산이 어떻게 이루어지고, 이를 통해 어떤 일이 일어나는지를 알아봅시다. 최적화된 프롬프트가 모델로 하여금 특정 태스크를 가장 잘 해결하는 출력(응답)을 제공할 수 있도록 하는 것은 사실 트랜스포머 아키텍처의 모든 구성 요소들 간의 상호작용의 결과입니다. 그렇지만, 이들 중에서도 프롬프트 최적화를 이야기하기 위해 무엇보다 주목해야 할 요소는 바로 셀프 어텐션 층입니다.[1]

프롬프트 최적화를 다루기 전에 먼저 일반적인 프롬프트가 주어졌을 때, 이것이 어떻게 처리되는지를 차례차례 살펴봅시다. 여기서는 프롬프트가 한 문장으로 이루어져 있다고 가정해 봅시다. 그리고 이 문장은 일련의 단어들로 구성되어 있겠죠.

  1. 이 프롬프트는 토크나이저에 의해 일련의 하위 단어 토큰들(토큰 시퀀스)로 분해됩니다. 여기서는 논의의 편의상 프롬프트를 구성하는 영어 단어 하나를 토큰 하나로 간주해 보겠습니다.

  2. 프롬프트를 구성하는 각 토큰들은 임베딩층을 통해 임베딩 벡터로 변환됩니다. 이 때 토큰의 임베딩 벡터는 토큰의 의미론적, 구문론적 속성을 담고 있는 벡터입니다.

  3. 각 토큰 임베딩 벡터에 위치 인코딩이 추가됩니다. 이를 통해 각 토큰이 프롬프트 문장 내에서 어디에 위치하는지에 대한 정보까지 갖고 있는 토큰 임베딩 벡터가 만들어지게 됩니다.

  4. 드디어 프롬프트를 구성하는 각 토큰의 임베딩 벡터가 인과적 셀프 어텐션 층에 입력됩니다.

일단 프롬프트 문장을 구성하는 각 토큰들의 임베딩 벡터가 인과적 셀프 어텐션 층에 입력되는 순간까지 알아봤습니다. 이제 셀프 어텐션 층에서 일어나는 일을 알아볼 시간입니다.

셀프 어텐션 층에서는 프롬프트를 구성하는 토큰들이 서로 어떤 문맥적 관계가 있는지를 파악하는 작업이 수행됩니다. 이렇게 파악된 문맥적 관계는 각 토큰의 임베딩 벡터에 인코딩됩니다. 토큰의 기본적인 의미론적, 구문론적 속성을 담고 있던 임베딩 벡터가 이제 프롬프트 문장을 구성하는 토큰 사이의 문맥적 연관성에 대한 정보를 담은 문맥 벡터로 업데이트되는 것이지요. 이 과정은 프롬프트 최적화에 있어 매우 중요한 통찰을 제시합니다. 셀프 어텐션 층에서 일어나는 연산 과정을 차례차례 알아봅시다.

  1. 셀프 어텐션 층에는 Query(Q) 가중치 행렬, Key(K) 가중치 행렬, Value(V) 가중치 행렬이라는 세 가지 파라미터가 존재합니다. 이들은 언어모델을 대규모 텍스트 데이터(코퍼스)로 훈련할 때 학습시킬 수 있는 파라미터(trainable parameters)입니다. 즉, 이들 가중치 행렬에는 사전 학습과정을 통해 얻어진 언어의 문법, 의미, 단어 및 구 구조 사이의 상호 관계, 언어를 사용할 때 나타나는 패턴 등을 포함한 언어 지식이 내포되어 있습니다. 즉, 언어에 대한 정보들이 가중치 행렬내에 분산 표상(distributed representation)의 형태로 인코딩되어 있다고 말할 수도 있습니다.

  2. 프롬프트를 구성하는 각 토큰들의 임베딩 벡터는 이 Q, K, V 가중치 행렬과의 행렬곱 연산을 통해 Q 벡터, K 벡터, V 벡터로 변환됩니다. 이들 벡터가 구체적으로 무엇을 의미하는지는 도입부에 소개드린 자료의 54-56페이지를 참고해주세요.

  3. 프롬프트 토큰 중, 현재 우리가 고려하고 있는 토큰(a token)의 Q 벡터와 프롬프트 문장 내의 다른 모든 토큰들(other tokens)의 K 벡터 사이의 유사도를 계산하고, 이 유사도를 소프트맥스 함수를 통해 정규화함으로써 어텐션 가중치가 계산됩니다. 이 어텐션 가중치는 프롬프트 내에서 현재 우리가 고려하고 있는 특정 토큰(a token)을, 프롬프트를 구성하고 있는 다른 토큰들(other tokens)이 얼마나 중요하게 생각하는지를 나타냅니다(49-52 페이지).

  4. 다시 말하자면, 어텐션 가중치는 현재 토큰(a token)에 대해 다른 토큰들이 할당한 ‘관련도’(혹은 ‘중요도’)를 나타냅니다. 혹은 어텐션 가중치는 한(a token) 토큰이 프롬프트 내에서 다른 토큰들과 얼마나 밀접하게 관련되어 있는지를 나타내는 지표이며, 이를 통해 언어 모델은 주어진 문맥 내에서 각 토큰의 의미를 더 잘 이해하고 표현할 수 있습니다.

  5. 어텐션 가중치와 문장 내의 다른 토큰(other tokens) V 벡터의 가중합 연산을 통해 현재 토큰(a token)에 임베딩 벡터가 프롬프트의 문맥을 반영한 ‘문맥 벡터’로 업데이트 됩니다 (51 페이지).

셀프 어텐션 층에서 수행되는 연산들을 1-5에 소개했습니다. 프롬프트를 구성하는 모든 토큰에 대해 이와 동일한 연산이 수행되며, 이를 통해 프롬프트 토큰들의 임베딩 벡터가 모두 문맥 정보를 포함한 임베딩 벡터(문맥벡터)로 업데이트 됩니다.

트랜스포머 아키텍처 관점에서 프롬프트 최적화를 이야기하기 위해 꼭 알아야 할 내용들은 이제 모두 다루었습니다. 방금 살펴본 셀프 어텐션 연산을 요약해 보겠습니다.

  • 입력 프롬프트를 구성하는 각 토큰들의 임베딩 벡터는 Q, K, V 가중치 행렬과의 연산을 통해 Q, K, V 벡터로 변환됩니다. 이를 통해 학습 과정에서 Q, K, V 가중치 행렬에 압축되어 있는 언어 지식/패턴에 대한 정보들이 토큰의 Q, K, V 벡터에 전이됩니다. 반영된다고도 할 수 있겠습니다.

  • 이렇게 반영된 언어 지식/패턴은 ‘어텐션 가중치’라는 수치를 통해 구체적인 형태로 “발현”됩니다. 즉, 어텐션 가중치에는 프롬프트 문장을 구성하는 토큰 간의 의존 관계와 문맥적 연관성이 수치적으로 나타나게 됩니다. 이 가중치들은 V 벡터와의 가중합을 통해 문맥 정보가 담긴 문맥 벡터 만들어집니다.

트랜스포머 아키텍처와 프롬프트 최적화

셀프 어텐션 층

앞에서 Q, K, V 가중치 행렬이라는 것을 이야기하며 진한 글씨로 표기한 이유가 있습니다. 이 가중치 행렬들은 셀프 어텐션 메커니즘에서 토큰들 간의 문맥적 연관성을 파악하는 데 핵심적인 역할을 하기 때문에, 최적의 프롬프트 설계에 있어 중요한 고려 요소 중 하나로 작용합니다. 다시 말해, 특정 태스크 수행을 위한 프롬프트를 만들 때, Q, K, V 가중치 행렬이 효과적으로 활용될 수 있도록 태스크와 관련된, 강한 문맥적 연관성을 지닌 토큰들을 포함시키는 것이 도움될 수 있습니다. 이는 Q, K, V 가중치 행렬이 속한 사전 학습된 언어 모델의 파라미터들이 언어의 문법, 의미, 상호 관계, 언어 사용 패턴 등에 대한 정보를 내재하고 있기에 가능한 일입니다. 하지만 최적의 프롬프트를 설계하기 위해서는 Q, K, V 가중치 행렬 외에도 사전 학습된 언어 모델의 다른 파라미터들과 모델 아키텍처 전반을 고려해야 하겠죠.

방금 이야기한 것을 예시를 보면서 좀 더 구체적으로 살펴보겠습니다.

서울의 관광 명소를 알아내기라는 태스크 달성을 위해 언어모델에게 다음과 같은 프롬프트를 입력했다고 가정해 보겠습니다.

“Where are the best places to go in Seoul?”

이 프롬프트는 관광 명소 찾기라는 태스크에 덜 최적화된 프롬프트라고 볼 수 있습니다. 이 문장에 등장하는 ‘best’, ‘places’, ‘go’라는 토큰들은 장소나 목적지와 관련된 단어이긴 하지만, 관광 명소라는 특정한 유형의 장소를 지칭하지는 않습니다. 그렇기 때문에 Q, K, V 가중치 행렬에 학습된 언어 패턴에서 ‘best’, ‘places’, ‘go’라는 토큰들이 관광 명소를 찾는 문맥에서 자주 함께 사용되지 않았을 가능성이 높습니다. 이로 인해 ‘best’, ‘places’, ‘go’ 토큰의 Q 벡터와 K 벡터 간의 유사도가 낮아질 것이고, 결국 이들 토큰 간의 어텐션 가중치가 (최적화된 프롬프트와 비교했을 때) 상대적으로 낮아지는 결과로 이어질 수 있습니다. 이는 관광 명소와 관련하여 이들 토큰들 간의 문맥적 연관성이 약하다는 것을 의미하며 이 프롬프트는 모호하고 브로드한 답변 생성을 유도할 가능성이 높습니다.

이번에는 이 프롬프트를 더 다듬어서 다음과 같이 태스크에 최적화시켰다고 해 봅시다.

“What are the top tourist attractions in Seoul?”

이 프롬프트에는 ‘top’, ‘tourist’, ‘attractions’이라는 토큰들이 포함되어 있습니다. 이 토큰들은 관광명소 찾기라는 태스크 달성에 있어 일종의 키워드 토큰이라고 부를 수도 있겠습니다. 사전 훈련 과정에서 학습된 Q, K, V 가중치 행렬에는 “top”, “tourist”, “attractions”과 같은 키워드 토큰이 관광 명소를 찾는 문맥에서 자주 사용되었다는 정보가 인코딩되어 있을 가능성이 큽니다. 왜냐하면 이 키워드 토큰들은 관광 명소를 찾는 문맥에서 강한 연관성을 가지고 자주 등장했을 것이기 때문입니다. 이에 따라 셀프 어텐션 연산 시 이들 키워드 토큰 간에는 높은 어텐션 가중치가 할당될 것입니다. 즉, ‘top’, ‘tourist’, ‘attractions’ 토큰들이 서로를 중요하게 여긴다는 의미죠. 이들 키워드 토큰에 부여된 높은 어텐션 가중치로 인해 “What are the top tourist attractions in Seoul?”의 각 토큰 임베딩 벡터에는 관광 명소와 관련된 풍부한 문맥 정보가 인코딩될 수 있습니다. 이는 언어 모델이 질문의 의도를 더 잘 파악하고 보다 정확한 답변을 생성할 수 있는 기반을 마련해 줄 것입니다.

FFNN 층과 최종 출력층

셀프 어텐션 층에서, 각 토큰 임베딩 벡터는 셀프 어텐션 연산을 통해 문맥적으로 중요한 다른 토큰들의 정보를 통합하여 업데이트됩니다. 업데이트된 토큰들은 FFNN 층으로 전달되어 차원 확장, 활성화 함수에 의한 비선형 변환, 차원 축소의 과정을 거칩니다. 여기서는 먼저 최적화된 프롬프트인 “What are the top tourist attractions in Seoul?”를 이용해 설명을 이어나가 보겠습니다.

FFNN 연산을 통해 ‘top’, ‘tourist’, ‘attractions’와 같은 키워드 토큰들은 단순히 ‘최고의’, ‘관광객’, ‘명소’라는 표면적 의미를 넘어서 ‘가장 인기 있는’, ‘많은 사람들이 찾는’, ‘외국인 관광객’, ‘국내 여행객’, ‘관광 목적의 방문자’, ‘역사적 명소’, ‘자연 경관’, ‘유명한 거리’ 등 더 구체적이고 세부적인 정보를 담게 됩니다. 또한 ‘top’과 ‘attractions’의 조합은 ‘가장 인기 있는 관광 명소’라는 의미를, ‘tourist’와 ‘attractions’의 조합은 ‘관광객들이 많이 찾는 명소’라는 의미를 나타내는 등, 이 토큰들 사이의 복잡한 상호작용과 관계가 모델링됩니다.

여러 개의 트랜스포머 블록들을 지나면서, 프롬프트의 토큰 임베딩 벡터들은 반복적인 셀프 어텐션 및 FFNN 연산을 통해 지속적으로 업데이트됩니다. 이를 통해 상위 계층의 트랜스포머 블록으로 갈수록 키워드 토큰의 표현들이 점점 더 세밀하고 정확해지며, 그 결과, 주어진 프롬프트의 전체적인 문맥과 의도를 더 잘 반영하게 됩니다. 즉, 상위 계층의 트랜스포머 블록에서는, 서울의 관광 명소를 묻는 프롬프트의 전체적인 문맥과 의도를 파악하고 이를 반영하는 ‘서울에서 가장 유명한 관광지’, ‘서울을 대표하는 명소’ 등의 추상적이고 고차원적인 문맥 정보가 토큰 임베딩 벡터에 인코딩됩니다.

이러한 키워드 토큰을 포함한 프롬프트 구성 토큰들의 임베딩 벡터가 최종 출력층에 도달하게 되면, 문맥과 의도를 반영하는 풍부한 정보를 함축한 임베딩 벡터의 요소들이 전체 어휘(그 언어를 구성하고 있는 모든 어휘, vocaburary)에 대한 로짓 점수로 변환됩니다 (그리고 이후 소프트맥스 함수 적용을 통한 정규화를 거쳐 확률분포가 만들어집니다). 최종 임베딩 벡터가 갖고 있는 풍부한 표현과 의미 덕분에 이 ‘최적 프롬프트’ 바로 뒤에 이어질 토큰으로 ‘Gyeongbokgung Palace(경복궁)’, ‘N Seoul Tower(남산타워)’ 등 서울의 대표적인 관광 명소를 지칭하는 토큰들이 높은 확률로 선택될 것입니다.

반면에 관광 명소 찾기에 덜 최적화된 프롬프트인 “Where are the best places to go in Seoul?”에서는 ‘best’, ‘places’, ‘go’와 같이 관광, 여행, 목적지 등 다양한 문맥에서 사용될 수 있는 일반적인 단어들이 사용되었습니다.

FFNN 층에서 이루어지는 변환 과정을 거치면서 이들 토큰들은 ‘인기 있는 장소’, ‘추천할 만한 곳’, ‘가볼 만한 곳’ 등의 의미를 내포하게 되겠지만, 이러한 의미가 관광 명소라는 특정 유형의 장소와 완전히 일치하는 것은 아닙니다. 오히려 ‘맛집’, ‘쇼핑 명소’, ‘힙한 거리’ 등의 다양한 장소들을 의미할 가능성이 높습니다. 또한 ‘best’, ‘places’, ‘go’ 토큰 간의 상호작용과 조합을 통해 만들어지는 표현들, 예를 들면 ‘best places’나 ‘places to go’ 역시 관광 명소를 직접적으로 지칭하기보다는 좀 더 포괄적인 의미를 갖게 됩니다.

트랜스포머 블록의 상위 계층에 도달했을 때, 이 프롬프트의 토큰들은 서울에서 방문할 만한 장소를 묻는 문맥과 의도를 반영하겠지만, 그 표현이 갖는 모호함과 포괄성으로 인해, 구체적인 서울의 관광 명소만을 지칭하는 고차원적인 문맥 정보를 임베딩 벡터에 반영하지는 못할 것입니다.

결과적으로 최종 출력층에 도달한 이 프롬프트의 토큰 임베딩 벡터들은 관광 명소와 관련된 정보를 일부 포함하겠지만, 그외 다양한 유형의 장소들과 연관된 정보들도 함께 내포하고 있을 가능성이 높습니다. 따라서 이 프롬프트 바로 뒤에 서울의 대표 관광지를 지칭하는 토큰이 출력될 확률은 최적 프롬프트에 비해 상대적으로 낮아지게 될 것입니다. 그 대신 ‘Hongdae Street(홍대 거리)’, ‘Gyeongnidan Street(경리단길)’과 같이 말 그대로 가볼만한 곳, 그러나 관광 명소라 하기에는 애매한 장소를 가리키는 토큰들이 생성될 가능성도 무시할 수 없게 되는 것입니다.

Concluding Remarks

프롬프트 최적화에는 다양한 접근법이 있을 수 있습니다. 그 중의 하나는 이번 포스팅에서 살펴본 것과 같이, 언어 모델의 근간을 이루는 트랜스포머 아키텍처와 그 구성 요소들의 작동 원리에 대한 이해를 기반으로 프롬프트 최적화를 시도하는 것일 수 있습니다. 이러한 면에서 볼 때, 프롬프트 엔지니어링을 통해 최적화된 프롬프트란 키워드 토큰들 간의 문맥적 연관성을 극대화하여, 언어 모델이 주어진 태스크의 문맥과 의도를 정확히 파악하고, 적절한 응답을 생성할 수 있도록 하는 프롬프트라 할 수 있겠습니다. 이어지는 2편에서는 최적 프롬프트 설계와 이에 있어 언어모델의 역할에 대해 짚어보고, 프롬프트 엔지니어링의 전망에 대해 간단히 스케치해볼까 합니다. 물론 필자의 사정에 따라 그 주제가 변경될 수도 있습니다.😅

References

[1] 디코더 유형 트랜스포머 아키텍처의 셀프 어텐션 층은 사실 인과적 셀프 어텐션(Causal Self-Attention) 층입니다. 혹은 마스크 셀프 어텐션(Masked Self-Attention) 층이라고 불리기도 합니다. 인과적 셀프 어텐션에서는 현재의 토큰이 자신 이전에 위치한 토큰들에만 관심을 기울이고, 자신 이후의 토큰에는 관심을 기울이지 않도록 합니다. 인과적 셀프 어텐션을 통해 각 토큰은 자신 이전까지의 문맥 정보를 담고 있게 됩니다. 즉, 현재 단어 다음에 올 단어를 예측하는데 있어서 문장 내의 단어들 간의 순차적인 의존성을 반영함으로써, 문장의 의미를 정확하게 파악하고 의미론적으로 일관된 텍스트를 생성할 수 있게 해 줍니다 (85-102페이지 참조).

This post is licensed under CC BY 4.0 by the author.