본문 바로가기
Theory/Pytorch

[파이토치 트랜스포머 #5] 3장 파이토치 기초 - 4) 모델/데이터세트 분리, 모델 저장 및 불러오기

by 남디윤 2024. 3. 6.

 

 

모델/데이터세트 분리

  • 파이토치의 모델은 인공 신경망 모듈을 활용해 구현
    • 데이터에 대한 연산 수행, 계층 정의, 순방향 연산 수행
    • 신경망 패키지의 모듈 클래스 활용
    • 새로운 모델 클래스를 생성하려면 모듈 클래스를 상속받아 임의의 서브 클래스를 생성
    • 이 클래스는 다른 모듈 클래스를 포함할 수 있으며 트리 구조로 중첩 가능

 

모듈 클래스

class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1=nn.Conv2d(1, 20, 5)
        self.conv2=nn.Conv2d(20, 20, 5)

    def forward(self, x):
        x=F.relu(self.conv1(x))
        x=F.relu(self.conv2(x))
        return x
  • 초기화 메서드 init 와 순방향 메서드 forward를 재정의해 활용
  • 초기화 메서드 init
    • 신경망에 사용될 계층을 정의하기 전에 super 함수로 모듈 클래스의 속성을 초기화
    • super 함수로 부모 클래스를 초기화하면 서브 클레스인 모델에서 부모 클래스의 속성 사용 가능
    • 모델 클래스 초기화 이후, 학습에 사용되는 계층을 초기화 메서드에 선언
    • self.conv1이나 self.conv2와 같은 인스턴스가 모델의 매개변수가 됨
  • 순방향 메서드 forward
    • 초기화 메서드에서 선언한 모델 매개변수를 활용해 신경망 구조를 설계
    • 모델이 데이터 x를 입력받아 학습을 진행하는 일련의 과정을 정의하는 영역
    • 모듈 클래스는 호출 가능한 형식 Callable Type 으로 모델의 인스턴스를 호출하는 순간 호출 메서드 call가 순방향 메서드를 실행
  • 초기화 메서드에서 super 함수로 부모 클래스를 초기화했으므로 역방향 backward 연산을 정의하지 않아도 됨
    • 파이토치의 자동 미분 기능인 Autograd 에서 모델의 매개변수를 역으로 전파해 자동으로 기울기 또는 변화도를 계산해줌 → 별도의 메서드로 역전파 기능 구성 필요X

 

비선형 회귀

  • 생략
import torch
import pandas as pd
from torch import nn
from torch import optim
from torch.utils.data import Dataset, DataLoader

class CustomDataset(Dataset):
    def __init__(self, file_path):
        df = pd.read_csv(file_path)
        self.x = df.iloc[:, 0].values
        self.y = df.iloc[:, 1].values
        self.length = len(df)

    def __getitem__(self, index):
        x = torch.FloatTensor([self.x[index] ** 2, self.x[index]])
        y = torch.FloatTensor([self.y[index]])
        return x, y

    def __len__(self):
        return self.length

class CustomModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = nn.Linear(2, 1)

    def forward(self, x):
        x = self.layer(x)
        return x

train_dataset = CustomDataset("datasets/non_linear.csv")
train_dataloader = DataLoader(train_dataset, batch_size=128, shuffle=True, drop_last=True)

device = "cuda" if torch.cuda.is_available() else "cpu"
model = CustomModel().to(device)
criterion = nn.MSELoss().to(device)
optimizer = optim.SGD(model.parameters(), lr=0.0001)

for epoch in range(10000):
    cost = 0.0

    for x, y in train_dataloader:
        x = x.to(device)
        y = y.to(device)

        output = model(x)
        loss = criterion(output, y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        cost += loss

    cost = cost / len(train_dataloader)

    if (epoch + 1) % 1000 == 0:
        print(f"Epoch : {epoch+1:4d}, Model : {list(model.parameters())}, Cost : {cost:.3f}")

 

모델 평가

  • 비선형 회귀에서 학습시킨 모델 사용
    with torch.no_grad():
        model.eval()
        inputs = torch.FloatTensor(
            [
                [1 ** 2, 1],
                [5 ** 2, 5],
                [11 ** 2, 11]
            ]
        ).to(device)
        outputs = model(inputs)
        print(outputs)​
  • torch.no_grad 클래스 활용
    • 기울기 계산을 비활성화하는 클래스. 자동 미분 기능을 사용하지 않도록 설정. 메모리 사용량 줄여 추론에 적합한 상태로 변경
  • eval 메서드
    • 모델 평가 모드
    • 모델을 평가모드로 변경하지 않으면 일관성 없는 추론 결과를 반환하기 때문에 평가 시 평가모드 선언 필요
    • 다시 학습을 진행하려는 경우, 다시 학습 모드 변경 필요
  • 모델 저장
    torch.save(
        model,
        "../models/model.pt"
    )​
    torch.save(
        model.state_dict(),
        "../models/model_state_dict.pt"
    )​

 

 

데이터세트 분리

  • 머신러닝 데이터세트는 훈련용 데이터 Training Data, 테스트 데이터 Testing Data, 검증용 데이터 Validation Data 분리해 활용
  • 훈련용 데이터
    • 모델 학습하는 데 사용되는 데이터세트
  • 검증용 데이터
    • 학습이 완료된 모델을 검증하기 위해 사용되는 데이터세트
    • 구조가 다른 모델의 성능 비교를 위해 사용되는 데이터세트를 의미
    • 계층이나 하이퍼파라미터 차이 등으로 인한 성능 비교를 위해 전체 데이터세트를 분리
  • 테스트 데이터
    • 검증용 데이터를 통해 결정된 성능이 가장 우수한 모델을 최종 테스트하기 위한 목적으로 사용되는 데이터세트
    • 검증용 데이터에 의해 선택된 모델은 검증용 데이터에 과대적합된 모델이거나 검증용 데이터가 해당 모델에 적합한 형태의데이터만 모여 있을 수도 있음
    • 기존 과정에서 평가해 보지 않은 새로운 데이터인 테스트 데이터로 최종 모델 성능 평가
  • 무작위 분리 함수 random_split
    subset=torch.utils.data.random_split(
    	dataset,
    	lengths,
    	generator
    )
    
    # 사용 예시
    dataset = CustomDataset("../datasets/non_linear.csv")
    dataset_size = len(dataset)
    train_size = int(dataset_size * 0.8)
    validation_size = int(dataset_size * 0.1)
    test_size = dataset_size - train_size - validation_size
    
    train_dataset, validation_dataset, test_dataset = random_split(dataset, [train_size, validation_size, test_size])
    print(f"Training Data Size : {len(train_dataset)}")
    print(f"Validation Data Size : {len(validation_dataset)}")
    print(f"Testing Data Size : {len(test_dataset)}")
    
    train_dataloader = DataLoader(train_dataset, batch_size=16, shuffle=True, drop_last=True)
    validation_dataloader = DataLoader(validation_dataset, batch_size=4, shuffle=True, drop_last=True)
    test_dataloader = DataLoader(test_dataset, batch_size=4, shuffle=True, drop_last=True)_size])​
      
    • 분리 길이 lengths 만큼 데이터세트 dataset의 서브셋 subset을 생성
    • 입력한 데이터세트의 개수와 분리 길이의 총합이 같아야 함
    • 생성자 generator는 서브셋에 포함될 무작위 데이터들의 난수 생성 시드를 의미
  • 검증용 데이터세트를 통한 평가
    with torch.no_grad():
        model.eval()
        for x, y in validation_dataloader:
            x = x.to(device)
            y = y.to(device)
            
            outputs = model(x)
            print(f"X : {x}")
            print(f"Y : {y}")
            print(f"Outputs : {outputs}")
            print("--------------------")​

 

 

 

 

 

모델 저장 및 불러오기

  • 파이토치 모델은 직렬화 Serialize와 역직렬화 Deserialize를 통해 객체를 저장하고 불러올 수 있음
    • 파이썬의 피클 Pickle을 활용해 파이썬 객체 구조를 바이너리 프로토콜 Binary Protocols로 직렬화함
    • 모델에 사용된 텐서나 매개변수를 저장함
    • 모델을 불러오려면 저장된 객체 파일을 역직렬화해 현재 프로세스의 메모리에 업로드
  • 주로 모델 학습이 모두 완료된 이후에 모델을 저장하거나, 특정 에폭이 끝날 때마다 저장함
  • 모델 파일 확장자는 주로 .pt 나 .pth로 저장함

 

모델 전체 저장/불러오기

  • 모델 저장 함수 
    • 저장하려는 모델 인스턴스 model 와 학습 결과 파일이 생성될 경로 path를 설정해 학습된 모델을 저장
  • torch.save( model, path )​
  • 모델 불러오기 함수 
    • 모델이 저장된 경로 path를 불러와 모델의 매개변수와 메타데이터를 적용해 인스턴스를 생성
    • map_location 매개변수: 모델을 불러올 때 적용하려는 장치 상태를 의미
      • 모델 학습 상태가 GPU인지 CPU인지 확인하지 않고 모델을 활용할 수 있게, map_location 매개변수로 장치 설정
    • 모델을 불러오는 경우에도 동일한 형태의 클래스 선언 필수
      • 이 코드처럼 그냥 단순히 load 하면 AttributeError 오류 발생, 모델 로드 불가
        import torch
        
        device = "cuda" if torch.cuda.is_available() else "cpu"
        model = torch.load("model/model.pt", map_location=device)
        print(model)​
    • 아래와 같이 CustomModel을 동일하게 선언하거나, 다음 모델 구조 확인을 통해 할 수도 있음
      # 1) 아예 CustomModel 동일하게 선언
      import torch
      from torch import nn
      
      
      class CustomModel(nn.Module):
          def __init__(self):
              super().__init__()
              self.layer = nn.Linear(2, 1)
      
          def forward(self, x):
              x = self.layer(x)
              return x
      device = "cuda" if torch.cuda.is_available() else "cpu"
      model = torch.load("model/model.pt", map_location=device)
      print(model)
      with torch.no_grad():
          model.eval()
          inputs = torch.FloatTensor(
              [
                  [1 ** 2, 1],
                  [5 ** 2, 5],
                  [11 ** 2, 11]
              ]
          ).to(device)
          outputs = model(inputs)
          print(outputs)​
  • model=torch.load( path, map_location )​
  • 모델 구조 확인
    • 모델 전체 파일은 가지고 있으나, 모델 구조를 알 수 없는 경우
      # 2) 모델 구조 출력
      import torch
      from torch import nn
      
      
      class CustomModel(nn.Module):
          pass
      device = "cuda" if torch.cuda.is_available() else "cpu"
      model = torch.load("model/model.pt", map_location=device)
      print(model)​​

 

 

 

모델 상태 저장/불러오기

  • 모델 전체를 저장하고 불러오는 방법이 아닌, 모델 상태 값만 저장해 불러오는 방법
    • 모델 전체를 저장하면 모델의 모든 정보를 저장 → 많은 저장 공간이 필요
  • 모델 상태 저장
    torch.save(
        model.state_dict(),
        "../models/model_state_dict.pt"
    )​
    • model 변수가 아닌 state_dict 메서드로 모델 상태를 저장
    • 모델 상태 torch.state_dict: 모델에서 학습이 가능한 매개변수를 순서가 있는 딕셔너리 OrderedDict 형식으로 반환
  • 모델 상태 불러오기
    import torch
    from torch import nn
    
    
    class CustomModel(nn.Module):
        def __init__(self):
            super().__init__()
            self.layer = nn.Linear(2, 1)
    
        def forward(self, x):
            x = self.layer(x)
            return x
    device = "cuda" if torch.cuda.is_available() else "cpu"
    model = CustomModel().to(device)
    
    model_state_dict = torch.load("model/model_state_dict.pt", map_location=device)
    model.load_state_dict(model_state_dict)
    with torch.no_grad():
        model.eval()
        inputs = torch.FloatTensor(
            [
                [1 ** 2, 1],
                [5 ** 2, 5],
                [11 ** 2, 11]
            ]
        ).to(device)
        outputs = model(inputs)​
    • 모델의 상태만 저장했으므로 CustomModel에 학습 결과 반영
    • 모델 상태만 불러오면 모델 구조를 알 수 없으므로 CustomModel 클래스가 동일하게 구현돼 있어야 함
    • 모델 상태 파일 pt도 torch.load 함수를 통해 불러옴
    • 모델의 가중치와 편향을 모델에 적용해야 하기 때문에 model 인스턴스의 load_state_dict 메서드 사용해 모델 상태를 반영함

 

 

체크포인트 저장/불러오기

  • 체크포인트 Checkpoint: 학습 과정의 특정 지점마다 저장하는 것을 의미
    • 데이터의 개수가 많고 깊은 구조의 모델을 학습하면 오랜 시간 소요됨
    • → 학습 과정에서 한 번에 전체 에폭을 반복하기 어렵거나, 모종의 이유로 학습 중단 가능성 존재
    • → 일정 에폭마다 학습된 결과를 저장, 나중에 이어서 학습
  • 체크포인트마다 모델 상태를 저장해 가장 최적화된 모델 상태로 추론 진행 가능
  • 체크포인트 저장
    # 중략
    
    checkpoint = 1
    for epoch in range(10000):
        cost = 0.0
    
        for x, y in train_dataloader:
            x = x.to(device)
            y = y.to(device)
    
            output = model(x)
            loss = criterion(output, y)
    
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
    
            cost += loss
    
        cost = cost / len(train_dataloader)
    
        if (epoch + 1) % 1000 == 0:
            torch.save(
                {
                    "model": "CustomModel",
                    "epoch": epoch,
                    "model_state_dict": model.state_dict(),
                    "optimizer_state_dict": optimizer.state_dict(),
                    "cost": cost,
                    "description": f"CustomModel 체크포인트-{checkpoint}",
                },
                f"../models/checkpoint-{checkpoint}.pt",
            )
            checkpoint += 1​
    • 학습을 이어서 진행하기 위한 목적이기 때문에
      • 에폭 epoch, 모델 상태 model.state_dict, 최적화 상태 optimizer.state_dict 등은 필수로 포함 필요
  • 체크포인트 불러오기
    # 중략
    
    checkpoint = torch.load("../models/checkpoint-6.pt")
    model.load_state_dict(checkpoint["model_state_dict"])
    optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
    checkpoint_epoch = checkpoint["epoch"]
    checkpoint_description = checkpoint["description"]
    print(checkpoint_description)
    
    for epoch in range(checkpoint_epoch + 1, 10000):
        cost = 0.0
    
        for x, y in train_dataloader:
            x = x.to(device)
            y = y.to(device)
    
            output = model(x)
            loss = criterion(output, y)
    
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
    
            cost += loss
            if (epoch + 1) % 1000 == 0:
                print(f"Epoch : {epoch+1:4d}, Model : {list(model.parameters())}, Cost : {cost:.3f}")​