영상을 보고 정리해보는 LoRA ~
🔆 Low-rank Adaptation == LoRA
행렬에서 rank란,행렬의 열 벡터들이 서로 독립적인 벡터의 개수를 의미한다. 즉, 선형적으로 독립인 열 벡터의 최대 수를 의미한다.
즉, 랭크가 낮을 수록 행렬의 정보가 중복되거나 불필요하다는 것을 의미한다. 따라서 랭크가 낮다면, 행렬의 차원을 줄일 수도 있다는 것을 의미한다.
절차를 살펴보면 다음과 같다.
원래 모델의 파라미터들을 고정한다. 원래 모델의 웨이트에 대해서는 오직 read만 수행하며, 백프로파게이션 등을 수행하지 않는다.
동일한 지도 학습 프로세스를 사용하여 한 쌍의 low-rank decomposition 행렬을 주입하고, 훈련한다 (사진 상에서는 2~3 step)
인퍼런스 과정에서는, 한 쌍의 행렬을 곱하여, 고정해두었던 원래 모델의 파라미터와 같은 크기의 행렬을 만든다음, 두 행렬을 더하여 업데이트한다.
웨이트 파라미터에 대해서 자세히 보게 되면, 영상에서는 예를 들어 설명하고 있다.
만약 Attention is All You Need 논문에서 사용한 트랜스포머 아키텍처를 사용하고, rank는 8로 설정한다고 가정해보자.
또한, 태스크를 여러개 수행하는 상황에서도, full-finetuning을 사용했을 때 처럼 개별 태스크에 대한 모델을 각각 정의할 것이 아니라, LoRA 매트릭스 값만 업데이트 해주면 되니, 메모리 측면에서 상당히 효율적이다.
low-rank approximation process에서 모델 성능에 영향을 미칠 수 있는 정보 손실이 일어날 수 있다. 연산량과 성능의 트레이드 오프 ..
논문의 실험 파트에서도 볼 수 있듯, 랭크를 얼마로 설정하냐가 성능에 큰 영향을 끼친다. 따라서 랭크 값을 잘 설정하는 것이 중요하다.
👉 github link : 마이크로 소프트에서 제공하는 오피셜 코드
nn.Linear, nn.Embedding, nn.Conv2d
을 대체할 수 있는 클래스들을 loralib/
에서 각각 제공하고 있다. nn.Linear
가 둘 이상의 레이어를 나타내는 경우를 위하여 nn.MergedLinear
도 제공하고 있다.class Embedding(nn.Embedding, LoRALayer)
: PyTorch의 nn.Embedding
모듈을 상속받아 LoRA 레이어를 추가한 클래스를 대표로 살펴보면 다음과 같다.class Embedding(nn.Embedding, LoRALayer):
# LoRA implemented in a dense layer
def __init__(
self,
num_embeddings: int,
embedding_dim: int,
r: int = 0,
lora_alpha: int = 1,
merge_weights: bool = True,
**kwargs
):
nn.Embedding.__init__(self, num_embeddings, embedding_dim, **kwargs)
LoRALayer.__init__(self, r=r, lora_alpha=lora_alpha, lora_dropout=0,
merge_weights=merge_weights)
# Actual trainable parameters
# LoRA 레이어에 대한 학습 가능한 가중치(한 쌍의 low-rank 분해 행렬) 를 초기화 함
if r > 0:
self.lora_A = nn.Parameter(self.weight.new_zeros((r, num_embeddings)))
self.lora_B = nn.Parameter(self.weight.new_zeros((embedding_dim, r)))
self.scaling = self.lora_alpha / self.r
# Freezing the pre-trained weight matrix
self.weight.requires_grad = False
self.reset_parameters()
def reset_parameters(self):
# 임베딩 레이어의 가중치를 초기화
nn.Embedding.reset_parameters(self)
if hasattr(self, 'lora_A'):
# initialize A the same way as the default for nn.Linear and B to zero
nn.init.zeros_(self.lora_A)
nn.init.normal_(self.lora_B)
def train(self, mode: bool = True):
nn.Embedding.train(self, mode)
if mode:
if self.merge_weights and self.merged:
# Make sure that the weights are not merged
if self.r > 0:
self.weight.data -= (self.lora_B @ self.lora_A).transpose(0, 1) * self.scaling
self.merged = False
else:
if self.merge_weights and not self.merged:
# Merge the weights and mark it
if self.r > 0:
self.weight.data += (self.lora_B @ self.lora_A).transpose(0, 1) * self.scaling
self.merged = True
def forward(self, x: torch.Tensor):
if self.r > 0 and not self.merged:
result = nn.Embedding.forward(self, x)
after_A = F.embedding(
x, self.lora_A.transpose(0, 1), self.padding_idx, self.max_norm,
self.norm_type, self.scale_grad_by_freq, self.sparse
)
result += (after_A @ self.lora_B.transpose(0, 1)) * self.scaling
return result
else:
return nn.Embedding.forward(self, x)
reset_parameters
: 행렬 lora_A와 lora_B을 초기화한다. 이 때, 논문과 동일하게 0과 가우시안 분포로 초기화한다.train
: 파라미터(mode, merged)에 따라서, weight를 업데이트 하거나 복원을 한다.self.weight.data += (self.lora_B @ self.lora_A).transpose(0, 1) * self.scaling
이렇게 표현된다. 행렬 곱을 하고 scaling을 해준 다음, 원래 웨이트와 더한다. 간단.forward
: 입력이 들어오면 임베딩을 넘겨주는 포워드 패스 연산을 정의하고 있다. nn.Embedding.forward(self, x)
를 통해 기존의 임베딩 레이어를 사용하여 result를 계산한다. F.embedding
을 통해 입력 x와 self.lora_A (파인튜닝된 파라미터)의 임베딩 결과인 after_A를 계산한다.
(self.lora_B @ self.lora_A) 를 해 준것에 왜 전치(transpose(0,1)) 를 해주는건가요? 이미 원래의 차원과 동일한 상황인 것 같은데