Kaggle手势识别第一名代码解析

Kaggle Google - Isolated Sign Language Recognition竞赛链接Kaggle
第一名比赛方案链接Link

Train 笔记

下载轮子

!pip install -q /kaggle/input/tensorflow-2120/tensorflow-2.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
!pip install -q tensorflow-addons==0.20.0
!pip install -q git+https://github.com/hoyso48/tf-utils@main

加载要用到的库

import numpy as np
import pandas as pd
import tensorflow as tf
import tensorflow_addons as tfa 
import matplotlib.pyplot as plt
import matplotlib as mpl
import tensorflow.keras.mixed_precision as mixed_precision

from tqdm.autonotebook import tqdm
import sklearn

from tf_utils.schedules import OneCycleLR, ListedLR
from tf_utils.callbacks import Snapshot, SWA
from tf_utils.learners import FGM, AWP #

import os
import time
import pickle
import math
import random
import sys
import cv2
import gc
import glob
import datetime

print(f'Tensorflow Version: {tf.__version__}') 
print(f'Python Version: {sys.version}')

这段代码主要是用于设置随机数种子和获取分布式策略。其中,seed_everything函数用于设置所有随机数生成器的种子,以确保结果的可重复性。get_strategy函数用于获取分布式策略,根据设备类型选择不同的策略,包括TPU、GPU和CPU。最后返回策略、副本数和是否使用TPU的标志。

# Seed all random number generators
def seed_everything(seed=42):
    os.environ['PYTHONHASHSEED'] = str(seed)
    random.seed(seed)
    np.random.seed(seed)
    tf.random.set_seed(seed)
    
def get_strategy(device='TPU-VM'): 
    if "TPU" in device:
        tpu = 'local' if device=='TPU-VM' else None
        print("connecting to TPU...")
        tpu = tf.distribute.cluster_resolver.TPUClusterResolver.connect(tpu=tpu)
        strategy = tf.distribute.TPUStrategy(tpu)
        IS_TPU = True

    if device == "GPU"  or device=="CPU":
        ngpu = len(tf.config.experimental.list_physical_devices('GPU'))
        if ngpu>1:
            print("Using multi GPU")
            strategy = tf.distribute.MirroredStrategy()
        elif ngpu==1:
            print("Using single GPU")
            strategy = tf.distribute.get_strategy()
        else:
            print("Using CPU")
            strategy = tf.distribute.get_strategy()
            CFG.device = "CPU"

    if device == "GPU": 
        print("Num GPUs Available: ", ngpu) 

    AUTO     = tf.data.experimental.AUTOTUNE 
    REPLICAS = strategy.num_replicas_in_sync 
    print(f'REPLICAS: {REPLICAS}') 
    
    return strategy, REPLICAS, IS_TPU 

STRATEGY, N_REPLICAS, IS_TPU = get_strategy() 
TRAIN_FILENAMES = glob.glob('/kaggle/input/islr-5fold/*.tfrecords')   
print(len(TRAIN_FILENAMES))   
# Train DataFrame
train_df = pd.read_csv('/kaggle/input/asl-signs/train.csv')
display(train_df.head())
display(train_df.info())
import re
def count_data_items(filenames):
    n = [int(re.compile(r"-([0-9]*)\.").search(filename.split('/')[-1]).group(1)) for filename in filenames]
    return np.sum(n)
print(count_data_items(TRAIN_FILENAMES), len(train_df))
assert count_data_items(TRAIN_FILENAMES) == len(train_df)
ROWS_PER_FRAME = 543
MAX_LEN = 384
CROP_LEN = MAX_LEN
NUM_CLASSES  = 250
PAD = -100.
NOSE=[
    1,2,98,327
]
LNOSE = [98]
RNOSE = [327]
LIP = [ 0, 
    61, 185, 40, 39, 37, 267, 269, 270, 409,
    291, 146, 91, 181, 84, 17, 314, 405, 321, 375,
    78, 191, 80, 81, 82, 13, 312, 311, 310, 415,
    95, 88, 178, 87, 14, 317, 402, 318, 324, 308,
]
LLIP = [84,181,91,146,61,185,40,39,37,87,178,88,95,78,191,80,81,82]
RLIP = [314,405,321,375,291,409,270,269,267,317,402,318,324,308,415,310,311,312]

POSE = [500, 502, 504, 501, 503, 505, 512, 513]
LPOSE = [513,505,503,501]
RPOSE = [512,504,502,500]

REYE = [
    33, 7, 163, 144, 145, 153, 154, 155, 133,
    246, 161, 160, 159, 158, 157, 173,
] 
LEYE = [
    263, 249, 390, 373, 374, 380, 381, 382, 362,
    466, 388, 387, 386, 385, 384, 398,
] 

LHAND = np.arange(468, 489).tolist()
RHAND = np.arange(522, 543).tolist()

POINT_LANDMARKS = LIP + LHAND + RHAND + NOSE + REYE + LEYE #+POSE

NUM_NODES = len(POINT_LANDMARKS)
CHANNELS = 6*NUM_NODES 

print(NUM_NODES) 
print(CHANNELS) 

def interp1d_(x, target_len, method='random'):
    '''这是一段Python代码,使用了TensorFlow库。
    函数名为interp1d_,作用是对输入的坐标信息进行插值处理。
    参数x为输入的坐标信息,target_len为目标长度,method为插值方法。
    首先使用tf.shape函数获取坐标信息的长度,然后使用tf.maximum函数确保目标长度不小于1。
    如果method为'random',则使用tf.random.uniform函数生成两个随机数,
    如果第一个随机数小于0.33,则使用双线性插值方法对坐标信息进行插值处理;
    否则,如果第二个随机数小于0.5,则使用三次插值方法对坐标信息进行插值处理;
    否则,使用最近邻插值方法对坐标信息进行插值处理。
    如果method不为'random',则直接使用指定的插值方法对坐标信息进行插值处理。
    最终返回插值处理后的坐标信息。'''
    length = tf.shape(x)[1] 
    target_len = tf.maximum(1,target_len)  
    if method == 'random':  
        if tf.random.uniform(()) < 0.33:  # 用于从均匀分布中输出随机值。
            x = tf.image.resize(x, (target_len,tf.shape(x)[1]),'bilinear')   # 双线性插值
        else: 
            if tf.random.uniform(()) < 0.5: 
                x = tf.image.resize(x, (target_len,tf.shape(x)[1]),'bicubic')   # 三次插值
            else: 
                x = tf.image.resize(x, (target_len,tf.shape(x)[1]),'nearest')  # 最近邻插值
    else: 
        x = tf.image.resize(x, (target_len,tf.shape(x)[1]),method)  
    return x 

'''这两段代码是用于计算张量中的NaN值的平均值和标准差。
第一段代码中,tf.where()函数用于将NaN值替换为0,
然后使用tf.reduce_sum()函数计算元素之和,最后除以非NaN值的数量来计算平均值。 
第二段代码中,如果中心值为None,则使用第一段代码中的函数计算中心值。 
然后计算差值并使用tf_nan_mean()函数计算平均值,最后使用tf.math.sqrt()函数计算标准差。'''

def tf_nan_mean(x, axis=0, keepdims=False):
    #tf.reduce_sum()作用是按一定方式计算张量中元素之和
    return tf.reduce_sum(tf.where(tf.math.is_nan(x), tf.zeros_like(x), x), axis=axis, keepdims=keepdims) / tf.reduce_sum(tf.where(tf.math.is_nan(x), tf.zeros_like(x), tf.ones_like(x)), axis=axis, keepdims=keepdims)

def tf_nan_std(x, center=None, axis=0, keepdims=False): 
    if center is None:  # 如果中心是None
        center = tf_nan_mean(x, axis=axis,  keepdims=True) #返回center
    d = x - center 
    return tf.math.sqrt(tf_nan_mean(d * d, axis=axis, keepdims=keepdims))

'''
这段代码定义了一个名为Preprocess的类,继承自tf.keras.layers.Layer。
该类的作用是对输入进行预处理,包括截取、标准化、差分等操作。
其中,max_len和point_landmarks是该类的两个参数,用于控制截取和差分的长度和位置。
call方法是该类的核心方法,用于实现具体的预处理操作。
具体实现包括对输入进行维度处理、计算均值和标准差、截取、差分、拼接等操作。
最后,使用tf.where函数将NaN值替换为0。
'''

class Preprocess(tf.keras.layers.Layer):
    #当我们需要函数接收带关键字的参数作为输入的时候,应当使用**kwargs
    def __init__(self, max_len=MAX_LEN, point_landmarks=POINT_LANDMARKS, **kwargs):
        super().__init__(**kwargs)
        self.max_len = max_len 
        self.point_landmarks = point_landmarks  

    def call(self, inputs):   
        if tf.rank(inputs) == 3:  #tf.rank返回维度  
            x = inputs[None,...]    
        else: 
            x = inputs
        #tf.gather(该接口的作用:就是抽取出params的第axis维度上在indices里面所有的index
        mean = tf_nan_mean(tf.gather(x, [17], axis=2), axis=[1,2], keepdims=True) 
        mean = tf.where(tf.math.is_nan(mean), tf.constant(0.5,x.dtype), mean)
        x = tf.gather(x, self.point_landmarks, axis=2) #N,T,P,C
        std = tf_nan_std(x, center=mean, axis=[1,2], keepdims=True)
        
        x = (x - mean)/std 

        if self.max_len is not None: 
            x = x[:,:self.max_len] #截取
        length = tf.shape(x)[1] 
        x = x[...,:2]

        dx = tf.cond(tf.shape(x)[1]>1,lambda:tf.pad(x[:,1:] - x[:,:-1], [[0,0],[0,1],[0,0],[0,0]]),lambda:tf.zeros_like(x))

        dx2 = tf.cond(tf.shape(x)[1]>2,lambda:tf.pad(x[:,2:] - x[:,:-2], [[0,0],[0,2],[0,0],[0,0]]),lambda:tf.zeros_like(x))

        x = tf.concat([
            tf.reshape(x, (-1,length,2*len(self.point_landmarks))),
            tf.reshape(dx, (-1,length,2*len(self.point_landmarks))),
            tf.reshape(dx2, (-1,length,2*len(self.point_landmarks))),
        ], axis = -1)
        
        x = tf.where(tf.math.is_nan(x),tf.constant(0.,x.dtype),x)
        
        return x
def decode_tfrec(record_bytes): 
    '''这段代码是用来解析TensorFlow记录文件(TFRecord)中的数据。
    具体来说,它将记录字节解析为一个包含两个特征的字典:一个是名为“coordinates”的字符串特征,另一个是名为“sign”的整数特征。
    然后,它使用TensorFlow的解码函数将“coordinates”特征中的原始字节解码为浮点数,并将其重新形状为一个三维张量。
    最后,它将解码后的特征和整数特征打包成一个字典并返回。'''
    features = tf.io.parse_single_example(record_bytes, {
        'coordinates': tf.io.FixedLenFeature([], tf.string), 
        'sign': tf.io.FixedLenFeature([], tf.int64),
    }) 
    out = {}  
    out['coordinates']  = tf.reshape(tf.io.decode_raw(features['coordinates'], tf.float32), (-1,ROWS_PER_FRAME,3))
    out['sign'] = features['sign'] 
    return out

def filter_nans_tf(x, ref_point=POINT_LANDMARKS):
    
    '''这是一段Python代码,使用了TensorFlow库。函数名为filter_nans_tf,
    作用是过滤掉输入张量x中在参考点ref_point处存在NaN值的样本。
    具体实现是通过tf.gather函数获取x张量在ref_point处的值,
    然后使用tf.math.is_nan函数判断是否为NaN值,
    再使用tf.reduce_all函数判断是否所有样本都存在NaN值,
    最后使用tf.boolean_mask函数过滤掉存在NaN值的样本。'''
    
    #tf.math.logical_not()x和y两个张量在相应位置上做非(!)操作。
    mask = tf.math.logical_not(tf.reduce_all(tf.math.is_nan(tf.gather(x,ref_point,axis=1)), axis=[-2,-1]))
    x = tf.boolean_mask(x, mask, axis=0)
    return x

def preprocess(x, augment=False, max_len=MAX_LEN):
    
    '''这是一段Python代码,使用了TensorFlow库。
    函数名为preprocess,作用是对输入数据进行预处理。
    首先从输入数据x中获取坐标信息,然后使用filter_nans_tf函数过滤掉存在NaN值的样本。
    如果augment参数为True,则使用augment_fn函数对坐标信息进行增强处理。
    接着使用tf.ensure_shape函数确保坐标信息的形状为(None,ROWS_PER_FRAME,3),其中None表示样本数量不确定。
    最后使用Preprocess(max_len=max_len)对坐标信息进行预处理,并将处理后的结果转换为tf.float32类型的张量,
    同时将标签信息转换为one-hot编码的形式。'''
    
    coord = x['coordinates']
    coord = filter_nans_tf(coord)
    if augment:
        coord = augment_fn(coord, max_len=max_len)
    coord = tf.ensure_shape(coord, (None,ROWS_PER_FRAME,3))
    
    return tf.cast(Preprocess(max_len=max_len)(coord)[0],tf.float32), tf.one_hot(x['sign'], NUM_CLASSES)

def flip_lr(x):
    
    '''这段代码是一个函数,用于将输入的张量沿着最后一个维度进行翻转。
    具体实现是先将输入张量按照最后一个维度进行拆分,然后将第一个元素取反,
    再将拆分后的张量重新组合起来,最后按照指定的索引位置进行交换。
    其中,tf.gather用于按照指定的索引位置获取张量中的元素,
    tf.tensor_scatter_nd_update用于按照指定的索引位置更新张量中的元素。'''
    
    x,y,z = tf.unstack(x, axis=-1)
    x = 1-x
    new_x = tf.stack([x,y,z], -1)
    new_x = tf.transpose(new_x, [1,0,2]) #将new_x改为(y,x,z)
    lhand = tf.gather(new_x, LHAND, axis=0)
    rhand = tf.gather(new_x, RHAND, axis=0)
    #tf.tensor_scatter_nd_update(对应位置的索引赋值
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(LHAND)[...,None], rhand)
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(RHAND)[...,None], lhand)
    llip = tf.gather(new_x, LLIP, axis=0)
    rlip = tf.gather(new_x, RLIP, axis=0)
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(LLIP)[...,None], rlip)
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(RLIP)[...,None], llip)
    lpose = tf.gather(new_x, LPOSE, axis=0)
    rpose = tf.gather(new_x, RPOSE, axis=0)
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(LPOSE)[...,None], rpose)
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(RPOSE)[...,None], lpose)
    leye = tf.gather(new_x, LEYE, axis=0)
    reye = tf.gather(new_x, REYE, axis=0)
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(LEYE)[...,None], reye)
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(REYE)[...,None], leye)
    lnose = tf.gather(new_x, LNOSE, axis=0)
    rnose = tf.gather(new_x, RNOSE, axis=0)
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(LNOSE)[...,None], rnose)
    new_x = tf.tensor_scatter_nd_update(new_x, tf.constant(RNOSE)[...,None], lnose)
    new_x = tf.transpose(new_x, [1,0,2]) #将new_x再改为(x,y,z)
    return new_x #左右翻转图像

def resample(x, rate=(0.8,1.2)):
    
    '''这段代码是一个函数,名为resample,它的作用是对输入的x进行重采样。
    重采样的比例在0.8到1.2之间随机生成。
    函数中使用了TensorFlow的函数tf.random.uniform来生成随机数,
    使用了tf.shape获取x的长度,使用了tf.cast进行类型转换,
    使用了interp1d_函数对x进行插值得到新的重采样后的x。最后返回新的x。'''
    
    rate = tf.random.uniform((), rate[0], rate[1])
    length = tf.shape(x)[0]
    new_size = tf.cast(rate*tf.cast(length,tf.float32), tf.int32) #tf.cast()函数用于执行tensorflow中张量数据类型转换
    new_x = interp1d_(x, new_size) #进行插值操作
    return new_x

def spatial_random_affine(xyz,
    scale  = (0.8,1.2),
    shear = (-0.15,0.15),
    shift  = (-0.1,0.1),
    degree = (-30,30),
):
    '''这段代码是一个用于进行空间随机仿射变换的函数。
    其中包含了对输入的三维坐标进行缩放、剪切、旋转和平移等操作。
    具体实现中使用了 TensorFlow 的一些函数和运算符,
    如 tf.constant、tf.random.uniform、tf.identity、@ 运算符和 tf.concat 等。
    其中,通过 @ 运算符可以方便地实现矩阵相乘,
    而 tf.identity 则用于创建一个恒等矩阵。
    此外,代码中还使用了 numpy 库中的 np.pi 常量。'''
    center = tf.constant([0.5,0.5])
    if scale is not None:
        scale = tf.random.uniform((),*scale)
        xyz = scale*xyz

    if shear is not None:
        #Numpy 中 a[::,1] 等价于 a[...,1]
        xy = xyz[...,:2]
        z = xyz[...,2:] 
        shear_x = shear_y = tf.random.uniform((),*shear)  
        if tf.random.uniform(()) < 0.5:  
            shear_x = 0. 
        else: 
            shear_y = 0. 
            #tf.identity在计算图内部创建了两个节点,send / recv节点,用来发送和接受两个变量,如果两个变量在不同的设备上,比如 CPU 和 GPU,那么将会复制变量,如果在一个设备上,将会只是一个引用。
        shear_mat = tf.identity([ 
            [1.,shear_x], 
            [shear_y,1.] 
        ]) 
        xy = xy @ shear_mat #通过@运算符可以方便的实现矩阵相乘,还可以通过tf.matmul(a, b)实现
        center = center + [shear_y, shear_x] 
        xyz = tf.concat([xy,z], axis=-1)

    if degree is not None:
        xy = xyz[...,:2]
        z = xyz[...,2:]
        xy -= center
        degree = tf.random.uniform((),*degree)
        radian = degree/180*np.pi
        c = tf.math.cos(radian)
        s = tf.math.sin(radian)
        rotate_mat = tf.identity([
            [c,s],
            [-s, c],
        ])
        xy = xy @ rotate_mat
        xy = xy + center
        xyz = tf.concat([xy,z], axis=-1)

    if shift is not None:
        shift = tf.random.uniform((),*shift)
        xyz = xyz + shift

    return xyz

def temporal_crop(x, length=MAX_LEN):
    '''这段代码是一个函数,名为temporal_crop,它的作用是对输入的x进行裁剪,使其长度为length。
    如果没有指定length,则默认为MAX_LEN。
    具体实现是先获取x的长度l,然后生成一个随机数offset,范围在[0, l-length]之间,
    最后将x从offset开始裁剪,长度为length。
    裁剪后的x作为函数的返回值。
    这段代码使用了TensorFlow库中的一些函数,如tf.shape、tf.random.uniform和tf.clip_by_value。'''
    l = tf.shape(x)[0]
    offset = tf.random.uniform((), 0, tf.clip_by_value(l-length,1,length), dtype=tf.int32)
    x = x[offset:offset+length]
    return x

def temporal_mask(x, size=(0.2,0.4), mask_value=float('nan')):
    '''这段代码是一个用于生成时间掩码的函数,主要用于在训练神经网络时对输入数据进行随机掩码处理,以增强模型的鲁棒性。
    函数的输入参数x是一个张量,size是一个元组,表示掩码的大小范围,mask_value是掩码的值。
    函数首先获取输入张量的长度l,然后随机生成掩码的大小mask_size,并将其转换为整数类型。
    接着,随机生成掩码的偏移量mask_offset,并使用tf.tensor_scatter_nd_update函数将掩码应用到输入张量的相应位置上。
    最后,函数返回处理后的张量。'''
    l = tf.shape(x)[0]
    mask_size = tf.random.uniform((), *size)
    mask_size = tf.cast(tf.cast(l, tf.float32) * mask_size, tf.int32)
    mask_offset = tf.random.uniform((), 0, tf.clip_by_value(l-mask_size,1,l), dtype=tf.int32)
    x = tf.tensor_scatter_nd_update(x,tf.range(mask_offset, mask_offset+mask_size)[...,None],tf.fill([mask_size,543,3],mask_value))
    return x

def spatial_mask(x, size=(0.2,0.4), mask_value=float('nan')):
    
    '''这段代码是一个用于生成空间掩码的函数。
    它使用了TensorFlow库中的随机数生成函数,生成了一个随机的掩码位置和大小,并将掩码应用于输入张量x的第一和第二维。
    掩码的值为NaN,即将掩码位置的值替换为NaN。
    最后,函数返回处理后的张量x。'''
    
    mask_offset_y = tf.random.uniform(())
    mask_offset_x = tf.random.uniform(())
    mask_size = tf.random.uniform((), *size)
    mask_x = (mask_offset_x<x[...,0]) & (x[...,0] < mask_offset_x + mask_size)
    mask_y = (mask_offset_y<x[...,1]) & (x[...,1] < mask_offset_y + mask_size)
    mask = mask_x & mask_y
    x = tf.where(mask[...,None], mask_value, x)
    return x

def augment_fn(x, always=False, max_len=None):
    
    '''这是一段Python代码,作用是对输入的坐标信息进行增强处理。
    函数名为augment_fn,参数x为输入的坐标信息,always为是否总是进行增强处理的标志,
    max_len为最大长度。首先使用tf.random.uniform函数生成一个随机数,
    如果小于0.8或always为True,则使用resample函数对坐标信息进行重采样。
    接着再次生成一个随机数,如果小于0.5或always为True,则使用flip_lr函数对坐标信息进行左右翻转。
    如果max_len不为None,则使用temporal_crop函数对坐标信息进行时间裁剪。
    再次生成一个随机数,如果小于0.75或always为True,则使用spatial_random_affine函数对坐标信息进行空间仿射变换。
    接着再次生成一个随机数,如果小于0.5或always为True,则使用temporal_mask函数对坐标信息进行时间遮盖。
    最后再次生成一个随机数,如果小于0.5或always为True,则使用spatial_mask函数对坐标信息进行空间遮盖。
    最终返回增强处理后的坐标信息。'''
    
    if tf.random.uniform(())<0.8 or always:
        x = resample(x, (0.5,1.5))
    if tf.random.uniform(())<0.5 or always:
        x = flip_lr(x)
    if max_len is not None:
        x = temporal_crop(x, max_len)
    if tf.random.uniform(())<0.75 or always:
        x = spatial_random_affine(x)
    if tf.random.uniform(())<0.5 or always:
        x = temporal_mask(x)
    if tf.random.uniform(())<0.5 or always:
        x = spatial_mask(x)
    return x

def get_tfrec_dataset(tfrecords, batch_size=64, max_len=64, drop_remainder=False, augment=False, shuffle=False, repeat=False):
    # Initialize dataset with TFRecords
    
    '''这段代码定义了一个函数,用于从TFRecords文件中读取数据集并进行预处理。
    函数的参数包括TFRecords文件路径、批次大小、最大长度、是否丢弃剩余数据、是否进行数据增强、是否打乱数据、是否重复数据等。
    函数首先使用TFRecordDataset初始化数据集,然后使用decode_tfrec函数解码数据,再使用preprocess函数进行预处理。
    如果需要重复数据,则使用repeat函数,如果需要打乱数据,则使用shuffle函数,
    并设置options.experimental_deterministic为False以确保每次打乱的结果不同。
    最后,使用padded_batch函数将数据集分成批次,并使用prefetch函数提前加载数据以提高性能。函数返回处理后的数据集。'''
    
    ds = tf.data.TFRecordDataset(tfrecords, num_parallel_reads=tf.data.AUTOTUNE, compression_type='GZIP')
    ds = ds.map(decode_tfrec, tf.data.AUTOTUNE)
    ds = ds.map(lambda x: preprocess(x, augment=augment, max_len=max_len), tf.data.AUTOTUNE)

    if repeat: 
        ds = ds.repeat()
        
    if shuffle:
        ds = ds.shuffle(shuffle)
        options = tf.data.Options()
        options.experimental_deterministic = (False)
        ds = ds.with_options(options)
    
    if batch_size:
        ds = ds.padded_batch(batch_size, padding_values=PAD, padded_shapes=([max_len,CHANNELS],[NUM_CLASSES]), drop_remainder=drop_remainder)

    ds = ds.prefetch(tf.data.AUTOTUNE)
        
    return ds

ds = get_tfrec_dataset(TRAIN_FILENAMES, augment=True, batch_size=1024)

'''这段代码的作用是从TRAIN_FILENAMES中获取TFRecord数据集,并进行数据增强,每个batch的大小为1024。
然后通过for循环遍历数据集,将第一个batch的数据赋值给temp_train。'''

for x in ds:
    temp_train = x
    break

这段代码主要是用于可视化人体姿态估计的结果。首先导入了IPython.display和matplotlib.animation模块。然后定义了一个函数filter_nans用于过滤掉帧中存在NaN值的部分。接着使用tf.data.TFRecordDataset读取数据集,并使用decode_tfrec函数对数据进行解码。然后通过循环遍历数据集,找到第一个包含左手的帧。接着定义了一个edges列表,用于表示人体骨架的连接关系。最后定义了两个函数plot_frame和animate_frames,用于绘制单个帧和多个帧的动画。其中plot_frame函数用于绘制单个帧,包括人体关键点的坐标和骨架的连接关系;animate_frames函数用于绘制多个帧的动画,使用matplotlib.animation模块中的FuncAnimation函数实现。

from IPython.display import HTML #要在Jupyter Notebook中打印HTML文本并以HTML格式显示,可以使用IPython.display模块中的HTML类
import matplotlib.animation as animation
from matplotlib.animation import FuncAnimation

def filter_nans(frames):
    return frames[~np.isnan(frames).all(axis=(-2,-1))]

ds = tf.data.TFRecordDataset(TRAIN_FILENAMES, num_parallel_reads=tf.data.AUTOTUNE, compression_type='GZIP')
ds = ds.map(decode_tfrec, tf.data.AUTOTUNE)
print(ds)
for x in ds:
    temp = x['coordinates'].numpy()
    if not len(filter_nans(temp[:,LHAND])) == 0:
        break 
    
edges = [(0,1),(1,2),(2,3),(3,4),(0,5),(0,17),(5,6),(6,7),(7,8),(5,9),(9,10),(10,11),(11,12),
         (9,13),(13,14),(14,15),(15,16),(13,17),(17,18),(18,19),(19,20)]

fig, ax = plt.subplots()

def plot_frame(frame, edges=[], idxs=[]):
        
    frame[np.isnan(frame)] = 0
    x = list(frame[...,0])
    y = list(frame[...,1])
    if len(idxs) == 0:
        idxs = list(range(len(x)))
    ax.clear()
    ax.scatter(x, y, color='dodgerblue')
    for i in range(len(x)):
        ax.text(x[i], y[i], idxs[i])
        
    for edge in edges:
        ax.plot([x[edge[0]], x[edge[1]]], [y[edge[0]], y[edge[1]]], color='salmon')
    ax.set_xticks([])
    ax.set_yticks([])
    ax.set_xticklabels([])
    ax.set_yticklabels([])

def animate_frames(frames, edges=[], idxs=[]):
    anim = FuncAnimation(fig, lambda frame: plot_frame(frame, edges, idxs), frames=frames, interval=100)
    return HTML(anim.to_jshtml())
# Animate the frames
animate_frames(filter_nans(temp[:,LHAND]),edges=edges)

在这里插入图片描述

animate_frames(filter_nans(augment_fn(temp,always=True).numpy()[:,RHAND]),edges=edges)

在这里插入图片描述

animate_frames(filter_nans(temp[:,POINT_LANDMARKS]))

在这里插入图片描述

animate_frames(filter_nans(augment_fn(temp,always=True).numpy()[:,POINT_LANDMARKS]), idxs=POINT_LANDMARKS)

在这里插入图片描述

这里给出了三个自定义的Keras层,分别是ECA、LateDropout和CausalDWConv1D。其中ECA是一种基于通道注意力机制的卷积神经网络层,用于增强模型的特征提取能力;LateDropout是一种在训练过程中逐渐增加dropout比例的dropout层,用于防止过拟合;CausalDWConv1D是一种因果卷积层,用于处理时间序列数据。同时,还给出了一个Conv1DBlock函数,用于构建一个高效的卷积块。这些层和函数的具体实现细节可以参考代码块中的注释。

class ECA(tf.keras.layers.Layer):
    def __init__(self, kernel_size=5, **kwargs):
        super().__init__(**kwargs)
        self.supports_masking = True
        self.kernel_size = kernel_size
        #tf.keras.layers.Conv1D(1,1个过滤器
        self.conv = tf.keras.layers.Conv1D(1, kernel_size=kernel_size, strides=1, padding="same", use_bias=False)

    def call(self, inputs, mask=None): 
        nn = tf.keras.layers.GlobalAveragePooling1D()(inputs, mask=mask)  #时间数据的全局平均池化操作
        nn = tf.expand_dims(nn, -1)  
        nn = self.conv(nn) 
        nn = tf.squeeze(nn, -1) 
        nn = tf.nn.sigmoid(nn) 
        nn = nn[:,None,:] #增加维度
        return inputs * nn  

class LateDropout(tf.keras.layers.Layer):   
    def __init__(self, rate, noise_shape=None, start_step=0, **kwargs):  
        super().__init__(**kwargs)  
        self.supports_masking = True 
        self.rate = rate 
        self.start_step = start_ste0p
        self.dropout = tf.keras.layers.Dropout(rate, noise_shape=noise_shape) 
        '''tf.keras.layers.Dropout()參數
    

    rate 在 0 和 1 之間浮點數。要刪除的輸入單位的分數。
    noise_shape 一維整數張量,表示將與輸入相乘的二進製 dropout 掩碼的形狀。例如,如果您的輸入具有 (batch_size, timesteps, features) 形狀,並且您希望所有時間步的 dropout 掩碼都相同,則可以使用 noise_shape=(batch_size, 1, features) 。
    seed 用作隨機種子的 Python 整數。

Dropout 層在訓練期間的每一步以 rate 的頻率將輸入單元隨機設置為 0,這有助於防止過度擬合。未設置為 0 的輸入按 1/(1 - 比率) 放大,以使所有輸入的總和保持不變。

請注意,Dropout 層僅在 training 設置為 True 時適用,這樣在推理期間不會丟棄任何值。使用 model.fit , training 時會自動適當地設置為 True,在其他情況下,您可以在調用層時將 kwarg 顯式設置為 True。

(這與為 Dropout 層設置 trainable=False 形成對比。trainable 不會影響層的行為,因為 Dropout 沒有任何可以在訓練期間凍結的變量/權重。)'''
      
    def build(self, input_shape):
        super().build(input_shape)
        agg = tf.VariableAggregation.ONLY_FIRST_REPLICA
        self._train_counter = tf.Variable(0, dtype="int64", aggregation=agg, trainable=False)

    def call(self, inputs, training=False):
        x = tf.cond(self._train_counter < self.start_step, lambda:inputs, lambda:self.dropout(inputs, training=training))
        if training:
            self._train_counter.assign_add(1)
        return x

class CausalDWConv1D(tf.keras.layers.Layer):
    def __init__(self, 
        kernel_size=17,
        dilation_rate=1,
        use_bias=False,
        depthwise_initializer='glorot_uniform',
        name='', **kwargs):
        super().__init__(name=name,**kwargs)
        self.causal_pad = tf.keras.layers.ZeroPadding1D((dilation_rate*(kernel_size-1),0),name=name + '_pad')
        #一維輸入的零填充層(例如時間序列)
        #深度可分离卷积
        self.dw_conv = tf.keras.layers.DepthwiseConv1D(
                            kernel_size,
                            strides=1,
                            dilation_rate=dilation_rate,
                            padding='valid',
                            use_bias=use_bias,
                            depthwise_initializer=depthwise_initializer,
                            name=name + '_dwconv')
        self.supports_masking = True
        
    def call(self, inputs):
        x = self.causal_pad(inputs)
        x = self.dw_conv(x)
        return x

def Conv1DBlock(channel_size,
          kernel_size,
          dilation_rate=1,
          drop_rate=0.0,
          expand_ratio=2,
          se_ratio=0.25,
          activation='swish',
          name=None):
    '''
    efficient conv1d block, @hoyso48
    '''
    if name is None:
        name = str(tf.keras.backend.get_uid("mbblock"))
    # Expansion phase
    def apply(inputs):
        channels_in = tf.keras.backend.int_shape(inputs)[-1]
        channels_expand = channels_in * expand_ratio

        skip = inputs

        x = tf.keras.layers.Dense(
            channels_expand,
            use_bias=True,
            activation=activation,
            name=name + '_expand_conv')(inputs)

        # Depthwise Convolution
        x = CausalDWConv1D(kernel_size,
            dilation_rate=dilation_rate,
            use_bias=False,
            name=name + '_dwconv')(x)

        x = tf.keras.layers.BatchNormalization(momentum=0.95, name=name + '_bn')(x)

        x  = ECA()(x)

        x = tf.keras.layers.Dense(
            channel_size,
            use_bias=True,
            name=name + '_project_conv')(x)

        if drop_rate > 0:
            x = tf.keras.layers.Dropout(drop_rate, noise_shape=(None,1,1), name=name + '_drop')(x)

        if (channels_in == channel_size):
            x = tf.keras.layers.add([x, skip], name=name + '_add') 
        return x

    return apply

这段代码定义了一个多头自注意力层(MultiHeadSelfAttention)和一个Transformer块(TransformerBlock)。

MultiHeadSelfAttention层的作用是对输入进行多头自注意力计算,其中dim表示每个头的维度,num_heads表示头的数量,dropout表示dropout的比例。在call方法中,首先通过一个全连接层(self.qkv)将输入映射到三个维度为dim的向量q、k、v,然后将这三个向量分别拆分成num_heads个头,进行多头自注意力计算,最后将计算结果通过一个全连接层(self.proj)映射回dim维度。

TransformerBlock块由两个子层组成,分别是多头自注意力层和前馈神经网络层。其中dim表示每个头的维度,num_heads表示头的数量,expand表示前馈神经网络层中间层的扩展倍数,attn_dropout表示多头自注意力层的dropout比例,drop_rate表示前馈神经网络层的dropout比例,activation表示前馈神经网络层的激活函数。在apply方法中,首先对输入进行批量归一化,然后通过多头自注意力层进行计算,再进行dropout和残差连接。接着再进行批量归一化,通过前馈神经网络层进行计算,再进行dropout和残差连接,最后返回计算结果。

class MultiHeadSelfAttention(tf.keras.layers.Layer):
    def __init__(self, dim=256, num_heads=4, dropout=0, **kwargs):
        super().__init__(**kwargs)
        self.dim = dim
        self.scale = self.dim ** -0.5
        self.num_heads = num_heads
        self.qkv = tf.keras.layers.Dense(3 * dim, use_bias=False)
        self.drop1 = tf.keras.layers.Dropout(dropout)
        self.proj = tf.keras.layers.Dense(dim, use_bias=False)
        self.supports_masking = True

    def call(self, inputs, mask=None):
        qkv = self.qkv(inputs)
        qkv = tf.keras.layers.Permute((2, 1, 3))(tf.keras.layers.Reshape((-1, self.num_heads, self.dim * 3 // self.num_heads))(qkv))
        q, k, v = tf.split(qkv, [self.dim // self.num_heads] * 3, axis=-1)

        attn = tf.matmul(q, k, transpose_b=True) * self.scale

        if mask is not None:
            mask = mask[:, None, None, :]

        attn = tf.keras.layers.Softmax(axis=-1)(attn, mask=mask)
        attn = self.drop1(attn)

        x = attn @ v
        x = tf.keras.layers.Reshape((-1, self.dim))(tf.keras.layers.Permute((2, 1, 3))(x))
        x = self.proj(x)
        return x


def TransformerBlock(dim=256, num_heads=4, expand=4, attn_dropout=0.2, drop_rate=0.2, activation='swish'):
    def apply(inputs):
        x = inputs
        x = tf.keras.layers.BatchNormalization(momentum=0.95)(x)
        x = MultiHeadSelfAttention(dim=dim,num_heads=num_heads,dropout=attn_dropout)(x)
        x = tf.keras.layers.Dropout(drop_rate, noise_shape=(None,1,1))(x)
        x = tf.keras.layers.Add()([inputs, x])
        attn_out = x

        x = tf.keras.layers.BatchNormalization(momentum=0.95)(x)
        x = tf.keras.layers.Dense(dim*expand, use_bias=False, activation=activation)(x)
        x = tf.keras.layers.Dense(dim, use_bias=False)(x)
        x = tf.keras.layers.Dropout(drop_rate, noise_shape=(None,1,1))(x)
        x = tf.keras.layers.Add()([attn_out, x])
        return x
    return apply

这段代码定义了一个函数get_model,用于构建一个神经网络模型。该模型包含了多个卷积层和Transformer层,以及一些其他的层,最终输出一个分类结果。具体来说,该模型的输入是一个形状为(max_len,CHANNELS)的张量,其中max_len表示输入序列的最大长度,CHANNELS表示每个时间步的特征维度。该张量首先经过一个Masking层,将输入序列中的PAD值进行mask,然后经过一个Dense层和BatchNormalization层,将输入序列的特征维度转换为dim。接下来,该张量经过多个Conv1DBlock层和TransformerBlock层的堆叠,用于提取输入序列的特征。最后,该张量经过一个全局平均池化层和一个LateDropout层,最终输出一个形状为(NUM_CLASSES,)的张量,表示输入序列的分类结果。在代码的最后,使用该模型对一个样本进行预测,并计算其损失值。

def get_model(max_len=64, dropout_step=0, dim=192):
    inp = tf.keras.Input((max_len,CHANNELS))
    x = tf.keras.layers.Masking(mask_value=PAD,input_shape=(max_len,CHANNELS))(inp)
    ksize = 17
    x = tf.keras.layers.Dense(dim, use_bias=False,name='stem_conv')(x)
    x = tf.keras.layers.BatchNormalization(momentum=0.95,name='stem_bn')(x)

    x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
    x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
    x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
    x = TransformerBlock(dim,expand=2)(x)

    x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
    x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
    x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
    x = TransformerBlock(dim,expand=2)(x)

    if dim == 384: #for the 4x sized model
        x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
        x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
        x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
        x = TransformerBlock(dim,expand=2)(x)

        x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
        x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
        x = Conv1DBlock(dim,ksize,drop_rate=0.2)(x)
        x = TransformerBlock(dim,expand=2)(x)

    x = tf.keras.layers.Dense(dim*2,activation=None,name='top_conv')(x)
    x = tf.keras.layers.GlobalAveragePooling1D()(x)
    x = LateDropout(0.8, start_step=dropout_step)(x)
    x = tf.keras.layers.Dense(NUM_CLASSES,name='classifier')(x)
    return tf.keras.Model(inp, x)

model = get_model()
y = model(temp_train[0])
tf.keras.losses.CategoricalCrossentropy(from_logits=True)(temp_train[1],y)

这段代码是用来检查模型中的每一层是否支持掩码操作。对于每一层,如果不支持掩码操作,则打印出该层的名称和支持掩码操作的状态

#check supports_masking
for x in model.layers:
    if not x.supports_masking:
        print(x.supports_masking, x.name)

这段代码是一个用于训练模型的函数,其中包含了一些数据预处理、模型构建、优化器设置、回调函数等步骤。具体来说,该函数会根据传入的参数进行数据集的划分,然后使用给定的模型构建函数构建模型,并使用给定的优化器和回调函数进行模型训练。训练过程中还会进行一些其他的设置,如设置学习率、权重衰减、梯度累积等。最终,该函数会返回训练好的模型、交叉验证结果和训练历史记录。

def train_fold(CFG, fold, train_files, valid_files=None, strategy=STRATEGY, summary=True):
    seed_everything(CFG.seed)
    tf.keras.backend.clear_session()
    gc.collect()
    tf.config.optimizer.set_jit(True)
        
    if CFG.fp16:
        try:
            policy = mixed_precision.Policy('mixed_bfloat16')
            mixed_precision.set_global_policy(policy)
        except:
            policy = mixed_precision.Policy('mixed_float16')
            mixed_precision.set_global_policy(policy)
    else:
        policy = mixed_precision.Policy('float32')
        mixed_precision.set_global_policy(policy)

    if fold != 'all':
        train_ds = get_tfrec_dataset(train_files, batch_size=CFG.batch_size, max_len=CFG.max_len, drop_remainder=True, augment=True, repeat=True, shuffle=32768)
        valid_ds = get_tfrec_dataset(valid_files, batch_size=CFG.batch_size, max_len=CFG.max_len, drop_remainder=False, repeat=False, shuffle=False)
    else:
        train_ds = get_tfrec_dataset(train_files, batch_size=CFG.batch_size, max_len=CFG.max_len, drop_remainder=False, augment=True, repeat=True, shuffle=32768)
        valid_ds = None
        valid_files = []
    
    num_train = count_data_items(train_files)
    num_valid = count_data_items(valid_files)
    steps_per_epoch = num_train//CFG.batch_size
    with strategy.scope():
        dropout_step = CFG.dropout_start_epoch * steps_per_epoch
        model = get_model(max_len=CFG.max_len, dropout_step=dropout_step, dim=CFG.dim)

        schedule = OneCycleLR(CFG.lr, CFG.epoch, warmup_epochs=CFG.epoch*CFG.warmup, steps_per_epoch=steps_per_epoch, resume_epoch=CFG.resume, decay_epochs=CFG.epoch, lr_min=CFG.lr_min, decay_type=CFG.decay_type, warmup_type='linear')
        decay_schedule = OneCycleLR(CFG.lr*CFG.weight_decay, CFG.epoch, warmup_epochs=CFG.epoch*CFG.warmup, steps_per_epoch=steps_per_epoch, resume_epoch=CFG.resume, decay_epochs=CFG.epoch, lr_min=CFG.lr_min*CFG.weight_decay, decay_type=CFG.decay_type, warmup_type='linear')
                
        awp_step = CFG.awp_start_epoch * steps_per_epoch
        if CFG.fgm:
            model = FGM(model.input, model.output, delta=CFG.awp_lambda, eps=0., start_step=awp_step)
        elif CFG.awp:
            model = AWP(model.input, model.output, delta=CFG.awp_lambda, eps=0., start_step=awp_step)

        opt = tfa.optimizers.RectifiedAdam(learning_rate=schedule, weight_decay=decay_schedule, sma_threshold=4)#, clipvalue=1.)
        opt = tfa.optimizers.Lookahead(opt,sync_period=5)

        model.compile(
            optimizer=opt,
            loss=[tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.1)], #[tf.keras.losses.CategoricalCrossentropy(from_logits=True)],
            metrics=[
                [
                tf.keras.metrics.CategoricalAccuracy(),
                ],
            ],
            steps_per_execution=steps_per_epoch,
        )
    
    if summary:
        print()
        model.summary()
        print()
        print(train_ds, valid_ds)
        print()
        schedule.plot()
        print()
        init=False
    print(f'---------fold{fold}---------')
    print(f'train:{num_train} valid:{num_valid}')
    print()
    
    if CFG.resume:
        print(f'resume from epoch{CFG.resume}')
        model.load_weights(f'{CFG.output_dir}/{CFG.comment}-fold{fold}-last.h5')
        if train_ds is not None:
            model.evaluate(train_ds.take(steps_per_epoch))
        if valid_ds is not None:
            model.evaluate(valid_ds)

    logger = tf.keras.callbacks.CSVLogger(f'{CFG.output_dir}/{CFG.comment}-fold{fold}-logs.csv')
    sv_loss = tf.keras.callbacks.ModelCheckpoint(f'{CFG.output_dir}/{CFG.comment}-fold{fold}-best.h5', monitor='val_loss', verbose=0, save_best_only=True,
                save_weights_only=True, mode='min', save_freq='epoch')
    snap = Snapshot(f'{CFG.output_dir}/{CFG.comment}-fold{fold}', CFG.snapshot_epochs)
    swa = SWA(f'{CFG.output_dir}/{CFG.comment}-fold{fold}', CFG.swa_epochs, strategy=strategy, train_ds=train_ds, valid_ds=valid_ds, valid_steps=-(num_valid//-CFG.batch_size))
    callbacks = []
    if CFG.save_output:
        callbacks.append(logger)
        callbacks.append(snap)
        callbacks.append(swa)
        if fold != 'all':
            callbacks.append(sv_loss)
        
    history = model.fit(
        train_ds,
        epochs=CFG.epoch-CFG.resume,
        steps_per_epoch=steps_per_epoch,
        callbacks=callbacks,
        validation_data=valid_ds,
        verbose=CFG.verbose,
        validation_steps=-(num_valid//-CFG.batch_size)
    )

    if CFG.save_output:
        try:
            model.load_weights(f'{CFG.output_dir}/{CFG.comment}-fold{fold}-best.h5')
        except:
            pass
    if fold != 'all':
        cv = model.evaluate(valid_ds,verbose=CFG.verbose,steps=-(num_valid//-CFG.batch_size))
    else:
        cv = None

    return model, cv, history

def train_folds(CFG, folds, strategy=STRATEGY, summary=True):
    for fold in folds:
        if fold != 'all':
            all_files = TRAIN_FILENAMES
            train_files = [x for x in all_files if f'fold{fold}' not in x]
            valid_files = [x for x in all_files if f'fold{fold}' in x]
        else:
            train_files = TRAIN_FILENAMES
            valid_files = None
        
        train_fold(CFG, fold, train_files, valid_files, strategy=strategy, summary=summary)
    return

这段代码定义了一个名为CFG的类,其中包含了一些超参数和配置信息,用于训练一个模型。其中包括数据集划分数、是否保存输出、输出目录、随机种子、最大长度、学习率、权重衰减、训练轮数、批量大小等等。此外,还包括了一些特殊的训练技巧,如混合精度训练、对抗训练、自适应权重裁剪等。这些超参数和技巧的具体含义需要根据具体的模型和任务来理解

class CFG:
    n_splits = 5
    save_output = True
    output_dir = '/kaggle/working'
    
    seed = 42
    verbose = 2 #0) silent 1) progress bar 2) one line per epoch
    
    max_len = 384
    replicas = 8
    lr = 5e-4 * replicas
    weight_decay = 0.1
    lr_min = 1e-6
    epoch = 300 #400
    warmup = 0
    batch_size = 64 * replicas
    snapshot_epochs = []
    swa_epochs = [] #list(range(epoch//2,epoch+1))
    
    fp16 = True
    fgm = False
    awp = True
    awp_lambda = 0.2
    awp_start_epoch = 15
    dropout_start_epoch = 15
    resume = 0
    decay_type = 'cosine'
    dim = 192
    comment = f'islr-fp16-192-8-seed{seed}'
train_folds(CFG, [0])

在这里插入图片描述在这里插入图片描述在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值