Multi Armed Bandit 알고리즘?

Multi Armed Bandit 알고리즘?

이 글의 주 소스 링크를 먼저 밝힙니다. 원작자에게 먼저 허락을 구하고 글을 작성했습니다.
https://www.kaggle.com/ruslankl/how-to-deal-with-multi-armed-bandit-problem

Multi Armed Bandit(MAB) 란?

마케팅이든 아니면 의학적인 실험에서든 사용자에게 어떤 게 가장 좋은 것 인지 확인하는 방법은 무엇일까요?
바로 Multi Armed Bandit Algorithm입니다. 특히 Thompson Sampling이라는 기법과 같이 사용된다면 굉장히 효과적으로 가장 좋은 선택이 무엇인지 알아낼 수 있습니다.(실제로 추천 알고리즘의 Cold Start 문제 등에 효과적으로 적용되고 있는 알고리즘 중 하나입니다.)

마케팅 캠페인을 한다고 합시다. 마케팅 캠페인에서는 보통 CTR(Click Through Rate)을 이용해서 광고가 효과적인지 판단하곤 합니다.(물론 마케팅 회사마다 케이스 바이 케이스이긴 합니다만, 일단 CTR이라고 가정하고 넘어가 봅시다)

  • CTR 예시, 어떤 광고가 100번 노출되고 유저가 10번 클릭을 한다면, 이 광고의 CTR은 10/100으로 0.1입니다.

이야기가 나온김에 Regret도 같이 설명하자면, Regret은 가능한 CTR중 최고의 CTR과 지금 있는 CTR을 빼준 값입니다. 광고 A의 CTR이 0.1이고 B가 0.3이라고 할 때, A를 보여줬을 때 Regret은 $0.3 - 0.1 = 0.2$가 됩니다.

이제 광고에 대한 여러 안들이 있고, 어떤 광고가 가장 효과적인지 확인하려고 합니다. 하지만 광고에 대해서 어떤 사전 정보도 없다면 어떨까요?, 어떻게 여러 대안중에 효과적인 광고를 골라낼 수 있을까요? 이럴 때는 보통 사용하는 방법이 A/B test입니다. A/B 테스트는 말 그대로 A안과 B안을 노출시켜서(노출 비율은 정할 수 있다) 두 집단의 각각 다른 효과를 확인하기 위해서 사용되는 방법입니다. (wiki 설명 : A/B 테스트는 변수 A에 비해 대상이 변수 B에 대해 보이는 응답을 테스트하고, 두 변수 중 어떤 것이 더 효과적인지를 판단함으로써 단일 변수에 대한 두 가지 버전을 비교하는 방법이다, https://ko.wikipedia.org/wiki/A/B_%ED%85%8C%EC%8A%A4%ED%8A%B8)

‘아 그럼 A/B 테스트 하고 좋은 거 그냥 뽑으면 되겠네!’라고 생각할 수 있겠지만, 회사에서 이 테스팅을 진행한다고 생각해 봅시다. 주의할 점이 있습니다. A안을 기존에 하던 광고라고 하고 B를 실험하는 광고라고 해봅시다. A안 광고를 통해서는 꾸준히 매출을 기록하고 있고, B안은 아직 확실하지 않습니다. B가 아마 효과적이라고 하는데 아직 의심스럽습니다. 만약 테스팅을 하는데 B의 효과가 너무 떨어진다면 어떨까요?

  • 기존 광고 효과의 목표치에 도달하지 못한다.
  • 매출이 떨어진다.
  • 고객이 실망하고 이탈한다.

이런 상황이 가능하지 않을까요? 그래서 MAB에서 중요한 것은, ExplorationExploitaion입니다. 한국어로 쉽게 말하면, 탐색하기와 뽑아먹기 입니다. 쉽게 탐색과 이용이라고 하겠습니다.

Exploration은 탐색하는 것입니다. 새로운 안에 대해서 계속 테스트하고 실험해 보는 것입니다.

Exploitation은 이용하는 것입니다. 즉, 기존에 효과적이었던 광고를 계속 하는 것입니다.

결국 A/B테스트이든, MAB이든 중요한 것은, 이 비율을 적절하게 맞춰서 탐색을 간결하게 하고 최대한 효과적으로 이용할 수 있는 대안을 선정하는 것입니다.

MAB, 즉 Multi Armed Bandit 알고리즘은 여러 대안들(슬롯머신의 Arm에서 이름을 따왔습니다)을 자동으로 실험하고 최적의 광고를 탐색과 이용사이에서 균형을 잡으면서 빠르게 찾는데 좋은 알고리즘입니다. Multi Armed Bandit 알고리즘들은 몇 가지 종류가 있습니다만 거의 모든 알고리즘은 위에서 소개한 Regret을 줄이는 것을 목표로 하고 있습니다.

주요 알고리즘들은 다음과 같습니다.

  • Random Selection
  • Epsilon Greedy
  • Thompson Sampling
  • Upper Confidence Bound (UCB1)

이 알고리즘들을 가지고 실험을 하기 전에 CTR을 사전에 설정해 둘 필요가 있습니다. 설정해둔 CTR로 광고가 주어졌을 때 클릭에 대한 시뮬레이션을 진행할 수 있습니다.

먼저 CTR을 비현실적이지만 0.45와 0.65로 설정하겠습니다.

1
ACTUAL_CTR = [.45, .65]

1. Random Selection

Random Selection은 말그대로 탐색을 하지 않고 동전 튕기기를 이용해서 앞면이면 광고0, 뒷면이면 광고1을 보여주는 알고리즘입니다. 정말 간단합니다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
n=1000

regret = 0
total_reward = 0
regret_list = []
ctr = {0: [], 1:[]} #lists for collecting the calculated CTR
index_list = [] # lists for collecting the number of randomly choosen Ad

#initial values for impressons and clicks
impressions = [0,0]
clicks = [0,0]

for i in range(n):
random_index = np.random.randint(0,2,1)[0] # randomly choose the value between [0,1]
index_list.append(random_index)
impressions[random_index] += 1
did_click = bernoulli.rvs(actual_ctr[random_index])

if did_click:
clicks[random_index] += did_click

if impressions[0] == 0 :
ctr_0 = 0

else :
ctr_0 = clicks[0]/impressions[0]

if impressions[1] == 0:
ctr_1 = 0
else :
ctr_1 = clicks[1]/impressions[1]

ctr[0].append(ctr_0)
ctr[1].append(ctr_1)

## calculate the regret and reward
regret += max(actual_ctr) - actual_ctr[random_index]
regret_list.append(regret)
total_reward += did_click
1
2
3
Ad #0 has been shown 48.4 % of the time.
Ad #1 has been shown 51.6 % of the time.
Total Reward (Number of Clicks): 546

CTR이야 0.65, 0.45를 잘 찾아간다지만, 중요한 것은 Regret입니다. Regret함수를 보면 함수값이 거의 100대에 육박하는 것을 볼 수 있습니다. 좀 더 좋은 알고리즘을 통해서 Regret을 낮출 필요가 있겠습니다. 마케팅 예산이 무한대라면 그냥 마구잡이로 보여주고 CTR을 측정해서, 높은 CTR을 보이는 광고안을 선정하면 그만입니다. 하지만 일개 사원인 우리들은 예산을 최대한 아껴서 좋은 효율적인 광고를 통해 매출을 극대화 해야하는 사람들입니다. 그렇다면 좀 더 좋은 알고리즘을 살펴보겠습니다.

2. Epsilon Greedy

Epsilon Greedy 알고리즘은 Random Selection에서 한 단계 업그레이드 된 모델입니다.
이 알고리즘은 탐색과 이용의 비율을 어느정도 조정한다는 것이 큰 특징입니다.

  • ~15%까지는 Exploration
  • ~85%까지 Exploitation

로직은 다음과 같습니다.

  1. 초기 몇번 까지는 Exploration(초기 값이 중요!)
  2. 각 Exploration마다 최고 점수를 받는 variant 고르기
  3. Epsilon 설정
  4. (1-E)%의 winning variant를 고르고 다른 옵션에는 E%를 설정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
e = 0.05
n_init = 100
impressions = [0,0]
clicks = [0,0]

for i in range(n_init):
random_index = np.random.randint(0,2,1)[0]

impressions[random_index] += 1
did_click = bernoulli.rvs(actual_ctr[random_index])
if did_click:
clicks[random_index] += did_click

ctr_0 = clicks[0] / impressions[0]
ctr_1 = clicks[1] / impressions[1]
win_index = np.argmax([ctr_0, ctr_1])

print('After', n_init, 'initial trials Ad #', \
win_index, 'got the highest CTR', round(np.max([ctr_0, ctr_1]),2),
'(Real CTR value is', actual_ctr[win_index], ')'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
regret = 0
total_reward = 0
regret_list = []
ctr = {0 : [], 1: []}
index_list = []
impressions = [0,0]
clicks = [0,0]

for i in range(n):
epsilon_index = random.choices([win_index, 1-win_index],
[1-e, e])[0]
index_list.append(epsilon_index)

impressions[epsilon_index] +=1
did_click = bernoulli.rvs(actual_ctr[epsilon_index])
if did_click :
clicks[epsilon_index] += did_click

if impressions[0] == 0 :
ctr_0 = 0
else :
ctr_0 = clicks[0]/impressions[0]

if impressions[1] ==0 :
ctr_1 = 0
else :
ctr_1 = clicks[1]/impressions[1]

ctr[0].append(ctr_0)
ctr[1].append(ctr_1)

regret += max(actual_ctr) - actual_ctr[epsilon_index]
regret_list.append(regret)
total_reward += did_click
1
2
3
Ad #0 has been shown 6.2 % of the time.
Ad #1 has been shown 93.8 % of the time.
Total Reward (Number of Clicks): 642

Random Selection model보다는 훨씬 괜찮은 결과가 나왔습니다. 간단한데 결과는 훨씬 좋아지네요. 하지만 탐색시의 winning variant는 최적의 variant가 아닐 수 있습니다. 사실 suboptimal variant로 탐색 한 것입니다. 이것은 regret을 올리고 보상을 감소시킬 수 밖에 없습니다. 큰 수의 법칙에 따르면, 초기 시도를 많이 할수록, winning variant를 찾을 가능성이 커집니다. 하지만 마케팅에서는 큰 수의 법칙에 결코 따를 수가 없을 겁니다. 우리는 일개 사원…

이 알고리즘의 좋은 점은 어떤 비율을 설정할 수 있다는 것입니다. 각기 다른 epsilon값을 선택함으로써 얼마나 자주 winning ad를 보여줄 수 있는지 조정할 수 있는 것입니다.

좀 더 좋은 알고리즘을 살펴볼까요?

3. Thompson Samling

  • 50% Exploration

  • 50% Exploitation
    Thompson Sampling의 탐색 부분은 Epsilon-greedy알고리즘보다 복잡합니다. 이 알고리즘은 단순히 epsilon을 정하는 것이 아니라, Beta distribution을 이용하기 때문입니다. 왜냐하면 광고를 클릭하는 것은 베르누의 과정에 속하기 때문입니다.(클릭했다, 안했다는 1,0으로 표현 가능합니다) 하지만 톰슨 샘플링은 일반적으로 어떤 분포, 어떤 파라미터에서든지 샘플링이 가능하다. 이게 가장 큰 장점 중 하나라고 생각합니다.

  • 참고로 Beta 분포는 alpha와 beta 파라미터로 분포의 모양을 조절한다.(prior 조정 가능)

로직은 다음과 같습니다.

  1. alpha와 beta를 고른다.
  2. $\alpha=prior+hits$, $\beta=prior+misses$로 계산한다. 우리의 경우는 hits는 클릭 수를 말하고, misses는 클릭없이 impression된 경우를 말합니다(클릭 없는 노출). prior는 CTR에 대한 prior정보가 있으면 유용합니다. 우리는 갖고 있지 않으므로 1.0을 사용할 것 입니다..
  3. CTR을 추정합니다. 실제 CTR을 베타 분포에서 샘플링하고 $B(\alpha_i,\beta_i)$에서, 추정 CTR이 가장 높은 것을 선택한다.
  4. 2-3을 반복한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
regret = 0
total_reward = 0
regret_list = []
ctr = {0 : [], 1: []}
index_list = []
impressions = [0,0]
clicks = [0,0]
priors = (1,1)
#randomly choose the first shown ad
win_index = np.random.randint(0,2,1)[0]

for i in range(n):
impressions[win_index] += 1
did_click = bernoulli.rvs(actual_ctr[win_index])
if did_click :
clicks[win_index] += did_click

ctr_0 = random.betavariate(priors[0] + clicks[0], priors[1] + impressions[0] - clicks[0])
ctr_1 = random.betavariate(priors[1] + clicks[1], priors[1] + impressions[1] - clicks[1])

win_index = np.argmax([ctr_0, ctr_1])
index_list.append(win_index)

ctr[0].append(ctr_0)
ctr[1].append(ctr_1)

regret += max(actual_ctr) - actual_ctr[win_index]
regret_list.append(regret)
total_reward += did_click
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
x = np.arange (0, 1, 0.01)
y = beta.pdf(x, priors[0]+clicks[0], priors[1] + impressions[0] - clicks[0])
y /= y.max() ## normalize

data1 = go.Scatter(x=x,
y=y,
name='(Ad #0)',
marker = dict(color=('rgba(10, 108, 94, 1)')),
fill='tozeroy',
fillcolor = 'rgba(10, 108, 94, .7)')

data2 = go.Scatter(x = [actual_ctr[0]] * 2,
y = [0, 1],
name = 'Actual CTR #0 Value',
mode='lines',
line = dict(
color = ('rgb(205, 12, 24)'),
width = 2,
dash = 'dash'))

y = beta.pdf(x, priors[0]+clicks[1], priors[1] + impressions[1] - clicks[1])
y /= y.max()

data3 = go.Scatter(x=x,
y=y,
name='(Ad #1)',
marker = dict(color=('rgba(187, 121, 24, 1)')),
fill='tozeroy',
fillcolor = 'rgba(187, 121, 24, .7)')

data4 = go.Scatter(x = [actual_ctr[1]] * 2,
y = [0, 1],
name = 'Actual CTR #1 Value',
mode='lines',
line = dict(
color = ('rgb(205, 12, 24)'),
width = 2,
dash = 'dash'))

layout = go.Layout(title='Beta Distributions for both Ads',
xaxis={'title': 'Possible CTR values'},
yaxis={'title': 'Probability Density'})

fig = go.Figure(data=[data1, data2, data3, data4], layout=layout)

# fig = tools.make_subplots(rows=1, cols=2, print_grid=False, shared_xaxes=False,
# subplot_titles=('Beta Distribution (Ad #0)','Beta Distribution (Ad #1)'))

# fig.append_trace(data1, 1, 1)
# fig.append_trace(data2, 1, 1)
# fig.append_trace(data3, 1, 2)
# fig.append_trace(data4, 1, 2)

# fig['layout'].update(showlegend=False)

iplot(fig, show_link=False)
1
2
3
Ad #0 has been shown 4.2 % of the time.
Ad #1 has been shown 95.8 % of the time.
Total Reward (Number of Clicks): 647

지금까지 본 regret중 가장 낮은 regret을 확인할 수 있습니다.

이 알고리즘은 지속적으로 탐색합니다. 자연스럽게 Beta distribution을 이용해 가장 가치가 높은 샘플을 가져와서 이용할 수 있습니다. Beta distribution Ad#1은 더 높고 좁은 분포를 갖고 있습니다. 이것은 샘플된 값들이 항상 Ad#0보다 높을 것이라는 것을 의미합니다. 결국 Ad#1이 우리가 원하는 광고임을 빠르게 파악할 수 있습니다.

UCB (Upper Confidence Bound)

  • 50% Exploration
  • 50% Exploitation
    Thompson Sampling과 달리 UCB는 불확실성에 더 초점을 맞춥니다. 한 variant에 대해 더 불확실 할 수록, 더 탐색을 해야만 하는 알고리즘입니다.

알고리즘은 가장 높은 UCB가 나오는 variant를 선택합니다. UCB를 통해 가장 높은 보상이 나올 것이라고 생각되는 variant를 고르는 것입니다.

$$UCB = \bar x_i + \sqrt{\frac{2 \cdot \log{t}}{n}}$$
이 수식을 따르며 뒤에 term에 따라 UCB의 파생 알고리즘들이 등장하게 됩니다.

$\bar x_i$ CTR이 i번째 단계일 때,
$t$ - 모든 variant에 대해 impression을 다 더한 숫자이다.
$n$ - 선택된 variant에 대해 impression을 다 더한 숫자이다.

로직은 직관적입니다.

  1. UCB를 모든 변량들에 대해 구합니다.
  2. 가장 높은 UCB를 가진 변량을 선택합니다.
  3. 1번으로 다시 돌아갑니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
regret = 0 
total_reward = 0
regret_list = []
index_list = []
impressions = [0,0]
clicks = [0,0]
ctr = {0: [], 1: []}
total_reward = 0

for i in range(n):

index = 0
max_upper_bound = 0
for k in [0,1]:
if (impressions[k] > 0):
CTR = clicks[k] / impressions[k]
delta = math.sqrt(2 * math.log(i+1) / impressions[k])
upper_bound = CTR + delta
ctr[k].append(CTR)
else:
upper_bound = 1e400
if upper_bound > max_upper_bound:
max_upper_bound = upper_bound
index = k
index_list.append(index)
impressions[index] += 1
reward = bernoulli.rvs(actual_ctr[index])

clicks[index] += reward
total_reward += reward

regret += max(actual_ctr) - actual_ctr[index]
regret_list.append(regret)
1
2
3
Ad #0 has been shown 19.2 % of the time.
Ad #1 has been shown 80.80000000000001 % of the time.
Total Reward (Number of Clicks): 596

결과는 다음과 같습니다. Regret이 생각보다 높네요, UCB알고리즘도 현업에서 자주 사용하는 알고리즘입니다만, 이 알고리즘은 가장 기본적인 알고리즘이기 때문에 그런 것 같습니다.

결론 및 성능 비교

이제 살펴봤던 모든 알고리즘의 성능을 비교해 볼 시간입니다. 기대가 되네요, 나온 결과를 시각화를 해서 살펴 보겠습니다.

1000번의 시도에 어떤 광고를 얼마나 노출시켰는 지에 대한 막대그래프입니다. Random Selection은 CTR이 낮은 광고를 너무 많이 노출 시켰네요, 그 다음은 UCB1, Epsilon Greedy, Thompson Sampling 순 입니다. Thompson Sampling이 가장 좋네요! 하지만 놀라운 것은 Epsilon Greedy입니다. 정말 간단한 알고리즘인데 성능이 좋군요. 다음 자료는 Regret에 대한 것입니다. 시도가 늘어날 수록, Random Selection이나 UCB는 쭉 쭉 증가하는 것이 보입니다. 하지만 Thomspon Sampling은 굉장히 안정적으로 Regret이 유지되네요. 마지막은 1000번의 시도에서 총 몇번의 클릭을 받았는가에 대한 시각화 자료입니다. 클릭이 많다면 더 효과적으로 실험을 하면서 광고를 했다고 할 수 있겠네요. 역시 Thompson Sampling이 가장 많은 클릭 수를 얻었습니다. 그 다음은 Epsilon Greedy, UCB1, Random Selection 순 입니다.

물론 Regret이 낮다고 가장 높은 보상이 있는 것은 아닙니다. 이 실험에서는 우연히 Thompson Sampling이 Regret도 낮고, 높은 보상을 얻었습니다. 알고리즘은 적절한 광고를(right ads) 보여줄 뿐 이고, 유저가 클릭하는 것은 보장하지 않습니다.

일반적으로 Thompson Sampling이 좋은 결과를 보여줍니다. 하지만 다른 알고리즘을 보면서 어떻게, 그리고 언제 그 알고리즘이 유용할지 생각해봐야 합니다. 어떤 문제를 풀 지는 각 개인 마다 다르기 때문에, 여러 알고리즘들 중에서 문제에 적합한 것을 선택할 수 있어야 합니다. 어떤 사전 정보를 갖고 있고, 알고리즘 적용 후에 어떤 정보를 알고싶은지를 명확하게 설정하는 것이 더 중요하다고 할 수 있겠습니다.

글또 3기에 들어서면서... 상반기 회고와 다짐

상반기 회고와 나의 다짐

상반기 회고 (2019.01.01 ~ 2019.06.24, 도서관)

새해 첫 날은 스페인에서 보냈었네요, 공부만 하다가 처음으로 짬이 나서! 계획했었던 영국~스페인 여행을 2주일 정도 갔었습니다. 항상 아침에 조깅을 하면서 '한국 돌아가서 뭘해볼까...' 이런 고민들을 했었고 그 중 제일 처음으로 해야겠다고 생각했던 것이 블로그였습니다.

일단 블로그부터 제대로 구축하자! 라는 계획으로 hexo 블로그를 만들었고, 여러 테마들을 돌려보면서 괜찮은 것들을 살펴봤습니다. 한 한달정도 블로그랑 씨름하다보니 어느정도 구축이 되었고, 배운 내용들을 글로 정리하기 시작했습니다.

사실 올해에는 취업 생각이 없었습니다. 데이터 이론이나 알고리즘 등 준비해야 할 것도 많다고 생각했고, 개인 프로젝트도 더 필요하다고 느꼈습니다. 하지만 3월에 상반기 대기업 취업 공고가 나니까 마음이 급해지기 시작했죠. ‘내가 공부는 정말 많이 했지만 내가 진짜 제대로 알고 있는걸까?’, ‘이 상태로 취업은 가능할까?’, ‘공부를 이렇게 하는게 맞나?’ 이런 고민들이었습니다.

이런 고민들로 3월부터 6월 동안 취업준비를 급하게 시작했습니다. 결국 데이터 관련 일은 데이터를 직접 만져봐야 얻는 게 있다는 결론을 내렸기 때문입니다. 이력서도 많이 쓰고 면접도 많이 봤습니다. 첫 면접부터 마지막 면접까지 하나하나 기억이 다 나네요, 쓰라렸지만 좋은 경험을 많이 했다고 생각합니다. 사실 상반기 회고를 하면서 면접에 관해서 글이 길어졌는데, 너무 무거운 내용들이 많아서 일단 나중에 정리해서 업로드 할 생각입니다. 이번 글은 조금 가벼운 느낌으로!

살다시피 했던 경영도서관
그래서 상반기 회고를 다시 하자면 저는 도서관에서 거의 살았었습니다. 아침 일찍 나가서 저녁 늦게 까지 책을 쌓아놓고 노트북 두들기면서 한 자리에만 있었습니다. 그때 공부를 하면서 느낀 건 공부를 오래하고 싶더라도 체력이 부족하면 불가능 하다는 것이었습니다. 운동도 시작했고, 식단 조절도 해보면서 건강을 챙겼습니다. 우연히 운동 좋아하는 후배들을 알게 되면서 좋은 습관들을 쌓게 된 것 같네요.

글쓰는 습관

취업 준비도 준비지만 또 다른 좋은 습관을 들이려고 노력한 것은 글쓰는 습관이었습니다. 배운 내용을 혼자 공부해서 갖고 있는 것보다, 글을 쓰고 공유하고, 얘기하는 것이 저에게 훨씬 더 큰 가치를 가져다 줄 수 있겠구나 하는 생각이 들었습니다. 부족하지만 이론을 정리한 걸 글로 작성하고, 알고리즘 문제 푼 것들도 어떤 생각의 흐름으로 풀었는지 기록했습니다. 이외로 면접 준비하면서 이걸 다시 보게 되니까 정리하는데 도움이 많이 되는 것 같았습니다. 특히 이론에 관해서 글을 쓸때는 완벽하게 알지 못하면 글을 쓰지 못하기 때문에, 어디가 부족한지 스스로 알 수 있게 되어서 더 좋은 것 같습니다.

그 외에 상반기에 했던 것들은 다 취업 준비가 대부분이었던 것 같네요.

회고를 해보니 너무 정신없이 살았던 것 같습니다. 정리 안되고 정신없는 거 별로 안 좋아하는데 상반기를 정리해보니 제 자신이 정리 안하고 살았었네요. 하반기에는 계획을 제대로 세워서 하나씩 클리어 하는 재미로 살아봐야겠습니다.

상반기 회고를 두 번 해보니 얻어지는 것이 있었습니다. 사실 취업준비를 하면서 많이 지쳤었거든요, 데이터 얘기만 들어도 싫고, 개발이나 알고리즘, 코드만 봐도 어지럽고 도망치고 싶었습니다. 하지만 취업도 했고, 상반기를 냉철하게, 처절하게 다시 들여다보고 나니, 다시 시작할 힘이 나는 느낌입니다. 바닥에 다시 내려왔고, 어디부터 공부를 해야할지 이제 감이 잡히는 느낌입니다.

어떤 글을 쓸까? / 다짐

글또 3기를 하면서 가끔은 넋두리 같은, 오늘 같은 이야기를 하게 될 것 같고, 캐글 대회에 참여하고 잘 되거나, 안되었던 것들을 정리할 계획입니다. 또 일하면서 필요한 부분을 공부하면서, 예를들어 AWS(사실 GCP를 더 공부하고 싶었는데 ㅠㅠ 회사는 AWS를 쓰는군요…)나 Apache Spark, NoSQL(MongoDB) 등을 정리한 내용을 공유할 것 같습니다.

하반기에 계획했었던 일 중 하나가 글또 3기 들어가는 것이였는데요, 벌써 체크 하나하게 되어서 너무 기쁩니다. 최소 12개의 글을 쓰게 될텐데 그 과정이 의외로 도전적일 것 같아 재밌을 것 같네요. 도전적인 자세로 하반기를 살아봐야겠습니다. 내일은 월요일, 도전이 생각보다 꽤 빨리 시작되는 느낌입니다.

Classification Metrics

Classification Metrics

분류 모델의 평가지표에 대해서 알아보자

Classification 모델을 만든 후에 모델의 성능이 어떤지 알기 위해서는 성능 평가 지표가 필요하다.
분류 모델의 성능지표를 알아보면서, 데이터의 상태에 따라서 어떤 지표를 사용해야하는지 공부해보자.

0과 1로 결정값이 한정되는 이진 분류 성능 평가 지표에 대해서 집중적으로 다뤄보자.

분류 성능 평가 지표

  • 정확도(Accuracy)
  • 오차행렬(Confusion Matrix)
  • 정밀도(Precision)
  • 재현율(Recall)
  • F1스코어
  • ROC AUC

정확도(Accuracy)

정확도는 실제 데이터에서 예측 데이터가 얼마나 같은지를 판단하는 지표이다.
$$Accuracy = {TP+TN\over TP+TN+FP+FN}$$
정확도는 직관적으로 모델 예측 성능을 나타내는 평가 지표이며, 기본적으로 많이 사용하는 지표중 하나이다.
하지만 정확도는 치명적인 약점이 존재하는데, 바로 불균형한 데이터 셋에서는 제대로 평가가 안된다는 것이다. 예를 들어보자 1000개의 샘플에 10개만 문제가 있는 샘플이다. 이럴 경우에 엉터리 분류기, 즉 모든 샘플에 대해서 정상이라고 분류하는 분류기를 이용해서 분류하고 정확도로 성능 평가를 한다면, 결과는 990/1000, 99%의 정확도를 보이게 된다.
엉터리 분류기가 과연 좋은 분류기일지 생각해보자. 만약 이 분류기에 문제가 있는 샘플을 더 추가한다면, 정확도는 기하급수적으로 떨어지게 될 것이다.

오차행렬(Confusion Matrix)

오차행렬은 학습된 분류 모델이 예측을 수행하면서 얼마나 헷갈리고 있는지도 함께 보여주는 지표이다. 즉, 이진 분류의 예측 오류가 얼마인지와 어떤 유형의 예측 오류가 발생하고 있는지를 같이 나타내 주는 지표이다.

오차행렬은 다음과 같이 표현한다.

Negative(0) Positive(1)
Negative(0) TN(True Negative) FP(False Positive)
Positive(1) FN(False Negative) TP(True Positive)

위의 표에서 진하게 표시된 것이 예측 클래스에 대한 것이고(Predicted Calss) 옅게 표시된 것이 실제 클래스(Actual Class)이다.

  • TP는 예측값을 Positive값 1으로 예측했고, 실제 값 역시 Positive값 1
  • TN는 예측값을 Negative 0으로 예측했고, 실제 값 역시 Negative값 0
  • FP는 예측값을 Positive값 1으로 예측했고, 실제 값은 Negative 값 0
  • FN는 예측값을 Negative값 0으로 예측했고, 실제 값 역시 Positive값 1

오차행렬을 기반으로 해서 정확도의 식을 다시 보면 결국, True에 해당하는 값인 TP와 TN에 값이 좌우되고 있다는 것을 알 수 있다. 정확도 = 예측 결과와 실제 값이 동일한 건수 / 전체 데이터 수 라고 다시 말할 수 있다.

불균형한 이진 분류 데이터 셋에서는 Positive 건수가 매우 작기 때문에 이러한 데이터로 학습된 ML 알고리즘은 Positive보다는 Negative로 예측 정확도가 높아지는 경향이 발생한다. TN값이 높아진다는 것이다. 결과적으로 불균형 데이터 셋에서는 Positive에 대한 예측 정확도를 판단하지 못하고 Negative에 대한 예측 정확도만으로 분류의 정확도가 매우 높게 나타나는 수치적인 판단 오류를 일으키게 된다.

이런 판단 오류를 극복하기 위해서 정밀도(Precision)와 재현율(Recall)이 성능지표로 사용된다.

정밀도와 재현율 (Precision and Recall)

정밀도와 재현율은 다음과 같은 공식으로 정의된다.
$$Precision = {TP \over FP+TP}$$
$$Recall = {TP \over FN+TP}$$
정밀도는 예측을 Positive로 한 대상 중에 예측과 실제 값이 Positive로 일치한 데이터의 비율을 뜻한다. 정밀도의공식에서 분모는 예측을 Positive로 한 모든 데이터 건수이다. Positive 예측 성능을 더욱 정밀하게 측정하기 위한 평가 지표로 양성 예측도라고 불린다.

재현율은 실제 값이 Positive인 대상 중에 예측과 실제 값이 Positive로 일치한 데이터의 비율을 뜻한다. 공식의 분모는 실제 값이 Positive인 모든 데이터 건수이다. 민감도 또는 TPR(True Positive Rate)라고도 불린다.

정밀도와 재현율은 중요하게 생각하는 부분이 서로 다르기 때문에, 주어진 업무 특성에 따라서 특정 평가 지표가 더 중요한 지표로 간주될 수 있다. 재현율이 중요한 경우를 생각해보자. 재현율이 중요 지표로 사용되는 경우는 실제 Positive 양성 데이터를 Negative로 잘못 판단하게 되면 크리티컬한 영향이 발생하는 경우이다. 예를 들어 암 판단 모델은 재현율이 중요한데, 실제 Positive인 경우, 즉, 암환자를 Negative, 정상으로 분류하는 경우 오류의 대가가 생명이 될 수 있을 정도로 치명적이다. 만약 정상환자를 암환자로 분류하는 경우에는, 재검진을 하는 정도의 비용이 소모된다.(Positive–>Negative로 잘못분류)

정밀도가 중요한 경우를 생각해보자. 스팸메일 여부를 확인하는 예를 들어보면, 실제 Positive인 스팸 메일을 Negative 정상 메일이라고 분류하게 되면 사용자가 불편함을 느끼는 정도지만, 정상메일을 Spam으로 분류해 버리면 업무메일 등이 스팸으로 처리되어 메일을 받지 못하게 돼 업무에 차질이 생길 수 있다.(Negative–>Positive로 잘못분류)

정리하자면,

  • 재현율이 더 중요한 경우, 실제 Positive 양성 데이터 예측을 Negative로 잘못 판단하게 되면 업무 상 큰 차질이 발생하는 경우
  • 정밀도가 더 중요한 경우, 실제 Negative 음성 데이터 예측을 Positive로 잘못 판단하게 되면 업무 상 큰 차질이 발생하는 경우

공식을 다시살펴보면, Precision은 FN이 분모에 사용되고, Recall은 FP가 분모에 사용된다. 재현율은 FN을 낮추는 데, 정밀도는 FP를 낮추는 데 초점이 맞춰진다. 가장 좋은 것은 둘다 높은 것인데, 두 성능 지표가 상호 보완적이기 때문에 Trade off가 존재한다.

정밀도/재현율 트레이드 오프

정밀도나 재현율은 분류의 결정 임계값을 조정해 정밀도나 재현율의 수치를 높일 수 있다. sklearn의 분류 모델들에서 threshold를 조절할 수 있는 파라미터를 찾아보면 된다. threshold값을 낮추면 보통 재현율 값이 올라가고 정밀도 값이 떨어진다. threshold값은 Positive 예측값을 결정하는 확률의 기준이 되고 낮출 수록 True값이 많아지기 때문이다.

Positive 예측값이 많아지면 상대적으로 Recall 값이 높아진다. 양성 예측을 많이 하다보니 실제 양성을 음성으로 예측하는 횟수가 상대적으로 줄어들기 때문이다(FN값이 떨어진다).

  • 임계값 증가하면 Negative 예측 값이 증가한다(FP값이 떨어짐) ==> Precision 증가
  • 임계값 감소하면 Positive 예측 값이 증가한다(FN값이 떨어짐) ==> Recall 증가

정밀도와 재현율의 맹점

Positive 예측의 임계값을 변경함에 따라 Precision과 Recall의 수치가 변경되는 것을 확인해 봤다. Threshold의 이런 변경은 업무 환경과 목적에 맞게 두 수치를 상호 보완할 수 있는 수준에서 적용되어야 한다. 단순히 성능지표로서 숫자를 올리는 수단으로 사용되면 안된다.

정밀도 100% 만들기

확실한 기준이 되는 것만 Positive로 예측하고 나머지는 모두 Negative로 예측한다. 정밀도 = TP / (TP+FP) 이다. 예를 들어 암환자를 예측한다고 해보자. 전체환자 1000명 중에 확실한 Positive 징후만 가진 환자가 단 1명이라면(죽기 일보직전의) 한명만 Positive로 예측하고 나머지는 모두 Negative로 예측하더라도 FP는 0, TP는 1이기 때문에, 정밀도는 1/(1+0)으로 100%가 된다. Precision은 100%지만, 초기 암진단을 예측하는 경우는 희박하고, 위험한 정도의 암환자도 정상이라고 분류할 수 있기 때문에 좋은 분류기라고 할 수 없을 것이다.

재현율 100% 만들기

모든 환자를 Positive로 예측하면 된다. 재현율 = TP / (TP+FN)이므로 전체 환자 1000명을 다 Positive로 예측하는 것이다. 이 중 실제 양성인 사람이 30명 정도라도 TN이 수치에 포함되지 않고 FN은 아예 0이므로 30/(30+0)으로 100%가 된다. 이렇게 되면 재현율은 100%지만 모델을 정말 신뢰할 수 있는지에 대해 의심이 발생할 것이다. 이런 모델은 정상인 사람도 암 환자로 예측하게 되므로, 재검사 비율을 매우 높이게 된다. 병원에서 재검사 비용을 대줘야 한다면, 혹은 환자로 분류된 사람이 재검사 비용을 내야 한다면, 병원이 손해를 막심하게 보거나, 고객들이 병원에 대해 신뢰를 하지 않을 것이다.

따라서 정밀도와 재현율을 적절하게 고려한 평가 지표가 필요하게 된다.

F1 Score

F1-Score는 정밀도와 재현율을 조화 평균한 지표이다. F1-Score는 정밀도와 재현율이 어느 한 쪽으로 치우치지 않는 수치를 나타낼 때 상대적으로 높은 값을 가진다. 공식은 다음과 같다.
$$F1={2\over{1\over{recall}}+{1\over{precision}}}=2\times{precision*\space recall\over precision+recall}$$

만일 A 예측 모델의 경우 Precision이 0.9, Recall이 0.1로 극단적인 차이가 나고, B 예측 모델은 Precision과 Recall이 0.5로 큰 차이가 없다면 A의 F1-Score는 0.18이고, B의 F1-Score는 0.5로 B의 모델이 좋은 점수를 얻게 된다. 사실 F1 Score는 Precision과 Recall에 동일한 가중치인 0.5를 적용한 값이다. F-Measure는 $\beta$를 이용해 가중치를 조절한다. 공식을 살펴보자.

$F_\beta=$$(1+\beta^2)(Precision * Recall)\over{\beta^2 Precision + Recall}$

$\beta$가 1보다 크면 Recall이 강조되고 1보다 작으면 Precision이 강조된다. 1일때의 점수를 $F_1$점수라고 한다.

ROC & AUC

ROC곡선(Receiver Operation Characteristic Curve)은 수신자 판단 곡선으로, 2차대전 때 통신 장비 성능 평가를 위해 고안된 수치이다. 요즘에는 이진 분류의 성능 평가 지표로 자주 사용된다. ROC Curve는 FPR(False Positive Rate)이 변할 때 TPR(True Positive Rate)이 어떻게 변하는지를 나타내는 곡선이다. FPR을 x축으로, TPR을 y축으로 잡으면 FPR에 대한 TPR의 변화가 곡선 형태로 나타난다.

TPR은 True Positive Rate의 약자이며, Recall을 나타낸다. 따라서 TPR은 TP/(TP+FN) 이다. 민감도라고도 불리며 민감도에 대응하는 지표로 TNR(True Negative Rate)이라고 불리는 특이성이 있다.

  • 민감도(TPR)는 실제값 Positive가 정확히 예측되어야 하는 수준을 나타낸다.(질병이 있는 사람은 질병이 있는 것으로 양성 판정)
  • 특이성은(TNR) 실제값 Negative가 정확이 예측되어야 하는 수준을 나타낸다.(정상인 사람은 정상으로 음성 판정)

TNR은 TN/(TN+FP)이며 X축의 기준인 FPR은 FP/(FP+TN)이므로 1-TNR로 표현할 수 있다.

ROC 곡선은 FPR을 0부터 1까지 변경하며 TPR의 변화 값을 구한다. Threshold값을 변경하면서, 처음에는 1로 지정해 FPR을 0으로 만든다. Threshold가 1일 때 Positive 예측 기준이 매우 높기 때문에 분류기가 Threshold보다 높은 확률을 가진 데이터를 Positive로 예측할 수 없다. 즉, 아예 Positive로 예측을 하지 않기 때문에 FP가 0이 되어 FPR이 0이된다. FPR = FP/(FP+TN)

반대로, FPR을 1로 만들려면 TN을 0으로 만들면 된다. Threshold를 0으로 지정하게 되면, 분류기가 모든 데이터에 대해서 Positive로 예측을 하게 된다. 이렇게 되면 Negative 예측은 없기 때문에 FPR이 1이 된다.

일반적으로 ROC Curve자체는 FPR과 TPR의 변화 값을 보는 데 이용하고, 분류의 성능 지표로 실제로 사용되는 것은 AUC(Area Under Curve)이다. 이 값은 ROC 곡선 밑의 면적을 구한 것으로, 일반적으로 1에 가까울수록 좋은 수치이다. AUC가 커지려면, FPR이 작은 상태에서 얼마나 큰 TPR을 구할 수 있는 지가 중요하다. 가운데 직선에서 멀어지고 좌상단 모서리로 곡선이 바짝 붙을 수록 직사각형에 가까운 곡선이 되어 면적이 1에 가까워진다. 가운데의 직선은 랜덤 수준의 이진 분류 AUC값으로 0.5이다.

PCA

Dimensional Reduction에 쓰이는 PCA에 대해 알아보자

PCA(Principal Component Analysis)

데이터 분석을 하다보면 답답한 경우가 자주 발생한다. 모델을 돌려야 하는데 feature가 너무 많아서 연산 코스트가 너무 많이 들고, 계산하는데 너무 오랜 시간이 걸리는 것이다. 결과를 봤더니, 복잡한 feature때문에 지저분하게 나오고, 노이즈도 많이 껴있는 것 같다. PCA는 이런 경우에 자주 사용되는 알고리즘이다. PCA는 그러니까 relative하지만 redundant한 feature를 제거하는데 자주 사용되거나 데이터를 단순화 할때 사용된다.

데이터를 단순화하는데는 다음의 두 가지 방법이 있다.

  • 차원 축소(Dimensional Reduction) : 데이터를 표현하는 속성의 수를 축소
  • 요소 분석(Factor Analysis) : 관찰 가능한 데이터 = 잠재적인 변수(latent variable)와 noise의 선형결합

우리는 차원 축소에 대한 내용을 살펴볼 것이다.

아까의 상황을 다시 가져와보자. 이전의 예에서 복잡한 feature들은 사실 highly correlated 되어 있기 때문에 문제가 있는 것이다. 변수들의 서로 연관되어 있으면 설명량은 올라가지만, 좋은 모델이라고 볼 수 없고, 어떤 변수가 타겟에 어떻게 영향을 주는지 알 수 없다. 불필요한(서로 연관되어 있거나, 결과와 상관없는) 변수들은, 변수들을 모으고 분석하는데 드는 비용을 증가시켜서, 예측 모델 또는 분류 모델을 만들 때 전체 비용을 증가시키는 원인이 된다.

따라서 불필요한 변수들을 제거할 필요가 있고, Machine Learning 영역에서는 본래 모델이 가지고 있던 성질이나 정확도를 최대한 유지하면서 차원을 줄이는 방법을 통하여 위에서 설명된 문제점을 해결하려고 한다.

모델의 차원(dimensionality)은 모델에 사용되는 독립(independence) 변수 또는 입력(input) 변수의 개수(number)를 의미한다. 우리가 GLM(Generalized Linear Model)을 사용하는 이유처럼 독립변수가 타겟에 미치는 영향을 제대로 알기 위해서 독립적인 변수가 필요한 것이다. 통계에서 항상 IID 조건을 사용하는 것과 의미가 비슷할 것이다.

정리하자면 PCA를 사용하는 이유는 다음과 같다.

  • feature가 너무 많으면 연산에 사용되는 cost가 너무 높고, 시간도 너무 오래 걸리기 때문에, 변수들을 줄여줄 필요가 있다.
  • 많은 feature들 중에서는 상관관계가 높은 feature들이 있다(high correlated). 이런 feature들은 모델의 설명량은 높일 수 있지만, 모델의 성능은 떨어트릴 수 있다. 또한 우리가 흥미 있어 하는 결과와 상관없는 변수들이 존재할 수 있는 상황이 발생할 수 있다.

Dimensional Reduction

Dimensional Reduction의 핵심 아이디어는, 상관도가 높은(interrelated, correlated) 변수들이 많이 존재하는 데이터 집합의 차원(Dimensionality)을 줄이면서, 동시에 데이터 집합에 존재하고 있는 차이(Variation, 정보)를 최대한 유지하는 것이다. 즉, 차원을 줄이되 “정보 손실을 최소화”하는 것이다. 여기서 정보란 데이터간의 거리, 실제적인 위치를 정보라고 표현한다. 다시말하면 위치, 상대적인 거리를 뜻한다. 하지만 차원축소는 정보의 손실을 어느 정도 감수해야 한다.

Dimensional Reduction은 원래 공간에서 데이터가 퍼져 있는 정도를 변환된(축소된) 공간에서 얼마나 잘 유지하느냐를 척도로 삼는다. 원래 공간의 정보가 변환된 공간에서 얼마나 잘 유지하는지는 변환된 공간에서의 데이터의 분산으로 측정한다. 따라서, 변환된 공간에서 데이터의 분산을 최대로 유지 할 수 있는 좌표축을 찾아야 한다.

즉, PCA는 원래 공간에서 데이터가 분산되어 있는 주요한 방향(Principal direction)을 찾는 것이 목표가 된다.
여러축으로 구성되어 있는 데이터를 주성분 분석으로 통해 기존의 feature들과는 다른 새로운 축으로써 다시 구성해보되, 분산을 최대로 유지한다.

PCA 수행방법

PCA에서 데이터가 분산되어 있는 주요한 방향(Principal Component)을 찾는 단계는 다음과 같다.

  1. 데이터를 투영(Projection)하기
  2. 투영된 공간에서 분산 측정하기
  3. 분산의 최대치는 어떻게 찾는가?

데이터를 여러 축에 투영해보면서 투영된 공간에서 분산을 측정하고, 가장 분산이 큰 축을 선택하는 것이 바로 PCA이다.
새로운 축이 $u$이라고 했을때, 축으로 이동된 새 데이터 포인트 $X_{new} = u^TX$이다. (𝕦t 𝕩= 𝕦 ⋅ 𝕩 cos𝜃= 𝕩 cos𝜃)

이제 투영된 공간에서 분산을 측정해보자. 먼저, PCA를 실행하기 전에 데이터의 평균(mean)과 분산(variance)를 정규화(standardization) 해 준다.(Pre-process the data) 데이터는 특성 벡터로 표현되는데, 특성 벡터의 각 원소들에서 각각의 평균과 빼 주고, 분산의 제곱근으로 나누어 준다.

정규화 과정에서

  • 데이터에서 평균을 빼는것:데이터의 평균이 0이 되도록 만든다.
  • 데이터에서 분산의 제곱근을 나누어 주는 것 : 데이터의 값들이 unit variance를 갖게 해 준다.

새 축으로 이동된 데이터의 분산 구하기

각각의 attribute의 평균이 0이 되고, 분산이 1이 된다. 즉 같은 “scale”을 가지게 되어, attribute간의 비교가 가능 해진다.
데이터 포인트 $x_{1}, x_{2}, x_{3}, x_{4}, x_{5}$가 있을 때, u의 축으로 투영된 데이터 포인트$x_{1}^Tu, x_{2}^Tu, x_{3}^Tu, x_{4}^Tu, x_{5}^Tu$의 분산을 구해보자.

먼저 평균값을 구해놓자.

$$\mu={1\over{m}}\sum_{i=1}^{m}x_i^Tu = 0$$
투영된 공간에서의 기댓값은 0이다. 왜냐하면 데이터 포인트들은 이미 standardizing을 한 상태이기 때문이다. 평균의 평균을 구하니까 0이 되는 것이다.

분산을 구해보자.

$$\sigma^2={1\over{m}}\sum_{i=1}^{m}(x_i^Tu - \mu)^2 ={1\over{m}}\sum_{i=1}^{m}(x_i^Tu)^2$$
($\mu$가 0이므로)

$$={1\over{m}}\sum_{i=1}^{m}(u^Tx_ix_i^Tu) = u^T({1\over{m}}\sum_{i=1}^{m}(x_ix_i^T))u$$

($u$는 unit vector이다.)

이것은 결론적으로
$$=u^T({1\over{m}}\sum_{i=1}^{m}(x_i-\mathbb{o})(x_i-\mathbb{o})^T)u$$

$$=u^T\sum u$$
식이 도출된다.

Σ는 공분산 행렬로 기존 데이터의 공분산 행렬을 사용한다.
결국 투영하려고 하는 축과 기존 데이터의 공분산 행렬의 곱으로 간단하게 새 축의 분산을 구할 수 있다.

분산의 최대치 구하기

우리는 Principal Component, 즉, 주성분을 구하는 것이 목적이므로, 데이터의 분산이 최대가 되도록 만드는 축을 구해야 한다. 분산의 최대치를 구하기 위해서 변환된(투영된) 공간에서 분산을 최대화 해 줄 수 있는 벡터 $u$를 찾아야 한다. $u$는 unit vector라고 생각하자. 우리가 구하고자 하는 $u$는 방향이 중요하기 때문이다. 즉, $u^Tu = 1$이다.

따라서 문제는 $u$가 unit vector일 때의 $u^T\sum u$의 최대값을 구하는 조건부 최적화 문제가 된다.

$$\max u^T\sum u$$

$$s.t \space u^Tu=1$$

이 문제는 라그랑지 승수 (Laglange Multiplier)를 이용해 해결 할 수 있다.
$$\mathcal{L}(u,\lambda)= u^T\sum u - \lambda(u^Tu-1)$$

$\mathcal{L}(u,\lambda)$를 미분해서 $u$의 최대치를 구한다.

이렇게 구한 식을 나타내면
$$u^T\sum u = u^T\lambda u=\lambda u^T u=\lambda$$

즉, 분산을 최대화 하는 문제는 Σ의 eigenvalue를 최대화 하는 문제가 된다.
$$argmax_{u}u^T\sum u = argmax_{u}\lambda$$

따라서, 변환된(축소된) 공간에서 분산의 최대값은 Σ의 eigenvalue의 최대값이다.

분산의 최대값은, 𝕦가 Σ의 eigenvalue 중 가장 큰 값을 가지는 eigenvalue에 대응되는 eigenvector일 때 달성된다. 우리는 이것을 주성분이라고도 부른다.

이 다음의 주성분을 구하는 것은 간단하다. D차원에서 주성분은 데이터 공분산 행렬의 가장 큰 eigenvalue에서 부터 D번째로 큰 eigenvalue까지에 대응되는 D개의 eigenvector가 될 것이다.

SQL_Recipe_01

SQL_Recipe_01

데이터 분석을 위한 SQL 레시피 with MySQL

데이터 분석을 위한 SQL 레시피의 3장에 있는 코드 내용들을 실습하고 MySQL코드로 변형시켜보았다.
데이터 분석을 위한 SQL 레시피 책에서는 PostgreSQL, Redshift, BigQuery, Hive, SparkSQL의 코드를 다룬다. 책에 있는 코드는 어떤 건 그대로 쳤을 때 돌아가고, 몇몇 개는 MySQL 쿼리대로 수정을 해주어야 한다.

3장의 mst_users_with_dates 테이블을 가지고 실습을 진행한다.
실습 진행 전에 테이블을 만들어준다.

1
2
3
4
5
6
DROP TABLE IF EXISTS mst_users_with_dates;
CREATE TABLE mst_users_with_dates (
user_id varchar(255)
, register_stamp varchar(255)
, birth_date varchar(255)
);
1
select * from mst_users_with_dates;

위 코드로 테이블이 잘 만들어졌는지 확인해본다.

잘 만들어졌으면, 데이터를 삽입한다.

1
2
3
4
5
6
INSERT INTO mst_users_with_dates
VALUES
('U001', '2016-02-28 10:00:00', '2000-02-29')
, ('U002', '2016-02-29 10:00:00', '2000-02-29')
, ('U003', '2016-03-01 10:00:00', '2000-02-29')
;

먼저 날짜 데이터들의 차이를 계산해보자. 현재 날짜와 등록한 날짜를 빼주는 방식이다.

1
2
3
4
select user_id, CURRENT_DATE AS today, 
date(timestamp(register_stamp)) AS regitser_date,
datediff(CURRENT_DATE(), date(register_stamp)) AS diff_days
from mst_users_with_dates ;

이렇게 만들어주면 원하는 결과가 나온다. 책에 있는 결과와 조금은 다를 수 있는데, 왜냐하면 CURRENT_DATE를 하면 현재의 날짜를 가져와 주기 때문에, 책에 있는 2017-02-05가 아니라, 지금 작성하고 있는 2019-05-21로 계산된다.

여기까지는 datediff함수가 MySQL에도 있기 때문에 책에 있는 그대로 쳐도 잘 돌아간다.

이번에는 사용자의 생년월일로 나이를 계산해보자.
나이를 계산하기 위한 전용함수가 구현되어 있는 것은 PostgreSQL뿐이다. PostgreSQL에는 age함수가 구현이 되어있어 편하게 나이를 구할 수 있다. MySQL의 경우에는 책에 있는 코드를 MySQL의 언어로 변형시켜 주어야 한다.

1
2
3
4
5
SELECT user_id, 
CURRENT_DATE AS today, date(register_stamp) as register_date, birth_date,
(YEAR(CURRENT_DATE)-YEAR(birth_date))- (RIGHT(CURRENT_DATE,5)<RIGHT(birth_date,5)) AS age,
(YEAR(register_stamp)-YEAR(birth_date))- (RIGHT(register_stamp,5)<RIGHT(birth_date,5)) AS register_age
FROM mst_users_with_dates;

책에 있는 코드와 다른점은 EXTRACT를 사용하지 않았다는 것이다. MySQL에는 EXTRACT가 없기 때문에 년도를 이용해 일일이 계산해 주어야 한다. YEAR함수를 이용해 년도만 가져와서 계산해준다. 주의해야 할 점은, YEAR함수에 today를 넣어주는 게 아니라 CURRENT_DATE를 넣어주어야 한다는 것이다. today를 넣어주면 syntax에러가 발생한다.

하지만 YEAR로 계산한 경우 연 부분만의 차이가 계산되므로, 해당 연의 생일을 넘었는지 제대로 계산이 되지 않는 문제가 발생한다.

1
2
3
4
5
SELECT user_id,
substring(register_stamp, 1, 10) as register_date, birth_date,
floor(( cast(replace(substring(register_stamp, 1,10), '-', '') AS unsigned) - cast(replace(birth_date, '-', '') AS unsigned)) / 10000) as register_age,
floor(( cast(replace(CAST(CURRENT_DATE as signed), '-', '') as unsigned) - cast(replace(birth_date, '-', '') AS unsigned)) / 10000) as current_age
from mst_users_with_dates;

이 코드로 실행시켜주면 문제가 해결된다. MySQL에서는 CAST함수 실행시에 주의해야 할 점이 있는데, 보통 프로그래밍 언어에서는 Integer나 String등으로 타입을 정해주는데, MySQL에서는 UNSIGNED–>INTEGER이고, SIGNED–>STRING임을 명심해야 한다. 코드를 바꿔주고 실행하면 문제없이 돌아가는 것을 확인할 수 있다.

cross_entropy_KL_divergence

Cross Entropy와 KL-Divergence에 대해서 알아보자

Cross Entropy

크로스 엔트로피에 대해서 알아보기 전에, 엔트로피 식을 다시한번 확인해 보자
엔트로피는 $H(x)=-\sum P(x)log_2P(x)$로 확률분포 $P(X)$에 대한 기댓값이다. 엔트로피는 확률분포가 있어야 정의가 될 수 있다. 확률 분포의 불확실한 정도를 뜻하는 것이라고 생각하면 된다.

이제 크로스 엔트로피 식을 확인해 보자
$H(P,Q)=-\sum_{X} P(x)logQ(x)$(자연로그 또는 이진로그)
식을 자세히 보면, $P(x)$가 들어갈 자리에 $Q(x)$가 들어가 있다. 어떤 의미가 숨어져 있는 것 같은데,

이 수식이 의미하는 것이 무엇일까?

크로스 엔트로피(Cross Entropy)는 실제 데이터는 $P$의 분포로부터 생성되지만, 분포 $Q$를 사용해서 정보량을 측정해서 나타낸 평균적 bit수를 의미한다. 이제 수식이 눈에 들어오기 시작할 것이다.

실제 데이터는 분포 P로 부터 생성되는데, 우리가 실제 P에 대해서 몰라서 분포 Q의 정보(or 코딩 스킴)을 대신 활용하면 어떨까?에 대한 답으로써 만들어졌다고 생각하면 편할 것이다.

크로스 엔트로피는 $H(P,Q)$와 같이 나타내고 일반적으로 $H(P,Q) >=H(P)$이다. 항상 크로스 엔트로피가 크거나 같을 수 밖에 없는 것은 데이터의분포를 Q로 가정한 코딩방식을 사용하게 되면, 실제의 분포 P를 가정한 코딩방식 보다 질의응답에 필요한 코드의 수(code length)가 많아지게 되기 때문이다.

KL-Divergence

KL-Divergence는 쿨백 라이블러 발산이라고 불리기도 한다. 이 역시 수식으로 먼저 확인해 보자
$D_{KL}(P||Q)=\sum_{X}P(x)log {P(x)\over{Q(x)}}$이다. 이 수식을 자세히 보면, Cross entropy 식이 들어가 있는 것을 확인 할 수 있다. 좀 더 풀어서 써보면

$D_{KL}(P||Q)=\sum_{X}P(x)log{1\over Q(x)}-P(x)log{1\over P(x)}$로
결국 $H(P,Q) - H(P)$, 즉 P와 Q의 크로스엔트로피에서 P의 엔트로피를 빼준 식이다. 이것은 결론적으로 Q를 이용한 정보량이 P의 분포와 얼마나 차이가 나는 지를 알려주는 것이다. 일종의 분포사이의 거리로 이해를 하면 된다. (KL divergence는 두 확률 분포 P와 Q의 차이를 측정한다. 하지만 엄밀히 말해서 거리는 아니다.)

다른 표현으로 데이터 인코딩 관점에서 보면 KL divergence는 데이터 소스의 분포인 P 대신 다른 분포 Q를 사용해서 인코딩하면 추가로 몇 bit의 낭비가 생기는지 나타낸다고 이해할 수 있다.

KL-Divergence는 거리함수가 아니다. 왜냐하면 교환법칙이 성립하지 않기 때문이다. Reverse KL은 별도의 개념으로 사용된다. 하지만, 두 분포가 다를수록 큰 값을 가지며 둘이 일치할 때에만 0이 되기 때문에 거리와 비슷한 용도로 사용할 수 있다.
[https://wiseodd.github.io/techblog/2016/12/21/forward-reverse-kl/]

Cross Entropy와 KL-Divergence가 어떤 관계에 있느냐고 묻는다면, KL-Divergence의 앞쪽 수식에 크로스 엔트로피가 있으므로, 크로스 엔트로피가 작을 수록, KL-Divergence값이 작아진다. 즉, 두 분포가 가까워진다고 말할 수 있겠다.

Boosting

부스팅 기법(Boosting)에 대해 알아보자

Weak Learner

부스팅 기법에 대해 알아보기 전에 알아야 할 몇가지 용어들이 있다. 그 중 하나가 Weak Learner이다.
Weak Learner는 다른 말로 Simple Learner이라고도 불리우며, 간단한 학습기 정도로 보면 될 것이다.

대표적인 Weak Learner는 다음과 같다.

  • Decision stumps : depth가 1인 decision tree
  • Shallow decision trees
  • Naïve Bayes
  • Logistic regression

우리는 Weak Learner를 많이 가질 것이고, 많은 학습기들을 이용해서 예측작업을 할 것이다.
Weak Learner들의 앙상블을 통해 어떤 결과를 예측해보려는 것이다. 이렇게 다수의 Weak Learner들을 이용해서 학습하면, input space의 다른 부분들을 보완해줄 수 있다.

부스팅에 대해서 공부할 때 배깅이 자주 등장하는데 차이를 비교해보자면 다음과 같다.

Bagging

  • 훈련 데이터에서 다수의 표본을 추출하고, 개별 트리의 결과를 병합하여 단일 예측 모델을 생성
  • 각 bootstrap 과정이 독립적이므로 병렬 처리가 가능

Boosting

  • Bagging과는 달리 순차적으로 시행되며 bootstrap 과정이 없음
  • Original dataset에서 약간의 수정을 거친 데이터를 개별적으로 학습한 후, 결합되어 강력한 분류기 생성

부스팅의 아이디어는 간단하다. 약한 학습기들을 이용해서 학습된 학습기들을 결합해 strong learner를 만드는 것이다. Classifier의 경우 학습기들의 결합방법은 Majority voting방식이 될 것이고, Regressor의 경우 학습기들의 결합방법은 평균이 될 것이다.

부스팅의 알고리즘도 심플하다. 앙상블 내, $t$번째 분류기 $c_t$와 $t+1$번째 분류기 $c_{t+1}$이 연관성을 가지고 생성하는 것이다.

  1. 훈련데이터 X의 샘플을 $c_t$가 옳게 분류하는 것과, 그렇지 않은 것으로 나눈다.
  2. 옳게 분류하는 샘플들은 인식이 가능하므로 가중치를 낮춘다.
  3. 틀리게 분류하는 샘플들은 가중치를 높인다.
  4. $c_{t+1}$학습시키기 위한 정책으로 sampling과정에서 가중치가 높은 샘플이 뽑힐 확률이 높아지게 한다.

Ada Boost(Adaptive Boosting)

Yoav Freund & Robert Schapire가 제안하였고, Weak learner를 반복적으로 사용하고, 그 결과를 합하여 모델의 accuracy를 향상시킨다.

AdaBoosting은 위에서 살펴본 알고리즘이 작동하는 방식과 거의 비슷하게 동작한다.

그림과 함께 살펴보자
Ada Boosting의 작동원리
첫번째 그림에서 약한 학습기인, 결정 그루터기가 하나의 결정경계를 가지고 +와 -를 나누고 있다. 이렇게 나눴을 때 위쪽의 3개의 +들은 잘못 분류가 되어 가중치가 높아진다. 두번째 그림에서는 오른쪽의 - 두 개만 잘 분류가 되었고 결정경계 왼쪽의 대개의 -들은 잘못 분류가 되어버렸다. 이 역시 가중치가 높아지고 학습된 3개의 Weight를 결합해서 + -를 잘 분류해내는 하나의 강 분류기를 만들어낸다. 결국 Weak Learner들의 앙상블이다.

가중치 업데이트 규칙은 다음과 같다.
$w^{(i)} = w^{(i)}$, $\hat{y_j}{(i)}= y_{(i)}$ 일때
$w^{(i)} = w^{(i)}exp(\alpha_j)$, $\hat{y_j}{(i)}\neq y_{(i)}$ 일때
그런 다음 모든 샘플의 가중치를 정규화 한다.(즉, $\sum_{i}^{m}w^{(i)}$로 나눠준다.)

마지막으로 새 예측기가 업데이트된 가중치를 사용해 훈련되고 전체 과정이 반복된다. 새 예측기의 가중치가 계산되고 샘플의 가중치를 업데이트해서 또 다른 예측기를 훈련시키는 방식이다.

Adaboost는 지정된 예측기 수에 도달하거나 완벽한 예측기가 만들어지면 중지된다.

Adaboost의 예측은 $\hat{y}(x)=\sum_{i=1}^{N}\alpha_j$로 이루어진다.($N$은 예측기의 수)

Gradient Boosting

그래디언트 부스팅은 Ada부스팅처럼 이전까지의 오차를 보정하도록 예측기를 순차적으로 추가한다. 하지만 Ada처럼 반복마다 샘플의 가중치를 수정하는 대신 이전 예측기가 만든 잔여 오차(residual error)에 새로운 예측기를 학습시킨다. 다시말해서 약한 분류기가 이전 학습에서 발견하기 어려웠던 문제성 관측값, 즉, 예측이 틀린 관측값에 집중하게 하는 것이다.

다른 boosting 기법처럼 모델을 단계적으로 구축해 나가는 것은 같지만 임의의 미분 가능한 손실 함수를 최
적화하는 문제로 일반화한 방법이다. GB는 여러개의 간단한 모델의 ensemble을 학습한다.

Motivation of Gradient Boosting

($x_1$,$y_1$),($x_2$,$y_2$) …, ($x_n$,$y_n$) 총 n개의 데이터가 있고, 이 데이터를 이용하여 회귀모형 $F(x)$ 를 학습하는 프로젝트를 진행한다고 생각해보자. 팀원이 모델 $F$를 만들었다. 하지만 성능이 그다지 좋지 않다. $F(x_1)$ = 0.8의
예측값을 생성한다. 하지만 실제 $y_1$ = 0.9이다. $y_2$ = 1.3인데, $F(x_2)$ = 1.4의 값이 나온다. 이 모델의 성능을
향상시켜야 하는데 한가지 제약조건이 있다. 팀원이 만든 모델 $F$는 절대 건드리지 않고, 모델을 향상시켜야
한다. 어떤 방법이 있을까?

방법은 간단하다. 원래 모델은 그냥 두고, 차이만큼을 더해주는 함수 $h(x)$를 만들어주면 되는 것이다.
완벽하게 우리의 목적을 달성시키지는 못하지만, 근사적으로 달성할수는 있다.

그렇다면$h(x)$는 어떻게 구할 수 있을까?
$h(x_1) = y_1 - F(x_1)$
$h(x_2) = y_2 - F(x_2)$
$…$
$h(x_n) = y_n - F(x_n)$ 이므로,$(x1, y_1-F(x_1))$, $(x2, y_2-F(x_2))$,$…,$,$(x_n, y_n-F(x_n))$을 학습하면 된다.

학습데이터를 이용하여 75%정도의 정확도까지 모델을 학습하고, 나머지 미설명 부분은 오차항에 남겨둔다.
$Y = F(x) + E$
오차항을 이용하여 다른 모델을 학습시킨 후, 그 전 모델에서는 미설명 부분이었으나 이번 학습에서는 설명
이 되는 부분을 찾아내 원 모델에 추가한다. 단, 추가 모델은 반드시 전체 정확도를 향상시켜야만 한다.
$Gradeint(E) = G(x) + E2$

모델이 약 80%의 정확도를 갖게 되면 식은 다음과 같게 된다.
$Y + F(x) + G(x) + E2$

이런 방법을 계속해서 사용해 나가고, GB는 단순 합보다 가중 평균을 사용하여(다른 모델보다 정확도가 높은 예측 결과를 가진 모델에 더 높은 중요도 점수를 부여) 모델의 정확도를 더 개선할 수 있다
$Y=\alpha{F(x)}+\beta{G(x)}+\gamma{H(x)} + E$ $…$

Gradient Boosting의 Loss Function

손실 함수는 해결하려는 문제에 따라 다르다. 부스팅에서는 처음부터 최적화를 하는 것이 아니라, 각 단계별로 이전 단계에서 설명되지 못한 손실에 관해 최적화를 수행한다.

  • 회귀 문제 : Least squares method (최소 자승법)
  • 분류 문제 : Log loss function (로그 손실 함수)

손실 함수를 최소화하기 위해 약한 분류기를 추가할 가법 모델(additive model)

  • 기존 트리는 변동이 없고 새로운 트리가 하나씩 추가된다.
  • 기울기 하강 절차가 사용되어 트리가 추가될 때의 손실을 최소화한다.

Leaf node마다 가중치, score가 부여가 된다. Gini계수 등을 사용하지 않는다.
분류 / 회귀 : Sklearn에서는 (friedman) mse를 사용한다.

Information_Theroy_Entropy

정보이론 기초, 정보량(Information)과 엔트로피(Entropy)에 대해 알아보자

정보량 (Information)

정보량은 말 그대로 얼마나 정보를 갖고 있는 지를 뜻하는 말이고 정보이론이란 불확실성을 다루는 학문이다. 하지만 일상에서 정보량에 대해서 접하기는 상당히 힘들고, 정보량이라는 단어를 일상에서 사용하는 사람은 매우 드물다.

알기 쉬운 예를 들어보자.

1
2
간만에 친구들과 약속을 잡아서 놀기로 했다. 12일 13일 14일 15일 중으로 날짜를 잡기로 했고
카카오톡 투표를 통해서 가장 많이 나온 날짜를 약속날로 잡자고 했다.

흔히 있는 상황이다. 약속날 후보로 5월 12일 13일 14일 15일이 있다고 해보자. 총 4개의 옵션이 있는 것이다. 근데 투표를 만든 사람이 자비롭게 중복투표를 허용해놨고, ‘음 난 다좋은데~’라고 생각하는 주관없는 친구가 모든 날짜를 다 눌러놨다고 생각해보자.

이 친구의 투표가 가진 정보량은 얼마일까?
직관적으로 생각했을 때 0이다. 하지만 수학적으로 왜 그런 것일까?

정보량의 공식을 보자.
정보량 $h(x) = \sum_{x}log_2p(x)$ 이다.

이 공식을 토대로 주관없는 친구의 5월 12일 날짜에 대한 정보량을 구해보면,
$p(x) = 1$이므로, $log_21 = 0$이란 값이 나온다.
13일, 14일, 15일 모두 같은 결과가 나오고, 주관없는 친구의 투표에 대한 정보량은 0이다.

어떻게 보면 어떤 사람의 주관은 일정의 정보량을 뜻하는 듯하다. 아무거나 빌런은 결국 어떤 정보도 갖고 있지 않다는 것이다.

정보량은 여기서 주관을 뜻하기도 하지만, 보통 정보량은 놀라움의 정도를 뜻한다.
축구 경기중에 골키퍼가 골을 넣는 사건은 굉장히 놀랍다. 이는 굉장히 정보량이 많다는 것을 뜻한다.
왜냐면 정보량은 확률에 반비례하기 때문이다.
Information with Probability

이번에는 예를 바꿔서, 우리가 쉽게 알 수 있는 주사위 case를 갖고 와 보자.
주사위를 던져서 짝수가 나타날 사상 $E_1$의 정보량은 몇일까?
공식에 의해서 $p(x) = {1\over2}$이므로
$P(E_1) = {1\over2}\longrightarrow I = -log_2{1\over2}=1(bit)$ 가 된다.

엔트로피(Entropy)

엔트로피는 흔히 열역학에서 자주 볼 수 있는 단어지만, 정보이론에서도 사용되는 말이기도 하다. 엔트로피라는 말에 대해서는 정보이론의 아버지인 Shannon이 정립하였다.

엔트로피의 공식을 먼저 확인해보자.
$H(X) = -\sum_{X}P(X)log_2P(X)$이다.

확률과 통계를 기본부터 잘 다져온 사람이라면 익숙한 공식이 눈에 들어올 것이다.
바로 기댓값이다. 수식을 그럼 천천히 다시봤을때, 엔트로피 공식이 뜻하는 것은 바로 확률분포 $P(X)$에 대한 기댓값이다. 확률분포가 있어야 정의가 될 수 있다. 확률 분포의 불확실한 정도를 뜻하는 것이라고 생각하면 된다.

엔트로피는 정보이론에서 사용되는 단어이므로, 이 역시 불확실도를 나타내는 척도로 사용된다.
직관적으로 이해하기 위해 그림을 통해 살펴보자.

Entropy Distribution

위 그림에서 보면 왼쪽의 분포는 몰려있고, 즉 정규분포로 따지자면 표본오차가 작은 모양이고, 오른쪽의 분포는 넓게 퍼진, 표본오차가 매우 큰 모습이다. 정보이론을 따라 분포를 다시 보면 왼쪽의 그림은 불균형한 분포로 불확실성이 적은 모양이다. 다시말해 엔트로피 값이 낮은 분포이다. 반면에 오른쪽 그림은 균등한 분포이며, 어떤 값이 나올지 모르는, 불확실성이 높은 모양이다. 즉, 엔트로피 값이 높은 분포라고 할 수 있다.

결론적으로, 엔트로피는 확률분포 P(X)에서 일어날 수 있는 모든 사건들의 정보량의 기댓값으로, P(X)의 불확실성 정도를 평가하는 척도로 사용된다.

엔트로피와 관련된 것으로 크로스 엔트로피(Cross-Entropy)가 있는데, 이것은 다음 포스트에 적도록 하겠다.

P.S 다시 엔트로피와 크로스 엔트로피에 대해 공부한 이유는, 면접을 최근에 보게 되었는데 이 부분에 대해서 제대로 공부를 하지 못해 대답을 우물쭈물 했기 때문이다. 데이터 사이언스를 공부하면서 느끼는 것은 항상 이런 것이다. 내가 진짜 알고있는지 아닌지 확인하기 어렵다는 것이다. 최대한 많이 부딪혀 봐야겠다. 그것이 캐글이 되었든, 아니면 면접이 되었든, 실제로 일을 하는 것이든, 직접 경험해 봐야 많이 필요성을 느낄 수 있고 많이 배울 수 있게 되는 것 같다.