knitr::opts_chunk$set(echo = TRUE)
SMOTE(Synthetic Minority Oversampling Technique)是一种基于线性插补的过采样方法。
其想法是对某一少数类样本点与少数类k近邻之一进行线性插补,得到新的少数类样本,从而达到数据增强的目的。
下面给出了数学表达式:
$$\boldsymbol{x}{syn}=\boldsymbol{x}_p+\delta(\boldsymbol{x}_q-\boldsymbol{x}_p),\quad \boldsymbol{x}_p,\boldsymbol{x}_q\in S{min},\quad \delta\sim U(0,1) $$
这里$\boldsymbol{x}_q$为$\boldsymbol{x}_p$的少数类k近邻之一。
核方法是被广泛应用的方法,其典型算法是支持向量机SVM与RBF-NN。
其想法是利用内积代替映射的显式表达,通过改变空间的度量结构达到简化学习任务的作用。
下面给出了核函数的定义:
$$K(\boldsymbol{x}_i,\boldsymbol{x}_j)=\langle \phi(\boldsymbol{x}_i),\phi(\boldsymbol{x}_j) \rangle$$ 这里$\boldsymbol{x}_i,\boldsymbol{x}_j$为特征空间中的点,$\boldsymbol{K}$为核函数,为再生核希尔伯特空间上的内积。
再生核希尔伯特空间上由核函数诱导的距离如下:
$$ d^{\phi}(\boldsymbol{x}_i,\boldsymbol{x}_j)=\Vert\phi(\boldsymbol{x}_i)-\phi(\boldsymbol{x}_j)\Vert^2=K(\boldsymbol{x}_i,\boldsymbol{x}_i)+K(\boldsymbol{x}_j,\boldsymbol{x}_j)-2K(\boldsymbol{x}_i,\boldsymbol{x}_j) $$
由于样本点间往往不具备线性结构,所以做简单的线性插补不能达到优化学习模型的效果。
基于这样的想法,我们可以构建基于核函数的SMOTE。
$$ \phi(\boldsymbol{x}_{syn})=\phi(\boldsymbol{x}_p)+\delta(\phi(\boldsymbol{x}_p)-\phi(\boldsymbol{x}_q)),\quad \delta\sim U(0,1) $$
这里为了方便表达,我们利用了$\phi$这一显式映射,事实上,在后续的算法实现过程中,$\phi$是不会被表示出来的,我们会利用其内积,即核函数$\boldsymbol{K}$来达到构建再生核希尔伯特空间的作用。
SVM是核方法的一个典型应用。
在经典的SVM算法中,我们需要解决如下优化问题:
$$ \begin{align} &\min_{\alpha}\quad \frac{1}{2}\sum_{i,j=1}^N\alpha_i\alpha_jy_iy_j K(x_i,x_j)-\sum_{i=1}^N\alpha_i\ &s.t \quad \sum_{i=1}^N\alpha_iy_i=0\ &\quad \quad 0\le\alpha_i\le C,\quad i=1,2,\cdots,N \end{align} $$
利用KKT条件,问题的解为:
$$ \begin{align} &w^{\ast}=\sum_{i=1}^{N}\alpha_i^{\ast}y_ix_i\ &b^{\ast}=y_j-\sum_{i=1}^N\alpha_i^{\ast}y_i K(x_i, x_j) \end{align} $$
分类决策函数为:
$$ \begin{align} f(x)&=sign(\sum_{i=1}^{N_s}\alpha_i^{\ast}y_iK(x_i,x)+b^{\ast}) \end{align} $$
无论是求解过程还是分类决策函数都与映射$\phi$无关,这意味着我们关心核矩阵即可。
计算Gram matrix K。这一步是核心,连接SMOTE和SVM。
$$ K=(K(\boldsymbol{x}i,\boldsymbol{x}_j)){(N+P)\times(N+P)}= \begin{bmatrix} K_1&K_2\ K_2^T&K_3 \end{bmatrix} $$
这里,$K_1=(K(\boldsymbol{x}i,\boldsymbol{x}_j)){N\times N},\quad \boldsymbol{x}_i,\boldsymbol{x}_j\in S$
$K_2=(K(\boldsymbol{x}i,\boldsymbol{x}_j)){N\times P},\quad \boldsymbol{x}_i\in S,\boldsymbol{x}_j\in S^{syn}$
$K_3=(K(\boldsymbol{x}i,\boldsymbol{x}_j)){P\times P},\quad \boldsymbol{x}_i,\boldsymbol{x}_j\in S^{syn}$
此外,为了方便后面的表示,我们用$x_i^{pq}$来表示$\boldsymbol{x}_p,\boldsymbol{x}_q$生成的样本,$\delta^{pq}$表示对应的随机数。
下面,我们可以给出$K_2,K_3$中元素的具体表达。(我们需要将其表示成原样本集与核函数的形式,这样我们就避免了显式生成新样本,说白了,我们生成的不是样本,而是$K(\boldsymbol{x}_i,\boldsymbol{x}_j)$,这就是核方法的精彩了)
$K_2$中元素:
$$ K(\boldsymbol{x}_i,\boldsymbol{x}_j^{pq})=(1-\delta^{pq})K(\boldsymbol{x}_i,\boldsymbol{x}_p)+\delta^{pq}K(\boldsymbol{x}_i,\boldsymbol{x}_q),\quad \boldsymbol{x}_i\in S,\boldsymbol{x}_j^{pq}\in S^{syn} $$
$K_3$中元素:
$$ K(\boldsymbol{x}_i^{lm},\boldsymbol{x}_j^{pq})=(1-\delta^{pq})(1-\delta^{lm})K(\boldsymbol{x}_p,\boldsymbol{x}_l)+(1-\delta^{pq})(\delta^{lm})K(\boldsymbol{x}_p,\boldsymbol{x}_m)+(\delta^{pq})(1-\delta^{lm})K(\boldsymbol{x}_q,\boldsymbol{x}_l)+(\delta^{pd})(\delta^{lm})K(\boldsymbol{x}_q,\boldsymbol{x}_m) $$
这里,值得一提的是,我们的样本生成与核函数的具体表示无关,所以这一SMOTE方法适用于所有的核函数。
下面是实现,分两步,一个是K-SMOTE,还有一个是基于SMO算法的SVM分类器。
首先加载训练集和测试集。
library(StatComp21092) data("yeast_tra") data("yeast_test") train_set=yeast_tra[1:500,] test_set=yeast_test
接下来我们利用前面的公式计算核矩阵。
#定义三阶多项式核函数 kernel_poly<-function(x,y) { #x,y为特征空间上的向量 return((x%*%y+1)^3) } #定义再生核希尔伯特空间上的距离平方 distance<-function(x,y) { #x,y为特征空间上的向量 d_square=kernel_poly(x,x)+kernel_poly(y,y)-2*kernel_poly(x,y) return(d_square) }
#K_SMOTE,同时计算新的核矩阵与分类决策函数的核因子 K_SMOTE<-function(train_set,test_set,P,k) { #train_set,test_set分别为训练集和测试集,类型为dataframe #P为需要生成的少数类样本数 #k为kNN中近邻的个数 #记录训练集中少数类和多数类的index min_index=(which(train_set['Class']==1)) max_index=(which(train_set['Class']==0)) #初始化核矩阵 N = nrow(train_set) K = matrix(0,N+P,N+P) D = matrix(0,N+P,N+P) #初始化生成对与随机数 p_index=numeric(length = P) q_index=numeric(length = P) delta=runif(P,0,1) #计算原核矩阵 for (i in 1:N) for (j in 1:N) K[i,j]=kernel_poly(data.matrix(train_set)[i,3:ncol(train_set)-1],data.matrix(train_set)[j,3:ncol(train_set)-1]) #计算距离矩阵(我们这里只关心少数类) for (i in min_index) for (j in min_index) D[i,j]=distance(data.matrix(train_set)[i,3:ncol(train_set)-1],data.matrix(train_set)[j,3:ncol(train_set)-1]) #用少数类的距离矩阵结合KNN得到生成对 for (i in 1:P) { p=sample(min_index,1,replace = TRUE) r=min_index[rank(D[p,min_index])] q=sample(r[1:k],1,replace = TRUE) p_index[i]=p q_index[i]=q } #生成,即填补核矩阵 #填K2和K2转置:一个为原样本,一个为生成样本 for(i in 1:P) for(j in 1:N) { p=p_index[i] q=q_index[i] K[i+N,j]=(1-delta[i])*K[p,j]+delta[i]*K[q,j] K[j,i+N]=K[i+N,j] } #填K3:均为生成样本 for(i in 1:P) for(j in 1:P) { l=p_index[i] m=q_index[i] p=p_index[j] q=q_index[j] K[i+N,j+N]=(1-delta[j])*(1-delta[i])*K[p,l]+(1-delta[j])*delta[i]*K[p,m]+delta[j]*(1-delta[i])*K[q,l]+delta[j]*delta[i]*K[q,m] } # 下面填测试样本与训练集的核矩阵 n=nrow(test_set) gram_test=matrix(0,n,N+P) for(i in 1:n) for(j in 1:N) gram_test[i,j]=kernel_poly(data.matrix(test_set)[i,3:ncol(train_set)-1],data.matrix(train_set)[j,3:ncol(train_set)-1]) for(i in 1:n) for(j in 1:P) { p=p_index[j] q=q_index[j] gram_test[i,j+N]=(1-delta[j])*gram_test[i,p]+delta[j]*gram_test[i,q] } mat=list("train_km"=K,"test_km"=gram_test) return(mat) }
由于R包e1071的svm函数的核函数参数中,只有rbf,poly,linear和nn这四种核函数,没有提供我们需要的核矩阵选项,所以我们还需要写一个基于SMO算法的SVM分类器。
我们之前提到了SVM算法是解决如下凸优化问题:
$$ \begin{align} &\min_{\alpha}\quad \frac{1}{2}\sum_{i,j=1}^N\alpha_i\alpha_jy_iy_j K(x_i,x_j)-\sum_{i=1}^N\alpha_i\ &s.t \quad \sum_{i=1}^N\alpha_iy_i=0\ &\quad \quad 0\le\alpha_i\le C,\quad i=1,2,\cdots,N \end{align} $$
但随着样本量的增加,参数量会变得异常庞大。因此,我们不能直接对这个问题用梯度下降进行求解。
SMO的想法是我们每次只优化两个变量$\alpha_i,\alpha_j$,将其余变量视为常数,同时由于有等式约束,我们可以得到$\alpha_i$与$\alpha_j$之间的关系,进而可以求解。基于这样的想法,SMO算法将一个多变量的优化问题转化为多个简单的二次规划问题。
#预处理 #得到两个核矩阵 P=400 mat=K_SMOTE(train_set,test_set,P,3) train_matrix=mat$train_km test_matrix=mat$test_km print(train_matrix[1:5,1:5]) print(test_matrix[1:5,1:5])
#提取label并修正为1和-1 modify_label=function(train_set, test_set, P) { train_target=array(1, dim = nrow(train_set)+P) test_target=array(1, dim = nrow(test_set)) for(i in 1:nrow(train_set)) if(train_set[i,'Class']==0) { train_target[i]=-1 } if(train_set[i,'Class']==1) { train_target[i]=1 } for(i in 1:nrow(test_set)) if(test_set[i,'Class']==0) { test_target[i]=-1 } if(test_set[i,'Class']==1) { test_target[i]=1 } target=list("train"=train_target,"test"=test_target) return(target) } all_target=modify_label(train_set,test_set,P)
update_alpha_2=function(alpha_2,L,H) { if(alpha_2>H) return(H) else if(alpha_2<L) return(L) else return(alpha_2) }
SMO_SVC=function(train_mat,test_mat,train_target,test_target,iter_max=10,epsilon=0.01,C=1) { #初始化 N=nrow(train_mat) alpha=numeric(length = nrow(train_mat)) b=1 k=0#迭代次数 #定义函数g g=function(i) { return(sum(alpha*train_target*train_mat[i,])+b) } sample_without_i=function(i) { j=sample(1:N,1) while(j==i) j=sample(1:N,1) return(j) } while (k<iter_max) { change=0 #取出i,j for(i in 1:N) { a_i=alpha[i] y_i=train_target[i] x_i=i E_i=g(x_i)-y_i j=sample_without_i(i) a_j=alpha[j] y_j=train_target[j] x_j=j E_j=g(x_j)-y_j eta=train_mat[i,i]+train_mat[j,j]-2*train_mat[i,j] if(eta<=0) next #未经剪辑的new_a_j new_a_j=a_j+y_j*(E_i-E_j)/eta #剪辑a_j_new if(y_i!=y_j) { L=max(0,a_j-a_i) H=min(C,C+a_j-a_i) } else { L=max(0,a_j+a_i-C) H=min(C,a_i+a_j) } a_j_new=update_alpha_2(new_a_j,L,H) a_i_new=a_i+y_i*y_j*(a_j-a_j_new) if(abs(a_j_new-a_j)<epsilon) next #更新alpha alpha[i]=a_i_new alpha[j]=a_j_new #更新b b_i=-E_i-y_i*train_mat[i,i]*(a_i_new - a_i) - y_j*train_mat[i,j]*(a_j_new - a_j) + b b_j=-E_j-y_i*train_mat[i,j]*(a_i_new - a_i) - y_j*train_mat[j,j]*(a_j_new - a_j) + b if(abs(a_i_new-C/2)<C/2) b=b_i else if(abs(a_j_new-C/2)<C/2) b=b_j else b=(b_i+b_j)/2 change=change+1 } if(change==0) k=k+1 else k=0 } #计算测试集结果 result=numeric(length = nrow(test_mat)) for (i in 1:nrow(test_mat)) { f=sum(alpha*train_target*test_mat[i,])+b if(f>0) result[i]=1 else result[i]=-1 } return(result) } pred=SMO_SVC(train_matrix,test_matrix,all_target$train,all_target$test,1e1,1e-2,1) target=all_target$test
ConfusionMatrix=function(pred,target) { N=length(pred) TP=FP=FN=TN=0 for(i in 1:N) { if(pred[i]==1 &&target[i]==1) TP=TP+1 if(pred[i]==-1&&target[i]==-1) TN=TN+1 if(pred[i]==1&&target[i]==-1) FP=FP+1 if(pred[i]==-1&&target[i]==1) FN=FN+1 } mat=matrix(c(TP,FP,FN,TN),ncol=2) return(mat) } ConfusionMatrix(pred,target)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.