Hidden Technical Debt in Machine Learning Systems 리뷰


Hidden Technical Debt in Machine Learning Systems 논문을 읽고 정리한 포스팅입니다. 머신러닝 시스템에서 발생할 수 있는 기술 부채에 대한 이야기와 실제 제품 도입시 아키텍쳐에 대해 나와있는 논문입니다. 의역이 있을 수 있으며 제가 잘못 이해한 것은 언제나 지적해주시면 감사하겠습니다!!!

1. 기술 부채(technical debt)

기술 부채라는 용어가 있습니다. 실행속도와 엔지니어링 품질 사이의 딜레마를 지칭하며 모든 부채가 나쁘진 않지만 부채는 복리가 되어 점점 쌓여 유지보수 비용의 상승, 깨지기 쉬운 시스템이 되어 혁신의 속도를 늦추는 역할을 합니다.
전통적인 방식으로 기술 부채를 관리하는 방법은 아래와 같습니다

  • 코드 리팩토링
  • 유닛 테스트 향상
  • 사용되지 않는 레거시(legacy) 코드 삭제
  • 의존성 감소
  • 엄격한 API 관리
  • 체계적으로 문서 작성

위와 같은 기술부채는 머신러닝 시스템에도 적용됩니다. 라이브 시스템을 적용하면서 머신러닝 커뮤니티들은 점점 다양한 경험을 축적했습니다. 그 결과 “머신러닝 시스템은 develop과 deploy는 상대적으로 빠르고 저렴하지만 시간이 지남에 따라 모델을 관리하는 것은 어렵고 비용이 많이 소모된다”라는 의견을 내놓았습니다. 또한 머신러닝 시스템의 부채는 대부분 코드 레벨보다 시스템 레벨에 존재합니다. 이 논문에선 머신러닝 알고리즘에 대해서가 아닌 이런 부채가 왜 생기고, 어떻게 해야 부채를 줄일 수 있는지에 대해 알려주고 있습니다.

사실 ML 코드는 정말 일부분에 지나지 않는다고 말해주고 있습니다-!!

2. 머신러닝 시스템 기술부채의 원인

전통적인 소프트웨어 공학에선 캡슐화(encapsulation)모듈 설계(Module Design)를 통해 강한 추상화 경계(Strong abstraction boundaries)를 만듭니다. 이 경계를 통해 독립된 환경을 만들어 유지보수가 가능한 코드를 만들어 복잡성이 큰 소프트웨어를 성공적으로 만들고 운영하고 있습니다.
강한 추상화 경계를 통해 해당 컴포넌트의 입력값과 출력값에 대해 논리적 일관성(logical consistency)불변성(invariants)을 구현했습니다. 하지만 기계학습 시스템에서 적용하긴 어렵습니다. 그 이유는 외부 데이터에 의존하지 않고 소프트웨어 로직만으로 구현이 불가능하기 때문입니다. 번역이 난해해서 원문을 작성하자면

There desired behavior cannot be effectively expressed in software logic wihtout dependency on external data

따라서 강한 추상화 경계가 외부 데이터의 유입으로 무너지기 때문에 기술 부채가 점점 쌓이게 됩니다

2.1 얽힘(Entanglement)

머신러닝 시스템은 신호들을 섞습니다. 이 뜻을 풀어보면, 다양한 기능(데이터 전처리, 학습, 예측, 시각화)이 서로 유기적으로 얽혀있어 머신러닝의 아웃풋을 생성합니다. 이 얽힘 때문에 시스템으로 격리를 통한 점진적 개선이 불가능합니다
CACE 원칙이 있습니다 : 어떤 것을 바꾸면 모든 것이 바뀐다(Changing Anything Changes Everything). 머신러닝 문제에 설명드리자면 특정 변수를 넣고 학습한 모델과 특정 변수를 제외하고 학습한 모델은 아예 다르기 때문에 시스템이 흔들릭 됩니다. 변수만 해당되는 것이 아닌 가중치나 하이퍼 파라미터를 변경시켜도 시스템에 영향력이 미치게 됩니다.

이러한 문제를 해결하기 위해 3가지 방식을 사용합니다

  • 앙상블 활용 : 문제를 하부 문제로 쪼갤수 있으며, 모듈 운영방식이 개별적으로 운영할 때보다 효과가 클 경우 유용
  • 고차원 시각화 도구를 사용 : 다차원 공간에서 효과를 슬라이싱으로 찾아내는 것
  • 더 정교한 정규화 방법론을 사용

2.2 지속적 보정(Correction cascade)

특정 문제 A에 대한 모델 m_a이 존재하고 다른 A` 문제가 있는 경우가 있습니다. 이 경우 약간 보정해 m'_a를 만드는 시도를 하곤 합니다. 하지만 이렇게 하면 m_a에 대한 의존성으로 향후 모형을 개선하거나 유지보수가 어렵게 됩니다.
이럴 경우 기존 모형에서 weight를 추출한 후, 추가로 풀려고 하는 문제에 변수를 추가해 모형을 개발하면 기술적 부채를 줄일 수 있습니다

2.3 미신고 고객(Undeclared Consumers)

머신러닝 시스템에서 나온 예측값이 실시간 혹은 로그파일에 기록되곤 합니다. 그 로그 파일을 인풋으로 받아 다른 시스템에서 사용될 수 있습니다(파이프라인에서 앞단의 output이 뒷단의 input이 되는 케이스) 이런 경우를 전통적 소프트웨어 공학에서는 가시성 부채(visibility debt)로 부릅니다. 시스템을 사용하는 고객 중 일부는 미신고 고객이 되서 조용하게 다른 시스템의 입력값으로 사용됩니다.
미신고된 고객은 시스템의 다른 모델에 숨겨진 타이트한 결합(hidden tight coupling of model)이 되어 최고로 비싸며 최악의 경우 위험합니다. 모델을 수정할 경우 결과에 대한 이해가 부족해지고 다른 부분에 영향을 미칩니다. 예를 들어, 클릭율(CTR) 시스템이 광고 시스템 중 글꼴 크기와 연결되어 있는 경우 숨은 피드백 루프를 따라 클릭율을 높이는 기계학습 시스템은 글꼴 크기를 커지게 합니다. 하지만 우리가 원하는 것은 글꼴 크기를 커지게 하는 것이 아닌, 광고의 전환이 목표입니다.

3. 코드 의존성보다 데이터 의존성이 비용이 더 크다

전통적 소프트웨어 공학에선 의존성 부채(dependency debt)가 코드 복잡성의 상단 부분을 차지하고 있습니다. 기계학습 시스템에선 데이터 의존성(data dependency)가 이 역할을 합니다. 코드 의존성은 정적 분석, 연결 그래프를 통해 상대적으로 쉽게 탐색되곤 합니다(=IDE를 통해 디버깅이 쉬운 편입니다) 그러나 데이터 의존성을 탐지하는 방법론은 최근에 이루어지고 있습니다

3.1 불안정한 데이터 의존성(Unstable Data Dependencies)

머신러닝 시스템에서 사용하는 다양한 Feature들은 여러 시스템을 통해 입력을 받습니다. 예를 들면 크롤링을 통해 Database에 넣고 그 Database를 모델이 알 수 있도록 전처리하는 한 후 Input으로 넣고, 어떤 데이터는 클라이언트 어플에서 나와서 특정 작업(맵리듀스같은)을 통해 Input으로 넣게 됩니다. 따라서 이 앞단의 시스템이 조금이라도 변화하면 전체 머신러닝 시스템에 영향을 미치게 됩니다. 코드를 작성할 때 버전 관리를 하듯 데이터 버전 관리하는 방식이 도입되고 있지만 데이터 버전 관리 자체가 비용이 되고, 관리하기 힘들기 때문에 기술부채로 되돌아 옵니다

3.2 활용도가 낮은 데이터 의존성(Underutilized Data Dependencies)

활용도가 낮은 데이터들은 필요없는 패키지들입니다. 활용도가 낮은 데이터들이 머신러닝 시스템에 몇가지 방식으로 잠입하게 됩니다

  • Legacy Feature : 기계학습 초기 시스템엔 예측 변수로 포함되었으나 시간이 지나면서 중요도가 떨어지고 필요없게 된 변수
  • Bundled Feature : 변수가 기술적/사업적 이유로 한꺼번에 포함된 경우(다른 변수과 함께 투입된 변수)
  • ϵ Feature : 모델의 정확도(accuracy)를 올리기 위해 매우 적은 기여를 하는 변수를 다수 포함
  • Correlated Features : 강한 상관관계를 가지는 변수들이 포함되는 경우입니다. 상관관계가 높은 변수들이 있다면 1개만 선택해서 넣는 것이 좋습니다

위와 같은 데이터들은 정기적으로 중요도가 낮은 변수를 찾아내 제거하고 관리해줘야 합니다

3.3 데이터 의존성 정적 분석(Static Analysis of Data Dependencies)

전통적인 코드에선 컴파일러와 빌드 시스템이 의존성에 대한 정적 분석을 형성해주곤 했습니다. 그러나 데이터 의존성에 대한 정적 분석에 대해선 도구나 경험이 상대적으로 부족한 상황입니다. 데이터 사전을 구축해 주기적으로 관리하는 방법 혹은 자동화된 Feature 관리 시스템을 사용하는 방법이 제시되고 있습니다.

4. 피드백 루프 (Feedback Loops)

라이브 머신러닝 시스템의 핵심기능 중 하나는 시간이 지남에 따라 그들의 행동이 모델의 결과에 영향을 미치는 것입니다. 이것을 분석 부채(Analysis debt) 라고 부릅니다. 행동에 따라 다른 현상을 보이기 때문에 릴리즈하기 전 모델의 상황을 예측하기 어렵습니다. 이런 피드백 루프는 다른 형태로 진행될 수 있으며 어떤 경우든 탐지하기 어렵습니다

4.1 직접적인 피드백 루프(Direct Feedback Loops)

모델이 미래에 사용될 교육 데이터 선택에 직접적으로 영향을 줍니다. 이것을 해결하기 위해 bandit algorithms(이건 어떤 정의인지 모르겠습니다)를 사용해야 하지만 일반적으로 Supervised 알고리즘을 사용합니다. 여기서 bandit 알고리즘은 일반적으로 필요한 동작 공간의 크기에 맞게 확장되지 않는 것이 문제입니다. 이것을 해결하기 위해 무작위화(randomization)을 사용해 주어진 모델에 의해 영향을 받지 않도록 데이터의 특정 부분을 분리합니다

4.2 숨겨진 피드백 루프(Hidden Feedback Loops)

직접적인 피드백 루프는 분석하는데 비용이 많이 듭니다. 그러나 직접적 피드백 루프는 머신러닝 연구자가 찾은 통계학적 접근에 대해 집중합니다. 반면 숨겨진 피드백 루프는 두개의 시스템이 서로 간접적으로 영향을 미치는 경우를 뜻합니다.
예를 들면 웹페이지에 표시할 제품을 선택하는 부분과 관련 리뷰를 선택하는 부분이 있습니다. 한 시스템을 개선하면 사용자가 변경 사항에 반응해 다른 요소를 더 많이 혹은 덜 클릭해 시스템의 동작이 변경될 수 있습니다.

5. 머신러닝 안티패턴

5.1 접착제 코드(Glue Code)

오픈소스 라이브러리를 사용할 경우 일반적으로 접착제 코드 시스템 디자인 패턴(Glue code system design pattern) 으로 귀결됩니다. 당장은 문제가 없지만 장기적으로 기술부채 문제를 야기할 수 있습니다. 우리가 라이브러리에서 사용하는 부분은 작은 부분인데, 그 부분이 변경된다면 우리의 코드도 수정해야 합니다. 또한 범용 머신러닝 시스템은 많은 문제를 해결하는데 초점을 맞춘 반면 실무에 적용된 시스템은 한 문제에 대해 확장성이 크게 개발되는 경향이 있기 때문에 딜레마가 생기곤 합니다.
이에대한 해법으로 R, Python으로 짜여진 코드를 C++, 자바, C로 재구현되는 것이 권장됩니다. 이렇게 하면 접착제 코드를 줄이며 테스트 및 유지보수도 쉽게 될 수 있습니다

5.2 파이프라인 정글(Pipeline Jungle)

접착제 코드의 특수한 경우에 파이프라인 정글이 데이터 전처리 과정에서 보이곤 합니다. 전처리 과정도 다양하게 존재합니다. 데이터를 긁어오는 과정(scraping), 병합(join), 샘플링(sampling), 그리고 중간 처리 과정에서 나타나는 파일들로 난잡하게 됩니다. 이렇게 파이프라인을 구축하면 오류를 탐지하기도 어렵고 장애 발생시 복구도 어렵습니다. 특히 End to End 테스트는 진행하기 어려울 수 있습니다

파이프라인 정글은 전체 아키텍쳐를 보면서 데이터 수집, Featrue Extraction, 전처리 과정을 천천히 나눠서 설계하면 피할 수 있습니다. 연구개발과 엔지니어링이 하나의 팀에서 접근하면 접착제 코드와 파이프라인 정글을 쉽게 해결할 수 있습니다

5.3 죽은 실험 경로(Dead Experimental Codepaths)

실제 운영준인 코드에 추가로 Branch를 생성해 다양한 실험을 통해 기존 모델의 성능을 높이는 노력을 많이 합니다. 하나의 브랜치는 괜찮지만 많은 사람들이 다양한 아이디어를 갖고 모형에 실험을 하게 된다면, 오랜 시간이 지나면 관리하기 힘든 기술부채를 만들게 됩니다.
정기적으로 사용하지 않거나 용도가 다 되었다고 판단되는 브랜치는 Dead Flag로 정의해 정리하는 작업이 반드시 필요합니다

5.4 추상화 부채(Abstraction Debt)

위의 이슈들은 모두 머신러닝 시스템을 지원할 강한 추상성이 부족하단 사실을 강조합니다. 데이터 스트리밍, 모델 또는 예측을 설명하는 인터페이스가 존재하는지 반문하면서 추상화가 더욱 필요하다고 말합니다.
특히 분산학습의 경우 맵리듀스가 널리 사용되는 이유는 강력한 분산학습 추상화가 없기 때문이라고 합니다. 맵리듀스는 반복적인 머신러닝 알고리즘의 빈약한 추상이라고 나와있습니다. 관련 논문을 몇개 제시했는데, 추가적으로 읽으면 이해에 더 도움이 될 것 같습니다

5.5 일반적인 상황(Common Smells)

전통적 소프트웨어 엔지니어링에선 컴포넌트와 시스템에서 겪을 수 있는 상황을 디자인합니다. 머신러닝 시스템에도 이런 상황을 정의했습니다

  • Plain-Old-Data Type Smell : 일반적으로 데이터는 float이나 integer형을 인코딩됩니다. 모델 매개변수는 로그를 취했는지, 임계값 기준인지를 알아야 하며, 예측은 이를 생성한 모델 및 사용 방법에 대한 정보를 알아야 합니다
  • Multiple-Language Smell : 주어진 언어로 시스템의 특정 부분을 사용하고 싶은 경우(다국어 지원) 해당 언어가 작업에 편한 라이브러리를 가질수도 있고, 아닐수도 있습니다. 하지만 진짜 중요한 것은 여러 언어를 사용하면 효과적인 테스트 비용이 증가하며 번역에 대한 오너십을 타인에게 주기 어려울 수 있습니다(개발자가 라이브러리를 통해 번역했다고 치면, 이에 대한 검증은 개발자가 해야되는 것인가? 다른 사람이 해야하는 것인가? 등..)
  • Prototype Smell : 프로로 타입을 통해 새로운 아이디어를 테스트하는 것은 편리합니다. 그러나 프로토 타입도 증가할수록 자체 비용이 소요되며 추후엔 프로토 타입을 생산 솔루션으로 사용할 수 있는 위험이 있습니다. 또한 프로토타입에서 발견된 결과는 실제 현실을 많이 반영하진 않습니다

6. 설정 부채(Configurations Debt)

머신러닝 시스템에선 점점 환경설정이 축적됩니다. 어떤 feature를 사용할지, 데이터를 얼마나 선택할지, 알고리즘의 learning rate를 얼마로 설정할지 등등 다양하게 고려할 것들이 많아집니다.
이런 설정이 복잡해지는 경우 컴포넌트들의 연산이 실수가 일어날 수 있습니다. 따라서 다음과 같은 규칙을 명확하게 하는 것이 좋습니다

  • 이전 환경설정에서 작은 변경은 쉬워야 합니다
  • 매뉴얼 error, omissions, oversights를 만드는 것은 어렵습니다
  • 두개의 환경 설정의 차이에 대해 쉽게 볼 수 있어야 합니다
  • 설정에 따른 기본적인 팩트를 검증하는 것이 쉬워야 합니다 : 사용된 Feature의 수, 데이터 종속성 등
  • 사용하지 않거나 중복된 설정을 감지할 수 있어야 합니다
  • 환경 설정은 레포지토리를 통해 전체 코드 검토를 통해 진행되야 합니다

7. 외부 세계의 변화를 다루기(Dealing with Changes in the External World)

7.1 고정된 임계치 설정(Fixed Thresholds in Dynamic Systems)

머신러닝 시스템에서 동작을 일으키도록 의사결정 임계값 설정이 필요합니다. 확률로 어떤 클래스 분류를 한다고 하면 0.7 이상이면 해당 클래스로 간주한다 라는 로직이 있스비다. 이 임계치는 수작업으로 설정되어 있으니 자동화하는 방식을 검토하는 것도 좋습니다

7.2 모니터링 및 테스트(Monitoring and Testing)

소프트웨어 시스템에선 단위테스트와 통합 테스트를 하는 것도 필요하지만 머신러닝 시스템은 예측력과 머신러닝 시스템이 취하는 동작에 대한 모니터링이 필요합니다. 그 전에 “무엇을 모니터링 할 것인가”에 대한 주요한 질문을 생각해주세요. 머신러닝 시스템에선 실험가능한 변수가 명백하게 주어지지 않을수도 있기 때문에 이런 질문에 대한 답변을 생각해야 합니다. 그 후 아래와 같은 것을 확인해주세요

  • Prediction Bias : 머신러닝 시스템이 의도한대로 움직이는지?
  • Action Limit : 원치않는 상황이 발생할 경우 경고를 알리거나 수동으로 전환이 쉬워야 합니다
  • Up-Stream Producers : 업스트림 프로세스를 모니터링할 수 있어야 합니다. 어떤 것이 실수했는지, 그래서 뒤의 작업이 진행 안되었는지 등을 이해해야 합니다. 파이썬을 사용한다면 Airflow를 적용하면 좋을 것 같습니다

8. ML과 관련 부채의 다른 영역

8.1 데이터 테스트 부채(Data Testing Debt)

input 데이터가 올바르게 들어가고 있는지 확인해야 합니다. 기본적으로 이치에 맞는 데이터인지 검증하는 것은 유용하나 input의 분포에 따라 모니터링의 난이도가 변화됩니다

8.2 재현성 부채(Reproducibility Debt)

다시 실험을 진행해서 유사한 결과가 나타나는 것은 과학자로서 매우 중요합니다. 이런 엄격하게 현실을 재현하기 위해 무작위 알고리즘(Randomized algorithms), 비결정론(non-determinism inherent), 초기조건 의존(reliance on intial conditions) 등을 사용하곤 합니다

8.3 프로세스 관리 부채(Process Management Debt)

유사한 모델에 대해 안전하고 자동으로 업데이트하는 문제나 비즈니스 우선 순위에 따라 모델간 리소스를 관리하고 할당(거기에 시각화까지), 파이프라인에서 데이터 흐름 감지 등 다양한 프로세스 관리에 대한 것도 알 수 있어야 합니다

8.4 문화적 부채(Cultural Debt)

가끔 머신러닝 연구자와 엔지니어간의 의견이 다를 수 있습니다. 팀의 문화에 따라 feature를 제거할지, 차원을 줄일지, 속도를 우선시할지에 대해 논의할 수 있습니다

9. 결론

기술 부채는 유용한 은유지만 엄격한 통계적 기준은 제공하지 않습니다. 시스템에서 기술적 부채를 측정하거나 부채의 비용을 계산하는 방법은 무엇일까요? 팀이 여전히 빠르게 움직일 수 있다는 답변은 시간이 지남에 따라 부채의 비용이 증가하는 것이 너무 분명하기 때문에 적절한 답은 아닙니다. 유용한 질문은 아래와 같습니다

  • 완전히 새로운 알고리즘 방식을 실제 규모에 얼마나 쉽게 테스트할 수 있나요?
  • 모든 데이터 종속성이 전이될 수 있나요?
  • 시스템에 대한 새로운 변화의 영향을 정확하게 측정할 수 있나요?
  • 한 모델이나 신호를 개선하면 다른 작업이 저하되나요?
  • 팀에 새로운 직원이 들어왔을 경우 얼마나 빨리 일을 시작할 수 있을까요?

후기

논문은 매번 읽고 있었는데 처음으로 논문을 읽고 블로그에 남기네요. 사실상 번역 느낌이 강하네요. 다음부터는 조금 더 요약하는 습관을 가져서 올려야겠습니다 :)

시간도 오래 소요되었기 때문에, 조금 더 효율적으로 논문 리뷰하는 방법을 아는 분들은 댓글주시면 정말 감사하겠습니다!!!!




© 2017. by Seongyun Byeon

Powered by zzsza