Vou usar o código sklearn , pois geralmente é muito mais limpo que o R
código.
Aqui está a implementação da propriedade feature_importances do GradientBoostingClassifier (removi algumas linhas de código que atrapalham o material conceitual)
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
Isso é muito fácil de entender. self.estimators_
é uma matriz que contém as árvores individuais no booster, portanto, o loop for está iterando sobre as árvores individuais. Há um hickup com o
stage_sum = sum(tree.feature_importances_
for tree in stage) / len(stage)
isso é cuidar do caso de resposta não binária. Aqui, ajustamos várias árvores em cada estágio de uma maneira um contra todos. É conceitualmente mais simples se concentrar no caso binário, em que a soma tem um somatório, e isso é justo tree.feature_importances_
. Portanto, no caso binário, podemos reescrever tudo isso como
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
Assim, em palavras, resuma as importâncias das árvores individuais e divida pelo número total de árvores . Resta ver como calcular as importâncias do recurso para uma única árvore.
O cálculo da importância de uma árvore é implementado no nível do cython , mas ainda pode ser seguido. Aqui está uma versão limpa do código
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
Isso é bem simples. Iterar através dos nós da árvore. Desde que você não esteja em um nó folha, calcule a redução ponderada na pureza do nó a partir da divisão nesse nó e atribua-a ao recurso que foi dividido em
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)
Em seguida, quando terminar, divida tudo pelo peso total dos dados (na maioria dos casos, o número de observações)
importances /= nodes[0].weighted_n_node_samples
Vale lembrar que o impureza é um nome comum para a métrica usar ao determinar qual divisão fazer ao cultivar uma árvore. Sob essa luz, estamos simplesmente resumindo o quanto a divisão em cada recurso nos permitiu reduzir a impureza em todas as divisões na árvore.
No contexto do aumento de gradiente, essas árvores são sempre árvores de regressão (minimizam o erro ao quadrado com avidez) ajustadas ao gradiente da função de perda.