基于樹模型的集成算法 ---XGBoost

一、模型介紹

XGBoost 是 boosting 算法的其中一種。Boosting 算法的思想是將許多弱分類器集成在一起形成一個強分類器。因為 XGBoost 是一種提升樹模型,所以它是將許多樹模型集成在一起,形成一個很強的分類器。而所用到的樹模型則是 CART 回歸樹模型。

二、模型原理

1. 算法思想

該算法思想就是不斷地添加樹,不斷地進行特征分裂來生長一棵樹,每次添加一個樹,其實是學(xué)習(xí)一個新函數(shù),去擬合上次預(yù)測的殘差。當(dāng)我們訓(xùn)練完成得到 k 棵樹,我們要預(yù)測一個樣本的分?jǐn)?shù),其實就是根據(jù)這個樣本的特征,在每棵樹中會落到對應(yīng)的一個葉子節(jié)點,每個葉子節(jié)點就對應(yīng)一個分?jǐn)?shù),最后只需要將每棵樹對應(yīng)的分?jǐn)?shù)加起來就是該樣本的預(yù)測值。

則對于每個預(yù)測值可以表示為:
\hat{y}_{i} = \sum_{k=1}^{K}f_{k}(x_{i}),\ f_{k}\ \epsilon\ F
其中 F 對應(yīng)了所有回歸樹的集合。

2. 目標(biāo)函數(shù)

假設(shè)損失函數(shù)為 L,正則項為 \Omega,則有目標(biāo)函數(shù)形如: Obj = L + \Omega。
Obj = \sum_{i=1}^{n}l(y_{i},\hat{y}_{i}) + \sum_{k=1}^{K}\Omega(f_{k})
其中第一部分是訓(xùn)練誤差,也就是大家相對比較熟悉的如平方誤差, logistic loss 等。而第二部分是每棵樹的復(fù)雜度的和。

因為現(xiàn)在我們的參數(shù)可以認(rèn)為是在一個函數(shù)空間里面,我們不能采用傳統(tǒng)的如 SGD 之類的算法來學(xué)習(xí)我們的模型,因此我們會采用一種叫做 additive training的方式。每一次保留原來的模型不變,加入一個新的函數(shù) f 到我們的模型中。如下圖所示:

image.png

3. 損失函數(shù)

接下來我們需要思考的是,我們?nèi)绾芜x擇每一輪的加入什么 f ? 很顯然,我們希望加入的 f 使我們的目標(biāo)函數(shù)盡可能的降低。對于第 t 步:
Obj^{(t)} = \sum_{i=1}^{n}l(y_{i}, \hat{y_{i}}^{t}) + \sum_{i=1}^{t}\Omega{(f_{i})} Obj^{(t)} = \sum_{i=1}^{n}l(y_{i}, \hat{y_{i}}^{t-1} + f_{t}(x_{i})) + \Omega{(f_{t})}+\sum_{i=1}^{t-1}\Omega{(f_{i})}
對于第 t 步來說,其中 \sum_{i=1}^{t-1}\Omega{(f_{i})} = constant。我們需要找到 f_{t} 優(yōu)化上式的前部分:\sum_{i=1}^{n}l(y_{i}, \hat{y_{i}}^{t-1} + f_{t}(x_{i})) + \Omega{(f_{t})}
我們考慮損失函數(shù)為平方誤差,則上式有:
Obj^{(t)} = \sum_{i=1}^{n}[y_{i}-(\hat{y_{i}}^{t-1}+f_{t}(x_{i}))]^{2} + \Omega{(f_{t})}+constant Obj^{(t)} = \sum_{i=1}^{n}[(y_{i})^2+(\hat{y_{i}}^{t-1})^2+(f_{t}(x_{i}))^2- 2*y_{i}*\hat{y_{i}}^{t-1} - 2*y_{i}*f_{t}(x_{i}) - 2*\hat{y_{i}}^{t-1}*f_{t}(x_{i})] + \Omega{(f_{t})}+constant
針對變量 f_{t} 而言,y_{i}\hat{y_{i}}^{t-1} 均為常量,則最后有:
Obj^{(t)} = \sum_{i=1}^{n}[2(\hat{y_{i}}^{t-1}-y_{i})f_{t}(x_{i})+(f_{t}(x_{i}))^2 ] + \Omega{(f_{t})}+constant
其中 (\hat{y_{i}}^{t-1}-y_{i}) 一般叫做殘差。
當(dāng)然,更加一般的話,例如損失函數(shù)不是平方誤差的時候。再回過頭來看我們的損失函數(shù):
L(\hat{y_{i}}^{t-1}) = l(y_{i}, \hat{y_{i}}^{t-1}) L(\hat{y_{i}}^{t-1}+f_{t}(x_{i})) = l(y_{i}, \hat{y_{i}}^{t-1}+f_{t}(x_{i}))
形如 f(x+\Delta{x}),我們可以采用泰勒展開近似來定義一個近似的目標(biāo)函數(shù),方便我們這一步的計算,同時更加的一般化。
則有:
f(x+\Delta{x}) \simeq f(x) + f'(x)\Delta{x} + \frac{1}{2}f''(x){\Delta{x}}^{2}
為了方便理解,這里定義:g_{i} 為一階導(dǎo)數(shù),h_{i} 為二階導(dǎo)數(shù)。
g_{i} = \partial_{\hat{y_i}^{t-1}} l(y_{i},\hat{y_i}^{t-1}) h_{i} = \partial_{\hat{y_i}^{t-1}}^{2} l(y_{i},\hat{y_i}^{t-1})
目標(biāo)函數(shù)可以寫作:
Obj^{(t)} \simeq \sum_{i=1}^{n}[l(y_{i}, \hat{y_{i}}^{t-1}) + g_{i}f_{t}(x_{i})+\frac{1}{2}h_{i}{f_{t}}^{2}(x_{i}) ] + \Omega{(f_{t})}+constant
至此,我們得到一個非常漂亮的式子。當(dāng)我們把常數(shù)項移除之后,這一個目標(biāo)函數(shù)有一個非常明顯的特點,它只依賴于每個數(shù)據(jù)點的在誤差函數(shù)上的一階導(dǎo)數(shù)和二階導(dǎo)數(shù)。
Obj^{(t)} \simeq\sum_{i=1}^{n}[g_{i}f_{t}(x_{i})+\frac{1}{2}h_{i}{f_{t}}^{2}(x_{i}) ] + \Omega{(f_{t})} + constant
這樣我們可以很清楚地理解整個目標(biāo)是什么,并且一步一步推導(dǎo)出如何進行樹的學(xué)習(xí)。這一個抽象的形式對于實現(xiàn)機器學(xué)習(xí)工具也是非常有幫助的。傳統(tǒng)的 GBDT 可能大家可以理解如優(yōu)化殘差,但是這樣一個形式包含可所有可以求導(dǎo)的目標(biāo)函數(shù)。也就是說有了這個形式,我們寫出來的代碼可以用來求解包括回歸,分類和排序的各種問題,正式的推導(dǎo)可以使得機器學(xué)習(xí)的工具更加一般。

4. 樹結(jié)構(gòu)以及復(fù)雜度

目前為止我們討論了目標(biāo)函數(shù)中訓(xùn)練誤差的部分。接下來我們討論如何定義樹的復(fù)雜度。
這里我們把樹拆分成結(jié)構(gòu)部分 q 和葉子權(quán)重部分 w,結(jié)構(gòu)函數(shù) q 把輸入映射到葉子的索引號上面去, 而w 給定了每個索引號對應(yīng)的葉子分?jǐn)?shù)是什么。
形如 f_{t}(x) = w_{q(x)},如下圖:

image.png

當(dāng)給定了如上定義之后,我們可以定義一棵樹的復(fù)雜度如下。
這個復(fù)雜度包含了一棵樹里面節(jié)點的個數(shù),以及每個樹葉子節(jié)點上面輸出分?jǐn)?shù)的 L_{2} 模平方。當(dāng)然這不是唯一的一種定義方式,不過這一定義方式學(xué)習(xí)出的樹效果一般都比較不錯。下圖給出了復(fù)雜度計算的一個例子:(其中 \gamma\lambda 為正則化系數(shù))
image.png

5. 目標(biāo)函數(shù)優(yōu)化求解

接下來,再增加一個定義,假設(shè)一共有 T 個葉子,用 I_{j} 來表示落在第j個葉子節(jié)點上的樣本集合。用公式表示為 I_{j} = \{i|q(x_{i})=j\}。結(jié)合這一點,舍棄掉常數(shù)項,我們再把上面的目標(biāo)函數(shù)改寫:
Obj^{(t)} \simeq\sum_{i=1}^{n}[g_{i}f_{t}(x_{i})+\frac{1}{2}h_{i}{f_{t}}^{2}(x_{i}) ] + \Omega{(f_{t})} =\sum_{i=1}^{n}[g_{i}w_{q(x_{i})}+\frac{1}{2}h_{i}{w}^{2}_{q(x_{i})}] + \gamma T+\frac{1}{2}\lambda\sum_{j=1}^{T}{w}^{2}_{j} =\sum_{j=1}^{T}[(\sum_{i\in I_{j}}g_{i})w_{j}+\frac{1}{2}(\sum_{i\in I_{j}}h_{i}){w}^{2}_{j}] + \gamma T+\frac{1}{2}\lambda\sum_{j=1}^{T}{w}^{2}_{j} =\sum_{j=1}^{T}[(\sum_{i\in I_{j}}g_{i})w_{j}+\frac{1}{2}(\sum_{i\in I_{j}}h_{i} + \lambda){w}^{2}_{j}] + \gamma T
通過上式我們可以看到這是一個關(guān)于 w 的二次函數(shù),為了方便表示,令 G_{j} = \sum_{i\in I_{j}}g_{i}H_{j} = \sum_{i\in I_{j}}h_{i}。則上式可以寫作:
Obj^{(t)} \simeq \sum_{j=1}^{T}[G_{j}w_{j} + \frac{1}{2}(H_{j}+\lambda){w}^{2}_{j}] + \gamma T
根據(jù)一元二次函數(shù)的最優(yōu)化問題,形如 ax^{2}+bx的極值點為 x=-\frac{2a}?;氐缴鲜絼t有:
w_{j}^{*} = -\frac{G_{j}}{H_{j}+\lambda} Obj = -\frac{1}{2}\sum_{j=1}^{T}(\frac{G^{2}_{j}}{H_{j}+\lambda}) + \gamma T
其中 w_{j}^{*}表示的式最好的 wObj 代表了當(dāng)我們指定一個樹的結(jié)構(gòu)的時候,我們在目標(biāo)上面最多減少多少。我們把它叫做結(jié)構(gòu)分?jǐn)?shù) (structure score) 。你可以認(rèn)為這個就是類似吉尼系數(shù)一樣更加一般的對于樹結(jié)構(gòu)進行打分的函數(shù)。

所以我們的算法也很簡單,我們不斷地枚舉不同樹的結(jié)構(gòu),利用這個打分函數(shù)來尋找出一個最優(yōu)結(jié)構(gòu)的樹,加入到我們的模型中,再重復(fù)這樣的操作。
很顯然,枚舉所有樹結(jié)構(gòu)這個操作不太可行,所以常用的方法是貪心算法,每一次嘗試去對已有的葉子加入一個分割。對于一個具體的分割方案,假設(shè)當(dāng)前節(jié)點左右子樹的一階導(dǎo)數(shù)和二階導(dǎo)數(shù)的和為 G_{L} G_{R} H_{L} H_{R};

image.png

我們可以獲得的增益可以由如下公式計算:
Obj_{nosplit} = -\frac{1}{2}\frac{(G_{L} + G_{R})^{2}}{H_{L}+H_{R}+\lambda} + \gamma T Obj_{split} = -\frac{1}{2}[\frac{{G}^{2}_{L}}{H_{L}+\lambda} + \frac{{G}^{2}_{R}}{H_{R}+\lambda}] + \gamma (T+1) Gain = Ojb_{nosplit} - Obj_{split} Gain = -\frac{1}{2}\frac{(G_{L} + G_{R})^{2}}{H_{L}+H_{R}+\lambda} + \gamma T +\frac{1}{2}\frac{{G}^{2}_{L}}{H_{L}+\lambda} + \frac{1}{2} \frac{{G}^{2}_{R}}{H_{R}+\lambda} - \gamma (T+1) Gain = \frac{1}{2}[\frac{{G}^{2}_{L}}{H_{L}+\lambda} + \frac{{G}^{2}_{R}}{H_{R}+\lambda} - \frac{(G_{L} + G_{R})^{2}}{H_{L}+H_{R}+\lambda} ] - \gamma
這里我們的決策樹分裂標(biāo)準(zhǔn)不再使用 CART 回歸樹的均方誤差,最大化增益 Gain 就是我們的最終想要的結(jié)果。
關(guān)于對葉子節(jié)點的分割,其實這里有兩種后續(xù)的分割思路:

  • 第一種,最好的分割情況下 Gain為負(fù)時就停止樹的生長,這樣的話效率會比較高也簡單,但是這樣也就放棄了未來更好的可能性。
  • 第二種,分割到指定的最大深度,遞歸的把劃分葉子節(jié)點得到 Gain 為負(fù)的收回。
    一般情況,第二種都要更好一些,當(dāng)然我們這里使用的也是第二種。

很有趣的一點,回顧以上過程。當(dāng)我們正式地推導(dǎo)目標(biāo)的時候,像計算分?jǐn)?shù)和剪枝這樣的策略都會自然地出現(xiàn)。而且有這樣一般的推導(dǎo),得到的算法可以直接應(yīng)用到回歸,分類排序等各個應(yīng)用場景中去。

最后我們總結(jié)下 XGBoost 的算法主流程,基于決策樹弱分類器。(不涉及運行效率的優(yōu)化和健壯性優(yōu)化的內(nèi)容)
  • 輸入是訓(xùn)練集樣本I=\{(x_{1},y_{1}),(x_{2},y_{2})……(x_{m},y_{m})\},特征數(shù)目為K,最大迭代次數(shù) T, 損失函數(shù) L, 正則化系數(shù)\lambda,\gamma
  • 輸出是強學(xué)習(xí)器 f(x)
  • 對于第t (t=1,2,3……T)輪迭代:
    • (1) 計算第i 個樣本在當(dāng)前損失函數(shù)L 基于f_{t-1}(x)的一階導(dǎo)數(shù)g^{t}_{i},二階導(dǎo)數(shù)h^{t}_{i}。所有樣本的一階導(dǎo)數(shù)和G_{t} = \sum_{i=1}^{m}g^{t}_{i},二階導(dǎo)數(shù)和H_{t} = \sum_{i=1}^{m}h^{t}_{i}。
    • (2) 基于當(dāng)前節(jié)點嘗試分裂決策樹,默認(rèn)分?jǐn)?shù) score=0,GH 為當(dāng)前需要分裂節(jié)點的一階二階導(dǎo)數(shù)之和。樣本特征k(k = 1,2,3……K):
      • (a) G_{L} = 0,H_{L} = 0
      • (b) 將樣本按特征 k 從小到大排列,依次取出第 i 個樣本,計算當(dāng)前i 個樣本放入左子樹后,左右子樹一階和二階導(dǎo)數(shù)和:
        G_{L} = G_{L} + g^{t}_{i},G_{R} = G - G_{L} H_{L} = H_{L} + h^{t}_{i},H_{R} = H - H_{L}
      • (c) 嘗試更新最大分?jǐn)?shù):
        score = max(score, \frac{1}{2}[\frac{{G}^{2}_{L}}{H_{L}+\lambda} + \frac{{G}^{2}_{R}}{H_{R}+\lambda} - \frac{(G_{L} + G_{R})^{2}}{H_{L}+H_{R}+\lambda} ] - \gamma)
    • (3) 基于最大 score 對應(yīng)的劃分特征和特征值分裂子樹。
    • (4) 如果最大 score 為 0,則當(dāng)前決策樹建立完畢,計算所有葉子區(qū)域的??^{t}_{??}, 得到弱學(xué)習(xí)器 ?^{t}(??),更新強學(xué)習(xí)器??_{t}(??),進入下一輪弱學(xué)習(xí)器迭代。如果最大 score 不是 0,則轉(zhuǎn)到第 (2) 步繼續(xù)嘗試分裂決策樹。

三、模型細(xì)節(jié)

XGBoost 算法運行效率的優(yōu)化

我們知道 Boosting 算法的弱學(xué)習(xí)器是沒法并行迭代的,但是單個弱學(xué)習(xí)器里面最耗時的是決策樹的分裂過程,XGBoost 針對這個分裂做了比較大的并行優(yōu)化。對于不同的特征的特征劃分點,XGBoost 分別在不同的線程中并行選擇分裂的最大增益。

同時,對訓(xùn)練的每個特征排序并且以塊的的結(jié)構(gòu)存儲在內(nèi)存中,方便后面迭代重復(fù)使用,減少計算量。計算量的減少參見上面的算法流程,首先默認(rèn)所有的樣本都在右子樹,然后從小到大迭代,依次放入左子樹,并尋找最優(yōu)的分裂點。這樣做可以減少很多不必要的比較。

此外,通過設(shè)置合理的分塊的大小,充分利用了 CPU 緩存進行讀取加速(cache-aware access)。使得數(shù)據(jù)讀取的速度更快。另外,通過將分塊進行壓縮(block compressoin)并存儲到硬盤上,并且通過將分塊分區(qū)到多個硬盤上實現(xiàn)了更大的 IO。

XGBoost算法健壯性的優(yōu)化

我們再來看看 XGBoost 在算法健壯性的優(yōu)化,除了上面講到的正則化項提高算法的泛化能力外,XGBoost 還對特征的缺失值做了處理。

XGBoost 沒有假設(shè)缺失值一定進入左子樹還是右子樹,則是嘗試通過枚舉所有缺失值在當(dāng)前節(jié)點是進入左子樹,還是進入右子樹更優(yōu)來決定一個處理缺失值默認(rèn)的方向,這樣處理起來更加的靈活和合理。

也就是說,上面算法的步驟 (a),(b),(c) 會執(zhí)行 2 次,第一次假設(shè)特征 k 所有有缺失值的樣本都走左子樹,第二次假設(shè)特征 k 所有缺失值的樣本都走右子樹。然后每次都是針對沒有缺失值的特征 k 的樣本走上述流程,而不是所有的的樣本。

XGBoost 與 GBDT 算法比較的優(yōu)勢

作為 GBDT 的高效實現(xiàn),XGBoost 是一個上限特別高的算法,因此在算法競賽中比較受歡迎。簡單來說,對比原算法 GBDT,XGBoos t主要從下面三個方面做了優(yōu)化:

  • 一是算法本身的優(yōu)化:在算法的弱學(xué)習(xí)器模型選擇上,對比 GBDT 只支持決策樹,還可以直接很多其他的弱學(xué)習(xí)器。在算法的損失函數(shù)上,除了本身的損失,還加上了正則化部分。在算法的優(yōu)化方式上,GBDT 的損失函數(shù)只對誤差部分做負(fù)梯度(一階泰勒)展開,而 XGBoost 損失函數(shù)對誤差部分做二階泰勒展開,更加準(zhǔn)確。
  • 二是算法運行效率的優(yōu)化:對每個弱學(xué)習(xí)器,比如決策樹建立的過程做并行選擇,找到合適的子樹分裂特征和特征值。在并行選擇之前,先對所有的特征的值進行排序分組,方便前面說的并行選擇。對分組的特征,選擇合適的分組大小,使用 CPU 緩存進行讀取加速。將各個分組保存到多個硬盤以提高 IO 速度。
  • 三是算法健壯性的優(yōu)化:對于缺失值的特征,通過枚舉所有缺失值在當(dāng)前節(jié)點是進入左子樹還是右子樹來決定缺失值的處理方式。算法本身加入了 L1 和 L2 正則化項,可以防止過擬合,泛化能力更強。

四、模型優(yōu)缺點

優(yōu)點:

    1. 精度更高:GBDT 只用到一階泰勒展開,而 XGBoost 對損失函數(shù)進行了二階泰勒展開。XGBoost 引入二階導(dǎo)一方面是為了增加精度,另一方面也是為了能夠自定義損失函數(shù),二階泰勒展開可以近似大量損失函數(shù);
    1. 靈活性更強:GBDT 以 CART 作為基分類器,XGBoost 不僅支持 CART 還支持線性分類器,使用線性分類器的 XGBoost 相當(dāng)于帶 和 正則化項的邏輯斯蒂回歸(分類問題)或者線性回歸(回歸問題)。此外,XGBoost 工具支持自定義損失函數(shù),只需函數(shù)支持一階和二階求導(dǎo);
    1. 正則化:XGBoost 在目標(biāo)函數(shù)中加入了正則項,用于控制模型的復(fù)雜度。正則項里包含了樹的葉子節(jié)點個數(shù)、葉子節(jié)點權(quán)重的 范式。正則項降低了模型的方差,使學(xué)習(xí)出來的模型更加簡單,有助于防止過擬合,這也是 XGBoost 優(yōu)于傳統(tǒng) GBDT 的一個特性。
    1. Shrinkage(縮減):相當(dāng)于學(xué)習(xí)速率。XGBoost 在進行完一次迭代后,會將葉子節(jié)點的權(quán)重乘上該系數(shù),主要是為了削弱每棵樹的影響,讓后面有更大的學(xué)習(xí)空間。傳統(tǒng) GBDT 的實現(xiàn)也有學(xué)習(xí)速率;
    1. 缺失值處理:對于特征的值有缺失的樣本,XGBoost 采用的稀疏感知算法可以自動學(xué)習(xí)出它的分裂方向;
    1. 列抽樣:XGBoost 借鑒了隨機森林的做法,支持列抽樣,不僅能降低過擬合,還能減少計算。這也是 XGBoost 異于傳統(tǒng) GBDT 的一個特性;
    1. 支持并行:boosting不是一種串行的結(jié)構(gòu)嗎? 怎么并行的?注意 XGBoost 的并行不是 tree 粒度的并行,XGBoost 也是一次迭代完才能進行下一次迭代的(第次迭代的代價函數(shù)里包含了前面次迭代的預(yù)測值)。XGBoost 的并行是在特征粒度上的。我們知道,決策樹的學(xué)習(xí)最耗時的一個步驟就是對特征的值進行排序(因為要確定最佳分割點),XGBoost 在訓(xùn)練之前,預(yù)先對數(shù)據(jù)進行了排序,然后保存為 block 結(jié)構(gòu),后面的迭代中重復(fù)地使用這個結(jié)構(gòu),大大減小計算量。這個 block 結(jié)構(gòu)也使得并行成為了可能,在進行節(jié)點的分裂時,需要計算每個特征的增益,最終選增益最大的那個特征去做分裂,那幺各個特征的增益計算就可以開多線程進行。
    1. 可并行的近似直方圖算法。樹節(jié)點在進行分裂時,我們需要計算每個特征的每個分割點對應(yīng)的增益,即用貪心法枚舉所有可能的分割點。當(dāng)數(shù)據(jù)無法一次載入內(nèi)存或者在分布式情況下,貪心算法效率就會變得很低,所以 XGBoost 還提出了一種可并行的近似直方圖算法,用于高效地生成候選的分割點。在近似算法中,我們通過區(qū)間劃分的方式將樣本集劃分為設(shè)定多個 buckets 所構(gòu)成的樣本子集,(即特征的離散化)只在 buckets 的邊界點中篩選最好的分裂結(jié)點,這大大簡化了計算。

缺點

    1. 雖然利用預(yù)排序和近似算法可以降低尋找最佳分裂點的計算量,但在節(jié)點分裂過程中仍需要遍歷數(shù)據(jù)集;
    1. 預(yù)排序過程的空間復(fù)雜度過高,不僅需要存儲特征值,還需要存儲特征對應(yīng)樣本的梯度統(tǒng)計值的索引,相當(dāng)于消耗了兩倍的內(nèi)存。

五、模型使用

from xgboost.sklearn import XGBClassifier
xgb = XGBClassifier(max_depth=3, learning_rate=0.1, n_estimators=100,
                    verbosity=1, silent=None,
                    objective="binary:logistic", booster='gbtree',
                    n_jobs=1, nthread=None, gamma=0, min_child_weight=1, max_delta_step=0,
                    subsample=1, colsample_bytree=1, colsample_bylevel=1,
                    colsample_bynode=1, reg_alpha=0, reg_lambda=1, scale_pos_weight=1,
                    base_score=0.5, random_state=0)
xgb.fit(x,y)
xgb.predict(test_x)
xgb.predict_proba(test_x)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容