반응형

최근 AI 분야가 각광을 받고, 다양한 ML 기반 서비스들이 제공되면서, MLops라는 용어가 널리 알려지게 되었다. 과거에는 AI 연구 영역과 엔지니어링 영역이 너무 멀게만 느껴져서, MLops에 그렇게 주목하지 않았지만, 실제 머신러닝 코드들을 개발하고, 배포하다 보니, MLops가 왜 필요하고, 어떤 기능을 포함해야 하는지에 대해 공감하게 되었다. 오늘 리뷰할 논문은 MLops의 시작이라고 할 수 있는 2015년 Google에서 발표한  "Hidden Technical Debt in Machine Learning Systems"이라는 논문이다. 사실, 머신러닝이 본격적으로 서비스로 제공되기 한참 이전의 논문이라, 현재의 MLops의 모습과 많이 달라져있을 수 있지만, 어떻게 MLops가 논의되기 시작했는지에 대해서는 알아두고 가야 할 것 같아서 논문을 읽어보았다.

Abstract

  • 머신러닝은 복잡한 예측을 빠른 시간 안에 가능하게 하지만, 빠른 성과를 얻는 것이 무조건 좋은 것은 아니다.
  • 소프트웨어 엔지니어링 공학의 "기술부채" 개념에서 실생활에서 머신러닝 도입은 크고, 지속적인 유지보수 비용을 부담하게 될 것이다.
  • 이 논문에서는 시스템 디자인에서 머신러닝에서 발생할 수 있는 특정 리스크 요인(boundry erosion, entanglement, hiddden feedback loop, undeclared consumers, data dependency 등) 들을 살펴본다.

 

Introduction

  • 머신러닝에 대한 개념과 경험이 쌓여가면서, ML 개발과 배포는 빠르고 쉽지만, 유지보수가 어렵다는 인식이 번져가기 시작했다.
  • 이러한 유지보수의 어려움에는 코드 리팩토링, unit test 개발, 코드 정리, 의존성 제거, 문서 정리 등 기술부채를 해결을 위한 비용을 포함한다. 
  • 이 논문에서는 ML에 특화된 기술부채들에 대해서 다룬다.
  • ML에서의 부채들은 코드 레벨이 아닌, 시스템 레벨에 포함되어 찾기 어려울 수 있다. 특히, 데이터가 머신러닝 시스템의 동작에 영향을 미치기 때문에, 전통적인 코드 레벨에서의 해결 방법은 ML 특화적 기술부채들을 다루기에 충분치 않다.
  • 이 논문에서는 ML 기술적 부채가 쉽게 커지는, 시스템 레벨에서의 interaction과 interface에 집중한다. 

 

Complex Models Erode Boundaries

  • 전통적 소프트웨어 엔지니어링에서는 캡슐화와 모듈화 디자인을 통해, 추상화 경계를 명확하게 나누어, 다른 모듈이나 캡슐의 변화에 영향을 받지 않도록 설계한다.
  • 이러한 분리 속에서는 각 모듈이 input과 output이 명확하기 때문에, 기능적 일관성을 유지할 수 있었다. (예를 들어, Interface 모듈은 Interface 기능만 담당함)
  • 하지만, ML에서는 목적 단위로 명확한 추상화 경계를 나누는 것이 어렵다. 실제로 ML의 서비스에서 외부 데이터에 의존하지 않는 깔끔한 캡슐화는 어렵다. 

[Entanglement]

  • 머신러닝은 여러 signal들을 혼합하여 활용하는데, 각 signal들의 효과를 분리하면서 성능 향상을 하는 것은 어렵다. 
  • 예를 들어, N개의 feature가 있을 때, 한 개의 feature가 변화하게 되면, importance나 weights, 다른 feature들의 필요성등 모든 것들이 변화한다. 이런 경우는 Batch 단위의 학습이든, 온라인 학습이든 모두 적용된다.
  • 새로운 feature를 추가하는 것이나, 특정 feature를 지우는 것도 비슷하다. 
  • 즉, 모든 Input들이 독립적이지 않다는 것이고, 이 논문에서는 이 현상을 CACE(Changing Anything Changes Everything pipeline으로 명명한다.
  • CACE는 input 뿐 아니라, hyper-parameter에도 적용된다. 
  • 이러한 현상을 완화하기 위한 방법으로는 model을 분리하고, 앙상블 형태로 serving 하는 것이다.
  • 이러한 접근 방법은 전체 문제를 sub problem으로 쪼갤 수 있는 경우에는 효과적일 수 있다. 하지만, 많은 경우에서 앙상블이 효과적인 이유는 각 모델에 속한 오류가 상관관계가 없기 때문인데, 결합에 의존하게 되면 오히려 각 모델의 성능을 향상하는 것이, 전체 앙상블 모델 성능을 떨어뜨릴 수 있다.
  • 두 번째 전략으로는 prediction behavior의 변화를 감지하는 것에 초점을 맞추는 것이다. 시각화 도구등을 활용하거나, 각 단계 별로 작동하는 metrics 등이 효과적일 수 있다.

 
[Correction Cascades]

  • 문제 A에 대한 model이 존재할 때, 살짝 다른 문제 A'를 풀기 위해, 두 문제 간의 차이를 빨리 학습하는 것이 필요하다. (예를 들어, pre-weight에 대한 Fine tuning)
  • 하지만, 이런 문제가 계속 생긴다고 하였을 때, 시스템 종속성이 계속 연결되어 발생하고, 큰 비용이 발생한다. 
  • 완화전략으로는 동일 모델에서 casecade가 아닌, 직접 correction을 학습하던지, 별도의 모델을 만드는 것이다.

 
[Undeclared Consumers]

  • 머신러닝 시스템의 예측은 runtime에 광범위하게 접근 가능하게 되거나, 다른 시스템에 소비될 파일이나 로그 형태로 생성된다.
  • access control이 없으면, 이러한 결과는 다른 시스템의 입력으로 충분한 공유 없이 사용할 수 있다. 이러한 문제를 전통 소프트웨어 엔지니어링에서는 visibility debt라고 한다. (의존성의 부채)
  • 이러한 문제는 구성 요소간의 상화 작용과 의존성을 파악하지 못해서, 예측 불가능한 문제를 발생시킬 수 있다.
  • Undeclared Consumer를 고려하기 위해서, 방화벽이나 access 제한 등의 접근을 제한할 수 있는 방법을 검토해야 한다.

 

Data Dependencies Cost More than Code Dependencies

  • 전통 소프트웨어에서는 의존성에 대한 cost가 기술 부채의 주요 요인으로 꼽힌다. 
  • ML에서는 data dependency가 주요 기술 부채 요인이 되지만, 오히려 감지는 더 어렵다.
  • Code 의존성등은 컴파일러 등을 통한 정적 분석으로 어느 정도 확인이 가능하다. data dependency도 비슷한 tool이 없다면, 데이터 의존성을 풀기 어렵다. 

[Unstable Data Dependecies]

  • 빠른 학습을 위해, 다른 시스템에서 생성된 Feature 등을 활용하는 것이 일반적이다. (pre-weight 등) 
  • 하지만, 몇몇의 input signal들은 질적 또는 양적으로 행동이 변화하는 불안정한 특성을 가질 수 있다. 
  • input signal을 제공하는 머신러닝 시스템 자체가 학습된 경우나, TF/IDF 같은 데이터 종속적 데이터들은 데이터 변화에 따라 변화한다.
  • 특히, 데이터와 모델의 ownership이 분리된 경우 많이 발생한다. 이 경우 input signal의 개선조차도, 사용하는 시스템에서는 업데이트에 대한 비용이 많이 발생한다. 
  • 완화를 위한 방법으로는 주어진 signal에 대한 버전 관리(DVC 같은)이다. 예를 들어, 시간에 따라 변화하는 input을 사용하는 것이 아니라, input을 특정 버전으로 freeze 하여 사용한다. 
  • 이러한 버전관리는 그 자체의 cost를 발생하지만, 아무도 모르게 모델 성능에 영향을 미치는 일은 일어나지 않을 것이다.

→ 이 부분은 Database에서의 Data Dependency와 비슷하다. 일반적으로 ML은 앞선 매우 많은 종류의 원천 데이터들을 ETL 하여 활용하기 때문에, Data Dependency에 대한 cost를 계속 안고 가야 한다. 사실 이 부분이 ML serving을 쉽게 하는 tool이 많이 만들어져도, ML engineer의 비중이 점점 더 커지고 있는 요인 아닐까 싶다. 
 
 
[Underutilized Data Dependencies]

  • Code에서의 불필요한 의존성은 보통 필요하지 않은 패키지들로 발생한다.
  • 비슷하게, underutilized data dependencies는 model 성능에 더 이상 이점을 제공하지 못하는 data들을 의미한다. 
  • 이러한 요소들은 ML 시스템을 불필요하게 변화에 취약하게 할 수 있다. 
    • Legacy Features: 시간에 변화에 따라 Feature 특징이 바뀌었는데, 필요 없지만 그것을 감지하지 못하는 경우
    • Bundled Features: bundle 형태로 제공되는 Feature 들 중, 필요성이 떨어지거나, 없는 Feature들이 포함된 경우
    • Epsilon-Features: 연구자들이 성능 향상을 위해, 성능에 미치는 영향이 미미하지만, 복잡한 데이터들을 포함해 놓은 경우
    • Correlated Features: 종종 두 개의 Feature들이 큰 상관관계를 가지고, 하나의 Feature가 다른 것의 원인인 경우. 많은 ML 방법들은 두 Feature가 의미적으로 동일하다는 것을 감지하지 못하거나, 원인이 아닌 결과를 선택하기도 한다. 
  • 적게 활용되는 종속성은 철저한 leave-one-feature-out 평가를 통해 감지될 수 있다. 이러한 평가를 정기적으로 실행하여, 불필요한 특징을 식별하고 제거하는 데 사용되어야 한다. 

→ 과거, 8개의 view를 활용하여 Object Detection을 진행하는 과제를 진행한 적이 있다. 중간에 2개 view를 더이상 evaluation 단에서 활용할 수 없는 경우가 발생하였고, 이에 따라, 데이터를 필터링하고 재학습하는 과정을 거쳤다. 당시 단일 모델을 많은 researcher들이 다루고 있어, 금방 해결했지만, 대규모 시스템에서는 데이터를 제외하고 재학습해야 하는 매우 큰 cost가 발생할 수 있을 것이다.
 
 
[Static Analysis of Data Dependencies]

  • 전통적인(머신러닝이 아닌) 코드 들은 컴파일러와 빌드 시스템이 종속성에 대한 정적 분석을 실행해 준다.
  • Data dependencies들에 대한 정적 분석 툴은 흔하지는 않지만, error checking이나, 마이그레이션, 업데이트 등에 중요한 요소이다. 
  • Ad Click Prediction 논문에서 소개한 자동 기능 관리 시스템은 이러한 작업을 가능하게 해준다. 자동 체크는 데이터 소스와 기능에 annotation을 달게 하여, 이를 자동으로 체크해 준다. 

→ 데이터 의존성을 정적 분석하기에는 모든 데이터를 cover하기 어려울 것이다. 그나마, 중간 단 feature의 visualization이나 통계값들을을 쉽게 뽑을 수 있는 tool을 활용하는 것이 최선이 아닐까 싶다.
 

Feedback Loop

  • ML 시스템의 특징 중 하나는 시간에 따라 업데이트가 진행되면서, 모델 자체 행동에 영향을 미칠 수 있다는 것이다. 이로 인해, 특정 모델의 행동을 예측하기 어려워지는 "anaylisis debt"가 발생한다.
  • 이러한 Feedback loop는 다양한 형태로 발생하지만, 시간에 따라 점진적으로 발생할 경우, 감지와 해결이 더 어렵다.

[Direct Feedback Loops]

  • 모델이 직접적으로 미래 training data 선택에 영향을 미치는 현상이다. 
  • 이는 bandit 알고리즘을 사용하여 해결가능하다.(무작위성 등을 이용하여, 공간을 줄임)

[Hidden Feedback Loops]

  • Direct feedback loops는 분석에 cost가 많이 들지만, ML 연구자들이 통계적으로 풀어낼 수 있을 것이다.
  • 더 어려운 케이스는 두 시스템이 서로에게 영향을 미치는 hidden feedback loops이다. 
  • 예를 들어, 두 다른 투자 회사에서 나온 주식 시장 예측 모델의 경우, 하나의 모델을 개선하면, 다른 모델의 입찰과 매수에 영향을 미칠 수 있다.

 

ML-System Anti-Patterns

  • 실제 ML 코드를 보면, 학습과 분석에 해당하는 코드는 전체 코드의 아주 일부분 만을 차지한다. 이러한 ML을 포함한 시스템에서 high-debt 디자인 패턴을 가지는 것이 흔한 현상이다. 
  • ML 시스템에서 발생할 수 있는 시스템 디자인 안티패턴들은 다음과 같다. 

[Glue Code]

  • 일반적으로 ML 코드 개발을 위해, 미리 만들어진 package를 사용하는 경우가 많다. 
  • 이러한 package들은 다양한 범위를 커버하기 위해 만들어졌기 때문에, 실제 package의 일부만 사용하더라도 대량의 코드를 import 하는 glue code가 발생한다. 
  • Glue code는 특정 버전의 패키지로 시스템을 고정하여, 개선을 어렵게 하는 등 장기적으로 비용이 많이 발생한다.
  • 이를 해결하기 위해, 블랙박스 패키지를 공통 API로 wrapping 하는 방법들이 있다. 이렇게 하면, 재사용성 향상과, package 변경에 따른 비용으로 절감할 수 있다.

 
[Pipeline Jungles]

  • glue code의 특별한 케이스로 발생하는 pipeline jungles는 data를 전처리할 때 자주 발생한다.
  • 이러한 경향은 새로운 signal과 source가 점진적으로 추가됨에 따라 발생하는데, 주의를 기울이지 않으면 ML 전처리 단에서 스크랩, 조인, 샘플링 단계등이 남발하는 정글이 발생할 수 있다.
  • 이런 pipleline을 관리하고, 에러를 발견하여 해결하는 것은 어렵고, 많은 비용이 든다. 예를 들어, pipeline 테스트를 위해서는 비용이 많이 드는 end-to-end 통합 테스트가 필요한데, 이런 테스트를 관리하는 것들이 기술적 부채를 발생시킨다.
  • Pipeline jungle은 데이터 수집 및 Feature 추출에 대해 종합적으로 생각해야만 피할 수 있다. 
  • Pipeline jungle을 제거하고, 처음부터 다시 디자인하는 것들은 공수가 많이 발생하지만, 비용 감소와 발전의 속도 측면에서 효과적일 수 있다. 
  • Glue Code와 Pipeline Jungle은 Research 영역과 Engineering 역할을 지나치게 분리되어 있어 발생하는 현상이기도 하다. 
  • Engineer와 Researcher를 동일 팀에서 편입시키는(혹은 동일한 사람이) 하이브리드 연구 방식은 이런 갈등의 근본 원인을 제거할 수 있다. 

 
[Dead Experimental Codepaths]

  • Glue code나 Pipeline jungle의 흔한 결과 중 하나는, 대체 방법 실험을 진행하는 것이다. 빠른 실험을 위해,  Production code 내에 조건부 브랜치를 만들어, 실험비용을 줄이기도 한다.
  • 하지만, 시간이 갈수록, 이런 codepath들은 쌓이게 되고, 역호환성을 유지하기 어려워지며, 순환 복잡성이 매우 증가하게 된다. 
  • 전통적 소프트웨어에서의 dead flag와 마찬가지로, 주기적으로 각 실험용 브랜치를 재조사하여, 제거하는 것이 해결 방법이 될 수 있다.

 
[Abstraction Debt]

  • 최근에는 머신러닝의 추상화에 대한 올바른 Reference가 없음에 대한 문제가 지적되고 있다.
  • 표준 추상화의 부재로 구성요소 간의 경계가 흐릿해지기 쉽게 된다.

[Common Smells]

  • Plain-Old Data Type Smell : ML 시스템에서 사용되는 많은 정보(hyper parameter 등)가 코드에 그대로 들어가 있는 경우가 있다. 
  • Multiple-Language Smell : 특정 부분을 특정 언어로 작성하는 것이 해당 작업에 적합한 경우가 있다. 하지만, 여러 언어를 사용하면, 효과적인 테스트 비용이 증가하게 된다.
  • Prototype Smell : 적은 양의 데이터로 프로토타입을 테스트하는 것이 유용하지만, 유지보수 측면에서는 별도의 cost가 발생한다는 점, 시스템의 복잡성이 증가한다는 점 등의 문제가 발생할 수 있다.

 
 

Cofiguration Debt

  • ML에서는 configuration으로 인한 부채가 발생할 수 있다. 
  • 대규모 시스템은 다양한 범위의 configuration 옵션 등을 포함하는데, researcher와 engineer 모두 이런 configuration을 을 고려하고 개발하지 않는다. 
  • 실제로, configuration을 확인이나 테스트도 중요하게 여겨지지 않는다. 
  • ML 시스템에서는 configuration을 위한 ocde line이 traditional code의 line 수를 훨씬 초과할 수 있다. 이런 configuration은 실수를 발생할 수 있는 여지가 있다.
  • 이러 configuration에서의 실수들은 시간, 컴퓨팅 리소스 등의 낭비나 production 레벨에서의 문제로 이어질 수 있다. 
  • 이를 고려한 좋은 configutaiton 시스템의 원칙은 다음과 같다.
    • 이전 configuration에서 작은 변경을 하는 것이 쉬워야 한다.
    • 수동 오류, 누락 또는 간과를 어렵게 해야 한다.
    • 두 모델 간의 configuration의 차이를 시각적으로 쉽게 파악할 수 있어야 한다.
    • configuration에 대해 자동 검증이 쉬워야 한다.
    • 사용하지 않거나, 중복된 설정을 감지하는 것이 가능해야 한다.
    • configuration을 전체 코드 검토를 거치고, 저장소에 올라가야 한다. 

 

Dealing with Changes in the External World

  • ML의 특징 중하나는, External World와 직접 상호 작용한다는 것이다. 하지만, External World는 일반적으로 안정되어 있지 않다. 계속된 변화들은 추가적인 유지보수 비용을 발생시킨다.

[Fixed Thresholds in Dynamic Systems]

  • 모델의 prediction을 위해, decision threshold를 선택하는 경우가 있다. ML에서 전형적인 접근 방법으로는 precision과 recall 등의 trade-off인 지표 들 사이에서 균형점을 찾는 것이다. 
  • 하지만, 이러한 Threshold는 종종 manual 하게 지정되기도 한다.
  • 이런 경우, 모델이 새로운 데이터로 업데이트되면, 예전에 manual로 지정한 threshold는 무효화된다. 
  • 여러 모델에 걸쳐 많은 threshold는 manual로 업데이트하는 것은 매우 번거롭고, 관리가 어렵다.
  • 이를 완화할 수 있는 방법으로는 threshold를 validation data에서 평가를 통해 뽑는 것이다.

[Monitoring and Testing]

  • 각 요소들의 unit test와 end-to-end test 모두 가치 있지만, 변화하는 환경에 대응하기 에는 충분치 않다.
  • 실시간으로 시스템의 동작을 모니터링하고, 이를 이용한 대응을 하는 것이 장기적 시스템 신뢰성 관점에서 중요하다.
  • 이때, 어떤 것을 모니터링해야 하는지에 대해 명확하지 않다. 모니터링을 위한 몇 가지 부분을 소개한다.
    • Prediction Bias : 의도된 대로 작동하는 시스템에서는 예측의 분포가 실제 분포와 일치해야 한다. 이는 종합적인 테스트는 아니지만, 입력과 관계없이 레이블 발생의 평균값을 예측하는 단순한 모델에 의해 확인될 수 있다. 이 방법은 종종 현재 현실을 반영하지 않는 과거 데이터로부터 추출된 훈련 분포가 갑자기 변경된 경우와 같은 문제를 신속하게 감지할 수 있다.
    • Action Limits : 실제 예측에 대한 Action을 진행하는 시스템들의 경우, action에 대해 합리적인 limit을 설정하고, 이를 강제하는 것이 유용하다. 
    • Up-Stream Producers : 데이터를 생성하는 Producer에 대해 정기적인 모니터링이 필요하다. 

 

그 밖에 ML 관련 부채

  • Data Testing Debt : 입력 데이터에 대한 어느 정도의 테스트가 필수 적이다. 입력 분포의 변화 등을 모니터링하는 테스트도 필요하다.
  • Reproducibility Debt: 실험을 재현하는 것이 난수 알고리즘, 병렬 학습에서의 결정 불가능 성, 초기 조건 등 매우 어렵다.
  • Process Management Debt : 실제 시스템에서는 단일 모델이 아닌 많은 모델을 유지보수 할 텐데, 비즈니스 우선순위가 다른 모델 간에 리소스를 어떻게 관리하고 할당할 것인지 등에 대한 고려도 필요하다. 또한, 프로더션 사고로부터 복구하기 위한 도구를 개발하는 것도 중요하다.
  • Cultural Debt: 때로 ML reseach와 Engineering 단에 경계가 존재하는데, 이런 경계는 시스템 건강에 역효과를 낼 수 있다. 

 

총평

  • 사실 MLops를 처음 접하고, 사용해보았을때에는 기술적 부채와 소프트웨어 공학적 이해가 부족하여, Bash Shell로 가능한 기능들을 이쁘게 만들어놓았구나라는 생각에 그쳤다. 
  • 이 논문은 실제 ML이 본격적으로 service 단에 오르기 전에 작성되었지만, 이 논문에서 말하는 MLops가 필요한 이유와 발생할 수 있는 문제들이 실제 service 단에서 많이 발생하고 있는 것들을 보면서 매우 신기하고, 대단하게 느껴졌다. 
  • 현재 존재하는 MLops 관련 tool들이 이 논문에서 제기한 문제들 중 매우 일부만 해결할 수 있다는 점 때문에 아직 MLops가 가야 할 길이 멀구나 하는 생각을 갖게 되었다. 

 

Reference

Sculley, David, et al. "Hidden technical debt in machine learning systems." Advances in neural information processing systems 28 (2015).

'MLops' 카테고리의 다른 글

Kubeflow (2) - Katlib  (1) 2023.04.11
Kubeflow (1) - 설치 (Windows 11 - WSL로 설치)  (3) 2023.03.28
Kubeflow (0) - 소개  (1) 2023.03.26
반응형

Scikit-learn 이란?

  • Python 기반의 머신러닝 라이브러리로, 머신러닝 관련 다양한 알고리즘과 함수들을 포함하고 있어, 머신러닝 프로젝트에서는 필수 라이브러리이다.

설치 방법

pip install scikit-learn

 

 

Scikit-learn 주요 함수 

  • 사실 Scikit-learn은 계속 새로운 버전이 등장하는 라이브러리이기 때문에, 라이브러리 내의 모든 함수를 보기 위해서는 공식 홈페이지에 방문하는 것이 좋다.
  • 아래 정리된 내용은 지금껏 Scikit-learn을 사용해 오면서, 순전히 주관적인 기준으로 Scikit-learn의 주요 함수를 정리한 것이다.
  • Scikit-learn은 크게 분류하면, Classification, Regression , Clustering 등의 특정 알고리즘을 구현해 놓은 알고리즘 함수와 전처리, 특성추출, 평가등을 통해, 머신러닝 데이터를 쉽게 처리할 수 있게 하는 비알고리즘 함수로 나눌 수 있다. 

 

[머신러닝 데이터처리 함수]

분류 함수명 설명
preprocessing StandardScaler z-score normalization
MinMaxScaler min-max normalization
OrdinalEncoder 범주형 변수 숫자로 encoding
model_selection train_test_split trainset, testset split
KFold K-fold 교차 검증
cross_validate 교차 검증
GridSearchCV 하이퍼파라미터 튜닝(Grid Search로)
RandomizedSearchCV 하이퍼파라미터 튜닝(Random Search로)
metrics accuracy_score accuracy 계산
top_k_accuracy_score top-k accuracy 계산
auc auc 계산
roc_curve roc curve(fpr, tpr)
roc_auc_score roc curve에서 auc 계산
confusion_matrix confusion matrix
recall_score reacall 계산
classification_report 분류 모델 성능 요약 report 생성
mean_squared_error MSE 계산
r2_score 결정계수 계산

 

 

<preprocessing>

preprocessing은 숫자형 데이터를 normalize하거나, encoding 하는 데 사용된다. 

  • StandardScaler : 평균 0, 표준편차 1로 값을 normalize한다. 
  • MinMaxScaler : min-max normalization을 진행한다. (0~1사이의 범위를 가짐)

→ fit을 이용하여, scaler를 조정(최대,최소 나 평균 표준편차)하고, transform을 통해, 데이터를 변환한다. fit_transform을 사용하면, 해당 데이터셋 내에서 scale을 조정할 수 있다. 

from sklearn import preprocessing
import numpy as np

if __name__ == '__main__':
    # x = [1,2,3,...,9]
    x = np.arange(10).reshape(10,1)
    
    # z-score normalize
    std_normal = preprocessing.StandardScaler()
    normalized_x =  std_normal.fit_transform(x)
    
    
    # min-max normalize
    minmax_normal = preprocessing.MinMaxScaler()
    normalized_x = minmax_normal.fit_transform(x)
  • OrdinalEncoder : 범주형 데이터들에 숫자를 mapping 해줄 때 사용한다.

→ Scaler와 마찬가지로, fit을 이용하여, 문자를 숫자로 만드는 dictionary 구조를 만들 수 있고, transform을 이용하여 적용할 수 있다. fit_transform으로 dictionary 생성 및 변환이 가능하고, inverse_transform을 통해, 거꾸로 숫자에서 문자로 바꾸는 변환도 가능하다.

from sklearn import preprocessing
import numpy as np

if __name__ == '__main__':
    category = np.array(["사과", "딸기", "배", "두리안"])
    category = np.expand_dims(category,axis=-1)
    encoder.fit_transform(category)
    
    # mapping 정보 확인 (해당 list의 index에 번호 대응)
    print(encoder.categories_)

 

 

<model_selection>

model_selection은 데이터를 나누거나, 교차검증, 하이퍼 파라미터등을 통해, model 실험에 도움이 되는 함수들이 존재한다.

 

  • train_test_split : data를 train과 test로 분할해준다. 비율을 지정할 수 있는 등, 굉장히 유용하여, 많이 사용한다.

→ array를 넣어주고, train size나 test size 중 1개를 설정하여(비율), 정한다. random_state를 명시적으로 지정하면, 복원 가능하다. 

from sklearn import datasets
from sklearn import model_selection

if __name__ == '__main__':
	# iris data load
	iris_data = datasets.load_iris()
    
    X = iris_data["data"]
	Y = iris_data["target"]
    
    # train과 test를 8:2로 분리
    x_train, x_test, y_train, y_test = model_selection.train_test_split(X,Y, train_size = 0.8)
    
    print(x_train.size) #480
    print(x_test.size)  #120

 

  • KFold : K-fold 교차 검증을 위한, 객체 생성에 사용된다. 

→ n_split를 통해, K값을 정해주고, shuffle을 통해, 각 반복에서 데이터를 섞을지 여부를 선택가능하다. 

from sklearn.datasets import load_iris
from sklearn.model_selection import KFold
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import numpy as np


if __name__ == '__main__':
    iris = load_iris()
    X, Y = iris.data, iris.target

    # K-Fold 교차 검증
    kfold = KFold(n_splits=5, shuffle=True, random_state=0)

    model = LogisticRegression()

    fold_accuracies = []

    for train_index, test_index in kfold.split(X):
        x_train, x_test = X[train_index], X[test_index]
        y_train, y_test = Y[train_index], Y[test_index]

        model.fit(x_train, y_train)

        y_pred = model.predict(x_test)

        accuracy = accuracy_score(y_test, y_pred)
        fold_accuracies.append(accuracy)
        
    for i, accuracy in enumerate(fold_accuracies, 1):
        print(f"Fold {i} Accuracy: {accuracy}")

    mean_accuracy = np.mean(fold_accuracies)
    print(f"\nMean Cross-Validation Accuracy: {mean_accuracy}")
  • GridSearchCV : GridSearch로 최적의 하이퍼파라미터를 찾는다. 
  • RandomizedSearchCV : Random Search로 최적의 하이퍼파라미터를 찾는다. GridSearch보다 빠르다. 

→ 최적화하려는 모델을 인자로 넣어준다. pram_grid 인자에 탐색할 하이퍼파라미터의 리스트들을 딕셔너리 형태로 넣어준다. scoring 인자를 통해, 모델의 성능 평가를 위한 지표 설정이 가능하다. cv를 통해, Fold 수를 지정 가능하다.

from sklearn.datasets import load_iris
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV, train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from scipy.stats import uniform, randint

if __name__ == '__main__':
    iris = load_iris()
    X, y = iris.data, iris.target

    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)

    model = LogisticRegression()

    param_grid = {
        'C': [0.001, 0.01, 0.1, 1, 10, 100],
        'penalty': ['l1', 'l2']
    }

    param_dist = {
        'C': uniform(0.001, 100),
        'penalty': ['l1', 'l2']
    }

    grid_search = GridSearchCV(model, param_grid, cv=5, scoring='accuracy')
    grid_search.fit(X_train, y_train)

    print("GridSearchCV - Best Parameters:", grid_search.best_params_)
    print("GridSearchCV - Best Accuracy:", grid_search.best_score_)

    random_search = RandomizedSearchCV(model, param_dist, n_iter=10, cv=5, scoring='accuracy', random_state=0)
    random_search.fit(X_train, y_train)

    print("\nRandomizedSearchCV - Best Parameters:", random_search.best_params_)
    print("RandomizedSearchCV - Best Accuracy:", random_search.best_score_)

 

 

<metrics> 

metrics는 모델의 성능을 쉽게 측정할 수 있도록 한다. torch 등의 딥러닝에서 얻은 데이터도 numpy나 list로 변환하여, 사이킷런 의 metrics를 이용하면, 매우 쉽게 성능을 구할 수 있다.

  • accuracy_score : accuracy 값을 구할 수 있다.
  • top_k_accuracy_score : top-k accuracy(상위 k개 중 정답 존재하는지) score를 구할 수 있다.
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, top_k_accuracy_score, confusion_matrix, recall_score, r2_score, classification_report

if __name__ == '__main__':
    iris = load_iris()
    X,y = iris["data"], iris["target"]
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

    model = DecisionTreeClassifier()
    model.fit(X_train, y_train)

    y_pred = model.predict(X_test)

    accuracy = accuracy_score(y_test, y_pred)
    print(f"Accuracy Score: {accuracy}")
    # Accuracy Score: 1.0
    
    top_k_acc = top_k_accuracy_score(y_test, model.predict_proba(X_test), k=2)
    print(f"Top-k Accuracy Score: {top_k_acc}")
    # Top-k Accuracy Score: 1.0
  • roc_curve : 분류 모델의 roc curve의 x축과 y축 (각각 fpr, tpr) 값을 구할 수 있다.
  • auc : AUC(area under the curve)를 쉽게 구할 수 있다. auc를 사용하기 전에는 roc_curve함수를 먼저 사용하여, fpr과 tpr을 구해야 한다. 
  • roc_auc_score : auc 값을 roc_curve 선행 없이 구할 수 있다.
  • confusion_matrix : confusion matrix를 쉽게 구할 수 있다.
  • recall_score : recall 값을 구할 수 있다.
  • classification_report : recall, precision , f1 score등의 결과를 쉽게 구할 수 있다. 
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import auc, roc_curve, roc_auc_score, confusion_matrix, recall_score, classification_report
import numpy as np

if __name__ == '__main__':
    iris = load_iris()
    X,y = iris["data"], iris["target"]
    
    y = np.array([1 if i==0 else 0 for i in y])

    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

    model = DecisionTreeClassifier()
    model.fit(X_train, y_train)

    y_pred = model.predict(X_test)
    y_prob = model.predict_proba(X_test)[:, 1]

    fpr, tpr, thresholds = roc_curve(y_test, y_prob)
    area_under_curve = auc(fpr, tpr)
    print(f"AUC Score: {area_under_curve}")
    # AUC Score: 1.0

    plt.plot(fpr, tpr, label='ROC Curve')
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title('Receiver Operating Characteristic (ROC) Curve')
    plt.legend()
    plt.show()

    roc_auc = roc_auc_score(y_test, y_prob)
    print(f"ROC AUC Score: {roc_auc}")
    # ROC AUC Score: 1.0

    conf_matrix = confusion_matrix(y_test, y_pred)
    print(f"Confusion Matrix:\n{conf_matrix}")
    # Confusion Matrix:
    # [[20  0]
    # [ 0 10]]

    recall = recall_score(y_test, y_pred)
    print(f"Recall Score: {recall}")
    # Recall Score: 1.0

    class_report = classification_report(y_test, y_pred)
    print(f"Classification Report:\n{class_report}")
    # Classification Report:
    #               precision    recall  f1-score   support
    # 
    #            0       1.00      1.00      1.00        20
    #            1       1.00      1.00      1.00        10
    # 
    #     accuracy                           1.00        30
    #    macro avg       1.00      1.00      1.00        30
    # weighted avg       1.00      1.00      1.00        30

 

  • mean_squared_error : 평균 제곱 오차(Mean Squared Error, MSE)를 구할 수 있다.
  • r2_score : 회귀 모델의 결정 계수를 구할 수 있다.
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score, mean_squared_error
import numpy as np

if __name__ == '__main__':
    iris = load_iris()
    X,y = iris["data"][:,0].reshape(-1,1), iris["data"][:,-1]

    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

    model = LinearRegression()
    model.fit(X_train, y_train)
    
    y_pred = model.predict(X_test)

    mse = mean_squared_error(y_test, y_pred)
    print(f"Mean Sqared Error:{mse}")
    # Mean Sqared Error:0.1541458433507937
    
    r2 = r2_score(y_test, y_pred) 
    print(f"R2 Score:{r2}")
    # R2 Score:0.7575009893273535

 

반응형

GPU 없는 환경에서 Image Classification을 해야 할 상황이 되었다. MobileNet으로 실험할까 하다, 성능도 어느 정도 챙기고 싶어, MobileViT 논문을 읽게 되었다. 

MobileViT v2 배경 설명

  • MobileVIT v2는 2022년 Apple에서 발표한 "Separable Self-attention for Mobile Vision Transformers" 논문에서 소개된 모델이다.
  • 기존 MobieViT가 model의 parameter 경량화에 성공하면서, CNN 기반의 mobilenet보다 좋은 성능을 보였지만, inference 속도가 느리다는 점을, separable self-attention 개념으로 해결한 논문이다.

 

Abstract

  • MobileVit가 적은 parameter로 좋은 성능을 보였지만, CNN 기반의 모델에 비해 수행시간이 오래 걸리는 문제가 있다.
  • 수행 시간 감소에 가장 큰 bottleneck은 MobileViT의 multi-headed self-attention(MHA)이다. 이 연산은 patch의 개수 k에 대해 O(k^2)의 연산 복잡도를 갖기 때문이다. 또한, MHA는 batch 단위의 matrix multiplication 같은 cost가 큰 연산을 포함하여, 수행시간이 길어지게 된다.
  • 이 논문에서는 선형복잡도를 가지는 "separable self-attention"을 제안한다. separable self-attention을 위해, element-wise 연산을 수행하여, 제한된 device 환경에서도 효율적으로 사용가능하다.
  • separable self-attention을 적용한 모델인 MobiileViTv2는 mobile vision task의 다양한 분야에서 SOTA 성능을 보인다. (ImageNet classification, MS-COCO OD)
  • 3 million parameter만으로 ImageNet top-1 accuracy 75.6%를 달성하였다. 이것은 MobileViT v1보다, 3.2배 빠르면서, 1% 정도의 높은 성능을 보여준 것이다.

 

Introduction

[배경]

  • MobileViT의 등장으로, ViT를 제한된 device 환경에서도 활용할 수 있게 되었지만, 이러한 모델은 속도 향상을 막는 주요 원인은 global representation을 학습하기 위해 꼭 필요한, multi-headed self-attention(MHA)이다. 
  • MHA 계산을 위해, token(ViT에서는 patch)의 제곱에 해당하는 연산 복잡도(O(k^2))를 가진 연산이 필요하다. 
  • 또한, MHA 계산에는 복잡한 matrix 연산이 필요한데, 이러한 cost가 큰 연산들은 제한된 메모리 환경과 power 환경에서 제한된 성능을 가지게 된다.
  • 따라서, 이 논문은 제한된 device 환경에서 사용할 수 있도록, transformer의 self-attention을 효율화할 수 있을 것인가에 대한 결과이다. 

[관련 연구]

  • ViT에 한정하지 않더라도, 몇 가지 연구들이 self-attention 연산을 최적화하기 위한 시도를 하였다.
  • 그들 중, 대다수는 self-attention layer의 sparsity를 도입하여, 각 toekn이 다른 token 전체를 계산하는 것이 아니라, 일부를 참조하도록 바꾸는 방향이다. 이러한 시도들은 연산복잡도를, O(k*sqrt(k))나 O(k*logk)까지 낮췄다. 하지만, 그에 따라 성능도 감소했다.
  • 다른 방법으로는 low-rank approximation을 이용한 연산이다. Linformer(아직 안 읽어봄)는 linear projection을 통해, self-attention 연산을 여러 개의 작은 self-attention 연산으로 decompose 하였다. 이로 인해, 연산복잡도는 O(k)까지 떨어졌다. 그러나, Linformer는 여전히 cost가 큰 batch 단위의 matrix multiplication 등을 유지하고 있다.

[소개]

  • 이 논문에서는 separable self-attention 이라는 O(k)의 연산 복잡도를 가진 새로운 self-attention 방법을 소개한다.
  • 효율적인 inference를 위해, 복잡한 연산들을 element-wise operation(덧셈, 곱셈)으로 대체했다. 

 

Model : MobileViT V2

  • MobileViT는 CNN과 ViT를 결합한 네트워크이다. MobileViT는 transformer를 convolution으로 보는 방식으로, convolution과 transformer의 장범을 모두 활용했다. MobileViT는 적은 parameter만으로도, CNN 기반의 MobileNet보다 좋은 성능을 보였으나, 속도가 느리다는 단점이 있고, 그 bottleneck은 multi-headed attention(MHA)이다.
  • MHA는 각 patch들간의 context 관계 연산을 위해, scaled dot-product attention을 사용한다. 하지만, MHA는 O(k^2)의 복잡 연산도를 가지기에, k가 큰 경우(큰 이미지나, patch를 작은 단위로 나눈 경우)에는 매우 연산량이 많아진다. 
  • 또한, MHA는 연산과 memory의 소모가 큰 batch 단위의 matrix multiplication 등의 연산을 사용하는데, 이것은 제한된 device 환경에서 성능 저하의 요인이 될 수 있다. 
  • 이를 해결하기 위해, 논문에서는 선형 복잡도를 가지는 separable self-attention을 제시한다.
  • separable self-attention의 핵심 아이디어는 latent token L에 대한 context score를 계산하는 것이다. 이 socre를 이용하여 input token을 re-weight 하고, global information을 담은 context vector도 만들어낸다. 

Overview of MHA

  • MHA는 transformer가 token 간의 관계를 encoding 할 수 있도록 하는 모듈이다.
  • Transformer의 MHA에 대한 자세한 내용은 아래 글 참조 바란다. 

2023.05.08 - [NLP 논문] - Transformer (Attention Is All You Need) - (1) 리뷰\

 

Transformer (Attention Is All You Need) - (1) 리뷰

Transformer 배경 설명 Transformer는 Google Brain이 2017년 "Attention is All You Need"라는 논문에서 제안된 딥러닝 모델이다. Transformer는 기존 자연어 처리 분야에서 주로 사용되던 RNN, LSTM 같은 순환 신경망 모

devhwi.tistory.com

 

Separable self-attention

  • separable self-attention은 MHA의 구조를 따랐다. MHA와 비슷하게, input 값은 Query, Key , Value의 3개의 branch를 통해 처리된다.

[Query]

  • ViT에서 Query는 이미지의 patch를 linear layer에 통과하여 얻은 d dimension의 feature이다. 이를 d X 1 linear layer에 통과시키고, softmax를 취해주어, context score를 생성한다. (기존 Transformer에서 연산량을 크게 줄인 부분, 과거에는 feature 단위로 attention을 계산하였지만, latent vector 단위로 attention을 계산하겠다.)

[Key]

  • 한편, Key에서는 input이 d X d linear layer를 거쳐서, k X d의 key feature를 만들고, 앞서 구한, context score와 key feature의 weighted sum으로 context vector를 구한다. 이렇게 구해진 context vector는 가볍지만, input x에 대한 모든 token 정보를 지니고 있다.

[Value]

  • 마지막으로, value에서는 input이 d X d linear layer와 ReLU를 거쳐서, k X d의 feature를 만들고, 앞서 구한 context vector를 Value feature에 적용해 준다. 이렇게 구해진 feature를 최종 d X d linear layer에 거쳐서, self-attention 값을 생성한다. 


MobileViTv2 아키텍처

  • separable self-attention의 효과를 입증하기 위해, 기본 MobileViT 구조에서 attention만 바꿨다. 
  • 추가적으로 기본 MobileViT에서 skip connection과 fusion block을 제거하여, 성능을 약간 향상했다. 

 

실험 결과

[실험 셋팅]

ImageNet-1k 학습

  • batch size 1024로 300 epoch을 scratch부터 학습시켰다.
  • optimizer로는 AdamW를 사용하였고, ImageNet-1k를 1.28 million과 5000개로 나누어, train set과 validation set을 구성했다. 

ImageNet-21k-P로 Pretraining & ImageNet-1k finetuning

  • ImageNet-21k(13 mullion, 19000 classes)를 이용하여 pretraining을 진행했다. 
  • ImageNet-21k의 validation set과 ImageNet-1k validationset은 중복되지 않는다. 

 

[기존 모델들과 비교]

  • 성능과 속도 측면, parameter 양에서 종합적으로 고려해 보았을 때, MobileViT는 좋은 성능을 보인다. 

 

[다른 Task]

  • 각각 semantic segmentation과 OD에서도 종합적으로 좋은 성능을 보인다. 

 

 

총평 

  • 간단한 아이디어인데, 논문에서 비슷한 내용이 계속 반복되는 느낌이 든다. 
  • 해결한 방법보다, 현재 transformer의 구조에서 속도가 느린 점을 명확히 파악한 것이 더 흥미롭다. 
  • 가벼운 모델 하면, EfficientNet과 MobileNet 정도 생각했었는데, 사용 가능한 선택지가 하나 추가된 것 같다. 

 

Reference

MEHTA, Sachin; RASTEGARI, Mohammad. Separable self-attention for mobile vision transformers. arXiv preprint arXiv:2206.02680, 2022.

 

반응형

 
 
원래, 일기도 거의 쓰지 않는 나지만, 올해보다 발전된 2024년을 바라면서, 2023년도를 회고해보고자 한다.
 

2023년에 있던 일들

2023년을 돌이켜보면 아쉬움은 많이 남지만, 나에게 많은 변화가 있던 해이다. 개발 블로그를 시작했으며, NLP 분야를 본격적으로 공부하기 시작했다. 평소 잘 알지 못했던 Clean Code에 대한 개념도 새로 배우게 되었고, 그 과정에서 수많은 코드들을 리팩터링 하였다. Spark를 공부하여, Plsql 기반 코드를 변환해보기도 하였고, 병렬 구조에 대한 이해가 더 생기기도 하였다. 개인적으로는 도전과 실패에 대한 무서움이 적어져, 예전보다 더 많은 도전을 하였지만, 그만큼 많은 실패를 하였다. 가끔 한숨이 나오기도 했지만, 다 내 삶에 자양분이 될 것이라고 생각한다. 
 

회사 업무

회사 업무의 큰 변화는 없었다. 요청 온 사항을 분석하고, 이를 해결하는 업무를 진행하였다. 다만, 변한점이 있다면, 1000줄 이상의 복잡한 SQL문으로 구성되는 코드들을 싫어했지만, 이제는 성능 관점에서의 적당한 양보(?)는 허용할 수 있어졌다. 보안상 자세한 이야기들은 할 수 없지만, 운영 중인 시스템에 성능 개선에 대한 니즈가 있어, 새로운 아키텍처들을 많이 찾아보게 되었다. 이 과정에서 코드 프로파일링이나, 자원 모니터링, SQL Tuning 등 많은 부분을 공부하게 되었다. 아쉬운 점으로는 진행하고자 하였던 많은 과제들이 다양한 사정을 통해 꺾이는 것을 보면서, 내 스스로 신입 때의 도전정신과 과격함이 점점 줄어드는 것 같아 걱정이 된다. 아쉬움에 잠 못 이루던 날들도 많았지만, 그래도 도전과 성장 욕구(?)를 채우기 위해, 회사 밖에서 더 노력할 수 있는 원동력이 되지 않았을까 하면서 스스로를 위로해본다.

 

조직 생활

파트에 유능한 신입 인재들이 많이 합류하였다. 모두들 실력도 뛰어나고, 의지도 넘쳐서, 같이 이야기 나눌 수 있는 사람들이 많아져서 기분이 좋아졌다. 새로 들어온 분들을 보면서, 재작년엔 스스로의 성장이 가장 중요한 오브젝티브였는데, 이젠 그들에게 도움이 될 수 있는 사람이 되고 싶다는 생각을 한다. 특히, 나에게 많은 질문을 해주는 동료가 있는데, 이 분께 최대한 정확한 답변과 도움을 주기 위해, 스스로 많이 찾아보게 된 것 같다. (감사합니다.)
한편으로는 그분들이 주장하는 다소 과격해 보이는 변화에 대해, 방어적인 태도를 취하는 나를 보며, 걱정하는 시기도 있었다. (지금은 기술적으로 이유 없는 반대하지 않기에서 타협점을 찾았다.)  이런 고민들을 할 수 있도록, 항상 의문을 갖고 최선을 다해주는 동료들이 있어서 조직생활에서는 좋은 일만 있었던 한 해였다.

 

코드 리뷰어

우리 회사에는 클린코드에 대해, 교육과 활동 등을 진행하고, 이를 자격으로 인증해 주는 제도가 있다. 사실 그동안 머신러닝이나 딥러닝 코드들을 혼자나 소수로 개발하고 공유하는 것에 익숙해져서, 좋은 코드를 짜는 것을 교육으로 운영한다는 것 자체에 거부감을 느꼈다. 하지만, 교육 기간 동안 굉장히 많은 것들을 배웠고, 뛰어난 타 부서 분들과 협업의 기회가 있어서, 매우 좋은 시간이었다. 교육 후에 거의 모든 코드를 리팩터링 하고, 부서원들로부터 코드리뷰를 받았는데, 그 과정에서 몰랐던 개념들을 많이 배웠고, 좋은 코드를 위해 코드리뷰가 얼마나 중요한지 다시 한번 느끼게 되었다. 
 

 

블로그 시작

지금 글을 쓰는 이 블로그를 올해 시작하게 되었다. 블로그를 시작하게 된 이유는 어차피 공부하는 내용이 휘발될 것이 아깝기도 하였고, 꾸준히 공부하기 위한 원동력이 필요하였다. 특히, 누군가 질문했을 때, 정돈된 답을 하고 싶다는 욕구가 존재하여, 공부한 내용을 글로 정리하게 되었다. 블로그에 글을 써가면서, 점점 이미 아는 내용의 소재가 떨어지고, 새로운 내용을 탐색하게 되면서, 나아가고 있다는 느낌을 받아, 매우 기분이 좋다. 블로그의 또 큰 장점은 회사 업무와 관련된 내용을 공부하고, 올렸을 때, 빨리 회사에 가서 새로 공부한 내용을 바로 적용해보고 싶다는 기분으로 회사 출근에 거부감을 없애준다는 것이다. 대학원 시절, 갑자기 생각난 아이디어를 빨리 적용해보고 싶어, 연구실로 향하던 기분을 조금이나마 재현해 냈다. 블로그 활동 중, 그나마 아쉬운 점을 뽑으라면, 퇴근 후에 글을 작성하다 보니, 간단하게 작성할 수 있는 이론 설명이나, 논문 리뷰성 글을 많이 올리게 되는 것 같다. 24년에는 개발한 내용을 정리하는 형식으로 발전해야겠다. 

 

NLP 공부 시작

컴퓨터 비전 베이스라, NLP도 비슷하여 쉽게 공부할 수 있을 것이라고 생각했다. 근데 NLP는 컴퓨터 비전과 차원이 다르게 빨리 발전하였고, 결과도 매우 명확하여 공부할 내용이 많았다. 흐름을 따라가기 위해, NLP 쪽 유명한 논문들은 최대한 다 읽으려고 노력했고, 직접 코드도 많이 짰다. 점점 데이터엔지니어링 영역으로 발전하는 LLM 논문들을 보면서, 모델링 중심의 AI보다는 데이터엔지니어링이 중요하겠구나라는 생각을 다시 하였고, 공부의 방향도 다시 설정하게 되었다. 모델링의 영역이 과거보다 희미해지니, 컴퓨터 비전쪽 논문도 처음보다는 덜 어렵게 읽히는 것 같아, 최근에는 컴퓨터 비전쪽 논문을 많이 읽으려 노력 중이다. 논문을 다수 읽으면서 느끼는 점은 단순 이해도 중요하지만, 가능하면 코드를 직접 짜보는 것이 좀 더 전문가로 성장할 수 있는 방향이 아닐까 싶다. 

 

도전과 실패

개인적으로는 23년에 정말 많은 도전을 하였고, 그에 따른 많은 실패도 하였다. 인생에서 그동안 실패의 기회들이 적었기 때문에, 작은 실패에도 굉장히 큰 타격을 받았고, 그것을 인정하는 것이 매우 어려웠다. 올해를 긍정적으로 생각하면, 실패에 대해 조금은 무뎌져, 도전의 어려움이 조금은 줄어들었다는 것이고, 아쉬운 점은 결국 아무것도 성공하지 못했다는 것이다. 그래도 내 인생의 좌우명처럼 "아무것도 하지 않으면 아무것도 이뤄지지 않는다"는 것을 알기에 나는 계속 시도를 할 예정이다. 이렇게 하다 보면 언젠가는 성공할 날도 있지 않을까?
 

 

Refresh

나의 스트레스를 푸는 유이한 방법은 운동과 먹는 것이다. 사실 먹는 것은 효과가 명확한데, 운동은 그 행위 자체의 행복보다는 나를 변화시키기 위해 무언가를 했다는 안도에 가까웠다. 23년에는 아직 완전히는 아니지만, 운동 자체에서 가끔 재미를 찾기도 하고, 다른 사람들이 가끔 칭찬을 해주면 기분이 매우 좋았다. 최근에는 거의 3~4년 만에 해외여행을 갔는데, 몇 년 간 묵었던 스트레스가 풀리는 기분도 나고, 세상이 넓음에 감탄하기도 했다. 가끔은 여행으로 스트레스를 푸는 것도 좋을 것 같다. 또 이번 여행에서 갑자기 오르골을 하나 사게 되었는데, 오르골 소리를 듣고 있으면 여행 때 좋았던 생각이 나기도 하고, 마음이 평온해진다. 매일 자기 전에 한 번씩 돌리고 자야겠다. 

 

2024년에는...

  • 24년 가장 이루고 싶은 것은 "증명"이다. 단순히 이 사람은 이걸 잘해가 아닌, 가시적인 결과를 내고 싶다. 
  • 24년 가장 바라는 점은 "많은 기회"다. 아직 나는 노력으로 뭐든 이룰 수 있다고 믿는다. 이를 위해 24년에는 많은 기회들이 주어졌으면 좋겠다.
  • 이를 위해, 올해는 공부할 내용을 조금 더 체계적으로 계획해야겠다. 
  • 내 주변 사람들 모두 행복했으면 좋겠다. 
반응형

논문 배경 설명

  • Fine-tuning Image Transformers using Learnable Memory은 2022년 CVPR에 제출된 Google 논문이다.
  • memory token 개념을 사용하여, ViT에서 과거 task에 대한 정보를 저장하여, 성능을 유지하고, 새로운 task에 대한 학습을 진행할 수 있는 방법을 소개했다. 
  • 저자들은 지속 & 확장 가능한 memory 개념으로 소개하는데, 만약 진짜라면, external memory 개념으로 탈부착 가능한 memory가 될 수도 있지 않을까? 하는 생각이 든다. 

 

Abstract

  • 이 논문에서는 Vision Transformer model에 학습 가능한 memory token을 넣은 새로운 ViT 모델을 소개한다.
  • 이 모델의 방법에서는 하나의 task에서 학습한 parameter들을 새로운 task 학습에서 활용 가능하다.
  • 이를 위해, 각 layer들에 특정 dataset에 대한 contextual 정보를 제공하는 "memory token"이라는  learnable embedding vector들을 도입하였다. 
  • 이러한 간단한 변형은 transfer learning에서 단순 head만을 fine-tuning 하는 것보다 큰 성능 향상을 보이고, 연산 비용이 매우 큰 full fine-tuning에 비교해서도 약간의 성능 차이만 보인다. (약간 낮다.)
  • 또한, 이 논문에서는 computation 재사용으로 새로운 downstream task들에 사용가능한 attention-masking 방법을 소개한다.

 

Introduction

[배경]

  • ViT 모델은 일반적으로 대량의 데이터들을 통해 학습되고, 다양한 downstream task에서 성능 향상을 위해 fine-tuning 하는 방법을 사용한다. 
  • 높은 정확도를 위해, 전체 모델을 목적에 맞는 task에 fine-tuning하는 것이 가장 좋은 방법이다.

[문제]

  • 하지만, 일반적으로 Transformer 기반 모델들은 많은 수의 parameter로 구성되어 있기 때문에, fine-tuning 과정에 연산 cost가 크고, 특히, 전체 모델 fine-tuning 방법은 learning rate에 민감하다는 문제가 있다. 

[소개]

  • 이 논문에서는 transformer의 각 layer에 learnable token을 새로 추가하여, pre-trained model을 구성하는 새로운 방법을 소개한다. 
  • 새로운 token들은 최종 prediction 성능 향상에 사용될 수 있는 contextual 정보들을 포함한 영구적 memory처럼 동작한다.
  • 또한, 이러한 token은 pixel 기반 patch부터 최종 concept 수준까지 다양한 수준의 추상화 개념을 나타낼 수 있다. (pixel 정보부터 다양한 문맥 정보까지 모두 포함할 수 있다.)

[결과]

  • 새로운 방법은 downstream task에서 기존 head를 fine-tuning한 모델보다, 큰 성능 향상을 보였다. 
  • 또한, 이러한 아키텍처 디자인은 약간의 computing cost 증가만으로, 새로운 taksk를 학습하면서도, 기존 task에 대한 성능을 유지할 수 있게 한다. (기존 모델들은 새로운 task를 학습하면, 이전 task에 대한 성능이 떨어짐)
  • 이를 위해, 적절한 attention masking이라는 새로운 방법을 소개하여, 과거 task에 대한 정보를 memory에 저장하는 방식을 사용한다. 이러한 방식은 continual learning의 개념처럼, 계속하여 새로운 기능을 더하여 다양한 task에 모두 활용 가능한 하나의 모델로 활용될 수 있다.

 

Memory Model

  • Base mode로는 original ViT 논문의 구조를 그대로 사용한다. (다만, 이 논문에서는 classification model들만을 고려하기 때문에, decoder는 사용하지 않는다. )
  • 일반적인 image transformer의 input은 다음과 같다. (Epos : positional embedding, Xcls : class token)

  • 논문에서는 새로운 memory 정보를 더하기 위해, 이 input에 m개의 learning embedding(Emem)을 concatenate해준다.

  • 즉, N+1+m token이 Transformer encoder layer들의 input으로 사용된다.
  • 각 layer들은 일반 ViT의 layer들과 거의 비슷하다. 유일하게 다른 점은 self attention module의 처음 N+1 token들만 output으로 보낸다는 점이다. (m개의 token들은 다음 layer로 보내지 않음)
  • 따라서, l번 layer의 output은 N+1개의 token을 가진 y_l로 명명하면, 연속적인 layer들은 모두 아래와 같은 pattern의 input을 받는다. (전 layer에서 token N+1개와 memory token m개)

  • 전체 network 구조는 아래와 같다. 

 

[Fine-tuning with full attention]

  • 이 모델에서 memory의 주된 용도는 fine-tuning에서의 활용이다.
  • 이를 위해, randomly-initialized memory token을 제시하고, gradient descent를 활용하여 memory token을 학습한다. 
  • 이 방법은 좋은 결과를 보여주지만, 이렇게 학습된 model은 hidden activation들의 변화가 일어나, 과거 task에 더 이상 활용 불가능하다.

[Attention masking for computation reuse]

  • 새로운 task만을 위한 fine-tuning에서는 문제가 되지 않지만, 앞선 task의 성능을 유지하면서, 새로운 task를 학습하고 싶은 경우에는 활용 불가능하다.
  • 이 논문에서는 기존 dataset class token을 유지하면서, 새로운 dataset class token에 대한 memory 구조를 사용한다. 
  • 결과적으로, parameter들과 computation을 reuse 하여 동일 input에 대한 multiple task들을 처리할 수 있다. 
  • 이를 위해, original class token 및 input patch들이 새로운 dataset이 사용하는 memory token 및 new class token에 활용되지 않도록 막는 attention mask를 사용한다.  (attention mask의 동작은 아래와 같다. 아래 그림처럼, 해당하는 학습에 대한 class token만 유지하고, 다른 class들은 masking 한다. 별표가 mask)
  • 각 task의 head는 각 dataset token에 연결된다. 

[model concatenation]

  • Attention masking은 새로운 task 확장을 위한 memory 추가에 이용될 수 있다.
  • 새로운 dataset에 대한 model 학습 시에는 attention masking을 이용하여 이전 dataset들에 대한 참조를 막고, 새로운 dataset에 대해 학습하는 방법을 사용한다. 
  • 아래 그림처럼, 각기 독립적으로 학습된 memory를 연결하여, 모델을 구성한다. 

Experiments

[실험 setup]

  • ViT-B/32 base transformer model을 base 모델로 사용하였고, ViT original paper에서 제공된 Imagenet-21K pretrained model을 사용했다. (80M parameters)
  • orginal ViT의 fine-tuning 방법을 따라, cosine learning reate schedule 등의 fine-tuning setup을 그대로 사용했다. 
  • batch size는 512이고, 20000 step의 fine-tuning을 진행했다. 
  • SGD를 사용했고, gradient clipping을 사용했다. 5-step의 linear rate warmup도 했다.
  • Memory는 N(0,0.02)로 initialization 시켰다. 
  • 성능 측정을 위한 dataset으로는 CIFAR-100, i-Naturalist, Places-365, SUN-397을 사용했다. 

 

[실험 모델: Baseline fine-tuning]

 

Full fine-tuning

  • 전체 model을 fine-tuning 함. 가장 expensive 하고, 각 task가 전체 model이 필요하기 때문에, 실용성이 가장 떨어짐. overfitting 가능성이나, learning rate에 민감함. 

Head-only fine-tuning

  • classifier의 head만 fine-tuning 함. 이 방식은 pre-trained의 mmbedding을 재활용함. 가장 큰 장점은 parameter들과 compute를 재사용할 수 있다는 것.

Head + Class token

  • head와 class token을 fine-tuning함. 이 방식에서는 input token attention이 새로운 class token에 따라 바뀌면서, 연산 재사용이 불가능함. 

[실험 모델: memory fine-tuning]

 

Memory + head + class token

  • memory fine-tuning 모델로 부르는, 논문에서 소개된 모델임. 1,2,5,10,20 cell에 대한 실험을 진행함. 각 layer에서 20개 이상에서는 더 이상 성능 향상이 일어나지 않음. cell 5개가 알맞았음. 

Memory with attention mask

  • head-only fine-tuning과 Head + Class token의 장점을 결합한 방법. Head + Class token보다 높은 성능을 보이고, full-attention fine-tuning보다는 약간 낮은 성능을 보임. 이 방법의 가장 큰 장점은 독립적으로 다양한 task를 fine-tuning 할 수 있다는 것이고, 여러 연산을 하나의 모델로 모을 수 있다는 것.

 

[Transfer Learning 성능 비교]

 

  • full fine-tuning은 learning rate에 민감하다. large learning tate에서 큰 성능 저하를 일으켜, 이 부분에서는 memory와 head-only fine-tuning이 오히려 더 좋은 성능을 보인다. 
  • 아래 그림에서 확인할 수 있듯, memory를 이용한 방법들이 다른 방식의 fine-tuning보다 좋은 성능을 보인다. 

  • 각 fine-tuning의 가장 optimal learning rate에서의 성능을 비교해 보았을 때, 결과는 아래 표와 같다. 
  • 많은 데이터셋에서 5~20 cell 사이의 memory fine-tuning이 좋은 성능을 보인다. 

 

Conclusion & Limitation

  • 이 논문에서는 memory 구조를 transformer model에 결합한 새로운 방법을 제안했다.
  • memory를 사용한 새로운 모델은 fine-tuning에서 좋은 성능을 보였고, 다양한 task 학습을 가능하게 했다.
  • memory yoken은 attention model의 중요한 부분으로, lifetime and continuous learning에 중요한 요소가 될 것으로 생각된다. 
  • 몇 가지 한계와 future work를 소개한다.

[scaling memory]

  • 저자들은 memory token이 특정 숫자를 넘어가면, 성능 향상이 줄어드는 것을 발견했다. 
  • 한 가지 가능한 방법은 Top-K memory token만 참조하도록 제한을 거는 것이다. 이러한 방법은 불필요한 token들의 참조를 막아, background noise 등을 제거할 수 있을 것이다.

[combining episodic memory with learnable memory]

  • Episodic memory와 함께 사용하는 방법도 생각해 볼 수 있다.

[incremetal model extension]

  • 논문에서는 multiple attention masking model이 독립적으로 학습되어 하나의 모델로 합쳐지는 방법을 사용하였다. 하지만, 순차적인 학습으로 인한 중간 메모리 축적으로 생기는 이점(아마, 각기 다른 학습 간의 시너지를 의미하는 것 같음)은 커리큘럼 학습에 중요하고, 이것이 향후 연구의 주제가 될 것이다. 

 

논문 총평

  • 매우 아이디어가 간단하고, 아이디어에 대한 실험 결과가 매우 명확한 논문이다. 
  • 과거에 Knowledge distilliation을 잠시 파본 기억이 있는데, CNN 기반에서 distillation이 매우 어렵다고 생각했었는데, ViT 기반에서 memory token 개념을 사용하여 해결한 것이 매우 인상적이다. (그 후론 CNN 기반에서 방법을 찾아본 적이 없어서, 비슷한 아이디어가 있는지는 모르겠다.)
  • 다만, 실제 데이터에서 잘 working 하는지는 직접 실험을 해봐야 판단이 가능할 것 같다.

 

Reference

Sandler, M., Zhmoginov, A., Vladymyrov, M., & Jackson, A. (2022). Fine-tuning image transformers using learnable memory. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 12155-12164).

반응형

MobileViT 배경 설명

  • MobileViT은 2022년 Apple에서 ICLR 2022에 제출한 논문이다. (Apple이여서, mobile에 대한 CNN이 더욱 필요했을 것이다.)
  • CNN에서 mobilenet이 나왔듯, ViT에서도 light cost에 초점을 맞춘 논문이 등장하였다.

 

Abstract

  • mobile 환경에서 구동 가능할 정도의 가벼운 vision task CNN 모델이 등장하였었다. (mobilenet) 
  • 하지만, CNN과 달리 ViT는 최근 많은 vision task에 사용됨에도 불구하고, global representation들을 활용하기 위한 self-attention 구조를 사용하기 때문에, CNN에 비해 모델이 무겁다.
  • 이 논문에서는 CNN과 ViT를 결합하여 mobile vision task에서 구동 가능한 가벼운 ViT 모델을 만들 수 있는가? 에 대한 답을 제시한다. 
  • 이를 위해, 가볍고, 좋은 성능을 내고, 다양한 용도로 사용할 수 있는 MobileViT를 소개한다. 
  • 실험에서는 MobileViT가 다양한 task와 dataset들에서 CNN과 ViT 기반의 모델보다 압도적인 성능을 보여준다.

 

Introduction

[배경]

  • ViT의 소개 이후로, ViT의 연구 트랜드는 model의 parameter의 양을 늘리면서, 좋은 성능의 모델을 만들어내는 것이었다. (LLM처럼...)
  • 하지만, 이런 모델 크기의 증가는 model의 용량 증가와 속도 저하등을 일으킨다. 
  • 실생활에서는 mobile 기기 같은 제한된 H/W에서 구동되어야 하는 task들이 많이 존재하기 때문에, 가볍고 빠른 모델에 대한 요구사항이 있다. 
  • 이를 위해, ViT의 parameter 개수를 줄이면, 오히려, 가벼운 CNN들보다도 좋지 않은 성능을 보여준다. 따라서, 어떻게좋은 성능을 유지하면서 가벼운 ViT를 만들 것인지에 대한 심도 깊은 고민이 필요하다.

[CNN + ViT]

  • 일반적으로 ViT는 CNN에 많은 wieght를 필요로 하고, optimize가 어렵다. 예를 들어, ViT 기반의 segmentation network는 345 million의 parameter가 필요한데, 비슷한 성능의 CNN은 59 million으로, ViT 기반이 비효율적이다. 이것은 ViT가 CNN의 특성인 image specific inductive bias가 부족하기 때문이다.
  • 좋은 성능의 ViT 모델을 위해서라면, convolution과 transformer의 하이브리드 방법을 채택하는 것이 효과적일 수 있다.
  •  하지만, 기존 시도된 하이브리드 방법들은 아직 매우 큰 model size를 가지고 있고, data augmentation에 민감하다는 단점이 있다. 

[MobileViT]

  • 이 논문에서는 제한된 H/W 리소스에서도 효과적으로 좋은 성능을 보이는 ViT 모델을 만들고자 한다. 
  • 모델의 효율성 측정을 위해 보통 사용되는 FLops는 mobile 장비 등에서 성능 측정에는 충분하지 않다. 이는 FLOPs는 memory access나 parallelism 정도, platform 특성등을 고려하지 않기 때문이다. 
  • 따라서, 논문에서는 FLOPs을 optimize 하기보다는, light weight, general-purpose, low latency에 초점을 맞췄다.  
  • 이 논문에서는 CNN의 spatial inductive bias와 ViT의 global processing에 장점들을 결합가능한 MobileViT model을 소개한다.
  • 특히, local 정보와 global 정보를 모두 encoding 할 수 있는 MobileViT block은 효과적인 representation을 포함한 feature를 만들 수 있게 해 준다.

 

MobileViT : A light-weight Transformer

  • 일반적인 ViT model에서는 (H X W X C) 크기의 이미지를 P 사이즈의 patch들로 자르고, flatten 하고, projection 하여, (N X d) 크기의 inter patch representation으로 만든다.  이때, comnputational cost는 O(N*N*d)이다. (N: number of patch)
  • 이러한 ViT 구조의 모델들은 CNN에 내재된 spatial inductive bias를 무시하기 때문에, CNN에 비해 더 많은 parameter를 필요로 한다. 
  • 또한, 이런 구조들의 모델은 L2 regularization이나, overfitting을 피하기 위해 많은 data augmentation이 필요한 등 optimization이 어렵다. 

 
[MobileViT 아키텍처]
 
MiobileViT의 핵심 아이디어는 Transformer를 convolution으로 사용하여 global representation들을 학습하는 것이다. 
 

  • MobileViT block 
    • MobileViT block은 model이 input의 local과 global 정보를 적은 paramter 안에 모두 담을 수 있도록 하는 것이 목적이다. 
    • 우선 MobileViT는 (n X n) 개의 convolutional layer와 (1 X 1) convolution을 수행해 준다. 이로 인해, local representation을 포함하게 된다. 
    • global representation을 학습하기 위해, 앞서 covolutional layer의 output을 flatten 하고 겹치지 않도록, patch로 나눠준다. 그러고 나서, 이 patch들을 input으로 Transformer에 넣어준다. 
    • 이로 인해, 아래 그림처럼, local 정보를 포함하면서, global 정보까지 포함할 수 있는 feature가 완성되게 된다.

 

  • Light Weight
    • MobileViT는 image specific inductive bias를 사용할 수 있기 때문에, 일반 ViT에 비해 훨씬 더 가벼운 모델로 학습이 가능하다. (dimension 등을 줄일 수 있음)
  • Computational Cost
    • MobileViT와 ViT의 computational cost를 비교하면, 각각 O(N*N*P*d)와 O(N*N*d)로 이론적으로는 MobileViT가 더 비효율적으로 보인다. 하지만, 실제로는 MobileViT가 DeIT ImageNet-1K image classification에서 2배 적은 FLOPs으로 1.8% 좋은 성능을 낼 정도로 더 효과적이다.
    • 이것은 더 가벼운 모델로 학습이 가능하기 때문이다. 

 
[Multi-scale sampler for training efficiency]

  • 일반적으로 ViT 기반의 모델들은 fine-tuning 시에 다양한 스케일의 representation들을 학습한다. 이 과정에서 Positional embedding을 사용하는데, 이는 input size에 기반하여 interpolation이 필요하고, interpolation 방법에 따라 성능에 영향을 미치게 된다. 
  • CNN과 마찬가지로, MobileViT에서도 이러한 positional embedding이 필요가 없다. 
  • MobileViT에서 이러한 다양한 스케일의 representation들을 학습하기 위해, batch size를 변경시키는 방법을 사용했다. 다양한 spatial resolutions set을 만들어 놓고, 각 bach 시, resoluton을 무작위로 뽑고, 해당 resolution에 따라 batch size를 유동적으로 변경하여 학습했다. (small spatial resolution에는 많은 batch) 
  • 이러한 학습 방법은 더 빠른 optimize가 가능하도록 하였다. 

 

실험 결과

  • MobileViT 모델을 scratch부터 ImageNet-1K classification dataset으로 학습했다.
  • multi-scale sampler로 각각 (160,160), (192,192), (256,256), (288,288), (320,320) 해상도를 사용했다. 
  • 성능 측정은 top-1 accuracy로 진행했다. 

[CNN과 비교]

  • MobileViT가 CNN 기반의 MobileNet 등의 light-weight CNN이나, ResNet 등의 일반적인 CNN의 성능을 압도했다.

[ViT 비교]

  • ViT와 비교해서도 좋은 성능을 보인다. 특히, DataAugmentation에 따라 성능이 크게 바뀌는 다른 모델들과 달리, Basic Augmentation만으로도 다른 ViT보다 좋은 성능을 보인다. 

 
 

총평

  • 매우 아이디어가 간단하고, 효과적인 것 같다.
  • 논문 내용만 보면, 장점만 많아서, 진짜인지 직접 돌려보고 싶다. 

 

Reference

Mehta, S., & Rastegari, M. (2021). Mobilevit: light-weight, general-purpose, and mobile-friendly vision transformer. arXiv preprint arXiv:2110.02178.

반응형

회사에서 정상적으로 운영하던 서비스에 문제가 생겼다. 돌이켜보면 매우 간단한 이슈였지만, 해결과정이 재밌었고 비슷한 상황에 도움이 될 수 있을 것 같아서 기록으로 남기게 되었다.

 

문제

  • 며칠 전부터 서버 간의 SSH 연결이 매우 늦어지는 상황이 일어났다. 특히, 특정일 10:00:00을 기준으로 특정 대역대 서버가 모두 영향을 받게 되었다.
  • 특히, expect를 이용한 명령어 수행이 기본 1분 내외의 수행시간에서 30분이 지나도 해결되지 않는 문제가 발생하였다.
  • 당장 서비스 중인 서버이기 때문에, 해당 대역대가 아닌 다른 서버로 연결을 바꾸어, 해결했지만 특정 대역대의 서버 전체의 "SSH"가 느려져서, 빠른 해결이 필요하였다.

 

가설 & 시도

  • 처음으로 의심한 것은 서버 자체의 부하이다. 
  • 하지만, ps와 top으로 확인해 보았을 때, H/W 리소스는 충분하였고, 특히 특정 대역대의 서버 전체의 SSH가 다 늦어졌다는 점이 서버 자체의 부하의 가설이 아닌 것을 의미했다. 
  • 다음 의심한 점은 해당 대역대의 서버들을 물고 있는 Switch의 문제였다. 하지만, SSH 연결 외에 다른 연결을 정상적으로 수행된다는 점이 이상하였다.

 

로그 확인

  • SSH 연결에서는 verbose 옵션을 통해 SSH 연결 과정을 debugging 할 수 있다.
$ time ssh -v {ip주소}
  • 나의 경우에는 SSH 연결 대상 서버에서 msg를 응답받는 구간에서, 응답을 받는 데까지 매우 오랜 시간이 걸리는 것을 확인했다.
  • 따라서, SSH 대상 서버를 들어가서 로그를 확인하였다.
  • 별도의 SSH 모니터링 툴을 사용하지 않는다면 Linux 서버에서 SSH 관련 로그를 확인해 볼 방법은 2가지이다.
    • /var/log/audit : Linux 자체에서 audit에 관한 로그를 저장하는 곳이다. 시스템에서 발생하는 이벤트와 활동을 추적하기 때문에, SSH 연결등의 정보가 이곳에 기록된다.  
    • /var/log/secure : 보안 이벤트와 사용자 인증에 대한 로그가 기록된다. sshd(SSH 데몬)에 의한 로그인 시도, 계정 관련 이슈 등이 기록된다. SSH 연결을 위한 보안의 시간이 기록되기 때문에, 확인해야 한다.

<audit log 예시 - ChatGPT가 만들어줌>

type=USER_AUTH msg=audit(1603989582.198:304): pid=1234 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=login id=jsmith exe=/usr/sbin/sshd (hostname=?, addr=192.168.1.100, terminal=sshd res=success)'

 

<secure log 예시 - ChatGPT가 만들어줌>

Nov 20 10:15:30 server sshd[1234]: Accepted publickey for jsmith from 192.168.1.100 port 22 ssh2
Nov 20 10:15:30 server sshd[1234]: pam_unix(sshd:session): session opened for user jsmith by (uid=0)
Nov 20 10:15:30 server systemd-logind[567]: New session 1234 of user jsmith.

 

  • 로그를 확인해 보았을 때, audit log에 정상적으로 success가 찍혀있고, secure log를 확인해 보니, 연결 시도부터까지의 시간이 느린 것을 발견하였다. 

 

방법 발견

  • 최초에 SSH가 느려지는 상황을 구글링 하였을 때, 가장 많이 나오는 방법이 /etc/ssh/sshd_config 파일의 UseDNS 옵션을 "no"로 바꾸는 것이다.
  • 사실 이 방법이 효과적일 것으로 생각했지만, 회사의 엄격한 보안 정책과 운영 중인 시스템에서 변경점을 가하기 어려워(sshd restart가 필요하기 때문), 운영 중이지 않은 서비스로 실험을 해보았다. 
  • /etc/ssh/sshd_config에 아래 내용을 추가한다. (만약 UseDNS yes로 되어 있으면 no로 변환한다) 그러고 나서, sshd를 재시작해준다.
$ sudo vim /etc/ssh/sshd_config

---------------------------------

UseDNS no

---------------------------------
$ sudo systemctl restart ssh

 

  • 원리를 알아보니, server에서 SSH로 연결을 시도할 때, SSH 데몬에 DNS를 사용하도록 설정되어 있으면,  target 서버에서 /etc/resolv.conf 파일을 읽어서, 해당 파일에 명시된 DNS 서버에, hostname을 묻는 DNS query를 날리고, 이 결과를 통해, 현재 SSH 연결을 시도한 서버에 해당하는 서버가 있으면 해당 hostname으로 연결을 진행한다.
  • 이 과정을 reverse DNS lookup이라고 하는데, resolve.conf의 각 서버와 통신하여, hostname을 묻는 과정이 포함되기 때문에 conf 파일 내의 모든 서버와의 연결이 정상적이어야 한다.

  • UseDNS를 no로 변경하니, 이슈 전처럼 정상적인 속도로 SSH가 진행되었다. 하지만, 단순히 DNS connection 몇 개가 추가되었다고, 이렇게 오랜 속도가 걸리는 것은 많이 이상하였다.

 

이슈 해결

  • 결론부터 말하면, resolv.conf에 해당한 서버 중, 특정 서버가 IP를 변경하여 생긴 문제였다. 문제가 되는 대역대는 VM 서버를 사용하는 서버들이었고, 하나의 서버를 copy 하는 방식으로 서버를 setup 했기 때문에, 이 대역대의 모든 서버에 대해 동일 현상이 발생하였다.
  • resolv.conf에서 문제가 되는 IP들을 제거해 주니, UseDNS 옵션을 다 시켜도, 예전처럼 빠른 속도로 SSH를 붙을 수 있게 되었다.
  • 특히, 문제가 발생한 서비스의 경우, shell script 상에 expect 명령어를 이용하도록 되어 있는데, DNS와의 연결이 되지 않으니, Target Server에서 적절한 response를 주지 못하고, 이로 인해 Target 서버와의 연결이 안 되는 것처럼 느껴진 것이었다.
  • 돌이켜보니, 매우 간단하고, 황당한 이슈였지만, 원인을 찾는 과정에서 많은 것을 배운 것 같다.

 

교훈

  • 최근 SSH에서는 UseDNS의 default 설정이 no라고 한다. 다만, 예전 버전을 사용하는 경우에는 UseDNS의 기본 옵션은 yes이다. 만약 보안적 문제가 없다면, UseDNS를 끄는 것이 더 좋을 것 같다.
  • 하지만, 불가피하게 사용하는 경우에는 꼭 DNS 설정을 잘 관리해야 한다. 해당 DNS도 현재는 거의 사용하지 않지만, History 및 영향도 파악이 불가하여, 별도의 삭제를 하지 않다가 이슈를 맞이하게 된 상황이다. 
  • Linux 명령어를 단순 사용하기만 했는데, 동작 원리를 알아야겠다! 

'Linux' 카테고리의 다른 글

[이슈 해결] Airflow에서 cx_Oracle 관련 에러 (dpi-1047)  (24) 2024.02.19
반응형

최근 회사에서 plsql로 수 천 줄이 넘는 query를 python으로 바꾸었다. 최초에는 기존 병렬 처리를 그대로 바꾸기 위해, pyspark로 바꾸는 방법을 선택했지만, 생각보다 속도가 매우 느리고, Python UDF 사용 시, 데이터 정합성이 틀어지는 문제등이 발생하였다. (아마 비동기 처리 때문일 것으로 예상된다.) 결국, 여러 가지 시도 후, 이 프로젝트에서는 pandas가 더 유용할 것이라고 판단되었고, 속도를 끌어올리기 위해 여러 방법을 사용해 보다가 발견한 방법을 소개한다.


Pandas Apply의 문제

  • Pandas는 C를 이용한 연산을 하는 numpy 기반으로 되어 있기 때문에, 어느 정도의 벡터화는 가능하지만, 무거운 연산등을 apply로 수행할 때, 속도적 한계를 가진다.
  • 이는 Python의 고질적 문제인 GIL(Global Interpreter Lock)으로 인한 병렬 처리의 한계와 타입 추론에 추가적인 cost가 든다는 점등이 있다. 
  • 특히, GIL 때문에, pandas는 하나의 쓰레드 만이 python 바이트 코드를 실행하기 때문에, 많은 row에 apply를 수행할 때, 병렬 처리의 효과를 보지 못해 오랜 연산 시간이 걸리게 된다.
  • 이를 해결하기 위해, dask나 swifter 같은 다양한 multiprocessing 지원 라이브러리가 나왔지만, ⓐ경험상 특정 연산이 아니면 속도적 이점이 뚜렷하지 않고, ⓑ처리해야 할 row수가 적은 경우에는 오히려 작업 병렬화에 드는 cost가 더 큰 경우가 많다.

2023.07.21 - [Python] - Pandas 성능 향상을 위한 방법들

 

Pandas 성능 향상을 위한 방법들

Pandas 란? Pandas는 파이썬에서 데이터 처리와 분석을 위한 라이브러리로, numpy를 기반으로 개발되었다. Pandas는 DataFrame과 Series라는 데이터 구조를 사용하여, 데이터를 쉽게 처리할 수 있도록 한다.

devhwi.tistory.com

  • 보통, 이런 경우에 다음 step으로 고민하는 것이 spark인데, 경험상 처리할 데이터가 수 GB 이하인 경우에는 pandas가 유리하다. 특히, 복잡한 로직이 전처리 과정에 많이 들어갈 경우에는 직렬화 cost가 많다는 점, API가 확실히 유연하지 않다는 점(예를 들어, 여러 칼럼에 대한 동시에 unnest를 수행할 때, pandas는 explode 한 번에 가능하지만, spark는 여러 칼럼을 묶었다 푸는 과정이 추가적으로 필요하다) 때문에 pandas가 소규모 데이터 처리에 더 큰 강점을 가진다고 생각된다.

 

Python Multiprocessing

  • 이를 해결하기 위해, 기존 apply를 python "multiprocessing" 라이브러리를 사용하여 처리 방식을 바꿔서 처리할 수 있다.
  • 원리는 처리해야하는 전체 row를 사용 가능한 process 수만큼으로 나눠서 처리하고, 그 연산 결과를 concat 하는 것이다.
  • 특히, 이 방식은 apply에서 참조하는 칼럼이 하나인 경우에는 연산처리 시간이 매우 크지 않아 차이가 덜한데, 여러 칼럼을 참조하는 경우(apply의 axis가 1인 경우)에는 그 효과가 매우 크다.
  • 물론, 이 방식이 항상 장점만 있는 것은 아니다. 우선 당연히 단일 코어가 아니여야하고, 적은 데이터에서는 프로세스 간 통신 오버헤드나 cocat 과정에서 발생하는 cost 등이 존재하여, 성능 상 오히려 손해를 볼 수도 있다. 또한, 메모리 사용량이 증가할 수 있다는 것이 단점이 될 수 있다.
  • 따라서, 모든 apply를 해당 방식으로 바꾸기보다는 time debugging 이후, 연산 시간이 너무 많이 소요되는 apply를 대체하여 비교하는 방식으로 적용하는 것을 추천한다.
  • 대체 코드는 다음과 같다. parallel_apply의 인자는 처리할 pandas 데이터와 그 함수에 적용할 func를 넘겨주면 된다.(이때, func에 lambda를 사용할 수 없는데, lambda는 직렬화가 되지 않기 때문이다. 따라서 일반 함수를 정의해서 사용하면 된다.)

 

[DataFrame으로 나누기]

import pandas as pd
from multiprocessing import Pool, cpu_count

def parallel_apply(data, func, num_processes=None):
    if num_processes is None:
        num_processes = cpu_count()

    chunk_size = len(data) // num_processes
    chunks = [data.iloc[i:i + chunk_size] for i in range(0, len(data), chunk_size)]

    with Pool(num_processes) as pool:
        results = pool.map(func, chunks)

    return pd.concat(results, axis=0)

 

[Row로 나누기]

def parallel_apply(data, func, num_processes=None):
    if num_processes is None:
        num_processes = cpu_count()
    # print("core 갯수 ", num_processes)

    with Pool(num_processes) as pool:
        results = pool.map(func, [row for index, row in df.iterrows()])
    return results

 

  • 데이터를 chunk 단위로 나누는 방식은 DataFrame 단위(하나의 큰 DataFrame을 여러 DataFrame으로 나눔)나, Row 단위(각 row의 연산을 병렬 수행함) 경험적으로 연산의 CPU 사용량 클 때는 Row 단위의 나눔이 성능 상 우위가 있는 것 같고, 아니면 chunk 단위로 나누는 것이 빠른 것 같다.

 

사용 예시

  • 우선 여러 DataFrame으로 나눠서 처리하는 예시이다. 실제 사용하는 함수에서는 apply에 들어가는 함수가 매우 heavy하여 그 효과가 더 컸지만, 그만큼 heavy 한 함수가 생각나지 않아 row의 크기를 늘렸다.
import time

import pandas as pd
import numpy as np
from multiprocessing import Pool, cpu_count
import math


def custom_func(row):
    return row['data1'] ** 2 - math.sqrt(row['data2'])

def parallel_custom_func(chunk):
    result = chunk.apply(custom_func, axis=1)
    return result


def parallel_apply(data, func, num_processes=None):
    if num_processes is None:
        num_processes = cpu_count()
    print("core 갯수 ", num_processes)

    chunk_size = len(data) // num_processes
    chunks = [data.iloc[i:i + chunk_size] for i in range(0, len(data), chunk_size)]

    with Pool(num_processes) as pool:
        results = pool.map(func, chunks)
    return pd.concat(results, axis=0)


if __name__ == '__main__':
    df = pd.DataFrame({'data1': np.arange(1000000), 'data2': np.arange(1000000)})

    # pandas apply
    start_time = time.time()
    df["data"] = df.apply(lambda row: row['data1'] ** 2 - math.sqrt(row['data2']), axis=1)
    print("일반 Apply",time.time() - start_time)

    # multiprocessing apply
    start_time = time.time()
    df["parallel_data"] = parallel_apply(df, parallel_custom_func)
    print("Parallel Apply",time.time() - start_time)

 

Apply 외 다른 함수에 적용 & 주의점

  • row 단위의 연산을 하는 다른 함수에도 적용 가능하다. 예를 들어, unnest를 위한 explode 함수등에도 사용 가능하다. 다만, 연산이 매우 무겁지 않은 경우에는 속도적 차이를 느끼지 못했다.
  • 경험상, pandas의 내장 함수가 가장 빠르기 때문에 내장 함수로 처리 가능한 것은 내장 함수를 우선적으로 선택하고(C를 이용한 벡터화가 매우 잘 되어 있을 것으로 예상됨), apply 함수 중, 연산 수행 시간이 매우 오래 걸리는 부분을 선택적으로 multiprocessing을 이용한 함수로 바꾸면 효과가 큰 것 같다.
  • 연산이 가볍고, row수가 많지 않을 때는 단순 apply가 빠르다. 꼭 time debugging 후 바꿔야한다!
  • 그럼에도 불구하고, multiprocessing이 swifter 등보다 좋은 점은 연산의 병렬 처리 정도를 연산에 참여하는 process 수를 조절하면서 적절하게 조정할 수 있다는 것이다. 단순히 apply를 사용하거나 전체 process를 다 사용하는 것이 아니라, concat 과정에서의 overhead와 비교해가면서 적절한 수준의 병렬처리를 해주면서 조절할 수 있다.

+ Recent posts