глубокое обучение с подкреплением на python openai gym и tensorflow для профи
Книга «Глубокое обучение с подкреплением на Python. OpenAI Gym и TensorFlow для профи»
Отрывок. Генерирование текстов песен посредством LSTM RNN
А теперь посмотрим, как использовать LSTM для генерирования текстов песен Зейна Малика. Набор данных с текстами песен Зейна можно загрузить по адресу https://github.com/sudharsan13296/Hands-On-Reinforcement-Learning-With-Python/blob/master/07.%20Deep%20Learning%20Fundamentals/data/ZaynLyrics.txt.
Работа начинается с импортирования необходимых библиотек:
Затем читается файл с текстами песен:
Убедимся в том, что данные были успешно загружены:
Теперь все символы сохраняются в переменной all_chars:
Количество уникальных символов сохраняется в unique_chars:
А общее количество символов сохраняется в переменной total_chars:
Сначала мы присвоим каждому символу индекс. char_to_ix будет содержать отображение символа на индекс, а ix_to_char — отображение индекса на символ:
Затем определяется функция generate_batch, которая генерирует входные и целевые значения. Целевые значения равны сдвигу входного значения, умноженному на i.
Например, если input = [12,13,24] со значением сдвига 1, то целевые значения будут равны [13,24]:
Мы определим длину последовательности, скорость обучения и количество узлов, которое равно числу нейронов:
Построим LSTM RNN. TensorFlow предоставляет функцию BasicLSTMCell() для построения ячеек LSTM; вы должны задать количество единиц в ячейке LSTM и тип используемой функции активации.
Итак, мы создаем ячейку LSTM и строим сеть RNN с этой ячейкой при помощи функции tf.nn.dynamic_rnn(), которая возвращает выход и значение состояния:
Теперь создадим заместителя для входа X и цели Y:
Преобразуем X и Y в int:
Также создадим onehot-представления для X и Y:
Получим выходы и состояния от RNN вызовом функции build_rnn:
Инициализируем веса и смещение:
Вычислим выход, умножая выход на веса и прибавляя смещение:
Теперь выполним softmax-активацию и получим вероятности:
Потеря cross_entropy будет вычислена в следующем виде:
Наша цель — минимизация потери, поэтому мы выполним обратное распространение для сети и проведем градиентный спуск:
Затем будет определена вспомогательная функция predict, которая даст индексы следующего прогнозируемого символа в соответствии с моделью RNN:
Затем будет задан размер пакета batch_size, количество пакетов и количество эпох, а также величина сдвига shift для генерирования пакета:
Наконец, мы создаем сеанс TensorFlow и строим модель:
Как видно из результатов, в исходной эпохе выход состоит из случайных символов, но по мере обучения результаты улучшаются:
Об авторе
Судхарсан Равичандиран — специалист по обработке и анализу данных, горячий поклонник искусственного интеллекта и видеоблогер. Он получил степень бакалавра в области computer science в Университете Анны и занимается исследованиями практической реализации глубокого обучения и обучения с подкреплением, включая обработку естественных языков и компьютерное зрение. Ранее работал внештатным веб-дизайнером и разработчиком, участвовал в создании ряда сайтов, отмеченных наградами. В настоящее время принимает участие в проектах с открытым кодом и часто отвечает на вопросы на Stack Overflow.
О научных редакторах
Суджит Пал (Sujit Pal) — руководитель технических исследований в Elsevier Labs, группы разработки новейших технологий компании Reed-Elsevier Group. Занимается исследованиями в области семантического поиска, обработки естественных языков, машинного и глубокого обучения. В Elsevier работал над несколькими инициативными проектами, включая оценку и совершенствование качества поиска, классификацию изображений и выявление дубликатов, аннотацию и разработку антологий медицинских и научных текстов. Он написал книгу о глубоком обучении совместно с Антонио Галли (Antonio Gulli) и пишет о технологиях в своем блоге Salmon Run.
Сурьядипан Рамамурти (Suriyadeepan Ramamoorthy) — исследователь искусственного интеллекта и инженер из AI researcher and engineer в Пондичерри (Индия). Основная тематика его работ — понимание естественных языков и формирование рассуждений. Он активно пишет в блоге, посвященном глубокому обучению. В SAAMA Technologies он применяет расширенные методы глубокого обучения для анализа биомедицинских текстов. Являясь ярым сторонником свободно распространяемого ПО, активно участвует в проектах по его разработке в сообществе FSFTN. Также интересуется коллективными сетями, визуализацией данных и творческим программированием.
Для Хаброжителей скидка 25% по купону — Python
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Книга «Глубокое обучение с подкреплением на Python. OpenAI Gym и TensorFlow для профи»
Отрывок. Генерирование текстов песен посредством LSTM RNN
А теперь посмотрим, как использовать LSTM для генерирования текстов песен Зейна Малика. Набор данных с текстами песен Зейна можно загрузить по адресу https://github.com/sudharsan13296/Hands-On-Reinforcement-Learning-With-Python/blob/master/07.%20Deep%20Learning%20Fundamentals/data/ZaynLyrics.txt.
Работа начинается с импортирования необходимых библиотек:
Затем читается файл с текстами песен:
Убедимся в том, что данные были успешно загружены:
Теперь все символы сохраняются в переменной all_chars:
Количество уникальных символов сохраняется в unique_chars:
А общее количество символов сохраняется в переменной total_chars:
Сначала мы присвоим каждому символу индекс. char_to_ix будет содержать отображение символа на индекс, а ix_to_char — отображение индекса на символ:
Затем определяется функция generate_batch, которая генерирует входные и целевые значения. Целевые значения равны сдвигу входного значения, умноженному на i.
Например, если input = [12,13,24] со значением сдвига 1, то целевые значения будут равны [13,24]:
Мы определим длину последовательности, скорость обучения и количество узлов, которое равно числу нейронов:
Построим LSTM RNN. TensorFlow предоставляет функцию BasicLSTMCell() для построения ячеек LSTM; вы должны задать количество единиц в ячейке LSTM и тип используемой функции активации.
Итак, мы создаем ячейку LSTM и строим сеть RNN с этой ячейкой при помощи функции tf.nn.dynamic_rnn(), которая возвращает выход и значение состояния:
Теперь создадим заместителя для входа X и цели Y:
Преобразуем X и Y в int:
Также создадим onehot-представления для X и Y:
Получим выходы и состояния от RNN вызовом функции build_rnn:
Инициализируем веса и смещение:
Вычислим выход, умножая выход на веса и прибавляя смещение:
Теперь выполним softmax-активацию и получим вероятности:
Потеря cross_entropy будет вычислена в следующем виде:
Наша цель — минимизация потери, поэтому мы выполним обратное распространение для сети и проведем градиентный спуск:
Затем будет определена вспомогательная функция predict, которая даст индексы следующего прогнозируемого символа в соответствии с моделью RNN:
Затем будет задан размер пакета batch_size, количество пакетов и количество эпох, а также величина сдвига shift для генерирования пакета:
Наконец, мы создаем сеанс TensorFlow и строим модель:
Как видно из результатов, в исходной эпохе выход состоит из случайных символов, но по мере обучения результаты улучшаются:
Об авторе
Судхарсан Равичандиран — специалист по обработке и анализу данных, горячий поклонник искусственного интеллекта и видеоблогер. Он получил степень бакалавра в области computer science в Университете Анны и занимается исследованиями практической реализации глубокого обучения и обучения с подкреплением, включая обработку естественных языков и компьютерное зрение. Ранее работал внештатным веб-дизайнером и разработчиком, участвовал в создании ряда сайтов, отмеченных наградами. В настоящее время принимает участие в проектах с открытым кодом и часто отвечает на вопросы на Stack Overflow.
О научных редакторах
Суджит Пал (Sujit Pal) — руководитель технических исследований в Elsevier Labs, группы разработки новейших технологий компании Reed-Elsevier Group. Занимается исследованиями в области семантического поиска, обработки естественных языков, машинного и глубокого обучения. В Elsevier работал над несколькими инициативными проектами, включая оценку и совершенствование качества поиска, классификацию изображений и выявление дубликатов, аннотацию и разработку антологий медицинских и научных текстов. Он написал книгу о глубоком обучении совместно с Антонио Галли (Antonio Gulli) и пишет о технологиях в своем блоге Salmon Run.
Сурьядипан Рамамурти (Suriyadeepan Ramamoorthy) — исследователь искусственного интеллекта и инженер из AI researcher and engineer в Пондичерри (Индия). Основная тематика его работ — понимание естественных языков и формирование рассуждений. Он активно пишет в блоге, посвященном глубокому обучению. В SAAMA Technologies он применяет расширенные методы глубокого обучения для анализа биомедицинских текстов. Являясь ярым сторонником свободно распространяемого ПО, активно участвует в проектах по его разработке в сообществе FSFTN. Также интересуется коллективными сетями, визуализацией данных и творческим программированием.
Для Хаброжителей скидка 25% по купону — Python
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Обучение с подкреплением на языке Python
В последней публикации уходящего года мы хотели упомянуть о Reinforcement Learning — теме, книгу на которую мы уже переводим.
Посудите сами: нашлась элементарная статья с Medium, в которой изложен контекст проблемы, описан простейший алгоритм с реализацией на Python. В статье есть несколько гифок. А мотивация, вознаграждение и выбор правильной стратегии на пути к успеху — это вещи, которые исключительно пригодятся в наступающем году каждому из нас.
Приятного чтения!
Обучение с подкреплением – это разновидность машинного обучения, при котором агент учится действовать в окружающей среде, выполняя действия и тем самым нарабатывая интуицию, после чего наблюдает результаты своих действий. В этой статье я расскажу, как понять и сформулировать задачу на обучение с подкреплением, а затем решить ее на Python.
В последнее время мы уже привыкли к тому, что компьютеры играют в игры против человека – либо как боты в многопользовательских играх, либо как соперники в играх «один на один»: скажем, в Dota2, PUB-G, Mario. Исследовательская компания Deepmind наделала шороху в новостях, когда в 2016 году их программа AlphaGo в 2016 году одолела чемпиона Южной Кореи по го. Если вы – заядлый геймер, то могли слышать о пятерке матчей Dota 2 OpenAI Five, где машины сражались против людей и в нескольких матчах одолели лучших игроков в Dota2. (Если вас интересуют подробности, здесь подробно проанализирован алгоритм и рассмотрено, как играли машины).
Последняя версия OpenAI Five берет Roshan.
Итак, начнем с центрального вопроса. Зачем нам требуется обучение с подкреплением? Используется ли оно только в играх, либо применимо в реалистичных сценариях для решения прикладных задач? Если вы впервые читаете про обучение с подкреплением, то просто не можете вообразить себе ответ на эти вопросы. Ведь обучение с подкреплением — одна из самых широко используемых и бурно развивающихся технологий в сфере искусственного интеллекта.
Вот ряд предметных областей, в которых особенно востребованы системы по обучению с подкреплением:
Итак, как же сформировался сам феномен обучения с подкреплением, когда у нас в распоряжении такое множество методов машинного и глубокого обучения? «Его изобрели Рич Саттон и Эндрю Барто, научный руководитель Рича, помогавший ему готовить PhD». Парадигма впервые оформилась в 1980-е и тогда была архаична. Впоследствии Рич верил, что у нее большое будущее, и она в конце концов получит признание.
Обучение с подкреплением поддерживает автоматизацию в той среде, где оно внедрено. Примерно также действуют и машинное, и глубокое обучение – стратегически они устроены иначе, но обе парадигмы поддерживают автоматизацию. Итак, почему же возникло обучение с подкреплением?
Оно очень напоминает естественный процесс обучения, при котором процесс/модель действует и получает обратную связь о том, как ей удается справляться с задачей: хорошо и нет.
Машинное и глубокое обучение – также варианты обучения, однако, они в большей степени заточены под выявление закономерностей в имеющихся данных. В обучении с подкреплением, с другой стороны, такой опыт приобретается методом проб и ошибок; система постепенно находит правильные варианты действий или глобальный оптимум. Серьезное дополнительное преимущество обучения с подкреплением заключается в том, что в данном случае не требуется предоставлять обширного набора учебных данных, как при обучении с учителем. Достаточно будет нескольких мелких фрагментов.
Понятие об обучении с подкреплением
Представьте, что учите ваших кошек новым фокусам; но, к сожалению, кошки не понимают человеческого языка, поэтому вы не можете взять и рассказать им, во что собираетесь с ними играть. Поэтому вы будете действовать иначе: имитировать ситуацию, а кошка в ответ будет пытаться реагировать тем или иным способом. Если кошка отреагировала так, как вы хотели, то вы наливаете ей молока. Понимаете, что будет дальше? Вновь оказавшись в аналогичной ситуации, кошка вновь выполнит желаемое вами действие, и с еще большим энтузиазмом, рассчитывая, что ее покормят еще лучше. Так происходит обучение на положительном примере; но, если пытаться «воспитывать» кошку отрицательными стимулами, например, строго смотреть на нее и хмуриться, она обычно не обучается на таких ситуациях.
Схожим образом работает и обучение с подкреплением. Мы сообщаем машине некоторый ввод и действия, а затем вознаграждаем машину в зависимости от вывода. Наша конечная цель – максимизация вознаграждения. Теперь давайте рассмотрим, как переформулировать изложенную выше проблему в терминах обучения с подкреплением.
Знакомство с терминологией обучения с подкреплением
Агент и Среда играют ключевые роли в алгоритме обучения с подкреплением. Среда – это тот мир, в котором приходится выживать Агенту. Кроме того, Агент получает от Среды подкрепляющие сигналы (вознаграждение): это число, характеризующее, насколько хорошим или плохим можно считать текущее состояние мира. Цель Агента — максимизировать совокупное вознаграждение, так называемый «выигрыш». Прежде чем написать наши первые алгоритмы на обучение с подкреплением, необходимо разобраться с нижеизложенной терминологией.
Теперь, познакомившись с терминологией обучения с подкреплением, давайте решим задачу, воспользовавшись соответствующими алгоритмами. Перед этим нужно понять, как сформулировать такую задачу, а при решении этой задачи опираться на терминологию обучения с подкреплением.
Решение задачи такси
Итак, переходим к решению задачи с применением подкрепляющих алгоритмов.
Допустим, у нас есть зона для обучения беспилотного такси, которое мы обучаем доставлять пассажиров на парковку в четыре различные точки ( R,G,Y,B ). Перед этим нужно понять и задать среду, в которой начнем программировать на Python. Если вы только начинаете осваивать Python, рекомендую вам эту статью.
Среду для решения задачи с такси можно настроить при помощи Gym от компании OpenAI – это одна из самых популярных библиотек для решения задач на обучение с подкреплением. Хорошо, прежде чем использовать gym, ее нужно установить на вашей машине, а для этого удобен менеджер пакетов Python под названием pip. Ниже приведена установочная команда.
“Имеем 4 местоположения (обозначенных разными буквами); наша задача – подхватить пассажира в одной точке и высадить его в другой. Получаем +20 очков за успешную высадку пассажира и теряем 1 очко за каждый шаг, затраченный на это. Также предусмотрен штраф 10 очков за каждую непредусмотренную посадку и высадку пассажира.” (Источник: gym.openai.com/envs/Taxi-v2)
Вот какой вывод мы увидим в нашей консоли:
Отлично, env – это сердце OpenAi Gym, представляет собой унифицированный интерфейс среды. Далее приведены методы env, которые нам весьма пригодятся:
env.reset : сбрасывает окружающую среду и возвращает случайное исходное состояние.
env.step(action) : Продвигает развитие окружающей среды на один шаг во времени.
env.step(action) : возвращает следующие переменные
В среде есть 4 точки, в которых допускается высадка пассажиров: это: R, G, Y, B или [(0,0), (0,4), (4,0), (4,3)] в координатах (по горизонтали; по вертикали), если бы можно было интерпретировать вышеуказанную среду в декартовых координатах. Если также учесть еще одно (1) состояние пассажира: внутри такси, то можно взять все комбинации локаций пассажиров и их мест назначения, чтобы подсчитать общее количество состояний в нашей среде для обучения такси: имеем четыре (4) места назначения и пять (4+1) локаций пассажиров.
Итак, в нашей среде для такси насчитывается 5×5×5×4=500 возможных состояний. Агент имеет дело с одним из 500 состояний и предпринимает действие. В нашем случае варианты действий таковы: перемещение в том или ином направлении, либо решение подобрать/высадить пассажира. Иными словами, у нас в распоряжении шесть возможных действий:
pickup, drop, north, east, south, west (Четыре последних значения – это направления, в которых может двигаться такси.)
Это пространство action space : совокупность всех действий, которые наш агент может предпринять в заданном состоянии.
Поскольку в эту матрицу записаны абсолютно все состояния, можно просмотреть заданные по умолчанию значения вознаграждений, присвоенные тому состоянию, что мы выбрали для иллюстрации:
Давайте напишем код для решения этой задачи без обучения с подкреплением.
Поскольку у нас есть P-таблица с заданными по умолчанию значениями вознаграждения для каждого состояния, можем попытаться организовать навигацию нашего такси просто на основе этой таблицы.
Создаем бесконечный цикл, проматывающийся до тех пор, пока пассажир не попадет в место назначения (один эпизод), либо, иными словами, пока показатель вознаграждения не достигнет 20. Метод env.action_space.sample() автоматически выбирает случайное действие из множества всех доступных действий. Рассмотрим, что происходит:
Задача решена, но не оптимизирована, либо этот алгоритм будет работать не во всех случаях. Нам нужен подходящий взаимодействующий агент, чтобы количество итераций, затрачиваемых машиной/алгоритмом на решение задачи оставалось минимальным. Здесь нам поможет алгоритм Q-обучения, реализацию которого мы рассмотрим в следующем разделе.
Знакомство с Q-обучением
Ниже представлен наиболее востребованный и один из самых простых алгоритмов на обучение с подкреплением. Среда вознаграждает агента за постепенное обучение и за то, что в конкретном состоянии он совершает наиболее оптимальный шаг. В реализации, рассмотренной выше, у нас была таблица вознаграждений «P», по которой будет учиться наш агент. Опираясь на таблицу вознаграждений, он выбирает следующее действие в зависимости от того, насколько оно полезно, а затем обновляет еще одну величину, именуемую Q-значением. В результате создается новая таблица, называемая Q-таблица, отображаемая на комбинацию (Состояние, Действие). Если Q-значения оказываются лучше, то мы получаем более оптимизированные вознаграждения.
Например, если такси оказывается в состоянии, где пассажир оказывается в той же точке, что и такси, исключительно вероятно, что Q-значение для действия «подобрать» выше, чем для других действий, например, «высадить пассажира» или «ехать на север».
Q-величины инициализируются со случайными значениями, и по мере того, как агент взаимодействует со средой и получает различные вознаграждения, совершая те или иные действия, Q-значения обновляются в соответствии со следующим уравнением:
Здесь возникает вопрос: как инициализировать Q-значения и как рассчитывать их. По мере выполнения действий Q-значения выполняются в данном уравнении.
Здесь Альфа и Гамма – параметры алгоритма на Q-обучение. Альфа – это темп обучения, а гамма – дисконтирующий множитель. Оба значения могут быть в диапазоне от 0 до 1 и иногда равны единице. Гамма может быть равна нулю, а альфа – не может, поскольку значение потерь при обновлении должно компенсироваться (темп обучения — положителен). Альфа-значение здесь такое же, как и при обучении с учителем. Гамма определяет, какую важность мы хотим придать вознаграждениям, ожидающим нас в перспективе.
Данный алгоритм кратко изложен ниже:
Итак, ваша модель обучена в условиях окружающей среды, и теперь умеет более точно подбирать пассажиров. А вы познакомились с феноменом обучения с подкреплением, и можете запрограммировать алгоритм для решения новой задачи.
Другие приемы обучения с подкреплением:
Книга «Глубокое обучение с подкреплением на Python. OpenAI Gym и TensorFlow для профи»
Написал(а): robot 2 лет,2 месяцев назад
Привет, Хаброжители! Глубокое обучение с подкреплением (Reinforcement Learning) — самое популярное и перспективное направление искусственного интеллекта. Практическое изучение RL на Python поможет освоить не только базовые, но и передовые алгоритмы глубокого обучения с подкреплением. Эта книга предназначена для разработчиков МО и энтузиастов глубокого обучения, интересующихся искусственным интеллектом и желающих освоить метод обучения с подкреплением. Прочитайте эту книгу и станьте экспертом в области обучения с подкреплением, реализуя практические примеры в работе или вне ее. Знания в области линейной алгебры, математического анализа и языка программирования Python помогут вам понять логику изложения материала.
Отрывок. Генерирование текстов песен посредством LSTM RNN
А теперь посмотрим, как использовать LSTM для генерирования текстов песен Зейна Малика. Набор данных с текстами песен Зейна можно загрузить по адресу https://github.com/sudharsan13296/Hands-On-Reinforcement-Learning-With-Python/blob/master/07.%20Deep%20Learning%20Fundamentals/data/ZaynLyrics.txt.
Работа начинается с импортирования необходимых библиотек:
Затем читается файл с текстами песен:
Убедимся в том, что данные были успешно загружены:
Теперь все символы сохраняются в переменной all_chars:
Количество уникальных символов сохраняется в unique_chars:
А общее количество символов сохраняется в переменной total_chars:
Сначала мы присвоим каждому символу индекс. char_to_ix будет содержать отображение символа на индекс, а ix_to_char — отображение индекса на символ:
Затем определяется функция generate_batch, которая генерирует входные и целевые значения. Целевые значения равны сдвигу входного значения, умноженному на i.
Например, если input = [12,13,24] со значением сдвига 1, то целевые значения будут равны [13,24]:
Мы определим длину последовательности, скорость обучения и количество узлов, которое равно числу нейронов:
Построим LSTM RNN. TensorFlow предоставляет функцию BasicLSTMCell() для построения ячеек LSTM; вы должны задать количество единиц в ячейке LSTM и тип используемой функции активации.
Итак, мы создаем ячейку LSTM и строим сеть RNN с этой ячейкой при помощи функции tf.nn.dynamic_rnn(), которая возвращает выход и значение состояния:
Теперь создадим заместителя для входа X и цели Y:
Преобразуем X и Y в int:
Также создадим onehot-представления для X и Y:
Получим выходы и состояния от RNN вызовом функции build_rnn:
Инициализируем веса и смещение:
Вычислим выход, умножая выход на веса и прибавляя смещение:
Теперь выполним softmax-активацию и получим вероятности:
Потеря cross_entropy будет вычислена в следующем виде:
Наша цель — минимизация потери, поэтому мы выполним обратное распространение для сети и проведем градиентный спуск:
Затем будет определена вспомогательная функция predict, которая даст индексы следующего прогнозируемого символа в соответствии с моделью RNN:
Затем будет задан размер пакета batch_size, количество пакетов и количество эпох, а также величина сдвига shift для генерирования пакета:
Наконец, мы создаем сеанс TensorFlow и строим модель:
Как видно из результатов, в исходной эпохе выход состоит из случайных символов, но по мере обучения результаты улучшаются:
Об авторе
Судхарсан Равичандиран — специалист по обработке и анализу данных, горячий поклонник искусственного интеллекта и видеоблогер. Он получил степень бакалавра в области computer science в Университете Анны и занимается исследованиями практической реализации глубокого обучения и обучения с подкреплением, включая обработку естественных языков и компьютерное зрение. Ранее работал внештатным веб-дизайнером и разработчиком, участвовал в создании ряда сайтов, отмеченных наградами. В настоящее время принимает участие в проектах с открытым кодом и часто отвечает на вопросы на Stack Overflow.
О научных редакторах
Суджит Пал (Sujit Pal) — руководитель технических исследований в Elsevier Labs, группы разработки новейших технологий компании Reed-Elsevier Group. Занимается исследованиями в области семантического поиска, обработки естественных языков, машинного и глубокого обучения. В Elsevier работал над несколькими инициативными проектами, включая оценку и совершенствование качества поиска, классификацию изображений и выявление дубликатов, аннотацию и разработку антологий медицинских и научных текстов. Он написал книгу о глубоком обучении совместно с Антонио Галли (Antonio Gulli) и пишет о технологиях в своем блоге Salmon Run.
Сурьядипан Рамамурти (Suriyadeepan Ramamoorthy) — исследователь искусственного интеллекта и инженер из AI researcher and engineer в Пондичерри (Индия). Основная тематика его работ — понимание естественных языков и формирование рассуждений. Он активно пишет в блоге, посвященном глубокому обучению. В SAAMA Technologies он применяет расширенные методы глубокого обучения для анализа биомедицинских текстов. Являясь ярым сторонником свободно распространяемого ПО, активно участвует в проектах по его разработке в сообществе FSFTN. Также интересуется коллективными сетями, визуализацией данных и творческим программированием.
Для Хаброжителей скидка 25% по купону — Python
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.