prompt tuning은 prefix tuning과 비슷한 아이디어를 가지지만, 전반적으로 간소화한 형태의 파인튜닝 방법이다. Prompt Tuning.은 특정한 다운스트림 테스크를 수행하기 위해 언어모델의 가중치를 고정하고, 개별 테스크마다 프롬프트의 파라미터를 업데이트 하는 soft prompt의 학습 방식을 사용한다.
prompt?
프롬프트는 생성 방법에 따라 크게 Manual-Search Prompt와 Auto-Search Prompt 두가지로 나뉜다.
Manual-Search Prompt
매뉴얼한 방식으로 각 태스크마다 적절한 프롬프트를 찾아주는 것이다. 직관적이고 모델 학습에 효과적이지만 적절한 프롬프트를 찾기까지 시간이 많이 들고, 프롬프트를 생성하는 사람에 따라 편차가 크다는 단점
Auto-Search Prompt
이 부분에서는 정답 레이블의 확률이 가장 높아지는 토큰을 gradient-guided search 방법으로 찾아 이를 프롬프트로 정의한다. 토큰을 찾는 방식은 기존의 연구인 UniversalTriggers에서 영감을 얻었으며, 모델의 성능 하락을 목표로 하는 Trigger Token Search 방식을 역으로 이용해 정답 확률을 높이는 토큰을 찾아 프롬프트로 정의한다.
프롬프트의 형태는 Hard Prompt와 Soft Prompt로 나눌 수 있다.
초기에는 사람이 직관적으로 이해할 수 있는 Hard Prompt가 주로 연구되었다. 주로 자연어로 구성되며, 주어진 태스크에 맞게 사람 혹은 모델이 적절한 프롬프트를 찾는 과정을 거친다. 하지만 이렇게 정의된 하드 프롬프트는 Suboptimal하다는 문제점이 있었다. 인공지는 모델은 이산적인 값이 아닌 연속적인 값으로 학습되기 때문이다. 이에 연속적이고, 조정가능한 Soft Prompt가 등장하였고 관련 연구가 활발히 이루어졌다.
Soft Prompt와 관련된 주요 연구로는 Prompt Tuning과 Prefix Tuning 두가지가 있다. 두 논문 모두 Soft Prompt를 이용하여 기존의 파인튜닝 대비 효과적인 학습이 가능하다는 점을 주요 포인트로 하고 있다.
고려사항
Prompt Tuning의 고려사항은 다음의 2가지와 같다.
Prompt 표현의 초깃값을 무엇으로 할 것인가?
Prompt 표현의 초깃값은 3가지 방법이 있다. 첫번째 방법은 무작위 초기화(random initialization)를 사용해 처음부터 모델을 훈련하는 것이다. 두번째 방법은 개별 프롬프트 토큰을 모델의 vocabulary에서 추출한 임베딩으로 초기화하는 것이다. 세번째는 프롬프트를 출력 클래스를 나열하는 임베딩으로 초기화하는 방법이다. 테스트 결과에 따르면, 무작위 초기화를 해주는 것보다 vocabulary 또는 출력 클래스의 임베딩으로 초기화를 해주는 것이 더 좋은 성능이 나왔다.
Prompt의 길이를 얼마로 할 것인가?
Prompt의 길이를 위한 파라미터의 비용은 EPEP로 계산한다. EE는 토큰 임베딩의 차원이고, PP는 프롬프트의 길이다. 프롬프트의 길이가 더 짧을수록, 새로운 파라미터가 더 적게 튜닝되어야 한다. 연구결과에 따르면, Prompt의 길이는 5~100 사이의 값이 적절한 것으로 나왔다.
Results
Prompt Tuning의 성능은 아래의 왼쪽 그림과 같다. Prompt Tuning은 모델의 크기가 커질수록 성능이 향상된다. 특히, 모델의 크기가 수십억 개의 파라미터를 초과하는 경우, Model Tuning과 거의 동일한 성능을 보인다.
오른쪽의 그림은 파인 튜닝에 필요한 파라미터의 개수를 비교한 결과다. Model Tuning이 100%의 파라미터를 갖고 있다면, Prompt Tuning은 0.001%의 파라미터를 가지고 Model Tuning만큼의 성능을 낼 수 있다. Prefix Tuning이 0.1%로 파라미터를 학습하는 것과 비교할 때, Prompt Tuning은 아주 적은 파라미터로 효율적인 Fine-Tuning이 가능해진다. 즉, Prompt Tuning이 대규모의 언어모델을 특정한 다운스트림 태스크에 적용하는 데 효율적으로 사용될 수 있다.
다른 PEFT 방법과 비교
Prefix tuning
Prefix tuning은 모든 트랜스포머 레이어에 접두어 시퀀스를 붙여 학습시키는 방법이다. Prefix tuning은 트랜스포머의 모든 계층에 접두어를 붙이는 반면, Prompt Tuning은 input 앞에 접두어를 붙여 하나의 프롬프트 표현을 사용한다. 즉, Prompt Tuning은 Prefix tuning 보다 더 적은 파라미터로 모델의 Fine-Tuning이 가능하다. 예를 들어, BART를 사용할 때, Prefix tuning은 인코더와 디코더 네트워크에 모두 접두사를 붙인다. 그러나, Prompt Tuning은 인코더의 프롬프트에만 접두어를 붙인다.
P-tuning
P-tuning은 인간이 디자인한 패턴을 사용해 학습이 가능한 연속적인 prompt를 input 전체에 삽입하는 방법이다. P-tuning은 입력 전체에 연속적인 prompt를 삽입하는 반면, Prompt Tuning은 접두어로 붙이기 때문에 P-tuning보다 단순해진다. 또한, P-tuning은 프롬프트와 모델의 주요 파라미터를 같이 업데이트하는 반면, Prompt Tuning은 언어모델을 고정된 상태로 사용하기 때문에 더 효율적이다.
직접 실행해본 결과 Prompt Tuning은 Text model에만 가능한 것으로 확인, FourierFT로 전환하였음
FourierFT
핵심 원리와 과정
가중치 변화 표현:
기존의 미세 조정 방법(예: LoRA)은 가중치 변화를 저차원 행렬 형식(∆W = BA)으로 표현한다. 반면, FourierFT는 이러한 변화를 주파수 도메인에서 푸리에 계수를 사용하여 표현한다.
핵심 아이디어는 가중치 변화 행렬(∆W)을 공간 도메인의 신호로 간주하고, 이를 푸리에 변환을 통해 주파수 도메인에서 표현하는 것이다.
스펙트럼 계수 학습:
전체 가중치 변화 행렬을 학습하는 대신, FourierFT는 소수의 푸리에 계수에 집중하여 학습한다. 이 계수들은 랜덤하게 선택되며, 모델의 모든 레이어에서 공유되기 때문에 학습해야 할 파라미터 수가 줄어든다.
학습된 푸리에 계수는 공간 도메인 행렬을 재구성하기 위해 역 푸리에 변환이 적용되어 미세 조정에 필요한 가중치 변화를 나타낸다.
구현:
스펙트럼 항목(E)의 행렬은 무작위로 초기화되며, 고정된 상태로 유지된다. 이 항목에 해당하는 계수만이 학습 가능하다.
역 이산 푸리에 변환(IDFT)이 이러한 스펙트럼 계수에 적용되어 공간 도메인 가중치 변화를 얻으며, 이는 사전 학습된 가중치와 결합되어 모델을 업데이트한다.
파라미터 효율성:
FourierFT는 학습해야 하는 파라미터 수를 기존 방법에 비해 크게 줄여준다. 예를 들어, LoRA는 저차원 행렬을 저장해야 하지만, FourierFT는 소수의 스펙트럼 계수와 이러한 계수가 적용되는 항목만을 저장하면 된다.
모델의 깊이와 너비가 커질수록 이 방법의 효율성은 더욱 증가한다.
FourierFT는 어떻게 분류를 수행하는가?
FourierFT는 자연어 처리 및 이미지 분류와 같은 작업에 대해 모델의 미세 조정을 강화함으로써, 특정 작업에서의 성능을 향상시킨다. 주파수 도메인에 중점을 둠으로써, 학습의 복잡성을 줄이면서 중요한 패턴을 포착한다.
전방 전달(Forward Pass):
전방 전달 과정에서 Fourier 변환된 가중치 변화가 입력 데이터에 적용되며, 예측된 결과는 실제 레이블과 비교되어 손실이 계산된다.
역전파(backpropagation)는 Fourier 계수를 조정하여 모델의 특정 작업에 대한 성능을 향상시킨다.
역 푸리에 변환:
학습된 스펙트럼 계수는 다시 공간 도메인으로 변환되어 모델의 가중치를 업데이트하며, 이를 통해 모델이 데이터를 분류할 수 있다.
FourierFT의 장점
파라미터 효율성:
FourierFT는 미세 조정을 위해 필요한 파라미터 수를 크게 줄여 저장 및 계산 측면에서 매우 효율적이다.
유연성:
FourierFT는 모델의 다양한 레이어에 적용할 수 있으며, LoRA와 달리 저차원 근사에 국한되지 않는다.
성능:
파라미터를 크게 줄였음에도 불구하고, FourierFT는 다른 미세 조정 방법들과 비교하여 동등하거나 더 나은 성능을 제공한다. 특히 LLaMA-2와 Vision Transformer(ViT)와 같은 대형 모델에서 효과적이다.
FourierFT의 단점
복잡성:
FourierFT를 구현하려면 푸리에 분석과 모델 아키텍처에 대한 깊은 이해가 필요하기 때문에 일부 사용자에게는 접근성이 떨어질 수 있다.
스펙트럼 계수 초기화:
스펙트럼 계수와 그 초기화 방법이 성능에 영향을 미칠 수 있다. 논문에서는 무작위 초기화를 제안하지만, 최적의 초기화 전략을 찾는 것은 어려울 수 있다.
도메인 특화 최적화:
FourierFT의 효과는 작업의 특성에 따라 다를 수 있다. 주파수 도메인 분석이 자연스럽지 않은 작업에서는 이 접근법이 그다지 큰 이점을 제공하지 못할 수 있다.
결론
FourierFT는 푸리에 기저 함수의 표현력을 활용하여 효율적으로 모델을 미세 조정하는 강력한 방법이다. 이는 특히 대형 모델에서 저장 및 계산 자원이 중요한 경우에 유리하다. 그러나 구현의 복잡성과 신중한 조정이 필요한 점은 고려해야 한다. FourierFT는 대형 모델을 효율적으로 학습시키면서도 성능을 극대화할 수 있는 방법으로, 특히 이미지와 같은 고차원 데이터의 특징을 잘 포착하고 분석하는 데 유리하다.
Code로 알아보는 FourierFT
from datasets import load_dataset
from transformers import AutoImageProcessor, AutoModelForImageClassification, TrainingArguments, Trainer
import torch
from torchvision.transforms import (
RandomHorizontalFlip,
RandomResizedCrop,
ToTensor,
Normalize,
Compose
)
import numpy as np
from PIL import Image
import logging
# Initialize logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Load dataset
dataset = load_dataset("FastJobs/Visual_Emotional_Analysis")
labels = dataset["train"].features["label"].names
label2id = {label: i for i, label in enumerate(labels)}
id2label = {i: label for i, label in enumerate(labels)}
# Image processor and transformations
image_processor = AutoImageProcessor.from_pretrained("google/vit-base-patch16-224-in21k")
normalize = Normalize(mean=image_processor.image_mean, std=image_processor.image_std)
train_transforms = Compose(
[
RandomResizedCrop(image_processor.size["height"]),
RandomHorizontalFlip(),
ToTensor(),
normalize,
]
)
def fourier_transform(image):
# Convert image to numpy array
image = np.array(image)
# Apply Fourier Transform
image_fft = np.fft.fft2(image, axes=(-2, -1))
image_fft_shifted = np.fft.fftshift(image_fft)
# Take magnitude spectrum
magnitude_spectrum = np.abs(image_fft_shifted)
# Normalize the magnitude spectrum
magnitude_spectrum = np.log(magnitude_spectrum + 1e-9) # Avoid log(0)
magnitude_spectrum = (magnitude_spectrum - np.min(magnitude_spectrum)) / (np.max(magnitude_spectrum) - np.min(magnitude_spectrum))
return Image.fromarray(np.uint8(magnitude_spectrum * 255))
def preprocess_train(example_batch):
# Apply Fourier Transform to each image
transformed_images = [fourier_transform(image.convert("RGB")) for image in example_batch["image"]]
transformed_images = [train_transforms(image) for image in transformed_images]
return {"pixel_values": transformed_images, "labels": example_batch["label"]}
# Preprocess train dataset
train_dataset = dataset["train"].map(preprocess_train, batched=True)
# Load the ViT model
model = AutoModelForImageClassification.from_pretrained(
"google/vit-base-patch16-224-in21k",
num_labels=len(labels),
label2id=label2id,
id2label=id2label,
)
# Training arguments
training_args = TrainingArguments(
output_dir="./results",
save_strategy="epoch",
learning_rate=5e-4,
per_device_train_batch_size=4,
num_train_epochs=2,
weight_decay=0.01,
logging_steps=10,
save_total_limit=2,
load_best_model_at_end=False,
logging_dir='./logs',
)
# Data collator definition
def collate_fn(examples):
# Ensure each example is a tensor
pixel_values = torch.stack([torch.tensor(example["pixel_values"]) if not isinstance(example["pixel_values"], torch.Tensor) else example["pixel_values"] for example in examples])
labels = torch.tensor([example["labels"] for example in examples], dtype=torch.long)
return {"pixel_values": pixel_values, "labels": labels}
# Trainer instance
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
data_collator=collate_fn,
)
# Training
trainer.train()
# Save the trained model and image processor locally
model.save_pretrained("./trained_model")
image_processor.save_pretrained("./trained_model")
Resolving data files: 100%
800/800 [00:00<00:00, 927.34it/s]
Fast image processor class <class 'transformers.models.vit.image_processing_vit_fast.ViTImageProcessorFast'> is available for this model. Using slow image processor class. To use the fast image processor class set `use_fast=True`.
Some weights of ViTForImageClassification were not initialized from the model checkpoint at google/vit-base-patch16-224-in21k and are newly initialized: ['classifier.bias', 'classifier.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
[400/400 04:36, Epoch 2/2]
Step Training Loss
10 2.160000
20 2.053400
30 2.142000
40 2.145600
50 2.051700
60 2.139700
70 2.103000
80 2.129600
90 2.093900
100 2.094400
110 2.096400
120 2.104300
130 2.104000
140 2.139900
150 2.154400
160 2.078200
170 2.093600
180 2.106000
190 2.122700
200 2.098700
210 2.077100
220 2.097800
230 2.092300
240 2.082300
250 2.099700
260 2.042500
270 2.085100
280 2.115800
290 2.123800
300 2.078600
310 2.068100
320 2.078000
330 2.079200
340 2.082300
350 2.088700
360 2.085400
370 2.077700
380 2.070900
390 2.099000
400 2.098600
['./trained_model/preprocessor_config.json']
from transformers import AutoImageProcessor, AutoModelForImageClassification
from PIL import Image
import torch
import os
import matplotlib.pyplot as plt
# 로그 테스트 - logging이라는 라이브러리가 작동하지 않아 print문으로 대체하였음!
print("로그 테스트 - 이 메시지가 출력되면 로그 설정이 정상적으로 작동하는 것입니다.")
# 모델과 이미지 프로세서 로드
try:
print("모델 로드 중...")
model_path = "./trained_model"
if not os.path.exists(model_path):
print("모델 경로가 존재하지 않습니다:", model_path)
raise FileNotFoundError(f"모델 경로가 존재하지 않습니다: {model_path}")
model = AutoModelForImageClassification.from_pretrained(model_path, num_labels=8)
#num_labels를 설정하지 않으면 오류가 생긴다!!!
image_processor = AutoImageProcessor.from_pretrained(model_path)
print("모델 및 이미지 프로세서 로드 완료")
except Exception as e:
print("모델 로드 실패:", e)
raise e
# 이미지 파일 경로
uploaded_image_path = "./content/amusement.jpg"
# 이미지 로드 및 전처리
try:
print("이미지 로드 중...")
if not os.path.exists(uploaded_image_path):
print("이미지 파일이 존재하지 않습니다:", uploaded_image_path)
raise FileNotFoundError(f"이미지 파일이 존재하지 않습니다: {uploaded_image_path}")
image = Image.open(uploaded_image_path)
encoding = image_processor(images=image.convert("RGB"), return_tensors="pt")
print("이미지 전처리 완료")
except Exception as e:
print("이미지 로드 및 전처리 실패:", e)
raise e
# 모델을 동일한 디바이스에 이동
try:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"디바이스: {device}")
model.to(device)
encoding = {k: v.to(device) for k, v in encoding.items()}
print("디바이스로 이동된 encoding:", encoding)
except Exception as e:
print("디바이스 이동 실패:", e)
raise e
# 모델 예측 수행
try:
print("모델 예측 수행 중...")
model.eval()
with torch.no_grad():
outputs = model(**encoding)
logits = outputs.logits
print("예측 완료")
except Exception as e:
print("모델 예측 실패:", e)
raise e
# 예측된 클래스 출력
try:
predicted_class_idx = logits.argmax(-1).item()
predicted_class = model.config.id2label[predicted_class_idx]
print("Predicted class:", predicted_class)
# 이미지 출력
plt.figure()
plt.imshow(image)
plt.title(f"Predicted class: {predicted_class}")
plt.axis('off') # 축 제거
plt.show()
except Exception as e:
print("예측된 클래스 출력 실패:", e)
raise e