Русский
Русский
English
Статистика
Реклама

Neural networks

Перевод Отец искусственного интеллекта Джефф Хинтон Глубокое обучение сможет делать всё

06.11.2020 16:10:59 | Автор: admin
В преддверии старта нового потока курса Machine Learning Pro + Deep Learning, делимся с вами переводом интервью MIT Technology Review с профессором Джеффри Хинтоном, который в 2012 году со своими студентами победил на ImageNet, применив глубокое обучение и добившись таким образом невероятного отрыва от соперников. В своё время его взгляды были противоположны взглядам большинства. Теперь всё иначе. Что профессор думает о развитии искусственного интеллекта, о различных подходах к нему? Об этом под катом.




Современная революция искусственного интеллекта началась с незаметного конкурса исследований. Это случилось в 2012 году, на третий год проведения соревнования ImageNet. Перед командами стояла следующая проблема: нужно было построить решение, которое распознало бы тысячу изображений, от животных и людей до ландшафтов.

В первые два года лучшие команды не добивались точности больше 75 %. На третий год команда исследователей профессор и его студенты внезапно пробила этот потолок. Они победили в соревновании с ошеломляющим отрывом в 10,8 %. Профессора звали Джеффри Хинтон, а его методом было глубокое обучение.

Хинтон работал с глубоким обучением с 1980-х годов, но эффективность ограничивалась недостатком данных и вычислительной мощности. Его стойкая вера в метод в в конечном счете принесла огромные дивиденды. На четвертый год соревнования почти каждая команда применяла глубокое обучение, достигая чудесного прироста точности. Вскоре глубокое обучение начали применять в разных отраслях, причем не только в задачах распознавания изображений.

В прошлом году Хинтона наряду с пионерами искусственного интеллекта Яном Лекуном и Джошуа Бенжио наградили премией Тьюринга за основополагающий вклад в эту область науки.

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


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

И если у нас будут такие прорывы, мы приблизим искусственный интеллект к человеческому с помощью глубокого обучения?


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

Говоря о масштабе вы имеете в виду большие нейронные сети, данные или и то, и другое?


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

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


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

Для таких вещей, как GPT-3, которая генерирует замечательные тексты, ясно, что она должна понимать многое, чтобы генерировать текст, при этом не совсем понятно, как много она понимает. Но, когда нечто открывает ящик, вынимает блок и говорит: Я только что открыло ящик и вынуло блок, трудно сказать, что оно не понимает того, что делает.

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


В когнитивистике очень давно шли дебаты между двумя школами мышления. Лидер первой школы, Стивен Косслин, считал, что когда мозг оперирует визуальными изображениями, речь идёт о пикселях и их перемещениях. Вторая школа больше соответствовала традиционному ИИ. Ее приверженцы говорили: Нет, нет, это нонсенс. Речь идет об иерархических, структурных описаниях. У разума есть определенная символическая структура, мы управляем именно этой структурой.

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

Есть люди, которые до сих пор считают, что символическое представление это один из подходов к ИИ


Совершенно верно. У меня есть хорошие друзья, например, Гектор Левеск, который действительно верит в символический подход и проделал в этом смысле большую работу. Я не согласен с ним, но символический подход вполне разумная вещь, чтобы попробовать его. Однако я думаю, что в конце концов мы осознаем, что символы просто существуют во внешнем мире, а мы выполняем внутренние операции над большими векторами.

Какой взгляд на ИИ вы считаете самым противоположным по отношению к другим?


Что ж, моя проблема заключается в том, что эти противоположные взгляды были у меня, а спустя пять лет они стали мейнстримом. Большинство моих противоположных взглядов из 1980-х годов теперь получили широкое признание. Сейчас довольно сложно найти людей, которые с ними не согласны. Так что да, мои противоположные взгляды в некотором роде были подорваны.

Кто знает, может ваши взгляды и методы работы с ИИ тоже будут в андеграунде, а через несколько лет станут отраслевым стандартом. Главное не останавливаться в своем прогрессе. А мы с удовольствием поможем вам в этом, даря специальный промокод HABR, который добавит 10 % к скидке на баннере.

image




Рекомендуемые статьи


Подробнее..

Фронтендер пишет нейронки. Уровень сложности хочу на ручки

05.01.2021 22:19:51 | Автор: admin

Рано или поздно это должно произойти

Рано или поздно, фронтенд - разработчик устает играть со своими фреймворками, устает докучать коллегам - бэкендерам, устает играть в девопс и начинает смотреть в сторону машинного обучения, дата - саенс и вот это вот все. Благо, каждый второй курс для тех кто хочет войти вайти способствует этому, крича на всех платформах, как это легко. Я тоже, насытившись перекладыванием данных из базы в API, а потом из API в таблицы и формы, решил взять небольшой отпуск и попробовать применить свои скилы фронтендера в машинном обучении. Благо, существуют такие люди какDaniel ShiffmanиCharlie Gerard, которые своим примером помогают не бросить начатое, увидев первые страницы с математическими формулами.

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

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

Схема нейронной сетиСхема нейронной сети

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

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

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

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

Говоря черный ящик - выдаю в себе любовь к тестам. Именно так. Сейчас нам не особо важно какие именно внутренние алгоритмы использует та или иная сеть. Мы просто бросаем некоторые рандомные факты в нее и получаем ответ как нам поступить.

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

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

Веса нейронной сетиВеса нейронной сети

Допустим, как мы уже сказали, нам не страшен лед если мы в ботинках. Тогда важность этого факта будет минимальной, мы можем учитывать только0.1*X1(W1== 0.1)от любого значенияX1. (Да, забыл сказать, так как у нас искусственная нейронная сеть, каждый факт должен быть выражен каким либо числом, но лучше, конечно, нормированным от -1 до 1). В этом случае какое бы значение мы не получили, мы будем принижать его важность в 10 раз. И наоборот, важность падения сосулек для нашей жизни максимальная, поэтому вес для такой связи будет1*X2(W2== 1).

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

Теперь, когда мы немного ближе рассмотрели нашу картинку, напоминает ли она еще что-то? По мне, так это самая настоящая функция (или более правильное название - функция активации).f(Х)=У. ГдеХ- это матрица всех входных данных или input слой,У- это матрица всевозможных вариантов исхода или output слой, и некоторый алгоритмf, который по какому-то паттерну преобразовывает входной слой в выходной.

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

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

И уже мы сами можем дать определенныйlabelэтим вероятностям. Допустим, если нейронка говорит, что наиболее вероятный маневр - этоУ3, а мы ранее дали этому выходному вариантуlabel=поверни налево, то в этой ситуации мы говорим, что нейронная сеть предложила повернуть налево. И не смотря на то, что нейронка предлагала еще два других варианта, мы ими пренебрегли, поскольку их вероятность была меньше.

Тут вы можете еще раз возразить. Жизнь все еще намного сложнее! И снова вы правы. Как же тогда люди учатся делать что-то, находить паттерны, если их веса никто не исправляет, никто не регулирует?

Тут мы подошли к такому понятию, как обучение нейронной сети. Собственно, как мы можем научить нейронную сеть настраивать свои веса?

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

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

Пример с погодой, который мы рассмотрели выше, относится к группе методов обучения с подкреплением (без учителя). Данная группа считается наиболее естесственной и близкой к настоящим нейронным сетям.

Что это значит? Это значит, что веса никто не настраивает. Нейронная сеть делает какие-то действия, и сама понимает какие паттерны что значат. Но для закрепления знания нейронной сети нужны стимулы. Для настоящей нейронной сети таким стимулом является жизнь ее носителя. Если после очередного шага носитель остается жить, то вероятнее всего это был правильный шаг и сеть его запомнит, поправит веса и в будущем сделает снова.

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

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

Итак, собрав эту небольшую информацию, как и зачем работают нейронки, давайте немного отдохнем и поиграемв raid shadow legends. Играть мы будем вdino gameот создателейgoogle chrome. Но нажимать пробел было бы очень просто, давайте напишем игру с нуля и нейронную сеть, которая сама будет играть в эту игру?

Dino game

В написании игры нам будет помогать такой редактор какp5.js. Данный инструмент уже заточен на реализацию подобных задач, когда необходимо реализовывать игровой цикл, работу с канвасом и обработкой событий во время самой игры. Любой скетч на р5 имеет две функции:setup, где мы инициализируем все наши переменные, рисуем канвас определенного размера и прочие вещи; иdraw- функция, которая вызывается на каждой итерации игрового цикла, здесь мы можем обновлять наши анимации и прочее.

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

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

class Cactus {  constructor() {}}class Dino {  constructor() {}}

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

...onTick(cb = () => { }) {  cb(this);}...

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

Далее мы вводим такие понятия как состояние дино (бег, прыжок, падение). У дино будет максимальная высота, на которую он сможет прыгнуть и скорость прыжка. После этого, при каждой итерации игрового цикла мы проверяем состояние дино: если дино бежит - мы пропускаем итерацию, если находится в прыжке, то инкрементируем текущую высоту, если приземляется, то декрементируем, проверяя при этом, максимальную высоту и уровень земли. Таким образом, мы только изменяем координатуУ, имитируя прыжок.

switch (this.state) {  case DinoStateEnum.run: break;  case DinoStateEnum.jump: {    this.currH += this.jumpSpeed;    if (this.currH == this.maxH) {      this.state = DinoStateEnum.fall;    }    break;  }  case DinoStateEnum.fall: {    this.currH -= this.jumpSpeed;    if (this.currH == 0) {      this.state = DinoStateEnum.run;    }    break;  }}

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

function updateCactuses() {  const copy = cactuses.slice();  for (let i = 0; i < copy.length; i++) {    let c = copy[i];    c.onTick(cactus => {      drawCactus(cactus)      cactus.currDistance -= dinoVelocitySlider.value();      if (cactus.currDistance + cactusW < initialDinoW && !cactus.passDinoPosition) {        updateDinoScore()        cactus.passDinoPosition = true;      }      if (cactus.currDistance < 0) {        cactuses.splice(i, 1);      }    })  }}

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

Нейроэволюция

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

<script  src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script><script>  tf.setBackend('cpu') // tf глобальная переменная</script>

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

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

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

Самый легкий алгоритм, который мы можем представить - это брутфорс. Мы рандомно выбираем веса и надеемся, что они будут примерно правильно настроены, чтобы адекватно реагировать на внешние условия. (Я сейчас говорю только про веса, потому что условия, которые мы будем учитывать, рассмотрим чуть позже).

Таким образом, мы можем получить идеально выученного дино как сразу же, так и через пару лет генерирования рандомных дино. Как нам ускорить этот процесс? Нам потребуется эволюция.

В чем идея? Мы генерируем популяцию рандомно - настроенных дино размером 200-300 особей, и смотрим насколько они способны выживать. Далее мы выбираем несколько особей, у которых продолжительность жизни (best score) наибольшая, пытаемся немного их мутировать, как если бы это делала настоящая эволюция и создаем новое поколение. То есть имитируем настоящую эволюцию, поощряя продолжительность жизни. В итоге через несколько поколений преобладающим качеством наших дино должна стать долгая жизнь. Это как выводить пшеницу с наибольшими зернами (селекция).

Теперь когда с теорией, наконец, закончили, давайте перейдем к имплементации.

Мы будем реализовывать простуюнейронную сеть из трех слоев.

  • Входной слой - это наши значимые условия окружающей среды.

  • Cкрытый слой - наш черный ящик.

  • Выходной слой - решения, которые принимает дино.

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

  • Tекущее положение дино по осиУ.

  • Скорость дино.

  • Расстояние до ближайшего кактуса.

  • Условие, когда дино приближается сразу к нескольким кактусам подряд.

Всего 4 входных узла или нейрона.

Настройку скрытого слоя доверим великому рандому и просто выберем 8 нейронов. Выходных нейронов будет 2, два решения:прыгать или не прыгать.

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

createModel() {  const model = tf.sequential();  const hiddenLayer = tf.layers.dense({    units: this.hidden_nodes, // кол-во нейронов в скрытом слое (8)    inputShape: [this.input_nodes], // кол-во нейронов во входном слое (4)    activation: "sigmoid" // функция активации  });  model.add(hiddenLayer);  const outputLayer = tf.layers.dense({    units: this.output_nodes, // кол-во нейронов в выходном слое (2)    activation: "sigmoid"  });  model.add(outputLayer);  return model;}

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

Добавим слой в модель и создадим таким же образом выходной слой, при этом заметьте, теперь нам нет необходимости указывать кол-во нейронов в прошлом слое, так какtensorflowсделает это за нас.

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

predict(inputs) {  return tf.tidy(() => {    const xs = tf.tensor([inputs]); // создание тензора из массива (входной слой)    const ys = this.model.predict(xs); // предсказание сети    const output = ys.dataSync(); // превращение тензора в массив    return output;  });}

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

Как уже сказано,tensorflowработает только с тензорами, поэтому в нашу функцию предсказания мы посылаем тензор, полученный путем преобразования одномерного массива. После обработки входящих данных мы также получаем тензор и, чтобы превратить его в удобочитаемый формат, вызываем специальный метод. (Можем читать эти данные, как синхронно так и асинхронно). На выходе мы также получим одномерный массив длиной 2, поскольку мы указали 2 выходных нейрона. И, если мы вспомним начало, то поймем, что массив заполнен вероятностями того или иного решения, то есть прыгать или нет. Нам теперь достаточно проверить, чтоoutput[0] > output[1], чтобы дино прыгнул.

Вот так в несколько строчек можно реализовать простую нейронную сеть дляdino npc.

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

Сначала мы устанавливаем размер популяции, потом генерируем это кол-во дино и в цикле отрисовываем каждого.

function drawDino(dino) {  if (dino.isDead) return;  if (dino.state != DinoStateEnum.run) {    // если дино прыгает, то рисуем его на текущей высоте    image(dino2, initialDinoW, initialDinoH - dino.currH, dinoW, dinoH); // р5 специальный метод добавления изображения на канвас  } else if (iteration % 7 == 0)    // иначе имитируем бег и перебирание ножками    image(dino1, initialDinoW, initialDinoH, dinoW, dinoH);  else    image(dino2, initialDinoW, initialDinoH, dinoW, dinoH);}

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

function updateGenerationIfNeeded() {  if (dinos.every(d => d.isDead)) {    cactuses = [];    dinoVelocitySlider.value(initDinoVelocity);    dinos = newGeneration(dinos)  }}

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

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

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

mutate(rate) {  tf.tidy(() => {    const weights = this.model.getWeights(); // берем веса модели    const mutatedWeights = [];    for (let i = 0; i < weights.length; i++) {      let tensor = weights[i]; // каждый вес - это тензор      let shape = weights[i].shape;      let values = tensor.dataSync().slice();      for (let j = 0; j < values.length; j++) {        if (Math.random() < rate) { // мутируем если нам повезло          let w = values[j];          values[j] = w + this.gaussianRandom(); // рандомное нормальное изменение в интервале от -1 до 1        }      }      let newTensor = tf.tensor(values, shape);      mutatedWeights[i] = newTensor;    }    this.model.setWeights(mutatedWeights); // ставим мутировавшие веса  });}

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

const calculateFitness = (dinos) => {  let sum = 0;  dinos.map(d => sum += d.score)  dinos.map(d => d.fitness = d.score / sum)}

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

Функция для такого действия может выглядеть так:

const pickOne = (dinos) => { // на входе дино отсортированные по убыванию fitness  let index = 0;  let r = Math.random();  while (r > 0) {    r = r - dinos[index].fitness;    index++;  }  index--;  let dino = dinos[index] // берем дино где-то из начала списка, как повезет с rate  const dinoBrain = dino.brain.copy();  dinoBrain.mutate(0.2) // делаем мутировавшую копию  let newDino = new Dino(dinoBrain) // дино для нового поколения  return newDino;}

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

for (let i = 0; i < TOTAL; i++) {  newDinos.push(pickOne(oldDinos));}console.log(currentGeneration++);return newDinos;

Запустив симуляцию мы можем увидеть несколько стратегий, которые появляются в первом поколении, дино либо прыгает постоянно, либо прыгает далеко перед кактусом и приземляется сразу за ним, либо прыгает прямо перед кактусом. К десятому поколению эти стратегии усредняются и дино способен прожить 50+ кактусов. По ссылке можно проверить симуляцию самому.

Исходники проекта на гитхабе.

P.S. Если возникли вопросы к материалу или заметили ошибку, welcome to PR's. Или напишите мне в твиттерv_hadoocken

Подробнее..

Фронтендер пишет нейронки. Уровень сложности мартышка и уравнение Беллмана

20.01.2021 22:15:38 | Автор: admin

Привет.

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

В комментариях к прошлой статье поднялся вопрос про reinforcement learning. Почему бы и нет. Давайте подробнее рассмотрим что это такое.

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

Итак, reinforcement learning, или обучение с подкреплением - это такая группа методов машинного обучения, подходы которой сначала выглядят как методы обучения без учителя, но со временем (время обучения) становятся методами обучения с учителем, где учителем становится сама нейронная сеть. Скорее всего, ничего непонятно. Это не страшно, мы все рассмотрим на примере.

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

Наш условный лабиринтНаш условный лабиринт

Представляя эту картину, мы можем увидеть все основные составляющие любого проекта с использованием обучения с подкреплением.Во-первых, у нас есть крыса - Агент (agent), наша нейронная сеть, которая мыслит и принимает решения.Во-вторых, у нас есть Окружение или среда (environment) агента - лабиринт, который имеет свое Состояние (state), расположение проходов, мест с котами, финальный островок и так далее. Крыса может принимать решения и совершать определенные Действия (actions), которые могут приводить к разным последствиям. Крыса либо получает Вознаграждение (reward), либо Санкции (penalty or -reward) за свои действия.

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

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

Семейство методов обучения с подкреплениемСемейство методов обучения с подкреплением

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

Мы имеем некоторое кол-во состояний (s1, s2 ...), наш агент может находится в одном из этих состояний в каждый момент времени. Цель агента достичь финального состояния.

Пример конечного автоматаПример конечного автомата

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

---

s1

s2

s3

s4

s5

s6

s7

s1

0

1

-1

---

---

---

---

s2

---

0

---

-10

1

---

---

s3

---

---

0

---

---

100

1

Но здесь появляется проблема, как только мы начнем изменять кол-во состояний, наша таблица становится бесполезной и мы должны заполнять ее заново. Здесь нам на помощь приходят нейронные сети. Зачем заполнять таблицы? Давайте просто предсказывать значение перехода.

В чем идея? Как нейронные сети нам помогут?

В прошлый раз мы условились, что будем воспринимать нейронные сети как черный ящик для простоты понимания. Давайте не будем нарушать эти условности.

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

Получение алгоритма распознавания котиковПолучение алгоритма распознавания котиков

Далее, если мы достанем еще несколько картинок котиков, которые отличны от тех 1000 штук, возьмем наш алгоритм, и все это отправим в черный ящик, он вернет нам правильное название для этих картинок, хотя этих картинок нейронная сеть еще не видела. Это нейронные сети 101 (базовый курс).

Распознавание новых котиковРаспознавание новых котиков

Так вот, с Q-обучением тоже самое. Зная только часть переходов между состояниями таблицы, используя нейронные сети, мы можем предсказывать новые состояния для новой таблицы с максимальным вознаграждением. Поэтому на свет появился Deep Q-learning алгоритм. Deep потому что deep neural networks, глубокие нейронные сети, глубокие - потому что много слоев.

Наверняка появились вопросы или я что-то упустил. Поэтому давайте перейдем к практической части.

Реализация окружения и агента

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

Начнем мы с того, что определим наши объектные модели. Самыми главными классами для нас будут класс Environment и класс Agent.

class Agent {  constructor(b, r, s, w, h) {    this.network = b;    this.rect = r;    this.speed = s;    this.width = w;    this.height = h;  }}class Environment {  constructor(w, h, r, c, es, as) {    this.width = w;    this.height = h;    this.rows = r;    this.columns = c;    this.enemySpeed = es;    this.agentSpeed = as;    this.agent = this.resetAgent();    this.eps = Environment.MAX_EPS;    this.discount = Environment.DISCOUNT;  }}

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

Это будет некое подобие лабиринта. Наш агент-крыса каждую игру начинает в левом верхнем углу и его цель - добраться до сыра в нижнем правом. На поле игры в рандомном месте генерится несколько котов. Задача агента состоит в том, чтобы добраться до сыра невредимым и миновать всех котов.

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

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

  • положение агента по оси Х

  • положение агента по оси У

  • наличие врага впереди на оси Х

  • наличие врага впереди на оси У

  • дистанция до врага по оси Х

  • дистанция до врага по оси У

  • наличие цели на оси Х

  • наличие цели на оси У

  • дистанция до цели

Параметры состояния окружения агентаПараметры состояния окружения агента

Таким образом, мы имитируем зрение агента, как если бы он двигался по настоящему лабиринту, то есть он может видеть что у него спереди и как близко он у цели. Собственно, чтобы двигаться агент может совершать 4 действия (шага) - вправо, вниз, влево, вверх.

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

const ACTIONS = [MOVE_RIGHT, MOVE_DOWN, MOVE_LEFT, MOVE_UP];chooseAction(state, eps) {  if (random(0, 1) < eps) {    return ACTIONS[random([0, 1, 2, 3])]; // рандомный шаг  } else {    return tf.tidy(() => {      // сеть возвращает массив из 4 значений      const probs = this.network.predict(state).dataSync();// шаг с максимальным значением      return ACTIONS[probs.indexOf(Math.max(...probs))];     });  }}

Агент получает текущее состояние и выдает ответ в какую сторону стоит сделать шаг. Здесь мы уже можем ответить на возможный вопрос, что за eps и зачем он нужен.

Это, так называемый, коэффициент исследования.

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

Почему? Это называется проблемой компромисса исследования и использования (не знаю как правильно перевести exploration-exploition trade-off).

Давайте рассмотрим на примере.

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

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

Вернемся обратно к агенту.

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

update(action) {  switch (action) {    case MOVE_UP:    this.rect.top = this.rect.top - this.speed;    break;    case MOVE_DOWN:    this.rect.top = this.rect.top + this.speed;    break;    case MOVE_RIGHT:    this.rect.left = this.rect.left + this.speed;    break;    case MOVE_LEFT:    this.rect.left = this.rect.left - this.speed;    break;  }}

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

createModel(inputShape) {  const model = tf.sequential();  model.add(tf.layers.dense({ inputShape: [inputShape], units: 36, activation: 'relu' }));  model.add(tf.layers.dense({ units: 36, activation: 'relu' }));  model.add(tf.layers.dropout({ rate: 0.20 }));  model.add(tf.layers.dense({ units: Agent.ACTIONS.length }));  model.compile({ optimizer: 'adam', loss: 'meanSquaredError' });  return model;}

У нас появился необычный слой (5 строка), который называется dropout. Его советуют использовать, если есть возможность того, что нейронка может перетренероваться (явление, когда сеть не предсказывает выходные данные, а просто запоминает связки инпут-аутпут из тренировочных данных). Но также нашел на хабре статью, в комментариях которой говорят, что у этого слоя куда больше применений, хотя их автор не упомянул. Не суть. Что делает этот слой? Он просто игнорирует некоторые нейроны с заданной вероятностью, то есть обнуляет их веса, чтобы те не влияли на ответ.

И вторая строчка, которая нас будет интересовать чуть позже (8 строка), метод компиляции модели. Здесь мы указываем то, как нейронка будет обрабатывать свои ошибки (разницы между предсказанными значениями и теми, которые должны быть). Оставим настройку этих параметров интернету и будем воспринимать их как черный ящик.

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

Обучение агента

Каков наш алгоритм обучения? На самом деле, в интеренете очень много примеров и реализаций DQN алгоритма, в частности на js, но, чтобы понять этот алгоритм мне пришлось потратить пару дней непрерывного чтения различных статей и обрывков книг, чтобы просто прочитать код. Я даже отчаялся и пошел просить помощи на stackoverflow. В итоге я не уверен, что полностью понимаю, что я сделал. Наверное, поэтому и пишу эту статью, но эй! Мы за этим здесь и собрались - учиться на ошибках. Поэтому буду очень рад обратной связи.

Итак, как же мы научим агента искать сыр?

Во-первых, мы разобьем наше обучение на несколько игр, 60-100 должно хватить. Установим кол-во шагов в каждой игре, пусть будет 1000, чтобы игра не шла вечно и агент не крутился на месте. Если агент израсходует свои шаги, игра начинается заново. Если агент натыкается на кота или находит сыр, игра начинается заново. Чтобы мотивировать агента избегать котов и искать сыр введем метод подсчета его вознаграждения и штрафов. За каждый шаг будем бить агента током, мотивируя его быстрее добраться до цели, причем, чем ближе агент к цели, тем меньше он получит разряд (в числах это от -0.2 до 0). Если агент натыкается на кота то умирает и получает -10. Если находит сыр, то его награда +100.

calcReward() {  // находим нормализованную дистанцию до цели (от 0 до 1) и умножаем на -0.2  let reward = distance(  this.agent.rect.left,  this.width,  this.agent.rect.top,  this.height) / distance(0, this.width, 0, this.height) * -0.2;  const agentRect = toRect(this.agent.rect);  const enemiesRects = this.enemies.map(e => toRect(e));  const goalRect = toRect(this.goal);  const intersected = enemiesRects.filter(e => rectsIntersected(e, agentRect));  reward += intersected.length && -10;  if (rectsIntersected(agentRect, goalRect)) reward = 100;  return reward;}

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

В-третьих, соберем все вместе.

Мы инициализируем игру и наше хранилище.

function setup() {  mem = new ReplayMemory();  env = new Environment(450, 300, 4, 6, 4, 2);  createCanvas(...env.dims);}

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

async function draw() {  CURRENT_STEP++;  background(220);  drawGoal(env.goal);  drawNet(env.net);  drawEnemies(env.enemies);  drawAgent(env.agent.rect);  ...

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

// environmentupdateAgent(STATE = this.getStateTensor()) {  const action = this.agent.chooseAction(STATE, this.eps);  this.agent.update(action);  const nextState = this.getStateTensor();  return [nextState, action, this.isDone()];}

Далее мы считаем награду за шаг агента и добавляем все эти данные в нашу память. Они нам потом понадобятся.

// draw in sketch fileconst [nextState, action, done] = env.updateAgent(STATE);const reward = env.calcReward();mem.append([STATE, action, reward, nextState, done]);STATE = nextState;...

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

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

Теперь к самой реализации. Думаю, я довольно потомил вас в ожидании, вот эта формула - уравнение Беллмана. Что же она нам говорит?

Если попробовать прочитать что тут написано, то получится нечно следующее: одно кольцо, чтоб править всеми максимально возможное вознаграждение (Q) агента в состоянии s равно сумме моментального вознаграждения r за его шаг а и максимально возможноного вознаграждения агента из состояния s помноженное на коэффициент понижения gamma. На слух - это точно эльфийский.

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

Допустим, у нас есть начальное состояние агента s1, далее агент имеет возможность перейти в состояние s2 и s3 при помощи действий (шагов) а1 и а2, после этого вариации выбора еще раз расширяются. И из состояния s2, все еще при помощи а1 и а2, агент может попасть в состояния s4 и s5, а из состояния s3 в состояния s6 и s7. За каждый переход агент будет получать моментальное вознаграждение, ну или штраф, а чтобы посчитать долгосрочное вознаграждение из состояния s1 нам нужно проверить все ветви нашего автомата. Собственно, становится понятно, что max Q для состояния s1 == 99 (-1 + 100), а для состояния s3 == 100 (100 - финальный переход) и логично отсюда вывести, что max Q для s1 равно вознаграждение за переход a2 плюс max Q из состояния, в которое мы попали (s3).

Но что это значит? Это значит, что нам вручную нужно ходить туда-сюда по состояниям и правильно настраивать Q значения - заполнять нашу таблицу, как мы уже поняли. И как мы уже решили, мы не будем этого делать, пусть нейронка сама нам считает эти значения. Поэтому вот так уравнение Беллмана выглядит на javascript.

async function replay() {  let miniBatch = mem.sample(500);  const filtered = miniBatch.filter(Boolean);  // фильтруем если очень мало шагов сделали  if (!filtered.length) return;  let currentStates = filtered.map((dp) => { return dp[0].dataSync() });  // предсказываем Q для каждого текущего состояния s в памяти  let currentQs = await env.agent.network.predict(tf.tensor(currentStates)).array();    let newCurrentStates = filtered.map((dp) => { return dp[3].dataSync() });  // предсказываем Q для каждого состояния s', в которое мы попали из s  let futureQs = await env.agent.network.predict(tf.tensor(newCurrentStates)).array();  let X = [];  let Y = [];  for (let index = 0; index < filtered.length; index++) {    // берем один слайс    const [state, action, reward, newState, done] = filtered[index];    let newQ;    let currentQ;    // уравнение Беллмана    if (!done) {      let maxFutureQ = Math.max(...futureQs[index]);      // находим максимальный Q для следующего состояния (s')       // и складываем с моментальным вознаграждением (r)      newQ = reward + (env.discount * maxFutureQ);    }    // если финальный переход, просто учитываем сам переход    else { newQ = reward }    currentQ = currentQs[index];    // корректируем текущее значение Q на то, которое посчитали    currentQ[action] = newQ;    X.push(state.dataSync()); // 9 параметров нашего состояния    Y.push(currentQ); // массив из 4 значений Q для наших шагов (вправо, вниз, влево, вверх)  }  // учим нашу сеть скорректированными данными  await env.agent.network.fit(tf.tensor(X), tf.tensor(Y), { verbose: 0 });}

Когда мы попадаем в реплай игры мы берем небольшой слайс записей наших шагов.

Мы достаем все стартовые состояния из этих записей. И просим нашу нейронку посчитать значение Q для каждого состояния.

Делаем то же самое для всех состояний, в которых агент оказался после своего шага.

Теперь мы считаем разницу между тем, что нам подсказала нейронка и тем, что мы сами посчитали.

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

После этого вызываем специальный метод fit, который помогает нейронке переосмыслить свои шаги с нашими корректировками.

И чуть не забыл, зачем нам вообще нужен коэффициент понижения? Нам нужен еще один пример.

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

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

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

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

Давайте уже запускать симуляцию.

Первые несколько игр Джери тупит в углу либо суицидиться об котов.

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

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

PS. если есть желание поконтрибьютить, welcome to PRs или напишите мне в твиттер: v_hadoocken

Подробнее..
Категории: Javascript , Tensorflow , Neural networks , Games , Q-learning , P5

Создание нейронной сети Хопфилда на JavaScript

05.06.2021 18:10:05 | Автор: admin

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

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

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

Исходникина Github и демо.

Дляреализациипонадобится:

  • Браузер

  • Базовоепониманиенейросетей

  • БазовыезнанияJavaScript/HTML

Немноготеории

Нейронная сеть Хопфилда (англ. Hopfield network) полносвязная нейронная сеть ссимметричной матрицей связей. Такая сеть может быть использована для организации ассоциативной памяти, как фильтр, атакже для решения некоторых задач оптимизации.

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

Структурная схема нейросети ХопфилдаСтруктурная схема нейросети Хопфилда

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

Алгоритм работы сети:

  1. Инициализация
    Веса нейронов устанавливаются по следующей формуле:

    w_{ij}=\left\{\begin{matrix} \sum_{k=1}^{m} x_{i}^{k} * x_{j}^{k} & i \neq j \\0, & i=j \end{matrix}\right.

    где m количество образов
    x_{i}^{k}, x_{j}^{k} i - ый и j - ый элементы вектора k - ого образца.

  2. Навходы сети подается неизвестный сигнал. Фактически его ввод осуществляется непосредственной установкой значений выходов:
    y_{j}(0) = x_{j}

  3. Рассчитывается выход сети (новое состояние нейронов иновые значения выходов):

    y_{j}(t+1)=f\left ( \sum_{i=1}^{n} w_{ij}*y_{i}(t)\right )

    где f пороговая активационная функция собластью значений [-1; 1];
    t номер итерации;
    j = 1...n; n количество входов инейронов.

  4. Проверка изменения выходных значений запоследнюю итерацию. Если выходы изменились переход кпункту3, иначе, если выходы стабилизировались, завершение функционирования. При этом выходной вектор представляет собой образец, наилучшим образом сочетающийся свходными данными.

Разработка

Визуальная часть

Для начала посмотрим как работает итоговый проект.

Демонстрация работы программыДемонстрация работы программы

Онсостоит издвух элементов Canvas итрех кнопок. Это простейший HTML иCSS код, ненуждающийся впояснении (можете скопировать сгитхаба).

Левый элемент Canvas нужен для рисования изображений, которые затем будут использованы для обучения (кнопка Запомнить) или распознавания нейросети. Направом элементе отображается результат распознавания сигнала, находящегося наданный момент налевом Canvas (вданном случае сеть вспомнила буквуТ наоснове искаженного сигнала).

Здесь нужно обратить внимание нато, что область для рисования представлена сеткой 1010 ипозволяет закрашивать клетки только черным цветом. Так как всети Хопфилда число нейронов равно числу входов, количество нейронов будет равно длине входного сигнала, тоесть 100 (унас всего 100 клеток наэкране). Входной сигнал при этом будет двоичным массив, состоящий из1и1, где 1 это белый, а1 черный цвет.

Наконец-то приступим кнаписанию кода, сначала инициализируем необходимые переменные.

Код инициализации
// Размер сетки установим равным 10 для простоты тестированияconst gridSize = 10;// Размер одного квадрата в пикселяхconst squareSize = 45;// Размер входного сигнала (100)const inputNodes = gridSize * gridSize;// Массив для хранения текущего состояния картинки в левом канвасе,// он же является входным сигналом сетиlet userImageState = [];// Для обработки движений мыши по канвасуlet isDrawing = false;// Инициализация состоянияfor (let i = 0; i < inputNodes; i += 1) {    userImageState[i] = -1;  }// Получаем контекст канвасов:const userCanvas = document.getElementById('userCanvas');const userContext = userCanvas.getContext('2d');const netCanvas = document.getElementById('netCanvas');const netContext = netCanvas.getContext('2d');

Реализуем функцию рисования сетки, используя инициализированные ранее переменные.

Функция отрисовки сетки
// Функция принимает контекст канваса и рисует// сетку в 100 клеток (gridSize * gridSize)const drawGrid = (ctx) => {  ctx.beginPath();  ctx.fillStyle = 'white';  ctx.lineWidth = 3;  ctx.strokeStyle = 'black';  for (let row = 0; row < gridSize; row += 1) {    for (let column = 0; column < gridSize; column += 1) {      const x = column * squareSize;      const y = row * squareSize;      ctx.rect(x, y, squareSize, squareSize);      ctx.fill();      ctx.stroke();    }  }  ctx.closePath();};

Чтобы оживить полученную сетку, добавим обработчики клика идвижения мыши поканвасу.

Обработчики движений мыши
// Обработка клика мышиconst handleMouseDown = (e) => {  userContext.fillStyle = 'black';  // Рисуем залитый прямоугольник в позиции x, y  // размером squareSize х squareSize (45х45 пикселей)  userContext.fillRect(    Math.floor(e.offsetX / squareSize) * squareSize,    Math.floor(e.offsetY / squareSize) * squareSize,    squareSize, squareSize,  );  // На основе координат вычисляем индекс,  // необходимый для изменения состояния входного сигнала  const { clientX, clientY } = e;  const coords = getNewSquareCoords(userCanvas, clientX, clientY, squareSize);  const index = calcIndex(coords.x, coords.y, gridSize);  // Проверяем необходимо ли изменять этот элемент сигнала  if (isValidIndex(index, inputNodes) && userImageState[index] !== 1) {    userImageState[index] = 1;  }  // Изменяем состояние (для обработки движения мыши)  isDrawing = true;};// Обработка движения мыши по канвасуconst handleMouseMove = (e) => {  // Если не рисуем, т.е. не было клика мыши по канвасу, то выходим из функции  if (!isDrawing) return;  // Далее код, аналогичный функции handleMouseDown  // за исключением последней строки isDrawing = true;  userContext.fillStyle = 'black';  userContext.fillRect(    Math.floor(e.offsetX / squareSize) * squareSize,    Math.floor(e.offsetY / squareSize) * squareSize,    squareSize, squareSize,  );  const { clientX, clientY } = e;  const coords = getNewSquareCoords(userCanvas, clientX, clientY, squareSize);  const index = calcIndex(coords.x, coords.y, gridSize);  if (isValidIndex(index, inputNodes) && userImageState[index] !== 1) {    userImageState[index] = 1;  }};

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

Вспомогательные функции
// Вычисляет индекс для изменения в массиве// на основе координат и размера сеткиconst calcIndex = (x, y, size) => x + y * size;// Проверяет, помещается ли индекс в массивconst isValidIndex = (index, len) => index < len && index >= 0;// Генерирует координаты для закрашивания клетки в пределах // размера сетки, на выходе будут значения от 0 до 9const getNewSquareCoords = (canvas, clientX, clientY, size) => {  const rect = canvas.getBoundingClientRect();  const x = Math.ceil((clientX - rect.left) / size) - 1;  const y = Math.ceil((clientY - rect.top) / size) - 1;  return { x, y };};

Далее напишем обработчик для кнопки Очистить. При нажатии наэту кнопку должны очищаться закрашенные квадраты двух кавасов и сбрасываться состояние входного сигнала.

Функция очистки сетки
const clearCurrentImage = () => {  // Чтобы убрать закрашенные клетки, просто заново отрисовываем   // всю сетку и сбрасываем массив входного сигнала  drawGrid(userContext);  drawGrid(netContext);  userImageState = new Array(gridSize * gridSize).fill(-1);};

Теперь можно переходить кразработке мозга программы.

Реализация алгоритма нейросети

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

Инициализация весов сети
...const weights = [];  // Массив весов сетиfor (let i = 0; i < inputNodes; i += 1) {  weights[i] = new Array(inputNodes).fill(0); // Создаем пустой массив и заполняем его 0  userImageState[i] = -1;}...

Так как каждый нейрон всети Хопфилда связан совсеми остальными нейронами, веса сети представлены двумерным массивом, каждый элемент которого является одномерным массивом размером inputNodes элементов. Витоге мыполучаем 100нейронов, укаждого изкоторых по100связей.

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

Код обработки входного сигнала
const memorizeImage = () => {  for (let i = 0; i < inputNodes; i += 1) {    for (let j = 0; j < inputNodes; j += 1) {      if (i === j) weights[i][j] = 0;      else {        // Напоминаю, что входной сигнал находится в массиве userImageState и является        // набором -1 и 1, где -1 - это белый, а 1 - черный цвет клеток на канвасе        weights[i][j] += userImageState[i] * userImageState[j];      }    }  }};

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

Функция распознавания искаженного сигнала
// Где-то в html подключаем библиотеку lodash:<script src="http://personeltest.ru/aways/cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>...const recognizeSignal = () => {  let prevNetState;  // На вход сети подается неизвестный сигнал. Фактически   // его ввод осуществляется непосредственной установкой значений выходов  // (2 шаг алгоритма), просто копируем массив входного сигнала  const currNetState = [...userImageState];  do {    // Копируем текущее состояние выходов, // т.е. теперь оно становится предыдущим состоянием    prevNetState = [...currNetState];    // Рассчитываем выход сети согласно формуле 3 шага алгоритма    for (let i = 0; i < inputNodes; i += 1) {      let sum = 0;      for (let j = 0; j < inputNodes; j += 1) {        sum += weights[i][j] * prevNetState[j];      }      // Рассчитываем выход нейрона (пороговая ф-я активации)      currNetState[i] = sum >= 0 ? 1 : -1;    }    // Проверка изменения выходов за последнюю итерацию    // Сравниваем массивы при помощи ф-ии isEqual  } while (!_.isEqual(currNetState, prevNetState));  // Если выходы стабилизировались (не изменились), отрисовываем восстановленный образ  drawImageFromArray(currNetState, netContext);};

Здесь для сравнения выходов сети напредыдущем итекущем шаге используется функция isEqual избиблиотеки lodash.

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

Функция отрисовки изображения из массива точек
const drawImageFromArray = (data, ctx) => {  const twoDimData = [];  // Преобразуем одномерный массив в двумерный  while (data.length) twoDimData.push(data.splice(0, gridSize));  // Предварительно очищаем сетку  drawGrid(ctx);  // Рисуем изображение по координатам (индексам массива)  for (let i = 0; i < gridSize; i += 1) {    for (let j = 0; j < gridSize; j += 1) {      if (twoDimData[i][j] === 1) {        ctx.fillStyle = 'black';        ctx.fillRect((j * squareSize), (i * squareSize), squareSize, squareSize);      }    }  }};

Финальные приготовления

Для полноценного запуска программы осталось только добавить наши функции вкачестве обработчиков для элементов HTML ивызвать функции отрисовки сетки.

Привязываем функции к HTML элементам
const resetButton = document.getElementById('resetButton');const memoryButton = document.getElementById('memoryButton');const recognizeButton = document.getElementById('recognizeButton');// Вешаем слушатели на кнопкиresetButton.addEventListener('click', () => clearCurrentImage());memoryButton.addEventListener('click', () => memorizeImage());recognizeButton.addEventListener('click', () => recognizeSignal());// Вешаем слушатели на канвасыuserCanvas.addEventListener('mousedown', (e) => handleMouseDown(e));userCanvas.addEventListener('mousemove', (e) => handleMouseMove(e));// Перестаем рисовать, если кнопка мыши отпущена или вышла за пределы канвасаuserCanvas.addEventListener('mouseup', () => isDrawing = false);userCanvas.addEventListener('mouseleave', () => isDrawing = false);// Отрисовываем сеткуdrawGrid(userContext);drawGrid(netContext);

Демонстрация работы нейросети

Обучим сеть двум ключевым образам, буквам Т и Н:

Эталонные образы для обучения сетиЭталонные образы для обучения сети

Теперь проверим работу сети на искаженных образах:

Попытка распознать искаженный образ буквы НПопытка распознать искаженный образ буквы НПопытка распознать искаженный образ буквы ТПопытка распознать искаженный образ буквы Т

Программа работает! Сеть успешно восстановила исходные образы.

В заключение стоит отметить, что для сети Хопфилда число запоминаемых образов mнедолжно превышать величины, примерно равной 0.15 * n(где n размерность входного сигнала иколичество нейронов). Кроме того, если образы имеют сильное сходство, то они, возможно, будут вызывать усети перекрестные ассоциации, тоесть предъявление навходы сети вектораА приведет кпоявлению наеевыходах вектораБ инаоборот.

Исходникина Github и демо.

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

Подробнее..

Прокачиваем разметку мультимодальных данных меньше асессоров, больше слоёв

18.08.2020 18:15:11 | Автор: admin

Всем привет! Мы учёные лаборатории Машинное обучение ИТМО и команда Core ML ВКонтакте проводим совместные исследования. Одна из важных задач VK заключается в автоматической классификации постов: она необходима не только чтобы формировать тематические ленты, но и определять нежелательный контент. Для такой обработки записей привлекаются асессоры. При этом стоимость их работы можно значительно снизить с помощью такой парадигмы machine learning, как активное обучение.


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


image


Введение


Активное обучение это раздел machine learning, где модель взаимодействует с учителем. Она запрашивает у него для тренировки лишь те данные, которые позволят обучиться лучше и, следовательно, быстрее.


Это направление интересно компаниям, которые привлекают асессоров для разметки данных (например, с помощью сервисов Amazon Mechanical Turk, Яндекс.Толока) и хотят удешевить этот процесс. Один из вариантов использовать reCAPTCHA, где пользователь должен отмечать снимки, скажем, со светофорами, и заодно получать бесплатную разметку для Google Street View. Другой способ применять активное обучение.


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


Компания Amazon в своей работе описывает фреймворк DALC (Deep Active Learning from targeted Crowds). Он раскрывает концепцию активного обучения с точки зрения нейронных сетей, байесовского подхода и краудсорсинга. В исследовании в том числе используется техника Monte Carlo Dropout (о ней мы тоже поговорим в этой статье). Ещё авторы вводят любопытное понятие noisy annotation. Если в большинстве работ по активному обучению предполагается, что асессор говорит правду и ничего, кроме правды, то здесь допускается вероятность ошибки в силу человеческого фактора.


Про ещё одно исследование от Amazon можно прочитать здесь. В нём рассматривается концепция иерархической разметки: когда асессор вместо классической одноклассовой разметки объекта должен дать бинарный ответ о его принадлежности к определённому надклассу/классу в иерархии. При этом бинарные вопросы, как и объект для разметки, выбирает сам алгоритм. Таким образом, конечная разметка может получиться неполной: будет определён не конечный класс объекта, а его категория. Однако этого будет достаточно для обучения.


Но хватит говорить про использование парадигмы пора рассмотреть активное обучение в деталях! У него есть несколько основных подходов, или сценариев. В нашем исследовании модель взаимодействовала с учителем по сценарию pool-based sampling.


Рис. 1. Общая схема pool-based сценария активного обучения
Рис. 1. Общая схема pool-based сценария активного обучения


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


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


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


Набор данных и задача


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


  1. векторное представление, или эмбеддинг (англ. embedding), картинки поста;
  2. векторное представление текста.

Стоит отметить, что набор данных сильно несбалансирован (см. рис. 2).


Рис. 2 гистограмма распределения классов
Рис. 2 гистограмма распределения классов


Модель для классификации


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


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


В проекте мы экспериментально изучили различные конфигурации глубоких нейронных сетей. Проводились эксперименты с добавлением residual соединений, highway блоков, использованием энкодеров (англ. encoder). Рассмотрели разные варианты, учитывающие мультимодальность на основе слияния (англ. fusion): метод внимания для мультимодальных данных, матричное слияние.
Но некоторые способы учёта мультимодальности данных невозможно было применить к нашей задаче например, выравнивание и обучение различным представлениям. Это связано с готовым представлением данных в виде предобученных векторов-эмбеддингов.


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


Критерием для выбора итоговой модели стала максимизация значения метрики валидационной точности. Итак, в качестве классификатора была подобрана следующая архитектура (рис. 3):


Рис. 3. Подобранная архитектура для классификации
Рис. 3. Подобранная архитектура для классификации


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


Блоки, обозначенные красным и синим на рис. 3, имеют следующий вид:


Рис. 4. Описание основных блоков модели нейронной сети для классификации
Рис. 4. Описание основных блоков модели нейронной сети для классификации


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


Один из закономерных и важных вопросов, которые возникли при построении этой архитектуры: как считать функцию потерь? Варианты:


  1. простое покомпонентное суммирование элементов функции потерь с разных голов;
  2. взвешенная функция потерь с ручным перебором весов;
  3. взвешенная функция потерь с обученными весами голов.

Мы выбрали третий вариант. Ещё вдохновились статьёй из области байесовского глубокого обучения: в ней комбинации функций потерь выводятся исходя из формулы maximum likelihood и учёта неопределённости предсказаний, возникающих из-за шума в данных. В итоге мы остановились на такой функции потерь:


$L = \frac{1}{\sigma_1 ^ 2}L_1 + \frac{1}{\sigma_2 ^ 2}L_2 + \frac{1}{\sigma_3 ^ 2}L_3 + \log{\sigma_1} + \log{\sigma_2} + \log{\sigma_3}$


где $L_1, L_2, L_3$ функции потерь для разных выходов модели (в нашем случае они представляют собой категориальную кросс-энтропию), а $\sigma_1, \sigma_2, \sigma_3$ настраиваемые параметры, характеризующие дисперсию и шум в данных.


Pool-based sampling


С моделью определились теперь опишем, как будем применять и тестировать различные стратегии активного обучения. Согласно сценарию pool-based sampling мы составили пайплайн экспериментов:


  1. Берём из тренировочного набора данных какое-то количество случайных объектов.
  2. Обучаем на них модель.
  3. Выбираем новую пачку данных из оставшегося тренировочного набора, основываясь на тестируемой стратегии, и добавляем их к размеченным данным.
  4. Дообучаем модель.
  5. Считаем метрики (валидационную точность).
  6. Повторяем шаги 35 до выполнения определённого критерия (например, пока не кончится весь тренировочный набор данных).

Первые два шага соответствуют пассивной фазе обучения, шаги 36 активной.


Помимо самой стратегии, в данном пайплайне значимыми являются два параметра, а именно:


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


  2. Размер запроса к эксперту. С одной стороны, можно отправлять объекты к эксперту по одному. В этом случае первый объект в запросе будет максимизировать критерий рассматриваемой стратегии активного обучения (при сортировке объектов по убыванию соответствия критерию). И после обучения на этом объекте остальные в запросе, скорее всего, перестанут представлять интерес. Но если выбирать объекты по одному, то эксперимент затянется и всё исследование усложнится. Поэтому мы остановились на 20 объектах в запросе.
    Ещё можно варьировать количество шагов в фазе активного обучения. Очевидно, что с его увеличением точность модели может расти. Но цель нашего проекта не достичь максимальной возможной точности классификации, а исследовать эффективность активного обучения. Поэтому мы решили зафиксировать количество шагов на 100 или 200.



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


Инсайт 1: влияние выбора batch size


В качестве baseline рассмотрим, как обучается модель при случайном выборе данных (будем называть это пассивным обучением) (рис. 5).


Рис. 5. График обучения baseline-модели пассивным способом. Приведён результат пяти запусков с доверительным интервалом
Рис. 5. График обучения baseline-модели пассивным способом. Приведён результат пяти запусков с доверительным интервалом


Для достоверности этот и все последующие эксперименты запускались по пять раз с разным random state. На графиках выводится средняя точность запусков с доверительным интервалом.


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


Чтобы устранить этот эффект, нужно осознать важность параметра размера батча (англ. batch size). В нашем случае он был по дефолту выбран равным 512 из-за большого количества классов (50). Получалось, что при конечном размере размеченного набора данных и фиксированном batch size последний батч мог оказаться крайне мал. Это вносило шум в значение градиента и негативно сказывалось на обучении модели в целом. Мы опробовали следующие варианты решения этой проблемы:


  1. upsample, чтобы порции данных были одной длины;
  2. увеличение числа эпох обучения, чтобы влияние маленького батча нивелировалось последующими.

Итоговым решением стало формирование адаптивного batch size: на каждом шаге активного дообучения он вычислялся согласно формуле (1).


$current\_batch\_size =b + \Big \lfloor\frac{n \mod b}{\lfloor\frac{n}{b}\rfloor}\Big\rfloor [1]$


где $b$ изначальный batch size, а $n$ текущий размер размеченного набора данных.
Адаптивный подход помог сгладить просадки точности и получить монотонно возрастающий график (рис. 6).


Рис. 6. Сравнение использования фиксированного параметра batch size (passive на графике) и адаптивного (passive + flexible на графике)
Рис. 6. Сравнение использования фиксированного параметра batch size (passive на графике) и адаптивного (passive + flexible на графике)


Примечание: Эти графики показывают работу модели c одним выходом. Но, не умаляя общности, адаптивный batch size можно применять также для модели с тремя выходами. Так мы и сделали в дальнейших экспериментах.


Теперь перейдём непосредственно к исследованию методов активного обучения для нашей задачи.


Uncertainty


Первыми были реализованы наиболее простые стратегии активного обучения из обзорной статьи методы группы uncertainty sampling. Как следует из названия, стратегия основана на выборе для разметки тех объектов, в предсказании которых модель наименее уверена.


В статье приводятся три варианта подсчёта неуверенности:


1. Минимальная уверенность (англ. Least confident sampling)


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


$x^{*}_{LC} = \underset{x}{\arg\max} \ 1 - P_{\theta }(\hat{y}|x) [2]$


где $\hat{y} = \underset{y}{\arg\max}\ P_{\theta}(y|x)$ класс с наибольшей вероятностью при классификации моделью, $y$ один из возможных классов, $x$ один из объектов набора данных, $x^{*}_{LC}$ объект, выбранный с помощью стратегии наименьшей уверенности объект.


Подробнее

Эту меру можно понимать так. Допустим, функция потерь на объекте выглядит как $1-\hat{y}$. В таком случае модель выбирает объект, на котором получит худшую оценку значения функции потерь. Она обучается на нём и тем самым уменьшает значение функции потерь.


Но у этого метода есть недостаток. Например, на одном объекте модель получила следующее распределение по трём классам: {0,5; 0,49; 0,01}, а на другом {0,49; 0,255; 0,255}. В таком случае алгоритм выберет второй объект, так как его наиболее вероятное предсказание (0,49) меньше, чем у первого объекта (0,5). Хотя интуитивно понятно, что бльшую информативность для обучения имеет первый объект: вероятности первого и второго класса в предсказании почти равны. Алгоритм стоит модифицировать, чтобы учитывать такие ситуации.


2. Минимальный отступ (англ. Margin sampling)


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


$x^{*}_{M} = \underset{x}{\arg\min} \ P_{\theta }(\hat{y}_{1}|x) - P_{\theta }(\hat{y}_{2}|x)[3]$


где $\hat{y}_1$ наиболее вероятный класс для объекта $x$, $\hat{y}_2$ второй по вероятности класс.


Подробнее

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


3. Максимальная энтропия (англ. Entropy sampling)


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


$x^{*}_{H} = \underset{x}{\arg\max} -\sum \ P_{\theta }(y_{i}|x)\log{P_{\theta }(y_{i}|x)}[4]$


где $y_{i}$ вероятность $i$-го класса для объекта $x$ при классификации данной моделью.


Подробнее

Энтропия удобна тем, что она обобщает два метода, которые мы описали выше. Она выбирает объекты обоих типов:


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

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


Но практические результаты в рамках решаемой задачи показали расхождение с теорией (рис. 7).


Рис. 7. Результаты сравнения различных видов стратегии uncertainty sampling с пассивным обучением (слева с методом минимальной уверенности, по центру с методом минимального отступа, справа с методом максимальной энтропии)
Рис. 7. Результаты сравнения различных видов стратегии uncertainty sampling с пассивным обучением (слева с методом минимальной уверенности, по центру с методом минимального отступа, справа с методом максимальной энтропии)


Как можно заметить, методы least confident и entropy sampling показали себя хуже, чем пассивное обучение со случайным выбором объектов для дообучения. В то же время margin sampling оказался более эффективным.


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


Перечисленные методы просты с точки зрения реализации и обладают низкой вычислительной сложностью. Можно оценить сложность одного запроса к эксперту как $O(p\log{q})$, где $p$ размер неразмеченного набора данных, а $q$ число объектов в запросе к эксперту. Также эти методы легко применять на практике, так как они не требуют изменения используемой модели.


BALD


Следующая стратегия, о которой пойдёт речь, BALD sampling (Bayesian Active Learning by Disagreement). Это байесовский подход к измерению неуверенности комитета моделей.


Согласно классификации методов активного обучения, это представитель стратегии query-by-committee (QBC). Её основная идея в использовании предсказаний нескольких моделей с конкурирующими гипотезами. Можно брать их усреднённое предсказание за основу для uncertainty sampling. Или выбирать для разметки те объекты, в предсказаниях которых модели несогласны в большей степени. Эксперименты проводились с методом QBC на основе Monte Carlo Dropout, речь о котором пойдёт дальше.


Проблема классических байесовских методов для глубокого обучения в том, что необходимо выводить большое количество параметров, а это делает обучение моделей в два раза дороже. Поэтому авторы предложили использовать dropout в качестве способа байесовской аппроксимации. Этот подход отличается от привычного применения dropout тем, что он используется во время инференса (на стадии предсказания). Причём для каждого объекта выборки предсказание делается несколько раз одной и той же моделью, но с разными dropout-масками (рис. 8). Такой способ сэмплирования называется Monte Carlo Dropout (MC Dropout) и не требует увеличения затрат памяти при обучении модели. Так с помощью одной модели можно получить несколько предсказаний, которые могут различаться для одного и того же объекта. Несогласие моделей (где они отличаются друг от друга лишь масками dropout) считается на основе Mutual Information (MI). MI здесь представляет собой эпистемическую неопределённость, или неуверенность комитета, то есть такой вид неопределённости, которая уменьшается с добавлением новых данных. Это согласуется с концепцией активного обучения в целом.


Рис. 8. Иллюстрация MC Dropout для метода BALD
Рис. 8. Иллюстрация MC Dropout для метода BALD


Итак, для начала мы использовали усреднённое предсказание полученного QBC на основе MC Dropout комитета и применили к нему различные методы uncertainty sampling. Это не дало прироста по сравнению с соответствующими методами, использующими только одно предсказание (рис. 9).


Рис. 9. Результаты сравнения различных видов стратегии uncertainty sampling на основе QBC и без него с пассивным обучением (слева - с методом минимальной уверенности, по центру - с методом минимального отступа, справа - с методом максимальной энтропии)
Рис. 9. Результаты сравнения различных видов стратегии uncertainty sampling (на основе QBC и без него) с пассивным обучением (слева с методом минимальной уверенности, по центру с методом минимального отступа, справа с методом максимальной энтропии)


На следующем шаге мы использовали меры несогласия комитета по методу BALD. Как уже было сказано, для этого применяется Mutual Information моделей комитета:


$a_{BALD}=\mathbb{H}(y_1,...,y_n)-\mathbb{E}[\mathbb{H}(y_1,...,y_n|\omega)] [5]$


$\mathbb{E}[\mathbb{H}(y_1,...,y_n|w)]=\frac{1}{k}\sum_{i=1}^{n}\sum_{j=1}^{k}\mathbb{H}(y_i|w_j) [6]$


где $n$ число классов, $k$ число моделей в комитете.


Первое слагаемое в формуле (5) представляет собой энтропию усреднённого предсказания комитета, второе среднюю энтропию каждой модели в отдельности. Таким образом, выбираются только те объекты, в предсказании для которых комитет менее всего согласен. Результаты применения метода BALD представлены на рис. 10.


Рис. 10. Результаты применения стратегии BALD в сравнении с пассивным способом обучения
Рис. 10. Результаты применения стратегии BALD в сравнении с пассивным способом обучения

К сожалению, пока что данный метод не дал ожидаемого результата на долгом запуске эксперимента, несмотря на прирост по сравнению с пассивным методом в начале.
Сложность алгоритмов стратегии query-by-committee в целом и BALD в частности пропорциональна числу предсказаний, сделанных для каждого объекта. В свою очередь, сложность предсказания для каждого объекта аналогична методам uncertainty sampling. Таким образом, сложность одного запроса $O(kp\log(q))$, где $p$ размер неразмеченного набора данных, $q$ число объектов в запросе к эксперту, а $k$ число предсказаний, посчитанных для одного объекта.


На практике применить метод BALD может быть нелегко при использовании фреймворка tf.keras, так как он не обладает достаточной гибкостью для работы со слоями. Поэтому в рамках данного проекта мы выбрали фреймворк PyTorch, который позволил не только с легкостью включать dropout во время инференса, но и отключать batch normalization в течение активной фазы, о чем пойдет речь далее.


Инсайт 2: отключение batch normalization в активной фазе


Подобранная модель классификации в своей структуре использует слои batch normalization. Суть подхода batch normalization в обучении параметров нормализации данных во время обучения и применении найденных параметров во время инференса, или предсказания. Идея, которую мы использовали, состоит в том, чтобы рассматривать активную фазу обучения как этап инференса, и отключать на ней обучение batch normalization. К тому же интуитивно кажется, что такой подход позволит избежать смещения модели. Насколько нам известно, данный вопрос ещё не исследовался в отношении методов активного обучения. Для экспериментов мы взяли за основу метод BALD. Рассмотрим результаты (рис. 11).


Рис. 11. Результаты отключения batch normalization для метода BALD в сравнении со стандартным методом и пассивным обучением
Рис. 11. Результаты отключения batch normalization для метода BALD в сравнении со стандартным методом и пассивным обучением


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


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


Learning loss


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


Создадим вспомогательную модель, принимающую на вход выходы промежуточных и последних слоёв модели. Задача вспомогательной модели предсказывать значение функции потерь. Мы будем выбирать для разметки те объекты, для которых это значение максимально. Этот метод называется learning loss, подробнее про него можно почитать здесь. Рассмотрим результаты первичных экспериментов, где метод применялся для базовой модели (рис. 12).


Рис. 12. Результаты применения Learning loss для базовой модели в сравнении с ее пассивным обучением
Рис. 12. Результаты применения Learning loss для базовой модели в сравнении с ее пассивным обучением

Метод learning loss не дал прироста по сравнению с пассивным обучением на случайно выбранных объектах. Логично было бы использовать его для моделей других архитектур или отказаться от него как от неэффективного для нашей задачи.
Но мы вместо этого попробуем провести следующий эксперимент. В обычном сценарии активного обучения модель не знает настоящих меток классов, а в нашей задаче они известны. Это позволяет посчитать идеальный learning loss: зная настоящую метку класса объекта, будем считать на нём значение функции потерь и добавлять в размеченный набор данных те объекты, у которых оно больше. Назовём такой подход ideal learning loss (рис. 13).


Рис. 13. Результаты применения ideal learning loss для базовой модели в сравнении с ее пассивным обучением
Рис. 13. Результаты применения ideal learning loss для базовой модели в сравнении с её пассивным обучением

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


  1. Обучаем модель на начальной выборке (2000 объектов), как для активного обучения;
  2. Выбираем из всего набора неразмеченных данных 10000 объектов (чтобы ускорить подсчёт);
  3. Для выбранных объектов неразмеченной выборки считаем значения функции потерь;
  4. Сортируем объекты по полученным значениям;
  5. Разбиваем на группы по 100 объектов;
  6. Для каждой группы параллельно обучаем на ней модель, стартуя с весов, полученных на шаге 1;
  7. Фиксируем получившиеся точности.

Далее считаем корреляцию Спирмена между точностью модели, обученной на определённой выборке, и средним значением функции потерь по каждому из объектов выборки. А также вычисляем, как средняя точность модели коррелирует со средним значением параметра отступа (из метода margin sampling).


Таблица 1. Корреляции точности и метрик активного обучения для набора данных публикаций ВКонтакте


Корреляция Спирмена p-value
Точность и loss -0,2518 0,0115
Точность и margin 0,2461 0,0136

Как видим, для метода margin sampling корреляция слабая и положительная то есть, зная отступ, можно быть уверенными, что объект полезен для обучения модели. А в случае c функцией потерь корреляция слабая отрицательная.


Возникает вопрос: а что если попробовать выбирать для разметки объекты с наименьшими значениями функции потерь?
Как ни странно, эксперименты показали, что и такой вариант тоже не работает ожидаемым образом (рис. 14).


Рис. 14. Результаты применения обратного ideal learning loss для базовой модели в сравнении с прямым ideal learning loss и пассивным обучением
Рис. 14. Результаты применения обратного ideal learning loss для базовой модели в сравнении с прямым ideal learning loss и пассивным обучением


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


Таблица 2. Корреляции точности и метрик активного обучения для набора данных MNIST


Корреляция Спирмена p-value
Точность и loss 0,2140 0,0326
Точность и энтропия 0,2040 0,0418

При этом сам метод ideal learning loss работает так, как ожидается (рис. 15).


Рис. 15. Активное обучение классификатора символов из набора данных MNIST стратегией ideal learning loss. Синий график ideal learning loss, оранжевый пассивное обучение
Рис. 15. Активное обучение классификатора символов из набора данных MNIST стратегией ideal learning loss. Синий график ideal learning loss, оранжевый пассивное обучение

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


Сложность метода learning loss та же, что и для методов uncertainty sampling: $O(p\log{q})$, где $p$ размер неразмеченного набора данных, а $q$ число объектов в запросе к эксперту. Но важно учесть, что при его применении обучать нужно не только основную модель, но и вспомогательную. Этот метод сложнее предыдущих в практическом применении ещё и потому, что требует проводить обучение каскада моделей.


Заключение


Не будем бесконечно раздувать статью, рассказывая обо всех применённых методах и проведённых экспериментах. Здесь мы постарались осветить основные и наиболее интересные. Любопытно, что эффективнее всех оказался самый первый и простой метод margin sampling результаты его длинного запуска можно увидеть на рис. 16.
Рис. 16. Сравнение обучения на случайно выбираемых данных (пассивное обучение) и на данных, выбираемых стратегией margin sampling
Рис. 16. Сравнение обучения на случайно выбираемых данных (пассивное обучение) и на данных, выбираемых стратегией margin sampling


График показывает: тренируя модель с помощью активного обучения (в нашем случае стратегией margin sampling), можно достичь предельной точности такой же, как у модели, обученной пассивным способом. Но при этом использовать на 25 тыс. объектов меньше. Экономия ресурсов разметки составит порядка 25% это довольно значимо.


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


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


  • выбор batch size;
  • целесообразность использования для активного обучения подходов, зарекомендовавших себя в глубоких нейронных сетях, например, batch normalization.
Подробнее..

Поиск автовладельцев в Instagram от хвостов китов до автомобилей

06.08.2020 08:23:05 | Автор: admin

image


К нам в рекламную группу Dentsu Aegis Network часто приходят компании-рекламодатели с запросом изучить и проанализировать их целевую аудиторию. И сделать это необходимо быстро и точно. Предположим, у нас есть клиент из автопрома, который хочет найти владельцев авто, а потом узнать их интересы, пол, возраст в общем, раскрасить аудиторию. Логично было бы сделать социологическое исследование, но это займет несколько недель. А если у клиента очень дорогие авто стоимостью выше 2,5 млн рублей? Много ли таких владельцев наберется для исследования? А для фокус-группы?


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


Будем решать задачи поступательно. Давайте подумаем, как найти автовладельца в социальной сети.


  1. Провести текстовый анализ постов
    Казалось бы, здесь все просто. Написал помыл свою зайку/ласточку или вечерние покатушки, и мы нашли нужного человека. Но потом выясняется, что часть примеров нерелевантна, а чистить приходится руками.
  2. Поискать в хэштегах
    Тоже вариант, но не соответствующих запросу аккаунтов еще больше: попадается и коммерция, и ребята, которые тюнингуют старые машины, и дрифтеры. А мы ищем владельцев авто за 2,5 млн рублей здесь и сейчас.
  3. Найти пост с фотографией того авто, которое нам нужно и определить модель
    Затем нужно придумать эвристику, которая с большой вероятностью бы говорила, что владелец этого аккаунта в соцсети также является владельцем нужной нам машины.

Мы пробовали все варианты, но остановились на последнем.


Первый подход к снаряду


Итак, нам необходима модель, которая бы определяла марку и производителя авто. Но сколько марок и производителей мы хотим охватить? Здравый смысл подсказывает, что можем взять наших клиентов, их основных конкурентов и на этом остановиться. Около 100 различных марок автомобилей более чем достаточно. Каждая марка автомобиля будет являться отдельным классом в модели.


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


А что если у нас появится новый клиент из нового сегмента? Будем добирать еще 50-100 марок? Да, есть компании, которые идут именно таким путем, новая проблема это новая модель. В итоге получается зоопарк различных моделей. Мы решили, что на обучение новой модели у нас просто нет времени, поэтому сделаем все сразу.


Небольшая, но важная подготовка датасета


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


Технически это, конечно, автомобили, но не совсем те, которые нужны нам.
Гораздо хуже, когда результат такой:
image


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


Существующие подходы в компьютерном зрении

image
Источник
Object detection это технология, связанная с компьютерным зрением и обработкой изображений, которая позволяет находить объекты определенного класса на изображениях и видео.


В качестве архитектуры взяли retinanet, так как уже был готов весь пайплайн, нужно только подложить разметку. Для разметки воспользовались инструментом CVAT (подробнее мы рассказывали на pycon19) и всей командой потратили несколько часов на это веселое занятие. За это время удалось разметить несколько тысяч картинок, что позволило обучить модель с mAP ~ 0.97.


С какими сложностями мы столкнулись при подготовке набора данных? Первое, что хочется отметить это отсутствие автолюбителей в нашей команде, из-за чего иногда возникали споры по поводу сложных случаев, например, когда кузов авто визуально едва ли отличим на разных моделях. Хорошим примером могут послужить Lexus RX и Lexus NX.
image


Гораздо сложнее, когда кузов один и тот же, а названия автомобилей разные. Такое случается, когда бренд по разному себя позиционирует на разных рынках. Примеры Chevrolet Spark и Ravon R2:
image


Autoencoder


Приступаем к выбору модели. Первое что пришло нам на ум, это автоэнкодеры.


Автоэнкодер

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


  • Энкодер сжимает входные данные в скрытое пространство (latent space).
  • Декодер восстанавливает входные данные из скрытого пространства.

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


С помощью автоэнкодеров кластеризуют даже Trading Card Game карточки, например, Magic the Gathering, так что появилось желание сделать кластеризацию автомобилей именно через этот инструмент. К сожалению, получилось неудачно: время потратили, а результат не оправдал ожиданий. Стали думать дальше.


Semantic Embeddings


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


Идея подробно:

Иерархия классов представляет собой направленный ацикличный граф $G = (V, E)$ с множеством вершин $V$ и множеством ребер $EVV$, что определяет гипонимические связи между семантическими понятиями. Другими словами, ребро $(u,v)E$ означает что $v$ является подклассом $u$. Тогда классы являются вершинами такого графа $C={c_{1},...,c_{n}}V$. Пример графа представлен ниже:



Авторы использовали меру непохожести $d_{G}:CC R$, рассчитываемую по формуле $d_{G}(u,v) =\frac{height(lcs(u,v))}{max_{wV}height(w)}$, где под высотой имеется в виду самый длинный путь от текущей вершины до листа. $lcs$ двух вершин это ближайший предок к этим двум вершинам. Так как $d_{G}$ ограничено между 0 и 1, авторы определили меру семантической близости между двумя семантическими понятиями как $s_{G}(u,v) = 1 d_{G}(u,v)$.
Рассмотрим граф из рисунка выше, его высота равняется 3, $inline$lcs("dog","cat")="mammal"$inline$, а $inline$lcs("dog","trout")="animal"$inline$, тогда $inline$s_{G}("dog","cat") = 1 d_{G}("dog","cat")=1-1/3=2/3$inline$, а $inline$s_{G}("dog","trout")=1-2/3=1/3$inline$. Таким образом, кошка и собака в представленной модели более близки семантически друг к другу, чем собака и форель.
Цель авторов посчитать вектора единичной длины $(c_{i})^n$ для всех классов $c_{i}, i=1,...,n$, так, чтобы скалярное произведение векторов соответствующих классов было равно мере их похожести:

$$display$$_{1 i,j n}:(c_{i})^T(c_{j}) =s_{G}(c_{i},c_{j})$$display$$


$$display$$_{1 i n}:(c_{i})= 1$$display$$



Собирать такой датасет показалось слишком долгим и дорогим процессом, ведь помимо сбора релевантных фотографий необходимо думать над грамотной иерархией классов по текстовым запросам. Но авторы предлагают вариант без иерархии, с поддержкой датасета Stanford Cars, который имеет 196 различных классов автомобилей и по 80 фотографий на каждый (почти то, что нам нужно, да?). Результат на stanford cars оказался лучшим, чем все то, что было до этого. Но на наших данных повторить успех не удалось. Понять, что на это повлияло плохая разметка, шум в данных или что-то еще не удалось, так как время на эксперименты закончилось, а проект был отложен на неопределенный срок.


Примерно так чувствовала себя наша нейросеть на тот момент:

Siamese Networks или от китов к машинам


Спустя 9 месяцев снова родилась необходимость в определении модели авто по фото. На этот раз у нас была возможность привлечь команду асессоров, чтобы собрать более качественный датасет. А самое главное, появилось понимание, какие марки авто нам нужны точно и какие необходимо добавить, чтобы иметь задел на будущее. Вместе с более качественной разметкой пришла идея использовать metric learning подход, например, сиамские сети c triplet loss.


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



Было привлекательно, что решение представляло из себя всего одну модель, а не целый зоопарк, как это бывает на kaggle. Из интересных особенностей, которые могли быть использованы у нас: на вход к 3 стандартным RGB каналам подаются маски. Поэтому размер входа составляет 512х256х4.


В первом подходе мы решили обучить модель без использования масок и получили на валидации mAP = 0.89. Такой результат с самого старта нас очень обрадовал. Но прогнав модель на отложенной выборке из живой среды обитания результат оказался плачевен.


И снова похожие ошибки:

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



Что поступало на вход, и это ещё очень удачный пример:



Очевидно, что нужно было что-то сделать с обучающим набором данных. Мы решили оставить всё как есть, но использовать более сильную аугментацию. Для этого использовали пакет albumentations. Он поддерживает bounding boxы и maskи, имеет множество готовых преобразований: помимо стандартных flip-crop-rotate еще и различные distortionы.


К более сильной аугментации решили сразу добавить маски. Для предсказания масок использовали фейсбуковский detectron. Мотивацией послужило наличие модели для сегментации изображений, обученной на датасете COCO, в котором присутствует класс авто. И наличие пайплайна под детектрон, потому что он уже был использован в команде. А еще мы любим копаться в гите facebookresearch.


Обучив модель на дополненных данных с новой аугментацией, мы смогли получить mAP ~ 0.81. Это сильно хуже предыдущего результата, но зато модель получилась более жизнеспособная.


Эвристики


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


  • Пользователь сфотографировался с автомобилем на фоне.
  • Авто могло оказаться на фоне случайно.
  • Автолюбитель выкладывает фотографии его любимых марок. Тут может быть случай и многих различных марок, и одной единственной.
  • Мы попали в профиль автодилера, автомастерской или автоблогера.
  • ...
  • И десятки других вариантов.

Как понять, что две фотографии одного и того же автомобиля являются одним и тем же автомобилем? Можно определить цвет автомобиля. Но с цветом имеются свои проблемы, так как даже зеленый цвет при определенном освещении выглядит серым. Кажется, что если мы научились отличать несколько сотен моделей авто друг от друга, то с цветом проблем возникнуть не должно. Поэтому мы взяли пайплайн для retinanet и обучили еще одну нейронную сеть на определение 16 цветов.


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


Заключение и применение модели


Мы научились находить владельцев определенных авто по их постам. Что нам для этого понадобилось: нейронная сеть для первичной чистки данных; 5 человек, помогающих с разметкой; нейронная сеть для создания масок; нейронная сеть для определения марки автомобиля; нейронная сеть для определения цвета автомобиля. Почему бы теперь не применить результат в бою?


Попробуем найти владельцев BMW с большим количеством подписчиков:



Фотография BMW M5. Источник.



Ещё один пример BMW M5 от того же автора. Источник.



BMW 3 серии. Источник.



BMW M3. Источник.


Зачем нам искать владельцев BMW с большим количеством подписчиков, которые часто постят свой автомобиль? Например, они могут стать амбассадорами бренда.


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



Источник.



Источник.



Источник.


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


Распределение по полу:
image


Распределение по возрасту:
image


Топ-10 верхнеуровневых интересов:
image


Спустимся на два уровня ниже в категорию Спорт и активных отдых:
image


Кажется, что Audi преимущественно не женский автомобиль. Разберемся почему же у нас выходит 57% женщин? Согласно исследованию brand analytics, распределение мужчин и женщин в инстаграме соотносится как 25:75. Учитывая этот факт, можно сделать перевзвес наших данных и получить более натуральное распределение по полу среди автовладельцев Audi. По этой причине при анализе социальной сети необходимо учитывать её специфику.


Что ещё мы можем узнать про автовладельцев?


Например, откуда они:
image


И куда они путешествуют:
image


Что можно сделать еще?


Здесь можно выделить два направления: улучшение текущего решения и новые подходы. Гипотезы для улучшения модели:


  • В первую очередь хотелось бы ещё раз пройтись по датасету. Как известно, есть прямая взаимосвязь между качеством моделей машинного обучения и данных, которые они используют.
  • Mixup аугментация. Смысл её в том, чтобы с разными весами смешать две картинки в одну. Веса при этом должны давать в сумме единицу. Сложность заключается в том, что для такой аугментации нужны несколько картинок. В то время как пакет albumentation работает с одной картинкой на вход. Делать самописное решение или добавлять стороннее для проверки гипотезы на тот момент показалось нецелесообразным.
    Пример:
    image
    Источник
  • Попробовать другие архитектуры нейронных сетей. Тут всё просто мы взяли решение первого места. Но ведь можно попробовать более простые архитектуры, потерять немного в качестве, но сильно выиграть в скорости. Ведь мы не на kaggle, и нам не так важны сотые и тысячные значения в метрике качества.
  • Добавить задачу определения цвета автомобиля в нейронную сеть определяющую марку и модель автомобиля. Иногда оказывается, что добавление дополнительного выхода в нейронную сеть для решения ещё одной проблемы повышает и качество метрики, и обобщающую способность.

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


image
Источник


Благодарим за внимание и надеемся что этот материал будет полезен и интересен читателям Хабра!


Статья написана при поддержке моих коллег Артёма Королёва, Алексея Маркитантова и Арины Решетниковой.


R&D Dentsu Aegis Network Russia.

Подробнее..

ИИ итоги уходящего 2020-го года в мире машинного обучения

01.01.2021 00:16:35 | Автор: admin

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

Если тебе интересно машинное обучение, то приглашаю вМишин Лернинг мой субъективный телеграм-канал об искусстве глубокого обучения, нейронных сетях и новостях из мира искусственного интеллекта.

GPT-3

Ресерчеры из OpenAI представили GPT-3 сеть Generative Pre-trained Transformer 3, одну из лучших языковых моделей на сегодняшний день. Архитектура GPT-3 подобна GPT-2, параметров стало 175 миллиардов, и модель учили на 570 гигабайтах текста.

Более крупные модели позволяют более эффективно использовать контекстную информацию.Более крупные модели позволяют более эффективно использовать контекстную информацию.

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


NVIDIA MAXINE

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

MuZero

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

Иллюстрация поиска по дереву Монте-Карло, используя MuZeroИллюстрация поиска по дереву Монте-Карло, используя MuZero

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

Алгоритм уже умеет играть в привычные нам для этой сферы игры: го, шахматы и кучу игр для приставки Atari.

AlphaFold

И опять DeepMind с их нейросетевым алгоритмом впредсказания трехмерной структуры белка по последовательности аминокислот AlphaFold 2. Точность такого алгоритма составляет 92,4 балла из 100, что является рекордом на сегодняшний день!

При считывании информации с матричной РНК (трансляция) молекула белка формируется как цепочка аминокислот. Потом, в зависимости от физических и химических свойств, цепочка начинает сворачиваться. Таким образом формируется третичная структура белка. Именно от этой структуры и зависят свойства конкретного белка.

Задача определения первичной аминокислотной структуры белка (самой последовательности) является элементарной. Это легко читается напрямую из ДНК. Кодон тройка нуклеотидных остатков (триплет) образуют 64 варианта, а именно 4 (аденин, гуанин, цитозин, тимин) в третей степени. Из них 61 комбинация кодирует определённые аминокислоты в будущем белке, а 3 оставшихся кодона сигнализируют об остановке трансляции и называются стоп-кодонами. При этом 61 комбинация кодирует всего 20 различный аминокислот белка. Знакомые с теорий информации и избыточностью уже улыбаются, ну, а биологи называют кодоны, кодирующие одинаковые аминокислоты, изоакцепторными кодонами.

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

Процесс предсказания третичной структуры белкаПроцесс предсказания третичной структуры белка

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

Чтобы сделать публикацию еще более полезной, я добавил образовательные материалы вышедшие в 2020м году:

Новая специализация TensorFlow: Advanced Techniques от deeplearning ai, основанной самим Andrew Ng

YouTube-Лекция: Нейронные сети: как их создают и где применяют? Два часа о самом главном

Поздравляю всех с Новым годом! Больше ресерча в наступающем году!

Подробнее..

DALL E от OpenAi Генерация изображений из текста. Один из важнейших прорывов ИИ в начале 2021 года

06.01.2021 06:14:44 | Автор: admin

Пару дней назад мы подводили ИИ итоги 2020-го года в мире машинного обучения. 2021-й год только начался, но мы определенно видим одну из важнейших работ в области ИИ текущего года.

Итак, исследователи в области искусственного интеллекта из openai создали нейронную сеть под названием DALL E, которая генерирует изображения из текстового описания на естественном языке.

Если тебе интересно машинное обучение, то приглашаю вМишин Лернинг мой субъективный телеграм-канал об искусстве глубокого обучения, нейронных сетях и новостях из мира искусственного интеллекта.

DALL E представляет собой версиюGPT-3с 12 миллиардами параметров,обученную генерировать изображения из текстовых описаний на датасете из пар текст-изображение.Исследователи обнаружили, что DALL E обладает огромным репертуаром генеративных возможностей, включая возможность создания антропоморфных животных и других необычных объектов, комбинирующих совершенно нетривиальные свойства, например "кресло в форме авокадо."

Изображения, сгенерированные DALL E на основании текстового описания "кресло в форме авокадо"Изображения, сгенерированные DALL E на основании текстового описания "кресло в форме авокадо"

Можно сказать, что уже были все предпосылки к созданию DALL E: прошлогодний триумф GPT-3 и успешное создание Image GPT сети, способной к генерации изображений на основе текста, использующей языковую модель трансформер GPT-2. Все уже подходило к тому, чтобы создать новую модель, взяв в этот раз за основу GPT-3. И теперь DALL E показывает невиданные доселе чудеса манипулирования визуальными концепциями с помощью естественного языка!

Как и GPT-3, DALL E это языковая модель-трансформер, принимающая на вход текст и изображение, как последовательность размером до 1280 токенов. Модель обучена максимизировать правдоподобие при генерации токенов, следующих один за другим.

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

Давайте посмотрим на примеры, которые говорят сами за себя. Исследователи утверждают, что не использовали ручной "cherry picking". Примерами являются изображения, полученные при помощи DALL E, в которых используются 32 лучших примера из 512-ти сгенерированных, отобранных созданным ранее (теми же openai) нейронным ранжированиемCLIP.

Text: a collection of glasses sitting on the table

Изображения, сгенерированные DALL EИзображения, сгенерированные DALL E

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

Text: an emoji of a baby penguin wearing a blue hat, red gloves, green shirt, and yellow pants

Эмодзи пингвиненка, одетого в голубую шапку, красные перчатки, зеленую футболку и желтые штаны Эмодзи пингвиненка, одетого в голубую шапку, красные перчатки, зеленую футболку и желтые штаны

DALL E может не только генерировать изображение с нуля, но и регенерировать (достраивать) любую прямоугольную область существующего изображения, вплоть до нижнего правого угла изображения, в соответствии с текстовым описанием. В качестве примера за основу взяли верхнюю часть фотографии бюста Гомера. Модель принимает на вход это изображение и текст: a photograph of a bust of homer

Text: a photograph of a bust of homer

Фотография бюста ГомераФотография бюста Гомера

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

Text: a photo of phone from the ...

Фотографии телефонов разных десятилетий XX векаФотографии телефонов разных десятилетий XX века

Название модели DALL E является словослиянием имени художника Сальвадора Дали и робота WALL E от Pixar. Вышел такой своеобразный Вали-Дали. Вообще в мире ИИ "придумывание" таких оригинальных названий это некий тренд. Что определенно радует, и делает эту область еще более оригинальной.

Старый добрый перенос стиля WALL E в DalСтарый добрый перенос стиля WALL E в Dal

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

Text: a snail made of harp

Улитка-Арфа. Фантастические твари и где они обитают..Улитка-Арфа. Фантастические твари и где они обитают..

Вывод

DALL E это декодер-трансформер, который принимает и текст, и изображение в виде единой последовательности токенов (1280 токенов = 256 для текста + 1024 для изображения) и далее генерирует изображения авторегрессивном режиме.

Что можно сказать? Наступает эра "великого объединения" языковых моделей, компьютерного зрения и генеративных сетей. То что мы видим сейчас, уже поражает воображение своими результатами, не говоря уже о том, насколько подобные подходы могут изменить процесс генерации контента.

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

Что ты думаешь о DALL E и подобных генеративных нейронных моделях, способных создавать визуальный контент по текстовому описанию? Где может быть полезна такая технология? Насколько тебя впечатлили результаты? Давай обсудим в комментариях.

Подробнее..

Миллион домашних фотографий лица, лица, лица

21.01.2021 02:23:02 | Автор: admin

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

Поэтому я решил немного углубиться в так называемый Face Recognition.

Все просто?

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

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

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

Но об этом чуть позже.

Белогривые лошадки?

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

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

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

На случай, если кого интересуют облачные решения, оставлю пару ссылок:

https://azure.microsoft.com/en-us/services/cognitive-services/face/

https://cloud.google.com/vision/docs/face-tutorial

https://aws.amazon.com/rekognition/

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

CPU -> GPU

Сначала я, будучи наивным, запустил поиск лиц по алгоритму CNN (см. ниже) на CPU.

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

Но, как и многие, я знал, что для ускорения некоторых типов вычислений можно применять GPU. Так вот, Face Recognition как из тех типов вычислений. Поэтому, почитав отзывы и подобрав подходящий вариант, я помчался на местную онлайн барахолку и за довольно небольшую сумму наличности купил GeForce GTX 1050 Ti. И, даже на такой скромной карточке, процесс пошел куда шустрее меньше секунды на одну фотографию! Но, увы, это не происходит по щелчку пальцев. Вначале надо чтобы весь зоопарк библиотек смог с этой видеокарточкой заработать.

И тут начинается веселье: сперва надо поставить драйвера для видеокарточки и CUDO. Потом поставить библиотеки с поддержкой CUDO? Нет, потом надо ставить сборочное окружение, так как теперь все те библиотеки, что запросто встали из репозиториев без поддержки GPU ускорения придется собирать и ставить руками.

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

Итак, сервер настроен, библиотеки стоят, вентиляторы охлаждения CPU и GPU работают на малых оборотах в ожидании задач, можно и начать распознавать лица.

Этапы распознавания

Весь этап распознавания лиц на фотографии можно разбить на несколько этапов:

  1. Нахождение лиц на фотографии (face detection)

  2. Поиск элементов лица (landmarks detection)

  3. Кодировка лица (face encoding)

  4. Сравнение лица с шаблонами (face matching)

Нахождение лиц на фотографии можно делать разными способами, но самые популярные это:

  • Гистограмма направленных градиентов (HOG).

  • Алгоритм на базе сверточных нейронных сетей (CNN).

HOG работает быстро, вполне достаточно CPU, но распознает хуже и только фронтальные лица.

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

Я использовал реализации этих алгоритмов из библиотеки face_recognition (пришлось немного ее модифицировать, чтобы бы заставить работать с элементами лица полученными в других библиотеках, см. ниже). Эта библиотека является по сути удобной оберткой над dlib.

При первом же прогоне полноформатной фотографии на 8 мегапикселей с цифрового фотоаппарата, видеокарта сказала: Упс и выкинула исключение о том, что, мол, милый друг, моих 4 GB видеопамяти для этой задачи маловато. Поэтому волевым решением было решено урезать все входные фотографии до 1000 пикселей по длинной стороне (max_image_size в конфиге, если что), этого размера вполне достаточно, если вас не сильно интересуют лица всяких прохожих далеко на заднем плане (о да, иногда, в процессе отладки, там попадались довольно странные лица)

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

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

Я игрался с двумя алгоритмами: первый из упомянутой выше библиотеки face_recognition (она же dlib), второй из библиотеки face-alignment.

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

Это меня немало так опечалило, и я предпринял две попытки это как-то исправить.

Вначале, решил в лоб научить нейронную сеть отличать хороший результат от плохого, руками отобрал примерно 10000 фотографий с хорошим/плохим результатом настроил нейронку и получил точность около 80%, что было совсем недостаточно, так как до этого система примерно на таком же количестве искала точки корректно. Да, немного точность повысилась, но процент брака, а соответственно потенциальной ручной работы, был велик.

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

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

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

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

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

Вот как раз с шаблонами и началась самая большая заморочка.

При малом количестве шаблонов (я в начале взял по одной фотографии лица каждого человека которого надо было распознавать, потом по 10) у меня так и не получилось подстроить пороги срабатывания так, чтобы близкие родственники однозначно распознавались, кто есть кто. Поставишь большой порог, система путает братьев/сестер, поставишь маленький, и для многих фотографий соответствия вообще не находится. В итоге, методом многократных итераций, подобрал максимальный порог при котором не было путаницы между отдельными людьми и смирился с тем что шаблонов придется делать много, очень много. Теперь если взять фотографии за тот год из которого нет ни одного шаблона, то лишь где-то две трети лиц будут однозначно распознаны, остальные либо попадут в категорию вроде бы похож (weak match) либо вообще не распознаны, такие приходиться посмотреть глазами, добавить в шаблоны и повторить итерацию сравнения.

Профильно-фронтальные проблемы

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

К сожалению, я не смог найти в открытом доступе готовых алгоритмов которые решают эту задачу и смирился с тем что шаблонов потребуется много. При этом я также понял, что в большинстве случаев для исходных задач (а ну-ка найди мне фотографии Алисы с бабушкой) чаще всего фотографии в профиль не так важны, поэтому добавил в систему возможность отсекать фотографии в профиль, тем самым снижая объем ручной работы. Так что, если кто-то захочет воспользоваться моей системой, но при этом не готов на подвиг в виде бессонных ночей рассматривания сотен и тысяч (пусть даже любимых) лиц, то можно просто использовать конфигурационный файл с именем frontal.cfg и снизить объем последующей ручной работы в несколько раз.

А что насчет видео?

Разобравшись с фотографиями, я решил, а не замахнуться ли нам на Вильяма, понимаете ли, нашего Шекспира распознавание лиц в видео? Эта задача, с практической точки зрения, выглядела даже более полезной чем предыдущая, так как, если на фотографии по иконкам в режиме предпросмотра можно довольно быстро понять кто есть кто, то на видео эта задача уже практически нерешаемая, лицо-то в видео далеко не всегда на первом же кадре. По реализации эта задача не очень сильно отличается от работы с фото, только вместо одного кадра на файл их теперь несколько тысяч.

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

Во-первых, распознавать только первые несколько минут видео (max_video_frames в конфиге) так как, чаще всего, все главные действующие лица уже появились на видео. Во-вторых, распознавать не каждый кадр, а с некоторым прореживанием (video_frames_step в конфиге) так как главные действующие лица не мелькают в кадре лишь на мгновение, а держатся долго. Ну и в довершение, лицо должно попасться не в одном кадре, а в нескольких (min_video_face_count в конфиге) по той же причине, если лицо только мигнуло, то для исходной задачи оно не так уж и важно.

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

Если кому-то захотелось воспользоваться моими наработками, все исходники и краткая инструкция тут.

Под катом я приведу

несколько скриншотов и сценариев использования системы

Представим, что вы из тех людей, что читают README файлы и систему уже поставили и даже запустили.

Перед вами главное окно. Запустим распознавание папки:

Recognition -> Add new files

(первый запуск будет довольно долгий, так как библиотеки подгружают необходимые модели из Интернета)

Получим кучу нераспознанных лиц:

Добавим (кликом на нужные или выделением с последующим кликом на одну из выделенных) их в шаблоны, введем новое имя для прекрасной незнакомки:

По мере добавления новых людей, список будет расширяться и вводить имена будет не нужно, достаточно выбрать из уже введённых ранее (заглавную фотографию человека можно заменить подложив в папку с шаблонами файл 0_face.jpg).

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

Если просочилась ошибка поиска элементов лица, до данную фотографию опасно добавлять в шаблоны, так как совершенно непредсказуемо, что именно потом с ним совпадет. Для этого есть галочка Bad encoding, текущая фотография будет отнесена к данному человеку, но в сравнениях с другими она участвовать не будет.

Повторим сравнение: Match -> Rematch folder.

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

Пока лица находятся в категории weak или unknown они не будут синхронизироваться и по ним не будет осуществляться поиск.

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

откроется оригинал фотографии.

А если хочется узнать с каким именно шаблоном было совпадение можно кликнуть на иконку

(если она подсвечена зеленым, то значит именно эта фотография и была использована как шаблон).

Ну распознали мы все, а что дальше?

Как дальше искать людей? Теперь в базе данных есть информация по каждой фотографии, кто на ней изображен. Отлично! Но как смотреть теперь? Поскольку у меня основной системой просмотра фотографий сейчас является Plex, то я не придумал ничего лучше, чем экспортировать данные о людях на фото в эту его базу в виде тегов. К сожалению, открытого API у них нет, но, к счастью, его база просто хранится в sqlite файле и имеет не очень сложный формат. Поэтому я просто пишу туда теги на прямую. (Не буду грузить статью деталями реализации базы данных Plex, но, если кому-то они интересны, могут посмотреть в исходниках в файле plexdb.py).

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

face-rec-plexsync -a set_tags

Немного подождать и вуаля! Теперь можно искать!

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

Например, с помощью вот такого запроса, можно найти все фотографии с Тимой и Алисой за 2020 год

face-rec-db -a find_files_by_names -f 2020 -n Тима,Алиса

Понятно, что смотреть фотографии в консоли не очень интересно, но если добавить после команды что-то вроде

| xargs -I{} ln -s {} /mnt/multimedia/query/

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

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

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

Заключение

Вот такой вот небольшой пример наведения порядка в домашних фотографиях.

Многое уже сделано, стало гораздо удобнее, но еще есть масса хотелок, надеюсь когда-нибудь и до них дойдут руки:

  • Распознать голос в видео и аудио записях. Есть десятки тысяч аудио заметок и искать по ним вообще невозможно, это даже хуже, чем видео.

  • Кроме лиц добавить распознание других объектов: животные, предметы.

  • Как фантазия на будущее, распознавать условия съемки и окружение: горы, море, в помещении и т.д.

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

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

Спасибо за внимание!

Подробнее..

Обнаружение объектов с помощью YOLOv3 на Tensorflow 2.0

08.05.2021 14:13:54 | Автор: admin
Кадр из аниме "Жрица и медведь"Кадр из аниме "Жрица и медведь"

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

В данной статье мы узнаем о системе YOLO Object Detection и как реализовать подобную систему в Tensorflow 2.0

О YOLO:

Наша унифицированная архитектура чрезвычайно быстра. Базовая модель YOLO обрабатывает изображения в режиме реального времени со скоростью 45 кадров в секунду. Уменьшенная версия сети, Fast YOLO, обрабатывает аж 155 кадра в секунду

You Only Look Once: Unified, Real-Time Object Detection, 2015

Что такое YOLO?

YOLO это новейшая (на момент написания оригинальной статьи) система (сеть) обнаружения объектов. Она была разработана Джозефом Редмоном (Joseph Redmon). Наибольшим преимуществом YOLO над другими архитектурами является скорость. Модели семейства YOLO исключительно быстры и намного превосходят R-CNN (Region-Based Convolutional Neural Network) и другие модели. Это позволяет добиться обнаружения объектов в режиме реального времени.

На момент первой публикации (в 2016 году) по сравнению с другими системами, такими как R-CNN и DPM (Deformable Part Model), YOLO добилась передового значения mAP (mean Average Precision). С другой стороны, YOLO испытывает трудности с точной локализацией объектов. Однако в новой версии были внесены улучшения в скорости и точности системы.

Альтернативы (на момент публикации статьи): Другие архитектуры в основном использовали метод скользящего окна по всему изображению, и классификатор использовался для определенной области изображения (DPM). Также, R-CNN использовал метод предложения регионов (region proposal method). Описываемый метод сначала создает потенциальные bounding boxы. Затем, на области, ограниченные bounding boxами, запускается классификатор и следующее удаление повторяющихся распознаваний, и уточнение границ рамок.

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

Теория

Так как YOLO необходимо только один взгляд на изображение, то метод скользящего окна не подходит в данной ситуации. Вместо этого, изображение будет поделено на сетку с ячейками размером S x S. Каждая ячейка может содержать несколько разных объектов для распознавания.

Во-первых, каждая ячейка отвечает за прогнозирование количества bounding boxов. Также, каждая ячейка прогнозирует доверительное значение (confidence value) для каждой области, ограниченной bounding boxом. Иными словами, это значение определяет вероятность нахождения того или иного объекта в данной области. То есть в случае, если какая-то ячейка сетки не имеет определенного объекта, важно, чтобы доверительное значение для этой области было низким.

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

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

Давайте подробней опишем вывод модели.

В YOLO используются anchor boxes (якорные рамки / фиксированные рамки) для прогнозирования bounding boxов. Идея anchor boxов сводится к предварительному определению двух различных форм. И таким образом, мы можем объединить два предсказания с двумя anchor boxами (в целом, мы могли бы использовать даже большее количество anchor boxов). Эти якоря были рассчитаны с помощью датасета COCO (Common Objects in Context) и кластеризации k-средних (K-means clustering).

У нас есть сетка, где каждая ячейка предсказывает:

  • Для каждого bounding box'а:

    • 4 координаты (tx , ty , tw , th)

    • 1 objectness error (ошибка объектности), которая является показателем уверенности в присутствии того или иного объекта

  • Некоторое количество вероятностей классов

Если же присутствует некоторое смещение от верхнего левого угла на cx , cy то прогнозы будут соответствовать:

b_{x} = \sigma(t_{x}) + c_{x}\\ b_{y} = \sigma(t_{y}) + c_{y}\\ b_{w} = p_{w}e^{t_{w}}\\ b_{h} = p_{h}e^{t_{h}}

где pw (ширина) и ph (высота) соответствуют ширине и высоте bounding box'а. Вместо того, чтобы предугадывать смещение как в прошлой версии YOLOv2, авторы прогнозируют координаты местоположения относительно местоположения ячейки.

Этот вывод является выводом нашей нейронной сети. В общей сложности здесьS x S x [B * (4+1+C)] выводов, где B это количество bounding box'ов, которое может предсказать ячейка на карте объектов, C это количество классов, 4 для bounding box'ов, 1 для objectness prediction (прогнозирование объектности). За один проход мы можем пройти от входного изображения к выходному тензору, который соответствует обнаруженным объектам на картинке. Также стоит отметить, что YOLOv3 прогнозирует bounding box'ы в трех разных масштабах.

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

Простое нахождение порогового значения избавит нас от прогнозов с низким доверительным значением. Для следующего шага важно определить метрику IoU (Intersection over Union / Пересечение над объединением). Эта метрика равняется соотношению площади пересекающихся областей к площади областей объединенных.

После этого все равно могут остаться дубликаты, и чтобы от них избавиться нужно использовать подавление не-максимумов (non-maximum suppression). Подавление не-максимумов заключается в следующем: алгоритм берёт bounding box с наибольшей вероятностью принадлежности к объекту, затем, среди остальных граничащих bounding box'ов с данной области, возьмёт один с наивысшим IoU и подавляет его.

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

Yolov3Yolov3

Мы также рекомендуем прочитать следующие статьи о YOLO:

Реализация в Tensorflow

Первым шагом в реализации YOLO это подготовка ноутбука и импортирование необходимых библиотек. Целиком ноутбук с кодом вы можете на Github или Kaggle:

Следуя этой статье, мы сделаем полную сверточную сеть (fully convolutional network / FCN) без обучения. Для того, чтобы применить эту сеть для определения объектов, нам необходимо скачать готовые веса от предварительно обученной модели. Эти веса были получены от обучения YOLOv3 на датасете COCO (Common Objects in Context). Файл с весами можно скачать по ссылке официального сайта.

# Создаем папку для checkpoint'ов с весами.# !mkdir checkpoints# Скачиваем файл с весами для YOLOv3 с официального сайта.# !wget https://pjreddie.com/media/files/yolov3.weights# Импортируем необходимые библиотеки.import cv2import numpy as np import tensorflow as tf from absl import loggingfrom itertools import repeatfrom PIL import Imagefrom tensorflow.keras import Modelfrom tensorflow.keras.layers import Add, Concatenate, Lambdafrom tensorflow.keras.layers import Conv2D, Input, LeakyReLUfrom tensorflow.keras.layers import MaxPool2D, UpSampling2D, ZeroPadding2Dfrom tensorflow.keras.regularizers import l2from tensorflow.keras.losses import binary_crossentropyfrom tensorflow.keras.losses import sparse_categorical_crossentropyyolo_iou_threshold = 0.6 # Intersection Over Union (iou) threshold.yolo_score_threshold = 0.6 # Score threshold.weightyolov3 = 'yolov3.weights' # Путь до файла с весами.size = 416 # Размер изображения. checkpoints = 'checkpoints/yolov3.tf' # Путь до файла с checkpoint'ом.num_classes = 80 # Количество классов в модели.# Список слоев в YOLOv3 Fully Convolutional Network (FCN).YOLO_V3_LAYERS = [    'yolo_darknet',    'yolo_conv_0',    'yolo_output_0',    'yolo_conv_1',    'yolo_output_1',    'yolo_conv_2',    'yolo_output_2']

По причине того, что порядок слоев в Darknet (open source NN framework) и tf.keras разные, то загрузить веса с помощью чистого функционального API будет проблематично. В этом случае, наилучшим решением будет создание подмоделей в keras. TF Checkpoints рекомендованы для сохранения вложенных подмоделей и они официально поддерживаются Tensorflow.

# Функция для загрузки весов обученной модели.def load_darknet_weights(model, weights_file):    wf = open(weights_file, 'rb')    major, minor, revision, seen, _ = np.fromfile(wf, dtype=np.int32, count=5)    layers = YOLO_V3_LAYERS    for layer_name in layers:        sub_model = model.get_layer(layer_name)        for i, layer in enumerate(sub_model.layers):            if not layer.name.startswith('conv2d'):                continue            batch_norm = None            if i + 1 < len(sub_model.layers) and \                sub_model.layers[i + 1].name.startswith('batch_norm'):                    batch_norm = sub_model.layers[i + 1]            logging.info("{}/{} {}".format(                sub_model.name, layer.name, 'bn' if batch_norm else 'bias'))                        filters = layer.filters            size = layer.kernel_size[0]            in_dim = layer.input_shape[-1]            if batch_norm is None:                conv_bias = np.fromfile(wf, dtype=np.float32, count=filters)            else:                bn_weights = np.fromfile(wf, dtype=np.float32, count=4*filters)                bn_weights = bn_weights.reshape((4, filters))[[1, 0, 2, 3]]            conv_shape = (filters, in_dim, size, size)            conv_weights = np.fromfile(wf, dtype=np.float32, count=np.product(conv_shape))            conv_weights = conv_weights.reshape(conv_shape).transpose([2, 3, 1, 0])            if batch_norm is None:                layer.set_weights([conv_weights, conv_bias])            else:                layer.set_weights([conv_weights])                batch_norm.set_weights(bn_weights)    assert len(wf.read()) == 0, 'failed to read weights'    wf.close()

На этом же этапе, мы должны определить функцию для расчета IoU. Мы используем batch normalization (пакетная нормализация) для нормализации результатов, чтобы ускорить обучение. Так как tf.keras.layers.BatchNormalization работает не очень хорошо для трансферного обучения (transfer learning), то мы используем другой подход.

# Функция для расчета IoU.def interval_overlap(interval_1, interval_2):    x1, x2 = interval_1    x3, x4 = interval_2    if x3 < x1:        return 0 if x4 < x1 else (min(x2,x4) - x1)    else:        return 0 if x2 < x3 else (min(x2,x4) - x3)def intersectionOverUnion(box1, box2):    intersect_w = interval_overlap([box1.xmin, box1.xmax], [box2.xmin, box2.xmax])    intersect_h = interval_overlap([box1.ymin, box1.ymax], [box2.ymin, box2.ymax])    intersect_area = intersect_w * intersect_h    w1, h1 = box1.xmax-box1.xmin, box1.ymax-box1.ymin    w2, h2 = box2.xmax-box2.xmin, box2.ymax-box2.ymin    union_area = w1*h1 + w2*h2 - intersect_area    return float(intersect_area) / union_area class BatchNormalization(tf.keras.layers.BatchNormalization):    def call(self, x, training=False):        if training is None: training = tf.constant(False)        training = tf.logical_and(training, self.trainable)        return super().call(x, training)# Определяем 3 anchor box'а для каждой ячейки.   yolo_anchors = np.array([(10, 13), (16, 30), (33, 23), (30, 61), (62, 45),                        (59, 119), (116, 90), (156, 198), (373, 326)], np.float32) / 416yolo_anchor_masks = np.array([[6, 7, 8], [3, 4, 5], [0, 1, 2]])

В каждом масштабе мы определяем 3 anchor box'а для каждой ячейки. В нашем случае если маска будет:

  • 0, 1, 2 означает, что будут использованы первые три якорные рамки

  • 3, 4 ,5 означает, что будут использованы четвертая, пятая и шестая

  • 6, 7, 8 означает, что будут использованы седьмая, восьмая, девятая

# Функция для отрисовки bounding box'ов.def draw_outputs(img, outputs, class_names, white_list=None):    boxes, score, classes, nums = outputs    boxes, score, classes, nums = boxes[0], score[0], classes[0], nums[0]    wh = np.flip(img.shape[0:2])    for i in range(nums):        if class_names[int(classes[i])] not in white_list:            continue        x1y1 = tuple((np.array(boxes[i][0:2]) * wh).astype(np.int32))        x2y2 = tuple((np.array(boxes[i][2:4]) * wh).astype(np.int32))        img = cv2.rectangle(img, x1y1, x2y2, (255, 0, 0), 2)        img = cv2.putText(img, '{} {:.4f}'.format(            class_names[int(classes[i])], score[i]),            x1y1, cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (0, 0, 255), 2)    return img

Теперь пришло время для реализации YOLOv3. Идея заключается в том, чтобы использовать только сверточные слои. Так как их здесь 53, то самым простым способом является создание функции, в которую мы будем передавать важные параметры, меняющиеся от слоя к слою.

Остаточные блоки (Residual Blocks) в диаграмме архитектуры YOLOv3 применяются для изучения признаков. Остаточный блок содержит в себе несколько сверточных слоев и дополнительные связи для обхода этих слоев.

Создавая нашу модель, мы строим нашу модель с помощью функционального API, который будет легко использовать. С его помощью мы можем без труда определить ветви в нашей архитектуре (ResNet Block) и делить слои внутри архитектуры.

def DarknetConv(x, filters, size, strides=1, batch_norm=True):    if strides == 1:        padding = 'same'    else:        x = ZeroPadding2D(((1, 0), (1, 0)))(x)        padding = 'valid'    x = Conv2D(filters=filters, kernel_size=size,              strides=strides, padding=padding,              use_bias=not batch_norm, kernel_regularizer=l2(0.0005))(x)    if batch_norm:        x = BatchNormalization()(x)        x = LeakyReLU(alpha=0.1)(x)    return xdef DarknetResidual(x, filters):    previous = x    x = DarknetConv(x, filters // 2, 1)    x = DarknetConv(x, filters, 3)    x = Add()([previous , x])    return xdef DarknetBlock(x, filters, blocks):    x = DarknetConv(x, filters, 3, strides=2)    for _ in repeat(None, blocks):        x = DarknetResidual(x, filters)           return xdef Darknet(name=None):    x = inputs = Input([None, None, 3])    x = DarknetConv(x, 32, 3)    x = DarknetBlock(x, 64, 1)    x = DarknetBlock(x, 128, 2)    x = x_36 = DarknetBlock(x, 256, 8)    x = x_61 = DarknetBlock(x, 512, 8)    x = DarknetBlock(x, 1024, 4)    return tf.keras.Model(inputs, (x_36, x_61, x), name=name)  def YoloConv(filters, name=None):    def yolo_conv(x_in):        if isinstance(x_in, tuple):            inputs = Input(x_in[0].shape[1:]), Input(x_in[1].shape[1:])            x, x_skip = inputs            x = DarknetConv(x, filters, 1)            x = UpSampling2D(2)(x)            x = Concatenate()([x, x_skip])        else:            x = inputs = Input(x_in.shape[1:])        x = DarknetConv(x, filters, 1)        x = DarknetConv(x, filters * 2, 3)        x = DarknetConv(x, filters, 1)        x = DarknetConv(x, filters * 2, 3)        x = DarknetConv(x, filters, 1)        return Model(inputs, x, name=name)(x_in)    return yolo_conv  def YoloOutput(filters, anchors, classes, name=None):    def yolo_output(x_in):        x = inputs = Input(x_in.shape[1:])        x = DarknetConv(x, filters * 2, 3)        x = DarknetConv(x, anchors * (classes + 5), 1, batch_norm=False)        x = Lambda(lambda x: tf.reshape(x, (-1, tf.shape(x)[1], tf.shape(x)[2],                                        anchors, classes + 5)))(x)        return tf.keras.Model(inputs, x, name=name)(x_in)    return yolo_outputdef yolo_boxes(pred, anchors, classes):    grid_size = tf.shape(pred)[1]    box_xy, box_wh, score, class_probs = tf.split(pred, (2, 2, 1, classes), axis=-1)    box_xy = tf.sigmoid(box_xy)    score = tf.sigmoid(score)    class_probs = tf.sigmoid(class_probs)    pred_box = tf.concat((box_xy, box_wh), axis=-1)    grid = tf.meshgrid(tf.range(grid_size), tf.range(grid_size))    grid = tf.expand_dims(tf.stack(grid, axis=-1), axis=2)    box_xy = (box_xy + tf.cast(grid, tf.float32)) /  tf.cast(grid_size, tf.float32)    box_wh = tf.exp(box_wh) * anchors    box_x1y1 = box_xy - box_wh / 2    box_x2y2 = box_xy + box_wh / 2    bbox = tf.concat([box_x1y1, box_x2y2], axis=-1)        return bbox, score, class_probs, pred_box

Теперь определим функцию подавления не-максимумов.

def nonMaximumSuppression(outputs, anchors, masks, classes):    boxes, conf, out_type = [], [], []    for output in outputs:        boxes.append(tf.reshape(output[0], (tf.shape(output[0])[0], -1, tf.shape(output[0])[-1])))        conf.append(tf.reshape(output[1], (tf.shape(output[1])[0], -1, tf.shape(output[1])[-1])))        out_type.append(tf.reshape(output[2], (tf.shape(output[2])[0], -1, tf.shape(output[2])[-1])))    bbox = tf.concat(boxes, axis=1)    confidence = tf.concat(conf, axis=1)    class_probs = tf.concat(out_type, axis=1)    scores = confidence * class_probs      boxes, scores, classes, valid_detections = tf.image.combined_non_max_suppression(        boxes=tf.reshape(bbox, (tf.shape(bbox)[0], -1, 1, 4)),        scores=tf.reshape(            scores, (tf.shape(scores)[0], -1, tf.shape(scores)[-1])),        max_output_size_per_class=100,        max_total_size=100,        iou_threshold=yolo_iou_threshold,        score_threshold=yolo_score_threshold)      return boxes, scores, classes, valid_detections

Основная функция:

def YoloV3(size=None, channels=3, anchors=yolo_anchors,            masks=yolo_anchor_masks, classes=80, training=False):    x = inputs = Input([size, size, channels])    x_36, x_61, x = Darknet(name='yolo_darknet')(x)    x = YoloConv(512, name='yolo_conv_0')(x)    output_0 = YoloOutput(512, len(masks[0]), classes, name='yolo_output_0')(x)    x = YoloConv(256, name='yolo_conv_1')((x, x_61))    output_1 = YoloOutput(256, len(masks[1]), classes, name='yolo_output_1')(x)    x = YoloConv(128, name='yolo_conv_2')((x, x_36))    output_2 = YoloOutput(128, len(masks[2]), classes, name='yolo_output_2')(x)    if training:        return Model(inputs, (output_0, output_1, output_2), name='yolov3')    boxes_0 = Lambda(lambda x: yolo_boxes(x, anchors[masks[0]], classes),                  name='yolo_boxes_0')(output_0)    boxes_1 = Lambda(lambda x: yolo_boxes(x, anchors[masks[1]], classes),                  name='yolo_boxes_1')(output_1)    boxes_2 = Lambda(lambda x: yolo_boxes(x, anchors[masks[2]], classes),                  name='yolo_boxes_2')(output_2)    outputs = Lambda(lambda x: nonMaximumSuppression(x, anchors, masks, classes),                  name='nonMaximumSuppression')((boxes_0[:3], boxes_1[:3], boxes_2[:3]))    return Model(inputs, outputs, name='yolov3')

Функция потерь:

def YoloLoss(anchors, classes=80, ignore_thresh=0.5):    def yolo_loss(y_true, y_pred):        pred_box, pred_obj, pred_class, pred_xywh = yolo_boxes(            y_pred, anchors, classes)        pred_xy = pred_xywh[..., 0:2]        pred_wh = pred_xywh[..., 2:4]        true_box, true_obj, true_class_idx = tf.split(            y_true, (4, 1, 1), axis=-1)        true_xy = (true_box[..., 0:2] + true_box[..., 2:4]) / 2        true_wh = true_box[..., 2:4] - true_box[..., 0:2]        box_loss_scale = 2 - true_wh[..., 0] * true_wh[..., 1]        grid_size = tf.shape(y_true)[1]        grid = tf.meshgrid(tf.range(grid_size), tf.range(grid_size))        grid = tf.expand_dims(tf.stack(grid, axis=-1), axis=2)        true_xy = true_xy * tf.cast(grid_size, tf.float32) - \            tf.cast(grid, tf.float32)        true_wh = tf.math.log(true_wh / anchors)        true_wh = tf.where(tf.math.is_inf(true_wh),                      tf.zeros_like(true_wh), true_wh)        obj_mask = tf.squeeze(true_obj, -1)        true_box_flat = tf.boolean_mask(true_box, tf.cast(obj_mask, tf.bool))        best_iou = tf.reduce_max(intersectionOverUnion(            pred_box, true_box_flat), axis=-1)        ignore_mask = tf.cast(best_iou < ignore_thresh, tf.float32)        xy_loss = obj_mask * box_loss_scale * \            tf.reduce_sum(tf.square(true_xy - pred_xy), axis=-1)        wh_loss = obj_mask * box_loss_scale * \            tf.reduce_sum(tf.square(true_wh - pred_wh), axis=-1)        obj_loss = binary_crossentropy(true_obj, pred_obj)        obj_loss = obj_mask * obj_loss + \            (1 - obj_mask) * ignore_mask * obj_loss        class_loss = obj_mask * sparse_categorical_crossentropy(            true_class_idx, pred_class)        xy_loss = tf.reduce_sum(xy_loss, axis=(1, 2, 3))        wh_loss = tf.reduce_sum(wh_loss, axis=(1, 2, 3))        obj_loss = tf.reduce_sum(obj_loss, axis=(1, 2, 3))        class_loss = tf.reduce_sum(class_loss, axis=(1, 2, 3))        return xy_loss + wh_loss + obj_loss + class_loss    return yolo_loss

Функция "преобразовать цели" возвращает кортеж из форм:

(    [N, 13, 13, 3, 6],    [N, 26, 26, 3, 6],    [N, 52, 52, 3, 6])

Где N число меток в пакете, а число 6 означает [x, y, w, h, obj, class] bounding box'а.

@tf.functiondef transform_targets_for_output(y_true, grid_size, anchor_idxs, classes):    N = tf.shape(y_true)[0]    y_true_out = tf.zeros(      (N, grid_size, grid_size, tf.shape(anchor_idxs)[0], 6))    anchor_idxs = tf.cast(anchor_idxs, tf.int32)    indexes = tf.TensorArray(tf.int32, 1, dynamic_size=True)    updates = tf.TensorArray(tf.float32, 1, dynamic_size=True)    idx = 0    for i in tf.range(N):        for j in tf.range(tf.shape(y_true)[1]):            if tf.equal(y_true[i][j][2], 0):                continue            anchor_eq = tf.equal(                anchor_idxs, tf.cast(y_true[i][j][5], tf.int32))            if tf.reduce_any(anchor_eq):                box = y_true[i][j][0:4]                box_xy = (y_true[i][j][0:2] + y_true[i][j][2:4]) / 2                anchor_idx = tf.cast(tf.where(anchor_eq), tf.int32)                grid_xy = tf.cast(box_xy // (1/grid_size), tf.int32)                indexes = indexes.write(                    idx, [i, grid_xy[1], grid_xy[0], anchor_idx[0][0]])                updates = updates.write(                    idx, [box[0], box[1], box[2], box[3], 1, y_true[i][j][4]])                idx += 1    return tf.tensor_scatter_nd_update(        y_true_out, indexes.stack(), updates.stack())def transform_targets(y_train, anchors, anchor_masks, classes):    outputs = []    grid_size = 13    anchors = tf.cast(anchors, tf.float32)    anchor_area = anchors[..., 0] * anchors[..., 1]    box_wh = y_train[..., 2:4] - y_train[..., 0:2]    box_wh = tf.tile(tf.expand_dims(box_wh, -2),                    (1, 1, tf.shape(anchors)[0], 1))    box_area = box_wh[..., 0] * box_wh[..., 1]    intersection = tf.minimum(box_wh[..., 0], anchors[..., 0]) * \    tf.minimum(box_wh[..., 1], anchors[..., 1])    iou = intersection / (box_area + anchor_area - intersection)    anchor_idx = tf.cast(tf.argmax(iou, axis=-1), tf.float32)    anchor_idx = tf.expand_dims(anchor_idx, axis=-1)    y_train = tf.concat([y_train, anchor_idx], axis=-1)    for anchor_idxs in anchor_masks:        outputs.append(transform_targets_for_output(            y_train, grid_size, anchor_idxs, classes))        grid_size *= 2    return tuple(outputs) # [x, y, w, h, obj, class]def preprocess_image(x_train, size):    return (tf.image.resize(x_train, (size, size))) / 255

Теперь мы можем создать нашу модель, загрузить веса и названия классов. В COCO датасете их 80.

yolo = YoloV3(classes=num_classes)load_darknet_weights(yolo, weightyolov3)yolo.save_weights(checkpoints)class_names =  ["person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck",    "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench",    "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe",    "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",    "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard",    "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl",    "banana","apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut",    "cake","chair", "sofa", "pottedplant", "bed", "diningtable", "toilet", "tvmonitor", "laptop",     "mouse","remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",    "refrigerator","book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"]def detect_objects(img_path, white_list=None):    image = img_path     # Путь к изображению.    img = tf.image.decode_image(open(image, 'rb').read(), channels=3)    img = tf.expand_dims(img, 0)    img = preprocess_image(img, size)    boxes, scores, classes, nums = yolo(img)    img = cv2.imread(image)    img = draw_outputs(img, (boxes, scores, classes, nums), class_names, white_list)    cv2.imwrite('detected_{:}'.format(img_path), img)    detected = Image.open('detected_{:}'.format(img_path))    detected.show()    detect_objects('test.jpg', ['bear'])

Итог

В этой статье мы поговорили об отличительных особенностях YOLOv3 и её преимуществах перед другими моделями. Мы рассмотрели способ реализации с использованием TensorFlow 2.0 (TF должен быть не менее версией 2.0).

Ссылки

Подробнее..

3D teeth instance segmentation. В темноте, но не один

23.05.2021 14:09:40 | Автор: admin

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

Дисклеймер

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

Об авторе

Добрый - всем, зовут Андрей(27). Постараюсь коротко. Почему программирование? По образованию - бакалавр электромеханик, профессию знаю. Отработал 2 года на должности инженера-энергетика в буровой компании вполне успешно, вместо повышения написал заявление - сгорел, да не по мне оказалось это всё. Нравится создавать, находить решения сложных задач, с ПК в обнимку с сознательных лет. Выбор очевиден. Вначале (полгода назад), всерьёз думал записаться на курсы от Я или подобные. Начитался отзывов, поговорил с участниками и понял что с получением информацией проблем нет. Так нашел сайт, там получил базу по Python и с ним уже начал свой путь (сейчас там постепенно изучаю всё, что связано с ML). Сразу заинтересовало машинное обучение, CV в частности. Придумал себе задачу и вот здесь (по мне, так отличный способ учиться).

1. Введение

В результате нескольких неудачных попыток, пришел к решению использовать 2 легковесные модели для получения желаемого результата. 1-ая сегментирует все зубы как [1, 0] категорию, а вторая делит их на категории[0, 8]. Но начнем по порядку.

2. Поиск и подготовка данных

Потратив не один вечер на поиск данных для работы, пришел в выводу что в свободном доступе челюсть в хорошем качестве и формате (*.stl, *.nrrd и т.д.) не получится. Лучшее, что мне попалось - это тестовый образец головы пациента после хирургической операции на челюсти в программе 3D Slicer.

Очевидно, мне не нужна голова целиком, поэтому обрезал исходник в той же программе до размера 163*112*120рх (в данном посте {x*y*z = ш-г-в} и 1рх - 0,5мм), оставив только зубы и сопутствующие челюстно-лицевые части.

Уже больше похоже на то что нужно, дальше - интереснее. Теперь нужно создать маски всех необходимых нам объектов. Для тех, кто уже работал с этим - "autothreshold" не то чтобы совсем не работает, просто лишнего много, думаю, исправление заняло бы столько же времени, сколько и разметка вручную(через маски).

- Пиксели(срезы слева)? - Вспоминаем размер изображения- Пиксели(срезы слева)? - Вспоминаем размер изображения

Размечал часов 12~14. И да, тот факт что я не сразу разметил каждый зуб как категорию стоил мне еще порядка 4 часов. В итоге у нас есть данные, с которыми у же можно работать.

Конечный вариант маски. Smooth 0.5. (сглаживание в обучении не использовалось)Конечный вариант маски. Smooth 0.5. (сглаживание в обучении не использовалось)

Должен добавить, даже на мой (без опыта) взгляд, этих данных очень мало для обучения и последующей полноценной работы нейронной сети. На данном этапе, единственное что пришло в голову, повернуть имеющиеся данные N-раз и соединить, random-crop использовать не стал.

Код подготовки данных
import nrrdimport torchimport torchvision.transforms as tfclass DataBuilder:    def __init__(self,                 data_path,                 list_of_categories,                 num_of_chunks: int = 0,                 augmentation_coeff: int = 0,                 num_of_classes: int = 0,                 normalise: bool = False,                 fit: bool = True,                 data_format: int = 0,                 save_data: bool = False                 ):        self.data_path = data_path        self.number_of_chunks = num_of_chunks        self.augmentation_coeff = augmentation_coeff        self.list_of_cats = list_of_categories        self.num_of_cls = num_of_classes        self.normalise = normalise        self.fit = fit        self.data_format = data_format        self.save_data = save_data    def forward(self):        data = self.get_data()        data = self.fit_data(data) if self.fit else data        data = self.pre_normalize(data) if self.normalise else data        data = self.data_augmentation(data, self.augmentation_coeff) if self.augmentation_coeff != 0 else data        data = self.new_chunks(data, self.number_of_chunks) if self.number_of_chunks != 0 else data        data = self.category_splitter(data, self.num_of_cls, self.list_of_cats) if self.num_of_cls != 0 else data        torch.save(data, self.data_path[-14:]+'.pt') if self.save_data else None        return torch.unsqueeze(data, 1)    def get_data(self):        if self.data_format == 0:            return torch.from_numpy(nrrd.read(self.data_path)[0])        elif self.data_format == 1:            return torch.load(self.data_path).cpu()        elif self.data_format == 2:            return torch.unsqueeze(self.data_path, 0).cpu()        else:            print('Available types are: "nrrd", "tensor" or "self.tensor(w/o load)"')    @staticmethod    def fit_data(some_data):        data = torch.movedim(some_data, (1, 0), (0, -1))        data_add_x = torch.nn.ZeroPad2d((5, 0, 0, 0))        data = data_add_x(data)        data = torch.movedim(data, -1, 0)        data_add_z = torch.nn.ZeroPad2d((0, 0, 8, 0))        return data_add_z(data)    @staticmethod    def pre_normalize(some_data):        min_d, max_d = torch.min(some_data), torch.max(some_data)        return (some_data - min_d) / (max_d - min_d)    @staticmethod    def data_augmentation(some_data, aug_n):        torch.manual_seed(17)        tr_data = []        for e in range(aug_n):            transform = tf.RandomRotation(degrees=(20*e, 20*e))            for image in some_data:                image = torch.unsqueeze(image, 0)                image = transform(image)                tr_data.append(image)        return tr_data    def new_chunks(self, some_data, n_ch):        data = torch.stack(some_data, 0) if self.augmentation_coeff != 0 else some_data        data = torch.squeeze(data, 1)        chunks = torch.chunk(data, n_ch, 0)        return torch.stack(chunks)    @staticmethod    def category_splitter(some_data, alpha, list_of_categories):        data, _ = torch.squeeze(some_data, 1).to(torch.int64), alpha        for i in list_of_categories:            data = torch.where(data < i, _, data)            _ += 1        return data - alpha

Имейте ввиду что это финальная версия кода подготовки данных для 3D U-net. Форвард:

  • Загружаем дату (в зависимости от типа).

  • Добавляем 0 по краям чтобы подогнать размер до 168*120*120 (вместо исходных 163*112*120). *пригодится дальше.

  • Нормализуем входящие данные в 0...1 (исходные ~-2000...16000).

  • Поворачиваем N-раз и соединяем.

  • Полученные данные режем на равные части чтобы забить память видеокарты по максимуму (в моем случае это 1, 1, 72, 120, 120).

  • Эта часть распределяет по категориям 28 имеющихся зубов и фон для облегчения обучения моделей (см. Введение):

    • одну категорию для 1-ой;

    • на 9 категорий (8+фон) для 2-ой.

Dataloader стандартный
import torch.utils.data as tudclass ToothDataset(tud.Dataset):    def __init__(self, images, masks):        self.images = images        self.masks = masks    def __len__(self): return len(self.images)    def __getitem__(self, index):        if self.masks is not None:            return self.images[index, :, :, :, :],\                    self.masks[index, :, :, :, :]        else:            return self.images[index, :, :, :, :]def get_loaders(images, masks,                batch_size: int = 1,                num_workers: int = 1,                pin_memory: bool = True):    train_ds = ToothDataset(images=images,                            masks=masks)    data_loader = tud.DataLoader(train_ds,                                 batch_size=batch_size,                                 shuffle=False,                                 num_workers=num_workers,                                 pin_memory=pin_memory)    return data_loader

На выходе имеем следующее:

Semantic

Instance

Predictions

Data

(27*, 1, 56*, 120,120)[0...1]

(27*, 1, 56*, 120,120) [0, 1]

(1, 1, 168, 120, 120)[0...1]

Masks

(27*, 1, 56*, 120,120)[0, 1]

(27*, 1, 56*, 120,120)[0, 8]

-

*эти размеры менялись, в зависимости от эксперимента, подробности - дальше.

3. Выбор и настройка моделей обучения

Цель работы - обучение. Поэтому взял наиболее простую и понятную для себя модель нейросети архитектуры U-Net. Код не выкладываю, можно посмотреть тут.

2D U-Net2D U-Net

Подробно рассказывать не буду, информации в достатке в сети. Метод оптимизации - Adam, функция расчета потерь Dice-loss(implement), спусков/подъемов 4, фильтры [64, 128, 256, 512] (знаю, много, об этом - позже). Обучал в среднем 60-80 epochs на эксперимент. Transfer learning не использовал.

model.summary()
model = UNet(dim=2, in_channels=1, out_channels=1, n_blocks=4, start_filters=64).to(device)print(summary(model, (1, 168, 120)))"""----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv2d-1         [-1, 64, 168, 120]             640              ReLU-2         [-1, 64, 168, 120]               0       BatchNorm2d-3         [-1, 64, 168, 120]             128            Conv2d-4         [-1, 64, 168, 120]          36,928              ReLU-5         [-1, 64, 168, 120]               0       BatchNorm2d-6         [-1, 64, 168, 120]             128         MaxPool2d-7           [-1, 64, 84, 60]               0         DownBlock-8  [[-1, 64, 84, 60], [-1, 64, 168, 120]]  0            Conv2d-9          [-1, 128, 84, 60]          73,856             ReLU-10          [-1, 128, 84, 60]               0      BatchNorm2d-11          [-1, 128, 84, 60]             256           Conv2d-12          [-1, 128, 84, 60]         147,584             ReLU-13          [-1, 128, 84, 60]               0      BatchNorm2d-14          [-1, 128, 84, 60]             256        MaxPool2d-15          [-1, 128, 42, 30]               0        DownBlock-16  [[-1, 128, 42, 30], [-1, 128, 84, 60]]  0           Conv2d-17          [-1, 256, 42, 30]         295,168             ReLU-18          [-1, 256, 42, 30]               0      BatchNorm2d-19          [-1, 256, 42, 30]             512           Conv2d-20          [-1, 256, 42, 30]         590,080             ReLU-21          [-1, 256, 42, 30]               0      BatchNorm2d-22          [-1, 256, 42, 30]             512        MaxPool2d-23          [-1, 256, 21, 15]               0        DownBlock-24  [[-1, 256, 21, 15], [-1, 256, 42, 30]]  0           Conv2d-25          [-1, 512, 21, 15]       1,180,160             ReLU-26          [-1, 512, 21, 15]               0      BatchNorm2d-27          [-1, 512, 21, 15]           1,024           Conv2d-28          [-1, 512, 21, 15]       2,359,808             ReLU-29          [-1, 512, 21, 15]               0      BatchNorm2d-30          [-1, 512, 21, 15]           1,024        DownBlock-31  [[-1, 512, 21, 15], [-1, 512, 21, 15]]  0  ConvTranspose2d-32          [-1, 256, 42, 30]         524,544             ReLU-33          [-1, 256, 42, 30]               0      BatchNorm2d-34          [-1, 256, 42, 30]             512      Concatenate-35          [-1, 512, 42, 30]               0           Conv2d-36          [-1, 256, 42, 30]       1,179,904             ReLU-37          [-1, 256, 42, 30]               0      BatchNorm2d-38          [-1, 256, 42, 30]             512           Conv2d-39          [-1, 256, 42, 30]         590,080             ReLU-40          [-1, 256, 42, 30]               0      BatchNorm2d-41          [-1, 256, 42, 30]             512          UpBlock-42          [-1, 256, 42, 30]               0  ConvTranspose2d-43          [-1, 128, 84, 60]         131,200             ReLU-44          [-1, 128, 84, 60]               0      BatchNorm2d-45          [-1, 128, 84, 60]             256      Concatenate-46          [-1, 256, 84, 60]               0           Conv2d-47          [-1, 128, 84, 60]         295,040             ReLU-48          [-1, 128, 84, 60]               0      BatchNorm2d-49          [-1, 128, 84, 60]             256           Conv2d-50          [-1, 128, 84, 60]         147,584             ReLU-51          [-1, 128, 84, 60]               0      BatchNorm2d-52          [-1, 128, 84, 60]             256          UpBlock-53          [-1, 128, 84, 60]               0  ConvTranspose2d-54         [-1, 64, 168, 120]          32,832             ReLU-55         [-1, 64, 168, 120]               0      BatchNorm2d-56         [-1, 64, 168, 120]             128      Concatenate-57        [-1, 128, 168, 120]               0           Conv2d-58         [-1, 64, 168, 120]          73,792             ReLU-59         [-1, 64, 168, 120]               0      BatchNorm2d-60         [-1, 64, 168, 120]             128           Conv2d-61         [-1, 64, 168, 120]          36,928             ReLU-62         [-1, 64, 168, 120]               0      BatchNorm2d-63         [-1, 64, 168, 120]             128          UpBlock-64         [-1, 64, 168, 120]               0           Conv2d-65          [-1, 1, 168, 120]              65================================================================Total params: 7,702,721Trainable params: 7,702,721Non-trainable params: 0----------------------------------------------------------------Input size (MB): 0.08Forward/backward pass size (MB): 7434.08Params size (MB): 29.38Estimated Total Size (MB): 7463.54"""
Эксп.12D U-Net, подача изображений покадрово, плоскость [x, z]Эксп.12D U-Net, подача изображений покадрово, плоскость [x, z]

Определенно, это - зубы. Только кроме зубов есть много всего, нам ненужного. Подробнее о трансформации numpy - *.stl в Главе 6. Посмотрим ещё раз на фактический размер и качество изображений, которые попадают на вход нейросети:

Слева на право:1. Не видно[x, y]. 2. Немного лучше[x, z]. 3.Ещё лучше[y, z]Слева на право:1. Не видно[x, y]. 2. Немного лучше[x, z]. 3.Ещё лучше[y, z]

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

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

Эксп.2Каскад 2-ух 2D U-Net, подача изображений покадрово, плоскость [y, z]Эксп.2Каскад 2-ух 2D U-Net, подача изображений покадрово, плоскость [y, z]

Прогресс виден, однако вместе с помехами пропадают и части зубов, дальнейшее обучение тому подтверждение:

Эксп.3Каскад 2-ух 2D U-Net, подача изображений покадрово плоскость [y, z]с увеличением времени обучения на 50%Эксп.3Каскад 2-ух 2D U-Net, подача изображений покадрово плоскость [y, z]с увеличением времени обучения на 50%

Ввиду последних событий было принято решение о переходе на 3D архитектуру нейронной сети. Переподготовил входные данные, а именно разделил на части размером (24*, 120, 120). Почему так? - изначально большая модель обучения (~22млн. параметров). Моя видеокарта(1063gtx) не могла физически вместить больше.

24*

Это размер глубины. Был подобран так чтобы:

  • количество данных(1512, 120, 120) делится нацело на это число - получается 63;

  • в свою очередь получившийся batch size (24, 120, 120) - максимум, вмещающийся в память видеокарты с текущими параметрами сети;

  • само это число (24) делилось на количество спусков/подъемов так же нацело (имеется в виду соответствие выражению 24/2/2/2=3 и 3*2*2*2=24, где количество делений/умножений на 2 соответствует количеству спусков/подъемов минус 1);

  • то же самое не только для глубины данных, но и длинны и ширины. Подробнее в .summary()

model.summary()
model = UNet(dim=3, in_channels=1, out_channels=1, n_blocks=4, start_filters=64).to(device)print(summary(model, (1, 24, 120, 120)))"""  ----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv3d-1     [-1, 64, 24, 120, 120]             1,792              ReLU-2     [-1, 64, 24, 120, 120]                 0       BatchNorm3d-3     [-1, 64, 24, 120, 120]               128            Conv3d-4     [-1, 64, 24, 120, 120]           110,656              ReLU-5     [-1, 64, 24, 120, 120]                 0       BatchNorm3d-6     [-1, 64, 24, 120, 120]               128         MaxPool3d-7        [-1, 64, 12, 60, 60]                0         DownBlock-8  [[-1, 64, 12, 60, 60], [-1, 64, 24, 120, 120]]               0            Conv3d-9       [-1, 128, 12, 60, 60]          221,312             ReLU-10       [-1, 128, 12, 60, 60]                0      BatchNorm3d-11       [-1, 128, 12, 60, 60]              256           Conv3d-12       [-1, 128, 12, 60, 60]          442,496             ReLU-13       [-1, 128, 12, 60, 60]                0      BatchNorm3d-14       [-1, 128, 12, 60, 60]              256        MaxPool3d-15       [-1, 128, 6, 30, 30]                 0        DownBlock-16  [[-1, 128, 6, 30, 30], [-1, 128, 12, 60, 60]]               0           Conv3d-17       [-1, 256, 6, 30, 30]           884,992             ReLU-18       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-19       [-1, 256, 6, 30, 30]               512           Conv3d-20       [-1, 256, 6, 30, 30]         1,769,728             ReLU-21       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-22       [-1, 256, 6, 30, 30]               512        MaxPool3d-23       [-1, 256, 3, 15, 15]                 0        DownBlock-24  [[-1, 256, 3, 15, 15], [-1, 256, 6, 30, 30]]               0           Conv3d-25       [-1, 512, 3, 15, 15]         3,539,456             ReLU-26       [-1, 512, 3, 15, 15]                 0      BatchNorm3d-27       [-1, 512, 3, 15, 15]             1,024           Conv3d-28       [-1, 512, 3, 15, 15]         7,078,400             ReLU-29       [-1, 512, 3, 15, 15]                 0      BatchNorm3d-30       [-1, 512, 3, 15, 15]             1,024        DownBlock-31  [[-1, 512, 3, 15, 15], [-1, 512, 3, 15, 15]]               0  ConvTranspose3d-32       [-1, 256, 6, 30, 30]         1,048,832             ReLU-33       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-34       [-1, 256, 6, 30, 30]               512      Concatenate-35       [-1, 512, 6, 30, 30]                 0           Conv3d-36       [-1, 256, 6, 30, 30]         3,539,200             ReLU-37       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-38       [-1, 256, 6, 30, 30]               512           Conv3d-39       [-1, 256, 6, 30, 30]         1,769,728             ReLU-40       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-41       [-1, 256, 6, 30, 30]               512          UpBlock-42       [-1, 256, 6, 30, 30]                 0  ConvTranspose3d-43       [-1, 128, 12, 60, 60]          262,272             ReLU-44       [-1, 128, 12, 60, 60]                0      BatchNorm3d-45       [-1, 128, 12, 60, 60]              256      Concatenate-46       [-1, 256, 12, 60, 60]                0           Conv3d-47       [-1, 128, 12, 60, 60]          884,864             ReLU-48       [-1, 128, 12, 60, 60]                0      BatchNorm3d-49       [-1, 128, 12, 60, 60]              256           Conv3d-50       [-1, 128, 12, 60, 60]          442,496             ReLU-51       [-1, 128, 12, 60, 60]                0      BatchNorm3d-52       [-1, 128, 12, 60, 60]              256          UpBlock-53       [-1, 128, 12, 60, 60]                0  ConvTranspose3d-54       [-1, 64, 24, 120, 120]          65,600             ReLU-55       [-1, 64, 24, 120, 120]               0      BatchNorm3d-56       [-1, 64, 24, 120, 120]             128      Concatenate-57      [-1, 128, 24, 120, 120]               0           Conv3d-58       [-1, 64, 24, 120, 120]         221,248             ReLU-59       [-1, 64, 24, 120, 120]               0      BatchNorm3d-60       [-1, 64, 24, 120, 120]             128           Conv3d-61       [-1, 64, 24, 120, 120]         110,656             ReLU-62       [-1, 64, 24, 120, 120]               0      BatchNorm3d-63       [-1, 64, 24, 120, 120]             128          UpBlock-64       [-1, 64, 24, 120, 120]               0           Conv3d-65        [-1, 1, 24, 120, 120]              65================================================================Total params: 22,400,321Trainable params: 22,400,321Non-trainable params: 0----------------------------------------------------------------Input size (MB): 0.61Forward/backward pass size (MB): 15974.12Params size (MB): 85.45Estimated Total Size (MB): 16060.18----------------------------------------------------------------"""
Эксп.43D U-Net, подача объемом, плоскость [y, z],время*0,38Эксп.43D U-Net, подача объемом, плоскость [y, z],время*0,38

С учетом сокращенного на ~60% времени обучения(25 epochs) результат меня устроил, продолжаем.

Эксп.53D U-Net, подача объемом, плоскость [y, z], 65 epochs ~ 1,5 часа Эксп.53D U-Net, подача объемом, плоскость [y, z], 65 epochs ~ 1,5 часа

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

Эксп.63D U-Net, подача объемом, плоскость [x, z], 105 epochs ~ 2,1 часа Эксп.63D U-Net, подача объемом, плоскость [x, z], 105 epochs ~ 2,1 часа

"Научный" перебор параметров в течении недели принес результат. Уменьшил количество параметров сети до ~400к (от первоначальных ~22м) путем уменьшения фильтра [18, 32, 64, 128] и спуска/подъема до 3. Изменил метод оптимизации на RSMProp. Уменьшение количества параметров нейросети позволило увеличить объем входных данных в три раза (1, 1, 72*, 120, 120). Посмотрим результат?

model.summary()
model = UNet(dim=3, in_channels=1, out_channels=1, n_blocks=3, start_filters=18).to(device)print(summary(model, (1, 1, 72, 120, 120)))"""----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv3d-1     [-1, 18, 72, 120, 120]             504              ReLU-2     [-1, 18, 72, 120, 120]               0       BatchNorm3d-3     [-1, 18, 72, 120, 120]              36            Conv3d-4     [-1, 18, 72, 120, 120]           8,766              ReLU-5     [-1, 18, 72, 120, 120]               0       BatchNorm3d-6     [-1, 18, 72, 120, 120]              36         MaxPool3d-7       [-1, 18, 36, 60, 60]               0         DownBlock-8  [[-1, 18, 36, 60, 60], [-1, 18, 24, 120, 120]]               0            Conv3d-9       [-1, 36, 36, 60, 60]          17,532             ReLU-10       [-1, 36, 36, 60, 60]               0      BatchNorm3d-11       [-1, 36, 36, 60, 60]              72           Conv3d-12       [-1, 36, 36, 60, 60]          35,028             ReLU-13       [-1, 36, 36, 60, 60]               0      BatchNorm3d-14       [-1, 36, 36, 60, 60]              72        MaxPool3d-15        [-1, 36, 18, 30, 30]              0        DownBlock-16  [[-1, 36, 18, 30, 30], [-1, 36, 36, 60, 60]]               0           Conv3d-17        [-1, 72, 18, 30, 30]         70,056             ReLU-18        [-1, 72, 18, 30, 30]              0      BatchNorm3d-19        [-1, 72, 18, 30, 30]            144           Conv3d-20        [-1, 72, 18, 30, 30]        140,040             ReLU-21        [-1, 72, 18, 30, 30]              0      BatchNorm3d-22        [-1, 72, 18, 30, 30]            144        DownBlock-23  [[-1, 72, 18, 30, 30], [-1, 72, 18, 30, 30]]               0  ConvTranspose3d-24       [-1, 36, 36, 60, 60]          20,772             ReLU-25       [-1, 36, 36, 60, 60]               0      BatchNorm3d-26       [-1, 36, 36, 60, 60]              72      Concatenate-27       [-1, 72, 36, 60, 60]               0           Conv3d-28       [-1, 36, 36, 60, 60]          70,020             ReLU-29       [-1, 36, 36, 60, 60]               0      BatchNorm3d-30       [-1, 36, 36, 60, 60]              72           Conv3d-31       [-1, 36, 36, 60, 60]          35,028             ReLU-32       [-1, 36, 36, 60, 60]               0      BatchNorm3d-33       [-1, 36, 36, 60, 60]              72          UpBlock-34       [-1, 36, 36, 60, 60]               0  ConvTranspose3d-35     [-1, 18, 72, 120, 120]           5,202             ReLU-36     [-1, 18, 72, 120, 120]               0      BatchNorm3d-37     [-1, 18, 72, 120, 120]              36      Concatenate-38     [-1, 36, 72, 120, 120]               0           Conv3d-39     [-1, 18, 72, 120, 120]          17,514             ReLU-40     [-1, 18, 72, 120, 120]               0      BatchNorm3d-41     [-1, 18, 72, 120, 120]              36           Conv3d-42     [-1, 18, 72, 120, 120]           8,766             ReLU-43     [-1, 18, 72, 120, 120]               0      BatchNorm3d-44     [-1, 18, 72, 120, 120]              36          UpBlock-45     [-1, 18, 72, 120, 120]               0           Conv3d-46      [-1, 1, 72, 120, 120]              19================================================================Total params: 430,075Trainable params: 430,075Non-trainable params: 0----------------------------------------------------------------Input size (MB): 1.32Forward/backward pass size (MB): 5744.38Params size (MB): 1.64Estimated Total Size (MB): 5747.34----------------------------------------------------------------"""
72*

Некоторые из вас подумают, исходные данные (168, 120, 120), а часть (72, 120, 120). Назревает вопрос, как делить. Всё просто, во 2 главе мы увеличивали размер наших данных и затем делили их на части, соответствующие объему памяти видеокарты. Я увеличил данные в 9 раз (1512, 120, 120) т.е. повернул на 9 различных углов относительно одной оси, а затем разделил на 21(batch size) часть по (72, 120, 120). Так же 72 соответствует всем условиям, описанным в 24*(выше).

Эксп.73D U-Net, подача объемом, плоскость [x, z],Маска (слева) и готовая сегментация (справа),оптимизированные параметры сети,время обучения(65 epochs) ~ 14мин.Эксп.73D U-Net, подача объемом, плоскость [x, z],Маска (слева) и готовая сегментация (справа),оптимизированные параметры сети,время обучения(65 epochs) ~ 14мин.

Результат вполне удовлетворительный, есть недочеты (вроде "похудевших" зубов). Возможно, исправим их в другом посте. Для этапа semantic segmentation я думаю мы сделали достаточно, теперь необходимо задать категории.

О размере подаваемых данных

Первоначальная идея при переходе на 3D архитектуру была в том чтобы делить данные не слайсами (как в данном посте) (1512, 120, 120) --> 21*(1, 72, 120, 120), а кубиками ~х*(30, 30, 30) или около того (результат этой попытки не был сохранен оп понятным причинам). Опытным путем понял 2 вещи: чем большими порциями ты подаешь 3-х мерные объекты, тем лучше результат(для моего конкретного случая); и нужно больше изучать теорию того, с чем работаешь.

О времени обучения и размере модели

Параметры сети подобраны так, что обучение 1 epochs на моей "старушке" занимает ~13сек, а размер конечной модели не превышает 2мб (прошлая>80мб). Время рабочего цикла примерно равно 1 epochs. Однако стоит понимать, это обучение и работа на данных достаточно маленького размера.

Для разделения на категории пришлось немного повозиться с функцией расчета ошибки и визуализацией данных. Первоначально поставил себе задачу разделить на 8 категорий + фон. О loss function и визуализации поговорим подробнее.

Код training loop
import torchfrom tqdm import tqdmfrom _loss_f import LossFunctionclass TrainFunction:    def __init__(self,                 data_loader,                 device_for_training,                 model_name,                 model_name_pretrained,                 model,                 optimizer,                 scale,                 learning_rate: int = 1e-2,                 num_epochs: int = 1,                 transfer_learning: bool = False,                 binary_loss_f: bool = True                 ):        self.data_loader = data_loader        self.device = device_for_training        self.model_name_pretrained = model_name_pretrained        self.semantic_binary = binary_loss_f        self.num_epochs = num_epochs        self.model_name = model_name        self.transfer = transfer_learning        self.optimizer = optimizer        self.learning_rate = learning_rate        self.model = model        self.scale = scale    def forward(self):        print('Running on the:', torch.cuda.get_device_name(self.device))        self.model.load_state_dict(torch.load(self.model_name_pretrained)) if self.transfer else None        optimizer = self.optimizer(self.model.parameters(), lr=self.learning_rate)        for epoch in range(self.num_epochs):            self.train_loop(self.data_loader, self.model, optimizer, self.scale, epoch)            torch.save(self.model.state_dict(), 'models/' + self.model_name+str(epoch+1)                       + '_epoch.pth') if (epoch + 1) % 10 == 0 else None    def train_loop(self, loader, model, optimizer, scales, i):        loop, epoch_loss = tqdm(loader), 0        loop.set_description('Epoch %i' % (self.num_epochs - i))        for batch_idx, (data, targets) in enumerate(loop):            data, targets = data.to(device=self.device, dtype=torch.float), \                            targets.to(device=self.device, dtype=torch.long)            optimizer.zero_grad()            *тут секрет*            with torch.cuda.amp.autocast():                predictions = model(data)                loss = LossFunction(predictions, targets,                                    device_for_training=self.device,                                    semantic_binary=self.semantic_binary                                    ).forward()            scales.scale(loss).backward()            scales.step(optimizer)            scales.update()            epoch_loss += (1 - loss.item())*100            loop.set_postfix(loss=loss.item())        print('Epoch-acc', round(epoch_loss / (batch_idx+1), 2))

4. Функция расчета ошибки

Мне в целом понравилось как проявляет себя Dice-loss в сегментации, только 'проблема' в том что он работает с форматом данных [0, 1]. Однако, если предварительно разделить данные на категории (а так же привести к формату [0, 1]), и пропускать пары (имеется ввиду "предсказание" и "маска" только одной категории) в стандартную Dice-loss функцию, то это может сработать.

Код categorical_dice_loss
import torchclass LossFunction:    def __init__(self,                 prediction,                 target,                 device_for_training,                 semantic_binary: bool = True,                 ):        self.prediction = prediction        self.device = device_for_training        self.target = target        self.semantic_binary = semantic_binary    def forward(self):        if self.semantic_binary:            return self.dice_loss(self.prediction, self.target)        return self.categorical_dice_loss(self.prediction, self.target)    @staticmethod    def dice_loss(predictions, targets, alpha=1e-5):        intersection = 2. * (predictions * targets).sum()        denomination = (torch.square(predictions) + torch.square(targets)).sum()        dice_loss = 1 - torch.mean((intersection + alpha) / (denomination + alpha))        return dice_loss    def categorical_dice_loss(self, prediction, target):        pr, tr = self.prepare_for_multiclass_loss_f(prediction, target)        target_categories, losses = torch.unique(tr).tolist(), 0        for num_category in target_categories:            categorical_target = torch.where(tr == num_category, 1, 0)            categorical_prediction = pr[num_category][:][:][:]            losses += self.dice_loss(categorical_prediction, categorical_target).to(self.device)        return losses / len(target_categories)    @staticmethod    def prepare_for_multiclass_loss_f(prediction, target):        prediction_prepared = torch.squeeze(prediction, 0)        target_prepared = torch.squeeze(target, 0)        target_prepared = torch.squeeze(target_prepared, 0)        return prediction_prepared, target_prepared

Тут просто, но всё равно объясню "categorical_dice_loss":

  • подготовка данных (убираем ненужные в данном расчете измерения);

  • получения списка категорий, которые содержит каждый batch масок;

  • для каждой категории берем "прогноз" и "маску" соответствующих категорий, приводим значения к формату [0, 1] и пропускаем через стандартную Dice-loss;

  • складывая результаты и деля на количество категорий, получаем усредненное значение для каждого batct. Ну а дальше всё без изменений.

Так же, думаю, помог бы перевод данных к one-hot формату, но только не в момент формирования основного дата сета (раздует в размере), а непосредственно перед расчетом ошибки, но я не проверял. Кто в курсе, напишите, пожалуйста, буду рад. Результат работы данной функции будет в Главе(5).

5. Визуализация данных

Так и хочется добавить "..как отдельный вид искусства". Начну с того что прочитать *.nrrd оказалось самым простым.

Код
import nrrd# читает в numpyread = nrrd.read(data_path) data, meta_data = read[0], read[1]print(data.shape, np.max(data), np.min(data), meta_data, sep="\n")(163, 112, 120)14982-2254  OrderedDict([('type', 'short'), ('dimension', 3), ('space', 'left-posterior-superior'), ('sizes', array([163, 112, 120])), ('space directions', array([[-0.5,  0. ,  0. ],       [ 0. , -0.5,  0. ],       [ 0. ,  0. ,  0.5]])), ('kinds', ['domain', 'domain', 'domain']), ('endian', 'little'), ('encoding', 'gzip'), ('space origin', array([131.57200623,  80.7661972 ,  32.29940033]))])

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

Неправильный путь

Иными словами, чтобы сделать куб нам необходимо 8 вершин и 12 треугольных поверхностей. В этом и состояла первая идея (до применения специальных библиотек) - заменить все пиксели (числа в 3-х мерной матрице) на такие кубики. Код я не сохранил, но смысл прост, рисуем куб на месте "пикселя" со сдвигом -1 по трем направлениям, потом следующий и т.д.

Выглядит это так же бредово, как и звучитВыглядит это так же бредово, как и звучит

Отрицательный результат - тоже результат, продолжаем. На этом этапе я уже понял, что без сторонних библиотек мне не обойтись. Первой попыткой в была пара Skimage и Stl.

from skimage.measure import marching_cubesimport nrrdimport numpy as npfrom stl import meshpath = 'some_path.nrrd'data = nrrd.read(path)[0]def three_d_creator(some_data):    vertices, faces, volume, _ = marching_cubes(some_data)    cube = mesh.Mesh(np.full(faces.shape[0], volume.shape[0], dtype=mesh.Mesh.dtype))    for i, f in enumerate(faces):        for j in range(3):            cube.vectors[i][j] = vertices[f[j]]    cube.save('name.stl')    return cubestl = three_d_creator(datas)

Пользовался этим способом, но иногда файлы "ломались" в процессе сохранения и не открывались. А на те, которые открывались, ругался встроенный в Win 10 3D Builder и постоянно пытался там что-то исправить. Так же еще придется "прикрутить" к коду модуль для просмотра 3D объектов без их сохранения. Решение "из коробки" дальше.

На момент написания статью пользуюсь v3do. Коротко, быстро, удобно и можно сразу осмотреть модель.

Код перевода npy в stl и вывода объекта на дисплей
from vedo import Volume, show, writeprediction = 'some_data_path.npy'def show_save(data, save=False):    data_multiclass = Volume(data, c='Set2', alpha=(0.1, 1), alphaUnit=0.87, mode=1)    data_multiclass.addScalarBar3D(nlabels=9)    show([(data_multiclass, "Multiclass teeth segmentation prediction")], bg='black', N=1, axes=1).close()    write(data_multiclass.isosurface(), 'some_name_.stl') if save else None    show_save(prediction, save=True)

Названия функций говорят сами за себя.

Пришло время увидеть конечный результат всего вышесказанного. Томить не буду:

model.summary()
model = UNet(dim=3, in_channels=1, out_channels=9, n_blocks=3, start_filters=9).to(device)print(summary(model, (1, 168*, 120, 120)))    """----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv3d-1      [-1, 9, 168, 120, 120]            252              ReLU-2      [-1, 9, 168, 120, 120]              0       BatchNorm3d-3      [-1, 9, 168, 120, 120]             18            Conv3d-4      [-1, 9, 168, 120, 120]          2,196              ReLU-5      [-1, 9, 168, 120, 120]              0       BatchNorm3d-6      [-1, 9, 168, 120, 120]             18         MaxPool3d-7        [-1, 9, 84, 60, 60]               0         DownBlock-8  [[-1, 9, 84, 60, 60], [-1, 9, 168, 120, 120]]               0            Conv3d-9       [-1, 18, 84, 60, 60]           4,392             ReLU-10       [-1, 18, 84, 60, 60]               0      BatchNorm3d-11       [-1, 18, 84, 60, 60]              36           Conv3d-12       [-1, 18, 84, 60, 60]           8,766             ReLU-13       [-1, 18, 84, 60, 60]               0      BatchNorm3d-14       [-1, 18, 84, 60, 60]              36        MaxPool3d-15       [-1, 18, 42, 30, 30]               0        DownBlock-16  [[-1, 18, 18, 42, 30], [-1, 18, 84, 60, 60]]               0           Conv3d-17       [-1, 36, 42, 30, 30]          17,532             ReLU-18       [-1, 36, 42, 30, 30]               0      BatchNorm3d-19       [-1, 36, 42, 30, 30]              72           Conv3d-20       [-1, 36, 42, 30, 30]          35,028             ReLU-21       [-1, 36, 42, 30, 30]               0      BatchNorm3d-22       [-1, 36, 42, 30, 30]              72        DownBlock-23  [[-1, 36, 42, 30, 30], [-1, 36, 42, 30, 30]]               0  ConvTranspose3d-24       [-1, 18, 84, 60, 60]           5,202             ReLU-25       [-1, 18, 84, 60, 60]               0      BatchNorm3d-26       [-1, 18, 84, 60, 60]              36      Concatenate-27       [-1, 36, 84, 60, 60]               0           Conv3d-28       [-1, 18, 84, 60, 60]          17,514             ReLU-29       [-1, 18, 84, 60, 60]               0      BatchNorm3d-30       [-1, 18, 84, 60, 60]              36           Conv3d-31       [-1, 18, 84, 60, 60]           8,766             ReLU-32       [-1, 18, 84, 60, 60]               0      BatchNorm3d-33       [-1, 18, 84, 60, 60]              36          UpBlock-34       [-1, 18, 84, 60, 60]               0  ConvTranspose3d-35      [-1, 9, 168, 120, 120]          1,305             ReLU-36      [-1, 9, 168, 120, 120]              0      BatchNorm3d-37      [-1, 9, 168, 120, 120]             18      Concatenate-38     [-1, 18, 168, 120, 120]              0           Conv3d-39      [-1, 9, 168, 120, 120]          4,383             ReLU-40      [-1, 9, 168, 120, 120]              0      BatchNorm3d-41      [-1, 9, 168, 120, 120]             18           Conv3d-42      [-1, 9, 168, 120, 120]          2,196             ReLU-43      [-1, 9, 168, 120, 120]              0      BatchNorm3d-44      [-1, 9, 168, 120, 120]             18          UpBlock-45      [-1, 9, 168, 120, 120]              0           Conv3d-46      [-1, 9, 168, 120, 120]             90================================================================Total params: 108,036Trainable params: 108,036Non-trainable params: 0----------------------------------------------------------------Input size (MB): 3.96Forward/backward pass size (MB): 12170.30Params size (MB): 0.41Estimated Total Size (MB): 12174.66----------------------------------------------------------------    """

*Ввиду ещё большего уменьшения параметров сети(фильтр[9, 18, 36, 72]), удалось уместить объект в память видеокарты целиком - 9*(168, 120, 120)

6. After words

Думал, что закончил, а оказалось - только начал. Тут еще есть над чем поработать. Мне, в целом, 2 этап не нравится, хоть он и работает. Зачем заново переопределять каждый пиксель, когда мне нужен целый регион? А если, образно, есть 28 разделенных регионов, зачем мне пытаться определить их все, не проще ли определить один зуб и завязать это всё на "условный" ориентированный/неориентированный граф? Или вместо U-net использовать GCNN и вместо Pytorch - Pytorch3D? Пятна, думаю, можно убрать с помощью выравнивания данных внутри bounding box(ведь один зуб может принадлежать только 1 категории). Но, возможно, это вопросы для следующей публикации.

Прототип (набросок)
Тот самый "условный граф"
Пример неориентированного графа на 28 категорий с "разделителями"Пример неориентированного графа на 28 категорий с "разделителями"

Отдельное спасибо моей жене - Алёне, за особую поддержку во время этого "погружения в темноту".

Благодарю всех за внимание. Конструктивная критика и предложения, как исправлений, так и новых проектов - приветствуются.

Подробнее..

Разработка AI-продукта на основе машинного зрения. Промежуточная ретроспектива процесса мысли, боль, страдания

11.08.2020 08:15:01 | Автор: admin

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


Disclaimer: Я не специалист в нейросетях, но выполняю роль владельца продукта, в котором ключевую роль занимает AI. Эта статья для тех, кто вынужден делать такую же работу, а так же для тех специалистов ML, которые хотят понять, как на их деятельность смотрят люди со стороны бизнеса.


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


Мы уже давно делаем различные проекты для заказчиков, в том числе с элементами ML, но проект, где эта часть занимает центральное место, мы делали впервые. За это время я, как владелец продукта, узнал много нового, странного и сформулировал некоторые принципы, которые важны для достижения успеха при создании таких продуктов.


Риск в продуктах с AI


Риск колоссальный. Собственно, создание AI-продукта заканчивается там, когда весь риск снят. Если в случае создания продуктов на классических алгоритмах вы тратите на работу с риском от 5 до 20% времени, то, в случае с AI-продуктами, сам процесс создания продукта это борьба с риском. Я оцениваю объем потраченного времени на борьбу с риском до 90-95% времени от создания AI продукта. Из данного наблюдения следуют важные выводы.


Для продуктовых компаний


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


Риск настолько велик, что делать что-то по продукту до того, как завершена, протестирована и сдана часть, связанная с AI не имеет смысла.


Для контракторов


Заказчиков в сфере разработки AI-продуктов в SMB будет мало/не будет. Если вы не можете "зайти" к условному Tinkoff, можно сворачивать лавочку, хорошего бизнеса не будет. Государство самый вероятный и прибыльный клиент.


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


Для руководителей


Не соглашайтесь работать над подсистемами AI по Scrum или другим методологиям, которые фиксируются на сроках продвижения.


Мне кажется, что для создания подсистем AI не Agile подходит плохо, потому что, при его использовании, вы будете двигаться в ритме "3 шага вперед, затем 2 назад" с непредсказуемыми сроками доставки функционала.


Никому нельзя верить


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


Из недавних диалогов с тимлидом:


Контекст: YOLOv4 самая точная real-time нейронная сеть на датасете Microsoft COCO


Я: а зачем мы тестируем нейросеть Yolo4 в сравнении с Yolo3;
TL: потому что мы не верим создателю модели, даже если он наш соотечественник.

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


Необходимость все проверять ведет к очень медленному продвижению по доставке продукта.


Четко зафиксируйте условия работы


Это не является открытием для инженера в сфере ML, но вы вряд ли об этом услышите от него четко и ясно. Мысль очень проста и проистекает из природы обучения нейросетевых моделей: подобрать/научить модель, которая будет работать в зафиксированных условиях, в разы проще, чем сделать модель, которая будет работать в широком спектре условий.


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


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


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


Используйте метод пристального взгляда для оценки


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


Однако, даже модель с прекрасными показателями Precision, Recall, F1, etc. при тестировании методом пристального взгляда может очень огорчить вас.


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


Снизьте разрыв с бизнес-требованиями


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


Ситуация. Допустим, вы хотите обрабатывать в realtime поток видео с помощью Yolo4. Ставите задачу инженеру дай мне 60 FPS пайплайна на Tesla T4. Он выберет сетку размера 416x416 и будет приводить видео из исходного размера к этому, показывая вам что все работает на заданном FPS.


При этом, очевидно что у Yolo4 есть минимальный размер людей в пикселах, которых она четко определяет (FYI: он составляет ~ 15% от высоты фрейма (около 110 px для 720p). Все люди, которые меньше этой высоты, будут детектироваться с низким качеством. Этот вопрос скорее всего останется за кадром, если никто его не поднимет на повестке. Я выяснил важность данного аспекта на кейсе, который приведен далее.


Нормальный человек думает так: чем больше разрешение видео тем больше важных деталей на нем и тем лучше все будет работать. В случае с шаблонным стандартным подходом применения детекторов Single Shot в ML это не так вообще нет никакой разницы какое разрешение вы подали, поскольку ваш фрейм просто уменьшат до размера входа нейросети и вы не получите никаких преимуществ.


Здесь есть явное противоречие бизнес-ожиданий и технологических возможностей.


Самое странное из того, что я видел выглядело так:


  • целевое видео было размечено на предмет детекции людей;
  • это видео было скормлено двум нейросетям Yolo4 размера 320x320, 416x416;
  • получены разные результаты и спокойно записаны в таблицу.

Я не смог получить понятный ответ на вопрос "Зачем вы это делали, если, очевидным образом, при уменьшении размера, часть людей просто выпало из поля зрения нейросети 320x320, но осталась в 416x416"?


Правильный процесс, на мой взгляд, должен был выглядеть так:


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

На самом деле, мораль в том, чтобы подтвердить легитимность подхода с масштабированием изображения при использовании детекторов single shot в рамках заданных бизнес-требований.


Добейтесь общения на человеческом языке


Я в IT уже 15 лет, умею программировать на нескольких языках, хорошо знаю матчасть. Однако, каждый раз когда я общаюсь с отделом ML, самая частая фраза, которую я говорю: "Я не понимаю, объясните понятнее".


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


У нас есть прецеденты, когда специалисты из BigData или Backend не могут найти общий язык с людьми из ML, поскольку не каждый может и хочет заставлять людей общаться на понятном языке.


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


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


Если вы, как менеджер, не можете добиться того, чтобы ваши специалисты ML объяснили вам все на понятном вам языке, думаю, что стоит сразу менять либо работу, либо специалистов.


Инструменты для производительного инференса Terra Incognita


Если вам надо, чтобы работало быстро на платформе Nvidia вам надо связываться с Nvidia DeepStream или аналогичными фреймворками. Однако, через DeepStream точно будет быстрее всего. Из моего разговора с представителем Nvidia Inception, они настолько заинтересованы в том, чтобы кто-то делал и демонстрировал практические кейсы на DeepStream, что складывается впечатление, что это почти никто не умеет.


При этом переход от "Работает в PyTorch" к "работает на DeepStream" это отдельный большой и сложный проект, который может потребовать как написать что-то нетривиальное на C, чтобы расширить Gstreamer, так и поменять модели, поскольку они, например, не совместимы TensorRT.


Сама по себе отладка приложений в DeepStream это тоже отдельная песня, которая включает регулярную борьбу с Segmentation Fault, даже если вы программируете на Python c NumPy, а сама отладка весьма нетривиальна из-за архитектуры Gstreamer.


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


Смекалка и брутфорс


ML-инженеры стремятся оставаться в рамках своих инструментов и склонны к брутфорсному решению задач именно искусственным интеллектом. Они будут пытаться решить проблему в рамках своих инструментов, всячески игнорируя эвристики, логические ограничения бизнес-среды, etc. При этом фокусировка на данных аспектах может значительно ускорить производительность и снизить нагрузку на оборудование.


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


Добейтесь четкого видения направления движения и плана по его достижению


Нигде как в ML я не видел такой ценности в диаграммах WBS Каждый аспект ML-пайплайна требует существенной работы для снятия риска доставки. Я пришел к тому, что сделал в Trello вот такую канбан-борду, чтобы оценивать готовность ML-части продукта с точки зрения удовлетворения бизнес-потребностей.


Картинки кликабельны:




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


Используйте инструменты принятия решений при создании задач


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


Мне кажется, что начать можно с заполнения квадрата Декарта для каждой исследовательской задачи:



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


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

Обеспечьте как можно больший объем данных как можно раньше


Чем раньше вы обеспечите команду ML данными, которые возможны в реальном мире, сформулируете ожидания относительно обработки этих данных, тем ниже шанс, что команда сделает что-то, что работает только при температуре 23 градуса цельсия, только с 14 до 16 часов, при ретроградном Юпитере.


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

Подробнее..

DialoGPT на русском

30.03.2021 22:07:52 | Автор: admin

Всем привет. В конце 2019 года вышла одна из работ по GPT-2. Инженеры из Microsoft обучили стандартную GPT-2 вести диалог. Тогда, прочитав их статью, я очень впечатлился и поставил себе цель обучить такую же модель, но уже на русском языке.

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

Итак, когда все звёзды сошлись, потратив пару месяцев ночей на конструирование, обработку и очистку датасета, наконец-то обучил модель GPT-3 medium от Сбера вести диалоги. Большое спасибо DevAlone, за то что создал проект Pikastat, без которого потратил бы годы на сбор данных. Модель обучал 2 эпохи на 32 ГБ текста на библиотеке Transformers от Hugging Face. Обучение шло 12 дней на 4x RTX 2080 Ti.

Выражаю благодарность компании ICL Services, которая предоставила вычислительные мощности для обучения модели.

Датасет состоит из цепочек комментариев (~92% Pikabu, ~6% YouTube, ~2% VK). Количество сэмплов для обучения - 41 миллион. От ненормативной лексики датасет специально не очищал, поэтому будьте готовы к "неожиданным" ответам.

Ниже привожу примеры диалогов с обученной моделью. Там, где ответы от GPT длинные (свыше 50 токенов), выбирал наилучший среди трёх сгенерированных ответов. Также на построение каждого диалога были две попытки. В остальном, ничего не изменял и не подстраивал.

На скриншотах реплики от GPT производила полностью модель. А потому "все имена и события вымышлены, любые совпадения случайны".

Посмотреть еще больше диалогов
Посмотреть неудачные (стандартные проблемы диалоговых систем и моделей)

Среднее количество реплик при обучении 4, потому длинные диалоги (от десяти и более реплик) модель воспринимает тяжело. Кроме того, необходимо помнить, что длина последовательности модели 256 токенов.

На валидации был только лосс. Поэтому, чтобы самому оценивать качество модели "на глаз", собрал web-приложение. С JavaScript особо не дружу, но реализовал минимальный функционал, который задумывал. Репозиторий приложения лежит тут - там же инструкция для запуска.

Приложение состоит из двух частей:

  1. Сам сайт на Flask (Python)

  2. Сервис-генератор на FastAPI (Python)

После запуска сервиса и приложения взаимодействие будет выглядить примерно так:

Качество ответов сильно зависит от параметров генерации (тут хорошая вводная статья с объяснением параметров). При установке генерации нужной длины ("Length generate" в блоке слева), некоторые параметры также следует изменить для лучшей генерации. Сейчас как раз работаю над их оптимизацией, а также работаю над классификатором, который будет отбирать среди длинных ответов наиболее "удачный".

Теперь посмотрим в каком виде строка кодируется в модель:

Здесь зеленым цветом 0 либо 1, это speaker id. Он показывает какая реплика кому принадлежит. Красным выделен параметр, отвечающий за длину генерации, который принимает следующие значения: [ -, 1, 2, 3].

  • "-" - означает, что мы не ожидаем от модели какой-то конкретной длины генерации

  • "1" - модель будет стараться сгенерировать короткий ответ. При обучении диапазон был до 15 токенов

  • "2" - модель будет стараться сгенерировать средний ответ. При обучении диапазон был от 15 до 50 токенов

  • "3" - модель будет стараться сгенерировать длинный ответ. При обучении диапазон был от 50 до 256 токенов

В данном примере мы ожидаем от системы "длинный" ответ на вопрос "Что нового?"

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

Также приметил, что неплохо отвечает на философские вопросы.

Но и минусов в данной версии модели тоже достаточно: обучение на небольшом датасете (41 миллион, у Microsoft было 147), генерация плохого качества длинных ответов, выдача "размытых" ответов, недообученность модели, плохой "симбиоз" с весами от Сбера.

Модель доступна на Hugging Face Model Hub. Также можете скачать с Google Drive.

Ну и напоследок: в детстве мне очень нравился фильм "Я робот". Давайте посмотрим, как отвечает модель на вопросы от детектива Спунера:

Видно, что до уровня Санни еще очень далеко. И это хорошо, так как есть куда стремиться.

По всем вопросам и пожеланиям пишите на grossmend@gmail.com. В дальнейшем постараюсь улучшать данную модель и выкладывать обновленные версии. До новых встреч!

Подробнее..

Artificial Intelligence, герой нашего времени. Этюд

11.04.2021 02:13:35 | Автор: admin

Хм. Один из пунктов, регламентирующих действия модераторов на Хабре, сформулирован следующим образом: не надо пропускать статьи, слабо относящиеся к IT-тематике или не относящиеся к ней вовсе. Что сходу заставило автора призадуматься, а имеет ли прямое отношение к "IT-тематике" его пост, повествующий о некоторых этапах программирования забавного и увлекательного своего pet-проекта, несложного AI, выстраивающего нейронную сеть на основе ruby-обертки FANN для игры в крестики-нолики? Вопрос не содержит скрытого кокетства, ведь описанию логики программного кода в моем рассказе предназначено далеко не первостепенное значение. "Да это злая ирония!" скажете вы. Не знаю.

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

Итак, с места в карьер... после первого запуска программа начинает процесс самообучения, проигрывая сама с собой несколько десятков (минута - две максимум) тысяч партий (число, понятное дело, доступно для редактирования в конфиге; учитывая описываемый далее не вполне обычный алгоритм, положенный в основу логики этого AI - эксперименты такого рода также способны предоставить интересный материал для умозаключений). Здесь имитируется процесс обучения, свойственный многим другим Artificial Intelligence, с той лишь разницей, что оба "игрока" в равной степени играть не умеют, делая абсолютно рандомные ходы. Но правила игры действуют: если случайный ход не соответствует, программа обязана переходить, соответственно и выигрыш достанется той стороне, которая выиграет. Все честно: никаких подчисток и хаков, скрытых предпочтений, никаких тебе фейковых допинг-проб, зачастую в реальной жизни опрокидывающих результаты спортивных игр.

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

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

Если есть на Хабре парочка-другая читателей, которым психология ближе (я не про эйчаров), нежели программирование - сказанное легко облечется для них в канву профессиональной терминологии. Но описываемый мной психологический сценарий - крайность... частные случаи которого, менее заметные и разительные - встречаются, на мой субъективный взгляд, очень часто.

Итак. Примем на минутку предложенную точку зрения: мир заведомо непознаваем, события случайны и призрачны. Опереться, таким образом, не на что, у нашей программки практически нет точек опоры в виде той или иной стратегии, она располагает лишь записями случайных ходов, каждая из которых снабжена, правда, еще и сопутствующей информацией: общее количество ходов и итог игры (выигрыш/проигрыш). Сумеет ли наш виртуальный игрок-нигилист, отказавшийся от несложной и эффективной логики игры на основе известных стратегий Tic Tac Toe - построить собственную стратегию, хотя бы мало-мальски успешную? Оказывается - да, вполне. Полученный результат сложно назвать инновационным и многообещающим, это, скорее, пародия на образ мыслей современного кафкианца, чем-то напоминающая историю барона Мюнхгаузена, тщащегося вытащить самого себя из болота за волосы, помните?... кстати, слово "болото" здесь удачно продолжает использованную аналогию; повторюсь, "точки опоры" у значительной части нашего с вами современного социума, как показывает житейская практика - "при наличии отстутствия", данное утверждение легко проверяется на многочисленных параноидальных мифах, от отрицания ковида и до злополучного "а вот не докажете!".

Попробуем аргументировать сказанное, хотя бы в контексте простенького нашего Artificial Intelligence. Как думаете, какой ход в любой момент игровой ситуации на поле 3x3, используемом для игры в крестики-нолики, является безусловно оптимальным? Или, иными словами, если у вас перед глазами лог игры, что именно вам необходимо, чтобы, задержав взгляд на строчке, описывающей очередной ход, и не читая далее - уверенно заявить, что в данной ситуации этот ход наилучший? Поставьте себя на место AI, вся "интеллектуальная мощь" которого заключена в нескольких коротких скриптах; здесь необходимо что-то совсем простое и безошибочное, без долгих логических рассуждений и необходимости просчитывать на несколько ходов вперед.

Хм, "и очень даже просто". (с) Если в логе случайно отыгранных игр присутствует хотя бы одна запись, где ход является последним, он в данной ситуации - наилучший. Не правда ли? Вот вам и вся логика, на основе которой начинаем формировать веса нейронной сети:

        if row[6].to_i - row[3].to_i == 1            x_data.push([row[0].to_i])            y_data.push([1]) # Присваиваем высший приоритет, т.е. максимально возможный вес, переопределяя начальный.        end

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

Внезапно, в самый разгар работы над Tic Tac Toe AI with Neural Network пазл сложился (это я уже не о кодинге). Разгадка оказалась удручающе простой, но путь к ней длинен и непрост: суть в том, что ни малейших попыток понимания в данном случае как и в случаях иных не было у моего знакомого и в помине. Странный объект моих отнюдь непрограммистских изысков жил в собственном мире, будто в бункере, видя во внешних объектах лишь проекции, разнообразные и разрозненные частички самого себя.

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

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

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

Немалую опасность для живущего в мире иллюзорной Матрицы виртуального игрока в крестики-нолики представляют вилки (просчитывать ситуацию на доске хотя бы на один - два хода вперед явно не наш life style). Что же, поиском вилок сейчас и попробуем заняться:

  WINNING_TRIADS = [    [0, 1, 2],    [3, 4, 5],    [6, 7, 8],    [0, 3, 6],    [1, 4, 7],    [2, 5, 8],    [6, 4, 2],    [0, 4, 8]  ].freeze

Далее, при формировании csv-лога ходов, ищем:

  def fork?    WINNING_TRIADS.select do |x|      @board[x[0]] == @board[x[1]] && @board[x[2]].class != @board[x[0]].class &&        place_x?(x[0]) ||        @board[x[1]] == @board[x[2]] && @board[x[0]].class != @board[x[2]].class &&          place_x?(x[1]) ||        @board[x[0]] == @board[x[2]] && @board[x[1]].class != @board[x[2]].class &&          place_x?(x[0])    end  end

Таким образом, если комбинация найдена два раза...

  if @game.fork?.size > 1

...вилка найдена.

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

Определим ряд потенциально опасных ситуаций:

  DANGEROUS_SITUATIONS_1 = [    [6, 4, 2],    [0, 4, 8]  ].freeze  DANGEROUS_SITUATIONS_2 = [    [0, 4, 7],    [0, 4, 5],    [2, 4, 3],    [2, 4, 7],    [3, 4, 8],    [1, 4, 8],    [1, 4, 6],    [5, 4, 6]  ].freeze
  def fork_danger_1?    DANGEROUS_SITUATIONS_1.detect do |x|      @board[x[0]] == @board[x[2]] &&        @board[x[0]] != @board[x[1]]    end  end    def fork_danger_2?    DANGEROUS_SITUATIONS_2.detect do |x|      @board[x[0]] == @board[x[2]] &&        @board[x[0]] != @board[x[1]]    end  end  def fork_danger_3?    DANGEROUS_SITUATIONS_1.detect do |x|      @board[x[0]] != @board[x[2]] &&        @board[x[1]] == @board[x[2]]    end  end

И, соответственно, создадим три массива, в которые, при анализе ситуации на доске, AI станет помещать удовлетворяющие условиям ходы: 1. однозначно неприемлемые, 2. потенциально приводящие к вилке и 3. атакующие (т.е. те, в силу которых противник вынужден, во избежание немедленного проигрыша, реализовать единственно возможный для него ход). Разумеется, массивы будут иногда пересекаться, учтем это при построении логики игры. Кроме того, последнее слово за Neural Network.

  array_of_games.each do |row|      row.each do |e|        next unless e == current_position        if row[6].to_i - row[3].to_i == 2 && row[4] == 'O' && row[2].to_f != 0.2          unacceptable_moves_array << row[0]        # Find moves that inevitably lead to a fork:        elsif fork_danger_1 && row[3].to_i == 3 && row[0].to_i.odd?          unacceptable_moves_array << row[0]        elsif (fork_danger_2 || fork_danger_3) && row[3].to_i == 3 && row[0].to_i.even?          unacceptable_moves_array << row[0]        end        next if row[5].nil?        # Find moves that may lead to a fork:        array_of_moves_to_fork << row[0] if row[3].to_i == row[5].to_i        # Find attacking moves:        attack_moves_array << row[0] if row[3].to_i == row[5].to_i && row[6].to_i < 7      end    end

Повторюсь, удалось бы обойтись без костылей, если бы массив игр, используемый AI для анализа, не формировался практически полностью рандомно. Но... я ведь оговорил с самого начала, данный программный код родился как иллюстрация рефлексий автора, родившегося в стране Онегина, Печорина, Базарова... к слову, герои "Бесов" Достоевского и несколько более симпатичный Феличе Риварес из книги Войнич тоже ведь в этом перечне. Некий исторический сарказм присутствует в том, что, судя по прочитанному и перечитанному уже много позже школы - российский нигилизм претерпел значительные изменения в своей, так сказать, результирующей... не замечали? - а вы припомните незабвенное "разговаривают, разговаривают, контрреволюция одна", сумеете проследить немало аллюзий и аналогий с нашим днем.

 array_of_games.each do |row|      row.each do |e|        next unless e == current_position        next if arrays[0].include?(row[0])        unless arrays[1].include?(row[0]) && !arrays[2].include?(row[0])          if row[6].to_i - row[3].to_i == 1            x_data.push([row[0].to_i])            y_data.push([1])          elsif row[6].to_i - row[3].to_i == 3            if arrays[2].include?(row[0])              x_data.push([row[0].to_i])              y_data.push([0.9])            elsif arrays[1].include?(row[0])              x_data.push([row[0].to_i])              y_data.push([0.3])            end          else            x_data.push([row[0].to_i])            y_data.push([row[2].to_f])          end        end      end

Сухой остаток скармливаем нейронке:

    data = nn_data(board, fork_danger_1, fork_danger_2, fork_danger_3, array_of_games)    fann_results_array = []      train = RubyFann::TrainData.new(inputs: data[0], desired_outputs: data[1])      model = RubyFann::Standard.new(        num_inputs: 1,        hidden_neurons: [4],        num_outputs: 1      )      model.train_on_data(train, 5000, 500, 0.01)      data[0].flatten.each do |i|        fann_results_array << model.run([i])      end    result = data[0][fann_results_array.index(fann_results_array.max)]

Интересная деталь: в одной и той же игровой ситуации на доске (и с одним и тем же csv-файлом) этот Neural Network способен выдавать различные варианты ходов.

В итоге - у вас максимум ничья, минимум - проигрыш, выиграть не получится. Разве что подведет рандомно сгенерированный csv-файл (такое случается, но нечасто), который в редком случае вашего выигрыша оптимально пересоздать. Впрочем, описанная ревизия кода - или не только кода - может статься, вовсе не окончательная, итоги подводить рано.

P.S. Описанный код всегда доступен полностью (а не фрагментарно, как диктует формат статьи) в моем гитхабе, разумеется, любой желающий может сделать git clone и поэкспериментировать с кодом, ну или просто поиграть. Я не сторонник запуска ruby-application под виндой, это очень не лучшая идея, но в данном случае работать будет, попробовал. Возможно, получится чуть менее эффектно, чем в консоли линукса, но логика отработает.

Подробнее..

Распознавание мяча в волейболе с OpenCV и Tensorflow

17.08.2020 06:18:36 | Автор: admin
После первого опыта распознавания спортивных движений у меня зачесались руки сделать что-нибудь еще в этом направлении. Домашняя физкультура уже казалась слишком мелкой целью, так что я замахнулся на игровые виды спорта.

Применение искусственного интеллекта к спорту недавняя тенденция, но уже есть интересные материалы:


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

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

Распознавание движущегося мяча (aka ball tracking) довольно популярная тема и про нее написано немало статей. Однако, в основном это демо-информация про возможности технологий, чем про применение в реальной жизни (и в реальных игровых видах спорта).

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

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

Австрийское видео имеет свои особенности. Главных деталей три:

  1. статичная камера за кортом
  2. Уровень игры. Уровень игры непосредственно влияет на скорость мяча, ибо у серьезных людей мяч часто можно увидеть только на замедленном повторе
  3. Цвет мяча, желто-синий не сильно контрастирует с цветом пола, что делает бессмысленными практически все подходы через цветовые фильтры

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

    mask = backSub.apply(frame)    mask = cv.dilate(mask, None)    mask = cv.GaussianBlur(mask, (15, 15),0)    ret,mask = cv.threshold(mask,0,255,cv.THRESH_BINARY | cv.THRESH_OTSU)

Таким образом вот эта, например, картинка:



Превращается в такую маску:



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

Это коненчно не всегда так. Например, здесь верхний контур оказался плечом судьи:



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

Датасет верхнего контура выглядит разнопланово:



Но с точки зрения нейросетей представляет собой не более чем бинарную классификацию цветных картинок. Таким образом за основу модели я взял известную задачу Котики-против-Собак.

Вариантов реализации миллион, самая популярная архитектура VGG, хорошо описана в этой статье блога Keras.

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

   model = Sequential([        Convolution2D(32,(3,3), activation='relu', input_shape=input_shape),        MaxPooling2D(),        Convolution2D(64,(3,3), activation='relu'),        MaxPooling2D(),        Flatten(),        Dense(64, activation='relu'),        Dropout(0.1),        Dense(2, activation='softmax')      ])    model.compile(loss="categorical_crossentropy", optimizer=SGD(lr=0.01), metrics=["accuracy"])

Как я ни крутил модель, добиться лучшег чем 20% ложно-отрицательных и 30% ложно-положительных не удалось.

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



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

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

В общем, на коленке пришлось сделать некий фреймворк управления траекториями.

Вот записанные траектории за розыгрыш:


(cиним кандидаты в траектории, зеленым статические пятна, серым случайные).

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

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



Может быть можно еще выжать какие-то улучшения с помощью компьютерного зрения, однако более перспективным вариантом видится предсказание траекторий через баллистику, и следующим шагом через смену состояний в розыгрыше (подача, прием, пас).

И в завершение еще несколько ссылок на подобные изыскания:

Подробнее..

Голосовой бот телефония на полном OpenSource. Часть 2 учим бота слушать и говорить

12.10.2020 04:15:08 | Автор: admin

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

Чтобы наш чат-бот заговорил и начал слушать, нужно пройти несколько подготовительных этапов:

  1. Озвучить все ответные фразы
  2. Установить систему телефонии
  3. Установить систему распознавания голоса
  4. Написать простые скрипты для связи телефонии и нашей нейронной сети с чат-ботом

Шаг 1: Озвучка ответных фраз


Так как наш бот довольно примитивный и может произносить только заранее подготовленные фразы, то первым делом озвучим все наши ответные фразы с использованием к примеру yandex speechkit. Положим их в корневую директорию с аудиозаписями freeswitch /usr/share/freeswitch/sounds/en/us/callie/ivr предварительно обрежем длину имени до 50 символов.

Шаг 2: Установка системы телефонии


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

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

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

PS. для удобства конфигурирования Freeswitch можно установить вэб-интерфейс FusionPBX

Шаг 3: Установка системы распознавания голоса


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

docker run -d -p 2700:2700 alphacep/kaldi-ru:latest

Далее необходимо сконфигурировать mod_vosk для freeswitch, для этого в директории /etc/freeswitch/autoload_configs необходимо создать файл vosk.conf.xml, если его нет. В данном файле необходимо указать только адрес вашего vosk сервера:

<configuration name="vosk.conf" description="Vosk ASR Configuration">  <settings>    <param name="server-url" value="ws://localhost:2700"/>    <param name="return-json" value="0"/>  </settings></configuration>

После настройки можно запустить сам freeswitch

systemctl start freeswitch

И запустить модуль

fs_cli -x "load mod_vosk

Шаг 4: Скрипты для запуска распознавания голоса


Для связки телефонии с нейронной сетью чат бота можно использовать rest api интерфейс, который необходимо реализовать и lua скрипт для передачи распознанного текста в rest api и озвучки ответов.

Шаг 4.1: REST API интерфейс для нейронной сети


Самым быстрым и удобным способом научить нейросеть отвечать на http запросы является библиотека Fastapi для python. Для начала объявим класс Prediction, который содержит формат входных данных для запроса.

class Prediction(BaseModel):text: str

Загрузим нашу модель

model = Sequential()model.add(LSTM(64,return_sequences=True,input_shape=(description_length, num_encoder_tokens)))model.add(LSTM(32))model.add(Dropout(0.25))model.add(Dense(1024, activation='relu'))model.add(Dropout(0.25))model.add(Dense(158, activation='softmax'))opt=keras.optimizers.adam(lr=0.01,amsgrad=True)model.compile(loss='categorical_crossentropy',  optimizer=opt,  metrics=['accuracy'])#model.summary()model.load_weights("h_10072020.h5")

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

def get_answer(text):t = preprocess_ru_text(text) # функция по препроцессингу текста, такая же как и при обучении моделиinput_data = np.zeros((1,description_length,num_encoder_tokens),dtype='float32')j=0for word in t:wordidxs = np.zeros((num_encoder_tokens),dtype='float32')if word in input_token_index:wordidxs[input_token_index[word]]=1input_data[0,j]=wordidxsj+=1print(word)results = model.predict(input_data)print (results[0][np.argmax(results)],          list(y_dict)[np.argmax(results)])if results[0][np.argmax(results)]>0.5:return random.choice(result_config['intents'][list(y_dict)[np.argmax(results)]]['responses'])else: #если уверенность нейронной сети меньше 50%, возвращаем фразу, что не расслышали вопрос.return random.choice(result_config['failure_phrases'])

и в конце сделаем интерфейс для получения ответа по запросу:

@app.post('/prediction/',response_model=Prediction)async def prediction_route(text: Prediction):question = text.textanswer = get_answer(question)return HTMLResponse(content=clear_text(answer)[:50], status_code=200) # обрезаем длину ответа до 50, чтобы совпадало с именем озвученных файлов

Можно запускать наш сервис:

uvicorn main:app --reload --host 0.0.0.0

Теперь при запросе на localhost:8000/prediction:

{"text":"привет"}

мы получаем ответ:

Хай

Шаг 4.2: LUA скрипт для запуска приложения на freeswitch


Задачами lua скрипта будут во первых получение распознанного текста из звонка, во вторых получение ответа от нейронной сети и воспроизведение подготовленного файла с озвученной фразой.

Для возможности осуществления http запросов из lua необходимо установить библиотеку luasocket.

Чтобы без проблем импортировать эту библиотеку, добавьте в свой скрипт строчку:

package.path = package.path .. ";" .. [[/usr/share/lua/5.2/?.lua]];

Далее напишем небольшую функцию получения ответа от нашей нейронной сети:

function sendRequest(speech_res)  local path = "http://localhost:8000/prediction/";  local payload = string.format("{\"text\":\"%s\"}",speech_res);  log.notice(payload);  local response_body = { };  log.notice(path);  local res, code, response_headers, status = http.request  {    url = path,    method = "POST",    headers =    {      ["Authorization"] = "",      ["Content-Type"] = "application/json",      ["Content-Length"] = payload:len()    },    source = ltn12.source.string(payload),    sink = ltn12.sink.table(response_body)  }  return trim1(table.concat(response_body))end

И запускаем в бесконечном цикле распознавание и воспроизведение ответных фраз:

session:execute("play_and_detect_speech", "ivr/привет я могу посоветовать тебе фильм или сериал.wav detect:vosk default");while session:ready() do    local res = session:getVariable('detect_speech_result');if res ~= nil thenlocal speech_res = session:getVariable("detect_speech_result");local response_body = sendRequest(speech_res);log.notice(response_body);session:execute("play_and_detect_speech", "ivr/"..response_body..".wav detect:vosk default");endend

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

<extension name="7000" continue="false"><condition field="destination_number" expression="7000"><action application="lua" data="test_vosk.lua"/></condition></extension>

И перечитать конфиги:

fs_cli -x "reloadxml"

Заключение


После запуска нейронной сети и freeswitch можно позвонить на номер с привязанным lua скриптом и поговорить с роботом.
Подробнее..

Первое место на AI Journey 2020 Digital Петр

27.12.2020 16:14:22 | Автор: admin
Приветъ ХабрПриветъ Хабр

Всем добрейшего дня! Совсем недавно закончилось ежегодное международное соревнование AI Contest, организатором которого является Сбер вместе с российскими и зарубежными партнёрами в рамках конференции Artificial Intelligence Journey. Задачи этого года: Digital Петр: распознавание рукописей Петра I, NoFloodWithAI: паводки на реке Амур и AI 4 Humanities: ruGPT-3. В этот раз в соревновании участвовало около 1000 человек из 43 государств.

Наша команда приняла участие в решении задачи "Digital Петр: распознавание рукописей Петра I" и заняла первое место. Я бы хотел рассказать о том, что мы наворотили в процессе решения соревнования, кто тут батя, какие трюки и фишки использовали. Информации много, будет много спецэфичных слов, для тех кто не в теме. Это не туториал, очень подробно я описывать не буду, но с удовольствием отвечу на вопросы в комментариях.

Можете посмотреть на команду мечты

План

Описание задачи

Формат данных, доступные ресурсы и ограничения

Если без воды, то: Необходимо перевести строку, написанную от руки Петром I, в печатный формат (см. пример ниже). Организаторы совместно с историками подготовили данные, разбив документы построчно, где каждая строка - картинка и ей соответствует текстовый файл с расшифровкой.

Примеры. Текст от руки и печатный аналог

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

Этапы решения

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

1. Предобработка данных

Выбросили картинки, на которых очевидно не верна разметка (нашли с помощью OOF), удалили редкие символы. Так как Пётр писал не только горизонтально (как на картинках выше), но и на полях (как на картинке ниже), то в данных присутствовали картинки с вертикальными надписями, которые нужно было перевернуть либо на +90, либо на -90 градусов. Для поворота картинок мы обучили сеть (Resnet34 с изменённой головой) которая предсказывала есть ли необходимость поворачивать картинку и в какую сторону. Это необходимо для того, чтобы поворачивать картинки на скрытых данных.

Пример вертикальной картинки

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

2. Описание нейронной сети

Мы рассматривали два варианта архитектур нейронных сетей, одну под CTCLoss и другую на классическом Attention. Отдельно про CTCLoss можно посмотреть тут, а про Attention почитать тут. Начали с CTCLoss, но на нём и остались, так как на подход с Attention не хватило времени. Сразу покажу картинку.

Где Bs - размер батча, (w, h, c) - параметры изображения (ширина, высота, каналы). Штрихи указывают на производные параметры от исходных. Hidden size - размер скрытого слоя в LSTM слое. Dict Size - количество буковок, которые будет знать наша нейронка. Dense - слой полносвязной сети в Keras, аналог Linear в PyTorch.

3. Аугментации

Что такое аугментации, как их применять можно посмотреть тут и тут. Мы использовали стандартные аугментации: ToGray, CLAHE, Rotate, CutOut.

Однако CutOut мы в середине соревнования заменили на другую аугментацию. Мы написали её сами, назвали HandWrittenBlots, суть в том, что это имитация человеческой почеркушки с различным размером, наклоном и прозрачностью. Сделано это для того, чтобы улучшить (кто бы мог подумать) распознавание перечёркнутых букв. CutOut накидывал, HandWrittenBlots накинул еще больше. Аугментацию можно найти в репозиторииAugmixations. Пример использования тут.

P.S. Форма вырезанных прямоугольников в CutOut тут такая потому, что параметры были подобраны эмпирически и вертикальные тонкие прямоугольники докидывали больше всего.

4. CharMasks

Это крутая штука, которая возможна, когда используешь CTC Loss. Дело в том, после предсказания моделью последовательности символов, есть возможность разбить входную картинку по этим символам, пропорционально размеру выходной последовательности (руками разбивать картинки конечно тоже можно, но это совсем прохладная история). Для этого нужно использовать координаты стыков различных букв (Именно так делали ребята для Action Labeling тут).

Таким образом мы получаем координаты начала и конца каждого символа. А затем, имея координаты всех символов во всех строках датасета, мы можем сами генерировать любую фразу почерком Петра. Вопрос остаётся в том, что нам пока негде брать древнерусские фразочки. Поэтому мы идём в гугл и нагугливаем книжки с текстами XVII-XVIII веков (Да, их тоже надо почистить и обработать). И уже теперь, с чистой совестью, достаём любую фразу из нашего корпуса текстов и составляем по ней картинку.

всемъ спасибо за соревъ piterвсемъ спасибо за соревъ piter

Да, она будет не идеальна, и если генерировать изображения, используя однобуквенные символы, то скор это не улучшит. Но тут в дело вступают токенизаторы. Обучаем мы их на предложениях из имеющегося датасета, в котором у нас есть соответствующие картинки. Таким образом для каждого токена мы теперь можем получить картинку. Далее с помощью токенизаторов (Multi Word Expression) мы можем составлять фразы из токенов (состоящих из нескольких символов и пробелов) различной длины и генерировать соответствующую картинку.

всемъ спасибо за соревъ piterвсемъ спасибо за соревъ piter

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

всемъ спасибо за соревъ piterвсемъ спасибо за соревъ piter

5. Spell correction using XLMRoberta

Сразу скажу, что в этом пункте много текста.

Естественно, нашасупермегапаверфьюженстелскрутаямодель не предсказывает идеальные предложения и всё же делает некоторые ошибки(особенно пробелы, ненавижу пробелы). И совершенно случайно в наши ряды затисались эксперты NLP. Ну они и обучили буквенную языковую модель XLMRoberta на корпусе XVII-XVIII в.в., а затем реализовали модель исправления опечаток в стиле Петра I. Делали следующее:

1. из сырого выхода OCR модели (перед тем как схлопнуть повторяющиеся символы и паддинги) склеивали повторяющиеся символы (включая паддинг) и пересчитывали их вероятности (среднее + softmax), брали 3 наиболее вероятные символа (буквы/цифры/blank в т.ч.) для каждой позиции в тексте;

2. каждую локальную позицию проверяли и исправляли так: давали 3-4 варианта модели, а она выбирала наиболее правильный - т.к. символы были буквы/цифры/blank, то таким образом мы боролись как с расстановкой пробелов, так и с другими видами опечаток с учетом контекста. Также с помощью данного подхода легко реализовать zero-shot learning, где предсказываются символы, которых не было в исходном датасете. Так мы накинули варианты похожих с точки зрения OCR латинских и кириллических букв ('р': 'p', 'о': 'o', 'е': 'e', 'с': 'c', 'а': 'a', 'х': 'x', 'и': 'u', 'к': k);

3. сортировали все локальные позиции по уверенности OCR модели и исправляли по одной step by step (!), что позволило улучшить и главное не испортить следующие предикты на более уверенных позициях;

4. обучали модель так: маскировали буквы (рандомно от 0 до 12), 50% масок превращали в padding (борьба с наличием лишних символов), 10% оставшихся букв заменяли на рандомный символ в тч и. паддинг (для стабилизации предикта). пытались предсказать маскированные буквы на фичах XLMRoberta из outputhiddenstates - почти как NER, но классификация на все заданные символы;

5. на GPU данная модель учится довольно долго, поэтому мы юзали TPU на Colab

P.S.
После завершения соревнования мы узнали (один из участников опубликовал своё решение), что в этой задаче можно было применить BeamSearch. Реализация которого есть тут.

6. Ensemble + Spell Correction Thresholds

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

Что не сработало

Other Backbones. Мы ставили эксперименты с кучей других бекбонов и доп блоками (EfficientNet, [SE, ECA]ResNet[xt], Mobilenet и др), но на удивление лучше всего заходит классический Resnet34.

Augmentations.Перепробовали практически весь набор аугментаций из всеми нами любимогоAlbumentations (Brightness, Gamma, Blur и др), остались только те, что я указал выше.

TTA (Test-Time Augmentations).Интересно то, что на нашей holdout выборке ТТА давал прирост, а на public test - нет. Мы решили верить паблик тесту, так как там выборка заметно больше нашей на holdout.

Classic Blending.Как было сказано выше, по причине того, что каждая модель может выдавать текста, длины которых будут отличаться, и на самом деле еще по ряду причин, связанных с рекуррентностью выходного текста.

Команда

Все четверо из команды работаем в компании ОЦРВ в лаборатории искусственного интеллекта и нейронных сетей в городе Сириус (Сочи). Спасибо ребятам, что продержались до конца и показали отличный результат! :)

Информация каждом члене команды

Карачёв Денис (github, linkedin, kaggle)

Шоненков Алексей (github, linkedin, kaggle)

Смолин Илья (github, linkedin, kaggle)

Новопольцев Максим (linkedin, kaggle)

Заключение

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

P.S.Наше самое быстрое решение (одна модель, public):
| CER: 2.531 | WER: 13.5 | ACC: 62.107 | TIME: 32s |
Код submission и веса опубликованы здесь.

P.P.S. Бонус

Особо пытливым предлагаю разгадать ребус, что же здесь написано? :)

Подробнее..

Рецепт обучения нейросетей

06.02.2021 02:09:12 | Автор: admin

Перевод статьи A Recipe for Training Neural Networks от имени автора (Andrej Karpathy). С некоторыми дополнительными ссылками.

Также доступна версия на украинском языке в личном блоге: Рецепт навчання нейрнонних мереж.

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

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

1) Нейронные сети это дырявая абстракция

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

>>> your_data = # подставьте свой датасет здесь>>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer)# покорите мир здесь

Эти библиотеки и примеры активируют часть нашего мозга которая привычна к стандартным программам - место где чистые API и абстракции часто достижимы. Например, библиотека requests:

>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))>>> r.status_code200

Круто! Смелый разработчик взял на себя бремя понимание строк запросов, URL, GET / POST запросов, HTTP соединений и т.д., и во многом скрыл сложность за несколькими строками кода. Это то, с чем мы знакомы и ожидаем. К сожалению, нейронные сети не похожи на это. Они не "готовая" технология, когда вы немного отклонились от обучения классификатора ImageNet. Я пытался указать на это в своей публикации "Да вы должны понимать метод обратного распространения ошибки" ("Yes you should understand backprop"), выбрав метод обратного распространения ошибки и назвав его "дырявой абстракцией", но ситуация, к сожалению, гораздо сложнее. "Обратное распространение ошибки" + "Стохастический градиентный спуск не делает вашу нейронную сеть магически работающей. Пакетная нормализация не заставляет ее магически сходиться быстрее. Рекуррентные нейронные сети не позволяют магически "вставить" текст. И только потому, что вы можете сформулировать вашу проблему в форме "обучение с подкреплением" не означает, что вы должны это делать. Если вы настаиваете на использовании технологии, не зная как она работает, вы, вероятно, потерпите неудачу. Что подводит меня к

2) Обучение нейронных сетей ломается молча

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

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

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

Рецепт

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

1. Cтаньте едиными c данными

Первый шаг к обучению нейронных сетей - это вообще не касаться кода нейронной сети, а взамен начать с тщательной проверки ваших данных. Этот шаг критический. Я люблю тратить много времени (измеряется в часах), проверяя тысячи примеров, понимая их распределение и ища закономерности. К счастью, ваш мозг хорошо с этим справляется. Однажды я обнаружил, что данные содержат примеры которые повторяются. В другой раз я обнаружил поврежденные изображения / разметку. Я ищу дисбаланс данных и смещения. Обычно я также обращаю внимание на свой собственный процесс классификации данных, который намекает на виды архитектур которые мы со временем изучим. В качестве примера - достаточно локальных особенностей, или нам нужен глобальный контекст? Сколько существует вариаций и какую форму они принимают? Какая вариация ошибочная и может быть предварительно обработана? Имеет ли значение пространственное расположение или мы хотим его усреднить (с помощью операции average pool)? Насколько важны детали и насколько сильно мы можем позволить себе уменьшить размер изображений? Насколько зашумленная разметка?

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

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

2. Настройте сквозной скелет обучения / оценки + получите простой базис (базовую модель)

Теперь, когда мы поняли наши данные, можем ли мы добраться до нашей чрезвычайно крупномасштабной ASPP FPN ResNet и начать обучение великолепных моделей? Точно нет. Это путь к страданиям. Наш следующий шаг - создать полный скелет обучение + оценка и завоевать доверие к его правильности путем серии экспериментов. На этом этапе лучше выбрать какую-то простую модель, которую невозможно как-то испортить - например линейный классификатор или очень крошечную сверточную сеть. Мы хотим обучать сеть, визуализировать потери, любые другие показатели (например, точность), моделировать прогнозы и проводить ряд экспериментов по отключению частей сети (при этом выдвигать гипотезы как это повлияет на результаты) на всем пути.

Советы и подсказки на этом этапе:

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

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

  • добавьте значащие цифры в вашей оценки. При построении графика тестовых потерь проведите оценку по всему (большому) набору тестов. Не просто складывайте тестовые потери по партиям, а затем полагайтесь на их сглаживания в Tensorboard. Мы преследуем правильность и серьезно настроены уделить время для сохранения уверенности.

  • проверяйте потери в начале. Убедитесь, что показатель потери начинается с правильного значения. Например, если вы правильно инициализирует свой конечный слой, то у вас должно получиться -log(1 / n_classes) для функции softmax при инициализации. Те же значения по умолчанию можно получить для регрессии L2, потерь Губера и тому подобное.

  • инициализируйте верно. Правильно инициализируйте веса конечного слоя. Например, если вы регрессируете некоторые значения, которые имеют среднее значение 50, тогда инициализируйте окончательное смещение к 50. Если у вас несбалансированный набор данных с соотношением 1:10, установите смещение на своих логитах так, чтобы ваша сеть давала предсказания 0.1 при инициализации. Правильная их установка ускорит сходимость и устранит кривые потерь в виде "хоккейной клюшки", где в первые несколько итераций ваша сеть в основном лишь изучает смещения.

  • человеческий базис. Отслеживайте и другие показатели, кроме потерь, которые можно интерпретировать и проверить человеком (например, точность). По возможности оценивайте собственную (человеческую) точность и сравнивайте с ней. Кроме того, дважды аннотируйте тестовые данные и для каждого примера рассмотрите одну аннотацию как предсказания, а вторую как основную правду.

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

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

  • проверяйте уменьшения потерь на тренировочной выборке. Думаю, ваш набор данных будет не очень объемным, так как вы работаете с игрушечной моделью. Попробуйте немного увеличить его объем. Значение потерь на тренировочной выборке снизились как следует?

  • визуализируйте непосредственно перед входом нейросети. Однозначно правильное место для визуализации ваших данных находится непосредственно перед вашим y_hat = model (x) (или sess.run в Tensorflow). То есть - вы должны визуализировать именно то, что попадает в вашу сеть, декодируя этот необработанный тензор данных и меток в виде какой-то визуализации. Это единственный "источник истины". Я не могу сосчитать, сколько раз это меня спасало и проявляло проблемы с предварительной обработкой и аугментацией данных.

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

  • используйте метод обратного распространения ошибки для отслеживания зависимостей. Ваш код для глубокого обучения часто может содержать сложные, векторизованные и трансляционные операции. Достаточно распространенная ошибка, с которой я сталкивался несколько раз, заключается в том, что люди достигают этого неправильно (например, они используют view, а не transpose / permute) и нечаянно смешивают информацию в измерении размера пакета. Удручает тот факт, что ваша сеть, как правило, все равно способна хорошо учиться, потому что она научится игнорировать данные из других примеров. Одним из способов налаживания этой (и других связанных с этим проблем) является установление функции потери как чего-то тривиального, такого как сумма всех выходов примера i, запуск обратного прохода до входного сигнала и обеспечения получения ненулевого градиента только на i-м входе. Ту же стратегию можно использовать, чтобы убедиться, что ваша авторегресивная модель в момент времени t зависит только от 1..t-1. В общем, градиенты дают вам информацию о том, что и от чего зависит в вашей сети, это может быть полезно для отладки.

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

3. Переобучайте

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

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

Несколько советов и подсказок на этом этапе:

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

  • Adam (метод адаптивной оценки моментов) безопасен. На ранних стадиях установления базиса мне нравится использовать Adam со скоростью обучения 3e-4. По моему опыту, Adam гораздо лояльнее к гиперпараметрам, включая плохую скорость обучения. Для сверточных нейросетей хорошо настроенный метод стохастического градиента (SGD) почти всегда немного превосходит Adam, но область оптимальной скорости обучения гораздо более узкая и зависит от задачи. (Примечание. Если вы используете рекуррентные нейросети и связанные с ними модели обработки последовательностей, то чаще используют Adam. Опять же, на начальном этапе своего проекта не будьте героем и соблюдайте самые популярные статьи.)

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

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

4. Регуляризируйте

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

  • получите больше данных. Во-первых, безусловно лучшим способом регуляризирования модели в любом практической среде является добавление большего количества реальных учебных данных. Очень распространенной ошибкой является проведение многих инженерных циклов, пытаясь выжать сок из небольшого набора данных, когда вместо этого можно было собирать больше данных. Насколько мне известно, добавление дополнительных данных является едва ли не единственным гарантированным способом монотонно улучшать производительность хорошо настроенной нейронной сети почти неограниченно долго. Остальные - это ансамбли нейросетей (если вы можете себе позволить), но это ограничивается ~ 5-ю моделями.

  • аугментация данных. Следующим лучшим способом после реальных данных является полу фальшивые данные - попробуйте более агрессивную аугментацию данных.

  • креативная аугментация. Если полу фальшивые данные не помогли, фейковые данные также могут что-то сделать. Люди находят творческие способы расширения наборов данных; Например, рандомизация доменов, использование моделирования, умные гибриды, такие как вставка (потенциально смоделированная) данных у сцены или даже GAN.

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

  • придерживайтесь контролируемого обучения (обучение с учителем). Не переоценивайте предварительное обучение без присмотра (без учителя). В отличие от того, что рассказывается в той заметке в блоге от 2008 года [не могу понять о каком сообщении тут идет речь], насколько мне известно, нет версий, которые показывают хорошие результаты на современных задачах компьютерного зрения (хотя NLP, кажется, вполне хорошо справляется вместе с BERT и компанией сегодня, вполне вероятно благодаря умышленному характеру текста и высшему соотношению сигнал / шум).

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

  • уменьшайте размер модели. Во многих случаях вы можете использовать ограничения информативности участка в сети, чтобы уменьшить ее размер. В качестве примера, раньше было модно использовать слои с полным соединением поверх основы из ImageNet, но с тех пор они были заменены простым средним объединением (average pooling), устраняя тонну параметров в процессе.

  • уменьшайте размер партии. Через нормализацию внутри нормы партии меньшие размеры партии несколько соответствуют сильной регуляризации. Это связано с тем, что эмпирическое среднее / стандартное распределение для партии является более приблизительной версией полного среднего / стандартное распределение, поэтому изменение масштаба и смещения "раскачивают" вашу партию больше.

  • отсеивайте. Добавьте отсеивания. Используйте dropout2d (пространственное отсеивания) для сверточных сетей. Используйте это умеренно / осторожно, поскольку, кажется, отсеивания нехорошо работает при нормализации партии.

  • уменьшение веса. Увеличьте коэффициент уменьшения веса (эффект забывания).

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

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

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

5. Тюнингуйте

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

  • случайный поиск по сетке. Для одновременной настройки нескольких гиперпараметров может показаться соблазнительным использовать поиск по сетке, чтобы обеспечить охват всех настроек, но имейте в виду, что лучше вместо этого использовать случайный поиск. Интуитивно это связано с тем, что нейронные сети часто гораздо более чувствительны к одним параметрам, чем к другим. В общем, если параметр a важен, но изменение b не имеет эффекта, вы подбираете значение a более продуманно, чем в нескольких фиксированных точках несколько раз.

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

6. Выжмите все соки

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

  • ансамбли. Ансамбли моделей - это почти гарантированный способ получить 2% точности на чем-либо. Если вы не можете позволить себе вычисления во время тестирования, посмотрите на перегонку своего ансамбля в сеть, используя темные знания.

  • оставьте ее тренироваться. Я часто видел людей, которые соблазняются прекратить обучение моделей, когда потеря валидации, кажется, выравнивается. По моему опыту, сети продолжают тренироваться не интуитивно долго. Однажды я случайно покинул тренировку модели во время зимних каникул, и когда вернулся в январе, я увидел результат SOTA (state of the art - "современный уровень").

Вывод

Как только вы дойдете сюда, у вас будут все составляющие успеха: Вы глубоко понимаете технологию, набор данных и проблему, вы создали всю инфраструктуру обучения / оценки и достигли высокой уверенности в ее точности, вы исследовали все более сложные модели, получая улучшения производительности способами, которые вы предугадывали на каждом шагу. Теперь вы готовы прочитать много работ, попробовать большое количество экспериментов и получить свои результаты SOTA. Удачи!

Подробнее..

Перевод Переосмысление предобучения и самообучения

21.04.2021 18:22:57 | Автор: admin

Перевод статьи подготовлен в преддверии старта курса "Deep Learning. Basic".

Предлагаем также всем желающим посмотреть запись вебинара
Knowledge distillation: нейросети обучают нейросети.


В конце 2018 года исследователи из FAIR опубликовали статью Переосмысление предобучения в ImageNet, которая впоследствии была представлена на ICCV2019. В статье представлены некоторые очень интересные выводы относительно предобучения. Я тогда не стал посвящать этому событию отдельный пост, но мы долго обсуждали его в нашем слаке (KaggleNoobs). Исследователи из Google Research and Brain team предложили расширенную версию той же концепции. Их новая публикация затрагивает не только тему предобучения (pre-training), она также исследует самообучение (self-training), сравнивая его с предобучением и обучением без учителя (self-supervised learning) на тех же наборах задач.

Введение

Прежде чем мы углубимся в детали, представленные в публикации, давайте сделаем один шаг назад и обсудим сначала несколько понятий. Предобучение очень распространенная практика в различных областях, таких как компьютерное зрение, NLP и генерация речи. Когда речь заходит о компьютерном зрении, мы ожидаем, что модель, предварительно обученная на одном наборе данных, поможет другой модели. Например, предобучение ImageNet с учителем является широко используемым методом инициализации для моделей обнаружения и сегментации объектов. Трансферное обучение (transfer learning) и точная настройка (fine-tuning) два распространенных метода для реализации этой затеи.

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

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

Что ж, хватит болтовни! Мудреные определения в сторону, ведь вы до сих пор не понимаете, о чем конкретно эта статья? Или мы собрались здесь, чтобы определения почитать?

Мотивация

Мы используем эти методы уже достаточно давно. Авторы заинтересованы в том, чтобы найти ответы на следующие вопросы:

  1. Насколько может помочь предобучение? Когда предобучение не приносит пользы?

  2. Можем ли мы использовать самообучение вместо предобучения и получить аналогичные или лучшие результаты по сравнению с предобучением и обучением без учителя?

  3. Если самообучение превосходит предобучение (если предположить, что это так), то насколько оно лучше, чем предобучение?

  4. В каких случаях самообучение лучше предобучения?

  5. Насколько самообучение гибкое и масштабируемое?

Сетап

Наборы данных и модели

  1. Обнаружение объектов: авторы использовали набор данных COCO (118K изображений) для обнаружения объектов с применением обучения с учителем. ImageNet (1,2М изображений) и OpenImages (1,7М изображений) использовались в качестве немаркированных наборов данных. Были использован детектор RetinaNet с EfficientNet-B7 в качестве базовой сети (backbone). Разрешение изображений было до 640 x 640, слои пирамиды от P3-P7 и использовались 9 якорей на пиксель.

  2. Семантическая сегментация: для обучения с учителем использовался набор для обучения сегментации PASCAL VOC 2012 (1,5K изображений). Для самообучения авторы использовали аугментированный набор данных PASCAL (9K изображений), COCO (240K размеченных, а также неразмеченных изображений) и ImageNet (1,2M изображений). Использовалась модель NAS-FPN с EfficientNet-B7 и EfficientNet-L2 в качестве базовых сетей.

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

Аугментация данных

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

  1. Augment-S1: это стандартная Flip and Crop (переворот и кадрирование) аугментация. Стандартный метод flip and crop состоит из горизонтальных переворотов изображения и флуктуаций масштаба (scale jittering). Операция флуктации также может быть случайной, так же как то как мы изменяем размер изображения до (0.8, 1.2) от размера исходного изображения, а затем обрезаем его.

  2. Augment-S2: состоит из AutoAugment и переворотов и кадрирований.

  3. Augment-S3: включает сильную флуктуацию масштаба, AutoAugment, перевороты и кадрирование. Флуктуация масштаба увеличен до (0.5, 2.0).

  4. Augment-S4: комбинация RandAugment, переворотов и кадрирования и сильной флуктуацией масштаба. Флуктуация масштаба такая же, как и в Augment-S2/S3.

Предварительное обучение

Для изучения эффективности предобучения использовались предварительно обученные контрольные точки (checkpoints) ImageNet. EfficientNet-B7 архитектура, используемая для оценки. Для этой модели использовались две разные контрольные точки. Они обозначаются как:

  1. ImageNet: контрольная точка EfficientNet-B7, обученная с помощью AutoAugment, которая достигает 84,5% top-1 точности в ImageNet.

  2. ImageNet++: контрольная точка EfficientNet-B7, обученная с помощью метода Noisy Student, который использует дополнительные 300 млн неразмеченных изображений и обеспечивает 86,9% top-1 точности.

Обучение из случайной инициализации обозначается как Rand Init.

Самообучение

Реализация самообучения основана на алгоритме Noisy Student и состоит из трех этапов:

  1. Модель-учитель обучается на размеченных данных, например, на наборе данных COCO.

  2. Затем модель-учитель используется для создания псевдометок для неразмеченных данных, например ImageNet.

  3. Модель-ученик обучается оптимизировать потери на человеческой разметке и псевдо-метках одновременно.

Ради бога, можем ли мы уже взглянуть на хоть какие-нибудь эксперименты?

Эксперименты

Влияние увеличения и размера размеченного набора данных на предобучение

Авторы использовали ImageNet для предварительного обучения с учителем и варьировали размер размеченного набора данных COCO для изучения эффекта предобучения. Разнился не только размер размеченных данных, но и аугментации разной силы для обучения RetinaNet с EfficientNet-B7 в качестве базовой сети. Авторы наблюдали следующие факты:

  1. Предобучение ухудшает эффективность, когда используется более сильная аугментация данных: авторы заметили, что, когда они используют стандартную аугментацию, Augment-S1, как она описана выше, предобучение помогает. Но по мере увеличения силы аугментации, предобучение помогает все меньше и меньше. Даже больше, они заметили, что при использовании сильнейшей аугментации (Augment-S3) предварительная тренировка на самом деле сильно ухудшает эффективность.

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

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

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

Влияние увеличения и размера размеченного набора данных на самообучение

Теперь, когда мы увидели влияние предварительного обучения, пришло время проверить результаты с той же задачей (в данном случае обнаружение объекта COCO) с той же моделью (RetinaNet детектор с базой EfficientNet-B7), но на этот раз с самообучением. Авторы использовали для самообучения набор данных ImageNet (метки для ImageNet в этом случае отбрасываются). Авторы отметили следующее:

  1. Самообучение хорошо помогает в режимах с большим объемом данных/сильной аугментацией, даже когда скорее предобучение мешает: авторы обнаружили, что при добавлении самообучения к случайно инициализированной модели, при использовании тяжелой аугментации, это не только повышает исходные результаты, но также превосходят результаты, достигаемые с предобучением. Вот результаты:

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

Погодите-ка секундочку! Когда используется ImageNet++ init, выигрыш невелик по сравнению с выигрышем в Rand init и ImageNet init. Есть какая-то конкретная причина?

Да, ImageNet++ init получается из контрольной точки, для которой использовались дополнительные 300M неразмеченных изображений.

Предварительное обучение с учителем против самообучения

Мы увидели, что предобучение ImageNet с учителем снижает эффективность в режиме с максимальным объемом данных и в режиме сильной аугментации данных. Но как насчет предобучения без учителя? Основная цель самостоятельного обучения, предобучения без меток, это создание универсальных представлений, которые можно перенести на более широкий круг задач и наборов данных.

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

Не хочется вас разочаровывать, но ответ НЕТ. Чтобы исследовать эффекты обучения без учителя, авторы использовали полный набор данных COCO и сильнейшие аугментации. Цель состояла в том, чтобы сравнить случайную инициализацию с моделью, предварительно обученной с помощью современного алгоритма с обучения без учителя. Контрольная точка для SimCLR в этом эксперименте использовалась до того, как она была тонко настроена (fine-tuned) в ImageNet. Поскольку SimCLR использует только ResNet50, основа детектора RetinaNet была заменена на ResNet50. Вот результаты:

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

Что мы узнали?

Предобучение и универсальные представления признаков

Мы увидели, что предварительное обучение (как контролируемое, так и самостоятельное) не всегда приводит к повышению эффективности. Более того, оно всегда уступает самообучению. Почему так происходит? Почему предобучение ImageNet не так хорошо для обнаружения объектов COCO? Почему представления, полученные с помощью предобучения без учителя, не помогли повысить эффективность?

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

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

Совместное обучение

Как указали авторы, одна из сильных сторон парадигмы самообучения состоит в том, что оно совместно обучает цели с учителем и цели самообучения, тем самым устраняя несоответствие между ними. Мы всегда можем утверждать, что вместо того, чтобы искать какой-либо другой метод устранения несоответствия различий между задачами, почему мы не можем обучаться вместе, например, совместно обучая ImageNet и COCO?

Авторы использовали тот же сетап, что и при самообучении для этого эксперимента, и обнаружили, что предобучение ImageNet дает улучшение +2,6AP, но использование случайной инициализации и совместного обучения дает больший выигрыш +2,9AP. Более того, предобучение, совместная тренировка и самообучение все могут работать вместе. Используя тот же источник данных ImageNet, предобучение ImageNet получает улучшение +2,6AP, предобучение + совместное обучение дает +0,7AP, а комбинация предобучения + совместного обучение + самообучение дает улучшение +3,3AP.

Важность согласования задач

Как мы видели выше, согласование задач важно для повышения производительности. В этой статье сообщалось о схожих выводах о том, что предобучение на Open Images снижает эффективность COCO, несмотря на то, что оба они помечены ограничивающими рамками. Это означает, что мы не только хотим, чтобы задача была такой же, но и чтобы аннотации были одинаковыми, чтобы предварительное обучение было действительно полезным. Авторы отметили еще две интересные вещи:

  1. Предварительное обучение ImageNet, даже с дополнительными человеческими метками, работает хуже, чем самообучение.

  2. При сильной аугментации данных (Augment-S4) обучение с помощью PASCAL (наборы данных для обучения + аугментация) на самом деле снижает точность. Между тем, псевдометки, созданные путем самообучения на одном и том же наборе данных, повышают точность.

Масштабируемость, универсальность и гибкость самообучения

Из всех экспериментов, проведенных авторами, мы можем сделать вывод, что:

  1. Что касается гибкости, самообучение хорошо работает в любой конфигурации: режим с низким объемом данных, режим с высоким объемом данных, слабая аугментация данных и сильная аугментация данных.

  2. Самообучение не зависит от архитектуры или набора данных. Он хорошо работает с различными архитектурами, такими как ResNets, EfficientNets, SpineNet и т. д., а также с различными наборами данных, такими как ImageNet, COCO, PASCAL и т. д.

  3. В общем, самообучение хорошо работает, когда предобучение терпит неудачу, но также и когда предобучение обучение тоже работает хорошо.

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

Это хорошо. Некоторые из перечисленных здесь пунктов вызывают множество вопросов о том, как все мы использовали предварительное обучение. Но все, что имеет плюсы, имеет и минусы. Вы, должно быть, скрываете какой-то важный момент?

Ограничения самообучения

Хотя у самообучение есть свои преимущества, у него также есть несколько ограничений.

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

  2. Ускорение от предобучения варьируется от 1,3x до 8x, в зависимости от качества предобученной модели, силы аугментации данных и размера набора данных.

  3. Самообучение не является полной заменой трансферного обучения и тонкой настройки. Оба эти метода также будут активно использоваться в будущем.

Заключение

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


Узнать подробнее о курсе "Deep Learning. Basic".

Смотреть запись вебинара Knowledge distillation: нейросети обучают нейросети.

Подробнее..

Перевод Глубокие нейронные деревья принятия решений

20.01.2021 00:15:59 | Автор: admin

Описание

Глубокие нейронные сети доказали свою эффективность при обработке данныхот органов чувств, таких,как изображения и аудио. Однако для табличных данных более популярны древовидные модели. Хорошим свойством древовидных моделей является их естественная интерпретируемость. В этой работе мы представляемDeepNeuralDecisionTrees(DNDT)древовидныемодели, реализованные нейронными сетями. DNDT внутренне интерпретируем, так как это дерево. Тем не менее, поскольку это также нейронная сеть (NN), ее можно легко реализоватьс помощьюинструментарияNN и обучитьпо алгоритмуградиентногоспуска, а непо жадному алгоритму (алгоритмужадного разбиения). Мы проводим оценку DNDT на нескольких табличных наборах данных, проверяем его эффективность и исследуем сходства и различия между DNDT и обычными деревьями решений. Интересно, что DNDT самообучаетсякак на разделенном, так и на функциональном уровне.

  1. Введение

Интерпретируемость прогностических моделей важна, особенно в тех случаях, когда речь идет об этикеправовой, медицинскойи финансовой,критически важных приложениях, где мы хотим вручную проверитьрелевантностьмодели. Глубокие нейронные сети (Lecunetal., 2015 [18];Schmidhuber, 2015 [25]) достигли превосходных результатов во многих областях, таких как компьютерное зрение, обработка речи и языковое моделирование. Однако отсутствие интерпретируемости не позволяет использоватьв приложенияхэто семейство моделейкак черныйящик, для которогомы должны знатьпроцедурупрогноза, чтобыверифицироватьпроцесс принятия решения. Более того, в некоторых областях, таких как бизнес-аналитика (BI), часто более важно знать, как каждый фактор влияет на прогноз, а не сам вывод. Методы, основанные на дереве решений (DT), такие как C4.5 (Quinlan, 1993 [23]) и CART (Breimanetal., 1984 [5]), имеют явное преимущество в этом аспекте, поскольку можно легко проследить структуру дерева и точно проверить, как делается прогноз.

В этой работе мы предлагаем новую модель на пересечении этих двух подходов глубокое нейронное дерево решений (DNDT),исследуем егосвязи с каждым из них. DNDT- это нейронные сети со специальной архитектурой, где любой выборвесов DNDT соответствует определенному дереву решений и поэтому интерпретируем. Однако, поскольку DNDT реализуется нейронной сетью (NN), она наследует несколько интересных свойств, отличныхоттрадиционныхDT: DNDT может быть легкореализованнесколькимистрокамикода в любом программном фреймворке NN; все параметры одновременно оптимизируются с помощью стохастического градиентного спуска, а не более сложной и потенциально неоптимальной процедурыжадногорасщепления.DNDTготов к крупномасштабной обработке с обучением на основе мини-патчейи ускорением GPUот коробочного решения, его можно подключить к любой более крупной модели NN в качестве строительного блока для сквозного обучения с обратным распространением (back-propagation).

2.Похожие работы

Модели на основе деревьев решений.Древовидные модели широко используются в обучении под наблюдением, например, взадачахклассификации. Они рекурсивно разбивают входное пространство и присваивают метку/оценку конечному узлу. Хорошо известные древовидные моделииспользуютC4. 5 (Quinlan, 1993 [23]) и CART (Breimanetal., 1984 [5]). Ключевым преимуществом древовидных моделей является то, что они легко интерпретируются, поскольку предсказания задаются набором правил. Также часто используется ансамбль из нескольких деревьев, таких какслучайный лес(Breiman, 2001 [6]) иXGBoost(Chen&Guestrin, 2016 [8]), чтобы повысить производительность за счет интерпретируемости. Такие древовидные модели часто конкурируют или превосходят нейронные сети в задачах прогнозирования с использованием табличных данных.

Интерпретируемые модели. По мере того как предсказания, основанные на машинном обучении,используютсяповсеместнои затрагивают многие аспекты нашей повседневной жизни, фокус исследований смещается от производительности модели (например, эффективности и точности) к другим факторам, таким как интерпретируемость (Weller, 2017 [26];Doshi-Velez, 2017 [11]). Это особеннонеобходимов приложениях, где существуютпроблемыэтические (Bostrom&Yudkowsky, 2014 [4]) или безопасности, и предсказания моделей должны быть объяснимы, чтобы проверить правильность процесса рассуждения или обосновать решениядля них. В настоящее время предпринимается ряд попыток сделать моделиобъяснимыми.Некоторые из них являются модельно-агностическими (Ribeiroetal., 2016 [24]), в то время как большинство из них связаны с определенным типом модели, например, классификаторамина основе правил (Dashetal., 2015 [10];Malioutovetal., 2017 [19]), моделями ближайших соседей (Kimetal., 2016 [15]) и нейроннымисетями (Kimetal., 2017 [16]).

Нейронные сети и деревья решений. В некоторых исследованиях предлагалось унифицировать модели нейронной сети и деревьев решений.Bul&Kontschieder(2014) [7] предложили нейронныелеса решений( Neural Decision Forests NDF) как ансамбль нейронных деревьев решений, где расщепленные функции реализуютсяслучайнымимногослойными персептронами.Deep-NDF (Kontschiederetal., 2015 [17]) использовал стохастическую и дифференцируемую модель дерева решений, которая совместно изучает представления (черезCNNs) и классификацию (через деревья решений). Предлагаемый нами DNDT во многом отличается от этих методов. Во-первых, у нас нет альтернативной процедуры оптимизации для изучения структуры (разделения) и обучения параметров (матрица оценок). Вместо этого мы изучаем их все с помощьюоднопроходногообратного распространения (back propagation). Во-вторых, мы не ограничиваем разбиение двоичным (левым или правым), поскольку мы применяем дифференцируемую функцию разбиения, которая может разбивать узлы на несколько ( 2) листьев. Наконец, что наиболее важно, мы разрабатываем нашу модель специально для интерпретируемости, особенно для приложенийк табличным данным, где мы можем интерпретировать каждую входную функцию. Напротив, модели в (Bul&Kontschieder, 2014 [7];Kontschiederetal., 2015 [17]) предназначены для прогнозирования и применяются к необработанным данным изображения. Некоторые проектные решения делают их непригодными для табличных данных. Например, вKontschiederetal. (2015 [17]), они используют менее гибкое дерево, в котором структуражесткофиксируется, пока изучается разбиение узла.

Несмотря на похожее название, наша работа кардинально отличается от работыБалестриеро(2017 [2]), которая разработала своего рода наклонное дерево решений, реализованное нейронной сетью. В отличие от обычных одномерных деревьев решений, каждый узел внаклонномдереве решений включает в себя все функции, а не одну функцию, что делает модель не интерпретируемой.

Альтернативныеактиваторыдереварешений.ОбычныеDT изучаются рекурсивнымжаднымрасщеплением признаков (Quinlan, 1993;Breimanetal., 1984 [23]). Это эффективно и имеет некоторые преимущества для выбора признаков, однако такойжадныйпоиск может быть неоптимальным (Norouzietal., 2015 [20]). В некоторых недавних работах исследуются альтернативные подходы к обучению деревьев решений, которые направлены на достижение лучшей производительности при менеетребовательнойоптимизации, например с помощью латентного структурированного прогнозирования переменных (Norouzietal., 2015 [20]) или обучения контроллера расщепления RNN с использованием обучения с подкреплением (Xiongetal., 2017 [28]). Напротив,нашDNDT намного проще, чемуказанные, но все же потенциально может найти лучшие решения, чем обычные индукторы DT,содновременным поискомструктурыи параметровдерева с SGD. Наконец, также отмечаем, что в то время как обычные активаторы DT используют только двоичные расщепления(для простоты), наша модель DNDT может одинаково легко работать с расщеплениями произвольной мощности, что иногда может привести к более интерпретируемым деревьям.

3.Методология

3.1.Функция мягкого контейнера

Основной модуль, который мы здесь реализуем, - это функция мягкой ячейки(Doughertyetal., 1995)или объединения множества точечных объектов в динамические полигоны (бины),которуюмы будем использовать для принятия решений о разделении в DNDT. Как правило, функциябиннингапринимает в качестве входных данныхвещественныйскаляр x и выдает индексячейки, которойонпринадлежит.Жесткоеразделение по ячейкамнедифференцируемое, поэтому мы предлагаем дифференцируемую аппроксимацию этой функции.

Предположим, что у нас есть непрерывная переменная x, которую мы хотим разбить на N + 1 интервалов. Это приводит к необходимости n точек среза, которые в данном контексте являются обучаемыми переменными. Обозначим точки среза [1, 2,, n]какмонотонно возрастающие, то есть 1 < 2 < < n. Во времяобученияпорядок может быть перетасован после обновления, поэтому мы должны сначала сортировать их в каждом прямом проходе. Однако это не повлияет на дифференцируемость, потому что сортировка просто меняет местами позиции .

Теперь построим однослойную нейронную сеть с функцией активацииsoftmax.

Здесь w-это константа, а не обучаемая переменная, и ее значение задается как w = [1; 2;:: : ; n + 1]. b строится как,

> 0 - фактор напряженности. При 0 выход стремится ктекущемувектору.

Мы можем проверить это, проверив три последовательныхлогита

o_{i-1},o_i, o_{i+1}.

Когда у нас есть как

o_i> o_{i-1} (при \quad x > _i), так \quad и \quad o_i> o_{i+1} (при \quad x < _{i+1}),

x должен попасть в интервал

(_i, _{i+1}).

Таким образом, нейронная сеть в уравнении 1 будет производить почти однократноегорячеекодированиеячейкиx, особенно при более низкой напряженности. При желании,мы можем применить трюкотжига наклона(Chungetal., 2017 [9]), который постепенно снижает напряжение приобучении, чтобы,в конце концов,получить более детерминированную модель.

Если кто-то предпочитает фактическийгорячий (текущийкодируемый)вектор, можно применитьStraight-Through(ST)Gumbel-Softmax(Jangetal., 2017): для прямого прохода мысэмплируемоднократный вектор, используя хитрость с Gumbel-Max, тогда как для обратного прохода (backward pass) мы используемGumbel-Softmaxпривычисленииградиента (см.Bengio(2013 [3]) для более подробного анализа.

На рис.1 показан конкретный пример, где мы имеем скаляр x в диапазоне [0, 1] и две точки среза в 0.33 и 0.66 соответственно. Основываясь на уравнениях1 и 2, мы имеем трилогитаo1 = x, o2 = 2x 0.33, o3 = 3x 0.99.

Рисунок 1. Конкретный пример нашей функции мягкого биннинга с использованием точек среза в 0.33 и 0.66. Ось x - это значение непрерывной входной переменной x2 [0; 1]. Вверху слева: исходные значения логитов; вверху справа: значения после применения функции softmax с т = 1; Внизу слева: т= 0.1; внизу справа: т = 0.01.Рисунок 1. Конкретный пример нашей функции мягкого биннинга с использованием точек среза в 0.33 и 0.66. Ось x - это значение непрерывной входной переменной x2 [0; 1]. Вверху слева: исходные значения логитов; вверху справа: значения после применения функции softmax с т = 1; Внизу слева: т= 0.1; внизу справа: т = 0.01.

3.2Построениепрогнозов

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

x \in R^D \, c \, функциями \,D

Связывая каждый признакxd со своей собственной нейронной сетьюfd(xd), мы можем исчерпывающе найти все конечные узлы с помощью,

Здесь z теперь также является почтигорячимвектором, который указывает индекс листового узла, куда поступает экземпляр x. Наконец, мы предполагаем, что линейный классификатор на каждом листе z классифицирует поступающие туда экземпляры. DNDT проиллюстрирован на Рис. 2.

Рисунок 2. Изученный DNDT для набора данных Iris (сокращенная версия с двумя функциями). Вверху: DNDT - показано, где красным шрифтом указаны обучаемые переменные, а черным константы. Внизу: DT визуализация той же сети, что и обычное дерево решений. Дроби указывают маршрут случайно выбранных 6 классифицируемых экземпляров.Рисунок 2. Изученный DNDT для набора данных Iris (сокращенная версия с двумя функциями). Вверху: DNDT - показано, где красным шрифтом указаны обучаемые переменные, а черным константы. Внизу: DT визуализация той же сети, что и обычное дерево решений. Дроби указывают маршрут случайно выбранных 6 классифицируемых экземпляров.

3.3Обучение дерева

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

Обсуждение. DNDT хорошо масштабируется с количеством экземпляров благодаря мини-пакетному обучению в стиле нейронной сети. Однако ключевым недостатком этогостилядо сих пор является то, что из-за использования продуктаKroneckerон не масштабируется по количеству функций. В нашей текущей реализации мы избегаем этой проблемы с "широкими" наборами данных, обучаялесслучайногоподпространства(Ho, 1998 [13]) - за счет нашей интерпретируемости. То есть вводится несколько деревьев, каждое из которых обучается на случайном подмножестве признаков. Лучшим решением, которое не требует неинтерпретируемоголеса, является использование разреженности конечногоразделения на ячейкиво время обучения: количество непустых листьев растет намного медленнее, чем общее количество листьев. Но это несколько усложняет простую в остальном реализацию DNDT.

4.Эксперименты

4.1Реализация

DNDT концептуально прост и легок в реализации с помощью 20 строк кода вTensorFlowилиPyTorch. Поскольку он реализован как нейронная сеть, DNDT поддерживает "из коробки" ускорение GPU и мини-пакетное обучение наборов данных, которые не помещаются в память, благодаря современным фреймворкам глубокого обучения.

4.2Наборы данных и конкуренты

Мы сравниваем DNDT с нейронными сетями (реализованнымиTensorFlow(Abadietal., 2015) [1]) и деревом решений (отScikit-learn(Pedregosaetal., 2011 [22])) на 14 наборах данных, собранных изKaggleи UCI (подробности набора данных см. В табл. 1).

Для базовой линии дерева решений (DT) мы установили два ключевых критериягиперпараметров: критерий'gini' иразделитель 'best'. Для нейронной сети (NN) мы используем архитектуру из двух скрытых слоев по 50 нейронов в каждом для всех наборов данных. DNDT также имеетгиперпараметр-количество точек среза для каждого объекта (коэффициент ветвления), который мы устанавливаемравным1 для всех объектов и наборов данных. Подробный анализ эффекта этогогиперпараметраможно найти в разделе 4.4.Длянаборовданных с более чем 12 признаками,мы используем ансамбль DNDT, где каждое дерево выбирает 10 признаков случайным образом, и у нас есть 10уровнейв общей сложности. Окончательный прогноз дается большинством голосов.

4.3Точность

Мы оцениваем производительность DNDT, дерева решений инейросетевыхмоделей на каждом из наборов данных в Табл. 1. точность тестового набора представлена в Табл.2.

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

Таблица 1. Коллекция из 14 наборов данных от Kaggle (обозначается буквой (K)) и UCI: количество экземпляров (#inst.), количество объектов (#feat.) и количество классов (#cl.)Таблица 1. Коллекция из 14 наборов данных от Kaggle (обозначается буквой (K)) и UCI: количество экземпляров (#inst.), количество объектов (#feat.) и количество классов (#cl.)Таблица 2. Точность тестового набора каждой модели: DT: дерево решений. NN: нейронная сеть. DNDT: наше глубокое нейронное дерево решений, где ( * ) указывает, что используется ансамблевая версия.Таблица 2. Точность тестового набора каждой модели: DT: дерево решений. NN: нейронная сеть. DNDT: наше глубокое нейронное дерево решений, где ( * ) указывает, что используется ансамблевая версия.

Условно говоря, нейронные сети не имеют явного преимущества в отношении такого рода данных. Однако DNDT немного лучше, чемванильнаянейронная сеть, так как она ближе к дереву решений попостроению. Конечно, это только ориентировочный результат, так как все эти модели имеют настраиваемыегиперпараметры. Тем не менее интересно, что ни одна модель не обладает доминирующим преимуществом. Это напоминает теоремы об отсутствиибесплатного обеда(Wolpert, 1996[27]).

4.4Анализ активных точек среза

В DNDT количество точек среза на объект является параметром сложности модели. Мы не связываем значения точек среза, а это значит, что некоторые из них неактивны, например, они либо меньше минимальногоxd, либо больше максимальногоxd.

В этом разделе мы исследуем, сколько точек среза фактически используется после обучения DNDT. Точка среза активна, когда по крайней мере один экземпляр из набора данных попадает на каждую ее сторону. Для четырех наборов данных-CarEvaluation,Pima,IrisиHabermanмы устанавливаем количество точек среза на объект от 1 до 5 и вычисляем процент активных точек среза, как показано на рис. 3.Видно, что по мере увеличения числа точек среза их использование в целом уменьшается. Это означает, что DNDT несколько саморегулируется: он не использует все доступные ему параметры.

Рисунок 3. Доля (%) активных точек, используемых DNDT.Рисунок 3. Доля (%) активных точек, используемых DNDT.

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

Рисунок 4. Точность тестирования DNDT для увеличения числа точек разреза (сложность модели).Рисунок 4. Точность тестирования DNDT для увеличения числа точек разреза (сложность модели).

4.5Анализ активных признаков

При обучении DNDT также возможно, что для определенного объекта все точки среза неактивны. Это соответствует отключению функции, чтобы она не влияла на прогнозирование,аналогично обычному ученику DT, который никогда не выбирает данную функцию, чтобызадать узел-распознавательв любом месте дерева. В этом разделе мы анализируем, как DNDT исключает функции таким образом. Мы запускаем DNDT 10 раз и записываем,сколькораз данный объект исключаетсяиз-за того,что все его точки среза неактивны.

Учитывая случайностьчерезинициализируемыевесадлямини-пакетнойвыборки, мы наблюдаем, что некоторые функции (например, функция индекса 0 вiris) последовательно игнорируются DNDT (см. табл. 3 для всех результатов). Это говорит о том, что DNDT делает некоторый неявныйвыбор объектов, выталкивая точки отсечениязаграницы данных для несущественных объектов. В качестве побочного продукта мы можем получить меру важности(веса)функции изнаборафункции в течение нескольких запусков: чем больше функция игнорируется, тем менее важной она, вероятно,ибудет.

Таблица 3. Процент ( % ) случаев, когда DNDT игнорирует каждую функциюТаблица 3. Процент ( % ) случаев, когда DNDT игнорирует каждую функцию

4.6Сравнение с деревом решений

Используя методы, разработанные в разделе 4.5, мы исследуем, благоприятствуют ли DNDT и DTсосходнымихарактеристиками. Мы сравниваем важность признака черезкритерийgini (Джини), используемыйв дереве решений (Рис. 5), с нашей метрикой скорости отбора (табл.3).

Рисунок 5. Рейтинг важности характеристик, произведенный DT (Gini).Рисунок 5. Рейтинг важности характеристик, произведенный DT (Gini).

Сравнивая эти результаты, мы видим, что иногда DNDT и DT предпочитаютвыбор признаков, например, дляIrisони оба оценивают Признак 3 как наиболее важный. Но бывает, что они также могут иметь разные взгляды, например, дляХаберманаDT выбрал функцию 0 как наиболее важную, тогда как DNDT полностью проигнорировал ее. На самом деле DNDT использует только функцию 2 для прогнозирования, которая занимает второе место поDT.Однако такого рода разногласия не обязательно могут привести к существенным различиям в производительности. Как видно из Табл. 2, дляХаберманаточность испытаний DNDT и DT составляет 70,9% и 66,1% соответственно.

Наконец, мы количественно оцениваем сходстворанжированийпризнаков DNDT и признаков DT, вычисляяTauкритерияКендаллаподвумрейтинговымспискам. Результаты, приведенные в Табл.4, свидетельствуют об умеренной корреляции в целом.

Таблица 4. Рейтинг функций DNDT и DT по Кендаллу: большие значения означают большее сходство.Таблица 4. Рейтинг функций DNDT и DT по Кендаллу: большие значения означают большее сходство.

4.7Ускорение GPU

Наконец, мы проверяем легкость ускорения обучения DNDT с помощью обработки графическим процессором - возможность, которая не является обычной или простойдля DT. Увеличивая количество точек отсечения для каждой функции, мы можем получить более крупные модели, для которых режим графического процессора имеет значительно меньшее время работы (см. Рис. 6).

Рисунок 6. Иллюстрация ускорения GPU: время обучения DNDT включено. 3,6 ГГц CPU против GTX Titan GPU. В среднем за 5 прогонов.Рисунок 6. Иллюстрация ускорения GPU: время обучения DNDT включено. 3,6 ГГц CPU против GTX Titan GPU. В среднем за 5 прогонов.

5.Заключение

Мы представили древовидную модель на основе нейронной сети DNDT. Он имеет лучшую производительность, чем NN для определенных наборов табличных данных, при этом обеспечиваетинтерпретируемое дерево решений. Между тем, по сравнению с обычными DT, DNDT проще в реализации, одновременно выполняет поиск в древовидной структуре и параметрах с помощью SGD и легко ускоряется на GPU. Есть много возможностей для будущей работы. Мы хотим исследовать источник наблюдаемой нами саморегуляции; изучить подключение DNDT как модуля, подключенного к обычному элементу обучения CNN, для сквозного обучения; выяснить, можно ли использовать обучение на основе SGD целого дерева DNDT в качестве постобработки для точной настройки обычных,жаднообученных DT и повышения ихпроизводительности; выяснить, можно ли использовать многие подходы кадаптивномуобучению на основе NN для обеспечения возможности переносаобучения для DT.

Ссылки
  1. Abadi, Martn, Agarwal, Ashish, Barham, Paul, Brevdo, Eugene, Chen, Zhifeng, Citro, Craig, Corrado, Greg S., Davis, Andy, Dean, Jeffrey, Devin, Matthieu, Ghemawat, Sanjay, Goodfellow, Ian, Harp, Andrew, Irving, Geoffrey, Isard, Michael, Jia, Yangqing, Jozefowicz, Rafal, Kaiser, Lukasz, Kudlur, Manjunath, Levenberg, Josh, Mane, Dandelion, Monga, Rajat, Moore, Sherry, Murray, Derek, Olah, Chris, Schuster, Mike, Shlens, Jonathon, Steiner, Benoit, Sutskever, Ilya, Talwar, Kunal, Tucker, Paul, Vanhoucke, Vincent, Vasudevan, Vijay, Viegas, Fernanda, Vinyals, Oriol, Warden, Pete, Wattenberg, Martin, Wicke, Martin, Yu, Yuan, and Zheng, Xiaoqiang. TensorFlow: Large-scale machine learning on heterogeneous systems, 2015. URL https://www.tensorflow.org/.

  2. Balestriero, R. Neural Decision Trees. ArXiv e-prints, 2017.

  3. Bengio, Yoshua. Estimating or propagating gradients through stochastic neurons. CoRR, abs/1305.2982, 2013.

  4. Bostrom, Nick and Yudkowsky, Eliezer. The ethics of artificial intelligence, pp. 316334. Cambridge University Press, 2014.

  5. Breiman, L., H. Friedman, J., A. Olshen, R., and J. Stone, C. Classification and Regression Trees. Chapman & Hall, New York, 1984.

  6. Breiman, Leo. Random forests. Machine Learning, 45(1): 532, October 2001.

  7. Bul, S. and Kontschieder, P. Neural decision forests for semantic image labelling. In CVPR, 2014.

  8. Chen, Tianqi and Guestrin, Carlos. Xgboost: A scalable tree boosting system. In KDD, 2016.

  9. Chung, J., Ahn, S., and Bengio, Y. Hierarchical Multiscale Recurrent Neural Networks. In ICLR, 2017.

  10. Dash, S., Malioutov, D. M., and Varshney, K. R. Learning interpretable classification rules using sequential rowsampling. In ICASSP, 2015.

  11. Doshi-Velez, Finale; Kim, Been. Towards a rigorous science of interpretable machine learning. ArXiv e-prints, 2017.

  12. Dougherty, James, Kohavi, Ron, and Sahami, Mehran. Supervised and unsupervised discretization of continuous features. In ICML, 1995.

  13. Ho, Tin Kam. The random subspace method for constructing decision forests. IEEE Transactions on Pattern Analysis and Machine Intelligence, 20(8):832844, 1998.

  14. Jang, E., Gu, S., and Poole, B. Categorical Reparameterization with Gumbel-Softmax. In ICLR, 20

  15. Kim, B., Gilmer, J., Viegas, F., Erlingsson, U., and Wattenberg, M. TCAV: Relative concept importance testing with Linear Concept Activation Vectors. ArXiv e-prints, 2017.

  16. Kim, Been, Khanna, Rajiv, and Koyejo, Sanmi. Examples are not enough, learn to criticize! Criticism for interpretability. In NIPS, 2016.

  17. Kontschieder, P., Fiterau, M., Criminisi, A., and Bul, S. R. Deep neural decision forests. In ICCV, 2015.

  18. Lecun, Yann, Bengio, Yoshua, and Hinton, Geoffrey. Deep learning. Nature, 521(7553):436444, 5 2015.

  19. Malioutov, Dmitry M., Varshney, Kush R., Emad, Amin, and Dash, Sanjeeb. Learning interpretable classification rules with boolean compressed sensing. In Transparent Data Mining for Big and Small Data, pp. 95121. Springer International Publishing, 2017.

  20. Norouzi, Mohammad, Collins, Maxwell D., Johnson, Matthew, Fleet, David J., and Kohli, Pushmeet. Efficient non-greedy optimization of decision trees. In NIPS, 2015.

  21. Paszke, Adam, Gross, Sam, Chintala, Soumith, Chanan, Gregory, Yang, Edward, DeVito, Zachary, Lin, Zeming, Desmaison, Alban, Antiga, Luca, and Lerer, Adam. Automatic differentiation in pytorch. In NIPS Workshop on Autodiff, 2017.

  22. Pedregosa, F., Varoquaux, G., Gramfort, A., Michel, V., Thirion, B., Grisel, O., Blondel, M., Prettenhofer, P., Weiss, R., Dubourg, V., Vanderplas, J., Passos, A., Cournapeau, D., Brucher, M., Perrot, M., and Duchesnay, E. Scikit-learn: Machine learning in Python. Journal of Machine Learning Research, 12:28252830, 2011.

  23. Quinlan, J. Ross. C4.5: Programs for Machine Learning. Morgan Kaufmann Publishers Inc., 1993.

  24. Ribeiro, Marco Tulio, Singh, Sameer, and Guestrin, Carlos. why should i trust you?: Explaining the predictions of any classifier. In KDD, 2016.

  25. Schmidhuber, J. Deep learning in neural networks: An overview. Neural Networks, 61:85117, 2015.

  26. Weller, Adrian. Challenges for transparency. In ICML Workshop on Human Interpretability in Machine Learning, pp. 5562, 2017.

  27. Wolpert, David H. The lack of a priori distinctions between learning algorithms. Neural Computation, 8(7):13411390, 1996.

  28. Xiong, Zheng, Zhang, Wenpeng, and Zhu, Wenwu. Learning decision trees with reinforcement learning. In NIPS Workshop on Meta-Learning, 2017.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru