Применяя глубокое обучение с подкреплением (RL) к робототехнике, мы сталкиваемся с загадкой: как научить робота выполнять задачу, когда для глубокого обучения требуются сотни тысяч, даже миллионы примеров? Чтобы добиться 96% успеха в захвате невидимых объектов, исследователи из Google и Беркли обучили робота-агента 580 000 попыток захвата в реальном мире. На этот подвиг потребовалось семь роботов и несколько недель. Без ресурсов Google это может показаться безнадежным для среднего специалиста по машинному обучению. Мы не можем рассчитывать на то, что легко проведем сотни тысяч итераций тренировок с использованием физического робота, который изнашивается и требует человеческого наблюдения, и ни то, ни другое не обходится дешево. Было бы гораздо более осуществимо, если бы мы могли предварительно обучить такие алгоритмы RL, чтобы резко сократить количество необходимых попыток в реальном мире.

С появлением глубокого обучения методы RL стали более зрелыми, но вместе с тем и спрос на данные. Пытаясь восполнить этот пробел, многие исследователи изучают синтетическую генерацию обучающих данных, используя методы 3D-рендеринга для создания макетов рабочей среды. Хотя этот метод творит чудеса в смоделированной среде, он плохо переносится на реальный мир. Любой, кто тесно сотрудничал с глубоким обучением, знает, насколько эффективно оно использует нюансы обучающих данных, чтобы «обмануть» задачу. На практике реальный мир выходит за рамки выборки для этих моделей, обученных в симуляции, и, что неудивительно, они терпят неудачу.

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

Концепция

Скажем, например, мы хотим сгенерировать синтетические данные для обучения, чтобы предварительно обучить классификатор, который может различать набор небольших объектов. Мы хотим выбрать объект, бросить его в виртуальную корзину и сделать снимок экрана. В идеале мы хотим повторить этот процесс тысячи раз с рандомизацией, чтобы создать богатый набор данных изображений объектов. С этой целью (вдохновленный работой, проделанной в OpenAI и Google), я недавно начал свой путь к генерации обучающих данных для моих собственных приложений. Это привело меня к PyBullet, модулю Python, разработанному для приложений робототехники и машинного обучения на основе Bullet Physics SDK.

В отличие от других решений для 3D-рендеринга, таких как Maya или Blender, PyBullet ориентирован на робототехнику и имеет собственные реализации для таких концепций, как суставы, динамическое моделирование, прямая и обратная кинематика и многое другое. Кроме того, его можно легко установить с помощью диспетчера пакетов, что позволяет легко интегрировать другие пакеты Python, такие как NumPy и TensorFlow, в ваши симуляции. PyBullet похож на проприетарную программу моделирования физики MuJoCo, но она бесплатна и проста в установке, поэтому это отличный выбор для тех, кто хочет экспериментировать с моделированием и робототехникой. Это руководство предназначено для тех, кто хочет создавать обучающие образы, но официальное руководство по быстрому запуску PyBullet можно найти здесь. Обратите внимание, что в этом руководстве предполагается, что вы используете Python версии ≥3.6 (если вы используете более старую версию, попробуйте создать виртуальную среду для этого учебника).

Установка

Перво-наперво мы установим PyBullet в нашу среду Python.

pip install numpy
pip install pybullet

Обратите внимание, что сначала установка NumPy не является обязательной, но рекомендуется при рендеринге изображений из-за накладных расходов на копирование буферов изображений между C / C ++ и Python.

Начиная

Теперь, когда у вас все успешно установлено, давайте перейдем непосредственно к сеансу PyBullet. PyBullet полагается на модель клиент-сервер, где ваш сеанс Python отправляет и получает данные с сервера моделирования. Есть в основном два типа серверов: ПРЯМЫЙ и GUI. Как следует из названия, графический интерфейс позволяет вам видеть физическое моделирование в графическом интерфейсе. DIRECT полезен для рендеринга без головы и может использоваться для эффективного рендеринга без графического процессора. DIRECT будет режимом выбора, когда мы будем готовы визуализировать тысячи изображений в качестве обучающих данных, но пока мы выберем GUI. Начнем с инициализации сервера PyBullet. Начните с запуска интерактивного сеанса Python, а затем введите следующее:

import pybullet as pb
physicsClient = pb.connect(pb.GUI)

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

Загрузка самолета

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

import pybullet_data
pb.setAdditionalSearchPath(pybullet_data.getDataPath())
planeId = pb.loadURDF(‘plane.urdf’)

Это делает несколько вещей. Во-первых, он загружает другой модуль под названием pybullet_data, который содержит URDF-заполнители для многих полезных вещей. Он добавляется к пути, чтобы, когда мы наконец вызываем loadURDF, он знал, где найти «plane.urdf», и загружал его в нашу сцену. Теперь у нас должна получиться плоскость, охватывающая нашу сцену.

Загрузка объектов для рендеринга

Наиболее поддерживаемые форматы файлов в PyBullet - URDF, SDF и MJCF. Эти форматы очень легко загружать и настраивать, и они имеют свои собственные специализированные функции загрузки. Однако многие из 3D-моделей, которые мы находим в Интернете, имеют простой и популярный формат файла Wavefront OBJ. Их можно загрузить, но для этого необходимо немного больше узнать об основных принципах работы с моделями в PyBullet, поэтому мы прижмемся к делу и научимся загружать наши собственные модели с файлами OBJ.

Мы будем использовать набор данных процедурно сгенерированных объектов для задач моделирования. Этот набор состоит из 1000 файлов OBJ, которые включают соответствующие коллайдеры, материалы и версии URDF. Загрузите их здесь и извлеките в папку с именем random_urdfs в папке вашего проекта.

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

visualShape

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

visualShapeId = pb.createVisualShape(
    shapeType=pb.GEOM_MESH,
    fileName='random_urdfs/000/000.obj',
    rgbaColor=None,
    meshScale=[0.1, 0.1, 0.1])

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

collisionShape

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

collisionShapeId = pb.createCollisionShape(
    shapeType=pb.GEOM_MESH,
    fileName='random_urdfs/000/000_coll.obj',
    meshScale=[0.1, 0.1, 0.1])

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

multiBody

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

multiBodyId = pb.createMultiBody(
    baseMass=1.0,
    baseCollisionShapeIndex=collisionShapeId, 
    baseVisualShapeIndex=visualShapeId,
    basePosition=[0, 0, 1],
    baseOrientation=pb.getQuaternionFromEuler([0, 0, 0]))

Мы передаем идентификаторы нашего коллайдера и визуальных форм в createMultiBody для создания нового объекта. Этому объекту необходимы положение и ориентация, чтобы знать, где его разместить в физическом мире, и масса, чтобы знать, как он должен взаимодействовать с другими объектами. . Задайте положение с помощью списка длиной 3 координат x, y, z. Для простоты мы пока установили массу на 1.0.

Ориентация немного сложнее и требует, чтобы мы указали ее в списке длины 4 из кватернионов. Кватернионы - это полезное 4-мерное представление для вращения в трехмерных пространствах, которые критически используются в программах компьютерной графики, таких как Blender, Maya и Unity, чтобы избежать артефактов вращения, таких как gimbal lock. PyBullet имеет удобную функцию под названием getQuaternionFromEuler, которая позволяет нам описывать вращение во вращениях вокруг осей x, y и z, которые людям легче визуализировать и понять. Такое представление вращения называется углами Эйлера.

Примечание: если вы получили сообщение типа «Неподдерживаемый формат изображения текстуры [random_urdfs / 000 / 000.png]», проигнорируйте его. В любом случае мы заменим текстуру по умолчанию на следующем шаге.

Применение текстур к объектам

Успех! Мы создали физический объект и поместили его в наш мир. Напомним, что мы хотели рандомизировать внешний вид и динамику наших 3D-моделей. Давайте начнем с внешнего вида, а также загрузим и извлечем Набор данных описываемых текстур здесь. Он содержит тысячи изображений, состоящих из 47 категорий текстур. Распакуйте их в папку с названием dtd. Давайте наугад выберем одну из этих текстур и применим ее к нашей модели.

import os, glob, random
texture_paths = glob.glob(os.path.join('dtd', '**', '*.jpg'), recursive=True)
random_texture_path = texture_paths[random.randint(0, len(texture_paths) - 1)]
textureId = pb.loadTexture(random_texture_path)
pb.changeVisualShape(multiBodyId, -1, textureUniqueId=textureId)

Этот фрагмент рекурсивно захватывает список путей ко всем текстурам в папке «dtd» с расширением «.jpg» и случайным образом выбирает одну из них. Затем он передает его функции loadTexture для загрузки текстуры и создания идентификатора. Затем он использует функцию changeVisualShape для применения текстуры с textureId к объекту с multiBodyId. Теперь к вашему объекту должна быть применена текстура. Вы также можете применить этот же метод, чтобы прикрепить текстуру к плоскости, если хотите.

Обратите внимание, что -1 необходим для обязательного параметра JointIndex, который указывает ссылку, к которой вы хотите применить текстуру. Ссылки полезны для создания объектов с иерархией движущихся частей, таких как роботы с множеством суставов. Это важная концепция в PyBullet, но она выходит за рамки этого руководства, поэтому мы указываем значение «-1», чтобы указать ему применить его к базовому объекту, а не к какой-либо ссылке.

Начало нашего моделирования

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

pb.setGravity(0, 0, -9.8)

Это применяет нисходящее отрицательное ускорение -9,8 м / с² к нашим объектам в сцене, точно так же, как мы испытываем на поверхности планеты Земля. Однако ваш объект все еще не падает, как вы ожидали. Это потому, что симуляция не пошаговая.

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

pb.setRealTimeSimulation(1)

Установите «1» для включения или «0» для отключения. Теперь ваш объект должен упасть и удариться о землю. Кстати, временной шаг по умолчанию составляет 1/240 секунд, но при желании вы можете изменить это значение по умолчанию, используя функцию setTimeStep.

Как отрендерить ваше изображение

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

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

Внешние свойства камеры по существу описывают, как наша камера расположена в мире, например, ее ориентация и положение. Внутренние свойства камеры описывают свойства самой камеры, такие как поле обзора (FOV) и соотношение сторон ее датчика (например, 4: 3, 16: 9 и т. Д.). В PyBullet мы описываем эти свойства с помощью матрицы представления и матрицы проекции соответственно. Краткое руководство PyBullet содержит ссылки на отличный ресурс, чтобы узнать больше о внешних и внутренних свойствах камеры и их соответствующих матрицах.

viewMatrix

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

viewMatrix = pb.computeViewMatrix(
    cameraEyePosition=[0, 0, 3],
    cameraTargetPosition=[0, 0, 0],
    cameraUpVector=[0, 1, 0])

Как следует из названия, cameraEyePosition описывает физическое расположение камеры в координатах x, y и z. Мы разместим его на 3 единицы прямо над плоскостью по центру нашего объекта. Затем мы опишем точку, в которую должна быть обращена камера, с помощью cameraTargetPosition. Мы хотим, чтобы он смотрел прямо на наш объект, поэтому мы указываем его на начало координат. Наконец, нам нужно описать ориентацию камеры. Мы делаем это с помощью cameraUpVector, который представляет собой вектор, указывающий из верхней части нашей камеры. Мы указываем на это по оси y. Теперь наша камера должна быть направлена ​​прямо вниз, чтобы плоскость изображения, которую она визуализирует, была идеально параллельна плоскости, которую мы создали.

projectionMatrix

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

projectionMatrix = pb.computeProjectionMatrixFOV(
    fov=45.0,
    aspect=1.0,
    nearVal=0.1,
    farVal=3.1)

Параметр fov указывает угол обзора камеры в градусах. Он описывает, насколько широкое поле зрения камеры, и в принципе похоже на фокусное расстояние реальной камеры. Параметр aspect описывает соотношение сторон камеры, которое для простоты мы установили здесь равным 1.0. Параметры nearVal и farVal описывают минимальное и максимальное расстояние, соответственно, на котором камера будет визуализировать объекты. Поскольку наша камера находится на 3 единицы над плоскостью, мы установили farVal на 3.1, чтобы просто включить эту плоскость. Эти параметры определены в документации OpenGL, и вы можете прочитать о них здесь.

getCameraImage

Мы описали свойства нашей камеры и теперь готовы рендерить некоторые изображения. Давайте сделаем это с помощью getCameraImage.

Эта функция возвращает три буфера изображений: rgbImg, depthImg и segImg. rgbImg - изображение uint8 с красным, зеленым, синим и альфа-каналами визуальных эффектов камеры. depthImg - это изображение в градациях серого с плавающей запятой, которое описывает расстояние отдельных визуализированных пикселей от камеры. Его можно использовать для моделирования поля зрения реального датчика глубины, такого как Microsoft Kinect. Наконец, segImg - это маска сегментации изображения, в которой каждый пиксель содержит уникальные целые числа с идентификаторами объектов. Они неоценимы для обучения алгоритмов сегментации для роботизированных агентов, таких как роботизированная рука, которая должна идентифицировать объекты для сортировки по соответствующим ячейкам, или для беспилотного автомобиля, который хочет идентифицировать пешеходов, уличные знаки и дороги.

width, height, rgbImg, depthImg, segImg = pb.getCameraImage(
    width=224, 
    height=224,
    viewMatrix=viewMatrix,
    projectionMatrix=projectionMatrix)

После того, как вы вызовете эту функцию, вы должны увидеть изображения, которые вы визуализировали, в трех окнах в вашем средстве просмотра. Если это так, то поздравляю! Вы визуализировали свой первый объект. Вы, вероятно, захотите преобразовать эти буферы изображений в векторы NumPy и сохранить их в библиотеке изображений, такой как PIL или imageio.

Заключение

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