<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Kaonashi&apos;s Blog</title><description>Welcome to Kaonashi&apos;s Blog!</description><link>https://kaonashi.dev/</link><item><title>Staying curious as a developer</title><link>https://kaonashi.dev/blog/stayingcuriousasadeveloper/</link><guid isPermaLink="true">https://kaonashi.dev/blog/stayingcuriousasadeveloper/</guid><description>How I still keep up with tech after years as a developer</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Compared to other sciences, computer science is probably the most recent one.
Its tools and best practices continue to evolve every year.
In tech, staying up to date with major changes is generally expected.
A large part of studying computer science is learning how to learn.&lt;/p&gt;
&lt;p&gt;I believe this is a field where you need to stay curious and keep discovering new things.
And no, I am not saying people should spend their personal time coding and reading about tech.
But there&apos;s a middle ground between it being an everyday passion and spending 10 years writing the same code with the same tools at the same company.&lt;/p&gt;
&lt;p&gt;I&apos;m convinced that trying new tools and languages, reading about how others work, and, most importantly,
sharing those experiences with others is both enjoyable and something that can mostly fit within working hours.&lt;/p&gt;
&lt;h1&gt;Tech Watch&lt;/h1&gt;
&lt;p&gt;The practice of monitoring technological developments is often called &lt;code&gt;tech watch&lt;/code&gt;.
Over time, I&apos;ve found multiple ways of staying informed. I&apos;ll share some of them with you.&lt;/p&gt;
&lt;p&gt;As strange as it may seem to introverted people, exchanging ideas with colleagues and fellow developers is a big part of staying up to date.
You may discuss news, ideas, and projects that you find interesting.&lt;/p&gt;
&lt;p&gt;At my previous workplace, we set up a short, 15-minute meeting with the tech team every Thursday morning to share tech news.
Sometimes we&apos;d have lots of cool stuff to share, and other times the meeting lasted a minute.
Having a dedicated time slot let developers know they were allowed and encouraged to read the news during work hours.&lt;/p&gt;
&lt;p&gt;To keep up with tech news, I mostly read individual blogs, but I also watch videos and browse some companies&apos; forums.
I&apos;ve started sharing some of them on my &lt;a href=&quot;/readlist/&quot;&gt;read list page&lt;/a&gt;.
More recently, I set up an &lt;a href=&quot;https://en.wikipedia.org/wiki/RSS&quot;&gt;RSS&lt;/a&gt; reader to follow the latest news from my favourite blogs and newsletters.
On Mac, I&apos;ve found &lt;a href=&quot;https://netnewswire.com/&quot;&gt;NetNewsWire&lt;/a&gt; to be a great tool.&lt;/p&gt;
&lt;p&gt;I wanted another way of sharing knowledge, so I helped promote the idea of workshops.
For me, workshops became one of the best ways to combine active learning, experimentation, and knowledge sharing.&lt;/p&gt;
&lt;h1&gt;Workshops&lt;/h1&gt;
&lt;p&gt;We&apos;d run a workshop about every two months. We&apos;d block a Friday afternoon and spend anywhere from one to four hours on it.
Anyone on the team could organise a new workshop or join an existing one.&lt;/p&gt;
&lt;p&gt;The sessions that attracted the most people were often the ones where participants could simply sit and listen.
The most memorable ones, however, were the hands-on workshops.&lt;/p&gt;
&lt;p&gt;Some workshops were directly linked to the business.
For instance, we spent an afternoon exploring ways to improve our OCR results, which had initially been a one-person job.&lt;/p&gt;
&lt;p&gt;Others were completely unrelated, such as learning how to pick locks.
This is a well-known cliché hobby for cyber security people.
I brought a few of my lock-picking kits for participants to try after showing them how to pick a lock.
A colleague brought handcuffs for a lock-picking demonstration.
I still have a photo of my manager at the time wearing the handcuffs.&lt;/p&gt;
&lt;p&gt;When AI, and specifically LLMs, started becoming popular, we organised a few workshops around those.
One team tried to create a Dungeon &amp;amp; Dragons Game Master by using OpenAI&apos;s API.
Later, we did a session on Speech-to-Text and Text-to-Speech where the same team reused this work so they could talk to their AI Game Master.
I also have a very fond memory of this workshop because I made a program that could drive a drone with voice commands: it could take off, land, move around, and even do a backflip.
That was so freaking cool.&lt;/p&gt;
&lt;p&gt;I&apos;ve included below a few more examples of workshops we ran. Maybe it&apos;ll give you some ideas.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Talk by a maintainer of an internal project about its architecture&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;General presentations: &quot;Hiring in Tech&quot;, &quot;Code Quality&quot;, &quot;Architecture 101&quot;, &quot;Playing with GPUs&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Based on our stack: &quot;Encryption at rest&quot;, &quot;Database optimization within our technology stack&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tools: Elasticsearch, GitLab CI, PlantUML&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Watching &lt;a href=&quot;https://symfonycasts.com/&quot;&gt;SymfonyCasts&lt;/a&gt; videos together and discussing the code&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Live coding in a specific language&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Build and host a blog on AWS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rust: Getting started by building a small application&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Build your own AI agent using MCP&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Godot: Build a simple game&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kubernetes &amp;amp; K3s setup&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.codingame.com/start/&quot;&gt;CodinGame&lt;/a&gt; challenges&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pentesting exercises&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;As you can see, the subjects were quite diverse, chosen mostly based on people&apos;s interests and knowledge, there was no need to be an expert to share something.&lt;/p&gt;
&lt;p&gt;The end goal was to create opportunities for others, and ourselves, to discover new things.
Sometimes we&apos;d learn a new skill, sometimes we&apos;d simply have fun, and other times we&apos;d realise we never wanted to touch a particular language or tool ever again.&lt;/p&gt;
&lt;p&gt;Looking back, these moments were also a particularly good way of fostering a healthy engineering culture&lt;/p&gt;
&lt;h1&gt;Personal projects&lt;/h1&gt;
&lt;p&gt;You may or may not enjoy coding outside of work, but hear me out before you brush off this part.&lt;/p&gt;
&lt;p&gt;There are plenty of side projects out there, more or less time consuming, and more or less related to tech.&lt;/p&gt;
&lt;p&gt;For instance, you could spend a weekend on a hackathon. I&apos;ve done a few, pouring way too many hours into projects I&apos;d never touch again.
I still do one every once in a while, for the competitiveness or just to spend time with friends. They&apos;re fun, but they eat up a lot of time.&lt;/p&gt;
&lt;p&gt;I get a similar kind of satisfaction from &lt;a href=&quot;https://adventofcode.com/&quot;&gt;Advent of Code&lt;/a&gt;, without losing my whole weekend to it.&lt;/p&gt;
&lt;p&gt;But here&apos;s another idea: why not build projects for yourself? Better yet, why not mix your hobbies with your skills?
This blog is one example, but there&apos;s so much more to do.
Personally, there are still plenty of things I want to build:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A NAS so I can share photos with my friends and family. I&apos;ll build my own app for it, since I don&apos;t like the existing ones.&lt;/li&gt;
&lt;li&gt;A personal finance app that fits my needs better than what&apos;s out there, so I can finally ditch my automated spreadsheet.&lt;/li&gt;
&lt;li&gt;A genealogy app that doesn&apos;t look like it&apos;s from 1990 and keeps my data locally.&lt;/li&gt;
&lt;li&gt;A way to automatically water my plants based on their type and needs.&lt;/li&gt;
&lt;li&gt;When I grow up, I&apos;ll buy a house, and I&apos;ll make it smart. I&apos;ve already started with a Raspberry Pi running &lt;a href=&quot;https://www.home-assistant.io/&quot;&gt;Home Assistant&lt;/a&gt;, but it doesn&apos;t do much yet... My lights flicker when it&apos;s bedtime and they turn red when the CO2 is too high.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A friend who&apos;s a big RPG fan made a &lt;a href=&quot;https://ludicrpg.com/alien-motion-tracker/&quot;&gt;TTRPG Alin Motion Tracker&lt;/a&gt; (The picture looks cool, I know, I know, you&apos;re welcome.).
A second friend made a &lt;a href=&quot;https://tabletop.krayorn.com&quot;&gt;board game tracker&lt;/a&gt;, where you can log the games you&apos;ve played and the board games you own.
A third is building a relatively smart apartment that helps manage electricity consumption.
And a fourth built a private sports-logging app for their friend circle to run competitions.&lt;/p&gt;
&lt;p&gt;With the rise of coding agents, building your own applications is more accessible than ever, though you&apos;ll admittedly learn a lot less along the way.
If you&apos;re concerned about privacy, money, or the environment, you can &lt;a href=&quot;https://themeansquare.medium.com/running-local-ai-models-in-cursor-the-complete-guide-4290fe0383fa&quot;&gt;run Ollama locally with Cursor&lt;/a&gt;.
Either way, there&apos;s never been a lower barrier to finally building that project you keep putting off.&lt;/p&gt;
&lt;h1&gt;Thoughts&lt;/h1&gt;
&lt;p&gt;I hope I&apos;ll be able to promote this idea of workshops at my next job.
Meanwhile, I&apos;ll keep doing personal projects and experiments in my free time. I&apos;ve already started sharing some of them here on this blog.&lt;/p&gt;
&lt;p&gt;You can&apos;t keep up with every new technology out there because there&apos;s simply too much.
However, making time to discover, experiment with, and share new things has always felt like a worthwhile investment as a developer.&lt;/p&gt;
</content:encoded></item><item><title>Building a movies RAG</title><link>https://kaonashi.dev/blog/moviesrag/</link><guid isPermaLink="true">https://kaonashi.dev/blog/moviesrag/</guid><description>My goal is to learn about RAG systems by building one that finds movies from natural language prompts</description><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Context&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;RAG, which stands for Retrieval-Augmented Generation,
is an AI framework that combines the strengths of traditional information retrieval systems (such as search and databases)
with the capabilities of generative large language models (LLMs).
By combining your data and world knowledge with LLM language skills,
grounded generation is more accurate, up-to-date,
and relevant to your specific needs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Source: &lt;a href=&quot;https://cloud.google.com/use-cases/retrieval-augmented-generation&quot;&gt;Google Cloud&lt;/a&gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;My goal with this project is to learn how to build a RAG system.
I read &lt;a href=&quot;https://go.dev/blog/llmpowered&quot;&gt;this post&lt;/a&gt; from The Go Blog to better understand the concepts behind RAG and how to build one.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Dataset&lt;/h1&gt;
&lt;p&gt;The first problem we face is finding a dataset large enough for the RAG system to retrieve information from.
For example, my brother built &lt;a href=&quot;https://github.com/bouhlet0/cve-rag&quot;&gt;a RAG using Common Vulnerabilities and Exposures (CVE)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I chose to build one focused on movies.
I found a useful &lt;a href=&quot;https://www.kaggle.com/datasets/asaniczka/tmdb-movies-dataset-2023-930k-movies&quot;&gt;dataset&lt;/a&gt; containing around 1 million movies based on data from The Movie Database (TMDB).&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Understanding Vectors&lt;/h1&gt;
&lt;p&gt;For this project, we will need a vector database, so let&apos;s make sure we understand the basics first.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;vector&lt;/strong&gt; is a mathematical object that has both magnitude and direction. Vectors can be added together and multiplied by scalars (numbers).
&lt;a href=&quot;https://www.mathsisfun.com/algebra/vectors.html&quot;&gt;This website&lt;/a&gt; provides a nice visual explanation with concrete examples.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;strong&gt;vector database&lt;/strong&gt;, vector store or vector search engine is a database that stores and retrieves embeddings of data in vector space.
Vector databases typically implement approximate nearest neighbor algorithms so users can search for records semantically similar to a given input,
unlike traditional databases which primarily look up records by exact match. &lt;em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Vector_database&quot;&gt;Wikipedia&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;strong&gt;vector space&lt;/strong&gt; is a set whose elements, often called vectors, can be added together and multiplied (&quot;scaled&quot;) by numbers called scalars. &lt;em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Vector_space&quot;&gt;Wikipedia&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Embedding&lt;/strong&gt; is a representation learning technique that maps complex, high-dimensional data into a lower-dimensional vector space of numerical vectors. &lt;em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Embedding_(machine_learning)&quot;&gt;Wikipedia&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example, two movie descriptions about space exploration may end up close together in the embedding space, even if they do not share the exact same words.&lt;/p&gt;
&lt;p&gt;Alright, but these are just definitions. It is still hard to picture what embeddings and vector databases actually do.
&amp;lt;img src=&quot;https://developers.google.com/static/machine-learning/crash-course/images/embeddings_3D_tangyuan.png&quot; alt=&quot;Embedding&quot; width=&quot;700&quot;/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Image from &amp;lt;a href=&quot;https://developers.google.com/machine-learning/crash-course/embeddings/embedding-space&quot;&amp;gt;Google Developers: Embeddings&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;I recommend reading the article &lt;a href=&quot;https://developers.google.com/machine-learning/crash-course/embeddings/embedding-space&quot;&gt;Embeddings&lt;/a&gt; by Google Developers to get a better intuition of how embedding spaces work.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Architecture&lt;/h1&gt;
&lt;p&gt;I have two main parts to build. First, I need to ingest the movie data into a vector database. Second, I need to build a way to query this data.&lt;/p&gt;
&lt;h2&gt;Ingestion Flow&lt;/h2&gt;
&lt;p&gt;Here is a high-level diagram of the ingestion flow I am aiming for:&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;High-level diagram of the data ingestion flow&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Raw movie data is converted into structured documents.&lt;/li&gt;
&lt;li&gt;The document text is transformed into embeddings using a text embedding model.&lt;/li&gt;
&lt;li&gt;The embeddings and associated metadata are stored in the vector database for later retrieval.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now, here is the same ingestion flow including the technologies used in this project:&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Lower level diagram of ingestion flow of the data&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Movie data is cleaned and converted into structured documents.&lt;/li&gt;
&lt;li&gt;The Nomic embedding model (served via Ollama) generates embeddings from each document.&lt;/li&gt;
&lt;li&gt;These embeddings, along with their metadata, are stored in Qdrant, where they can be queried using similarity search at runtime.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;RAG (User Flow / Query Pipeline)&lt;/h2&gt;
&lt;p&gt;Here is a simple diagram of the RAG user flow I am aiming for:&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;High-level diagram of the RAG user flow&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user submits a query through the interface, which forwards it to the API.&lt;/li&gt;
&lt;li&gt;The API converts the query into an embedding, retrieves similar documents from the vector database, and prepares the context.&lt;/li&gt;
&lt;li&gt;The LLM generates an answer using the retrieved context, and the response is returned to the user.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is the same RAG user flow including the technologies used in this project:&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Lower-level diagram of the RAG user flow&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user interacts with the Chainlit interface, which sends the request to FastAPI.&lt;/li&gt;
&lt;li&gt;FastAPI uses the Nomic embedding model, served through Ollama, to embed the query and performs a similarity search in Qdrant using cosine distance.&lt;/li&gt;
&lt;li&gt;The retrieved documents are passed to Llama 3.1, served through Ollama, which generates the final response returned through FastAPI to the interface.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;Setup&lt;/h1&gt;
&lt;p&gt;I run my applications on a Mac M3 with 18 GB of RAM and an 11-core CPU.&lt;/p&gt;
&lt;h2&gt;Python stack&lt;/h2&gt;
&lt;p&gt;I chose Python for this project because I wanted to try some of the newer tools in the ecosystem.&lt;/p&gt;
&lt;p&gt;I picked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt; as the package manager&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astral.sh/ruff/&quot;&gt;Ruff&lt;/a&gt; as the linter and formatter&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astral.sh/ty/type-checking/&quot;&gt;ty&lt;/a&gt; as the type checker&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fastapi.tiangolo.com/&quot;&gt;FastAPI&lt;/a&gt; as the API framework&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://uvicorn.dev/&quot;&gt;uvicorn&lt;/a&gt; as the web server&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AI stack&lt;/h2&gt;
&lt;p&gt;The tools used for the AI part are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt; to run LLMs locally on my machine&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://qdrant.tech/&quot;&gt;Qdrant&lt;/a&gt; as the vector database&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nomic.ai/news/nomic-embed-text-v1&quot;&gt;Nomic&lt;/a&gt; (&lt;code&gt;nomic-embed-text&lt;/code&gt; through &lt;a href=&quot;https://ollama.com/library/nomic-embed-text&quot;&gt;Ollama&lt;/a&gt;) as the text embedding model&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.llama.com/&quot;&gt;Llama&lt;/a&gt; (&lt;code&gt;llama3.1:8b&lt;/code&gt; through &lt;a href=&quot;https://ollama.com/library/llama3.1:8b&quot;&gt;Ollama&lt;/a&gt;) as the LLM&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.chainlit.io/get-started/overview&quot;&gt;Chainlit&lt;/a&gt; as an open-source Python framework for building conversational AI applications&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Docker&lt;/h2&gt;
&lt;p&gt;If you check the repository, you will see that I Dockerized everything from the start to have a portable environment.&lt;/p&gt;
&lt;p&gt;This is a personal preference and is probably overkill for this project. A Python virtual environment with the required tools installed locally would work perfectly fine.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Ingestion&lt;/h1&gt;
&lt;p&gt;I&apos;ll walk through the steps I took to ingest the data into the vector database.&lt;/p&gt;
&lt;h2&gt;Step 0: Install the tools&lt;/h2&gt;
&lt;p&gt;We don&apos;t need the full stack installed yet, so let&apos;s iterate step by step.&lt;/p&gt;
&lt;p&gt;The first tools we need are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/&quot;&gt;Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://qdrant.tech/&quot;&gt;Qdrant&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also need to download the embedding model from Nomic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama pull nomic-embed-text
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a few Python libraries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/kagglehub/&quot;&gt;kagglehub&lt;/a&gt; - interact with Kaggle resources&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/ollama/&quot;&gt;ollama&lt;/a&gt; - communicate with the Ollama API from Python.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/pandas/&quot;&gt;pandas&lt;/a&gt; - data analysis and manipulation library&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/qdrant-client/&quot;&gt;qdrant-client&lt;/a&gt; - Python client library for Qdrant&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/requests/&quot;&gt;requests&lt;/a&gt; - HTTP client library&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Installing these libraries with uv is trivial:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv add kagglehub ollama pandas qdrant-client requests
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 1: Get the dataset&lt;/h2&gt;
&lt;p&gt;The chosen dataset is a large CSV file, so we will use pandas later to read and process it.&lt;/p&gt;
&lt;p&gt;We download the dataset using the &lt;code&gt;kagglehub&lt;/code&gt; Python library.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pathlib import Path


def downloadDataset():
    raw_path = kagglehub.dataset_download(
        &quot;asaniczka/tmdb-movies-dataset-2023-930k-movies&quot;
    )

    target_dir = Path(&quot;/app/data/tmdb&quot;)

    target_dir.mkdir(parents=True, exist_ok=True)

    for item in Path(raw_path).rglob(&quot;*&quot;):
        if item.is_file():
            dest = target_dir / item.name
            shutil.copy2(item, dest)

    print(&quot;[DATASET] stored at:&quot;, target_dir)

    return str(target_dir)


if __name__ == &quot;__main__&quot;:
    downloadDataset()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Github file: &lt;a href=&quot;https://github.com/kaonashi-42/movies-rag/blob/dbbedc7df5fa61937ee200f450595b39f7f8c194/cmd/downloadDataset.py&quot;&gt;scripts/download_dataset.py&lt;/a&gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 2: Explore the dataset&lt;/h2&gt;
&lt;p&gt;Before ingesting the data, we need to understand the structure of the CSV file and decide which fields will be useful for our documents.&lt;/p&gt;
&lt;p&gt;First, let&apos;s create a function to load the dataset.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from src.config import DATASET_PATH


def load_movies():
    if not DATASET_PATH.exists():
        raise FileNotFoundError(f&quot;Dataset not found at {DATASET_PATH}&quot;)

    df = pd.read_csv(DATASET_PATH)

    return df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Github file: &lt;a href=&quot;https://github.com/kaonashi-42/movies-rag/blob/dbbedc7df5fa61937ee200f450595b39f7f8c194/src/dataset/loader.py&quot;&gt;src/dataset/loader.py&lt;/a&gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;Let&apos;s inspect the dataset using pandas:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from src.dataset.loader import load_movies
from src.dataset.documentBuilder import movieToDocument


if __name__ == &quot;__main__&quot;:
    try: 
        df = load_movies().fillna(&quot;&quot;)

        print(df.columns)  # The titles
        print(df.head())  # The first 5 rows
        movie = df.iloc[0]

        for column in df.columns:
            print(f&quot;{column}: {movie[column]}&quot;)  # The first movie&apos;s columns

    except Exception as err:
        print(f&quot;err {err}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Github file: &lt;a href=&quot;https://github.com/kaonashi-42/movies-rag/blob/dbbedc7df5fa61937ee200f450595b39f7f8c194/cmd/exploreDataset.py&quot;&gt;cmd/exploreDataset.py&lt;/a&gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Index([&apos;id&apos;, &apos;title&apos;, &apos;vote_average&apos;, &apos;vote_count&apos;, &apos;status&apos;, &apos;release_date&apos;,
       &apos;revenue&apos;, &apos;runtime&apos;, &apos;adult&apos;, &apos;backdrop_path&apos;, &apos;budget&apos;, &apos;homepage&apos;,
       &apos;imdb_id&apos;, &apos;original_language&apos;, &apos;original_title&apos;, &apos;overview&apos;,
       &apos;popularity&apos;, &apos;poster_path&apos;, &apos;tagline&apos;, &apos;genres&apos;,
       &apos;production_companies&apos;, &apos;production_countries&apos;, &apos;spoken_languages&apos;,
       &apos;keywords&apos;],
      dtype=&apos;str&apos;)

       id  ...                                           keywords
0   27205  ...  rescue, mission, dream, airplane, paris, franc...
1  157336  ...  rescue, future, spacecraft, race against time,...
2     155  ...  joker, sadism, chaos, secret identity, crime f...
3   19995  ...  future, society, culture clash, space travel, ...
4   24428  ...  new york city, superhero, shield, based on com...

[5 rows x 24 columns]
id: 27205
title: Inception
vote_average: 8.364
vote_count: 34495
status: Released
release_date: 2010-07-15
revenue: 825532764
runtime: 148
adult: False
backdrop_path: /8ZTVqvKDQ8emSGUEMjsS4yHAwrp.jpg
budget: 160000000
homepage: https://www.warnerbros.com/movies/inception
imdb_id: tt1375666
original_language: en
original_title: Inception
overview: Cobb, a skilled thief who commits corporate espionage by infiltrating the subconscious of his targets is offered a chance to regain his old life as payment for a task considered to be impossible: &quot;inception&quot;, the implantation of another person&apos;s idea into a target&apos;s subconscious.
popularity: 83.952
poster_path: /oYuLEt3zVCKq57qu2F8dT7NIa6f.jpg
tagline: Your mind is the scene of the crime.
genres: Action, Science Fiction, Adventure
production_companies: Legendary Pictures, Syncopy, Warner Bros. Pictures
production_countries: United Kingdom, United States of America
spoken_languages: English, French, Japanese, Swahili
keywords: rescue, mission, dream, airplane, paris, france, virtual reality, kidnapping, philosophy, spy, allegory, manipulation, car crash, heist, memory, architecture, los angeles, california, dream world, subconscious
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 3: Initalize the database&lt;/h2&gt;
&lt;p&gt;We create a Qdrant client, connect to the Qdrant instance, and create a collection called &lt;code&gt;movies&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In the &lt;a href=&quot;https://ollama.com/library/nomic-embed-text:latest/blobs/970aa74c0a90&quot;&gt;model&apos;s documentation&lt;/a&gt;, we can see that &lt;code&gt;nomic-bert.embedding_length=768&lt;/code&gt;.
This means each embedding generated by the model contains 768 dimensions, so the Qdrant collection needs to be configured with the same vector size.&lt;/p&gt;
&lt;p&gt;Qdrant is a vector database that performs nearest-neighbor search. We use cosine distance to measure the similarity between vectors.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance

client = QdrantClient(url=&quot;http://qdrant:6333&quot;)


def createCollection(vector_size: int = 768):
    client.recreate_collection(
        collection_name=&quot;movies&quot;,
        vectors_config=VectorParams(
            size=vector_size,
            distance=Distance.COSINE,
        ),
    )

    print(&quot;[QDRANT] collection &apos;movies&apos; ready&quot;)

if __name__ == &quot;__main__&quot;:
    createCollection(vector_size=768)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Github file: &lt;a href=&quot;https://github.com/kaonashi-42/movies-rag/blob/dbbedc7df5fa61937ee200f450595b39f7f8c194/src/vectordb/qdrantClient.py&quot;&gt;src/vectordb/qdrantClient.py&lt;/a&gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 4: Ingestion&lt;/h2&gt;
&lt;p&gt;Before creating the ingestion pipeline, we need a way to convert text into embeddings.
For this, we send the movie text to the &lt;code&gt;nomic-embed-text&lt;/code&gt; embedding model through Ollama.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
OLLAMA_URL = &quot;http://ollama:11434&quot;


def embedText(text: str) -&amp;gt; list[float]:
    response = requests.post(
        f&quot;{OLLAMA_URL}/api/embeddings&quot;,
        json={
            &quot;model&quot;: &quot;nomic-embed-text&quot;,
            &quot;prompt&quot;: text,
        },
        timeout=30,
    )
    response.raise_for_status()
    return response.json()[&quot;embedding&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Github file: &lt;a href=&quot;https://github.com/kaonashi-42/movies-rag/blob/dbbedc7df5fa61937ee200f450595b39f7f8c194/src/embeddings/httpEmbedding.py&quot;&gt;src/embeddings/httpEmbedding.py&lt;/a&gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;An embedding is a list of numbers that represents a piece of data in a vector space.&lt;/p&gt;
&lt;p&gt;For text, embedding models transform sentences into vectors where similar concepts tend to be located closer together.
This allows computers to compare text based on semantic similarity rather than only exact word matches.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from numpy.typing import NDArray


Embedding = NDArray[np.float64]


def cosine_similarity(a: Embedding, b: Embedding) -&amp;gt; float:
    return float(
        np.dot(a, b) /
        (np.linalg.norm(a) * np.linalg.norm(b))
    )


def compare_texts(text_a: str, text_b: str) -&amp;gt; float:
    embedding_a = np.array(embedText(text_a), dtype=np.float64)
    embedding_b = np.array(embedText(text_b), dtype=np.float64)

    return cosine_similarity(embedding_a, embedding_b)


def main() -&amp;gt; None:
    base_sentence: str = &quot;Prince, Princess&quot;
    sentence_comparison1: str = &quot;A fairy tale about royalty and a king and queen&quot;
    similarity = compare_texts(base_sentence, sentence_comparison1)
    print(f&quot;{base_sentence} &amp;lt;-- vs -&amp;gt; {sentence_comparison1}&quot;)
    print(f&quot;Similarity: {round(similarity, 2)}\n&quot;)

    sentence_comparison2: str = &quot;A car engine repair manual with mechanical instructions&quot;
    similarity = compare_texts(
        base_sentence,
        sentence_comparison2
    )
    print(f&quot;{base_sentence} &amp;lt;-- vs --&amp;gt; {sentence_comparison2}&quot;)
    print(f&quot;Similarity: {round(similarity, 2)}&quot;)
    

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Prince, Princess &amp;lt;-- vs -&amp;gt; A fairy tale about royalty and a king and queen
Similarity: 0.73

Prince, Princess &amp;lt;-- vs --&amp;gt; A car engine repair manual with mechanical instructions
Similarity: 0.42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we ingest a few movies into Qdrant.
We do not need to test the project with a database containing 1 million entries yet, so 200 movies is enough for now.&lt;/p&gt;
&lt;p&gt;Each movie becomes a Qdrant point. The vector contains the embedding, while the payload contains metadata associated with the movie.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from src.dataset.loader import load_movies
from src.embeddings.httpEmbedding import embedText
from src.vectordb.qdrantClient import client


def ingest(n=200):
    df = load_movies().fillna(&quot;&quot;)
    df = df.head(n)

    for i, row in df.iterrows():
        text = f&quot;{row[&apos;title&apos;]} {row[&apos;overview&apos;]} {row[&apos;tagline&apos;]}&quot;
        vector = embedText(text)

        client.upsert(
            collection_name=&quot;movies&quot;,
            points=[
                {
                    &quot;id&quot;: int(row[&quot;id&quot;]),
                    &quot;vector&quot;: vector,
                    &quot;payload&quot;: {
                        &quot;title&quot;: row[&quot;title&quot;],
                        &quot;overview&quot;: row[&quot;overview&quot;],
                        &quot;genres&quot;: row[&quot;genres&quot;],
                    },
                }
            ],
        )

        if i % 50 == 0:
            print(&quot;ingested:&quot;, i)


if __name__ == &quot;__main__&quot;:
    ingest()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Github file: &lt;a href=&quot;https://github.com/kaonashi-42/movies-rag/blob/dbbedc7df5fa61937ee200f450595b39f7f8c194/cmd/ingest.py&quot;&gt;cmd/ingest.py&lt;/a&gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 5: Search database&lt;/h2&gt;
&lt;p&gt;Now that we have stored data in the vector database, we can query it.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;top_k&lt;/code&gt; parameter determines how many results Qdrant should return. These results are ranked by vector similarity.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from qdrant_client import QdrantClient
from qdrant_client.models import SearchRequest
from src.embeddings.httpEmbedding import embedText

client = QdrantClient(url=&quot;http://qdrant:6333&quot;)


def search_movies(query: str, top_k: int = 5):
    query_vector = embedText(query)

    results = client.query_points(
        collection_name=&quot;movies&quot;,
        query=query_vector,
        limit=top_k,
        with_payload=True,
    )

    for r in results.points:
        print(&quot;\n---&quot;)
        print(&quot;title:&quot;, r.payload[&quot;title&quot;])
        print(&quot;genres:&quot;, r.payload[&quot;genres&quot;])
        print(&quot;overview:&quot;, r.payload[&quot;overview&quot;][:200])


if __name__ == &quot;__main__&quot;:
    search_movies(&quot;dream infiltration mind heist&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Github file: &lt;a href=&quot;https://github.com/kaonashi-42/movies-rag/blob/dbbedc7df5fa61937ee200f450595b39f7f8c194/cmd/qdrantSearch.py&quot;&gt;cmd/qdrantSearch.py&lt;/a&gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Inception
genres: Action, Science Fiction, Adventure
overview: Cobb, a skilled thief who commits corporate espionage by infiltrating the subconscious of his targets is offered a chance to regain his old life as payment for a task considered to be impossible: &quot;inc

---
title: Fear in the Night
genres: Crime, Drama, Mystery, Thriller
overview: The dream is unusually vivid: Bank employee Vince Grayson finds himself murdering a man in a sinister octagonal-shaped room lined with mirrors while a mysterious woman breaks into a safe. It is so viv

---
title: The Master Touch
genres: Thriller, Action, Crime
overview: A master thief, just out of prison, concocts a risky final score that would net him over a million dollars.

---
title: Trance
genres: Thriller, Crime, Drama, Mystery
overview: A violent gang enlists the help of a hypnotherapist in an attempt to locate a painting which somehow vanished in the middle of a heist.

---
title: In Dreams
genres: Mystery, Thriller, Horror
overview: A suburban housewife learns that she has psychic connections to a serial killer, and can predict this person&apos;s motives through her dreams.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 6: Qdrant client&lt;/h2&gt;
&lt;p&gt;On this url &lt;code&gt;http://localhost:6333/dashboard&lt;/code&gt; you may access Qdrant&apos;s dashboard.
It allows you to visualize and query your vector database, inspect collections, and explore stored points.&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Qdrant collections&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;Qdrant point example&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;The Qdrant API provides query operations, such as listing collections:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// List all collections
GET collections
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which returns:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;result&quot;: {
    &quot;collections&quot;: [
      {
        &quot;name&quot;: &quot;movies&quot;
      }
    ]
  },
  &quot;status&quot;: &quot;ok&quot;,
  &quot;time&quot;: 0.000262373
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also retrieve points using filters:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST collections/movies/points/scroll
{
  &quot;limit&quot;: 10,
  &quot;filter&quot;: {
    &quot;must&quot;: [
      {
        &quot;key&quot;: &quot;title&quot;,
        &quot;match&quot;: {
          &quot;any&quot;: [
            &quot;Star Wars&quot;
          ]
        }
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This returns points matching the filter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;result&quot;: {
    &quot;points&quot;: [
      {
        &quot;id&quot;: 11,
        &quot;payload&quot;: {
          &quot;movie_id&quot;: 11,
          &quot;imdb_id&quot;: &quot;tt0076759&quot;,
          &quot;title&quot;: &quot;Star Wars&quot;,
          &quot;overview&quot;: &quot;Princess Leia is captured and held hostage by the evil Imperial forces in their effort to take over the galactic Empire. Venturesome Luke Skywalker and dashing captain Han Solo team together with the loveable robot duo R2-D2 and C-3PO to rescue the beautiful princess and restore peace and justice in the Empire.&quot;,
          &quot;genres&quot;: &quot;Adventure, Action, Science Fiction&quot;,
          &quot;popularity&quot;: 88.559,
          &quot;vote_average&quot;: 8.204,
          &quot;vote_count&quot;: 19155,
          &quot;runtime&quot;: 121,
          &quot;adult&quot;: false,
          &quot;release_date&quot;: &quot;1977-05-25&quot;
        }
      }
    ],
    &quot;next_page_offset&quot;: null
  },
  &quot;status&quot;: &quot;ok&quot;,
  &quot;time&quot;: 0.024117376
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;RAG&lt;/h1&gt;
&lt;p&gt;We now have a vector database containing a few movie points. Let&apos;s build the Retrieval-Augmented Generation part.&lt;/p&gt;
&lt;h2&gt;Step 0: Install the tools&lt;/h2&gt;
&lt;p&gt;The last tool we need is &lt;a href=&quot;https://docs.chainlit.io/get-started/overview&quot;&gt;Chainlit&lt;/a&gt;, which will provide the chat interface for interacting with our RAG.&lt;/p&gt;
&lt;p&gt;We also need to download the LLM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama pull llama3.1:8b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a few more Python libraries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/uvicorn/&quot;&gt;uvicorn&lt;/a&gt; - ASGI web server implementation for Python&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/chainlit/&quot;&gt;chainlit&lt;/a&gt; - framework for building conversational AI interfaces&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/fastapi/&quot;&gt;fastapi&lt;/a&gt; - web framework for building APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&apos;re installing them with uv:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv add uvicorn chainlit fastapi
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 1: Chainlit&lt;/h2&gt;
&lt;p&gt;We&apos;ll use Chainlit only as the user interface layer.
It receives messages from the user and forwards the queries to the FastAPI backend that we will create next.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@cl.on_message&lt;/code&gt; is a Chainlit event handler that runs every time the user sends a message.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
API_URL = &quot;http://app:8000/ask&quot;


@cl.on_message
async def main(message: cl.Message):
    response = requests.post(
        API_URL,
        json={&quot;question&quot;: message.content},
        timeout=120,
    )

    if response.status_code != 200:
        await cl.Message(content=&quot;Error calling API&quot;).send()
        return

    answer = response.json()[&quot;answer&quot;]

    await cl.Message(content=answer).send()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 2: Building our app&apos;s API&lt;/h2&gt;
&lt;p&gt;We need a FastAPI backend that responds to the &lt;code&gt;/ask&lt;/code&gt; route when we send a message from Chainlit.&lt;/p&gt;
&lt;p&gt;When we receive a message, we want to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Convert the user&apos;s text into an embedding.&lt;/li&gt;
&lt;li&gt;Search Qdrant for similar movie points.&lt;/li&gt;
&lt;li&gt;Build a context from the retrieved movies.&lt;/li&gt;
&lt;li&gt;Send this context to the LLM and ask it to generate an answer.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is the main RAG pipeline: retrieval + generation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fastapi import FastAPI
from pydantic import BaseModel

from src.embeddings.httpEmbedding import embedText
from qdrant_client import QdrantClient

app = FastAPI()

qdrant = QdrantClient(url=&quot;http://qdrant:6333&quot;)
OLLAMA_URL = &quot;http://ollama:11434&quot;


class AskRequest(BaseModel):
    question: str


def search_qdrant(vector: list[float], top_k=5):
    results = qdrant.query_points(
        collection_name=&quot;movies&quot;,
        query=vector,
        limit=top_k,
        with_payload=True,
    )
    return results.points


def build_context(points):
    context = []
    for p in points:
        payload = p.payload
        context.append(
            f&quot;{payload.get(&apos;title&apos;)}: {payload.get(&apos;overview&apos;)}&quot;
        )
    return &quot;\n\n&quot;.join(context)


def generate_answer(question, context):
    prompt = f&quot;&quot;&quot;
You are a movie recommendation system.

Use ONLY the context below.

Context:
{context}

User question:
{question}

Answer:
&quot;&quot;&quot;

    response = requests.post(
        f&quot;{OLLAMA_URL}/api/generate&quot;,
        json={
            &quot;model&quot;: &quot;llama3.1:8b&quot;,
            &quot;prompt&quot;: prompt,
            &quot;stream&quot;: False,
        },
    )

    response.raise_for_status()
    return response.json()[&quot;response&quot;]


@app.post(&quot;/ask&quot;)
def ask(req: AskRequest):
    vector = embedText(req.question)
    matches = search_qdrant(vector)
    context = build_context(matches)
    answer = generate_answer(req.question, context)

    return {
        &quot;question&quot;: req.question,
        &quot;answer&quot;: answer,
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The app is run with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvicorn src.api.client:app --host 0.0.0.0 --port 8000 --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 3: Using our RAG&lt;/h2&gt;
&lt;p&gt;We now have a functional RAG application and can start asking questions about movies.&lt;/p&gt;
&lt;p&gt;I verified in the database that the returned movies are stored in Qdrant, have a rating above 6.5/10, and were released between 2010 and 2020.&lt;/p&gt;
&lt;h1&gt;What next&lt;/h1&gt;
&lt;p&gt;I cleaned up the code, added a linter (Ruff), a type checker (ty), some tests, a GitHub pipeline, and tidied up my Makefile, Docker, and Compose setup.
&lt;a href=&quot;https://github.com/kaonashi-42/movies-rag/tree/010136e5199512fb41744efd4253819628bd8b0a&quot;&gt;Here is the repository at the moment&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At this point, we have a working RAG system, but there is still a lot we could improve.&lt;/p&gt;
&lt;h2&gt;1. Ingestion&lt;/h2&gt;
&lt;p&gt;Qdrant currently contains 200 movies, but the full dataset contains around 1 million entries.&lt;/p&gt;
&lt;p&gt;I tried ingesting more data, but I ran into a performance issue: ingestion is very slow. I ingested around 40k points in an hour, which means the full dataset would take roughly 25 hours.&lt;/p&gt;
&lt;p&gt;During ingestion, my machine is running at full capacity. There are a few things I could try:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sending batches of movies instead of one movie at a time&lt;/li&gt;
&lt;li&gt;Checking whether running Ollama outside Docker improves performance&lt;/li&gt;
&lt;li&gt;Using a more powerful machine or a remote server for the embedding process&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. Chat memory&lt;/h2&gt;
&lt;p&gt;The LLM currently has no awareness of previous conversations.&lt;/p&gt;
&lt;p&gt;A possible improvement would be to send Chainlit&apos;s &lt;code&gt;chat_context&lt;/code&gt; to the &lt;code&gt;/ask&lt;/code&gt; route, allowing the model to use previous messages when answering follow-up questions.&lt;/p&gt;
&lt;h2&gt;3. Reranking&lt;/h2&gt;
&lt;p&gt;Currently, we retrieve 5 points from Qdrant.
A possible improvement would be to retrieve more candidates (for example 20) and rerank them before sending the final context to the LLM.&lt;/p&gt;
&lt;p&gt;The closest embedding is not always the most relevant movie.&lt;/p&gt;
&lt;p&gt;A reranker model (for example a cross-encoder) could compare the user query with each retrieved movie and select the best matches.&lt;/p&gt;
&lt;h2&gt;4. Hybrid search&lt;/h2&gt;
&lt;p&gt;We could combine semantic search with lexical search.&lt;/p&gt;
&lt;p&gt;Semantic search understands concepts. For example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Give me a movie like Avatar&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A vector search can understand:
Avatar
→ blue aliens
→ another planet
→ nature
→ science fiction&lt;/p&gt;
&lt;p&gt;However, lexical search can directly match the word &lt;code&gt;Avatar&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For instance &quot;Give me a movie like Avatar&quot;.
Vector search understands &quot;Avatar -&amp;gt; blue aliens -&amp;gt; planet -&amp;gt; nature -&amp;gt; sci-fi&quot;. While exact keyword will match &quot;Avatar&quot; directly.&lt;/p&gt;
&lt;p&gt;This is useful when users mention exact movie titles, actors, or keywords.
Qdrant supports sparse vectors, which could be combined with dense vectors to create a hybrid search system.&lt;/p&gt;
&lt;h2&gt;5. Validation&lt;/h2&gt;
&lt;p&gt;My current prompt says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Use ONLY the context below&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;but this is only a soft instruction. The LLM could still hallucinate a movie that does not exist in the database.&lt;/p&gt;
&lt;p&gt;A possible improvement would be to include movie IDs in the retrieved context and validate the generated answer against the database.&lt;/p&gt;
&lt;p&gt;Another option would be to use a second LLM as a validator, but that feels unnecessary for this project (and would increase the hardware requirements).&lt;/p&gt;
&lt;h2&gt;6. Evals&lt;/h2&gt;
&lt;p&gt;We could add evaluations with a tool such as &lt;a href=&quot;https://docs.ragas.io/en/stable/&quot;&gt;Ragas&lt;/a&gt; to measure RAG quality.&lt;/p&gt;
&lt;p&gt;The test data could look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
 {
  &quot;question&quot;: &quot;movie about space survival&quot;,
  &quot;expected&quot;: [
    &quot;Interstellar&quot;,
    &quot;The Martian&quot;
  ]
 },
 {
  &quot;question&quot;: &quot;sad romance movies&quot;,
  &quot;expected&quot;: [
    &quot;Titanic&quot;
  ]
 }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This would allow us to measure whether changes improve or degrade the system.&lt;/p&gt;
&lt;h2&gt;7. More ideas&lt;/h2&gt;
&lt;p&gt;Other possible improvements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use an LLM for query rewriting so the user&apos;s question is transformed into a better search query&lt;/li&gt;
&lt;li&gt;Create a user profile to personalize recommendations based on preferences&lt;/li&gt;
&lt;li&gt;Use metadata filters for structured data such as dates, ratings, runtime, and popularity&lt;/li&gt;
&lt;li&gt;Add a recommendation score combining semantic similarity with movie metadata&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Making sense of AI taxonomy</title><link>https://kaonashi.dev/blog/aitaxonomy/</link><guid isPermaLink="true">https://kaonashi.dev/blog/aitaxonomy/</guid><pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have been looking for diagrams that classify AI types, methods, and definitions.
It feels like no one agrees on the definitions. Every diagram differs from one source to another.
Instead of helping me understand the field as a whole, it confused me even more.&lt;/p&gt;
&lt;p&gt;I decided to make my own, with Wikipedia, &lt;a href=&quot;https://reactflow.dev/&quot;&gt;React Flow&lt;/a&gt;, and time.&lt;/p&gt;
&lt;p&gt;I am certain there are many things I have missed, but this was my first step toward understanding the big picture.
If you spot any mistakes, please feel free to contact me at me@kaonashi.dev.&lt;/p&gt;
&lt;p&gt;Some thoughts after doing this exercise:
AI taxonomy should be a huge graph with connections going in all directions, or lots and lots of smaller diagrams.
Here I mixed paradigms, architectures, models, learning methods and so on so I could make a mental map of it all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You may click on boxes to get a definition.&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>Building my first AI with PyTorch</title><link>https://kaonashi.dev/blog/buildmyai/</link><guid isPermaLink="true">https://kaonashi.dev/blog/buildmyai/</guid><description>Build a binary image classifier using supervised learning</description><pubDate>Mon, 01 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I wanted to build my own AI for a while.
I used different types of AI at work (symbolic NLU, RAG, OCR, STT, TTS, agents, and LLMs),
but I never truly understood what was happening behind the scenes.&lt;/p&gt;
&lt;p&gt;My brother studied this field, so I asked him to guide me through building one.&lt;/p&gt;
&lt;p&gt;We chose a very serious and important problem: distinguishing dogs from muffins.&lt;/p&gt;
&lt;p&gt;Apparently, it&apos;s a classic exercise taught in engineering schools, and a dataset already exists for it.&lt;/p&gt;
&lt;p&gt;My goal was to satisfy my curiosity about how models are built, learn some of the vocabulary, and understand the environment and tools required for this kind of project.&lt;/p&gt;
&lt;p&gt;For this project, we will use machine learning, more specifically, deep learning.
Since we have labeled examples of dogs and muffins, this is a supervised learning task.
To solve it, we will build a Convolutional Neural Network (CNN).&lt;/p&gt;
&lt;h1&gt;Vocabulary&lt;/h1&gt;
&lt;p&gt;To understand each other, we first needed a little of AI vocabulary. I&apos;ll go through what we discussed.&lt;/p&gt;
&lt;p&gt;A CNN is a type of neural network used for images.&lt;/p&gt;
&lt;p&gt;Instead of looking at the whole image at once, it looks at small parts of the image first (like edges, shapes, and textures), then combines them to understand more complex patterns.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First layers detect simple things like lines or borders&lt;/li&gt;
&lt;li&gt;Middle layers detect shapes like ears or eyes&lt;/li&gt;
&lt;li&gt;Later layers combine these into full objects like &quot;dog&quot; or &quot;muffin&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This step-by-step feature detection makes CNNs very effective for image classification tasks.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;alt&quot;&amp;gt;A CNN Architecture, &lt;a href=&quot;https://cdn.learnopencv.com/wp-content/uploads/2023/01/04090236/Convolutional-Neural-Networks.png&quot;&gt;source&lt;/a&gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;Supervised learning is a type of machine learning where a model learns from labeled examples.&lt;/p&gt;
&lt;p&gt;Each training example includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an input (data)&lt;/li&gt;
&lt;li&gt;the correct output (label)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The model&apos;s job is to learn the relationship between inputs and outputs so it can predict the correct output for new, unseen inputs.&lt;/p&gt;
&lt;p&gt;Example: If you show a model many emails labeled &quot;spam&quot; or &quot;not spam,&quot; it learns patterns and can then classify new emails.&lt;/p&gt;
&lt;p&gt;A GAN is a model that generates new data, such as images.&lt;/p&gt;
&lt;p&gt;It has two parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generator: creates fake data&lt;/li&gt;
&lt;li&gt;Discriminator: tries to detect real vs fake&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They train against each other, and over time the generator learns to produce more realistic results.&lt;/p&gt;
&lt;p&gt;A Recurrent Neural Network (RNN) is a type of neural network designed to work with sequences of data, where order matters.&lt;/p&gt;
&lt;p&gt;Unlike CNNs (which focus on images), RNNs process data step by step while keeping a kind of &quot;memory&quot; of what they have seen before. This makes them useful for things like text, speech, or time series.&lt;/p&gt;
&lt;p&gt;For example, in a sentence, each word depends on the previous ones. An RNN can use earlier words to help understand the current word.&lt;/p&gt;
&lt;p&gt;Example: &lt;a href=&quot;https://github.com/tesseract-ocr/tesseract&quot;&gt;Tesseract (OCR)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Tesseract is an OCR (Optical Character Recognition) system that reads text from images.
Internally, systems like this can use sequence-based models (like RNNs or similar architectures) to interpret characters in order, forming words and sentences from visual input.&lt;/p&gt;
&lt;p&gt;This is useful because text is not just individual symbols-it is a sequence where context matters.&lt;/p&gt;
&lt;p&gt;A token is a basic unit of input data that a model processes.&lt;/p&gt;
&lt;p&gt;In text models: a token is usually a word, part of a word, or a character chunk.
Example: &quot;unbelievable&quot; might be split into tokens like &quot;un&quot;, &quot;believ&quot;, &quot;able&quot;.
In vision models (less common usage of the term): &quot;tokens&quot; can refer to patches of an image (especially in Vision Transformers), where an image is divided into small regions.&lt;/p&gt;
&lt;p&gt;In other words, tokens are what the model reads as input.&lt;/p&gt;
&lt;p&gt;A parameter is a learned internal value inside the neural network that determines how input is transformed into output.&lt;/p&gt;
&lt;p&gt;Parameters include weights and biases in layers (including CNN convolution filters).
In CNNs:
A filter (kernel) is a set of parameters that learns to detect patterns like edges, textures, or shapes.
These values are adjusted during training through backpropagation.&lt;/p&gt;
&lt;p&gt;To summerize, parameters are what the model learns during training.&lt;/p&gt;
&lt;p&gt;A loss function is a way to measure how wrong the model&apos;s predictions are.&lt;/p&gt;
&lt;p&gt;It compares the model&apos;s output with the correct answer and gives a single number:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;small value → the model is doing well&lt;/li&gt;
&lt;li&gt;large value → the model is doing poorly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;During training, the goal is to reduce the loss as much as possible, so the model gradually improves its predictions.&lt;/p&gt;
&lt;p&gt;A label is the correct answer for a given input.&lt;/p&gt;
&lt;p&gt;For example, if the input is an image of a cat, the label is &quot;cat&quot;. It represents the truth that the model is supposed to learn from.&lt;/p&gt;
&lt;p&gt;During training, the model&apos;s prediction is compared to the label to see if it is correct or not.&lt;/p&gt;
&lt;p&gt;A feature is a small piece of information in the input that helps the model make a prediction.&lt;/p&gt;
&lt;p&gt;In images, features can be simple things like edges, corners, textures, or shapes (for example: an ear, an eye, or a border).&lt;/p&gt;
&lt;p&gt;The model combines many small features to understand the full object, like recognizing a cat or a muffin.&lt;/p&gt;
&lt;p&gt;In the machine learning field, Data engineering is the process of preparing raw data so it can be used by a model.&lt;/p&gt;
&lt;p&gt;This includes cleaning the data (removing errors or missing values), organizing it, and sometimes creating new useful information from it (called feature engineering).&lt;/p&gt;
&lt;p&gt;Feature engineering means transforming raw data into better inputs for the model.
For example, instead of only using sales numbers, we might add a feature like &quot;holiday = yes/no&quot; if sales tend to increase during holidays.&lt;/p&gt;
&lt;p&gt;Transfer learning is when we take a model that has already been trained on a large dataset and reuse it for a new task.&lt;/p&gt;
&lt;p&gt;Instead of training from scratch, we keep the learned knowledge (like basic patterns in images) and adapt it to a smaller, specific dataset.&lt;/p&gt;
&lt;p&gt;This is useful because the model already knows general features, so it needs less data and less training time to learn the new task.&lt;/p&gt;
&lt;p&gt;An agent is a system (often an AI model) that can take a goal and decide what actions to take to achieve it.&lt;/p&gt;
&lt;p&gt;Instead of just producing an answer directly, an agent can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;write or run code&lt;/li&gt;
&lt;li&gt;use tools or APIs&lt;/li&gt;
&lt;li&gt;call external systems or scripts&lt;/li&gt;
&lt;li&gt;combine multiple steps to solve a problem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, if asked to analyze data, an agent might first load a file, then run code to clean it, then compute results, and finally produce a summary.&lt;/p&gt;
&lt;p&gt;Inference is when we use a trained model to make a prediction.&lt;/p&gt;
&lt;p&gt;It is the step where we give the model new input data, and it produces an output (a result).&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;input: an image of a cat&lt;/li&gt;
&lt;li&gt;inference output: &quot;cat&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Inferences doe not change the model. It only uses what the model has already learned.&lt;/p&gt;
&lt;p&gt;A generative pre-trained transformer (GPT) is a type of large language model (LLM).&lt;/p&gt;
&lt;p&gt;A Transformer is a type of neural network used for sequences like text.&lt;/p&gt;
&lt;p&gt;It works by looking at all words in a sentence at the same time and deciding which words are important to understand the meaning.&lt;/p&gt;
&lt;p&gt;The key idea is attention: the model learns which parts of the input to focus on when making a prediction.&lt;/p&gt;
&lt;p&gt;This makes Transformers very effective for language tasks like translation, text generation, and chat models.&lt;/p&gt;
&lt;p&gt;A system prompt is the first instruction given to an AI model before any user input.&lt;/p&gt;
&lt;p&gt;It defines how the model should behave, such as its tone, rules, or role.&lt;/p&gt;
&lt;p&gt;For example, it can tell the model to be formal, concise, or to follow specific constraints. The system prompt is always applied before the user&apos;s message.&lt;/p&gt;
&lt;p&gt;RAG is a method that improves a language model by giving it access to external information.&lt;/p&gt;
&lt;p&gt;Instead of only using what the model learned during training, it first retrieves relevant data from a database or documents, and then uses that information to generate an answer.&lt;/p&gt;
&lt;p&gt;This helps the model give more accurate and up-to-date responses, especially for specific or recent information.&lt;/p&gt;
&lt;p&gt;ReLU is an activation function used in neural networks.&lt;/p&gt;
&lt;p&gt;It decides what information should pass through the network by changing values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;if the input is positive → keep it&lt;/li&gt;
&lt;li&gt;if the input is negative → replace it with 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This helps the model learn faster and adds non-linearity, which allows it to learn more complex patterns.&lt;/p&gt;
&lt;h4&gt;More resources&lt;/h4&gt;
&lt;p&gt;My brother also recommended these videos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=aircAruvnKk&quot;&gt;Neural Networks explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=UGO_Ehywuxc&quot;&gt;The Dark Matter of AI [Mechanistic Interpretability]&lt;/a&gt;. This one felt a bit too advanced for me yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Tools&lt;/h1&gt;
&lt;p&gt;We use &lt;a href=&quot;https://www.kaggle.com/datasets/samuelcortinhas/muffin-vs-chihuahua-image-classification&quot;&gt;a public image dataset&lt;/a&gt; from Kaggle:&lt;/p&gt;
&lt;p&gt;The dataset is structured in a simple folder format, where each class has its own directory. This makes it compatible with PyTorch&apos;s ImageFolder, which automatically assigns labels based on folder names.&lt;/p&gt;
&lt;p&gt;Our dataset hierarchy looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data/
|-- test/
|   |-- chihuahua/
|   `-- muffin/
`-- train/
    |-- chihuahua/
    `-- muffin/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This structure separates training and testing data, allowing us to train the model on one set of images and evaluate it on unseen examples.&lt;/p&gt;
&lt;p&gt;Tools used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;VS Code IDE&lt;/li&gt;
&lt;li&gt;Jupyter Notebook Plugin for VSCode&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Python libraties:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;torch==2.12.0  # (model building and training)
torchvision==0.27.0  # (image loading and transformations)
matplotlib==3.10.9  # (image file handling)
pillow==12.2.0  # (visualization)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To install the dependencies, we first create a Python virtual environment.
This isolates the project so that its packages do not interfere with other Python installations on the system.
&lt;em&gt;Back here a month later, you may use &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt; to handle dependencies easily&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Let&apos;s build&lt;/h1&gt;
&lt;p&gt;If you wish to access the notebook directly, here is my &lt;a href=&quot;https://github.com/kaonashi-42/CNN-chihuahua-muffin/tree/main&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;First we create a &lt;code&gt;notebook.ipynb&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;We import PyTorch here to confirm the environment is set up correctly before going further.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;We prepare an image so it can be used as input for a neural network.
Machine learning models cannot work directly with raw image files,
so the image must first be converted into a numerical format they can process.&lt;/p&gt;
&lt;p&gt;The image is resized to 224x224 because most neural networks require inputs of a fixed size.
Without this step, images of different dimensions would not be compatible with the model.&lt;/p&gt;
&lt;p&gt;Next, the pixel values are scaled to a range between 0 and 1 and converted into floating-point numbers.
This makes the data easier and more stable for the model to learn from, since very large integer values (0-255) can slow down or destabilize training.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
from pathlib import Path
from typing import cast
from PIL import Image

PATH: Path = Path(&quot;./data/test/chihuahua/img_4_568.jpg&quot;)

img: Image.Image = Image.open(PATH).convert(&quot;RGB&quot;)

transform: transforms.Compose = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToImage(),
    transforms.ToDtype(torch.float32, scale=True),
])

img_tensor: torch.Tensor = cast(torch.Tensor, transform(img))

_, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4))

ax1.imshow(img)
ax1.set_title(&quot;Original&quot;)
ax1.axis(&quot;off&quot;)

ax2.imshow(img_tensor.permute(1, 2, 0).numpy())
ax2.set_title(&quot;Transformed (224x224)&quot;)
ax2.axis(&quot;off&quot;)

plt.tight_layout()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;We create training and testing datasets so the model can learn from one set of images and be evaluated on another it has not seen before.
This separation is important because it lets us measure how well the model generalizes to new data instead of just memorizing the training images.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;ImageFolder&lt;/code&gt;, which automatically reads images from folders where each class has its own directory.
This makes it easy to assign labels based on folder names without manually labeling each image.&lt;/p&gt;
&lt;p&gt;We apply the same transformations as before to ensure all images have the same size and format, and that pixel values are scaled between 0 and 1.
This consistency is necessary so the model receives data in a uniform structure during both training and testing.&lt;/p&gt;
&lt;p&gt;Finally, we print &lt;code&gt;class_to_idx&lt;/code&gt; to see how class names (folder names) are mapped to numeric labels, which is what the model actually uses internally.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from torchvision import datasets
from torchvision.datasets import ImageFolder

transform: transforms.Compose = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.ToDtype(torch.float32, scale=True),
])

train_dataset: ImageFolder = datasets.ImageFolder(
    root=&quot;./data/train&quot;,
    transform=transform,
)

test_dataset: ImageFolder = datasets.ImageFolder(
    root=&quot;./data/test&quot;,
    transform=transform,
)

class_to_idx: dict[str, int] = train_dataset.class_to_idx
print(class_to_idx)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&apos;chihuahua&apos;: 0, &apos;muffin&apos;: 1}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;We create data loaders to feed images into the model in small groups called batches.
Neural networks do not process one image at a time efficiently, so batching helps speed up training and makes better use of memory.&lt;/p&gt;
&lt;p&gt;We set batch_size=32, which means the model will process 32 images at once before updating its weights.
This improves training stability compared to processing single images.&lt;/p&gt;
&lt;p&gt;We shuffle the training data so the model sees images in a different order each epoch, which helps prevent it from learning patterns based on the order of the data.
We do not shuffle the test data because we want consistent evaluation results.&lt;/p&gt;
&lt;p&gt;Finally, we load one batch from the training loader to check that everything works correctly.
The image tensor shape shows 32 images with 3 color channels and 224x224 size, and the label tensor shows one label per image.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from torch.utils.data import DataLoader

train_loader: DataLoader[tuple[torch.Tensor, torch.Tensor]] = DataLoader(
    train_dataset,
    batch_size=32,
    shuffle=True,
)

test_loader: DataLoader[tuple[torch.Tensor, torch.Tensor]] = DataLoader(
    test_dataset,
    batch_size=32,
    shuffle=False,
)

images: torch.Tensor
labels: torch.Tensor
images, labels = next(iter(train_loader))

print(images.shape)
print(labels.shape)
print(labels)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;torch.Size([32, 3, 224, 224])
torch.Size([32])
tensor([0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0,
        0, 0, 0, 0, 0, 1, 1, 1])
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;We define a convolutional neural network (CNN) to learn patterns in images and classify them into two categories.
The convolution layers act as feature extractors, detecting simple patterns like edges and textures first,
then combining them into more complex features. Pooling layers reduce the spatial size of the image,
which makes the model more efficient and helps it focus on the most important information.&lt;/p&gt;
&lt;p&gt;After feature extraction, we flatten the data and pass it through fully connected layers, which act like a decision-maker.
These layers take the extracted features and learn how they relate to the final classes.
The last layer outputs two values, one for each class, which the model uses to make a prediction.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
class SnifferCNN(nn.Module):
    def __init__(self) -&amp;gt; None:
        super().__init__()

        self.conv_layers = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )

        self.fc_layers = nn.Sequential(
            nn.Flatten(),
            nn.Linear(32 * 56 * 56, 128),
            nn.ReLU(),
            nn.Linear(128, 2),
        )

    def forward(self, x: torch.Tensor) -&amp;gt; torch.Tensor:
        x = self.conv_layers(x)
        x = self.fc_layers(x)
        return x
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;We create the model and move it to the CPU so it is ready to process data.
Printing the model shows its full structure, including all layers and their order.
This helps us verify that the network was built correctly and understand how input data flows through it from convolution layers to the final classification output.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;device: torch.device = torch.device(&quot;cpu&quot;)
model: SnifferCNN = SnifferCNN().to(device)
print(model)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SnifferCNN(
  (conv_layers): Sequential(
    (0): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (4): ReLU()
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc_layers): Sequential(
    (0): Flatten(start_dim=1, end_dim=-1)
    (1): Linear(in_features=100352, out_features=128, bias=True)
    (2): ReLU()
    (3): Linear(in_features=128, out_features=2, bias=True)
  )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;We define the loss function and the optimizer, which are two key parts of training a neural network.&lt;/p&gt;
&lt;p&gt;The loss function measures how wrong the model&apos;s predictions are compared to the true labels.
Here we use CrossEntropyLoss, which is commonly used for classification problems (like deciding which class an image belongs to).&lt;/p&gt;
&lt;p&gt;The optimizer controls how the model learns from that error. We use Adam, which updates the model&apos;s parameters step by step to reduce the loss over time.&lt;/p&gt;
&lt;p&gt;The learning rate (lr=0.001) controls the size of each update step:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If it is too high, the model changes too aggressively and may miss the best solution.&lt;/li&gt;
&lt;li&gt;If it is too low, learning becomes very slow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Together, the loss function and optimizer define how the model improves during training.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
loss_function: nn.CrossEntropyLoss = nn.CrossEntropyLoss()
optimizer: optim.Adam = optim.Adam(
    model.parameters(),
    lr=0.001,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;We train the model by showing it many batches of images and gradually adjusting its internal parameters to reduce errors.
Each epoch means one full pass through the training dataset.&lt;/p&gt;
&lt;p&gt;For each batch, we send the images through the model to get predictions, then compare those predictions to the correct labels using the loss function.
This tells us how wrong the model is.&lt;/p&gt;
&lt;p&gt;We then reset previous gradients, compute new gradients using &lt;code&gt;backward()&lt;/code&gt;, and update the model using the optimizer.
This is the step where the model actually learns by slightly adjusting its weights.&lt;/p&gt;
&lt;p&gt;At the end of each epoch, we compute the average loss.
A decreasing loss over time indicates that the model is improving its predictions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;epochs: int = 5

epoch: int
for epoch in range(epochs):
    model.train()
    total_loss: float = 0.0

    batch_images: torch.Tensor
    batch_labels: torch.Tensor
    for batch_images, batch_labels in train_loader:
        batch_images = batch_images.to(device)
        batch_labels = batch_labels.to(device)

        outputs: torch.Tensor = model(batch_images)
        loss: torch.Tensor = loss_function(outputs, batch_labels)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    print(f&quot;Epoch {epoch + 1}, Loss: {total_loss / len(train_loader):.4f}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Epoch 1, Loss: 0.6052
Epoch 2, Loss: 0.3601
Epoch 3, Loss: 0.2964
Epoch 4, Loss: 0.2346
Epoch 5, Loss: 0.1701
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;We test the model by switching it into evaluation mode so it behaves correctly during inference (for example, disabling training-specific behavior like dropout if it existed).&lt;/p&gt;
&lt;p&gt;We take a single image from the test dataset and prepare it in the same format the model expects.
Since the model processes batches, we add an extra dimension so the image becomes a batch of size 1.&lt;/p&gt;
&lt;p&gt;We then run the image through the model without tracking gradients, because we are not training anymore.
The model outputs scores for each class, and we select the class with the highest score as the prediction.&lt;/p&gt;
&lt;p&gt;Finally, we compare the predicted label with the true label and display the image along with both values so we can visually check whether the model is performing correctly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model.eval()

image: torch.Tensor
label: int
image, label = test_dataset[0]

image_input: torch.Tensor = image.unsqueeze(0).to(device)

with torch.no_grad():
    output: torch.Tensor = model(image_input)
    predicted: torch.Tensor
    _, predicted = torch.max(output, 1)

true_class: str = test_dataset.classes[label]
predicted_class: str = test_dataset.classes[predicted.item()]

print(&quot;True label:&quot;, true_class)
print(&quot;Predicted:&quot;, predicted_class)

title: str = f&quot;True: {true_class} | Pred: {predicted_class}&quot;

plt.imshow(image.permute(1, 2, 0).numpy())
plt.title(title)
plt.axis(&quot;off&quot;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;True label: chihuahua
Predicted: chihuahua
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;We evaluate the model on multiple random test images to get a better sense of its behavior instead of looking at just one prediction.&lt;/p&gt;
&lt;p&gt;We first switch the model to evaluation mode so it behaves consistently during inference.
Then we randomly select 10 images from the test dataset to avoid bias and to see varied examples.&lt;/p&gt;
&lt;p&gt;For each image, we prepare it in the correct format by adding a batch dimension and passing it through the model without computing gradients.
The model outputs raw scores, which we convert into probabilities using softmax so we can interpret them as confidence levels for each class.&lt;/p&gt;
&lt;p&gt;We then display each image along with its true label and the model&apos;s predicted probability for every class.
This makes it easier to see not only whether the model is correct, but also how confident it is in its predictions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model.eval()

indices: list[int] = random.sample(range(len(test_dataset)), 10)

plt.figure(figsize=(14, 10))

i: int
idx: int
for i, idx in enumerate(indices):
    image: torch.Tensor
    label: int
    image, label = test_dataset[idx]

    input_tensor: torch.Tensor = image.unsqueeze(0).to(device)

    with torch.no_grad():
        output: torch.Tensor = model(input_tensor)
        probabilities: torch.Tensor = F.softmax(output, dim=1)[0]

    prob_text: str = &quot;\n&quot;.join(
        f&quot;{name}: {probabilities[j].item():.2f}&quot;
        for j, name in enumerate(test_dataset.classes)
    )

    true_class: str = test_dataset.classes[label]

    plt.subplot(2, 5, i + 1)
    plt.imshow(image.permute(1, 2, 0).numpy())
    plt.title(f&quot;True: {true_class}\n{prob_text}&quot;, fontsize=8)
    plt.axis(&quot;off&quot;)

plt.tight_layout()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;We use this code to test the model on completely new images and understand how it behaves on real inputs outside the dataset.&lt;/p&gt;
&lt;p&gt;We first collect all images from a folder and load the trained model in evaluation mode so predictions are consistent.
Each image is preprocessed using the same transformations as during training so the model receives data in the correct format.&lt;/p&gt;
&lt;p&gt;For each image, we run inference to get both the predicted class and the probability for each possible class.
These probabilities come from applying softmax, which converts raw model scores into values between 0 and 1 that sum to 1.&lt;/p&gt;
&lt;p&gt;We then visualize three things side by side: the original image, the transformed version that the model actually sees, and a bar chart showing the model&apos;s confidence for each class.
This helps us understand not only what the model predicts, but also how certain it is.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
img_paths: list[Path] = sorted(Path(&quot;./data/others&quot;).glob(&quot;*.jpg&quot;))
img_paths: list[Path] = sorted(
    p for ext in (&quot;*.jpg&quot;, &quot;*.jpeg&quot;, &quot;*.png&quot;)
    for p in Path(&quot;./data/others&quot;).glob(ext)
)

model.eval()

n_cols: int = 3
n_rows: int = len(img_paths)
plt.figure(figsize=(15, 5 * n_rows))

img_path: Path
for i, img_path in enumerate(img_paths):
    image: Image.Image = Image.open(img_path).convert(&quot;RGB&quot;)
    image_tensor: torch.Tensor = cast(torch.Tensor, transform(image))

    input_tensor: torch.Tensor = image_tensor.unsqueeze(0).to(device)

    with torch.no_grad():
        output: torch.Tensor = model(input_tensor)
        predicted: torch.Tensor
        _, predicted = torch.max(output, 1)
        probabilities: torch.Tensor = F.softmax(output, dim=1)

    class_name: str = test_dataset.classes[predicted.item()]

    probs: list[float] = [p.item() for p in probabilities[0]]

    # left: original image
    plt.subplot(n_rows, n_cols, i * n_cols + 1)
    plt.imshow(image)
    plt.title(f&quot;Original\n{img_path.name}&quot;, fontsize=9)
    plt.axis(&quot;off&quot;)

    # middle: transformed image (denormalized for display)
    plt.subplot(n_rows, n_cols, i * n_cols + 2)
    plt.imshow(image_tensor.permute(1, 2, 0).numpy())
    plt.title(f&quot;Transformed\nPrediction: {class_name}&quot;, fontsize=9)
    plt.axis(&quot;off&quot;)

    # right: probability bar chart
    plt.subplot(n_rows, n_cols, i * n_cols + 3)
    plt.barh(test_dataset.classes, probs)
    plt.xlim(0, 1)
    plt.xlabel(&quot;Probability&quot;)
    plt.title(&quot;Class probabilities&quot;, fontsize=9)

plt.tight_layout()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;We evaluate the model on the test dataset to measure how well it generalizes to unseen data. We switch the model to evaluation mode so it behaves correctly during inference, and we disable gradient tracking because no learning happens during testing.&lt;/p&gt;
&lt;p&gt;We loop through the test data in batches, pass each batch through the model, and take the class with the highest score as the prediction.
We compare predictions with the true labels and keep a running count of correct predictions and total samples.&lt;/p&gt;
&lt;p&gt;We then compute accuracy by dividing correct predictions by the total number of samples.
This gives a single metric that summarizes model performance on the test set.&lt;/p&gt;
&lt;p&gt;Finally, we also print the total number of trainable parameters in the model to understand its size and complexity.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model.eval()

correct: int = 0
total: int = 0

with torch.no_grad():
    batch_images: torch.Tensor
    batch_labels: torch.Tensor
    for batch_images, batch_labels in test_loader:
        batch_images = batch_images.to(device)
        batch_labels = batch_labels.to(device)

        outputs: torch.Tensor = model(batch_images)

        predicted: torch.Tensor
        _, predicted = torch.max(outputs, 1)

        total += batch_labels.size(0)
        correct += int((predicted == batch_labels).sum().item())

accuracy: float = correct / total
print(f&quot;Test Accuracy: {accuracy:.4f}&quot;)

total_params: int = sum(p.numel() for p in model.parameters())
print(f&quot;Total parameters: {total_params:,}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Test Accuracy: 0.8446
Total parameters: 12,850,530
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test accuracy of 0.8446 means the model correctly classified about 84.46% of the images in the test dataset.
This gives an estimate of how well the model generalizes to new, unseen data.
The remaining errors show cases where the model made incorrect predictions, which can happen due to similar-looking classes, limited training data, or model limitations.&lt;/p&gt;
&lt;p&gt;The model has 12,850,530 parameters, which represents the number of learnable weights and biases inside the network.
This value reflects the model&apos;s capacity: more parameters allow the model to learn more complex patterns, but also increase memory usage and the risk of overfitting if the dataset is too small.&lt;/p&gt;
&lt;p&gt;For comparison:&lt;/p&gt;
&lt;p&gt;This model: ~13 million parameters
Small language models (like BERT-base): ~110 million parameters (about 8x bigger)
Large language models (like GPT-3): ~175 billion parameters (over 10,000x bigger)
Very large modern models: even larger, but exact sizes are not always public&lt;/p&gt;
&lt;p&gt;So this image model is relatively small compared to modern language models, but still large enough to learn useful visual patterns.&lt;/p&gt;
&lt;h1&gt;Going further&lt;/h1&gt;
&lt;p&gt;There are several practical ways to improve this image model and deepen my understanding of how larger models work.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;more parameters (animals): Make the model bigger (more layers or filters). This can help it learn more detailed patterns, like differences between similar animals, but it also needs more data.&lt;/li&gt;
&lt;li&gt;add normalization: Standardize the input data so values are in a consistent range. This usually makes training more stable and faster.&lt;/li&gt;
&lt;li&gt;larger dataset (fine tuning): Train on more images or start from a model that was already trained on a large dataset. Then adjust it for your specific task, similar to how LLMs are adapted for different uses.&lt;/li&gt;
&lt;li&gt;Data augmentation: Create variations of training images (flip, rotate, crop, color change). This helps the model generalize better and reduces overfitting.&lt;/li&gt;
&lt;li&gt;More training epochs + monitoring loss: Train longer while tracking validation loss to avoid stopping too early or overfitting.&lt;/li&gt;
&lt;li&gt;Better model architecture: Replace the simple CNN with a pretrained model like ResNet or EfficientNet. These are much stronger and already trained on large datasets.&lt;/li&gt;
&lt;li&gt;Learning rate tuning: Try different learning rates. Small changes can significantly affect performance.&lt;/li&gt;
&lt;li&gt;Validation set: Split test data into validation + test. Validation is used during training to tune settings, test is only for final evaluation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Thoughts&lt;/h1&gt;
&lt;p&gt;This exercise was done in an afternoon.
I am happy I did it, it helped me gain vocabulary, understand what an example setup looks like, and get a first practical sense of how a model is created and trained.&lt;/p&gt;
&lt;p&gt;I feel like there is still a lot more to understand because there are many different types of neural network models, and PyTorch does most of the heavy lifting.
I have yet to truly understand the maths behind the library&apos;s functions, which is probably the next important step.&lt;/p&gt;
</content:encoded></item><item><title>Reclaiming my time</title><link>https://kaonashi.dev/blog/reclaimingmytime/</link><guid isPermaLink="true">https://kaonashi.dev/blog/reclaimingmytime/</guid><description>How I cut the brainrot, small steps at a time</description><pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I was talking to a Gen Z friend the other day about our phone screen time, specifically social media such as TikTok and Instagram.
She mentioned hers was over 10 hours a week, which is a little less than a full waking day.
That&apos;s enormous ! She justified it by saying it&apos;s mostly commute time, and the rest of the time spent is done volontary.
But is it really ?&lt;/p&gt;
&lt;h1&gt;Will versus Dark Patterns&lt;/h1&gt;
&lt;p&gt;Those platforms make money from your time and data. You log in, post and read content, see ads, and your behavior is shared so you can be shown more relevant ads.
How did they get you to spend time on those platforms?&lt;/p&gt;
&lt;p&gt;Thousands of brilliant brains have worked for years on how to make you stay on social media.
The design is a well thought out loop. Infinite scrolling without friction delivers quick and variable rewards which are thought to engage the brain reward system, with feeds tailored to your preferences.
This can be compared to a &lt;a href=&quot;https://en.wikipedia.org/wiki/Operant_conditioning_chamber&quot;&gt;Skinner Box&lt;/a&gt; with extra steps, and you&apos;re the rat.&lt;/p&gt;
&lt;p&gt;Can we talk about social media addiction then? I dug a little and found a meta analysis titled &lt;a href=&quot;https://www.sciencedirect.com/science/article/pii/S0306460321000307&quot;&gt;&quot;Prevalence of social media addiction across 32 nations: Meta-analysis with subgroup analysis of classification schemes and cultural values&quot;&lt;/a&gt;.
Its sources come from 63 independent samples with 34,798 respondents from 32 nations spanning seven world regions.&lt;/p&gt;
&lt;p&gt;Here is the summary:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The pooled prevalence estimate was 5% (95% CI: 3%-7%) for studies adopting monothetic or strict monothetic classifications. A higher pooled prevalence estimate (13%; 95% CI: 8%-19%) was found for studies adopting a cutoff for severe level or strict polythetic classifications, and that estimate was even higher (25%; 95% CI: 21%-29%) for studies adopting a cutoff for moderate level or polythetic classifications.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In simpler words, researchers look at social media addiction using six signs: Thinking about it constantly, using it to change mood, needing more and more, feeling bad when you cannot use it, it causing problems in life, and going back to heavy use after trying to stop.
Depending on how strictly you define addiction, between 5% and 25% of people meet the criteria.
At most, one in four people could be considered addicted.&lt;/p&gt;
&lt;p&gt;You can check BSMAS Test on the internet if you&apos;re curious about your addiction level.
Note that messenging apps do count.
&lt;a href=&quot;https://www.ampsych.com.au/assessments/bsmas&quot;&gt;This one&lt;/a&gt; is an example which does not require login but does store anonymous cookies.&lt;/p&gt;
&lt;p&gt;But maybe you are not one of those people. Maybe you are not addicted. So it must be fine to use social media as you please, right?
Now what if there were other effects beyond using your time and selling your data?&lt;/p&gt;
&lt;h1&gt;Shorts and the brain&lt;/h1&gt;
&lt;p&gt;The other day I stumbled upon (or, youtube algorithm chose that I watch) this video, &lt;a href=&quot;https://www.youtube.com/watch?v=Wo4-eE12bUA&amp;amp;t=82s&quot;&gt;&quot;TikTok and the brain: what 70 studies really say&quot;&lt;/a&gt;.
The channel Scilabus is run by a doctor in science communication who explains scientific subjects.
The video discusses a meta analysis titled &quot;&lt;a href=&quot;https://psycnet.apa.org/fulltext/2026-89350-001.html&quot;&gt;Feeds, Feelings, and Focus: A Systematic Review and Meta-Analysis Examining the Cognitive and Mental Health Correlates of Short-Form Video Use&lt;/a&gt;&quot; which includes data from 98,299 participants across 71 studies.&lt;/p&gt;
&lt;p&gt;If you do not want to read the full paper, here are some key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;This systematic review and meta-analytic investigation found that SFV (Short-Form Video) use was associated with poorer cognition (attention, inhibitory control, language, memory, and working memory) and most mental health indices except body image and self-esteem.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;Moderator analyses revealed that these associations were consistent across youth and adult populations but were strongest when SFV use was measured via addiction scales and when general SFV use (as opposed to TikTok-specific use) was assessed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;The continuous cycle of swiping and receiving new, emotionally stimulating content has been proposed to trigger dopamine release, creating a reinforcement loop that contributes to patterns of habitual use and greater emotional reliance on digital interactions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;Tasks requiring prolonged concentration (e.g., reading comprehension, complex problem solving) may be more difficult to sustain, especially as SFV
platforms reinforce brief, high-reward interactions through rapid feedback and algorithmic content delivery.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I did not start by reading meta analyses though. I started with simple steps to break the habit so I could spend time on things I value more.&lt;/p&gt;
&lt;h1&gt;Analysis of my screen time&lt;/h1&gt;
&lt;p&gt;My screen time in a week was high considering I work on a computer all day.
Add scrolling during commute time on Instagram and Reddit, plus messaging apps such as Messenger, WhatsApp, and Signal.
Add bad days where I came home and scrolled until I manage to get up from the couch.
All combined, I reached up to eight hours a week on my phone.&lt;/p&gt;
&lt;p&gt;Screen time information is easy to find. Apple and Android provide it in settings.
I started checking it every week, which made me realize how much time I was spending.&lt;/p&gt;
&lt;h1&gt;Adding friction&lt;/h1&gt;
&lt;p&gt;Countering seamless navigation is simple. You add friction and force yourself to think.&lt;/p&gt;
&lt;p&gt;I set up three methods:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, I added a daily limit of one hour for Reddit and Instagram. After an hour, the phone asks whether to continue. It is small, but it creates a break.&lt;/li&gt;
&lt;li&gt;Second, my boyfriend would ask what I was doing when I stayed on my phone too long. Then he would ask if I was enjoying my time. That worked only when we were together.&lt;/li&gt;
&lt;li&gt;Third, and most effective, was an app called &lt;a href=&quot;https://one-sec.app/&quot;&gt;OneSec&lt;/a&gt;. I set it to trigger every ten minutes and when opening Reddit or Instagram. It asks you to breathe for a few seconds before access. I used it for a year. It prevented me from opening those apps 2569 times, which it estimates as 5.4 days saved.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My usage decreased by half. I barely used Instagram and preferred text on Reddit.&lt;/p&gt;
&lt;p&gt;I also watched a video titled  &lt;a href=&quot;https://www.youtube.com/watch?v=bNOol5OTasw&quot;&gt;&quot;You&apos;re not addicted to tiktoks/reels, you&apos;re addicted to the scrolling&quot;&lt;/a&gt;.
She describes opening Reddit through browser tabs sorted by top of the month. The content gradually becomes less interesting, so the brain receives fewer rewards.
That&apos;s a little tedious but seems to work well for her.&lt;/p&gt;
&lt;p&gt;I also asked myself whether I was enjoying my time. The answer was not much.
On Instagram, I saw cute animals, a few places, some hobbies. It became repetitive.
Reddit often pushed stressful news instead of something pleasant. I don&apos;t care to see that the US president shat himself publicly once again.
So on the whole, I was more being stressed by grave news than happy to see kittens.&lt;/p&gt;
&lt;p&gt;I must also recommend this article, &lt;a href=&quot;https://aaronfrancis.com/2024/an-argument-for-logging-off-9a4de45b&quot;&gt;An argument for logging off&lt;/a&gt;, which tries to make you think about the influence you have over the information you get in you daily life.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Caring about something over which you have no agency is stressful&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Unless you intend to influence national politics, what is the point of checking every day?&lt;/p&gt;
&lt;h1&gt;Data Awareness Prompted the Uninstall&lt;/h1&gt;
&lt;p&gt;At some point, Meta updated their privacy policy. When I logged into Instagram I received a notification.&lt;/p&gt;
&lt;p&gt;That was a wake up call. They let you choose to be the product or the customer.
I read &lt;a href=&quot;https://www.facebook.com/privacy/policy/&quot;&gt;their privacy policy&lt;/a&gt; to understand what that meant.
It took hours of reading and searching. I will go into more detail in a future post.&lt;/p&gt;
&lt;p&gt;After that, I uninstalled Instagram.
I also installed &lt;a href=&quot;https://signal.org/&quot;&gt;Signal&lt;/a&gt; and encouraged friends to switch.
Their &lt;a href=&quot;https://signal.org/legal/&quot;&gt;privacy policy&lt;/a&gt; is quite straight foreward, it states:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Privacy of user data. Signal does not sell, rent or monetize your personal data or content in any way - ever.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Further steps&lt;/h1&gt;
&lt;p&gt;At that point, I was free from Instagram doomscrolling, and almost free from short videos. Almost.
I do use YouTube on my desktop computer, and since 2020 YouTube Shorts have been taking more and more space on the website.
I would sometimes get lost in them for an hour instead of relaxing properly.
I installed the extension &lt;a href=&quot;https://chromewebstore.google.com/detail/remove-youtube-shorts/mgngbgbhliflggkamjnpdmegbkidiapm?hl=en&quot;&gt;Remove YouTube Shorts&lt;/a&gt;,
which simply hides them from sight. Booyah, no more easy access to shorts !&lt;/p&gt;
&lt;p&gt;My daily commute totals two hours. I used to organize my life during that time, and when that was done I would go on social media.
I tried phone games and puzzles, but my interest never lasted long.
I invested 100€ in an e-reader that fits in my pocket. It has been a good decision so far, I now spend that time reading books I enjoy.&lt;/p&gt;
&lt;p&gt;A few months ago I got the flu and spent the day lying on the couch scrolling through Reddit.
At the end of the day I felt I had wasted it, letting my brain rot and not even resting properly.
I decided to uninstall Reddit the next day. At that point I had no more apps to scroll.
Since then I have not felt the need to go back. I occasionally spend a few minutes on Reddit&apos;s website when I am waiting for something without my e-reader available.&lt;/p&gt;
&lt;p&gt;There are a few more distractions I try to remove.
One is email spam. I unsubscribe from every newsletter. I repeat this every few years to make sure I am only bothered for a reason.
Another is phone calls. France has a service called &lt;a href=&quot;https://www.bloctel.gouv.fr/&quot;&gt;Bloctel&lt;/a&gt; which helps with cold calls.
On top of that, I use a French app called &lt;a href=&quot;https://applications-et-logiciels.orange.fr/applications/orange-telephone&quot;&gt;Orange Phone&lt;/a&gt;.
I gave it access to incoming numbers, and it blocks unwanted calls reported by users. I have had good results with it.&lt;/p&gt;
&lt;p&gt;So what&apos;s next ? I still use messaging apps daily to communicate with friends and I enjoy doing so.
One thing still bothers me, the disruptiveness of notifications. If my phone is beside me and I see it light up, my brain pauses and shifts focus.
Even though I keep it in silent mode most of the time, seeing the screen is enough to interrupt my train of thought.
The solution is simple. I need to keep my phone away more often and pick it up only when I intend to.&lt;/p&gt;
&lt;h1&gt;Final thoughts&lt;/h1&gt;
&lt;p&gt;Time is one of the only finite resource you possess, you should use it to do things that make you happy and/or proud.
I hope reading this helped questioning your own usage of social media.&lt;/p&gt;
</content:encoded></item><item><title>Why a blog</title><link>https://kaonashi.dev/blog/whyablog/</link><guid isPermaLink="true">https://kaonashi.dev/blog/whyablog/</guid><pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This post is a wink at &lt;a href=&quot;https://www.krayorn.com/posts/1-why-a-blog/&quot;&gt;Why a blog&lt;/a&gt; by Krayorn.&lt;/p&gt;
&lt;h1&gt;How I started writing&lt;/h1&gt;
&lt;p&gt;I won&apos;t lie, the main reason I worked on a blog is that Krayorn harassed me about making one.
He&apos;s even been boasting about it &lt;a href=&quot;https://www.krayorn.com/posts/friends-blogging/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But let&apos;s get back a little further than that.
Two years ago, I had a rather philosophical debate (still with Krayorn) about positivity and negativity, and how to become more positive.&lt;/p&gt;
&lt;p&gt;To put this in context, I&apos;m a very fact-driven person, which is, I think, a great advantage in the tech world.
Say you have made a feature for your app, you&apos;ll need to think of all the cases that could go wrong to prevent them from happening. Well, I think I&apos;m doing ok applying &lt;a href=&quot;https://en.wikipedia.org/wiki/Murphy%27s_law&quot;&gt;Murphy&apos;s law&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The drawbacks of this are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I may become a downer for others: instead of hyping up a project, I&apos;ll state everything that could go wrong.&lt;/li&gt;
&lt;li&gt;When presenting my own projects, I&apos;ll want to start with everything I messed up rather than simply listing what I&apos;ve accomplished, because I convince myself it&apos;s better to be factual and truthful. Because of this alone, I&apos;d make a terrible salesperson.&lt;/li&gt;
&lt;li&gt;Sometimes, it becomes an easy excuse to simply be negative on some subjects. &quot;You see, there are way too many problems with this idea, so why bother?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wanted to keep my critical thinking while improving my communication skills, and hopefully become a more positive person. That&apos;s a tough goal. Communication skills I can deal with, but becoming more positive felt like asking a depressive person to &quot;just be happy&quot;. I was reluctant about the possibility of it happening at all, but still wanted to try.
After hours of discussion, we came to an agreement that I should try to keep a gratitude journal.&lt;/p&gt;
&lt;p&gt;At some point during the holidays, I used the &quot;journal&quot; app on my phone, linked it to a picture I had just taken, and thought hard about nice things I could say about it.
So on Thursday, 25 January 2024, my first post was:&lt;/p&gt;
&lt;p&gt;That was no easy first step, but it was the start of something. I kept going and added some highlights from my days. At some point, I started writing more and more (full sentences, I swear) about anything that happened that day.
I kept writing in my journal every day for about a year. After that, it became scarce, and I finally stopped writing in the journal app in October 2025.&lt;/p&gt;
&lt;p&gt;You know that feeling when you open your fridge and ask yourself, &quot;What was I fetching already?&quot; That decreased greatly. It was a great memory exercise, and there were fewer &quot;the heck did I do today?&quot; moments.&lt;/p&gt;
&lt;p&gt;Apart from that, I am still uncertain whether I became more positive afterwards. Maybe I struggle a little less to find good things that happened in a day now. And maybe that&apos;s what being more positive is about, my core functioning hasn&apos;t changed.&lt;/p&gt;
&lt;p&gt;I stopped journaling in my phone app but kept writing on paper, logging my vacations, and writing holiday letters.
At that time, the idea of starting a blog crept back into the back of my mind.&lt;/p&gt;
&lt;h1&gt;Why a blog&lt;/h1&gt;
&lt;p&gt;Writing will surely improve my English, which is a cool bonus.
However, I&apos;m proficient enough to navigate the corporate world at my current level, and I do not intend to become an author, so there&apos;s no real objective here for me.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imperva.com/blog/2025-imperva-bad-bot-report-how-ai-is-supercharging-the-bot-threat/&quot;&gt;Half of the internet traffic is now bots&lt;/a&gt;,
no doubt, this will only increase in the future. A growing number of websites will be AI-generated.
I also like that I can add a drop of human content to an ocean of bots.
But that&apos;s not a fight I can have a big impact on.&lt;/p&gt;
&lt;p&gt;So what are my motivations?&lt;/p&gt;
&lt;p&gt;When I say I do 3D printing as a hobby, I&apos;m always asked for details. This is not yet a widespread hobby, so I need to show examples. I&apos;ll go through my phone and scroll through it, trying to find one to three examples.
I ended up making a picture folder with a few examples. But that&apos;s not enough because some people want to show their friends, and I haven&apos;t put that on any social media.
How convenient would it be if I had a link I could share with everything I&apos;ve done?&lt;/p&gt;
&lt;p&gt;Ok, we have a first motivator. But that&apos;s a little light, let&apos;s think harder.&lt;/p&gt;
&lt;p&gt;When I reflect on my flaws, there&apos;s one thing that strikes me: I&apos;m not good at debating.
Of course, talking a little louder would help, but that&apos;s not how I want to communicate.
To improve, I see two paths: Improve my improvisation skills or prepare the topics that matter to me.&lt;/p&gt;
&lt;p&gt;Writing about a subject means you have to think hard about it, maybe search for more information to consolidate your thoughts.
You should end up having clear, well-thought-out arguments that will have more impact in a debate than loud, imprecise claims.&lt;/p&gt;
&lt;p&gt;That&apos;s a second motivator that could have an impact on the few subjects I chose to write about.
Considering I&apos;ll write about what matters to me, this should be a great starting point.&lt;/p&gt;
&lt;p&gt;I like the idea that this place on the internet is mine: No ads, no cookies, no pop-ups. Just me, my projects, and thoughts.
I can put whatever I want on it, but more importantly, the content is mine.
It doesn&apos;t belong to any big corporation&apos;s app that can be bought by any rich guy with a criminal record.
This counts as a third motivator.&lt;/p&gt;
&lt;p&gt;Let&apos;s say I make a blog, except for 3D printing, what would I even talk about ?
Well, in that case, I already had 10 ideas before getting started.
I&apos;m bad at falling asleep. It takes time, even if I spend the day out, eat correctly, sleep early, and so on...
My mind wanders a lot, either summing up what happened that day or going on any random thought.
These thoughts are pretty clear, I just didn&apos;t put them to use by taking notes the next day.
It&apos;s time they stopped going to waste.&lt;/p&gt;
&lt;h1&gt;What now ?&lt;/h1&gt;
&lt;p&gt;Well, I&apos;ve made it. I&apos;ve published my blog already!
There&apos;s very little content yet, but I intend on posting more.
Hopefully, link some of my projects to it soon.&lt;/p&gt;
</content:encoded></item><item><title>A try at Pixel Art</title><link>https://kaonashi.dev/blog/pixelarttry/</link><guid isPermaLink="true">https://kaonashi.dev/blog/pixelarttry/</guid><description>I&apos;ve been curious about pixel art lately, so I started doing a few pieces here and there, I&apos;m a complete newbie but still happy to share some of them here.</description><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;December 2025&lt;/h2&gt;
&lt;p&gt;&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Novembre 2025&lt;/h2&gt;
&lt;p&gt;I needed a custom cursor for my website, so I used the default mac cursor and modified it with the colors of my website.
Who says website says favicon, so here&apos;s little pixelated Kaonashi.
I also made this podium for my article on &lt;a href=&quot;/blog/atributetovideogames/&quot;&gt;a tribute to video games&lt;/a&gt;.
&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Making some basic tiles
&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Here&apos;s playing with perspective
&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Here are the rest of the experiments I made.
I&apos;ve been trying to do some &lt;a href=&quot;https://www.pixilart.com/challenges&quot;&gt;pixel art daily challenge&lt;/a&gt; to improve
&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;At first I was on &lt;a href=&quot;https://store.steampowered.com/app/1204050/Pixel_Studio__pixel_art_editor/&quot;&gt;Pixel Studio&lt;/a&gt; and ended up buying &lt;a href=&quot;https://www.aseprite.org/&quot;&gt;Aseprite&lt;/a&gt; because the use is just smoother.
I&apos;ve been asked to make a wallpaper for a pirate game, so layer by layer, with heavy inspiration from the internet, the project produced this stack of backgrounds and tent props:
The sky and clouds
&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
Now some birds and then the sea
&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
Sprinkle some sand and greens
&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
Let&apos;s add a plank and tents from where the pirates come out
&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;
And here&apos;s the first result&lt;/p&gt;
&lt;p&gt;The tent sprite when it takes damage
&amp;lt;div class=&quot;pixel-grid&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;It&apos;s nowhere near perfect but that looks like a good start !&lt;/p&gt;
</content:encoded></item><item><title>A tribute to Video Games</title><link>https://kaonashi.dev/blog/atributetovideogames/</link><guid isPermaLink="true">https://kaonashi.dev/blog/atributetovideogames/</guid><description>For those who came before</description><pubDate>Mon, 24 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My father is a gamer, he put my brother and me on a computer very early.
At two years old I was already on Adibou, and I was most likely a pro Tetris player by the time I was four.
I captured over 150 Pokémon at least six times before I was eight.
I spent many hours on various MMORPGs during my teenage years.
I&apos;ve crushed many heads on FPS to keep my sanity while getting my master&apos;s degree.&lt;/p&gt;
&lt;p&gt;Gaming has been my main hobby since childhood.
I want to write this post to remember the games I&apos;ve played,
the enjoyable moments, the friends I&apos;ve made (make it more cheesy and it&apos;ll turn into a Raclette),
and to acknowledge the titles that have shaped my life.&lt;/p&gt;
&lt;h2&gt;&quot;This is our story! Now let&apos;s see this thing through together!&quot; - Final Fantasy X&lt;/h2&gt;
&lt;p&gt;I&apos;m a PC MaStER rAcE player, not because I believe PC &amp;gt; Console, but because it was what I had access to for a while. I eventually got a DS and a Wii.
Emulators were available, so I used them to play older games (Thanks dad!).&lt;/p&gt;
&lt;p&gt;In my early years, I mostly played alone or with my younger brother, who hated losing.
I mean, I hate(d) losing too but let&apos;s be honest, I was the best.
We&apos;d go to war on Age of Mythology, continuously spamming &quot;Wololo&quot; at each other,
compete on Rayman Raving Rabbids, screaming &quot;Bwaaah&quot; while our parents watched, helpless at the chaos,
and tried to outperform each other in Mario Kart DS and Smash Bros.&lt;/p&gt;
&lt;h2&gt;&quot;Bonjour mon ami!&quot; - Clair Obscur: Expedition 33&lt;/h2&gt;
&lt;p&gt;In middle and high school, I discovered MMORPGs. I made many friends there, and even met two partners through different games at different times.
Dofus was my first MMO. I once organized an event that crashed the servers because too many players joined the same map.&lt;/p&gt;
&lt;p&gt;In highschool I remember being told by my english teacher that I would never achieve anything in life and would certainly never be able to speak english. Oh well.
I don&apos;t mean to brag when I say gaming has brought me a list of skills which helped me navigate life more easily:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My english level mainly comes from playing online games on non French servers.&lt;/li&gt;
&lt;li&gt;Taking down a raid requires planning, communication, and coordination.&lt;/li&gt;
&lt;li&gt;Patience and perseverance are key when you&apos;re on game such as Dark Souls.&lt;/li&gt;
&lt;li&gt;Portal makes your brain cells work overtime, that&apos;s puzzle solving for you.&lt;/li&gt;
&lt;li&gt;Curiosity tends to make you go towards that &quot;?&quot; lost in the map and you get rewarded for that (unless you&apos;re playing Ubisoft games).&lt;/li&gt;
&lt;li&gt;Having to debug an error message caused by a terrible anti-cheat system that leads you in the bios of your computer tends to give you the key to end up building your own machine a few years later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After high school, convinced by my parents that I needed a prestigious degree to earn good money, I enrolled in an international business school.
I did not fit in, neither did I want to fit in.
At the time, I was playing Final Fantasy XIV. My guild master was studying computer science and offered to show me around his school. I switched to that path the following year.&lt;/p&gt;
&lt;p&gt;Once in computer science school, a friend introduced me to Overwatch, which I still play occasionally.
This game can be truely fun when played with a group, either because you&apos;re try harding to win or because you&apos;re just chit chatting while poucing the other team.
I&apos;ve made multiple lifelong friends with that game in the background.
I love playing Ana, grandma healer sniper with some control skills.
One evening I used the group finder and posted something like &quot;Come play with grandma&quot;.
I teamed up with an American dude who played Zarya, the buffed as heck tank of the Motherland,
after a night full of victory we kept playing together for a while, we&apos;ve been pen pals ever since and still chat almost daily.&lt;/p&gt;
&lt;p&gt;I&apos;ve been told multiple times that it&apos;s not possible to create real, deep connections with people online.
I believe and know it&apos;s possible, I&apos;ve had the best random phylosophical talks with friends at night on Mumble and Discord.
Some people I met have had an impact on my life, just like I&apos;ve had an impact on theirs.
For instance, I became a programmer partially thanks to my Final Fantasy XIV Guild Master back in 2014.&lt;/p&gt;
&lt;h2&gt;&quot;I choose you!&quot; - Pokémon&lt;/h2&gt;
&lt;p&gt;When asked what my favorite games are, I definitely have a strong first and second, however the third place is a tie between several games.&lt;/p&gt;
&lt;p&gt;My favorite is The Witcher 3. After finishing the whole series and reading the books,
I realized that a story-driven RPG with moral complexity, strong music, and satisfying gameplay is what I enjoy the most.
Now add a cool ass main character, an open world and put that into a Dark Fantasy universe and I&apos;ll sign up.
My heart was broken when I originally finished the game and had the worst ending possible, so I reloaded a save 40 hours earlier to get a different ending.
I have over 200 hours in the game, including the DLC and completion attempts.&lt;/p&gt;
&lt;p&gt;The second place was taken recently by Clair Obscur: Expedition 33. All the boxes are ticked once again: story, music, gameplay, I only saw the sunlight again after completing the game.
It&apos;s great in all aspects cited above, but add to that the fact that they&apos;re a small french team and even if I&apos;m not very much of a patriot I was somehow proud of them.
I&apos;ve had the chance to go to their first concert in Paris and it was a remarkable experience, you could see the joy they had in sharing their work.&lt;/p&gt;
&lt;p&gt;For third place, the contenders are Baldur&apos;s Gate 3, Ori and the Blind Forest, Hades, Overwatch, and Final Fantasy XIV.
They are all strong in different ways, but I would choose Final Fantasy XIV due to the amount of days and nights I invested in it.
If I played it again now, I might not rate it as highly, since many expansions have changed the game significantly.&lt;/p&gt;
&lt;p&gt;&amp;lt;div class=&quot;podium-section&quot;&amp;gt;
&amp;lt;div class=&quot;podium-stage&quot;&amp;gt;
&amp;lt;img
src={Podium.src}
width={Podium.width}
height={Podium.height}
alt=&quot;Pixel art podium for top three games&quot;
class=&quot;podium-stage__base&quot;
loading=&quot;lazy&quot;
/&amp;gt;
&amp;lt;img
src={Witcher3Cover.src}
width={Witcher3Cover.width}
height={Witcher3Cover.height}
alt=&quot;The Witcher 3: Wild Hunt cover art&quot;
class=&quot;podium-cover podium-cover--first&quot;
loading=&quot;lazy&quot;
/&amp;gt;
&amp;lt;img
src={ClairObscurCover.src}
width={ClairObscurCover.width}
height={ClairObscurCover.height}
alt=&quot;Clair Obscur: Expedition 33 cover art&quot;
class=&quot;podium-cover podium-cover--second&quot;
loading=&quot;lazy&quot;
/&amp;gt;
&amp;lt;img
src={FFXIVCover.src}
width={FFXIVCover.width}
height={FFXIVCover.height}
alt=&quot;Final Fantasy XIV: A Realm Reborn cover art&quot;
class=&quot;podium-cover podium-cover--third&quot;
loading=&quot;lazy&quot;
/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;&quot;I used to be an adventurer like you&quot; - Skyrim&lt;/h2&gt;
&lt;p&gt;And I wish I can keep on adventuring like that!
Even though I have found new hobbies and go touch grass more regularly, I still play vdeo games and I intend to continue.
We&apos;re in an amazing timeline for that hobby, they have become more accessible, they&apos;re seen less as a danger to society (by television, find the bug),
more and more people are creating video games and even more people are playing them.
I am thankful for the teams who developed these titles, for the hours of entertainment, the memories, and the friends I&apos;ve made.
They have been a great escape from reality for a long while, now they complement my reality nicely, the cake is no longer a lie.&lt;/p&gt;
&lt;h2&gt;&quot;Reconstructing test chamber data.&quot; - Portal 2&lt;/h2&gt;
&lt;p&gt;Do you know what I like aside from video games? Data.
Data&apos;s fun, I grouped the games by genre to create the chart below.
I&apos;ll keep denying if someone says I have a type though.&lt;/p&gt;
&lt;p&gt;Game count (I have forgoten plenty that&apos;s for sure): {gameCovers.length}&lt;/p&gt;
&lt;p&gt;Here&apos;s to many more years of gaming!&lt;/p&gt;
</content:encoded></item></channel></rss>