动作识别9——TSN训练实验

目录

一、前言

二、训练结果1:0.8214

[2.1 损失曲线和top1准确率可视化](#2.1 损失曲线和top1准确率可视化)

[2.2 测试验证集](#2.2 测试验证集)

[2.3 配置文件](#2.3 配置文件)

三、训练结果2:0.9286

初始学习率为什么要降到0.001

四、训练结果3:0.9286

[为什么要把milestones [4, 8] 修改为[10, 16]](#为什么要把milestones [4, 8] 修改为[10, 16])

好像我们把配置文件里面train_dataloader的batch_size改成4之后,没有把val_dataloader的batch_size也改成4,val_dataloader的batch_size要跟训练一致吗?

五、训练结果:0.9286

[六、现在的参数+正负样本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.7191

Epoch(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.7047

loss: 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 场景下,这一步长必须缩短,否则训练会"跑飞"。核心原因只有三条,记住就能自己推:

  1. 梯度是"平均"得到的

    官方配置用 32×8=256 帧/迭代,你只有 4 帧/迭代。

    同样一段视频,原来 256 帧一起平均,噪声被"抹平";现在 4 帧平均,方差大了 √64=8 倍。

    如果还用 0.01,相当于把噪声放大 8 倍的梯度去更新参数,一步就冲出最优点。

  2. 线性缩放规则(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 比理论值稍大,留一点余量让收敛更快,但数量级必须降到千分之一。

  3. 预训练权重本身已经"很尖"

    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,但背后有两条量化依据,搞清后自己就能定。


  1. 先算"等比例映射"

    原配置 100 epoch,milestones=[40, 80](mmaction2 里 tsn 的默认 schedule 就是 40/80)。

    现在压缩到 20 epoch,简单按比例:

    40/100×20 = 8,80/100×20 = 16 → 理论最公道的是 [8, 16]。

    这条线保证"学习率下降的时机占总训练的百分比"与大数据集实验一致。

  2. 再考虑"小数据集滞后效应"

  • 数据量只有 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要跟训练一致吗?

不是"必须"一致,而是"懒得改"+"省显存"+"减少踩坑" 三件事叠加后的习惯做法。拆开说:

  1. 显存 already 够用

    训练时能把 4 个视频一次性塞进 GPU,说明这批尺寸没爆显存;

    验证阶段模型参数一样、输入分辨率一样,占显存的主要是激活值

    训练能装下,验证自然也能装下,于是直接复用 batch_size=4,

    省得再算一次"最大能放几批"。

  2. 验证集太小,再拆碎没意义

    你只有 28 条视频,

    • batch=4 → 7 次迭代就测完,统计量已经够;

    • 若 batch=1,要跑 28 次,速度反而慢,而且"平均"得到的指标方差更大。

  3. 避免两次 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 统计扰动"在作怪。两件事叠加:


  1. 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。
      模型权重没变,只是投票方式变了。

  2. 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 % 的验证集稳态准确率。理由就两条:

  1. 数据量太少,drop=0.4 直接抹掉 40 % 特征,正样本瞬间"消失"

    你只有 70 个正例,每个 epoch 喂给 cls_head 的有效激活本来就没几次;

    再随机扔掉 40 %,网络经常"看不到"关键特征,于是

    • 训练 loss 下不去,验证 acc 在 0.85 左右来回晃;

    • 把 dropout 调到 0.3,相当于每轮多留 10 % 激活,信息量↑,欠拟合缓解

      验证 acc 通常能抬 1~2 个百分点,且不会明显过拟合(反正 20 epoch 就停)。

  2. 小 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准确率可能主要来自负类正确,正类召回率实际很低

为什么删除样本效果最佳?

因为这创造了一个物理上最干净的学习环境

  1. 梯度一致性:每个batch的类别分布稳定,优化器学习率策略正常工作

  2. 无样本冲突:避免了困难正样本与简单负样本的"拉锯战"

  3. 评估可靠性:验证集也1:1时,准确率=性能的真实反映

  4. 模型容量匹配:你的模型复杂度可能恰好适合"简化后"的任务难度

相关推荐
清空mega16 小时前
动手学深度学习——卷积层详解:卷积核是怎么被学出来的?
人工智能·深度学习
沸点小助手16 小时前
「百虾大战 & 晒晒你的Token账单」沸点获奖名单公示|本周互动话题上新🎊
人工智能·ai编程·沸点
cyyt16 小时前
深度学习周报(3.23~3.29)
人工智能·深度学习
badhope16 小时前
10个高星GitHub项目推荐
python·深度学习·计算机视觉·数据挖掘·github
科威舟的代码笔记16 小时前
OpenClaw 权限风险深度剖析与 AI Agent 授权治理的技术思考
人工智能·openclaw
DeepModel16 小时前
【特征选择】嵌入法(Embedded)
人工智能·python·深度学习·算法
云烟成雨TD16 小时前
Spring AI 1.x 系列【14】三月双版本连发!Spring AI 最新功能全掌握
java·人工智能·spring
LaughingZhu16 小时前
Product Hunt 每日热榜 | 2026-03-28
数据库·人工智能·经验分享·神经网络·chatgpt
nimadan1217 小时前
**Minimax写小说软件2025推荐,AI辅助创作提升故事流畅度与情节合理性**
人工智能·python
码农三叔17 小时前
第三卷:《人形机器人的控制与运动规划》
人工智能·机器人·人形机器人