본문 바로가기

버츄얼유튜버

ViT 코드

https://www.kaggle.com/code/utkarshsaxenadn/vit-vision-transformer-in-keras-tensorflow

 

ViT(Vision Transformer) in Keras & Tensorflow

Explore and run machine learning code with Kaggle Notebooks | Using data from No attached data sources

www.kaggle.com

에서 쓰인 여러 함수에 대해 정리한 글

Vision Transformer (AN IMAGE IS WORTH 16X16 WORDS, TRANSFORMERS FOR IMAGE RECOGNITION AT SCALE) - gaussian37

 

Vision Transformer (AN IMAGE IS WORTH 16X16 WORDS, TRANSFORMERS FOR IMAGE RECOGNITION AT SCALE)

gaussian37's blog

gaussian37.github.io

역시 위 글을 많이 참고

 

tfi.extract_patches :

input img로부터 patche를 추출. 

output patches는 stack처럼 저장됨.

최종 결과물은 4D 텐서로, batch, row, column으로 인덱싱됨(indexed by batch, row and column).

 

걍 예를 들어보자.

input img가, 1 x 10 x 10 x 1 짜리, 1부터 100까지의 수를 저장한 array라 하자.

그럼

 n = 10
  # images is a 1 x 10 x 10 x 1 array that contains the numbers 1 through 100
  images = [[[[x * n + y + 1] for y in range(n)] for x in range(n)]]

  # We generate two outputs as follows:
  # 1. 3x3 patches with stride length 5
  # 2. Same as above, but the rate is increased to 2

라 볼 수 있다. 이 때, stride length = 5인 3x3짜리 patches를 만들고자 한다.

다음으로, 이와 조건은 같으나, rate가 2로 증가한 것을 만들고자 한다.

그렇다면 

 tf.image.extract_patches(images=images,
                           sizes=[1, 3, 3, 1],
                           strides=[1, 5, 5, 1],
                           rates=[1, 1, 1, 1],
                           padding='VALID')

  # Yields:
  [[[[ 1  2  3 11 12 13 21 22 23]
     [ 6  7  8 16 17 18 26 27 28]]
    [[51 52 53 61 62 63 71 72 73]
     [56 57 58 66 67 68 76 77 78]]]]

이 때, Yields는 보면 알겠지만 하나의 patches가 가지고 있는 픽셀값이다.

밑을 보면 하나의 3x3은 1 2 3 11 12 13 21 22 23을 지나간다. 

 

여기에서, input img 중 output에 관여한 픽셀값을 *로 마킹하면, 아래와 같다.

3x3짜리 박스가, stride=5로 지나간 것을 볼 수 있다.

   *  *  *  4  5  *  *  *  9 10
   *  *  * 14 15  *  *  * 19 20
   *  *  * 24 25  *  *  * 29 30
  31 32 33 34 35 36 37 38 39 40
  41 42 43 44 45 46 47 48 49 50
   *  *  * 54 55  *  *  * 59 60
   *  *  * 64 65  *  *  * 69 70
   *  *  * 74 75  *  *  * 79 80
  81 82 83 84 85 86 87 88 89 90
  91 92 93 94 95 96 97 98 99 100

다음으로 아래는 rate를 2로 증가시켰다.

  tf.image.extract_patches(images=images,
                           sizes=[1, 3, 3, 1],
                           strides=[1, 5, 5, 1],
                           rates=[1, 2, 2, 1],
                           padding='VALID')

  # Yields:
  [[[[  1   3   5  21  23  25  41  43  45]
     [  6   8  10  26  28  30  46  48  50]]

    [[ 51  53  55  71  73  75  91  93  95]
     [ 56  58  60  76  78  80  96  98 100]]]]

다시금 그림을 활용하여 input img를 표시하면 아래와 같다.

rates를 2로 늘린 효과는, 3x3 내에서 하나씩 띄어서 살피는 것이다.

아래를 보면 알 수 있듯, 하나의 3x3에 rates=2를 설정하면, 총 3+(2-1)+(2-1) = 5사이즈를 살펴본다.

1, 3, 5, 21, 23, 25, 41, 43, 45가 하나의 patch이다.

   *  2  *  4  *  x  7  x  9  x
  11 12 13 14 15 16 17 18 19 20
   * 22  * 24  *  x 27  x 29  x
  31 32 33 34 35 36 37 38 39 40
   * 42  * 44  *  x 47  x 49  x
   + 52  + 54  +  o 57  o 59  o
  61 62 63 64 65 66 67 68 69 70
   + 72  + 74  +  o 77  o 79  o
  81 82 83 84 85 86 87 88 89 90
   + 92  + 94  +  o 97  o 99  o

이렇게 획득한 patches는 4D텐서다(위를 보면 알 수 있듯...)

 


Normalization 방법 :

norm = Normalization()
norm.adapt(X_train)

 

이 때, 1) 레이어의 평균 및 분산 값은 시공 시 제공되거나 adapt()를 통해 학습되어야 한다.

2) adapt()는 데이터의 평균과 분산을 계산하고 레이어의 가중치로 저장한다.

3) adapt()는 fit(), evaluate() 또는 predict() 앞에 호출되어야 함.

 

adapt() : 데이터셋 내의 값의 평균과 분산을 계산.

>>> data = tf.constant(np.arange(10).reshape(5, 2) * 10, dtype=tf.float32)
>>> print(data)
tf.Tensor(
[[ 0. 10.]
 [20. 30.]
 [40. 50.]
 [60. 70.]
 [80. 90.]], shape=(5, 2), dtype=float32)
 
 
>>> layer = tf.keras.layers.LayerNormalization(axis=1)
>>> output = layer(data)
>>> print(output)
tf.Tensor(
[[-1. 1.]
 [-1. 1.]
 [-1. 1.]
 [-1. 1.]
 [-1. 1.]], shape=(5, 2), dtype=float32)
 
 
>>>layer = tf.keras.layers.LayerNormalization(axis = 0)
>>>output = layer(data)
>>>print(output)
tf.Tensor(
[[-1.4142127  -1.4142127 ]
 [-0.70710635 -0.70710635]
 [ 0.          0.        ]
 [ 0.70710635  0.7071065 ]
 [ 1.4142127   1.4142128 ]], shape=(5, 2), dtype=float32)

위의 단순한 Normalization을 보면, data는 Normalization 이후 각 값들이 평균 0, 표준편차 1에 가깝게 바뀌는 것을 볼 수 있다.

 

그렇다면 아래를 보자.

>>> adapt_data = np.array([1., 2., 3., 4., 5.], dtype='float32')
>>> input_data = np.array([1., 2., 3.], dtype='float32')
>>> layer = tf.keras.layers.Normalization(axis=None)
>>> layer.adapt(adapt_data)
>>> layer(input_data)

array [-1.4142127, -0.70710635, 0. ]

원래대로라면 [ -1.4, 0, 1.4] 가 되어야 하지만,

실제로는 adapt_array를 기반으로 이미 분산, 평균이 이미 계산되어 있다.

즉, input_data는 이 adapt_array 기반으로 만들어진 정규화 값에 그저 대입되는 이다.

 

따라서, norm.adapt(X_train)도, X_train을 정규화 시킨 다음, 이후 들어올 값이 뭐든 간에 이를 기반으로 적절한 정규화 값으로 치환시킬 수 있다.


keras 내의 MHA(MultiHead Attention)

 

차원 전환의 예시.

https://keras.io/api/layers/attention_layers/multi_head_attention/

>>> layer = MultiHeadAttention(num_heads=2, key_dim=2)
>>> target = tf.keras.Input(shape=[8, 16])
>>> source = tf.keras.Input(shape=[4, 16])
>>> output_tensor, weights = layer(target, source,
...                                return_attention_scores=True)
>>> print(output_tensor.shape)
(None, 8, 16)
>>> print(weights.shape)
(None, 2, 8, 4)

Performs 2D self-attention over a 5D input tensor on axes 2 and 3.

>>> layer = MultiHeadAttention(
...     num_heads=2, key_dim=2, attention_axes=(2, 3))
>>> input_tensor = tf.keras.Input(shape=[5, 3, 4, 16])
>>> output_tensor = layer(input_tensor, input_tensor)
>>> print(output_tensor.shape)
(None, 5, 3, 4, 16)

 

도큐먼트에선, 

>>> mha = MultiHeadAttention(head_size = 128, num_heads = 12)
>>> query = np.random.rand(3,5,4)  # (batch_size, query_elements, query_depth)
>>> key = np.random.rand(3,6,5)  # (batch_size, key_elements, key_depth)
>>> value = np.random.rand(3,6,6)  # (batch_size, query_elements, value_depth)

>>> attention = mha([query, key, value])  # (batch_size, query_elements, value_depth)
>>> attention.shape

TensorShape([3,5,6])

의 형태로 나타난다.

즉, mha의 인자로 query, key, value가 함께 들어있는 배열을 전달한다는 것이다.

 

그런데 해당 코드에서는 다르다.

x = self.MHA(x, x)   # Our Target and the Source element are the same,

이렇게 두 개를 전달한다. Src와 Target이 같으니 SelfAttention에 부합하긴 하는데...

>>> attention = mha([query, key])

value가 주어지지 않을 경우, value = key로 보고 위처럼 작성할 수 있기도 하다. 여기서도 물론 1개의 배열을 전달.

 

어째서 도큐먼트와 윗페이지가 다른진 모르겠지만, Self-Attention인만큼 src=target으로 지정해야 하며, 따라서 MHA(x, x)의 형태를 띠는 것임을 알 수 있다.


"""
ViT 구조. 일단 이곳에서, 모델을 만든다.
실제 훈련을 시키는 건 아래에서... 
즉, 여러 구간에 그저 데이터size를 나타내는 텐서일 뿐인 inputs를 
각 클래스에 call시키는 이유는, 밑의 문단의
model = Model(...) 와 model.fit(...)에 들어갈 inputs과 outputs의 형태를
만들기 위함. 아래를 통해 완성한 inputs과 outputs 형태를 
Model과 fit에 넣고, 이 model에 X_train과 y_train을 넣는 과정을 거침.

1) (32, 32, 3) shape의 inputs용 더미(틀)을 만든다.

2) 입력값인 X_train(훈련시킬 것)을, 표준편차 1을 갖는 0을 중심으로 하는 분포로 이동시킨다.
이 정보를 저장한 norm에 X_train을 넣어 adapt시킴. 이에 대한 자세한 정보는 블로그에
아무튼 이걸 한 다음엔, 해당 정규화와 Size, Input을 Augmentation에 대입(SIZE = 72).
정규화 - resize - flip - rotate - zoom 을 실행
이 때, 기존의 트랜스포머 구조완 다르게, Normalization을 한 다음 MHA를 사용.

3) 이미지를 패치로 나눈다. 자세한 건 블로그 참조. - [순서1]

4) PATCH_ENCODER = 144. PROJECTION_DIMS = 64를 대입하여 patch-encoding을 진행.
자세한 건 상술.

5) 진또배기. NUM_HEADS= 4(즉, 하나의 patch에 대해 4개씩 돌림), 
Hidden units = [128,64], PROJECTION_DIMS(=key_dims = dk = query와 head의 attention head의 size) 대입

6) 레이어정규화 + flatten + Dropout + MLP 진행.

7) Dense - output레이어를 추가
"""
# 1) Input Layer
inputs = Input(shape = input_shape)

# 2) Apply Data Augemntation
norm = Normalization()
norm.adapt(X_train)
x = DataAugmentation(norm, SIZE)(inputs)

# 3) Get Patches
x = Patches(PATCH_SIZE)(x)

# 4) PatchEncoding Network
x = PatchEncoder(NUM_PATCHES, PROJECTION_DIMS)(x)

# 5) Transformer Network
x = Transformer(8, NUM_HEADS, PROJECTION_DIMS, HIDDEN_UNITS)(x)

# 6) Output Network
x = LayerNormalization(epsilon = 1e-6)(x)
x = Flatten()(x)
x = Dropout(0.5)(x)

x = MLP(OUTPUT_UNITS, rate = 0.5)(x)   # output_units = [2048, 1024]

# 7) Output Layer
outputs = Dense(100)(x)

구조 자체는 위처럼 되어 있는데

일단 Wq Wk Wv를 어떻게 학습시키는지는 차치하고(이후 더 알아내서 서술할 예정)

원래 비전트랜스포머의 과정과 함께 비교해보고자 함. 1과 2는 건너뜀.

 

3) Patches - 이미지를 패치로 나눈다. 이에 대해선 이미 서술했다.

 

4) PatchEncoder - 우선 Dense(x)를 한다. 즉 input 사이즈만큼 행렬을 곱해준다. 다음으로 position 값을 넣음. .

이는, 과정에 있어 패치를 나눈 것들에 임베딩을 한 다음, 포지션 임베딩 값을 넣는 것에 해당한다.

아마 무작위 행렬을 넣음으로써 임베딩이 형성되는 것 같기도.... Dense를 잘 보면, 출력 뉴런의 수를 D로 해놨는데,

따라서 기존 글에서 쓴 것 처럼 차원의 수를 (N, D)로 맞춰주기 위해 (N = x[패치의 개수), D[d_embed, 하나의 임베딩의 사이즈])로 맞추기 위해 넣은 게 아닐까 싶음.

 def call(self, X) :
    inputs = X
    x = X
    for _ in range(self.L):
      x = self.norm(x)
      x = self.MHA(x, x)   # Our Target and the Source element are the same,
      y = self.add([x, inputs])
      x = self.norm(y)
      x = self.net(x)
      x = self.add([x, y])
    return x

5) Transformer - 기존 코드는 위와 같다.

각 레이어 개수만큼(인코더 개수만큼) 
1) Layer정규화  
2) MHA 실행, 이후 원본(inputs)과 합침. 이를 따로 저장해둠(y)  
3) (MHA + 원본)을 정규화, 이후 MLP실행(위의 class).  
4) 이렇게 MLP 실행한 것에, (MHA + 원본)을 다시 더함.

이게 과정인데... MSA 글을 보면

인코더 과적은 좌측과 같다.

norm(=Layer Normalization) - MHA - add with Original(Residual Connection) - norm - net(Fully Connected Layer) - add with MHA+Original의 과정을 충실히 따르는 것을 볼 수 있다.

 

Residual Connection은 아래와 같이, 전체를 학습시키기 보단 F(x)를 줄이는 방향으로 나아가고자 하는 것이다. 이는 정보의 손실과 모델의 학습을 돕는다고 한다.

 

6) Output Network - 이렇게 잔차 연결을 한 다음엔 (정규화 - Flatten - Dropout) 처리를 한 다음 MLP처리를 한다. 이는 이전글의 과정(3)에 해당. 보면 알 수 있듯, MLP(LN)이므로, 정규화가 우선한다.

 

7) Output Layer - 마지막으론 Dense(100)(x)를 하는데, cifar100은 100개 object 검출이 목표이므로 dims=100을 넣는것으로 볼 수 있다.


여태껏 한 것을 톺아보면 의문점이 몇 가지 있다

1) W(가중치)는 대체 어디서 오는가? --> 조만간 해결 예정

2) 잔차는 왜 넣었는가? --> 잔차연결은, 아래층의 표현이 신경망 위로 흘러갈 수 있도록 하기에, 아래층에서 학습된 정보가 데이터처리과정에서 손실되는 것을 방지. 막말로 말해서, 이건 그냥 '해보니 잘되길래 넣어봤다'인듯?

3) LN은 왜 자꾸 하는가? --> 

'버츄얼유튜버' 카테고리의 다른 글

DataLoader 이해 - Boosting-Crowd-Counting... 을 기반으로  (0) 2022.11.29
ResNet 관련 링크  (0) 2022.10.12
Vision Transformer(22.03.03 재포스팅)  (0) 2022.09.08
Attention과 Transformer  (0) 2022.08.31
Generative Models  (0) 2022.08.11