Вступление

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

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

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

Исходные данные

База планировок

Для тестовой модели мы подготовили базу планировок в Notion. В дальнейшем к ней легко подключиться по API из Google Colab.

База планировок квартир
База планировок квартир

Данные от Заказчика по параметрам этажа

  1. Ширина этажа

  2. Квартирограмма

  3. Количество поколений для поиска решения

Методика генетического алгоритма поиска решения

  1. Данные на входе

    1. Массив значений вида: [1, 0, 25000, 7000, 0, 100, 0, 0]

      1. Расшифровка: [Floors, Angle, Floor_width, Floor_deep, Room_S, Room_1k, Room_2k, Room_3k]

  2. Структура классов

    1. Опишем квартиру (FLAT_DNA) как ДНК, состоящую из двух генов:

      1. Тип квартиры

        1. Торцевая (E)

        2. Стандарт (S)

      2. Тип планировки

        1. Студия (S)

        2. 1 комнатная (1k)

        3. 2 комнатная (2k)

        4. 3 комнатная (3k)

    2. ДНК этажа (FLOOR) будет иметь такую структуру:

      1. Первый ген = ДНК квартиры с зафиксированным геном “Тип квартиры” = (E)

      2. Со второго до предпоследнего все ДНК квартиры случайные

      3. Последний ген = ДНК квартиры с зафиксированным геном “Тип квартиры” = (E)

  3. Первое поколение (BuildNewGeneration)

    1. По умолчанию для первого поколения мы создаем (GENETIC_FLOOR.NEWBORN) планировок этажей и выбираем размер этажа = 10 квартирам (FLOOR_SIZE)

    2. Заполняем полученное поколение планировок конкретными квартирами из Базы Данных с помощью функции FillFloor

      1. В функции заложено правило, что первая и последняя квартира являются строго типа E, т.е. торцевыми

    3. Определяем лучшую планировку этажа в поколении (FindBestFloor)

      1. Для каждой планировки в поколении определяем 3 дельты:

        1. Отклонение от заданной процентовки квартир (DeltaP)

        2. Отклонение от заданной ширины этажа (DeltaS)

        3. Сводное отклонение (Delta)

        4. Выбираем вариант планировки с наименьшим значением сводного отклонения

        5. Меняем размер этажа на кол-во квартир в лучшем варианте (GENETIC_FLOOR.FLOOR_SIZE)

    4. Передаем ДНК лучшего варианта в первом поколении в следующее поколение

  4. Последующие поколения

    1. На основе полученных вариантов из предыдущего поколения создаем GENETIC_FLOOR.MUTATED вариантов с измененными ДНК квартир

      1. Вероятность мутации 1/20

      2. Мутация затрагивает только тип планировки

    2. Добавляем новые планировки, но с уже измененным количеством квартир на этажа (GENETIC_FLOOR.FLOOR_SIZE)

    3. Заполняем полученное поколение планировок конкретными квартирами из Базы Данных с помощью функции FillFloor

    4. Определяем лучшую планировку этажа в поколении (FindBestFloor)

    5. Передаем ДНК лучшего варианта в первом поколении в следующее поколение

Структура ДНК квартир и этажа
Структура ДНК квартир и этажа

Исходный код на Python

import random 
import copy
import pprint
from PIL import Image as IMG, ImageDraw, ImageFilter
import requests

class FLAT_DNA():
    
    Gens = {
        # Тип квартиры
        # E - Торцевая квартира, S - Стандартная квартира
        'Type': [['E', 'S'], 0.5], 

        # Планировка
        # S - Студия, 1k - 1 комнатная, 2k - 2 комнатная, 3k - 3 комнатная
        'Layout': [['S', '1k', '2k', '3k'], 0.5],
    }
    
    def __init__(self, DNA = None):

        self.DNA = {
            'Type' : random.choice(FLAT_DNA.Gens['Type'][0]),
            'Layout' : random.choice(FLAT_DNA.Gens['Layout'][0])
        }
        
        if DNA is not None:
            for i in DNA.keys():
              self.DNA[i] = DNA[i]
        
    def __repr__(self):
        return str(self.DNA)

class FLOOR():

  def __init__(self, N):
    self.N = N
    self.Plan = []
    for i in range(N):
      self.Plan.append([FLAT_DNA({'Type' : 'S'}), []])
    self.Plan[0][0].DNA['Type'] = 'E'
    self.Plan[-1][0].DNA['Type'] = 'E'

  # Функция наполнения ДНК этажа конкретными вариантами кварти из БД (случайным перебором)
  def FillFloor(self):
    for i in range(len(self.Plan)):
      if self.Plan[i][0].DNA['Type'] == 'E':
        flat = random.choice(DB_FlatType_Dict['Торцевая'])
        self.Plan[i][1].append(flat)
        #print(flat)
      if self.Plan[i][0].DNA['Type'] == 'S':
        flat = random.choice(DB_FlatType_Dict['Рядовая'])
        self.Plan[i][1].append(flat)
        #print(flat)
  
  # Функция определения фактической геометрии этажа и площади
  def FloorSquare(self):
    self.Width = 0
    self.Deep = 0
    for i in range(len(self.Plan)):
      self.Width += self.Plan[i][1][0]['properties']['Ширина']['number']
      self.Deep += self.Plan[i][1][0]['properties']['Глубина']['number']
    self.Square = self.Width * self.Deep

  # Функция генерации словаря с процентовкой квартир на этаже
  def FloorPercent(self):
    self.FloorPercent_Dict = {}
    for g in FLAT_DNA.Gens['Layout'][0]:
      if g not in self.FloorPercent_Dict.keys():
        self.FloorPercent_Dict[g] = 0
      for i in range(len(self.Plan)):
        if self.Plan[i][0].DNA['Layout'] == g:
          self.FloorPercent_Dict[g] += 1      
    for i in self.FloorPercent_Dict.keys():
      self.FloorPercent_Dict[i] = self.FloorPercent_Dict[i]*100/self.N

  def GetPNG(self):
    floor_img = IMG.new('RGB', (6000, 3000), color = 'white')
    W = 0
    for i in range(len(self.Plan)):
      url = self.Plan[i][1][0]['properties']['Foto']['files'][0]['file']['url']
      im = IMG.open(requests.get(url, stream=True).raw)
      floor_img.paste(im, (int(W/10), 0))
      W += self.Plan[i][1][0]['properties']['Ширина']['number']
    floor_img.save(f'./floor_img.png')


class GENETIC_FLOOR():

  FLOOR_SIZE = 10
  NEWBORN = 20
  MUTATED = 100
  SURVIVERS = 1

  # [Floors, Angle, Floor_width, Floor_deep, Room_S, Room_1k, Room_2k, Room_3k]
  def __init__(self, data):
    self.data = data
    self.Generation = {}

  def RunGenerations(self, N):
    for i in range(N):
      #print(f'--- Поколение №{(i+1)} ---')
      if hasattr(self, 'BestFloor'):
        # Создаем поколение и передаем в него лучший вариант из предыдущего поколения
        self.BuildNewGeneration([self.BestFloor[0]])
      else:
        # Создаем первое поколение
        self.BuildNewGeneration()
      # Определяем лучшый вариант в текущем поколении
      self.FindBestFloor(GENETIC_FLOOR.SURVIVERS)
      #print(f'Первый вариант в поколении: {self.Generation[0].Width}')
      #print(f'Ко-во вариантов в поколении: {len(self.Generation)}')
    print(f'Итоговое распределение квартир: {self.BestFloor[0].FloorPercent_Dict}, отклонения: {self.BestDeltaP}/{self.BestDeltaS}/{self.BestDelta}, ширина: {self.BestFloor[0].Width}')
      #print(f'Квартир на этаже: {self.BestFloor[0].N}')
  
  def BuildNewGeneration(self, Survivers_array=[]):
    NewGeneration = {}
    self.Generation = {}
    index = 0

    # Добавляем лучшие этажи из предыдущего поколения к новому поколению
    for i in Survivers_array:
        NewGeneration[index] = copy.deepcopy(i)
        index += 1

    # Меняем планировки в лучших вариантах из предыдущего поколения
    for i in Survivers_array:
      for k in range(GENETIC_FLOOR.MUTATED):
        for p in range(len(i.Plan)):
          if i.Plan[p][0].DNA['Type'] == 'E':
            if random.randint(1, 20) == 5:
              flat = random.choice(DB_FlatType_Dict['Торцевая'])
              i.Plan[p][1][0] = flat
              #print(f'Мутация торцевой квартиры')
          if i.Plan[p][0].DNA['Type'] == 'S':
            if random.randint(1, 20) == 5:
              flat = random.choice(DB_FlatType_Dict['Рядовая'])
              i.Plan[p][1][0] = flat
              #print(f'Мутация радовой квартиры')
        NewGeneration[index] = i
        index += 1

    # Добавляем новых вариантов к новому поколению
    for i in range(GENETIC_FLOOR.NEWBORN):
      N_new = GENETIC_FLOOR.FLOOR_SIZE + random.randint(-3, 4)
      if N_new<=0:
        N_new = 1
      floor1 = FLOOR(N_new)
      floor1.FillFloor()
      NewGeneration[index] = floor1
      index += 1

    self.Generation = copy.deepcopy(NewGeneration)

  def FindBestFloor(self, N=1):
    DeltaP = 1000000 # Отклонение по процентам
    DeltaS = 1000000 # Отклонение по площади
    Delta = 1000000 # Отклонение сводное
    #Delta_array = []
    BestFloor = []
    for i in self.Generation.keys():
      g = self.Generation[i]
      # Определили процентовку
      g.FloorPercent()
      # Определили геометрию
      g.FloorSquare()
      # Считаем отклонения от заданной процентовки
      DeltaP_tmp = self.CampareFloorPercent({'S': self.data[4], '1k': self.data[5], '2k': self.data[6], '3k': self.data[7]}, g.FloorPercent_Dict)
      # Считаем отклонение от Ширины этажа
      DeltaS_tmp = abs(g.Width - self.data[2])
      # Выводим сводное отклонение
      Delta_tmp = DeltaP_tmp + DeltaS_tmp / 1500
      if Delta_tmp < Delta:
        BestFloor.insert(0, g)
        DeltaP = DeltaP_tmp
        DeltaS = DeltaS_tmp
        Delta = Delta_tmp
        #Delta_array.append(Delta_tmp)
    self.BestFloor = BestFloor
    self.BestDelta = Delta
    self.BestDeltaP = DeltaP
    self.BestDeltaS = DeltaS
    GENETIC_FLOOR.FLOOR_SIZE = self.BestFloor[0].N


  def CampareFloorPercent(self, d1, d2):
    Delta = 0
    for i in d1.keys():
      Delta += abs(d1[i] - d2[i])
    return Delta

Результат работы алгоритма

Исходные данные: [1, 0, 60000, 10000, 10, 60, 30, 0]
Идет рассчет...
Итоговое распределение квартир: {'S': 12.5, '1k': 62.5, '2k': 25.0, '3k': 0.0}, отклонения: 10.0/5350/13.566666666666666, ширина: 54650
Итоговая длина этажа: 54650
Исходные данные: [1, 0, 60000, 10000, 10, 60, 30, 0]
Идет рассчет...
Итоговое распределение квартир: {'S': 12.5, '1k': 62.5, '2k': 25.0, '3k': 0.0}, отклонения: 10.0/800/10.533333333333333, ширина: 59200
Итоговая длина этажа: 59200
Исходные данные: [1, 0, 60000, 10000, 10, 60, 30, 0]
Идет рассчет...
Итоговое распределение квартир: {'S': 7.142857142857143, '1k': 57.142857142857146, '2k': 28.571428571428573, '3k': 7.142857142857143}, отклонения: 14.285714285714281/3000/16.28571428571428, ширина: 63000
Итоговая длина этажа: 63000
Исходные данные: [1, 0, 60000, 10000, 10, 60, 30, 0]
Идет рассчет...
Итоговое распределение квартир: {'S': 9.090909090909092, '1k': 63.63636363636363, '2k': 27.272727272727273, '3k': 0.0}, отклонения: 7.272727272727268/50/7.306060606060601, ширина: 60050
Итоговая длина этажа: 60050
Исходные данные: [1, 0, 60000, 10000, 10, 60, 30, 0]
Идет рассчет...
Итоговое распределение квартир: {'S': 10.0, '1k': 60.0, '2k': 30.0, '3k': 0.0}, отклонения: 0.0/850/0.5666666666666667, ширина: 60850
Итоговая длина этажа: 60850
Выбранные планировки, удовлетворяющие поставленной Заказчиком задаче
Выбранные планировки, удовлетворяющие поставленной Заказчиком задаче

Пример работы алгоритма

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


  1. AigizK
    10.05.2022 07:40
    +2

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


    1. Javian
      10.05.2022 07:54
      +2

      и менее тоже. В условиях южных регионов наиболее комфортные квартиры те, где солнце уходит в тень к 12-13 часам.


    1. TAU15 Автор
      10.05.2022 09:09

      Это конечно. Там много будет ограничений. Но они все будут являться дополнением в свойства генов. И будут учитываться в функции генерации ДНК квартиры и ДНК этажа.


  1. sshmakov
    10.05.2022 08:54
    +3

    Какая всё же целевая функция, какую характеристику хотел оптимизировать заказчик?


    1. TAU15 Автор
      10.05.2022 09:08

      Целевых задач две:

      1. Попасть в процентовку квартир

      2. Занять максимально точно площадь этажа


      1. IvaYan
        10.05.2022 09:40
        +1

        То есть оптимизация у вас многокритериальная? Как в итоге считали целевую функцию? Взвешенная сумма?


        1. TAU15 Автор
          10.05.2022 10:01

          # Считаем отклонения от заданной процентовки
          DeltaP_tmp = self.CampareFloorPercent({'S': self.data[4], '1k': self.data[5], '2k': self.data[6], '3k': self.data[7]}, g.FloorPercent_Dict)
          # Считаем отклонение от Ширины этажа
          DeltaS_tmp = abs(g.Width - self.data[2])
          # Выводим сводное отклонение
          Delta_tmp = DeltaP_tmp + DeltaS_tmp / 1500

          Пока так, но это как раз момент требующий тонкой настройки.


          1. IvaYan
            10.05.2022 15:16

            Вообще, выглядит так, что у вас разная размерность слагаемых в

            Delta_tmp = DeltaP_tmp + DeltaS_tmp / 1500

            DeltaP_tmp это процент, то есть 0..100, а DeltaS_tmp это квадратные метры, тут от 0 до бесконечности. И даже деление на 1500 (интересно, почему именно столько?) скорее всего не спасёт.


            1. TAU15 Автор
              10.05.2022 16:32

              DeltaS - это действительно квадратные метры, но в реальности есть ограничение на длину этажа, так что тут нет бесконечности.

              1500 это константа подобранная пока просто экспериментальным путем.


              1. IvaYan
                10.05.2022 20:14

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

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


                1. TAU15 Автор
                  10.05.2022 20:38

                  Да, согласен, спасибо ????


  1. amarao
    10.05.2022 12:15
    +2

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

    ... Но почему генетический алгоритм, а не, например, метод градиентного спуска?


    1. TAU15 Автор
      10.05.2022 13:33

      Хорошая идея попробовать метод градиентного спуска. Единственное опасение у меня, что тут очень большая вариативность и сложность поиска решения расчет очень сильно в зависимости от числа планировок в базе.


      1. amarao
        10.05.2022 13:39
        +2

        Ну, тут ещё момент, что есть метрика заказчика (плотность упаковки), а есть метрика качества (довольство покупателя планировкой). В целом, если написать синтетическую функцию "довольства", то можно попытаться свести задачу к двухмерной.

        А вот коэфиценты "довольства" становятся чуть ли не "конструктором квартиры".

        Например, можно вести базу "хорошего" и "плохого" и в первом приближении давать 1 балл за хорошее, и забирать балл за плохое. При входе в комнату не видно окна (дверь открывается в встроенный шкаф) - минус 1 балл. Окно по центру комнаты - +1 балл. Между кухней и туалетом есть хотя бы ещё одна дверь или метр стены - +1 балл. Открытые двери пересекаются - -1 балл и т.д.

        Вообще, само направление звучит как фантастика, потому что если создать механизм для задачи критериев, то это будет прям disruption в индустрии.


        1. TAU15 Автор
          10.05.2022 14:28

          Очень интересная мысль ???? при успешной реализации напишу продолжение статьи ????


  1. Biga
    10.05.2022 12:29
    +1

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


    1. TAU15 Автор
      10.05.2022 13:20

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


    1. amarao
      10.05.2022 13:40
      +1

      А в России запрещён in-suite? В Европе наличие in-suite bathroom (туалет, душ, а может, даже, и ванная) в master bedroom - это позиция в плюс (т.е. это указывают как преимущество в описании жилья).


      1. Biga
        10.05.2022 14:19
        +1

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


        1. amarao
          10.05.2022 14:29
          +1

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


          1. Ilusha
            11.05.2022 02:18
            +1

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

            Да и люди сами делают вход там, где им нужно. При покупке/продаже просто никто не запаривается.


  1. DirectoriX
    10.05.2022 13:38
    +2

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

    Только перебирал бы я не вообще все варианты расположения всех возможных квартир, а их отсортированные версии (потому что планы 001111223334 и 011231243130 имеют одинаковую ширину и процентовку, мы просто поменяли квартиры местами, прям как книги на полке переставили). Это, кстати, и в вашем генетическом алгоритме можно соптимизировать. Квартиры поменять местами в конце не трудно - хоть вручную, хоть случайно.


    1. TAU15 Автор
      10.05.2022 15:41

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


  1. LuchS-lynx
    10.05.2022 15:00
    +2

    Интересный подход. На самом деле эту задачу решает функциональная Архитектура

    https://ru.wikipedia.org/wiki/Архитектура_системы

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

    • кухня должна быть рядом с туалетом

    • спальная должна быть рядом с ванной/душевой

    • гостинный зал/комната играющая его роль должна быть рядом с кухней

    • и т.д. и т.п.

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

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

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



  1. TAU15 Автор
    10.05.2022 15:37

    Спасибо за наводку про функциональную архитектуру! Очень интересно.

    Вы все правильно поняли, алгоритм работает по самой примитивной схема пока и принимает квартиру просто как прямоугольник.

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


  1. DonAgosto
    10.05.2022 19:08
    +2

    А почему запилили свой велосипед, а не взяли готовый?
    Тот же scipy.optimize.differential_evolution например вполне удобный, и bounds и всякие constraints уже из коробки. Оно не сильно быстрое, но вполне может параллелится на имеющихся CPU.
    А вобще для multiobjective optimization даже несколько готовых фреймворков имеется, генетика кажется есть в pymoo и в pygmo.


    1. TAU15 Автор
      10.05.2022 20:24

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


  1. anick107
    11.05.2022 05:18
    +2

    Схожий прием используется в этой работе: https://estudogeral.sib.uc.pt/handle/10316/25438

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


    1. TAU15 Автор
      11.05.2022 05:20

      Спасибо большое за наводку! Поизучаю.


  1. Valien
    11.05.2022 10:38
    +1

    видео после первой минуты - ????


    1. TAU15 Автор
      11.05.2022 14:46
      +1

      Поправил видео ;)


  1. SmirnoffA
    11.05.2022 20:31
    -1

    Боже, какой бред.