Processing math: 100%

Tekhartha의 인공지능 기술블로그

train/dev/test data 나누기

|


딥 러닝 / 머신 러닝 모델을 만들면서 참 중요한 부분 중 하나가 훈련/테스트 데이터를 나누는 일이다. Robust한 모델을 만들기 위해서 이 부분을 정확히 알고 구현해야 한다.

데이터셋이 train set, dev set, test set으로 나누어져서 존재하면 더할 나위 없이 고마운 일이겠지만, 그런 경우보다 그렇지 않은 경우가 훨씬 많다.

이 포스팅에서는 데이터 셋이 나누어져 있지 않을 때, 10-fold validation을 어떻게 하는지에 대하여 적는다.

[Train set, Test set과 Dev set(Validation set)]

데이터는 총 train set, dev set(development set; validation set)과 test set 세 가지로 나누어 주어야 한다. test set은 모델이 잘 만들어졌는지를 평가할 때에만 쓰이고, 나머지 데이터를 train data와 dev set으로 나누어서 cross over validation을 진행한다.

cross over validation이란, dev set을 데이터의 일정 부분만큼 떼내고, 나머지 데이터로 train을 진행하고, 일정 step마다 dev set로 모델을 test하며 가장 dev set의 성능을 좋게 할 때 끊는(early stopping) 방법을 모든 데이터에 대해서 반복하는 것을 말한다.

문장으로 쓰면 이해하기 힘들기 때문에 step-by-step으로 설명한다.

[Original Data]

기계학습을 위한 데이터셋들은 대부분 x : data와 y : label 형식으로 되어 있을 것이다. x의 경우는 다루고자 하는 데이터마다 천차만별일 것이고, y의 경우는 [0,0,1,0,0,0] 등으로 one-hot encoding 되어 있는 list의 list일 것이다 (즉 이중 list일 것이다.)

자 이제, 데이터의 총 개수를 n개, 전체 중 train set의 비율을 0.1, 전체 중 dev set(validation set)의 비율을 0.1이라고 하자.

[Test set 나누기]

먼저, 전체 data의 10%만큼을 test data로 쓸 것이므로 미리 잘라낸다.

이 original data를

요렇게 잘라낸다. 이 잘라낸 test data는 나중에 쓸 것이다.

이제, 이 잘라낸 데이터에서 dev set을 또 잘라낸다.

요로코롬. dev set은 “data after test data cutting”의 10%만큼 잘라낸다.

자 이제, “Data after dev set cutting” 데이터를 가지고 모델을 훈련시킨다. 훈련시키며 여러분이 설정한 특정 step 마다 해당 모델로 dev set을 테스트해 본다.

dev set의 accuracy가 최대치를 찍고 다시 내려가려는 순간, 학습을 종료시킨다. 이를 Early stopping 이라고 한다. 이는 모델의 성능을 최대화하기 위한 weight tuning 기법이다.

(“Early stopping을 정확히 언제 해야 하느냐” : “어느 시점부터가 Overfitting이 되는 순간이냐” 의 문제는 그리 쉬운 문제가 아니다. 본 포스팅에서는 dev set의 accuracy가 다시 내려가기 시작할 때라고 적었지만, 다른 곳에서는 train loss와 dev loss를 비교하란 말도 있고, 사람마다 조금씩 다른 것 같다. 일단 가시적인 성과가 가장 눈에 띄는 accuracy로 먼저 진행해 봅시다)

이제 이 모델로 test data를 test해 본다. 그리고 그 때의 모델의 성능을 기록한다.

이제 dev set을 “data after test data cutting”의 다른 10%만큼 잘라낸다. 그리고 나머지를 모델의 훈련에 사용한다. 그리고 dev set의 성능이 최대가 될 때 stop하고 test data로 모델의 성능을 기록한다.

우리는 dev ratio를 0.1로 설정했으므로 이 과정을 10번 반복하면 된다. 그러면 dev set에는 모든 “Data after test data cutting”이 한 번씩 들어갈 것이다.

이러한 방법을 Cross-validation이라 한다. 이 경우는 10번 반복하므로 10-fold Cross-Validation이라고 부른다.

10번을 다 했으면? 그 때는 우리가 test data로 기록해 놓은 10개의 모델의 성능을 평균낸다, 이 값이 진짜 모델의 성능이 되는 것이다.

Anytree(파이썬 트리 라이브리리)

|


작업을 하다 보면 트리 형식으로 구현해야 할 때가 꽤 생긴다. C라면 한땀한땀 구조체로 구현해서 사용해야겠지만, 파이썬 같은 high-level의 언어는 트리 라이브러리를 지원한다. 이번 포스팅에서는 파이썬의 꽤 강력한 트리 라이브러리인 “Anytree”를 다룬다. 이 라이브러리는 트리의 탐색, 노드의 추가/삭제 등을 간편하게 구현할 수 있게 만들어 놓았다. 뒤에 보면 알겠지만 메소드들이 매우 직관적이고 쓰기 쉽게 만들어 놓았다. 게다가 트리를 시각화해서 출력하는 기능까지 제공한다.


[Installation]

일단, 공식 documentation은 여기를 들어가서 볼 수 있다.

간단히 pip install anytree 또는 conda install anytree로 설치할 수 있다.

[Library Import]

from anytree import Node, RenderTree
from anytree.exporter import DotExporter

위의 코드로 anytree를 import해온다.

[Making Nodes]

이제부터는 Node()라는 함수를 통해서 트리의 노드를 정의내릴 수 있다. documentation에 있는 코드를 기준으로 설명하면,

udo = Node("Udo")
marc = Node("Marc", parent=udo)
lian = Node("Lian", parent=marc)
dan = Node("Dan", parent=udo)
jet = Node("Jet", parent=dan)
jan = Node("Jan", parent=dan)
joe = Node("Joe", parent=dan)

위와 같이 7개의 노드로 구성되고 root가 udo인 트리를 만들었다.

Node는 위와 같은 형식으로 만든다. 먼저 쌍따옴표로 Node의 name을 정하고(이는 나중에 udo.name으로 접근할 수 있다) 그 뒤에는 parent를 어떤 노드로 할 것인지 정한다. 이렇게 설정하면 자동적으로 marc의 부모 노드는 udo, udo의 자식 노드는 marc라고 인식하게 된다. 그러니까 노드를 잇고 끊을 때는 parent만 신경써주면 된다는 뜻이다.

한 가지 주의할 점. Node들을 연결할 때 해당 노드를 가리키는 변수로 연결해야 한다. 그러니까 udo에 연결해야 하지, Node(“Udo”) 에 연결하면 안 된다는 거다. 모든 노드는 변수처럼 선언되어야 사용할 수 있다.

Node에는 parent라는 argument 말고도 사용자가 직접 argument를 추가할 수도 있다.

data = Node("Data", data_index = 4, data_spec = "Key", mat = [1,2,3])

이런 식으로 내 마음대로 Node에다가 데이터를 집어넣을 수 있다. 데이터의 형식은 아무래도 상관없다.

다만, 모든 트리의 argument들은 다 똑같아야 한다. 위처럼 Node를 또 만들 때 mat이 없는 Node라면 mat = [] 또는 mat = None 으로 설정해줘야 한다는 말이다.

[Visualization]

for pre, fill, node in RenderTree(udo):
    print("%s%s" % (pre, node.name))

위와 같은 코드를 사용하면 udo를 root로 하는 트리를 시각화해서 볼 수 있다.

Udo
├── Marc
│   └── Lian
└── Dan
    ├── Jet
    ├── Jan
    └── Joe

이런 식으로 아름답게 시각화되어 출력된다. 직접 실험해본 결과 엥간히 큰 규모의 트리도 txt 파일에 저장시키면 다 시각화시켜서 볼 수 있다. 사실상 이 라이브러리의 가장 강력한 기능이 아닐까 한다.

[Methods of Node]

Node를 선언하고 .을 덧붙이면 메소드들을 볼 수 있는데, 메소드들은 다음과 같다 :

  • ancestors : 해당 노드의 조상 노드들을 모두 반환한다(tuple). 바로 위만 반환하는 게 아니라 그 위, 그 위…root까지 모두 반환한다.
  • anchestors : ancestors와 같음. 사용하지 말 것.
  • children : 해당 노드의 자식 노드들을 모두 반환한다(tuple). 자신에게 직접 연결된 자식 노드만.
  • depth : 해당 노드의 깊이를 반환한다.(int) root Node는 0을 반환
  • descendants : 해당 노드의 자손 노드들을 모두 반환한다(tuple). 자식의 자식의 자식…까지 전부 반환
  • height : 해당 노드의 높이를 반환한다.(int) leaf Node는 0을 반환
  • is_leaf : 해당 노드가 leaf인지 아닌지를 반환한다(bool).
  • is_root : 해당 노드가 root인지 아닌지를 반환한다(bool).
  • name : 해당 노드의 이름을 반환한다(str). Node 정의할 때 큰따옴표 안에 있던 그것.
  • parent : 해당 노드의 부모 노드를 반환한다.(Node)
  • path : root부터 해당 노드까지의 path를 반환한다.(tuple)
  • root : 해당 노드가 속한 트리의 root Node를 반환한다.(Node)
  • separator : 해당 노드의 구분자를 출력한다. 별로 쓸 일은 없다.
  • siblings : 해당 노드와 같은 부모를 가진 노드들을 출력한다(tuple)

사실상 노드에서 알아내야 하는 모든 정보들을 메소드들로부터 알 수 있다. 매우 파워풀.

[Attatch / Detach]

트리에 노드를 추가 / 삭제하는 방법 또한 매우 간단하다.

  • 추가할 땐 노드를 새로 정의하면서 parent argument에 parent로 삼고 싶은 노드를 써 주면 된다. 이러면 알아서 부모 노드는 자식을 알아본다.
  • 삭제할 땐 해당 노드의 parent를 None으로 설정하면 끝난다. 예를 들어 data라는 Node를 트리에서 제거하고 싶으면 data.parent = None 이라는 코드 한 줄로 끝난다. 만약 이 노드가 leaf 노드가 아니라면, data 노드를 root로 하는 새로운 트리가 만들어진 것이다. 메모리 문제 때문에 트리에서 떼낸 Node를 제거하고 싶다면 del data 코드를 추가해주자.


이 정도면 자유롭게 라이브러리를 통해 tree를 사용할 수 있을 것이다.

실제 사용할 때는 Node들을 모아 놓은 list를 하나 만들거나 해서 함께 관리해 주는 게 좀더 효율적이다.

기초적 데이터 분석(Basic Data Analysis)

|


우리는 일을 하면서, 공부를 하면서, 연구를 하면서 수많은 데이터들과 맞부딪히게 된다. 이 때, 이 데이터의 특성이 무엇인지 미리 정보를 알고 있는 경우도 있지만 많은 경우에는 데이터가 어떻게 생겼는지 알아내기가 쉽지 않다. 어떻게 생겼는지도 모르는데, 그로부터 의미 있는 결과를 도출해 내기는 더더욱 어려울 것이다.

이번 포스팅에서는 미지의 데이터를 마주쳤을 때 어디서부터 분석을 시작해 나가야 하는지를 다룬다.


[자료의 종류]

자료(Data)는 크게 두 가지 종류로 나누어 볼 수 있다. 질적 자료(Qualitiative Data)는 범주형 자료(Categorical Data)라고도 하며 수치화하거나 서열을 매길 수 없는 자료로, 전화번호, 혈액형, 주소 등등을 들 수 있다. 양적 자료(Quantitive Data)는 수치적 자료(Numerical Data)라고도 하는데, 수치화하여 나타낼 수 있는 자료로 성적, 키, 몸무게, 나이 등등을 들 수 있다.세상에 존재하는 모든 데이터는 이렇게 ‘수치화할 수 있는가?’의 기준으로 두 가지로 나누어 볼 수 있다.

특정 데이터 셋을 받았는데 그 데이터 셋이 질적 자료로만 이루어져 있을 때는 빈도표, 백분율, 막대그래프, 원그래프 등을 사용하여 어떤 자료가 해당 자료에서 얼마만큼의 비중을 차지하는 지 알아볼 수 있다.

양적 자료로만 이루어진 자료는 해당 자료를 구간화하는 새로운 변수를 만들어서 그 새로운 변수에 해당하는 데이터들을 대상으로 빈도 및 백분율을 구한다. 가장 익숙한 것이 중학교 과정에서 배우는 도수분포표와 히스토그램이다. 또한 상자 그림(Boxplot) 등으로도 자료를 표현할 수 있다.

[기술통계량(Descriptive Statistics)]

기술통계량은 요약통계량(Summary Statistics)이라고도 하며 자료로부터 추출해 내서 자료의 특성을 파악해 볼 수 있는 통계량들을 말한다.

a b c d e
2 4 6 8 10

위와 같이 n=5인 데이터가 있다고 가정해 보자.

[1. 대푯값]

해당 자료를 대표할 수 있는 값들을 대푯값이라고 칭하는데,

  • 평균(기댓값, Mean) : (a+b+c+d+e)/n=6 …가장 일반적으로 쓰는 대표값

  • 절사평균(Trimmed Mean) : 전체 자료 중에서 상위 m%만 제외한 자료를 가지고 평균을 내는 방법이다. 보통 이상치 데이터가 섞여 있을 때 많이 활용한다. 위 자료에서 20% 절사평균을 낼 때, 5개 * 20% = 1개의 자료를 제외한다.

  • 중위수(Median) : 전체 데이터를 오름/내림차순으로 정렬하였을 때 한가운데에 있는 자료를 말한다. 위 자료에서는 c가 되겠고, 전체 데이터 개수가 짝수일 때는 가운데 2개의 자료가 중위수가 된다.
  • 최빈수(Mode) : 전체 데이터에서 가장 많은 비율을 차지하는 데이터를 말한다.

[2. 퍼짐(산포)]

데이터들이 얼마나 서로 멀리 떨어져 있는지를 나타내는 통계량들이다.

  • 범위(Range) : 해당 자료에서 최대값과 최소값의 차이값을 의미한다. 위 자료에서는 10-2 = 8.
  • 사분위범위(IQR : Inner Quartile Range) : 제1사분위수(Q1) ~ 제3사분위수(Q3)까지 있는데, Q1은 데이터의 25%가 해당 값보다 작거나 같을 때의 값을 나타내고, Q2는 50%, Q3는 75%를 의미한다.
  • 분산(Variance) : 매우 유명한 통계량. 데이터가 기댓값으로부터 얼마나 멀리 떨어져 있는지를 나타내는 수. 많이들 아는 식인 V(X)=E((Xμ)2)=E(X2)E(X)2 로 계산한다. 위 자료의 경우 V(X) = 8이 된다.
  • 표준편차(Standard Deviation) : 역시 분산과 함께 많이 쓰이는 통계량. 분산의 제곱근을 씌우면 표준편차가 된다.
  • 중위수절대편차(MAD : Median Absolute Deviation) : 이상치에 영향을 좀 덜 받기 위해서 만들어진 통계량인데, 중위수로부터 각 데이터가 얼마나 떨어져 있는지를 표현한 값이다. 표본 데이터에서 중위수를 구하고, 각 데이터에서 중위수를 빼고, 그 결과를 절대값으로 바꾸고, 그 결과에서 중위수를 구한다. 위 자료는 평균과 마찬가지로 6이 나온다.

[3. 분포의 모양]

  • 왜도(비대칭도, Skewness) : 데이터의 분포가 얼마나 한쪽으로 치우쳐 있는지를 나타낸다. 왜도가 양수이면 자료가 왼쪽에 더 많고 오른쪽의 꼬리가 길며, 음수이면 자료가 오른쪽에 더 많고 왼쪽의 꼬리가 길다. 계산하는 방법은 모멘트와 적률을 알아야 해서 좀 어렵다. 궁금하면 wiki를 참조하자.
  • 첨도(Kurtosis) : 데이터의 분포가 얼마나 뾰족한지를 나타내는 지표이다. 첨도가 3에 가까울수록 정규분포의 모양과 비슷해지며, 3보다 작으면 정규분포보다 더 납작하고 완만한 분포, 3보다 크면 정규분포보다 더 뾰족한 분포로 생각할 수 있다. 역시 구하는 공식은 wiki를 참고하자.

[4. 기타]

  • 최댓값(Maximum) : 설명이 필요한가? 해당 자료들 중 가장 큰 값.
  • 최솟값(Minimum) : 역시 설명이 필요한가? 해당 자료들 중 가장 작은 값.


자료를 파악하는 데에 위와 같은 통계량들이 주로 많이 쓰인다. 유의할 점은 한두 개의 지표만 보고서 ‘아 이 자료는 이렇구나’ 하고 속단해 버리면 안 된다는 거다. 세상에 존재하는 데이터는 백이면 백 다 다르기 때문에 위에 열거한 통계량들뿐 아니라 해당 자료가 생성된 방법, 결측치의 존재 유무 등 파악해야 되는 것들이 훨씬 많다. 정성적+정량적 방법을 통해서 데이터를 복합적, 입체적으로 이해해야 올바른 분석이 가능하다. 위의 통계량들은 깜깜한 어둠 속에서 벽이 어디에 있는지를 보여주는 작은 등불 정도로 생각하고 사용하자.

희소행렬(Sparse Matrix)

|


희소행렬(Sparse Matrix)이란, 0이나 Null Data가 대부분이고 의미있는 데이터는 몇 없는 자료를 핸들링하기 위해 고안된 자료구조이다.

보통 NLP나 웹 관련 작업 중 각각의 문서별로 Word-Embedding을 해야 할 때, 기본이 되는 input 모양새는 전체 코퍼스의 단어 갯수만큼의 0이 있는 벡터를 만들고 그 중 해당 문서에서 나온 단어들에 해당하는 열만 1을 가지는 one-hot encoding을 사용한다.

matrix = [[0,0,0,...1,0,0,1,...0],
          [0,1,0,...0,0,1,0,...0],
                    ...
          [0,1,0,...0,1,0,1,...0]]

(행의 개수 : 문서의 개수, 열의 개수 : 코퍼스의 전체 단어 개수)

요런 식으로. 그런데 이렇게 만들면 matrix 안의 0의 개수가 너무 많고 의미있는 1의 개수가 굉장히 적다. 이러한 데이터의 형태를 Sparse(희소)하다고 말하는데, 이를 해결하기 위해 Sparse Matrix 구조를 사용한다.

Sparse Matrix 구조를 다루는 방법은 여러 가지가 있다.(사실 만들기 나름이다)

파이썬 기준으로, 처음에는 딕셔너리를 사용했다.

matrix = {(0,1):1, (2,2):3, (2,5):2, ...}

key값의 튜플은 (행,열)을 나타내고 value값은 그 위치에 저장되어 있는 숫자를 나타낸다.

그런데 이렇게 만들면 단순한 행 하나만 뽑아내는 데에도 전체 딕셔너리를 순회해야 하기 때문에 연산 시간에서 매우 손해를 본다. 내적 등은 말할 것도 없고…

그래서 리스트와 살짝 혼합해 봤다.

matrix = [{0:3, 1:4, 6:2, ...}, {2:3, 3:1, 10:2, ...}, ...{5:3, 6:2, ...}]

여기서 각 딕셔너리는 각 행을 나타내고, 각 딕셔너리의 key는 열값, value는 그곳에 들어 있는 숫자를 나타낸다.

이렇게 만들면 해당하는 행을 순회 없이 바로 접근할 수 있어서 연산 시간이 많이 단축된다.


파이썬 같은 경우는 scipy에서 희소행렬 연산을 지원하는 라이브러리를 제공한다. 들어가 보면 희소행렬을 표현하는 방법이 꽤 많음을 알 수 있다. 익숙해지면 이것도 쓸만할 것 같지만 적용해 본 사람들의 경험담에 따르면 속도가 빠르진 않다고(…)

구현도 어렵지 않은데 쓸 일이 있을 때마다 함수나 클래스로 정의해서 쓰면 될 것 같다.

F1 Score & Confusion Matrix

|


머신 러닝으로 특정 주제를 분류하는 모형 공부를 하다 보면 (특히 논문을 보다 보면) 모델이 정답을 얼마나 잘 맞추는지에 대한 검증 방식이 필요한데, 그 때 사용되는 검증 방식 중의 하나가 F1 Score이다.

왜 F1 이라는 이름이 붙었는지는 wiki를 참조하면 되겠다. (다른 F함수의 이름을 차용했다는거 같음)

F1 Score를 이해하기 위해서는 혼동행렬부터 알아야 한다.

[혼동행렬(Confusion Matrix)]

특정 모델로 참/거짓의 이진 분류를 한다고 가정했을 시, 모델의 분류 결과값들은 다음과 같은 표에 넣을 수 있다 :

  실제 T 실제 F
T로 예측 a b
F로 예측 c d

이렇게 생긴 표를 혼동행렬이라고 부른다. 이 예시는 가장 간단한 이진 분류를 기준으로 작성되어서 2X2의 작은 크기를 가지고 있지만, 분류 결과가 여러 가지일 경우는 행렬의 크기도 그만큼 늘어난다. 기본적으로 혼동행렬은 행과 열의 개수가 같다 (그럴 수밖에 없다).

이 행렬에서 a는 실제 T인 결과를 모델이 T로 정확히 예측한 것이고 b는 실제는 F인데 T로 예측한 결과, c는 실제 T인데 F로 예측한 결과, d는 실제 F인데 F로 예측한 결과를 의미한다.

[정확도(Accuracy)]

정확도는 전체에서 예측을 정확히 한 데이터의 비율을 나타낸다.

Accuracy=(a+d)/(a+b+c+d)

[정밀도(Precision)]

정밀도는 T로 예측한 것들 중 실제 T인 데이터의 비율을 나타낸다. T로 예측한 것이 얼마나 정확하게 맞았는지를 나타낸다. Positive Predictive Value라고도 한다.

Precision=a/(a+b)

[Negative Predictive Value]

이 값은 정밀도의 반대인데, 한국어로 정확한 번역이 없는 것 같다. (정확히는 업계별로 부르는 명칭이 다른 듯 하다)

F로 예측한 것들 중 실제 F인 데이터의 비율을 나타낸다. F로 예측한 것이 얼마나 정확하게 맞았는지를 나타낸다.

NegativePredictiveValue=d/(c+d)

[민감도, 재현율(Sensitivity, Recall, True Positive Rate)]

중요한 수치라서 그런지 부르는 말이 많다. 민감도와 재현율 둘 다 자주 쓰는 말이라 둘 다 알아두는 것이 좋다.

실제 T인 데이터들 중 T로 예측한 데이터의 비율을 나타낸다.

Sensitivity(Recall)=a/(a+c)

[특이도(Specificity)]

민감도의 반대 개념이다. 실제 F인 데이터들 중 F로 예측한 데이터의 비율을 나타낸다.

Specificity=d/(b+d)

모델을 평가할 때는 위 지표들 중 하나만 높다고 해서 좋은 모델이 아니고, 여러 가지 지표들의 수치를 종합해서 살펴야 한다. 학계/업계에 따라, task에 따라 중요하게 여겨지는 지표가 다르므로 각 상황에 맞게 지표를 활용할 것.


[F1 Score]

위 지표들을 하나의 숫자로 표현해서 모델을 평가할 수 없을까? 라는 생각에서 나온 지표가 F-Score이다. F-Score는 Precision과 Recall의 조화평균인데, 그냥 평균을 내면 값의 왜곡이 생길 수 있기 때문에 가중치 지표 β를 추가해 사용한다.

Fβ=(1+β2)(Precision×Recall)(β2×Precision+Recall)

여기에서 β가 1일 경우를 F1 Score라고 한다.

F1=2×(Precision×Recall)(Precision+Recall)

많은 논문에서 모델의 성능을 측정하기 위해 사용되는 지표이다.



F1-Score를 구하기 위해 Precision과 Recall을 구해야 할 때 다음과 같은 문제가 발생할 수 있다.

Precision과 Recall의 식은 다음과 같은데,

Precision=a/(a+b)

Sensitivity(Recall)=a/(a+c)

이 때 분모가 0이 되는 경우가 생길 수가 있다. 그래서 종종 프로그래밍을 하다 보면 Zero-Divion Error가 날 때가 있다. 이는 무슨 의미일까?

  • Precision이 0이 되는 경우

    Precision의 분모인 a+b가 0이 되는 경우는 모든 예측값이 negative로 예측될 때 발생한다.

  • Recall이 0이 되는 경우

    Recall의 분모인 a+c가 0이 되는 경우는 input data 자체에 positive한 값이 없을 때 발생한다. 데이터가 완전히 한쪽에만 있다는 얘기이다.

두 경우 다 이진 분류에서는 거의 일어날 수 없는 일이다. 하지만 클래스가 여러 개일 때(Multi-Class Classify)에는 특정 class의 Precision의 값이 0이 되는 경우가 왕왕 일어난다. 이 때는 모델의 성능을 조금 더 높여서 그런 class가 없게 만들던지, 해당 클래스를 분석에서 제외하는 등의 방법을 생각해볼 수 있다.