테일러 전개: 0·1·2차 근사의 사다리

이 장의 물음

파이토치로 학습 코드를 짜면 한 스텝은 늘 같은 세 줄이다. loss = criterion(model(x), y)로 지금 파라미터에서 손실 값을 하나 재고, loss.backward()로 그래디언트를 채우고, optimizer.step()으로 파라미터를 옮긴다. 옵티마이저가 손에 쥔 것은 지금 서 있는 한 점에서 잰 숫자들뿐이다. 손실이 0.69이고, 어느 쪽으로 가면 얼마나 빨리 줄어드는지. 조금 떨어진 곳의 손실은 직접 재 보기 전에는 모르는데도, 옵티마이저는 스텝마다 어디로, 얼마나 갈지를 정해야 한다. 한 점에서 잰 숫자만으로 떨어진 곳을 내다보는 일이 어떻게 가능할까?

옵티마이저마다 재는 것도 다르다. 손실 값만 재는 방법이 있고, 그래디언트까지 쓰는 방법이 있고, 손실이 얼마나 휘었는지까지 쓰는 방법이 있다. 더 많이 재면 더 멀리 내다볼 것 같은데, 큰 신경망은 왜 거의 모두 그래디언트까지만 쓸까? 그리고 그래디언트로 내다본 예측은 얼마나 먼 곳까지 믿어도 될까? 학습률을 너무 키우면 학습이 무너지는 일도 예측을 믿을 수 있는 거리와 이어져 있을 것 같다.

이 장은 다음 물음에 차례로 답한다.

1차 근사: 그래디언트로 한 스텝 앞을 내다보기

지금 자리의 손실 값 하나만 안다면, 조금 떨어진 곳의 손실은 어떻게 짐작할까? 가장 정직한 대답은 지금 값을 그대로 다음 자리의 예측으로 쓰는 「모르지만 아마 비슷하겠지」다. 이 예측은 어느 방향으로 가도 같은 값을 말하므로, 어디로 가야 손실이 줄어드는지 알려 주지 않는다. 그래디언트(손실이 가장 빨리 커지는 방향과, 그 방향으로 움직일 때 손실이 커지는 비율을 함께 담은 벡터)까지 알면, 오른쪽으로 0.1 가면 손실이 대략 얼마 늘거나 줄지를 말할 수 있다. 그 「대략」은 얼마나 맞고, 멀리 갈수록 얼마나 빨리 틀려 갈까?

역사: 로그표의 칸 사이를 직선으로 메우다

계산기가 없던 시절, 곱셈을 덧셈으로 바꿔 주는 로그는 천문학자와 항해사의 도구였고, 사람들은 계산한 로그 값을 표로 묶어 썼다. 영국의 수학자 헨리 브리그스(Henry Briggs)가 1624년에 펴낸 『로그 산술(Arithmetica Logarithmica)』은 1부터 20,000까지, 그리고 90,000부터 100,000까지의 정수에 대해 상용로그를 소수 열네 자리까지 실었다. 가운데 비어 있던 20,000부터 90,000까지는 네덜란드의 아드리안 블락(Adriaan Vlacq)이 1628년 판에 채웠다. 표에는 정수만 실리므로, 칸 사이에 있는 수의 로그가 필요하면 이웃한 두 칸의 값을 직선으로 이어 어림한다. 칸 사이의 짧은 곡선을 직선으로 보는 것이다.

이 직선 어림은 얼마나 틀릴까? 정수 n과 n + 1의 한가운데에서 직선이 곡선과 어긋나는 양을 계산하면, n이 1,000일 때 5.4 × 10⁻⁸이고 20,000일 때 1.4 × 10⁻¹⁰이다. 수가 20배 커지자 어긋남이 약 400배(20²배) 준 것은, 로그 곡선이 수가 클수록 펴져서 같은 한 칸 안에서도 직선과 덜 벌어지기 때문이다. 직선 어림의 어긋남은 곡선이 얼마나 휘었는지와 칸이 얼마나 넓은지 두 가지로 정해진다. 이 절은 이 관찰을 식으로 옮긴다.

왼쪽: 눈금이 1씩인 성긴 표에서 log₁₀2와 log₁₀3을 직선으로 이으면 2.5에서 0.0089만큼 어긋난다. 오른쪽: 이웃한 두 정수 n, n + 1의 한가운데에서 직선 어림이 어긋나는 양(두 축 모두 로그 눈금). 수가 커질수록 곡선이 펴져 어긋남이 n²에 반비례해 준다. 분홍 띠는 브리그스의 1624년 표가 실은 구간(sources/taylor/s1_first_order.py, figs.py)
왼쪽: 눈금이 1씩인 성긴 표에서 log₁₀2와 log₁₀3을 직선으로 이으면 2.5에서 0.0089만큼 어긋난다. 오른쪽: 이웃한 두 정수 n, n + 1의 한가운데에서 직선 어림이 어긋나는 양(두 축 모두 로그 눈금). 수가 커질수록 곡선이 펴져 어긋남이 n²에 반비례해 준다. 분홍 띠는 브리그스의 1624년 표가 실은 구간(sources/taylor/s1_first_order.py, figs.py)

계산기 없이 ln 1.1 어림하기

로그표 대신 손실 함수에서 직선 어림을 해 보자. 예제로는 ln(1 + x)를 고르는데, 분류 신경망의 손실인 크로스 엔트로피가 −ln p 꼴의 로그라서 ML 손실이 휘어 있는 모양의 대표이고, x = 0에서 값과 그래디언트를 암산으로 구할 수 있고, 계산기로 참값을 바로 확인할 수 있기 때문이다. 근사의 기준으로 삼는 점을 전개점이라 부르고 a로 적자. 전개점 a = 0에서 값은 f(0) = ln 1 = 0이고, 그래디언트는 f′(x) = 1/(1 + x)에서 f′(0) = 1이다.

값만 쓰는 예측은 f(h) ≈ 0이다. 그래디언트까지 쓰면 「1만큼 가면 1만큼 오른다」는 직선으로 f(h) ≈ 0 + 1 × h = h를 얻는다. 그러면 ln 1.1 ≈ 0.1인데, 계산기로는 0.095310이니 0.004690만큼 크게 어림했다. 스텝 h를 바꿔 가며 두 예측의 오차를 재면 다음과 같다.

스텝 h 참값 ln(1 + h) 값만 쓴 예측 0의 오차 그래디언트까지 쓴 예측 h의 오차
0.01 0.009950 0.009950 0.000050
0.1 0.095310 0.095310 0.004690
0.5 0.405465 0.405465 0.094535
1 0.693147 0.693147 0.306853

스텝을 0.1에서 0.01로 10배 줄이면, 값만 쓴 예측의 오차는 9.58배 줄고 그래디언트까지 쓴 예측의 오차는 94.42배 준다. 그래디언트까지 쓴 예측의 오차는 거의 h²/2다(h = 0.1이면 0.005, 실제 0.004690). 스텝을 반으로 줄이면 오차는 4분의 1로 준다. 오차가 늘 같은 방향(예측이 참값보다 큼)인 것도 눈여겨보자. ln(1 + x)는 위로 볼록하게 휘어서, 전개점에서 그은 직선보다 늘 아래에 있다.

지금 자리의 값과 그래디언트만으로 떨어진 곳의 손실을 직선으로 예측하는 것을 1차 근사 (접선으로 내다보기 / first-order approximation)라 한다. 스텝 h의 1제곱까지 쓰기 때문에 붙은 이름이고, 같은 뜻으로 값만 쓰는 예측은 0차 근사라 부른다.

f(a+h)  ≈  f(a)+f′(a) h\textcolor{#d62728}{f}(\textcolor{#665522}{a} + \textcolor{#0078e1}{h}) \;\approx\; \textcolor{#d62728}{f}(\textcolor{#665522}{a}) + \textcolor{#b0892a}{f'}(\textcolor{#665522}{a})\,\textcolor{#0078e1}{h}
a전개점: 값과 그래디언트를 잰 자리h스텝: 전개점에서 옮긴 거리f손실(장난감에서는 ln⁡(1+x))f′(a)전개점의 그래디언트\begin{array}{ll} \textcolor{#665522}{a} & \text{전개점: 값과 그래디언트를 잰 자리} \\ \textcolor{#0078e1}{h} & \text{스텝: 전개점에서 옮긴 거리} \\ \textcolor{#d62728}{f} & \text{손실(장난감에서는 } \ln(1+x)\text{)} \\ \textcolor{#b0892a}{f'}(\textcolor{#665522}{a}) & \text{전개점의 그래디언트} \end{array}

파라미터가 여럿이면 파라미터 벡터 θ(세타)를 옮기는 스텝도 벡터 Δθ(델타 세타)가 되고, 그래디언트 ∇𝓛(∇는 나블라라고 읽는다)와 스텝의 내적, 곧 파라미터마다 (그 방향의 그래디언트 성분) × (그 방향으로 옮긴 양)을 모두 더한 것이 1차 근사의 변화량이다.

L(θ+Δθ)  ≈  L(θ)+∇L(θ)⋅Δθ\textcolor{#d62728}{\mathcal{L}}(\textcolor{#665522}{\theta} + \textcolor{#0078e1}{\Delta\theta}) \;\approx\; \textcolor{#d62728}{\mathcal{L}}(\textcolor{#665522}{\theta}) + \textcolor{#b0892a}{\nabla \mathcal{L}}(\textcolor{#665522}{\theta}) \cdot \textcolor{#0078e1}{\Delta\theta}
θ지금의 파라미터 벡터Δθ파라미터를 옮기는 스텝(벡터)L손실∇L(θ)지금 자리의 그래디언트\begin{array}{ll} \textcolor{#665522}{\theta} & \text{지금의 파라미터 벡터} \\ \textcolor{#0078e1}{\Delta\theta} & \text{파라미터를 옮기는 스텝(벡터)} \\ \textcolor{#d62728}{\mathcal{L}} & \text{손실} \\ \textcolor{#b0892a}{\nabla \mathcal{L}}(\textcolor{#665522}{\theta}) & \text{지금 자리의 그래디언트} \end{array}
왼쪽: ln(1 + x)를 전개점 0에서 값만으로(회색 점선), 그래디언트까지로(금색 파선) 내다본 예측. 스텝 h = 1에서 0차 근사는 0.693, 1차 근사는 0.307만큼 틀린다. 오른쪽: 스텝 h와 오차를 두 축 모두 로그 눈금으로 그리면 0차 오차는 기울기 1, 1차 오차는 기울기 2인 직선에 가깝다(sources/taylor/s1_first_order.py, figs.py)
왼쪽: ln(1 + x)를 전개점 0에서 값만으로(회색 점선), 그래디언트까지로(금색 파선) 내다본 예측. 스텝 h = 1에서 0차 근사는 0.693, 1차 근사는 0.307만큼 틀린다. 오른쪽: 스텝 h와 오차를 두 축 모두 로그 눈금으로 그리면 0차 오차는 기울기 1, 1차 오차는 기울기 2인 직선에 가깝다(sources/taylor/s1_first_order.py, figs.py)

ML에서: 경사하강은 1차 근사를 믿고 걷는다

그래디언트 반대쪽으로 그래디언트에 학습률 η(에타)를 곱한 만큼 옮기는 경사하강의 한 스텝 Δθ = −η∇𝓛를 1차 근사에 넣으면, 손실이 ∇𝓛·(η∇𝓛) = η|∇𝓛|²만큼 준다고 예측된다. 같은 스텝 길이라면 이 예측 감소가 가장 큰 방향이 그래디언트 반대쪽이므로, 경사하강을 「가장 가파른 내리막」이라 부르는 것은 1차 근사 안에서 하는 말이다.

입력이 둘이고 샘플이 200개인 로지스틱 회귀(분류 손실을 쓰는 가장 작은 선형 모델. 데이터는 씨앗을 고정해 만들었다)에서 파라미터 0을 출발점으로 이 예측을 실제와 맞대 보자. 출발점의 손실은 0.6931이고 |∇𝓛|² = 0.1075다. 학습률 0.1이면 1차 근사가 예측한 감소는 0.01075, 실제 감소는 0.01062로 98.8%가 맞는다. 학습률 1이면 예측 0.1075에 실제 0.0950으로 88.4%만 맞으니, 스텝이 열 배 길어지자 예측이 실제에서 눈에 띄게 벗어나기 시작했다. 어디까지가 믿을 만한 스텝인지는 1차 근사가 버린 항의 크기를 따져야 답할 수 있고, 그 계산은 나머지항이라는 개념으로 한다.

문제 1. 출발하는 자동차의 위치 어림

정지해 있던 자동차가 매초 속도가 2 m/s씩 붙으며 출발했다. 출발 뒤 t초에 자동차는 출발점에서 t² m 떨어져 있으므로, 5초 뒤에는 25 m 지점을 속도 10 m/s로 지나고 있다. (가) 지금 속도 그대로 간다고 보고 1초 뒤, 2초 뒤의 위치를 어림하라. 실제 위치와는 몇 m 다른가? (나) 10초 뒤의 어림은 몇 m 틀리는가? (다) 어림하는 시간을 10배 늘리면 틀리는 양은 몇 배가 되는가?

김민준 M01
김민준

1초 뒤는 25 + 10 = 35 m인데 실제는 6² = 36 m라서 1 m 틀려요. 그러면 10초 뒤는 10배니까 10 m쯤 틀리겠네요.

선생님 T01
선생님

10초 뒤도 직접 계산해 볼까요?

김민준 M05
김민준

어림은 25 + 100 = 125 m, 실제는 15² = 225 m요. 100 m나 틀려요! 2초 뒤는 45 m와 49 m로 4 m, 5초 뒤는 75 m와 100 m로 25 m고요. 1, 4, 25, 100이에요.

이서연 S01
이서연

틀린 양이 시간의 제곱이네. (5 + h)²을 풀면 25 + 10h + h²이잖아. 25 + 10h가 지금 속도로 간다는 어림이고, 남는 게 h²이야.

선생님 T02
선생님

그 h²은 어디서 왔을까요? 지금 속도 10 m/s를 쓴 어림이 놓친 것이 무엇인지 보세요.

이서연 S01
이서연

속도가 매초 2씩 붙는다는 거요. 가속도를 놓쳤어요.

선생님 T01
선생님

가속도 때문에 더 간 거리를 식으로 쓰면요?

이서연 S07
이서연

½ × 2 × h² = h²이요. 아까 풀어 쓴 식에서 남은 그 h²이네요.

김민준 M01
김민준

그럼 (다)는 10배가 아니라 100배네.

이서연 S01
이서연

응. 지금 속도만 쓰는 어림은 1차 근사이고, 그 오차는 시간의 제곱으로 커지는 거야.

김민준 M11
김민준

지난주 하루에 과제 다섯 쪽 썼으니 마감까지 열흘이면 쉰 쪽이라고 계산했다가 크게 틀린 적이 있어요. 날마다 쓰는 속도가 조금씩 늘었으니, 지금 빠르기로 열흘 뒤를 내다본 게 틀린 거네요.

문제 2. 1차 근사가 0이라고 말하는 스텝

함수 f(x, y) = x²y의 점 (1, 2)에서 그래디언트는 (2xy, x²) = (4, 1)이다. (가) 스텝 (0.1, −0.1)에 대해 1차 근사가 예측하는 변화와 실제 변화를 구하라. (나) 스텝 (0.1, −0.4)는 1차 근사로 얼마나 변한다고 예측되는가? 실제 변화는? (다) (나)의 스텝을 (0.01, −0.04)로 10배 줄이면 실제 변화는 몇 배 줄어드는가?

이서연 S01
이서연

(가)는 4 × 0.1 + 1 × (−0.1) = 0.3이고, 실제로는 1.1² × 1.9 − 2 = 0.299야. 거의 맞아.

김민준 M01
김민준

(나)는 4 × 0.1 + 1 × (−0.4) = 0이니까 변하지 않아요. 그래디언트와 수직인 스텝이에요.

선생님 T01
선생님

실제 값도 계산해 볼까요?

김민준 M04
김민준

1.1² × 1.6 = 1.936이라서 0.064만큼 줄었어요. 0이라더니 줄었네요. 반대 스텝 (−0.1, 0.4)로 가 봐도 0.9² × 2.4 = 1.944라서 0.056만큼 줄어요. 양쪽 다 줄어요.

이서연 S06
이서연

1차 근사라면 반대로 가면 부호가 바뀌어야 하잖아. 양쪽 다 준다는 건 1차가 아닌 무언가가 남았다는 뜻이야. (다)를 해 보면 스텝을 10배 줄였을 때 변화가 −0.064에서 −0.000604로 106배 줄어. 거의 100배니까 스텝의 제곱에 비례하는 양이네.

선생님 T14
선생님

그래요. 그래디언트와 수직인 방향에서는 1차 항이 0이라, 그다음 항이 변화를 정해요. 그 항은 무엇이 정할까요?

이서연 S08
이서연

스텝의 제곱에 비례하니까 함수가 그 방향으로 얼마나 휘었는지가 정하겠죠. 1차 근사가 0이라고 말해도 실제로 아무 일이 없다는 뜻은 아니네요. 해석학에서 극값의 후보는 미분이 0인 곳에서 찾지만, 극대인지 극소인지는 거기서 끝나지 않던 것과 같아요.

김민준 M07
김민준

등고선을 따라 걸으면 높이가 안 변한다고 배웠는데, 곧은 직선으로 걸으면 휜 등고선에서 벗어나니까 결국 높이가 변하는 거군요.

문제 3. float32 로 계산한 ln(1 + 0.000001)

이진 분류 손실을 계산하다 보면 아주 작은 x에서 ln(1 + x)를 구할 일이 생긴다. 신경망은 보통 float32(32비트 실수)로 계산하고, float32에서 1 바로 다음 수는 1과 1.19 × 10⁻⁷만큼 떨어져 있다. (가) x = 10⁻⁶일 때 float32로 1 + x를 먼저 만들고 로그를 취하면 얼마가 나오는가? 참값 9.999995 × 10⁻⁷과 견줘라. (나) 1차 근사 ln(1 + x) ≈ x로는? (다) x = 10⁻⁸이면 두 방법은 각각 무엇을 내는가?

김민준 M01
김민준

컴퓨터가 로그를 틀릴 리 없죠. torch.log(1 + x)로 하면 돼요. 돌려 보니… 9.536739 × 10⁻⁷이요? 참값보다 4.6%나 작아요.

선생님 T01
선생님

로그를 취하기 전에, float32로 만든 1 + 10⁻⁶은 정확히 1.000001일까요?

김민준 M05
김민준

아, 1 근처에서는 수가 1.19 × 10⁻⁷ 간격으로만 있으니까 1.000001은 가장 가까운 칸으로 반올림돼요. 로그는 정확한데 넣은 수가 이미 틀렸어요. x = 10⁻⁸이면 1 + x가 그냥 1이 돼서 로그가 0이에요.

이서연 S01
이서연

1차 근사 x는 10⁻⁶을 그대로 내니까 상대 오차가 x²/2를 x로 나눈 5 × 10⁻⁷밖에 안 돼. 10⁻⁸에서는 더 작고. 「정확한 식」보다 「근사」가 더 정확하네.

선생님 T14
선생님

파이토치에는 torch.log1p라는 함수가 따로 있어요. 1 + x를 만들지 않고 ln(1 + x)를 계산하도록 만든 함수예요. 10⁻⁶에 넣으면 9.999995 × 10⁻⁷, 10⁻⁸에 넣으면 1.000000 × 10⁻⁸을 내요. 왜 이런 함수가 필요했을까요?

이서연 S07
이서연

작은 x에서 ln(1 + x)는 x와 거의 같은데, 1 + x를 만드는 순간 x의 아랫자리가 반올림으로 사라지니까요. x가 그만큼 작을 때는 1차 근사가 가르쳐 주는 대로 x를 직접 다뤄야 정보가 남아요.

김민준 M07
김민준

이진 크로스 엔트로피를 직접 짜지 말고 라이브러리의 BCEWithLogitsLoss를 쓰라는 말이 그래서였구나. 보고서 표에서 0.4%를 소수 첫째 자리로 반올림해 0%라고 적었다가 조교님께 지적받은 적이 있는데, 1 + x를 만들면서 x의 아랫자리가 사라지는 것도 그렇게 반올림에 묻히는 거네요.