动作识别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. 模型容量匹配:你的模型复杂度可能恰好适合"简化后"的任务难度

相关推荐
ai_top_trends7 分钟前
AI 生成 PPT 工具横评:效率、质量、稳定性一次说清
人工智能·python·powerpoint
三千世界00610 分钟前
Claude Code Agent Skills 自动发现原理详解
人工智能·ai·大模型·agent·claude·原理
云和恩墨13 分钟前
数据库运维的下一步:Bethune X以AI实现从可观测到可处置
人工智能·aiops·数据库监控·数据库运维·数据库巡检
飞睿科技16 分钟前
探讨雷达在智能家居与消费电子领域的应用
人工智能·嵌入式硬件·智能家居·雷达·毫米波雷达
沛沛老爹19 分钟前
Web转AI决策篇 Agent Skills vs MCP:选型决策矩阵与评估标准
java·前端·人工智能·架构·rag·web转型
Baihai_IDP24 分钟前
如何减少单智能体输出结果的不确定性?利用并行智能体的“集体智慧”
人工智能·面试·llm
老蒋每日coding24 分钟前
AI智能体设计模式系列(五)—— 工具使用模式
人工智能·设计模式
抠头专注python环境配置25 分钟前
2026终极诊断指南:解决Windows PyTorch GPU安装失败,从迷茫到确定
人工智能·pytorch·windows·深度学习·gpu·环境配置·cuda
GISer_Jing26 分钟前
Claude Skills
人工智能·prompt·aigc
丝斯201126 分钟前
AI学习笔记整理(49)——大模型应用开发框架:LangChain
人工智能·笔记·学习