Classificando perguntas duplicadas do Quora com Keras



Classificando perguntas duplicadas do Quora com Keras

Introdução

Neste put up, usaremos Keras para classificar perguntas duplicadas do Quora. O conjunto de dados apareceu pela primeira vez na competição Kaggle Pares de perguntas quora e consiste em aproximadamente 400.000 pares de perguntas, juntamente com uma coluna indicando se o par de perguntas é considerado uma duplicata.

Nossa implementação é inspirada no Arquitetura recorrente siamesacom modificações na medida de similaridade e nas camadas de incorporação (o artigo unique usa vetores de palavras pré-treinados). Usar esse tipo de arquitetura remonta a 2005 com Le Cun et al e é útil para tarefas de verificação. A idéia é aprender uma função que mapeia os padrões de entrada em um espaço de destino, de modo que uma medida de similaridade no espaço de destino se aproxime da distância “semântica” no espaço de entrada.

Após a competição, o Quora também descreveu sua abordagem a esse problema neste Postagem do weblog.

Dados de dowloading

Os dados podem ser baixados do Kaggle Página da net do conjunto de dados
ou de Quora’s liberação do conjunto de dados:

library(keras)
quora_data <- get_file(
  "quora_duplicate_questions.tsv",
  "https://qim.ec.quoracdn.web/quora_duplicate_questions.tsv"
)

Estamos usando as keras get_file() função para que o obtain do arquivo seja em cache.

Leitura e pré -processamento

Primeiro, carregaremos dados no R e faremos algum pré -processamento para facilitar a inclusão do modelo. Depois de baixar os dados, você pode lê -los usando o Readr read_tsv() função.

Vamos criar um Keras tokenizer para transformar cada palavra em um token inteiro. Também especificaremos um hiperparâmetro do nosso modelo: o tamanho do vocabulário. Por enquanto, vamos usar as 50.000 palavras mais comuns (ajustaremos este parâmetro posteriormente). O Tokenizer será adequado usando todas as perguntas exclusivas do conjunto de dados.

tokenizer <- text_tokenizer(num_words = 50000)
tokenizer %>% fit_text_tokenizer(distinctive(c(df$question1, df$question2)))

Vamos salvar o tokenizer no disco para usá -lo para inferência posteriormente.

save_text_tokenizer(tokenizer, "tokenizer-question-pairs")

Agora usaremos o tokenizador de texto para transformar cada pergunta em uma lista de números inteiros.

question1 <- texts_to_sequences(tokenizer, df$question1)
question2 <- texts_to_sequences(tokenizer, df$question2)

Vamos dar uma olhada no número de palavras em cada pergunta. Isso nos ajudará a decidir o comprimento do preenchimento, outro hiperparâmetro do nosso modelo. O preenchimento das seqüências os normaliza do mesmo tamanho para que possamos alimentá -los com o modelo Keras.

80% 90% 95% 99% 
 14  18  23  31 

Podemos ver que 99% das perguntas têm no máximo 31, então escolheremos um comprimento de preenchimento entre 15 e 30. Vamos começar com 20 (também ajustaremos esse parâmetro posteriormente). O valor padrão do preenchimento é 0, mas já estamos usando esse valor para palavras que não aparecem dentro dos 50.000 mais frequentes, por isso usaremos 50.001.

question1_padded <- pad_sequences(question1, maxlen = 20, worth = 50000 + 1)
question2_padded <- pad_sequences(question2, maxlen = 20, worth = 50000 + 1)

Agora terminamos as etapas de pré -processamento. Agora executaremos um modelo simples de referência antes de passar para o modelo Keras.

Benchmark simples

Antes de criar um modelo complicado, vamos adotar uma abordagem simples. Vamos criar dois preditores: porcentagem de palavras da pergunta1 que aparecem no Question2 e vice-versa. Em seguida, usaremos uma regressão logística para prever se as perguntas são duplicadas.

perc_words_question1 <- map2_dbl(question1, question2, ~imply(.x %in% .y))
perc_words_question2 <- map2_dbl(question2, question1, ~imply(.x %in% .y))

df_model <- information.body(
  perc_words_question1 = perc_words_question1,
  perc_words_question2 = perc_words_question2,
  is_duplicate = df$is_duplicate
) %>%
  na.omit()

Agora que temos nossos preditores, vamos criar o modelo logístico. Tomaremos uma pequena amostra para validação.

val_sample <- pattern.int(nrow(df_model), 0.1*nrow(df_model))
logistic_regression <- glm(
  is_duplicate ~ perc_words_question1 + perc_words_question2, 
  household = "binomial",
  information = df_model(-val_sample,)
)
abstract(logistic_regression)
Name:
glm(method = is_duplicate ~ perc_words_question1 + perc_words_question2, 
    household = "binomial", information = df_model(-val_sample, ))

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-1.5938  -0.9097  -0.6106   1.1452   2.0292  

Coefficients:
                      Estimate Std. Error z worth Pr(>|z|)    
(Intercept)          -2.259007   0.009668 -233.66   <2e-16 ***
perc_words_question1  1.517990   0.023038   65.89   <2e-16 ***
perc_words_question2  1.681410   0.022795   73.76   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial household taken to be 1)

    Null deviance: 479158  on 363843  levels of freedom
Residual deviance: 431627  on 363841  levels of freedom
  (17 observations deleted on account of missingness)
AIC: 431633

Variety of Fisher Scoring iterations: 3

Vamos calcular a precisão em nosso conjunto de validação.

pred <- predict(logistic_regression, df_model(val_sample,), sort = "response")
pred <- pred > imply(df_model$is_duplicate(-val_sample))
accuracy <- desk(pred, df_model$is_duplicate(val_sample)) %>% 
  prop.desk() %>% 
  diag() %>% 
  sum()
accuracy
(1) 0.6573577

Temos uma precisão de 65,7%. Nem muito melhor do que adivinhação aleatória. Agora vamos criar nosso modelo em Keras.

Definição do modelo

Usaremos uma rede siamesa para prever se os pares são duplicados ou não. A idéia é criar um modelo que possa incorporar as perguntas (sequência de palavras) em um vetor. Em seguida, podemos comparar os vetores para cada pergunta usando uma medida de similaridade e informar se as perguntas são duplicadas ou não.

Primeiro, vamos definir as entradas para o modelo.

input1 <- layer_input(form = c(20), title = "input_question1")
input2 <- layer_input(form = c(20), title = "input_question2")

Então vamos definir a parte do modelo que incorporará as perguntas em um vetor.

word_embedder <- layer_embedding( 
  input_dim = 50000 + 2, # vocab measurement + UNK token + padding worth
  output_dim = 128,      # hyperparameter - embedding measurement
  input_length = 20,     # padding measurement,
  embeddings_regularizer = regularizer_l2(0.0001) # hyperparameter - regularization 
)

seq_embedder <- layer_lstm(
  models = 128, # hyperparameter -- sequence embedding measurement
  kernel_regularizer = regularizer_l2(0.0001) # hyperparameter - regularization 
)

Agora definiremos o relacionamento entre os vetores de entrada e as camadas de incorporação. Observe que usamos as mesmas camadas e pesos nas duas entradas. É por isso que isso é chamado de rede siamesa. Faz sentido, porque não queremos ter saídas diferentes se a pergunta1 for alterada com o Question2.

vector1 <- input1 %>% word_embedder() %>% seq_embedder()
vector2 <- input2 %>% word_embedder() %>% seq_embedder()

Em seguida, definimos a medida de similaridade que queremos otimizar. Queremos que perguntas duplicadas tenham valores mais altos de similaridade. Neste exemplo, usaremos a similaridade do cosseno, mas qualquer medida de similaridade pode ser usada. Lembre -se de que a similaridade do cosseno é o produto de ponto normalizado dos vetores, mas para o treinamento, não é necessário normalizar os resultados.

cosine_similarity <- layer_dot(checklist(vector1, vector2), axes = 1)

Em seguida, definimos uma camada sigmóide closing para gerar a probabilidade de ambas as perguntas serem duplicadas.

output <- cosine_similarity %>% 
  layer_dense(models = 1, activation = "sigmoid")

Agora que vamos definir o modelo Keras em termos de entradas e saídas e compilá -lo. Na fase de compilação, definimos nossa função de perda e otimizador. Como no Kaggle Problem, minimizaremos o logloss (equivalente a minimizar a crossentropia binária). Usaremos o Adam Optimizer.

mannequin <- keras_model(checklist(input1, input2), output)
mannequin %>% compile(
  optimizer = "adam", 
  metrics = checklist(acc = metric_binary_accuracy), 
  loss = "binary_crossentropy"
)

Podemos então dar uma olhada no modelo de fora com o abstract função.

_______________________________________________________________________________________
Layer (sort)                Output Form       Param #    Related to                 
=======================================================================================
input_question1 (InputLayer (None, 20)         0                                       
_______________________________________________________________________________________
input_question2 (InputLayer (None, 20)         0                                       
_______________________________________________________________________________________
embedding_1 (Embedding)     (None, 20, 128)    6400256    input_question1(0)(0)        
                                                          input_question2(0)(0)        
_______________________________________________________________________________________
lstm_1 (LSTM)               (None, 128)        131584     embedding_1(0)(0)            
                                                          embedding_1(1)(0)            
_______________________________________________________________________________________
dot_1 (Dot)                 (None, 1)          0          lstm_1(0)(0)                 
                                                          lstm_1(1)(0)                 
_______________________________________________________________________________________
dense_1 (Dense)             (None, 1)          2          dot_1(0)(0)                  
=======================================================================================
Whole params: 6,531,842
Trainable params: 6,531,842
Non-trainable params: 0
_______________________________________________________________________________________

Ajuste do modelo

Agora vamos nos encaixar e ajustar nosso modelo. No entanto, antes de proceder, vamos pegar uma amostra para validação.

set.seed(1817328)
val_sample <- pattern.int(nrow(question1_padded), measurement = 0.1*nrow(question1_padded))

train_question1_padded <- question1_padded(-val_sample,)
train_question2_padded <- question2_padded(-val_sample,)
train_is_duplicate <- df$is_duplicate(-val_sample)

val_question1_padded <- question1_padded(val_sample,)
val_question2_padded <- question2_padded(val_sample,)
val_is_duplicate <- df$is_duplicate(val_sample)

Agora usamos o match() função para treinar o modelo:

mannequin %>% match(
  checklist(train_question1_padded, train_question2_padded),
  train_is_duplicate, 
  batch_size = 64, 
  epochs = 10, 
  validation_data = checklist(
    checklist(val_question1_padded, val_question2_padded), 
    val_is_duplicate
  )
)
Prepare on 363861 samples, validate on 40429 samples
Epoch 1/10
363861/363861 (==============================) - 89s 245us/step - loss: 0.5860 - acc: 0.7248 - val_loss: 0.5590 - val_acc: 0.7449
Epoch 2/10
363861/363861 (==============================) - 88s 243us/step - loss: 0.5528 - acc: 0.7461 - val_loss: 0.5472 - val_acc: 0.7510
Epoch 3/10
363861/363861 (==============================) - 88s 242us/step - loss: 0.5428 - acc: 0.7536 - val_loss: 0.5439 - val_acc: 0.7515
Epoch 4/10
363861/363861 (==============================) - 88s 242us/step - loss: 0.5353 - acc: 0.7595 - val_loss: 0.5358 - val_acc: 0.7590
Epoch 5/10
363861/363861 (==============================) - 88s 242us/step - loss: 0.5299 - acc: 0.7633 - val_loss: 0.5358 - val_acc: 0.7592
Epoch 6/10
363861/363861 (==============================) - 88s 242us/step - loss: 0.5256 - acc: 0.7662 - val_loss: 0.5309 - val_acc: 0.7631
Epoch 7/10
363861/363861 (==============================) - 88s 242us/step - loss: 0.5211 - acc: 0.7701 - val_loss: 0.5349 - val_acc: 0.7586
Epoch 8/10
363861/363861 (==============================) - 88s 242us/step - loss: 0.5173 - acc: 0.7733 - val_loss: 0.5278 - val_acc: 0.7667
Epoch 9/10
363861/363861 (==============================) - 88s 242us/step - loss: 0.5138 - acc: 0.7762 - val_loss: 0.5292 - val_acc: 0.7667
Epoch 10/10
363861/363861 (==============================) - 88s 242us/step - loss: 0.5092 - acc: 0.7794 - val_loss: 0.5313 - val_acc: 0.7654

Após a conclusão do treinamento, podemos salvar nosso modelo para inferência com o save_model_hdf5()
função.

save_model_hdf5(mannequin, "model-question-pairs.hdf5")

Ajuste do modelo

Agora que temos um modelo razoável, vamos ajustar os hiperparâmetros usando o
TFRUNS pacote. Começaremos adicionando FLAGS declarações em nosso script para todos os hiperparâmetros que queremos ajustar (FLAGS Permita -nos variar os hiperpaeters sem alterar nosso código -fonte):

FLAGS <- flags(
  flag_integer("vocab_size", 50000),
  flag_integer("max_len_padding", 20),
  flag_integer("embedding_size", 256),
  flag_numeric("regularization", 0.0001),
  flag_integer("seq_embedding_size", 512)
)

Com isso FLAGS Definição agora podemos escrever nosso código em termos de sinalizadores. Por exemplo:

input1 <- layer_input(form = c(FLAGS$max_len_padding))
input2 <- layer_input(form = c(FLAGS$max_len_padding))

embedding <- layer_embedding(
  input_dim = FLAGS$vocab_size + 2, 
  output_dim = FLAGS$embedding_size, 
  input_length = FLAGS$max_len_padding, 
  embeddings_regularizer = regularizer_l2(l = FLAGS$regularization)
)

O código -fonte completo do script com FLAGS pode ser encontrado aqui.

Além disso, adicionamos um retorno de chamada de parada antecipado na etapa de treinamento para parar o treinamento se a perda de validação não diminuir para 5 épocas seguidas. Esperamos que isso reduza o tempo de treinamento para modelos ruins. Também adicionamos um redutor da taxa de aprendizado para reduzir a taxa de aprendizado em um fator de 10 quando a perda não diminui para 3 épocas (essa técnica normalmente aumenta a precisão do modelo).

mannequin %>% match(
  ...,
  callbacks = checklist(
    callback_early_stopping(endurance = 5),
    callback_reduce_lr_on_plateau(endurance = 3)
  )
)

Agora podemos executar uma corrida de ajuste para investigar a combinação excellent de hiperparâmetros. Nós chamamos o tuning_run() função, passando uma lista com os valores possíveis para cada sinalizador. O tuning_run() A função será responsável por executar o script para todas as combinações de hiperparâmetros. Nós também especificamos o pattern Parâmetro para treinar o modelo para apenas uma amostra aleatória de todas as combinações (reduzindo significativamente o tempo de treinamento).

library(tfruns)

runs <- tuning_run(
  "question-pairs.R", 
  flags = checklist(
    vocab_size = c(30000, 40000, 50000, 60000),
    max_len_padding = c(15, 20, 25),
    embedding_size = c(64, 128, 256),
    regularization = c(0.00001, 0.0001, 0.001),
    seq_embedding_size = c(128, 256, 512)
  ), 
  runs_dir = "tuning", 
  pattern = 0.2
)

A corrida de ajuste retornará um information.body com resultados para todas as execuções. Descobrimos que a melhor execução atingiu 84,9% de precisão usando a combinação de hiperparâmetros mostrados abaixo, por isso modificamos nosso script de treinamento para usar esses valores como os padrões:

FLAGS <- flags(
  flag_integer("vocab_size", 50000),
  flag_integer("max_len_padding", 20),
  flag_integer("embedding_size", 256),
  flag_numeric("regularization", 1e-4),
  flag_integer("seq_embedding_size", 512)
)

Fazendo previsões

Agora que treinamos e ajustamos nosso modelo, podemos começar a fazer previsões. No momento da previsão, carregaremos o tokenizador de texto e o modelo que salvamos no disco anterior.

library(keras)
mannequin <- load_model_hdf5("model-question-pairs.hdf5", compile = FALSE)
tokenizer <- load_text_tokenizer("tokenizer-question-pairs")

Como não continuaremos treinando o modelo, especificamos o compile = FALSE argumento.

Agora vamos definir uma função para criar previsões. Nesta função, pré -processamos os dados de entrada da mesma maneira que pré -processamos os dados de treinamento:

predict_question_pairs <- perform(mannequin, tokenizer, q1, q2) {
  q1 <- texts_to_sequences(tokenizer, checklist(q1))
  q2 <- texts_to_sequences(tokenizer, checklist(q2))
  
  q1 <- pad_sequences(q1, 20)
  q2 <- pad_sequences(q2, 20)
  
  as.numeric(predict(mannequin, checklist(q1, q2)))
}

Agora podemos chamá -lo com novos pares de perguntas, por exemplo:

predict_question_pairs(
  mannequin,
  tokenizer,
  "What's R programming?",
  "What's R in programming?"
)
(1) 0.9784008

A previsão é bastante rápida (~ 40 milissegundos).

Implantando o modelo

Para demonstrar a implantação do modelo treinado, criamos um simples Brilhante Aplicação, onde você pode colar 2 perguntas do Quora e encontrar a probabilidade de eles serem duplicados. Tente alterar as perguntas abaixo ou entrar em duas perguntas totalmente diferentes.

O aplicativo brilhante pode ser encontrado em https://jjallaire.shinyapps.io/shiny-quora/ E é o código -fonte em https://github.com/dfalbel/shiny-quora-question-airs.

Observe que, ao implantar um modelo Keras, você só precisa carregar o arquivo e o tokenizer do modelo salvo anteriormente (não são necessários dados de treinamento ou etapas de treinamento de modelo).

Embrulhando

  • Treinamos um LSTM siamês que nos dá uma precisão razoável (84%). O estado da arte do Quora é de 87%.
  • Podemos melhorar nosso modelo usando incorporações de palavras pré-treinadas em conjuntos de dados maiores. Por exemplo, tente usar o que é descrito em este exemplo. O Quora usa seu próprio corpus completo para treinar a palavra incorporação.
  • Após o treinamento, implantamos nosso modelo como um aplicativo brilhante, que dadas duas perguntas do Quora calcula a probabilidade de serem duplicatas.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *