scikit-learn 梯度提高樹(GBDT)調參小結

    在梯度提高樹(GBDT)原理小結中,咱們對GBDT的原理作了總結,本文咱們就從scikit-learn裏GBDT的類庫使用方法做一個總結,主要會關注調參中的一些要點。html

1. scikit-learn GBDT類庫概述

    在sacikit-learn中,GradientBoostingClassifier爲GBDT的分類類, 而GradientBoostingRegressor爲GBDT的迴歸類。二者的參數類型徹底相同,固然有些參數好比損失函數loss的可選擇項並不相同。這些參數中,相似於Adaboost,咱們把重要參數分爲兩類,第一類是Boosting框架的重要參數,第二類是弱學習器即CART迴歸樹的重要參數。node

    下面咱們就從這兩個方面來介紹這些參數的使用。git

2. GBDT類庫boosting框架參數

    首先,咱們來看boosting框架相關的重要參數。因爲GradientBoostingClassifier和GradientBoostingRegressor的參數絕大部分相同,咱們下面會一塊兒來說,不一樣點會單獨指出。github

    1) n_estimators: 也就是弱學習器的最大迭代次數,或者說最大的弱學習器的個數。通常來講n_estimators過小,容易欠擬合,n_estimators太大,又容易過擬合,通常選擇一個適中的數值。默認是100。在實際調參的過程當中,咱們經常將n_estimators和下面介紹的參數learning_rate一塊兒考慮。算法

    2) learning_rate: 即每一個弱學習器的權重縮減係數$\nu$,也稱做步長,在原理篇的正則化章節咱們也講到了,加上了正則化項,咱們的強學習器的迭代公式爲$f_{k}(x) = f_{k-1}(x) + \nu h_k(x) $。$\nu$的取值範圍爲$0 < \nu \leq 1 $。對於一樣的訓練集擬合效果,較小的$\nu$意味着咱們須要更多的弱學習器的迭代次數。一般咱們用步長和迭代最大次數一塊兒來決定算法的擬合效果。因此這兩個參數n_estimators和learning_rate要一塊兒調參。通常來講,能夠從一個小一點的$\nu$開始調參,默認是1。框架

    3) subsample: 即咱們在原理篇的正則化章節講到的子採樣,取值爲(0,1]。注意這裏的子採樣和隨機森林不同,隨機森林使用的是放回抽樣,而這裏是不放回抽樣。若是取值爲1,則所有樣本都使用,等於沒有使用子採樣。若是取值小於1,則只有一部分樣本會去作GBDT的決策樹擬合。選擇小於1的比例能夠減小方差,即防止過擬合,可是會增長樣本擬合的誤差,所以取值不能過低。推薦在[0.5, 0.8]之間,默認是1.0,即不使用子採樣。dom

    4) init: 即咱們的初始化的時候的弱學習器,擬合對應原理篇裏面的$f_{0}(x)$,若是不輸入,則用訓練集樣原本作樣本集的初始化分類迴歸預測。不然用init參數提供的學習器作初始化分類迴歸預測。通常用在咱們對數據有先驗知識,或者以前作過一些擬合的時候,若是沒有的話就不用管這個參數了。函數

    5) loss: 即咱們GBDT算法中的損失函數。分類模型和迴歸模型的損失函數是不同的。post

      對於分類模型,有對數似然損失函數"deviance"和指數損失函數"exponential"二者輸入選擇。默認是對數似然損失函數"deviance"。在原理篇中對這些分類損失函數有詳細的介紹。通常來講,推薦使用默認的"deviance"。它對二元分離和多元分類各自都有比較好的優化。而指數損失函數等於把咱們帶到了Adaboost算法。學習

      對於迴歸模型,有均方差"ls", 絕對損失"lad", Huber損失"huber"和分位數損失「quantile」。默認是均方差"ls"。通常來講,若是數據的噪音點很少,用默認的均方差"ls"比較好。若是是噪音點較多,則推薦用抗噪音的損失函數"huber"。而若是咱們須要對訓練集進行分段預測的時候,則採用「quantile」。

    6) alpha:這個參數只有GradientBoostingRegressor有,當咱們使用Huber損失"huber"和分位數損失「quantile」時,須要指定分位數的值。默認是0.9,若是噪音點較多,能夠適當下降這個分位數的值。

3. GBDT類庫弱學習器參數

    這裏咱們再對GBDT的類庫弱學習器的重要參數作一個總結。因爲GBDT使用了CART迴歸決策樹,所以它的參數基原本源於決策樹類,也就是說,和DecisionTreeClassifier和DecisionTreeRegressor的參數基本相似。若是你已經很熟悉決策樹算法的調參,那麼這一節基本能夠跳過。不熟悉的朋友能夠繼續看下去。

    1) 劃分時考慮的最大特徵數max_features: 可使用不少種類型的值,默認是"None",意味着劃分時考慮全部的特徵數;若是是"log2"意味着劃分時最多考慮$log_2N$個特徵;若是是"sqrt"或者"auto"意味着劃分時最多考慮$\sqrt{N}$個特徵。若是是整數,表明考慮的特徵絕對數。若是是浮點數,表明考慮特徵百分比,即考慮(百分比xN)取整後的特徵數。其中N爲樣本總特徵數。通常來講,若是樣本特徵數很少,好比小於50,咱們用默認的"None"就能夠了,若是特徵數很是多,咱們能夠靈活使用剛纔描述的其餘取值來控制劃分時考慮的最大特徵數,以控制決策樹的生成時間。

    2) 決策樹最大深度max_depth: 默承認以不輸入,若是不輸入的話,默認值是3。通常來講,數據少或者特徵少的時候能夠無論這個值。若是模型樣本量多,特徵也多的狀況下,推薦限制這個最大深度,具體的取值取決於數據的分佈。經常使用的能夠取值10-100之間。

    3) 內部節點再劃分所需最小樣本數min_samples_split: 這個值限制了子樹繼續劃分的條件,若是某節點的樣本數少於min_samples_split,則不會繼續再嘗試選擇最優特徵來進行劃分。 默認是2.若是樣本量不大,不須要管這個值。若是樣本量數量級很是大,則推薦增大這個值。

    4) 葉子節點最少樣本數min_samples_leaf: 這個值限制了葉子節點最少的樣本數,若是某葉子節點數目小於樣本數,則會和兄弟節點一塊兒被剪枝。 默認是1,能夠輸入最少的樣本數的整數,或者最少樣本數佔樣本總數的百分比。若是樣本量不大,不須要管這個值。若是樣本量數量級很是大,則推薦增大這個值。

    5)葉子節點最小的樣本權重和min_weight_fraction_leaf:這個值限制了葉子節點全部樣本權重和的最小值,若是小於這個值,則會和兄弟節點一塊兒被剪枝。 默認是0,就是不考慮權重問題。通常來講,若是咱們有較多樣本有缺失值,或者分類樹樣本的分佈類別誤差很大,就會引入樣本權重,這時咱們就要注意這個值了。

    6) 最大葉子節點數max_leaf_nodes: 經過限制最大葉子節點數,能夠防止過擬合,默認是"None」,即不限制最大的葉子節點數。若是加了限制,算法會創建在最大葉子節點數內最優的決策樹。若是特徵很少,能夠不考慮這個值,可是若是特徵分紅多的話,能夠加以限制,具體的值能夠經過交叉驗證獲得。

    7) 節點劃分最小不純度min_impurity_split:  這個值限制了決策樹的增加,若是某節點的不純度(基於基尼係數,均方差)小於這個閾值,則該節點再也不生成子節點。即爲葉子節點 。通常不推薦改動默認值1e-7。

4. GBDT調參實例

    這裏咱們用一個二元分類的例子來說解下GBDT的調參。這部分參考了這個Github上的數據調參過程Parameter_Tuning_GBM_with_Example。這個例子的數據有87000多行,單機跑會比較慢,下面的例子我只選擇了它的前面20000行,我將其打包後,下載地址在這

    完整代碼參見個人github: https://github.com/ljpzzz/machinelearning/blob/master/ensemble-learning/gbdt_classifier.ipynb

    首先,咱們載入須要的類庫:   

import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn import cross_validation, metrics
from sklearn.grid_search import GridSearchCV

import matplotlib.pylab as plt
%matplotlib inline

    接着,咱們把解壓的數據用下面的代碼載入,順便看看數據的類別分佈。

train = pd.read_csv('train_modified.csv')
target='Disbursed' # Disbursed的值就是二元分類的輸出
IDcol = 'ID'
train['Disbursed'].value_counts() 

     能夠看到類別輸出以下,也就是類別0的佔大多數。

0    19680
1      320
Name: Disbursed, dtype: int64

    如今咱們獲得咱們的訓練集。最後一列Disbursed是分類輸出。前面的全部列(不考慮ID列)都是樣本特徵。

x_columns = [x for x in train.columns if x not in [target, IDcol]]
X = train[x_columns]
y = train['Disbursed']

    無論任何參數,都用默認的,咱們擬合下數據看看:

gbm0 = GradientBoostingClassifier(random_state=10)
gbm0.fit(X,y)
y_pred = gbm0.predict(X)
y_predprob = gbm0.predict_proba(X)[:,1]
print "Accuracy : %.4g" % metrics.accuracy_score(y.values, y_pred)
print "AUC Score (Train): %f" % metrics.roc_auc_score(y, y_predprob)

    輸出以下,可見擬合還能夠,咱們下面看看怎麼經過調參提升模型的泛化能力。

Accuracy : 0.9852
AUC Score (Train): 0.900531

   首先咱們從步長(learning rate)和迭代次數(n_estimators)入手。通常來講,開始選擇一個較小的步長來網格搜索最好的迭代次數。這裏,咱們將步長初始值設置爲0.1。對於迭代次數進行網格搜索以下:

param_test1 = {'n_estimators':range(20,81,10)}
gsearch1 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, min_samples_split=300,
                                  min_samples_leaf=20,max_depth=8,max_features='sqrt', subsample=0.8,random_state=10), 
                       param_grid = param_test1, scoring='roc_auc',iid=False,cv=5)
gsearch1.fit(X,y)
gsearch1.grid_scores_, gsearch1.best_params_, gsearch1.best_score_

    輸出以下,可見最好的迭代次數是60。

([mean: 0.81285, std: 0.01967, params: {'n_estimators': 20},
  mean: 0.81438, std: 0.01947, params: {'n_estimators': 30},
  mean: 0.81451, std: 0.01933, params: {'n_estimators': 40},
  mean: 0.81618, std: 0.01848, params: {'n_estimators': 50},
  mean: 0.81751, std: 0.01736, params: {'n_estimators': 60},
  mean: 0.81547, std: 0.01900, params: {'n_estimators': 70},
  mean: 0.81299, std: 0.01860, params: {'n_estimators': 80}],
 {'n_estimators': 60},
 0.8175146087398375)

    找到了一個合適的迭代次數,如今咱們開始對決策樹進行調參。首先咱們對決策樹最大深度max_depth和內部節點再劃分所需最小樣本數min_samples_split進行網格搜索。

param_test2 = {'max_depth':range(3,14,2), 'min_samples_split':range(100,801,200)}
gsearch2 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60, min_samples_leaf=20, 
      max_features='sqrt', subsample=0.8, random_state=10), 
   param_grid = param_test2, scoring='roc_auc',iid=False, cv=5)
gsearch2.fit(X,y)
gsearch2.grid_scores_, gsearch2.best_params_, gsearch2.best_score_

    輸出以下,可見最好的最大樹深度是7,內部節點再劃分所需最小樣本數是300。

([mean: 0.81199, std: 0.02073, params: {'min_samples_split': 100, 'max_depth': 3},
  mean: 0.81267, std: 0.01985, params: {'min_samples_split': 300, 'max_depth': 3},
  mean: 0.81238, std: 0.01937, params: {'min_samples_split': 500, 'max_depth': 3},
  mean: 0.80925, std: 0.02051, params: {'min_samples_split': 700, 'max_depth': 3},
  mean: 0.81846, std: 0.01843, params: {'min_samples_split': 100, 'max_depth': 5},
  mean: 0.81630, std: 0.01810, params: {'min_samples_split': 300, 'max_depth': 5},
  mean: 0.81315, std: 0.01898, params: {'min_samples_split': 500, 'max_depth': 5},
  mean: 0.81262, std: 0.02090, params: {'min_samples_split': 700, 'max_depth': 5},
  mean: 0.81807, std: 0.02004, params: {'min_samples_split': 100, 'max_depth': 7},
  mean: 0.82137, std: 0.01733, params: {'min_samples_split': 300, 'max_depth': 7},
  mean: 0.81703, std: 0.01773, params: {'min_samples_split': 500, 'max_depth': 7},
  mean: 0.81383, std: 0.02327, params: {'min_samples_split': 700, 'max_depth': 7},
  mean: 0.81107, std: 0.02178, params: {'min_samples_split': 100, 'max_depth': 9},
  mean: 0.80944, std: 0.02612, params: {'min_samples_split': 300, 'max_depth': 9},
  mean: 0.81476, std: 0.01973, params: {'min_samples_split': 500, 'max_depth': 9},
  mean: 0.81601, std: 0.02576, params: {'min_samples_split': 700, 'max_depth': 9},
  mean: 0.81091, std: 0.02227, params: {'min_samples_split': 100, 'max_depth': 11},
  mean: 0.81309, std: 0.02696, params: {'min_samples_split': 300, 'max_depth': 11},
  mean: 0.81713, std: 0.02379, params: {'min_samples_split': 500, 'max_depth': 11},
  mean: 0.81347, std: 0.02702, params: {'min_samples_split': 700, 'max_depth': 11},
  mean: 0.81444, std: 0.01813, params: {'min_samples_split': 100, 'max_depth': 13},
  mean: 0.80825, std: 0.02291, params: {'min_samples_split': 300, 'max_depth': 13},
  mean: 0.81923, std: 0.01693, params: {'min_samples_split': 500, 'max_depth': 13},
  mean: 0.81382, std: 0.02258, params: {'min_samples_split': 700, 'max_depth': 13}],
 {'max_depth': 7, 'min_samples_split': 300},
 0.8213724275914632)
   

    因爲決策樹深度7是一個比較合理的值,咱們把它定下來,對於內部節點再劃分所需最小樣本數min_samples_split,咱們暫時不能一塊兒定下來,由於這個還和決策樹其餘的參數存在關聯。下面咱們再對內部節點再劃分所需最小樣本數min_samples_split和葉子節點最少樣本數min_samples_leaf一塊兒調參。

param_test3 = {'min_samples_split':range(800,1900,200), 'min_samples_leaf':range(60,101,10)}
gsearch3 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=7,
                                     max_features='sqrt', subsample=0.8, random_state=10), 
                       param_grid = param_test3, scoring='roc_auc',iid=False, cv=5)
gsearch3.fit(X,y)
gsearch3.grid_scores_, gsearch3.best_params_, gsearch3.best_score_

輸出結果以下,可見這個min_samples_split在邊界值,還有進一步調試小於邊界60的必要。因爲這裏只是例子,因此你們能夠本身下來用包含小於60的網格搜索來尋找合適的值。

([mean: 0.81828, std: 0.02251, params: {'min_samples_split': 800, 'min_samples_leaf': 60},
  mean: 0.81731, std: 0.02344, params: {'min_samples_split': 1000, 'min_samples_leaf': 60},
  mean: 0.82220, std: 0.02250, params: {'min_samples_split': 1200, 'min_samples_leaf': 60},
  mean: 0.81447, std: 0.02125, params: {'min_samples_split': 1400, 'min_samples_leaf': 60},
  mean: 0.81495, std: 0.01626, params: {'min_samples_split': 1600, 'min_samples_leaf': 60},
  mean: 0.81528, std: 0.02140, params: {'min_samples_split': 1800, 'min_samples_leaf': 60},
  mean: 0.81590, std: 0.02517, params: {'min_samples_split': 800, 'min_samples_leaf': 70},
  mean: 0.81573, std: 0.02207, params: {'min_samples_split': 1000, 'min_samples_leaf': 70},
  mean: 0.82021, std: 0.02521, params: {'min_samples_split': 1200, 'min_samples_leaf': 70},
  mean: 0.81512, std: 0.01995, params: {'min_samples_split': 1400, 'min_samples_leaf': 70},
  mean: 0.81395, std: 0.02081, params: {'min_samples_split': 1600, 'min_samples_leaf': 70},
  mean: 0.81587, std: 0.02082, params: {'min_samples_split': 1800, 'min_samples_leaf': 70},
  mean: 0.82064, std: 0.02698, params: {'min_samples_split': 800, 'min_samples_leaf': 80},
  mean: 0.81490, std: 0.02475, params: {'min_samples_split': 1000, 'min_samples_leaf': 80},
  mean: 0.82009, std: 0.02568, params: {'min_samples_split': 1200, 'min_samples_leaf': 80},
  mean: 0.81850, std: 0.02226, params: {'min_samples_split': 1400, 'min_samples_leaf': 80},
  mean: 0.81855, std: 0.02099, params: {'min_samples_split': 1600, 'min_samples_leaf': 80},
  mean: 0.81666, std: 0.02249, params: {'min_samples_split': 1800, 'min_samples_leaf': 80},
  mean: 0.81960, std: 0.02437, params: {'min_samples_split': 800, 'min_samples_leaf': 90},
  mean: 0.81560, std: 0.02235, params: {'min_samples_split': 1000, 'min_samples_leaf': 90},
  mean: 0.81936, std: 0.02542, params: {'min_samples_split': 1200, 'min_samples_leaf': 90},
  mean: 0.81362, std: 0.02254, params: {'min_samples_split': 1400, 'min_samples_leaf': 90},
  mean: 0.81429, std: 0.02417, params: {'min_samples_split': 1600, 'min_samples_leaf': 90},
  mean: 0.81299, std: 0.02262, params: {'min_samples_split': 1800, 'min_samples_leaf': 90},
  mean: 0.82000, std: 0.02511, params: {'min_samples_split': 800, 'min_samples_leaf': 100},
  mean: 0.82209, std: 0.01816, params: {'min_samples_split': 1000, 'min_samples_leaf': 100},
  mean: 0.81821, std: 0.02337, params: {'min_samples_split': 1200, 'min_samples_leaf': 100},
  mean: 0.81922, std: 0.02377, params: {'min_samples_split': 1400, 'min_samples_leaf': 100},
  mean: 0.81545, std: 0.02221, params: {'min_samples_split': 1600, 'min_samples_leaf': 100},
  mean: 0.81704, std: 0.02509, params: {'min_samples_split': 1800, 'min_samples_leaf': 100}],
 {'min_samples_leaf': 60, 'min_samples_split': 1200},
 0.8222032996697154)

    咱們調了這麼多參數了,終於能夠都放到GBDT類裏面去看看效果了。如今咱們用新參數擬合數據:

gbm1 = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=7, min_samples_leaf =60, 
               min_samples_split =1200, max_features='sqrt', subsample=0.8, random_state=10)
gbm1.fit(X,y)
y_pred = gbm1.predict(X)
y_predprob = gbm1.predict_proba(X)[:,1]
print "Accuracy : %.4g" % metrics.accuracy_score(y.values, y_pred)
print "AUC Score (Train): %f" % metrics.roc_auc_score(y, y_predprob)

    輸出以下:

Accuracy : 0.984
AUC Score (Train): 0.908099

    對比咱們最開始徹底不調參的擬合效果,可見精確度稍有降低,主要原理是咱們使用了0.8的子採樣,20%的數據沒有參與擬合。

    如今咱們再對最大特徵數max_features進行網格搜索。

param_test4 = {'max_features':range(7,20,2)}
gsearch4 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=7, min_samples_leaf =60, 
               min_samples_split =1200, subsample=0.8, random_state=10), 
                       param_grid = param_test4, scoring='roc_auc',iid=False, cv=5)
gsearch4.fit(X,y)
gsearch4.grid_scores_, gsearch4.best_params_, gsearch4.best_score_

    輸出以下:

([mean: 0.82220, std: 0.02250, params: {'max_features': 7},
  mean: 0.82241, std: 0.02421, params: {'max_features': 9},
  mean: 0.82108, std: 0.02302, params: {'max_features': 11},
  mean: 0.82064, std: 0.01900, params: {'max_features': 13},
  mean: 0.82198, std: 0.01514, params: {'max_features': 15},
  mean: 0.81355, std: 0.02053, params: {'max_features': 17},
  mean: 0.81877, std: 0.01863, params: {'max_features': 19}],
 {'max_features': 9},
 0.822412506351626)

    如今咱們再對子採樣的比例進行網格搜索:

param_test5 = {'subsample':[0.6,0.7,0.75,0.8,0.85,0.9]}
gsearch5 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=7, min_samples_leaf =60, 
               min_samples_split =1200, max_features=9, random_state=10), 
                       param_grid = param_test5, scoring='roc_auc',iid=False, cv=5)
gsearch5.fit(X,y)
gsearch5.grid_scores_, gsearch5.best_params_, gsearch5.best_score_

    輸出以下:

([mean: 0.81828, std: 0.02392, params: {'subsample': 0.6},
  mean: 0.82344, std: 0.02708, params: {'subsample': 0.7},
  mean: 0.81673, std: 0.02196, params: {'subsample': 0.75},
  mean: 0.82241, std: 0.02421, params: {'subsample': 0.8},
  mean: 0.82285, std: 0.02446, params: {'subsample': 0.85},
  mean: 0.81738, std: 0.02236, params: {'subsample': 0.9}],
 {'subsample': 0.7},
 0.8234378969766262)

    如今咱們基本已經獲得咱們全部調優的參數結果了。這時咱們能夠減半步長,最大迭代次數加倍來增長咱們模型的泛化能力。再次擬合咱們的模型:

gbm2 = GradientBoostingClassifier(learning_rate=0.05, n_estimators=120,max_depth=7, min_samples_leaf =60, 
               min_samples_split =1200, max_features=9, subsample=0.7, random_state=10)
gbm2.fit(X,y)
y_pred = gbm2.predict(X)
y_predprob = gbm2.predict_proba(X)[:,1]
print "Accuracy : %.4g" % metrics.accuracy_score(y.values, y_pred)
print "AUC Score (Train): %f" % metrics.roc_auc_score(y, y_predprob)

    輸出以下:

Accuracy : 0.984
AUC Score (Train): 0.905324

    能夠看到AUC分數比起以前的版本稍有降低,這個緣由是咱們爲了增長模型泛化能力,爲防止過擬合而減半步長,最大迭代次數加倍,同時減少了子採樣的比例,從而減小了訓練集的擬合程度。

    下面咱們繼續將步長縮小5倍,最大迭代次數增長5倍,繼續擬合咱們的模型:

gbm3 = GradientBoostingClassifier(learning_rate=0.01, n_estimators=600,max_depth=7, min_samples_leaf =60, 
               min_samples_split =1200, max_features=9, subsample=0.7, random_state=10)
gbm3.fit(X,y)
y_pred = gbm3.predict(X)
y_predprob = gbm3.predict_proba(X)[:,1]
print "Accuracy : %.4g" % metrics.accuracy_score(y.values, y_pred)
print "AUC Score (Train): %f" % metrics.roc_auc_score(y, y_predprob)

    輸出以下,可見減少步長增長迭代次數能夠在保證泛化能力的基礎上增長一些擬合程度。

Accuracy : 0.984
AUC Score (Train): 0.908581

    最後咱們繼續步長縮小一半,最大迭代次數增長2倍,擬合咱們的模型:

gbm4 = GradientBoostingClassifier(learning_rate=0.005, n_estimators=1200,max_depth=7, min_samples_leaf =60, 
               min_samples_split =1200, max_features=9, subsample=0.7, random_state=10)
gbm4.fit(X,y)
y_pred = gbm4.predict(X)
y_predprob = gbm4.predict_proba(X)[:,1]
print "Accuracy : %.4g" % metrics.accuracy_score(y.values, y_pred)
print "AUC Score (Train): %f" % metrics.roc_auc_score(y, y_predprob)

    輸出以下,此時因爲步長實在過小,致使擬合效果反而變差,也就是說,步長不能設置的太小。

Accuracy : 0.984
AUC Score (Train): 0.908232

    

    以上就是GBDT調參的一個總結,但願能夠幫到朋友們。

 

(歡迎轉載,轉載請註明出處。歡迎溝通交流: liujianping-ok@163.com) 

相關文章
相關標籤/搜索