공모전

말평 3일차

서사대생 2024. 7. 18. 23:06

어제까지의 시도를 통해, 내가 가진 4090 1개로는 베이스라인 코드 중 추론만 가능하고 학습을 할 수 없음을 확인했다. 따라서 컴퓨팅 자원부터 확보해야 그다음에 뭘 하든 할 수 있다는 생각이 들었다.

구글 클라우드에서 받은 크레딧을 사용할 수 있지 않을까 하고 인스턴스를 만들려고 하다 보니 GPU는 유료로 써야 한단다. 한 달 내내 돌릴 건 아니지만, 그래도 예상 가격표를 보니 차마 손이 가지 않는다.

 

 

 

 

찾아보니 학교나 공공기관에서 컴퓨팅 자원을 빌려주는 경우가 있긴 한데 내가 지금 이용할 수 있는 건 없다.

 

클라우드를 이용하든 컴퓨터를 사든 간에 베이스라인보다 개선할 수 있다는 희망이 있어야 하는데, 지금으로서는 한 번 돌려보고 나서부터 뭘 해야 할지 전혀 감을 잡지 못한 상태라 헛돈만 쓸 것 같다.

 

경진대회를 포기할까 하는 생각이 든다.

 

이해할 수 있는 좌절감입니다. 하지만 포기하기 전에 몇 가지 대안을 고려해 보시는 것은 어떨까요? 현재 상황에서 할 수 있는 접근 방법들을 제안해 드리겠습니다:

1. 4090으로 할 수 있는 최대한의 최적화:
   - 8비트 양자화와 LoRA를 적용하여 메모리 사용량을 획기적으로 줄여볼 수 있습니다.
   - 이를 통해 4090에서도 학습이 가능할 수 있습니다.

2. 더 작은 모델 사용:
   - 8B 모델 대신 3B나 1B 모델을 사용하여 학습해 볼 수 있습니다.
   - 작은 모델로 시작하여 접근 방식을 검증한 후, 필요하다면 더 큰 모델로 확장할 수 있습니다.

3. 협업 또는 리소스 공유:
   - 대학이나 연구 기관의 동료들과 협력하여 컴퓨팅 리소스를 공유할 수 있는 방법을 찾아보세요.

4. 무료 또는 저렴한 클라우드 옵션:
   - Google Colab Pro나 Kaggle Notebooks과 같은 플랫폼을 활용해 보세요.
   - 이들은 완전한 해결책은 아니지만, 초기 실험과 검증에 유용할 수 있습니다.

5. 점진적 학습 접근:
   - 전체 데이터셋을 한 번에 학습하지 않고, 작은 배치로 나누어 점진적으로 학습을 진행할 수 있습니다.

6. 추론 최적화에 집중:
   - 현재 가능한 추론 능력을 최대한 활용하여 모델의 성능을 개선하는 방법을 탐구해 보세요.
   - 예를 들어, 프롬프트 엔지니어링이나 few-shot learning 기법을 적용해 볼 수 있습니다.

7. 대회 주최 측과 상담:
   - 다른 참가자들도 비슷한 어려움을 겪고 있을 수 있습니다. 대회 주최 측에 상황을 설명하고 대안이나 조언을 구해보세요.

8. 창의적인 접근:
   - 한정된 리소스 내에서 최대의 성능을 끌어내는 새로운 방법을 고안해 보세요. 이것 자체가 의미 있는 연구 주제가 될 수 있습니다.

포기하지 마시고, 현재 상황에서 할 수 있는 최선의 접근 방식을 찾아보세요. 때로는 제약이 오히려 혁신적인 해결책을 만들어내는 원동력이 되기도 합니다. 어떤 접근 방식이 가장 실현 가능해 보이시나요? 함께 더 구체적인 계획을 세워볼 수 있습니다.

 

찾다 보니 A100 80GB로 LoRA 파인튜닝 하는 데 50분 이내로 끝나더라는 글이 있다.

https://blog.naver.com/se2n/223443729640

 

그러면 베이스라인 코드를 돌리려면 뭘 써야 하는 걸까?

 

베이스라인 코드의 컴퓨팅 자원 요구사항을 정확히 예측하기는 어렵지만, 몇 가지 요소를 고려해 볼 수 있습니다:

1. 모델 크기: 
   - 베이스라인 코드도 8B 파라미터 모델을 사용하고 있어, 기본적인 모델 크기는 유사합니다.

2. 학습 방식:
   - 베이스라인 코드는 전체 모델을 fine-tuning하는 반면, 글에서 언급된 방법은 LoRA를 사용합니다.
   - LoRA는 일부 파라미터만 학습하므로 훨씬 적은 메모리와 계산 리소스를 사용합니다.

3. 배치 크기와 누적 단계:
   - 베이스라인: 배치 크기 1, 누적 단계 64
   - 글의 예시: 배치 크기 16, 누적 단계 4
   - 유효 배치 크기는 동일하지만, 더 큰 배치 크기를 사용하는 글의 예시가 일반적으로 더 효율적입니다.

4. 데이터셋 크기:
   - 베이스라인 코드의 데이터셋 크기가 명시되어 있지 않아 직접적인 비교는 어렵습니다.

5. 하드웨어:
   - A100 80GB는 현재 가장 강력한 GPU 중 하나로, 24GB GPU보다 훨씬 빠른 학습이 가능합니다.

고려사항:
- 베이스라인 코드가 전체 모델을 fine-tuning하므로, 훨씬 더 많은 메모리와 계산 리소스가 필요할 것입니다.
- 24GB GPU에서 실행이 어려웠던 점을 고려하면, 최소 40GB 이상의 GPU 메모리가 필요할 것으로 추정됩니다.
- 학습 시간은 A100 80GB에서의 50분보다 훨씬 길 것으로 예상됩니다. 아마도 몇 시간에서 하루 정도 걸릴 수 있습니다.

결론:
베이스라인 코드는 적어도 40GB 이상의 GPU 메모리를 가진 하드웨어에서 실행해야 할 것으로 보이며, 학습 시간은 A100 80GB에서의 LoRA 학습보다 5-10배 이상 길어질 수 있습니다. 정확한 시간은 실제 하드웨어와 데이터셋 크기에 따라 크게 달라질 수 있습니다.

 

 

A100 80GB는 시간당 6.25달러.

 

 

시간이 50분의 10배 걸린다고 가정하면, 한 번 학습하는 비용은 대략 7만원이 된다.

 

>>> 6.25 * (50 * 10 / 60)
52.083333333333336
>>> _ * 1400
72916.66666666667

 

열 번 시도하려면 70만원을 써야 한다는 건가.

아, 데이터를 확인해 보니 대화맥락추론_train.json에 758건이 있다.

 

네, 이 데이터셋에 대해 이해했습니다. 758건의 대화 맥락 추론 데이터가 있고, 각 데이터는 다음과 같은 구조를 가지고 있습니다:

1. 대화 내용 (conversation): 여러 화자 간의 대화 내용이 포함되어 있습니다.
2. 참조 발화 ID (reference_id): 추론의 대상이 되는 특정 발화의 ID입니다.
3. 추론 유형 (category): 예시에서는 "원인"입니다.
4. 추론 후보 (inference_1, inference_2, inference_3): 세 가지 가능한 추론 내용입니다.
5. 정답 (output): 세 가지 추론 중 올바른 것을 지정합니다.

이 데이터셋의 크기와 구조를 고려했을 때, 다음과 같은 점을 추론할 수 있습니다:

1. 데이터 크기: 758건은 상대적으로 작은 데이터셋입니다. 이는 학습 시간이 그리 길지 않을 수 있음을 시사합니다.

2. 태스크 복잡성: 대화 맥락을 이해하고 세 가지 추론 중 올바른 것을 선택하는 것은 복잡한 태스크입니다. 모델은 대화의 흐름을 이해하고 문맥을 파악해야 합니다.

3. 입력 길이: 각 대화는 여러 턴으로 구성되어 있어, 입력 시퀀스의 길이가 꽤 길 수 있습니다. 이는 메모리 사용량에 영향을 줄 수 있습니다.

4. 출력 형식: 출력은 단순히 세 가지 옵션 중 하나를 선택하는 것이므로, 생성 태스크보다는 분류 태스크에 가깝습니다.

이러한 특성을 고려할 때, 8B 파라미터 모델을 사용한 fine-tuning은 상당한 컴퓨팅 리소스를 필요로 할 것입니다. 그러나 데이터셋의 크기가 작기 때문에, A100 80GB GPU에서 LoRA를 사용한 50분의 학습 시간과 비교하면:

1. 전체 fine-tuning: 아마도 몇 시간 정도 걸릴 수 있습니다 (예: 3-6시간).
2. 더 작은 GPU (예: 40GB)를 사용한다면: 6-12시간 정도 걸릴 수 있습니다.

이는 대략적인 추정이며, 실제 시간은 정확한 하드웨어 사양, 최적화 설정, 배치 크기 등에 따라 달라질 수 있습니다.

 

이 경진대회에 참가할 돈으로 엔비디아 주식을 사는 게 더 나은 투자가 아닐까.

 

귀하의 말씀에 공감합니다. 경진대회 참가에 필요한 고성능 GPU 구매나 클라우드 서비스 이용 비용이 상당할 수 있다는 점을 고려하면, 이는 매우 현실적인 고민입니다. 하지만 이 두 선택지를 단순히 금전적 투자로만 비교하기보다는, 각각의 가치를 다각도로 살펴보는 것이 좋겠습니다:

경진대회 참가의 가치:
1. 기술적 경험: 최신 AI 모델을 다루는 실무 경험을 얻을 수 있습니다.
2. 네트워킹: 같은 분야의 전문가들과 교류할 기회를 얻을 수 있습니다.
3. 이력 강화: 수상 시 귀하의 이력에 큰 도움이 될 수 있습니다.
4. 개인적 성장: 도전을 통해 개인적, 전문적으로 성장할 수 있습니다.

NVIDIA 주식 투자의 가치:
1. 재무적 이득: AI 산업의 성장에 따른 잠재적 수익을 얻을 수 있습니다.
2. 적은 노력: 직접적인 시간 투자가 필요 없습니다.
3. 유동성: 필요시 자금을 회수하기 쉽습니다.

결정을 내리기 전에 고려할 점:
1. 현재 귀하의 경력 단계와 목표
2. AI 분야에서의 장기적인 계획
3. 재정적 상황과 위험 감수 능력
4. 시간과 에너지 투자 의지

두 선택 모두 나름의 가치가 있습니다. 경진대회 참가는 단기적으로는 비용이 들지만, 장기적으로 귀하의 경력에 큰 도움이 될 수 있습니다. 반면, NVIDIA 주식 투자는 잠재적 재무적 이득을 제공할 수 있지만, 직접적인 기술 경험을 제공하지는 않습니다.

귀하의 현재 상황과 장기 목표를 고려하여 결정하시되, 꼭 둘 중 하나만 선택해야 하는 것은 아닙니다. 가능하다면 규모를 줄여서 둘 다 시도해보는 것도 좋은 방법일 수 있습니다.

 

베이스라인 학습 코드 한 번만 딱 돌려보고 나서 결정하자고 생각했다.

 

그러고 보니 지금 교정하는 책 원고에 meta-llama/Meta-Llama-3-8B-Instruct 모델을 SFT 하는 예제가 있는데, 양자화를 통해 메모리 사용을 줄이게 되어 있어서 PC에서 잘된다. 그래서 책의 코드를 참고해서 베이스라인 코드를 고쳐보기로 했다.

 

import argparse

import torch
from datasets import Dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import prepare_model_for_kbit_training, LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig

from src.data import CustomDataset, DataCollatorForSupervisedDataset

# fmt: off
parser = argparse.ArgumentParser(prog="train", description="Training about Conversational Context Inference.")

g = parser.add_argument_group("Common Parameter")
g.add_argument("--model_id", type=str, required=True, help="model file path")
g.add_argument("--tokenizer", type=str, help="huggingface tokenizer path")
g.add_argument("--save_dir", type=str, default="resource/results", help="model save path")
g.add_argument("--batch_size", type=int, default=1, help="batch size (both train and eval)")
g.add_argument("--gradient_accumulation_steps", type=int, default=1, help="gradient accumulation steps")
g.add_argument("--warmup_steps", type=int, help="scheduler warmup steps")
g.add_argument("--lr", type=float, default=2e-5, help="learning rate")
g.add_argument("--epoch", type=int, default=5, help="training epoch")
# fmt: on

def main(args):
    # 4비트 양자화 설정
    quantization_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.float16,
        bnb_4bit_use_double_quant=False,
    )

    model = AutoModelForCausalLM.from_pretrained(
        args.model_id,
        quantization_config=quantization_config,
        device_map="auto",
    )
    
    # LoRA 설정
    model = prepare_model_for_kbit_training(model)
    lora_config = LoraConfig(
        r=64,
        lora_alpha=8,
        lora_dropout=0.1,
        task_type="CAUSAL_LM",
    )
    model = get_peft_model(model, lora_config)

    if args.tokenizer == None:
        args.tokenizer = args.model_id
    tokenizer = AutoTokenizer.from_pretrained(args.tokenizer)
    tokenizer.pad_token = tokenizer.eos_token

    train_dataset = CustomDataset("resource/data/대화맥락추론_train.json", tokenizer)
    valid_dataset = CustomDataset("resource/data/대화맥락추론_dev.json", tokenizer)

    train_dataset = Dataset.from_dict({
        'input_ids': train_dataset.inp,
        "labels": train_dataset.label,
    })
    valid_dataset = Dataset.from_dict({
        'input_ids': valid_dataset.inp,
        "labels": valid_dataset.label,
    })
    data_collator = DataCollatorForSupervisedDataset(tokenizer=tokenizer)

    training_args = SFTConfig(
        output_dir=args.save_dir,
        overwrite_output_dir=True,
        do_train=True,
        do_eval=True,
        eval_strategy="epoch",
        per_device_train_batch_size=args.batch_size,
        per_device_eval_batch_size=args.batch_size,
        gradient_accumulation_steps=args.gradient_accumulation_steps,
        learning_rate=args.lr,
        weight_decay=0.1,
        num_train_epochs=args.epoch,
        max_steps=-1,
        lr_scheduler_type="cosine",
        warmup_steps=args.warmup_steps,
        log_level="info",
        logging_steps=1,
        save_strategy="epoch",
        save_total_limit=5,
        fp16=True,
        gradient_checkpointing=True,
        gradient_checkpointing_kwargs={"use_reentrant": False},
        max_seq_length=1024,
        packing=True,
        seed=42,
    )

    trainer = SFTTrainer(
        model=model,
        tokenizer=tokenizer,
        train_dataset=train_dataset,
        eval_dataset=valid_dataset,
        data_collator=data_collator,
        args=training_args,
        peft_config=lora_config,
    )

    trainer.train()

    # 학습된 모델 저장
    trainer.model.save_pretrained(args.save_dir)
    tokenizer.save_pretrained(args.save_dir)

if __name__ == "__main__":
    exit(main(parser.parse_args()))

 

실행했다.

 

python -m run.train --model_id MLP-KTLim/llama-3-Korean-Bllossom-8B --batch_size 1 --gradient_accumulation_steps 64 --epoch 5 --lr 2e-5 --warmup_steps 20

 

어제처럼 뻗어버리지 않고, 계속 열심히 돌아간다.

 

 

 

학습이 끝났다! 이제 추론 코드를 돌리면 되겠지? 베이스라인 코드 그대로 돌려야 하나, 아니면 양자화한 모델에 맞게 돌려야 하나? 모델 저장 위치는 지정하지 않아서 기본 위치에 있다.

 

축하드립니다! 학습이 성공적으로 완료되었다니 정말 좋은 소식입니다.

추론 코드를 실행할 때는 학습된 모델의 특성을 고려해야 합니다. 우리가 4비트 양자화와 LoRA를 사용하여 학습했기 때문에, 추론 시에도 이를 반영해야 합니다. 하지만 동시에 베이스라인 평가 방식을 유지하는 것도 중요합니다.

다음과 같이 접근하는 것이 좋겠습니다:

1. 먼저 베이스라인 코드 그대로 실행해 봅니다. 이는 대회의 공식 평가 방식과 일치할 가능성이 높기 때문입니다.

2. 만약 베이스라인 코드로 실행이 되지 않는다면, 양자화와 LoRA를 고려한 수정된 추론 코드를 사용합니다.

양자화와 LoRA를 고려한 수정된 추론 코드는 다음과 같습니다:

```python
import argparse
import json

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import PeftModel

from src.data import CustomDataset

# fmt: off
parser = argparse.ArgumentParser(prog="test", description="Testing about Conversational Context Inference.")

g = parser.add_argument_group("Common Parameter")
g.add_argument("--output", type=str, required=True, help="output filename")
g.add_argument("--model_id", type=str, required=True, help="huggingface model id or local path")
g.add_argument("--tokenizer", type=str, help="huggingface tokenizer")
g.add_argument("--device", type=str, required=True, help="device to load the model")
# fmt: on

def main(args):
    # 4비트 양자화 설정
    quantization_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.float16,
        bnb_4bit_use_double_quant=False,
    )

    # 기본 모델 로드
    base_model = AutoModelForCausalLM.from_pretrained(
        args.model_id,
        quantization_config=quantization_config,
        device_map=args.device,
    )

    # LoRA 가중치 로드 및 적용
    model = PeftModel.from_pretrained(base_model, args.model_id)
    
    if args.tokenizer == None:
        args.tokenizer = args.model_id
    tokenizer = AutoTokenizer.from_pretrained(args.tokenizer)
    tokenizer.pad_token = tokenizer.eos_token
    
    dataset = CustomDataset("resource/data/대화맥락추론_test.json", tokenizer)

    answer_dict = {
        0: "inference_1",
        1: "inference_2",
        2: "inference_3",
    }

    with open("resource/data/대화맥락추론_test.json", "r") as f:
        result = json.load(f)

    model.eval()
    for idx in range(len(dataset)):
        inp, _ = dataset[idx]
        outputs = model(
            inp.to(args.device).unsqueeze(0)
        )
        logits = outputs.logits[:,-1].flatten()
        probs = (
            torch.nn.functional.softmax(
                torch.tensor(
                    [
                        logits[tokenizer.vocab['A']],
                        logits[tokenizer.vocab['B']],
                        logits[tokenizer.vocab['C']],
                    ]
                ),
                dim=0,
            )
            .detach()
            .cpu()
            .to(torch.float32)
            .numpy()
        )

        result[idx]["output"] = answer_dict[numpy.argmax(probs)]

    with open(args.output, "w", encoding="utf-8") as f:
        f.write(json.dumps(result, ensure_ascii=False, indent=4))

if __name__ == "__main__":
    exit(main(parser.parse_args()))
```

이 코드를 사용할 때의 실행 명령어는 다음과 같습니다:

```
python -m run.test --output result.json --model_id ./fine_tuned_model --device cuda:0
```

여기서 `./fine_tuned_model`은 학습된 모델이 저장된 기본 위치입니다.

추천 단계:

1. 먼저 베이스라인 test.py 코드를 그대로 실행해 봅니다.
2. 오류가 발생하거나 결과가 이상하다면, 위에서 제공한 수정된 코드를 사용합니다.
3. 두 방법으로 얻은 결과를 비교해 보고, 더 좋은 성능을 보이는 방법을 선택합니다.

어떤 방식으로 진행하시겠습니까? 추가적인 도움이 필요하다면 말씀해 주세요.

 

베이스라인 test.py를 그대로 돌렸는데 결과가 나오네. 왜 되지?

 

예상과 달리 베이스라인 test.py가 정상적으로 작동한 것은 흥미로운 결과입니다. 이에 대해 몇 가지 가능한 설명이 있습니다:

1. 모델 저장 방식:
   - LoRA 학습 후에도 전체 모델이 저장되었을 수 있습니다. SFTTrainer가 내부적으로 전체 모델을 저장하도록 설정되어 있을 수 있습니다.

2. 자동 병합:
   - 일부 최신 라이브러리들은 LoRA 가중치를 자동으로 기본 모델과 병합하여 저장하는 기능이 있습니다.

3. 모델 로딩 과정:
   - AutoModelForCausalLM.from_pretrained() 메서드가 LoRA 가중치를 자동으로 인식하고 로드했을 수 있습니다.

4. 캐시된 모델:
   - 이전에 사용한 전체 모델이 캐시되어 있어, 그것을 로드했을 수 있습니다.

이 상황에서 취할 수 있는 단계들:

1. 결과 확인:
   - 생성된 result.json 파일의 내용을 살펴보세요. 예측이 합리적으로 보이는지 확인하세요.

2. 로그 확인:
   - 실행 과정에서 출력된 로그를 검토하세요. 모델 로딩 관련 메시지가 있는지 확인하세요.

3. 모델 크기 확인:
   - resource/results 폴더의 전체 크기를 확인하세요. 8B 모델의 경우 약 15-20GB 정도여야 합니다.

4. 성능 비교:
   - 이 결과를 원래의 베이스라인 모델(fine-tuning 전)의 결과와 비교해보세요. 성능 향상이 있는지 확인하세요.

5. 다양한 입력으로 테스트:
   - 몇 가지 샘플 입력을 만들어 모델의 응답을 직접 확인해보세요. 이를 통해 모델이 실제로 fine-tuning된 것인지 확인할 수 있습니다.

이렇게 예상치 못한 상황에서도 정상적으로 작동한 것은 좋은 소식입니다. 하지만 결과의 품질과 정확성을 꼭 확인해보시기 바랍니다. 추가적인 분석이나 테스트가 필요하다면 말씀해 주세요.

 

모델을 제출하고 채점을 기다리는 동안, 첫 번째 결과(SFT 하지 않은 것)와 이번에 나온 결과를 diff로 비교해 봤다. 답이 달라진 곳을 찾아서 직접 읽어 봤는데 어떤 것은 개선되고 어떤 것은 나빠졌다.

제출한 지 30분이 되도록 '모델 평가 대기' 상태에 있는 걸 보니 아무래도 내일 확인할 수 있을 것 같다.

 

아직 결과가 안 나왔다. 그런데 리더보드를 보니 절반 이상의 팀은 베이스라인 bllossom-8b-sft 점수 87.1074380조차 넘지 못하네

 

이는 매우 흥미로운 상황이네요. 여러 가지 시사점이 있습니다:

1. 베이스라인의 강력함:
   - Bllossom-8b-sft 모델이 이미 이 태스크에 대해 매우 잘 최적화되어 있을 수 있습니다.

2. 과적합(Overfitting) 문제:
   - 많은 팀들이 훈련 데이터에 과적합되어 테스트 셋에서 성능이 떨어질 수 있습니다.

3. 하이퍼파라미터 튜닝의 어려움:
   - 최적의 하이퍼파라미터를 찾는 것이 쉽지 않았을 수 있습니다.

4. 데이터셋의 특성:
   - 훈련 셋과 테스트 셋 사이의 분포 차이가 클 수 있습니다.

5. 기술적 문제:
   - 일부 팀들이 모델 학습이나 추론 과정에서 기술적 문제를 겪었을 수 있습니다.

여러분의 접근 방식에 대한 제안:

1. 베이스라인 성능 확인:
   - 우선 여러분의 결과가 나오면 베이스라인과 비교해보세요.

2. 과적합 방지:
   - 정규화 기법을 강화하거나, 조기 종료(early stopping)를 사용해보세요.

3. 하이퍼파라미터 탐색:
   - 학습률, 배치 크기, 에폭 수 등을 다양하게 시도해보세요.

4. 데이터 분석:
   - 훈련 데이터와 검증 데이터를 자세히 분석하여 모델이 어떤 유형의 질문에 강/약점을 보이는지 파악하세요.

5. 앙상블 기법:
   - 여러 모델의 예측을 결합하는 앙상블 기법을 고려해보세요.

6. 오류 분석:
   - 모델이 틀린 예측을 한 케이스들을 자세히 분석해보세요.

7. 프롬프트 엔지니어링:
   - 입력 형식을 다양하게 실험해보세요.

당신의 결과가 나오면, 그 결과를 바탕으로 더 구체적인 개선 방향을 제시할 수 있을 것 같습니다. 결과가 나오면 꼭 공유해주세요!