本文首发于我的知乎,地址为:https://zhuanlan.zhihu.com/p/102351953
0、前言
1、卷积与多项式乘法
1.1、Convolution和Correlation的区别
1.2、卷积与多项式乘法的关系
2、理解Winograd算法需要的数学理论知识
2.1、欧几里得算法
2.2、多项式的欧几里得算法
2.3、扩展欧几里得算法
2.4、多项式的扩展欧几里得算法
2.5、乘法模逆元
2.6、多项式乘法模逆元
2.7、中国剩余定理
2.8、多项式的中国剩余定理
3、多项式的中国剩余定理的应用
3.1、卷积操作与中国剩余定理的联系
3.2、Winograd F(2,3)变换矩阵推导
3.3、Winograd F(4,3)变换矩阵推导
4、参考资料
其实网上已经有不少从数学原理的角度去解说Winograd[1,2,3,4,5,6,10]
这个算法的文章了,为什么我还要写这篇文章。
主要是在看完许多相关的文章之后,对于Winograd这个算法背后的数学原理我还是没法完全理解,尤其是Winograd的变换矩阵究竟是如何生成的。而在查阅到的资料里面,在描述到一些相关数学定理的时候,许多细节部分都没有很详细的说明,只能通过额外去查找资料和手推公式来理解。
这也是促成我写这篇文章的主要原因,想把有关Winograd这个算法背后所涉及到的数学知识用比较通俗的方式给读者描述一遍,并且在这的过程中也会添加一些我个人的理解,当然我的理解也不一定正确,如果有误也请读者指出。
总的来说感觉Winograd这个算法真的很巧妙,要理解这个算法,需要懂得前置数学知识挺多的,如果其中一个地方没弄懂,都会对理解这个算法的数学原理造成困难。而且即使已经看懂了整体部分,但是很多细节部分如果仔细去想就会觉得自己还没有完全弄懂。
这里我把收集到的所有相关资料链接都统一放到文末参考资料里面,也方便读者去查阅。
首先卷积其实有两个含义[8,9]
:
第一个是指一般数学意义上的的两个离散序列的卷积(Convolution);
第二个是深度学习中所用到的卷积(操作上更像Correlation而不是Convolution);
通俗来说两个离散序列在做Convolution操作的时候,首先需要将其中一个序列做镜像翻转,然后两个序列相向移动,从开始第一个元素重合到最后一个元素重合为止,相向移动步长为1,每次把重合的部分做点乘累加的到新的元素,最后生成新的序列。
而两个离散序列在做Correlation操作的时候,除了不需要翻转序列,操作上和Convolution一致。而深度学习中的Convolution其实和Correlation很像,但是又不完全一样。
不一样的地方在于,计算第一个元素的时候是直接把较短的序列和较长的序列从左首元素开始对齐,然后较短的序列按步长向右移动(假设步长也是1),一直到当前较短序列最右边的元素和较长序列最右边的元素对齐,要求短序列的每个元素都必须要和长序列的元素有重合,然后每次把重合的部分点乘累加得到新的元素。
从这里开始,下文提到的Correlation操作都是指深度学习中的卷积操作。所以给定同样长度的两个序列,分别做Convolution和Correlation操作得到的结果序列长度是不一样的。
假设两个长度分别是 和 的序列,分别做Convolution和Correlation操作(步长都为1)得到的结果序列长度的计算公式分别为:
下面简单画个示意图解释的两者的区别:
左边的Convolution操作还有最后两步没有画出来,不过这已经能足够解释两者的区别了。
然后来看下两者的输出元素个数计算公式之间的联系,比如给定两个序列长度分别为 和 ,Convolution操作的到的序列长度为 ,然后和 或者 长度的序列做 Correlation 可以得到长度为 或 的序列。
还有一点要提下,就是Winograd这个算法发明出来其实是用来加速Convolution操作的,所以计算变换矩阵也是从Convolution角度去计算,而计算出来的变换矩阵在做一点小变动之后,也可以直接应用在深度学习的Correlation操作中,这个在下文会讲到。
为什么提这个是因为,之前我在理解Winograd这个算法的是陷入了一个误区,一直是从深度学习卷积(Correlation)应用的这个角度去理解这个算法,然后一直想不明白,后来换成是从Convolution角度去理解很多地方就豁然开朗了。
Convolution操作其实直观上等价于多项式乘法操作[7]
。
还是用上面举的例子来说明,假设两个离散序列 和 ,我们可以把这两个序列看成是两个多项式 和 。这两个多项式相乘的结果是 ,把其系数从低次冥到高次冥排列,刚好就等价于这两个序列做 Convolution 的结果 。
从多项式相乘结果的最高次冥也可以看出两者的联系,假设两个离散序列长度分别为 和 ,则对应的多项式的最高次冥分别为 和 ,则这两个多项式相乘结果的最高次冥为 ,再加上一个最低次冥 总共就是 个元素,算上系数0的次冥。
下面再举一个例子,看下多项式乘法和Convolution操作的关系:
我们先来看下可以求解两个整数最大公约数的欧几里得算法[11,13]
,可能换成“辗转相除法”这个名字读者会更加熟悉。
我们知道两个整数 和 的公约数是既能整除 又能整除 的整数,而两者的最大公约数 就是这些数里面最大的数,通常用数学公式表示为 ,是Greatest common divisor 的缩写。
简单复习下整除的定义: 整除 可以记作 ,即存在一个整数 使得 。
然后如果 ,则称 和 互素,表示它们的最大公约数为1。而两个整数是否互素和它们本身是否是素数无关。
简单复习下素数[12]
的定义:素数(Prime number)又称质数,指在大于1的自然数中,除了1和该数自身外,无法被其他自然数整除的数。
引用[11]
的一个例子来解释最大公因数的概念,假设现在 ,那么设一个长方形的高是 ,宽是 ,因为 和 的任何公约数 都可以整除 和 ,所以长方形的高和宽都可以等分为长度是 的线段。通俗的说法是也就是长方形的内部可以刚好被边长是 的正方形填满。而最大公约数 是其中最大的一个正方形的边长,下面画个简单的示意图来说明:
接着我们看下如何用欧几里得算法求解最大公约数,先给出欧几里得算法的定义[11,13]
。
其中 也可写作 ,也就是求 除以 的余数,上面公式还有个条件就是 ,第一个公式很好理解,因为任何整数都能整除0。这里首先引入同余式的概念:若正整数 和 分别对 取模的余数相同,则可以记作 ,也就是 和 模 同余。再继续证明欧几里得算法第二个公式之前,先来看一下求模运算的一些运算规则[15]
:
(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p) % p
(a * b) % p = (a % p * b % p) % p
然后对于第二条公式的证明引用自[13]
,更多细节可以参考资料[11,13,14]
:
除以 的商是 余数是 ,则可以表示为:
考虑到 和 的最大公约数 可知:
然后根据运算规则和上面的两个公式可得:
所以 和 的最大公约数可以整除 ,也即 ,所以 能同时整除 、 和 ,所以 和 的最大公约数也是 和 的公约数,也即 也可以整除 ,也就是 。
然后对等式做下变换:
然后考虑 a 和 b 的最大公约数 GCD(a,b) 可知:
接着同样根据取模运算规则和上面的两个公式可得:
所以同理可得 也可以整除 ,也就是
所以通过上面的推导可得:
然后就可以根据这个求解最大公约数的递归式来写实现代码了:
#include <iostream>
int GCD(int m, int n) {
int t = 1;
while(t != 0) {
t=m%n;
m=n;
n=t;
}
return m;
}
int GCDRecursive(int m, int n) {
if (n == 0) return m;
return GCDRecursive(n, m % n);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
return 0;
}
int a = atoi(argv[1]);
int b = atoi(argv[2]);
if (a < b) {
int t = a;
a = b;
b = t;
}
int gcd = GCD(a, b);
printf("GCD(%d, %d)=%d\n", a, b, gcd);
gcd = GCDRecursive(a, b);
printf("GCD(%d, %d)=%d\n", a, b, gcd);
return 0;
}
运行结果:
欧几里得算法也可以推广到多项式上,和整数最大公因数类似的多项式上也有最大公因式的概念,一样也有整除和求余的概念。所以求解两个多项式的最大公因式一样也可以应用欧几里得算法[18]
。
首先下面通过3个例子来说明多项式除法[16,17]
是如何u操作的,这里引用资料[17]
对多项式除法规则的定义:
例子一、整除的情况, 除以 :
所以有 。
例子二、带余式的情况, 除以 :
所以有 。
例子三、带缺项的情况, 和 :
所以有 。
多元多项式的情况可以参考资料[17]
。
有了多项式除法的概念之后,用一个例子来说明多项式中的欧几里得算法[18]
,求 和 的最大公因式,同样利用性质 。
首先第一步计算 除以 得余式 :
然后用 去除以余式 ,可以整除 :
而因为 能被 整除,所以最大公因式是 。
直接对 和 做因式分解也能看出最大公因式:
但是因式分解看起来就很难用代码实现,而欧几里得算法用代码来实现相对容易。
在介绍扩展欧几里得算法之前先来看下“裴蜀等式”,下面引用wikipedia上的解释[19]
:
在数论中,裴蜀等式或裴蜀定理是一个关于最大公约数(或最大公因式)的定理。说明了对任何整数 、 和 ,关于未知数 和 的方程:, 有整数解时当且仅当 是 和 的最大公约数 的倍数,也就是要求 。
裴蜀等式有解时必然有无穷多个整数解,每组解 、 都称为裴蜀数,可用扩展欧几里得算法求解。
比如,12和42的最大公约数是6,则方程 。事实上有:
特别来说, 有整数解当且仅当 和 互素,即 。证明过程有兴趣的读者可以参考[19]
。
接着来看下如何用扩展欧几里得[13,20]
算法求解裴蜀等式,简单来说扩展欧几里德算法是对欧几里德算法的扩展,它可以用来求解形如 的方程的一组整数解。
我们可以从欧几里德算法的等式来实现扩展欧几里得算法:
我们先来看下方程 的边界情况,当 的时候,方程可化为 ,然后根据最大公约数的性质可知 ,所以可以解得 , 。
然后对于一般情况,也是应用最大公因数的性质 ,首先设 ,然后同样有方程
联合需要求解的方程可得
所以
整理一下式子可得
对比系数两边可得求解递归式
下面看下实现代码:
#include <iostream>
int exgcd(int a, int b, int &x, int &y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
int gcd = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - a / b * x;
return gcd;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
return 0;
}
int a = atoi(argv[1]);
int b = atoi(argv[2]);
if (a < b) {
int t = a;
a = b;
b = t;
}
int x, y;
int gcd = exgcd(a, b, x, y);
printf("%d * %d + %d * %d = %d\n", a, x, b, y, gcd);
return 0;
}
运行结果:
所以扩展欧几里得算法可以同时求出 方程的解和最大公因数 。
同样类似的扩展欧几里得算法也可以应用在求解多项式的裴蜀等式,假设现在已知有两个多项式 和 以及最大公因式 ,求解如下方程
下面举个例子说明如何用扩展欧几里得算法求解,还是用上面的例子,已知 和的最大公因式是 。
直接套用扩展欧几里得算法递归式:
代码的话可以参考[21]
,下面看下每一步的计算过程:
所以可得
两边同除以2可得
模逆元[22,23]
也称为模倒数。整数 对同余 的模逆元是指满足下面公式的整数 :
整数 对模数 的模逆元存在充分必要条件是 和 互素,也即 ,
所以有 ,可用扩展欧几里得算法求解。求得的 即为 关于模 的其中一个模逆元。事实上 都是 关于模 的模逆元,这里我们取最小的正整数解 ,这也很好理解,假设 是最小的正整数解,则有
代码:
#include <iostream>
int exgcd(int a, int b, int &x, int &y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
int gcd = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - a / b * x;
return gcd;
}
int reverse_unit(int a, int b) {
int x, y;
int gcd = exgcd(a, b, x, y);
if (gcd != 1) {
printf("reverse unit does not exist.\n");
return -1;
}
return (x % b + b) % b;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
return 0;
}
int a = atoi(argv[1]);
int b = atoi(argv[2]);
int reverse= reverse_unit(a, b);
if (reverse != -1)
printf("%d * %d = 1 (mod %d) \n", a, reverse, b);
return 0;
}
运行结果:
同理也可以应用扩展欧几里得算法求解多项式模的逆元,下面直接举例进行说明。求 的逆元,而因为:
所以有解。
构造方程
下面给出扩展欧几里得算法每一步的计算过程:
所以有
两边同除以24得
验证下
所以 的逆元是。
有了前面知识点的铺垫,理解中国剩余定理[24,25,26]
就容易多了。文章[24]
对中国剩余定理的解释非常透彻,下面对中国剩余定理的解释大部分是参考这篇文章。推荐对数学感兴趣读者可以关注该专栏,都是和数学相关的内容。这里先来看下“孙子算经”[27]
里面的第二十六题,原文如下:
今有物,不知其數。三三數之,賸二;五五數之,賸三;七七數之,賸二。
問:物幾何?
答曰:二十三。
術曰:
三三數之,賸二,置一百四十;
五五數之,賸三,置六十三;
七七數之,賸二 ,置三十。
并之,得二百三十三,以二百一十減之,即得。
凡三三數之,賸一,則置七十;
五五數之,賸一,則置二十一;
七七數之,賸一,則置十五。
一百六以上,以一百五減之,即得。
用通俗的语言描述第二十六题就是:
现在有一个整数,该整数除以3余2、除以5余3、除以7余2,求该整数是多少?
答案是:23
解法:
除以3余2,加140;
除以5余3,加63;
除以7余2,加30;
求和140+63+30=233,再减去210,就得到23。
只要是除以3余1,就加70;
只要是除以5余1,就加21;
只要是除以7余1,就加15;
然后累加,如果超过了106就减去105就得到结果了。
首先把这个问题转化为一个求解同余方程组的问题,然后对这个问题的解法就称为中国剩余定理:
就是要求解一个整数 x ,同时满足除3余2,除5余3和除7余2。
首先我们可以把问题分解一下,如果能找到3个整数 x1,x2,x3 ,分别满足:
那么 就是解,因为根据取模运算法则:
然后接着接续分解问题,如果能找到3个整数 y1,y2,y3 ,分别满足:
那么令
,,,
即可求得解 ,
因为根据取模运算法则有:
然后来看下怎么求解 。
首先求 ,因为其同时满足被5和7整除,所以一定是5和7的公倍数,也就是5x7=35的倍数,且除3余1,也就有 , 就把问题转化为求解35模3的逆元的问题,用上面讲到的扩展欧几里得算法就可以求出 ,将 记作 (口算可得 k=2 ),然后就可以求得:
刚好对应了原文 “三三數之,賸二,置一百四十;” 这一句。
接着求 ,因为其同时满足被3和7整除,所以一定是3和7的公倍数,也就是3x7=21的倍数,且除5余1,也就有 ,同样可以求得:
刚好对应了原文 “五五數之,賸三,置六十三;” 这一句。
最后求 ,因为其同时满足被3和5整除,所以一定是3和5的公倍数,也就是3x5=15的倍数,且除7余1,也就有 ,同样可以求得:
刚好对应原文 “七七數之,賸二 ,置三十。” 这一句。
然后求得:
最后注意到,如果 x 满足除以3余2、除以5余3、除以7余2,那么 也同样满足,这个结论应用取模运算法则推导一下就知道是正确的。因此要计算满足要求的最小的非负整数,就只需要按照上面的方法计算得到总和之后,再除以105得到的余数就是最小的非负整数。
而 , ,所以答案就是 ,23满足除以3余2、除以5余3、除以7余2。
刚好对应了原文 “并之,得二百三十三,以二百一十減之,即得。”
然后假设如果存在整数 都满足 “除以3余a、除以5余b、除以7余c” 。则根据取模运算法则有:
所以 满足 “除以3余0、除以5余0、除以7余0” ,因此 一定是 3,5,7的公倍数,也就是 的倍数。这也就说明,在“模105同余”的意义下,之前通过分解问题、组合解答的方法所得到的 恰恰就是唯一解。
把这个问题推广到一般情况,假设整数 两两互素,则对于任意的整数,同余方程组
都存在整数解,而且若都满足该方程组,则必有,其中。而可以下面公式求解:
这就是中国剩余定理,如果弄懂了上面孙子算经的题目,应该就很容易理解这个求解公式了。
简单描述下一般情况求解 x 过程:
首先分别找到 的公倍数 ,满足除以 余 1,然后 即可,而求解 就相当于先求 模 的逆元,然后再乘以,用扩展欧几里得算法求解即可,最终把所有 加起来再模 就得到结果了。
代码:
#include <iostream>
int exgcd(int a, int b, int &x, int &y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
int gcd = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - a / b * x;
return gcd;
}
int get_crt(int *a, int *m, int len) {
int r, y;
int N = 1;
for (int i = 0; i < len; ++i) {
N *= m[i];
}
int X = 0;
for(int i=0; i<len; ++i) {
int Mi = N / m[i];
int gcd = exgcd(Mi, m[i], r, y);
X += a[i] * Mi * r;
}
return X % N;
}
int main(int argc, char *argv[]) {
int m[3] = {3, 5, 7};
int a[3] = {2, 3, 2};
int X = get_crt(a, m, 3);
printf("crt = %d \n", X);
return 0;
}
运行结果:
类似的中国剩余定理同样可以应用到多项式上,下面参考[28]
给出多项式版本的中国剩余定理的定义:
假设存在理数系数的多项式 它们之间两两互素,则对于任意的有理数系数的多项式,同余方程组
都存在有理数系数的多项式解,且若都满足该同余方程组,则必有,其中。
而求解方法与整数中国剩余定理类似:
简单描述下一般情况求解 过程:
首先分别找到 的公倍式 ,满足除以 余 1,然后 即可,而求解 就相当于先求 模 的逆元,然后再乘以,应用扩展欧几里得算法求解即可,最终把所有 加起来再模 就得到结果了。
下面举个实际例子,假设有如下同余方程组:
先求 , 然后利用扩展欧几里得算法求得逆元 和
然后套公式
验证结果
终于到了本文最重点的部分了,在开始看本节之前确保已经理解了前面提到的数学知识。通过前面的介绍我们已经知道了卷积操作等价于多项式乘法,下面简要描述下卷积是怎么和中国剩余定理的产生联系的,这也是我理解的Winograd这个算法的核心。需要注意的是下面的一些结论是我根据实际例子比如和推导得到的结论不一定正确。
假设现在有两个离散序列 和 做卷积操作,首先把这两个序列转化为两个多项式和。
和 最高分别为 和 次,然后卷积操作就可以变为多项式乘法 ,最高次为 。我们先有个概念就是Winograd是一个构造式的算法,是人为去构造一个计算 的等价变换,下面介绍如何构造。首先构造 个互素的多项式 ,假设是 ,然后有
则 可以表示为 ,因为 与 同次 所以余式 次数小于 ,而为啥商是 是因为 ,而 ,所以应用多项式除法,商就是 ,余式就是 所以问题就转化为求余式的问题。
然后现在已知 和 ,所以可以求得 和 除以这些互素多项式的余式
接着根据取模运算法则有
然后因为 可以被 整除,所以有
然后求余式 就变成求解同余方程组的问题,
就可以套用中国剩余定理去求解 :
所以原来的多项式乘法就化为:
再套用到具体情况比如2x3, 4x3卷积的时候,如果变换之后等式右边的所需的乘法次数小于 的乘法次数就能达到加速的目的
现在来看下具体到的变换矩阵是如何得到的。首先假设有两个长度是2和3的离散序列 , 和它们等价的多项式表示 和,。
然后相乘的结果:
所以从系数的计算上可以看出总共有6次乘法和2次加法。
然后构造2+3-2=3个互素多项式, ,它们的乘积 。所以有
然后求 除以这3个互素多项式的余数:
然后就可以得到关于 的同余方程组:
然后套用中国剩余定理,首先求逆元 ,用扩展欧几里得算法求解
求解过程:
相当于求解方程 的解
所以
求解过程:
相当于求解方程 的解
所以
求解过程:
相当于求解方程 的解
所以
然后套用中国剩余定理求解 :
所以
所以用上式替代了原始下式的计算:
然后我们把新的式子需要做乘法的地方写在下面:
所以就是4次乘法和9次加法,除2的操作的开销可以在实际应用的时候把除2操作放到权值变换那里,就可以把运行时的开销去掉了。可以看到比原来的6次乘法和2次加法,少两次乘法,但是加法次数变多了。
然后看下如何提取出变换矩阵,首先对公式作一些改动,把除2操作移动到 的计算里面:
通过观察上面的式子,就能抽取出序列 , 各自的变换矩阵 ,还有最后输出变换矩阵 :
然后就可以用以下公式计算 的系数了:
“*” 表示矩阵向量乘法而“ ”表示向量点乘,下面验证下:
所以 结果 为:
和直接卷积结果一致。
然后来看下这个变换是如何应用到深度学习中的卷积(Correlation)里面的,对于的应用,是用在1x3或者3x1卷积里面,长度是3的卷积核连续卷积两次得到两个输出,输入序列长度是4,刚好是把Winograd的变换矩阵反着来用的,为了和上面的公式对应,这里用 分别表示,输入,权值和输出:
把矩阵 A 和 B 做转置:
验证下,假设输入 ,权值 和输出 ,直接做correlation的结果是:
然后验证下
所以 结果为:
结果与直接做Correlation一致。
其实这里有一点没想明白的地方是,卷积操作中的Winograd变换公式是如何变成用在Correlation中变换公式的,直接推导的话推不出来,感觉中间还缺了一环,但是确实结论是正确的,实际推导结果也正确。
再来看下的变换矩阵是如何得到的。首先假设有两个长度是4和3的离散序列 , 和它们等价的多项式表示 和 。
然后相乘的结果:
所以从系数的计算上可以看出总共有12次乘法和6次加法。
然后构造4+3-2=5个互素多项式:
所以它们的乘积 所以有
然后求 除以这5个互素多项式的余式:
然后就可以得到关于 的同余方程组:
然后套用中国剩余定理,首先求逆元 ,用扩展欧几里得算法求解
求解过程:
相当于求解方程 的解
所以
求解过程:
相当于求解方程 的解
所以
求解过程:
相当于求解方程 的解
所以
求解过程:
相当于求解方程 的解
所以
求解过程:
相当于求解方程 的解
所以
然后套用中国剩余定理求解 :
所以
所以用上式替代了原始下式的计算:
然后我们把一些关键的公式重新列一下:
通过观察上面的式子,就能抽取出序列, 各自的变换矩阵 ,还有最后输出变换矩阵 :
然后就可以用以下公式计算 的系数了:
同样“*” 表示矩阵向量乘法而“ ”表示向量点乘,因为 矩阵的每一列只会与 的对应行相乘,更进一步相当于只会与 的对应行相乘,所以可以把 矩阵的每一列抽取公因子,然后乘到 的对应行上, 而 每一列的公因子为 ,然后把公因子乘到 对应行,则得到新的矩阵:
最后就可以得到参考文献[5]
中给出的 变换矩阵。
终于写完了,真的是第一次写那么长的博客,而且公式也比较多,如果有哪里写的不对或者公式错误的地方,请读者见谅。