인공지능/자연어 처리

허깅페이스 1 기초 - 모델 부르기, 모델 공유하기, 모델 구성 요소 변경하기

이게될까 2025. 11. 12. 17:38
728x90
728x90

https://huggingface.co/docs/transformers/v4.57.1/quicktour

 

Quickstart

 

huggingface.co

from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf", torch_dtype="auto", device_map="auto")

여기선 safetensors 파일 형식으로 저장된 가중치가 있다면 로드한다.

기존 PyTorch는 보안에 취약한 pickle 유틸맅티로 직렬화 되어있었는데 Safetensor파일은 더 안전하고 속도가 빠름 

 

configuration.py - hidden layer 수, 사전 크기, activation function 등 저장 

modeling.py - 각 레이어의 정의와 레이어 안에서 일어나는 수학적 연산을 정의

 

모델의 타입

  • AutoModel - hidden state를 출력
  • CausalLM - Head가 붙어 특정 작업을 수행 
from transformers import AutoModelForCausalLM, MistralForCausalLM

# AutoClass 또는 모델별 클래스(model-specific class) 를 이용해 로드
model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1", torch_dtype="auto", device_map="auto")
model = MistralForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1", torch_dtype="auto", device_map="auto")

 

Automodel - 모델 구조와 특성을 자동으로 파악해줌 및 다양한 작업에 다른 헤드를 달아서 사용할 수 있음 

from transformers import (
    AutoModelForCausalLM,   # 언어 생성 (텍스트 생성) 용도
    AutoModelForSequenceClassification,  # 분류하기 용도
    AutoModelForQuestionAnswering  # 질의응답 용도
)

# 동일한 체크포인트(meta-llama/Llama-2-7b-hf)지만 작업별로 불러오는 클래스가 다름
model_gen = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
model_cls = AutoModelForSequenceClassification.from_pretrained("meta-llama/Llama-2-7b-hf")
model_qa = AutoModelForQuestionAnswering.from_pretrained("meta-llama/Llama-2-7b-hf")

 

모델을 불러오는 과정은 아래와 같다

  1. random weight로 model 생성
  2. pre-trained weight 로드
  3. weight를 model에 적용

 

모델 저장은 하나에 하는 것이 아니라 

model-00001-of-00003.safetensors, model-00002-of-00003.safetensors 

이런 식으로 각각 파라미터를 나눠서 저장함 

그건 save_pretrained에서 알아서 해주고, 읽을 때도 알아서 해줌

from transformers import AutoModel
import tempfile
import os

model = AutoModel.from_pretrained("biomistral/biomistral-7b")
with tempfile.TemporaryDirectory() as tmp_dir:
    model.save_pretrained(tmp_dir, max_shard_size="5GB")
    print(sorted(os.listdir(tmp_dir)))

 

모델 불러오기

with tempfile.TemporaryDirectory() as tmp_dir:
    model.save_pretrained(tmp_dir)
    new_model = AutoModel.from_pretrained(tmp_dir)

 

모델을 새로 지정하는 것이아니라 기존 모델에 파라미터 덮어 씌우기도 가능

from transformers.modeling_utils import load_sharded_checkpoint

with tempfile.TemporaryDirectory() as tmp_dir:
    model.save_pretrained(tmp_dir, max_shard_size="5GB")
    load_sharded_checkpoint(model, tmp_dir)

 

metadata는 모델의 전체 크기를 제공하며, 각 샤드와 모델의 위치를 지정해줌 

 

device_map은 자동으로 모델의 가중치를 사용 가능한 모든 디바이스에 분산하며 가장 빠른 디바이스(GPU)부터 느린 디바이스(CPU, 하드디스크)로 할당 -> 메모리 사용량과 로딩 시간이 줄어든다. 

hf_device_map 레이어나 모듈이 어디에 올라가있는지 파악

from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    device_map="auto"
)

# 모델이 실제로 각 디바이스에 어떻게 분포되어 있는지 확인
print(model.hf_device_map)

## 출력 
{
    "model.layers.1": 0,
    "model.layers.14": 1,
    "model.layers.31": "cpu",
    "lm_head": "disk"
}

 

torch_dtype : Pytorch는 기본적으로 float32형이기 때문에 다른 데이터 타입으로 바꾸려면 다시 로드를 해야 하기 때문에 추가 메모리가 필요하다 -> 데이터 타입을 명시적으로 설정하여 한번에 하기 

 

trust_remote_code - 허깅페이스에서 기본적으로 악성코드 스캔이 있지만 여전히 존재할 수 있기에 커스텀 모델은 한번 더 물어본다.
물어보는 것이 귀찮으면 True로 두기 

특정 버전을 불러올 수도 있다.

commit_hash = "ed94a7c6247d8aedce4647f00f20de6875b5b292"
model = AutoModelForImageClassification.from_pretrained(
    "sgugger/custom-resnet50d", trust_remote_code=True, revision=commit_hash
)

 

모델 공유하기

모델을 만들기 위해선 configuration을 통해 모델을 초기화하기 위한 구성이 필요하다. 

from transformers import PretrainedConfig
from typing import List

# 이미지 인식 모델 ResNet을 위한 설정(Config) 클래스 선언  
class ResnetConfig(PretrainedConfig):
    # Hugging Face 모델 타입 명시 (모델 저장/호환시 사용)
    model_type = "resnet"

    def __init__(
        self,
        block_type="bottleneck",    # 블록 종류: basic(간단) or bottleneck(깊은 네트워크에 적합)
        layers: list[int] = [3, 4, 6, 3],    # 각 stage별 block 개수 (예: ResNet-50 구조)
        num_classes: int = 1000,        # 분류 예측 클래스 개수 (이미지넷 기준 1000)
        input_channels: int = 3,        # 입력 채널 수 (RGB 이미지면 3)
        cardinality: int = 1,           # 그룹 합성곱에 쓰이는 그룹 개수 (ResNeXt 용)
        base_width: int = 64,           # 블록 내부 합성곱의 기본 channel width
        stem_width: int = 64,           # 입력 레이어(stem) channel 크기
        stem_type: str = "",            # stem 레이어 구조 선택 ("", "deep", "deep-tiered")
        avg_down: bool = False,         # 다운샘플링(스트라이드)시 avgpool 활용 여부 (성능 옵션)
        **kwargs,                       # 추가 파라미터 (상위 PretrainedConfig에서 관리)
    ):
        # block_type validation: 올바른 값 아니면 에러 발생
        if block_type not in ["basic", "bottleneck"]:
            raise ValueError(f"`block_type` must be 'basic' or bottleneck', got {block_type}.")
        # stem_type validation
        if stem_type not in ["", "deep", "deep-tiered"]:
            raise ValueError(f"`stem_type` must be '', 'deep' or 'deep-tiered', got {stem_type}.")

        # 각 파라미터를 객체 속성으로 저장
        self.block_type = block_type
        self.layers = layers
        self.num_classes = num_classes
        self.input_channels = input_channels
        self.cardinality = cardinality
        self.base_width = base_width
        self.stem_width = stem_width
        self.stem_type = stem_type
        self.avg_down = avg_down

        # 나머지 인자들은 부모 클래스(PretrainedConfig)에 전달
        super().__init__(**kwargs)

PretrainedConfig를 상속해야 하며 __init__의 모든 kwargs를 허용하며 상위 클래스 __init__에 전달되어야 한다.

# 모델 만들고 저장하기

resnet50d_config = ResnetConfig(block_type="bottleneck", stem_width=32, stem_type="deep", avg_down=True)
resnet50d_config.save_pretrained("custom-resnet")

# 모델 로드하기 

resnet50d_config = ResnetConfig.from_pretrained("custom-resnet")

 

이제 모델 구성이 있으니 작성할 수 있다. 

하나는 분류, 하나는 hidden features 추출이다.

from transformers import PreTrainedModel   # Hugging Face 모델 표준 클래스
from timm.models.resnet import BasicBlock, Bottleneck, ResNet   # timm 라이브러리의 ResNet 관련 클래스
from .configuration_resnet import ResnetConfig   # Custom Config 클래스 (아까 작성한 클래스)

# 블록 종류에 따라 대응되는 클래스 매핑 (ResNet의 기본/병목 블록)
BLOCK_MAPPING = {
    "basic": BasicBlock,
    "bottleneck": Bottleneck
}

# Hugging Face 프레임워크와 호환되는 Custom ResNet 모델 클래스
class ResnetModel(PreTrainedModel):
    # 사용할 config 클래스 명시 (후에 모델 저장/로드시 자동 연결됨)
    config_class = ResnetConfig

    def __init__(self, config):
        # 부모 클래스 초기화 (Hugging Face 표준 로직 사용)
        super().__init__(config)
        # 설정(config)에 따라 사용할 블록 클래스 결정 ("basic" 또는 "bottleneck")
        block_layer = BLOCK_MAPPING[config.block_type]
        # 실제 timm의 ResNet 모델 생성
        self.model = ResNet(
            block_layer,                     # 사용할 블록 종류
            config.layers,                   # 각 스테이지별 블록 개수 (e.g. [3,4,6,3])
            num_classes=config.num_classes,  # 분류 클래스 수
            in_chans=config.input_channels,  # 입력 채널 수 (보통 3, RGB)
            cardinality=config.cardinality,  # 그룹 합성곱 그룹 수 (ResNeXt에서 사용)
            base_width=config.base_width,    # 블록 내부 기본 채널 수
            stem_width=config.stem_width,    # 입력(stem) 레이어 채널 수
            stem_type=config.stem_type,      # stem 구조 타입 ("", "deep" 등)
            avg_down=config.avg_down,        # 다운샘플링시 avgpool 사용할지 여부
        )

    def forward(self, tensor):
        # 입력 텐서에 대한 모델의 forward 과정 정의
        # timm ResNet의 features 추출 함수 사용 (forward_features)
        return self.model.forward_features(tensor)

이제 분류 모델에는 forward 메소드만 마지막에 변경하면 된다. 

import torch

# 이미지 분류 전용 Custom Hugging Face 모델 클래스
class ResnetModelForImageClassification(PreTrainedModel):
    # 사용할 Config 클래스 명시 (후에 모델 저장/로드/배포시 자동 연결됨)
    config_class = ResnetConfig

    def __init__(self, config):
        # 부모 클래스 초기화 (Hugging Face 프레임워크 내부 표준 로직)
        super().__init__(config)
        # config에 등록된 block_type에 따라 BasicBlock/Bottleneck 선택
        block_layer = BLOCK_MAPPING[config.block_type]
        # 실제 timm의 ResNet 인스턴스 생성 (입력 config 기반 구조 설정)
        self.model = ResNet(
            block_layer,                      # 사용할 블록 유형
            config.layers,                    # 각 stage별 블록 개수 (리스트)
            num_classes=config.num_classes,   # 분류 예측 클래스 수 (예: 이미지넷 1000)
            in_chans=config.input_channels,   # 입력 채널 수 (RGB=3)
            cardinality=config.cardinality,   # 그룹 합성곱 그룹 개수 (ResNeXt 용)
            base_width=config.base_width,     # 블록 내부 채널수 기본값
            stem_width=config.stem_width,     # 입력 레이어(stem) 채널 크기
            stem_type=config.stem_type,       # stem 구조 형태 ("", "deep", "deep-tiered")
            avg_down=config.avg_down,         # 다운샘플링시 avgpool 활용 여부
        )

    def forward(self, tensor, labels=None):
        # 입력 이미지를 모델에 통과시켜 예측 logits(클래스별 스코어) 획득
        logits = self.model(tensor)
        # labels가 들어온 경우 (학습 or eval시 정답 레이블이 있음)
        if labels is not None:
            # cross_entropy로 loss 계산 (예측값=logits, 정답=labels)
            loss = torch.nn.functional.cross_entropy(logits, labels)
            # 평소 Hugging Face 모델 출력 형태에 맞춰 딕셔너리로 반환 (loss 포함)
            return {"loss": loss, "logits": logits}
        # labels가 없으면 inference 용 (logits만 반환)
        return {"logits": logits}

지정해둔 ResNet에서 연산도 다 진행됨 

 

# 모델 생성하기 
resnet50d = ResnetModelForImageClassification(resnet50d_config)

# 모델 가중치 불러오기
# 동일한 모델 형태이기 때문에 가중치 로드 가능 
import timm

pretrained_model = timm.create_model("resnet50d", pretrained=True)
resnet50d.model.load_state_dict(pretrained_model.state_dict())

 

Hub로 코드 업로드 하기 위해선 modeling과 config python 코드가 모두 같은 경로안에 있어야 함

.
└── resnet_model
    ├── __init__.py
    ├── configuration_resnet.py
    └── modeling_resnet.py

 

# 모델과 구성 import 하기 
from resnet_model.configuration_resnet import ResnetConfig
from resnet_model.modeling_resnet import ResnetModel, ResnetModelForImageClassification

# 저장하기
ResnetConfig.register_for_auto_class()
ResnetModel.register_for_auto_class("AutoModel")
ResnetModelForImageClassification.register_for_auto_class("AutoModelForImageClassification")

모델일 경우에는 AutoModel 지정해줘야 함 

 

구성과 모델 작성

resnet50d_config = ResnetConfig(block_type="bottleneck", stem_width=32, stem_type="deep", avg_down=True)
resnet50d = ResnetModelForImageClassification(resnet50d_config)

pretrained_model = timm.create_model("resnet50d", pretrained=True)
resnet50d.model.load_state_dict(pretrained_model.state_dict())

 

resnet50d.push_to_hub("custom-resnet50d")

이런 형식을 통해 모델 저장 가능함 

 

이제 이렇게 불러올 수 있음 

from transformers import AutoModelForImageClassification

model = AutoModelForImageClassification.from_pretrained("sgugger/custom-resnet50d", trust_remote_code=True)

 

구성 요소 변경

모델을 새로 작성하는 대신 구성 요소를 수정하여 모델 맞춤 설정하는 방버이 있음 => 이를 통해 특정 사용 사례에 맞게 모델 조정 가능함

clear import cache를 통해 캐시된 트랜스포머 모듈을 제거하여 Python이 환경을 재시작하지 않아도 수정된 코드를 다시 가져올 수 있음

from transformers import AutoModel
from transformers.utils.import_utils import clear_import_cache

model = AutoModel.from_pretrained("bert-base-uncased")
# 모델 코드 수정
# 캐시를 지워 수정된 코드를 다시 가져오기
clear_import_cache()
# 업데이트된 코드를 사용하기 위해 다시 가져오기
model = AutoModel.from_pretrained("bert-base-uncased")

원래는 qkv를 하나의 큰 Linear로 처리하지만 LoRA를 위해 3개의 각각 q, k, v로 나눠준다. 

import torch
import torch.nn as nn
from transformers.models.sam.modeling_sam import SamVisionAttention

class SamVisionAttentionSplit(SamVisionAttention, nn.Module):
    def __init__(self, config, window_size):
        super().__init__(config, window_size)
        # 결합된 qkv 제거
        del self.qkv
        # q, k, v 개별 프로젝션 생성
        self.q = nn.Linear(config.hidden_size, config.hidden_size, bias=config.qkv_bias)
        self.k = nn.Linear(config.hidden_size, config.hidden_size, bias=config.qkv_bias)
        self.v = nn.Linear(config.hidden_size, config.hidden_size, bias=config.qkv_bias)
        self._register_load_state_dict_pre_hook(self.split_q_k_v_load_hook)

개별로 만들어준다!

정의만 되어있는 상태로 아직 사전학습 된 가중치는 할당되지 않았음 

 

    def split_q_k_v_load_hook(self, state_dict, prefix, *args):
        keys_to_delete = []
        for key in list(state_dict.keys()):
            if "qkv." in key:
                # 결합된 프로젝션에서 q, k, v 분리
                q, k, v = state_dict[key].chunk(3, dim=0)
                # 개별 q, k, v 프로젝션으로 대체
                state_dict[key.replace("qkv.", "q.")] = q
                state_dict[key.replace("qkv.", "k.")] = k
                state_dict[key.replace("qkv.", "v.")] = v
                # 기존 qkv 키를 삭제 대상으로 표시
                keys_to_delete.append(key)
        
        # 기존 qkv 키 제거
        for key in keys_to_delete:
            del state_dict[key]

기존에 합쳐진 가중치를 각각 나눠서 전달해주는 역할을 한다. 

 

    def forward(self, hidden_states: torch.Tensor, output_attentions=False) -> torch.Tensor:
        batch_size, height, width, _ = hidden_states.shape
        qkv_shapes = (batch_size *  self.num_attention_heads,  height * width, -1)
        query = self.q(hidden_states).reshape((batch_size,  height * width,self.num_attention_heads, -1)).permute(0,2,1,3).reshape(qkv_shapes)
        key = self.k(hidden_states).reshape((batch_size,  height * width,self.num_attention_heads, -1)).permute(0,2,1,3).reshape(qkv_shapes)
        value = self.v(hidden_states).reshape((batch_size,  height * width,self.num_attention_heads, -1)).permute(0,2,1,3).reshape(qkv_shapes)

        attn_weights = (query * self.scale) @ key.transpose(-2, -1)

        attn_weights = torch.nn.functional.softmax(attn_weights, dtype=torch.float32, dim=-1).to(query.dtype)
        attn_probs = nn.functional.dropout(attn_weights, p=self.dropout, training=self.training)
        attn_output = (attn_probs @ value).reshape(batch_size, self.num_attention_heads, height, width, -1)
        attn_output = attn_output.permute(0, 2, 3, 1, 4).reshape(batch_size, height, width, -1)
        attn_output = self.proj(attn_output)

        if output_attentions:
            outputs = (attn_output, attn_weights)
        else:
            outputs = (attn_output, None)
        return outputs

Attention 연산을 통해 forward하는 함수를 정의해준다. 

 

from transformers import SamModel

# 사전 훈련된 SAM 모델 가져오기
model = SamModel.from_pretrained("facebook/sam-vit-base")

# 비전-인코더 모듈에서 어텐션 클래스 교체
for layer in model.vision_encoder.layers:
    if hasattr(layer, "attn"):
        layer.attn = SamVisionAttentionSplit(model.config.vision_config, model.config.vision_config.window_size)

이제 이렇게 변경해줄 수 있다. 

 

이제 LoRA를 단다.

from peft import LoraConfig, get_peft_model

config = LoraConfig(
    r=16,                  # 추가되는 저랭크 행렬의 차원(랭크)
    lora_alpha=32,         # Scaling factor; 보통 r과 비슷하게 설정
    target_modules=["q", "v"],   # LoRA를 적용할 모듈 이름(q, v에 각각 적용)
    lora_dropout=0.1,      # 학습시 드롭아웃 적용해 일반화
    task_type="FEATURE_EXTRACTION"
)

이제 LoRA config를 준비해뒀으니 모델에 달아보자

model = get_peft_model(model, config)
model.print_trainable_parameters()
"trainable params: 589,824 || all params: 94,274,096 || trainable%: 0.6256"

이제 이러고 학습하면 된다!

728x90