반복문을 쓰고 싶지만, 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로 가르켜도 된다는 뜻이다.
즉, 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;
}
'Engineering > C++' 카테고리의 다른 글
Chapter3. Sharing Data Between Threads (1) | 2025.01.20 |
---|---|
Chapter 4. Synchronizing Concurrent Operations (0) | 2025.01.18 |
C++ STL 정리 (3) | 2024.12.19 |
C++17 Class Template Argument Deduction (CTAD) (0) | 2024.12.19 |
C++에서 auto vs decltype vs typeid (0) | 2024.06.10 |