Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5.2 Управление параметрами

Выбрав архитектуру и задав гиперпараметры, мы переходим к этапу обучения. На этом этапе цель состоит в том, чтобы найти значения параметров модели, которые минимизируют функцию потерь. После обучения эти параметры понадобятся нам для будущих предсказаний. Кроме того, иногда мы хотим извлечь параметры, чтобы повторно использовать их в других условиях, сохранить модель, чтобы её можно было выполнить в другом программном обеспечении, или изучить параметры для научного понимания.

В предыдущих разделах мы полагались только на фреймворк глубокого обучения при обучении модели и пропускали детали работы с параметрами. В этом разделе мы рассмотрим следующее:

  • доступ к параметрам для отладки, диагностики и визуализации;
  • инициализация параметров;
  • совместное использование параметров между различными компонентами модели.

Сначала рассмотрим многослойный перцептрон с одним скрытым слоем.

MXNet:

from mxnet import init, np, npx
from mxnet.gluon import nn
npx.set_np()

net = nn.Sequential()
net.add(nn.Dense(8, activation='relu'))
net.add(nn.Dense(1))
net.initialize()  # Используем метод инициализации по умолчанию

X = np.random.uniform(size=(2, 4))
net(X)  # Прямое распространение

PyTorch:

import torch
from torch import nn

net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)

TensorFlow:

import tensorflow as tf

net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(4, activation=tf.nn.relu),
    tf.keras.layers.Dense(1),
])

X = tf.random.uniform((2, 4))
net(X)

PaddlePaddle:

import warnings
warnings.filterwarnings(action='ignore')
import paddle
from paddle import nn

net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = paddle.rand([2, 4])
net(X)

5.2.1 Доступ к параметрам

Параметры можно получить из уже существующей модели. Если модель задана с помощью Sequential, к любому её слою можно обратиться по индексу. Модель ведёт себя как список, а параметры каждого слоя находятся в его атрибутах. Как показано ниже, мы можем проверить параметры второго полносвязного слоя.

MXNet:

print(net[1].params)

PyTorch, PaddlePaddle:

print(net[2].state_dict())

TensorFlow:

print(net.layers[2].weights)

Вывод даёт нам несколько важных сведений. Во-первых, этот полносвязный слой содержит два параметра: весовую матрицу и вектор смещений. Оба хранятся как числа с плавающей точкой одинарной точности (float32). Обратите внимание, что имена параметров позволяют однозначно идентифицировать каждый параметр, даже в сети из сотен слоёв.

5.2.1.1 Целевые параметры

Каждый параметр представлен экземпляром класса параметров. Чтобы выполнить с параметром какую-либо операцию, сначала нужно получить доступ к его числовому значению. Существует несколько способов это сделать: одни проще, другие универсальнее. Код ниже извлекает смещение из второго полносвязного слоя (то есть из третьего слоя сети). Извлечённый объект является экземпляром класса параметров, и далее мы обращаемся к значению этого параметра.

MXNet:

print(type(net[1].bias))
print(net[1].bias)
print(net[1].bias.data())

PyTorch:

print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)

TensorFlow:

print(type(net.layers[2].weights[1]))
print(net.layers[2].weights[1])
print(tf.convert_to_tensor(net.layers[2].weights[1]))

PaddlePaddle:

print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.value)

MXNet, PyTorch, PaddlePaddle: Параметры — составные объекты, которые содержат значения, градиенты и дополнительную информацию. Поэтому нам и нужно явно обращаться к значениям параметров. Помимо значения, мы также можем получить градиент каждого параметра. В приведённой сети мы ещё не вызывали обратное распространение, поэтому градиенты параметров находятся в начальном состоянии.

MXNet:

net[1].weight.grad()

PyTorch, PaddlePaddle:

net[2].weight.grad == None

5.2.1.2 Одновременный доступ ко всем параметрам

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

MXNet:

print(net[0].collect_params())
print(net.collect_params())

PyTorch, PaddlePaddle:

print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])

TensorFlow:

print(net.layers[1].weights)
print(net.get_weights())

Это даёт нам другой способ доступа к параметрам сети, как показано ниже.

MXNet:

net.collect_params()['dense1_bias'].data()

PyTorch:

net.state_dict()['2.bias'].data

TensorFlow:

net.get_weights()[1]

PaddlePaddle:

net.state_dict()['2.bias']

5.2.1.3 Сбор параметров из вложенных блоков

Посмотрим, как работает соглашение об именовании параметров, если вкладывать блоки друг в друга. Сначала определим функцию, которая создаёт блоки (её можно назвать «фабрикой блоков»), а затем объединим эти блоки в более крупный блок.

MXNet:

def block1():
    net = nn.Sequential()
    net.add(nn.Dense(32, activation='relu'))
    net.add(nn.Dense(16, activation='relu'))
    return net

def block2():
    net = nn.Sequential()
    for _ in range(4):
        # Вложение блоков
        net.add(block1())
    return net

rgnet = nn.Sequential()
rgnet.add(block2())
rgnet.add(nn.Dense(10))
rgnet.initialize()
rgnet(X)

PyTorch:

def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                         nn.Linear(8, 4), nn.ReLU())

def block2():
    net = nn.Sequential()
    for i in range(4):
        # Вложение блоков
        net.add_module(f'block {i}', block1())
    return net

rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)

TensorFlow:

def block1(name):
    return tf.keras.Sequential([
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(4, activation=tf.nn.relu)],
        name=name)

def block2():
    net = tf.keras.Sequential()
    for i in range(4):
        # Вложение блоков
        net.add(block1(name=f'block-{i}'))
    return net

rgnet = tf.keras.Sequential()
rgnet.add(block2())
rgnet.add(tf.keras.layers.Dense(1))
rgnet(X)

PaddlePaddle:

def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), 
                         nn.Linear(8, 4), nn.ReLU())

def block2():
    net = nn.Sequential()
    for i in range(4):
        # Вложение блоков
        net.add_sublayer(f'block {i}', block1())
    return net

rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)

Спроектировав сеть, посмотрим, как она работает.

MXNet:

print(rgnet.collect_params)
print(rgnet.collect_params())

PyTorch, PaddlePaddle:

print(rgnet)

TensorFlow:

print(rgnet.summary())

Поскольку слои вложены иерархически, к ним можно обращаться так же, как по индексам вложенного списка. Ниже мы получаем смещение первого слоя во втором подблоке первого основного блока.

MXNet:

rgnet[0][1][0].bias.data()

PyTorch:

rgnet[0][1][0].bias.data

TensorFlow:

rgnet.layers[0].layers[1].layers[1].weights[1]

PaddlePaddle:

print(rgnet[0].state_dict()['block 0.0.bias'])

5.2.2 Инициализация параметров

Теперь, когда мы знаем, как получить доступ к параметрам, рассмотрим, как правильно их инициализировать. Мы обсуждали необходимость хорошей инициализации в разделе 4.8. Фреймворки глубокого обучения предоставляют инициализацию по умолчанию, а также позволяют создавать собственные методы инициализации, которые удовлетворяют нашим правилам.

MXNet: По умолчанию MXNet инициализирует весовые параметры, случайно выбирая их из равномерного распределения , а параметры смещения задаёт равными 0. Модуль init MXNet предоставляет несколько готовых методов инициализации.

PyTorch: По умолчанию PyTorch инициализирует весовые матрицы и векторы смещений равномерно в диапазоне, который вычисляется по размерностям входа и выхода. Модуль nn.init PyTorch предоставляет несколько готовых методов инициализации.

TensorFlow: По умолчанию Keras инициализирует весовые матрицы равномерно в диапазоне, который вычисляется по размерностям входа и выхода. Параметры смещения задаются равными 0. TensorFlow предоставляет различные методы инициализации в корневом модуле и в модуле keras.initializers.

PaddlePaddle: По умолчанию PaddlePaddle инициализирует весовые матрицы методом Xavier, а параметры смещения задаёт равными 0. Модуль nn.initializer PaddlePaddle предоставляет несколько готовых методов инициализации.

5.2.2.1 Встроенная инициализация

Начнём с вызова встроенного инициализатора. Приведённый ниже код инициализирует все весовые параметры гауссовскими случайными величинами со стандартным отклонением 0.01, а параметры смещения задаёт равными нулю.

MXNet:

# Параметр force_reinit гарантирует, что параметры будут заново инициализированы,
# независимо от того, инициализировались ли они ранее
net.initialize(init=init.Normal(sigma=0.01), force_reinit=True)
net[0].weight.data()[0]

PyTorch:

def init_normal(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, mean=0, std=0.01)
        nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]

TensorFlow:

net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4, activation=tf.nn.relu,
        kernel_initializer=tf.random_normal_initializer(mean=0, stddev=0.01),
        bias_initializer=tf.zeros_initializer()),
    tf.keras.layers.Dense(1)])

net(X)
net.weights[0], net.weights[1]

PaddlePaddle:

def init_normal(m):
    if type(m) == nn.Linear:
        paddle.nn.initializer.Normal(mean=0.0, std=0.01)
        paddle.zeros(m.bias)    
net.apply(init_normal)
net[0].weight[0],net[0].state_dict()['bias']

Мы также можем инициализировать все параметры заданной константой, например 1.

MXNet:

net.initialize(init=init.Constant(1), force_reinit=True)
net[0].weight.data()[0]

PyTorch:

def init_constant(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 1)
        nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]

TensorFlow:

net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4, activation=tf.nn.relu,
        kernel_initializer=tf.keras.initializers.Constant(1),
        bias_initializer=tf.zeros_initializer()),
    tf.keras.layers.Dense(1),
])

net(X)
net.weights[0], net.weights[1]

PaddlePaddle:

def init_constant(m):
    if type(m) == nn.Linear:
        paddle.nn.initializer.Constant(value = 1)
        paddle.zeros(m.bias)
net.apply(init_constant)
net[0].weight[0],net[0].state_dict()['bias']

Также мы можем применять разные методы инициализации к разным блокам. Например, ниже мы инициализируем первый слой нейронной сети методом Xavier, а затем третий слой инициализируем константой 42.

MXNet:

net[0].weight.initialize(init=init.Xavier(), force_reinit=True)
net[1].initialize(init=init.Constant(42), force_reinit=True)
print(net[0].weight.data()[0])
print(net[1].weight.data())

PyTorch:

def init_xavier(m):
    if type(m) == nn.Linear:
        nn.init.xavier_uniform_(m.weight)
def init_42(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 42)

net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)

TensorFlow:

net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4,
        activation=tf.nn.relu,
        kernel_initializer=tf.keras.initializers.GlorotUniform()),
    tf.keras.layers.Dense(
        1, kernel_initializer=tf.keras.initializers.Constant(1)),
])

net(X)
print(net.layers[1].weights[0])
print(net.layers[2].weights[0])

PaddlePaddle:

def xavier(m):
    if type(m) == nn.Linear:
        paddle.nn.initializer.XavierUniform(m.weight)
def init_42(m):
    if type(m) == nn.Linear:
        paddle.nn.initializer.Constant(42)
        
net[0].apply(xavier)
net[2].apply(init_42)
print(net[0].weight[0])
print(net[2].weight)

5.2.2.2 Пользовательская инициализация

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

MXNet: Здесь мы определяем подкласс класса Initializer. Обычно достаточно реализовать функцию _init_weight, которая принимает тензор-аргумент (data) и присваивает ему нужное значение инициализации.

PyTorch: Аналогично мы реализуем функцию my_init и применяем её к net.

TensorFlow: Здесь мы определяем подкласс Initializer и реализуем функцию __call__. Эта функция возвращает требуемый тензор заданной формы и типа данных.

PaddlePaddle: Аналогично мы реализуем функцию my_init и применяем её к net.

MXNet:

class MyInit(init.Initializer):
    def _init_weight(self, name, data):
        print('Инициализация', name, data.shape)
        data[:] = np.random.uniform(-10, 10, data.shape)
        data *= np.abs(data) >= 5

net.initialize(MyInit(), force_reinit=True)
net[0].weight.data()[:2]

PyTorch:

def my_init(m):
    if type(m) == nn.Linear:
        print("Инициализация", *[(name, param.shape)
                        for name, param in m.named_parameters()][0])
        nn.init.uniform_(m.weight, -10, 10)
        m.weight.data *= m.weight.data.abs() >= 5

net.apply(my_init)
net[0].weight[:2]

TensorFlow:

class MyInit(tf.keras.initializers.Initializer):
    def __call__(self, shape, dtype=None):
        data=tf.random.uniform(shape, -10, 10, dtype=dtype)
        factor=(tf.abs(data) >= 5)
        factor=tf.cast(factor, tf.float32)
        return data * factor

net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4,
        activation=tf.nn.relu,
        kernel_initializer=MyInit()),
    tf.keras.layers.Dense(1),
])

net(X)
print(net.layers[1].weights[0])

PaddlePaddle:

def my_init(m):
    if type(m) == nn.Linear:
        print("Инициализация", *[(name, param.shape) 
                        for name, param in m.named_parameters()][0])
        paddle.nn.initializer.XavierUniform(m.weight, -10, 10)
        h = paddle.abs(m.weight) >= 5
        h = paddle.to_tensor(h)
        m = paddle.to_tensor(m.weight)
        m *= h       

net.apply(my_init)
net[0].weight[:2]

Обратите внимание, что мы всегда можем задавать параметры напрямую.

MXNet:

net[0].weight.data()[:] += 1
net[0].weight.data()[0, 0] = 42
net[0].weight.data()[0]

PyTorch:

net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]

TensorFlow:

net.layers[1].weights[0][:].assign(net.layers[1].weights[0] + 1)
net.layers[1].weights[0][0, 0].assign(42)
net.layers[1].weights[0]

PaddlePaddle:

net[0].weight.set_value(net[0].weight.numpy() + 1)
val = net[0].weight.numpy()
val[0, 0] = 42
net[0].weight.set_value(val)
net[0].weight[0]

MXNet: Для продвинутых пользователей: если нужно изменять параметры внутри области autograd, следует использовать set_data, чтобы не вводить в заблуждение механизм автоматического дифференцирования.

5.2.3 Связывание параметров

Иногда нам хочется совместно использовать параметры между несколькими слоями: мы можем определить один плотный слой, а затем использовать его параметры для задания параметров другого слоя.

MXNet:

net = nn.Sequential()
# Нужно дать общему слою имя, чтобы можно было обращаться к его параметрам
shared = nn.Dense(8, activation='relu')
net.add(nn.Dense(8, activation='relu'),
        shared,
        nn.Dense(8, activation='relu', params=shared.params),
        nn.Dense(10))
net.initialize()

X = np.random.uniform(size=(2, 20))
net(X)

# Проверяем, совпадают ли параметры
print(net[1].weight.data()[0] == net[2].weight.data()[0])
net[1].weight.data()[0, 0] = 100
# Убедимся, что это действительно один и тот же объект, а не просто совпадающие значения
print(net[1].weight.data()[0] == net[2].weight.data()[0])

PyTorch:

# Нужно дать общему слою имя, чтобы можно было обращаться к его параметрам
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))
net(X)
# Проверяем, совпадают ли параметры
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# Убедимся, что это действительно один и тот же объект, а не просто совпадающие значения
print(net[2].weight.data[0] == net[4].weight.data[0])

TensorFlow:

# tf.keras ведёт себя немного иначе: он автоматически удаляет повторяющиеся слои
shared = tf.keras.layers.Dense(4, activation=tf.nn.relu)
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    shared,
    shared,
    tf.keras.layers.Dense(1),
])

net(X)
# Проверяем, различаются ли параметры
print(len(net.layers) == 3)

PaddlePaddle:

# Нужно дать общему слою имя, чтобы можно было обращаться к его параметрам.
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))
net(X)
# Проверяем, совпадают ли параметры
print(net[2].weight[0] == net[4].weight[0])

MXNet: Этот пример показывает, что параметры второго и третьего слоёв связаны. Они не только имеют одинаковые значения, но и представлены одним и тем же тензором. Поэтому, если мы изменим один из параметров, изменится и другой. Возникает вопрос: что происходит с градиентами, когда параметры связаны? Ответ: поскольку модельные параметры содержат градиенты, при обратном распространении градиенты второго и третьего скрытых слоёв складываются.

PyTorch: Этот пример показывает, что параметры третьего и пятого слоёв нейронной сети связаны. Они не только имеют одинаковые значения, но и представлены одним и тем же тензором. Поэтому, если мы изменим один из параметров, изменится и другой. Возникает вопрос: что происходит с градиентами, когда параметры связаны? Ответ: поскольку модельные параметры содержат градиенты, при обратном распространении градиенты второго скрытого слоя (то есть третьего слоя сети) и третьего скрытого слоя (то есть пятого слоя сети) складываются.

Резюме

  • Существует несколько способов доступа к параметрам модели, их инициализации и связывания.
  • Мы можем использовать пользовательские методы инициализации.

Упражнения

  1. Используя модель FancyMLP, определённую в разделе 5.1, получите доступ к параметрам её слоёв.
  2. Изучите документацию модуля инициализации, чтобы узнать о различных методах инициализации.
  3. Постройте многослойный перцептрон с общими параметрами слоёв и обучите его. Во время обучения наблюдайте за параметрами и градиентами слоёв модели.
  4. Почему совместное использование параметров — хорошая идея?

MXNet: Обсуждение

PyTorch: Обсуждение

TensorFlow: Обсуждение

PaddlePaddle: Обсуждение