대메뉴 바로가기 본문 바로가기

데이터 기술 자료

데이터 기술 자료 상세보기
제목 기계 학습의 A to Z : 줄리아(Julia)와 기계 학습(1)
등록일 조회수 7740
첨부파일  

기계 학습의 A to Z

줄리아(Julia)와 기계 학습(1)



비트겐슈타인의 ‘내 언어의 한계는 내 세계의 한계를 의미한다’는 말은 소프트웨어 엔지니어에게 한 말이었을까? 당연히 아니다. 1930년대 컴퓨터 이론을 정립한 튜링과 비트겐슈타인이 만나, 이들이 나눈 이야기는 프로그램 언어나 소프트웨어에 대한 것이 아니었을 것이다. 하지만 이 문장은 소프트웨어 엔지니어에게 잘 맞는다. 가령 C 언어 프로그램 엔지니어에게 클래스는 낯선 개념이며 C++ 언어 엔지니어에게 동적(dynamic) 언어는 같은 것일 테니까. 컴퓨팅 환경이 빠르게 변화하면서 각 분야 맞는 언어들이 나타나고 있으며 언어를 익힌다는 것은 세계를 넓힌다는 의미가 될 수 있다. 특히 빅데이터 시대에 증가하는 계산량과 멀티코어와 같은 환경에 맞는 새로운 데이터 언어에 대한 요구가 늘어가고 있다.



줄리아(Julia, julialang.org)는 MIT의 앨런 에델만(Alan Edelman) 지도 아래 응용 컴퓨팅 그룹에서 개발한 프로그래밍 언어다. 지난 2009년부터 개발해 2012년 2월 첫 버전을 공개했다. 현재 버전은 0.3.8이다. 줄리아는 계산을 중심으로 하는 매트랩(MATLAB), 알(R), 파이썬(Python)과 같은 언어가 가진 한계점을 극복하고자 개발됐다.

이 언어는 절차형(procedural), 함수형(functional), 메타프로그래밍(metaprogramming), 완전하지 않지만 객체 지향(object oriented)을 지원하는 다중 패러다임(multiple paradigms)이기 때문에 자신의 경험한 언어가 무엇이든 쉽게 접근할 수 있다. 특히, 파이썬, 자바스크립트를 다룬 경험이 있다면 바로 사용할 수 있을 정도다. 이번 글에서는 줄리아를 활용해 ‘인종 분리(racial segregation)’ 모델 알고리즘을 구현하고 시각화해 보자. 이를 위한 간략한 줄리아 문법도 다루어 보겠다.



줄리아(Julia)

줄리아 언어는 다양한 특징을 가지고 있다. 다음과 같은 몇 가지 특징을 갖고 있다.

- 오픈 소스이자 자유(MIT) 라이센스
- 빠른 속도와 계산력
- 높은 추상력과 사용하기 쉬움
- 대화형(interactive) 환경
- 다른 언어와의 접착성(glue)
- 멀티코어 환경에 맞는 병렬 기능 제공
- 관리하기 쉬운 패키지

이 언어의 놀라운 점은 뛰어난 성능이다. 그 성능의 비밀은 LLVM(Low Level Virtual Machine) 기반 just-in-time(JIT) 컴파일러에 있다. 파이썬 사용자라면, 드롭박스의 Pyston을 본 적이 있을 테다. Pyston도 LLVM과 JIT 기술을 이용해 개발중이다. Numba (http://numba.pydata.org)도 LLVM을 사용한다. LLVM에 대해서는 다음 http://www.aosabook.org/en/llvm.html을 참고하길 바란다. <그림 1>은 각 언어와 줄리아의 성능 비교(http://julialang.org/benchmarks/)다.



만델(mandel), 퀵소트(quicksort) 등 다양한 알고리즘을 구현하여 성능을 비교한 결과, 줄리아는 전체적으로 빠른 성능을 보인다. 또 다른 특성으로 사용하기 쉬운 문법의 병렬처리다. 단 몇 줄로 병렬처리를 구현할 수 있다. 나머지 특징들은 진행하면서 차차 보도록 한다.



줄리아 설치

줄리아 설치는 매우 간단하다. http://julialang.org/downloads/에서 운영체제에 맞는 버전을 내려 받아 설치한다. 설치 후 실행하면 <그림 2>와 같은 화면을 볼 수 있다.



이것으로 사용할 준비는 끝이다. 만약 설치 없이 간단히 사용해 보고자 한다면, https://www.juliabox.org/을 사용한다. 아이줄리아(IJuila), 콘솔, 파일 저장등 웹 환경에서 바로 사용해 볼 수 있다. 줄리아 대화형 인터페이스는 다양한 기능을 제공한다. 아이파이썬(IPython)에 익숙하다면 별 부담없이 사용할 수 있다. 줄리아 콘솔에서 다음 명령을 실행해보자.(<리스트 1> 참조)



<리스트 1> 콘솔 명령어 julia>; shell>ls Git LICENSE.md Uninstall.exe bin etc include julia.lnk lib libexec share julia>? help?>println Base.println(x) Print (using "print()") "x" followed by a newline. julia>OS_NAME :Windows



julia>에서 ‘;’을 입력하면 운영체제 쉘로 변환되어 운영체제 명령어를 입력할 수 있다. 예컨대, <리스트 1>에서 ls을 입력하면 현재 폴더의 파일을 보인다. julia>에서 ‘?’을 입력한 후, 궁금한 함수 이름을 넣으면 관련 정보가 나온다. 그러면, 간략한 출력을 한다.



<리스트 2> 콘솔에서 프로그래밍 julia> for word in ["Hello", " ", "world"] print(word) end Hello world julia> function say_hello() println("Hello World") end say_hello (generic function with 1 method) julia> say_hello() Hello World



콘솔에서 <리스트 2>와 같이 작성하면 그 결과를 볼 수 있다. 줄리아는 C, 자바와 같이 함수의 시작과 끝에서 {,}을 사용하지 않고, 단순히 함수가 끝나는 부분에 end를 넣는다. 외견상 파이썬과 비슷하지만 indent와 상관없다. 파일로 작성해 보자. hello.jl이라는 파일을 만들고 다음과 같은 코드를 입력한다.



<리스트 3> hello.jl 파일 for word in ["Hello", " ", "world"] print(word) end



명령어창에서 "julia hello.jl"을 입력하거나 줄리아 콘솔에서 include("/yourDirectory/hello.jl") 입력하여 결과를 확인 할 수 있다.



인종 분리 모델

줄리아의 모든 문법을 다루기 보다는 간단한 알고리즘을 구현하는데 필요한 문법을 주로 다루고자 한다. 당연히 좀 더 많은 문법이 따로 살펴볼 필요가 있다. 가볍게 주요 문법을 본 후 좀 더 찾아보도록 하자. 우선, 구현하고자 하는 ‘인종 분리’ 모델을 잠시 살펴보자. 2005년 노벨 경제학상을 수상자인 토머스 셀링(Thomas C. Schelling)은 ‘인종 분리‘ 모델을 개발하였다. 이 모델은, 자신의 이웃이 자신과 같은 인종이기 바라기 때문에 결국에는 인종별로 거주 공간이 분리된다는 모델이다, 알고리즘은 상당히 간단하며 다음과 같다.



<리스트 4> 인종 분리 모델 알고리즘 1 -모든 사람(agent)이 이사하지 않을 때까지 -모든 사람에 대해 - 이웃 10명중 6명 이하로 자신과 같은 인종이라면 -다른 지역으로 이사한다.



모든 사람은 자신과 같은 인종이 7명 이상 이웃으로 있을 때까지 계속 이사를 한다. 단순하지만 어느 정도 설득력이 있고 현실에서도 쉽게 볼 수 있는 예제이다. 여기서는 인종을 결정 조건으로 두었지만 다른 조건으로 얼마든지 변경할 수 있다. 이러한 특성은 기계 학습의 k 평균(k-means)이나 kNN(k-nearest neighbors)를 사용할 수 있는 근거가 된다. 대략적인 코드는 <리스트 5>와 같다.



<리스트 5> 인종 분리 모델 알고리즘 2 agents = Array(500) # 500명 while check_move(agent) # agent들이 더 이상 이사하지 않을 때까지 for agent in agents while is_happy(agent) # 이웃 10명중 6명 이하로 자신과 같은 인종이라면 move # 다른 지역으로 이동



대상 인구는 총 500명으로, 모든 사람은 이웃 7명 이상이 자신과 같은 인종이라면 행복감(happy)을 느끼며 그곳에 정착한다. 구현하는 방법은 다양하겠지만 다음과 같은 구조로 구현하고자 한다. agents는 agent를 500개를 가진 배열이다. 총 4-5개 함수가 필요하다. check_move() 함수는 모든 사람들에 대해 이사 여부를 확인하는 함수다. is_happy() 함수는 자기와 가장 가까운 10 이웃 중 7명 이상이 자신과 같은 인종인지를 확인하는 함수다. 마지막으로 이 알고리즘을 다루는 main() 함수가 필요하다. 사실, 함수 하나가 더 필요한데 그것은 자신과 이웃간의 거리를 구하는 함수로, 유클리드 거리 함수 euclidean()이다.



함수 정의

그럼, 줄리아로 함수를 정의해 보자. 다음은 줄리아 함수의 기본 형태다.



<리스트 6> euclidean1 함수 # 주석 #= 여러 줄 주석 =# function function_name(args) return 0 end function euclidean1(a, b) println("calling Any") if a >= b return a - b else return b - a end end



함수는 function으로 시작하여 함수이름과 매개변수를 정의하고, 함수 본체를 구현한 다음 end로 닫는다. x -> x^2와 같이 이름없는(anonymous) 함수로도 정의할 수 있다. 차후 이러한 이름없는 함수의 용도를 볼 수 있다. euclidean1(1, 2)을 호출해 보자. 1를 반환한다. 그럼, 정수형이 아닌 부동소수점형 euclidean1(1.2, 2.4)을 호출하면, 1.2가 반환된다. 입력 아규먼트의 타입에 상관없이 함수를 실행한다. 하지만, 문자열을 입력하면, euclidean1("Hello", "world")은 ERROR: `-` has no method matching -(::ASCII String, ::ASCIIString) in euclidean1 at none:3을 반환한다. 사실, 함수가 호출되면, 줄리아는 가상 메소드 테이블(virtual method table)인 vtable에서 함수의 이름과 파라메터 타입을 보고 저장된 함수가 있으면 호출한다. 이를 멀티플 디스패치(multiple dispatch)라 한다. 그러나 파라메터 타입에 해당하는 함수가 없으면 컴파일러가 최적의 함수를 실시간으로 만든다. 이를테면, euclidean1(1, 2)을 호출하면 정수형 파라메터가 없기 때문에 컴파일러는 실시간으로 최적의 함수를 만든다. euclidean1(1.2, 2.4)을 호출해도 마찬가지다. 그럼 다음과 같이 euclidean1을 추가한다.



<리스트 7> euclidean1 함수 function euclidean1(a::Int64, b::Int64) println("calling Int64") if a >= b return a - b else return b - a end end function euclidean1(a::Float64, b::Float64) println("calling Float64") if a >= b return a - b else return b - a end end



아규먼트의 a::Int64, b::Int64, a::Float64, b::Float64은 타입을 지정한 형태다. 이를 어노테이션(annotation)이라한다. 즉, a는 Int64이어야 하며, 또 다른 정의에서는 Float64이어야 한다. 그럼 함수를 호출시 위치 아규먼트의 타입을 보고 해당 함수를 호출한다. 다시 euclidean1(1, 2)와 euclidean1(1.2, 2.4)을 호출해 보자. 새로 정의한 함수를 호출한다. 사실, euclidean1(a, b)는 euclidean1(a::Any, b::Any)와 같다. 모든 타입은 Any를 상속하기 때문에 어떤 타입이라도 괜찮다는 뜻이다. 이러한 특징은 줄리아의 장점이다. 구현할 때 구체적인 타입을 알지 못할 경우, 첫 번째 함수처럼 함수를 정의하여 빠르게 개발할 수 있으며, 성능을 고려할 경우, 구체적인 타입을 명시하여 속도를 높일 수 있다. 파이썬에서 Cython의 cdef로 변수 타입을 선언하여 성능을 향상하는 것과 비슷한 개념이라 할 수 있다. 같은 역할을 하지만 <리스트 8>과 같이 간략하게 구현할 수 있다.



<리스트 8> euclidean1 함수 function euclidean1(a, b) return a >= b ? a-b; b-a end function euclidean1(a, b) return abs(a-b) end



타입 정의

줄리아에서 타입은 클래스와 비슷한 개념이지만 완전한 객체 지향 언어와는 약간 차이가 있다. C언어의 구조체라고 생각해도 괜찮을 듯하다(참고로, 멀티플 디스패치와 더불어 싱글 디스패치를 비교해 보길 바란다. http://en.wikipedia.org/wiki/Multiple_dispatch을 참고한다). 다음은 타입을 정의하는 방법으로, type으로 시작하여 타입의 이름(여기서는 Agent)을 정의한 후, 관련 데이터로 구성한 후 end로 닫는다(<리스트 9> 참조).

Agent 타입은 정수형 타입인 color, 배열 타입인 location, 불린 타입인 move로 구성된다. color는 피부색, location은 x, y좌표, move는 이사여부를 나타낸다. Agent(color::Int) = new(color, rand(1, 2), true)은 생성자이다. rand(1, 2)는 0에서 1까지 범위 값이 두 개인 배열을 반환한다. color::Int와 같이 어노테이션을 사용하여 성능을 높일 수 있지만 타입 없이 color로 정의할 수도 있다. 그러면 500개의 agent는 배열에 넣어보자. 줄리아는 리스트 컴프리헨션(list comprehension)을 지원한다.



<리스트 9> agent 타입 정의 type Agent color::Int location::Array move::Bool Agent(color::Int) = new(color, rand(1, 2), true) end



<리스트 10> agent 배열 생성 const RED = 0 const BLUE = 1 const TOTAL_AGENT = 500 const NEAR_AGENT = 10 const PASS_AGENT = 7 agents = [n>TOTAL_AGENT/2 ? Agent(RED): Agent(BLUE) for n=1:TOTAL_AGENT]



TOTAL_AGENT은 500으로 250개는 RED이며 250개는 BLUE다. agents는 배열로 10번째 agent의 color를 얻고자 한다면, agents[10].color를 사용한다. 줄리아에서 배열은 0이 아닌 1부터 시작한다. 튜플도 마찬가지다. m = (10, 20, 30)라는 m[1]은 10이다. 그러면 각 agent의 거리를 구하는 euclidean함수를 다시 구현해 보자. 유클리드 거리 공식은 다음과 같다.



이를 구현하면 <리스트 11>과 같다.



<리스트 11> euclidean 함수 function euclidean(a::Agent, b::Agent) return sqrt(sum((a.location .- b.location) .^ 2)) end



,-와 .^ 2는 각 원소단위로 연산을 적용하는 명령이다. 즉, [2, 3] .^ 2는 [4, 9]가 된다.



함수형 언어

줄리아는 다중 패러다임 언어다. 함수형 언어의 특징도 가지며 filter, map, reduce등 다양한 함수를 지원한다. 그러면 모든 사람들에 대해 이사 여부를 확인하는 check_move() 함수를 구현한다.



<리스트 12> check_move() 함수 function check_move(agents) return length(filter(x->x.move==true, agents)) == 0 ? false : true end



x->x.move == true는 이름없는 함수이다. x.move가 true인 것만 선별(fliter)한다. agent가 하나라도 이사했다면 length는 1이상으로 true가 반환된다. 이번에는 is_unhappy() 함수를 구현한다. 이웃 10명중 7명 이상이 같은 인종인지를 확인하는 함수다.



<리스트 13> is_unhappy() 함수 function is_unhappy(target, agents) dist = sort!([(euclidean(target, agent), agent) for agent in agents])[1:NEAR_AGENT] return length(filter(x->x[2].color==target.color, dist)) < PASS_AGENT ? true : false end



target과 가장 가까운 이웃 10명을 구한 후, 이웃의 color를 확인한다. sort!()함수는 (유클리드 거리, agent)로 구성된 배열에서 target와 가장 가까운 10명을 구하고 dist 배열에 넣는다. 그리고 이름없는 함수인 x->x[2].color==target.color로 7명보다 큰지를 확인한다. 모든 함수를 완성했다.



<리스트 14> main() 함수 function main() agents = [n>TOTAL_AGENT/2 ? Agent(RED): Agent(BLUE) for n=1:TOTAL_AGENT] while check_move(agents) for target in agents target.move = false while is_unhappy(target, agents) target.location = rand(1, 2) target.move = true end end end draw_agents(agents) end



지금까지 하나의 타입과 5개의 함수를 정의하여 ‘인종 분리’ 모델을 구현했다. 언어의 세부적인 부분을 다루지 않았지만 대략적으로 자신이 알고 있는 기존 언어들과 비교하면서 유사성과 차이점을 느낄 수 있었을테다. 마지막으로 이 모델을 시각화 해보자.



시각화

줄리아는 패키지 관리가 매우 편리하다. R의 CRAN처럼 바로 사용할 수 있다. 줄리아 콘솔에서 <리스트 15>와 같이 입력하자.



<리스트 15> 패키지 설치 julia>Pkg.update() julia>Pkg.add("Gadfly")



Gadfly(http://dcjones.github.io/Gadfly.jl/)는 줄리아에서 쉽게 시각화를 해주는 패키지다. Gadfly를 사용하려면 코드에 using Gadfly를 추가한다. 시각화하는 함수를 <리스트 16>와 같이 정의한다.



<리스트 16> draw_agents() 함수 function draw_agents(agents) x = [agent.location[1] for agent in agents] y = [agent.location[2] for agent in agents] c = [agent.color for agent in agents] plot(x=x, y=y, color=c) end



x는 agent의 x 좌표, y는 agent의 y좌표, c는 color다. 다음과 같은 결과를 얻을 수 있다. 중요한 점은 처음 코드를 실행하였을 때, 시간이 많이 걸릴 수도 있다.



이는 컴파일을 하고 최적의 기계 코드를 작성하는데 걸리는 시간이다. 두 번째부터는 확연히 빠르게 실행한다.





5회 반복 후 더 이상 이동은 없었다. 최종 그림을 보면 일정한 형태로 모인 것을 볼 수 있다. 이 예제는 다음 http://quant-econ.net/jl/schelling.html을 참고하였으며 다른 구현 방법도 있으니 참고하기 바란다.



결론

지금까지 ‘인종 분리’ 모델을 구현하기 위해, 줄리아를 사용했다. 관련 코드는 http://nbviewer.ipython.org/github/brenden17/Schelling-Segregation-Model/blob/master/model.ipynb에서 볼 수 있다. 이 모델을 구현하기 위한 아주 기본적인 구문과 개념을 설명하였기에, 부족한 부분이 많다. 줄리아는 데이터를 다룰 수 있는 선형대수와 같은 다양한 함수를 지원하며 성능도 우수하다. 다음번에는 줄리아를 사용하여 데이터를 다루고 기계 학습을 적용하는 방법을 살펴보겠다.



출처 : 마이크로소프트웨어 6월호

제공 : 데이터 전문가 지식포털 DBguide.net