Userò lo sklearn codice, in quanto è in genere molto più pulito rispetto al R
codice.
Ecco l'implementazione della proprietà feature_importances di GradientBoostingClassifier (ho rimosso alcune righe di codice che ostacolano le cose concettuali)
def feature_importances_(self):
total_sum = np.zeros((self.n_features, ), dtype=np.float64)
for stage in self.estimators_:
stage_sum = sum(tree.feature_importances_
for tree in stage) / len(stage)
total_sum += stage_sum
importances = total_sum / len(self.estimators_)
return importances
Questo è abbastanza facile da capire. self.estimators_
è un array che contiene i singoli alberi nel booster, quindi il ciclo for sta iterando sui singoli alberi. C'è un singhiozzo con il
stage_sum = sum(tree.feature_importances_
for tree in stage) / len(stage)
questo si occupa del caso di risposta non binaria. Qui inseriamo più alberi in ogni fase in un modo uno contro tutti. Concettualmente è più semplice concentrarsi sul caso binario, in cui la somma ha un summand, e questo è giusto tree.feature_importances_
. Quindi nel caso binario, possiamo riscrivere tutto come
def feature_importances_(self):
total_sum = np.zeros((self.n_features, ), dtype=np.float64)
for tree in self.estimators_:
total_sum += tree.feature_importances_
importances = total_sum / len(self.estimators_)
return importances
Quindi, in parole, riassumi le caratteristiche dell'importanza dei singoli alberi, quindi dividi per il numero totale di alberi . Resta da vedere come calcolare le importazioni delle funzionalità per un singolo albero.
Il calcolo dell'importanza di un albero è implementato a livello di cython , ma è ancora seguibile. Ecco una versione ripulita del codice
cpdef compute_feature_importances(self, normalize=True):
"""Computes the importance of each feature (aka variable)."""
while node != end_node:
if node.left_child != _TREE_LEAF:
# ... and node.right_child != _TREE_LEAF:
left = &nodes[node.left_child]
right = &nodes[node.right_child]
importance_data[node.feature] += (
node.weighted_n_node_samples * node.impurity -
left.weighted_n_node_samples * left.impurity -
right.weighted_n_node_samples * right.impurity)
node += 1
importances /= nodes[0].weighted_n_node_samples
return importances
Questo è abbastanza semplice Scorrere i nodi dell'albero. Finché non ci si trova in un nodo foglia, calcolare la riduzione ponderata della purezza del nodo dalla divisione in questo nodo e attribuirla alla funzione su cui è stata suddivisa
importance_data[node.feature] += (
node.weighted_n_node_samples * node.impurity -
left.weighted_n_node_samples * left.impurity -
right.weighted_n_node_samples * right.impurity)
Quindi, al termine, dividere tutto per il peso totale dei dati (nella maggior parte dei casi, il numero di osservazioni)
importances /= nodes[0].weighted_n_node_samples
Vale la pena ricordare che l' impurità è un nome comune che la metrica deve usare quando si determina quale scissione fare quando si coltiva un albero. Alla luce di ciò, stiamo semplicemente riassumendo la suddivisione su ciascuna caratteristica che ci ha permesso di ridurre l'impurità in tutte le divisioni nella struttura.
Nel contesto del potenziamento del gradiente, questi alberi sono sempre alberi di regressione (minimizzano avidamente l'errore al quadrato) adatti al gradiente della funzione di perdita.