DiagrammeR – R 다이어그램 그리기

R 패키지중에 DiagrammeR라는 다이어그램(diagram)을 그릴 수 있게 해주는 것이 있습니다. 다이어그램은 플로우차트(flow chart), 간트 차트(gantt chart), 시퀀스 다이어그램 (sequence diagram)같은 것입니다.

RStudio의 DiagrammeR 스크린샷

다이어그램을 그릴 때 쓰는 도구는 Visio (Windows 쓰시는 분들) 아니면 OmniGraffle (Mac 쓰시는 분들) 아니면 PowerPoint 와 같이 손으로 그리는 것들이 있고 GraphViz 또는 Mermaid와 같이 정해진 문법을 텍스트로 입력하면 해석해서 시각적으로 표현해주는 도구가 있습니다.

DiagrammeR는 GraphViz와 Mermaid를 묶어서 연동해 놓은 패키지인데 3가지 방식으로 그래프를 그리게 해줍니다.

  1. Mermaid 문법을 텍스트로 입력한 후 텍스트를 해석해서 렌더링
  2. GraphViz 문법을 텍스트로 입력한 후 텍스트를 해석해서 렌더링
  3. R함수를 사용해서 노드와 엣지를 구성하고 렌더링

패키지를 사용하던 중 GraphViz는 원래 C로 만들어진 binary이므로 R에서 연동해서 사용할 수 있습니다만 Mermaid는 Javascript로 만들어져서 이 두가지를 어떻게 한꺼번에 연동해서 그래프를 시각적으로 표현하게 했는지 갑자기 궁금했습니다.
그래서 살펴봤더니 GraphViz.js와 Mermaid.js를 가져다 연동한 것이고 렌더링된 결과를 표현할 때는 htmlwidgets 패키지를 사용하는 것입니다.
그러니까 결국 웹브라우저를 열고 Javascript를 이용해서 렌더링하는 방식입니다.

그래서 R-GUI에서 렌더링을 하게 되면 웹브라우저가 열립니다.  RStudio에서 렌더링을 시도하면 연동이 되어서 웹브라우저가 실행되지 않고 그래프 패널에 바로 렌더링된 결과를 표현해 줍니다.  위에 넣어 놓은 스크린샷 이미지에서 확인할 수 있습니다.

앞서 말한 렌더링을 하는 세가지 방식중에 Mermaid 문법을 사용하는 것과 GraphViz 문법을 사용하는 것은  RStudio에 연동되어서 파일을 편집할 수 있게 제공하고 있기때문에 R 코드에서 DiamgrammeR 패키지를 로딩할 일이 없게 만들기도 합니다.

RStudio는 DiagrammeR를 연동해서 .mmd 확장자를 가지는 Mermaid 파일과 .dot 확장자를 가지는 Graphviz dot 파일을 바로 편집하고 코드 하일라이트도 되고 렌더링할 수 있도록 해줍니다.
아래의 링크에서 내용을 확인할 수 있습니다.

https://blog.rstudio.com/2015/05/01/rstudio-v0-99-preview-graphviz-and-diagrammer/

위의 3가지 방식 중 2가지는 앞서 말씀드린 것 처럼 RStudio와 연동으로 인해 DiagrammeR 패키지의 함수를 사용할 일이 없게 만들지만  DiagrammeR 함수를 이용한 방식의 렌더링을 사용하면 data.frame에 있는 데이터를 연동해서 그래프를 그릴 수 있습니다. 이게 가장 큰 장점이지요.

아래는 각각의 방법으로 만든 간단한 장난감 예제입니다. 

Mermaid 문법을 이용한 렌더링

Mermaid 문법을 이용한 예제입니다.

library(DiagrammeR)

DiagrammeR('
graph LR
subgraph ""
   N01[H] --- N02
   N02[E] --- N03
   N03[L] --- N04
   N04[L] --- N05
   N05[O] --- N06
   N06[!]
end
N06 --> N07
subgraph ""
   N07((M)) --- N08
   N08((E)) --- N09
   N09((R)) --- N10
   N10((M)) --- N11
   N11((A)) --- N12
   N12((I)) --- N13
   N13((D))
end

classDef box1 color:white,fill:#eee,stroke:#000,stroke-width:5px 
classDef box2 color:white,fill:#fff,stroke:#33d,stroke-width:5px

class N01,N02,N03,N04,N05,N06 box1
class N07,N08,N09,N10,N11,N12,N13 box2

linkStyle 0 stroke:#ddd,stroke-width:20px
linkStyle 1 stroke:#ddd,stroke-width:20px
linkStyle 2 stroke:#ddd,stroke-width:20px
linkStyle 3 stroke:#ddd,stroke-width:20px
linkStyle 4 stroke:#ddd,stroke-width:20px

linkStyle 6  stroke:#99d,stroke-width:20px
linkStyle 7  stroke:#99d,stroke-width:20px
linkStyle 8  stroke:#99d,stroke-width:20px
linkStyle 9  stroke:#99d,stroke-width:20px
linkStyle 10 stroke:#99d,stroke-width:20px
linkStyle 11 stroke:#99d,stroke-width:20px
')

GraphViz 문법을 이용한 렌더링

GraphViz 문법을 이용한 예제입니다.

grViz('
digraph boxes_and_circles {
  graph [overlap = true, fontsize = 12]
  rankdir="LR"

  subgraph cluster_2 {
    node [shape = circle,
      fixedsize = true,
      width = 0.9]
      color=none
      rank=same
      N07[label="G"]
      N08[label="R"]
      N09[label="A"]
      N10[label="P"]
      N11[label="H"]
      N12[label="V"]
      N13[label="I"]
      N14[label="Z"]
      N07->N08->N09->N10->N11->N12->N13->N14
      }
      
  
  subgraph cluster_1 {
    node [shape = box,
          fontname = Helvetica]
  	color=none
    rank=same
    N01[label="H"]
    N02[label="E"]
    N03[label="L"]
    N04[label="L"]
    N05[label="O"]
    N06[label="!"]
    N01->N02->N03->N04->N05->N06
  }
}
')

GrammerR의 R 함수를 이용한 렌더링

전통적인 스타일의 R 함수를 사용한 예제입니다. 아래의 함수들은 DiagrammeR 최신 패키지를 설치해야만 됩니다. 최근에 함수 이름에 변화가 있었던 모양입니다.

library(DiagrammeR)

nodes <-
  create_node_df(
    n = 7,
    type = "number")

edges <-
  create_edge_df(
    from = c(1, 1, 1, 1, 1, 1),
    to = c(2, 3, 4, 5, 6, 7),
    rel = "related")

graph <-
  create_graph(
    nodes_df = nodes,
    edges_df = edges)

render_graph(graph)

dplyr와 연동한 DiagrammeR

역시 RStudio에서 만은 부분을 기여한 패키지인 만큼 dplyr 스타일의 매우 스타일리쉬한 파이프라인 형태의 코딩도 지원합니다.

library(DiagrammeR)

example_graph <-
  create_graph() %>%
  add_pa_graph(
    n = 50,
    m = 1,
    set_seed = 23) %>%
  add_gnp_graph(
    n = 50,
    p = 1/100,
    set_seed = 23) %>%
  join_node_attrs(
    df = get_betweenness(.)) %>%
  join_node_attrs(
    df = get_degree_total(.)) %>%
  colorize_node_attrs(
    node_attr_from = total_degree,
    node_attr_to = fillcolor,
    palette = "Greens",
    alpha = 90) %>%
  rescale_node_attrs(
    node_attr_from = betweenness,
    to_lower_bound = 0.5,
    to_upper_bound = 1.0,
    node_attr_to = height) %>%
  select_nodes_by_id(
    nodes = get_articulation_points(.)) %>%
  set_node_attrs_ws(
    node_attr = peripheries,
    value = 2) %>%
  set_node_attrs_ws(
    node_attr = penwidth,
    value = 3) %>%
  clear_selection() %>%
  set_node_attr_to_display(
    attr = NULL)
#> `select_nodes_by_id()` INFO: created a new selection of 34 nodes
#> `clear_selection()` INFO: cleared an existing selection of 34 nodes
example_graph %>%
  render_graph(layout = "nicely")

추가로 네트워크 분석 관련 패키지인 igraph에서 생성한 객체를 DiagrammeR 형태로 변환하는 함수들도 제공합니다.  사용해 보지는 않았습니다. ^^;

앙상블 모델 – 배깅 Bagging

기계학습 부류. 분류(classification) 또는 예측(prediction)에서 여러 모델을 합쳐서 더 좋은 결과를 얻는 방법을 앙상블(Ensemble) 모델이라고 합니다.  앙상블 기법은 배깅(Bagging), 부스팅(Boosting), 스태킹(Stacking) 3종류로 나눌 수 있습니다. 
이 포스트에서는 우선 배깅(Bagging)에 대해서 설명합니다.

앙상블 모델을 배울때 보통 Bagging과 Boosting을 알게 되고 그 다음 Stacking을 생각하게 됩니다. 순서는 중요하지 않지만 기법들이 생각보다 많이  다르고 복잡함의 종류도 다릅니다. 그래서 한꺼번에 설명하기 어렵습니다.

앙상블 모델은 보통은 지도학습(supervised learning)에 사용됩니다. 군집화나 학습데이터가 없는 아웃라이어 감지(outlier detection), 어노멀리 감지(anomaly detection), 클러스터링(clustering) 같은 것에는 쓰기 어렵습니다.

비지도학습(unsupervised learning)에도 앙상블을 할 수 있다고 하는데 실제로 사례를 본적은 없습니다.

배깅 Bagging

배깅은 모델을 병렬로 연결해서 취합하는 방법입니다.
예를들어 결정트리(Decision Tree; 분류 나무)와 같은 알고리즘을 병렬로 연결한다고 하면 여러 개의 트리를 만들어서 결과를 취합합니다.  결합할 때는 다수결(Majority vote)를 쓸 수도있고 가중치(Weighted Majority Vote)를 쓸 수 있는데 기본으로는 다수결을 쓴다고 알려져 있습니다.

배깅의 대표적으로 알려진 알고리즘은 Leo Brieman이 만든 그 유명한 랜덤포레스트(Random Forest)가 있습니다. 이름이 랜덤포레스트인 이유도 배깅과 관련이 있기 때문입니다. 랜덤요소를 이용해 트리를 여러 개 만들고 합쳐서 숲을 만듭니다.

앙상블에서 모델을 몇 개를 결합할지는 보통 초매개변수(Hyper parameter)로 만드는 사람에 의해서 정해지게 됩니다. 결정트리를 앙상블로 결합하는 경우는 보통 100개 이상입니다.

배깅을 조금 구체적으로 설명하면 데이터로 입력값을 주면 Y 또는 N를 알려주는 트리모델을 결합해서 배깅으로 앙상블시키려고 하면 가지고 있는 학습데이터로 100개의 트리 모델을 만들고 실제로 판별에 사용할 때 입력을 100개의 트리모델에 주고 각 트리들이 Y과 N을 각각 던져 주면 그 중 많은 것을 답으로 취하는 방식입니다.  물론 이것은 아주 간단한 예이고 더 복잡하게도 변형이 가능합니다.

그런데 한뭉치의 학습데이터로 모델을 여러 개를 만든다고 했는데 어떻게 여러 개를 만드느냐가 의문입니다.
100개의 결정트리를 만들려면 학습데이터를 100등분해서 각각 만들면 되지 않을까라고 생각하겠지만 그렇게 나눌 양이 되지 않는 경우가 많고 학습 데이터가 부족해서 10묶음 교차검증(10 Fold Cross Validation)같은 것 까지 하는 판국에 학습데이터를 잘게 쪼개서 모델을 만들 여유가 없게 됩니다.
지도학습에서는 학습데이터의 양이 항상 문제입니다. 언제나 부족하다고 느껴집니다. 사회과학이나 의료같은 문제에서는 대량의 학습데이터를 얻기 어려운 경우가 많으니까요.  이미지 인식같은 종류의 자연과학 데이터로 부터 문제를 해결하는 딥러닝하고는 입장이 많이 다릅니다. 100등분을 해서 나눌 여유도 없고 그렇게 나누면 각각의 모형들이 편향이 생기거나 분산이 커질 여지가 많습니다.
그래서 학습데이터를 분할해서 모델을 각각 만든다는 것이 다소 비현실적인 경우가 많습니다. (다 그런것은 아닙니다)

적은 데이터로 모델을 여러개 만드는 방법은 배깅이라는 명칭을 풀어보면 알 수 있습니다.

배깅이라는 단어는 영어사전에서 찾을 수 있는 단어는 아니고 부트스트랩 어그리게이팅 Bootstrap AGGregING의 약어 입니다.

풀어서 보면 부트스트랩(Bootstrap)은 샘플을 다시 샘플링하는 것을 부트스트래핑(Bootstraping)이라고 하고 어그리게이팅은 그냥 취합한다는 뜻입니다.  즉 부트스트래핑 기법으로 학습데이터를 뻥튀기하는 효과로 여러개의 트리를 만드는데 사용하고 그 결과들을 취합합니다. 그것을 배깅이라고 부릅니다.

부트스트래핑은 통계학의 샘플링에서 매우 중요하게 다루는 개념 중 하나입니다. 어렵고 내용이 길어지므로 설명은 다음기회에 해보겠습니다.

부트스트래핑(뻥튀기)을 조금 더 쉽게 설명하면
10000개의 레코드로 된 데이터세트가 있다고 가정합니다.
10000개의 레코드를 10000번 복원추출(resampling)을 합니다. 그러면 갯수는 똑같이 10000개가 됩니다. 다시 이 과정을 반복해서 100번을 해서 10000개 짜리 데이터세트를 100개를 만들고 이 것으로 각 모델들을 만듭니다. 그러면 100개의 조금씩 다른 모델을 만들 수 있습니다.

“10000개에서 10000개를 표본추출(샘플링)하면 똑같은 것 아닌가?”
라고 생각할 수 있습니다.  또
“똑같은 것 100개를 만들어서 각각 모델을 만들면 다 똑같은 것 아닌가?”
라고 생각할 수 있습니다.
복원추출을 했기 때문에 안 똑같습니다.
복원추출은 영어로 리샘플(Resample)이라고 합니다. 가지고 있는 학습데이터가 모집단으로 부터 표본추출한 데이터라고 볼 수 있습니다. 즉 모집단에 대한 샘플데이터입니다.  
표본추출한 것을 데이터 갯수 만큼 복원추출을 다시 하게 되면 어떤 것은 같은 것이 중복해서 뽑히고 어떤것은 아예 뽑히지 않게 됩니다.  이것이 배깅의 효과인데 이게 무슨짓인가 싶겠지만 이렇게 표본을 다시 복원추출하면 원래 모집단의 특성을 더 잘 반영되도록 재구성되는 경향이 있다고 알려져 있습니다. (중심극한정리와 비슷해 보이지만 다른 것입니다)

이 특성을 이용해서 조금씩 다른 모델들을 만들고 그것들의 결과를 취합하는 것입니다. 
“데이터가 전부 비슷하니 결과도 별차이가 없겠네”
라고 생각할 수 있겠지만 데이터가 빼곡해지는 효과가 있고 조금씩 다른 모델들이 투표를 하는 방식이므로 배깅으로 만들어진 앙상블 모델은 결과들에 대한 편차가 크지 않고 안정적인 결과를 보여지도록 향상됩니다.
학습데이터가 원래 편향이 있다면 그로 인한 편향문제까지는 해결하지는 못하지만 미지의 데이터(Unseen data)에 상당히 괜찮은 성능을 보이고 노이즈나 아웃라이어에 대해서도 강해지는 것으로 알려져 있습니다.
실제로 단순한 트리모형과 랜덤포레스트 모델을 만들고 비교를 해보면 차이를 알 수 있겠습니다.

R 3.5.0 릴리즈 – Joy in playing

지난 2018-04-23에 R 3.5.0이 릴리즈 되었습니다.
이전 버전은 R 3.4.4입니다.
R 3.5.0의 닉네임은 “Joy in playing”이고 늘 그래왔듯이 이 닉네임도 만화 피너츠에 나오는 대사입니다.

https://www.gocomics.com/peanuts/1973/01/27

R 3.4.x에서 앞자리 숫자가 바뀌면서 R 3.5.0으로 올라가면서 이전의 버전업에  비해서 업데이트 내역이 조금 많습니다.

꽤 많아서 나열하기는 힘들고 그 중에서 체감할 수 있는 가장 중요한 업데이트는 R에 설치되는 패키지가 설치할 때 모두 bytecode로 컴파일 된다는 것입니다.

그래서 바로 버전업을 하면 예상치 못한 문제가 발생할 여지가 많아서 사용하던 패키지가 이상하게 작동하거나 RStudio가 오작동 한다거하는 문제가 있을 수 있습니다.

버전업을 조금 미루시거나 RStudio를 최신으로 빠르게 반복해서 업데이트 해 주는 것이 필요할 것 같습니다.

R팁 – 두 벡터의 모든 멤버가 동일한지 비교하기 all.equal

두 벡터가 동일한지 비교하는 간단한 팁입니다.

R은 벡터(vector)와 스칼라(scala)의 구분이 없이 사실은 모든 변수를 벡터로 취급하기 때문에 다른 언어에는 없는 몇 가지 문제가 생깁니다. 이것도 그것과 관련이 있습니다.

두 벡터, 즉 2개의 변수가 있고 변수가 모두 length가 2 이상일 때 두 벡터가 완전히 동일한지 비교할 때 아래의 코드에서 첫번째 if구문과 같은 실수를 합니다.

v1 <- c(1, 2, 3)
v2 <- c(1, 2, 4)

if (v1 == v2) {
  print("same")
} else {
  print("not same")
}

if (all.equal(v1, v2) == TRUE) {
  print("realy same")
} else {
  print("realy not same")
}

위의 예제 코드에서 첫번째 if 구문은 상식적으로 의도한 대로 작동하지 않습니다.   == 연산자가 두 변수의 첫번째 요소(first element)만을 비교하기 때문에 두 벡터가 같다고 나옵니다.

물론 다음과 같은 경고 메세지를 콘솔창에 뿌려주기 때문에 문제가 있다는 것을 알 수는 있습니다. 무심결에 경고메세지를 무시해 버리면 큰 문제가 생길 수 있습니다.

Warning message:
In if (v1 == v2) { :
  the condition has length > 1 and only the first element will be used

만약 두 벡터의 멤버가 모두 동일한지 비교하려면 처음 코드에서 두 번째 사용한 if 구문처럼 all.equal을 사용해야 합니다.

all.equal(v1, v2) == TRUE

코드를 조금 고쳐서 다음과 같은 것을 실행해 보세요.

v1 <- c(1,2,3)
v2 <- c(1,2,4)
v3 <- c(1,2,3)

if (v1 == v2 & v1 == v2) {
  print("same")
} else {
  print("not same")
}

if (all.equal(v1, v2) == TRUE & all.equal(v1, v3)) {
  print("realy same")
} else {
  print("realy not same")
}

all.equal(v1, v2) == TRUE
all.equal(v1, v3) == TRUE

사실 R을 사용해서 작업을 할 때 두 벡터가 완전히 동일한지 비교할 일이 별로 없습니다. 그래서 새까맣게 까먹고 있다고 가끔 실수를 저지를 때가 있습니다.

유클리디안 거리 – Euclidean Similarity

유클리디안 유사도라고도 하는데 원래 유클리디안 거리(Euclidean distance)라고 말하는 것이 맞는 것 같습니다. 유클리디안 유사도는 다소 이상한 단어의 조합이라는 생각이 듭니다. 하지만 유클리디안 유사도라는 말도 많이 통용되므로 이 포스트에서도 그냥 그렇게 하기로 하겠습니다.

유클리디안 유사도(Euclidean similarity)는 유클리디안 거리를 구해서 두 벡터의 유사도로 사용한다는 뜻입니다.

유클리디안 거리는 직선 거리다

유클리디안 거리는 기하학적으로 볼 때 두 점의 직선거리를 구하는 것입니다.  또는 선형대수에서 주로 다루는 벡터 스페이스(Vector space)라고 불리는 선형 공간에서도 동일하게 최단 거리를 구하는 것을 말합니다.

코사인 유사도를 설명할 때 언급한 적이 있습니다만 유사도는 2개의 데이터만 가지고 계산해서 결과값을 뽑아내도 그것만으로는 아무짝에도 쓸모가 없습니다.

세상에 사람이 둘 만 남았다면 두 사람은 서로 닮은 걸까요? 안 닮은 걸까요? 모릅니다.

유사도는 다음과 같은 방식으로 주로 사용합니다.

  1. 여러 개의 데이터에서 주어진 것과 가장 가까운 것이 어떤것인가?
  2. 여러 개의  데이터에서 가장 가까운 것들끼리 묶어보자

유클리디안 거리는 데이터마이닝이나 기계학습에 익숙하시다면 K-means (K민즈, K중심값, K평균 이라고 번역합니다) 같은 것에서 사용하는 것을 본 적이 있을 것입니다. 유사도라는 것이 사실은 거리를 측정하는 방법(distance measurement)일 수 밖에 없습니다. 거리를 측정하는 방법을 어떤 것을 쓰느냐에 따라 이름을 무슨 무슨 유사도 이렇게 “유사도”라는 단어를 붙여서 부릅니다.

유클리디안 거리 구하기

유클리디안 거리를 구하는 방법은 간단하고 매우 쉽습니다.
피타고라스 정리를 알면 됩니다.

직각삼각형의 빗변의 길이를 구하는 것입니다.

위키피디아를 보면 거기에 그림을 아래와 같이 넣어놓고 설명해 놨습니다.

간단 한 수식이 있지만 그림으로 보니 눈이 아프군요.

그림에서 p와  q의 유클리디안 거리는 p와 q의 직선거리를 구하면 되는 것이고 가운데 만들어진 삼각형이 직각삼각형이니까 피타고라스 정리를 쓰면 빗변의 길이, 즉 대각선의 길이를 구할 수 있습니다. 이 대각선의 길이가 유클리디안 거리입니다.

결론은 삼각형의 빗변의 길이를 계산하면 됩니다.

참고로 피타고라스 정리가 3차원 이사의 고차원에서도 되는 건지 헷갈릴 수 있겠습니다.  당연히 3차원 이상에서도 적용이 됩니다.  3차원, 4차원, 5차원, …, R차원 다 됩니다.

수학자들이 증명해 놓은 것이 있습니다. 그냥 믿고 쓰시면 됩니다.

5차원인 경우를 예를 들어서 설명하면
아래와 같이 2개의 5차원 벡터가 있다고 하고

a = (1, 2, 3, 4, 5)
b = (2, 3, 4, 5, 6)

벡터의 멤버수가 5개씩이므로 둘 다 5차원 벡터입니다.  차원이 다르면 안됩니다. 맞춰 줘야지요.

각각 차원(축)을 맞춰서 순서때로 빼준 다음에 제곱해서 더한 다음에 루트를 씌우면 됩니다.

1번째 차원: 1 – 2를 계산해서 제곱 = 1
2번째 차원: 2 – 3을 계산해서 제곱 = 1
3번째 차원: 3 – 4를 계산해서 제곱 = 1
4번째 차원: 4 – 5를 계산해서 제곱 = 1
5번째 차원: 5 – 6를 계산해서 제곱 = 1

다 더한 다음에 루트

sqrt(1 + 1 + 1 + 1 + 1)

답은 2.236068 입니다.

R코드로는 이렇게 하면 됩니다.

a_vector <- c(1, 2, 3, 4, 5)
b_vector <- c(2, 3, 4, 5, 6)

dist(rbind(a_vector, b_vector))

추가로 유클리디안 거리는 양적인 것을 기반으로 하는 것이라서 축의 스케일이 맞지 않으면 이상한 측정이 됩니다.  축의 스케일을 맞춰야 할지 말아야 할지는 그때 그때 다릅니다.

이런 말이 나오면 항상 골치만 아픕니다만 어쨌든 뭐든 쉽게 쓸 수 있는 것은 없는 것 같습니다.

예를 들면 이런 경우입니다.

a = (1, 2, 3000000, 4, 5)
b = (2, 3, 4000000, 5, 6)
c = (3, 4, 5000000, 6, 7)

3번째 차원, 3번째 축의 값에 의해 가장 큰 영향을 받습니다. 다른 차원의 값들은 구실을 못하게 됩니다.

기회가 되면 다른 포스트에 스케일을 맞추는 여러가지 방법도 적어 보겠습니다.