위성 사진 데이터셋에 대해 공부하고 있어서 위성사진 데이터처리 공부중에 교수님의 논문의 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 논문 데이터처리 코드 리뷰를 해보았다.