Computer Vision

[논문 코드 리뷰] Instance Dependent Multi Label Noise Generation for Multi-Label Remote Sensing Image Classification 데이터 전처리, 메인 코드 리뷰

se0_ing 2024. 12. 29. 18:20
반응형

 

 

위성 사진 데이터셋에 대해 공부하고 있어서 위성사진 데이터처리 공부중에 교수님의 논문의 Instance depedent noise generation 이라는 방법론이 눈에 띄여 데이터처리 과정 코드 리뷰을 해보기로 했다.

 

 

논문 메인 아이디어

 

 

 

 

 

위 그림은 사전학습된 CLIP 모델을 이용해 remote sensing image를 멀티라벨로 예측(Zero-shot prediction)하는 전체 과정을 나타낸다.

 

이미지를 CLIP의 이미지 인코더에 입력 → 이미지 임베딩 추출하고, 텍스트를 CLIP의 텍스트 인코더에 입력 → 텍스트 임베딩 추출. 각각의 이미지 임베딩과 텍스트 임베딩 사이의 유사도를 내적하여, 어떤 label과 가장 밀접한지 확인한 후, 결과로부터 Zero-shot prediction score가 산출되어, 여러 라벨 중 해당 이미지와 가장 관련성이 큰 label들을 찾아낼 수 있음.

 

 

 

 

 

아래 코드는 UCMerced Land Use Dataset(이미지 2,100장, 각 클래스당 100장)을 훈련 세트와 테스트 세트로 나누어, 이미지 경로와 라벨(멀티라벨)을 각각 별도의 .npy 파일로 저장하는 전처리 스크립트이다. 

 

 

https://github.com/youngwk/IDMN/tree/main

 

GitHub - youngwk/IDMN: [IEEE JSTARS] Instance-Dependent Multi-Label Noise Generation for Multi-Label Remote Sensing Image Classi

[IEEE JSTARS] Instance-Dependent Multi-Label Noise Generation for Multi-Label Remote Sensing Image Classification - youngwk/IDMN

github.com

 

 

 

 

 

 

 

각 부분의 단계별 설명.

 

1. 주요 변수 및 작업 흐름

1. 인자(argument) 파싱

--load-path: 원본 UCmerced 데이터셋이 있는 경로 (예: data/UCMerced_LandUse)

--save-path: 전처리한 결과물을 저장할 경로 (예: data/UCMerced_LandUse)

 

클래스 이름 → 클래스 ID 매핑(catName_to_catID)

UCmerced에 포함된 21개 클래스 이름을 0~20까지의 정수로 매핑.

예: 'agricultural': 0, 'airplane': 1, 'tenniscourt': 20 등.

 

멀티라벨 정보 로드(multilabel_metadata)

LandUse_multilabels.mat 파일에서 labels라는 키로 멀티라벨 정보를 읽어옴.

이 매트릭스((17, 2100) 크기로 추정)에는 각 이미지별 17개 라벨(UCmerced에서 사용하는 멀티라벨 스키마로 보임)이 0 또는 1 형태로 저장되어 있는 것으로 추측됨.

 

참고: UCmerced 기본적으로 단일 클래스(21개)로 유명하지만, 여기서는 별도의 멀티라벨 정보를 만든 것으로 보임. 클래스 갯수(21)와 멀티라벨 갯수(17)는 다를 수 있음.

 

 

배열 초기화

image_list_train, image_list_test: 훈련/테스트 이미지 경로를 담을 리스트.

label_matrix_train, label_matrix_test: 훈련/테스트 라벨 정보를 담을 배열.

크기는 (int(2100*0.8), 17) = (1680, 17)(int(2100*0.2), 17) = (420, 17).

즉 전체 2100장의 80% → 1680장은 훈련용, 20% → 420장은 테스트용.

 

이미지 파일 순회

예: agricultural00.tif, agricultural01.tif … 식으로 이미지를 순회.

각 클래스당 이미지가 100장씩 있고, 클래스 ID에 따라 인덱스를 구분하려는 의도.

예: airplane는 ID=1이므로 1*100 + imgnum → 100~199 범위 인덱스.

 

훈련/테스트 분할

이미지 번호(80~99) → 테스트 데이터

해당 분류 기준에 따라 image_list_train(혹은 image_list_test)에 이미지 경로를 append, label_matrix_train(혹은 label_matrix_test)에 라벨을 기록.

 

 

이 스크립트는 UCmerced 데이터셋(클래스명 폴더로 구분된 2,100장)을 멀티라벨(MAT 파일)과 매핑하여, 훈련 세트(80%)·테스트 세트(20%)로 분할하고, 이미지 경로와 라벨을 각각 별도의 .npy 파일로 저장한다. 주요 아이디어는 클래스 ID에 따라 이미지를 0~99 범위로 번호 매긴 뒤, 이 번호를 이용해 LandUse_multilabels.mat의 라벨을 가져온다는 점이다. 이렇게 전처리해두면, 추후 모델 구현 시 .npy 파일만 로드하여 쉽게 학습·평가를 진행할 수 있다.

 

 

 

 

 

 

아래 코드는 CLIP 모델을 이용해 UCMerced 데이터셋 이미지를 입력받은 뒤, 각 클래스(17개)에 대한 로짓(logits) 값을 구하고, 텍스트 임베딩 간의 유사도 행렬(Adjacency)을 저장하는 과정을 보여준다. 주요 흐름과 각각의 의미를 단계별로 정리해보겠다.

 

 

1. 전처리 및 준비

 

UCMERCED_CATEGORY = [

    'airplane', 'bare soil', 'buildings', 'cars', 'chaparral', 

    'court', 'dock', 'field', 'grass', 'mobile home', 

    'pavement', 'sand', 'sea', 'ship', 'tanks', 

    'trees', 'water'

]

 

UCMerced 데이터셋에 대해 정의한 17개 라벨 이름. (단일라벨이 아닌 멀티라벨 구성이므로, 이미지 한 장이 여러 개 라벨을 가질 수 있음)

 

실험 설정(데이터셋 이름, 이미지 크기, 클래스 수, 데이터 경로 등)을 담은 인자(Args).

 

model, preprocess = clip.load('ViT-B/32', device=device)

 

OpenAI에서 제공하는 CLIP 모델(ViT-B/32)과 해당 모델에 맞는 전처리 함수를 로드.

preprocess는 이미지를 (224 or 256) x 224 or 256 크기로 리사이즈하고, 정규화 등을 수행.

 

17개 클래스 이름에 대해 clip.tokenize(...)를 호출해 토큰화.

예: "an aerial image of a airplane", "an aerial image of a bare soil", …

이후 텐서를 하나로 합친(torch.cat) 뒤, GPU로 옮김.

 

 

 

 

2. 텍스트 임베딩 / 유사도(Adjacency) 계산

 

with torch.no_grad():

    text_features = model.encode_text(text_inputs)

    text_features /= text_features.norm(dim=-1, keepdim=True)

    similarity = text_features @ text_features.T



np.save('data/UCMerced_LandUse/clip_adjacency.npy', similarity.cpu().numpy())

 

 

model.encode_text(text_inputs)

입력된 17개 문장(프롬프트)에 대해, CLIP의 텍스트 인코더가 임베딩을 계산.

결과는 (17 × D) 형태. (D는 CLIP의 임베딩 차원, 예: 512)

 

text_features /= text_features.norm(dim=-1, keepdim=True)

각 문장 벡터를 L2 노멀라이즈해서, 길이가 1이 되도록 맞춤.

이렇게 하면 두 벡터 간의 내적이 코사인 유사도(cosine similarity)와 동일해짐.

 

similarity = text_features @ text_features.T

17개의 텍스트 임베딩끼리 코사인 유사도를 계산 → (17 × 17) 행렬.

이를 clip_adjacency.npy로 저장.

 

의도: 서로 다른 카테고리끼리 텍스트 임베딩이 얼마나 유사/차별적인지 확인하려는 목적.

 

 

 

 

 

3. 이미지 입력 후 로짓 계산

 

fp_clip = np.zeros((len(dataset['train']), args.num_classes))

fn_clip = np.zeros((len(dataset['train']), args.num_classes))

clip_logits_matrix = np.zeros((len(dataset['train']), args.num_classes))

 

 

fp_clip, fn_clip는 아마 False Positive / False Negative 관련 통계를 추적하기 위해 준비된 것으로 보이지만, 아래 코드에서는 실제로 채워지지 않고 있다(또는 후속 코드가 생략되었을 수 있음).

 

clip_logits_matrix: (훈련 샘플 수 × 17) 형태로, 각 이미지에 대해 17개 카테고리에 대한 로짓(logits)을 저장할 배열.

 

 

 

 

for idx in range(len(dataset['train'])):

    image_path = dataset['train'].image_paths[idx]

    label = dataset['train'].label_matrix[idx]

    pos_idx = np.nonzero(label)[0]

    neg_idx = np.nonzero(label == 0)[0]



    image = preprocess(Image.open(image_path)).unsqueeze(0).to(device)



    with torch.no_grad():

        _, logits_per_text = model(image, text_inputs)

        logits_per_text = logits_per_text.squeeze().cpu()



        clip_logits_matrix[idx] = logits_per_text

 

 

루프: for idx in range(...):

훈련용 이미지(예: 1,680장)를 순회.

 

image_path, label

이미지 경로(image_path)와 해당 이미지의 멀티라벨 벡터(label, 길이 17)를 가져옴.

pos_idx는 1이 들어있는 라벨의 인덱스(즉, 실제 양성 클래스),

neg_idx는 0이 들어있는 라벨 인덱스(즉, 실제 음성 클래스).

 

이미지 전처리 (preprocess)

이미지를 CLIP에서 학습할 때 사용한 방식에 맞춰 사이즈 조정, 정규화 등의 작업을 수행하고,

배치 차원을 만들기 위해 unsqueeze(0)를 사용.

 

model(image, text_inputs)

CLIP 모델에 이미지와 텍스트를 동시에 입력하면,

반환 값은 (logits_per_image, logits_per_text) 형태.

여기서는 _, logits_per_text만 받고 있음.

logits_per_text는 텍스트를 기준으로 한 로짓. 보통 CLIP은

logits_per_image: 이미지 vs. 텍스트 임베딩 내적

logits_per_text: 텍스트 vs. 이미지 임베딩 내적

같은 값을 전치(Transpose) 형태로 표현하는 경우가 많음.

(17,) 모양의 벡터를 가져오려면 squeeze().cpu()로 차원을 줄이고, CPU 텐서로 변환.

 

clip_logits_matrix[idx] = logits_per_text

현재 이미지에 대한 각 클래스별 로짓을 저장.

이후 로짓값을 사용해 스코어 임계값에 따라 예측을 계산하거나, FP/TP/정밀도 등을 측정할 수 있음.

 

 

정리하자면, 이 코드는 CLIP을 활용해 “UCMerced의 멀티라벨 위성 이미지가 텍스트 프롬프트와 얼마나 대응되는지”를 확인하기 위한 준비 작업에 가깝다. 텍스트 임베딩 간 유사도, 그리고 이미지 입력에 대한 각 텍스트 라벨 로짓을 산출해 .npy로 저장함으로써 이후 분석/학습 파이프라인에서 손쉽게 재활용할 수 있도록 해주는 역할을 한다.

 

 

 

 

 

 

해당 inject_noise 함수가 ‘CLIP 로그it을 활용한 멀티라벨 noise injection’의 메인 아이디어

 

코드의 핵심 로직을 간단히 정리하면 다음과 같다:

 

def inject_noise(self, args):
        
        clip_logits = np.load(os.path.join(args.path_to_dataset, 'clip_logits.npy'))
        pos_logits = clip_logits[self.label_matrix == 1]
        neg_logits = clip_logits[self.label_matrix == 0]

        subtractive_threshold = np.percentile(pos_logits, args.noise_rate)
        additive_threshold = np.percentile(pos_logits, 100 - args.noise_rate)
        

        num_subtractive = 0 
        num_additive = 0 

        for i in range(self.label_matrix.shape[0]):
            for j in range(self.label_matrix.shape[1]):
                if self.label_matrix[i][j] == 0 and clip_logits[i][j] > additive_threshold:
                    self.label_matrix[i][j] = 1 # additive noise
                    num_additive += 1
                elif self.label_matrix[i][j] == 1 and clip_logits[i][j] < subtractive_threshold:
                    self.label_matrix[i][j] = 0 # subtractive noise
                    num_subtractive += 1
        print(f'Noise rate : {args.noise_rate}')
        print(f'Number of additive noise : {num_additive}')
        print(f'Number of subtractive noise : {num_subtractive}')

 

 

1. 이전에 CLIP 모델로부터 얻은 이미지별 클래스별 로짓(logits) 행렬을 불러온다.

 

2. 양성/음성 분포 파악

 

현재 라벨 매트릭스(self.label_matrix) 상에서 실제로 1(양성, positive)인 위치에 해당하는 로짓만 따로 모으고(pos_logits), 0(음성, negative)인 위치는 neg_logits로 모은다.

여기서 중요한 것은, 이미지를 “양성”으로 라벨링했을 때 그 CLIP 로짓 값들이 어떠한 분포를 갖고 있는지를 확인할 수 있다는 점이다.

 

3. Threshold(임계값) 설정

 

subtractive_threshold: “양성 샘플” 로짓 분포에서 하위 noise_rate%에 해당하는 지점 → 로짓값이 이보다 작으면 “실제로는 양성이 아닐 수도 있겠다”라고 간주.

additive_threshold: “양성 샘플” 로짓 분포에서 상위 noise_rate%에 해당하는 지점 → 로짓값이 이보다 크면 “실제로는 양성일 수도 있겠다”라고 간주.

예를 들어, args.noise_rate = 10이라면, 상위 90% 지점(additive_threshold), 하위 10% 지점(subtractive_threshold)이 된다.

 

4. 노이즈 주입 (라벨 뒤집기)

 

Additive noise(0→1): 원래 라벨은 0(음성)이었지만, CLIP 로짓이 매우 높다면(양성 분포의 상위 구간을 넘는다면) 사실상 누락된 라벨(거짓 음성)일 가능성이 있다고 보고 1로 바꿔준다.

Subtractive noise(1→0): 원래 라벨은 1(양성)이었지만, CLIP 로짓이 매우 낮다면(양성 분포의 최하위 구간이라면) 잘못 붙은 라벨(거짓 양성)일 수 있다고 보고 0으로 바꿔준다.

 

최종적으로, 초기의 label_matrix가 CLIP 로짓 정보를 바탕으로 일부 수정(=노이즈 주입 또는 라벨 보정)되어, 기존에 0이었던 위치가 1이 되거나, 1이었던 위치가 0이 될 수 있다.

“노이즈 주입”이라 부르지만, 실제로는 “CLIP 모델을 통해 부분 라벨 누락(거짓 음성)과 잘못된 라벨(거짓 양성)을 보정”하려는 목적과 유사하다.

로그를 보면, “Number of additive noise : X”, “Number of subtractive noise : Y” 형태로 몇 개 라벨이 바뀌었는지 표시해준다.

 

 

한줄 요약

 

inject_noise 함수는 CLIP 로짓을 이용해,

(1) 라벨이 0이지만 CLIP 점수가 높은 경우 → 1로 뒤집어 (additive noise)

(2) 라벨이 1이지만 CLIP 점수가 낮은 경우 → 0으로 뒤집어 (subtractive noise)

하는 방식으로 멀티라벨 데이터에서 누락되거나 잘못된 라벨을 보정하는 핵심 아이디어를 담고 있다.

 

 

 

 

 

 

 

 

 

 

 

...오늘은  Instance Dependent Multi Label Noise Generation for Multi-Label Remote Sensing Image Classification 논문 데이터처리 코드 리뷰를 해보았다.

반응형