파트 7: CUDA와 Python 통합
Source: Dev.to
(번역할 텍스트가 제공되지 않았습니다. 번역이 필요한 본문을 알려주시면 한국어로 번역해 드리겠습니다.)
XOR 테스트
신경망을 성공적으로 설정한 후, XOR 연산으로 테스트했습니다. XOR은 비선형 연산이므로, 네트워크가 비선형성을 감지할 수 있는지를 확인하는 일종의 “Hello World”입니다. 테스트는 원활하게 진행되었으며, 네트워크가 더 큰 과제를 수행할 준비가 되었음을 나타냅니다.
같은 작업을 Rust로 복제해야 했지만, 머리가 허락하지 않았습니다. Rust 코드를 한 줄이라도 쓰려고 할 때마다 다음과 같은 질문이 떠올랐습니다:
- 복잡한 방정식을 입력하면 어떻게 될까?
- 선형 회귀 데이터를 신경망에 통과시키면 어떻게 될까?
- 로지스틱 회귀 데이터셋을 어떻게 처리할까?
- CUDA와 함께 작동할까?
- 선형 회귀 프로그램이 CUDA와 함께 작동할까?
답이 없는 질문이 너무 많았습니다. 흐름이 끊겼습니다.
CUDA‑기반 선형 회귀 프로그램
이미 CUDA를 사용하고 있었기 때문에, 가장 손쉽게 할 수 있는 일은 내 라이브러리의 Python 및 Rust 버전에 CUDA를 통합하는 것이었습니다. 그래서 선형 회귀 프로그램을 GPU에서 실행하도록 전환했습니다.
예상대로, 이것은 작동하지 않았습니다. CPU‑만 사용한 프로그램은 매우 낮은 Root MSE = 7.75를 보였지만, GPU 버전은 40이라는 값을 반환했습니다.
디버깅
코드를 한 줄씩 살펴본 결과, 선형 예측 함수에서 GPU가 0으로 채워진 행렬을 반환하고 있음을 발견했습니다. 실제 출력을 반환하도록 수정한 뒤, 프로그램은 정상적으로 동작하기 시작했습니다.
Results:
Total samples: 93
Mean Squared Error: 59.6779
Root MSE: 7.7251
마찬가지로 로지스틱 회귀를 위한 또 다른 CUDA 프로그램을 작성했으며, 이 역시 정상적으로 동작했습니다. 안타깝게도 CUDA 로지스틱 프로그램의 결과를 캡처하는 것을 놓쳤습니다.
One question shot down: Rust CUDA program can work with the regression datasets. Let’s move on.
Source:
CUDA Integration with Python
한 번 결과에 만족했을 때, 나는 XOR 테스트 데이터셋에 사용된 파이썬 신경망 스크립트로 눈을 돌렸다. 나는 이미 더 큰 데이터셋을 다뤄본 경험이 있었기 때문에, 단순한 XOR 테스트를 실행하는 것이 별다른 의미가 없었다. 선형 회귀와 로지스틱 회귀 데이터셋도 신경망에서 실행하고 싶었고, 이상적으로는 문제 없이 제대로 동작해야 했다.
JSON 파일을 스크립트에 연결하고 신경망 훈련을 시작했다.
오, 또 다른 토끼굴을 열어버렸네.
거대한 데이터 로드에서는 CPU가 순차적으로 작업하기에 충분히 강력하지 않다. NumPy가 배열에 대해 아주 미미한 병렬화를 수행하지만, 이메일 스팸/햄 데이터셋이 한계점이 되었다—CPU가 더 이상 로드를 감당하지 못했다. 나는 scikit‑learn에서 해결책을 찾아보았고, 해당 라이브러리가 cupy 를 통한 제한적인 GPU 지원을 제공한다는 것을 알게 되었지만, 설정에 몇 가지 어려움이 있었다.
수학을 이미 이해하고 있었고, NumPy 버전의 신경망 프로그램도 가지고 있었기 때문에, 나는 NumPy 임포트를 CuPy 로 바꾸었다. 물론 최적화가 많이 부족하겠지만, 나중에 최적화 기법을 배우는 데 도움이 될 것이다.
계획된 해결책: WSL을 실행하고 cupy를 설치한 뒤, 스크립트에서 numpy를 cupy로 교체했다. 그러자 또 다른 모래늪에 빠졌다.
NumPy 버전에서는 수천 번의 반복을 10분 안에 수행했지만, CuPy 버전에서는 10분 안에 1 000번도 완료하지 못했다. 다행히 이전에 Rust를 다뤘던 경험이 도움이 되었다: 병목 현상이 호스트‑투‑디바이스(H2D)와 디바이스‑투‑호스트(D2H) 데이터 복사 오버헤드였음이 밝혀졌다.
라이브러리 문서를 다시 살펴보고 해결책을 찾아 적용했다. 두 가지 조정이 필요했다:
- D2H 복사를 피하기 위해 각 epoch마다 오류 계산을 제거한다.
- 각 epoch마다가 아니라 연산 집합 후에
synchronize를 사용한다.
요약
H2D/D2H 복사 오버헤드를 해결한 뒤, 프로그램은 꽤 괜찮게 동작했습니다. 이제 같은 스크립트를 10–15 초 안에 실행할 수 있었으며, 이는 ≈60배의 속도 향상이었습니다.
솔직히 말해서, 속도에 중독됐어요…
설정과 디버깅을 하루 종일 고생한 뒤, 잠깐의 놀이 시간이 허락되었습니다. 저는 실험을 시작했습니다:
- 먼저 단일 레이어(부하가 거의 없음).
- 그 다음 재미로 레이어를 두 개 더 추가했습니다.
이렇게 하면 신경망이 선형 회귀 데이터셋의 MSE를 더 낮게 만들었습니다:
Test MSE: 52.3265 (이전 59.6779)
Test MAE: 5.3395
다시 한 번 노력은 보람 있었습니다. 각 출력에서 네트워크가 단계별로 학습하는 모습을 보는 것이 만족스러웠습니다. 속도 향상 덕분에 더 낮은 학습률과 더 높은 epoch 수(훈련 반복 횟수)를 선택할 수 있었고, 네트워크 구성(레이어 수, 레이어당 노드 수 등)도 조정할 수 있었습니다.
Epoch 1/200000 | Error: 25.926468
Epoch 1000/200000| Error: 0.482236
Epoch 2000/200000| Error: 0.414885
Epoch 3000/200000| Error: 0.377820
Epoch 4000/200000| Error: 0.354329
Epoch 5000/200000| Error: 0.340112
Epoch 6000/200000| Error: 0.331060
Epoch 7000/200000| Error: 0.324392
Epoch 8000/200000| Error: 0.319276
Epoch 9000/200000| Error: 0.315130
Epoch 10000/200000| Error: 0.311793
Epoch 11000/200000| Error: 0.308888
Epoch 12000/200000| Error: 0.306242
Epoch 13000/200000| Error: 0.303405
Epoch 14000/200000| Error: 0.300487
Epoch 15000/200000| Error: 0.298240
Epoch 16000/200000| Error: 0.296392
오류를 살펴보니 Gradient Descent가 작동하는 방식을 알 수 있었습니다: 처음에는 오류가 크고 네트워크가 빠르게 수렴을 향해 교정하지만, 시간이 지나면서 네트워크가 학습함에 따라 오류가 감소하고 교정 폭도 작아집니다.
학습률을 바꿔 보았습니다. 학습률을 크게 잡으면 신경망이 수렴하지 못하고 두 점 사이를 오가다 결국 더 높은 오류를 반환했습니다.
학습률 및 네트워크 깊이에 대한 실험
반대로 학습률을 작게 잡으면 수렴이 부드러워지지만, 수렴하는 데 매우 오랜 시간이 걸렸습니다.
학습률을 0.005와 같이 아주 작게 잡아도 20 000 epoch 정도 지나면 거의 안정화되지만, 여전히 약간의 진동을 보일 수 있다는 점을 발견했습니다.
또 다른 관찰은 훈련 루프를 많이 돌려도 Mean Absolute Error(MAE)가 크게 변하지 않는다는 점이었습니다. 어느 순간 포화가 불가피해집니다.
2개의 은닉 레이어로 얻은 최고의 결과
Training completed in 285.5212 seconds.
Final Results after 100 000 epochs and learning rate 0.001:
Test MSE: 46.2653
Test MAE: 5.2730
(epoch 수 감소, 학습률 증가) 비교 결과
Training completed in 117.2746 seconds.
Final Results after 40 000 epochs and learning rate 0.005:
Test MSE: 52.6716
Test MAE: 5.3199
Starting training...
1개의 은닉 레이어로 얻은 최고의 결과
Training completed in 77.6143 seconds.
Final Results after 40 000 epochs and learning rate 0.005:
Test MSE: 45.5669
Test MAE: 5.1707
모든 실험을 마칠 때쯤 6시간이 흘렀습니다. 이러한 통찰을 통해 신경망을 이전보다 조금 더 잘 이해하게 되었습니다.
예전 Rust로 작성한 로지스틱 회귀 프로그램의 성능을 늘 항상 의심해 왔습니다. CUDA 통합에 성공했으니 이제 공정한 비교를 할 차례였습니다. 저는 단일 선형 레이어와 시그모이드 레이어를 이어 붙여 신경망 안에 로지스틱 회귀를 구현했습니다.
그 결과는 놀라웠습니다: 비효율적으로 작성한 CUDA 프로그램이 실제로 cupy보다 더 빠르게 동작했습니다.
프로그램. 더 이상 조사하지 않았고, 하루를 마무리했습니다.
빠른 인벤토리 확인
- CPU 선형 회귀
- CPU 로지스틱 회귀
- GPU 선형 회귀
- GPU 로지스틱 회귀
- GPU 기반 신경망을 실행하는 Python 스크립트
그리고 알겠어요? 이것이 더 많은 실험을 위한 완벽한 기반을 만들었습니다. 다음에 무엇이 나올지 기대해 주세요!
