как отследить создание объекта класса 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 шт):
Сразу скажу что в нейронках сильно не разбираюсь, как реализовано это внутри 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)