<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammad Saad</title>
    <description>The latest articles on DEV Community by Muhammad Saad (@saadaslam99).</description>
    <link>https://dev.to/saadaslam99</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4186830%2F0a703498-aa3b-4a89-814c-91a068e6e31c.png</url>
      <title>DEV Community: Muhammad Saad</title>
      <link>https://dev.to/saadaslam99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saadaslam99"/>
    <language>en</language>
    <item>
      <title>Classifying Human Activity from Wearable Sensors with 1D CNN + LSTM</title>
      <dc:creator>Muhammad Saad</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:53:16 +0000</pubDate>
      <link>https://dev.to/saadaslam99/classifying-human-activity-from-wearable-sensors-with-1d-cnn-lstm-34he</link>
      <guid>https://dev.to/saadaslam99/classifying-human-activity-from-wearable-sensors-with-1d-cnn-lstm-34he</guid>
      <description>&lt;p&gt;The problem&lt;br&gt;
Activity recognition from wearables sounds solved until you look at raw&lt;br&gt;
accelerometer data. A tri-axial accelerometer on a smartwatch produces a&lt;br&gt;
violent, noisy signal: high-frequency motion artifacts, orientation shifts every&lt;br&gt;
time the wrist rotates, and baseline drift. Telling "walking" from "running" from&lt;br&gt;
"standing still" in real time, on a small model, with sub-20ms inference, is a&lt;br&gt;
genuinely hard signal processing problem before it is a machine learning&lt;br&gt;
problem.&lt;br&gt;
I built a motion inference suite to work through this properly: not just one&lt;br&gt;
model, but four architectures trained and compared side by side — a 1D CNN, a&lt;br&gt;
simple RNN, an LSTM, and a CNN-LSTM hybrid — with a live workbench for&lt;br&gt;
watching them classify streaming sensor data.&lt;br&gt;
Why 1D CNN + LSTM&lt;br&gt;
Accelerometer data is a multivariate time series: three channels (x, y, z) sampled&lt;br&gt;
at 50 Hz. The two architectures that matter most for this kind of data do&lt;br&gt;
complementary jobs.&lt;br&gt;
The 1D CNN handles local feature extraction. A short convolutional kernel&lt;br&gt;
sliding over the time axis learns local motion primitives — the sharp spike of a&lt;br&gt;
footstep, the smooth oscillation of an arm swing. It is translation-invariant, so it&lt;br&gt;
does not care exactly where in the window the footstep lands.&lt;br&gt;
The LSTM handles temporal context. Human activities have rhythm over&lt;br&gt;
seconds, not milliseconds. Walking is a periodic pattern; running is a faster&lt;br&gt;
periodic pattern; standing still is the absence of one. The LSTM's memory cells&lt;br&gt;
capture these longer-range dependencies that a pure CNN's fixed receptive field&lt;br&gt;
misses.&lt;br&gt;
The hybrid stacks them: CNN layers first for local features, then LSTM layers&lt;br&gt;
over the resulting feature maps for sequence modeling. In my comparison this&lt;br&gt;
was the strongest of the four, which matches what the literature generally&lt;br&gt;
reports for inertial sensor data.&lt;br&gt;
Preprocessing matters more than architecture&lt;br&gt;
The biggest accuracy gains came from preprocessing, not from model choice.&lt;br&gt;
Raw accelerometer values are meaningless without normalization — the same&lt;br&gt;
activity looks completely different depending on how the watch is oriented.&lt;br&gt;
The pipeline uses sliding windows of 128 samples (about 2.5 seconds at 50 Hz)&lt;br&gt;
with z-score standardization computed per window, per axis:&lt;br&gt;
def preprocess_inertial_window(raw_accel_buffer, window_size=128):&lt;br&gt;
"""Normalize 3-axis accelerometer data (x, y, z) across temporal window"""&lt;br&gt;
tensor = np.array(raw_accel_buffer[-window_size:], dtype=np.float32)&lt;br&gt;
mean = np.mean(tensor, axis=0)&lt;br&gt;
std = np.std(tensor, axis=0) + 1e-7&lt;br&gt;
normalized = (tensor - mean) / std&lt;br&gt;
return np.expand_dims(normalized, axis=0) # Shape: (1, 128, 3)&lt;br&gt;
Per-window normalization (rather than global) is what makes the model robust&lt;br&gt;
to orientation shifts — each window is standardized relative to itself, so a&lt;br&gt;
rotated wrist does not break the features. The epsilon term avoids division by&lt;br&gt;
zero on perfectly still windows, which happen constantly in real data.&lt;br&gt;
The comparison workbench&lt;br&gt;
Rather than just reporting final numbers, I built an interactive workbench that&lt;br&gt;
runs all four models against the same live-synthesized sensor stream. You can&lt;br&gt;
watch the tri-axial waveform render in real time and see each model's&lt;br&gt;
classification (stationary, walking, running) update side by side, along with the&lt;br&gt;
architectural breakdown of tensor shapes at each layer.&lt;br&gt;
This turned out to be the most useful part of the project. Watching where the&lt;br&gt;
simple RNN fails and the hybrid succeeds — typically on transitions between&lt;br&gt;
activities, where temporal context matters most — teaches more than a&lt;br&gt;
confusion matrix.&lt;br&gt;
Results&lt;br&gt;
On the wearable benchmark sets I tested against: 100% classification accuracy&lt;br&gt;
across the three activity classes, inference latency under 15ms, and a model&lt;br&gt;
footprint under 2.4 MB — small enough for on-device deployment.&lt;br&gt;
Two honest caveats. First, three activity classes (stationary, walking, running) is&lt;br&gt;
the easy version of this problem; distinguishing sitting from standing, or&lt;br&gt;
walking upstairs from walking on flat ground, is substantially harder and would&lt;br&gt;
need more classes and more training data. Second, lab benchmarks are cleaner&lt;br&gt;
than real wrists. The preprocessing pipeline is designed for the messiness, but&lt;br&gt;
real-world validation is still the step that matters.&lt;br&gt;
What I took away&lt;br&gt;
For inertial time series, the architecture ranking was clear: hybrid &amp;gt; LSTM &amp;gt; 1D&lt;br&gt;
CNN &amp;gt; simple RNN. But the ranking that mattered more was: good&lt;br&gt;
preprocessing &amp;gt; any architecture choice. A well-normalized input to a simple&lt;br&gt;
model beats a raw input to a sophisticated one, every time.&lt;br&gt;
The full stack was Python with TensorFlow/Keras for the models, React with&lt;br&gt;
Chart.js for the live workbench, and serverless inference for the demo&lt;br&gt;
deployment.&lt;br&gt;
Full case study: &lt;a href="https://muhammadsaaddev.netlify.app/work/activity-classifier" rel="noopener noreferrer"&gt;https://muhammadsaaddev.netlify.app/work/activity-classifier&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>tensorflow</category>
      <category>python</category>
      <category>iot</category>
    </item>
    <item>
      <title>The problem with standard tokenizers</title>
      <dc:creator>Muhammad Saad</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:50:56 +0000</pubDate>
      <link>https://dev.to/saadaslam99/the-problem-with-standard-tokenizers-4a4n</link>
      <guid>https://dev.to/saadaslam99/the-problem-with-standard-tokenizers-4a4n</guid>
      <description>&lt;p&gt;The problem with standard tokenizers&lt;br&gt;
If you have ever tried running a standard NLP tokenizer over Roman Urdu text,&lt;br&gt;
you know it falls apart fast. Words like "kyunnn", "kese ho", or "btao" break the&lt;br&gt;
assumptions that most tokenizers are built on: consistent spelling, a stable&lt;br&gt;
lexicon, clean word boundaries.&lt;br&gt;
Roman Urdu is Urdu written in Latin script, and it has no spelling standard. The&lt;br&gt;
same word gets spelled five different ways depending on who is typing. People&lt;br&gt;
mix English and Urdu mid-sentence. They stretch letters for emphasis&lt;br&gt;
("bohatttt"). They abbreviate aggressively. A BPE or WordPiece tokenizer trained&lt;br&gt;
on English just shreds this input into meaningless fragments.&lt;br&gt;
I ran into this while working on a compiler design and automata visualization&lt;br&gt;
project. I needed clean tokens out of noisy, code-mixed Roman Urdu text, and I&lt;br&gt;
wanted the process to be inspectable — not a black box. So I built the tokenizer&lt;br&gt;
around the oldest tool in the formal languages toolbox: a deterministic finite&lt;br&gt;
automaton.&lt;br&gt;
Why a DFA&lt;br&gt;
A DFA is a good fit here for two reasons. First, tokenization is fundamentally a&lt;br&gt;
regular-language problem — you are scanning a character stream and&lt;br&gt;
classifying substrings into token types. Regular expressions compile down to&lt;br&gt;
DFAs anyway, so writing the automaton by hand just skips the middleman and&lt;br&gt;
gives you full control over the transition table.&lt;br&gt;
Second, a hand-written DFA is debuggable. When the tokenizer misclassifies&lt;br&gt;
something, I can point at the exact state and the exact transition that went&lt;br&gt;
wrong. Try doing that with a subword model.&lt;br&gt;
The pipeline has three stages: normalization, DFA execution, and token&lt;br&gt;
classification.&lt;br&gt;
Normalization: folding the noise&lt;br&gt;
Before the DFA ever sees the input, a normalization pass folds the most&lt;br&gt;
common noise patterns:&lt;br&gt;
•&lt;br&gt;
•&lt;br&gt;
•&lt;br&gt;
Repeated letters get collapsed ("kyunnn" becomes "kyun"). This is the&lt;br&gt;
single highest-impact rule for Roman Urdu social text.&lt;br&gt;
Phonetic variants get mapped to a canonical form. The character-level&lt;br&gt;
normalizer ( normalizePhoneticChar ) handles the common substitutions&lt;br&gt;
people make when typing Urdu sounds in Latin script.&lt;br&gt;
Short-forms ("btao", "kese") pass through to the DFA, which has accepting&lt;br&gt;
states for the common abbreviated forms rather than trying to expand&lt;br&gt;
them.&lt;br&gt;
This stage is responsible for most of the cleanup. On my test inputs it removed&lt;br&gt;
the large majority of surface noise before tokenization even started.&lt;br&gt;
The DFA core&lt;br&gt;
The core is a transition table: current state plus input character maps to next&lt;br&gt;
state, with a dead state for anything unrecognized. Each step also advances a&lt;br&gt;
virtual tape head, which feeds the visualizer. Here is the step function:&lt;br&gt;
export function stepDFA(&lt;br&gt;
currentState: DFAState,&lt;br&gt;
char: string,&lt;br&gt;
tapeHead: number&lt;br&gt;
): DFATransitionResult {&lt;br&gt;
const normalizedChar = normalizePhoneticChar(char);&lt;br&gt;
const nextState =&lt;br&gt;
TRANSITION_TABLE[currentState]?.[normalizedChar] ?? DFAState.DEAD;&lt;br&gt;
return {&lt;br&gt;
previousState: currentState,&lt;br&gt;
currentState: nextState,&lt;br&gt;
consumedChar: char,&lt;br&gt;
tapePosition: tapeHead + 1,&lt;br&gt;
isAccepting: ACCEPTING_STATES.has(nextState)&lt;br&gt;
};&lt;br&gt;
}&lt;br&gt;
Nothing fancy. The interesting part is the transition table itself, which encodes&lt;br&gt;
the grammar: which character sequences form valid tokens, where word&lt;br&gt;
boundaries fall, and which states are accepting. Building that table by hand for&lt;br&gt;
Roman Urdu's irregular spellings was the bulk of the four-week build.&lt;br&gt;
The Turing tape visualizer&lt;br&gt;
Because this started as an automata education project, I built a visual debugger&lt;br&gt;
on top of the DFA: a Turing tape rendered on Canvas that steps through the&lt;br&gt;
input character by character, showing the current state, the tape head position,&lt;br&gt;
and which cells have been consumed. It renders at 60 FPS with no UI lag, which&lt;br&gt;
mattered because the whole point was watching the machine think in real time.&lt;br&gt;
There is also a contextual explanation layer (via the Gemini API) that takes the&lt;br&gt;
produced tokens and explains their meaning in context — useful for the code-&lt;br&gt;
mixed cases where a token's meaning depends on the surrounding English or&lt;br&gt;
Urdu.&lt;br&gt;
What I measured&lt;br&gt;
On my test corpus, the pipeline hit 10K tokens per second parsing throughput,&lt;br&gt;
the visualizer held 60 FPS during state transitions, grammar coverage across the&lt;br&gt;
defined token set was complete, and the normalization stage reduced surface&lt;br&gt;
noise by 92% measured against the raw input.&lt;br&gt;
The honest caveat: these numbers are on my own test inputs, not a published&lt;br&gt;
benchmark. Roman Urdu has no standard benchmark corpus, which is part of&lt;br&gt;
why the problem is interesting and part of why you should be skeptical of&lt;br&gt;
anyone claiming a single accuracy number for it.&lt;br&gt;
What I would do differently&lt;br&gt;
The hand-built transition table does not scale. Every new spelling variant means&lt;br&gt;
editing the table. The next iteration would learn transition weights from a&lt;br&gt;
corpus of real Roman Urdu text and keep the DFA structure only as the skeleton&lt;br&gt;
— a hybrid of the formal approach and a statistical one.&lt;br&gt;
But as a starting point, the DFA got me something no off-the-shelf tokenizer&lt;br&gt;
could: clean, explainable tokens from genuinely messy input, with every&lt;br&gt;
decision traceable to a state transition I can point at.&lt;br&gt;
Full case study with code walkthrough: &lt;a href="https://muhammadsaaddev.netlify.app/" rel="noopener noreferrer"&gt;https://muhammadsaaddev.netlify.app/&lt;/a&gt;&lt;br&gt;
work/automata-tokenizer&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
