레거시 모델 기여하기

레거시 모델 기여하기 (Legacy model contribution)

[!TIP] 새 모델을 추가할 때는 먼저 더 modular한 접근을 시도해 보세요. 그러면 Transformers에 모델을 기여하는 것이 훨씬 쉬워져요!

Transformers의 많은 모델은 개발자와 연구자들이 기여한 거예요. 오픈소스 우선(open-source first) 프로젝트로서 우리는 커뮤니티가 적극적이고 독립적으로 더 많은 모델을 추가하도록 지원하는 데 힘을 쏟고 있어요.

출처: 문서

본문

Transformers에 모델을 추가하면 다음을 배우게 돼요:

  • 오픈소스 모범 사례
  • 모델 아키텍처
  • Transformers의 설계 원칙
  • 큰 모델을 효율적으로 테스트하는 방법
  • Black과 Ruff 같은 Python 유틸리티로 깔끔하고 읽기 좋은 코드를 만드는 방법

도전적이지만 보람 있는 과정이에요.

이 가이드는 예시로 BrandNewLlama PyTorch 모델을 Transformers에 추가하는 과정을 안내해요. 시작하기 전에 라이브러리에 익숙해지는 것이 좋아요.

Transformers 개요 (Transformers overview)

Transformers는 독특한 철학과 설계 선택을 가진, 주관이 뚜렷한(opinionated) 라이브러리예요. 이런 선택들이 Transformers를 지속 가능하게 확장하고 유지하게 해줘요.

[!TIP] 설계 원칙에 대해 더 알아보려면 Philosophy 문서를 확인해 주세요.

이러한 설계 선택 중 일부는:

  • 구성(composition) > 과도한 추상화(over-abstraction)
  • 가독성과 접근성을 크게 높인다면 중복 코드가 항상 나쁜 건 아니다
  • 모델 파일은 자족적(self-contained)이고 필요한 모든 모델 코드는 modeling_mymodel.py 파일에 있다

이 설계 선택은 모델과 상호작용하는 모든 사람에게 중요해요. 읽고, 이해하고, 수정하기가 더 쉽기 때문이지요.

이 섹션에서는 모델과 configuration 클래스가 어떻게 상호작용하는지, 그리고 Transformers 코드 스타일을 설명할게요.

모델과 설정 (Model and configuration)

모든 Transformers 모델은 기본 PreTrainedModel과 PreTrainedConfig 클래스를 상속받아요. configuration은 모델의 청사진이에요.

코드를 읽기 쉽게 유지하기 위해 어떤 모델도 추상화 레벨이 두 단계를 넘지 않아요. 여기 예시 모델인 BrandNewLlama는 BrandNewLlamaPreTrainedModel과 PreTrainedModel을 상속받아요. 새 모델이 from_pretrained()과 save_pretrained() 메서드를 사용할 수 있도록 PreTrainedModel에만 의존하는 것이 중요해요.

forward 메서드 같은 다른 중요한 함수는 modeling.py 파일에 정의돼요.

특정 모델 헤드(예: 시퀀스 분류나 언어 모델링)는 추상화를 낮게 유지하기 위해 상속하는 대신 forward 패스에서 기본 모델을 호출해야 해요.

새 모델에는 BrandNewLlamaConfig 같은 configuration이 필요하고, 이는 PreTrainedModel의 속성으로 저장돼요.

model = BrandNewLlamaModel.from_pretrained("username/brand_new_llama")
model.config

PreTrainedConfig는 from_pretrained()과 save_pretrained() 메서드를 제공해요.

PreTrainedModel.save_pretrained()을 사용하면 자동으로 PreTrainedConfig.save_pretrained()을 호출해서 모델과 configuration이 함께 저장돼요.

모델은 model.safetensors 파일로, configuration은 config.json 파일로 저장돼요.

코드 스타일 (Code style)

Transformers는 더 추상화된 코드 스타일보다 깔끔하고 읽기 좋은 코드를 선호해요. 몇 가지 코드 스타일 선택은:

  • 코드는 비영어권 사용자에게도 접근 가능해야 해요. 설명적인 변수 이름을 고르고 약어를 피해요. 예를 들어 "act"보다 "activation"이 선호돼요. 한 글자 변수 이름은 for 루프의 인덱스가 아니라면 강력히 권장하지 않아요.

  • 더 짧은 코드보다 명시적인 코드가 선호돼요 — 더 길어도 말이지요.

  • nn.Sequential 서브클래싱을 피해요. 대신 nn.Module을 서브클래싱해서 print 문이나 breakpoint로 코드를 빠르게 디버깅할 수 있게 해요.

  • 함수 시그니처는 타입 어노테이션을 붙여야 해요. 그렇지 않으면 더 이해하기 쉽도록 좋은 변수 이름을 사용해요.

새 모델 추가 이슈 (New model addition issue)

특정 모델을 추가하려면 New model addition 이슈를 열어요.

[!TIP] GitHub에서 New model 라벨로 필터링해서 기존 모델 요청을 보고 추가할 수 있어요.

이제 BrandNewLlama에 익숙해지기에 좋은 시기예요. 모델의 기술 설계와 구현을 이해하려면 연구 논문을 읽는 게 도움이 돼요. 이론적 세부사항에 너무 신경 쓸 필요는 없어요. 대신 실용적인 것에 집중해요. 아래 질문을 읽을 때 가이드로 활용해 보세요.

  • BrandNewLlama는 어떤 타입의 모델인가요? encoder, decoder, 아니면 encoder-decoder 모델인가요?
  • BrandNewLlama는 어떤 태스크에 쓸 수 있나요?
  • BrandNewLlama를 다른 모델과 구분 짓는 것은 무엇인가요?
  • Transformers에서 BrandNewLlama와 가장 비슷한 모델은 무엇인가요?
  • BrandNewLlama는 어떤 tokenizer를 사용하나요?

모델에 대해 더 배우는 것 외에도 아래 팁을 사용해서 모델을 더 빨리 추가할 수 있어요.

[!TIP] 각 기여자는 Transformers에 모델을 추가하는 고유한 스타일과 워크플로가 있어요. 예시로 Gemma가 어떻게 추가됐는지 살펴보세요.

  • 바퀴를 다시 발명하지 마세요! 기존 모델과 tokenizer를 탐색해서 무엇을 복사·재사용할 수 있는지 확인하는 데 시간을 쓰세요. Grep과 ripgrep이 이에 좋은 도구예요.
  • 이는 과학보다 엔지니어링에 가까운 도전이에요. (효율적인 디버깅 환경 구성 같은) 더 실용적인 것에 집중하고 모델의 이론적 측면에는 신경 쓰지 마세요.
  • 도움을 요청하는 걸 주저하지 마세요! 우리가 지원할게요. 🤗

개발 환경 (Dev environment)

Transformers 저장소의 Fork 버튼을 클릭해서 작업할 여러분만의 복사본을 만들어요. 저장소를 로컬 디스크에 클론하고 기본 저장소를 remote로 추가해요.

git clone https://github.com/[your Github handle]/transformers.git
cd transformers
git remote add upstream https://github.com/huggingface/transformers.git

가상 환경을 만들고 "dev" 또는 개발 의존성으로 라이브러리를 editable install 해요.

python -m venv .env
source .env/bin/activate
pip install -e ".[dev]"

Transformers가 성장함에 따라 선택적 의존성 수가 많아져서 이 명령이 실패할 수 있어요. 그 경우 "quality" 의존성을 설치해요. 또한 딥러닝 프레임워크가 설치되어 있는지 확인해 주세요.

pip install -e ".[quality]"

부모 디렉터리로 돌아가 원래 BrandNewLlama 저장소를 클론하고 설치해요.

git clone https://github.com/org_that_created_brand_new_llama_org/brand_new_llama.git
cd brand_new_bert
pip install -e .

BrandNewLlama 포팅을 시작하려면 Transformers 클론으로 돌아가요.

cd transformers

원래 모델을 실행하기 위한 두 가지 디버깅 환경이 있어요: 노트북(Google Colab이나 Jupyter) 또는 로컬 Python 스크립트.

[!WARNING] 원래 모델을 실행하기 위해 GPU 환경을 설정하는 것은 비쌀 수 있으므로 권장하지 않아요. 대신 먼저 CPU 환경에서 작업해서 모델이 Transformers에서 동작하는지 확인해요. 동작하면 그때 GPU에서 확인할 수 있어요.

노트북은 코드를 셀 단위로 실행하기 좋아서 논리적 컴포넌트를 서로 분리하는 데 도움이 돼요. 중간 결과를 저장할 수 있어 디버깅 사이클을 가속할 수도 있어요. 다른 기여자와 협업할 때 노트북을 공유할 수도 있어요.

단점은 익숙하지 않다면 적응하는 데 시간이 걸릴 수 있다는 거예요.

[!TIP] 모델 아키텍처가 기존 모델과 동일하다면 변환 스크립트 추가로 건너뛰세요. 기존 모델의 아키텍처를 재사용할 수 있으니까요.

아래 명령을 실행해서 새 모델에 대한 기본 정보가 담긴 질문지를 시작하고 완료해요. 이 명령은 적응해야 할 모델 코드를 자동으로 생성해서 과정을 빠르게 시작하게 해줘요.

transformers add-new-model-like

Pull Request 만들기 (Create a pull request)

코드를 적응시키기 전에 진행 상황을 추적하고 Transformers 팀의 피드백을 받기 위해 pull request를 먼저 만들어요. [WIP] Add BrandNewLlama 로 제목을 붙여서 진행 중인 작업임을 분명히 해요.

main 브랜치에서 설명적인 이름의 브랜치를 만들어요.

git checkout -b add_brand_new_bert

코드를 커밋한 다음 main 브랜치에서 fetch하고 rebase 해요.

git add .
git commit
git fetch upstream
git rebase upstream/main

변경 사항을 브랜치에 푸시하고 Compare & pull request를 클릭해서 GitHub에서 pull request를 열어요. draft 상태로 열어서 진행 중임을 나타내세요.

git push -u origin a-descriptive-name-for-my-changes

질문, 피드백, 댓글, 리뷰를 위해 관련 Hugging Face 팀원의 GitHub 핸들을 pull request에 포함해 주세요. Files changed 탭을 클릭하고 줄 번호 왼쪽의 **+**를 클릭해 댓글을 추가하면 팀원을 코드의 특정 부분으로 안내할 수 있어요. 질문이나 문제가 해결되면 Resolve를 클릭해 해결됐음을 나타내요. 이렇게 하면 대화가 체계적이고 깔끔하게 유지돼요.

작업을 주기적으로 커밋·푸시하고, 현재 main 브랜치로 작업을 업데이트하는 것을 잊지 마세요.

git fetch upstream
git merge upstream/main

원래 checkpoint (Original checkpoint)

먼저 원래 모델 구현을 작업해서 어떻게 동작하는지 이해하는 데 시간을 써요.

원래 모델 저장소에 문서가 부족하거나 코드베이스가 복잡하면 어려울 수 있어요. 하지만 이를 Transformers에서 모델을 구현하려는 동기로 삼아야 해요. 여러분의 기여는 그것을 모두에게 더 접근 가능하고 사용자 친화적으로 만들어 줘요!

다음을 수행해서 원래 저장소에 익숙해져요.

  • 사전 학습된 가중치를 찾아요.
  • 사전 학습된 가중치를 모델에 로드하는 방법을 파악해요.
  • 모델과 독립적으로 tokenizer를 실행하는 방법을 파악해요.
  • 하나의 forward 패스를 추적해서 어떤 클래스와 함수가 필요한지 이해해요. 아마 그것들이 여러분이 구현해야 할 유일한 클래스와 함수일 거예요.
  • 모델의 모든 중요한 컴포넌트(모델 클래스, 모델 서브클래스, self-attention 레이어 등)를 찾아요.
  • 원래 저장소에서 모델을 디버깅하는 방법을 파악해요. print 문을 추가하고, ipdb 같은 대화형 디버거나 PyCharm 같은 효율적인 통합 개발 환경(IDE)을 사용해요.

마지막 항목이 특히 중요한데, Transformers에서 다시 구현하기 전에 원래 모델 안에서 무슨 일이 일어나는지 철저히 이해해야 하기 때문이에요. 문제가 생기면 원래 저장소에 이슈와 pull request를 열어도 괜찮아요.

좋은 첫 단계는 작은 사전 학습 checkpoint를 로드하고 예시 정수 벡터 입력으로 단일 forward 패스를 재현해 보는 거예요. 예를 들어 의사코드로는 이렇게 생겼을 거예요.

model = BrandNewLlamaModel.load_pretrained_checkpoint("/path/to/checkpoint/")
input_ids = [0, 4, 5, 2, 3, 7, 9]  # vector of input ids
original_output = model.generate(input_ids)

디버깅 (Debugging)

문제가 생기면 원래 모델의 코드베이스에 따라 다음 디버깅 전략 중 하나를 선택해야 해요.

몇 가지 전략은 코드를 eager 모드에서 쉽게 실행할 수 있을 때처럼 원래 모델을 더 작은 하위 컴포넌트로 쪼개는 데 의존해요. 더 어렵지만 이 접근에는 몇 가지 장점이 있어요.

  1. 나중에 원래 모델을 여러분의 구현과 비교하기 더 쉬워요. 각 개별 컴포넌트가 Transformers 구현의 해당 컴포넌트와 일치하는지 자동으로 검증할 수 있어요. 이는 print 문에 기반한 시각적 비교에 의존하는 것보다 나아요.
  2. 전체 모델보다 개별 컴포넌트를 포팅하기 더 쉬워요.
  3. 모델을 더 작은 부분으로 쪼개면 모델이 어떻게 동작하는지 이해하기 더 쉬워요.
  4. 코드를 변경할 때 컴포넌트별 테스트 덕분에 나중에 회귀(regression)를 예방하기 더 쉬워요.

[!TIP] 모델을 더 작은 컴포넌트로 분해하는 좋은 예시는 ELECTRA 통합 체크를 참고해 주세요.

이 전략은 원래 코드베이스가 너무 복잡하거나, 중간 컴포넌트만 컴파일 모드로 실행할 수 있거나, 모델을 더 작은 하위 컴포넌트로 분리하기 시간이 너무 오래 걸리(어쩌면 불가능할) 때 유효해요.

예를 들어 T5의 MeshTensorFlow 구현은 너무 복잡해서 모델을 하위 컴포넌트로 분해하는 단순한 방법을 제공하지 않아요. 이런 상황에서는 print 문 검증에 의존해야 해요.

어떤 전략을 선택하든, 초기 레이어를 먼저 디버깅하고 최종 레이어를 마지막에 디버깅하는 것을 권장해요. 다음 레이어들의 출력을 print 문이나 하위 컴포넌트 함수로 이 순서대로 가져와요.

  1. 모델에 전달되는 input ids
  2. word embeddings
  3. 첫 번째 Transformer 레이어의 입력
  4. 첫 번째 Transformer 레이어의 출력
  5. 다음 n-1개 Transformer 레이어들의 출력
  6. 전체 모델의 출력

input ids는 input_ids = [0, 4, 4, 3, 2, 4, 1, 7, 19] 같은 정수 배열이어야 해요.

레이어 출력은 종종 다차원 float 배열로 구성돼요.

[[
 [-0.1465, -0.6501,  0.1993,  ...,  0.1451,  0.3430,  0.6024],
 [-0.4417, -0.5920,  0.3450,  ..., -0.3062,  0.6182,  0.7132],
 [-0.5009, -0.7122,  0.4548,  ..., -0.3662,  0.6091,  0.7648],
 ...,
 [-0.5613, -0.6332,  0.4324,  ..., -0.3792,  0.7372,  0.9288],
 [-0.5416, -0.6345,  0.4180,  ..., -0.3564,  0.6992,  0.9191],
 [-0.5334, -0.6403,  0.4271,  ..., -0.3339,  0.6533,  0.8694]]],

모든 Transformers 모델 출력은 1e-3의 정밀도 또는 오차 허용치를 가져야 해요. 이는 다른 라이브러리 프레임워크를 사용해서 생기는 출력 차이를 감안한 거예요. 원래 모델의 중간 출력과 Transformers 구현을 비교해서 거의 동일한지 확인해요. 이 단계에서는 효율적인 디버깅 환경이 중요해요.

효율적인 디버깅 환경을 위한 몇 가지 팁이에요.

  • 중간 결과를 디버깅하려면 원래 모델 저장소가 사용하는 머신러닝 프레임워크에 따라 달라져요. PyTorch의 경우 원래 모델을 더 작은 하위 컴포넌트로 분해해 중간 값을 가져오는 스크립트를 작성해야 해요.

  • forward 패스가 10초 이상 걸리는 큰 checkpoint보다 작은 사전 학습 checkpoint로 디버깅하는 게 더 빨라요. 큰 checkpoint만 가능하면 랜덤 초기화 가중치로 더미 모델을 만들고 그 가중치를 저장해서 Transformers 구현과 비교해요.

  • 모델의 forward 패스를 호출하는 가장 쉬운 방법을 찾아요. 이상적으로 이 함수(predict, evaluate, forward, __call__ 등일 수 있어요)는 forward 패스를 한 번만 호출해야 해요. forward 패스를 여러 번 호출하는 함수는 디버깅하기 더 어려워요.

  • 토큰화를 forward 패스에서 분리해요. forward 패스에서 문자열 입력이 input ids로 바뀌는 위치를 찾아 여기서 시작해요. 입력 문자열 대신 input ids를 직접 입력하도록 작은 스크립트를 만들거나 원래 코드를 수정해야 할 수도 있어요.

  • 모델이 학습 모드(training mode)에 있지 않도록 해요. 모델의 여러 dropout 레이어 때문에 랜덤 출력이 생길 수 있어요. 디버깅 환경의 forward 패스는 dropout 레이어가 사용되지 않도록 결정적(deterministic) 이어야 해요.

원래 checkpoint를 실행할 수 있게 되면 Transformers용 모델 코드를 적응시키기 시작할 준비가 된 거예요.

모델 코드 적응하기 (Adapt the model code)

transformers add-new-model-like 명령이 모델과 configuration 파일을 생성했을 거예요.

  • src/transformers/models/brand_new_llama/modeling_brand_new_llama.py
  • src/transformers/models/brand_new_llama/configuration_brand_new_llama.py

modeling.py 파일에 자동 생성된 코드는 decoder-only 모델이라고 답하면 Llama와 같은 아키텍처를, encoder-decoder 모델이라고 답하면 BART와 같은 아키텍처를 가져요. 생성된 코드는 단지 출발점이에요. 새 모델에 대한 연구를 바탕으로 생성된 코드를 적응시켜 그 특정 변경사항을 구현해야 해요. 여기에는 self-attention 레이어, 정규화 레이어 순서 등의 변경이 포함될 수 있어요.

모델 초기화 (Model initialization)

이 시점에는 코드가 깔끔하거나 완전히 정확할 필요가 없어요. 첫 초안을 빠르게 만들고 반복해서 개선하는 게 더 효율적이에요. 가장 중요한 것은 모델을 Transformers에서 인스턴스화할 수 있다는 거예요. 아래 명령은 랜덤 가중치로 configuration에서 모델을 만들어 __init__ 메서드가 동작하는지 확인해요.

from transformers import BrandNewLlama, BrandNewLlamaConfig
model = BrandNewLlama(BrandNewLlamaConfig())

랜덤 초기화는 BrandNewLlamaPreTrainedModel의 _init_weights 메서드에서 일어나요. 모든 리프(leaf) 모듈은 configuration의 변수에 따라 초기화돼요. 가중치나 그 .data에 직접 in-place 연산을 호출하는 대신, transformers.initialization 모듈의 헬퍼를 사용해서 값을 설정해요.

from transformers import initialization as init

def _init_weights(self, module):
    """Initialize the weights"""
    if isinstance(module, nn.Linear):
        init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
        if module.bias is not None:
            init.zeros_(module.bias)
    elif isinstance(module, nn.Embedding):
        init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
        if module.padding_idx is not None:
            init.zeros_(module.weight[module.padding_idx])
    elif isinstance(module, nn.LayerNorm):
        init.zeros_(module.bias)
        init.ones_(module.weight)

가중치는 항상 transformers.initialization 헬퍼를 통해 초기화해요. module.bias.zero_() 같은 in-place 연산이나 module.weight.data를 건드리는 어떤 것이든, 이미 로드된 파라미터를 표시하는 _is_hf_initialized를 우회해요. from_pretrained()은 checkpoint를 로드한 후 _init_weights를 실행하므로, in-place 연산은 로드된 가중치를 랜덤 값으로 조용히 덮어써요. 이는 Transformers 모델에서 TRF012 규칙으로 강제돼요.

초기화 방식은 모델에 적응시켜야 한다면 다르게 보일 수 있어요. 자체 reset_parameters 메서드를 가진 서브모듈은 직접 호출할 수 있어요. 예를 들어 Wav2Vec2ForPreTraining은 마지막 두 개의 linear 레이어에서 nn.Linear를 초기화해요. from_pretrained()이 _init_weights를 실행하는 동안 기본 torch.nn.init 함수를 패치해서 _is_hf_initialized 플래그를 존중하게 하므로 이 호출은 안전해요. 그래서 로드된 가중치가 덮어쓰이지 않아요.

def _init_weights(self, module):
    """Initialize the weights"""
    if isinstance(module, Wav2Vec2ForPreTraining):
        module.project_hid.reset_parameters()
        module.project_q.reset_parameters()

Checkpoint를 Transformers로 변환하기 (Convert checkpoints to Transformers)

원래 checkpoint는 Transformers 호환 checkpoint로 변환해야 해요.

[!TIP] 복사, 적응, 재사용할 기존 변환 스크립트를 찾아보세요!

  • 모델을 TensorFlow에서 PyTorch로 포팅한다면 BERT 변환 스크립트가 좋은 출발점일 수 있어요.
  • 모델을 PyTorch에서 PyTorch로 포팅한다면 BART 변환 스크립트가 좋은 출발점일 수 있어요.

모든 필수 가중치가 초기화되는지 확인하고, 초기화에 사용되지 않은 checkpoint 가중치 전체를 출력해서 모델이 올바르게 변환됐는지 확인해요.

변환 중에 잘못된 shape 문이나 이름 할당을 마주칠 수 있어요. 이는 대부분 BrandNewLlamaConfig의 잘못된 파라미터, 잘못된 아키텍처, 여러분 구현의 init 메서드 버그, 또는 checkpoint 가중치 중 하나를 transpose 해야 하기 때문일 가능성이 높아요.

모든 checkpoint 가중치가 올바르게 로드될 때까지 모델 코드 적응하기 섹션을 계속 반복해요. 모델에 checkpoint를 로드할 수 있게 되면 폴더에 저장해요. 이 폴더에는 model.safetensors 파일과 config.json 파일이 있어야 해요.

model.save_pretrained("/path/to/converted/checkpoint/folder")

변환을 돕기 위해, 다음 섹션에서 PyTorch 모델이 레이어 가중치와 이름을 어떻게 저장·정의하는지 간략히 설명할게요.

PyTorch 레이어 가중치와 이름 (PyTorch layer weights and names)

레이어 이름이 어떻게 정의되고 가중치가 어떻게 초기화되는지 이해하려면 기본 PyTorch 모델을 만드는 게 도움이 돼요.

from torch import nn

class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.dense = nn.Linear(10, 10)
        self.intermediate = nn.Linear(10, 10)
        self.layer_norm = nn.LayerNorm(10)

PyTorch 레이어 이름은 레이어의 클래스 속성 이름(dense, intermediate, layer_norm)으로 정의돼요. SimpleModel 인스턴스를 만들어 모든 레이어를 랜덤 가중치로 채워요.

model = SimpleModel()
print(model)
SimpleModel(
  (dense): Linear(in_features=10, out_features=10, bias=True)
  (intermediate): Linear(in_features=10, out_features=10, bias=True)
  (layer_norm): LayerNorm((10,), eps=1e-05, elementwise_affine=True)
)

특정 레이어의 가중치 값은 랜덤으로 초기화돼요.

print(model.dense.weight.data)
tensor([[-0.0818,  0.2207, -0.0749, -0.0030,  0.0045, -0.1569, -0.1598,  0.0212,
         -0.2077,  0.2157],
        [ 0.1044,  0.0201,  0.0990,  0.2482,  0.3116,  0.2509,  0.2866, -0.2190,
          0.2166, -0.0212],
        [-0.2000,  0.1107, -0.1999, -0.3119,  0.1559,  0.0993,  0.1776, -0.1950,
         -0.1023, -0.0447],
        [-0.0888, -0.1092,  0.2281,  0.0336,  0.1817, -0.0115,  0.2096,  0.1415,
         -0.1876, -0.2467],
        [ 0.2208, -0.2352, -0.1426, -0.2636, -0.2889, -0.2061, -0.2849, -0.0465,
          0.2577,  0.0402],
        [ 0.1502,  0.2465,  0.2566,  0.0693,  0.2352, -0.0530,  0.1859, -0.0604,
          0.2132,  0.1680],
        [ 0.1733, -0.2407, -0.1721,  0.1484,  0.0358, -0.0633, -0.0721, -0.0090,
          0.2707, -0.2509],
        [-0.1173,  0.1561,  0.2945,  0.0595, -0.1996,  0.2988, -0.0802,  0.0407,
          0.1829, -0.1568],
        [-0.1164, -0.2228, -0.0403,  0.0428,  0.1339,  0.0047,  0.1967,  0.2923,
          0.0333, -0.0536],
        [-0.1492, -0.1616,  0.1057,  0.1950, -0.2807, -0.2710, -0.1586,  0.0739,
          0.2220,  0.2358]]).

변환 스크립트에서는 랜덤 가중치를 원래 checkpoint의 해당 레이어에서 정확한 가중치로 바꿔야 해요.

# retrieve matching layer weights with recursive algorithm
layer_name = "dense"
pretrained_weight = array_of_dense_layer

model_pointer = getattr(model, "dense")
model_pointer.weight.data = torch.from_numpy(pretrained_weight)

랜덤 초기화 가중치와 그에 해당하는 사전 학습 checkpoint 가중치가 동일한 shape과 name을 갖는지 확인해요. shape에 assert 문을 추가하고 checkpoint 가중치 이름을 출력해요.

assert (
    model_pointer.weight.shape == pretrained_weight.shape
), f"Pointer shape of random weight {model_pointer.shape} and array shape of checkpoint weight {pretrained_weight.shape} mismatched"

logger.info(f"Initialize PyTorch weight {layer_name} from {pretrained_weight.name}")

shape이나 name이 일치하지 않으면 랜덤 초기화 레이어에 잘못된 checkpoint 가중치를 할당했을 수 있어요. 잘못된 shape은 BrandNewLlama 파라미터가 원래 모델 파라미터와 정확히 일치하지 않기 때문일 수 있어요. 하지만 PyTorch 레이어 구현이 가중치를 먼저 transpose 해야 하기 때문일 수도 있어요.

Forward pass 구현하기 (Implement the forward pass)

모델이 올바르게 로드되면 다음으로 forward pass를 구현해야 해요. forward pass는 어떤 입력을 받아 모델 출력을 반환해요.

model = BrandNewLlamaModel.from_pretrained("/path/to/converted/checkpoint/folder")
input_ids = [0, 4, 4, 3, 2, 4, 1, 7, 19]
output = model.generate(input_ids).last_hidden_states

forward pass가 원래 모델의 출력과 동일하지 않거나 오류를 반환해도 낙담하지 마세요. forward pass가 오류를 던지지 않는지 확인해요. 이는 종종 차원이 잘못됐거나 잘못된 데이터 타입을 사용했기 때문이에요(torch.float32 대신 torch.long).

출력은 1e-3의 정밀도를 가져야 해요. 출력 shape과 출력 값이 동일한지 확인해요. 출력이 동일하지 않은 흔한 이유는:

  • 일부 레이어가 추가되지 않음(활성화 레이어나 잔차 연결)
  • word embedding 행렬이 tie 되어 있지 않음
  • 원래 구현이 오프셋을 포함해서 잘못된 positional embedding을 사용함
  • forward pass 중 dropout이 적용됨. model.training이 False인지 확인하고 self.training을 torch.nn.functional.dropout에 전달해서 이 오류를 고쳐요.

원래 모델과 여러분 구현의 forward pass를 비교해서 차이가 있는지 확인해요. 이상적으로는 두 구현의 forward pass 중간 출력을 디버깅하고 print 해서 원래 구현이 여러분 것과 어디에서 다른지 정확히 짚어내요.

  1. 두 구현 모두에 하드코딩된 input_ids가 동일한지 확인해요.
  2. input_ids의 첫 번째 변환(보통 word embeddings)의 출력이 동일한지 확인하고, 마지막 레이어까지 차근차근 진행해요.

두 구현 사이의 어떤 차이든 여러분 구현의 버그를 가리켜야 해요.

최고의 전략 중 하나는 두 구현의 같은 위치에 print 문을 많이 추가하고, 중간 출력이 동일한 값일 때 그것들을 차례로 제거하는 거예요.

두 구현이 같은 출력을 만들면 출력이 1e-3의 정밀도 안에 있는지 확인해요.

torch.allclose(original_output, output, atol=1e-3)

이것은 보통 과정에서 가장 어려운 부분이에요. 여기까지 왔다면 축하해요!

그리고 이 단계에서 막히거나 어려움을 겪고 있다면 pull request에서 주저 말고 도움을 요청해요.

모델 테스트 추가하기 (Add model tests)

모델이 동작하지만 여전히 Transformers와 호환되는지 확인하는 테스트를 추가해야 해요. 테스트는 사용자가 특정 테스트를 보면서 여러분의 작업을 이해하고, 어떤 변경이 있어도 미래에 모델이 깨지지 않게 하므로 중요해요.

Cookiecutter가 모델용 테스트 파일을 추가했을 거예요. 아래 테스트 파일을 실행해서 모든 공통 테스트가 통과하는지 확인해요.

pytest tests/models/brand_new_llama/test_modeling_brand_new_llama.py

통합 테스트는 이전에 Transformers에서 새 모델을 구현할 때 사용했던 디버깅 스크립트와 같은 목적을 제공하므로 먼저 추가해야 해요. Cookiecutter가 추가한 그 모델 테스트 템플릿인 BrandNewLlamaModelIntegrationTests를 채워야 해요. 통과하는지 확인하려면 다음 명령을 실행해요.

RUN_SLOW=1 pytest -sv tests/models/brand_new_llama/test_modeling_brand_new_llama.py::BrandNewLlamaModelIntegrationTests
SET RUN_SLOW=1 pytest -sv tests/models/brand_new_llama/test_modeling_brand_new_llama.py::BrandNewLlamaModelIntegrationTests

BrandNewLlama에 고유한 모든 기능은 BrandNewLlamaModelTester/BrandNewLlamaModelTest 아래의 별도 테스트에서 테스트해야 해요. 이 테스트는 종종 간과되지만 매우 중요해요. 왜냐하면:

  • 모델의 새로운 기능이 어떻게 동작하는지 보여줌으로써 과정에서 얻은 지식을 커뮤니티에 전달하는 데 도움이 돼요
  • 미래의 기여자는 이 특수 테스트를 실행해서 모델 변경을 빠르게 테스트할 수 있어요

Tokenizer 구현하기 (Implement tokenizer)

[!TIP] 사용자에게 최상의 성능을 주려면 fast tokenizer(PreTrainedTokenizerFast)를 추가하는 것을 권장해요. PreTrainedTokenizerFast 추가 방법에 대한 도움은 PR에서 @ArthurZucker나 @itazap을 태그해 주세요.

모델을 마쳤으니 이제 tokenizer에 집중할 차례예요. tokenizer는 Transformers의 기존 tokenizer와 동일하거나 매우 유사해야 해요.

원래 tokenizer 파일을 찾아 여러분의 구현에 로드해요. 원래 저장소에 문자열을 입력해서 input_ids를 반환하는 스크립트를 만들어요. 의사코드는 아래 코드와 비슷해야 해요.

input_str = "This is a long example input string containing special characters .$?-, numbers 2872 234 12 and words."
model = BrandNewLlamaModel.load_pretrained_checkpoint("/path/to/checkpoint/")
input_ids = model.tokenize(input_str)

원래 저장소를 검색해서 올바른 tokenizer 함수를 찾거나, input_ids만 반환하도록 원래 저장소 클론의 기존 tokenizer를 수정해야 할 수도 있어요. tokenizer 스크립트는 다음과 비슷해야 해요.

from transformers import BrandNewLlamaTokenizer

input_str = "This is a long example input string containing special characters .$?-, numbers 2872 234 12 and words."
tokenizer = BrandNewLlamaTokenizer.from_pretrained("/path/to/tokenizer/folder/")
input_ids = tokenizer(input_str).input_ids

두 구현이 같은 input_ids를 가지면 tokenizer 테스트 파일을 추가해요. 이 파일은 modeling 테스트 파일과 유사해요. tokenizer 테스트 파일에는 몇 가지 하드코딩된 통합 테스트가 있어야 해요.

Image processor 구현하기 (Implement image processor)

[!TIP] Image processor는 이제 backend 기반 아키텍처를 사용해요. 기본 backend는 TorchvisionBackend로, torchvision 라이브러리를 사용하고 GPU에서 이미지 처리를 수행할 수 있어요. PIL/NumPy 대안 backend(PilBackend)도 제공돼요. 두 backend 모두 image_processing_backends에서 import 돼요. 도움이 필요하면 @yonigozlan을 태그해 주세요.

이 예시에는 image processor가 없지만, 모델이 이미지 입력을 요구한다면 하나 구현해야 할 수도 있어요. image processor는 이미지를 모델에 적합한 형식으로 변환하는 책임이 있어요. 새로 구현하기 전에 Transformers 라이브러리의 기존 image processor를 재사용할 수 있는지 확인해요. 많은 모델이 비슷한 이미지 처리 기법을 공유하니까요. 또한 image processor에 modular을 사용해서 기존 컴포넌트를 재사용할 수 있다는 점도 참고해 주세요.

새 image processor를 구현해야 한다면 각 모델에는 두 개의 processor 파일이 있어요:

  • image_processing_<model>.py: 기본 torchvision 기반 processor(<Model>ImageProcessor), TorchvisionBackend를 상속해요. 이는 이전의 "fast" processor를 대체해요.
  • image_processing_pil_<model>.py: PIL/NumPy 대안 processor(<Model>ImageProcessorPil), PilBackend를 상속해요. 이는 이전의 "slow" processor를 대체해요.

torchvision backend 파일은 PIL 파일이 import 하는 커스텀 kwargs 클래스도 정의해요. 두 파일 모두 @auto_docstring 데코레이터를 사용해요. 수동 클래스 docstring을 추가하지 마세요. 단계별 안내와 완전한 예시는 IMAGE_PROCESSOR_REFACTORING_GUIDE.md를 참조해 주세요.

tests/models/your_model_name/test_image_processing_your_model_name.py에 image processor 테스트를 추가해요. 이 테스트들은 다른 image processor의 테스트와 비슷해야 하고 image processor가 이미지 입력을 올바르게 처리하는지 확인해야 해요. image processor에 고유한 기능이나 처리 메서드가 있다면 그것들에 대한 특정 테스트도 추가해야 해요.

Processor 구현하기 (Implement processor)

모델이 텍스트와 이미지 같은 여러 양식을 받아들인다면 processor를 추가해야 해요. processor는 모델에 전달하기 전에 서로 다른 양식의 전처리를 중앙화해요.

processor는 __call__ 함수 안에서 각 입력 타입을 올바르게 처리하기 위해 적절한 양식별 processor를 호출해야 해요. 라이브러리의 기존 processor를 확인해서 그들이 기대하는 구조를 이해하세요. Transformers는 __call__ 함수 시그니처에서 다음 관례를 사용해요.

def __call__(
    self,
    images: ImageInput = None,
    text: Union[TextInput, PreTokenizedInput, list[TextInput], list[PreTokenizedInput]] = None,
    audio=None,
    videos=None,
    **kwargs: Unpack[YourModelProcessorKwargs],
) -> BatchFeature:
    ...

YourModelProcessorKwargs는 전형적인 모든 처리 인자와 특정 processor가 요구할 수 있는 추가 인자를 포함하는 TypedDict예요.

tests/models/your_model_name/test_processor_your_model_name.py에 processor 테스트를 추가해요. 이 테스트들은 다른 processor의 테스트와 비슷해야 하고 processor가 서로 다른 양식을 올바르게 처리하는지 확인해야 해요.

통합 테스트 (Integration tests)

이제 모델과 tokenizer가 있으니 tests/models/brand_new_llama/test_modeling_brand_new_llama.py에 모델과 tokenizer에 대한 end-to-end 통합 테스트를 추가해요.

테스트는 모델이 기대대로 동작함을 보여주는 의미 있는 text-to-text 예시를 제공해야 해요. 예를 들어 원본→번역 쌍, 기사→요약 쌍, 질문→답변 쌍을 포함할 수 있어요.

checkpoint가 다운스트림 태스크로 파인튜닝되지 않았다면 모델 테스트로 충분해요.

마지막으로 모델 내부 텐서에 .to(self.device) 문을 추가해서 테스트가 GPU에서 실행될 수 있게 해 보세요. GPU에 접근할 수 없다면 우리가 처리해 줄게요.

문서 추가하기 (Add documentation)

모델은 사용자가 어떻게 사용하는지 알아야만 유용해요. 그래서 문서와 docstring을 추가하는 게 중요해요. Cookiecutter가 docs/source/model_doc/brand_new_llama.md 템플릿 파일을 추가했는데, 여기에 모델에 대한 정보를 채울 수 있어요.

이것은 보통 사용자가 모델과 처음 상호작용하는 곳이므로 문서는 명확하고 간결해야 해요. 모델을 어떻게 사용해야 하는지에 대한 예시를 추가하는 것이 매우 유용한 경우가 많아요.

src/transformers/models/brand_new_llama/modeling_brand_new_llama.py에 docstring이 추가되고 필요한 모든 입력과 출력을 포함하는지 확인해요. 문서와 docstring 작성에 대한 우리의 가이드를 검토해 주세요.

리팩터링 (Refactor)

정리하고 코드 스타일이 라이브러리의 나머지와 일관되게 하는 시간이에요. 다음 명령을 실행해서 잘못된 스타일을 자동으로 고쳐요.

make style

코드 스타일이 품질 검사를 통과하는지 확인하려면 아래 명령을 실행해요.

make check-repo

Transformers의 엄격한 설계 테스트 때문에 pull request에 다른 실패 테스트나 검사(docstring 누락이나 잘못된 이름 등)가 있을 수 있어요. 막히면 이런 문제를 도와줄게요.

코드가 올바르게 실행되는지 확인한 후, 더 읽기 쉽거나 깔끔하게 리팩터링하고 싶을 수도 있어요.

Hub에 업로드하기 (Upload to the Hub)

모든 checkpoint를 변환해서 Hub에 업로드해요. 모델에 대한 더 많은 투명성과 맥락을 제공하려면 모델 카드를 추가해요. 모델 카드는 checkpoint의 특정 특성, 모델이 어떻게 학습됐는지, 어떻게 사용하는지에 대한 코드 예시를 강조해야 해요.

[!TIP] 많은 경우 사용자가 실행할 수 있는 대화형 노트북을 추가하는 것은 추론이나 다운스트림 태스크에서 모델을 파인튜닝하는 방법을 보여주는 좋은 방법이에요. 필수는 아니지만, 노트북을 포함하면 모델의 더 큰 채택을 이끌 수 있어요.

또한 모델에 적절한 이름을 정하고 모델을 업로드하는 데 필요한 접근 권한을 얻기 위해 Transformers 팀과 상의해야 해요.

모델을 업로드하려면 push_to_hub() 메서드를 사용해요.

brand_new_bert.push_to_hub("brand_new_llama")

모델을 Hub에 업로드하는 방법에 대한 자세한 내용은 Sharing 가이드를 참조해 주세요.

모델 병합하기 (Merge your model)

드디어 pull request를 병합하고 모델을 공식적으로 Transformers에 추가할 준비가 됐어요! 모든 테스트가 통과하고 모든 댓글과 피드백이 처리됐는지 확인해요.

Transformers에 새 모델을 추가한 것을 축하해요! 🥳

이것은 매우 중요한 기여예요. 여러분의 작업은 Transformers를 전 세계의 개발자와 연구자에게 더 접근 가능하게 만들어요. 여러분의 기여를 자랑스러워하고 커뮤니티와 성과를 공유하세요!

함께 보기 (See also)

  • 모델 구조 규칙 — 모든 modeling_*.py와 configuration_*.py 파일에 적용되는 정적 규칙. PR을 열기 전에 make typing을 실행해 확인해 주세요.
  • Pull request 검사 — PR에서 어떤 CI 검사가 실행되고 그것을 통과하는 방법에 대한 전체 참조.

더 알아보기 (Learn more)