Как стать автором
Обновить

Учим робота готовить пиццу. Часть 1: Получаем данные

Время на прочтение23 мин
Количество просмотров9.8K


Автор изображения: Chuchilko


Не так давно, после завершения очередного конкурса на Kaggle — вдруг возникла идея попробовать сделать тестовое ML-приложение.
Например, такое: "помоги роботу сделать пиццу".


Разумеется, основная цель этого ровно та же — изучение нового.


Захотелось разобраться, как работают генеративные нейронные сети (Generative Adversarial Networks — GAN).


Ключевой идеей было обучить GAN, который по выбранным ингредиентам сам собирает картинку пиццы.


Ну что ж, приступим.


Начало


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


И тут я подумал — а почему бы не дёрнуть данные с сайта Додо-пиццы.


Disclaimer

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


Итак, в первом пункте плана действий появилось:


  1. получить с сайта нужные данные

Загрузка данных


Так как вся нужная нам информация доступна на сайте Додо-пиццы, применим так называемый парсинг сайтов (он же — Web Scraping).


Здесь нам поможет статья: Web Scraping с помощью python.


И всего две библиотеки:


import requests
import bs4

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


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

Это окошко появляется в результате GET-запроса, который можно эмулировать, передав нужные заголовки:


headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36',
    'Referer': siteurl,
    'x-requested-with': 'XMLHttpRequest'
}
res = requests.get(siteurl, headers = headers)

в ответ получаем кусок html-кода, который уже можно распарсить.


Сразу же можно обратить внимание, что статический контент распространяется через CDN akamaihd.net


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


На выходе работы скрипта получается несколько csv-файлов и директорий с фотографиями.
Для этого выполняются следующие действия:


  • получение списка городов (на тот момент — 146)
  • получение списка пицц (для Города)
  • получение информации о пицце
  • загрузка фотографий пиццы

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


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


На выходе получилось всего 20 пицц.
На каждую пиццу получается по 3 картинки. Нас интересует только третья картинка, на которой есть вид пиццы сверху.


Разумеется, после получения картинок, их нужно дополнительно обработать — вырезать и отцентровать пиццу.
Думаю, это не должно составить особых проблем, так как все картинки одинаковые — 710х380.


Обработка данных


После scraping-а сайта, получили привычные по kaggle — csv-файл с данными (и директории с картинками).
Настала пора изучить пиццы.


Подключаем необходимые библиотеки.


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline 

import seaborn as sns

np.random.seed(42)

import cv2
import os
import sys

df = pd.read_csv('pizzas.csv', encoding='cp1251')
print(df.shape)

(20, 13)

df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20 entries, 0 to 19
Data columns (total 13 columns):
city_name         20 non-null object
city_url          20 non-null object
pizza_name        20 non-null object
pizza_eng_name    20 non-null object
pizza_url         20 non-null object
pizza_contain     20 non-null object
pizza_price       20 non-null int64
kiloCalories      20 non-null object
carbohydrates     20 non-null object
proteins          20 non-null object
fats              20 non-null object
size              20 non-null int64
weight            20 non-null object
dtypes: int64(2), object(11)
memory usage: 2.1+ KB

df.head()


city_name city_url pizza_name pizza_eng_name pizza_url pizza_contain pizza_price kiloCalories carbohydrates proteins fats size weight
0 Калининград /Kaliningrad Двойная пепперони double-pepperoni https://dodopizza.ru/Kaliningrad/Product/doubl... Томатный соус, двойная порция пепперони и увел... 395 257,52 26,04 10,77 12,11 25 470±50
1 Калининград /Kaliningrad Крэйзи пицца crazy-pizza https://dodopizza.ru/Kaliningrad/Product/crazy... Томатный соус, увеличенные порции цыпленка и п... 395 232,37 31,33 9,08 7,64 25 410±50
2 Калининград /Kaliningrad Дон Бекон pizza-don-bekon https://dodopizza.ru/Kaliningrad/Product/pizza... Томатный соус, бекон, пепперони, цыпленок, кра... 395 274 25,2 9,8 14,8 25 454±50
3 Калининград /Kaliningrad Грибы и ветчина gribvetchina https://dodopizza.ru/Kaliningrad/Product/gribv... Томатный соус, ветчина, шампиньоны, моцарелла 315 189 23,9 9,3 6,1 25 370±50
4 Калининград /Kaliningrad Пицца-пирог pizza-pirog https://dodopizza.ru/Kaliningrad/Product/pizza... Сгущенное молоко, брусника, ананасы 315 144,9 29,8 2,9 2,7 25 420±50


Приведём данные к более удобному виду.


df['kiloCalories'] = df.kiloCalories.apply(lambda x: x.replace(',','.'))
df['carbohydrates'] = df.carbohydrates.apply(lambda x: x.replace(',','.'))
df['proteins'] = df.proteins.apply(lambda x: x.replace(',','.'))
df['fats'] = df.fats.apply(lambda x: x.replace(',','.'))
df['weight'], df['weight_err'] = df['weight'].str.split('±', 1).str

df['kiloCalories'] = df.kiloCalories.astype('float32')
df['carbohydrates'] = df.carbohydrates.astype('float32')
df['proteins'] = df.proteins.astype('float32')
df['fats'] = df.fats.astype('float32')
df['weight'] = df.weight.astype('int64')
df['weight_err'] = df.weight_err.astype('int64')

df.head()


city_name city_url pizza_name pizza_eng_name pizza_url pizza_contain pizza_price kiloCalories carbohydrates proteins fats size weight weight_err
0 Калининград /Kaliningrad Двойная пепперони double-pepperoni https://dodopizza.ru/Kaliningrad/Product/doubl... Томатный соус, двойная порция пепперони и увел... 395 257.519989 26.040001 10.77 12.11 25 470 50
1 Калининград /Kaliningrad Крэйзи пицца crazy-pizza https://dodopizza.ru/Kaliningrad/Product/crazy... Томатный соус, увеличенные порции цыпленка и п... 395 232.369995 31.330000 9.08 7.64 25 410 50
2 Калининград /Kaliningrad Дон Бекон pizza-don-bekon https://dodopizza.ru/Kaliningrad/Product/pizza... Томатный соус, бекон, пепперони, цыпленок, кра... 395 274.000000 25.200001 9.80 14.80 25 454 50
3 Калининград /Kaliningrad Грибы и ветчина gribvetchina https://dodopizza.ru/Kaliningrad/Product/gribv... Томатный соус, ветчина, шампиньоны, моцарелла 315 189.000000 23.900000 9.30 6.10 25 370 50
4 Калининград /Kaliningrad Пицца-пирог pizza-pirog https://dodopizza.ru/Kaliningrad/Product/pizza... Сгущенное молоко, брусника, ананасы 315 144.899994 29.799999 2.90 2.70 25 420 50


Учитывая, что пищевая ценность продукта приводится в расчёте на 100 грамм, то для лучшего понимания — домножим их на массу пиццы.


df['pizza_kiloCalories'] = df.kiloCalories * df.weight / 100
df['pizza_carbohydrates'] = df.carbohydrates * df.weight / 100
df['pizza_proteins'] = df.proteins * df.weight / 100
df['pizza_fats'] = df.fats * df.weight / 100

df.describe()


pizza_price kiloCalories carbohydrates proteins fats size weight weight_err pizza_kiloCalories pizza_carbohydrates pizza_proteins pizza_fats
count 20.00000 20.000000 20.000000 20.000000 20.00000 20.0 20.000000 20.0 20.000000 20.000000 20.000000 20.000000
mean 370.50000 212.134491 25.443501 8.692500 8.44250 25.0 457.700000 50.0 969.942043 115.867950 39.857950 38.736650
std 33.16228 34.959122 2.204143 1.976283 3.20358 0.0 43.727746 0.0 175.835991 8.295421 9.989803 15.206275
min 315.00000 144.899994 22.100000 2.900000 2.70000 25.0 370.000000 50.0 608.579974 88.429999 12.180000 11.340000
25% 367.50000 188.250000 23.975000 7.975000 6.05000 25.0 420.000000 50.0 858.525000 113.010003 35.625000 28.159999
50% 385.00000 212.500000 24.950000 9.090000 8.20000 25.0 460.000000 50.0 966.358490 114.779002 39.580000 35.930001
75% 395.00000 235.527496 26.280001 9.800000 9.77500 25.0 485.000000 50.0 1095.459991 120.597001 45.707500 47.020001
max 395.00000 274.000000 31.330000 12.200000 14.80000 25.0 560.000000 50.0 1243.960000 128.453000 60.999999 68.080001


Настала пора узнать, какие же пиццы самые-самые...


Итак, самая калорийная пицца


df[df.pizza_kiloCalories == np.max(df.pizza_kiloCalories)]


city_name city_url pizza_name pizza_eng_name pizza_url pizza_contain pizza_price kiloCalories carbohydrates proteins fats size weight weight_err pizza_kiloCalories pizza_carbohydrates pizza_proteins pizza_fats
2 Калининград /Kaliningrad Дон Бекон pizza-don-bekon https://dodopizza.ru/Kaliningrad/Product/pizza... Томатный соус, бекон, пепперони, цыпленок, кра... 395 274.0 25.200001 9.8 14.8 25 454 50 1243.96 114.408003 44.492001 67.192001


Самая жирная пицца:


df[df.pizza_fats == np.max(df.pizza_fats)]


city_name city_url pizza_name pizza_eng_name pizza_url pizza_contain pizza_price kiloCalories carbohydrates proteins fats size weight weight_err pizza_kiloCalories pizza_carbohydrates pizza_proteins pizza_fats
14 Калининград /Kaliningrad Мясная myasnaya-pizza https://dodopizza.ru/Kaliningrad/Product/myasn... Томатный соус, охотничьи колбаски, бекон, ветч... 395 268.0 24.200001 9.1 14.8 25 460 50 1232.8 111.320004 41.860002 68.080001


Самая богатая углеводами:


df[df.pizza_carbohydrates == np.max(df.pizza_carbohydrates)]


city_name city_url pizza_name pizza_eng_name pizza_url pizza_contain pizza_price kiloCalories carbohydrates proteins fats size weight weight_err pizza_kiloCalories pizza_carbohydrates pizza_proteins pizza_fats
1 Калининград /Kaliningrad Крэйзи пицца crazy-pizza https://dodopizza.ru/Kaliningrad/Product/crazy... Томатный соус, увеличенные порции цыпленка и п... 395 232.369995 31.33 9.08 7.64 25 410 50 952.71698 128.453 37.228 31.323999


Самая богатая белками:


df[df.pizza_proteins == np.max(df.pizza_proteins)]


city_name city_url pizza_name pizza_eng_name pizza_url pizza_contain pizza_price kiloCalories carbohydrates proteins fats size weight weight_err pizza_kiloCalories pizza_carbohydrates pizza_proteins pizza_fats
7 Калининград /Kaliningrad Гавайская gavayskaya-pizza https://dodopizza.ru/Kaliningrad/Product/gavay... Томатный соус, ананасы, цыпленок, моцарелла 315 216.0 25.0 12.2 7.4 25 500 50 1080.0 125.0 60.999999 37.0


Самая тяжёлая по весу пицца:


df[df.weight == np.max(df.weight)]


city_name city_url pizza_name pizza_eng_name pizza_url pizza_contain pizza_price kiloCalories carbohydrates proteins fats size weight weight_err pizza_kiloCalories pizza_carbohydrates pizza_proteins pizza_fats
8 Калининград /Kaliningrad Додо pizza-dodo https://dodopizza.ru/Kaliningrad/Product/pizza... Томатный соус, говядина (фарш), ветчина, пеппе... 395 203.899994 22.1 8.6 8.9 25 560 50 1141.839966 123.760002 48.160002 49.839998


Самая лёгкая по весу пицца:


df[df.weight == np.min(df.weight)]


city_name city_url pizza_name pizza_eng_name pizza_url pizza_contain pizza_price kiloCalories carbohydrates proteins fats size weight weight_err pizza_kiloCalories pizza_carbohydrates pizza_proteins pizza_fats
3 Калининград /Kaliningrad Грибы и ветчина gribvetchina https://dodopizza.ru/Kaliningrad/Product/gribv... Томатный соус, ветчина, шампиньоны, моцарелла 315 189.0 23.9 9.3 6.1 25 370 50 699.3 88.429999 34.410001 22.57


Получаем названия пицц


pizza_names = df['pizza_name'].tolist()
pizza_eng_names = df['pizza_eng_name'].tolist()
print( pizza_eng_names )

['double-pepperoni', 'crazy-pizza', 'pizza-don-bekon', 'gribvetchina', 'pizza-pirog', 'pizza-margarita', 'syrnaya-pizza', 'gavayskaya-pizza', 'pizza-dodo', 'pizza-chetyre-sezona', 'ovoshi-i-griby', 'italyanskaya-pizza', 'meksikanskaya-pizza', 'morskaya-pizza', 'myasnaya-pizza', 'pizza-pepperoni', 'ranch-pizza', 'pizza-syrnyi-cyplenok', 'pizza-cyplenok-barbekyu', 'chizburger-pizza']

Получаем путь до картинок — нас интересует 3-я картинка (вид сверху)


image_paths = []
for name in pizza_eng_names:
    path = os.path.join(name, name+'3.jpg')
    image_paths.append(path)
print(image_paths)

['double-pepperoni\\double-pepperoni3.jpg', 'crazy-pizza\\crazy-pizza3.jpg', 'pizza-don-bekon\\pizza-don-bekon3.jpg', 'gribvetchina\\gribvetchina3.jpg', 'pizza-pirog\\pizza-pirog3.jpg', 'pizza-margarita\\pizza-margarita3.jpg', 'syrnaya-pizza\\syrnaya-pizza3.jpg', 'gavayskaya-pizza\\gavayskaya-pizza3.jpg', 'pizza-dodo\\pizza-dodo3.jpg', 'pizza-chetyre-sezona\\pizza-chetyre-sezona3.jpg', 'ovoshi-i-griby\\ovoshi-i-griby3.jpg', 'italyanskaya-pizza\\italyanskaya-pizza3.jpg', 'meksikanskaya-pizza\\meksikanskaya-pizza3.jpg', 'morskaya-pizza\\morskaya-pizza3.jpg', 'myasnaya-pizza\\myasnaya-pizza3.jpg', 'pizza-pepperoni\\pizza-pepperoni3.jpg', 'ranch-pizza\\ranch-pizza3.jpg', 'pizza-syrnyi-cyplenok\\pizza-syrnyi-cyplenok3.jpg', 'pizza-cyplenok-barbekyu\\pizza-cyplenok-barbekyu3.jpg', 'chizburger-pizza\\chizburger-pizza3.jpg']

Загружаем картинки


images = []
for path in image_paths:
    print('Load image:', path)
    image = cv2.imread(path)
    if image is not None:
        images.append(image)
    else:
        print('Error read image:', path)

Load image: double-pepperoni\double-pepperoni3.jpg
Load image: crazy-pizza\crazy-pizza3.jpg
Load image: pizza-don-bekon\pizza-don-bekon3.jpg
Load image: gribvetchina\gribvetchina3.jpg
Load image: pizza-pirog\pizza-pirog3.jpg
Load image: pizza-margarita\pizza-margarita3.jpg
Load image: syrnaya-pizza\syrnaya-pizza3.jpg
Load image: gavayskaya-pizza\gavayskaya-pizza3.jpg
Load image: pizza-dodo\pizza-dodo3.jpg
Load image: pizza-chetyre-sezona\pizza-chetyre-sezona3.jpg
Load image: ovoshi-i-griby\ovoshi-i-griby3.jpg
Load image: italyanskaya-pizza\italyanskaya-pizza3.jpg
Load image: meksikanskaya-pizza\meksikanskaya-pizza3.jpg
Load image: morskaya-pizza\morskaya-pizza3.jpg
Load image: myasnaya-pizza\myasnaya-pizza3.jpg
Load image: pizza-pepperoni\pizza-pepperoni3.jpg
Load image: ranch-pizza\ranch-pizza3.jpg
Load image: pizza-syrnyi-cyplenok\pizza-syrnyi-cyplenok3.jpg
Load image: pizza-cyplenok-barbekyu\pizza-cyplenok-barbekyu3.jpg
Load image: chizburger-pizza\chizburger-pizza3.jpg

Посмотрим на картинку


def plot_img(img):
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    plt.imshow(img_rgb)

print(images[0].shape)
plot_img(images[0])

(380, 710, 3)


Пиццы располагаются в одной и той же области — вырезаем


pizza_imgs = []
for img in images:
    y, x, height, width = 0, 165, 380, 380
    pizza_crop = img[y:y+height, x:x+width]
    pizza_imgs.append(pizza_crop)
print(pizza_imgs[0].shape)
print(len(pizza_imgs))
plot_img(pizza_imgs[0])

(380, 380, 3)
20


Посмотрим все фотографии


fig = plt.figure(figsize=(12,15))
for i in range(0, len(pizza_imgs)):
    fig.add_subplot(4,5,i+1)
    plot_img(pizza_imgs[i])


Пицца четыре сезона явно выбивается по своей структуре, так как, по сути, состоит из четырёх разных пицц.


Изучим ингредиенты


def split_contain(contain):
    lst = contain.split(',')
    print(len(lst),':', lst)

for i, row in df.iterrows():
    split_contain(row.pizza_contain)

2 : ['Томатный соус', ' двойная порция пепперони и увеличенная порция моцареллы']
4 : ['Томатный соус', ' увеличенные порции цыпленка и пепперони', ' моцарелла', ' кисло-сладкий соус']
6 : ['Томатный соус', ' бекон', ' пепперони', ' цыпленок', ' красный лук', ' моцарелла']
4 : ['Томатный соус', ' ветчина', ' шампиньоны', ' моцарелла']
3 : ['Сгущенное молоко', ' брусника', ' ананасы']
4 : ['Томатный соус', ' томаты', ' увеличенная порция моцареллы', ' орегано']
4 : ['Томатный соус', ' брынза', ' увеличенная порция сыра моцарелла', ' орегано']
4 : ['Томатный соус', ' ананасы', ' цыпленок', ' моцарелла']
9 : ['Томатный соус', ' говядина (фарш)', ' ветчина', ' пепперони', ' красный лук', ' маслины', ' сладкий перец', ' шампиньоны', ' моцарелла']
8 : ['Томатный соус', ' пепперони', ' ветчина', ' брынза', ' томаты', ' шампиньоны', ' моцарелла', ' орегано']
9 : ['Томатный соус', ' брынза', ' маслины', ' сладкий перец', ' томаты', ' шампиньоны', ' красный лук', ' моцарелла', ' базилик']
6 : ['Томатный соус', ' пепперони', ' маслины', ' шампиньоны', ' моцарелла', ' орегано']
8 : ['Томатный соус', ' халапеньо', ' сладкий перец', ' цыпленок', ' томаты', ' шампиньоны', ' красный лук', ' моцарелла']
6 : ['Томатный соус', ' креветки', ' маслины', ' сладкий перец', ' красный лук', ' моцарелла']
5 : ['Томатный соус', ' охотничьи колбаски', ' бекон', ' ветчина', ' моцарелла']
3 : ['Томатный соус', ' пепперони', ' увеличенная порция моцареллы']
6 : ['Соус Ранч', ' цыпленок', ' ветчина', ' томаты', ' чеснок', ' моцарелла']
4 : ['Сырный соус', ' цыпленок', ' томаты', ' моцарелла']
6 : ['Томатный соус', ' цыпленок', ' бекон', ' красный лук', ' моцарелла', ' соус Барбекю']
7 : ['Сырный соус', ' говядина', ' бекон', ' соленые огурцы', ' томаты', ' красный лук', ' моцарелла']

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


  • "двойная порция"
  • "увеличенная порция"

При этом, после модификаторов может идти перечисление ингредиетов через союз И.


Гипотезы:


  • модификатор двойная порция можно заменить указав нужный ингредиент два раза.
  • модификатор увеличенная порция пока можно опустить (так как на фотографиях разница не заметна)

Видим, что модификатор "увеличенная порция", относится только к сыру моцарелла.
Так же один раз встречается:
"увеличенная порция сыра моцарелла"


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


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


def split_contain2(contain):
    lst = contain.split(',')
    #print(len(lst),':', lst)
    for i in range(len(lst)):
        item = lst[i]
        item = item.replace('увеличенная порция', '')
        item = item.replace('увеличенные порции', '')
        item = item.replace('сыра моцарелла', 'моцарелла')
        item = item.replace('моцареллы', 'моцарелла')
        item = item.replace('цыпленка', 'цыпленок')
        and_pl = item.find(' и ')
        if and_pl != -1:
            item1 = item[0:and_pl]
            item2 = item[and_pl+3:]
            item = item1
            lst.insert(i+1, item2.strip())
        double_pl = item.find('двойная порция ')
        if double_pl != -1:
            item = item[double_pl+15:]
            lst.insert(i+1, item.strip())
        lst[i] = item.strip()
    # last one
    for i in range(len(lst)):
        lst[i] = lst[i].strip()
    print(len(lst),':', lst)
    return lst

ingredients = []
ingredients_count = []
for i, row in df.iterrows():
    print(row.pizza_name)
    lst = split_contain2(row.pizza_contain)
    ingredients.append(lst)
    ingredients_count.append(len(lst))
ingredients_count

Двойная пепперони
4 : ['Томатный соус', 'пепперони', 'пепперони', 'моцарелла']
Крэйзи пицца 
5 : ['Томатный соус', 'цыпленок', 'пепперони', 'моцарелла', 'кисло-сладкий соус']
Дон Бекон
6 : ['Томатный соус', 'бекон', 'пепперони', 'цыпленок', 'красный лук', 'моцарелла']
Грибы и ветчина
4 : ['Томатный соус', 'ветчина', 'шампиньоны', 'моцарелла']
Пицца-пирог
3 : ['Сгущенное молоко', 'брусника', 'ананасы']
Маргарита
4 : ['Томатный соус', 'томаты', 'моцарелла', 'орегано']
Сырная
4 : ['Томатный соус', 'брынза', 'моцарелла', 'орегано']
Гавайская
4 : ['Томатный соус', 'ананасы', 'цыпленок', 'моцарелла']
Додо
9 : ['Томатный соус', 'говядина (фарш)', 'ветчина', 'пепперони', 'красный лук', 'маслины', 'сладкий перец', 'шампиньоны', 'моцарелла']
Четыре сезона
8 : ['Томатный соус', 'пепперони', 'ветчина', 'брынза', 'томаты', 'шампиньоны', 'моцарелла', 'орегано']
Овощи и грибы
9 : ['Томатный соус', 'брынза', 'маслины', 'сладкий перец', 'томаты', 'шампиньоны', 'красный лук', 'моцарелла', 'базилик']
Итальянская
6 : ['Томатный соус', 'пепперони', 'маслины', 'шампиньоны', 'моцарелла', 'орегано']
Мексиканская
8 : ['Томатный соус', 'халапеньо', 'сладкий перец', 'цыпленок', 'томаты', 'шампиньоны', 'красный лук', 'моцарелла']
Морская
6 : ['Томатный соус', 'креветки', 'маслины', 'сладкий перец', 'красный лук', 'моцарелла']
Мясная
5 : ['Томатный соус', 'охотничьи колбаски', 'бекон', 'ветчина', 'моцарелла']
Пепперони
3 : ['Томатный соус', 'пепперони', 'моцарелла']
Ранч пицца
6 : ['Соус Ранч', 'цыпленок', 'ветчина', 'томаты', 'чеснок', 'моцарелла']
Сырный цыплёнок
4 : ['Сырный соус', 'цыпленок', 'томаты', 'моцарелла']
Цыплёнок барбекю
6 : ['Томатный соус', 'цыпленок', 'бекон', 'красный лук', 'моцарелла', 'соус Барбекю']
Чизбургер-пицца
7 : ['Сырный соус', 'говядина', 'бекон', 'соленые огурцы', 'томаты', 'красный лук', 'моцарелла']

[4, 5, 6, 4, 3, 4, 4, 4, 9, 8, 9, 6, 8, 6, 5, 3, 6, 4, 6, 7]

Посмотрим минимальное и максимальное количество ингредиентов


min_count = np.min(ingredients_count)
print('min:', min_count)
max_count = np.max(ingredients_count)
print('max:', max_count)

min: 3
max: 9

print('min:', np.array(pizza_names)[ingredients_count == min_count] )
print('max:', np.array(pizza_names)[ingredients_count == max_count] )

min: ['Пицца-пирог' 'Пепперони']
max: ['Додо' 'Овощи и грибы']

Интересно, больше всего ингредиентов (9 штук) в пиццах: Додо и Овощи и грибы.


Заполним табличку ингредиентов.


df_ingredients = pd.DataFrame(ingredients)
df_ingredients.fillna(value='0', inplace=True)
df_ingredients


0 1 2 3 4 5 6 7 8
0 Томатный соус пепперони пепперони моцарелла 0 0 0 0 0
1 Томатный соус цыпленок пепперони моцарелла кисло-сладкий соус 0 0 0 0
2 Томатный соус бекон пепперони цыпленок красный лук моцарелла 0 0 0
3 Томатный соус ветчина шампиньоны моцарелла 0 0 0 0 0
4 Сгущенное молоко брусника ананасы 0 0 0 0 0 0
5 Томатный соус томаты моцарелла орегано 0 0 0 0 0
6 Томатный соус брынза моцарелла орегано 0 0 0 0 0
7 Томатный соус ананасы цыпленок моцарелла 0 0 0 0 0
8 Томатный соус говядина (фарш) ветчина пепперони красный лук маслины сладкий перец шампиньоны моцарелла
9 Томатный соус пепперони ветчина брынза томаты шампиньоны моцарелла орегано 0
10 Томатный соус брынза маслины сладкий перец томаты шампиньоны красный лук моцарелла базилик
11 Томатный соус пепперони маслины шампиньоны моцарелла орегано 0 0 0
12 Томатный соус халапеньо сладкий перец цыпленок томаты шампиньоны красный лук моцарелла 0
13 Томатный соус креветки маслины сладкий перец красный лук моцарелла 0 0 0
14 Томатный соус охотничьи колбаски бекон ветчина моцарелла 0 0 0 0
15 Томатный соус пепперони моцарелла 0 0 0 0 0 0
16 Соус Ранч цыпленок ветчина томаты чеснок моцарелла 0 0 0
17 Сырный соус цыпленок томаты моцарелла 0 0 0 0 0
18 Томатный соус цыпленок бекон красный лук моцарелла соус Барбекю 0 0 0
19 Сырный соус говядина бекон соленые огурцы томаты красный лук моцарелла 0 0


df_ingredients.describe()


0 1 2 3 4 5 6 7 8
count 20 20 20 20 20 20 20 20 20
unique 4 13 10 12 6 7 4 4 3
top Томатный соус пепперони пепперони моцарелла 0 0 0 0 0
freq 16 4 3 5 8 10 15 16 18


Как и ожидалось — самый используемый соус — томатный. Стандартный рецепт — состоит из 4 ингредиентов.


Забавно, что образовался новый рецепт для пиццы.


Посмотрим сколько раз встречается тот или иной ингредиент:


df_ingredients.stack().value_counts()

0                     69
моцарелла             19
Томатный соус         16
пепперони              8
томаты                 7
цыпленок               7
красный лук            7
шампиньоны             6
ветчина                5
сладкий перец          4
бекон                  4
орегано                4
маслины                4
брынза                 3
ананасы                2
Сырный соус            2
чеснок                 1
кисло-сладкий соус     1
базилик                1
соус Барбекю           1
креветки               1
халапеньо              1
Сгущенное молоко       1
соленые огурцы         1
говядина (фарш)        1
охотничьи колбаски     1
брусника               1
говядина               1
Соус Ранч              1
dtype: int64

Опять же: моцарелла, Томатный соус, пепперони.


df_ingredients.stack().value_counts().drop('0').plot.pie()

<matplotlib.axes._subplots.AxesSubplot at 0xea8d358>


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


from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import OneHotEncoder

ingredients_full = df_ingredients.values.tolist()

# flatten lists
flat_ingredients = [item for sublist in ingredients_full for item in sublist]
print(flat_ingredients)
print(len(flat_ingredients))

np_ingredients = np.array(flat_ingredients)
#print(np_ingredients)

labelencoder = LabelEncoder()
ingredients_encoded = labelencoder.fit_transform(np_ingredients)
print(ingredients_encoded)

label_max = np.max(ingredients_encoded)
print('max:', label_max)

['Томатный соус', 'пепперони', 'пепперони', 'моцарелла', '0', '0', '0', '0', '0', 'Томатный соус', 'цыпленок', 'пепперони', 'моцарелла', 'кисло-сладкий соус', '0', '0', '0', '0', 'Томатный соус', 'бекон', 'пепперони', 'цыпленок', 'красный лук', 'моцарелла', '0', '0', '0', 'Томатный соус', 'ветчина', 'шампиньоны', 'моцарелла', '0', '0', '0', '0', '0', 'Сгущенное молоко', 'брусника', 'ананасы', '0', '0', '0', '0', '0', '0', 'Томатный соус', 'томаты', 'моцарелла', 'орегано', '0', '0', '0', '0', '0', 'Томатный соус', 'брынза', 'моцарелла', 'орегано', '0', '0', '0', '0', '0', 'Томатный соус', 'ананасы', 'цыпленок', 'моцарелла', '0', '0', '0', '0', '0', 'Томатный соус', 'говядина (фарш)', 'ветчина', 'пепперони', 'красный лук', 'маслины', 'сладкий перец', 'шампиньоны', 'моцарелла', 'Томатный соус', 'пепперони', 'ветчина', 'брынза', 'томаты', 'шампиньоны', 'моцарелла', 'орегано', '0', 'Томатный соус', 'брынза', 'маслины', 'сладкий перец', 'томаты', 'шампиньоны', 'красный лук', 'моцарелла', 'базилик', 'Томатный соус', 'пепперони', 'маслины', 'шампиньоны', 'моцарелла', 'орегано', '0', '0', '0', 'Томатный соус', 'халапеньо', 'сладкий перец', 'цыпленок', 'томаты', 'шампиньоны', 'красный лук', 'моцарелла', '0', 'Томатный соус', 'креветки', 'маслины', 'сладкий перец', 'красный лук', 'моцарелла', '0', '0', '0', 'Томатный соус', 'охотничьи колбаски', 'бекон', 'ветчина', 'моцарелла', '0', '0', '0', '0', 'Томатный соус', 'пепперони', 'моцарелла', '0', '0', '0', '0', '0', '0', 'Соус Ранч', 'цыпленок', 'ветчина', 'томаты', 'чеснок', 'моцарелла', '0', '0', '0', 'Сырный соус', 'цыпленок', 'томаты', 'моцарелла', '0', '0', '0', '0', '0', 'Томатный соус', 'цыпленок', 'бекон', 'красный лук', 'моцарелла', 'соус Барбекю', '0', '0', '0', 'Сырный соус', 'говядина', 'бекон', 'соленые огурцы', 'томаты', 'красный лук', 'моцарелла', '0', '0']
180
[ 4 20 20 17  0  0  0  0  0  4 26 20 17 13  0  0  0  0  4  7 20 26 14 17  0
  0  0  4 10 28 17  0  0  0  0  0  1  8  5  0  0  0  0  0  0  4 24 17 18  0
  0  0  0  0  4  9 17 18  0  0  0  0  0  4  5 26 17  0  0  0  0  0  4 12 10
 20 14 16 21 28 17  4 20 10  9 24 28 17 18  0  4  9 16 21 24 28 14 17  6  4
 20 16 28 17 18  0  0  0  4 25 21 26 24 28 14 17  0  4 15 16 21 14 17  0  0
  0  4 19  7 10 17  0  0  0  0  4 20 17  0  0  0  0  0  0  2 26 10 24 27 17
  0  0  0  3 26 24 17  0  0  0  0  0  4 26  7 14 17 23  0  0  0  3 11  7 22
 24 14 17  0  0]
max: 28

Получается, что для приготовления, используется целых 27 ингредиентов.


for label in range(label_max):
    print(label, labelencoder.inverse_transform(label))

0 0
1 Сгущенное молоко
2 Соус Ранч
3 Сырный соус
4 Томатный соус
5 ананасы
6 базилик
7 бекон
8 брусника
9 брынза
10 ветчина
11 говядина
12 говядина (фарш)
13 кисло-сладкий соус
14 красный лук
15 креветки
16 маслины
17 моцарелла
18 орегано
19 охотничьи колбаски
20 пепперони
21 сладкий перец
22 соленые огурцы
23 соус Барбекю
24 томаты
25 халапеньо
26 цыпленок
27 чеснок

lb_ingredients = []
for lst in ingredients_full:
    lb_ingredients.append(labelencoder.transform(lst).tolist())
#lb_ingredients = np.array(lb_ingredients)
lb_ingredients

[[4, 20, 20, 17, 0, 0, 0, 0, 0],
 [4, 26, 20, 17, 13, 0, 0, 0, 0],
 [4, 7, 20, 26, 14, 17, 0, 0, 0],
 [4, 10, 28, 17, 0, 0, 0, 0, 0],
 [1, 8, 5, 0, 0, 0, 0, 0, 0],
 [4, 24, 17, 18, 0, 0, 0, 0, 0],
 [4, 9, 17, 18, 0, 0, 0, 0, 0],
 [4, 5, 26, 17, 0, 0, 0, 0, 0],
 [4, 12, 10, 20, 14, 16, 21, 28, 17],
 [4, 20, 10, 9, 24, 28, 17, 18, 0],
 [4, 9, 16, 21, 24, 28, 14, 17, 6],
 [4, 20, 16, 28, 17, 18, 0, 0, 0],
 [4, 25, 21, 26, 24, 28, 14, 17, 0],
 [4, 15, 16, 21, 14, 17, 0, 0, 0],
 [4, 19, 7, 10, 17, 0, 0, 0, 0],
 [4, 20, 17, 0, 0, 0, 0, 0, 0],
 [2, 26, 10, 24, 27, 17, 0, 0, 0],
 [3, 26, 24, 17, 0, 0, 0, 0, 0],
 [4, 26, 7, 14, 17, 23, 0, 0, 0],
 [3, 11, 7, 22, 24, 14, 17, 0, 0]]

onehotencoder = OneHotEncoder(sparse=False)
ingredients_onehotencoded = onehotencoder.fit_transform(ingredients_encoded.reshape(-1, 1))
print(ingredients_onehotencoded.shape)
ingredients_onehotencoded[0]

(180, 29)

array([ 0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
        0.,  0.,  0.])

Теперь у нас есть данные с которыми мы можем работать.


Автоэнкодер


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


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline 

import seaborn as sns

np.random.seed(42)

import cv2
import os
import sys

import load_data
import prepare_images

Загружаем фотографии пицц


pizza_eng_names, pizza_imgs = prepare_images.load_photos()

Read csv...
(20, 13)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20 entries, 0 to 19
Data columns (total 13 columns):
city_name         20 non-null object
city_url          20 non-null object
pizza_name        20 non-null object
pizza_eng_name    20 non-null object
pizza_url         20 non-null object
pizza_contain     20 non-null object
pizza_price       20 non-null int64
kiloCalories      20 non-null object
carbohydrates     20 non-null object
proteins          20 non-null object
fats              20 non-null object
size              20 non-null int64
weight            20 non-null object
dtypes: int64(2), object(11)
memory usage: 2.1+ KB
None
['double-pepperoni', 'crazy-pizza', 'pizza-don-bekon', 'gribvetchina', 'pizza-pirog', 'pizza-margarita', 'syrnaya-pizza', 'gavayskaya-pizza', 'pizza-dodo', 'pizza-chetyre-sezona', 'ovoshi-i-griby', 'italyanskaya-pizza', 'meksikanskaya-pizza', 'morskaya-pizza', 'myasnaya-pizza', 'pizza-pepperoni', 'ranch-pizza', 'pizza-syrnyi-cyplenok', 'pizza-cyplenok-barbekyu', 'chizburger-pizza']
['double-pepperoni\\double-pepperoni3.jpg', 'crazy-pizza\\crazy-pizza3.jpg', 'pizza-don-bekon\\pizza-don-bekon3.jpg', 'gribvetchina\\gribvetchina3.jpg', 'pizza-pirog\\pizza-pirog3.jpg', 'pizza-margarita\\pizza-margarita3.jpg', 'syrnaya-pizza\\syrnaya-pizza3.jpg', 'gavayskaya-pizza\\gavayskaya-pizza3.jpg', 'pizza-dodo\\pizza-dodo3.jpg', 'pizza-chetyre-sezona\\pizza-chetyre-sezona3.jpg', 'ovoshi-i-griby\\ovoshi-i-griby3.jpg', 'italyanskaya-pizza\\italyanskaya-pizza3.jpg', 'meksikanskaya-pizza\\meksikanskaya-pizza3.jpg', 'morskaya-pizza\\morskaya-pizza3.jpg', 'myasnaya-pizza\\myasnaya-pizza3.jpg', 'pizza-pepperoni\\pizza-pepperoni3.jpg', 'ranch-pizza\\ranch-pizza3.jpg', 'pizza-syrnyi-cyplenok\\pizza-syrnyi-cyplenok3.jpg', 'pizza-cyplenok-barbekyu\\pizza-cyplenok-barbekyu3.jpg', 'chizburger-pizza\\chizburger-pizza3.jpg']
Load images...
Load image: double-pepperoni\double-pepperoni3.jpg
Load image: crazy-pizza\crazy-pizza3.jpg
Load image: pizza-don-bekon\pizza-don-bekon3.jpg
Load image: gribvetchina\gribvetchina3.jpg
Load image: pizza-pirog\pizza-pirog3.jpg
Load image: pizza-margarita\pizza-margarita3.jpg
Load image: syrnaya-pizza\syrnaya-pizza3.jpg
Load image: gavayskaya-pizza\gavayskaya-pizza3.jpg
Load image: pizza-dodo\pizza-dodo3.jpg
Load image: pizza-chetyre-sezona\pizza-chetyre-sezona3.jpg
Load image: ovoshi-i-griby\ovoshi-i-griby3.jpg
Load image: italyanskaya-pizza\italyanskaya-pizza3.jpg
Load image: meksikanskaya-pizza\meksikanskaya-pizza3.jpg
Load image: morskaya-pizza\morskaya-pizza3.jpg
Load image: myasnaya-pizza\myasnaya-pizza3.jpg
Load image: pizza-pepperoni\pizza-pepperoni3.jpg
Load image: ranch-pizza\ranch-pizza3.jpg
Load image: pizza-syrnyi-cyplenok\pizza-syrnyi-cyplenok3.jpg
Load image: pizza-cyplenok-barbekyu\pizza-cyplenok-barbekyu3.jpg
Load image: chizburger-pizza\chizburger-pizza3.jpg
Cut pizza from images...
(380, 380, 3)
20

def plot_img(img):
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    plt.imshow(img_rgb)

plot_img(pizza_imgs[0])


Замечательно, что пицца обладает осевой симметрией — для аугментации, её можно будет вращать вокруг центра, а так же отражать по вертикали.


img_flipy = cv2.flip(pizza_imgs[0], 1)
plot_img(img_flipy)


Результат поворота вокруг центра на 15 градусов:


img_rot15 = load_data.rotate(pizza_imgs[0], 15)
plot_img(img_rot15)


Сделаем аугментацию для первой пиццы в списке (предварительно уменьшив до размеров: 56 на 56) — вращение вокруг оси на 360 градусов с шагом в 1 градус и отражением по вертикали.


channels, height, width = 3, 56, 56

lst0 = load_data.resize_rotate_flip(pizza_imgs[0], (height, width))
print(len(lst0))

720

plot_img(lst0[0])


Попробуем автоэнкодер


Преобразуем список с картинками к нужному виду и разобьём его на тренировочную и тестовую выборки.


image_list = lst0
image_list = np.array(image_list, dtype=np.float32)
image_list = image_list.transpose((0, 3, 1, 2))
image_list /= 255.0
print(image_list.shape)

(720, 3, 56, 56)

x_train = image_list[:600]
x_test = image_list[600:]
print(x_train.shape, x_test.shape)

(600, 3, 56, 56) (120, 3, 56, 56)

from keras.models import Model
from keras.layers import Input, Dense, Flatten, Reshape
from keras.layers import Conv2D, MaxPooling2D, UpSampling2D

from keras import backend as K
#For 2D data (e.g. image), "channels_last" assumes (rows, cols, channels) while "channels_first" assumes  (channels, rows, cols).
K.set_image_data_format('channels_first')

Using Theano backend.

Создаём автоэнкодер


def create_deep_conv_ae(channels, height, width):
    input_img = Input(shape=(channels, height, width))

    x = Conv2D(16, (3, 3), activation='relu', padding='same')(input_img)
    x = MaxPooling2D(pool_size=(2, 2), padding='same')(x)
    x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
    encoded = MaxPooling2D(pool_size=(2, 2), padding='same')(x)

    # at this point the representation is (8, 14, 14)

    input_encoded = Input(shape=(8, 14, 14))
    x = Conv2D(8, (3, 3), activation='relu', padding='same')(input_encoded)
    x = UpSampling2D((2, 2))(x)
    x = Conv2D(16, (3, 3), activation='relu', padding='same')(x)
    x = UpSampling2D((2, 2))(x)
    decoded = Conv2D(channels, (3, 3), activation='sigmoid', padding='same')(x)

    # Models
    encoder = Model(input_img, encoded, name="encoder")
    decoder = Model(input_encoded, decoded, name="decoder")
    autoencoder = Model(input_img, decoder(encoder(input_img)), name="autoencoder")
    return encoder, decoder, autoencoder

c_encoder, c_decoder, c_autoencoder = create_deep_conv_ae(channels, height, width)
c_autoencoder.compile(optimizer='adam', loss='binary_crossentropy')

c_encoder.summary()
c_decoder.summary()
c_autoencoder.summary()

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 3, 56, 56)         0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 16, 56, 56)        448       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 16, 28, 28)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 8, 28, 28)         1160      
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 8, 14, 14)         0         
=================================================================
Total params: 1,608
Trainable params: 1,608
Non-trainable params: 0
_________________________________________________________________
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         (None, 8, 14, 14)         0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 8, 14, 14)         584       
_________________________________________________________________
up_sampling2d_1 (UpSampling2 (None, 8, 28, 28)         0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 16, 28, 28)        1168      
_________________________________________________________________
up_sampling2d_2 (UpSampling2 (None, 16, 56, 56)        0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 3, 56, 56)         435       
=================================================================
Total params: 2,187
Trainable params: 2,187
Non-trainable params: 0
_________________________________________________________________
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 3, 56, 56)         0         
_________________________________________________________________
encoder (Model)              (None, 8, 14, 14)         1608      
_________________________________________________________________
decoder (Model)              (None, 3, 56, 56)         2187      
=================================================================
Total params: 3,795
Trainable params: 3,795
Non-trainable params: 0
_________________________________________________________________

c_autoencoder.fit(x_train, x_train,
                epochs=20,
                batch_size=16,
                shuffle=True,
                verbose=2,
                validation_data=(x_test, x_test))

Train on 600 samples, validate on 120 samples
Epoch 1/20
10s - loss: 0.5840 - val_loss: 0.5305
Epoch 2/20
10s - loss: 0.4571 - val_loss: 0.4162
Epoch 3/20
9s - loss: 0.4032 - val_loss: 0.3956
Epoch 4/20
8s - loss: 0.3884 - val_loss: 0.3855
Epoch 5/20
10s - loss: 0.3829 - val_loss: 0.3829
Epoch 6/20
11s - loss: 0.3808 - val_loss: 0.3815
Epoch 7/20
9s - loss: 0.3795 - val_loss: 0.3804
Epoch 8/20
8s - loss: 0.3785 - val_loss: 0.3797
Epoch 9/20
10s - loss: 0.3778 - val_loss: 0.3787
Epoch 10/20
10s - loss: 0.3771 - val_loss: 0.3781
Epoch 11/20
9s - loss: 0.3764 - val_loss: 0.3779
Epoch 12/20
8s - loss: 0.3760 - val_loss: 0.3773
Epoch 13/20
9s - loss: 0.3756 - val_loss: 0.3768
Epoch 14/20
10s - loss: 0.3751 - val_loss: 0.3766
Epoch 15/20
10s - loss: 0.3748 - val_loss: 0.3768
Epoch 16/20
9s - loss: 0.3745 - val_loss: 0.3762
Epoch 17/20
10s - loss: 0.3741 - val_loss: 0.3755
Epoch 18/20
9s - loss: 0.3738 - val_loss: 0.3754
Epoch 19/20
11s - loss: 0.3735 - val_loss: 0.3752
Epoch 20/20
8s - loss: 0.3733 - val_loss: 0.3748

<keras.callbacks.History at 0x262db6a0>

#c_autoencoder.save_weights('c_autoencoder_weights.h5')
#c_autoencoder.load_weights('c_autoencoder_weights.h5')

Посмотрим на результаты работы автоэнкодера


n = 5
imgs = x_test[:n]
encoded_imgs = c_encoder.predict(imgs, batch_size=n)
decoded_imgs = c_decoder.predict(encoded_imgs, batch_size=n)

def get_image_from_net_data(data):
    res = data.transpose((1, 2, 0))
    res *= 255.0
    res = np.array(res, dtype=np.uint8)
    return res

#image0 = get_image_from_net_data(decoded_imgs[0])
#plot_img(image0)

fig = plt.figure()
j = 0
for i in range(0, len(imgs)):
    j += 1
    fig.add_subplot(n,2,j)
    plot_img( get_image_from_net_data(imgs[i]) )
    j += 1
    fig.add_subplot(n,2,j)
    plot_img( get_image_from_net_data(decoded_imgs[i]) )


продолжение: Часть 2: Состязание нейронных сетей


Ссылки


Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 22: ↑21 и ↓1+20
Комментарии4

Публикации

Истории

Работа

Data Scientist
78 вакансий
Python разработчик
121 вакансия

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань