이 서평은 출판사의 후원을 받아 작성하였습니다.
어떤 분야가 되었건 마찬가지겠지만, 머신러닝도 똑같이 실무에 가면 이런저런 문제에 부닥친다. 특히 단순히 머신러닝의 수학적 모델링에 관한 문제뿐 아니라, 서버나 다른 인프라 같은 문제들이 실무를 해야 하는 사람들에게 풀기 어려운 과제가 되곤 한다.
풀기 어렵다는 말은 단순히 문제가 기술적인 측면에서 어렵다는 의미는 아니다.
물론 기술적으로 어렵다. 분산처리 시스템인데 당연히 어려울 수밖에 없다.
하지만, 실무를 해야하는 곳이 분산처리 시스템을 운용해야할 만큼 규모가 큰 데이터를 다루는 곳이라면, 각 팀마다 R&R이 다를 것이고, 인프라를 다루는 팀이나 DBA나 각각 분리되어있고, PM 조직도 따로 있을 것이기 때문에 이들과의 소통 및 협업을 통해 원하는 것을 이끌어내야 한다는 점이 더 어려울 것이다.
그렇기에 이 책은 ML을 실무로 하지 않는 나 같은 사람에게도 유용한 책일 수 있다.
이 책을 100% 이해할 필요가 없기 때문이다. 깊이 있는 MLOps는 잘 모르더라도, 대략적으로 어떤 용어를 어떤 개념으로 사용하는지, 어떻게 문제접근을 하는지 소통을 하기 위한 도구 정도로 생각하면서 읽어본다면, 컴퓨터 공학적 지식도 쌓으면서 인프라와 ML 모두에 가까워질 수 있다. 물론 쉽진 않겠지만 말이다.
밑줄긋기
p.51
넘파이 배열 형태로 메모리에 올라가있던 Fashion-MNIST 데이터셋을 텐서플로의 from_tensor_slices() API를 사용해 tf.Dataset 객체로 변환했다. 이 과정에서 넘파이 배열은 메모리상에 여러 번 복사되었고, 결과적으로 tf.GraphDef의 메모리 제한값인 2GB에 다다르면서 메모리 에러가 발생했다. 즉, 이런 방식으로는 이보다 큰 데이터셋은 사용할 수 없다.
이런 문제는 텐서플로와 같은 프레임워크를 사용하다보면 흔히 발생할 수 있다. 해결책은 간단하다. 이런 패턴은 텐서플로의 성능을 최대로 활용하지 않는 패턴이므로, 더 큰 데이터셋을 사용하기 위해서는 전체 데이터셋을 메모리에 모두 올리지 않고 사용할 수 있는 다른 API를 사용해야 한다.
예를들어 tensorflow_io는 기본 텐서플로가 제공하지 않는 파일 시스템이나 파일 포맷 등과 관련된 기능을 모아놓은 라이브러리이다. 아래 코드와 같이 tfio.IODataset.from_mnist()를 이용하면 MNIST 데이터셋을 로컬 디스크에 저장하지 ㅇ낳고 바로 메모리에 올릴 수 있다. 이는 tensorflow_io가 내부적으로 HTTP 파일 시스템을 사용하기 때문이다.
import tensroflow_io as tfio
dataset_url = "https://storage.googleapis.com/cvdf-datasets/mnist/"
d_train = tfio.IODataset.from_mnist(
dataset_url + "train-images-idx3-ubyte.gz"
dataset_url + "train-labels-idx1-ubyte.gz"
)
p.54~55
배치 처리를 수행하기에 앞서 고려해야 하는 사항이 있다. 이 방식은 데이터셋을 스트리밍 방식으로 전체 데이터셋의 일부로 학습을 실행할 수 있는 수학적 연산이나 알고리즘에만 적용할 수 있다. 다시 말해 알고리즘 학습을 위해 전체 데이터셋을 한 번에 학습하는 것이 필요하다면 배치 처리를 사용할 수 없다. 예를 들어 학습을 위해 특정 피처의 총합을 알아야 하는 알고리즘이라면 배치 처리는 실행 가능한 방법이 아니다. 전체 데이터셋의 부분 집합으로는 해당 정보를 얻을 수 없기 때문이다. 머신러닝 연구자나 실무자가 Fashion-MNIST에 대해 더 나은 성능과 정확도를 얻고자 다양한 머신러닝 모델을 사용한다고 가정해보자. 이때 어떤 알고리즘이 모델의 파라미터를 업데이트하기 위해 각 클래스마다 최소 10개의 데이터를 필요로 한다면, 배치 처리는 적절한 방법이 아니다. 모든 미니배치에 각 클래스별로 항상 최소 10개의 데이터가 포함되는 것이 아니기 때문이다. 특히 배치 크기가 작다면 더욱 그렇다. 극단적인 예시로 배치 크기가 10인 경우를 생각해보자. 모든 미니배치에 각 클래스의 데이터가 최소 하나씩 포함되는 경우, 즉 10개 클래스의 데이터를 모두 한 장씩 포함한 10장이 구성되는 경우는 매우 드물 것이다.
기억해야 할 또 다른 점이 있다. 머신러닝, 특히 딥러닝 모델을 학습시킬 때 적절한 배치 크기는 자원을 얼마나 사용할 수 있느냐에 따라 크게 달라진다는 것이다. 특히 공유 자원 환경(shared-resource environment)이라면 배치 크기를 결정하는 것이 더욱 어려워진다. 머신러닝 학습에서 자원을 얼마나 효율적으로 활용하는지는 모델의 구조뿐만 아니라 배치 크기에도 달려 있다. 이렇게 자원과 배치 크기 사이의 상호 의존성은 머신러닝 실무자가 작업을 효율적으로 실행하고 자원을 활용하기 위해 고려해야 하는 것들을 더욱 복잡하게 만든다.
다행히도 배치 크기를 자동으로 최적화해주는 알고리즘과 프레임워크가 있다. 예를 들어 AdaptDL(https://github.com/petuum/adaptdl)을 사용하면 자동으로 배치 크기를 조절하여 효율적으로 분산학습을 할 수 있다. 이 프레임워크는 학습 과정에서 시스템 성능과 그레이디언트 노이즈 스케일을 측정하여 가장 효율적인 배치 크기를 선택한다.
p.58
배치 처리 방식은 모델을 배치별로 순차적으로 학습시킬 충분한 시간이 있는 경우에만 괜찮은 방법일 수 있다. 그러나 실제 애플리케이션을 개발하기 위해서는 더 효율적으로 모델을 학습시키는 방법이 필요하다.
p.60
큰 데이터셋을 여러 개의 작은 조각으로 나누어 여러 워커에 분산시키는 프로세스를 샤딩(sharding)이라고 하며, 이러한 작은 데이터 조각을 데이터 샤드(data shard)라고 한다.
p.61
샤드는 본질적으로 전체 데이터셋의 일부를 포함하도록 분할하는 수평 데이터 분할 (horizontal data partition)이며, 가로 분할이라고도 한다.
p.63
수동 샤딩의 가장 큰 문제는 샤드가 균일하지 않게 할당될 수 있다는 점이다. 샤드의 크기가 균일하지 않으면 문제가 발생할 수 있다. 일부 샤드는 과도하게 커져 과부하가 걸리는 반면, 다른 샤드는 지나치게 작아 워커의 자원이 낭비될 수 있다. 이러한 불균형은 여러 개의 워커로 모델을 학습시키는 프로세스가 지연되는 원인이 된다.
p.64~65
한 샤드에 너무 많은 데이터가 들어가게 되면 속도가 느려지거나 서버에 과부하가 걸릴 수 있으므로 피하는 것이 좋다. 이 문제는 전체 데이터셋을 너무 적은 수의 샤드에 강제로 분산시키는 경우에 발생할 수 있다. 개발 환경 혹은 테스트 환경에서는 시도해도 큰 문제가 되지 않지만, 프로덕션 환경에서 사용하기에는 이상적이지 않다.
또한 데이터셋에 새로운 데이터가 업데이트될 때마다 수동으로 샤딩을 진행하는 것은 운영 비용이 클 뿐만 아니라 복잡도 또한 높아진다. 데이터 유실을 방지하기 위해 여러 개의 워커가 전부 백업되어야 하며, 데이터 마이그레이션 혹은 스키마 변경이 일어나는 경우 모든 샤드가 동일한 스키마 복사본을 유지하는 것 또한 보정되어야 한다.
이러한 문제를 해결하기 위해 수동 샤딩 대신 알고리즘을 기반으로 하는 자동 샤딩을 사용할 수 있다. 예를 들어 해시 샤딩(hash sharding)은 데이터 샤드의 키값으로 해시를 생성한다. 생성된 해시값은 각 데이터가 속할 샤드를 결정한다. 균일한 해싱 알고리즘을 사용한다면 해시 함수가 데이터를 서로 다른 워커에 고르게 분산시킬 것이므로 위에서 언급한 문제를 줄일 수 있다. 또한 비슷한 데이터가 같은 샤드에 배치될 가능성도 낮아진다.
샤딩 패턴은 매우 큰 데이터셋을 여러 데이터 샤드로 나누어 여러 워커에 분산시키고, 각 워커가 개별 데이터 샤드를 독립적으로 사용함으로써 성능을 크게 높인다. 이 방법을 활용하면 배치 처리가 순차적으로 모델을 학습시킴으로써 생기는 지연을 피할 수 있다. 배치와 샤딩 패턴은 전체 데이터셋을 모두 사용하면서 모델 학습 프로세스를 향상시킨다. 다만, 전체 데이터셋을 여러 번 사용해야 하는 머신러닝 알고리즘과 같은 경우에는 배치 및 샤딩을 두 번 이상 수행해야 한다.
p.66
트리 기반 알고리즘이나 딥러닝 같은 현대의 머신러닝 알고리즘은 대부분 여러 에포크에 걸친 학습이 필요하다. 1 에포크란 전체 데이터셋을 모두 사용하는 한 번의 주기를 의미한다. 즉, 데이터셋에 포함된 모든 데이터를 최소 한 번씩 전부 사용해서 학습을 진행하는 단위다.
p.68~69
머신러닝 모델을 여러 에포크에 걸쳐 학습시키는 데 비현실적으로 많은 시간이 든다면 어떤 방법으로 개선할 수 있을까? 첫 번째 에포크는 말 그대로 머신러닝 모델이 전체 데이터셋을 처음 사용해서 학습하는 단계이기 때문에 할 수 있는 방법이 거의 없다. 하지만 두 번째라면 어떨까? 모델이 이미 데이터셋을 한 번은 사용했다는 점을 활용해 볼 수 있지 않을까?
모델 학습에 사용하는 노트북이 충분한 연산 자원과 메모리, 저장 공간을 가지고 있다고 가정해보자. 머신러닝 모델이 학습 데이터를 메모리에 올려서 사용했다면 나중에 학습할 때 디스크에서 메모리로 올리는 작업을 반복하는 대신 데이터를 메모리에 유지시키는 방법이 있다. 다른 말로 하면 학습 데이터를 캐싱(caching)해놓는 것이다. 데이터가 디스크 대신 메모리에 유지된다면 해당 데이터에 다시 접근하는 것은 훨씬 빨라진다.
p.70~71
캐싱 패턴을 활용하면 모델 학습 과정에서 동일한 데이터셋을 반복적으로 가져오느라 낭비되는 시간을 크게 줄일 수 있다. 캐싱의 또 다른 장점은 머신러닝 파이프라인에서 예상치 못한 오류가 발생했을 때 캐시에 저장된 데이터를 재활용할 수도 있다는 점이다.
p.72
일반적으로, 보다 견고하고 안정성이 요구되는 시스템이 필요한 상황이라면 디스크 캐시를 사용하고, 속도나 시간 비용을 줄이는 것이 더 중요한 상황이라면 인메모리 캐시를 사용하는 편이 좋다. 특히 디스크 캐시는 원격 데이터베이스로부터 데이터를 가져와서 사용할 때 매우 유용하다. 네트워크 통신은 매우 느리고 때로는 불안정한 경우도 있기 때문이다.
p.87
머신러닝 엔지니어 혹은 DevOps 엔지니어는 파라미터 서버 패턴을 구성할 때 파라미터 서버와 워커를 각각 몇 대씩, 혹은 어떤 비율로 구성할지 결정하는 데 어려움을 겪을 수 있다. 워커가 계산한 그레이디언트뿐만 아니라 파라미터 서버에 있는 최신 모델 파티션을 주고받는 것까지 고려한다면 워커와 파라미터 서버 간 발생하는 통신 비용은 적지 않다. 모델이 더욱 커서 파라미터 서버의 수가 더 늘어난다면 각 워커에서 실행되는 연산에 비해 통신 비용이 막대해지는 경우가 생길 수도 있다.