IT-инфраструктура для бизнеса и творчества

Как приготовить 10 000 меню за три недели, когда есть Data Science

В период перехода на удалёнку команда Data Science 2ГИС придумала решение, как быстро приготовить красивые меню для 10 000 ресторанов и кафе более чем в 100 городах.

Придумываем блюдо

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

С RunFlat можно ехать даже после прокола

Оценив востребованность, решили развивать поиск в новых рубриках.

Основной камень преткновения — полнота данных. Чтобы поиск давал исчерпывающие результаты, нужны цены очень многих компаний. И желательно в машиночитаемом формате. Но у одних компаний нет сайта — значит, они не могут дать ссылку на прайс-лист. У других не настроена автоматическая выгрузка из 1С. Решение с нашим FTP тоже не взлетело, так как многие срезались на стадии формата файлов.

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

Весной этого года мы готовились к релизу. Оставалось несколько недель работы, как пришёл COVID-19 и последующий за ним карантин. И исходя из новых условий, мы сфокусировались на самых востребованных категориях: «еда» и «лекарства».

Отправляем мини-паука

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

В 2ГИС несколько десятков тысяч сайтов заведений общепита. И примерно треть — с ценами на блюда. И было очень логично взять цены с этих сайтов.

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

Классифицируем данные

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

  • название блюда,
  • описание или ингредиенты,
  • другая строка.

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

В одном из лучших мест Новосибирска пицца бывает не только с ананасами

При помощи headless-браузера мы скачали несколько сотен гигабайт страниц с сайтов общепита и сохранили computed style каждого html-элемента. Так мы получили возможность определять взаимное расположение элементов для необходимых строк.

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

Готовим датасет

Датасет собирали самостоятельно.

Класс «название»
Названия взяли из рецептов блюд в различных сборниках. Выучили простой классификатор, а на основе скачанных данных делали разметку.

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

Салат — это блюдо или ингредиент? А моцарелла с томатами — описание или название?

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

Класс «остальное»
Собрать такие данные легче всего, так как до этого мы уже собирали данные с сайтов других категорий — к примеру, тех же СТО. Требовалось только итерационно очистить этот класс от названий и описаний.

Замешиваем обучение

Пробовали различные варианты моделей: от классических регрессий до претрейна Bert. Самым стабильным и предсказуемым вариантом на наших данных оказался vowpal wabbit на различных текстовых фичах и их комбинациях.

Когда модель начала давать приемлемое качество, сделали кластеризацию найденных строк по визуальному расположению и обязательному наличию цифрового токена, похожего на цену. Кому интересны значения, «приемлемое качество» — F1 0.90+.

Для классификации «цена»/«не цена» использовали набор эвристик: название html-класса, наличие символов, указывающих на валюту, и исключение токенов с явным указанием граммовки и калорий.

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

Убираем алкоголь

Выпускать в таком виде мы не могли из-за различного мусора и запрещённых товаров вроде алкоголя.

Потребовалось сделать ещё две модели. Для мусора — классификатор мусора, где мы улучшали бинарную классификацию «блюдо»/«не блюдо». А для алкоголя… классификатор алкоголя.

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

Раскладываем на категории

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

Как и с другими задачами — разметили данные и итеративно улучшали метрики и «внешний вид» меню.

Добавляем фото

Отдельная задача — выбор фотографии для блюда.

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

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

Подаём на бой

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

Грузинская кухня — приятный способ приобретения антитела

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

{ "author_name": "Sergey Kolomenkin", "author_type": "self", "tags": ["2\u0433\u0438\u0441"], "comments": 7, "likes": 34, "favorites": 29, "is_advertisement": false, "subsite_label": "dev", "id": 178602, "is_wide": true, "is_ugc": true, "date": "Thu, 19 Nov 2020 10:41:26 +0300", "is_special": false }
(function () { let cdnUrl = `https://specialsf378ef5-a.akamaihd.net/SelectelBranding/images/` let previousArticleNumber = null let currentArticleNumber = 0 let platform = 'Desktop' let articles = [ // { // name: 'camera', // url: `${cdnUrl}CameraCat`, // text: 'умную камеру для\u00A0наблюдения за\u00A0котиками', // link: '1', // num: 3 // }, { name: 'chill', url: `${cdnUrl}ChillCat`, text: 'трекер, который подскажет, когда пора отдохнуть', link: 'https://vc.ru/promo/288561-eye-tracker', num: 1 }, { name: 'cloud', url: `${cdnUrl}CloudCat`, text: 'котика: даёшь ему «пять», а\u00A0он делает бэкап в облако', link: 'https://vc.ru/dev/294799-maneki-neko', num: 2 } ] let buttonCycle = document.querySelector('.button--cycle') let buttonChoose = document.querySelector('.button--choose') let buttonMobile = document.querySelector('.button--mobile') let textField = document.querySelector('.selectel-footer-subtitle') let imageAgent = document.querySelector('.image--agent') let banner = document.querySelector('.selectel-footer') buttonCycle.addEventListener('click', cycleClick) buttonChoose.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) buttonMobile.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) let media = window.matchMedia("(max-width: 570px)") media.addEventListener('change', matchMedia) function matchMedia() { if (media.matches) { platform = 'Mobile' } else { platform = 'Desktop' } update() } matchMedia() function cycleClick(event) { sendEvent(`Promo ${articles[currentArticleNumber].num} Right`, 'Click') if (event) { event.preventDefault() event.stopPropagation() } window.open('https://vc.ru/tag/selectelDIY', '_blank') //cycle(event) } function cycle(event) { // incrementArticleNumber() textField.innerHTML = generatedText() imageAgent.src = articles[currentArticleNumber].url + platform + '.svg?3' imageAgent.setAttribute("class", "") imageAgent.classList.add('image--agent', articles[currentArticleNumber].name) banner.href = articles[currentArticleNumber].link } function update() { banner.href = articles[currentArticleNumber].link imageAgent.src = articles[currentArticleNumber].url + platform + '.svg' textField.innerHTML = generatedText() } function incrementArticleNumber() { previousArticleNumber = currentArticleNumber if (currentArticleNumber >= articles.length - 1) { currentArticleNumber = 0 } else { currentArticleNumber++ } } const sendEvent = (label, action = 'Click') => { const value = `SelectelDIY — loc: Footer — ${label} — ${action}`; if (window.dataLayer !== undefined) { window.dataLayer.push({ event: 'data_event', data_description: value, }); } }; function generatedText() { let defaultText if (platform === 'Desktop') { defaultText = `Мы тут собрали %text%. Хотите научим?` } else { defaultText = `Мы тут собрали %text%.` } return defaultText.replace('%text%', articles[currentArticleNumber].text) } function getRandom(min, max) { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min } (function create() { currentArticleNumber = getRandom(0, articles.length - 1) cycle() let page = document.querySelector('.page--entry') if (page) { function insertAfter() { let parents = page.querySelectorAll('[data-id="7"]') let referenceNode = parents[0] referenceNode.parentNode.insertBefore(banner, referenceNode.nextSibling); loaded() } setTimeout(() => insertAfter(), 0) } }()) function loaded() { banner.classList.add('loaded') } loadImages([ `${cdnUrl}CameraCatDesktop.svg`, `${cdnUrl}ChillCatDesktop.svg`, `${cdnUrl}CloudCatDesktop.svg`, `${cdnUrl}CameraCatMobile.svg`, `${cdnUrl}ChillCatMobile.svg`, `${cdnUrl}CloudCatMobile.svg?3`, ]) function loadImages(urls) { return Promise.all(urls.map(function (url) { return new Promise(function (resolve) { var img = document.createElement('img'); img.onload = resolve; img.onerror = resolve; img.src = url; }); })); } }())
0
7 комментариев
Популярные
По порядку
Написать комментарий...

Команда, вы супер! Гениально, мощно, очень быстро. Лучшее MVP, что я знаю из такого обьема 🔥

4

Есть ли официально сам 2Gis на VC? Узнал о посте через ваш канал: https://www.instagram.com/p/CHxI2xgFs4c

2

Пока нет. Прощупываем )

2

Как вам идея добавить в 2 гис функционал аналога приложения Citizen, чтобы пользователи могли добавлять контент с мест событий?

0

Пока не планировали.

0

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

0

В ближайших планах не планировали.

0
Читать все 7 комментариев
«Что вы нам предлагаете. Это всё шарлатанство. Ничего не сработает». Как УБРиР меняет подход к технологиям изнутри
Крохотные кулер, буфет и кондиционер: как развивается японский рынок миниатюр «гачапон» Статьи редакции

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

Мини-автомат с содовой Toys Spirits
Hashbon Rocket — децентрализованная платформа кросс-блокчейн обмена токенов ERC-20 на BEP-20

История создания платформы Hashbon Rocket от начала запуска до покорения DeFi рынка

Делай бэкап, или как мы чуть не потеряли самое ценное, что у нас было

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

В Москве обязали компании перевести на удалёнку 30% сотрудников с 25 октября по 25 февраля Статьи редакции

И всех сотрудников старше 60 лет и страдающих хроническими заболеваниями.

Как я умоляю Samsung уже два месяца взять мои 160 000 рублей

11 августа 2021 смотрю презентацию Samsung Galaxy Z Fold3 5G, после чего решаю что надо брать, ибо девайс интересный, а мне давно пора было поменять свой рабочий телефон. OMG, давно я так не ошибался…

1₽ на хорошее дело: как работает Добрая подписка от Альфа-Банка
До AI Journey осталось меньше месяца. Рассказываем, что будет на конференции

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

Защита интеллектуальной собственности – это защита инвестиций

На «Неделях российского бизнеса» прошел Форум по креативным индустриям и интеллектуальной собственности, где представители власти, бизнеса и институтов развития обсудили проблемы защиты интеллектуальной собственности в России, введения третейских судов и регулирования оборота продуктов, созданных искусственным интеллектом.

«СберАвто» запустил продажу электромобилей Tesla в Москве с доставкой в день заказа Статьи редакции

Электромобили будут стоит от 3,5 млн рублей.

null