토크나이저 재학습

1. KorQuAD 데이터셋

  • 한국어 기계 독해 데이터셋
  • 위키피디아에서 추출한 질의-응답 쌍 데이터셋
print("KorQuAD 데이터셋 다운로드 중...")
dataset = load_dataset("KETI-AIR/korquad", "v1.0")
DatasetDict({
    train: Dataset({
        features: ['id', 'title', 'context', 'question', 'answers'],
        num_rows: 60407
    })
    dev: Dataset({
        features: ['id', 'title', 'context', 'question', 'answers'],
        num_rows: 5774
    
  • context: 내용
  • question: 내용에 기반한 문제
  • answers: 답

데이터셋을 리스트로 담음

def extract_texts_from_korquad(dataset_split):
    texts = []
    for data in dataset_split:
        texts.append(data['context'])
        texts.append(data['question'])
        texts.append(data['answers']['text'][0])

    return texts

train_texts = extract_texts_from_korquad(dataset["train"])
valid_texts = extract_texts_from_korquad(dataset["dev"])
all_texts = train_texts + valid_texts

임시파일에 텍스트 저장

temp_files = []
chunk_size = 10000
for i in range(0, len(all_texts), chunk_size):
    chunk = all_texts[i:i+chunk_size]
    temp_file = tempfile.NamedTemporaryFile(delete=False, mode="w", encoding="utf-8")
    temp_file.write("\n".join(chunk))
    temp_file.close()
    temp_files.append(temp_file.name)

print(f"{len(temp_files)}개의 임시 파일 생성 완료")


2. 토크나이저 학습

토크나이저의 구성

  • 원본 : "Hello안녕하세요"
  • pre-tokenizer
    • 공백 앞에 추가: " Hello 안녕하세요"
    • 공백 처리: "ĠHelloĠ안녕하세요"
    • 이후 각 문자는 UTF-8 바이트 수준에서 처리됨
  • tokenizer
    • ["Ġ", "Hello", "Ġ", "안녕", "하세", "요"]

데이터셋으로 토크나이저 학습

  • tokenizer.train(files=temp_files, trainer=trainer)

print("토크나이저 학습 시작...")

# 새 토크나이저 초기화 (BPE 방식 사용)
tokenizer = Tokenizer(models.BPE(unk_token="<unk>"))
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=True)

# 트레이너 설정
trainer = trainers.BpeTrainer(
    vocab_size=32000,  # 어휘 크기
    min_frequency=2,   # 최소 등장 빈도
    special_tokens=["<s>", "</s>", "<unk>", "<pad>", "<mask>"],  # 특수 토큰 <s> 시작 </s> 끝 토큰
    show_progress=True,
)

# 토크나이저 학습
tokenizer.train(files=temp_files, trainer=trainer)

# 후처리 추가
tokenizer.post_processor = TemplateProcessing(
    # 모델이 시작과 끝을 인식할 수 있도록
    single="<s> $A </s>", # 단일 텍스트 시퀀스에 대한 형식
    pair="<s> $A </s> $B </s>", # 텍스트 쌍에 대한 형식 (예: 질문-답변, 번역 등)
    # 특수토큰 : 시작, 끝
    special_tokens=[
        ("<s>", tokenizer.token_to_id("<s>")),
        ("</s>", tokenizer.token_to_id("</s>")),
    ],
)

tokenizer.decoder = decoders.ByteLevel()

# 새 토크나이저 저장
os.makedirs("custom_tokenizer", exist_ok=True)
tokenizer_path = "custom_tokenizer/tokenizer.json"
tokenizer.save(tokenizer_path)

print(f"토크나이저 학습 완료, 저장 경로: {tokenizer_path}")

Hugging Face 형식으로 토크나이저 변환

hf_tokenizer = PreTrainedTokenizerFast(
    tokenizer_file=tokenizer_path,
    bos_token="<s>",
    eos_token="</s>",
    unk_token="<unk>",
    pad_token="<pad>",
    mask_token="<mask>"
)
hf_tokenizer.save_pretrained("custom_tokenizer")

print("Hugging Face 형식 토크나이저 저장 완료")

챗봇용 데이터 형식으로 변환

def format_for_chatbot(dataset_split):
    examples = []
    for item in dataset_split:
        # 질문-답변 형식으로 변환
        instruction = item["question"]
        context = item["context"]
        answer = item["answers"]["text"][0] if item["answers"]["text"] else "답변을 찾을 수 없습니다."

        examples.append({
            "instruction": instruction,
            "context": context,
            "response": answer
        })
    return examples

train_chatbot_data = format_for_chatbot(dataset["train"])
valid_chatbot_data = format_for_chatbot(dataset["dev"])

# 챗봇 형식의 데이터 저장
os.makedirs("chatbot_data", exist_ok=True)
with open("chatbot_data/train.json", "w", encoding="utf-8") as f:
    json.dump(train_chatbot_data, f, ensure_ascii=False, indent=2)
with open("chatbot_data/valid.json", "w", encoding="utf-8") as f:
    json.dump(valid_chatbot_data, f, ensure_ascii=False, indent=2)

print("챗봇 데이터 준비 완료")


3. 언어 모델 학습

모델 로드

custom_tokenizer = AutoTokenizer.from_pretrained(
    "./custom_tokenizer",
    trust_remote_code=True
)
# 모델 로드
# unsloth 라이브러리 FastLanguageModel
model, _ = FastLanguageModel.from_pretrained(
    model_name="MLP-KTLim/llama-3-Korean-Bllossom-8B",
    max_seq_length=2048, #2048개의 토큰까지 처리. 대략 4000자
    dtype=None,
    load_in_4bit=True, # 4bit 양자화: 기본 32bit를 1/8로 줄임.
    use_flash_attention_2=False
)

생략된 부분: 모델 학습전에 항상 토크나이저의 어휘사전 크기와 모델의 어휘사전 크기를 비교한다. 또한, 모델의 어휘사전 크기 초기화 뒤에는 실제 값도 초기화하는 과정이 필요하다.

LLoRA 구성

# model = prepare_model_for_kbit_training(model)

# LoRA 구성
lora_config = LoraConfig(
    r=16,  # LoRA에서 사용하는 행렬의 랭크(rank) : 높을수록 표현력 증가, 학습량 증가
    lora_alpha=32,# LoRA 업데이트의 스케일링 계수 : 보통 r의 2배
    # LoRA를 적용할 특정 모듈(레이어)의 이름 목록
    # q_proj, k_proj, v_proj, o_proj: 어텐션 메커니즘의 쿼리, 키, 값, 출력 프로젝션
    # gate_proj, up_proj, down_proj: MLP/FFN 부분의 게이트, 업스케일, 다운스케일 프로젝션
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    lora_dropout=0.05, # LoRA 어댑터에 적용되는 드롭아웃 : 과적합 방지 (보통 0.0~0.1)
    bias="none", # 성능에 큰 영향X 보통 비활성화
    # LoRA가 적용될 태스크 유형 지정
    # "CAUSAL_LM": 인과적 언어 모델링(다음 토큰 예측)
    # "SEQ_CLS": 시퀀스 분류
    # "SEQ_2_SEQ_LM": 시퀀스-투-시퀀스 모델링
    task_type="CAUSAL_LM"
)

# LoRA 어댑터 적용
model = get_peft_model(model, lora_config)

데이터셋(korquad) 로드 및 준비

def prepare_training_data(dataset):
    result = []
    for example in dataset:
        # 맥락과 질문을 결합하여 입력 생성
        if 'context' in example and 'question' in example:
            context = example['context']
            question = example['question']

            # 답변이 있는 경우
            if 'answers' in example and example['answers']['text']:
                answer = example['answers']['text'][0]
                text = f"### 맥락:\n{context}\n\n### 질문:\n{question}\n\n### 답변:\n{answer}"
                result.append({"text": text})
    return result

# 데이터셋 로드 및 준비
korquad = load_dataset("KETI-AIR/korquad", "v1.0")
train_data = prepare_training_data(korquad["train"])
valid_data = prepare_training_data(korquad["dev"])  # dev가 validation 데이터

데이터셋 인스턴스

class TextDataset(Dataset):
    def __init__(self, data, tokenizer, max_length=512):
        self.data = data
        self.tokenizer = tokenizer
        self.max_length = max_length

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

    def __getitem__(self, idx):
        text = self.data[idx]["text"]
        encodings = self.tokenizer(
            text,
            truncation=True,
            max_length=self.max_length,
            padding="max_length",
            return_tensors="pt"
        )

        input_ids = encodings.input_ids[0]
        attention_mask = encodings.attention_mask[0]

        return {
            "input_ids": input_ids,
            "attention_mask": attention_mask,
            "labels": input_ids.clone()
        }

# 데이터셋 인스턴스 생성
train_dataset = TextDataset(train_data, custom_tokenizer)
valid_dataset = TextDataset(valid_data, custom_tokenizer)

언어 모델 학습

# 학습 설정
training_args = TrainingArguments(
    output_dir="./fine_tuned_model",
    per_device_train_batch_size=4,
    per_device_eval_batch_size=4,
    evaluation_strategy="steps",
    eval_steps=500,
    logging_steps=100,
    gradient_accumulation_steps=4,
    num_train_epochs=3,
    weight_decay=0.01,
    warmup_steps=500,
    lr_scheduler_type="cosine",
    learning_rate=2e-4,
    save_steps=1000,
    fp16=True,
    save_total_limit=3,
    load_best_model_at_end=True,
    report_to=["none"]
)

# 트레이너 설정 및 학습 시작
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=valid_dataset
)

# 학습 실행
trainer.train()

# 모델 저장
model.save_pretrained("./final_model")
custom_tokenizer.save_pretrained("./final_model")


댓글 쓰기

다음 이전