1. 程式人生 > 其它 >NLP真實專案:利用這個模型能夠通過商品評論去預測一個商品的銷量

NLP真實專案:利用這個模型能夠通過商品評論去預測一個商品的銷量

前言

由於是日語專案,用到的分詞軟體等,在中文任務中需要替換為相應的中文分詞軟體。例如結巴分詞 : https://github.com/fxsjy/jieba

前提知識和術語解釋

如果需要獲得更多知識,請自行百度,谷歌。中文資料不是很多,有能力請閱讀相關論文資料。

術語縮寫

PV-DM: Distributed Memory Model of Paragraph Vectors 句向量的分佈記憶模型 PV-DBOW: Distributed Bag of Words version of Paragraph Vector 句向量的分

布詞袋

注意:distributed這裡是區別於one-hot

distrubted 表示一個個體用幾個編碼單元而不是一個編碼單元表示,即一個個體分佈在幾個編碼單元上,主要是相對one-hot編碼中一個編碼單元表示一個個體。

one-hot representation與distributed representation學習筆記

餘弦相似度

餘弦相似度,又稱為餘弦相似性,是通過計算兩個向量的夾角餘弦值來評估他們的相似度。餘弦相似度將向量根據座標值,繪製到向量空間中,如最常見的二維空間。

餘弦相似度

將向量根據座標值,繪製到向量空間中。如最常見的二維空間。  求得他們的夾角,並得出夾角對應的餘弦值,此餘弦值就可以用來表徵,這兩個向量的相似性。夾角越小,餘弦值越接近於1,它們的方向更加吻合,則越相似。

單位向量

單位向量是指模等於1的向量。由於是非零向量,單位向量具有確定的方向。一個非零向量除以它的模,可得所需單位向量。單位向量有無數個。 (向量和向量是同一個意思,Vector,這裡習慣用向量這個詞語)

gensim.matutils.unitvec(vec, norm='l2') Scale a vector to unit length. The only exception is the zero vector, which is returned back unchanged. Output will be in the same format as input (i.e., gensim vector=>gensim vector, or np array=>np array, scipy.sparse=>scipy.sparse).

向量加減

平行四邊形定則解決向量加法的方法:將兩個向量平移至公共起點,以向量的兩條邊作平行四邊形,結果為公共起點的對角線。 平行四邊形定則解決向量減法的方法:將兩個向量平移至公共起點,以向量的兩條邊作平行四邊形,結果由減向量的終點指向被減向量的終點。

image.png

向量點積

代數定義

設二維空間內有兩個向量

,定義它們的數量積(又叫內積、點積)為以下實數:

更一般地,n維向量的內積定義如下:

點乘的結果就是兩個向量的模相乘,然後再與這兩個向量的夾角的餘弦值相乘。或者說是兩個向量的各個分量分別相乘的結果的和。很明顯,點乘的結果就是一個數,這個數對我們分析這兩個向量的特點很有幫助。如果點乘的結果為0,那麼這兩個向量互相垂直;如果結果大於0,那麼這兩個向量的夾角小於90度;如果結果小於0,那麼這兩個向量的夾角大於90度。

TF-IDF

TF-IDF是一種統計方法,用以評估一字詞對於一個檔案集或一個語料庫中的其中一份檔案的重要程度。字詞的重要性隨著它在檔案中出現的次數成正比增加,但同時會隨著它在語料庫中出現的頻率成反比下降。TF-IDF加權的各種形式常被搜尋引擎應用,作為檔案與使用者查詢之間相關程度的度量或評級。除了TF-IDF以外,因特網上的搜尋引擎還會使用基於連結分析的評級方法,以確定檔案在搜尋結果中出現的順序。 在gensim裡面有多個主題模型,TfidfModel可以直接用庫來計算.

from gensim.models import TfidfModel, LsiModel, LdaModel
corpus = [dic.doc2bow(text) for text in words]
tfidf = models.TfidfModel(corpus)

LSI(latent semantic index)

潛在語義索引

我們希望找到一種模型,能夠捕獲到單詞之間的相關性。如果兩個單詞之間有很強的相關性,那麼當一個單詞出現時,往往意味著另一個單詞也應該出現(同義詞);反之,如果查詢語句或者文件中的某個單詞和其他單詞的相關性都不大,那麼這個詞很可能表示的是另外一個意思(比如在討論網際網路的文章中,Apple更可能指的是Apple公司,而不是水果) 。

LSA(LSI)使用SVD來對單詞-文件矩陣進行分解。SVD可以看作是從單詞-文件矩陣中發現不相關的索引變數(因子),將原來的資料對映到語義空間內。在單詞-文件矩陣中不相似的兩個文件,可能在語義空間內比較相似。

SVD,亦即奇異值分解,是對矩陣進行分解的一種方法,一個td維的矩陣(單詞-文件矩陣)X,可以分解為TSDT,其中T為tm維矩陣,T中的每一列稱為左奇異向量(left singular bector),S為mm維對角矩陣,每個值稱為奇異值(singular value),D為dm維矩陣,D中的每一列稱為右奇異向量。在對單詞文件矩陣X做SVD分解之後,我們只儲存S中最大的K個奇異值,以及T和D中對應的K個奇異向量,K個奇異值構成新的對角矩陣S’,K個左奇異向量和右奇異向量構成新的矩陣T’和D’:X’=T’S’D’T形成了一個新的t*d矩陣。

LDA

LDA(Latent Dirichlet Allocation)是一種文件主題生成模型,也稱為一個三層貝葉斯概率模型,包含詞、主題和文件三層結構。所謂生成模型,就是說,我們認為一篇文章的每個詞都是通過“以一定概率選擇了某個主題,並從這個主題中以一定概率選擇某個詞語”這樣一個過程得到。文件到主題服從多項式分佈,主題到詞服從多項式分佈。 LDA是一種非監督機器學習技術,可以用來識別大規模文件集(document collection)或語料庫(corpus)中潛藏的主題資訊。它採用了詞袋(bag of words)的方法,這種方法將每一篇文件視為一個詞頻向量,從而將文字資訊轉化為了易於建模的數字資訊。但是詞袋方法沒有考慮詞與詞之間的順序,這簡化了問題的複雜性,同時也為模型的改進提供了契機。每一篇文件代表了一些主題所構成的一個概率分佈,而每一個主題又代表了很多單詞所構成的一個概率分佈。

請不要將自然語言處理的LDA和機器學習的LDA混淆(Linear Discriminant Analysis, 以下簡稱LDA)

Doc2Vec

在自然語言處理中,一個很重要的技術手段就是將文件轉換為一個向量,這個過程一般是使用gensim這個庫進行處理的。

gensim官網地址 如果你需要Java版本的Doc2Vec: https://github.com/NLPchina/Word2VEC_java

作為一個處理可變長度文字的總結性方法,Quoc Le 和 Tomas Mikolov 提出了 Doc2Vec方法。除了增加一個段落向量以外,這個方法幾乎等同於 Word2Vec。和 Word2Vec 一樣,該模型也存在兩種方法:Distributed Memory(DM) 和 Distributed Bag of Words(DBOW)。DM 試圖在給定上下文和段落向量的情況下預測單詞的概率。在一個句子或者文件的訓練過程中,段落 ID 保持不變,共享著同一個段落向量。DBOW 則在僅給定段落向量的情況下預測段落中一組隨機單詞的概率。

Token

Token在詞法分析中是標記的意思。自然語言處理中,一般來說,Token代表“詞”。自然語言預處理中,一個很重要的步驟就是將你收集的句子進行分詞,將一個句子分解成“詞”的列表。

交叉驗證

交叉驗證(Cross validation),有時亦稱迴圈估計, 是一種統計學上將資料樣本切割成較小子集的實用方法。於是可以先在一個子集上做分析, 而其它子集則用來做後續對此分析的確認及驗證。 一開始的子集被稱為訓練集。而其它的子集則被稱為驗證集或測試集。交叉驗證是一種評估統計分析、機器學習演算法對獨立於訓練資料的資料集的泛化能力(generalize)

least-one-out cross-validation(loocv) 假設dataset中有n個樣本,那LOOCV也就是n-CV,意思是每個樣本單獨作為一次測試集,剩餘n-1個樣本則做為訓練集。

優點: 1)每一回閤中幾乎所有的樣本皆用於訓練model,因此最接近母體樣本的分佈,估測所得的generalization error比較可靠。 2)實驗過程中沒有隨機因素會影響實驗資料,確保實驗過程是可以被複制的。 但LOOCV的缺點則是計算成本高,為需要建立的models數量與總樣本數量相同,當總樣本數量相當多時,LOOCV在實作上便有困難,除非每次訓練model的速度很快,或是可以用平行化計算減少計算所需的時間。

LinearRegression

[sklearn學習]linear_model.LinearRegression

from sklearn.linear_model import LinearRegression

fit(X, y[, n_jobs]) 對訓練集X, y進行訓練。是對scipy.linalg.lstsq的封裝 score(X, y[,]sample_weight) 定義為(1-u/v),其中u = ((y_true - y_pred)**2).sum(),而v=((y_true-y_true.mean())**2).mean() 最好的得分為1.0,一般的得分都比1.0低,得分越低代表結果越差。 其中sample_weight為(samples_n,)形狀的向量,可以指定對於某些sample的權值,如果覺得某些資料比較重要,可以將其的權值設定的大一些。

程式碼和處理流程

語料庫的準備

語料庫的準備,就是將你準備好的文章庫,轉換為一個語料庫。 你的文章一般會被儲存為TaggedDocument,也就是帶有標籤的文件。 一篇文章對應著一個TaggedDocument物件。 TaggedDocument裡面存放的是Token列表和Tag: 其中Token列表就是將文章通過分詞軟體分成的詞語的列表,Tag這裡儲存著原來文章的編號。 這裡的Tag也可以是文件的標題,或者任何可以代表文件的Paragraph Id。 下面這個程式碼中 tdocs變量表示一個TaggedDocument陣列。

注意:在gensim以前版本中TaggedDocument是LabeledSentence

corpus = Doc2Vec(tdocs, dm=1, dm_mean=1,
                 size=300, window=8, min_count=2, workers=4, iter=20)
corpus.save(os.path.join(WORK_DIR, 'base-pv_dm.mdl'))

關於這個函式的引數介紹,可以參考這裡,全英文非常晦澀難懂的介紹: https://radimrehurek.com/gensim/models/doc2vec.html

dm defines the training algorithm. By default (dm=1), ‘distributed memory’ (PV-DM) is used. Otherwise, distributed bag of words (PV-DBOW) is employed. dm:定義了訓練的演算法,預設值為1,使用 ‘distributed memory’方法,不然則使用分散式的“bag of words” 方法。(dm,就是distributed memory的意思) size is the dimensionality of the feature vectors. size:是向量的維度,本專案維度設定是300。維度這個引數也是需要通過大量實驗獲得最佳的值。 dm_mean = if 0 (default), use the sum of the context word vectors. If 1, use the mean. Only applies when dm is used in non-concatenative mode. dm_mean:如果是預設值0,則使用上下文向量的和(SUM),如果是1的話,則使用上下文向量的平均值。這個僅僅在dm使用non-concatenative的模式才發生效果。 workers = use this many worker threads to train the model (=faster training with multicore machines). 如果是多核處理器,這裡可以指定並行數 iter = number of iterations (epochs) over the corpus. The default inherited from Word2Vec is 5, but values of 10 or 20 are common in published ‘Paragraph Vector’ experiments. 迭代次數:預設的迭代次數是5,但是最佳實踐應該是10或者20. min_count = ignore all words with total frequency lower than this. 如果出現頻率少於min_count,則忽略 window is the maximum distance between the predicted word and context words used for prediction within a document. window是被預測詞語和上下文詞語在同一個文件中的最大的距離。

語料庫也是支援序列化操作的,語料庫可以儲存為磁碟上的檔案:

Save the object to file (also see load). fname_or_handle is either a string specifying the file name to save to, or an open file-like object which can be written to. If the object is a file handle, no special array handling will be performed; all attributes will be saved to the same file.

語料庫建成之後,就可以進行一些有趣的檢索了。 例如參考文件 [Algorithm & NLP] 文字深度表示模型——word2vec&doc2vec詞向量模型 中的句子相似度實驗:

下面是sentence2vec的結果示例。先利用中文sentence語料訓練句向量,然後通過計算句向量之間的cosine值,得到最相似的句子。可以看到句向量在對句子的語義表徵上還是相當驚歎的。 句子相似度結果

相似檢索

這裡的相似度檢索是指,給定一個正面的句子,然後檢索和其相似度最大的句子。 當然,這裡也可以指定一個負面的句子,也就是和這個句子越不相似越好。 這裡有一個限制,如果正面的句子和負面的句子,進行分詞之後,沒有一個詞語是被訓練過的(被訓練過的詞語,是指語料庫裡面存在的詞語),則無法進行操作。

具體在求相似度的操作之前,檢索用向量需要進行一下處理。 假設positive變數是一個數組,數組裡面存放著正面的Token。 corpus[token]表示token的向量,這裡對向量進行按列求和,結果是一個和token維度一樣的向量。換句話說,就是將多個向量合併為單個向量。(Token向量的求和向量)

然後將上面那個“Token向量的求和向量”,和新的positive的推測向量進行相加,獲得一個新的"求相似度用向量"

(Negative和Positive類似)

    p = np.array([ corpus[token] for token in positive ]).sum(axis=0)
    p = p + corpus.infer_vector(positive, steps=20)
    n = np.array([ corpus[token] for token in negative ]).sum(axis=0)
    n = n + corpus.infer_vector(negative, steps=20)

在語料庫物件(Document Model)中有一個很有用的方法infer_vector,這個方法可以基於當前的文件模型快速,將一個文件轉換(按照模型推測)成一個向量。

infer_vector (doc_words, alpha=0.1, min_alpha=0.0001, steps=5) Infer a vector for given post-bulk training document. Document should be a list of (word) tokens.

在機器學習界,有兩種機器學習方式,一種是Online的,一種是Offline的。Online的方式,模型可以實時更新,新的樣本會被實時進行訓練,訓練結果也實時反映到模型中去。Offline的方式,如果有新的樣本,則需要將新老樣本放在一起,重新進行訓練。這裡的話,模型無法進行Online的訓練,所以新的樣本,只是基於訓練好的模型,被轉換(推測 Infer,有些類似於預測Predict)為一個向量。

相似度計算的核心方法是most_similar

most_similar (positive=[], negative=[], topn=10, clip_start=0, clip_end=None, indexer=None) Find the top-N most similar docvecs known from training. Positive docs contribute positively towards the similarity, negative docs negatively. This method computes cosine similarity between a simple mean of the projection weight vectors of the given docs. Docs may be specified as vectors, integer indexes of trained docvecs, or if the documents were originally presented with string tags, by the corresponding tags. The ‘clip_start’ and ‘clip_end’ allow limiting results to a particular contiguous range of the underlying doctag_syn0norm vectors. (This may be useful if the ordering there was chosen to be significant, such as more popular tag IDs in lower indexes.) 尋找最相似的N個文件。正面(Positive)文件向相似度貢獻正面的值,負面(Negative)文件貢獻負面的值。這個方法通過計算給定文章的向量的加權平均值的餘弦相似度來給出結果。可以通過向量,被訓練過的文件向量的下標,或者原始的字串標籤來指定文件(正面或者負面文件)。 ‘clip_start’ 和 ‘clip_end’則是指定了相似度檢索的範圍。

官方文件其實說明的不是很清楚,很多地方還是不容易理解。 topn這個引數應該沒有問題,你想返回最相似的多少個,這裡就指定多少即可。 對於positive和nagative的指定,首先明確一下,這裡必須是一個數組,即使只有一個值,也必須是陣列。 positive和nagative數組裡面的值,可以是: 1.具體的文件的向量 2.被訓練過的文件的下標 3.文件的Tag字元。(本專案裡面的Tag就是文件的編號) 具體到這個專案中,Positive則是上文提到的"求相似度用向量"。 ‘clip_start’ 和 ‘clip_end’則是指定了相似度檢索的範圍,這個一般是用來限定檢索範圍,例如只想在1年或者3年的資料中進行檢索。

情感模型建立

MiniBatchKMeans

情感分析是建立在文件的聚類基礎上的。由於計算量比較巨大,專案使用的是MiniBatchKMeans。 在有效減少計算時間的同時,也能保證計算誤差在可接受範圍中。 專案中使用的是Leave-One-Out Cross Validation,每次將一個樣本作為測試集,一共進行n次交叉驗證。

K-Means演算法是常用的聚類演算法,但其演算法本身存在一定的問題,例如在大資料量下的計算時間過長就是一個重要問題。為此,Mini Batch K-Means,這個基於K-Means的變種聚類演算法應運而生。 Mini Batch K-Means

from sklearn.cluster import MiniBatchKMeans
X_km_norm = [ unitvec(x) for x in X_km ]  #轉為單位向量

km_all = MiniBatchKMeans(n_clusters=8)       #分為8個簇

km_all.fit(X_km_norm)                      #計算8個簇的質心

fit(X, y=None) Compute the centroids on X by chunking it into mini-batches. fit擬合操作,實際上就是計算每個簇的質心。 所以說,如果簇只有一個的話,擬合的意義是求出整個資料的質心。 predict(X) Predict the closest cluster each sample in X belongs to. predict預測操作,是給出每個樣本屬於哪個簇的結果

訓練樣本的分類和整理

專案中將所有樣本按照時間分為:1年期樣本和3年期樣本。同時根據其他業務規則進行了分類(分類規則需要保密)。由於收集樣本的渠道不同(不同的公司,組織提供的樣本資料),所有的樣本還需要進行Remove Common Factor的操作: 1.樣本的分類並沒有按照渠道進行分類,所以這裡同一收集渠道的樣本,也會被分在不同的類中 2.所有分類樣本,例如1年期,3年期的樣本,都必須進行Remove Common Factor 3.同一樣本可能同時存在於不同分類組裡面,因為有些是按照時間分類的,有些是按照業務分類的,分類的維度不同。

程式碼的邏輯如下: fit:XX[tag]裡面的tag表示收集渠道,XX[tag]表示某個渠道的樣本向量陣列: _cf[tag]:表示某個渠道的Common Factor向量,這裡使用np.array(XX[tag]).mean(axis=0)按列求均值獲得的。每一個渠道有一個Common Factor向量。 remove程式碼則是將某個渠道里面所有的向量,都剪去Common Factor向量 (程式碼有刪減,原來程式碼裡面有對於未知渠道的防禦程式碼,這裡已經簡化)

在我們的訓練樣本(document)中,有一個權重(weight)屬性,這個屬性是和業務有關的,也是這個專案需要進行預測的。例如,有一些使用者對於商品的評論,可以看作一個訓練樣本(document),這個商品的銷量可以看作權重屬性(weight)。我們需要訓練的模型就是獲得一個商品評論和銷量的關係模型,

利用這個模型能夠通過商品評論去預測一個商品的銷量。 這裡假設我們的權重分為5個級別,-2(無人問津),-1(滯銷),0(正常),1(暢銷),2(爆款) 分別對應5種銷量級別。 然後我們將所有樣本通過MiniBatchKMeans分到8個不同簇裡面,注意,這裡的簇數和級別數是不一樣的。

另外請注意,訓練後的簇,其簇的編號和權重也是沒有任何關係的,簇號0-7和權重-2到2,完全是兩個獨立的體系。

    def fit(self, X, y):
        X = np.array(X)
        y = np.array(y).reshape(-1, 1)
        size_x = X[0].shape[0]
        self.km.fit([ unitvec(x) for x in X ])       
     #為評分Score進行資料整理
        n_clusters = self.km.get_params()['n_clusters']
        self.p_vec = np.zeros((n_clusters, size_x), np.float32)
        self.n_vec = np.zeros((n_clusters, size_x), np.float32)
        self.pn_vec = np.zeros((n_clusters, size_x), np.float32)        
     #簇的編號和權重沒有任何關係
        p_vecs = [ [] for i in range(n_clusters)]                   
     #存放每個簇的Positive向量
        n_vecs = [ [] for i in range(n_clusters)]                   
     #存放每個簇的Nagative向量
        for c, x, (w,) in zip(self.km.predict(X), X, y):
            ext = [x] * abs(w)                                      
     #向量 * 權重的絕對值
            if w > 0:
                p_vecs[c].extend(ext)                              
      #weight[0]是正數
            else:
                n_vecs[c].extend(ext)                               
      #weight[0]是負數和零
        for c in range(n_clusters):           
       if len(p_vecs[c]) == 0:
                p_vecs[c] = [ np.zeros(size_x, np.float32) ]           
        if len(n_vecs[c]) == 0:
                n_vecs[c] = [ np.zeros(size_x, np.float32) ]
            self.p_vec[c] = np.array(p_vecs[c]).mean(axis=0)       
         #簇的權重為正數的向量均值
            self.n_vec[c] = np.array(n_vecs[c]).mean(axis=0)       
          #簇的權重為負數的向量均值
            self.pn_vec[c] = unitvec(self.p_vec[c] - self.n_vec[c])
           #簇的正負差,單位向量化
        return self

同時,我們還需要獲得一個訓練的得分(score)

    def predict(self, X):
        if not X: return (([], []), [])
        X = np.array(X)
        res = np.zeros((X.shape[0], 2))
        labels = self.km.predict(X)       
     for i, (x, c) in enumerate(zip(X, labels)):           
      #pn_vec[c]已經單位向量化了,注意:p_score計算式中的是n_vec,反之亦然
            
      #負分數 =(待預測向量 - 簇的權重為正數的向量均值)點積 單位向量化簇的正負差
            p_score = np.dot(unitvec(x - self.n_vec[c]), self.pn_vec[c])    
            #正分數 =(待預測向量 - 簇的權重為負數的向量均值)點積 單位向量化簇的正負差
            n_score = np.dot(unitvec(x - self.p_vec[c]), -self.pn_vec[c])   
            res[i] = (p_score, n_score)       
       return (res, labels)

評價訓練得分的部分使用了餘弦原理,通過單位向量的點乘(點積)結果來獲得相似度。注意,一定要將兩個向量都單位化,轉換為模為1的向量,這樣點乘出來的結果才是餘弦值。

負分數 = 向量單位化(待預測向量 - 簇的權重為正數的向量均值)點積 單位向量化簇的正負差

負分數示意圖

Python語言

推薦通過網路上的 廖雪鋒的Python教程 學習python語法

numpy sum

axis:求和的維。

>>> np.sum([0.5, 1.5])2.0>>> np.sum([0.5, 0.7, 0.2, 1.5], dtype=np.int32)1>>> np.sum([[0, 1], [0, 5]])6>>> np.sum([[0, 1], [0, 5]], axis=0)
array([0, 6])>>> np.sum([[0, 1], [0, 5]], axis=1)
array([1, 5])

numpy mean

axis:求平均的維。

>>> a = np.array([[1, 2], [3, 4]])>>> np.mean(a)2.5>>> np.mean(a, axis=0)
array([ 2.,  3.])>>> np.mean(a, axis=1)
array([ 1.5,  3.5])

lambda 和浮點數

python中使用 .1 代表浮點數 0.1 或者 1. 代表浮點數 1.0。 原因是要保證結果的精度,防止程式自動強制轉換。

score = lambda X, y: 1.-((y-X)**2).sum()/((y-y.mean())**2).sum()

percentile

幾つかの數値データを小さい順に並べたとき、小さい方から數えて全體のX%に位置する値をXパーセンタイルと言います。 (數值按照從小到大進行排列,從小的數字開始計算,全體數字的X%的位置,數值是多少)

例えば10人のクラスがあるとして、各生徒のテストの點數が[40, 50, 60, 70, 75, 80, 83, 86, 89, 95]だったとします。 その時、下から95%に位置する點數(逆に言うと上位5%に位置する點數)が何點なのか示すものが95パーセンタイルになります。

以下、numpyを使ったサンプルです。

>>> import numpy as np>>> a = np.array([40, 50, 60, 70, 75, 80, 83, 86, 89, 95])>>> np.percentile(a, 95) # 95パーセンタイルを求めます(逆に言うと上位5%に位置する點數)92.299999999999997# 95パーセンタイルは約92.3點であることがわかります>>> np.percentile(a, 30) # 30パーセンタイルを求めます(逆に言うと上位70%に位置する點數)67.0# 30パーセンタイルは67.0點であることがわかります

numpy的mask

numpy的高階特性,可以進行資料的篩選。下面的程式碼沒有完全看懂,猜測了一下意思: n_clusters 是一個數字,則c也是一個數字。P[下標]也應該是一個數字,這個下標數字是由C==c這個條件來獲得的。在C這個數組裡面存放的就是數字,C[x] == c的時候,x則是需要求出的下標,P[C==c],則實際上就是P[x].當然,這裡x應該是多個值,則P[x]的結果也是一個數組。

    def score(self, X, y):
        score = lambda X, y: 1.-((y-X)**2).sum()/((y-y.mean())**2).sum()        #predict的結果為 (res, labels) 
        #res[i] = (p_score, n_score)
        #則最後的形式是[[p_score, n_score],[labels]]形式
        # P 為 p_score,N 為 n_score,C為label
        P, N, C = zip(*self.predict(X))
        P = np.array(P)
        N = np.array(N)
        C = np.array(C)
        y = np.array(y).reshape(-1, 1)
        n_clusters = self.km.get_params()['n_clusters']
        res = np.zeros((n_clusters+1, 1))       
     #==使用了numpy的mask特性
        for c in range(n_clusters):
            res[c] = score(P[C==c]-N[C==c], y[C==c])
        res[-1] = score(P-N, y)       
      return res