Проблема нейросетей - невозможность обучаться на единичных примерах. Справиться может табличное RL, но обучаться на данных большой размерности - иная неразрешимая сторона этой парадигмы https://habr.com/ru/post/437020/. Решение только в одном: видеть мир иерархически, где каждая его подчасть также может быть выражена иерархически.

Как здесь https://habr.com/ru/post/420219/ - сложность игры которой сокращается за счет введения небоевого состояния, подсостояниями которого являются ожидание и патрулирование.

Хабр. Обзор техник реализации игрового ИИ
Хабр. Обзор техник реализации игрового ИИ

Теперь маштабируем этот подход. На этот счет интересны рассуждения В.Турчина

Турчин // Феномен науки. Кибернетический подход к эволюции
Турчин // Феномен науки. Кибернетический подход к эволюции

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

Вот что пишет об этом А. Карпатый https://habr.com/ru/post/439674/. Как только вы поймете «хитрость», с помощью которой работают эти алгоритмы, вы сможете понять их сильные и слабые стороны. В частности, эти алгоритмы далеко отстают от людей в построении абстрактных представлений об играх.

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

Р. Эшби .//Введение в кибернетику [стр 63]
Р. Эшби .//Введение в кибернетику [стр 63]

Пусть имеется мячик (синий квадрат) как в игре пинг-понг, летящий по некой траектории.

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

Из трассировок видно, например, что из состояния (1,2) следует как состояние (1,3), так и состояния (2,3), (0,3). По этой причине невозможно (если переходы равновероятны) поймать синий мячик из этого состояния. Выигрыш не более 1/3, если основываться только на политике без учета предшествующих состояний.

Можно попытаться найти такое действие, которое бы приводило к желаемой однозначности. Им является предшествующее состояние. Тогда: (1,2) перейдет однозначно в (1,3) при действии (1,1); (1,2) перейдет только в (2,3) при действии (0,1); (1,2) перейдет только в (0,3) при (2,1).

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

  1. Действие неизвестно. По умолчанию оно одно (условно f). Коэффициент однозначности 0.6

  2. Вносим неустраняемую неопределенность отскока от черного квадрата. Коэффициент будет ниже.

  3. Выбраны верные действия. Коэффициент единица (однозначность сто процентов),. Теперь, зная граф переходов, безошибочно вычисляется куда прилетит синий мяч.

  4. Добавляем неопределенность к верным действиям. Из состояния (1.2) она максимальна из-за черного квадрата (неопределенный отскок)

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

Ниже лабиринт (Л) - это одно из сменяемых состояний {11, 12, 14, 15} в результате действий {31, 32}. В свою очередь, состояние лабиринта ключ (11) тоже выражено сменой своих подсостояний {45, 44, 41} в результате действий {35, 36, 34}. И состояние лабиринта дверь (12) управляется ключом (11). Управляется по той причине, что состояния ключа являются действиями для двери. Когда подсостояние двери принимает значение открыто (54), агент может из лабиринта (Л) перейти в комнату (км) и найти клад (63). В отдельно взятой таблице поиск оптимального пути может осуществляться средствами RL.

Но! В начале игры агент ничего не ведает, что есть клад. Не ведает что есть эти отдельности (лабиринт, ключ, двер, комната). Он лишь наблюдает поток данных. Сначала [32, 52, .., 52, ..], затем [.., .., 44, .. 36] и т.д. Рассматривает разные гипотезы, стремясь максимизировать коэффициент однозначности. Так, рассмотрев черную гипотезу, приходит к выводу, что есть некая сущность (11), состояниями которой являются {45, 44, 41}. Обнаруживает также однозначность переходов сущности (12). И далее уже о сущности (Л) тоже мыслит как о какой-то отдельности, переходы состояний которой однозначны. По той простой причине, что для красной гипотезы (попытка понять что есть лабиринт) состояния {44, 41, 45} заменятся на (11), а состояния {52, 53, 54} заменятся на (12).

По Турчину, (11) и (12) являются классификаторами нижнего уровня для классификатора (Л) более верхнего уровня, образуя иерархию понятий.

Ниже представлен код для игры в понг, но без части максимизации однозначности и без оптимизации маршрута по графу (состояние и действие мяча, состояние и действие ловящего мяч) . Без комментариев.

# Выразить иерархически: вопрос как увидеть хамелена
from tkinter import *
import random


class Hd():
	def __init__(self, graph):
		self.graph = graph
		
	def rnd_get(self, v):
		return random.choice(v.split("|"))
		
	def show(self, graph):
		for x in graph:
			print(x, ":", graph[x])
			
	def get_ku(self, graph):
		'''return (коэффициент однозначности, размер)'''
		n = 0; u = 0
		for x in graph:
			for y in graph[x]:
				if y[0]:
					u += y[0].count("|")
					n += 1
		if n != 0:
			return (round(1- u/(n+u), 3), n) 
		else:
			return (None, None)
			
	def get_states(self, x, f_list = [], n=11):
		x_list = []
		x_list.append(x)
		
		if f_list != []:
			for f in f_list:
				xf = [xf for xf in g[x] if xf[1] == f]
				if not xf:
					x_list.append(''); f_list[len(x_list)-2] = ''
					return (x_list, f_list[:len(x_list)-1])
				x = self.rnd_get(xf[0][0])
				x_list.append(x)
		else:
			for i in range(n):
				if not self.graph[x]:
					x_list.append(''); f_list.append('')
					break
				t = random.choice(self.graph[x])
				f = t[1]
				x = random.choice(t[0].split('|'))
				x_list.append(x); f_list.append(f)
			
		if len(f_list) == len(x_list) -1:
			return (x_list, f_list)
		else:
			return ([], [])
			
	def get_yfx(self, f_list, x_list, t = True):
		if len(x_list) == len(f_list):
			x_list.append('')
			
		path = []
		for i in range(len(f_list)):
			path.append((x_list[i], f_list[i], x_list[i+1]))
		
		if t:	
			return path #(x, f, next_x)
		else:
			p = []
			for xfy in path:
				if xfy[2] != '':
					p.append(xfy[2]+'='+xfy[1]+'('+xfy[0]+')')
			return p
			
	def flow(self, path, rnd=False):
		if not path:  return []
		fl = []
		for p in path:
			if not rnd:
				fl.append(p[:-1])
			else:
				pr = list(p[:-1])
				#random.shuffle(pr)
				fl.append(tuple(pr))
				fl.append(pr)
		return fl
		
	def fORx(self, flow, f=0):
		'''index: f=0, x=1 or x=0, f=1'''
		def add_empty():
			empty = []
			for k in graph:
				for x in graph[k]:
					z = list(set(x[0].split('|')) - set(list(graph.keys())))
					if z:
						empty.append(z[0])
			return empty
			
		if not flow: return []
		
		graph = {}
		fli = flow[0]
		
		for t in flow[1:]:
			if f == 0:
				if fli[1] not in graph:
					graph[fli[1]] = []
				r = [(i, xf) for i,xf in enumerate(graph[fli[1]]) if xf[1] == fli[0]]
				if r:
					if t[1] not in r[0][1][0]:
						x_new = r[0][1][0] + "|" + t[1]
						if x_new != '':
							graph[fli[1]][r[0][0]] = (x_new, r[0][1][1])
				if not r:
					if t[1] != '':
						graph[fli[1]].append((t[1], fli[0]))
					
			if f == 1:
				if fli[0] not in graph:
					graph[fli[0]] = []
				r = [(i, xf) for i,xf in enumerate(graph[fli[0]]) if xf[1] == fli[1]]
				if r:
					if t[0] not in r[0][1][0]:
						x_new = r[0][1][0] + "|" + t[0]
						if x_new != '':
							graph[fli[0]][r[0][0]] = (x_new, r[0][1][1])
				if not r:
					if t[0] != '':
						graph[fli[0]].append((t[0], fli[1]))
			fli = t
				
		em = add_empty()
		if em:
			graph[em[0]] = []
		return graph
		
	def merge(self, g1, g2):
		def find_index():
			for i in range(len(graph[k])):
				if graph[k][i][1] == x[1]:
					return i
			return -1
					
		all_keys = set(list(g1.keys()) + list(g2.keys()))
		graph = {}
		
		for k in all_keys:
			if k not in graph: 
				if k in g1:
					graph[k] = g1[k]
				else:
					graph[k] = g2[k]
					
			if k in g2:
				for x in g2[k]:
					ind = find_index()
					if ind != -1:
						if x[0] != []:
							t1 = x[0].split('|')
						if graph[k][ind] != -1:
							t2 = graph[k][ind][0].split('|')
						z = "|".join(set(t1+t2))
						#xf = [z, graph[k][ind][1]]
						xf = (z, graph[k][ind][1])
						graph[k][ind] = xf
					else:
						graph[k].append(x)
				
		return graph


class Wednesday():
    def __init__(self, root, maps, ban):
        self.root = root
        self.maps = maps
        self.ban = ban
        Wednesday.time = {}
        Wednesday.hierarchy = {}
        Wednesday.neighbor = {}
          
        self.init_3_dict()
        
    def init_3_dict(self):
        def is_en(yx):
            if yx[0] < 0 or yx[0] > len(self.maps)-1: return
            if yx[1] < 0 or yx[1] > len(self.maps[0])-1: return
            if yx in self.ban: return
            return True
            
        for y in range(len(self.maps)):
            for x in range(len(self.maps[0])):
                Wednesday.hierarchy[(y,x)] = self.maps[y][x]
                
                n = []
                if is_en((y-1,x)):
                    n.append((y-1,x))
                if is_en((y+1,x)):
                    n.append((y+1,x))
                if is_en((y,x-1)):
                    n.append((y,x-1))
                if is_en((y,x+1)):
                    n.append((y,x+1))

                if is_en((y-1,x-1)):
                    n.append((y-1,x-1))
                if is_en((y-1,x+1)):
                    n.append((y-1,x+1))
                if is_en((y+1,x+1)):
                    n.append((y+1,x+1))
                if is_en((y+1,x-1)):
                    n.append((y+1,x-1))
                    
                Wednesday.neighbor[(y,x)] = n
                
                Wednesday.time[(y,x)] = 0
                if (y, x) in self.ban:
                    lb = Label(text=" ", background = 'black')
                    lb.configure(text=self.maps[y][x])
                else:
                    bg = "white"
                    if CHANGE_BG:
                        bg = random.choice(["yellow", "red", "white", "brown", "pink"])
                    lb = Label(text=" ", background = bg)
                    lb.configure(text=self.maps[y][x])
                lb.grid(row=y, column=x, ipadx=15, ipady=10, padx=1, pady=1)
               
                    
    def update(self):
        for y in range(len(self.maps)):
            for x in range(len(self.maps[0])):
                if Wednesday.time[(y,x)]:
                    Wednesday.time[(y,x)] -=1
                else:
                   Wednesday.hierarchy[(y,x)] = self.maps[y][x] 
                   
                if not (y, x) in self.ban:
                    bg = "white"
                    if CHANGE_BG:
                        bg = random.choice(["yellow", "red", "white", "brown", "pink"])
                    lb = Label(text=" ", background = bg)
                    lb.configure(text=Wednesday.hierarchy[(y,x)])
                    lb.grid(row=y, column=x, ipadx=15, ipady=10, padx=1, pady=1)
                       
        self.root.after(TIME, self.update)

class Base():
    def __init__(self, root, color, node):
        self.root = root
        self.color = color
        self.y = node[0]
        self.x = node[1]
        self.visit = {}
        self.neighbor = {}
        self.count = 0
     
    def show(self, y, x, color):
        lb = Label(text=" ", background = color)
        try:
            lb.configure(text=Wednesday.hierarchy[(y,x)] )
            lb.grid(row=self.y, column=self.x, ipadx=15, ipady=10, padx=1, pady=1)
        except:
            pass
        
    def add_neighbor(self):
        try:
            self.neighbor[(self.y, self.x)] = Wednesday.neighbor[(self.y, self.x)]
        except:
            pass
             
    def choice(self, yx):
        self.add_neighbor()
        return random.choice(self.neighbor[yx])
            
    def move(self):     
        yx = self.choice((self.y, self.x))
        self.redraw(yx)
        self.count +=1
  
    def redraw(self, yx):
        self.show(self.y, self.x, 'white')  
        self.y = yx[0]
        self.x = yx[1]
        self.show(yx[0], yx[1], self.color)
        
    def update(self):
        self.move()   
        self.root.after(TIME, self.update)
        
    def env(self):
        return (self.color, (self.y, self.x))


class WhoBeats(Base):
    def __init__(self, root, color, node, lenYX):
        super().__init__(root, color, node)
        self.lenYX = lenYX
        self.balls = []

    def choice(self, yx):
        self.add_neighbor()
        
        if self.count % (self.lenYX[1] +PAUSE)  == 0:
            y = yx[0] + random.choice([-1, 0, 1])
            if y < 0:
                y = 0
            if y > self.lenYX[0] -1:
                y = self.lenYX[0] -1
            dy = y-yx[0]
            ball = Ball(root, "blue", (y, yx[1]), self.lenYX, dy)
            self.balls.append(ball)
            ball.update()

        yx = list(random.choice(self.neighbor[yx]))
        yx[1] = 0
    
        return tuple(yx)
        
    def get_balls(self):
        for ball in self.balls:
            pass
        return self.balls


class Ball(Base):
    def __init__(self, root, color, node, lenYX, dy):
        super().__init__(root, color, node)
        self.lenYX = lenYX
        self.dy = dy

    def choice(self, yx):
        if BALL_CHANGE_BG:
            self.color = random.choice(["grey", "blue"])
        self.add_neighbor()
        
        if yx[0] <= 0:
            self.dy = 1
        if yx[0] >= self.lenYX[0]-1:
            self.dy = -1
         
        try:   
            if (yx[0] +self.dy, yx[1] + 1) in self.neighbor[yx]:
                yx = (yx[0] +self.dy, yx[1] + 1)

            else:
                if yx[1]+1 >= self.lenYX[1]: 
                    yx = (yx[0], self.lenYX[1])
                else: 
                    v = []
                    for i in self.neighbor[yx]: 
                        if not i in self.visit:
                            self.visit[i] = 0
                        v.append((i, self.visit[i])) 
                    
                    v.sort(key = lambda x: x[1], reverse = False)
                    v = [i for i in v if v[0][1] == i[1]]
                    v = random.choice(v)
                    self.visit[v[0]] += 1
                    yx = v[0]
                    self.dy = random.choice([-1,1])
        except:
            pass
         
            
        return yx
    
    
class WhoСatches(Base):
    def __init__(self, root, color, node, lenYX, whoBeats):
        super().__init__(root, color, node)
        self.lenYX = lenYX
        self.whoBeats = whoBeats
        self.win = 0
        
        self.routes = []
        self.route = []
        self.gf = [{}, {}]
        
        self.hdWC = Hd({})
        

    def choice(self, yx):
        self.add_neighbor()
        self.neighbor[yx] = [x for x in self.neighbor[yx] if x[1] == self.lenYX[1]-1]
        
        dy = self.action()
        if (yx[0] +dy, yx[1]) in self.neighbor[(yx[0], self.lenYX[1]-1)]:

            hd = Hd({})
            hdF = self.tuple_str((dy,0))
            hdX = self.tuple_str(yx)
            hdY = self.tuple_str((yx[0] +dy, yx[1]))
            path = hd.get_yfx([hdX, hdY], [hdF, ""])
            gn = hd.fORx(hd.flow(path), f=0)
            self.hdWC.graph = hd.merge(self.hdWC.graph, gn)
            print("\nwhoСatches ku = ", self.hdWC.get_ku(self.hdWC.graph))
            self.hdWC.show(self.hdWC.graph)

            yx = (yx[0] +dy, self.lenYX[1]-1) 
        
        self.winner()

        return yx
        
        
    def action(self, choice = None):
        if choice == None:
            dy = random.choice([-1,0,1])
        return dy
        
        
    def hypothesis(self, x_list, f = None):
        if f ==  None:          
            f_list = ["f"]*len(x_list)
            return (f_list, x_list)
        else:
            f_list = x_list[:-1] #дейсвие = предыдущее состояние
            x_list = x_list[1:]  
            return (f_list, x_list)
            
    def tuple_str(self, v, convert = True):
        if convert:
            delimiter = ','
            r = delimiter.join([str(x) for x in v])
        else:
            r = [ x for x in v.split(',')]
        return r
        
    
    def winner(self):
        balls = self.whoBeats.get_balls()

        for i, ball in enumerate(balls):  
            if ball.env()[1][1] == 1:
                self.route = []
                self.route.append(ball.env()[1])
            elif ball.env()[1][1] < self.lenYX[1]-1:
                self.route.append(ball.env()[1])
            else:
                self.route.append(ball.env()[1])
                if self.route not in self.routes:
                    self.routes.append(self.route)
            if self.x == ball.env()[1][1]:
                if self.y == ball.env()[1][0]:
                    self.win +=1
                else:
                    self.win -=1
                root.title("win = " + str(self.win))
                self.whoBeats.balls.pop(i)
        
        if  self.routes: 
            hd = Hd({}) 
            for x_list in self.routes:
                x_list = [self.tuple_str(x) for x in x_list]
                if HYPOTHESIS:
                    f_list, x_list = self.hypothesis(x_list, f=True)
                else:
                    f_list, x_list = self.hypothesis(x_list)
                path = hd.get_yfx(x_list, f_list)
                gn = hd.fORx(hd.flow(path), f=0)
                self.gf[0] = hd.merge(self.gf[0], gn)
            ku = hd.get_ku(self.gf[0])
            print("\n\nball  ku =", ku)
            hd.show(self.gf[0])
            print("\nroutes")
            t = [print(x) for x in self.routes]

        
        
#--------- main ---------#    
if __name__ == "__main__":

    TIME = 500 #частота обновления
    PAUSE = 5
    HYPOTHESIS = True
    
    CHANGE_BG = False 
    BALL_CHANGE_BG = False 
    #CHANGE_BG = True
    #BALL_CHANGE_BG = True

    
    root = Tk()
    
    #ban=[(1,3), (1,5), (1,6), (1,7), (0,7), (1,4), (2,4), (3,4), (4,3), (4,4), (5,6), (5,7), (4,7), (3,7), (2,7)] 
    #M=8; N=12
    
    ban=[(1,3)] 
    #ban = []
    M=3; N=6
    
    maps = [["" for i in range(N)] for j in range(M)]
    
    def update():
        #w1.update()
        b1.update()
        c1.update()
        
    w1 = Wednesday(root, maps, ban)
    b1 = WhoBeats(root, "red", (1,0), (M, N))
    c1 = WhoСatches(root, "green", (1,N-1), (M, N), b1)
    
    update()
    root.mainloop()
    


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


  1. sturex
    28.09.2022 09:48

    Вот тут недавно писали про иерархическое мышление: Структурное мышление...

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


    1. bvv2311 Автор
      28.09.2022 09:58

      Вот тут недавно писали про иерархическое мышление: Структурное мышление...

      Читал

       А как учитывается время в иерархии сущностей? Или, иначе, как строятся причинно-следственные отношения между сущностями?

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


      1. bvv2311 Автор
        28.09.2022 10:02

        Эшби // глава Соединение систем []


        1. sturex
          28.09.2022 10:24

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


          1. bvv2311 Автор
            28.09.2022 10:30

            чтобы синтез автомата был поточным и осуществлялся за один проход по данным.

            наверно есть., я не встречал.


  1. iskateli
    28.09.2022 20:39

    Ограниченный подход, есть вещи которые нельзя представить в виде иерархии, всякие циклические зависимости и т.п., почему не гиперграф использовали?


    1. bvv2311 Автор
      29.09.2022 00:13

      Ограниченный подход, есть вещи которые нельзя представить в виде иерархии, всякие циклические зависимости и т.п

      И какой же у вас подход, позволяющий уменьшить размерность пространства состояний?

      почему не гиперграф использовали?

      По сути и так множества не одной пары, а нескольких.


      1. iskateli
        29.09.2022 06:55

        По сути и так множества не одной пары, а нескольких.

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

        И какой же у вас подход, позволяющий уменьшить размерность пространства состояний?

        а я и не предлагаю, я просто указываю что это ограниченный подход. А методов уменьшающих пространство состояний много всяких, в т.ч. и нейросетевых, AlphaGo например умудряется эффективно искать среди 10^171 вариантов.


        1. bvv2311 Автор
          29.09.2022 07:01

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

          Причем здесь это?

          У меня графы


        1. bvv2311 Автор
          29.09.2022 07:07

          а я и не предлагаю, я просто указываю что это ограниченный подход. А методов уменьшающих пространство состояний много всяких, в т.ч. и нейросетевых, AlphaGo например умудряется эффективно искать в среди 10^171 вариантов.

          Я в курсе. Сам КМС по шахматам. ....

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


          1. iskateli
            29.09.2022 11:36

            " Ну, есть такие, которые не сводятся к иерархии. "
            Не "ну", а это принципиальная возможность и её надо обязательно обеспечить, иначе это ограниченный подход, см. https://habr.com/ru/post/690518/comments/#comment_24775958


            1. bvv2311 Автор
              29.09.2022 12:01

              ответ ниже вашей ссылки


    1. bvv2311 Автор
      29.09.2022 00:58

      Или вот, что более конкретно ... Пусть имеется последовательность с шумом: .. 1 2 .. 2 3 2 .. 1 3 .. Ваша версия гиперграфа?


      1. iskateli
        29.09.2022 07:05

        Лучше спросите себя, как вы помощью дерева собираетесь выразить например число Пи? Вы же не будете перечислять до бесконечности все цифры.


        1. bvv2311 Автор
          29.09.2022 07:15

          Лучше спросите себя, как вы помощью дерева собираетесь выразить например число Пи? Вы же не будете перечислять до бесконечности все цифры.

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

          Или вот, что более конкретно ... Пусть имеется последовательность с шумом: .. 1 2 .. 2 3 2 .. 1 3 .. Ваша версия гиперграфа?

          так как?


          1. iskateli
            29.09.2022 11:21

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


            1. bvv2311 Автор
              29.09.2022 11:34

              Разве я утверждал обратное?

              Вот здесь (.. 1 2 .. 2 3 2 .. 1 3 .. ) тоже нет иерархии. Но каждое состояние этого преобразования может быть представлено своим пространством состояний. А само это (.. 1 2 .. 2 3 2 .. 1 3 .. ) быть состоянием чего-либо еще. В этом суть иерархии


              1. bvv2311 Автор
                29.09.2022 12:12

                Мораль... Если с отдельным пространством состояний можно разобраться и отслеживать его изменения, то иметь дело со всеми сразу - крайне не выгодно ввиду растущей сложности.


      1. bvv2311 Автор
        29.09.2022 07:08

        iskateli ...и?