目录
[2.1 损失曲线和top1准确率可视化](#2.1 损失曲线和top1准确率可视化)
[2.2 测试验证集](#2.2 测试验证集)
[2.3 配置文件](#2.3 配置文件)
[为什么要把milestones [4, 8] 修改为[10, 16]](#为什么要把milestones [4, 8] 修改为[10, 16])
[六、现在的参数+正负样本1:2的数据 训练结果:0.8810](#六、现在的参数+正负样本1:2的数据 训练结果:0.8810)
一、前言
在上一篇中我们在自建数据集上训练了TSN,在验证集上准确率是0.83。然后我们通过推理发现推理结果倾向于负样本,怀疑可能是因为正样本:负样本=1:2导致的。
于是这一篇我们首先删除一半的负样本,使得正样本:负样本=1:1,看看训练结果,结果就是在epoch=18的是top1准确率达到了最佳的0.8571,但是运行测试脚本却只有0.8214。这种下降可能是因为我们删除了一半的负样本之后,训练集的图片数本身也减少了,原先训练集:验证集=168:42,现在训练集:验证集=112:28,而且我们运行自己的转数据脚本之后,验证集跟之前也发生了变化,好像很难控制变量。这一点我们不考虑控制变量,而是当作一个新的开始,如果这个新的开始能达到跟之前差不多甚至更高的准确率, 我们将此基础上继续调参,否则回退。
最后我们发现,学习率对准确率的影响是最大的。而且好像同一份数据集很快无论怎么调都很难上升了。
接下来我觉得要考虑换TSM模型。
二、训练结果1:0.8214
操作:删除一半的负样本,使得正样本:负样本=1:1,用上一篇文章第五节转数据的脚本生成训练集和验证集。配置文件configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py按照上一篇文章那样改一下,然后改一下数据名,比如。
data_name = "my_kinetics_data2"
运行下面命令开始训练:
python tools/train.py configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py
Epoch(val) [16][1/1] acc/top1: 0.7857 acc/top5: 1.0000 acc/mean1: 0.7857 data_time: 1.3844 time: 1.7191Epoch(val) [17][1/1] acc/top1: 0.8214 acc/top5: 1.0000 acc/mean1: 0.8214 data_time: 1.4142 time: 1.8037
Epoch(train) [18][28/28] lr: 1.0000e-03 eta: 0:00:27 time: 0.3873 data_time: 0.0009 memory: 1226 grad_norm: 3.7047loss: 0.6442 top1_acc: 0.7500 top5_acc: 1.0000 loss_cls: 0.6442
Epoch(val) [18][1/1] acc/top1: 0.8571 acc/top5: 1.0000 acc/mean1: 0.8571 data_time: 1.3969 time: 1.7826
Epoch(val) [20][1/1] acc/top1: 0.7500 acc/top5: 1.0000 acc/mean1: 0.7500 data_time: 1.3033 time: 1.6286
在epoch=18的时候验证集top1准确率达到了最佳的0.8571,看每个epoch的acc/top1变化,感觉还是比较古怪的。看来我们得先写个代码把损失曲线和top1准确率可视化。
2.1 损失曲线和top1准确率可视化
下面这个地方有损失曲线可视化的代码
https://github.com/open-mmlab/mmaction2/blob/main/tools/analysis_tools/analyze_logs.py
对应我们项目的这个路径:
mmaction2/tools/analysis_tools/analyze_logs.py
使用下面的命令运行代码就能同时绘制损失曲线和Top-1准确率变化曲线。蓝色的部分要替换成你自己训练完之后产生的json,如果你也是训练的tsn的这个模型,主要就是训练时间不同产生的文件夹名和json名不同而已。
此外,因为我们设置的--out是multitask_curve.png,所以输出图像路径就是这个。不过运行这个代码会出问题,随后会给出修改版。
python tools/analysis_tools/analyze_logs.py plot_curve work_dirs\tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb\20251224_092321\vis_data\20251224_092321.json ^
--keys loss_cls top1_acc ^
--legend "loss" "Top-1 accuracy ^
--out multitask_curve.png
# Copyright (c) OpenMMLab. All rights reserved.
import argparse
import json
from collections import defaultdict
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
def cal_train_time(log_dicts, args):
"""计算训练时间统计信息"""
for i, log_dict in enumerate(log_dicts):
print(f'{"-" * 5}Analyze train time of {args.json_logs[i]}{"-" * 5}')
all_times = []
for epoch in log_dict.keys():
# 是否包含每个epoch的第一个iter(通常较慢)
if args.include_outliers:
all_times.append(log_dict[epoch]['time'])
else:
all_times.append(log_dict[epoch]['time'][1:]) # 跳过第一个iter
all_times = np.array(all_times)
epoch_ave_time = all_times.mean(-1) # 每个epoch的平均时间
slowest_epoch = epoch_ave_time.argmax()
fastest_epoch = epoch_ave_time.argmin()
std_over_epoch = epoch_ave_time.std()
print(f'slowest epoch {slowest_epoch + 1}, '
f'average time is {epoch_ave_time[slowest_epoch]:.4f}')
print(f'fastest epoch {fastest_epoch + 1}, '
f'average time is {epoch_ave_time[fastest_epoch]:.4f}')
print(f'time std over epochs is {std_over_epoch:.4f}')
print(f'average iter time: {np.mean(all_times):.4f} s/iter')
print()
def plot_curve(log_dicts, args):
"""绘制训练/验证指标曲线"""
if args.backend is not None:
plt.switch_backend(args.backend)
sns.set_style(args.style) # 设置seaborn绘图风格
# 自动生成图例:格式为 {文件名}_{指标名}
legend = args.legend
if legend is None:
legend = []
for json_log in args.json_logs:
for metric in args.keys:
legend.append(f'{json_log}_{metric}')
assert len(legend) == (len(args.json_logs) * len(args.keys))
metrics = args.keys
num_metrics = len(metrics)
for i, log_dict in enumerate(log_dicts):
# 获取所有epoch编号并排序
epochs = list(log_dict.keys())
epochs.sort() # 建议添加排序,确保epoch顺序正确
for j, metric in enumerate(metrics):
print(f'plot curve of {args.json_logs[i]}, metric is {metric}')
if metric not in log_dict[epochs[0]]:
raise KeyError(
f'{args.json_logs[i]} does not contain metric {metric}')
xs = [] # x轴:iter数
ys = [] # y轴:指标值
for epoch in epochs:
iters = log_dict[epoch]['iter']
# 如果是验证模式,去掉最后一个iter(通常是val的汇总值)
#if log_dict[epoch]['mode'][-1] == 'val':
# iters = iters[:-1]
# [修改] 检查mode列表是否非空
if len(log_dict[epoch]['mode']) > 0 and log_dict[epoch]['mode'][-1] == 'val':
iters = iters[:-1]
# [修改] 额外保护:确保iters非空
if len(iters) == 0:
continue
# num_iters_per_epoch = iters[-1]
# 将epoch和iter转换为总iter数
# xs.append(np.array(iters) + (epoch - 1) * num_iters_per_epoch)
# [修改] 直接使用日志中的iter值,不再加上epoch偏移
# 你的日志中的iter已经是全局iter或相对iter,无需额外计算
xs.append(np.array(iters))
# 提取指标值,确保与iter数量对应
ys.append(np.array(log_dict[epoch][metric][:len(iters)]))
# 合并所有epoch的数据
xs = np.concatenate(xs)
ys = np.concatenate(ys)
plt.xlabel('iter') # x轴标签
plt.plot(xs, ys, label=legend[i * num_metrics + j], linewidth=0.5)
plt.legend()
if args.title is not None:
plt.title(args.title)
# 显示或保存图像
if args.out is None:
plt.show()
else:
print(f'save curve to: {args.out}')
plt.savefig(args.out)
plt.cla() # 清空图像,避免重叠
def add_plot_parser(subparsers):
"""添加plot_curve子命令的参数解析"""
parser_plt = subparsers.add_parser(
'plot_curve', help='parser for plotting curves')
parser_plt.add_argument(
'json_logs',
type=str,
nargs='+',
help='path of train log in json format')
parser_plt.add_argument(
'--keys',
type=str,
nargs='+',
default=['top1_acc'],
help='the metric that you want to plot')
parser_plt.add_argument('--title', type=str, help='title of figure')
parser_plt.add_argument(
'--legend',
type=str,
nargs='+',
default=None,
help='legend of each plot')
parser_plt.add_argument(
'--backend', type=str, default=None, help='backend of plt')
parser_plt.add_argument(
'--style', type=str, default='dark', help='style of plt')
parser_plt.add_argument('--out', type=str, default=None)
def add_time_parser(subparsers):
"""添加cal_train_time子命令的参数解析"""
parser_time = subparsers.add_parser(
'cal_train_time',
help='parser for computing the average time per training iteration')
parser_time.add_argument(
'json_logs',
type=str,
nargs='+',
help='path of train log in json format')
parser_time.add_argument(
'--include-outliers',
action='store_true',
help='include the first value of every epoch when computing '
'the average time')
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='Analyze Json Log')
# currently only support plot curve and calculate average train time
subparsers = parser.add_subparsers(dest='task', help='task parser')
add_plot_parser(subparsers)
add_time_parser(subparsers)
args = parser.parse_args()
return args
def load_json_logs(json_logs):
"""
加载JSON日志文件并转换为字典结构
外层key是epoch,内层key是各种指标(如loss_cls, top1_acc)
内层value是对应指标在所有iter上的值列表
"""
log_dicts = [dict() for _ in json_logs]
for json_log, log_dict in zip(json_logs, log_dicts):
with open(json_log, 'r') as log_file:
for line in log_file:
log = json.loads(line.strip())
# 跳过没有epoch字段的行(如配置信息)
if 'epoch' not in log:
continue
epoch = log.pop('epoch')
if epoch not in log_dict:
log_dict[epoch] = defaultdict(list)
for k, v in log.items():
log_dict[epoch][k].append(v)
return log_dicts
def main():
args = parse_args()
json_logs = args.json_logs
for json_log in json_logs:
assert json_log.endswith('.json')
log_dicts = load_json_logs(json_logs)
eval(args.task)(log_dicts, args) # 动态调用plot_curve或cal_train_time
if __name__ == '__main__':
main()
运行代码报了下面的错误
if log_dict[epoch]['mode'][-1] == 'val':
IndexError: list index out of range
而且迭代次数也是怪怪的的。上面[修改]的地方就是为了避免上面这个报错的。但还是感觉上面的代码不太好看。
所以改了一下上面的代码的plot_curve函数。下面是修改版,修改的点主要是把损失、训练的top1准确率、验证的top1准确率都画出来,虽然横轴是iter,但我们还加了竖直线来区分不同的epoch。可以运行下面的命令
python tools/analysis/analyze_logs.py plot_curve work_dirs\tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb\20251224_092321\vis_data\20251224_092321.json ^
--keys loss_cls top1_acc ^
--show_epoch ^
--legend "Loss" "Train-Top1" "Val-Top1" ^
--out iter_with_val_ 20251224_092321.png
# Copyright (c) OpenMMLab. All rights reserved.
import argparse
import json
from collections import defaultdict
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
def cal_train_time(log_dicts, args):
"""计算训练时间统计信息"""
for i, log_dict in enumerate(log_dicts):
print(f'{"-" * 5}Analyze train time of {args.json_logs[i]}{"-" * 5}')
all_times = []
for epoch in log_dict.keys():
# 是否包含每个epoch的第一个iter(通常较慢)
if args.include_outliers:
all_times.append(log_dict[epoch]['time'])
else:
all_times.append(log_dict[epoch]['time'][1:]) # 跳过第一个iter
all_times = np.array(all_times)
epoch_ave_time = all_times.mean(-1) # 每个epoch的平均时间
slowest_epoch = epoch_ave_time.argmax()
fastest_epoch = epoch_ave_time.argmin()
std_over_epoch = epoch_ave_time.std()
print(f'slowest epoch {slowest_epoch + 1}, '
f'average time is {epoch_ave_time[slowest_epoch]:.4f}')
print(f'fastest epoch {fastest_epoch + 1}, '
f'average time is {epoch_ave_time[fastest_epoch]:.4f}')
print(f'time std over epochs is {std_over_epoch:.4f}')
print(f'average iter time: {np.mean(all_times):.4f} s/iter')
print()
def plot_curve(log_dicts, args):
"""横轴=iter,同图绘制训练/验证曲线,并标注epoch边界"""
import matplotlib.pyplot as plt
from collections import defaultdict
import numpy as np
import seaborn as sns
from matplotlib.ticker import MaxNLocator
if args.backend is not None:
plt.switch_backend(args.backend)
sns.set_style(args.style)
# 图例处理
legend = args.legend
if legend is None:
legend = []
for json_log in args.json_logs:
for metric in args.keys:
legend.append(f'{json_log}_train_{metric}')
if 'top1_acc' in args.keys: # 若有准确率,添加验证图例
legend.append(f'{args.json_logs[0]}_val_top1')
metrics = args.keys
num_metrics = len(metrics)
# ---------- 遍历每个日志文件 ----------
for i, json_log in enumerate(args.json_logs):
# 数据收集
train_iter = defaultdict(list) # iter -> [metric_values]
val_points = {} # epoch -> (iter, val_value)
epoch_last_iter = {} # epoch -> last_train_iter
last_iter = 0 # 追踪最新训练iter
# 解析日志
with open(json_log, 'r') as f:
for line in f:
log = json.loads(line.strip())
# 1. 训练段:有 epoch 和 iter
if 'epoch' in log and 'iter' in log:
epoch = int(log['epoch'])
it = int(log['iter'])
last_iter = it
epoch_last_iter[epoch] = it
for j, metric in enumerate(metrics):
if metric in log:
train_iter[it].append(log[metric])
# 2. 验证段:无 epoch,用 step 当 epoch 号
elif 'acc/top1' in log and 'step' in log:
epoch = int(log['step'])
# 验证点对齐到该epoch最后一个训练iter + 0.5(避免重叠)
val_it = epoch_last_iter.get(epoch, last_iter) + 0.5
val_points[epoch] = (val_it, log['acc/top1'])
# ---------- 3. 画训练曲线 ----------
for j, metric in enumerate(metrics):
xs_train = sorted(train_iter.keys())
ys_train = [np.mean(train_iter[it][j]) for it in xs_train]
plt.plot(xs_train, ys_train, '.-',
label=f'{json_log}_train_{metric}',
linewidth=0.5, markersize=3)
# ---------- 4. 画验证曲线 ----------
if val_points:
xs_val = [p[0] for p in val_points.values()]
ys_val = [p[1] for p in val_points.values()]
plt.plot(xs_val, ys_val, 's--',
label=f'{json_log}_val_top1',
linewidth=1, markersize=5)
# ---------- 5. 画epoch竖线 ----------
if hasattr(args, 'show_epoch') and args.show_epoch and epoch_last_iter:
for ep, last_it in sorted(epoch_last_iter.items()):
plt.axvline(x=last_it, color='gray', linestyle='--',
linewidth=0.8, alpha=0.7)
plt.text(last_it, plt.ylim()[1] * 0.95, f'E{ep}',
rotation=90, fontsize=7, va='top')
# ---------- 6. 收尾 ----------
plt.xlabel('iter')
plt.legend()
if args.title:
plt.title(args.title)
if args.out:
plt.savefig(args.out, dpi=300, bbox_inches='tight')
print(f'save curve to: {args.out}')
plt.cla()
else:
plt.show()
def add_plot_parser(subparsers):
"""添加plot_curve子命令的参数解析"""
parser_plt = subparsers.add_parser(
'plot_curve', help='parser for plotting curves')
parser_plt.add_argument(
'json_logs',
type=str,
nargs='+',
help='path of train log in json format')
parser_plt.add_argument(
'--keys',
type=str,
nargs='+',
default=['top1_acc'],
help='the metric that you want to plot')
parser_plt.add_argument('--title', type=str, help='title of figure')
parser_plt.add_argument(
'--legend',
type=str,
nargs='+',
default=None,
help='legend of each plot')
parser_plt.add_argument(
'--backend', type=str, default=None, help='backend of plt')
parser_plt.add_argument(
'--style', type=str, default='dark', help='style of plt')
parser_plt.add_argument('--out', type=str, default=None)
parser_plt.add_argument(
'--show_epoch',
action='store_true',
help='draw vertical lines at epoch boundaries'
)
def add_time_parser(subparsers):
"""添加cal_train_time子命令的参数解析"""
parser_time = subparsers.add_parser(
'cal_train_time',
help='parser for computing the average time per training iteration')
parser_time.add_argument(
'json_logs',
type=str,
nargs='+',
help='path of train log in json format')
parser_time.add_argument(
'--include-outliers',
action='store_true',
help='include the first value of every epoch when computing '
'the average time')
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='Analyze Json Log')
# currently only support plot curve and calculate average train time
subparsers = parser.add_subparsers(dest='task', help='task parser')
add_plot_parser(subparsers)
add_time_parser(subparsers)
args = parser.parse_args()
return args
def load_json_logs(json_logs):
"""
加载JSON日志文件并转换为字典结构
外层key是epoch,内层key是各种指标(如loss_cls, top1_acc)
内层value是对应指标在所有iter上的值列表
"""
log_dicts = [dict() for _ in json_logs]
for json_log, log_dict in zip(json_logs, log_dicts):
with open(json_log, 'r') as log_file:
for line in log_file:
log = json.loads(line.strip())
# 跳过没有epoch字段的行(如配置信息)
if 'epoch' not in log:
continue
epoch = log.pop('epoch')
if epoch not in log_dict:
log_dict[epoch] = defaultdict(list)
for k, v in log.items():
log_dict[epoch][k].append(v)
return log_dicts
def main():
args = parse_args()
json_logs = args.json_logs
for json_log in json_logs:
assert json_log.endswith('.json')
log_dicts = load_json_logs(json_logs)
eval(args.task)(log_dicts, args) # 动态调用plot_curve或cal_train_time
if __name__ == '__main__':
main()
我这里放不了输出图像上来,从我的输出图像上看,损失确实先下降后在0.6徘徊,但是感觉损失停在0.6附近还是太大了。此外,验证集从epoch=14的时候top1准确率逐渐上升,直到epoch=18上升到最高,然后下降;训练集在epoch=11左右top1准确率达到最高,随后又下降,且在震荡。
2.2 测试验证集
你看下你生成的best_acc_top1_epoch_x.pth那个x是什么数字就是代表你在哪个epoch达到最佳,我的是x=18。你的可能有所不同
python tools/test.py configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py work_dirs/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb/best_acc_top1_epoch_18.pth
测试结果
Epoch(test) [28/28] acc/top1: 0.8214 acc/top5: 1.0000 acc/mean1: 0.8214 data_time: 0.3291 time: 0.8983
2.3 配置文件
configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py
_base_ = [
'../../_base_/models/tsn_r50.py', '../../_base_/schedules/sgd_100e.py',
'../../_base_/default_runtime.py'
]
# dataset settings
dataset_type = 'VideoDataset'
data_root = 'data/kinetics400/videos_train'
data_root_val = 'data/kinetics400/videos_val'
ann_file_train = 'data/kinetics400/kinetics400_train_list_videos.txt'
ann_file_val = 'data/kinetics400/kinetics400_val_list_videos.txt'
file_client_args = dict(io_backend='disk')
train_pipeline = [
dict(type='DecordInit', **file_client_args),
dict(type='SampleFrames', clip_len=1, frame_interval=1, num_clips=3),
dict(type='DecordDecode'),
dict(type='Resize', scale=(-1, 256)),
dict(
type='MultiScaleCrop',
input_size=224,
scales=(1, 0.875, 0.75, 0.66),
random_crop=False,
max_wh_scale_gap=1),
dict(type='Resize', scale=(224, 224), keep_ratio=False),
dict(type='Flip', flip_ratio=0.5),
dict(type='FormatShape', input_format='NCHW'),
dict(type='PackActionInputs')
]
val_pipeline = [
dict(type='DecordInit', **file_client_args),
dict(
type='SampleFrames',
clip_len=1,
frame_interval=1,
num_clips=3,
test_mode=True),
dict(type='DecordDecode'),
dict(type='Resize', scale=(-1, 256)),
dict(type='CenterCrop', crop_size=224),
dict(type='FormatShape', input_format='NCHW'),
dict(type='PackActionInputs')
]
test_pipeline = [
dict(type='DecordInit', **file_client_args),
dict(
type='SampleFrames',
clip_len=1,
frame_interval=1,
num_clips=25,
test_mode=True),
dict(type='DecordDecode'),
dict(type='Resize', scale=(-1, 256)),
dict(type='TenCrop', crop_size=224),
dict(type='FormatShape', input_format='NCHW'),
dict(type='PackActionInputs')
]
train_dataloader = dict(
batch_size=32,
num_workers=8,
persistent_workers=True,
sampler=dict(type='DefaultSampler', shuffle=True),
dataset=dict(
type=dataset_type,
ann_file=ann_file_train,
data_prefix=dict(video=data_root),
pipeline=train_pipeline))
val_dataloader = dict(
batch_size=32,
num_workers=8,
persistent_workers=True,
sampler=dict(type='DefaultSampler', shuffle=False),
dataset=dict(
type=dataset_type,
ann_file=ann_file_val,
data_prefix=dict(video=data_root_val),
pipeline=val_pipeline,
test_mode=True))
test_dataloader = dict(
batch_size=1,
num_workers=8,
persistent_workers=True,
sampler=dict(type='DefaultSampler', shuffle=False),
dataset=dict(
type=dataset_type,
ann_file=ann_file_val,
data_prefix=dict(video=data_root_val),
pipeline=test_pipeline,
test_mode=True))
val_evaluator = dict(type='AccMetric')
test_evaluator = val_evaluator
default_hooks = dict(checkpoint=dict(interval=3, max_keep_ckpts=3))
# Default setting for scaling LR automatically
# - `enable` means enable scaling LR automatically
# or not by default.
# - `base_batch_size` = (8 GPUs) x (32 samples per GPU).
auto_scale_lr = dict(enable=False, base_batch_size=256)
# 设置训练批大小为 4
train_dataloader['batch_size'] = 4
# 每轮都保存权重,并且只保留最新的权重
default_hooks = dict(
checkpoint=dict(type='CheckpointHook', interval=1, max_keep_ckpts=1))
# 将最大 epoch 数设置为 10,并每 1 个 epoch验证模型
train_cfg = dict(type='EpochBasedTrainLoop', max_epochs=10, val_interval=1)
#根据 10 个 epoch调整学习率调度
param_scheduler = [
dict(
type='MultiStepLR',
begin=0,
end=10,
by_epoch=True,
milestones=[4, 8],
gamma=0.1)
]
model = dict(
cls_head=dict(num_classes=2))
load_from = 'checkpoints/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb_20220906-cd10898e.pth'
三、训练结果2:0.9286
我们去下面这个文件把优化器的初始学习率改为0.001
configs/base/schedules/sgd_100e.py
train_cfg = dict(
type='EpochBasedTrainLoop', max_epochs=100, val_begin=1, val_interval=1)
val_cfg = dict(type='ValLoop')
test_cfg = dict(type='TestLoop')
param_scheduler = [
dict(
type='MultiStepLR',
begin=0,
end=100,
by_epoch=True,
milestones=[40, 80],
gamma=0.1)
]
# optim_wrapper = dict(
# optimizer=dict(type='SGD', lr=0.01, momentum=0.9, weight_decay=0.0001),
# clip_grad=dict(max_norm=40, norm_type=2)
# )
optim_wrapper = dict(
optimizer=dict(type='SGD', lr=0.001, momentum=0.9, weight_decay=0.0001),
clip_grad=dict(max_norm=40, norm_type=2)
)
同时按照上一篇的那样修改配置文件configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py,epoch=20,milestones=[8,16]
# 设置训练批大小为 4
train_dataloader['batch_size'] = 4
# 每轮都保存权重,并且只保留最新的权重
default_hooks = dict(
checkpoint=dict(type='CheckpointHook', interval=1, max_keep_ckpts=1))
# 将最大 epoch 数设置为 10,并每 1 个 epoch验证模型
train_cfg = dict(type='EpochBasedTrainLoop', max_epochs=20, val_interval=1)
param_scheduler = [
dict(
type='MultiStepLR',
begin=0,
end=10,
by_epoch=True,
milestones=[8, 16],
gamma=0.1)
]
model = dict(
cls_head=dict(num_classes=2))
load_from = 'checkpoints/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb_20220906-cd10898e.pth'
训练结果:epoch=6达到最佳acc/top1: 0.9286
Epoch(val) [6][1/1] acc/top1: 0.9286 acc/top5: 1.0000 acc/mean1: 0.9286 data_time: 1.3455 time: 1.6224
可视化:
python tools/analysis/analyze_logs.py plot_curve work_dirs\tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb\20251224_140757\vis_data\20251224_140757.json ^
--keys loss_cls top1_acc ^
--show_epoch ^
--legend "Loss" "Train-Top1" "Val-Top1" ^
--out iter_with_val_20251224_140757.png
测试结果:
Epoch(test) [28/28] acc/top1: 0.9286 acc/top5: 1.0000 acc/mean1: 0.9286 data_time: 0.3270 time: 0.8841
初始学习率为什么要降到0.001
把初始学习率从 0.01 调到 0.001 并不是"越小越好",而是让梯度下降的步长与当前 batch 带来的"噪声规模"相匹配。在小 batch 场景下,这一步长必须缩短,否则训练会"跑飞"。核心原因只有三条,记住就能自己推:
梯度是"平均"得到的
官方配置用 32×8=256 帧/迭代,你只有 4 帧/迭代。
同样一段视频,原来 256 帧一起平均,噪声被"抹平";现在 4 帧平均,方差大了 √64=8 倍。
如果还用 0.01,相当于把噪声放大 8 倍的梯度去更新参数,一步就冲出最优点。
线性缩放规则(Linear Scaling Rule)
这是 FAIR 在 2017 年就给出的经验公式:
lr_new = lr_base × (batch_new / batch_base)官方 base_batch=256,base_lr≈0.01;你现在 batch=4
⇒ 0.01 × (4/256) = 0.000156
取 0.001 比理论值稍大,留一点余量让收敛更快,但数量级必须降到千分之一。
预训练权重本身已经"很尖"
ImageNet 预训练模型在 1000 类上收敛,特征空间非常尖锐。小数据集微调时,大学习率会直接"崩"掉那些已训练好的特征;0.001 属于"微调"区间,既能拉开两类边界,又不会把原权重一脚踹翻。
一句话:batch 变小 → 梯度噪声变大 → 学习率必须同比例缩小,否则训练发散或 loss 原地震荡。 0.001 就是按"4/256"这个比例缩放后的安全值。
四、训练结果3:0.9286
随后我们把配置文件中的milestones [4, 8] 修改为[10, 16] 延后学习率衰减点,让模型充分训练。为什么要这么修改呢?如果修改成[8,16]跟修改成[10,16]有什么区别?(因为原来的epoch=10,现在变成20)
为什么要把milestones [4, 8] 修改为[10, 16]
把 milestones 从 [4, 8] 改成 [8, 16] 还是 [10, 16],本质都是在问:
"20 个 epoch 里,我该在第几轮再提醒模型'该放慢脚步了'?"
差别只有 2 个 epoch,但背后有两条量化依据,搞清后自己就能定。
先算"等比例映射"
原配置 100 epoch,milestones=[40, 80](mmaction2 里 tsn 的默认 schedule 就是 40/80)。
现在压缩到 20 epoch,简单按比例:
40/100×20 = 8,80/100×20 = 16 → 理论最公道的是 [8, 16]。
这条线保证"学习率下降的时机占总训练的百分比"与大数据集实验一致。
再考虑"小数据集滞后效应"
数据量只有 112 视频,每轮迭代次数 ≈ 112/4 = 28 步,信息量小。
两类 1:1,分布简单,模型很快就把训练集 acc 拉到 90%+,但验证集指标还在爬坡 。
如果 8 轮就降 lr,步长突然变小,验证集可能还差 2-3 个百分点就"爬不动"了。
把第一次下降点往后挪 2 epoch → 10 轮,给 SGD 多 2×28=56 步在大学习率下继续"冲"宽边界,
通常能把验证集 acc 再抬 1~2 %,且不会明显过拟合(反正总共才 20 轮)。
总结一句话:
8, 16\] 是"数学上的等比例移植";\[10, 16\] 是"小数据实战里的保险微调"。 两者都合理,**如果你希望完全复刻官方时间占比,用 \[8, 16\];如果想再多给模型一点高学习率下的'宽边界'搜索时间,用 \[10, 16\]**。差别就是 2 个 epoch,跑两次实验对比一下验证曲线,选高的即可。
训练结果:epoch=14达到最佳acc/top1: 0.9643
Epoch(val) [14][1/1] acc/top1: 0.9643 acc/top5: 1.0000 acc/mean1: 0.9643 data_time: 1.3429 time: 1.7247
测试结果:
Epoch(test) [28/28] acc/top1: 0.9643 acc/top5: 1.0000 acc/mean1: 0.9643 data_time: 0.3067 time: 0.8821
好像我们把配置文件里面train_dataloader的batch_size改成4之后,没有把val_dataloader的batch_size也改成4,val_dataloader的batch_size要跟训练一致吗?
不是"必须"一致,而是"懒得改"+"省显存"+"减少踩坑" 三件事叠加后的习惯做法。拆开说:
显存 already 够用
训练时能把 4 个视频一次性塞进 GPU,说明这批尺寸没爆显存;
验证阶段模型参数一样、输入分辨率一样,占显存的主要是激活值 ,
训练能装下,验证自然也能装下,于是直接复用 batch_size=4,
省得再算一次"最大能放几批"。
验证集太小,再拆碎没意义
你只有 28 条视频,
batch=4 → 7 次迭代就测完,统计量已经够;
若 batch=1,要跑 28 次,速度反而慢,而且"平均"得到的指标方差更大。
避免两次 batch_norm 统计差异
TSN 里用的是 2D backbone(ResNet50),batch_norm 的 running-mean/var 在 train/eval 两套逻辑里更新。
如果验证 batch 突然变得特别小(比如 1),
当前 batch 的统计量与训练时差异大,running-mean/var 会被拉偏,
导致验证分数抖动。保持同样 batch_size 能让 BN 统计更稳定。
什么时候可以不一致?
验证集很大、GPU 装不下,才把 val_batch_size 调小;
想做"真正在线"的逐样本 inference(batch=1)测 latency,才刻意设 1。
结论:
对小数据集、同机评测的场景,直接照搬训练 batch_size 是最省事且最稳的做法,不是硬性规定。
我们在配置文件的train_dataloader['batch_size'] = 4后面加一句
val_dataloader['batch_size'] = 4
重新训练,我们看下训练结果,居然epoch=2就最优了,0.9286
Epoch(val) [2][7/7] acc/top1: 0.9286 acc/top5: 1.0000 acc/mean1: 0.9286 data_time: 0.0699 time: 0.0969
这么看感觉上次那个0.9643是个巧合呀,再把val_dataloader['batch_size'] = 4删除了,重新训练一次看看训练结果。不是巧合,确实是能冲到0.9643,但是奇怪的是稳定不了0.9643,因为总共是epoch=20,epoch=13之后验证集准确率又掉回0.9286了。
Epoch(val) [13][1/1] acc/top1: 0.9643 acc/top5: 1.0000 acc/mean1: 0.9643 data_time: 1.3503 time: 1.6999
为什么我训练集batch_size=4,验证集batch_size=4训练结果是epoch=2的时候就直接0.9286准确率,而设置训练集batch_size=4,验证集batch_size=32,epoch=13的是是0.9642准确率,之后又降回0.9286?
同一份权重,换一下验证
batch_size就能把"最高准确率"从 0.9286 抬到 0.9642,再跌回去------这不是模型突然变好了,而是"统计样本数"和"BN 统计扰动"在作怪。两件事叠加:
28 条视频太少,"多算对 1~2 条"就能把 acc 拉上去 3.6 %
batch_size=4 时,一次迭代 4 条,7 次迭代把整个验证集跑完;
只要其中有 1 条被错分,acc 就会掉 1/28 ≈ 3.57 %。
batch_size=32 时,一次就喂完 28 条 ,
相当于"把 7 个小 batch 的 logits 一次性平均"再投票,
随机噪声被平均掉,原来被错分的 1~2 条可能就翻案了 ,
于是 28 条里多了 1 条正确,acc 立刻从 25/28=0.9286 → 26/28=0.9642。
模型权重没变,只是投票方式变了。BN 的 running-mean/var 被"大 batch"重新算了一遍
验证阶段 BN 用 running 统计量,但最后一个训练 batch 只有 4 张图 ,
留下的 running-mean/var 并不稳定;
当你第一次用 batch=32 做验证时,PyTorch 会把 28 张图一次性送进网络 ,
BN 层会用这 28 张图的实时统计量 替代 running 统计量(默认行为
model.eval()下track_running_stats=True,但第一次 forward 仍会用当前 batch 统计量更新 running-mean/var )。结果:running 统计量被"大 batch"重新校准,特征分布更准,acc 跳高。
可接下来再测,running 统计量又被小 batch 的训练带偏,acc 又跌回 0.9286。
一句话:0.9642 是"大 batch 平均 + BN 统计临时校准"给出的幻觉;0.9286 才是模型真实水平。
小数据验证集上,一条样本就能让 acc 抖动 3.6 %,别再盯着绝对数值,看趋势和 loss 曲线更靠谱。
以后我们还是训练集和验证集batch_size都设置为4吧
train_dataloader['batch_size'] = 4
val_dataloader['batch_size'] = 4
五、训练结果:0.9286
我们去configs/base/models/tsn_r50.py 把dropout_ratio=0.4改为0.3。
把 0.4 → 0.3 并不是"绝对更好",而是在小数据、小 batch、1:1 类别分布 这三重条件下,把 dropout 稍微拧小一点,让模型"多看几眼"有限的正样本,从而换 1~2 % 的验证集稳态准确率。理由就两条:
数据量太少,drop=0.4 直接抹掉 40 % 特征,正样本瞬间"消失"
你只有 70 个正例,每个 epoch 喂给 cls_head 的有效激活本来就没几次;
再随机扔掉 40 %,网络经常"看不到"关键特征,于是
训练 loss 下不去,验证 acc 在 0.85 左右来回晃;
把 dropout 调到 0.3,相当于每轮多留 10 % 激活,信息量↑,欠拟合缓解 ,
验证 acc 通常能抬 1~2 个百分点,且不会明显过拟合(反正 20 epoch 就停)。
小 batch 本身自带"噪声",dropout 可以轻一点
batch=4 时,SGD 的梯度噪声已经很大,相当于给优化过程加了天然正则 ;
此时再把 dropout 拉到 0.4,正则叠加过头,反而进入"欠拟合区"。
经验规则:batch 越小,dropout 权重越低(0.1~0.3 区间调)。
什么时候保持 0.4?
- 数据量回到千级别以上,或用了大的 batch(≥32),再改回 0.4 甚至 0.5。
结论:
0.3 是"小数据 + 小 batch"场景下的经验折中 ,不是铁律;
你可以 0.3、0.4 各跑一把,哪个验证曲线稳就用哪个,差不到 1 % 就随它去。
训练结果:感觉差不多。
Epoch(val) [3][7/7] acc/top1: 0.9286 acc/top5: 1.0000 acc/mean1: 0.9286 data_time: 0.0613 time: 0.0884
六、现在的参数+正负样本1:2的数据 训练结果:0.8810
我们认为现在正负样本1:1的数据准确率差不多就是到0.9286了,改动的参数主要是:
把训练集和验证集的batch_size从32改为4,
学习率lr从0.1改到0.01,
epoch改为20,milestones改为[10,16]
configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py
# 设置训练批大小为 4
train_dataloader['batch_size'] = 4
val_dataloader['batch_size'] = 4
# 每轮都保存权重,并且只保留最新的权重
default_hooks = dict(
checkpoint=dict(type='CheckpointHook', interval=1, max_keep_ckpts=1))
# 将最大 epoch 数设置为 10,并每 1 个 epoch验证模型
train_cfg = dict(type='EpochBasedTrainLoop', max_epochs=20, val_interval=1)
#根据 10 个 epoch调整学习率调度
param_scheduler = [
dict(
type='MultiStepLR',
begin=0,
end=10,
by_epoch=True,
milestones=[10, 16],
gamma=0.1)
]
model = dict(
cls_head=dict(num_classes=2))
load_from = 'checkpoints/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb_20220906-cd10898e.pth'
configs/base/schedules/sgd_100e.py
optim_wrapper = dict(
optimizer=dict(type='SGD', lr=0.001, momentum=0.9, weight_decay=0.0001),
clip_grad=dict(max_norm=40, norm_type=2)
)
那如果把这套参数用在上一篇我们正负样本1:2的数据会怎么样?
训练结果:
Epoch(val) [6][11/11] acc/top1: 0.8810 acc/top5: 1.0000 acc/mean1: 0.8571 data_time: 0.0529 time: 0.0804
测试结果:
Epoch(test) [42/42] acc/top1: 0.8810 acc/top5: 1.0000 acc/mean1: 0.8571 data_time: 0.2023 time: 0.7386
果然,这个初始学习率改了之后,准确率提升了。至于这个0.8810比不过0.9286,目前不确定是不是正负样本比例的原因,因为没有完全控制变量,有可能是比例的原因。
下面是无效操作,不用看。
我们把新增一个类别权重参数 class_weight=[2.0, 1.0] # 0=负样本权重,1=正样本权重
# 设置训练批大小为 4
train_dataloader['batch_size'] = 4
val_dataloader['batch_size'] = 4
# 每轮都保存权重,并且只保留最新的权重
default_hooks = dict(
checkpoint=dict(type='CheckpointHook', interval=1, max_keep_ckpts=1))
# 将最大 epoch 数设置为 10,并每 1 个 epoch验证模型
train_cfg = dict(type='EpochBasedTrainLoop', max_epochs=20, val_interval=1)
#根据 10 个 epoch调整学习率调度
param_scheduler = [
dict(
type='MultiStepLR',
begin=0,
end=10,
by_epoch=True,
milestones=[10, 16],
gamma=0.1)
]
model = dict(
cls_head=dict(num_classes=2,
dropout_ratio=0.3,
loss_cls=dict(
type='CrossEntropyLoss', # 默认就是 CE,这里显式写出来
class_weight=[2.0, 1.0] # 0=负样本权重,1=正样本权重
)
)
)
load_from = 'checkpoints/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb_20220906-cd10898e.pth'
训练结果:
Epoch(val) [7][11/11] acc/top1: 0.9048 acc/top5: 1.0000 acc/mean1: 0.8571 data_time: 0.0625 time: 0.0897
有点古怪,个别epoch冲到0.9,后面稳定在0.8几。所以感觉设置这个class_weight可能不对。感觉还是正负样本1:1比较靠谱。
我用mmaction2的TSN模型训练时,我设置class_weight为2:1,然后用负正样本2:1的数据,结果是0.90的准确率,设置之后最佳权重是提高了,但收敛的时候还是0.8几,就是看准确率曲线是单个epoch高而已,后面又调回0.8几。如果不设置class_weight,则是最佳都只有0.83的准确率。但是我直接删除一半正样本,使得正负样本1:1,结果是0.92的准确率,为什么?
可能的原因:
1. class_weight 的实现副作用
具体表现:
某些batch恰好全是负样本 → 权重=2 → 梯度放大2倍
某些batch全是正样本 → 权重=1 → 正常梯度
导致优化轨迹震荡,偶尔"撞"到高点0.90,但无法稳定
2. 数据难度分布不均
你的原始数据可能存在:
负样本(2份): 大部分是"简单样本"(明显易分) 正样本(1份): 可能全是"困难样本"(边界模糊)
删除正样本:物理去除了困难样本,任务变简单,模型轻松达到0.92
保留+class_weight:困难样本仍在,权重只是强迫模型"死记硬背",导致过拟合
3. 评估指标的误导性
准确率(Accuracy)在不平衡数据上具有欺骗性:
假设验证集也是2:1:
模型全预测负类:准确率 = 2/3 ≈ 0.667
你的0.90准确率可能主要来自负类正确,正类召回率实际很低
为什么删除样本效果最佳?
因为这创造了一个物理上最干净的学习环境:
梯度一致性:每个batch的类别分布稳定,优化器学习率策略正常工作
无样本冲突:避免了困难正样本与简单负样本的"拉锯战"
评估可靠性:验证集也1:1时,准确率=性能的真实反映
模型容量匹配:你的模型复杂度可能恰好适合"简化后"的任务难度