编写: @_散沙_Python玩家_

Concrete Math with Python III

Python带你游览具体世界的数学基础

一般来说,高深的数学总会让人有种抗拒心理,但跟具体世界紧密联系的简单数学知识会则不容易让人有如下的疑问:

"我们知道这些知识到底有什么用处?"

这些数学知识第一次出现在高中、大学课本时可能会让人摸不到头脑,而他们往往是基础的计算机科学、统计学、经济学知识的基石。

这部分内容包括:

  • 数列与金融基础
  • 排列组合与概率基础
  • 方程组/矩阵
  • 线性规划
  • 质数与加密
  • 随机数与概率分布

注意:具备扎实的高中数学、理工科本科数学基础的朋友可以简单阅览或者跳过这一部分。


In [1]:
import itertools as itt
import numpy as np
import numpy.random as nprnd
import scipy.stats as spstat
import scipy.special as ssp
import pandas as pd
import bokeh
import bokeh.plotting as bplt
import bokeh.palettes as bpal

In [2]:
# 工具配置
TOOLS = "pan,wheel_zoom,box_zoom,box_select"
# 作图配置
plot_config = dict(plot_width=800, plot_height=500, tools=TOOLS)
bplt.output_notebook()


BokehJS successfully loaded.

1 矩阵的用处:多元一次方程组

我们先了解一下矩阵最重要也是最基本的工程用途:通过矩阵求解多元一次方程组,这已经成功应用于农业(?)和金融业。

本节对于矩阵的相关概念、计算细节都不做任何解释或者证明。

鸡兔同笼

鸡兔同笼问题是经典的小学竞赛题:

  • 小明家养了很多鸡和兔子。
  • 只数头的话,发现有20个头。
  • 只数脚的话,发现有62只脚。

问鸡和兔子分别有多少?

高斯消去法

把$y$写成关于$x$的表达式并代入的方法的本意,是用来得到一个消去$y$的式子从而求得$x$,再根据$x,y$的关系求得$y$

  • 第一步: $$ x+y = 20\\ 4x+2y = 62 $$

  • 第二步,用第二个式子减去2倍的第一个式子,然后扔掉第一个式子: $$ 2x = 22\\ 4x+2y = 62 $$

后面的故事还是很熟悉,但是我们为什么可以扔掉第一个式子呢?因为 新来的第一个式子和旧有的第二个式子可以恢复出旧有的第一个式子,因此我们能够利用的信息并没有变多,也没有变少。

  • 第三步,第一个式子缩水成为原来的1/2: $$ x = 11\\ 4x+2y = 62 $$

  • 最后一步,第二个式子先减去第一个式子,再缩水到1/2:

$$ x = 11\\ y = 9 $$

矩阵法

  • 高斯消去法可以根据方程组系数来制定求解方程组的计划了。实际上,我们可以方便的用以下的矩阵乘法来表示:
$$\begin{bmatrix} 1 & 1 \\ 4 & 2 \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} 20\\ 62 \end{bmatrix}$$

根据上式推出

$$\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} 11\\ 9 \end{bmatrix}$$

左边方框包住的方方正正的东西就是一个2*2的系数矩阵了。中间未知数代表的矩阵只有1列所以也可以叫列向量,右边的1列也是列向量。简化一下:

$$Ax = b$$

为了求得未知数列向量$x$,我们可以直接使用:

$$x = A^{-1}b$$

来得到,这就简单多了!


In [3]:
A = np.mat([[1,1],[4,2]])
A_Inv = A.I
b = np.array([20,62])
A_Inv.dot(b)


Out[3]:
matrix([[ 11.,   9.]])

方程组是否有唯一解?

上面的方法仔细想想有一个问题:

  • 首先,矩阵不一定是正方形的,在这种情况下我们没有办法使用求逆的手段来直接求解方程组。
  • 其次,就算矩阵是正方形的,在某些情况下我们在矩阵求逆的时候还是会报错,比如:
$$x + 3y = 10\\ 4x + 12y = 40(或者41)$$

In [4]:
A = np.mat([[1,3],[4,12]])
A_Inv = A.I


---------------------------------------------------------------------------
LinAlgError                               Traceback (most recent call last)
<ipython-input-4-4ca29d4a1a16> in <module>()
      1 A = np.mat([[1,3],[4,12]])
----> 2 A_Inv = A.I

/Users/wangweiyang/anaconda/anaconda/lib/python2.7/site-packages/numpy/matrixlib/defmatrix.pyc in getI(self)
    960         else:
    961             from numpy.dual import pinv as func
--> 962         return asmatrix(func(self))
    963 
    964     def getA(self):

/Users/wangweiyang/anaconda/anaconda/lib/python2.7/site-packages/scipy/linalg/basic.pyc in inv(a, overwrite_a, check_finite)
    675         inv_a, info = getri(lu, piv, lwork=lwork, overwrite_lu=1)
    676     if info > 0:
--> 677         raise LinAlgError("singular matrix")
    678     if info < 0:
    679         raise ValueError('illegal value in %d-th argument of internal '

LinAlgError: singular matrix
  • 如果第二个式子中等号右边是40,第二个式子就和第一个式子的4倍完全一样,相当于这是一个2元方程,但是有无穷多个解
  • 如果是41,那么这两个式子直接就矛盾了。

这种情况下,矩阵的某些行通过加减乘除的组合能够完全得到另一些行,称这个矩阵是线性相关的,也可以称作奇异(singular)

最终的结果是:去掉那些多余的行之后,

  • 矩阵行数<列数:方程有无穷多个解,可以使用特征值分解法求出右手侧为0时的解,找出特征值为0时对应的向量即可。
  • 矩阵行数=列数,方程刚好有唯一解(右手侧为0时,恰好有所有未知数全部为0的解)
  • 矩阵行数>列数,现在方程组是矛盾的,我们能通过求解矩阵广义逆的方式获取所谓的唯一解。这种方法也被称为最小二乘法。

多元一次方程组的应用

情况1:矩阵行数<列数:构建市场中性组合

假设我们要建立一个关于某只股票X的组合,要求这个组合对于X股价的一阶以及二阶变化都不敏感。组合包含:

a份股票 $\Delta = 1.0, \Gamma = 0.0$

b分某种看涨期权 $\Delta = 0.1, \Gamma = 0.05$

c份某种看空期权 $\Delta = -0.2, \Gamma = 0.15$

$$\begin{bmatrix} 1.0 & 0.1 & -0.2 \\ 0.0 & 0.05 & 0.15 \end{bmatrix} \begin{bmatrix} a \\ b \\ c \end{bmatrix} = \begin{bmatrix} 0\\ 0\\ 0 \end{bmatrix}$$

In [5]:
# 先补齐最后一行
A = np.array([[1,0.1,-0.2],[0,0.05,0.15],[0,0,0]])
d,U = np.linalg.eig(A)
D = np.diag(d)
print D
print U


[[ 1.    0.    0.  ]
 [ 0.    0.05  0.  ]
 [ 0.    0.    0.  ]]
[[ 1.         -0.10468478  0.15617376]
 [ 0.          0.99450545 -0.93704257]
 [ 0.          0.          0.31234752]]

这时我们有:$AU = UD$

其中,$U$的每一个列向量$v$在左边乘以了$A$之后都变成了$v$自己的倍数。

$D$是$A$这个矩阵特征值组成的对焦矩阵,$D$里面的每一项,都顺序地对应着$U$的每一个列向量。我们称$U$的列向量是

我们要把$Av = 0*v = 0$的向量找到,也就是:


In [6]:
U[:,2]


Out[6]:
array([ 0.15617376, -0.93704257,  0.31234752])

至此我们知道,15.6份股票、做空93.7份看涨期权与买入31.2份看跌期权可以实现我们构建市场中性组合的目的。

情况2:矩阵求逆法可解,略

情况3:矩阵行数>列数:预测GDP

  • $I$: 发电总量为0时的基准GDP
  • $E$: 总发电量
$$\begin{bmatrix} 1.0 & 1.0 \\ 1.0 & 0.7 \\ 1.0 & 0.9 \\ 1.0 & 1.1 \end{bmatrix} \begin{bmatrix} I \\ E \end{bmatrix} = \begin{bmatrix} 3500\\ 2700\\ 3600\\ 3350 \end{bmatrix}$$

简称 $Ax = b$

可以看到,模型很粗糙,甚至还有矛盾,比如(3),(4)式的平均无法得出(1)式。

不过不要紧,我们给出一个$x = (A^TA)^{-1}A^T b$的解


In [7]:
A = np.array([[1.0,1.0],[1.0,0.7],[1.0,0.9],[1.0,1.1]])
b = np.array([3500,2700,3600,3350])

x = (np.mat(A.T.dot(A)).I).dot(A.T).dot(b.reshape(4,1))
b_ = A.dot(x)

print x
print b_


[[ 1688.57142857]
 [ 1728.57142857]]
[[ 3417.14285714]
 [ 2898.57142857]
 [ 3244.28571429]
 [ 3590.        ]]

这使得:

  • $Ax$无法精确的等于$b$,但等于$b$在矩阵$A$列向量构成空间上的投影$b'$: $ Ax = A(A^TA)^{-1}A^T b = b' $
  • 从另一个意义上能得出一个也还凑合的过去的等式:$ A^TAx = A^Tb $
  • 不加证明的指出,为了使得$b-\hat{b}$误差的平方和最小,推出的结果就是这个解,也叫最小二乘法的解。

情况3扩展阅读:股票收益率的因子模型 https://en.wikipedia.org/wiki/Fama–French_three-factor_model

其建模方法论可以简单的利用情况3的代码亲自实践。

2 有趣的矩阵:基础篇

自从人们发现可以将一个方形的一组数据抽象成为矩阵的概念之后,很多计算就变得更为简化了。这一节我们就了解一下矩阵有趣而且重要的性质。

如下这个矩阵,我们已经知道

$$A = \begin{bmatrix} 5 & 0 & -2 \\ 3 & 1 & 1 \end{bmatrix}$$

矩阵的元素与切片

我们用$A_{ij}$来表示矩阵A的第i行,第j列个元素。在Python语言中,$i,j$的起点都是0。

在NumPy中,不涉及计算的时候我们可以用np.array或者np.mat来表示矩阵。


In [8]:
A = np.array([[5,0,-2],[3,1,1]])

print A[0,1],A[1,0]


0 3

我们平日里习惯称矩阵$A$的第1行第2列元素为0,第2行第1列元素为3,就这样被取出来了。上图我们还可以看出使用了双重方括号来表示矩阵。

分别看一下矩阵A按照行列范围切出来的部分与直接取出一行/一列有什么区别:

  • 里面的数据展开之后是一样的
  • 按范围切出来的部分还是双重方括号,我们可以认为切出了一个一行或者一列的矩阵,或者切出来的向量是保留了排列方向的。

In [9]:
print A[0:1,:]
print A[0,:]


[[ 5  0 -2]]
[ 5  0 -2]

In [10]:
print A[:,0:1]
print A[:,0]


[[5]
 [3]]
[5 3]

矩阵的基本运算

矩阵的加法或者标量(单个数字)乘法是简单易懂的:

  • 要想相加,矩阵必须形状一致
  • 要想做标量乘法,$2A=A+A$
$$A = \begin{bmatrix} 5 & 0 & -2 \\ 3 & 1 & 1 \end{bmatrix}, B = \begin{bmatrix} 0 & 3 & 3 \\ -2 & 7 & -1 \end{bmatrix}$$

In [11]:
B = np.array([[0,3,3],[-2,7,-1]])

print A
print B
print A+B
print A+A
print 2*A
print 1.5*A


[[ 5  0 -2]
 [ 3  1  1]]
[[ 0  3  3]
 [-2  7 -1]]
[[5 3 1]
 [1 8 0]]
[[10  0 -4]
 [ 6  2  2]]
[[10  0 -4]
 [ 6  2  2]]
[[ 7.5  0.  -3. ]
 [ 4.5  1.5  1.5]]

如上,读者可以自己验证矩阵加法满足交换律与结合律:

$A+B=B+A,(A+B)+C=A+(B+C)$

矩阵的乘法虽然看起来不那么易懂,但就是从方程组抽象而来:

中俄边境有一些农贸市场,也有很多风格的餐馆。

农贸市场中商品的标价是以两种货币的计价(汇率有微妙的不同,因为两边商品的保存与生产成本不一样)

  • 鱼:60卢布/10人民币 每斤
  • 辣椒:30卢布/3人民币 每斤
  • 猪肉:120卢布/8人民币 每斤

两家中俄连锁饭店菜品不一样,因此每个月进货也是不一样的:

  • 餐馆A: 500斤鱼,200斤辣椒,200斤猪肉
  • 餐馆B: 100斤鱼,100斤辣椒,700斤猪肉

假设餐馆A要以人民币计价进行采购,我们分别列出价格矩阵和数量矩阵:

$$P = \begin{bmatrix} 10 & 3 & 8 \end{bmatrix} , Q = \begin{bmatrix} 500 \\ 200 \\ 200 \end{bmatrix}$$

In [12]:
P = np.array([[10,3,8]])
Q = np.array([[500],[200],[200]])

print P.dot(Q)
print 10*500+3*200+8*200


[[7200]]
7200

总结一下矩阵乘法的规律:当$R = PQ$时,有

$$R_{ij} = \sum_{k} P_{ik}Q_{kj}$$

矩阵之所以很方便,就是因为,我们可以一次性的计算出2家餐馆分别以2种货币进货时所需要付出的代价:

我们刚才知道以人民币计价餐馆A每个月进货需要7200元。


In [13]:
P = np.array([[60,30,120],[10,3,8]])
Q = np.array([[500,100],[200,100],[200,700]])
print P.dot(Q)


[[60000 93000]
 [ 7200  6900]]
  • P的第一行、第二行分别是卢布计价、人民币计价的商品价格
  • Q的第一列、第二列分别是餐馆A、餐馆B所需要的商品数量

如果我们想知道以卢布计价餐馆B每个月的进货成本,直接可以从矩阵里看出是93000卢布。

注意

虽然矩阵乘法很方便,但是$AB = BA$并不是能够始终被满足的

  • 一方面,当$A,B$不为方阵的时候,$AB$相乘必须保证$A$的列数等于$B$的行数,因此$BA$可能无法相乘,或者和$AB$的行列数不一样。
  • 另一方面,即使$A,B$为方阵,$AB=BA$在大部分时间也不成立,如:

In [58]:
A = np.array([[1,2],[6,-2]])
B = np.array([[2,3],[-5,1]])

In [59]:
print A.dot(B)


[[-8  5]
 [22 16]]

In [60]:
print B.dot(A)


[[ 20  -2]
 [  1 -12]]

矩阵的转置

矩阵的转置比较容易理解:$A^T_{ji} = A_{ij}$,也就是按照对角线镜像了一下。


In [61]:
print A
print A.T


[[ 1  2]
 [ 6 -2]]
[[ 1  6]
 [ 2 -2]]

同时根据矩阵加法和乘法的定义,能够知道:$$(A+B)^T = A^T+B^T\\ (AB)^T = B^TA^T$$


In [62]:
print (A+B).T
print A.T+B.T


[[ 3  1]
 [ 5 -1]]
[[ 3  1]
 [ 5 -1]]

In [63]:
print (A.dot(B)).T
print B.T.dot(A.T)


[[-8 22]
 [ 5 16]]
[[-8 22]
 [ 5 16]]

张成,线性无关,基和维数

还是从鸡兔同笼的问题开始看:假设有x只兔子,y只鸡:

$$\begin{bmatrix} 1 & 1 \\ 4 & 2 \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} 22\\ 62 \end{bmatrix}$$

如果我们把如上的矩阵写成$Ax = b$,可以看出,矩阵$A$中,第一列代表兔子的头/腿数,第二列代表鸡的头/腿数。改变写法

$$A[:,0]x+A[:,1]y = b$$

可以看到,$b$是$A$中列向量乘以标量之和。此时,

  • 我们就称$A$的列向量能够张成$b$
  • 以及$A$的列向量和$b$在一起是线性相关的。如果像前面预测GDP的例子,$A$的列向量和$b$就是线性无关的(因为永远无法给出$Ax=b$的精确解)。
  • $A$中一组线性无关的向量能够张成$A$中任意一列向量,可以称这组向量为$A$的一组基
  • 这组基的数目被称为$A$张成空间的维数或者秩(rank), 对于$M$行$N$列的矩阵$A$,$Rank(A)\leq min(M,N)$

In [17]:
A = np.array([[1,1,22],[4,2,62]])
np.linalg.matrix_rank(A)


Out[17]:
2

空间几何形态与行列式

我们稍微想的复杂一点:如果把矩阵乘法想成是将一个空间向量映射成另一个空间向量的函数。

$e1$和$e2$是两个简单的列向量,$A$矩阵左乘它们之后,就把它们映射成为$A$矩阵自己的列向量了。


In [18]:
A = np.array([[4,1],[1,2]])
e1 = np.array([[1],[0]])
e2 = np.array([[0],[1]])

In [19]:
c1 = A.dot(e1)
print c1


[[4]
 [1]]

In [20]:
c2 = A.dot(e2)
print c2


[[1]
 [2]]

In [21]:
p1 = bplt.figure(title='向量与面积的映射',background_fill='#FFFFFF',**plot_config)
p1.line(*zip(np.array([0,0]),e1.flatten()),line_width=3)
p1.line(*zip(np.array([0,0]),e2.flatten()),line_width=3)
p1.patch(*zip(np.array([0,0]),e1.flatten(),(e1+e2).flatten(),e2.flatten()),alpha=0.3)
p1.line(*zip(np.array([0,0]),c1.flatten()),line_width=3,line_color='darkred')
p1.line(*zip(np.array([0,0]),c2.flatten()),line_width=3,line_color='darkred')
p1.patch(*zip(np.array([0,0]),c1.flatten(),(c1+c2).flatten(),c2.flatten()),alpha=0.3,color='red')

bplt.show(p1)


经过简单的切割法,我们知道向量$e1,e2$经过矩阵$A$的映射之后,形成的红色平行四边形的面积是蓝色正方形的7倍。

这就涉及到一个概念:行列式


In [22]:
A1 = np.array([[4,1],[1,2]])
print np.linalg.det(A1)


7.0

In [23]:
A2 = np.array([[1,4],[2,1]])
print np.linalg.det(A2)


-7.0

行列式的意思就是:一个矩阵把一个平行四边形映射之后放大的面积。

我们注意到如果交换了$A1$的两列变成了$A2$,则行列式变成了负数。

原本$e1,e2$是逆时针排列的,映射后$c1,c2$变为顺时针排列,因此行列式要带负号。

而且从这种映射关系我们了解到,尽管矩阵乘法无法对于任意$A,B$满足$AB=BA$,但是可以满足

$$det(AB) = det(A)det(B)=det(B)det(A)=det(BA)$$

尽管刚才我们都在计算2阶矩阵(2*2矩阵)的行列式,这个规则同样也适用于3阶甚至高阶的矩阵

  • 每一个N阶矩阵都可以被视作一个空间映射,这个映射把一个N阶空间中的小立方体(超立方体)映射为平行多面体后,体积放大的倍数就是矩阵的行列式
  • 当映射之后,体积放大倍数为0倍时,说明矩阵的列之间是线性相关的,以至于把互相垂直的$e_1,e_2,...e_n$映射到了低维空间中。

特殊矩阵:对角矩阵

对角矩阵是一种特殊的方形矩阵

  • 顾名思义,仅仅在对角线上有值,其他位置元素都是0
  • 当我们把它视作一个映射时,我们可以把矩形仍然映射为矩形,在平行于坐标轴的方向上进行伸缩
  • 根据上一个概念,我们容易知道对角矩阵的行列式就是对角线上元素的连乘

例如,$$D = \begin{bmatrix} 5 & 0 \\ 0 & 3 \end{bmatrix}$$


In [49]:
D = np.array([[5,0],[0,3]])
e1 = np.array([[1],[0]])
e2 = np.array([[0],[1]])
c1 = D.dot(e1)
c2 = D.dot(e2)

p1 = bplt.figure(title='对角矩阵对向量与面积的映射',background_fill='#FFFFFF',**plot_config)
p1.line(*zip(np.array([0,0]),e1.flatten()),line_width=3)
p1.line(*zip(np.array([0,0]),e2.flatten()),line_width=3)
p1.patch(*zip(np.array([0,0]),e1.flatten(),(e1+e2).flatten(),e2.flatten()),alpha=0.3)
p1.line(*zip(np.array([0,0]),c1.flatten()),line_width=3,line_color='darkred')
p1.line(*zip(np.array([0,0]),c2.flatten()),line_width=3,line_color='darkred')
p1.patch(*zip(np.array([0,0]),c1.flatten(),(c1+c2).flatten(),c2.flatten()),alpha=0.3,color='red')

bplt.show(p1)



In [50]:
np.linalg.det(D)


Out[50]:
15.0

方形矩阵的可逆性

不加证明的给出:

  • 方形矩阵对于一个小立方体映射后,如果放大倍数不为0,说明矩阵列之间线性无关,也就说明映射时信息没有丢失。
  • 既然信息没有丢失,我们总能找到另一个矩阵,把刚才被扭曲放大的立方体再映射回这个小立方体
  • 之前的方形矩阵$T$称为可逆的,而后来找到的这个矩阵$S$称为$T$的逆矩阵。

这时,$ST = TS = I$, $I$是对角线上都是1,其他位置都是0的矩阵,称为单位矩阵。

有时我们直接把$S$写成$T^{-1}$,表示它是$T$的逆矩阵。


In [24]:
T1 = np.mat(A1)
S1 = T1.I
print T1.dot(S1)
print S1.dot(T1)
print np.eye(2)


[[ 1.  0.]
 [ 0.  1.]]
[[ 1.  0.]
 [ 0.  1.]]
[[ 1.  0.]
 [ 0.  1.]]

不难知道,单位矩阵$I$能把空间中任何的平行多面体映射到自身,因此放大倍数永远是1,$det(I)=1$永远成立。

矩阵的特征值,特征向量,对角化

当我们把一个矩阵视为一种空间的线性变换时,这个矩阵固有的某些性质是我们特别在意的,比如:

空间中的哪些向量在经过变换后还能够保持方向不变?(伸长缩短无所谓)

  • 这样变换后方向保持不变的向量称作特征向量
  • 保持方向不变时,伸长缩短的倍数成为矩阵的特征值

如, $$A = \begin{bmatrix} 4 & 3 \\ 1 & 2 \end{bmatrix}, v_1 = \begin{bmatrix} 3 \\ 1 \end{bmatrix}, v_2 = \begin{bmatrix} 1 \\ -1 \end{bmatrix}$$

$$Av_1 = 5v_1, Av_2 = v_2$$

In [25]:
A = np.array([[4,3],[1,2]])
v1 = np.array([[3],[1]])
v2 = np.array([[1],[-1]])

In [26]:
print A.dot(v1)
print A.dot(v2)


[[15]
 [ 5]]
[[ 1]
 [-1]]

我们把刚才的关系理顺一下,用更简单的方式表达:

如果我们把特征值写到一个矩阵的对角线里,再把特征值排成一排写到另一个矩阵里:

$$D = \begin{bmatrix} 5 & 0 \\ 0 & 1 \end{bmatrix},V = \begin{bmatrix} 3 & 1 \\ 1 & -1 \end{bmatrix}$$

则有关系:

$$AV = VD$$

如果能找到一个可逆的$V$,则

$$V^{-1}AV = D$$
  • 我们把对于$A$通过前后分别乘以一个互逆的矩阵得到一个对角矩阵$D$成为对角化
  • 而且我们也同时得到,$V$每列都是$A$的特征向量,对应的特征值在$D$中都能找到。

In [27]:
A = np.array([[4,3],[1,2]])
V = np.array([[3,1],[1,-1]])
D = np.array([[5,0],[0,1]])

print A.dot(V)
print V.dot(D)


[[15  1]
 [ 5 -1]]
[[15  1]
 [ 5 -1]]

这样说来矩阵$V$,$D$的性质相当不错。如何找到$V$和$D$呢?

可以看到,特征分解函数eig可以轻松帮我们找到:


In [77]:
d,V = np.linalg.eig(A)
print d
print np.diag(d)
print V


[ 3.27491722 -4.27491722]
[[ 3.27491722  0.        ]
 [ 0.         -4.27491722]]
[[ 0.66026926 -0.3545255 ]
 [ 0.75102896  0.93504634]]

这时我们发现,算法给出的$V$矩阵每一列都是我们自己找到的$V$矩阵中对应列的倍数,所以这个新来的$V$的每一列仍然是特征向量:

$$Av = dv \rightarrow A(kv) = k(Av) = k(dv) = d(kv)$$

矩阵特征值、特征向量的应用:状态转移与游戏升级系统

问题的提出:某个游戏推出一个升级系统

  • 武器的等级一共有5级(0~4),每天系统会随机为武器进化或者退化
  • 向上升一级的概率是15%(5级满级时等级不变),向下降一级的概率是40%(1级初始级别时等级不变)
  • 每天升降级的改变都是独立的:前一天是否升降级不会影响到下一天的升降级。

问,当整个系统玩家上升到一定数量的时候,各个等级之间的比例将是怎样的?

我们要用更好的方式来描述问题:

  • 状态链:一个实体一共可能有0-4共五种不同的状态,状态之间是根据一定的几率进行跳转的,这个实体在时间轴上状态的改变排成一排称为状态链
  • 马尔科夫性:昨天和今天的跳转之间不会互相影响
  • 马尔科夫链:满足马尔科夫性的状态链

而所谓根据一定的几率进行跳转,这个几率我们仍然可以用一个状态转移概率矩阵来描述:$P_{ij}$表示从状态$i$跳转到状态$j$的概率:

$$P = \begin{bmatrix} 0.85 & 0.15 & 0 & 0 & 0\\ 0.40 & 0.45 & 0.15 & 0 & 0 \\ 0 & 0.40 & 0.45 & 0.15 & 0 \\ 0 & 0 & 0.40 & 0.45 & 0.15 \\ 0 & 0 & 0 & 0.40 & 0.6 \end{bmatrix}$$

容易发现这个5*5的概率矩阵每一行之和为1。不加证明的给出:

  • 这类矩阵一定为1的特征值
  • 这类所有特征值的绝对值都小于等于1

对于一个用行向量表示的概率状态$v^T$, $v^TP$将表示它在下一步跳转发生后的概率状态。


In [53]:
P = np.array([[0.85,0.15,0,0,0],[0.4,0.45,0.15,0,0],[0,0.4,0.45,0.15,0],[0,0,0.4,0.45,0.15],[0,0,0,0.4,0.6]])
v = np.array([0,0,1,0,0])

print v.dot(P)


[ 0.    0.4   0.45  0.15  0.  ]

由上式我们可以看出,当$v^T$表示一个确定的2级状态的时候,$v^TP$能够表示它在下一步的状态概率分布:

  • 40%的几率回到1级
  • 45%的几率保持在2级
  • 15%的几率升到3级

我们最初的问题变成了$v^TP = v^T$,求$v^T$,因为对于上述的式子转置后得到

$$P^T v = v$$

因此$v$就是$P^T$这个矩阵对应特征值为1时候的特征向量


In [55]:
d,V = np.linalg.eig(P.T)
print np.diag(d)
print V


[[ 0.05366423  0.          0.          0.          0.        ]
 [ 0.          0.29861321  0.          0.          0.        ]
 [ 0.          0.          1.          0.          0.        ]
 [ 0.          0.          0.          0.60138679  0.        ]
 [ 0.          0.          0.          0.          0.84633577]]
[[ 0.34777449  0.56012786 -0.9270503   0.74300224 -0.86304603]
 [-0.69236317 -0.77211776 -0.34764386 -0.46180043  0.00790601]
 [ 0.55560528  0.08217313 -0.13036645 -0.45340205  0.33147584]
 [-0.29087942  0.25844434 -0.04888742  0.00157745  0.32547458]
 [ 0.07986282 -0.12862757 -0.01833278  0.17062278  0.1981896 ]]

我们取出$V[:,2]$,并且对它进行归一化:

(归一化的意思是对它进行缩放,使得这个向量中的元素之和为1,以满足概率分布的要求)


In [57]:
v = V[:,2]/np.sum(V[:,2])
print v
print v.dot(P)


[ 0.62966949  0.23612606  0.08854727  0.03320523  0.01245196]
[ 0.62966949  0.23612606  0.08854727  0.03320523  0.01245196]

由此我们知道,如果一个系统中武器等级的概率分布为$v$,状态概率转移矩阵为$P$,那么下一步跳转后,状态分布仍然还是$v$。

这个$v$被称作平稳分布,我们也可以很轻松的知道:

在用户足够多的时候,武器满级的玩家大概占所有人的1.245%。

这样,游戏的数值策划们就可以通过微调矩阵$P$来让拥有各个武器等级比例更加合理了。

矩阵对角化的应用: 求解Fibbonacci数列

在我们了解了对角化之后,矩阵的乘方会变的特别容易,因为

$$A = VDV^{-1} \\ A^{N} = AAA...A = VDV^{-1}VDV^{-1}...VDV{-1} = VD^{N}V^{-1}$$

我们把曾经介绍过的Fibonacci数列问题再一次提出来,并寄希望于矩阵法能够更快的求解出结果:

$$\begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}\begin{bmatrix} F_{n+1}\\ F_{n} \end{bmatrix} = \begin{bmatrix} F_{n+2}\\ F_{n+1} \end{bmatrix}$$

反复应用上式,容易使用一个简洁的公式表达递推关系: $$\begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^N \begin{bmatrix} F_{1}\\ F_{0} \end{bmatrix} = \begin{bmatrix} F_{N+1}\\ F_{N} \end{bmatrix}$$


In [2]:
FibMat = np.array([[1,1],[1,0]])
d,V = np.linalg.eig(FibMat)
print np.diag(d)
print V


[[ 1.61803399  0.        ]
 [ 0.         -0.61803399]]
[[ 0.85065081 -0.52573111]
 [ 0.52573111  0.85065081]]

又看到了我们熟悉的1.618和0.618(黄金分割点)。

如果矩阵能够对角化的话,计算起来几乎仅仅耗费对角化的时间,比如我们想求$F_{17}$:


In [3]:
# N=16,先求FibMat的16次方
FibMat16 = V.dot(np.diag(d**16)).dot(np.mat(V).I)
print FibMat16


[[ 1597.   987.]
 [  987.   610.]]

In [4]:
# 再乘以向量(1,1)
print FibMat16.dot(np.array([[1],[1]]))


[[ 2584.]
 [ 1597.]]

上面的数字2584就是$F_{17}$了,可以看到求值的速度那是相当之快!

退一万步讲,就算一个矩阵不能成功的对角化,通过矩阵反复的乘方也比Fibonacci数列强行循环来的快。工具好就是不一样。


In [44]:
# 循环跟自己乘方四次:
Mat = FibMat
for i in xrange(4):
    Mat = Mat.dot(Mat)
print Mat


[[1597  987]
 [ 987  610]]

正交矩阵

有一类特殊的矩阵,满足条件:$T^{-1} = T^T$,被称为正交矩阵。也就是这个矩阵的逆就是它自己的转置。

例如,以下形式的二阶矩阵就都满足正交矩阵的性质:

$$T = \begin{bmatrix} \cos(\theta) & \sin(\theta) \\ -\sin(\theta) & \cos(\theta) \end{bmatrix}$$

根据矩阵乘法的定义,容易知道这类矩阵有几个很好的性质:

  • 每个行向量或者每个列向量的空间长度(自己与自己的内积)为1
  • 不同行向量之间的内积为0,不同列向量之间的内积也为0
  • 因为转置不会改变矩阵的行列式,因此正交矩阵的行列式只可能为1或者-1
  • 如果把正交矩阵视作空间变换,那么这种变换仅仅对空间的立方体进行了旋转而不进行伸缩

In [70]:
theta = np.pi/6
T = np.array([[np.cos(theta),np.sin(theta)],[-np.sin(theta),np.cos(theta)]])
print T


[[ 0.8660254  0.5      ]
 [-0.5        0.8660254]]

In [71]:
e1 = np.array([[1],[0]])
e2 = np.array([[0],[1]])
c1 = T.dot(e1)
c2 = T.dot(e2)

p1 = bplt.figure(title='正交矩阵对向量与面积的映射',background_fill='#FFFFFF',**plot_config)
p1.line(*zip(np.array([0,0]),e1.flatten()),line_width=3)
p1.line(*zip(np.array([0,0]),e2.flatten()),line_width=3)
p1.patch(*zip(np.array([0,0]),e1.flatten(),(e1+e2).flatten(),e2.flatten()),alpha=0.3)
p1.line(*zip(np.array([0,0]),c1.flatten()),line_width=3,line_color='darkred')
p1.line(*zip(np.array([0,0]),c2.flatten()),line_width=3,line_color='darkred')
p1.patch(*zip(np.array([0,0]),c1.flatten(),(c1+c2).flatten(),c2.flatten()),alpha=0.3,color='red')

bplt.show(p1)



In [72]:
np.linalg.det(T)


Out[72]:
1.0

对称矩阵

有一类特殊的矩阵,满足条件:$S_{ij} = S_{ji}, S^T = S$,被称为对称矩阵。

直观的来看,它们的对角线镜像就是它自身。


In [64]:
S = np.array([[3,1],[1,3]])
print S
print S.T


[[3 1]
 [1 3]]
[[3 1]
 [1 3]]

要说明对阵矩阵有什么好处,就要先说明其他矩阵有什么不好:

  • 有些矩阵在求得特征值、特征向量之后,我们发现这类矩阵 无法对角化
  • 而就算有些矩阵能够对角化,我们发现此类矩阵 特征值不全为非负实数

无法对角化的例子


In [66]:
M1 = np.array([[1,1],[0,1]])
d,V = np.linalg.eig(M1)
D = np.diag(d)
print D
print V


[[ 1.  0.]
 [ 0.  1.]]
[[  1.00000000e+00  -1.00000000e+00]
 [  0.00000000e+00   2.22044605e-16]]

In [67]:
print M1.dot(V)
print V.dot(D)


[[  1.00000000e+00  -1.00000000e+00]
 [  0.00000000e+00   2.22044605e-16]]
[[  1.00000000e+00  -1.00000000e+00]
 [  0.00000000e+00   2.22044605e-16]]

尽管$M_1$是一个维数为2的矩阵,而且$M_1V=VD$也成立,可是$V$并不是可逆矩阵,不能满足$V^{-1}M_1V = D$,这种情况我们称$M_1$无法对角化。

对角化后特征值不全为非负实数的例子


In [69]:
M2 = np.array([[4,8],[-5,2]])
d,V = np.linalg.eig(M2)
D = np.diag(d)
print D
print V


[[ 3.+6.244998j  0.+0.j      ]
 [ 0.+0.j        3.-6.244998j]]
[[ 0.78446454+0.j          0.78446454-0.j        ]
 [-0.09805807+0.61237244j -0.09805807-0.61237244j]]

实对称矩阵的优点

  • 一定能对角化:$V^{-1}SV = D$
  • 对角化后,特征值全部为非负实数:$D_{i,i}\geq 0$

这两个性质加上对称这个性质在一起,就有了更厉害的性质:

  • 存在一个正交矩阵$T$,使得$T^{-1}ST = T^TST = D$
  • 矩阵能够"开平方":存在一个对阵矩阵$C$,使得$S = C^TC = CC$

实际上NumPy在处理实对称矩阵对角化的时候,直接会给出一个由特征向量排列组成的正交矩阵。


In [75]:
print np.float64(S)
d,V = np.linalg.eig(S)
D = np.diag(d)
print D
print V


[[ 3.  1.]
 [ 1.  3.]]
[[ 4.  0.]
 [ 0.  2.]]
[[ 0.70710678 -0.70710678]
 [ 0.70710678  0.70710678]]

正交矩阵与对称矩阵的应用:SVD

在电影评论平台上,后台记录了形形色色的用户对电影评价的记录。

可以用有一个大矩阵X来粗略的保存"用户-电影"之间的关系:

  • 如果用户i喜欢电影j,那么$X_{ij}$为1
  • 如果用户i不喜欢电影j,那么$X_{ij}$为-1
  • 如果用户i没评价过电影j或者说不上喜欢讨厌,那么$X_{ij}$为0

如果我们希望发现这个大矩阵中存在某种模式,使得一个低维数的矩阵能够在数值上尽量接近原来的矩阵,那么我们需要SVD算法。


In [84]:
X = np.random.randint(-1,2,(30,10))
print X


[[ 0 -1  0 -1  0  1  1  1 -1  1]
 [ 1  1  1  0  0 -1 -1  1 -1  0]
 [ 1 -1  0  0  0  0 -1  1  0 -1]
 [-1  1 -1 -1  1 -1  0  1 -1 -1]
 [-1  1  0  1  1  0  0 -1  0  0]
 [ 1 -1 -1  0  0  1  1  1  0  0]
 [-1  0 -1  0  1  1 -1  0 -1 -1]
 [ 1  1 -1  1 -1  1 -1  0  0  0]
 [ 1  0  1 -1  1  1 -1  0  0  1]
 [ 0 -1  0 -1  0  1 -1  0  1  0]
 [ 0  1 -1 -1  1 -1  1  1  0  1]
 [ 0 -1 -1 -1  1  0  0 -1  0  1]
 [ 1 -1  1 -1 -1  0 -1 -1  1 -1]
 [ 1  1  0 -1  0  1  0  1 -1  0]
 [ 1 -1 -1  1  1  1  1  1  0  1]
 [ 1  1  0 -1  0 -1  1  1  1 -1]
 [ 1 -1  1 -1  1 -1 -1  1  0 -1]
 [ 0  0 -1  0 -1 -1  1 -1  1  0]
 [ 1 -1 -1  1  0  1  0 -1 -1  0]
 [-1  1  0  0 -1  1 -1  0 -1  1]
 [ 0  0 -1 -1  1  1  0 -1  0  0]
 [ 0  1  1  1  0  1  1 -1  1 -1]
 [ 0  1  0 -1 -1 -1 -1  0  1 -1]
 [ 0  0 -1 -1  0  1 -1 -1 -1  0]
 [ 1  0 -1 -1  0 -1  1 -1  1  1]
 [ 0  0  0  0  0  1 -1 -1  0  1]
 [-1  0 -1  0  0 -1  0  1 -1  0]
 [-1 -1  0 -1  1 -1  0  1 -1  1]
 [-1  1  0  0  1  0  1  1 -1 -1]
 [ 0 -1 -1 -1  0  0  1  0 -1 -1]]

我们随机生成了30位用户对10部电影的偏好。SVD算法的用处在于,能够找到两个正交矩阵$U,V$,使得: $$X = UDV\\ U^TXV^T = D $$

这里很明显,$X$并不是方阵,因此$U$是一个30阶方阵,$V$是一个10阶方阵,而$D$是一个非方形的对角矩阵


In [97]:
U,d,V = np.linalg.svd(X)
D = U.T.dot(X).dot(V.T)

In [100]:
print np.round(D,2)


[[ 6.19 -0.   -0.    0.    0.    0.   -0.    0.   -0.    0.  ]
 [-0.    5.86  0.   -0.    0.    0.   -0.    0.   -0.    0.  ]
 [ 0.    0.    5.33 -0.   -0.   -0.    0.   -0.    0.   -0.  ]
 [-0.    0.   -0.    5.16  0.    0.   -0.    0.    0.   -0.  ]
 [-0.    0.    0.   -0.    4.46  0.    0.    0.   -0.    0.  ]
 [-0.    0.    0.    0.    0.    3.98 -0.    0.   -0.    0.  ]
 [ 0.   -0.    0.    0.    0.    0.    3.84  0.   -0.   -0.  ]
 [-0.    0.    0.   -0.   -0.    0.   -0.    3.47 -0.    0.  ]
 [-0.   -0.    0.    0.   -0.   -0.   -0.   -0.    2.9   0.  ]
 [-0.   -0.   -0.   -0.    0.   -0.   -0.   -0.   -0.    2.32]
 [-0.   -0.    0.   -0.   -0.   -0.   -0.    0.   -0.   -0.  ]
 [-0.   -0.    0.    0.    0.   -0.    0.   -0.    0.   -0.  ]
 [ 0.   -0.   -0.   -0.   -0.    0.   -0.    0.   -0.    0.  ]
 [ 0.   -0.   -0.    0.    0.   -0.   -0.    0.    0.    0.  ]
 [-0.    0.    0.   -0.   -0.    0.    0.   -0.    0.   -0.  ]
 [-0.    0.   -0.   -0.   -0.   -0.   -0.   -0.   -0.   -0.  ]
 [ 0.    0.   -0.   -0.   -0.    0.   -0.   -0.   -0.   -0.  ]
 [ 0.   -0.    0.   -0.   -0.    0.   -0.   -0.   -0.   -0.  ]
 [ 0.    0.   -0.    0.    0.   -0.    0.    0.   -0.    0.  ]
 [ 0.   -0.    0.   -0.    0.   -0.    0.   -0.    0.    0.  ]
 [-0.   -0.    0.   -0.   -0.    0.    0.    0.    0.   -0.  ]
 [-0.    0.   -0.    0.    0.   -0.   -0.   -0.   -0.    0.  ]
 [-0.    0.    0.    0.   -0.    0.   -0.   -0.   -0.   -0.  ]
 [ 0.    0.   -0.   -0.    0.   -0.    0.    0.    0.    0.  ]
 [ 0.   -0.   -0.    0.    0.   -0.   -0.    0.   -0.   -0.  ]
 [ 0.   -0.   -0.    0.    0.    0.   -0.    0.    0.   -0.  ]
 [-0.    0.    0.   -0.    0.    0.    0.   -0.   -0.   -0.  ]
 [-0.   -0.    0.    0.    0.   -0.    0.   -0.    0.   -0.  ]
 [ 0.    0.    0.    0.   -0.   -0.    0.   -0.    0.    0.  ]
 [ 0.   -0.   -0.   -0.   -0.   -0.    0.    0.    0.    0.  ]]

当我们抛弃掉$D$矩阵对角元素末位的一些"信息"之后,很可能发现原来的矩阵并没有太大的损失.

而且我们可以假定,越小的对角元素所对应的信息越可能是"噪声",也就是原来矩阵$X$中体现用户对电影随机偏好的那部分。


In [101]:
np.sum((d**2)[:6])/np.sum(d**2)


Out[101]:
0.80099981175816048

我们可以获得一个修正后的modD矩阵,这个矩阵仅有6维:


In [103]:
modD = D
for i in xrange(6,10):
    modD[i,i] = 0.0
print np.round(modD,2)


[[ 6.19 -0.   -0.    0.    0.    0.   -0.    0.   -0.    0.  ]
 [-0.    5.86  0.   -0.    0.    0.   -0.    0.   -0.    0.  ]
 [ 0.    0.    5.33 -0.   -0.   -0.    0.   -0.    0.   -0.  ]
 [-0.    0.   -0.    5.16  0.    0.   -0.    0.    0.   -0.  ]
 [-0.    0.    0.   -0.    4.46  0.    0.    0.   -0.    0.  ]
 [-0.    0.    0.    0.    0.    3.98 -0.    0.   -0.    0.  ]
 [ 0.   -0.    0.    0.    0.    0.    0.    0.   -0.   -0.  ]
 [-0.    0.    0.   -0.   -0.    0.   -0.    0.   -0.    0.  ]
 [-0.   -0.    0.    0.   -0.   -0.   -0.   -0.    0.    0.  ]
 [-0.   -0.   -0.   -0.    0.   -0.   -0.   -0.   -0.    0.  ]
 [-0.   -0.    0.   -0.   -0.   -0.   -0.    0.   -0.   -0.  ]
 [-0.   -0.    0.    0.    0.   -0.    0.   -0.    0.   -0.  ]
 [ 0.   -0.   -0.   -0.   -0.    0.   -0.    0.   -0.    0.  ]
 [ 0.   -0.   -0.    0.    0.   -0.   -0.    0.    0.    0.  ]
 [-0.    0.    0.   -0.   -0.    0.    0.   -0.    0.   -0.  ]
 [-0.    0.   -0.   -0.   -0.   -0.   -0.   -0.   -0.   -0.  ]
 [ 0.    0.   -0.   -0.   -0.    0.   -0.   -0.   -0.   -0.  ]
 [ 0.   -0.    0.   -0.   -0.    0.   -0.   -0.   -0.   -0.  ]
 [ 0.    0.   -0.    0.    0.   -0.    0.    0.   -0.    0.  ]
 [ 0.   -0.    0.   -0.    0.   -0.    0.   -0.    0.    0.  ]
 [-0.   -0.    0.   -0.   -0.    0.    0.    0.    0.   -0.  ]
 [-0.    0.   -0.    0.    0.   -0.   -0.   -0.   -0.    0.  ]
 [-0.    0.    0.    0.   -0.    0.   -0.   -0.   -0.   -0.  ]
 [ 0.    0.   -0.   -0.    0.   -0.    0.    0.    0.    0.  ]
 [ 0.   -0.   -0.    0.    0.   -0.   -0.    0.   -0.   -0.  ]
 [ 0.   -0.   -0.    0.    0.    0.   -0.    0.    0.   -0.  ]
 [-0.    0.    0.   -0.    0.    0.    0.   -0.   -0.   -0.  ]
 [-0.   -0.    0.    0.    0.   -0.    0.   -0.    0.   -0.  ]
 [ 0.    0.    0.    0.   -0.   -0.    0.   -0.    0.    0.  ]
 [ 0.   -0.   -0.   -0.   -0.   -0.    0.    0.    0.    0.  ]]

使用modD矩阵重构出来的modX矩阵可以被视作去掉了噪声的X矩阵:


In [108]:
modX = U.dot(modD).dot(V)
print np.round(modX,2)


[[ 0.4  -0.96 -0.42 -0.38  0.8   0.53  0.44  0.91 -0.8   1.03]
 [ 0.14  0.58  1.34 -0.15  0.06 -0.54 -0.84  1.35 -0.38 -0.15]
 [ 1.05 -0.75  0.33 -0.42 -0.05  0.33 -0.6   0.84 -0.16 -1.08]
 [-1.24  0.69 -0.82 -0.97  0.79 -1.07  0.1   0.98 -1.14 -1.04]
 [-1.13  1.   -0.23  0.73 -0.1   0.07  0.07 -0.7  -0.21  0.21]
 [ 1.04 -1.23 -0.75  0.2   0.32  0.77  1.05  0.71 -0.23 -0.12]
 [-0.88  0.05 -0.97 -0.19  0.44  0.95 -0.83 -0.07 -1.41 -0.92]
 [ 0.16  0.11  0.18  1.03 -0.45  1.1  -0.43 -0.4  -0.1  -0.46]
 [ 0.59 -0.71  0.84 -0.66  0.2   0.54 -1.21  0.22 -0.24  1.13]
 [ 0.61 -0.93  0.04 -0.84 -0.07  0.49 -0.95 -0.52  0.18  0.03]
 [-0.57  0.16 -0.5  -0.76  0.81 -1.28  1.17  0.86 -0.29  0.85]
 [-0.15 -1.01 -1.02 -1.19  0.53  0.13  0.1  -0.84 -0.03  1.06]
 [ 1.18 -0.81  0.68 -0.87 -0.81 -0.04 -1.26 -0.88  1.22 -0.92]
 [ 0.24 -0.23  0.2  -0.13  0.38  0.37 -0.38  1.04 -0.81 -0.15]
 [ 0.89 -1.12 -0.63  0.71  0.51  1.07  1.4   0.83 -0.45  0.95]
 [ 0.55  0.11  0.21 -0.49 -0.13 -1.54  0.88  0.92  0.75 -1.07]
 [ 1.01 -0.68  0.76 -1.43  0.18 -0.7  -0.88  1.3  -0.03 -0.81]
 [-0.09  0.03 -0.69 -0.04 -0.39 -0.8   1.13 -1.14  1.22 -0.18]
 [ 0.45 -0.94 -0.87  0.62 -0.01  1.65  0.17 -0.59 -0.33 -0.06]
 [-1.    0.72  0.33  0.44  0.13  0.76 -1.1  -0.37 -0.88  0.75]
 [-0.33 -0.66 -1.1  -0.66  0.3   0.53 -0.17 -0.87 -0.3   0.11]
 [ 0.09  0.66  0.26  1.5  -0.86  0.32  0.43 -0.7   0.75 -0.67]
 [-0.15  0.58  0.54 -0.8  -0.52 -1.2  -0.81 -0.39  0.76 -1.23]
 [-0.5  -0.52 -0.82 -0.73  0.27  0.93 -1.09 -0.9  -0.71 -0.12]
 [ 0.28 -0.67 -0.62 -0.87  0.01 -0.97  1.1  -0.88  1.21  0.85]
 [-0.17 -0.23  0.14  0.04 -0.1   0.94 -0.94 -1.03 -0.09  0.96]
 [-0.85  0.44 -0.59 -0.38  0.64 -0.54  0.37  0.75 -0.87 -0.2 ]
 [-0.49 -0.25 -0.27 -1.34  1.07 -0.7   0.03  1.01 -0.92  1.1 ]
 [-0.9   0.85 -0.5   0.32  0.54 -0.4   0.61  1.2  -1.1  -0.79]
 [ 0.08 -0.81 -1.4  -0.78  0.49  0.03  0.61  0.24 -0.44 -0.95]]

这时我们分别从X矩阵和modX矩阵中拿出第0位用户的电影评价数据来做比较:


In [111]:
print np.round(np.float64(X[0,:]),2)
print np.round(modX[0,:],2)


[ 0. -1.  0. -1.  0.  1.  1.  1. -1.  1.]
[ 0.4  -0.96 -0.42 -0.38  0.8   0.53  0.44  0.91 -0.8   1.03]

用户0对于0号、2号、4号电影的评价都是0,假设这3部电影他都没看过。

而重构出来的矩阵中,他对这三部电影的评价的评分则是0.4,-0.42,0.8。这时你就可以试探性的优先推荐4号电影,而不要急着推荐2号电影了。

SVD备注

SVD之所以能够工作,实际就借助了正交矩阵与对称矩阵的性质:

  • 尽管$X$连方阵都不是,但我们知道,$X^TX$与$XX^T$都是对称矩阵!
  • 分别找到$X^TX$与$XX^T$对角化所使用的$U$与$V$即可实现SVD算法。

目标:$$ X = UDV $$ 容易知道:$$ X^TX = V^TD^2V\\ XX^T = UD^2U^T$$


In [ ]: