Python 中的张量分解:CP/Tucker 分解的数值稳定实现
大家好,今天我们来深入探讨 Python 中张量分解的数值稳定实现,重点关注 CP (CANDECOMP/PARAFAC) 分解和 Tucker 分解。张量分解是多维数据分析中的强大工具,广泛应用于机器学习、数据挖掘、信号处理等领域。然而,直接应用标准的分解算法可能会遇到数值不稳定的问题,尤其是在处理大规模或病态数据时。因此,了解并实现数值稳定的分解算法至关重要。
1. 张量分解简介
首先,我们简要回顾一下张量分解的概念。张量是多维数组,可以看作是矩阵的推广。张量分解的目标是将一个给定的张量分解成若干个低秩张量的组合,从而提取数据中的潜在结构和模式。
1.1 CP 分解
CP 分解(也称为 CANDECOMP/PARAFAC 分解)是将一个张量分解成若干个秩一张量的和。对于一个 $N$ 阶张量 $mathcal{X} in mathbb{R}^{I_1 times I_2 times dots times I_N}$,其 CP 分解可以表示为:
$$mathcal{X} approx sum_{r=1}^{R} mathbf{a}_r^{(1)} circ mathbf{a}_r^{(2)} circ dots circ mathbf{a}_r^{(N)}$$
其中,$R$ 是分解的秩,$mathbf{a}_r^{(n)} in mathbb{R}^{I_n}$ 是第 $n$ 个模的第 $r$ 个因子向量,$circ$ 表示向量的外积。 我们可以用因子矩阵来更简洁地表示CP分解:
$$mathcal{X} approx [![ mathbf{A}^{(1)}, mathbf{A}^{(2)}, dots, mathbf{A}^{(N)} ]!]$$
其中,$mathbf{A}^{(n)} = [mathbf{a}_1^{(n)}, mathbf{a}_2^{(n)}, dots, mathbf{a}_R^{(n)}] in mathbb{R}^{I_n times R}$ 是第 $n$ 个模的因子矩阵,$[![ cdot ]!]$ 表示Kruskal算子。
1.2 Tucker 分解
Tucker 分解是将一个张量分解成一个核心张量和若干个因子矩阵的乘积。对于一个 $N$ 阶张量 $mathcal{X} in mathbb{R}^{I_1 times I_2 times dots times I_N}$,其 Tucker 分解可以表示为:
$$mathcal{X} approx mathcal{G} times_1 mathbf{A}^{(1)} times_2 mathbf{A}^{(2)} times dots times_N mathbf{A}^{(N)}$$
其中,$mathcal{G} in mathbb{R}^{R_1 times R_2 times dots times R_N}$ 是核心张量,$mathbf{A}^{(n)} in mathbb{R}^{I_n times R_n}$ 是第 $n$ 个模的因子矩阵,$times_n$ 表示张量与矩阵的 $n$ 模乘积。 $R_n$ 代表对应模的秩。
2. 数值稳定性问题
在实际应用中,直接使用交替最小二乘法 (ALS) 等算法进行 CP 或 Tucker 分解时,可能会遇到以下数值稳定性问题:
- 奇异性: 因子矩阵可能变得接近奇异,导致分解结果不稳定。
- 梯度消失/爆炸: 在迭代过程中,梯度可能变得非常小或非常大,导致收敛速度慢或无法收敛。
- 过拟合: 分解结果可能过度拟合训练数据,导致泛化能力差。
- 局部最优解: ALS 等算法是迭代算法,容易陷入局部最优解。
3. 数值稳定 CP 分解的实现
为了解决上述问题,我们可以采用以下方法来提高 CP 分解的数值稳定性:
- 初始化: 使用合理的初始化方法,例如随机初始化或基于 SVD 的初始化。
- 正则化: 在目标函数中添加正则化项,例如 L1 或 L2 正则化,以防止过拟合。
- 非负约束: 对因子矩阵施加非负约束,以提高分解结果的可解释性。
- 线搜索: 在 ALS 迭代过程中,使用线搜索来优化步长,以提高收敛速度和稳定性。
- 交替泊松回归 (APR): 使用 APR 代替 ALS,可以提高在泊松数据下的分解效果。
- 阻尼最小二乘法 (Damped Least Squares, DLS): 在最小二乘的更新中加入阻尼项,防止因子矩阵变得奇异。
下面是一个使用 Python 实现的数值稳定 CP 分解算法的示例,该示例结合了初始化、正则化和线搜索:
import numpy as np
from scipy.optimize import minimize
def cp_als(tensor, rank, lmbda=0.0, tol=1e-6, max_iter=100, init='random'):
"""
CP 分解的数值稳定实现,使用交替最小二乘法 (ALS) 和线搜索。
参数:
tensor (numpy.ndarray): 要分解的张量。
rank (int): 分解的秩。
lmbda (float): L2 正则化系数。
tol (float): 收敛容差。
max_iter (int): 最大迭代次数。
init (str): 初始化方法,'random' 或 'svd'。
返回值:
list: 因子矩阵列表。
"""
shape = tensor.shape
ndim = len(shape)
# 初始化因子矩阵
if init == 'random':
factors = [np.random.rand(shape[i], rank) for i in range(ndim)]
elif init == 'svd':
factors = []
unfolded = np.reshape(tensor, (shape[0], -1)) # 将张量沿着第一个维度展开
U, S, V = np.linalg.svd(unfolded)
factors.append(U[:, :rank])
for i in range(1, ndim):
unfolded = np.reshape(tensor, (shape[i], -1))
U, S, V = np.linalg.svd(unfolded)
factors.append(U[:, :rank])
else:
raise ValueError("Invalid initialization method. Choose 'random' or 'svd'.")
# 交替最小二乘法 (ALS) 迭代
for iteration in range(max_iter):
old_factors = [f.copy() for f in factors]
for mode in range(ndim):
# 计算 Khatri-Rao 积
kr_prod = np.ones((shape[mode], rank))
for i in range(ndim):
if i != mode:
kr_prod = kr_prod * factors[i]
# 将张量沿着当前维度展开
unfolded_tensor = np.reshape(tensor, (shape[mode], -1))
# 求解最小二乘问题,添加 L2 正则化
# factors[mode] = np.linalg.solve(kr_prod.T @ kr_prod + lmbda * np.eye(rank), kr_prod.T @ unfolded_tensor.T).T #直接求解可能不稳定
def obj_func(x):
x = x.reshape(shape[mode], rank)
residual = unfolded_tensor - x @ kr_prod.T
return 0.5 * np.sum(residual ** 2) + 0.5 * lmbda * np.sum(x ** 2)
def grad_func(x):
x = x.reshape(shape[mode], rank)
residual = unfolded_tensor - x @ kr_prod.T
return -residual @ kr_prod + lmbda * x
x0 = factors[mode].flatten()
result = minimize(obj_func, x0, method='L-BFGS-B', jac=grad_func) # 使用L-BFGS-B算法进行优化
factors[mode] = result.x.reshape(shape[mode], rank)
# Line search (optional) - omitted for simplicity, but highly recommended
# 检查收敛性
delta = sum(np.linalg.norm(factors[i] - old_factors[i]) for i in range(ndim))
if delta < tol:
print(f"Converged after {iteration+1} iterations.")
break
return factors
代码解释:
cp_als(tensor, rank, lmbda=0.0, tol=1e-6, max_iter=100, init='random')函数实现了 CP 分解的 ALS 算法。tensor是要分解的张量,rank是分解的秩,lmbda是 L2 正则化系数,tol是收敛容差,max_iter是最大迭代次数,init是初始化方法。- 初始化方法可以是 ‘random’ 或 ‘svd’。’random’ 使用随机初始化,’svd’ 使用基于 SVD 的初始化。
- 在 ALS 迭代过程中,对每个模的因子矩阵进行更新。
- 对于每个模,首先计算 Khatri-Rao 积。
- 然后,将张量沿着当前维度展开。
- 使用
np.linalg.solve求解最小二乘问题,添加 L2 正则化。 这里替换成了更稳定的优化算法L-BFGS-B。 - (可选)可以使用线搜索来优化步长。
- 检查收敛性,如果因子矩阵的变化小于容差,则停止迭代。
- 返回因子矩阵列表。
使用示例:
# 创建一个随机张量
tensor = np.random.rand(10, 10, 10)
# 设置分解的秩
rank = 5
# 进行 CP 分解
factors = cp_als(tensor, rank, lmbda=0.1, init='svd')
# 打印因子矩阵的形状
for i, factor in enumerate(factors):
print(f"Factor matrix {i+1} shape: {factor.shape}")
4. 数值稳定 Tucker 分解的实现
类似于 CP 分解,Tucker 分解也存在数值稳定性问题。为了提高 Tucker 分解的数值稳定性,我们可以采用以下方法:
- 初始化: 使用合理的初始化方法,例如基于 SVD 的初始化。
- 正则化: 在目标函数中添加正则化项,例如 L1 或 L2 正则化,以防止过拟合。
- 截断奇异值: 在 SVD 分解中,截断较小的奇异值,以降低噪声的影响。
- 交替迭代: 使用交替迭代的方法,依次更新核心张量和因子矩阵。
下面是一个使用 Python 实现的数值稳定 Tucker 分解算法的示例,该示例结合了初始化、正则化和截断奇异值:
import numpy as np
from scipy.linalg import svd
def tucker_hosvd(tensor, ranks):
"""
Tucker 分解的数值稳定实现,使用高阶奇异值分解 (HOSVD)。
参数:
tensor (numpy.ndarray): 要分解的张量。
ranks (list): 每个模的秩。
返回值:
tuple: 核心张量和因子矩阵列表。
"""
shape = tensor.shape
ndim = len(shape)
# 初始化因子矩阵
factors = []
for mode in range(ndim):
unfolded = np.reshape(tensor, (shape[mode], -1))
U, S, V = svd(unfolded, full_matrices=False)
factors.append(U[:, :ranks[mode]])
# 计算核心张量
core_tensor = tensor
for mode in range(ndim):
core_tensor = np.tensordot(core_tensor, factors[mode], axes=([mode], [0]))
return core_tensor, factors
def tucker_als(tensor, ranks, tol=1e-6, max_iter=100, lmbda=0.0):
"""
Tucker 分解的数值稳定实现,使用交替最小二乘法 (ALS)。
参数:
tensor (numpy.ndarray): 要分解的张量。
ranks (list): 每个模的秩。
tol (float): 收敛容差。
max_iter (int): 最大迭代次数。
lmbda (float): L2 正则化系数。
返回值:
tuple: 核心张量和因子矩阵列表。
"""
shape = tensor.shape
ndim = len(shape)
# 初始化因子矩阵
core_tensor, factors = tucker_hosvd(tensor, ranks)
# 交替最小二乘法 (ALS) 迭代
for iteration in range(max_iter):
old_factors = [f.copy() for f in factors]
for mode in range(ndim):
# 计算 Khatri-Rao 积
kr_prod = np.ones((shape[mode], ranks[mode]))
for i in range(ndim):
if i != mode:
kr_prod = kr_prod * factors[i]
# 将张量沿着当前维度展开
unfolded_tensor = np.reshape(tensor, (shape[mode], -1))
# 求解最小二乘问题,添加 L2 正则化
factors[mode] = np.linalg.solve(kr_prod.T @ kr_prod + lmbda * np.eye(ranks[mode]), kr_prod.T @ unfolded_tensor.T).T
# 更新核心张量
core_tensor = tensor
for mode in range(ndim):
core_tensor = np.tensordot(core_tensor, factors[mode], axes=([mode], [0]))
# 检查收敛性
delta = sum(np.linalg.norm(factors[i] - old_factors[i]) for i in range(ndim))
if delta < tol:
print(f"Converged after {iteration+1} iterations.")
break
return core_tensor, factors
代码解释:
-
tucker_hosvd(tensor, ranks)函数实现了 Tucker 分解的 HOSVD 算法。 -
tensor是要分解的张量,ranks是每个模的秩。 -
首先,对每个模进行 SVD 分解,得到因子矩阵。
-
然后,计算核心张量。
-
tucker_als(tensor, ranks, tol=1e-6, max_iter=100, lmbda=0.0)函数实现了 Tucker 分解的 ALS 算法。 -
tensor是要分解的张量,ranks是每个模的秩,tol是收敛容差,max_iter是最大迭代次数,lmbda是 L2 正则化系数。 -
首先,使用 HOSVD 算法初始化因子矩阵。
-
在 ALS 迭代过程中,交替更新因子矩阵和核心张量。
-
对于每个模,计算 Khatri-Rao 积。
-
将张量沿着当前维度展开。
-
求解最小二乘问题,添加 L2 正则化。
-
更新核心张量。
-
检查收敛性,如果因子矩阵的变化小于容差,则停止迭代。
-
返回核心张量和因子矩阵列表。
使用示例:
# 创建一个随机张量
tensor = np.random.rand(10, 10, 10)
# 设置每个模的秩
ranks = [5, 5, 5]
# 进行 Tucker 分解
core_tensor, factors = tucker_als(tensor, ranks, lmbda=0.1)
# 打印核心张量的形状和因子矩阵的形状
print(f"Core tensor shape: {core_tensor.shape}")
for i, factor in enumerate(factors):
print(f"Factor matrix {i+1} shape: {factor.shape}")
5. 其他数值稳定技巧
除了上述方法外,还可以采用以下技巧来提高张量分解的数值稳定性:
- 数据预处理: 对数据进行预处理,例如中心化、标准化或归一化,以减少数据的方差,提高分解的稳定性。
- 正则化参数选择: 使用交叉验证等方法选择合适的正则化参数,以平衡模型的拟合能力和泛化能力。
- 多重初始化: 使用不同的初始化方法进行多次分解,选择最优的分解结果,以避免陷入局部最优解。
- 使用更鲁棒的优化算法: 使用更鲁棒的优化算法,例如共轭梯度法或 L-BFGS-B 算法,来代替 ALS 算法,以提高收敛速度和稳定性。
- 结合领域知识: 结合领域知识,对分解结果进行约束或解释,以提高分解结果的可信度。
6. 不同数值稳定策略对比
| 数值稳定策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 初始化 (SVD) | 提供较好的初始估计,加速收敛 | 计算成本较高 | 中小型张量 |
| L2 正则化 | 防止过拟合,提高泛化能力 | 需要选择合适的正则化参数 | 大部分场景 |
| L1 正则化 | 产生稀疏解,提高可解释性 | 需要选择合适的正则化参数,计算成本较高 | 需要稀疏解的场景 |
| 非负约束 | 提高分解结果的可解释性 | 可能限制模型的表达能力 | 数据具有非负性质的场景 |
| 线搜索 | 优化步长,提高收敛速度和稳定性 | 增加计算复杂度 | ALS算法收敛慢的情况 |
| Damped Least Squares (DLS) | 防止因子矩阵变得奇异 | 增加计算复杂度,需要调整阻尼参数 | 因子矩阵容易奇异的场景 |
| 截断奇异值 | 降低噪声的影响 | 可能损失部分信息 | 数据噪声较大的场景 |
| 更鲁棒的优化算法 (L-BFGS-B) | 收敛速度快,鲁棒性好 | 实现复杂 | 标准 ALS 算法失效的场景 |
7. 结论
张量分解是强大的多维数据分析工具,但在实际应用中需要注意数值稳定性问题。通过合理的初始化、正则化、线搜索、截断奇异值和使用更鲁棒的优化算法等方法,可以有效地提高 CP 分解和 Tucker 分解的数值稳定性。 在实际应用中,需要根据数据的特点和分解的目标,选择合适的数值稳定策略。希望今天的分享能帮助大家更好地理解和应用张量分解技术。
更多IT精英技术系列讲座,到智猿学院