Разработка Kirill Kazakov
2 737

Научить автомобиль ездить самостоятельно при помощи машинного обучения

Перевод выступления технического директора Overleaf Джона Лиса-Миллера.

В закладки

Введение

Этот проект был личным, но в некоторой степени он связан с моей работой. Я занимаю должность технического директора в Overleaf, онлайн-редакторе LaTeX. Сегодня платформа насчитывает 3 млн пользователей.

Я и мой коллега-сооснователь прежде занимались беспилотными автомобилями.

Overleaf мы запустили, одновременно проектируя Heathrow Pods, первую в мире систему беспилотных такси. Она открылась в 2011 году и до сих пор перевозит пассажиров.

Ещё в 2011 году нам удалось создать сеть беспилотников благодаря отдельной сети дорог. Heathrow Pods — закрытая система, поэтому мы полагались на традиционные методы разработки и проверки безопасности.

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

Один из пионеров этой области — Себастьян Трун, профессор Стэнфорда и победитель гонки DARPA Grand Challenge в 2005 году, во многом ознаменовавшей современную эпоху беспилотников. Позже Трун основал платформу онлайн-обучения Udacity. В 2016 году на сайте появился курс по беспилотным машинам. Я записался.

Себастьян Трун

В основе моего выступления — одна из лабораторных работ курса, которая, в свою очередь, опирается на доклад Nvidia «Сквозное обучение в беспилотных автомобилях».

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

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

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

Наша задача на той лабораторной — воспроизвести эту магию. Пойдём по тому же пути:

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

Поехали.

Собираем данные для обучения

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

В правом верхнем углу видна иконка «Запись»: мы записываем изображение с камеры и углы поворота в каждом кадре десять раз в секунду.

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

Синие пики — нажатия кнопок; зелёные и красные кривые — результаты применения экспоненциального и Гауссового сглаживания соответственно; второе оказалось лучше

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

Таким образом машина учится возвращаться в центр трассы (надеюсь, это не научит её съезжать).

Проехавшись по дороге несколько раз и записав возврат на курс, я получил 11 тысяч кадров:

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

Перенос обучения

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

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

Сеть Inception огромна — более 25 млн параметров; на её обучение Google потратила немало сил и денег. Как же нам её адаптировать? Ответ прост: мы проведём «лоботомию» и вытащим лишь первые 44 слоя (отмечены красной рамкой).

Почему это работает? Дело в том, что вырезанные слои типичны для обработки изображений (например, детекторы границ). Разница между породами собак определяется позже, как и другие классы изображений нам не нужные. Мы дополним группу Inception тремя собственными слоями:

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

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

Свёрточные нейронные сети

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

  • свёртка;
  • изменение размера (особенно в меньшую сторону);
  • функции активации.

Рассмотрим каждый по очереди. В качестве образца возьмём кадр ниже:

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

Первой идёт свёртка — простая, но универсальная операция. Для её выполнения нам потребуется ядро — небольшая числовая матрица. Мы задействуем ядро 3x3 пикселя.

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

Как работает свёртка

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

Чтобы задействовать свёртку в нейросети, нужно не высчитывать ядра самостоятельно, а позволить системе самой проанализировать данные и вывести множество ядер. Используемые слои Inception располагают около 700 тысячами параметров, немало из которых — параметры ядер, обученные Google на 1,2 млн изображений.

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

Подвыборка

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

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

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

Перейдём к функциям активации — в словосочетании «нейронная сеть» появляется слово «нейронный».

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

Так, у нейрона есть дендриты, «входы», соединённые с восходящими нейронами. Если «входы» в сумме преодолевают определённый порог, этот нейрон активируется и отсылает сигнал через аксон в нисходящие нейроны.

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

Вот и всё. Эти три операции мы повторяем снова и снова. Отмечу, однако, что свёртка (по крайней мере, тип используемый здесь) — линейная операция. Не будь изменение размера и функции активации нелинейными, композиция свёрток попросту схлопнулась бы в одну большую линейную функцию.

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

Сборка

С теорией покончено. Теперь посмотрим, на что способна наша сеть, используя картинку-образец. Пропустив её через 44 слоя сети Inception, мы получим 256 изображений в серых тонах; их стороны примерно в десять раз меньше сторон изображения на входе, но они гораздо глубже.

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

Истолковать ответ нейронов будет проще, если наложить его на входное изображение:

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

На 43-ем (сверху в середине), похоже, видна поверхность дороги — тоже может пригодиться. Кое-где, правда, проступает фон, но 48 изображение (справа в средней строке) почти полностью берёт его на себя. Комбинация этих изображений, судя по всему, даст нам полезную информацию.

Замечу: для этой части сети мы не указывали черты, необходимые в решении нашей задачи. На самом деле она обучена Google для классификации изображений.

Это переносит нас к фрагменту сети, который мы обучаем сами, — те три слоя, добавленные в конце.

Первый слой — очередная свёртка, только с ядром 1х1. Эта свёртка подбирает заданное число линейных комбинаций изображений, получившихся после обработки Inception; нередко её используют для понижения размерности.

Такой выбор архитектуры обусловлен выше — вместо изображения 42, 43 и 48 могут хорошо работать на распознавание дороги. Мы же хотим, чтобы сеть отбирала самые полезные комбинации, верно?

В этом примере мы выберем 64 линейные комбинации из 256 изображений. Ниже — изображения до и после свёртки с ядром 1х1. Они похожи, но вторая группа в целом ярче и сглаженнее.

И вновь на некоторых изображениях отчётливо проявляются края дороги, например на 45-м (на этот раз из 64 картинок).

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

Схема справа описывает одну из таких комбинаций — один нейрон — в одном полносвязном слое. Такой слой состоит из множества нейронов, соединнёных со всеми выходами предыдущего слоя. Здесь мы обучаем веса wi и выходы xi. И снова мы применяем функцию активации f к каждому нейрону — вносим нелинейность.

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

Обучение

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

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

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

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

Layer (type) Output Shape Param # Connected to ==================================================================================================== convolution2d_1 (Convolution2D) (None, 17, 37, 64) 16448 convolution2d_input_1[0][0] ____________________________________________________________________________________________________ flatten_1 (Flatten) (None, 40256) 0 convolution2d_1[0][0] ____________________________________________________________________________________________________ dense_1 (Dense) (None, 32) 1288224 flatten_1[0][0] ____________________________________________________________________________________________________ dense_2 (Dense) (None, 1) 33 dense_1[0][0] ==================================================================================================== Total params: 1304705 ____________________________________________________________________________________________________ Epoch 1/30 27144/27144 [==============================] - 160s - loss: 79.0047 - val_loss: 0.0780 Epoch 2/30 27144/27144 [==============================] - 147s - loss: 25.4130 - val_loss: 0.0692 Epoch 3/30 27144/27144 [==============================] - 148s - loss: 8.4912 - val_loss: 0.0670 Epoch 4/30 27144/27144 [==============================] - 148s - loss: 2.9383 - val_loss: 0.0638 … Epoch 12/30 27144/27144 [==============================] - 148s - loss: 0.0851 - val_loss: 0.0572 Epoch 13/30 27144/27144 [==============================] - 148s - loss: 0.0785 - val_loss: 0.0568 Epoch 14/30 27144/27144 [==============================] - 148s - loss: 0.0802 - val_loss: 0.0546 Epoch 15/30 27144/27144 [==============================] - 147s - loss: 0.0769 - val_loss: 0.0569 Epoch 16/30 27144/27144 [==============================] - 147s - loss: 0.0793 - val_loss: 0.0560 Epoch 17/30 27144/27144 [==============================] - 148s - loss: 0.0832 - val_loss: 0.0574

Тут много всего интересного, но я прокомментирую следующее. В начале — краткое описание обучаемой модели, включающее несколько параметров, которым нужно соответствовать. Помните: слои Inception остаются неизменными, мы занимаемся обучением лишь блока на конце объединённой сети.

Во второй секции — процесс обучения, который Keras описывает по мере развития. Я разбил собранные данные для обучения на две части: обучающую (80%) и контрольную (20%). Каждую эпоху Keras записывает значение средней абсолютной ошибки в прогнозах сети по обучающей (loss) и контрольной (val_loss) группам.

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

Спустя 17 эпох потери в десятки раз ниже, около 0,08 пункта. Обучение заканчивается тогда, когда начинают расти потери в контрольной выборке (val_loss), — большее число эпох весьма вероятно приведёт к переобучению.

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

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

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

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

Доработки

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

  • Различные функции потерь: как нам измерить ошибку? Применив формулы среднего квадрата ошибки и средней абсолютной ошибки, я остановился на втором варианте.
  • Различные функции активации: я попробовал сигмоиду, гиперболический тангенс и выпрямитель; тангенс показал себя лучше.
  • Различная регуляризация: отбраковка весов, чтобы они не становились слишком большими, — распространённая практика избежания переобучения; я перебрал несколько весов L2-регуляризации.
  • Различные распределения начальных весов решили пару проблем со сходимостью во время обучения.
  • Различные размеры слоёв: сколько нейронов в каждом скрытом слое?
  • Различные архитектуры сети — добавить ли слоёв? Или убрать?

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

Задержка контроллера — 0,35 секунды

Контроллер тратил слишком много времени на обработку каждого кадра, поэтому подруливать он мог лишь три раза в секунду.

Кроме того, дальнейшее расследование показало: система проводила большую часть времени на слоях Inception. С одной стороны, проблему можно было решить покупкой более быстрого ноутбука. С другой — количество слоёв Inception можно сократить с 44-х до 12-ти (на картинке семь: остальные невидимы).

Уменьшаем задержку

Посмотрим, как система ведёт себя при низкой задержке (около 0,1 секунды вместо 0,35 секунды):

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

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

Результат

Со всеми дополнениями сеть смогла проехать полный круг:

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

Расширение

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

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

Заключение

В этом выступлении мы:

  • взяли нейронную сеть, обученную распознаванию пород собак;
  • переделали её под управление автомобилем;
  • научили систему водить, располагая 35 минутами образцов;
  • посмотрели, как она едет по незнакомому пути.

Более того:

  • в 2015 году мало кто считал, что и это будет возможно;
  • в 2016 году тысячи людей вроде меня занимались обучением беспилотников в свободное время на онлайн-курсах;
  • в 2018 году я руководил (совсем немного) работой школьника Джоша, который проделал всё это сам на радиоуправляемой машинке с Raspberry Pi.

Код проекта находится в открытом доступе.

#машинноеобучение #беспилотники

{ "author_name": "Kirill Kazakov", "author_type": "editor", "tags": ["\u043c\u0430\u0448\u0438\u043d\u043d\u043e\u0435\u043e\u0431\u0443\u0447\u0435\u043d\u0438\u0435","\u0431\u0435\u0441\u043f\u0438\u043b\u043e\u0442\u043d\u0438\u043a\u0438"], "comments": 2, "likes": 17, "favorites": 5, "is_advertisement": false, "subsite_label": "dev", "id": 59789, "is_wide": false, "is_ugc": false, "date": "Thu, 28 Feb 2019 16:33:40 +0300" }
{ "id": 59789, "author_id": 127882, "diff_limit": 1000, "urls": {"diff":"\/comments\/59789\/get","add":"\/comments\/59789\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/59789"}, "attach_limit": 2, "max_comment_text_length": 5000, "subsite_id": 235819 }

2 комментария 2 комм.

Популярные

По порядку

0

Не до конца понятно, но интересно :) Не знал, что можно резать готовые нейронки и переиспользовать для других задач. ЗЫ, вспомнился мем:

Ответить
0

Погружение в сон от статьи - 45 секунд. Спасибо!

Ответить
0
{ "page_type": "article" }

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fizc" } } }, { "id": 4, "label": "240х200_mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "flbq" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fjog" } } }, { "id": 10, "disable": true, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "disable": true, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "bscsh", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "bugf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-223676-0", "render_to": "inpage_VI-223676-0-1104503429", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=bugf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Плашка на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudx", "p2": "ftjf" } } }, { "id": 16, "label": "Кнопка в шапке мобайл", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byzqf", "p2": "ftwx" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "bugf", "p2": "fzvc" } } }, { "id": 19, "label": "Тизер на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "p1": "cbltd", "p2": "gazs" } } } ]
Нейронная сеть научилась читать стихи
голосом Пастернака и смотреть в окно на осень
Подписаться на push-уведомления
{ "page_type": "default" }