함수 번들과 이들이 참조하는 주변 상태 ( lexical environment 라 칭한다) 의 조합을 의미한다.
쉽게 말하자면, 클로저는 독립적인 (자유) 변수를 가리키는 함수로, 클로저 안에 정의된 함수는 만들어진 환경을 기억한다. 즉, 클로저를 통해 inner function scope 에서 outer function scope에 접근할 수 있으며, JavaScript 에서는 함수가 생성되는 시점에 클로저도 생성된다.
흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다. 하지만 대개는 정의한 함수를 리턴하고 사용은 바깥에서 하게된다.
이해를 돕기 위해 다음 코드를 보자.
1 2 3 4 5 6 7 8 9
functiongetClosure() { var text = 'variable 1'; returnfunction() { return text; }; }
var closure = getClosure(); console.log(closure()); // 'variable 1'
위에서 정의한 getClosure()는 함수를 반환하고, 반환된 함수는 getClosure() 내부에서 선언된 변수를 참조하고 있다. 또한 이렇게 참조된 변수는 함수 실행이 끝났다고 해서 사라지지 않았고, 여전히 제대로 된 값을 반환하고 있는 걸 알 수 있다. 여기서 반환된 함수가 클로저이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
var base = 'Hello, '; functionsayHelloTo(name) { var text = base + name; returnfunction() { console.log(text); }; }
var hello1 = sayHelloTo('승민'); var hello2 = sayHelloTo('현섭'); var hello3 = sayHelloTo('유근'); hello1(); // 'Hello, 승민' hello2(); // 'Hello, 현섭' hello3(); // 'Hello, 유근'
출력된 결과를 보면 text 변수가 동적으로 변화하고 있는 것처럼 보인다. 실제로는 text라는 변수 자체가 여러 번 생성된 것이다. 즉, hello1()과 hello2(), hello3()은 서로 다른 환경을 가지고 있다.
Lexical Scoping
Lexical Scoping 은 함수들이 중첩되어 있을 때, parser가 변수 이름을 어떻게 해석하는지에 대해 설명한다. 즉, lexical scope는 중첩 함수가 연속적으로 존재하는 그룹에서, inner function이 그의 parent scope의 변수를 포함한 자원들에 접근이 가능함을 의미한다. 결국 자식 함수들이 부모 함수가 실행되는 문맥에 어휘적으로 묶이는 것 을 의미한다. 이를 static scope 라고 칭하기도 한다.
이를 직관적으로 이해하기 위해 다음 코드를 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13
functiongrandfather() { var name = 'Hammad'; // 'likes' is not accessible here functionparent() { // 'name' is accessible here // 'likes' is not accessible here functionchild() { // Innermost level of the scope chain // 'name' is also accessible here var likes = 'Coding'; } } }
위 예시에서 lexical scope은 forward 방향으로 작동한다는 것을 알 수 있다. 이는 child 함수가 실행될 때 해당 문맥에 name 변수가 포함되어 접근이 가능하다는 것을 의미한다. 하지만 반대로, backward로는 작동하지 않기 때문에 likes는 child의 부모 함수에서 접근이 불가능하다.
이는 같은 이름을 가졌지만 다른 실행 문맥을 가진 변수들이 위에서부터 아래로 코드가 작동하면서 실행 스택 (execution stack) 에 선행 값들 (우선순위) 을 가지고 있음을 의미한다. 이 때 같은 이름을 가진 변수의 경우 안쪽에 존재하는 함수 (실행 스택에서 위쪽에 존재하는 문맥, 함수) 일수록 높은 우선순위를 가진다.
다른 예시를 보자.
1 2 3 4 5 6 7 8
functioninit() { var name = 'Mozilla'; // name is a local variable created by init functiondisplayName() { // displayName() is the inner function, a closure alert(name); // use variable declared in the parent function } displayName(); } init();
위 코드에서,
outer(parent) function 은 init() 이 되고,
inner(child) function 은 displayName() 이 된다.
init 함수가 지역변수인 name 과 displayName 함수를 생성한다. 이 때, displayName은 init 안에 정의된 inner function 이므로 init 내에서만 작동할 수 있다. 특히 해당 함수는 그만의 지역 변수를 생성하지 않는데, inner function의 경우 outer function의 변수에 접근할 수 있으므로 displayName은 그의 parent function인 init 함수의 지역변수인 name에 접근할 수 있다.
Closure
1 2 3 4 5 6 7 8 9 10
functionmakeFunc() { var name = 'Mozilla'; functiondisplayName() { alert(name); } return displayName; }
var myFunc = makeFunc(); myFunc();
위와 동일한 코드이지만, displayName이 실행되기 전 outer function에서 반환된다는 점이 다르다.
보통 함수의 실행이 끝나면 그 내부에 있는 지역변수에 더이상 접근이 불가한 것이 일반적인 경우이지만, 위 코드에서는 makeFunc의 실행이 종료되어도 name 변수에 접근이 가능하다.
그 이유는 JavaScript의 함수는 클로저를 생성하기 때문이다. 클로저는 앞서 말했듯이 함수와 해당 함수가 선언된 lexical environment을 아울러 정의하는데, 이 ‘환경’ 은 클로저가 생성된 그 시기의, in-scope에 해당하는 모든 지역변수를 포함한다.
따라서 위 코드의 경우, myFunc은 makeFunc이 실행될 때 생성된 displayName instance의 참조변수이며, displayName 인스턴스는 그의 lexical environment - name 변수가 존재하는 - 를 지속적으로 참조 (유지) 한다. 따라서, myFunc이 호출되었을 때 name 변수에 계속해서 접근 가능하므로 alert에 “Mozilla”가 전달된다.
또 다른 예시를 보자.
1 2 3 4 5 6 7 8 9 10 11
functionmakeAdder(x) { returnfunction(y) { return x + y; }; }
var add5 = makeAdder(5); var add10 = makeAdder(10);
위 코드에서는 하나의 인자 x 를 받는 makeAdder(x) 함수가 존재하고, 이는 또 다시 다른 하나의 인자 y 를 받아 x와 y의 합을 반환하는 함수를 반환한다. 결국 makeAdder는 함수 공장으로 작용하는데, 이는 특정 값(y) 을 인자(x) 에 더하는 함수를 생성한다.
위 예시에서는 2개의 함수가 생성되는데,
add5 : 하나는 5를 그의 인자에 더하는 함수이고
add10 : 다른 하나는 10을 더하는 함수이다.
add5 와 add10 은 모두 클로저이다. 이들은 같은 body를 가진 함수를 공유하지만 다른 lexical environment를 가진다. 이는 add5 의 lexical environment는 x=5이고, add10 에서는 x=10 임을 의미한다.
Closure Scope Chain
모든 클로저는
Local Scope (Own Scope)
Outer Functions Scope
Global Scope
를 가진다.
주의해야 할 부분은 outer function이 중첩된 함수 (중 하나) 일 경우에, 해당 함수의 scope에 접근하는 것은 그를 둘러싼 scope 또한 포함한다는 것 - 결국 함수 scope의 chain을 생성한다 - 이다.
// global scope var e = 10; functionsum(a){ returnfunction(b){ returnfunction(c){ // outer functions scope returnfunction(d){ // local scope return a + b + c + d + e; } } } }
console.log(sum(1)(2)(3)(4)); // log 20
// You can also write without anonymous functions:
// global scope var e = 10; functionsum(a){ returnfunctionsum2(b){ returnfunctionsum3(c){ // outer functions scope returnfunctionsum4(d){ // local scope return a + b + c + d + e; } } } }
var s = sum(1); var s1 = s(2); var s2 = s1(3); var s3 = s2(4); console.log(s3) // log 20
위 예시에서는 중첩 함수가 연속적으로 정의되어 있으며 이들은 모두 outer function scope에 접근이 가능하다. 이 때, 클로저는 모든 outer function scope에 접근 가능하다고 정의된다.
이 글은 이화여자대학교 2021-1학기 캡스톤디자인프로젝트B - 스타트7팀(이화BTS) 나정현의 기술 블로그 제출물입니다. 본 팀은 “ERP: Customizing Recommended Data Visualization Plots” 을 주제로 웹 서비스를 제작 중에 있으며, 해당 서비스는 Data2Vis의 Encoder-Decoder 모델, Seq2Seq2 모델에 기반하여 Plot 추천 시스템을 제공합니다.
이는 이름 그대로, 전문가가 일일이 차트를 만들지 않아도 제공되는 차트 리스트 중에 가장 적합한 것을 고를 수 있도록, 자동적으로 차트를 만들어 주는 추천 시스템이다.
본 팀은 큰 데이터셋과 이와 관련된 차트를 이용하여 학습시킨 모델을 통해 시각화 타입과 디자인 초이스를 추천해주는 시스템을 설계하고, 이를 웹에 올려 웹 서비스로서 Visualization 추천 시스템을 제공하는 ERP를 제작하고자 한다.
data를 활용하는 일이 급진적으로 증가하고 있고, 이에 따라 data specialist도 많아지고 있다. 이 트렌드에 맞게 굳이 플랏을 그리기까지의 모든 과정을 다 컨트롤하지 않아도 (길었지만, 흔히 노가다 라고 합니다..) 자동적으로 플랏을 추천해주는 서비스에 대한 요구도 증가하고 있다. 실제로 이에 가장 유명한 서비스는 Tableau 인데, 직접 사용해 본 결과 UI 도 복잡하고 UX도 좋지 못하다고 느꼈다. 하나의 윈도우에 너무 많은 정보가 한꺼번에 떠있는 느낌이었고, UX 플로우가 원활하지 못해 그리는 동안 엑셀에 엔터가 안 쳐지는 것과 비슷한 불편함을 느꼈다.
이를 보완하기 위해, 최근 핫하고 사용성으로 유명한 노션 의 UI와 UX를 차용하여 위 서비스를 사용자 중심 서비스로 탈바꿈하는 것이 어떨까, 생각하였다. 여기에 연구 결과(성능)가 좋아 사용하고 싶었던 Data2Vis 모델을 이용하고, 부족했다고 느꼈거나 원했던 점도 추가해 볼 예정이다.
더하여, - 노션이 가진 최대 장점이라고 생각하는 - 자유자재로 element를 배치하여 활용할 수 있는 대시보드의 편리함을 서비스의 2번째 메인 기능으로 제공하고자 한다. 사용자가 임의로 플랏을 그릴 수 있는 기능을 마지막 메인 기능으로 추가할 예정이다.
이렇게 총 3가지의 메인 기능을 가지고 있는 본 팀의 서비스는 가장 중요한 분야 중 하나인 data 의 활용을 돕고 사용성을 증대시키고자 하며, 이를 통해 전문가가 데이터를 활용함에 있어서 중요한 부분에 집중할 수 있도록 돕고자 한다.
🌝 그럼, 이제부터 전체적인 data 플로우와 웹 UI, 사용한 테크 스택, 실행 방법 및 데모 등에 대해 소개해보겠다.
Model
전체적인 서비스의 흐름도를 ~~아주, 아주!~~ 간단하게 표현해보자면 다음과 같다.
일단 사용자는 플랏으로 그리고자 하는 데이터가 배열에 담긴 .json.csv 과 같은 유형의 data 파일을 input으로 넣는다.
해당 데이터는 앞으로 설명하게 될 Encoder-Decoder과 Seq2Seq2 모델을 기반으로 한 모델을 통해 플랏으로 그려진다. 이 때 데이터는 한개의 플랏이 아닌 모델에서 제시하는 추천 플랏들 k (임의 설정) 개로 표현된다.
이를 토대로 사용자가 원하는 플랏을 선택한 후 세부적인 사항을 조정한 뒤 저장하면, 사용자의 ‘마이 리스트’에 해당 플랏이 저장되어 메인 대시보드에서 이를 이용할 수 있다. 이 때 이용이란, 그래프를 격자 형태를 가진 대시보드에 임의로 배치하고 크기 등을 조정하여 서로 연관된 플랏들을 한눈에 파악 가능하도록 하는 것을 의미한다.
본 팀이 베이스로 사용하게 될 모델은 Vega-Lite를 기반으로 한 Data2Vis 모델이다. 이에 대해 자세히 알아보자.
Data2Vis
Data2Vis를 이해하기 위해, 이와 연관된 연구이자 기본에 충실해 Data Visualization에 대한 이해도를 높일 수 있는 [VizML](https://leahincom.github.io/2021/05/19/VizML-paper-review/) 논문을 리뷰하였으며, 이후 [Data2Vis](https://leahincom.github.io/2021/05/21/Data2Vis-paper-review/) 논문을 리뷰하였다. 이와 관련한 요약본은 다음과 같다.
이 때, 모델에 학습시킬 데이터셋 마련을 위해 양질의 데이터를 크롤링하는 데 한계가 있다고 판단하여, 오픈 소스로 공개되어 있는 Vega-Lite 기반의 Data2Vis의 데이터셋을 이용하여 1차적으로 training 시켰으며, 이후 Plotly 기반의 VizML 데이터셋을 이용하여 Plotly JavaScript 라이브러리를 이용하여 프론트엔드를 구현할 예정이다.
Specification
Data2Vis의 examplesdata에서 가져온 airports.json 의 structure는 다음과 같다.
해당 데이터를 plot으로 그리기 위해 사용된 Vega-Lite grammer은 다음과 같다.
위와 같은 그래머를 training data로 학습시킨 모델이 Vega Editor를 통해 그린 plot들은 다음과 같다.
결론적으로, 본 팀이 사용하고자 하는 모델의 input 과 output , dataset 및 model 개요 는 다음과 같다.
input
model
output
dataset
1개의 dataset(.csv)
Seq2Seq + Encoder-Decoder Model
visualization recommended plots (Top-k개)
Data2Vis의 dataset
input dataset의 경우 Training : Eval : Test = 0.8 : 0.1 : 0.1 의 비율로 구성한다.
WEB
Guideline
1. 먼저, 소스를 받기 위해 `git clone` 으로 모든 파일을 local server로 가져온다.
2. 이후, 필요한 모든 npm package를 다운받아 프로젝트를 run할 준비를 마친다.
3. 아직 배포를 하지 않은 상태이므로, localhost로 실행하기 위해 client 폴더로 이동한다.
4. client를 실행시킨다.
※ 자동으로 브라우저가 실행되지 않는 경우, http://localhost:3000 를 주소창에 입력하여 프론트를 확인해 주세요.
1 2 3 4
git clone https://github.com/Ewha-BTS/ERP.git npm i cd erp-client npm start
서버의 경우 Data2Vis의 웹 데모 오픈 소스에서 webserver.py 를 본 팀의 프론트엔드에 맞게 변형하여 라우팅 및 기능을 구현하였으며, 모델의 경우 직접 학습시킨 model.ckpt-15000 을 연동시켰다.
Conclusion
본 팀은 현재 Recommendation System Model을 서버에 올려 메인 기능인 "추천 플랏 보여주기" 을 구현하였다.
이후,
남은 메인 기능 구현
그에 따른 서버 연동
모델 수정
배포
등을 수행하고, 이 과정에서 Plotly 와 같은 다양한 모델과 그에 따른 JavaScript 라이브러리를 이용하여 더 나은 서비스를 구축할 예정이다. 결론적으로 목표하는 바는 Customizing Recommended Data Visualization Plots 을 제공하는 서비스의 완성도를 높이고 직접 배포한 후 유지, 보수 관리를 꾸준히 이어나가는 것이다.
서버 응답, 캐시 데이터, 지역적으로 생성해서 사용하고 있지만 아직 서버에 저장되지 않은 데이터,
활성화된 라우트, 선택된 탭, 로딩을 보여줄지 여부, 페이지네이션 컨트롤 등 다양한 UI 상태
모델이 다른 모델을 업데이트하고, 그리고 뷰가 모델을 업데이트 할 수 있고, 이 뷰가 다시 다른 모델을 업데이트하고, 이에 따라 또 다른 뷰가 업데이트 된다.
더하여, 프론트엔트 제품 개발에 있어서 새로 갖춰야할 복잡한 요건들이 늘어나고 있다. 낙관적 업데이트(Optimistic update), 서버 렌더링, 라우트가 일어나기 전에 데이터 가져오기 등이 이에 해당한다. 이러한 복잡함은 변화(mutation) 나 비동기(asyncronicity) 와 같이 사람이 연동하여 추론하기 어려운 개념을 섞어서 사용한다 는 데서 비롯된다.
비관적(pessimistic) 업데이트
사용자 입력 -> 수정 요청 -> 성공 시 화면 갱신
사용자에게는 불편하지만, 개발자에게는 쉬움
낙관적(optimistic) 업데이트
사용자 입력 -> 바로 화면 먼저 갱신 -> 수정 요청
e.g. slack, trello
Redux
자바스크립트 앱을 위한 예측 가능한 상태 컨테이너
일관적, 서로 다른 환경(서버, 클라이언트, 네이티브)에서 작동, 테스트하기 쉬운 앱 작성을 도와준다.
범용적인 애플리케이션(universal application, 하나의 코드 베이스로 다양한 환경에서 실행 가능한 코드)을 만들기 쉽다.
서버로부터 가져온 상태 는 연결되거나(serialized) 수화되어(hydrated) 전달되며 클라이언트에서 추가적인 코딩 없이도 사용할 수 있다.
상태를 변화시키는 유일한 방법은 무슨 일이 벌어지는 지를 묘사하는 액션 객체를 전달하는 방법뿐
뷰나 네트워크 콜백 등에서 상태를 직접 바꾸지 못 하도록 보장한다.
변화는 순수 함수로 작성되어야
reducer는 이전 상태 와 액션 을 받아 다음 상태를 반환하는 순수 함수이다. 이 때, 이전 상태를 변경하는 대신 새로운 상태 객체를 생성해서 반환해야한다.
처음에는 하나의 reducer 만으로 충분하지만, 애플리케이션이 성장해나가면 상태 트리의 특정한 부분들을 조작하는 더 작은 개별적인 리듀서들로 나누는 것도 가능하다. (React 의 경우 하나의 루트 컴포넌트에서 시작해서 여러 작은 컴포넌트의 조합으로 나누는 것과 동일)
Concepts
상태
1
type State = any
상태(상태 트리)는 넓은 의미의 단어이지만, Redux API에서는 보통 저장소에 의해 관리되고 getState()에 의해 반환되는 하나의 상태값 을 지칭한다.
(넓은 의미의) 상태는 Redux 애플리케이션의 전체 상태 를 나타내며, 보통 깊게 중첩되어 있는 객체입니다.
액션
1
type Action = Object
액션은 상태를 변화시키려는 객체이다.
어떤 형태의 액션이 행해질지 표시하는 type 필드를 가져야 한다. type은 상수로 정의되고 다른 모듈에서 임포트할 수 있다.
리듀서
1
type Reducer<S, A> = (state: S, action: A) => S
리듀서(리듀싱 함수)는 누적(state)값 과 (누적될) 값 을 받아서 새로운 누적값을 반환 하는 함수이다. 이들은 값들의 컬렉션을 받아서 하나의 값으로 줄이는데 사용된다.
Redux에서 누적”값”은 상태 객체이고, 누적”될” 값은 액션이다. 리듀서는 주어진 이전 상태와 액션에서 새로운 상태를 계산한다.
/* * 이것이 (state, action) => state 형태의 순수 함수인 리듀서입니다. * 리듀서는 액션이 어떻게 상태를 다음 상태로 변경하는지 서술합니다. */ functioncounter(state = 0, action) { switch (action.type) { case'INCREMENT': return state + 1 case'DECREMENT': return state - 1 default: return state } }
// 앱의 상태를 보관하는 Redux 저장소를 만듭니다. // API로는 { subscribe, dispatch, getState }가 있습니다. let store = createStore(counter)
// subscribe()를 이용해 상태 변화에 따라 UI가 변경되게 할 수 있습니다. // 보통은 subscribe()를 직접 사용하기보다는, // 뷰 바인딩 라이브러리(예를 들어 React Redux)를 사용합니다. // 하지만 현재 상태를 localStorage에 영속적으로 저장할 때도 편리합니다. store.subscribe(() =>console.log(store.getState())))
// 내부 상태를 변경하는 유일한 방법은 액션을 보내는 것뿐입니다. // 액션은 직렬화할수도, 로깅할수도, 저장할수도 있으며 나중에 재실행할수도 있습니다. store.dispatch({ type: 'INCREMENT' }) // 1 store.dispatch({ type: 'INCREMENT' }) // 2 store.dispatch({ type: 'DECREMENT' }) // 1
상태를 바로 변경하는 대신, 액션이라 불리는 평범한 객체를 통해 일어날 변경을 명시한다.
그리고 각각의 액션이 전체 애플리케이션의 상태를 어떻게 변경할지 결정하는 특별한 함수인 리듀서를 작성합니다.
보통의 Redux 앱에는 하나의 루트 리듀서 함수를 가진 단 하나의 저장소가 있다.
Sample
action은 setter 가 없는 모델(클래스)과 같은 객체이다. 다른 코드가 임의로 이를 수정할 수 없기 때문에, 버그 발생률을 낮춘다.
위 오류 메시지는 초깃값을 설정해 놓은 매개변수 뒤에 초깃값을 설정해 놓지 않은 매개변수는 사용할 수 없다는 뜻이다. 즉 매개변수로 (name, old, man=True)는 되지만 (name, man=True, old)는 안 된다는 것이다. 초기화시키고 싶은 매개변수를 항상 뒤쪽에 놓는 것을 잊지 말자.
함수 안에서 새로 만든 매개변수는 함수 안에서만 사용하는 “함수만의 변수”이기 때문이다.
즉 함수 안에서 사용하는 매개변수는 함수 밖의 변수 이름과는 전혀 상관이 없다는 뜻이다.
첫 번째 방법은 return을 사용하는 방법이다.
여기에서도 물론 vartest 함수 안의 a 매개변수는 함수 밖의 a와는 다른 것이다.
두 번째 방법은 global 명령어를 사용하는 방법이다. ( global a )
하지만 프로그래밍을 할 때 global 명령어는 사용하지 않는 것이 좋다. 왜냐하면 함수는 독립적으로 존재하는 것이 좋기 때문이다.
lambda는 함수를 생성할 때 사용하는 예약어로 def와 동일한 역할을 한다. 보통 함수를 한줄로 간결하게 만들 때 사용한다. 우리말로는 “람다”라고 읽고 def를 사용해야 할 정도로 복잡하지 않거나 def를 사용할 수 없는 곳에 주로 쓰인다.
※ lambda 예약어로 만든 함수는 return 명령어가 없어도 결괏값을 돌려준다.
input은 입력되는 모든 것을 문자열로 취급한다.
따옴표로 둘러싸인 문자열을 연속해서 쓰면 + 연산을 한 것과 같다.
콤마(,)를 사용하면 문자열 사이에 띄어쓰기를 할 수 있다.
print() 사용 시 한 줄에 결괏값을 계속 이어서 출력하려면 매개변수 end를 사용해 끝 문자를 지정해야 한다.
파일을 쓰기 모드로 열면 해당 파일이 이미 존재할 경우 원래 있던 내용이 모두 사라지고, 해당 파일이 존재하지 않으면 새로운 파일이 생성된다.
프로그램을 종료할 때 파이썬 프로그램이 열려 있는 파일의 객체를 자동으로 닫아주기 때문이다. 하지만 close()를 사용해서 열려 있는 파일을 직접 닫아 주는 것이 좋다. 쓰기모드로 열었던 파일을 닫지 않고 다시 사용하려고 하면 오류가 발생하기 때문이다.
readline()을 사용해서 파일의 첫 번째 줄을 읽어 출력한다.
readline()은 더 이상 읽을 줄이 없을 경우 빈 문자열(‘’)을 리턴한다.
readlines()는 파일의 모든 줄을 읽어서 각각의 줄을 요소로 갖는 리스트로 돌려준다.
f.read()는 파일의 내용 전체를 문자열로 돌려준다.
추가 모드로 파일을 열었기 때문에 새파일.txt 파일이 원래 가지고 있던 내용 바로 다음부터 결괏값을 적기 시작한다.
with문을 사용하면 with 블록을 벗어나는 순간 열린 파일 객체 f가 자동으로 close되어 편리하다.
인스턴스라는 말은 특정 객체(a)가 어떤 클래스(Cookie)의 객체인지를 관계 위주로 설명할 때 사용한다.
“a는 인스턴스”보다는 “a는 객체”라는 표현이 어울리며 “a는 Cookie의 객체”보다는 “a는 Cookie의 인스턴스”라는 표현이 훨씬 잘 어울린다.
클래스 안에 구현된 함수는 다른 말로 메서드(Method)라고 부른다.
일반 함수와는 달리 메서드의 첫 번째 매개변수 self는 특별한 의미를 가진다.
메서드의 첫 번째 매개변수 self에는 setdata메서드를 호출한 객체 a가 자동으로 전달된다.
잘 사용하지는 않지만 다음과 같이 클래스를 통해 메서드를 호출하는 것도 가능하다.
1 2
>>> a = FourCal() >>> FourCal.setdata(a, 4, 2)
위와 같이 클래스 이름.메서드 형태로 호출할 때는 객체 a를 첫 번째 매개변수 self에 꼭 전달해 주어야 한다. 반면에 다음처럼 객체.메서드 형태로 호출할 때는 self를 반드시 생략해서 호출해야 한다.
1 2
>>> a = FourCal() >>> a.setdata(4, 2)
객체에 생성되는 객체만의 변수를 객체변수라고 부른다.
클래스로 만든 객체의 객체변수는 다른 객체의 객체변수에 상관없이 독립적인 값을 유지한다.
객체에 초깃값을 설정해야 할 필요가 있을 때는 setdata와 같은 메서드를 호출하여 초깃값을 설정하기보다는 생성자를 구현하는 것이 안전한 방법이다. 생성자(Constructor)란 객체가 생성될 때 자동으로 호출되는 메서드를 의미한다.
보통 상속은 기존 클래스를 변경하지 않고 기능을 추가하거나 기존 기능을 변경하려고 할 때 사용한다.
“클래스에 기능을 추가하고 싶으면 기존 클래스를 수정하면 되는데 왜 굳이 상속을 받아서 처리해야 하지?” 라는 의문이 들 수도 있다. 하지만 기존 클래스가 라이브러리 형태로 제공되거나 수정이 허용되지 않는 상황이라면 상속을 사용해야 한다.
클래스 변수는 위 예와 같이 클래스이름.클래스 변수로 사용할 수 있다.
또는 클래스로 만든 객체를 통해서도 클래스 변수를 사용할 수 있다.
클래스 변수는 클래스로 만든 모든 객체에 공유된다는 특징이 있다.
import는 현재 디렉터리에 있는 파일이나 파이썬 라이브러리가 저장된 디렉터리에 있는 모듈만 불러올 수 있다.
모듈 이름 없이 함수 이름만 쓰고 싶은 경우도 있을 것이다. 이럴 때는 “from 모듈 이름 import 모듈 함수”를 사용하면 된다.
if __name__ == "__main__"을 사용하면 C:\doit>python mod1.py처럼 직접 이 파일을 실행했을 때는 __name__ == "__main__"이 참이 되어 if문 다음 문장이 수행된다. 반대로 대화형 인터프리터나 다른 파일에서 이 모듈을 불러서 사용할 때는 __name__ == "__main__"이 거짓이 되어 if문 다음 문장이 수행되지 않는다.
파이썬의 __name__ 변수는 파이썬이 내부적으로 사용하는 특별한 변수 이름이다. 만약 C:\doit>python mod1.py처럼 직접 mod1.py 파일을 실행할 경우 mod1.py의 __name__ 변수에는 __main__ 값이 저장된다. 하지만 파이썬 셸이나 다른 파이썬 모듈에서 mod1을 import 할 경우에는 mod1.py의 __name__ 변수에는 mod1.py의 모듈 이름 값 mod1이 저장된다.
sys.path는 파이썬 라이브러리가 설치되어 있는 디렉터리를 보여 준다. 만약 파이썬 모듈이 위 디렉터리에 들어 있다면 모듈이 저장된 디렉터리로 이동할 필요 없이 바로 불러서 사용할 수 있다. 그렇다면 sys.path에 C:\doit\mymod 디렉터리를 추가하면 아무 곳에서나 불러 사용할 수 있다.
sys.path.append를 사용해서 C:/doit/mymod라는 디렉터리를 sys.path에 추가한다.
set 명령어를 사용해 PYTHONPATH 환경 변수에 mod2.py 파일이 있는 C:\doit\mymod 디렉터리를 설정한다. 그러면 디렉터리 이동이나 별도의 모듈 추가 작업 없이 mod2 모듈을 불러와서 사용할 수 있다.
파이썬 패키지는 디렉터리와 파이썬 모듈로 이루어진다.
패키지 === 폴더
모듈 === 파일
try문에 else절 사용하기
try문 수행중 오류가 발생하면 except절이 수행되고 오류가 없으면 else절이 수행된다.
abs
abs(x)는 어떤 숫자를 입력받았을 때, 그 숫자의 절댓값을 돌려주는 함수이다.
all
all(x)는 반복 가능한(iterable) 자료형 x를 입력 인수로 받으며 이 x의 요소가 모두 참이면 True, 거짓이 하나라도 있으면 False를 돌려준다.
※ 반복 가능한 자료형이란 for문으로 그 값을 출력할 수 있는 것을 의미한다. 리스트, 튜플, 문자열, 딕셔너리, 집합 등이 있다.
만약 all의 입력 인수가 빈 값인 경우에는 True를 리턴한다.
any
any(x)는 반복 가능한(iterable) 자료형 x를 입력 인수로 받으며 이 x의 요소 중 하나라도 참이 있으면 True를 돌려주고, x가 모두 거짓일 때에만 False를 돌려준다. all(x)의 반대이다.
리스트 자료형 [0, “”]의 요소 0과 “”은 모두 거짓이므로 False를 돌려준다.
만약 any의 입력 인수가 빈 값인 경우에는 False를 리턴한다.
chr
chr(i)는 유니코드(Unicode) 값을 입력받아 그 코드에 해당하는 문자를 출력하는 함수이다.
dir
dir은 객체가 자체적으로 가지고 있는 변수나 함수를 보여 준다.
divmod
divmod(a, b)는 2개의 숫자를 입력으로 받는다. 그리고 a를 b로 나눈 몫과 나머지를 튜플 형태로 돌려주는 함수이다.
enumerate
enumerate는 “열거하다”라는 뜻이다. 이 함수는 순서가 있는 자료형(리스트, 튜플, 문자열)을 입력으로 받아 인덱스 값을 포함하는 enumerate 객체를 돌려준다.
※ 보통 enumerate 함수는 다음 예제처럼 for문과 함께 자주 사용한다.
eval
eval(expression )은 실행 가능한 문자열(1+2, ‘hi’ + ‘a’ 같은 것)을 입력으로 받아 문자열을 실행한 결괏값을 돌려주는 함수이다.
filter
filter란 무엇인가를 걸러낸다는 뜻으로 filter 함수도 동일한 의미를 가진다.
filter 함수는 첫 번째 인수로 함수 이름을, 두 번째 인수로 그 함수에 차례로 들어갈 반복 가능한 자료형을 받는다. 그리고 두 번째 인수인 반복 가능한 자료형 요소가 첫 번째 인수인 함수에 입력되었을 때 반환 값이 참인 것만 묶어서(걸러 내서) 돌려준다.
hex
hex(x)는 정수 값을 입력받아 16진수(hexadecimal)로 변환하여 돌려주는 함수이다.
id
id(object)는 객체를 입력받아 객체의 고유 주소 값(레퍼런스)을 돌려주는 함수이다.
input
input([prompt])은 사용자 입력을 받는 함수이다. 매개변수로 문자열을 주면 다음 세 번째 예에서 볼 수 있듯이 그 문자열은 프롬프트가 된다
int
int(x)는 문자열 형태의 숫자나 소수점이 있는 숫자 등을 정수 형태로 돌려주는 함수로, 정수를 입력으로 받으면 그대로 돌려준다.
X.shape는 70000개의 data에 대해 각 data 당 28*28에 해당하는 pixel 강도(0~255)을 담고 있는 배열을 나타내고,
y.shape는 70000개의 분류된 class를 나타낸다.
mnist의 data를 담은 X 배열에서 하나의 이미지를 예시로 추출하여 출력해보도록 한다.
1
some_digit = X[0] # 0번째 data some_digit_image = some_digit.reshape(28, 28) # 28*28 size에 맞게 벡터 reshape
some_digit.reshape(28, 28)이 의미하는 바는, 현재 [1, 784]로 저장되어 있는 X[0]의 이미지 벡터는 [28,28]의 배열로 reshaping한다는 것이다.
1
y[0] >>> '5'
y에는 분류된 class가 담기므로, X[0]은 ‘5’ class에 속하는 data라는 것이다.
위에서 출력된 것은 ‘(작은따옴표)’로부터 알 수 있듯이 string data이므로 이를 정수로 바꾸는 코드가 다음과 같다.
1
y = y.astype(np.uint8)
1장에서 공부했듯이, 위의 MNIST은 지도 학습 모델이므로,
더 좋은 training result를 내기 위해서는 하나의 dataset에서 train dataset과 test dataset을 적절히 나누어야 한다. 따라서 해당 MNIST에서는 6:1로(전체 7만개의 dataset) train 및 test dataset을 나누었다.
또한 MNIST dataset은 데이터셋이 적절히 섞여 있어, 교차 검증 폴드(train / test set 간 분포의 균질성)가 비슷하고 훈련 샘플의 순서에 따른 성능 저하를 방지한다.
위의 2가지 변수는 “감지기” 그 자체이다. 즉, 오른쪽 조건이 True인지 False인지 판단하는 감지기의 역할을 한다.
분류 모델로 사용할 SGD(Stochastic Gradient Descent) 분류기는 손실함수 자체를 최소화하는 것이 아닌, 손실함수의 기댓값을 최소화하는 방법을 이용한다. 즉, gradient가 아닌 gradient의 기댓값의 추정치를 이용한다. 이에 사용되는 수식은 다음과 같다. 𝑤(𝑘+1)=𝑤(𝑘)+E[∇𝐿]. 모든 학습 데이터를 사용하는 것이 아닌 minibatch(일부 데이터, 하나의 훈련 샘플 그룹)를 이용하여 gradient의 추정치를 구하므로, gradient의 기댓값의 추정치는 표본 평균으로 작용한다. 따라서 계산량과 학습 데이터가 많은 deep learning과 온라인 학습(미니배치 이용)에 적합하다.
SGDClassifier(random_state=42)에서 random_state은 무작위 값을 넣으며 최적값을 찾는 SGDClassifier의 특성에 따라, 이전에 실행했던 결과값을 repeatable하게 받기 위해 특정값을 저장하는 것을 의미한다. 값의 의미는 딱히 없으므로, 아무 정수로 설정할 수 있다. 이 후 classifier에 data와 y_train_5를 넣어 classifier을 fitting한다.
classifier은 predict의 결과로 some_digit(=X[0])을 넣었을 때 y_train_5에 따라 5 여부를 감지한 결과를 array에 담는다.
train/test_index으로 skfolds 객체의 split()을 호출하여 학습용/검증용 데이터로 분할할 수 있는 인덱스를 반환받고 실제 분할된 데이터를 해당 index를 대입하여 추출한다. 이 때, split()의 대상은 X_train → y_train_5 집합이다. 이를 토대로 y 값의 prediction인 y_pred를 X_test_fold로부터 predict하고, 올바른 예측의 수를 y_pred와 y_test_fold를 비교하여 sum 값을 통해 계산한다.
위의 과정은 sklearn의 cross_val_score과 거의 같은 기능을 나타내므로 다음과 같은 코드로 정리할 수 있다.
이 때 Never5Classifier의 predict 함수는 배열의 결과를 전부 False로 세팅하여 배열을 반환한다.
해당 classifier을 cross_val_score에 적용할 시 정확도가 90% 가량 나오는 것을 확인할 수 있다. 이는 약 10%의 이미지가 숫자 5이므로 이를 제외한 90%를 정확도로 출력한다. 따라서, 불균형한 데이터셋(5임/5가 아님 = 어떤 클래스가 다른 것보다 월등히 많음)을 다룰 때는 정확도를 분류기의 성능 측정 지표로 선호하지 않는다.
3.3.2 오차 행렬
오차 행렬 : 데이터와 다른 클래스로 잘못 분류한 횟수를 담은 행렬
1 2 3
from sklearn.model_selection import cross_val_predict y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
cross_val_score() : 테스트 세트의 output의 평균을 이용해 평가 점수 반환
cross_val_predict() : 테스트 세트의 input의 각 element에 대한 깨끗한 예측 반환 (훈련하는 동안 사용되지 않은 데이터에 대해 예측)
따라서, 위의 결과에서 첫 번째 행이 “5 아님”(음성 클래스)일 때, 첫 번째 열은 True Negative(예측 N, 실제 N)을 나타내며, 두 번째 열은 False Positive(예측 P, 실제 N)를 나타낸다. 두 번째 행은 “5임”(양성 클래스)이며, 첫 번째 열은 FN(예측 N, 실제 P), 두 번째 열은 TP(예측 P, 실제 P)이다.
이에 완벽한 분류기는 TP와 TN만 가지고 있을 것이므로, 실제 confusion matrix의 출력값은 주대각선 값만 존재한다.
0.729는 정밀도로, 5로 판별된 이미지 중 72.9%가 정확하다는 의미이며, 0.756은 재현율으로, 전체 숫자 5에서 75.6%의 5만 감지한 것을 의미한다.
이를 하나의 숫자로 통합해 살펴볼 수 있는 평가 지표가 F1 점수, 즉 정밀도와 재현율의 조화 평균이다.
F1 = TP / (TP + (FN + FP) / 2)
1
f1_score(y_train_5, y_train_pred) >>> 0.742096...
정밀도와 재현율이 비슷할 시 F1의 값이 높게 나오므로 간단한 평가 지표로 이용할 수 있지만,
정밀도/재현율 트레이드오프에 따라 정밀도와 재현율이 반비례하는 경우 상황에 따라 둘의 중요성이 달라지므로 F1을 주요 평가 지표로 사용하는 것이 바람직하지 않다.
3.3.4 정밀도/재현율 트레이드오프
정밀도와 재현율은 결정 함수의 결과로 나온 output dataset에 적용하는 임곗값에 따라 달라진다.
즉, 임곗값을 높게 설정할 경우 정밀도는 높아지나 포함되는 TP가 낮아지므로 재현율은 떨어지며, 임곗값을 낮게 설정할 경우 정밀도는 떨어지나 재현율은 높아진다. some_digit의 예측에 사용한 점수를 확인하고 임곗값을 정해 양성/음성 예측을 만들기 위해 decision_function()을 이용한다.
TPR이 높아질 수록 FPR도 높아지므로, 좋은 분류기는 완전한 랜덤 분류기(TPR : FPR = 1 : 1)에서 최대한 왼쪽 위 모서리(TPR은 높고 FPR은 낮은)로 멀리 떨어져야 한다. 이를 평가하는 지표로는 곡선 아래의 면적(AUC)가 있는데, AUC를 계산하기 위해 RandomForestClassifier을 이용한다.
RandomForestClassifier에서는 decision_function 대신 predict_proba 기능을 제공한다.
그 결과로서, RandomForestClassifier는 SGDClassifier보다 왼쪽 위 모서리에 더 가까운 ROC 곡선을 생성하므로 AUC 점수로 더 높다.
3.4 다중 분류
다중 분류기로 둘 이상의 클래스를 구별할 수 있으나, 보통 이진 분류기를 이용해 다음과 같은 2가지의 방법으로 다중 클래스를 구별한다. 보통 다중 클래스 분류 작업에 이진 분류 알고리즘을 선택하면 자동적으로 OvR이나 OvO를 실행하지만, import를 통해 클래스 객체를 생성하여 강제적으로 원하는 Classifier을 설정할 수도 있다.
OvR(OvA) : 특정 클래스만 구분하는 클래스별 이진 분류기 n개를 훈련시켜 클래스가 n개의 클래스 분류 시스템을 만든다. 이후 각 분류기의 결정 점수 중에서 가장 높은 것을 클래스로 선택한다. 이는 큰 훈련 세트에서 적은 분류기를 훈련시킨다.
1
from sklearn.multiclass import OneVsRestClassifier
OvO : 각 클래스의 조합마다 이진 분류기를 훈련시킨다. 이 중 가장 많이 양성으로 분류된 클래스를 선택한다. 이는 각 분류기의 훈련에 전체 훈련 세트 중 구별할 두 클래스의 샘플만 필요하므로, 작은 훈련 세트에서 많은 분류기를 훈련시킬 수 있다.
1
from sklearn.multiclass import OneVsOneClassifier
더 높은 정확도를 위해, 입력의 스케일을 조정하면 분류기의 성능을 높일 수 있다.
1 2 3
from sklearn.preprocessing import StandartScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train.astype(float64))
3.5 에러 분석
cross_val_predict()를 이용해 결과값을 예측하고, 이 후 confusion_matrix()를 이용해 오차 행렬을 생성한다. 그 후 matshow()를 실행 시, 각 클래스 별로 예측한 클래스의 확률이 밝기로 나타나는데, 밝을 수록 높은 확률을 나타낸다. 따라서 올바른 오차 행렬은 주대각선의 밝기가 밝고, 나머지는 어두워야 한다.
이러한 오차 행렬의 특성에 따라 잘못 분류된 클래스를 파악하고 분류기의 성능 향상 방안에 대해 생각해볼 수 있다. 특히 이 과정에서 데이터의 전처리가 중요한데, 3과 5의 클래스를 분류하는 예시의 경우 SGDClassifier을 적용할 경우 미세한 픽셀 강도의 차이가 클래스 분류의 오류로 이어지므로 이미지의 회전값을 0으로 조정하는 것이 이에 해당한다.
3.6 다중 레이블 분류
다중 레이블 분류 시스템 : 하나의 샘플에 여러 개의 클래스가 포함될 경우, 레이블을 달아 각 클래스를 구별하는 시스템
다중 레이블 분류 시 적절한 지표를 사용하는 것이 중요한데, 다음과 같이 F1 점수에 다른 가중치 기준을 적용하여 분류기를 평가할 수 있다.
1 2 3
y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3) f1_score(y_multilabel, y_train_knn_pred, average = "macro") # 모든 레이블의 가중치가 같음 f1_score(y_multilabel, y_train_knn_pred, average = "weighted") # 타깃 레이블에 속한 샘플 수(지지도)에 가중치를 둠
3.7 다중 출력 분류
다중 출력 분류(다중 출력 다중 클래스 분류) 시스템 : 다중 레이블 분류에서 각 레이블이 다중 클래스가 될 수 있도록 일반화한 시스템
이를 잡음이 포함된 이미지를 예시로 설명해보자면, 잡음이 포함된 이미지의 각 픽셀은 하나의 레이블에 해당되며 모든 픽셀은 0~255의 다양한 값을 가질 수 있다. 해당 이미지에서 잡음을 제거 시 각 픽셀의 값은 변화하므로 다중 클래스가 될 수 있다. 따라서 이는 다중 레이블에 다중 클래스가 합쳐진 다중 출력 다중 클래스 분류이다.
이 때, 싱글스레드 프로그래밍 언어이기 때문에 동기적 방식에 비동기 처리가 필수적이다. 비동기 처리는 그 결과가 언제 반환 될지 알 수 없기 때문에 동기식과 같이 결과 반환을 예상할 수 있게 처리하는 기법들이 사용되어야 한다. 자바스크립트에서 이와 같은 비동기 처리의 대표적 방식은 setTimeOut , callback , promise 등이 있다.
자바스크립트는 Ajax를 호출하는 등의 기능을 수행할 때만 (자동적으로) 비동기식 언어로 작동한다. Ajax 호출에 대한 응답이 돌아와야만 (성공적이든 거절되든) Ajax 호출 실행이 중단되고 다른 (다음) 코드가 실행된다. 이 때, 콜백함수는 동기적으로 작동한다. 응답이 돌아오기 전까지, 콜백함수가 실행 중일 때는 다른 코드는 실행되지 않으며 실행 중인 다른 코드 (작동 중인 다른 프로그램) 를 방해하지는 않는다.
동기(synchronous)적 방식 : 현재 실행 중인 코드가 완료된 후 다음 코드를 실행
요청 처리가 완료된 후 다음 요청을 처리 하는 방식으로 이전 요청을 처리하는 시간이 다음 요청에 영향을 준다.
요청과 응답이 같은 시간대에 있다. (사이에 시간 간격을 두지 않음)
일반적으로 작성한 코드는 보통 동기 방식으로 처리된다.
비동기(asynchronous)적 방식 : 현재 실행 중인 코드의 완료 여부 (매개변수가 아닌 함수 실행 여부) 와 무관하게 즉시 다음 코드로 넘어가서 실행
요청과 응답이 다른 시간대에 일어날 수 있다.
setTimeout()
single-threaded
싱글스레드 언어는 한 번에 하나의 작업만 수행할 수 있다.
🤔 그렇다면 자바스크립트를 주로 사용하는 웹 사이트에서는 어떻게 한번에 여러 요청을 받을까? 그리고 여러 요청이 오갈 수 있는 자바스크립트는 왜 싱글 쓰레드일까?
정확하게 말하면 자바스크립트의 메인 쓰레드인 이벤트 루프가 싱글 쓰레드이기 때문에 자바스크립트를 싱글 쓰레드 언어라고 부른다. 하지만 이벤트 루프만 독립적으로 실행되지 않고 웹 브라우저나 NodeJS같은 멀티 쓰레드 환경에서 실행된다.즉, 자바스크립트 자체는 싱글 쓰레드가 맞지만 자바스크립트 런타임은 싱글 쓰레드가 아니다.
콜백 지옥
콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 복잡도가 커지는 현상으로, 엄청나게 많은 중괄호 중첩을 사용하여 가독성이 떨어지고 코드를 수정하기 어려움.
new 연산자와 함께 호출한, Promise 의 인자로 넘겨주는 콜백 함수 는 호출 시 바로 실행되지만(비동기적)그 내부에 resolve 또는 reject함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then또는 catch로 넘어가지 않는다(동기적). 따라서 비동기 작업이 완료될 때 resolve 또는 reject 를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능해진다.
Generator 함수를 실행하면 Iterator가 반환되는데, Iterator 는 next 메서드를 가지고 있다. 이 next 메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield 전까지 코드를 실행한다. 따라서 비동기 작업이 완료되는 시점마다 next 메서드를 호출하면 Generator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행되는 것과 같다.