本文的基本内容翻译自Lift: Multi-Label Learning with Label-Specific Features[1],含部分本人的理解,最后附带了我复现的python代码的github链接。
类属属性
所谓多标记学习是相对于单标记学习而言的一种机器学习问题。顾名思义,模型会对输入的特征向量返回多个label的预测结果,比如说给一张图片让程序判断其中是否有“蓝天”,“白云”,“大海”,这便是一个多标记学习问题。通常我们会用一个二进制向量
y=(y1,y2,y3,...yq)
y
=
(
y
1
,
y
2
,
y
3
,
.
.
.
y
q
)
来表示模型的输出。每个label对应一个类别,
yi=1
y
i
=
1
表示第
i
i
个label为正例,则表示该类别为负例。
从上面的描述中可以知道,原始特征空间应该包含了所有类别的相关信息,但它呈现给我们的只是一个特征向量而已。简单来说,类属属性便是多标记问题中每个标记类别相对应的特征信息。ML Zhang认为:利用某种方法将每个label对应的类属属性提取出来,再进行后续模型的构建,是一个有助于提高模型性能的预处理步骤。实验结果也验证了这种猜想,这便是LIFT算法的渊源。
类属属性的提取
从定义上来说,类属属性便是基于原始特征空间和对应label的一种embedding。至于如何提取类属属性有很多种方法,比如利用
L1
L
1
正则化的特征稀疏性质,可以选出和对应标记相关性最大的特征;或者可以给出个3层的神经网络来拟合原始特征向量和某个label,然后取出中间层的输出作为label的类属属性;这些都是可行的方法,至于效果如何得做实验验证。
LIFT算法利用的是聚类方法来提取类属属性。聚类本身是一种无监督式学习的方法,经常被用在数据的预处理阶段。在LIFT算法中,作者并不是直接对原始特征向量进行聚类,而是先利用标记的正负信息进行数据的划分再分别聚类,以体现正负标记对应的样本原始特征在分布上的区别,这是算法最难理解的地方。下面我具体讲一下算法提取类属属性的过程:
我们把标记正负类定义为:
Pk={xi|(xi,Yi)∈D,lk∈Yi},Nk={xi|(xi,Yi)∈D,lk∉Yi}
P
k
=
{
x
i
|
(
x
i
,
Y
i
)
∈
D
,
l
k
∈
Y
i
}
,
N
k
=
{
x
i
|
(
x
i
,
Y
i
)
∈
D
,
l
k
∉
Y
i
}
。
Pk
P
k
表示的是第k个label为正的样本集合,
Nk
N
k
表示的是第k个样本为负的样本集合。LIFT算法的第一步就是找出所有label对应的
Pk
P
k
和
Nk
N
k
,后面的聚类操作便是在
Pk
P
k
和
Nk
N
k
上进行的。至于聚类时的簇数如何确定,文章中定义了
m+k
m
k
+
和
m−k
m
k
−
,分别对应于
Pk
P
k
和
Nk
N
k
。其中
m+k=m−k=r⋅min(|Pk|,|Nk|)
m
k
+
=
m
k
−
=
r
⋅
m
i
n
(
|
P
k
|
,
|
N
k
|
)
,其中
r∈[0,1]
r
∈
[
0
,
1
]
。
聚类操作结束后,每个label我们都能得到
m+k
m
k
+
个positive聚类中心和
m−k
m
k
−
个negative聚类中心。这
2m+k
2
m
k
+
个聚类中心能够反映标记
lk
l
k
对应的正负样本的内部结构。到这里,类属属性的提取只差一步:计算出原始特征向量到这
2m+k
2
m
k
+
个聚类中心的欧式距离
dki,i∈[1,2m+k]
d
k
i
,
i
∈
[
1
,
2
m
k
+
]
,用这
2m+k
2
m
k
+
个距离构成新的特征向量
ϕ(x)=(d1,d2,...,d2m+k)
ϕ
(
x
)
=
(
d
1
,
d
2
,
.
.
.
,
d
2
m
k
+
)
,这便是标记
lk
l
k
对应的类属属性。下面给出LIFT算法的完整过程:
Y=LIFT(D,r,u)
Y
=
L
I
F
T
(
D
,
r
,
u
)
输入:
D:
D
:
多标记学习的训练集
{(xi,Yi)|1≤i≤m}
{
(
x
i
,
Y
i
)
|
1
≤
i
≤
m
}
;
r:
r
:
确定聚类簇数的参数;
u:
u
:
测试样本;
输出:
Y:
Y
:
对测试样本
u
u
的预测结果;
过程:
k=1:1:q
k
=
1
:
1
:
q
do
d
o
找到标记
lk
l
k
对应的正样本集合
Pk
P
k
和负样本集合
Nk
N
k
,分别在这两个集合上进行
k
k
均值聚类,聚类簇数为。计算原始特征向量到这
2m+k
2
m
k
+
个中心的欧式距离构成新的特征向量空间
ϕk
ϕ
k
。利用类属属性特征
ϕk
ϕ
k
和
Yk
Y
k
训练二分类器
βk
β
k
。
end
e
n
d
for
f
o
r
k=1:1:q
k
=
1
:
1
:
q
do
d
o
计算未知实例
u
u
到个中心的欧式距离构成新的特征向量,利用二分类器
βk
β
k
预测其输出
yk
y
k
;
end
e
n
d
return
r
e
t
u
r
n
Y={y1,y2,y3,...,yq}
Y
=
{
y
1
,
y
2
,
y
3
,
.
.
.
,
y
q
}
代码块
import numpy as np
from mlab.releases import latest_release as mb
import evaluate as ev
import sys
from sklearn.preprocessing import scale
import scipy.io as sio
from Clustering import *
from copy import copy
from sklearn.model_selection import KFold
sys.path.append('.\libsvm-3.22\python')
from svmutil import *
class LIFT(object):
def __init__(self, ratio, svm_type):
#svm_type:
#1:Linear;2:RBF;3:Polynomial
self.ratio = ratio
self.type = svm_type
self.svm = []
self.binary = []
def fit(self, X, Y):
train_data,train_target = copy(X),copy(Y)
num_train, dim = train_data.shape
tmp_value, num_class = train_target.shape
self.P_Centers,self.N_Centers = [],[]
for i in range(num_class):
print('Performing clustering for the', i+1, '-th class')
p_idx = train_target[:,i]==1
n_idx = train_target[:,i]==0
p_data = train_data[p_idx,:]
n_data = train_data[n_idx,:]
k1 = int(mb.ceil(min(sum(p_idx)*self.ratio,
sum(n_idx)*self.ratio)))
k2 = k1
if k1==0:
label = 1 if sum(p_idx)>sum(n_idx) else 0
self.binary.append(label)
POS_C = []
NEG_C = []
else:
self.binary.append(2)
if p_data.shape[0] == 1:
POS_C = p_data
else:
POS_IDX,POS_C = KMeans(p_data,k1)
if n_data.shape[0] == 1:
NEG_C = n_data
else:
NEG_IDX,NEG_C = KMeans(n_data,k2)
self.P_Centers.append(POS_C)
self.N_Centers.append(NEG_C)
for i in range(num_class):
print('Building classifiers: ',i+1,'/',num_class)
if self.binary[i] != 2:
self.svm.append([])
else:
centers = np.vstack((self.P_Centers[i],
self.N_Centers[i]))
num_center = centers.shape[0]
if num_center >= 5000:
print('Too many cluster centers!')
sys.exit()
else:
blocksize = 5000-num_center
num_block = int(mb.ceil(num_train*1.0/blocksize))
data = np.zeros((num_train,num_center))
for j in range(1,num_block):
low = (j-1)*blocksize
high = j*blocksize
tmp_mat = np.vstack((centers,train_data[low:high,:]))
Y = mb.pdist(tmp_mat)
Z = mb.squareform(Y)
data[low:high,] = Z[num_center:num_center+blocksize,0:num_center]
#data.append(Z[num_center:num_center+blocksize,0:num_center].tolist())
low = (num_block-1)*blocksize
high = num_train
tmp_mat = np.vstack((centers,train_data[low:high,:]))
Y = mb.pdist(tmp_mat)
Z = mb.squareform(Y)
data[low:high,] = Z[num_center:num_center+blocksize,0:num_center]
#data.append(Z[num_center:num_center+blocksize,0:num_center].tolist())
training_instance = copy(data)
training_label = train_target[:,i]
if self.type == 1:
para = '-t 0 -b 1 -q'
elif self.type == 2:
para = '-t 1 -b 1 -q'
else:
para = '-t 2 -b 1 -q'
self.svm.append(svm_train(training_label.tolist(),
training_instance.tolist(),
para))
def predict(self, test_X, test_Y):
test_data,test_target = copy(test_X),copy(test_Y)
num_test, num_class = test_target.shape
Pre_Labels = np.zeros(test_target.shape)
Outputs = copy(Pre_Labels)
for i in range(num_class):
if self.binary[i] != 2:
Pre_Labels[:,i] = self.binary[i]
Outputs[:,i] = self.binary[i]
else:
centers = np.vstack((self.P_Centers[i],
self.N_Centers[i]))
num_center = centers.shape[0]
data = np.zeros((num_test,num_center))
if num_center >= 5000:
print('Too many cluster centers!')
sys.exit()
else:
blocksize = 5000-num_center
num_block = int(mb.ceil(num_test*1.0/blocksize))
for j in range(1,num_block):
low = (j-1)*blocksize
high = j*blocksize
tmp_mat = np.vstack((centers,test_data[low:high,:]))
Y = mb.pdist(tmp_mat)
Z = mb.squareform(Y)
data[low:high,] = Z[num_center:num_center+blocksize,0:num_center]
#data.append(Z[num_center:num_center+blocksize,0:num_center].tolist())
low = (num_block-1)*blocksize
high = num_test
tmp_mat = np.vstack((centers,test_data[low:high,:]))
Y = mb.pdist(tmp_mat)
Z = mb.squareform(Y)
data[low:high,] = Z[num_center:num_center+blocksize,0:num_center]
#data.append(Z[num_center:num_center+blocksize,0:num_center].tolist())
new_test_data = copy(data)
Pre_Labels[:,i],tmp,tmpoutputs = svm_predict(test_target[:,i].tolist(), \
new_test_data.tolist(), \
self.svm[i],'-b 1')
pos_index = 0 if self.svm[i].label[0]==1 else 1
Outputs[:,i] = np.array(tmpoutputs)[:,pos_index]
return [ev.HammingLoss(Pre_Labels,test_target),
ev.rloss(Outputs,test_target),
ev.Coverage(Outputs,test_target),
ev.OneError(Outputs,test_target),
ev.avgprec(Outputs,test_target)]#,
#ev.MacroAveragingAUC(Outputs,test_target)]
if __name__ == '__main__':
path = 'F:/PyLift/dataset/'
dataset = ['birds','CAL500','corel5k','emotions',
'enron','genbase','Image','languagelog',
'recreation','scene','slashdot','yeast']
ratio = 0.1
svm_type = 1#Linear
for i in range(3,4):
dataset_path = path + dataset[i] + '.mat'
tmp = sio.loadmat(dataset_path)
data,target = scale(tmp['data']),tmp['target'].T
target[target==-1] = 0
kf = KFold(n_splits=10, shuffle=True, random_state=2017)
result = []
for dev_index,val_index in kf.split(data):
train_data, test_data = data[dev_index], data[val_index]
train_target, test_target = target[dev_index], target[val_index]
lift = LIFT(ratio,svm_type)
lift.fit(train_data,train_target)
result.append(lift.predict(test_data,test_target))
res_mean = np.mean(result,0)
res_std = np.std(result,0)
print('Hamming Loss:',res_mean[0],'+-',res_std[0])
print('Ranking Loss:',res_mean[1],'+-',res_std[1])
print('Coverage:',res_mean[2],'+-',res_std[2])
print('One Error:',res_mean[3],'+-',res_std[3])
print('Average Precision:',res_mean[4],'+-',res_std[4])
#print('Macro Averaging AUC:',res_mean[5],'+-',res_std[5])
上述代码只给出了LIFT类的代码,另外还有两个py文件,一个是KMeans.py,一个是evaluate.py。完整代码的github地址为:https://github.com/ZesenChen/PyLift。其中还包含了libsvm的源码和常用的几个多标记dataset,有兴趣的可以自己做实验,性能和论文中给出的实验结果基本相同。