This repository documents my learning journey in Generative AI. It contains notes, experiments, and hands-on projects as I explore LLMs, AI agents, deep learning, and supporting tools.
gen-ai-training/
│
├── python-fundamentals/ # Core Python concepts (data structures, OOP, regex)
│ ├── data-structure/ # list.ipynb, dict.ipynb
│ ├── oop/ # oop.ipynb
│ └── regex/ # regex.ipynb
│
├── pandas/ # DataFrames, Series, and common operations
│ └── pandas.ipynb
│
├── streamlit/ # Standalone Streamlit demos (not a multi-page app)
│ ├── core.py # DataFrames + line chart
│ ├── widgets.py # Inputs, sliders, file upload
│ └── all_inputs_demo.py # Reference sheet: every input widget in one page
│
├── NLP/ # Natural Language Processing experiments
│ ├── tokenization.ipynb
│ ├── text_processing_stopwords.ipynb
│ ├── text_processing_stemming.ipynb
│ ├── text_processing_lemmatization.ipynb
│ ├── part_of_speech_tag.ipynb
│ ├── named_entity_recognition.ipynb
│ ├── one_hot_encoding/ # One-hot encoding → Keras Tokenizer → Embedding layer
│ ├── bow/ # Bag of Words
│ │ ├── bow_basics.ipynb # Minimal CountVectorizer + ngram_range examples
│ │ ├── bag_of_words.ipynb # Full pipeline on the SMS spam dataset
│ │ └── bow_streamlit.py # Same pipeline as a Streamlit demo
│ ├── n-grams/ # BoW with n-grams
│ │ └── n_grams.ipynb # ngram_range on the SMS spam dataset
│ ├── tf-idf/ # TF-IDF vectorization
│ │ └── tfidf_basics.ipynb # TfidfVectorizer on a 3-sentence corpus
│ └── datasets/ # Shared datasets (smsspamcollection.csv)
│
├── genai/ # LLM API work (OpenAI SDK against a Groq endpoint)
│ ├── openai-api/ # main.py — chat loop with conversation history
│ ├── prompting/ # prompt_engineering_demo.ipynb
│ ├── rag-workflow/ # main.py — pull staff rows from SQLite, build context
│ ├── cag-workflow/ # main.py — cache/context-augmented generation over staff.db
│ ├── staff.db # SQLite sample data for the RAG/CAG scripts
│ └── requirements.txt # openai, python-dotenv, pydantic, httpx
│
├── deep-learning/ # Neural networks, split by framework
│ ├── tensorflow/
│ │ └── ann/ # Artificial neural networks
│ │ └── ann-classification/
│ │ ├── experiments.ipynb # Preprocessing, model definition, training
│ │ ├── prediction.ipynb # Load saved artifacts, predict on one customer
│ │ ├── ann_classification_app.py # Streamlit churn predictor
│ │ ├── Churn_Modelling.csv # Bank customer churn dataset (10k rows)
│ │ ├── requirements.txt # tensorflow, tensorboard, sklearn, streamlit
│ │ ├── gender_label_encoder.pkl # Fitted LabelEncoder (Gender)
│ │ ├── geo_one_hot_encoder.pkl # Fitted OneHotEncoder (Geography)
│ │ ├── scaler.pkl # Fitted StandardScaler
│ │ ├── sequential_model.h5 # Trained model, legacy HDF5 format
│ │ ├── sequential_model.keras # Trained model, native Keras format
│ │ └── logs/fit/ # TensorBoard run logs (gitignored)
│ └── pytorch/
│ ├── basics.ipynb # Tensors, dtypes, ops, device check
│ └── ann/ # Same ANN problem in PyTorch (in progress)
│ └── experiments.ipynb
│
└── requirements.txt # Combined deps for the workspace
Notebook filenames use underscores and say which tier they belong to:
| Tier | Pattern | Example |
|---|---|---|
| Toy example, small enough to verify by hand | <topic>_basics.ipynb |
NLP/tf-idf/tfidf_basics.ipynb |
| Full pipeline on a real dataset | <topic>.ipynb |
NLP/n-grams/n_grams.ipynb |
| Streamlit demo of the same pipeline | <topic>_streamlit.py |
NLP/bow/bow_streamlit.py |
A topic doesn't need every tier — the suffix describes what the notebook actually is. bow/ is the only one with both so far (bow_basics.ipynb and bag_of_words.ipynb); tf-idf/ has only the toy example, so the plain tfidf.ipynb name stays free for a dataset version later.
The topic prefix is kept even though the folder already carries it — it makes editor tabs and fuzzy-find usable once several _basics notebooks exist. Folder names are still mixed (n-grams, tf-idf with hyphens vs one_hot_encoding with an underscore); notebooks are never imported, so this is cosmetic only.
# Create + activate a virtual environment (Windows / bash)
python -m venv .venv
source .venv/Scripts/activate
# Install dependencies
pip install -r requirements.txtgenai/ and deep-learning/tensorflow/ann/ann-classification/ each ship their own requirements.txt. Use those if you'd rather keep a lighter, per-topic environment instead of installing everything at the root.
The root requirements.txt carries both deep-learning stacks — tensorflow / tensorboard and torch / torchinfo — so a single root install covers every notebook here.
The NLP code uses NLTK corpora that aren't bundled with the pip package. Download them once before running any NLP notebook or NLP/bow/bow_streamlit.py:
import nltk
for pkg in [
"stopwords", "punkt", "punkt_tab", "wordnet",
"averaged_perceptron_tagger", "averaged_perceptron_tagger_eng",
"maxent_ne_chunker", "maxent_ne_chunker_tab", "words",
]:
nltk.download(pkg)The scripts under genai/ read credentials from genai/.env (gitignored) via python-dotenv:
GROQ_API_KEY=...
GROQ_BASE_URL=https://api.groq.com/openai/v1
GROQ_MODEL=...
They use the official openai SDK pointed at that base URL, so the same code works against any OpenAI-compatible endpoint.
- Notebooks — open in VS Code or Jupyter and select the
.venvkernel. - Streamlit demos —
streamlit run <file>.py. Working directory matters:NLP/bow/bow_streamlit.pyreads../datasets/smsspamcollection.csvrelative to its own folder, so run it fromNLP/bow/. genai/scripts —python main.pyfrom inside the script's own folder; they resolvestaff.dband.envby relative path.- TensorFlow ANN notebooks — run from
deep-learning/tensorflow/ann/ann-classification/; every path inside them is relative to that folder. Runexperiments.ipynbfirst (it writes the.pklencoders and both model files), thenprediction.ipynb, which loads them. The encoders and models are committed deliberately — a Streamlit deployment builds from the repo, so they have to be present at runtime rather than refitted on the server. - Churn predictor app —
streamlit run deep-learning/tensorflow/ann/ann-classification/ann_classification_app.py. Unlike the NLP demo it resolves its artifacts fromos.path.dirname(__file__), so the working directory doesn't matter. - PyTorch notebooks — no data files or relative paths, so they run from anywhere.
basics.ipynbprintstorch.cuda.is_available()up front; everything in it works on CPU either way.
streamlit/all_inputs_demo.py is a reference sheet rather than an exercise — one page that renders every input widget and echoes its return value, grouped by kind:
- Text & number —
text_input,text_area,number_input - Selection —
selectbox,multiselect,radio,checkbox,toggle,select_slider - Sliders —
sliderfor a single value and for a(low, high)tuple - Date & time —
date_input,time_input - Buttons —
button,download_button,link_button - File & media —
file_uploader,camera_input,audio_input - Other —
color_picker,data_editor(withnum_rows="dynamic"),feedback,pills - Forms —
st.form+form_submit_button, which batch their inputs into a single rerun instead of one per interaction
No dataset or state, so it runs from anywhere: streamlit run streamlit/all_inputs_demo.py.
The NLP notebooks and bow_streamlit.py share the same preprocessing shape:
- Lowercase the text
re.sub("[^a-zA-Z]", " ", ...)to strip non-alphabetic charactersword_tokenize- Drop NLTK English stopwords
- Stem (
PorterStemmer) or lemmatize (WordNetLemmatizer, oftenpos="v") - Re-join words and feed into
CountVectorizer
NLP/one_hot_encoding/one_hot_encoding.ipynb walks the same three toy sentences ("The food is good", "The food is bad", "Pizza is amazing") through four different ways of turning words into numbers, ending at word embeddings. The notebook first prints the expected matrices by hand, then reproduces them in code.
- Tokenize each sentence with
.lower().split() - Flatten to
sorted(set(all_words))— a 7-word vocabulary reshape(-1, 1)the vocabulary, sinceOneHotEncoderexpects 2D input, and fit withsparse_output=Falsefor readable dense output- Encode a sentence by transforming its token column, giving one row per word rather than one row per sentence
That last part is the takeaway: a sentence becomes a (n_words × vocab_size) matrix whose width grows with the vocabulary and which carries no frequency or ordering information — which is exactly what Bag of Words, n-grams, and TF-IDF go on to address.
Same result with labelled columns instead of a bare array: build a pd.Series of every token, call get_dummies, then slice the rows back into per-sentence blocks. It also shows the gotcha — encoding unseen words (["love", "good", "food"]) needs .reindex(columns=dummies.columns, fill_value=0), because get_dummies derives its columns from whatever it's handed and has no memory of a fitted vocabulary.
Despite the name, this hashes each word into [1, vocab_size) rather than one-hot encoding it. The notebook demonstrates the failure directly: "the food is bad" → [7, 2, 2, 1], where food and is collide on index 2. Setting vocab_size=2 makes the collapse total. The problems are that there's no inspectable/reversible word index, uniqueness can't be guaranteed (only made less likely by inflating vocab_size), and the allocation is random hashing rather than anything vocabulary-aware. hash(word) % 30 is run directly to show what's actually happening underneath.
Tokenizer().fit_on_texts(...) gives a real, inspectable word_index ({'is': 1, 'the': 2, 'food': 3, ...}) with no collisions, and texts_to_sequences turns sentences into integer sequences. From there:
vocab_size = len(tokenizer.word_index) + 1— the+1reserves index 0 for paddingpad_sequences(..., padding='pre', maxlen=4)to a uniform length- A one-layer
Sequential([Embedding(vocab_size, 10)])compiled withadam/mse model.predict(padded_docs)to read out each word's 10-dimensional vector
Nothing is trained here — the embeddings are still random initialisations. The point is the shape change: from a sparse vocab_size-wide one-hot row per word to a dense 10-dimensional vector per word, which is the representation everything downstream actually uses.
This notebook imports TensorFlow/Keras for parts 3 and 4, so it needs the root
requirements.txtrather than an NLP-only environment.
NLP/bow/bow_basics.ipynb is the minimal version, kept deliberately separate from the SMS spam pipeline in bag_of_words.ipynb. It uses CountVectorizer(stop_words="english") on two sentences that differ only in a negation and a repeat:
"The food is good" → good: 1
"The food is not good good" → good: 2
Two things it makes visible. First, sklearn.feature_extraction.text.ENGLISH_STOP_WORDS (printed in full) is a different list from NLTK's — and it contains not, so the built-in stop_words="english" silently discards the negation and both sentences reduce to the same word. Second, a clean_data() helper rebuilds the cleaned text by hand alongside the count matrix in a single DataFrame, so the vocabulary, vectorizer.vocabulary_ indices, and counts line up row by row.
The second half of the notebook switches to a five-sentence corpus ("He is a good Boy", "She is a good girl", …) and wraps the vectorizer in with_n_grams(n_grams=(1,1)), so the same corpus can be re-vectorized at different ngram_range values and the DataFrames compared side by side:
ngram_range |
Features |
|---|---|
(1,1) |
unigrams — the default; every feature is a single word |
(1,2) |
unigrams + bigrams |
(1,3) |
unigrams + bigrams + trigrams |
(2,3) |
bigrams + trigrams only — single words are dropped |
(2,4) |
bigrams, trigrams, and 4-grams |
The range is inclusive at both ends, and the numbers count words per feature. The detail worth watching in the output: n-grams are built after stopword removal, so "He is a good Boy" contributes the bigram good boy rather than is a — and because the runs have to be adjacent in the cleaned text, "Boy and girl are good" yields boy girl, not good girl.
NLP/n-grams/n_grams.ipynb then does the same thing on the SMS spam dataset with the NLTK pipeline and max_features, where the vocabulary is large enough that the n-gram columns have to be capped.
bag_of_words.ipynb and bow_streamlit.py run the full NLTK pipeline above on that real dataset.
Binary classification on the bank customer churn dataset — predict Exited from 10 customer attributes. Three pieces: experiments.ipynb trains and saves, prediction.ipynb loads and infers, ann_classification_app.py wraps the same inference in a Streamlit UI.
- Drop identifier columns (
RowNumber,CustomerId,Surname) LabelEncoderonGenderOneHotEncoderonGeography, expanded into columns and concatenated back — 12 features totaltrain_test_split(80/20,random_state=42) intoX/yonExitedStandardScaler—fit_transformon train,transformon test- Pickle the fitted encoders and scaler so inference reuses the exact same transforms
Sequential([
Dense(64, input_shape=(12,), activation="relu"), # Hidden layer 1
Dense(32, activation="relu"), # Hidden layer 2
Dense(1, activation="sigmoid"), # Output — churn probability
])
2,945 trainable parameters. Compiled with adam / binary_crossentropy, tracking accuracy. Trained for up to 100 epochs with two callbacks:
EarlyStopping—monitor="val_loss",patience=10,restore_best_weights=TrueTensorBoard— writes to a timestampedlogs/fit/<YYYYMMDD-HHMMSS>directory
Saved in both formats to compare them: .h5 (legacy HDF5, emits a deprecation warning) and .keras (current recommended).
Loads the three pickles plus both model files, then mirrors the training transforms on a single-row DataFrame: label-encode Gender → one-hot Geography → concat → scaler.transform → predict → threshold at 0.5. Both model formats produce identical output, which is the point of saving both.
The same inference path, driven by widgets instead of a hardcoded row. Worth noting:
- Model and encoders load once behind
@st.cache_resource— they're unhashable objects, so this is the right cache decorator rather than@st.cache_data. - The dropdown options come from the fitted encoders themselves (
geo_one_hot_encoder.categories_[0],gender_label_encoder.classes_), so the UI can't offer a category the model was never trained on. - It shows each stage — raw → encoded → scaled → prediction — so the transform pipeline is visible rather than hidden.
- Only
sequential_model.kerasis loaded here; the.h5copy exists for the notebook comparison.
tensorboard --logdir deep-learning/tensorflow/ann/ann-classification/logs/fitOr inline in the notebook via %load_ext tensorboard and %tensorboard --logdir logs/fit. The logs/ directory is gitignored — it's machine-specific and regenerated on every training run, so a fresh clone has nothing to view until you run experiments.ipynb.
deep-learning/pytorch/ is the second framework track — the same ANN problem approached from the other side, so the Keras and PyTorch versions can be compared directly.
Tensor fundamentals, deliberately including the failure cases:
- Device check —
torch.__version__,torch.cuda.is_available(),torch.cuda.device_count() - Tensors hold numbers, not text —
torch.tensor([["Love", "Me"], ...])is run expecting it to fail, then fixed by mapping words through avocabdict to integer IDs first. Same idea as the one-hot notebook: text has to become numeric before a model sees it. - Shape and dtype —
.shape,.dtype, and constructorstorch.rand,torch.zeros - Operations — element-wise
+and*,@for dot product, plus.sum(),.mean(),.view()(PyTorch's reshape),.T - Derivatives —
f = 3a³ - b²withdf/da = 9a²anddf/db = -2bworked out by hand, ahead of letting autograd compute them
Placeholder — the PyTorch port of the churn classifier hasn't been written yet.
- Strengthen core Python knowledge for AI development
- Build practical projects with Generative AI
- Experiment with LLMs, agents, and AI tools
- Document learning and progress
- Python fundamentals (data structures, OOP, regex)
- Pandas for data analysis
- Streamlit for interactive demos — charts, the full input widget set, forms, caching
- NLP: tokenization, stopwords, stemming, lemmatization, POS tagging, NER, one-hot encoding, Bag of Words, n-grams, TF-IDF
- Text-to-numeric representations:
OneHotEncodervspd.get_dummiesvs Keras hashing (one_hot) vsTokenizer, padding, and dense word embeddings via a KerasEmbeddinglayer - LLM APIs: OpenAI SDK, OpenAI-compatible endpoints, multi-turn conversation history
- Prompt engineering
- RAG and CAG workflows over a SQLite knowledge source
- Deep learning with TensorFlow / Keras: ANN binary classification — feature encoding and scaling, callbacks (early stopping, TensorBoard), model serialization and reload for inference, serving the trained model through Streamlit
- Deep learning with PyTorch: tensor creation, shapes and dtypes, element-wise and matrix operations, reshaping, manual derivatives ahead of autograd