Node.js 란?

웹 브라우저를 통해 PC에 있는 문서 파일 하나를 업로드 하고 싶다면 먼저 웹 서버에 업로드를 요청해야 합니다. 이때 웹 서버에는 파일 업로드 기능을 담당하는 핸들러(Handler)를 하나 만들어줍니다. 파일의 크기가 크다면 파일을 업로드하는 데 1분 또는 그 이상의 시간이 걸릴 수 있습니다. 파일 업로드를 완료하기 전에는 서버에 있는 다른 파일의 정보를 확인하거나 파일 업로드가 어떻게 진행되는지 요청하는 것이 불가능하여 업로드가 진행될 때까지 대기해야 했습니다.

위와 같은 문제를 해결하기 위해 개발된 것이 노드입니다. 하나의 요청 처리가 끝날 때까지 기다리지 않고 다른 요청을 동시에 처리할 수 있는 비동기 입출력(Non-Blocking IO) 방식을 적용했습니다. 


동기/비동기 입출력 방식이란?

동기 입출력 방식의 프로그램의 작동 순서는 다음과 같습니다.

  1. 프로그램이 파일 시스템에 읽기 요청 전송
  2. 파일 시스템에서 디스크에 있는 파일 확인 / 준비
  3. 프로그램이 파일의 내용을 읽어와 화면에 띄움


비동기 입출력 방식의 프로그램의 작동 순서는 다음과 같습니다.

  1. 프로그램이 파일 시스템에 읽기 요청
  2. 등록된 콜백 함수를 호출하여 파일 내용을 화면에 띄움


동기 입출력 방식과 비동기 입출력 방식은 아래와 같이 코드를 짤 수 있습니다.

/* Blocking IO */
var content = file.read('a.txt');

=================대기===============
doShow(contents);
var result = doAdd(10, 10);



/* Non-Blocking IO */
file.read('a.txt', function(contents){
doShow(contents);
});

var result = doAdd(10, 10);

위의 코드에서는 첫 번째 코드가 실행되고 대기하는 시간이 있지만, 아래의 코드는 파일 읽기를 요청한 후 바로 doAdd() 함수를 호출합니다. 파일 읽기가 완료되었을 때 파일 시스템에서 콜백 함수를 호출하는데, 파일 시스템이 이벤트와 함께 호출하는 방식이면 이벤트 기반 입출력(Event Driven I/O) 모델이라고 부릅니다.


노드에서 구현하는 이벤트 기반 입출력 방식

노드를 설치하고 노드로 프로그램을 만들어 실행하면 크롬 V8 엔진 위에서 실행됩니다. V8 엔진에는 필요한 기능을 병렬로 실행하는 '스레드 풀'과 이벤트를 받아 처리하는 '이벤트 루프' 등의 기본 기능이 있으며, 그 위에 네트워크를 담당하는 소켓(Socket), http 라이브러리 들이 있습니다. 그리고 그 위에 표준 라이브러리가 구현되어 있습니다. 

노드 프로그램이 동작할 수 있도록 만든 이 아키텍쳐에서 가장 중요한 부분 중 하나가 이벤트를 받아 처리하는 기능입니다. 특히 노드는 서버 쪽에서 동작하는 프로그램을 만드는 것이 주 임무이기 때문에 기본 기능인 소켓이나 HTTP 프로토콜을 사용해 데이터를 송수신하는 기능을 포함하고 있습니다. 따라서 데이터 송수신 부분에도 이벤트 처리 방식을 그대로 사용하고 있습니다.

노드의 기본 기능인 http를 이용해 웹 서버로 요청을 보내고 응답을 받는 코드의 일부를 살펴보도록 하겠습니다.

http.request(options, function(res){
res.on('data', function(chunk){
console.log('BODY :' + chunk);
});
});

http 객체는 HTTP 프로토콜로 웹 서버에 데이터를 요청할 수 있는 기능이 있습니다. request() 함수를 호출하여 웹 서버에 데이터를 요청할 수 있고 응답을 받으면 콜백 함수가 자동으로 호출됩니다. 응답을 처리할 수 있는 콜백 함수는 res라는 이름의 객체를 전달받는데, 이 res객체는 수신한 데이터를 이벤트 이름으로 구별할 수 있습니다. 예를 들어, data 이벤트가 있을 때 이 이벤트 이름으로 콜백 함수를 등록하면, 등록한 콜백 함수에 data 이벤트로 전달된 응답 데이터가 전달됩니다.

자바스크립트에서는 on() 메소드를 활용하여 이벤트를 콜백 함수와 바인딩(Binding) 할 수 있습니다. 응답 객체인 res 객체의 on() 메소드를 사용해 data 이벤트와 콜백 함수를 바인딩하면 data라는 이름의 이벤트를 받았을 때 등록한 콜백 함수가 실행됩니다. 앞에서 살펴본 코드는 이벤트 이름이 data이며 응답으로 받은 데이터는 chunk 변수에 들어가 있습니다.


References

  • Do it! Node.js 프로그래밍


D3.js란?


D3는 Data - Driven Document에서 따온 것이다. D3.js는 웹으로 접근할 수 있고 데이터 시각화를 정교하게 하려는 요구를 만족하기 위해 만들어졌습니다. D3.js는 CSS3, HTML5, SVG(Scalable Vector Graphics)등 웹 표준 기능을 활용하여 여러가지 기능을 제공합니다.

D3.js는 웹 표준과 HTML의 핵심인 DOM(Document Object Model)과 통합되어 있다. D3를 사용하면 데이터 기반의 대화형 콘텐츠를 제작할 수 있습니다. 또한, 고성능 데이터 대시보드를 만들어 정교하게 데이터를 시각화하고 동적으로 웹 콘텐츠를 갱신할 수 있는 도구를 제공합니다.


D3.js의 작동 방식

D3를 사용하는 웹 페이지는 일반적으로 스타일, 데이터, 콘텐츠를 로딩하는 전통적인 HTML 웹 페이지를 만드는 방식으로 이루어집니다.초기화면은 HTML 요소의 D3 셀렉션을 이용하거나 선택적으로 데이터를 바인딩하고 페이지의 구조와 모습을 변경합니다. 구조가 변경되면 사용자가 반응하게 되며, 사용자의 반응은 페이지의 콘텐츠를 더 많이 바뀌게 합니다. 여기에서 1단계는 페이지를 로딩할 때 한 번만 일어나므로 회색으로 표시했으며, 그 밖에 다른 단계는 사용자의 입력에 따라 여러번 일어날 수 있습니다.

셀렉션 안의 요소에 바인딩된 데이터에 대한 참조를 이용해 요소의 모습을 변경할 수 있습니다. D3는 셀렉션 안의 요소에 바인딩된 데이터로 같은 작업을 반복 적용해 다양한 시각적 효과를 낼 수 있습니다. 같은 작업을 적용해도 데이터값이 다르면 효과도 다릅니다. 이번 장 마지막 부분에 데이터 바인딩 예제가 처음으로 나오며, 이후 다양한 예를 볼 수 있습니다.


EEG-based worker's stress recognition at Construction Sites


Paper Information.

  • YEAR : 2018
  • AUTHOR : Houtan Jebelli 외 2명
  • JOURNAL : Automation in Construction / SCIE


Abstract

  과도한 스트레스에 시달리는 많은 건설 노동자가 안전과 건강에 악영향을 미치기 때문에 스트레스를 미리 인지하는 것은 중요하다고 할 수 있습니다. 본 연구에서는 뇌파 신호를 이용하여 건설 현장에서 작업자의 스트레스를 자동으로 인식하는 모델을 개발하였습니다. 


Method / Results

Procedure 

  14개의 다른 채널을 통해 EEG 신호를 측정하였습니다. EEG 신호는 1초에 128개의 데이터 포인트를 가지고 있습니다. EEG 신호의 눈 움직임, 숨쉬기 등과 같은 노이즈는 저자의 이전 연구에서 제안한 EEG 신호 전처리 방법으로 제거하였습니다. 이후 시간 및 주파수 영역의 뇌파 특징을 추출하였습니다. 이후, 스트레스를 가장 잘 나타내는 클래스를 선택하기 위해, 여러 가지 지도 학습 알고리즘의 성능을 비교하였습니다.

< Fig. 1. Overview of stress recognition procedure >


Feature Selection

본 연구에서 사용한 특징 추출법은 Correlation-based Method(상관 관계 기반)과 Wrapper Method입니다. 540개의 특징들 중 Correlation-based Method를 사용하여 예측 정확도와의 연관성이 가장 적은 특징들을 필터링하고 Wrapper Method를 사용하여 분류 정확도를 최대화하는 특징들의 하위 집합을 조사하여 80개의 최종 특징을 추출하였습니다. 

< Fig. 2. Time and Frequency domains features, extracted from EEG signals >


EEG Classification 

본 연구에서는 k-NN, GDA, SVM, HMM, DT, Logistic 머신 러닝 기법을 통해 성능 평가를 진행하였습니다. 훈련 데이터와 테스트 데이터는 9:1의 비율로 나누었습니다. 고정 윈도우 방식과 슬라이딩 윈도우 방식에서 모두 GDA가 80.32%의 정확도로 가장 우수한 성능을 나타내었습니다.

< Fig. 3. Classification accuracies of each tested algorithm >

Summary

  본 연구는 아래와 같은 순서로 다음과 같은 결과를 나타내었습니다.

  1. 14채널 EEG 장비를 통해 EEG 신호를 측정하였습니다.

  2. EEG 신호의 노이즈는 ICA를 적용하여 제거하였습니다.

  3. EEG 신호를 시간 및 주파수 영역으로 구분하여 뇌파 특징을 추출하였습니다.

  4. 특징 추출에는 Correlation-based Method와 Wrapper-Based Method가 사용되었습니다.
    >> 최초 540개의 특징을 위의 2가지 방법을 사용하여 80개로 축소하였습니다.

  5. 추출된 특징을 5개의 머신 러닝 알고리즘에 적용하였습니다.
    >> GDA 알고리즘이 고정 윈도우, 슬라이딩 윈도우에서 모두 우수한 성능을 나타냈습니다.



Design of User Concentration Classification Model by EEG Analysis Based on Visual SCPT

Paper & Journal Info.

Title 

 Design of User Concentration Classification Model by EEG Analysis Based on Visual SCPT

Author 

 Jinhyeok Park, Seokhwan Kang, Byungmun Lee, Ungu Kang, Youngho Lee 

Journal / Conf.

 Journal of The Korea Society of Computer and Information

Journal Grade 

 KCI


ABSTRACT

In this study, we designed a model that can measure the level of user's concentration by measuring and analyzing EEG data of the subjects who are performing Continuous Performance Test based on visual stimulus . This study focused on alpha and beta waves, which are closely related to concentration in various brain waves. There are a lot of research and services to enhance not only concentration but also brain activity. However, there are formidable barriers to ordinary people for using routinely because of high cost and complex procedures. Therefore, this study designed the model using the portable EEG measurement device with reasonable cost and Visual Continuous Performance Test which we developed as a simplified version of the existing CPT. This study aims to measure the concentration level of the subject objectively through simple and affordable way, EEG analysis. Concentration is also closely related to various brain diseases such as dementia, depression, and ADHD. Therefore, we believe that our proposed model can be useful not only for improving concentration but also brain disease prediction and monitoring research. In addition, the combination of this model and the Brain Computer Interface technology can create greater synergy in various fields. 


METHOD

본 연구에서 진행된 실험 총 20명을 대상으로 교내 실험실에서 진행되었으며, 실험실의 온도 및 조도는 실험을 진행하는 동안 동일하게 유지하였다. 헤드밴드 형태로 제작된 뇌파 측정 장비를 착용한 상태로 의자에 착석하여 책상 위의 모니터 쪽을 바라보는 상태로 실험이 진행되었다.

의 진행 차는 모든 자에게 동일하게 적용하였다. 피자는 실실에 실하여 의자에 은 상에서 사전 설문지를 성하였다. 사전 설문지에는 피자의 간단한 인적 정보를 파악할 수 있는 질들과 현기분 상, 수시간 등 뇌파에 영향을 줄 수 있는 일반적 성 질으로 구성하였다. 


뇌파 측정은 기존의 복잡하고 부하기 힘기존의 침습형 뇌파 장비와 달리 간단하고 빠르게 측정할 수 있도록 Fig. 1과 같이 드 형로 제InteraXon 사의 4채널 EEG 장비 인 ‘Muse 2017'을 사용하여 측정하였다. 'Muse 2017'은 측정할 수 있는 기기로 전두엽의 2채널 (AF8, AF9)과 측두엽 2채널(Tp9, Tp10) 총 4채널에서 발생 하는 뇌파 데이터 수집할 수 있다. Muse 2017은 다양한 시중 의 뇌파 측정 장비들에 비해 비적 저한 가격대를 형성하고 있으, 간소화된 대용 뇌파 측정 장비로서 그 기능을 검증받았다.

주의력과 충동성을 검사하기 위해 개발된 검사인 CPT(Continuous Performance Test)는 컴퓨터를 이용하여 실 자가 집중 상얼마잘 유지할 수 있는지를 가하기 위한 검사이다[12,13]. 단순한 도형이나 알파자 등의 표적 자극과 비표적 자극이 1에서 4간격으로 화0.1 에서 0.2무작위로 표시된다. 실정한 표적 자극이 제시된 경우에만 실대상자가 반응하도록 하는 형로 구성어 있다.

이는 그 용성이 이인정어 널리 이고는 있지만, 2~3시간소요가 시간이 너무 길고, 자나 자를 자 극으로 사용할 경우에는 어 능력이나 자 능력이 자라는 아동에게 실시할 수 다는 한계점을 가지고 있다. 따라서, 앞 언급된한계점들을보완개선하기위해본연구진이자 개발한 SCPT(Simplified Continuous Performance Test)를 수행하였다.

본 실에서 사용한 Visual SCPT 콘텐츠Unity 3D로 개 발된 로그컴퓨모니터를 해 제시하였으, 자극의 제시 토콜‘Oddball Paradigm'에 따라 시행되었다. 'Oddball Paradigm'은 피자로 하여금 빈번한 비표적 자극과 표적 자극을 분하게 하는 것으로, SCPT 콘텐츠를 구성하는 데 있어서 중요한 역할을 수행하였다[14].

SCPT는 원, 각형, 사각형, 각형, 별 모양 등 간단한 도 형 및 모양으로 구성하여 보다 다양한 연대에 적용할 수 있 도록 하였다. 또한, 자극이 시각 자극으로만 제한는 것이 아, 각, 각과 같이 다감각적으로 사용자에게 적용수 있 다. 또한 기존의 CPT가 2~3시간검사를 진행, SCPT 는 10~15분 사이로 진행하여 사용자의 검사 부담을 소화 하였다.

자극은 10분간 제시되며 표적 자극과 비표적 자극은 2:8의 비로 제시된다. 피자는 의자에 모니터의 정을 응시하여 표적 자극이 제시때에만 왼쪽 버튼러 반응하도록 하였다. 제시는 자극의 종류는 정사각형, 원, 각형 등으로 구성된 Fig. 3의 6개의 도형이, 그 중 표적 자 극은 Visual SCPT 수행 전 피자 본인이 선호하는 표적 자극 을 선택할 수 있도록 하였다. 


데이터 분석은 크게 EEG 주의집중도와 SCPT 주의집중도로 나누어 분석하였다. EEG 주의집중도는 뇌파를 측정할 때에 발생하는 데이터를 기준으로, SCPT 주의집중도는 SCPT 평가 결과에 따른 주의집중도로 정의할 수 있다. EEG 주의집중도는 집중도와 관련이 있는 3개의 데이터(알파파, 베타파, 세타파)를 사용하였고, SCPT 주의집중도는 정반응비율, 반응시간 평균, 반응시간 표준편차 3가지 기준에 따라 산출하였다.




동일 집단의 명상할 때와 Visual SCPT를 할 때의 주의집중도 차이를 비교분석하기 위해 Paired T-Test와 Pearson's product-moment correlation 기법을 사용하였다.


RESULT

명상할 때에 비해 Visual SCPT 콘텐츠를 진행할 때의 주의집중도가 유의하게 증가함을 확인할 수 있었다. 또한, EEG 주의집중도와 SCPT 주의집중도 간의 상관성을 확인할 수 있었다.


The Effect of Binaural beat-based Audiovisual Stimulation on Brain waves and concentration



Paper & Journal Info.

Title 

 The effect of binaural beat-based audiovisual stimulation on brain waves and concentration

Author 

 Jinhyeok Park, Hyunjin Kwon, Seokhwan Kang, Youngho Lee 

Journal / Conf.

 ICT Convergence Powered by Smart Intelligence

Journal Grade 

 SCOPUS


ABSTRACT

In this study, we analyzed the effect of binaural beat auditory stimulation on EEG. In particular, this research focused on alpha and beta waves of EEG, which are known to be closely related to attention. As a result, we were able to confirm that alpha waves are more active on all channels when providing alpha binaural beats than in meditation, and the temporal lobe was activated when we provided a beta binaural beat. Also, when we provided the alpha and beta binaural beats, the attention and concentration index was more activated on most channels. These results can help build content that is based on attention and concentration using binaural beat. 


METHOD

실험 대상은 총 15명이고, 실험은 조용한 환경의 교내 연구실에서 진행되었습니다. 뇌파 데이터는 헤드밴드 형태로 제작된 EEG 장비를 통해 전두엽 2곳, 측두엽 2곳의 뇌파 데이터를 추출하였습니다. 측정 위치는 International 10-20 System에 기반하였습니다. 뇌파는 일반적으로 알파파, 베타파, 세타파, 감마파, 델타파 총 5가지로 구분됩니다. 데이터 전처리에 원시 데이터를 주파수값으로 변환하기 위해 FFT기법을 활용하였습니다. 

< Fig. 1. International 10-20 System >


실험절차는 사전설문 > 명상 > 바이노럴 비트 제공 > 종료 순으로 진행되었습니다. 정제된 데이터를 얻기 위해 사전 설문을 통해 실험 대상이 실험에 적합한지 구분하였습니다. 구분 요소로 정신질환 여부, 카페인 섭취 여부, 수면 시간 등을 고려하였습니다. 이후, 바이노럴 비트를 들을 때와 듣지 않을 때의 데이터를 비교하기 위해 명상을 약 5분간 진행하였습니다. 명상 이후 알파 바이노럴 비트와 베타 바이노럴 비트를 각각 5분간 제공하였습니다.

< Fig. 2. Experimental Procedure Diagram >


데이터 R 3.5.0 Ver.을 사용하여 알파 바이노럴 비트, 베타 바이노럴 비트가 뇌파와 주의집중도에 어떠한 영향을 미치는지를 중점으로 분석하였습니다. 주의집중도는 국내∙외 논문에서 주의집중도를 산출하기 위해 많이 사용하고 있는 (알파파 + 베타파) / 세타파 공식을 사용하였습니다.


RESULT

명상 상태의 알파파와 알파 바이노럴 비트를 피험자에게 제공할 때의 알파파, 베타파의 평균을 비교 분석하였습니다. 알파 바이노럴 비트를 제공했을 때 알파파의 평균이 증가하는 것을 확인할 수 있습니다.


명상 상태의 베타파와 알파 바이노럴 비트를 피험자에게 제공할 때의 알파파, 베타파의 평균을 비교 분석하였습니다. 알파 바이노럴 비트를 제공했을 때 알파파의 평균이 증가하는 것을 확인할 수 있습니다.


명상 상태의 주의집중도와 알파, 베타 바이노럴 비트를 제공할 때의 주의집중도 평균을 비교 분석하였습니다. 대체로 바이노럴 비트를 제공할 때에 주의집중도가 높아지는 것을 확인할 수 있었습니다.





MNIST 데이터셋 검증 - 오차행렬

1. 오차 행렬

분류기의 성능을 평가하는 더 좋은 방법은 오차 행렬(Confusion Matrix)을 조사하는 것입니다. 클래스 A의 샘플이 클래스 B로 분류된 횟수를 세는 것입니다.

오차 행렬을 만들기 위해서는 실제 타깃과 비교할 수 있도록 먼저 예측값을 만들어야 합니다. 테스트 세트로 예측을 만들 수 있지만 여기서 사용해서는 안됩니다. 대신 cross_val_predict() 함수를 사용할 수 있습니다.

Code

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
print(confusion_matrix(y_train_5, y_train_pred))

Output

54,125개를 '5 아님'으로 정확하게 분류하였고(TN), 나머지 454개는 '5'라고 잘못 분류하였습니다(FP).
1,567개를 '5 아님'으로 잘못 분류하였고(FN), 나머지 3,854개를 정확히 '5'라고 분류하였습니다(TP). 

완벽한 분류기라면 진짜 양성진짜 음성만 가지고 있을 것이므로 오차 행렬의 주대각선만 0이 아닌 값이 됩니다.



오차 행렬이 많은 정보를 제공해주지만, 가끔 더 요약된 지표가 필요할 때도 있습니다.
살펴볼만한 것 하나는 양성 예측의 정확도입니다. 이를 분류기의 정밀도(Precision)라고 합니다. 


< Exp. 1. 정밀도 공식 >

TP는 진짜 양성의 수이고, FP는 거짓 양성의 수입니다.

확실한 양성 샘플 하나만 예측하면 간단히 완벽한 정밀도를 얻을 수 있지만, 이는 분류기가 다른 모든 양성 샘플을 무시하기 때문에 그리 유용하지 않습니다. 정밀도는 재현율(Recall) 이라는 또 다른 지표와 같이 사용하는 것이 일반적입니다. 재현율은 분류기가 정확하게 감지한 양성 샘플의 비율로, 민감도(Sensitivity), TPR(True Positive Rate) 이라고도 합니다.



2. 정밀도와 재현율

사이킷런에서는 정밀도와 재현율을 포함하여 분류기의 지표를 계산하는 여러 함수를 제공하고 있습니다.

Code

from sklearn.metrics import precision_score, recall_score
print(precision_score(y_train_5, y_train_pred))
print(recall_score(y_train_5, y_train_pred))

Output

5로 판별된 이미지 중 74%만 정확합니다. 전체 숫자에서는 85%만 감지하였습니다.



정밀도와 재현율을 F₁ 점수라고 하는 하나의 숫자로 만들면 편리할 때가 많습니다. 특히 두 분류기를 비교할 때 유용합니다.
F₁ 점수는 정밀도와 재현율의 조화 평균(Harmonic Mean)입니다.


< Exp. 2. F₁ 점수 공식 >


F₁ 점수를 계산하기 위해서는 f1_score() 함수를 호출하면 됩니다.

Code

from sklearn.metrics import f1_score
print(f1_score(y_train_5, y_train_pred))

Output

정밀도와 재현율이 비슷한 분류기에서는 F₁ 점수가 높습니다. 하지만 이게 항상 바람직한 것은 아닙니다. 상황에 따라 정밀도가 중요할 수도 있고 재현율이 중요할 수도 있습니다.
아쉽지만 정밀도와 재현율을 둘다 얻을 수는 없습니다. 정밀도를 올리면 재현율이 줄고 그 반대도 마찬가지 입니다. 이를 정밀도 / 재현율 트레이드오프라고 합니다.



References

  • 오렐리앙 제롱, '핸즈온 머신러닝', 한빛미디어, 2018


MNIST 데이터셋 분류


1. MNIST

MNIST는 고등학생과 미국 인구조사국 직원들이 손으로 쓴 70,000개의 작은 숫자 이미지를 모은 데이터셋입니다.
각 이미지에는 어떤 숫자를 나타내는지 레이블 되어 있습니다. 이 데이터셋은 학습용으로 아주 많이 사용됩니다.

사이킷런에서 제공하는 여러 헬퍼 함수를 사용해 MNIST 데이터셋을 내려받을 수 있습니다.

Code

from sklearn.datasets import fetch_mldata
X, y = mnist["data"], mnist["target"]
print(X.shape)
print(y.shape)

Output

이미지가 70,000개가 있고 각 이미지에는 784개의 특성이 있습니다. 이미지가 28 × 28 픽셀이기 때문입니다.
개개의 특성은 단순히 0(흰색) 부터 255 (검은색) 까지의 픽셀 강도를 나타냅니다.



데이터셋에서 이미지 하나를 확인해보도록 하겠습니다. 샘플의 특성 벡터를 추출해서 28 × 28 배열로 크기를 바꾸고 맷플롯립의 imshow() 함수를 활용합니다.

Code

import matplotlib
import matplotlib.pyplot as plt

some_digit = X[36000]
some_digit_image = some_digit.reshape(28, 28)

plt.imshow(some_digit_image, cmap=matplotlib.cm.binary,
interpolation="nearest")
plt.axis("off") plt.show() print(y[36000])

Output


>> 5.0

그림 상으로 숫자 '5' 처럼 보이는데, 실제 레이블을 확인해보니 5.0으로 나타납니다.



데이터를 분석하기 전 테스트 세트를 만들고 시작하도록 하겠습니다. MNIST 데이터셋의 경우 이미 훈련 세트(앞쪽 60,000개)와 테스트 세트(뒤쪽 10,000)로 나누어져 있습니다.

Code

import numpy as np
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
shuffle_index = np.random.permutation(60000)
X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]

훈련 세트를 섞어 모든 교차 검증 폴드가 비슷해지도록 만들었습니다. 특정 학습 알고리즘은 훈련 샘플의 순서에 민감하게 반응해서 많은 비슷한 샘플이 연이어 나타나면 성능이 나빠집니다. 데이터셋을 섞으면 이러한 문제를 방지할 수 있습니다.



2. 이진 데이터 훈련

문제를 단순화해서 하나의 숫자만 식별해보도록 하겠습니다. 이 감지기는 '5'와 '5가 아님' 두개의 클래스를 구분할 수 있는 이진 분류기(Binary Classifier) 의 예입니다.

Code

from sklearn.linear_model import SGDClassifier

y_train_5 = (y_train == 5)
y_test_5 = (y_test == 5)

sgd_clf = SGDClassifier(max_iter=5, random_state=42)
sgd_clf.fit(X_train, y_train_5)

print(sgd_clf.predict([some_digit]))

Output

>> [True]

분류기가 해당 이미지가 5를 나타낸다고 추측했습니다.



3. 성능 측정

3.1 교차 검증을 사용한 정확도 측정

교차 검증은 모델을 평가하는 좋은 방법 중 하나입니다. 사이킷런에서 교차 검증 함수를 제공하지만, 교차 검증 과정을 더 많이 제어해야 하는 경우도 있습니다.
이때는 교차 검증 기능을 직접 구현하면 됩니다. 아래 코드는 사이킷런의 cross_val_score() 함수와 거의 같은 작업을 수행하고 동일한 결과를 출력합니다.

StratifiedKFold는 클래스별 비율이 유지되도록 폴드를 만들기 위해 계층적 샘플링을 수행합니다. 매 반복에서 분류기 객체를 복제하여 훈련 폴드로 훈련시키고 테스트 폴드로 예측을 만듭니다. 이후 올바른 예측 수를 세어 정확한 예측의 비율을 출력합니다.

Code

from sklearn.linear_model import SGDClassifier
from sklearn.model_selection import StratifiedKFold
skfolds = StratifiedKFold(n_splits=3, random_state=42)

for train_index, test_index in skfolds.split(X_train, y_train_5):
clone_clf = clone(sgd_clf)
X_train_folds = X_train[train_index]
y_train_folds = y_train_5[train_index]
X_test_fold = X_train[test_index]
y_test_fold = y_train_5[test_index]

clone_clf.fit(X_train_folds, y_train_folds)
y_pred = clone_clf.predict(X_test_fold)
n_correct = sum(y_pred == y_test_fold)
print(n_correct / len(y_pred))

Output



사이킷런의 cross_val_score() 함수를 사용하여 폴드가 3개인 K-겹 교차 검증을 사용해 SGDClassifier 모델을 평가해보겠습니다. K-겹 교차 검증은 훈련 세트를 K개의 폴드로 나누고, 각 폴드에 대해 예측을 만들고 평가하기 위해 나머지 폴드로 훈련시킨 모델을 사용합니다.

Code

from sklearn.model_selection import cross_val_score

print(cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy"))

Output

모든 교차 검증 폴드에 대해 정확도(Accuracy)가 95% 이상임을 확인할 수 있습니다.



그렇다면, 모든 이미지를 '5가 아님' 클래스로 분류하는 더미 분류기를 만들어 성능을 비교해보도록 하겠습니다.

Code

from sklearn.base import BaseEstimator
class Never5Classifier(BaseEstimator):
def fit(self, X, y=None):
pass

def predict(self, X):
return np.zeros((len(X), 1), dtype=bool)


never_5_clf = Never5Classifier()
print(cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy"))

Output

정확도가 90% 이상 나오는 것을 확인할 수 있습니다만, 숫자 '5'를 분류할 때보다 성능이 떨어지는 것을 확인할 수 있었습니다.
이 예제는 정확도를 분류기의 성능 지표로 선호하지 않는 이유를 보여줍니다. 특히 불균형한 데이터셋을 다룰 때 더욱 그렇습니다.



References

  • 오렐리앙 제롱, '핸즈온 머신러닝', 한빛미디어, 2018


머신러닝 알고리즘을 위한 특성 스케일링

데이터에 적용할 가장 중요한 변환 중 하나가 특성 스케일링(Feature Scaling)입니다. 

머신러닝 알고리즘은 입력 숫자 특성들의 스케일이 많이 다르면 잘 작동하지 않습니다. 주택 가격 데이터도 이에 해당합니다. 즉, 전체 방 개수의 범위는 6에서 39,320인 반면 중간 소득의 범위는 0에서 15까지 입니다. 타깃 값에 대한 스케일링은 일반적으로 불필요합니다.

모든 특성의 범위를 같도록 만들어주는 방법으로 min-max 스케일링(정규화, Normalization)표준화(Standardization)가 널리 사용됩니다.

min-max 스케일링은 매우 간단합니다. 0~1 범위에 들도록 값을 이동하고 스케일을 조정하면 됩니다. 데이터에서 최소값을 뺀 후 최대값과 최소값의 차이로 나누면 이렇게 할 수 있습니다. 사이킷런에는 이에 해당하는 MinMaxScaler 변환기를 제공합니다. 0~1사이를 원하지 않는다면 feature_range 매개변수로 범위를 조정할 수 있습니다.

표준화는 많이 다릅니다. 먼저 평균을 뺀 후 표준편차로 나누어 결과 분포의 분산이 1이 되도록 합니다. 범위의 상한과 하한이 없어 어떤 알고리즘에서는 문제가 될 수 있습니다. 그러나 표준화는 이상치에 영향을 덜 받습니다. 사이킷런에는 표준화를 위한 StandardScaler 변환기가 있습니다.

*모든 변환기에서 스케일링은 (테스트 세트가 포함된) 전체 데이터가 아닌 훈련 데이터에 대해서만 fit() 함수를 적용해야 합니다. 이후 훈련 세트와 테스트 세트에 대해 transform() 함수를 사용합니다.



1. 변환 파이프라인

머신 러닝을 하기 위한 수많은 변환 단계는 정확한 순서대로 실행되어야 합니다. 사이킷런에는 연속된 변환을 순서대로 처리할 수 있도록 도와주는 Pipeline 클래스가 있습니다.

Code

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

num_pipeline = Pipeline([
('imputer', Imputer(strategy = "median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])

housing_num_tr = num_pipeline.fit_transform(housing_num)



Pipeline은 연속된 단계를 나타내는 이름/추정기 쌍의 목록을 입력으로 받습니다. 마지막 단계에는 변환기와 추정기를 모두 사용할 수 있고 그 외에는 모두 변환기여야 합니다. (즉, fit_transform() 함수를 가지고 있어야 합니다.)

파이프라인의 fit() 함수를 호출하면 모든 변환기의 fit_transform() 함수를 순서대로 호출하면서 한 단계의 출력을 다음 단계의 입력으로 전달합니다. 마지막 단계에서는 fit() 함수만 호출합니다.

파이프라인 객체는 마지막으로 추정기와 동일한 함수를 제공합니다. 이 예에서는 마지막 추정기가 변환기 StandardScaler이므로 파이프라인이 데이터에 대해 모든 변환을 순서대로 적용하는 transform() 함수를 가지고 있습니다. 

수치형 컬럼을 넘파이 배열로 추출하는 대신 판다스의 데이터프레임을 파이프라인에 직접 주입할 수 있다면 좋을 것 같습니다. 사이킷런이 판다스의 데이터프레임을 다룰 수는 없지만 이를 처리하는 변환기를 직접 만들 수 있습니다.

Code

from sklearn.base import BaseEstimator, TransformerMixin
class DataFrameSelector(BaseEstimator, TransformerMixin):
def __init__(self, attributes_names):
self.attributes_names = attributes_names

def fit(self, X, y = None):
return self

def transform(self, X):
return X[self.attributes_names].values

DataFrameSelector는 나머지는 버리고 필요한 특성을 선택하여 데이터프레임을 넘파이 배열로 바꾸는 식으로 데이터를 변환합니다. 이를 이용해 데이터프레임을 받아 수치형만 다루는 파이프라인을 손쉽게 만들 수 있습니다. 수치형 특성을 선택한 DataFrameSelector로 파이프라인을 시작해서 앞서 이야기한 다른 전처리 단계들을 나열합니다. 범주형 특성을 다루는 또 다른 파이프라인도 DataFrameSelector로 범주형 특성을 선택하고 CategoricalEncoder를 적용하면 간단합니다.

Code

from sklearn.pipeline import FeatureUnion
num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]

num_pipeline = Pipeline([
('selector', DataFrameSelector(num_attribs)),
('imputer', Imputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])

cat_pipeline = Pipeline([
('selector', DataFrameSelector(cat_attribs)),
('cat_encoder', CategoricalEncoder(encoding="onehot-dense"))
])

full_pipeline = FeatureUnion(transformer_list=[
("num_pipeline", num_pipeline),
("cat_pipeline", cat_pipeline),
])

housing_prepared = full_pipeline.fit_transform(housing)
print(housing_prepared)

전체 파이프라인을 간단하게 실행할 수 있습니다.



References

  • 오렐리앙 제롱, '핸즈온 머신러닝', 한빛미디어, 2018


머신러닝 알고리즘을 위한 데이터 준비

이전 포스팅 '머신러닝을 위한 데이터 가져오기', '머신러닝을 위한 테스트 세트 만들기'에 이어 진행됩니다.

머신러닝 알고리즘을 위해 데이터를 준비할 차례입니다. 이러한 작업들은 아래와 같은 이유로 함수를 통해 자동화됩니다.

  • 어떤 데이터셋에 대해서도 데이터 변환을 쉽게 반복할 수 있습니다.
  • 향후 프로젝트에 사용할 수 있는 변환 라이브러리를 점진적으로 구축하게 됩니다.
  • 실제 시스템에서 알고리즘에 새 데이터를 주입하기 전에 변환시키는 데 이 함수를 이용합니다.
  • 여러 가지 데이터 변환을 쉽게 시도해볼 수 있고 어떤 조합이 가장 좋은지 확인하는 데 편리합니다.

먼저 원래 훈련 세트로 복원하고, 예측 변수와 타깃 값에 같은 변형을 적용하지 않기 위해 예측 변수와 레이블을 분리하도록 하겠습니다.

Code

housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()



1. 데이터 정제

대부분의 머신러닝 알고리즘은 누락된 특성을 다루지 못하므로 이를 처리할 수 있는 함수를 몇 개 만들도록 하겠습니다. 방법은 아래와 같이 세 가지입니다.

  • 해당 구역을 제거합니다.
  • 전체 특성을 삭제합니다.
  • 어떤 값으로 채웁니다. (0, 평균값, 중간값 등)


데이터 프레임의 dropna(), drop(), fillna() 함수를 이용하면 이런 작업들을 간단히 처리할 수 있습니다.

Code

housing.dropna(subset=["total_bedrooms"]) # Option 1
housing.drop("total_bedrooms", axis=1) # Option 2
median = housing["tottal_bedrooms"].median() # Option 3
housing["total_bedrooms"].fillna(median, inplace=True)



하지만, 사이킷런의 Imputer는 누락된 값을 손쉽게 다루도록 해줍니다. 먼저 누락된 값을 특성의 중간값으로 대체한다고 지정하여 Imputer 객체를 생성합니다. 중간값이 수치형 특성에게만 계산될 수 있기 때문에 텍스트 특성인 ocean_proximity를 제외한 데이터 복사본을 생성합니다. 이후 imputer 객체의 fit() 함수를 사용해 훈련 데이터에 적용합니다.

imputer는 각 특성의 중간값을 계산하여 그 결과를 객체의 statistics_ 속성에 저장합니다. total_bedrooms 특성에만 누락된 값이 있지만, 나중에 시스템이 서비스될 때 새로운 데이터에서 어떤 값이 누락될지 확신할 수 없으므로 모든 수치형 특성에 imputer를 적용하는 것이 바람직할 것입니다.

Code

from sklearn.preprocessing import Imputer

imputer = Imputer(strategy="median")
housing_num = housing.drop("ocean_proximity", axis=1)
imputer.fit(housing_num)

print(imputer.statistics_)
print(housing_num.median().values)

Output

imputer 객체에 중간값이 정상적으로 학습된 것을 확인할 수 있습니다.



이제 학습된 imputer 객체를 사용해 훈련 세트에서 누락된 값을 학습한 중간값으로 바꿀 수 있습니다.

Code

X = imputer.transform(housing_num)
housing_tr = pd.DataFrame(X, columns=housing_num.columns,
index=list(housing.index.values))



2. 텍스트와 범주형 특성 다루기

앞서 살펴본 범주형 특성 ocean_proximity가 텍스트라 중간값을 계산할 수 없었습니다.

대부분의 머신러닝 알고리즘은 숫자형을 다루므로 이 카테고리를 텍스트에서 숫자로 바꾸도록 하겠습니다.
이를 위해 각 카테고리를 다른 정숫값으로 매핑해주는 판다스의 factorize() 함수를 사용합니다.

Code

housing_cat = housing["ocean_proximity"]
housing_cat_encoded, housing_categories = housing_cat.factorize()
housing_cat_encoded[:10]

print(housing_cat_encoded)

Output



위와 같은 표현 방식의 문제는 머신러닝 알고리즘이 가까이 있는 두 값이 떨어져 있는 두 값보다 더 비슷하다고 생각한다는 점입니다. 
이 문제는 일반적으로 카테고리별 이진 특성을 만들어 해결합니다. 한 특성만 1이고 나머지는 0이므로 이를 원-핫 인코딩(One-Hot Encoding)이라고 부릅니다.

사이킷런은 숫자로 된 범주형 값을 원-핫 벡터로 바꿔주는 OneHotEncoder를 제공합니다. 카테고리들을 원-핫 벡터로 인코딩 해보겠습니다. fit_transform() 함수는 2차원 배열을 넣어줘야 하는데 housing_cat_encoded는 1차원 배열이므로 구조를 바꿔주어야 합니다. 또한 출력 형태가 사이파이(Scipy) 희소 행렬입니다. 이는 수천 개의 카테고리가 있는 범주형 특성일 경우 매우 효율적입니다. 이런 특성을 원-핫 인코딩하면 열이 수천 개인 행렬로 변하고 각 행은 1이 하나뿐이고 그 외에는 모두 0으로 채워져 있을 것입니다. 0을 모두 메모리에 저장하는 것은 낭비이므로 희소 행렬은 0이 아닌 원소의 위치만 저장합니다. 이 행렬을 거의 일반적인 2차원 배열처럼 사용할 수 있지만 넘파이 배열로 바꾸려면 toarray() 함수를 호출하면 됩니다.

Code

from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder()
housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1))
print(housing_cat_1hot.toarray())

Output




References

  • 오렐리앙 제롱, '핸즈온 머신러닝', 한빛미디어, 2018



머신러닝을 위한 테스트 세트 만들기

이전에 포스팅 했던 '머신러닝을 위한 데이터 가져오기'에 이어 진행됩니다.

데이터를 더 깊게 들여다보기 이전에 테스트 세트를 따로 떼어 놓아야 합니다.
테스트 세트는 추후 훈련된 데이터를 평가하는데 사용되기 때문에 해당 데이터 세트는 훈련에 사용되어서는 안됩니다.


1. 테스트 세트 만들기

테스트 세트를 만드는 일은 이론적으로 매우 간단합니다. 무작위로 어떠한 샘플을 선택하여 데이터셋의 20% 정도를 떼어 놓으면 됩니다.
아래는 데스트 세트를 생성하는 코드입니다.

Code

import numpy as np

def split_train_test(data, test_ratio):
shuffled_indices = np.random.permutation(len(data))
test_set_size = int(len(data) * test_ratio)
test_indices = shuffled_indices[:test_set_size]
train_indices = shuffled_indices[test_set_size:]

return data.iloc[train_indices], data.iloc[test_indices]


train_set, test_set = split_train_test(housing, 0.2)
print(len(train_set), "train +", len(test_set), "test")

Output

전체 20930개의 데이터가 16512개의 훈련 데이터, 4128개의 테스트 데이터로 나뉜 것을 확인할 수 있습니다.



하지만, 위의 코드는 완벽하지 않습니다. 프로그램을 다시 실행하면 다른 테스트 세트가 생성되기 때문입니다.
여러 번 계속하면 전체 데이터 셋을 보는 셈이므로 이러한 상황은 피하는 것이 좋습니다.

따라서, 일반적인 해결책으로 샘플의 식별자를 사용하여 테스트 세트로 보낼지 말지를 정하는 것입니다.
각 샘플마다 식별자의 해시값을 계산하여 해시의 마지막 바이트 값이 51보다 작거나(256의 20%정도) 같은 샘플만 테스트 세트로 보낼 수 있습니다.
새로운 테스트 세트는 새 샘플의 20%를 갖게 되지만, 이전에 훈련 세트에 있던 샘플은 포함시키지 않을 것입니다.

안타깝게도 주택 데이터셋에는 식별자 컬럼이 없습니다. 대신 행의 인덱스를 ID로 사용하면 간단히 해결됩니다.
행의 인덱스를 고유 식별자로 사용할 때 새 데이터는 데이터셋의 끝에 추가되어야 하고 어떤 행도 삭제되지 않아야 합니다.
이것이 불가능할 땐 고유 식별자를 만드는 데 안전한 특성을 사용해야 합니다. (예를 들어 위도와 경도)

다음은 이를 구현한 코드입니다.

Code

from zlib import crc32
def test_set_check(identifier, test_ratio):
return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32


def split_train_test_by_id(data, test_ratio, id_column):
ids = data[id_column]
in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio))
return data.loc[~in_test_set], data.loc[in_test_set]


housing_with_id = housing.reset_index()
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")

housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")



위와 같은 복잡한 과정 없이 사이킷런에서는 데이터셋을 여러 서브셋으로 나누는 다양한 방법을 제공합니다. 
가장 간단한 함수는 train_test_split으로, 앞서 우리가 만든 split_train_test와 아주 비슷하지만 두 가지 특징이 있습니다.

첫째, 앞서 설명한 난수 초기값을 지정할 수 있는 random_state 매개변수가 있고,
둘째, 행의 개수가 같은 여러 개의 데이터셋을 넘겨 같은 인덱스를 기반으로 나눌 수 있습니다.

코드는 아래와 같습니다.

Code

from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
print(len(train_set), "train +", len(test_set), "test")

Output


split_train_test() 함수와 동일한 결과를 얻을 수 있는 것을 확인했습니다.


지금까지는 순수한 무작위 샘플링 방식을 살펴 보았습니다. 데이터셋이 충분히 크다면 일반적으로 괜찮지만, 그렇지 않다면 샘플링 편향이 생길 가능성이 큽니다.
따라서, 전체를 대표할 수 있는 샘플들을 선택하기 위해 노력해야 합니다. 미국 인구의 51.3%가 여성이고 48.7%가 남성이라면, 잘 구성된 설문조사는 샘플에서도 이 비율을 유지해야 합니다. 이를 계층적 샘플링(Stratified Sampling) 이라고 합니다.

전체 모수는 계층(Strata) 이라는 동질의 그룹으로 나뉘고, 테스트 세트가 전체 모수를 대표하도록 각 계층에서 올바른 수의 샘플을 추출합니다.
기본 무작위 샘플링을 사용하면 49%보다 적거나 54%보다 많은 여성이 테스트 세트에 들어갈 확률이 약 12%입니다. 어느 방법을 사용하든 설문조사 결과를 편향시키게 됩니다.

따라서, 계층별로 데이터셋에 충분한 샘플 수가 있어야 합니다. 그렇지 않으면 계층의 중요도를 추정하는 데 편향이 발생할 수 있습니다.
이 말은 너무 많은 계층으로 나누면 안 된다는 뜻이고 각 계층이 충분히 커야 합니다. 다음 코드는 중간 소득을 1.5로 나누고(소득의 카테고리 수 제한),
ceil 함수를 사용하여 반올림해 소득 카테고리 특성을 만들고(이산적인 카테고리를 만들기 위해), 5보다 큰 카테고리는 5로 합칩니다.

아래의 코드를 통해 확인할 수 있습니다.

Code

housing["income_cat"] = np.ceil(housing["median_income"] / 1.5)
housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True)

housing["income_cat"].hist(bins=50, figsize=(20, 15))
plt.show()

Output

소득 카테고리가 5개로 나뉜 히스토그램을 확인할 수 있습니다.



이제 소득 카테고리를 기반으로 계층 샘플링을 할 준비가 되었습니다. 사이킷런의 Stratified ShuffleSplit을 사용할 수 있습니다.

Code

from sklearn.model_selection import StratifiedShuffleSplit
split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)

for train_index, test_index in split.split(housing, housing["income_cat"]):
strat_train_set = housing.loc[train_index]
strat_test_set = housing.loc[test_index]

print(housing["income_cat"].value_counts() / len(housing))

Output

전체 주택 데이터셋에서 소득 카테고리의 비율을 확인할 수 있습니다. 



비슷한 코드로 테스트 세트에 있는 소득 카테고리의 비율을 측정합니다. 아래의 그림 Fig. 1은 전체 데이터셋과 계층 샘플링으로 만든 테스트 세트에서 소득 카테고리 비율을 비교했습니다. 그림에서 보듯 계층 샘플링을 사용해 만든 테스트 세트가 전체 데이터셋에 있는 소득 카테고리의 비율과 거의 같습니다. 반면 일반 무작위 샘플링으로 만든 테스트 세트는 비율이 많이 달라졌습니다.


< Fig. 1. 계층 샘플링과 순수한 무작위 샘플링의 편향 비교 >



References

  • 오렐리앙 제롱, '핸즈온 머신러닝', 한빛미디어, 2018


+ Recent posts