<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Statistically Speaking]]></title><description><![CDATA[Exploring data science, AI, and real-world analytics through clear, insightful stories and hands-on guides. Welcome to Statistically Speaking.]]></description><link>https://statisticallyspeaking.tuhindutta.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1746943159393/a43556da-e26d-48e3-bf35-d91cc74b0a1e.png</url><title>Statistically Speaking</title><link>https://statisticallyspeaking.tuhindutta.com</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 14 Sep 2026 08:16:31 GMT</lastBuildDate><atom:link href="https://statisticallyspeaking.tuhindutta.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Neural Networks From Scratch]]></title><description><![CDATA[Introduction
When we train a neural network using frameworks such as PyTorch or TensorFlow, a large portion of the learning process remains hidden behind abstraction layers. We define a model, specify]]></description><link>https://statisticallyspeaking.tuhindutta.com/neural-networks-from-scratch</link><guid isPermaLink="true">https://statisticallyspeaking.tuhindutta.com/neural-networks-from-scratch</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[DeepLearning]]></category><category><![CDATA[neural networks]]></category><category><![CDATA[Python]]></category><category><![CDATA[numpy]]></category><category><![CDATA[backpropagation neural netowrk]]></category><category><![CDATA[Gradient-Descent ]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Mathematics]]></category><dc:creator><![CDATA[Tuhin Kumar Dutta]]></dc:creator><pubDate>Mon, 22 Jun 2026 17:56:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/681f9f18677f0d877517d5ae/03d3f0ea-8924-44c5-91b7-f87c16e28d3e.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2>
<p>When we train a neural network using frameworks such as PyTorch or TensorFlow, a large portion of the learning process remains hidden behind abstraction layers. We define a model, specify a loss function, call <code>.backward()</code>, and let the framework compute gradients and update parameters.</p>
<p>While this abstraction is incredibly useful in practice, it often leaves an important question unanswered:</p>
<p><strong>What actually happens when a neural network learns?</strong></p>
<p>To answer that question, I implemented a small neural network from scratch using only NumPy. The goal is not to build a production-ready framework. The goal is to expose the mechanics of learning by implementing:</p>
<ul>
<li><p>Forward propagation</p>
</li>
<li><p>Loss computation</p>
</li>
<li><p>Backpropagation</p>
</li>
<li><p>Gradient descent optimization</p>
</li>
</ul>
<p>from first principles.</p>
<p>To keep the focus entirely on learning mechanics, activations, softmax, regularization, and advanced optimizers have been intentionally omitted.</p>
<p>The accompanying handwritten derivations contain the complete mathematical proof of the gradients used throughout this implementation. This article focuses on connecting those mathematical results to working code.</p>
<h3><a href="https://tuhindutta.github.io/neural-network-from-scratch/">GitHub repository link containing the handwritten notes and implementation code.</a></h3>
<hr />
<h2>The Problem Setup</h2>
<p>We will train a neural network to predict house prices from four features:</p>
<ul>
<li><p>House size (square feet)</p>
</li>
<li><p>Number of bedrooms</p>
</li>
<li><p>House age</p>
</li>
<li><p>Distance from city center</p>
</li>
</ul>
<p>The target value is generated using a known linear relationship:</p>
<p>$$y = 600 \cdot size_sqft + 5 \cdot bedrooms + 15 \cdot age_years + 12 \cdot distance + noise$$</p>
<p>The features and targets are standardized before training.</p>
<p>This dataset was intentionally chosen because it follows a linear relationship. The objective is not to solve a difficult machine learning problem but to observe learning behavior clearly.</p>
<hr />
<h2>The Network Architecture</h2>
<img src="https://cdn.hashnode.com/uploads/covers/681f9f18677f0d877517d5ae/7ebc075f-d02b-46ab-89c0-be357bcf3136.png" alt="" style="display:block;margin:0 auto" />

<p>The network consists of three dense layers:</p>
<p>$$X \rightarrow Layer_1 \rightarrow Layer_2 \rightarrow Layer_3 \rightarrow \hat y$$</p>
<p>Each dense layer performs:</p>
<p>$$Z = XW + b$$</p>
<p>where:</p>
<ul>
<li><p>\(\mathit{X}\) is the input matrix</p>
</li>
<li><p>\(\mathit{W}\) is the weight matrix</p>
</li>
<li><p>\(\mathit{b}\) is the bias vector</p>
</li>
<li><p>\(\mathit{Z}\) is the output of the layer</p>
</li>
</ul>
<p>The implementation stores:</p>
<pre><code class="language-python">self.weight
self.bias
</code></pre>
<p>inside each <code>DenseLayer</code>.</p>
<p>During the forward pass:</p>
<pre><code class="language-python">output = np.dot(input_matrix, self.weight) + self.bias
</code></pre>
<p>which directly corresponds to the equation above.</p>
<hr />
<h2>A Deliberate Simplification</h2>
<p>You may notice that the network contains no activation functions.</p>
<p>This is intentional.</p>
<p>Without activations, multiple dense layers collapse into a single linear transformation:</p>
<p>$$XW_1W_2W_3 = XW_{effective}$$</p>
<p>Therefore:</p>
<p>$$\hat y = XW_{effective} + b_{effective}$$</p>
<p>This means the network does <strong>not</strong> gain additional representational power from having multiple layers.</p>
<p>The additional layers exist solely to demonstrate how gradients propagate backward through several stages.</p>
<p>The purpose of this implementation is not model capacity.</p>
<p>The purpose is visibility into the learning process.</p>
<hr />
<h2>Forward Propagation</h2>
<p>The network begins with the following input matrix for every layer:</p>
<p>$$Z_l = X_lW_l + b_l$$</p>
<p>The output of one layer becomes the input of the next.</p>
<p>In code:</p>
<pre><code class="language-python">for key, value in self.layers.items():
    value.forward(output)
    output = value.output
</code></pre>
<p>After the final layer:</p>
<p>$$\hat y = logits$$</p>
<p>The implementation stores this prediction as:</p>
<pre><code class="language-python">self.logits
</code></pre>
<hr />
<h2>Measuring Error Using Mean Squared Error</h2>
<p>Once predictions are produced, they must be compared against the target values.</p>
<p>The error is:</p>
<p>$$error = y - \hat y$$</p>
<p>which is implemented as:</p>
<pre><code class="language-python">error = self.logits - target.reshape(*self.logits.shape).astype(point_precidion)
</code></pre>
<p>The loss is computed as:</p>
<p>$$L = (\hat y - y)^2$$</p>
<p>implemented as:</p>
<pre><code class="language-python">loss = error ** 2
</code></pre>
<p>The network now has a numerical measure of how wrong its predictions are.</p>
<p>Learning begins by asking:</p>
<p><strong>How does this loss change if a weight changes?</strong></p>
<hr />
<h2>The Two Local Derivatives Every Dense Layer Knows</h2>
<p>For a dense layer:</p>
<p>$$Z = XW + b$$</p>
<p>there are only two local derivatives required for backpropagation.</p>
<h3>Local Derivative With Respect to Weights</h3>
<p>$$\frac{\partial Z}{\partial W}=X$$</p>
<p>represented by:</p>
<pre><code class="language-python">get_local_derivative_wrt_weights()
</code></pre>
<p>which returns:</p>
<pre><code class="language-python">self.input_matrix
</code></pre>
<hr />
<h3>Local Derivative With Respect to Inputs</h3>
<p>$$\frac{\partial Z}{\partial X}=W$$</p>
<p>represented by:</p>
<pre><code class="language-python">get_local_derivative_wrt_inputs()
</code></pre>
<p>which returns:</p>
<pre><code class="language-python">self.weight
</code></pre>
<p>These two quantities are sufficient for gradient propagation.</p>
<p>Every dense layer only needs to know:</p>
<ol>
<li><p>How its output changes when weights change.</p>
</li>
<li><p>How its output changes when inputs change.</p>
</li>
</ol>
<p>Everything else comes from the chain rule.</p>
<hr />
<h2>Backpropagation: The Chain Rule in Action</h2>
<p>Backpropagation is often presented as a separate algorithm.</p>
<p>In reality, it is simply repeated application of the chain rule.</p>
<p>The derivative of the loss with respect to the prediction is:</p>
<p>$$\frac{\partial L}{\partial \hat y}= \frac{2(\hat y-y)}{N}$$</p>
<p>implemented as:</p>
<pre><code class="language-python">loss_gradient_wrt_prediction =
(2 * self.error) / self.error.size
</code></pre>
<p>This becomes the initial gradient flowing backward through the network:</p>
<pre><code class="language-python">cumulated_backpropagating_gradient
</code></pre>
<hr />
<h2>Computing Weight Gradients</h2>
<p>For a layer:</p>
<p>$$\frac{\partial L}{\partial W}= \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial W}$$</p>
<p>Using the local derivative:</p>
<p>$$\frac{\partial Z}{\partial W}=X$$</p>
<p>the implementation computes:</p>
<pre><code class="language-python">weight_gradient =
np.dot(
    layer.get_local_derivative_wrt_weights().T,
    cumulated_backpropagating_gradient
)
</code></pre>
<p>This produces the gradient used to update the layer's weights.</p>
<hr />
<h2>Computing Bias Gradients</h2>
<p>Because each neuron receives its bias directly:</p>
<p>$$\frac{\partial Z}{\partial b}=1$$</p>
<p>the bias gradient is obtained by summing the incoming gradient:</p>
<pre><code class="language-python">bias_gradient =
np.sum(
    cumulated_backpropagating_gradient,
    axis=0,
    keepdims=True
)
</code></pre>
<hr />
<h2>Propagating Error to the Previous Layer</h2>
<p>The chain rule also tells us how to continue moving backward.</p>
<p>$$\frac{\partial L}{\partial X}= \frac{\partial L}{\partial Z} \cdot \frac{\partial Z}{\partial X}$$</p>
<p>Since:</p>
<p>$$\frac{\partial Z}{\partial X}=W$$</p>
<p>the implementation becomes:</p>
<pre><code class="language-python">cumulated_backpropagating_gradient =
np.dot(
    cumulated_backpropagating_gradient,
    layer.get_local_derivative_wrt_inputs().T
)
</code></pre>
<p>This is the core mechanism of backpropagation.</p>
<p>The gradient is transformed by each layer's local derivative and passed to the previous layer.</p>
<p>The same process repeats until the first layer is reached.</p>
<hr />
<h2>Gradient Descent Optimization</h2>
<p>Once gradients have been computed, parameters are updated.</p>
<p>For every weight:</p>
<p>$$W_{new}= W- \eta \frac{\partial L}{\partial W}$$</p>
<p>For every bias:</p>
<p>$$b_{new}= b- \eta \frac{\partial L}{\partial b}$$</p>
<p>where:</p>
<p>\(\eta\) is the learning rate.</p>
<p>The implementation performs:</p>
<pre><code class="language-python">new_weight =
weight -
learning_rate * weight_gradient

new_bias =
bias -
learning_rate * bias_gradient
</code></pre>
<p>This step moves the parameters in the direction that reduces loss.</p>
<hr />
<h2>Training Results</h2>
<p>Refer to the training performed <a href="https://github.com/tuhindutta/neural-network-from-scratch/blob/main/train.ipynb">here</a> to see the results.</p>
<p>The loss consistently decreases, indicating that gradient descent is successfully adjusting the parameters.</p>
<p>Comparing predictions and targets shows that the model has learned the underlying relationship in the dataset.</p>
<hr />
<h2>What This Implementation Teaches</h2>
<p>This implementation intentionally avoids many components found in modern neural networks.</p>
<p>There are:</p>
<ul>
<li><p>No activation functions</p>
</li>
<li><p>No softmax</p>
</li>
<li><p>No cross-entropy</p>
</li>
<li><p>No Adam optimizer</p>
</li>
<li><p>No regularization</p>
</li>
</ul>
<p>These omissions are deliberate.</p>
<p>The objective is to isolate and expose the learning process itself.</p>
<p>The most important takeaway is that every dense layer only needs two local derivatives:</p>
<p>$$\frac{\partial Z}{\partial W}$$</p>
<p>and</p>
<p>$$\frac{\partial Z}{\partial X}$$</p>
<p>Once those local derivatives are known, the chain rule handles the rest.</p>
<p>Backpropagation is not a mysterious algorithm hidden inside deep learning frameworks.</p>
<p>It is simply calculus applied repeatedly from the output layer back to the input layer.</p>
<p>Modern frameworks automate these calculations, but the mathematics remains exactly the same.</p>
<p>Understanding this process removes much of the abstraction surrounding neural network training and provides a clearer picture of what is actually happening when a model learns.</p>
]]></content:encoded></item><item><title><![CDATA[What Happens At The End Of A Transformer]]></title><description><![CDATA[This topic is frequently encountered by users when utilizing a LLM API for inference. Grasping this seemingly peripheral concept in depth is essential for designing a LLM-based application.
Let's expl]]></description><link>https://statisticallyspeaking.tuhindutta.com/what-happens-at-the-end-of-a-transformer</link><guid isPermaLink="true">https://statisticallyspeaking.tuhindutta.com/what-happens-at-the-end-of-a-transformer</guid><category><![CDATA[transformers]]></category><category><![CDATA[llm]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[nlp]]></category><category><![CDATA[neural networks]]></category><category><![CDATA[AI Engineering]]></category><category><![CDATA[attention-mechanism]]></category><dc:creator><![CDATA[Tuhin Kumar Dutta]]></dc:creator><pubDate>Thu, 28 May 2026 05:22:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/681f9f18677f0d877517d5ae/41456732-e10d-47f7-9f34-9266f5d6bad6.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This topic is frequently encountered by users when utilizing a LLM API for inference. Grasping this seemingly peripheral concept in depth is essential for designing a LLM-based application.</p>
<p>Let's explore a few concepts with clear, in-depth intuition. I'll aim to keep it jargon-free and conceptually transparent.</p>
<h2>Architecture</h2>
<p>I'll begin slightly before the main point to set the stage for discussion. First, remember that the Transformer block (whether using the encoder-decoder or decoder architecture) always produces a <em><strong>hidden state</strong></em> for each index in the <em><strong>sequence</strong></em> of the <em><strong>batch</strong></em>. Thus, the shape (dimension) of the output is:</p>
<p>$$h_t = (B, S, D)$$</p>
<ul>
<li><p>B = Batch Size, consisting of multiple sequences of inputs processed simultaneously.</p>
</li>
<li><p>S = Sequence Length, the maximum length of tokens the model handles (also sometimes referred to as the context window).</p>
</li>
<li><p>D = Hidden State, a vector representing the semantic knowledge, fingerprint, and meaning of the corresponding token.</p>
</li>
</ul>
<p>To understand this, there is an open-source API from HuggingFace called <a href="https://huggingface.co/docs/transformers/v5.8.1/en/model_doc/auto#transformers.AutoModel">AutoModel</a> that exposes the raw hidden state.</p>
<hr />
<h2>Head (Linear Output projection)</h2>
<p>There are several types of heads positioned over the Transformer blocks to fulfill different purposes. Some common ones include:</p>
<ol>
<li><p>Language Modeling Head (Causal/Masked)</p>
</li>
<li><p>Classification Head</p>
</li>
<li><p>Token-Classification Head</p>
</li>
<li><p>Sequence-to-Sequence Head</p>
</li>
</ol>
<p>Today, we will specifically focus on the <strong>Language Modeling Head</strong>:</p>
<p>For practical purposes, we need the logits, which are the raw, unnormalized output values from the final layer before being converted into probabilities, for all tokens in the vocabulary. A similar concept is implemented in HuggingFace's AutoModel, which provides <a href="https://huggingface.co/docs/transformers/v5.8.1/en/model_doc/auto#transformers.AutoModelForCausalLM">AutoModelForCausalLM</a>. This is essentially the same API, but with a causal language modeling head added.</p>
<h3>Understanding the Head in More Detail</h3>
<p>Let's delve into the head in more detail. I'll outline a few noteworthy points in sequence for better clarity.</p>
<p><strong>Weight Tying:</strong> When the same weights are shared between the input embedding matrix <em>(Embedding Layer)</em> and the output vocabulary projection matrix or linear projection matrix, it is known as <strong>tied-weights</strong>. Conversely, <strong>untied-weights</strong> refer to having separate matrices for these components. The head may use either of the two variants.</p>
<p><strong>The Embedding Layer:</strong> It is a learnable component that encodes the semantic meaning of all tokens in the vocabulary into vectors. It has the following dimensions:</p>
<p>$$E \in \mathbb{R}^{V \times D}$$</p>
<p>Where:</p>
<ul>
<li><p>V: Vocabulary size.</p>
</li>
<li><p>D: Embedding dimension (the size of the vector representing each token).</p>
</li>
</ul>
<hr />
<h3>Working of the head and Token Probabilities</h3>
<p>The following steps occur in sequence:</p>
<ol>
<li><p><strong>Layer Normalization:</strong> It passes the final hidden state through a LayerNorm to stabilize the variance. This layer is in gray area as theoretically this bridges the flow between the core Transformer output and the head and therefore part of neither.</p>
</li>
<li><p><strong>Linear Output Projection:</strong> The normalized hidden state is then multiplied with the transpose of the learned linear projection matrix. Since the hidden state dimension and embedding dimension are of the same order, the multiplication remains mathematically feasible.</p>
</li>
</ol>
<p>$$h_t \times E^T = (B, S, D) \times (D, V) = (B, S, V)$$</p>
<ol>
<li><p><strong>Softmax Layer:</strong> The output from the linear projection layer is converted into a probability distribution using the softmax function:</p>
<p>$$\sigma (z_{i})=\frac{e^{z_{i}}}{\sum {j=1}^{K}e^{z{j}}}$$</p>
<p>Where:</p>
<ul>
<li><p>\(\sigma(z_i)\): The resulting probability of class <code>i</code></p>
</li>
<li><p>\(z_{i}\): The raw score (logit) for class <code>i</code></p>
</li>
<li><p>\(e^{z_{i}}\) and \(e^{z_{j}}\): The standard exponential function applied to the scores</p>
</li>
<li><p><em>K</em>: The total number of classes in the multi-class classification problem</p>
</li>
<li><p>\(\sum_{j=1}^{K} e^{z_j}\): The sum of all exponentiated scores, which acts as the normalizing constant (denominator)</p>
</li>
</ul>
</li>
</ol>
<p>This function transforms the logits into probabilities, where each value represents the likelihood of a token being selected from the vocabulary.</p>
<p>The way softmax is applied depends on whether the model is in the training or inference phase.</p>
<p>During training, the entire input sequence is processed in parallel. Since the model learns to predict every next token position simultaneously, softmax is applied across all sequence positions.</p>
<p>During inference, generation happens autoregressively, meaning one token is generated at a time. Although the model processes the full sequence context available so far, only the logits from the final sequence position are used to predict the next token. Therefore, softmax is applied only to the last position during token generation.</p>
<p>From this point onward, we will refer to the softmax output as (<strong>token) probabilities</strong>.</p>
<hr />
<h2>Token Probabilities</h2>
<p>After the LM head, we obtain the probabilities, which are used for either training or inference.</p>
<p>The key difference (already mentioned in the previous section) is that during training, probabilities for the entire sequence are generated simultaneously for loss computation and back-propagation. In contrast, inference occurs auto-regressively, following these high-level steps, which will be discussed in detail later in a separate article:</p>
<ul>
<li><p>Probabilities are generated, and the probabilities corresponding to the latest token position are used for prediction.</p>
</li>
<li><p>The next token is determined.</p>
</li>
<li><p>The predicted token is appended back to the input sequence for predicting the subsequent token.</p>
</li>
</ul>
<p>This process continues until the "EOS" token is reached or the maximum sequence length is exhausted.</p>
<h3>Below is a visual representation of the entire process.</h3>
<img src="https://cdn.hashnode.com/uploads/covers/681f9f18677f0d877517d5ae/85d1895a-1ca2-4061-96e2-c394dd17cee2.png" alt="" style="display:block;margin:0 auto" />

<h2>Next Agenda</h2>
<p>In the next article, we will delve into how token probabilities culminate in the actual prediction of tokens, with a hint already provided in the visual representation above.</p>
]]></content:encoded></item><item><title><![CDATA[Unveiling the Power of Support Vector Machines in Classification Tasks]]></title><description><![CDATA[Support Vector Machines (SVM) are among the most effective and widely used classification algorithms in machine learning. At its core, the SVM algorithm works by finding the optimal boundary—called a hyperplane—that best separates different classes i...]]></description><link>https://statisticallyspeaking.tuhindutta.com/unveiling-the-power-of-support-vector-machines-in-classification-tasks</link><guid isPermaLink="true">https://statisticallyspeaking.tuhindutta.com/unveiling-the-power-of-support-vector-machines-in-classification-tasks</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[support vector machines]]></category><category><![CDATA[SVM]]></category><category><![CDATA[classification]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[kernel trick]]></category><category><![CDATA[#MLAlgorithms]]></category><category><![CDATA[Python]]></category><category><![CDATA[sklearn]]></category><dc:creator><![CDATA[Tuhin Kumar Dutta]]></dc:creator><pubDate>Sat, 22 Jan 2022 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747245209095/b3111a92-cb2e-4136-a549-8c47d7a05272.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Support Vector Machines (SVM) are among the most effective and widely used classification algorithms in machine learning. At its core, the SVM algorithm works by finding the optimal boundary—called a <em>hyperplane</em>—that best separates different classes in a dataset.</p>
<ul>
<li><p>In the case of <strong>2-dimensional data</strong>, this boundary is a <strong>line</strong>.</p>
</li>
<li><p>For <strong>3-dimensional data</strong>, it becomes a <strong>plane</strong>.</p>
</li>
<li><p>And for <strong>higher dimensions</strong>, it is referred to as a <strong>hyperplane</strong>.</p>
</li>
</ul>
<p>To understand this concept intuitively, let's begin by exploring how SVM behaves with a simple 2D dataset. We'll start by generating an arbitrary dataset for visualization and demonstration purposes.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd, numpy <span class="hljs-keyword">as</span> np, matplotlib.pyplot <span class="hljs-keyword">as</span> plt, seaborn <span class="hljs-keyword">as</span> sns
<span class="hljs-keyword">from</span> sklearn.svm <span class="hljs-keyword">import</span> SVC
<span class="hljs-keyword">import</span> plotly.express <span class="hljs-keyword">as</span> px

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">making_df</span>(<span class="hljs-params">slope_type=<span class="hljs-number">1</span>,n=<span class="hljs-number">100</span>, p1=<span class="hljs-number">100</span>, p2=<span class="hljs-number">30</span>, p3=<span class="hljs-number">2</span></span>):</span>
    np.random.seed(n)

    p=np.random.randn(p1)*p2
    q=np.random.randn(p1)*p2

    <span class="hljs-keyword">if</span> slope_type == <span class="hljs-number">1</span>:        
        r=p+p3*p.max()
        s=q-p3*q.max()
    <span class="hljs-keyword">else</span>:
        r=p+p3*p.max()
        s=q+p3*q.max()        


    df1 = pd.DataFrame({<span class="hljs-string">'ones'</span>:np.ones(len(p)),<span class="hljs-string">'feature1'</span>:p,
                       <span class="hljs-string">'feature2'</span>:q, <span class="hljs-string">'label'</span>:np.ones(len(p))})

    df2 = pd.DataFrame({<span class="hljs-string">'ones'</span>:np.ones(len(r)),<span class="hljs-string">'feature1'</span>:r,
                       <span class="hljs-string">'feature2'</span>:s, <span class="hljs-string">'label'</span>:[<span class="hljs-number">-1</span>]*len(r)})

    df = pd.concat([df1,df2], axis=<span class="hljs-number">0</span>)
    df = df.sample(n=len(df))
    <span class="hljs-keyword">return</span> [df,p,q,r,s]
</code></pre>
<p>To avoid diving too deeply into the implementation details, let’s assume that the above-defined function returns a dataframe consisting of two visually separable clusters of data points. These clusters represent two distinct classes:</p>
<ul>
<li><p><strong>(p, q)</strong> for <em>negative class</em> data points</p>
</li>
<li><p><strong>(r, s)</strong> for <em>positive class</em> data points</p>
</li>
</ul>
<p>The function also accepts a few input parameters, such as the desired <strong>slope direction</strong> (positive or negative) and other customization options for generating the dataset. Since we’ll be generating dataframes multiple times throughout our practical exploration, having a reusable function makes the process more efficient.</p>
<p>Now, to understand how SVM finds a boundary between these classes, we refer to the general equation of a straight line:</p>
<h3 id="heading-axbyc0"><strong><em>ax+by+c=0</em></strong></h3>
<p>Where:</p>
<ul>
<li><p><strong><em>x</em></strong> and <strong><em>y</em></strong> are the coordinate variables</p>
</li>
<li><p><strong><em>a</em></strong>, <strong><em>b</em></strong>, and <strong><em>c</em></strong> are constants</p>
</li>
</ul>
<p>From this, we can derive:</p>
<ul>
<li><p><strong>Slope</strong> of the line: <strong><em>−a/b</em></strong></p>
</li>
<li><p><strong>Y-intercept</strong>: <strong><em>−c/b</em></strong></p>
</li>
</ul>
<p>Without delving further into theory just yet, let’s generate an arbitrary dataframe and plot it. This visual representation will help us clearly see the separation between classes and better understand what we are working with.</p>
<pre><code class="lang-python">obt = making_df(slope_type=<span class="hljs-number">1</span>,n=<span class="hljs-number">40</span>)

df = obt[<span class="hljs-number">0</span>]
p = obt[<span class="hljs-number">1</span>]
q = obt[<span class="hljs-number">2</span>]
r = obt[<span class="hljs-number">3</span>]
s = obt[<span class="hljs-number">4</span>]

plt.figure(figsize=(<span class="hljs-number">20</span>,<span class="hljs-number">8</span>))
plt.scatter(p, q, facecolors=<span class="hljs-string">'purple'</span>, marker=<span class="hljs-string">'_'</span>, s=<span class="hljs-number">100</span>)
plt.scatter(r,s, facecolors=<span class="hljs-string">'red'</span>,marker=<span class="hljs-string">'+'</span>, s=<span class="hljs-number">100</span>)
plt.xlabel(<span class="hljs-string">'Features'</span>, fontsize=<span class="hljs-number">16</span>)
plt.ylabel(<span class="hljs-string">'Label'</span>, fontsize=<span class="hljs-number">16</span>)

plt.show()
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/data2.png?w=1024" alt /></p>
<p>Now we have a clear dataset with distinct data points, conveniently labeled as ‘+1’ and ‘-1’.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">primary_fit</span>(<span class="hljs-params">weight</span>):</span> 
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">nor</span>(<span class="hljs-params">lst</span>):</span>
        factor = (<span class="hljs-number">1</span>/sum([i**<span class="hljs-number">2</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> lst[:<span class="hljs-number">2</span>]]))**<span class="hljs-number">0.5</span>
        <span class="hljs-keyword">return</span> [i*factor <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> lst[:<span class="hljs-number">2</span>]]
    a = nor(weight)[<span class="hljs-number">0</span>]
    b = nor(weight)[<span class="hljs-number">1</span>]
    c = weight[<span class="hljs-number">2</span>]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">st</span>(<span class="hljs-params">a,b,c</span>):</span>
        slope = -(a/b)
        intercept = -(c/b)
        x = np.concatenate([p,r])
        y = slope*x + intercept
        plt.plot(x,y) 
        <span class="hljs-keyword">return</span> [slope,intercept]

    plt.figure(figsize=(<span class="hljs-number">20</span>,<span class="hljs-number">8</span>))
    plt.scatter(p, q, facecolors=<span class="hljs-string">'purple'</span>, marker=<span class="hljs-string">'_'</span>, s=<span class="hljs-number">100</span>)
    plt.scatter(r,s, facecolors=<span class="hljs-string">'red'</span>,marker=<span class="hljs-string">'+'</span>, s=<span class="hljs-number">100</span>)
    plt.xlabel(<span class="hljs-string">'Features'</span>, fontsize=<span class="hljs-number">16</span>)
    plt.ylabel(<span class="hljs-string">'Label'</span>, fontsize=<span class="hljs-number">16</span>)
    st(a,b,c)
    plt.show()


primary_fit([<span class="hljs-number">-1</span>,<span class="hljs-number">1</span>,<span class="hljs-number">110</span>])
</code></pre>
<p>We can try to fit a line using a = -1, b = 1, and c = 110.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/data-1.png?w=1024" alt /></p>
<p>With the selected values of <strong><em>a</em></strong>, <strong><em>b</em></strong>, and <strong><em>c</em></strong>, we were able to fit a line in the form of <strong><em>ax+by+c=0</em></strong></p>
<p>This line successfully separates the data points into two distinct clusters or classes. In the context of Support Vector Machines (SVM), this separating boundary is referred to as a <strong>hyperplane</strong>.</p>
<ul>
<li><p>In <strong>2D</strong>, the hyperplane is simply a line.</p>
</li>
<li><p>In <strong>3D</strong>, it becomes a 2D <strong>plane</strong>.</p>
</li>
<li><p>In higher dimensions (4D, 5D, etc.), the hyperplane could theoretically take forms like a <strong>cube</strong> or even a <strong>tesseract</strong>, although we can't visualize them directly.</p>
</li>
</ul>
<p>From the plotted graph, we can logically infer:</p>
<ul>
<li><p>If <strong><em>ax+by+c &gt;</em> 0</strong>, the point can be classified as belonging to one class (say, <strong>–1</strong>)</p>
</li>
<li><p>If <strong><em>ax+by+c</em> &lt; 0</strong>, the point belongs to the other class (say, <strong>+1</strong>)</p>
</li>
</ul>
<p>This behavior forms the basis for classification using SVM. However, one interesting observation is that different values of aaa, bbb, and ccc can generate <strong>different hyperplanes</strong>—each potentially separating the points in a slightly different manner.</p>
<p>Let’s explore this further by fitting another line using a new set of parameters:</p>
<p><strong>a=−1, b=3, c=80</strong></p>
<p>We’ll visualize how this new line affects the classification boundary and compare it with the previous one.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/data3.png?w=1024" alt /></p>
<p>We observe that with every new combination of values for <strong><em>a</em></strong>, <strong><em>b</em></strong>, and <strong><em>c</em></strong>, an entirely different line is produced. This illustrates a key challenge in Support Vector Machines:</p>
<ol>
<li><p><strong>The first issue</strong> is that the parameters aaa, bbb, and ccc need to be carefully optimized in order to find the <em>best possible separating line</em>—one that generalizes well to unseen data.</p>
</li>
<li><p><strong>The second issue</strong> arises when, during real-world application, new data points appear that are either very close to the decision boundary or even on the opposite side of it. These edge cases can lead to misclassifications and reduce the model’s reliability.</p>
</li>
</ol>
<p>To address both of these concerns, <strong>SVM introduces the concept of margins</strong>.</p>
<h3 id="heading-what-are-margins">What are Margins?</h3>
<p>Simply put, margins are the regions on either side of the classifier (or hyperplane) that define the boundaries of each class. More formally:</p>
<blockquote>
<p>A <strong>margin</strong> is the sum of the perpendicular distances from the classifier to the closest data point in each class.</p>
</blockquote>
<p>The goal of an SVM is not just to find <em>any</em> separating line, but to find the one that maximizes this margin. This optimal boundary is known as the <strong>maximum margin classifier</strong> and is considered to be the most robust in terms of generalization.</p>
<p>In the next section, we’ll visualize these margins and understand how they make SVM one of the most powerful classification algorithms available.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/dis.png?w=656" alt class="image--center mx-auto" /></p>
<h2 id="heading-svm-formulation">🧠 SVM Formulation</h2>
<p>The fundamental goal of a Support Vector Machine (SVM) is to find a <strong>hyperplane</strong> that not only separates the data but does so with the <strong>maximum possible margin</strong>. This margin is the buffer zone that helps ensure better generalization when the model is applied to unseen data.</p>
<p>Mathematically, this objective can be expressed as:</p>
<p>$$l_i\cdot (W \cdot y_i) \geq M$$</p><p>Where:</p>
<ul>
<li><p>\(l_i\) is the label of the ithi^{th}ith training data point (either +1 or -1),</p>
</li>
<li><p>\(y_i\)​ is the feature vector of the ithi^{th}ith point,</p>
</li>
<li><p>\(W\) is the weight vector perpendicular to the hyperplane,</p>
</li>
<li><p>\(M\) is the <strong>margin</strong> we want to maximize.</p>
</li>
</ul>
<h3 id="heading-understanding-the-expression">🧾 Understanding the Expression</h3>
<p>The term \(W \cdot y_i\)​ represents the <strong>projected distance</strong> of the point from the hyperplane. Since both \(l_i\) (the class label) and the projection share the same sign, their product will always be <strong>positive</strong> when the point is on the correct side of the margin.</p>
<p>Thus, the constraint \( l_i\cdot (W \cdot y_i) \geq M\) ensures that all data points lie <strong>outside or on the margin</strong>, reinforcing the separation.</p>
<hr />
<h3 id="heading-distance-from-a-point-to-a-hyperplane">📐 Distance from a Point to a Hyperplane</h3>
<p>For any point PPP, the perpendicular distance ddd from a line (or hyperplane) defined by \(ax+by+c=0\) is given by:</p>
<p>$$d=\frac{|a x + b y + c|}{\sqrt{a^2 + b^2}}$$</p><p>This formula helps quantify how close each point is to the classifier. In the context of SVM, we are particularly interested in the <strong>closest points</strong> from both classes—these are the <strong>support vectors</strong>.</p>
<hr />
<h3 id="heading-goal-of-svm">🎯 Goal of SVM</h3>
<p>The classifier that we are searching for must be one that is <strong>farther from the closest points</strong> (support vectors) of both classes. That is, the SVM algorithm <strong>maximizes the minimum distance</strong> to any data point, ensuring the most robust separation.</p>
<p>This leads to the optimization problem at the heart of SVM:</p>
<p>\(min⁡\frac{1}{2}\|W\|^2\) subject to \(l_i(W \cdot y_i + b)≥1\)</p>
<p>In simple terms, this optimization tries to:</p>
<ul>
<li><p>Minimize the norm of the weight vector (which is equivalent to maximizing margin),</p>
</li>
<li><p>While making sure that all data points are correctly classified and lie outside the margin.</p>
</li>
</ul>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sup</span>(<span class="hljs-params">weight, epsilon=<span class="hljs-number">0</span></span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">nor</span>(<span class="hljs-params">lst</span>):</span>
        factor = (<span class="hljs-number">1</span>/sum([i**<span class="hljs-number">2</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> lst[:<span class="hljs-number">2</span>]]))**<span class="hljs-number">0.5</span>
        <span class="hljs-keyword">return</span> [i*factor <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> lst[:<span class="hljs-number">2</span>]]

    a = nor(weight)[<span class="hljs-number">0</span>]
    b = nor(weight)[<span class="hljs-number">1</span>]
    c = weight[<span class="hljs-number">2</span>]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">st</span>(<span class="hljs-params">a,b,c</span>):</span>
        slope = -(a/b)
        intercept = -(c/b)

        x = np.concatenate([p,r])
        y = slope*x + intercept
        <span class="hljs-keyword">return</span> [x,y]

    df[<span class="hljs-string">'distances'</span>] = np.matmul(np.array(df.iloc[:,:<span class="hljs-number">3</span>]),np.array([c,a,b]))

    df[<span class="hljs-string">'formulation'</span>] = df.label * df.distances

    <span class="hljs-keyword">if</span> -(a/b)&gt;<span class="hljs-number">0</span>:
        df_neg = df[df.label&lt;<span class="hljs-number">0</span>].sort_values(<span class="hljs-string">'formulation'</span>)
        df_pos = df[df.label&gt;<span class="hljs-number">0</span>].sort_values(<span class="hljs-string">'formulation'</span>)
    <span class="hljs-keyword">elif</span> -(a/b)&lt;<span class="hljs-number">0</span>:
        df_neg = df[df.label&lt;<span class="hljs-number">0</span>].sort_values(<span class="hljs-string">'formulation'</span>, ascending=<span class="hljs-literal">False</span>)
        df_pos = df[df.label&gt;<span class="hljs-number">0</span>].sort_values(<span class="hljs-string">'formulation'</span>, ascending=<span class="hljs-literal">False</span>)



    neg_dist = df_neg.formulation.values[<span class="hljs-number">0</span>]
    pos_dist = df_pos.formulation.values[<span class="hljs-number">0</span>]


    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">vert_dist</span>(<span class="hljs-params">distance</span>):</span>
        <span class="hljs-keyword">import</span> math
        slope = -(a/b)
        angle = abs(math.degrees(math.atan(slope)))
        vertical = distance/math.cos(math.radians(angle))
        <span class="hljs-keyword">return</span> vertical

    neg_vert_dist = vert_dist(neg_dist)
    pos_vert_dist = vert_dist(pos_dist)

    intercept = -(c/b)
    pos_intercept = intercept + pos_vert_dist
    neg_intercept = intercept - neg_vert_dist
    c_intercept = min([neg_intercept, pos_intercept]) + abs((neg_intercept - pos_intercept)/<span class="hljs-number">2</span>)

    c_pos = -(pos_intercept*b)
    c_neg = -(neg_intercept*b)
    c_new = -(c_intercept*b)

    <span class="hljs-keyword">if</span> epsilon != <span class="hljs-number">0</span>:

        margin = (neg_dist+pos_dist)*(<span class="hljs-number">1</span>-epsilon)
        distance = margin/<span class="hljs-number">2</span>
        dist_vert_dist = vert_dist(distance)

        pos_intercept = c_intercept + dist_vert_dist
        neg_intercept = c_intercept - dist_vert_dist
        c_pos = -(pos_intercept*b)
        c_neg = -(neg_intercept*b) 


    plt.figure(figsize=(<span class="hljs-number">20</span>,<span class="hljs-number">8</span>))
    plt.scatter(p, q, facecolors=<span class="hljs-string">'purple'</span>, marker=<span class="hljs-string">'_'</span>, s=<span class="hljs-number">100</span>)
    plt.scatter(r,s, facecolors=<span class="hljs-string">'red'</span>, marker=<span class="hljs-string">'+'</span>, s=<span class="hljs-number">100</span>)
    plt.xlabel(<span class="hljs-string">'Features'</span>, fontsize=<span class="hljs-number">16</span>)
    plt.ylabel(<span class="hljs-string">'Label'</span>, fontsize=<span class="hljs-number">16</span>)

    plt.plot(st(a,b,c_new)[<span class="hljs-number">0</span>], st(a,b,c_new)[<span class="hljs-number">1</span>], linewidth=<span class="hljs-number">3.5</span>, label=<span class="hljs-string">'classifier'</span>)
    plt.plot(st(a,b,c_pos)[<span class="hljs-number">0</span>], st(a,b,c_pos)[<span class="hljs-number">1</span>], <span class="hljs-string">'--'</span>, linewidth=<span class="hljs-number">1</span>, alpha=<span class="hljs-number">0.8</span>, label=<span class="hljs-string">'negative margin'</span>)
    plt.plot(st(a,b,c_neg)[<span class="hljs-number">0</span>], st(a,b,c_neg)[<span class="hljs-number">1</span>], <span class="hljs-string">'--'</span>, linewidth=<span class="hljs-number">1</span>, alpha=<span class="hljs-number">0.8</span>, label=<span class="hljs-string">'positive margin'</span>)
    plt.legend()
    plt.show()


weights = [<span class="hljs-number">-1</span>,<span class="hljs-number">1</span>,<span class="hljs-number">110</span>]
sup(weights)
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Note:</strong> The above function is solely intended for visualization and demonstration purposes to aid conceptual understanding. It does not represent the actual model-building process in practice.</div>
</div>

<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/pro.png?w=1024" alt /></p>
<p>The negative and positive margin lines pass through the nearest negative and positive data points to the classifier, respectively. However, there can be instances where data points lie very close to the classifier. In such scenarios, the model may attempt to overfit the data by shrinking the margin, as it strives to achieve the ideal condition of zero classification errors. Let’s visualize how this behavior manifests.</p>
<pre><code class="lang-python">sup(weights, overfit=<span class="hljs-literal">True</span>)
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/overfit.png?w=1024" alt /></p>
<p>While this overfitted model may perform exceptionally well during training, it suffers a significant drawback. Due to the narrow margin—i.e., a highly constrained decision boundary—the model is more likely to misclassify data during evaluation or testing. Let’s understand this visually. Suppose we receive a new data point that truly belongs to the positive class but falls just above the negative margin. The model, relying on the tight boundary, would incorrectly classify it as ‘-1’. This misclassification leads to a drop in overall accuracy.</p>
<p>To address this issue, Support Vector Machines introduce a <strong>slack variable (ϵ)</strong> into the formulation. This allows the model to tolerate some degree of misclassification, thereby increasing its generalization ability.</p>
<p>The updated SVM constraint becomes:</p>
<p>$$l_i\cdot (W \cdot y_i) \geq M(1 - \epsilon_i)$$</p><p>From this formulation, we can clearly infer that the margin is inversely proportional to the slack variable. The slack variable <strong>ϵᵢ</strong> represents the degree of allowable error for each data point. In other words, it measures how much a data point is permitted to violate the margin.</p>
<p>A higher value of ϵ indicates more flexibility, meaning the model is allowed to misclassify certain points, thus preventing overfitting. Let’s visualize the impact of this by setting ϵ = 0.6 and examining how the decision boundary adapts.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/sof.png?w=1024" alt /></p>
<p>Now, the model becomes <strong>generalized</strong> by allowing a few training data points to cross the margin boundaries. These relaxed thresholds are known as <strong>soft margins</strong>, which define the extent to which the model can tolerate errors in the training set without significantly compromising its generalization capabilities.</p>
<p>By incorporating this flexibility using the slack variable ε\varepsilonε, the model avoids overfitting and becomes more robust to unseen data. Let’s now revisit the same scenario illustrated previously, where the overfitted model misclassified a positive point due to overly narrow margins, and observe how the introduction of soft margins improves the outcome.</p>
<pre><code class="lang-python">sup(weights, epsilon=<span class="hljs-number">0.6</span>, test_pos_pos=<span class="hljs-number">48</span>)
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/conseq.png?w=1024" alt /></p>
<p>Unlike the earlier overfitted model, this generalized version will no longer misclassify the data point (represented by the red dot), as it now falls within the permissible margin. This demonstrates how the introduction of soft margins helps the model tolerate minor violations and improves overall robustness.</p>
<p>I hope this demonstration has provided a clear understanding of the foundational concepts behind Support Vector Machines. Now, let’s explore an additional insight: as the value of ε\varepsilonε increases, the soft margin becomes narrower. Conversely, when <em>ε=0</em>, the soft margins coincide exactly with the classifier, as the condition becomes \(l_i(W⋅yi​)≥0\). Let’s visualize how the fit changes with different values of <em>ε</em>.</p>
<pre><code class="lang-python"><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> np.arange(<span class="hljs-number">0.2</span>,<span class="hljs-number">1.1</span>,<span class="hljs-number">0.2</span>):
    sup(weights, epsilon=i, show=<span class="hljs-literal">True</span>)
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/1.png?w=1024" alt /></p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/1.png?w=1024" alt /></p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/3.png?w=1024" alt /></p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/3.png?w=1024" alt /></p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/5.png?w=1024" alt /></p>
<p>The greater the value of <em>ε</em>, the simpler the model becomes. A larger <em>ε</em> allows the model to be more flexible, reducing its tendency to fit the classifier too aggressively and thereby avoiding overfitting.</p>
<p>However, all these formulations assume that the classes are linearly separable—an ideal condition that rarely holds true in real-world scenarios. To better reflect practical situations, let’s now construct another dataset that introduces some overlap between classes, mimicking the complexity of real-world data.</p>
<pre><code class="lang-python">x = np.linspace(<span class="hljs-number">-5.0</span>, <span class="hljs-number">2.0</span>, <span class="hljs-number">100</span>)
y = np.sqrt(<span class="hljs-number">10</span>**<span class="hljs-number">2</span> + x**<span class="hljs-number">2</span>)
y=np.hstack([y,-y])
x=np.hstack([x,-x])

x1 = np.linspace(<span class="hljs-number">-5.0</span>, <span class="hljs-number">2.0</span>, <span class="hljs-number">100</span>)
y1 = np.sqrt(<span class="hljs-number">5</span>**<span class="hljs-number">2</span> - x1**<span class="hljs-number">2</span>)
y1=np.hstack([y1,-y1])
x1=np.hstack([x1,-x1])

df1 =pd.DataFrame(np.vstack([y,x]).T,columns=[<span class="hljs-string">'X1'</span>,<span class="hljs-string">'X2'</span>])
df1[<span class="hljs-string">'Y'</span>]=<span class="hljs-number">0</span>
df2 =pd.DataFrame(np.vstack([y1,x1]).T,columns=[<span class="hljs-string">'X1'</span>,<span class="hljs-string">'X2'</span>])
df2[<span class="hljs-string">'Y'</span>]=<span class="hljs-number">1</span>
df = df1.append(df2)

df.head()
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/tab.png?w=181" alt /></p>
<p>Let's take a look at the graphical representation of the data.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/gr.png?w=370" alt /></p>
<p>As we can see, these data points are not linearly separable. To handle such cases, the concept of <strong>Kernels</strong> was introduced. Kernels are mathematical functions that, when applied to the data points from different features, transform them into a higher-dimensional space, allowing us to better classify the data. Essentially, a kernel function allows us to find a decision boundary in a transformed feature space, even when the original space is not linearly separable.</p>
<p>There are several types of kernels, each of which performs a different transformation. While the mathematical expressions for these kernels vary, their main goal is the same: to map the data points into a higher-dimensional space where they become linearly separable.</p>
<p>For simplicity, let’s consider one of the most commonly used kernels: <strong>Polynomial Kernel</strong>.</p>
<p>$$K(x,y)=(x^Ty+c)^d$$</p><p>Above mentioned is the mathematical expression for the Polynomial kernel. Let’s now calculate the transformation and observe the new dimensions formulated by the kernel. In our dataset, \(x\) and \(y\) are represented by\(X_1\) and\(X_2\) respectively.</p>
<p>When applying the Polynomial kernel function, we are essentially mapping the input features (from a lower-dimensional space) into a higher-dimensional space. The transformation process allows us to achieve linear separability in situations where the original data is not linearly separable.</p>
<p>We can compute the transformed feature space for our dataset by applying the kernel to pairs of data points, where each pair is processed through the following:</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/mt.png?w=1024" alt /></p>
<p>From the above derivation, we obtained three new dimensions, namely:</p>
<ol>
<li><p>\(X_1^2\)</p>
</li>
<li><p>\(X_2^2\)</p>
</li>
<li><p>\(X_1 \cdot X_2\)</p>
</li>
</ol>
<p>Let's calculate these new dimensions and integrate them into the original dataframe.</p>
<pre><code class="lang-python">df[<span class="hljs-string">'X1_Square'</span>]= df[<span class="hljs-string">'X1'</span>]**<span class="hljs-number">2</span>
df[<span class="hljs-string">'X2_Square'</span>]= df[<span class="hljs-string">'X2'</span>]**<span class="hljs-number">2</span>
df[<span class="hljs-string">'X1X2'</span>] = (df[<span class="hljs-string">'X1'</span>] *df[<span class="hljs-string">'X2'</span>])
df.head()
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/inc-1.png?w=401" alt /></p>
<p>Now, let’s plot these 3 newly formed dimensions in a 3D graph.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Creating dataset</span>
df1 = df[df.Y==<span class="hljs-number">0</span>]
a = np.array(df1.X1_Square)
b = np.array(df1.X2_Square)
c = np.array(df1.X1X2)

<span class="hljs-comment"># Creating figure</span>
plt.figure(figsize = (<span class="hljs-number">13</span>,<span class="hljs-number">13</span>))
ax = plt.axes(projection =<span class="hljs-string">"3d"</span>)

<span class="hljs-comment"># Creating plot</span>
ax.scatter3D(a, b, c, s=<span class="hljs-number">8</span>, label=<span class="hljs-string">'class 0'</span>)

<span class="hljs-comment"># Creating dataset</span>
df1 = df[df.Y==<span class="hljs-number">1</span>]
a = np.array(df1.X1_Square)
b = np.array(df1.X2_Square)
c = np.array(df1.X1X2)

<span class="hljs-comment"># Creating plot</span>
ax.scatter3D(a, b, c, s=<span class="hljs-number">8</span>, label=<span class="hljs-string">'class 1'</span>)

ax. set_xlabel(<span class="hljs-string">'X1_Square'</span>, fontsize=<span class="hljs-number">13</span>)
ax. set_ylabel(<span class="hljs-string">'X2_Square'</span>, fontsize=<span class="hljs-number">13</span>)
ax. set_zlabel(<span class="hljs-string">'X1X2'</span>, fontsize=<span class="hljs-number">13</span>)
plt.legend(fontsize=<span class="hljs-number">12</span>)

ax.view_init(<span class="hljs-number">30</span>,<span class="hljs-number">250</span>)

<span class="hljs-comment"># show plot</span>
plt.show()
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/08/plt-1.png?w=721" alt /></p>
<p>After applying the kernel, we can easily imagine fitting a 2D plane between the two classes, using all the theoretical concepts discussed earlier. Unlike before kernel execution, where the data was not linearly separable, now there exists a hyperplane that can successfully classify both the classes efficiently. This becomes possible only due to the introduction of additional dimensions over the initial 2D Cartesian plane, allowing the model to find a suitable separating boundary in the transformed feature space.</p>
<p>I hope this explanation provided a clear understanding of the Support Vector Machine (SVM) classifier algorithm and the internal process it follows. Although the practical implementation using automated processes with libraries (such as <code>scikit-learn</code>) has not been discussed here, I can assure you that once the working of the algorithm is clear, model building becomes pretty straightforward.</p>
<p>For further details and practical implementation, you can refer to the scikit-learn SVC documentation.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>For more insights, projects, and articles, visit my portfolio at </em><a target="_blank" class="autolinkedURL autolinkedURL-url" href="http://www.tuhindutta.com/"><em>tuhindutta.com</em></a><em>.</em></div>
</div>]]></content:encoded></item><item><title><![CDATA[Boosting Machine Learning Adaboost Guide]]></title><description><![CDATA[Boosting is one of the most widely used classes of algorithms in machine learning, applied globally to tackle a variety of complex problems. It involves combining multiple weak learners—typically simple models that perform just slightly better than r...]]></description><link>https://statisticallyspeaking.tuhindutta.com/boosting-machine-learning-adaboost-guide</link><guid isPermaLink="true">https://statisticallyspeaking.tuhindutta.com/boosting-machine-learning-adaboost-guide</guid><category><![CDATA[ML Algorithms]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[boosting]]></category><category><![CDATA[#MachineLearning #AdaBoost #EnsembleLearning #AI #Python #DataScience #Boosting #TechInnovation #MLAlgorithms #Hashnode #AIModels #TechBlog #DataAnalytics]]></category><category><![CDATA[#MLAlgorithms]]></category><category><![CDATA[Python]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[ensemblelearning]]></category><category><![CDATA[adaboost]]></category><dc:creator><![CDATA[Tuhin Kumar Dutta]]></dc:creator><pubDate>Wed, 17 Nov 2021 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746973654279/d34434a2-f98d-4825-ab07-291cb37bf043.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Boosting</strong> is one of the most widely used classes of algorithms in machine learning, applied globally to tackle a variety of complex problems. It involves combining multiple <strong>weak learners</strong>—typically simple models that perform just slightly better than random guessing—to collectively form a strong predictive model. Each learner is trained sequentially, with a focus on correcting the mistakes made by its predecessors. Misclassified or hard-to-learn data points are given more importance in subsequent rounds. In this blog, we’ll explore one of the simplest and most well-known boosting algorithms, <strong>AdaBoost</strong> (Adaptive Boosting). We'll also implement it from scratch and demonstrate how it can significantly improve model accuracy.</p>
<p>Before diving into AdaBoost, let’s first understand the core principle of boosting through the following illustration.</p>
<p><a target="_blank" href="https://criticalmind.tech.blog/wp-content/uploads/2021/09/9ca3e-1m2uhkzwwj0kfqyl5tbfnsq.png"><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/boost.png?w=544" alt /></a></p>
<p>In the illustration above, Boxes 1, 2, and 3 represent classifications made by individual models—D1, D2, and D3—each of which is a weak learner and performs poorly when used alone. However, when these models are combined, as shown in Box 4, they work together to make highly accurate predictions.</p>
<p>This is the core idea behind <strong>AdaBoost</strong> as well. In AdaBoost, each model’s prediction is weighted, and these weights are updated during training based on whether the model classifies each instance correctly or not. Incorrectly classified instances are given higher weights, making them more influential in the next iteration.</p>
<p>We’ll walk through each step of the AdaBoost algorithm and its implementation. For practical understanding, we’ll use the <strong>Breast Cancer</strong> dataset from the <code>sklearn</code> library. Let's start by loading and visualizing the dataset.</p>
<pre><code class="lang-python">df = pd.DataFrame(load_breast_cancer().data, columns=load_breast_cancer().feature_names)
df[<span class="hljs-string">'label'</span>] = load_breast_cancer().target

copy = df.copy()

df.head()
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/da1.png?w=951" alt /></p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/da2.png?w=399" alt /></p>
<p>Since the dataset contains a large number of features, we’ll primarily focus on the <code>label</code> column along with the new columns that will be appended next to it during the boosting process.</p>
<p>To begin, let’s use a <strong>Decision Tree</strong> as our initial weak learner to make a first-round prediction and observe the accuracy achieved.</p>
<pre><code class="lang-python">dt = DecisionTreeClassifier(random_state=<span class="hljs-number">50</span>, min_samples_split=<span class="hljs-number">100</span>)
dt.fit(df.iloc[:,:<span class="hljs-number">-1</span>],df.iloc[:,<span class="hljs-number">-1</span>])
df[<span class="hljs-string">'prediction'</span>] = dt.predict(df.iloc[:,:<span class="hljs-number">-1</span>])
df.iloc[:<span class="hljs-number">5</span>,<span class="hljs-number">-2</span>:]
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/pr-1.png?w=139" alt /></p>
<pre><code class="lang-bash">Accuracy = 0.945518453427065
We are getting an accuracy of around 95%.
</code></pre>
<p>Let’s now apply AdaBoost and see whether it improves the model's accuracy.</p>
<h2 id="heading-step-1">Step 1</h2>
<p>Assign an initial weight of 1\m to each data point, where mmm is the total number of data points in the dataset.</p>
<pre><code class="lang-python">df[<span class="hljs-string">'weight'</span>] = <span class="hljs-number">1</span>/len(df)
df.iloc[:<span class="hljs-number">5</span>,<span class="hljs-number">-3</span>:]
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/wei.png?w=199" alt /></p>
<h2 id="heading-step-2">Step 2</h2>
<p>Calculate the number of incorrectly classified data points.</p>
<pre><code class="lang-python">
no_of_errors = len(df[df.label != df.prediction1])
no_of_errors
</code></pre>
<p><strong>There are 31 incorrectly classified data points.</strong></p>
<p>Next, let's calculate the total error, where the total error is given by:</p>
<p><strong>total error = no_of_errors/total number of data points</strong></p>
<pre><code class="lang-python">total_errors = no_of_errors/len(df)
total_errors
</code></pre>
<p>The total error is calculated to be <strong>0.0545</strong>.</p>
<h3 id="heading-step-3">Step 3</h3>
<p>Calculating the amount of say (α)</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/say.png?w=976" alt /></p>
<pre><code class="lang-python">alpha = <span class="hljs-number">0.5</span> * np.log((<span class="hljs-number">1</span>-total_errors)/total_errors)
alpha
</code></pre>
<p>We get 1.423 as the value for the amount of say.</p>
<h3 id="heading-step-4">Step 4</h3>
<p><strong>The weight update is performed using the following rules:</strong></p>
<ul>
<li><p>For a correct prediction:</p>
<p>  <strong>updated weight = old weight X e<sup>–α</sup></strong></p>
</li>
<li><p>For an incorrect prediction:</p>
<p>  <strong>updated weight = old weight X e<sup>α</sup></strong></p>
</li>
</ul>
<pre><code class="lang-python">df[<span class="hljs-string">'weight_updated'</span>] = df.loc[df.label != df.prediction].weight * np.exp(alpha)
df.weight_updated = df[<span class="hljs-string">'weight_updated'</span>].fillna(df[df.label == df.prediction].weight * np.exp(-alpha))
df.iloc[:<span class="hljs-number">5</span>,<span class="hljs-number">-4</span>:]
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/upda.png?w=300" alt /></p>
<p>The updated weights are then normalized, ensuring that the sum of all the weights in the column equals 1.</p>
<pre><code class="lang-python">df.weight_updated = df.weight_updated/df.weight_updated.sum()
df.iloc[:<span class="hljs-number">5</span>,<span class="hljs-number">-4</span>:]
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/nor.png?w=304" alt /></p>
<p>Now, we can observe that in the updated weights column, the values for the incorrectly classified data points are significantly higher compared to those for the correctly classified ones.</p>
<h2 id="heading-step-5">Step 5</h2>
<p>Next, ranges are created for each updated weight, which represent the cumulative sum of the values in the column. For example, the range for index 0 is '0 to 0.016129', the range for index 1 is '0.016123 to (0.016129 + 0.000929)', the range for index 2 is '(0.016129 + 0.000929) to ((0.016129 + 0.000929) + 0.000929)', and so on.</p>
<p>In this manner, the range values for the last index will sum up to 1, as the weights have been normalized, ensuring their total is equal to 1.</p>
<h2 id="heading-step-6">Step 6</h2>
<p>Resampling of data is performed by selecting a random number between 0 and 1. The range in which this number falls determines which index from the 'df' dataframe is included in the new resampled dataframe. Since the weights of the incorrectly predicted data points are higher, the corresponding ranges will also be larger. As a result, many of the randomly chosen numbers will fall within the ranges of the incorrectly predicted data points. Consequently, these data points will be repeated more frequently in the resampled dataframe, giving them more priority in subsequent iterations.</p>
<pre><code class="lang-python">resampled = pd.DataFrame(columns=df.columns[:<span class="hljs-number">31</span>])
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(df)):
    index = df[df.ranges == df[np.random.rand()&lt;df.ranges].ranges.min()].index
    resampled.loc[i] = list(df.iloc[index,:<span class="hljs-number">31</span>].values[<span class="hljs-number">0</span>])

resampled.head()
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/res.png?w=944" alt /></p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/res2.png?w=394" alt /></p>
<p>The resampled data is then processed in the same way as in step 1. These 6 steps are repeated iteratively until the total error becomes zero, or the number of iterations reaches infinity. To automate this process, let's build a function that accumulates all the above steps to perform the iterations.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">adaboost</span>(<span class="hljs-params">df</span>):</span>
    dt = DecisionTreeClassifier(random_state=<span class="hljs-number">50</span>, min_samples_split=<span class="hljs-number">100</span>)
    dt.fit(df.iloc[:,:<span class="hljs-number">30</span>],df.iloc[:,<span class="hljs-number">30</span>])
    df[<span class="hljs-string">'prediction'</span>] = dt.predict(df.iloc[:,:<span class="hljs-number">30</span>])

    df[<span class="hljs-string">'weight'</span>] = <span class="hljs-number">1</span>/len(df)

    no_of_errors = len(df[df.label != df.prediction])

    total_errors = no_of_errors/len(df)

    alpha = <span class="hljs-number">0.5</span> * np.log((<span class="hljs-number">1</span>-total_errors)/total_errors)

    df[<span class="hljs-string">'weight_updated'</span>] = df.loc[df.label != df.prediction].weight * np.exp(alpha)
    df.weight_updated = df[<span class="hljs-string">'weight_updated'</span>].fillna(df[df.label == df.prediction].weight * np.exp(-alpha))

    df.weight_updated = df.weight_updated/df.weight_updated.sum()

    p = <span class="hljs-number">0</span>
    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(df)):
        df.loc[i,<span class="hljs-string">'ranges'</span>] = df.loc[i,<span class="hljs-string">'weight_updated'</span>] + p
        p = df.loc[i,<span class="hljs-string">'ranges'</span>]

    resampled = pd.DataFrame(columns=df.columns[:<span class="hljs-number">31</span>])
    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(df)):
        index = df[df.ranges == df[np.random.rand()&lt;df.ranges].ranges.min()].index
        resampled.loc[i] = list(df.iloc[index,:<span class="hljs-number">31</span>].values[<span class="hljs-number">0</span>])  

    df = resampled

    <span class="hljs-keyword">return</span> [df, dt]
</code></pre>
<p>The above function returns the resampled DataFrame and the trained model from each iteration. Upon execution, it stores the final resampled DataFrame along with the list of trained models across all iterations.</p>
<pre><code class="lang-python">df = copy.copy()

models = []    

<span class="hljs-keyword">try</span>:
    <span class="hljs-keyword">for</span> iter <span class="hljs-keyword">in</span> range(<span class="hljs-number">20</span>):        
        ada = adaboost(df)
        df = ada[<span class="hljs-number">0</span>]    
        models.append(ada[<span class="hljs-number">1</span>])
        print(<span class="hljs-string">'Decision stamp {0}'</span>.format(iter+<span class="hljs-number">1</span>))

<span class="hljs-keyword">except</span> Exception:
    <span class="hljs-keyword">pass</span>
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/dec-1.png?w=154" alt /></p>
<p>We have obtained 10 decision stumps (weak learners), which will now be used collectively to make future predictions. These models will be applied to the same dataset on which we initially observed an accuracy of 95%. Let's aggregate the outputs from all these models to evaluate the performance of the boosted ensemble.</p>
<pre><code class="lang-python">pred = np.zeros(len(df))
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(models)):    
    pred += models[i].predict(copy.iloc[:,:<span class="hljs-number">-1</span>])

pred
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/out-1.png?w=550" alt /></p>
<p>These values represent the aggregated predictions from all the models. Since each model outputs either a 0 or 1, a value like 2 in the array indicates that 2 models classified the instance as class 1, while a value of 6 means 6 out of the 10 models predicted class 1 for that particular instance.</p>
<p>Based on the number of models used, a threshold is set at half that number. If the aggregate output for any data point exceeds this threshold, it is classified as 1; otherwise, it is classified as 0. In our case, since we used 10 models, any output value greater than 5 is considered class 1, and the rest are classified as class 0.</p>
<pre><code class="lang-python">threshold = len(models)/<span class="hljs-number">2</span>
vec = np.vectorize(<span class="hljs-keyword">lambda</span> x: <span class="hljs-number">1</span> <span class="hljs-keyword">if</span> x&gt;threshold <span class="hljs-keyword">else</span> <span class="hljs-number">0</span>)
final_prediction = vec(pred)
final_prediction
</code></pre>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/out2.png?w=669" alt /></p>
<p>Now, using the output above, we calculate the accuracy.</p>
<pre><code class="lang-python">copy[<span class="hljs-string">'final_prediction'</span>] = final_prediction

print(<span class="hljs-string">'Accuracy ='</span>,accuracy_score(copy.label, copy.final_prediction))
</code></pre>
<pre><code class="lang-bash">Accuracy = 0.9753954305799648
This time we obtained an accuracy of 98%.
</code></pre>
<p>Thus, we can see that we have successfully improved the accuracy using Boosting.</p>
<p><em>Point to note:</em> This entire process is purely for demonstration and conceptual understanding. It is <strong>not recommended</strong> to use this manual implementation for solving real-world problems. For practical purposes, you should use the automated and optimized <code>AdaBoostClassifier</code> provided by the <code>sklearn</code> library.</p>
<p>I hope I was able to explain the algorithm clearly enough for you to understand and experiment with.<br />Your valuable feedback is always appreciated!</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>For more insights, projects, and articles, visit my portfolio at </em><a target="_new" href="https://www.tuhindutta.com"><em>www.tuhindutta.com</em></a><em>.</em></div>
</div>]]></content:encoded></item><item><title><![CDATA[Understanding the Sigmoid Function and Its Applications in Machine Learning]]></title><description><![CDATA[Let’s start with the basics—what exactly is the sigmoid function?
The sigmoid function is a mathematical function commonly used in machine learning, especially in binary classification problems and neural networks. It’s defined by the formula:

When ...]]></description><link>https://statisticallyspeaking.tuhindutta.com/understanding-sigmoid-function-applications-machine-learning</link><guid isPermaLink="true">https://statisticallyspeaking.tuhindutta.com/understanding-sigmoid-function-applications-machine-learning</guid><category><![CDATA[sigmoid function]]></category><category><![CDATA[logistic regression]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[binary classification]]></category><category><![CDATA[Statistical Methods]]></category><dc:creator><![CDATA[Tuhin Kumar Dutta]]></dc:creator><pubDate>Sat, 11 Sep 2021 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746950828123/9f496a26-8dd3-40f2-808d-6bf02ba87f32.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let’s start with the basics—what exactly is the sigmoid function?</p>
<p>The sigmoid function is a mathematical function commonly used in machine learning, especially in binary classification problems and neural networks. It’s defined by the formula:</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/sig.png?w=292" alt class="image--center mx-auto" /></p>
<p>When plotted on a graph, the sigmoid function creates a smooth, S-shaped curve that asymptotically approaches 0 and 1 at the extremes.</p>
<p><a target="_blank" href="https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Logistic-curve.svg/1200px-Logistic-curve.svg.png"><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/sig2.png?w=1024" alt /></a></p>
<p>In the realm of machine learning, the sigmoid function proves to be incredibly useful—particularly in classification tasks where outcomes are binary, with one class represented as 0 and the other as 1.</p>
<p>A prominent example of such an application is <strong>Logistic Regression</strong>, a fundamental algorithm used for binary classification. In this context, the sigmoid function is employed to convert the model's linear output into a probability between 0 and 1, allowing us to interpret the result as the likelihood of belonging to a particular class.</p>
<p>To understand this more concretely, let’s walk through a simple example using an arbitrary dataset with one independent (input) variable and one dependent (output) variable. This will help illustrate the role sigmoid plays in logistic regression.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/df-1.png?w=108" alt /></p>
<p>Let’s plot the data to gain a visual understanding.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/pla.png?w=974" alt /></p>
<p>We clearly can't fit a straight line through the data points, as there is no apparent linear relationship.<br />However, for illustrative purposes, let’s assume a weight and intercept to fit an arbitrary linear model.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/arb.png?w=975" alt /></p>
<p>It’s evident that the linear model doesn’t help much in this case.<br />To address this, we apply the sigmoid function to transform the linear equation—essentially replacing xxx in the sigmoid expression with the linear combination of features (as defined in our earlier equation).</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/scr.png?w=560" alt /></p>
<p>Now, let’s plot the transformed values against the independent variables to visualize the relationship.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/fit-1.png?w=972" alt /></p>
<p>From the graph above, we observe that the straight line has been transformed into an S-shaped sigmoid curve, with values ranging between 0 and 1.<br />Now, each data point can be projected onto the sigmoid curve, and we can define a threshold above which the predicted class is 1.<br />For this example, let’s set the threshold at 0.23.</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/thr.png?w=977" alt /></p>
<p>For any new data, after applying the sigmoid transformation, if the resulting value exceeds the threshold (0.23), we classify it as belonging to class 1.<br />Note that, in the <strong>Logistic Regression</strong> model from the <strong>scikit-learn</strong> library, the default threshold is 0.5. Any data point with a sigmoid output greater than 0.5 is classified as class 1.</p>
<p>To fit the best sigmoid curve, we need to choose the optimal values for the weights and intercept. This process is similar to linear regression, where gradient descent is used to minimize the loss function. For logistic regression, the loss function is referred to as <strong>Log Loss</strong>, which is defined by the following expression:</p>
<p><img src="https://criticalmind.tech.blog/wp-content/uploads/2021/09/log.png?w=1024" alt /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>For more insights, projects, and articles, visit my portfolio at </em><a target="_new" href="https://www.tuhindutta.com"><em>www.tuhindutta.com</em></a><em>.</em></div>
</div>]]></content:encoded></item></channel></rss>