Недавно я хотел использовать кодировщик моей тонко настроенной языковой модели ULMFiT для создания встраиваемых документов для нас в инструменте поиска схожести документов. Я нашел несколько вопросов на форумах fastai по этому поводу, но без ответов, поэтому делюсь своим решением.

Если вы просто хотите получить ответ без объяснения причин, перейдите к tl; dr внизу.

Я предполагаю, что у вас уже есть отлаженный изучающий языковую модель ULMFiT, созданный примерно так:

from fastai.text.data import load_data
from fastai.text.models.awd_lstm import AWD_LSTM
from fastai.text.learner import language_model_learner
# my_databunch created from TextLMDataBunch.from_csv or similar
db = load_data(path, 'my_databunch.pkl', bs=64, bptt=80)
learn = language_model_learner(db, AWD_LSTM)
learn = learn.load('my_saved_learner', with_opt=True)

и тестовый документ, представляющий собой строку, например:

doc = "this is my test doc"

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

Первый подготавливает вводное предложение для вывода.

Обработка входного документа

def process_doc(doc):
    xb, yb = learn.data.one_item(doc)
    return xb

Первая строка основана на первых нескольких строках fastai.text.learner.LanguageLearner.predict. Это применяет всю предварительную обработку fastai к документу, включая токенизацию и числовую оценку, и возвращает пакет с 1 образцом в нем.

xb, yb = learner.data.one_item(doc)
print(xb, yb)
> tensor([[   2,   30,   21,  244,  271, 5952]]) tensor([0])

Выходной сигнал y всегда будет тензором 0 и может быть исключен. Мы можем проверить, что вывод x является нашим входным документом, передав его обратно через наш словарь:

learn.data.vocab.textify(xb[0])
> 'xxbos this is my test doc'

Кодировать входной документ

Вторая функция применяет первую к своему входу, а затем пропускает обработанный документ через кодировщик ULMFiT.

def encode_doc(learn, doc):
    xb = process_doc(learn, doc)
    awd_lstm = learn.model[0]
    awd_lstm.reset()
    with torch.no_grad():
        out = awd_lstm.eval()(xb)
    # Return final output, for last RNN, on last token in sequence
    return out[0][2][0][-1].detach().numpy()

Если вы читаете это, вы, вероятно, уже знакомы с архитектурой ULMFiT, но я быстро сделаю обзор. ULMFiT состоит из двух компонентов: кодировщика (AWD_LSTM) и декодера (линейный слой).

learn.model
>
SequentialRNN(
  (0): AWD_LSTM(
    (encoder): Embedding(60000, 400, padding_idx=1)
    (encoder_dp): EmbeddingDropout(
      (emb): Embedding(60000, 400, padding_idx=1)
    )
    (rnns): ModuleList(
      (0): WeightDropout(
        (module): LSTM(400, 1152, batch_first=True)
      )
      (1): WeightDropout(
        (module): LSTM(1152, 1152, batch_first=True)
      )
      (2): WeightDropout(
        (module): LSTM(1152, 400, batch_first=True)
      )
    )
    (input_dp): RNNDropout()
    (hidden_dps): ModuleList(
      (0): RNNDropout()
      (1): RNNDropout()
      (2): RNNDropout()
    )
  )
  (1): LinearDecoder(
    (decoder): Linear(in_features=400, out_features=60000, bias=True)
    (output_dp): RNNDropout()
  )
)

Мы хотим получить результат, который кодер передает декодеру, поэтому мы извлекаем кодировщик из последовательной модели.

awd_lstm = learn.model[0]

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

awd_lstm(xb)
> AttributeError: 'AWD_LSTM' object has no attribute 'hidden'

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

awd_lstm.reset()

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

with torch.no_grad():
    out = awd_lstm.eval()(xb)

Выход прямого прохода имеет много смысла. Его размеры:

(
  2,   # raw output vs output with dropout applied, explained below
  3,   # number of LSTM layers in our AWD_LSTM
  1,   # batch size
  6,   # the length of our processed input doc, including xxbos, etc
  400, # the default embedding size, which is also the output 
       # dim of the encoder, so will be the dim of our doc encodings
)

Сам вывод представляет собой кортеж из 2 элементов. Первый - это необработанный вывод каждого слоя LSTM без применения отбрасывания, второй - это вывод каждого слоя LSTM с примененным исключением скрытого слоя AWD_LSTM. Поскольку мы находимся в режиме оценки, они должны быть такими же, но на всякий случай мы будем использовать необработанные выходные данные. См. Fastai.text.models.awd_lstm.AWD_LSTM.forward.

Каждый из этих выходов представляет собой список из 3 элементов, которые являются тензорами, возвращаемыми каждым слоем LSTM нашего AWD_LSTM. Нам нужен вывод из нашего последнего слоя LSTM.

last_layer_out = out[0][2]
last_layer_out.shape()
> torch.Size([1, 6, 400])

Первое измерение нашего последнего выходного слоя - это размер нашего пакета, равного 1. Второе измерение - это длина последовательности, при этом каждое значение представляет собой выходной сигнал скрытого состояния модели LSTM на каждом временном шаге (см. Первую строку цикла for в fastai .text.learner.LanguageLearner.predict). Кодировка, которую мы хотим, представляет собой некоторую совокупность кодировок этих временных шагов. Здесь я беру max, который оказался идеальным для семантического кодирования, но среднее значение также является популярным подходом.

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

doc_encoding = last_layer_out[0].max(0).values
doc_encoding.shape
> torch.Size([400])

Это дает нам кодировку для нашего документа с длиной, равной размеру встраивания нашего кодировщика.

tl;dr

Определите эти функции и передайте учащегося и документ encode_doc.

def process_doc(learn, doc):
    xb, yb = learn.data.one_item(doc)
    return xb
def encode_doc(learn, doc):
    xb = process_doc(learn, doc)
# Reset initializes the hidden state
    awd_lstm = learn.model[0]
    awd_lstm.reset()
    with torch.no_grad():
        out = awd_lstm.eval()(xb)
    # Return raw output, for last RNN, on last token in sequence
    return out[0][2][0].max(0).values.detach().numpy()

Ссылка:

(1) https://towardsdatascience.com/the-hype-behind-infersent-aaceb9449283