Недавно я хотел использовать кодировщик моей тонко настроенной языковой модели 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