Link

실습: 트레이너 클래스 구현하기

이제 앞서 작성한 모델 클래스의 객체를 학습하기 위한 트레이너trainer 클래스를 살펴볼 차례입니다. 이에 따라 클래스의 각 메서드(함수)들을 살펴보도록 하겠습니다. 다음은 클래스의 가장 바깥에서 실행될 train 함수입니다.

def train(self, train_data, valid_data, config):
    lowest_loss = np.inf
    best_model = None

    for epoch_index in range(config.n_epochs):
        train_loss = self._train(train_data[0], train_data[1], config)
        valid_loss = self._validate(valid_data[0], valid_data[1], config)

        # You must use deep copy to take a snapshot of current best weights.
        if valid_loss <= lowest_loss:
            lowest_loss = valid_loss
            best_model = deepcopy(self.model.state_dict())

        print("Epoch(%d/%d): train_loss=%.4e  valid_loss=%.4e  lowest_loss=%.4e" % (
            epoch_index + 1,
            config.n_epochs,
            train_loss,
            valid_loss,
            lowest_loss,
        ))

    # Restore to best model.
    self.model.load_state_dict(best_model)

앞서 코드를 살펴보기 전에 그림을 통해 전체 과정을 설명하였는데요. 그때 학습train과 검증validation 등을 아우르는 큰 루프loop가 있었고, 학습과 검증 내의 작은 루프가 있었습니다. train 함수 내의 for 반복문은 큰 루프를 구현한 것입니다. 따라서 내부에는 self._train 함수와 self._validate 함수를 호출하는 것을 볼 수 있습니다. 그리고 곧이어 검증 손실 값에 따라 현재까지의 모델을 따로 저장하는 과정도 구현된 것을 확인할 수 있습니다.

현재까지의 최고 성능 모델을 best_model 변수에 저장하기 위해서는 state_dict라는 함수를 사용하는 것을 볼 수 있는데요. 이 state_dict 함수는 모델의 가중치 파라미터 값들을 json 형태로 변환하여 리턴합니다. 이 json 값의 메모리를 best_model에 저장하는 것이 아닌, 값 자체를 새로 복사하여 best_model에 할당하는 것을 볼 수 있습니다. 그리고 학습이 종료되면 best_model에 저장된 가중치 파라미터 json 값을 load_state_dict를 통해 self.model에 다시 로딩하는 것을 볼 수 있습니다. 이 마지막 라인을 통해서 학습 종료 후에 오버피팅이 되지 않은 가장 좋은 상태의 모델로 복원할 수 있게 됩니다.

이번에는 _train 함수를 살펴봅니다. 이 함수는 한 에포크epoch의 학습을 위한 for 반복문을 구현하였습니다.

def _train(self, x, y, config):
    self.model.train()

    x, y = self._batchify(x, y, config.batch_size)
    total_loss = 0

    for i, (x_i, y_i) in enumerate(zip(x, y)):
        y_hat_i = self.model(x_i)
        loss_i = self.crit(y_hat_i, y_i.squeeze())

        # Initialize the gradients of the model.
        self.optimizer.zero_grad()
        loss_i.backward()

        self.optimizer.step()

        if config.verbose >= 2:
            print("Train Iteration(%d/%d): loss=%.4e" % (i + 1, len(x), float(loss_i)))

        # Don't forget to detach to prevent memory leak.
        total_loss += float(loss_i)

    return total_loss / len(x)

함수의 시작부분에서 잊지 않고 train() 함수를 호출해서 모델을 학습 모드로 전환하는 것을 확인할 수 있습니다. 만약 이 라인이 생략된다면 이전 에포크의 검증validation 과정에서 추론 모드였던 모델이 그대로 학습에 활용될 것입니다. for 반복문은 작은 루프를 담당하고, 해당 반복문의 내부는 미니배치의 피드포워드feed-forward와 역전파back-propagation, 그리고 경사하강법gradient descent에의한 파라미터 업데이트가 담겨있습니다. 마지막으로 config.verbose에 따라 현재 학습 현황을 출력합니다. config는 가장 바깥의 train.py에서 사용자의 실행시 파라미터 입력에 따른 설정값이 들어있는 객체입니다.

_train 함수의 가장 첫부분에 _batchify 함수를 호출하는 것을 볼 수 있습니다. 다음의 _batchify 함수는 매 에포크마다 SGDStochastic Gradient Descent를 수행하기 위해 셔플링shuffling 후, 미니배치를 만드는 과정입니다.

def _batchify(self, x, y, batch_size, random_split=True):
    if random_split:
        indices = torch.randperm(x.size(0), device=x.device)
        x = torch.index_select(x, dim=0, index=indices)
        y = torch.index_select(y, dim=0, index=indices)

    x = x.split(batch_size, dim=0)
    y = y.split(batch_size, dim=0)

    return x, y

검증validation 과정에서는 random_split이 필요 없으므로 False로 넘어올 수 있음을 유의하세요.

다음 코드는 검증 과정을 위한 _validate 함수입니다.

def _validate(self, x, y, config):
    # Turn evaluation mode on.
    self.model.eval()

    # Turn on the no_grad mode to make more efficintly.
    with torch.no_grad():
        x, y = self._batchify(x, y, config.batch_size, random_split=False)
        total_loss = 0

        for i, (x_i, y_i) in enumerate(zip(x, y)):
            y_hat_i = self.model(x_i)
            loss_i = self.crit(y_hat_i, y_i.squeeze())

            if config.verbose >= 2:
                print("Valid Iteration(%d/%d): loss=%.4e" % (i + 1, len(x), float(loss_i)))

            total_loss += float(loss_i)

        return total_loss / len(x)

대부분 _train 과 비슷하게 구현되어 있음을 알 수 있습니다. 다만 가장 바깥 쪽에 torch.no_grad()가 호출되어 있음을 유의하세요.