L2 norm的作用及使用方法

608 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。​

L2 norm的作用

对于模型的输出,各个特征的模长均不一样, L2 norm是强行把同个维度的特征归一化。经过L2 norm的数据都处于0到1之间,距离就变得有一个上界了,显然样本间差异变小了。

image.png

L2 norm的应用场景

在人脸识别,行人重识别等图像分类的任务中,经常使用到该函数。尤其是对于triplet loss这种基于欧式距离的损失函数,若没有对特征进行L2 norm,使用不同尺度的特征计算则会低效。

  1. 因为尺度不一,由于每次选择的anchor也不一样,所以每次计算优化的方向不一致,导致效率低下。
  2. 若联合交叉熵损失函数联合训练,会导致两个损失函数的优化方向不一致

交叉熵损失函数的优化方向VS三元损失函数的优化方向:

image.png

注:triplet loss 是深度学习的一种损失函数,主要是用于训练差异性小的样本,比如人脸等;其次在训练目标是得到样本的embedding任务中,triplet loss 也经常使用,比如文本、图片的embedding。

图像搜索的arcface损失是利用边角距离,也需要对特征进行L2 norm。arcface在训练过程中就有对特征提取网络输出的embedding进行了L2 norm归一化,在推理时会将arcface去掉,这时候要记得对特征进行L2 norm!

L2 norm的几种实现方法

1、(推荐) torch.nn.functional.normalize(input, p=2, dim=1, eps=1e-12, out=None)

功能:将某一个维度除以那个维度对应的范数。

默认是2范数,默认在第一个维度上操作,因此默认输入维度应是[batchsize, embeddingsize]

推荐用法:可以直接加到网络的forward里

import torch.nn.functional as F
import torch.nn as nn
class my_model(nn.Module):
    def __init__(self,):
        super().__init__()
        self.prj_model = embedding_model

    def forward(self,x):
        x = self.prj_model.forward_embedding(x)
        x = F.normalize(x)
        return x

输入、输出示例

import torch
#输入
a = torch.Tensor([[1,2,3],[-3,-2,-1]])
b = torch.nn.functional.normalize(a)
#输出b
#tensor([[ 0.2673,  0.5345,  0.8018],[-0.8018, -0.5345, -0.2673]])

2、sklearn.preprocessing.normalize(X, norm='l2', axis=1, copy=True, return_norm=False)

功能:将某一个维度除以那个维度对应的范数。

默认是2范数,默认在第一个维度上操作,因此默认输入维度应是[batchsize, embeddingsize]

推荐用法:在网络外部归一化,输入维度要正确,输入格式可以是tensor、numpy、list都行

import torch.nn as nn
from sklearn import preprocessing
import numpy as np
class my_model(nn.Module):
    def __init__(self,):
        super().__init__()
        self.prj_model = embedding_model

    def forward(self,x):
        x = self.prj_model.forward_embedding(x)
        return x

model = my_model()
with torch.no_grad():
    feature = model(x)
img_feats=feature.cpu().numpy()
feature_256 = np.array(img_feats,dtype = "float32")
feature_256 = preprocessing.normalize(feature_256, norm='l2')

输入、输出示例

import torch
from sklearn import preprocessing
import numpy as np
#输入
a1 = [[1,2,3],[-3,-2,-1]]
a2 = torch.Tensor([[1,2,3],[-3,-2,-1]])
a3 = np.array(a1,dtype = "float32")
#输出
b1 = preprocessing.normalize(a1, norm='l2')
#array([[ 0.26726124,  0.53452248,  0.80178373],
#       [-0.80178373, -0.53452248, -0.26726124]])
b2 = preprocessing.normalize(a2, norm='l2')
#array([[ 0.26726124,  0.53452248,  0.80178373],
#       [-0.80178373, -0.53452248, -0.26726124]])
b3 = preprocessing.normalize(a3, norm='l2')
#array([[ 0.26726124,  0.5345225 ,  0.8017837 ],
#       [-0.8017837 , -0.5345225 , -0.26726124]], dtype=float32)
# 注意,如果直接使用np.array(a1)转numpy,默认的格式是dtype('int64')
# 使用float32的话,因为精度原因会有一定影响

3、faiss.normalize_L2(embedding)

功能:对被索引矩阵和查询向量,进行L2归一化

输入维度也应该是[samplesize,embeddingsize]

推荐用法:当前面都没有做归一化时,向量已经保存好,要进行相似度查询了,这个时候可以利用faiss的normalize_L2函数进行L2 norm

import faiss
feature = get_feature()
feature = np.array(feature).astype('float32')
faiss.normalize_L2(feature)

res = faiss.StandardGpuResources()
index_img = faiss.IndexFlatIP(256)  # 假设embedding维度是256
index_img = faiss.index_cpu_to_gpu(res, 0, index_img)
index_img.add(feature)

query_embedding = get_query_feature()
query_embedding = np.array(query_embedding).astype('float32')
faiss.normalize_L2(query_embedding)

similarities_img, indexes_img = index_img.search(query_embedding, 10) # 查询top10

输入float32格式,输出同2中的b3

4、torch手动实现

import torch
a = torch.FloatTensor(((1,2),(2,3)))
a_norm = a.div(torch.norm(a, p=2, dim=1, keepdim=True))

输入、输出示例

import torch
#输入
a = torch.Tensor([[1,2,3],[-3,-2,-1]])
# tensor([[ 1., 2., 3.],
#        [-3., -2., -1.]])

#输出
b = a.div(torch.norm(a, p=2, dim=1, keepdim=True))
# tensor([[ 0.2673, 0.5345, 0.8018],
	      [-0.8018, -0.5345, -0.2673]])

# 这种方法精度较低