Unit 11, lesson 72

Text Features and TF-IDF

How tokenization and a vocabulary fitted on training documents turn text into bag-of-words counts, how document frequency gives TF-IDF weights, and why the document-term matrix is stored as a sparse matrix.

  • Video 72
  • Sparse Text Features and Classification

A delivery service wants to sort short customer messages such as “parcel arrives late” by what they say. A classifier multiplies feature vectors by weights, so each message first has to become a vector, by a rule that gives the same vector every time and that also works for messages written after the model was fitted. Which words become features, how much should a word count that appears in almost every message, and what does the vector forget? This lesson answers these questions for four constructed messages, from tokens to a weighted sparse matrix, and states every convention the numbers depend on.

Key terms

Tokenization

Definition Tokenization is a fixed rule that splits a document, a string of characters, into a sequence of tokens, the units that will later be counted. The rule on this page first lowercases the text and then takes every maximal run of the letters a to z as one token, in order. Every other character, including digits, punctuation, apostrophes, spaces and letters with accents, only separates tokens and is dropped. No word is reduced to a stem and no common word is removed. Other rules split at whitespace, keep numbers, remove stop words such as the, reduce words to stems or cut rare words into subword pieces, and each choice defines a different representation. The rule is part of the model, and the same rule must run at training time and at prediction time.

In plain words Tokenization decides what counts as a word. Every later step sees only the tokens, so whatever the rule throws away is lost to every model built on it. A rule this simple suits short English messages and fails in predictable ways on contractions, numbers, accented words and other writing systems.

Example The four constructed training messages are “parcel arrives today”, “parcel arrives late”, “train arrives today” and “train leaves early”, and each one splits into three tokens. The message “PARCEL, parcel, Parcel!” gives parcel three times, so case and punctuation disappear. The message “Parcel #42 didn’t arrive” gives parcel, didn, t and arrive. The number is gone, and the contraction falls apart into two pieces, one of them the single letter t. The word “café” becomes caf.

Vocabulary

Definition The vocabulary VV is the list of distinct terms that the representation keeps, in a fixed order, so that term jj owns column jj of every feature vector. On this page it is fitted on the training documents only. It contains every token that occurs in at least one training document, sorted alphabetically, and its size is p=∣V∣p = \lvert V \rvert. Applying the fitted representation to validation, test or future documents never changes VV, and a token outside VV is ignored, which is this page’s declared policy for unknown tokens. Rules that prune the vocabulary, for example by keeping only terms found in at least a set number of training documents, are part of the fit as well. Other policies add a column that counts unknown tokens or spell words with subword pieces, and each of them is a different representation that has to be declared.

In plain words The vocabulary decides which words the model can see and where each one sits. Its order matters as much as its contents, because a fitted weight belongs to a column, and the column has to mean the same word when the model is used. Building the vocabulary from all documents, the test documents included, would let the test set decide which features exist.

Example The four training messages contain seven distinct tokens. In alphabetical order the vocabulary is arrives, early, late, leaves, parcel, today and train, so p=7p = 7, arrives owns column 1 and train owns column 7. The new message “parcel arrives tomorrow” keeps parcel and arrives and drops tomorrow. The message “mystery” keeps nothing, so its feature vector is the zero vector, and a linear classifier can only answer it with its intercept, a multinomial naive Bayes classifier only with its class prior. That answer carries no information about the message and should be flagged rather than trusted.

Bag of words and document-term matrix

Definition A bag-of-words representation describes a document by the number of times each vocabulary term occurs in it, regardless of where. For nn documents and a vocabulary of pp terms, the document-term matrix is the n×pn \times p matrix CC whose entry cijc_{ij} is the number of tokens of document ii equal to term jj. Row ii is the count vector of document ii, every entry is a nonnegative integer, and the row sum is the number of the document’s tokens that lie in the vocabulary. Two documents get the same row exactly when their tokens in the vocabulary agree as a multiset, that is, the same terms with the same counts, in whatever order.

In plain words Pour the tokens of a message into a bag and count what is inside. The counts survive and the order does not. Any model that receives only the counts must give the same output for two messages with the same counts, however different their meanings. Counting pairs of adjacent tokens, called bigrams, keeps some local order at the price of a much larger vocabulary, and sequence models keep order in another way.

Example With the columns in vocabulary order, the four training messages give the document-term matrix

[1000110101010010000110101001].\begin{bmatrix} 1 & 0 & 0 & 0 & 1 & 1 & 0 \\ 1 & 0 & 1 & 0 & 1 & 0 & 0 \\ 1 & 0 & 0 & 0 & 0 & 1 & 1 \\ 0 & 1 & 0 & 1 & 0 & 0 & 1 \end{bmatrix} .

The first row has ones in the columns of arrives, parcel and today. Now take the messages “train leaves early, parcel arrives late” and “train leaves late, parcel arrives early”. Both have count 1 for every term except today, so they get the same row, although they disagree about which trip is late. Their bigrams differ, since only the first contains leaves early and arrives late.

Term frequency and document frequency

Definition The term frequency of term jj in document ii measures how prominent the term is inside that one document. On this page it is the raw count cijc_{ij}. Common alternatives are the relative frequency cij/∑kcikc_{ij} / \sum_k c_{ik} for a document with at least one known token, the indicator of cij>0c_{ij} > 0, and 1+ln⁡cij1 + \ln c_{ij} when cij>0c_{ij} > 0 with 0 otherwise. The document frequency of term jj is the number of training documents that contain it at least once,

df⁡j=∑i=1n1{cij>0},\operatorname{df}_j = \sum_{i=1}^{n} \mathbf{1}\{c_{ij} > 0\} ,

where the indicator 1{⋅}\mathbf{1}\{\cdot\} equals 1 when its condition holds and 0 otherwise. When the vocabulary comes from the same nn training documents, every term has 1≤df⁡j≤n1 \le \operatorname{df}_j \le n. Like the vocabulary, the document frequencies are computed from the training documents and then kept fixed.

In plain words Term frequency looks inside one document and asks how often. Document frequency looks across the training collection and asks in how many documents. Repeating a word five times in one message raises that message’s term frequency to 5 but adds only 1 to the word’s document frequency.

Example In the training matrix arrives occurs in three of the four messages, so its document frequency is 3. The terms parcel, today and train occur in two messages each, and early, late and leaves in one each. Had the second message read “parcel parcel arrives late”, its term frequency for parcel would be 2, while the document frequency of parcel would still be 2, because that message counts once.

TF-IDF

Definition TF-IDF, short for term frequency times inverse document frequency, is a family of weightings that multiply each term frequency by a weight that decreases as the term’s document frequency grows. This page uses raw counts and the smoothed inverse document frequency

idf⁡j=1+ln⁡1+n1+df⁡j,\operatorname{idf}_j = 1 + \ln \frac{1 + n}{1 + \operatorname{df}_j} ,

with the natural logarithm and the nn training documents, so the TF-IDF matrix has entries tij=cijidf⁡jt_{ij} = c_{ij} \operatorname{idf}_j. Because df⁡j≤n\operatorname{df}_j \le n, every weight satisfies idf⁡j≥1\operatorname{idf}_j \ge 1, with equality exactly for a term found in every training document, which therefore keeps weight 1 instead of disappearing. An optional row normalization divides each nonzero row ti\mathbf{t}_i by its Euclidean length ∥ti∥2\lVert \mathbf{t}_i \rVert_2 and leaves a zero row as the zero vector, since a zero row has no direction to keep. Other members of the family use ln⁡(n/df⁡j)\ln(n / \operatorname{df}_j), which gives weight 0 to a term in every document, other logarithm bases or other term frequencies. The name TF-IDF alone does not fix the numbers, so the formula has to be stated with the model.

In plain words A word that appears in almost every message does little to tell messages apart, so it is weighted down, and a word confined to a few messages is weighted up. The weight measures spread in the training collection, not meaning or importance. A typo or a customer number is rare too, and a large weight does not make such a token reliable or appropriate to use. After normalization every nonzero row has length 1, whatever the length of the message, and the dot product of two normalized nonzero rows is their cosine similarity.

Example With n=4n = 4 the weights follow from the document frequencies.

TermsDocument frequencyIDF weight
arrives31+ln⁡(5/4)≈1.22311 + \ln(5/4) \approx 1.2231
parcel, today, train21+ln⁡(5/3)≈1.51081 + \ln(5/3) \approx 1.5108
early, late, leaves11+ln⁡(5/2)≈1.91631 + \ln(5/2) \approx 1.9163

The first message gets the value 1.2231 for arrives and 1.5108 for each of parcel and today, with zeros elsewhere. Its squared length is 1.22312+2⋅1.51082≈6.06131.2231^2 + 2 \cdot 1.5108^2 \approx 6.0613, so its length is about 2.4620 and its normalized nonzero values are about 0.4968, 0.6137 and 0.6137. The weighting changed the values but not the positions of the zeros.

Sparse matrix

Definition A matrix is sparse when most of its entries are zero, and a sparse storage format keeps only the nonzero entries and their positions. Their number is written nnz⁡\operatorname{nnz}. Compressed sparse row (CSR) storage of an n×pn \times p matrix uses three arrays. Counting from 0, they hold the nonzero values row by row, the column index of each value, and n+1n + 1 row pointers, where the entries of row ii occupy the positions from pointer ii up to but not including pointer i+1i + 1. The matrix and its shape n×pn \times p stay the same, only the storage changes. Memory and the cost of a matrix-vector product grow with nnz⁡+n\operatorname{nnz} + n instead of npnp. Operations that send zeros to zeros, such as multiplying columns by IDF weights or dividing rows by their lengths, keep the matrix sparse. Subtracting the column means does not, since it turns every zero of a column with a nonzero mean into a nonzero entry.

In plain words One message uses a handful of words from a vocabulary that may hold tens of thousands, so nearly every entry of a text matrix is zero. Storing only the nonzero entries lets such matrices fit in memory, and computations skip the zeros. Centering the columns, as standard scaling does, would fill the matrix, which is why scaling for sparse text features usually leaves out the centering step.

Example The training count matrix has 12 nonzero entries among 28, so 3/73/7 of it, about 43 percent, is filled, and at this tiny size the storage format makes no practical difference. Its CSR arrays are twelve values equal to 1, the column indices 0, 4, 5, 0, 2, 4, 0, 5, 6, 1, 3, 6 and the row pointers 0, 3, 6, 9, 12. At a constructed but realistic scale of 100,000 documents, a vocabulary of 50,000 terms and 40 distinct terms per document on average, only 0.08 percent of the entries are nonzero. The dense matrix has 5×1095 \times 10^9 entries, about 40 gigabytes as 8-byte floating-point numbers, while CSR with 4-byte column indices and 8-byte row pointers needs about 49 megabytes. The code below fits the vocabulary and the weights on the training messages only and stores each new row as a dictionary from column index to weight.

import math
import re

def tokens(text):
    return re.findall(r"[a-z]+", text.lower())

train = ["parcel arrives today", "parcel arrives late",
         "train arrives today", "train leaves early"]

# fitted on the training documents only, then frozen
vocab = sorted({w for d in train for w in tokens(d)})
column = {w: j for j, w in enumerate(vocab)}
n = len(train)
df = [sum(w in tokens(d) for d in train) for w in vocab]
idf = [1 + math.log((1 + n) / (1 + k)) for k in df]

def tfidf_row(text):
    """Sparse row {column index: weight}, unknown tokens ignored."""
    counts = {}
    for w in tokens(text):
        if w in column:
            j = column[w]
            counts[j] = counts.get(j, 0) + 1
    return {j: round(c * idf[j], 4) for j, c in counts.items()}

print(len(vocab), df)                       # 7 [3, 1, 1, 1, 2, 2, 2]
print(tfidf_row("train arrives tomorrow"))  # {6: 1.5108, 0: 1.2231}
print(tfidf_row("mystery"))                 # {}

Check yourself

  1. Apply the representation fitted on the four training messages, with unnormalized TF-IDF, to the new message “parcel parcel late”. Give its nonzero entries and its normalized version. Does the document frequency of parcel change?
Show the answer

The tokens are parcel, parcel and late, so the counts are 2 for parcel and 1 for late. Multiplying by the frozen weights gives

tparcel=2(1+ln⁡53)≈3.0217,tlate=1+ln⁡52≈1.9163,\begin{aligned} t_{\text{parcel}} &= 2\bigl(1 + \ln \tfrac{5}{3}\bigr) \\[0.4em] &\approx 3.0217 , \\[0.4em] t_{\text{late}} &= 1 + \ln \tfrac{5}{2} \\[0.4em] &\approx 1.9163 , \end{aligned}

and zeros in the other five columns. The squared length is 3.02172+1.91632≈12.80253.0217^2 + 1.9163^2 \approx 12.8025, so the length is about 3.5781 and the normalized entries are about 0.8445 for parcel and 0.5356 for late. Normalization changes the scale but keeps the ratio between the two entries, about 1.577.

The document frequency of parcel stays 2. Applying a fitted representation never refits the vocabulary or the weights. Even if this message had been a training message, its two copies of parcel would have added only 1 to the document frequency.

  1. Before splitting the data, a colleague fits the vocabulary and the IDF weights on five messages, the four training messages and the test message “train leaves late”. Which weights change, and why does the test evaluation stop being honest even though no label was used?
Show the answer

With n=5n = 5 every weight changes, because the ratio inside the logarithm is now 6/(1+df⁡j)6/(1 + \operatorname{df}_j). The document frequencies of late, leaves and train rise to 2, 2 and 3. The weights become about 1.4055 for arrives and train, 1.6931 for late, leaves, parcel and today, and 2.0986 for early. Even parcel and today, which the test message does not contain, move from 1.5108 to 1.6931. The vocabulary stays the same here only because the test message happens to use known words. A test message with a new word would have created a column that exists only because of the test set.

The test documents have helped shape the features of the model that is then scored on them, and no future message gets that advantage. In a tiny example the effect on accuracy may be small or even zero, but the evaluation no longer measures the procedure that will run on new messages. The fix is to fit the vocabulary and the weights on the training documents only, as the first steps of the pipeline, and to refit them inside each training fold when cross-validating.

  1. Find the unnormalized TF-IDF rows of “train leaves early, parcel arrives late” and “train leaves late, parcel arrives early”. Can any classifier trained on these features tell the two messages apart? What kind of representation could?
Show the answer

Both messages consist of the tokens train, leaves, parcel and arrives together with one early and one late. Both rows therefore have 1.2231 for arrives, 1.9163 for each of early, late and leaves, 1.5108 for each of parcel and train, and 0 for today.

The rows are identical, so every function of the row gives the same output for both messages, whether it is naive Bayes, logistic regression or a neural network. The information about which trip is late was removed by the representation, before any classifier was involved, and a larger model cannot recover it.

A representation that keeps some order could separate them. Bigram counts would, since the first message contains leaves early and arrives late and the second contains leaves late and arrives early. Sequence models that read the tokens in order are another option. Either one is a new candidate model, to be fitted on the training documents and compared on validation data.

How this lesson connects