Projects


College Projects

JobSMART Recommendation Engine

Goal: A semantic job recommendation engine that returns relevant job postings from 100,000+ LinkedIn listings in under 100 milliseconds.

The Problem: Traditional keyword-based job search fails to understand user intent. A search for "software engineer" might miss "developer" or "programmer" roles, while a vague query like "I want to work with data" yields no results. Users need relevant recommendations based on meaning, not just exact keyword matches, and they need results instantly—not after several seconds of processing.

Methodology: I started with a public dataset of over 100,000 LinkedIn job postings. The raw data was messy—missing company names, null salary values, malformed timestamps, and duplicate entries. I built a Python ETL pipeline using pandas to clean and normalize everything. Missing company names were filled with 'Unknown', salary fields were set to 0.0 for consistency, and timestamps were converted from Unix milliseconds to PostgreSQL-compatible format. I also designed a normalized PostgreSQL schema with four tables—company, job, job_embedding, and posting—to organize the relational data efficiently.

To handle data quality issues gracefully, I implemented per-row transaction rollback. If a single malformed record caused an insertion failure, the transaction rolled back and the cursor reset, but processing continued on the next row. This prevented one bad record from crashing the entire 100,000-row batch.

For semantic search, I needed to convert text into meaning-preserving vectors. I used the all-MiniLM-L6-v2 sentence transformer model, which produces 768-dimensional embeddings. I chose this model because it runs efficiently on CPU—no GPU required—while still capturing semantic relationships well enough for a recommendation engine. Each job description was passed through the model to generate a dense vector representation, and each vector was normalized using L2 normalization so cosine similarity could be computed as a simple dot product for faster query performance. Embedding generation was the bottleneck during ingestion, so I processed rows in batches and committed to the database every 100 rows.

Instead of adding a separate vector database, I used PostgreSQL with the pgvector extension. This allowed me to store both relational data—jobs, companies, postings—and vector embeddings in a single unified database. The job_embedding table stores the 768-dimensional vectors in a dedicated VECTOR column with a 1:1 relationship to each job, enforced by a UNIQUE constraint.

The core performance challenge was that exact cosine similarity search scans all 100,000+ vectors for every query, which took several seconds. My solution was implementing an IVFFlat (Inverted File Flat) index, which partitions the embedding space using k-means clustering. I calculated the optimal number of buckets dynamically using the formula 10 * sqrt(row_count), then floored to the nearest 10 because PostgreSQL requires the lists parameter to be a multiple of 10. At query time, the index identifies only the closest buckets to the query vector and searches within those clusters, dramatically reducing the search space. The index creation is idempotent—it drops any existing index before creating a new one—so I can safely re-run optimization as the dataset grows.

To improve the user experience, I built an LLM-based agent that pre-computes concise summaries for each job description. The agent was prompted to extract the top three key responsibilities, the three most important required skills, and one sentence about why someone would want this job. These summaries are stored in the database and displayed alongside search results, allowing users to quickly glance at multiple recommendations without reading full job descriptions.

When a user enters a query, the same sentence transformer converts it into a normalized 768-dimensional embedding. PostgreSQL's cosine distance operator (<=>) compares the query embedding against all indexed job embeddings, efficiently finding the k nearest neighbors using the IVFFlat index. The system then retrieves the corresponding job details—title, description, summary, salary, company, and location—from the job table and returns the results.

Results: Semantic search successfully retrieves conceptually relevant jobs regardless of exact keyword overlap. A query for "data scientist" returns data analyst, machine learning engineer, and AI researcher roles—positions that keyword search would miss. Average query response time improved from several seconds to under 100 milliseconds after IVFFlat optimization. The pre-computed summaries allow users to evaluate five or more recommendations in under 30 seconds of reading time.

Multimodal Retrieval in Minecraft

Multi-Perspective RAG Text Summarizer

Goal: A RAG pipeline that improves multi-perspective summarization by dynamically retrieving and integrating live web documents to produce more balanced, well-grounded summaries on controversial topics.

The Problem: Traditional multi-perspective summarization systems rely on static, curated corpora that quickly become outdated and limited in perspective diversity, often reinforcing one-sided narratives and failing to capture the full range of arguments available on controversial topics. Meanwhile, naive web retrieval introduces noise and irrelevant content, making it difficult to reliably improve summary quality without careful filtering and structured generation.

Methodology: We built a TF-IDF vectorization pipeline using scikit-learn with lowercase normalization for consistent matching. For each query, computed cosine similarity against a 4,107-document corpus from ThePerspective dataset to retrieve the top-k most relevant offline documents. This established a clean baseline for comparison and allowed us to quantify the marginal value of web augmentation.

We then implemented a robust web retrieval module using the Tavily search API with exponential backoff retry logic (starting at 1 second, doubling up to 32 seconds) to handle rate limiting gracefully. The module caches results locally to avoid redundant API calls during development and experimentation. Each retrieved document is normalized into a consistent schema storing URL, content, title, domain, and source metadata for traceability.

Then we deployed GPT-5-Nano as a binary relevance classifier to filter web documents before summarization. The LLM judge receives the query and each web document, returning a structured JSON mapping of relevance classifications. We validated this approach with human annotation: two independent annotators labeled 600 instances (30 queries × 10 web docs × 2 people) achieving 93% raw inter-rater agreement. This filtering step is critical - it prevents noise from degrading summary quality while preserving novel, relevant perspectives.

We also designed a flexible merger that combines the TF-IDF offline documents with relevance-flagged web documents into a unified corpus for each query. The merger supports configurable weighting and can be easily extended to incorporate additional document sources. This modular design enables future experimentation with different retrieval strategies (e.g., BM25, GritLM) without rewriting downstream components.

To enforce strict JSON schema compliance, we built a constrained generation pipeline using Llama-3.2-3B-Instruct with the Outlines library. The model outputs exactly 2 opposing claims, each with a list of supporting perspectives and associated evidence document IDs. We implemented a retry system with up to 10 regeneration attempts (early exiting after 5 parsing failures) and temperature=0.1 for deterministic, reproducible outputs. The prompt was iteratively refined to balance perspective diversity, factual grounding, and coverage while avoiding penalization of web-derived perspectives that diverge from static gold summaries.

Results: Web-augmented summaries consistently outperformed offline baselines across all k configurations:

Web relevance rates were impressive at lower k values (94-96% at k=5 and k=10) but dropped to 73.8% at k=20, revealing diminishing returns from excessive document retrieval. Notably, 44% of high-context experiments failed due to context window limits (1,500 token generation budget), highlighting the practical trade-off between information richness and model capacity—a finding that aligns with recent "Lost in the Middle" research on LLM context utilization.

Evaluation was comprehensive, using LLM-as-Judge scoring across five dimensions (claim relevance, perspective-claim alignment, perspective distinctness, coverage of core arguments, and factual grounding), each rated 0-2 for a total out of 10. All metrics were calculated with bootstrapped 95% confidence intervals for statistical rigor.

CFR Poker Agent

Goal: A poker agent for two-player Limit Texas Hold'em that learns robust, mixed strategies through offline Counterfactual Regret Minimization (CFR+) and executes them at runtime via a precomputed lookup table for near-instant decision-making.

The Problem: Poker is a fundamentally challenging AI domain because it involves imperfect information, stochastic outcomes, and adversarial opponents—unlike games like chess, where all states are visible. Building a competitive agent requires reasoning about hidden cards, balancing aggressive and conservative play, and avoiding predictable, exploitable patterns. A hard-coded rule-based agent is brittle, while a full game-tree search is computationally infeasible under the tournament's strict per-action time limit (~0.38 seconds). We needed an agent that could learn strategic depth offline and deploy lightning-fast decisions online.

Methodology: We trained our agent using Counterfactual Regret Minimization (CFR+), an iterative self-play algorithm that converges to a Nash equilibrium in imperfect-information, zero-sum games.

To make training tractable, we abstracted the game state into a compact information set defined by four components: betting street (preflop/flop/turn/river), discretized hand strength (5 buckets, from very weak to very strong), discretized pot size (3 buckets), and current raise count (capped at 4).

Over 200,000 training iterations, CFR+ accumulated regret for each action at each information state and updated its policy via regret matching, with CFR+ clipping to stabilize convergence.

The final average strategy was serialized to a JSON lookup table. For comparison, we also implemented a separate Monte Carlo Tree Search (MCTS) agent using time-budgeted determinization rollouts.

Results: The CFR+ agent achieved a 90% win rate against both a random baseline and an aggressive raise-always opponent, demonstrating that the learned strategy meaningfully exploits weak play and robustly handles aggression. In a direct head-to-head matchup against our MCTS agent, CFR+ won 49% of matches (vs. 51% for MCTS) but achieved a positive average profit of +3.6 chips per game—while MCTS posted –3.6—indicating that CFR+ extracts chip value more efficiently and avoids large losses, a key trait of a robust equilibrium strategy.

Stream-ML: Stream-Based Continual Learning

U.S. Food Allergy Prevalence

Pre-College Projects

Here are some of my AI and games projects in Python, C, C++, and Java.

AI:

Games:


Uncover Secret Formula

How does Google Alpha-Go learn to play Go in an hour and beat the best human Go players? I know it is based on discovering hidden patterns, yet its details are beyond my current ability to grasp.

Can I use AI to do something interesting and within my capabilities?

In one of my presentations to the AI Club, I proposed a grading challenge problem, where a teacher would give a pass/fail grade based on a student's class participation, quiz, and final exam grades. Depending on his mode, he may choose to use a simple average, weighted average, or an arbitrary secret formula.

I showed the club that once given enough data, a machine learning tool can uncover the hidden patterns indiscernible to a simple formula. Using the latest machine learning tool TensorFlow 2, I built a neural network like below.



I used Python Pandas and Numpy tools to generate simulated student grades. I then split the data into two parts: one for training my AI model, the other for checking model correctness. I trained the neural network by feeding it with the training data. After five passes, the model achieved an accuracy of 94.1%. Then I tested it on test data; astoundingly, the accuracy was 97.7%. Below is a sample test run. The source code can be downloaded here.

    $python grade_pass.py
Please choose grading method (type 1, 2, or 3): 3
Epoch 1/5
2813/2813 [==============================] - 16s 5ms/step - loss: 0.4431 - accuracy: 0.7865
Epoch 2/5
2813/2813 [==============================] - 14s 5ms/step - loss: 0.2762 - accuracy: 0.8810
Epoch 3/5
2813/2813 [==============================] - 14s 5ms/step - loss: 0.1780 - accuracy: 0.9304
Epoch 4/5
2813/2813 [==============================] - 14s 5ms/step - loss: 0.1395 - accuracy: 0.9375
Epoch 5/5
2813/2813 [==============================] - 14s 5ms/step - loss: 0.1234 - accuracy: 0.9405
313/313 [==============================] - 2s 4ms/step - loss: 0.0732 - accuracy: 0.9770

=== Out of sample test accuracy: 0.9769999980926514 ===

=== Test: Failed Predictions 230, or 2.3% ***

*** samples of failed predictions***
        class  quiz  final    grade1  grade2    grade3     grade  pass  pass_predict_odds  pass_predict
98794   0.46  0.93   0.41  0.600000   0.576  0.585996  0.585996     0           0.810490             1
98819   0.47  0.89   0.44  0.600000   0.581  0.594624  0.594624     0           0.783957             1
98847   0.42  0.97   0.41  0.600000   0.580  0.588508  0.588508     0           0.892313             1
98878   0.47  0.92   0.42  0.603333   0.580  0.591289  0.591289     0           0.839082             1
98917   0.46  0.40   0.95  0.603333   0.687  0.585153  0.585153     0           0.862356             1
98925   0.65  0.44   0.77  0.620000   0.647  0.595652  0.595652     0           0.623241             1
98926   0.44  0.90   0.42  0.586667   0.568  0.579854  0.579854     0           0.582080             1
98969   1.00  0.61   0.40  0.670000   0.583  0.595171  0.595171     0           0.537886             1
98976   0.67  0.80   0.42  0.630000   0.584  0.597724  0.597724     0           0.777348             1
98981   0.42  0.95   0.40  0.590000   0.569  0.577153  0.577153     0           0.735859             1
99028   0.76  0.51   0.62  0.630000   0.615  0.601853  0.601853     1           0.442510             0
99064   0.60  0.82   0.41  0.610000   0.571  0.583862  0.583862     0           0.556755             1
99067   0.52  0.58   0.68  0.593333   0.618  0.606410  0.606410     1           0.436331             0
99082   0.47  0.40   0.91  0.593333   0.669  0.576659  0.576659     0           0.640742             1
99093   0.50  0.63   0.64  0.590000   0.609  0.607984  0.607984     1           0.428010             0
99125   0.65  0.78   0.43  0.620000   0.579  0.593310  0.593310     0           0.620491             1
99135   0.59  0.80   0.43  0.606667   0.573  0.587212  0.587212     0           0.507702             1

We should also note that even the failures are close misses of passing grade criteria of 0.60.

If AI can uncover arbitrary rules in simulated data or man-made games, it should discover physical rules hidden in actual data. Our science and engineering may have reached a phase complicated enough that future discoveries or achievements may depend on AI.


Battleship

This is the traditional Battleship game we played as children. The difference is that I am the designer this time, not merely a player.

Using OOP, this game can be designed using 2D arrays to manipulate player states, allowing players to play against the computer. As my great C++ teacher Mr. Forhan often encourages us to go beyond, I studied the powerful and generic C++ Standard Template Library from this great college C++ textbook: Accelerated C++: Practical Programming by Example 1st Edition by Mike Hendrickson (Author), Andrew Koenig

I was amazed by the professional way of solving complex problems using standard generic algorithms.


Mini-Jeopardy

In my junior year of high school, I took a C++ course taught by Mr. Forhan. For the final project, I developed a Mini-Jeopardy game, mimicking the popular TV game show Jeopardy. A player would answer questions posted by the host in four areas: Math, US State Capitals, US Presidents, and Antonyms.

The game worked well. I realized its methodology could be applied for students to perfect their scores in a chosen field, as long as we provide the questions and answers in a suitable library.

However, it had one weakness: as I emailed Mr. Forhan in discussion, the C++ program's console display is less than ideal for the eyes. I hoped to adapt it to web format.

When developing this website in the summer of 2021, I tried the conversion. The text to HTML conversion is straightforward, but I soon encountered two big technical obstacles:

Is there a way for the server to "push" updates to the players? After considerable research, I found that the solution is to use the latest websocket bi-direction communication methods.

The gist of websocket methodologies is depicted by the figure below:



Since multi-player gaming using websocket is among the most advanced and tricky web technologies, it took me considerable trials to make it work.

The end result is my multiplayer web Mini-Jeopardy Game. You can click to play.