Как динамически добавлять новые параметры в оптимизаторы в Pytorch? - PullRequest
7 голосов
/ 11 апреля 2019

Я просматривал это сообщение на форуме pytorch, и я тоже хотел это сделать.Оригинальный пост удаляет и добавляет слои, но я думаю, что моя ситуация не так уж отличается.Я также хочу добавить слои или несколько фильтров или вложения слов.Моя главная мотивация заключается в том, что агент ИИ не знает весь словарь / словарь заранее, потому что он большой.Я настоятельно предпочитаю (на данный момент) не делать посимвольные RNN.

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

Итак, я хочу убедиться, что:

  1. вложения добавляются правильно (прив нужный момент, когда создается новый граф вычислений), чтобы оптимизатор мог их обновлять
  2. нет проблем с сохраненной информацией о прошлых параметрах, например, если он использует какой-то импульс

Как это сделать?Любой пример кода, который работает?

Ответы [ 2 ]

2 голосов
/ 19 апреля 2019

Это сложный вопрос, так как я бы сказал, что ответ «зависит», в частности от того, как вы хотите работать с оптимизатором.

Давайте начнем с вашей конкретной проблемы - встраивания. В частности, вы спрашиваете, как добавить вложения, чтобы динамически увеличивать словарный запас. Мой первый совет: если вы хорошо понимаете верхнюю границу размера вашего словаря, сделайте вложение достаточно большим, чтобы справиться с ним с самого начала, так как это более эффективно, и, так или иначе, вам все равно понадобится память. Но это не то, что вы спросили. Итак, для динамического изменения встраивания вам нужно заменить старый на новый и сообщить оптимизатору об этом изменении. Вы можете просто делать это всякий раз, когда вы сталкиваетесь с исключением из-за старого встраивания, в блоке try ... except Это должно примерно следовать этой идее:

# from within whichever module owns the embedding
# remember the already trained weights
old_embedding_weights = self.embedding.weight.data
# create a new embedding of the new size
self.embedding = nn.Embedding(new_vocab_size, embedding_dim)
# initialize the values for the new embedding. this does random, but you might want to use something like GloVe
new_weights = torch.randn(new_vocab_size, embedding_dim)
# as your old values may have been updated, you want to retrieve these updates values
new_weights[:old_vocab_size] = old_embedding_weights
self.embedding.weights.data.copy_(new_weights)

Однако вам не следует делать это для каждого нового слова, которое вы получаете, так как это копирование требует времени (и большого количества памяти, поскольку вложение существует дважды в течение короткого времени - если у вас почти не осталось памяти, просто сделайте ваше вложение достаточно большим с самого начала). Поэтому вместо этого динамически увеличивайте размер на пару сотен слотов за раз.

Кроме того, этот первый шаг уже вызывает некоторые вопросы:

  1. Как мой соответствующий nn.Module узнает о новом параметре внедрения? Об этом позаботится __setattr__ метод nn.Module (см. здесь )
  2. Во-вторых, почему бы мне просто не изменить свой параметр? Это уже указывает на некоторые проблемы смены оптимизатора: pytorch внутренне хранит ссылки по идентификатору объекта. Это означает, что если вы измените свой объект, все эти ссылки будут указывать на потенциально несовместимый объект, так как его свойства изменились. Так что вместо этого мы должны просто создать новый параметр.
  3. А как насчет других nn.Parameters или nn.Modules, которые не являются вложениями? К этим вы относитесь одинаково. Вы просто создаете их экземпляры и присоединяете их к родительскому модулю. Метод __setattr__ позаботится обо всем остальном. Таким образом, вы можете сделать это полностью синхронно ...

За исключением, конечно, оптимизатора. Оптимизатор - это единственное, что «знает» о ваших параметрах, кроме вашей основной модели-модуля. Поэтому вы должны сообщить оптимизатору о любых изменениях.

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

В любом случае, если вам все равно, простой

# simply overwrite your old optimizer
optimizer = optim.SGD(model.parameters(), lr=0.001)

сделает. Однако если вы хотите перенести старое состояние, вы можете сделать это так же, как вы можете сохранить, а затем загрузить параметры и состояния оптимизатора с диска: используя методы .state_dict() и .load_state_dict(). Это, однако, работает только с поворотом:

# extract the state dict from your old optimizer
old_state_dict = optimizer.state_dict()
# create a new optimizer
optimizer = optim.SGD(model.parameters())
new_state_dict = optimizer.state_dict()
# the old state dict will have references to the old parameters, in state_dict['param_groups'][xyz]['params'] and in state_dict['state']
# you now need to find the parameter mismatches between the old and new statedicts
# if your optimizer has multiple param groups, you need to loop over them, too (I use xyz as a placeholder here. mostly, you'll only have 1 anyways, so just replace xyz with 0
new_pars = [p for p in new_state_dict['param_groups'][xyz]['params'] if not p in old_state_dict['param_groups'][xyz]['params']]
old_pars = [p for p in old_state_dict['param_groups'][xyz]['params'] if not p in new_state_dict['param_groups'][xyz]['params']]
# then you remove all the outdated ones from the state dict
for pid in old_pars:
    old_state_dict['state'].pop(pid)
# and add a new state for each new parameter to the state:
for pid in new_pars:
    old_state_dict['param_groups'][xyz]['params'].append(pid)
    old_state_dict['state'][pid] = { ... }  # your new state def here, depending on your optimizer

Однако вот причина, по которой вам, вероятно, никогда не следует обновлять свой оптимизатор таким образом, а вместо этого следует заново инициализировать его с нуля и просто принять потерю информации о состоянии: при изменении графика вычислений вы меняете прямое и обратное вычисление для всех параметров на пути вашего вычисления (если у вас нет разветвленной архитектуры, этот путь будет вашим целым графом). В частности, это означает, что входные данные для ваших функций (= layer / nn.Module) будут другими, если вы измените некую функцию (= layer / nn.Module), примененную ранее, и градиенты изменятся, если вы измените какую-либо функцию (= Слой / nn.Module) применяется позже. Это в свою очередь делает недействительным все состояние вашего оптимизатора . Таким образом, если вы сохраняете состояние вашего оптимизатора, это будет состояние, вычисленное для другого графа вычислений, и, вероятно, приведет к катастрофическому поведению со стороны вашего оптимизатора, если вы попытаетесь применить его к новому графу вычислений. (Я был там ...)

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

0 голосов
/ 20 апреля 2019

Просто добавьте ответ на заголовок вашего вопроса: «Как динамически добавлять новые параметры в оптимизаторы в Pytorch?»

Вы можете добавлять параметры в любое время в оптимизатор:

import torch
import torch.optim as optim

model = torch.nn.Linear(2, 2) 

# Initialize optimizer
optimizer = optim.Adam(model.parameters(), lr=0.001, momentum=0.9)

extra_params = torch.randn(2, 2)
optimizer.param_groups.append({'params': extra_params })

#then you can print your `extra_params`
print("extra params", extra_params)
print("optimizer params", optimizer.param_groups)
...