모델 학습의 효율성을 위해

한승수·2024년 9월 12일
post-thumbnail

크기가 큰 데이터셋을 학습하는 과정은 정말 오래걸린다. 내 GPU의 한계도 있겠지만 그래도 조금이나마 속도를 높여 5분이라도 학습을 일찍 끝낼 수 있는 방법은 없을까?

오늘은 3가지 정도의 방법론을 설명하고자 합니다.

1. Data 캐싱

  • 반복적인 과정을 미리 처리해두어 반복적으로 데이터를 로드하는 등의 시간을 줄임으로써 처리 속도를 향상
  • Custom Dataset의 __getitem__()함수는 DB에 있는 데이터를 불러와 Dataloader에서 배치단위로 Data를 처리 할 때 해당 index의 데이터를 뽑아주는 역할을 합니다. 그러나 이런 과정에서 __getitem__() 함수가 활욜 될 때 마다 매번 이미지를 load할 경우, 비효율적인 과정이 반복해서 일어납니다.
  • 이를 해결하기 위해서 아래의 방법 등을 제안할 수 있습니다.
    1. 미리 image를 load하고 npy파일로 저장해두고, indexing하는 방식
    - 데이터를 __getitem__()에서 load하는 것이 아닌, 미리 load를 해놓은 뒤 indexing을 통해 원하는 데이터를 불러오는 방식입니다.
    2. 첫번째 epoch을 돌 경우에만 불러오고, 이후에는 불러온 image에서 뽑아 쓰는 방식
    - 이 방법은 Image를 __getitem__() 함수를 통해 load를 하되, 한번 load된 이미지는 다시 load하지 않는 방법으로 주로 첫번째 epoch 때만 데이터를 load하고 그 이후에는 load된 이미지를 indexing하는 방법으로 epoch이 진행됩니다.
             def __getitem__(self,idx):
             	if self.images[idx] is None:
             		img_path = os.path.join(self.root_dir,self.df[idx]['image_path']
             		image = cv2.imread(img_path,cv2.IMREAD_COLOR)
             		image = cv2.cvtColor(image,cv2.COLOR_BGR2RGB)
             		self.image[idx] = image #image를 첫 epoch에만 load 후 list에 저장
             	else:
             	  image =	self.image[idx] #두번째 epoch부터 list에서 뽑아씀

2. Gradient Accumulation

  • Gradient를 매 batch마다 업데이트 하는 것이 아니라 여러 배치의 gradient를 파라미터에 누적해서 업데이트 하는 방식

  • GPU 메모리 제한으로 큰 배치 사이즈를 처리할 수 없을 때 활용할 수 있으며 더 큰 가상의 배치 사이즈를 사용하는 것과 유사한 효과를 냄

    for i,(images,targets) in enumerate(train_loader):
    	images,targets = images.to(device), targets.to(device)
    	outputs = self.model(images)
    	loss = self.loss_fn(outputs,targets)
    	loss = loss/accumulateion_steps #누적을 위한 loss 평균값 계산
    	loss.backward() #backpropagation 연산해서 parameter.grad에 누적됨
    	total_loss += loss.item()
    	
    	accumulation_steps = 4 #(4개의 배치마다 가중치 업데이트)
    	if(i+1)%accumulation_steps == 0 or (i+1) == len(self.train_loader):
    		self.optimizer_step() #가중치 업데이트
    		self.optimizer.zero_grad() #가중치 0으로 초기화
    		self.scheduler.step()
    	
    	return total_loss / len(self.train_loader)

3. Mixed Precision Training

  • torch에서는 Float32형을 표준 부동 소수점 형식으로 활용. 정밀도가 높아 기본 계산 단위로 활용됨
  • 반면에 Float16은 정밀도는 낮지만 속도 측면에서 매우 유리
  • 정밀한 연산이 필요한 loss계산이나 model weight update와 같이 모델의 성능과 관련된 연산은 Flaot32로 계산, 그 외는 Float16으로 연산하여 두 Float유형의 단점을 줄이고 장점을 극대화 할 수 있음
    from torch.cuda.amp import autocast, GradScaler
    
    scaler = GradScaler()  # FP16에서 gradient scaling을 처리하기 위한 도구
    
    for input, target in data_loader:
        optimizer.zero_grad()
    
        with autocast(): #FP16으로 연산되는 부분
            output = model(input)  # 모델의 연산이 자동으로 혼합 정밀도로 처리됨
            loss = loss_fn(output, target)
    
        # 역전파 시 기울기를 스케일링하여 안정성을 높임
        scaler.scale(loss).backward() #loss는 FP16으로 처리됐기 때문에 scale로 FP32로 맞춰줘야함
    
        # 스케일된 기울기를 사용해 optimizer를 적용
        scaler.step(optimizer)
        scaler.update()
profile
Grooovy._.Han

0개의 댓글