В прошлой статье (KNN для задачи классификации) мы попытались понять, как работает KNN. Теперь давайте реализуем алгоритм KNN в R/R Studio. Мы будем строить модель KNN и прогнозировать наши тестовые данные, чтобы увидеть, насколько хорошо работает наша модель.

Набор данных, над которым мы будем работать, — это данные о грибах от Kaggle. Этот набор данных включает образцы 23 видов грибов. Каждый вид определяется как определенно съедобный и определенно ядовитый, данные о грибах содержат 23 переменных и 8124 наблюдения.

Начнем с загрузки необходимых библиотек

# Loading the necessary libraries
```{r}
library(readr)
library(tidyverse)
library(visdat)
library(missMethods)
library(fastDummies)
library(caret)
```
# Loading The Data
```{r}
mushrooms <- read.csv("mushrooms.csv")
head(mushrooms)
```
```{r}
str(mushrooms)
```
output:
'data.frame': 8124 obs. of  23 variables:
 $ class                   : chr  "p" "e" "e" "p" ...
 $ cap.shape               : chr  "x" "x" "b" "x" ...
 $ cap.surface             : chr  "s" "s" "s" "y" ...
 $ cap.color               : chr  "n" "y" "w" "w" ...
 $ bruises                 : chr  "t" "t" "t" "t" ...
 $ odor                    : chr  "p" "a" "l" "p" ...
 $ gill.attachment         : chr  "f" "f" "f" "f" ...
 $ gill.spacing            : chr  "c" "c" "c" "c" ...
 $ gill.size               : chr  "n" "b" "b" "n" ...
 $ gill.color              : chr  "k" "k" "n" "n" ...
 $ stalk.shape             : chr  "e" "e" "e" "e" ...
 $ stalk.root              : chr  "e" "c" "c" "e" ...
 $ stalk.surface.above.ring: chr  "s" "s" "s" "s" ...
 $ stalk.surface.below.ring: chr  "s" "s" "s" "s" ...
 $ stalk.color.above.ring  : chr  "w" "w" "w" "w" ...
 $ stalk.color.below.ring  : chr  "w" "w" "w" "w" ...
 $ veil.type               : chr  "p" "p" "p" "p" ...
 $ veil.color              : chr  "w" "w" "w" "w" ...
 $ ring.number             : chr  "o" "o" "o" "o" ...
 $ ring.type               : chr  "p" "p" "p" "p" ...
 $ spore.print.color       : chr  "k" "n" "n" "k" ...
 $ population              : chr  "s" "n" "n" "s" ...
 $ habitat                 : chr  "u" "g" "m" "u" ...

Все переменные являются категориальными, поэтому нашим первым шагом будет преобразование всех этих переменных в фактор. Категориальная переменная не относится ни к какой шкале. Алгоритмы машинного обучения требуют чисел в качестве входных данных, поэтому, если ваша категориальная переменная принимает такие значения, как «яблоко», «апельсин» и «груша», вам нужно каким-то образом закодировать ее как числа, поскольку функция.factor() предоставляет способ сделать это.

```{r}
mushrooms[sapply(mushrooms, is.character)] <- lapply(
mushrooms[sapply(mushrooms, is.character)], as.factor)
```
```{r}
str(mushrooms)
```

output: 
'data.frame': 8124 obs. of  23 variables:
 $ class                   : Factor w/ 2 levels "e","p": 2 1 1 2 1 1 1 1 2 1 ...
 $ cap.shape               : Factor w/ 6 levels "b","c","f","k",..: 6 6 1 6 6 6 1 1 6 1 ...
 $ cap.surface             : Factor w/ 4 levels "f","g","s","y": 3 3 3 4 3 4 3 4 4 3 ...
 $ cap.color               : Factor w/ 10 levels "b","c","e","g",..: 5 10 9 9 4 10 9 9 9 10 ...
 $ bruises                 : Factor w/ 2 levels "f","t": 2 2 2 2 1 2 2 2 2 2 ...
 $ odor                    : Factor w/ 9 levels "a","c","f","l",..: 7 1 4 7 6 1 1 4 7 1 ...
 $ gill.attachment         : Factor w/ 2 levels "a","f": 2 2 2 2 2 2 2 2 2 2 ...
 $ gill.spacing            : Factor w/ 2 levels "c","w": 1 1 1 1 2 1 1 1 1 1 ...
 $ gill.size               : Factor w/ 2 levels "b","n": 2 1 1 2 1 1 1 1 2 1 ...
 $ gill.color              : Factor w/ 12 levels "b","e","g","h",..: 5 5 6 6 5 6 3 6 8 3 ...
 $ stalk.shape             : Factor w/ 2 levels "e","t": 1 1 1 1 2 1 1 1 1 1 ...
 $ stalk.root              : Factor w/ 5 levels "?","b","c","e",..: 4 3 3 4 4 3 3 3 4 3 ...
 $ stalk.surface.above.ring: Factor w/ 4 levels "f","k","s","y": 3 3 3 3 3 3 3 3 3 3 ...
 $ stalk.surface.below.ring: Factor w/ 4 levels "f","k","s","y": 3 3 3 3 3 3 3 3 3 3 ...
 $ stalk.color.above.ring  : Factor w/ 9 levels "b","c","e","g",..: 8 8 8 8 8 8 8 8 8 8 ...
 $ stalk.color.below.ring  : Factor w/ 9 levels "b","c","e","g",..: 8 8 8 8 8 8 8 8 8 8 ...
 $ veil.type               : Factor w/ 1 level "p": 1 1 1 1 1 1 1 1 1 1 ...
 $ veil.color              : Factor w/ 4 levels "n","o","w","y": 3 3 3 3 3 3 3 3 3 3 ...
 $ ring.number             : Factor w/ 3 levels "n","o","t": 2 2 2 2 2 2 2 2 2 2 ...
 $ ring.type               : Factor w/ 5 levels "e","f","l","n",..: 5 5 5 5 1 5 5 5 5 5 ...
 $ spore.print.color       : Factor w/ 9 levels "b","h","k","n",..: 3 4 4 3 4 3 3 4 3 3 ...
 $ population              : Factor w/ 6 levels "a","c","n","s",..: 4 3 3 4 1 3 3 4 5 4 ...
 $ habitat                 : Factor w/ 7 levels "d","g","l","m",..: 6 2 4 6 2 2 4 4 2 4 ...

Здесь, если мы видим, что корень переменной стебля имеет наблюдения, содержащие «?» что ничего не значит, это означает, что отсутствующие данные должны быть заменены на NA.

Работа с пропущенными значениями

```{r}
mushrooms$stalk.root[mushrooms$stalk.root=="?"]
```
# replacing ? with NA's
```{r}
mushrooms$stalk.root[mushrooms$stalk.root == "?"] <- NA
```
# dealing with missing values
```{r}
vis_miss(mushrooms)
```

Здесь мы видим, что 1,3% данных отсутствуют для переменной корня стебля. Решением проблемы с отсутствующими данными является вменение, другими словами, замена отсутствующих значений наилучшими предполагаемыми значениями. Поскольку значения отсутствующих данных являются категориальными, мы не можем использовать подход среднего и медианного значения для вменения отсутствующих данных. Мы можем использовать модовый подход для импутации данных, но мы должны проверить почти нулевую дисперсию данных.

Переменные с почти нулевой дисперсией — это те переменные, которые содержат только одно уникальное значение и не предоставляют никакой полезной информации и должны быть удалены из данных.

# Checking Near-Zero Variance
```{r}
caret::nearZeroVar(mushrooms, saveMetrics= TRUE) %>%
  filter(nzv)
```

output:
 
                freqRatio <dbl> percentUnique <dbl> zeroVar <lgl> nzv <lgl>
gill.attachment 37.68571        0.02461841          FALSE         TRUE
veil.type       0.00000         0.01230921          TRUE          TRUE
veil.color      82.54167        0.04923683          FALSE         TRUE

Приведенный выше вывод показывает три переменные, которые содержат почти нулевую дисперсию и должны быть удалены.

# Removing Near-zero variance variables
```{r}
mushrooms_new <- mushrooms[,-c(7,11,18)]
```
# Mode imputaion for missing values

```{r}
mushrooms_new <- impute_mode(mushrooms_new)
```

```{r}
vis_miss(mushrooms_new)
```

Вменение режима заменяет отсутствующие значения категориальной переменной (в нашем случае это корень переменной) модой непропущенных значений этой переменной.

```{r}
ggplot(mushrooms_new, aes(x=class)) + 
  geom_bar(color = "black", fill = "lightgreen")
```

На приведенной выше гистограмме показан наш класс переменной ответа, где «e» представляет съедобный, а «p» — ядовитый. Обе группы почти равны: съедобные грибы составляют 52%, а ядовитые - 48%.

Преобразование категориальных данных в числовые

Все переменные в наших данных являются категориальными. Алгоритм KNN не может обрабатывать категориальные переменные, поэтому нам нужно кодировать наши данные, и для этого мы будем использовать фиктивное кодирование вместо одноразового кодирования, поскольку фиктивное кодирование имеет тенденцию удалять повторяющиеся категории, присутствующие в одном горячем кодировании. Другими словами, одно горячее кодирование создает фиктивные переменные, которые равны количеству категорий (k) в переменной, тогда как фиктивное кодирование использует k-1 фиктивные переменные, удаляя повторяющиеся категории.

```{r}
encoded_mushrooms <- dummy_cols(mushrooms_new, 
select_columns = c("cap.shape", "cap.surface", "cap.color", "bruises", "odor", 
"gill.spacing", "gill.size","gill.color", "stalk.root", "stalk.surface.above.ring", 
"stalk.surface.below.ring", "stalk.color.above.ring", "stalk.color.below.ring",
"veil.type", "ring.number", "ring.type", "spore.print.color", "population", "habitat"),
remove_selected_columns = TRUE, 
remove_first_dummy  = TRUE)
```

Набор для обучения и тестирования

Разделение данных на наборы для обучения и тестирования, чтобы мы могли проверить точность нашей модели. Мы обучим наши данные на обучающем наборе и проверим точность модели на тестовом наборе, чтобы увидеть, насколько хорошо работает наша модель.

# Data Partition
```{r}
set.seed(42)
trn_idx = sample(nrow(encoded_mushrooms), size = trunc(0.70 * nrow(encoded_mushrooms)))
trn_data = encoded_mushrooms[trn_idx, ]
tst_data = encoded_mushrooms[-trn_idx, ]
```

Данные обучения содержат 5686 наблюдений, а данные тестирования — 2438 наблюдений.

Выбор значения К

Нам нужно найти оптимальное значение k, так как низкие значения k обычно перекрываются, а большие значения часто не соответствуют. Давайте создадим классификатор KNN, который мы можем использовать для прогнозирования съедобных или ядовитых грибов.

# Creating a resampling method
```{r}
cv <- trainControl(
  method = "repeatedcv",
  number = 10,
  repeats = 5,
  classProbs = TRUE,
  summaryFunction = twoClassSummary
)
```

# Create a hyperparameter grid search
```{r}
hyper_grid <- expand.grid(k = seq(3, 25, by = 2))
```

В приведенном выше коде мы используем 10 итераций повторной выборки с 5-кратной перекрестной проверкой. Мы получим оптимальное значение k.

```{r}
set.seed(42) # for reproducibility

KNN_Model <- train(class ~ ., data = encoded_mushrooms,
                   method = "knn",
                   tuneGrid = hyper_grid,
                   tuneLength = 20,
                   trControl = cv,
                   metric = "ROC")

ggplot(KNN_Model)
```

```{r}
KNN_Model
```

output:
k-Nearest Neighbors 

8124 samples
  91 predictor
   2 classes: 'e', 'p' 

No pre-processing
Resampling: Cross-Validated (10 fold, repeated 5 times) 
Summary of sample sizes: 7312, 7312, 7313, 7311, 7311, 7311, ... 
Resampling results across tuning parameters:

  k   ROC        Sens  Spec     
   3  1.0000000  1     1.0000000
   5  1.0000000  1     0.9998980
   7  1.0000000  1     0.9996429
   9  1.0000000  1     0.9992342
  11  1.0000000  1     0.9985192
  13  1.0000000  1     0.9980081
  15  0.9999999  1     0.9979571
  17  0.9999987  1     0.9979571
  19  0.9999976  1     0.9979571
  21  0.9999944  1     0.9979571
  23  0.9999925  1     0.9979571
  25  0.9999897  1     0.9979571

ROC was used to select the optimal model using the largest value.
The final value used for the model was k = 13.

На приведенном выше графике показаны результаты поиска по сетке, и наша лучшая модель использовала 13 ближайших соседей и обеспечила точность 99%.

Теперь давайте посмотрим на важные переменные в нашей модели KNN.

```{r}
varImp(KNN_Model)
```
output:
 
                           Importance
odor_n                     100.00000   
odor_f                     70.78455   
ring.type_p                69.38451   
stalk.surface.above.ring_k 68.62144   
stalk.surface.below.ring_k 66.39304   
gill.size_n                64.09885   
bruises_t                  63.47782   
stalk.surface.above.ring_s 60.67201   
population_v               56.97877   
stalk.surface.below.ring_s 53.35282   
1-10 of 20 rows

Для нашей модели важны 20 переменных из закодированных 91 переменной. Давайте дополнительно оценим модель и спрогнозируем наши тестовые данные, чтобы увидеть, как работает наша модель.

```{r}
Prediction <- predict(KNN_Model, newdata = tst_data)

confusionMatrix(Prediction, tst_data$class) 
```
output:
Confusion Matrix and Statistics

          Reference
Prediction    e    p
         e 1264    1
         p    0 1173
                                     
               Accuracy : 0.9996     
                 95% CI : (0.9977, 1)
    No Information Rate : 0.5185     
    P-Value [Acc > NIR] : <2e-16     
                                     
                  Kappa : 0.9992     
                                     
 Mcnemar's Test P-Value : 1          
                                     
            Sensitivity : 1.0000     
            Specificity : 0.9991     
         Pos Pred Value : 0.9992     
         Neg Pred Value : 1.0000     
             Prevalence : 0.5185     
         Detection Rate : 0.5185     
   Detection Prevalence : 0.5189     
      Balanced Accuracy : 0.9996     
                                     
       'Positive' Class : e

Таким образом, в целом наша модель хорошо работает на тестовых данных, где 1264 гриба съедобны, наша модель также предсказывает то же самое. Ядовитых грибов 1174, где наша модель предсказывает, что 1173 гриба ядовиты, а 1 съедобен, поэтому наша модель точна на 99%. Здесь этот набор данных является отличным примером модели переобучения. Итак, что такое переобучение???

Модель, дающая точность 99%, является редким случаем, и больше шансов на переоснащение, мы подробно рассмотрим переоснащение и недообучение модели в моем следующем блоге.

Весь код в статье выложен на GitHub. Вот ссылка: G https://github.com/prabhtalwar/Projects-in-R/blob/main/Classification-on-Mushrooms-Data.Rmd

Надеюсь, вам понравилась статья. До следующего раза!!!