Введение
Генетический алгоритм, как он определен в Википедии, черпает вдохновение в процессе естественного отбора, предложенном Чарльзом Дарвином. В более общем плане мы можем понять естественный процесс и то, как он связан с генетическим алгоритмом, используя следующее описание:
Мы начнем с начальной популяции, которая будет иметь определенные черты, как показано на рисунке 1. Эта начальная популяция будет протестирована в определенной среде, чтобы наблюдать, насколько хорошо индивидуумы (родители) в этой популяции работают на основе заранее определенных критериев пригодности. Пригодность в случае машинного обучения может быть любой метрикой производительности - точностью, точностью, отзывчивостью, оценкой F1, auc и другими. Основываясь на значении приспособленности, мы выбираем наиболее эффективных родителей («выживаемость наиболее приспособленных») в качестве выжившей популяции (рис. 2).
Теперь родители в выжившей популяции будут спариваться, чтобы произвести потомство, используя комбинацию двух шагов: кроссовер / рекомбинация и мутация. В случае кроссовера гены (параметры) от спаривающихся родителей будут рекомбинированы, чтобы произвести потомство, при этом каждый ребенок унаследует некоторые гены (параметры) от каждого родителя (рисунок 3).
Наконец, в случае мутации некоторые значения генов (параметров) будут изменены для сохранения генетического разнообразия (Рисунок 4). Это позволяет природному / генетическому алгоритму обычно находить лучшее решение.
На рисунке 5 показано второе поколение населения, которое будет включать как выживших родителей, так и детей. Мы оставляем выживших родителей, чтобы сохранить наилучшие параметры приспособленности на случай, если показатели приспособленности потомства окажутся хуже, чем у родителей.
Модуль генетического алгоритма для XGBoost:
Мы создадим модуль генетического алгоритма, настроенный для XGBoost. Ниже приводится описание XGboost:
XGBoost - это оптимизированная библиотека с распределенным повышением градиента, которая отличается высокой эффективностью, гибкостью и переносимостью. В нем реализованы алгоритмы машинного обучения в рамках Gradient Boosting.
Модуль будет иметь функции для выполнения четырех шагов: (i) инициализация, (ii) выбор, (iii) кроссовер и (iv) мутация, аналогичные тому, что обсуждалось выше (небольшой раздел этого кода вдохновлен пост здесь).
Инициализация:
Первым шагом является инициализация, при которой параметры инициализируются случайным образом для создания популяции. Это похоже на первое поколение популяции, показанное на рисунке 1. Код ниже показывает процесс инициализации, в котором мы генерируем вектор, содержащий параметры. В случае XGBoost мы выбрали 7 параметров для оптимизации: learning_rate, n_estimators, max_depth, min_child_weight, subsample, colsample_bytree и gamma. Подробное описание этих параметров можно найти здесь.
def initilialize_poplulation(numberOfParents): learningRate = np.empty([numberOfParents, 1]) nEstimators = np.empty([numberOfParents, 1], dtype = np.uint8) maxDepth = np.empty([numberOfParents, 1], dtype = np.uint8) minChildWeight = np.empty([numberOfParents, 1]) gammaValue = np.empty([numberOfParents, 1]) subSample = np.empty([numberOfParents, 1]) colSampleByTree = np.empty([numberOfParents, 1]) for i in range(numberOfParents): print(i) learningRate[i] = round(random.uniform(0.01, 1), 2) nEstimators[i] = random.randrange(10, 1500, step = 25) maxDepth[i] = int(random.randrange(1, 10, step= 1)) minChildWeight[i] = round(random.uniform(0.01, 10.0), 2) gammaValue[i] = round(random.uniform(0.01, 10.0), 2) subSample[i] = round(random.uniform(0.01, 1.0), 2) colSampleByTree[i] = round(random.uniform(0.01, 1.0), 2) population = np.concatenate((learningRate, nEstimators, maxDepth, minChildWeight, gammaValue, subSample, colSampleByTree), axis= 1) return population
Пределы параметров основаны либо на пределах, описанных в документации XGBoost, либо на разумном предположении, если верхний предел установлен на бесконечность. Мы начинаем с создания пустого массива для каждого параметра, а затем заполняем его случайными значениями.
Выбор родителей (выживание наиболее приспособленных)
На втором этапе мы обучаем нашу модель, используя исходную популяцию, и вычисляем значение пригодности. В этом случае мы рассчитаем F1-рейтинг.
def fitness_f1score(y_true, y_pred): fitness = round((f1_score(y_true, y_pred, average='weighted')), 4) return fitness #train the data annd find fitness score def train_population(population, dMatrixTrain, dMatrixtest, y_test): fScore = [] for i in range(population.shape[0]): param = { 'objective':'binary:logistic', 'learning_rate': population[i][0], 'n_estimators': population[i][1], 'max_depth': int(population[i][2]), 'min_child_weight': population[i][3], 'gamma': population[i][4], 'subsample': population[i][5], 'colsample_bytree': population[i][6], 'seed': 24} num_round = 100 xgbT = xgb.train(param, dMatrixTrain, num_round) preds = xgbT.predict(dMatrixtest) preds = preds>0.5 fScore.append(fitness_f1score(y_test, preds)) return fScore
Мы определим, сколько родителей мы хотели бы выбрать, и создадим массив с выбранными родителями на основе их значения пригодности.
#select parents for mating def new_parents_selection(population, fitness, numParents): selectedParents = np.empty((numParents, population.shape[1])) #create an array to store fittest parents #find the top best performing parents for parentId in range(numParents): bestFitnessId = np.where(fitness == np.max(fitness)) bestFitnessId = bestFitnessId[0][0] selectedParents[parentId, :] = population[bestFitnessId, :] fitness[bestFitnessId] = -1 #set this value to negative, in case of F1-score, so this parent is not selected again return selectedParents
Кроссовер
Существуют различные методы определения кроссовера в случае генетических алгоритмов », такие как одноточечный, двухточечный и k-точечный кроссовер, равномерный кроссовер и кроссовер для упорядоченных списков. Мы собираемся использовать однородный кроссовер, где каждый параметр для ребенка будет независимо выбран от родителей на основе определенного распределения. В нашем случае мы будем использовать «дискретное равномерное распределение из numpy random function.
''' Mate these parents to create children having parameters from these parents (we are using uniform crossover method) ''' def crossover_uniform(parents, childrenSize): crossoverPointIndex = np.arange(0, np.uint8(childrenSize[1]), 1, dtype= np.uint8) #get all the index crossoverPointIndex1 = np.random.randint(0, np.uint8(childrenSize[1]), np.uint8(childrenSize[1]/2)) # select half of the indexes randomly crossoverPointIndex2 = np.array(list(set(crossoverPointIndex) - set(crossoverPointIndex1))) #select leftover indexes children = np.empty(childrenSize) ''' Create child by choosing parameters from two parents selected using new_parent_selection function. The parameter values will be picked from the indexes, which were randomly selected above. ''' for i in range(childrenSize[0]): #find parent 1 index parent1_index = i%parents.shape[0] #find parent 2 index parent2_index = (i+1)%parents.shape[0] #insert parameters based on random selected indexes in parent 1 children[i, crossoverPointIndex1] = parents[parent1_index, crossoverPointIndex1] #insert parameters based on random selected indexes in parent 1 children[i, crossoverPointIndex2] = parents[parent2_index, crossoverPointIndex2] return children
Мутация
Последним шагом будет привнесение разнообразия в детей путем случайного выбора одного из параметров и изменения его значения на случайную величину. Мы также введем некоторые ограничения, чтобы ограничить изменяемые значения в определенном диапазоне. Пропуск этих ограничений может привести к ошибке.
def mutation(crossover, numberOfParameters): #Define minimum and maximum values allowed for each parameter minMaxValue = np.zeros((numberOfParameters, 2)) minMaxValue[0:] = [0.01, 1.0] #min/max learning rate minMaxValue[1, :] = [10, 2000] #min/max n_estimator minMaxValue[2, :] = [1, 15] #min/max depth minMaxValue[3, :] = [0, 10.0] #min/max child_weight minMaxValue[4, :] = [0.01, 10.0] #min/max gamma minMaxValue[5, :] = [0.01, 1.0] #min/maxsubsample minMaxValue[6, :] = [0.01, 1.0] #min/maxcolsample_bytree # Mutation changes a single gene in each offspring randomly. mutationValue = 0 parameterSelect = np.random.randint(0, 7, 1) print(parameterSelect) if parameterSelect == 0: #learning_rate mutationValue = round(np.random.uniform(-0.5, 0.5), 2) if parameterSelect == 1: #n_estimators mutationValue = np.random.randint(-200, 200, 1) if parameterSelect == 2: #max_depth mutationValue = np.random.randint(-5, 5, 1) if parameterSelect == 3: #min_child_weight mutationValue = round(np.random.uniform(5, 5), 2) if parameterSelect == 4: #gamma mutationValue = round(np.random.uniform(-2, 2), 2) if parameterSelect == 5: #subsample mutationValue = round(np.random.uniform(-0.5, 0.5), 2) if parameterSelect == 6: #colsample mutationValue = round(np.random.uniform(-0.5, 0.5), 2) #indtroduce mutation by changing one parameter, and set to max or min if it goes out of range for idx in range(crossover.shape[0]): crossover[idx, parameterSelect] = crossover[idx, parameterSelect] + mutationValue if(crossover[idx, parameterSelect] > minMaxValue[parameterSelect, 1]): crossover[idx, parameterSelect] = minMaxValue[parameterSelect, 1] if(crossover[idx, parameterSelect] < minMaxValue[parameterSelect, 0]): crossover[idx, parameterSelect] = minMaxValue[parameterSelect, 0] return crossover
Реализация
Мы реализуем модуль, описанный выше, для обучения на наборе данных. Набор данных взят из Репозитория машинного обучения UCI. Он содержит набор из 102 молекул, 39 из которых идентифицированы людьми как обладающие запахом, который можно использовать в парфюмерии, а 69 не имеют желаемого запаха. Набор данных содержит 6 590 низкоэнергетических конформаций этих молекул, содержащих 166 характеристик. Мы делаем минимальные предубеждения как цель этого урока, чтобы понять генетический алгоритм.
# Importing the libraries import numpy as np import pandas as pd import geneticXGboost #this is the module we crated above import xgboost as xgb np.random.seed(723) # Importing the dataset dataset = pd.read_csv('clean2.data', header=None) X = dataset.iloc[:, 2:168].values #discard first two coloums as these are molecule's name and conformation's name y = dataset.iloc[:, 168].values #extrtact last coloum as class (1 => desired odor, 0 => undesired odor) # Splitting the dataset into the Training set and Test set from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20, random_state = 97) # Feature Scaling from sklearn.preprocessing import StandardScaler sc = StandardScaler() X_train = sc.fit_transform(X_train) X_test = sc.transform(X_test) #XGboost Classifier #model xgboost #use xgboost API now xgDMatrix = xgb.DMatrix(X_train, y_train) #create Dmatrix xgbDMatrixTest = xgb.DMatrix(X_test, y_test)
Для начала у нас есть 8 родителей, и мы выбираем 4 наиболее подходящих для спаривания родителей. Создадим 4 поколения и будем следить за фитнесом (F1-score). Половина родителей в следующем поколении будут наиболее подходящими родителями, выбранными из предыдущего поколения. Это позволит нам сохранить лучший показатель пригодности, по крайней мере, такой же, как у предыдущего поколения, в случае, если показатель пригодности детей будет хуже.
numberOfParents = 8 #number of parents to start numberOfParentsMating = 4 #number of parents that will mate numberOfParameters = 7 #number of parameters that will be optimized numberOfGenerations = 4 #number of genration that will be created #define the population size populationSize = (numberOfParents, numberOfParameters) #initialize the population with randomly generated parameters population = geneticXGboost.initilialize_poplulation(numberOfParents) #define an array to store the fitness hitory fitnessHistory = np.empty([numberOfGenerations+1, numberOfParents]) #define an array to store the value of each parameter for each parent and generation populationHistory = np.empty([(numberOfGenerations+1)*numberOfParents, numberOfParameters]) #insert the value of initial parameters in history populationHistory[0:numberOfParents, :] = population for generation in range(numberOfGenerations): print("This is number %s generation" % (generation)) #train the dataset and obtain fitness fitnessValue = geneticXGboost.train_population(population=population, dMatrixTrain=xgDMatrix, dMatrixtest=xgbDMatrixTest, y_test=y_test) fitnessHistory[generation, :] = fitnessValue #best score in the current iteration print('Best F1 score in the this iteration = {}'.format(np.max(fitnessHistory[generation, :]))) #survival of the fittest - take the top parents, based on the fitness value and number of parents needed to be selected parents = geneticXGboost.new_parents_selection(population=population, fitness=fitnessValue, numParents=numberOfParentsMating) #mate these parents to create children having parameters from these parents (we are using uniform crossover) children = geneticXGboost.crossover_uniform(parents=parents, childrenSize=(populationSize[0] - parents.shape[0], numberOfParameters)) #add mutation to create genetic diversity children_mutated = geneticXGboost.mutation(children, numberOfParameters) ''' We will create new population, which will contain parents that where selected previously based on the fitness score and rest of them will be children ''' population[0:parents.shape[0], :] = parents #fittest parents population[parents.shape[0]:, :] = children_mutated #children populationHistory[(generation+1)*numberOfParents : (generation+1)*numberOfParents+ numberOfParents , :] = population #srore parent information
Наконец, мы получаем лучший результат и соответствующие параметры:
#Best solution from the final iteration fitness = geneticXGboost.train_population(population=population, dMatrixTrain=xgDMatrix, dMatrixtest=xgbDMatrixTest, y_test=y_test) fitnessHistory[generation+1, :] = fitness #index of the best solution bestFitnessIndex = np.where(fitness == np.max(fitness))[0][0] #Best fitness print("Best fitness is =", fitness[bestFitnessIndex]) #Best parameters print("Best parameters are:") print('learning_rate', population[bestFitnessIndex][0]) print('n_estimators', population[bestFitnessIndex][1]) print('max_depth', int(population[bestFitnessIndex][2])) print('min_child_weight', population[bestFitnessIndex][3]) print('gamma', population[bestFitnessIndex][4]) print('subsample', population[bestFitnessIndex][5]) print('colsample_bytree', population[bestFitnessIndex][6])
Теперь давайте визуализируем изменение приспособленности популяции с каждым поколением (рисунок ниже). Хотя мы уже начали с высокого показателя F1 (~ 0,98), у двух родителей в случайно сгенерированной исходной популяции мы смогли улучшить его в последнем поколении. Самый низкий показатель F1 составил 0,9143 для одного родителя в исходной популяции, а лучший результат - 0,9947 для одного из родителей в последнем поколении. Это демонстрирует, что мы можем улучшить метрику производительности в XGBoost с помощью простой реализации генетического алгоритма. Окончательный код можно найти в моем аккаунте на github. Он также содержит код, который позволит вам наблюдать за изменением различных параметров с каждым поколением.