반응형

항상 문제에 봉착하기 전에는 내가 모르는 것이  뭐인지 모르게 된다. 항상 Batch Normalization은 당연하게 사용하였지, 그 의미에 대해서 대략적으로만 알고 있었던 것 같아서, 이번 기회에 Batch Normalization의 논문을 읽으면서 기본기부터 다시 쌓고자 한다.


Batch Normalization 배경 설명

  • Batch Normalization은 딥러닝을 접해본 사람이면, 누구나 알 것이다. Batch Normalization은 2015년 구글에서 ICML에 발표한 논문이다.
  • Internal Covariate Shift 문제를 정의하고, 이를 해결하기 위한 mini batch 단위의 Normalization 방법에 대해서 제안한다.

Abstract

  • DNN의 학습 과정에서 앞선 layer들의 parameter 변화에 따라, 뒷 layer들의 input의 분포가 변하기 때문에 학습이 매우 어렵다.
  • 이 현상으로 인해 학습의 learning rate를 크게 가져가지 못하고, parameter 초기화를 신중히 해야 한다. 따라서, 비선형성 모델의 학습을 느리게 만든다.
  • 이 논문에서는 이 현상을 "internal covariate shift"라 부르고, 이를 해결하기 위해, layer의 input을 normalize 하는 방법을 사용한다.
  • 이 모델에서는 normalization을 model 아키텍처의 일부분으로 각 training mini-batch마다 normalization을 수행하는 방법을 사용한다.
  • Batch Normalization을 통해, 더 높은 learning rate를 사용할 수 있고, 모델이 parameter 초기화에 덜 민감해지게 된다. 또한, regularizer로 작동하기 때문에, 일부 상황에서는 Droupout을 대체하기도 한다. 
  • 그 당시 SOTA image classification model에 적용하여, Batch Normalization은 training step을 14 단계나 줄이면서 비슷한 accuracy를 달성하였다. 

 

Introduction

[배경]

  • stochastic gradient descent(SGD)는 deep network 학습에 효과적인 방법이라는 것이 밝혀졌다. SGD의 변형 버전들은 SOTA 성능을 이끌고 있다.
  • SGD는 parameter를 optimize 하기 위해 loss를 minimize 한다. SGD에서 학습은 단계적으로 이뤄지고, 각 학습의 단계는 mini batch로 여겨진다. 이 mini-batch는 loss function의 gradient를 추정하기 위해, 아래와 같은 식을 사용한다.

  • mini-batch들을 사용하면, 데이터를 하나씩 학습하는 것에 비해 몇 가지 장점이 있다.
    1. mini-batch를 통해 구해진 loss의 gradient를 통해, training set의 gradient를 추정할 수 있다. 
    2. bach 계산은 병렬 연산을 이용하기 때문에 각 example을 m번 연산하는 것보다 효과적이다. 

[문제]

  • 이렇게, SGD는 간단하고 효과적이지만, model hyper-parameter를 신중하게 tuning 해야 한다. 특히, 앞선 layer들의 parameter 변화가 뒷 layer들에 영향을 미치기 때문에, model parameter나 learning rate 같은 hyper parameter 선택 시 주의를 요한다. 
  • 특히, layer input의 분포 변화는 layer들이 새로운 분포를 계속 처리해야 하는 문제를 야기한다. 이러한 input 분포의 변화를 covariate shift라 한다. 예를 들어, 아래와 같은 gradient descent step 연산이 있을 때, x (input 값)이 고정된다면, parameter는 x의 새로운 분포에 적응하는데, 연산을 낭비하는 것 없이, parameter optimize에 집중할 수 있다.

  • 추가적으로, 이런 고정된 input의 distribution은 다른 장점을 가진다. 예를 들어, sigmoid를 생각해 보았을 때, sigmoid의 input의 절댓값이 커지면(큰 음수값을 가질 때), sigmoid의 값은 0에 가까워진다. 이는, 학습의 속도를 늦추게 된다. 이러한 현상은 모델의 깊이가 깊어질수록 가속화되는데, 이러한 현상(staturation problem으로 야기된 gradient vanishing)을 실제 학습에서는 ReLu function과 좋은 parameter initialization, 또는 작은 learning rate 적용을 통해 방지하곤 한다.
  • 하지만, 이러한 비선형의 input들이 조금 더 안정적인 분포를 띄게 된다면, optimizer는 saturate에 덜 빠지게 되어, 학습이 가속화될 것이다.
  • 이 논문에서는 deep network의 internal node의 분포 변화를 "internal Covariate Shift"라고 명명하고, 이것을 없애면 더 빠른 학습이 된다는 것을 밝힌다. 

[Batch Normalization] 

  • 이를 위해, "Batch Normalization"이라는 방법을 소개한다. 이 방법은 input layer들의 평균과 분산을 고정된 값으로 normalization 하는 과정을 포함한다. 
  • Batch Normalization을 사용하면, parameter들의 초깃값의 scale에 따른 gradient scale의 민감도를 줄여줘서, divergence 위험 없이 더 큰 learning rate를 선택할 수 있게 해 준다. 
  • 추가적으로 Batch Normalization은 model을 regularize 해줘서, Dropout의 필요성을 줄여준다.

 

Internal Covariate Shift를 줄이기 위한 방법

  • 이전부터, input을 whitned(평균 0, 표준편차 1의 값)하게 되면 network training이 빠르게 수렴된다는 것이 알려져 있었다.
  • whiening을  모든 학습 과정이나, 일정한 interval 단위로 network를 직접 바꾸거나, optimization algorithm을 활용한 parameter 변경을 할 수 있지만, 이러한 변화들이 optimization 단계에서 gradient step의 영향을 줄여, 학습의 속도를 저하시킬 수 있다. (normalization이 되기 때문에) 
  • 만약, network가 항상 원하는 형태의 분포를 갖게 된다면, loss에 대한 model parameter의 gradient가 normalization을 고려하고, model parameter에 대한 의존성을 동시에 고려할 수 있게 된다. 
  • x가 layer input vector이고, X가 training dataset의 전체의 input vector 집합이라고 할 때, normalization은 다음과 같이 계산될 수 있다

  • 이 식의 backpropagation을 위해서 Jacobian을 계산해야 하는데, 아래 식에서 두 번째 term을 무시하면 explosion이 일어날 수 있다. 이 framework에서 layer의 input을 normalization 하기 위해서는 매우 큰 비용이 드는데, covariance matrix를 구해야 하기 때문이다. 

  • 이를 해결하기 위해, 저자들은 미분 가능한 다른 대안을 생각해 보았고, 앞서 통계적 방법이나 feature map들을 사용한 방법들이 있었지만, network의 성능을 떨어뜨릴 수 있기 때문에, network의 정보를 유지하면서 전체 training data의 통계값에 대한 trainign example을 normalization 하는 방법들을 고안하게 된다.

 

Mini-Batch 통계값을 이용한 Normalization

  • 각 scalar feature들을 독립적으로 평균 0, 분산 1을 갖도록 normalize 한다. input들의 모든 dimension을 아래와 같이 normalization 한다.

  • 단순히, 위처럼 input을 normalization 하면, 각 layer의 output 값들이 변경될 수 있기 때문에, 아래처럼 original model param을 구해 준다. 이때, gamma과 beta는 학습가능한 parameter이다. 즉, normalization 한 값을 original 값으로 돌리기 위한 parameter들을 학습하는 것이다. 

 

  • Normalize를 위해 전체 training set을 모두 통계내는 것은 비효율적이다. 따라서, mini-batch의 개념을 이용하여, 각 mini-batch에서 평균과 분산을 계산한다. (즉, batch 내에서 각 dimension에 대한 평균과 분산을 구한다.)
  • 이 과정을 Batch Normalization이라고 하고, 그 자세한 과정은 아래와 같다.

  • Batch Normalization은 단순히 하나의 batch 내에 있는 training example들에 의존하는 것이 아니라, 다른 mini-batch들을 고려할 수 있게 된다. 이를 통해, 모델의 학습 과정에서는 항상 평균이 0이고, 분산이 1인 input, 즉 고정된 distribution이 사용되게 된다. 

 

CNN에서 Batch Normalization

  • Batch Normalization은 CNN에도 적용할 수 있는데, 아래와 같은 식이 있다고 할 때, (u: layer input, g: activation function, W: weight, b: bias) Convolutional layer 직후 (activation function 전)에 Batch Normalization을 걸어 줄 수 있다.

  • BN을 model input인 u에 걸어주지 않는 이유는, model input은 다른 nonlinearity의 output이기 때문에 (전 layer의 activation function의 결과이기 때문에) 분포가 학습과정에서 계속 바뀔 수 있기 때문이다. 반면에, convolutional layer 직후(Wu+b)는 더 대칭적이고, non-sparse 한 분포를 띌 가능성이 높고, 더 "Gaussian"하기 때문에 조금 더 안정적인 distribution을 만들 수 있다고 한다. b는 합항이기 때문에 당장 고려하지 않아도 된다. (어차피 학습될 Beta가 해당 값을 포괄할 수 있기 때문에) 따라서, layer의 output은 다음과 같이 나타낼 수 있다.

  • convolutional layer에서는 BN을 위해, 각 mini batch에서 모든 위치에 걸쳐 공통적으로 정규화하는 방법을 사용하고 있다. (즉, 기존은 dimension 단위의 normalization이었다면, 이번엔 feature의 spatial 부분까지 고려한 normalization이다.) 따라서, learned parameter인 gamma와 beta도 feature map의 크기를 갖는다. 

 

Batch Normalization으로 인한 high learning rate 사용 가능

  • 기존 deep network에서 큰 learning rate를 적용하면  gradient의 explosion이나 vanish가 일어나게 되어, local minima에 빠지게 된다.
  • Batch Normalization은 parameter의 변화로 인한 gradient의 큰 변화를 방지하여, high learning rate를 사용 가능하게 해준다.

 

실험 결과

[batch normalization으로 인한 high learning rates 사용 가능]

  • batch normalization으로 인해 큰 learning rates를 사용할 수 있게 되었다. 

 

[ImageNet Classification]

  • 아래 그래프의 가로축은 training step이고 세로축은 validation accuracy이다. Batch Normalization을 이용하면 더 적은 step에서 좋은 성능을 발휘하는 것을 확인할 수 있다.

 

 

Reference

Ioffe, Sergey, and Christian Szegedy. "Batch normalization: Accelerating deep network training by reducing internal covariate shift." International conference on machine learning. pmlr, 2015.

 

총평

  • 당연히 Batch Normalization에 대해 이해하고 있다고 생각했고, 따로 논문 찾아볼 생각을 안했는데, 읽기를 잘했다.
  • 논문이라는 게 문제를 정의하고 푸는 과정이기 때문에, "왜?" 쓰는지를 깊이 알려면, 논문을 직접 읽는 게 나은 것 같다.
반응형

DETR 배경 설명

  • DETR은 2020년 Facebook AI 팀에 의해 발표된 논문이다. 
  • Transformer를 Object Detection 분야에 최초로 적용한 논문이다.

 

Abstract

  • 이 논문에서는 한 번에 물체의 위치와 classification을 진행할 수 있는 DETR이라는 새로운 네트워크를 소개한다.
  • 기존에 Object Detection에서 존재하던 NMS(Non-maximum suppression)이나, anchor box 생성 같은 manual 작업들을 제거한 detection pipeline을 구성하였다.
  • DETR의 주요 아이디어는 bipartite matching을 통한 unique predictions를 강제하는 "set-based global loss"와 transformer의 encoder-decoder 구조이다.
  • object query가 있을 때, DETR은 object들과 전체적 이미지의 context 간의 관계를 추론하고, 이 정보를 기반으로 최종 예측을 병렬적으로 수행한다.
  • 새로운 모델은 구조적으로 간단하고, 별도의 라이브러리를 사용하지 않는다는 장점이 있다.
  • DETR은 accuracy와 run-time 측면에서 Faster R-CNN 기반의 최적화된 모델들에 버금갈 정도의 좋은 성능을 보인다. 

 

Introduction

[배경]

  • 기존 Object detection 방법들은 미리 구성해 놓은 anchor box를 이용하여, 많은 object들의 후보군을 만들어 놓고, 이를 regression과 classification을 통해, 예측하고, 비슷한 예측결과를 지우는(NMS) 과정들을 통해 진행된다.
  • 이러한 과정을 간단하게 하기 위해, 이 논문에서는 이미지에서 직접 물체의 위치를 추론하는 방법으로 접근하고자 한다.
  • 이미 음성인식등의 다른 분야에서는 이러한 end-to-end 방식이 성공을 거두었지만, object detection 분야에서는 이러한 시도들이 prior knowledge를 다른 방식으로 사용하거나, 성능 상에서 경쟁력이 없었다. 

[소개]

  • 이를 위해, 이 논문에서는 object detection의 학습 과정을 direct set prediction problem으로 접근한다. 
  • 논문에서는 transformer의 encoder-decoder 구조를 사용하였다. transformer의 self-attention 메커니즘은 비슷한 예측들을 제거하는데 적절한 constraints가 될 것이다. (아마, NMS를 대체할 수 있을 것이다.)
  • DETR은 모든 object를 한번에 예측하고, 예측값과 ground-truth object들 간의 bipartite matching을 이용한 set loss function을 통해 end-to-end로 학습된다.
  • 이를 통해서, 기존 object detection에서 prior knowledge를 담은 anchor box나 NMS 등을 제거할 수 있었다. 

[기존 set prediction과의 비교]

  • 기존의 direct set prediction과 비교하면, DETR은 bipartite maching loss와 transformer를 이용한 parallel decoding의 결합이 차이다. 
  • 기존 방식들은 RNN 방식에 의존하여, 예측 object들의 배열에 의존하는 것에 반해, DETR은 transformer를 이용한 parallel 연산으로 예측 object들의 배열에 의존하지 않는다.

[실험]

  • DETR을 가장 많이 사용되는 object detection의 dataset인 COCO에서 Faster R-CNN 기반의 좋은 성능 모델들과 비교해 보았다.
  • 최근(그 당시) 모델들은 최초 Faster R-CNN에서 구조 변화를 통해 성능이 크게 향상되었음에도 불구하고, DETR은 그들과 필적할만한 좋은 성능을 보였다.
  • 조금 더 상세하게는, DETR은 large object들에 대해서는 더 좋은 성능을, small object들에 대해서는 더 낮은 성능을 보였다. 

 

 

DETR Model

  • DETR의 핵심요소는 (1) 예측과 ground truth 간의 unique matching을 만들어주는 set prediction loss와 (2) object 집합들을 예측하고, 그들 사이의 관계를 modeling 하는 아키텍처이다. 

[object detection set prediction loss]

<1번째 : matching 구하기>

  • 첫 번째로, DETR은 고정된 개수 N개에 대한 예측을 한다. 이때, N은 일반적인 이미지의 object 개수보다 큰 숫자이다. 
  • 이러한 구조의 학습과정에서 주요 이슈는 예측된 object를 ground truth와 어떻게 비교하냐이다. 이를 위해, 예측과 ground truth 간에 최적의 matching을 만드는 loss를 사용하였다.
  • y가 object들의 ground truth이고, y는 no object의 class도 가질 수 있다고 할 때, 예측과 ground truth 간의 bipartite matching을 갖는 조합을 찾기 위해 아래 식을 계산한다.

  • 위 식에서 L_match는 pair-wise matching cost인데, 이 는 class의 prediction과 예측과 ground-truth 간의 box 차이를 모두 고려한 cost이다.
  • 이러한 방식은 기존 detector들의 방식의 anchor 등과 유사한데, 그들과의 차이점은 기존 detector에서는 비슷한 예측들이 많이 존재하는데, matching loss에서는 1대 1 매칭이 된다는 것이다.

<2번째 : loss 구하기>

  • 두 번째로, 앞서 구했던 전체 matching 쌍들에 대해 Hungarian loss function을 계산한다. 이때의 loss는 일반적인 object detection의 loss와 비슷하게, class prediction에 대한 loss와 box loss의 합이다. 

  • 위의 식에서 우선 no object class의 matching cost는 prediction의 영향을 받지 않는다. (상수이기 때문에 학습 과정에서 영향을 미치지 않는다.)
  • 위의 식에서 box loss를 구성할 때, 기존 object detection의 box loss와 달리, loss의 scale에 대한 이슈가 있다. (기존에는 anchor에 대한 상대적 위치를 loss로 썼기 때문에) 이를 해결하기 위해, box loss로 주로 사용되는 L1 loss 뿐 아니라, IoU loss를 추가적으로 사용했다. 
  • 따라서 box loss는 아래와 같다. 

 

 

[DETR 아키텍처]

  • DETR의 전체 아키텍처는 아래 그림에서 보이는 것처럼 매우 간단하다. 

  • DETR은 크게 3가지 주요 구조로 구성되어 있다. (1) compact feature를 뽑기 위한 CNN backbone (2) encoder-decoder transformer (3) 최종 예측을 위한 feed forward network(FFN)
  • DETR은 Pytorch로 50줄 미만으로 inference code를 짤 수 있을 만큼 간단하다.
  • Backbone : 이미지를 CNN backbone을 통과시켜, feature를 얻는다. 일반적으로 2048 X (H/32) X (W/32) shape의 feature를 뽑았다.
  • Transformer encoder : 우선 1X1 convolution을 통해, channel dimension을 줄여줬다. encoder의 input으로 sequence 값이 필요하기 때문에, spatial 부분(H와 W)을 HW개의 sequence로 만들었다. encoder는 일반적인 transformer의 encoder이다. transformer 구조는 배열 순서에 무관하게 연산되기 때문에, 고정된 positional encoding 값을 더해서 이를 보충했다.
  • Transformer decoder : decoder도 일반적인 transformer의 구조를 따랐다. 조금 다른 점은 DETR은 N개 object를 병렬적으로 각 decoder layer에서 연산하였다는 점이다. decoder도 마찬가지로 배열 순서에 무관하게 연산되기 때문에, positional encoding을 decoder의 각 attention layer에 추가하여 연산한다. decoder에 의해 N개의 object query들은 output embedding으로 변환되고, 각각 독립적으로 FFN에 의해 box coordinate와 class label을 예측하게 된다. 
  • FFN : 최종 prediction은 ReLU activation을 사용하는 3개의 layer에 의해 연산된다. FFN은 box의 중심 좌표와 height, width와 class label을 예측한다. 최종 N개의 결과가 나오는데, 이중 no object를 제외하고, 나머지들이 DETR의 최종 예측이 된다. 
  • Auxiliary decoding loss : auxiliary loss를 사용하는 것이 decoder의 학습 중 각 class의 정확한 object 개수를 추정하는 데에 도움이 된다는 것을 발견했다.  모든 prediction FFN은 parameter를 공유한다. 

 

 

Experiments

  • COCO dataset에서 Faster-RCNN 기반의 최근(그 당시) object detection model과 필적할 정도의 좋은 성능을 보인다. 

  • 그밖에 encoder size나, selt-attention, segmentation에 적 등의 실험이 있다. 논문을 참조 바란다.

 

Reference

Carion, Nicolas, et al. "End-to-end object detection with transformers." European conference on computer vision. Cham: Springer International Publishing, 2020.

 

 

총평

  • Transformer가 당연(?)하게도 OD 분야에 적용되었다. 2020년에 발표되어 늦은 감이 있지만, 지금이라도 읽어서 다행이다.
  • Object Detection을 처음 접했을 때, anchor box를 사용하는 것과 NMS를 사용하는 것이 매우 비효율적이라고 생각하면서도 어쩔 수 없다고 생각하였는데, 결국 해결되는 것 같다. 
  • OD 논문을 열심히 읽어야겠다.
반응형

Python으로 데이터를 다루는 업무에서 Pandas는 거의 필수적이다. Pandas는 쉽고, 빠르고(Python에 비해), 유용하고, 간편하기 때문이다.  또한, 테이블 형태의 구조를 가지고 있는 pandas DataFrame은 매우 친숙하고, 안정적인 느낌을 준다. 평소에는 Pandas의 사용하는 기능만 사용하고, 필요한 기능이 있으면 그때그때 알아보면서 사용하고 있는데, 이번 기회에 Pandas의 함수를 정리해보고자 한다. 

(사실 pandas의 전체 함수를 정리하는 것은 거의 불가능하다. 내 기준으로 많이 사용하고 유용할 것 같은 함수를 정리했다. 필요한 함수가 있다면 https://pandas.pydata.org/docs/reference/index.html 를 직접 참조하는 것이 좋을 것이다.)


Pandas의 데이터 구조

  • Series : Pandas의 series는 1D array와 유사한 데이터구조로, index와 value로 구성되어 있다. Series가 1D의 형태를 가지고 있다 보니, axis가 1개 존재하는데, 이 axis를 index라고 한다. value로는 ndarray나, dictionary, 스칼라 값들이 들어갈 수 있다.
  • DataFrame : DatFrame은 2D의 데이터 구조로, SQL 테이블과 비슷하게, column과 row로 구성되어있다. DataFrame의 각혈은 Series로, DataFrame은 칼럼명이 key인 Series들의 dictionary로 생각할 수 있다. DataFrame은 Series나 2D ndarray, 다른 DataFrame, 1D 데이터(ndarray, list, Series 등등..)들로 구성된 Dictionary를 통해 만들 수 있다.
  • 기본적으로 DataFrame은 Numpy를 기반으로 만들어졌다. 따라서, Series는 Numpy 1D 배열(ndarray)과 동일하다고 생각하면 된다. 때문에, numpy에 있는 거의 모든 함수들이 Pandas 데이터 구조에 호환된다.  

 

Pandas의 함수

[Indexing/Selection]

  • Indexing/Selection은 Python의 리스트처럼 매우 직관적이여서, 공식 사이트의 부분으로 설명을 대체한다.

  • query : 위에서 나온 DataFrame 기본 indexing 말고도, SQL 형식으로 DataFrame에서 조건에 맞는 row를 가지고 올 수 있다. 
import seaborn as sns

if __name__ == '__main__':
    titanic_data = sns.load_dataset("titanic")
    print("[query 출력값]")
    print(titanic_data.query('age > 30'))

 

  • between : 특정 숫자형 열의 범위 내의 데이터를 filtering하기 위해 사용할 수 있는 함수이다. 
import seaborn as sns

if __name__ == '__main__':
    titanic_data = sns.load_dataset("titanic")
    print("[between 출력값]")
    print(titanic_data[titanic_data['age'].between(30,40)])

 

[데이터 연산]

  • 사실 Series와 DataFrame이 많이 사용되는 이유이기도 한데, Series나 DataFrame 끼리 연산이 된다.
  • 단순 연산이 간단할 뿐 아니라, 간단한 연산 위주로 수행한다면, 속도가 매우 빠르다. (Pandas는 numpy로 구성되고, numpy는 C로 변환하여 연산을 하기 때문에)
import pandas as pd
import numpy as np

if __name__ == '__main__':

    df1 = pd.DataFrame(np.arange(10))
    df2 = pd.DataFrame(np.arange(10)*2)

    print(df1+df2)
    print(df1-df2)
    print(df1*df2)
    print(df1/df2)

 

[데이터 읽기&쓰기]

  • read_csv, read_excel : 각각 csv 형태, excel 형태를 쉽게 읽어서, DataFrame 형태로 반환한다.
  • to_csv, to_execel : DataFrame을 각각 csv, excel 형태로 export 한다.

 

[데이터 요약&추출]

  • head : DataFrame의 처음 N개(인자값) 행을 출력한다. (default:5) 
  • tail :  DataFrame의 마지막 N개(인자값) 행을 출력한다. (default:5) 
  • sample : DataFrame에서 무작위 행 N개를(인자값) 추출한다. (default:1), DataFrame의 행을 무작위로 섞고 싶다면, frac 인자(추출 비율)를 1로 지정하면, 전체 DataFrame이 무작위로 섞여서 나온다. 

  • describe : DataFrame의 칼럼 중, 숫자 칼럼에 대한 통계값(평균, 표준편차, 최솟값, 사분위수, 백분위수, 최댓값)을 표현해 준다.

  • value_counts : Series 객체 내의 unique value의 빈도를 반환한다. Categorical 데이터 개수 파악에 주로 사용한다.

  • dtypes : dtypes는 함수가 아닌 속성이다. 각 열의 데이터 유형을 출력한다. pandas의 DataFrame은 기본적으로 열의 데이터 유형을 추정하는 방식을 사용하기 때문에, 추정한 유형을 알기 위해 출력이 필요하다.
  • info : DataFrame의 각 칼럼 값과, Index 범위, not null 인자 개수, 데이터 타입, 메모리 정보를 출력해 준다. 

 

import seaborn as sns

if __name__ == '__name__':
    titanic_data = sns.load_dataset("titanic")

    print("[head 출력값]")
    print(titanic_data.head(5))
    print("[tail 출력값]")
    print(titanic_data.tail(5))
    print("[sample 출력값]")
    print(titanic_data.sample(5))
    print("[describe 출력값]")
    print(titanic_data.describe())
    print("[value_counts 출력값]")
    print(titanic_data.value_counts())
    print("[dtypes 출력값]")
    print(titanic_data.dtypes)
    print("[info 출력값]")
    print(titanic_data.info())

 

[데이터 결합]

  • combine_first : 두 DataFrame을 결합할 때, 첫 번째 DataFrame이 누락된 경우, 두 번째 DataFrame의 값으로 대체하는 값이다. DataFrame을 이용한 coalesce or nvl 함수라고 보면 된다. 어떠한 DataFrame에 비어있는 값을 다른 DataFrame의 값으로 대체하기 위한 함수이다. 
  • combine : 두 DataFrame을 결합할 때, 개발자가 정의한 함수에 따라서 결합하기 위한 함수이다. combine_first가 단순히 하나의 DataFrame에 없는 값을 대체해 주는 것에 비해, combine은 결합 조건 및 결합 방식을 직접 정의 가능하다.
  • concat : DataFrame을 단순히 쌓는다. axis가 0이면, row에 append 하는 구조로 DataFrame을 결합하고, axis가 1이면 칼럼이 늘어나는 방식이다. 
  • join : SQL에서의 join처럼 두 DataFrame 간의 join을 위해 사용되는 함수이다. 다만, 인덱스 기준으로 DataFrame을 결합하기 때문에, index를 가공하지 않으면 사용이 제한적이다. join 방법을 지정하는 how 인자에 left, right, inner, outer를 지정할 수 있다.(default left)
  • merge : 조금 더, 유연한 방식의 join 방법이다. join은 DataFrame 함수이고, merge는 pandas 함수이기 때문에, join은 인덱스 기준으로만 join이 가능한 반면, merge는 join key를 지정해 줄 수 있다. 인자는 다양하지만, join key를 지정하는 "on", join 방식을 결정하는 "how" 정도만 사용하면 된다. "how" 인자로는 join과 마찬가지로  left, right, inner, outer를 지정할 수 있지만, default 값이 inner join이다. 
import pandas as pd
import numpy as np

if __name__ == '__main__':
    df1 = pd.DataFrame({'data': [1, 2, np.nan, 4, 5, 6]}, index=[np.arange(6)])
    df2 = pd.DataFrame({'data': [0, 0, 5, 0, 0, 0]}, index=[np.arange(6)])

    df1.columns = ['key']
    df2.columns = ['key']

    combine_df1 = df1.combine_first(df2)
    combine_df2 = df1.combine(df2, lambda x, y: x ** 2 + y ** 2)
    concat_df1 = pd.concat([df1, df2], axis=0)
    concat_df2 = pd.concat([df1, df2], axis=1)
    join_df = df1.join(df2, how='inner',lsuffix='left', rsuffix='right')
    merge_df = pd.merge(df1, df2, on='index', how='inner')

    print("[combine_first 출력값]")
    print(combine_df1)
    print("[combine 출력값]")
    print(combine_df2)
    print("[concat 행 방향 출력값]")
    print(concat_df1)
    print("[concat 열 방향 출력값]")
    print(concat_df2)
    print("[join 출력값]")
    print(join_df)
    print("[merge 출력값]")
    print(merge_df)

 

 

[데이터 변환]

  • apply : pandas에서 가장 유용한 함수라고 생각한다. 인자로 함수를 넣어주면, DataFrame의 각 행에 함수를 적용할 수 있다. 일반적으로 간단한 연산들은 단순 반복문보다 apply로 처리하는 게 속도적으로 유리하다.
  • fillna : DataFrame이나 Series의 누락된 값을 다른 값으로 채울 수 있다. 보통 결측치를 보정하는 데 사용한다.
  • replace : DataFrame의 특정 값을 다른 값으로 변환해 준다.
  • map : dictionary 형태의 mapping을 통해, DataFrame 내의 값을 다른 값으로 mapping 해줄 수 있다. 
import seaborn as sns

if __name__ == '__main__':
    titanic_data = sns.load_dataset("titanic")

    print("[apply 출력값]")
    print(titanic_data['fare'].apply(lambda x: (x+10)*2))
    print("[fillna 출력값]")
    print(titanic_data['age'].fillna(titanic_data['age'].mean()))
    print("[replace 출력값]")
    print(titanic_data['sex'].replace('male', 1))
    print("[map 출력값]")
    print(titanic_data['sex'].map({'male':1, 'female':2}))

 

[집계 함수]

  • mean, max, min, sum, median, std, count : pandas의 DataFrame은 Table 형태의 구조를 띄고 있기 때문에, 집계가 매우 용이하다. DataFrame의 특정 칼럼을 지정하고, 해당 함수를 사용하면, 칼럼의 통계값을 쉽게 얻을 수 있다. 
  • agg : 집계를 한 번에 적용할 때, 쉽게 사용할 수 있다. 보통 groupby와 함께 자주 사용된다. axis가 0이면 칼럼별로, 1이면 행별로 함수를 적용한다.
  • cut : 연속형 데이터를 구간으로 나누는 데 사용된다. 
if __name__ == '__main__':
    titanic_data = sns.load_dataset("titanic")

    print("[max 출력값]")
    print(titanic_data['fare'].max())
    print("[agg 출력값]")
    print(titanic_data['fare'].agg(['max', 'min', 'mean'], axis=0))
    print("[cut 출력값]")
    bins = [0, 18, 35, 50, 100]
    labels = ['미성년자', '청년', '중년', '노년']
    print(pd.cut(titanic_data['age'], bins=bins, labels=labels))

 

 

[정렬 함수]

  • sort_values : SQL의 orderby 함수처럼 특정 칼럼의 값에 따라 정렬할 수 있다. 기본적으로 오름차순으로 정렬한다."by" 인자에 열을 명시해줘야 한다.
  • sort_index : Index 기반으로 DataFrame을 정렬한다. 마찬가지로 기본적으로 오름차순으로 정렬한다.
  • nsamllest, nlargest : 각각 DataFrame 내에서 가장 큰 값, 가장 작은 값 N개를 가진 행을 찾는 데 사용된다. 
if __name__ == '__main__':
    titanic_data = sns.load_dataset("titanic")

    print("[sort_values 출력값]")
    print(titanic_data.sort_values(by='age'))
    print("[sort_values 출력값]")
    print(titanic_data.sort_values(by='age'))
    print("[sort_index 출력값]")
    titanic_data = titanic_data.sample(frac=1)
    print(titanic_data.sort_index())
    print("[nlargest 출력값]")
    print(titanic_data["age"].nlargest(5))
    print("[nsamllest 출력값]")
    print(titanic_data["age"].nsmallest(5))

 

 

[그 밖 유용한 함수들]

  • groupby : SQL의 groupby처럼 DataFrame을 grouping 하여, group 별 집계, 연산에 사용하는 함수이다. groupby key를 지정해 주면, 해당 기준으로 grouping 된다. 
if __name__ == '__main__':
    titanic_data = sns.load_dataset("titanic")

    group_titanic_data = titanic_data.groupby('sex')
    print(group_titanic_data)
    print(group_titanic_data["fare"].max())
    print(group_titanic_data["fare"].mean())
    print(group_titanic_data["fare"].min())
    print(group_titanic_data["fare"].agg(['max', 'mean', 'count']))

  • explode : SQL의 Unnest와 같다. DataFrame 내에 List 또는 Series로 저장된 요소들을 분해하여 풀어내는 데 사용된다. 길이가 같은 여러 칼럼의 list를 한 번에 풀 수 있는 게 매우 유용하다. (spark에선 이 기능이 직관적이지 않다.)
if __name__ == '__main__':
    titanic_data = sns.load_dataset("titanic")

    group_titanic_data = titanic_data.groupby('sex')
    # group by를 통해 각 age 요소를 list 형태로 만듬
    group_data = group_titanic_data['age'].agg(list).reset_index()
    print(group_data)
    print(group_data.explode('age'))

 

 

반응형

나는 업무 중에 Oracle Database(이하, Oracle)를 직접적으로 사용하지는 않는다. 간접적으로나마 Oracle을 사용 중인데, 그마저도 이젠 더 사용하지 않게 될 것 같다. 그럼에도 Oracle에 대해 알아보기로 결심한 건, Oracle의 내부 동작이 굉장히 복잡하고, 체계적이기 때문에 이번 기회에 공부해 놓으면, 다른 DB나 데이터 처리 동작 관련하여 더 잘 이해할 수 있게 될 것 같기 때문이다.
전문가가 아니라, Oracle의 내부 동작은 거의 모르고, Oracle 동작에 대한 별도의 서적을 찾지 못해, Release Note를 보면서 공부하기로 했다.
 


Introduction

[Database]

  • Database는 Application 들이 사용할 수 있게, information을 수집하고, 저장하고, 검색하기 위해 존재한다.
  • Database Management system(DBMS)은 위의 Database의 목적을 충족할 수 있도록 조정하는 소프트웨어이다.

[Relational Model]

  • 1970년에 "A Relational Model of Data for Large Shared Data Banks"에서 E.F.Codd란 사람이 수학 집합론에서 찾아 하여 relational model 개념을 제안했고, 이것이 요즘 Database 들에서 채택되어 relational database의 형태로 사용 중이다.
  • relational model은 아래와 같은 특징을 갖는다.
    • Structure : Database에 저장 및 접근 가능하도록 잘 정의된 object
    • Operations : Application이 data를 잘 조작할 수 있도록 명확하게 정의된 action
    • Integrity rules : 무결성 규칙을 지키면서, Database 직접을 제어

[RDBMS]

  • Relational model을기반으로, relational database management system(RDBMS)가 만들어졌다. RDBMS는 데이터를 database 안에 넣고, 데이터를 저장하고, application이 조정 가능하도록 데이터를 검색할 수 있도록 한다.
  • RDBMS는 2개 type의 operation을 지원한다.
    • Logical Operation : Application이 필요한 내용을 지정하는 Operation. 즉, application에서 데이터를 요청하거나, 데이터베이스에 정보를 추가하는 경우
    • Physical Operation : RDBMS가 어떻게 작업을 수행할지를 결정하고, 그 작업을 수행하는 것. 즉, application이 요청한 작접을 처리하기 위해, 어떻게 데이터를 찾고, 어느 메모리에서 읽을 것인지 등등의 과정을 포함함. Application 단에서는 해당 작업을 신경 쓸 필요가 없어야 함. 
  • Oracle Dtabase도 1977년 개발된 RDBMS이다. 

 

Oracle 특징

[Schema 구조]

  • 앞서 언급한 대로, RDBMS에서는 Logical Operation과 Physical Operation을 분리하기 위해, Logical Data Structure와 Physical Data Storage는 분리되어야 한다. 
  • 이를 위해, Oracle Database는 logical data structure들의 집합인 schema를 사용하고, 각 user들이 schema를 소유하는 방식을 사용한다.
  • Database는 다양한 schema를 지원하는데, 가장 중요한 것은 Table과 Index이다. 
    • Table : Column과 Row로 이루어진 2D 형태의 데이터. user는 Table에 무결성 제약 조건을 지정할 수 있다. 
    • Index : Data 검색을 빠르도록, row의 위치 정보를 일정 규칙 형태로 가지고 있는 것. Table마다 하나 이상의 index를 생성할 수 있다. Index는 logical 영역이기 때문에, 실제 데이터의 위치를 변경하지 않음. 따라서, Index를 지워도 다른 부분에 영향을 미치지 않음.

[Transaction 관리]

  • Oracle database는 여러 user가 사용할 수 있도록 설계되었다. 따라서, database는 여러 user의 요청을 다른 user들의 data에 영향을 미치지 않도록 유지하면서, 동시에 처리해야 한다.
  • 이를 위해, Oracle에서는 Transaction의 개념을 사용한다. Transaction은 1개나 그 이상의 SQL 질의문에 대한 logical, atomic unit이다.
  • RDBMS는 이러한 transaction을 database에 모두 적용(commit)되거나, 취소(rollback) 되도록 할 수 있어야 한다. 즉, data 처리를 위한 여러 SQL들을  묶음으로 묶어, 데이터 무결성을 유지하는 것이다. 갑작스러운 장애로 인해, transaction 내에 존재하는 하나의 SQL이 수행되지 않는다면, database는 해당 trasaction에 속한 모든 SQL을 rollback 하여 데이터 무결성을 유지한다. 

[Data 동시성]

  • Data 무결성을 유지하기 위해, DBMS는 데이터 동시성에 대한 처리가 필요하다. 
  • Oracle에서는 OS나 C언어처럼,Lock의 개념을 사용하여 데이터 동시성을 제어한다. 
  • Lock은 공유 리소스에 접근하는 transaction 간에 충돌을 방지하여, 데이터 무결성을 보장하면서 최대한의 동시 접근을 허용한다. 

[Data 일관성]

  • Data 일관성은 database에 접근한 사용자가 data에 대해 일관된 내용을 볼 수 있어야 하는 것을 의미한다
  • Oracle database에서는 항상 statement-level의 read 일관성을 시행한다. 이것은 하나의 query가 반환하는 데이터가 단일 시간 시점에서 commit 되고, 일관성을 보장한다는 것을 의미한다.
  • Database는 tansaction 수준에서 read 일관성을 제공한다. 이 경우에는 transaction 내의 문장이 동일한 시간 시점에서 data를 볼 수 있다. 

→ Sub Query를 이용한 SQL이 처리에서 각기 다른 sub query에서 동일 데이터를 조회했을 때, 실제 physical 단위의 수행 시점에 따라 data가 달라지지는 않는다는 것을 의미한다. 
 
 

Oracle Database 아키텍처

  • database server가 information 관리에 키다.
  • 일반적으로 server는 위에서 언급한 Oracle 특징인, 여러 사용자가 동시에 동일 데이터에 안정적으로 접근할 수 있도록 대량의 데이터를 관리해야 한다. 
  • database server는 또한, 보안적인 관점과 failure recovery에 대해 효과적인 solution을 제공해야 한다.

[Database and Instance]

  • Oracle Database Server는 하나의 database와 한 개 이상의 database instance로 구성된다. 
  • 일반적으로 database와 instance는 밀접하게 연관되어 있어, Oralce database는 둘을 포함하는 말이지만, 엄격하게 구별하면 다음과 같다.
    • Database : Database는 data 저장을 목적으로 disk 상에 위치한 file들의 집합을 의미한다. 이 file들은 실제로 data를 저장한다. database file은 database와 독립적으로 존재할 수 있다.
    • Database instance : Database file을 관리하는 역할을 한다. Instance는 공유 메모리 영역인 system global area(SGA)와 background process들을 포함한다. instance는 datavbase file과 독립적으로 존재한다.
  • Oracle Database와 instance의 구조도는 아래와 같다. 
    • Client의 요청에 의해, SQL과 Transactuion을 처리하는 역할을 하는 Server Process가 존재하는데. 이 sever process가 사용하는 메모리 공간이 program global area(PGA)이다. 

 
 
엄격하게 구분하자면, Oracle database의 Physical 구조와 Logical 구조를 어떻게 연결하느냐에 따라 2가지 아키텍처가 존재하는데, 각가 Multitenant Architecture과 Sharding Architecture이다.
 
[Multitenant Architecture]

  • Multitenant Architecture는 Oracle database가 하나의 physical database에서 다중 테넌트 공유할 수 있도록 하는 것이다. 

※ 테넌트란? : Database 시스템 내에서 독립적으로 관리되는 하나의 사용자 또는 Application을 나타냄. 다중 테넌트에서는 여러 테넌트가 하나의 Database 시스템을 공유하지만, 각 테넌트는 자체 데이터 및 application을 logical 하게 격리하여 사용함. 

  • 이 아키텍처의 가장 큰 장점은 장점은 관리 machine 수가 줄어든다는 것이다. 장점을 세부적으로 보면 다음과 같다.
    • 하드웨어 비용 감소 : 여러 대 physical server에서 운영되는 여러 database를 단일 machine의 단일 database로 통합 가능
    • Data와 Code 이동이 쉬움 : 단일 machine에 존재하기 때문에, 데이터 마이그레이션에 추가적인 network I/O 등이 들지 않는다.
    • Physical Database 관리 편이 : 관리 machine의 수가 줄어, 관리가 용이하다.
    • Data와 Code를 분리
    • 권한과 역할 분리

 
[Sharding Architecture]

  • Oracle Sharding은 여러 Oracle database들에 수평적인 분할을 통해 database를 scaling 하는 방법이다. Application 단에서는 이 database pool을 하나의 logical database로 인식한다.
  • 이 아키텍처의 장점은 선형 확장과 장애 격리이다. 
  • Sharding 아키텍처에서 각 database는 고유한 server에 호스팅 되며(physical 분리) 각 database는 다른 machine이다. 이때 각 database를 shard라 부린다. 
  • 수평 분할 시, database 테이블을 여러 shard에 각각 나눠 보관하는데, 이러한 방식으로 분할된 table은 shared table이라고 부른다. 

 

 
 

Oracle Database Storage 구조

  • Database 저장 구조는 Dataase를 Physical 및 Logical 관점에서 고려할 수 있다.
  • Physical 저장소와 Logical 저장소는 분리되어 있기 때문에, Pysical 저장소를 관리할 때, Logical 저장소 구조에 영향을 미치지 않고 관리 가능하다.

[Physical Storage 구조]

  • Physical database 구조는 data를 저장하기 위한 file들이다.
  • CREATE DATABASE를 입력하는 순간 아래의 file들이 생성된다.
    • Data File : 모든 Oracle Database는 database의 data를 가지고 있는 하나 이상의 physical data file들을 포함한다. Logical Database 구조는 Data File 형태로 저장된다.
    • Control File : 모든 Oracle Dtabase는 control file을 가지고 있다. control file은 database의 physical 구조를 정의하는, database name 등이나 위치정보 같은 metadata를 포함한다.  
    • Online redo log file: databse에 발생하는 모든 변경 사항을 기록하는 File이다. Transaction Log 기록 등을 보관한다.
    • Parameter File : Database server의 설정과 동작을 제어하는 데 사용한다. 
    • Networking File : Database server와 client 간 통신을 구성하는 데 사용한다.
    • Backup File : Database 복구를 지원하기 위해 사용된다. 일정 간격으로 Database의 복사본을 저장한다.
  • File에는 Offline File과 Online File이 있는데, 두 용어는 File의 상태를 나타낸다.
    • Online File : database가 활성화되어 잇고, 접근 가능한 상태에서 사용되는 File. Database의 일부로 사용되고, Application이 Server와 상호작용하고 Data를 읽고 쓸 수 있는 파일. Database Table, Index, View, Online Redo log File 등이 해당됨.
    • Offline File : Database가 사용 중이 아니거나, 접근 불가능한 상태에서 사용되는 File. 주로 Backup, 복구, Data 이동, File 관리 등의 작업을 수행할 때, Database File을 일시적으로 Offline 상태로 전환할 때 사용된다. Backup File, Archived Redo log file 들 중, 이미 사용된 것은 Offline File에 해당됨.

[Logical Storage 구조]

  • Logical Storage 구조는 Oracle Database가 disk space를 잘 사용할 수 있도록 control 한다.
  • Logical Storage 구조는 다음을 포함한다.
    • Data Block :가장 작은 저장 단위. 하나의 Datablock은 Disk 상의 특정 바이트 개의 영역으로 할당됨. 
    • Extent : 논리적으로 연속적인 Data Block의 특정 개수. 한 번의 할당으로 얻은 것으로 특정 유형의; 정보를 저장하는 데 사용됨. Data의 물리적, 저장 관리를 위한 단위로 사용됨.
    • Segments : user object, undo data, 임시 data 등을 할당하기 위한 extent들의 집합
    • Tablespaces : Database를 Logical Storage 단위로 분할한 것을 의미함. Tablespace는 segment의 logical container로, 각 tablespace는 적어도 하나의 data file로 구성됨. Tablespace를 사용하여, data를 조직하고, segment를 배치하여 database의 성능과 관리 최적화가 가능함.
반응형

LLaVa배경 설명

  • LLaVa는 2023년 NeurIPS 발표된 논문으로, multimodal LLM에 대해 다룬 논문이다.
  • multimodal LLM에 대한 부분도 놀랍지만, 코드와 weight를 open source로 발표하여, 많은 관심을 받고 있다.
  •  https://llava-vl.github.io/ 
 

LLaVA

Based on the COCO dataset, we interact with language-only GPT-4, and collect 158K unique language-image instruction-following samples in total, including 58K in conversations, 23K in detailed description, and 77k in complex reasoning, respectively. Please

llava-vl.github.io

 

Abstract

  • 최근 LLM을 이용해 instruction-following data를 생성하여 instruction 능력을 향상하는 연구들이 많이 이뤄지고 있다. 하지만, multimodal 분야에서는 아직 많이 연구되지 않았다.
  • 이 논문에서는 언어만을 사용하는 GPT-4를 이용하여 multimodal language-image instruction-following 데이터를 만든다. 
  • 이를 이용한 instruction tuning을 통해, 이 논문에서는 LLaVa(Large Language and Vision Assistant)라는 vision encoder와 LLM을 연결한 end-to-end model을 소개한다.
  • 초기 실험에서 LLaVa는 multimodal chat 능력에서 multimodal GPT-4에 대한 85.1%의 상대적 score를 보였다. 
  • Science-QA로 finetuning 하였을 때, LLaVA와 GPT-4의 시너지는 새로운 SOTA인 92.53%의 정확도를 보였다. 

 

Introduction

  • 인간은 vision과 language 등 다양한 채널을 통해 세상을 인지한다. 
  • LLM은 최근 다양한 분야에서 좋은 성능을 내고 있지만, text만을 다룬다.
  • 이 논문에서는 "visual instruction-tuning"을 소개한다. 먼저, instruction-tuning을 multimodal space로 확장하여, vision 분야의 general-purpose를 위한 초석을 쌓는다. 
  • 논문의 Contribution은 다음과 같다.
    • Multimodal instruction-following data : 가장 큰 제약은 vision-language instruction-following data가 적다는 것이다. 논문에서는 data 생성 방법을 소개하고, ChatGPT나 GPT-4를 통해  image-text pair들을 적절한 instruction-following format으로 변환하는 파이프라인을 소개한다.
    • Large multimodal models : open-set visual encoder인 CLIP과 LLaMA를 연결하고, 앞서 만든 데이터를 통해, 이 둘을 end-to-end로 학습한다. 특히, GPT-4로 진행했을 때, Science QA multimodal reasoning dataset에 대해 SOTA 성능을 보였다. 
    • Open_Source : Code와 model checkpoint 등을 모두 공개하였다. 

 

GPT를 이용한 Visual Instruction Data 생성

multimodal instruction following 데이터는 부족한데, 최근 GPT model의 text-annotation task에서 성공에서 영감을 받아, ChatGPT/GPT-4를 이용한 multimodal instruction-following 데이터를 생성하는 방법을 제시한다.

 

  • Image와 그에 대한 Caption이 있는 데이터가 있을 때, 가장 간단한 방법은 GPT-4를 통해 Caption을 해석하고, 그에 대한 Question들을 가지고 질문하는 것이다.

→ 쉽게 생각하면, 아래와 같이 이미지 묘사를 넣고 퀴즈를 내는 것이다.

 

  •  하지만, 이렇게 하면 만들어지는 데이터의 다양성이 떨어지고, 깊이 있는 추론을 포함하는 instruction과 response를 만들지 못한다. 이 현상을 완화하기 위해, GPT에 이미지를 묘사할 수 있는 2개 type의 데이터를 제공한 뒤 질문을 진행한다.

1. Image를 묘사하는 Caption 데이터

2. 이미지 내, 물체들에 대한 Bounding Box 데이터(각 box에는 해당 물체의 종류와 위치 정보를 담고 있다. 즉, GPT가 이미지를 input으로 사용하지는 못하지만, text 형태로 이미지 내 물체들의 위치 정보와 종류를 인식할 수 있는 것이다.)

 

  • COCO image를 이용하여 3개 타입의 instruction-following dataㄹ르 생성하였다. 데이터 생성 시에는 인간이 만든 prompt를 이용하여, 데이터를 얻었다. (총 158K)
    • Coversation : 인간과 GPT간 이미지에 대한 Q&A 대화를 생성했다. GPT가 이미지를 보고 있는 것처럼 질문을 했다. 질문에는 물체  종류, 물체 개수, 물체의 행동, 물체의 위치, 물체 사이의 상대적 위치 등이 포함된다. (58K)
    • Detailed Description : Image에 대한 자세한 설명을 요청하는 질문들을 만들어 요청하였다.(23K)
    • Complex Reasoning : 앞선 2개의 데이터들은 이미지 자체에 집중하지만, GPT에 깊은 추론을 요하는 질문들을 진행했다. (77K)

  • 초기에는 ChatGPT와 GPT-4를 모두 활용했으나, GPT-4의 quality가 더 좋아서 GPT-4만 사용했다. 

 

Visual Instruction Tuning

[구조]

  • Model의 주된 구조적 목표는 visual model과 pre-trained LLM을 효과적으로 결합하는 것이다.
  • LLM으로는 LLaMA를 사용하였다. 
  • 우선 이미지(Xv)를 CLIP visual encoder에 넣어서, Feature(Zv)를 뽑고, training 가능한 Linear Layer를 거쳐, Language model의 embedding space와 동일한 dimension의 language embedding token(Hv)를 만든다.  
  • 논문에서는 가볍고 간단한 구조를 생각해서 Linear layer를 사용했다고 하는데, 다른 여러 방식을 사용한 embedding은 future work로 남기겠다고 한다.

[학습]

  • 각 이미지마다 T개의 turn이 있는 conversation data를 생성하였고(첫 번째 Question, 첫 번째 Answer, 두 번째 Question,... 이런 형식으로), 이들을 sequence형태로 연결하였다. 
  • 이때, 첫번째 turn의 경우에는 이미지 Feature를 (1) 질문 앞에 두는 경우, (2) 질문 뒤에 두는 경우를 random 하게 결정한다. 

  • 학습을 위해서는 기존 LLM의 방식처럼 이전 token들을 통해, 현재 token을 추정하는 방식의 학습을 진행한다. 따라서, 학습을 위한 Loss도 기존 LLM과 비슷하다. 수식에 보면, 현재 token 추정 시, 단순 과거 Q&A 데이터뿐만 아니라 이미지 Feature도 함께 사용한다는 것이 기존 LLLM과의 차이이다.

  • LLaVa model 학습에는 2가지 stage가 존재한다.

1. Pre-training for Feature Alignment

  • LLaVa의 구조에서 이미지를 word embedding으로 변환할 때, Linear Layer를 사용하는데, 해당 논문의 관심사가 multi-modal Language Model인 만큼, 이미지를 얼마나 잘 word embedding으로 변환하냐가 매우 중요하다.
  • 이를 위해, CC3M 이미지-텍스트 데이터를 single turn conversation 형식으로 만들어, LLM과 visual encoder의 파라미터를 freeze 한 뒤, Linear layer만 학습을 진행했다.
  • 아마, visual encoder와 LLM은 pre-trained model을 사용했지만, Linear layer는 별도의 학습된 weight를 사용한 게 아니어서 해당 stage가 있는 것으로 생각된다. 

2. Fine-tuning End-to-End

  • visual encoder는 계속 freeze 시켜놓고, LLM과 Linear layer를 같이 학습한다. 
  • 학습에는 앞서 위에서 언급했던 3개 type의 데이터 (Conversation, Detailed Description, Complex Reasoning)와 저자들이 추가적으로 고안한 ScienceQA 데이터를 이용하였다. ScienceQA 데이터는 과학적 질문에 대한 multimodal 데이터 셋이다. ScienceQA는 single turn conversation 형식으로 사용했다.

 

Experiments

[multi modal chatbot]

  • LLaVa의 이미지 이해력과 conversation 능력을 보기 위해, Chatbot 데모를 고안했다. 우선, GPT-4 논문에 있는 Example들을 사용했다.
  • 비교를 위해, prompt와 response는 multimodal GPT-4 논문에 존재하는 것을 사용하였고, 다른 모델과 결과도 해당 논문에서 인용했다. (multimodal GPT-4, BLIP-2, OpenFlamingo) 
  • 놀랍게도, LLaVA는 매우 적은 양의 multimodal 데이터로 학습했음에도 불구하고, 2개의 example에서 multimodal GPT-4와 거의 비슷한 수준의 reasoning 결과를 보임을 확인했다. 이때, 2개의 example들은 LLaVA의 학습에 사용된 이미지의 범위가 아님에도 (학습 시, 데이터를 외운 것이 아니다.) 장면을 이해하고, question에 대한 적절한 대답을 했다.

  • 단순 example 비교 뿐 아니라, LLaVA의 성능을 측정하기 위해, GPT-4를 이용하여, ouput의 quality를 측정했다.
  • 구체적으로 COCO validation에서 30개의 이미지를 무작위로 추출하여, 앞서 언급한 3개 종류의 question을 만들고, LLaVA가 이미지와 해당 question을 기반으로 answer를 내도록 했다. 그리고, question, ground-truth bounding box, captions을 이용하여 GPT-4에게 answer을 내도록 하여, reference를 생성했다. (즉, GPT-4 대비, LLaVA의 성능을 측정하겠다는 뜻)
  • 그러고 나서, GPT-4를 이용하여 helpfulness, relevance, accuracy, detail 정도의 quality를 1~10까지 점수로 나타내게 했다. (점수가 높을수록 좋은 것)
  • 가장 좋은 모델은 GPT-4의 85.1% 수준의 매우 좋은 성능을 보였다. 

 

Reference

Liu, H., Li, C., Wu, Q., & Lee, Y. J. (2023). Visual instruction tuning. arXiv preprint arXiv:2304.08485.

 

총평

  • Open source로 모든 것을 공개했다는 점이 우선 제일 놀라웠다.
  • GPT-4를 이용해서 데이터를 만드는 방식에서 bounding box를 텍스트 형식으로 제공하는 부분에서 어떻게 이런 생각을 할 수 있지라는 생각이 들었다.
  • multimodal LLM을 Q&A에 적용하였는데, Open source로 소스가 제공된 만큼 objective function이라던지, 데이터의 방식에서 금방 새로운 아이디어가 적용되어, 더 다양한 부분에 사용될 수 있지 않을까 싶다.
  • 이미 있는지는 모르겠지만, 소리 등등의 다른 modal에도 적용할 수 있을 것 같다. 
반응형

DeepViT 배경 설명

  • DeepVit는 2021년에 ViT에 후속으로 나온 논문이다.
  • ViT의 등장 이후, CNN 처럼 ViT를 깊게 쌓기 위한 방법을 제시한 논문으로, ImageNet classification에서 기존 CNN 기반의 SOTA를 넘어서는 성능을 보였다고 한다.

 

Abstract

  • 이 논문에서는 Image Classification에서 Layer가 깊어질수록 좋은 성능을 내는 CNN과 달리, ViT의 performance는 layer가 깊어질수록 성능이 더 빨리 saturate 되는 것을 발견했다.
  • 이것은 transformer의 사이즈가 커지면서, attention map들이 점점 비슷한 형태를 띠는 "attention collapse issue" 때문이다.
  • 이것은 ViT에 deeper layer들을 적용하기 위해서 self-attention 방식이 효과적이지 못함을 보여준다.
  • 이러한 결과로, 이 논문에서는 간단하면서 효과적인 "Re-attention"이라는 attention map을 재생성해서, layer들의 다양성을 향상하는 방법을 제안한다.
  • 이 방법은 기존 ViT를 조금만 수정하더라도 좋은 성능을 보인다.

 

Introduction

[배경]

  • CNN 방식은 Image Classification 학습 시에 더 깊은 layer를 사용할 수록, 더 풍부하고, 복잡한 형태의 representations를 학습하기 때문에, layer를 어떻게 더 효과적으로 쌓는지에 대해서 연구가 많이 되고 있다.
  • ViT가 좋은 성능을 보이면서, 자연스럽게 ViT도 model을 깊게 쌓으면 CNN 처럼 성능이 좋아질 것인가? 에 대한 관심이 생기고 있다.

[ViT 깊이 실험]

  • 이를 검증하기 위해, 각기 다른 block number의 ViT를 가지고 ImageNet classification 성능을 비교해보았다.
  • 실험 결과, ViT 깊이가 커질수록 성능이 좋아지지 않는다는 것을 발견했고, 심지어 성능이 떨어지기도 하였다.
  • 실험적으로 이유를 확인해 보았을 때, ViT의 깊이가 깊어지면, 특정 layer 이상에서 attention map이 비슷해지는 현상을 발생하였다. (즉, attention의 역할을 제대로 수행하지 못함.) 더 깊어지면, 아예 모든 값들이 같아짐을 확인했다.
  • 즉, ViT의 깊이가 깊어지면 self-attention 방식이 더 이상 working 하지 않음을 의미한다.

[방법 제안]

  • 이러한 "attention collapse" 문제를 해결하고, ViT를 효과적으로 scale 하기 위해, 이 논문에서는 간단하지만 효과적인 self-attention 방식인, "Re-Attention" 방식을 소개한다.
  • Re-Attention은 Multi-Head self-attention 구조를 따르고, 다른 attention head들의 information을 이용하여, 좋은 성능을 내게 한다.
  • 이 방식을 도입하면, 별도의 augmentation이나 regularization 추가 없이도, 더 깊은 block의 ViT를 효과적으로 학습하여, 성능향상을 확인할 수 있다. (SOTA)

 

Attention Collapse

  • ViT에서 Transformer block 개수를 다르게 ImageNet Classification을 수행해 보았을 때, 기존 ViT에서는 block 개수가 커질수록 Improvement가 점점 감소되고, 심지어 성능이 줄어드는 것을 확인할 수 있다.
  • 이 이유를 CNN에는 없는 self-attention 때문으로 지목했는데, model의 깊이에 따른 attention을 확인해 보았다.
  • Transformer block이 32개 일 때, 각 block layer에서 다른 layer (인접한 k개의 layer) 와의 유사도를 구해본 결과 17번째 block을 넘어서는 순간 90% 이상의 거의 비슷한 output을 냄을 확인할 수 있다. 즉, 이후의 attention map들이 거의 비슷한 형태를 보이고, MHSA가 MLP를 악화시킬 수 있다는 것을 의미한다. 

 

Re-Attention

  • Attention Collapse를 해결하기 위해, 두 가지 방법을 제안한다. 첫 번째는, self-attention 연산을 위한 hidden dimension의 수를 늘리는 것이고, 두 번째는,  re-attention 방식이다.

[Self-Attention in Higher Diemnsion Space]

  • Self-Attention이 비슷해지는 것을 방지하기 위해, Dimension size를 늘리면, 더 많은 정보를 가지고 있게 되고, attention map이 더 다양해지고, 비슷해지지 않게 된다.
  • 아래 그림과 표를 보면, 12 Block의 ViT에서 Dimension size를 늘렸을 때, 비슷한 Block들의 수가 줄어들면서, ImageNet 성능이 향상됨을 확인할 수 있다.
  • 하지만, 이러한 방식은 성능적 한계가 있다는 점과, Parameter 숫자가 매우 늘었다는 단점이 있다.

[Re-Attention]

  • 다른 Transformer block 사이의 attnention map은 매우 비슷하지만, 동일 transformer block에서 다른 head 사이에서는 similarity가 작음을 확인했다.
  • 같은 attention layer의 다른 head들은 각기 다른 aspect에 집중하고 있기 때문이다.
  • 이 결과를 바탕으로, cross-head communication을 위해, attention map들을 재생성하는 방식을 제안한다.
  • 이를 위해, learnable parameter인 transformation matrix(H X H)를 개념을 도입하여, self-attention map의 head dimension 방향으로 곱해준다. 그 후 layer normalization을 진행하여 "Re-Attention" 구성한다. 

  • Re-Attention의 장점은 크게 2가지이다. 첫 번째는 Re-Attention map은 다른 attention head 들 사이에 정보를 교환할 수 있어, 상호 보완이 가능하고, attention map의 다양성을 늘린다. 또한, Re-Attention map은 효과적이면서 간단하다. 

→ 쉽게 말하면, self-attention을 진행할 때, 기존처럼 단순 softmax 값으로 값 참조를 하는 것이 아닌, 별도의 learnable parameter로 다양성을 향상하자는 개념임.

 

Experiments

  • 실험에서는 attention collapse 문제에 대한 설명을 위한 실험을 진행한다. 추가적으로 Re-attention의 장점에 대한 추가적인 실험을 진행한다. (생략) 
  • 논문에서 주장한 것처럼 Re-Attention을 사용하였을 때, 비슷한 attention 패턴이 매우 줄고, 이로 인해 image classification에서 기존 ViT보다 더 높은 성능을 보인다. (ImageNet)

  • Image Classification SOTA 모델들과 비교해 보았을 때도, 더 좋은 성능을 보인다.

Reference

ZHOU, Daquan, et al. Deepvit: Towards deeper vision transformer. arXiv preprint arXiv:2103.11886, 2021.

 

논문 총평

  • 내 식견이 넓지 않은 까닭인지 저자들이 주장하는 Attention Collapse 현상과 Re-Attention 논리 구조를 100% 이해하진 못했다. 
  • 다만, CNN SOTA와 비견할 정도로 높은 성능을 보인다는 점에서 좋은 연구였다고 생각한다.
반응형

SQL에서 가장 유용하다고 생각하는 기능을 집계함수이다. 테이블 내에서 Grouping을 통해 통계값을 추출하고, 값을 모아 연산하는 과정이 다른 절차형 언어들에 비해 SQL이 가지는 특장점이라고 생각한다. Spark의 구조적 API도 SQL의 기본적인 기능을 제공하기 때문에, 마찬가지로 집계 연산 함수를 위한 API를 제공한다. 


Spark 집계 연산 함수란?

  • 집계함수는 키나 그룹을 지정하고, 하나 이상의 칼럼을 특정 연산으로 모아서, 그룹별로 결과 내는 데 사용하는 함수이다.
  • 기본적으로 Spark의 집계함수는 pyspark.sql.functions 내에 정의되어 있다.
  • SQL의 문법과 거의 비슷하게 지원하지만, SQL의 모든 기능을 100% 지원하지는 않는다. (2023년 9월 기준)

 

Spark의 groupBy

  • Spark의 groupBy는 데이터를 특정 열 기준으로 그룹화하고, 그룹별로 집계 연산을 수행하는 데 사용된다.
  • Spark의 DataFrame에서 groupBy 함수에 그룹화를 위한 칼럼을 인자로 넣어주고, agg 함수의 인자로 집계 연산 식을 넣어준다. 
  • Spark는 groupBy 작업을 여러 파티션에 병렬로 분산 처리하기 때문에, 효율적 그룹화와 집계가 가능하다.
from sklearn import datasets
import pandas as pd
from pyspark.sql import SparkSession
import pyspark.sql.functions as F

if __name__ == '__main__':
    iris = datasets.load_iris()
    df = pd.DataFrame(data=iris.data, columns=iris.feature_names)
    df["species"] = iris.target

    spark = SparkSession.builder.appName("iris").getOrCreate()
    df = spark.createDataFrame(df)

    grouped_df = df.groupBy(F.col("species")).agg(F.avg(F.col("petal width (cm)")).alias("avg petal width (cm)"))

    grouped_df.show()

Spark의 partitionBy

  • SQL에서 지원하는 partition by를 Spark에서도 사용할 수 있다.
  • groupBy와의 차이는 groupBy는 개별데이터가 없어지고, 그룹화에 참여하지 않은 칼럼 정보가 사라지지만, partitionBy는 그렇지 않다는 점이다. (group by 하고, join을 한 것 동일한 효과를 낸다.)
  • pyspark.sql.window에 Window를 사용하여, window(그룹화 방식)을 설정해주고, 집계함수 뒤에 over 함수에 window를 넣어줘서 partitionBy를 수행한다.
from sklearn import datasets
import pandas as pd
from pyspark.sql import SparkSession
from pyspark.sql.window import Window
import pyspark.sql.functions as F

if __name__ == '__main__':
    iris = datasets.load_iris()
    df = pd.DataFrame(data=iris.data, columns=iris.feature_names)
    df["species"] = iris.target

    spark = SparkSession.builder.appName("iris").getOrCreate()
    df = spark.createDataFrame(df)

    window = Window.partitionBy(F.col("species"))

    partitioned_df = df.select("*", F.avg(F.col("petal width (cm)")).over(window).alias("avg petal width (cm)"))

    partitioned_df.show()

 

Spark의 집계 연산 함수 종류

Pyspark에서 집계함수를 위한 API도 pyspark.sql.functions에 선언되어 있다. 
 
[Agg 함수]

  • count
    • 가장 기본적인 집계함수로 해당 그룹에 포함되는 row의 개수를 세는데 사용한다.
    • spark에서는 SQL과 동일하게 count라는 API를 제공한다.
grouped_df = df.groupBy(F.col("species")).agg(F.count(F.col("petal width (cm)")))
grouped_df.show()

 

  • countDistinct 
    • 집계를 하다보면, 단순 포함되는 element 개수가 중요한 경우도 있지만, 값 자체가 중요한 경우가 많다.
    • 중복 값을 제외하고, 고유한 값의 개수를 집계한다.
    • SQL에서는 count(distinct column)의 형식으로 표현되지만, spark에서는 countDistinct라는 별도의 API로 집계한다.
grouped_df = df.groupBy(F.col("species")).agg(F.countDistinct(F.col("petal width (cm)")))
grouped_df.show()
  • approx_count_distinct
    • 기본적으로 count distinct 연산은 속도와 메모리를 많이 사용한다. 고윳값의 개수를 정확히 알지 않아도 되는 연산등에서는 이러한 연산은 불필요한 cost를 발생시킨다.
    • 예를 들어, 통계분포를 사용하기 위해, 대략 고윳값이 30개 이상인지 아닌지를 파악하기 위해서, countDistinct 함수를 사용하는 것은 매우 비효율적이다.
    • pyspark에서는 approx_count_distinct 함수를 제공하여, 대략적인 고윳값의 개수를 파악할 수 있다. approx_count_distinct 함수는 HyperLogLog 알고리즘을 사용하여, 근사치를 사용하여 계산한다. (메모리 내의 데이터 분포로 전체 데이터의 고윳값 개수를 추정)
    • relativeSD 인자를 통해, 정확성을 조정 가능하다. (기본값은 0.05) 정확성이 높아질수록, 메모리 사용량 및 연산 시간은 늘어남.
grouped_df = df.groupBy(F.col("species")).agg(F.approx_count(F.col("petal width (cm)")))
grouped_df.show()

 

  • first, last
    • 특정 순서로 정렬되어 있는 데이터들에서 그룹의 첫 값이나, 마지막 값이 필요한 경우가 있다. 
    • Spark에서는 first, last 함수를 제공하여, 해당 그룹의 첫번째 행의 값과 마지막 행의 값을 표기할 수 있다. 
    • first와 last가 유의미한 값을 갖기 위해서는 orderBy와 함께 사용되어야 한다. first와 last는 행의 정렬 기준으로 값을 추출하기 때문에 정렬되지 않은 DataFrame에서 유의미한 결과가 아닐 수 있다.
df = df.orderBy(F.col("petal width (cm)"))
grouped_df = df.groupBy(F.col("species")).agg(F.first(F.col("petal width (cm)")), F.last(F.col("petal width (cm)")))
grouped_df.show()

 

  • min, max
    • 통계값의 꽃 min, max이다. Spark에서도 모두 활용 가능하다.
    • min, max는 수치형 데이터 타입, 소수형 데이터 타입, timestamp 데이터 타입, datetype 등에 모두 사용 가능하다. 
    • 문자형 데이터 타입에서도 사용할 수 있는데, 사전순으로 정렬하여 가장 큰 값을 찾는다.
df = df.orderBy(F.col("petal width (cm)"))
grouped_df = df.groupBy(F.col("species")).agg(F.avg(F.col("petal width (cm)")), F.min(F.col("petal width (cm)")),
                                              F.max(F.col("petal width (cm)")))
grouped_df.show()
  • avg, sum
    • avg, sum도 Spark에서 활용 가능하다.
    • avg, sum은 수치형 데이터 타입, 소수형 데이터 타입에만 활용 가능하다. 
grouped_df = df.groupBy(F.col("species")).agg(F.avg(F.col("petal width (cm)")), F.sum(F.col("petal width (cm)")))
grouped_df.show()

 

  • sumDistinct
    • countDistinct와 같이, 중복을 제외하고 고유한 값들을 더하기 위해, sumDistinct가 존재한다.
grouped_df = df.groupBy(F.col("species")).agg(F.avgDistinct(F.col("petal width (cm)")))
grouped_df.show()

 

  • collect_list, collect_set
    • 그룹에 있는 모든 원소의 값들이 필요한 경우도 존재한다. 이 경우 collect_list와 collect_set을 이용하여, 각각 칼럼 값들을 원소로 한 list와 set을 구할 수 있다.
    • 특히, 그룹화 후, 파이썬등으로 인자를 넘길때, collect_list와 collect set을 많이 활용한다.
    • 다만, 그룹화 시에, row 순서가 유지되지 않기 때문에 주의해야 한다.
grouped_df = df.groupBy(F.col("species")).agg(F.collect_list(F.col("petal width (cm)")),
                                              F.collect_set(F.col("petal width (cm)")))
grouped_df.show()

 

  • mode
    • Spark 3.4 버전에서 새로 등장한, 최빈값을 추출하는 집계함수이다.
grouped_df = df.groupBy(F.col("species")).agg(F.mode(F.col("petal width (cm)")))
grouped_df.show()
  • product
    • 그룹 내의 모든 값을 곱하는 집계함수이다. 
grouped_df = df.groupBy(F.col("species")).agg(F.product(F.col("petal width (cm)")))
grouped_df.show()

 
이 밖에도, 표준편차나 분산, 첨도 등을 구하는 다양한 함수들이 존재하니, Spark 공식 문서를 참고하는 것이 좋다.
https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/functions.html

Functions — PySpark 3.4.1 documentation

Computes the first argument into a string from a binary using the provided character set (one of ‘US-ASCII’, ‘ISO-8859-1’, ‘UTF-8’, ‘UTF-16BE’, ‘UTF-16LE’, ‘UTF-16’).

spark.apache.org

 
 
[Window 함수]

  • row_number, rank, dense_rank
    • 그룹화한 후에, 그룹 내에서 순위등을 매기는 경우가 있다. 이러한 경우에는 row_number나 rank, dense_rank를 사용할 수 있다.
    • row_number와 rank, dense_rank는 window에 orderBy 절을 추가해야 한다.
    • row_number는 정렬 후 row 순대로 값이 나오기 때문에 빈값이 없지만, 값이 같은 경우 orderBy 된 순서가 달라질 수 있다. rank의 경우에는 빈값이 존재 가능하다. (1위 동률이 많으면, 2등이 없다.) dense_rank는 빈값 없이 순위를 매긴다. 
window = Window.partitionBy(F.col("species")).orderBy(F.col("petal width (cm)"))
partitioned_df = df.select("*", F.row_number().over(window).alias("row_num"), F.rank().over(window).alias("rank"),
                           F.dense_rank().over(window).alias("dense_rank"))
partitioned_df.show()

 
 

Pyspark 집계 연산 함수의 아쉬운 점

  • Spark의 집계 함수를 사용할 때, 가장 헷갈리는 부분은 groupBy 시에 collect_list와 같은 복합 집계 함수를 사용하였을 때, 순서가 유지되느냐였다.
  • 우선, collect_list를 여러 칼럼에 사용하면, 묶는 순서는 유지된다. 즉, collect_list를 여러 칼럼에 사용했더라도, 같은 index에 존재하는 값은 같은 row를 통해서 온 값이다.
  • 하지만 Pyspark의 집계함수가 아쉬운 점이 하나 있는데, groupBy의 collect_list 때, 순서를 지정해 줄 수 없다는 점이다. 
  • SQL에서는 아래와 같은 기능이 하나의 쿼리를 통해 구현된다. 
SELECT STRING_AGG(COL1,',' ORDER BY COL4),STRING_AGG(COL2,',' ORDER BY COL5) FROM TABLE GROUP BY COL3
  • Pyspark의 경우, 각기 다른 collect_list가 같은 정렬 기준으로 결합된다면, orderBy 후에 groupBy를 수행하면 되지만, 위와 같은 SQL을 처리하기 위해서는 orderBy를 여러 번 호출해서 withColumn 형식으로 groupBy 테이블에 추가해줘야 한다는 점이다.
  • 그래도, 여러 단계를 거치면 구현이 가능하다는 점과, Spark는 계속 발전하고 있다는 점이 긍정적이다...

 


SQL의 문법이 익숙해서, Pyspark가 약간 아쉬운 면도 있는 것 같다. 하지만, UDF의 호환성이 훌륭하다는 점에서 Pyspark는 매우 좋은 것 같다.

반응형

업무에서 Cluster 기반의 Database 엔진을 사용하고 있다. Database 서버 내에서 굉장히 많은 양의 PL/SQL Function을 사용 중인데, 이것을 Spark로 바꾸고 있다. 성격상 이론보다 개발부터 진행 중이지만, 이론도 꼭 필요하다고 생각하여 공부 중이다.


Spark의 구조적 API란?

  • 구조적 API는 주로 정형 데이터 (CSV, JSON, Table) 등의 데이터를 처리하기 위한 고수준의 API이다. 
  • 정형화가 가능한 데이터들은 일반적으로 Table 형태로 표현할 수 있는데, Spark는 이러한 Table 구조의 데이터들을 빠르게 연산할 수 있는 다양한 API 등을 지원해 준다. 
  • 매우 쉽게 생각하면, Database의 Query 혹은 Function 등의 기능을 제공해 주는 기능이라고 생각하면 된다.
  • API라는 말이 이질적으로 다가올 수 있는데, Spark는 실행 계획 수립과 처리에 사용하는 자체 데이터 타입 정보를 가지고 있는 Catalyst 엔진을 사용한다. 즉, Pyspark를 통해 전달하는 데이터도 Catalyst 엔진에서 자체 데이터 타입으로 변경된다. 사용자는 Spark 코드를 짠다고 하지만, 실제로는 사용자의 코드가 그대로 연산되는 것이 아니라, Catalyst 엔진에서 수행 가능하도록 만들어 놓은 API를 호출하는 것이기 때문에, API라고 부른다. 
  • 개인적인 경험상, SQL Query Function을 Spark API로 바꾸는 것이 복잡하긴 해도, 80% 이상의 변환이 가능하였다. (안 되는 부분도 몇 단계의 과정을 거치면 거의 변환이 되었다.)

구조적 데이터 종류

  • DataFrame : Run time 단계에서 데이터 타입 일치를 확인
  • DataRow : Compile 단계에서 데이터 타입의 일치 여부를 확인, 다만 Spark와 같이 JVM 기반의 언어인 Scala와 Java에서만 지원한다.

 

Schema

  • DataFrame은 Schema로 정의되는데, Schema에는 칼럼명과 각 칼럼의 데이터 타입을 정의한다.
  • Schema는 데이터 소스에서 읽거나, 직접 정의가 가능하다. 
  • 간단한 데이터 분석 등에 대해서는 타입 추론을 많이 사용하지만, 운영을 위한 ETL 등에서는 데이터 타입을 미리 지정해 주는 것이 좋다.

 

Spark 구조적 데이터 타입 

  • 기본적으로 Python은 타입추론을 하여, 변수의 타입 선언에 민감하지 않다.
  • Spark는 앞서 말했듯, Python에서 연산을 처리하는 것이 아닌, Catalyst 엔진에서 데이터의 연산을 처리하기 때문에, DataType을 지정해줘야 한다.
  • 기본적으로 Pandas의 Dataframe처럼 DataFrame 생성 시, 데이터의 타입을 추론하지만, 특정 칼럼의 전 데이터가 NULL인 경우 등, 데이터 타입 추론이 불가한 경우에는 에러가 뜰 수 있기 때문에, DataFrame 선언 시 아래의 데이터 타입을 지정해 주는 것이 좋다. 
  • DataFrame 내에는 Array나 Struct 타입의 복합 데이터 타입도 지정할 수 있다.

[Spark, Python 데이터 타입 비교 및 사용 예시]

(편의를 위해, spark session 접근 및 "import pyspark.sql.functions as F"는 생략

Spark 데이터 타입 Python 데이터 타입 pyspark import & 사용예시
ByteType bytes from pyspark.sql.types import ByteType
byte_column = F.lit(byte_value).cast(ByteType())
ShortType int from pyspark.sql.types import ShortType
short_value = 10
short_column = F.lit(short_value).cast(ShortType())
IntegerType int from pyspark.sql.types import IntegerType
int_value = 100
int_column = F.lit(int_value).cast(IntegerType())
LongType int from pyspark.sql.types import LongType
long_value = 1000
long_column = F.lit(long_value).cast(LongType())
FloatType float from pyspark.sql.types import FloatType
float_value = 3.14
float_column = F.lit(float_value).cast(FloatType())
DoubleType float from pyspark.sql.types import DoubleType
double_value = 2.71828
double_column = F.lit(double_value).cast(DoubleType())
DecimalType decimal from pyspark.sql.types import DecimalType
decimal_value = Decimal('3.14159265359')
decimal_column = F.lit(decimal_value).cast(DecimalType(10, 10))  # (precision, scale)
StringType str from pyspark.sql.types import StringType
string_value = "Hello, World!"
string_column = F.lit(string_value).cast(StringType())
BinaryType bytes from pyspark.sql.types import BinaryType
binary_value = bytes([0, 1, 2, 3, 4])
binary_column = F.lit(binary_value).cast(BinaryType())
BooleanType bool from pyspark.sql.types import BooleanType
boolean_value = True
boolean_column = F.lit(boolean_value).cast(BooleanType())
TimestampType datetime의 timestamp from pyspark.sql.types import TimestampType
from datetime import datetime
timestamp_value = datetime(2023, 9, 3, 12, 0, 0)
timestamp_column = F.lit(timestamp_value).cast(TimestampType())
DateType date from pyspark.sql.types import DateType
from datetime import date
date_value = date(2023, 9, 3)
date_column = F.lit(date_value).cast(DateType())
ArrayType list from pyspark.sql.types import ArrayType, IntegerType

python_list = [1, 2, 3, 4, 5]
array_column = F.lit(python_list).cast(ArrayType(IntegerType()))
MapType dict from pyspark.sql.types import MapType, StringType, IntegerType

python_dict = {"apple": 1, "banana": 2, "cherry": 3}

map_column = F.lit(python_dict).cast(MapType(StringType(), IntegerType()))
StructType class
(C언어의 구조체 데이터 타입)
from pyspark.sql.types import StructType, StructField, StringType, IntegerType

python_struct = [("Alice", 30), ("Bob", 25), ("Charlie", 35)]

schema = StructType([
    StructField("name", StringType(), nullable=True),
    StructField("age", IntegerType(), nullable=True)
])

struct_column = F.lit(python_struct).cast(schema)
StructField class
(StructType의 각 필드에 대한 메타 정보 설정에 사용됨)

 

 

구조적 API의 실행 과정

  • Spark를 공부하면서 느낀 점은 Database 엔진과 매우 비슷하다는 점이다. Database 엔진은 SQL에 대해서 통계적 정보 등을 이용하여 실행 계획을 만드는데, Spark도 마찬가지이다. 
  • 구조적 API는 다음과 같이 실행된다.
    1. 다양한 언어를 통해, DataFrame/SQL 등을 이용한 코드를 작성한다.
      • 코드가 검증 전 논리적 실행 계획으로 변환된다. 이 과정은 코드의 유효성이나, 테이블, 칼럼 등의 존재 여부만 확인한다. 쉽게 말해서, 문법검사 단계만 진행한다고 생각하면 된다.
    2. 코드에서 이상이 없다면, Spark가 이 코드를 논리적 실행 계획으로 변환한다.
      • Spark의 Analyzer에 의해, 칼럼과 테이블등을 검증한다. 이 과정에서 Catalog와 DataFrame의 정보 등을 활용한다. 
      • Catalyst의 Optimizer에 의해 Predicate Pushing Down 등의 논리 계획 최적화가 이뤄진다. (Database 엔진의 Catalyst Optimizer와 거의 동일하다.)
    3. Spark의 논리적 실행 계획을 물리적 실행 계획으로 변환하고, 추가적 최적화를 진행한다.
      • 각 Plan에 대한 Cost를 고려하여 논리적 실행 계획을 물리적 실행 계획으로 변환한다. 
      • 특히, Spark는 Cluster 환경에서 많이 사용하기 때문에, 이 부분이 Spark의 강점이다. (사실 Cluster 기반의 Database 엔진의 강점이기도 하다.)
    4. Spark는 Cluster에서 물리적 실행계획(RDD 처리)을 실행한다.

 

 

구조적 API 기본 연산 

  • 구조적 API 사용을 위해, 기본적인 연산들을 알아놓아야 한다. 거의 대부분이 SQL에 있는 함수 및 쿼리이고, 동일한 동작을 한다.
  • 글 쓰는 시점에서 가장 최근인 3.4.1(23.6.20) 기준으로 작성했다. (Spark는 계속 업데이트 중이다. 사용 전에 함수를 살펴보는 것도 좋다. : https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/index.html)
  • 매우 유용한 함수들이 많지만, 나의 기준에서 많이 활용했던 기본 Function 기준으로 소개한다.

 [DataFrame 관련 함수]

 

 

  •  Select
    • SQL에서와 마찬가지로, spark에서 칼럼 조회를 위해 사용하는 API이다. Query와 동일하게 select 안에 칼럼을 지정해 주면 되고, 모든 칼럼을 의미하는 '*'도 동작한다.
# 모든 칼럼을 선택
selected_df = df.select("*")

# 특정 칼럼을 선택
name_df = df.select("name")

# 다중 칼럼을 선택 가능
column_list = ['a','b','c']
selected_df = df.select(*column_list)

# Alias 사용 가능
selected_df = df.select("name").alias("name2")
  • SelectExpr
    • SelectExpr은 select와 비슷하지만, SQL처럼 표현식을 사용가능하다. 'as'를 이용한 alias 지정도 가능하고, 숫자형 칼럼에는 연산도 가능하다. 
    • Select를 SQL에 가까운 형태로 사용할 수 있다.
# 칼럼 이름 변경하여 선택
name_renamed_df = df.selectExpr("name as full_name")

# 새로운 칼럼 추가하여 선택
age_plus_10_df = df.selectExpr("*", "age + 10 as age_plus_10")
age_double_10_df = df.selectExpr("*", "age * 2 as age_double")
  • withColumn
    • withColumn은 DataFrame에 새로운 칼럼을 추가하거나, 기존 칼럼을 변경하는 데 사용된다. 
    • 한 번에 하나의 칼럼밖에 못 바꾸기 때문에, 여러 칼럼을 바꿔야 하면, withColumn을 계속 연결해서 사용해줘야 한다.
    • withColumnRenamed를 사용하면, 칼럼명을 바꿀 수 있다. (바꿀 칼럼명, 바뀔 칼럼명)
# withColumn으로 새로운 열 추가
df = df.withColumn("square_age", df["age"]*df["age"])

# withColumnRenamed로 칼럼명 변경
df = df.withColumnRenamed("square_age", "squared_age")
  • Drop
    • Drop은 DataFrame에서 칼럼을 제거할 때 사용한다. 
    • Drop의 호출만으로 DataFrame의 칼럼이 제거되는 것이 아닌, 새로운 DataFrame에 칼럼이 제거된 상태로 할당되는 구조이기 때문에, 꼭 반환값을 받아야 한다.
# 특정 칼럼을 제거한 DataFrame 생성
df_without_age = df.drop("age")
df_without_name = df.drop(col("age"))
  • Cast
    • Cast는 DataFrame의 칼럼의 데이터타입을 변경하는 함수이다. 
# "age" 칼럼의 데이터 타입을 문자열(string)로 변환
df_casted = df.withColumn("age_string", df["age"].cast("string"))
  • Where/Filter
    • Where와 Filter는 동일한 동작을 하는 함수로, DataFrame에서 특정 조건을 만족하는 행을 선택할 때 사용된다.
    • SQL의 Where와 같다. 
    • and 조건 (&)과 or 조건 (|) 연산자를 지원하기 때문에, 조건을 하나의 filter나 where에 담을 수 있다. (중복 조건일 때는 연속해서 where or filter를 걸어도 된다.)
# Filter와 Where 조건
filtered_df = df.filter(df["age"]>=30)
where_df = df.where(df["age"]>30)

# 다중 Filter
filtered_df = df.filter(df["age"] <=20 | df["age"] > 50)
filtered_df = df.filter(df["age"] <=30 & df["name"] == "Choi")
filtered_df = df.filter(df["age"] <=30).filter(df["name"] == "Choi")
  • Distinct
    • Distinct 함수는 중복 Row를 제거하고, 고유한 Row만 선택한다.
    • SQL의 distinct와 동일하다.
    • subset 매개변수로, 특정 칼럼들을 list 형태로 전달하면, 해당 칼럼을 기준으로 Distinct가 가능하다.
# 전체 칼럼 중, 중복행 제거
distinct_df= df.distinct()

# 특정 칼럼 중복행 제거
distinct_df= df.distinct(["name"])
  • sort, orderBy
    • sort와 OrderBy는 특정 칼럼 기준으로 순서를 나열하기 위해 사용하는 함수이다.
    • 둘 다 비슷한 동작을 하지만, 사용 방법이 약간 다르다.
    • sort는 ascending 인자를 True or False로 오름차순, 내림차순을 선택하고, orderBy는 칼럼기준으로 asc함수나 desc 함수를 사용하여, 정렬 방법을 지정해줘야 한다.
    • orderBy는 각 칼럼의 정렬순을 따로 지정(어떤 칼럼은 오름차순, 어떤 칼럼은 내림차순으로)할 수 있기 때문에 유용하다.
# sort, orderby를 이용한 정렬
sorted_desc_df = df.sort(col("age"), ascending=False)
sorted_desc_df = df.orderBy(col("age"))

# 다중 칼럼 정렬
sorted_desc_df = df.sort(["age","name","city"])
sorted_df = df.sort(col("age"), ascending=True).sort(col("name"), ascending=False).sort(col("city"), ascending=True)
sorted_df = df.orderBy(col("age").asc(), col("name").desc(), col("city").asc())
  •  limit
    • limit은 DataFrame에서 특정 수의 행을 선택하여 반환하는 데 사용한다. 
    • limit을 이용하여, 특정 칼럼 이상을 Filtering 하거나, 일부 데이터만 보는 데 사용할 수 있다. 
    • 다만, 특정 순으로 정렬되어 있지 않으면, 순서가 유지되지 않을 수 있다.
    • Oracle의 rownum 조건이나, Postgresql의 limit과 동일하다.
sort_df = df.orderBy[col("age")]
limited_df = sort_df.limit(2)
  • repartiton
    • repartition은 DataFrame의 partition을 변경하거나, 재분할하는 데 사용된다.
    • 숫자를 인자로 받아서, 해당 숫자만큼의 partition으로 나눈다. (칼럼을 지정하지 않으면, 모든 칼럼을 고려하여 나눈다.)
    • Spark의 강점이 데이터 분산 처리이기 때문에, 데이터 분산을 최적화하여, 성능에 큰 영향을 미친다. 
# 단순 partition 숫자 지정
repartitioned_df = df.repartition(4)

# partition 칼럼 지정
repartitioned_df = df.repartition(4, "name", "age")
  • coalesce
    • coaleasce는 DataFrame의 partition 수를 줄이는 데 사용된다. (repartition은 증/감이 모두 가능하다는 것과 다르다.)
    • partition을 줄일 때는 repartition보다 성능이 좋기 때문에(default가 shuffle을 사용하지 않기 때문에), 줄일때는 coalesce를 사용하는 것이 좋다.
    • 숫자를 인자로 받아서,  해당 숫자만큼의 partition으로 줄인다.
    • 불필요한 partition을 줄여서 오버헤드를 줄이는 데 사용할 수 있다. 
coalesced_df = df.coalesce(2)
  • collect, show, take
    • collect, show, take는 DataFrame에서 데이터를 가져오는 데 사용하는 함수이다.
    • collect는 DataFrame의 전체 row를 로컬 메모리에 array 형태로 변수 할당할 때 사용하는 함수이다. 이를 이용해 unittest 등에 활용 가능하다. 다만, 모든 데이터를 다 불러오기 때문에, 로컬에 메모리가 충분할 때만 사용하는 것이 좋다.
    • show는 DataFrame의 일부 행을 로컬 메모리에 출력한다. Default는 20이며, 인자로 숫자를 지정해 주면, 그 숫자만큼 이 출력된다. Spark는 비동기 처리이기 때문에, 중간중간에 show를 수행해 줘야, 데이터 처리의 중간단계를 확인 가능하다.
    • take는 collect와 비슷하나, 인자로 숫자를 받아, 해당 숫자만큼의 상위 데이터들을 가져와서 array 형태로 반환한다.
df.show(2)
data = df.take(2)
collect_data = df.collect()

[기본 연산을 위한 함수]

기본적으로 pyspark에서 function은 pyspark.sql.functions에서 import 가능하다. (보통 alia로 F를 지정해서 많이 사용한다.)

 

  • Lit
    • Lit은 column에 상수값을 추가하거나, 리터럴 값을 생성할 때 사용하는 함수이다. when이나 expr이랑 같이 사용하면, 복잡한 연산등을 같이 담을 수 있다.
df = df.withColumn("country", F.lit("KOREA"))
df_with_country = df.withColumn("d_day", F.lit(50))
  • nanvl
    • nanvl은 2개의 칼럼을 받아서, 첫 번째 칼럼이 nan이 아닐 경우 첫번째 칼럼을, nan일 경우 두 번째 칼럼을 반환하는 함수이다. 
    • SQL의 nvl이나 postgresql의 coalesce와 동일하다.
df_with_age = df.withColumn("age", F.nanvl(df["age"], 0.0))
  • union
    • union은 두 개 이상의 DataFrame을 결합하여 새로운 DataFrame을 생성하는 데, 사용된다.
    •  결합하는 DataFrame은 모든 행이 동일한 데이터 타입을 가져야 한다.
    • SQL의 merge와 다른 점은 중복행을 제거하지 않는다는 것이다. (만약, 필요하다면, union 후에 distinct 처리가 필요하다.)
# 두 개의 DataFrame 생성
data1 = [("Minsu", 30), ("Daehwi", 30)]
data2 = [("Jaeeun", 28), ("JoonSung", 29)]
columns = ["Name", "Age"]
df1 = spark.createDataFrame(data1, columns)
df2 = spark.createDataFrame(data2, columns)

# 두 개의 DataFrame을 합침
union_df = df1.union(df2)
  • join
    • join은 DataFrame을 합치는 데 사용된다. 
    • SQL의 join과 같은데, SQL이 inner, outer, left, right join이 각각 있는 것에 비해, join 함수에서 인자로 결합 방식을 선택한다.
    • on에 공통 열의 이름을, how에 join 방법(inner, outer, left, right)을 지정해 준다.
    • join 후에는 두 테이블의 모든 칼럼명이 존재한다. 이때, 테이블의 alias를 지정 안 하고, 두 테이블에 공통으로 존재하는 칼럼을 참조하려 하면, 어느 테이블의 칼럼을 의미하는지 몰라서(join에 참여했든 안 했든) 에러가 뜬다. 따라서, join 전에 드라이빙 테이블과 join 테이블 모두를 alias 지정해 주고, join 후에 select로 사용할 칼럼만 남기는 것이 좋다.
# 두 DataFrame을 "Name" 열을 기준으로 inner 조인
result_df = df1.alias("A").join(df2.alias("B"), on="Name", how="inner")
result_df = result_df.select("A.Name", "A.ID", "B.Occupation")

# 두 DataFrame을 "Name"과 "EmployeeName" 열을 기준으로 inner 조인
result_df = df1.join(df2, col("Name") == col("EmployeeName"), how="inner")
  • isNull
    • isNull은 DataFrame의 특정 칼럼이 null인지를 확인하기 위한 함수이다.
    • Null일 경우 True, 아닐 경우 False를 반환한다.
filtered_df = df.filter(col("age").isNull())
  • monotonically_increasing_id
    • monotonically_increasing_id는 DataFrame에 1씩 증가하는 interger 형태의 숫자를 부여한다.
    • GroupBy 등에서 grouping 번호를 지정할 때, 매우 요긴하게 사용 가능하다.
df_with_id = df.withColumn("ID", monotonically_increasing_id())

 

 

 


SQL을 많이 쓰다 보니, Spark의 직관적이지 않은 데이터 처리 방법이 익숙하지 않다. 다음 시간에는 데이터 타입에 특화된 함수를 중심으로 알아봐야겠다. 

+ Recent posts