[Computer Vision] 머신러닝 & OpenCV #1 머신러닝 개요, KNN

2023. 10. 11. 00:07Run/Computer Vision

1. 머신러닝

  • 주어진 데이터를 분석하여 규칙성, 패턴 등 찾고 이를 통해 의미있는 정보 추출하는 과정
  • 학습(train): 데이터(or feature)로부터 규칙 찾아내는 과정
  • 모델(model): 학습에 의해 결정된 규칙
  • 예측(predict)/추론(inference): 새 데이터를 학습된 모델 입력으로 넣고 결과를 판단하는 과정
  • 딥러닝 = 머신러닝의 한 종류

 

  • 머신러닝: 지도학습(supervised learning), 비지도학습(unsupervised learning), 강화학습(reinforcement learning)
  • 지도학습: 정답(label) 알고 있는 데이터 이용하여 학습 진행. 가장 많이 사용.
    • 영상의 픽셀 값은 조명 변화, 객체 이동 등에 의해 민감하게 변하기 때문에 픽셀 값을 그대로 머신러닝 입력으로 사용하지 않음
    • 대신 영상의 변환에도 크게 변하지 않는 특징 정보(feature vector) 추출하여 머신러닝 입력으로 사용
    • 지도학습에서는 다수의 훈련 영상에서 feature vector 추출하고 이를 통해 머신러닝 알고리즘을 학습시켜 모델을 구함
    • 추론 과정에서도 입력 영상으로부터 feature vector 추출하고 모델에 입력해 결과 얻을 수 있음
    • 회귀(regression): 연속된 수치 값 예측하는 작업 (ex. 키 주어졌을 때 몸무게 예측)
    • 분류(classification): 이산적인 값 결과로 출력하는 작업 (ex. 사과 or 바나나)

 

지도학습에 의한 classification

 

  • 비지도학습: 훈련 데이터의 정답에 대한 정보 없이 오로지 데이터 자체만을 이용하여 학습 진행.
    • 여러 집합에서 서로 구분되는 특징 이용하여 서로 분리하는 작업
    • 군집화(clustering)

 

  • K-fold cross-validation
    • 5개로 나눠서, 4개로 훈련, 나머지로 성능 측정, ... 한 사이클 반복
    • 결과에 따라 learning rate 수정, 다시 한 사이클 반복, ... → 최적의 learning rate 찾아나가기
  • Test set으로 성능 측정하면 파라미터가 test set에 최적화됨, 하지만 우리가 원하는 것은 실제로 사용할 데이터에 최적인 파라미터, 그래서 validation set 따로 둠

 

  • 일반적으로 훈련 데이터에는 오류(잡음 or 이상치)가 있음
  • (1) 데이터에 잡음 없도록 하기 (2) 모델이 잡음에 대응할 수 있게 하기 → (1)이 우선
  • 두 종류를 완벽히 구분하는 경계면은 새 입력 데이터에 대해 오히려 정확도 떨어질 수 있음(overfitting), 적당한 경계면이 필요

 

2. OpenCV 머신러닝 클래스

  • OpenCV에서 제공하는 머신러닝 클래스는 주로 ml 모듈에 포함
  • cv::ml::StatModel 추상 클래스 상속받아 만들어짐
    • StatModel::train(): 머신러닝 알고리즘 학습시키는 멤버 함수 (가상함수로 선언됨)
    • StatModel::predict(): 테스트 데이터에 대한 결과 예측하는 멤버 함수 (가상함수로 선언됨)
  • 몇몇 클래스는 자신만의 학습, 예측을 위한 멤버 함수 따로 제공하기도 함 (직접 오버로딩하여 사용)

 

  • virtual bool StatModel::train(InputArray samples, int layout, InputArray responses)
    • 가상함수로 선언됨
    • 각 머신러닝 알고리즘은 각자에 맞는 방식으로 train() 재정의 (필수는 아니지만 보통 그렇게 함)
    • StatModel 클래스 상속받은 클래스 객체에서 train() 호출
    • layout
      • ROW_SAMPLE: 각 훈련 데이터가 samples 행렬에 row 단위로 저장
      • COL_SAMPLE: 각 훈련 데이터가 samples 행렬에 col 단위로 저장
  • virtual float StatModel::predict(InputArray samples, OutputArray results = noArray(), int flags = 0) const;
    • 순수가상함수로 선언됨
    • 각 머신러닝 알고리즘은 각자에 맞는 방식으로 predict() 재정의
    • 일부 머신러닝 알고리즘 구현 클래스는 predict() 대신할 수 있는 고유의 예측 함수 제공

 

ANN_MLP 인공신경망(artificial neural network) 다층 퍼셉트론(multi-layer perceptrons)
여러 개의 은닉층을 포함한 신경망 학습시킬 수 있고, 입력 데이터에 대한 결과 예측할 수 있음
DTrees 이진 의사 결정 트리 (decision tree) 알고리즘
Boost 클래스(부스팅 알고리즘), RTree 클래스(랜덤 트리 알고리즘)의 부모 클래스
Boost 다수의 약한 분류기(weak classifier)에 적절한 가중치 부여하여 성능 좋은 분류기 만듦
RTrees 입력 feature vector를 다수의 트리로 예측하고 결과 취합하여 분류 or 회귀 수행
EM 기댓값 최대화(expectation maximization)
가우시안 혼합 모델 이용한 군집화 알고리즘
KNearest KNN 알고리즘
LogisticRegression 로지스틱 회귀
이진 분류 알고리즘의 일종
NormalBayesClassifier 정규 베이즈 분류기
각 클래스의 feature vector가 정규 분포 따른다고 가정,
전체 데이터 분포 가우시안 혼합 모델로 표현
학습 데이터로부터 각 클래스의 평균 벡터와 공분산 행렬 계산, 이를 예측에 사용
SVM Support Vector Machine
두 클래스의 데이터를 가장 여유있게 분리하는 초평면 구함
SVMSGD Stochastic gradient descent SVM
SGD를 SVM에 적용하여 대용량 데이터에 대해서도 빠른 학습 가능

 

3. KNN (K-Nearest Neighbor)

  • 분류 또는 회귀에 사용되는 지도학습 알고리즘
  • 분류에 사용할 경우
    • 특정 공간에서 테스트 데이터와 가장 가까운 k개의 훈련 데이터 찾음
    • k개의 훈련 데이터 중 가장 많은 클래스를 테스트 데이터의 클래스로 지정
  • 회귀에 사용할 경우
    • 테스트 데이터와 인접한 k개의 훈련 데이터의 평균을 테스트 데이터의 값으로 지정

 

  • 위 예제에서 녹색 점과 가장 가까운 것은 빨간색이지만, 지정한 범주(k) 내에 가장 많은 것은 파란색
  • 보통 k는 1보다 큰 값으로 설정 (1로 설정하면 NN 알고리즘)
  • 최적의 k 값을 찾아 설정해야 함 (분포가 아주 균일하면 k가 굳이 클 필요 없음)

 

  • OpenCV KNearest 클래스 사용하기
    • ml 모듈에 포함되어 있고 cv::ml 네임스페이스에 정의되어 있음
    • KNearest::create(): 정적 멤버 함수 사용하여 KNearest 객체 생성
    • KNearest::setDefaultK(): k 값 변경 가능 (default 10)
    • StatModel::predict(): k 값 만큼의 최근접 이웃 데이터 찾음
    • KNearest::findNearest(InputArray samples, int k, OutputArray results, OutputArray neighborResponses = noArray(), OutputArray dist = noArray()) const;
      • 사용 시 함수 인자로 k 값 명시적 지정 가능, predict()보다 더 자주 사용
      • 입력 samples 행렬의 각 row에 저장된 테스트 데이터와 가장 가까운 k개의 훈련 데이터 찾아 results 행렬에 저장, 반환
      • samples 행렬 row 수 = results 행렬 row 수, col 수 항상 1
    • KNearest::setIsClassifier(): 기본적으로 분류에 쓰이지만, 회귀에 적용하려는 경우 false 지정하여 호출
    • StatModel::train(): 객체 생성하고 속성 설정한 후 학습 진행
      • Lazy training: 실질적으로 학습하는 건 아님, 훈련 데이터와 레이블 데이터를 KNearest 클래스 멤버 변수에 저장

 

✅ 2차원 점 분류

#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;
using namespace cv::ml;

Mat img;
Mat train, label;
Ptr<KNearest> knn;
int k_value = 1; // k 값 설정

void addPoint(const Point& pt, int cls);
void trainAndDisplay();

int main() {
    img = Mat::zeros(Size(500, 500), CV_8UC3); // 0으로 초기화
    knn = KNearest::create(); // KNearest 객체 생성

    namedWindow("knn");

    const int NUM = 30;
    Mat rn(NUM, 2, CV_32SC1); // 30x2 랜덤 integer mat 생성
 
    /*
    훈련 데이터 (150, 150), (350, 150), (250, 400)을 중심으로 하는 가우시안 분포의 R, G, B점 30개씩 생성
    (0, 0) ~ (499, 499) 좌표 사이 모든 점에 대해 kNN 분류 수행하여 R, G, B 색상으로 나타냄
    */

    randn(rn, 0, 50); // 평균 0, 표준편차 50 분포 rn에 출력
    for (int i = 0; i < NUM; i++) {
        addPoint(Point(rn.at<int>(i, 0) + 150, rn.at<int>(i, 1) + 150), 0); // (150, 150) 중심, 클래스 0
    }

    randn(rn, 0, 50); // 평균 0, 표준편차 50 분포 rn에 출력
    for (int i = 0; i < NUM; i++) {
        addPoint(Point(rn.at<int>(i, 0) + 350, rn.at<int>(i, 1) + 150), 1); // (350, 150) 중심, 클래스 1
    }

    randn(rn, 0, 70); // 평균 0, 표준편차 70 분포 rn에 출력
    for (int i = 0; i < NUM; i++) {
        addPoint(Point(rn.at<int>(i, 0) + 250, rn.at<int>(i, 1) + 400), 2); // (250, 400) 중심, 클래스 2
    }

    trainAndDisplay();

    waitKey(0);
    return 0;
}

// 데이터, 레이블 받고 훈련 데이터(좌표), 레이블 데이터(0/1/2) 생성
void addPoint(const Point& pt, int cls) {
    Mat new_sample = (Mat_<float>(1, 2) << pt.x, pt.y);
    train.push_back(new_sample);

    Mat new_label = (Mat_<int>(1, 1) << cls);
    label.push_back(new_label);
}

void trainAndDisplay() {

    /* Train */
    knn->train(train, ROW_SAMPLE, label); // samples, layout, responses

    // img의 매 pixel마다 nearest neighbor 찾고 label에 따라 R, G, B 색칠
    for (int i = 0; i < img.rows; ++i) {
        for (int j = 0; j < img.cols; ++j) {
            Mat sample = (Mat_<float>(1, 2) << j, i); // 각 pixel

            Mat res;
            knn->findNearest(sample, k_value, res); // samples, k, results
            int response = cvRound(res.at<float>(0, 0)); // float -> int

            // 배경 색칠
            if (response == 0) {
                img.at<Vec3b>(i, j) = Vec3b(128, 128, 255); // R
            }
            else if (response == 1) {
                img.at<Vec3b>(i, j) = Vec3b(128, 255, 128); // G
            }
            else if (response == 2) {
                img.at<Vec3b>(i, j) = Vec3b(255, 128, 128); // B
            }
        }
    }

    /* Display */
    for (int i = 0; i < train.rows; i++) {
        int x = cvRound(train.at<float>(i, 0)); // float -> int
        int y = cvRound(train.at<float>(i, 1)); // float -> int
        int l = label.at<int>(i, 0);

        // 훈련 데이터 점 색칠
        if (l == 0) {
            circle(img, Point(x, y), 5, Scalar(0, 0, 128), -1, LINE_AA);
        }
        else if (l == 1) {
            circle(img, Point(x, y), 5, Scalar(0, 128, 0), -1, LINE_AA);
        }
        else if (l == 2) {
            circle(img, Point(x, y), 5, Scalar(128, 0, 0), -1, LINE_AA);
        }
    }

    imshow("knn", img);
}

  • 빨간색 영역에 가까운 초록색 점, 초록색 영역에 가까운 파란색 점 때문에 볼록하게 튀어나온 부분 생김
  • k 값을 늘리면 다소 완만한 형태로 변함

 

✅ 필기체 숫자 인식

 

digits.png (2000 x 1000 pixel)

  • OpenCV에서 5000개(0~9까지 500장씩)의 필기체 숫자 영상 제공함
  • 이미지 크기: 2000 x 1000 / 각 숫자 크기: 20 x 20 / 각 숫자 개수: 100 x 5 → 각 숫자 영상 잘라내서 훈련 데이터 생성
  • 영상들이 이미 어느 정도 정제되어 있으므로 따로 feature vector 추출하지 않고 그대로 사용
  • 테스트 데이터 따로 나누지 않고 다 훈련 데이터로 사용

 

  • 숫자 영상 한 장: 20 x 20 → 1 x 400 변환
  • 모든 영상 세로로 쌓으면 전체 5000 x 400 행렬, 이를 KNearest 클래스 훈련 데이터로 전달
  • 이때 각 필기체 숫자 영상이 나타내는 숫자 값을 레이블 데이터로 전달
  • 레이블 행렬의 row 크기 = 훈련 데이터 영상 개수, col 크기 = 1 → 5000 x 1

 

#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;
using namespace cv::ml;

Point ptPrev(-1, -1);

Ptr<KNearest> train_knn();
void on_mouse(int event, int x, int y, int flags, void* userdata);

int main() {
    Ptr<KNearest> knn = train_knn(); // 훈련 함수 호출
    if (knn.empty()) {
        cerr << "Training failed!" << endl;
        return -1;
    }

    Mat img = Mat::zeros(400, 400, CV_8U); // 400x400 Canvas
    imshow("img", img);
    setMouseCallback("img", on_mouse, (void*)&img); // 마우스 콜백 함수 등록 (on_mouse 불릴 때마다 img 인자로)

    while (true) {
        int c = waitKey(0);
        // ESC 누르면 종료
        if (c == 27) break;
        // Space 누르면 글씨 인식, 결과 출력
        else if (c == ' ') { 
            /* 테스트 데이터 가공 */
            Mat img_resize, img_float, img_flatten, res;
            resize(img, img_resize, Size(20, 20), 0, 0, INTER_AREA); // 400x400 -> 20x20 (훈련 데이터와 같도록)
            img_resize.convertTo(img_float, CV_32F); // 테스트 데이터 float 형태여야 함
            img_flatten = img_float.reshape(1, 1); // 1x20x20 -> 1x1x400

            /* 예측 결과 확인 */
            knn->findNearest(img_flatten, 3, res);
            cout << cvRound(res.at<float>(0, 0)) << endl;

            /* 초기화 */
            img.setTo(0); // 행렬 0으로 초기화
            imshow("img", img);
        }
    }
    return 0;
}

Ptr <KNearest> train_knn() {
    /* 데이터셋 */
    Mat digits = imread("images/digits.png", IMREAD_GRAYSCALE);
    if (digits.empty()) {
        cerr << "Image load failed!" << endl;
        return 0;
    }

    /* 훈련 데이터, 레이블 데이터 가공 */
    Mat train_images, train_labels; // 훈련 데이터 행렬, 레이블 데이터 행렬
    for (int j = 0; j < 50; j++) {
        for (int i = 0; i < 100; i++) {
            Mat roi, roi_float, roi_flatten;
            roi = digits(Rect(i * 20, j * 20, 20, 20)); // ROI 20x20 pixel
            roi.convertTo(roi_float, CV_32F); // 훈련 데이터 float 형태여야 함
            roi_flatten = roi_float.reshape(1, 1); // 1x20x20 -> 1x1x400

            train_images.push_back(roi_flatten); // train images 세로로 추가
            train_labels.push_back(j / 5); // 0 ~ 4: 0, ..., 45 ~ 49: 9
        }
    }

    /* 훈련 데이터, 레이블 데이터로 훈련 진행 */
    Ptr<KNearest> knn = KNearest::create(); // kNearest 객체 생성하고 shared 포인터에 넣음
    knn->train(train_images, ROW_SAMPLE, train_labels); // 훈련 진행

    return knn;
}

void on_mouse(int event, int x, int y, int flags, void* userdata) {
    Mat img = *(Mat*)userdata;

    if (event == EVENT_LBUTTONDOWN) {
        ptPrev = Point(x, y);
    }
    else if (event == EVENT_LBUTTONUP) {
        ptPrev = Point(-1, -1);
    }
    else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) {
        line(img, ptPrev, Point(x, y), Scalar::all(255), 40, LINE_AA, 0);
        ptPrev = Point(x, y);
        imshow("img", img);
    }
}

  • 글씨 너무 작거나 크게 입력하거나 중앙이 아닌 곳에 입력하면 결과 잘못될 수 있음

 

머신러닝 & OpenCV #2 SVM, HOG (https://coollikethat.tistory.com/54) 로 이어집니다.