[Из песочницы] Нейросети: реализация задачи про грибы на Tensor Flow и Python

habr.png

Tensor Flow — фреймворк для построения и работы с нейросетями от компании Google. Позволяет абстрагироваться от внутренних деталей машинного обучения и сосредоточиться непосредственно на решении своей задачи. Очень мощная вещь, позволяет создавать, обучать и использовать нейронные сети любого известного типа. Не нашел на Хабре ни одного толкового текста на эту тему, поэтому пишу свой. Ниже будет описана реализация решения задачи про грибы с помощью библиотеки Tensor Flow. Кстати, алгоритм, описанный ниже, подходит для предсказаний практически в любой области. Например, вероятности рака у человека в будущем или карт у соперника в покере.

Задача


Суть задачи: на основе входных параметров гриба определить его съедобность. Специфика в том, что параметры эти категорийные, а не числовые. Например, параметр «форма шляпки» может иметь значение «плоская» или «выпуклая» или «конусообразная». Набор данных грибов для обучения сети взяты из репозитория машинного обучения. Таким образом, решение задачи можно назвать своеобразным Hello World в области машинного обучения, наряду с задачей про ирисы, где параметры цветка выражены цифровыми значениями.

Исходники


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

Подготовка


Предполагается, что у вас есть готовая установка Tensor Flow. Если нет, установить можно по ссылке.

Исходник

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import tensorflow as tf
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

import os

# Функция отвечает за подготовку данных из репозитория.
# Результатом работы являются два CSV-файла с подогнанными под Tensor Flow данными для обучения и тестирования нейронной сети. В частности, категорийные параметры грибов конвертируются в числовые (0 и 1)
def prepare_data(data_file_name):
header = ['class', 'cap_shape', 'cap_surface', # Шапка CSV-файла в виде массива, сформирована на основе файла 'agaricus-lepiota.name' из репозитория
'cap_color', 'bruises', 'odor', 'gill_attachment',
'gill_spacing', 'gill_size', 'gill_color', 'stalk_shape',
'stalk_root', 'stalk_surface_above_ring',
'stalk_surface_below_ring', 'stalk_color_above_ring',
'stalk_color_below_ring', 'veil_type', 'veil_color',
'ring_number', 'ring_type', 'spore_print_color',
'population', 'habitat']
df = pd.read_csv(data_file_name, sep=',', names=header)

# Записи с "?" вместо параметра символизируют его отсутствие
# выбрасываем эти записи из нашего набора данных
df.replace('?', np.nan, inplace=True)
df.dropna(inplace=True)

# Съедобность или ядовитость обозначаются в нашем наборе данных
# символами 'e' или 'p' соответственно. Необходимо представить эти данные в числовом
# виде, поэтому делаем 0 вместо ядовитого, 1 - вместо съедобного значения
df['class'].replace('p', 0, inplace=True)
df['class'].replace('e', 1, inplace=True)

# Изначально параметры грибов представлены в символьном виде,
# то есть в виде слов. Tensor Flow может работать только с цифровыми
# данными. Библиотека Pandas с помощью функции "get_dummies"
# конвертирует наши данные в цифры
cols_to_transform = header[1:]
df = pd.get_dummies(df, columns=cols_to_transform)

# Теперь надо разделить конвертированные данные
# на два набора - один для тренировки (большой)
# и один для тестирования нейросети (поменьше)
df_train, df_test = train_test_split(df, test_size=0.1)

# Определяем количество строк и столбцов в каждом из наборов данных
num_train_entries = df_train.shape[0]
num_train_features = df_train.shape[1] - 1

num_test_entries = df_test.shape[0]
num_test_features = df_test.shape[1] - 1

# Итоговые наборы записываем во временные csv-файлы, т.к.
# необходимо записать количества столбцов и строк в начало шапки
# рабочих csv, как того требует Tensor Flow
df_train.to_csv('train_temp.csv', index=False)
df_test.to_csv('test_temp.csv', index=False)

# Пишем количества в тренировочный файл, затем в тестовый
open("mushroom_train.csv", "w").write(str(num_train_entries) +
"," + str(num_train_features) +
"," + open("train_temp.csv").read())

open("mushroom_test.csv", "w").write(str(num_test_entries) +
"," + str(num_test_features) +
"," + open("test_temp.csv").read())

# Удаляем временные файлы, они больше не нужны
os.remove("train_temp.csv")
os.remove("test_temp.csv")

# Функция формирует входные данные для тестирования для Tensor Flow
def get_test_inputs():
x = tf.constant(test_set.data)
y = tf.constant(test_set.target)

return x, y

# Функция формирует входные данные для тренировки для Tensor Flow
def get_train_inputs():
x = tf.constant(training_set.data)
y = tf.constant(training_set.target)

return x, y

# Функция возвращает данные двух пробных грибов для
# предсказания их съедобности (ожидаемый результат: съедобен, ядовит)
# Иными словами, это функция для проверки обученной и протестированной нейросети
def new_samples():
return np.array([[0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0,
1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0,
0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0,
0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0,
0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1,
0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0,
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0,
0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0,
0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 1]], dtype=np.int)

if __name__ == "__main__":
MUSHROOM_DATA_FILE = "agaricus-lepiota.data"

# Подготавливаем данные грибов для Tensor Flow,
# создав два CSV-файла (тренировка и тест)
prepare_data(MUSHROOM_DATA_FILE)

# Загружаем подготовленные данные
training_set = tf.contrib.learn.datasets.base.load_csv_with_header(
filename='mushroom_train.csv',
target_dtype=np.int,
features_dtype=np.int,
target_column=0)

test_set = tf.contrib.learn.datasets.base.load_csv_with_header(
filename='mushroom_test.csv',
target_dtype=np.int,
features_dtype=np.int,
target_column=0)

# Определяем, что все параметры цветов имеют реальные значения (подробнее ниже)
feature_columns = [tf.contrib.layers.real_valued_column("", dimension=98)]

# Создаем трехслойную DNN-нейросеть с 10, 20 и 10 нейронами в слое
classifier = tf.contrib.learn.DNNClassifier(
feature_columns=feature_columns,
hidden_units=[10, 20, 10],
n_classes=2,
model_dir="/tmp/mushroom_model")

# Тренируем нейросеть
classifier.fit(input_fn=get_train_inputs, steps=2000)

# Нормализуем нейросеть с помощью тестового набора данных
accuracy_score = classifier.evaluate(input_fn=get_test_inputs,
steps=1)["accuracy"]

print("\nТочность предсказаний: {0:f}\n".format(accuracy_score))

# Пробуем запустить нейросеть на двух наших пробных грибах
predictions = list(classifier.predict_classes(input_fn=new_samples))

print("Предсказания съедобности пробных грибов: {}\n"
.format(predictions))


Загружаем и подготавливаем данные из репозитория


Данные для тренировки и тестирования нейросети будем загружать из специально созданного для этого репозитория машинного обучения. Все данные представлены в виде двух файлов: agaricus-lepiota.data и agaricus-lepiota.names. В первом 8124 строки и 22 колонки. Одна строка предоставляет один гриб, каждая колонка — один из 22-х параметров гриба в виде символа-сокращения от целого слова-параметра. Легенда всех символов находится в файле agarius-lepiota.names.

Данные из репозитория необходимо обработать, дабы привести к приемлемому для Tensor Flow виду. Вначале импортируем несколько библиотек для работы

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import tensorflow as tf
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

import os


Затем сформируем шапку из параметров гриба для Tensor Flow, чтобы библиотека знала, какой столбец в файле данных какому параметру соответствует. Шапка склеивается с файлом данных. Формируем в виде массива, элементы которого берем из файла agaricus-lepiota.names.

header = ['class', 'cap_shape', 'cap_surface',
          'cap_color', 'bruises', 'odor', 'gill_attachment',
          'gill_spacing', 'gill_size', 'gill_color', 'stalk_shape',
          'stalk_root', 'stalk_surface_above_ring',
          'stalk_surface_below_ring', 'stalk_color_above_ring',
          'stalk_color_below_ring', 'veil_type', 'veil_color',
          'ring_number', 'ring_type', 'spore_print_color',
          'population', 'habitat']
df = pd.read_csv(data_file_name, sep=',', names=header)


Теперь нужно разобраться с отсутствующими данными. В этом случае в файле agaricus-lepiota.data вместо параметра выставлен символ »?». Есть много методов обработки подобных случаев, мы же будем просто удалять всю строку с хотя бы одним отсутствующим параметром.

df.replace('?', np.nan, inplace=True)
df.dropna(inplace=True)


Далее необходимо вручную заменить символьный параметр съедобности на цифровой. То есть «p» и «e» заменить на 0 и 1.

df['class'].replace('p', 0, inplace=True)
df['class'].replace('e', 1, inplace=True)


А уже после этого можно конвертировать остатки данных в цифру. Этим занимается функция get_dummies библиотеки pandas.

cols_to_transform = header[1:]
df = pd.get_dummies(df, columns=cols_to_transform)


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

df_train, df_test = train_test_split(df, test_size=0.1)


И последнее. Tensor Flow требует, чтобы в начале файлов данных были обозначены количество строк и столбцов файла. Мы вручную извлечем эту информацию из наших тренировочного и калибровочного наборов данных и затем запишем в итоговые CSV-файлы.

# Определяем количество столбцов и строк в каждом наборе
num_train_entries = df_train.shape[0]
num_train_features = df_train.shape[1] - 1

num_test_entries = df_test.shape[0]
num_test_features = df_test.shape[1] - 1

# Пишем наборы во временные CSV
df_train.to_csv('train_temp.csv', index=False)
df_test.to_csv('test_temp.csv', index=False)

# Пишем полученные выше количества в итоговый CSV, затем туда же пишем временные
open("mushroom_train.csv", "w").write(str(num_train_entries) +
                                      "," + str(num_train_features) +
                                      "," + open("train_temp.csv").read())

open("mushroom_test.csv", "w").write(str(num_test_entries) +
                                     "," + str(num_test_features) +
                                     "," + open("test_temp.csv").read())


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

Забрасываем сформированные данные в Tensor Flow


Теперь, когда мы загрузили из репозитория и обработали CSV-файлы с данными грибов, можно отдать их в Tensor Flow на обучение. Это делается с помощью функции load_csv_with_header (), предоставляемой самим фрейворком:

training_set = tf.contrib.learn.datasets.base.load_csv_with_header(
    filename='mushroom_train.csv',
    target_dtype=np.int,
    features_dtype=np.int,
    target_column=0)

test_set = tf.contrib.learn.datasets.base.load_csv_with_header(
    filename='mushroom_test.csv',
    target_dtype=np.int,
    features_dtype=np.int,
    target_column=0)


Функция load_csv_with_header () занимается формированием тренировочного набора данных из тех файлов, что мы собрали выше. Помимо файла с данными в качестве аргумента функция принимает target_dtype, что является типом предсказываемых в итоге данных. В нашем случае необходимо научить нейросеть предсказывать съедобность либо ядовитость гриба, что можно выразить значениями 1 или 0. Таким образом, в нашем случае target_dtype — целое значение (integer). features_dtype — параметр, где задается тип принимаемых на обучение параметров. В нашем случае это также integer (изначально были string, но, как вы помните, мы перегнали их в цифру). В конце задается параметр target_column, что есть индекс колонки с параметром, который нейросети предстоит предсказывать. То есть с параметром съедобности.

Создаем объект классификатора Tensor Flow


То есть объект класса, который занимается непосредственно предсказаниями результата. Иными словами, класса самой нейросети.

feature_columns = [tf.contrib.layers.real_valued_column("", dimension=98)]

classifier = tf.contrib.learn.DNNClassifier(
    feature_columns=feature_columns,
    hidden_units=[10, 20, 10],
    n_classes=2,
    model_dir="/tmp/mushroom_model")


Первый параметр — feature_columns. Это параметры грибов. Обратите внимание, что значение параметра создается тут же, чуть выше. Там на вход принимается значение 98 параметра dimension, что значит 98 различных параметров гриба, за исключением съедобности.

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

n_classes — количество классов для предсказания. У нас их два — съедобный и нет.

model_dir — путь, куда будет сохранена натренированная модель нейросети. И в будущем будет использоваться для предсказания результатов, дабы не обучать сеть каждый раз.

Тренировка


Для простоты работы в будущем, создадим две функции:

def get_test_inputs():
  x = tf.constant(test_set.data)
  y = tf.constant(test_set.target)

  return x, y

def get_train_inputs():
  x = tf.constant(training_set.data)
  y = tf.constant(training_set.target)

  return x, y


Каждая функция выдает свой набор входных данных — для тренировки и для калибровки. x и y — константы Tensor Flow, которые необходимы фреймвору для работы. Не вдавайтесь в подробности, просто примите, что эти функции должны быть как посредник между данными и нейросетью.

Тренируем сеть:

classifier.fit(input_fn=get_train_inputs, steps=2000)


Первый параметр принимает сформированные чуть выше входные данные, второй — количество шагов тренировки. Опять же, цифра использовалась в одном из мануалов Tensor Flow, и понимание данной настройки придет к вам с опытом.

Далее калибруем натренированную сеть. Это делается с помощью сформированного выше калибровочного набора данных. Результатом работы будет точность будущих предсказаний сети (accuracy_score).

accuracy_score = classifier.evaluate(input_fn=get_test_inputs,
                                     steps=1)["accuracy"]

print("\nТочность предсказаний: {0:f}\n".format(accuracy_score))


Опробуем в деле


Сейчас нейросеть готова, и можно попробовать предсказать с её помощью съедобность гриба.

def new_samples():
    return np.array([[0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0,
                      1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0,
                      0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0,
                      0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0,
                      0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
                      0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1,
                      0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                      1, 0, 1, 0, 0, 0, 0],
                     [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0,
                      0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
                      0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0,
                      0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0,
                      0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
                      0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0,
                      0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1,
                      0, 0, 0, 0, 0, 0, 1]], dtype=np.int)


Функция выше отдает данные двух совершенно новых грибов, не присутствовавших ни в тренировочном, ни в калибровочном наборах (на самом деле они просто были вытащены из последнего). Представьте, например, что вы купили их на рынке, и пытаетесь понять, можно ли их есть. Код ниже определит это:

predictions = list(classifier.predict(input_fn=new_samples))
print("Предсказания съедобности пробных грибов:    {}\n"
      .format(predictions))


Результатом работы должно быть следующее:

Предсказания съедобности пробных грибов:    [0, 1]


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

© Habrahabr.ru