Engineering/C++

C++에서 다양한 타입을 벡터 혹은 배열에 저장하는 법

luckydipper 2024. 6. 4. 09:47
반응형
역전파 코드를 개발 과정에서 문제점을 발견했다. 다른 class(type)의 레이어들을 어떻게 순서대로 방문할 것인가?  

반복문을 쓰고 싶지만, vector나 array는 같은 타입의 묶음만 정의가 가능하다. 만약 이들을 순서대로 방문해 주는 자료형이 없다면 아래와 같이 하나씩 방문했을 것이다.

// https://github.com/luckydipper/MNIST_DEEP_LEARNING_CLASSIFIER
int main(){
  neural::Linear l1{flatten_img_size, in_out_size[0]},
      l2{in_out_size[0], in_out_size[1]}, l3{in_out_size[1], in_out_size[2]},
      l4{in_out_size[2], in_out_size[3]};
  neural::ReLU act1{}, act2{}, act3{}, act4{};
  neural::SoftmaxLoss sft{};
  
  // 만약 반복문이 없다면 이것 들을 하나씩 다 손 코딩 해야한다.
  for (int i = 0; i < imgs_iter.size(); i++) {
    auto l_1 = l1.forward(imgs_iter[i]);
    auto a1 = act1.forward(l_1);
    auto l_2 = l2.forward(a1);
    auto a2 = act2.forward(l_2);
    auto l_3 = l3.forward(a2);
    auto a3 = act3.forward(l_3);
    auto l_4 = l4.forward(a3);
    auto a4 = act4.forward(l_4);
    double current_loss = sft.forward(a4, lable_iter[i]);

    cout << i << " iter, current_loss is " << current_loss << "\n";
    auto b1 = sft.backward();
    auto b2 = act4.backward(b1);
    auto b3 = l4.backward(b2);
    auto b4 = act3.backward(b3);
    auto b5 = l3.backward(b4);
    auto b6 = act2.backward(b5);
    auto b7 = l2.backward(b6);
    auto b8 = act1.backward(b7);
    auto b9 = l1.backward(b8);
    vector<neural::Linear *> layers = {&l1, &l2, &l3, &l4};
    
    // optimize
    for (auto &layer : layers) {
      layer->weight.array() -= lr * layer->delta_weight.array();
      layer->bias.array() -= lr * layer->delta_bias.array();
    }
  }

}

 

서로 다른 type을 묶는 방법은 세 가지가 존재한다. 

1. 부모 class의 인터페이스를 이용한다.

2. std::varient를 사용한다. (C++17부터 가능)

3. std::any를 사용한다. (C++17부터 가능)

결론 : 1번과 2번 방법을 추천한다. 

1. 부모 class의 인터페이스를 이용한다.

C++의 public 상속은 구조적으로 is-a 관계를 갖는다. 즉, 자식은 부모이다. 하지만 부모는 자식이 아니다.

내부적으로 보았을 때, C++의 상속은 class의 확장이다. 즉 child class를 interface pointer로 가르켜도 된다는 뜻이다. 

https://www.trytoprogram.com/cplusplus-programming/inheritance/

즉, bird pointer로 Eigle을 가르킬 수 있다. 

struct ModelInterface {
  ModelInterface() { ; };
  virtual Eigen::MatrixXd forward(const Eigen::MatrixXd &X) = 0;
  virtual Eigen::MatrixXd backward(const Eigen::MatrixXd &delta_out) = 0;
};
struct Linear : public ModelInterface {
  explicit Linear(const int input_size, const int output_size)
      : input_size(input_size), output_size(output_size),
        weight(createHeWeight(input_size, output_size)),
        bias(Eigen::VectorXd::Zero(output_size)) {
    ;
  };
  ~Linear() = default;

  Eigen::MatrixXd forward(const Eigen::MatrixXd &X) override {
	// 생략
  }

  Eigen::MatrixXd backward(const Eigen::MatrixXd &delta_out) override {
    // 생략	
  }
};

struct ReLU : public ModelInterface {
  ReLU() = default;
  Eigen::MatrixXd forward(const Eigen::MatrixXd &X) override {
	// 생략
  };

  Eigen::MatrixXd backward(const Eigen::MatrixXd &delta_out) override {
	// 생략
};
};


int main(){
  // model definition
  const int flatten_img_size = 28 * 28;
  const int in_out_size[] = {400, 300, 100, 10};
  neural::Linear l1{flatten_img_size, in_out_size[0]},
      l2{in_out_size[0], in_out_size[1]}, l3{in_out_size[1], in_out_size[2]},
      l4{in_out_size[2], in_out_size[3]};
  neural::ReLU act1{}, act2{}, act3{}, act4{};
  neural::SoftmaxLoss sft{};
  vector<neural::ModelInterface *> layers = {&l1, &act1, &l2, &act2, &l3, &act3, &l4, &act4};
  
  for (auto &layer : layers) 
	layer->forward();
  for (auto &layer : layers) 
	layer->backward();
        
  }
}

이런 식으로 짤 수 있다. 

이 방식의 장점은 추가적인 공간을 차지하지 않는다는 것이다. 2번과 3번 방법은 새로운 class를 만들어서 해결하는 방법이라 추가적인 공간이 필요하다. 단점은 원래 child classed가 무엇인지 알기 힘들다는 것이다. (RTTI: Run-Time Type Information) 기술인 typeid를 사용하면 runtime에 알 수 있다. 그러나 보안과 성능상의 이유로 추천되지 않는다. 어떤 child class에서 왔는지 비교하면서 함수를 실행하는 것 자체가 overhead이다. 

2. std::varient를 사용한다. (C++17부터 가능)

std::varient는 유저가 정의 한 타입 중 한개의 타입으로 정의된다는 것이다. 

generic programming을 이용하여 compile time에 해당하는 코드를 만들어 준다. tag로 확장된 class를 만들어서 용량을 더 차지하는 대신 3개의 값을 한 번에 저장할 수 있도록 만든다. 

 

#include <string>
#include <iostream>
#include <variant>
#include <vector>

int main(){
  using pyString = std::variant<int, std::string, double>;

  pyString python_string = 3;
  
  // index는 현재 가지고 있는 type의 순서 int->0, sting->1 double->2로 return
  // get<T>는 현재 type이 T인지 확인 
  std::cout << python_string.index() << std::get<int>(python_string);
  
  std::vector<pyString> vec = {1.2 ,3, 1,5,4,3, std::string("hello world")};
  
    for (const auto& v : vec) {
    // visit은 2번째 element를 읽어서 lamda를 실행한다
    std::visit([](const auto& value) {
        std::cout << value << std::endl;
    }, v);
    }
}

3. 3. std::any를 사용한다. (C++17부터 가능)

any는 어떠한 type도 저장할 수 있는 객체이다. any_cast를 활용하여 사용 가능하다. 하지만 overhead가 커보인다. any_cast로 type 1개 1개 체크해야 한다. 

#include <iostream>
#include <any>
#include <vector>
#include <string>

int main() {
    using pyString = std::any;

    pyString python_string = 3;

    // std::any_cast를 이용하여 값을 가져오고, 타입이 맞지 않으면 예외 발생
    try {
        std::cout << "index: " << 0 << " value: " << std::any_cast<int>(python_string) << std::endl;
    } catch (const std::bad_any_cast& e) {
        std::cout << "Bad any cast: " << e.what() << std::endl;
    }

    // 벡터에 다양한 타입의 값 저장
    std::vector<pyString> vec = {1.2, 3, 1.5, 4, 3.0, std::string("hello world")};

    // 벡터의 각 요소에 대해 std::any_cast로 값을 가져와 출력
    for (const auto& v : vec) {
        if (v.type() == typeid(int)) {
            std::cout << std::any_cast<int>(v) << std::endl;
        } else if (v.type() == typeid(double)) {
            std::cout << std::any_cast<double>(v) << std::endl;
        } else if (v.type() == typeid(std::string)) {
            std::cout << std::any_cast<std::string>(v) << std::endl;
        } else {
            std::cout << "Unknown type" << std::endl;
        }
    }

    return 0;
}

 

 

반응형