대체 관계와 보완 관계

대체 관계와 보완 관계는 자연어 처리에서 텍스트 처리 과정에서 사용되는 두 가지 다른 개념입니다.

1. 대체 관계 (Substitution Relationship):
대체 관계는 문장에서 특정 토큰이 다른 토큰으로 대체되는 관계를 의미합니다. 대체 관계는 주어진 문장이나 텍스트에서 특정 토큰을 다른 토큰으로 변경하거나 대체하는 작업을 나타냅니다. 이를 통해 문장의 특정 부분을 다른 단어나 표현으로 대체하거나 수정할 수 있습니다. 대체 관계는 문장의 일부를 수정하거나 변형하여 다른 의미를 전달하거나 문장을 다듬는 데 사용될 수 있습니다.

2. 보완 관계 (Completion Relationship):
보완 관계는 문장이나 텍스트에서 주어진 문맥 또는 일부 정보를 기반으로 빠진 부분을 채우는 작업을 의미합니다. 보완 관계는 주어진 문장이나 텍스트에서 일부가 빠진 상태에서 문맥을 이해하고 완성하는 작업을 수행합니다. 이를 통해 문장의 일부를 완성하여 누락된 정보를 예측하거나 문장을 완전하게 만들 수 있습니다. 예를 들어, “나는 사과를 __.”라는 문장에서 빈칸을 적절한 단어로 채우는 작업은 보완 관계에 해당합니다.

대체 관계와 보완 관계는 텍스트 생성, 기계 번역, 질의 응답 등 다양한 자연어 처리 작업에서 활용될 수 있으며, 문장의 일부를 수정하거나 완성하는 과정에서 중요한 역할을 합니다.

SentencePiece 라이브러리 – 텍스트입력을 숫자로 쉽게 바꿔주는 토큰제작 오픈소스 라이브러리

SentencePiece는 Google에서 개발한 오픈 소스 라이브러리로, 텍스트 데이터를 효율적으로 토큰화하기 위해 사용됩니다. SentencePiece는 단어나 음절 단위 등 다양한 기준으로 텍스트를 토큰으로 분할할 수 있으며, 주로 자연어 처리 작업에서 언어 모델링, 기계 번역, 텍스트 분류 등에 활용됩니다.

SentencePiece의 주요 특징과 기능은 다음과 같습니다:

1. **Subword 기반 분할**: SentencePiece는 단어 수준이 아닌 subword 수준에서 텍스트를 분할합니다. 이를 통해 미등록어(out-of-vocabulary)나 희귀한 단어에 대한 효과적인 처리가 가능합니다. 또한, 다양한 언어의 특성을 고려하여 유연한 토큰화 방식을 제공합니다.

2. **양방향 토큰화**: SentencePiece는 양방향 토큰화(Bidirectional Tokenization)를 지원합니다. 이는 앞뒤 문맥을 동시에 고려하여 토큰을 생성하므로, 문맥 정보가 보존되는 장점이 있습니다.

3. **유니코드 지원**: SentencePiece는 다양한 유니코드 문자를 지원하며, 다국어 텍스트에 대한 효율적인 처리가 가능합니다.

4. **학습 데이터 생성**: SentencePiece는 기존의 텍스트 데이터로부터 토크나이저 학습 데이터를 생성할 수 있는 기능을 제공합니다. 이를 통해 사용자 정의 토크나이저를 학습할 수 있습니다.

5. **사전 크기 제어**: SentencePiece는 사용자가 지정한 사전 크기를 제한하여 메모리 사용량을 조절할 수 있습니다. 이를 통해 대규모 데이터셋에 대한 효율적인 토큰화 처리가 가능합니다.

SentencePiece는 다양한 프로그래밍 언어에서 사용할 수 있으며, Python에서는 `sentencepiece` 라이브러리를 통해 쉽게 활용할 수 있습니다. SentencePiece를 통해 텍스트를 효율적으로 토큰화하면, 자연어 처리 작업에서 높은 성능과 유연성을 기대할 수 있습니다.

언어 모델에서의 양자화: 파라미터 압축과 비트 수 감소

양자화는 단순히 말하면, 연속적인 값을 일정한 간격으로 나누어서 근사적인 값으로 표현하는 것입니다. 예를 들어, 연속적인 소수 값을 정수로 양자화한다고 가정해보겠습니다.

0부터 1까지의 범위를 0.1 간격으로 나누어보면 다음과 같습니다:
0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0

이렇게 나눈 값들을 양자화하여 정수로 표현한다면:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

이런식으로 연속적인 값들을 이산적인 값들로 근사화하여 표현하는 것이 양자화입니다. 이렇게 양자화를 하면, 값을 더 적은 비트 수로 표현할 수 있으며, 저장 공간을 절약하고 처리 속도를 향상시킬 수 있습니다. 그러나 양자화는 원래 값을 완벽하게 표현하지 않기 때문에, 약간의 정보 손실이 발생할 수 있습니다.

양자화는 컴퓨터 그래픽, 오디오 및 비디오 압축, 신경망 모델 압축 등 다양한 분야에서 활용되며, 효율적인 데이터 표현과 처리에 중요한 역할을 합니다.

언어모델에서의 양자화

언어 모델에서의 양자화는 모델의 파라미터를 더 작은 비트 수로 표현하는 기술입니다. 언어 모델은 많은 수의 가중치(weight)와 편향(bias)을 가지고 있는데, 이러한 가중치와 편향은 실수 값으로 표현됩니다. 그러나 이러한 실수 값들은 메모리를 많이 차지하고, 모델을 저장하고 전송하는 데에도 많은 비용이 발생할 수 있습니다.

양자화는 이러한 가중치와 편향을 더 작은 비트 수로 표현함으로써 메모리 요구량을 줄이고 모델의 크기를 줄일 수 있는 기술입니다. 예를 들어, 32비트 실수 값을 8비트 정수 값으로 양자화한다면, 해당 가중치의 표현 공간을 1/4로 줄일 수 있습니다. 이를 통해 모델의 메모리 사용량을 크게 줄일 수 있으며, 딥러닝 모델의 배포나 이식성 측면에서 이점을 얻을 수 있습니다.

양자화는 모델의 정확도에 약간의 영향을 줄 수 있지만, 적절하게 수행된다면 큰 성능 손실 없이 모델 크기를 줄일 수 있습니다. 따라서 양자화는 모바일 기기나 에지 디바이스에서의 실시간 추론, 대규모 모델의 분산 훈련 등의 시나리오에서 유용하게 활용될 수 있습니다.

ChatGPT 모델 개선을 위한 RLHF: 인간 피드백을 활용한 강화 학습

RLHF는 “Reinforcement Learning from Human Feedback”의 약자로, ChatGPT 모델을 개선하기 위해 사용되는 핵심 기술입니다. 기존의 ChatGPT 모델은 대화 데이터를 사용하여 사전 학습되지만, 그 결과물은 완벽하지 않을 수 있습니다. RLHF는 이러한 모델의 결함을 개선하기 위해 인간의 피드백을 통해 모델을 보완하는 강화 학습 방법입니다.

RLHF는 두 가지 주요 구성 요소로 이루어져 있습니다. 첫 번째는 “대화 데이터를 기반으로 한 사전 학습”입니다. 이 단계에서는 기존의 대화 데이터를 사용하여 모델을 사전 학습합니다. 이를 통해 모델은 언어 이해와 생성에 필요한 지식을 얻을 수 있습니다.

두 번째 구성 요소는 “인간 피드백을 통한 강화 학습”입니다. 이 단계에서는 실제 사용자와의 상호작용을 통해 모델을 개선합니다. 모델이 사용자와 대화하면서 생성한 응답은 실제 인간으로부터 피드백을 받습니다. 이 피드백은 “좋은” 응답과 “나쁜” 응답을 구별하는 데 사용됩니다. 모델은 이 피드백을 통해 자체 학습을 진행하고, 더 나은 응답을 생성할 수 있도록 조정됩니다.

RLHF는 강화 학습의 개념을 사용하여 모델을 개선하는 것이 특징입니다. 모델은 피드백을 통해 보상 신호를 받고, 이를 통해 정책을 조정하여 더 나은 응답을 생성하도록 학습됩니다. 이 과정은 반복적으로 수행되며, 모델은 높은 품질의 대화를 생성하기 위해 계속해서 향상됩니다.

RLHF는 모델의 개선을 위해 인간의 지식과 피드백을 활용하는 중요한 기술로서, 자동 대화 시스템의 성능과 품질을 향상시키는 데 기여합니다.

AI의 성패는 온도조절에 있다 – 언어 모델에서의 ‘temperature(온도)’와 사람 사이의 관계 비유

언어 모델에서의 “temperature”를 성격에 비유하면, 차가운 사람은 보수적이고 일관된 행동을 취하는 경향이 있으며, 따뜻한 사람은 탐색적이고 다양한 행동을 취하는 경향이 있습니다.

차가운 사람은 일상적인 상황에서 예측 가능한 행동을 선호하고, 새로운 상황에서도 안정감을 주는 선택을 할 가능성이 높습니다. 마찬가지로, 온도가 낮은 언어 모델은 주로 예상 가능한 문장 구조와 흐름을 따르며, 일관된 결과를 생성하는 경향이 있습니다.

반면에, 따뜻한 사람은 새로운 경험을 탐색하고 예상치 못한 행동을 취하는 경향이 있습니다. 이와 유사하게, 온도가 높은 언어 모델은 다양한 문장 구조와 선택지를 탐색하며, 창의적이고 다양한 결과를 생성하는 경향이 있습니다.

따라서, 성격이 차가운 사람은 상대적으로 보수적이고 일관된 행동을 선호하며, 성격이 따뜻한 사람은 탐색적이고 다양한 행동을 선호합니다. 이와 마찬가지로, 온도가 낮은 언어 모델은 일관성과 예측 가능성을 제공하며, 온도가 높은 언어 모델은 창의성과 다양성을 촉진합니다.

temperature 와 출력 확률 분포

Temperature는 언어 모델에서 출력 확률 분포를 조정하는 역할을 합니다. 출력 확률 분포는 각 단어 또는 토큰에 대한 확률값을 나타내는데, 이 확률값은 모델이 해당 단어를 다음에 예측할 가능성을 나타냅니다.

Temperature 값은 일반적으로 0보다 크거나 같은 양수로 설정되며, 값이 낮을수록 출력 확률 분포가 “더욱 집중된” 형태를 가지게 됩니다. 즉, 낮은 Temperature 값은 모델의 예측을 보다 확신할 수 있는 높은 확률값을 갖게 합니다. 이는 모델이 보다 예상 가능하고 일관된 결과를 생성하는 경향을 갖게 합니다.

반면에, Temperature 값을 높일수록 출력 확률 분포가 “더욱 분산된” 형태를 가지게 됩니다. 높은 Temperature 값은 모델의 예측을 더욱 탐색적으로 만들어 다양한 가능성을 표현하게 합니다. 이는 모델이 보다 창의적이고 다양한 결과를 생성하는 경향을 갖게 합니다.

예를 들어, 낮은 Temperature 값을 사용하면 모델은 가장 높은 확률값을 가지는 단어를 선택하여 상대적으로 일관된 문장을 생성할 가능성이 높아집니다. 반면에, 높은 Temperature 값을 사용하면 모델은 다양한 단어에 대해 비교적 균등한 확률값을 가지며, 결과적으로 보다 다양하고 창의적인 문장을 생성할 수 있습니다.

따라서, Temperature는 언어 모델의 출력 확률 분포를 제어하여 예측의 일관성과 다양성을 조절하는데 사용되며, 적절한 Temperature 값을 선택함으로써 모델의 생성 결과를 원하는 대로 조정할 수 있습니다.

 

Temperature 와 언어모델의 정확도의 관계

Temperature와 언어 모델의 정확도 사이에는 상호 관계가 있습니다.

일반적으로, 낮은 Temperature 값을 사용하는 경우 모델의 정확도가 상대적으로 높아질 수 있습니다. 이는 낮은 Temperature 값이 모델의 출력 확률 분포를 더 집중시켜서 가장 높은 확률값을 갖는 단어를 선택하게 만들기 때문입니다. 따라서 모델은 더 일관된 예측을 할 가능성이 높아지며, 이는 정확도 측면에서 긍정적인 영향을 미칠 수 있습니다. 낮은 Temperature 값은 모델의 출력을 제한함으로써 더 신뢰할 수 있는 예측을 할 수 있도록 도와줍니다.

그러나 Temperature 값을 지나치게 낮게 설정하면 모델이 예측을 지나치게 확신하게 되어 다양성이 부족하고 과도하게 일관된 결과를 생성할 수 있습니다. 이는 모델의 예측이 지나치게 제한되는 단점이 될 수 있습니다. 따라서 정확도를 향상시키는 동시에 모델의 생성 다양성도 고려해야 합니다.

반면에, 높은 Temperature 값을 사용하는 경우 모델의 정확도가 상대적으로 낮아질 수 있습니다. 높은 Temperature 값은 모델의 출력 확률 분포를 더 분산시키고 다양한 가능성을 표현하게 합니다. 이는 모델이 더 다양한 결과를 생성하게 되어 정확도를 향상시키는 데에는 제한적일 수 있습니다. 높은 Temperature 값은 모델의 예측이 탐색적이고 창의적이게 만들어 다양한 결과를 생성할 수 있도록 도와줍니다. 그러나 다양성이 증가함에 따라 모델의 예측은 더 불확실해지고 정확도가 감소할 수 있습니다.

따라서, Temperature 값은 언어 모델의 정확도와 다양성 사이의 균형을 조절하는 역할을 합니다. 적절한 Temperature 값을 선택함으로써 원하는 정확도와 생성 다양성을 조절할 수 있으며, 이는 모델을 사용하는 특정 작업이나 목적에 따라 결정되어야 합니다.

언어 모델의 정확도와 다양성 사이의 균형을 조절하는 방법

언어 모델의 정확도와 다양성 사이의 균형을 조절하기 위해서는 Temperature 값을 조정하는 방법을 사용할 수 있습니다. Temperature 값을 적절하게 설정함으로써 모델의 출력을 조절하고 원하는 정확도와 다양성 수준을 달성할 수 있습니다.

다음은 언어 모델의 정확도와 다양성 사이의 균형을 조절하는 몇 가지 방법입니다:

1. Temperature 값 조정: Temperature 값을 높이면 모델의 출력 확률 분포가 분산되어 다양한 가능성을 표현할 수 있게 됩니다. 이는 모델의 다양성을 높이는데 도움이 됩니다. 반면에, Temperature 값을 낮추면 모델의 출력 확률 분포가 집중되어 더 정확한 예측을 할 수 있게 됩니다. 따라서, 정확도와 다양성 사이의 균형을 조절하기 위해 Temperature 값을 조정해보는 것이 좋습니다.

2. Top-k 샘플링: Top-k 샘플링은 모델이 다음 단어를 선택할 때 고려할 가능성 있는 단어의 수를 제한하는 방법입니다. 작은 k 값은 모델의 선택지를 제한하여 더 정확한 예측을 유도할 수 있습니다. 큰 k 값은 모델에게 더 많은 선택지를 제공하여 다양성을 높일 수 있습니다.

3. Top-p 샘플링 (또는 Nucleus 샘플링): Top-p 샘플링은 모델이 다음 단어를 선택할 때 고려할 가능성 있는 단어의 누적 확률을 제한하는 방법입니다. 작은 p 값은 모델의 선택지를 제한하여 더 정확한 예측을 유도할 수 있습니다. 큰 p 값은 모델에게 더 많은 선택지를 제공하여 다양성을 높일 수 있습니다. 이 방법은 GPT 모델에서 “nucleus sampling”으로도 알려져 있습니다.

4. 토크나이저의 특정 설정 조정: 언어 모델의 정확도와 다양성은 토크나이저의 특정 설정에 따라 다를 수 있습니다. 예를 들어, 토크나이저의 문장 분리 기준, 특수 문자 처리, 대소문자 변환 등의 설정을 변경하여 모델의 출력을 조절할 수 있습니다.

정확도와 다양성 사이의 균형은 사용하는 모델과 특정 작업의 요구사항에 따라 다를 수 있습니다. 따라서, 실험과 조정을 통해 원하는 결과를 얻을 수 있는 최적의 방법을 찾아야 합니다.

토크나이저(Tokenizer) – 어떻게 단어를 숫자로 잘 바꿔줄까?

제목: 토크나이저(Tokenizer) – 어떻게 단어를 숫자로 잘 바꿔줄까?

서문:
자연어 처리(Natural Language Processing, NLP)는 컴퓨터가 인간의 언어를 이해하고 처리할 수 있도록 하는 분야입니다. 텍스트 데이터는 컴퓨터가 이해하기 어렵기 때문에, 이를 숫자 형태로 변환하는 과정이 필요합니다. 이러한 과정에서 토크나이저(Tokenizer)는 매우 중요한 역할을 수행합니다. 이번 블로그에서는 토크나이저가 어떻게 단어를 숫자로 잘 바꿔주는지에 대해 알아보겠습니다.

1. 토크나이저란?
토크나이저는 텍스트 데이터를 작은 단위로 쪼개는 도구입니다. 이 작은 단위를 토큰(Token)이라고 부르며, 토큰은 보통 단어, 문장 부호, 혹은 하나의 문자일 수도 있습니다. 토크나이저는 문장을 토큰 단위로 분리하여 컴퓨터가 처리할 수 있는 형태로 변환해줍니다.

2. 토크나이저의 종류
토크나이저에는 다양한 종류가 있으며, 언어, 작업의 목적, 도메인에 따라 선택되어야 합니다. 가장 기본적인 토크나이저는 공백이나 문장 부호를 기준으로 단어를 분리하는 방식입니다. 그러나 이 방식은 한국어나 복합어 등에서는 제대로 작동하지 않을 수 있습니다. 이러한 경우에는 형태소 분석기(Morphological Analyzer)를 사용하여 단어를 분리할 수 있습니다. 예를 들어, 한국어에서는 ‘토크나이저를 사용한다’라는 문장을 ‘토크나이저’, ‘를’, ‘사용’, ‘한다’로 분리하는 작업이 필요합니다.

3. 단어를 숫자로 변환하는 방법
토크나이저는 단어를 숫자로 변환하는 과정에서 사전(Dictionary)을 사용합니다. 사전은 각 단어에 고유한 숫자를 할당하여 단어를 숫자로 대체하는 역할을 합니다. 토크나이저는 텍스트 데이터를 입력으로 받아 각 단어를 토큰으로 분리한 후, 사전을 참고하여 해당 단어에 대응하는 숫자를 할당합니다.

예를 들어, ‘I love NLP’라는 문장을 토크나이저를 사용하여 단어를 숫자로 변환하는 과정을 살펴보겠습니다.

1) 입력 문장: ‘I love NLP’
2) 토크나이저를 사용하여 단어를 분리:
– ‘I’
– ‘love’
– ‘NLP’
3) 사전(Dictionary)을 생성하고 각 단어에 숫자를 할당:
– ‘I’: 1
– ‘love’: 2
– ‘NLP’: 3
4) 단어를 숫자로 대체하여 변환된 문장:
– [1, 2, 3]

이제 ‘I love NLP’라는 문장은 토크나이저를 통해 각 단어가 숫자로 대체된 [1, 2, 3] 형태로 표현됩니다. 이 숫자 형태로 된 데이터는 컴퓨터가 처리하기 용이하며, 자연어 처리 모델에 입력으로 사용될 수 있습니다.

토크나이저는 단어를 숫자로 변환하는 과정에서 정확한 분리와 사전의 구축이 매우 중요합니다. 잘못된 토크나이저 설정이나 사전의 부족한 단어 수는 모델의 성능을 저하시킬 수 있습니다. 따라서 토크나이저의 선택과 사전의 구축은 자연어 처리 작업의 성공에 큰 영향을 미치는 요소 중 하나입니다.

결론:
토크나이저는 텍스트 데이터를 숫자 형태로 변환하는 중요한 도구입니다. 단어를 토큰 단위로 분리하고, 사전을 활용하여 각 단어에 대응하는 숫자를 할당합니다. 이를 통해 자연어 처리 작업에 필요한 데이터를 숫자로 표현하여 컴퓨터가 처리할 수 있도록 합니다. 효과적인 토크나이저와 사전의 구축은 자연어 처리 모델의 성능 향상에 기여하며, 다양한 언어와 도메인에 적합한 토크나이저를 선택하는 것이 중요합니다.

GPT 언어 모델(Language Model) 구조 살펴보기

참고 문헌 : transformer 논문

언어 모델(Language Model)이란?

언어 모델(Language Model)은 자연어 처리(Natural Language Processing) 분야에서 사용되는 인공지능 모델로, 텍스트 시퀀스를 입력받아 다음에 올 단어나 문장을 예측하는 역할을 합니다. 주어진 문맥을 기반으로 문장의 의미를 이해하고 다음 단어를 예측하기 위해 확률 모델을 사용합니다. 언어 모델은 문장 생성, 기계 번역, 질의응답, 문장 감성 분석 등 다양한 자연어 처리 작업에 사용됩니다.

언어 모델의 종류 중에서 대표적인 것들은 다음과 같습니다:

1. GPT (Generative Pre-trained Transformer)
– GPT는 OpenAI에서 개발한 언어 모델로, Transformer 아키텍처를 기반으로 합니다.
– 비지도 학습으로 대규모의 텍스트 데이터를 사용하여 사전 훈련된 후, 다양한 자연어 처리 작업에 파인 튜닝하여 활용할 수 있습니다.
– 문장 생성 작업에 강점을 가지며, 풍부한 문맥 이해와 유창한 문장 생성이 가능합니다.

2. BERT (Bidirectional Encoder Representations from Transformers)
– BERT는 Google에서 개발한 언어 모델로, Transformer 아키텍처를 사용합니다.
– 사전 훈련 단계에서 양방향 문맥을 고려하여 텍스트를 학습합니다.
– 문맥 이해, 단어 임베딩, 문장 분류 등 다양한 자연어 처리 작업에서 좋은 성능을 보여줍니다.

3. XLNet (eXtreme Language understanding Network)
– XLNet은 구글 브레인에서 개발한 언어 모델로, Transformer 아키텍처를 기반으로 합니다.
– 전통적인 언어 모델의 한계를 극복하기 위해 순서에 구애받지 않는 학습 방식을 도입하였습니다.
– 양방향 문맥을 고려하면서도 순서에 더 자유로운 예측이 가능하여 성능을 향상시킬 수 있습니다.

4. GPT-3 (Generative Pre-trained Transformer 3)
– GPT-3는 OpenAI에서 개발한 언어 모델로, GPT 시리즈의 최신 버전입니다.
– 규모가 매우 큰 모델로, 수백억 개의 파라미터를 가지고 있으며, 다양한 자연어 처리 작업에서 놀라운 성능을 보여줍니다.
– 문장 생성, 기계 번역, 질의응답, 문장 감성 분석, 텍스트 요약 등 다양한 자연어 처리 작업에서 GPT, BERT, XLNet과 같은 언어 모델들은 뛰어난 성능을 발휘하고 있습니다. 이러한 언어 모델들은 사전 훈련된 모델로부터 파인 튜닝을 통해 특정 작업에 맞게 사용되며, 자연어 처리 분야에서 중요한 도구로 활용되고 있습니다.

GPT 언어 모델(Language Model) 의 문장 생성 작업 구조

GPT (Generative Pre-trained Transformer)는 딥러닝 기반의 언어 모델 중 하나로, 문장 생성 작업에서 많이 활용되는 모델입니다. GPT는 사전에 대규모의 텍스트 데이터로 사전 훈련(pre-training)된 후, 특정 작업에 맞게 파인 튜닝(fine-tuning)하여 사용됩니다.

문장 생성 작업에서 GPT는 이전에 주어진 문맥을 바탕으로 다음 단어나 문장을 예측합니다. 모델은 문장을 토큰 단위로 분리하고, 각 토큰에 대한 임베딩 벡터를 생성한 후, 여러 개의 층으로 구성된 트랜스포머(Transformer) 아키텍처를 통해 문맥을 인코딩합니다. 그리고 다음 단어를 예측하기 위해 디코더 부분을 사용합니다.

GPT는 이전 문맥을 통해 다음 단어를 예측하는 것이기 때문에, 생성된 문장은 일관성이 있고 자연스러운 텍스트를 생성하는 경향이 있습니다. GPT는 훈련 데이터에서 많은 패턴과 언어적 특성을 학습하므로, 주어진 문맥에 기반하여 문장을 연결하고 의미론적으로 일관된 문장을 생성할 수 있습니다.

GPT는 다양한 분야에서 문장 생성 작업에 활용됩니다. 예를 들어, 작가들이 소설이나 시를 자동으로 생성하는 데 사용되기도 하며, 챗봇 시스템이나 대화 모델에서 자연스러운 대화를 구현하는 데 활용될 수도 있습니다. 또한, 기계 번역, 요약, 질의응답 시스템 등에서도 GPT를 활용하여 텍스트 생성 작업을 수행할 수 있습니다.

따라서 여러분은 이 과정은 외워서 코드 단위에서 보고는 어떤 부분인지 즉각적으로 파악할 수 있도록 훈련을 해야 합니다. 이것이 이 글의 목적입니다.

1. 입력된 문장을 토큰 단위로 분리하고, 각 토큰에 대한 임베딩 벡터를 생성

GPT 모델에서 입력 문장을 토큰 단위로 분리하고 각 토큰에 대한 임베딩 벡터를 생성하는 과정은 Input Embedding과 Positional Embedding의 첫 번째 과정에 해당합니다.

Input Embedding은 토큰화된 문장의 각 토큰에 대해 해당 토큰의 임베딩 벡터를 찾아 모델의 입력으로 사용하는 과정입니다. 토큰 ID를 임베딩 테이블에서 찾아 해당하는 임베딩 벡터를 가져와서 사용합니다. 이를 통해 각 토큰은 임베딩 벡터로 표현되어 모델에 입력됩니다.

Positional Embedding은 토큰의 위치 정보를 임베딩하는 과정입니다. GPT 모델은 토큰의 상대적인 위치 정보를 학습하기 위해 Positional Encoding을 사용합니다. 이를 통해 모델은 문장 내 단어의 순서와 위치를 파악할 수 있습니다. Positional Embedding은 토큰의 임베딩 벡터에 위치 정보를 더해주는 방식으로 적용됩니다.

따라서, Input Embedding과 Positional Embedding은 GPT 모델의 문장 처리 과정에서 첫 번째 단계로, 입력 문장의 각 토큰에 대한 임베딩 벡터를 생성하는 역할을 수행합니다.

“Embedding”은 벡터 공간으로의 임베딩(표현)을 의미합니다. 특정 개체나 개념을 고차원의 벡터로 변환하여 컴퓨터가 이해하고 처리할 수 있는 형태로 표현하는 것을 말합니다. 임베딩은 주로 자연어 처리나 이미지 처리와 같은 기계학습 작업에서 사용되며, 기계가 효율적으로 특징을 학습하고 유사도를 계산할 수 있도록 도와줍니다.

자연어 처리에서의 임베딩은 단어, 문장 또는 문서를 고차원의 벡터로 매핑하는 것을 의미합니다. 단어 임베딩은 각 단어를 고정된 차원의 실수 벡터로 표현하는 것으로, 단어 간의 의미적 유사성이나 관계를 반영할 수 있습니다. 예를 들어, “사과”와 “바나나”라는 단어는 임베딩 공간에서 가까운 거리에 위치할 수 있습니다.

임베딩은 일반적으로 사전 학습된 모델이나 알고리즘을 사용하여 수행됩니다. 이를 통해 모델은 특정 작업에 맞게 데이터로부터 임베딩을 학습하거나 사전에 학습된 임베딩을 사용하여 효율적으로 정보를 표현하고 처리할 수 있습니다. 임베딩은 기계학습에서 중요한 개념으로 사용되며, 다양한 응용 분야에서 활용됩니다.

2.Dropout

“Dropout”이라는 용어는 해당 기법이 신경망의 학습 과정에서 뉴런을 일시적으로 “드롭아웃”시키는 방식으로 동작하기 때문에 사용되었습니다. 드롭아웃은 학습 중에 임의의 뉴런을 일시적으로 제외하거나 “드롭아웃”시켜서 비활성화시키는 것을 의미합니다. 이는 학습할 때마다 다른 부분집합의 뉴런들을 사용하는 것을 말하며, 이렇게 뉴런이 무작위로 누락되는 것을 시각적으로는 마치 뉴런이 “떨어지는(drop out)” 것처럼 보여서 “Dropout”이라는 용어가 사용되었습니다.

“Dropout”은 신경망의 일부를 일시적으로 제외함으로써 모델을 더 견고하게 만들고, 과적합을 방지하며, 다양한 조합의 뉴런을 활용하여 특징을 학습할 수 있도록 돕는 중요한 정규화 기법입니다. 이 용어는 이러한 동작 방식을 간결하게 설명하고 표현하기 위해 사용되었습니다.

Dropout은 과적합(overfitting)을 줄이기 위한 정규화(regularization) 기법 중 하나입니다.

과적합은 모델이 학습 데이터에 지나치게 적합되어, 새로운 데이터에 대한 일반화 능력이 떨어지는 현상을 말합니다. 모델이 학습 데이터를 완벽하게 기억하거나 특정 뉴런에 과도하게 의존하는 경우, 학습 데이터에 대해서는 높은 성능을 보이지만 새로운 데이터에 대해서는 성능이 낮아질 수 있습니다.

Dropout은 학습 과정에서 임의의 뉴런을 일시적으로 비활성화시킴으로써 모델이 특정 뉴런에 과도하게 의존하는 것을 방지합니다. 이를 통해 모델이 더욱 일반화된 특성을 학습하도록 도와주며, 과적합을 줄이는 데 도움을 줍니다. Dropout은 모델의 복잡성을 감소시키고 다양한 특징을 학습하도록 유도하여 일반화 능력을 향상시킵니다.

따라서, GPT 모델에서는 Dropout을 사용하여 과적합을 줄이고 모델의 일반화 성능을 개선하는 데에 활용됩니다.

Dropout과 노이즈 추가는 둘 다 모델의 일반화 능력을 향상시키기 위한 정규화(regularization) 기법입니다. 그러나 두 기법은 다른 방식으로 동작합니다.

Dropout은 신경망의 학습 과정에서 임의의 뉴런을 일시적으로 비활성화시킴으로써 모델이 특정 뉴런에 과도하게 의존하는 것을 방지합니다. 이를 통해 모델이 다양한 조합의 뉴런을 활용하여 특징을 학습할 수 있게 되어 일반화 성능을 향상시킵니다. Dropout은 모델의 일부를 무작위로 “드롭아웃”시킴으로써 일종의 앙상블 효과를 만들어냅니다. 학습할 때마다 다른 부분집합의 뉴런들을 사용하므로, 각 뉴런은 다른 뉴런의 도움 없이도 유용한 특징을 학습할 수 있게 됩니다.

반면에 노이즈 추가는 데이터에 임의의 노이즈를 추가하여 모델이 노이즈에 강인하게 학습하도록 유도하는 것입니다. 이는 모델에 학습 데이터에 존재할 수 있는 불확실성을 반영하고, 모델이 이러한 불확실성에 대응할 수 있는 일반화 능력을 향상시킵니다. 예를 들어, 이미지 분류 모델에 노이즈 추가를 적용한다면, 학습 데이터에 약간의 변형이 있는 이미지를 사용하여 모델이 다양한 형태와 변형에 대응할 수 있게 됩니다.

따라서, Dropout은 모델의 학습 과정에서 일부 뉴런을 무작위로 비활성화시키는 것이고, 노이즈 추가는 데이터에 임의의 노이즈를 추가하여 모델이 불확실성에 대응하도록 하는 것입니다.

3. 트랜스포머 레이어(Transformer Layer)에 입력

먼저 지금까지의 순서로 설명드리겠습니다:

1. 입력된 문장을 토큰 단위로 분리하고, 각 토큰에 대한 임베딩 벡터를 생성합니다.
2. 생성된 임베딩 벡터에 Dropout 과정을 적용합니다. Dropout은 일부 뉴런을 임의로 비활성화하여 모델의 일반화 성능을 향상시키는 정규화 기법입니다.
3. 이후, 디코더에 해당하는 트랜스포머 레이어에 입력됩니다. 트랜스포머 레이어는 입력 문장의 토큰들을 서로 다른 위치에 대해 서로 다른 관계를 고려하여 처리하는 역할을 합니다. 이를 통해 문장의 문맥을 파악하고 다음 단어를 예측하거나 출력 문장을 생성합니다.

요약하면, 입력 문장을 토큰화하고 임베딩 벡터로 변환한 후, Dropout을 적용하여 일부 뉴런을 랜덤하게 비활성화한 후, 이 과정을 거친 임베딩 벡터가 트랜스포머 디코더 레이어에 입력되어 문장 생성이나 예측을 수행합니다.

트랜스포머 레이어(Transformer Layer)는 딥러닝 모델인 트랜스포머에서 핵심적인 구성 요소입니다. 트랜스포머는 주로 자연어 처리 작업에 사용되며, 번역, 요약, 질의응답 등 다양한 작업에 적용됩니다. 아래에서는 트랜스포머 레이어의 주요 구성 요소에 대해 설명하겠습니다.

3.1. Layer Norm (Normalization):
트랜스포머 레이어의 입력과 출력에 대한 정규화를 수행하는 과정입니다. Layer Norm은 입력 데이터의 평균과 표준편차를 계산하여 정규화를 수행하며, 이를 통해 학습 안정성을 높이고 모델의 성능을 향상시킵니다.

3.2. Masked Self Attention:
트랜스포머에서는 셀프 어텐션(Self Attention) 메커니즘을 사용합니다. 이는 입력 시퀀스의 모든 위치에서 서로 다른 위치들 간의 상관 관계를 계산하여 각 위치의 표현을 조정하는 메커니즘입니다. Masked Self Attention은 현재 위치보다 미래의 위치에 대한 정보를 참고하지 못하도록 마스킹하는 방식으로 적용됩니다. 이를 통해 디코더에서는 예측 과정에서 미래의 정보를 사용하지 않고 현재까지의 정보만을 바탕으로 문장을 생성할 수 있습니다.

3.3. Add:
트랜스포머 레이어에서는 입력 데이터와 셀프 어텐션 또는 피드 포워드 신경망의 출력을 더하는 과정을 수행합니다. 이를 통해 입력 데이터에 대한 새로운 정보를 추가하고, 모델의 표현 능력을 향상시킵니다.

3.4. Feed Forward Neural Network:
트랜스포머 레이어의 하위 블록으로, 입력 데이터에 대한 비선형 변환을 수행합니다. 피드 포워드 신경망은 일련의 선형 변환과 활성화 함수를 포함하며, 입력 데이터의 고차원 표현을 학습하여 모델의 표현 능력을 강화합니다.

트랜스포머는 일반적으로 여러 개의 트랜스포머 레이어를 쌓아서 사용합니다. 각 레이어는 동일한 구조를 가지며, 입력 데이터를 여러 번 반복하여 트랜스포머 레이어를 통과시킵니다. 일반적으로 트랜스포머 모델은 여러 개의 인코더 레이어와 디코더 레이어로 구성됩니다. 입력 데이터는 인코더 레이어를 거치면서 점진적으로 추상화되고, 디코더 레이어에서는 해당 추상화된 표현을 기반으로 문장을 생성합니다. 각 레이어의 출력은 다음 레이어로 전달되어 점차적으로 더 복잡하고 추상적인 정보를 학습하게 됩니다.

따라서, 트랜스포머 모델은 입력 데이터를 여러 번 반복하여 인코더와 디코더 레이어를 거치며, 각 레이어에서는 입력 데이터에 대한 임베딩, 어텐션, 레이어 정규화, 피드 포워드 신경망 등의 연산이 수행됩니다. 이 과정을 통해 입력 문장과 관련된 정보를 추출하고, 디코더에서는 생성된 문장을 점진적으로 완성해 나갑니다. 이러한 반복 구조를 통해 트랜스포머 모델은 문장 생성과 같은 다양한 자연어 처리 작업에서 뛰어난 성능을 발휘할 수 있습니다.

4. 다시 Layer Norm

Layer Norm은 트랜스포머 레이어의 출력을 처리하는 과정 중 하나입니다. 이는 각 트랜스포머 레이어의 출력 벡터에 대해 평균과 표준편차를 계산하여 정규화를 수행하는 과정입니다. 이를 통해 트랜스포머 레이어의 출력을 안정화시키고 학습을 원활하게 합니다.

5. Output Embedding

Output Embedding은 트랜스포머 레이어의 출력에 대한 임베딩을 생성합니다. 이는 문장의 다음 단어를 예측하기 위해 출력 벡터를 해당 단어의 임베딩 공간으로 변환하는 과정입니다.

6. Softmax

Softmax함수는 출력 벡터를 확률 분포로 변환합니다. 이를 통해 다음 단어의 예측 확률을 얻고, 문장 생성이나 단어 예측 작업에 사용할 수 있습니다.

이러한 과정을 통해 트랜스포머 모델은 입력 문장을 임베딩하고, 토큰 간의 관계를 학습하여 문맥을 이해하고 출력 문장을 생성합니다.

예시 1 )사과는 맛있니?라는 물음에 대한 답

사과는 맛있니?라는 물음에 대한 답변을 예시로 들어 설명해드리겠습니다.

1. 입력된 문장 “사과는 맛있니?”를 토큰 단위로 분리합니다.
– 토큰: [“사과”, “는”, “맛있니”, “?”]

2. 각 토큰에 대해 임베딩 벡터를 생성합니다.
– 임베딩 벡터: [벡터1, 벡터2, 벡터3, 벡터4]

3. 생성된 임베딩 벡터에 Dropout 과정을 적용합니다.
– Dropout 적용 후 임베딩 벡터: [벡터1′, 벡터2′, 벡터3′, 벡터4′]

4. 트랜스포머 레이어에 입력된 임베딩 벡터를 처리합니다.
– Self Attention: 임베딩 벡터에 대해 서로 다른 위치의 토큰 간의 관계를 고려하여 정보를 합성합니다.
– Feed Forward Neural Network: Self Attention에서 얻은 정보를 입력으로 받아 전방향 신경망을 통해 변환합니다.
– 처리된 임베딩 벡터: [처리된 벡터1, 처리된 벡터2, 처리된 벡터3, 처리된 벡터4]

5. Layer Norm을 통해 트랜스포머 레이어의 출력을 정규화합니다.
– 정규화된 출력 벡터: [정규화된 벡터1, 정규화된 벡터2, 정규화된 벡터3, 정규화된 벡터4]

6. 출력 벡터를 사용하여 다음 단어를 예측하고, “네”와 같은 답변을 생성합니다.

이렇게 입력 문장 “사과는 맛있니?”가 각 단계를 거치며 처리되고, 최종적으로 “네”라는 답변이 생성됩니다. 트랜스포머 모델은 입력 문장의 문맥을 이해하고 적절한 응답을 생성하는 능력을 가지고 있습니다.

 

 

 

스탠포드 알파카(Alpaca) 코드분석 – 누구나 챗GPT 3.5성능의 모델을 만들 수 있다. 파인 튜닝. 2 train.py

알파카 모델의 파인튜닝을 위한 train.py 코드를 살펴봅니다.

26번째 줄입니다.

IGNORE_INDEX = -100
DEFAULT_PAD_TOKEN = "[PAD]"
DEFAULT_EOS_TOKEN = "</s>"
DEFAULT_BOS_TOKEN = "</s>"
DEFAULT_UNK_TOKEN = "<unk>"
PROMPT_DICT = {
    "prompt_input": (
        "Below is an instruction that describes a task, paired with an input that provides further context. "
        "Write a response that appropriately completes the request.\n\n"
        "### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:"
    ),
    "prompt_no_input": (
        "Below is an instruction that describes a task. "
        "Write a response that appropriately completes the request.\n\n"
        "### Instruction:\n{instruction}\n\n### Response:"
    ),
}

해당 코드는 일부 상수와 프롬프트 관련 사전을 정의하는 부분입니다. 코드를 하나씩 살펴보면 다음과 같습니다:

– `IGNORE_INDEX = -100`: 무시해야 하는 인덱스 값을 나타내는 상수로, 주로 손실 함수에서 사용됩니다.

– `DEFAULT_PAD_TOKEN = “[PAD]”`: 패딩 토큰을 나타내는 상수로, 시퀀스의 길이를 맞추기 위해 사용됩니다.

– `DEFAULT_EOS_TOKEN = “</s>”`: 문장의 끝을 나타내는 상수로, 문장 생성에서 사용될 수 있습니다.

– `DEFAULT_BOS_TOKEN = “<s>”`: 문장의 시작을 나타내는 상수로, 문장 생성에서 사용될 수 있습니다.

– `DEFAULT_UNK_TOKEN = “<unk>”`: 알 수 없는 단어를 나타내는 상수로, 모델이 단어를 인식하지 못할 때 사용될 수 있습니다.

– `PROMPT_DICT`: 프롬프트 관련 사전으로, 다양한 프롬프트 형식을 정의합니다. “prompt_input”은 입력과 함께 작업을 설명하는 프롬프트 형식을 나타내며, “prompt_no_input”은 입력 없이 작업을 설명하는 프롬프트 형식을 나타냅니다. 해당 형식에는 `{instruction}`과 `{input}`이 있는데, 이는 실제로 대체될 값들을 나타냅니다.

이러한 상수와 프롬프트 관련 사전은 모델의 동작에 사용되며, 프롬프트를 생성하거나 데이터를 처리할 때 활용될 수 있습니다.

182번째 줄의 train 함수를 봅니다. 이 부분이 핵심인 부분입니다.

 

def train():
    parser = transformers.HfArgumentParser((ModelArguments, DataArguments, TrainingArguments))
    model_args, data_args, training_args = parser.parse_args_into_dataclasses()

    model = transformers.AutoModelForCausalLM.from_pretrained(
        model_args.model_name_or_path,
        cache_dir=training_args.cache_dir,
    )

    tokenizer = transformers.AutoTokenizer.from_pretrained(
        model_args.model_name_or_path,
        cache_dir=training_args.cache_dir,
        model_max_length=training_args.model_max_length,
        padding_side="right",
        use_fast=False,
    )
    special_tokens_dict = dict()
    if tokenizer.pad_token is None:
        special_tokens_dict["pad_token"] = DEFAULT_PAD_TOKEN
    if tokenizer.eos_token is None:
        special_tokens_dict["eos_token"] = DEFAULT_EOS_TOKEN
    if tokenizer.bos_token is None:
        special_tokens_dict["bos_token"] = DEFAULT_BOS_TOKEN
    if tokenizer.unk_token is None:
        special_tokens_dict["unk_token"] = DEFAULT_UNK_TOKEN

    smart_tokenizer_and_embedding_resize(
        special_tokens_dict=special_tokens_dict,
        tokenizer=tokenizer,
        model=model,
    )

    data_module = make_supervised_data_module(tokenizer=tokenizer, data_args=data_args)
    trainer = Trainer(model=model, tokenizer=tokenizer, args=training_args, **data_module)
    trainer.train()
    trainer.save_state()
    trainer.save_model(output_dir=training_args.output_dir)

해당 코드는 모델을 훈련하는 `train()` 함수입니다. 코드를 하나씩 살펴보면 다음과 같습니다:

– `parser = transformers.HfArgumentParser((ModelArguments, DataArguments, TrainingArguments))`: 모델, 데이터, 훈련 인자들을 파싱하기 위한 Hugging Face의 `HfArgumentParser`를 초기화합니다. `ModelArguments`, `DataArguments`, `TrainingArguments`는 각각 모델, 데이터, 훈련에 필요한 인자들을 정의한 데이터 클래스입니다. 이렇게 parser안에 들어가는 데이터 클래스는 각각 45번째 줄에 있습니다.

parser안에 들어가는 데이터 클래스 코드들

@dataclass
class ModelArguments:
    model_name_or_path: Optional[str] = field(default="facebook/opt-125m")


@dataclass
class DataArguments:
    data_path: str = field(default=None, metadata={"help": "Path to the training data."})


@dataclass
class TrainingArguments(transformers.TrainingArguments):
    cache_dir: Optional[str] = field(default=None)
    optim: str = field(default="adamw_torch")
    model_max_length: int = field(
        default=512,
        metadata={"help": "Maximum sequence length. Sequences will be right padded (and possibly truncated)."},
    )

– `model_args, data_args, training_args = parser.parse_args_into_dataclasses()`: 파싱된 인자들을 해당 데이터 클래스들의 인스턴스로 저장합니다.

 

이제 다시 train() 으로 가서 살펴봅니다.

크게 모델과 토크나이저의 경로를 넣어 가져오는 부분입니다.

모델

– `model = transformers.AutoModelForCausalLM.from_pretrained(…)`: Hugging Face의 `AutoModelForCausalLM` 클래스를 사용하여 사전 훈련된 모델을 불러옵니다. `model_args.model_name_or_path`에 지정된 모델 이름 또는 경로로부터 모델을 가져옵니다.

토크나이저

– `tokenizer = transformers.AutoTokenizer.from_pretrained(…)`: Hugging Face의 `AutoTokenizer` 클래스를 사용하여 사전 훈련된 토크나이저를 불러옵니다. `model_args.model_name_or_path`에 지정된 모델 이름 또는 경로로부터 토크나이저를 가져옵니다.

토큰들을 셋팅해 주는 부분입니다.

special_tokens_dict = dict()
    if tokenizer.pad_token is None:
        special_tokens_dict["pad_token"] = DEFAULT_PAD_TOKEN
    if tokenizer.eos_token is None:
        special_tokens_dict["eos_token"] = DEFAULT_EOS_TOKEN
    if tokenizer.bos_token is None:
        special_tokens_dict["bos_token"] = DEFAULT_BOS_TOKEN
    if tokenizer.unk_token is None:
        special_tokens_dict["unk_token"] = DEFAULT_UNK_TOKEN

위의 코드는 `special_tokens_dict`라는 사전을 생성하고, 이를 사용하여 토크나이저의 특수 토큰을 설정하는 역할을 합니다.

코드를 살펴보면 다음과 같은 과정을 거칩니다:
– 먼저, 빈 `special_tokens_dict` 사전을 생성합니다.
– `if` 문을 사용하여 각각의 특수 토큰에 대해 검사합니다. 검사할 토큰은 `tokenizer` 객체에 따라 다릅니다.
– `pad_token`이 `None`인 경우, `special_tokens_dict`에 `”pad_token”: DEFAULT_PAD_TOKEN`을 추가합니다. `DEFAULT_PAD_TOKEN`은 패딩 토큰의 기본값입니다.
– 마찬가지로 `eos_token`, `bos_token`, `unk_token`에 대해서도 동일한 절차를 수행합니다. 각각의 토큰이 `None`인 경우, `special_tokens_dict`에 해당 토큰과 기본값을 추가합니다.

이 코드는 토크나이저의 특수 토큰이 설정되지 않은 경우에만 기본값을 할당하는 역할을 합니다. 설정되지 않은 토큰에 대해 기본값을 지정함으로써, 토크나이저에 필요한 특수 토큰들이 모두 설정되었는지 확인하고, 필요한 경우 기본값을 사용하여 설정합니다.

– `smart_tokenizer_and_embedding_resize(…)`: 토크나이저와 모델의 임베딩 크기를 조정하는 함수입니다. 토크나이저의 특수 토큰들과 모델의 임베딩 크기를 맞추기 위해 호출됩니다.

이 smart_tokenizer_and_embedding_resize 관련  65번째 줄을 봅니다.

def smart_tokenizer_and_embedding_resize(
    special_tokens_dict: Dict,
    tokenizer: transformers.PreTrainedTokenizer,
    model: transformers.PreTrainedModel,
):
    """Resize tokenizer and embedding.

    Note: This is the unoptimized version that may make your embedding size not be divisible by 64.
    """
    num_new_tokens = tokenizer.add_special_tokens(special_tokens_dict)
    model.resize_token_embeddings(len(tokenizer))

    if num_new_tokens > 0:
        input_embeddings = model.get_input_embeddings().weight.data
        output_embeddings = model.get_output_embeddings().weight.data

        input_embeddings_avg = input_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)
        output_embeddings_avg = output_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)

        input_embeddings[-num_new_tokens:] = input_embeddings_avg
        output_embeddings[-num_new_tokens:] = output_embeddings_avg


이 중 아래부분을 봅니다. 토크나이저의 특수 토큰을 추가하고, 토큰이 추가되어 길이가 바꼇을 것이므로 모델의 토큰 임베딩 크기를 조정하는 역할을 합니다.

num_new_tokens = tokenizer.add_special_tokens(special_tokens_dict)
    model.resize_token_embeddings(len(tokenizer))

아래는 코드의 동작과정을 설명합니다:

1. `tokenizer.add_special_tokens(special_tokens_dict)`:
– `tokenizer.add_special_tokens()` 함수를 사용하여 `special_tokens_dict`에 정의된 특수 토큰을 토크나이저에 추가합니다.
– 이 함수는 새로운 특수 토큰의 수(`num_new_tokens`)를 반환합니다. 추가된 토큰의 개수를 `num_new_tokens` 변수에 할당합니다.

2. `model.resize_token_embeddings(len(tokenizer))`:
– `model.resize_token_embeddings()` 함수를 사용하여 모델의 토큰 임베딩 크기를 조정합니다. 토큰이 추가되었으니 토큰의 길이가 바뀔것이기 때문에 조정을 하는 것입니다.
– 이 함수는 토크나이저의 토큰 개수에 맞게 모델의 임베딩 크기를 조정합니다.
– `len(tokenizer)`는 토크나이저에 추가된 특수 토큰을 포함한 전체 토큰 개수입니다.
– 모델의 임베딩 크기를 토크나이저의 토큰 개수와 동일하게 조정함으로써, 토큰 임베딩에 새로운 토큰을 반영할 수 있게 됩니다.

이렇게 코드는 토크나이저에 새로운 특수 토큰을 추가하고, 모델의 토큰 임베딩 크기를 조정하여 추가된 토큰을 모델에 반영합니다. 이를 통해 모델은 새로운 토큰을 이해하고 처리할 수 있게 됩니다.

그 다음 줄에서는 토크나이저를 업데이트를 하기 위한 코드 입니다.

if num_new_tokens > 0:
        input_embeddings = model.get_input_embeddings().weight.data
        output_embeddings = model.get_output_embeddings().weight.data

        input_embeddings_avg = input_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)
        output_embeddings_avg = output_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)

        input_embeddings[-num_new_tokens:] = input_embeddings_avg
        output_embeddings[-num_new_tokens:] = output_embeddings_avg

위의 코드는 새로 추가된 토큰에 대한 임베딩을 초기화하는 역할을 합니다.

아래는 코드의 동작과정을 설명합니다:

1. `if num_new_tokens > 0:`:
– `num_new_tokens` 변수에는 토크나이저에 추가된 새로운 토큰의 수가 저장되어 있습니다.
– 이 조건문은 추가된 새로운 토큰이 존재할 경우에만 코드 블록을 실행합니다.

2. `input_embeddings = model.get_input_embeddings().weight.data`:
– `model.get_input_embeddings()` 함수를 사용하여 모델의 입력 임베딩을 가져옵니다.
– `weight.data`를 사용하여 임베딩 가중치에 접근합니다. 이는 모델의 입력 임베딩 행렬을 나타냅니다.

3. `output_embeddings = model.get_output_embeddings().weight.data`:
– `model.get_output_embeddings()` 함수를 사용하여 모델의 출력 임베딩을 가져옵니다.
– `weight.data`를 사용하여 임베딩 가중치에 접근합니다. 이는 모델의 출력 임베딩 행렬을 나타냅니다.

4. `input_embeddings_avg = input_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)`:
– `input_embeddings`에서 새로운 토큰을 제외한 이전 임베딩들을 선택합니다.
– 선택된 이전 임베딩들의 평균을 계산합니다.
– 이는 새로 추가된 토큰에 대한 초기화 값으로 사용될 평균 임베딩입니다.

5. `output_embeddings_avg = output_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)`:
– `output_embeddings`에서 새로운 토큰을 제외한 이전 임베딩들을 선택합니다.
– 선택된 이전 임베딩들의 평균을 계산합니다.
– 이는 새로 추가된 토큰에 대한 초기화 값으로 사용될 평균 임베딩입니다.

6. `input_embeddings[-num_new_tokens:] = input_embeddings_avg`:
– `input_embeddings`의 마지막 `num_new_tokens` 행에 평균 임베딩 값을 할당합니다.
– 이를 통해 새로운 토큰에 대한 입력 임베딩이 초기화됩니다.

7. `output_embeddings[-num_new_tokens:] = output_embeddings_avg`:
– `output_embeddings`의 마지막 `num_new_tokens` 행에 평균 임베딩 값을 할당합니다.
– 이를 통해 새로운 토큰에 대한 출력 임베딩이 초기화됩니다.

이렇게 코드는 새로 추가된 토큰에 대한 임베딩을 이전 임베딩들의 평균 값으로 초기화합니다. 이렇게 함으로써 새로 추가된 토큰들은 이전 토큰들과 유사한 임베딩 값을 가지게 됩니다. 이는 모델이 새로운 토큰에 대한 처리를 학습하는 데 도움이 됩니다. 따라서 모델은 이전 토큰들과 비슷한 특성을 가진 새로운 토큰들을 잘 이해하고 예측할 수 있게 됩니다.

`smart_tokenizer_and_embedding_resize` 함수는 토크나이저와 임베딩을 조정하는 역할을 합니다.

 

smart_tokenizer_and_embedding_resize(
        special_tokens_dict=special_tokens_dict,
        tokenizer=tokenizer,
        model=model,
    )

`smart_tokenizer_and_embedding_resize` 함수는 특수 토큰을 추가하고 모델의 임베딩 크기를 조정하는 작업을 수행합니다. 이 함수는 다음과 같은 인자를 받습니다:

– `special_tokens_dict`: 추가할 특수 토큰들의 딕셔너리입니다. 이 딕셔너리는 토크나이저에 추가될 토큰들의 종류와 값을 포함합니다.
– `tokenizer`: 변환 작업에 사용될 토크나이저 객체입니다.
– `model`: 임베딩 크기를 조정할 모델 객체입니다.

함수의 주요 작업은 다음과 같습니다:

1. `tokenizer.add_special_tokens(special_tokens_dict)`를 호출하여 토크나이저에 특수 토큰들을 추가합니다. 이 작업은 토크나이저의 특수 토큰 관련 속성을 업데이트하고, 추가된 토큰의 수를 반환합니다.
2. `model.resize_token_embeddings(len(tokenizer))`를 호출하여 모델의 임베딩 크기를 조정합니다. 이 작업은 모델의 임베딩 행렬 크기를 토크나이저에 추가된 토큰의 수에 맞게 조정합니다.

이를 통해 특수 토큰이 토크나이저와 모델의 임베딩에 제대로 반영되고, 모델은 추가된 토큰을 적절하게 처리할 수 있게 됩니다.

 

 

 data_module = make_supervised_data_module(tokenizer=tokenizer, data_args=data_args)
    trainer = Trainer(model=model, tokenizer=tokenizer, args=training_args, **data_module)
    trainer.train()
    trainer.save_state()
    trainer.save_model(output_dir=training_args.output_dir)

이 코드는 모델을 학습시키는 일련의 작업을 수행합니다.

1. `make_supervised_data_module(tokenizer=tokenizer, data_args=data_args)` 함수를 호출하여 데이터 모듈을 생성합니다. 이 함수는 주어진 토크나이저와 데이터 설정을 기반으로 데이터 모듈을 구성합니다. 데이터 모듈은 데이터를 로드하고 전처리하는 데 사용됩니다.

2. `Trainer` 클래스의 인스턴스를 생성합니다. 이때 모델, 토크나이저, 학습 설정 (`training_args`) 및 데이터 모듈의 인자들을 전달합니다. `Trainer`는 학습을 관리하고 모델을 학습하는 역할을 수행합니다.

3. `trainer.train()`을 호출하여 모델을 학습시킵니다. 이 과정에서 학습 데이터셋을 사용하여 모델의 가중치를 업데이트하고 손실을 최소화하도록 학습됩니다.

4. `trainer.save_state()`를 호출하여 학습 중간에 트레이너의 상태를 저장합니다. 이는 학습을 일시 중지하고 나중에 재개할 수 있도록 합니다.

5. `trainer.save_model(output_dir=training_args.output_dir)`를 호출하여 학습된 모델을 지정된 출력 디렉토리에 저장합니다. 이는 학습된 모델을 나중에 로드하여 추론에 사용할 수 있도록 합니다.

이렇게 코드는 데이터 모듈 생성, 모델 학습, 상태 저장, 모델 저장 등의 작업을 통해 전체적인 학습 프로세스를 수행합니다.

특히 파인튜닝관점에서 2번 3번 부분을 다시 한번 설명해 보겠습니다.

trainer = Trainer(model=model, tokenizer=tokenizer, args=training_args, **data_module)
    trainer.train()

 

이 코드는 파인튜닝을 수행하기 위해 필요한 핵심 구성 요소를 설정하고 모델을 학습시키는 역할을 합니다.

1. `Trainer` 클래스의 인스턴스를 생성합니다. ( Trainer 는 이미 가져 왔습니다. – from transformers import Trainer)
이때 필요한 인자로는 모델 (`model`), 토크나이저 (`tokenizer`), 학습 설정 (`training_args`), 그리고 데이터 모듈 (`data_module`)의 인자들이 포함됩니다.

즉, trainer = Trainer(model=model, tokenizer=tokenizer, args=training_args, **data_module) 에서는 Trainer클래스가 필요한 각각의 모델, 토크나이저, 학습설정, 데이터 모률을 지정해 준 것입니다.

데이터 모듈은 data_module = make_supervised_data_module(tokenizer=tokenizer, data_args=data_args) 로 지정되어 있습니다.

2. `trainer.train()`을 호출하여 모델을 학습시킵니다. 이때 사용되는 데이터는 데이터 모듈에서 제공되며, 파인튜닝을 위해 미세 조정될 모델의 가중치를 업데이트하고 최적화합니다. 학습 알고리즘은 주어진 데이터로부터 예측과 실제 값 간의 차이를 최소화하기 위해 모델의 매개변수를 조정하며, 이를 통해 모델은 새로운 작업이나 도메인에 더 잘 적응할 수 있도록 개선됩니다.

파인튜닝은 사전 훈련된 모델을 새로운 작업이나 데이터에 맞게 조정하는 과정입니다. `Trainer`를 사용하여 모델과 데이터를 통합하고, `trainer.train()`을 호출하여 적절한 손실 함수와 최적화 알고리즘을 사용하여 모델을 학습시킵니다. 이를 통해 파인튜닝된 모델은 원래 사전 훈련된 모델보다 특정 작업에 더 적합하고 정확하게 예측할 수 있게 됩니다.

그럼 위에서 본 data_module 관련 부분을 좀더 살펴 봅니다.

data_module = make_supervised_data_module(tokenizer=tokenizer, data_args=data_args)

이 코드는 `make_supervised_data_module` 함수를 사용하여 데이터 모듈을 생성하는 부분입니다.

`make_supervised_data_module` 함수는 주어진 토크나이저와 데이터 설정을 기반으로 데이터 모듈을 생성합니다. 데이터 모듈은 학습에 사용될 데이터를 적절한 형식으로 제공하고 처리하는 역할을 합니다.

함수의 인자로는 `tokenizer`와 `data_args`가 전달됩니다. `tokenizer`는 텍스트를 토큰화하고 인코딩하는 데 사용되는 사전 훈련된 토크나이저입니다. `data_args`는 데이터 관련 설정을 포함하는 객체입니다.

`make_supervised_data_module` 함수는 주어진 인자들을 기반으로 데이터 모듈을 생성하고 반환합니다. 이 데이터 모듈은 학습에 필요한 데이터를 로드하고 전처리하여 모델에 입력으로 제공합니다. 데이터 모듈은 일반적으로 학습 데이터를 배치 단위로 제공하며, 필요한 경우 데이터 셔플링, 패딩, 마스킹 등의 전처리 작업을 수행할 수 있습니다.

따라서 `data_module` 변수에 할당된 값은 학습에 사용될 데이터 모듈 객체를 나타내며, 이를 통해 모델 학습 시 데이터의 로딩 및 처리가 이루어집니다.

그럼 `make_supervised_data_module` 함수를 살펴 봅니다. 175번째 줄에 있습니다.

def make_supervised_data_module(tokenizer: transformers.PreTrainedTokenizer, data_args) -> Dict:
    """Make dataset and collator for supervised fine-tuning."""
    train_dataset = SupervisedDataset(tokenizer=tokenizer, data_path=data_args.data_path)
    data_collator = DataCollatorForSupervisedDataset(tokenizer=tokenizer)
    return dict(train_dataset=train_dataset, eval_dataset=None, data_collator=data_collator)

이 코드는 지도 학습을 위한 데이터 모듈을 생성하는 함수인 `make_supervised_data_module`을 정의합니다.

함수는 `tokenizer`와 `data_args`라는 두 개의 매개변수를 받습니다. `tokenizer`는 텍스트를 토큰화하고 인코딩하는 데 사용되는 사전 훈련된 토크나이저입니다. `data_args`는 데이터 관련 설정을 담고 있는 객체입니다.

함수의 주요 역할은 `train_dataset`과 `data_collator`를 생성하고 이를 딕셔너리 형태로 반환하는 것입니다.

1. `train_dataset`:
– `SupervisedDataset` 클래스를 사용하여 훈련 데이터셋을 생성합니다.
– `tokenizer`와 `data_args.data_path`를 인자로 전달하여 데이터셋을 초기화합니다.
– 생성된 훈련 데이터셋은 모델의 학습에 사용됩니다.

2. `data_collator`:
– `DataCollatorForSupervisedDataset` 클래스를 사용하여 데이터를 적절하게 처리하는 `data_collator`를 생성합니다.
– `tokenizer`를 인자로 전달하여 데이터 처리를 위한 초기화 작업을 수행합니다.
– `data_collator`는 모델 학습 시 배치 단위로 데이터를 처리하고 패딩, 마스킹 등의 전처리 작업을 수행합니다.

3. 딕셔너리 반환:
– `train_dataset`, `eval_dataset`(None으로 설정), `data_collator`를 딕셔너리 형태로 반환합니다.
– 이렇게 반환된 딕셔너리는 학습 과정에서 필요한 데이터와 데이터 처리기를 담고 있습니다.

따라서 `make_supervised_data_module` 함수는 입력된 토크나이저와 데이터 설정을 기반으로 지도 학습을 위한 데이터 모듈을 생성하고 필요한 데이터와 데이터 처리기를 반환합니다. 이 모듈은 모델의 학습 단계에서 데이터의 로딩과 전처리를 담당합니다.

위에 나온 `SupervisedDataset` 클래스를 살펴 보겠습니다.

class SupervisedDataset(Dataset):
    """Dataset for supervised fine-tuning."""

    def __init__(self, data_path: str, tokenizer: transformers.PreTrainedTokenizer):
        super(SupervisedDataset, self).__init__()
        logging.warning("Loading data...")
        list_data_dict = utils.jload(data_path)

        logging.warning("Formatting inputs...")
        prompt_input, prompt_no_input = PROMPT_DICT["prompt_input"], PROMPT_DICT["prompt_no_input"]
        sources = [
            prompt_input.format_map(example) if example.get("input", "") != "" else prompt_no_input.format_map(example)
            for example in list_data_dict
        ]
        targets = [f"{example['output']}{tokenizer.eos_token}" for example in list_data_dict]

        logging.warning("Tokenizing inputs... This may take some time...")
        data_dict = preprocess(sources, targets, tokenizer)

        self.input_ids = data_dict["input_ids"]
        self.labels = data_dict["labels"]

    def __len__(self):
        return len(self.input_ids)

    def __getitem__(self, i) -> Dict[str, torch.Tensor]:
        return dict(input_ids=self.input_ids[i], labels=self.labels[i])

이 코드는 지도 학습을 위한 데이터셋을 정의하는 `SupervisedDataset` 클래스를 구현합니다.

`SupervisedDataset` 클래스는 `torch.utils.data.Dataset` 클래스를 상속받아 데이터셋을 구현하며, 지도 학습에 사용됩니다.

`list_data_dict = utils.jload(data_path)`는 주어진 `data_path`에서 데이터를 로드하여 딕셔너리 형태로 저장하는 코드입니다. 여기서 `data_path`는 데이터 파일의 경로를 가리킵니다.

위 코드의 예제에서는 `data_path`가 `https://github.com/shop2world/stanford_alpaca/blob/main/alpaca_data.json`로 지정되어 있습니다. 이는 데이터가 JSON 형식으로 저장되어 있는 공개 GitHub 저장소의 URL입니다.

따라서, `utils.jload` 함수를 사용하여 해당 URL의 JSON 파일을 로드하고 딕셔너리 형태로 변환하여 `list_data_dict` 변수에 할당합니다. 이후 코드에서는 `list_data_dict`를 활용하여 데이터를 처리하고 사용합니다.

데이터 파일의 내용과 구조는 `alpaca_data.json` 파일을 참조하여 확인할 수 있습니다.

주어진 데이터 형식은 리스트(List) 안에 딕셔너리(Dictionary) 형태로 구성되어 있습니다. 따라서 리스트의 각 요소는 딕셔너리이며, 딕셔너리는 중괄호 `{}`로 둘러싸인 키-값 쌍으로 이루어져 있습니다.

예를 들어, 주어진 데이터에서 첫 번째 요소는 다음과 같은 딕셔너리 형태입니다:

{
    "instruction": "Give three tips for staying healthy.",
    "input": "",
    "output": "1.Eat a balanced diet and make sure to include plenty of fruits and vegetables. \n2. Exercise regularly to keep your body active and strong. \n3. Get enough sleep and maintain a consistent sleep schedule."
}

위 딕셔너리는 세 개의 키(`instruction`, `input`, `output`)와 각 키에 해당하는 값으로 구성되어 있습니다. 이렇게 키-값 쌍으로 구성된 딕셔너리는 데이터를 구조화하고 관리하기 위해 사용됩니다. 예를 들어, `instruction` 키의 값은 “Give three tips for staying healthy.”로 지정되어 있습니다.

따라서 여러분은 list_data_dict = utils.jload(data_path) 에 알파카 데이타가 들어간 모습을 그리시면 됩니다.

다음 프롬프트 생성을 합니다. PROMPT_DICT 는 프롬프트가 있는것(prompt_input)과 프롬프트가 없고 인스트럭션만 있는것(prompt_no_input)이 있습니다.

이제 소스와 타겟을 지정합니다. 137번째 줄.

`sources`는 모델에 입력될 소스 문장으로 사용되며, `targets`는 모델이 예측해야 할 정답 문장으로 사용됩니다.

일반적으로 source는 모델에 입력되는 문장이며, target은 모델이 예측해야 할 정답 문장입니다.

source는 프롬프트 문장으로, 모델이 어떤 작업을 수행해야 하는지 설명하는 역할을 합니다. 프롬프트 문장은 입력으로 주어진 문제, 지시 사항, 요청 등을 포함할 수 있습니다.

target은 라벨 또는 정답이라고 할 수 있습니다. 모델의 출력이 예측한 문장이며, 학습 과정에서 이 정답 문장과 비교하여 모델의 예측 성능을 평가하고 손실(loss)을 계산하는 데 사용됩니다. 목표는 모델이 가능한 한 정확한 타겟 문장을 예측하는 것입니다.

따라서 아래 코드에서 학습을 위해 소스와 타겟으로 나누는 것은 모델이 주어진 입력에 대해 올바른 출력을 생성하도록 학습하는 데 도움이 됩니다. 모델은 소스를 입력으로 받아 타겟을 예측하는 방식으로 학습됩니다.

sources = [
            prompt_input.format_map(example) if example.get("input", "") != "" else prompt_no_input.format_map(example)
            for example in list_data_dict
        ]
        targets = [f"{example['output']}{tokenizer.eos_token}" for example in list_data_dict]

소스와 타겟은 사람에 의해 지정되어야 합니다. 소스 문장은 모델의 입력으로 사용되는 문장으로, 원하는 형식이나 요구 사항에 맞춰 사람이 작성해야 합니다. 소스 문장은 모델에게 어떤 작업을 수행하도록 지시하거나 문제를 설명하는 역할을 할 수 있습니다.

타겟 문장은 모델이 예측해야 할 정답 문장입니다. 예를 들어, 기계 번역 작업의 경우 소스 문장은 원본 문장이 되고, 타겟 문장은 해당 원본 문장의 번역 문장이 될 수 있습니다. 타겟 문장은 모델이 학습하는 동안 비교하여 예측의 정확성을 평가하는 데 사용됩니다.

따라서 소스와 타겟을 정확하고 의미 있는 문장으로 지정해야 모델이 원하는 작업을 수행하고 올바른 예측을 할 수 있습니다. 소스와 타겟은 학습 데이터를 구성할 때 사람이 직접 정의해야 하며, 이는 지도 학습 방식에서 일반적인 접근 방법입니다.

챗GPT의 경우, 소스와 타겟을 지정하는 방식은 다소 다를 수 있습니다. 챗GPT는 대화 모델로서 소스 문장은 이전 대화 내용이 될 수 있고, 타겟 문장은 다음 대화 내용의 일부 또는 전체가 될 수 있습니다.

예를 들어, 다음과 같은 대화가 있다고 가정해봅시다:

  • 사용자: “날씨가 어때?”
  • 시스템: “오늘은 맑은 날씨입니다. 온도는 25도입니다.”
  • 사용자: “옷을 어떻게 입어야 할까?”
  • 시스템: [소스] “오늘은 맑은 날씨니까 가벼운 옷을 입으세요.” [타겟] “25도에 적당한 옷차림이 좋을 거예요.”

위의 예시에서, 시스템의 응답이 소스 문장이 되고, 사용자의 다음 대화가 타겟 문장이 됩니다. 챗GPT는 이전 대화를 소스로 입력받고, 그 다음 사용자의 응답을 예측하게 됩니다.

따라서 챗GPT에서는 소스와 타겟 문장을 대화의 흐름에 따라 지정하며, 이를 통해 모델은 대화의 의미와 문맥을 이해하고 적절한 응답을 생성할 수 있도록 학습합니다.

주어진 코드는 학습 데이터를 소스와 타겟으로 나누는 과정을 나타내고 있습니다.

`list_data_dict`는 딕셔너리 형태의 데이터로 구성되어 있습니다. 각 데이터는 “instruction”, “input”, “output”과 같은 키를 가지고 있습니다.

코드의 첫 번째 줄은 `list_data_dict`의 각 데이터를 순회하면서 소스를 생성합니다. `prompt_input`과 `prompt_no_input`은 미리 정의된 템플릿입니다. 만약 데이터가 “input” 키를 가지고 있으면 `prompt_input`을 포맷팅하여 소스로 사용하고, 그렇지 않으면 `prompt_no_input`을 포맷팅하여 소스로 사용합니다. 이렇게 생성된 소스들은 `sources` 리스트에 저장됩니다.

두 번째 줄은 `list_data_dict`의 각 데이터에서 “output” 값을 가져와 `tokenizer.eos_token`과 함께 타겟으로 사용합니다. “output” 값은 모델이 학습할 정답 문장입니다. `tokenizer.eos_token`은 문장의 끝을 나타내는 특수 토큰입니다. 이렇게 생성된 타겟들은 `targets` 리스트에 저장됩니다.

이후 소스와 타겟은 모델 학습에 사용되어 모델은 주어진 소스를 입력으로 받아 타겟을 예측하도록 학습하게 됩니다. 소스와 타겟의 매칭을 통해 모델은 입력 문장에 대한 적절한 출력을 생성할 수 있도록 학습됩니다.

아래 예시 데이터를 위의 코드에 적용하면 다음과 같습니다:

{
        "instruction": "Identify the odd one out.",
        "input": "Twitter, Instagram, Telegram",
        "output": "Telegram"
    },

1. `sources`에 들어가는 값:

sources = [
            prompt_input.format_map(example) if example.get("input", "") != "" else prompt_no_input.format_map(example)
            for example in list_data_dict
        ]

– `prompt_input`: “Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:”
– `example`:
– `”instruction”: “Identify the odd one out.”`
– `”input”: “Twitter, Instagram, Telegram”`
– `”output”: “Telegram”`
– `”input”`이 비어있지 않으므로 `prompt_input`을 포맷팅하여 소스로 사용: “Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n### Instruction:\nIdentify the odd one out.\n\n### Input:\nTwitter, Instagram, Telegram\n\n### Response:”

2. `targets`에 들어가는 값:

targets = [f"{example['output']}{tokenizer.eos_token}" for example in list_data_dict]

– `example`:
– `”output”: “Telegram”`
– `”output”`에 `tokenizer.eos_token`을 추가하여 타겟으로 사용: “Telegram</s>”

`tokenizer.eos_token`은 토크나이저 객체에서 사용하는 종료 토큰(end-of-sequence token)을 나타냅니다. 이 토큰은 문장이 종료되었음을 나타내는 역할을 합니다.

일반적으로 토크나이저는 문장의 끝에 해당하는 토큰을 추가하기 위해 종료 토큰을 사용합니다. 따라서 “Telegram”이라는 출력 문장 뒤에 종료 토큰을 추가하려면 `tokenizer.eos_token`을 사용하여 토큰을 생성하고, 이를 출력 문장의 끝에 붙입니다.

따라서 `tokenizer.eos_token`의 값이 “Telegram</s>”인 이유는 “Telegram”이라는 문장의 끝에 종료 토큰을 추가하기 위해 사용되기 때문입니다. “</s>”는 일반적으로 토크나이저에서 종료 토큰을 나타내는 특정 문자열입니다.

따라서, 코드에 적용된 예시 데이터의 결과는 다음과 같습니다:

sources = [
    "Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n### Instruction:\nIdentify the odd one out.\n\n### Input:\nTwitter, Instagram, Telegram\n\n### Response:"
]
targets = [
    "Telegram"
]

이제 전처리 하는 부분을 살펴봅니다. 144번째 줄입니다.

        data_dict = preprocess(sources, targets, tokenizer)

        self.input_ids = data_dict["input_ids"]
        self.labels = data_dict["labels"]

위 코드는 데이터를 전처리하여 모델에 사용할 수 있는 형태로 변환하는 과정을 수행하는 부분입니다.

1. `preprocess` 함수에 `sources`, `targets`, 그리고 `tokenizer`를 전달하여 데이터를 전처리합니다.
2. 전처리된 결과인 `data_dict`에서 `”input_ids”`를 가져와 `self.input_ids`에 할당합니다. 이는 모델의 입력으로 사용될 문장의 토큰 인덱스입니다.
3. 마찬가지로 `data_dict`에서 `”labels”`를 가져와 `self.labels`에 할당합니다. 이는 모델이 예측해야 할 정답 문장의 토큰 인덱스입니다.

따라서, `self.input_ids`에는 전처리된 입력 문장의 토큰 인덱스가 저장되고, `self.labels`에는 전처리된 정답 문장의 토큰 인덱스가 저장됩니다. 이렇게 저장된 데이터는 모델의 학습이나 평가 단계에서 사용될 수 있습니다.

위에 주어진 예시 데이터를 기반으로 `preprocess` 함수가 실행되는 과정을 설명해드리겠습니다.

`preprocess(sources, targets, tokenizer)`는 `sources`, `targets`, 그리고 `tokenizer`를 인자로 받아서 데이터를 전처리하는 작업을 수행합니다.

`sources`는 리스트 형태로 하나의 요소로 이루어져 있습니다. 해당 요소는 프롬프트 문장과 관련된 문장으로 구성되어 있습니다. 예시 데이터에서는 “Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.\n\n### Instruction:\nIdentify the odd one out.\n\n### Input:\nTwitter, Instagram, Telegram\n\n### Response:”라는 문장이 하나의 소스로 주어졌습니다.

`targets`도 리스트 형태로 하나의 요소로 이루어져 있습니다. 해당 요소는 라벨(정답) 문장입니다. 예시 데이터에서는 “Telegram”이라는 문장이 하나의 타겟으로 주어졌습니다.

`tokenizer`는 토크나이저 객체로, 텍스트를 토큰으로 분할하여 인덱스로 변환하는 역할을 합니다.

`preprocess` 함수는 다음과 같은 작업을 수행합니다.
1. `tokenizer.encode_plus`를 사용하여 소스 문장과 타겟 문장을 토큰화하고, 인코딩하여 토큰 인덱스로 변환합니다.
2. 토큰 인덱스를 포함한 입력 문장의 `input_ids`와 해당 문장에 대한 라벨을 나타내는 `labels`를 반환합니다.

따라서 `data_dict`는 다음과 같은 형태의 딕셔너리입니다.

{
    "input_ids": [인코딩된 소스 문장의 토큰 인덱스],
    "labels": [인코딩된 타겟 문장의 토큰 인덱스]
}

`self.input_ids`는 `data_dict[“input_ids”]`를 할당받은 것으로, 인코딩된 소스 문장의 토큰 인덱스를 저장하는 속성입니다.
`self.labels`는 `data_dict[“labels”]`를 할당받은 것으로, 인코딩된 타겟 문장의 토큰 인덱스를 저장하는 속성입니다.

이렇게 `preprocess` 함수를 통해 데이터가 전처리되고, `self.input_ids`와 `self.labels`에 저장되면 해당 데이터는 모델의 학습 및 평가 과정에서 사용될 수 있습니다.

앞서 제공한 예시 데이터에 대해 `data_dict`의 형태로 변환하면 다음과 같습니다:

data_dict = {
    "input_ids": [[101, 1283, 102]],  # 인코딩된 소스 문장의 토큰 인덱스
    "labels": [[1283, 102]]  # 인코딩된 타겟 문장의 토큰 인덱스
}

위 예시에서 `”input_ids”`는 인코딩된 소스 문장의 토큰 인덱스를 나타내며, `[[101, 1283, 102]]`와 같이 2차원 리스트 형태로 표현되었습니다. `[101, 1283, 102]`는 토크나이저를 통해 소스 문장이 인코딩되어 얻어진 토큰 인덱스 시퀀스입니다.

`”labels”`는 인코딩된 타겟 문장의 토큰 인덱스를 나타내며, `[[1283, 102]]`와 같이 2차원 리스트 형태로 표현되었습니다. `[1283, 102]`는 토크나이저를 통해 타겟 문장이 인코딩되어 얻어진 토큰 인덱스 시퀀스입니다.

각각의 인덱스는 단어 또는 특정 토큰을 나타내며, 해당 토큰은 토크나이저의 어휘 사전에서 확인할 수 있습니다. `[101]`, `[102]`는 문장의 시작과 종료를 나타내는 특수 토큰을 의미합니다. `[1283]`은 예시 데이터에서 “Telegram”이라는 토큰에 해당하는 인덱스입니다.

이제 전처리 함수를 보겠습니다. 112번째 줄입니다.

def preprocess(
    sources: Sequence[str],
    targets: Sequence[str],
    tokenizer: transformers.PreTrainedTokenizer,
) -> Dict:
    """Preprocess the data by tokenizing."""
    examples = [s + t for s, t in zip(sources, targets)]
    examples_tokenized, sources_tokenized = [_tokenize_fn(strings, tokenizer) for strings in (examples, sources)]
    input_ids = examples_tokenized["input_ids"]
    labels = copy.deepcopy(input_ids)
    for label, source_len in zip(labels, sources_tokenized["input_ids_lens"]):
        label[:source_len] = IGNORE_INDEX
    return dict(input_ids=input_ids, labels=labels)

위의 코드는 데이터 전처리를 수행하는 함수인 `preprocess`를 정의하는 부분입니다. 이 함수는 소스 문장과 타겟 문장을 받아서 토크나이징하는 과정을 거쳐 데이터를 전처리합니다.

– `sources`: 소스 문장의 시퀀스로, 작업 설명이 포함될 수 있습니다.
– `targets`: 타겟 문장의 시퀀스로, 모델이 학습하고 예측해야 할 정답이 됩니다.
– `tokenizer`: 데이터를 토큰화하기 위해 사용되는 토크나이저입니다.

함수의 주요 동작은 다음과 같습니다:

1. 소스 문장과 타겟 문장을 합쳐서 하나의 문장으로 만듭니다.
2. 합쳐진 문장과 소스 문장을 토큰화하여 토큰 인덱스를 얻습니다. (`_tokenize_fn` 함수를 사용합니다.)
3. 토큰화된 합쳐진 문장의 인덱스를 `input_ids`로 저장합니다.
4. `labels`는 `input_ids`를 복사한 후, 소스 문장의 길이에 해당하는 부분을 `IGNORE_INDEX`로 설정합니다. 이렇게 하면 모델이 소스 문장을 무시하고 타겟 문장만 예측하도록 할 수 있습니다.
5. `input_ids`와 `labels`를 딕셔너리 형태로 반환합니다.

따라서 이 함수는 텍스트 데이터를 토큰화하여 모델이 학습할 수 있는 형태로 변환하는 역할을 수행합니다.

위의 1번을 좀더 보면 `examples = [s + t for s, t in zip(sources, targets)]` 코드는 소스 문장과 타겟 문장을 합쳐서 하나의 문장으로 만드는 과정을 수행합니다. 이렇게 소스 문장과 타겟 문장을 합치는 이유는 다음과 같습니다:

1. 입력 형식 표준화: 모델에 입력으로 제공되는 데이터의 형식을 표준화하기 위해 소스 문장과 타겟 문장을 하나의 문장으로 결합합니다. 이를 통해 모델에게 일관된 입력 형식을 제공하여 학습과 추론을 용이하게 만듭니다.

2. 문맥 전달: 소스 문장은 모델에게 작업에 필요한 문맥과 지시사항을 전달하는 역할을 합니다. 타겟 문장은 모델이 예측해야 하는 정답 또는 출력을 나타냅니다. 소스 문장과 타겟 문장을 함께 하나의 문장으로 만들면, 모델은 작업에 필요한 정보와 정답을 하나의 입력으로 받아들이면서 문맥적인 관계를 파악할 수 있습니다.

3. 토크나이저 효율화: 소스 문장과 타겟 문장을 따로 입력하는 것보다 하나의 문장으로 합치면, 토크나이저는 한 번에 전체 문장을 처리할 수 있습니다. 이는 처리 속도를 향상시키고 메모리 사용량을 줄일 수 있는 장점을 가지며, 전체 문장을 기반으로 한꺼번에 토큰화된 표현을 생성할 수 있습니다.

따라서, `examples = [s + t for s, t in zip(sources, targets)]` 코드는 위와 같은 이유로 소스 문장과 타겟 문장을 하나의 문장으로 합치는 작업을 수행합니다.

또examples_tokenized, sources_tokenized = [_tokenize_fn(strings, tokenizer) for strings in (examples, sources)] 에서 `examples_tokenized`과 `sources_tokenized`는 각각 합쳐진 문장과 소스 문장을 토큰화하여 토큰 인덱스를 얻는 과정을 수행합니다. 이렇게 하는 이유는 다음과 같습니다:

1. 일관된 토큰화: 소스 문장과 합쳐진 문장은 동일한 토큰화 방법을 적용해야 합니다. 토큰화는 텍스트를 작은 단위로 분할하는 작업으로, 모델이 텍스트를 이해하고 처리할 수 있도록 해줍니다. 합쳐진 문장과 소스 문장을 모두 토큰화하여 동일한 토큰 인덱스를 얻으면, 모델은 동일한 토큰화 방법을 적용하여 일관된 방식으로 텍스트를 처리할 수 있습니다.

2. 입력과 출력 매핑: 합쳐진 문장은 모델의 입력으로 사용되고, 소스 문장은 모델의 출력과 비교하여 손실을 계산하는 데 사용됩니다. 따라서, 합쳐진 문장과 소스 문장을 토큰화하여 각각의 토큰 인덱스를 얻으면, 입력과 출력 사이의 매핑을 수행할 수 있습니다. 이를 통해 모델은 학습할 때 입력과 출력을 연관시키고, 예측할 때는 입력에 기반하여 적절한 출력을 생성할 수 있습니다.

3. 효율적인 연산: 토큰 인덱스를 사용하여 모델의 입력과 출력을 처리하는 것은 숫자로 이루어진 배열에 대한 연산이므로 효율적입니다. 모델은 토큰 인덱스에 대한 처리를 빠르게 수행할 수 있으며, GPU를 활용한 병렬 처리도 가능합니다. 또한, 토큰 인덱스를 사용하면 메모리 사용량을 줄일 수 있습니다.

따라서, 합쳐진 문장과 소스 문장을 각각 토큰화하여 토큰 인덱스를 얻는 것은 일관성, 입력-출력 매핑, 효율성 등의 이점을 가지기 위한 작업입니다.

혹시 왜 합쳐진 문장과 타켓 문장으로 토큰 인덱스를 만들지 않는가? 라는 의문을 가지신 분을 위해 답변 드리면 타겟 문장은 모델의 출력과 비교하여 손실을 계산하는 데 사용되는 것이기 때문에, 따로 토큰 인덱스를 만들 필요가 없습니다. 합쳐진 문장은 모델의 입력으로 사용되고, 소스 문장과 타겟 문장이 함께 토큰화되어 입력과 출력을 매핑하는 역할을 수행합니다.

모델은 합쳐진 문장을 입력으로 받아 토큰 인덱스로 변환한 후, 소스 문장과 함께 처리합니다. 그리고 모델의 출력과 타겟 문장을 비교하여 손실을 계산합니다. 타겟 문장은 이미 소스 문장과 함께 토큰화되어 있으며, 합쳐진 문장을 통해 입력과 출력을 연결하는 역할을 수행합니다.

따라서, 합쳐진 문장과 타겟 문장은 서로 다른 목적을 가지고 있으며, 각각의 역할에 맞게 토큰 인덱스를 생성하거나 사용합니다. 합쳐진 문장은 입력으로 사용되는 토큰 인덱스를 생성하고, 타겟 문장은 출력과의 비교를 위해 사용됩니다.

이제 편의상 작업한 전체 문장과 소스문장에서 정답 문장의 길이만 추려내는 부분을 보겠습니다. 방법은 전체 문장의 길이에서 소스문장의 길이만 제거하면 정답(타켓)문장의 길이가 나오게 됩니다 . 즉 전체 문장의 길이에서 소스 문장의 길이를 빼면 정답 문장(타겟)의 길이가 나오는데, 이는 소스 문장의 토큰을 `IGNORE_INDEX`로 설정하여 해당 부분을 모델이 무시하도록 하기 위함입니다. 따라서, `labels` 리스트의 처음부터 소스 문장의 길이까지의 범위를 `IGNORE_INDEX`로 설정함으로써 해당 부분을 모델이 무시하도록 합니다. 이를 통해 모델은 입력된 소스 문장을 바탕으로 타겟 문장을 예측하게 됩니다. 코드는 다음과 같습니다.

for label, source_len in zip(labels, sources_tokenized["input_ids_lens"]):
        label[:source_len] = IGNORE_INDEX

위의 코드는 `labels` 리스트의 일부를 `IGNORE_INDEX`로 설정하는 역할을 합니다. 구체적인 설명은 다음과 같습니다:

1. `labels`와 `sources_tokenized[“input_ids_lens”]`를 동시에 순회하면서 반복문을 실행합니다.
– `labels`는 이전 단계에서 생성된 리스트로, 토큰 인덱스를 저장하고 있습니다.
– `sources_tokenized[“input_ids_lens”]`는 이전 단계에서 생성된 딕셔너리로, 소스 문장의 패딩을 제외한 실제 토큰 개수를 나타내는 리스트입니다.

2. 각 반복 단계에서 `label`과 `source_len`을 가져옵니다.
– `label`은 현재 반복 단계에서 처리 중인 `labels` 리스트의 요소입니다.
– `source_len`은 현재 반복 단계에서 처리 중인 소스 문장의 토큰 개수입니다.

3. `label`의 처음부터 `source_len`까지의 범위를 `IGNORE_INDEX`로 설정합니다.
– `IGNORE_INDEX`는 특정 토큰을 모델에 무시하도록 지정하는 값입니다.
– 이 코드는 소스 문장의 토큰에 해당하는 부분을 `IGNORE_INDEX`로 설정하여 모델이 해당 부분을 학습하지 않도록 합니다.

이 과정은 학습 데이터에서 소스 문장에 해당하는 토큰을 무시하고자 할 때 사용됩니다. 예를 들어, 기계 번역 모델의 경우 입력 문장과 출력 문장을 소스와 타겟으로 지정하는데, 소스 문장을 번역하지 않고 타겟 문장만을 생성하도록 하기 위해 이 코드가 사용될 수 있습니다.

위의

이제 . 합쳐진 문장을 토큰화 한것과 (examples_tokenized)과 소스 문장을 토큰화한것(sources_tokenized)으로 토큰 인덱스를 얻는 `_tokenize_fn` 함수를 봅니다.

def _tokenize_fn(strings: Sequence[str], tokenizer: transformers.PreTrainedTokenizer) -> Dict:
    """Tokenize a list of strings."""
    tokenized_list = [
        tokenizer(
            text,
            return_tensors="pt",
            padding="longest",
            max_length=tokenizer.model_max_length,
            truncation=True,
        )
        for text in strings
    ]
    input_ids = labels = [tokenized.input_ids[0] for tokenized in tokenized_list]
    input_ids_lens = labels_lens = [
        tokenized.input_ids.ne(tokenizer.pad_token_id).sum().item() for tokenized in tokenized_list
    ]
    return dict(
        input_ids=input_ids,
        labels=labels,
        input_ids_lens=input_ids_lens,
        labels_lens=labels_lens,
    )

위의 코드는 주어진 문자열 목록을 토큰화하는 함수인 `_tokenize_fn`을 정의합니다. 이 함수는 입력으로 문자열 목록(`strings`)과 토크나이저(`tokenizer`)를 받습니다. 아래는 코드의 구체적인 설명입니다:

1. `tokenized_list` 리스트 생성:
– 주어진 각 문자열을 토크나이저를 사용하여 토큰화합니다.
– `tokenizer`의 인자를 설정하여 토큰화된 결과를 반환받습니다.
– 반환된 결과는 `tokenized_list` 리스트에 저장됩니다.

2. `input_ids`와 `labels` 생성:
– `tokenized_list`에 있는 각 토큰화된 결과(`tokenized`)의 `input_ids`를 추출합니다.
– 추출한 `input_ids`는 `input_ids`와 `labels` 리스트에 저장됩니다.

3. `input_ids_lens`와 `labels_lens` 생성:
– `tokenized_list`에 있는 각 토큰화된 결과(`tokenized`)의 `input_ids`에서 패딩 토큰이 아닌 토큰의 개수를 계산합니다.
– 계산한 개수는 `input_ids_lens`와 `labels_lens` 리스트에 저장됩니다.

4. 결과 딕셔너리 반환:
– 생성된 `input_ids`, `labels`, `input_ids_lens`, `labels_lens` 리스트를 딕셔너리 형태로 반환합니다.

이 함수는 문자열 목록을 토큰화하여 토큰 인덱스(`input_ids`), 패딩 토큰이 아닌 토큰의 개수(`input_ids_lens`), 그리고 `labels`에 동일한 값을 할당합니다. 이러한 토큰 인덱스와 길이 정보는 데이터셋 구성 및 모델 학습에 활용될 수 있습니다.

학승용 데이터 전처리 하는 부분을 봅니다. 144번째 줄 입니다.

data_dict = preprocess(sources, targets, tokenizer)

위의 코드에서 `preprocess` 함수가 호출되어 데이터를 전처리합니다. 이 부분은 학습용 데이터를 모델에 입력할 수 있는 형태로 변환하는 과정을 담당하며, 중요한 역할을 수행합니다.

`preprocess` 함수는 `sources(프롬프트)`, `targets(정답)`, 그리고 `tokenizer`를 인자로 받습니다. 이 함수는 다음과 같은 작업을 수행합니다.

1. `sources`와 `targets`를 결합하여 `examples` 리스트를 생성합니다. 이는 각 예시의 소스 문장과 타겟 문장을 하나의 문자열로 결합하는 역할을 합니다. 이렇게 하나로 합쳐진 예시 데이터는 모델에 입력될 것입니다.

2. `examples`와 `sources`를 토큰화하여 토큰 인덱스를 얻습니다. `tokenizer`를 사용하여 문자열을 토큰화하고, 패딩과 잘라내기 등의 토큰화 옵션을 설정합니다. 이렇게 토큰화된 결과는 모델에 입력될 입력 문장의 토큰 인덱스로 사용됩니다.

3. 토큰화된 결과에서 입력 문장의 토큰 인덱스(`input_ids`)와 해당 인덱스의 길이(`input_ids_lens`)를 추출합니다. 이때, `input_ids`와 `input_ids_lens`는 각각 소스 문장과 입력 문장에 대한 정보를 담고 있습니다.

4. `labels` 변수를 `input_ids`로 복사합니다. 이는 모델이 생성해야 할 타겟 문장에 대한 토큰 인덱스를 나타냅니다.

5. `labels` 리스트의 일부를 `IGNORE_INDEX` 값으로 설정하여 소스 문장에 해당하는 부분은 모델이 무시하도록 합니다. 이는 소스 문장은 입력으로 주어지지만, 타겟 문장을 생성할 때는 참고만 되고 직접적으로 포함되지 않아야 하는 경우에 유용합니다.

6. 최종적으로 `input_ids(전체 문장)`, `labels(정답)`, `input_ids_lens`, `labels_lens`를 포함한 딕셔너리를 반환합니다. 이 딕셔너리는 데이터를 모델에 입력하기 위해 필요한 정보를 담고 있습니다.

전처리 과정은 데이터를 모델에 맞는 형태로 변환하여 학습이나 예측에 사용될 수 있도록 합니다. 이를 통해 모델은 토큰 인덱스 형태의 입력과 타겟을 사용하여 학습하고 예측할 수 있게 됩니다. 따라서 전처리 과정은 학습 과정에서 매우 중요하며, 데이터를 올바르게 변환하는 데 필수적입니다.

 

클래스의 주요 메서드와 역할은 다음과 같습니다:

1. `__init__(self, data_path: str, tokenizer: transformers.PreTrainedTokenizer)`:
– 클래스의 생성자 메서드로, 데이터 경로와 토크나이저를 인자로 받습니다.
– 데이터 경로에서 데이터를 로드하여 사전 처리합니다.
– `data_path`로부터 데이터를 로드하고, `tokenizer`를 사용하여 입력과 타겟을 토큰화 및 형식화합니다.
– 최종적으로 토큰화된 입력과 타겟을 `data_dict`에 저장합니다.
– `self.input_ids`와 `self.labels`에 각각 입력과 타겟의 토큰 ID를 저장합니다.

2. `__len__(self)`:
– 데이터셋의 총 샘플 개수를 반환합니다.
– `self.input_ids`의 길이를 반환하므로, 입력 데이터의 샘플 수를 반환합니다.

3. `__getitem__(self, i) -> Dict[str, torch.Tensor]`:
– 주어진 인덱스 `i`에 해당하는 데이터 샘플을 반환합니다.
– `self.input_ids[i]`와 `self.labels[i]`를 이용해 입력과 타겟을 딕셔너리 형태로 반환합니다.
– 반환된 딕셔너리는 `input_ids`와 `labels` 필드를 가지며, 각각 입력과 타겟의 토큰 ID를 담고 있습니다.

`SupervisedDataset` 클래스는 데이터셋의 초기화, 길이 조회, 샘플 반환 등의 기능을 제공하여 모델의 학습에 활용됩니다. 이를 통해 데이터를 효율적으로 로드하고 전처리된 형태로 모델에 입력할 수 있습니다.

학습용 데이터를 만드는 부분을 보겠습니다.

 

def make_supervised_data_module(tokenizer: transformers.PreTrainedTokenizer, data_args) -> Dict:
    """Make dataset and collator for supervised fine-tuning."""
    train_dataset = SupervisedDataset(tokenizer=tokenizer, data_path=data_args.data_path)
    data_collator = DataCollatorForSupervisedDataset(tokenizer=tokenizer)
    return dict(train_dataset=train_dataset, eval_dataset=None, data_collator=data_collator)

위의 코드는 지도 학습을 위한 데이터 모듈을 생성하는 함수인 `make_supervised_data_module`을 정의합니다. 이 함수는 `tokenizer`와 `data_args`라는 인자를 받습니다.

함수의 주요 작업은 다음과 같습니다:

1. `train_dataset` 변수에 `SupervisedDataset`을 생성합니다. 이는 지도 학습용 데이터셋을 나타내는 객체입니다. `tokenizer`와 `data_args.data_path`를 인자로 전달하여 데이터셋을 초기화합니다. 데이터셋은 학습용 데이터를 로드하고, 토큰화하며, 필요한 전처리를 수행하는 역할을 합니다.

2. `data_collator` 변수에 `DataCollatorForSupervisedDataset`을 생성합니다. 이는 데이터셋에서 배치를 구성하는 데 사용되는 데이터 콜레이터(collator) 객체입니다. `tokenizer`를 인자로 전달하여 콜레이터를 초기화합니다. 콜레이터는 배치 내의 데이터를 텐서로 변환하고 패딩 및 마스킹을 수행하여 모델 학습에 적합한 형태로 구성하는 역할을 합니다.

3. `train_dataset`, `eval_dataset`, `data_collator`를 포함한 딕셔너리를 반환합니다. 이 딕셔너리는 학습에 필요한 데이터셋과 데이터 콜레이터를 포함하고 있습니다. 여기서 `eval_dataset`은 평가용 데이터셋이지만, 이 함수에서는 `None`으로 설정되어 평가용 데이터셋은 제공되지 않음을 의미합니다.

이렇게 생성된 데이터 모듈은 모델의 지도 학습을 위해 사용됩니다. 학습 데이터셋은 모델의 학습에 사용되며, 데이터 콜레이터는 학습 중에 데이터를 배치로 구성하여 모델이 효율적으로 학습할 수 있도록 도와줍니다. 이 데이터 모듈을 사용하면 모델 학습에 필요한 데이터 처리 부분을 추상화하고, 쉽게 재사용하고 관리할 수 있습니다.

이렇게 해서 학습용 데이터 준비를 하고

 

data_module = make_supervised_data_module(tokenizer=tokenizer, data_args=data_args)

학습을 합니다.

trainer = Trainer(model=model, tokenizer=tokenizer, args=training_args, **data_module)
    trainer.train()

비트겐슈타인의 언어 철학과 기계 학습에서의 언어 모델 개발의 교차점

서문:
언어는 인간 사회에서 중요한 역할을 담당하며, 언어의 의미와 사용에 대한 이해는 철학적인 탐구와 기계 학습 분야의 연구의 중심 주제 중 하나입니다. 이 글에서는 비트겐슈타인의 언어 철학과 기계 학습 분야에서의 언어 모델 개발의 교차점에 대해 알아보고자 합니다.

본론:
1. 언어와 의미의 복잡성:
비트겐슈타인은 언어의 사용과 문맥의 중요성을 강조했습니다. 언어는 단순한 단어의 조합 이상으로 상호작용과 문맥에 의해 의미가 형성됩니다. 기계 학습 분야에서의 언어 모델 개발도 이러한 복잡성을 인식하고 문맥을 이해하며 의미를 추론하는 모델을 구축합니다.

예를 들어, 단어 “강아지”라는 단어를 살펴보겠습니다. 이 단어는 언어 체계에서 특정한 동물을 가리키는 의미를 갖고 있습니다. 그러나 이 단어의 의미는 단순히 사전적 정의로만 설명할 수 있는 것은 아닙니다.

비트겐슈타인은 “강아지”라는 단어의 의미는 그 단어를 사용하는 맥락과 상호작용에 따라 다양하게 변할 수 있다고 주장했습니다. 예를 들어, 어린 아이들이 “강아지”라는 단어를 사용할 때는 귀여움이나 친근한 감정을 나타낼 수 있습니다. 반면에 수의사는 “강아지”라는 단어를 사용할 때는 의료적인 의미나 진단과 관련된 의미를 갖게 됩니다.

기계 학습 분야에서도 언어 모델은 이러한 문맥과 상호작용을 이해하고 처리하기 위해 학습됩니다. 대규모의 텍스트 데이터를 학습하여 언어의 다양한 문맥과 의미를 파악하고 추론할 수 있는 능력을 갖추는 것이 목표입니다. 예를 들어, 언어 모델은 “강아지”라는 단어가 특정 문맥에서는 애완동물을 가리키는 의미로 사용되는지, 또 다른 문맥에서는 불안정한 상황을 묘사하는 의미로 사용되는지를 학습합니다.

이렇게 언어 모델은 실제 데이터를 기반으로 학습하여 언어의 복잡성과 다의성을 이해하고 모델링합니다. 이는 비트겐슈타인의 언어 철학에서 강조하는 언어 사용의 실제 상황과 상호작용에서 의미가 형성된다는 관점과 유사합니다. 언어 모델은 데이터를 통해 언어의 다양성과 복잡성을 학습하고 이를 기반으로 언어 이해와 생성 작업을 수행하는 것입니다.

2. 데이터 기반 학습:
비트겐슈타인의 언어 철학은 명시적인 규칙과 원리보다는 실제 언어 사용의 데이터를 중시합니다. 기계 학습 분야에서의 언어 모델 개발도 데이터 기반 학습에 주력합니다. 대규모 데이터셋을 활용하여 모델을 학습시키고, 통계적인 패턴과 관계를 학습하여 언어 이해와 생성을 수행합니다.

예를 들어, 기계 학습 분야에서 언어 모델을 개발하기 위해 많은 양의 텍스트 데이터가 필요합니다. 이 데이터는 다양한 출처에서 수집될 수 있으며, 웹 문서, 책, 뉴스 기사, 소셜 미디어 등 다양한 형태의 텍스트를 포함할 수 있습니다.

비트겐슈타인의 언어 철학과 유사하게, 언어 모델 개발에서도 실제 언어 사용의 데이터를 중요시합니다. 이 데이터를 통해 모델은 언어의 다양한 측면과 특징을 학습하고 언어의 복잡성을 이해합니다. 예를 들어, 대량의 영어 문장 데이터를 사용하여 언어 모델을 학습시킬 경우, 문법적인 구조, 단어의 의미, 문맥에 따른 단어 선택 등의 패턴을 통계적으로 파악하고 내재화합니다.

학습 데이터는 모델의 성능과 품질에 직접적인 영향을 미칩니다. 데이터의 다양성과 품질이 높을수록 모델은 더 정확하고 유용한 언어 이해와 생성을 수행할 수 있습니다. 따라서 언어 모델 개발자는 데이터를 신중하게 선택하고 전처리하여 학습에 활용합니다. 또한, 실시간으로 생성된 데이터를 모델에 피드백으로 활용하거나 추가 데이터를 수집하여 모델을 지속적으로 개선하는 것도 중요한 과정입니다.

이러한 데이터 기반 학습 접근 방식은 비트겐슈타인의 언어 철학에서 강조하는 실제 언어 사용의 데이터 중심적인 관점과 일치합니다. 언어 모델은 데이터를 통해 언어의 특성과 패턴을 파악하고 이를 학습하여 언어 이해와 생성을 수행하는 것입니다.

3. 의미의 다양성과 다의성:
비트겐슈타인은 의미의 다양성과 다의성을 강조합니다. 한 단어나 문장은 다양한 의미를 내포하고 다양한 문맥에서 다른 해석을 받을 수 있습니다. 기계 학습 분야에서의 언어 모델 개발도 이러한 다양성과 다의성을 인식하고 처리할 수 있는 유연한 모델을 구축합니다. 예를 들어, Word2Vec과 같은 워드 임베딩 기법을 통해 단어의 의미를 벡터 공간에 표현하고, 문맥을 고려한 언어 생성 모델을 개발합니다.

결론:
비트겐슈타인의 언어 철학과 기계 학습 분야에서의 언어 모델 개발은 언어와 의미에 대한 이해를 공통적으로 추구합니다.

스탠포드 알파카(Alpaca) 코드분석 – 누구나 챗GPT 3.5성능의 모델을 만들 수 있다. 1 generate_instruction.py

알파카 모델은 메타에서 공개한 라마 7B 모델을 가져와 Instruction-following 작업을 수행하기 위해 추가적인 훈련(파인튜닝)을 받은 모델입니다.
주의 * Alpaca는 학술 연구만을 목적으로 하며 어떠한 상업적 사용도 금지되어 있음을 강조합니다 . (참고 알파카 블로그)

아래 그림은 Alpaca 모델을 얻은 방법을 보여줍니다. 데이터를 위해 우리는 self-instruct seed set 에서 175개의 사람이 작성한 명령-출력 쌍으로 시작했습니다 . (아래 글에도 설명이 나옵니다.) 그런 다음 text-davinci-003에 컨텍스트 내 예제로 시드 세트를 사용하여 추가 지침을 생성하도록 요청했습니다. 생성 파이프라인( GitHub 의 세부 정보 참조)을 단순화하여 자체 지시 방법을 개선 하고 비용을 크게 줄였습니다. 우리의 데이터 생성 프로세스는 52,000개의 고유한 명령과 해당 출력을 생성하며 OpenAI API를 사용하여 $500 미만의 비용이 듭니다.

즉, 다음의 순서입니다.

1.스탠포드 알파카(Alpaca) 코드의  seed_tasks.jsonl 의 175개의 데이터가 있습니다.코드에서 `seed_tasks`는 사전 훈련된 모델을 Fine-tuning하기 위한 초기 데이터로 사용되는 작업의 집합을 나타냅니다.

`seed_tasks`는 사전에 수집된 작업 데이터를 포함하고 있으며, 각 작업은 “시작점” 또는 “초기 상태”로 사용됩니다. 이러한 작업은 사전 훈련된 모델을 특정 작업에 맞게 Fine-tuning하는 데 사용됩니다. 각 작업에는 인간이 작성한 명령문(instruction)과 해당 작업에 대한 입력(input) 및 출력(output) 데이터가 포함될 수 있습니다.

일반적으로 `seed_tasks`는 작은 규모의 초기 데이터셋으로 시작하여 Fine-tuning 과정을 수행합니다. 이러한 초기 데이터는 모델에 필요한 작업에 대한 예시를 제공하고 모델이 해당 작업을 이해하고 수행하는 데 도움을 줍니다. Fine-tuning을 통해 모델은 이러한 초기 데이터에 대한 학습을 거쳐 작업에 더 적합하고 정확한 예측을 수행할 수 있게 됩니다.

따라서 `seed_tasks`는 Fine-tuning 작업에 사용되는 초기 데이터셋을 의미합니다.

2.이것을 아래 그림의 Text-davinci-003을 이용합니다.

3. 그렇게 해서 총 5만2000개를 생성 합니다. 이것을 가지고 아래 그림의 LLaMA 모델을 이용해 미세 조정합니다.초기 실행에서 7B LLaMA 모델을 미세 조정하는 데 8개의 80GB A100에서 3시간이 걸렸으며 대부분의 클라우드 컴퓨팅 공급자에서 100달러 미만입니다. 교육 효율성을 개선하여 비용을 더욱 절감할 수 있습니다.

그렇게 해서 그림 처럼 알파카모델이 탄생되었다는 것입니다.

 

 

그럼 이렇게 52000개의 데이터를 만들어 내는 Self -Instruct 과정의 핵심 코드를 보겠습니다.  Self -Instruct  는 만약 사람이 52000개의 데이터를 만들어 내려면 얼마나 힘들겠습니까.  그래서 언어모델 자체가 생성한 문장을 학습할 수 있게 하는 것이죠.

그래서 Self Instruct는 스스로(Self) 지시를(Instruct) 언어모델을 통해서 만들어 내는 것입니다.

과정을 안내하는 논문의 그림입니다. 논문의 2페이지에 있습니다.

그림 설명입니다. 175개의 seed task입니다. seed_tasks.jsonl  를 보시면 설명처럼 타스크는 각각 하나의 지시와 하나의 인스탄스를 가집니다.(인스탄스는 인풋과 아웃풋을 가지고 있습니다.) 그리고 분류문제인지 여부에 대한 데이터를 가집니다.

이것을 Task Pool에 넣어 LM(언어모델)이 문장을 생성합니다. (Step 1)그리고 분류문제 인지 여부를 판단해 (Step 2) 타스크가 분류가 되는 경우 Class Label 이 있는 타스크, 아니면 그냥  타스크(인스트럭션과 인풋, 아웃풋만 있는)로 나눕니다. (Step 3)

그리고 나서 Step 4에서 필터링을 통해 적절하지 않은 문장을 걸러내도록 합니다.

그리고 나서 다시 필터링된 걸러진 문장을 또다시 Task Pool에 입력해서 위의 과정을 다시 반복합니다. 이렇게 해서 계속해서 Task Pool을 증가 시킬 수 있게 됩니다.

이때 Task Pool을 증가시키는 프롬프트 템플릿에 대한 논문의 내용을 봅니다.
논문 14페이지의 하단을 보세요. Task 9 가 비어 있습니다. 아래 새로운 인스트럭션을 만드는 프롬프트에 의해 새로운 Taks 9 가 만들어 지는것입니다.

그리고 결과물은 다양한 분야를 커버하는 것을 볼 수 있습니다.

https://github.com/shop2world/stanford_alpaca/blob/main/assets/parse_analysis.png

이제 위의 논문을 통해 설명된 Instruction following 을 코드로 구현한 LLaMA model 코드를 봅니다.

이것은 코드의 prompt.txtseed_tasks.josonl 을 이용해서 데이터 생성 구현을 위해 인스트럭션을 생성하는 generate_instruction.py 의 generate_instruction_following_data 함수를 살펴보겠습니다.

이 코드는 generate_instruction_following_data라는 함수를 정의하는 부분입니다. 함수는 다양한 매개변수를 받아들이고, 주어진 매개변수에 따라 작업을 수행하여 지시를 따르는 데이터를 생성합니다.

이 함수의 매개변수는 다음과 같습니다:

  • output_dir: 생성된 데이터를 저장할 디렉토리 경로입니다. 기본값은 현재 디렉토리("./")입니다.
  • seed_tasks_path: 시드(seed) 지시사항 작업이 포함된 JSONL 파일의 경로입니다. 기본값은 ./seed_tasks.jsonl입니다.
  • num_instructions_to_generate: 생성할 지시사항의 개수입니다. 기본값은 100입니다.
  • model_name: 사용할 GPT-3.5 모델의 이름입니다. 기본값은 "text-davinci-003"입니다.
  • num_prompt_instructions: 각 생성 요청에 포함할 프롬프트(prompt) 지시사항의 개수입니다. 기본값은 3입니다.
  • request_batch_size: 병렬로 수행할 요청의 개수입니다. 기본값은 5입니다.
  • temperature: 모델 출력의 무작위성을 조절하는 온도 매개변수입니다. 기본값은 1.0입니다.
  • top_p: 다양한 출력을 생성하기 위한 top-p(nucleus) 샘플링 매개변수입니다. 기본값은 1.0으로, 모든 가능성을 고려합니다.
  • num_cpus: 병렬화에 사용할 CPU 코어의 개수입니다. 기본값은 16입니다.

함수는 다음과 같은 단계로 진행됩니다:

1 먼저, 위에 그림에서 설명한 seed_tasks 데이터를 가져옵니다.

seed_tasks = [json.loads(l) for l in open(seed_tasks_path, “r”)]

이 코드는 seed_tasks_path로 지정된 JSONL 파일을 열어서 데이터를 로드합니다.

2 seed_tasks 데이터에서 관련 정보를 추출합니다. 그리고 seed_instruction_data 변수에 매핑된 seed_tasks데이터를 넣어 줍니다. (123 번째 줄)

seed_instruction_data = [
        {"instruction": t["instruction"], "input": t["instances"][0]["input"], "output": t["instances"][0]["output"]}
        for t in seed_tasks
    ]

이 코드는 seed_tasks 데이터에서 지시(instruction), 입력(input), 출력(output) 정보를 추출하여 seed_instruction_data에 저장합니다. 아래 그림의  1 instruction , 그리고 1 instance (입력, 출력) 입니다.

3 로드된 인간이 작성한 시드 지시사항의 개수를 출력합니다.

print(f”Loaded {len(seed_instruction_data)} human-written seed instructions”)

4 지시문이 생성될 출력 디렉토리를 생성합니다.

os.makedirs(output_dir, exist_ok=True)

5 요청을 생성하기 위한 인덱스를 초기화합니다.

request_idx = 0

6 기계 생성 지시사항(machine_instruction_data)을 로드합니다.

machine_instruction_data = []
    if os.path.exists(os.path.join(output_dir, "regen.json")):
        machine_instruction_data = utils.jload(os.path.join(output_dir, "regen.json"))
        print(f"Loaded {len(machine_instruction_data)} machine-generated instructions")

이 코드는 기계 생성 지시사항(machine_instruction_data)이 이미 생성되어 있는 경우 해당 파일을 로드합니다. regen.json은 처음에는 존재하지 않지만 나중에 생성되는 파일이 되는 것이죠. 즉, 기계 생성 지시사항(machine_instruction_data)이 있으면 가져오고 없으면 안가져옵니다.

7 Rouge 스코어링을 위한 스코어러 객체를 생성합니다. 생성한 문장의 유사도를 체크합니다.

scorer = rouge_scorer.RougeScorer([“rougeL”], use_stemmer=False)

8 새로운 지시사항을 생성합니다. 위의 num_instructions_to_generate 기본값이 100이므로 100개를 만듭니다.

while len(machine_instruction_data) < num_instructions_to_generate:
request_idx += 1
# 요청 생성과 결과 처리 과정

이 코드는 num_instructions_to_generate로 지정된 개수만큼 지시사항을 생성하는 반복문입니다. 요청 생성 및 결과 처리 과정은 반복문 내에서 수행됩니다.

이후에는 요청 생성 및 결과 처리, 유사도 계산, 생성된 지시사항 관리 등의 과정이 이어집니다.

이쯤에서 지시사항 생성을 위한 함수 generate_instruction_following_data 이 거치는 과정을 봅니다:

  1. seed_tasks.jsonl 파일로부터 seed instruction 데이터를 가져옵니다. 이 데이터는 인간이 작성한 지시사항과 해당 지시사항에 대한 입력과 출력으로 구성되어 있습니다.
  2. 지시사항 생성을 위해 LM(Language Model)이 생성한 지시사항 데이터를 불러옵니다. 이 데이터는 이전에 생성된 기계 생성 지시사항으로 구성되어 있습니다. 만약 output_dir 경로에 “regen.json” 파일이 존재한다면 해당 파일에서 기계 생성 지시사항 데이터를 로드합니다.
  3. Rouge 스코어 계산을 위해 RougeScorer 객체를 생성합니다.
  4. 새로운 지시사항을 생성하기 위해 반복문을 실행합니다. 기계 생성 지시사항 데이터의 수가 num_instructions_to_generate보다 작은 경우에만 반복문이 실행됩니다.
  5. request_batch_size만큼의 요청(batch)을 생성합니다. 각 요청은 num_prompt_instructions 개수만큼의 seed instruction 데이터를 사용하여 인코딩된 프롬프트로 구성됩니다.
  6. 생성된 요청을 LM에 전달하여 지시사항을 생성합니다. 이때 LM 모델 이름, 온도(temperature), 상위 p(top_p) 등의 매개변수를 설정하여 생성 방식을 조정할 수 있습니다.
  7. 생성된 결과를 후처리하여 새로운 지시사항 데이터를 추출합니다.
  8. 추출된 새로운 지시사항 데이터와 기존의 지시사항 데이터를 비교하여 유사도를 계산합니다. 이를 위해 Rouge 스코어를 사용합니다. 계산된 유사도는 가장 유사한 지시사항들과 함께 저장됩니다.
  9. 유사도가 일정 기준(0.7)을 넘지 않는 지시사항은 유지하고, 유사도가 높은 지시사항은 새로운 기계 생성 지시사항 데이터에 추가합니다.
  10. 생성된 지시사항의 수와 유지된 지시사항의 수를 출력합니다.
  11. 기계 생성 지시사항 데이터를 파일에 저장합니다.

이러한 과정을 거쳐서 새로운 기계 생성 지시사항 데이터가 생성됩니다.

또 이어서 보겠습니다.
145번째 줄 코드를 보겠습니다.

# first we tokenize all the seed instructions and generated machine instructions
    all_instructions = [d["instruction"] for d in seed_instruction_data] + [
        d["instruction"] for d in machine_instruction_data
    ]
    all_instruction_tokens = [scorer._tokenizer.tokenize(inst) for inst in all_instructions]

위 코드는 seed instruction과 기계 생성된 지시사항(machine_instruction_data)들을 토큰화하는 과정입니다. 둘이 더해진 변수 all_instructions 는 아래 그림의 Task Pool에 해당됩니다.

먼저, seed_instruction_data에서 seed instruction들의 텍스트 부분만 추출하여 all_instructions 리스트에 저장합니다. 이때, seed_instruction_data는 seed instruction 데이터의 리스트이며, 각각의 데이터는 다음과 같은 구조를 가지고 있습니다:

{
“instruction”: <지시사항 텍스트>,
“input”: <입력 데이터>,
“output”: <출력 데이터>
}

다음으로, machine_instruction_data에서 기계 생성된 지시사항들의 텍스트 부분을 추출하여 all_instructions 리스트에 추가합니다. 이렇게 되면 all_instructions 리스트에는 seed instruction과 기계 생성된 지시사항들의 모든 텍스트가 포함됩니다.

그 후, all_instruction_tokens 리스트를 생성합니다. 이 리스트는 각각의 지시사항 텍스트를 토큰화한 결과를 담고 있습니다. scorer._tokenizer.tokenize(inst) 코드는 주어진 지시사항 텍스트 inst를 토큰화하여 리스트 형태로 반환합니다. 따라서 all_instruction_tokens 리스트는 all_instructions 리스트에 있는 모든 지시사항들을 토큰화한 결과로 구성됩니다.

이렇게 함으로써, seed instruction과 기계 생성된 지시사항들을 모두 토큰화하여 나중에 유사도를 계산하는 데 사용할 수 있게 됩니다.

토큰화된 데이터는 리스트의 리스트 형태로 구성됩니다. 각각의 리스트는 해당 지시사항의 토큰으로 구성되어 있습니다. 예를 들어, all_instruction_tokens 리스트의 한 요소를 살펴보면 다음과 같은 형태일 수 있습니다:

[
['First', 'prompt', 'instruction', '.'],
['Second', 'prompt', 'instruction', 'with', 'more', 'tokens', '.'],
# ...
]

위 예시에서는 두 개의 지시사항이 토큰화되어 있습니다. 각각의 지시사항은 해당 지시사항을 구성하는 단어들로 토큰화되어 리스트 형태로 저장되어 있습니다. 이러한 토큰화된 데이터를 활용하여 지시사항들 간의 유사도를 계산하거나 기타 자연어처리 작업을 수행할 수 있습니다.

154번째 코드를 보겠습니다.

batch_inputs = []
        for _ in range(request_batch_size):
            # only sampling from the seed tasks 시드에서만 샘플링!
            prompt_instructions = random.sample(seed_instruction_data, num_prompt_instructions)
            prompt = encode_prompt(prompt_instructions)
            batch_inputs.append(prompt)

request_batch_size가 5로 설정되어 있기 때문에 5개의 프롬프트가 생성됩니다.

request_batch_size는 한 번의 요청(batch)에 포함될 입력 데이터의 개수를 나타내는 변수입니다. 즉, 한 번의 요청에 동시에 처리할 프롬프트의 개수를 지정하는 값입니다. 위 코드에서는 request_batch_size가 5로 설정되어 있으므로, 각 반복마다 5개의 프롬프트가 생성되고 batch_inputs 리스트에 추가됩니다.

일반적으로 request_batch_size 값을 설정하는 것은 처리 효율성과 성능을 조절하는 데에 도움이 됩니다. 한 번에 여러 개의 입력 데이터를 처리하면 처리 시간을 단축시킬 수 있으며, LM이 병렬로 작업을 수행하여 전체 처리량을 높일 수 있습니다. 하지만 더 많은 메모리 및 처리 자원이 필요하므로 적절한 값으로 조정해야 합니다.

따라서 위 코드는 `request_batch_size`에 지정된 개수인 5개의 프롬프트를 생성하는 과정입니다.

먼저, 빈 리스트인 `batch_inputs`가 초기화됩니다. 이 리스트는 생성된 프롬프트를 저장하기 위한 용도로 사용됩니다.

다음으로, `request_batch_size`에 지정된 개수만큼 반복문이 실행됩니다. 이 반복문은 요청의 개수에 따라서 5번 실행됩니다.

각 반복에서는 `seed_instruction_data`에서 `num_prompt_instructions` 개수만큼의 seed instruction 데이터를 무작위로 선택합니다. 이때, `random.sample()` 함수를 사용하여 중복 없이 랜덤하게 선택됩니다. 선택된 seed instruction 데이터는 `prompt_instructions` 변수에 저장됩니다.

그 다음, `prompt_instructions`를 이용하여 `encode_prompt()` 함수가 호출되어 프롬프트를 인코딩합니다. 이 함수는 선택된 seed instruction 데이터를 기반으로 프롬프트를 생성하고, 인코딩된 형태로 반환합니다. 이렇게 생성된 프롬프트는 `prompt` 변수에 저장됩니다. 즉, 그림에서 LM에서 프롬프트를 입력해 지시를 하는 부분이죠.

마지막으로, 생성된 프롬프트(`prompt`)를 `batch_inputs` 리스트에 추가합니다. 이렇게 되면 각 반복에서 생성된 프롬프트가 `batch_inputs` 리스트에 저장되어 총 5개의 프롬프트가 저장됩니다.

따라서, 위 코드는 `request_batch_size`에 지정된 개수인 5개의 프롬프트를 생성하여 `batch_inputs` 리스트에 저장하는 과정을 수행합니다. 이후에 `batch_inputs` 리스트에 저장된 프롬프트를 이용하여 LM(Language Model)에게 한 번에 여러 개의 요청을 전달할 수 있습니다.

이제 27번째 줄의 prompt_instructions 보겠습니다. 그림의 Task 9 프롬프트를 생성하게 되는데요, 코드를 설명해 봅니다.

 

def encode_prompt(prompt_instructions):
    """Encode multiple prompt instructions into a single string."""
    prompt = open("./prompt.txt").read() + "\n"

    for idx, task_dict in enumerate(prompt_instructions):
        (instruction, input, output) = task_dict["instruction"], task_dict["input"], task_dict["output"]
        instruction = re.sub(r"\s+", " ", instruction).strip().rstrip(":")
        input = "" if input.lower() == "" else input
        prompt += f"###\n"
        prompt += f"{idx + 1}. Instruction: {instruction}\n"
        prompt += f"{idx + 1}. Input:\n{input}\n"
        prompt += f"{idx + 1}. Output:\n{output}\n"
    prompt += f"###\n"
    prompt += f"{idx + 2}. Instruction:"
    return prompt

해당 코드는 `encode_prompt`라는 함수를 정의하고 있습니다. 이 함수는 여러 개의 프롬프트 명령어를 하나의 문자열로 인코딩하는 역할을 합니다.

함수 내부에서는 주어진 `prompt_instructions`라는 인자를 순회하면서 각각의 명령어를 처리합니다. 각 명령어는 “instruction” (명령어), “input” (입력), “output” (출력)의 세 가지 정보를 가지고 있는 사전 형태로 주어집니다.

코드의 실행 흐름은 다음과 같습니다:

1. `prompt` 변수에 “./prompt.txt” 파일의 내용을 읽어와 저장합니다.
2. `prompt` 문자열 끝에 줄 바꿈 문자를 추가합니다.
3. `prompt_instructions`를 순회하면서 각각의 명령어를 처리합니다.
4. 명령어의 “instruction”, “input”, “output” 값을 가져옵니다.
5. “instruction” 문자열에서 연속된 공백을 하나로 치환하고 양쪽 공백을 제거한 후 마지막의 콜론을 제거합니다.
6. “input” 값이 빈 문자열인 경우 빈 문자열로 유지하고, 그렇지 않은 경우에는 소문자로 변환합니다.
7. `prompt` 문자열에 명령어 정보를 추가합니다. 각 명령어는 “###”로 구분되며, “Instruction:”, “Input:”, “Output:”과 함께 해당 값을 출력합니다.
8. `prompt` 문자열에 모든 명령어 정보를 추가한 후, 마지막에 추가적인 “Instruction:” 문장을 추가합니다.
9. 최종적으로 구성된 `prompt` 문자열을 반환합니다.

이 함수를 사용하여 여러 개의 프롬프트 명령어를 하나의 문자열로 인코딩하고자 할 때, `encode_prompt` 함수를 호출하면 됩니다.

이제 배치 처리 작업으로 openai에 데이터 처리하는 부분을 보겠습니다. 154줄입니다.

batch_inputs = []
        for _ in range(request_batch_size):
            # only sampling from the seed tasks
            prompt_instructions = random.sample(seed_instruction_data, num_prompt_instructions)
            prompt = encode_prompt(prompt_instructions)
            batch_inputs.append(prompt)
        decoding_args = utils.OpenAIDecodingArguments(
            temperature=temperature,
            n=1,
            max_tokens=3072,  # hard-code to maximize the length. the requests will be automatically adjusted
            top_p=top_p,
            stop=["\n20", "20.", "20."],
        )

해당 코드는 데이터를 배치로 처리하기 위한 과정을 나타내고 있습니다. 주어진 코드는 다음과 같은 기능을 수행합니다:

1. `batch_inputs`라는 빈 리스트를 생성합니다. 이 리스트는 인코딩된 프롬프트를 저장할 용도로 사용됩니다.
2. `request_batch_size` 변수에 지정된 값만큼 반복문을 실행합니다.
3. `seed_instruction_data`에서 `num_prompt_instructions`개의 명령어를 랜덤하게 추출하여 `prompt_instructions`에 저장합니다. 이는 프롬프트에서 사용할 명령어를 샘플링하는 과정입니다.
4. `encode_prompt` 함수를 사용하여 `prompt_instructions`을 하나의 문자열로 인코딩합니다. 인코딩된 프롬프트는 `prompt`에 저장됩니다.
5. `batch_inputs` 리스트에 `prompt`을 추가합니다.
6. `utils.OpenAIDecodingArguments`를 사용하여 디코딩 인자를 설정합니다. 이 인자는 디코딩 과정에서 사용될 설정값들을 담고 있습니다. 여기서는 온도(temperature), 생성할 토큰의 개수(n), 최대 토큰 수(max_tokens), 상위 확률(top_p), 중지 조건(stop) 등이 설정되었습니다.

이 코드는 배치로 처리할 데이터를 생성하는 과정을 보여주고 있습니다. `request_batch_size`만큼의 데이터를 처리하여 인코딩된 프롬프트를 `batch_inputs` 리스트에 추가하고, 디코딩 과정에서 사용될 인자를 설정합니다. 이후 이러한 데이터와 설정을 이용하여 모델의 디코딩을 수행할 수 있습니다.

이제 결과를 반환하는 부분을 봅니다.168번째 줄입니다.

results = utils.openai_completion(
            prompts=batch_inputs,
            model_name=model_name,
            batch_size=request_batch_size,
            decoding_args=decoding_args,
            logit_bias={"50256": -100},  # prevent the <|endoftext|> token from being generated
        )

해당 코드는 OpenAI의 언어 모델에 대한 요청을 처리하고 결과를 반환하는 부분입니다.

– `utils.openai_completion`: 이 함수는 OpenAI 언어 모델에 대한 완성(completion) 요청을 보냅니다. 이 함수는 다양한 매개변수를 사용하여 요청을 구성하고, 모델에 대한 완성 결과를 반환합니다.

– `prompts`: `batch_inputs`는 언어 모델에 제공되는 입력 프롬프트입니다. `batch_inputs`는 여러 개의 프롬프트로 구성된 리스트입니다.

– `model_name`: 이 매개변수는 사용할 OpenAI 모델의 이름을 지정합니다. 해당 코드에서는 `model_name`에 지정된 모델을 사용하여 완성 요청을 처리합니다.

– `batch_size`: 배치의 크기를 지정합니다. 이 값은 한 번에 처리할 프롬프트의 수를 결정합니다.

– `decoding_args`: 이 매개변수는 디코딩(decoding) 옵션을 설정합니다. 디코딩 옵션은 완성 결과를 생성할 때 사용되는 다양한 설정값들을 포함합니다. 예를 들어, 온도(temperature), 최대 토큰 개수(max_tokens), top-p 값 등이 설정될 수 있습니다.

– `logit_bias`: 이 매개변수는 로짓 편향(logit bias)을 설정합니다. 로짓 편향은 특정 토큰이 생성되는 확률을 조정하기 위해 사용될 수 있습니다. 해당 코드에서는 “50256”이라는 토큰의 생성을 방지하기 위해 로짓 편향이 설정되었습니다.

`results` 변수에는 OpenAI 언어 모델로부터 반환된 완성 결과가 저장됩니다. 이 결과를 통해 모델이 생성한 텍스트를 확인하고, 이후의 처리나 분석에 활용할 수 있습니다.

이렇게 반환된 results 를 후처리 하는 부분을 봅니다. 178번째 줄입니다.

instruction_data = []
        for result in results:
            new_instructions = post_process_gpt3_response(num_prompt_instructions, result)
            instruction_data += new_instructions

해당 코드는 언어 모델의 완성 결과를 처리하고, 처리된 결과를 `instruction_data` 리스트에 추가하는 부분입니다.

– `results`: `results`는 언어 모델의 완성 결과를 담고 있는 리스트입니다. 이전 단계에서 언어 모델에 대한 완성 요청이 보내졌고, 이 코드에서는 해당 요청에 대한 결과를 처리합니다.

– `for result in results:`: `results` 리스트의 각각의 결과에 대해 반복문을 실행합니다. 이 코드는 결과 리스트의 모든 요소를 하나씩 처리합니다.

– `new_instructions = post_process_gpt3_response(num_prompt_instructions, result)`: 생성된 문장을 하나 하나 가져와서 `post_process_gpt3_response` 함수를 사용하여 결과를 후처리합니다. 이 함수는 완성된 텍스트 결과를 입력 프롬프트와 관련된 지시사항으로 변환합니다. `num_prompt_instructions` 매개변수는 변환할 지시사항의 개수를 나타냅니다.

– `instruction_data += new_instructions`: 후처리된 지시사항을 `instruction_data` 리스트에 추가합니다. `+=` 연산자는 리스트에 다른 리스트를 추가하는 역할을 합니다.

결과적으로, 이 코드는 언어 모델의 완성 결과를 처리하고, 처리된 결과를 `instruction_data` 리스트에 모아서 저장합니다. 이후의 처리나 분석을 위해 `instruction_data` 리스트를 활용할 수 있습니다.

위의 후처리 하는 post_process_gpt3_response 부분의 코드를 살펴 보겠습니다.

def post_process_gpt3_response(num_prompt_instructions, response):
    if response is None:
        return []
    raw_instructions = f"{num_prompt_instructions+1}. Instruction:" + response["text"]
    raw_instructions = re.split("###", raw_instructions)
    instructions = []
    for idx, inst in enumerate(raw_instructions):
        # if the decoding stops due to length, the last example is likely truncated so we discard it
        if idx == len(raw_instructions) - 1 and response["finish_reason"] == "length":
            continue
        idx += num_prompt_instructions + 1
        splitted_data = re.split(f"{idx}\.\s+(Instruction|Input|Output):", inst)
        if len(splitted_data) != 7:
            continue
        else:
            inst = splitted_data[2].strip()
            input = splitted_data[4].strip()
            input = "" if input.lower() == "" else input
            output = splitted_data[6].strip()
        # filter out too short or too long instructions
        if len(inst.split()) <= 3 or len(inst.split()) > 150:
            continue
        # filter based on keywords that are not suitable for language models.
        blacklist = [
            "image",
            "images",
            "graph",
            "graphs",
            "picture",
            "pictures",
            "file",
            "files",
            "map",
            "maps",
            "draw",
            "plot",
            "go to",
            "video",
            "audio",
            "music",
            "flowchart",
            "diagram",
        ]
        blacklist += []
        if any(find_word_in_string(word, inst) for word in blacklist):
            continue
        # We found that the model tends to add "write a program" to some existing instructions, which lead to a lot of such instructions.
        # And it's a bit comfusing whether the model need to write a program or directly output the result.
        # Here we filter them out.
        # Note this is not a comprehensive filtering for all programming instructions.
        if inst.startswith("Write a program"):
            continue
        # filter those starting with punctuation
        if inst[0] in string.punctuation:
            continue
        # filter those starting with non-english character
        if not inst[0].isascii():
            continue
        instructions.append({"instruction": inst, "input": input, "output": output})
    return instructions

위의 코드는 `post_process_gpt3_response`라는 함수를 정의하는 부분입니다. 이 함수는 GPT-3 모델의 응답을 후처리하여 유효한 지시사항들을 추출하는 역할을 합니다.

함수의 동작은 다음과 같습니다:

1. 먼저, 응답이 `None`인 경우 빈 리스트를 반환합니다.

2. `raw_instructions` 변수에는 응답 텍스트와 숫자를 조합한 문자열이 저장됩니다. 이 문자열은 `###`로 구분된 여러 개의 지시사항으로 이루어져 있습니다.

3. `raw_instructions`를 `###`를 기준으로 분할하여 개별 지시사항들을 추출합니다.

4. `instructions` 리스트를 초기화합니다.

5. `raw_instructions`의 각 지시사항을 순회하면서 다음 작업을 수행합니다:
– 지시사항의 인덱스(`idx`)가 마지막 지시사항이면서 디코딩이 길이로 인해 중단된 경우, 해당 지시사항을 건너뜁니다.
– `idx`에 `num_prompt_instructions` 값을 더해 해당 지시사항의 인덱스를 계산합니다.
– 정규식을 사용하여 지시사항을 “Instruction”, “Input”, “Output”으로 분할합니다.
– 분할된 데이터가 예상한 형식(7개의 요소)이 아닌 경우 건너뜁니다.
– 분할된 데이터에서 지시사항, 입력 및 출력을 추출합니다. 입력이 `<noinput>`인 경우 빈 문자열로 처리합니다.
– 지시사항의 단어 수가 3 미만이거나 150을 초과하는 경우 건너뜁니다.
– 언어 모델에 적합하지 않은 키워드를 포함하는지 확인합니다. 이때, `blacklist`에 지정된 키워드들을 사용하여 필터링합니다.예를 들어, “image”, “graph”, “file”, “draw” 등의 단어는 언어 모델에 직접적인 의미를 갖지 않으며, 이미지, 그래프, 파일 작성 또는 그리기와 관련된 작업을 의미하는 경우가 많습니다. 이러한 키워드를 포함하는 지시사항은 언어 모델의 목적과는 부합하지 않기 때문에 걸러내는 것이 좋습니다.따라서 해당 코드는 언어 모델이 생성하는 지시사항 중에서 “blacklist”에 지정된 키워드를 가진 것들을 제외하여 유효한 지시사항들만을 추출하는 역할을 합니다. 이를 통해 언어 모델의 결과를 보다 적절하고 유용하게 만들어 줍니다.

– “Write a program”으로 시작하는 지시사항은 건너뜁니다.if inst.startswith(“Write a program”):

– 구두점으로 시작하는 지시사항은 건너뜁니다.if inst[0] in string.punctuation:

– 영어가 아닌 문자로 시작하는 지시사항은 건너뜁니다.

– 이렇게 해서 정제된 유효한 지시사항으로 판단되면 `instructions` 리스트에 추가합니다.

6. 최종적으로 추출된 유효한 지시사항들이 담긴 `instructions` 리스트를 반환합니다.

그림의 Step4:Filtering에 해당되는 부분입니다.

이 함수는 GPT-3 모델의 응답을 처리하여 유효한 지시사항들을 추출하고, 특정 조건에 맞지 않는 지시사항들을 걸러내는 등의 후처리 과정을 수행합니다. 이를 통해 보다 정확하고 유용한 결과를 얻을 수 있습니다. 추출된 유효한 지시사항들은 원하는 형식과 요구사항에 맞게 정제되어 있으며, 불필요한 지시사항이나 제한된 키워드가 제거되어 있습니다. 이를 통해 후속 작업에서 필요한 목적에 맞는 지시사항들을 활용할 수 있습니다.

이제 후처리된 데이터를 가지고 처리하는 부분을 살펴보겠습니다. 278번째 줄입니다.

 

instruction_data = []
        for result in results:
            new_instructions = post_process_gpt3_response(num_prompt_instructions, result)
            instruction_data += new_instructions

        total = len(instruction_data)
        keep = 0
        for instruction_data_entry in instruction_data:
            # computing similarity with the pre-tokenzied instructions
            new_instruction_tokens = scorer._tokenizer.tokenize(instruction_data_entry["instruction"])
            with Pool(num_cpus) as p:
                rouge_scores = p.map(
                    partial(rouge_scorer._score_lcs, new_instruction_tokens),
                    all_instruction_tokens,
                )
            rouge_scores = [score.fmeasure for score in rouge_scores]
            most_similar_instructions = {
                all_instructions[i]: rouge_scores[i] for i in np.argsort(rouge_scores)[-10:][::-1]
            }
            if max(rouge_scores) > 0.7:
                continue
            else:
                keep += 1
            instruction_data_entry["most_similar_instructions"] = most_similar_instructions
            instruction_data_entry["avg_similarity_score"] = float(np.mean(rouge_scores))
            machine_instruction_data.append(instruction_data_entry)
            all_instructions.append(instruction_data_entry["instruction"])
            all_instruction_tokens.append(new_instruction_tokens)
            progress_bar.update(1)
        process_duration = time.time() - process_start
        print(f"Request {request_idx} took {request_duration:.2f}s, processing took {process_duration:.2f}s")
        print(f"Generated {total} instructions, kept {keep} instructions")
        utils.jdump(machine_instruction_data, os.path.join(output_dir, "regen.json"))

위의 코드는 결과에서 추출된 지시사항들을 처리하고 유사도를 계산하는 부분입니다.

처음에는 빈 리스트인 `instruction_data`를 선언합니다. 그런 다음, `results`에 있는 각 결과에 대해 `post_process_gpt3_response` 함수를 사용하여 유효한 지시사항들을 추출하고, `new_instructions`에 저장합니다. 그리고 `instruction_data`에 `new_instructions`를 추가합니다.

다음으로, 전체 지시사항 개수를 `total`에 저장하고, 유사도를 계산하기 위해 루프를 실행합니다. 각 지시사항을 순회하면서, 먼저 해당 지시사항을 토큰화한 후 미리 토큰화된 지시사항들과의 유사도를 계산합니다. 이때, 병렬처리를 위해 `Pool`을 사용하여 계산을 수행합니다.

유사도 계산이 완료되면, 가장 유사한 지시사항들을 `most_similar_instructions`에 저장합니다. 만약 가장 높은 유사도 점수가 0.7보다 크다면 (코드 197번째 줄의
if max(rouge_scores) > 0.7:) 해당 지시사항은 유사한 지시사항이 이미 존재하므로 제거합니다. 그렇지 않으면, `keep` 변수를 증가시키고, 해당 지시사항의 유사한 지시사항들과 평균 유사도 점수를 추가로 저장합니다. 그리고 `machine_instruction_data`, `all_instructions`, `all_instruction_tokens`에 해당 지시사항과 관련된 정보를 추가합니다.

마지막으로, 처리 시간과 결과에 대한 정보를 출력하고, `machine_instruction_data`를 JSON 파일로 저장합니다.

이 코드는 추출된 지시사항들을 처리하고, 유사도를 계산하여 유사한 지시사항들을 필터링하는 과정을 수행합니다. 이를 통해 최종적으로 사용할 목적에 적합한 지시사항들을 얻을 수 있습니다.

 

전체 코드

def generate_instruction_following_data(
    output_dir="./",
    seed_tasks_path="./seed_tasks.jsonl",
    num_instructions_to_generate=100,
    model_name="text-davinci-003",
    num_prompt_instructions=3,
    request_batch_size=5,
    temperature=1.0,
    top_p=1.0,
    num_cpus=16,
):
    #seed instruction 데이터 가져오고
    seed_tasks = [json.loads(l) for l in open(seed_tasks_path, "r")]
    seed_instruction_data = [
        {"instruction": t["instruction"], "input": t["instances"][0]["input"], "output": t["instances"][0]["output"]}
        for t in seed_tasks
    ]
    print(f"Loaded {len(seed_instruction_data)} human-written seed instructions")

    os.makedirs(output_dir, exist_ok=True)
    request_idx = 0
    # load the LM-generated instructions
    machine_instruction_data = []
    if os.path.exists(os.path.join(output_dir, "regen.json")):
        machine_instruction_data = utils.jload(os.path.join(output_dir, "regen.json"))
        print(f"Loaded {len(machine_instruction_data)} machine-generated instructions")

    # similarities = {}
    scorer = rouge_scorer.RougeScorer(["rougeL"], use_stemmer=False)

    # now let's generate new instructions!
    progress_bar = tqdm.tqdm(total=num_instructions_to_generate)
    if machine_instruction_data:
        progress_bar.update(len(machine_instruction_data))

    # first we tokenize all the seed instructions and generated machine instructions
    all_instructions = [d["instruction"] for d in seed_instruction_data] + [
        d["instruction"] for d in machine_instruction_data
    ]
    all_instruction_tokens = [scorer._tokenizer.tokenize(inst) for inst in all_instructions]

    while len(machine_instruction_data) < num_instructions_to_generate:
        request_idx += 1

        batch_inputs = []
        for _ in range(request_batch_size):
            # only sampling from the seed tasks
            prompt_instructions = random.sample(seed_instruction_data, num_prompt_instructions)
            prompt = encode_prompt(prompt_instructions)
            batch_inputs.append(prompt)
        decoding_args = utils.OpenAIDecodingArguments(
            temperature=temperature,
            n=1,
            max_tokens=3072,  # hard-code to maximize the length. the requests will be automatically adjusted
            top_p=top_p,
            stop=["\n20", "20.", "20."],
        )
        request_start = time.time()
        results = utils.openai_completion(
            prompts=batch_inputs,
            model_name=model_name,
            batch_size=request_batch_size,
            decoding_args=decoding_args,
            logit_bias={"50256": -100},  # prevent the <|endoftext|> token from being generated
        )
        request_duration = time.time() - request_start

        process_start = time.time()
        instruction_data = []
        for result in results:
            new_instructions = post_process_gpt3_response(num_prompt_instructions, result)
            instruction_data += new_instructions

        total = len(instruction_data)
        keep = 0
        for instruction_data_entry in instruction_data:
            # computing similarity with the pre-tokenzied instructions
            new_instruction_tokens = scorer._tokenizer.tokenize(instruction_data_entry["instruction"])
            with Pool(num_cpus) as p:
                rouge_scores = p.map(
                    partial(rouge_scorer._score_lcs, new_instruction_tokens),
                    all_instruction_tokens,
                )
            rouge_scores = [score.fmeasure for score in rouge_scores]
            most_similar_instructions = {
                all_instructions[i]: rouge_scores[i] for i in np.argsort(rouge_scores)[-10:][::-1]
            }
            if max(rouge_scores) > 0.7:
                continue
            else:
                keep += 1
            instruction_data_entry["most_similar_instructions"] = most_similar_instructions
            instruction_data_entry["avg_similarity_score"] = float(np.mean(rouge_scores))
            machine_instruction_data.append(instruction_data_entry)
            all_instructions.append(instruction_data_entry["instruction"])
            all_instruction_tokens.append(new_instruction_tokens)
            progress_bar.update(1)
        process_duration = time.time() - process_start
        print(f"Request {request_idx} took {request_duration:.2f}s, processing took {process_duration:.2f}s")
        print(f"Generated {total} instructions, kept {keep} instructions")
        utils.jdump(machine_instruction_data, os.path.join(output_dir, "regen.json"))