요약


  • 딥러닝에서의 stride: 컨볼루션 연산에서 커널의 이동 범위를 나타냅니다.
  • numpy 배열에서의 stride: 배열의 각 차원에서 다음 요소로 이동하기 위한 메모리 상의 바이트 수를 나타냅니다.

 

보통 딥러닝하시는 분들이면 딥러닝의 CNN layer에서 stride을 들어보셨을 텐데, numpy 에서의 stride가 어떤 개념인지 정리해보겠습니다.


numpy 배열에서의 Stride: 배열의 각 차원에서 다음 요소로 이동하기 위한 메모리 상의 바이트 수

numpy 배열에서의 stride는 메모리에서 다음 요소로 이동하기 위해 건너뛰어야 하는 바이트 수를 나타냅니다. numpy 배열은 연속된 메모리 블록을 사용하여 데이터를 저장하며, stride는 배열의 각 차원을 따라 이동할 때의 바이트 수를 지정합니다. 배열이 슬라이싱되거나 뒤집힐 때 stride가 음수가 될 수 있습니다. 이는 배열이 역순으로 참조되고 있음을 의미합니다.

예를 들어, numpy.array을 3, 3 shape의 unit8 타입의 행렬을 만들었다고 가정해보겠습니다. 여기서 [0,0]의 위치에서 그 다음인 [0, 1]을 읽으려면 uint8이기 때문에 8bit(1byte)을 이동해서 읽어야합니다. 한편, 그 다음행인 [1, 0]을 읽어야하면, 한 행이 3개의 원소를 담고 있기에, 3*8bit(3byte)을 이동해서 읽어야합니다. Axis 1이 컬럼이며, Axis 0이 행이기에, Axis[0](행)을 이동하려면 3bytes (stride=3)으로 표시되어야하며, Axis[1](열)을 이동하려면 1byte (stride=1)을 이동해야합니다.

stride: https://stackoverflow.com/questions/53097952/how-to-understand-numpy-strides-for-layman

 

아래와 같이 3, 3 정수형의 uint8을 생성해보겠습니다. 이 배열의 `.strides`속성은 축이 2개이기에, 2개의 값이 튜플로 (3, 1)로 반환됩니다. 그리고, 위에서 언급했던 것처럼 stride가 (3, 1)입니다.

>>> import numpy as np

>>> arr = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
], dtype=np.uint8)

>>> print("배열:")
>>> print(arr)
>>> print("Stride:", arr.strides)

배열:
[[1 2 3]
 [4 5 6]
 [7 8 9]]
Stride: (3, 1)

 

Stride은 음수도 될 수 있습니다. 이 행렬을 뒤집는 경우, 다음 원소를 읽기위해서는 이전의 메모리를 참조해야합니다. 예를 들어, 행렬을 한번 뒤짚은 경우라면, `array[0]`이후에 `array[1]`을 읽으려면 +로 바이트를 이동시켜야하는게 아니라 (-)으로 바이트를 이동시켜야합니다.

>>> import numpy as np

>>> arr = np.array([1, 2, 3, 4, 5])
>>> reversed_arr = arr[::-1]

>>> print("원래 배열:", arr)
>>> print("뒤집힌 배열:", reversed_arr)
>>> print("원래 배열의 stride:", arr.strides)
>>> print("뒤집힌 배열의 stride:", reversed_arr.strides)
원래 배열: [1 2 3 4 5]
뒤집힌 배열: [5 4 3 2 1]
원래 배열의 stride: (8,)
뒤집힌 배열의 stride: (-8,)

 

딥러닝에서의 Stride: 컨볼루션 연산에서 커널의 이동 범위

딥러닝에서의 stride는 주로 컨볼루션 연산에서 사용됩니다. 컨볼루션 연산에서 stride는 커널이 입력 이미지 위를 얼마나 많이 움직이는지를 의미합니다. 예를 들어, stride가 1이면 커널이 한 픽셀씩 이동하며 연산을 수행하고, stride가 2이면 두 픽셀씩 건너뛰면서 연산을 수행합니다.

 

Filter(kernel)이 image에서 2칸씩 이동하며 연산하면 stride가 2입니다. src: https://medium.com/machine-learning-algorithms/what-is-stride-in-convolutional-neural-network-e3b4ae9baedb



연관 에러: ValueError: At least one stride in the given numpy array is negative, and tensors with negative strides are not currently supported. (You can probably work around this by making a copy of your array with array.copy().)

이미지를 전처리하는 과정에서 flip 등의 연산이 들어가게되면, 위의 예시처럼 stride가 음수가 되는 경우, 위의 에러가 발생합니다. "적어도 하나 이상의 stride가 음수가 됨을 의미합니다". 따라서 이 경우에는 flip한 array을 다시 메모리에 재할당하여(=copy)하여 처리하면 해결이됩니다.

# 변경전

transform = A.Compose(
    [
        A.RandomCrop(height=224, width=224, p=1),
        A.Resize(224, 224),
        A.HorizontalFlip(p=0.5),
        A.VerticalFlip(p=0.5),
        A.RandomRotate90(p=0.5), 
        A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
        ToTensorV2(),
    ]    
)


train_dataset = dataset(train_images, train_masks, transform)
train_dataset[0] # 에러 발생

# 변경

반응형

 

Preliminaries

  1. IID(Independent and identical distributed) : 인공지능 알고리즘들은 IID로 소스/타깃 데이터에 대한 주요 가정을 합니다. 이 가정은 주로 2가지입니다 [1].
    • 독립성(Independence): 각 데이터 포인트가 서로 독립적이라는 것을 의미합니다. 즉, x1이 다른 x2에 영향을 미치지 않는 것을 의미합니다.
    • 동일 분포(identically distributed): 모든 데이터가 같은 확률분포에서 추출되었다는 것을 의미합니다.  즉 x1을 뽑든 x2을 뽑든, test data에서 x3을 뽑든 동일분포에서 나왔음을 가정합니다. 따라서, 데이터 셋이 통계적으로 유사한 통계량값을 가진다는 것을 의미합니다. 
  2. Domain shift: training(source)와 test data (target) 데이터가 다름을 의미하는 문제입니다. 여기서 "domain"이란 "source data"을 의미합니다.  수학에서 배우는 정의역(domain)은 주로 가능한 입력값의 전체를 의미하는 것이라, 머신러닝에서의 domain이랑은 개념이 다소 다릅니다.
    • domain shift은 distribution shift라고도 불립니다. 

 

주요 기술개념 1: Domain adaptation

Domain Adopation(DA): 특정 학습데이터로 학습된 모델이, 다른 분포에서 온 테스트데이터에서 잘 예측하기를 원하는 것입니다. 예를 들어, A라는 고객의 이메일의 스팸필터링이, B라는 고객의  스펨필터링에도 잘 동작하는것을 원하는 시나리오가 대표적입니다. A라는 고객과 B라는 고객이 주로 오는 스팸이 다를 것(non indentical distributed)이기 때문입니다.

일반적인 머신러닝에서는 입력 스페이스가 X, 출력이 Y, 인공지능 모델 h가 X을 잘 학습해서 Y을 예측하기를 바랍니다. 이를 수식으로 표현하면 아래와 같습니다.

  • $h: X \rightarrow Y$ : 인공지능 모델이 입력 X을 학습하여 Y을 출력하게 만듬
  • $S= \{(x_{i},y_{i})\in (X\times Y)\}_{i}^{m}$: S은 학습샘플을 의미합니다. 총 데이터수는 1부터 m까지 m개가 있으며, 각 순서는 i입니다. 각 데이터포인트는 입력과 출력을 튜플로 $ (x_{i},y_{i}) $로 가집니다.

주간 / 야간의 운행 이미지. 주간이미지(source data)로 야간 주행이미지(target data)을 잘 예측해야하는 시나리오. https://paperswithcode.com/task/domain-adaptation

일반적인 지도학습에서는 IID를 가정하기에 모든 i의 데이터포인트 $ (x_{i},y_{i}) $가 하나의 분포에서 왔다고 가정합니다. 즉, $D_{s}$에서 왔다고 생각하는것입니다. Domain Adopation(DA)은 서로 다른 분포지만 어느정도 유사한 분포에서 동작하기를 의미하는 것입니다. 수식으로 표현하면, $D_{S}$에서 학습했던 내용을 $D_{T}$로 트랜스퍼하는 과정을 의미합니다.

Domain adoptation의 상세한 기술들은 문제 상황에 따라서, 3가지 정도로 요약됩니다.

  1. Unsupervised domain adaptation: 소스데이터의 라벨링된 데이터 + 레이블 안된 소스 데이터 + 레이블안된 타깃데이터를 학습
  2. Semi-supervised domain adaptation: 소수의 labeled target 데이터를 학습
  3. Supervised domain adaptation: 모든 데이터가 라벨링 된 경우 

 

supervised domain adapation의 한 예시로는 "Semantic alignment loss"을 이용한 사례입니다. 소스 데이터는 누끼가 잘 따진 이미지 상품 이미지이며, target data은 실제이미지들을 의미합니다. 비전 인코더 $g$을 잘 통과시킨 후, constrative learning으로 유사한 클레스/다른 클레스임을 알려주며, 마지막 분류만 classification loss로 적용한 사례입니다.

https://openaccess.thecvf.com/content_ICCV_2017/papers/Motiian_Unified_Deep_Supervised_ICCV_2017_paper.pdf

 

위와 같은 지도학습은 전체적으로 아래와 같은 스킴처럼 될 것 같습니다. domain 1 (source)의 결정경계를 학습할거지만 domain 2의 의미론적인 임베딩값들은 domain1과 유사하게 맞추는 과정입니다.

https://www.v7labs.com/blog/domain-adaptation-guide#h1

 

주요 기술개념 2: Domain generalization

domain generalization은 그리 연구가 오래된 분야는 아닙니다. 2011년에 본격적(formal)하게 처음 소개가되었습니다[2]. non-IID로 인한 OOD(out of distribution)문제에서  가장쉬운 방법은 일부 target data을 구해서 모델을 target domain에 맞추는 방법입니다. 하지만 현실적으로는 target domain에 대한 데이터를 모두 구할 수 없는게 현실입니다. 구할 수 있는 target data을 구하더라도 아직 안알려진경우도 대부분일 수 있습니다. 

그렇기에 target data을 모르는 경우에도 domain shift 문제를 해결할수 있도록하는 domain generalization 이 연구초점이 주로 되고 있습니다. domain generalization (DG)의 주목적은 한 또는 소수의 연관된 데이터로 모델을 학습시키고, 꽤 이질적인 도메인이더라도 일반화가 잘 되는 모델을 만들고 싶은게 주 목적입니다.

https://www.sciencedirect.com/science/article/abs/pii/S0951832023001035

 

domain generalization 문제의 정의는 아래와 같이 해볼 수 있습니다[3].

  • $S=\{S_{k}=\{(x^{k},y^{(k)})\}\}_{k=1}^{K}$: K개의 소스 도메인이 여러개 있다고 생각해볼 수 있습니다. 예를 들어, 차량인식이라면, 한국차, 일본차, 독일차 정도의 차량별로 소스 도메인이 있다고 K을 정의해볼 수 있습니다.
  • $P_{XY}^{k}$: 각 소스 도메인별로 X, Y가 결합확률로 표현될 수 있습니다. 예를 들어, 한국차 2000cc 중, 흰색 색차량 중에 택시로 많이 이용되는 차량이라는 X라면, Y은 중형차라는 라벨로 확률분포를 예상할 수 있는 것을 의미합니다 .독일은 3000cc, 벤츠의 중대형차가 많겠죠.
  • $f:X\rightarrow y$: 예측모델f은 소스도메인 x만가지고 y을 에측해야하고, 
  • $\mathcal{T}=\{ x ^{\mathcal{T} \}$ 에대해서도 예측이 정확했으면 합니다.
  • $P_{XY}^{\mathcal{T}} \neq P_{XY}^{(k)} $: 문제로 어떤 k이든지 unseend target domain으로부터 관찰된적 없는 경우를 의미합니다.

이 DG도 다시 크게 2부류로 source domain이 몇개인지로 다시 나뉩니다. 

  1. Multi-Source DG: 소스 도메인이 2개 이상인 경우
  2. Single-Source DG: 훈련데이터가 1개인 경우 (매우 homogenous data)

 

Histopathology에서의 적용

조직병리학에서의 domain shift은 주로 1)염색 프로토콜, 2) 스캐너에 따른 domain shift가 발생한다고 생각됩니다. 이 두문제를 해결하는 방법은 크게 3가지로 구분할 수 있습니다.

염색이 다르거나, 스캐너가 달라서 내가 학습했던 source domain과 실제 예측하는 경우가 매우 다른 타깃 도메인을 예측하는 경우 주로 실패하게됩니다(여기서는 single domain source)

domain shift 문제 [4]: https://www.diva-portal.org/smash/get/diva2:1478702/FULLTEXT02.pdf

 

이를 해결하기 위한 방법으로는 3가지를 제시해볼 수 있습니다[4].

  1. 컬러 및 색상강도 증강(Color & Intensity Augmentation): 염색의 색상 차이를 통해 학습을 강화하고, source domain의 범위를 확장할 수 있습니다. 예를 들어, RGB을 HSV 컬러 시스템으로 변환 후에, 색상(Hue), 채도(Saturation), 밝기(Value)를 조절함으로써 사람이 인지하는 색상의 범위 내에서 이미지의 범위를 넓혀 변환하여 작업할 수 있습니다. 이는 domain generalization의 목표와 매우 유사하며, 이는 unseen target domain $D_{mathcal{T}}$의 분포에서도 강건하게 학습될 수 있도록, 훈련때부터 훈련 데이터를 증강하여 domain generalization을 적용하는 것입니다.
  2. 염색 정규화(Stain normalization): 염색의 일관성을 유지하게 염색 스타일을 소스도메인에 맞추는 것을 의미합니다. 염색 정규화는 타깃 도메인이 매우 다를 것이라, 소스도메인에서 참조할만한 이미지(reference image)을 하나 뽑아서, 이 reference image에 모두 target domain을 변환하는 것을 의미합니다. 즉, 소스 도메인에 맞추는 것을 의미합니다. domain adaptation에 가깝습니다. domain adaptation을 일반적으로 target domain을 어느정도 아는 편이지만, stain normalziation은 target domain을 모르더라도 내가 원하는 reference image에 맞추기만 되기에, 약간 차이가 있습니다. 그리고, 훈련 데이터도 reference image에 모두 맞추기 때문에 아래의 그림처럼 source domain의 범위도 좁아집니다.
  3. Cycle-GAN stain normalization: Cycle GAN을 이용하는 경우 image-to-image style transfer가 가능합니다. 그렇기에 아래의 그림처럼 source domain을 아래의 target domain에 맞춰서 진행할 수 있고, source domain의 일부 다양성을 일부러 좁히지 않고 예측합니다. domain adaptation에 가깝고 target domain을 source domain에 맞춰야하기이 이미 이미 분포를 알아야하고 학습해야합니다.

 

 

[1]: https://www.linkedin.com/pulse/statistical-inference-vs-machine-learning-iid-ajit-jaokar-ss88e/

[3] : https://arxiv.org/abs/2103.02503

[4] https://www.diva-portal.org/smash/get/diva2:1478702/FULLTEXT02.pdf

반응형

 

if else구문이 아닌, 구문에서의 else의 사용

1. for - else 구문:

for loop가 정상적으로 완료되면 else 구문이 실행됩니다. break으로 중단되는 경우 else 블록은 실행되지 않습니다.

numbers = [1, 2, 3, 4, 5]

for number in numbers:
    if number == 3:
        print("Found 3!")
        break
else:
    print("3 is not in the list.")

 

2. try-except-else:

try-except에서 예외가 발생하지 않을 때, else 블록이 실행됩니다.

이거 왜 쓰냐는 질문이 종종있는데요. 아래의 예시를 들어보겠습니다. 아래의 try-except구문에서 FileNoteFoundError을 open()함수에서 발생할 수 있다는 것을 경험적으로 알 수 있습니다. 

try:
    file = open('example.txt', 'r')
    content = file.read()
    print(content)
    file.close()
except FileNotFoundError:
    print("File not found.")

이는 open()에서 자주 예외가 발생할 수 있는데, 만일 open이 아니라 my_fun1(), my_fun2()라면 어느구문에서 해당예외가 발생한다고 예측할 수 있을까요? 즉, 실제로 예외가 발생하지 않는 경우에도 try 블록에 많은 코드가 포함되어있어서 디버깅이 어렵습니다. try-except구문에서는 예외가 발생할 수 있는 함수의 호출을 최소한의 블록으로 담아야 더 명시적입니다. 

try:
    my_func1()
    my_func2()
except FileNotFoundError:
    print("file not found)

 

따라서, 이를 처리하기위해 아래와 같이 try-except-else 구문으로 처리해볼 수 있습니다. 

try:
    file = open('example.txt', 'r')  #open()에 대해서만 예외처리
except FileNotFoundError:
    print("File not found.")
else:
    content = file.read()
    print(content)
    file.close()

이는 EAFP(Easier to ask for forgiveness than permission, 허락보다 용서가 쉽다) 코딩스타일로 `if os.path.exist()`등으로 처리가 가능하나, 파이썬에서는 EAFP스타일을 사용을 많이합니다.

 

with 구문

with 구문은 크게 2가지로 구성됩니다.

  1. `__enter__`: with 구문의 실행 (진입)
  2. `__exit__`: with 구문의 종료. + with 구문에 끝에 finally 절의 역할이 수행됩니다.

 

with 구문은 컨텍스트 관리자 객체를 사용하여, 코드블록의 진입종료 시에 특별한 동작을 합니다. 컨텍스트 관리자 객체는 __enter__와 __exit__ 메서드를 구현하여, 코드 블록 진입 시에 __enter__ 메서드를, 종료 시에 __exit__ 메서드를 호출하게 합니다. 즉, 컨텍스트 관리자를 이용하면, 자원 해제, 예외 처리 등을 신뢰성 있게 관리할 수 있습니다.

아래의 예시를 하나 들어보겠습니다. 아래의 예시에서는 파일 객체를 반환하고,

  1. with구문으로 실행될때 (__enter__)가 실행되고,
  2. with 구문이 종료되면 (__exit__)이 종료되는 것 과 같습니다. 
with open('example.txt', 'r') as file:
    content = file.read()
    print(content)

실제로 `__enter__`, `__exit__`이 있는지 살펴보겠습니다.

In [1]: # open 함수로 반환된 파일 객체의 __enter__와 __exit__ 메서드 확인
   ...: with open('example.txt', 'r') as file:
   ...:     print(hasattr(file, '__enter__'))  # True 출력
   ...:     print(hasattr(file, '__exit__'))   # True 출력
   ...:     content = file.read()
   ...:     print(content)
   ...:
True
True

 

또 다른 예시로는 사용자 정의 컨테스트를 만들 수 있습니다. 이 때도, 마찬가지로 `__enter__`, `__exit__`의 메직메서드의 구현이 되어있어야합니다.

class Resource:
    def __enter__(self):
        print('Resource allocated')
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print('Resource released')

# 컨텍스트 관리자 사용
with Resource() as resource:
    print('Using the resource')

Resource allocated  # with 구문이 실행될 때
Using the resource  # print구문
Resource released   # with 구문의 종료

 

sqlite3나 mysqlDB등을 이용할때의 db connection을 명시적으로 종료하지않고 with구문으로 컨텍스트매니저로 사용할 수 있습니다.

import sqlite3

with sqlite3.connect('example.db') as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM users')
    results = cursor.fetchall()
    print(results)

 

 

contextlib: python 표준라이브러리

1. @contextmanager: 이 데코레이터는 컨텍스트 메니저를 생성할때 코드를 줄여줍니다.

@contextmanager 데코레이터는 컨텍스트 관리자를 생성할 때 사용됩니다. 이 데코레이터를 사용하면 __enter__와 __exit__ 메서드를 명시적으로 구현하지 않고도 컨텍스트 관리자를 만들 수 있습니다. 대신, 함수 내부에서 yield를 사용하여 진입 및 종료 시 실행할 코드를 정의합니다.

아래와 같은 예시를 들어보겠습니다. 

from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.time()
    try:
        yield
    finally:
        end = time.time()
        print(f'Elapsed time: {end - start} seconds')

# 사용 예시
with timer():   # start = time.time()이 서브루틴에서 할당됨. yield로 메인루틴으로 전환
    time.sleep(2)  # yield 이후 메인루틴의 실행, with 구문종료시 서브루틴의 finally 구문으로 전환

 

또 다른 예시로,DB의  FastAPI의 contextmanager을 디팬던시로 사용하는 경우입니다. 

FastAPI에서 contextmanager역할로 `__enter__`, `__exit__`메서드를 구현했지만, contextlib 내의 contextmangager 데코레이터를 이용하면 그럴필요는 없습니다.

 

아래와 같이 구현이 가능합니다.

from contextlib import contextmanager

@contextmanager
def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

# FastAPI dependency 사용 예시
@app.get("/items/")
async def read_items(db: DBSession = Depends(get_db)):
    # 데이터베이스 작업
    items = db.execute("SELECT * FROM items").fetchall()
    return items
반응형

 

객체표현: __repr__, __str__ 차이

파이썬에서 객체의 설명을 보려면, `repr()`, `str()`함수가 필요합니다. 이 두 함수는  `__repr__`와 `__str__`이 정의되어야 확인할 수 있습니다.  이 두 함수의 차이점은 아래와 같습니다.

공통점

  • 두 함수 모두 반환되는 형태가 `str` 입니다.

차이점

  • `repr`, `__repr__`은 주로 개발자을 위해 사용되며, 내부 디버깅을 위해 사용되는 표현방식입니다. 주로 `obj`을 직접 호출할 때 사용되고, 문자열 그 자체로 객체가 생성되도록 작성을 합니다.
  • `str`, `__str__`은 주로 사용자를 위해 사용되며, 보기쉬운 형태로 전환하거나 타입 캐스팅을 하는 용도로 사용됩니다. 또한, `__str__`이 정의되어있지 않다면, `__rerp__`이 호출되기도 합니다.
from dataclasses import dataclass

@dataclass
class MyClass:
    attribute1: int
    attribute2: str

    def __repr__(self):
        return f"MyClass(attribute1={self.attribute1}, attribute2={self.attribute2!r})"

# 객체 생성
obj = MyClass(attribute1=10, attribute2="example")

# repr() 함수 호출
print(repr(obj))  # MyClass(attribute1=10, attribute2='example')

# str() 함수 호출 - __str__이 정의되지 않았으므로 __repr__이 대신 호출됩니다.
print(str(obj))  # MyClass(attribute1=10, attribute2='example')

 

from dataclasses import dataclass

@dataclass
class MyClass:
    attribute1: int
    attribute2: str

    def __str__(self):
        return f"MyClass with attribute1: {self.attribute1} and attribute2: {self.attribute2}"

# 객체 생성
obj = MyClass(attribute1=10, attribute2="example")

# repr() 함수 호출 - __repr__이 정의되지 않았으므로 기본 구현이 호출됩니다.
print(repr(obj))  # MyClass(attribute1=10, attribute2='example')

# str() 함수 호출
print(str(obj))  # MyClass with attribute1: 10 and attribute2: example

 

위의 코드에서 추가로 `MyClass(attribute1=10, attribute2='example')`을 그대로 호출해보면 객체가 생성이 가능합니다.

a = MyClass(attribute1=10, attribute2='example')
a
MyClass(attribute1=10, attribute2='example')

 

그 외에도, byte 시퀀스로 호출하려면 `byte()`함수를 호출하여 사용이 가능합니다. 이 때도, `__bytes__`매직메서드가 구현되어있어야합니다.

from dataclasses import dataclass

@dataclass
class MyClass:
    attribute1: int
    attribute2: str

    def __repr__(self):
        return f"MyClass(attribute1={self.attribute1}, attribute2={self.attribute2!r})"

    def __str__(self):
        return f"MyClass with attribute1: {self.attribute1} and attribute2: {self.attribute2}"

    def __bytes__(self):
        # 객체를 바이트 시퀀스로 변환
        return f"MyClass(attribute1={self.attribute1}, attribute2={self.attribute2})".encode('utf-8')

# 객체 생성
obj = MyClass(attribute1=10, attribute2="example")
print(bytes(obj))  # b'MyClass(attribute1=10, attribute2=example)'
b'MyClass(attribute1=10, attribute2=example)'

 

객체 생성의 방법: @classmethod을 이용한 팩토리패턴

아래의 코드는 @classmethod에서 자기 자신의 클레스의 인스턴스를 생성 패턴입니다. 생성자를 꼭 만들필요없고, 대안적으로 아래와 같은 디자인 패턴이 가능합니다.

@dataclass
class MulticlassMetrics:
    accuracy: float
    auroc: float
    prauc: float

    @classmethod
    def calculate(
        cls, labels: np.ndarray, confidences: np.ndarray
    ) -> MulticlassMetrics:
        pred_label = np.argmax(confidences, axis=1)
        accuracy = (pred_label == labels).mean()
        auroc = calculate_auroc_ovr(confidences, labels)
        prauc = calculate_prauc_ovr(confidences, labels)

        return cls(accuracy, auroc, prauc)

    def to_dict(self, prefix=str()) -> dict:
        return {
            prefix + metric_name: round(value, 5)
            for metric_name, value in asdict(self).items()
        }

 

클레스 해시하기

파이썬 Set과 같은 기본자료구조에 넣으려면, 객체는 모두 hashable해야합니다. 즉, 해시값에 대해서 유일해야합니다. 이 내부적으로 hash()함수를 이용해서 객체를 해시하는데, 이를 이용하려면 `__hash__`을 정의해야합니다. 

이 디자인의 예시는 아래와 같습니다. HPO라는 온톨로지(개념)의 데이터클레스입니다. HPO의 id가 유일하므로 이 hash가 다르면 id가 내용이 각각 다름이 보장됩니다. 따라서, `__hash__`을 id을 기준으로 hash합니다.

@dataclass
class HPO:
    """HPO (node)"""

    id: str
    name: str = str()
    definition: str = str()
    synonyms: Set[str] = field(default_factory=set)
    xref: Set[str] = field(default_factory=set)
    vector: np.ndarray = np.empty(shape=(1,))
    depth: int = 0
    ic: float = -1

    subclasses: Set[HPO] = field(default_factory=set, repr=False)
    ancestors: Set[HPO] = field(default_factory=set, repr=False)

    def __hash__(self) -> int:
        return hash(self.id)

    def _get_subclasses_recursively(self, subclass: HPO, container: set) -> Set[HPO]:
        for sub_subclass in subclass.subclasses:
            container.add(sub_subclass.id)
            self._get_subclasses_recursively(sub_subclass, container)

        return container

 

클레스 내 속성 보호하기: read only attributes : @property

파이썬에서는 비공개 속성(private attribute)을 생성하는 방법은 없지만, 수정을 막는 방법은 있습니다. @property을 이용하여 readonly로 작성할 수 있습니다.

아래의 예제 코드를 보면, all_subclasses을 `self._all_subclasses`로 저장해두고 @property로 읽어오려고합니다. 그리고, `_all_subclasses`을 `all_subclasses `을 통해서 볼 수는 있지만 직접 수정을 막기위해서, 감쌌습니다.

@dataclass
class HPO:
    """HPO (node)"""

    id: str
    name: str = str()
    definition: str = str()
    synonyms: Set[str] = field(default_factory=set)
    xref: Set[str] = field(default_factory=set)
    vector: np.ndarray = np.empty(shape=(1,))
    depth: int = 0
    ic: float = -1

    subclasses: Set[HPO] = field(default_factory=set, repr=False)
    ancestors: Set[HPO] = field(default_factory=set, repr=False)

    def __hash__(self) -> int:
        return hash(self.id)

    def _get_subclasses_recursively(self, subclass: HPO, container: set) -> Set[HPO]:
        for sub_subclass in subclass.subclasses:
            container.add(sub_subclass.id)
            self._get_subclasses_recursively(sub_subclass, container)

        return container

    @property
    def all_subclasses(self) -> List[HPO]:
        """현재 클래스를 포함하여 모든 하위 클래스를 찾아 반환
        Return
            List[HPO]: 모든 하위 클래스의 ID 목록
        Example:
            >>> self.all_subclasses
            [
                HPO(...),
                HPO(...),
                ...
                HPO(...),
            ]
        """
        if hasattr(self, "_all_subclasses"):
            return self._all_subclasses

        container = set()
        self._get_subclasses_recursively(self, container)

        self._all_subclasses = [HPO(hpo_id) for hpo_id in container]
        return self._all_subclasses

 

__slot__ 으로 클레스 속성 메모리를 효율화 하기

파이썬에서는 객체의 속성이 생기면 `__dict__`라는 딕셔너리형 속성에 저장합니다. 따라서, 매우 빠르게 속성을 접근할 수 있습니다. 하지만, 반대로 속성이 매우 많은 객체라면, 메모리부담이 매우 커지게됩니다. 

아래와 같이 `__slots__`이라는 정의하면, 인스턴스에 `__dict__`라는 딕셔너리 속성이 저장되지않고, 고정된 튜플로만 관리합니다. 따라서, 동적으로 속성을 추가할 수는 없지만, dict객체를 따로 들고있지 않아도되니 속도가 빠르거나 메모리가 효율적으로 동작할 수 있습니다.

class Person:
    # __slots__를 사용하여 객체의 속성을 제한합니다.
    __slots__ = ('name', 'age')

    def __init__(self, name, age):
        self.name = name
        self.age = age

# 객체 생성
person1 = Person('John', 30)

# 속성에 접근
print(person1.name)  # 출력: John
print(person1.age)   # 출력: 30

# 새로운 속성 추가 시 에러 발생
# person1.address = '123 Street'  # 'Person' object has no attribute 'address'

# __dict__를 사용하여 객체의 속성을 확인할 때 에러 발생
# print(person1.__dict__)  # AttributeError: 'Person' object has no attribute '__dict__'
반응형

사전지식

  • Openslide-python은 파이썬 바인딩된 openslide의 패키지입니다. 여기서는 일반적인 WSI(whole slide Image)의 이미지 피라미드 구조로 다운 샘플링하여 처리함
  • Deepzoom은 Deepzoom은 OpenSlide와 함께 사용되는 강력한 도구로, 대형 이미지를 다양한 줌 레벨에서 효율적으로 탐색할 수 있게 해줍니다. 이는 특히 WSI와 같은 고해상도 이미지에 유용

 

요약


  • Openslide의 level 0가 가장 초해상도 이미지 (mpp)
  • Deepzoom의 level max가 가장 초고해상도 이미지 (mpp). 

 

Deepzoom level과 openslide level의 매핑


Deepzoom level_count와 openslide.Openslide.level_count의 숫자가 일치하지 않을 수 있습니다. Deepzoom에서의 level의 정의가 다르기 때문입니다.

  • Openslide level: level 0가 가장 초고해상도로 level max까지 해상도가 작아집니다. level 0에 mpp의 기준이됩니다.
  • Deepzoom level: level max가 초고해상도로, level min이 작아질수록 해상도가 작아집니다. level min값이 될때까지 이미지를 절반으로 줄여나갑니다.

아래는 openslide-python의 DeepZoomGenerator의 소스코드입니다. 자세히보면 아래와 같은 구성입니다.

  1. level_count을 read only attribute로 가지고있고, 원본은 `self._dz_levels`
  2. `self._dz_levels`은 초기화시 생성됨. 그리고, 이는`self.z_dimension`의 개수로 저장됨
  3. ` self.z_dimension`의 갯수가 deepzoom level 수인데, 이 숫자는 level 0의 dimension(이미지해상도)을 절반씩 줄여나가서 x축이나 y축이 1보다 작을때까지 dimension을 늘림
  4. 그렇기에, deepzoom level의 1(0인덱스)~5(4인덱스)사이에서는 주로 (1, 1), (1, 2), (2,1)과 같은 사이즈를 관찰할 수 있습니다. openslide level에서는 이를 정의하지 않죠. 여기서 level count의 갯수차이가 납니다.

 

class DeepZoomGenerator:
    """Generates Deep Zoom tiles and metadata."""

    BOUNDS_OFFSET_PROPS = (
        openslide.PROPERTY_NAME_BOUNDS_X,
        openslide.PROPERTY_NAME_BOUNDS_Y,
    )
    BOUNDS_SIZE_PROPS = (
        openslide.PROPERTY_NAME_BOUNDS_WIDTH,
        openslide.PROPERTY_NAME_BOUNDS_HEIGHT,
    )
    
    ...
        z_size = self._l0_dimensions
        z_dimensions = [z_size]
        while z_size[0] > 1 or z_size[1] > 1:
            z_size = tuple(max(1, int(math.ceil(z / 2))) for z in z_size)
            z_dimensions.append(z_size)
        self._z_dimensions = tuple(reversed(z_dimensions))
   ....
   
   		# Deep Zoom level count
        self._dz_levels = len(self._z_dimensions)
        
	@property
    def level_count(self):
        """The number of Deep Zoom levels in the image."""
        return self._dz_levels

 

반응형

Microsoft에서 발표한 Foundation 모델인 Prov-GigaPath입니다. 26개의 테스크에서 25개가 SOTA로 outperform을 주장했던 논문입니다. 

자세한 개발방법은 아래와 같습니다.

Data collection

  1. slides: H&E stained, and IHC(immunohistochemistry) 171,189 slides
  2. 백그라운드 제외: Tissue sementation. 자세히는 Otsu 알고리즘을으로 저해상도에서 진행해서 전경을 뽑은 후에 배율에 맞춰 사용함(HIPT에서도 활용)
  3. 해상도 조정: 0.5mpp (20x)에 맞춰 진행 (URL)**
  4. Tiling: 256 x 256 픽셀로 타일링 (2번에 오츄알고리즘으로 배경이 10% 이상인 경우만 활용)

 * 기타: 200node (32 CPU, 256GB RAM / node)에서의 분산처리를 진행

** level 0의 mpp가 0.25인 경우 level1으로 뽑는 식으로 lipvips을 안써도될 것 같음.

 

아키텍처 및 학습방법

GigaPath은 2단계의 학습 방법을 따릅니다. 하나는 tile-encoder이며, 또 다른 하나는 slide encoder입니다. 자기지도학습방법은 DINO v2을 따릅니다. 

Tile encoder(configuration)

  1. DINO v2: 정방 256px 이미지를 224px로 global view, 96 px의 local view로 하고 iBOT으로 maksed image modeling도 함께 사용합니다. 자세한 내용(URL)
  2. learning rate: 4x10^{-3}
  3. batch_size (/gpu): 12 (patch)
  4. total_batch: 384
  5. LongNet: default parameters
  6. tile coordinates: WSI이미지가 스캐너마다 각각 다르니, 패치사이즈 256px에 가로x세로가 1,000장씩있다고 셋팅
  7. image augmentation: cropping (크롭비율 0.875=224pixels) + moving distance 은 타일링하면서 uniform distribution으로 간격을 겹치게끔 타일링 + horizontally flipping (p=0.5)

Masked encoder (configuration)

  1. 아키텍처: LongNet. LongNet은 Flash attention보다 더 많은 시퀀스를 더 빨리 처리할 수 있는 아키텍처입니다. dilated attention machanism으로 GPU에서 매우 많은 수의 token을 받도록 되어있습니다. LongNet이 flash attention보다 더 빠른 속도로 학습이 가능했다고 합니다.
  2. MAE(Masked autoencoder) 적용 (URL)
  3. learning rate: 5x10^{-4}.
  4. batch_size (/gpu): 4
  5. positional encoding: $x_{l}, y_{l}$좌표가 있으면, 이 원래좌표와 grid 대비해서 flooring하여 작은값을 사용합니다 (식 3).


Dilated attention: 입력 시퀀스의 길이가 L이라고할 떄, 각 길이를 w 구간으로 나눠, L/w의 세그먼트로 나눕니다. w은 r을 어떻게 세팅하느냐에 따라 달라집니다. "r"이라는 간격으로 시퀀스의 간격(sparsity)을 조절합니다. 예를 들어 아래와 같습니다.

  • 전체 2048개의 토큰이고 windows size가 512면, r=1일 때, 4개의 구간이 얻어집니다.
  • 전체 2024개의 토큰이고, r=2이면, 토큰마다 1개가 skipping되어 1024개의 구간이 2개가 얻어집니다.

즉 Attention을 구간별로 적용하기 떄문에, 메모리 사용량이 원래 N이엇다면, $\frac{N}{w}r^{2}$로 얻어집니다. LongNet은 이 attention을 가중합하여 사용합니다. 

LongNet의 Dilated attention


 

반응형

+ Recent posts