Datawhale AI 夏令营——CV图像竞赛(Deepfake攻防)——Task1学习笔记

        本次赛题任务:判断一张人脸图像是否为Deepfake图像,并输出其为Deepfake图像的概率评分。参赛者需要开发和优化检测模型,以应对多样化的Deepfake生成技术和复杂的应用场景,从而提升Deepfake图像检测的准确性和鲁棒性。

        本篇博客为学习笔记分享,用来解释 baseline 的个人理解,后续优化在下一篇 Task2 。

  • 训练集 (train_label.txt) 来训练模型
  • 验证集 (val_label.txt) 仅用于模型调优

        这两个文件的每一行包含两个部分,分别是图片文件名和标签值(label=1 表示Deepfake图像,label=0 表示真实人脸图像)

img_name,target
3381ccbc4df9e7778b720d53a2987014.jpg,1
63fee8a89581307c0b4fd05a48e0ff79.jpg,0
7eb4553a58ab5a05ba59b40725c903fd.jpg,0
…
  •  测试集在官网公布,我们用自己的模型评测后得到预测评分文件 (prediction.txt)

        这个文件的每一行包含两个部分,分别是图片文件名和模型预测的Deepfake评分(即样本属于Deepfake图像的概率值)

img_name,y_pred
cd0e3907b3312f6046b98187fc25f9c7.jpg,1
aa92be19d0adf91a641301cfcce71e8a.jpg,0.5
5413a0b706d33ed0208e2e4e2cacaa06.jpg,0.5

        进入项目Deepfake-FFDI-图像赛题 baseline (kaggle.com),一键运行。

        代码中已附详细注释,辅助阅读


一、准备

# 统计行数
!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 

        得到结果:

        可以看到,训练集共有 524429 条,验证集共有 147363 条,总体的数据量并不算很大。

!pip install timm
from PIL import Image
Image.open('/kaggle/input/deepfake/phase1/trainset/63fee8a89581307c0b4fd05a48e0ff79.jpg')

        timm (short for "PyTorch Image Models") 是一个基于 PyTorch 的库,主要用于图像分类任务。它提供了大量的预训练模型以及实用工具,使得在图像处理任务中应用这些模型更加便捷。

        PIL(Python Imaging Library)是一个用于图像处理的库,而Pillow是其派生版本,更加现代化和活跃维护。通过Pillow,可以方便地对图像进行打开、修改、保存等操作。

        open()打开了一张测试集里面的图片,是一张大帅哥,用于测试的,这里我就不再展示。

import torch
torch.manual_seed(0)
torch.backends.cudnn.deterministic = False
torch.backends.cudnn.benchmark = True

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

import pandas as pd
import numpy as np
import cv2
from PIL import Image
from tqdm import tqdm_notebook
  • torch.manual_seed(0):设置随机种子为 0,以确保实验的可重复性。
  • torch.backends.cudnn.deterministic = False:设置 CuDNN 后端的确定性,这样可以获得更高的性能,但结果不一定可重复。
  • torch.backends.cudnn.benchmark = True:启用 CuDNN 的自动优化,提升性能。

        导入一堆库,用于图像处理与数据处理计算等等。

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']
  • 读取训练和验证集的标签文件,文件格式为 CSV(逗号分隔值)
  • 为每个样本生成对应的图片路径,便于后续的数据加载和处理
train_label['target'].value_counts()

        统计 train_label DataFrame 中 target 列中每个不同取值的频数(即每个类别的样本数量)。

val_label['target'].value_counts()

        对 val_label DataFrame 中 target 列进行统计,获取每个不同类别的样本数量。

train_label.head(10)

        查看训练集前 10 项的结构,从左到右的每一列含义为:ID、图片名、类别、路径。

二、模型训练

class AverageMeter(object):
    """计算并存储平均值和当前值"""
    def __init__(self, name, fmt=':f'):
        self.name = name    # 指标名称
        self.fmt = fmt      # 格式化字符串,用于打印输出
        self.reset()        # 初始化对象
 
    def reset(self):
        self.val = 0        # 当前值
        self.avg = 0        # 平均值
        self.sum = 0        # 总和
        self.count = 0      # 更新次数计数
 
    def update(self, val, n=1):
        self.val = val                     # 更新当前值为给定的 val
        self.sum += val * n                # 将 val * n 累加到总和 sum 中
        self.count += n                    # 计数器累加 n
        self.avg = self.sum / self.count   # 重新计算平均值
 
    def __str__(self):
        # 格式化字符串
        fmtstr = '{name} {val' + self.fmt + '} ({avg' + self.fmt + '})'
        return fmtstr.format(**self.__dict__)

        构建 AverageMeter 类,用于计算和存储某个度量的当前值、平均值、总和和计数。

  • init() 用于初始化一个 AverageMeter 实例。
  • reset() 用于重置所有统计量。
  • update() 用于更新统计量。
  • str() 用于返回一个格式化的字符串,显示当前值和平均值。
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) + ']'    # 返回格式化后的批次信息字符串

        构建 ProgressMeter 类,用于显示训练过程中的进度,包括当前批次号和各种度量的值。

  • init() 用于初始化一个 ProgressMeter 实例。
  • pr2int() 用于打印当前批次的进度和各度量的值。
  • get_batch_fmtstr() 返回一个格式化字符串,显示批次号。

        这两个类通常在训练循环中使用,用于跟踪并显示训练进度和度量。

# 参数分别为:训练数据加载器、要训练的模型、损失函数、优化器、训练轮数
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)
  • 初始化用于跟踪时间、损失和准确率的 AverageMeter 实例,并创建 ProgressMeter
  • 将模型切换到训练模式 (model.train())。
  • 遍历训练数据集,计算模型输出、损失和准确率,并更新对应的度量。
  • 执行梯度下降和参数更新。
  • 每 100 个批次打印一次当前进度和度量。

三、模型验证

# 参数:验证数据集的数据加载器、要评估的模型、损失函数
def validate(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)
 
    # 将模型设为评估模式,这会影响一些层(如批归一化层和 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()
 
            # 使用模型 model 对输入 input 进行前向传播,得到输出 output
            output = model(input)    
            # 使用损失函数 criterion 计算模型输出 output 和目标标签 target 的损失值 loss
            loss = criterion(output, target)
 
            # 计算模型在当前批次上的准确率
            acc = (output.argmax(1).view(-1) == target.float().view(-1)).float().mean() * 100
 
            # 更新损失和准确率的统计信息
            losses.update(loss.item(), input.size(0))
            top1.update(acc, input.size(0))
 
            # 更新每个批次的运行时间
            batch_time.update(time.time() - end)
            end = time.time()
 
        # 打印当前验证集的准确率(平均值)
        print(' * Acc@1 {top1.avg:.3f}'
              .format(top1=top1))
        return top1
  • 初始化用于跟踪时间、损失和准确率的 AverageMeter 实例,并创建 ProgressMeter
  • 将模型切换到评估模式 (model.eval())。
  • 遍历验证数据集,计算模型输出、损失和准确率,并更新对应的度量。
  • 打印并返回验证集上的平均准确率。

四、模型预测

# 参数分别为:测试数据加载器、用于预测的模型、测试增加时间
def predict(test_loader, model, tta=10):
    # 将模型切换到评估模式
    model.eval()
    
    # 测试时间增强后的预测结果
    test_pred_tta = None

    # 开始进行测试时间增强(TTA)的循环
    for _ in range(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()
 
                # 使用深度学习模型 model 进行输入数据 input 的预测,得到模型的输出
                output = model(input)
                # 对模型的输出在第一维进行 softmax 操作,将其转换为概率分布
                output = F.softmax(output, dim=1)
                # 转换为 NumPy 数组,并从 GPU 上移动到 CPU 上
                output = output.data.cpu().numpy()
                # 添加当前结果
                test_pred.append(output)
        # 将每个批次的预测结果垂直堆叠,形成一个大的二维数组,行数等于所有预测结果的总数
        test_pred = np.vstack(test_pred)
    
        # 累加预测结果
        if test_pred_tta is None:
            test_pred_tta = test_pred
        else:
            test_pred_tta += test_pred
    
    return test_pred_tta
  • 将模型切换到评估模式 (model.eval())。
  • 进行 TTA 次数的循环,每次遍历测试数据集并进行预测。
  • 将每次预测的结果累加。
  • 返回累加后的预测结果。

        上面 3 个函数一起组成了一个完整的深度学习训练、验证和预测流程。train 函数用于训练模型,validate 函数用于在验证集上评估模型性能,而 predict 函数用于在测试集上进行预测。通过使用 AverageMeterProgressMeter,可以方便地跟踪和显示训练和验证过程中的各种度量。

五、数据读取

class FFDIDataset(Dataset):
    def __init__(self, img_path, img_label, transform=None):
        # 接收参数
        self.img_path = img_path
        self.img_label = img_label
        
        # 图像变换
        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)
        
        # 转换为 NumPy 数组,再转换为 Tensor 类型
        return img, torch.from_numpy(np.array(self.img_label[index]))
    
    # 返回数据集的长度,即数据集中图像的数量
    def __len__(self):
        return len(self.img_path)

        这个 FFDI 数据集类继承自 torch.utils.data.Dataset,用于加载和处理图像数据及其对应的标签。

六、加载模型

import timm
model = timm.create_model('resnet18', pretrained=True, num_classes=2)
model = model.cuda()
  • 使用 timm 库创建一个预训练的 ResNet18 模型,并将输出类别数设置为 2。
  • 将模型移动到 GPU 上以加速训练。
# 训练数据加载器
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
)
  • FFDI 数据集类实例化,传入训练图像路径和标签。
  • 应用了一系列图像变换,包括调整大小、随机水平和垂直翻转、转换为张量以及标准化。
  • 创建一个数据加载器,设置 batch_size 为 40,启用数据打乱,使用 4 个工作线程,并启用 pin_memory 以加快数据传输到 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
)

        与训练数据加载器类似,但不应用随机翻转变换,并且不打乱数据。

# 使用交叉熵损失函数
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()         # 更新学习率
    print('Epoch: ', epoch)  # 打印当前的 epoch
 
    # 调用 train 函数进行训练
    train(train_loader, model, criterion, optimizer, epoch)
    # 调用 validate 函数进行验证,并返回验证集上的准确率
    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 = torch.utils.data.DataLoader(
    FFDIDataset(val_label['path'], val_label['target'], 
            transforms.Compose([
                        transforms.Resize((256, 256)),
                        transforms.ToTensor(),
                        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
)
  • FFDI 数据集类实例化,传入验证图像路径和标签。
  • 应用了一系列图像变换,包括调整大小、转换为张量以及标准化。
  • 创建一个数据加载器,设置 batch_size 为 40,不打乱数据,使用 4 个工作线程,并启用 pin_memory 以加快数据传输到 GPU。
val_label['y_pred'] = predict(test_loader, model, 1)[:, 1]

        使用模型对测试数据进行预测。这里的 tta=1 表示没有使用 TTA,只进行一次预测。获取模型预测的第二个类别的概率。

val_label[['img_name', 'y_pred']].to_csv('submit.csv', index=None)

        将预测结果添加到 val_label 数据框中,选择图像名称和预测结果列,保存到一个名为 submit.csv 的 CSV 文件中,不包含索引。


结果

        训练、验证、测试的流程一共花了大概 20 分钟,最后生成以下的目录。

        提交到官网进行评分。

总结

        这就是一个二分类模型,对给定的图像进行 deepfake 评分。整体逻辑比较清晰,重难点在于模型的改进与优化。

        上面的第六板块中的 RandomHorizontalFlip() 等函数用于数据变换,只在训练集上进行,而验证集不进行,目的就是为了让验证不具有随机性,让验证分数稳定,确保分数变化的来源就是自己修改的部分。

改进方向:

  1. 更多的的数据集增强(旋转、颜色变化、mixup、cutmix)
  2. 更换模型(在满足要求的情况下最大,不能模型混合)

注意点:

  1. 训练集与验证集产生的 deepfake 的逻辑相同,而官方的测试集不一定,所以是竞赛难点
  2. 提前缩放数据集等操作可以加快训练速度
  3. 上面的文字解释均在代码下面,请对照观看

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值