Сразу к сути: есть склад, где все бизнес-процессы уже отлажены и в целом всех всё устраивает. Ничего, что даст рост в 30%, сделать уже невозможно, но хочется. Поставлена цель: оптимизировать маршрут, по которому идёт сборщик товаров, чтобы товар собирался быстрее. Результат 2-3% роста вполне устроит. Ограничения:

  • останавливать работу склада для экспериментов нельзя

  • денег — кроме зарплаты — не выделим

  • специалистов в этой области не имеется — свободен только веб-программист, да и тот без профильного образования

  • закончить нужно не то, чтобы ещё вчера, но через две недели точно.

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

Для прочтения знать сам алгоритм k-ближайших соседей не требуется — он очень простой и станет ясен ещё в ходе прочтения. Он чем-то похож на теорему Пифагора, только на стероидах.

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

Товар

Срок годности

Количество

Ячейка

Блины 20 см. №10

3 дня

4

03-02-07

Блины со сгущёнкой 25 см. №4

2 дня

4

02-02-01

Варенье земляничное, 400 г.

01.08.2023

1

01-01-05

Здесь строчки пока что отсортированы по ячейкам: от самой дальней к самой ближней. Ячейки имеют такой формат наименования: /номер_стеллажа/-/номер_полки/-/номер_ячейки/. Стеллажи 01 и 02 находятся у входа и следующие идут по нарастающей. Мы попробуем отсортировать строчки в более оптимальном порядке.

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

Начнём пристрелку

Если сталкиваемся с поиском оптимального маршрута в первый раз, то с вероятностью 100% поисковик расскажет нам об алгоритме Дейкстры. Беглый осмотр алгоритма покажет, что он не умеет работать с отрицательными весами и имеет ещё несколько минусов. Этой информации ещё недостаточно, чтобы отвергнуть его или, наоборот, продолжить копать под него глубже. Таким образом мы сталкиваемся с первым выбором: ищем новый алгоритм, или дорабатываем этот? В статье про алгоритм Левенштейна мы пошли вторым путём, здесь для примера пойдём первым. Если есть подозрительные особенности алгоритма, важность которых вам не очевидна — это первый звоночек, чтобы вы пошли искать что-то другое. Не стоит считать, что это универсальное правило, но часто это именно так и может экономить время.

Лайфхак, чтобы при поисках решений начать мыслить шире

Гораздо полезней научиться переформулировать изначальную цель. Это может дать новые идеи для поисков, затронув смежные сферы. В данном случае можно перефразировать «поиск оптимального маршрута» в «поиск рекомендуемого маршрута». Это незначительное изменение не только изменит выдачу поисковой системы, но и сразу покажет нам интересные примеры. Я, например, наткнулся на то, как с помощью алгоритма поиска k-ближайших соседей выстроили систему рекомендаций в каком-то интернет-магазине (сообщения в стиле: «возможно, вам понравится этот товар»).

Как я уже писал, сборщики работают с помощью сборочных листов. Рабочий день у них идёт так:

  1. Печатаем сборочный лист

  2. Идём налегке к самой дальней ячейке из листа и от неё направляемся ко всем остальным, постепенно набирая товар в корзинку или тележку

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

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

На каких критериях, влияющих на сборку, остановимся? Для примера попробуем эти:

  1. «Этаж» полки на стеллаже, на котором находится эта ячейка

  2. Расстояние до ячеек (кстати, как от места старта, так и между ними)

  3. Масса переносимого товара

  4. Количество «особенных» точек на пути между ячейками

«Особенными» точками считаются повороты и перекрёстки.

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

Первые два критерия сильно связаны с «географией» склада. Остановимся на них подробней. Как их представить в цифровом виде? Ничего нового придумывать не нужно — вы уже сами хорошо знаете эту двухмерную систему координат:

А вот упрощённый пример обозначения стеллажей на такой сетке:

В этом примере все стеллажи кроме одного имеют по три ячейки на полке. Все координаты ячеек отсчитываются от общей точки отсчёта. Для удобства, она размещена в левом нижнем углу, чтобы все значения были положительными числами — это удобней. Этаж учитывается с помощью добавления третьего измерения. Единицы измерения осей пока не нужны.

Для тех кому метрики интересней, чем сам алгоритм — прочитайте текст под спойлером.

Пройдёмся по каждой из них с картинками

Высота полки

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

Что это даст сборщику? Это даст возможность сперва собрать товар с одного уровня, а потом полностью с другого. Не нужно будет брать подъёмник для сборки первого товара и затем возвращать, чтобы потом снова взять его для сборки, например, пятого. То, какие ячейки собирать первыми — высокие или низкие — пока оставим открытым вопросом. Это легко можно поменять однократным измерением приоритета.

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

Расстояние до ячеек

Тут объяснять нечего, всё предельно просто:

S = |x_1 - x_2| + |y_1 - y_2|

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

На рисунке обозначено три маршрута: зелёный, бирюзовый и красный. Все три ведут из точки (0;4) в точку (13;7). Визуально заметно, что пройденное расстояние у всех трёх совпало, хотя количество поворотов у всех отличается.

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

Количество особенных точек на пути

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

Нам нужно сделать так, чтобы между каждым товаром количество таких точек было минимальным. И тут есть проблема — наличие разных маршрутов. Ещё раз посмотрите на предыдущую картинку:

  • у зелёного маршрута есть четыре особенных точки: два поворота и два перекрёстка между ними

  • у бирюзового маршрута четыре точки: все из них перекрёстки

  • у красного маршрута вообще количество точек можно посчитать по-разному, так как часть стеллажей на него выходят торцом, а часть стоят вдоль. Некоторые промежутки между ними при этом совмещены как в точке (5;4), а вот точка (13;4) — нет.

То есть длина маршрута в метрах у всех такая же, а количество препятствий — нет.

Масса товара

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

Как выглядит алгоритм?

Если человек знает теорему Пифагора, то и с алгоритмом поиска k-ближайших соседей справится. Как я уже намекал, теорема Пифагора является его частным случаем. Сравните теорему:

a^2 = b^2 + c^2

И наш алгоритм для случая с тремя признаками:

a^2 = b^2 + c^2 + d^2

У теоремы Пифагора b — это длина первого катета, а c — длина второго. На рисунке ниже первый катет — это (x2-x1), а второй — это (y2-y1). Длина катета — это разность координат его конечной точки и начальной, убедитесь в этом из рисунка:

У алгоритма k-ближайших соседей из примера выше есть три параметра: b, c и d. Это тоже разность между значениями этих параметров. Если сравнивать нужно 10 характеристик, то и слагаемых в правой части тоже будет 10.

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

На уровне кода наш алгоритм — это функция, которая принимает на вход два товара из сборочного листа, а в ответ возвращает какое-то число. Это число показывает то, как эти товары «близки» между собой. «Близость» — это вовсе не то, о чём вы подумали. Да, речь не об их физической близости: 2 метра, 7 или 10. Близкими они являются по совокупности предложенных мной параметров. Чем меньше число, тем они ближе друг к другу, тем лучше их собирать вместе. Наверное.

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

Блины 20 см. №10

Блины со сгущёнкой 25 см. №4

Варенье земляничное, 400 г.

Блины 20 см. №10

0

Блины со сгущёнкой 25 см. №4

0

Варенье земляничное, 400 г.

0

Кстати, а почему мы отталкиваемся от товаров, а не от признаков?

Отталкиваться от товаров в сборочном листе удобно из-за того, что их обычно немного. Их полный перебор не займёт много времени, даже если сложность возрастает квадратично. Это называется сложностью O(n2), когда при увеличении числа товаров в 10 раз объём работы увеличивается сразу в 100 раз, при увеличении в 5 раз — в 25 и т. д. Для небольших сборочных листов — самое оно. Признаков, правда, у нас тоже немного, поэтому обратный подход тоже имеет право на жизнь.

Покажу пример того, как нужно начинать расчёт и исправлять в нём первые ошибки/проблемы. Давайте вспомним метрики:

  1. «Этаж» полки на стеллаже

  2. Расстояние до ячейки

  3. Масса товара

  4. Количество «особенных» точек на пути

Сравним обычные блины (товар №1) из нашего сборочного листа с блинами со сгущёнкой (товар №2). У них третья и вторая полка (этаж) соответственно. Масса у первых, например, 40 г, а у вторых 80 г. Расстояние мы измеряем линейкой и получаем, допустим, 20 метров до первых и 28 метров до вторых. Количество особенных точек считаем на пальчиках и насчитываем 3 и 4. Черновой вид формулы у нас получается такой:

a^2 = (3-2)^2 + (80-40)^2 + (28-20)^2 + (4-3)^2

Думаю, теперь по алгоритму вопросов больше не осталось и как действовать программисту — ясно. Но внимательные и критически настроенные читатели сразу увидят в алгоритме недостаток: критерии неравноценны. Например, масса блинчиков вносит куда более существенный вклад, чем высота полки. А ещё в текущем виде формула зависит от единиц измерения. Если мы начнём измерять массу в килограммах, то результат кардинально изменится:

a^2 = (3-2)^2 + (0.08-0.04)^2 + (28-20)^2 + (4-3)^2

Это противоестественно, ведь ситуация с товаром не поменялась, а результат уже другой!

Решение для этих проблем уже придумали, причём оно одно для обеих: нормализация. Её идея заключается в том, что в формулу нужно подставлять не сами величины, а их преобразованные версии. Как их преобразовывать? Придумано несколько вариантов, я покажу один из них:

x_0 = \frac{(x - \min(X))}{(\max(X)-\min(X))}

После такого преобразования все критерии будут лежать в интервале от 0 до 1 и без всяких единиц измерения. Это очевидный момент, или всё же не очень?

С алгоритмом разобрались, а как это воплощать в жизнь?

Нужно «всего лишь» это:

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

  2. Какой-нибудь показатель, который позволит сказать, что наш алгоритм хуже или лучше прежнего

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

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

Чуть-чуть скучного текста про то, как получить информацию по ячейкам

Список ячеек, как правило, в любой WMS уже есть. Там же — в идеале — присутствуют масса и размеры товара.

Что с остальными параметрами? Хотя это не работа программиста, лучше организовать её самому.

Сперва обращаемся к главному инженеру (или кто у вас там?) и в срочном порядке требуем актуальный план всех этажей склада с указанием размеров здания. Если человек сговорчивый — просим ещё, чтобы сам убрал с плана ненужные нам условные обозначения огнетушителей, эвакуационных выходов и всего такого.

Далее находим сравнительно низкооплачиваемого сотрудника, который пройдётся с планом здания по складу и нанесёт на него местоположение ячеек в метрах. Как вы помните, дополнительного финансирования нам выделять не стали, но истинный программист может придумать алгоритм, чтобы это обойти. Например, можно сходить в бухгалтерию и ласково попросить их написать приказ на «инвентаризацию стеллажей и осмотра их технического состояния». Когда бухгалтерия говорит: «Нам это нужно, это же инвентаризация!», им не отказывают. Конечно, точность такого плана будет хромать, но в качестве отправной точки — отличное начало. Из своей практики скажу, что здесь я столкнулся с проблемами, которых я никак не ждал:

  1. не все люди умеют пользоваться рулеткой

  2. не все люди могут научиться пользоваться рулеткой

  3. обычная рулетка может стать причиной массовой драки среди сотрудников (видео 18+)

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

Как всё это хранить в БД? К сожалению, этот вопрос тоже будет опущен, так как тянет на ещё одну статью. Признаки ячеек можно попробовать хранить и в плоском виде, и иерархически, но всегда есть нюансы.

Странно, но это всё

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

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

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

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


  1. sunnybear
    20.11.2022 11:24
    +2

    Пройдите уже пару вводных курсов по машинному обучению, и вам перестанет казаться, что между теоремой Пифагора и k ближайших соседей много общего :)


    1. sepetov Автор
      20.11.2022 11:29

      Тс-с-с-с! Начинающим об этом не говорите. Это следующая ступенька :-)


  1. spc
    20.11.2022 11:49

    У меня непрофильный вопрос. А зачем в описанных условиях это потребовалось программисту без профильного образования?


    1. Zhuikoff
      20.11.2022 11:55
      +3

      Попробую ответить за автора. Может чтобы немного подрасти, почувствовать свои возможности, развить их и идти дальше?


    1. sepetov Автор
      20.11.2022 11:59
      +1

      @Zhuikoffуже верно ответил - для меня это главная причина. Есть и другая - кто-то же должен это делать? У предприятия есть потребность, а из свободных людей только я.


      1. Zenitchik
        21.11.2022 18:22
        -1

        Если потребность не платёжеспособная, то почему её кто-то должен удовлетворять?


  1. thevlad
    20.11.2022 12:14
    +1

    Чем это не задача коммивояжера(traveling salesman problem)?


    1. sepetov Автор
      20.11.2022 12:22

      Задача коммивояжера подходит, конечно же!

      Однако в статье не описать всех нюансов - многое опущено. Если кратко: поиск k-ближайших соседей позволяет эффективней объединить некоторые товары в группы. На волновой сборке (та, которая в два этапа) это экономит число спуско-подъёмных операций раз в 40.


      1. thevlad
        20.11.2022 12:28
        +1

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


        1. sepetov Автор
          20.11.2022 12:42

          Они потому и не приведены, так как решение не является оптимальным, а лишь возможным. Там был рост всего в ~2%. Вам, например, это сразу бросилось в глаза и, вероятно, очевидно даже на интуитивном уровне.

          Я потому и написал, что алгоритм выдал скромные результаты и перекочевал из сборочных листов. Когда появились ресурсы, они стали генерироваться иначе.


  1. EzikBro
    20.11.2022 19:09
    +1

    Так и не понял, по какому именно маршруту идут сборщики после вашей обработки их листа. Просто после каждого предмета жадно идут к ближайшему (с точки зрения вашей "близости") невзятому продукту и берут его?


    1. sepetov Автор
      20.11.2022 19:45

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

      1. Первый и второй имеют "близость" 10.

      2. Первый и третий: 9.

      3. Первый и четвёртый: 8.

      4. Второй и третий: 7.

      5. Второй и четвёртый: 6.

      6. Третий и четвёртый: 5.

      При таких показателях наибольшая сумма может быть 25. Например, она будет у такой последовательности сборки:

      • Товар №3, потом товар №1, потом товар №2, потом №4.

      25 здесь получается как сумма: 9+10+6.

      При всех возможных комбинациях такая сумма может быть у нескольких маршрутов (3->2->1->4, либо 4->1->2->3, например). Какой выбирается из них - это ещё один "алгоритм".


  1. mixsture
    20.11.2022 19:40
    +2

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


    1. sepetov Автор
      20.11.2022 19:59

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

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

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


  1. alex-open-plc
    21.11.2022 01:48
    +1

    Симплекс-метод?
    Не, не слышали...


    1. sepetov Автор
      21.11.2022 07:45

      Да, верно. На тот момент для меня 99,9% всего это было чем-то новым: "Линейное программирование? Что это такое? В моём вузе такого не бывает!"

      Даже вменяемая статья на хабре появилась спустя год. Собственно, так всегда: сначала возникает потребность, а уже потом человек начинает разбираться в теме. Реализация своих велосипедов как раз происходит между ними.


  1. Germanjon
    21.11.2022 08:05
    +1

    Навскидку вопросы по алгоритму:
    1. Проходы между полками имеют одинаковую ширину или есть "магистрали/улицы/переулки".
    2. Брать товары можно с любой стороны полки, или обязательно заходить "спереди".
    3. Есть ли разница в движении между точками (1,7)-(3,7) и (3,7)-(3,5) на вашей схеме (первая цифра - координата по горизонтали). Разница на практике и в вашем алгоритме.
    4. В любом ли проходе между полками может развернуться погрузчик? То есть - нужно ли учитывать необходимость доп. телодвижений, когда нужно достать груз с двух полок, расположенных напротив друг друга через проход? Различаются ли скорости движения "вперёд" и "назад" у погрузчика? Могут ли два погрузчика разъехаться в одном проезде?
    5. В любой ли проход может пройти человек со стремянкой? Учитывается ли в алгоритме необходимость идти в другой конец склада за стремянкой (если идёт сначала сбор в дальнем конце)?
    6. Ограничены ли дополнительные ресурсы (стремянка, погрузчик) по количеству?
    7. Как влияют друг на друга рабочие, собирающие заказ в одном переулке/на одном стеллаже/на одной полке?


    1. sepetov Автор
      21.11.2022 08:30
      +1

      1. В реальном складе, конечно же, ширина разная. Кстати, ячейки тоже.

      2. Ячейки с разных сторон одного стеллажа - это у нас разные ячейки. Поэтому подойти можно только с одной стороны.

      3. Сложный вопрос, тем более, если обобщить. Думаю, разница с практикой больше, чем в половине случаев.

      4. Нужно учитывать, но не учитывается. Причём, не только в рамках статьи. Я это и в реальности тогда не смог учесть - вся техника как будто в идеальных условиях и никогда не сталкивается с проблемами. Вся техника, к слову, была поделена на два вида: стремянка и погрузчик.

      5. Как и в п. 1, и в п. 4 - не в любой. Некоторые проходы заставлены сезонным товаром, поэтому не пролезть. Но, на удивление, это учитывалось. Проходы между стеллажами в системе заведены как виртуальные ячейки, поэтому на них и товар вполне официально числится, и размеры в системе есть, но есть признак, что по ней можно пройти, когда пустая.
        5.1. Необходимость в стремянке и погрузчике позже стал учитывать отдельным вычислением, в этот алгоритм не вписалось.

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

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