기울기소멸 Vanishing gradient

정의

- In machine learning, the vanishing gradient problem is encountered when training artificial neural networks with gradient-based learning methods and backpropagation.

이해

- 당연한것 아닌가?

  • 그레디언트 기반의 학습 (그레디언트 기반의 옵티마이저): 손실함수의 기울기를 통하여 업데이트 하는 방식
  • 역전파: 손실함수의 기울기를 구하는 테크닉 (체인룰 + $\alpha$). 구체적으로는 (1) 손실함수를 여러단계로 쪼개고(여러 합성함수로 표현하고) (2) 각 단계의 미분값을 각각 구하고 (3) 그것들을 모두 곱하여 기울기를 계산한다.
  • 0 근처의 숫자를 계속 곱하면 터지거나 0으로 간다. (사실 안정적인 기울기가 나올 것이라고 생각하는것 자체가 사실 이상함)
import numpy as np 
grads = np.random.uniform(low=-2,high=2,size=100) 
grads
array([ 1.25061919,  1.06949841,  1.35311978,  0.58158985,  1.76683863,
       -0.151217  , -1.14061494,  1.36810518,  0.0398364 , -0.43395175,
        0.72469629, -1.48156131, -0.594101  ,  1.25013534,  1.84981613,
       -0.91010133,  0.82005588,  0.65678338,  1.00464755, -1.17373755,
        1.55499399, -1.02264197, -1.07086263, -1.75650821,  1.31665262,
        1.1641988 , -1.15956353,  1.5431544 ,  1.60339074, -0.52551961,
       -1.74771219,  0.09466559,  1.04927929,  1.24797762, -1.95957155,
        1.6131183 ,  0.48512285, -0.69409283, -0.80991242,  1.05306652,
       -0.7497829 , -1.2826751 , -0.9659906 , -0.10317097, -0.43583316,
        0.53283005, -1.27581754,  1.06188645, -0.59590509, -1.50368236,
       -0.52279979,  1.35875764,  0.48325635,  0.69482476,  0.04276697,
        0.5990455 ,  0.71583573,  1.29761969, -1.74874052, -0.24522533,
        0.80958567, -0.18179041, -1.79318476, -1.46738069, -1.238332  ,
       -0.94801909, -1.84434559,  0.28968162,  1.97567079, -0.66347898,
        1.34647117, -0.50126953, -1.01165531,  0.95103219,  1.65630128,
        0.89124097, -0.42934255,  1.83242969,  1.58535197,  1.729627  ,
       -1.57923394,  0.53882344,  1.78275255,  0.92732809,  1.93114402,
        0.397884  , -0.28289665, -0.09000512,  1.52746467,  1.82483229,
        1.52212373,  1.15494177, -1.57815235,  1.46995688, -0.18524338,
       -1.77016608, -0.950607  , -0.65663769, -0.79979375,  1.68782803])
grads.prod()
3.292214486300399e-08

몇 번 해봐도 0이 나오는 형상

  • 기울기가 소멸함
grads = np.random.uniform(low=-5,high=5,size=100) 
grads.prod()
-1.5317453661642396e+19

0에 까까울 수도, 엄청 큰 수가 나올수도...

  • 기울기가 폭발함.
grads = np.random.uniform(low=-1,high=3.5,size=100) 
grads.prod()
-0.0013960825728759768

- 도깨비: 기울기가 소멸하기도 하고 터지기도 한다.

해결책 (기울기 소멸에 대한 해결책)

Note: 잘 정리되어있는것이 없어서 제 머리속에서 정리했습니다. 부정확할수도 있어요

- 개념

  • 데이터 $\to$ (아키텍처,손실함수,역전파,업데이트)

- Multi-level hierarchy

  • 여러층을 쪼개서 학습하자 $\to$ 어떻게? 사전학습, 층별학습
  • 기울기소실문제를 해결하여 딥러닝을 유행시킨 태초의(?) 방법임.
  • 결국 입력자료(위에서는 데이터)를 바꾼뒤에 학습하는 형태

- Faster hardware

  • GPU를 중심으로 한 테크닉
  • 근본적인 문제해결책은 아니라는 힌튼의 비판
  • CPU를 쓸때보다 GPU를 쓰면 약간 더 깊은 모형을 학습할 수 있다 정도?

- Residual networks

  • 훌륭한 접근법중 하나임
  • 아키텍처를 변경하는 방법이지만, 사실상 손실함수를 부드럽게 만드는 기법으로 이해해도 된다.
  • 솟컷이라는 아키텍처를 추가하여 이리저리 실험해보니까 손실함수가 부드러워졌다. <--- 이런게 아니고
  • 손실함수를 부드럽게 하기 위해서는 층별의 차이(residual)를 학습하는게 유리할 것 같다. 그런데 이것을 위한 효과를 주기 위해서는 단지 아키텍처에 숏만만 추가하면 되겠다. <--- 이런 모티브였을 것이다.

ref: Image of residual net loss function

- Other activation functions

  • 렐루의 개발

ref: Image of ReLU vs Sigmoid vanishing

4주차 ReLU 문제 보기

- 배치정규화

  • 어쩌다보니 되는것.
  • 배치정규화는 원래 공변량 쉬프트를 잡기 위한 방법임. 그런데 기울기 소멸에도 효과가 있음. 현재는 기울기소멸문제에 대한 해결책으로 빠짐없이 언급되고 있음. 2015년의 원래 논문에는 기울기소멸에 대한 언급은 없었음. (https://arxiv.org/pdf/1502.03167.pdf)
  • 심지어 배치정규화는 오버피팅을 잡는효과도 있음 (이것은 논문에 언급했음)

- 기울기를 안구하면 안되나?`

기울기 없이 최솟값 구하기, 방법이 개발되어 있지 않기도 하고,..

추천시스템

import

import pandas as pd 
import torch 
from fastai.collab import * 
from fastai.tabular.all import * 

Data (시뮬레이션)

df=pd.read_csv('https://raw.githubusercontent.com/guebin/2021BDA/master/_notebooks/2021-11-30-recommend.csv')
df
user item rating item_name
0 1 15 1.084308 홍차5
1 1 1 4.149209 커피1
2 1 11 1.142659 홍차1
3 1 5 4.033415 커피5
4 1 4 4.078139 커피4
... ... ... ... ...
995 100 18 4.104276 홍차8
996 100 17 4.164773 홍차7
997 100 14 4.026915 홍차4
998 100 4 0.838720 커피4
999 100 7 1.094826 커피7

1000 rows × 4 columns

user-item matrix

- 아래와 같은 매트릭스를 고려하자.

df2 = pd.DataFrame([[None]*20]*100,columns=['커피'+str(i) for i in range(1,11)]+['홍차'+str(i) for i in range(1,11)]) 
df2.index = pd.Index(['user'+str(i) for i in range(1,101)])
df2
커피1 커피2 커피3 커피4 커피5 커피6 커피7 커피8 커피9 커피10 홍차1 홍차2 홍차3 홍차4 홍차5 홍차6 홍차7 홍차8 홍차9 홍차10
user1 None None None None None None None None None None None None None None None None None None None None
user2 None None None None None None None None None None None None None None None None None None None None
user3 None None None None None None None None None None None None None None None None None None None None
user4 None None None None None None None None None None None None None None None None None None None None
user5 None None None None None None None None None None None None None None None None None None None None
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
user96 None None None None None None None None None None None None None None None None None None None None
user97 None None None None None None None None None None None None None None None None None None None None
user98 None None None None None None None None None None None None None None None None None None None None
user99 None None None None None None None None None None None None None None None None None None None None
user100 None None None None None None None None None None None None None None None None None None None None

100 rows × 20 columns

item(커피1 ~ 커피10, 홍차1 ~ 홍차10)이 20개, user가 100명

i = 1
j = 1
df.query('user == @i and item == @j')['rating'].to_list()[0]
4.149208812750556
for (i,j) in zip(df.user.to_list(), df.item.to_list()):
    df2.iloc[i-1,j-1]=df.query('user == @i and item == @j')['rating'].to_list()[0]

안 먹었으면 none 그대로 넣고, 먹었으면 기존 df에서 가져와서 rate 넣어주자

  • i
df2.iloc[:5]
커피1 커피2 커피3 커피4 커피5 커피6 커피7 커피8 커피9 커피10 홍차1 홍차2 홍차3 홍차4 홍차5 홍차6 홍차7 홍차8 홍차9 홍차10
user1 4.149209 None None 4.078139 4.033415 4.071871 None None None None 1.142659 1.109452 None 0.603118 1.084308 None 0.906524 None None 0.903826
user2 4.031811 None None 3.822704 None None None 4.07141 3.996206 None None 0.839565 1.011315 None 1.120552 0.91134 None 0.860954 0.871482 None
user3 4.082178 4.196436 None 3.956876 None None None 4.450931 3.97209 None None None None 0.983838 None 0.918576 1.206796 0.913116 None 0.956194
user4 None 4.000621 3.89557 None 3.838781 3.967183 None None None 4.105741 1.147554 None 1.34686 None 0.614099 1.297301 None None None 1.147545
user5 None None None None 3.888208 None 3.97033 3.97949 None 4.010982 None 0.920995 1.081111 0.999345 None 1.195183 None 0.818332 1.236331 None
  • user1~user5는 대체로 커피를 선호한다.
  • user1은 커피 2,3,7,8,9,10을 먹지는 않았는데, 아마 먹었다면 높은 점수를 주었을 것임.
df2.iloc[47:53]
커피1 커피2 커피3 커피4 커피5 커피6 커피7 커피8 커피9 커피10 홍차1 홍차2 홍차3 홍차4 홍차5 홍차6 홍차7 홍차8 홍차9 홍차10
user48 None None 3.718709 None None None 4.484278 4.242403 None 3.654447 0.981285 1.089422 0.945297 None 1.367151 0.984146 None 0.779323 None None
user49 4.012018 4.347253 3.752401 None None None 4.182699 3.763614 4.112049 None None 0.921155 0.947049 0.937485 None None 1.046802 None None None
user50 4.208589 None 3.995654 3.85952 4.186735 None None 3.955416 4.017231 4.312376 None None 1.148465 None None None 1.2535 None None 0.985061
user51 None None 1.158754 None None 0.961621 0.958685 0.986134 1.207339 0.799213 None 4.053459 None None 4.214456 3.94822 None 4.168223 None None
user52 0.794371 None None None None 0.72175 None 0.99541 None None None None 4.713239 4.061688 3.791844 4.008471 3.96703 4.420732 4.055309 None
user53 None 0.586384 1.170372 0.939492 None None 1.299762 None None 0.818039 4.155103 4.131003 None 3.839847 3.859583 None None 3.873978 None None
  • user1 $\sim$ user50은 커피를 선호하고 user51 $\sim$ user100은 홍차를 선호함
df2.iloc[94:]
커피1 커피2 커피3 커피4 커피5 커피6 커피7 커피8 커피9 커피10 홍차1 홍차2 홍차3 홍차4 홍차5 홍차6 홍차7 홍차8 홍차9 홍차10
user95 1.190142 None 0.818878 None 1.150194 None None 1.287971 None 0.586373 None None 4.06455 4.399665 None None 3.573139 3.944602 None 4.012274
user96 0.511905 1.066144 None 1.31543 None 1.285778 None 0.6784 1.02302 0.886803 None 4.055996 None None 4.156489 4.127622 None None None None
user97 None 1.035022 None 1.085834 None 0.812558 None 1.074543 None 0.852806 3.894772 None 4.071385 3.935935 None None 3.989815 None None 4.267142
user98 None 1.115511 None 1.101395 0.878614 None None None 1.329319 None 4.12519 None 4.354638 3.811209 4.144648 None None 4.116915 3.887823 None
user99 None 0.850794 None None 0.927884 0.669895 None None 0.665429 1.387329 None None 4.329404 4.111706 3.960197 None None None 3.725288 4.122072
user100 None None 1.413968 0.83872 None None 1.094826 0.987888 None 1.177387 3.957383 4.136731 None 4.026915 None None 4.164773 4.104276 None None

예측?

- None의 값을 추론하는 방법? 머리속으로는 알고있음. 그런데 어떻게 수식화 할지?

df2.loc['user1']
커피1     4.149209
커피2         None
커피3         None
커피4     4.078139
커피5     4.033415
커피6     4.071871
커피7         None
커피8         None
커피9         None
커피10        None
홍차1     1.142659
홍차2     1.109452
홍차3         None
홍차4     0.603118
홍차5     1.084308
홍차6         None
홍차7     0.906524
홍차8         None
홍차9         None
홍차10    0.903826
Name: user1, dtype: object

- 유저의 취향은 예를들면 아래와 같이 표현할 수 있다.

  • user1의 취향 = (커피좋아함, 홍차싫어함) = (0.8, 0.21)

- 코딩해보자.

user1_prprnc = (0.8,0.21) 
user1_prprnc
(0.8, 0.21)

- 여기에서 (0.8,0.21)은 각각 (커피음료,홍차음료)와 같은 음료의 특징으로 해석가능. 현재는 단순하게 원소가 2인 벡터이지만 다양한 feature도 가능하다.

  • 음료의특징: (커피음료, 홍차음료, 우유포함, 설탕포함정도, 아이스여부, 행사여부)

- 아이템4(커피4)와 아이템11(홍차1)의 특징은 예를들면 아래와 같이 표현할 수 있다.

item4_ftr = (0.71, 0.03) # 커피4 
item11_ftr = (0.1, 0.88) # 홍차1  

- 유저의 선호도와 아이템의 feature를 내적하여보자.

np.array(user1_prprnc) @ np.array(item4_ftr) 
0.5742999999999999
np.array(user1_prprnc) @ np.array(item11_ftr) 
0.26480000000000004

- 유저의 선호도와 item의 feature가 비슷할수록 값이 높게 나온다. 실제로 user1이 커피4와 홍차1에 매긴 평점은 (4.078139,1.142659) 인데 (0.5743, 0.2648) 가 실제평점과 비슷하게 나오도록 하면 좋겠다.

- 적당히 값을 조정하여 보자.

user1_prprnc = (0.8,0.21) 
item4_ftr = (5, 0.03) # 커피4 
item11_ftr = (0.1, 4.8) # 홍차1
np.array(user1_prprnc) @ np.array(item4_ftr) 
4.0063
np.array(user1_prprnc) @ np.array(item11_ftr) 
1.088

- 신기한것은 이 파라메터들이 있다면 None에 대한 추론도 가능하다는 것이다. 예를들어서 user2는 홍차1을 먹지 않았지만 item11_ftr(홍차1)의 coefficient(계수값)이 다른유저를 통해 학습되었다면 user2가 홍차1을 먹고 내릴 평점이 예측가능하다.

df2.loc['user2']
커피1     4.031811
커피2         None
커피3         None
커피4     3.822704
커피5         None
커피6         None
커피7         None
커피8      4.07141
커피9     3.996206
커피10        None
홍차1         None
홍차2     0.839565
홍차3     1.011315
홍차4         None
홍차5     1.120552
홍차6      0.91134
홍차7         None
홍차8     0.860954
홍차9     0.871482
홍차10        None
Name: user2, dtype: object
user2_prprnc = (0.77,0.18) 
np.array(user2_prprnc) @ np.array(item11_ftr)
0.9410000000000001

- 생각해보니까 user1_prprnc, ... , user100_prprnc, item1_ftr, ... , item20_ftr를 ${\bf W}$라고 해석하면 우리가 ${\bf W}$ 바꿈에 따라 예측값과 손실이 결정되므로 네트워크 구조가 만들어진다.

- 정리하면 적당한 아래와 같은 매트릭스를 정의하여

  • ${\bf U} = \begin{bmatrix} \tt user1~ prprnc \\ \dots \\ \tt user100~prprnc \end{bmatrix}$ $\quad 100 \times 2$ mat
  • ${\bf V} = \begin{bmatrix} \tt item1~ ftr \\ \dots \\ \tt item20~ftr \end{bmatrix}$ $\quad 20 \times 2$ mat

${\bf U}{\bf V}^\top \approx $ df2 가 만족하도록 하면 된다. (None은 무시) $\rightarrow$ 100$\times$20 matrix

fastai를 학습: df $\to$ dls

  • user의 인덱스
  • item 인덱스
  • rating -item 이름
dls=CollabDataLoaders.from_df(df,bs=100) 
dls.items
user item rating item_name
327 33 17 0.963251 홍차7
896 90 12 4.391382 홍차2
667 67 16 4.016279 홍차6
358 36 7 4.341957 커피7
599 60 13 3.856908 홍차3
... ... ... ... ...
77 8 10 4.392112 커피10
127 13 12 0.601409 홍차2
910 92 6 1.118395 커피6
851 86 4 0.917732 커피4
608 61 11 3.909074 홍차1

800 rows × 4 columns

fastai를 통한 학습: learn

lrnr = collab_learner(dls,n_factors=2,y_range=(0,5))
lrnr.fit(30,0.01)
epoch train_loss valid_loss time
0 2.331800 2.332946 00:00
1 2.300353 2.327208 00:00
2 2.260273 2.284232 00:00
3 2.202531 2.197926 00:00
4 2.123347 2.063728 00:00
5 2.020970 1.876104 00:00
6 1.893997 1.643050 00:00
7 1.745477 1.373834 00:00
8 1.579301 1.088966 00:00
9 1.403262 0.812427 00:00
10 1.226403 0.570626 00:00
11 1.057899 0.386377 00:00
12 0.904757 0.261071 00:00
13 0.770549 0.183208 00:00
14 0.655766 0.138373 00:00
15 0.558695 0.114488 00:00
16 0.477071 0.101892 00:00
17 0.408472 0.095397 00:00
18 0.350721 0.091764 00:00
19 0.302058 0.089963 00:00
20 0.260959 0.087893 00:00
21 0.226200 0.085657 00:00
22 0.196771 0.083802 00:00
23 0.171794 0.082128 00:00
24 0.150561 0.080108 00:00
25 0.132525 0.078216 00:00
26 0.117140 0.076819 00:00
27 0.104016 0.075675 00:00
28 0.092806 0.074477 00:00
29 0.083250 0.073452 00:00
lrnr.show_results()
user item rating rating_pred
0 46.0 13.0 0.996056 0.995639
1 19.0 3.0 4.181732 3.858179
2 36.0 9.0 4.057546 4.010473
3 91.0 15.0 4.094357 4.271381
4 83.0 1.0 0.925993 0.853108
5 94.0 5.0 0.922070 1.378054
6 7.0 9.0 3.981694 3.827851
7 79.0 12.0 3.750808 4.117280
8 50.0 9.0 4.017231 4.051401

- 학습이 잘 되었음.

추천: 마지막유저에 대한 예측결과 (아마 홍차를 좋아할듯)

X,y = dls.one_batch()
X[:5]
tensor([[93,  4],
        [13, 14],
        [72, 16],
        [83,  4],
        [99,  9]])
y[:5]
tensor([[1.3570],
        [1.0816],
        [3.7841],
        [0.9617],
        [0.6654]])
  • 93번째 user는 홍차를 좋아하는 사람이었고, 13번은 커피를 좋아하는 user였다.
  • 그러니 1(4는 커피4)은 낮게 나오고 2(14는 홍차3)도 낮게 나올 것이다라는 예측.
X[0]
tensor([93,  4])

[이 하나지만 [[이렇게 두 개로 차원을 맞춰주자

torch.tensor([[94,3]])
tensor([[94,  3]])
lrnr.model(torch.tensor([[94,3]]).to("cuda:0"))
tensor([1.3168], device='cuda:0', grad_fn=<AddBackward0>)
x100 = torch.tensor([[100,j] for j in range(1,21) ])
lrnr.model(x100.to("cuda:0"))
tensor([0.9640, 1.0734, 0.9781, 1.0932, 1.0213, 1.0500, 1.0856, 1.0284, 1.0870,
        1.0680, 3.9622, 4.1116, 4.0082, 3.9710, 4.0666, 4.0106, 3.9812, 4.0612,
        4.0237, 3.9965], device='cuda:0', grad_fn=<AddBackward0>)

- 예상대로 홍차를 매우 좋아함.

숙제

숙제는 유저2번에 대한 선호도 조사

  • 아마 커피를 좋아핳 것이다
x2 = torch.tensor([[2,j] for j in range(1,21) ])
lrnr.model(x2.to("cuda:0"))
tensor([3.9742, 3.8795, 3.8931, 4.0259, 3.9215, 3.8973, 3.9502, 3.9448, 3.8838,
        3.8957, 0.9073, 0.8718, 1.0020, 0.9880, 0.7960, 0.9337, 1.0272, 0.8376,
        1.0132, 1.0098], device='cuda:0', grad_fn=<AddBackward0>)

- 예상대로 커피를 매우 좋아함.