1. 데이터 소스

  • 다양한 염색, 스캐너, 생물학적 오브젝트의 데이터셋을 구성했습니다. 공공데이터 뿐만아니라, 따로(Private)모은 데이터셋을 포함해서 총 158,852 Slide이미지를 구했습니다. 이 약 16만장의 이미지를 4개 종류의 Resolution으로 1억 9천만장의 패치를 얻었습니다. 
  • 장기 수: 50개 이상
  • 질환 그룹: 28종류 이상
  • 스캐너: 11종류
  • 배율: 20x, 40x 각
  1. 조직 영역 검출(Tissue regions): ArtifactDetection라는 PathAI의 Fore/Background 이미지를 분리하는 로직을 넣었습니다. 전경만 뽑았구요. 외부로 공개되지 않았지만 오츄알고리즘 보다, 다양한 상황(스캐너/염색) 등에서 더 좋은 성능을 보인다고합니다.
  2. 타일링(Tiling): 40x (0.25mpp), 20x (0.5mpp), 10x(1mpp), 5x (2mpp)로 배율로 위에서 얻은 전경의 티슈만 뽑았습니다.

 

2. 학습을 위한 구성요소(아키텍처)

PLUTO은 DINO v2의 학습방법을 기본적으로 따라갑니다. DINO v2은 DINO라는 라벨링 없이 하는 지식증류방법과 iBOT이라는 MIM(Maksed image modeling)방법으로 학습하는 방법입니다(URL).

DINO v2은 자연계에서 얻어진 이미지들이고, PULTO은 병리이미지를 위한 모델이니 이 차이점을 반영합니다. WSI에는 핵, 세포, 분비샘(gland)등 이 다양한 사이즈에서 관찰되니, 이를 더 잘 반영할 수 있도록(glanularity), MAE(Masked autoencoder)을 추가합니다.

이 아키텍처 및 학습방법에 들어간 기술적인 방법은 아래와 같습니다.

  1. DINO v2
  2. Masked AutoEncoder(MAE)
  3. FlexiViT scheme
  4. Fourier loss based term

이 중, 2, 3, 4번에 해당하는 내용만 서술해보겠습니다. DINO v2은 여기를 참고하세요


2. MAE(Masked AutoEncoder) objective: 입력이미지의 일부를 재구성하도록 합니다. MAE은 학습에 원본이미지의 다수(예 75%)을 마스킹하고, 인코딩한 결과를 각각 벡터로 얻고, 이 벡터로 이미지를 복원하는 연산을하는 디코더가 한 묶음입니다. 이 복원은 maksed patch로 픽셀을 만든 것만 MSE의 목적함수를 갖습니다 [URL]. 일반적인 오토인코더와의 차이점은 1)ViT을 이용하면서 더 작은 단위의 패치이미지로 재구성한다는 것, 2)마스킹 했다는 것이 차이점입니다.


 


또한, 패치사이즈를 유연하게 받을 수 있게, FlexiViT을 사용합니다. FlexViT을 이용하는 경우 병리이미지의 배율을 조정하여 학습하기 있기 때문에 사용합니다. FlexiViT의 기술적 설명 아래와 같습니다.

3. FlexiViT: 보통 Vision Transformer은 패치 크기를 낮추게되면서 패치수가 증가하는데 성능은 좋아지지만 컴퓨팅비용은 커집니다. FlexiVIt은 학습시에 랜덤하게 패치 사이즈를 조절해가면서(=token 수가 증가하거나 감소하면서) 학습하는 방법을 의미합니다.


4. Fourier-loss based term: 푸리에 관련 손실 함수를 목적함수에 추가했다고 합니다. 이 손실함수에 대해서는 명확히 설명/참조가 되어있지않지만, 이미지를 푸리에변환 후(URL), 복원한 푸리에주파수와 원본 푸리에주파수를 비교한 것처럼 보입니다.

 

3. 학습 방법 (구현)

PLUTO은 전체적으로 DINO v2의 아키텍처를 따릅니다. 자세한 순서는 아래와 같습니다.

  1. 샘플링: 여러 Resolution으로 타일링한 이미지에서 1장을 뽑습니다.
  2. Cropping: 이 한장으로 2장의 이미지를 만듭니다. 하나는 224 crop하여 global view을 만들고, 또 하나는 96 size로 만들어 local view로 만듭니다.
  3. Masking & Forwarding:
    1. forwarding: local view은 student 모델에 들어가 포워딩되고, global view은 teacher model에 포워딩됩니다. 당연히 DINO v2의 iBOT 구성을 따르기에 student model에 들어가는 crop view도 일부를 마스킹합니다. 두 모델은 FlexiViT입니다.
    2. parameter update: DINO v2와 같이 Teacher 모델의 파라미터는 student model의 파라미터를 EMA(Exponential moving average)을 한 후 업데이트하도록함.
    3. global view masking: global view에 해당하는 부분은 마스킹을 하여 FlexiVIT에 넣습니다.
    4. MAE decoder: student model에서 나온 임베딩값들을 MAE decoder모델에 넣어 적용합니다. 이 MAE에서 나온 masked patch embeddign 값은 global view의 patch 픽셀들과 비교하게됩니다(L2 loss). 
    5. Fourier transform loss: 재구축한 푸리에변환 주파수와 global view의 푸리에주파수를 비교합니다. L2 loss 입니다.

 

요약하면 총 손실함수는 $L= L_{DINO} + L_{iBOT} + L_{MAE} + L_{Fourier}$ 으로 구성됩니다.

 

Slide level prediction

Slide level prediction은 addictive MIL을 이용했습니다. Addictive MIL은 PathAI에서 고안한 MIL방법론이며, 사후해석이 아니라 모델 자체에서 해석이 가능하도록 집계하는 방식입니다(URL).

$p_{Addictive}(x)=\sum_{i=1}^{N}\psi_{p} (m_{i}(x))$

  • $ \psi_{p} (m_{i}(x))$: $i$패치에서 class-wise contibution을 계산합니다. 패치수가 N개고 클레수가 C개라면 $\mathbb{R}^{C}\timesN$의 메트릭스를 생산합니다. 이후 N축(axis=1)으로 합하여 C만남겨, confidence을 계산합니다. 

이 Addictive MIL에서는 이론적인 설명이 매우 잘되어있습니다.

 

반응형

 

요약


iBOT(masked image modeling)과 DINO(knowledge distillation with no label)방법을 혼합한 방법

 

Transactions on Machine Learning Research (01/2024)

 

Data processing: LVD-142M dataset의 생성과정

많은 양의 데이터 학습을 위해, 데이터 처리방식을 1) 라벨, 비라벨 이미지 수집, 2) 중복이미지 제거, 3) 이미지 검색

  1. 데이터 수집: 데이터 수집은 라벨링된 데이터와 라벨링 안된 데이터 크게 2가지의 데이터셋을 구축했습니다. 라벨링 된 데이터는 classifcation, segmentation, depth estimation 등의 다양한 문제에 사용되는 공공데이터를 수집했습니다. 다른 한편으로는 웹크롤링을 통해서 <img>테그가 있는 경우를 모두 가져오고, PCA hash dedupliation 등을 이용해서 이미지의 중복을 제거했습니다.
  2. 중복제거(de-duplication): 비슷한이미지만 모여있으면, 이미지의 다양성을 학습하기 어려우니, 유사한 이미지들을 제거하기위해서 중복제거를 image-copy-detection알고리즘을 이용해서 처리했습니다.
  3. Self-supervised image retrieval: 라벨링된 데이터셋과 라벨링안된 데이터셋에서도 매우 비슷한 이미지가 있을 수 있기때문에,  이를 제거해야합니다. 첫 번째로, 이미지를 사전학습된 ViT-H/16로 "비정제 /정제 "임베딩 시키고 각 이미지를 코사인유사도로 유사성을 평가합니다. 그 후, K-means clustering을 이용해서 "비정제" 데이터를 군집화합니다. 하나의 정제 이미지의 벡터를 이용해서, 가장 가까운 N(4)의 이미지를 검색합니다. 1개의 정제데이터에 대해서, 비정제 데이터 4개의 유사한 Cosine similarity을 확인하고, 유사도가 낮으면(=이미지가 충분하지 않다면), 쿼리 이미지가 속한 클러스터에서 추가로 M개를 샘플링해서 보완합니다.

실제 구현은 vector DB로 쓰이는 Faiss 을이용해서 진행했습니다. 약 20개의 노드에서 위 과정을 분산처리하여 LVD-142M dataset을 생성합니다.

 

Discriminative Self-supervised Pre-training

이 연구에서 제안하는 방법론은 DINO와 iBOT loss을 혼합하는 방법론입니다. 이를 혼합하기 위해서, Student모델과 Teacher model은 각각 2개의 head을 가지고 있게됩니다.

1. Image-level objective: DINO v1에서 사용했던 것과 같습니다. Student, Teacher 모델이 있고, Student model이 반환하는 vector 와 teacher model이 반환하는 vector가 유사해지는 것을 목적으로합니다. (+centering + moving average에 대한 설명). 그 후, 파라미터는 EMA(Exponential moving average)로 student model에서 teacher 모델로 반영합니다.

$L_{DINO}=-\sum p_{t}log p_{s}$

2.Patch-level objective: iBOT을 이용한 자기지도 학습입니다. iBOT은 MIM(Masked image modeling0)으로 이미지를 더작은 패치로 나눈다음에 훈련하는 방법을 의미합니다. 이렇게 만든 저 작은 패치를 랜덤으로 마스킹합니다. (Fig 2. iBOT) 마스킹된 경우 마스킹 토큰만 줍니다. 

  1. $\textbf{x}=\{x_{i}\}^{N}_{i=1}$: 이미지를 작은 토큰으로 생각해서, 패치단위로 N개로 잘게 쪼갭니다.
  2. $\textbf {m}^{N}$: 마스킹인덱스입니다. 0 또는 1입니다. N개의 패치마다 마스킹할건지 여부를 결정합니다. $m_{i}=1$이면 마스킹된 상태입니다.
  3. $\tilde{x}\triangleq \{\textbf {x}_{i} | m_{i}=1\}$: 마스크된 이미지를 의미합니다. m=1인 경우, 이미지들이 마스크 토큰으로 변경됩니다.
  4. $\hat{x}\triangleq  \{\tilde{x}|(1-m_{i})x_{i} +m_{i}\textbf {e}_{[MASK]}\}$: N개의 패치가 마스크 토큰 또는 보존

학습의 목적은 마스크된 토큰($\tilde{x}$)을 복원하는 학습을 합니다. 아래의 그림 Figure 2에서, VIT(student)에는 마스크된 토큰을, teacher 모델에서는 전체의 패치를 다 본 후, masking 된 패치만 가져와 두 분포가 동일하도록 하는 것을 원합니다.

$L_{MIM} = -\sum _{i=1}^{N}m_{i}\cdot P_{\theta}^{patch}(u_{i})^{T} log P_{{\theta}}^{patch}(\hat{u_{i}})$

위 식에서 $m_{i}$가 곱해져서 반드시 마스킹된 경우만, 임베딩값을 복원하는 문제로 변경됩니다. 여기서도 DINO와 마찬가지로 softmax, centering이 쓰입니다. 논문에서는 아래와 같이 간략하게 표현했습니다. 

$L_{iBOT}=\sum_{i} p_{ti}logp_{si}$ (i:masked patch index, t: teacher model, s: student model)

Figure 2. iBOT의 MIM의 방식의 예시

 

3. 두 해드의 파라미터는 공유: Zhou et al(2022)에 논문에선서는 DINO와 iBOT의 해드 파라미터를 공유하는게 더 나은 성능을 보였으나, 본인들이 실험해보니 따로두는게 더 나았다고합니다.

4. Sinkhorn-Knopp centering: DINO에서는 teacher모델의 표현을 centering 후의 temperature scaling을 진행합니다. DINO v2에서는 SK(Sinkhorn-Knopp) batch normalization을 사용하는게 더 낫다고하여 Sinkhorn-knopp algorithm을 진행합니다.

5. KoLeo regularizer: 배치내에서 피쳐가 균등하게 만들기위해서 KoLeo regularizer을 사용합니다. KoLeo 정규화전에 각 벡터에 L2 norm을 진행합니다. 

$L_{koleo}= -\frac{1}{n}\sum_{i=1}^{n}log(d_{n,i})$,

$d_{n,i}=min_{j=i}||x_{i}-x{j}||$은 벡터i와 배치 내에 어떤 벡터든간의 최소거리를 의미합니다. 벡터를 L2 norm을 진행하면 거리가 1로 정규화되고, 각만 달라지게됩니다. 이 벡터들간의 거리가 좁을 때, 점점더 좁아지면 -log값이 매우커지니 서로 띄어놓게하기위해(uniform span) 이 정규화를 진행합니다. 

KoLeo regulariziation

6. Adapting the resolution: 작은 이미지를 패치로 나누면 더 작게되어 pixel level task(semgnetaion, detection)등에서 성능열하가 발생합니다. 그렇기에 학습후반에만 일부 큰 이미지518x518로 학습합니다.

 

 

DINO v2의 PCA시각화

비지도학습을 이용해서, 임베딩값이 어떻게 픽셀상에 표현되는지 알기위해, 각 이미지를 RGB로 3개의 주성분을 가진 경우로 표현합니다. 임베딩을 주성분(각 성분은 RGB로 취급)하여 시각화합니다. 이 단계는 4단계로 구분됩니다.

 

  • 첫 번째 PCA 수행:
    • 모델을 사용하여 이미지 패치(조각)들을 추출하고 이를 통해 특성(features)을 얻습니다.
    • 이 특성들에 대해 첫 번째 PCA를 수행하여 주성분들을 구합니다.
    • 첫 번째 주성분(첫 번째 컴포넌트)을 기준으로, 값을 임계값(threshold) 이상인 패치들만 남깁니다. 이렇게 하면 이미지의 주요 객체와 배경이 분리됩니다. 이 과정에서 첫 번째 주성분 방향이 가장 큰 분산을 설명하기 때문에, 주성분 값을 기준으로 분리하면 주로 배경과 전경이 나눠집니다.
  • 배경과 전경 분리:
    • 첫 번째 주성분 값을 기준으로 배경과 전경이 분리된 후, 배경 패치를 제외한 나머지 패치들만 남깁니다. 즉, 전경 패치들만 남기게 됩니다.
  • 두 번째 PCA 수행:
    • 전경 패치들에 대해 두 번째 PCA를 수행합니다. 여기서 주성분 3개를 추출합니다.
    • 이 주성분들은 이미지의 주요 객체의 다양한 부분들을 설명하는 데 사용됩니다.
  • 시각화:
    • 두 번째 PCA로 얻은 3개의 주성분을 각각 R, G, B 채널에 할당하여 시각화합니다. 이렇게 하면, 이미지의 주요 객체의 다른 부분들이 서로 다른 색상으로 표현됩니다.

 

 

아래의 이미지를 샘플로 진행해보겠습니다.

from torchvision.io.image import read_image
from torchvision.transforms import ToPILImage
from matplotlib import pyplot as plt
image = read_image("....png")

plt.imshow(ToPILImage()(image))

 

dino 모델의 입력스케일이 [0, 1]의 float이기 때문에 /255로 나누고 `forward_features`메서드로 포워딩해줍니다.  features은 dictionary 형태로 나오고, cls token을 포함한 여러가지 output이 나옵니다. 이 중에서, 패치단위로 나눈 토큰(token)의 벡터를 구하기 위해서 "x_norm_patchtoken"의 값을 가져옵니다.

embedding_patch(E_patch)의 shape은 (1, 256, 178)인데, 1은 하나의 이미지, 256은 token의 수(패치수), 768은 임베딩 차원을 의미합니다.

import torch
from einops import rearrange
from torchvision.transforms import Normalize
from torchvision.transforms.functional import resize

IMAGENET_DEFAULT_MEAN = (0.485, 0.456, 0.406)
IMAGENET_DEFAULT_STD = (0.229, 0.224, 0.225)

norm = Normalize(mean=IMAGENET_DEFAULT_MEAN, std=IMAGENET_DEFAULT_STD)

I_norm = norm(image / 255)
dinov2 = torch.hub.load('facebookresearch/dinov2', 'dinov2_vitb14')
features = dinov2.forward_features(I_norm.unsqueeze(0))
E_patch = features["x_norm_patchtokens"]
print(E_patch.shape) # (1, 256, 768)


<class 'dict'> dict_keys(['x_norm_clstoken', 'x_norm_regtokens', 'x_norm_patchtokens', 'x_prenorm', 'masks'])
torch.Size([1, 256, 768])

 

PCA을 돌리기위해서 3차원의 1의 배치를 squeeze하여 2차원으로 변경해줍니다. (1, 256, 768) -> (256,768). 그리고, PCA로 주성분을 구하고, 그 주성분중에 가장 큰 주성분 벡터(V[:, 0])을 뽑아서, 임베딩 차원 -> 주성분차원으로 정사영(projection)시켜줍니다.

E_patch_norm = E_patch.squeeze(dim=0)
print(E_patch_norm.shape)

_, _, V = torch.pca_lowrank(E_patch_norm)  # V은 주성분. 각 벡터는 직교(orthogonal)

E_pca_1 = torch.matmul(E_patch_norm, V[:, 0])

주성분으로 프로젝션된 각 패치들 중에는 배경도 있고, 전경도 있을것입니다. 이 주성분은 가장 분산이 큰 설명하는 벡터이기에 배경/전경을 설명할 수 있습니다. 따라서, 주성분 1번(인덱스0번)에 내적하여 projection시킵니다. 

프로젝션된 벡터를 정규화하여, threshold을 걸어 배경과 전경을 분리합니다. 각 256 패치들이 전경에 속하는지, 배경에속하는지에 대한 마스킹을 갖계됩니다(boolean). 여기까지가 첫 번째 PCA이후, threshold을 걸어 배경과 전경을 분리하는 것입니다..

def minmax_norm(x):
    """Min-max normalization"""
    return (x - x.min(0).values) / (x.max(0).values - x.min(0).values)
    
E_pca_1_norm = minmax_norm(E_pca_1)
print(E_pca_1_norm.shape)
M_fg = E_pca_1_norm.squeeze() > 0.5 # (256, )
M_bg = E_pca_1_norm.squeeze() <= 0.5 # (256, )

 

여기서, 전경의 픽셀만 다시 분리하여 PCA을 새롭게 진행합니다. 이 두 번째 PCA의 목적은 주성분을 3개를 뽑아서RGB에 대응시켜 모델이 어느 패치(포지션)을 보고 임베딩을 했는지, 의미있게 임베딩했는지 확인해보자는 것입니다. 마찬가지로, 전경만 뽑아 PCA을 합니다.

PCA의 결과는 (N, 3)입니다. N은 전경의 패치수, 3은 프로젝션된 값입니다.

# 전경 패치만 뽑아서, PCA을 돌립니다.
_, _, V = torch.pca_lowrank(E_patch_norm[M_fg])

# PCA에서 가장 분산이 큰 주성분 3개를 뽑아 각 벡터에 projection합니다.
E_pca_3_fg = torch.matmul(E_patch_norm[M_fg], V[:, :3])
E_pca_3_fg = minmax_norm(E_pca_3_fg)  # (N, 3)

 

여기서 (N, 3)을 다시 이미지로 그려야합니다. (256,)의 패치에 전경부분만 (N, 3)의 (, 3)의 값들을 넣어줘야합니다. 그리고 각 패치에 맞도록 다시 rearange해줍니다. 이 과정은 (256, 3)을 패치 형태에 맞는 (16, 16, 3)으로 바꾸는 과정입니다.

B, L, _ = E_patch.shape  
Z = B * L
print(Z)
I_draw = torch.zeros(Z,3)
I_draw[M_fg] = E_pca_3_fg
I_draw = rearrange(I_draw, "(B L) C -> B L C", B=B)
I_draw = rearrange(I_draw, "B (h w) C -> B h w C", h=224//14, w=224//14)
print(I_draw.shape)

 

R, G, B형태에 맞게 시각화하고 resize해줍니다. 아래의 우측그림과 같이 우 하단영역의 염증부분에 더 많이 강조가 됨을 알 수 있습니다. 반면 좌측상단은 stroma부분으로 이 부분을 주의깊게 설명하는건 아니라는 것으로 해석해볼 수 있습니다.

 

# Unpacking PCA images
image_1_pca = I_draw[0]

# To chanel first format torchvision format
image_1_pca = rearrange(image_1_pca, "H W C -> C H W")

H, W = 224, 224
# Resizing it to ease visualization 
image_1_pca = resize(image_1_pca, (H,W))

from matplotlib import pyplot as plt
fig, axes = plt.subplots(1, 2)
axes[0].imshow(ToPILImage()(I))
axes[1].imshow(ToPILImage()(image_1_pca.detach()))

반응형

 

 

matrix factorization 하면 되는거 아님?

기존 방법론에는 SVD, BCD 등의 방법론이 있습니다.

  • SVD 의 문제점: 벡터의 방향을 고유벡터의 5%, 95%값 사이의 있는 벡터를 고름에따라서 이미지 간의 컬러분포(inter-image variation)에 좀 강건하다고는 합니다. 하지만, 전문가가 타깃이미지를 따로 정해주는것대비 별로 성능이 좋지 않다고합니다. 그리고, 고유벡터들이 음의값을 가지거나, 희소성을 보장하지 못하는 경우가 있어서 사용하긴 어렵습니다. 희소성을 가져야하는 이유는 염색이 된 밀도가 각각의 픽셀에서 균등하게 흩어져있으면, 물체(조직)의 구분이 뚜렷하지 않기에 희소성을 갖도록합니다. + 음의 값이 나오면 해석이 어렵습니다. stain density값이 (-)의면 빛이 발산한다는 것이기 때문입니다.
  • BCD(Blind color decomposition): 다른 방법론 대비 RMSE가 낮은 matrix factorization이지만 최적화할때 쓰이는 하이퍼파라미터 선정이 매우 까다로워 사용하기 곤란합니다.

 

SPCN의 주요가정: Non-negativity, sparsity

  • Non-negativitiy: 현미경으로 볼때, 조직내에서 빛을 발산하는게 아니라, 흡수만하기 때문에 positive density을 가집니다. 0이면 받은 빛을 모두 투과시키는 것이고, positive이면 조직이 있어서 일부만 흡수되는 것입니다.
  • Sparsity: 각 픽셀은 조직의 일부를 표현하고있고, 그 조직은 하나 이상의 염색을 가지고 있다는 것입니다. 예를 들어, 핵 부분은 Hematoxylin에 염색이 주로되고, Eosin에는 소량만 된다는 것입니다. (H와 E에 배타적으로 염색되는 것이 아닙니다). 즉 H&E 각 고유염색의 혼합으로 표현이 가능하다는 것입니다.
  • Soft classification: 조직과 조직사이의 가장자리(엣지)의 일부는 2개 이상의 염색이 되어있을 것이다라고 가정

SPCN 방법론

H&E slide을 V를 광학밀도(optical density)로 표현된 이미지로 표현할 수 있습니다. 각 픽셀값을 $V=log\frac{I_{0}}{I}$으로 변경할 수 있습니다. 즉 최대값대비(I0=255, 8bit image) 얼마나 조직에서 흡수되고, 통과된빛이 센서에 남느냐에 대한 지표입니다. 이 V값은 사실, 염색약과 조직이 흡수한정도의 곱으로 표현할 수 있습니다.  그렇기에 아래와 같이 표현이 가능합니다.

$V=WH$ 

  • W은 staining estimation (stain vector)로 염색약의 고유색(color appearance라고도 함)을 의미합니다. m, r 형태며, 컬럼벡터가 염색의 basis(R,G,B 고유색상)이며, r은 염색수를 의미합니다. H&E염색이면 2에 해당됩니다.
  • H은 조직이 흡수한 정도를 의미합니다(Stain density).

 

1. 염색 분리(Stain separation, SNMF-based Stain separation): vahadane의 방식은 V을 W,H로 분해하는 것으로 시작합니다. 아래 목적함수를 만족하려면, W, H가 잘분해되어 V와 같아지면되는데, 이 최적화가 non-convex이기에 global optimum을 만족할 수 없을 수 있습니다(=stain vector(W)추정이 안될수있음). 

$min_{W,H} \frac{1}{2}||V-WH||_{F}^{2}$, such that W, H >= 0

W, H가 항상 양수이여이하기때문에 NMF(Non-negative matrix factoriziation)을진행하는데, vahadane은 좀더 진보된 NMF을 제안합니다. W, H가 양수여야하는 이유는 염색자체가 빛을 방출하지는 않고, 흡수만 되기 때문에 그렇습니다.

 

$min_{W,H} \frac{1}{2}||V-WH||_{F}^{2} + \lambda \sum_{j=1}^{r}||H(j,:)||_{1}$, such that W, H >= 0, $||W(:, j)||^{2}_{2}=1$ (5)

이 식을 표현해보면, 아래와 같습니다.

  • $||W(:, j)||^{2}_{2}=1$ : 각 염색별로 고유색상의 R,G,B 흡수욜은 1이 되어야함
  • $ \sum_{j=1}^{r}||H(j,:)||_{1}$: 염색별로의 편차가 작아져야함.

근데, 위 5번식을 바로 못풉니다. 그렇기에, W또는 H을 하나 랜덤으로 초기화 후에 고정해놓고, 최적화 합니다. 예를 들어, V은 주어진거니까, W을 고정하고 H을 최적화합니다. 또 한편으로는 H을 고정하고, W을 최적화합니다. 최적화 방법은 sparse coding/dictionary learning 라는 방법으로 최적화합니다.

2. Structure-preserving color normalization (SPCN)

1의 stain separation 방법을 적용하면, 금방 stain vectors(matrix, W)와 densitiy(H)을 구할 수 있습니다. 이 방법을 이용해서 다음과 같이 진행합니다.

  1. Source image s 에 대해서 $W_{s}, H_{s}$을 구합니다.
  2. target image t 에 대해서  $W_{t}, H_{t}$을 구합니다.
  3. source image s -> t로 변환하는 작업: 소스 이미지의 염색밀도(stain density, $H_{s}$)에 target image의 염색을 곱해줍니다. $W_{t}H_{s}$ (이 작업으로 염색밀도의 구조적인 내용이 보존된다고하고, 단지W염색만 변경됩니다)

 

 

Macenko (PCA based) stain normalization 차이

Macenko et al 의 방식은 염색의 sparseness와 non-negativity을 가정하지 않았기 때문에, 정규화 후에 구조적 정보가 회손될 수 있다고 합니다. 

 

Structure preserving 이라고 하는 이유

이 방법론은 소스 이미지의 염색벡터랑 염색밀도를 각각 구한 다음, "타깃 이미지의 염색벡터"와 "소스 이미지의 염색밀도"를 곱하는 연연산이기 때문에, 염색 밀도는 변하지않고 염색의 색감만 변하게 됩니다. 즉 염색밀도측면에서는 구조적정보가 보존이 됩니다.

반응형

요약


Cyclic learning rate (CLR) 은 learning rate을 수동으로 조절해갈 필요없이, 자동으로 최적의 값을 찾는 과정의 스케쥴링입니다. CLR은 LearningRateReduceOnPlatue와 같이 감소하는 방향으로만 학습율을 조절하는 것이 아니라, 특정 영역에서 증감을 반복합니다. 그리고 이를 실험적으로 증명했습니다.

 
 
 
 

Deep learning 에서의 Learning rate

딥러닝의 파라미터 업데이트은 stochastic gradient descent로, 아래와 같은 식으로 학습합니다.

$\theta^{t}=\theta^{t-1}-\epsilon_{t}\frac{\partial L}{\partial \theta}$

  • L: 손실함수
  • $\epsilon_{t}$: 학습

문제는 학습률이 너무 작으면 학습의 수렴이 느려지고, 너무 큰 경우는 학습의 수렴을 보장할 수 없습니다. 이를 해결하기위해서, CLR이 나오기 전까지는 단순히 학습율을 줄여나가는 방식등을 사용했습니다. 

 

 

Cyclic learning rate (CLR)

CLR을 사용하는 경우는 아래의 그림(Figure 1)과 같이 더 적은 iteration내에 빠른 수렴을 실험적으로 할 수 있음을 보여주었습니다. CLR은 학습율을 증가시키는 것이 짧게는 학습에 부정적인 영향을 끼치지만, 학습의 긴 추이에서는 더 나은 최적화를 보여준다는 것을 경험적으로 확인해서 제안된 것입니다.

 

Cyclic learning rate은 최조값(minimum bound, base_lr), 최대값(Maximum bound, max_lr)을 지정해서 주기적으로 학습율을 증/감시키면서 학습합니다. 학습율을 어떻게 증감시킬지의 방법(policy)을 3가지로 제안합니다. 아래의 3가지 방법이 가능하며, 어떤 것을 사용하더라도 큰 차이없다고 합니다. 

  1. Triangular window (linear)
  2. Welch window (parabolic)
  3. Hann window (sinusoidal)

아래는 triangular window (linear)의 방법입니다. 최대학습률, 최소 학습률, 스텝수(stepsize)의 3가지 파라미터를 사용자가 지정했을 떄, 최대최소율에 맞추어 증가, 감소하며 학습률이 변경됩니다. stepsize 반복할때까지 maxmum bound까지 올라가고, 이후 그 다음 stepsize까지 반복해서 최소학습률에 도달하는 식입니다. 파이토치에는 이를 구분하여 구현되어있습니다.(step_size_up, step_size_down) 

CLR이 효과적인 이유(주장)

1. Saddle point에서 학습률을 크게해서 탈출이 가능: Dauphin et al 연구팀의 주장에 따르면  학습이 제대로 안되는 이유는 로컬미니멈(local mimima)에 빠져서라기보단, 안장점(saddle point, gradient가 0 또는 거의0 지점)때문이라는 주장이 있습니다. 아래의 그림과 같이 saddle point인 경우, 평평하기 떄문에 gradient값이 작아 학습이 느리게 됩니다. 이때 학습률을 크게 증가시키면 이 saddle point을 탈출할 수 있다는 것입니다. 

2. 최소/최대값의 범위를 진동하며 사용하니, 최적의 학습률이 사용이 된다는 것

 

Saddle point, local minima , src: https://www.researchgate.net/figure/Definition-of-grey-level-blobs-from-local-minima-and-saddle-points-2D-case_fig1_10651758

 

pytorch implementation. base_lr, max_lr, step_size 정하기

pytorch에서는 아래와 같이 torch.optim.lr_schedular.CyclicLR로 CLR이 구현되어있습니다. 논문에서 언급했던 것과 같이 base_lr, max_lr, stepsize을 지정해야합니다. pytorch에서는 step_size가 up구간, down구간으로 나눠져있습니다. 이 인자들을 어떻게 전달해야하는지 알아보겠습니다.

torch.optim.lr_scheduler.CyclicLR(
	optimizer, 
    base_lr, 
    max_lr, 
    step_size_up=2000, 
    step_size_down=None, 
    mode='triangular', 
    ...,
    verbose='deprecated'
)

 

1. base_lr (minimum lr), maximum lr 구하기

  • LR range test: 모델을 적은수의 에폭만 미리 돌려보는것입니다. 예를 들어, 2에폭정도에 learning rate을 0부터 0.02까지 바꿔본다고 생각해보겠습니다. 2에폭정도 돈 후에, learning rate, accuracy의 라인플롯을 그립니다. accuracy가 나오게되는데, 0부터 증가했다가 감소하는 구간을 보입니다. 저자들은 증가하는 시점의 learning rate과 감소하는 시점의 learning rate이 base_lr, max_lr로 선택하는 것이 좋다고 주장합니다. 또는 최적의 학습율을 보이는 수의 2배 내에 있기 때문에, base_lr과 max_lr의 크기가 3~4배정도 주게 하면 좋다고합니다. 

또 다른 예시로, AlexNet의 LR range test의 결과를 보여드리겠습니다. 학습율(x축, learning rate)이 0.006이전까지는 학습이없다가 0.006정도부터 학습이 되기에, base_lr은 0.006으로 셋팅하면됩니다. 그리고, 이 그레프에서 0.015부터 학습률이 지속적으로 감소하기에 max_lr은 0.015으로 학습하는게 합리적이라고 합니다.

2. stepsize (cycle length) 구하기. stepsize은 배치사이즈를 구하면 1에폭당 얼마의 step으로 갈지 계산할 수 있습니다. CIFAR-10같은 경우 학습데이터가 50,000장의 이미지가 있는데, 배치사이즈가 100이라면, 500step(iteration)을 하게됩니다. CLR은 stepsize에 따라 성능차이가 크게 없다고는하지만, 보통 iteration(step)수의 2배 ~ 10배정도 설정하면 좋다고합니다. (*주의 논문에서는 하나의 배치사이즈를 학습한 것을 epoch)이라고 합니다. 논문에서 제시한 것과 같으면, 위의 예시에서 CIFAR-10의 배치사이즈가 100이었으면, 총 스텝수가 500이므로, CLR의 step으로 들어가는 것은 4,000정도 (=8*500)가 좋은 선택이라고합니다.

반응형

'Best Paper review' 카테고리의 다른 글

[5분 컷 리뷰] Supervised Contrastive Learning  (0) 2024.07.23

Poetry 설치

Poetry은 "https://install.python-poetry.org/"에 있고, 파이썬으로 excute가능하게 되어있어서, Linux, MacOS, WSL에서아래와 같은 명령어로 설치가능합니다.

$ curl -sSL https://install.python-poetry.org | python3 -

// 환경변수까지 추가합니다.
// .bashrc에 "excutebable binary을 바로 환경변수로 추가합니다"
$ vi ~/.bashrc

// vi에서 아래를 추가
"export PATH="/home/heon/.local/bin:$PATH"

$ poetry --version
Poetry (version 1.8.3)

 

TOML(Tom's obvious Minimal Language) 포맷

TOML파일은 structured data을 저장하는 목적으로 JSON포맷과 달리 여러 기능을 지원합니다.

  • key-value로 이뤄져있습니다.
  • 사람이 쉽게 읽고 쓸 수 있는 형태로 디자인되었습니다.
  • 간단한 구조를 가지고 있고, 주석 기능이 포함되어 있습니다.
  • TOML은 각각의 설정 항목을 [...] 섹션으로 그룹화합니다.  

아래와 같은 형태로 []을 이용하여 그룹화가 가능합니다. [too.poetry]은 Poetry프로젝트의 metadata을 저장하는 부분입니다.  이후에 [tool.poetry.dependencies]로 필수프로젝트의 의존성을 설치하고,  [tool.poetry.dev-dependencies]은 개발에 필요한 부분만 의존성으로 설치할 수 있습니다. 개발이기 때문에, 배포환경(production)에서는 필요하지 않는 것들이 포함될 수 있어 분리할 수 있습니다.

[tool.poetry]
name = "example-project"
version = "1.0.0"
description = "This is an example project"
authors = ["John Doe <john@example.com>"]

[tool.poetry.prod]
python = "^3.8"
requests = "^2.25.1"

[tool.poetry.dev]
pytest = "^6.2.2"

 

파이썬 프로젝트가 이미 있는경우 poetry로 초기화: poetry init

보통 개발이 충분히 진행된 경우에 poetry로 배포를 염두해서 작업하기에, 파이썬 프로젝트가 이미 있을 수 있습니다. 이런 경우 다음의 명령어로 초기화해서 사용할 수 있습니다. 초기화하는 경우 toml파일의 간략히 만들어줍니다.

$ poetry init

 

$ cat pyproject.toml 
[tool.poetry]
name = "testname"
version = "0.2.1"
description = ""
authors = [".... hoheon0509@gmail.com>"]
license = "MIT"
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.8"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

 

종속성 명시하기

poetry에서 종속성을 명시하는 부분은 "[tool.poetry.dependencies]"을 명시하면됩니다. 이 명시된 종속성들은 2가지의 리포지토리에서 검색해서 설치하게됩니다. 

  1. tool.poetry.source: tool.poetry.source에 명시하면 명시된 소스에서 종속성을 설치합니다.
  2. PyPI: 디폴트로 PyPI에서 검색해 설치합니다.

최신버전을 설치하고싶으면 "*"로 명시합니다.

[tool.poetry.dependencies]
python = "^3.8"
fastapi = "*"

또는 CLI을 이용해서 추가가 가능합니다. 추가하면 추가하면서 설치도 동시에 진행됩니다.

$ poetry add pytorch

 이 때, 설치 환경에 따른 지정도 가능합니다.

$ poetry add torch --extras dev

 

 

 

개발/배포 환경에 따른 종속성설치: poetry install 

poetry은 여러 옵션이 있어 상황에 맞게 설치할 수 있습니다.

  • --no-root: 루트 패키지를 매번 설치할 것인지 여부("--no-root" 지정시 스킵됨)
  • --extras [group명]: 그룹명 맞춰서 설치할 수 있습니다.

 

// 필수종속성만 설치하는 경우
$ poetry install

// 필수 + 개발환경 종속성
poetry install --extras dev

// 필수 + 배포환경 종속성
poetry install --extras prod

--https://python-poetry.org/docs/#installing-with-the-official-installer

 

Introduction | Documentation | Poetry - Python dependency management and packaging made easy

If you installed using the deprecated get-poetry.py script, you should remove the path it uses manually, e.g. rm -rf "${POETRY_HOME:-~/.poetry}" Also remove ~/.poetry/bin from your $PATH in your shell configuration, if it is present.

python-poetry.org

 

poetry run 명령

poetry을 이용해서 의존성을 설치한 python 스크립트를 실행할 수 있습니다.

poetry run pytest

 

단점

1. poetry에서 파이썬 버전을 아래와 같이 지정한 경우, 파이썬 버전을 만족하는 파이썬 인터프리터가 설치되어있어야합니다.  poetry가 직접 이 파이썬 인터프리터를 설치해주지 않습니다. 따라서, 가상환경을 이용해서 파이썬 인터프리터 버전을 미리 사전에 맞추어야합니다.

 

 

poetry로 가상환경에서 의존성설치하기

$ poetry shell

poetry shell 명령어는 Poetry를 사용하여 관리되는 가상환경을 활성화하는 명령어입니다. 이를 통해 사용자는 프로젝트에 필요한 의존성이 설치된 가상환경에서 작업을 할 수 있습니다.

이 명령어를 이용하면 아래와 같은 기능을 이용할 수 있습니다.

  1. 가상환경 활성화: poetry shell 명령어를 실행하면, Poetry가 해당 프로젝트에 대해 생성한 가상환경이 활성화됩니다. 이는 가상환경 내의 Python 인터프리터와 패키지들을 사용할 수 있게 해줍니다.
  2. 쉘 시작: 활성화된 가상환경 내에서 새로운 쉘이 시작됩니다. 이 쉘에서는 pyproject.toml 파일에 정의된 의존성이 자동으로 적용됩니다.

poetry shell로 가상환경을 만들었다면, 이미 작성된 toml파일을 이용해서, 의존성을 설치해주면 됩니다.

$ poetry install

poetry 가상환경이 활성화되어있기 때문에, 이 의존성을 poetry 가상환경내에 설치가 됩니다.

실제로, 파이썬의 의존성의 주소를 확인해보면 다음과 같습니다.

>>> import torch
torch.__version__

>>> torch.__version__
'2.3.1+cu121'

>>> torch.__path__
['/mnt/unity_ssd/home/heon_slurm/.cache/pypoetry/virtualenvs/seedp-project-wf8_zn9U-py3.11/lib/python3.11/site-packages/torch']

 

가상환경의 주소는 아래와 같습니다. 이 주소에 디렉토리에 접근이 가능하다면, poetry 가상환경을 다른 노드나 다른 사용자가 사용할 수도 있습니다.

$ poetry env info --path
/data/home/heon_slurm/.cache/pypoetry/virtualenvs/seedp-project-wf8_zn9U-py3.11
source [poetry_env_path]/bin/activate

 

환경 삭제

poetry env list라는 명령어로 환경 목록을 출력합니다.

$ poetry env list

 

그리고 `poetry env remove [환경명]`의 명령어를 이용해 삭제합니다.

$ poetry env remove [env_name]
반응형

 

개념정리

  • 코드 포인트(code point): 각 유니코드 문자에는 고유한 코드포인트(숫자)가 할당되어있습니다. 이 유니코드 하나하나에 매핑되는 숫자를 코드포인트라고합니다. 1:1로 매핑되어있고, 코드포인트를 어떤 인코딩 방식을 하냐에 따라, 문자열이 달라질 수 있습니다.
  • 인코딩: 코드포인트를 바이트 시퀀스로 변환하는 규식을 일컫습니다.
  • 디코딩: 바이트 시퀀스를 코드포인트로 역변환 하는 규칙을 일컫습니다
  • 유니코드(Unicode): 모든 문자를 컴퓨터에서 일관되게 표현하고 다루기 위한 국제 표준입니다. 모든 문자열은 코드포인트를 이용하여, 1:1 매핑되어있습니다. 그리고 유니코드로 표현되는 경우 "U+"라는 접두사를 사용합니다.

 

코드포인트: 문자와 1:1로 매핑하는 숫자

코드포인트는 문자와 1:1로 매핑되는 숫자입니다. 예를 들어, `U+1f62e`와 같이 생긴 숫자가 코드포인트입니다. 유니코드의 코드포인트는 유니코드 표준으로 "U+"라는 접두사를 붙여서 4자리에서 6자리의 16진수로 표현합니다. `U+` 뒤에 1f62e은 각각이 16문자 문자열입니다. 이 문자는 😮의 이모지를 뜻합니다.

 

이 코드포인트를 UTF-8로 인코딩한다면, f0 9f 98 ae가 됩니다.

>>> code_point = int("1f62e", 16) # "U+1f62e"은 16진수로 표현합니다.
>>> print(code_point)
128558

>>> chr(code_point)
😮

>>> chr(code_point).encode("utf-8")
b'\xf0\x9f\x98\xae'

 

UTF-8은 가변 길이 인코딩 방식으로, 각 유니코드 코드 포인트를 1바이트에서 4바이트까지 다양한 길이로 인코딩합니다. 아래에 간단한 UTF-8 인코딩 테이블을 제시하겠습니다:

유니코드 범위 (16진수)UTF-8 바이트 시퀀스 (이진수)

U+0000 - U+007F 0xxxxxxx
U+0080 - U+07FF 110xxxxx 10xxxxxx
U+0800 - U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+10000 - U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

여기서 "x"는 유니코드 코드 포인트의 비트를 나타냅니다. UTF-8에서 첫 번째 바이트는 0, 110, 1110, 또는 11110으로 시작하며, 다음 바이트는 항상 10으로 시작합니다. 이 테이블을 사용하여 유니코드 문자를 UTF-8로 인코딩할 수 있습니다.

이제 "b'\xf0\x9f\x98\xae'"을 디코딩 과정을 설명해 보겠습니다. UTF-8에서 각 문자의 첫 번째 바이트를 보고 이 바이트가 어떤 범위에 속하는지에 따라 그 문자가 몇 바이트인지 결정됩니다. 그런 다음 각 바이트에서 필요한 비트를 추출하여 이를 유니코드 코드 포인트로 변환합니다.

UTF-8 디코딩의 과정은 다음과 같습니다:

  1. 첫 번째 바이트를 확인합니다. => '\xf0'이며, 이는 이진수로 '1111 0000'을 나타냅니다. 이 바이트는 4바이트 문자의 첫 번째 바이트를 나타냅니다. 
  2. 첫 번째 바이트를 보고 문자의 길이를 결정합니다. => '11110000'은 네 번째 범위에 속하므로 이 문자는 총 4바이트로 구성됩니다. 
  3. 첫 번째 바이트의 마지막 4비트를 제외한 나머지 비트는 유니코드 코드 포인트를 나타냅니다. 여기서는 '1110'으로 시작하므로 이후 3바이트의 유니코드 코드 포인트가 함께 사용됩니다.
  4. 다음 바이트부터는 모두 '10'으로 시작해야 합니다. 여기서는 '\x9f', '\x98', '\xae'가 모두 이 조건을 충족합니다.
  5. 각 바이트에서 첫 번째 바이트 이후의 유니코드 코드 포인트를 나타내는 비트를 추출합니다. 이 비트는 모두 '10xxxxxx' 형식입니다. 따라서 각 바이트의 마지막 6비트를 추출하여 이를 합칩니다.
  6. 마지막으로 이진수를 십진수로 변환한 다음 이를 유니코드 코드 포인트로 해석합니다.
  7. 유니코드 코드 포인트를 유니코드 문자로 변환합니다.

 

또 다른 파이썬에서의 예시를 들어보겠습니다. 안녕하세요의 문자열이 있는 경우 `ord()`함수를 이용해서 10진법으로 표현할 수 있습니다. hex은 정수를 입력받아 16진법으로 표현합니다. 그렇기에, 이 10진법으로 표현한 것을 다시 16진법으로 표현하기위해서 `hex()`함수를 이용해서 변환합니다. 그리고 출력하면 아래와 같이 얻을 수 있습니다. 그리고 각 10진법으로 표현하면 숫자로만 열거됩니다. 

>>> text = "안녕하세요"
>>> code_points = [hex(ord(char)) for char in text]
>>> print(code_points)
['0xc548', '0xb155', '0xd558', '0xc138', '0xc694']

code_points = [ord(c) for c in text]
>>> print(code_points)
[50504, 45397, 54616, 49464, 50836]

 

`0x`은 파이썬에서 16진법을 표현하는 방식입니다.  "안"에 해당하는 문자는 10진법으로 50504이며, 이를 16진법으로 표현하면 "C548"입니다.

windows 계산기를 이용하여 50504을 16진법으로 변경한 예시

 

"안녕하세요"를 utf-8로 인코딩하면 아래와 같습니다. b로 시작하는 것은 바이트 리터럴을 나타냅니다. 파이썬에서 바이트 리터럴은 바이트 문자열을 나타내며, 이는 일련의 8비트 바이트로 구성된 바이트 시퀀스입니다.

 

인코딩을 하고하면, b''로 표현되는데 어떤문자는 문자 그대로 표현되고, 어떤 경우는 16진법으로 표현됩니다. 이 차이는 무엇일까요? 

아래의 예시를 하나 보겠습니다. apple은 utf-8의 경우는 b'apple이 출력됩니다. b''여도 16진수로 표현되는 경우가 있고, 그렇지 않은 경우가 있는데요. 아스키문자(공백부터 물결표(~))까지는 아스키 문자 그대로 출력이 가능한 경우는 그대로 출력됩니다. 그렇지 않은 경우는 16진수로 표현되기 때문입니다. 아스키코드 확인(URL)

>>> text ="안녕하세요"
>>> text.encode("utf-8")
b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

>>> text2 = 'apple'
>>> text2.encode('utf-8')
b'apple'

>>> text2.encode('utf-8')[0]
97

>>> text2.encode('ascii')[0]
97

 

아래의 아스키코드를 보면 Dec, Hx, Oct, Char가 있습니다. Dec은 Decimal(10진법)이며, Hx은 Heximal(16진법)입니다. Oct은 Octal으로 8진법입니다. apple을 utf-8로 인코딩한다음에 표현해보면, 97이나오고, 97에해당한는 문자(char)은 'a'입니다. 마찬가지로, 이를 ascii로 인코딩해도 97이나옵니다.

UTF-8와 ASCII는 대부분의 영문 알파벳 및 일부 특수 문자에 대해서는 동일한 방식으로 인코딩됩니다. ASCII는 UTF-8의 서브셋이며, UTF-8은 ASCII를 포함하고 있기 때문에 영문 알파벳과 일부 특수 문자에 대해서는 두 인코딩이 동일한 결과를 생성합니다.

따라서 'utf-8'과 'ascii' 인코딩을 사용하여 같은 문자열을 인코딩할 경우, 영문 알파벳과 일부 특수 문자에 대해서는 두 인코딩 결과가 동일할 수 있습니다. 이 경우에는 두 결과가 모두 97로 동일하게 나타나게 됩니다.

 

파이썬에서의 자료형: bytes, bytearray

  1. bytes:
    • bytes는 불변(immutable)한 바이트 시퀀스입니다. 즉, 한 번 생성되면 내용을 변경할 수 없습니다.
    • 바이트 객체는 0부터 255까지의 숫자로 이루어진 값들의 불변된 시퀀스입니다.
    • 주로 파일 입출력, 네트워크 통신 등에서 사용됩니다.
    • 예를 들어, b'hello'는 바이트 리터럴로, 문자열 'hello'를 바이트로 표현한 것입니다.
  2. bytearray:
    • bytearray는 변경 가능한(mutable)한 바이트 시퀀스입니다. 즉, 내용을 변경할 수 있습니다. 
    • 0부터 255까지 숫자로 이루어진 값들의 시퀀스입니다.
    • 바이트 배열은 bytes와 유사하지만, 변경 가능하고 가변성이 있습니다.
    • bytearray는 리스트와 유사하게 인덱싱 및 슬라이싱을 통해 요소를 수정하거나 추가할 수 있습니다.
    • 주로 이진 데이터를 변경하거나 조작할 때 사용됩니다.
    • 예를 들어, bytearray(b'hello')는 바이트 배열을 생성하고 초기값으로 문자열 'hello'를 사용한 것입니다.

간단히 말해, bytes는 불변하고 bytearray는 변경 가능한 바이트 시퀀스를 나타냅니다. 때에 따라서는 데이터를 수정해야 하는 경우 bytearray를 사용하고, 데이터를 변경할 필요가 없는 경우에는 bytes를 사용하는 것이 좋습니다.

In [23]: s = "안녕하세요"

In [24]: len(s)
Out[24]: 5

In [25]: s.encode('utf8')
Out[25]: b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

In [26]: encoded_s = s.encode('utf8')

In [27]: type(encoded_s)
Out[27]: bytes

In [28]: bytes("안녕하세요", encoding='utf-8')
Out[28]: b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

 

바이트(bytes)은 바이트어레이(bytearray)은 슬라이싱 및 인덱싱에 따라 아래와 같은 결과를 보입니다.

  • bytes의 슬라이싱: bytes
  • bytes의 인덱싱: int
  • bytearray의 슬라이싱: bytearray
  • bytearray의 인덱싱: int
>>> s = "안녕하세요"
>>> bytes(s, encoding='utf-8')
b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

>>> bytes(s, encoding='utf-8')[0]
236

>>> bytes(s, encoding='utf-8')[:1]
b'\xec'

>>> bytearray(s, encoding='utf-8')
bytearray(b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94')

>>> bytearray(s, encoding='utf-8')[:1]
bytearray(b'\xec')

>>> bytearray(s, encoding='utf-8')[0]
236

 

Struct module

파이썬 내장 모듈인 `struct` 모듈은 바이트로 전환하거나, 바이트를 문자열로 변경할 때, 주로 사용됩니다. 포맷을 지정하는 등의 역할도 가능합니다. `struct`모듈은 주로 아래와 같은 기능을 합니다.

  1. 패킹(Packing):
    • struct.pack(format, v1, v2, ...) 함수를 사용하여 파이썬 데이터를 이진 데이터로 변환합니다.
    • 이진 데이터는 바이트 문자열 또는 바이트 배열로 반환됩니다.
    • 주어진 형식(format)에 따라 데이터가 패킹됩니다. 예를 들어, I는 4바이트의 부호 없는 정수를 나타내는 형식입니다.
  2. 언패킹(Unpacking):
    • struct.unpack(format, buffer) 함수를 사용하여 이진 데이터를 파이썬 데이터 타입으로 변환합니다.
    • 주어진 형식에 따라 바이트 데이터가 해체됩니다.
    • 이 함수는 바이트 문자열 또는 바이트 배열을 받아서 주어진 형식에 따라 데이터를 언패킹합니다.
  3. 형식 지정자(Format Specifiers):
    • 패킹 및 언패킹 작업에 사용되는 형식 지정자는 바이트 순서, 데이터 유형 및 크기를 정의합니다.
    • 예를 들어, I는 부호 없는 4바이트 정수를 나타내며, f는 4바이트 부동 소수점을 나타냅니다.

 

Memory view

메모리뷰는 바이트 시퀀스를 생성/저장하는게 아니라(복사X), 공유메모리 방식으로 버퍼데이터를 조작할 때 주로 사용됩니다. 복사가 아니기에 직접 값을 변경은 불가능합니다.

메모리는 메모리에 대한 접근을 보다 효율적으로 다루는 데 사용됩니다. 아래는 memoryview의 간단한 예시입니다.

# 바이트 데이터로부터 memoryview 생성
>>> data = b'hello world'
>>> mv = memoryview(data)

# memoryview의 내용 확인
>>> print(mv)  # <memory at 0x7fbba30d2080>>

# memoryview를 이용하여 데이터 접근
>>> print(mv[0])  # 104 (b'h'의 ASCII 코드 값)
>>> print(mv[1:5])  # <memory at 0x7fbba98234c0>>
>>> print(bytes(mv[1:5]))  # b'ello' (슬라이싱된 바이트 데이터)

# memoryview를 이용하여 데이터 수정
>>> mv[6] = 66  # ASCII 코드 66은 'B' (원본 데이터가 수정불가, TypeError 발생)

 

예시: GIF포맷처리

GIF은 움짤로 주로 사용되는데요. GIF포맷을 struct을 이용해서 언패킹해보겠습니다.

GIF포맷은 아래와같이 포맷이 지정되어있습니다.

  • 0번부터 3바이트는 GIF을 의미하며,
  • 3번부터 3개의 바이트는 87a, 89a GIf 버전을 의미합니다. 
  • offset 6부터 2바이트: logicla width 
  • offset 8부터 2바이트: logical height

이를 파이썬으로 접근하면 아래와 같습니다. fmt의 "<"은 리틀엔디언, "3s"은 3개의 바이트, "H"은 하나의 정수를 의미합니다.

>>> import struct

>>>fmt = "<3s3sHH"

>>>with open("200w.gif", "rb") as fh:
   ...:     img = memoryview(fh.read())
   ...: 


>>> header = img[:10]
>>> header
<memory at 0x7f8b757bfac0>

>>> bytes(header)
b'GIF89a\xc8\x00\xff\x00'

>>> struct.unpack(fmt, header)
(b'GIF', b'89a', 200, 255)
  • <: 리틀 엔디안(Endian)을 나타냅니다. 리틀 엔디안은 낮은 주소부터 데이터를 읽음을 의미합니다. (파일 형식에 따라 다를 수 있으며, 여기서는 일반적으로 사용되는 리틀 엔디안을 가정합니다.)
  • 3s: 3바이트 문자열을 의미합니다. 여기서는 GIF 파일 형식을 나타내는 문자열입니다. "GIF"을 읽어왔습니다.
  • 3s: 3바이트 문자열을 의미합니다. 여기서는 GIF 파일 버전을 나타내는 문자열입니다. "89a"을 읽어왔습니다.
  • HH: 각각 2바이트(16비트)의 부호 없는(short) 정수를 나타냅니다. 여기서는 GIF 이미지의 가로 및 세로 크기를 나타내는데 사용됩니다.

src: https://www.researchgate.net/figure/GIF-Header-Format-adapted-from-14_tbl1_228678318

 

문제: tiff파일에서 바이트 조직

이 사이트에서 다운로드받은 tiff파일의 이미지 헤더를 다음을 참조하여(스펙), 이 파일 포맷에서 다음을 구하세요.

  • 바이트 오더: 리틀엔디언, 빅엔디언
  • tiff 파일 여부:
  • IFD 시작 오프셋:

 

 

 

이중 모드 스트링 / 바이트 API

바이트로 정규표현식을 만들면 "\d"와 같은 글자나 "\w"은 아스키문자만 매칭되지만 str로 이 패턴을 만들면 아스키문자외에 유니코드나 숫자도 매칭이된다. 

 

import re

# bytes로 정규표현식 패턴 컴파일하기
pattern_bytes = re.compile(rb'\d')

# bytes 대상으로 매칭 시도하고 모든 매칭 결과 찾기
binary_data = b'123 abc 456'
matches_bytes = pattern_bytes.findall(binary_data)

print("Matches with bytes regex pattern:", matches_bytes)
Matches with bytes regex pattern: [b'1', b'2', b'3', b'4', b'5', b'6']

 

import re

# str로 정규표현식 패턴 컴파일하기
pattern_str = re.compile(r'\d')

# str 대상으로 매칭 시도하고 모든 매칭 결과 찾기
text = '123 abc 456'
matches_str = pattern_str.findall(text)

print("Matches with str regex pattern:", matches_str)
Matches with str regex pattern: ['1', '2', '3', '4', '5', '6']
반응형

+ Recent posts