Довольно часто
простые модели НС прямого распространения можно представить в виде
последовательных слоев соединенных друг с другом:
Поэтому в PyTorch для их описания
существует специальный класс:
torch.nn.Sequential
Например, нашу
модель классификации цифр с его помощью можно определить следующим образом:
model = nn.Sequential(
nn.Linear(28 * 28, 32),
nn.ReLU(),
nn.Linear(32, 10)
)
Создается объект
класса и в качестве аргументов указываются слои, через которые будет проходить
сигнал. Обратите внимание, что функция активации ReLU здесь
определена в виде объекта класса ReLU, так как при определении
последовательной модели мы должны передавать объекты классов, унаследованных от
nn.Module. К ним, в частности,
относятся Linear и ReLU.
Все, модель
создана и готова к использованию. Метод forward прописывать не
нужно, логика его работы формируется автоматически на основе указанных слоев
модели.
Однако сейчас в
объекте model не очень удобно
получать доступ к отдельным слоям. Поэтому часто можно увидеть такой способ
определения последовательной модели:
model = nn.Sequential()
model.add_module('layer_1', nn.Linear(28 * 28, 32))
model.add_module('relu', nn.ReLU())
model.add_module('layer_2', nn.Linear(32, 10))
Сначала
создается объект модели Sequential, а затем, с помощью метода add_module по порядку
добавляются необходимые слои с указанием их имен. В результате в объекте model формируются
локальные атрибуты с этими именами и через них можно получать доступ к тому или
иному слою сети. Например:
model.layer_1 # возвращается объект класса Linear 1-го слоя
Так как сам по
себе класс nn.Sequential унаследован от
класса nn.Module, то его можно
воспринимать, как отдельный блок и использовать наряду с другими стандартными
модулями. Например, описать внутри класса модели DigitNN:
class DigitNN(nn.Module):
def __init__(self, input_dim, num_hidden, output_dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, num_hidden),
nn.ReLU(),
nn.Linear(num_hidden, output_dim),
)
def forward(self, x):
return self.net(x)
Или же отдельно
создать блок из последовательных слоев:
block = nn.Sequential(
nn.Linear(32, 32),
nn.LeakyReLU(),
nn.Linear(32, 16),
nn.LeakyReLU(),
)
А затем,
использовать его как часть более сложной сети:
model = nn.Sequential()
model.add_module('layer_1', nn.Linear(28 * 28, 32))
model.add_module('relu', nn.ReLU())
model.add_module('block', block)
model.add_module('layer_2', nn.Linear(16, 10))
То есть, здесь
допустимы самые разные комбинации использования объектов класса Sequential: и как отдельной
модели, и как блока внутри других моделей.
Класс nn.ModuleList
Следующий класс,
который мы рассмотрим, будет класс nn.ModuleList. Но вначале
посмотрим на пример, показывающий его необходимость.
Допустим, мы бы
хотели генерировать модели из различного количества последовательных
полносвязных слоев. И для этого объявляем следующий класс:
class ModelNN(nn.Module):
def __init__(self, input_dim, output_dim, n_layers=3):
super().__init__()
self.layers = [nn.Linear(input_dim // n, input_dim // (n+1)) for n in range(1, n_layers+1)]
sz_input = self.layers[-1].out_features
self.layer_out = nn.Linear(sz_input, output_dim)
def forward(self, x):
for layer in self.layers:
x = layer(x)
x = nn.functional.tanh(x)
x = self.layer_out(x)
return x
На первый взгляд
здесь все может показаться правильным и логичным. В инициализаторе создается
список из линейных слоев с числом входов и выходов, вычисленных по определенной
формуле. Затем, отдельно создается последний слой с нужным числом выходов НС.
После этого в методе forward тензор x последовательно пропускается
по всем созданным слоям с функцией активации гиперболический тангенс и
возвращается полученное значение. Однако если создать объект этой модели,
например, командой:
md = ModelNN(28 * 28, 10)
и вывести ее в
консоль:
то увидим только
один слой:
ModelNN(
(layer_out):
Linear(in_features=196, out_features=10, bias=True)
)
И команда:
вернет только
веса этого последнего слоя. Спрашивается, куда пропали остальные созданные слои?
Конечно, они никуда не исчезли и хранятся в списке self.layers, но помещая их
сразу в список, они перестают существовать для модели этого класса. Но если на
созданный слой сразу ведет какая-либо переменная объекта класса, то такой слой
автоматически регистрируется в модели и мы его видим при ее отображении
в консоли.
Получается,
чтобы список из слоев был виден на уровне модели, все эти слои нужно зарегистрировать
в ней. Как раз для этого был разработан специальный класс ModuleList. Достаточно
обернуть этот список в класс ModuleList и все слои автоматически
регистрируются в модели:
class ModelNN(nn.Module):
def __init__(self, input_dim, output_dim, n_layers=3):
super().__init__()
self.layers = nn.ModuleList(
[nn.Linear(input_dim // n, input_dim // (n+1)) for n in range(1, n_layers+1)]
)
sz_input = self.layers[-1].out_features
self.layer_out = nn.Linear(sz_input, output_dim)
def forward(self, x):
...
При отображении
модели в консоли увидим:
ModelNN(
(layers):
ModuleList(
(0):
Linear(in_features=784, out_features=392, bias=True)
(1):
Linear(in_features=392, out_features=261, bias=True)
(2):
Linear(in_features=261, out_features=196, bias=True)
)
(layer_out):
Linear(in_features=196, out_features=10, bias=True)
)
И количество
параметров модели теперь определяется списком из 8 элементов:
x = list(md.parameters())
len(x) # 8
так как у
каждого слоя отдельно веса для входного тензора и bias.
Кстати,
аналогичного результата можно добиться, используя класс nn.Sequential, следующим
образом:
class ModelNN(nn.Module):
def __init__(self, input_dim, output_dim, n_layers=3):
super().__init__()
layers = [nn.Linear(input_dim // n, input_dim // (n+1)) for n in range(1, n_layers+1)]
self.net = nn.Sequential(*layers)
sz_input = layers[-1].out_features
self.layer_out = nn.Linear(sz_input, output_dim)
def forward(self, x):
for layer in self.net:
x = layer(x)
x = nn.functional.tanh(x)
x = self.layer_out(x)
return x
Получаем набор
зарегистрированных слоев. Но, как мы уже знаем, класс Sequential служит для
описания последовательно соединенных слоев. Именно так его и следует
использовать. Если же слои срабатывают не по порядку и сеть имеет более сложную
организацию, то лучше использовать класс ModuleList. Кроме того,
объект класса Sequential образует
самостоятельную модель, которую можно обучать и применять. Тогда как класс ModuleList лишь
регистрирует набор модулей и не создает своей независимой модели.
Но вернемся к
классу ModuleList. Сейчас слои
недоступны из модели через отдельные переменные. Если это необходимо, то можно
вначале создать объект класса ModuleList, а затем, с помощью метода add_module последовательно
добавить слои, указав их имена:
class ModelNN(nn.Module):
def __init__(self, input_dim, output_dim, n_layers=3):
super().__init__()
self.layers = nn.ModuleList()
for n in range(1, n_layers+1):
self.layers.add_module(f'layer_{n}', nn.Linear(input_dim // n, input_dim // (n+1)))
self.layer_out = nn.Linear(input_dim // (n_layers+1), output_dim)
def forward(self, x):
...
Теперь мы можем
обратиться к нужному слою объекта layers по его имени,
например, так:
ls1 = md.layers.layer_1 # 1-й слой объекта layers
Класс nn.ModuleDict
В заключение
этого занятия рассмотрим еще один аналогичный класс ModuleDict, который также
регистрирует отдельные слои, но хранит их в виде словаря, а не списка.
Например, им можно воспользоваться следующим образом:
class ModelNN(nn.Module):
def __init__(self, input_dim, output_dim, n_layers=3, act_type=None):
super().__init__()
self.act_type = act_type
self.layers = nn.ModuleList()
for n in range(1, n_layers+1):
self.layers.add_module(f'layer_{n}', nn.Linear(input_dim // n, input_dim // (n+1)))
self.layer_out = nn.Linear(input_dim // (n_layers+1), output_dim)
self.act_lst = nn.ModuleDict({
'relu': nn.ReLU(),
'lk_relu': nn.LeakyReLU(),
})
def forward(self, x):
for layer in self.layers:
x = layer(x)
if self.act_type and self.act_type in self.act_lst:
x = self.act_lst[self.act_type](x)
x = self.layer_out(x)
return x
Мы дополнительно
определили параметр act_type – тип функции
активации. А сами функции храним в виде объекта класса ModuleDict с
соответствующими ключами. Затем, в методе forward выбираем
функцию активацию по ключу, если она присутствует в словаре, и пропускаем через
нее тензор x.
Вот так разнообразно
можно описывать модели во фреймворке PyTorch, используя классы
nn.Module, nn. Sequential,
nn.ModuleList и nn.ModuleDict.