张量分解

参考文献:Kolda TG, Bader BW. Tensor Decomposition and Application. SIAM Rev 2009;51:455–500. https://doi.org/10/dzcrv6.

张量可以视为多维数组,其“形状”取决于张量的阶(order)数。标量是第0阶张量,向量是第1阶张量,矩阵是第2阶张量,第3阶或阶数更高的张量被称为高阶张量(higher-order tensor),一般提到的张量都是特指高阶张量。

在矩阵中,我们需要用第 i i i行第 j j j列的形式定位一个元素,需要两个索引确定一个元素的位置。如果要确定高阶张量中某一元素的位置则需要更多的索引,所需索引数量与张量阶数相同。如下图所示的三阶张量,就可以用 ( i , j , k ) (i,j,k) (i,j,k)的形式确定元素位置。
在这里插入图片描述

数学符号说明

  1. 向量以加粗小写字母表示,如 a \bold{a} a b \bold{b} b
  2. 矩阵以加粗大写字母表示,如 A \bold{A} A B \bold{B} B
  3. 高阶张量以加粗花体字母显示, 如 X \boldsymbol{\mathcal{X}} X Y \boldsymbol{\mathcal{Y}} Y
  4. 一个由 M M M个矩阵(向量)组成的集合表示为 { A [ m ] ∈ R I m × N } m M \left\{\bold{A}_{[m]}\in\mathbb{R}^{I_m\times N}\right\}_m^M {A[m]RIm×N}mM { a [ m ] ∈ R I m × N } m M \left\{\bold{a}_{[m]}\in\mathbb{R}^{I_m\times N}\right\}_m^M {a[m]RIm×N}mM
  5. 定位张量中某一具体元素时以下标展示,如向量 a \bold{a} a中第 i i i个元素为 a i a_i ai,矩阵 A \bold{A} A的第 i i i行第 j j j列元素为 a i j a_{ij} aij,张量 X \boldsymbol{\mathcal{X}} X的某一元素可用 x i j k x_{ijk} xijk表示
  6. 元素切片表示方法与numpy保持一致,用下标表示。矩阵 A \bold{A} A切片如 a i : \bold{a}_{i:} ai:表示矩阵第 i i i行,张量切片如下图所示。

在这里插入图片描述

  1. ∣ ∣ X ∣ ∣ p \left|\left| \boldsymbol{\mathcal{X}}\right|\right|_p Xp表示张量的 p p p范数,缺省状态下取 p = 2 p=2 p=2
    ∣ ∣ X ∣ ∣ p = ( ∑ i ∑ j ∑ k ∣ x i j k ∣ p ) 1 p \left|\left| \boldsymbol{\mathcal{X}}\right|\right|_p= \left(\sum_i\sum_j\sum_k{\left| x_{ijk}\right|^p}\right)^{\frac{1}{p}} Xp=(ijkxijkp)p1

  2. < X , Y > \left<\boldsymbol{\mathcal{X}},\boldsymbol{\mathcal{Y}}\right> XY表示张量的内积
    < X , Y > = ∑ i ∑ j ∑ k x i j k y i j k \left<\boldsymbol{\mathcal{X}},\boldsymbol{\mathcal{Y}}\right>= \sum_i\sum_j\sum_k{ x_{ijk}y_{ijk}} XY=ijkxijkyijk

  3. X ∗ Y \boldsymbol{\mathcal{X}}\ast\boldsymbol{\mathcal{Y}} XY表示张量的哈达玛积,或element-wise product
    Z = X ∗ Y \boldsymbol{\mathcal{Z}}=\boldsymbol{\mathcal{X}}\ast\boldsymbol{\mathcal{Y}} Z=XY

    z i j k = x i j k y i j k z_{ijk}=x_{ijk}y_{ijk} zijk=xijkyijk

  4. A ⊗ B \bold{A}\otimes\bold{B} AB表示Kronecker积,若 A ∈ R I × J , Y ∈ R M × N \bold{A}\in\mathbb{R}^{I\times J},\boldsymbol{Y}\in\mathbb{R}^{M\times N} ARI×JYRM×N,则 { A ⊗ B } ∈ R I M × J N \left\{\bold{A}\otimes\bold{B}\right\}\in\mathbb{R}^{IM\times JN} {AB}RIM×JN,计算方式如下:
    A ⊗ B = [ a 11 B a 12 B ⋯ a 1 m 2 B a 21 B a 22 B ⋯ a 2 m 2 B ⋮ ⋮ ⋱ ⋮ a m 1 1 B + + a m 1 2 B ⋯ a m 1 m 2 B ] \bold{A}\otimes \bold{B}= \left[\begin{array}{cccc} a_{11}\bold{B}&a_{12}\bold{B}&\cdots&a_{1m_2}\bold{B}\\ a_{21}\bold{B}&a_{22}\bold{B}&\cdots&a_{2m_2}\bold{B}\\ \vdots&\vdots&\ddots&\vdots\\ a_{m_11}\bold{B}+&+a_{m_12}\bold{B}&\cdots&a_{m_1m_2}\bold{B} \end{array}\right] AB=a11Ba21Bam11B+a12Ba22B+am12Ba1m2Ba2m2Bam1m2B

  5. A ⊙ B \bold{A}\odot\bold{B} AB表示Khatri-Rao积,若 A ∈ R I × J , Y ∈ R K × J \bold{A}\in\mathbb{R}^{I\times J},\boldsymbol{Y}\in\mathbb{R}^{K\times J} ARI×JYRK×J,则 { A ⊙ B } ∈ R I K × J \left\{\bold{A}\odot\bold{B}\right\}\in\mathbb{R}^{IK\times J} {AB}RIK×J,计算方式如下:
    A ⊙ B = ( a : 1 ⊗ b : 1 , a : 2 ⊗ b : 2 , … , a : n ⊗ b : n ) \bold{A}\odot\bold{B}= \left( \boldsymbol{a_{:1}}\otimes \boldsymbol{b_{:1}}, \boldsymbol{a_{:2}}\otimes \boldsymbol{b_{:2}}, \dots, \boldsymbol{a_{:n}}\otimes \boldsymbol{b_{:n}} \right) AB=(a:1b:1,a:2b:2,,a:nb:n)

    A [ 1 ] ⊙ A [ 2 ] ⊙ ⋯ ⊙ A [ M ] = ˙ ⨀ m = 1 M A [ m ] \boldsymbol{A_{[1]}}\odot\boldsymbol{A_{[2]}}\odot\cdots\odot\boldsymbol{A_{[M]}} \dot{=}\bigodot^M_{m=1}\boldsymbol{A_{[m]}} A[1]A[2]A[M]=˙m=1MA[m]

  6. a ∘ b \bold{a}\circ\bold{b} ab表示向量的外积
    a ∘ b = a b T \bold{a}\circ\bold{b}=\bold{a}\bold{b}^T ab=abT

  7. X × ˉ n Y \boldsymbol{\mathcal{X}}\bar{\times}_n\boldsymbol{Y} X×ˉnY,表示张量 X \boldsymbol{\mathcal{X}} X与矩阵 Y \boldsymbol{Y} Y n n n模态积,若 X ∈ R I 1 × I 2 × ⋯ × I N , Y ∈ R m × I n \boldsymbol{\mathcal{X}}\in\mathbb{R}^{I_1\times I_2\times\dots\times I_N},\boldsymbol{Y}\in\mathbb{R}^{m\times I_n} XRI1×I2××INYRm×In,则 { X × ˉ n Y } ∈ R I 1 × ⋯ × I n − 1 × m × I n + 1 × ⋯ × I N \left\{\boldsymbol{\mathcal{X}}\bar{\times}_n\boldsymbol{Y}\right\}\in\mathbb{R}^{I_1\times\dots\times I_{n-1}\times m \times I_{n+1}\times\dots\times I_N} {X×ˉnY}RI1××In1×m×In+1××IN
    X × ˉ 1 Y [ 1 ] × ˉ 2 Y [ 2 ] × ˉ 3 ⋯ × ˉ M Y [ M ] = X ∏ m = 1 M × ˉ m Y [ m ] \boldsymbol{\mathcal{X}}\bar{\times}_1\boldsymbol{Y_{[1]}}\bar{\times}_2\boldsymbol{Y_{[2]}}\bar{\times}_3\cdots\bar{\times}_M\boldsymbol{Y_{[M]}}= \boldsymbol{\mathcal{X}}\prod_{m=1}^M\bar{\times}_mY_{[m]} X×ˉ1Y[1]×ˉ2Y[2]×ˉ3×ˉMY[M]=Xm=1M×ˉmY[m]

  8. X † \boldsymbol{X}^{\dagger} X表示矩阵 X \boldsymbol{X} X的广义逆(Moore–Penrose pseudoinverse)

  9. X ( n ) \boldsymbol{\mathcal{X}}_{(n)} X(n)表示张量以第 n n n模态( n − m o d e n-mode nmode)展开得到的矩阵,若 X ∈ R 4 × 3 × 2 \boldsymbol{\mathcal{X}}\in\mathbb{R}^{4\times 3\times 2} XR4×3×2,则
    X ( 1 ) = [ X ( : , : , 1 ) , X ( : , : , 2 ) ] X ( 2 ) = [ X ( : , : , 1 ) T , X ( : , : , 2 ) T ] X ( 3 ) = [ X ( : , 1 , : ) T , X ( : , 2 , : ) T , X ( : , 3 , : ) T ] {\mathcal{X}}_{\left(1\right)}=\Big[{\mathcal{X}}\left(:,:,1\right),{\mathcal{X}}\left(:,:,2\right)\Big]\\ {\mathcal{X}}_{\left(2\right)}=\Big[{\mathcal{X}}\left(:,:,1\right)^T,{\mathcal{X}}\left(:,:,2\right)^T\Big]\\ {\mathcal{X}}_{\left(3\right)}=\Big[{\mathcal{X}}\left(:,1,:\right)^T,{\mathcal{X}}\left(:,2,:\right)^T, {\mathcal{X}}\left(:,3,:\right)^T\Big] X(1)=[X(:,:,1),X(:,:,2)]X(2)=[X(:,:,1)T,X(:,:,2)T]X(3)=[X(:,1,:)T,X(:,2,:)T,X(:,3,:)T]

CP分解

基本概念

首先我们将可以由多个向量外积得到的张量成为秩1(rank-one)张量,如 Y = a ∘ b ∘ c \boldsymbol{\mathcal{Y}}=\bold{a}\circ\bold{b}\circ\bold{c} Y=abc就是一个秩1矩阵,CP分解就是将一个张量分解为多个形状相同的秩1张量的和,或者说用多个秩1张量去近似表示原始张量,如下图所示:

在这里插入图片描述

X ∈ R I × J × K \boldsymbol{\mathcal{X}}\in\mathbb{R}^{I\times J\times K} XRI×J×K A = ( a 1 , a 2 , … , a R ) ∈ R I \bold{A}=\left(\bold{a}_1,\bold{a}_2,\dots,\bold{a}_R\right)\in\mathbb{R}^I A=(a1,a2,,aR)RI B = ( b 1 , b 2 , … , b R ) ∈ R J \bold{B}=\left(\bold{b}_1,\bold{b}_2,\dots,\bold{b}_R\right)\in\mathbb{R}^J B=(b1,b2,,bR)RJ C = ( c 1 , c 2 , … , c R ) ∈ R K \bold{C}=\left(\bold{c}_1,\bold{c}_2,\dots,\bold{c}_R\right)\in\mathbb{R}^K C=(c1,c2,,cR)RK,则CP分解可以公式化为:
X ≈ ∑ r = 1 R a r ∘ b r ∘ c r \boldsymbol{\mathcal{X}}\approx \sum_{r=1}^R \bold{a}_r\circ\bold{b}_r\circ\bold{c}_r Xr=1Rarbrcr
CP分解公式还可以进一步矩阵化为:
X ( 1 ) ≈ A ( C ⊙ B ) T \boldsymbol{\mathcal{X}}_{(1)}\approx \bold{A}\left(\bold{C}\odot\bold{B}\right)^T X(1)A(CB)T

X ( 2 ) ≈ B ( C ⊙ A ) T \boldsymbol{\mathcal{X}}_{(2)}\approx \bold{B}\left(\bold{C}\odot\bold{A}\right)^T X(2)B(CA)T

X ( 3 ) ≈ C ( B ⊙ A ) T \boldsymbol{\mathcal{X}}_{(3)}\approx \bold{C}\left(\bold{B}\odot\bold{A}\right)^T X(3)C(BA)T

与矩阵类似,张量也存在秩的概念,使 X = ∑ r = 1 R a r ∘ b r ∘ c r \boldsymbol{\mathcal{X}}= \sum_{r=1}^R \bold{a}_r\circ\bold{b}_r\circ\bold{c}_r X=r=1Rarbrcr时最小的 R R R值,或者说能够完美拟合原始张量所需的最少秩1张量数量,即为张量 X \boldsymbol{\mathcal{X}} X的秩。

在实际进行CP分解时,通常还会对 A 、 B 、 C \bold{A}、\bold{B}、\bold{C} ABC按列归一化,并将其范数作为权重存储为向量 λ ∈ R R \boldsymbol{\lambda}\in\mathbb{R}^R λRR,则新的公式为:
X ≈ ∑ r = 1 R λ r a r ∘ b r ∘ c r \boldsymbol{\mathcal{X}}\approx \sum_{r=1}^R \boldsymbol{\lambda}_r\bold{a}_r\circ\bold{b}_r\circ\bold{c}_r Xr=1Rλrarbrcr
对于更高阶的张量 X ∈ R I 1 × I 2 × ⋯ × I N \boldsymbol{\mathcal{X}}\in\mathbb{R}^{I_1\times I_2\times\cdots\times I_N} XRI1×I2××IN,通用表达式为:
X ( n ) ≈ A [ n ] Λ ( A [ N ] ⊙ ⋯ ⊙ A [ n + 1 ] ⊙ A [ n − 1 ] ⊙ ⋯ ⊙ A [ 1 ] ) T \boldsymbol{\mathcal{X}}_{(n)}\approx \bold{A}_{[n]}\bold{\Lambda}\left( \bold{A}_{[N]}\odot\cdots\odot\bold{A}_{[n+1]}\odot\bold{A}_{[n-1]}\odot\cdots\odot\bold{A}_{[1]} \right)^T X(n)A[n]Λ(A[N]A[n+1]A[n1]A[1])T
其中 Λ = d i a g ( λ ) \bold{\Lambda}=diag(\boldsymbol{\lambda}) Λ=diag(λ) d i a g diag diag为对角矩阵化。

ALS-CP

ALS(alternating least squares、交替最小二乘法)

公式推导

下面以三阶张量 X \boldsymbol{\mathcal{X}} X为例介绍如何使用ALS计算CP分解的因子矩阵 A 、 B 、 C \bold{A}、\bold{B}、\bold{C} ABC。首先我们的最终目标是让由

A 、 B 、 C \bold{A}、\bold{B}、\bold{C} ABC估计得到的张量 X ^ \hat{\boldsymbol{\mathcal{X}}} X^尽可能的接近原始张量 X \boldsymbol{\mathcal{X}} X,即:
m i n ∣ ∣ X ^ − X ∣ ∣  with  X ^ = ∑ r = 1 R λ r a r ∘ b r ∘ c r min\left|\left| \hat{\boldsymbol{\mathcal{X}}}-\boldsymbol{\mathcal{X}} \right|\right| \text{ with } \hat{\boldsymbol{\mathcal{X}}}= \sum_{r=1}^R \boldsymbol{\lambda}_r\bold{a}_r\circ\bold{b}_r\circ\bold{c}_r minX^X with X^=r=1Rλrarbrcr
为此,我们利用公式(12)(16)可以得到如下估计因子矩阵 A ^ = A Λ \hat{\bold{A}}=\bold{A}\bold{\Lambda} A^=AΛ的方法:
m i n ∣ ∣ X ^ ( 1 ) − X ( 1 ) ∣ ∣ = m i n ∣ ∣ A ^ ( C ⊙ B ) T − X ( 1 ) ∣ ∣ min\left|\left| \hat{\boldsymbol{\mathcal{X}}}_{(1)}-\boldsymbol{\mathcal{X}}_{(1)} \right|\right|= min\left|\left| \hat{\bold{A}}\left(\bold{C}\odot\bold{B}\right)^T -\boldsymbol{\mathcal{X}_{(1)}} \right|\right| minX^(1)X(1)=minA^(CB)TX(1)

A ^ = X ( 1 ) [ ( C ⊙ B ) T ] † \hat{\bold{A}}=\boldsymbol{\mathcal{X}_{(1)}}\left[\left(\bold{C}\odot\bold{B}\right)^T\right]^{\dagger} A^=X(1)[(CB)T]

根据Khatri-Rao的性质 ( A ⊙ B ) T = A T A ∗ B T B \left(\bold{A}\odot\bold{B}\right)^T=\bold{A}^T\bold{A}\ast\bold{B}^T\bold{B} (AB)T=ATABTB ( A ⊙ B ) † = ( A T A ∗ B T B ) † ( A ⊙ B ) T \left(\bold{A}\odot\bold{B}\right)^{\dagger}=\left(\bold{A}^T\bold{A}\ast\bold{B}^T\bold{B}\right)^{\dagger}\left(\bold{A}\odot\bold{B}\right)^T (AB)=(ATABTB)(AB)T,公式(19)可转化为:
A ^ = X ( 1 ) ( C ⊙ B ) ( C T C ∗ B T B ) † \hat{\bold{A}}=\boldsymbol{\mathcal{X}_{(1)}} \left(\bold{C}\odot\bold{B}\right) \left(\bold{C}^T\bold{C}\ast\bold{B}^T\bold{B}\right)^{\dagger} A^=X(1)(CB)(CTCBTB)
在得到 A ^ \hat{\bold{A}} A^后,我们又可以通过 A ^ \hat{\bold{A}} A^ C \bold{C} C得到 B ^ \hat{\bold{B}} B^,再通过 A ^ \hat{\bold{A}} A^ B ^ \hat{\bold{B}} B^计算 C ^ \hat{\bold{C}} C^,从而完成一轮 A 、 B 、 C 、 Λ \bold{A}、\bold{B}、\bold{C}、\bold{\Lambda} ABCΛ的更新,重复此过程即可使 X ^ \hat{\boldsymbol{\mathcal{X}}} X^不断接近原始张量 X \boldsymbol{\mathcal{X}} X,最终完成CP分解。
B ^ = X ( 2 ) ( C ⊙ A ) ( C T C ∗ A T A ) † \hat{\bold{B}}=\boldsymbol{\mathcal{X}_{(2)}} \left(\bold{C}\odot\bold{A}\right) \left(\bold{C}^T\bold{C}\ast\bold{A}^T\bold{A}\right)^{\dagger} B^=X(2)(CA)(CTCATA)

C ^ = X ( 2 ) ( B ⊙ A ) ( B T B ∗ A T A ) † \hat{\bold{C}}=\boldsymbol{\mathcal{X}_{(2)}} \left(\bold{B}\odot\bold{A}\right) \left(\bold{B}^T\bold{B}\ast\bold{A}^T\bold{A}\right)^{\dagger} C^=X(2)(BA)(BTBATA)

对高阶张量 X ∈ R I 1 × I 2 × ⋯ × I N \boldsymbol{\mathcal{X}}\in\mathbb{R}^{I_1\times I_2\times\cdots\times I_N} XRI1×I2××IN以及参数矩阵集合 { A [ n ] ∈ R I n × R } n N \left\{\bold{A}_{[n]}\in\mathbb{R}^{I_n\times R}\right\}_n^N {A[n]RIn×R}nN的通用表达式为:
V = ( A [ N ] T A [ N ] ∗ ⋯ A [ n + 1 ] T A [ n + 1 ] ∗ A [ n − 1 ] T A [ n − 1 ] ∗ ⋯ ∗ A [ 1 ] T A [ 1 ] ) \bold{V}= \left( \bold{A}_{[N]}^T\bold{A}_{[N]}\ast\cdots\bold{A}_{[n+1]}^T\bold{A}_{[n+1]}\ast\bold{A}_{[n-1]}^T\bold{A}_{[n-1]}\ast\cdots\ast\bold{A}_{[1]}^T\bold{A}_{[1]} \right) V=(A[N]TA[N]A[n+1]TA[n+1]A[n1]TA[n1]A[1]TA[1])

A ^ [ n ] = X ( n ) ( A [ N ] ⊙ ⋯ ⊙ A [ n + 1 ] ⊙ A [ n − 1 ] ⊙ ⋯ ⊙ A [ 1 ] ) V † \hat{\bold{A}}_{[n]}=\boldsymbol{\mathcal{X}}_{(n)} \left( \bold{A}_{[N]}\odot\cdots\odot\bold{A}_{[n+1]}\odot\bold{A}_{[n-1]} \odot\cdots\odot\bold{A}_{[1]} \right)\bold{V}^{\dagger} A^[n]=X(n)(A[N]A[n+1]A[n1]A[1])V

程序实现

程序参照《Tensor Decompositions and Applications》实现,流程如下:

在这里插入图片描述

  • Step1: 初始化参数矩阵集合 { A [ n ] ∈ R I n × R } n N = A \left\{\bold{A}_{[n]}\in\mathbb{R}^{I_n\times R}\right\}_n^N=\text{A} {A[n]RIn×R}nN=A以及权重向量 λ = lbd \boldsymbol{\lambda}=\text{lbd} λ=lbd:
# 使用tensorly提供的函数初始化
lbd, A = initialize_cp(tensor, rank, init='svd', svd='numpy_svd',
                       random_state=0,
                       normalize_factors=True)
# 或自己进行随机初始化
A = []
lbd = tl.ones(rank)
for n in range(N):
    A.append(tl.tensor(np.random.random((tensor.shape[n], rank))))
  • Step2: 通过公式(23)计算 V \bold{V} V
# 使用None
V = None
for i in range(N):
	if i != n:
		if V is None:
			V = np.matmul(A[i].T, A[i])
		else:
			V = np.matmul(A[i].T, A[i]) * V

# 或将V初始化为RxR的1矩阵
V = np.ones((R, R))
for i in range(N):
	if i != n:
		V = np.matmul(A[i].T, A[i]) * V
  • Step3: 通过公式(24)计算 A ^ [ n ] = A[n] \hat{\bold{A}}_{[n]}=\text{A[n]} A^[n]=A[n]
T = khatri_rao(A, skip_matrix=n)
A[n] = np.matmul(np.matmul(tl.unfold(tensor, mode=n), T), np.linalg.pinv(V))

T = ( A [ N ] ⊙ ⋯ ⊙ A [ n + 1 ] ⊙ A [ n − 1 ] ⊙ ⋯ ⊙ A [ 1 ] ) T=\left( \bold{A}_{[N]}\odot\cdots\odot\bold{A}_{[n+1]}\odot\bold{A}_{[n-1]} \odot\cdots\odot\bold{A}_{[1]} \right) T=(A[N]A[n+1]A[n1]A[1])

X ( n ) = tl.unfold(tensor, mode=n) \boldsymbol{\mathcal{X}}_{(n)}=\text{tl.unfold(tensor, mode=n)} X(n)=tl.unfold(tensor, mode=n)

V † = np.linalg.pinv(V) \bold{V}^{\dagger}=\text{np.linalg.pinv(V)} V=np.linalg.pinv(V)

  • Step4: 对 A ^ [ n ] \hat{\bold{A}}_{[n]} A^[n]的每一列做归一化得到权重向量 λ \boldsymbol{\lambda} λ以及 A [ n ] \bold{A}_{[n]} A[n]
for r in range(R):
    lbd[r] = tl.norm(A[n][:, r])
A[n] = A[n] / tl.reshape(lbd, (1, -1))
  • Step5: 结束迭代的条件包括损失值足够小或不再变小,因子矩阵的变化很小,目标值接近于零,或者超过预定义的最大迭代次数。下面只实现了第一种作为示例:
tensor_pred = tl.fold(np.matmul(np.matmul(A[0], np.diag(lbd)), 
                                khatri_rao(A, skip_matrix=0).T),
                      mode=0,
                      shape=tensor.shape)
if tl.norm(tensor - tensor_pred) <= 1e-7:
    return A, lbd

l o s s = ∣ ∣ X ^ − X ∣ ∣ = tl.norm(tensor - tensor_pred) loss=\left|\left| \hat{\boldsymbol{\mathcal{X}}}-\boldsymbol{\mathcal{X}} \right|\right| =\text{tl.norm(tensor - tensor\_pred)} loss=X^X=tl.norm(tensor - tensor_pred)

X ^ = tensor_pred ,  计算方式参考公式(16) \hat{\boldsymbol{\mathcal{X}}}=\text{tensor\_pred}, \text{ 计算方式参考公式(16)} X^=tensor_pred, 计算方式参考公式(16)

Λ = np.diag(lbd) \bold{\Lambda}=\text{np.diag(lbd)} Λ=np.diag(lbd)

完整程序如下:

import numpy as np
import tensorly as tl
from tensorly.decomposition._cp import initialize_cp

from tensorly.tenalg import khatri_rao
from tqdm import tqdm

def cp_als(tensor: np.ndarray, R=1, max_iter=100):
    N = tl.ndim(tensor)
    # Step 1
    lbd, A = initialize_cp(tensor, R, init='svd', svd='numpy_svd',
                           random_state=0,
                           normalize_factors=True)
    # A = []
    # for n in range(N):
    #     np.random.seed(N)
    #     A.append(tl.tensor(np.random.random((tensor.shape[n], rank))))
    # lbd = tl.ones(rank)

    for epoch in tqdm(range(max_iter)):
        for n in range(N):
            # Step 2
            V = np.ones((R, R))
            for i in range(N):
                if i != n:
                    V = np.matmul(A[i].T, A[i]) * V
            # V = None
            # for i in range(N):
            #     if i != n:
            #         if V is None:
            #             V = np.matmul(A[i].T, A[i])
            #         else:
            #             V = np.matmul(A[i].T, A[i]) * V


            # Step 3
            T = khatri_rao(A, skip_matrix=n)
            A[n] = np.matmul(np.matmul(tl.unfold(tensor, mode=n), T), np.linalg.pinv(V))

            # Step 4
            for r in range(R):
                lbd[r] = tl.norm(A[n][:, r])
            A[n] = A[n] / tl.reshape(lbd, (1, -1))
		# Step 5
        tensor_pred = tl.fold(np.matmul(np.matmul(A[0], np.diag(lbd)),
                                        khatri_rao(A, skip_matrix=0).T),
                              mode=0,
                              shape=tensor.shape)
        if tl.norm(tensor - tensor_pred) <= 1e-7:
            return A, lbd, epoch

    return A, lbd, max_iter

if __name__ == '__main__':
    np.random.seed(10086)
    inpt = tl.tensor(np.random.random((3, 3, 3)), dtype=np.float32)
    A, lbd, epoch = cp_als(inpt, R=5, max_iter=1000)
    tensor_pred = tl.fold(np.matmul(np.matmul(A[0], np.diag(lbd)),
                                    khatri_rao(A, skip_matrix=0).T),
                          mode=0,
                          shape=inpt.shape)

    print(tl.norm(inpt - tensor_pred), epoch)
附加说明

initialize_cp中使用SVD初始化的方式:

  1. 为了初始化 A [ n ] \bold{A}_{[n]} A[n],需要先对 X ( n ) \boldsymbol{\mathcal{X}}_{(n)} X(n)做SVD得到左奇异矩阵 U ∈ R M × N \bold{U}\in\mathbb{R}^{M\times N} URM×N:

X ( n ) = U Σ V \boldsymbol{\mathcal{X}}_{(n)}=\bold{U}\bold{\Sigma}\bold{V} X(n)=UΣV

  1. 若选取的 R ≤ N \bold{R}\leq N RN,取左奇异矩阵 U \bold{U} U的前 R \bold{R} R列作为 A [ n ] \bold{A}_{[n]} A[n]即可:

A [ 1 ] = [ u : 1 , u : 2 , … , u : R ] \bold{A}_{[1]}=\left[\bold{u}_{:1},\bold{u}_{:2},\dots,\bold{u}_{:R}\right] A[1]=[u:1,u:2,,u:R]

  1. 若选取的 R > N \bold{R}> N R>N,则还需要生成随机矩阵 F ∈ R M × ( R − N ) \bold{F}\in\mathbb{R}^{M\times\left(R-N\right)} FRM×(RN)拼接到矩阵 U \bold{U} U的右侧作为 A [ n ] \bold{A}_{[n]} A[n]

BP-CP

BP(Back Propagation,反向传播),利用梯度下降法求解CP参数 { A [ n ] ∈ R I n × R } n = 1 N \left\{\bold{A}_{[n]}\in\mathbb{R}^{I_n\times R}\right\}^N_{n=1} {A[n]RIn×R}n=1N

公式推导

与ALS-CP相同,先以三阶张量为例,我们的目标是让由 A 、 B 、 C \bold{A}、\bold{B}、\bold{C} ABC估计得到的张量 X ^ \hat{\boldsymbol{\mathcal{X}}} X^尽可能的接近原始张量 X \boldsymbol{\mathcal{X}} X,因此将损失/目标函数设置为:
L = 1 2 [ X ( 1 ) − A ( C ⊙ B ) T ] 2 = 1 2 [ X ( 2 ) − B ( C ⊙ A ) T ] 2 = 1 2 [ X ( 3 ) − C ( B ⊙ A ) T ] 2 \begin{aligned} \bold{L} &=\frac{1}{2}\left[\boldsymbol{\mathcal{X}}_{(1)}-\bold{A}(\bold{C}\odot\bold{B})^T\right]^2\\ &=\frac{1}{2}\left[\boldsymbol{\mathcal{X}}_{(2)}-\bold{B}(\bold{C}\odot\bold{A})^T\right]^2\\ &=\frac{1}{2}\left[\boldsymbol{\mathcal{X}}_{(3)}-\bold{C}(\bold{B}\odot\bold{A})^T\right]^2 \end{aligned} L=21[X(1)A(CB)T]2=21[X(2)B(CA)T]2=21[X(3)C(BA)T]2
乘以 1 2 \frac{1}{2} 21是为了方便求导,为了简化表达式,设 Θ = X ( 1 ) − A ( C ⊙ B ) T \bold{\Theta}=\boldsymbol{\mathcal{X}}_{(1)}-\bold{A}(\bold{C}\odot\bold{B})^T Θ=X(1)A(CB)T,则公式(34)可以化作 l o s s = 1 2 ( Θ ) 2 loss=\frac{1}{2}\left(\bold{\Theta}\right)^2 loss=21(Θ)2,对 A \bold{A} A求偏导的结果为:
∂ L ∂ A = 2 × 1 2 Θ × ∂ Θ ∂ A = Θ ∂ X ( 1 ) − A ( C ⊙ B ) T ∂ A = − Θ ( C ⊙ B ) \begin{aligned} \frac{\partial\bold{L}}{\partial\bold{A}} &=2\times\frac{1}{2}\bold{\Theta}\times\frac{\partial\bold{\Theta}}{\partial\bold{A}}\\ &=\bold{\Theta}\frac{\partial\boldsymbol{\mathcal{X}}_{(1)}-\bold{A}(\bold{C}\odot\bold{B})^T}{\partial\bold{A}}\\ &=-\bold{\Theta}\left(\bold{C}\odot\bold{B}\right) \end{aligned} AL=2×21Θ×AΘ=ΘAX(1)A(CB)T=Θ(CB)
同理可以算出 ∂ L ∂ B \frac{\partial\bold{L}}{\partial\bold{B}} BL ∂ L ∂ C \frac{\partial\bold{L}}{\partial\bold{C}} CL,然后用梯度下降法更新参数即可,下式中 l r lr lr表示学习率:
A ← A − l r ∗ ∂ L ∂ A B ← B − l r ∗ ∂ L ∂ B C ← C − l r ∗ ∂ L ∂ C \begin{aligned} \bold{A}\leftarrow \bold{A}-lr\ast\frac{\partial\bold{L}}{\partial\bold{A}}\\ \bold{B}\leftarrow \bold{B}-lr\ast\frac{\partial\bold{L}}{\partial\bold{B}}\\ \bold{C}\leftarrow \bold{C}-lr\ast\frac{\partial\bold{L}}{\partial\bold{C}} \end{aligned} AAlrALBBlrBLCClrCL
综上所述,高阶张量的通用公式如下,损失函数为:
L = 1 2 [ X ( n ) − A [ n ] ( A [ N ] ⊙ ⋯ ⊙ A [ n + 1 ] ⊙ A [ n − 1 ] ⊙ ⋯ ⊙ A [ 1 ] ) T ] 2 \begin{aligned} \bold{L} &=\frac{1}{2}\left[\boldsymbol{\mathcal{X}}_{(n)}-\bold{A}_{[n]}\left(\bold{A}_{[N]}\odot\cdots\odot\bold{A}_{[n+1]}\odot\bold{A}_{[n-1]} \odot\cdots\odot\bold{A}_{[1]}\right)^T\right]^2 \end{aligned} L=21[X(n)A[n](A[N]A[n+1]A[n1]A[1])T]2
偏导为:
∂ L ∂ A [ n ] = 2 × 1 2 Θ × ∂ Θ ∂ A [ n ] = Θ ∂ X ( n ) − A [ n ] ( A [ N ] ⊙ ⋯ ⊙ A [ n + 1 ] ⊙ A [ n − 1 ] ⊙ ⋯ ⊙ A [ 1 ] ) T ∂ A [ n ] = − Θ ( A [ N ] ⊙ ⋯ ⊙ A [ n + 1 ] ⊙ A [ n − 1 ] ⊙ ⋯ ⊙ A [ 1 ] ) \begin{aligned} \frac{\partial\bold{L}}{\partial\bold{A}_{[n]}} &=2\times\frac{1}{2}\bold{\Theta}\times\frac{\partial\bold{\Theta}}{\partial\bold{A}_{[n]}}\\ &=\bold{\Theta}\frac{\partial\boldsymbol{\mathcal{X}}_{(n)}-\bold{A}_{[n]}\left(\bold{A}_{[N]}\odot\cdots\odot\bold{A}_{[n+1]}\odot\bold{A}_{[n-1]} \odot\cdots\odot\bold{A}_{[1]}\right)^T}{\partial\bold{A}_{[n]}}\\ &=-\bold{\Theta}\left(\bold{A}_{[N]}\odot\cdots\odot\bold{A}_{[n+1]}\odot\bold{A}_{[n-1]} \odot\cdots\odot\bold{A}_{[1]}\right) \end{aligned} A[n]L=2×21Θ×A[n]Θ=ΘA[n]X(n)A[n](A[N]A[n+1]A[n1]A[1])T=Θ(A[N]A[n+1]A[n1]A[1])

Θ = X ( n ) − A [ n ] ( A [ N ] ⊙ ⋯ ⊙ A [ n + 1 ] ⊙ A [ n − 1 ] ⊙ ⋯ ⊙ A [ 1 ] ) T \bold{\Theta}=\boldsymbol{\mathcal{X}}_{(n)}-\bold{A}_{[n]}\left(\bold{A}_{[N]}\odot\cdots\odot\bold{A}_{[n+1]}\odot\bold{A}_{[n-1]} \odot\cdots\odot\bold{A}_{[1]}\right)^T Θ=X(n)A[n](A[N]A[n+1]A[n1]A[1])T

参数更新公式为:
A [ n ] ← A [ n ] − l r ∗ ∂ L ∂ A [ n ] \bold{A}_{[n]}\leftarrow \bold{A}_{[n]}-lr\ast\frac{\partial\bold{L}}{\partial\bold{A}_{[n]}} A[n]A[n]lrA[n]L

程序实现
  • Step1: 初始化参数矩阵集合 { A [ n ] ∈ R I n × R } n N = A \left\{\bold{A}_{[n]}\in\mathbb{R}^{I_n\times R}\right\}_n^N=\text{A} {A[n]RIn×R}nN=A、梯度矩阵(Jacobian矩阵)集合 { L A [ n ] ∈ R I n × R } n N = grad_A \left\{\frac{\bold{L}}{\bold{A}_{[n]}}\in\mathbb{R}^{I_n\times R}\right\}_n^N=\text{grad\_A} {A[n]LRIn×R}nN=grad_A以及权重向量 λ = lbd \boldsymbol{\lambda}=\text{lbd} λ=lbd:
# 使用tensorly提供的函数初始化
lbd, A = initialize_cp(tensor, rank, init='random', svd='numpy_svd',
                       random_state=0,
                       normalize_factors=True)
# 或自己进行随机初始化
A = []
lbd = tl.ones(rank)
for n in range(N):
    A.append(tl.tensor(np.random.random((tensor.shape[n], rank))))
  • Step2: 在每个epoch开始时计算 Θ = theta \Theta=\text{theta} Θ=theta,并将梯度矩阵(Jacobian矩阵)集合 { L A [ n ] ∈ R I n × R } n N = grad_A \left\{\frac{\bold{L}}{\bold{A}_{[n]}}\in\mathbb{R}^{I_n\times R}\right\}_n^N=\text{grad\_A} {A[n]LRIn×R}nN=grad_A归零:
tensor_pred = tl.fold(np.matmul(np.matmul(A[0], np.diag(lbd)), 
                                khatri_rao(A, skip_matrix=0).T),
                      mode=0,
                      shape=tensor.shape)
theta = (tensor - tensor_pred)
grad_A = []
  • Step3: 计算每个参数矩阵 A [ n ] \bold{A}_{[n]} A[n]对应的梯度矩阵 L A [ n ] \frac{\bold{L}}{\bold{A}_{[n]}} A[n]L并存储,对应公式47:
for n in range(N):
    grad_A.append(np.zeros_like(A[n]))
    grad_A[n] = np.matmul(tl.unfold(theta, n), khatri_rao(A, skip_matrix=n))
  • Step4: 更新参数矩阵,对应公式49,减法变成加法是因为由公式47求得的梯度矩阵带有负号:
for n in range(N):
    A[n] = A[n] + lr * grad_A[n]
  • Step5: 最后计算以下每个epoch的损失值并输出,方便观察收敛情况,这里对 L \bold{L} L内元素求和输出:
loss = np.sum(0.5 * np.square(tl.unfold(theta, 0)))
print("epoch {}: loss={}".format(epoch, loss))

完整程序如下:

import numpy as np
import tensorly as tl
from tensorly.decomposition._cp import initialize_cp

from tensorly.tenalg import khatri_rao
from tqdm import tqdm


def cp_bp(tensor: np.ndarray, R=1, lr=1e-2, max_iter=100):
    N = tl.ndim(tensor)
    # Step 1
    lbd, A = initialize_cp(tensor, R, init='random', svd='numpy_svd',
                         random_state=0,
                         normalize_factors=True)
    
    for epoch in range(max_iter):
        # Step 2
        tensor_pred = tl.fold(np.matmul(np.matmul(A[0], np.diag(lbd)), 
        								khatri_rao(A, skip_matrix=0).T),
                              mode=0,
                              shape=tensor.shape)
        theta = (tensor - tensor_pred)
        grad_A = []
        
        # Step 3
        for n in range(N):
            grad_A.append(np.zeros_like(A[n]))
            grad_A[n] = np.matmul(tl.unfold(theta, n), khatri_rao(A, skip_matrix=n))
            
        # Step 4
        for n in range(N):
            A[n] = A[n] + lr * grad_A[n]
            
        # Step 5
        loss = np.sum(0.5 * np.square(tl.unfold(theta, 0)))
        print("epoch {}: loss={}".format(epoch, loss))
        
    return A, lbd

if __name__ == '__main__':
    np.random.seed(10086)
    inpt = tl.tensor(np.random.random((3, 3, 3)), dtype=np.float32)
    A, lbd = cp_bp(inpt, R=5, lr=1e-2, max_iter=100)
    tensor_pred_cp = tl.fold(np.matmul(np.matmul(A[0], np.diag(lbd)),
                                    khatri_rao(A, skip_matrix=0).T),
                          mode=0,
                          shape=inpt.shape)

    print("tensor_pred_cp: ", tl.norm(inpt - tensor_pred_cp), epoch)
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐