강에서 층 두 개를 손으로 이었습니다. 층이 백 개면 그 일을 백 번 해야 합니다.
계산 그래프는 식을 마디와 화살로 적은 것입니다. 마디 하나는 자기 국소 미분만 알면 되고, 어떻게 이어지는지는 그래프가 압니다.
이 강의는 그래프를 세우고, 뒤로 훑는 규칙을 정하고, 줄짜리 자동미분을 직접 만들어 강의 정규방정식과 같은 답에 이르는 것을 확인합니다.
문제. 식을 그림으로 바꿉니다.
() 무엇을 마디로 두는지 정리하세요.
() 간단한 식을 마디로 쪼개세요.
() 위상 정렬로 계산 순서를 정하세요.
생각의 실마리. 를 한 덩어리로 보면 미분이 복잡합니다. 곱하기 하나, 더하기 하나, 지수 하나로 쪼개면 각각은 아주 쉽습니다.
풀이. () 정리합니다.
| 무엇 | 무엇을 담나 | 어떻게 알아보나 |
|---|---|---|
| 잎 | 입력이나 파라미터 | 들어오는 화살이 없음 |
| 연산 마디 | 더하기 곱하기 지수 등 | 부모가 있고 자식이 있음 |
| 뿌리 | 손실 | 나가는 화살이 없음 |
| 화살 | 값이 흐르는 방향 | 앞으로 갈 때의 방향 |
| 갈래 | 한 값이 여러 곳에 쓰임 | 뒤로 갈 때 더해짐 |
강에서 손으로 이은 두 층이 여기서는 마디 여섯 개짜리 그래프입니다.
() 간단한 식을 마디로 쪼갭니다. 이고 입니다.
| 마디 | 무엇을 계산 | 값 |
|---|---|---|
| e^ | ||
마디 가 두 곳에 쓰입니다. 를 만들 때와 을 만들 때입니다.
이런 갈래가 있으면 뒤로 갈 때 두 경로의 기여를 더해야 합니다.
() 위상 정렬로 계산 순서를 정합니다. 부모를 먼저 방문하고 자기를 넣습니다.
| 마디 | 부모 | 순서에서의 자리 |
|---|---|---|
| 없음 | ||
| 없음 | ||
계산 순서는 입니다. 어느 마디도 자기 부모보다 먼저 오지 않습니다.
순환이 있으면 이 정렬이 실패합니다. 그래서 계산 그래프는 순환이 없어야 합니다.
이 문제에서 배우는 것. 그래프로 적으면 무엇을 어떤 순서로 계산할지가 그림에서 읽힙니다. 사람이 순서를 정할 필요가 없고, 위상 정렬이 정해 줍니다.
확인 1-1. 갈래가 있으면 뒤로 갈 때 무엇을 해야 하는지 쓰세요.
답. 두 경로의 기여를 더해야 합니다.
확인 1-2. 검산에서 마디 의 값을 쓰세요.
답. 입니다.
확인 1-3. 계산 그래프에 순환이 있으면 안 되는 이유를 쓰세요.
답. 위상 정렬이 실패해 계산 순서를 못 정하기 때문입니다.
문제. 거꾸로 갑니다.
() 국소 미분을 정리하세요.
() 손으로 뒤로 훑으세요.
() 수치 미분으로 확인하세요.
() 갈래를 안 더하면 어떻게 되는지 보세요.
생각의 실마리. 뿌리에서 로 시작합니다. 그리고 마디마다 자기 국소 미분을 곱해 부모에게 넘깁니다.
풀이. () 정리합니다.
| 연산 | 뒤로 무엇을 곱하나 | 구체적으로 |
|---|---|---|
| 더하기 | 양쪽에 그대로 | 과 |
| 곱하기 | 상대방 값 | 와 |
| 지수 | 자기 출력 | 출력 그대로 |
| 로그 | 나누기 입력 | 역수 |
| 최대 | 이긴 쪽에만 | 과 |
마디 하나는 자기 국소 미분만 알면 됩니다. 전체 식이 무엇인지는 몰라도 됩니다. 그것이 그래프의 힘입니다.
() 손으로 뒤로 훑습니다.
| 마디 | 값 | 그래디언트 | 어디서 왔나 |
|---|---|---|---|
| 뿌리 | |||
| 로그의 미분 | |||
| 두 경로의 합 | |||
| 더하기는 그대로 | |||
| 곱하기는 상대 값 | |||
| 두 경로의 합 |
로 오는 그래디언트가 더하기 입니다.
를 지나온 쪽은 입니다. 지수와 로그가 서로 지워 정확히 이 됩니다.
() 수치 미분으로 확인합니다.
| 무엇 | 손으로 훑은 값 | 수치 미분 | 차이 |
|---|---|---|---|
손으로 훑은 값과 수치 미분이 소수점 아래 여섯 자리까지 같습니다.
() 갈래를 안 더하면 어떻게 되는지 봅니다.
| 무엇으로 계산 | 값 | 참값과의 차 |
|---|---|---|
| 두 경로를 더함 | ||
| 를 지난 경로만 | ||
| 로 바로 간 경로만 |
한 경로만 세면 크게 틀립니다. 이어야 할 것이 이나 가 됩니다.
강 문제 의 브로드캐스트도 같은 이야기입니다. 복사는 뒤에서 합이 됩니다.
이 문제에서 배우는 것. 뒤로 훑기는 곱하고 더하는 두 동작뿐입니다. 자기 국소 미분을 곱해서 부모에게 넘기고, 여러 자식에게서 온 것은 더합니다.
확인 2-1. 곱하기 마디가 뒤로 무엇을 곱하는지 쓰세요.
답. 상대방의 값을 곱합니다.
확인 2-2. 검산에서 의 그래디언트와 의 그래디언트를 쓰세요.
답. 과 입니다.
확인 2-3. 검산에서 를 지난 경로만 셌을 때의 값과 참값과의 차를 쓰세요.
답. 과 입니다.
문제. 자동미분을 구현합니다.
() 무엇이 필요한지 정리하세요.
() 마디를 클래스로 적으세요.
() 이 그래프로 학습시키세요.
() 마디 수가 늘어나는 것을 보세요.
생각의 실마리. 마디 하나가 값·부모·국소 미분 규칙·그래디언트 자리 넷만 들고 있으면 됩니다. 나머지는 순회 함수 하나입니다.
풀이. () 정리합니다.
| 무엇 | 무엇인가 | 어떻게 쓰나 |
|---|---|---|
| 값 | 앞으로 갈 때 계산한 결과 | 저장해 둡니다 |
| 부모 | 누구로부터 왔나 | 뒤로 갈 방향 |
| 국소 미분 규칙 | 연산마다 다름 | 마디에 붙여 둠 |
| 그래디언트 자리 | 쌓아 더할 곳 | 으로 시작 |
| 방문 순서 | 위상 정렬의 역순 | 한 번씩만 방문 |
() 마디를 클래스로 적고 같은 식을 돌립니다.
| 무엇 | 손으로 훑은 값 | 그래프가 낸 값 | 차이 |
|---|---|---|---|
마디는 개이고 방문은 한 번씩입니다.
각 마디는 자기 국소 미분만 압니다. 연결은 그래프가 압니다. 코드에서 backward 함수 안에 그래프 구조가 전혀 안 적혀 있는 것이 그 뜻입니다.
() 이 그래프로 이차식을 학습시킵니다. 참 계수는 과 와 입니다.
| 걸음 | 손실 | 첫 계수 | 둘째 계수 | 셋째 계수 |
|---|---|---|---|---|
정규방정식으로 푼 값과 견줍니다.
| 무엇 | 첫 계수 | 둘째 계수 | 셋째 계수 |
|---|---|---|---|
| 그래프로 학습 | |||
| 정규방정식 |
가장 큰 차이는 입니다.
걸음으로 정규방정식의 답에 소수점 둘째 자리까지 닿았습니다. 더 돌리면 더 가까워집니다. 경사하강은 유한 걸음으로 정확히 도달하지 않습니다.
() 마디 수가 늘어나는 것을 봅니다.
| 표본 수 | 마디 수 | 표본당 마디 |
|---|---|---|
표본 하나가 마디 열 개 남짓을 만듭니다. 표본 개면 마디가 개입니다.
그래서 실제 구현은 표본마다 마디를 만들지 않고 배치를 한 덩어리로 다룹니다. 강 문제 의 배치 규칙이 그것을 가능하게 합니다.
이 문제에서 배우는 것. 자동미분은 마법이 아니라 자료구조입니다. 마디 하나에 네 가지만 담고 역순으로 훑으면 끝이고, 그것으로 강의 정규방정식과 같은 자리에 갑니다.
확인 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. 이 강의가 다음 강의로 어떻게 이어지는지 정리하세요.
| 이 강의 | 다음에서 |
|---|---|
| 마디와 국소 미분 | 강 신경망 층이 마디가 됨 |
| 뒤로 훑기 | 강 역전파 알고리즘 |
| 배치를 덩어리로 | 강 벡터화된 유도 |
| 값을 저장함 | 강 그래디언트 소실 |
| 더하기는 값쌈 | 강 잔차 연결 |
강은 이 그래프 훑기를 신경망에 붙입니다. 마디 하나가 연산 하나에서 층 하나로 굵어질 뿐 규칙은 같습니다.
정답.
| 기호 | 읽는 법 | 뜻 |
|---|---|---|
| 계산 그래프 | computational graph | 식을 마디와 화살로 적은 것입니다 |
| 방향 비순환 그래프 | directed acyclic graph | 화살이 있고 순환이 없는 그래프입니다 |
| 위상 정렬 | topological sort | 부모가 자식보다 먼저 오게 줄 세웁니다 |
| 잎 | leaf | 부모가 없는 마디입니다 |
| 뿌리 | root | 자식이 없는 마디입니다 |
| 국소 미분 | local derivative | 마디가 자기 입력에 대해 갖는 미분입니다 |
| 뒤로 가는 미분 | reverse-mode differentiation | 출력에서 입력 쪽으로 훑습니다 |
| 앞으로 가는 미분 | forward-mode differentiation | 입력에서 출력 쪽으로 훑습니다 |
| 그래디언트 체크포인트 | gradient checkpointing | 일부만 저장하고 나머지는 다시 계산합니다 |
| 그냥 통과시키는 추정기 | straight-through estimator | 미분 불가능한 마디를 통과시킵니다 |
다음은 233강 역전파 알고리즘입니다. 이 강의에서 마디 하나가 연산 하나였습니다. 다음 강의는 마디 하나를 층 하나로 굵게 만들고, 그 위에서 같은 훑기를 적습니다.
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())
print("=" * 78)
print("232강 계산 그래프 코드 검산")
print("=" * 78)
print()
print("문제 1. 식을 그래프로 적기")
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(" 231강에서 손으로 이은 두 층이 여기서는 마디 여섯 개짜리 그래프입니다")
print(" 그래프로 적으면 무엇을 어떤 순서로 계산할지가 그림에서 읽힙니다")
print()
print(" (2) 간단한 식을 마디로 쪼갭니다")
xa, ya = 2.0, 3.0
steps = [
("a", "x 곱하기 y", xa * ya),
("b", "a 더하기 x", xa * ya + xa),
("c", "지수 b", float(np.exp(xa * ya + xa))),
("L", "로그 c 더하기 b", float(np.log(np.exp(xa * ya + xa)) + (xa * ya + xa))),
]
print(" x 는 %.4f 이고 y 는 %.4f 입니다" % (xa, ya))
print(" " + rw("마디", 8) + " " + rw("무엇을 계산", 18) + " " + rl("값", 16))
for nm, ex, v in steps:
print(" " + rw(nm, 8) + " " + rw(ex, 18) + " " + rl("%.6f" % v, 16))
print(" 마디 b 가 두 곳에 쓰입니다. c 를 만들 때와 L 을 만들 때입니다")
print(" 이런 갈래가 있으면 뒤로 갈 때 두 경로의 기여를 더해야 합니다")
print()
print(" (3) 위상 정렬로 계산 순서를 정합니다")
graph = {
"x": [],
"y": [],
"a": ["x", "y"],
"b": ["a", "x"],
"c": ["b"],
"L": ["c", "b"],
}
order = []
seen = set()
def visit(n):
if n in seen:
return
for p in graph[n]:
visit(p)
seen.add(n)
order.append(n)
visit("L")
print(" 부모를 먼저 방문하고 자기를 넣습니다")
print(" 계산 순서는 %s 입니다" % " ".join(order))
print(" " + rw("마디", 8) + " " + rw("부모", 14) + " " + rl("순서에서의 자리", 18))
for n in order:
print(" " + rw(n, 8) + " " + rw(" ".join(graph[n]) if graph[n] else "없음", 14) + " " + rl("%d" % (order.index(n) + 1), 18))
print(" 어느 마디도 자기 부모보다 먼저 오지 않습니다")
print(" 순환이 있으면 이 정렬이 실패합니다. 그래서 계산 그래프는 순환이 없어야 합니다")
print()
print("문제 2. 뒤로 훑기")
print()
print(" (1) 국소 미분을 정리합니다")
rows = [
("더하기", "양쪽에 그대로", "1 과 1"),
("곱하기", "상대방 값", "y 와 x"),
("지수", "자기 출력", "출력 그대로"),
("로그", "1 나누기 입력", "역수"),
("최대", "이긴 쪽에만", "1 과 0"),
]
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) 손으로 뒤로 훑습니다")
a_v = xa * ya
b_v = a_v + xa
c_v = float(np.exp(b_v))
gL = 1.0
gc = gL * (1.0 / c_v)
gb_direct = gL * 1.0
gb_via_c = gc * c_v
gb = gb_direct + gb_via_c
ga = gb * 1.0
gy = ga * xa
gx_via_a = ga * ya
gx_via_b = gb * 1.0
gx = gx_via_a + gx_via_b
print(" 뿌리에서 1 로 시작해 거꾸로 갑니다")
print(" " + rw("마디", 8) + " " + rl("값", 14) + " " + rl("그래디언트", 14) + " " + "어디서 왔나")
print(" " + rw("L", 8) + " " + rl("%.6f" % steps[3][2], 14) + " " + rl("%.6f" % gL, 14) + " " + "뿌리")
print(" " + rw("c", 8) + " " + rl("%.6f" % c_v, 14) + " " + rl("%.6f" % gc, 14) + " " + "로그의 미분")
print(" " + rw("b", 8) + " " + rl("%.6f" % b_v, 14) + " " + rl("%.6f" % gb, 14) + " " + "두 경로의 합")
print(" " + rw("a", 8) + " " + rl("%.6f" % a_v, 14) + " " + rl("%.6f" % ga, 14) + " " + "더하기는 그대로")
print(" " + rw("y", 8) + " " + rl("%.6f" % ya, 14) + " " + rl("%.6f" % gy, 14) + " " + "곱하기는 상대 값")
print(" " + rw("x", 8) + " " + rl("%.6f" % xa, 14) + " " + rl("%.6f" % gx, 14) + " " + "두 경로의 합")
print(" b 로 오는 그래디언트가 %.6f 더하기 %.6f 입니다" % (gb_direct, gb_via_c))
print(" c 를 지나온 쪽이 지수와 로그가 서로 지워 정확히 1 이 됩니다")
print()
print(" (3) 수치 미분으로 확인합니다")
def F(xv, yv):
a = xv * yv
b = a + xv
return float(np.log(np.exp(b)) + b)
h = 1e-6
gx_num = (F(xa + h, ya) - F(xa - h, ya)) / (2 * h)
gy_num = (F(xa, ya + h) - F(xa, ya - h)) / (2 * h)
print(" " + rw("무엇", 10) + " " + rl("손으로 훑은 값", 16) + " " + rl("수치 미분", 14) + " " + rl("차이", 14))
print(" " + rw("x", 10) + " " + rl("%.6f" % gx, 16) + " " + rl("%.6f" % gx_num, 14) + " " + rl("%.10f" % abs(gx - gx_num), 14))
print(" " + rw("y", 10) + " " + rl("%.6f" % gy, 16) + " " + rl("%.6f" % gy_num, 14) + " " + rl("%.10f" % abs(gy - gy_num), 14))
print(" 손으로 훑은 값과 수치 미분이 소수점 아래 여섯 자리까지 같습니다")
print()
print(" (4) 갈래를 안 더하면 어떻게 되는지 봅니다")
gx_wrong1 = gx_via_a
gx_wrong2 = gx_via_b
print(" " + rw("무엇으로 계산", 24) + " " + rl("값", 14) + " " + rl("참값과의 차", 14))
print(" " + rw("두 경로를 더함", 24) + " " + rl("%.6f" % gx, 14) + " " + rl("%.10f" % abs(gx - gx_num), 14))
print(" " + rw("a 를 지난 경로만", 24) + " " + rl("%.6f" % gx_wrong1, 14) + " " + rl("%.10f" % abs(gx_wrong1 - gx_num), 14))
print(" " + rw("b 로 바로 간 경로만", 24) + " " + rl("%.6f" % gx_wrong2, 14) + " " + rl("%.10f" % abs(gx_wrong2 - gx_num), 14))
print(" 한 경로만 세면 크게 틀립니다")
print(" 231강 문제 4 의 브로드캐스트도 같은 이야기입니다. 복사는 뒤에서 합이 됩니다")
print()
print("문제 3. 그래프를 직접 만들기")
print()
print(" (1) 무엇이 필요한지 정리합니다")
rows = [
("값", "앞으로 갈 때 계산한 결과", "저장해 둡니다"),
("부모", "누구로부터 왔나", "뒤로 갈 방향"),
("국소 미분 규칙", "연산마다 다름", "마디에 붙여 둠"),
("그래디언트 자리", "쌓아 더할 곳", "0 으로 시작"),
("방문 순서", "위상 정렬의 역순", "한 번씩만 방문"),
]
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) 마디 하나를 클래스로 적어 자동미분을 만듭니다")
class Node(object):
def __init__(self, v, parents=(), back=None, name="?"):
self.v = float(v)
self.parents = parents
self.back = back
self.g = 0.0
self.name = name
def __add__(self, o):
o = o if isinstance(o, Node) else Node(o, name="const")
return Node(self.v + o.v, (self, o), lambda g: (g, g), "add")
def __mul__(self, o):
o = o if isinstance(o, Node) else Node(o, name="const")
return Node(self.v * o.v, (self, o), lambda g: (g * o.v, g * self.v), "mul")
def __sub__(self, o):
o = o if isinstance(o, Node) else Node(o, name="const")
return Node(self.v - o.v, (self, o), lambda g: (g, -g), "sub")
def nexp(a):
out = Node(np.exp(a.v), (a,), None, "exp")
out.back = lambda g: (g * out.v,)
return out
def nlog(a):
return Node(np.log(a.v), (a,), lambda g: (g / a.v,), "log")
def nrelu(a):
return Node(a.v if a.v > 0 else 0.0, (a,), lambda g: (g * (1.0 if a.v > 0 else 0.0),), "relu")
def backward(root):
order2, seen2 = [], set()
def vis(n):
if id(n) in seen2:
return
seen2.add(id(n))
for p in n.parents:
vis(p)
order2.append(n)
vis(root)
for n in order2:
n.g = 0.0
root.g = 1.0
for n in reversed(order2):
if n.back is None:
continue
gs = n.back(n.g)
for p, gp in zip(n.parents, gs):
p.g += gp
return order2
X = Node(xa, name="x")
Y = Node(ya, name="y")
A = X * Y
Bn = A + X
C = nexp(Bn)
Ln = nlog(C) + Bn
od = backward(Ln)
print(" 같은 식을 만든 그래프로 돌립니다")
print(" " + rw("무엇", 10) + " " + rl("손으로 훑은 값", 16) + " " + rl("그래프가 낸 값", 16) + " " + rl("차이", 14))
print(" " + rw("x", 10) + " " + rl("%.6f" % gx, 16) + " " + rl("%.6f" % X.g, 16) + " " + rl("%.10f" % abs(gx - X.g), 14))
print(" " + rw("y", 10) + " " + rl("%.6f" % gy, 16) + " " + rl("%.6f" % Y.g, 16) + " " + rl("%.10f" % abs(gy - Y.g), 14))
print(" 마디는 %d 개이고 방문은 한 번씩입니다" % len(od))
print(" 각 마디는 자기 국소 미분만 압니다. 연결은 그래프가 압니다")
print()
print(" (3) 신경망 한 층을 이 그래프로 학습시킵니다")
r = np.random.default_rng(20232)
n = 200
xs = r.uniform(-2, 2, n)
ys = 0.7 * xs * xs - 1.2 * xs + 0.4 + r.normal(0, 0.2, n)
p0 = [Node(v, name="w%d" % i) for i, v in enumerate(r.normal(0, 0.5, 3))]
def model(px, ps):
return ps[0] * (px * px) + ps[1] * px + ps[2]
def train(ps, lr, steps):
hist = []
for t in range(steps):
tot = Node(0.0, name="zero")
for i in range(n):
d = model(Node(xs[i], name="xi"), ps) - Node(ys[i], name="yi")
tot = tot + d * d
L = tot * Node(1.0 / n, name="inv")
backward(L)
for pn in ps:
pn.v -= lr * pn.g
if t in (0, 4, 19, 49, 99):
hist.append((t + 1, L.v, [pn.v for pn in ps]))
return hist
hist = train(p0, 0.05, 100)
print(" 참 계수는 0.7000 과 -1.2000 과 0.4000 입니다")
print(" " + rl("걸음", 8) + " " + rl("손실", 14) + " " + rl("첫 계수", 12) + " " + rl("둘째 계수", 12) + " " + rl("셋째 계수", 12))
for t, Lv, pv in hist:
print(" " + rl("%d" % t, 8) + " " + rl("%.6f" % Lv, 14) + " " + rl("%.6f" % pv[0], 12) + " " + rl("%.6f" % pv[1], 12) + " " + rl("%.6f" % pv[2], 12))
print(" 정규방정식으로 푼 값과 견줍니다")
Xd = np.column_stack([xs * xs, xs, np.ones(n)])
bstar = np.linalg.solve(Xd.T @ Xd, Xd.T @ ys)
print(" " + rw("무엇", 16) + " " + rl("첫 계수", 12) + " " + rl("둘째 계수", 12) + " " + rl("셋째 계수", 12))
print(" " + rw("그래프로 학습", 16) + " " + rl("%.6f" % p0[0].v, 12) + " " + rl("%.6f" % p0[1].v, 12) + " " + rl("%.6f" % p0[2].v, 12))
print(" " + rw("정규방정식", 16) + " " + rl("%.6f" % bstar[0], 12) + " " + rl("%.6f" % bstar[1], 12) + " " + rl("%.6f" % bstar[2], 12))
print(" 가장 큰 차이는 %.6f 입니다" % maxdiff([pn.v for pn in p0], bstar))
print(" 100 걸음으로 정규방정식의 답에 소수점 둘째 자리까지 닿았습니다")
print(" 더 돌리면 더 가까워집니다. 경사하강은 유한 걸음으로 정확히 도달하지 않습니다")
print()
print(" (4) 마디 수가 늘어나는 것을 봅니다")
def count_nodes(k):
ps = [Node(1.0), Node(1.0), Node(1.0)]
tot = Node(0.0)
for i in range(k):
d = model(Node(xs[i]), ps) - Node(ys[i])
tot = tot + d * d
L = tot * Node(1.0 / k)
return len(backward(L))
print(" " + rl("표본 수", 10) + " " + rl("마디 수", 12) + " " + rl("표본당 마디", 14))
for k in [1, 10, 50, 200]:
cnt = count_nodes(k)
print(" " + rl("%d" % k, 10) + " " + rl("%d" % cnt, 12) + " " + rl("%.4f" % (cnt / float(k)), 14))
print(" 표본 하나가 마디 열 개 남짓을 만듭니다")
print(" 그래서 실제 구현은 표본마다 마디를 만들지 않고 배치를 한 덩어리로 다룹니다")
print(" 231강 문제 4 의 배치 규칙이 그것을 가능하게 합니다")
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) 비용을 견줍니다")
print(" 한 번 훑는 비용을 1 로 두고 필요한 횟수를 셉니다")
print(" " + rl("입력 수", 10) + " " + rl("출력 수", 10) + " " + rl("뒤로 가는 횟수", 16) + " " + rl("앞으로 가는 횟수", 18) + " " + "어느 쪽이 싼가")
for din, dout in [(1, 1), (1000000, 1), (1, 1000000), (1000, 1000)]:
cheap = "뒤로" if dout < din else ("앞으로" if din < dout else "같음")
print(" " + rl("%d" % din, 10) + " " + rl("%d" % dout, 10) + " " + rl("%d" % dout, 16) + " " + rl("%d" % din, 18) + " " + cheap)
print(" 신경망은 둘째 줄입니다. 파라미터가 백만 개이고 손실은 하나입니다")
print(" 한 번 뒤로 가면 백만 개의 그래디언트를 모두 얻습니다")
print(" 그래서 딥러닝은 언제나 뒤로 가는 미분을 씁니다")
print()
print(" (3) 앞으로 가는 미분을 직접 돌려 봅니다")
def fwd_mode(xv, yv, seed_x, seed_y):
x_, dx = xv, seed_x
y_, dy = yv, seed_y
a_, da = x_ * y_, dx * y_ + x_ * dy
b_, db = a_ + x_, da + dx
c_, dc = float(np.exp(b_)), float(np.exp(b_)) * db
L_, dL = float(np.log(c_)) + b_, dc / c_ + db
return L_, dL
_, dx1 = fwd_mode(xa, ya, 1.0, 0.0)
_, dy1 = fwd_mode(xa, ya, 0.0, 1.0)
print(" 값과 미분을 쌍으로 들고 앞으로 함께 갑니다")
print(" " + rw("무엇", 10) + " " + rl("앞으로 가는 미분", 18) + " " + rl("뒤로 가는 미분", 16) + " " + rl("차이", 14))
print(" " + rw("x", 10) + " " + rl("%.6f" % dx1, 18) + " " + rl("%.6f" % X.g, 16) + " " + rl("%.10f" % abs(dx1 - X.g), 14))
print(" " + rw("y", 10) + " " + rl("%.6f" % dy1, 18) + " " + rl("%.6f" % Y.g, 16) + " " + rl("%.10f" % abs(dy1 - Y.g), 14))
print(" 같은 답이 나옵니다. 다만 두 번 돌려야 했습니다")
print(" 뒤로 가는 미분은 한 번에 둘 다 냈습니다")
print()
print(" (4) 무엇을 저장해야 하는지 봅니다")
rows = [
("곱하기", "양쪽 입력", "상대 값이 필요함"),
("지수", "출력", "출력이 곧 미분"),
("로그", "입력", "역수가 필요함"),
("정류 선형", "부호만", "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(" 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) 층 수를 바꿔 가며 비용을 셉니다")
print(" 층 L 개를 k 개 구간으로 나눠 구간 경계만 저장합니다")
print(" " + rl("층 수", 10) + " " + rl("다 저장할 때 메모리", 20) + " " + rl("체크포인트 메모리", 20) + " " + rl("몇 배 줄었나", 14))
for L in [16, 64, 256, 1024]:
k = int(round(np.sqrt(L)))
mem = k + L // k
print(" " + rl("%d" % L, 10) + " " + rl("%d" % L, 20) + " " + rl("%d" % mem, 20) + " " + rl("%.4f" % (L / float(mem)), 16))
print(" 메모리가 층 수에서 층 수의 제곱근으로 줄어듭니다")
print(" 층이 1024 개면 1024 에서 %d 로 줄어듭니다" % (int(round(np.sqrt(1024))) + 1024 // int(round(np.sqrt(1024)))))
print(" 대신 뒤로 갈 때 각 구간을 앞으로 한 번 더 계산합니다")
print(" 앞으로 가는 계산이 두 배가 되고 전체 계산량은 대략 1.3 배가 됩니다")
print()
print(" (3) 다시 계산한 값이 같은지 확인합니다")
r2g = np.random.default_rng(30232)
depth = 6
Ws = [r2g.normal(0, 0.6, (4, 4)) for _ in range(depth)]
x0 = r2g.normal(0, 1, 4)
def run_all(x_):
acts = [x_]
for Wl in Ws:
x_ = np.tanh(Wl @ x_)
acts.append(x_)
return acts
def run_from(x_, start, end):
for Wl in Ws[start:end]:
x_ = np.tanh(Wl @ x_)
return x_
acts = run_all(x0)
print(" 층 %d 개를 통과시키고 중간값을 저장합니다" % depth)
print(" " + rl("층", 8) + " " + rl("저장해 둔 값의 첫 원소", 24) + " " + rl("다시 계산한 값", 18) + " " + rl("차이", 14))
for t in [2, 4, 6]:
again = run_from(acts[0], 0, t)
print(" " + rl("%d" % t, 8) + " " + rl("%.10f" % acts[t][0], 24) + " " + rl("%.10f" % again[0], 18) + " " + rl("%.2e" % maxdiff(acts[t], again), 14))
print(" 다시 계산한 값이 저장해 둔 값과 비트 단위로 같습니다")
print(" 같은 연산을 같은 순서로 했기 때문입니다")
print(" 무작위가 섞이면 이야기가 달라집니다. 드롭아웃은 씨앗을 함께 저장해야 합니다")
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(" 233강은 이 그래프 훑기를 신경망에 붙여 역전파 알고리즘으로 적습니다")
print()
print("=" * 78)
print("검산 끝")
print("=" * 78)
# ==============================================================================
# 232강 계산 그래프 코드 검산
# ==============================================================================
#
# 문제 1. 식을 그래프로 적기
#
# (1) 무엇을 마디로 두는지 정리합니다
# 무엇 무엇을 담나 어떻게 알아보나
# 잎 입력이나 파라미터 들어오는 화살이 없음
# 연산 마디 더하기 곱하기 지수 등 부모가 있고 자식이 있음
# 뿌리 손실 나가는 화살이 없음
# 화살 값이 흐르는 방향 앞으로 갈 때의 방향
# 갈래 한 값이 여러 곳에 쓰임 뒤로 갈 때 더해짐
# 231강에서 손으로 이은 두 층이 여기서는 마디 여섯 개짜리 그래프입니다
# 그래프로 적으면 무엇을 어떤 순서로 계산할지가 그림에서 읽힙니다
#
# (2) 간단한 식을 마디로 쪼갭니다
# x 는 2.0000 이고 y 는 3.0000 입니다
# 마디 무엇을 계산 값
# a x 곱하기 y 6.000000
# b a 더하기 x 8.000000
# c 지수 b 2980.957987
# L 로그 c 더하기 b 16.000000
# 마디 b 가 두 곳에 쓰입니다. c 를 만들 때와 L 을 만들 때입니다
# 이런 갈래가 있으면 뒤로 갈 때 두 경로의 기여를 더해야 합니다
#
# (3) 위상 정렬로 계산 순서를 정합니다
# 부모를 먼저 방문하고 자기를 넣습니다
# 계산 순서는 x y a b c L 입니다
# 마디 부모 순서에서의 자리
# x 없음 1
# y 없음 2
# a x y 3
# b a x 4
# c b 5
# L c b 6
# 어느 마디도 자기 부모보다 먼저 오지 않습니다
# 순환이 있으면 이 정렬이 실패합니다. 그래서 계산 그래프는 순환이 없어야 합니다
#
# 문제 2. 뒤로 훑기
#
# (1) 국소 미분을 정리합니다
# 연산 뒤로 무엇을 곱하나 구체적으로
# 더하기 양쪽에 그대로 1 과 1
# 곱하기 상대방 값 y 와 x
# 지수 자기 출력 출력 그대로
# 로그 1 나누기 입력 역수
# 최대 이긴 쪽에만 1 과 0
# 마디 하나는 자기 국소 미분만 알면 됩니다
# 전체 식이 무엇인지는 몰라도 됩니다. 그것이 그래프의 힘입니다
#
# (2) 손으로 뒤로 훑습니다
# 뿌리에서 1 로 시작해 거꾸로 갑니다
# 마디 값 그래디언트 어디서 왔나
# L 16.000000 1.000000 뿌리
# c 2980.957987 0.000335 로그의 미분
# b 8.000000 2.000000 두 경로의 합
# a 6.000000 2.000000 더하기는 그대로
# y 3.000000 4.000000 곱하기는 상대 값
# x 2.000000 8.000000 두 경로의 합
# b 로 오는 그래디언트가 1.000000 더하기 1.000000 입니다
# c 를 지나온 쪽이 지수와 로그가 서로 지워 정확히 1 이 됩니다
#
# (3) 수치 미분으로 확인합니다
# 무엇 손으로 훑은 값 수치 미분 차이
# x 8.000000 8.000000 0.0000000002
# y 4.000000 4.000000 0.0000000006
# 손으로 훑은 값과 수치 미분이 소수점 아래 여섯 자리까지 같습니다
#
# (4) 갈래를 안 더하면 어떻게 되는지 봅니다
# 무엇으로 계산 값 참값과의 차
# 두 경로를 더함 8.000000 0.0000000002
# a 를 지난 경로만 6.000000 2.0000000002
# b 로 바로 간 경로만 2.000000 6.0000000002
# 한 경로만 세면 크게 틀립니다
# 231강 문제 4 의 브로드캐스트도 같은 이야기입니다. 복사는 뒤에서 합이 됩니다
#
# 문제 3. 그래프를 직접 만들기
#
# (1) 무엇이 필요한지 정리합니다
# 무엇 무엇인가 어떻게 쓰나
# 값 앞으로 갈 때 계산한 결과 저장해 둡니다
# 부모 누구로부터 왔나 뒤로 갈 방향
# 국소 미분 규칙 연산마다 다름 마디에 붙여 둠
# 그래디언트 자리 쌓아 더할 곳 0 으로 시작
# 방문 순서 위상 정렬의 역순 한 번씩만 방문
#
# (2) 마디 하나를 클래스로 적어 자동미분을 만듭니다
# 같은 식을 만든 그래프로 돌립니다
# 무엇 손으로 훑은 값 그래프가 낸 값 차이
# x 8.000000 8.000000 0.0000000000
# y 4.000000 4.000000 0.0000000000
# 마디는 7 개이고 방문은 한 번씩입니다
# 각 마디는 자기 국소 미분만 압니다. 연결은 그래프가 압니다
#
# (3) 신경망 한 층을 이 그래프로 학습시킵니다
# 참 계수는 0.7000 과 -1.2000 과 0.4000 입니다
# 걸음 손실 첫 계수 둘째 계수 셋째 계수
# 1 2.294004 0.388890 -0.707712 0.114683
# 5 0.221043 0.700915 -0.938065 0.288694
# 20 0.049283 0.730793 -1.179865 0.344481
# 50 0.047060 0.718515 -1.210341 0.365501
# 100 0.047009 0.714235 -1.210272 0.374317
# 정규방정식으로 푼 값과 견줍니다
# 무엇 첫 계수 둘째 계수 셋째 계수
# 그래프로 학습 0.714235 -1.210272 0.374317
# 정규방정식 0.713382 -1.210168 0.376086
# 가장 큰 차이는 0.001769 입니다
# 100 걸음으로 정규방정식의 답에 소수점 둘째 자리까지 닿았습니다
# 더 돌리면 더 가까워집니다. 경사하강은 유한 걸음으로 정확히 도달하지 않습니다
#
# (4) 마디 수가 늘어나는 것을 봅니다
# 표본 수 마디 수 표본당 마디
# 1 16 16.0000
# 10 106 10.6000
# 50 506 10.1200
# 200 2006 10.0300
# 표본 하나가 마디 열 개 남짓을 만듭니다
# 그래서 실제 구현은 표본마다 마디를 만들지 않고 배치를 한 덩어리로 다룹니다
# 231강 문제 4 의 배치 규칙이 그것을 가능하게 합니다
#
# 문제 4. 앞으로 가는 미분과 뒤로 가는 미분
#
# (1) 둘을 가릅니다
# 무엇 뒤로 앞으로
# 뒤로 가는 미분 출력에서 시작 출력이 적을 때
# 앞으로 가는 미분 입력에서 시작 입력이 적을 때
# 한 번에 얻는 것 뒤로는 모든 입력 앞으로는 모든 출력
# 몇 번 돌리나 뒤로는 출력 수만큼 앞으로는 입력 수만큼
# 값을 저장하나 뒤로는 저장함 앞으로는 안 함
#
# (2) 비용을 견줍니다
# 한 번 훑는 비용을 1 로 두고 필요한 횟수를 셉니다
# 입력 수 출력 수 뒤로 가는 횟수 앞으로 가는 횟수 어느 쪽이 싼가
# 1 1 1 1 같음
# 1000000 1 1 1000000 뒤로
# 1 1000000 1000000 1 앞으로
# 1000 1000 1000 1000 같음
# 신경망은 둘째 줄입니다. 파라미터가 백만 개이고 손실은 하나입니다
# 한 번 뒤로 가면 백만 개의 그래디언트를 모두 얻습니다
# 그래서 딥러닝은 언제나 뒤로 가는 미분을 씁니다
#
# (3) 앞으로 가는 미분을 직접 돌려 봅니다
# 값과 미분을 쌍으로 들고 앞으로 함께 갑니다
# 무엇 앞으로 가는 미분 뒤로 가는 미분 차이
# x 8.000000 8.000000 0.0000000000
# y 4.000000 4.000000 0.0000000000
# 같은 답이 나옵니다. 다만 두 번 돌려야 했습니다
# 뒤로 가는 미분은 한 번에 둘 다 냈습니다
#
# (4) 무엇을 저장해야 하는지 봅니다
# 연산 무엇을 저장 왜
# 곱하기 양쪽 입력 상대 값이 필요함
# 지수 출력 출력이 곧 미분
# 로그 입력 역수가 필요함
# 정류 선형 부호만 1 비트면 충분
# 더하기 아무것도 국소 미분이 상수
# 뒤로 가는 미분은 앞으로 간 값을 저장해야 합니다. 그것이 메모리를 먹습니다
# 마지막 줄이 더하기가 값싼 이유입니다
# 245강의 잔차 연결이 더하기인 것은 우연이 아닙니다
#
# 문제 5. 다시 계산해서 메모리를 아끼기
#
# (1) 맞바꿈을 정리합니다
# 방식 무엇이 좋나 무엇을 치르나
# 다 저장 가장 빠름 메모리가 층 수에 비례
# 아무것도 안 저장 메모리 최소 다시 계산이 층 수의 제곱
# 체크포인트 가운데 둘 다 층 수의 제곱근
#
# (2) 층 수를 바꿔 가며 비용을 셉니다
# 층 L 개를 k 개 구간으로 나눠 구간 경계만 저장합니다
# 층 수 다 저장할 때 메모리 체크포인트 메모리 몇 배 줄었나
# 16 16 8 2.0000
# 64 64 16 4.0000
# 256 256 32 8.0000
# 1024 1024 64 16.0000
# 메모리가 층 수에서 층 수의 제곱근으로 줄어듭니다
# 층이 1024 개면 1024 에서 64 로 줄어듭니다
# 대신 뒤로 갈 때 각 구간을 앞으로 한 번 더 계산합니다
# 앞으로 가는 계산이 두 배가 되고 전체 계산량은 대략 1.3 배가 됩니다
#
# (3) 다시 계산한 값이 같은지 확인합니다
# 층 6 개를 통과시키고 중간값을 저장합니다
# 층 저장해 둔 값의 첫 원소 다시 계산한 값 차이
# 2 -0.6048102727 -0.6048102727 0.00e+00
# 4 -0.7779505213 -0.7779505213 0.00e+00
# 6 -0.3419523506 -0.3419523506 0.00e+00
# 다시 계산한 값이 저장해 둔 값과 비트 단위로 같습니다
# 같은 연산을 같은 순서로 했기 때문입니다
# 무작위가 섞이면 이야기가 달라집니다. 드롭아웃은 씨앗을 함께 저장해야 합니다
#
# (4) 이 강의를 한 장으로 모읍니다
# 물음 한 줄로
# 무엇이 마디인가 연산 하나가 마디 하나입니다
# 순서는 누가 정하나 위상 정렬이 정합니다
# 마디가 아는 것 자기 국소 미분뿐입니다
# 갈래는 어떻게 뒤로 갈 때 더합니다
# 왜 뒤로 가나 출력이 하나이고 입력이 많기 때문입니다
# 무엇을 저장하나 국소 미분에 필요한 값만 저장합니다
# 메모리가 모자라면 다시 계산해서 바꿉니다
# 233강은 이 그래프 훑기를 신경망에 붙여 역전파 알고리즘으로 적습니다
#
# ==============================================================================
# 검산 끝
# ==============================================================================