function getModel(opts) {
  const {embeddingDims, sequenceLength, modelType, numLabels, weights} = opts;

  const model = tf.sequential();
  model.add(tf.layers.inputLayer({
    inputShape: [sequenceLength, embeddingDims],
  }));

  // This function can return one of three different model architectures:
  //  'lstm': A unidirectional (one layer) LSTM
  //  'bidirectional-lstm': A bidirectional (one layer) LSTM
  //  'dense': A single layer dense network. This can be a baseline.

  let lstmLayer;
  switch (modelType) {
    case 'lstm':
      lstmLayer = tf.layers.lstm({
        units: sequenceLength,
        recurrentInitializer: 'glorotNormal',
        returnSequences: true,
      });
      break;
    case 'bidirectional-lstm':
      lstmLayer = tf.layers.lstm({
        units: sequenceLength,
        recurrentInitializer: 'glorotNormal',
        returnSequences: true,
      });
      lstmLayer = tf.layers.bidirectional({layer: lstmLayer});
      break;
  }

  if (modelType.match('lstm')) {
    model.add(lstmLayer);
    const dense = tf.layers.dense({units: numLabels});
    model.add(tf.layers.timeDistributed({layer: dense}));
    model.add(tf.layers.activation({activation: 'softmax'}));
  } else {
    const dense = tf.layers.dense({units: numLabels, activation: 'softmax'});
    model.add(dense);
  }

  model.compile({
    optimizer: 'adam',
    loss: 'categoricalCrossentropy',
    metrics: ['accuracy'],
    // Optionally weight misclassifications for different tags differently.
    // This allows us to prioritize getting certain classes correct.
    classWeight: weights,
  });

  model.summary();

  return model;
}
Return a tf.Model to tag tokens in an input sequence.

@param {Object} opts
@return {tf.Model} the model instance