# Named Entity Recognition (NER) Implementation Approaches: LSTM vs. BERT in Microsoft AI-For-Beginners

> Explore Named Entity Recognition NER implementation approaches. Compare classic LSTM models with powerful BERT transformers using Microsoft AI-For-Beginners examples.

- Repository: [Microsoft/AI-For-Beginners](https://github.com/microsoft/AI-For-Beginners)
- Tags: deep-dive
- Published: 2026-08-03

---

**The Microsoft AI-For-Beginners repository implements two primary approaches for Named Entity Recognition: a classic LSTM-based sequence model built with TensorFlow/Keras, and a transformer-based approach using pre-trained BERT from Hugging Face.**

The [`microsoft/AI-For-Beginners`](https://github.com/microsoft/AI-For-Beginners) repository provides hands-on implementations for NER in its natural language processing curriculum. Located in `lessons/5-NLP/19-NER/`, the materials demonstrate how modern deep learning architectures solve token classification tasks. This guide examines both implementation approaches with complete, runnable code examples from the source.

## Approach 1: LSTM-Based NER with TensorFlow/Keras

The repository's primary implementation uses **recurrent neural networks with LSTM layers** to predict entity tags sequence-to-sequence. This classical approach teaches foundational concepts before introducing transformers.

### Architecture Overview

The model in `lessons/5-NLP/19-NER/NER-TF.ipynb` stacks:
- **Embedding layer** for dense token representations
- **Bidirectional LSTM** for context-aware encoding
- **Dense output layer** with softmax for tag prediction

### Core Implementation

```python
import tensorflow as tf
from tensorflow.keras import layers, Model

# Build LSTM-based NER model as implemented in NER-TF.ipynb

def build_lstm_ner_model(vocab_size, embedding_dim, lstm_units, num_tags):
    """
    Constructs the LSTM NER model from lessons/5-NLP/19-NER/NER-TF.ipynb
    
    Parameters match the notebook's architecture for CoNLL-2003 style data.
    """
    inputs = layers.Input(shape=(None,), dtype='int32', name='token_ids')
    
    # Token embeddings - converts word indices to dense vectors

    x = layers.Embedding(
        input_dim=vocab_size,
        output_dim=embedding_dim,
        mask_zero=True,  # Handles variable-length sequences

        name='token_embedding'
    )(inputs)
    
    # Bidirectional LSTM captures left-to-right and right-to-left context

    x = layers.Bidirectional(
        layers.LSTM(lstm_units, return_sequences=True),
        name='bilstm_encoder'
    )(x)
    
    # Output: probability distribution over NER tags for each token

    outputs = layers.Dense(num_tags, activation='softmax', name='ner_classifier')(x)
    
    model = Model(inputs, outputs, name='lstm_ner')
    return model

# Example usage matching the notebook parameters

vocab_size = 20000      # Size of token vocabulary

embedding_dim = 100     # Dimension of token embeddings

lstm_units = 128        # Hidden units in each LSTM direction

num_tags = 9            # BIO tags: O, B-PER, I-PER, B-LOC, I-LOC, B-ORG, I-ORG, B-MISC, I-MISC

model = build_lstm_ner_model(vocab_size, embedding_dim, lstm_units, num_tags)
model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

```

### Data Format and BIO Tagging

The LSTM implementation expects **BIO-encoded** sequences (Beginning, Inside, Outside). The repository uses the CoNLL-2003 dataset format where each token maps to a tag:

```python

# Sample data structure from ner_dataset.csv referenced in the notebook

# Token       POS    Chunk    NER_tag

# ------------------------------------

# EU          NNP    B-NP     B-ORG

# rejects     VBZ    B-VP     O

# German      JJ     B-NP     B-MISC

# call        NN     I-NP     O

# to          TO     B-VP     O

# boycott     VB     I-VP     O

# British     JJ     B-NP     B-MISC

# lamb        NN     I-NP     O

```

### Training Configuration

```python

# Training parameters from NER-TF.ipynb

history = model.fit(
    x_train,           # Padded token sequences (batch_size, max_len)

    y_train,           # Padded tag indices (batch_size, max_len)

    validation_split=0.1,
    batch_size=32,
    epochs=5,
    callbacks=[
        tf.keras.callbacks.EarlyStopping(patience=2, restore_best_weights=True)
    ]
)

```

## Approach 2: Transformer-Based NER with BERT

The notebook references a **production-grade implementation** using Hugging Face transformers. This approach leverages pre-trained contextual representations rather than training embeddings from scratch.

### Why BERT for NER

- **Pre-trained context**: BERT captures semantic relationships from billions of tokens
- **Fine-tuning efficiency**: Requires less task-specific data than LSTM training
- **State-of-the-art accuracy**: Contextualized representations handle polysemy and ambiguous boundaries

### Implementation with Hugging Face

The notebook points to [Hugging Face's NER tutorial](https://huggingface.co/course/chapter7/2?fw=pt). Below is the equivalent implementation using the same patterns:

```python
from transformers import (
    AutoTokenizer,
    AutoModelForTokenClassification,
    TrainingArguments,
    Trainer,
    DataCollatorForTokenClassification
)
from datasets import load_dataset
import numpy as np

# BERT-based NER implementation matching the Hugging Face approach

class BERT_NER_Pipeline:
    """
    Fine-tunes BERT for token classification as referenced in NER-TF.ipynb.
    Uses bert-base-cased (cased preserves entity capitalization patterns).
    """
    
    def __init__(self, model_name='bert-base-cased', num_labels=9):
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModelForTokenClassification.from_pretrained(
            model_name,
            num_labels=num_labels
        )
        self.label_list = ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 
                          'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']
    
    def tokenize_and_align_labels(self, examples):
        """
        Tokenizes words and aligns NER labels with WordPiece subwords.
        Critical: -100 label ignores subword tokens after the first in a word.
        """
        tokenized_inputs = self.tokenizer(
            examples['tokens'],
            truncation=True,
            is_split_into_words=True,
            padding='max_length',
            max_length=128
        )
        
        labels = []
        for i, label in enumerate(examples['ner_tags']):
            word_ids = tokenized_inputs.word_ids(batch_index=i)
            previous_word_idx = None
            label_ids = []
            
            for word_idx in word_ids:
                if word_idx is None:
                    # Special tokens ([CLS], [SEP], [PAD]) get -100 (ignored)

                    label_ids.append(-100)
                elif word_idx != previous_word_idx:
                    # First subword of a word gets the actual label

                    label_ids.append(label[word_idx])
                else:
                    # Subsequent subwords are ignored in loss computation

                    label_ids.append(-100)
                previous_word_idx = word_idx
            
            labels.append(label_ids)
        
        tokenized_inputs['labels'] = labels
        return tokenized_inputs
    
    def compute_metrics(self, eval_pred):
        """Evaluates precision, recall, F1 excluding -100 (ignored) labels."""
        predictions, labels = eval_pred
        predictions = np.argmax(predictions, axis=2)
        
        true_predictions = [
            self.label_list[p] 
            for pred, label in zip(predictions, labels) 
            for p, l in zip(pred, label) if l != -100
        ]
        true_labels = [
            self.label_list[l] 
            for label in labels 
            for l in label if l != -100
        ]
        
        from seqeval.metrics import classification_report
        return classification_report(true_labels, true_predictions, output_dict=True)

# Training setup

def train_bert_ner():
    """Minimal training loop equivalent to the Hugging Face tutorial."""
    # Load CoNLL-2003 or compatible dataset

    dataset = load_dataset('conll2003')  # Or custom dataset matching repo format

    
    pipeline = BERT_NER_Pipeline()
    
    # Preprocess: tokenize and align labels

    tokenized_dataset = dataset.map(
        pipeline.tokenize_and_align_labels,
        batched=True,
        remove_columns=dataset['train'].column_names
    )
    
    # Training configuration

    training_args = TrainingArguments(
        output_dir='./bert-ner-results',
        evaluation_strategy='epoch',
        learning_rate=5e-5,
        per_device_train_batch_size=16,
        per_device_eval_batch_size=16,
        num_train_epochs=3,
        weight_decay=0.01,
        save_total_limit=2,
        load_best_model_at_end=True,
        metric_for_best_model='f1'
    )
    
    trainer = Trainer(
        model=pipeline.model,
        args=training_args,
        train_dataset=tokenized_dataset['train'],
        eval_dataset=tokenized_dataset['validation'],
        tokenizer=pipeline.tokenizer,
        data_collator=DataCollatorForTokenClassification(pipeline.tokenizer),
        compute_metrics=pipeline.compute_metrics
    )
    
    trainer.train()
    return trainer

```

### Hardware Considerations

| Approach | VRAM Requirement | Training Time | Inference Speed |
|----------|-----------------|---------------|---------------|
| LSTM (128 units) | ~2 GB | ~30 min/epoch (CPU) | Fast |
| BERT-base | ~8-12 GB | ~2 hrs/epoch (GPU) | Moderate |

## Comparing the Two Approaches

**LSTM-based NER** (`lessons/5-NLP/19-NER/NER-TF.ipynb`) excels when:
- Teaching fundamental sequence modeling concepts
- Deploying to resource-constrained environments
- Working with domain-specific vocabularies without pre-trained embeddings

**BERT-based NER** (Hugging Face tutorial) excels when:
- Maximum accuracy is required on standard entity types
- Limited labeled data is available (transfer learning from pre-training)
- Downstream integration with other NLP pipelines

### Performance Characteristics

According to standard benchmarks referenced in the repository materials:
- LSTM with GloVe embeddings: ~85% F1 on CoNLL-2003
- BERT-base fine-tuned: ~91-92% F1 on CoNLL-2003

## Key Files and Resources

- **[`lessons/5-NLP/19-NER/NER-TF.ipynb`](https://github.com/microsoft/AI-For-Beginners/blob/main/lessons/5-NLP/19-NER/NER-TF.ipynb)** — Complete LSTM implementation with data loading, model training, and evaluation
- **[CoNLL-2003 dataset](https://www.kaggle.com/datasets/alaakhaled/conll003-englishversion)** — Standard NER benchmark used by the notebook (download via Kaggle)
- **[Hugging Face NER Course](https://huggingface.co/course/chapter7/2?fw=pt)** — BERT fine-tuning tutorial referenced in notebook comments

## Summary

- **Two validated approaches**: The Microsoft AI-For-Beginners repository demonstrates both classical (LSTM) and modern (BERT) NER implementations
- **LSTM implementation**: Found in `NER-TF.ipynb`, uses bidirectional LSTM with embedding layers, trains from scratch on BIO-tagged data
- **BERT implementation**: Referenced via Hugging Face, leverages `AutoModelForTokenClassification` with label alignment for WordPiece tokenization
- **Key technical detail**: BERT requires careful label alignment (`-100` for ignored subwords) while LSTM uses simple sequence-to-sequence mapping
- **Learning progression**: The repository intentionally presents LSTM first to build intuition before introducing transformer complexity

## Frequently Asked Questions

### What dataset does the LSTM notebook use?

The `NER-TF.ipynb` notebook references the **CoNLL-2003 English dataset**, a standard benchmark containing news articles annotated with person, organization, location, and miscellaneous entities. The data follows BIO tagging format with tab-separated columns for tokens, part-of-speech tags, chunk tags, and NER labels. Download links are typically provided within the notebook or via the repository's data specifications.

### Why does BERT need label alignment while LSTM does not?

**BERT uses WordPiece tokenization**, which splits rare words into subwords (e.g., "playing" → "play", "##ing"). Since NER labels apply to whole words, not subwords, the Hugging Face implementation assigns `-100` to all subwords after the first so they don't contribute to loss. **LSTM implementations typically use whole-word vocabulary indices**, maintaining a 1:1 token-label correspondence without subword splitting.

### Can I run the LSTM notebook without a GPU?

**Yes, the LSTM model trains efficiently on CPU** due to its modest parameter count (~2-5 million). The bidirectional LSTM in `NER-TF.ipynb` uses 128-dimensional hidden states, which modern CPUs handle in reasonable time (minutes per epoch for small datasets). BERT fine-tuning practically requires GPU acceleration—expect 10-50x slower training on CPU with significantly higher memory requirements.

### How do I extend these approaches to custom entity types?

**Modify the `num_tags` or `num_labels` parameter** to match your tag inventory. For the LSTM approach in `lessons/5-NLP/19-NER/NER-TF.ipynb`, change the final `Dense` layer output dimension. For BERT, pass your custom `id2label` and `label2id` mappings to `AutoModelForTokenClassification.from_pretrained()`. Ensure your training data uses consistent BIO encoding for the new entity types.