Створення світу Досвід створення розумного життя своїми руками

Іноді проводиш день в спробах без використання термінів «рекурсивний виклик» і «ідіоти» пояснити головному бухгалтеру, чому насправді проста зміна облікової системи затягується майже на тиждень із-за орфографічної помилки, допущеної ким-то в коді у 2009 році. У такі дні хочеться пооборвать руки того розумника, який створив цей світ, і переписати все з нуля.

image

TL;DR
Під катом історія про те, як я в якості практики для вивчення Python розробляю свою бібліотеку для агентного моделювання з машинним навчанням і богами.

Посилання на github. Для роботи з коробки потрібен pygame. Для попереднього прикладу знадобиться sklearn.

Зародження ідеї
Ідея спорудити велосипед, про який я буду розповідати, поставала поступово. По-перше, популярність теми машинного навчання не обійшла мене стороною. Кілька курсів на курсере дали оманливе відчуття причетності. Кілька відкритих конкурсів і реєстрація на kaggle трохи відкоригували зарозумілість, однак, не охолодили запал.

По-друге, будучи представником касти недоторканних вітчизняного IT спільноти, я не часто маю можливість попрактикувати ніжно улюблений мною Python. А від розумних людей я чув, що свій проект в цьому плані — це якраз те, що треба.

Але поштовхом послужило розчарування No man's Sky. Технічно шикарна ідея, але процедурно згенерований світ виявився порожнім. І як будь-який розчарувався вболівальник, я почав думати, що б я зробив, якби мене запитували. І придумав, що світ виявився порожнім тому, що в ньому насправді дуже мало розумного життя. Безкраї простори, звичка покладатися тільки на себе, радість першовідкривача — це все, звичайно, добре. Але не вистачає можливості повернутися на базу, потинятися по ринку, дізнатися останні плітки в забігайлівці. Доставити посилку і отримати за це свої 100 золотих, в кінці кінців. Зрозуміло, що будь-яке місто, будь-який діалог або квест в іграх — плід праці живої людини і населити життям такий величезний світ силами людей не представляється можливим. Але що якщо ми б могли так само процедурно генерувати NPC з їх потребами, маленькими історіями і квестами?

План в загальних рисах
Так з'явилася ідея певної бібліотеки, або навіть, якщо дозволите, фреймворку, у якого були б такі сценарії використання:

  1. Класичне агентное моделювання (про існування якого я дізнався тільки тоді, коли сів писати цю статтю). Створюємо світ, описуємо дії агентів в цьому світі, дивимося, що вийшло, міняємо якісь параметри, запускаємо симуляцію ще раз. І так по колу, поки не з'ясуємо, як зміни в діях окремих агентів впливають на загальну картину. Дуже корисна штука.

  2. Навчання з підкріпленням (воно ж reinforcement learning). Побудова моделей навчання, що адаптуються до взаємодії з певною середовищем. Простий приклад — навчання грі, правила якої ви не знаєте, але в будь-який момент можете отримати інформацію про стан партії, вибрати одне з певного набору дій і подивитися, як це вплинуло на кількість зароблених вами очок (конкурс на цю тему, правда, вже закінчився). Тут багато відмінностей від звичайних моделей класифікаторів або регресій. Це і можлива отложенность результату, і необхідність планування, і багато інших особливостей.

  3. І, нарешті, після того, як ми створимо світ і населим його живністю, розумною і не дуже, непогано було б мати можливість відправитися туди особисто, прихопивши свій вірний бластер, улюблений меч, багатофункціональну кирку або червоний обценьки.
Трохи технічних подробиць
Отже, для початку треба визначитися з низькорівневої фізикою нашого світу. Вона повинна бути простою, але досить гнучкою для моделювання різних ситуацій:

  1. Візьмемо за основу звичайний клітинний автомат — двовимірний прямокутний дискретний світ, кожен об'єкт якого займає одну планковскую довжину в квадраті. Відстані менше планковскої довжини не будуть мати сенсу — не можна помістити об'єкт між двома клітинами, можна розташувати його так, щоб він займав більше однієї клітини, навіть не повністю.

  2. Відстань будемо вимірювати кроками тільки в чотирьох напрямках, тобто у клітини сусідніми будуть 4, а не 8. До тих, що по діагоналі, буде за 2 кроки.

  3. Щоб трохи розбавити монолітність отриманої конструкції додамо трохи глибини: кожен об'єкт буде володіти ознакою прохідності. За одним і тим же просторовими координатами в світі може перебувати не менше одного, але не більше двох об'єктів: прохідний та/або непрохідний. Можна подати це поверхня, на якій стоять і по якій переміщуються об'єкти. Типи поверхонь бувають різні, типи об'єктів теж. Можна на килим (прохідний об'єкт) поставити тумбу (непрохідний об'єкт). Але не можна постелити лінолеум, на ламінат (тому що, хто так робить взагалі?) і не можна поставити стілець на тумбу.

  4. Зате в тумбі можуть зберігатися різні предмети. І в килимі можуть, і в кишенях активних об'єктів теж. Тобто, будь-який об'єкт може бути контейнером для предметів. Але не для інших об'єктів, інакше ми порушимо третій закон.

  5. Час також йде дискретно. Кожен крок, кожен об'єкт живе одне планковское час, протягом якого він може отримати ззовні інформацію про світ навколо нього станом на дану епоху. Зараз це саме слабке місце — об'єктів доводиться діяти по черзі, з-за цього виходить деякий рассинхрон. Об'єктів, до яких «хід» доходить пізніше, доводиться враховувати стан об'єктів, що вже «були схожі» у цю епоху. Якщо дозволяти об'єктів орієнтуватися тільки на початок епохи, то це може призвести до того, що два непрохідних об'єкта, наприклад, встануть на одну і ту ж вільну на початку епохи клітку. Чи отримають з комода один і той же носок. Це можна трохи нівелювати, звертаючись до об'єктів кожну епоху у випадковому порядку, але такий підхід не вирішує проблему повністю.

Це дає нам кілька необхідних базових об'єктів: сам світ (Field), об'єкт цього світу (Entity) і предмет (Substance). Тут і далі-код в статті — просто ілюстрація. Повністю його можна подивитися у бібліотеці на github.

Класи Entity з прикладами
class Entity(object):
def __init__(self):
# universe home
self.board = None

# time-space coordinates
self.x = None
self.y = None
self.z = None

# lifecycle properties
self.age = 0
self.alive = False
self.time_of_death = None

# common properties
self.passable = False
self.scenery = True
self._container = []

# visualization properties
self.color = None

def contains(self, substance_type):
for element in self._container:
if type(element) == substance_type:
return True
return False

def live(self):
self.z += 1
self.age += 1

class Blank(Entity):
def __init__(self):
super(Blank, self).__init__()
self.passable = True
self.color = "#004400"

def live(self):
super(Blank, self).live)

if random.random() <= 0.0004:
self._container.append(substances.Substance())

if len(self._container) > 0:
self.color = "#224444"
else:
self.color = "#004400"

class Block(Entity):
def __init__(self):
super(Block, self).__init__()
self.passable = False
self.color = "#000000"


Клас Field
class Field(object):
def __init__(self, length, height):
self.__length = length
self.__height = height
self.__field = []
self.__epoch = 0
self.pause = False

for y in range(self.__height):
row = []
self.__field.append(row)
for x in range(self.__length):
if y == 0 або x == 0 or y == (height - 1) or x == (length - 1):
init_object = Block()
else:
init_object = Blank()

init_object.x = x
init_object.y = y
init_object.z = 0

row.append([init_object])


Клас Substance описувати сенсу не має, у ньому нічого немає.

За час у нас буде відповідати сам світ. Кожну епоху він буде опитувати що в ньому знаходяться об'єкти і змушувати їх робити хід. Як вони проведуть цей хід, вже їх справа:

Час, вперед!
class Field(object):
...
def make_time(self):
if self.pause:
return

for y in range(self.height):
for x in range(self.length):
for element in self.__field[y][x]:
if element.z == self.epoch:
element.live)

self.__epoch += 1
...


Але навіщо нам світ, та ще й із запланованою можливістю помістити в нього протагоніста, якщо ми не можемо його побачити? З іншого боку, якщо почати розбиратися з графікою, можна сильно відволіктися, і правління світом відкладеться на невизначений термін. Тому не витрачаючи часу освоюємо ось цю чудову статтю про написання платформера з допомогою pygame (насправді, нам знадобиться тільки перша третина статті), видаємо кожному об'єкту ознака кольору, і ось у нас вже є якась подоба карти.

Код візуалізації
class Field(object):
...
def list_obj_representation(self):
representation = []
for y in range(self.height):
row_list = []
for cell in self.__field[y]:
row_list.append(cell[-1])
representation.append(row_list)
return representation
....

def visualize(field):
pygame.init()
screen = pygame.display.set_mode(DISPLAY)
pygame.display.set_caption("Field game") 
bg = Surface((WIN_WIDTH, WIN_HEIGHT))
bg.fill(Color(BACKGROUND_COLOR))

myfont = pygame.font.SysFont("monospace", 15)

f = field
tick = 10

timer = pygame.time.Clock()
go_on = True

while go_on:
timer.tick(tick)
for e in pygame.event.get():
if e.type == QUIT:
raise SystemExit, "QUIT"
if e.type == pygame.НАТИСКАННЯ:
if e.key == pygame.K_SPACE:
f.pause = not f.pause
elif e.key == pygame.K_UP:
tick += 10
elif e.key == pygame.K_DOWN and tick >= 11:
tick -= 10
elif e.key == pygame.K_ESCAPE:
go_on = False

screen.blit(bg, (0, 0))

f.integrity_check()
f.make_time()
level = f.list_obj_representation()
label = myfont.render("Epoch: {0}".format(f.epoch), 1, (255, 255, 0))
screen.blit(label (630, 10))

stats = f.get_stats()
for i, element in enumerate(stats):
label = myfont.render("{0}: {1}".format(element, stats[element]), 1, (255, 255, 0))
screen.blit(label (630, 25 + (i * 15)))

x = y = 0

for row level in:
for element in row:
pf = Surface((PLATFORM_WIDTH, PLATFORM_HEIGHT))
pf.fill(Color(element.color))
screen.blit(pf, (x, y))

x += PLATFORM_WIDTH
y += PLATFORM_HEIGHT
x = 0

pygame.display.update()


Звичайно, пізніше можна буде написати кілька більш зрозумілу модуль візуалізації, та не один. Але поки різнокольорових бігають квадратиків цілком вистачає для того, щоб зануритися в атмосферу зароджується буття. До того ж, це розвиває фантазію.

image

Тепер треба продумати, як активні агенти будуть діяти. По-перше, всі значимі дії будуть об'єктами (об'єктами Python, не об'єктами світу, приношу вибачення за неоднозначність). Так можна зберігати історію, маніпулювати їх станом, відрізняти одну дію від іншого, навіть якщо вони однотипні. Отже, дії будуть виглядати наступним чином:

  1. Кожна дія повинна мати суб'єкт. Суб'єктом дії може бути тільки об'єкт нашого світу (Entity).

  2. Кожна дія повинна мати результати. Як мінімум «завершено/не завершено» і «мета досягнута/мета не досягнута». Але можуть бути і додаткові залежно від типу дії: наприклад, дія «НайтиБлижайшуюПиццерию» може в якості результатів, крім обов'язкових, мати координати або сам об'єкт піцерії.

  3. Кожна дія може мати, а може не мати набір параметрів. Наприклад, дія «НалитьЧашечкуКофе» може не мати параметрів, так як не вимагає уточнення, в той час як для дії «Налити» необхідна можливість уточнити, що налити і куди.

  4. Дія може бути миттєвим або не миттєвим. Протягом однієї доби на один об'єкт може зробити не більше одного миттєвої дії і будь-яку кількість миттєвих. Це спірний момент — якщо у нас дискретно простір і ми не можемо посунутися на полклетки, то можливість здійснювати необмежену кількість дій протягом однієї доби виглядає дивним і дещо розмиває чітке дискретне протягом часу. Була також ідея задавати кожному типу дій час, який необхідно на нього витратити, в межах від 0 до 1, де дія тривалістю в 1 займає всю епоху. Поки я зупинився на варіанті з ознакою миттєвостей, так як для чіткості дискретного часу завжди можна всі дії, необхідні для симуляції, зробити не миттєвими, а варіант з тривалістю все надто ускладнює.
Таким чином, з технічної точки зору об'єкт дії (Action) представляє собою деяку подобу функції, якій можна задати параметри, виконати, отримати результат, і яка при цьому сама в собі зберігає і передані їй параметри, і результат, і все що завгодно, що пов'язано з її виконанням, починаючи з того, хто її викликав, і закінчуючи станом світу навколо під час її виконання. Тому ми можемо в один час її створити, в інше задати параметри, третє виконати, отримати значення, що повертається, і покласти на поличку для подальшого аналізу.

Об'єкт Action
class Action(object):
def __init__(self, subject):
self.subject = subject
self.accomplished = False
self._done = False
self.instant = False

def get_objective(self):
return {}

def set_objective(self, control=False, **kwargs):
valid_objectives = self.get_objective().keys()

for key in kwargs.keys():
if key not in valid_objectives:
if control:
raise ValueError("{0} is not a valid objective".format(key))
else:
pass # maybe need to print
else:
setattr(self, "_{0}".format(key), kwargs[key])

def action_possible(self):
return True

def do(self):
self.check_set_results()
self._done = True

def check_set_results(self):
self.accomplished = True

@property
def results(self):
out = {"done": self._done, "accomplished": self.accomplished}
return out

def do_results(self):
self.do()
return self.results


Якщо хто-небудь крім мене раптом захоче створити собі затишний світ з допомогою цієї бібліотеки, то передбачається, що з коробки вона буде містити набір необхідних основних дій — вирушити з координатами, слідувати за об'єктом, знайти певний об'єкт або предмет, підібрати предмет і т. д. Цими діями можна буде користуватися як самими по собі, так і комбінувати їх для твору якихось складних маніпуляцій. Приклад таких комплексних дій буде далі, в описі першого експерименту.

По-друге, кожен поважаючий себе активний агент повинен вміти планувати свої дії. Тому розділимо фазу його активності протягом доби на 2 етапи: планування і дію. В якості інструменту планування у нас буде проста чергу дій, які ми будемо послідовно виконувати. Проте якщо у нас вже є план, то нема чого зайвий раз роздумувати, треба діяти швидко, рішуче. Виходить, що на початку ходу активний об'єкт визначає, чи треба на цей хід планувати (для початку будемо вважати, що треба, коли черга дій порожня), потім планує, якщо вирішив, що це необхідно, і в кінці виконує дії. Чи повинна планування, як серйозний процес, який не терпить поспіху, займати весь хід — питання дискусійне. Для своїх цілей я поки зупинився на тому, що ні — мої агенти довго не роздумують і приступають до виконання плану в той же хід.

Планування і дію
class Agent(Entity):
...
def live(self):
...
if self.need_to_update_plan():
self.plan()

if len(self.action_queue) > 0:

current_action = self.action_queue[0]

self.perform_action(current_action)

while len(self.action_queue) > 0 and self.action_queue[0].instant:
current_action = self.action_queue[0]

self.perform_action(current_action)

def need_to_update_plan(self):
return len(self.action_queue) == 0

def perform_action(self, action):
results = action.do_results()

if results["done"] or not action.action_possible():
self.action_log.append(self.action_queue.pop(0))

return results
...
...


Додатково до цього мені здалося зручним ввести таку сутність як стан об'єкта, яке могло б впливати на його дії. Адже, агент може втомитися, бути не в настрої, намокнути, отруїтися або навпаки, бути веселим і повним сил. Іноді навіть одночасно. Тому додамо нашим об'єктам масив станів, кожне з яких буде впливати на об'єкт на початку епохи.

Код про стан
class State(object):
def __init__(self, subject):
self.subject = subject
self.duration = 0

def affect(self):
self.duration += 1

class Entity(object):
def __init__(self):
...
self._states_list = []
...
...
def get_affected(self):
for state in self._states_list:
state.affect()

def live(self):
self.get_affected()
self.z += 1
self.age += 1
...


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

Приблизно так
import copy

def run_simulation(initial_field, check_stop_function, score_function, times=5, verbose=False):
list_results = []
for iteration in range(times):
field = copy.deepcopy(initial_field)
while not check_stop_function(field):
field.make_time()
current_score = score_function(field)
list_results.append(current_score)
if детального:
print "Iteration: {0} Score: {1})".format(iteration+1, current_score)

return list_results


На цьому етапі всі, в принципі, готовий для закриття першого сценарію використання нашої бібліотеки: моделювання, якщо ми не хочемо навчати агентів, а хочемо прописувати логіку їхніх дій самостійно. Порядок дій у такому випадку наступний:

  1. Вирішуємо, які статичні об'єкти ми хочемо бачити в світі: стіни, гори, меблі, типи поверхонь і т. д. Описуємо їх, наслідуючи клас Entity. Те ж саме робимо з предметами і класом Substance.
  2. Створюємо світ потрібного розміру, заповнюємо його пейзажем з цих об'єктів і предметів.
  3. Успадковуємо клас Action і описуємо всі потрібні нам дії. Те ж саме робимо з класом State і станами, якщо вони потрібні для нашої симуляції.
  4. Створюємо клас наших агентів, наслідуючи Agent. Додаємо в нього службові функції, описуємо процес планування.
  5. Населяем наш світ активними агентами.
  6. Для налагодження дій і насолоди спогляданням свого творіння можна поганяти візуалізацію.
  7. І в підсумку, награвшись з візуалізацією, запускаємо симуляцію і оцінюємо, наскільки вдало створені нами агенти грають за створеними нами правилами в створеному нами світі.
Proof of concept I
Отже, оголосимо умови першого експерименту.

  • Світ складається з: стіни, земля. Стіни — просто непрохідні стіни, більше нічого. З землею цікавіше — кожну епоху є ненульова ймовірність, що в будь-який або в декількох клітинах землі з'явиться одиниця ресурсу.
  • Населення: істоти двох статей. Так як для простоти ми пів будемо зберігати в логічної змінної, підлога може бути помилковий або істинний.
  • Істоти помилкового підлоги жаднюги і мають на меті свого життя зібрати якомога більше ресурсу. Як тільки вони з'являються, вони знаходять найближчу клітку з ресурсом, йдуть до неї, збирають ресурс і так по колу. Однак саме вони наділені здатністю до дітородіння.
  • Істоти істинного підлоги трохи різноманітніше. У них на вибір є два дії: теж збирати ресурс або шукати партнера для спаровування (природно, протилежної статі, щоб ненароком не потрапити в місця, де ресурсів небагато, а про можливих партнерів для спаровування краще навіть не думати).
  • Коли вирішила злучатися істота істинного підлоги наздоганяє обраного партнера і пропонує йому усамітнитися, істота помилкового підлоги вирішує, розташоване воно до спаровування, по визначеним правилам, заснованим на кількості ресурсу в обох учасників. Якщо у пропонує ресурсів більше, то він отримує згоду. Якщо менше або однаково, то ймовірність спарювання залежить від різниці в кількості ресурсів.
  • Через десять епох після зачаття народжується істота випадкового підлоги. Воно з'являється на світ відразу, дорослим, і діє у відповідності з правилами для своєї статі.
  • Всі істоти повинні померти. У кожної істоти кожну епоху, починаючи з десятої після його народження, є ненульова і постійна ймовірність припинити своє активне існування.
Нашої завданням буде написати процедуру планування для істот істинного підлоги таким чином, щоб популяція істот розмножувалася як можна швидше.

Які дочитали до цього місця я із вдячності не буду втомлювати довгими ілюстраціями, покажу тільки:
Варіант реалізації комплексної дії на прикладі спарювання
class GoMating(Action):
def __init__(self, subject):
super(GoMating, self).__init__(subject)

self.search_action = SearchMatingPartner(subject)
self.move_action = MovementToEntity(subject)
self.mate_action = Mate(subject)

self.current_action = self.search_action

def action_possible(self):

if not self.current_action:
return False

return self.current_action.action_possible()

def do(self):
if self.subject.has_state(states.NotTheRightMood):
self._done = True
return

if self.results["done"]:
return

if not self.action_possible():
self._done = True
return

first = True

while first or (self.current_action and self.current_action.instant) and not self.results["done"]:

first = False

current_results = self.current_action.do_results()

if current_results["done"]:
if current_results["accomplished"]:
if isinstance(self.current_action, SearchMatingPartner):
if current_results["accomplished"]:
self.current_action = self.move_action
self.current_action.set_objective(**{"target_entity": current_results["partner"]})
elif isinstance(self.current_action, MovementXY):
self.current_action = self.mate_action
self.current_action.set_objective(**{"target_entity": self.search_action.results["partner"]})
elif isinstance(self.current_action, Mate):
self.current_action = None
self.accomplished = True
self._done = True
else:
self.current_action = None
self._done = True
else:
break

def check_set_results(self):
self.accomplished = self._done


І варіант планування, на якому я вирішив, що модель працює
class Creature(Agent):
...
def plan(self):
nearest_partner = actions.SearchMatingPartner(self).do_results()["partner"]
if nearest_partner is None:
chosen_action = actions.HarvestSubstance(self)
chosen_action.set_objective(** {"target_substance_type": type(substances.Substance())})
self.queue_action(chosen_action)
else:
self_has_substance = self.count_substance_of_type(substances.Substance)
partner_has_substance = nearest_partner.count_substance_of_type(substances.Substance)
if partner_has_substance - self_has_substance > 2:
self.queue_action(actions.GoMating(self))
else:
chosen_action = actions.HarvestSubstance(self)
chosen_action.set_objective(**{"target_substance_type": type(substances.Substance())})
self.queue_action(chosen_action)
...


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

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

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

І ось як я собі це уявляю:

  • Для того щоб набрати датасет і приймати рішення, нам треба декларативно описати отримання ознак, на яких ми будемо тренуватися і які ми будемо використовувати для передбачення моделі в процесі планування. Зараз я реалізував це як масив функцій, кожна з яких повертає значення певної ознаки. Для нашого тестового завдання це, наприклад, функція, що повертає наявність можливих партнерів, ще одна, яка вважає кількість ресурсу в істоти, що приймає рішення, ще відстань до найближчого ресурсу і т. д. Можливо, вдаліше була б функція, що повертає масив ознак, але на момент написання масив функцій здався мені зручніше. Таким чином, ми отримуємо можливість у будь-який момент часу одержати цікавлячу нас опис навколишнього світу.

  • Так само декларативно описуємо спосіб отримання потрібного нам результату. У разі нашого прикладу це, припустимо, вдало або невдало пройшло спарювання.

  • Вказуємо модель навчання, яку хочемо використовувати, і її параметри. Наприклад, стохастичний градієнтний спуск, випадковий ліс, яку-небудь нейромережа або взагалі щось своє.

  • Запускаємо моделювання. Спочатку кожен раз, коли необхідно прийняти рішення, істоти з описаних нами правилами отримують масив ознак і вибирають якусь дію без використання моделі (яка у нас ще порожня). Виконавши цю дію вони знову ж таки за описаним нами правилами визначають результат, з'єднують його з описом обстановки, в якій рішення було прийнято (набір ознак, отриманий перед дією), і вуаля, у нас готовий семпл для тренувального датасета. Проробивши це певну кількість разів і накопичивши достатньо даних, істоти, нарешті, згодовують тренувальний датасет в модель. Після того як модель натренована, вони вже в процесі планування починають використовувати її.

  • Далі можливі варіанти. Можна на цьому і зупинитися. А можна продовжувати запам'ятовувати набори ознак і результати дій і шматками згодовувати їх моделі, якщо модель дозволяє доучуватися. Або, наприклад, перетренировывать її у випадку, якщо відсоток бажаних результатів починає знижуватися.
Тут нам знадобиться кілька нових об'єктів. По-перше, у істот повинна бути якась пам'ять, в яку вони будуть складати свої датасеты. Вона повинна вміти окремо запам'ятовувати набір ознак. Окремо приєднувати до них результат рішення, прийнятого при цьому наборі ознак. Повертати нам датасет в зручному вигляді. Ну, і забувати все, чому нас вчили у вузі.

Таємниці пам'яті
class LearningMemory(object):
def __init__(self, host):
self.host = host
self.memories = {}

def save_state(self, state, action):
self.memories[action] = {"state": state}

def save_results(self, results, action):
if action in self.memories:
self.memories[action]["results"] = results
else:
pass

def make_table(self, action_type):
table_list = []
for memory in self.memories:
if isinstance(memory, action_type):
if "state" not in self.memories[memory] or "results" not in self.memories[memory]:
continue
row = self.memories[назва]["state"][:]
row.append(self.memories[назва]["results"])
table_list.append(row)

return table_list

def obliviate(self):
self.memories = {}


По-друге, нам треба навчити агентів отримувати завдання і запам'ятовувати обстановку і результати своїх дій.

Отримання завдань
class Agent(Entity):
def __init__(self):
...
self.memorize_tasks = {}
....
...
def set_memorize_task(self, action_types, features_list, target):
if isinstance(action_types, list):
for action_type in action_types:
self.memorize_tasks[action_type] = {"features": features_list,
"target": target}
else:
self.memorize_tasks[action_types] = {"features": features_list,
"target": target}

def get_features(self, action_type):
if action_type not in self.memorize_tasks:
return None

features_list_raw = self.memorize_tasks[action_type]["features"]
features_list = []

for feature_raw in features_list_raw:
if isinstance(feature_raw, dict):
if "kwargs" in feature_raw:
features_list.append(feature_raw["func"](**feature_raw["kwargs"]))
else:
features_list.append(feature_raw["func"]())
elif callable(feature_raw):
features_list.append(feature_raw())
else:
features_list.append(feature_raw)

return features_list

def get_target(self, action_type):
if action_type not in self.memorize_tasks:
return None

target_raw = self.memorize_tasks[action_type]["target"]

if callable(target_raw):
return target_raw()
elif isinstance(target_raw, dict):
if "kwargs" in target_raw:
return target_raw["func"](**target_raw["kwargs"])
else:
return target_raw["func"]()
else:
return target_raw

def queue_action(self, action):

if type(action) in self.memorize_tasks:
self.private_learning_memory.save_state(self.get_features(type(action)), action)
self.public_memory.save_state(self.get_features(type(action)), action)

self.action_queue.append(action)

def perform_action_save_memory(self, action):
self.chosen_action = action

if type(action) in self.memorize_tasks:
results = self.perform_action(action)
if results["done"]:
self.private_learning_memory.save_results(self.get_target(type(action)), action)
self.public_memory.save_results(self.get_target(type(action)), action)
else:
results = self.perform_action(action)
...


І, нарешті, нам потрібно якийсь менеджер, по-батьківськи наглядає за всіма агентами, роздає їм завдання, встановлює моделі навчання, що пояснює, як їх використовувати, рятує від лукавого, многомилостивий і всеблагий. Простіше кажучи, чим прописувати всю логіку в агентах, а загальні об'єкти зберігати десь у властивостях світу або ще де-небудь, нам зручніше вклинюватися в момент появи агента в нашому світі і наділяти його свідомістю, визначаючи всі необхідні властивості типу пам'яті, способи планування і моделі навчання.

Для цього створимо об'єкт Demiurge, який буде підключатися до створюваного світу. Всі об'єкти, окрім початкових, повинні з'являтися у світі за допомогою методу insert_object. В нього і буде вклинюватися наше божество, попередньо обробляючи своє чергове творіння:

Теогонія
class Demiurge(object):
def handle_creation(self, creation, refuse):
pass

class Field(object):
def __init__(self, length, height):
...
self.demiurge = None
...
def insert_object(self, x, y, entity_object, epoch_shift=0):
if self.demiurge is not None:
refuse = False
self.demiurge.handle_creation(entity_object, refuse)
if refuse:
return

assert x < self.length
assert y < self.height

self.__field[y][x][-1] = entity_object

entity_object.z = self.epoch + epoch_shift

entity_object.board = self
entity_object.x = x
entity_object.y = y
...


Такий підхід дає багато можливостей:

  • Вся логіка навчання винесена в окреме незалежне (наскільки це можливо) місце. Якщо у нас є добре продумані і описані об'єкти агентів і дій, то ми можемо спокійно концентруватися на нюансах навчання.

  • Ми можемо швидко і безболісно замінити одного деміурга іншим — для порівняння, або, наприклад, в змагальних цілях. Наприклад, можна створити світ, агентів і дії і влаштувати конкурс, чий бог самий істинний найбільш ефективно навчить агентів виконувати поставлене завдання.

  • є ідея (поки нереалізована) створювати цілі пантеони, щоб демиурги доповнювали один одного (один вчить ковальства, інший вирощувати виноград, третій плодитися і розмножуватися). Або навпаки, конкурували, і можна було влаштовувати локальний Рагнарок силами послідовників різних богів.
Додатково агентам була додана така функція як використання публічних або приватних пам'яті і моделі навчання. Публічна пам'ять — щось на зразок загальнодоступних описів експериментів. У неї опису своїх дій і їх результатів можуть записати всі агенти і прочитати теж може будь-хто. Публічна модель навчання — підручник з описом того, що треба робити в тій чи іншій ситуації, яким може скористатися будь-хто. Ну, а приватні пам'ять і модель навчання — це життєвий досвід і вміння кожного конкретного агента. Їх можна комбінувати, наприклад, черпати приклади з загальної пам'яті, але мати свою приватну модель навчання.

Proof of concept II
Отже, спробуємо вирішити ту ж саму задачу з збиранням ресурсів та істотами помилкового підлоги. Так як задача досить проста, то у нас навряд чи вийде вирішити її за допомогою машинного навчання краще, ніж можна було б за допомогою жорстко заданого алгоритму. Однак для визнання концепції робочої нам досить того, щоб наші збирачі не вимерли як вид від небажання чи невміння розмножуватися.

Нижче повний текст прикладу вирішення такого завдання:

  • Успадковуємо деміурга і створюємо своє божество (Priapus).
  • Пам'ять і модель навчання будемо використовувати загальні, тому визначаємо їх в оголошенні. Модель виберемо, наприклад, SGDClassifier з бібліотеки sklearn.
  • Перевизначаємо handle_creation.
  • У рішенні про те, паруватися або збирати будемо керуватися трьома параметрами: в настрої чи ми, чи взагалі існує потенційний партнер і різниця в рівнях добробуту. Описуємо те, як ці параметри отримати, і даємо завдання агенту запам'ятовувати їх кожен раз, коли він вирішить спаровуватися.
  • Описуємо процес планування: поки модель навчання не готова, будемо діяти рандомно і все запам'ятовувати (до речі, ніхто не знає, як в sklearn визначити, навчена модель чи ні?). Як тільки назбираємо достатньо досвіду (в даному випадку 20 спроб на всіх, так як ми використовуємо загальну пам'ять), намагаємося передбачити, хороша ідея злучитися в поточних обставинах. Якщо так, то біжимо в найближчий кіоск за вином і шоколадкою. Якщо ні, вирушаємо добувати ресурс.
  • Проганяємо 30 разів по 500 епох і дивимося середня кількість живуть на 500ю епоху істот. Робимо те ж саме з використанням рандомного планування і бачимо, що стохастичний градієнтний спуск трохи, але все ж краще.
  • Включаємо візуалізацію і розчулюємося свого творіння.
Що вийшло
# Create deity
class Priapus(field.Demiurge): # Create deity
def __init__(self):
self.public_memory = brain.LearningMemory(self)
self.public_decision_model = SGDClassifier(warm_start=True)

def handle_creation(self, creation, refuse):
if isinstance(creation, entities.Creature):
creation.public_memory = self.public_memory
creation.public_decision_model = self.public_decision_model
creation.memory_type = "public"
creation.model_type = "public"
creation.memory_batch_size = 20

if creation.sex:
def difference_in_num_substance(entity):
nearest_partner = actions.SearchMatingPartner(entity).do_results()["partner"]
if nearest_partner is None:
return 9e10
else:
self_has_substance = entity.count_substance_of_type(substances.Substance)
partner_has_substance = nearest_partner.count_substance_of_type(substances.Substance)
return partner_has_substance - self_has_substance


def possible_partners_exist(entity):
find_partner = actions.SearchMatingPartner(entity)
search_results = find_partner.do_results()
return float(search_results["accomplished"])

features = [{"func": lambda creation: float(creation.has_state(states.NotTheRightMood)),
"kwargs": {"creation": creation}},
{"func": difference_in_num_substance,
"kwargs": {"entity": creation}},
{"func": possible_partners_exist,
"kwargs": {"entity": creation}}]

creation.set_memorize_task(actions.GoMating, features,
{"func": lambda creation: creation.chosen_action.results["accomplished"],
"kwargs": {"creation": creation}})

def plan(creature):
if creature.sex:
try:
# raise NotFittedError
current_features = creature.get_features(actions.GoMating)
current_features = np.asarray(current_features).reshape(1, -1)
if creature.public_decision_model.predict(current_features):
go_mating = actions.GoMating(creature)
creature.queue_action(go_mating)
return
else:
harvest_substance = actions.HarvestSubstance(creature)
harvest_substance.set_objective(
**{"target_substance_type": type(substances.Substance())})
creature.queue_action(harvest_substance)
return
except NotFittedError:
chosen_action = random.choice(
[actions.GoMating(creature), actions.HarvestSubstance(creature)])
if isinstance(chosen_action, actions.HarvestSubstance):
chosen_action.set_objective(
**{"target_substance_type": type(substances.Substance())})
creature.queue_action(chosen_action)
return
else:
harvest_substance = actions.HarvestSubstance(creature)
harvest_substance.set_objective(**{"target_substance_type": type(substances.Substance())})
creature.queue_action(harvest_substance)

creation.plan_callable = plan


universe = field.Field(60, 40) # Create sample universe (length, height

universe.set_demiurge(Priapus()) # Assign deity to universe

# Fill universe with чарівна історія пусті клітинки, blocks, other scenery if necessary
for y in range(10, 30):
universe.insert_object(20, y, field.Block())

for x in range(21, 40):
universe.insert_object(x, 10, field.Block())

for y in range(10, 30):
universe.insert_object(40, y, field.Block())

universe.populate(entities.Creature, 20) # Populate universe with creatures

def check_stop_function(field):
return field.epoch >= 500


def score_function(field):
stats = field.get_stats()
if "Creature" not in stats:
return 0
else:
return stats["Creature"]

res = modelling.run_simulation(universe, check_stop_function, score_function, verbose=True, times=30)
print res
print np.asarray(res).mean()


Висновок
В такому вигляді, як вона зараз є бібліотека, звичайно, являє собою заготівлю альфа-версії тестового варіанту ілюстрації концепції. А третій сценарій використання (про занурення в створений світ) взагалі не готовий. І ось що я маю намір робити далі:

  • Розширення можливостей для машинного навчання. А конкретно додавання можливості довгострокового планування та обліку відкладеного результату дій.
  • Розширення можливостей управління світом при візуалізації (прибрати/додати об'єкти, подивитися статистику окремого агента, зберегти/завантажити стан світу і т. д.). І взагалі доопрацювання візуалізації — можливо, навіть з підключенням якогось веб-інтерфейсу.
  • Розширення бібліотеки стандартних дій.
  • Вступ багатобожжя.
  • Додавання можливості зійти в створений світ і відправиться в шлях, повний випробувань, пригод і нових знайомств.
  • Покриття тестами
  • Оптимізація
  • ...
  • PROFIT
Я, звичайно, віддаю собі звіт в тому, що це за фактом локальний хобі-проект, бачу багато багів і неоптимальних рішень в своєму коді і знаю, що напевно є ще щось, чого я поки не бачу. Але я б злукавив, якби сказав, що десь у потаємних закутках своєї темної душі не сподіваюся, що ця бібліотека може перерости у щось, що можна буде використовувати для прикладних завдань, хоча б у вигляді ідеї або підходу. Тому закликаю всіх зацікавлених вказувати мені на мої криві руки, обзивати індусом і тикати носом в книжки про патерни, якщо це допоможе зробити світ краще. Або агентів розумніший, або богів всемогутня.

Ще раз посилання на github. І нагадую про pygame і sklearn, якщо раптом у вас ще немає.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.