머신러닝

  • 주어진 데이터를 분석하여 규칙성, 패턴 등을 찾고, 이를 이용하여 의미 있는 정보를 추출하는 과정

image

머신러닝의 예

  • 머신러닝에 의한 붓꽃 분류
    • 붓꽃(IRIS) 품종 분류 : Setosa vs Versicolor
    • 꽃잎(petal)과 꽃받침(sepal)의 길이에 의한 분류

image

  • Adaline : Adaptive Linear Neruon

image

머신러닝의 종류

image

머신러닝의 단계

  • 훈련(Train) : 훈련 데이터를 이용하여 모델 을 학습하는 과정
  • 예측(Predict) : 학습된 모델을 이용하여 새로운 데이터로부터 적절한 값을 예측하는 과정. 추론(Inference)

image

머신러닝학습의 목적

  • 미래의 새로운 데이터를 더 정확하게 예측하기 위함
    • 모델의 일반화 성능을 향상시키는 방향으로 학습해야 함

image

  • 과적합(overfitting)

    • 훈련 데이터 셋을 지나치게 정확하게 구분하도록 학습하여 모델의 일반화 성능이 떨어지게 되는 현상

      • 훈련 데이터 셋이 너무 적은 경우에 발생함

      • 훈련 데이터 셋이 전체 데이터 셋의 특성/분포를 반영하지 않은 경우에도 발생함

    • 모델이 복잡할수록 과적합 발생확률이 높음

훈련 데이터의 분할

  • 학습 가능한 데이터를 훈련, 검증, 테스트 데이터 셋으로 분할하여 사용

image

  • K-폴드 교차 검증(k-fold cross-validation)
    • 훈련 데이터를 k개로 분할하여 여러 번 검증 수행

image

OpenCV 머신러닝 클래스

image

  • 머신러닝 클래스 설명

image

image

머신러닝 알고리즘 훈련
virtual bool StatModel::train(InputArray samples, int layout, InputArray responses);
  • samples : 훈련 데이터 행렬
  • layout : 훈련 데이터 배치 방법
    • ROW_SAMPLE : 하나의 데이터가 한 행으로 구성됨
    • COL_SAMPLE : 하나의 데이터가 한 열로 구성됨
  • responses : 각 훈련 데이터에 대응되는 응답(레이블) 행렬
  • 반환값 : 훈련이 잘 되었으면 true
머신러닝 알고리즘 예측
virtual float StatModel::predict(InputArray samples, OutputArray results = noArray(), int flags = 0) const;
  • samples : 입력 벡터가 행 단위로 저장된 행렬(CV_32F)
  • results : 각 입력 샘플에 대한 예측(분류 or 회귀) 결과를 저장한 행렬
  • flags : 추가적인 플래그 상수
  • 반환값 : 입력 벡터가 하나인 경우에 대한 응답

서포터 벡터 머신(SVM) 알고리즘

  • 기본적으로 2개의 그룹(데이터)을 분리하는 방법으로 데이터들과 거리가 가장 먼 초평면(hyperplane)을 선택하여 분리하는 방법

image

  • 최대 마진 초평면 구하기

image

  • 오분류 에러 허용하기
    • 주어진 샘플을 완벽하게 두 개의 그룹으로 선형 분리를 할 수 없을 경우, 오분류 에러를 허용(Soft margin, C-SVM)

image

  • 비선형 데이터 분류하기
    • SVM은 선형 분류 알고리즘이지만 실제 데이터는 비선형으로 분포할 수 있음
    • 비선형 데이터의 차원을 확장하면 선형으로 분리가 가능함

image

image

SVM 객체 생성
static Ptr<SVM> SVM::create();
  • 비어있는 SVM 객체를 생성
  • StatModel::train() 메소드를 이용하여 훈련을 해서 사용해야 함
SVM 타입 지정
Virtual void SVM::setType(int val)
  • val : SVM::Types 열거형 상수 중 하나를 지정

image

SVM 커널 지정
Virtual void SVM::setKernel(int kernelType);
  • kernelType : 커널 함수 종류. SVM::KernelTypes 열거형 상수 지정

image

SVM 자동 훈련

image

  • K-폴드 교차 검증을 통해 최선의 파라미터를 찾아 훈련함

SVM을 이용한 2차원 점 분류 예제 코드

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

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

int main()
{
	Mat train = Mat_<float>({ 8, 2 }, { 
		150, 200, 200, 250, 100, 250, 150, 300,
		350, 100, 400, 200, 400, 300, 350, 400 });
		// 첫번째 점의 x, y, 두번째 점의 x, y 순서로 총 8개 점의 좌표
		
	Mat label = Mat_<int>({ 8, 1 }, { 0, 0, 0, 0, 1, 1, 1, 1 });
	// 각각의 점들의 클래스 번호로 처음 4개의 점은 0번, 그 뒤에 4개의 점은 1번

	Ptr<SVM> svm = SVM::create(); // svm 객체 생성

#if 1
	svm->setType(SVM::C_SVC); // svm 객체 타입 설정
	svm->setKernel(SVM::RBF); // svm 객체 커널 설정
	svm->trainAuto(train, ROW_SAMPLE, label);
	// 자동으로 최적의 하이퍼파라미터를 찾아 모델을 학습하는 함수
#else
	svm->setType(SVM::C_SVC);
	svm->setKernel(SVM::LINEAR);
	svm->trainAuto(train, ROW_SAMPLE, label);
#endif

	cout << svm->getC() << endl;
	cout << svm->getGamma() << endl;

	Mat img = Mat::zeros(Size(500, 500), CV_8UC3); 500 x 500의 비어있는 영상을 만듬

	for (int y = 0; y < img.rows; y++) { // 각각의 픽셀값 좌표를
		for (int x = 0; x < img.cols; x++) {
			Mat test = Mat_<float>({ 1, 2 }, { (float)x, (float)y });
			int res = cvRound(svm->predict(test));

			if (res == 0)
				img.at<Vec3b>(y, x) = Vec3b(128, 128, 255); // R 빨강색으로 채우기
			else
				img.at<Vec3b>(y, x) = Vec3b(128, 255, 128); // G 녹색으로 채우기
		}
	}

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

		if (l == 0)
			circle(img, Point(x, y), 5, Scalar(0, 0, 128), -1, LINE_AA); // R
		else
			circle(img, Point(x, y), 5, Scalar(0, 128, 0), -1, LINE_AA); // G
	}

	imshow("svm", img);
	waitKey();
}
  • 점들의 분포는 다음과 같음

image

  • 결과

image

  • ​ svm->setKernel(SVM::LINEAR);

image

HOG 알고리즘

  • Histogram of Oriented Gradients

  • 영상의 지역적 그래디언트 방향 정보를 벡터로 사용
  • 2005년 CVPR 학회에서 보행자 검출 방법으로 소개되어 널리 사용되기 시작함
  • 이후 다양한 객체 인식에서 활용됨

image

image

  • 방향성에 대한 히스토그램을 계산할 때는 20도로 나눠서 계산함
  • 아래 그림과 같은 방법으로 9개 성분에 대한 히스토그램을 구함

image

  • 블록 히스토그램

image

  • 하나의 부분 영상 패치에서의 특징 벡터 크기는?
    • 76(=64/8 -1)* 15(=128/8 -1) * 36(=4*9) = 3780

image

image

HOG & SVM 필기체 숫자 인식

  • 만약 정해진 포트로 인쇄된 숫자라면?
    • 템플릿 매칭으로도 가능함
  • 하지만 필기체 인식은?
    • 다수의 필기체 숫자 데이터가 필요함
HOG 특징 벡터를 이용한 SVM 학습

image

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

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

Mat img;
Point ptPrev(-1, -1);

void on_mouse(int event, int x, int y, int flags, void*)
{
	if (x < 0 || x >= img.cols || y < 0 || y >= img.rows)
		return;
	if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) // 마우스가 떼질 때
		ptPrev = Point(-1, -1);
	else if (event == EVENT_LBUTTONDOWN) // 마우스가 눌릴 때
		ptPrev = Point(x, y);
	else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) // 마우스가 눌려있으면서 움직일 때
	{
		Point pt(x, y);
		if (ptPrev.x < 0)
			ptPrev = pt;
		line(img, ptPrev, pt, Scalar::all(255), 40, LINE_AA, 0);
		ptPrev = pt;

		imshow("img", img);
	}
}

int main()
{
	Mat digits = imread("digits.png", IMREAD_GRAYSCALE); 

	if (digits.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

#if _DEBUG // 디버그 모드일때 실행
	HOGDescriptor hog(Size(20, 20), // _winSize
		Size(8, 8),		// _blockSize
		Size(4, 4),		// _blockStride,
		Size(4, 4),		// _cellSize,
		9);				// _nbins,
#else
	HOGDescriptor hog(Size(20, 20), // _winSize
		Size(10, 10),	// _blockSize
		Size(5, 5),		// _blockStride,
		Size(5, 5),		// _cellSize,
		9);				// _nbins,
#endif

	size_t descriptor_size = hog.getDescriptorSize();
	cout << "Descriptor Size : " << descriptor_size << endl;

	Mat train_hog, train_labels;

	for (int j = 0; j < 50; j++) {
		for (int i = 0; i < 100; i++) {
			Mat roi = digits(Rect(i * 20, j * 20, 20, 20)).clone(); 
			// digits 영상의 (i*20, j*20) 좌표에서 시작해서 가로 20px, 세로 20px 를 짤라내서 roi에 저장함

			vector<float> desc;
			hog.compute(roi, desc); // 이 잘라낸 roi로부터 hog 계산

			Mat desc_mat(desc, true); 
			// vector <float> desc로 계산이되는데 Mat 형태로 변환, 324 x 1 세로로 긴 형태로 만듬
			
			train_hog.push_back(desc_mat.t()); 
			// train_hog에 desc_mat(324 x 1)의 transpose(1 x 324)를 새로운 행을 하나씩 추가함
			
			train_labels.push_back(j / 5); // 각각의 숫자자 이미지에 대한 클래스 번호를 담고 있음
			// 처음 500개의 행렬은 0, 그 다음 500개의 행렬은 1 ... 이렇게 9까지
		}
	}

	Ptr<SVM> svm = SVM::create(); // SVM 객체 생성
//	Ptr<SVM> svm = SVM::load("svmdigits.yml");

	svm->setType(SVM::C_SVC); // SVM 타입
	svm->setKernel(SVM::RBF); // SVM 커널

#if 1
	svm->setGamma(0.50625);
	svm->setC(2.5);
	svm->train(train_hog, ROW_SAMPLE, train_labels); // 위의 parameter 값을 이용해 train만 진행
#else
	svm->trainAuto(train_hog, ROW_SAMPLE, train_labels); 
	// 이 함수를 실행하게되면 너무 오래걸려서 이 코드를 통해 계산된 결과 파라미터 값을 위에서 직접 입력하여 train만 진행함
#endif
//	svm->save("svmdigits.yml"); // 학습된 결과는 sml 또는 yml 파일로 저장할 수 있음
	// 이 파일을 불러오게되면 학습된 데이터가 다 들어있으므로 따로 학습할 필요는 없음

	// 입력 이미지 생성
	img = Mat::zeros(400, 400, CV_8U);

	imshow("img", img);
	setMouseCallback("img", on_mouse); // mouse콜백함수를 등록함

	while (true) {
		int c = waitKey();

		if (c == 27) { // esc 키를 누르면 종료
			break;
		} else if (c == ' ') { // 사용자가 스페이스바를 누 르면
			Mat img_blur, img_resize;
			GaussianBlur(img, img_blur, Size(), 1); // 400 x 400 이미지를 블러랑함
			resize(img_blur, img_resize, Size(20, 20), 0, 0, INTER_AREA); // 20 x 20 으로 resize를 함

			vector<float> desc;
			hog.compute(img_resize, desc); // resize된 이미지에서 hog 특징벡터를 계산함

			Mat desc_mat(desc, true); // hog 특징벡터를 매트리스 형식으로 변환
			float res = svm->predict(desc_mat.t()); // 324 x 1 형태의 매트리스를 1 x 324로 transpose한 행렬을 SVM알고리즘의 predict 함수로 전달하여 그 결과 값일 res로 저장
			cout << cvRound(res) << endl; // res를 반올림한 후 출력

			img.setTo(0);
			imshow("img", img);
		} else if (c == 'c') {
			img.setTo(0);
			imshow("img", img);
		}
	}
}
  • 실행된 창에서 숫자를 입력하고 space바를 누르면 인식된 숫자를 출력함
  • 입력할 때 가운데에 입력해야 제대로 인식함
    • 한쪽에 치우쳐서 입력하면 다른 쪽에서 그 숫자가 가지는 엣지 정보를 얻을 수 없으므로 인식률이 떨어질 수 밖에 없음
  • digits.png 데이터를 80 : 20 으로 나눠서 검증하면 98.7%의 정확도를 가짐

image

image

숫자 영상 정규화
  • 훈련 데이터 영상과 테스트 데이터 영상의 위치, 크기, 회전 등의 요소를 정규화하여 인식 성능을 향상 시킬 수 있음

    • 숫자 영상의 바운딩 박스를 기준으로 중앙으로 위치를 보정

    • 무게중심을 중앙에 위치하도록 위치 보정

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

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

Mat img;
Point ptPrev(-1, -1);

void on_mouse(int event, int x, int y, int flags, void*)
{
	if (x < 0 || x >= img.cols || y < 0 || y >= img.rows)
		return;
	if (event == EVENT_LBUTTONUP || !(flags & EVENT_FLAG_LBUTTON)) // 마우스가 떼질 때
		ptPrev = Point(-1, -1);
	else if (event == EVENT_LBUTTONDOWN) // 마우스가 눌릴 때
		ptPrev = Point(x, y);
	else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) // 마우스가 눌려있으면서 움직일 때
	{
		Point pt(x, y);
		if (ptPrev.x < 0)
			ptPrev = pt;
		line(img, ptPrev, pt, Scalar::all(255), 40, LINE_AA, 0);
		ptPrev = pt;

		imshow("img", img);
	}
}

Mat norm_digit(Mat& src) // 작은 크기의 이미지를 인자로 받아와서 
{
	CV_Assert(!src.empty() && src.type() == CV_8UC1);

	Mat src_bin;
	threshold(src, src_bin, 0, 255, THRESH_BINARY | THRESH_OTSU); // 이진화를 수행

	Mat labels, stats, centroids;
	int n = connectedComponentsWithStats(src_bin, labels, stats, centroids); 
	// connectedCompentsWithStats를 통해 lalbeling 작업을 진행함
	// centroids에 각각의 객체의 무게중심 좌표를 double 값으로 결과를 줌

	Mat dst = Mat::zeros(src.rows, src.cols, src.type());
	for (int i = 1; i < n; i++) {
		if (stats.at<int>(i, 4) < 10) continue;

		int cx = cvRound(centroids.at<double>(i, 0)); // 무게중심 좌표의 x 좌표를 받아옴
		int cy = cvRound(centroids.at<double>(i, 1)); // 무게중심 좌표의 y 좌표를 받아옴

		double dx = 10 - cx; 
		double dy = 10 - cy; // (10,10) 위치가 되도록

		Mat warpMat = (Mat_<double>(2, 3) << 1, 0, dx, 0, 1, dy); // (10,10) 위치가 되도록 이동 변환을 수행함
		warpAffine(src, dst, warpMat, dst.size());
	}

	return dst;
}

int main()
{
	Mat digits = imread("digits.png", IMREAD_GRAYSCALE); 

	if (digits.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

#if _DEBUG // 디버그 모드일때 실행
	HOGDescriptor hog(Size(20, 20), // _winSize
		Size(8, 8),		// _blockSize
		Size(4, 4),		// _blockStride,
		Size(4, 4),		// _cellSize,
		9);				// _nbins,
#else
	HOGDescriptor hog(Size(20, 20), // _winSize
		Size(10, 10),	// _blockSize
		Size(5, 5),		// _blockStride,
		Size(5, 5),		// _cellSize,
		9);				// _nbins,
#endif

	size_t descriptor_size = hog.getDescriptorSize();
	cout << "Descriptor Size : " << descriptor_size << endl;

	Mat train_hog, train_labels;

	for (int j = 0; j < 50; j++) {
		for (int i = 0; i < 100; i++) {
			Mat roi = digits(Rect(i * 20, j * 20, 20, 20)).clone(); 
			// digits 영상의 (i*20, j*20) 좌표에서 시작해서 가로 20px, 세로 20px 를 짤라내서 roi에 저장함

			vector<float> desc;
			hog.compute(norm_digit(roi), desc); // 이 잘라낸 roi로부터 hog 계산

			Mat desc_mat(desc, true); 
			// vector <float> desc로 계산이되는데 Mat 형태로 변환, 324 x 1 세로로 긴 형태로 만듬
			
			train_hog.push_back(desc_mat.t()); 
			// train_hog에 desc_mat(324 x 1)의 transpose(1 x 324)를 새로운 행을 하나씩 추가함
			
			train_labels.push_back(j / 5); // 각각의 숫자자 이미지에 대한 클래스 번호를 담고 있음
			// 처음 500개의 행렬은 0, 그 다음 500개의 행렬은 1 ... 이렇게 9까지
		}
	}

	Ptr<SVM> svm = SVM::create(); // SVM 객체 생성
//	Ptr<SVM> svm = SVM::load("svmdigits.yml");

	svm->setType(SVM::C_SVC); // SVM 타입
	svm->setKernel(SVM::RBF); // SVM 커널

#if 1
	svm->setGamma(0.50625);
	svm->setC(2.5);
	svm->train(train_hog, ROW_SAMPLE, train_labels); // 위의 parameter 값을 이용해 train만 진행
#else
	svm->trainAuto(train_hog, ROW_SAMPLE, train_labels); 
	// 이 함수를 실행하게되면 너무 오래걸려서 이 코드를 통해 계산된 결과 파라미터 값을 위에서 직접 입력하여 train만 진행함
#endif
//	svm->save("svmdigits.yml"); // 학습된 결과는 sml 또는 yml 파일로 저장할 수 있음
	// 이 파일을 불러오게되면 학습된 데이터가 다 들어있으므로 따로 학습할 필요는 없음

	// 입력 이미지 생성
	img = Mat::zeros(400, 400, CV_8U);

	imshow("img", img);
	setMouseCallback("img", on_mouse); // mouse콜백함수를 등록함

	while (true) {
		int c = waitKey();

		if (c == 27) { // esc 키를 누르면 종료
			break;
		} else if (c == ' ') { // 사용자가 스페이스바를 누 르면
			Mat img_blur, img_resize;
			GaussianBlur(img, img_blur, Size(), 1); // 400 x 400 이미지를 블러랑함
			resize(img_blur, img_resize, Size(20, 20), 0, 0, INTER_AREA); // 20 x 20 으로 resize를 함

			vector<float> desc;
			hog.compute(norm_digit(img_resize), desc); // resize된 이미지에서 hog 특징벡터를 계산함

			Mat desc_mat(desc, true); // hog 특징벡터를 매트리스 형식으로 변환
			float res = svm->predict(desc_mat.t()); // 324 x 1 형태의 매트리스를 1 x 324로 transpose한 행렬을 SVM알고리즘의 predict 함수로 전달하여 그 결과 값일 res로 저장
			cout << cvRound(res) << endl; // res를 반올림한 후 출력

			img.setTo(0);
			imshow("img", img);
		} else if (c == 'c') {
			img.setTo(0);
			imshow("img", img);
		}
	}
}