HuggingFace Model Fine Truning

해당 페이지는 HuggingFace를 사용한 모델 파인튜닝(Fine-tuning) 과정에 대한 내용을 담고 있습니다. 주요 특징은 다음과 같습니다:

# 목표
- 한국어 분류 데이터셋을 활용한 한국어 토픽 분류 모델 구현해보기
- 데이터 준비부터 미세조정 (파인 튜닝) 훈련까지 흐름 경험
- 모델 토크나이저, FineTuning에 필요한 클래스를 직접 조작하며, 허깅페이스 내부 동작에 대해 좀 더 깊이 있는 실습 진행

import torch

# GPU 사용 여부 확인
device = torch.device('cuda') if torch.cuda.is_available() else "cpu"
print("Using device:", device)

# 추후 Trainer 클래스 확인용

# 1. 데이터셋 로드
- Huggin Face datasets 활용 로드 해보기

# Huggin Face datasets 라이브러리 로드
from datasets import load_dataset

pip install -U datasets huggingface_hub fsspec

### YANT 데이터셋
- 허깅페이스 링크 : <https://huggingface.co/datasets/klue/klue#considerations-for-using-the-data>
- 연합뉴스 (Yonhap News) 기사 제목 (헤드라인)을 7개 주제로 분류
- 레이블 0 ~ 6: 정치 / 경제 / 사회 / 생활,문화 / IT,과학 / 스포츠

# 연합뉴스 기사 제목 데이터셋 로드
raw_datasets = load_dataset('klue', 'ynat')

# klue : 한국어 NLP 전용 종합 벤치마크
# 기계번역, 문장 분류, 개체명 인식 등 8개 태스크 포함

raw_datasets
# DatasetDict : 여러 Dataset을 담는 dict 형태의 클래스 (허깅페이스 제공)

raw_datasets.keys()

len(raw_datasets['train'])

len(raw_datasets['validation'])

# 첫 번째 학습 데이터 출력
raw_datasets['train'][0]

# DataFrame 으로도 확인 가능
raw_datasets['train'].to_pandas()

##### 워크 플로우

- 업스트립 (Upstream): 모델이 일반적인 언어 패턴을 배우는 사전학습 과정
- 다운스트림 (Downstream): 사전 학습된 모델을 특정 작업에 맞게 조정하는 Fine tuning 과정

구성 요소
> 1. Trainer
> - 데이터 전처리부터 학습, 평가, 저장 까지 모든 과정을 한 번에 수행 하는 API
> - 아래 4가지 주요 구성 요소를 조합해 Fine-tuning 워크 플로우 완성 키여야함

> 2. DataCollator
> - 데이터를 배치 단위로 묶어주는 클래스
> - 각 배치의 입력 데이터를 일정한 크기를 맞추기 위해 패딩 처리 등을 수행함

> 3. Tokenizer
> - 사전 학습 된 모델에 맞는 토큰화 도구
> - 업스트림 시 동일한 토크나이저를 사용해 입력 데이터를 처리

> 4. Downstream Task Classes
> - Fine-tuning 대상 모델
> - 사전 학습된 모델을 다양한 NLP 작업에 맞게 활용 할 수 있음

> 5. Training Arguments
> - 학습 과정에서 필요한 하이퍼파라미터들을 설정하는 클래스
> - 학습률, 배치크기, 에포크, 수 등 학습 관련 설정 값을관리

## 2. 토크나이저 로딩
- KoELECTAR 모델로 실습 진행 : monologg/koelectra-base-v3-discriminator 활용

from transformers import AutoTokenizer

model_name = "monologg/koelectra-base-v3-discriminator"

# 사전 학습 된 토크나이저 다운
tokenizer = AutoTokenizer.from_pretrained(model_name, clean_up_tokenization_spaces=True)

# 뉴스 헤드라인 문장에 접근
raw_datasets['train'][0]['title']

temp_result = tokenizer(
    raw_datasets['train'][0]['title'],
    truncation = True, #최대 길이 넘으면 자르기
    max_length = 128 # 토큰최대 길이 설정
    )
# 숫자로 변환 2 : 시작 3 : 끝
temp_result

tokenizer.convert_ids_to_tokens(temp_result['input_ids'][0])

# 토크나이저 함수 정의
def tokenizer_function(example):
  return tokenizer(example['title'], truncation=True, max_length=128)

- 데이터에 토크나이저를 적용하기 위한 함수

> #### map()
> - 변환을 즉시 적용하며, 반환된 결과는 새로운 데이터셋으로 반환됨
> - 토큰화, 라벨 변환 등 데이터 전처리 작업에서 자주 사용

> #### with_transform()
> - 변환을 등록해 두고, 실제 데이터 접근 시 변환을 적용함 (스트림형)
> - 학습을 수행할 때 호출하여 변형이 이루어짐
> - 메모리 절약을 위해, 또는 데이터셋을 한 번에 모두 변형할 필요가 있을때 유용

# 데이터셋에 토크나이저 적용
tokenized_datasets = raw_datasets.map(tokenizer_function,  # 전처리 함수
                 batched=True) # 동작 방식 설정 하나씩 아닌 여러 개 (배치 )씩 모아서 'tokenizer_function'에 전달

tokenized_datasets
# 기존 필드에 'token_type_ids', 'attention_mask' 추가

# features 필드 확인
tokenized_datasets['train'].features

# 원데이터 불필요한 삭제용 칼럼명 추출
remove_columns = [key for key in raw_datasets['train'].features.keys() if key != 'label']
remove_columns

tokenized_datasets = tokenized_datasets.remove_columns(remove_columns)

# 학습에 필요한 레이블만 남음 ('label', 'input_ids', 'token_type_ids', 'attention_mask')
tokenized_datasets

# 3. 모델 불러오기
- Downstream Task Classes 로드 (사전 학습 모델 불러오기)
- 해석하기 쉽게 모델 설정에서 레이블 이름도 등록

# 레이블 명 추출
# 인덱스 순서로대로 레이블 이름 리스트 반환
label_names = tokenized_datasets['train'].features['label'].names
label_names

# enumerate: 인덱스와 원소로 이루어진 튜플 만듦
# 예측값에 대해 해석하기 쉽게 모델에 반영하기 위한 단계
id2label = {i:name for i, name in enumerate(label_names)} # i는 인덱스, name: label 이름
label2id = {name:i for i, name in enumerate(label_names)}

id2label,label2id

from transformers import AutoModelForSequenceClassification

# 사전 학습 모델 로드
model = AutoModelForSequenceClassification.from_pretrained(
    model_name,
    num_labels = len(label_names), # 레이블 개수
)

# 모델 설정
# 모델 설정에 라벨 이름 매핑 업데이트
model.config.id2label = id2label
model.config.label2id = label2id

model.config.id2label, model.config.label2id

# 4. Data Collator & 평가 지표 함수 정의
> Collator
> - Trainer에게 전단해주는 클래스
> - 이때, Transformer 모델 입력은 길이가 다를 수 있는 시퀀스 이므로, 한 배치 내 최대 길이에 맞추어 패딩이 필요

> 평가 지표 함수
> - 학습 중 검증 단계에서 계산할 평가 지표 함수 정의
> - 정확도, 정밀도, 재현율, F1 활용해보기

# 동적으로 배치 내 최대 길이에 맞춰 패딩하여 Trainer 에게 전달해주는 클래스
from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer = tokenizer)

평가 지표 함수를 정의하기 전에 validtaion set 활용해서 test
- 1) 예측에 사용할 모델 입력

model = model.to(device)

device = "cuda" if torch.cuda.is_available() else "cpu"
print("Using : ",device)

# 검증셋에서 샘플 가져오기
sample = tokenized_datasets['validation'][0]
print(sample)

# 모델이 입력으로 전달하기 위해 변환 작업 필요
use_colums = [key for key in tokenized_datasets['validation'].features.keys() if key != 'label']
use_colums

# 1. 처리된 모델 입력값을 담아 둘 딕셔너리 초기화
inputs = {}

# 2. sample 순회
for key,value in sample.items():
  # 3, 사용할 필드 확인 ( 'input_ids', 'token_type_ids', 'attention_mask')
  if key in use_colums:
    tensor = torch.tensor(value) # Pytorch Tensor 로 형변환, Transformer 모델 입력은 Tensor여야만 처리가능
    tensor = tensor.unsqueeze(0).to(device) # 배치 차원 추가
    tensor = tensor.to(device) # gpu 이동

    inputs[key] = tensor

inputs

# 정답 레이블도 같은 방식 적용 (Tensor 변환, gpu 이동)
labels = sample['label']
labels_tensor = torch.tensor([labels])
# 차원을 높혀야하니까 unsqueeze 를 했는데 여기는 필요 없음 >> 정답은 정답만 필요하므로
labels_tensor.to(device)
labels_tensor

# 모델 예측
# 추론 (예측)에 gradient 계산 불필요 -> 메모리 연산 절약을 위해 기울기 계산끄기

with torch.no_grad():
  outputs = model(**inputs)

- 2) 평가지표 활용

import numpy as np
# sklearn metrics는 cpu 메모리 사용 및 numpy 배열 선호

# 로짓(logits) 값을 CPU로 가져와 Numpy 배열로 변환
logits = outputs.logits.cpu().numpy()
logits

# 예측 vs 실제 비교
pred = np.argmax(logits)
print("예측값: ", pred, '->', model.config.id2label[pred])
print("실제값 : ", labels,'->', model.config.id2label[labels])

# sklearn 평가지표 가져오기
from sklearn.metrics import accuracy_score, precision_recall_fscore_support

labels = labels_tensor.cpu().numpy()
labels

# 단일 예측값을 1차원 array 로 변환
preds = np.array([pred])
preds

acc = accuracy_score(labels, preds)
acc

# 정밀도 재현율 f1 계산
prec, rec, f1, _ = precision_recall_fscore_support(
    labels,
    preds,
    average='macro', # 각 클래스별 결과를 계산한 뒤, 모든 클래스를 동일한 비중으로 평균냄
    # (클래스 불균형 상황에도 각 클래스 성능을 동등하게 평가)
    zero_division = 0 # 분모가 0이 되는 경우 결과를 0 으로 처리
)
prec, rec, f1, _

## Trainer에게 전달할 평가지표 함수 정의

def compute_metrics(eval_pred):  # eval_pred : (logits, labels) 튜플
  logits, labels = eval_pred
  preds = np.argmax(logits, axis = 1) # 두번째 축 (클래스 차원)에서 최대값의 인덱스를 찾으라는 뜻

  # 정확도
  acc = accuracy_score(labels, preds)

  # 정밀도, 재현율, f1
  prec, rec, f1, _ = precision_recall_fscore_support(labels, preds, average='macro')
  return {"accruacy : ",acc, "precision : ", prec, "recall : ",rec, "f1 : ",f1}

## 5. Training Arguments 설정

# 배치 사이즈설정
# 모델이 한 번의 학습(훈련) 과정에서 처리할 데이터 수
batch_size = 64

from transformers import TrainingArguments

traning_args = TrainingArguments(
    output_dir = "./data/ynat-model", # 학습 결과 (모델 가중치, 체크포인트 등)를 저장할 경로
    num_train_epochs = 3, # 학습 횟수
    per_device_train_batch_size = batch_size, # 학습에 사용될 배치 크기
    per_device_eval_batch_size = batch_size, # 검증에 사용될 배치 크기
    learning_rate = 5e-5, # 학습률 (BERT 계열에서 흔희 쓰이는 값)
    eval_strategy = "epoch", # 각 epochs 가 끝날때 검증
    logging_steps = 50, # 학습중간에 로그를 찍는간격 (스텝 수)
    save_strategy = "epoch", # 모델 저장 전략 (epchos 마다 저장)
    load_best_model_at_end = True, # 검증 성능이 최고인 체크포인트를 마지막에 불러옴
    metric_for_best_model = "accruacy", # 최고 모델 판단에 사용할 메트릭
    push_to_hub = False, # 학습 모델을 허깅페이스에 바로 저장할건지 ?   False설정
    report_to = 'none' # W&B 비활성화
)

# 6. Trainer 초기화 & Fine - Tuning
- Train 클래스 : 모델 훈련을 간소화 해주는 고수준 API
- Training Arguments 모델 데이터셋 데이터 골레이터 평가 함수 등 인자 학습 루프를 관리

from transformers import Trainer

# 학습 객체 생성
trainer = Trainer(
    model = model,
    args = traning_args,
    train_dataset = tokenized_datasets['train'], # 학습 시 사용할 객체
    eval_dataset = tokenized_datasets['validation'], # 검증시 사용할 객체
    data_collator = data_collator,
    processing_class = tokenizer,
    compute_metrics = compute_metrics
)

trainer.train()

# global_step: 전체 학습 스텝수
# train_sample_per_second : 초당 처리한 샘플수
# train_steps_per_second : 초당 처리한 샘플수
# total_flos: 학습에 사용된 총 부동소수점 연산량

# 6. 모델 평가 및 성능 분석
- 검증 데이터에 대한 최종 성능을 확인과 함께 혼돈 행렬 관측
- trianer.predict로 개별 예측도 가능

metrics = trainer.evaluate()
metrics

# 검증 셋에 대한 최종 평가
trainer.save_metrics("eval", metrics)

- 혼동 행렬을 통해 어떤 주제에서 오분류가 발생하는지 관측

from sklearn.metrics import confusion_matrix

# 검증 셋으로 예측 수행
pred_result = trainer.predict(tokenized_datasets['validation'])
pred_result

# 혼돈행렬 출력
confusion_matrix(pred_result.label_ids, pred_result.predictions.argmax(-1))

- 시각화로 혼돈행렬 관측

import matplotlib.pyplot as plt
plt.rc('font', family ='Gulim')
plt.rc('axes', unicdoe_minus = False) # 마이너스 기호가 깨지지 않도록

label_names # 레이블 이름 리스트

# Figure & Axes(그래프 그릴 영역) 생성
fig, ax = plt.subplots(figsize=(10, 10)) # 전체 그림 크기를 10×10인치로 지정

# 혼동행렬 시각화
# ax.imshow: 2D 배열(혼돈행렬)을 컬러맵으로 시각화
im = ax.imshow(cm, cmap='Blues') # 파란색 계열 색상표 사용

# 컬러바 추가
cbar = fig.colorbar(
    im,               # 시각화된 이미지 객체
    ax = ax,          # 컬러바를 붙일 Axes 지정
    fraction = 0.046, # 컬러바 너비 조절
    pad = 0.04        # 컬러바 간격 조절
)

# 컬러바 축의 레이블 설정
cbar.ax.set_ylabel(
    'Count',        # 레이블 텍스트
    rotation = -90, # 레이블 세로 방향으로 회전
    va = "bottom",  # 수직 정렬을 아래쪽으로
    fontsize = 12   # 글자 크기
)

# 눈금 위치 및 레이블 설정
n = len(label_names)                # 레이블 개수
ax.set_xticks(np.arange(n)) # 눈금(tick) 위치를 0..n-1로 지정
ax.set_yticks(np.arange(n)) # 눈금(tick) 위치를 0..n-1로 지정

# 눈금에 표시할 텍스트 지정
ax.set_xticklabels(label_names, rotation = 45, ha = "right", fontsize = 12) # X축 레이블 45도 회전, 수평 정렬을 오른쪽으로, 글자 크기 12
ax.set_yticklabels(label_names, fontsize = 12)

# 축 제목 및 메인 타이틀
ax.set_xlabel("Predicted Label", fontsize=14)
ax.set_ylabel("True Label", fontsize=14)
ax.set_title("Confusion Matrix (YNAT topic classification)", fontsize = 16, pad = 20) # 전체 그래프 제목 설정
                                                                                      # pad : 제목과 그래프 사이 여백(px)

# 각 셀에 값 표시
threshold = cm.max() / 2                                     # 셀 색 대비가 어두운지 밝은지 판단 기준
for i in range(n):
    for j in range(n):
        color = "white" if cm[i, j] > threshold else "black" # 셀 값이 threshold 이상이면 흰색 텍스트, 아니면 검정색
        ax.text(j, i, cm[i, j], ha = "center", va = "center", color = color, fontsize = 11) # ha, va: 수평·수직 정렬 방식

# 레이아웃 자동 조정 & 출력
plt.tight_layout() # 여백을 자동으로 조절해 레이블 겹침 방지
plt.show()

# 7. Huggin Face 허브에 모델 업로드 하기
- 생성된 모델을 Hugging Face 모델 허브에 업로드 하여 공유 배포 가능

!huggingface-cli login

!git config --global credential.helper store

model.config._name_or_path # 사전학습 모델 이름

# 허깅페이스에 업로드에 시 함께 전달할 정보
kwargs = {
    "finetuned_from": model.config._name_or_path, # 이번에 파인튜닝에 활용한 사전학습 모델 이름/경로
    "tasks": ["text-classification"],             # 이 모델이 수행하는 태스크 종류(여러 개일 경우 리스트로)
    "dataset": ["klue-ynat"],                     # 학습·평가에 사용된 데이터셋 이름
    "tags": [                                     # 모델을 설명하는 태그들 (모델 검색 시 키워드 역할)
        "text-classification",
        "KoELECTRA",
        "Korean-NLP",
        "topic-classification",
        "news-classification"
    ],
    "language": ["ko"],                           # 모델이 지원하는 언어 (ISO 코드)
}

# 모델 업로드
trainer.push_to_hub("byeongrok/ko-news-classifier", **kwargs)

# 8. 업로드된 모델로 파이프라인 사용

from transformers import pipeline