5.2 Бинарные деревья решений

Как было отмечено в разделе 4.3, использование иерархических древовидных структур (decision trees) является одним из наиболее универсальных и эффективных методов классификации и регрессии (Hunt et al., 1966; Breiman at al., 1984; Loh and Shih, 1997). Классификационные модели деревьев рекурсивно делят набор данных на подмножества, являющиеся все более и более гомогенными относительно определенных признаков. Создается решающее правило классификации иерархического типа и формируется ассоциативный логический ключ, дающий возможность выполнять распознавание объектов из новых выборок.

Самым простым, частным случаем деревьев решений являются деревья бинарной классификации, в узлах которых ветвление может вестись только в двух направлениях, т.е. осуществляется выбор из двух альтернатив (“да” и “нет”). Создание такой дихотомической классификационной модели осуществляется с использованием алгоритма ID3 (Interactive Dichotomizer).

В основе алгоритма (Quinlan, 1986) лежит циклическое разбиение обучающей выборки на классы в соответствии с переменной, имеющей наибольшую “классифицирующую силу”. Каждое подмножество наблюдений (объектов), выделяемое такой переменной, вновь разбивается на классы с использованием следующей переменной с наибольшей классифицирующей способностью и т.д. Разбиение заканчивается, когда в итоговом подмножестве оказываются объекты лишь одного класса. Пути движения по полученному дереву решений с верхнего уровня на самые нижние определяются логическими правилами в виде цепочек конъюнкций.

При выборе критерия, оценивающего классифицирующую силу, обычно используют теоретико-информационный подход. Мера информационного выигрыша IG (Information Gain) для разбиения A определяется как разность энтропии Шеннона \(H(S)\) для исходного набора данных и суммы энтропий всех фрагментов данных после разбиения \(Н(S_{\nu})\) с соответствующим весом в зависимости от размера каждой части:

\[IG(S, A) = H(S) - \sum_{\nu \in V}\frac{S_{\nu}}{S}H(S_{\nu}),\]

где \(\nu\) выбранное опорное значение переменной \(V\), использованное для разбиения \(A\). Если разделение на классы выполнено оптимально, то каждое подмножество \(S_{\nu}\) будет иметь небольшую энтропию, и, следовательно, значение \(IG\) будет максимальным. На языке R этот критерий может быть рассчитан следующим образом:

Entropy <- function(vls) {
    res <- vls/sum(vls)*log2(vls/sum(vls))
    res[vls == 0] <- 0
    -sum(res)
}
InformationGain <- function(tble) {
    entropyBefore <- Entropy(colSums(tble))
    s <- rowSums(tble)
    entropyAfter <- sum(s/sum(s) *
                            apply(tble, MARGIN = 1, FUN = Entropy ))
    informationGain <- entropyBefore - entropyAfter
    return (informationGain)
}

Для реализации алгоритма ID3 используем компоненты пакета data.tree: объект класса tree (дерево) и метод AddChild , добавляющий к выстраиваемому дереву очередной дочерний узел (см. http://ipub.com/wp-content):

IsPure <- function(data) {
    length(unique(data[, ncol(data)])) == 1
}

TrainID3 <- function(node, data) {
    node$obsCount <- nrow(data)
    #  если текущий набор данных принадлежит к одному классу, то
    if (IsPure(data)) {
        #создается лист дерева с экземплярами этого класса
        child <- node$AddChild(unique(data[, ncol(data)]))
        node$feature <- tail(names(data), 1)
        child$obsCount <- nrow(data)
        child$feature <- ''
    } else {
        # рассчитывается вектор информационных выигрышей IG
        ig <- sapply(colnames(data)[-ncol(data)], 
                     function(x) InformationGain(
                         table(data[, x], data[, ncol(data)])))
        #  выбирается значение признака с наибольшей величиной IG
        feature <- names(which.max(ig))
        node$feature <- feature
        # создается подмножества данных на основе этого значения 
        childObs <- split(data[, names(data) != feature, 
                               drop = FALSE], data[, feature], drop = TRUE)
        #  создаются дочерние узлы дерева c именем признака
        for (i in 1:length(childObs)) {
            child <- node$AddChild(names(childObs)[i])
            # осуществляется рекурсия алгоритма для дочернего узла
            TrainID3(child, childObs[[i]])
        }
    }
}

Обратим внимание, что наша функция TrainID3 является рекурсивной, т.е. для выполнения итераций вызов функции осуществляется из тела ее самой. Функция завершает работу, если все ветви дерева завершаются листьями с объектами одного класса.

Для иллюстрации работы алгоритма ID3 обратимся к нашему примеру по классификации лиц избирателей (см. рис. 5.1 в разделе 5.1). Для получения осмысленной визуализации дерева перейдем от численных обозначений к натуральным названиям признаков и их градаций. В результате получаем следующее дерево решений:

DFace <- read.delim(file = "data/Faces.txt", header = TRUE, row.names = 1)
DFaceN <- DFace[, -17]; Class <- DFace$Class 
DFaceN[DFaceN == 1] <- "Да" ; DFaceN[DFaceN == 0] <- "Нет"
Class[Class == 1] <- "Патриот"
Class[Class == 2] <- "Демократ"
DFaceN <- cbind(DFaceN,Class) 

colnames(DFaceN) <- c("голова_круглая", "уши_оттопырен",
                      "нос_круглый", "глаза_круглые", "лоб_морщины",
                      "носогубн_складка", "губы_толстые", "волосы", "усы","борода",
                      "очки","родинка_щеке", "бабочка", "брови_подняты", "серьга",
                      "курит_трубка", "Группа")
library(data.tree)
tree <- Node$new("DFaceN")  # Создаем «пустое» дерево
TrainID3(tree, DFaceN)
print(tree, "feature", "obsCount")
##                       levelName       feature obsCount
## 1  DFaceN                       уши_оттопырен       16
## 2   ¦--Да                              борода       10
## 3   ¦   ¦--Да                     нос_круглый        7
## 4   ¦   ¦   ¦--Да                курит_трубка        3
## 5   ¦   ¦   ¦   ¦--Да                  Группа        1
## 6   ¦   ¦   ¦   ¦   °--Патриот                       1
## 7   ¦   ¦   ¦   °--Нет                 Группа        2
## 8   ¦   ¦   ¦       °--Демократ                      2
## 9   ¦   ¦   °--Нет               губы_толстые        4
## 10  ¦   ¦       ¦--Да                  Группа        1
## 11  ¦   ¦       ¦   °--Демократ                      1
## 12  ¦   ¦       °--Нет                 Группа        3
## 13  ¦   ¦           °--Патриот                       3
## 14  ¦   °--Нет                         Группа        3
## 15  ¦       °--Демократ                              3
## 16  °--Нет                      брови_подняты        6
## 17      ¦--Да                          Группа        2
## 18      ¦   °--Демократ                              2
## 19      °--Нет                         Группа        4
## 20          °--Патриот                               4

Выращивание дерева начинается с переменной, имеющей наибольшее значение критерия IG, каковой оказались оттопыренные уши: ее значению “Да” соответствует группа из 10 объектов, а значению “Нет” - из 6. Если в первой ветви для классификации последовательно привлекаются еще 4 признака (наличие бороды, круглого носа, курительной трубки и толстых губ), то во втором случае безошибочное разбиение сразу происходит при использовании поднятых бровей. Проверим теперь, насколько эффективен полученный классификатор на обучающей выборке:

Predict <- function(tree, features) {
    if (tree$children[[1]]$isLeaf) 
        return(tree$children[[1]]$name)
    child <- tree$children[[features[[tree$feature]]]]
    return( Predict(child, features))
}
pred <- apply(DFaceN[, -17], 1, function(x) Predict(tree, x))
table(Факт = DFaceN$Группа, Прогноз = pred)
##           Прогноз
## Факт       Демократ Патриот
##   Демократ        8       0
##   Патриот         0       8

Предсказание электоральной группы по всей обучающей выборке, как и в случае логистической регрессии, также оказалось безошибочным. Однако нет уверенности, что построенные деревья будут столь же успешны при практической реализации на “свежих” данных. Для оценки эффективности модели выполним перекрестную проверку:

Nerr <- 0
for (i in 1:nrow(DFaceN)) {
    tree <- Node$new("DFaceN")
    TrainID3(tree, DFaceN[-i, ])
    Nerr = Nerr + 
        sum(DFaceN[i,17] != Predict(tree,DFaceN[i, -17]))  }
(Nerr/nrow(DFaceN))
## [1] 0.25

При выполнении скользящего контроля четыре объекта из 16 оказались неверно предсказанными с помощью бинарного дерева, построенного после исключения тестируемого примера. Считается (Дук, Самойленко, 2001), что алгоритм ID3 в случае взаимозависимых признаков нередко направляет ход логического вывода по ложному пути и создает лишь иллюзию правильного рассуждения. Но для более обоснованных выводов следует рассматривать результаты тестирования на более серьезных примерах, чем наш.