{"id":473,"date":"2026-06-05T00:00:00","date_gmt":"2026-06-04T23:00:00","guid":{"rendered":"https:\/\/kosokoking.com\/?p=473"},"modified":"2026-05-24T17:49:54","modified_gmt":"2026-05-24T16:49:54","slug":"feature-extraction","status":"publish","type":"post","link":"https:\/\/kosokoking.com\/index.php\/technology\/feature-extraction\/","title":{"rendered":"Feature extraction"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The preprocessing pipeline from the previous entry stripped punctuation, removed stop words, stemmed tokens, and rejoined the result into clean strings. The classifier still cannot read any of it. Machine learning models operate on numbers, not words, which means every message must be converted into a numerical vector before training can begin. This conversion step, feature extraction, defines the exact mathematical surface the classifier will learn from, and the exact mathematical surface an attacker can manipulate.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A spam message reading &#8220;free prize now&#8221; and a ham message reading &#8220;meeting at noon&#8221; will each become a row of integers in a matrix, where each column corresponds to a term from the dataset&#8217;s vocabulary. The classifier will learn to separate these rows. But the vocabulary itself, the set of terms the model is even allowed to see, is built from choices made during feature extraction. Those choices determine what signals survive and what blind spots exist. From a red teaming perspective, every blind spot is an evasion path.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The bag-of-words model<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The most straightforward way to represent text numerically is the bag-of-words model. It works by constructing a vocabulary of every unique term in the dataset, then representing each message as a vector of term counts. Each position in the vector corresponds to one term, and the value at that position records how many times the term appears in the message.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The name is literal. The model treats each document as a bag of words, discarding all information about word order. &#8220;Claim your free prize&#8221; and &#8220;prize your free claim&#8221; produce identical vectors. For general text understanding, this is a significant loss. For spam classification, it is often acceptable because the presence of certain terms matters more than their arrangement. The word &#8220;free&#8221; appearing in a message is a stronger spam signal than the specific sentence structure surrounding it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To recover some ordering information, the model can include bigrams, which are pairs of consecutive words. A unigram vocabulary might contain &#8220;free&#8221; and &#8220;prize&#8221; as separate features. A bigram vocabulary adds &#8220;free prize&#8221; as a distinct feature, capturing the fact that these two words appeared next to each other. The bigram &#8220;free prize&#8221; is a stronger spam indicator than either word alone, because it reflects a phrase pattern characteristic of promotional spam rather than a coincidental co-occurrence.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, bigrams only capture local adjacency. The global structure of the sentence, its syntax, its rhetorical flow, its intent, is still lost. The bag-of-words model, even with bigrams, is a lossy compression of language into frequency counts.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">CountVectorizer in practice<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Scikit-learn&#8217;s&nbsp;<code>CountVectorizer<\/code>&nbsp;implements the bag-of-words approach in three stages. First, it tokenises each message into individual terms and bigrams based on the specified n-gram range. Second, it builds a vocabulary by filtering terms according to frequency thresholds. Third, it transforms each message into a vector of term counts using that vocabulary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The filtering thresholds are where the security-relevant decisions happen.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from sklearn.feature_extraction.text import CountVectorizer\n\n# Initialise CountVectorizer with bigrams and frequency thresholds\nvectorizer = CountVectorizer(min_df=1, max_df=0.9, ngram_range=(1, 2))\n\n# Fit and transform the message column\nX = vectorizer.fit_transform(df&#91;\"message\"])\n\n# Convert labels to binary\ny = df&#91;\"label\"].apply(lambda x: 1 if x == \"spam\" else 0)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Three parameters control what enters the vocabulary:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>min_df=1<\/code>\u00a0means a term must appear in at least one document to be included. Setting this to 1 keeps every term, including rare ones that might appear in only a single message.<\/li>\n\n\n\n<li><code>max_df=0.9<\/code>\u00a0removes any term appearing in more than 90% of documents. These are words so common across both spam and ham that they provide no discriminative value.<\/li>\n\n\n\n<li><code>ngram_range=(1, 2)<\/code>\u00a0includes both unigrams (individual words) and bigrams (consecutive word pairs), giving the model access to limited phrase-level patterns.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">After this step,&nbsp;<code>X<\/code>&nbsp;is a sparse matrix where each row is a message and each column is a vocabulary term. The values are raw counts. This matrix is what the classifier trains on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Walking through the vocabulary construction<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Consider five short documents to see how the vocabulary is built and how term filtering works:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>&#8220;The free prize is waiting for you&#8221;<\/li>\n\n\n\n<li>&#8220;The spam message offers a free prize now&#8221;<\/li>\n\n\n\n<li>&#8220;The spam filter might detect this&#8221;<\/li>\n\n\n\n<li>&#8220;The important news says you won a free trip&#8221;<\/li>\n\n\n\n<li>&#8220;The message truly is important&#8221;<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">With&nbsp;<code>max_df=0.9<\/code>, any term appearing in more than 90% of documents is removed. With five documents, the threshold is 4.5, so a term must appear in all five to be excluded. &#8220;The&#8221; appears in all five and gets dropped. Every other unigram survives because none exceeds the threshold.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The resulting unigram matrix records simple presence and frequency:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Document<\/th><th>free<\/th><th>prize<\/th><th>is<\/th><th>waiting<\/th><th>for<\/th><th>you<\/th><th>spam<\/th><th>message<\/th><th>offers<\/th><th>a<\/th><th>now<\/th><th>filter<\/th><th>might<\/th><th>detect<\/th><th>this<\/th><th>important<\/th><th>news<\/th><th>says<\/th><th>won<\/th><th>trip<\/th><th>truly<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><\/tr><tr><td>2<\/td><td>1<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><\/tr><tr><td>3<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><\/tr><tr><td>4<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>0<\/td><\/tr><tr><td>5<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">With&nbsp;<code>ngram_range=(1, 2)<\/code>, the vocabulary expands to include bigrams. &#8220;Free prize&#8221; now appears as its own feature in Documents 1 and 2, separate from the individual unigrams &#8220;free&#8221; and &#8220;prize&#8221;. The bigram &#8220;spam filter&#8221; captures a phrase pattern absent from individual word counts, and &#8220;is important&#8221; links two words that individually carry weak signal but together indicate a specific type of ham message.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Document<\/th><th>free<\/th><th>prize<\/th><th>&#8230;<\/th><th>free prize<\/th><th>prize is<\/th><th>is waiting<\/th><th>spam message<\/th><th>spam filter<\/th><th>is important<\/th><th>&#8230;<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>1<\/td><td>1<\/td><td>&#8230;<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>&#8230;<\/td><\/tr><tr><td>2<\/td><td>1<\/td><td>1<\/td><td>&#8230;<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>0<\/td><td>0<\/td><td>&#8230;<\/td><\/tr><tr><td>3<\/td><td>0<\/td><td>0<\/td><td>&#8230;<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>0<\/td><td>&#8230;<\/td><\/tr><tr><td>4<\/td><td>1<\/td><td>0<\/td><td>&#8230;<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>&#8230;<\/td><\/tr><tr><td>5<\/td><td>0<\/td><td>0<\/td><td>&#8230;<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>0<\/td><td>1<\/td><td>&#8230;<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The full bigram matrix is considerably wider than the unigram-only version. Every pair of consecutive words that survives the frequency filters becomes a new column. In a real dataset with thousands of messages, this expansion can produce vocabularies of tens or hundreds of thousands of features, most of which are zero for any given message. The resulting matrix is extremely sparse.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The feature space as an attack surface<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Everything described so far is standard machine learning practice. From a red teaming perspective, the interesting question is what this feature space looks like to an attacker.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The bag-of-words model creates a specific and well-studied vulnerability known as the <a href=\"https:\/\/ix.cs.uoregon.edu\/~lowd\/kdd05lowd.pdf\" title=\"\">good word attack<\/a>, first formalised by Lowd and Meek in 2005. The attack is straightforward. Because the classifier treats each message as a bag of independent term counts, an attacker can shift a spam message&#8217;s feature vector toward the ham region of the feature space by injecting words that are strongly associated with legitimate messages. The classifier sees the counts, not the coherence. A message reading &#8220;FREE CASH NOW meeting agenda quarterly review budget&#8221; is nonsensical to a human reader, but to a bag-of-words classifier, it looks like a message that shares vocabulary with both spam and ham, and the injected ham words may be enough to tip the classification.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This attack works precisely because the bag-of-words representation discards word order. The classifier has no way to know that &#8220;meeting agenda quarterly review budget&#8221; was appended to the end of a spam message rather than being part of a genuine business communication. The features for those words activate identically regardless of context.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The frequency thresholds introduce their own attack surface. Setting&nbsp;<code>min_df=1<\/code>&nbsp;means every term in the training corpus is included, even rare ones. If a specific rare word appears only in ham messages during training, it becomes a reliable ham signal that an attacker can co-opt. Conversely,&nbsp;<code>max_df=0.9<\/code>&nbsp;removes universally common terms, which means the model is blind to any manipulation that operates purely through high-frequency vocabulary. An attacker who understands which words were filtered out during vocabulary construction knows exactly which words they can use without triggering any feature at all.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bigrams add discriminative power, but they also add predictable structure. If the classifier has learned that &#8220;free prize&#8221; is a strong spam bigram, an attacker can break the bigram by inserting a word between &#8220;free&#8221; and &#8220;prize&#8221;, because &#8220;free your prize&#8221; produces the bigrams &#8220;free your&#8221; and &#8220;your prize&#8221; instead of &#8220;free prize&#8221;. The spam signal vanishes while the individual unigrams remain, and the unigrams alone may not carry enough weight to trigger classification.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The sparse matrix problem<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The feature matrices produced by CountVectorizer are overwhelmingly sparse. In a typical SMS dataset, a message might activate 10 to 30 features out of a vocabulary of 50,000 or more. That means over 99.9% of each message&#8217;s feature vector is zeros.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This sparsity has a direct security implication. An attacker does not need to manipulate many features to shift a message&#8217;s classification. In a high-dimensional sparse space, small changes to the non-zero entries, or strategic additions of new non-zero entries, can move a data point a significant distance in feature space relative to the decision boundary. Research by <a href=\"https:\/\/arxiv.org\/abs\/2005.12154\" title=\"\">Zhang, Chan, Biggio, Yeung, and Roli<\/a> has shown that classifiers with unevenly distributed feature weights are particularly vulnerable because a single high-weight feature can dominate the classification, and flipping that feature&#8217;s presence changes the outcome. Their work also demonstrated that adversary-aware feature selection, where the expected attacker manipulation strategy is incorporated into the feature selection criterion, measurably improves robustness.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The sparsity also means that the model&#8217;s effective decision is often based on a handful of features per message. For a red teamer, this is an invitation. If you can identify which features carry the most weight for a given classifier, you know exactly which terms to add or remove to flip the result. The feature extraction step has compressed each message into a numerical fingerprint, and that fingerprint can be reverse-engineered.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What the feature matrix encodes, and what it does not<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After feature extraction, the variable&nbsp;<code>X<\/code>&nbsp;contains the numerical matrix that the classifier will train on, and&nbsp;<code>y<\/code>&nbsp;contains the binary labels. Every subsequent step in the pipeline, model selection, training, evaluation, operates on this representation. The model will never see the original text. It will only see term counts in the dimensions defined by the vocabulary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This means that every assumption baked into the feature extraction step propagates forward. The choice to discard word order, the frequency thresholds that define the vocabulary boundaries, the decision to include bigrams but not trigrams, all of these become fixed properties of the classifier&#8217;s worldview. The model cannot learn patterns that the feature representation has already destroyed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From an adversarial perspective, the preprocessing pipeline (covered in the previous entry) and the feature extraction step (covered here) together define the complete transformation chain from raw text to model input. An attacker who understands both stages can craft messages that survive preprocessing intact and land in the exact region of the feature space that the classifier associates with legitimate messages. The model is only as robust as the assumptions embedded in its feature representation, and every assumption is a potential exploit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The next entry will train the Naive Bayes classifier on this feature matrix and examine what the model actually learns from these term counts, including how the learned decision boundary responds when an adversary starts pushing against it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>How extraction builds the feature space a spam classifier learns from, and why every vocabulary decision creates an evasion path for a red teamer to find.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[668,630,759,758,51,757,136,751,738,754],"class_list":["post-473","post","type-post","status-publish","format-standard","hentry","category-technology","tag-adversarial-machine-learning","tag-ai-red-teaming","tag-bag-of-words","tag-countvectorizer","tag-cybersecurity","tag-feature-extraction","tag-machine-learning","tag-natural-language-processing","tag-scikit-learn","tag-spam-classification"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/posts\/473","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/comments?post=473"}],"version-history":[{"count":1,"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/posts\/473\/revisions"}],"predecessor-version":[{"id":474,"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/posts\/473\/revisions\/474"}],"wp:attachment":[{"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/media?parent=473"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/categories?post=473"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kosokoking.com\/index.php\/wp-json\/wp\/v2\/tags?post=473"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}