인공지능/공부

허깅페이스 2 기초 - Transformers 모듈, 모델 추가

이게될까 2025. 11. 12. 22:00
728x90
728x90

2025.11.12 - [인공지능/자연어 처리] - 허깅페이스 1 기초 - 모델 부르기, 모델 공유하기, 모델 구성 요소 변경하기

 

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

https://huggingface.co/docs/transformers/v4.57.1/quicktour Quickstart huggingface.cofrom transformers import AutoModelForCausalLMmodel = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf", torch_dtype="auto", device_map="auto")여기선 safete

yoonschallenge.tistory.com

여기서 이어지는 남는 시간에 Transformers 공부하기

 

모듈화를 통해 조금만 수정하고, 그대로 사용할 부분은 그대로 상속받는다!

더보기

쉽게 풀어서 설명

이 문단은 🤗 Transformers 라이브러리의 "디자인 철학"과 기존 구조의 한계점, 그리고 이를 개선하기 위한 모듈식 트랜스포머 구조의 취지를 다룹니다.


1. 기존 방식: "단일 모델, 단일 파일"

  • 기존에는 모델마다 하나의 파일에 코드가 모두 들어감(예: modeling_bert.py, modeling_roberta.py 등).
  • 이렇게 하면 각 모델이 완전히 독립적으로 동작하지만, 같거나 비슷한 코드가 여러 파일에 중복됨(특히 어텐션, 각종 레이어 등).
  • 여러 모델을 추가할수록 비슷한 코드가 계속 복사되고, 각각 조금씩 달라지는 "분기"가 생김 → 관리가 점점 어려워짐.

2. 그동안의 해결 방법: "복사본 주석"

  • 라이브러리에서는 같은 코드임을 알려주는 주석을 코드에 남겨둠(Copied from ...)으로써, 자동화된 도구(CI)로 복사본들끼리 차이가 생기지 않게 관리하려고 했음.
  • 하지만, 이는 여전히 번거롭고, 모델 하나 추가할 때 수천 줄의 중복/보일러플레이트 코드를 반복하게 됨.

3. 문제점 정리

  • 코드를 여러 번 복사해야 하니, 모델 기여(새 모델 추가)의 장벽이 높아짐.
  • 코드의 변경점이 여러 곳에서 발생해, 일관성 유지가 어려움.

4. 모듈식 트랜스포머의 취지

  • 공통 레이어와 로직을 여러 모델에서 "모듈"처럼 공유할 수 있도록 구조 개편.
  • 이제 "공통 코드"는 한 곳에 두고, 모델간 차이는 필요한 부분만 오버라이드!
  • 기여자는 필요한 변경 부분만 작성하면 되고, 나머지 코드는 자동으로 상속받음.

실제 예시: BERT & RoBERTa

  • 옛날에는 전체 코드를 복사해서 조금만 바꿨지만,
  • 이제는 BERT의 코드를 "상속"해서, 필요한 부분만 오버라이드.
  • 코드 중복과 보일러플레이트가 크게 줄어듦.

요약

  • 예전 방식: 모든 모델 파일에 코드 중복이 심했고, 관리가 힘들었음.
  • 모듈식 방식: 공통 코드를 재활용해, 기여가 쉽고 유지보수가 쉬워짐.
  • 앞으로는 새 모델 추가할 때 필요한 부분만 작성하면 되고, 큰 PR과 중복이 없어질 예정.

 

Hugging Face의 모듈식 트랜스포머 — ‘와닿게’ 설명

🔎 핵심만 직관적으로:

  • 전통적인 방식에서는 모델마다 파일을 복사해서, 내부 코드를 약간씩 고쳤어요. 이러면 여러 모델(예: BERT/Roberta)이 실제로 대부분 비슷한데 코드가 여기저기 중복돼서 관리도 어렵고 번거롭죠.
  • 모듈식 트랜스포머는 "공통되는 부분(예: 어텐션, 기본 구조)"을 "모듈화"해서 한 번만 정의하고, 필요한 모델만 상속해서 차이점만 적어요.

1. 모듈식 파일 — 쉽게 말하면?

  • "모듈식" 파일은 다른 모델에서 가져오거나(Class 상속), 임포트가 자유로운 ‘공통 코드 덩어리’ 역할.
  • 예를 들어, BERT와 Roberta가 거의 비슷하다면, 실제로 BERT의 코드를 이어받고, 달라진 부분(임베딩, config 등)만 변경하면 됩니다.

2. 자동 변환(linter) — 사용자에게는 투명

  • 내부적으로, 새로운 ‘linter’(자동화 도구)가 "모듈식 코드"를 일반 모델 파일 형태로 자동 변환해줍니다.
  • 즉, 개발자 입장에선 필요한 부분만 작성하면 되고, 최종적으로는 우리가 평소 임포트하던 방식(단일 파일)로 쓸 수 있어요.

3. 왜 좋은가? (쉽게 와닿는 비유)

  • 예전 방식: 레고 블록을 똑같이 복사해서 같은 집 여러 개를 만들면서, 벽지 무늬만 살짝 바꿈. (블록 중복, 유지보수 불편)
  • 모듈식: 공통 벽과 문, 지붕은 하나의 블록으로 두고, 창문 모양만 다를 때 창문만 새로 만들어서 붙이는 방식! (블록 재활용, 작은 변화만 관리함)

4. 개발·기여 관점에서의 이점

  • 반복되는(중복) 코드가 확 줄고,
  • 추가되는 모델마다 수정할 부분만 신경 쓰면 되니, 관리와 기여 장벽이 낮아짐.
  • 유지보수, 코드 리뷰도 훨씬 쉬워짐.

🌱 짧은 요약

"모듈식 트랜스포머는 같은 부분(공통 기능)을 하나로 정의해, 필요한 모델만 조금씩 고쳐 쓰는 방식이다. 덕분에 코드 중복이 줄고, 새 모델 기여가 쉬워진다. 최종적으로는 기존 Transformers처럼 단일 파일로 그대로 쓸 수 있다."

 

예시를 통해 좀 더 쉽게 이해해본다. 

from torch import nn
from ..bert.configuration_bert import BertConfig
from ..bert.modeling_bert import (
    BertModel,
    BertEmbeddings,
    BertForMaskedLM
)

# RoBERTa 구성은 BERT의 구성과 동일합니다
class RobertaConfig(BertConfig):
    model_type = 'roberta'

# 여기서 패딩 ID 차이를 강조하기 위해 임베딩을 재정의하고, 위치 임베딩을 재정의합니다
class RobertaEmbeddings(BertEmbeddings):
    def __init__(self, config):
        super().__init__(config())

        self.padding_idx = config.pad_token_id
        self.position_embeddings = nn.Embedding(
            config.max_position_embeddings, config.hidden_size, padding_idx=self.padding_idx
        )

# RoBERTa 모델은 임베딩 레이어를 제외하면 BERT 모델과 동일합니다.
# 위에서 임베딩을 재정의했으므로, 여기서는 추가 작업이 필요 없습니다
class RobertaModel(BertModel):
    def __init__(self, config):
        super().__init__(config)
        self.embeddings = RobertaEmbeddings(config)

# 헤드는 이제 내부에서 올바른 `RobertaModel`을 재정의하기만 하면 됩니다
class RobertaForMaskedLM(BertForMaskedLM):
    def __init__(self, config):
        super().__init__(config)
        self.model = RobertaModel(config)
더보기

네, 정확하게 이해했어요! 아래 코드의 핵심은 필요한 부분만 새로 정의하고, 나머지는 상속자동 추론을 통해 '알아서 생기게' 하는 모듈식 구조에 있습니다.

왜 이렇게 동작하나요?

  • RobertaConfig(BertConfig)처럼 구성(config)은 거의 그대로 쓰고,
  • 임베딩 구조(RobertaEmbeddings) 등 실제로 BERT와 다른 부분만 새로 정의,
  • 모델 전체(RobertaModel, RobertaForMaskedLM)는 베이스 클래스(BertModel, BertForMaskedLM)를 상속해서, 변경된 임베딩만 끼워줍니다.

이렇게 하면:

  • 공통적으로 쓸 수 있는 모든 기능(포워드, 레이어 구조, 기타 유틸 함수 등)은 자동으로 상속됩니다.
  • 오직 필요한 차이(패딩 시 위치 임베딩 등)만 새로 적으면 되고, 그 외엔 '알아서' 코드가 채워져요. (자동 linter/추론 시스템이 상속 구조 기반으로 단일 파일을 생성)

예시 설명: 너가 올린 코드

# 1. Config 상속(사실상 동일)
class RobertaConfig(BertConfig):
    model_type = 'roberta'

# 2. 임베딩만 재정의 → 차이만 명시
class RobertaEmbeddings(BertEmbeddings):
    ...

# 3. 모델 로직은 BERT와 똑같으니, 임베딩만 갈아끼움
class RobertaModel(BertModel):
    ...

# 4. 마스크드 LM 헤드도 BERT와 동일 (모델만 갈아끼워줌)
class RobertaForMaskedLM(BertForMaskedLM):
    ...

즉, 네! "각자 필요한 것만 새로 정의하며, 나머지는 알아서 채워진다"가 바로 모듈식 트랜스포머 구조의 핵심입니다.

이해를 더 확인해보고 싶다면 “이런 구조에서 만약 Roberta와 완전히 다른 attention 메커니즘을 넣으려면 어디만 변경하면 좋을까?”를 스스로 답해보는 것도 추천해요. 필요하면 도움이 될만한 예시를 추가로 들어줄 수도 있어요!

  1. https://huggingface.co/docs/transformers/v4.57.1/ko/modular_transformers

 

모델 추가

더보기

Hugging Face Transformers에 새 모델 추가하기

아래는 Hugging Face Transformers에 새로운 모델을 추가하는 공식 방법의 내용을 핵심 중심으로 쉽고 빠르게 정리한 안내입니다.


1. 추가 프로세스 개요

  • 커뮤니티 기여: 누구나 새로운 Transformer 모델을 라이브러리에 추가할 수 있으며, 팀의 적극적인 지원을 받을 수 있음.
  • 사전 준비: 새 모델 추가 전, 이슈 등록 후 팀과 논의 시작.
  • 라이브러리 철학:
    • 독립적인 파일 구조를 유지(각 모델은 자체 파일),
    • 공통 기능도 복사하여 접근성을 높임,
    • 추상화를 최소화, 가독성과 유지보수를 우선함.

2. 모델/구성 클래스 구조

  • PreTrainedModel / PretrainedConfig를 기반으로 한 상속 구조.
  • 모든 모델은 from_pretrained 및 save_pretrained 등의 메서드를 자동으로 제공받음.
  • 실제 모델과 헤드(for masked LM 등), 그리고 config 클래스를 별도 구현.

3. 코딩 스타일 가이드

  • forward 메서드는 명확하게 작성될 것.
  • 변수 이름은 직관적으로(약어 금지),
  • 복잡한 매직을 최소화하고 명시적인 코드 선호,
  • PyTorch의 모듈을 그대로 활용하며 필요시 코드 복사 후 주석(Copied from ...)으로 명시.

4. 단계별 이식 방법 (요약)

단계를 핵심 To-do 형태로 요약하면:

  1. 모델 전반을 논문/리포에서 이해 (선택)
  2. 개발 환경 준비 (fork/clone, venv, pip install)
  3. 원본 체크포인트/리포 환경에서 모델이 제대로 동작하는지 확인
  4. Transformers 환경에 모델 스켈레톤 추가
  5. 체크포인트 변환 및 가중치 정상로드
  6. 순방향패스(forward) 구현 및 출력 비교 (정밀도 1e-3 이내)
  7. 모든 테스트 추가/통과
  8. 토크나이저 구현 및 테스트
  9. 종단간 통합 테스트
  10. 문서화 및 코드 스타일 체크
  11. 모델 허브에 업로드, 모델 카드 작성
  12. PR 작성 및 커뮤니티 공유

5. 디버깅과 변환 스크립트 작성

  • 원본 모델 디버깅 환경(노트북, 스크립트 등) 세팅 → 중간 값(임베딩, 레이어 출력)까지 세밀하게 비교.
  • 가중치와 레이어 이름, shape까지 일치하게 만드는 변환(매치) 스크립트 작성.

6. 최종적으로 무엇을 얻는가?

  • 누구나 사용할 수 있는 새로운 Transformers 모델,
  • 효율, 가독성, 버그 방지, 테스트까지 합쳐진 수준 높은 오픈 소스 구현!
  • 기여자의 이름이 공식 문서와 코드, 모델 카드에 남게 되어 NLP 커뮤니티에 흔적을 남길 수 있음.

⏩ 즉, Hugging Face Transformers에 새 모델을 추가하려면:

"기본 구조와 스타일을 지키면서, 원본 환경에서 debugging/checkpoint 변환, output 비교, 테스트와 문서화를 반복하며, PR로 커뮤니티에 공유하는 과정"이 필요하다.

설명 이해가 잘 되는지 확인하고, 혹시 실제로 새 모델을 추가할 때 단계별로 막힐 부분이 있다면 구체 사례로 더 안내해줄 수 있어. 필요하면 각 단계별 예시 코드도 만들어줄 수 있음. 원하는 단계 추가 설명 요청해도 괜찮아!

  1. https://huggingface.co/docs/transformers/v4.57.1/ko/add_new_model

 

더보기

🤗 Transformers 모델 구조 - 이미지 요약

이 이미지는 Hugging Face Transformers에 새로운 모델을 추가할 때 사용하는 구조 패턴을 보여줍니다. 코드 구조와 계층을 시각적으로 정리해, "아무 모델이나 추가하는 방법"을 한 번에 이해하기 좋게 도식화한 자료입니다.


1. 기본(공통) 클래스: General model classes

- PreTrainedConfig

  • 모든 config의 공통 부모 클래스
  • 예: is_encoder_decoder, output_attentions와 같은 기본 속성 포함
  • 주요 메서드: from_pretrained(), save_pretrained()

- PreTrainedModel

  • 모든 모델의 공통 부모 클래스
  • 예: config 속성, from_pretrained(), save_pretrained(), init_weights(), generate() 등 제공

2. 모델별 클래스: Specific to each model

- BrandNewBertConfig (예시 config)

  • PreTrainedConfig를 상속
  • 모델 고유의 속성(예: vocab_size, num_hidden_layers, num_attention_heads)만 추가

- BrandNewBertPreTrainedModel

  • PreTrainedModel을 상속
  • 클래스 공통 속성과 초기화 전략 포함 (예: init_weights)
  • 여기서 기본 모델과 head붙은 모델 모두 이 클래스를 상속하게 함

- BrandNewBertModel (base model)

  • BrandNewBertPreTrainedModel을 상속
  • 인코더 등 주요 모듈을 정의, 메인 forward() 구현
  • self-contained하게 포워드 추론

- BrandNewBertForMaskedLM (헤드 모델)

  • 역시 BrandNewBertPreTrainedModel 상속
  • 헤드 레이어(예: masked_lm_head) 포함
  • forward()에서 베이스 모델 포워드 + 헤드 호출하여 결과 반환

3. 상속·조합 구조

  • 각 모델별 config/model은 "공통 클래스"를 상속받아 재사용성을 갖고, 필요한 부분만 새로 작성함.
  • 헤드가 붙는 모델(MaskedLM 등)도 베이스 모델 구조에 헤드만 얹어서 쉽게 정의.

4. 핵심 내용 요약

  • 공통 기능은 PreTrainedConfig/Model이 제공 → 어떤 모델이든 자동으로 from_pretrained, save_pretrained 기능 사용 가능.
  • 모델별로 config·모델·헤드 레이어만 최소 코드로 오버라이드.
  • 베이스(encoder)와 헤드(예: MaskedLM)가 분명하게 분리되어 있고, 각자 필요한 부분만 재정의.
  • 각 계층의 역할이 주석과 예시로 명확히 안내됨.

이 패턴을 알면, 새로운 모델을 추가하거나 다른 모델 코드를 읽을 때 구조를 빠르게 파악하고 최소만 고쳐 쓸 수 있습니다. 다음과 같이 정리해도 좋겠네요:

"Transformers는 공통 기본 구조(PreTrainedConfig/Model)를 제공하고, 모델별로 꼭 필요한 속성만 얹는 계층적 상속 구조를 가집니다. 이 패턴에 따라 쉽게 새로운 모델/헤드/구성을 추가할 수 있음!"

이 전에 가지고 있던 기능(save나 form)은 새로 정의하지 않아도 가지게 된다. 

중요한 기능인 BrandNewBertModel.forward와 같은 기능들은 modeling_brand_new_bert.py 스크립트에서 정의되어야 한다.

 

공통된 라이브러리를 수정하는 것이므로 규칙이 있다.

  1. forward pass는 모델 파일에 작성해야 함! 코드를 재사용하려면 코드를 복사하여 # copied from으로 기록 남겨좋기
  2. 코드는 가독성이 좋아야 함! 변수 이름을 명확하게 한다.
  3. 길고 명시적인 코드 선호
  4.  nn.Module을 하위 클래스로 만들고 forward pass를 작성하여 다른 사람이 코드를 빨리 디버그 할 수 있도록 한다! 
  5. 함수 시그니처에는 타입 주석을 써라 (입 출력의 타입을 힌트로 명시해줘라 )
더보기

왜 nn.Sequential 대신 nn.Module과 직접 forward를 쓸까?

PyTorch의 nn.Sequential과 nn.Module의 차이를 이해하면, 위 문장의 의도를 쉽게 파악할 수 있어요.


1. nn.Sequential 구조

  • 여러 층(layer)을 '순서대로만' 쌓고 싶을 때 간단하게 사용할 수 있는 PyTorch 모듈입니다.
  • 문제점: 내부 계산 순서가 고정되어 있기 때문에, 중간 연산의 값에 직접 접근하기 어렵고, 디버깅용 print나 breakpoint를 중간에 넣기 힘듭니다.
model = nn.Sequential(
    nn.Linear(10, 20),
    nn.ReLU(),
    nn.Linear(20, 5)
)

2. nn.Module 직접 상속 + forward 구현

  • nn.Module를 상속하고, forward() 함수에서 연산을 직접 적을 때는 중간마다 print문을 넣거나 원하는 위치에서 중단점(breakpoint)을 만들 수 있습니다.
  • 장점: 원하는 위치 어디든 print/debugger/breakpoint를 넣을 수 있어, 코드나 연산 과정 확인이 매우 용이합니다.
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear1 = nn.Linear(10, 20)
        self.linear2 = nn.Linear(20, 5)
    def forward(self, x):
        x = self.linear1(x)
        print('After first layer:', x)
        x = self.linear2(x)
        return x

3. 정리

  • nn.Sequential은 수식처럼 단순한 "순차 레이어 쌓기"에는 편하지만, 복잡한 연산이나 디버깅, 내부 값 확인이 필요할 땐 불편합니다.
  • nn.Module 상속 + 직접 forward()를 쓰면, 중간 확인·수정·디버그가 훨씬 쉽고, 다른 사람도 코드를 빠르게 분석·고칠 수 있게 됩니다.

확인 질문

  • 만약 모델 중간값을 프린트하거나, 특정 레이어의 출력에만 breakpoint를 걸고 싶으면 nn.Sequential로 가능할까요? 어떤 방식이 더 나을까요?

만약 실제 예시 코드나, 디버깅 포인트 추가하는 법이 궁금하면 언제든 물어보세요!

  1. https://huggingface.co/docs/transformers/v4.57.1/ko/add_new_model#overview-of-models

 

def _init_weights(self, module):
    """Initialize the weights"""
    # 인자로 들어온 모듈(신경망 layer)에 대해 가중치를 초기화하는 함수

    if isinstance(module, nn.Linear):
        # Linear(완전연결) 레이어일 경우
        module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
        # 가중치를 평균 0, 표준편차 self.config.initializer_range의 정규분포로 초기화
        if module.bias is not None:
            module.bias.data.zero_()
            # bias가 있을 경우 0으로 초기화

    elif isinstance(module, nn.Embedding):
        # 임베딩 레이어일 경우
        module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
        # 임베딩 가중치를 평균 0, 표준편차 self.config.initializer_range의 정규분포로 초기화
        if module.padding_idx is not None:
            module.weight.data[module.padding_idx].zero_()
            # padding_idx가 있으면 해당 임베딩 벡터는 0으로 초기화

    elif isinstance(module, nn.LayerNorm):
        # LayerNorm 레이어일 경우
        module.bias.data.zero_()
        # bias를 0으로 초기화
        module.weight.data.fill_(1.0)
        # weight(스케일 파라미터)를 1로 초기화

 

 

728x90