지난 글에서 AI는 행렬의 연산이 주로 이루어지기 때문에 먼저 Matrix 객체를 만들었습니다. 이 글에서는 이를 활용하여 선형 레이어를 만들고 테스트해 보겠습니다. 👍

📈 선형 레이어

💡 레이어(Layer)

신경망에서 입력받아 어떤 연산을 하고, 그 결과를 다음 단계로 넘기는 구성 요소가 되고 이러한 레이어들이 여러 개로 구성되어 하나의 모델이 된다.

(ex: MLP, LeNet-5, ResNet-50, GPT…)

선형 레이어는 입력된 숫자들을 가중치와 편향으로 계산해 새로운 숫자들로 바꾸는 레이어 입니다.

$$\underset{\substack{\uparrow\\\text{출력}}}{y}=\underset{\substack{\uparrow\\\text{입력}}}{X}\underset{\substack{\uparrow\\\text{가중치}}}{W}^T+\underset{\substack{\uparrow\\\text{편향}}}{b}$$

⚠️ 가중치는 입력과 출력 특성의 수를 어떤 순서로 받느냐에 따라 전치 연산이 필요할 수도 있고 아닐 수 있습니다!

이 글에서 가중치 \(W\)를 \([\text{Output}, \text{Input}]\)로 받기 때문에 전치 연산이 필요합니다.

입력에 가중치를 곱하고 편향을 더하는 간단한 레이어 입니다.

구현

입력이 들어오면 계산해 낼 forward 함수와 차후에 가중치 업데이트(학습)에 필요한 값을 얻기 위한 backward 함수를 만듭니다. 전체 코드는 이곳에서 보실 수 있습니다.

💡 순전파(Forward) / 역전파(Backward)

⬇️ Forward(순전파)​는 입력을 레이어들에 차례로 넣어 출력을 계산하는 과정

⤴️ Backward(역전파)​는 출력의 오차가 각 가중치와 편향에 얼마나 영향을 받았는지 계산해, 학습에 필요한 기울기를 구하는 과정

생성자에서 input과 output의 순서를 정의했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// linear.hpp
template <typename T>
class Linear : public Layer<T> {
public:
    Linear(std::size_t outFeature, std::size_t inFeature)
        // [output, input] 순서 중요! 전치 필요 유무
        : Layer<T>(outFeature, inFeature) {
        
        for(std::size_t i=0; i<outFeature; i++) {
            this->_bias[i] = 0;
        }
    }
...

`forward` 함수입니다. broadcasting 연산이 함수 내부에 박혀있는데 나중에 빼야할 듯 싶습니다.

```cpp
// linear.hpp
    Matrix<T> forward(const Matrix<T>& input) override {
        if(input.col() != this->_weight.col()) throw std::exception();

        this->_input = input;
        Matrix<T> w_t = this->_weight.transpose(); // W[out, in] 형태, 곱셈을 위해 전치연산
        Matrix<T> ret = input * w_t;

        // 편향 b에 대해 Broadcasting
        for(std::size_t row=0; row<ret.row(); row++) {
            for(std::size_t col=0; col<ret.col(); col++) {
                ret(row, col) += this->_bias(0, col);
            }
        }

        return ret;
    }

Linear 레이어에서 backward시 구해야 할 값은 아래 세 가지 입니다.

  • 이전 레이어에서 역전파로 받은 기울기를 \(G=\frac{\partial L}{\partial Y}\)라고 한다면
    • \(\frac{\partial L}{\partial W}=G^TX\) (가중치에 대한 기울기)
    • \(\frac{\partial L}{\partial b}=\sum^N_\text{i=1}{G_i}\) (편향에 대한 기울기)
    • \(\frac{\partial L}{\partial X}=GW\) (입력에 대한 기울기)
complicated

💡 backward할 때 forward에서 사용했던 input값과 weight, bias 값이 모두 필요합니다!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    Matrix<T> backward(const Matrix<T>& gradOutput) override {
        // 가중치 기울기 dW
        Matrix<T> lossGradient_t = gradOutput.transpose();
        this->_weightGradient = lossGradient_t * this->_input;
        
        // 편향 기울기 db
        this->_biasGradient = Matrix<T>(1, gradOutput.col());
        for(std::size_t row=0; row<gradOutput.row(); row++) {
            for(std::size_t col=0; col<gradOutput.col(); col++) {
                this->_biasGradient(0, col) += gradOutput(row, col);
            }
        }
        
        // 입력 기울기 dX
        Matrix<T> inputGradient = gradOutput * this->_weight;

        return inputGradient;
    }

맨 마지막에 있는 레이어의 gradOutput 값은 예측값과 실제 정답과 비교한 오차의 미분값을 사용하는데 이때 오차를 구하는 방법은 추후 포스팅하겠습니다. 😎

테스트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
TEST_CASE(AI_LINEAR_FORWARD_TEST) {
    Yami::AI::Matrix<int> input(2, 2);
    Yami::AI::Matrix<int> weight(2, 2);
    Yami::AI::Matrix<int> bias(1, 2);
    
    for(int i=0; i<4; i++) {
        input[i] = i+1;
        weight[i] = i+5;
    }
    bias(0, 0) = 1;
    bias(0, 1) = 2;
    
    Yami::AI::Linear<int> linear(2, 2);
    linear.setWeight(weight).setBias(bias);
    
    Yami::AI::Matrix<int> ret = linear(input);
    ASSERT_EQ(ret(0,0), 18);
    ASSERT_EQ(ret(0,1), 25);
    ASSERT_EQ(ret(1,0), 40);
    ASSERT_EQ(ret(1,1), 55);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
TEST_CASE(AI_LINEAR_BACKWARD_TEST) {
    Yami::AI::Matrix<float> x(3, 5);
    x.setElements(
        1.0, 2.0, 1.0, 0.0, -1.0,
        0.0, 1.0, 2.0, 1.0, 0.0,
        -1.0, 0.0, 1.0, 2.0, 1.0
    );
    Yami::AI::Matrix<float> w(3, 5);
    w.setElements(
        0.1, 0.2, -0.1, 0.0, 0.1,
        0.2, 0.1, 0.0, -0.2, 0.1,
        -0.1, 0.0, 0.2, 0.1, -0.1
    );
    Yami::AI::Matrix<float> b(1, 3);
    b.setElements(
        0.1, -0.1, 0.2
    );

    Yami::AI::Linear<float> linear(3, 5);
    linear.setWeight(w).setBias(b);

    linear(x);
    
    Yami::AI::Matrix<float> dy(3, 3);
    dy.setElements(
        1.0, 0.5, -1.0,
        -0.5, 1.0, 0.0,
        0.0, -1.0, 1.0
    );
    
    Yami::AI::Matrix<float> dX = linear.backward(dy);
    ASSERT_FLOAT_EQ(dX(0,0), 0.3);
    ASSERT_FLOAT_EQ(dX(0,1), 0.25);
    ASSERT_FLOAT_EQ(dX(0,2), -0.3);
    ASSERT_FLOAT_EQ(dX(0,3), -0.2);
    ASSERT_FLOAT_EQ(dX(0,4), 0.25);
    ASSERT_FLOAT_EQ(dX(1,0), 0.15);
    ASSERT_FLOAT_EQ(dX(1,1), 0);
    ASSERT_FLOAT_EQ(dX(1,2), 0.05);
    ASSERT_FLOAT_EQ(dX(1,3), -0.2);
    ASSERT_FLOAT_EQ(dX(1,4), 0.05);
    ASSERT_FLOAT_EQ(dX(2,0), -0.3);
    ASSERT_FLOAT_EQ(dX(2,1), -0.1);
    ASSERT_FLOAT_EQ(dX(2,2), 0.2);
    ASSERT_FLOAT_EQ(dX(2,3), 0.3);
    ASSERT_FLOAT_EQ(dX(2,4), -0.2);
}
1
2
3
4
[RUN      ] AI_LINEAR_FORWARD_TEST
[     OK  ] AI_LINEAR_FORWARD_TEST
[RUN      ] AI_LINEAR_BACKWARD_TEST
[     OK  ] AI_LINEAR_BACKWARD_TEST
good

AI가 샘플로 만들어준 데이터와 결과를 비교했을 때 잘 작동하는 것을 볼 수 있었습니다. 👍