Что такое «Машинное обучение»

Цель машинного обучения — предсказать результат по входным данным без явного программирования всей логики. Алгоритмы обучения (learning algorithms) делают предсказания или принимают решения не на основе строго статических программных команд, а на основе обучающей выборки (т.е. обучающих данных)

Машинное обучение vs глубокое обучение

Искусственный интеллект — название всей области, как биология или химия.

Машинное обучение — это раздел искусственного интеллекта. Важный, но не единственный.

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

Глубокое обучение — архитектура нейросетей, один из подходов к их построению и обучению. На практике сегодня мало кто отличает, где глубокие нейросети, а где не очень. Говорят название конкретной сети и всё.

Решаемые при помощи ML задачи

Методы машинного обучения разделяются по типам решаемых задач. При этом ещё и возникают новые типы задач и даже целые новые дисциплины машинного обучения, например, добыча данных (data mining).

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

Кластеризация (сегментация) – разделение множества объектов или ситуаций на кластеры с помощью обучения без учителя. Кластеризация (обучение без учителя) отличается от классификации (обучения с учителем) тем, что перечень групп четко не задан и определяется в процессе работы алгоритма, т.е. нет заранее определённых «правильных» ответов.

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

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

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

Восстановление плотности распределения вероятности по набору данных (kernel density estimate). Данная задача является центральной проблемой математической статистики. Математическая статистика решает обратные задачи: по результату эксперимента определяет свойства закона распределения. Исчерпывающей характеристикой закона распределения является плотность распределения вероятностей. Например, известен возраст людей, берущих кредит в банке, требуется найти плотность распределения вероятности возрастов заёмщиков.

Понижение размерности данных и их визуализация. Является частным случаем кластеризации. Каждый объект может быть представлен в виде многомерного вектора признаков, но нужно получить более компактное признаковое описание объекта. Понижение размерности может помочь другим методам путём устранения избыточных данных. Используется при разведочном анализе и для устранения «проклятия размерности», когда данные быстро становятся разреженными при увеличении размерности пространства признаков. Например, дан список документов на человеческом языке, требуется найти документы с похожими темами.

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

Построение ранговых зависимостей. Ранжирование – это процедура упорядочения объектов по степени выраженности какого-либо качества в порядке убывания этого качества. Задачами ранжирования являются: сортировка веб-страниц согласно заданному поисковому запросу, персонализация новостной ленты, рекомендации товаров (видео, музыки), адресная реклама.

Добыча данных (data mining) – совокупность методов обнаружения в данных ранее неизвестных, нетривиальных знаний, необходимых для принятия решений в различных сферах человеческой деятельности. Интеллектуальный анализ данных и машинное обучение имеют различные цели: машинное обучение прогнозирует на основе известных свойств, полученных от обучающей выборки, а интеллектуальный анализ данных фокусируется на добыче новых ранее неизвестных зависимостей в данных.

Виды машинного обучения

Данные, признаки, алгоритмы

Итак, если мы хотим обучить машину, нам нужны три вещи:

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

Данные собирают как могут. Кто-то вручную — получается дольше, меньше, зато без ошибок. Кто-то автоматически — просто сливает машине всё, что нашлось, и верит в лучшее. Самые хитрые, типа гугла, используют своих же пользователей для бесплатной разметки. Вспомните ReCaptcha, которая иногда требует «найти на фотографии все дорожные знаки» — это оно и есть.

За хорошими наборами данных (датасетами) идёт большая охота. Крупные компании, бывает, раскрывают свои алгоритмы, но датасеты — крайне редко.

2. Признаки. Мы называем их фичами (features). Фичи, свойства, характеристики, признаки — ими могут быть пробег автомобиля, пол пользователя, цена акций, даже счетчик частоты появления слова в тексте может быть фичей.

Машина должна знать, на что ей конкретно смотреть. Хорошо, когда данные просто лежат в табличках — названия их колонок и есть фичи. А если у нас сто гигабайт картинок с котами? Когда признаков много, модель работает медленно и неэффективно. Зачастую отбор правильных фич занимает больше времени, чем всё остальное обучение. Но бывают и обратные ситуации, когда кожаный мешок сам решает отобрать только «правильные» на его взгляд признаки и вносит в модель субъективность — она начинает дико врать.

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

Искусственные нейронные сети

Термин «машинное обучение» в 1959 году ввёл исследователь в области компьютерных игр Артур Самуэль и определил его как «процесс, в результате которого машина (компьютер) способна показывать поведение, которое в неё не было явно заложено (запрограммировано)». Игру в шашки, изобретенную Самуэлем в 1952 году, принято считать первой программой, способной самообучаться. Самуэль выбрал шашки, потому что правила игры относительно просты, но имеют развитую стратегию.

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

Первой попыткой построить ИНС по принципу функционирования мозга были нейронные сети Уоррена Мак-Каллока и Уолтера Питтса, описанные в статье 1943 года. Это была яркая идея, учитывая то, что электрическая природа сигналов нейронов будет продемонстрирована только спустя семь лет в конце 1950-х годов. Математическая модель сети Мак-Каллока и Питтса из искусственных нейронов теоретически могла выполнять числовые или логические операции любой сложности.

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

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

Связи — это каналы, через которые нейроны шлют друг другу циферки. У каждой связи есть свой вес — её единственный параметр, который можно условно представить как прочность связи. Когда через связь с весом 0.5 проходит число 10, оно превращается в 5. Сам нейрон не разбирается, что к нему пришло и суммирует всё подряд — вот веса и нужны, чтобы управлять на какие входы нейрон должен реагировать, а на какие нет.

Чтобы сеть не превратилась в анархию, нейроны решили связывать не как захочется, а по слоям. Внутри одного слоя нейроны никак не связаны, но соединены с нейронами следующего и предыдущего слоя. Данные в такой сети идут строго в одном направлении — от входов первого слоя к выходам последнего. Такие сети называются feed forward

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

Вот как это выглядит в действии для управления змейкой:

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

Когда мы построили сеть, наша задача правильно расставить веса, чтобы нейроны реагировали на нужные сигналы. Тут нужно вспомнить, что у нас же есть данные — примеры «входов» и правильных «выходов». Будем показывать нейросети рисунок той же цифры 4 и говорить «подстрой свои веса так, чтобы на твоём выходе при таком входе всегда загоралась четвёрка».

Сначала все веса просто расставлены случайно, мы показываем сети цифру, она выдаёт какой-то случайный ответ (весов-то нет), а мы сравниваем, насколько результат отличается от нужного нам. Затем идём по сети в обратном направлении, от выходов ко входам, и говорим каждому нейрону — так, ты вот тут зачем-то активировался, из-за тебя всё пошло не так, давай ты будешь чуть меньше реагировать на вот эту связь и чуть больше на вон ту, ок?

Через тысяч сто таких циклов «прогнали-проверили-наказали» есть надежда, что веса в сети откорректируются так, как мы хотели. Научно этот подход называется Backpropagation или «Метод обратного распространения ошибки». Забавно то, что чтобы открыть этот метод понадобилось двадцать лет. До него нейросети обучали как могли.

Генетический алгоритм для обучения

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

Итак, нейронная сеть у нас задаётся кучей чисел. Выпишем их в ряд и назовём геномом. Для каждой сети мы должны уметь оценить числом, «насколько она хороша». Это число часто называется fitness. Теперь шаги алгоритма.

  1. Создать популяцию из случайных сетей;
  2. Посчитать «годность» каждой сети;
  3. Проверить, не подходят ли по качеству эти лучшие?
  4. Добавить в новое поколение:
    • Несколько самых годных сетей;
    • Несколько мутантов, полученных из самых годных;
    • Несколько сетей, полученных «скрещиванием» (crossover);
    • Ещё какой-нибудь магии;
  5. Повторить п.2.

Если бы коэффициентов было всего два, то можно было бы нарисовать трёхмерный график зависимости годности от параметров. Результат работы генетического алгоритма выглядел бы как-то так:

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

Часть первая: сеть из одного нейрона и её обвязка

Нейронная сеть из одного нейрона может научиться делать линейную регрессию — находить такую зависимость $y = ax + b$, которая лучше всего описывает данные. Так как формула для перевода градусов Цельсия в Фаренгейты имеет в точности такой вид, то одиночный нейрон должен отлично учиться это делать.

ai2.svg

Чтобы обучать и использовать нейронные сети, нужно сначала подготовить вспомогательные функции. Общий план работ:

ai1.svg

(если пишете на другом языке, то меняйте всё в соответствии с языком)

A: Цельсии в Фаренгейты

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

0
32.0
100
212.0

B: Создание дата-сета

Напишите функцию gen_dataset, с единственным параметром: size — размер обучающей выборки. Функция должна возвращать два массива, которые традиционно называются x_train и y_train.
x_train — набор случайных чисел на отрезке от −100 до +400 (random.uniform поможет) длины size.
y_train — набор «ответов» (результатов перевод из Цельсия в Фаренгейты) для каждого вопроса.

5
-6.478113785946121 -68.95986937659728 159.64353217340033 388.95577315676417 347.81609825396555 20.339395185296983 -92.1277648778751 319.3583579121206 732.1203916821755 658.0689768571381

Обратите внимание, данные у вас будут другими.

C: Средний квадрат ошибки

Напишите функцию mean_squared_error, которая берёт список правильных ответов y_true и список предсказаний y_pred и по ним вычисляет средний квадрат ошибки. Если мы обозначим правильные ответы через $t_1, t_2, \ldots, t_n$, а предсказания — через $p_1, p_2, \ldots, p_n$, то функция должна вернуть $$ MSE = \displaystyle\frac{\sum\limits_{i=1}^n (t_i - p_i)^2}{n}=\displaystyle\frac{(t_1-p_1)^2+(t_2-p_2)^2+\ldots+(t_n-p_n)^2}{n}. $$

3 1 2 3 1 2 3
0.0
3 1 2 3 2 3 4
1.0
5 1 2 3 4 5 1 2 3 4 10
5.0

D: Класс Network: конструктор и вывод

Создайте класс Network с констуктором и фукцией для вывода.
В питоне это «волшебные» методы __init__ и __repr__.
В C++ это констуктор и перегрузка оператора ostream& operator<<.
В javascript конструктор и метод toString.
В rust конструктор — это реализация метода pub fn new, а вывод — реализация трейта std::fmt::Display.

Если параметры для создания сети не переданы, то в качестве параметров должны быть выбраны случайные числа на отрезке $[-1, 1]$.

На вход даётся либо два числа, либо строка random. Создайте экземляр сети либо с данными параметрами, либо со случайными.

1.0 3.25
Network(1.0, 3.25)
random
Network(-0.3437820465576418, 0.038800142646235214)

В случае использования rust'а выводите Network::new(...).

E: Предсказание

ai2.svg

Реализуйте метод predict. Так как нейрон всего один и функция активации у него тождественная, то нейронная сеть должна на входе $x$ выдавать $ax + b$, где $a$ и $b$ — коэффициенты нейронной сети.

1.0 2.0 3.0
5.0
2.0 2.0 3.0
8.0

F: Оценка

Реализуйте метод score для оценки сети. Метод принимает на вход список запросов x_test, список правильных ответов y_true. Для каждого запроса необходимо «предсказать» ответ. После чего вернуть средний квадрат ошибки.

В первой строчке даются два действительных числа — параметры сети.
Во второй — число запросов.
В третьей — список запросов.
В четвёртой — список ответов.

1.0 0.0 3 1 2 3 1 2 3
0.0
1.0 2.0 4 1 2 3 4 3 4 5 7
0.25

G: Мутация

Реализуйте метод mutate, который создаёт новую сеть из исходной при помощи мутации. У метода должно быть два параметра: $p$ — вероятность мутации, $mx$ — максимальная величина мутации.

Метод должен создать новую нейронную сеть, каждый коэффициент которой с вероятностью $p$ изменён на случайное число от $-mx$ до $mx$.

В первой строчке параметры сети.
Во второй строчке три числа: вероятность $0\le p \le 1$, максимальное изменение $-1000\le mx \le 1000$ и необходимое число клонов.

1.0 2.0 0.5 0.01 5
Network(1.0081921165106547, 2.0) Network(0.9973208355732132, 1.998181015625907) Network(1.0, 2.002416305815798) Network(1.0, 2.0) Network(1.0003493138064643, 2.005287480453127)
1.0 2.0 0.99 100.0 3
Network(4.56527977787448, 0.31026039390799554) Network(66.22912000489308, 78.86699848962598) Network(28.722950305358324, 63.49145034955487)

H: Запуск эволюции

Реализуйте функцию run_evolution, которая будет управлять эволюцией.

Параметры функции:

ai3.svg

На вход в первой строчке даётся число $n$ — количество поколений, которые нужно провести.
Во второй строчке два числа — параметры мутаций: вероятность $0\le p \le 1$, максимальное изменение $-1000\le mx \le 1000$.
Для того, чтобы сделать нечто с вероятностью $p$ достаточно сравнить случайное число от 0 до 1 с $p$ (см. функцию random.random).
Создайте популяцию из 50 случайных сетей, тестовые данные из 10 запросов (см. задачу B, gen_dataset) и выполните $n$ поколений отбора. Выведите лучшую из сетей и её MSE (средний квадрат ошибки) в каждом из поколений.

1 1.0 1000.0
Network(0.9209812607434618, 0.004457841667385054) 60050.485193485896
5 0.75 1.0
Network(0.846402684824314, 0.5640911800707655) 60345.476849672836 Network(1.7785262443910892, 0.6971271696678896) 1263.6352302427074 Network(1.9316575092014656, -0.670957630683588) 317.4379044459942 Network(1.9317971144130943, 1.2715962180671463) 293.30423493888253 Network(1.9317971144130943, 1.7776168523048028) 288.211923299147
1000 0.5 0.1
Network(0.995873805302049, -0.8297230643290996) 54319.17510829984 Network(1.0908930247662008, -0.755077045805747) 43691.45122015884 ... Network(1.80004394800788, 31.99013125313795) 2.5350237282543833e-05 Network(1.80004394800788, 31.99013125313795) 2.5350237282543833e-05

I: Линейная регрессия

На вход даются два числа $n$ и $k$: число запросов для обучения и число запросов для тестирования. Затем идёт строка из $n$ входов и строка из $n$ ответов к ним. После этого идёт строка из $k$ тестов. Обучите нейронную сеть в течение 2 секунды и предскажите ответы для каждого из тестов.

Отсечку по времени можно делать так:

from time import time
start = time()
while time() - start < 1:
   ...

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

10 4 -100 -55 -11 33 77 122 166 211 255 300 -148.0 -67.0 12.2 91.4 170.6 251.6 330.8 411.8 491.0 572.0 -18 0 36 100
-0.3999963705134988 32.000003104792846 96.80000205540554 212.0000001898281

Часть вторая: сети прямого распространения сигнала

Для упрощения модели в сетях прямого распространения сигнала (feed forward) нейроны связывают по слоям. Внутри одного слоя нейроны никак не связаны, но соединены с нейронами следующего и предыдущего слоя. Данные в такой сети идут строго в одном направлении — от входов первого слоя к выходам последнего.

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

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

В атрибуте layer_sizes будем хранить число входов/нейронов/выходов в каждом слое. Например, для сети из одного входа и одного нейрона размеры слоёв — layer_sizes = [1, 1]. Для сети на картинке выше layer_sizes = [3, 4, 2]. Таким образом у нашей сети число входов всегда равно layer_sizes[0], а число выходов — layer_sizes[-1].

Итак, структуру слоёв храним в атрибуте layer_sizes, а сами коэффициенты — в атрибуте coefs. Для примера пронумеруем все коэффициенты сети со слоями layer_sizes = [3, 2, 3]:

Если слоёв больше, то будем продолжать их нумеровать в том же духе:

Пусть layer_sizes$ = [l_1, l_2, \ldots, l_n]$. Посчитаем, сколько нам нужно коэффициентов для нашей сети. Слоёв в нашей сети $n$, а переходов из слоя в слой — $n-1$. Если в одном слое $a$ входов/нейронов, а в следующем — $b$, то для каждого из $b$ нейронов требуется $a$ весов и 1 число для смещения. То есть получается $(a+1)b$ коэффициентов. Для всей сети тогда потребуется $$ (l_1+1)l_2+(l_2+1)l+\ldots+(l_{n-1}+1)l_n. $$

Новая задача

В качестве примера мы будем переводить сразу две температуры в градусах Цельсия в Фаренгейты. При этом будем обучать сеть со структурой layer_sizes = [2, 3, 2] — это больше, чем требуется для этой задачи. Но сеть-то про это не знает!

Необходимые доработки

Чтобы начать работать с такой универсальной структурой сети нам нужно выполнить следующие доработки:

B2: Создание дата-сета — 2

Напишите функцию gen_dataset, с двумя параметрами: размер обучающей выборки и количество температур в одном запросе. Функция должна возвращать два массива, которые традиционно называются x_train и y_train.
x_train — набор массивов случайных чисел на отрезке от −100 до +400 (random.uniform поможет) длины size.
y_train — набор массивов «ответов» (результатов перевод из Цельсия в Фаренгейты) для каждого вопроса.

Примените вашу функцию и выведите результат её работы в соответствии с примером ниже:

2 3
x_train [350.7507043201898, -0.9110215131114785, 312.3285675971266] [-70.70846364118368, 158.33238402140466, 121.93515254939194] y_train [663.3512677763417, 30.36016127639934, 594.1914216748279] [-95.27523455413063, 316.9982912385284, 251.4832745889055]

Обратите внимание, данные у вас будут другими. В C++ и rust можете выводить без скобок и пробелов (350.75 -0.91 312.32).

C2: Средний квадрат ошибки — 2

Напишите функцию mean_squared_error, которая берёт список правильных ответов y_true и список предсказаний y_pred и по ним вычисляет средний квадрат ошибки. Если мы обозначим правильные ответы через $$[t_{11},t_{12},\ldots,t_{1k}], [t_{21},t_{22},\ldots,t_{2k}], \ldots, [t_{n1},t_{n2},\ldots,t_{nk}],$$ а предсказания — через $$[p_{11},p_{12},\ldots,p_{1k}], [p_{21},p_{22},\ldots,p_{2k}], \ldots, [p_{n1},p_{n2},\ldots,p_{nk}],$$ то функция должна вернуть $$ MSE = \displaystyle\frac{\sum\limits_{i=1}^n (t_{i1} - p_{i1})^2 + (t_{i2} - p_{i2})^2 + \ldots + (t_{ik} - p_{ik})^2}{n}. $$

В первой стоке дан размер выборки и количество чисел в каждом запросе. Остальное смотрите из примера.

2 3 y_true 1 2 3 2 3 4 y_pred 1 2 3 2 3 4
0.0
2 3 y_true 1 2 3 2 3 4 y_pred 2 3 4 3 4 5
3.0
1 5 y_true 1 2 3 4 5 y_pred 1 2 3 4 10
25.0

D2: Класс Network: конструктор и вывод — 2

Поправьте класс Network с констуктором и фукцией для вывода.

На вход даётся число — количество слоёв с учётом входов. Затем — количество входов/нейронов в этих слоях. Создайте экземляр сети со случайными параметрами.

2 1 1
Network(layer_sizes=[1, 1], coefs=[0.5593072612849401, -0.21349956473615594])
3 2 3 2
Network(layer_sizes=[2, 3, 2], coefs=[0.9388365735531314, 0.9330665502448439, 0.515214611232337, 0.18587585063618928, 0.6834441819730446, -0.0032976444457140097, -0.8668989278688017, 0.6074629431219982, 0.9764584835243215, -0.2491933393348964, -0.5680809944167209, 0.2779504167888651, 0.8791321805561028, -0.785045887754154, 0.4289298878133563, -0.1869204112854117, -0.7418060634600554])

В случае использования С++ выводите в виде Network({1, 2, 3}, {1.0, 2.0, 3.0}).

В случае использования rust'а выводите в виде Network::new(vec![1, 2, 3], vec![1.0, 2.0, 3.0]).

В случае хранения коэффициентов не в плоской структуре, выводите не в плоской (coefs=[[1,2,3],[3,4,5]]). Если используете несколько атрибутов, выводите несколько. Используете numpy? numpy.array([]) — тоже пойдёт. Главное — число весов должно быть правильным.

E2: Предсказание — 2

Реализуйте метод predict, принимающий на вход список/кортеж чисел, и вычисляющий на этом входе ответ нейронной сети.

В первой количество слоёв с учётом входов.
Во второй — количество входов/нейронов в этих слоях.
В третьей — параметры сети. Гарантируется, что их ровно столько, сколько нужно $(l_1+1)l_2+(l_2+1)l+\ldots+(l_{n-1}+1)l_n$.
В четвёртой — данные на вход. Гарантируется, что их ровно столько, сколько чисел в нулевом слое.

2 2 2 1 2 3 4 5 6 100 10000
20103 50406
3 3 2 3 1 0 0 0 0 0 1 0 1 0 0 0 0 57 0 1 0 179 57 2020
179 57 2020

Для второго теста веса и процесс вычисления на картинке выше. Эта сеть на входе $[x_1, x_2, x_3]$ всегда выдаёт $[x_1, 57, x_3]$.

ai = Network([2, 2], coefs=[1, 2, 3, 4, 5, 6])
print(ai)
print(ai.predict([100, 10000]))
# Network(layer_sizes=[2, 2], coefs=[1, 2, 3, 4, 5, 6])
# [20103, 50406]
ai = Network([3, 2, 3], coefs=[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 57, 0, 1, 0])
print(ai)
print(ai.predict([179, 57, 2020]))
# Network(layer_sizes=[3, 2, 3], coefs=[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 57, 0, 1, 0])
# [179, 57, 2020]
Подсказка
Да чёт сложно...
  1. Текущий набор значений сделать равным входу
  2. Индекс в списке коэффициентов сделать 0
  3. Для каждой пары ($p$, $q$) размеров соседних слоёв:
  4. —Создать список для нового набора длины $q$
  5. —Для каждого из $q$ чисел нового набора:
  6. ——Просуммировать произведения $p$ чисел предыдущего набора и очередного веса;
  7. ——Добавить смещение — просто очередной вес;
  8. ——Добавить полученную сумму в список для нового набора;
  9. —Сделать новый набор значений чисел текущим
  10. Вернуть последний набор в качестве ответа.

F2: Оценка — 2

Реализуйте метод score для оценки сети. Метод принимает на вход список запросов x_test, список правильных ответов y_true. Для каждого запроса необходимо «предсказать» ответ. После чего вернуть средний квадрат ошибки.

В первой количество слоёв с учётом входов.
Во второй — количество входов/нейронов в этих слоях.
В третьей — параметры сети. Гарантируется, что их ровно столько, сколько нужно $(l_1+1)l_2+(l_2+1)l+\ldots+(l_{n-1}+1)l_n$.
Во четвёртой — число запросов.
Затем строка x_test и сами запросы.
Затем строка y_true и сами ответы.

2 2 2 1 2 3 4 5 6 2 x_test 1 0 0 1 y_true 4 10 5 11
0.0
3 3 2 3 1 0 0 0 0 0 1 0 1 0 0 0 0 57 0 1 0 3 x_test 179 57 2020 180 58 2021 181 59 2022 y_true 179 57 2020 180 58 2021 181 59 2022
1.6666666666666667

G2: Мутация — 2

Реализуйте метод mutate, который создаёт новую сеть из исходной при помощи мутации. У метода должно быть два параметра: $p$ — вероятность мутации, $mx$ — максимальная величина мутации.

Метод должен создать новую нейронную сеть, каждый коэффициент которой с вероятностью $p$ изменён на случайное число от $-mx$ до $mx$.

В первой количество слоёв с учётом входов.
Во второй — количество входов/нейронов в этих слоях.
В третьей — параметры сети. Гарантируется, что их ровно столько, сколько нужно $(l_1+1)l_2+(l_2+1)l+\ldots+(l_{n-1}+1)l_n$.
В четвёртой — три числа: вероятность $0\le p \le 1$, максимальное изменение $-1000\le mx \le 1000$ и необходимое число клонов.

2 2 2 1 2 3 4 5 6 0.167 0.0001 5
Network(layer_sizes=[2, 2], coefs=[1, 2, 3, 3.999961644219391, 5, 6]) Network(layer_sizes=[2, 2], coefs=[1, 2, 2.999958203178873, 4, 5, 6]) Network(layer_sizes=[2, 2], coefs=[1, 2.000050068821061, 3, 3.9999462287830347, 5, 6.000086378096772]) Network(layer_sizes=[2, 2], coefs=[1, 2, 3, 4.000036830155414, 5, 6]) Network(layer_sizes=[2, 2], coefs=[0.9999126556811868, 2, 3, 4, 5, 6.000067302518694])
2 2 2 1 2 3 4 5 6 1.0 10000 1
Network(layer_sizes=[2, 2], coefs=[-9895.078036268365, -731.9142314969595, -357.31738931927794, -1652.4098933000168, 3770.14610508668, 5046.554839261091])

При использовании другого языка/другой структуры/numpy и т.п. выводите в удобном формате. Проверяются только веса.

PyPy

В целом язык python достаточно быстрый. Но когда дело доходит до того, чтобы «перемолоть» много миллиардов небольших чисел, то оказывается, что не достаточно. В таких случаях реализация на C++ оказывается в 50-200 раз быстрее :(

Python — это язык. Эталонной реализацией Python является интерпретатор CPython, поддерживающий большинство активно используемых платформ. Однако есть и другие реализации. Одна из них — PyPy. В ней питоновский код транслируются в Си и компилируется. За счёт этого теряется часть возможностей CPython, зато числомололки начинают работать в 10-20 раз быстрее. Текущий листок — как раз числомололки. Чтобы нейронные сети тренировались быстрее, очень рекомендуется для этого листка использовать PyPy.

Со страницы http://pypy.org/download.html скачайте интерпретатор из раздела в духе «Python 3.6 compatible PyPy3.6 v7.3.0» для вашей операционной системы. Скачанный архив нужно куда-нибудь разархивировать. После чего настроить PyCharm на использование этого интерпретатора. Для этого вы идёте в File->Settings...->Project КакНазвали->Project Interpreter. Там справа от выбранного «Project Interpreter» есть шестерёнка, после нажатия на которую нужно выбрать «Add...» В новом окошке слева выбираете «System Interpreter», потом справа — многоточие для выбора пути внучную. Теперь нужно найти папку, в которую вы разархивировали PyPy, и выбрать в ней файл pypy3.exe. После этого вы сможете выбрать PyPy как интерпретатор для вашего проекта.

Если всё пошло по плану, то, во-первых, долгое обучение должно стать раз в 10-20 быстрее. А во-вторых первая строка вывода консоли будет содержать pypy3.6-v7.1.1-win32\pypy3.exe — ваш интерпретатор.

H2: Запуск эволюции — 2

Реализуйте функцию run_evolution, которая будет управлять эволюцией.

Параметры функции:

На вход в первой строчке даётся число $n$ — количество поколений, которые нужно провести.
Во второй строчке два числа — параметры мутаций: вероятность $0\le p \le 1$, максимальное изменение $-1000\le mx \le 1000$.
Создайте популяцию из 50 случайных сетей со структурой $[2, 3, 2]$, тестовые данные из 10 запросов, в каждом по 2 температуры (см. задачу B1 , gen_dataset) и выполните $n$ поколений отбора. В каждом поколении выведите лучшую из сетей и её MSE (средний квадрат ошибки) на следующей строчке.

1 1.0 1000.0
Network(layer_sizes=[2, 3, 2], coefs=[0.9456, 0.571994, 0.147098, 0.868726, 0.576262, 0.17558, 0.982867, 0.67302, 0.377256, 0.125932, 0.873431, 0.0182968, -0.496357, 0.972813, 0.321328, 0.141057, -0.219958]) 71268.45764378546
3 0.3 1.0
Network(layer_sizes=[2, 3, 2], coefs=[-0.688652, 0.959441, 0.0642981, -0.627895, 0.987505, 0.518048, -0.591327, -0.633911, -0.342515, 0.428231, -0.083056, -0.752163, -0.867829, -0.0802127, 0.827957, -0.824213, 0.679725]) 70491.30094494004 Network(layer_sizes=[2, 3, 2], coefs=[-0.6133, 0.959441, 0.0642981, -0.627895, 1.02095, 0.518048, -0.64377, -0.633911, -0.342515, 0.344315, -0.120291, -0.752163, -0.867829, -0.0688561, 0.827957, -0.824213, 0.679725]) 60600.66649569578 Network(layer_sizes=[2, 3, 2], coefs=[-0.557871, 0.959441, 0.103136, -0.627895, 1.02095, 0.518048, -0.594342, -0.720994, -0.420186, 0.399548, -0.120291, -0.814297, -0.867829, -0.101714, 0.827957, -0.860567, 0.679725]) 51327.893309360305
2000 0.17 0.1
Network(layer_sizes=[2, 3, 2], coefs=[0.12142, 0.958708, 0.509871, -0.251813, -0.437596, 0.901329, 0.819683, 0.892718, -0.331174, 0.318049, 0.7462, 0.848256, 0.979218, 0.145761, -0.819722, 0.727628, 0.668259]) 27693.429386247713 ... Network(layer_sizes=[2, 3, 2], coefs=[-0.339687, 0.601551, 6.65224, 0.657973, -0.865249, 0.078044, 0.902559, 0.954937, 19.803, 0.0180095, 1.2317, 1.10456, 9.83016, 0.359039, -0.932743, 0.814535, 13.4161]) 0.016733833832104167

I2: Линейная регрессия — 2

В этой задаче мы будем тренировать сеть со слоями [3, 1] выполнять линейную регрессию. У нас 3 входа, обозначим из через $x, y, z$. Будем искать такое выражение $ax+by+cz+d$, которое «лучше всего» описывает данные.

На вход даются два числа $n$ и $k$: число запросов для обучения и число запросов для тестирования.
Затем идёт строка x_train, после которой $n$ строк по 3 числа в каждом.
Затем идёт строка y_train, после которой $n$ строк по 1 числу в каждой (ответы).
Затем идёт строка x_test, после которой $k$ строк по 3 числа в каждом.

Обучите нейронную сеть в течение 2 секунды и предскажите ответы для каждого из тестов.

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

4 2 x_train 0 0 0 1 0 0 0 1 0 0 0 1 y_train 0 1 1 1 x_test 1 2 3 1 -1 0
6.0 0.0

Переобучение и недообучение

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

Недообучение (underfitting) – явление, при котором ошибка обученной модели оказывается слишком большой. Недообучение возникает при использовании слишком простых моделей, как правило, с малым количеством нейронов и синапсов, либо при прекращении процесса обучения до достижения состояния с достаточно малой ошибкой, либо при неудачной обучающей выборке.

Причины переобучения разнообразные:

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

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

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

Поэтому имеющиеся данные всегда делят на две части: x_train и x_test (с ответами y_train и y_test). Для обучения используют только x_train. Для оценки качества получившейся модели используют только x_test.

Функция активации

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

В реальности используется много разных функций активации, но мы будем использовать только линейный выпрямитель — ReLU, Rectified linear unit. $$ReLU(x) = max(x, 0) = \begin{cases} 0, & \text{если } x < 0 \\ x, & \text{если } x\ge 0 \end{cases}$$

Можно доказать (универсальная теорема аппроксимации), что нейронная сеть с одным скрытым слоем и функцией активации ReLU может с любой точность приблизить любое непрерывное отображение (многомерную функцию) из фиксированного ограниченного множества в фиксированное ограниченное множество. Например, непрерывные функции от 10 переменных $-1000 \le x_i \le 1000$ с 5 значениями $-10000 \le y_j \le 10000$. Только потребуется очень много нейронов :) Эту теорему (с некоторым оговорками) вы и сами можете доказать для случая, когда у сети один выходной нейрон. Нужно начать с того, чтобы получить произвольную букву П с немного покатыми краями.

B3: Создание дата-сета — 3

Итак, без тестовых данных никто не обучает. Поэтому будем делать тестовые данные. Чтобы было проще сравнивать успехи разных сетей, в качестве тестовых данных всегда будем использовать арифметическую прогрессию от -100 до 400.

Напишите функцию gen_dataset с тремя параметрами: размер обучающей выборки $n$, размер тестовой выборки $k$ и количество температур в одном запросе $t$. Функция должна возвращать четыре массива, которые традиционно называются x_train, y_train и x_test и y_test.
x_train — набор массивов случайных чисел на отрезке от −100 до +400 длины n (по $t$ чисел в каждом).
y_train — набор массивов «ответов» (результатов перевод из Цельсия в Фаренгейты) для каждого вопроса.
x_test — набор массивов $t$ одинаковых чисел, представляющих из себя арифметическую прогрессию от −100 до +400 длины $k$.
y_test — набор массивов «ответов» (результатов перевод из Цельсия в Фаренгейты) для каждого вопроса.

Примените вашу функцию и выведите результат её работы в соответствии с примером ниже (квадратные скобки и запятые выводить по желанию):

2 3 4
x_train [372.676, -63.19, 204.251, 90.976] [141.076, 141.278, 395.324, 160.186] y_train [702.8168, -81.742, 399.6518, 195.7568] [285.9368, 286.30039999999997, 743.5832, 320.3348] x_test [-100.0, -100.0, -100.0, -100.0] [150.0, 150.0, 150.0, 150.0] [400.0, 400.0, 400.0, 400.0] y_test [-148.0, -148.0, -148.0, -148.0] [302.0, 302.0, 302.0, 302.0] [752.0, 752.0, 752.0, 752.0]

Обратите внимание, данные у вас будут другими.

E3: Предсказание — 3

В метод predict (см. E2) добавьте использование функции активации ReLU в каждый нейрон, кроме последнего выходного слоя.

2 2 2 1 2 3 4 5 6 10 100
213 546
2 2 2 1 2 3 4 5 6 -10 -100
-207 -534
3 2 2 2 1 0 0 0 1 0 1 2 3 4 5 6 10 100
213 546
3 2 2 2 1 0 0 0 1 0 1 2 3 4 5 6 -10 -100
3 6

H3: Запуск эволюции — 3

Поправьте функцию run_evolution (см. H2): добавьте параметры x_test и y_test — данные для тестирования. Обучайте сеть со структурой [2, 3, 2] на выборке с 20 строчками, тестируйте на выборке со 100 строчками (везде по 2 числа). Для каждой популяции кроме ошибки на обучающих данных выведите ошибку на тестовых данных.

1 1.0 1000.0
Network(layer_sizes=[2, 3, 2], coefs=[-0.933305, -0.693081, 0.486346, 0.874076, 0.550371, 0.788543, 0.859253, -0.700309, 0.929393, 0.233593, 0.559359, 0.361341, -0.381897, 0.794201, 0.903387, -0.503072, 0.418783]) 124875.51832898942 96416.79167863644
10000 0.17 0.1
Network(layer_sizes=[2, 3, 2], coefs=[0.0809916, -0.790067, 0.465084, 0.950167, 0.358506, 0.134228, -0.161492, 0.543506, -0.011548, 0.885476, 0.577005, 0.866211, 0.793233, 0.0171874, 0.471628, 0.878454, -0.339169]) 138069.85090769245 133396.73341563327 ... Network(layer_sizes=[2, 3, 2], coefs=[-0.134111, -1.29082, 1.37418, 1.30641, 0.0814849, 16.4967, 0.0680974, 0.930618, 13.6496, 0.0766439, 1.38414, -0.121044, 10.7982, -1.22214, -0.101245, 1.9432, 7.10777]) 0.0006706187430612997 2561.938099665415

Переобучение и недообучение во всей красе

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

Если же в тестовых данных сделать 50 запросов вместо 10, то картинка сильно изменится:

Теперь если запустить обучение на 30000 поколений, то переобучение «вернётся» наместо:

Ещё одна грустная новость: никаких гарантий, что обучение будет идти с пользой, нет :)

I3: Регрессия — 3

Из универсальной теоремы аппроксимации следуюет, что любую непрерывную функцию на квадрате $f\colon [0, 100] \times [0, 100] \to [0, 1000]$ можно приблизить так, чтобы ошибка в любой точке квадрата не превышала 0.1. И хотя теоретически такая точность достижима, вряд ли у вас она получится :)

В файле I3.txt находится обучающая выборка из 3000 строчек, полученная на основе одной уже известной вам физической функции. Вам нужно у себя обучить сеть, которая предсказывает её значения на квадрате $f\colon [0, 90] \times [0, 90]$, и добавить её в программу.

При помощи обученной сети предскажите ответы на тестовых данных. Для прохождения тестов необходимо, чтобы MSE не превосходила 200.

2 x_test 0.0 0.0 10.0 10.0
0.0 3.488
Нарисовать график вашей аппроксимации
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
import numpy as np


class Network:
    ...


ai = Network(...)

fig = plt.figure()
ax = fig.gca(projection='3d')

X = np.linspace(0, 90, 91)
Y = np.linspace(0, 90, 91)
X2d, Y2d = np.meshgrid(X, Y)
Z = np.zeros_like(X2d)
for i in range(0, 91):
    for j in range(0, 91):
        Z[i, j] = ai.predict([i, j])[0]
surf = ax.plot_surface(X2d, Y2d, Z, cmap=cm.coolwarm)
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()

Обучение с подкреплением: игра «змейка»

В этой части листка мы напишем эмулятор для игры в змейку и обучим нейронную сеть, которая будет в неё играть. Здесь у нас нет данных для обучения, зато есть среда, с которой можно взаимодействовать. Будем запускать игры и смотреть, какая из сетей лучше играет. Численную характеристику вот этого «лучше играет» будем использовать для отбора сетей.

Чтобы всё это реализовать, понадобятся:

  1. Класс SnakeGame, который будет хранить текущее состояние поля и взаимодействовать с сетью. В частности:
  2. SnakeGame.__init__ — конструктор. Берёт размер поля и сохраняет внутри себя; Инициирует случайное положение змейки и яблока;
  3. SnakeGame.__str__ — вывод, чтобы можно было контролировать ситуацию;
  4. SnakeGame.gen_features — вычисление входа для нейронной сети на основе текущей позиции;
  5. SnakeGame.make_move — сделать ход в соответствии с ответом нейронной сети и посчитать очки за ход;
  6. SnakeGame.play — полностью проиграть одну партию;
  7. run_evolution — новая версия эволюции для змейки;

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

SA: Класс SnakeGame: конструктор и вывод

Реализуйте конструктор для эмулятора змейки. Он должен принимать на вход ширину и высоту поля. И необязательные параметры — список координат каждой клетки змейки в порядке от хвоста к голове и координаты яблока.

Для хранения координат будем использовать одно число — порядковый номер ячейки, если считать слева направо и сверху вниз. То есть число $y\cdot W + x$, где $(x, y)$ — двумерные координаты, а $W$ — ширина поля.

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

Как хранить эффективно хранить змейку
Лучше сначала придумайте что-нибудь своё...

Будем хранить координаты клеток змейки одновременно в деке (см. класс deque из модуля collections) и в множестве. В дек в голову мы будем добавлять новое положение головы и удалять клетку хвоста. А множество нужно для быстрой проверки самопересечения змейки.

Метод __str__ должен вывести поле в удобном для просмотра формате. См. пример.

5 5 random
..... ..... ...S. ...H. .A...
10 5 6 1 2 12 13 14 4 48
.SS.H..... ..SSS..... .......... .......... ........A.

SB: Фичи на основе позиции

Реализуйте метод gen_features, который на основе текущей позиции создаст список фич для передачи нейронной сети.

В качестве фич будем использовать 4 числа:

  1. Правда ли, что спереди свободно (1 — свободно, 0 — занято);
  2. Правда ли, что слева свободно;
  3. Правда ли, что справа свободно;
  4. Угол на яблоко (от $-\pi$ до $\pi$);
10 5 6 1 2 12 13 14 4 48
0 1 1 2.356194490192345
Число 2.356194490192345 — это $135^\circ = 135/180\cdot\pi$, так как голова смотрит наверх, а для поворота на яблоко нужно повернуть на $135^\circ$ в положительном направлении.

SC: Сделать ход

Реализуйте метод make_move, который на основе ответа нейронной сети сделает ход и вернёт число — оценку «качества хода».

Метод принимает массив из трёх чисел: то, насколько сеть хочет повернуть налево, насколько хочет двигаться прямо, и насколько хочет повернуть направо. Нужно выбрать среди этих чисел максимальное, после чего сделать ход. Также нужно оценить «качество» хода. Давайте договоримся, что поражение — это $-100$ очков. Остальное попробуйте придумать сами. Если змейка съела яблоко, то нужно выбрать новое незанятое случайное положение для яблока.

Подсказка
Если уже попробовали, а оно не учится...

Например, если голова змейки приблизилась к яблоку, то можно дать 2 очка. А если отдалилась — то -3 очка. Это для того, чтобы движение по циклу не было выгодным. Также если змейка сделала больше ходов без съедения яблока, чем удвоенный периметр, то нужно признать её техничесчкое поражение.

10 5 6 1 2 12 13 14 4 5 1.23 10.17 2.14
-100
10 5 6 1 2 12 13 14 4 48 1.23 2.14 10.17
..S.SH.... ..SSS..... .......... .......... ........A.
10 5 6 1 2 12 13 14 4 5 1.23 2.14 10.17
.SS.SH.... ..SSS..... .......... ...A...... ..........

SD: Один раунд игры

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

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

10 5 3 4 3 3 0 0 0 0 0 0 0 0 0 0 1
TBD

SE: Эволюция

Запустите эволюцию для поля 20 × 20 и обучите змейку до состояния, когда она в среднем съедает хотя бы 40 яблок.

20 20
TBD