Вступление

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

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

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

Предположим, что есть 3 функции F1, F2 и F3, и каждая имеет 3 элемента функции. Таким образом, длина вектора признаков составляет 3x3 = 9. Выбор функций просто выбирает определенные типы функций и исключает другие. Например, просто выберите F1 и F2 и удалите F3. Длина вектора признаков теперь равна 6, а не 9. При уменьшении объектов отдельные элементы каждого объекта могут быть исключены. Например, на этом шаге можно удалить первый и третий элементы из F3, сохранив при этом второй элемент. Таким образом, длина вектора признаков сокращается с 9 до 7.

Прежде чем приступить к этому руководству, стоит упомянуть, что это расширение к ранее опубликованным 2 руководствам в моем профиле LinkedIn.

Первое руководство называется Реализация искусственной нейронной сети с использованием NumPy и классификация набора данных изображений Fruits360. Он начинается с извлечения вектора признаков длиной 360 из 4 классов набора данных Fruits360. Затем он создает искусственную нейронную сеть (ИНС) с использованием NumPy с нуля, чтобы классифицировать набор данных. Он доступен здесь https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad. Его проект GitHub доступен здесь: https://github.com/ahmedfgad/NumPyANN.

Второе руководство называется Оптимизация искусственных нейронных сетей с использованием генетического алгоритма. Он создает и использует GA для оптимизации параметров ИНС с целью повышения точности классификации. Он доступен здесь https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad. Его проект GitHub также доступен здесь: https://github.com/ahmedfgad/NeuralGenetic.

В этом руководстве обсуждается, как использовать генетический алгоритм (GA) для уменьшения вектора признаков, извлеченного из набора данных Fruits360 длиной 360. Это руководство начинается с обсуждения шагов, которые необходимо выполнить. После этого шаги реализуются на Python в основном с использованием NumPy и Sklearn.

Реализация этого руководства доступна на моей странице GitHub здесь: https://github.com/ahmedfgad/FeatureReductionGenetic

GA начинается с начальной популяции, которая состоит из ряда хромосом (то есть растворов), где каждая хромосома имеет последовательность генов. Используя фитнес-функцию, GA выбирает лучшие решения в качестве родителей для создания новой популяции. Новые решения в такой новой популяции создаются путем применения двух операций над родителями: кроссовера и мутации. Применяя ГА к данной проблеме, мы должны определить представление гена, подходящую функцию приспособленности и то, как применяются кроссовер и мутация. Посмотрим, как все работает.

Дополнительная информация о GA

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

  • Введение в оптимизацию с помощью генетического алгоритма

Https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad/

Https://www.kdnuggets.com/2018/03/introduction-optimization-with-genetic-algorithm.html

Https://towardsdatascience.com/introduction-to-optimization-with-genetic-algorithm-2f5001d9964b

  • Оптимизация генетического алгоритма (ГА) - Пошаговый пример

Https://www.slideshare.net/AhmedGadFCIT/genetic-algorithm-ga-optimization-stepbystep-example

  • Реализация генетического алгоритма в Python

Https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad/

Https://www.kdnuggets.com/2018/07/genetic-algorithm-implementation-python.html

Https://towardsdatascience.com/genetic-algorithm-implementation-in-python-5ab67bb124a6

Https://github.com/ahmedfgad/GeneticAlgorithmPython

Я также написал книгу в 2018 году, в одной из глав которой рассматривается GA. Книга цитируется как Ахмед Фаузи Гад« Практические приложения компьютерного зрения, использующие глубокое обучение с CNN . Dec. 2018, Apress, 978–1–4842–4167–7 », который доступен здесь, на Springer, https://www.springer.com/us/book/9781484241660 .

Представление хромосомы

Ген в GA является строительным блоком хромосомы. Для начала нам нужно определить, какие гены находятся внутри хромосомы. Для этого учтите, что каждое свойство, которое может повлиять на результаты, следует рассматривать как ген. Поскольку целью нашей проблемы является выбор наилучшего набора элементов функции, каждый элемент функции может повлиять на результаты, если он выбран или нет. Таким образом, каждый элемент признака рассматривается как ген. Хромосома будет состоять из всех генов (то есть всех элементов признаков). Поскольку существует 360 элементов признаков, то и генов будет 360. Теперь ясно, что длина хромосомы равна 360.

После определения выбранных генов следует определить репрезентативность генов. Существуют различные представления, такие как десятичное, двоичное, плавающее, строковое и другие. Наша цель - узнать, выбран ли ген (т.е.элемент признака) в сокращенном наборе признаков. Таким образом, значение, присвоенное гену, должно отражать, выбран он или нет. Основываясь на этом описании, становится ясно, что для каждого гена существует 2 возможных значения. Одно значение означает, что ген выбран, а другое - когда он не выбран. Таким образом, двоичное представление - лучший выбор. Когда значение гена равно 1, он будет выбран в сокращенном наборе функций. Если 0, то им пренебрегают.

Таким образом, хромосома будет состоять из 360 генов, представленных в двоичном формате. Как показано на следующем рисунке, существует взаимно однозначное соответствие между вектором признаков и хромосомой. То есть первый ген в хромосоме связан с первым элементом вектора признаков. Когда значение этого гена равно 1, это означает, что выбран первый элемент в векторе признаков.

Фитнес-функция

Узнав, как создать хромосому, начальная популяция может быть легко инициализирована случайным образом с помощью NumPy. После инициализации выбираются родители. GA основан на теории Дарвина "Выживание сильнейшего". То есть для спаривания выбираются лучшие решения в нынешней популяции, чтобы получить лучшие решения. Сохраняя хорошие решения и убивая плохие решения, мы можем достичь оптимального или полуоптимального решения.

Критерий, используемый для выбора родителей, - это значение приспособленности, связанное с каждым решением (т. Е. Хромосомой). Чем выше значение пригодности, тем лучше решение. Значение пригодности рассчитывается с использованием функции пригодности. Итак, какую функцию лучше всего использовать в нашей задаче? Целью нашей проблемы является создание сокращенного вектора признаков, который увеличивает точность классификации. Таким образом, критерием, определяющим, является ли решение хорошим или нет, является точность классификации. В результате функция пригодности вернет число, указывающее точность классификации для каждого решения. Чем выше точность, тем лучше решение.

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

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

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

Кроссовер и мутация

На основе функции приспособленности мы можем фильтровать решения в текущей популяции для выбора лучших из них, которые называются родителями. GA предполагает, что соединение двух хороших решений даст третье лучшее решение. Спаривание означает обмен некоторыми генами от 2 родителей. Обмен генами осуществляется с помощью операции кроссовера. Есть разные способы применения такой операции. В этом руководстве используется одноточечный кроссовер, при котором точка делит хромосому. Гены до точки берутся из одного раствора, а гены после точки - из другого.

Просто применяя кроссовер, все гены берутся от предыдущих родителей. У нового потомства нет нового гена. Если у всех родителей есть плохой ген, то этот ген будет передан потомству. По этой причине применяется операция мутации, чтобы ввести новые гены в потомство. В бинарном представлении генов мутация применяется путем переворота значений некоторых случайно выбранных генов. Если значение гена равно 1, то оно будет равно 0 и наоборот.

После создания потомства мы можем создать новую популяцию следующего поколения. Эта популяция состоит из предыдущих родителей в дополнение к потомству.

На этом этапе обсуждаются все шаги. Далее следует реализовать их на Python. Обратите внимание, что я написал предыдущее руководство под названием «Реализация генетического алгоритма в Python» для реализации GA на Python, в котором я просто изменю его код для работы с нашей проблемой. Лучше прочитать.

Реализация Python

Проект разбит на 2 файла. Один файл называется «GA.py», который содержит реализацию шагов GA как функций. Другой файл, который является основным файлом, просто импортирует этот файл и вызывает его функцию в цикле, который проходит через поколения.

Основной файл начинается с чтения функций, извлеченных из набора данных Fruits360, в соответствии с приведенным ниже кодом. Функции возвращаются в переменную data_inputs. Подробные сведения об извлечении этих функций доступны в 2 учебных пособиях, упомянутых в начале учебного пособия. Файл также считывает метки классов, связанные с образцами в переменной data_outputs.

Некоторые образцы выбираются для обучения, их индексы хранятся в переменной train_indices. Точно так же индексы тестовых выборок хранятся в переменной test_indices.

import numpy
import GA
import pickle
import matplotlib.pyplot

f = open("dataset_features.pkl", "rb")
data_inputs = pickle.load(f)
f.close()

f = open("outputs.pkl", "rb")
data_outputs = pickle.load(f)
f.close()

num_samples = data_inputs.shape[0]
num_feature_elements = data_inputs.shape[1]
train_indices = numpy.arange(1, num_samples, 4)
test_indices = numpy.arange(0, num_samples, 4)
print("Number of training samples: ", train_indices.shape[0])
print("Number of test samples: ", test_indices.shape[0])

"""
Genetic algorithm parameters:
    Population size
    Mating pool size
    Number of mutations
"""

sol_per_pop = 8 # Population size.
num_parents_mating = 4 # Number of parents inside the mating pool.
num_mutations = 3 # Number of elements to mutate.
# Defining the population shape.
pop_shape = (sol_per_pop, num_feature_elements)
 
# Creating the initial population.
new_population = numpy.random.randint(low=0, high=2, size=pop_shape)
print(new_population.shape)
 
best_outputs = []
num_generations = 100

Он инициализирует все параметры GA. Это включает количество решений на популяцию, которое установлено равным 8 в соответствии с переменной sol_per_pop, количество потомков, которое установлено равным 4 в переменной num_parents_mating, и количество мутаций. который имеет значение 3 в переменной num_mutations. После этого он случайным образом создает начальную популяцию в переменной new_population.

Есть пустой список с именем best_outputs, в котором содержится лучший результат после каждого поколения. Это помогает визуализировать прогресс ГА после завершения всех поколений. Количество поколений установлено на 100 в переменной num_generations. Обратите внимание, что вы можете изменить все эти параметры, что может дать лучшие результаты.

После подготовки функций, меток классов и параметров GA мы можем пройти итерации GA в соответствии со следующим кодом. Сначала значение пригодности для всех решений вычисляется путем вызова функции пригодности с именем cal_pop_fitness (), определенной в файле GA. Эта функция принимает текущую популяцию, извлеченные объекты, метки классов, индексы поездов и индексы тестов. Функция возвращает значение пригодности для всех решений в переменной с именем пригодность. Помните, что значение пригодности представляет точность классификации. Наивысшая (т. Е. Самая высокая) точность классификации сохраняется в списке best_outputs.

На основе рассчитанных значений пригодности лучшие решения с наивысшей точностью классификации выбираются в качестве родителей в пуле сопряжения с помощью функции select_mating_pool (), определенной в файле GA.py. Он принимает текущую популяцию, значения пригодности и количество возвращаемых родителей. Он возвращает выбранных родителей в переменную parent.

for generation in range(num_generations):
    print("Generation : ", generation)
    # Measuring the fitness of each chromosome in the population.
    fitness = GA.cal_pop_fitness(new_population, data_inputs, data_outputs, train_indices, test_indices)

    best_outputs.append(numpy.max(fitness))
    # The best result in the current iteration.
    print("Best result : ", best_outputs[-1])

    # Selecting the best parents in the population for mating.
    parents = GA.select_mating_pool(new_population, fitness, num_parents_mating)

    # Generating next generation using crossover.
    offspring_crossover = GA.crossover(parents, offspring_size=(pop_shape[0]-parents.shape[0], num_feature_elements))

    # Adding some variations to the offspring using mutation.
    offspring_mutation = GA.mutation(offspring_crossover, num_mutations=num_mutations)

    # Creating the new population based on the parents and offspring.
    new_population[0:parents.shape[0], :] = parents
    new_population[parents.shape[0]:, :] = offspring_mutation

Далее следует применить операцию кроссовера к выбранным родителям для создания потомства. Это делается внутри функции crossover (), определенной в файле GA.py. Он принимает родителей и форму массива потомков, которые позже будут возвращены в переменную offspring_crossover. Затем к этому массиву применяется операция мутации с помощью функции mutation (), которая также доступна в файле GA.py. В дополнение к результатам кроссовера эта функция принимает количество мутаций.

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

После завершения всех поколений выполняется следующий код, чтобы вернуть наилучший выбранный набор элементов функции и количество выбранных элементов. После завершения 100 поколений алгоритм использовал 174 элемента признаков для достижения точности 99,59%.

fitness = GA.cal_pop_fitness(new_population, data_inputs, data_outputs, train_indices, test_indices)

# Then return the index of that solution corresponding to the best fitness.
best_match_idx = numpy.where(fitness == numpy.max(fitness))[0]
best_match_idx = best_match_idx[0]

best_solution = new_population[best_match_idx, :]
best_solution_indices = numpy.where(best_solution == 1)[0]
best_solution_num_elements = best_solution_indices.shape[0]
best_solution_fitness = fitness[best_match_idx]

print("best_match_idx : ", best_match_idx)
print("best_solution : ", best_solution)
print("Selected indices : ", best_solution_indices)
print("Number of selected elements : ", best_solution_num_elements)
print("Best solution fitness : ", best_solution_fitness)

matplotlib.pyplot.plot(best_outputs)
matplotlib.pyplot.xlabel("Iteration")
matplotlib.pyplot.ylabel("Fitness")
matplotlib.pyplot.show()

В приведенном выше коде также отображается рисунок, показывающий прогресс GA по всем поколениям, который показан ниже.

Вот полный код основного файла.

import numpy
import GA
import pickle
import matplotlib.pyplot

f = open("dataset_features.pkl", "rb")
data_inputs = pickle.load(f)
f.close()

f = open("outputs.pkl", "rb")
data_outputs = pickle.load(f)
f.close()

num_samples = data_inputs.shape[0]
num_feature_elements = data_inputs.shape[1]

train_indices = numpy.arange(1, num_samples, 4)
test_indices = numpy.arange(0, num_samples, 4)
print("Number of training samples: ", train_indices.shape[0])
print("Number of test samples: ", test_indices.shape[0])

"""
Genetic algorithm parameters:
    Population size
    Mating pool size
    Number of mutations
"""
sol_per_pop = 8 # Population size.
num_parents_mating = 4 # Number of parents inside the mating pool.
num_mutations = 3 # Number of elements to mutate.

# Defining the population shape.
pop_shape = (sol_per_pop, num_feature_elements)

# Creating the initial population.
new_population = numpy.random.randint(low=0, high=2, size=pop_shape)
print(new_population.shape)

best_outputs = []
num_generations = 100
for generation in range(num_generations):
    print("Generation : ", generation)
    # Measuring the fitness of each chromosome in the population.
    fitness = GA.cal_pop_fitness(new_population, data_inputs, data_outputs, train_indices, test_indices)

    best_outputs.append(numpy.max(fitness))
    # The best result in the current iteration.
    print("Best result : ", best_outputs[-1])

    # Selecting the best parents in the population for mating.
    parents = GA.select_mating_pool(new_population, fitness, num_parents_mating)

    # Generating next generation using crossover.
    offspring_crossover = GA.crossover(parents, offspring_size=(pop_shape[0]-parents.shape[0], num_feature_elements))

    # Adding some variations to the offspring using mutation.
    offspring_mutation = GA.mutation(offspring_crossover, num_mutations=num_mutations)

    # Creating the new population based on the parents and offspring.
    new_population[0:parents.shape[0], :] = parents
    new_population[parents.shape[0]:, :] = offspring_mutation

# Getting the best solution after iterating finishing all generations.
# At first, the fitness is calculated for each solution in the final generation.
fitness = GA.cal_pop_fitness(new_population, data_inputs, data_outputs, train_indices, test_indices)
# Then return the index of that solution corresponding to the best fitness.
best_match_idx = numpy.where(fitness == numpy.max(fitness))[0]
best_match_idx = best_match_idx[0]

best_solution = new_population[best_match_idx, :]
best_solution_indices = numpy.where(best_solution == 1)[0]
best_solution_num_elements = best_solution_indices.shape[0]
best_solution_fitness = fitness[best_match_idx]

print("best_match_idx : ", best_match_idx)
print("best_solution : ", best_solution)
print("Selected indices : ", best_solution_indices)
print("Number of selected elements : ", best_solution_num_elements)
print("Best solution fitness : ", best_solution_fitness)

matplotlib.pyplot.plot(best_outputs)
matplotlib.pyplot.xlabel("Iteration")
matplotlib.pyplot.ylabel("Fitness")
matplotlib.pyplot.show()

Реализация GA.py

Реализация файла GA.py указана ниже. В функции cal_pop_fitness () SVC обучается в соответствии с выбранными элементами функции каждым решением. Перед обучением функции фильтруются в соответствии с выбранными элементами, генам которых присваивается значение 1. Это делается внутри функции reduce_features (). Он принимает текущее решение в дополнение к полным функциям для всех образцов.

После обучения точность классификации рассчитывается с помощью функции classification_accuracy (). Эта функция возвращает точность, которая хранится в массиве с именем accuracies в функции cal_pop_fitness ().

Реализация функций crossover () и mutation () очень похожа на то, что обсуждалось в моем предыдущем руководстве под названием «Реализация генетического алгоритма в Python ». Одно из основных отличий заключается в том, что функция mutation () изменяет случайно выбранные гены, переворачивая их значения, потому что мы используем двоичное представление.

import numpy
import sklearn.svm


def reduce_features(solution, features):
    selected_elements_indices = numpy.where(solution == 1)[0]
    reduced_features = features[:, selected_elements_indices]
    return reduced_features


def classification_accuracy(labels, predictions):
    correct = numpy.where(labels == predictions)[0]
    accuracy = correct.shape[0]/labels.shape[0]
    return accuracy


def cal_pop_fitness(pop, features, labels, train_indices, test_indices):
    accuracies = numpy.zeros(pop.shape[0])
    idx = 0

    for curr_solution in pop:
        reduced_features = reduce_features(curr_solution, features)
        train_data = reduced_features[train_indices, :]
        test_data = reduced_features[test_indices, :]

        train_labels = labels[train_indices]
        test_labels = labels[test_indices]

        SV_classifier = sklearn.svm.SVC(gamma='scale')
        SV_classifier.fit(X=train_data, y=train_labels)

        predictions = SV_classifier.predict(test_data)
        accuracies[idx] = classification_accuracy(test_labels, predictions)
        idx = idx + 1
    return accuracies

def select_mating_pool(pop, fitness, num_parents):
    # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation.
    parents = numpy.empty((num_parents, pop.shape[1]))
    for parent_num in range(num_parents):
        max_fitness_idx = numpy.where(fitness == numpy.max(fitness))
        max_fitness_idx = max_fitness_idx[0][0]
        parents[parent_num, :] = pop[max_fitness_idx, :]
        fitness[max_fitness_idx] = -99999999999
    return parents


def crossover(parents, offspring_size):
    offspring = numpy.empty(offspring_size)
    # The point at which crossover takes place between two parents. Usually, it is at the center.
    crossover_point = numpy.uint8(offspring_size[1]/2)

    for k in range(offspring_size[0]):
        # Index of the first parent to mate.
        parent1_idx = k%parents.shape[0]
        # Index of the second parent to mate.
        parent2_idx = (k+1)%parents.shape[0]
        # The new offspring will have its first half of its genes taken from the first parent.
        offspring[k, 0:crossover_point] = parents[parent1_idx, 0:crossover_point]
        # The new offspring will have its second half of its genes taken from the second parent.
        offspring[k, crossover_point:] = parents[parent2_idx, crossover_point:]
    return offspring


def mutation(offspring_crossover, num_mutations=2):
    mutation_idx = numpy.random.randint(low=0, high=offspring_crossover.shape[1], size=num_mutations)
    # Mutation changes a single gene in each offspring randomly.
    for idx in range(offspring_crossover.shape[0]):
        # The random value to be added to the gene.
        offspring_crossover[idx, mutation_idx] = 1 - offspring_crossover[idx, mutation_idx]
    return offspring_crossover

Для связи с автором