Menu

Natural Language Processing with TensorFlow

Natural Language Processing with TensorFlow

Text is unstructured, variable in length, and made of discrete symbols, none of which a neural network can consume directly. This chapter walks through how TensorFlow turns raw text into numbers, and how those numbers flow through architectures ranging from simple sequence models up to a Transformer built entirely from scratch.

Text Tokenization and Embeddings

Tokenization is the process of splitting raw text into smaller units, words, subwords, or characters, and mapping each unit to an integer ID. TextVectorization is TensorFlow's built-in layer for this, and it can be saved directly inside your model so that preprocessing travels with it into production.

import tensorflow as tf

texts = ['TensorFlow makes deep learning accessible',

'Natural language processing is powerful']

vectorizer = tf.keras.layers.TextVectorization(

max_tokens=10000,

output_mode='int',

output_sequence_length=20,

)

vectorizer.adapt(texts)   # builds the vocabulary

sequences = vectorizer(texts)

print(sequences.shape)   # (2, 20)

Once text is integer-encoded, an Embedding layer maps each token ID to a dense vector that captures semantic meaning; words used in similar contexts end up with similar vectors. These embeddings can be learned from scratch alongside your model, or loaded pre-trained from sources like GloVe or Word2Vec.

embedding_layer = tf.keras.layers.Embedding(

input_dim=10000, # vocabulary size

output_dim=128,  # embedding dimension

input_length=20,

)

# Input: (batch, 20) integer IDs -> Output: (batch, 20, 128) dense vectors

Sequence-to-Sequence Models

Sequence-to-sequence (seq2seq) models map an input sequence to an output sequence of a potentially different length, the backbone of machine translation, summarisation, and chatbots. The classic architecture pairs an encoder, which compresses the input into a context vector, with a decoder, which generates the output one token at a time, conditioned on that context.

import tensorflow as tf

latent_dim = 256

# Encoder

encoder_inputs = tf.keras.Input(shape=(None,))

enc_emb = tf.keras.layers.Embedding(10000, latent_dim)(encoder_inputs)

_, state_h, state_c = tf.keras.layers.LSTM(latent_dim, return_state=True)(enc_emb)

encoder_states = [state_h, state_c]

# Decoder

decoder_inputs = tf.keras.Input(shape=(None,))

dec_emb = tf.keras.layers.Embedding(10000, latent_dim)(decoder_inputs)

decoder_lstm = tf.keras.layers.LSTM(latent_dim, return_sequences=True, return_state=True)

decoder_outputs, _, _ = decoder_lstm(dec_emb, initial_state=encoder_states)

decoder_outputs = tf.keras.layers.Dense(10000, activation='softmax')(decoder_outputs)

model = tf.keras.Model([encoder_inputs, decoder_inputs], decoder_outputs)

The encoder's final hidden state acts as a compressed summary of the entire input, handed to the decoder as its starting point. This works reasonably well for short sequences but struggles as inputs grow longer, which is exactly the limitation attention mechanisms were designed to fix.

Building a Transformer from Scratch

The Transformer replaced recurrence entirely with self-attention, letting every token look directly at every other token in the sequence in parallel, regardless of distance. This solves the long-range dependency problem of RNNs and is dramatically more parallelisable on GPUs and TPUs.

The core building block is scaled dot-product attention, computed from three projections of the input: Queries (Q), Keys (K), and Values (V).

import tensorflow as tf

def scaled_dot_product_attention(q, k, v, mask=None):

matmul_qk = tf.matmul(q, k, transpose_b=True)

dk = tf.cast(tf.shape(k)[-1], tf.float32)

scaled_logits = matmul_qk / tf.math.sqrt(dk)

if mask is not None:

scaled_logits += (mask * -1e9)

weights = tf.nn.softmax(scaled_logits, axis=-1)

output = tf.matmul(weights, v)

return output, weights

Multi-head attention runs several of these attention computations in parallel, each with its own learned projections, so different heads can specialise in capturing different kinds of relationships (syntax, position, meaning).

class MultiHeadAttention(tf.keras.layers.Layer):

def __init__(self, d_model, num_heads):

super().__init__()

self.num_heads = num_heads

self.depth = d_model // num_heads

self.wq = tf.keras.layers.Dense(d_model)

self.wk = tf.keras.layers.Dense(d_model)

self.wv = tf.keras.layers.Dense(d_model)

self.dense = tf.keras.layers.Dense(d_model)

def split_heads(self, x, batch_size):

x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))

return tf.transpose(x, perm=[0, 2, 1, 3])

def call(self, q, k, v, mask=None):

batch_size = tf.shape(q)[0]

q = self.split_heads(self.wq(q), batch_size)

k = self.split_heads(self.wk(k), batch_size)

v = self.split_heads(self.wv(v), batch_size)

attn_output, _ = scaled_dot_product_attention(q, k, v, mask)

attn_output = tf.transpose(attn_output, perm=[0, 2, 1, 3])

concat = tf.reshape(attn_output, (batch_size, -1, self.num_heads * self.depth))

return self.dense(concat)

A full Transformer encoder layer wraps multi-head attention and a feed-forward network with residual connections and layer normalisation, then stacks several of these layers to build depth.

Using TensorFlow Hub for NLP

Training a Transformer from scratch is valuable for learning, but in practice, most teams fine-tune a pre-trained model instead. TensorFlow Hub hosts ready-to-use models, including BERT and its many variants, that have already learned rich language representations from billions of words.

import tensorflow_hub as hub

import tensorflow as tf

bert_preprocess = hub.KerasLayer(

'https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3')

bert_encoder = hubKerasLayer(

'https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/4')

text_input = tf.keras.layers.Input(shape=(), dtype=tf.string)

encoder_inputs = bert_preprocess(text_input)

outputs = bert_encoder(encoder_inputs)

pooled_output = outputs['pooled_output']   # (batch, 768) sentence embedding

classifier = tf.keras.layers.Dense(1, activation='sigmoid')(pooled_output)

model = tf.keras.Model(text_input, classifier)

This pattern, attaching a small classification head on top of a frozen or lightly fine-tuned pre-trained encoder, is called transfer learning, and it routinely outperforms training from scratch, especially when labelled data is limited.

NLP Applications and Use Cases

Use Case

Typical Approach

Example

Sentiment analysis

Fine-tuned BERT or LSTM classifier

Classifying product reviews as positive or negative

Machine translation

Seq2seq with attention, or Transformer

Translating English text to French

Text summarisation

Encoder-decoder Transformer

Condensing news articles into short summaries

Named entity recognition

Token classification with a pre-trained encoder

Extracting names, dates, and locations from documents

Question answering

Span-extraction model on top of BERT

Finding the answer to a question inside a passage

Did You Know?

Self-attention's complexity grows quadratically with sequence length, since every token attends to every other token. This is why long-document models use techniques like sparse attention or sliding windows to remain efficient on inputs of thousands of tokens.