【機器學習實戰】第7章 集成方法 ensemble method


第7章 集成方法 ensemble method

利用AdaBoost元算法提高分類

集成方法: ensemble method(元算法: meta algorithm) 概述

  • 概念:是對其他算法進行組合的一種形式。

  • 通俗來說: 當做重要決定時,大家可能都會考慮吸取多個專家而不只是一個人的意見。 機器學習處理問題時又何嘗不是如此? 這就是集成方法背后的思想。

  • 集成方法:

    1. 投票選舉(bagging: 自舉匯聚法 bootstrap aggregating): 是基於數據隨機重抽樣分類器構造的方法
    2. 再學習(boosting): 是基於所有分類器的加權求和的方法

集成方法 場景

目前 bagging 方法最流行的版本是: 隨機森林(random forest)
選男友:美女選擇擇偶對象的時候,會問幾個閨蜜的建議,最后選擇一個綜合得分最高的一個作為男朋友

目前 boosting 方法最流行的版本是: AdaBoost
追女友:3個帥哥追同一個美女,第1個帥哥失敗->(傳授經驗:姓名、家庭情況) 第2個帥哥失敗->(傳授經驗:興趣愛好、性格特點) 第3個帥哥成功

bagging 和 boosting 區別是什么?

  1. bagging 是一種與 boosting 很類似的技術, 所使用的多個分類器的類型(數據量和特征量)都是一致的。
  2. bagging 是由不同的分類器(1.數據隨機化 2.特征隨機化)經過訓練,綜合得出的出現最多分類結果;boosting 是通過調整已有分類器錯分的那些數據來獲得新的分類器,得出目前最優的結果。
  3. bagging 中的分類器權重是相等的;而 boosting 中的分類器加權求和,所以權重並不相等,每個權重代表的是其對應分類器在上一輪迭代中的成功度。

隨機森林

隨機森林 概述

  • 隨機森林指的是利用多棵樹對樣本進行訓練並預測的一種分類器。
  • 決策樹相當於一個大師,通過自己在數據集中學到的知識用於新數據的分類。但是俗話說得好,一個諸葛亮,玩不過三個臭皮匠。隨機森林就是希望構建多個臭皮匠,希望最終的分類效果能夠超過單個大師的一種算法。

隨機森林 原理

那隨機森林具體如何構建呢?
有兩個方面:

  1. 數據的隨機性化
  2. 待選特征的隨機化

使得隨機森林中的決策樹都能夠彼此不同,提升系統的多樣性,從而提升分類性能。

數據的隨機化:使得隨機森林中的決策樹更普遍化一點,適合更多的場景。

(有放回的准確率在:70% 以上, 無放回的准確率在:60% 以上)

  1. 采取有放回的抽樣方式 構造子數據集,保證不同子集之間的數量級一樣(不同子集/同一子集 之間的元素可以重復)
  2. 利用子數據集來構建子決策樹,將這個數據放到每個子決策樹中,每個子決策樹輸出一個結果。
  3. 然后統計子決策樹的投票結果,得到最終的分類 就是 隨機森林的輸出結果。
  4. 如下圖,假設隨機森林中有3棵子決策樹,2棵子樹的分類結果是A類,1棵子樹的分類結果是B類,那么隨機森林的分類結果就是A類。

數據重抽樣

待選特征的隨機化

  1. 子樹從所有的待選特征中隨機選取一定的特征。
  2. 在選取的特征中選取最優的特征。

下圖中,藍色的方塊代表所有可以被選擇的特征,也就是目前的待選特征;黃色的方塊是分裂特征。
左邊是一棵決策樹的特征選取過程,通過在待選特征中選取最優的分裂特征(別忘了前文提到的ID3算法,C4.5算法,CART算法等等),完成分裂。
右邊是一個隨機森林中的子樹的特征選取過程。

特征重抽樣

隨機森林 開發流程

收集數據:任何方法
准備數據:轉換樣本集
分析數據:任何方法
訓練算法:通過數據隨機化和特征隨機化,進行多實例的分類評估
測試算法:計算錯誤率
使用算法:輸入樣本數據,然后運行 隨機森林 算法判斷輸入數據分類屬於哪個分類,最后對計算出的分類執行后續處理

隨機森林 算法特點

優點:幾乎不需要輸入准備、可實現隱式特征選擇、訓練速度非常快、其他模型很難超越、很難建立一個糟糕的隨機森林模型、大量優秀、免費以及開源的實現。
缺點:劣勢在於模型大小、是個很難去解釋的黑盒子。
適用數據范圍:數值型和標稱型

項目案例: 聲納信號分類

項目概述

這是 Gorman 和 Sejnowski 在研究使用神經網絡的聲納信號分類中使用的數據集。任務是訓練一個模型來區分聲納信號。

開發流程

收集數據:提供的文本文件
准備數據:轉換樣本集
分析數據:手工檢查數據
訓練算法:在數據上,利用 random_forest() 函數進行優化評估,返回模型的綜合分類結果
測試算法:在采用自定義 n_folds 份隨機重抽樣 進行測試評估,得出綜合的預測評分
使用算法:若你感興趣可以構建完整的應用程序,從案例進行封裝,也可以參考我們的代碼

收集數據:提供的文本文件

樣本數據:sonar-all-data.txt

0.02,0.0371,0.0428,0.0207,0.0954,0.0986,0.1539,0.1601,0.3109,0.2111,0.1609,0.1582,0.2238,0.0645,0.066,0.2273,0.31,0.2999,0.5078,0.4797,0.5783,0.5071,0.4328,0.555,0.6711,0.6415,0.7104,0.808,0.6791,0.3857,0.1307,0.2604,0.5121,0.7547,0.8537,0.8507,0.6692,0.6097,0.4943,0.2744,0.051,0.2834,0.2825,0.4256,0.2641,0.1386,0.1051,0.1343,0.0383,0.0324,0.0232,0.0027,0.0065,0.0159,0.0072,0.0167,0.018,0.0084,0.009,0.0032,R
0.0453,0.0523,0.0843,0.0689,0.1183,0.2583,0.2156,0.3481,0.3337,0.2872,0.4918,0.6552,0.6919,0.7797,0.7464,0.9444,1,0.8874,0.8024,0.7818,0.5212,0.4052,0.3957,0.3914,0.325,0.32,0.3271,0.2767,0.4423,0.2028,0.3788,0.2947,0.1984,0.2341,0.1306,0.4182,0.3835,0.1057,0.184,0.197,0.1674,0.0583,0.1401,0.1628,0.0621,0.0203,0.053,0.0742,0.0409,0.0061,0.0125,0.0084,0.0089,0.0048,0.0094,0.0191,0.014,0.0049,0.0052,0.0044,R
0.0262,0.0582,0.1099,0.1083,0.0974,0.228,0.2431,0.3771,0.5598,0.6194,0.6333,0.706,0.5544,0.532,0.6479,0.6931,0.6759,0.7551,0.8929,0.8619,0.7974,0.6737,0.4293,0.3648,0.5331,0.2413,0.507,0.8533,0.6036,0.8514,0.8512,0.5045,0.1862,0.2709,0.4232,0.3043,0.6116,0.6756,0.5375,0.4719,0.4647,0.2587,0.2129,0.2222,0.2111,0.0176,0.1348,0.0744,0.013,0.0106,0.0033,0.0232,0.0166,0.0095,0.018,0.0244,0.0316,0.0164,0.0095,0.0078,R

准備數據:轉換樣本集

# 導入csv文件 def loadDataSet(filename): dataset = [] with open(filename, 'r') as fr: for line in fr.readlines(): if not line: continue lineArr = [] for featrue in line.split(','): # strip()返回移除字符串頭尾指定的字符生成的新字符串 str_f = featrue.strip() if str_f.isdigit(): # 判斷是否是數字 # 將數據集的第column列轉換成float形式 lineArr.append(float(str_f)) else: # 添加分類標簽 lineArr.append(str_f) dataset.append(lineArr) return dataset

分析數據:手工檢查數據

訓練算法:在數據上,利用 random_forest() 函數進行優化評估,返回模型的綜合分類結果

  • 樣本數據隨機無放回抽樣-用於交叉驗證
def cross_validation_split(dataset, n_folds): """cross_validation_split(將數據集進行抽重抽樣 n_folds 份,數據可以重復重復抽取)   Args:  dataset 原始數據集  n_folds 數據集dataset分成n_flods份  Returns:  dataset_split list集合,存放的是:將數據集進行抽重抽樣 n_folds 份,數據可以重復重復抽取  """ dataset_split = list() dataset_copy = list(dataset) # 復制一份 dataset,防止 dataset 的內容改變 fold_size = len(dataset) / n_folds for i in range(n_folds): fold = list() # 每次循環 fold 清零,防止重復導入 dataset_split while len(fold) < fold_size: # 這里不能用 if,if 只是在第一次判斷時起作用,while 執行循環,直到條件不成立 # 有放回的隨機采樣,有一些樣本被重復采樣,從而在訓練集中多次出現,有的則從未在訓練集中出現,此則自助采樣法。從而保證每棵決策樹訓練集的差異性 index = randrange(len(dataset_copy)) # 將對應索引 index 的內容從 dataset_copy 中導出,並將該內容從 dataset_copy 中刪除。 # pop() 函數用於移除列表中的一個元素(默認最后一個元素),並且返回該元素的值。 fold.append(dataset_copy.pop(index)) # 無放回的方式 # fold.append(dataset_copy[index]) # 有放回的方式 dataset_split.append(fold) # 由dataset分割出的n_folds個數據構成的列表,為了用於交叉驗證 return dataset_split
  • 訓練數據集隨機化
# Create a random subsample from the dataset with replacement def subsample(dataset, ratio): # 創建數據集的隨機子樣本 """random_forest(評估算法性能,返回模型得分)   Args:  dataset 訓練數據集  ratio 訓練數據集的樣本比例  Returns:  sample 隨機抽樣的訓練樣本  """ sample = list() # 訓練樣本的按比例抽樣。 # round() 方法返回浮點數x的四舍五入值。 n_sample = round(len(dataset) * ratio) while len(sample) < n_sample: # 有放回的隨機采樣,有一些樣本被重復采樣,從而在訓練集中多次出現,有的則從未在訓練集中出現,此則自助采樣法。從而保證每棵決策樹訓練集的差異性 index = randrange(len(dataset)) sample.append(dataset[index]) return sample
  • 特征隨機化
# 找出分割數據集的最優特征,得到最優的特征 index,特征值 row[index],以及分割完的數據 groups(left, right) def get_split(dataset, n_features): class_values = list(set(row[-1] for row in dataset)) # class_values =[0, 1] b_index, b_value, b_score, b_groups = 999, 999, 999, None features = list() while len(features) < n_features: index = randrange(len(dataset[0])-1) # 往 features 添加 n_features 個特征( n_feature 等於特征數的根號),特征索引從 dataset 中隨機取 if index not in features: features.append(index) for index in features: # 在 n_features 個特征中選出最優的特征索引,並沒有遍歷所有特征,從而保證了每課決策樹的差異性 for row in dataset: groups = test_split(index, row[index], dataset) # groups=(left, right), row[index] 遍歷每一行 index 索引下的特征值作為分類值 value, 找出最優的分類特征和特征值 gini = gini_index(groups, class_values) # 左右兩邊的數量越一樣,說明數據區分度不高,gini系數越大 if gini < b_score: b_index, b_value, b_score, b_groups = index, row[index], gini, groups # 最后得到最優的分類特征 b_index,分類特征值 b_value,分類結果 b_groups。b_value 為分錯的代價成本 # print b_score return {'index': b_index, 'value': b_value, 'groups': b_groups}
  • 隨機森林
# Random Forest Algorithm def random_forest(train, test, max_depth, min_size, sample_size, n_trees, n_features): """random_forest(評估算法性能,返回模型得分)   Args:  train 訓練數據集  test 測試數據集  max_depth 決策樹深度不能太深,不然容易導致過擬合  min_size 葉子節點的大小  sample_size 訓練數據集的樣本比例  n_trees 決策樹的個數  n_features 選取的特征的個數  Returns:  predictions 每一行的預測結果,bagging 預測最后的分類結果  """ trees = list() # n_trees 表示決策樹的數量 for i in range(n_trees): # 隨機抽樣的訓練樣本, 隨機采樣保證了每棵決策樹訓練集的差異性 sample = subsample(train, sample_size) # 創建一個決策樹 tree = build_tree(sample, max_depth, min_size, n_features) trees.append(tree) # 每一行的預測結果,bagging 預測最后的分類結果 predictions = [bagging_predict(trees, row) for row in test] return predictions

測試算法:在采用自定義 n_folds 份隨機重抽樣 進行測試評估,得出綜合的預測評分。

  • 計算隨機森林的預測結果的正確率
# 評估算法性能,返回模型得分 def evaluate_algorithm(dataset, algorithm, n_folds, *args): """evaluate_algorithm(評估算法性能,返回模型得分)   Args:  dataset 原始數據集  algorithm 使用的算法  n_folds 數據的份數  *args 其他的參數  Returns:  scores 模型得分  """ # 將數據集進行隨機抽樣,分成 n_folds 份,數據無重復的抽取 folds = cross_validation_split(dataset, n_folds) scores = list() # 每次循環從 folds 從取出一個 fold 作為測試集,其余作為訓練集,遍歷整個 folds ,實現交叉驗證 for fold in folds: train_set = list(folds) train_set.remove(fold) # 將多個 fold 列表組合成一個 train_set 列表, 類似 union all """  In [20]: l1=[[1, 2, 'a'], [11, 22, 'b']]  In [21]: l2=[[3, 4, 'c'], [33, 44, 'd']]  In [22]: l=[]  In [23]: l.append(l1)  In [24]: l.append(l2)  In [25]: l  Out[25]: [[[1, 2, 'a'], [11, 22, 'b']], [[3, 4, 'c'], [33, 44, 'd']]]  In [26]: sum(l, [])  Out[26]: [[1, 2, 'a'], [11, 22, 'b'], [3, 4, 'c'], [33, 44, 'd']]  """ train_set = sum(train_set, []) test_set = list() # fold 表示從原始數據集 dataset 提取出來的測試集 for row in fold: row_copy = list(row) row_copy[-1] = None test_set.append(row_copy) predicted = algorithm(train_set, test_set, *args) actual = [row[-1] for row in fold] # 計算隨機森林的預測結果的正確率 accuracy = accuracy_metric(actual, predicted) scores.append(accuracy) return scores

使用算法:若你感興趣可以構建完整的應用程序,從案例進行封裝,也可以參考我們的代碼

完整代碼地址https://github.com/apachecn/MachineLearning/blob/master/src/python/7.RandomForest/randomForest.py

AdaBoost

AdaBoost (adaptive boosting: 自適應 boosting) 概述

能否使用弱分類器和多個實例來構建一個強分類器? 這是一個非常有趣的理論問題。

AdaBoost 原理

AdaBoost 工作原理

AdaBoost 工作原理

AdaBoost 開發流程

收集數據:可以使用任意方法
准備數據:依賴於所使用的弱分類器類型,本章使用的是單層決策樹,這種分類器可以處理任何數據類型。
    當然也可以使用任意分類器作為弱分類器,第2章到第6章中的任一分類器都可以充當弱分類器。
    作為弱分類器,簡單分類器的效果更好。
分析數據:可以使用任意方法。
訓練算法:AdaBoost 的大部分時間都用在訓練上,分類器將多次在同一數據集上訓練弱分類器。
測試算法:計算分類的錯誤率。
使用算法:通SVM一樣,AdaBoost 預測兩個類別中的一個。如果想把它應用到多個類別的場景,那么就要像多類 SVM 中的做法一樣對 AdaBoost 進行修改。

AdaBoost 算法特點

* 優點:泛化(由具體的、個別的擴大為一般的)錯誤率低,易編碼,可以應用在大部分分類器上,無參數調節。
* 缺點:對離群點敏感。
* 適用數據類型:數值型和標稱型數據。

項目案例: 馬疝病的預測

項目流程圖

AdaBoost代碼流程圖

基於單層決策樹構建弱分類器

  • 單層決策樹(decision stump, 也稱決策樹樁)是一種簡單的決策樹。

項目概述

預測患有疝氣病的馬的存活問題,這里的數據包括368個樣本和28個特征,疝氣病是描述馬胃腸痛的術語,然而,這種病並不一定源自馬的胃腸問題,其他問題也可能引發疝氣病,該數據集中包含了醫院檢測馬疝氣病的一些指標,有的指標比較主觀,有的指標難以測量,例如馬的疼痛級別。另外,除了部分指標主觀和難以測量之外,該數據還存在一個問題,數據集中有30%的值是缺失的。

開發流程

收集數據:提供的文本文件
准備數據:確保類別標簽是+1和-1,而非1和0
分析數據:統計分析
訓練算法:在數據上,利用 adaBoostTrainDS() 函數訓練出一系列的分類器
測試算法:我們擁有兩個數據集。在不采用隨機抽樣的方法下,我們就會對 AdaBoost 和 Logistic 回歸的結果進行完全對等的比較
使用算法:觀察該例子上的錯誤率。不過,也可以構建一個 Web 網站,讓馴馬師輸入馬的症狀然后預測馬是否會死去

收集數據:提供的文本文件

訓練數據:horseColicTraining.txt
測試數據:horseColicTest.txt

2.000000	1.000000	38.500000	66.000000	28.000000	3.000000	3.000000	0.000000	2.000000	5.000000	4.000000	4.000000	0.000000	0.000000	0.000000	3.000000	5.000000	45.000000	8.400000	0.000000	0.000000	-1.000000
1.000000	1.000000	39.200000	88.000000	20.000000	0.000000	0.000000	4.000000	1.000000	3.000000	4.000000	2.000000	0.000000	0.000000	0.000000	4.000000	2.000000	50.000000	85.000000	2.000000	2.000000	-1.000000
2.000000	1.000000	38.300000	40.000000	24.000000	1.000000	1.000000	3.000000	1.000000	3.000000	3.000000	1.000000	0.000000	0.000000	0.000000	1.000000	1.000000	33.000000	6.700000	0.000000	0.000000	1.000000

准備數據:確保類別標簽是+1和-1,而非1和0

def loadDataSet(fileName): # 獲取 feature 的數量, 便於獲取 numFeat = len(open(fileName).readline().split('\t')) dataArr = [] labelArr = [] fr = open(fileName) for line in fr.readlines(): lineArr = [] curLine = line.strip().split('\t') for i in range(numFeat-1): lineArr.append(float(curLine[i])) dataArr.append(lineArr) labelArr.append(float(curLine[-1])) return dataArr, labelArr

分析數據:統計分析

過擬合(overfitting, 也稱為過學習)

  • 發現測試錯誤率在達到一個最小值之后有開始上升,這種現象稱為過擬合。

過擬合

  • 通俗來說:就是把一些噪音數據也擬合進去的,如下圖。

過擬合

訓練算法:在數據上,利用 adaBoostTrainDS() 函數訓練出一系列的分類器

def adaBoostTrainDS(dataArr, labelArr, numIt=40): """adaBoostTrainDS(adaBoost訓練過程放大)  Args:  dataArr 特征標簽集合  labelArr 分類標簽集合  numIt 實例數  Returns:  weakClassArr 弱分類器的集合  aggClassEst 預測的分類結果值  """ weakClassArr = [] m = shape(dataArr)[0] # 初始化 D,設置每個樣本的權重值,平均分為m份 D = mat(ones((m, 1))/m) aggClassEst = mat(zeros((m, 1))) for i in range(numIt): # 得到決策樹的模型 bestStump, error, classEst = buildStump(dataArr, labelArr, D) # alpha目的主要是計算每一個分類器實例的權重(組合就是分類結果) # 計算每個分類器的alpha權重值 alpha = float(0.5*log((1.0-error)/max(error, 1e-16))) bestStump['alpha'] = alpha # store Stump Params in Array weakClassArr.append(bestStump) print "alpha=%s, classEst=%s, bestStump=%s, error=%s " % (alpha, classEst.T, bestStump, error) # 分類正確:乘積為1,不會影響結果,-1主要是下面求e的-alpha次方 # 分類錯誤:乘積為 -1,結果會受影響,所以也乘以 -1 expon = multiply(-1*alpha*mat(labelArr).T, classEst) print '(-1取反)預測值expon=', expon.T # 計算e的expon次方,然后計算得到一個綜合的概率的值 # 結果發現: 判斷錯誤的樣本,D對於的樣本權重值會變大。 D = multiply(D, exp(expon)) D = D/D.sum() # 預測的分類結果值,在上一輪結果的基礎上,進行加和操作 print '當前的分類結果:', alpha*classEst.T aggClassEst += alpha*classEst print "疊加后的分類結果aggClassEst: ", aggClassEst.T # sign 判斷正為1, 0為0, 負為-1,通過最終加和的權重值,判斷符號。 # 結果為:錯誤的樣本標簽集合,因為是 !=,那么結果就是0 正, 1 負 aggErrors = multiply(sign(aggClassEst) != mat(labelArr).T, ones((m, 1))) errorRate = aggErrors.sum()/m # print "total error=%s " % (errorRate) if errorRate == 0.0: break return weakClassArr, aggClassEst
發現:
alpha (模型權重)目的主要是計算每一個分類器實例的權重(加和就是分類結果)
  分類的權重值:最大的值= alpha 的加和,最小值=-最大值
D (樣本權重)的目的是為了計算錯誤概率: weightedError = D.T*errArr,求最佳分類器
  樣本的權重值:如果一個值誤判的幾率越小,那么 D 的樣本權重越小

AdaBoost算法權重計算公式

測試算法:我們擁有兩個數據集。在不采用隨機抽樣的方法下,我們就會對 AdaBoost 和 Logistic 回歸的結果進行完全對等的比較。

def adaClassify(datToClass, classifierArr): """adaClassify(ada分類測試)  Args:  datToClass 多個待分類的樣例  classifierArr 弱分類器的集合  Returns:  sign(aggClassEst) 分類結果  """ # do stuff similar to last aggClassEst in adaBoostTrainDS dataMat = mat(datToClass) m = shape(dataMat)[0] aggClassEst = mat(zeros((m, 1))) # 循環 多個分類器 for i in range(len(classifierArr)): # 前提: 我們已經知道了最佳的分類器的實例 # 通過分類器來核算每一次的分類結果,然后通過alpha*每一次的結果 得到最后的權重加和的值。 classEst = stumpClassify(dataMat, classifierArr[i]['dim'], classifierArr[i]['thresh'], classifierArr[i]['ineq']) aggClassEst += classifierArr[i]['alpha']*classEst return sign(aggClassEst)

使用算法:觀察該例子上的錯誤率。不過,也可以構建一個 Web 網站,讓馴馬師輸入馬的症狀然后預測馬是否會死去。

# 馬疝病數據集 # 訓練集合 dataArr, labelArr = loadDataSet("input/7.AdaBoost/horseColicTraining2.txt") weakClassArr, aggClassEst = adaBoostTrainDS(dataArr, labelArr, 40) print weakClassArr, '\n-----\n', aggClassEst.T # 計算ROC下面的AUC的面積大小 plotROC(aggClassEst.T, labelArr) # 測試集合 dataArrTest, labelArrTest = loadDataSet("input/7.AdaBoost/horseColicTest2.txt") m = shape(dataArrTest)[0] predicting10 = adaClassify(dataArrTest, weakClassArr) errArr = mat(ones((m, 1))) # 測試:計算總樣本數,錯誤樣本數,錯誤率 print m, errArr[predicting10 != mat(labelArrTest).T].sum(), errArr[predicting10 != mat(labelArrTest).T].sum()/m

完整代碼地址https://github.com/apachecn/MachineLearning/blob/master/src/python/7.AdaBoost/adaboost.py

要點補充

非均衡現象:

在分類器訓練時,正例數目和反例數目不相等(相差很大)

  • 判斷馬是否能繼續生存(不可誤殺)
  • 過濾垃圾郵件(不可漏判)
  • 不能放過傳染病的人
  • 不能隨便認為別人犯罪

ROC 評估方法

  • ROC 曲線: 最佳的分類器應該盡可能地處於左上角

ROC曲線

  • 對不同的 ROC 曲線進行比較的一個指標是曲線下的面積(Area Unser the Curve, AUC).
  • AUC 給出的是分類器的平均性能值,當然它並不能完全代替對整條曲線的觀察。
  • 一個完美分類器的 AUC 為1,而隨機猜測的 AUC 則為0.5。

代價函數

  • 基於代價函數的分類器決策控制:TP*(-5)+FN*1+FP*50+TN*0

代價函數

抽樣

  • 欠抽樣(undersampling)或者過抽樣(oversampling)
    • 欠抽樣: 意味着刪除樣例
    • 過抽樣: 意味着復制樣例(重復使用)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM