Statystyczna Eksploracja Danych

LABORATORIUM 7

W wielu przypadakch skuteczność rozpatrywanych klasyfikatorów nie jest dla nas satsfakcjonująca lub też okazuje się, że są one bardzo podatne na drobne zmiany. Szczególnie dotyczy to drzew. Jedną z metodą, która pozwala omijać te mankamenty jest tworzenie zespołów klasyfikatorów. Ogólna idea jest związana z prostym faktem: jeśli będziemy dysponowali zestawem \(M\) słabych klasyfikatorów (np. o ułamku błędnych klasyfikacji \(err = 0.45\)), to dla dużej wartości \(M\) decyzja podjęta przez większość będzie prawie na pewno prawdziwa. Oczywiście, tego typu twierdzenie dotyczt jedynie zespołu nieskorelowanych klasyfikatorów. Na dziesiejszych zajęciach poznamy trzy najpoularniejsze metody: bagging, boosting oraz lasy losowe.

BAGGING

Metoda agregacji bootstrapowej, czyli bagging, została szerzej omówiona naWykładzie 5. W tym miejscu przypomnimy jedynie sam algorytm:

  1. dla każdego \(m = 1,...M\) wylosuj pseudopróbę \(P_m\) z oryginalnej \(N\)-elementowej próby uczącej,
  2. na każdej pseudopróbie \(P_m\) naucz klasyfikator (np. drzewo) \(T_m\).
W efekcie posiadamy rodzinę \(M\) klasyfikatorów, każdy wyuczone na oddzielnej \(N\)-elementowej próbie powstałej poprzez metodę repróbkowania (bootstrap). Aby zaklasyfikować nową obserwację sprawdzamy jaka jest odpowiedź każdego z klasyfikatorów i wybieramy taką klasę, która została przypisana przez większość.

Po umieszczeniu bibliotek oraz standardowej funkcji związanej z rozkładem Gaussa a także rysowaniem

library(MASS)
library(rpart)
library(rpart.plot)
library(Hmisc)

# Generowanie
draw.data.gauss <- function(S1, S2, m1, m2, n1, n2) {

X1 <- mvrnorm(n1, m1, S1)
X2 <- mvrnorm(n2, m2, S2)

X1 <- data.frame(X1); colnames(X1) <- c("x", "y")
X2 <- data.frame(X2); colnames(X2) <- c("x", "y")

X1$class <- 1; X2$class <- 2

data <- rbind(X1, X2); data$class <- factor(data$class)

return(data)
}

# Rysowanie punktów
plot.data <- function(data) {

cols <- c("blue", "orange")

plot(data[,1:2], col = cols[data$class], cex = 2)
text(data[,1:2], labels = 1:nrow(data), cex = 0.6)

}

wybieramy parametry i tworzymy interesujące nas dane i wizualizujemy je

# Parametry danych z rozkładu Gaussa
S1 <- matrix(c(2, 0, 0, 2), 2, 2)
S2 <- matrix(c(2, 0, 0, 2), 2, 2)

m1 <- c(-0.25, -0.25)
m2 <- c(0.25, 0.25)

n1 <- 50
n2 <- 50

# Ustawienie ziarna dla losowania danych
set.seed(1290)

# Generowanie obserwacji
data <- draw.data.gauss(S1, S2, m1, m2, n1, n2)
newdata <- draw.data.gauss(S1, S2, m1, m2, n1, n2)
# Rysowanie danych
par(mfrow = c(1,2))
plot.data(data); title("Próba ucząca")
plot.data(newdata); title("Próba testowa")

Rysunek 7.1

Ponieważ wartości oczekiwane klas są bardzo blisko siebie, a także losujemy po 50 elementów z każdej z klas, klasyfikator typu przycięte drzewo będzie miał dość niską skuteczność. Sprawdźmy to poniżej, wprowadzając funkcję obliczania błędu oraz przycinania drzew. Utworzymy też pełne drzewo, a następnie przytniemy je za pomocą algorytmu kosztu-złożoności (patrz poprzednie zajęcia). Poza tym wyznaczone zostaną błędy klasyfikacji dla pełnego drzewa dla PT (\(err_0\)) oraz przyciętego dla PU (\(err_1\)) i PT (\(err_2\)).

# Wyznaczanie optymalnej wartości CP zgodnie z regułą 1-SE
choose.cp <- function(tree) {

n <- which.min(tree$cptable[, 4])
n.min <- min(which(tree$cptable[, 4] < tree$cptable[n, 4] + tree$cptable[n, 5]))

return(tree$cptable[n.min, 1])
}

# Wyznaczanie blędu klasyfikatora
err.rate <- function(org.class, pred.class) {

CM <- table(org.class, pred.class)

return(1 - sum(diag(CM)) / sum(CM))
}

# Tworzenie pełnego drzewa
tree0 <- rpart(class ~ ., data, minsplit = 0, minbucket = 0, cp = 0)

# Przycinanie drzewa
tree <- prune(tree0, cp = choose.cp(tree0))

err0 <- err.rate(newdata$class, predict(tree0, newdata, type = "class"))

err1 <- err.rate(data$class, predict(tree, data, type = "class"))
err2 <- err.rate(newdata$class, predict(tree, newdata, type = "class"))

Rysunek 7.2

W tym momencie zaimplementujemy wreszcie same funkcje do metody bagging: bagging.own() oraz bagging.own.pred(). Pierwsza z nich służy do trenowania klasyfikatora, druga - do doknywania przewidywania (de facto pierwsza z nich korzysta od razu również z drugiej do wyznaczenia błędu klasyfikacji na zbiorze treningowym).

# Funkcja do przeprowadzanie procedury bagging
bagging.own <- function(data, N) {

# Dane: losowanie z oryginalnej próby
dane <- replicate(N, sample(1:nrow(data), rep = T))

# tworzenie drzew
trees <- lapply(1:N, function(i) rpart(class ~ ., data = data[dane[,i],], maxdepth = 1))

tmp <- list(dane = dane)

tmp$N <- N
tmp$data <- data
tmp$trees <- trees

tmp1 <- bagging.own.pred(tmp, data)

tmp$trees.class <- tmp1$trees.class
tmp$votes <- tmp1$votes
tmp$class <- tmp1$class

tmp$err <- tmp1$err

return(tmp)
}

# Funkcja do przeprowadzania przewidywania za pomoca baggingu
bagging.own.pred <- function(bag, data) {

tmp <- list()

trees.class <- sapply(1:bag$N, function(i) predict(bag$trees[[i]], newdata = data, type = "class"))
votes <- t(sapply(1:nrow(trees.class), function(i) table(factor(trees.class[i,], levels = levels(data$class)))))

class <- factor(levels(data$class)[apply(votes, 1, which.max)], levels = levels(data$class))

tmp$trees.class <- trees.class
tmp$votes <- votes
tmp$class <- class

tmp$err <- err.rate(data$class, tmp$class)

return(tmp)
}

Finalnie wykonujemy po 10 realizacji dla każdego rozmiaru (tj. ilości klasyfikatorów w zespole) zarówno dla PU jak i PT.

# Liczba drzew
vals <- c(1, 5, 10, 20, 50)

# Wywołanie algorytmu bagging dla różnej liczby drzew dla PU (10 realizacji)
tab <- sapply(vals, function(v) replicate(10, bagging.own(data, v)$err))

# Wywołanie algorytmu bagging dla różnej liczby drzew dla PT (10 realizacji)
tab.new <- sapply(vals, function(v) replicate(10, bagging.own.pred(bagging.own(data, v), newdata)$err))

# Wyznaczanie wartości średnich i odchylenia dla PU
tab.m <- apply(tab, 2, mean)
tab.s <- apply(tab, 2, sd)

# Wyznaczanie wartości średnich i odchylenia dla PT
tab.new.m <- apply(tab.new, 2, mean)
tab.new.s <- apply(tab.new, 2, sd)

# Tworzenie wykresów
errbar(vals, tab.m, tab.m + tab.s, tab.m - tab.s, ylim = c(0.3, 0.55))
errbar(vals, tab.new.m, tab.new.m + tab.new.s, tab.new.m - tab.new.s, add = T, col = "red", errbar.col = "red")

abline(h = err0, lty = 2)
abline(h = err1)
abline(h = err2, col = "red")

Rysunek 7.3

Jedną z dostępnych bibliotek implementującą algorytm baggingu jest adabag. W jej ramach działają funkcje bagging() oraz predict.bagging(), za pomocą których można wykonać obliczenia podobne do tych powyżej. Ze względu na trochę wolniejsze działanie dokonamy symbolicznych uśrednień (po 2).

library(adabag)

# Algorytm bagging
do.bag <- function(data, newdata, n) {

bag <- bagging(class ~ ., data = data, mfinal = n)
bag.pred <- predict.bagging(bag, newdata = newdata)

return(bag.pred$error)
}

# 2 realizacje bagging dla PU i PT
tab.bag <- sapply(vals, function(i) replicate(2, do.bag(data, data, i)))
tab.bag.new <- sapply(vals, function(i) replicate(2, do.bag(data, newdata, i)))

# Wyznaczanie wartości średnich i odchylenia dla PU
tab.bag.m <- apply(tab.bag, 2, mean)
tab.bag.s <- apply(tab.bag, 2, sd)

# Wyznaczanie wartości średnich i odchylenia dla PT
tab.bag.new.m <- apply(tab.bag.new, 2, mean)
tab.bag.new.s <- apply(tab.bag.new, 2, sd)

# Tworzenie wykresów
errbar(vals, tab.bag.m, tab.bag.m + tab.bag.s, tab.bag.m - tab.bag.s, ylim = c(0.2, 0.6), xlab = "Liczba drzew", ylab = "Bład klasyfikacji")
errbar(vals, tab.bag.new.m, tab.bag.new.m + tab.bag.new.s, tab.bag.new.m - tab.bag.new.s, add = T, col = "red", errbar.col = "red")

abline(h = err0, lty = 2)
abline(h = err1)
abline(h = err2, col = "red")

Rysunek 7.4

BOOSTING

Metoda boosting, choć w swojej konstrukcji podobna do baggingu, jest oparta na trochę innym pomyśle: kolejne klasyfikatory są uczone na zbiorze danych, w których prawdopodobieństwo wylosowania obserwacji jest zwiększane, jeśli w poprzednim kroku została ona źle zaklasyfikowana. Początkowo wszystkie wagi \(w_i\) $i=1,...,N$ są ustawione jako \(1/N\) (\(N\) - liczba elementów PU). W każdym kroku $m=1,..,M$ na danych trenowany jest przy użyciu wag \(w\) klasyfikator \(f_m\), a następnie zaś jest obliczana dla niego ważona ilość błędów

\(\mathrm{err}_m = \sum\limits_{i=1}^{i=N}w_i \mathrm{I}[y_i \ne f_m(\mathbf{x}_i)])\),

które są wykorzystywane do zmiany wag

\(w_i = w_i \exp(\gamma_m \mathbf{I}[y_i \ne f_m(\mathbf{x}_i)])\),

gdzie \(\gamma_m = \frac{1-\mathrm{err}_m}{\mathrm{err}_m}\); wagi są w międzyczasie normowane do jedności. Na końcu co do każdej obserwacji podejmowana jest decyzja zgodnie z regułą

\(\mathrm{sgn}\left[ \sum\limits_{m=1}^M \gamma_m f_m(\mathbf{x})\right]\).

W pakiecie adabag do boostingu są dedykowane funkcje boosting() oraz predict.boosting().

# Algorytm boosting
do.boost <- function(data, newdata, n) {

boost <- boosting(class ~ ., data = data, mfinal = n, control=rpart.control(maxdepth = 1))
boost.pred <- predict.boosting(boost, newdata = newdata)

return(boost.pred$error)
}

# 2 realizacje boosting dla PU i PT
tab.boost <- sapply(vals, function(i) replicate(2, do.boost(data, data, i)))
tab.boost.new <- sapply(vals, function(i) replicate(2, do.boost(data, newdata, i)))

# Wyznaczanie wartości średnich i odchylenia dla PU
tab.boost.m <- apply(tab.boost, 2, mean)
tab.boost.s <- apply(tab.boost, 2, sd)

# Wyznaczanie wartości średnich i odchylenia dla PT
tab.boost.new.m <- apply(tab.boost.new, 2, mean)
tab.boost.new.s <- apply(tab.boost.new, 2, sd)

# Tworzenie wykresów
errbar(vals, tab.boost.m, tab.boost.m + tab.boost.s, tab.boost.m - tab.boost.s, ylim = c(0.2, 0.6), xlab = "Liczba drzew", ylab = "Bład klasyfikacji")
errbar(vals, tab.boost.new.m, tab.boost.new.m + tab.boost.new.s, tab.boost.new.m - tab.boost.new.s, add = T, col = "red", errbar.col = "red")

abline(h = err0, lty = 2)
abline(h = err1)
abline(h = err2, col = "red")

Rysunek 7.5

LASY LOSOWE

Ostatnim opisywanym algorytmem są lasy losowe. Tu także za każdym razem dokonujemy bootstrapingu, losując elemnty z PU do pseudopróby. Różnica polega na tym, że w każdym węźle drzewa wybiermay jedynie \(r\) spośród \(p\) składowych, na których dokonywany jest podział, przy czym sugeruje się \(r = \sqrt{p}\). Na koniec znów dokonujemy ostatecznej klasyfikacji zgodnie z większością głosów.

Wykorzystamy pakiet randomForest wraz z jego funkcją randomForest().

library(randomForest)

# Algorytm lasu losowego
do.rf <- function(data, newdata, n) {

rf <- randomForest(class ~ ., data = data, ntree = n)
rf.pred <- predict(rf, newdata = newdata)

return(err.rate(newdata$class, rf.pred))
}

# Parametry dla 10-wymiarowego rozkładu Gaussa
n1 <- 50
n2 <- 50

m1 <- rep(-1, 10)
m2 <- rep(1, 10)

S1 <- matrix(0, 10, 10)
diag(S1) <- 10
S2 <- S1

# Ustawienie ziarna dla losowania danych
set.seed(1290)

data <- draw.data.gauss(S1, S2, m1, m2, n1, n2)
newdata <- draw.data.gauss(S1, S2, m1, m2, n1, n2)

colnames(data) <- c("X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8", "X9", "X10", "class")
colnames(newdata) <- c("X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8", "X9", "X10", "class")

vals <- c(1, 2, 5, 10, 20, 50, 100, 200, 500, 1000)

# 10 realizacji lasu losowego dla PU i PT
tab.rf <- sapply(vals, function(i) replicate(10, do.rf(data, data, i)))
tab.rf.new <- sapply(vals, function(i) replicate(10, do.rf(data, newdata, i)))

# Wyznaczanie wartości średnich i odchylenia dla PU
tab.rf.m <- apply(tab.rf, 2, mean)
tab.rf.s <- apply(tab.rf, 2, sd)

# Wyznaczanie wartości średnich i odchylenia dla PT
tab.rf.new.m <- apply(tab.rf.new, 2, mean)
tab.rf.new.s <- apply(tab.rf.new, 2, sd)

# Tworzenie pełnego drzewa
tree0 <- rpart(class ~ ., data, minsplit = 0, minbucket = 0, cp = 0)

# Przycinanie drzewa
tree <- prune(tree0, cp = choose.cp(tree0))

err0 <- err.rate(newdata$class, predict(tree0, newdata, type = "class"))

err1 <- err.rate(data$class, predict(tree, data, type = "class"))
err2 <- err.rate(newdata$class, predict(tree, newdata, type = "class"))

# Tworzenie wykresów
errbar(vals, tab.rf.m, tab.rf.m + tab.rf.s, tab.rf.m - tab.rf.s, ylim = c(0, 0.6), xlab = "Liczba drzew", ylab = "Bład klasyfikacji", log="x")
errbar(vals, tab.rf.new.m, tab.rf.new.m + tab.rf.new.s, tab.rf.new.m - tab.rf.new.s, add = T, col = "red", errbar.col = "red")

abline(h = err0, lty = 2)
abline(h = err1)
abline(h = err2, col = "red")

title("Las losowy")

Rysunek 7.6