From 0c3a91c4431d85a6a77e8f0970bb13d4e7ca5c6d Mon Sep 17 00:00:00 2001 From: Michael Tu Date: Fri, 25 May 2018 00:15:50 -0400 Subject: [PATCH] Check in MP-CNN Lite Model (#108) * Add MP-CNN Lite model * MP-CNN Lite bug fixes --- mp_cnn/__main__.py | 6 ++- mp_cnn/lite_model.py | 116 +++++++++++++++++++++++++++++++++++++++++++ mp_cnn/model.py | 2 +- 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 mp_cnn/lite_model.py diff --git a/mp_cnn/__main__.py b/mp_cnn/__main__.py index 060b11f..2c39516 100644 --- a/mp_cnn/__main__.py +++ b/mp_cnn/__main__.py @@ -13,6 +13,7 @@ from common.evaluation import EvaluatorFactory from common.train import TrainerFactory from utils.serialization import load_checkpoint from .model import MPCNN +from .lite_model import MPCNNLite def get_logger(): @@ -39,6 +40,7 @@ def evaluate_dataset(split_name, dataset_cls, model, embedding, loader, batch_si if __name__ == '__main__': parser = argparse.ArgumentParser(description='PyTorch implementation of Multi-Perspective CNN') parser.add_argument('model_outfile', help='file to save final model') + parser.add_argument('--arch', help='model architecture to use', choices=['mpcnn', 'mpcnn_lite'], default='mpcnn') parser.add_argument('--dataset', help='dataset to use, one of [sick, msrvid, trecqa, wikiqa]', default='sick') parser.add_argument('--word-vectors-dir', help='word vectors directory', default=os.path.join(os.pardir, 'Castor-data', 'embeddings', 'GloVe')) @@ -95,7 +97,9 @@ if __name__ == '__main__': filter_widths = list(range(1, args.max_window_size + 1)) + [np.inf] ext_feats = dataset_cls.EXT_FEATS if args.sparse_features else 0 - model = MPCNN(args.word_vectors_dim, args.holistic_filters, args.per_dim_filters, filter_widths, + + model_cls = MPCNN if args.arch == 'mpcnn' else MPCNNLite + model = model_cls(args.word_vectors_dim, args.holistic_filters, args.per_dim_filters, filter_widths, args.hidden_units, dataset_cls.NUM_CLASSES, args.dropout, ext_feats, args.attention, args.wide_conv) diff --git a/mp_cnn/lite_model.py b/mp_cnn/lite_model.py new file mode 100644 index 0000000..51c4813 --- /dev/null +++ b/mp_cnn/lite_model.py @@ -0,0 +1,116 @@ +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +from mp_cnn.model import MPCNN + + +class MPCNNLite(MPCNN): + + def __init__(self, n_word_dim, n_holistic_filters, n_per_dim_filters, filter_widths, hidden_layer_units, num_classes, dropout, ext_feats, attention, wide_conv): + super(MPCNNLite, self).__init__(n_word_dim, n_holistic_filters, n_per_dim_filters, filter_widths, hidden_layer_units, num_classes, dropout, ext_feats, attention, wide_conv) + self.arch = 'mpcnn_lite' + + def _add_layers(self): + holistic_conv_layers_max = [] + + for ws in self.filter_widths: + if np.isinf(ws): + continue + + padding = ws - 1 if self.wide_conv else 0 + + holistic_conv_layers_max.append(nn.Sequential( + nn.Conv1d(self.in_channels, self.n_holistic_filters, ws, padding=padding), + nn.Tanh() + )) + + self.holistic_conv_layers_max = nn.ModuleList(holistic_conv_layers_max) + + def _get_n_feats(self): + COMP_1_COMPONENTS_HOLISTIC, COMP_2_COMPONENTS = 2 + self.n_holistic_filters, 2 + n_feats_h = self.n_holistic_filters * COMP_2_COMPONENTS + n_feats_v = ( + # comparison units from holistic conv for max pooling for non-infinite widths + ((len(self.filter_widths) - 1) ** 2) * COMP_1_COMPONENTS_HOLISTIC + + # comparison units from holistic conv for max pooling for infinite widths + 3 + ) + n_feats = n_feats_h + n_feats_v + self.ext_feats + return n_feats + + def _get_blocks_for_sentence(self, sent): + block_a = {} + for ws in self.filter_widths: + if np.isinf(ws): + sent_flattened, sent_flattened_size = sent.contiguous().view(sent.size(0), 1, -1), sent.size(1) * sent.size(2) + block_a[ws] = { + 'max': F.max_pool1d(sent_flattened, sent_flattened_size).view(sent.size(0), -1) + } + continue + + holistic_conv_out_max = self.holistic_conv_layers_max[ws - 1](sent) + block_a[ws] = { + 'max': F.max_pool1d(holistic_conv_out_max, holistic_conv_out_max.size(2)).contiguous().view(-1, self.n_holistic_filters) + } + + return block_a + + def _algo_1_horiz_comp(self, sent1_block_a, sent2_block_a): + comparison_feats = [] + regM1, regM2 = [], [] + for ws in self.filter_widths: + x1 = sent1_block_a[ws]['max'].unsqueeze(2) + x2 = sent2_block_a[ws]['max'].unsqueeze(2) + if np.isinf(ws): + x1 = x1.expand(-1, self.n_holistic_filters, -1) + x2 = x2.expand(-1, self.n_holistic_filters, -1) + regM1.append(x1) + regM2.append(x2) + + regM1 = torch.cat(regM1, dim=2) + regM2 = torch.cat(regM2, dim=2) + + # Cosine similarity + comparison_feats.append(F.cosine_similarity(regM1, regM2, dim=2)) + # Euclidean distance + pairwise_distances = [] + for x1, x2 in zip(regM1, regM2): + dist = F.pairwise_distance(x1, x2).view(1, -1) + pairwise_distances.append(dist) + comparison_feats.append(torch.cat(pairwise_distances)) + + return torch.cat(comparison_feats, dim=1) + + def _algo_2_vert_comp(self, sent1_block_a, sent2_block_a): + comparison_feats = [] + ws_no_inf = [w for w in self.filter_widths if not np.isinf(w)] + for ws1 in self.filter_widths: + x1 = sent1_block_a[ws1]['max'] + for ws2 in self.filter_widths: + x2 = sent2_block_a[ws2]['max'] + if (not np.isinf(ws1) and not np.isinf(ws2)) or (np.isinf(ws1) and np.isinf(ws2)): + comparison_feats.append(F.cosine_similarity(x1, x2).unsqueeze(1)) + comparison_feats.append(F.pairwise_distance(x1, x2).unsqueeze(1)) + comparison_feats.append(torch.abs(x1 - x2)) + + return torch.cat(comparison_feats, dim=1) + + def forward(self, sent1, sent2, ext_feats=None, word_to_doc_count=None, raw_sent1=None, raw_sent2=None): + # Attention + if self.attention != 'none': + sent1, sent2 = self.concat_attention(sent1, sent2, word_to_doc_count, raw_sent1, raw_sent2) + + # Sentence modeling module + sent1_block_a = self._get_blocks_for_sentence(sent1) + sent2_block_a = self._get_blocks_for_sentence(sent2) + + # Similarity measurement layer + feat_h = self._algo_1_horiz_comp(sent1_block_a, sent2_block_a) + feat_v = self._algo_2_vert_comp(sent1_block_a, sent2_block_a) + combined_feats = [feat_h, feat_v, ext_feats] if self.ext_feats else [feat_h, feat_v] + feat_all = torch.cat(combined_feats, dim=1) + + preds = self.final_layers(feat_all) + return preds diff --git a/mp_cnn/model.py b/mp_cnn/model.py index a6104d6..96fc658 100644 --- a/mp_cnn/model.py +++ b/mp_cnn/model.py @@ -216,7 +216,7 @@ class MPCNN(nn.Module): attention_emb2 = torch.cat((attention_weighted_sent2, sent2), dim=1) return attention_emb1, attention_emb2 - def forward(self, sent1, sent2, ext_feats=None, word_to_doc_count=None, raw_sent1=None, raw_sent2=None, sent1_nonstatic=None, sent2_nonstatic=None): + def forward(self, sent1, sent2, ext_feats=None, word_to_doc_count=None, raw_sent1=None, raw_sent2=None): # Attention if self.attention != 'none': sent1, sent2 = self.concat_attention(sent1, sent2, word_to_doc_count, raw_sent1, raw_sent2)