관리 메뉴

Bull

[ML] 데이터 세트와 데이터 분리 | study book 본문

Artificial Intelligence/Machine Learning

[ML] 데이터 세트와 데이터 분리 | study book

Bull_ 2024. 8. 18. 01:36

데이터 세트는 데이터의 집합을 의미하며 입력값과 결과 값에 대한 정보를 제공하거나 일련의 데이터 묶음을 전달한다. 모듈화, 재사용성, 가독성 등을 향상시키기 위해 데이터 세트와 로더를 사용한다.

데이터 세트

데이터 세트는 학습에 필요한 데이터 샘플을 정제하고 정답을 저장하는 기능을 제공한다. 초기화 메소드(__init__), 호출 메소드(__getitem__), 길이 반환 메소드(__len__) 등을 재정의하여 사용한다.

데이터 로더

데이터 로더는데이터 세트에 저장된 데이터를 어떤 방식으로 불러와 활용할 지 정의한다. 배치 크기(batch size), 데이터 순서 변경(shuffle), 데이터 로드 프로세스 수(num_workers) 등의 기능을 제공한다.

 

배치는 데이터의 개수가 많아 에폭에서 모든 데이터를 올릴 수 없을 때 나누는 역할을 한다. 데이터 순서 변경은 데이터를 순서대로 학습하는 것을 방지하고자 수행한다. 순서를 그대로 사용하면 모델이 순서까지 학습해버리기 때문에 과대적합을 막기 위함이다. 여기서 순서는 배치 데이터 순서가 아닌 단일 데이터 x1, x2, ... 에대한 순서이다.

데이터 로드 프로세스 수는 데이터를 불러올 때 사용할 프로세스의 개수이다. 학습 제외한 코드에서 데이터를 불러올 때 시간이 가장 오래 걸린다. 따라서 프로세스의 개수를 늘려 성능을 올리는 방법도 중요하다

다중 선형 회귀

import torch
from torch import nn
from torch import optim
from torch.utils.data import TensorDataset, DataLoader 


train_x = torch.FloatTensor([
    [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]
])
train_y = torch.FloatTensor([
    [0.1, 1.5], [1, 2.8], [1.9, 4.1], [2.8, 5.4], [3.7, 6.7], [4.6, 8]
])
train_dataset = TensorDataset(train_x, train_y)
train_dataloader = DataLoader(train_dataset, batch_size=2, shuffle=True, drop_last=True)

배치 크기를 2로 선언하여 한 번의 배치마다 두 개의 데이터 샘플과 정답을 가져오게 한다. x를 기준으로 [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7] 처럼 2개, 2개, 2개, ... 씩 나눠서 학습을 진행 한다는 뜻이다. drop_last 는 배치 크기에 맞지 않는 나머지 데이터를 없앨 때 사용한다.

model = nn.Linear(2, 2, bias=True)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.001)
for epoch in range(20000):
    cost = 0.0

    for batch in train_dataloader:
        x, y = batch
        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}")

여기서 배치만큼 학습을 해주었기 때문에 cost를 0으로 초기화하고 train_dataloader 크기만큼 나눠준다. train_dataloader로 나눠주는 이유는 MSE 비용함수를 배치 개별적으로 적용하기 때문이다.

모델/데이터 분리

파이토치 모델은 인공 신경망 모듈을 활용해 구현된다. 새로운 모델 클래스를 생성하려면 클래스를 상속받아 서브 클래스를 생성하는데 이 클래스는 트리 구조로 중첩할 수 있다.

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 메소드 를 재정의한다.

 

순방향 메소드는 초기화 메서드에서 선언한 모델 매개변수를 활용해 신경망 구졸르 설계한다. 모듈 클래스는 호출 가능한 형식으로 모델의 인스턴스를 호출하는 순간 호출 메서드(__call__)가 순방향 메소드를 실행한다.

 

초기화 메소드에서 super 함수로 부모 클래스를 초기화 했으므로 역방향 메소드는 정의할 필요 없다. 자동 미분 기능인 Autograd 에서 모델의 매개 변수를 역으로 전파해 자동으로 기울기 또는 변화도를 계산한다.

비선형 회귀

책에서는 비선형 회귀를 구현하기 위해 데이터 셋이 주어진다. $y=3.1x^2-1.7x+random(0.01,0.99)$의 관계를 갖는다.

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

df에 csv 데이터가 저장된다. x는 1열의 모든 값을 리스트 형태로 저장한다. y도 마찬가지. length 200으로 행의 크기를 나타낸다. 각 짝으로 200개씩 존재한다.

 

__getitem__ 메소드는 [x^2,x] 형태로 가져온다. 왜냐하면 y=(이차식) 이기 때문에 해당 벡터만 필요한 것이다.

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

사용자가 정의한 신경망 모델이다. layer는 입력 차원 2, 출력 차원이 1인 선형 변환 함수이다.

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

데이터 셋을 받고 데이터 로더를 정의한다. 배치사이즈는 128이다.

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}")

학습과정이다. 이전에 배운 것과 달라진 점을 하나 꼽자면 to 메소드이다. to 메소드는 어떤 장치를 가지고 연산할 지 지정한다. 대개 파이토치의 모델 인스턴스나 프로퍼티 뒤에 붙이면 되는 거 같다.

Epoch : 10000, Model : [Parameter containing:
tensor([[ 3.1075, -1.7030]], requires_grad=True), Parameter containing:
tensor([0.0416], requires_grad=True)], Cost : 0.155

약 10000회 학습 째, 가중치와 편향(가중치)이 구해졌다. $y=3.1x^2-1.7x+random(0.01,0.99)$로 구성돼 제대로 Fit 된 거 같다.

모델 평가

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.eval() 메소드는 모델 평가 모드로 변경한다. 임의의 값 inputs 을 모델에 넣어 모델을 평가하고 torch.no_grad 클래스를 활용한다. 기울기 계산을 비활성화하는 역할을 한다. 즉 자동 미분 기능을 사용하지 않도록 설정하여 메모리 사용량을 줄이고 추론에 적합한 상태로 변경한다. with 는 객체 생성 -> 사용 -> 소멸을 바로 해주는 키워드이다.

모델 저장

다음은 모델을 저장하는 두 가지 방식이다.

torch.save(
    model,
    "/content/model.pt"
)
torch.save(
    model.state_dict(),
    "/content/model_state_dict.pt"
)

추론을 위해 모델을 저장할 때는 그 모델의 학습된 매개변수만 저장하면 된다. torch.save() 를 사용하여 모델의 state_dict 를 저장하는 것이 나중에 모델을 사용할 때 가장 유연하게 사용할 수 있는, 모델 저장 시 권장하는 방법이다. state_dict는 모델 상태를 순서가 있는 딕셔너리 형식으로 반환한다.

 

추론을 실행하기 전에 반드시 model.eval() 을 호출하여 드롭아웃 및 배치 정규화를 평가 모드로 설정하여야 한다. 이 과정을 거치지 않으면 일관성 없는 추론 결과가 출력된다.

데이터 세트 분리

머신러닝에서 데이터 세트는 2개 또는 세분화하면 3가지 까지 분류된다. 기본적으로 훈련용 데이터/테스트 데이터, 근데 여기서 더 세분화하면 훈련용데이터/검증용데이터 와 테스트데이터로 나뉜다.

 

훈련용 데이터는 모델을 학습하는데 사용된다. 검증용 데이터는 학습이 완료된 모델을 검증하기 위해 사용되는 데이터이다. 주로 구조가 다른 모델의 성능 비교를 위해서 사용되는 데이터 셋이다. 테스트 데이터는 검증용 데이터를 통해 결정된 성능이 가장 우수한 모델을 최종 테스트하기 위핸 목적으로 사용된다.

 

예를 들어 수험생이 수능 점수 향상을 위해 문제집을 푼다고 가정하자. 문제지 = 훈련용, 답안지 = 검증용, 실제 시험지 = 테스트 데이터로 비유할 수 있다.

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)

torch.util.data 모듈에서는 random_split 메소드를 실행시킨다.

subset = torch.utils.data.random_split(
  dataset,
  lengths,
  generator
)

random_split 메소드는 분리 길이만큼 데이터셋의 서브셋을 생성한다. 예를 들어 [300,100,50]을 lengths에 입력되면 서브셋은 3개가 생성되고 순서대로 300,100,50개의 데이터를 생성한다. 마지막의 generator는 랜덤시드이다.

모델 저장 및 불러오기

파이토치 모델은 직렬화/역직렬화를 통해 객체를 저장하고 불러올 수 있다. 모델을 저장하려면 파이썬의 Pickle을 활용해 파이썬 객체 구조를 Binary protocols로 직렬화한다. 모델에 사용된 텐서나 매개변수를 저장한다.

 

모델을 불러올 때 저장된 객체 파일을 역직렬화해 현재 프로세스 메모리에 업로드한다. .pt.pth로 저장된다.

# 저장
torch.save(
  model,
  path
)
# 불러오기
torch.load(
  path,
  map_location # 장치 상태
)

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

for epoch in range(10000):
    # ...
    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

1000 에폭마다 모델을 저장할 수 있다.

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)
    # ...

반대로 저장해두었던 체포인트에서 모델을 그대로 다시 학습 시킬 수 있다. 위에 저장한 바와 같이 epoch, model.state_dic, optimzer.state_dict을 필수로 포함해야 된다.

참고 자료

https://product.kyobobook.co.kr/detail/S000209621433

 

파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습 | 윤대희 - 교보문고

파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습 | 트랜스포머는 딥러닝 분야에서 성능이 우수한 모델로 현대 인공지능 분야의 핵심 기술입니다. 트랜스포머와 비전 트랜스

product.kyobobook.co.kr