Метод случайного леса

Алгоритм случайного леса

Здравствуйте и вновь добро пожаловать на занятия по теме «Машинное обучение с учителем на языке Python: ансамблевые методы».

В этой статье мы обсудим алгоритм случайного леса, который основывается на понятиях, рассмотренными нами ранее в курсе, и так же рассмотрим некоторые методы случайного леса.

Напомним,что если у нас есть B независимых и одинаково распределённых переменных,каждая с дисперсией σ2, тосумма или, что то же самое, среднее значение выборки случайных переменных будетиметь дисперсию

Напомнимтакже, что это не так, если переменные только одинаково распределённые, но ненезависимые. В этом случае если возможная корреляция между двумя любымислучайными переменными равна ρ, тодисперсия суммы, или среднего значения выборки, будет равна

Напомнимтакже, что это не так, если переменные только одинаково распределённые, но ненезависимые. В этом случае если возможная корреляция между двумя любымислучайными переменными равна ρ, тодисперсия суммы, или среднего значения выборки, будет равна

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

Напомню,что суть бэггинга заключалась в усреднении результатов моделей с большой дисперсиейи малым смещением. Деревья подходят для этого идеально, поскольку они могутбыть сколь угодно глубокими и охватывать сложные взаимодействия. Таким образом,в большинстве случаев они достигают 100% точности на учебном наборе данных или,другими словами, имеют нулевое смещение. В тоже время это приводит к тому, чтоони же имеют большую дисперсию. Однако согласно предыдущему уравнению дисперсиидля ансамбля, мы можем добиться меньшей общей дисперсии, отыскав деревья, некоррелирующие друг с другом.

Тутвозникает хороший вопрос: можно ли предпринять что-то более существенное, чтобыгарантировать, что деревья не коррелируют друг с другом, чем простопредполагать, что деревья с максимальной глубиной, созданные на различныхбутстреп-выборках, будут сильно различаться?

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

Итак,как же метод случайного леса декоррелирует деревья? Аналогично тому, как мыможем случайным образом выбирать, на каких примерах проводить обучение, мыможем случайным образом выбирать и каким признакам обучаться. Возьмём матрицуданных X. Один из способовполучить разные деревья – сделать выборку из разных строк, мы это уже делали.Но есть и другой способ – сделать выборку из разных признаков, что эквивалентнообучению дерева только определённому подмножеству признаков.

Обычномы выбираем размерность подмножества d, намного меньшую D, предполагая, что X – матрица размерности NxD. Изобретатели метода случайного лесасоветуют устанавливать d, равным целой части квадратного корня из D, если речь идёт оклассификации, вплоть до 1. Что же касается регрессии, то они советуютустанавливать d равным целой части от D/3, вплоть до 5. Лучшимвыходом, как всегда, является подобрать ту размерность, которая лучше всегоподходит к вашим конкретным данным, воспользовавшись методами вроде перекрёстнойпроверки.

Алгоритмобучения случайного леса следующий. Мы запускаем цикл B раз, каждый разсоздавая выборку с возвращением из учебного набора, создаём новое дерево, но необучаем его обычным способом, а запускаем цикл, пока не достигнем конечного узлаили не достигнем указанной максимальной глубины. На практике такой цикл можетбыть рекурсией, как мы это уже делали в моём первом курсе по машинному обучениюс учителем. На каждой итерации мы выбираем случайным образом d признаков из их общегочисла и из этих d признаков выбираемлучший разделитель, используя критерий наподобие максимума прироста информации,вновь-таки, как мы это уже делали в первом курсе по машинному обучению сучителем. Этот разделитель добавляется к текущему дереву, после чего рекурсияпродолжается, пока не дерево не закончится, то есть пока не достигнемузла-листа или максимальной указанной глубины. Затем, как обычно, добавляем этумодель в список моделей:

for b=1..B:

    Xb, Yb = выборка_с_возвращением(X, Y)

    model = DecisionTree()

    пока не конечный узел и не достигнуто max_depth:

        выбрать d признаков случайнымобразом

        выбрать лучший разделитель из d признаков (например, макс. прирост информации)

        добавить разделитель в модель

    models.append(model)

Тутнужно отметить несколько вещей. Во-первых, это похоже на бэггинг с тем лишьисключением, что в дополнение к созданию на каждом этапе выборок методомбутстрепа из учебного набора данных мы также создаём выборки из признаков.Поэтому иногда этот процесс предпочитают называть бэггингом признаков.Во-вторых, это не ансамбли простейших деревьев решений вроде тех, что мы делалипри бэггинге, поскольку теперь мы изменяем алгоритм дерева решений в частивопроса о разделителе, в частности, не позволяя ему проводить поиск по всемусписку признаков. В связи с этим мы не можем создать случайный лес, используявстроенный класс деревьев решений, но и не будем в этом курсе писать алгоритмслучайного леса с нуля, поскольку большаячасть работы окажется переписыванием дерева решений с целью дать емувозможность выбирать случайное подмножество признаков в каждом узле. Впрочем,для вас будет чудесным упражнением для тренировки навыков, если попробуетесделать это самостоятельно.  Мы ужерассмотрели, как написать код для дерева решений в первом курсе по машинномуобучению с учителем, предпосылке к настоящему курсу, а вам надо лишьвидоизменить его, воспользовавшись бэггингом.

Напомним,что мы уже говорили о том, как делать прогноз для ансамблей. Для двоичнойклассификации можно просто округлить среднее значение, а для регрессии можновывести его. Поскольку об этом уже упоминалось, мы не будем здесьостанавливаться на данном вопросе. Главная сложность, когда речь идёт о методеслучайного леса, – это функция train.

Ввидутого, что мы воспользуемся классом RandomForest библиотеки Ski-kit Learn, стоит взглянуть на ряд возможностей, который он предоставляет.Многие из них будут вам знакомы, поскольку мы говорили о них либо в этом курсе,либо в предыдущем. Обратить внимание нужно лишь на несколько вещей. Преждевсего обнаруживается, что по умолчанию стоит 10 деревьев как для классификации,так и для регрессии. Если взглянуть на критерий разделения, то видно, что классификаториспользует критерий Джинни, тогда как регрессор – среднеквадратическую ошибку.Критерий Джинни аналогичен энтропии, так что большой разницы тут не будет.

Аргументmax_feature сообщает случайному лесу, сколькопризнаков выбирать при каждом разделении. Обратите внимание, что по умолчаниюдля классификации стоит значение квадратного корня из D, а вот для регрессиизначение по умолчанию просто равно D, что не соответствует рекомендации выбирать значение D/3.

Далееидёт множество опций, связанных с построением дерева, например глубина дерева max_depth (по умолчанию стоит значение None), минимальное количество выборок для разделения min_samples_split, минимальное числовыборок на лист  min_samples_leaf и максимальное число узлов-листьев max_leaf_nodesв целом. Заметьте, как много этих опций имеют отношение к дереву, а нелесу.

Важныйвопрос, возникающий в машинном обучении: а что, если множество входныхпризнаков не имеют значения и являются просто шумом? Что будет, еслибольшинство признаков являются шумом, а случайный лес, выбирая входные признакислучайным образом, выберет только шум? И что, если такое случится созначительной частью деревьев?

Пусть,например, у нас есть 3 значащие переменные и 100 незначащих и мы выбираем d, равное целому от квадратного корня из103, то есть 10. Какова тогда вероятность того, что в списке будет одназначащая переменная? Ответ: 26,6%. На самом деле это не так уж и страшно,поскольку 3 намного меньше 100. Если бы у нас было 6 значащих переменных, товероятность возросла бы до 46%. Однако проблема остаётся, если количествозначащих переменных мало. Позже мы изучим алгоритм, называющийся бустингом,который решает эту проблему.

Большим преимуществом метода случайного леса является то, что у него мало настраиваемых величин. Исследования показали, что можно позволить всем деревьям иметь произвольную глубину, не получая при этом большого увеличения ошибок. Метод случайного леса хорошо и быстро работает, что делает его идеальным решением для многих ситуаций. Именно поэтому когда мне встречаются люди, интересующиеся глубоким обучением, но представляющие его как некое готовое решение, которое просто можно запустить и работать, я, как правило, рекомендую использовать метод случайного леса. Нейронные же сети могут давать очень разные результаты в зависимости от заданных гиперпараметров и, к тому же, существует их множество вариаций. Так что метод случайного леса – отличный выбор, если вы ищете решение в стиле «запусти и работай».

Метод случайного леса. Регрессия

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

Набор данных, который мы используем, представляет собой данные о ценах на недвижимость, а целью будет спрогнозировать цену на дом с учётом ряда его свойств. Адрес, по которому можно загрузить данные, следующий:

https://archive.ics.uci.edu/ml/machine-learning-databases/housing/

Все файлы надо разместить в папку large_files, которая должна находиться по соседству с папкой курса. Интернет-адрес я включит в файл Python, так что набирать адрес вручную не надо. Соответствующий файл в репозитарии называется rf_regression.py. Я всё же настоятельно рекомендую попытаться написать код самостоятельно. На самом деле тут нет ничего нового: вы уже знаете, как пользоваться библиотекой Sci-Kit Learn, и знаете два способа работы с входящими признаками, когда они являются численными или категориальными. К счастью, наш набор данных очень прост в работе: все признаки численные, кроме одного – он двоичный, но уже закодирован в виде набора нулей и единиц, так что от вас не требуется ничего особенного. Кроме того, на этом занятии мы не будем заниматься ни конструированием, ни выбором признаков при рассмотрении данных, так как это уместнее при анализе данных.

Мыпросто пройдёмся по коду, а не будем его писать с нуля, поскольку это было бы чересчурутомительно.

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

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

from sklearn.preprocessing import LabelEncoder,StandardScaler

from sklearn.ensemble import RandomForestRegressor

from sklearn.linear_model import LinearRegression

from sklearn.tree import DecisionTreeRegressor

from sklearn.model_selection import cross_val_score

Далееу нас идёт несколько констант. У нас будет список столбцов чисел, которыенеобходимо будет нормализовать, чтобы получить среднее значение, равное нулю, идисперсию, равную единице. Единственный признак, который не будет подвергнутизменению, – river, поскольку он и так имеет значения либо 0, либо 1:

NUMERICAL_COLS = [

  ‘crim’, #numerical

  ‘zn’, #numerical

  ‘nonretail’, #numerical

  ‘nox’, #numerical

  ‘rooms’, #numerical

  ‘age’, #numerical

  ‘dis’, #numerical

  ‘rad’, #numerical

  ‘tax’, #numerical

  ‘ptratio’, #numerical

  ‘b’, #numerical

  ‘lstat’, #numerical

]

NO_TRANSFORM = [‘river’]

Далееопределяется наш класс DataTransformer дляпреобразования данных с функциями fit, transform fit_transform. Нам необходимо иметь возможностьпреобразовать данные без их подгонки, поскольку когда мы находим среднеезначение и дисперсию признака, нам нужно использовать их только для учебногонабора данных, чтобы на проверочном наборе пользоваться только уже вычисленнымисредними значениями и дисперсиями.

class DataTransformer:

  def fit(self, df):

    self.scalers= {}

    for col inNUMERICAL_COLS:

      scaler =StandardScaler()

      scaler.fit(df[col].as_matrix().reshape(-1,1))

     self.scalers[col] = scaler

  def transform(self, df):

    N, D =df.shape

    X =np.zeros((N, D))

    i = 0

    for col,scaler in self.scalers.iteritems():

      X[:,i] =scaler.transform(df[col].as_matrix().reshape(-1, 1)).flatten()

      i += 1

    for col inNO_TRANSFORM:

      X[:,i] =df[col]

      i += 1

    return X

  def fit_transform(self, df):

    self.fit(df)

    returnself.transform(df)

Следующейидёт функция для загрузки и преобразования данных. Данные имеют формат, оченьсхожий на CSV, с тем лишь исключением, что вместо запятых ониразделены произвольным количеством пробелов. К счастью, для решения такойпроблемы есть выражение sep=r”\s*”. Кроме того, файл данных не содержитназваний столбцов, их мы присвоили вручную.

def get_data():

  # regex allowsarbitrary number of spaces in separator

  df =pd.read_csv(‘../large_files/housing.data’, header=None,sep=r”\s*”, engine=’python’)

  df.columns = [

    ‘crim’, #numerical

    ‘zn’, #numerical

    ‘nonretail’,# numerical

    ‘river’, #binary

    ‘nox’, #numerical

    ‘rooms’, #numerical

    ‘age’, #numerical

    ‘dis’, #numerical

    ‘rad’, #numerical

    ‘tax’, #numerical

    ‘ptratio’, #numerical

    ‘b’, #numerical

    ‘lstat’, #numerical

    ‘medv’, #numerical — this is the target

  ]

Быстропросмотрим данные. Тут всё, что и следовало ожидать: уровень местнойпреступности, возраст домов, количество комнат в доме, величина налога на дом итак далее.

Следующимидёт создание экземпляра преобразователя данных и само преобразование данных.Данные разделяются так, чтобы 70% составлял учебный набор, а 30% – проверочный.Обратите внимание, что мы берём логарифм от целевых переменных (представляющихсобой средние цены на недвижимость). Это общеупотребимая операция длямасштабирования числовых столбцов с большим разбросом значений, но необходимовнимательно следить за корректностью соотношения со значением. Имеется в виду,что если цена дома равна 10 тысячам долларов, а вы ошиблись на 5 тысяч – этоочень грубая ошибка. Но если дом стоит миллион долларов, а вы ошиблись на те же5 тысяч, то тут нет ничего страшного.

  # transformthe data

  transformer =DataTransformer()

  # shuffle thedata

  N = len(df)

  train_idx =np.random.choice(N, size=int(0.7*N), replace=False)

  test_idx = [ifor i in xrange(N) if i not in train_idx]

  df_train =df.loc[train_idx]

  df_test =df.loc[test_idx]

  Xtrain =transformer.fit_transform(df_train)

  Ytrain =np.log(df_train[‘medv’].as_matrix())

  Xtest =transformer.transform(df_test)

  Ytest =np.log(df_test[‘medv’].as_matrix())

  return Xtrain, Ytrain, Xtest, Ytest

Наконец,мы переходим к главному разделу. Вначале создаётся случайный лес. Я использовал100 деревьев, но вы можете попробовать 10, 20, 200 или даже 500.

if __name__ == ‘__main__’:

  Xtrain,Ytrain, Xtest, Ytest = get_data()

  model =RandomForestRegressor(n_estimators=100)# try 10, 20, 50, 100, 200

 model.fit(Xtrain, Ytrain)

  predictions =model.predict(Xtest)

Далеемы получаем прогноз и можем вывести его на экран для сравнения с целевойпеременной. На первом графике целевые переменные идут вдоль оси x, а прогнозы –по оси y.Если прогнозы точны, то они должны быть рядом с прямой y = x, так что этапрямая также выводится на экран. После этого создаётся альтернативный графиктолько целевых переменных и прогнозов в виде линейных диаграмм.

  # plotpredictions vs targets

 plt.scatter(Ytest, predictions)

 plt.xlabel(“target”)

 plt.ylabel(“prediction”)

  ymin =np.round( min( min(Ytest), min(predictions) ) )

  ymax =np.ceil( max( max(Ytest), max(predictions) ) )

  print“ymin:”, ymin, “ymax:”, ymax

  r = range(int(ymin), int(ymax) + 1)

  plt.plot(r, r)

  plt.show()

 plt.plot(Ytest, label=’targets’)

  plt.plot(predictions,label=’predictions’)

  plt.legend()

  plt.show()

Инаконец мы проводим перекрёстную проверку всех наших моделей – выступающейэталоном линейной регрессии, единичного дерева решений и случайного леса. Крометого, мы выводим ошибку проверочного набора для всех трёх моделей.

  # do aquick baseline test

  baseline =LinearRegression()

  single_tree =DecisionTreeRegressor()

  print “CVsingle tree:”, cross_val_score(single_tree, Xtrain, Ytrain).mean()

  print “CVbaseline:”, cross_val_score(baseline, Xtrain, Ytrain).mean()

  print “CVforest:”, cross_val_score(model, Xtrain, Ytrain).mean()

  # test score

 single_tree.fit(Xtrain, Ytrain)

 baseline.fit(Xtrain, Ytrain)

  print“test score single tree:”, single_tree.score(Xtest, Ytest)

  print“test score baseline:”, baseline.score(Xtest, Ytest)

  print“test score forest:”, model.score(Xtest, Ytest)

Итак,запустим и посмотрим на результат.

Вначале видим график целевых переменных и прогнозов с прямой y = x. Выглядит довольно неплохо. Далее идут  линейные диаграммы целевых переменных и прогнозов и тоже выглядят весьма неплохо. Затем идут перекрёстная проверка и точность. Как видим, единичное дерево справилось довольно плохо – всего 61%, у эталонной регрессии 68%, а у случайного леса лучший результат – 80%. Та же картина и в случае проверочного набора: единичное дерево хуже всех с 67%, получше дела у линейной модели – 82%, а победителем становится случайный лес с 88%.

Метод случайного леса. Классификация

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

https://archive.ics.uci.edu/ml/ datasets/Musroom

Вновь-таки, файлы данных должны быть в папке large_files, а сама эта папка – быть рядом с папкой кода. Если вы не хотите писать код самостоятельно, то соответствующий файл в репозитарии называется rf_classification.py.

Есливы посмотрите на данные, то увидите, что они состоят из 22 столбцов,представляющих из себя атрибуты гриба, такие как размер, форма, цвет и всёостальное, описывающее его вид. Обратите внимание, что в описании указываетсяоб отсутствующих значениях, помеченных знаком вопроса. Применительно ккатегориальным переменным мы можем рассматривать вопросительный знак какдополнительный знак в этой категории. Это всё, что касается данных.

Вначалеидут импорты. Главное отличие тут в том, что теперь мы импортируемклассификаторы, а не регрессоры. Аналогом классификатора для линейной регрессиивыступает регрессия логистическая, поскольку они оба являются линейнымимоделями.

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

from sklearn.preprocessing import LabelEncoder,StandardScaler

from sklearn.tree import DecisionTreeClassifier

from sklearn.ensemble import RandomForestClassifier

from sklearn.linear_model import LogisticRegression

from sklearn.model_selection import cross_val_score

Всенаши столбцы представляют категории, числовых столбцов у нас нет. Напомню, чтоу нас нет строки заголовков, поэтому Pandas простопронумерует их: 0, 1, 2 и так далее. Поскольку столбец целевых переменных имеетномер 0, то первый признак будет иметь номер 1, а последний – 22. Мы можемлегко создать массив с этими числами, использовав функцию arrange библиотеки Numpyи добавив единицу.

NUMERICAL_COLS = ()

CATEGORICAL_COLS = np.arange(22) + 1 # 1..22 inclusive

Далееу нас идёт класс DataTransformer. Он очень похожна DataTransformer из предыдущей лекции с тем лишьисключением, что теперь нам нужно преобразовать столбцы категорий. В идеалеследовало вообще создать единый класс, который бы обрабатывал столбцы обоихтипов, но из лени я этого не сделал. По сути мы используем функцию StandardScaler библиотеки Sci-Kit Learnдля числовых столбцов и функцию LabelEncoder для столбцовкатегориальных. LabelEncoder пронумеруетметки от 0 до K – 1, что позже позволит нам использовать их вкачестве индексов.

# transforms data from dataframe to numerical matrix

# one-hot encodes categories and normalizes numericalcolumns

# we want to use the scales found in training whentransforming the test set

# so only call fit() once

# call transform() for any subsequent data

class DataTransformer:

  def fit(self, df):

   self.labelEncoders = {}

    self.scalers= {}

    for col inNUMERICAL_COLS:

      scaler =StandardScaler()

     scaler.fit(df[col].reshape(-1, 1))

     self.scalers[col] = scaler

    for col inCATEGORICAL_COLS:

      encoder =LabelEncoder()

      # in casethe train set does not have ‘missing’ value but test set does

      values =df[col].tolist()

     values.append(‘missing’)

     encoder.fit(values)

     self.labelEncoders[col] = encoder

    # finddimensionality

    self.D =len(NUMERICAL_COLS)

    for col,encoder in self.labelEncoders.iteritems():

      self.D +=len(encoder.classes_)

    print“dimensionality:”, self.D

  def transform(self, df):

    N, _ =df.shape

    X =np.zeros((N, self.D))

    i = 0

    for col,scaler in self.scalers.iteritems():

      X[:,i] =scaler.transform(df[col].as_matrix().reshape(-1, 1)).flatten()

      i += 1

    for col,encoder in self.labelEncoders.iteritems():

      # print“transforming col:”, col

      K = len(encoder.classes_)

     X[np.arange(N), encoder.transform(df[col]) + i] = 1

      i += K

    return X

  def fit_transform(self, df):

    self.fit(df)

    returnself.transform(df)

Далее у нас идёт функция replace_missing. Это из старой версии кода и на самом деле она не нужна для нашего набора данных, но может оказаться полезной при преобразовании данных из датафрейма Pandas, в котором есть нулевые значения. Я пользовался ею, когда тестировал различные наборы данных для данного курса.

def replace_missing(df):

  # standardmethod of replacement for numerical columns is median

  for col inNUMERICAL_COLS:

    ifnp.any(df[col].isnull()):

      med =np.median(df[ col ][ df[col].notnull() ])

      df.loc[df[col].isnull(), col ] = med

  # set aspecial value = ‘missing’

  for col inCATEGORICAL_COLS:

    ifnp.any(df[col].isnull()):

      print col

      df.loc[df[col].isnull(), col ] = ‘missing’

Следующейидёт функция get_data. Мы загружаемданные и преобразуем их с помощью DataTransformer, после чеговозвращаем X и Y.

def get_data():

  df =pd.read_csv(‘../large_files/mushroom.data’, header=None)

  # replacelabel column: e/p –> 0/1

  # e = edible =0, p = poisonous = 1

  df[0] =df.apply(lambda row: 0 if row[0] == ‘e’ else 1, axis=1)

  # check ifthere is missing data

 replace_missing(df)

  # transformthe data

  transformer =DataTransformer()

  X =transformer.fit_transform(df)

  Y =df[0].as_matrix()

  return X, Y

И последнее – функция main. Тут мы получаем данные и запускаем перекрёстную проверку на наших трёх моделях – эталонной, которой является логистическая регрессия, единичном дереве и случайном лесе.

if __name__ == ‘__main__’:

  X, Y =get_data()

  # do a quickbaseline test

  baseline =LogisticRegression()

  print “CVbaseline:”, cross_val_score(baseline, X, Y, cv=8).mean()

  # single tree

  tree =DecisionTreeClassifier()

  print “CVone tree:”, cross_val_score(tree, X, Y, cv=8).mean()

  model =RandomForestClassifier(n_estimators=20) # try 10, 20, 50, 100, 200

  print “CVforest:”, cross_val_score(model, X, Y, cv=8).mean()

Итак,запустим и посмотрим на результат.

Мывидим, что эталонная модель оказалась худшей – 92,7%, единичное дерево чутьлучше – 93%, а случайных лес оказывается победителем с почти 94%.

Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: