一、赛题背景
随着人工智能技术的迅猛发展,深度伪造技术(Deepfake)正成为数字世界中的一把双刃剑。这项技术不仅为创意内容的生成提供了新的可能性,同时也对数字安全构成了前所未有的挑战。Deepfake技术可以通过人工智能算法生成高度逼真的图像、视频和音频内容,这些内容看起来与真实的毫无二致。然而,这也意味着虚假信息、欺诈行为和隐私侵害等问题变得更加严重和复杂。
为了应对这一挑战,我们举办了“外滩大会 - 全球Deepfake攻防挑战赛”。该挑战赛旨在邀请全球的参与者开发、测试和改进更加准确、有效和创新的检测模型,以应对各种类型的Deepfake攻击。这些模型将在真实世界的场景中进行测试,从而推动创新防御策略的发展,提高Deepfake识别的准确性。此次挑战赛不仅是对技术的比拼,更是对全球数字安全的一次重要贡献。我们期待着通过这次比赛,能够激发更多的创新思维和技术突破,共同应对Deepfake带来的安全威胁,保护数字世界的安全与真实性。
二、赛题任务
在这个赛道中,比赛任务是判断一张人脸图像是否为Deepfake图像,并输出其为Deepfake图像的概率评分。参赛者需要开发和优化检测模型,以应对多样化的Deepfake生成技术和复杂的应用场景,从而提升Deepfake图像检测的准确性和鲁棒性。
三、赛题数据集
1、第一阶段
在第一阶段,主办方将发布训练集和验证集。参赛者将使用训练集 (train_label.txt) 来训练模型,而验证集 (val_label.txt) 仅用于模型调优。文件的每一行包含两个部分,分别是图片文件名和标签值(label=1 表示Deepfake图像,label=0 表示真实人脸图像)。例如:
train_label.txt
img_name,target
3381ccbc4df9e7778b720d53a2987014.jpg,1
63fee8a89581307c0b4fd05a48e0ff79.jpg,0
7eb4553a58ab5a05ba59b40725c903fd.jpg,0
…
val_label.txt
img_name,target
cd0e3907b3312f6046b98187fc25f9c7.jpg,1
aa92be19d0adf91a641301cfcce71e8a.jpg,0
5413a0b706d33ed0208e2e4e2cacaa06.jpg,0
…
2、第二阶段
在第一阶段结束后,主办方将发布测试集。在第二阶段,参赛者需要在系统中提交测试集的预测评分文件 (prediction.txt),主办方将在线反馈测试评分结果。文件的每一行包含两个部分,分别是图片文件名和模型预测的Deepfake评分(即样本属于Deepfake图像的概率值)。例如:
prediction.txt
img_name,y_pred
cd0e3907b3312f6046b98187fc25f9c7.jpg,1
aa92be19d0adf91a641301cfcce71e8a.jpg,0.5
5413a0b706d33ed0208e2e4e2cacaa06.jpg,0.5
…
3、第三阶段
在第二阶段结束后,前30名队伍将晋级到第三阶段。在这一阶段,参赛者需要提交代码docker和技术报告。Docker要求包括原始训练代码和测试API(函数输入为图像路径,输出为模型预测的Deepfake评分)。主办方将检查并重新运行算法代码,以重现训练过程和测试结果。
三、评价指标
评估指标
比赛的性能评估主要使用ROC曲线下的AUC(Area under the ROC Curve)作为指标。AUC的取值范围通常在0.5到1之间。若AUC指标不能区分排名,则会使用TPR@FPR=1E-3作为辅助参考。
相关公式:
真阳性率 (TPR):
TPR = TP / (TP + FN)
假阳性率 (FPR):
FPR = FP / (FP + TN)
其中:
- TP:攻击样本被正确识别为攻击;
- TN:真实样本被正确识别为真实;
- FP:真实样本被错误识别为攻击;
- FN:攻击样本被错误识别为真实。
四、baseline解析
# 统计行数
!wc -l /kaggle/input/deepfake/phase1/trainset_label.txt
!wc -l /kaggle/input/deepfake/phase1/valset_label.txt
# 统计训练集中文件总数
!ls /kaggle/input/deepfake/phase1/trainset/ | wc -l
# 统计验证集中文件总数
!ls /kaggle/input/deepfake/phase1/valset/ | wc -l
!pip install timm
from PIL import Image
Image.open('/kaggle/input/deepfake/phase1/trainset/63fee8a89581307c0b4fd05a48e0ff79.jpg')
import torch
: 导入PyTorch库。torch.manual_seed(0)
: 设置PyTorch的随机种子为0,这样可以保证每次运行时生成的随机数是固定的,有助于结果的复现性。torch.backends.cudnn.deterministic = False
: 如果使用了CuDNN(CUDA深度神经网络库),此行代码表示不使用确定性算法,可以提高性能。torch.backends.cudnn.benchmark = True
: 启用CuDNN的自动寻找最适合当前配置的高效算法,以提升性能。
import torch
torch.manual_seed(0)
torch.backends.cudnn.deterministic = False
torch.backends.cudnn.benchmark = True
导入了一些PyTorch和相关库的模块和函数,用于构建和训练深度学习模型。
torchvision.models
:包含了常见的预训练模型,如AlexNet、ResNet等。torchvision.transforms
:包含常见的图像变换操作,如裁剪、旋转、缩放等。torchvision.datasets
:包含常见的数据集,如MNIST、CIFAR-10等。torch.nn
:定义了神经网络层的接口和功能。torch.optim
:包含了优化器,如SGD、Adam等。torch.autograd.Variable
:提供了自动求导机制的变量类型。torch.utils.data.dataset.Dataset
:定义了一个抽象的数据集类,用于自定义数据集。timm
:一个用于图像模型的库,提供了大量现代化的模型架构。time
:Python的时间处理库,通常用于计时或延时操作。
import torchvision.models as models
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data.dataset import Dataset
import timm
import time
导入了一些常见的数据处理和图像处理相关的库。
pandas
:用于数据操作和分析,例如读取CSV文件。numpy
:用于科学计算,支持多维数组和矩阵运算。cv2
:OpenCV库,用于图像处理和计算机视觉任务。PIL.Image
:Python Imaging Library,用于图像处理,如打开、保存、裁剪等。tqdm_notebook
:用于在Jupyter Notebook中显示进度条,便于监控长时间运行的任务进度。
import pandas as pd
import numpy as np
import cv2
from PIL import Image
from tqdm import tqdm_notebook
读取了CSV格式的标签文件,并为每条数据添加了图像路径的列。
pd.read_csv()
:使用pandas库读取CSV文件,将其转换为DataFrame格式。train_label['path']
和val_label['path']
:为每个数据样本添加了图像的完整路径,方便后续读取和处理图像数据。
train_label = pd.read_csv('/kaggle/input/deepfake/phase1/trainset_label.txt')
val_label = pd.read_csv('/kaggle/input/deepfake/phase1/valset_label.txt')
train_label['path'] = '/kaggle/input/deepfake/phase1/trainset/' + train_label['img_name']
val_label['path'] = '/kaggle/input/deepfake/phase1/valset/' + val_label['img_name']
# 统计train_label DataFrame 中 target 列中每个不同取值的频数(即每个类别的样本数量)
train_label['target'].value_counts()
# 对 val_label DataFrame 中 target 列进行统计,获取每个不同类别的样本数量。
val_label['target'].value_counts()
# 一个 pandas DataFrame 的函数调用,用于查看 train_label DataFrame 的前 10 行数据
train_label.head(10)
2、模型训练与验证
class AverageMeter(object):
"""Computes and stores the average and current value"""
def __init__(self, name, fmt=':f'):
self.name = name # 存储指标名称
self.fmt = fmt # 格式化字符串,用于打印输出
self.reset() # 调用 reset 方法初始化对象
def reset(self):
self.val = 0 # 当前值初始化为 0
self.avg = 0 # 平均值初始化为 0
self.sum = 0 # 值的总和初始化为 0
self.count = 0 # 更新次数计数初始化为 0
def update(self, val, n=1):
self.val = val # 更新当前值为给定的 val
self.sum += val * n # 将 val * n 累加到总和 sum 中
self.count += n # 更新计数器 count,增加 n
self.avg = self.sum / self.count # 计算新的平均值
def __str__(self):
fmtstr = '{name} {val' + self.fmt + '} ({avg' + self.fmt + '})'
return fmtstr.format(**self.__dict__)
class ProgressMeter(object):
def __init__(self, num_batches, *meters):
self.batch_fmtstr = self._get_batch_fmtstr(num_batches) # 获取批次格式化字符串
self.meters = meters # 存储所有的指标对象
self.prefix = "" # 前缀,用于输出时添加在格式化字符串前
def pr2int(self, batch):
entries = [self.prefix + self.batch_fmtstr.format(batch)] # 添加批次信息
entries += [str(meter) for meter in self.meters] # 添加每个指标的字符串表示
print('\t'.join(entries)) # 打印输出,以制表符分隔每个条目
def _get_batch_fmtstr(self, num_batches):
num_digits = len(str(num_batches // 1)) # 计算批次数的位数
fmt = '{:' + str(num_digits) + 'd}' # 格式化字符串,用于输出批次信息
return '[' + fmt + '/' + fmt.format(num_batches) + ']' # 返回格式化后的批次信息字符串
def validate(val_loader, model, criterion):
# val_loader 是验证数据集的数据加载器,model 是要评估的模型,criterion 是损失函数
batch_time = AverageMeter('Time', ':6.3f')
losses = AverageMeter('Loss', ':.4e')
top1 = AverageMeter('Acc@1', ':6.2f')
progress = ProgressMeter(len(val_loader), batch_time, losses, top1)
# switch to evaluate mode
# 将模型设为评估模式,这会影响一些层(如批归一化层和 dropout),使其在评估时表现正常。
model.eval()
with torch.no_grad():
end = time.time() # 记录每个批次的开始时间
for i, (input, target) in tqdm_notebook(enumerate(val_loader), total=len(val_loader)): # 创建一个进度条来显示验证过程中的迭代进度
# 将输入数据 input 和目标标签 target 移到 GPU 上进行加速计算
input = input.cuda()
target = target.cuda()
# compute output
# 使用模型 model 对输入 input 进行前向传播,得到输出 output
output = model(input)
# 使用损失函数 criterion 计算模型输出 output 和目标标签 target 的损失值 loss
loss = criterion(output, target)
# measure accuracy and record loss
# 计算模型在当前批次上的准确率
acc = (output.argmax(1).view(-1) == target.float().view(-1)).float().mean() * 100
# 使用 AverageMeter 更新损失和准确率的统计信息
losses.update(loss.item(), input.size(0))
top1.update(acc, input.size(0))
# measure elapsed time
# 使用 AverageMeter 更新每个批次的运行时间
batch_time.update(time.time() - end)
end = time.time()
# TODO: this should also be done with the ProgressMeter
# 打印当前验证集的准确率(平均值)
print(' * Acc@1 {top1.avg:.3f}'
.format(top1=top1))
return top1
# 模型推理预测
def predict(test_loader, model, tta=10):
# switch to evaluate mode
model.eval() # 将模型切换到评估模式
test_pred_tta = None # 初始化,用来存储测试时间增强后的预测结果
for _ in range(tta): # 开始进行测试时间增强(TTA)的循环
test_pred = [] # 用来存储单次测试时间增强的每个样本的预测结果
# 上下文管理器,确保在进行推理时不计算梯度,以节省内存和提高速度
with torch.no_grad():
end = time.time()
for i, (input, target) in tqdm_notebook(enumerate(test_loader), total=len(test_loader)): # 枚举测试数据加载器中的每个批次数据
input = input.cuda()
target = target.cuda()
# compute output
output = model(input) # 使用深度学习模型 model 进行输入数据 input 的预测,得到模型的输出
output = F.softmax(output, dim=1) # 对模型的输出进行 softmax 操作,将其转换为概率分布。dim=1 表示在第一维(通常是类别维度)上进行 softmax 操作
output = output.data.cpu().numpy() # 将输出 output 转换为 NumPy 数组,并将其从 GPU 上移动到 CPU 上
test_pred.append(output) # 将当前批次的预测结果 output 添加到 test_pred 列表中
test_pred = np.vstack(test_pred) # 将 test_pred 列表中的每个数组(每个批次的预测结果)垂直堆叠起来,形成一个大的二维数组,行数等于所有预测结果的总数
if test_pred_tta is None:
test_pred_tta = test_pred
else:
test_pred_tta += test_pred # 累积所有测试时间增强的预测结果
return test_pred_tta
def train(train_loader, model, criterion, optimizer, epoch):
# 创建用于测量训练过程中时间、损失和准确率的平均值计量器
batch_time = AverageMeter('Time', ':6.3f')
losses = AverageMeter('Loss', ':.4e')
top1 = AverageMeter('Acc@1', ':6.2f')
# 创建进度条显示训练进度
progress = ProgressMeter(len(train_loader), batch_time, losses, top1)
# 将模型设置为训练模式
model.train()
# 初始化计时器
end = time.time()
# 遍历训练数据集
for i, (input, target) in enumerate(train_loader):
# 将输入数据和目标标签移动到GPU上(如果可用),并设置为非阻塞操作
input = input.cuda(non_blocking=True)
target = target.cuda(non_blocking=True)
# 计算模型的输出
output = model(input)
# 计算损失值
loss = criterion(output, target)
# 记录损失值
losses.update(loss.item(), input.size(0))
# 计算准确率并记录
acc = (output.argmax(1).view(-1) == target.float().view(-1)).float().mean() * 100
top1.update(acc, input.size(0))
# 梯度清零,进行反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 测量经过的时间
batch_time.update(time.time() - end)
end = time.time()
# 每隔100个batch打印一次训练进度
if i % 100 == 0:
progress.pr2int(i)
class FFDIDataset(Dataset):
def __init__(self, img_path, img_label, transform=None):
self.img_path = img_path
self.img_label = img_label
# 设置数据集的变换操作 transform
if transform is not None:
self.transform = transform
else:
self.transform = None
def __getitem__(self, index):
# 将其转换为 RGB 模式的 PIL.Image 对象
img = Image.open(self.img_path[index]).convert('RGB')
if self.transform is not None:
img = self.transform(img)
# 标签数据首先通过 np.array 转换为 NumPy 数组,然后再通过 torch.from_numpy 转换为 PyTorch 的 Tensor 类型
return img, torch.from_numpy(np.array(self.img_label[index]))
# 返回数据集的长度,即数据集中图像的数量
def __len__(self):
return len(self.img_path)
3、加载模型
import timm
model = timm.create_model('resnet18', pretrained=True, num_classes=2)
model = model.cuda()
train_loader = torch.utils.data.DataLoader(
FFDIDataset(train_label['path'].head(1000), train_label['target'].head(1000),
transforms.Compose([
transforms.Resize((256, 256)), # 将图像大小调整为256x256像素
transforms.RandomHorizontalFlip(), # 随机水平翻转图像
transforms.RandomVerticalFlip(), # 随机垂直翻转图像
transforms.ToTensor(), # 将图像转换为Tensor格式
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
), batch_size=40, shuffle=True, num_workers=4, pin_memory=True
# batch_size=40:每个批次的样本数量为40。
# shuffle=True:每个 epoch 开始时打乱数据,有助于模型学习。
# num_workers=4:使用多进程加载数据,加快数据读取速度。
# pin_memory=True:将数据加载到 CUDA 固定内存中,加快 GPU 加速
)
val_loader = torch.utils.data.DataLoader(
FFDIDataset(val_label['path'].head(1000), val_label['target'].head(1000),
transforms.Compose([
transforms.Resize((256, 256)), # 将图像大小调整为256x256像素
transforms.ToTensor(), # 将图像转换为Tensor格式
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
), batch_size=40, shuffle=False, num_workers=4, pin_memory=True
)
# 定义了损失函数,这里使用交叉熵损失函数 CrossEntropyLoss
criterion = nn.CrossEntropyLoss().cuda()
# 使用 Adam 优化器来优化模型参数,学习率为 0.005
optimizer = torch.optim.Adam(model.parameters(), 0.005)
# 设置了学习率调度器,每 4 个 epoch 学习率乘以 0.85
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=4, gamma=0.85)
best_acc = 0.0
for epoch in range(2):
scheduler.step() # 调用学习率调度器的 step 方法,更新学习率
print('Epoch: ', epoch)
train(train_loader, model, criterion, optimizer, epoch)
val_acc = validate(val_loader, model, criterion)
# 如果当前验证集准确率高于历史最佳准确率,则更新最佳准确率并保存模型参数
if val_acc.avg.item() > best_acc:
best_acc = round(val_acc.avg.item(), 2)
torch.save(model.state_dict(), f'./model_{best_acc}.pt')
# 创建了一个名为 test_loader 的数据加载器对象,用于在训练或评估期间批量加载数据
test_loader = torch.utils.data.DataLoader(
# 将多个图像变换组合在一起的类,这里的变换按顺序作用于每个图像
FFDIDataset(val_label['path'], val_label['target'],
transforms.Compose([
transforms.Resize((256, 256)), # 将输入图像大小调整为 (256, 256) 像素
transforms.ToTensor(), # 将图像数据转换为 PyTorch 张量
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]) # 对图像进行标准化,使用给定的均值和标准差
), batch_size=40, shuffle=False, num_workers=4, pin_memory=True
# batch_size=40:每个批次包含的图像数量为 40。
# shuffle=False:不打乱数据顺序,用于验证数据集通常不需要打乱。
# num_workers=4:用于数据加载的线程数目,这可以加速数据加载过程。
# pin_memory=True:如果设为 True,数据将会被加载到 CUDA 的固定内存区域,这可以提升 GPU 数据加载的速度。
)
# 使用 predict 函数对验证数据集进行预测,并将预测结果存储在 val_label 数据帧的 'y_pred' 列中
# 选择所有行的第二列(索引为 1)作为预测概率
val_label['y_pred'] = predict(test_loader, model, 1)[:, 1]
# 将'img_name' 和 'y_pred' 两列保存到submit.csv文件中,不包括行索引
val_label[['img_name', 'y_pred']].to_csv('submit.csv', index=None)