在机器学习和深度学习中,距离计算就像是我们衡量两个事物相似程度的尺子。想象一下你在电商平台购物,系统为什么会给你推荐某些商品?背后就是通过计算你和其他用户、或者商品之间的相似度来实现的。
PyTorch提供了多种距离计算方式,每种都有其适用场景。比如:
我刚开始接触时也犯过迷糊,为什么要有这么多不同的方法?后来在实际项目中才发现,不同的距离度量会直接影响模型效果。比如在推荐系统中,用余弦相似度比欧式距离往往效果更好。
余弦相似度衡量的是两个向量在方向上的相似程度,取值范围在[-1,1]之间。在自然语言处理中特别常用。
python复制import torch
import torch.nn as nn
# 创建两个示例向量
user1 = torch.tensor([1.0, 2.0, 3.0])
user2 = torch.tensor([4.0, 5.0, 6.0])
# 初始化余弦相似度计算器
cos_sim = nn.CosineSimilarity(dim=0)
# 计算相似度
similarity = cos_sim(user1, user2)
print(f"余弦相似度: {similarity:.4f}")
这里有几个容易踩的坑:
dim参数要设置正确,对于一维向量设为0我在实际项目中发现,对向量先做归一化再计算余弦相似度,效果会更稳定:
python复制user1_normalized = user1 / torch.norm(user1, p=2)
user2_normalized = user2 / torch.norm(user2, p=2)
欧式距离就是我们常说的"两点之间直线距离"。PyTorch中至少有三种实现方式:
方法一:使用PairwiseDistance
python复制pdist = nn.PairwiseDistance(p=2) # p=2表示欧式距离
distance = pdist(user1, user2)
方法二:手动计算
python复制distance = torch.sqrt(torch.sum((user1 - user2)**2))
方法三:使用torch.linalg.vector_norm
python复制distance = torch.linalg.vector_norm(user1 - user2, ord=2)
这三种方法计算结果相同,但性能有差异。在我的测试中,对于大批量数据,torch.linalg.vector_norm通常最快。
当我们需要计算多组向量之间的距离时,torch.cdist就是神器。比如在图像检索中,需要计算查询图像与数据库中所有图像的相似度。
python复制# 创建批次数据:3个查询图像,5个数据库图像,每个图像用512维向量表示
queries = torch.randn(3, 512)
database = torch.randn(5, 512)
# 计算所有查询与所有数据库图像的距离
distances = torch.cdist(queries, database, p=2)
print(distances.shape) # 输出: torch.Size([3, 5])
这个例子中,我们一次性计算了3×5=15个距离,输出矩阵的每个元素(i,j)表示第i个查询与第j个数据库图像的距离。
在处理超大规模数据时,内存可能成为瓶颈。我总结了几个优化经验:
python复制chunk_size = 1000
results = []
for i in range(0, len(database), chunk_size):
chunk = database[i:i+chunk_size]
results.append(torch.cdist(queries, chunk))
final_distances = torch.cat(results, dim=1)
python复制with torch.cuda.amp.autocast():
distances = torch.cdist(queries.half(), database.half())
python复制queries = queries.cuda()
database = database.cuda()
在电商推荐场景,我们通常要计算用户-商品相似度矩阵。假设我们有:
直接计算10000×100万矩阵显然不现实。解决方案:
python复制# 示例:分批次计算用户-商品相似度
user_batch_size = 100
item_batch_size = 10000
for i in range(0, num_users, user_batch_size):
user_batch = all_users[i:i+user_batch_size]
batch_results = []
for j in range(0, num_items, item_batch_size):
item_batch = all_items[j:j+item_batch_size]
sim = torch.cdist(user_batch, item_batch, p=2)
batch_results.append(sim)
# 合并结果并保存
full_sim = torch.cat(batch_results, dim=1)
save_results(full_sim)
在图像检索任务中,我们常用CNN提取的特征向量进行相似度计算。一个典型流程:
python复制def search_similar_images(query_feature, database_features, top_k=5):
# query_feature: 1xD
# database_features: NxD
distances = torch.cdist(query_feature.unsqueeze(0), database_features)
_, indices = torch.topk(distances, k=top_k, largest=False)
return indices
这里有个细节要注意:query_feature需要unsqueeze(0)变成二维张量,因为cdist要求输入至少是2D的。
距离计算中经常遇到数值不稳定的情况,比如:
python复制# 解决方案:添加小epsilon
cos_sim = nn.CosineSimilarity(dim=0, eps=1e-8)
python复制# 解决方案:先做归一化
features = features / torch.norm(features, p=2, dim=1, keepdim=True)
不同的距离度量适用于不同场景:
| 距离类型 | 适用场景 | 不适用场景 |
|---|---|---|
| 余弦相似度 | 文本分类、推荐系统 | 需要考虑向量长度的场景 |
| 欧式距离 | 空间距离、聚类分析 | 高维稀疏数据 |
| 曼哈顿距离 | 离散特征、异常检测 | 需要旋转不变性的场景 |
我在一个商品推荐项目中做过对比实验,发现对于用户行为数据,余弦相似度比欧式距离的推荐准确率高15%左右。
处理大规模距离矩阵时,内存消耗是常见瓶颈。除了前面提到的分块计算,还可以:
python复制from torch.sparse import to_sparse_safe
# 只保留距离小于阈值的关系
mask = distances < threshold
sparse_distances = to_sparse_safe(distances * mask)
python复制from sklearn.decomposition import PCA
pca = PCA(n_components=64)
reduced_features = pca.fit_transform(features)
python复制# 创建内存映射张量
dist_matrix = torch.tensor(np.memmap('dist.dat', dtype='float32',
mode='w+', shape=(N, N)))
距离计算是机器学习中的基础操作,但魔鬼藏在细节中。根据我的经验,理解每种方法的适用场景比记住公式更重要。在实际项目中,我通常会先在小数据集上验证不同距离度量的效果,再扩展到全量数据。