Ssul's Blog

[NLP, Fine-Tuning] 허깅페이스(Huggingface) 사용법 본문

AI & ML/사용하기

[NLP, Fine-Tuning] 허깅페이스(Huggingface) 사용법

Ssul 2024. 1. 10. 14:04

0. 허깅페이스는 무엇인가?(huggingface.co)

허깅페이스(Hugging Face)는 인공 지능(AI) 분야에서 자연어 처리(NLP)를 중심으로 한 다양한 딥러닝 모델과 도구들을 제공하는 회사.  오픈소스 라이브러리인 'Transformers'를 통해 유명해짐. 이 라이브러리는 다양한 전처리 방법, 모델 아키텍처(BERT, GPT, T5 등), 그리고 후처리 방법을 포함하여 NLP 분야에서 광범위하게 사용.

- AI관련 깃허브 느낌

- 내가 만든 모델/데이터셋을 Public, private로 올릴수 있고, Public일 경우 누구나 내가 올려놓은 데이터셋, 모델을 사용할수 있음

- 당연히 다른 사람이 만든 언어모델이 Public으로 공개되어 있다면, 나는 해당 모델을 기반으로 파인 튜닝이 가능함.

User가 생성한 모델, 데이터셋이 저장됨

 

 

1. 허깅페이스에서 데이터셋 가져오기

: 알고 있는 3가지의 데이터셋을 가져오기 방법공유. 

첫번째는 깃허브 git clone 처럼 올려져있는 데이터셋/모델 가져와서 사용하는 방법

두번째는 로컬에 있는 자기 데이터 사용하는 방법

세번째는 허깅페이스 허브에서 제공하는 데이터 사용하는 방법

 

1-1. 허깅페이스 다른사람 public 데이터셋 또는 내 허깅페이스 데이터셋 가져오기

- 허깅페이스 나의 데이터셋 페이지에서, 데이터셋 생성(id/test-데이터셋 주소)

- 데이터셋은 아래와 같이, train / valid / test 파일 업로드

허깅페이스id/test에 3개의 데이터파일 업로드

 

from datasets import load_dataset

load_dataset("허깅페이스id/test") #데이터셋 주소(git주소와 동일한 개념)

- load_dataset 함수 가져오고,

- 허깅페이스에 올린, 데이터셋 주소 입력

- 자동으로 train/vaild/test 구분하여 가져오게 됨. 아래와 같이 train/validation/test를 DatasetDict형태로 가져옴

train / validation / test 데이터를 DatasetDict구조로 가져옴

 

1-2. 로컬데이터셋 가져오기

emotions_local = load_dataset("csv", data_files="train.txt", sep=";",
                              names=["text", "label"])

- 로컬에 있는 txt파일에서 데이터셋 가져오기

 

dataset_url = "https://huggingface.co/datasets/transformersbook/emotion-train-split/raw/main/train.txt"
emotions_remote = load_dataset("csv", data_files=dataset_url, sep=";",
                               names=["text", "label"])

- 온라인에 있는 txt파일에서 데이터셋 가져오기

 

 

1-3. 허깅페이스 허브가 제공하는 데이터셋 가져오기

from datasets import load_dataset
from huggingface_hub import list_datasets

all_datasets = [ds.id for ds in list_datasets()]
print(f"현재 허브에는 {len(all_datasets)}개의 데이터셋이 있습니다.")
print(f"처음 10개 데이터셋: {all_datasets[:10]}")

# emotion 데이터셋이 다운로드되지 않으면 SetFit/emotion을 사용합니다.
emotions = load_dataset("emotion")

- 허깅페이스 허브가 제공하는 emotion데이터 가져오기

- load_dataset함수에 이미 정해져있는 데이터셋 키워드를 입력하면 가져올수 있습니다.

 

2. 모델 가져오기

- 다른 사람의 허깅페이스에 공개된 라마2-7b모델을 가져오기

from transformers import AutoModel

model_ckpt = "TinyPixel/Llama-2-7B-bf16-sharded"
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = AutoModel.from_pretrained(model_ckpt).to(device)

- TinyPixel라는 분이 올려놓은 라마2 7b모델을 가져오는 코드입니다. 끝

 

3. 훈련하기

- 모델은 문장을 입력받으면, 6가지 감정(슬픔, 즐거움, 사랑,...)중 1개로 분류하는 모델(예: i love you를 입력받으면, 출력은 2(love를 의미) 

- 두가지 형태의 훈련방법. 하나는 가져온 Pretrain모델은 업데이트 없이 학습하기. 두번째는 pretrain모델도 업데이트 하는 구조

 

3-1. Pretrain모델은 고정, 뒤에 classifier만 업데이트

pretrain고정, classifier업데이트

모델/토크나이저 가져오기 코드

from transformers import AutoModel
from transformers import AutoTokenizer

model_ckpt = "distilbert-base-uncased"
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = AutoModel.from_pretrained(model_ckpt).to(device)
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)

- pretrain모델 가져오고, 모델에 사용된 토크나이져도 함께 가져옵니다.

- transformers의 AutoModel, AutoTokenizer는 모델명 입력만으로, 자동으로 가져옵니다.

 

Pretrain모델의 output확인하기

- clasifier를 붙이기 위해서는 Pretrained모델이 어떤 구조의 값을 뱉어내는지 알아야 합니다.

classifier를 뒤에 붙이려면, 어떤 값이 나오는지 알아야 함

- 직접 i love you를 입력해서 출력값을 확인합니다

text = "i love you"
# 파이토치 텐서로 리턴
inputs = tokenizer(text, return_tensors="pt")
print(f"입력 텐서 크기: {inputs['input_ids'].size()}")

#입력 텐서 크기: torch.Size([1, 5])

inputs

#{'input_ids': tensor([[ 101, 1045, 2293, 2017,  102]]), 'attention_mask': tensor([[1, 1, 1, 1, 1]])}

- 토크나이저를 통해, 5개의 토큰으로 분리되어 입력됨을 확인가능

- 시작토큰, i, love, you, 끝토큰 5개로 구성된것으로 예상됨

 

inputs = {k:v.to(device) for k,v in inputs.items()}
with torch.no_grad():
    outputs = model(**inputs)
print(outputs)

- 모델에 입력. 출력된 구조를 보면 3차원으로 구성되어 있고, last_hidden_state라는 값을 확인할 수 있음

- 출력된 데이터구조를 살펴보면 [1, 5, 768]

- [배치, 토큰, 임베딩차원]으로 1개배치, 5개 토큰, 768차원으로 해석이 가능함

- 이는 시작토큰/i/love/you/끝토큰 5개의 토큰이 입력되면, 모델은 5개의 토큰을 각각 768차원으로 임베딩해서 출력해주는 구조

가져온 pretrain모델의 output

 

classifier붙이기

- output을 확인하였으니, 뒤에 classifier를 붙이자.

- NLP에서 classifier를 보통 시작토큰([CLS])에 학습시킨다.(*허깅페이스 관련글이므로 자세한 내용은 생략)

- 그렇기 때문에 CLS의 결과값만 가지고, classifier를 이어 붙이고, 데이터를 학습시키면, 0-5사이의 분류값으로 학습이 가능하다

시작토큰의 결과값만 사용

def extract_hidden_states(batch):
    # 모델 입력을 GPU로 옮깁니다.
    inputs = {k:v.to(device) for k,v in batch.items()
              if k in tokenizer.model_input_names}
    # 마지막 은닉 상태를 추출합니다.
    with torch.no_grad():
        last_hidden_state = model(**inputs).last_hidden_state
    # [CLS] 토큰에 대한 벡터를 반환합니다.
    return {"hidden_state": last_hidden_state[:,0].cpu().numpy()}

- 결과값 중에서, CLS=시작토큰 의 결과값 뽑아내는 함수 선어

- last_hidden_state[:,0]이 빨강색. 시작토큰의 Pretrain모델의 output 결과값

emotions_hidden = emotions_encoded.map(extract_hidden_states, batched=True)

- 입력한 토큰에 대한 토큰별 768차원의 벡터를 결과값으로 주던 것을, extract_hidden_states함수를 적용하여, cls토큰의 768차원만 반환

X_train = np.array(emotions_hidden["train"]["hidden_state"])
X_valid = np.array(emotions_hidden["validation"]["hidden_state"])

- emotions_hidden의 값은, Pretrain모델을 통과해서, 그 결과값 중 CLS토큰의 결과값만 가져옴

- 해당 데이터를 train/validation으로 구분

 

# 데이터 로더 생성
train_dataset = TensorDataset(X_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

valid_dataset = TensorDataset(X_valid, y_valid)
valid_loader = DataLoader(valid_dataset, batch_size=32)

# 분류기 정의 (768 -> 6, 감성 분류를 위한 레이블 개수)
classifier = nn.Linear(768, 6)
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.Adam(classifier.parameters(), lr=2e-5)

# 모델을 GPU로 옮깁니다 (GPU 사용 가능한 경우)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
classifier.to(device)

# 훈련 함수
def train(model, data_loader, loss_fn, optimizer, device):
    model.train()
    total_loss = 0

    for batch in data_loader:
        inputs, labels = batch
        inputs, labels = inputs.to(device), labels.to(device)

        # Forward pass
        outputs = model(inputs)
        loss = loss_fn(outputs, labels)

        # Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    return total_loss / len(data_loader)

# 검증 함수
def validate(model, data_loader, loss_fn, device):
    model.eval()
    total_loss = 0
    with torch.no_grad():
        for batch in data_loader:
            inputs, labels = batch
            inputs, labels = inputs.to(device), labels.to(device)

            outputs = model(inputs)
            loss = loss_fn(outputs, labels)

            total_loss += loss.item()

    return total_loss / len(data_loader)

# 훈련 및 검증 루프
num_epochs = 3
for epoch in range(num_epochs):
    train_loss = train(classifier, train_loader, loss_fn, optimizer, device)
    valid_loss = validate(classifier, valid_loader, loss_fn, device)
    print(f'Epoch {epoch+1}/{num_epochs}, Train Loss: {train_loss}, Valid Loss: {valid_loss}')

- classifier = nn.Linear(768, 6): 입력된 값의 cls토큰의 최종값 768입력. 이를 6개중 1개의 값으로 뽑아내는 classifier 만들기

- loss_fn = nn.CrossEntropyLoss(): softmax와 X_pred과 y_train의 손실값 계산

- 위 코드를 통해서, pretrain모델의 파라미터는 변화가 없이, classifier파라미터 값만 분류를 잘하는 방향으로 업데이트

 

 

3-2. Pretrain모델, classifier 모두 업데이트

Pretrain모델 + classifier모두 업데이트

- 이미 두개가 합쳐진 모델을 허깅페이스에서 제공

from transformers import AutoModelForSequenceClassification

num_labels = 6
model = (AutoModelForSequenceClassification
         .from_pretrained(model_ckpt, num_labels=num_labels)
         .to(device))

- AutoModelForSequenceClassification을 가져옵니다

 

from sklearn.metrics import accuracy_score, f1_score

def compute_metrics(pred):
    labels = pred.label_ids
    preds = pred.predictions.argmax(-1)
    f1 = f1_score(labels, preds, average="weighted")
    acc = accuracy_score(labels, preds)
    return {"accuracy": acc, "f1": f1}

- 주요 매트릭 정의

- 예측된 결과값을 label과 비교하여, 주요 정확도 및 손실을 확인합니다

 

from transformers import Trainer, TrainingArguments

batch_size = 64
logging_steps = len(emotions_encoded["train"]) // batch_size
model_name = f"{model_ckpt}-finetuned-emotion"
training_args = TrainingArguments(output_dir=model_name,
                                  num_train_epochs=2,
                                  learning_rate=2e-5,
                                  per_device_train_batch_size=batch_size,
                                  per_device_eval_batch_size=batch_size,
                                  weight_decay=0.01,
                                  evaluation_strategy="epoch",
                                  disable_tqdm=False,
                                  logging_steps=logging_steps,
                                  push_to_hub=True,
                                  save_strategy="epoch",
                                  load_best_model_at_end=True,
                                  log_level="error")

- 트레이너의 주요 옵션을 설정합니다.

 

from transformers import Trainer

trainer = Trainer(model=model, args=training_args,
                  compute_metrics=compute_metrics,
                  train_dataset=emotions_encoded["train"],
                  eval_dataset=emotions_encoded["validation"],
                  tokenizer=tokenizer)
trainer.train();

- 학습을 실행합니다

 

preds_output = trainer.predict(emotions_encoded["validation"])

- 훈련을 마친 모델로 예측을 합니다.

 

4. 훈련한 모델 허깅페이스에 업로드

: trainer를 통해 학습한 모델을 허깅페이스에 업로드 합니다(git push와 유사)

trainer.push_to_hub(commit_message="Training completed!")

- 업로드 끝.

 

 

5. 업로드한 모델 사용하기(또는 남의 모델 사용하기)

from transformers import pipeline

# 허브 사용자 이름
model_id = "yourid/distilbert-base-uncased-finetuned-emotion"
classifier = pipeline("text-classification", model=model_id)

- 모델을 가져옵니다

 

custom_msg = "i love you"
preds = classifier(custom_msg, top_k=None)
preds

- i love you를 입력해봅니다

- label2의 확률값이 가장 높음을 알수 있습니다 > love의 감성이 가장 높음을 확인

 

preds_sorted = sorted(preds, key=lambda d: d['label'])
preds_df = pd.DataFrame(preds_sorted)
plt.bar(labels, 100 * preds_df["score"], color='C0')
plt.title(f'"{custom_msg}"')
plt.ylabel("Class probability (%)")
plt.show()

- 그래프로도 확인

 

 

6. 정리

- 허깅페이스는 깃허브와 유사하다. ai학습을 할때 사용되는 모델, 데이터셋을 업로드하고, 함께 사용할수 있다.

- 허깅페이스를 통해 쉽게 pretrain모델을 가져올수 있고,

- 모두를 업데이트하는 학습을 하거나, 일부만 업데이트 하는 학습을 할수 있다.

- 나의 모델을 업로드하고, 공개된 다른 모델들을 쉽게 가져와서 바로 사용할 수 있다.

NLP를 공부한다면, 허깅페이스는 필수!!