Как-то два года назад, случайно включив телевизор, я увидел интересный сюжет в программе "Вести". В нём рассказывали о том, что департамент информационных технологий Москвы создает нейросеть, которая будет считывать показания счетчиков воды по фотографиям. В сюжете телеведущий попросил горожан помочь проекту и прислать снимки своих счетчиков на портал mos.ru, чтобы на них обучить нейронную сеть. 


Если Вы — департамент Москвы, то выпустить ролик на федеральном канале и попросить людей прислать изображения счетчиков — не очень большая проблема. Но что делать, если Вы — маленький стартап, и сделать рекламу на телеканале не можете? Как получить 50000 изображений счетчиков в таком случае? На помощь приходит Яндекс.Толока!


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


Постановка задачи


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


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


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


Благодарности

Прошлая статья стала ТОП-2 в рейтинге статей от сообщества ODS. Спасибо, что комментируете и ставите плюсы!) 



Часть 1. Получение изображений


Что может быть проще? Всего лишь нужно попросить человека открыть приложение Яндекс.Толока на своем телефоне и сфотографировать свой счетчик. Если бы я не работал несколько лет с Толокой, то моя инструкция звучала бы так: "Вам нужно сфотографировать свой счетчик воды (горячей либо холодной) и прислать нам изображение"


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


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

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

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




Интерфейс задания пишется всего в 2 строки! 




При создании пула указываем время на выполнение задания, отложенную приемку и цену за задание 0.01$.




А чтобы люди по несколько раз не выполняли задание и не отправляли одинаковые фотографии, в блоке контроля качества запрещаем повторное выполнение задания.




Укажем, что нам нужны русскоговорящие пользователи, которые выполняют задание через мобильное приложение Яндекс.Толока.




Загружаем задания в пул.




Запускаем пул, радуемся и ждем ответов пользователей! Вот так выглядит наше задание со стороны толокера: 



Часть 2. Приемка заданий


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


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


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


Звучит потрясающе и грандиозно, не правда ли?
Тогда пора претворять задумку в жизнь!


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


  • На фотографии ровно один счетчик холодной либо горячей воды;
  • Показания на счетчике отчетливо видны.

В иных случаях фотографию считаем плохой.


С критериями разобрались, теперь пишем инструкцию!

В качестве входных параметров передаем ссылку на изображение. На выходе будет два флага: 


  • check_count — ответ на первый вопрос
  • check_quality — ответ на второй вопрос

В переменную value будут записываться показания счетчика.




Интерфейс этого задания занимает уже 14 строк.




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




Допустим к заданию 50% лучших исполнителей.




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


В Толоке есть два основных инструмента, которые позволяют поддерживать хорошее качество: 


  1. Обучение. Перед выполнением основного задания мы можем попросить толокеров пройти обучение. В пуле обучения людям даются задания, на которые мы заранее знаем правильные ответы. В случае, если человек ответил неправильно, ему показывается ошибка и объясняется, как нужно было ответить. После прохождения обучения мы видим с каким процентом заданий исполнитель справился и можем допустить к основному пулу заданий только тех, кто справился хорошо.
  2. Блоки контроля качества. Может быть такая ситуация, что пул обучения исполнитель прошел на «отлично», мы его допустили к заданию, но через пять минут  он ушел играть в футбол, оставив за компьютером своего трехлетнего брата. К счастью, в Толоке есть множество методов, которые позволяют следить за тем, как люди выполняют задания.

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




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


Мнение большинства
Мы даем задание 5-и независимым людям. И если четыре человека на вопрос отвечают "Да", а пятый отвечает "Нет", то пятый, вероятно, ошибся. Таким образом, мы можем смотреть, как ответы человека согласуются с ответами других людей, и блокировать пользователей, которые отвечают не так, как остальные.




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




Ура, задание создано! Вот так выглядит интерфейс со стороны исполнителя:




Часть 3. Объединение заданий


Отлично, задания готовы! Но возникает вопрос, как соединить задания между собой? Как сделать так, чтобы после первого задания запускалось второе?


Конечно, можно пошаманить с бубном и сделать это вручную через интерфейс Толоки, но есть способ проще и быстрее! В Яндекс.Толоке есть API, воспользуемся им и напишем скрипт на питоне!


Я знаю, многие из вас не любят читать код, поэтому спрятал его под спойлер
import pandas as pd
import numpy as np
import requests
import boto3

# Данная функция скачивает изображение из первого задания, загружает
# в Yandex Object Storage и возвращает ссылку на изображение
def load_image_on_yandex_storage(img_id):
    session = boto3.session.Session(
        region_name="us-east-1", aws_secret_access_key="", aws_access_key_id=""
    )
    s3 = session.client(
        service_name="s3", endpoint_url="https://storage.yandexcloud.net"
    )
    file = requests.get(
        url=URL_API + "attachments/%s/download" % img_id, headers=HEADERS
    )
    s3.put_object(Bucket="schetchiki", Key=img_id, Body=file.content)
    return "https://storage.yandexcloud.net/schetchiki/%s" % img_id

# Указываем ключ к API, а также ID пула первого и второго задания
TOLOKA_OAUTH_TOKEN = ""
POOL_ID_FIRST = 7156932
POOL_ID_SECOND = 7006945
URL_API = "https://toloka.yandex.ru/api/v1/"
HEADERS = {
    "Authorization": "OAuth %s" % TOLOKA_OAUTH_TOKEN,
    "Content-Type": "application/JSON",
}

# Получаем список всех заданий из первого пула, которые ждут проверки
url_assignments = (
    URL_API + "assignments/?status=SUBMITTED&limit=10000&pool_id=%s" % POOL_ID_FIRST
)
submitted_tasks = requests.get(url_assignments, headers=HEADERS).json()["items"]

# Заводим словари, чтобы помнить, как соотносятся id задания из первого пула
# и id задания из второго пула
url_to_first_id_map = {}
first_id_to_second_id_map = {}
json_second_task = []

# Для каждого задания из первого пула:
# * Запоминаем его id
# * Загружаем картинку в Yandex Object Storage
# * Оборачиваем параметры в json для второго задания
for task in submitted_tasks:
    first_task_id = task["id"]
    img_id = task["solutions"][0]["output_values"]["img"]
    url_img = load_image_on_yandex_storage(img_id)
    url_to_first_id_map[url_img] = first_task_id
    json_second_task.append(
        {"input_values": {"image": url_img}, "pool_id": POOL_ID_SECOND, "overlap": 5}
    )

# Загружаем задания во второй пул
# "Не баг, а фича": добавлять через API задания в пул можно только тогда,
# когда сам пул создан через API
second_tasks_request = requests.post(
    url=URL_API + "tasks?open_pool=true", headers=HEADERS, json=json_second_task
).json()

# В ответ нам выдали id вторых заданий. 
# По ним мы сможем запросить ответы после завершения задания, поэтому запомним их
for second_task in second_tasks_request["items"].values():
    second_task_id = second_task["id"]
    img_url = second_task["input_values"]["image"]
    first_task_id = url_to_first_id_map[img_url]
    first_id_to_second_id_map[first_task_id] = second_task_id

# Эту функцию я писал ночью, утром я сам не смог понять, как она работает
# Она возращает ответы пользователей для конкретного поля
def unknown_fun(k):
    return list(map(lambda t: t['solutions'][np.where(np.array(list(map(lambda x: x['id'], t['tasks']))) == second_task_id)[0][0]]['output_values'][k], second_task))

# Меняем keys и values местами
first_id_to_url_map = dict((v, k) for k, v in url_to_first_id_map.items())
db = []

# Выполняем этот код только после того, как задание 2 будет выполнено
for first_task_id in first_id_to_second_id_map:

    # Для каждого проверяемого задания 1
    second_task_id = first_id_to_second_id_map[first_task_id]

    # Получаем результаты задания 2
    url_assignments = (
        URL_API + "assignments/?status=ACCEPTED&task_id=%s" % second_task_id
    )
    second_task = requests.get(url_assignments, headers=HEADERS).json()["items"]

    # Получаем вектор ответов пользователей
    value_list = unknown_fun("value")
    check_count_list = unknown_fun("check_count")
    check_quality_list = unknown_fun("check_quality")

    # Если больше двух людей ответили на первый вопрос «нет»,
    # то значит счетчика на изображении нет,
    # либо на изображении несколько счетчиков. Отклоняем задание
    if np.sum(check_count_list) < 3:
        json_check = {
            "status": "REJECTED",
            "public_comment": "На фотографии должен быть ровно один счетчик холодной либо горячей воды",
        }
    # Если больше двух людей сказали, что показания не видны, отклоняем задание
    elif np.sum(check_quality_list) < 3:
        json_check = {
            "status": "REJECTED",
            "public_comment": "Показания на счетчике отчетливо не видны",
        }
    # В остальных случаях принимаем задание
    else:
        json_check = {
            "status": "ACCEPTED",
            "public_comment": "Изображение счетчика принято",
        }

    url = URL_API + "assignments/%s" % first_task_id
    result_patch_request = requests.patch(url, headers=HEADERS, json=json_check)

    # Найдем для принятых заданий самый частый ответ
    (values, counts) = np.unique(value_list, return_counts=True)
    ind = np.argmax(counts)
    if counts[ind] > 3 and json_check["status"] == "ACCEPTED":
        print(
            "Показания счетчика: %s. Его подтвердили %d из 5 пользователей"
            % (values[ind], counts[ind])
        )

    # Чтобы ничего не забыть и не потерять, записываем в массив
    db.append(
        {
            "first_task_id": first_task_id,
            "second_task_id": second_task_id,
            "url_img": first_id_to_url_map[first_task_id],
            "check_count_list": check_count_list,
            "check_quality_list": check_quality_list,
            "value_list": value_list,
        }
    )

# Сохраняем получившийся результат
pd.DataFrame(db).to_csv("result.csv")

Запускаем код и вот долгожданный результат: датасет из 871 изображений счетчиков готов.




Цена


Давайте оценим экономическую составляющую проекта.
За присланное изображение в первом задании мы предлагаем 0.01$.
К сожалению, если мы платим исполнителю 0.01$, нам придется отдать 0.018$.
Как это получается?


  • Комиссия Яндекса равна min(0.005,20%). Для задания ценой 0.01$ комиссия будет 50%;
  • НДС составляет 20%.  

За проверку 10 изображений счетчиков мы платим 0.01$. При этом одно изображение проверяют 5 раз независимые люди. Итого, за проверку одного изображения мы отдаем: (0.01 x 5 / 10) x 1.2 x 1.5 = 0.009$. 


Из 1000 присланных заданий было принято 871 изображение, а 129 было отклонено. Значит, чтобы получить датасет из 871 изображений, мы заплатили:
0.018$ x 871 + 0.009$ x 1000 = 25$ и для получения датасета в размере 50000 изображений понадобится 92000 руб. Это определенно дешевле, чем заказывать рекламу на федеральном канале! 


Но и данную цифру реально уменьшить в несколько раз. Можно:


  • Предлагать в первом задании делать не одно фото, а несколько. При этом поднять цену, тогда комиссия Яндекса будет не 50%, а 20%;
  • Использовать динамическое перекрытие во втором задании. Если 4 из 5 людей дали одинаковый ответ, то уже не имеет смысла выдавать задание пятому человеку;
  • Работать с Толокой как иностранное юридическое лицо. В этом случае вы не платите НДС.

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


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

Комментарии (28)


  1. ebt
    11.10.2019 14:53

    Пожалуйста, если возможно, покажите, как соединять задания через GUI.


    1. kucev Автор
      11.10.2019 15:20

      Пример того, как соединять задания через GUI можно посмотреть в этой инструкции от Яндекс.Толоки.


  1. LAG_LAGbI4
    11.10.2019 15:13

    и много желающих фоткать счётчик за 5 рублей?


    1. iridiumhawk
      11.10.2019 15:37

      Вот мне тоже сложно представить, чтобы я поперся с ванную делать в неудобном месте фотку счетчика за 5-6 руб. Может тут какая хитрость есть?


      1. LAG_LAGbI4
        11.10.2019 15:46

        я тут посчитал. Если у человека зп 40 тыс, то в минуту на работе он получает 4 руб 17 копеек. 0,01$ =0.64 руб. тут я ошибся, не 5 рублей, а 64 копейки.

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


      1. UksusoFF
        11.10.2019 18:50

        А если их все равно уже сфоткал для отправки в УК и они вот рядом лежат в картинках? Причем сразу два.


    1. kucev Автор
      11.10.2019 15:39

      Данное задание довольно легкое, имеет простую инструкцию, не содержит пула обучения. Такие задания люди выполняют на много охотнее, чем сложные задания с десятистраничной инструкцией и долгим процессом обучения. Плюс практически у каждого человека есть счетчик в квартире. Поэтому процент желающих выполнить задание довольно большой. Единственная проблема: количество пользователей мобильной версии Толоки в несколько раз меньше, чем пользователей десктопной версии. На данный момент в Толоке зарегистрировано 4 миллиона человек, DAU более 25к человек.


    1. kucev Автор
      11.10.2019 15:55

      Довольно интересный факт для пользователей каршеринга Яндекс.Драйв. Если в начале аренды вы фотографируете повреждения/царапины на автомобиле, то полученные фотографии первым делом загружаются в Толоку и люди оценивают присланные изображения.image


      1. LAG_LAGbI4
        11.10.2019 16:03

        Я в недоумении.

        Допустим это задания потоковые, человек не тормоз, интернет работает быстро. 5 секунд на задание. 1 цент — 125 секунд. 20 дней по 8 часов = 46$ или 2700 рублей. или 13500 рублей если на задание тратить 1 секунду. лажа какая-то


        1. tangro
          11.10.2019 16:13
          +3

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


          1. tvr
            11.10.2019 16:18

            Лучше сформулировать, по моему, невозможно.


          1. Firz
            11.10.2019 16:36

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


        1. Irker
          11.10.2019 16:54

          В таких системах в основном работают следующие категории людей:
          1) привязанные к дому (домохозяйки, мамы, бабушки, инвалиды)
          2) ковыряющиеся в носу во время основной работы (начальник идет — свернул браузер)
          3) люди с временными проблемами с трудоустройством (ищут работу, только что уволили, а кушать хочется)
          4) обобщенный пункт: люди, которые еще по каким-то причинам не могут устроиться на нормальную работу

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


          1. tangro
            11.10.2019 17:12

            Безусловно. Что не отменяет тупости занятия чем-угодно на фултайм за 2700 рублей в месяц.


        1. Sing
          11.10.2019 23:40

          Я не понял расчётов.

          5 секунд на задание. 1 цент — 125 секунд.
          За задание же дают один цент, то есть «1 цент — 5 секунд». В итоге выходит ~73728 р. или в пять, соответственно, раз больше, если делать за секунду.


          1. LAG_LAGbI4
            11.10.2019 23:45

            Комментарий про яндекс драйв был изменён. Там изначально было указано 25 изображений 1 цент.


        1. gremlin244
          12.10.2019 09:12

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


  1. igorloban92
    11.10.2019 16:38

    # "Не баг, а фича": добавлять через API задания в пул можно только тогда,
    # когда сам пул создан через API

    На сколько я знаю, разницы быть не должно. kucev можете подсказать, как проявляется невозможность добавить задания в пул через API, если он создан через интерфейс?


    1. kucev Автор
      11.10.2019 16:59

      В случае, когда пул создается не через API, а через интерфейс, в настройках пула отсутствует блок «Количество заданий на странице». Если добавлять в такой пул задания через API, то задания добавляются, но при этом пользователи их не видят.image


      1. ortemij
        11.10.2019 18:38
        +1

        Рома, привет! Это действительно очень не очевидное поведение, которое стоит исправить. Дело в том, что если ты хочешь, чтобы Толока сама формировала страницы из твоего набора отдельных заданий, нужно задать настройки умного смешивания. Сейчас это можно сделать через интерфейс. Чтобы просто сохранить настройки, не заливая задания через UI, сейчас есть work-around: можно загрузить фейковый файлик. :)


        1. kucev Автор
          12.10.2019 01:37

          Артем, попробовал загрузить через интерфейс пустой файлик (без заданий) и выбрать «Умное смешивание». Все получилось, после этого действия загрузка заданий через API стала работать. Спасибо за очень интересный и простой способ решения проблемы!)image


  1. UksusoFF
    11.10.2019 18:51

    А как быть если счетчик стоит так что нормально не сфоткать? Сможет распознать?


    1. kucev Автор
      12.10.2019 01:47

      Боюсь, что в таком случае нейронная сеть распознать счетчик не сможет.
      Но это не точно :)


  1. Andrey_Epifantsev
    12.10.2019 06:50

    Очень впечатляющая статья. Данный сервис работает только в России? Или по всему миру тоже?
    Или в других странах есть аналоги?

    Например я пишу приложение обучающее правильному произношению на иностранном языке. Алгоритм автоматически определяет правильно ли обучаемый произнёс слово. Для обучения алгоритма нужен датасет со словами произнесёнными носителями языка. Похоже что Яндекс.Толока очень удобный инструмент для создания этого датасета. Но можно ли с помощью него нанимать людей в других странах?


    1. kuber
      12.10.2019 08:00

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


    1. bougakov
      12.10.2019 12:20

      гуглите «Amazon Mechanical Turk»


    1. Londoner
      12.10.2019 13:24

      forvo.com же…


    1. kucev Автор
      12.10.2019 13:58

      Данный сервис работает по всему миру, но в основном в Толоке люди из России и из стран СНГ, также есть небольшая часть людей из Турции. Как мне кажется, для вашей задачи больше подойдет «Amazon Mechanical Turk», чем «Яндекс.Толока».