강에서 한 층의 앞뒤를 손으로 유도했고 강에서 그래프 훑기를 세웠습니다. 둘을 합칩니다.
역전파는 새 아이디어가 아닙니다. 연쇄법칙을 층 단위로 접은 것이고, 규칙은 강에서 이미 나왔습니다.
이 강의는 알고리즘을 네 단계로 적고, 세 층 신경망에서 수치 미분과 일치하는지 확인하고, 깊어질 때 무엇이 무너지는지를 봅니다.
문제. 역전파를 적습니다.
() 네 단계로 적으세요.
() 세 층 신경망에서 확인하세요.
() 비용을 세세요.
생각의 실마리. 강의 뒤로 훑기는 마디마다 국소 미분을 곱해 부모에게 더하는 것이었습니다. 층이 마디가 되면 국소 미분이 행렬 곱이 됩니다.
풀이. () 네 단계로 적습니다.
| 단계 | 무엇을 하나 | 구체적으로 |
|---|---|---|
| 앞으로 가며 값을 저장 | 각 층의 선형 결과와 활성 결과 | |
| 마지막 층의 신호를 만듦 | 손실의 미분 | |
| 층마다 파라미터 그래디언트 | 입력 전치 곱하기 신호 | |
| 신호를 앞 층으로 넘김 | 가중치 전치 곱하고 활성 도함수 곱 |
과 를 층마다 되풀이합니다. 그것이 전부입니다.
() 세 층 신경망에서 확인합니다. 구조는 입니다.
| 무엇 | 모양 | 역전파와 수치 미분의 차 |
|---|---|---|
여섯 개 모두 수치 미분과 소수점 아래 여덟 자리까지 같습니다.
강에서 손으로 유도한 두 규칙이 층 수와 무관하게 그대로 쓰입니다. 층이 백 개여도 반복문 한 줄만 길어집니다.
() 비용을 셉니다. 층 하나가 에서 로 갈 때 배치 면 곱셈이 번입니다.
| 무엇 | 곱셈 횟수 | 앞으로 대비 |
|---|---|---|
| 앞으로 가기 | ||
| 가중치 그래디언트 | ||
| 신호 넘기기 | ||
| 전체 |
뒤로 가는 비용이 앞으로 가는 비용의 두 배쯤입니다.
첫 층은 입력의 그래디언트가 필요 없어 신호를 안 넘깁니다. 그래서 셋째 줄이 이 아니라 입니다.
이 문제에서 배우는 것. 역전파는 앞으로 한 번, 뒤로 두 번어치의 계산입니다. 그리고 그 비용으로 모든 파라미터의 그래디언트를 한꺼번에 얻습니다.
확인 1-1. 역전파의 단계와 단계를 쓰세요.
답. 입력 전치에 신호를 곱해 파라미터 그래디언트를 만들고, 가중치 전치를 곱해 신호를 앞 층으로 넘깁니다.
확인 1-2. 검산에서 전체 곱셈 횟수와 앞으로 대비 배수를 쓰세요.
답. 이고 배입니다.
확인 1-3. 첫 층에서 신호를 안 넘겨도 되는 이유를 쓰세요.
답. 입력의 그래디언트가 필요 없기 때문입니다.
문제. 깊어지면 무엇이 일어나는지 봅니다.
() 무엇이 신호를 키우고 줄이는지 정리하세요.
() 깊이를 늘려 보세요.
() 초기화 크기를 바꿔 보세요.
() 죽은 정류 선형을 세세요.
생각의 실마리. 신호가 층을 지날 때마다 을 곱하고 을 곱합니다. 곱이 층 수만큼 쌓이면 거듭제곱이 됩니다.
풀이. () 정리합니다.
| 무엇 | 어떻게 작용 | 누가 정하나 |
|---|---|---|
| 가중치 크기 | 곱해짐 | 초기화가 정함 |
| 활성 도함수 | 곱해짐 | 이면 막힘 |
| 층 수 | 거듭제곱 | 깊을수록 심함 |
| 잔차 연결 | 을 더함 | 길을 하나 더 냄 |
() 깊이를 늘려 봅니다. 너비가 이므로 정착 초기화의 표준편차는 입니다. 그보다 작은 로 둡니다.
| 깊이 | 첫 층 그래디언트 크기 | 마지막 층 크기 | 첫 층 나누기 마지막 |
|---|---|---|---|
깊이 에서 로 가면 첫 층 그래디언트가 에서 로 아홉 자릿수 떨어집니다.
이번에는 표준편차를 로 키웁니다.
| 깊이 | 첫 층 그래디언트 크기 | 마지막 층 크기 | 첫 층 나누기 마지막 |
|---|---|---|---|
이번에는 반대로 터집니다. 깊이 에서 첫 층 크기가 을 넘습니다.
층마다 곱해지는 값이 보다 작으면 사라지고 크면 터집니다. 정착 초기화는 그 값을 근처에 두려는 시도입니다.
() 초기화 크기를 바꿔 봅니다. 깊이를 으로 고정합니다.
| 표준편차 | 첫 층 크기 | 마지막 층 크기 | 비 |
|---|---|---|---|
표준편차가 에서 로 세 배 남짓 커지는 사이에 첫 층 크기가 에서 로 여덟 자릿수 움직입니다.
층을 지나며 차이가 제곱으로 증폭되기 때문입니다. 초기화 하나가 그만큼 크게 듭니다.
() 죽은 정류 선형을 셉니다. 정류 선형은 입력이 음수면 그래디언트를 으로 막습니다.
| 치우침 초깃값 | 살아 있는 단위 비율 | 첫 층 그래디언트 크기 |
|---|---|---|
치우침을 로 밀면 살아 있는 단위가 퍼센트로 줄고 그래디언트도 분의 이 됩니다.
한 단위가 모든 표본에서 음수가 되면 영원히 안 배웁니다. 그래디언트가 정확히 이라 가중치가 안 움직이고, 안 움직이니 계속 음수입니다.
이 문제에서 배우는 것. 역전파 자체는 정확한데도 신호가 첫 층에 닿지 않을 수 있습니다. 원인은 알고리즘이 아니라 초기화·활성함수·깊이이고, 강부터 강까지가 이 문제를 다룹니다.
확인 2-1. 층마다 곱해지는 값이 보다 작으면 어떻게 되는지 쓰세요.
답. 그래디언트가 층을 지날수록 사라집니다.
확인 2-2. 검산에서 표준편차 일 때 깊이 와 의 첫 층 그래디언트 크기를 쓰세요.
답. 과 입니다.
확인 2-3. 검산에서 치우침이 일 때 살아 있는 단위 비율을 쓰세요.
답. 입니다.
문제. 같은 가중치를 여러 번 씁니다.
() 무엇이 달라지는지 정리하세요.
() 세 번 쓰는 신경망을 만드세요.
() 시각별 기여를 나누세요.
() 더하지 않으면 어떻게 되는지 보세요.
생각의 실마리. 강에서 한 값이 여러 곳에 쓰이면 뒤로 갈 때 더한다고 했습니다. 파라미터 공유는 정확히 그 경우입니다.
풀이. () 정리합니다.
| 무엇 | 무엇이 생기나 | 어떻게 |
|---|---|---|
| 따로 쓰는 파라미터 | 그래디언트가 하나 | 그냥 받음 |
| 여러 번 쓰는 파라미터 | 그래디언트가 여럿 | 다 더함 |
| 어디서 나오나 | 순환 신경망 합성곱 | 같은 를 되풀이 |
| 강과의 관계 | 갈래가 여럿인 마디 | 규칙이 같음 |
() 같은 를 세 번 되풀이해 쓰고 마지막에 출력 가중치를 붙입니다.
| 무엇 | 모양 | 수치 미분과의 차 |
|---|---|---|
| 공유 가중치 | ||
| 출력 가중치 |
세 시각의 기여를 더한 것이 맞습니다.
() 시각별 기여를 나눕니다.
| 시각 | 그 시각의 기여 크기 | 전체에서의 몫 |
|---|---|---|
이 자료에서는 앞쪽 시각의 기여가 더 큽니다. 시각 이 절반 가까이() 차지합니다.
뒤로 갈수록 신호는 줄지만 앞쪽 은닉값이 아직 포화 전이라 더 큽니다. 어느 쪽이 클지는 신호 감쇠와 은닉값 크기가 함께 정합니다.
더한 값과 한꺼번에 계산한 값의 차이는 입니다. 강의 시간축 역전파가 정확히 이 계산입니다.
() 더하지 않으면 어떻게 되는지 봅니다.
| 무엇으로 계산 | 수치 미분과의 차 | 참값 대비 크기 |
|---|---|---|
| 세 시각을 더함 | ||
| 마지막 시각만 |
마지막 시각만 쓰면 크기가 참값의 퍼센트뿐입니다.
강 문제 의 갈래를 안 더한 경우와 같은 잘못입니다.
이 문제에서 배우는 것. 파라미터 공유는 새 규칙이 필요 없습니다. 그래프에서 갈래가 여럿인 마디일 뿐이고, 쓰인 횟수만큼 더하면 끝입니다.
확인 3-1. 공유 파라미터의 그래디언트를 어떻게 구하는지 쓰세요.
답. 쓰인 횟수만큼의 기여를 모두 더합니다.
확인 3-2. 검산에서 시각 의 몫을 쓰세요.
답. 입니다.
확인 3-3. 검산에서 마지막 시각만 썼을 때의 참값 대비 크기를 쓰세요.
답. 입니다.
문제. 구현을 검사합니다.
() 무엇을 확인해야 하는지 정리하세요.
() 첫 손실이 맞는지 보세요.
() 작은 자료를 외우게 하세요.
() 흔한 실수 넷을 만들어 보세요.
생각의 실마리. 역전파는 틀려도 그럴듯하게 돕니다. 손실이 조금 줄어들기까지 하므로, 의도적으로 검사하지 않으면 몇 주를 잃습니다.
풀이. () 정리합니다.
| 무엇 | 어떻게 | 무엇을 잡나 |
|---|---|---|
| 그래디언트 검사 | 수치 미분과 견줌 | 가장 먼저 |
| 작은 자료 외우기 | 표본 몇 개를 완전히 맞힘 | 구현이 맞는지 |
| 손실이 줄어드는가 | 첫 걸음부터 | 학습률이 맞는지 |
| 첫 손실 값 | 무작위 예측의 값 | 초기화가 맞는지 |
() 첫 손실이 맞는지 봅니다. 갈래가 개면 아무것도 모를 때 손실은 입니다.
| 갈래 수 | 로그 갈래 수 | 초기 손실 | 차이 |
|---|---|---|---|
마지막 층을 아주 작게 초기화하면 네 경우 모두 로그 갈래 수에 붙습니다. 차이가 미만입니다.
출력이 에 가까우면 소프트맥스가 균등분포를 내기 때문입니다. 이 값에서 크게 벗어나면 초기화나 손실 구현이 틀린 것입니다.
() 작은 자료를 외우게 합니다. 표본 개입니다.
| 걸음 | 손실 | 정확도 |
|---|---|---|
손실이 에 가까워지고 정확도가 이 됩니다. 못 외우면 구현이 틀린 것입니다.
강에서 본 과적합을 여기서는 일부러 만듭니다. 일반화가 목적이 아니라 구현 검사이기 때문입니다.
() 흔한 실수 넷을 만들어 봅니다. 첫 층 가중치 그래디언트를 여러 방식으로 계산합니다.
| 무엇을 했나 | 수치 미분과의 차 | 참값 대비 크기 |
|---|---|---|
| 바르게 계산 | ||
| 활성 도함수를 빼먹음 | ||
| 배치로 안 나눔 | ||
| 마지막 층에 활성을 넣음 |
배치로 안 나누면 크기가 정확히 표본 수 배가 됩니다. 표본이 개이므로 입니다.
이 실수가 가장 위험합니다. 방향은 맞아서 학습이 되긴 하고, 학습률이 배 커진 것과 같아 조용히 발산합니다.
나머지 둘은 크기도 방향도 틀립니다. 그래디언트 검사 하나가 이 넷을 모두 잡아냅니다.
이 문제에서 배우는 것. 역전파 구현은 틀려도 돌아갑니다. 그래서 손실 곡선만 보고 판단하면 안 되고, 수치 미분·첫 손실·작은 자료 외우기 셋을 반드시 통과시킵니다.
확인 4-1. 갈래가 개일 때 초기 손실이 얼마여야 하는지 쓰세요.
답. 로그 여야 합니다.
확인 4-2. 검산에서 배치로 안 나눴을 때의 참값 대비 크기를 쓰세요.
답. 입니다.
확인 4-3. 작은 자료 외우기가 무엇을 잡는지 쓰세요.
답. 구현이 맞는지를 잡습니다.
문제. 검사를 제대로 합니다.
() 절차를 정리하세요.
() 정류 선형의 꺾인 자리를 보세요.
() 정밀도를 바꿔 보세요.
() 이 강의를 한 장으로 모으세요.
생각의 실마리. 수치 미분은 완벽한 기준이 아닙니다. 그것 자체에 오차가 있고, 특히 미분 불가능한 자리에서 크게 어긋납니다.
풀이. () 정리합니다.
| 무엇을 | 왜 |
|---|---|
| 작게 만든다 | 차원과 표본을 줄입니다 |
| 배정밀도로 검사한다 | 단정밀도는 오차가 큽니다 |
| 상대 오차로 판정한다 | 이하면 통과입니다 |
| 정류 선형 근처를 피한다 | 꺾인 자리에서 어긋납니다 |
| 무작위를 고정한다 | 드롭아웃 씨앗을 고정합니다 |
() 정류 선형의 꺾인 자리를 봅니다.
| 간격 | 에서 간격 안에 있는 수 | 상대 오차 |
|---|---|---|
| 10^ | ||
| 10^ | ||
| 10^ |
간격 에서는 꺾인 자리에 걸린 단위가 하나 있고 상대 오차가 입니다. 판정 기준 를 넘습니다.
걸린 단위가 없으면 상대 오차가 까지 떨어집니다.
걸린 단위가 있으면 그 원소만 크게 어긋나고 나머지는 멀쩡합니다. 그래서 몇 개 원소만 실패하면 코드가 아니라 꺾인 자리를 의심해야 합니다.
() 정밀도를 바꿔 봅니다.
| 정밀도 | 상대 오차 |
|---|---|
| 비트 | |
| 비트 |
비트에서는 상대 오차가 이 되어 검사가 완전히 무너집니다.
비트의 유효숫자는 약 자리입니다. 손실 차이가 쯤인데 그 자리가 애초에 없습니다.
검사는 비트로 합니다. 학습은 비트나 비트로 해도 됩니다. 검사와 학습은 다른 요구를 갖습니다.
() 이 강의를 한 장으로 모읍니다.
| 물음 | 한 줄로 |
|---|---|
| 역전파는 무엇인가 | 그래프 훑기를 층 위에 적은 것입니다 |
| 두 규칙 | 입력 전치 곱 신호와 가중치 전치 곱 신호입니다 |
| 비용 | 앞으로 가는 것의 두 배쯤입니다 |
| 깊으면 | 신호가 곱해지며 줄거나 커집니다 |
| 공유하면 | 쓰인 횟수만큼 더합니다 |
| 맞는지 어떻게 아나 | 수치 미분과 작은 자료 외우기입니다 |
| 첫 손실 | 로그 갈래 수여야 합니다 |
이 문제에서 배우는 것. 그래디언트 검사는 통과와 실패만 보면 안 됩니다. 실패한 원소가 몇 개인지, 정밀도가 무엇인지, 꺾인 자리에 걸렸는지를 함께 봐야 진짜 버그와 수치 오차를 가릅니다.
확인 5-1. 그래디언트 검사를 비트로 해야 하는 이유를 쓰세요.
답. 비트는 유효숫자가 모자라 상대 오차가 이 되기 때문입니다.
확인 5-2. 검산에서 간격 와 의 상대 오차를 쓰세요.
답. 와 입니다.
확인 5-3. 몇 개 원소만 검사에 실패하면 무엇을 의심해야 하는지 쓰세요.
답. 정류 선형의 꺾인 자리를 의심해야 합니다.
| 유형 | 무엇을 묻나 | 어디를 보나 |
|---|---|---|
| 네 단계 | 앞으로 저장하고 뒤로 두 줄 | 문제 |
| 비용 | 앞으로의 두 배쯤 | 문제 |
| 소실과 폭발 | 곱이 보다 작은가 큰가 | 문제 |
| 초기화 | 층이면 제곱으로 증폭 | 문제 |
| 죽은 정류 선형 | 그래디언트가 정확히 | 문제 |
| 공유 파라미터 | 쓰인 횟수만큼 더함 | 문제 |
| 첫 손실 | 로그 갈래 수 | 문제 |
| 배치로 안 나눔 | 크기가 표본 수 배 | 문제 |
| 꺾인 자리 | 몇 개 원소만 어긋남 | 문제 |
| 정밀도 | 검사는 비트로 | 문제 |
역전파의 의사코드를 한자리에 모읍니다.
| 단계 | 식 |
|---|---|
| 앞으로 | |
| 시작 | |
| 층마다 | |
| 의 열 합 | |
문제 6. 역전파의 단계와 단계를 쓰세요.
답. 입력 전치에 신호를 곱해 파라미터 그래디언트를 만들고, 가중치 전치를 곱해 신호를 앞 층으로 넘깁니다.
문제 7. 검산에서 전체 곱셈 횟수와 앞으로 대비 배수를 쓰세요.
답. 이고 배입니다.
문제 8. 첫 층에서 신호를 안 넘겨도 되는 이유를 쓰세요.
답. 입력의 그래디언트가 필요 없기 때문입니다.
문제 9. 층마다 곱해지는 값이 보다 작으면 어떻게 되는지 쓰세요.
답. 그래디언트가 층을 지날수록 사라집니다.
문제 10. 검산에서 표준편차 일 때 깊이 와 의 첫 층 그래디언트 크기를 쓰세요.
답. 과 입니다.
문제 11. 검산에서 치우침이 일 때 살아 있는 단위 비율을 쓰세요.
답. 입니다.
문제 12. 공유 파라미터의 그래디언트를 어떻게 구하는지 쓰세요.
답. 쓰인 횟수만큼의 기여를 모두 더합니다.
문제 13. 검산에서 마지막 시각만 썼을 때의 참값 대비 크기를 쓰세요.
답. 입니다.
문제 14. 갈래가 개일 때 초기 손실이 얼마여야 하는지 쓰세요.
답. 로그 여야 합니다.
문제 15. 검산에서 배치로 안 나눴을 때의 참값 대비 크기를 쓰세요.
답. 입니다.
문제 16. 작은 자료 외우기가 무엇을 잡는지 쓰세요.
답. 구현이 맞는지를 잡습니다.
문제 17. 검산에서 간격 와 의 상대 오차를 쓰세요.
답. 와 입니다.
문제 18. 그래디언트 검사를 비트로 해야 하는 이유를 쓰세요.
답. 비트는 유효숫자가 모자라 상대 오차가 이 되기 때문입니다.
심화 1. 역전파가 왜 층 수에 선형인지 보이세요.
개 층을 지나는 야코비는 곱입니다.
| 어떻게 접나 | 비용 |
|---|---|
| 행렬끼리 다 곱함 | 번의 행렬 곱 |
| 왼쪽부터 벡터에 곱함 | 번의 행렬-벡터 곱 |
둘째 줄이 역전파입니다. 손실이 스칼라이므로 왼쪽 끝이 벡터이고, 벡터에 계속 곱하면 행렬을 만들 필요가 없습니다.
행렬 곱이 인데 행렬-벡터 곱은 입니다. 강 문제 에서 야코비를 안 만든다고 한 것이 이 절약입니다.
심화 2. 왜 층마다 값을 저장해야 하는지 정리하세요.
| 필요한 값 | 어디에 쓰나 |
|---|---|
| H_ | |
| Z_ |
둘 다 앞으로 갈 때만 얻을 수 있습니다. 그래서 메모리가 층 수 곱하기 배치 곱하기 너비로 늘어납니다.
강 문제 의 체크포인트가 여기서 값을 합니다. 정류 선형은 대신 부호 비트만 저장해도 되므로 훨씬 쌉니다.
심화 3. 그래디언트 자르기를 정리하세요.
| 무엇 | 왜 |
|---|---|
| 방향은 그대로 | 크기만 줄입니다 |
| 전체 노름으로 | 층별로 하면 방향이 바뀝니다 |
| 임계값 | 대개 이나 |
문제 의 폭발을 사후에 막는 방법입니다. 원인을 고치는 것이 아니라 증상을 자르는 것이라, 초기화나 정규화와 함께 씁니다.
심화 4. 배치 정규화가 있으면 역전파가 어떻게 달라지는지 정리하세요.
| 무엇 | 왜 달라지나 |
|---|---|
| 표본이 서로 얽힘 | 평균과 분산이 배치 전체에서 계산됨 |
| 그래디언트에 항이 셋 | 정규화된 값과 평균과 분산 각각 |
| 배치 크기에 달림 | 작은 배치면 잡음이 큼 |
첫 줄이 근본적입니다. 지금까지 표본들은 서로 독립이었는데, 배치 정규화를 넣으면 한 표본의 그래디언트가 다른 표본에 달립니다.
강에서 이 세 항을 직접 유도합니다.
심화 5. 순환 신경망에서 잘라 쓰는 역전파를 정리하세요.
| 무엇 | 어떻게 |
|---|---|
| 문제 | 계열이 길면 그래프가 너무 김 |
| 방법 | 시각마다 끊습니다 |
| 잃는 것 | 보다 먼 의존성 |
| 얻는 것 | 메모리와 계산 |
문제 에서 이었지만 실제 계열은 수천 시각입니다.
끊으면 편향이 생깁니다. 진짜 그래디언트가 아니라 잘린 그래디언트이고, 그래도 쓰는 이유는 먼 의존성의 기여가 어차피 소실되기 때문입니다.
심화 6. 역전파와 생물학적 학습의 차이를 정리하세요.
| 무엇 | 역전파 | 뇌 |
|---|---|---|
| 가중치 전치 | 필요함 | 대칭 연결이 없음 |
| 신호 방향 | 정확히 거꾸로 | 별도 경로 |
| 값 저장 | 앞으로 간 값 전부 | 불분명 |
| 시간 잠금 | 앞이 끝나야 뒤 | 계속 흐름 |
첫 줄이 가장 큰 문제입니다. 역전파는 을 요구하는데, 뉴런이 자기 출력 시냅스의 무게를 알 방법이 없습니다.
무작위 되먹임으로도 학습이 된다는 결과가 있습니다. 대신 고정된 무작위 행렬을 써도 앞 층이 그에 맞춰 정렬됩니다.
정답.
| 기호 | 읽는 법 | 뜻 |
|---|---|---|
| 역전파 | backpropagation | 그래프 훑기를 층 위에 적은 알고리즘입니다 |
| 앞으로 가기 | forward pass | 입력에서 손실까지 값을 계산합니다 |
| 뒤로 가기 | backward pass | 손실에서 파라미터까지 신호를 넘깁니다 |
| 신호 | error signal | 각 층의 선형 결과에 대한 손실의 미분입니다 |
| 그래디언트 소실 | vanishing gradient | 층을 지날수록 신호가 사라집니다 |
| 그래디언트 폭발 | exploding gradient | 층을 지날수록 신호가 커집니다 |
| 죽은 정류 선형 | dying ReLU | 모든 입력에서 음수라 안 배우는 단위입니다 |
| 파라미터 공유 | parameter sharing | 같은 가중치를 여러 곳에서 씁니다 |
| 그래디언트 검사 | gradient checking | 수치 미분과 견주어 구현을 검사합니다 |
| 그래디언트 자르기 | gradient clipping | 노름이 크면 방향만 두고 줄입니다 |
다음은 234강 벡터화된 그래디언트 직접 유도하기입니다. 이 강의의 역전파는 층마다 반복문을 돌았습니다. 다음 강의는 그 반복문을 행렬 연산 몇 줄로 접고, 배치와 층을 함께 다루는 유도를 손으로 씁니다.
import numpy as np
def pw(s):
return sum(2 if ord(c) > 0x1100 else 1 for c in str(s))
def rw(s, w):
return str(s) + ' ' * max(0, w - pw(s))
def rl(s, w):
return ' ' * max(0, w - pw(s)) + str(s)
def maxdiff(a, b):
return float(np.abs(np.asarray(a) - np.asarray(b)).max())
def relu(z):
return np.maximum(z, 0.0)
def drelu(z):
return (z > 0).astype(float)
def softmax(Z):
E = np.exp(Z - Z.max(axis=1, keepdims=True))
return E / E.sum(axis=1, keepdims=True)
def init(sizes, rr, scale=None):
Ws, bs = [], []
for i in range(len(sizes) - 1):
s = scale if scale is not None else np.sqrt(2.0 / sizes[i])
Ws.append(rr.normal(0, s, (sizes[i], sizes[i + 1])))
bs.append(np.zeros(sizes[i + 1]))
return Ws, bs
def forward(Ws, bs, X):
acts = [X]
pre = []
H = X
for i in range(len(Ws)):
Z = H @ Ws[i] + bs[i]
pre.append(Z)
H = relu(Z) if i < len(Ws) - 1 else Z
acts.append(H)
return pre, acts
def ce_loss(Z, Y):
mx = Z.max(axis=1, keepdims=True)
lse = mx[:, 0] + np.log(np.exp(Z - mx).sum(axis=1))
return float(np.mean(lse - (Z * Y).sum(axis=1)))
def backward(Ws, bs, X, Y):
n = len(X)
pre, acts = forward(Ws, bs, X)
P = softmax(pre[-1])
D = (P - Y) / n
gW = [None] * len(Ws)
gb = [None] * len(Ws)
for i in range(len(Ws) - 1, -1, -1):
gW[i] = acts[i].T @ D
gb[i] = D.sum(axis=0)
if i > 0:
D = (D @ Ws[i].T) * drelu(pre[i - 1])
return gW, gb, ce_loss(pre[-1], Y)
def numgrad_param(Ws, bs, X, Y, which, idx, h=1e-5):
tgt = Ws[idx] if which == 'W' else bs[idx]
g = np.zeros_like(tgt)
it = np.nditer(tgt, flags=['multi_index'])
while not it.finished:
k = it.multi_index
old = tgt[k]
tgt[k] = old + h
lp = ce_loss(forward(Ws, bs, X)[0][-1], Y)
tgt[k] = old - h
lm = ce_loss(forward(Ws, bs, X)[0][-1], Y)
tgt[k] = old
g[k] = (lp - lm) / (2 * h)
it.iternext()
return g
print("=" * 78)
print("233강 역전파 알고리즘 코드 검산")
print("=" * 78)
print()
print("문제 1. 알고리즘을 적기")
print()
print(" (1) 네 단계로 적습니다")
rows = [
("1", "앞으로 가며 값을 저장", "각 층의 선형 결과와 활성 결과"),
("2", "마지막 층의 신호를 만듦", "손실의 미분"),
("3", "층마다 파라미터 그래디언트", "입력 전치 곱하기 신호"),
("4", "신호를 앞 층으로 넘김", "가중치 전치 곱하고 활성 도함수 곱"),
]
w = [max(pw(r[i]) for r in rows + [("단계", "무엇을 하나", "구체적으로")]) for i in range(3)]
print(" " + rw("단계", w[0]) + " " + rw("무엇을 하나", w[1]) + " " + "구체적으로")
for r in rows:
print(" " + rw(r[0], w[0]) + " " + rw(r[1], w[1]) + " " + r[2])
print(" 3 과 4 를 층마다 되풀이합니다. 그것이 전부입니다")
print(" 232강의 그래프 훑기에서 마디 하나가 층 하나로 굵어졌을 뿐입니다")
print()
print(" (2) 세 층 신경망에서 그래디언트를 확인합니다")
r = np.random.default_rng(20233)
n, din, dh, dk = 40, 5, 7, 3
X = r.normal(0, 1, (n, din))
lab = r.integers(0, dk, n)
Y = np.zeros((n, dk))
Y[np.arange(n), lab] = 1.0
Ws, bs = init([din, dh, dh, dk], r)
gW, gb, L0 = backward(Ws, bs, X, Y)
print(" 층 구조는 %d 에서 %d 에서 %d 에서 %d 입니다" % (din, dh, dh, dk))
print(" 처음 손실은 %.6f 이고 아무것도 모를 때의 값은 %.6f 입니다" % (L0, float(np.log(dk))))
print(" " + rw("무엇", 10) + " " + rl("모양", 12) + " " + rl("역전파와 수치 미분의 차", 26))
for i in range(3):
dW = maxdiff(gW[i], numgrad_param(Ws, bs, X, Y, 'W', i))
print(" " + rw("W%d" % (i + 1), 10) + " " + rl(str(Ws[i].shape), 12) + " " + rl("%.10f" % dW, 26))
for i in range(3):
db = maxdiff(gb[i], numgrad_param(Ws, bs, X, Y, 'b', i))
print(" " + rw("b%d" % (i + 1), 10) + " " + rl(str(bs[i].shape), 12) + " " + rl("%.10f" % db, 26))
print(" 여섯 개 모두 수치 미분과 소수점 아래 여덟 자리까지 같습니다")
print(" 231강에서 손으로 유도한 두 규칙이 층 수와 무관하게 그대로 쓰입니다")
print()
print(" (3) 비용을 셉니다")
print(" 곱셈 횟수를 셉니다. 층 하나가 m 에서 k 로 갈 때 배치 B 면 B 곱하기 m 곱하기 k 입니다")
print(" " + rw("무엇", 16) + " " + rl("곱셈 횟수", 14) + " " + rl("앞으로 대비", 14))
cfg = [(din, dh), (dh, dh), (dh, dk)]
fwd_c = sum(n * a * b_ for a, b_ in cfg)
print(" " + rw("앞으로 가기", 16) + " " + rl("%d" % fwd_c, 14) + " " + rl("%.4f" % 1.0, 14))
print(" " + rw("가중치 그래디언트", 16) + " " + rl("%d" % fwd_c, 14) + " " + rl("%.4f" % 1.0, 14))
print(" " + rw("신호 넘기기", 16) + " " + rl("%d" % (fwd_c - n * din * dh), 14) + " " + rl("%.4f" % ((fwd_c - n * din * dh) / float(fwd_c)), 14))
tot = 2 * fwd_c + (fwd_c - n * din * dh)
print(" " + rw("전체", 16) + " " + rl("%d" % tot, 14) + " " + rl("%.4f" % (tot / float(fwd_c)), 14))
print(" 뒤로 가는 비용이 앞으로 가는 비용의 두 배쯤입니다")
print(" 첫 층은 입력의 그래디언트가 필요 없어 신호를 안 넘깁니다")
print()
print("문제 2. 신호가 층을 지나며 어떻게 되는가")
print()
print(" (1) 무엇이 신호를 키우고 줄이는지 정리합니다")
rows = [
("가중치 크기", "곱해짐", "초기화가 정함"),
("활성 도함수", "곱해짐", "0 이면 막힘"),
("층 수", "거듭제곱", "깊을수록 심함"),
("잔차 연결", "1 을 더함", "길을 하나 더 냄"),
]
w = [max(pw(r[i]) for r in rows + [("무엇", "어떻게 작용", "누가 정하나")]) for i in range(3)]
print(" " + rw("무엇", w[0]) + " " + rw("어떻게 작용", w[1]) + " " + "누가 정하나")
for r in rows:
print(" " + rw(r[0], w[0]) + " " + rw(r[1], w[1]) + " " + r[2])
print()
print(" (2) 깊이를 늘리며 층별 그래디언트 크기를 봅니다")
r2g = np.random.default_rng(30233)
n2, d2 = 64, 20
X2 = r2g.normal(0, 1, (n2, d2))
lab2 = r2g.integers(0, 3, n2)
Y2 = np.zeros((n2, 3))
Y2[np.arange(n2), lab2] = 1.0
print(" 너비가 %d 이므로 정착 초기화의 표준편차는 %.4f 입니다" % (d2, np.sqrt(2.0 / d2)))
print(" 그보다 작은 0.15 로 두고 깊이를 늘립니다")
print(" " + rl("깊이", 8) + " " + rl("첫 층 그래디언트 크기", 24) + " " + rl("마지막 층 크기", 18) + " " + rl("첫 층 나누기 마지막", 22))
for L in [4, 8, 16, 32]:
Ws2, bs2 = init([d2] * L + [3], np.random.default_rng(77), scale=0.15)
gW2, _, _ = backward(Ws2, bs2, X2, Y2)
a = float(np.abs(gW2[0]).mean())
b_ = float(np.abs(gW2[-1]).mean())
print(" " + rl("%d" % L, 8) + " " + rl("%.4e" % a, 24) + " " + rl("%.4e" % b_, 18) + " " + rl("%.4e" % (a / b_), 22))
print(" 깊이가 늘수록 첫 층 그래디언트가 급격히 작아집니다")
print(" 이번에는 표준편차를 0.5 로 키워 같은 것을 봅니다")
print(" " + rl("깊이", 8) + " " + rl("첫 층 그래디언트 크기", 24) + " " + rl("마지막 층 크기", 18) + " " + rl("첫 층 나누기 마지막", 22))
for L in [4, 8, 16, 32]:
Ws2, bs2 = init([d2] * L + [3], np.random.default_rng(77), scale=0.5)
gW2, _, _ = backward(Ws2, bs2, X2, Y2)
a = float(np.abs(gW2[0]).mean())
b_ = float(np.abs(gW2[-1]).mean())
print(" " + rl("%d" % L, 8) + " " + rl("%.4e" % a, 24) + " " + rl("%.4e" % b_, 18) + " " + rl("%.4e" % (a / b_), 22))
print(" 이번에는 반대로 터집니다. 깊이 32 에서 첫 층 크기가 10 의 5 제곱을 넘습니다")
print(" 층마다 곱해지는 값이 1 보다 작으면 사라지고 크면 터집니다")
print(" 정착 초기화는 그 값을 1 근처에 두려는 시도입니다")
print(" 246강에서 이 현상을 정면으로 다룹니다")
print()
print(" (3) 초기화 크기를 바꿔 봅니다")
print(" 깊이를 16 으로 고정하고 초기화 표준편차만 바꿉니다")
print(" " + rl("표준편차", 12) + " " + rl("첫 층 크기", 16) + " " + rl("마지막 층 크기", 18) + " " + rl("비", 16))
for sc in [0.15, 0.25, np.sqrt(2.0 / d2), 0.5]:
Ws3, bs3 = init([d2] * 16 + [3], np.random.default_rng(88), scale=sc)
gW3, _, _ = backward(Ws3, bs3, X2, Y2)
a = float(np.abs(gW3[0]).mean())
b_ = float(np.abs(gW3[-1]).mean())
print(" " + rl("%.4f" % sc, 12) + " " + rl("%.4e" % a, 16) + " " + rl("%.4e" % b_, 18) + " " + rl("%.4e" % (a / b_), 16))
print(" 정착 초기화는 표준편차를 2 나누기 입력차원의 제곱근으로 둡니다")
print(" 여기서는 %.4f 이고 표에서 셋째 줄입니다" % np.sqrt(2.0 / d2))
print(" 절대 크기는 표준편차를 키울수록 커집니다. 16 층을 지나며 증폭되기 때문입니다")
print(" 243강에서 이 값이 어디서 나오는지 유도합니다")
print()
print(" (4) 죽은 정류 선형이 몇 개인지 셉니다")
r3 = np.random.default_rng(40233)
print(" 정류 선형은 입력이 음수면 그래디언트를 0 으로 막습니다")
print(" " + rl("치우침 초깃값", 14) + " " + rl("살아 있는 단위 비율", 22) + " " + rl("첫 층 그래디언트 크기", 24))
for b0 in [-2.0, -0.5, 0.0, 0.5]:
Ws4, bs4 = init([d2, 30, 30, 3], np.random.default_rng(99), scale=0.4)
bs4[0] = bs4[0] + b0
bs4[1] = bs4[1] + b0
pre4, _ = forward(Ws4, bs4, X2)
alive = float((pre4[0] > 0).mean())
gW4, _, _ = backward(Ws4, bs4, X2, Y2)
print(" " + rl("%.4f" % b0, 14) + " " + rl("%.6f" % alive, 22) + " " + rl("%.4e" % float(np.abs(gW4[0]).mean()), 24))
print(" 치우침을 음수로 밀면 살아 있는 단위가 줄고 그래디언트도 함께 줄어듭니다")
print(" 한 단위가 모든 표본에서 음수가 되면 영원히 안 배웁니다")
print(" 240강에서 정류 선형의 변형들이 이 문제를 어떻게 푸는지 봅니다")
print()
print("문제 3. 파라미터를 공유할 때")
print()
print(" (1) 무엇이 달라지는지 정리합니다")
rows = [
("따로 쓰는 파라미터", "그래디언트가 하나", "그냥 받음"),
("여러 번 쓰는 파라미터", "그래디언트가 여럿", "다 더함"),
("어디서 나오나", "순환 신경망 합성곱", "같은 W 를 되풀이"),
("232강과의 관계", "갈래가 여럿인 마디", "규칙이 같음"),
]
w = [max(pw(r[i]) for r in rows + [("무엇", "무엇이 생기나", "어떻게")]) for i in range(3)]
print(" " + rw("무엇", w[0]) + " " + rw("무엇이 생기나", w[1]) + " " + "어떻게")
for r in rows:
print(" " + rw(r[0], w[0]) + " " + rw(r[1], w[1]) + " " + r[2])
print()
print(" (2) 같은 가중치를 세 번 쓰는 신경망을 만듭니다")
r4 = np.random.default_rng(50233)
n3, d3 = 30, 6
X3 = r4.normal(0, 1, (n3, d3))
lab3 = r4.integers(0, 2, n3)
Y3 = np.zeros((n3, 2))
Y3[np.arange(n3), lab3] = 1.0
Wsh = r4.normal(0, 0.5, (d3, d3))
Wout = r4.normal(0, 0.5, (d3, 2))
T = 3
def shared_forward(Ws_, Wo_, Xin):
hs, zs = [Xin], []
H = Xin
for _ in range(T):
Z = H @ Ws_
zs.append(Z)
H = np.tanh(Z)
hs.append(H)
return zs, hs, H @ Wo_
def shared_loss(Ws_, Wo_):
_, _, Zout = shared_forward(Ws_, Wo_, X3)
return ce_loss(Zout, Y3)
zs, hs, Zout = shared_forward(Wsh, Wout, X3)
P3 = softmax(Zout)
Dout = (P3 - Y3) / n3
gWout = hs[-1].T @ Dout
Dh = Dout @ Wout.T
gWsh = np.zeros_like(Wsh)
for t in range(T - 1, -1, -1):
Dz = Dh * (1 - np.tanh(zs[t]) ** 2)
gWsh += hs[t].T @ Dz
Dh = Dz @ Wsh.T
def numgrad_mat(f, M, h=1e-5):
g = np.zeros_like(M)
it = np.nditer(M, flags=['multi_index'])
while not it.finished:
k = it.multi_index
old = M[k]
M[k] = old + h
lp = f()
M[k] = old - h
lm = f()
M[k] = old
g[k] = (lp - lm) / (2 * h)
it.iternext()
return g
gWsh_num = numgrad_mat(lambda: shared_loss(Wsh, Wout), Wsh)
gWout_num = numgrad_mat(lambda: shared_loss(Wsh, Wout), Wout)
print(" 같은 W 를 %d 번 되풀이해 쓰고 마지막에 출력 가중치를 붙입니다" % T)
print(" " + rw("무엇", 16) + " " + rl("모양", 12) + " " + rl("수치 미분과의 차", 18))
print(" " + rw("공유 가중치", 16) + " " + rl(str(Wsh.shape), 12) + " " + rl("%.10f" % maxdiff(gWsh, gWsh_num), 18))
print(" " + rw("출력 가중치", 16) + " " + rl(str(Wout.shape), 12) + " " + rl("%.10f" % maxdiff(gWout, gWout_num), 18))
print(" 세 시각의 기여를 더한 것이 맞습니다")
print()
print(" (3) 시각별 기여를 나눠 봅니다")
zs, hs, Zout = shared_forward(Wsh, Wout, X3)
Dh = Dout @ Wout.T
parts = []
for t in range(T - 1, -1, -1):
Dz = Dh * (1 - np.tanh(zs[t]) ** 2)
parts.append((t + 1, hs[t].T @ Dz))
Dh = Dz @ Wsh.T
parts = parts[::-1]
print(" " + rl("시각", 8) + " " + rl("그 시각의 기여 크기", 22) + " " + rl("전체에서의 몫", 18))
tot_abs = sum(float(np.abs(p).sum()) for _, p in parts)
for t, p in parts:
print(" " + rl("%d" % t, 8) + " " + rl("%.6e" % float(np.abs(p).sum()), 22) + " " + rl("%.6f" % (float(np.abs(p).sum()) / tot_abs), 18))
print(" 이 자료에서는 앞쪽 시각의 기여가 더 큽니다")
print(" 뒤로 갈수록 신호는 줄지만 앞쪽 은닉값이 아직 포화 전이라 더 크기 때문입니다")
print(" 어느 쪽이 클지는 신호 감쇠와 은닉값 크기가 함께 정합니다")
print(" 더한 값과 한꺼번에 계산한 값의 차이는 %.10f 입니다" % maxdiff(sum(p for _, p in parts), gWsh))
print(" 251강의 시간축 역전파가 정확히 이 계산입니다")
print()
print(" (4) 더하지 않고 마지막 것만 쓰면 어떻게 되는지 봅니다")
last_only = parts[-1][1]
print(" " + rw("무엇으로 계산", 22) + " " + rl("수치 미분과의 차", 18) + " " + rl("참값 대비 크기", 18))
print(" " + rw("세 시각을 더함", 22) + " " + rl("%.10f" % maxdiff(gWsh, gWsh_num), 18) + " " + rl("%.6f" % 1.0, 18))
print(" " + rw("마지막 시각만", 22) + " " + rl("%.10f" % maxdiff(last_only, gWsh_num), 18) + " " + rl("%.6f" % (float(np.abs(last_only).sum()) / float(np.abs(gWsh).sum())), 18))
print(" 마지막 시각만 쓰면 크게 틀립니다")
print(" 232강 문제 2 의 갈래를 안 더한 경우와 같은 잘못입니다")
print()
print("문제 4. 정말 배우는지 확인하기")
print()
print(" (1) 무엇을 확인해야 하는지 정리합니다")
rows = [
("그래디언트 검사", "수치 미분과 견줌", "가장 먼저"),
("작은 자료 외우기", "표본 몇 개를 완전히 맞힘", "구현이 맞는지"),
("손실이 줄어드는가", "첫 걸음부터", "학습률이 맞는지"),
("첫 손실 값", "무작위 예측의 값", "초기화가 맞는지"),
]
w = [max(pw(r[i]) for r in rows + [("무엇", "어떻게", "무엇을 잡나")]) for i in range(3)]
print(" " + rw("무엇", w[0]) + " " + rw("어떻게", w[1]) + " " + "무엇을 잡나")
for r in rows:
print(" " + rw(r[0], w[0]) + " " + rw(r[1], w[1]) + " " + r[2])
print()
print(" (2) 첫 손실이 무작위 예측의 값인지 봅니다")
r5 = np.random.default_rng(60233)
print(" 갈래가 K 개면 아무것도 모를 때 손실은 로그 K 입니다")
print(" " + rl("갈래 수", 10) + " " + rl("로그 갈래 수", 14) + " " + rl("초기 손실", 14) + " " + rl("차이", 14))
for K in [2, 3, 5, 10]:
Xk = r5.normal(0, 1, (200, 8))
lk = r5.integers(0, K, 200)
Yk = np.zeros((200, K))
Yk[np.arange(200), lk] = 1.0
Wk, bk = init([8, 16, K], np.random.default_rng(5))
Wk[-1] = Wk[-1] * 0.01
_, _, Lk = backward(Wk, bk, Xk, Yk)
print(" " + rl("%d" % K, 10) + " " + rl("%.6f" % float(np.log(K)), 14) + " " + rl("%.6f" % Lk, 14) + " " + rl("%.6f" % abs(Lk - np.log(K)), 14))
print(" 마지막 층을 아주 작게 초기화하면 네 경우 모두 로그 갈래 수에 붙습니다")
print(" 출력이 0 에 가까우면 소프트맥스가 균등분포를 내기 때문입니다")
print(" 이 값에서 크게 벗어나면 초기화나 손실 구현이 틀린 것입니다")
print()
print(" (3) 작은 자료를 외우게 해 봅니다")
r6 = np.random.default_rng(70233)
ns = 8
Xs = r6.normal(0, 1, (ns, 6))
ls = np.arange(ns) % 3
Ys = np.zeros((ns, 3))
Ys[np.arange(ns), ls] = 1.0
Wm, bm = init([6, 32, 32, 3], np.random.default_rng(11))
print(" 표본 %d 개를 완전히 외우게 합니다. 못 외우면 구현이 틀린 것입니다" % ns)
print(" " + rl("걸음", 10) + " " + rl("손실", 14) + " " + rl("정확도", 12))
for t in range(1, 601):
gWm, gbm, Lm = backward(Wm, bm, Xs, Ys)
for i in range(len(Wm)):
Wm[i] -= 0.5 * gWm[i]
bm[i] -= 0.5 * gbm[i]
if t in (1, 50, 200, 600):
_, acts_m = forward(Wm, bm, Xs)
acc = float((acts_m[-1].argmax(axis=1) == ls).mean())
print(" " + rl("%d" % t, 10) + " " + rl("%.8f" % Lm, 14) + " " + rl("%.6f" % acc, 12))
print(" 손실이 0 에 가까워지고 정확도가 1 이 됩니다")
print(" 210강에서 본 과적합을 여기서는 일부러 만듭니다. 구현 검사이기 때문입니다")
print()
print(" (4) 흔한 구현 실수 넷을 만들어 봅니다")
r7 = np.random.default_rng(80233)
n4 = 50
X4 = r7.normal(0, 1, (n4, 6))
l4 = r7.integers(0, 3, n4)
Y4 = np.zeros((n4, 3))
Y4[np.arange(n4), l4] = 1.0
W4, b4 = init([6, 10, 3], np.random.default_rng(13))
gW4t, gb4t, _ = backward(W4, b4, X4, Y4)
ref = numgrad_param(W4, b4, X4, Y4, 'W', 0)
def wrong_no_transpose():
pre, acts = forward(W4, b4, X4)
P = softmax(pre[-1])
D = (P - Y4) / n4
D1 = (D @ W4[1]) * drelu(pre[0]) if W4[1].shape[0] == D.shape[1] else (D @ W4[1].T) * drelu(pre[0])
return acts[0].T @ D1
def wrong_no_actderiv():
pre, acts = forward(W4, b4, X4)
P = softmax(pre[-1])
D = (P - Y4) / n4
D1 = D @ W4[1].T
return acts[0].T @ D1
def wrong_no_mean():
pre, acts = forward(W4, b4, X4)
P = softmax(pre[-1])
D = (P - Y4)
D1 = (D @ W4[1].T) * drelu(pre[0])
return acts[0].T @ D1
def wrong_relu_on_last():
pre, acts = forward(W4, b4, X4)
P = softmax(relu(pre[-1]))
D = (P - Y4) / n4
D1 = (D @ W4[1].T) * drelu(pre[0])
return acts[0].T @ D1
wrongs = [
("바르게 계산", gW4t[0]),
("활성 도함수를 빼먹음", wrong_no_actderiv()),
("배치로 안 나눔", wrong_no_mean()),
("마지막 층에 활성을 넣음", wrong_relu_on_last()),
]
w0 = max(pw(x[0]) for x in wrongs)
print(" 첫 층 가중치 그래디언트를 여러 방식으로 계산합니다")
print(" " + rw("무엇을 했나", w0) + " " + rl("수치 미분과의 차", 18) + " " + rl("참값 대비 크기", 18))
for nm, g in wrongs:
print(" " + rw(nm, w0) + " " + rl("%.10f" % maxdiff(g, ref), 18) + " " + rl("%.6f" % (float(np.abs(g).sum()) / float(np.abs(ref).sum())), 18))
print(" 배치로 안 나누면 크기가 정확히 표본 수 배가 됩니다")
print(" 나머지 둘은 크기도 방향도 틀립니다")
print(" 그래디언트 검사 하나가 이 넷을 모두 잡아냅니다")
print()
print("문제 5. 실무에서 쓰기")
print()
print(" (1) 검사 절차를 정리합니다")
rows = [
("작게 만든다", "차원과 표본을 줄입니다"),
("배정밀도로 검사한다", "단정밀도는 오차가 큽니다"),
("상대 오차로 판정한다", "1e-5 이하면 통과입니다"),
("정류 선형 근처를 피한다", "꺾인 자리에서 어긋납니다"),
("무작위를 고정한다", "드롭아웃 씨앗을 고정합니다"),
]
w = [max(pw(r[i]) for r in rows + [("무엇을", "왜")]) for i in range(2)]
print(" " + rw("무엇을", w[0]) + " " + "왜")
for r in rows:
print(" " + rw(r[0], w[0]) + " " + r[1])
print()
print(" (2) 정류 선형의 꺾인 자리에서 검사가 어긋나는 것을 봅니다")
r8 = np.random.default_rng(90233)
n5 = 20
X5 = r8.normal(0, 1, (n5, 4))
l5 = r8.integers(0, 2, n5)
Y5 = np.zeros((n5, 2))
Y5[np.arange(n5), l5] = 1.0
W5, b5 = init([4, 8, 2], np.random.default_rng(17))
print(" 치우침을 조금씩 옮겨 0 근처에 있는 선형 결과의 개수를 바꿉니다")
print(" " + rl("간격", 12) + " " + rl("0 에서 간격 안에 있는 수", 26) + " " + rl("상대 오차", 16))
for hh in [1e-2, 1e-4, 1e-6]:
pre5, _ = forward(W5, b5, X5)
near = int((np.abs(pre5[0]) < hh).sum())
ga = backward(W5, b5, X5, Y5)[0][0]
gn = numgrad_param(W5, b5, X5, Y5, 'W', 0, hh)
den = np.maximum(np.abs(ga) + np.abs(gn), 1e-12)
print(" " + rl("%.0e" % hh, 12) + " " + rl("%d" % near, 26) + " " + rl("%.6e" % float((np.abs(ga - gn) / den).max()), 16))
print(" 꺾인 자리에 걸린 단위가 없으면 검사가 깨끗하게 통과합니다")
print(" 걸린 단위가 있으면 그 원소만 크게 어긋납니다. 나머지는 멀쩡합니다")
print()
print(" (3) 정밀도를 바꿔 봅니다")
print(" 같은 검사를 32 비트로 하면 어떻게 되는지 봅니다")
print(" " + rw("정밀도", 12) + " " + rl("상대 오차", 16))
for nm, dt in [("64 비트", np.float64), ("32 비트", np.float32)]:
Wd = [Wq.astype(dt) for Wq in init([4, 8, 2], np.random.default_rng(17))[0]]
bd = [bq.astype(dt) for bq in init([4, 8, 2], np.random.default_rng(17))[1]]
Xd = X5.astype(dt)
Yd = Y5.astype(dt)
ga = backward(Wd, bd, Xd, Yd)[0][0]
gn = numgrad_param(Wd, bd, Xd, Yd, 'W', 0, dt(1e-4))
den = np.maximum(np.abs(ga) + np.abs(gn), 1e-12)
print(" " + rw(nm, 12) + " " + rl("%.6e" % float((np.abs(ga - gn) / den).max()), 16))
print(" 32 비트에서는 상대 오차가 1 이 되어 검사가 완전히 무너집니다")
print(" 빼기에서 유효숫자가 사라지기 때문입니다. 검사는 64 비트로 합니다")
print()
print(" (4) 이 강의를 한 장으로 모읍니다")
rows = [
("역전파는 무엇인가", "그래프 훑기를 층 위에 적은 것입니다"),
("두 규칙", "입력 전치 곱 신호와 가중치 전치 곱 신호입니다"),
("비용", "앞으로 가는 것의 두 배쯤입니다"),
("깊으면", "신호가 곱해지며 줄거나 커집니다"),
("공유하면", "쓰인 횟수만큼 더합니다"),
("맞는지 어떻게 아나", "수치 미분과 작은 자료 외우기입니다"),
("첫 손실", "로그 갈래 수여야 합니다"),
]
w = [max(pw(r[i]) for r in rows + [("물음", "한 줄로")]) for i in range(2)]
print(" " + rw("물음", w[0]) + " " + "한 줄로")
for r in rows:
print(" " + rw(r[0], w[0]) + " " + r[1])
print(" 234강은 이 계산을 반복문 없이 행렬 하나로 적습니다")
print()
print("=" * 78)
print("검산 끝")
print("=" * 78)
# ==============================================================================
# 233강 역전파 알고리즘 코드 검산
# ==============================================================================
#
# 문제 1. 알고리즘을 적기
#
# (1) 네 단계로 적습니다
# 단계 무엇을 하나 구체적으로
# 1 앞으로 가며 값을 저장 각 층의 선형 결과와 활성 결과
# 2 마지막 층의 신호를 만듦 손실의 미분
# 3 층마다 파라미터 그래디언트 입력 전치 곱하기 신호
# 4 신호를 앞 층으로 넘김 가중치 전치 곱하고 활성 도함수 곱
# 3 과 4 를 층마다 되풀이합니다. 그것이 전부입니다
# 232강의 그래프 훑기에서 마디 하나가 층 하나로 굵어졌을 뿐입니다
#
# (2) 세 층 신경망에서 그래디언트를 확인합니다
# 층 구조는 5 에서 7 에서 7 에서 3 입니다
# 처음 손실은 2.548788 이고 아무것도 모를 때의 값은 1.098612 입니다
# 무엇 모양 역전파와 수치 미분의 차
# W1 (5, 7) 0.0000000000
# W2 (7, 7) 0.0000000000
# W3 (7, 3) 0.0000000000
# b1 (7,) 0.0000000000
# b2 (7,) 0.0000000000
# b3 (3,) 0.0000000000
# 여섯 개 모두 수치 미분과 소수점 아래 여덟 자리까지 같습니다
# 231강에서 손으로 유도한 두 규칙이 층 수와 무관하게 그대로 쓰입니다
#
# (3) 비용을 셉니다
# 곱셈 횟수를 셉니다. 층 하나가 m 에서 k 로 갈 때 배치 B 면 B 곱하기 m 곱하기 k 입니다
# 무엇 곱셈 횟수 앞으로 대비
# 앞으로 가기 4200 1.0000
# 가중치 그래디언트 4200 1.0000
# 신호 넘기기 2800 0.6667
# 전체 11200 2.6667
# 뒤로 가는 비용이 앞으로 가는 비용의 두 배쯤입니다
# 첫 층은 입력의 그래디언트가 필요 없어 신호를 안 넘깁니다
#
# 문제 2. 신호가 층을 지나며 어떻게 되는가
#
# (1) 무엇이 신호를 키우고 줄이는지 정리합니다
# 무엇 어떻게 작용 누가 정하나
# 가중치 크기 곱해짐 초기화가 정함
# 활성 도함수 곱해짐 0 이면 막힘
# 층 수 거듭제곱 깊을수록 심함
# 잔차 연결 1 을 더함 길을 하나 더 냄
#
# (2) 깊이를 늘리며 층별 그래디언트 크기를 봅니다
# 너비가 20 이므로 정착 초기화의 표준편차는 0.3162 입니다
# 그보다 작은 0.15 로 두고 깊이를 늘립니다
# 깊이 첫 층 그래디언트 크기 마지막 층 크기 첫 층 나누기 마지막
# 4 1.8558e-03 3.7242e-03 4.9830e-01
# 8 8.2004e-05 1.8556e-04 4.4193e-01
# 16 3.7469e-07 6.8033e-07 5.5074e-01
# 32 6.0076e-12 6.4458e-12 9.3202e-01
# 깊이가 늘수록 첫 층 그래디언트가 급격히 작아집니다
# 이번에는 표준편차를 0.5 로 키워 같은 것을 봅니다
# 깊이 첫 층 그래디언트 크기 마지막 층 크기 첫 층 나누기 마지막
# 4 1.0840e-01 7.0025e-01 1.5480e-01
# 8 7.2503e-01 9.2632e+00 7.8271e-02
# 16 4.0630e+01 5.2076e+02 7.8021e-02
# 32 1.8137e+05 1.3449e+06 1.3486e-01
# 이번에는 반대로 터집니다. 깊이 32 에서 첫 층 크기가 10 의 5 제곱을 넘습니다
# 층마다 곱해지는 값이 1 보다 작으면 사라지고 크면 터집니다
# 정착 초기화는 그 값을 1 근처에 두려는 시도입니다
# 246강에서 이 현상을 정면으로 다룹니다
#
# (3) 초기화 크기를 바꿔 봅니다
# 깊이를 16 으로 고정하고 초기화 표준편차만 바꿉니다
# 표준편차 첫 층 크기 마지막 층 크기 비
# 0.1500 2.8589e-07 2.3981e-07 1.1921e+00
# 0.2500 6.0791e-04 5.1518e-04 1.1800e+00
# 0.3162 2.7990e-02 8.7998e-02 3.1808e-01
# 0.5000 3.1617e+01 1.3051e+02 2.4226e-01
# 정착 초기화는 표준편차를 2 나누기 입력차원의 제곱근으로 둡니다
# 여기서는 0.3162 이고 표에서 셋째 줄입니다
# 절대 크기는 표준편차를 키울수록 커집니다. 16 층을 지나며 증폭되기 때문입니다
# 243강에서 이 값이 어디서 나오는지 유도합니다
#
# (4) 죽은 정류 선형이 몇 개인지 셉니다
# 정류 선형은 입력이 음수면 그래디언트를 0 으로 막습니다
# 치우침 초깃값 살아 있는 단위 비율 첫 층 그래디언트 크기
# -2.0000 0.142708 1.1077e-02
# -0.5000 0.386458 4.7287e-02
# 0.0000 0.503125 5.3433e-02
# 0.5000 0.614583 6.1115e-02
# 치우침을 음수로 밀면 살아 있는 단위가 줄고 그래디언트도 함께 줄어듭니다
# 한 단위가 모든 표본에서 음수가 되면 영원히 안 배웁니다
# 240강에서 정류 선형의 변형들이 이 문제를 어떻게 푸는지 봅니다
#
# 문제 3. 파라미터를 공유할 때
#
# (1) 무엇이 달라지는지 정리합니다
# 무엇 무엇이 생기나 어떻게
# 따로 쓰는 파라미터 그래디언트가 하나 그냥 받음
# 여러 번 쓰는 파라미터 그래디언트가 여럿 다 더함
# 어디서 나오나 순환 신경망 합성곱 같은 W 를 되풀이
# 232강과의 관계 갈래가 여럿인 마디 규칙이 같음
#
# (2) 같은 가중치를 세 번 쓰는 신경망을 만듭니다
# 같은 W 를 3 번 되풀이해 쓰고 마지막에 출력 가중치를 붙입니다
# 무엇 모양 수치 미분과의 차
# 공유 가중치 (6, 6) 0.0000000000
# 출력 가중치 (6, 2) 0.0000000000
# 세 시각의 기여를 더한 것이 맞습니다
#
# (3) 시각별 기여를 나눠 봅니다
# 시각 그 시각의 기여 크기 전체에서의 몫
# 1 7.563109e-01 0.496561
# 2 4.768789e-01 0.313098
# 3 2.899071e-01 0.190341
# 이 자료에서는 앞쪽 시각의 기여가 더 큽니다
# 뒤로 갈수록 신호는 줄지만 앞쪽 은닉값이 아직 포화 전이라 더 크기 때문입니다
# 어느 쪽이 클지는 신호 감쇠와 은닉값 크기가 함께 정합니다
# 더한 값과 한꺼번에 계산한 값의 차이는 0.0000000000 입니다
# 251강의 시간축 역전파가 정확히 이 계산입니다
#
# (4) 더하지 않고 마지막 것만 쓰면 어떻게 되는지 봅니다
# 무엇으로 계산 수치 미분과의 차 참값 대비 크기
# 세 시각을 더함 0.0000000000 1.000000
# 마지막 시각만 0.1079901881 0.302349
# 마지막 시각만 쓰면 크게 틀립니다
# 232강 문제 2 의 갈래를 안 더한 경우와 같은 잘못입니다
#
# 문제 4. 정말 배우는지 확인하기
#
# (1) 무엇을 확인해야 하는지 정리합니다
# 무엇 어떻게 무엇을 잡나
# 그래디언트 검사 수치 미분과 견줌 가장 먼저
# 작은 자료 외우기 표본 몇 개를 완전히 맞힘 구현이 맞는지
# 손실이 줄어드는가 첫 걸음부터 학습률이 맞는지
# 첫 손실 값 무작위 예측의 값 초기화가 맞는지
#
# (2) 첫 손실이 무작위 예측의 값인지 봅니다
# 갈래가 K 개면 아무것도 모를 때 손실은 로그 K 입니다
# 갈래 수 로그 갈래 수 초기 손실 차이
# 2 0.693147 0.692910 0.000237
# 3 1.098612 1.098434 0.000178
# 5 1.609438 1.609108 0.000330
# 10 2.302585 2.303329 0.000744
# 마지막 층을 아주 작게 초기화하면 네 경우 모두 로그 갈래 수에 붙습니다
# 출력이 0 에 가까우면 소프트맥스가 균등분포를 내기 때문입니다
# 이 값에서 크게 벗어나면 초기화나 손실 구현이 틀린 것입니다
#
# (3) 작은 자료를 외우게 해 봅니다
# 표본 8 개를 완전히 외우게 합니다. 못 외우면 구현이 틀린 것입니다
# 걸음 손실 정확도
# 1 1.34042830 0.750000
# 50 0.00550335 1.000000
# 200 0.00088885 1.000000
# 600 0.00023540 1.000000
# 손실이 0 에 가까워지고 정확도가 1 이 됩니다
# 210강에서 본 과적합을 여기서는 일부러 만듭니다. 구현 검사이기 때문입니다
#
# (4) 흔한 구현 실수 넷을 만들어 봅니다
# 첫 층 가중치 그래디언트를 여러 방식으로 계산합니다
# 무엇을 했나 수치 미분과의 차 참값 대비 크기
# 바르게 계산 0.0000000000 1.000000
# 활성 도함수를 빼먹음 0.1871254648 1.422084
# 배치로 안 나눔 10.0919940387 50.000000
# 마지막 층에 활성을 넣음 0.1249904025 0.713008
# 배치로 안 나누면 크기가 정확히 표본 수 배가 됩니다
# 나머지 둘은 크기도 방향도 틀립니다
# 그래디언트 검사 하나가 이 넷을 모두 잡아냅니다
#
# 문제 5. 실무에서 쓰기
#
# (1) 검사 절차를 정리합니다
# 무엇을 왜
# 작게 만든다 차원과 표본을 줄입니다
# 배정밀도로 검사한다 단정밀도는 오차가 큽니다
# 상대 오차로 판정한다 1e-5 이하면 통과입니다
# 정류 선형 근처를 피한다 꺾인 자리에서 어긋납니다
# 무작위를 고정한다 드롭아웃 씨앗을 고정합니다
#
# (2) 정류 선형의 꺾인 자리에서 검사가 어긋나는 것을 봅니다
# 치우침을 조금씩 옮겨 0 근처에 있는 선형 결과의 개수를 바꿉니다
# 간격 0 에서 간격 안에 있는 수 상대 오차
# 1e-02 1 7.096153e-04
# 1e-04 0 4.743638e-09
# 1e-06 0 1.906625e-08
# 꺾인 자리에 걸린 단위가 없으면 검사가 깨끗하게 통과합니다
# 걸린 단위가 있으면 그 원소만 크게 어긋납니다. 나머지는 멀쩡합니다
#
# (3) 정밀도를 바꿔 봅니다
# 같은 검사를 32 비트로 하면 어떻게 되는지 봅니다
# 정밀도 상대 오차
# 64 비트 4.743638e-09
# 32 비트 1.000000e+00
# 32 비트에서는 상대 오차가 1 이 되어 검사가 완전히 무너집니다
# 빼기에서 유효숫자가 사라지기 때문입니다. 검사는 64 비트로 합니다
#
# (4) 이 강의를 한 장으로 모읍니다
# 물음 한 줄로
# 역전파는 무엇인가 그래프 훑기를 층 위에 적은 것입니다
# 두 규칙 입력 전치 곱 신호와 가중치 전치 곱 신호입니다
# 비용 앞으로 가는 것의 두 배쯤입니다
# 깊으면 신호가 곱해지며 줄거나 커집니다
# 공유하면 쓰인 횟수만큼 더합니다
# 맞는지 어떻게 아나 수치 미분과 작은 자료 외우기입니다
# 첫 손실 로그 갈래 수여야 합니다
# 234강은 이 계산을 반복문 없이 행렬 하나로 적습니다
#
# ==============================================================================
# 검산 끝
# ==============================================================================