스탠포드 알파카(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()

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다