# 实战指南:用Python的Tensorly库快速掌握张量分解核心技巧
如果你正在处理一些“奇怪”的数据——比如一个同时包含用户、商品、时间、评分四个维度的推荐系统日志,或者是一组包含空间、时间和光谱通道的多维图像数据——那么恭喜你,你已经进入了**高阶张量**的世界。传统的二维表格和矩阵在这里显得力不从心,而张量分解,特别是**CP分解**和**Tucker分解**,正是为你解开这些高维数据内在结构的钥匙。
过去,张量分解的理论门槛让很多实践者望而却步,复杂的数学公式和迭代算法令人头疼。但现在,借助Python生态中强大的`Tensorly`库,我们完全可以在几分钟内完成从数据加载到分解结果可视化的全过程,把精力真正聚焦在解决实际问题上。这篇文章就是为你准备的快速上手指南,无论你是数据科学初学者,还是希望将张量方法引入现有工作流的中级从业者,都能在这里找到可立即运行的代码和清晰的思路。
我们将彻底避开繁琐的数学推导,直接切入如何在`Tensorly`中调用几个关键函数,如何解读输出结果,以及如何将这些结果应用到推荐系统、图像压缩等真实场景中。你会发现,掌握这些工具比想象中简单得多。
## 1. 环境搭建与张量基础速成
在开始分解之前,我们需要一个干净的工作环境。我强烈建议使用`conda`或`venv`创建一个独立的Python环境,这能避免库版本冲突带来的各种诡异问题。
```bash
# 创建并激活一个名为tensor_demo的虚拟环境(conda方式)
conda create -n tensor_demo python=3.9
conda activate tensor_demo
# 安装核心库
pip install tensorly
pip install numpy scipy matplotlib jupyter
```
> 提示:`Tensorly`是一个专门为张量运算和分解设计的库,其API设计非常直观。同时安装`numpy`和`scipy`是因为它们是`Tensorly`的底层依赖,而`matplotlib`则用于后续的结果可视化。
安装完成后,我们首先来理解一下`Tensorly`中张量的基本操作。张量本质上就是一个多维数组。在代码中,我们通常用NumPy数组来表示它。
```python
import numpy as np
import tensorly as tl
# 创建一个随机的3阶张量,形状为 (用户数, 商品数, 时间点)
# 例如:5个用户,4种商品,3个时间点的评分数据
tensor_data = np.random.randn(5, 4, 3)
print(f"张量形状: {tensor_data.shape}")
print(f"张量在位置(0,1,2)的值: {tensor_data[0, 1, 2]}")
```
在`Tensorly`中,张量的“模”(mode)指的是它的维度。对于一个3阶张量(用户,商品,时间),它有3个模。**模展开**(unfolding)是一个关键操作,它把张量沿着某个维度“铺平”成一个矩阵。理解这个概念对后续分解至关重要。
```python
# 沿着第0个模(用户维)展开张量
unfolded_0 = tl.unfold(tensor_data, mode=0)
print(f"沿模0展开后的矩阵形状: {unfolded_0.shape}")
# 输出: (5, 12) 即 (用户数, 商品数*时间点)
# 我们可以再把它折叠回去
folded_back = tl.fold(unfolded_0, mode=0, shape=tensor_data.shape)
print(f"重新折叠后数据是否一致: {np.allclose(tensor_data, folded_back)}")
```
为了让大家对后续的分解结果有一个直观的期待,我们先来看一个最简单的应用场景:用张量分解来“猜测”缺失的数据。假设我们有一个用户-商品-时间的评分张量,其中很多值是未知的(NaN)。
```python
# 模拟一个带有缺失值的评分张量
rating_tensor = np.random.rand(5, 4, 3)
mask = np.random.rand(5, 4, 3) > 0.7 # 随机掩盖30%的数据作为缺失
rating_tensor_with_missing = rating_tensor.copy()
rating_tensor_with_missing[mask] = np.nan
print(f"原始张量中的缺失值数量: {np.sum(np.isnan(rating_tensor_with_missing))}")
```
接下来的CP和Tucker分解,其核心目标之一就是利用数据的低秩结构,来预测这些缺失的值。这是一种比简单矩阵分解更强大的协同过滤技术。
## 2. 五分钟上手CP分解:从调用到解读
**CP分解**(Canonical Polyadic Decomposition)的思想非常直观:它试图将一个复杂的张量,分解成一系列“秩一张量”的和。你可以把它想象成,把一道混合了多种食材的浓汤,分解出其中每一种基础原料。在推荐系统中,这些“原料”可能对应着潜在的用户偏好因子、商品属性因子和时间趋势因子。
在`Tensorly`中,执行一个CP分解只需要一行代码。但在这之前,我们需要决定一个关键参数:**秩(rank)**。这个秩决定了我们想用多少个“基础成分”来近似原始张量。秩太小,近似会太粗糙;秩太大,则可能引入噪声并导致过拟合。对于初学者,一个经验法则是从较小的秩开始尝试。
```python
from tensorly.decomposition import parafac
# 对一个3阶张量进行秩为2的CP分解
rank = 2
factors = parafac(tensor_data, rank=rank)
```
`parafac`函数返回的是一个`CP`类的对象。我们如何解读它?它包含了一个重要的属性:`factors`,这是一个列表,列表中的每个元素都是一个矩阵,对应一个模的因子矩阵。
```python
# 查看分解结果
print(f"CP分解得到的因子矩阵数量(等于张量的阶数): {len(factors.factors)}")
for i, factor_matrix in enumerate(factors.factors):
print(f"模 {i} 的因子矩阵形状: {factor_matrix.shape}")
# 对于形状为(5,4,3)的张量,秩为2时:
# 模0因子矩阵形状: (5, 2) -> 5个用户,2个潜在因子上的得分
# 模1因子矩阵形状: (4, 2) -> 4个商品,2个潜在因子上的属性
# 模2因子矩阵形状: (3, 2) -> 3个时间点,2个潜在因子的强度变化
```
仅仅得到因子矩阵还不够,我们通常需要评估分解的质量。最常用的指标是**重构误差**,即分解后再组合起来的张量与原始张量之间的差异。
```python
# 从分解结果重构出近似张量
reconstructed_tensor = tl.cp_to_tensor(factors)
# 计算相对重构误差
error = tl.norm(tensor_data - reconstructed_tensor) / tl.norm(tensor_data)
print(f"CP分解(秩={rank})的相对重构误差: {error:.4f}")
```
在实际项目中,我们往往需要尝试不同的秩,并观察误差的变化,以选择一个合适的值。下面是一个简单的秩选择实验:
```python
import matplotlib.pyplot as plt
ranks = [1, 2, 3, 4, 5]
errors = []
for r in ranks:
factors_r = parafac(tensor_data, rank=r, init='random', tol=1e-6)
recon_r = tl.cp_to_tensor(factors_r)
err = tl.norm(tensor_data - recon_r) / tl.norm(tensor_data)
errors.append(err)
plt.figure(figsize=(8,5))
plt.plot(ranks, errors, 'o-', linewidth=2)
plt.xlabel('CP分解的秩 (Rank)')
plt.ylabel('相对重构误差')
plt.title('不同秩下的CP分解误差')
plt.grid(True, alpha=0.3)
plt.show()
```
运行这段代码,你会看到误差随着秩的增加而下降的曲线。通常,曲线会出现一个“拐点”,这个点对应的秩就是一个不错的候选值,因为它意味着再增加秩所带来的精度提升已经不明显了。
> 注意:CP分解对初始值比较敏感,`parafac`函数提供了`init`参数(如`'random'`, `'svd'`)来控制初始化方式。对于重要任务,建议用不同的随机种子多运行几次,选择结果最好(重构误差最小)的那一次。
为了让分解结果更具可解释性,我们可以将每个模的因子矩阵可视化。例如,在用户-商品-时间的场景中,我们可以绘制出“潜在因子1”和“潜在因子2”在各个时间点上的强度变化。
```python
# 假设factors是上面秩为2的分解结果
time_factors = factors.factors[2] # 模2(时间)的因子矩阵,形状(3,2)
plt.figure(figsize=(10,4))
plt.subplot(1,2,1)
plt.plot(range(3), time_factors[:, 0], 's-', label='潜在因子1')
plt.xlabel('时间点')
plt.ylabel('因子强度')
plt.title('潜在因子1随时间变化')
plt.legend()
plt.subplot(1,2,2)
plt.plot(range(3), time_factors[:, 1], 'o-', color='orange', label='潜在因子2')
plt.xlabel('时间点')
plt.ylabel('因子强度')
plt.title('潜在因子2随时间变化')
plt.legend()
plt.tight_layout()
plt.show()
```
通过这样的可视化,你或许能发现,因子1可能代表了“工作日偏好”,因子2代表了“周末偏好”,从而对业务产生直接的洞察。
## 3. 深入Tucker分解:灵活的多模低秩近似
如果说CP分解追求的是极致的简洁(用最少的秩一张量之和),那么**Tucker分解**则提供了更大的灵活性。它允许你为张量的每一个维度(模)单独指定一个秩,并通过一个小的**核心张量**(core tensor)来精确刻画这些维度之间的交互模式。这就像不仅分解出汤的原料,还得到了一个描述“哪种原料和哪种原料搭配会产生什么风味”的配方手册。
在`Tensorly`中,最常用的Tucker分解算法是**高阶奇异值分解(HOSVD)**和其改进版**高阶正交迭代(HOOI)**。我们先从最简单的HOSVD开始。
```python
from tensorly.decomposition import tucker
# 使用HOSVD进行Tucker分解,为每个模指定秩
# 对于形状(5,4,3)的张量,我们指定多模秩为(3,2,2)
rank = (3, 2, 2)
core, factors = tucker(tensor_data, rank=rank, init='svd', tol=1e-6, n_iter_max=100)
```
这里,`tucker`函数返回两个结果:
1. `core`: 核心张量,形状与我们指定的多模秩`(3,2,2)`一致。
2. `factors`: 一个列表,包含每个模的因子矩阵(通常是正交矩阵)。
让我们来检查一下这些输出:
```python
print(f"核心张量形状: {core.shape}")
print(f"核心张量的大小远小于原始张量: {core.size} vs {tensor_data.size}")
for i, (f_mat, r) in enumerate(zip(factors, rank)):
print(f"模{i}因子矩阵形状: {f_mat.shape} (应为: ({tensor_data.shape[i]}, {r}))")
```
Tucker分解的重构方式与CP不同,它需要将核心张量与各个因子矩阵进行**模乘**(mode-n product)。
```python
# 从Tucker分解结果重构张量
reconstructed_tucker = tl.tucker_to_tensor((core, factors))
# 计算误差
error_tucker = tl.norm(tensor_data - reconstructed_tucker) / tl.norm(tensor_data)
print(f"Tucker分解(秩{rank})的相对重构误差: {error_tucker:.4f}")
```
**如何为每个模选择合适的秩?** 这是Tucker分解实践中的核心挑战。一个有效的方法是分析每个模展开矩阵的**奇异值衰减情况**。奇异值下降缓慢的模,可能需要保留更高的秩来捕捉信息。
```python
def suggest_tucker_rank(tensor, variance_threshold=0.95):
"""基于解释方差比例,为每个模建议Tucker秩"""
suggested_ranks = []
for mode in range(tl.ndim(tensor)):
# 沿该模展开
matrix = tl.unfold(tensor, mode)
# 计算奇异值(通过SVD)
U, S, Vt = np.linalg.svd(matrix, full_matrices=False)
# 计算累积解释方差比例
cum_var = np.cumsum(S**2) / np.sum(S**2)
# 找到达到阈值的第一个索引
rank = np.argmax(cum_var >= variance_threshold) + 1
suggested_ranks.append(rank)
return suggested_ranks
suggested = suggest_tucker_rank(tensor_data, 0.9)
print(f"基于90%方差保留建议的多模秩: {suggested}")
```
在实际应用中,我们经常使用**HOOI算法**来优化HOSVD得到的初始结果。HOOI通过迭代的方式,寻找在给定多模秩约束下重构误差更小的解。在`Tensorly`中,只需在`tucker`函数中指定`n_iter_max`为一个较大的数(如100),并使用`init='svd'`(即先进行HOSVD),它就会自动执行HOOI迭代。
```python
# 使用HOOI进行优化(通过增加迭代次数)
core_hooi, factors_hooi = tucker(tensor_data, rank=rank, init='svd', tol=1e-8, n_iter_max=500)
recon_hooi = tl.tucker_to_tensor((core_hooi, factors_hooi))
error_hooi = tl.norm(tensor_data - recon_hooi) / tl.norm(tensor_data)
print(f"经过HOOI优化后的重构误差: {error_hooi:.4f}")
```
通常,HOOI优化后的误差会比初始的HOSVD更低。为了更深入地理解核心张量的作用,我们可以尝试将其可视化。对于一个3阶的核心张量,我们可以将其切片展示。
```python
# 可视化核心张量的前两个切片(针对前两个模)
fig, axes = plt.subplots(1, core.shape[2], figsize=(4*core.shape[2], 4))
for i in range(core.shape[2]):
im = axes[i].imshow(core[:, :, i], cmap='RdBu_r', aspect='auto')
axes[i].set_title(f'核心张量 - 模2索引={i}')
plt.colorbar(im, ax=axes[i])
plt.suptitle('Tucker核心张量切片可视化', fontsize=14)
plt.tight_layout()
plt.show()
```
核心张量中的每个元素,都代表了不同模的因子之间交互的强度。数值大的位置,意味着对应的因子组合在原始数据中扮演了重要角色。
## 4. 实战演练:构建一个简单的张量推荐模型
理论说得再多,不如动手实践。让我们用一个完整的微型案例,将CP分解和Tucker分解应用到推荐系统场景中。假设我们有一个小型数据集,记录了5个用户对4件商品在3个不同时间段的评分(1-5分)。我们的目标是预测缺失的评分。
首先,我们模拟并创建这个带有缺失值的张量。
```python
np.random.seed(42) # 固定随机种子,确保结果可复现
# 生成一个低秩结构的“真实”评分张量(便于模拟)
true_user_factors = np.random.randn(5, 2)
true_item_factors = np.random.randn(4, 2)
true_time_factors = np.random.randn(3, 2)
# 通过CP结构生成张量(秩为2)
true_rating = np.einsum('ir,jr,kr->ijk', true_user_factors, true_item_factors, true_time_factors)
# 将评分缩放到1-5分,并添加一些噪声
true_rating = 3 + 2 * (true_rating / np.max(np.abs(true_rating))) + 0.1 * np.random.randn(5,4,3)
true_rating = np.clip(true_rating, 1, 5)
# 随机掩盖部分数据作为训练集(缺失值)
mask = np.random.rand(5,4,3) < 0.7 # 70%的数据作为观测值
train_tensor = true_rating.copy()
train_tensor[~mask] = np.nan # 将未观测到的位置设为NaN
print(f"观测到的评分数量: {np.sum(mask)} / {5*4*3}")
print(f"缺失的评分数量: {np.sum(~mask)}")
```
我们的任务是利用`mask`为`True`的那些已知评分,来训练一个模型,并预测`mask`为`False`位置的评分。`Tensorly`提供了直接处理缺失值的分解函数。
**方案一:使用带缺失值的CP分解**
```python
from tensorly.decomposition import non_negative_parafac_hals
# 为了处理缺失值,我们需要一个权重张量,在观测位置为1,缺失位置为0
weight_mask = mask.astype(float)
# 使用非负CP分解(HALS算法),这对评分数据是合理的假设(评分非负)
cp_rank = 2
cp_factors_missing = non_negative_parafac_hals(train_tensor, rank=cp_rank, n_iter_max=1000, tol=1e-6, sparsity_coefficients=None, mask=weight_mask)
# 重构出完整的评分张量
cp_recon_full = tl.cp_to_tensor(cp_factors_missing)
# 计算在测试集(缺失部分)上的预测误差
test_indices = np.where(~mask)
cp_predictions = cp_recon_full[test_indices]
cp_ground_truth = true_rating[test_indices]
cp_mae = np.mean(np.abs(cp_predictions - cp_ground_truth))
cp_rmse = np.sqrt(np.mean((cp_predictions - cp_ground_truth)**2))
print(f"[CP分解] 在测试集上的平均绝对误差(MAE): {cp_mae:.3f}")
print(f"[CP分解] 在测试集上的均方根误差(RMSE): {cp_rmse:.3f}")
```
**方案二:使用带缺失值的Tucker分解**
对于Tucker分解,`Tensorly`没有直接内置处理缺失值的函数。一个常见的技巧是使用**交替最小二乘法(ALS)**进行迭代填充:先用已知值初始化缺失值(例如用全局平均值),然后进行Tucker分解,用分解结果更新缺失值的预测,再重新分解,如此迭代。
```python
def tucker_als_with_missing(tensor, rank, mask, n_iter=50, init='svd'):
"""使用ALS处理带有缺失值的Tucker分解"""
# 初始化:用观测值的平均值填充缺失部分
filled_tensor = tensor.copy()
global_mean = np.nanmean(tensor)
filled_tensor[~mask] = global_mean
for it in range(n_iter):
# 对填充后的完整张量进行Tucker分解
core, factors = tucker(filled_tensor, rank=rank, init=init, n_iter_max=10)
# 重构
recon = tl.tucker_to_tensor((core, factors))
# 仅用重构值更新缺失部分,保持观测值不变
filled_tensor[~mask] = recon[~mask]
return core, factors, filled_tensor
tucker_rank = (3, 3, 2)
core_als, factors_als, tucker_recon_full = tucker_als_with_missing(train_tensor, tucker_rank, mask, n_iter=30)
tucker_predictions = tucker_recon_full[test_indices]
tucker_mae = np.mean(np.abs(tucker_predictions - cp_ground_truth))
tucker_rmse = np.sqrt(np.mean((tucker_predictions - cp_ground_truth)**2))
print(f"[Tucker分解+ALS] 在测试集上的平均绝对误差(MAE): {tucker_mae:.3f}")
print(f"[Tucker分解+ALS] 在测试集上的均方根误差(RMSE): {tucker_rmse:.3f}")
```
为了直观对比两种方法的预测效果,我们可以将预测值与真实值画成散点图。
```python
plt.figure(figsize=(12,5))
plt.subplot(1,2,1)
plt.scatter(cp_ground_truth, cp_predictions, alpha=0.6)
plt.plot([1,5], [1,5], 'r--') # 对角线,理想情况
plt.xlabel('真实评分')
plt.ylabel('CP分解预测评分')
plt.title(f'CP分解预测 vs 真实值 (MAE={cp_mae:.3f})')
plt.grid(True, alpha=0.3)
plt.subplot(1,2,2)
plt.scatter(cp_ground_truth, tucker_predictions, alpha=0.6, color='green')
plt.plot([1,5], [1,5], 'r--')
plt.xlabel('真实评分')
plt.ylabel('Tucker分解预测评分')
plt.title(f'Tucker分解预测 vs 真实值 (MAE={tucker_mae:.3f})')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
```
通过这个简单的对比,你可能会发现,在这个小规模模拟数据上,CP分解和Tucker分解的表现可能相近,也可能一方略优。这取决于数据内在的结构。CP分解假设数据可以由少数几个全局的秩一成分叠加而成,而Tucker分解则允许更复杂的、各维度独立压缩的交互模式。在实际项目中,最好的方法是两种都尝试,并通过交叉验证来选择。
最后,我们可以将预测结果整理成一个更直观的表格,看看针对某个特定用户,模型对其未评分商品在不同时间段的预测情况。
```python
# 假设我们关注用户0(索引为0)
user_id = 0
print(f"\n为用户 {user_id} 生成的评分预测:")
print("商品 | 时间1 | 时间2 | 时间3")
print("-"*30)
for item in range(4):
preds = []
for time in range(3):
if mask[user_id, item, time]:
preds.append(f"{true_rating[user_id, item, time]:.1f}(真)") # 显示真实值
else:
# 这里展示CP分解的预测结果
preds.append(f"{cp_recon_full[user_id, item, time]:.1f}")
print(f"{item:4d} | {preds[0]:^6} | {preds[1]:^6} | {preds[2]:^6}")
```
这个表格能清晰地展示出模型在哪里进行了预测,以及预测的值是多少。在实际的推荐系统中,我们可以对这些预测分数进行排序,将分数最高的商品推荐给相应用户。
## 5. 进阶技巧与性能优化指南
当你熟悉了基础操作后,可能会遇到更大规模的数据或更复杂的需求。本章节分享一些我在实际项目中积累的进阶技巧,帮助你提升代码的效率和稳定性。
**技巧一:处理大规模张量——使用随机化算法**
当张量非常大时,标准的SVD计算会变得非常缓慢且内存消耗巨大。`Tensorly`的`randomized_svd`模块提供了基于随机投影的近似SVD,能显著加速计算,尤其适用于Tucker分解中的HOSVD步骤。
```python
from tensorly.random import check_random_state
from tensorly.decomposition._tucker import partial_tucker
# 生成一个更大的张量
large_tensor = np.random.randn(100, 80, 60)
large_rank = (20, 15, 10)
# 使用随机化SVD进行部分Tucker分解(仅分解指定的模)
# 这里我们只对前两个模进行压缩
modes = [0, 1]
core, factors = partial_tucker(large_tensor, rank=large_rank, modes=modes, init='randomized_svd', n_iter_max=5)
print(f"部分Tucker分解完成。核心张量形状: {core.shape}")
```
**技巧二:利用GPU加速计算**
如果你的机器有NVIDIA GPU,可以轻松地使用`cupy`或`torch`后端来加速`Tensorly`的运算。只需在导入时设置后端即可。
```python
# 方法1: 使用PyTorch后端 (需要安装torch)
import tensorly as tl
tl.set_backend('pytorch')
import torch
# 创建PyTorch张量
tensor_torch = torch.randn(50, 40, 30)
# 进行CP分解,计算会在GPU上自动进行(如果可用)
factors_torch = tl.decomposition.parafac(tensor_torch, rank=5)
print(f"使用PyTorch后端,因子矩阵类型: {type(factors_torch.factors[0])}")
# 切换回numpy后端
tl.set_backend('numpy')
```
**技巧三:为分解添加约束(非负性、稀疏性)**
在许多实际场景中,因子矩阵或核心张量具有物理意义,需要非负(如化学浓度、图像像素)或稀疏(许多因子为零)。`Tensorly`提供了相应的约束分解函数。
```python
# 非负CP分解 (之前已使用过non_negative_parafac_hals)
# 非负Tucker分解
from tensorly.decomposition import non_negative_tucker
data_nonneg = np.abs(np.random.randn(10, 10, 10)) # 模拟非负数据
core_nn, factors_nn = non_negative_tucker(data_nonneg, rank=(5,5,5), n_iter_max=100, tol=1e-6)
# 检查分解结果是否真的非负
print(f"核心张量最小值: {core_nn.min():.2e}")
print(f"因子矩阵最小值: {min([f.min() for f in factors_nn]):.2e}")
```
**技巧四:使用Tensorly进行张量补全(完整案例)**
张量补全是推荐系统和图像修复中的核心任务。下面是一个更稳健的补全流程,结合了初始化、迭代和早停策略。
```python
def robust_tensor_completion(tensor_with_missing, rank, method='cp', max_iter=200, patience=10):
"""
一个更稳健的张量补全函数。
参数:
tensor_with_missing: 带有NaN值的张量
rank: 分解的秩 (CP为整数,Tucker为元组)
method: 'cp' 或 'tucker'
max_iter: 最大迭代次数
patience: 验证误差不再改善的容忍迭代次数
"""
mask = ~np.isnan(tensor_with_missing)
tensor_filled = tensor_with_missing.copy()
# 初始化:用每根纤维(fiber)的平均值填充,若全为NaN则用全局平均值
for indices in np.ndindex(tensor_filled.shape):
if not mask[indices]:
# 尝试找到同一条纤维上其他已知值的平均值
# 这里简化处理:使用全局平均值
tensor_filled[indices] = np.nanmean(tensor_with_missing)
best_error = np.inf
patience_counter = 0
history = []
for iteration in range(max_iter):
if method == 'cp':
factors = parafac(tensor_filled, rank=rank, init='svd', n_iter_max=1)
recon = tl.cp_to_tensor(factors)
else: # tucker
core, factors = tucker(tensor_filled, rank=rank, init='svd', n_iter_max=1)
recon = tl.tucker_to_tensor((core, factors))
# 计算在已知数据上的重构误差
known_vals = tensor_with_missing[mask]
recon_known = recon[mask]
current_error = np.mean((known_vals - recon_known) ** 2)
history.append(current_error)
# 早停检查
if current_error < best_error:
best_error = current_error
best_recon = recon.copy()
patience_counter = 0
else:
patience_counter += 1
if patience_counter >= patience:
print(f"早停在第{iteration}次迭代")
break
# 用重构值更新缺失部分
tensor_filled[~mask] = recon[~mask]
return best_recon, history
# 使用示例
tensor_missing_large = np.random.randn(20,20,20)
mask_large = np.random.rand(*tensor_missing_large.shape) > 0.5
tensor_missing_large[~mask_large] = np.nan
completed_cp, hist_cp = robust_tensor_completion(tensor_missing_large, rank=5, method='cp')
completed_tucker, hist_tucker = robust_tensor_completion(tensor_missing_large, rank=(8,8,5), method='tucker')
# 绘制误差下降曲线
plt.figure()
plt.plot(hist_cp, label='CP分解')
plt.plot(hist_tucker, label='Tucker分解')
plt.xlabel('迭代次数')
plt.ylabel('在已知数据上的MSE')
plt.title('张量补全误差收敛过程')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
```
这个函数展示了如何构建一个简单的迭代补全框架,并加入了早停机制以防止过拟合。在实际应用中,你可能还需要考虑更复杂的初始化策略(如使用其他低秩分解方法的结果进行热启动)和更精细的误差评估。
**技巧五:结果的持久化与加载**
训练好的分解模型(因子矩阵、核心张量)可以保存下来,供后续预测或分析使用。
```python
import pickle
# 假设我们已经得到了一个CP分解结果
cp_model = parafac(tensor_data, rank=3)
# 保存模型
with open('cp_model_rank3.pkl', 'wb') as f:
pickle.dump(cp_model, f)
# 加载模型
with open('cp_model_rank3.pkl', 'rb') as f:
loaded_model = pickle.load(f)
# 使用加载的模型进行重构
recon_from_disk = tl.cp_to_tensor(loaded_model)
```
对于非常大的张量或模型,也可以考虑使用`numpy.savez`来分别保存各个数组,以控制存储空间。
掌握了这些核心操作和进阶技巧后,你已经具备了将张量分解应用到真实项目中的能力。关键在于理解CP和Tucker分解各自的特点:CP追求简洁和可解释性,适合成分叠加清晰的场景;Tucker则更加灵活,能捕捉更复杂的交互,但需要为每个维度选择合适的秩。多实验,多观察重构误差和业务指标,你就能找到最适合手中数据的那把钥匙。