91강에서 을 상수로 썼습니다. 그것이 어디서 오는지는 설명하지 않았습니다.
그리고 이 과목 내내 같은 이야기가 반복됐습니다.
| 강의 | 무너진 것 |
|---|---|
| 45 | 가 이 됨 |
| 67 | 피벗팅 없는 소거 |
| 78 | 행렬식이 넘침 |
| 80 | 고전 그람슈미트 |
| 81 | 정규방정식 |
수학적으로 옳은 식이 부동소수점에서 무너집니다.
이 강의는 그 이유를 하드웨어 수준에서 봅니다. 실수가 어떻게 저장되고, 어디서 정보가 사라지며, 어떤 코딩 습관이 그것을 피하는지입니다.
62강에서 벡터를 정의하고 시작해, 행렬을 변환으로 읽고, 공간과 좌표를 세우고, 분해로 계산하고, 91강에서 그 계산이 얼마나 믿을 만한지 쟀습니다. 이제 그 밑바닥을 봅니다.
문제. 부동소수점의 성질을 확인합니다.
(1) 입니까?
(2) 을 소수 스무 자리까지 출력하세요.
(3) 근처의 간격을 재세요.
생각의 실마리. 컴퓨터는 이진수로 저장합니다. 은 십진수로는 유한하지만 이진수로는 순환소수입니다.
풀이. (1) 검산에서 False이고 가
입니다.
(2) 이 실제로는
로 저장됩니다.
(3) 검산에서 기계 엡실론과 표현 범위가
| 양 | 값 |
|---|---|
| 2.2204\times10^ | |
| 최대 | 1.7977\times10^ |
| 최소 정규수 | 2.2251\times10^ |
이고 이지만 입니다.
이 문제에서 배우는 것: 부동소수점의 구조.
IEEE 754 배정밀도. 비트를 이렇게 나눕니다.
부분 비트 뜻 부호 양수인지 음수인지 지수 크기의 규모 가수 유효숫자
가수가 비트이고 앞의 이 암묵적이므로 유효숫자가 비트입니다.
과 그다음 표현 가능한 수의 간격이며, 십진수로 약 자리입니다.
이 부정확한 이유를 봅니다.
의 거듭제곱으로 유한하게 표현되지 않습니다. 이 십진수로 순환하는 것과 같습니다.
, , 는 정확하고 , , 은 아닙니다.
간격이 크기에 비례합니다. 검산에서
| 간격 | |
|---|---|
| 2.2204\times10^ | |
| 10^ | 1.1642\times10^ |
| 10^ | 2.1176\times10^ |
상대 정밀도가 일정하다는 뜻입니다. 큰 수 근처에서는 간격이 크고 작은 수 근처에서는 촘촘합니다.
반올림의 모형을 세웁니다.
모든 연산이 이 오차를 냅니다. 91강의 후방 오차 해석이 이 모형 위에서 이루어집니다.
실용적 결론 하나를 짚습니다.
대신 허용오차를 씁니다.
바로 확인 1.
확인 1-1. 이 부정확한 이유를 쓰세요.
답. 이진수로 순환소수이기 때문입니다.
확인 1-2. 의 뜻을 쓰세요.
답. 과 그다음 표현 가능한 수의 간격이며 입니다.
확인 1-3. 간격이 크기에 비례한다는 것의 뜻을 쓰세요.
답. 상대 정밀도가 일정하다는 뜻입니다.
문제. 를 봅니다. 참값은 에서 입니다.
(1) 식대로 계산하세요.
(2) 를 작게 하면 어떻게 됩니까?
(3) 식을 고쳐 보세요.
생각의 실마리. 가 작으면 이라 **가 거의 **입니다. 비슷한 두 수를 빼면 유효숫자가 사라집니다.
풀이. (1)과 (2)를 검산에서 봅니다.
| 식대로 | 고친 식 | |
|---|---|---|
| 10^ | ||
| 10^ | ||
| 10^ | ||
| 10^ |
에서 답이 통째로 이 되었습니다.
(3) 반각 공식을 씁니다.
뺄셈이 사라졌습니다.
이 문제에서 배우는 것: 파국적 상쇄.
파국적 상쇄. 비슷한 크기의 두 수를 빼면 유효숫자가 대량으로 사라집니다.
왜 그런지 봅니다. 이면
인데 ** 근처의 간격이 **입니다. 은 그보다 작으므로 반올림되어 사라집니다.
뺄셈 자체는 정확합니다. 두 수가 이미 정확하지 않은 것이며, 뺄셈이 그 부정확함을 확대해 드러낼 뿐입니다.
45강에서 처음 만났고, 67강의 피벗팅, 80강의 고전 그람슈미트가 모두 이 현상입니다.
대응 원칙을 정리합니다.
| 위험한 식 | 안전한 식 |
|---|---|
expm1(x) |
|
log1p(x) |
|
| \dfrac{-b+\sqrt{b^{2}-4ac}} | 문제 3의 방법 |
셋째와 넷째 줄이 표준 라이브러리에 있습니다. 가 작을 때를 위해 따로 구현된 함수이며, 반드시 써야 합니다.
둘째 줄의 기법이 일반적입니다. 켤레를 곱해 뺄셈을 나눗셈으로 바꿉니다.
분자에서 상쇄가 정확히 일어나 뺄셈이 사라집니다.
바로 확인 2.
확인 2-1. 파국적 상쇄가 무엇입니까?
답. 비슷한 수를 빼서 유효숫자가 사라지는 현상입니다.
확인 2-2. 뺄셈이 오차를 만듭니까?
답. 아닙니다. 숨어 있던 오차를 드러낼 뿐입니다.
확인 2-3. 를 어떻게 계산합니까?
답. log1p(x)를 씁니다.
문제. 을 가 클 때 풉니다.
(1) 근의 공식을 그대로 쓰세요.
(2) 잔차를 확인하세요.
(3) 고쳐 보세요.
생각의 실마리. 가 크면 입니다. 가 상쇄됩니다.
풀이. (1)과 (2)를 검산에서 봅니다.
| 공식대로 (작은 근) | 고친 식 | 공식의 잔차 | 고친 식의 잔차 | |
|---|---|---|---|---|
| 10^ | -1.000000\times10^ | -1.000000\times10^ | 1.118\times10^ | |
| 10^ | -7.450581\times10^ | -1.000000\times10^ | 2.549\times10^ |
에서 작은 근이 로 나오는데 참값은 입니다.
틀렸습니다.
(3) 상쇄가 없는 쪽을 먼저 구하고 근과 계수의 관계로 나머지를 얻습니다.
부호를 맞춰 더하므로 상쇄가 없습니다. 그리고 를 써서 나머지를 구합니다.
이 문제에서 배우는 것: 식을 다시 쓰는 기술.
원칙. 상쇄가 일어나지 않는 쪽을 먼저 계산하고, 관계식으로 나머지를 얻습니다.
잔차 표를 조심해서 읽어야 합니다. 에서 고친 식의 잔차가 로 오히려 큽니다.
그런데 해는 고친 쪽이 정확합니다. 왜 그런지 봅니다.
과 이 상쇄되어 이 남아야 하는데, 그것이 근처의 간격보다 작아 사라집니다. 계산된 잔차 은 잔차 계산 자체의 상쇄 때문입니다.
91강 문제 5의 교훈이 다시 나옵니다. 잔차는 유용한 지표이지만 그것만 믿으면 안 됩니다. 여기서는 잔차가 오히려 정확한 답을 나쁘게 보이게 만듭니다.
어떻게 검증하는가. 참값을 아는 예를 만들거나, 높은 정밀도로 다시 계산해 비교합니다. 이 예에서는 근과 계수의 관계로 참값이 과 임을 알 수 있습니다.
이 관계가 검산의 근거입니다.
정리하면 이렇습니다.
| 대상 | 상태 |
|---|---|
| 큰 근 | 두 방법 모두 정확 |
| 작은 근 (공식대로) | 오차 |
| 작은 근 (고친 식) | 정확 |
| 잔차 지표 | 이 경우 오해를 부릅니다 |
바로 확인 3.
확인 3-1. 근의 공식에서 어디가 위험합니까?
답. 와 의 부호가 반대일 때 상쇄됩니다.
확인 3-2. 어떻게 고칩니까?
답. 부호를 맞춰 한 근을 구하고 근과 계수의 관계로 나머지를 얻습니다.
확인 3-3. 잔차가 크면 언제나 답이 나쁩니까?
답. 아닙니다. 잔차 계산 자체가 상쇄를 겪을 수 있습니다.
문제. float32에서 하나와 을 개 더합니다.
(1) 근처의 간격을 재세요.
(2) 큰 것부터 더한 결과와 작은 것부터 더한 결과를 비교하세요.
(3) 보정 기법을 써 보세요.
생각의 실마리. float32는 가수가 비트라 근처의 간격이 큽니다. 그보다 작은 것을 더하면 사라집니다.
풀이. (1) 검산에서 간격이 입니다.
(2) 결과를 비교합니다.
| 방법 | 을 뺀 값 |
|---|---|
| 순진하게 큰 것부터 | |
| 순진하게 작은 것부터 | |
math.fsum |
|
| 카한 합 |
첫 줄에서 개의 이 통째로 사라졌습니다.
(3) 카한 합이 정확한 답을 줍니다.
이 문제에서 배우는 것: 합의 순서와 보정.
문제. 에서 가 의 간격보다 작으면 가 통째로 사라집니다.
한 번에 하나씩 잃습니다. 번 반복하면 을 잃습니다.
작은 것부터 더하면 합이 커지기 전에 작은 값들이 모이므로 살아남습니다.
가 가장 간단한 대책입니다. 다만 정렬 비용이 듭니다.
카한 합은 정렬 없이 해결합니다.
카한 합. 매 단계에서 잃어버린 부분을 기억해 다음에 보정합니다.
가 잃어버린 양입니다. 가 실제로 더해진 양이고, 그것과 의 차가 손실입니다.
누적 오차가 항의 개수에 무관해집니다.
numpy는 쌍대합을 씁니다. 배열을 반씩 나눠 재귀적으로 더하며, 오차가 입니다.
그래서 np.sum이 파이썬 루프보다 정확합니다. 빠를 뿐 아니라 안전합니다.
math.fsum은 완전히 정확합니다. 부분합을 모두 유지해 정확한 반올림을 보장하지만 느립니다.
정리합니다.
| 방법 | 오차 | 비용 |
|---|---|---|
| 순진한 루프 | ||
| 정렬 후 합 | 개선됨 | |
쌍대합 (np.sum) |
||
| 카한 | ||
math.fsum |
정확 | 느림 |
실무의 선택은 대개 셋째 줄입니다. 정확도가 결정적이면 넷째나 다섯째를 씁니다.
이 문제가 실제로 나타나는 곳을 봅니다.
| 상황 | 내용 |
|---|---|
| 긴 시계열의 평균 | 누적하면 잃습니다 |
| 온라인 분산 | 순진한 공식이 위험합니다 |
| 신경망의 손실 합 | 배치가 크면 문제 |
| 저정밀도 학습 | float16에서 특히 심각 |
둘째 줄이 유명합니다. 을 쓰면 두 큰 수의 차라 상쇄됩니다. 웰퍼드 알고리즘이 그 대안이며 심화 3에서 봅니다.
바로 확인 4.
확인 4-1. 큰 것부터 더하면 왜 잃습니까?
답. 합이 커져 간격이 벌어지면 작은 항이 사라집니다.
확인 4-2. 카한 합의 원리를 쓰세요.
답. 잃어버린 양을 기억해 다음 단계에 보정합니다.
확인 4-3. np.sum이 파이썬 루프보다 정확한 이유를 쓰세요.
답. 쌍대합을 써서 오차가 입니다.
문제. 다음을 계산합니다.
(1) 의 노름
(2) 의 노름
(3) 에서
생각의 실마리. 제곱하면 지수가 두 배가 됩니다. **을 제곱하면 **이라 표현 범위를 넘습니다.
풀이. (1)과 (2)를 검산에서 봅니다.
| 크기 | 순진한 계산 | 척도 조정 | np.linalg.norm |
|---|---|---|---|
| 10^ | inf |
5\times10^ | inf |
| 10^ | 5\times10^ |
참값이 과 으로 표현 범위 안인데도 실패합니다.
np.linalg.norm도 실패한다는 점을 눈여겨보십시오. 척도 조정을 하지 않기 때문입니다.
(3) 이 넘칩니다. 검산에서 직접 계산하면 inf입니다.
최댓값을 빼면
이고 가 나옵니다.
이 문제에서 배우는 것: 척도 조정.
원칙. 중간 계산이 넘치거나 밑넘치면 크기를 미리 조정합니다.
노름의 경우 최댓값으로 나눕니다.
괄호 안이 이하라 제곱해도 안전합니다.
로그-합-지수도 같은 발상입니다.
지수가 이하라 넘치지 않습니다. 그리고 인 항이 을 주므로 밑넘쳐 전부 이 되는 일도 없습니다.
이 기법이 확률에서 필수입니다.
| 상황 | 왜 필요 |
|---|---|
| 소프트맥스 | 지수가 넘칩니다 |
| 로그우도 | 확률의 곱이 밑넘칩니다 |
| 은닉 마르코프 | 전방 알고리즘의 누적 |
| 혼합 모형 | 성분별 밀도의 합 |
둘째 줄이 가장 흔합니다. 확률 짜리 개를 곱하면 이라 밑넘칩니다.
곱을 로그의 합으로 바꾸는 것이 표준이며, 78강 심화 3의 로그행렬식과 같은 발상입니다.
소프트맥스 구현을 봅니다.
분자와 분모에서 이 약분되므로 값이 바뀌지 않으면서 안전합니다.
넘침과 밑넘침의 차이도 짚어 둡니다.
| 현상 | 결과 | 위험도 |
|---|---|---|
| 넘침 | inf, 이후 nan |
눈에 띕니다 |
| 밑넘침 | 조용히 틀립니다 |
둘째 줄이 더 위험합니다. inf는 즉시 보이는데 은 정상적인 값처럼 보입니다. 의 노름이 으로 나온 것이 그 예입니다.
바로 확인 5.
확인 5-1. 노름 계산에서 척도 조정을 어떻게 합니까?
답. 최댓값으로 나눈 뒤 계산하고 다시 곱합니다.
확인 5-2. 로그-합-지수의 식을 쓰세요.
답. 이며 은 최댓값입니다.
확인 5-3. 넘침과 밑넘침 중 어느 것이 위험합니까?
답. 밑넘침입니다. 조용히 이 되어 눈에 띄지 않습니다.
| 개념 | 값 |
|---|---|
| (배정밀도) | 2^{-52}=2.22\times10^ |
| 표현 범위 | \sim10^ |
| 반올림 모형 | , |
| 간격 | 크기에 비례 |
| 위험 | 대응 |
|---|---|
| 켤레를 곱합니다 | |
| , | expm1, log1p |
| 근의 공식 | 부호를 맞춰 한 근, 관계식으로 나머지 |
| 긴 합 | 쌍대합, 카한, fsum |
| 큰 노름 | 최댓값으로 나눕니다 |
| 지수의 합 | 로그-합-지수 |
| 확률의 곱 | 로그의 합 |
| 합의 정확도 | 오차 |
|---|---|
| 순진한 루프 | |
쌍대합 (np.sum) |
|
| 카한 | |
math.fsum |
정확 |
| 자주 하는 실수 | 바로잡기 |
|---|---|
실수를 ==로 비교합니다 |
허용오차를 씁니다 |
| 뺄셈이 오차를 만든다고 봅니다 | 드러낼 뿐입니다 |
| 잔차만 보고 판단합니다 | 잔차 계산도 상쇄됩니다 |
| 밑넘침을 무시합니다 | 조용히 틀립니다 |
문제 6. 이 부정확한 이유를 쓰세요.
답. 이진수로 순환소수이기 때문입니다.
문제 7. 배정밀도의 를 쓰세요.
답. 입니다.
문제 8. 간격이 크기에 비례한다는 것의 뜻을 쓰세요.
답. 상대 정밀도가 일정합니다.
문제 9. 파국적 상쇄를 정의하세요.
답. 비슷한 수를 빼서 유효숫자가 사라지는 현상입니다.
문제 10. 뺄셈이 오차를 만듭니까?
답. 아닙니다. 숨어 있던 오차를 드러냅니다.
문제 11. 를 안전하게 쓰세요.
답. 입니다.
문제 12. 와 을 어떻게 계산합니까?
답. log1p와 expm1을 씁니다.
문제 13. 근의 공식을 안전하게 고치는 방법을 쓰세요.
답. 부호를 맞춰 한 근을 구하고 로 나머지를 얻습니다.
문제 14. 큰 것부터 더하면 왜 잃습니까?
답. 합이 커져 간격이 벌어지면 작은 항이 사라집니다.
문제 15. 카한 합의 원리를 쓰세요.
답. 잃어버린 양을 기억해 다음에 보정합니다.
문제 16. 노름의 척도 조정을 쓰세요.
답. 최댓값으로 나눈 뒤 계산하고 다시 곱합니다.
문제 17. 로그-합-지수의 식을 쓰세요.
답. 입니다.
문제 18. 넘침과 밑넘침 중 어느 것이 더 위험합니까?
답. 밑넘침입니다. 조용히 이 됩니다.
심화 1. 부동소수점 연산의 대수 법칙이 어디까지 성립하는지 논하세요.
풀이. 실수에서 당연한 법칙이 부동소수점에서 깨집니다.
결합법칙이 깨집니다. 검산에서
괄호 위치가 답을 바꿉니다.
분배법칙도 깨집니다. 와 가 다를 수 있습니다.
성립하는 것과 아닌 것을 정리합니다.
| 법칙 | 성립 |
|---|---|
| 교환법칙 () | 예 |
| 결합법칙 | 아닙니다 |
| 분배법칙 | 아닙니다 |
| 예 | |
| 예 | |
| 예 |
교환법칙은 성립합니다. 두 수를 더하는 것은 정확한 합을 반올림하는 것이라 순서와 무관합니다.
결합법칙이 깨지는 것의 함의가 큽니다.
| 결과 | 내용 |
|---|---|
| 컴파일러 최적화 제한 | 식의 순서를 함부로 못 바꿉니다 |
| 병렬 계산의 비결정성 | 스레드 순서에 따라 답이 다릅니다 |
| 재현성 문제 | 같은 코드가 다른 결과를 냅니다 |
둘째 줄이 실무에서 자주 문제입니다. GPU에서 배치 크기를 바꾸면 합의 순서가 바뀌어 결과가 미세하게 달라집니다. 학습 결과가 재현되지 않는 흔한 원인입니다.
IEEE 754가 보장하는 것도 정리합니다.
| 연산 | 보장 |
|---|---|
| 정확한 결과를 반올림 | |
| \sqrt | 정확한 결과를 반올림 |
| 나머지 함수 | 구현에 따라 다릅니다 |
**첫 두 줄이 "정확히 반올림된다"**는 뜻입니다. 참값에 가장 가까운 표현 가능한 수를 줍니다. , 등은 그 보장이 없어 라이브러리마다 마지막 자리가 다를 수 있습니다.
심화 2. 정밀도 선택의 절충을 논하세요.
풀이. 검산에서 세 정밀도를 비교합니다.
| 형식 | 최대 | 최소 정규수 | 유효 비트 | |
|---|---|---|---|---|
| float16 | 9.7656\times10^ | 6.5504\times10^ | 6.1035\times10^ | |
| float32 | 1.1921\times10^ | 3.4028\times10^ | 1.1755\times10^ | |
| float64 | 2.2204\times10^ | 1.7977\times10^ | 2.2251\times10^ |
float16의 최대가 뿐입니다. 조금만 커도 넘칩니다.
91강 심화 1의 자릿수 계산을 적용합니다.
| 형식 | 십진 자릿수 | 한계 |
|---|---|---|
| float16 | 10^ | |
| float32 | 10^ | |
| float64 | 10^ |
셋째 열이 실용적 기준입니다. 조건수가 그보다 크면 유효숫자가 남지 않습니다.
왜 낮은 정밀도를 쓰는가.
| 이유 | 내용 |
|---|---|
| 메모리 | 절반 또는 사분의 일 |
| 대역폭 | 메모리 접근이 병목입니다 |
| 처리량 | 전용 하드웨어가 훨씬 빠릅니다 |
둘째 줄이 결정적입니다. 66강 심화 6에서 행렬 곱의 병목이 메모리 접근이라 했는데, 절반 크기면 두 배 빠릅니다.
혼합 정밀도가 실무의 답입니다.
행렬 곱에서 곱셈은 float16으로 하고 합은 float32로 누적하면, 속도를 얻으면서 문제 4의 손실을 피합니다. 현대 GPU의 텐서 코어가 이 방식입니다.
bfloat16이라는 형식도 있습니다.
| 형식 | 지수 비트 | 가수 비트 |
|---|---|---|
| float16 | ||
| bfloat16 | ||
| float32 |
bfloat16은 float32와 지수 범위가 같습니다. 정밀도를 더 희생하고 범위를 얻은 것이며, 학습에서 넘침이 더 큰 문제이기 때문입니다.
429강에서 이 형식들을 자세히 다룹니다.
심화 3. 분산 계산의 안전한 방법을 다루세요.
풀이. 문제 4에서 언급한 예를 봅니다.
위험한 공식입니다.
두 큰 수의 차입니다. 데이터가 근처에 몰려 있고 분산이 이면
이고 차가 입니다. 자리가 상쇄되어 배정밀도의 자리 중 자리만 남습니다.
심하면 음수가 나옵니다. 분산은 음수일 수 없는데도 그렇습니다.
두 단계 방법이 간단한 대책입니다.
먼저 평균을 구하고 편차를 제곱합니다. 데이터를 두 번 훑어야 하지만 안전합니다.
89강 문제 1의 중심화가 이 조작입니다. 주성분분석에서 중심화를 먼저 하라고 한 것에 수치적 이유도 있습니다.
웰퍼드 알고리즘은 한 번만 훑습니다.
평균을 갱신하며 편차 제곱합을 누적합니다. 이 표본분산입니다.
| 방법 | 훑기 | 안정성 |
|---|---|---|
| 제곱합 공식 | 번 | 나쁩니다 |
| 두 단계 | 번 | 좋습니다 |
| 웰퍼드 | 번 | 좋습니다 |
공분산에도 같은 문제가 있습니다. 89강의 공분산 행렬을 계산할 때 중심화를 먼저 해야 하며, 그것을 안 하면 상관이 이상하게 나옵니다.
일반 원칙을 뽑습니다.
문제 2의 파국적 상쇄와 같은 원리이며, 이 강의의 중심 교훈입니다.
심화 4. 반복 개선과 혼합 정밀도 계산을 다루세요.
풀이. 91강 심화 1에서 언급한 기법을 자세히 봅니다.
반복 개선. 를 풀고 잔차로 보정합니다.
| 단계 | 하는 일 | 정밀도 |
|---|---|---|
| 1 | 분해 | 낮음 |
| 2 | 를 구합니다 | 낮음 |
| 3 | 높음 | |
| 4 | 을 풉니다 | 낮음 |
| 5 | \hat{\mathbf{x}}\leftarrow\hat{\mathbf{x}}+\mathbf | 높음 |
3단계가 핵심입니다. 잔차는 큰 수의 차라 상쇄가 심하므로 높은 정밀도로 계산해야 합니다.
4단계에서 분해를 재사용하므로 입니다. 70강의 전략입니다.
효과를 봅니다. 각 반복에서 오차가 인자로 줄어들며
이면 수렴합니다. 최종 정확도는 높은 정밀도 수준이 됩니다.
현대 하드웨어에서 다시 주목받습니다. float16 텐서 코어가 float64보다 훨씬 빠르므로, 저정밀도로 분해하고 반복 개선으로 정확도를 회복하면 큰 이득입니다.
| 조합 | 효과 |
|---|---|
| float32 분해 + float64 잔차 | 고전적 |
| float16 분해 + float32 잔차 | GPU 시대 |
| 혼합 정밀도 QR | 최소제곱에 적용 |
적용 조건이 있습니다. 가 너무 크면 수렴하지 않습니다.
float16이면 이라 이어야 합니다. 조건이 좋은 문제에서만 쓸 수 있습니다.
91강 심화 2의 표가 여기서도 적용됩니다. 조건이 나쁘면 어떤 기법도 구제하지 못합니다.
심화 5. 특수한 값들과 그 처리를 다루세요.
풀이. IEEE 754는 보통 수 말고도 몇 가지를 정의합니다.
| 값 | 뜻 | 생기는 경우 |
|---|---|---|
| 무한대 | 넘침, | |
nan |
수가 아님 | , , \sqrt |
| 부호 있는 영 | 밑넘침의 부호를 기억 | |
| 비정규수 | 아주 작은 수 | 밑넘침 근처 |
nan의 성질이 특이합니다.
자기 자신과도 같지 않습니다. 그래서 x != x로 검사할 수 있고, 표준 함수는 isnan입니다.
한 번 생기면 전파됩니다. nan이 들어간 연산은 모두 nan이 되므로, 어디서 생겼는지 추적해야 합니다.
| 원인 | 내용 |
|---|---|
| 확률이 일 때 | |
| 정규화에서 분모가 | |
| 분산이 반올림으로 음수 | |
| 넘침 후 | 그래디언트 폭발 |
셋째 줄이 심화 3의 문제입니다. 제곱합 공식으로 분산이 음수가 되면 표준편차 계산에서 nan이 납니다.
대응은 방어적 코딩입니다.
다만 을 아무렇게나 넣으면 결과가 바뀝니다. 값의 규모에 맞춰야 합니다.
비정규수도 주의합니다. 보다 작은 수를 표현하려고 정밀도를 희생하는 형식인데, 연산이 매우 느립니다. 하드웨어에 따라 배까지 느려집니다.
성능이 중요한 곳에서 켜지만, 밑넘침이 조용히 일어나므로 주의해야 합니다.
부호 있는 영도 쓸모가 있습니다. 이고 이라, 극한의 방향을 보존합니다.
심화 6. S4 전체를 마무리합니다.
풀이. 62강부터 92강까지를 되돌아봅니다.
이 과목이 한 일을 한 문장으로 하면 이렇습니다.
일곱 단원의 흐름입니다.
| 단원 | 한 일 |
|---|---|
| 01 (62~65) | 기하를 대수로 옮겼습니다 |
| 02 (66~70) | 방정식을 푸는 절차를 세웠습니다 |
| 03 (71~75) | 공간과 좌표의 언어를 만들었습니다 |
| 04 (76~79) | 좌표를 벗어나 변환 자체를 봤습니다 |
| 05 (80~83) | 가장 가까운 것을 찾았습니다 |
| 06 (84~89) | 방향별로 들어갔습니다 |
| 07 (90~92) | 그 계산의 한계를 쟀습니다 |
중심 정리 몇 개를 모읍니다.
| 정리 | 강의 |
|---|---|
| 계수-퇴화차수 | 73 |
| 네 부분공간의 직교 | 74 |
| 정규방정식 | 82 |
| 스펙트럼 정리 | 86 |
| 특이값 분해 | 88 |
| 조건수의 오차 증폭 | 91 |
86강과 88강이 정점입니다. 대칭이면 완벽하게 다룰 수 있고, 대칭이 아니어도 기저를 둘 쓰면 언제나 됩니다.
반복된 교훈도 정리합니다.
| 공식 | 실제 계산 |
|---|---|
| 크라메르 (67) | 소거 |
| (69) | solve |
| 여인수 전개 (78) | LU |
| 정규방정식 (82) | QR |
| 특성방정식 (84) | QR 알고리즘 |
| (85) | 슈어, SVD |
| 의 고유분해 (88) | 직접 SVD |
일곱 번 반복됐습니다. 이론이 무엇을 뜻하는지 알려 주고, 계산은 다른 길로 갑니다.
앞으로 어디에 쓰이는가도 적어 둡니다.
| 이어지는 곳 | 무엇을 씁니까 |
|---|---|
| S5 다변수 미적분 (93~120) | 야코비안, 헤세, 그래디언트 |
| 확률과 통계 | 공분산, 다변량 정규분포 |
| 최적화 (205~) | 양정치, 조건수, 뉴턴법 |
| 기계학습 (219~) | 모든 것 |
120강 뒤에 관문 3이 있습니다. 62강부터 120강까지를 섞어 다시 묻습니다.
마지막으로 태도를 남깁니다.
조건수를 재고, 잔차만 보지 말고, 직교 변환 기반 알고리즘을 쓰고, 상쇄가 일어날 자리를 피합니다. 이 습관이 07단원의 진짜 결론입니다.
이 강의에서는 numpy와 math만 씁니다. 부동소수점의 성질을 직접 확인하고, 위험한 식과 안전한 식의 차이를 수치로 보이며, 합의 순서와 척도 조정의 효과를 봅니다.
import numpy as np, math
# --- 문제 1: 실수가 정확히 저장되지 않습니다 ----------------------------
print(0.1 + 0.2 == 0.3, repr(0.1+0.2)) # False 0.30000000000000004
print("%.20f" % 0.1) # 0.10000000000000000555
eps = np.finfo(float).eps
print("%.4e %.4e %.4e" % (eps, np.finfo(float).max, np.finfo(float).tiny))
# 2.2204e-16 1.7977e+308 2.2251e-308
print(1.0 + eps/2 == 1.0, 1.0 + eps != 1.0) # True True
for x in [1.0, 1e6, 1e-6]:
print("%.0e" % x, "%.4e" % float(np.spacing(x)))
# 1e+00 2.2204e-16
# 1e+06 1.1642e-10
# 1e-06 2.1176e-22 <- 간격이 크기에 비례합니다
# --- 문제 2: 파국적 상쇄 -------------------------------------------------
def f_bad(x): return (1 - math.cos(x))/x**2
def f_good(x): return 2*math.sin(x/2)**2/x**2 # 1-cos x = 2 sin^2(x/2)
for x in [1e-2, 1e-4, 1e-6, 1e-8]:
print("%.0e" % x, "%.12f %.12f" % (f_bad(x), f_good(x)))
# 1e-02 0.499995833347 0.499995833347
# 1e-04 0.499999996961 0.499999999583
# 1e-06 0.500044450291 0.500000000000
# 1e-08 0.000000000000 0.500000000000 <- 답이 통째로 사라집니다
# --- 문제 3: 근의 공식 ---------------------------------------------------
def roots_bad(a,b,c):
d = math.sqrt(b*b - 4*a*c)
return ((-b+d)/(2*a), (-b-d)/(2*a))
def roots_good(a,b,c):
d = math.sqrt(b*b - 4*a*c)
q = -(b + math.copysign(d, b))/2 # 부호를 맞춰 더합니다
return (q/a, c/q) # 나머지는 관계식으로
for b in [1e4, 1e8]:
r1 = roots_bad(1.0, b, 1.0); r2 = roots_good(1.0, b, 1.0)
print("%.0e" % b, "%.6e %.6e" % r1, "%.6e %.6e" % r2)
print(" 잔차 %.3e %.3e" % (abs(r1[0]**2 + b*r1[0] + 1),
abs(r2[0]**2 + b*r2[0] + 1)))
# 1e+04 -1.000000e-04 -1.000000e+04 -1.000000e+04 -1.000000e-04
# 잔차 1.118e-09 0.000e+00
# 1e+08 -7.450581e-09 -1.000000e+08 -1.000000e+08 -1.000000e-08
# 잔차 2.549e-01 1.000e+00
# 참값은 -1e-8 과 -1e8 (근과 계수의 관계). 공식대로는 작은 근이 25% 틀립니다.
# 잔차는 오히려 고친 쪽이 크게 나오는데, 잔차 계산 자체가 상쇄되기 때문입니다.
# --- 문제 4: 합의 순서 ---------------------------------------------------
xs = np.concatenate([[1e8], np.ones(100000)]).astype(np.float32)
print("%.4e" % float(np.spacing(np.float32(1e8)))) # 8.0000e+00
def naive(a):
s = np.float32(0.0)
for v in a: s = np.float32(s + v)
return s
print("%.1f" % float(naive(xs) - np.float32(1e8))) # 큰 것부터
print("%.1f" % float(naive(xs[::-1]) - np.float32(1e8))) # 작은 것부터
print("%.1f" % float(math.fsum(xs.astype(float)) - 1e8))
def kahan(a):
s = np.float32(0.0); c = np.float32(0.0)
for v in a:
y = np.float32(v - c); t = np.float32(s + y)
c = np.float32((t - s) - y); s = t # 잃은 양을 기억합니다
return s
print("%.1f" % float(kahan(xs) - np.float32(1e8)))
# 0.0 <- 100000 개의 1 이 통째로 사라졌습니다
# 100000.0
# 100000.0
# 100000.0
# --- 문제 5: 넘침과 밑넘침 ----------------------------------------------
def norm_bad(v): return math.sqrt(sum(float(x)*float(x) for x in v))
def norm_good(v):
m = max(abs(float(x)) for x in v)
if m == 0: return 0.0
return m*math.sqrt(sum((float(x)/m)**2 for x in v)) # 최댓값으로 나눕니다
for scale in [1e200, 1e-170]:
v = np.array([3.0, 4.0])*scale
print("%.0e" % scale, "%.6e %.6e %.6e" % (norm_bad(v), norm_good(v),
np.linalg.norm(v)))
# 1e+200 inf 5.000000e+200 inf
# 1e-170 0.000000e+00 5.000000e-170 0.000000e+00
# np.linalg.norm 도 척도 조정을 하지 않아 함께 실패합니다
xs2 = np.array([800., 801., 802.])
with np.errstate(over="ignore"):
print("%.6e" % float(np.sum(np.exp(xs2)))) # inf
m = xs2.max()
print("%.10f %.10f" % (m + np.log(np.sum(np.exp(xs2-m))), 802.4076059644))
# 802.4076059644 802.4076059644
# --- 심화 1: 결합법칙이 깨집니다 ----------------------------------------
a, b, c = 1e16, -1e16, 1.0
print((a+b)+c, a+(b+c)) # 1.0 0.0
print(0.1+0.2+0.3, 0.1+(0.2+0.3)) # 0.6000000000000001 0.6
# --- 심화 2: 정밀도별 특성 ----------------------------------------------
for t in [np.float16, np.float32, np.float64]:
fi = np.finfo(t)
print(t.__name__, "%.4e %.4e %.4e" % (fi.eps, fi.max, fi.tiny), fi.nmant+1)
# float16 9.7656e-04 6.5504e+04 6.1035e-05 11
# float32 1.1921e-07 3.4028e+38 1.1755e-38 24
# float64 2.2204e-16 1.7977e+308 2.2251e-308 53
실행하면 주석과 같은 값이 나옵니다. 다섯 곳을 짚어 둡니다.
첫째, 이고 이 실제로 입니다. 그리고 간격이 크기에 비례합니다. 근처에서는 이고 근처에서는 입니다.
둘째, 가 에서 **정확히 **이 됩니다. 참값 와 완전히 다릅니다. 반각 공식으로 고치면 소수 열두 자리까지 정확합니다.
셋째가 이 강의에서 가장 조심스러운 지점입니다. 근의 공식에서 작은 근이 로 나오는데 **참값이 **이라 틀렸습니다. 그런데 잔차는 고친 쪽이 더 큽니다. 을 넣으면 이 되어 잔차 계산 자체가 상쇄되기 때문이며, 잔차만 보면 정반대로 판단하게 됩니다.
넷째, float32에서 부터 더하면 **개의 이 통째로 사라져 **입니다. 간격이 이라 을 더해도 값이 바뀌지 않습니다. 순서를 뒤집거나 카한 합을 쓰면 정확합니다.
다섯째, 노름 계산에서 np.linalg.norm도 함께 실패합니다. 척도 조정을 하지 않기 때문이며, 참값이 표현 범위 안인데도 inf와 을 냅니다. 라이브러리를 무조건 믿으면 안 된다는 예입니다.
코드로 할 수 없는 일도 분명히 해 둡니다. 부동소수점의 규칙은 실험으로 정해지는 것이 아니라 IEEE 754 표준입니다. 여기서 확인한 값들은 그 표준을 따르는 구현에서의 관측이며, 다른 하드웨어나 컴파일러 설정에서는 다를 수 있습니다. 특히 확장 정밀도 레지스터나 융합 곱셈-덧셈이 결과를 바꿀 수 있습니다. 또 문제 3의 "잔차가 오해를 부른다"는 관찰은 이 특정 예에서의 것이며, 일반적으로 잔차는 여전히 유용한 지표입니다.
정답.
| 기호 | 읽는 법 | 뜻 |
|---|---|---|
| 기계 엡실론 | 입니다 | |
| 부동소수점 값 | 반올림된 값입니다 | |
| 파국적 상쇄 | catastrophic cancellation | 유효숫자가 사라집니다 |
| 카한 합 | Kahan summation | 손실을 보정합니다 |
| 쌍대합 | pairwise summation | numpy의 기본입니다 |
| logsumexp | 로그-합-지수 | 최댓값을 빼고 계산합니다 |
| 비정규수 | subnormal | 아주 작은 수, 느립니다 |
| bfloat16 | brain float | 범위를 얻고 정밀도를 버립니다 |
| 반복 개선 | iterative refinement | 잔차로 보정합니다 |
이것으로 S4 선형대수학이 완결됩니다.
62강에서 벡터를 정의하고, 66강에서 행렬을 변환으로 읽고, 71강에서 공간의 언어를 세우고, 76강에서 좌표를 벗어나고, 80강에서 가장 가까운 것을 찾고, 84강에서 방향별로 들어가고, 90강에서 그 계산의 한계를 쟀습니다.
다음 93강부터 시작하는 S5 다변수 미적분과 최적화는 이 과목의 도구를 함수에 적용합니다. 야코비 행렬이 미분의 표현행렬이 되고, 헤세 행렬의 부호가 극값을 판정하며, 조건수가 경사하강법의 속도를 결정합니다. 선형대수가 그곳의 언어입니다.