eliya commited on
Commit
67982e2
·
verified ·
1 Parent(s): af734e5

Upload 3rd_ce_full_0304

Browse files
Files changed (5) hide show
  1. README.md +122 -0
  2. checkpoint_epoch_5.pt +3 -0
  3. inference.py +61 -0
  4. model.py +157 -0
  5. requirements.txt +4 -0
README.md CHANGED
@@ -1,3 +1,125 @@
1
  ---
2
  license: cc-by-nc-4.0
 
 
 
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: cc-by-nc-4.0
3
+ pipeline_tag: audio-classification
4
+ library_name: pytorch
5
+ language:
6
+ - en
7
+ base_model: microsoft/wavlm-large
8
+ tags:
9
+ - audio-classification
10
+ - deepfake-detection
11
+ - anti-spoofing
12
+ - speech
13
+ - aasist
14
+ - wavlm
15
  ---
16
+
17
+ # forensics_0.3B_base
18
+
19
+ **The default speech deepfake detector of the Forensics family.** WavLM-large + AASIST graph-attention, fully fine-tuned end-to-end (no frozen shortcuts) across a wide multi-source mix of TTS spoofs, voice conversion, codec artifacts, and the standard anti-spoofing benchmark suite. A combined cross-entropy + OC-Softmax + supervised-contrastive objective gives it a decision boundary that holds up well outside its own training distribution — not just on the data it saw.
20
+
21
+ Feed it 5 seconds of audio, get back a calibrated real/fake probability. Sub-1% EER on held-out data, and under 2% across most of a 14-benchmark external sweep (ASVspoof, ADD, In-the-Wild, LibriSeVoc, SONAR, and more).
22
+
23
+ - **Backbone:** `microsoft/wavlm-large` (~300M params, fully unfrozen)
24
+ - **Pooling:** AASIST graph-attention
25
+
26
+ ## Part of the Forensics family
27
+
28
+ | Model | Use it for |
29
+ |---|---|
30
+ | **`forensics_0.3B_base`** (this model) | general-purpose default |
31
+ | [`forensics_0.3B_xlsr_wild`](https://huggingface.co/eliya/forensics_0.3B_xlsr_wild) | uncontrolled / real-world audio |
32
+ | [`forensics_0.3B_wavlm_age_gender`](https://huggingface.co/eliya/forensics_0.3B_wavlm_age_gender) | + speaker age/gender |
33
+ | [`forensics_0.3B_wavlm_oc_softmax`](https://huggingface.co/eliya/forensics_0.3B_wavlm_oc_softmax) | tighter bonafide boundary, ensembling |
34
+
35
+ Full family: [huggingface.co/collections/eliya/forensics](https://huggingface.co/collections/eliya/forensics)
36
+
37
+ ## Training
38
+
39
+ Trained using an agentic training loop — see [eliyasegev/autotrain](https://github.com/eliyasegev/autotrain).
40
+
41
+ Fine-tuned with a combined loss for robustness beyond any single objective:
42
+ - Cross-entropy (label-smoothed)
43
+ - OC-Softmax — pulls bonafide speech into a compact embedding sphere and pushes every spoof type outside it
44
+ - Supervised contrastive, real-anchor-only
45
+
46
+ Trained across a wide net of public, free-to-use research sources: SpeechFake, MD-CommonVoice, DFADD, CodecFake, ASVspoof2019-LA, EnvSDD. 5-second crops, AdamW, cosine LR schedule, and a heavy augmentation stack — codec transcoding (mp3/aac/opus/vorbis/µ-law/A-law/GSM), MUSAN noise, RIR, RawBoost, SpecAugment, FreqMask, splice/mix, and cross-class splice — so the model sees more distortion during training than it will ever encounter in the wild.
47
+
48
+ ## Results
49
+
50
+ | Eval set | EER % |
51
+ |---|---|
52
+ | Val (held-out) | 0.72 |
53
+ | MLAAD (v7) | 0.71 |
54
+ | CodecFake | 0.54 |
55
+ | DFADD | 0.00 |
56
+ | MD-CommonVoice | 0.17 |
57
+ | In-the-Wild | 1.38 |
58
+ | ASVspoof2019-LA | 0.26 |
59
+ | ASVspoof2021-LA | 1.56 |
60
+ | ASVspoof2024 | 11.91 |
61
+ | ADD2022-Track1 | 17.34 |
62
+ | ADD2022-Track3 | 3.03 |
63
+ | ADD2023-Round1 | 6.46 |
64
+ | ADD2023-Round2 | 13.00 |
65
+ | LibriSeVoc | 0.04 |
66
+ | SONAR | 0.44 |
67
+ | **Avg (all sets)** | **3.84** |
68
+ | **Avg (external only)** | **4.06** |
69
+
70
+ Consistently sub-2% EER across almost every external benchmark, with strong results even on the harder ADD/ASVspoof2024 tracks.
71
+
72
+ ## Files in this repo
73
+ | file | purpose |
74
+ |---|---|
75
+ | `3rd_ce_full_0304_resume_ep5.safetensors` | model weights, safe format (add this — see `convert_to_safetensors.py`) |
76
+ | `3rd_ce_full_0304_resume_ep5.pt` | model weights, legacy pickle (add this if not converting) |
77
+ | `inference.py` | run script — prefers the `.safetensors` file automatically |
78
+ | `model.py` | architecture |
79
+ | `requirements.txt` | deps |
80
+
81
+ ## Setup
82
+ ```bash
83
+ pip install -r requirements.txt # torch, torchaudio, transformers, safetensors
84
+ ```
85
+
86
+ ## Run
87
+ ```bash
88
+ python inference.py <audio.wav>
89
+ ```
90
+ (Optionally override the checkpoint: `python inference.py <audio.wav> <checkpoint.pt>`.)
91
+
92
+ Audio is auto-converted to mono / 16 kHz and trimmed/padded to 5 s.
93
+
94
+ ## Output
95
+ ```
96
+ fake_probability: <0..1> # >= 0.5 -> FAKE
97
+ bonafide_score: <0..1> # raw P(real)
98
+ verdict: REAL | FAKE
99
+ ```
100
+
101
+ ## Example
102
+ ```
103
+ $ python inference.py real_human.wav
104
+ fake_probability: 0.0503
105
+ bonafide_score: 0.9497
106
+ verdict: REAL
107
+
108
+ $ python inference.py tts_fake.wav
109
+ fake_probability: 0.8641
110
+ bonafide_score: 0.1359
111
+ verdict: FAKE
112
+ ```
113
+ Higher `fake_probability` = more likely a deepfake. Score is `1 − sigmoid(logit)`,
114
+ since the classifier is trained with label `1 = real, 0 = fake`.
115
+
116
+ ## References
117
+
118
+ - WavLM: Chen et al., 2022, *"WavLM: Large-Scale Self-Supervised Pre-Training for Full Stack Speech Processing"*, arXiv:2110.13900
119
+ - AASIST: Jung et al., ICASSP 2022, *"AASIST: Audio Anti-Spoofing Using Integrated Spectro-Temporal Graph Attention Networks"*
120
+ - RawBoost augmentation: Tak et al., ICASSP 2022
121
+ - FreqMask augmentation: Xie et al., 2024, arXiv:2408.06922
122
+
123
+ ## License
124
+
125
+ CC-BY-NC-4.0 — free for personal and research use. For commercial use, contact eliya@vocos.io.
checkpoint_epoch_5.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2293d041e54c475362d254bf48724c975417daf68cadb9a4a01220338bcbf356
3
+ size 3808741218
inference.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Speech deepfake detection.
2
+ Run: python inference.py <audio.wav> [checkpoint.pt]
3
+ fake_probability = 1 - sigmoid(logit) (>= 0.5 -> FAKE); model trained with label 1=real, 0=fake.
4
+ Audio is auto-converted to mono / 16 kHz and trimmed/padded to 5 s.
5
+ """
6
+ import os
7
+ import sys
8
+ import torch
9
+ import torchaudio
10
+ from model import DeepfakeDetector
11
+
12
+ CHECKPOINT = "3rd_ce_full_0304_resume_ep5.pt"
13
+
14
+
15
+ def load_audio(path, sr=16000, seconds=5.0):
16
+ wav, orig = torchaudio.load(path)
17
+ if wav.shape[0] > 1:
18
+ wav = wav.mean(0, keepdim=True)
19
+ wav = wav.squeeze(0)
20
+ if orig != sr:
21
+ wav = torchaudio.functional.resample(wav, orig, sr)
22
+ wav = wav / (wav.abs().max() + 1e-8)
23
+ n, cur = int(seconds * sr), wav.shape[0]
24
+ if cur < n:
25
+ wav = wav.repeat((n + cur - 1) // cur)[:n]
26
+ elif cur > n:
27
+ s = (cur - n) // 2
28
+ wav = wav[s:s + n]
29
+ return wav
30
+
31
+
32
+ def load_state_dict(pt_path):
33
+ """Prefers a sibling .safetensors file (no code execution risk) over the pickled .pt."""
34
+ st_path = pt_path.rsplit(".", 1)[0] + ".safetensors"
35
+ if os.path.exists(st_path):
36
+ from safetensors.torch import load_file
37
+ return load_file(st_path)
38
+ ck = torch.load(pt_path, map_location="cpu", weights_only=False)
39
+ return ck["model_state_dict"] if isinstance(ck, dict) and "model_state_dict" in ck else ck
40
+
41
+
42
+ @torch.no_grad()
43
+ def main():
44
+ if len(sys.argv) < 2:
45
+ sys.exit("Usage: python inference.py <audio.wav> [checkpoint.pt]")
46
+ audio = sys.argv[1]
47
+ ckpt = sys.argv[2] if len(sys.argv) > 2 else CHECKPOINT
48
+ device = "cuda" if torch.cuda.is_available() else "cpu"
49
+
50
+ model = DeepfakeDetector().to(device).eval()
51
+ model.load_state_dict(load_state_dict(ckpt), strict=False)
52
+
53
+ bonafide = torch.sigmoid(model(load_audio(audio).unsqueeze(0).to(device)).float()).item()
54
+ fake = 1.0 - bonafide
55
+ print(f"fake_probability: {fake:.4f}")
56
+ print(f"bonafide_score: {bonafide:.4f}")
57
+ print(f"verdict: {'FAKE' if fake >= 0.5 else 'REAL'}")
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
model.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """WavLM-large + AASIST speech deepfake detector (inference only)."""
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ from transformers import WavLMModel
6
+
7
+
8
+ class _GraphAttentionLayer(nn.Module):
9
+ def __init__(self, in_dim, out_dim, temperature):
10
+ super().__init__()
11
+ self.att_proj = nn.Linear(in_dim, out_dim)
12
+ self.att_weight = nn.Parameter(torch.empty(out_dim, 1))
13
+ self.proj_with_att = nn.Linear(in_dim, out_dim)
14
+ self.proj_without_att = nn.Linear(in_dim, out_dim)
15
+ self.bn = nn.BatchNorm1d(out_dim)
16
+ self.temp = temperature
17
+
18
+ def forward(self, x):
19
+ N = x.size(1)
20
+ x_i = x.unsqueeze(2).expand(-1, -1, N, -1)
21
+ att = torch.matmul(torch.tanh(self.att_proj(x_i * x_i.transpose(1, 2))), self.att_weight) / self.temp
22
+ att = F.softmax(att, dim=-2)
23
+ out = self.proj_with_att(torch.matmul(att.squeeze(-1), x)) + self.proj_without_att(x)
24
+ s = out.shape
25
+ return F.selu(self.bn(out.reshape(-1, s[-1])).reshape(s))
26
+
27
+
28
+ class _HtrgGraphAttentionLayer(nn.Module):
29
+ def __init__(self, in_dim, out_dim, temperature):
30
+ super().__init__()
31
+ self.proj_type1 = nn.Linear(in_dim, in_dim)
32
+ self.proj_type2 = nn.Linear(in_dim, in_dim)
33
+ self.att_proj = nn.Linear(in_dim, out_dim)
34
+ self.att_projM = nn.Linear(in_dim, out_dim)
35
+ self.att_weight11 = nn.Parameter(torch.empty(out_dim, 1))
36
+ self.att_weight22 = nn.Parameter(torch.empty(out_dim, 1))
37
+ self.att_weight12 = nn.Parameter(torch.empty(out_dim, 1))
38
+ self.att_weightM = nn.Parameter(torch.empty(out_dim, 1))
39
+ self.proj_with_att = nn.Linear(in_dim, out_dim)
40
+ self.proj_without_att = nn.Linear(in_dim, out_dim)
41
+ self.proj_with_attM = nn.Linear(in_dim, out_dim)
42
+ self.proj_without_attM = nn.Linear(in_dim, out_dim)
43
+ self.bn = nn.BatchNorm1d(out_dim)
44
+ self.temp = temperature
45
+
46
+ def forward(self, x1, x2, master):
47
+ n1 = x1.size(1)
48
+ x = torch.cat([self.proj_type1(x1), self.proj_type2(x2)], dim=1)
49
+ N = x.size(1)
50
+ x_i = x.unsqueeze(2).expand(-1, -1, N, -1)
51
+ att_map = torch.tanh(self.att_proj(x_i * x_i.transpose(1, 2)))
52
+ board = torch.zeros_like(att_map[:, :, :, 0]).unsqueeze(-1)
53
+ board[:, :n1, :n1] = torch.matmul(att_map[:, :n1, :n1], self.att_weight11)
54
+ board[:, n1:, n1:] = torch.matmul(att_map[:, n1:, n1:], self.att_weight22)
55
+ board[:, :n1, n1:] = torch.matmul(att_map[:, :n1, n1:], self.att_weight12)
56
+ board[:, n1:, :n1] = torch.matmul(att_map[:, n1:, :n1], self.att_weight12)
57
+ board = F.softmax(board / self.temp, dim=-2)
58
+ out = self.proj_with_att(torch.matmul(board.squeeze(-1), x)) + self.proj_without_att(x)
59
+ s = out.shape
60
+ out = F.selu(self.bn(out.reshape(-1, s[-1])).reshape(s))
61
+ att_m = F.softmax(torch.matmul(torch.tanh(self.att_projM(x * master)), self.att_weightM) / self.temp, dim=-2)
62
+ master = self.proj_with_attM(torch.matmul(att_m.squeeze(-1).unsqueeze(1), x)) + self.proj_without_attM(master)
63
+ return out.narrow(1, 0, n1), out.narrow(1, n1, N - n1), master
64
+
65
+
66
+ class _GraphPool(nn.Module):
67
+ def __init__(self, k, in_dim):
68
+ super().__init__()
69
+ self.k = k
70
+ self.proj = nn.Linear(in_dim, 1)
71
+
72
+ def forward(self, h):
73
+ scores = torch.sigmoid(self.proj(h))
74
+ n_keep = max(int(h.size(1) * self.k), 1)
75
+ idx = torch.topk(scores, n_keep, dim=1)[1].expand(-1, -1, h.size(-1))
76
+ return torch.gather(h * scores, 1, idx)
77
+
78
+
79
+ class AASISTPool(nn.Module):
80
+ def __init__(self, feat_dim, num_layers, gat_dims=(128, 64),
81
+ pool_ratios=(0.5, 0.7, 0.5, 0.5), temps=(2.0, 2.0, 100.0, 100.0)):
82
+ super().__init__()
83
+ g = list(gat_dims)
84
+ self.proj = nn.Linear(feat_dim, g[0])
85
+ self.pos_S = nn.Parameter(torch.randn(1, num_layers, g[0]))
86
+ self.master1 = nn.Parameter(torch.randn(1, 1, g[0]))
87
+ self.master2 = nn.Parameter(torch.randn(1, 1, g[0]))
88
+ self.GAT_S = _GraphAttentionLayer(g[0], g[0], temps[0])
89
+ self.GAT_T = _GraphAttentionLayer(g[0], g[0], temps[1])
90
+ self.HtrgGAT_11 = _HtrgGraphAttentionLayer(g[0], g[1], temps[2])
91
+ self.HtrgGAT_12 = _HtrgGraphAttentionLayer(g[1], g[1], temps[3])
92
+ self.HtrgGAT_21 = _HtrgGraphAttentionLayer(g[0], g[1], temps[2])
93
+ self.HtrgGAT_22 = _HtrgGraphAttentionLayer(g[1], g[1], temps[3])
94
+ self.pool_S = _GraphPool(pool_ratios[0], g[0])
95
+ self.pool_T = _GraphPool(pool_ratios[1], g[0])
96
+ self.pool_hS1 = _GraphPool(pool_ratios[2], g[1])
97
+ self.pool_hT1 = _GraphPool(pool_ratios[2], g[1])
98
+ self.pool_hS2 = _GraphPool(pool_ratios[2], g[1])
99
+ self.pool_hT2 = _GraphPool(pool_ratios[2], g[1])
100
+ self.output_dim = 5 * g[1]
101
+
102
+ def forward(self, hidden_states):
103
+ projected = self.proj(torch.stack(hidden_states, dim=1))
104
+ e_S = torch.max(torch.abs(projected), dim=2)[0] + self.pos_S
105
+ e_T = torch.max(torch.abs(projected), dim=1)[0]
106
+ out_S = self.pool_S(self.GAT_S(e_S))
107
+ out_T = self.pool_T(self.GAT_T(e_T))
108
+ out_T1, out_S1, m1 = self.HtrgGAT_11(out_T, out_S, self.master1)
109
+ out_S1, out_T1 = self.pool_hS1(out_S1), self.pool_hT1(out_T1)
110
+ a_T, a_S, a_m = self.HtrgGAT_12(out_T1, out_S1, m1)
111
+ out_T1, out_S1, m1 = out_T1 + a_T, out_S1 + a_S, m1 + a_m
112
+ out_T2, out_S2, m2 = self.HtrgGAT_21(out_T, out_S, self.master2)
113
+ out_S2, out_T2 = self.pool_hS2(out_S2), self.pool_hT2(out_T2)
114
+ a_T, a_S, a_m = self.HtrgGAT_22(out_T2, out_S2, m2)
115
+ out_T2, out_S2, m2 = out_T2 + a_T, out_S2 + a_S, m2 + a_m
116
+ out_T, out_S, master = torch.max(out_T1, out_T2), torch.max(out_S1, out_S2), torch.max(m1, m2)
117
+ return torch.cat([torch.max(torch.abs(out_T), 1)[0], out_T.mean(1),
118
+ torch.max(torch.abs(out_S), 1)[0], out_S.mean(1), master.squeeze(1)], dim=1)
119
+
120
+
121
+ class MLPBlock(nn.Module):
122
+ def __init__(self, dim):
123
+ super().__init__()
124
+ self.net = nn.Sequential(nn.Linear(dim, dim * 2), nn.GELU(), nn.Dropout(0.1),
125
+ nn.Linear(dim * 2, dim), nn.GELU(), nn.Dropout(0.1))
126
+
127
+ def forward(self, x):
128
+ return self.net(x)
129
+
130
+
131
+ class Classifier(nn.Module):
132
+ def __init__(self, input_dim, hidden_dim, num_layers, num_labels=1):
133
+ super().__init__()
134
+ self.input_proj = nn.Linear(input_dim, hidden_dim) if input_dim != hidden_dim else nn.Identity()
135
+ self.mlp_blocks = nn.ModuleList([MLPBlock(hidden_dim) for _ in range(num_layers)])
136
+ self.layer_norms = nn.ModuleList([nn.LayerNorm(hidden_dim) for _ in range(num_layers)])
137
+ self.output_head = nn.Sequential(nn.Linear(hidden_dim, hidden_dim), nn.GELU(),
138
+ nn.Dropout(0.1), nn.Linear(hidden_dim, num_labels))
139
+
140
+ def forward(self, x):
141
+ x = self.input_proj(x)
142
+ for mlp, ln in zip(self.mlp_blocks, self.layer_norms):
143
+ x = x + mlp(ln(x))
144
+ return self.output_head(x)
145
+
146
+
147
+ class DeepfakeDetector(nn.Module):
148
+ def __init__(self, hidden_dim=320, classifier_layers=3, aasist_gat_dims=(128, 64)):
149
+ super().__init__()
150
+ self.wavlm = WavLMModel.from_pretrained("microsoft/wavlm-large")
151
+ self.pool = AASISTPool(self.wavlm.config.hidden_size, self.wavlm.config.num_hidden_layers, aasist_gat_dims)
152
+ self.classifier = Classifier(self.pool.output_dim, hidden_dim, classifier_layers)
153
+
154
+ @torch.no_grad()
155
+ def forward(self, waveform): # (B, T) 16 kHz -> logit (B,); sigmoid(logit)=P(real)
156
+ hs = list(self.wavlm(waveform, output_hidden_states=True).hidden_states[1:])
157
+ return self.classifier(self.pool(hs)).squeeze(-1)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ torch
2
+ torchaudio
3
+ transformers
4
+ safetensors