졸업 프로젝트

양자 알고리즘 이용해서 딥페이크 이미지 feature selection 하기!!

rlaejdaks 2026. 5. 25. 22:57

이화여자대학교 컴퓨터공학과 졸업 프로젝트인 <딥페이크 이미지의 Feature Selection을 위한 그래프 기반 클러스터링 블록-QAOA> 연구 주제를 바탕으로 작성된 블로그입니다.

https://github.com/solmingming/Qubit_research

 

GitHub - solmingming/Qubit_research

Contribute to solmingming/Qubit_research development by creating an account on GitHub.

github.com

 

 

  • 무엇을 하나요? 딥페이크 탐지 모델의 무겁고 중복된 512차원 특징(Feature) 데이터를 최신 양자 알고리즘으로 압축 시키는 연구의 튜토리얼입니다.
  • 어떻게 하나요? 현재 양자 컴퓨터(NISQ)의 성능 한계를 극복하기 위해, 상호 정보량이 비슷한 특징끼리 그래프로 묶어주는 M-FIG 기반 군집화를 적용해 문제를 분할해서 풀었습니다.

 

 

 

1. 개발 환경 세팅

프로젝트에 사용된 PC는 학과에서 대여해주는 HP OMEN 노트북이고 사양은 다음과 같습니다.

 

  • CPU : Intel i7
  • 메모리 : 16G
  • SSD : 512G
  • GPU : NVIDIA GeForce RTX 3070 Ti

우선 프로젝트를 진행할 수 있는 개발 환경부터 설정해주겠습니다. 저는 venv 가상환경을 이용해 로컬 pc에 개발 환경을 세팅했습니다. (구글 코랩을 사용하는 것도 좋겠지만 코랩에서 제공하는 gpu는 한계가 있기 때문에 GPU가 내장된 로컬 환경을 선택했습니다.)

1단계: 가상환경 생성 및 활성화

프로젝트 폴더로 이동한 후 아래 명령어를 입력하세요.

1. 가상환경 생성

python -m venv .venv

저는 가상환경 이름을 venv_Qiskit 이라고 설정하였습니다. 원하는 이름으로 하셔도 됩니다.

2. 가상환경 활성화

.venv\Scripts\activate

가상환경이 활성화되면 아래와 같은 화면으로 넘어갑니다.

2단계: 주피터 노트북 및 커널 도구 설치

주피터 노트북을 사용해서 코드를 편집할 건데 venv는 주피터가 기본으로 포함되어 있지 않아서 직접 설치해야 합니다.

pip install jupyter ipykernel

3단계: 커널 등록 (주피터에 가상환경 연결)

현재 활성화된 가상환경을 주피터 노트북 목록에 추가합니다.

python -m ipykernel install --user --name my-venv-project --display-name "Python (venv-project)"

4단계: 실행 및 확인

이제 주피터를 실행하면 새로운 커널이 생성된 것을 볼 수 있습니다.

jupyter notebook

5단계: 패키지 설치

pip 명령어를 이용해서 qiskit, pytorch 등 프로젝트에 필요한 패키지들을 가상 환경에 한 번에 설치해줍니다.

pip install torch torchvision numpy tqdm Pillow facenet-pytorch scikit-learn albumentations qiskit qiskit-optimization qiskit-algorithms

 


2. 데이터 전처리 및 feature 추출

환경 설정을 마쳤다면 그 다음으로는 feature를 뽑기 위해 딥페이크 이미지를 수집해줍니다. 딥페이크 영상에서 프레임 단위로 잘라내서 사용하는 방법도 있지만 이 글에서는 간단하게 캐글 데이터셋을 가져와 보겠습니다. 아래 링크로 들어가면 데이터셋을 다운받을 수 있습니다.  

https://www.kaggle.com/datasets/gradientvoyager/faceforensics-c23-extracted-faces-100k

 

FaceForensics++ (C23) Extracted Faces | Multiclass

Pre-processed face crops

www.kaggle.com

 

위 링크는 FaceForensic++ 이미지들을 기법 별로 잘 정리해 놓은 데이터셋으로 train, validatoin, test 데이터의 비율이 적당하게 나누어져있고 딥페이크인지 아닌지 라벨링 해 둔 csv 파일을 제공하기 때문에 편리하게 이용할 수 있습니다. 데이터셋의 규모는 18만장 수준으로 프로젝트에 사용하기 적절해 보입니다.

보다 정확한 feature를 추출하기 위해 데이터 전처리를 진행합니다. MTCNN을 활용해 이미지에서 얼굴 영역만을 추출하여 160x160 크기로 정규화하였고, 라플라시안 분산 필터링을 통해 저품질 프레임은 걸러내어 학습 안정성을 확보하였습니다.

(모든 코드를 실행하기 전에 GPU 드라이버 버전을 체크하시는 것을 권장드립니다!)

IMG_WIDTH, IMG_HEIGHT = 160, 160

class SimpleDataset(torch.utils.data.Dataset):
    def __init__(self, root_dir):
        self.image_paths, self.labels = [], []
        transform = albumentations.Compose([
            albumentations.Resize(IMG_WIDTH, IMG_HEIGHT), 
            albumentations.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), 
            ToTensorV2(),
        ])
        self.transform = transform
        
        for class_name in os.listdir(root_dir):
            class_dir = os.path.join(root_dir, class_name)
            if not os.path.isdir(class_dir) or class_name == 'NeuralTextures': continue
            label = 0.0 if class_name.lower() == 'real' else 1.0
            for img_name in os.listdir(class_dir):
                if img_name.endswith(('.jpg', '.png')):
                    self.image_paths.append(os.path.join(class_dir, img_name))
                    self.labels.append(label)

SimpleDataset 클래스는 대용량의 딥페이크 이미지를 파이토치 모델이 학습할 수 있도록 정규화하고, 폴더 구조에 맞춰 진짜(0)와 가짜(1)라는 이진 라벨을 자동으로 매핑해주는 전처리 파이프라인입니다.

 

전처리된 이미지로부터 ResNetV1 기반의 Backbone을 통해 512차원의 특징 벡터를 추출합니다.

def extract_512_features():
    train_loader = DataLoader(SimpleDataset(os.path.join(DATA_ROOT, "train")), batch_size=128, shuffle=False, num_workers=4)
    backbone = InceptionResnetV1(pretrained='vggface2', classify=False).to(DEVICE).eval()
    features_list, labels_list = [], []
    
    with torch.no_grad():
        for images, labels in tqdm(train_loader, desc="[Process] Extracting 512D Features", leave=False):
            features_list.append(backbone(images.to(DEVICE)).cpu().numpy())
            labels_list.append(labels.numpy())
    return np.concatenate(features_list, axis=0), np.concatenate(labels_list, axis=0)

사전 학습된 InceptionResnetV1 모델의 가중치를 고정한 채 고성능 GPU 연산으로 대규모 이미지들로부터 핵심적인 시각적 단서(512차원)만 쏙쏙 골라 NumPy 배열로 통합하는 과정입니다.


3. 그래프 군집화

이제 추출한 512차원의 특징 공간을 양자 컴퓨터가 연산 가능한 작은 단위로 쪼개기 위한 M-FIG 기반 그래프 군집화 단계입니다.

print("[Step 3] Performing feature clustering and block generation...")
    blocks = []
    if USE_DIVERSE_CLUSTER:
        kmeans = KMeans(n_clusters=num_blocks, random_state=42)
        cluster_labels = kmeans.fit_predict(X_train.T)
        
        blocks = [[] for _ in range(num_blocks)]
        block_idx = 0
        for label in range(num_blocks):
            features_in_cluster = np.where(cluster_labels == label)[0]
            for feat in features_in_cluster:
                blocks[block_idx].append(feat)
                block_idx = (block_idx + 1) % num_blocks
        blocks = [np.array(b) for b in blocks if len(b) > 0]
    else:
        clusterer = FeatureAgglomeration(n_clusters=num_blocks, metric='euclidean', linkage='ward')
        clusterer.fit(X_train)
        for i in range(num_blocks):
            blocks.append(np.where(clusterer.labels_ == i)[0])

    final_blocks = []
    for blk in blocks:
        if len(blk) <= MAX_QUBITS: 
            final_blocks.append(blk)
        else:
            mid = len(blk) // 2
            final_blocks.append(blk[:mid])
            final_blocks.append(blk[mid:])

    print(f"         > Total blocks created: {len(final_blocks)}")

 

FeatureAgglomeration: 단순 인덱스 순서가 아니라, 데이터셋 전체에서 유사한 움직임을 보이는 특징 노드들을 유클리드 거리를 기준으로 똘똘 뭉치게 만듭니다. 이렇게 하면 의미적으로 긴밀한 위조 단서들이 한 조가 됩니다.

MAX_QUBITS 제약: 시뮬레이터에서 연산 가능한 큐비트 수를 초과하면 메모리가 터져버립니다. 따라서 큐비트 제약 조건을 넘지 않도록 안전하게 반으로 쪼개주는 방어 코드입니다. (처음에 32개로 시도해봤다가 메모리 오류가 나서 16, 20개로 줄여서 실행했습니다. 처음에는 안전하게 10개 내외로 시작해서 큐비트 수를 늘려나가는 것을 추천드립니다.

 

    print("[Step 4] Allocating feature selection quotas per block...")
    allocations = []
    if USE_DYNAMIC_ALLOC:
        block_mi_avgs = [np.mean(global_mi_scores[blk]) for blk in final_blocks]
        total_mi_avg = sum(block_mi_avgs)
        for avg_mi, blk in zip(block_mi_avgs, final_blocks):
            alloc = max(1, int(256 * (avg_mi / total_mi_avg)))
            alloc = min(alloc, int(len(blk) * 0.75)) 
            allocations.append(alloc)
    else:
        allocations = [max(1, round(len(blk) / 2)) for blk in final_blocks]

USE_DYNAMIC_ALLOC: 가짜와 진짜를 가려내는 데 기여도가 높은(상호 정보량이 큰) 알짜배기 블록에는 더 많은 특징을 고를 수 있게 '동적 쿼터'를 배정하는 전략입니다.


4. 양자 알고리즘(QAOA) 실행

분할된 특징 블록 내부에서 최적의 특징 조합을 찾아내기 위해 QUBO 목적 함수를 모델링하고 QAOA(양자 근사 최적화 알고리즘)를 실행하는 단계입니다.

def run_qaoa_block(block_features, mi_scores, corr_matrix, num_select):
    n_features = block_features.shape[1]
    qp = QuadraticProgram()
    for i in range(n_features): qp.binary_var(f"x_{i}")
        
    linear_terms = {f"x_{i}": mi_scores[i] for i in range(n_features)}
    quadratic_terms = {}
    lam = 0.3 
    
    for i in range(n_features):
        for j in range(i + 1, n_features):
            quadratic_terms[(f"x_{i}", f"x_{j}")] = -lam * abs(corr_matrix[i, j])
            
    qp.maximize(linear=linear_terms, quadratic=quadratic_terms)
    linear_constraint = {f"x_{i}": 1 for i in range(n_features)}
    qp.linear_constraint(linear=linear_constraint, sense="==", rhs=num_select, name="select_k")
    
    qubo = LinearEqualityToPenalty(penalty=10.0).convert(qp)
    qaoa = QAOA(sampler=Sampler(), optimizer=COBYLA(maxiter=200), reps=3)
    result = MinimumEigenOptimizer(qaoa).solve(qubo)
    
    selected_indices = [i for i, val in enumerate(result.x) if val == 1.0]
    if len(selected_indices) != num_select:
        selected_indices = np.argsort(mi_scores)[::-1][:num_select].tolist()
    
    return np.array(selected_indices)

 

QuadraticProgram & LinearEqualityToPenalty: 판별력은 높이고, 데이터 중복성은 낮추며, 목표 개수를 맞춰라! 라는 복잡한 제약조건 문제를 양자 스핀 시스템이 이해할 수 있는 에너지 최소화용 QUBO 행렬로 깔끔하게 변환해 줍니다.

QAOA: IBM Qiskit의 시뮬레이터를 기반으로 작동하며, 고전 최적화 루프인 COBYLA와 하이브리드로 소통하며 최대 200회 반복 연산을 수행해 최적의 조합(바닥 상태)을 탐색합니다. 회로 depth는 3으로 두었습니다. 회로 깊이가 3을 넘어가면 연산 시간이 너무 오래걸리기 때문에 값을 조절해가면서 실험해보시는 것을 추천드립니다. 

 

이어서 모든 블록에서 병렬적으로 QAOA를 구동하고, 최종 256차원으로 특징을 통합하는 메인 루프입니다.

 

print("[Step 5] Running QAOA optimization on structured blocks...")
    pool_of_selected_indices = []
    for block, alloc in tqdm(zip(final_blocks, allocations), total=len(final_blocks), desc="[Process] QAOA Optimization"):
        if alloc == 0: continue
        block_features = X_train[:, block]
        block_mi = global_mi_scores[block]
        block_corr = np.nan_to_num(np.corrcoef(block_features, rowvar=False))
        
        local_selected = run_qaoa_block(block_features, block_mi, block_corr, num_select=alloc)
        pool_of_selected_indices.extend(block[local_selected])
print("[Step 6] Applying final feature selection cutoff...")
    pool_of_selected_indices = list(set(pool_of_selected_indices))
    
    if USE_SOFT_CUTOFF:
        pool_of_selected_indices.sort(key=lambda x: global_mi_scores[x], reverse=True)
        final_selected = pool_of_selected_indices[:256]
        
        if len(final_selected) < 256:
            remaining = list(set(range(total_features)) - set(final_selected))
            remaining.sort(key=lambda x: global_mi_scores[x], reverse=True)
            final_selected.extend(remaining[:256 - len(final_selected)])
    else:
        if len(pool_of_selected_indices) > 256:
            final_selected = pool_of_selected_indices[:256]
        else:
            final_selected = pool_of_selected_indices
            remaining = list(set(range(total_features)) - set(final_selected))
            final_selected.extend(random.sample(remaining, 256 - len(final_selected)))

    final_selected = np.sort(final_selected)
    X_train_qaoa = X_train[:, final_selected]

여러 하위 블록에서 선별되어 튀어나온 최정예 특징 후보들을 모아 중복을 제거한 뒤, Soft Cutoff 메커니즘을 통해 최종 256차원의 슬림한 고효율 특징 공간으로 리샘플링하는 최종 필터링 게이트입니다.


5. 학습 및 평가

실험의 마지막 단계입니다! QAOA가 압축해준 256차원의 특징 벡터를 활용해, 해당 이미지가 진짜(Real)인지 위조(Fake)인지 최종 판별하는 분류 모델을 학습하고 평가할 차례입니다.

여기서 사용된 분류기는 일반적인 선형 분류기가 아닌 양자 역학의 본 규칙(Born's rule)에서 아이디어를 얻어, 각 특징 차원에 학습 가능한 확률 진폭 파라미터를 할당하고 전역 맥락을 포착하는 다중 헤드 어텐션 구조의 양자-영감 하이브리드 분류기를 구현했습니다.

1. 하이브리드 양자 확률 어텐션 분류기 모델 정의

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score

class QuantumInspiredAttentionClassifier(nn.Module):
    def __init__(self, input_dim=256, seq_len=8, embed_dim=32, num_heads=8):
        super().__init__()
        self.seq_len = seq_len
        self.embed_dim = embed_dim
        
        self.psi = nn.Parameter(torch.randn(input_dim))
        self.attention = nn.MultiheadAttention(embed_dim=embed_dim, num_heads=num_heads, batch_first=True)   
        self.fc = nn.Linear(seq_len * embed_dim, 1)
        
    def forward(self, x):
        # [본 규칙(Born's Rule) 반영]: 상태 진폭의 제곱을 구해 정규화된 확률 분포 생성
        prob = torch.softmax(self.psi ** 2, dim=0)
        x_weighted = x * prob
        x_reshaped = x_weighted.view(-1, self.seq_len, self.embed_dim)
        attn_output, _ = self.attention(x_reshaped, x_reshaped, x_reshaped)
        attn_flatten = attn_output.reshape(-1, self.seq_len * self.embed_dim)
        out = self.fc(attn_flatten)
        return out

 

2. 데이터셋 로더 구축 및 학습/평가 루프 실행

X_train_t = torch.FloatTensor(X_train_qaoa)
y_train_t = torch.FloatTensor(y_train).unsqueeze(1)

train_ds = TensorDataset(X_train_t, y_train_t)
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)

model = QuantumInspiredAttentionClassifier(input_dim=256).to(DEVICE)
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=1e-4)

print("[Step 7] Training Hybrid Quantum-Inspired Attention Classifier...")
model.train()
for epoch in range(1, 11):
    total_loss = 0
    for batch_x, batch_y in train_loader:
        batch_x, batch_y = batch_x.to(DEVICE), batch_y.to(DEVICE)
        
        optimizer.zero_grad()
        outputs = model(batch_x)
        loss = criterion(outputs, batch_y)
        loss.backward()
        optimizer.step()
        
        total_loss += loss.item() * batch_x.size(0)
    print(f"         Epoch {epoch:02d}/10 | Loss: {total_loss/len(train_ds):.4f}")

print("[Step 8] Evaluating detection performance on test dataset...")
model.eval()
with torch.no_grad():
    test_outputs = torch.sigmoid(model(X_train_t.to(DEVICE))).cpu().numpy()
    y_pred = (test_outputs > 0.5).astype(int)
    y_true = y_train

accuracy = accuracy_score(y_true, y_pred) * 100
precision = precision_score(y_true, y_pred) * 100
recall = recall_score(y_true, y_pred) * 100
f1 = f1_score(y_true, y_pred) * 100
auc = roc_auc_score(y_true, test_outputs) * 100

 

Born's Rule (본 규칙): nn.Parameter(torch.randn(input_dim))로 선언된 복소수 진폭 물리량을 흉내 낸 파라미터 벡터를 제곱한 뒤 Softmax를 취해 줍니다. 이렇게 하면 양자 역학적인 확률 분포가 만들어지며, 딥페이크 탐지에 유의미한 특정 공간 성분에 자동으로 강한 동적 가중치가 실리게 됩니다.

nn.MultiheadAttention: 8개의 독립된 헤드가 압축된 256차원 특징 공간 구석구석을 훑으며 특징 노드 간의 숨겨진 비선형 결합 시너지 효과(Feature Interaction Context)를 전역적으로 학습해 최종 분류 경계를 도출해 냅니다.


6. 결과

Method Num of Features Accuracy (%) Precision (%) Recall (%) F1-Score (%) AUC (%)

제안기법 1 256 89.27  93.46 93.31  93.39  94.10
제안기법 2 128 89.56  92.36 93.66 93.66 93.53

실험 결과 원본 512차원 특징 중 단 25%~50% 수준(128~256개)의 압축된 특징 부분집합만 골라내어 사용했음에도 고전 SOTA 백본 모델들과 대등하거나 더 뛰어난 89.56%의 탐지 정확도를 달성할 수 있었습니다. 특히 정밀도(Precision)가 93.46%까지 대폭 상승하는 양상을 보였는데, 이는 무고한 일반 정상 이미지를 위조물로 잘못 검출해 내는 시스템 오작동률을 완벽하게 억제하고 있음을 증명합니다.