deepclip 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepclip/__init__.py +31 -0
- deepclip/config/deepclip_b0.yaml +65 -0
- deepclip/config/deepclip_b5.yaml +65 -0
- deepclip/data/__init__.py +9 -0
- deepclip/data/collate.py +104 -0
- deepclip/data/dataset.py +67 -0
- deepclip/data/handle_imbalance.py +44 -0
- deepclip/data/split_data.py +83 -0
- deepclip/data/text/__init__.py +16 -0
- deepclip/data/text/bpe_simple_vocab_16e6.txt.gz +0 -0
- deepclip/data/text/template.py +133 -0
- deepclip/data/text/tokenizer.py +146 -0
- deepclip/data/transforms.py +76 -0
- deepclip/layers/__init__.py +13 -0
- deepclip/layers/attention.py +153 -0
- deepclip/layers/clip_text.py +61 -0
- deepclip/layers/drop.py +35 -0
- deepclip/layers/featextractor.py +56 -0
- deepclip/layers/global_query.py +65 -0
- deepclip/layers/mlp.py +41 -0
- deepclip/layers/patch.py +58 -0
- deepclip/layers/reducesize.py +45 -0
- deepclip/layers/se.py +28 -0
- deepclip/layers/transformer.py +266 -0
- deepclip/layers/weight_init.py +30 -0
- deepclip/layers/window.py +22 -0
- deepclip/losses.py +16 -0
- deepclip/models/__init__.py +8 -0
- deepclip/models/deepclip.py +199 -0
- deepclip/models/image_encoder.py +198 -0
- deepclip/models/text_encoder.py +125 -0
- deepclip/optimizer/__init__.py +1 -0
- deepclip/optimizer/build_optimizer.py +29 -0
- deepclip/scheduler/__init__.py +1 -0
- deepclip/scheduler/fetch_scheduler.py +58 -0
- deepclip/utils/__init__.py +4 -0
- deepclip/utils/build_metrics.py +62 -0
- deepclip/utils/early_stopping.py +24 -0
- deepclip/utils/meter.py +26 -0
- deepclip/utils/seed.py +23 -0
- deepclip-0.1.0.dist-info/METADATA +36 -0
- deepclip-0.1.0.dist-info/RECORD +45 -0
- deepclip-0.1.0.dist-info/WHEEL +5 -0
- deepclip-0.1.0.dist-info/licenses/LICENSE +21 -0
- deepclip-0.1.0.dist-info/top_level.txt +1 -0
deepclip/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import logging
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("deepclip") # pyproject.toml
|
|
7
|
+
except PackageNotFoundError: # Not installed (e.g. run from the repository)
|
|
8
|
+
__version__ = "0.0.0"
|
|
9
|
+
|
|
10
|
+
# ------- 로그 및 경고 제어 ---------
|
|
11
|
+
warnings.filterwarnings("ignore") # 일반 경고 숨기기
|
|
12
|
+
logging.getLogger("timm").setLevel(logging.ERROR) # timm 내부 로그 숨기기
|
|
13
|
+
logging.getLogger("torch").setLevel(logging.ERROR) # torch 관련 로그 숨기기
|
|
14
|
+
|
|
15
|
+
from .models import image_encoder, text_encoder, deepclip
|
|
16
|
+
from .models.image_encoder import DeepImageEncoder
|
|
17
|
+
from .models.text_encoder import DeepTextEncoder
|
|
18
|
+
from .models.deepclip import (
|
|
19
|
+
DeepClip,
|
|
20
|
+
deepclip_b0,
|
|
21
|
+
deepclip_b5,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"__version__",
|
|
26
|
+
"DeepImageEncoder",
|
|
27
|
+
"DeepTextEncoder",
|
|
28
|
+
"DeepClip",
|
|
29
|
+
"deepclip_b0",
|
|
30
|
+
"deepclip_b5",
|
|
31
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
dataset: "FaceForensics++"
|
|
2
|
+
|
|
3
|
+
model:
|
|
4
|
+
model_name: "tf_efficientnet_b0.ns_jft_in1k"
|
|
5
|
+
img_size: [224,224]
|
|
6
|
+
|
|
7
|
+
# ===== Feature Extraction Blocks =====
|
|
8
|
+
l_block_idx: 1 # Low-level feature stage index
|
|
9
|
+
h_block_idx: 6 # High-level feature stage index
|
|
10
|
+
|
|
11
|
+
# ===== Transformer Dimensions =====
|
|
12
|
+
l_dim: 24 # Dimension for low-block transformer
|
|
13
|
+
h_dim: 256 # Dimension for high-block transformer
|
|
14
|
+
l_depths: [2,2,4,2] # Number of transformer layers (Low)
|
|
15
|
+
h_depths: [4] # Number of transformer layers (High)
|
|
16
|
+
|
|
17
|
+
# ===== Attention Settings =====
|
|
18
|
+
l_windows: [7,7,14,7] # Window Size for Attention (Low)
|
|
19
|
+
h_windows: [7] # Window Size for Attention (High)
|
|
20
|
+
l_heads: [1,2,4,8] # Multi Head Attention (Low)
|
|
21
|
+
h_heads: [4] # Multi Head Attention (High)
|
|
22
|
+
l_ratio: [4,4,4,4] # MLP expansion ratio (Low)
|
|
23
|
+
h_ratio: [4] # MLP expansion ratio (High)
|
|
24
|
+
|
|
25
|
+
# ===== Regularization (Dropout) =====
|
|
26
|
+
l_drop: 0 # Pos dropout, Mlp dropout, Proj dropout (Low)
|
|
27
|
+
h_drop: 0.05 # Pos dropout, Mlp dropout, Proj dropout (High)
|
|
28
|
+
l_attn_drop: 0.05 # Attention Dropout (Low)
|
|
29
|
+
h_attn_drop: 0 # Attention Dropout (High)
|
|
30
|
+
l_drop_path: 0.1 # Stochastic depth rate (Low)
|
|
31
|
+
h_drop_path: 0.05 # Stochastic depth rate (High)
|
|
32
|
+
|
|
33
|
+
# ===== Norm (GCViT + image projection) =====
|
|
34
|
+
image_norm: "rmsnorm" # rmsnorm or layernorm
|
|
35
|
+
|
|
36
|
+
# ===== Text Encoder (frozen) =====
|
|
37
|
+
clip_name: "ViT-B/16"
|
|
38
|
+
embed_dim: 512 # Text embedding dimension of clip_name (checked)
|
|
39
|
+
|
|
40
|
+
text:
|
|
41
|
+
type_sampling_alpha: 1.0 # Type weight = freq^-alpha
|
|
42
|
+
region_sampling_beta: 1.0 # Region weight = region_freq^-beta
|
|
43
|
+
|
|
44
|
+
loss:
|
|
45
|
+
real_text_weight: 1.0 # Weight of the REAL_TEXT column of the loss
|
|
46
|
+
fake_text_weight: 1.0 # Weight of the FAKE caption columns of the loss
|
|
47
|
+
|
|
48
|
+
train:
|
|
49
|
+
epochs: 10
|
|
50
|
+
use_amp: True # Mixed Precision Training
|
|
51
|
+
n_accumulate: 1 # Gradient Accumulation steps
|
|
52
|
+
optimizer: "AdamW"
|
|
53
|
+
scheduler: "CosineAnnealingLR"
|
|
54
|
+
|
|
55
|
+
# DataLoader
|
|
56
|
+
train_bs: 16
|
|
57
|
+
valid_bs: 32
|
|
58
|
+
|
|
59
|
+
# Optimizer (decay / no_decay groups, same lr)
|
|
60
|
+
lr: 2e-4
|
|
61
|
+
weight_decay: 1e-3
|
|
62
|
+
|
|
63
|
+
# Early Stopping (valid_loss)
|
|
64
|
+
patience: 3
|
|
65
|
+
min_delta: 0.001
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
dataset: "FaceForensics++"
|
|
2
|
+
|
|
3
|
+
model:
|
|
4
|
+
model_name: "tf_efficientnet_b5.ns_jft_in1k"
|
|
5
|
+
img_size: [384,384]
|
|
6
|
+
|
|
7
|
+
# ===== Feature Extraction Blocks =====
|
|
8
|
+
l_block_idx: 1 # Low-level feature stage index
|
|
9
|
+
h_block_idx: 6 # High-level feature stage index
|
|
10
|
+
|
|
11
|
+
# ===== Transformer Dimensions =====
|
|
12
|
+
l_dim: 48 # Dimension for low-block transformer
|
|
13
|
+
h_dim: 512 # Dimension for high-block transformer
|
|
14
|
+
l_depths: [2,2,6,2] # Number of transformer layers (Low)
|
|
15
|
+
h_depths: [6] # Number of transformer layers (High)
|
|
16
|
+
|
|
17
|
+
# ===== Attention Settings =====
|
|
18
|
+
l_windows: [12,12,24,12] # Window Size for Attention (Low)
|
|
19
|
+
h_windows: [12] # Window Size for Attention (High)
|
|
20
|
+
l_heads: [2,4,8,16] # Multi Head Attention (Low)
|
|
21
|
+
h_heads: [16] # Multi Head Attention (High)
|
|
22
|
+
l_ratio: [3,3,3,3] # MLP expansion ratio (Low)
|
|
23
|
+
h_ratio: [3] # MLP expansion ratio (High)
|
|
24
|
+
|
|
25
|
+
# ===== Regularization (Dropout) =====
|
|
26
|
+
l_drop: 0.0 # Pos dropout, Mlp dropout, Proj dropout (Low)
|
|
27
|
+
h_drop: 0.1 # Pos dropout, Mlp dropout, Proj dropout (High)
|
|
28
|
+
l_attn_drop: 0.1 # Attention Dropout (Low)
|
|
29
|
+
h_attn_drop: 0.0 # Attention Dropout (High)
|
|
30
|
+
l_drop_path: 0.15 # Stochastic depth rate (Low)
|
|
31
|
+
h_drop_path: 0.1 # Stochastic depth rate (High)
|
|
32
|
+
|
|
33
|
+
# ===== Norm (GCViT + image projection) =====
|
|
34
|
+
image_norm: "rmsnorm" # rmsnorm or layernorm
|
|
35
|
+
|
|
36
|
+
# ===== Text Encoder (frozen) =====
|
|
37
|
+
clip_name: "ViT-L/14"
|
|
38
|
+
embed_dim: 768 # Text embedding dimension of clip_name (checked)
|
|
39
|
+
|
|
40
|
+
text:
|
|
41
|
+
type_sampling_alpha: 1.0 # Type weight = freq^-alpha
|
|
42
|
+
region_sampling_beta: 1.0 # Region weight = region_freq^-beta
|
|
43
|
+
|
|
44
|
+
loss:
|
|
45
|
+
real_text_weight: 1.0 # Weight of the REAL_TEXT column of the loss
|
|
46
|
+
fake_text_weight: 1.0 # Weight of the FAKE caption columns of the loss
|
|
47
|
+
|
|
48
|
+
train:
|
|
49
|
+
epochs: 10
|
|
50
|
+
use_amp: True # Mixed Precision Training
|
|
51
|
+
n_accumulate: 1 # Gradient Accumulation steps
|
|
52
|
+
optimizer: "AdamW"
|
|
53
|
+
scheduler: "CosineAnnealingLR"
|
|
54
|
+
|
|
55
|
+
# DataLoader
|
|
56
|
+
train_bs: 16
|
|
57
|
+
valid_bs: 32
|
|
58
|
+
|
|
59
|
+
# Optimizer (decay / no_decay groups, same lr)
|
|
60
|
+
lr: 5e-5
|
|
61
|
+
weight_decay: 2e-4
|
|
62
|
+
|
|
63
|
+
# Early Stopping (valid_loss)
|
|
64
|
+
patience: 3
|
|
65
|
+
min_delta: 0.001
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .dataset import DeepClipDataset
|
|
2
|
+
from .collate import DeepClipCollate
|
|
3
|
+
from .transforms import (
|
|
4
|
+
get_train_transforms,
|
|
5
|
+
get_valid_transforms,
|
|
6
|
+
get_test_transforms,
|
|
7
|
+
)
|
|
8
|
+
from .handle_imbalance import class_imbalance_handle
|
|
9
|
+
from .split_data import split_data
|
deepclip/data/collate.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
from torch.utils.data import get_worker_info
|
|
4
|
+
from deepclip.data.text import REAL_TEXT, parse_pair_label, sample_selection, render_caption
|
|
5
|
+
|
|
6
|
+
class DeepClipCollate:
|
|
7
|
+
"""
|
|
8
|
+
Makes the captions of a batch and the targets of the loss.
|
|
9
|
+
|
|
10
|
+
For every image: REAL -> REAL_TEXT, FAKE -> the caption of the pair chosen by sample_selection
|
|
11
|
+
(a new choice at every step). The B captions are reduced to the U unique captions (in order).
|
|
12
|
+
|
|
13
|
+
Returns a dict:
|
|
14
|
+
images (B,3,H,W)
|
|
15
|
+
labels (B,) 0: REAL, 1: FAKE
|
|
16
|
+
text_idx (U,) rows of text_table of the unique captions
|
|
17
|
+
Y (B,U) 1 if caption j is true for image i
|
|
18
|
+
(REAL_TEXT column: image i is REAL, FAKE column: the pair of caption j is on in image i)
|
|
19
|
+
W (B,U) real_text_weight for the REAL_TEXT column, fake_text_weight for the FAKE columns
|
|
20
|
+
pair_targets (B,20) pair_label of every image (all 0 for REAL), target of map and the AP
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
caption_to_idx: dict,
|
|
26
|
+
type_weights: np.ndarray,
|
|
27
|
+
region_weights: np.ndarray,
|
|
28
|
+
real_text_weight: float = 1.0,
|
|
29
|
+
fake_text_weight: float = 1.0,
|
|
30
|
+
seed: int | None = None,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Args:
|
|
34
|
+
caption_to_idx: caption -> row of text_table (DeepClip.caption_to_idx).
|
|
35
|
+
type_weights: (4,5) weights of the types (type_weights of the train FAKE frames).
|
|
36
|
+
region_weights: (4,) weights of the regions (region_weights of the train FAKE frames).
|
|
37
|
+
real_text_weight, fake_text_weight: Weights of the REAL_TEXT column and the FAKE columns of the loss.
|
|
38
|
+
seed: Fixed seed for the same captions at every epoch (valid). None for new captions at every epoch (train).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
self.caption_to_idx = caption_to_idx
|
|
42
|
+
self.type_weights = type_weights
|
|
43
|
+
self.region_weights = region_weights
|
|
44
|
+
self.real_text_weight = real_text_weight
|
|
45
|
+
self.fake_text_weight = fake_text_weight
|
|
46
|
+
self.seed = seed
|
|
47
|
+
self.reset()
|
|
48
|
+
|
|
49
|
+
def reset(self):
|
|
50
|
+
"""Restarts the random captions (with seed: the same captions as the previous epoch when num_workers=0)"""
|
|
51
|
+
self.rng = np.random.default_rng(self.seed if self.seed is not None else np.random.randint(2**31))
|
|
52
|
+
self._worker_id = None
|
|
53
|
+
|
|
54
|
+
def _update_rng(self):
|
|
55
|
+
# Every DataLoader worker gets a copy of this object, so the rng is set again in each worker
|
|
56
|
+
info = get_worker_info()
|
|
57
|
+
|
|
58
|
+
if info is not None and self._worker_id != info.id:
|
|
59
|
+
self._worker_id = info.id
|
|
60
|
+
self.rng = np.random.default_rng([self.seed, info.id] if self.seed is not None else info.seed)
|
|
61
|
+
|
|
62
|
+
def __call__(self, batch):
|
|
63
|
+
self._update_rng()
|
|
64
|
+
|
|
65
|
+
imgs, labels, pair_labels = zip(*batch)
|
|
66
|
+
images = torch.stack(imgs) # (B,3,H,W)
|
|
67
|
+
labels = torch.stack(labels).float() # (B,)
|
|
68
|
+
|
|
69
|
+
lab = np.stack([parse_pair_label(p) for p in pair_labels]) # (B,4,5)
|
|
70
|
+
is_fake = labels.numpy() == 1
|
|
71
|
+
|
|
72
|
+
# ====== Caption of every image ======
|
|
73
|
+
captions, selections = [], []
|
|
74
|
+
for i in range(len(batch)):
|
|
75
|
+
if is_fake[i]:
|
|
76
|
+
sel = sample_selection(lab[i], self.type_weights, self.region_weights, self.rng)
|
|
77
|
+
captions.append(render_caption(sel))
|
|
78
|
+
else:
|
|
79
|
+
sel = np.zeros_like(lab[i])
|
|
80
|
+
captions.append(REAL_TEXT)
|
|
81
|
+
selections.append(sel)
|
|
82
|
+
|
|
83
|
+
# ====== Unique captions (in order) ======
|
|
84
|
+
uniq = list(dict.fromkeys(captions))
|
|
85
|
+
first = [captions.index(c) for c in uniq]
|
|
86
|
+
text_idx = torch.tensor([self.caption_to_idx[c] for c in uniq], dtype=torch.long) # (U,)
|
|
87
|
+
|
|
88
|
+
# ====== Targets Y and weights W ======
|
|
89
|
+
is_real_col = np.array([c == REAL_TEXT for c in uniq]) # (U,)
|
|
90
|
+
sel_cols = np.stack([selections[i] for i in first]) # (U,4,5)
|
|
91
|
+
|
|
92
|
+
# the pair of caption j is on in image i
|
|
93
|
+
subset = ~(sel_cols[None] & ~lab[:, None]).any(axis=(2, 3)) # (B,U)
|
|
94
|
+
Y = np.where(is_real_col[None], ~is_fake[:, None], is_fake[:, None] & subset)
|
|
95
|
+
W = np.where(is_real_col, self.real_text_weight, self.fake_text_weight)
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"images": images,
|
|
99
|
+
"labels": labels,
|
|
100
|
+
"text_idx": text_idx,
|
|
101
|
+
"Y": torch.from_numpy(Y).float(),
|
|
102
|
+
"W": torch.from_numpy(np.broadcast_to(W, Y.shape).copy()).float(),
|
|
103
|
+
"pair_targets": torch.from_numpy(lab.reshape(len(batch), -1)).long(),
|
|
104
|
+
}
|
deepclip/data/dataset.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from typing import List
|
|
3
|
+
import cv2
|
|
4
|
+
import torch
|
|
5
|
+
from torch.utils.data import Dataset
|
|
6
|
+
import albumentations as A
|
|
7
|
+
|
|
8
|
+
class DeepClipDataset(Dataset):
|
|
9
|
+
"""
|
|
10
|
+
Dataset class for DeepClip, based on DeepFakeDataset of DeepGuard.
|
|
11
|
+
Reads image paths, labels and pair labels from a pandas DataFrame, loads images,
|
|
12
|
+
and applies specified transformations. The caption is made by DeepClipCollate.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
meta_df (pd.DataFrame): Dataframe containing image paths, labels and pair labels.
|
|
16
|
+
transforms (A.Compose): Albumentations transformation pipeline.
|
|
17
|
+
img_size (List[int]): [Height, Width] of the image.
|
|
18
|
+
img_col (str): Name of the column containing image file paths.
|
|
19
|
+
label_col (str): Name of the column containing labels (0: REAL, 1: FAKE).
|
|
20
|
+
pair_col (str): Name of the column containing pair labels (20 characters of 0 and 1).
|
|
21
|
+
return_label (bool): Whether to return (image, label, pair_label) or just image.
|
|
22
|
+
"""
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
meta_df: pd.DataFrame,
|
|
26
|
+
img_size: List[int],
|
|
27
|
+
transforms: A.Compose,
|
|
28
|
+
img_col: str = "img_path",
|
|
29
|
+
label_col: str = "label",
|
|
30
|
+
pair_col: str = "pair_label",
|
|
31
|
+
return_label: bool = True,
|
|
32
|
+
):
|
|
33
|
+
self.meta_df = meta_df
|
|
34
|
+
self.img_size = img_size
|
|
35
|
+
self.transforms = transforms(img_size)
|
|
36
|
+
self.img_col = img_col
|
|
37
|
+
self.label_col = label_col
|
|
38
|
+
self.pair_col = pair_col
|
|
39
|
+
self.return_label = return_label
|
|
40
|
+
|
|
41
|
+
def __len__(self):
|
|
42
|
+
return len(self.meta_df)
|
|
43
|
+
|
|
44
|
+
def _load_image(self, path: str):
|
|
45
|
+
img_bgr = cv2.imread(path)
|
|
46
|
+
if img_bgr is None:
|
|
47
|
+
raise FileNotFoundError(f"Cannot read image: {path}")
|
|
48
|
+
|
|
49
|
+
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
|
50
|
+
|
|
51
|
+
return img_rgb
|
|
52
|
+
|
|
53
|
+
def __getitem__(self, idx):
|
|
54
|
+
row = self.meta_df.iloc[idx]
|
|
55
|
+
img_path = str(row[self.img_col])
|
|
56
|
+
|
|
57
|
+
img = self._load_image(img_path)
|
|
58
|
+
img = self.transforms(image=img)['image']
|
|
59
|
+
|
|
60
|
+
if not self.return_label:
|
|
61
|
+
return img
|
|
62
|
+
else:
|
|
63
|
+
label = row[self.label_col]
|
|
64
|
+
return img, torch.tensor(label, dtype=torch.float32), str(row[self.pair_col])
|
|
65
|
+
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return f"DeepClipDataset with {len(self.meta_df)} samples"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
def class_imbalance_handle(
|
|
5
|
+
real_df: pd.DataFrame,
|
|
6
|
+
fake_df: pd.DataFrame,
|
|
7
|
+
k: int,
|
|
8
|
+
seed: int = 2025):
|
|
9
|
+
"""
|
|
10
|
+
Constructs a balanced training dataset by combining real data with a specific slice of fake data.
|
|
11
|
+
|
|
12
|
+
This function implements a sliding-window approach to select synthetic (fake) samples
|
|
13
|
+
to match the count of real samples. It ensures a 1:1 class distribution for
|
|
14
|
+
iterative training or cross-validation scenarios.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
real_df (pd.DataFrame): The original/minority class dataframe.
|
|
18
|
+
fake_df (pd.DataFrame): The synthetic/majority class dataframe to sample from.
|
|
19
|
+
k (int): The fold index used to determine the starting position of the fake data slice.
|
|
20
|
+
seed (int, optional): Random seed for reproducibility during sampling and shuffling. Defaults to 2025.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
n_real = len(real_df)
|
|
25
|
+
start_idx = k * n_real
|
|
26
|
+
end_idx = start_idx + n_real
|
|
27
|
+
|
|
28
|
+
# Handle wrap-around if the slice exceeds the number of fake samples
|
|
29
|
+
if end_idx > len(fake_df):
|
|
30
|
+
remaining = fake_df[start_idx:]
|
|
31
|
+
n_remaining = len(remaining)
|
|
32
|
+
|
|
33
|
+
# Sample additional fake data from the start to match the required slice
|
|
34
|
+
extra = fake_df[:start_idx].sample(n=n_real - n_remaining, replace=False, random_state=seed)
|
|
35
|
+
fake_slice = pd.concat([remaining, extra], axis=0).reset_index(drop=True)
|
|
36
|
+
|
|
37
|
+
else:
|
|
38
|
+
fake_slice = fake_df[start_idx:end_idx].reset_index(drop=True)
|
|
39
|
+
|
|
40
|
+
# Combine real and fake samples, then shuffle
|
|
41
|
+
train_df = pd.concat([real_df, fake_slice], ignore_index=True)
|
|
42
|
+
train_df = train_df.sample(frac=1, random_state=seed).reset_index(drop=True)
|
|
43
|
+
|
|
44
|
+
return train_df
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from sklearn.model_selection import train_test_split, GroupKFold
|
|
3
|
+
|
|
4
|
+
def split_data(
|
|
5
|
+
meta_df: pd.DataFrame,
|
|
6
|
+
seed: int = 2025,
|
|
7
|
+
label_col: str = 'label',
|
|
8
|
+
ori_vid: str = 'ori_vid',
|
|
9
|
+
dataset: str = 'kodf',
|
|
10
|
+
group_col: str = "pid",
|
|
11
|
+
test_size: float = 0.2,
|
|
12
|
+
balance_val: bool = True,
|
|
13
|
+
debug: bool = False,
|
|
14
|
+
debug_ratio: float = 0.1,
|
|
15
|
+
) :
|
|
16
|
+
"""Splits the dataset into training and validation sets based on original video IDs.
|
|
17
|
+
|
|
18
|
+
This function ensures that all fake videos derived from the same original video
|
|
19
|
+
stay within the same split (Train or Val) to prevent data leakage. It first
|
|
20
|
+
splits the real videos and then maps the corresponding fake videos.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
meta_df: The complete metadata DataFrame containing labels and video IDs.
|
|
24
|
+
seed: Random seed for reproducibility. Defaults to 2025.
|
|
25
|
+
label_col: Name of the column containing labels (0 for REAL, 1 for FAKE).
|
|
26
|
+
ori_vid: Name of the column identifying the original video source.
|
|
27
|
+
test_size: Proportion of the dataset to include in the validation split.
|
|
28
|
+
balance_val: If True, samples the fake validation set to match the
|
|
29
|
+
length of the real validation set.
|
|
30
|
+
debug: If True, downsamples the final datasets for quick testing.
|
|
31
|
+
debug_ratio: The ratio of data to keep when debug mode is enabled.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
tuple[pd.DataFrame, pd.DataFrame]: A tuple containing (train_df, valid_df).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Split real and fake(0: REAL, 1: FAKE)
|
|
38
|
+
|
|
39
|
+
assert meta_df[label_col].dtype in [int, float], "You need to label encoding {'FAKE': 1, 'REAL': 0}"
|
|
40
|
+
|
|
41
|
+
df_real = meta_df[meta_df[label_col] == 0].reset_index(drop=True)
|
|
42
|
+
df_fake = meta_df[meta_df[label_col] == 1].reset_index(drop=True)
|
|
43
|
+
|
|
44
|
+
# Split real Images
|
|
45
|
+
if dataset.lower() != "kodf":
|
|
46
|
+
real_train, real_val = train_test_split(df_real, test_size=test_size, random_state=seed, shuffle=True)
|
|
47
|
+
else:
|
|
48
|
+
df_real_shuffled = df_real.sample(frac=1, random_state=seed).reset_index(drop=True)
|
|
49
|
+
|
|
50
|
+
n_splits = int(1 / test_size)
|
|
51
|
+
gkf = GroupKFold(n_splits=n_splits)
|
|
52
|
+
|
|
53
|
+
splits = list(gkf.split(df_real_shuffled, groups=df_real_shuffled[group_col]))
|
|
54
|
+
train_idx, val_idx = splits[0]
|
|
55
|
+
|
|
56
|
+
real_train = df_real_shuffled.iloc[train_idx]
|
|
57
|
+
real_val = df_real_shuffled.iloc[val_idx]
|
|
58
|
+
|
|
59
|
+
# Match fake images to corresponding real train/val
|
|
60
|
+
fake_train = df_fake[df_fake[ori_vid].isin(real_train[ori_vid].unique())].reset_index(drop=True)
|
|
61
|
+
fake_val = df_fake[df_fake[ori_vid].isin(real_val[ori_vid].unique())].reset_index(drop=True)
|
|
62
|
+
|
|
63
|
+
# Sample fake_val to match real_val length
|
|
64
|
+
if balance_val:
|
|
65
|
+
fake_val = fake_val.sample(len(real_val), random_state=seed)
|
|
66
|
+
|
|
67
|
+
# Concatenate real + fake
|
|
68
|
+
train_df = pd.concat([real_train, fake_train], axis=0)
|
|
69
|
+
valid_df = pd.concat([real_val, fake_val], axis=0)
|
|
70
|
+
|
|
71
|
+
# Debug mode: downsample dataset
|
|
72
|
+
if debug:
|
|
73
|
+
n_train = max(1, int(len(train_df) * debug_ratio))
|
|
74
|
+
n_valid = max(1, int(len(valid_df) * debug_ratio))
|
|
75
|
+
train_df = train_df.sample(n=n_train, random_state=seed).reset_index(drop=True)
|
|
76
|
+
valid_df = valid_df.sample(n=n_valid, random_state=seed).reset_index(drop=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Shuffle final datasets
|
|
80
|
+
train_df = train_df.sample(frac=1, random_state=seed).reset_index(drop=True)
|
|
81
|
+
valid_df = valid_df.sample(frac=1, random_state=seed).reset_index(drop=True)
|
|
82
|
+
|
|
83
|
+
return train_df, valid_df
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .template import (
|
|
2
|
+
REGIONS,
|
|
3
|
+
TYPES,
|
|
4
|
+
PAIR_ORDER,
|
|
5
|
+
REAL_TEXT,
|
|
6
|
+
parse_pair_label,
|
|
7
|
+
pair_frequency,
|
|
8
|
+
region_frequency,
|
|
9
|
+
type_weights,
|
|
10
|
+
region_weights,
|
|
11
|
+
sample_selection,
|
|
12
|
+
render_caption,
|
|
13
|
+
all_captions,
|
|
14
|
+
single_pair_caption,
|
|
15
|
+
)
|
|
16
|
+
from .tokenizer import tokenize
|
|
Binary file
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Face forgery text template.
|
|
3
|
+
|
|
4
|
+
Every rule about captions is kept in this file: the order of the 20 (region, type) pairs of `pair_label`
|
|
5
|
+
(also used by preprocess/raw_annotation.py), the caption phrases, the sampling of the pairs described by a
|
|
6
|
+
caption and the list of every caption a model can see (the rows of its text table).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
REGIONS = ("face", "eyes", "nose", "mouth")
|
|
12
|
+
TYPES = ("blur", "color", "texture", "structure", "blend")
|
|
13
|
+
# Order of the 20 characters of pair_label: index = 5 * region + type
|
|
14
|
+
PAIR_ORDER = [(region, type_) for region in REGIONS for type_ in TYPES]
|
|
15
|
+
|
|
16
|
+
REGION_PHRASES = {
|
|
17
|
+
"face": "the whole face",
|
|
18
|
+
"eyes": "the eyes",
|
|
19
|
+
"nose": "the nose",
|
|
20
|
+
"mouth": "the mouth",
|
|
21
|
+
}
|
|
22
|
+
TYPE_PHRASES = {
|
|
23
|
+
"blur": "unnatural blur",
|
|
24
|
+
"color": "unnatural color variations",
|
|
25
|
+
"texture": "unusual texture patterns",
|
|
26
|
+
"structure": "structural distortion",
|
|
27
|
+
"blend": "blending artifacts",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# REAL caption of the loss, also the caption of the real/fake score (p_fake = 1 - p_real)
|
|
31
|
+
REAL_TEXT = "a photo of a real face."
|
|
32
|
+
|
|
33
|
+
def parse_pair_label(pair_label: str) -> np.ndarray:
|
|
34
|
+
"""'0101...' (20 characters) -> (4, 5) bool array"""
|
|
35
|
+
|
|
36
|
+
if len(pair_label) != len(PAIR_ORDER):
|
|
37
|
+
raise ValueError(f"pair_label must have {len(PAIR_ORDER)} characters: {pair_label!r}")
|
|
38
|
+
|
|
39
|
+
return (np.frombuffer(pair_label.encode(), dtype=np.uint8) == ord("1")).reshape(len(REGIONS), len(TYPES))
|
|
40
|
+
|
|
41
|
+
def _stack(pair_labels) -> np.ndarray:
|
|
42
|
+
return np.stack([parse_pair_label(s) for s in pair_labels])
|
|
43
|
+
|
|
44
|
+
def pair_frequency(pair_labels) -> np.ndarray:
|
|
45
|
+
"""
|
|
46
|
+
pair_label of the FAKE frames -> (4, 5) frequency.
|
|
47
|
+
freq(region, type) = frames where the type flag of the region is on / frames where the region is forged
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
labels = _stack(pair_labels)
|
|
51
|
+
forged = labels.any(axis=2).sum(axis=0)
|
|
52
|
+
|
|
53
|
+
return labels.sum(axis=0) / np.maximum(forged, 1)[:, None]
|
|
54
|
+
|
|
55
|
+
def region_frequency(pair_labels) -> np.ndarray:
|
|
56
|
+
"""pair_label of the FAKE frames -> (4,) ratio of the frames where each region is forged"""
|
|
57
|
+
|
|
58
|
+
return _stack(pair_labels).any(axis=2).mean(axis=0)
|
|
59
|
+
|
|
60
|
+
def type_weights(freq: np.ndarray, alpha: float) -> np.ndarray:
|
|
61
|
+
"""(4, 5) w = freq^-alpha (0 for a pair that never appears)"""
|
|
62
|
+
|
|
63
|
+
return np.where(freq > 0, np.power(np.maximum(freq, 1e-12), -alpha), 0.0)
|
|
64
|
+
|
|
65
|
+
def region_weights(freq: np.ndarray, beta: float) -> np.ndarray:
|
|
66
|
+
"""(4,) rw = freq^-beta (0 for a region that is never forged)"""
|
|
67
|
+
|
|
68
|
+
return np.where(freq > 0, np.power(np.maximum(freq, 1e-12), -beta), 0.0)
|
|
69
|
+
|
|
70
|
+
def _choice(rng: np.random.Generator, items: np.ndarray, size: int, weights: np.ndarray) -> np.ndarray:
|
|
71
|
+
"""
|
|
72
|
+
Weighted choice without replacement (uniform when every weight is 0).
|
|
73
|
+
An item with weight 0 (a pair or region never seen in the train FAKE frames) gets the smallest nonzero
|
|
74
|
+
weight, otherwise there can be fewer nonzero weights than size and rng.choice fails.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
nonzero = weights > 0
|
|
78
|
+
if not nonzero.all():
|
|
79
|
+
weights = np.where(nonzero, weights, weights[nonzero].min() if nonzero.any() else 1.0)
|
|
80
|
+
|
|
81
|
+
return rng.choice(items, size=size, replace=False, p=weights / weights.sum())
|
|
82
|
+
|
|
83
|
+
def sample_selection(label: np.ndarray, weights: np.ndarray, region_weights: np.ndarray,
|
|
84
|
+
rng: np.random.Generator) -> np.ndarray:
|
|
85
|
+
"""
|
|
86
|
+
Chooses the (region, type) pair described by the caption of one FAKE image in this step.
|
|
87
|
+
|
|
88
|
+
1. One region is chosen among the forged regions with the probability region_weights[region]
|
|
89
|
+
(normalized among them).
|
|
90
|
+
2. One type is chosen among the types which are on in that region with the probability
|
|
91
|
+
weights[region, type] (normalized among them).
|
|
92
|
+
|
|
93
|
+
label: (4, 5) bool, the pairs which are on in the image
|
|
94
|
+
return: (4, 5) bool with one True, always a subset of label
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
active_regions = np.flatnonzero(label.any(axis=1))
|
|
98
|
+
|
|
99
|
+
if len(active_regions) == 0:
|
|
100
|
+
raise ValueError("A FAKE image needs at least one pair which is on")
|
|
101
|
+
|
|
102
|
+
selection = np.zeros_like(label, dtype=bool)
|
|
103
|
+
|
|
104
|
+
region = _choice(rng, active_regions, 1, region_weights[active_regions])[0]
|
|
105
|
+
active_types = np.flatnonzero(label[region])
|
|
106
|
+
type_ = _choice(rng, active_types, 1, weights[region, active_types])[0]
|
|
107
|
+
selection[region, type_] = True
|
|
108
|
+
|
|
109
|
+
return selection
|
|
110
|
+
|
|
111
|
+
def render_caption(selection: np.ndarray) -> str:
|
|
112
|
+
"""Chosen pair ((4, 5) bool with one True) -> caption. No pair gives REAL_TEXT."""
|
|
113
|
+
|
|
114
|
+
pairs = np.argwhere(selection)
|
|
115
|
+
|
|
116
|
+
if len(pairs) == 0:
|
|
117
|
+
return REAL_TEXT
|
|
118
|
+
if len(pairs) != 1:
|
|
119
|
+
raise ValueError(f"A caption describes one (region, type) pair, not {len(pairs)}")
|
|
120
|
+
|
|
121
|
+
region, type_ = pairs[0]
|
|
122
|
+
return f"a photo of a fake face with {TYPE_PHRASES[TYPES[type_]]} in {REGION_PHRASES[REGIONS[region]]}."
|
|
123
|
+
|
|
124
|
+
def all_captions() -> list:
|
|
125
|
+
"""Every caption the collate can make, in a fixed order: REAL_TEXT, then the 20 captions of PAIR_ORDER"""
|
|
126
|
+
|
|
127
|
+
return [REAL_TEXT] + [single_pair_caption(region, type_) for region, type_ in PAIR_ORDER]
|
|
128
|
+
|
|
129
|
+
def single_pair_caption(region: str, type_: str) -> str:
|
|
130
|
+
selection = np.zeros((len(REGIONS), len(TYPES)), dtype=bool)
|
|
131
|
+
selection[REGIONS.index(region), TYPES.index(type_)] = True
|
|
132
|
+
|
|
133
|
+
return render_caption(selection)
|