[논문 코드 리뷰] Instance Dependent Multi Label Noise Generation for Multi-Label Remote Sensing Image Classification 데이터 전처리, 메인 코드 리뷰
위성 사진 데이터셋에 대해 공부하고 있어서 위성사진 데이터처리 공부중에 교수님의 논문의 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 논문 데이터처리 코드 리뷰를 해보았다.