# Datawhale # AI夏令营 # 夏令营
文章目录
- 1. 赛题简要介绍
- 2. 赛题数据集
- 3. 评价指标
- 4. Baseline整体
- 4.1 计算样本数
- 4.2 创建video对象
- 4.3 下载需要的库&&补充知识
- 4.4 设置pytorch随机种子&&CUDNN配置
- 4.5 音视频预处理
- 4.6 创建训练数据文件夹
- 4.7 生成梅尔频谱图
- 4.8 定义AverageMeter和ProgressMeter
- 4.9 深度学习和模型评估流程(重点)
- 4.10 加载训练和验证数据集的标签
- 4.11 加载图像和图像转换
- 4.12 设置训练流程(重点)
- 4.12.1 对resnet18模型的改进思路
- 4.12.2 损失函数的选择改进
- 4.12.3 优化器的选择改进
- 4.13 评估模型
- 4.14 整合最终数据集
- 5. 结语
- 6. 拓展
- 6.1 深度学习的任务定义
- 6.2 AIGC和Deepfake的关系
1. 赛题简要介绍
比赛任务是判断一张人脸图像是否为Deepfake图像,并输出其为Deepfake图像的概率评分。参赛者需要开发和优化检测模型,以应对多样化的Deepfake生成技术和复杂的应用场景,从而提升Deepfake图像检测的准确性和鲁棒性。
2. 赛题数据集
训练集的标签文件train_label.txt用于训练模型,而验证集的标签文件val_label.txt仅用于模型调优。例如,在train_label.txt或val_label.txt中,每行包含两部分,由逗号分隔。第一部分是文件名(后缀为.mp4),第二部分是真实值。
目标值为1表示深度伪造音视频,目标值为0表示真实的人脸音视频。
以下是train_label.txt和val_label.txt的样本:
train_label.txt
video_name,target
96b04c80704f02cb426076b3f624b69e.mp4,0
16fe4cf5ae8b3928c968a5d11e870360.mp4,1
val_label.txt
video_name,target
f859cb3510c69513d5c57c6934bc9968.mp4,0
50ae26b3f3ea85babb2f9dde840830e2.mp4,1
文件中的每一行包含两部分,由逗号分隔。第一部分是视频文件名,第二部分是模型预测对应的深度伪造分数(即样本属于深度伪造视频的概率值)。请参考下面的提交模板:
prediction.csv
video_name,score
658042526e6d0c199adc7bfeb1f7c888.mp4,0.123456
a20cf2d7dea580d0affc4d85c9932479.mp4,0.123456
第二阶段在第一阶段之后,第二阶段发布公共测试集。参与者需要在系统中提交测试集的预测分数文件prediction_test.csv,实时在线反馈测试分数结果。
在第二阶段结束后,前30名队伍将晋级到第三阶段。在这一阶段,参赛者需要提交代码docker和技术报告。Docker要求包括原始训练代码和测试API(函数输入为图像路径,输出为模型预测的Deepfake评分)。主办方将检查并重新运行算法代码,以重现训练过程和测试结果。
仅允许提交单个模型,有效网络参数不得超过200M(使用thop工具统计模型参数)。
仅允许使用ImageNet1K训练预模型。基于发布的训练集生成的扩展样本(通过数据增强/Deepfake工具)可以用于训练,但这些工具需在第三阶段提交以供重现。
3. 评价指标
评价指标主要使用ROC曲线下的AUC作为指标,AUC的取值范围通常在0.5-1之间,否则我们认为这不是一个好的机器学习模型。AUC越接近1,模型就越好。如果AUC呈现了模棱两可的排名结果,我们再使用**TPR(真阳性率)**作为辅助参考。当然,对应的还有FPR这种方法。
F1-Score也是我们可以参考的指标:它是精准率和召回率的调和平均值。
F 1 _ S c o r e = 2 ∗ ( T P ) / ( 2 T P + F N + F P ) F1\_Score = 2*(TP)/(2TP+FN+FP) F1_Score=2∗(TP)/(2TP+FN+FP)
在学习机器学习之前我们应该回顾两个重要的概念:精准率和召回率。
精准率: P r e c i s i o n = T P T P + F P Precision = \frac{TP}{TP+FP} Precision=TP+FPTP,它用于衡量模型的查准性能,正确预测的样本中,预测为正的样本的比例。
召回率: R e c a l l = T P T P + F N Recall = \frac{TP}{TP+FN} Recall=TP+FNTP,它用于衡量模型的查全性能,预测为正的样本中,实际为正的样本的比例。
真阳性率 (TPR):
TPR = TP / (TP + FN)
假阳性率 (FPR):
FPR = FP / (FP + TN)
其中:
TP:攻击样本被正确识别为攻击;
TN:真实样本被正确识别为真实;
FP:真实样本被错误识别为攻击;
FN:攻击样本被错误识别为真实。
参考文献:Aghajan, H., Augusto, J. C., & Delgado, R. L. C. (Eds.). (2009). (题目中给的链接打不开请点击:全书)
这里贴出我的TPR计算脚本:
l1 = [0,1,1,1,0,0,0,1]
l2 = [0,1,0,1,0,1,0,0]
def accuracy(y_true, y_pred):
# 正确预测数初始化一个简单计数器
correct_counter = 0
# 遍历y_true, y_pred中所有元素
# zip函数接受多个元组,返回他们组成的列表
for yt, yp in zip(y_true, y_pred):
if yt == yp:
# 如果预测标签与真实标签相同,则增加计数器
correct_counter += 1
# 返回正确率,正确标签数/总标签数
return correct_counter / len(y_true)
def false_positive(y_true, y_pred):
# 初始化假阳性样本计数器
fp = 0
# 遍历y_true,y_pred中所有元素
for yt, yp in zip(y_true, y_pred):
# 若真实标签为负类但预测标签为正类,计数器增加
if yt == 0 and yp == 1:
fp += 1
return fp
def false_negative(y_true, y_pred):
# 初始化假阴性样本计数器
fn = 0
# 遍历y_true,y_pred中所有元素
for yt, yp in zip(y_true, y_pred):
# 若真实标签为正类但预测标签为负类,计数器增加
if yt == 1 and yp == 0:
fn += 1
return fn
def true_positive(y_true, y_pred):
# 初始化真阳性样本计数器
tp = 0
# 遍历y_true,y_pred中所有元素
for yt, yp in zip(y_true, y_pred):
# 若真实标签为正类且预测标签也为正类,计数器增加
if yt == 1 and yp == 1:
tp += 1
return tp
def true_negative(y_true, y_pred):
# 初始化真阴性样本计数器
tn = 0
# 遍历y_true,y_pred中所有元素
for yt, yp in zip(y_true, y_pred):
# 若真实标签为负类且预测标签也为负类,计数器增加
if yt == 0 and yp == 0:
tn += 1
# 返回真阴性样本数
return tn
# 您可以尝试更好的精确度计算方式
def accuracy_v2(y_true, y_pred):
# 真阳性样本数
tp = true_positive(y_true, y_pred)
# 假阳性样本数
fp = false_positive(y_true, y_pred)
# 假阴性样本数
fn = false_negative(y_true, y_pred)
# 真阴性样本数
tn = true_negative(y_true, y_pred)
# 准确率
accuracy_score = (tp + tn) / (tp + tn + fp + fn)
return accuracy_score
# F1-score的计算方法
def f1(y_true,y_pred):
p = precision(y_true, y_pred)
r = recall(y_true,y_pred)
score = 2*p*r/(p+r)
return score
如果需要分类,您可能需要一个阈值。它与预测值的关系如下:
P r e d i c t i o n = P r o b a b i l i t y > T h r e s h o l d Prediction = Probability > Threshold Prediction=Probability>Threshold
学习 AUC 后,你应该学习的另⼀个重要指标是对数损失。对于二元分类问题,我们将对数损失定义为:
L o g L o s s = − t a r g e t ∗ l o g ( p ) − ( 1 − t a r g e t ) ∗ l o g ( 1 − p ) LogLoss = -target*log(p) - (1-target)*log(1-p) LogLoss=−target∗log(p)−(1−target)∗log(1−p)
其中,目标值为0或1,预测值为样本属于类别1的概率。对数损失会对非常确定和非常错误的预测进⾏惩罚。对数损失越小,模型预测的概率就越接近目标值。
分类问题中我们还可能用到这些指标:
- 宏观平均精确率(Macro-averaged precision):分别计算所有类别的精确率然后求平均值
- 微观平均精确率(Micro-averaged precision):计算所有类别的精确率,然后计算它们的加权平均值。
- 加权精确率(Weighted precision):计算所有类别的精确率,然后计算它们的加权平均值。加权平均值是每个类别的权重的乘积。
4. Baseline整体
4.1 计算样本数
# “word count” 的缩写,是一个用于计数的 Unix 命令。-l 只计算行数
!wc -l /kaggle/input/ffdv-sample-dataset/ffdv_phase1_sample/train_label.txt
!wc -l /kaggle/input/ffdv-sample-dataset/ffdv_phase1_sample/val_label.txt
我们只需要计算行数,这指示了样本数量。
4.2 创建video对象
from IPython.display import Video
Video("/kaggle/input/ffdv-sample-dataset/ffdv_phase1_sample/valset/00882a2832edbcab1d3dfc4cc62cfbb9.mp4", embed=True)
Video创建了video对象,embed则是当设置为 True 时,视频播放器会直接显示在 notebook 单元格的输出中。
在kaggle的baseline上跑完您应该能看见这样的结果:
4.3 下载需要的库&&补充知识
!pip install moviepy librosa matplotlib numpy timm
用到的库的文档链接:
moviepy
librosa(librosa是一个非常强大的python语音信号处理的第三方库,在本baseline中我们主要使用到了MEL频谱图生成,频谱图转换)
matplotlib
numpy
timm(图像分类模型库,快速搭建出各种sota模型)
什么是SOTA?SOTA的全称是State of the arts,它指的是在该领域表现的最好的模型。SOTA在一些benchmark的数据集上跑分非常高。
非端到端模型(pipeline):首先要搞清楚端是啥,两个端分别指的是输入端到输出端。传统的机器学习流程由多个模块组成,这些模块之间是独立的,后一模块的结果依赖于上一结果的水平,影响整个训练结果。
端到端模型(end to end):首先要明白预测是从输入端到输出端产生的,这个预测结果和真实结果相比会得到误差(一定要记住,机器学习的核心任务还是预测),这个误差反向传播到神经网络里的各个层去,调整模型的权重和参数直到模型收敛,或者得到我们预期的结果。如果用控制系统去看待,这是一个闭环控制系统。(e.g.BP神经网络)
序列到序列(seq2seq):这是一种通用的端到端序列预测方法,它的结构是编码器和解码器,如果您使用问题和答案数据集去编码/解码,您可以获得一个问答机器人,这就是序列到序列的应用。
问题回到Baseline本身,Baseline是什么?Baseline通常指的是一个简单且易于实现的基准模型。
在算法调优和调参的过程中,Baseline的任务就是自己和自己比较,使得模型越来越好。
Benchmark也是一个重要的概念,它的含义是基准测试。它通常指的是一种对算法、 模型或方法性能的标准化评估和比较方法,用于衡量模型之间的差异优劣。
您可以在模型跑分网站上高频率地见到他们。比方说司南。
4.4 设置pytorch随机种子&&CUDNN配置
我跑baseline的时候出现了CUDA配置错误的报错,请使用其他Accelerator:
import torch
# 设置pytorch的种子
torch.manual_seed(0)
# deterministic当设置为False时,cuDNN将允许一些操作的非确定性优化
torch.backends.cudnn.deterministic = False
# benchmark设置为true允许cuDNN在每个前向传播中自动寻找最适合当前配置的卷积算法,以提高性能。
torch.backends.cudnn.benchmark = True
# 导入必要的库,我们需要用到cv2,glob,os,PIL
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, glob, os
from PIL import Image
4.5 音视频预处理
generate_mel_spectrogram接受的参数有视频文件路径、划分梅尔频率滤波器个数、最高频率(控制计算的频谱范围)、目标图像大小。
import moviepy.editor as mp
import librosa
import numpy as np
import cv2
def generate_mel_spectrogram(video_path, n_mels=128, fmax=8000, target_size=(256, 256)):
# 提取音频
audio_path = 'extracted_audio.wav'
# video_path 应该是之前定义的变量,包含了要处理的视频文件的路径。创建了一个 VideoFileClip 对象,存储在 video 变量中。
video = mp.VideoFileClip(video_path)
# video.audio 访问视频的音频轨道。write_audiofile() 方法将音频写入文件。verbose=False: 设置为False表示不在控制台输出处理进度。logger=None: 设置为None表示不使用日志记录器。实际上我们做这个预测没有这样的需求,也就不消耗占存。
# 其默认参数:write_audiofile(self, filename, fps=None, nbytes=2, buffersize=2000, codec=None, bitrate=None, ffmpeg_params=None, write_logfile=False, verbose=True, logger='bar')
video.audio.write_audiofile(audio_path, verbose=False, logger=None)
# 加载音频文件,加载采样率
y, sr = librosa.load(audio_path)
# 生成MEL频谱图(梅尔频谱图,与之相对应的有mel倒频谱图)
# 默认参数:librosa.feature.melspectrogram(y=None, sr=22050, S=None, n_fft=2048, hop_length=512, power=2.0, **kwargs)
# 参数解释:y:音频时间序列,sr:采样率,n_mels 是指在计算梅尔频谱图时,将频谱图划分为多少个梅尔频率滤波器(Mel filters),其决定了最终生成的梅尔频谱图的分辨率,也可以理解为梅尔频谱图的高度。
S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=n_mels)
# 将频谱图转换为dB单位,S:输入功率,ref:作为参考,如果是标量,则振幅 abs(S) 相对于 ref: 10 * log10(S / ref) 进行缩放。此处np.max指的是将谱图中的最大值作为参考值,这也是一种常用的参考值取法
S_dB = librosa.power_to_db(S, ref=np.max)
# 归一化到0-255之间,NORM_MINMAX:数组的数值被平移或缩放到一个指定的范围,线性归一化。
S_dB_normalized = cv2.normalize(S_dB, None, 0, 255, cv2.NORM_MINMAX)
# 将浮点数转换为无符号8位整型
S_dB_normalized = S_dB_normalized.astype(np.uint8)
# 缩放到目标大小256,256
img_resized = cv2.resize(S_dB_normalized, target_size, interpolation=cv2.INTER_LINEAR)
return img_resized
# 使用示例
video_path = '/kaggle/input/ffdv-sample-dataset/ffdv_phase1_sample/trainset/001b0680999447348bc9f89efce0f183.mp4' # 替换为您的视频文件路径
mel_spectrogram_image = generate_mel_spectrogram(video_path)
4.6 创建训练数据文件夹
!mkdir ffdv_phase1_sample
!mkdir ffdv_phase1_sample/trainset
!mkdir ffdv_phase1_sample/valset
4.7 生成梅尔频谱图
数据量过于庞大,在此就不贴图了,贴一张一般情况下的梅尔图:
图源:西蒙弗雷泽大学
如果你打开听过,那么他是一段渐降的音频。
# 使用glob.glob函数查找/kaggle/input/ffdv-sample-dataset/ffdv_phase1_sample/trainset/目录下前400个.mp4视频文件的路径。
for video_path in glob.glob('/kaggle/input/ffdv-sample-dataset/ffdv_phase1_sample/trainset/*.mp4')[:400]:
mel_spectrogram_image = generate_mel_spectrogram(video_path)
cv2.imwrite('./ffdv_phase1_sample/trainset/' + video_path.split('/')[-1][:-4] + '.jpg', mel_spectrogram_image)
# a. 调用generate_mel_spectrogram(video_path)函数生成梅尔频谱图,并将其存储在mel_spectrogram_image变量中。b. 使用cv2.imwrite函数将梅尔频谱图保存为JPEG图像。图像被保存在./ffdv_phase1_sample/trainset/目录下,并使用与原始视频文件相同的名称(但扩展名改为.jpg)。
for video_path in glob.glob('/kaggle/input/ffdv-sample-dataset/ffdv_phase1_sample/valset/*.mp4'):
mel_spectrogram_image = generate_mel_spectrogram(video_path)
cv2.imwrite('./ffdv_phase1_sample/valset/' + video_path.split('/')[-1][:-4] + '.jpg', mel_spectrogram_image)
4.8 定义AverageMeter和ProgressMeter
AverageMeter 类用于计算和存储一个变量的平均值和当前值。
- name:变量的名称。
- fmt:格式化字符串,用于格式化输出。
- reset():重置所有统计数据(val、avg、sum、count)。
- update(val, n=1):更新统计数据,val 是当前值,n 是该值的权重(通常是样本数量)。
- str():返回格式化的字符串,包括当前值和平均值。
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()
def reset(self):
self.val = 0
self.avg = 0
self.sum = 0
self.count = 0
def update(self, val, n=1):
self.val = val
self.sum += val * n
self.count += n
self.avg = self.sum / self.count
def __str__(self):
fmtstr = '{name} {val' + self.fmt + '} ({avg' + self.fmt + '})'
return fmtstr.format(**self.__dict__)
ProgressMeter 类用于在训练过程中输出当前的批次信息和统计指标。
- num_batches:总批次数。
- meters:包含 AverageMeter 对象的列表,用于存储不同的指标。
- prefix:输出行的前缀。
- print(batch):打印当前批次的信息,包括当前批次号和每个指标的当前值。
- _get_batch_fmtstr(num_batches):生成批次格式化字符串,确保输出对齐和格式化。
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) + ']'
4.9 深度学习和模型评估流程(重点)
validate 函数在训练过程中定期评估模型在验证集上的表现,计算并打印Top-1准确率。
def validate(val_loader, model, criterion):
batch_time = AverageMeter('Time', ':6.3f')# 批处理时间
losses = AverageMeter('Loss', ':.4e')# 损失
top1 = AverageMeter('Acc@1', ':6.2f')# Top-1准确率
progress = ProgressMeter(len(val_loader), batch_time, losses, top1)# 输出ProgressMeter
# switch to evaluate mode,eval()为评估函数,关闭训练时使用的一些特定层(如 Dropout),并启用 Batch Normalization 层的运行统计。
model.eval()
with torch.no_grad():# 定时设置requires_grad为False,防止梯度计算并节省内存。
end = time.time()
for i, (input, target) in enumerate(val_loader):
input = input.cuda()# 将输入数据和目标数据转移到GPU计算
target = target.cuda()
# compute output
output = model(input)
loss = criterion(output, target)# 计算训练损失
# measure accuracy and record loss,acc百分比显示
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))
# measure elapsed time
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
predict 函数用于在测试集上进行推断,支持使用测试时数据增强(Test Time Augmentation,TTA),通过多次预测并取平均来提升模型预测的稳定性。
def predict(test_loader, model, tta=10):
# switch to evaluate mode
model.eval()
# TTA(Test Time Augmentation)
test_pred_tta = None
for _ in range(tta):# 执行 TTA 次数的循环,每次循环会生成一个略有不同的输入数据。
test_pred = []
with torch.no_grad():
end = time.time()
for i, (input, target) in enumerate(test_loader):
input = input.cuda()
target = target.cuda()
# compute output
output = model(input)
output = F.softmax(output, dim=1)# 对模型输出进行 softmax 归一化处理,以获得类别概率。
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
train 函数负责模型的训练,通过计算损失函数和准确率,并执行反向传播和优化步骤来更新模型参数。
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)
# switch to train mode
model.train()
end = time.time()
for i, (input, target) in enumerate(train_loader):
input = input.cuda(non_blocking=True)
target = target.cuda(non_blocking=True)
# compute output
output = model(input)
loss = criterion(output, target)
# measure accuracy and record loss
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))# 更新 top1 计量器,记录当前批次的准确率。
# compute gradient and do SGD step
optimizer.zero_grad() # 清除之前累积的梯度。
loss.backward()# 计算损失相对于模型参数的梯度
optimizer.step()# 根据 backward() 计算的梯度更新模型参数。
# measure elapsed time
batch_time.update(time.time() - end)# 更新 batch_time 计量器,记录当前批次的处理时间。
end = time.time()
if i % 100 == 0:
progress.pr2int(i)
4.10 加载训练和验证数据集的标签
train_label = pd.read_csv("/kaggle/input/ffdv-sample-dataset/ffdv_phase1_sample/train_label.txt")
val_label = pd.read_csv("/kaggle/input/ffdv-sample-dataset/ffdv_phase1_sample/val_label.txt")
train_label['path'] = '/kaggle/working/ffdv_phase1_sample/trainset/' + train_label['video_name'].apply(lambda x: x[:-4] + '.jpg')
val_label['path'] = '/kaggle/working/ffdv_phase1_sample/valset/' + val_label['video_name'].apply(lambda x: x[:-4] + '.jpg')
train_label = train_label[train_label['path'].apply(os.path.exists)]
val_label = val_label[val_label['path'].apply(os.path.exists)]
4.11 加载图像和图像转换
transform为后续数据增强留个参数,预设为None。
图像转换为RGB模式。
标签以 torch.Tensor 的形式返回。
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):
img = Image.open(self.img_path[index]).convert('RGB')
if self.transform is not None:
img = self.transform(img)
return img, torch.from_numpy(np.array(self.img_label[index]))
def __len__(self):
return len(self.img_path)
4.12 设置训练流程(重点)
引用了上面设置的FFDID类。
train_loader = torch.utils.data.DataLoader(
FFDIDataset(train_label['path'].values, train_label['target'].values,
transforms.Compose([
transforms.Resize((256, 256)),
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
), batch_size=40, shuffle=True, num_workers=12, pin_memory=True
)
val_loader = torch.utils.data.DataLoader(
FFDIDataset(val_label['path'].values, val_label['target'].values,
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=10, pin_memory=True
)
# 重点:这里调用timm提供的resnet18模型,因为分类为0/1(真视频/假视频),可以在后续改进,比如换用更深的网络ResNet-34、ResNet-50或是其他变体
model = timm.create_model('resnet18', pretrained=True, num_classes=2)
model = model.cuda()
# 交叉熵损失,针对多类别
criterion = nn.CrossEntropyLoss().cuda()
# Adam优化器,学习率设置为0.003。
optimizer = torch.optim.Adam(model.parameters(), 0.003)
# 每4个epoch将学习率按0.85的因子进行调整。
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=4, gamma=0.85)
# 初始化最优acc
best_acc = 0.0
for epoch in range(10):
scheduler.step()
print('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')
输出:
Epoch: 0
[ 0/10] Time 6.482 ( 6.482) Loss 7.1626e-01 (7.1626e-01) Acc@1 35.00 ( 35.00)
* Acc@1 64.000
Epoch: 1
[ 0/10] Time 0.819 ( 0.819) Loss 4.6079e-01 (4.6079e-01) Acc@1 80.00 ( 80.00)
* Acc@1 75.500
Epoch: 2
[ 0/10] Time 0.914 ( 0.914) Loss 1.4983e-01 (1.4983e-01) Acc@1 97.50 ( 97.50)
* Acc@1 88.500
Epoch: 3
[ 0/10] Time 0.884 ( 0.884) Loss 2.4681e-01 (2.4681e-01) Acc@1 87.50 ( 87.50)
* Acc@1 84.000
Epoch: 4
[ 0/10] Time 0.854 ( 0.854) Loss 5.3736e-02 (5.3736e-02) Acc@1 100.00 (100.00)
* Acc@1 90.500
Epoch: 5
[ 0/10] Time 0.849 ( 0.849) Loss 5.9881e-02 (5.9881e-02) Acc@1 97.50 ( 97.50)
* Acc@1 89.500
Epoch: 6
[ 0/10] Time 0.715 ( 0.715) Loss 1.6215e-01 (1.6215e-01) Acc@1 92.50 ( 92.50)
* Acc@1 65.000
Epoch: 7
[ 0/10] Time 0.652 ( 0.652) Loss 5.3892e-01 (5.3892e-01) Acc@1 80.00 ( 80.00)
* Acc@1 78.500
Epoch: 8
[ 0/10] Time 0.847 ( 0.847) Loss 6.6098e-02 (6.6098e-02) Acc@1 97.50 ( 97.50)
* Acc@1 81.000
Epoch: 9
[ 0/10] Time 0.844 ( 0.844) Loss 9.4254e-02 (9.4254e-02) Acc@1 97.50 ( 97.50)
* Acc@1 81.500
4.12.1 对resnet18模型的改进思路
更深的网络:如果需要更高的性能和更复杂的特征提取能力,可以考虑使用更深的网络,如ResNet-34、ResNet-50甚至更大的ResNet变体(如ResNet-101或ResNet-152)。
其他预训练模型:除了ResNet系列,还有许多其他预训练模型可供选择,例如:
EfficientNet: 具有优秀的性能和参数效率。
DenseNet: 密集连接的网络结构,有助于更好地利用特征。
VGG系列: 简单而经典的架构,适合在资源受限的情况下使用。
自定义模型:根据具体的数据集特点和任务需求,你也可以考虑设计和训练一个定制的模型架构,这可能需要更多的调试和实验。
集成学习:考虑使用集成学习方法(如Bagging或Boosting)来结合多个模型的预测结果,以进一步提高性能和稳定性。
超参数调整:除了模型选择外,还可以通过调整学习率、批量大小、优化器的选择以及数据增强策略来优化模型性能。
4.12.2 损失函数的选择改进
考虑在后续应用Dice Loss改进损失函数,Dice Loss衡量预测结果与目标掩码之间的相似度,对于边界明显的二分类任务效果较好。它是一个在像素级预测上表现较好的损失函数。
同时关注Focal Loss。专门用于解决类别不平衡问题,它通过减少易分类样本的权重来关注困难样本,可以进一步改善模型在少数类别上的性能。
4.12.3 优化器的选择改进
RAdam是对Adam的改进,通过动态调整学习率的修正来提高稳定性和性能。
AdamW是Adam的一种变体,通过引入权重衰减来解决Adam在一些情况下可能引入的性能问题,特别是在模型参数数量较多时。
AdamW是Adam的一种变体,通过引入权重衰减来解决Adam在一些情况下可能引入的性能问题,特别是在模型参数数量较多时。
4.13 评估模型
# 用模型 (model) 对验证数据集 (val_loader) 进行预测。这部分假设 [:, 1] 给出了类别1的概率。
val_pred = predict(val_loader, model, 1)[:, 1]
# 赋值,预测的概率(或者预测值)赋给了 val_label 数据框中名为 "y_pred" 的列
val_label["y_pred"] = val_pred
4.14 整合最终数据集
submit = pd.read_csv("/kaggle/input/multi-ffdv/prediction.txt.csv")
# 使用 merge 函数将提交文件 (submit) 中的数据与验证数据集标签 (val_label) 中的 video_name 和 y_pred 列合并
merged_df = submit.merge(val_label[['video_name', 'y_pred']], on='video_name', suffixes=('', '_df2'), how='left', )
# 将合并后的数据中 y_pred_df2 列(从验证集中获取的预测结果)的值填充到 y_pred 列中
merged_df['y_pred'] = merged_df['y_pred_df2'].combine_first(merged_df['y_pred'])
merged_df[['video_name', 'y_pred']].to_csv('submit.csv', index=None)
5. 结语
跑完Baseline并不需要10分钟,一般会卡在4.7 生成梅尔频谱图。但需要花5小时进行耐心的摸索。在此对关键流程进行一个默写式的总结:
- 计算样本数
- 创建音视频对象
- 导入训练数据集
- 导入音视频数据
- 设置随机种子/CUDNN
- 音视频预处理
- 生成梅尔频谱图
- 定义验证集、预测集、训练集方法
- 图像转换
- 使用timm提供的SOTA训练、优化-评估模型
6. 拓展
6.1 深度学习的任务定义
深度学习的任务定义其实可以用“反向传播”概括,因为它的核心就在用反向传播算法来调整模型参数,以最小化定义的损失函数。
用深度学习去处理这样的音视频任务非常合适,我认为第一个是音视频数据量庞大,并且需要对这些数据进行复杂的分类。深度学习的机制就决定了它需要庞大的数据量,而且其实很重要的一点就是deepfake本身需要分类思想,本质上也是一个分类任务,而深度学习则在大数据量、精细化分类上非常具有优势,一个熟悉的例子就是对抗生成网络GAN。
第二个,在统计学意义上相似的数据被大量创建,它能够学习到数据量庞大的分布。反过来的任务它也能做。
6.2 AIGC和Deepfake的关系
AIGC应该是包括了Deepfake的。从发展的角度来看,Deepfake技术的发展和维度的上升一定会随着AIGC的处理能力上升,我们会面临浩如烟海的真假数据,当然,也需要看这些视频是作何用处,如果它能够节省故事创作者的精力,那么无疑有利于文艺娱乐的发展;如果它被用在欺骗上,它即将面临伦理道德的挑战。
我的一个思考是,如果结合当今的短剧热潮来看,Deepfake能够使得大众获得超低成本的影音娱乐体验,但是它同时挑战了传统影视业乃至人类的眼睛到底需要什么样的演员和视听刺激的讨论。这是一个值得商榷的话题。