<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Projects on Michael Dunn</title><link>https://mdunn99.com/categories/projects/</link><description>Recent content in Projects on Michael Dunn</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 03 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://mdunn99.com/categories/projects/index.xml" rel="self" type="application/rss+xml"/><item><title>Exploring Inversion Attacks While Building A CTF Challenge</title><link>https://mdunn99.com/posts/exploring-an-ai-attack-vector-while-building-a-ctf-challenge/</link><pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate><guid>https://mdunn99.com/posts/exploring-an-ai-attack-vector-while-building-a-ctf-challenge/</guid><description>&lt;p&gt;I created my first CTF (&lt;a href="https://en.wikipedia.org/wiki/Capture_the_flag_(cybersecurity)"&gt;Capture The Flag&lt;/a&gt;) challenge for a certain CTF event (kept discrete since it will be used in next year&amp;rsquo;s iteration). In this post, I&amp;rsquo;ll be talking about what that challenge is at a high-level, some of the more technical aspects, my inspiration for it, and the challenges I encountered while developing it.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="inspiration"&gt;Inspiration&lt;/h1&gt;
&lt;p&gt;I recently watched &lt;a href="https://www.youtube.com/watch?v=O7BI4jfEFwA"&gt;Patrick Walsh&amp;rsquo;s lecture from DEF CON 33&lt;/a&gt; that single-handedly inspired this project, where, being already familiar with some other ideas Walsh expressed very elegantly in his talk (prompt engineering and the like), really enjoyed his demo on &lt;strong&gt;inversion attacks&lt;/strong&gt; showcased with the &lt;a href="https://github.com/vec2text/vec2text"&gt;vec2text&lt;/a&gt; tool.&lt;/p&gt;
&lt;p&gt;SentinelOne defines &lt;a href="https://www.sentinelone.com/cybersecurity-101/cybersecurity/model-inversion-attacks/"&gt;Model Inversion Attacks&lt;/a&gt; as &amp;ldquo;reverse-engineering machine learning models to extract sensitive information about their training data, exploiting model outputs and confidence scores through iterative queries.&amp;rdquo; By &amp;ldquo;model outputs,&amp;rdquo; SentinalOne is specifically talking about &lt;a href="https://www.ibm.com/think/topics/vector-embedding"&gt;embedding matrices&lt;/a&gt;, which are the unintelligible numbers that some machine learning models (&lt;a href="https://www.seangoedecke.com/how-llms-work/"&gt;like LLMs&lt;/a&gt;) use to perform calculations and produce a final output. However, the idea of embedding documents into &amp;ldquo;vectors&amp;rdquo; has been around for &lt;a href="https://en.wikipedia.org/wiki/Word_embedding"&gt;a while now&lt;/a&gt;, and besides just reverse-engineering not-very-accessible vector embeddings from frontier language models, the specific embedding inversion attack illustrated in Walsh&amp;rsquo;s talk (&lt;a href="https://www.reddit.com/r/cybersecurity/comments/1s9ybbu/embedding_inversion_attacks_make_hosted_vector/"&gt;and seemingly more talked about as of recent&lt;/a&gt;) is targeted more-so at RAG (Retrieval Augmented Generation) systems. A document gets embedded, stored in a vector DB, and retrieved by similarity search to feed context back into an LLM. If that vector DB is exposed, so are the documents it was supposed to protect.&lt;/p&gt;
&lt;p&gt;Such an attack can be visualized below. In this instance, a vulnerable database stores vector embeddings. This can be very valuable to an attacker who can perform inversion attacks.&lt;/p&gt;
&lt;p&gt;&lt;img src="inversion_attack_diagram.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;DEF CON 33 - Exploiting Shadow Data from AI Models and Embeddings. &lt;a href="https://www.youtube.com/watch?v=O7BI4jfEFwA"&gt;Source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Given that I was actively searching for an opportunity to develop a challenge for this CTF event around the time I saw the video, it felt natural that I&amp;rsquo;d introduce the ~3,000 players to what I&amp;rsquo;d just found out about. I&amp;rsquo;d also gain some hands-on experience applying this redteaming technique.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="designing-the-challenge"&gt;Designing The Challenge&lt;/h1&gt;
&lt;p&gt;The specifics of the challenge changed throughout, but the primary idea remained mostly intact. It&amp;rsquo;s final playthrough is something like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;A concerned CEO forwards an article about RAG security to his sysadmin.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The sysadmin appreciates the advice but assures the CEO that vector embeddings are a practically non-reversible, hash-like list of values.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The sysadmin&amp;rsquo;s carelessness leaves a vector database open for query by anybody with no authentication, with some contents stored in plaintext and some stored purely as embedding vectors.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To obtain the flag, the player must generate a custom ruleset to crack a leaked hash, the rules of which are obtained by inverting one of the entries in the database using vec2text.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I went ahead and picked the most convenient option for a vector database, which I found to be &lt;a href="https://www.trychroma.com/"&gt;ChromaDB&lt;/a&gt; given it&amp;rsquo;s plug-and-play Python library.&lt;/p&gt;
&lt;p&gt;When it came to choosing which information to have the player invert, I learned through experimentation - but also by drawing on my understanding of the volatility of token placement - that expecting a string to be extracted precisely was going to be impossible. This is why I opted for the inversion candidate not to be a flag, hash, or other dense string, but rather something like a set of instructions, like a list of &lt;em&gt;minimum password requirements&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="tokenizer_diagram.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;An illustration of the tokenization process. &lt;a href="https://www.linkedin.com/pulse/tokenization-how-llms-process-text-tokens-nikitha-r-gnbkf/"&gt;Source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Initially, I kept things as close to a realistic deployment as possible by having Chroma ingest some strings and allow the software to embed the sensitive data itself. The player would have to discover the ChromaDB instance, extract these documents/strings from the database, and then use vec2text to perform the inversion attack. As you&amp;rsquo;ll see, this wasn&amp;rsquo;t a feasible option.&lt;/p&gt;
&lt;h2 id="the-compute-factor"&gt;The Compute Factor&lt;/h2&gt;
&lt;p&gt;I naively went into this project anticipating a rich cluster of GPUs at the disposal of the organizers of the event, so I was testing vec2text&amp;rsquo;s effectiveness with as many n_steps (the number of times vec2text&amp;rsquo;s corrector would iterate to reduce loss between guessed inversions and the real embeddings) as would be necessary for producing an accurate inversion. That assumption was unfortunately not true. I had to find some way to optimize the challenge in a way where the number of iterations that a player would use would be high enough to yield an accurate-enough result while not being so high as to not be able to be ran on anything other than a GTX 5060.&lt;/p&gt;
&lt;p&gt;While optimizing for my modest laptop CPU to perform the inversion attack in under a minute, the output was quite a bit &lt;a href="https://en.wikipedia.org/wiki/Lossy_compression"&gt;lossy&lt;/a&gt; in the sense that inverting the string of interest led to inconsistent and poor results. Luckily, vec2text has utility functions to take some strings and create its own embeddings based on any HuggingFace model. So, I stored embeddings pre-computed by vec2text in Chroma, so that when a player exfiltrates the embeddings, there would be no doubt about the accuracy (as long as they enter the correct parameters).
&lt;img src="chroma_vec2text_pipeline_comparison.png" alt=""&gt;
&lt;em&gt;A visual explaining the differences in ingestion approaches. Source: Claude Sonnet 5&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I also had to try a few variations of the original string as well as tweaking the value of vec2text&amp;rsquo;s &amp;ldquo;beam_width,&amp;rdquo; which essentially just seeds the randomization of the inversion, leading to higher accuracy and consistency. I didn&amp;rsquo;t dig deep into how beam_width works internally, but this is how I understand it.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="completing-the-challenge"&gt;Completing The Challenge&lt;/h1&gt;
&lt;p&gt;Information that a player would need to make the inverted minimum password requirements useful (a SHA256 hash and a &amp;ldquo;magic string&amp;rdquo;) is stored in plaintext on the database, rewarding them for their enumeration and leading them to the right path. Metadata included in the database also includes the model that was used to embed the vectors which would&amp;rsquo;ve been necessary for performing the attack.&lt;/p&gt;
&lt;p&gt;As mentioned earlier, a player would have to use the minimum password requirements to construct a custom ruleset including a reasonably-sized arbitrary string of text placed in an arbitrary position in the context of the real password (a &amp;ldquo;magic string&amp;rdquo;). This also doesn&amp;rsquo;t include two specific characters that are prepended to the password. Using this custom ruleset and the SHA256 hash to compare attempts to, a player would use a password-cracking utility like hashcat&amp;rsquo;s &lt;a href="https://hashcat.net/wiki/doku.php?id=mask_attack"&gt;&amp;ldquo;Mask Attack&amp;rdquo;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The purpose of the &amp;ldquo;magic string&amp;rdquo; is to simultaneously introduce complexity to the password as to not be guessed by pure brute force while also shifting some of the burden of this complexity from the information to be extracted during the inversion process onto the metadata discovery process.&lt;/p&gt;
&lt;p&gt;Finally, this password acts as a key to unzip a password-protected 7z file found in a few steps earlier during the challenge through an SSRF exploit, leading to the flag.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="mitigation"&gt;Mitigation&lt;/h1&gt;
&lt;p&gt;In the challenge I developed, the &amp;ldquo;sysadmin&amp;rdquo; took only some partial appropriate steps for securing the sensitive information in the RAG database. Let alone the problematic architectural choices of unauthenticated retrieval, writing, and full access to the database, only one of the sensitive documents was stored without the plaintext metadata to accompany it. The following are a list of guidelines that, if in a real-world scenario, I would recommend to the people deploying the application that this challenge hosts.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do not allow unauthenticated, arbitrary access to any internal company store or API.&lt;/strong&gt;
This one should go unsaid. Applications intended for internal use only should only be accessible&amp;hellip; internally. That means network segmentation, such as whitelisting select LAN IPs (unless a more robust and secure tunneling approach is used) AND at least any amount of HTTP security like Basic Auth or Token Auth (through .htpasswd and Chroma environment variables).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do not store sensitive information in plaintext.&lt;/strong&gt;
The documents stored in the vector database in this challenge, while being embedded and ready for use in the RAG, should have any associated plaintext stripped. Of course, embeddings are still prone to inversion attacks as shown in this post (and &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/RAG_Security_Cheat_Sheet.html#section-2-embedding-manipulation"&gt;should NOT be assumed to be irreversible&lt;/a&gt;), but removing this plaintext would greatly reduce the attack surface.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do not store unnecessary information where it doesn&amp;rsquo;t belong.&lt;/strong&gt;
There&amp;rsquo;s no legitimate reason to embed secrets and hashes in a RAG system. Besides just the security implications of doing so, LLMs shouldn&amp;rsquo;t be expected to extract super-rich semantically dense text. Also, LLMs should retrieve sensitive information like records, credentials and other secrets, and miscellaneous PII from tool/function calls that use traditional access-control approaches. More information about how LLMs and their RAGs are unreliable as arbiters of who has access to their own information can be found at &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/RAG_Security_Cheat_Sheet.html#section-4-access-control-inheritance"&gt;OWASP&amp;rsquo;s guidance on access control inheritance in RAG&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Writing this challenge was an excellent opportunity to get familiar with LLM-related attack vectors on a much deeper level than simply completing a challenge somebody else wrote for me. Besides just that lane of attacks, building the infrastructure leading up to the actual inversion attack let me become more intimate with how things like PHP access control works and how important proper network segmentation is to the integrity of a data pipeline. Learning how to construct a complex Docker project was also a very valuable experience for me throughout the building of this project. While I haven&amp;rsquo;t had the chance to poll opinions from players yet, that feedback will be crucial for improving my understanding of the concepts I implemented in this challenge.&lt;/p&gt;</description></item><item><title>Writing a Mock Pentest Report</title><link>https://mdunn99.com/posts/writing-a-mock-pentest-report/</link><pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate><guid>https://mdunn99.com/posts/writing-a-mock-pentest-report/</guid><description>&lt;p&gt;A few months ago, my friend and I were inspired by some other peers to take our redteaming HackTheBox exercises a step further: writing a full mock penetration test report.&lt;img src="coverpage.png" alt=""&gt;&lt;em&gt;The cover page generated for us by Claude Sonnet 5&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;They themselves were fascinated by what our school&amp;rsquo;s award winning C3 team does twice per year in their &lt;a href="https://cp.tc/"&gt;CCDC competitions&lt;/a&gt;.
&lt;img src="c32026.png" alt=""&gt;
&lt;em&gt;UCF C3 team earlier this year during the NCCDC competition.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;During the course of 24 hours, a team of no more than 10 people participate in a hackathon-like competition to probe, poke at, and, finally, document vulnerabilities of a multi-machine network that culminates in a typically ~100-page report that simulates a real-world penetration test.&lt;/p&gt;
&lt;p&gt;Myself and my friend, &lt;a href="https://www.linkedin.com/in/stephenmiller05/"&gt;Stephen&lt;/a&gt;, set out to take any HackTheBox machine and do the same thing. In the process, we learned a lot!&lt;/p&gt;
&lt;h2 id="hacking-the-box"&gt;Hacking the Box&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re not already aware, &lt;a href="https://www.hackthebox.com"&gt;HackTheBox&lt;/a&gt; is a free platform that allows users to break into intentionally vulnerable machines and &amp;ldquo;capture the flag&amp;rdquo; that awaits for the user upon successful exploitation. My friends and I use this website a ton to practice our redteaming skills (we even keep a scoreboard on the UCF Discord server).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.hackthebox.com/machines/silentium"&gt;Silentium&lt;/a&gt; is an easy-rated, CVE-heavy HackTheBox machine that allowed us to focus mostly on the technical writing aspect of this challenge and less of the redteaming part. In the future, I&amp;rsquo;d like to make more mock reports on machines offered by HackTheBox that allow me to go into more detail on specific processes for exploiting the system rather than the technical details (sometimes a bit over our head) offered by the exploit we stumbled across while enumerating the machine for vulnerabilities.&lt;/p&gt;
&lt;h2 id="gathering-inspiration"&gt;Gathering Inspiration&lt;/h2&gt;
&lt;p&gt;The first thing we had to ask before going in and writing was: &amp;ldquo;what does a real, official pentest report look like?&amp;rdquo; Luckily, we didn&amp;rsquo;t have to wonder. There are plenty of resources out in the wild that offered us a glimpse into how pentest reports are structured - textually and visually. So, we spent a lot of time perusing &lt;a href="https://github.com/reconmap/pentest-reports/"&gt;this repository&lt;/a&gt; and compiling visual elements throughout the writing process that we found cool. More importantly, it gave us some insight into the appropriate length of certain sections, where figures should be inserted, and small things like tone selection.&lt;/p&gt;
&lt;h2 id="writing"&gt;Writing&lt;/h2&gt;
&lt;p&gt;It just so happens that the semester in college that we wrote this report was the semester that I was taking a professional writing class, which not only meant that my writing muscles had been worked recently, but also that I understood a key part of writing professional reports. What I had in mind was to balance the report in a &amp;ldquo;PAM&amp;rdquo; triangle, meaning not just prioritizing the Purpose of the report, but also the Message and how it&amp;rsquo;s conveyed, and the Audience it&amp;rsquo;s written for. This appears to be an adaptation of Aristotle&amp;rsquo;s Rhetorical Triangle.
&lt;img src="pamtriad.png" alt=""&gt;
&lt;em&gt;One variation of the Rhetorical Triangle.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Essentially it just reminded us that for the purposes of this &amp;ldquo;assignment&amp;rdquo;, we&amp;rsquo;d be writing so that the report can be understood across a wide spectrum of C-suites, senior-level technicians, and lower-level vulnerability researchers and security engineers. After all, the people at a company that approve a large sum of money to be used for commissioning a pentest report want to understand the value that they received from such an investment. Likewise, the details of a successful exploitation by the pentesters is a useful vector for anybody at the company that works to secure their digital infrastructure.&lt;/p&gt;
&lt;p&gt;To cover each of the roles we were catering to, each section existed for a purpose. For instance, plenty of summaries existed before the meat of the report (findings and their CVSS scores). This provided an introduction to the specifics of the engagement to ground not only potentially tech-unsavvy individuals but also their engineers. Even in the individual finding reports, impact summaries are provided to help an employee understand the scope of a particular issue.&lt;/p&gt;
&lt;p&gt;Providing engineers with clear reproduction steps will go a long way in helping them quickly identify where the issue exists and remediating it, with or without our suggestions.&lt;/p&gt;
&lt;p&gt;Equally (and arguably more) effective for communicating some of these ideas at a high-level are visuals.&lt;/p&gt;
&lt;h2 id="visuals"&gt;Visuals&lt;/h2&gt;
&lt;p&gt;One important aspect of writing an effective report is conveying things visually, like the layout of the network. Below, I documented the attack chain that a threat actor could take to gain root access on the host system, incorporating the network architecture into said path.
&lt;img src="drawio.png" alt=""&gt;&lt;em&gt;A visual that I created with &lt;a href="https://draw.io"&gt;draw.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We also wanted to highlight the most important vulnerabilities that we found in the stack before any others. So, the assessment findings are sorted from highest to least severity and color-coded accordingly. Further, in a heavily-inspired way from a &amp;ldquo;Vuln Table&amp;rdquo; I had seen in the repository mentioned earlier, we kept things brief and &amp;ldquo;compressed&amp;rdquo;. Also, hyperlinks!
&lt;img src="vulntables.png" alt=""&gt;&lt;em&gt;Assessment findings in the report followed by our vulnerability summary table&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Code blocks are presented inside of a 1x1 table in monospace font; censor sensitive information:
&lt;img src="codeblock.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Quote blocks are not the same as code blocks:
&lt;img src="quoteblock.png" alt=""&gt;&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I gained valuable soft skills throughout this exercise like designing visuals, working with word editors, and conveying complex ideas to a wide range of people. It also tested my understanding of the very engagement that my friend and I spent no longer than a day on (plus one more in the beginning of the CTF). I hope to produce more like this in the future!&lt;/p&gt;</description></item><item><title>Training a Basic Random Forest Regression Model</title><link>https://mdunn99.com/posts/forest_regression/</link><pubDate>Fri, 30 Jan 2026 00:00:00 +0000</pubDate><guid>https://mdunn99.com/posts/forest_regression/</guid><description>&lt;p&gt;You can read the code for this project on my &lt;a href="https://github.com/mdunn99/housing-prices-competition"&gt;Github&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id="random-forest-regression---hyperparameters-and-feature-selection"&gt;Random Forest Regression - Hyperparameters and Feature Selection&lt;/h1&gt;
&lt;p&gt;It was earlier last month when I decided to get a head-start on learning how to build machine learning models and manipulate datasets using libraries like pandas and numpy. &lt;a href="https://www.kaggle.com/"&gt;Kaggle&lt;/a&gt;, the excellent dataset resource, it turns out, also provides in-depth courses and challenges to stimulate the exact kinds of things I wanted to get involved in. Their short &lt;a href="https://www.kaggle.com/learn/intro-to-machine-learning"&gt;&amp;ldquo;Intro to Machine Learning&amp;rdquo; course&lt;/a&gt; was an excellent primer for building a simple model leveraging the SciKit sklearn Python libraries.&lt;/p&gt;
&lt;p&gt;Completing this course brought me to a page where I was able to submit my own model for an associated competition for predicting housing prices. This was an excellent opportunity to apply what I had just learned and to prove my problem-solving and creative-thinking skills.&lt;/p&gt;
&lt;p&gt;After completing this project, I became very familiar with the train_test_split method of building machine learning models and the process of training -&amp;gt; fitting, training -&amp;gt; fitting. I learned how looping a training process with incremental changes to the training model will yield optimal results - things like proper feature selection and hyperparameter tuning.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I encountered a few problems throughout this project:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Most of the features from the dataset were categorical, non-integer data. How can a random forest make sense of textual data?&lt;/li&gt;
&lt;li&gt;How could I select hyperparameters more effectively - less manually?&lt;/li&gt;
&lt;li&gt;How could I select features more effectively - less manually?&lt;/li&gt;
&lt;li&gt;How did I know that a random forest regressor was ideal for this dataset?&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="converting-categorical-data-to-numerical-representations"&gt;Converting Categorical Data to Numerical Representations&lt;/h3&gt;
&lt;p&gt;My first problem was quite trivial. I wrote a function to loop through &amp;lsquo;object&amp;rsquo; dtypes in a given pandas dataframe and used sklearn.preprocessing&amp;rsquo;s &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html"&gt;LabelEncoder&lt;/a&gt; to &lt;code&gt;fit_transform()&lt;/code&gt; the selected column, effectively creating numerical representations of comparable values.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;convert_categorical_to_integer_labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataframe&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataframe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select_dtypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;object&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        &lt;span class="c1"&gt;# ignore non-feature&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;SalePrice&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        &lt;span class="c1"&gt;# remove missing data from selected dataframe&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        &lt;span class="n"&gt;dataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Missing&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        &lt;span class="c1"&gt;# apply fit_transform&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        &lt;span class="n"&gt;dataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataframe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dataframe&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Redefining my main dataframe, &lt;code&gt;train_data&lt;/code&gt; as itself ran through this function now yields a fully-usable dataset.&lt;/p&gt;
&lt;h3 id="tuning-hyperparameters-and-selecting-features"&gt;Tuning Hyperparameters and Selecting Features&lt;/h3&gt;
&lt;p&gt;Developing this small model gave me insight into how decision trees work on a granular level, like how too many leaf nodes in a decision tree or random forest can lead to poor model performance. As the model is built and becomes too large, it may try to find similarities between features that don&amp;rsquo;t exist. This is why it&amp;rsquo;s imperative to guide the model in the right direction through the process of tuning.&lt;/p&gt;
&lt;p&gt;Tuning hyperparameters like max_leaf_nodes is useful for creating a more accurate model, but it&amp;rsquo;s far more effective to automate the process. I simply defined a list of some integers from 0-500 &lt;code&gt;candidate_leaf_nodes = [5,10,25,50,100,200,500,700,1000]&lt;/code&gt; and ran it through a function that I also used to iterate through feature selection:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;train_model_return_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_leaf_nodes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RandomForestRegressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_leaf_nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="n"&gt;y_pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="n"&gt;mae&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean_absolute_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mae&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&amp;hellip;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;leaf&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidate_leaf_nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="n"&gt;mae&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_model_return_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    &lt;span class="n"&gt;candidate_leaf_nodes_mae_index&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mae&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A list pairing each candidate with its MAE allowed me to select the &lt;code&gt;ideal_leaf_nodes&lt;/code&gt; with an anonymous function:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ideal_leaf_nodes = min(candidate_leaf_nodes_mae_index, key = lambda x: x[1])[0]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;When we view the results from &lt;code&gt;candidate_leaf_nodes_mae_index&lt;/code&gt;, we can clearly see diminishing returns as max_leaf_nodes increases, and even inconsistent negative growth as the model reaches a count of 1000.&lt;/p&gt;
&lt;p&gt;&lt;img src="mae_wrt_max_leaf_nodes.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I initially planned to use a sliding window approach, but discovered Random Forest&amp;rsquo;s built-in importance scores were more principled and computationally efficient.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;importances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feature_importances_&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="feature_importances_wrt_feature.png" alt=""&gt;
&lt;em&gt;Top 20 features by importance (80 total were selected). The steep dropoff shows how concentrated predictive power is in just a few features.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This project is the first of many in my exploration of machine learning models and their optimization. Exploring different approaches to feature selection, from manual sliding windows to sklearn&amp;rsquo;s built-in importance scores, taught me the value of leveraging well-tested libraries. I also learned about the power of continuous model fitting and doing so in a results-oriented way, using mean absolute error as one method of measuring effectiveness.&lt;/p&gt;
&lt;p&gt;Coming away from this project had me wondering if a random forest regressor is truly ideal for this project. What other ensemble learning methods could I try? In the future, I&amp;rsquo;ll examine other ensemble methods like kernel methods or gradient boosting and try to understand how each can be used for different kinds of applications.&lt;/p&gt;</description></item></channel></rss>