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'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 |