抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

本文为论文Mode-Adaptive Neural Networks for Quadruped Motion Control(SIGGRAPH 2018 )的代码解读,算法部分只会描述最终使用的方案,Motivation和具体原理可参考原论文或者:

[合集] Data-Driven Character Motion Synthesis - 知乎 (zhihu.com)

对《Mode-Adaptive Neural Networks for Quadruped Motion Control》一文的理解(上)_Arcobaleno-CSDN博客

原论文代码在github的地址为:https://github.com/sebastianstarke/AI4Animation

网络结构

img

输入、输出

输入的具体格式

1
2
3
4
5
6
7
8
9
10
这一 state (i) 的Trajectory,所有值都相对于根节点
(pos.x, pos.z, dir.x, dir.z, vel.x, vel.z, speed, 以及Styles的6维one-hot向量),共13维
13 * 12 = 156 (从0到110共12个)

上一 state (i-1) 的关节
位置,foward和up共同构成的rotation信息,速度
(pos.x, pos.y, pos.z, foward.x, foward.y, foward.z, up.x, up.y, up.z, vel.x, vel.y, vel.z),共12维
12 * 27 = 324

输入共156 +324 = 480维

输出的具体格式

1
2
3
4
5
6
7
8
9
10
11
12
13
下一 state (i+1) 的 Trajectory
pos.x, pos.z, dir.x, dir.z, vel.x, vel.z
6 * 6 = 36(只预测未来的Point,60, 70, 80, 90, 100, 110)

这一 state (i)的关节
(pos.x, pos.y, pos.z, foward.x, foward.y, foward.z, up.x, up.y, up.z, vel.x, vel.y, vel.z)
12 * 27 = 324

根节点相对于上一帧的位移
(x, 角度, z)
3

输出共324 + 36 + 3 = 363维

前向传播

混合公式

门控网络的输出维度为num_experts,相应地存在num_experts个动作预测网络,根据门控网络的输出的混合参数对动作预测网络进行混合。论文中提到作者尝试了num_experts=4和num_experts=8两种设置。

α=i=1Kwiαi\alpha = \sum_{i=1}^{K} w_i \alpha_i

其中系数 wiw_i 是门控网络的输出。

Gating Network(门控网络)

Ω(x^;μ)=σ(W2ELU(W1ELU(W0x^+b0)+b1)+b2)\Omega(\hat{\mathbf{x}} ; \mu)=\sigma\left(\mathbf{W}_{2}^{\prime} \operatorname{ELU}\left(\mathbf{W}_{1}^{\prime} \operatorname{ELU}\left(\mathbf{W}_{0}^{\prime} \hat{\mathbf{x}}+\mathbf{b}_0^{\prime}\right)+\mathbf{b}_{1}^{\prime}\right)+\mathbf{b}_2^\prime\right)

每一层都做了dropout, σ\sigma 为softmax函数。

ELU为激活函数指数ReLU。

ELU(x)=max(x,0)+exp(min(x,0))1\operatorname{ELU}(x)=\operatorname{max}(x,0)+\operatorname{exp}(\operatorname{min}(x,0))-1

Motion Prediction Network(动作预测网络)

Θ(x;α)=W2ELU(W1ELU(W0x+b0)+b1)+b2\Theta(\mathbf{x} ; \alpha)=\mathbf{W}_{2} \operatorname{ELU}\left(\mathbf{W}{1} \operatorname{ELU}\left(\mathbf{W}{0} \mathbf{x}+\mathbf{b}{0}\right)+\mathbf{b}{1}\right)+\mathbf{b}{2} 每一层都做了dropout。

训练参数和其他细节

使用z-score归一化

由于trot和canter的周期有限,作者将它们的数据各复制了11次。

损失函数:MSE

Cost(X,Y;β,μ)=YΘ(X,Ω(X^;μ);β)22\operatorname{Cost}(X,Y;\beta,\mu)=\|Y-\Theta(X,\Omega(\hat{\mathbf{X}};\mu);\beta)\|_2^2

用SGD和AdamWR中的warm restart技术来训练模型。

AdamWR自带了正则化所以损失函数不包含正则化项。

AdamWR用参数 TiT_iTmultT_{mult} 来控制学习率 η\eta 和权重衰减率 λ\lambda ,初始 η=1.0104\eta=1.0 \cdot 10^{-4}λ=2.5103\lambda=2.5 \cdot 10^{-3}

TiT_i 是第i次运行的epoch数,初始为10。每次重启时,将 TiT_i 乘以 Tmult=2T_{mult}=2

总轮数为150,所以训练过程中一共会重启三次,在第11轮,31轮和71轮时重启。

batch size=32

Dropout保留率为0.7

工作流程

作者使用的游戏引擎为Unity,深度学习库为Tensorflow。

整个流程可以分为3个阶段。

第一阶段做数据预处理。神经网络算法落地的大部分工作都是特征工程,原始的bvh数据没有任何标签,为了给数据打上粗粒度的标签(idle, move, lie, sit, stand, jump),作者选择在Unity中用自定义格式和插件进行可视化的操作,整个打标过程完成后导出成txt格式的训练集。

第二阶段,得到训练集后用TensorFlow训练,训练完成后得到.bin格式的权重文件。

第三阶段便可以在Unity中按照网络结构定义MANN类,读入权重预测每一帧的骨骼位置啦。注意由于网络输入的Trajectory中包含了Style信息,输入神经网络前要根据玩家输入的控制信号修改上一帧的Trajectory。

img

C#代码文件总览

虽然流程图看上去很简单,但用C#做数据处理和神经网络预测其实是个比较复杂的过程。尤其是数据处理阶段,自定义格式导致了频繁的函数跳转,在没有任何注释的情况下读起代码来非常容易陷入困惑。

整个C#部分用cloc统计一共有9592行C#代码,697行注释,1575行空白。

其中数据预处理相关的代码约3000行,运行时接受输入预测下一帧位置(第三阶段)的代码约2000行,第三方库、数学运算、其他代码共4000多行。

以下是主要代码文件的简单说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Assets

Demo

BioAnimation_Wolf.cs 647行,MonoBehaviour类,在Update函数中根据玩家输入的信号预测轨迹、调用MANN执行预测、调用MotionEditing做后处理和IK,更新计算出来的骨骼位置

MotionEditing.cs 89行,MonoBehaviour类,调用IK类,进行后处理

Scripts
Animation

Actor.cs 373行,MonoBehaviour类,提取、筛选Wolf的骨骼的Transform,并且显示骨骼,挂载在Wolf上

Controller.cs 233行,从InputHandler查询玩家输入的控制信号确定当前的运动Style

Trajectory.cs 360行,定义Wolf的轨迹,包含111个Point

Deep Learning

Models

MANN.cs 120行,MonoBehaviour类,定义MANN的网络结构

PFNN.cs 95行,MonoBehaviour类,定义PFNN的网络结构

NeuralNetwork.cs 261行,MonoBehaviour类,相当于torch.nn.Module,MANN和PFNN继承自此类

Parameter.cs 96行,ScriptableObject类,存放权重数据的缓冲,从bin文件读入权重暂时存在这,MANN类之后再从Parameters读入权重

Tensor.cs 178行,定义Tensor

IK

SerialIK.cs 87行,MonoBehaviour类,挂载到Wolf->Skeleton->Hips->Tail

FootIK.cs 135行。MonoBehaviour类,挂载到LeftUpLeg, RightUpLeg, LeftShoulder, RightShoulder

Tools

PIDController.cs 46行,PID控制器,BioAnimation_Wolf的PredictTrajectory函数会调用此类

Cameracontroller.cs 385行,MonoBehaviour类,接受输入,更新维护视角的四种摄像机,挂载在Camera上

InputHandler.cs 58行,MonoBehaviour类,缓存键盘输入,挂载在Wolf上

FPS.cs 25行,MonoBehaviour类,显示帧率,挂载在Camera上

Stick.cs 29行,MonoBehaviour类,挂在Scene的Lights上

RunningStatistics.cs 50行,MotionExporter的Data类会调用此类获得统计信息

DataProcessing

Importer

FBXImporter.cs 243行,EditorWindow类,加载fbx保存成MotionData格式的Asset文件

BVHImporter.cs 416行,EditorWindow类,训练集数据处理,从bvh文件变成MotionData格式的asset文件

AnimatorImporter.cs 372行,MonoBehaviour类

Modules

PhaseModule.cs 1108行,继承Module,对一个bvh文件的phase进行标记

StyleModule.cs 336行,继承Module,对一个MotionData asset文件的style进行标记

TrajectoryModule.cs 137行,继承Module,生成一个MotionData asset文件的Trajectory

Module.cs 48行,ScriptableObject类,PhaseModule、StyleModule、TrajectoryModule的基类

Frame.cs 178行,定义Frame类,MotionData的一部分,保存每一帧的骨骼状态

MotionData.cs 330行,ScriptableObject类,存放动捕数据,包括骨骼的结构和每一帧骨骼的状态,对应一个bvh文件

MotionEditor.cs 790行,MonoBehaviour类,挂在Wolf上。加载MotionData格式的asset文件,调用Module类标记运动的Style(Idle, Move, Jump, Sit, Stand, Lie)

MotionExporter.cs 465行,EditorWindow类,用MotionEditor对训练集打标签后,导出txt格式的训练集

数据处理部分

由于作者没做任何说明,数据处理的步骤是笔者通读所有代码后自己推断的o(╯□╰)o

存放数据的ScriptableObject类

img

ScriptableObject类在Unity中专门用于存放数据,可以被存储为asset文件。

作者为了在Unity中处理数据自定义了MotionData格式,一个MotionData实例就对应于一个bvh文件,包含的内容包括骨骼的结构(Hierarchy类)和捕捉到的动画中每一帧每个关节的位置(Frame类)。为了对动捕数据的运动模式打标、提取运动轨迹数据,又定义了StyleModule类和TrajectoryModule类。每个MotionData实例都需要添加一个StyleModule和一个TrajectoryModule。

MotionData的内部类Sequence指示MotionData中一段时间的数据,默认只有一个从头到尾的sequence。Hierarchy的内部类Bone对应骨骼上的一个关节。

StyleModule类用关键帧的方式对动捕数据进行标记。布尔数组Keys的长度等于动捕文件的帧数,标记每一帧是否为关键帧。GetStyle函数返回one-hot向量表示这一帧的Styles。

StyleFunction类执行实际的标记处理。这里采用了对每个Style分别处理的方式,本文中一共有6个Style,所以StyleModule中有6个StyleFunction。

StyleFunction的Name属性表示该StyleFunction处理哪一个Style,Values数组表示每一帧该Style的值,关键帧的Value为0或者1。标记了关键帧的Value后,StyleFunction的Compute函数对中间帧的Value做线性插值。

TrajectoryModule用于生成Trajectory类型的轨迹数据,TrajectoryModule::GetTrajectory()函数为MotionData中的一帧生成一个Trajectory实例,每个Trajectory类都包含若干个Point。

PhaseModule推测应是实现前一篇论文PFNN时给运动标记相位的模块,在MANN中用不上。

另外,还有一个Parameters类也继承了ScriptableObject,其作用为在游戏运行阶段保存从.bin文件中读取的神经网络权重,实际在第三阶段游戏运行时用到。

MotionEditor组件和EditorWindow类

img

在Unity中继承EditorWindow类可以在菜单栏生成按钮打开自定义的窗口。

作者用了两个EditorWindow类和一个MotionEditor组件来处理数据,步骤为:首先用BVHImporter从指定的文件夹中导入bvh文件转换成MotionData格式的asset文件,然后用Wolf对象上挂载的MotionEditor组件读取MotionData格式的asset文件,标记动捕数据的Style。最后用MotionExporter将带有标签的数据导出为txt文件当做下一阶段的训练集。

Actor类也是一个组件,其Bones数组指向所有要处理的关节,其他所有类需要处理骨骼的transform时都只需使用Actor的Bones数组,而不用GameObject.Find函数或GameObject.GetChild函数。Actor的ExtractSkeleton方法有两个重载,不要参数的版本从当前挂载的游戏对象上提取骨骼信息,并且提供了筛选关节的GUI界面,需要Transform数组作为参数的版本用于从文件中读取骨骼信息,配合MotionEditor给asset文件打标。

读取骨骼的调用链

调用链1MotionEditor挂载到GameObject上,点击Import时

MotionEditor.Import()->MotionEditor.Initialise()->MotionEditor.LoadFile(Files[0])

MotionEditor::Import()先读取MotionData格式的.asset文件存入Files(调用AssetDatabase.FindAssets和AssetDatabase.LoadAssetAtPath),然后调用Initialise()->LoadFile(Files[0])将骨骼数据保存到Instance中。

Files数组放着所有的asset文件,Instance是当前处理的文。Instance和Files的类型为File,File.Data类型为MotionData,

调用链2任意一处调用MotionEditor.GetActor()时(MotionEditor_Editor.Inspector()函数会循环调用GetActor()):

MotionEditor.GetActor()->MotionEditor.Createskeleton()->Actor.ExtractSkeleton()->GetCurrentFile()

创建Actor,调用actor.ExtractSkeleton(Transform[] bones),从当前激活的Instance(MotionData文件)中加载骨骼保存到Actor.Bones,返回Actor。

MotionExporter::ExportData函数

MotionExporter::ExportData方法开始执行时仅标记了Style,还没有生成Trajectory。MotionExporter::ExportData调用State类的构造函数,State的构造函数又会调用TrajectroyModule.GetTrajectory方法来生成Trajecory。一个State实例中保存着一帧的骨骼和Trajectory,State类的数组中有训练集需要的所有数据。最后MotionExporter::ExportData以Data类为媒介将这些数据写入Input.txt和Output.txt。其中Data类相当于一个tensor。

图中还有一个没提到的FBXImporter类,其代码是导入fbx和Actor结合后导出成MotionData格式的asset,不过在MANN项目中似乎不需要这个操作,可以略过。

训练

MANN结构简单,使用TensorFlow训练的代码很容易看懂。

这里只提一下ExpertWeights.py为神经网络中的一层,其中参数的维度前面加上了个num_experts,也就是说每个ExpertWeights类实例中存放了num_experts组一层的参数,其get_NNweight和get_NNbias根据Gating.py输出的混合参数将这几组参数混合得到真正的参数。

游戏运行时

img

有了权重文件,接下来要搭建MANN网络读入参数,这里只需要实现前向传播。

像各大深度学习库一样,作者先实现了一个神经网络的基类NeuralNetwork,再定义MANN继承NeuralNetwork实现具体的网络结构,MANN的一些重要参数在Unity Editor中设置。

需要注意,读入参数时先从bin文件读入Parameters类,再从Parameters读入MANN。

调用链1在Editor点击MANN组件的store parameters时

NeuralNetwork_Editor.OnInspectorGUI()->NN.StoreParameters()->MANN.StoreParametersDerived()->Parameters.Store()->Parameters.ReadBinary()

从bin文件中读取权重存入Parameter.Matrices

Parameters是ScriptableObject类,临时存放网络权重

调用链2BioAnimation_Wolf组件Awake时

BioAnimation_Wolf.Awake()->NN.LoadParameters()->MANN.LoadParametersDerived()->Parameters.Load()

从Parameter.Matrices中加载权重到MANN

InputHandler组件缓存用户输入。Controller类从InputHandler获得用户输入,判断用户输入的Style。每种Style对应的用户输入等参数需要在Unity Editor中设置。

Trajectory类保存游戏对象的轨迹,在MANN的代码实现中Trajectory包含111个Point,输入神经网络时,从0号Point开始每隔10个Point取一个,共有13个Point输入MANN。而MANN输出新Point时,编号非10的倍数的Point由前后两个Point线性插值得到。

SerialIK组件和FootIK组件挂在要做IK的骨骼关节上, MotionEditing组件(注意和前面给动捕数据打标的MotionEditor组件不一样)调用它们执行实际的IK算法。

BioAnimation_Wolf组件负责预测并更新骨骼Transform的整个过程。其Awake方法初始化Trajectory并调用NN.LoadParameters()让MANN从Parameters中读取权重。 其Update方法先调用自身的PredictTrajectory函数,根据用户输入修改当前的Trajecotory,再调用自身的Animate函数,用MANN.Predict函数预测Trajectory和骨骼位置并做插值处理,最后调用MotionEditing处理IK过程。

评论