mnist_neural_network.ipynb — training

0%

SURAJ
SAHOO

I ship models, then document where they break.

Self-taught in data science and machine learning. The numbers on this page are the ones that survived testing — not the ones that looked good first.

↗ GitHub surajgoeswithds ↗ LinkedIn suraj-sahoo ↗ Kaggle surajgotnochill ↗ Email surajsahoo20007@gmail.com

Focus

ML engineering · applied AI · cybersecurity

Local time

--:--:-- IST

Status

Open to internships

0

URLs labelled
and cleaned

0

Lexical features
engineered

0

Retrains to kill
data leakage

0

Attack requests
honeypot-contained

Selected work

Four projects worth reading closely

Each leads with the number that matters, and the caveat that comes with it.

0.937
Mean CV recall
5-fold

Safora — phishing URL detection

Role: ML + backend

A Chrome extension that scores URLs for phishing risk in real time. I built the ML side: 11 lexical features extracted from the URL string alone — Shannon entropy, subdirectory depth, abused TLDs, raw IP in place of a domain — feeding a RandomForest classifier served through a Flask API. Trained on ~476K labelled URLs after cleaning.

The interesting part was a failure. The first model hit 1.00 recall, then classified mail.google.com as 99.6% phishing. The legitimate-URL dataset held only bare domains, so the model had quietly learned "URL with a path = phishing." feature_importances_ confirmed it — two features carried over 60% of the decision weight. Five retrains and a dataset swap later, real recall is 0.937 and 9 of 11 hard-case URLs classify correctly.

Feature importances — deployed model

num_subdirs.321
has_https.201
url_length.134
has_ip.115
digits_count.093
entropy.047
num_dots.046
Known limitation

Keyword-density features can't separate a genuine bank login page from a phishing lure using the same vocabulary — secure.chase.com/web/auth/login still scores 0.769. Structural to the feature design, not a training bug.

scikit-learnRandomForestFlaskpandasRender
AES-256
GCM, client-side
PBKDF2 derived

ChatX — end-to-end encrypted chat

Role: backend

A chat app where the server is deliberately incapable of reading messages. I built the Flask + SQLite backend around a single constraint: it stores and forwards ciphertext and holds no decryption logic at all. Encryption happens in the browser through the Web Crypto API before anything is sent, so leaking the source code or the database still reveals nothing.

Four endpoints — send, poll, delete, health. Polling is incremental through a since=messageId cursor rather than refetching history every cycle. All queries parameterised. Every endpoint verified against a live instance on both success and failure paths, not just checked for a clean start.

Scoped tradeoff

Message confidentiality is cryptographically guaranteed; sender attribution is not. senderName is a self-reported label with no auth behind it — anyone holding the room passphrase can post under any name.

FlaskSQLiteGunicornRESTRender
6,682
Chunks embedded
from a 4hr video

RAG teaching assistant

Role: full pipeline

Retrieval-augmented question answering over a ~4-hour video course. faster-whisper transcribes and translates the audio into timestamped segments; each segment becomes a chunk carrying its start/end back to the exact moment in the video. Chunks are embedded with bge-m3 through Ollama, and retrieval runs on manual cosine similarity in NumPy — no vector database, dot product over norms across all 6,682 rows at once. A local Llama 3.2 then answers grounded in the retrieved chunks and cites the timestamp to watch.

Retrieval and generation run fully local through Ollama. The 4-hour audio was split into 20-minute pieces first, because feeding the whole file at once exhausted system RAM and crashed the runtime. Embedding vectors are rounded to 4 decimal places (~49MB vs ~174MB at full precision) with no measurable impact on retrieval quality.

Scoped tradeoff

Chunks are raw transcript segments, so short fragments like "using sql" can outscore denser, more useful ones on a keyword match. A neighbouring-chunk window softens it at retrieval time; the real fix is sentence-level chunking, deferred for the demo build. Generation is also deliberately blended — grounded in the transcript for topic and timestamp, but allowed to supply clean SQL syntax the spoken audio states messily.

faster-whisperOllamabge-m3Llama 3.2NumPyStreamlit
1%
False-positive rate
200-sample test

Ghost Guard — adaptive API defense

Role: ML + backend

A behavior-based API defense system that detects unknown attacks without predefined signatures. I built the entire Python/Flask backend: an Isolation Forest trained exclusively on synthetic normal traffic learns what baseline behavior looks like — request rate, payload size, endpoint pattern, HTTP method. Any request that deviates from that learned baseline gets silently routed to a parallel honeypot layer of fake endpoints (/admin, /admin/users, /login) that return believable but fake data, while the attacker's full action sequence is logged for analysis.

The interesting part was a scoring inconsistency. Legitimate GET /api/products requests were being flagged as anomalous because the training data assumed all requests had ~200-byte payloads — but real GET requests carry zero payload. Fixing the synthetic data to use method-dependent payload distributions dropped false positives to 1%. A second bug surfaced when the router AND-ed two independent ML signals (is_anomalous and anomaly_score) that were calibrated differently, causing silent disagreements — fixed by choosing a single authoritative signal.

Detection pipeline — request scoring

request_rateprimary
payload_sizestrong
endpointencoded
methodbinary
Scoped tradeoff

Detection includes a structural safety-net alongside the ML signal — endpoints outside a known-safe whitelist get a forced minimum score, because pure rate-based ML was inconsistent at extreme values. This is a deliberate hybrid (defense-in-depth), not purely signature-free in the strictest sense.

Isolation Forestscikit-learnFlaskSQLiteGunicornRender

What went wrong

The bugs that taught me the most

A model that scores well and a model that works are different things. From data leakage in phishing detection to a security system that flagged itself as an attack — these are the gaps, and how each one closed.

1.00 → 0.937

Perfect recall from a leaked shortcut

The legitimate-URL training set contained only bare domains, so the classifier learned that any URL with a path was phishing. It scored flawlessly in cross-validation and failed instantly on real traffic. Diagnosed through feature importances, fixed by replacing the dataset — and a third candidate set was rejected first for having the same zero path diversity that caused the problem.

0.40 – 0.50

A UI tier that contradicted the model

The extension's risk tiers and the backend's binary threshold were set independently, opening a band where the interface warned "medium risk" for URLs the model had already called legitimate. Fixed by moving the tier boundary onto the model's own decision threshold so the two can no longer disagree.

0.14 precision

Deleting the leak by deleting the features

One attempted fix removed the two features carrying most of the leaked signal outright. Precision collapsed to 0.14 — those features were genuinely informative, not merely leaky. Reverted, and solved at the data layer instead of the feature layer.

OOM → 12 chunks

A memory fix that fixed nothing

Transcribing a 4-hour file in one call crashed Colab on system RAM. The first patch bolted on VAD filtering and a CPU-thread cap — plausible-sounding flags that left the real cause untouched, and it crashed the same way again. The actual problem was loading the whole file at once, not decoding overhead; splitting the audio into twelve 20-minute pieces solved it. The lesson stuck harder than the bug: understand why something failed before trusting the fix, or you just move the crash.

restated ≠ answered

Retrieval that was right and useless

Vector search correctly found the intro to "selecting data" — a 2-second, six-word fragment. With only that in context, the model could restate the topic but not explain it. The retrieval wasn't wrong; the chunk was too small to be useful. Fixed by expanding each hit into a centred window of neighbouring segments, with explicit boundary clamping so a match near row 0 doesn't wrap around and pull chunks from the end of the video.

59 → 42 score

Training data that didn't match real traffic

Ghost Guard's Isolation Forest was flagging legitimate GET /api/products requests as anomalous (score 59, threshold 45). The synthetic training data assumed all requests had ~200-byte payloads, but real GET requests carry zero payload — a 4-standard-deviation gap the model correctly treated as unusual. Fixed by making payload generation method-dependent: GET payloads ~0, POST ~200. False-positive rate dropped to 1% on a 200-sample retest.

49 ≥ 45 but "normal"

Two ML signals that silently disagreed

The router AND-ed two independent outputs from Isolation Forest — anomaly_score (a manually scaled 0–100 number) and is_anomalous (the model's own internal binary). Score was 49 (above the 45 threshold), but is_anomalous returned False because the model's internal cutoff is calibrated differently. The result: requests that should have been honeypot-routed slipped through as normal. Fixed by using a single authoritative signal — score only.

self → honeypot

A dashboard that flagged itself as an attack

Ghost Guard's own dashboard endpoints (/events, /status, /honeypot-log) were being scored by the middleware and routed to honeypot — because their paths weren't in the known-safe whitelist. The threat level showed "HIGH" with zero real attacks, purely from the dashboard polling itself. Fixed by adding an EXCLUDED_PATHS list in the middleware that skips internal endpoints entirely.

Also built

Foundations

The work that put the stack under my hands.

Stack

What I work with

Listed because I've built something with it, not because I've read about it.

Language + data
PythonpandasNumPySQL / MySQLSQLiteBeautifulSoup
ML + deep learning
scikit-learnRandomForestIsolation Forestanomaly detectionPipelinesColumnTransformerTensorFlowKerasRAGembeddings
Cybersecurity
honeypot designbehavioral detectionAES-256-GCMPBKDF2Web Crypto APIphishing detectionthreat scoring
Serving + deployment
FlaskREST APIsCORSGunicornJoblibOllamaStreamlitRenderVercel
Tooling
GitGitHubColabJupyterfaster-whisperffmpegMatplotlibSeaborn

Contact

Open to internships and collaboration in ML and data.

GET /contact
{
  "email":          "surajsahoo20007@gmail.com",
  "github":         "surajgoeswithds",
  "linkedin":       "suraj-sahoo",
  "kaggle":         "surajgotnochill",
  "open_to":        ["internships", "collaboration", "open source"],
  "timezone":       "Asia/Kolkata" // UTC+5:30
}