0%

背景

近期我们在做一个通用AI的项目时使用了UE4引擎,一个同事在阅读源码时发现了这一段代码写的比较“秀”,于是推荐给我,我阅读之后在此记录笔记。

原代码

这一段代码来自Epic Games Unreal Engine 4,其最新版本为: Github 这是一个私有仓库,访问代码可能需要先加入开发组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreTypes.h"

/**
* Tests if a From* is convertible to a To*
**/
template <typename From, typename To>
struct TPointerIsConvertibleFromTo
{
private:
static uint8 Test(...);
static uint16 Test(To*);

public:
enum { Value = sizeof(Test((From*)nullptr)) - 1 };
};


分析

C++编程语言可以在编译期间确定一个类型是否可以转换为另一个类型,但是它没有提供一个展示这个信息的关键字,如果不加检测的直接使用类型转换操作符(C style转换和 static_cast<>)操作符,则会引起编译器报错。

上面这个代码实现了一个这样的转换检测的类似“编译期运算符”的操作, 可以在编译期获知两个类型是否可转换的关系。

利用 sizeof 和 函数调用编译期绑定的特性

C++ sizeof运算符可以在编译期实现“常量替换” (这里注意sizeof并不总是在编译期就能确定其值)。

C++ 在进行函数调用时,基于编译期函数调用解析(有些资料把它命名为静态多态)的特征,我们在编译时即可确定将要调用哪一个具体函数。这里,就是利用了函数返回值类型不同来在源码层级上确定了到底是调用了哪一个函数,从而反映出目标类型与对照类型的转换关系。

实现的关键代码:

1
2
template <typename From, typename To>
enum { Value = sizeof(Test((From*)nullptr)) - 1 };

对空指针做一个强制类型转换,解释为From类型指针,然后作为参数调用Test函数, 在代码中,Test有两个实现,其中一个为To*类型参数, 另一个则为任意类型的泛参。

这个调用在编译期即可确定, 若From类型可以兼容To类型(即,有继承层级上的直接关系,或者满足隐式转换规则),则uint16 Test(To*) 函数将被绑定, 否则,uint8 Test(...) 函数将被绑定。而这两个函数的返回值类型被sizeof运算符在编译期捕捉到,并得到长度,使用长度来反映是否可以转换来给Value赋值,从而实现了一个源码级别上的在编译期确定是否兼容的检测运算符。

常用函数导数公式

本篇主要总结常见的函数形式的导数公式。以类别为要。其他形式的函数和复合形式的函数均可以通过变形套用本篇公式来结合 求导规则 推导导函数。

常数函数导数

常数函数, 形如

\[ f(x) = C \]

其中C为常数, 则导数为0

\[ f'(x) = 0 \]

幂函数导数

幂函数形如:

\[ f(x) = x^{n} \]

则幂函数的导函数为:

\[ f'(x) = nx^{n-1} \]

有些幂函数不以明显的形式来表现出来, 例如 \(f(x) = \sqrt{x}\) 或者 \(f(x) = \frac{1}{x}\) 的形式, 实际上, 他们只是 \(n\) 取值不同的表达, 开根号表达形式的 \(n\) 为一个自然数为底的分数,分式形式时 \(n\) 为自然数的负值, 但是他们都遵从幂函数的导函数形式。

指数函数的导数

指数函数形如:

\[ f(x) = n^x \]

其导数为:

\[ f(x) = n^x\ln{n} \]

对数函数导数

形如

\[ f(x) = log_n(x) \]

的函数。

\[ f'(x) = \cfrac{1}{x\ln{n}} \]

三角函数导数

常用三角函数导数

正弦函数:

\[ f(x) = sin(x) \]

\[ f'(x) = cos(x) \]

余弦函数

\[ f(x) = cos(x) \]

\[ f'(x) = -sin(x) \]

矩阵相关知识

首先声明一个“原矩阵”的概念。我们用作参考来说明的原始矩阵 \(\boldsymbol{M}\) , 在下面的文字中被称为原矩阵。为了方便描述各种矩阵,先建立一个矩阵假设原型 \[ \boldsymbol{M} = \begin{bmatrix} c_{11} & c_{12} & c_{13} \\ c_{21} & c_{22} & c_{23} \\ c_{31} & c_{32} & c_{33} \\ \end{bmatrix} \]

约定对角线为方阵从左上角到后下角各行号与列号相等的元素构成,反对角线为方阵从右上角到左下角其行号和列号互补的元素构成。

由于学习过程中参考了不同的资料,对于n维矩阵和n阶矩阵交替出现, 但是他们都代表相同的含义。

转置矩阵

将原矩阵中所有元素的行列编号颠倒构成新的矩阵 \(\boldsymbol{M}^T\) , 该矩阵就叫做原矩阵的转置矩阵

其变换公式为 \(c'_{rc} = c_{cr}\)

\[ c'_{rc} = c_{cr} \]

公式中 \(c_{cr}\) 代表原矩阵第c行第r列的元素。 \(c'_{rc}\) 代表转置矩阵中第r行第c列的元素。

用更形象一点的表述为,将原矩阵沿对角线进行了“翻转”。

转置矩阵 \(\boldsymbol{M}^T\) 为:

\[ \boldsymbol{M}^T = \begin{bmatrix} c_{11} & c_{21} & c_{31} \\ c_{12} & c_{22} & c_{32} \\ c_{13} & c_{23} & c_{33} \\ \end{bmatrix} \]

转置矩阵的性质

一个重要的性质是:转置矩阵的行列式和原矩阵行列式保持不变

对于转置矩阵的运算性质

\[ \begin{eqnarray} (\boldsymbol{M}^T)^T &=& \boldsymbol{M} \tag{1} \\ (k\boldsymbol{M})^T &=& k\boldsymbol{M}^T \tag{2} \\ (\boldsymbol{A}\boldsymbol{B})^T &=& \boldsymbol{B}^T\boldsymbol{A}^T \tag{3} \\ (\boldsymbol{A} + \boldsymbol{B})^T &=& \boldsymbol{A}^T + \boldsymbol{B}^T \tag{4} \\ \det{\boldsymbol{M}} &=& \det{\boldsymbol{M}^T} \tag{5} \end{eqnarray} \]

即:

  • (1)矩阵转置后再转置,得到原矩阵。
  • (2)矩阵数乘后转置,等于转置矩阵数乘。 (该条性质可以由(1)根据数乘性质推得)
  • (3)两矩阵乘后转置,等于其转置矩阵反向乘。
  • (4)两矩阵加和后转置,等于他们的转置矩阵的和。
  • (5)原矩阵的行列式和转置矩阵的行列式相等。

对称矩阵与反对称矩阵

特别的,对于矩阵是方阵的类型,若 \(\boldsymbol{M}^T = \boldsymbol{M}\) , 则方阵可以被称为对称矩阵

对于矩阵是方阵的类型,若 \(\boldsymbol{M}^T = \boldsymbol{-M}\) , 则方阵可以被称为反对称矩阵

由于转置过程中 \(c'_{rc} = c_{cr}\)\(r=c\) 位置(即对角线位)的元素不会变化, 要满足 \(c'_{rc}=-c_{cr}\) 的唯一条件是该位置元素为0。所以我们可以得出结论,非对称矩阵对角线元素一定为0

形象的记忆,这里的对称,可以理解为方阵沿主对角线轴对称

行列式

我一时找不到一个合适的描述来给行列式下一个定义。

对于任意方阵,总存在这么一个标量, 它可以由“行列式定义”计算得到,我们把这个标量称呼为矩阵的行列式

需要注意的是,行列式只对方阵有定义,对非方阵没有定义。1维矩阵的行列式与它唯一的元素相等。

行列式的记号

对于矩阵 \(\boldsymbol{M}\) 来说,学术圈主流有两种记号来表达它的行列式

  • \(|\boldsymbol{M}|\)
  • \(\det{\boldsymbol{M}}\)

行列式的几何意义

对于任意的二维/三维矩阵 \(\boldsymbol{M}\) 来说,我们把它视为一个变换,那么行列式在几何上表达这个变换前后物体的面积/体积的变化率(即变化倍数)。

若行列式为负值,其表明物体的 面积/体积翻转(可类比镜像变换,但是这里并不能表达出其角度打小的恒定,只表达出方向的变化)。

当行列式值为0时,则说明其面积/体积变为0,直接说明此变换包含一个投影, 此时有至少一个维度上所有信息被归零丢失, 我们无法做此变换的逆变换了,矩阵 \(\boldsymbol{M}\) 不可逆,为奇异矩阵。

拓展到更高的维度上, 行列式也表达高维体积的变化率。对于一维向量来说,它表达更丰富的含义,但最直接的,它表达向量的模(长度信息)的变化率。

行列式的定义

我在这里将行列式用复合形式定义,对于低维度矩阵使用表式定义,对于高维矩阵使用递归定义。

二维矩阵行列式

\[ \begin{aligned} |\boldsymbol{M}| & = \left| \begin{matrix} c_{11} & c_{12} \\ c_{21} & c_{22} \\ \end{matrix} \right| \\ & = (c_{11}c_{22})-(c_{12}c_{21}) \end{aligned} \]

三维矩阵行列式

\[ \begin{aligned} |\boldsymbol{M}| &= \left| \begin{matrix} c_{11} & c_{12} & c_{13} \\ c_{21} & c_{22} & c_{23} \\ c_{31} & c_{32} & c_{33} \\ \end{matrix} \right| \\ &=(c_{11}c_{22}c_{33})+(c_{12}c_{23}c_{31})+(c_{13}c_{21}c_{32}) \\ &\;-(c_{11}c_{23}c_{32})-(c_{12}c_{21}c_{33})-(c_{13}c_{22}c_{31}) \end{aligned} \]

更高维矩阵行列式

更高维度的矩阵的行列式定义,这里需要引入一个概念,叫做余子式。余子式的概念和相关性质在后文中给出,这里只是先使用。

从矩阵中任意选择一行或者一列, 对该行或列的每个元素都乘以该元素对应的代数余子式, 将他们作和就是该矩阵的行列式。

以下公式将选择一行i做为固定值, 对元素按列进行迭代。

\[ \begin{aligned} \det\boldsymbol{M} &= \sum_{j=1}^{n} m_{ij}c_{ij} \\ &= \sum_{j=1}^{n}m_{ij}(-1)^{i+j}\left| \boldsymbol{M}^{\{ij\}}\right| \end{aligned} \]

这里 \(|\boldsymbol{M}^{\{ij\}}|\) 又为其余子式的行列式, 但是其阶数为(n-1), 递归的套用上式, 最终可使得公式中代数余子式的维度为1(或者2,或者3皆可), 然后我们套用之前的定义,可以求得其行列式。

行列式的性质

\[ \begin{aligned} |\boldsymbol{A}| &= |\boldsymbol{A}^T| \\ |\boldsymbol{A}\boldsymbol{B}| &= |\boldsymbol{B}\boldsymbol{A}| \\ |k\boldsymbol{A}| &= k^n|\boldsymbol{A}| \\ |\boldsymbol{A}\boldsymbol{B}| &= |\boldsymbol{A}||\boldsymbol{B}| \\ |\boldsymbol{A}^n| &= |\boldsymbol{A}|^n \\ \end{aligned} \]

n 为矩阵的维数

余子式

余子式是一个与原矩阵行列相关的子矩阵。将矩阵 \(\boldsymbol{M}\) 中某些行/列去掉后,剩余的矩阵就叫做余子式,也有叫法叫做余因式。

n阶矩阵 \(\boldsymbol{M}\) , 选定其第i行j列的元素 \(c_{ij}\) ,去除矩阵 \(\boldsymbol{M}\) 的第i行和第j列, 剩下的元素保持位置组成的矩阵就叫做元素 \(c_{ij}\) 的余子式。余子式与元素 \(c_{ij}\) 的值无关,只与元素所在的位置有关。

\(i\) 行第 \(j\) 列的余子式记做 \(\boldsymbol{M}^{\{ij\}}\)

代数余子式

区别于余子式,代数余子式是一个 标量, 它是由矩阵元素 \(m_{ij}\) 与其对应余子式 \(\boldsymbol{M}^{\{ij\}}\) 的行列式的乘积再乘以符号因子 \((-1)^{i+j}\) 。 显然的, 当行列号之和 \(j+j\) 为偶数时符号因子为正,行列号之和 \(i+j\) 为奇数时,符号因子为负。

第i行j列的代数余子式记做 \(c_{ij}\)

\[ c_{ij} = (-1)^{i+j}m_{ij}\left|\boldsymbol{M}^{\{ij\}}\right| \]

伴随矩阵

伴随矩阵是原矩阵的代数余子式构成的矩阵的转置矩阵。记做 \(adj\boldsymbol{M}\)

\[ \begin{aligned} adj\boldsymbol{M} &= \begin{bmatrix} c_{11} & c_{12} & c_{13} \\ c_{21} & c_{22} & c_{23} \\ c_{31} & c_{32} & c_{33} \end{bmatrix}^T \\ & = \begin{bmatrix} c_{11} & c_{21} & c_{31} \\ c_{12} & c_{22} & c_{32} \\ c_{13} & c_{23} & c_{33} \end{bmatrix} \end{aligned} \]

逆矩阵

一个(组)向量 经过矩阵 \(\boldsymbol{M}\) 变换后再经过矩阵 \(\boldsymbol{N}\) 变换仍得到原先的向量, 此时,就仿佛 \(\boldsymbol{N}\)\(\boldsymbol{M}\) 所施加的变换“撤销”了。 使用公式表述即 \(\boldsymbol{N}\boldsymbol{M}\boldsymbol{A}=\boldsymbol{A}\) , \(\boldsymbol{N}\boldsymbol{M}=\boldsymbol{I}\) 。若满足这个条件 \(\boldsymbol{N}\) 叫做 \(\boldsymbol{M}\)逆矩阵,记做 \(\boldsymbol{M}^{-1}\)

更简短一点的表述,若两个方阵的积为单位矩阵, 则这两个矩阵互为逆矩阵。

\[ \boldsymbol{M}\boldsymbol{M}^{-1} = \boldsymbol{I} \]

逆矩阵的性质

\[ \begin{aligned} \boldsymbol{M}\boldsymbol{M}^{-1} &= \boldsymbol{I} \\ \boldsymbol{I}^{-1} &= \boldsymbol{I} \\ (\boldsymbol{M}^{-1})^{-1} &= \boldsymbol{M} \\ (\boldsymbol{M}^{-1})^T &= (\boldsymbol{M}^T)^{-1} \\ (\boldsymbol{A}\boldsymbol{B})^{-1} &= \boldsymbol{A}^{-1}\boldsymbol{B}^{-1} \end{aligned} \]

求逆矩阵的主要方式

a. 伴随矩阵法

\[ \boldsymbol{M}^{-1} = \dfrac{adj\boldsymbol{M}}{\det{\boldsymbol{M}}} \]

b. 高斯消元
c. 对于确定的正交矩阵,直接使用其转置矩阵

对于正交矩阵来说,由于其性质 \(\boldsymbol{M}\boldsymbol{M}^T = \boldsymbol{I}\) , 所以其逆矩阵就是其转置矩阵。

正交矩阵

若对于方阵 \(\boldsymbol{M}\) , 满足 \(\boldsymbol{M}\) 与它的转置矩阵 \(\boldsymbol{M}^T\) 的乘积为单位矩阵, 则矩阵 \(\boldsymbol{M}\) 被称为正交矩阵。

求导法则

复杂函数的求导, 可以看做是对几个简单函数(暂且称为子函数)的复合函数的求导。一般可以分以下几个情况。

常数项系数

函数的常数项系数的复合方式是最简单的, 它的形式一般如:

\[ f(x) = c \cdot h(x) \]

其中, \(c\) 是常数, \(h(x)\) 是任意关于 \(x\) 的函数, 则 \(f(x)\) 的求导规则为:

\[ f'(x) = c \cdot h'(x) \]

就是简单的对 \(h(x)\) 求导数, 然后乘以常数 \(c\)

子函数的和/差

若一个函数, 它可以被表述为多个子函数的和或者差的形式, 这里只以和的形式距离(差可以认为其中一个子函数乘以-1就好了), 形如:

\[ f(x) = h(x) + g(x) \]

此类函数的导数, 可以对其子函数分别求导, 然后再相加:

\[ f'(x) = h'(x) + g'(x) \]

多项乘积

两项的情况

若一个函数, 他可以被表述为两个子函数的乘积的形式, 形如:

\[ f(x) = h(x) \cdot g(x) \]

则, 它的导数为:

\[ f'(x) = h'(x) \cdot g(x) + h(x) \cdot g'(x) \]

记忆方式为 将两个子函数相乘, 加两次, 然后分别依次在各项的对应函数上加一个撇号

三项的情况

三项乘积的形式形如:

\[ f(x) = h(x) \cdot g(x) \cdot k(x) \]

其求导规则为

\[ f'(x) = h'(x) \cdot g(x) \cdot k(x) + h(x) \cdot g'(x) \cdot k(x) + h(x) \cdot g(x) \cdot k'(x) \]

记忆方式为 将三个子函数相乘, 加三次, 然后分别依次在各项的对应函数上加一个撇号

实际上, 多个函数的相乘, 都可以分解为两个项的相乘, 然后使用两项求导公式求导, 然后对每个子项递归下去。

推导一下:

\[ f(x) = h(x) \cdot g(x) \cdot k(x) \]

\[u(x) = h(x) \cdot g(x)\]

\[f(x) = u(x) \cdot k(x)\]

那么,根据两项相乘的求导公式:

\[ \begin{eqnarray} f'(x) = u'(x) \cdot k(x) + u(x) \cdot k'(x) \tag{1} \\ u'(x) = h'(x) \cdot g(x) + h(x) \cdot g'(x) \tag{2} \end{eqnarray} \]

将(2)带入(1)得:

\[ f'(x) = (h'(x) \cdot g(x) + h(x) \cdot g'(x)) \cdot k(x) + h(x) \cdot g(x) \cdot k'(x) \]

将上式乘开:

\[ f'(x) = h'(x) \cdot g(x) \cdot k(x) + h(x) \cdot g'(x) \cdot k(x) +h(x) \cdot g(x) \cdot k'(x) \]

即得到三项乘积的导数公式。

商式

若一个函数, 可以写作两个子函数商的形式, 形如:

\[ f(x) = \frac{g(x)}{h(x)} \]

它的导数公式为:

\[ f'(x) = \frac{g'(x)\cdot h(x) - g(x) \cdot h'(x)}{h(x)^2} \]

记忆方式: 按照两项乘积的公式, 对后项取负号, 然后除以商函数的平方

实际上, 商公式可以简单的由乘积公式推导而来。

推导:

\[ f(x) = \frac{g(x)}{h(x)} \]

我们假设有函数

\[ k(x) = h(x)^{-1} \tag{2.1} \]

则:

\[ \begin{eqnarray} f(x) &= g(x) \cdot k(x) \tag{2.2} \\ f'(x) &= g'(x) \cdot k(x) + g(x) \cdot k'(x) \tag{2.3} \end{eqnarray} \]

这里要提前引入一下链式函数的求导法则:

\[ k'(x) = -1 \cdot h(x)^{-2} \dot h'(x) \tag{2.4} \]

然后, 将 \((2.1) (2.4)\) 代入 \((2.3)\) 得到

\[ f'(x) = g'(x)\cdot h(x)^{-1} - g(x) \cdot h(x)^{-2} \cdot h'(x) \]

对上式提公因式

\[ \begin{eqnarray} f'(x) &= (g'(x) \cdot h(x) - g(x)\cdot h'(x)) \cdot h(x)^{-2} \notag \\ f'(x) &= \frac{g'(x) \cdot h(x) - g(x) \cdot h'(x)}{h(x)^{2}} \notag \end{eqnarray} \]

至此, 由乘法求导公式推导得到商式的求导公式。

链式函数

若一个函数由几层函数复合而成, 形如:

\[ f(x) = g(h(x)) \]

这里需要注意一下, 所谓的 "链式" 隐含一层自变量替换的概念, 它是指, 其中一个子函数作为另一个函数的 "自变量", 而非原生的自变量 \(x\)

那么, 链式求导法则为:

\[ f'(x) = g'(h(x)) \cdot h'(x) \]

上式正确, 但是稍显凌乱, 我重新表达一下

\[ 设变量 z = h(x) \]

\[ f'(x) = g'(z) \cdot h'(x) \]

用偏自然语言描述就是: 把作为自变量的函数看做一个整体, 对外层函数求导, 然后将结果乘以内层函数关于原生自变量$x$的导数。

更深层一点的是多层链式函数, 即可能一层复合函数无法精简的表达整个函数, 需要将关于$x$的函数复合多次, 这种情况下可以一层层套用两层链式法则。(每层展开乘以下层关于自己的自变量的导数即可)

常见变换

先约定一下,本文中使用的向量都是列向量,相应的公式也都是基于此前提。

线性变换

如果变换F保持了基本运算,加法和数乘,那么就可以称该变换是线性的。

数学上,要求线性变换满足这样的性质:

\[ \begin{eqnarray} F(\boldsymbol{a}+\boldsymbol{b}) &=& F(\boldsymbol{a}) + F(\boldsymbol{b}) \tag{1} \\ F(k\boldsymbol{a}) &=& kF(\boldsymbol{a}) \tag{2} \end{eqnarray} \]

需要注意的是,这里的函数自变量 \(\boldsymbol{a} \boldsymbol{b}\) 都是向量,公式中已经使用粗体表达, 再次强调一下。在自变量为标量时,公式2 可以由公式1推导得出,但是对于向量, 我们只能单独列出公式2,使变换满足。

仿射变换

仿射变换是线性变换后接着平移。所以,线性变换是一类特殊的仿射变换,它的平移量为 \(\boldsymbol{0}\) 。 换一句更加专业一点的表达方式。仿射变换是线性变换的超集,任何线性变换都是仿射变换

\[ \boldsymbol{v}' = \boldsymbol{Mv} + \boldsymbol{b} \]

其更正式一点的表达, 所有满足公式3的变换都是仿射变换,其中 \(\boldsymbol{b}\) 可以为0向量。

可逆变换

如果一个变换 \(F\) 可以被另一个变换 \(F^{-1}\) 所"撤销",那么,这个变换叫做可逆变换。一个重要的事实是,所有的仿射变换,除了“投影”,其他所有形式的仿射变换都是可逆的。当然,也存在一些可逆的非仿射变换,这里先不做讨论。

等角变换

如果变换前后各向量之间的夹角不发生变化(包括大小和方向)那么,该变换即成为等角变换。 仅有平移旋转均匀缩放是等角变换,而镜像非等角变换,因为镜像变换会更改角方向。

按照这里的说法,其实“方向”一词的出现是不严谨的,因为我们往往在空间中定义角度时会预先约定角方向,其方向信息可以使用角打小的符号来表示。

正交变换

正交变换是坐标轴保持垂直,且不进行缩放的变换。

平移旋转镜像变换是仅有的正交变换。

刚体变换

只改变物体位置和方向的变换是刚体变换。它不改变物体任何的自由属性(面积,体积,形状 等)。 刚体变换是正交变换等角变换的交集。 同时,刚体变换本身具有可逆的性质(非投影均可逆), 所以它也是可逆的变换。实际上,刚体变换是本文以上所有变换的“交集”。