问题背景与免责声明

  因为我只是个 python 菜鸟。所以写本文的初衷是讨论和求教。先透露个底细。我 python 的总代码量不足 10000 行。很可能不到我 c/c++ 代码量的十分之一。也没有用 python 写过任何算法和数据结构。所以对 python 各部分的效率并不十分清楚。故而这也正是我想求教的一个重点。如发现任何错误,或愚蠢的想法。望不吝赐教。

  本文的范例用了 Tensorflow。这并非是要讨论任何和机器学习有关的内容。仅仅是因为 Tensorflow 提供了对 Tensor 的支持。比起只能算矩阵乘法。显然该包让后续讨论,有了更广阔的测试场景。如果有 anaconda3 可以一键搭一个和我一样的环境

1
2
conda create -n tensorTest python=3.5 tensorflow
. activate tensorTest

  最后想做一个免责声明。我写任何语言都并没有遵照任何编程规范。而是采用了一套我自己发明的规范。这并非是我狂妄自大。而是因为我的视力有问题。因为双目弱视。所以即使佩戴眼镜。双目同时测试。也难以达到 4.8。其中一只眼睛更是无论戴何种镜片都看不到 4.6。所以在写程序时。如保持正常明视距离。其实并不能看清楚自己在写什么。只能看到大概轮廓。而我小时候学程序,并养成这种书写习惯时。更是必须把眼睛放到屏幕的 15 cm 以内才能看清屏幕。类似地。打星际和魔兽也看不到自己钱或矿的数值。只能约略猜出有几位数。所以在写程序时。我比别人加入了更多的空格,更多的 \t,更多的 \n。请尝试理解一位身残志坚的程序员。如果实在看着想吐。可以使用 1,$s/\ ,\ /,\ /g等技术,或更加高科技的正则表达式。感谢对残疾人的理解和支持。有时,人们觉得触及手机屏幕时,又震动又带语音非常愚蠢。但那却是对残障人士最温暖的关怀。

下面进入正题

  什么是张量呢。我不太清楚计算机科学和数学中的定义。因为自己是野鸡专业出身的。N 阶张量是能把 N 个矢量映射到实数集的量。如果把矢量用行列向量写出分量。那么张量的分量也就成了 N 阶矩阵。Tensorflow 提供的张量运算并不只是保存了运算出结果的矩阵元。而是事实上保存了张量间的运算【映射】关系。直到在 Sessionrun。才真正计算出结果。所以其实我觉得这个包在机器学习之外可能还有更广阔的用途。下面我会用我们野鸡专业的术语,描述本应高端,大气,上档次,的机器学习程序包。

后面的程序会用到三个包

1
2
3
import tensorflow as tf
import functools as ft
from numpy.random import RandomState as rnd

定义一些可能用到的张量

1
2
3
4
5
6
7
8
#定义两个分量呈标准正态分布的张量。方括号内是维数。固定种子方便比较输出结果。
w1 = tf.Variable( tf.random_normal( [ 2 , 3 ] , seed = 1 ) )
w2 = tf.Variable( tf.random_normal( [ 3 , 1 ] , seed = 1 ) )
#定义矢量空间的态矢量。其维数是 2。第一个 None 表示可以一起放入任意多个维数为 2 的矢量。
x = tf.placeholder( tf.float32 , shape = ( None , 2 ) , name = "xin" )
#dataSize 表示上面的 None 到底要塞进去多少矢量。下面会从矢量集 X 中取出矢量 x。
dataSize = 1
X = rnd(1).rand( dataSize , 2 )

如果我们要实现一系列的矩阵乘法。但永远只能通过如下方法实现

1
2
3
A = tf.matmul( x , w1 )
y = tf.matmul( A , w2 )
#或者 y = tf.matmul( tf.matmul( x , w1 ) , w2 )

就可能产生和我一样的一种情感。为什么我们不去写 C 呢。至少飞快。所以我想,如果我能像写数学表达式一样写矩阵乘法,乃至一切张量运算。岂不是很开心。

$$y^{(i)} = x^{(i)}w^{(1)}w^{(2)}$$

  我听说 python 支持 mapreduce 两种操作。可以参考 gg 的文章 MapReduce: Simplified Data Processing on Large Clusters。我发现的第一个问题是,mapreduce 方向相反。map 是符合数学方向的。而 reduce 是反过来的。map 的两个参数表达的是左面的函數作用到右面的 list 上。即向右操作。这和数学一致。但 reduce 则不然。

$$reduce( f , [x_1,x_2,x_3] )\equiv f( f(x_1,x_2),x_3)$$

也就是说 reduce 是向左操作的。我尝试自己写了一些向右操作的 reduce。如

1
2
3
4
5
def reduceR( f , x ):
if len(x) == 2:
return f( x[0] , x[1] )
else:
return f( x[0] , reduceR( f , x[1:] ) )

但发现比 reduce 慢不少。因为不知道人家的 reduce 到底如何实现。所以就干脆把 map 也改成了向左运算。在统统使用向左的约定后。矩阵链乘就非常简单了。

1
matCh = lambda x : ft.reduce( tf.matmul , x )

写出来和数学表达式看着很相似

1
y = matCh([ x , w1 , w2 ])

但显然我还希望有更一般的张量表达式。如

$$f:g:x\equiv f(g(x))$$

因为已经改了向左运算所以事实上希望得到的是

$$x\leftarrow f\leftarrow g\equiv g(f(x))$$

这样就需要组合使用 mapreduce。需要注意。与 python2 不同。python3 的 map 不再返回一个 List 而是一个 Iterator。故而。最后需要用 [*]Iterator 的内容释放出来。注意下面的代码中用颠倒的 map,即 lambda x , y : map( y , x ),代替了右向的 map

1
2
3
opt = lambda z : [ *ft.reduce( lambda x , y : map( y , x ) , z ) ]
act = lambda z : [ *ft.reduce( lambda x , y : map( y , x ) ,
[ z[:1] ] + z[1:] ) ][0]

其中 act 比较特殊。只能针对一个单独的元素。但是 opt 却能一般地作用在一个 List 中的所有元素上。定义了这两个泛函【将函数映射至实数集】后。理想的数学表达式就得到了。

$$act( x , f , g )\equiv g(f(x))$$

$$opt([x_1,x_2] , f , g )\equiv [ g(f(x_1)) , g(f(x_2))]$$

但还有一个小问题是要把矩阵乘法也写成一种操作

1
matAct = lambda y : lambda x : tf.matmul( x , y )

这样就得到了看起来都非常数学,非常自然的等价表达式

$$matCh(\ x,\ w^{(1)},\ w^{(2)}\ )\Leftrightarrow act(\ x ,\ matAct(w^{(1)}) ,\ matAct(w^{(2)})\ )$$

可以随手写几个测试下。

1
2
3
4
5
y1 = opt([ [ matCh([ x , w1 , w2 ]) ] , tf.nn.relu ])[0]
y2 = act([ x , matAct(w1) , matAct(w2) , tf.nn.relu ])
y3 = opt([ [ x , x ] , matAct(w1) , matAct(w2) , tf.nn.relu ])[0]
y4 = opt([ [ x , x ] , matAct(w1) , matAct(w2) , tf.nn.relu ])[1]
y5 = act([ matCh([ x , w1 , w2 ]) , tf.nn.relu ])

会发现这些表达式都是以,和数学表达式相同,的形态呈现出来的。只需要构造出 optact 所引用的 [] 就能表达一切张量运算。这样就把面向过程的计算变成了构造一个 List。这种写法有很多好处。但其基本要求是性能不能打太大折扣。性能方面因为我没有搞清楚 MapList 的机制。所以还有待研究和讨论。但单从书写方便角度讲。四条非常固定的定义已经带来了巨大好处。

  最后。如上所述。Tensorflow 进行的计算仅仅是张量运算。需要在 Session 中运行才能得到结果。

1
2
3
4
5
6
7
8
9
10
11
12
with tf.Session() as sess:
sess.run( tf.global_variables_initializer() )
print( "\nw1" )
print( w1.eval() )
print( "w2" )
print( w2.eval() )
print( '' )
print( y1.eval( feed_dict = { x : X } ) )
print( y2.eval( feed_dict = { x : X } ) )
print( y3.eval( feed_dict = { x : X } ) )
print( y4.eval( feed_dict = { x : X } ) )
print( y5.eval( feed_dict = { x : X } ) )

会发现五个看起来大同小异的表达式确实给出了相同的结果。而且 dataSize 改大以后。会自动得到多组 y 输出。表达式就和我们对数学表达式的期待一样。完全不需要任何改变。

  上面写的内容很可能幼稚,无聊,纯属无用功。但还是希望坚持读完的朋友能够提出一些批评。更希望有高手指点下效率问题。该讨论的动机确实很无聊。就是想把乱七八糟的面向过程程序。写成和数学表达式一样的泛函表达式。