이전 글에서 선형 레이어와 활성화 함수의 forward와 backward를 구현해 봤습니다.

forward는 input 값을 넣어 여러 행렬 계산을 통해 예측값을 만들어 냈습니다. 반대로 backward는 예측값과 실제 결괏값에 대한 오차를 입력으로 예측값을 만들어 냈던 모든 레이어의 손실(오차)에 대한 기울기를 구합니다.

이번 글에서는 예측값과 실제 결과의 손실을 구하는 방법 중 하나인 “평균제곱오차” 함수를 알아보고 구현해 보겠습니다. 👍

📒 평균제곱오차

💡 평균제곱오차(MSE, Mean Squared Error)는 모델의 예측값과 실제값의 차이를 제곱해 평균한 손실함수이다.

$$MSE=\frac{1}{n}\sum^{n}_{i=1}(\hat{y_i}-y_i)^2$$
  • \(y_i\): 실제값
  • \(\hat{y_i}\): 예측값
  • \(n\): 데이터 개수

MSE는 모든 원소의 값을 더하고 나누어서 스칼라값이 나오게 됩니다. 0에 가까울수록 오차가 적은 것이죠.

오차를 제곱하므로 큰 오차에 더 큰 벌점을 줍니다. 그래서 회귀 문제에서 자주 쓰이고, 학습할 때 모델은 MSE가 작아지는 방향으로 가중치를 조정하게 됩니다.

학습을 할때는 손실을 가중치로 미분한 값(기울기)가 필요합니다. 기울기가 가중치를 어느 방향으로 얼마나 바꿀지 알려주기 때문입니다.

예측값 \(\hat{y}\)에 대한 MSE 미분은 아래와 같습니다.

$$\frac{\partial L}{\partial \hat{y_i}}=\frac{2}{n}(\hat{y_i}-y_i)$$

구현

⚠️ 아래 구현 코드는 오래된 코드일 수 있습니다. 이 곳에서 최신화 된 최신 코드를 볼 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// mse.hpp
double forward(const Matrix<T>& prediction, const Matrix<T>& target) override {
    if(prediction.row() != target.row() && prediction.col() != target.col()) throw std::exception();

    this->_prediction = prediction;
    this->_target = target;

    const std::size_t kRow = prediction.row();
    const std::size_t kCol = prediction.col();
    const std::size_t kElementCount = kRow * kCol;
    double sum = 0.0f;
    T diff;

    for(std::size_t row=0; row<kRow; row++) {
        for(std::size_t col=0; col<kCol; col++) {
            // 예측값에서 정답을 빼고 제곱
            diff = prediction(row, col) - target(row, col);
            sum += diff * diff;
        }
    }

    // 나눈 값을 반환
    return sum / static_cast<double>(kElementCount);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// mse.hpp
Matrix<T> backward() override {
    const std::size_t kRow = this->_prediction.row();
    const std::size_t kCol = this->_prediction.col();
    const std::size_t kElementCount = kRow * kCol;

    Matrix<T> lossGradient(kRow, kCol);

    for(std::size_t row=0; row<kRow; row++) {
        for(std::size_t col=0; col<kCol; col++) {
            // forward 입력 매개변수(예측값)
            lossGradient(row, col) = 2 * (this->_prediction(row, col) - this->_target(row, col)) / kElementCount;
        }
    }

    return lossGradient;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TEST_CASE(AI_MSE_LOSS_TEST) {
    Yami::AI::Matrix<float> target(2, 3);
    target.setElements(
        1.0, 2.0, 3.0,
        4.0, 5.0, 6.0
    );

    Yami::AI::Matrix<float> prediction(2, 3);
    prediction.setElements(
        1.5, 2.0, 2.5,
        4.5, 4.0, 6.5
    );

    Yami::AI::MSE<float> mse;
    ASSERT_FLOAT_EQ(mse(prediction, target), 0.333333);

    Yami::AI::Matrix<float> dZ = mse.backward();
    ASSERT_FLOAT_EQ(dZ(0, 0), 0.166667);
    ASSERT_FLOAT_EQ(dZ(0, 1), 0);
    ASSERT_FLOAT_EQ(dZ(0, 2), -0.166667);
    ASSERT_FLOAT_EQ(dZ(1, 0), 0.166667);
    ASSERT_FLOAT_EQ(dZ(1, 1), -0.333333);
    ASSERT_FLOAT_EQ(dZ(1, 2), 0.166667);
}
1
2
[RUN      ] AI_MSE_LOSS_TEST
[     OK  ] AI_MSE_LOSS_TEST
good

테스트 결과가 잘 나온 것을 확인할 수 있습니다.