강의 역전파는 층마다 반복문을 돌았고, 그 안에서 다시 표본마다 돌았습니다. 바깥 반복문은 어쩔 수 없지만 안쪽은 다릅니다.
강 문제 에서 가 바깥곱 개의 합과 같다는 것을 봤습니다. 그 항등식이 딥러닝 코드 전체의 기본 문법입니다.
이 강의는 모양표를 먼저 그리고 식을 나중에 채우는 유도 방식을 익히고, 두 층 신경망과 잔차 연결을 그 방식으로 직접 유도합니다.
문제. 무엇이 접히는지 봅니다.
() 무엇을 접는지 정리하세요.
() 표본 반복을 접으세요.
() 무엇이 줄어드는지 세세요.
생각의 실마리. 반복문이 접히려면 각 걸음이 서로 독립이어야 합니다. 표본 의 계산이 표본 의 결과를 안 쓰면 접힙니다.
풀이. () 정리합니다.
| 무엇에 대한 반복 | 무엇이 되나 | 구체적으로 |
|---|---|---|
| 표본에 대한 반복 | 행렬 곱 하나 | |
| 출력 차원에 대한 반복 | 같은 행렬 곱 안 | 열이 출력 차원 |
| 층에 대한 반복 | 못 접음 | 순서가 있기 때문 |
| 시각에 대한 반복 | 못 접음 | 앞이 뒤를 만들기 때문 |
앞의 둘은 서로 독립이라 접힙니다. 뒤의 둘은 의존이 있어 못 접습니다.
그래서 층과 시각만 반복문으로 남습니다. 실제 딥러닝 코드에서 파이썬 반복문이 딱 그 둘뿐인 이유입니다.
() 표본 반복을 접습니다. 배치 개입니다.
| 어떻게 계산 | 모양 | 첫 원소 | 차이 |
|---|---|---|---|
| 바깥곱 개를 더함 | |||
| 행렬 곱 한 번 |
반복문 번이 행렬 곱 한 번이 됩니다.
() 무엇이 줄어드는지 셉니다.
| 배치 | 입력차원 | 출력차원 | 곱셈 횟수 | 반복문 판의 파이썬 걸음 | 행렬 곱 판의 걸음 |
|---|---|---|---|---|---|
곱셈 횟수는 두 판이 정확히 같습니다. 줄어드는 것은 파이썬이 도는 횟수입니다.
파이썬 반복문 한 걸음마다 형 검사와 함수 호출이 붙습니다. 행렬 곱은 그 비용을 한 번만 치르고 안에서 연속된 메모리를 훑습니다.
이 문제에서 배우는 것. 벡터화는 계산량을 줄이는 것이 아닙니다. 같은 곱셈을 다른 순서로 다른 층위에서 하는 것이고, 그 차이가 실제 속도를 정합니다.
확인 1-1. 접히는 반복과 안 접히는 반복을 가르는 기준을 쓰세요.
답. 각 걸음이 서로 독립이면 접히고 의존이 있으면 안 접힙니다.
확인 1-2. 검산에서 바깥곱을 더한 것과 행렬 곱의 첫 원소를 쓰세요.
답. 둘 다 입니다.
확인 1-3. 검산에서 배치 일 때 곱셈 횟수와 파이썬 걸음 수를 쓰세요.
답. 와 입니다.
문제. 모양으로 식을 찾습니다.
() 모양표를 그리세요.
() 맞는 조합이 하나인지 세세요.
() 수치로 확인하세요.
() 평균과 합을 견주세요.
생각의 실마리. 식을 외우려 하지 말고 모양을 먼저 적습니다. 그러면 가능한 조합이 몇 개 안 남고, 대개 하나만 맞습니다.
풀이. () 모양표를 그립니다.
| 무엇 | 모양 | 무엇인가 |
|---|---|---|
| 입력 | ||
| 가중치 | ||
| 치우침 | ||
| 선형 결과 | ||
| 에 대한 미분 | ||
| \Delta W^ |
() 맞는 조합이 정말 하나인지 셉니다. , , 입니다.
| 어떤 조합 | 왼쪽 모양 | 오른쪽 모양 | 곱이 되나 | 결과 모양 |
|---|---|---|---|---|
| 예 | ||||
| 예 | ||||
| X\Delta^ | 아니오 | 안 됨 | ||
| \Delta X^ | 아니오 | 안 됨 |
곱이 되는 것 자체는 둘인데 결과 모양이 맞는 것은 첫째 줄 하나뿐입니다.
둘째 줄은 이라 전치를 한 번 더 해야 합니다. 그러면 첫째 줄과 같아집니다.
() 수치로 확인합니다.
| 무엇에 대한 | 유도한 식 | 상대 오차 |
|---|---|---|
| 의 열 합 | ||
| UW^ |
세 식 모두 상대 오차가 아래입니다. 강의 판정 기준 를 넉넉히 통과합니다.
() 손실을 평균으로 둘 때와 합으로 둘 때를 견줍니다.
| 손실 정의 | 유도한 식 | 상대 오차 | 크기 비 |
|---|---|---|---|
| 합 | |||
| 평균 | 배치로 나눔 |
크기가 정확히 배치 수 배 차이입니다. 입니다.
강 문제 에서 이것을 빼먹으면 학습률이 배치 수 배가 된다고 했습니다. 손실 정의를 바꾸면 학습률도 함께 바꿔야 합니다.
이 문제에서 배우는 것. 모양표를 먼저 그리면 식을 외울 필요가 없습니다. 후보가 네 개로 줄고 그중 결과 모양이 맞는 것은 하나뿐입니다.
확인 2-1. 모양이 맞는 조합을 찾는 방법을 쓰세요.
답. 결과가 있어야 할 모양을 적고 그 모양이 나오는 곱을 찾습니다.
확인 2-2. 검산에서 에 대한 유도 식과 상대 오차를 쓰세요.
답. 이고 입니다.
확인 2-3. 검산에서 손실을 평균으로 둘 때의 크기 비를 쓰세요.
답. 입니다.
문제. 원소별 연산을 다룹니다.
() 원소별 연산을 정리하세요.
() 네 활성함수를 확인하세요.
() 소프트맥스와 교차엔트로피를 배치로 확인하세요.
() 행과 열을 헷갈리면 어떻게 되는지 보세요.
생각의 실마리. 활성함수는 원소마다 따로 적용됩니다. 그러면 야코비가 대각이고, 대각행렬 곱은 원소별 곱입니다.
풀이. () 정리합니다.
| 함수 | 도함수 | 어떻게 계산 |
|---|---|---|
| 정류 선형 | 보다 큰지 | 지시함수와 곱 |
| 하이퍼볼릭 탄젠트 | 빼기 제곱 | 출력으로 계산 |
| 로지스틱 | 출력으로 계산 | |
| 소프트플러스 | 로지스틱 | 다른 함수의 값 |
| 항등 | 그대로 통과 |
둘째와 셋째 줄은 출력만 있으면 도함수가 나옵니다. 입력을 안 저장해도 되므로 메모리가 절약됩니다.
() 네 활성함수를 한꺼번에 확인합니다.
| 함수 | 상대 오차 |
|---|---|
| 정류 선형 | |
| 하이퍼볼릭 탄젠트 | |
| 로지스틱 | |
| 소프트플러스 |
네 함수 모두 원소별 곱 한 번으로 끝납니다. 강 심화 에서 본 대로 야코비가 대각이기 때문입니다.
() 소프트맥스와 교차엔트로피를 배치로 확인합니다. 배치 개이고 갈래 개입니다.
| 무엇 | 상대 오차 |
|---|---|
| 소프트맥스 빼기 정답 나누기 배치 |
행마다 독립이라 배치가 그냥 축 하나로 붙습니다.
강 문제 의 한 표본 결과가 그대로 행에 놓입니다. 배치를 위해 새로 유도할 것이 없습니다.
() 행과 열을 헷갈리면 어떻게 되는지 봅니다.
| 무엇을 했나 | 상대 오차 | 참값 대비 크기 |
|---|---|---|
| 바르게 계산 | ||
| 소프트맥스를 열 방향으로 | ||
| 갈래 수로 나눔 |
열 방향 소프트맥스는 모양이 우연히 맞아 오류 없이 돌아갑니다. 상대 오차가 인데도 예외 하나 안 납니다.
그래서 모양 검사만으로는 안 잡힙니다. 반드시 수치로 봐야 합니다.
이 문제에서 배우는 것. 원소별 연산은 벡터화가 쉽지만 어느 축을 따라 정규화하는지는 자동으로 안 정해집니다. 소프트맥스처럼 축이 뜻을 갖는 연산에서 실수가 조용히 숨습니다.
확인 3-1. 활성함수의 야코비가 대각인 이유를 쓰세요.
답. 출력의 각 원소가 같은 자리 입력에만 달렸기 때문입니다.
확인 3-2. 검산에서 하이퍼볼릭 탄젠트의 상대 오차를 쓰세요.
답. 입니다.
확인 3-3. 검산에서 소프트맥스를 열 방향으로 했을 때의 상대 오차를 쓰세요.
답. 입니다.
문제. 실제 신경망을 유도합니다.
() 유도 절차를 정리하세요.
() 두 층 신경망을 유도하세요.
() 반복문 판과 견주세요.
() 잔차 연결을 유도하세요.
생각의 실마리. 절차는 언제나 같습니다. 모양을 적고, 손실에서 시작하고, 층마다 두 줄을 쓰고, 수치로 검산합니다.
풀이. () 절차를 정리합니다.
| 단계 | 무엇을 하나 | 요령 |
|---|---|---|
| 앞으로 가는 식을 적습니다 | 모양을 옆에 적어 둡니다 | |
| 손실에서 시작합니다 | 델타의 모양은 마지막 와 같습니다 | |
| 층마다 두 줄을 씁니다 | 파라미터 하나와 신호 하나 | |
| 모양을 확인합니다 | 안 맞으면 전치를 바꿉니다 | |
| 수치로 검산합니다 | 상대 오차 이하 |
() 두 층 신경망을 유도합니다. 구조는 이고 배치는 입니다.
| 무엇 | 모양 | 유도한 식 | 상대 오차 |
|---|---|---|---|
| 의 열 합 | |||
| 의 열 합 |
반복문 없이 여섯 줄로 두 층을 모두 유도했습니다.
() 반복문 판과 견줍니다.
| 무엇 | 두 판의 최대 차이 |
|---|---|
차이가 이므로 부동소수점 마지막 자리 수준입니다.
덧셈 순서만 다르고 값은 같습니다. 벡터화는 답을 바꾸지 않습니다.
() 잔차 연결을 유도합니다. 출력에 입력을 더합니다.
| 무엇에 대한 | 유도한 식 | 상대 오차 |
|---|---|---|
| 잔차를 빠뜨림 | 만 |
더하기 마디라 신호가 그대로 한 벌 더 옵니다.
강 문제 에서 갈래는 뒤에서 더한다고 했습니다. 잔차 연결은 가 두 곳으로 가는 갈래이므로 두 경로가 더해집니다.
빠뜨리면 상대 오차가 입니다. 이 직통 경로가 깊은 신경망에서 신호를 살리고, 강에서 다시 봅니다.
이 문제에서 배우는 것. 유도 절차는 구조가 바뀌어도 그대로입니다. 잔차 연결이든 곁가지든 그래프에서 갈래가 어디인지만 보면 나머지는 기계적입니다.
확인 4-1. 유도 절차의 단계를 쓰세요.
답. 층마다 파라미터 그래디언트 한 줄과 신호 한 줄을 씁니다.
확인 4-2. 검산에서 의 반복문 판과 벡터화 판의 차이를 쓰세요.
답. 입니다.
확인 4-3. 검산에서 잔차를 빠뜨렸을 때의 상대 오차를 쓰세요.
답. 입니다.
문제. 실수를 피합니다.
() 흔한 모양 실수를 정리하세요.
() 정사각 행렬에서 전치 실수를 보세요.
() 브로드캐스트가 조용히 하는 일을 보세요.
() 이 단원을 한 장으로 모으세요.
생각의 실마리. 모양이 안 맞으면 곧바로 예외가 납니다. 위험한 것은 모양이 우연히 맞는 경우입니다.
풀이. () 정리합니다.
| 무엇 | 어떻게 드러나나 | 찾기 |
|---|---|---|
| 전치를 빠뜨림 | 모양이 안 맞아 곧바로 터짐 | 쉬움 |
| 전치가 우연히 맞음 | 정사각 행렬일 때 | 어려움 |
| 배치 축을 헷갈림 | 소프트맥스를 열 방향으로 | 어려움 |
| 합과 평균을 헷갈림 | 크기만 배치 수 배 | 매우 어려움 |
| 브로드캐스트가 조용히 됨 | 모양이 늘어남 | 매우 어려움 |
() 정사각 행렬에서 전치 실수를 봅니다.
| 입력차원 | 출력차원 | 전치를 틀리면 | 상대 오차 |
|---|---|---|---|
| 모양이 안 맞음 | 잴 수 없음 | ||
| 모양이 맞음 | |||
| 모양이 맞음 |
정사각이면 전치를 틀려도 모양이 맞아 조용히 돌아갑니다.
은닉층 너비가 같은 신경망에서 특히 위험합니다. 실무의 신경망은 대개 은닉층 너비를 같게 두므로, 이 실수가 숨을 자리가 많습니다.
() 브로드캐스트가 조용히 하는 일을 봅니다. 치우침의 그래디언트는 배치 축으로 더한 것입니다.
| 어느 축으로 더했나 | 결과 모양 | 치우침 모양과 맞나 |
|---|---|---|
| 배치 축 | 맞음 | |
| 차원 축 | 안 맞음 | |
| 아예 안 더함 | 안 맞음 |
치우침이 차원이므로 배치 축으로 더해야 합니다.
그런데 더하지 않고 그대로 빼도 오류가 안 납니다. 결과 모양이 가 됩니다.
브로드캐스트가 치우침을 배치만큼 복제해 버립니다. 갱신이 조용히 잘못되고 손실은 그럴듯하게 줄어듭니다.
() 이 단원을 한 장으로 모읍니다.
| 강 | 무엇을 물었나 | 한 줄 답 |
|---|---|---|
| 무엇에 대해 미분하나 | 결과 모양은 분자 곱하기 분모 | |
| 연결을 누가 아나 | 그래프가 압니다 | |
| 층 위에서 어떻게 도나 | 앞으로 저장하고 뒤로 두 줄 | |
| 반복문을 어떻게 없애나 | 표본 축을 행렬 곱으로 접습니다 |
네 강의가 하나의 물음에 답합니다. 그래디언트를 어떻게 정확하고 빠르게 얻는가입니다.
이 문제에서 배우는 것. 파이썬과 넘파이는 친절해서 위험합니다. 브로드캐스트가 모양을 맞춰 주고 정사각 행렬이 전치를 감춰 주므로, 예외가 안 난다는 것이 맞다는 뜻이 아닙니다.
확인 5-1. 가장 위험한 모양 실수의 특징을 쓰세요.
답. 모양이 우연히 맞아 예외가 안 나는 경우입니다.
확인 5-2. 검산에서 정사각일 때 전치를 틀린 상대 오차를 쓰세요.
답. 입니다.
확인 5-3. 치우침 그래디언트를 안 더하면 결과 모양이 어떻게 되는지 쓰세요.
답. 브로드캐스트로 배치만큼 복제되어 가 됩니다.
| 유형 | 무엇을 묻나 | 어디를 보나 |
|---|---|---|
| 무엇이 접히나 | 걸음이 독립인가 | 문제 |
| 벡터화의 이득 | 곱셈이 아니라 파이썬 걸음 | 문제 |
| 모양표 | 맞는 조합은 하나 | 문제 |
| 합과 평균 | 배치 수 배 차이 | 문제 |
| 원소별 야코비 | 대각이라 곱 한 번 | 문제 |
| 축을 헷갈림 | 예외가 안 남 | 문제 |
| 두 층 유도 | 여섯 줄 | 문제 |
| 잔차 연결 | 신호가 한 벌 더 | 문제 |
| 정사각 함정 | 전치가 우연히 맞음 | 문제 |
| 브로드캐스트 | 조용히 복제 | 문제 |
벡터화된 역전파를 한자리에 모읍니다.
| 무엇 | 식 | 모양 |
|---|---|---|
| 앞으로 | ||
| 활성 | ||
| 시작 | ||
| 가중치 | ||
| 치우침 | ||
| 신호 |
문제 6. 접히는 반복과 안 접히는 반복을 가르는 기준을 쓰세요.
답. 각 걸음이 서로 독립이면 접히고 의존이 있으면 안 접힙니다.
문제 7. 검산에서 바깥곱을 더한 것과 행렬 곱의 첫 원소를 쓰세요.
답. 둘 다 입니다.
문제 8. 검산에서 배치 일 때 곱셈 횟수와 파이썬 걸음 수를 쓰세요.
답. 와 입니다.
문제 9. 모양이 맞는 조합을 찾는 방법을 쓰세요.
답. 결과가 있어야 할 모양을 적고 그 모양이 나오는 곱을 찾습니다.
문제 10. 검산에서 에 대한 유도 식과 상대 오차를 쓰세요.
답. 이고 입니다.
문제 11. 검산에서 손실을 평균으로 둘 때의 크기 비를 쓰세요.
답. 입니다.
문제 12. 활성함수의 야코비가 대각인 이유를 쓰세요.
답. 출력의 각 원소가 같은 자리 입력에만 달렸기 때문입니다.
문제 13. 검산에서 소프트맥스를 열 방향으로 했을 때의 상대 오차를 쓰세요.
답. 입니다.
문제 14. 검산에서 의 반복문 판과 벡터화 판의 차이를 쓰세요.
답. 입니다.
문제 15. 검산에서 잔차를 빠뜨렸을 때의 상대 오차를 쓰세요.
답. 입니다.
문제 16. 가장 위험한 모양 실수의 특징을 쓰세요.
답. 모양이 우연히 맞아 예외가 안 나는 경우입니다.
문제 17. 검산에서 정사각일 때 전치를 틀린 상대 오차를 쓰세요.
답. 입니다.
문제 18. 치우침 그래디언트를 안 더하면 결과 모양이 어떻게 되는지 쓰세요.
답. 브로드캐스트로 배치만큼 복제되어 가 됩니다.
심화 1. 배치 축이 앞인지 뒤인지 정리하세요.
| 관례 | 모양 | 누가 쓰나 |
|---|---|---|
| 배치가 앞 | 대부분의 프레임워크 | |
| 배치가 뒤 | 일부 교재 |
둘은 전치 관계이고 식이 모두 뒤집힙니다. 배치가 뒤면 이 됩니다.
섞어 쓰면 반드시 틀립니다. 한 코드베이스 안에서 하나로 고정하는 것이 유일한 방어입니다.
심화 2. 여러 축이 있는 텐서로 확장하세요.
이미지는 처럼 축이 넷입니다.
| 무엇 | 어떻게 |
|---|---|
| 배치처럼 다룰 축 | 앞으로 모읍니다 |
| 곱할 축 | 뒤로 모읍니다 |
| 모양을 폅니다 | 행렬 곱을 씁니다 |
| 다시 접습니다 | 원래 모양으로 |
핵심은 축을 두 무리로 나누는 것입니다. 그러면 언제나 곱하기 꼴이 됩니다.
강의 합성곱도 이렇게 행렬 곱으로 펴서 계산합니다.
심화 3. 아인슈타인 표기를 정리하세요.
| 규칙 | 무엇 |
|---|---|
| 두 번 나오는 첨자 | 더합니다 |
| 한 번 나오는 첨자 | 결과에 남습니다 |
| 전치가 필요 없음 | 첨자가 위치를 말합니다 |
셋째 줄이 이 표기의 값입니다. 인지 인지 고민할 필요가 없고, 첨자만 맞추면 됩니다.
문제 의 표를 이 표기로 적으면 후보가 아예 안 생깁니다.
심화 4. 메모리 레이아웃이 왜 중요한지 정리하세요.
| 무엇 | 왜 |
|---|---|
| 행 우선 저장 | 같은 행의 원소가 붙어 있음 |
| 전치는 복사일 수 있음 | 뷰로 끝날 수도 있음 |
| 연속이 아니면 느림 | 캐시가 놓침 |
문제 에서 곱셈 횟수가 같은데 속도가 다른 이유가 이것입니다.
전치 자체는 대개 공짜입니다. 넘파이는 보폭만 바꾸고 자료를 안 옮깁니다. 다만 그다음 연산이 연속 접근을 요구하면 그때 복사가 일어납니다.
심화 5. 혼합 정밀도에서 무엇이 달라지는지 정리하세요.
| 무엇 | 어느 정밀도 |
|---|---|
| 앞으로 가기 | 비트 |
| 뒤로 가기 | 비트 |
| 가중치 원본 | 비트 |
| 갱신 | 비트 |
작은 그래디언트가 비트에서 이 됩니다. 그래서 손실에 큰 수를 곱해 두었다가 갱신 직전에 나누는 손실 크기 조절을 씁니다.
강 문제 에서 검사는 비트로 한다고 했습니다. 검사와 학습은 다른 정밀도를 씁니다.
심화 6. 이 단원 전체를 한 문장으로 정리하세요.
| 무엇이 정확도를 정하나 | 무엇이 속도를 정하나 |
|---|---|
| 연쇄법칙을 빠짐없이 적용했나 | 어느 축을 접었나 |
| 갈래를 다 더했나 | 메모리를 어떻게 읽나 |
| 축을 안 헷갈렸나 | 무엇을 저장했나 |
왼쪽 열은 수학이고 오른쪽 열은 공학입니다. 이 단원은 둘을 함께 다뤘고, 단원부터는 이렇게 얻은 그래디언트를 어떻게 쓸지를 봅니다.
정답.
| 기호 | 읽는 법 | 뜻 |
|---|---|---|
| 벡터화 | vectorization | 반복문을 배열 연산으로 바꿉니다 |
| 배치 축 | batch axis | 표본을 늘어놓은 축입니다 |
| 모양표 | shape table | 각 값의 모양을 적어 둔 표입니다 |
| 원소별 곱 | elementwise product | 같은 자리끼리 곱합니다 |
| 열 합 | column sum | 배치 축으로 더합니다 |
| 잔차 연결 | residual connection | 출력에 입력을 더합니다 |
| 아인슈타인 표기 | Einstein notation | 첨자로 축을 지정합니다 |
| 메모리 레이아웃 | memory layout | 배열이 메모리에 놓인 순서입니다 |
| 혼합 정밀도 | mixed precision | 계산과 저장에 다른 정밀도를 씁니다 |
| 손실 크기 조절 | loss scaling | 작은 그래디언트가 사라지지 않게 키웁니다 |
여기까지가 단원 행렬미분과 역전파입니다. 네 강의로 그래디언트를 정확하고 빠르게 얻는 법을 세웠습니다. 단원부터는 그 그래디언트를 어떻게 쓸지를 봅니다. 자료 전체가 아니라 일부만 보고 한 걸음을 딛는 것이 왜 되는지가 강의 물음입니다.
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 relative(a, b):
a, b = np.asarray(a), np.asarray(b)
den = np.maximum(np.abs(a) + np.abs(b), 1e-12)
return float((np.abs(a - b) / den).max())
print("=" * 78)
print("234강 벡터화된 그래디언트 직접 유도하기 코드 검산")
print("=" * 78)
print()
print("문제 1. 반복문을 행렬로 접기")
print()
print(" (1) 무엇을 접는지 정리합니다")
rows = [
("표본에 대한 반복", "행렬 곱 하나", "X 전치 곱하기 델타"),
("출력 차원에 대한 반복", "같은 행렬 곱 안", "열이 출력 차원"),
("층에 대한 반복", "못 접음", "순서가 있기 때문"),
("시각에 대한 반복", "못 접음", "앞이 뒤를 만들기 때문"),
]
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(" 뒤의 둘은 의존이 있어 못 접습니다. 그래서 층과 시각만 반복문으로 남습니다")
print()
print(" (2) 표본 반복을 접어 봅니다")
r = np.random.default_rng(20234)
B, din, dout = 32, 12, 8
Xb = r.normal(0, 1, (B, din))
Db = r.normal(0, 1, (B, dout))
g_loop = np.zeros((din, dout))
for i in range(B):
g_loop += np.outer(Xb[i], Db[i])
g_mat = Xb.T @ Db
print(" " + rw("어떻게 계산", 22) + " " + rl("모양", 12) + " " + rl("첫 원소", 14) + " " + rl("차이", 14))
print(" " + rw("바깥곱 %d 개를 더함" % B, 22) + " " + rl(str(g_loop.shape), 12) + " " + rl("%.6f" % g_loop[0, 0], 14) + " " + rl("%.10f" % 0.0, 14))
print(" " + rw("행렬 곱 한 번", 22) + " " + rl(str(g_mat.shape), 12) + " " + rl("%.6f" % g_mat[0, 0], 14) + " " + rl("%.10f" % maxdiff(g_loop, g_mat), 14))
print(" 231강 문제 4 에서 본 것과 같은 항등식입니다")
print(" 반복문 %d 번이 행렬 곱 한 번이 됩니다" % B)
print()
print(" (3) 무엇이 줄어드는지 셉니다")
sizes = [(64, 32, 16), (256, 64, 32), (512, 128, 64), (1024, 256, 128)]
print(" 곱셈 횟수는 두 판이 같습니다. 다른 것은 파이썬이 도는 횟수입니다")
print(" " + rl("배치", 8) + " " + rl("입력차원", 10) + " " + rl("출력차원", 10) + " " + rl("곱셈 횟수", 14) + " " + rl("반복문 판의 파이썬 걸음", 26) + " " + rl("행렬 곱 판의 걸음", 20))
for Bs, dis, dos in sizes:
print(" " + rl("%d" % Bs, 8) + " " + rl("%d" % dis, 10) + " " + rl("%d" % dos, 10) + " " + rl("%d" % (Bs * dis * dos), 14) + " " + rl("%d" % Bs, 26) + " " + rl("%d" % 1, 20))
print(" 곱셈 횟수가 같은데도 빠른 이유가 이것입니다")
print(" 파이썬 반복문 한 걸음마다 형 검사와 함수 호출이 붙습니다")
print(" 행렬 곱은 그 비용을 한 번만 치르고 안에서 연속된 메모리를 훑습니다")
print()
print("문제 2. 층 하나를 벡터화해 유도하기")
print()
print(" (1) 모양표를 먼저 그립니다")
rows = [
("X", "B 곱하기 m", "입력"),
("W", "m 곱하기 k", "가중치"),
("b", "k", "치우침"),
("Z", "B 곱하기 k", "선형 결과"),
("델타", "B 곱하기 k", "Z 에 대한 미분"),
("W 의 그래디언트", "m 곱하기 k", "X 전치 곱 델타"),
("X 의 그래디언트", "B 곱하기 m", "델타 곱 W 전치"),
]
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()
print(" (2) 모양이 맞는 조합이 정말 하나인지 세어 봅니다")
Bq, mq, kq = 5, 3, 4
cands = [
("X 전치 곱 델타", (mq, Bq), (Bq, kq), True),
("델타 전치 곱 X", (kq, Bq), (Bq, mq), False),
("X 곱 델타 전치", (Bq, mq), (kq, Bq), False),
("델타 곱 X 전치", (Bq, kq), (mq, Bq), False),
]
print(" W 의 그래디언트는 %d 곱하기 %d 여야 합니다" % (mq, kq))
print(" " + rw("어떤 조합", 20) + " " + rl("왼쪽 모양", 14) + " " + rl("오른쪽 모양", 14) + " " + rl("곱이 되나", 12) + " " + rl("결과 모양", 14))
for nm, lsh, rsh, ok in cands:
can = lsh[1] == rsh[0]
res = "%d 곱하기 %d" % (lsh[0], rsh[1]) if can else "안 됨"
print(" " + rw(nm, 20) + " " + rl("%d 곱하기 %d" % lsh, 14) + " " + rl("%d 곱하기 %d" % rsh, 14) + " " + rl("예" if can else "아니오", 12) + " " + rl(res, 14))
print(" 곱이 되는 것 자체는 둘인데 결과 모양이 맞는 것은 첫째 줄 하나뿐입니다")
print(" 둘째 줄은 %d 곱하기 %d 이라 전치를 한 번 더 해야 합니다" % (kq, mq))
print()
print(" (3) 유도한 식을 수치로 확인합니다")
r2g = np.random.default_rng(30234)
B2, m2, k2 = 16, 6, 4
X2 = r2g.normal(0, 1, (B2, m2))
W2 = r2g.normal(0, 0.6, (m2, k2))
b2 = r2g.normal(0, 0.3, k2)
U2 = r2g.normal(0, 1, (B2, k2))
def loss_lin(Xv, Wv, bv):
return float((U2 * (Xv @ Wv + bv)).sum())
def numgrad(f, M, h=1e-6):
g = np.zeros_like(M)
it = np.nditer(M, flags=['multi_index'])
while not it.finished:
i = it.multi_index
old = M[i]
M[i] = old + h
lp = f()
M[i] = old - h
lm = f()
M[i] = old
g[i] = (lp - lm) / (2 * h)
it.iternext()
return g
print(" " + rw("무엇에 대한", 16) + " " + rw("유도한 식", 20) + " " + rl("상대 오차", 14))
print(" " + rw("W", 16) + " " + rw("X 전치 곱 U", 20) + " " + rl("%.4e" % relative(numgrad(lambda: loss_lin(X2, W2, b2), W2), X2.T @ U2), 14))
print(" " + rw("b", 16) + " " + rw("U 의 열 합", 20) + " " + rl("%.4e" % relative(numgrad(lambda: loss_lin(X2, W2, b2), b2), U2.sum(axis=0)), 14))
print(" " + rw("X", 16) + " " + rw("U 곱 W 전치", 20) + " " + rl("%.4e" % relative(numgrad(lambda: loss_lin(X2, W2, b2), X2), U2 @ W2.T), 14))
print(" 세 식 모두 상대 오차가 1e-6 아래입니다. 판정 기준 1e-5 를 넉넉히 통과합니다")
print()
print(" (4) 손실을 평균으로 둘 때와 합으로 둘 때를 견줍니다")
def loss_mean(Xv, Wv, bv):
return float((U2 * (Xv @ Wv + bv)).sum()) / B2
gW_sum = X2.T @ U2
gW_mean = X2.T @ U2 / B2
print(" " + rw("손실 정의", 14) + " " + rl("유도한 식", 20) + " " + rl("상대 오차", 14) + " " + rl("크기 비", 12))
print(" " + rw("합", 14) + " " + rl("X 전치 곱 U", 20) + " " + rl("%.4e" % relative(numgrad(lambda: loss_lin(X2, W2, b2), W2), gW_sum), 14) + " " + rl("%.4f" % 1.0, 12))
print(" " + rw("평균", 14) + " " + rl("배치로 나눔", 20) + " " + rl("%.4e" % relative(numgrad(lambda: loss_mean(X2, W2, b2), W2), gW_mean), 14) + " " + rl("%.4f" % (1.0 / B2), 12))
print(" 크기가 정확히 배치 수 배 차이입니다")
print(" 233강 문제 4 에서 이것을 빼먹으면 학습률이 배치 수 배가 된다고 했습니다")
print()
print("문제 3. 활성함수와 손실을 벡터화하기")
print()
print(" (1) 원소별 연산을 정리합니다")
rows = [
("정류 선형", "0 보다 큰지", "지시함수와 곱"),
("하이퍼볼릭 탄젠트", "1 빼기 제곱", "출력으로 계산"),
("로지스틱", "s 곱 1 빼기 s", "출력으로 계산"),
("소프트플러스", "로지스틱", "다른 함수의 값"),
("항등", "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()
print(" (2) 네 활성함수를 한꺼번에 확인합니다")
r3 = np.random.default_rng(40234)
Zt = r3.normal(0, 1.5, (5, 4))
Ut = r3.normal(0, 1, (5, 4))
def num_elem(f, Z, h=1e-6):
g = np.zeros_like(Z)
it = np.nditer(Z, flags=['multi_index'])
while not it.finished:
i = it.multi_index
old = Z[i]
Z[i] = old + h
lp = float((Ut * f(Z)).sum())
Z[i] = old - h
lm = float((Ut * f(Z)).sum())
Z[i] = old
g[i] = (lp - lm) / (2 * h)
it.iternext()
return g
sig = lambda z: 1.0 / (1.0 + np.exp(-z))
acts = [
("정류 선형", lambda z: np.maximum(z, 0.0), lambda Z: Ut * (Zt > 0)),
("하이퍼볼릭 탄젠트", np.tanh, lambda Z: Ut * (1 - np.tanh(Zt) ** 2)),
("로지스틱", sig, lambda Z: Ut * sig(Zt) * (1 - sig(Zt))),
("소프트플러스", lambda z: np.log1p(np.exp(z)), lambda Z: Ut * sig(Zt)),
]
w0 = max(pw(a[0]) for a in acts)
print(" " + rw("함수", w0) + " " + rl("상대 오차", 14))
for nm, f, g in acts:
print(" " + rw(nm, w0) + " " + rl("%.4e" % relative(num_elem(f, Zt), g(Zt)), 14))
print(" 네 함수 모두 원소별 곱 한 번으로 끝납니다")
print(" 231강 심화 4 에서 본 대로 야코비가 대각이기 때문입니다")
print()
print(" (3) 소프트맥스와 교차엔트로피를 배치로 확인합니다")
r4 = np.random.default_rng(50234)
Bs, Ks = 7, 5
Zs = r4.normal(0, 1.2, (Bs, Ks))
ls = r4.integers(0, Ks, Bs)
Ys = np.zeros((Bs, Ks))
Ys[np.arange(Bs), ls] = 1.0
def softmax(Z):
E = np.exp(Z - Z.max(axis=1, keepdims=True))
return E / E.sum(axis=1, keepdims=True)
def ce(Z):
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 * Ys).sum(axis=1)))
ga = (softmax(Zs) - Ys) / Bs
gn = numgrad(lambda: ce(Zs), Zs)
print(" 배치 %d 개이고 갈래 %d 개입니다" % (Bs, Ks))
print(" " + rw("무엇", 24) + " " + rl("상대 오차", 14))
print(" " + rw("소프트맥스 빼기 정답 나누기 배치", 24) + " " + rl("%.4e" % relative(gn, ga), 14))
print(" 행마다 독립이라 배치가 그냥 축 하나로 붙습니다")
print(" 231강 문제 3 의 한 표본 결과가 그대로 행에 놓입니다")
print()
print(" (4) 행 방향과 열 방향을 헷갈리면 어떻게 되는지 봅니다")
wrong1 = (softmax(Zs.T).T - Ys) / Bs
wrong2 = (softmax(Zs) - Ys) / Ks
print(" " + rw("무엇을 했나", 26) + " " + rl("상대 오차", 14) + " " + rl("참값 대비 크기", 16))
print(" " + rw("바르게 계산", 26) + " " + rl("%.4e" % relative(gn, ga), 14) + " " + rl("%.6f" % 1.0, 16))
print(" " + rw("소프트맥스를 열 방향으로", 26) + " " + rl("%.4e" % relative(gn, wrong1), 14) + " " + rl("%.6f" % (float(np.abs(wrong1).sum()) / float(np.abs(ga).sum())), 16))
print(" " + rw("갈래 수로 나눔", 26) + " " + rl("%.4e" % relative(gn, wrong2), 14) + " " + rl("%.6f" % (float(np.abs(wrong2).sum()) / float(np.abs(ga).sum())), 16))
print(" 열 방향 소프트맥스는 모양이 우연히 맞아 오류 없이 돌아갑니다")
print(" 그래서 모양 검사만으로는 안 잡힙니다. 반드시 수치로 봐야 합니다")
print()
print("문제 4. 두 층을 한꺼번에 유도하기")
print()
print(" (1) 유도 절차를 정리합니다")
rows = [
("1", "앞으로 가는 식을 적습니다", "모양을 옆에 적어 둡니다"),
("2", "손실에서 시작합니다", "델타의 모양은 마지막 Z 와 같습니다"),
("3", "층마다 두 줄을 씁니다", "파라미터 하나와 신호 하나"),
("4", "모양을 확인합니다", "안 맞으면 전치를 바꿉니다"),
("5", "수치로 검산합니다", "상대 오차 1e-5 이하"),
]
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(60234)
B3, d0, d1, d2 = 20, 7, 9, 4
X3 = r5.normal(0, 1, (B3, d0))
W1 = r5.normal(0, np.sqrt(2.0 / d0), (d0, d1))
b1 = np.zeros(d1)
W2 = r5.normal(0, np.sqrt(2.0 / d1), (d1, d2))
b2v = np.zeros(d2)
l3 = r5.integers(0, d2, B3)
Y3 = np.zeros((B3, d2))
Y3[np.arange(B3), l3] = 1.0
def fwd(Xv, W1v, b1v, W2v, b2w):
Z1 = Xv @ W1v + b1v
H1 = np.maximum(Z1, 0.0)
Z2 = H1 @ W2v + b2w
return Z1, H1, Z2
def loss2():
_, _, Z2 = fwd(X3, W1, b1, W2, b2v)
mx = Z2.max(axis=1, keepdims=True)
lse = mx[:, 0] + np.log(np.exp(Z2 - mx).sum(axis=1))
return float(np.mean(lse - (Z2 * Y3).sum(axis=1)))
Z1, H1, Z2 = fwd(X3, W1, b1, W2, b2v)
D2 = (softmax(Z2) - Y3) / B3
gW2 = H1.T @ D2
gb2 = D2.sum(axis=0)
D1 = (D2 @ W2.T) * (Z1 > 0)
gW1 = X3.T @ D1
gb1 = D1.sum(axis=0)
items = [("W1", W1, gW1), ("b1", b1, gb1), ("W2", W2, gW2), ("b2", b2v, gb2)]
print(" 구조는 %d 에서 %d 에서 %d 이고 배치는 %d 입니다" % (d0, d1, d2, B3))
print(" " + rw("무엇", 8) + " " + rl("모양", 12) + " " + rw("유도한 식", 22) + " " + rl("상대 오차", 14))
forms = ["X 전치 곱 D1", "D1 의 열 합", "H1 전치 곱 D2", "D2 의 열 합"]
for (nm, M, g), fm in zip(items, forms):
print(" " + rw(nm, 8) + " " + rl(str(M.shape), 12) + " " + rw(fm, 22) + " " + rl("%.4e" % relative(numgrad(loss2, M), g), 14))
print(" 반복문 없이 여섯 줄로 두 층을 모두 유도했습니다")
print(" 233강의 반복문 판과 결과가 같습니다")
print()
print(" (3) 반복문 판과 벡터화 판이 같은지 확인합니다")
gW1_loop = np.zeros_like(W1)
gW2_loop = np.zeros_like(W2)
for i in range(B3):
z1 = X3[i] @ W1 + b1
h1 = np.maximum(z1, 0.0)
z2 = h1 @ W2 + b2v
e = np.exp(z2 - z2.max())
p = e / e.sum()
d2i = (p - Y3[i]) / B3
gW2_loop += np.outer(h1, d2i)
d1i = (W2 @ d2i) * (z1 > 0)
gW1_loop += np.outer(X3[i], d1i)
print(" " + rw("무엇", 10) + " " + rl("두 판의 최대 차이", 20))
print(" " + rw("W1", 10) + " " + rl("%.4e" % maxdiff(gW1, gW1_loop), 20))
print(" " + rw("W2", 10) + " " + rl("%.4e" % maxdiff(gW2, gW2_loop), 20))
print(" 부동소수점 덧셈 순서만 다르고 값은 같습니다")
print(" 벡터화는 답을 바꾸지 않습니다. 계산 순서만 바꿉니다")
print()
print(" (4) 잔차 연결이 있으면 어떻게 되는지 유도합니다")
r6 = np.random.default_rng(70234)
d = 6
B4 = 15
X4 = r6.normal(0, 1, (B4, d))
Wr = r6.normal(0, 0.5, (d, d))
br = np.zeros(d)
U4 = r6.normal(0, 1, (B4, d))
def loss_res():
Zr = X4 @ Wr + br
Hr = np.maximum(Zr, 0.0) + X4
return float((U4 * Hr).sum())
Zr = X4 @ Wr + br
Dh = U4
Dz = Dh * (Zr > 0)
gWr = X4.T @ Dz
gX_res = Dz @ Wr.T + Dh
print(" 잔차 연결은 출력에 입력을 더합니다")
print(" " + rw("무엇에 대한", 16) + " " + rw("유도한 식", 26) + " " + rl("상대 오차", 14))
print(" " + rw("W", 16) + " " + rw("X 전치 곱 Dz", 26) + " " + rl("%.4e" % relative(numgrad(loss_res, Wr), gWr), 14))
print(" " + rw("X", 16) + " " + rw("Dz 곱 W 전치 더하기 Dh", 26) + " " + rl("%.4e" % relative(numgrad(loss_res, X4), gX_res), 14))
print(" " + rw("X 잔차를 빠뜨림", 16) + " " + rw("Dz 곱 W 전치만", 26) + " " + rl("%.4e" % relative(numgrad(loss_res, X4), Dz @ Wr.T), 14))
print(" 더하기 마디라 신호가 그대로 한 벌 더 옵니다")
print(" 이 직통 경로가 깊은 신경망에서 신호를 살립니다. 245강에서 다시 봅니다")
print()
print("문제 5. 실무에서 쓰기")
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) 정사각 행렬에서 전치 실수가 안 잡히는 것을 봅니다")
r7 = np.random.default_rng(80234)
print(" " + rl("입력차원", 10) + " " + rl("출력차원", 10) + " " + rw("전치를 틀리면", 18) + " " + rl("상대 오차", 14))
for din_, dout_ in [(6, 4), (5, 5), (8, 8)]:
Xt = r7.normal(0, 1, (10, din_))
Wt = r7.normal(0, 0.5, (din_, dout_))
Ut2 = r7.normal(0, 1, (10, dout_))
right = Xt.T @ Ut2
try:
wrongm = Ut2.T @ Xt
shape_ok = wrongm.shape == right.shape
err = relative(right, wrongm) if shape_ok else float('nan')
txt = "모양이 맞음" if shape_ok else "모양이 안 맞음"
except ValueError:
txt, err = "곱이 안 됨", float('nan')
es = "%.4e" % err if err == err else "잴 수 없음"
print(" " + rl("%d" % din_, 10) + " " + rl("%d" % dout_, 10) + " " + rw(txt, 18) + " " + rl(es, 14))
print(" 정사각이면 전치를 틀려도 모양이 맞아 조용히 돌아갑니다")
print(" 은닉층 너비가 같은 신경망에서 특히 위험합니다")
print()
print(" (3) 브로드캐스트가 조용히 모양을 바꾸는 것을 봅니다")
r8 = np.random.default_rng(90234)
Dbb = r8.normal(0, 1, (12, 5))
right_b = Dbb.sum(axis=0)
wrong_b = Dbb.sum(axis=1)
print(" 치우침의 그래디언트는 배치 축으로 더한 것입니다")
print(" " + rw("어느 축으로 더했나", 22) + " " + rl("결과 모양", 14) + " " + "치우침 모양과 맞나")
print(" " + rw("배치 축", 22) + " " + rl(str(right_b.shape), 14) + " " + "맞음")
print(" " + rw("차원 축", 22) + " " + rl(str(wrong_b.shape), 14) + " " + "안 맞음")
print(" " + rw("아예 안 더함", 22) + " " + rl(str(Dbb.shape), 14) + " " + "안 맞음")
bshape = np.zeros(5)
print(" 치우침이 %d 차원이므로 배치 축으로 더해야 합니다" % len(bshape))
try:
_ = bshape - Dbb
print(" 그런데 더하지 않고 그대로 빼도 오류가 안 납니다. 결과 모양은 %s 입니다" % str((bshape - Dbb).shape))
except ValueError as e:
print(" 더하지 않고 빼면 오류가 납니다")
print(" 브로드캐스트가 치우침을 배치만큼 복제해 버립니다")
print(" 갱신이 조용히 잘못되고 손실은 그럴듯하게 줄어듭니다")
print()
print(" (4) 이 단원을 한 장으로 모읍니다")
rows = [
("231", "무엇에 대해 미분하나", "결과 모양은 분자 곱하기 분모"),
("232", "연결을 누가 아나", "그래프가 압니다"),
("233", "층 위에서 어떻게 도나", "앞으로 저장하고 뒤로 두 줄"),
("234", "반복문을 어떻게 없애나", "표본 축을 행렬 곱으로 접습니다"),
]
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(" 02 단원부터는 그 그래디언트를 어떻게 쓸지를 봅니다")
print()
print("=" * 78)
print("검산 끝")
print("=" * 78)
# ==============================================================================
# 234강 벡터화된 그래디언트 직접 유도하기 코드 검산
# ==============================================================================
#
# 문제 1. 반복문을 행렬로 접기
#
# (1) 무엇을 접는지 정리합니다
# 무엇에 대한 반복 무엇이 되나 구체적으로
# 표본에 대한 반복 행렬 곱 하나 X 전치 곱하기 델타
# 출력 차원에 대한 반복 같은 행렬 곱 안 열이 출력 차원
# 층에 대한 반복 못 접음 순서가 있기 때문
# 시각에 대한 반복 못 접음 앞이 뒤를 만들기 때문
# 앞의 둘은 서로 독립이라 접힙니다
# 뒤의 둘은 의존이 있어 못 접습니다. 그래서 층과 시각만 반복문으로 남습니다
#
# (2) 표본 반복을 접어 봅니다
# 어떻게 계산 모양 첫 원소 차이
# 바깥곱 32 개를 더함 (12, 8) 7.577350 0.0000000000
# 행렬 곱 한 번 (12, 8) 7.577350 0.0000000000
# 231강 문제 4 에서 본 것과 같은 항등식입니다
# 반복문 32 번이 행렬 곱 한 번이 됩니다
#
# (3) 무엇이 줄어드는지 셉니다
# 곱셈 횟수는 두 판이 같습니다. 다른 것은 파이썬이 도는 횟수입니다
# 배치 입력차원 출력차원 곱셈 횟수 반복문 판의 파이썬 걸음 행렬 곱 판의 걸음
# 64 32 16 32768 64 1
# 256 64 32 524288 256 1
# 512 128 64 4194304 512 1
# 1024 256 128 33554432 1024 1
# 곱셈 횟수가 같은데도 빠른 이유가 이것입니다
# 파이썬 반복문 한 걸음마다 형 검사와 함수 호출이 붙습니다
# 행렬 곱은 그 비용을 한 번만 치르고 안에서 연속된 메모리를 훑습니다
#
# 문제 2. 층 하나를 벡터화해 유도하기
#
# (1) 모양표를 먼저 그립니다
# 무엇 모양 무엇인가
# X B 곱하기 m 입력
# W m 곱하기 k 가중치
# b k 치우침
# Z B 곱하기 k 선형 결과
# 델타 B 곱하기 k Z 에 대한 미분
# W 의 그래디언트 m 곱하기 k X 전치 곱 델타
# X 의 그래디언트 B 곱하기 m 델타 곱 W 전치
# 모양이 맞는 조합은 하나뿐입니다. 그래서 식을 외울 필요가 없습니다
#
# (2) 모양이 맞는 조합이 정말 하나인지 세어 봅니다
# W 의 그래디언트는 3 곱하기 4 여야 합니다
# 어떤 조합 왼쪽 모양 오른쪽 모양 곱이 되나 결과 모양
# X 전치 곱 델타 3 곱하기 5 5 곱하기 4 예 3 곱하기 4
# 델타 전치 곱 X 4 곱하기 5 5 곱하기 3 예 4 곱하기 3
# X 곱 델타 전치 5 곱하기 3 4 곱하기 5 아니오 안 됨
# 델타 곱 X 전치 5 곱하기 4 3 곱하기 5 아니오 안 됨
# 곱이 되는 것 자체는 둘인데 결과 모양이 맞는 것은 첫째 줄 하나뿐입니다
# 둘째 줄은 4 곱하기 3 이라 전치를 한 번 더 해야 합니다
#
# (3) 유도한 식을 수치로 확인합니다
# 무엇에 대한 유도한 식 상대 오차
# W X 전치 곱 U 7.0175e-09
# b U 의 열 합 1.1155e-10
# X U 곱 W 전치 1.3326e-07
# 세 식 모두 상대 오차가 1e-6 아래입니다. 판정 기준 1e-5 를 넉넉히 통과합니다
#
# (4) 손실을 평균으로 둘 때와 합으로 둘 때를 견줍니다
# 손실 정의 유도한 식 상대 오차 크기 비
# 합 X 전치 곱 U 7.0175e-09 1.0000
# 평균 배치로 나눔 7.0175e-09 0.0625
# 크기가 정확히 배치 수 배 차이입니다
# 233강 문제 4 에서 이것을 빼먹으면 학습률이 배치 수 배가 된다고 했습니다
#
# 문제 3. 활성함수와 손실을 벡터화하기
#
# (1) 원소별 연산을 정리합니다
# 함수 도함수 어떻게 계산
# 정류 선형 0 보다 큰지 지시함수와 곱
# 하이퍼볼릭 탄젠트 1 빼기 제곱 출력으로 계산
# 로지스틱 s 곱 1 빼기 s 출력으로 계산
# 소프트플러스 로지스틱 다른 함수의 값
# 항등 1 그대로 통과
# 둘째와 셋째 줄은 출력만 있으면 도함수가 나옵니다. 입력을 안 저장해도 됩니다
#
# (2) 네 활성함수를 한꺼번에 확인합니다
# 함수 상대 오차
# 정류 선형 1.7807e-09
# 하이퍼볼릭 탄젠트 9.6257e-08
# 로지스틱 1.0048e-08
# 소프트플러스 2.0265e-09
# 네 함수 모두 원소별 곱 한 번으로 끝납니다
# 231강 심화 4 에서 본 대로 야코비가 대각이기 때문입니다
#
# (3) 소프트맥스와 교차엔트로피를 배치로 확인합니다
# 배치 7 개이고 갈래 5 개입니다
# 무엇 상대 오차
# 소프트맥스 빼기 정답 나누기 배치 2.3567e-08
# 행마다 독립이라 배치가 그냥 축 하나로 붙습니다
# 231강 문제 3 의 한 표본 결과가 그대로 행에 놓입니다
#
# (4) 행 방향과 열 방향을 헷갈리면 어떻게 되는지 봅니다
# 무엇을 했나 상대 오차 참값 대비 크기
# 바르게 계산 2.3567e-08 1.000000
# 소프트맥스를 열 방향으로 7.8532e-01 0.806473
# 갈래 수로 나눔 1.6667e-01 1.400000
# 열 방향 소프트맥스는 모양이 우연히 맞아 오류 없이 돌아갑니다
# 그래서 모양 검사만으로는 안 잡힙니다. 반드시 수치로 봐야 합니다
#
# 문제 4. 두 층을 한꺼번에 유도하기
#
# (1) 유도 절차를 정리합니다
# 단계 무엇을 하나 요령
# 1 앞으로 가는 식을 적습니다 모양을 옆에 적어 둡니다
# 2 손실에서 시작합니다 델타의 모양은 마지막 Z 와 같습니다
# 3 층마다 두 줄을 씁니다 파라미터 하나와 신호 하나
# 4 모양을 확인합니다 안 맞으면 전치를 바꿉니다
# 5 수치로 검산합니다 상대 오차 1e-5 이하
#
# (2) 두 층 신경망을 직접 유도합니다
# 구조는 7 에서 9 에서 4 이고 배치는 20 입니다
# 무엇 모양 유도한 식 상대 오차
# W1 (7, 9) X 전치 곱 D1 1.8731e-07
# b1 (9,) D1 의 열 합 1.5480e-08
# W2 (9, 4) H1 전치 곱 D2 1.2380e-07
# b2 (4,) D2 의 열 합 2.5103e-09
# 반복문 없이 여섯 줄로 두 층을 모두 유도했습니다
# 233강의 반복문 판과 결과가 같습니다
#
# (3) 반복문 판과 벡터화 판이 같은지 확인합니다
# 무엇 두 판의 최대 차이
# W1 5.5511e-17
# W2 3.4694e-17
# 부동소수점 덧셈 순서만 다르고 값은 같습니다
# 벡터화는 답을 바꾸지 않습니다. 계산 순서만 바꿉니다
#
# (4) 잔차 연결이 있으면 어떻게 되는지 유도합니다
# 잔차 연결은 출력에 입력을 더합니다
# 무엇에 대한 유도한 식 상대 오차
# W X 전치 곱 Dz 5.9491e-09
# X Dz 곱 W 전치 더하기 Dh 2.2206e-08
# X 잔차를 빠뜨림 Dz 곱 W 전치만 1.0000e+00
# 더하기 마디라 신호가 그대로 한 벌 더 옵니다
# 이 직통 경로가 깊은 신경망에서 신호를 살립니다. 245강에서 다시 봅니다
#
# 문제 5. 실무에서 쓰기
#
# (1) 흔한 모양 실수를 정리합니다
# 무엇 어떻게 드러나나 찾기
# 전치를 빠뜨림 모양이 안 맞아 곧바로 터짐 쉬움
# 전치가 우연히 맞음 정사각 행렬일 때 어려움
# 배치 축을 헷갈림 소프트맥스를 열 방향으로 어려움
# 합과 평균을 헷갈림 크기만 배치 수 배 매우 어려움
# 브로드캐스트가 조용히 됨 모양이 늘어남 매우 어려움
#
# (2) 정사각 행렬에서 전치 실수가 안 잡히는 것을 봅니다
# 입력차원 출력차원 전치를 틀리면 상대 오차
# 6 4 모양이 안 맞음 잴 수 없음
# 5 5 모양이 맞음 1.0000e+00
# 8 8 모양이 맞음 1.0000e+00
# 정사각이면 전치를 틀려도 모양이 맞아 조용히 돌아갑니다
# 은닉층 너비가 같은 신경망에서 특히 위험합니다
#
# (3) 브로드캐스트가 조용히 모양을 바꾸는 것을 봅니다
# 치우침의 그래디언트는 배치 축으로 더한 것입니다
# 어느 축으로 더했나 결과 모양 치우침 모양과 맞나
# 배치 축 (5,) 맞음
# 차원 축 (12,) 안 맞음
# 아예 안 더함 (12, 5) 안 맞음
# 치우침이 5 차원이므로 배치 축으로 더해야 합니다
# 그런데 더하지 않고 그대로 빼도 오류가 안 납니다. 결과 모양은 (12, 5) 입니다
# 브로드캐스트가 치우침을 배치만큼 복제해 버립니다
# 갱신이 조용히 잘못되고 손실은 그럴듯하게 줄어듭니다
#
# (4) 이 단원을 한 장으로 모읍니다
# 강 무엇을 물었나 한 줄 답
# 231 무엇에 대해 미분하나 결과 모양은 분자 곱하기 분모
# 232 연결을 누가 아나 그래프가 압니다
# 233 층 위에서 어떻게 도나 앞으로 저장하고 뒤로 두 줄
# 234 반복문을 어떻게 없애나 표본 축을 행렬 곱으로 접습니다
# 네 강의가 하나의 물음에 답합니다. 그래디언트를 어떻게 정확하고 빠르게 얻는가입니다
# 02 단원부터는 그 그래디언트를 어떻게 쓸지를 봅니다
#
# ==============================================================================
# 검산 끝
# ==============================================================================