как отследить создание объекта класса python?

В библиотеке глубокого обучения pytorch есть вот такая конструкция, которая позволяет собирать нейронную сеть из разных слоев.

import torch.nn as nn
import torch.nn.functional as F

class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 20, 5)
        self.conv2 = nn.Conv2d(20, 20, 5)

    def forward(self, x):
        x = self.conv1(x)
        x = F.relu(x) # activation function
        x = self.conv2(x)
        return F.relu(x)

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

self.conv1 = nn.Conv2d(1, 20, 5)
self.conv2 = nn.Conv2d(20, 20, 5)

Мы создаем два объекта класса Conv2d, присваиваем ссылки на них объекту класса Model. В процессе обучения(обратного распространения ошибки), градиент последовательно с конца проходит по всем слоям такой сети (сначала по self.conv2, потом по self.conv1 ). Отсюда следует вопрос, каким образом алгоритм узнает о всех слоях объекта класса Model? Это обсуждение частично отвечает на мой вопрос. Я понял, что есть еще один класс, который собирает в некоторый список ссылки на объекты классов-"слоев" при их создании. У себя я реализовал это так

class Parameter:
    layers = []
    calling = dict()
    number_of_classes = 0

    def __init__(self, info):
        Parameter.layers.append(info[0])
        Parameter.calling[info[0]] = info[1:]


class Conv2d:
    def __init__(self, input_channels: int, output_channels: int, kernel_size: tuple, bias = True):
        # something happen here
        Parameter([self, self.kernel_array, self.bias_array])

Такой способ работает. Однако, пока объект класса Model ровно один. Как только создается еще один объект (ещё одна нейронка), то в Parameter.layers уже хранятся ссылки для разных объектов класса Model и соответственно всё перестает работать.

Есть ли у вас идеи, как это можно избежать?


Ответы (2 шт):

Автор решения: kristal

Сразу скажу что в нейронках сильно не разбираюсь, как реализовано это внутри pytorch не знаю и на задачу смотрю с точи зрения чистого python.

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

def ParameterObj():
    layers_list = []

    class Parameter:
        layers = layers_list
        calling = dict()
        number_of_classes = 0

        def __init__(self, info):
            Parameter.layers.append(info[0])
            Parameter.calling[info[0]] = info[1:]

    return Parameter


def Conv2dObj():
    constructor_Parameter = ParameterObj()

    class Conv2d:
        def __init__(self,
                     input_channels: int,
                     output_channels: int,
                     kernel_size: tuple,
                     bias=True
                     ):
            self.layers = constructor_Parameter([self, input_channels, output_channels])

    return Conv2d


class Model:
    def __init__(self, *args):
        self._constructor_Conv2d = Conv2dObj()
        self.conv1 = self._constructor_Conv2d(*args)
        self.conv2 = self._constructor_Conv2d(*[i + 1 for i in args])


m1 = Model(1, 2, 3)
print(m1.conv1.layers.layers)
# [<__main__.Conv2dObj.<locals>.Conv2d object at 0x00CB41B0>, <__main__.Conv2dObj.<locals>.Conv2d object at 0x00CB4210>]
m2 = Model(1, 2, 3)
print(m2.conv1.layers.layers)
# [<__main__.Conv2dObj.<locals>.Conv2d object at 0x00CB4330>, <__main__.Conv2dObj.<locals>.Conv2d object at 0x00CB4370>]

Как видим, расположение в памяти у объектов в разных массивах разное. В итоге, что тут происходит? При создании экземпляра класса Model, для него создаётся поле _constructor_Conv2d, в котором (логично) хранится конструктор класса Conv2d, однако с одной небольшой особенностью. При создании экземпляра с помощью этого конструктора в Parameter.layers будут попадать ссылки только на те объекты, которые создавались с помощью этого самого конструктора. В случае, если создать новый конструктор, согласно вашему вопросу, все ссылки будут попадать в другой список. Это работает благодаря тому, что мы таким же образом создаём конструктор для класса Parameter, а в классе Parameter мы замыкаем layers_list, поэтому для каждого нового ParameterObj будет создан новый пустой массив (если всё-таки непонятно, почитайте про замыкания).

Для второй идеи код будет излишен. Вот в чём суть: можно в Model создать экземпляр класса Parameter и просто передавать его при создании экземпляров Conv2d, где уже по ссылке Parameter.layers будут добавляться нужные вам данные. Ну и не забудьте инициализировать layers в init, иначе в текущем виде данный список будет общим для всех экземпляров класса.

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

P.S. Вообще можно и Model обернуть в функцию что бы замкнуть constructor_Conv2d и не создавать лишнее поле в классе.

→ Ссылка
Автор решения: Тима

Спасибо большое @kristal за ответ. Именно с помощью замыканий я смог у себя всё реализовать.

В отдельном модуле сделал всё ту же вспомогательную сущность Parameter, но уже как замыкание

def ParameterObj():
    class Parameter:
        layers = []
        calling = dict()
        def __init__(self, info):
            Parameter.layers.append(info[0])
            Parameter.calling[info[0]] = info[1:]
    return Parameter

В основном модуле решил использовать глобальную переменную Parameter, которая будет изменяться при инициализации объекта дочернего класса (дочернего к Module), а также при вызове __call__.

Так как обучение (back propagation) происходит только после прямого прохождения по сети (forward propagation, вызова __call__), то ожидается что глобальная переменная Parameter будет ссылаться на именно тот класс Parameter, который был создан при создании нового объекта и соответственно при вызове self._constructor_Parameter = ParameterObj()

Parameter = None

class Module:
    def __init__(self):
        self._constructor_Parameter = ParameterObj()
        global Parameter
        Parameter = self._constructor_Parameter

class Conv:
    def __init__(self, input_channels: int, output_channels: int, kernel_size: tuple, bias = True):
        # something happen here
        Parameter([self, self.filter_array, np.zeros(self.n_filters)])

И теперь создание нейронной сети выглядит так

import nn

class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Сonv(3, 5)
        self.act1 = nn.Relu()
        self.conv2 = nn.Conv(5, 5)
        self.sftmx = nn.Softmax()

    def forward(self, x):
        x = self.conv1(x)
        x = self.act1(x)
        x = self.conv2(x)
        x = self.sftmx(x)
        return x

model = SimpleNet()
prediction = model(x)
→ Ссылка