<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
  <title>hotchpotch.dev</title>
  <link>https://hotchpotch.dev</link>
  <description>Notes and projects by Yuichi Tateno, a Japanese software engineer working on information retrieval.</description>
  <language>en</language>
  <lastBuildDate>Tue, 09 Jun 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>trn: A Fast Local Translation Command for macOS Tahoe</title>
  <link>https://hotchpotch.dev/articles/mac-translate-cli-trn/</link>
  <guid>https://hotchpotch.dev/articles/mac-translate-cli-trn/</guid>
  <description>I built trn, a small macOS translation CLI that uses Apple&apos;s on-device Translation framework, with a low-latency default that was about 12x faster than high-fidelity mode in my local README test.</description>
  <content:encoded><![CDATA[<p>After updating my Mac to macOS Tahoe 26, I became interested again in the APIs behind Apple's on-device intelligence features. Apple Intelligence is the visible product name, but the more interesting part for developers is that some of the underlying system capabilities are exposed as ordinary frameworks.</p>
<p>Translation is one of those capabilities. On Tahoe, the Translation framework can use two different strategies: a low-latency path and a high-fidelity path that uses Apple Intelligence when available. I wanted to see how practical that was from a small local tool, so I built <a href="https://github.com/hotchpotch/mac-translate-cli">mac-translate-cli</a>.</p>
<p>It provides a command named <code>trn</code>.</p>
<h2>What <code>trn</code> Does</h2>
<p><code>trn</code> is deliberately small. It only translates text. It uses the macOS built-in Translation framework and exposes it as a command-line tool.</p>
<p>On macOS Tahoe 26.4 or later, it can be installed with Homebrew:</p>
<pre><code class="hljs language-bash">brew tap hotchpotch/mac-translate-cli https://github.com/hotchpotch/mac-translate-cli
brew install hotchpotch/mac-translate-cli/trn
</code></pre>
<p>Basic usage:</p>
<pre><code class="hljs language-bash">trn --to ja <span class="hljs-string">"Hello world!"</span>
<span class="hljs-comment">#=> こんにちは、世界！</span>
</code></pre>
<p>It also reads from standard input:</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"Hello world!"</span> | trn --to ja
<span class="hljs-built_in">cat</span> notes.txt | trn --to en
pbpaste | trn --to ja | pbcopy
</code></pre>
<p>When <code>--from</code> is omitted, <code>trn</code> detects the source language from the input. Longer input is split into chunks, translated concurrently, and written back in the original order. The default concurrency is <code>4</code>.</p>
<p>The command depends on the language packages installed in macOS. If a required package is supported but missing, <code>trn</code> reports that it should be installed from System Settings.</p>
<h2>Why <code>low</code> Is the Default</h2>
<p><code>trn</code> has two quality modes:</p>
<pre><code class="hljs language-bash">trn --from en --to ja --quality low <span class="hljs-string">"Hello world!"</span>
trn --from en --to ja --quality high <span class="hljs-string">"Hello world!"</span>
</code></pre>
<p><code>low</code> is the default. It uses Apple's lower-latency traditional translation models. <code>high</code> uses Apple Intelligence high-fidelity translation when available.</p>
<p>I initially expected <code>high</code> to be the obvious default. After checking English/Japanese translation quality and speed, I changed my mind. The qualitative results were not simply "high is better." In a small hand-checked evaluation, direct English/Japanese translation averages were almost the same:</p>
<table>
<thead>
<tr>
<th>Direction</th>
<th align="right"><code>high</code> average</th>
<th align="right"><code>low</code> average</th>
</tr>
</thead>
<tbody>
<tr>
<td>English to Japanese</td>
<td align="right">3.83 / 5</td>
<td align="right">3.84 / 5</td>
</tr>
<tr>
<td>Japanese to English</td>
<td align="right">4.35 / 5</td>
<td align="right">4.32 / 5</td>
</tr>
</tbody>
</table>
<p>The details are in the <a href="https://github.com/hotchpotch/mac-translate-cli/blob/main/translation-quality-check.md">translation quality report</a>. It is not a comprehensive benchmark, but it was useful for making the default choice. <code>low</code> was sometimes better on practical terminology, including some finance and software examples. <code>high</code> was not uniformly better.</p>
<p>Neither mode should be trusted blindly. Proper names, roles, honorifics, and long round trips still produced drift and broken fragments. For publication, legal, financial, education, emergency, or other sensitive text, the output needs human review.</p>
<p>For ordinary English/Japanese translation, though, <code>low</code> was good enough often enough, and the speed difference was too large to ignore.</p>
<h2>Speed</h2>
<p>I translated the current <code>mac-translate-cli</code> <code>README.md</code> from English to Japanese five times for each quality mode.</p>
<table>
<thead>
<tr>
<th>Item</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Machine</td>
<td>MacBook Pro, Mac16,5</td>
</tr>
<tr>
<td>Chip</td>
<td>Apple M4 Max</td>
</tr>
<tr>
<td>CPU</td>
<td>16 cores: 12 performance cores and 4 efficiency cores</td>
</tr>
<tr>
<td>Memory</td>
<td>128 GB</td>
</tr>
<tr>
<td>OS</td>
<td>macOS 26.5</td>
</tr>
<tr>
<td><code>trn</code></td>
<td>0.1.2</td>
</tr>
<tr>
<td>Input</td>
<td><code>README.md</code>, 5,591 characters / 5,924 bytes</td>
</tr>
<tr>
<td>Command</td>
<td><code>trn --from en --to ja --quality &#x3C;low|high> &#x3C; README.md</code></td>
</tr>
<tr>
<td>Concurrency</td>
<td>default, <code>4</code></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Quality</th>
<th align="right">Mean wall time</th>
<th align="right">Input speed</th>
<th align="right">Relative speed</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>low</code></td>
<td align="right">3.560 s</td>
<td align="right">1,570.6 chars/s</td>
<td align="right">11.91x faster</td>
</tr>
<tr>
<td><code>high</code></td>
<td align="right">42.383 s</td>
<td align="right">131.9 chars/s</td>
<td align="right">1.00x</td>
</tr>
</tbody>
</table>
<p>For this README-sized input, <code>low</code> felt like a normal local utility. <code>high</code> felt like invoking a much heavier model. That is why <code>trn</code> defaults to <code>low</code>, while still keeping <code>high</code> available when I want to compare whether the high-fidelity path helps a specific text.</p>
<h2>What This Suggests About the Mac</h2>
<p>The interesting part of <code>trn</code> is not the CLI itself. It is that a system-level on-device model can be exposed through a tiny tool and still feel fast enough to use casually.</p>
<p>This does not mean local models replace large cloud models. Remote LLMs are still much stronger for many tasks. But smaller local models are becoming practical, even around the 1B-parameter scale, and Apple can optimize the hardware, OS, model runtime, and application APIs together.</p>
<p>Compared with buying expensive NVIDIA GPUs, a Mac is not cheap, but it is also not an exotic machine. Many developers already use one. If useful local models continue to become available at the OS level, and if normal apps and small tools can call them with low friction, that is a strong direction for the Mac as an edge machine-learning platform.</p>
<p><code>trn</code> is a small example: a focused local tool, backed by system-level machine learning, that improves everyday UX without requiring a remote service.</p>]]></content:encoded>
  <pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 08 Jun 2026 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>OpenAI API-Compatible Access Without Additional API Billing via Codex</title>
  <link>https://hotchpotch.dev/articles/openai-api-server-via-codex/</link>
  <guid>https://hotchpotch.dev/articles/openai-api-server-via-codex/</guid>
  <description>A Codex-authenticated OpenAI API-compatible server for Responses, Chat Completions, and image generation. Within the Codex subscription scope, it can be used without additional API usage billing.</description>
  <content:encoded><![CDATA[<p>I built a local server that uses Codex credentials and exposes an OpenAI API-compatible interface. In addition to the Responses API and Chat Completions API, the current version also supports <code>/v1/images/generations</code> for image generation, although that support is still not a complete implementation of the official OpenAI API surface.</p>
<p>The main benefit is that, within the scope of a Codex subscription, you can use an OpenAI API-compatible interface without additional OpenAI API usage billing. If your environment is already authenticated with Codex, you can start the compatible server with a single <code>uvx</code> command.</p>
<ul>
<li><a href="https://github.com/hotchpotch/openai-api-server-via-codex">https://github.com/hotchpotch/openai-api-server-via-codex</a></li>
</ul>
<pre><code class="hljs language-bash">uvx openai-api-server-via-codex
</code></pre>
<p>This starts a local server. By default it listens on <code>http://127.0.0.1:18080</code>, and clients that can call the OpenAI API can use endpoints such as <code>/v1/responses</code>, <code>/v1/chat/completions</code>, and <code>/v1/images/generations</code>. This is convenient when you want to use existing OpenAI SDKs or GUI clients.</p>
<p>For image generation, the server can be called through OpenAI SDK methods such as <code>client.images.generate(...)</code>. At the moment, however, responses are returned as base64 <code>b64_json</code>; URL responses, streaming, and image edit APIs are not supported. It is better to think of this as a practical compatibility layer for local experiments and personal tooling, not a complete replacement for the official OpenAI API.</p>
<p>For example, if you point a GUI client that supports the OpenAI API at the local server, it looks like this. The screenshot below uses a model that was available through Codex when the article was first published.</p>
<p><img src="https://storage.googleapis.com/secons-site-images/other/blog_images/20260506-openai-api-server-via-codex.webp" alt="OpenAI API compatible server screenshot"></p>
<hr>
<h2>Why Subscription-Scoped Use Matters</h2>
<p>OpenAI's official API usage is billed separately from ChatGPT and Codex subscriptions. For example, on the <a href="https://openai.com/api/pricing/">official OpenAI pricing page</a>, <code>gpt-5.5</code> is listed at 5.00 USD per 1M input tokens and 30.00 USD per 1M output tokens.</p>
<p>This server does not make the official OpenAI API free. It uses Codex credentials and exposes the models and execution environment available through Codex as a local OpenAI API-compatible interface. Within subscription limits and the applicable terms, that can make it possible to experiment with expensive model capabilities from local development tools or personal workflows without additional API usage charges.</p>
<p>This is especially useful when you want to keep using existing OpenAI SDK-compatible tools, or when you are iterating quickly on a local prototype. It should not be used to bypass limits, resell access, provide an API to third parties, or run the backend of a public service.</p>
<h2>How It Works</h2>
<p>This approach is also used by <a href="https://github.com/badlogic/pi-mono">Pi</a>, one of the agent execution engines used when OpenClaw runs through Codex. It uses Codex authentication information and calls the Codex API endpoint at <code>https://chatgpt.com/backend-api/codex</code>.</p>
<p>I was not sure whether it was acceptable to use this API endpoint directly, but after reading Simon Willison's article, <a href="https://simonwillison.net/2026/Apr/23/gpt-5-5/">A pelican for GPT-5.5 via the semi-official Codex backdoor API</a>, it seemed likely enough to be usable. "Semi-official" is a fitting description.</p>
<h2>Notes and Caveats</h2>
<p>This project is not an official OpenAI API. It is only a way to use Codex credentials that you are authorized to use and convert them into an OpenAI API-compatible shape in a local or development environment. Uses such as the following may violate OpenAI's terms:</p>
<ul>
<li>Using it to bypass ChatGPT or Codex usage limits</li>
<li>Providing it as an API to people other than yourself</li>
<li>Reselling or sharing a subscription</li>
<li>Using it as the backend for a public API or third-party service</li>
<li>Sharing Codex credentials or tokens with someone else</li>
</ul>
<p>The Codex backend may also change without notice. Request formats or model names that work today may suddenly stop working.</p>
<h2>Difference Between the Codex and Claude Code Subscription Positions</h2>
<p>In April 2026, <a href="https://www.theverge.com/ai-artificial-intelligence/907074/anthropic-openclaw-claude-subscription-ban">Claude Code announced that subscription use through tools such as OpenClaw would be prohibited</a>. In contrast, Codex has started publishing mechanisms such as <a href="https://developers.openai.com/codex/app-server">Codex App Server</a> and <a href="https://developers.openai.com/codex/sdk">Codex SDK</a>, at least for now, to support broader use even with subscriptions. The difference in stance is interesting.</p>
<p>Anthropic seems to be placing strict limits because inference resources for Claude are currently tight. OpenAI, on the other hand, gives the impression of having more room and being comparatively generous. That said, if OpenAI's resources become tight, or if competition weakens, it is entirely possible that the policy could become stricter or subscriptions could change in the future. It is hard to predict what will happen.</p>]]></content:encoded>
  <pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 20 May 2026 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Japanese Full-Text Search in SQLite and DuckDB with Vaporetto</title>
  <link>https://hotchpotch.dev/articles/sqlite-duckdb-vaporetto/</link>
  <guid>https://hotchpotch.dev/articles/sqlite-duckdb-vaporetto/</guid>
  <description>How to add Japanese full-text search to SQLite and DuckDB with Vaporetto, including extensions, a browser demo, and BM25 search examples.</description>
  <content:encoded><![CDATA[<p>With the rise of various agents, embedded databases such as SQLite and DuckDB have become increasingly interesting because they run without a server and persist as local files. I looked into how these databases handle Japanese full-text search. SQLite FTS5 can search with <a href="https://www.sqlite.org/fts5.html">trigrams</a>, but out of the box it does not seem to provide search specialized for Japanese vocabulary.</p>
<p>There are approaches using <a href="https://github.com/lindera/lindera">Lindera</a>, but this time I made extensions that embed <a href="https://github.com/daac-tools/vaporetto">Vaporetto</a>, a lightweight and fast tokenizer implemented in Rust.</p>
<ul>
<li>SQLite + Vaporetto
<ul>
<li><a href="https://github.com/hotchpotch/sqlite-vaporetto">https://github.com/hotchpotch/sqlite-vaporetto</a></li>
</ul>
</li>
<li>DuckDB + Vaporetto
<ul>
<li><a href="https://github.com/hotchpotch/duckdb-vaporetto">https://github.com/hotchpotch/duckdb-vaporetto</a></li>
</ul>
</li>
</ul>
<p>Vaporetto uses a <a href="https://www.anlp.jp/proceedings/annual_meeting/2022/pdf_dir/D2-5.pdf">pointwise prediction method</a>, judging character boundaries with a linear classification model. It can be used with a dictionaryless model, though dictionary-based models also exist, so it can keep the model size small depending on the use case.</p>
<p>I also made a technical demo that runs entirely in the browser, combining DuckDB and Vaporetto to perform full-text search sorted by BM25 relevance score. When the number of target texts is small, the benefits of full-text search, which stays fast as the number of documents grows, and BM25, which considers term frequency and document length, are less visible.</p>
<ul>
<li><a href="https://duckdb-vaporetto-wasm-demo.surge.sh/">https://duckdb-vaporetto-wasm-demo.surge.sh/</a></li>
</ul>
<p>As another example, I tried building article search for this blog, secon.dev, with SQLite + FTS5 + Vaporetto. For about 2,700 articles, BM25 search usually ran in around 3 ms.</p>
<p><img src="https://storage.googleapis.com/secons-site-images/other/blog_images/20260427-secon-dev-sqlite-search.webp" alt="secon.dev search example"></p>
<p>This makes casual Japanese full-text search possible with SQLite and DuckDB, so I think it can be useful when the use case fits.</p>
<hr>
<p>Update: I received a helpful comment from Kudo-san, a leading expert in morphological analysis. For full-text search, word segmentation by pointwise prediction is not well suited because inconsistency can be a problem. That makes sense.</p>
<blockquote>
<p>Pointwise word segmentation is not well suited to full-text search. Especially without a dictionary, context-dependent segmentation is unavoidable, which increases the risk of missed matches. I discuss this in my book on morphological analysis.</p>
</blockquote>
<ul>
<li><a href="https://x.com/taku910/status/2048676651517768163">https://x.com/taku910/status/2048676651517768163</a></li>
</ul>
<blockquote>
<p>Context dependence means, for example, that the segmentation of the phrase "morphological analysis" is uniquely determined without being affected by surrounding context. It is important that the query segmentation can be reproduced in the document. A unigram language model satisfies this condition. Accuracy is sacrificed, but consistency is guaranteed.</p>
</blockquote>
<ul>
<li><a href="https://x.com/taku910/status/2048695518176665637">https://x.com/taku910/status/2048695518176665637</a></li>
</ul>]]></content:encoded>
  <pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 27 Apr 2026 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Building a Machine Learning PC with Two RTX 5090 GPUs</title>
  <link>https://hotchpotch.dev/articles/rtx5090x2-pc/</link>
  <guid>https://hotchpotch.dev/articles/rtx5090x2-pc/</guid>
  <description>Notes from building a two RTX 5090 machine learning PC in Japan, including power supply constraints, cooling, parts, and multi-GPU training tradeoffs.</description>
  <content:encoded><![CDATA[<p>I like training small Transformer models, usually around 100M parameters or less, and I run training jobs often. I have built and used custom PCs with RTX 3090, 4090, and 5090 GPUs.</p>
<blockquote>
<p>[!NOTE]
I live in Japan, so some details in this build are specific to Japanese residential environments. For example, standard household wall outlets are usually 100 V and limited to 1500 W, which strongly affects power supply choices for a two-GPU machine.</p>
</blockquote>
<p>This time I wanted a little more training speed and also wanted to gain practical knowledge about multi-GPU training, so I built a custom PC with two RTX 5090 GPUs. Recently, NVLink has been removed from consumer GPUs, and power consumption has also increased. There are surprisingly few examples of systems using two RTX 5090s, so I had to research more than expected. This article summarizes the build. The example reflects the situation around the end of 2025.</p>
<p><a href="https://storage.googleapis.com/secons-site-images/photo/large/20260118_L1001763.webp"><img src="https://storage.googleapis.com/secons-site-images/photo/medium/20260118_L1001763.webp" alt="photo"></a></p>
<h2>Power</h2>
<p>The first difficult point with two RTX 5090s is power. The RTX 5090 has a maximum TBP of 575 W, and there are two of them. Considering the CPU and everything else, I wanted at least a 1600 W power supply. However, household 100 V outlets in Japan are limited to 1500 W, and from what I could find, ordinary PC power supplies sold for 100 V top out at 1300 W.</p>
<p>There are many 1300 W power supplies, but options above that become extremely limited. Higher-wattage power supplies also use a C19 input connector instead of the common C13 connector. By supplying 200 V power to that connector, output above 1300 W becomes possible.</p>
<p>I therefore had electrical work done to install a NEMA 20 A 250 V wall outlet, choosing Panasonic WF2520B, and made 200 V 20 A, up to 4000 W, available. The power cable needs to be NEMA L6-20P to IEC 60320 C19, so I used a Schneider Electric AP8753J Power Cord, Locking C19 to L6-20P. This outlet is also fed directly from its own breaker.</p>
<p>For the power supply, I chose the 1650 W ASRock Taichi TC-1650T, which seemed to have a good reputation. It supports ATX 3.1, which is relevant for the safety of the 12V-2x6 connectors that supply large amounts of power to the GPUs. This power supply also comes with a cable that can plug into a 100 V C19-C20 power cable. That only supports up to 1300 W, but because this type of cable is hard to find on the market, it is useful for test booting.</p>
<p>Update: Another possible method is to use a case that can install two power supplies and connect two 1300 W units to separate 100 V outlets.</p>
<h2>GPU</h2>
<p>Because the RTX 5090 produces a lot of heat, most air-cooled models are three to four PCI slots thick. When using two GPUs of that thickness, you often need riser cables to physically separate them. Otherwise they may collide with the case or motherboard and fail to fit.</p>
<p>The main options are:</p>
<ul>
<li>Use air-cooled models that are three slots or thinner, though the lack of spacing may make heat a concern</li>
<li>Use liquid-cooled AIO models for both GPUs</li>
<li>Use one air-cooled GPU and one AIO liquid-cooled GPU</li>
<li>Use riser cables somehow</li>
</ul>
<p>I already had an RTX 5090 that was about 3.5 slots thick, so I used one AIO liquid-cooled GPU and one air-cooled GPU. If I had not already owned an RTX 5090, I probably would have used two AIO liquid-cooled GPUs and an air-cooled CPU. That would cost a little more, but it would make internal case layout easier and likely lower GPU temperatures further.</p>
<p>The GPUs I used are:</p>
<ul>
<li>MSI GeForce RTX 5090 32G VENTUS 3X OC
<ul>
<li>Air-cooled, about 3.5 slots thick, which I already owned</li>
</ul>
</li>
<li>MSI GeForce RTX 5090 32G SUPRIM LIQUID SOC
<ul>
<li>Slightly over two slots thick, with a 120 x 360 liquid-cooling radiator</li>
</ul>
</li>
</ul>
<p>If budget allows, another option is RTX 6000 Pro, which uses the same Blackwell architecture as the RTX 5090 and has 96 GB of memory. The RTX PRO 6000 Blackwell Max-Q is also an option. Its performance is somewhat lower, but power consumption is much lower at 300 W. The Max-Q model should also reduce cooling concerns and make installation easier.</p>
<h2>Motherboard</h2>
<p>The motherboard requirements were that it could run two GPUs at PCIe 5.0 x8, and that there was enough spacing between GPU 1, the liquid-cooled card in the upper slot, and GPU 2, the air-cooled card in the lower slot. I chose the ASUS ProArt X870E-CREATOR WiFi AMD AM5 X870E ATX, partly because I found examples of it being sold overseas in prebuilt RTX 5090 x2 PCs.</p>
<p>It has an onboard Wi-Fi 7 chip, but there does not currently seem to be a Linux kernel driver for it. If you plan to connect with onboard Wi-Fi, that may matter. In my use case I do not use wireless and connect over wired LAN, so it has not been a problem.</p>
<h2>Case</h2>
<p>I needed a case that would leave a reasonable amount of space when a 3.5-slot-thick GPU was installed in the lower slot, and that could install two AIO radiators, one for the CPU and one for GPU 1. I chose the CORSAIR 7000D AIRFLOW. It is larger than a normal case, but the larger internal space is a clear cooling advantage. I did not need a glass side panel to see inside the PC, but after building it I found it looked good and I am satisfied with it.</p>
<h2>Airflow</h2>
<p>When the system can consume up to around 1650 W inside the case, the generated heat is substantial. Air must circulate in a reasonable way.</p>
<p>Because the CPU and GPU 1 use AIO liquid cooling and GPU 2 uses air cooling, I needed to think about how to bring in and exhaust air. PC cooling fans can be switched between intake and exhaust by flipping them around. After discussing options with AI, I used the airflow below. I am not an airflow expert, so there may be a better layout.</p>
<ul>
<li>Front intake
<ul>
<li>Two 140 mm fans included with the case; ideally I should add one more 140 mm fan</li>
<li>Positioned to hit GPU 2, the air-cooled GPU</li>
</ul>
</li>
<li>Side intake
<ul>
<li>GPU 1 liquid cooler, 120 mm x 3</li>
</ul>
</li>
<li>Top exhaust
<ul>
<li>CPU liquid cooler, 120 mm x 3</li>
</ul>
</li>
<li>Rear exhaust
<ul>
<li>One 140 mm fan included with the case</li>
</ul>
</li>
</ul>
<p>This was the part where I had the hardest time finding information. The remaining parts are mostly a matter of preference, but I will describe them with comments from the perspective of a machine learning PC.</p>
<h2>CPU</h2>
<p>I used the AMD Ryzen 9 9950X, with 16 cores and 32 threads. The 9950X3D was also available, but since I do not use this machine for games, the performance difference seemed marginal, and the 9950X was about 20,000 yen cheaper. Data processing is often parallel, so more CPU cores are useful, but going beyond this would mean Threadripper. I chose 16 cores.</p>
<h2>RAM</h2>
<p>I considered installing the maximum 192 GB, but due to the rapid increase in memory demand from AI-related data centers, prices were staying about four to five times higher than in September 2025. That was too expensive, so I used DDR5-5600 32 GB x 2, for 64 GB. I wanted ECC, but that was also too expensive. In my use case, 64 GB occasionally touches swap, but because the swap is on a fast NVMe drive, it rarely causes real problems. More RAM would be nice, but 64 GB has mostly been enough.</p>
<p>This time I bought DDR5 5600 MHz 32 GB x 2 from a Chinese brand called Acclamator, which was selling for about 60% of the price of other brands with the same capacity. It seems the price has gone up since then. I ran memtest86 and <code>stresstest-cli</code> at 5600 MHz for about 12 hours and saw no errors. I do not yet know about long-term durability or summer heat, since it is currently winter and cold. RAM speed has almost no effect during GPU training, so I lowered it to 4800 MHz for stability. There are cases where RAM speed matters, such as CPU offload during inference, but I do not plan to use it that way.</p>
<p>Update: I eventually felt the lack of memory and added another 32 GB x 2, for a total of 128 GB.</p>
<h2>Storage: NVMe</h2>
<p>Training data can involve random access if handled casually, because the data is often shuffled. For example, Hugging Face Transformers shuffles data by default during training. For that reason, a large NVMe SSD is useful. More capacity is better.</p>
<ul>
<li>Sandisk SN850X NVMe SSD WDS800T2X0E 8TB
<ul>
<li>CPU-connected PCIe lanes. Even 8 TB is not enough, and I use it while deleting data, so I would like more capacity.</li>
</ul>
</li>
<li>Samsung 980 Pro 2TB
<ul>
<li>Added because I had one spare</li>
<li>Chipset-shared lanes</li>
</ul>
</li>
</ul>
<h2>Storage: HDD</h2>
<p>I use a 14 TB HDD as a temporary location for raw downloaded data. It is too slow for workloads with random access, but it works for this purpose. In practice, the Hugging Face <code>datasets</code> library first downloads data to the directory specified by <code>HF_HUB_CACHE</code>, but when the library loads it, Parquet files are converted to Arrow format. As long as the latter can be accessed from NVMe, I can set only <code>HF_HUB_CACHE</code> to the HDD and separate the roles.</p>
<ul>
<li>TOSHIBA MG07ACA14TE 14TB</li>
</ul>
<h2>CPU Cooler</h2>
<p>I did not have a strong preference as long as it was a 120 x 3 radiator AIO, so I used the CORSAIR NAUTILUS 360 RS LCD. I bought it because the LCD on the CPU cooler could display CPU temperature, which seemed nice. After buying it, I realized the display is controlled over USB, making it difficult to control from Linux. There are OSS options, but showing temperature quickly did not seem straightforward. If I were buying now, I would probably choose a model without the LCD.</p>
<h2>Assembly</h2>
<p>Other than the case, power supply, and air-cooled GPU being heavy enough to cause muscle soreness, and my own repeated mistakes with fan orientation and radiator orientation, the build was straightforward. It booted on the first try and has been running without problems.</p>
<h2>OS</h2>
<p>I used Ubuntu Server 24 LTS, which I am used to. I only connect over SSH and do not use a GUI at all.</p>
<h1>Impressions After Building an RTX 5090 x2 PC</h1>
<p>It has been about a month since I built it. Perhaps because it is winter, even when both GPUs are fully used, it has been stable without particular problems. One good point is that when PCIe is not the bottleneck, for example when training a bi-encoder model with MLM, training speed is about 1.8 times faster than with one RTX 5090. Inference is also convenient when horizontal processing is possible. For example, processing 10 million records with Qwen3-8B on vLLM can run at almost twice the speed.</p>
<p>CUDA makes it easy to switch which GPU a program can see with the <code>CUDA_VISIBLE_DEVICES</code> environment variable. If I want to use GPU 2, I can set <code>CUDA_VISIBLE_DEVICES=1</code>, and the program recognizes it as a single GPU without any code changes. This makes it easy to switch between GPUs.</p>
<p>It has also been useful for learning about multi-GPU systems. Until now I had only used one GPU, so I was able to learn methods and ways of thinking about training and inference in a multi-GPU environment.</p>
<p>On the other hand, PCIe 5.0 x8 speed often feels like a bottleneck. For example, PyTorch DDP performs All-Reduce to synchronize data between GPUs at every training step, and depending on the training method that can take a long time. Large-batch contrastive learning is one example. GPU SM idle time can increase substantially, and the speedup may be only around 1.2x. In some cases one GPU can even be faster.</p>
<p>With datacenter GPUs such as B200 and H200, NVLink can provide hundreds of GB/s to TB/s between GPUs depending on the configuration. PCIe 5.0 x8 has an effective speed of about 20-30 GB/s, so it is much slower than NVLink. Expensive GPUs are well designed for a reason. A machine with eight B200s might cost around 80 million yen.</p>
<p>Overall, I am very satisfied with the build. The timing of buying parts was also relatively good. Memory was already expensive, but by mid-January 2026, storage, memory, and RTX 5090 GPUs had become even more expensive. AI demand and the weak yen have made many things costly.</p>]]></content:encoded>
  <pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 20 May 2026 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Looking Back on 2025</title>
  <link>https://hotchpotch.dev/articles/furikaeri-2025/</link>
  <guid>https://hotchpotch.dev/articles/furikaeri-2025/</guid>
  <description>A personal look back at 2025, covering the birth of our child, life in the countryside, a new car, and AI and retrieval work.</description>
  <content:encoded><![CDATA[<h1>Life</h1>
<h2>Child</h2>
<p>Our child was born. People often say that the birth of a child is the biggest change in life, and now that I am living through it, I think that is exactly right. My way of thinking has shifted so that everything starts from the child. Also, the child is cute. Very cute. I feel fortunate to be able to raise a child. Working remotely also means I can see the child's face often, which I am grateful for.</p>
<p>Before the birth, my wife became unwell because of a chronic condition and was hospitalized for a long period. Even after the birth, we were quite anxious about how things would go. Fortunately, her recovery has been good, and she is now able to live daily life without problems. Health is something to be grateful for. I am mostly fine too, apart from frozen shoulder.</p>
<p><a href="https://storage.googleapis.com/secons-site-images/photo/large/20251231_L1001845.webp"><img src="https://storage.googleapis.com/secons-site-images/photo/medium/20251231_L1001845.webp" alt="photo"></a></p>
<h2>House</h2>
<p>The house we built last year, and started living in at the end of last year, is very comfortable. Winter is reasonably cold, and because the area around us is farmland, there are many insects in summer. Even with those tradeoffs, it is very easy to live here. Now that our family has grown, I am glad we built it.</p>
<h2>Car</h2>
<p>We welcomed a Tesla Model Y Juniper Long Range. The comfort of a BEV without an engine, quietness and quick acceleration, is good in itself, but the software and UX are also very well made. Most cars extend the UX of past cars, but a newer company does not need to inherit all of that history, so it can design a different experience. I feel that clearly. Car travel, which is almost all travel in the countryside, has become much easier and more comfortable.</p>
<p>The current Autopilot driver assistance is already quite comfortable, and I expect FSD, or practically speaking a good automated driving assistance system, will become available in Japan in the future. I am looking forward to that too.</p>
<h2>Technology</h2>
<p>I continued working mainly around information retrieval, along with various technical work and product development. This was the year coding agents rose quickly, and almost all of my technical work is now helped by AI. The number of available moves has increased, and I feel the range of what I can do has expanded significantly. This is not limited to ordinary software development. For example, in information retrieval model development, AI can handle a large part of the work. Tasks that previously required too much manual effort can now often be approached in a more experimental way, which is very useful.</p>
<p>I think it is important, especially when the right product direction is unclear, to work on technical areas that AI cannot or does not easily replace. I have been able to work on those technical areas, so it has been enjoyable. Of course, I may be biased, and some of those areas may turn out to be replaced more easily than I expect.</p>
<p>At work, the AI product I had mainly been building was released, and it seems to have received a certain level of recognition both inside the company and in the broader market. I am grateful for that. It was also the result of a team environment that made development easy, so thank you to everyone involved. Next year I want to keep relying heavily on the team and on AI, while continuing technical work and building new products.</p>
<hr>
<p>A lot happened in 2025, but the main event was that our child was born. As always, I am grateful to my wife, who supports me in many ways. With that, I hope to see you all in 2026.</p>
<p><a href="https://storage.googleapis.com/secons-site-images/photo/large/20251231_L1001851.webp"><img src="https://storage.googleapis.com/secons-site-images/photo/medium/20251231_L1001851.webp" alt="photo"></a></p>
<hr>
<ul>
<li><a href="/articles/furikaeri-2024">Looking Back on 2024</a></li>
<li><a href="/articles/furikaeri-2023">Looking Back on 2023</a></li>
<li><a href="https://secon.dev/entry/2022/12/31/070000/">Looking Back on 2022</a></li>
<li><a href="https://secon.dev/entry/2021/12/31/070000/">Looking Back on 2021</a></li>
<li><a href="https://secon.dev/entry/2020/12/31/080000-hurikaeri-2020/">Looking Back on 2020</a></li>
</ul>]]></content:encoded>
  <pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 31 Dec 2025 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>OpenProvence: A Model for Removing Irrelevant Sentences Before Passing Text to an LLM</title>
  <link>https://hotchpotch.dev/articles/open-provence-release/</link>
  <guid>https://hotchpotch.dev/articles/open-provence-release/</guid>
  <description>I released OpenProvence, an open project for pruning irrelevant sentences from retrieved text before passing it to an LLM.</description>
  <content:encoded><![CDATA[<p>Recently, systems often recursively search, gather information, create additional search queries from multiple angles to fill missing knowledge, and extract only the necessary information from the results in order to build "good knowledge" for an LLM to answer with. In 2025 trends such as AI agents, Deep Research, and context engineering, this kind of search often happens behind the scenes. The ability to retrieve useful information can be a key part of the system.</p>
<p>However, when a system searches a lot, the amount of search-result text also grows. That can make it harder for the LLM to extract the truly necessary information, increase hallucinations, slow processing because of larger inputs, and increase LLM usage cost.</p>
<p><a href="https://arxiv.org/abs/2501.16214">Provence</a> is an approach for deleting irrelevant information before passing search results to an LLM, while also assigning a relevance score. It can remove irrelevant parts from documents returned by search. When I measured the model's performance, an evaluation using long-form question-answer datasets, MLDR plus LLM evaluation, showed that it could remove about 80-95% of the text. In other words, a 10,000-character text can be reduced to roughly 500-2,000 characters before being passed to the LLM. That can substantially reduce input size. Even for datasets made of many shorter sentence-like chunks, depending on the domain, it removed about 30-70% of sentences.</p>
<p><img src="https://storage.googleapis.com/secons-site-images/other/open_provence/carbon_xsmall_ja.png" alt="Example of pruning short sentences"></p>
<p>The Provence implementation and models published for research are non-commercial, and no Japanese dataset was available. I therefore created a project called <a href="https://github.com/hotchpotch/open_provence/">OpenProvence</a> and published training and inference code, model weights, and related artifacts under open licenses. I also created and published Japanese datasets. Many of the datasets themselves are not under open licenses because they inherit the original source licenses.</p>
<hr>
<h2>Trying OpenProvence</h2>
<p>I prepared a Hugging Face Spaces demo that runs on CPU:</p>
<ul>
<li>🤗 <a href="https://huggingface.co/spaces/hotchpotch/open_provence_demo">https://huggingface.co/spaces/hotchpotch/open_provence_demo</a></li>
</ul>
<p>For example, if you use the sample Wikipedia page about information retrieval and run sentence pruning with the query "What is vector search?", the article of about 5,000 Japanese characters is reduced to about 400 characters, leaving only the information about vector search.</p>
<p>You can also run the demo locally with the following steps. On a recent MacBook, inference should be fairly fast.</p>
<pre><code class="hljs language-bash">git <span class="hljs-built_in">clone</span> https://huggingface.co/spaces/hotchpotch/open_provence_demo
<span class="hljs-built_in">cd</span> open_provence_demo
uv <span class="hljs-built_in">sync</span>
uv run python app.py
</code></pre>
<h3>Using It from Python</h3>
<p>From Python, it can be used as follows. The small xsmall model can run on CPU. In a GPU environment, NVIDIA with FlashAttention 2, inference should finish almost immediately and prune the text. I think it is fast enough to be integrated into a production search environment.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> AutoModel

<span class="hljs-comment"># Change this to the model you want to use.</span>
model_name = <span class="hljs-string">"hotchpotch/open-provence-reranker-xsmall-v1"</span>
provence = AutoModel.from_pretrained(model_name, trust_remote_code=<span class="hljs-literal">True</span>)

question:<span class="hljs-built_in">str</span> = <span class="hljs-string">"日本の首都について"</span>
context:<span class="hljs-built_in">str</span> = <span class="hljs-string">"""
今日は学校に行き、さまざまなことを学んだり、友達と学食でたらふく食べた。
日本の首都は東京で、東京は日本の政治、経済、文化の中心地らしい。この都市は約1,400万人の人口を抱える世界有数の大都市らしい。
夜は飲み会に誘われたが、参加せずに帰宅した、今月そんなにお金が残ってないからなぁ、残念だ。
"""</span>

result = provence.process(question, context, threshold=<span class="hljs-number">0.1</span>)
<span class="hljs-built_in">print</span>(<span class="hljs-string">f"Reranking Score: <span class="hljs-subst">{result[<span class="hljs-string">'reranking_score'</span>]:<span class="hljs-number">.4</span>f}</span>"</span>)
<span class="hljs-built_in">print</span>(<span class="hljs-string">f"Compression Rate: <span class="hljs-subst">{result[<span class="hljs-string">'compression_rate'</span>]:<span class="hljs-number">.1</span>f}</span>%"</span>)
<span class="hljs-built_in">print</span>(<span class="hljs-string">f"Pruned Context:\n<span class="hljs-subst">{result[<span class="hljs-string">'pruned_context'</span>]}</span>"</span>)

<span class="hljs-comment"># Output example:</span>
<span class="hljs-comment"># Reranking Score: 0.7043</span>
<span class="hljs-comment"># Compression Rate: 62.5%</span>
<span class="hljs-comment"># Pruned Context:</span>
<span class="hljs-comment"># 日本の首都は東京で、東京は日本の政治、経済、文化の中心地らしい。</span>
<span class="hljs-comment"># この都市は約1,400万人の人口を抱える世界有数の大都市らしい。</span>
</code></pre>
<h2>Using Coding Agents</h2>
<p>For OpenProvence, I set a constraint that I would not write a single line of code myself. All implementation work, including inference, training model code, evaluation code, and dataset creation code, was done by coding agents such as Claude Code and Codex. I still had to give many correction instructions, but for a side project progressed in spare time, I think the result is quite good. Looking at the final code, it could probably be simpler. At the same time, with current LLMs, this amount of explicit and somewhat verbose code may be easier for the model to understand and modify.</p>
<p>The term <a href="https://simonwillison.net/2025/Oct/7/vibe-engineering/">vibe engineering</a> has emerged for building production-quality software in collaboration with AI by continuing to provide suitable instructions, development guidelines, and an environment where AI can develop and improve the project itself.</p>
<p>In addition to the usual software development practices used with coding agents, such as development guidelines, unit tests, CI, and code review, I found that machine learning model projects of a certain size can also be developed this way by preparing a small baseline that can train quickly, evaluation data whose accidental changes would indicate bugs, and detailed explanations of the datasets.</p>
<h2>Closing</h2>
<p>An approach like OpenProvence, which removes text unrelated to the question, should work especially well for products that process very large documents.</p>
<p>RAG was a major topic in 2024, and in 2025 trends such as AI agents, Deep Research, and context engineering have made this area even more important. I am impressed by, and grateful for, the foresight of the Provence team at Naver Labs Europe, who worked on an important technical point early. Provence was published in January 2025.</p>
<p>For products using LLMs, information retrieval behind the scenes can increase value, and retrieval technology remains very interesting. I hope this project is useful in products or research.</p>
<ul>
<li><a href="https://github.com/hotchpotch/open_provence/">https://github.com/hotchpotch/open_provence/</a></li>
<li><a href="https://huggingface.co/collections/hotchpotch/openprovence">https://huggingface.co/collections/hotchpotch/openprovence</a></li>
</ul>]]></content:encoded>
  <pubDate>Fri, 31 Oct 2025 00:00:00 GMT</pubDate>
  <lastBuildDate>Fri, 31 Oct 2025 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Evaluating the Japanese Performance of Embedding Gemma 300M with JMTEB</title>
  <link>https://hotchpotch.dev/articles/embedding-gemma300m/</link>
  <guid>https://hotchpotch.dev/articles/embedding-gemma300m/</guid>
  <description>I benchmarked google/embeddinggemma-300m on JMTEB v1 and compared its Japanese embedding performance with multilingual and Japanese models.</description>
  <content:encoded><![CDATA[<p>Google recently released the embedding model <a href="https://huggingface.co/google/embeddinggemma-300m">google/embeddinggemma-300m</a>. It performs quite well on MTEB Multilingual v2, so I benchmarked it with JMTEB v1 to properly measure its Japanese performance.</p>
<p>The short version is that, for Japanese, Embedding Gemma performed very poorly in my measurement.</p>
<h2>JMTEB v1 Benchmark</h2>
<table>
<thead>
<tr>
<th>Model</th>
<th>Params</th>
<th>Avg</th>
<th>Retrieval</th>
<th>STS</th>
<th>Classification</th>
<th>Reranking</th>
<th>Clustering</th>
<th>PairClass</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://huggingface.co/google/embeddinggemma-300m">google/embeddinggemma-300m</a></td>
<td>308M</td>
<td><strong>58.10</strong></td>
<td>42.18</td>
<td>73.36</td>
<td>63.23</td>
<td>91.55</td>
<td>45.87</td>
<td>62.42</td>
</tr>
<tr>
<td><a href="https://huggingface.co/intfloat/multilingual-e5-small">intfloat/multilingual-e5-small</a></td>
<td>118M</td>
<td><strong>69.52</strong></td>
<td>67.27</td>
<td>80.07</td>
<td>67.62</td>
<td>93.03</td>
<td>46.91</td>
<td>62.19</td>
</tr>
<tr>
<td><a href="https://huggingface.co/intfloat/multilingual-e5-large">intfloat/multilingual-e5-large</a></td>
<td>560M</td>
<td><strong>71.65</strong></td>
<td>70.98</td>
<td>79.70</td>
<td>72.89</td>
<td>92.96</td>
<td>51.24</td>
<td>62.15</td>
</tr>
<tr>
<td><a href="https://huggingface.co/cl-nagoya/ruri-v3-30m">cl-nagoya/ruri-v3-30m</a></td>
<td>37M</td>
<td><strong>74.51</strong></td>
<td>78.08</td>
<td>82.48</td>
<td>74.80</td>
<td>93.00</td>
<td>52.12</td>
<td>62.40</td>
</tr>
<tr>
<td><a href="https://huggingface.co/cl-nagoya/ruri-v3-310m">cl-nagoya/ruri-v3-310m</a></td>
<td>315M</td>
<td><strong>77.24</strong></td>
<td>81.89</td>
<td>81.22</td>
<td>78.66</td>
<td>93.43</td>
<td>55.69</td>
<td>62.60</td>
</tr>
</tbody>
</table>
<p><em>Note: This is the micro average, simple average, across the 16 Japanese tasks in JMTEB v1.</em></p>
<p>The JMTEB configuration is <a href="https://github.com/hotchpotch/JMTEB/compare/e152a7a351d0550466a...f23d9a737f70c57f8#diff-7c452491cda037603553f833680a105d21981861f8bd037a91779d824f641e45">here</a>, and prefixes and similar settings should be applied. The result JSON, <a href="https://gist.github.com/hotchpotch/62eb81698b66e3fea5a15816521f695d">summary.json, is in this gist</a>. The reproduction steps are also in the gist. If my measurement is wrong, please let me know.</p>
<p>Update, 2025-10-03: Because of a <a href="https://huggingface.co/google/embeddinggemma-300m/discussions/3#68baf8490495f751dd1a654b">Transformers bug</a>, the latest version reportedly improves performance to around the level of <code>ruri-base</code>. Thank you to LM8 (@ShengzheLi) for the information.</p>
<h2>JQaRA / JaCWIR</h2>
<p>Because the JMTEB v1 score was very low, I also evaluated it separately on <a href="https://github.com/hotchpotch/JQaRA">JQaRA</a> and <a href="https://github.com/hotchpotch/JaCWIR">JaCWIR</a>. The results were again quite low.</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>JQaRA (nDCG@10)</th>
<th>JQaRA (MRR@10)</th>
<th>JaCWIR (MAP@10)</th>
<th>JaCWIR (HIT_RATE@10)</th>
</tr>
</thead>
<tbody>
<tr>
<td>google/embeddinggemma-300m</td>
<td>0.261</td>
<td>0.457</td>
<td>0.730</td>
<td>0.904</td>
</tr>
<tr>
<td>intfloat/multilingual-e5-small</td>
<td>0.492</td>
<td>0.729</td>
<td>0.869</td>
<td>0.970</td>
</tr>
<tr>
<td>intfloat/multilingual-e5-large</td>
<td>0.554</td>
<td>0.799</td>
<td>0.876</td>
<td>0.973</td>
</tr>
</tbody>
</table>
<h1>Strong MTEB Does Not Necessarily Mean Strong Japanese Performance</h1>
<p>This was also true for Qwen3 Embedding, which I evaluated recently in <a href="/articles/qwen3-embedding-jmteb">Evaluating the Japanese Performance of Qwen3 Embedding with JMTEB</a>. Recent multilingual embedding models with high MTEB scores often have weak Japanese performance. Looking at the <code>Language-specific</code> <code>Japanese</code> section of the MTEB leaderboard, both Qwen3 Embeddings and Embedding Gemma only show Pair Classification for Japanese, so it is not very informative. That makes the meaning of multilingual performance somewhat unclear.</p>
<p>Both Qwen3 Embedding and Embedding Gemma are based on decoder-model architectures. Looking inside <code>embeddinggemma-300m</code>, it uses an <a href="https://huggingface.co/google/embeddinggemma-300m/tree/main">embedding head, pooling plus two dense layers, with mean pooling</a>.</p>
<p>For decoder-based models with small parameter counts, at least in Japanese, the performance was much lower than other encoder-based multilingual models. It is unclear whether this is because the models were barely trained on Japanese embedding tasks, or because the original small decoder model has weak Japanese generalization.</p>]]></content:encoded>
  <pubDate>Thu, 18 Sep 2025 00:00:00 GMT</pubDate>
  <lastBuildDate>Thu, 18 Sep 2025 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>JFWIR: A Large Japanese Information Retrieval Dataset Built from Japanese FineWeb</title>
  <link>https://hotchpotch.dev/articles/jfwir-japanese-fineweb-ir/</link>
  <guid>https://hotchpotch.dev/articles/jfwir-japanese-fineweb-ir/</guid>
  <description>I released JFWIR, a 64M-pair Japanese information retrieval dataset built from FineWeb2 Edu Japanese, with query types and hard negatives.</description>
  <content:encoded><![CDATA[<p>In Japanese information retrieval, many datasets have historically been built around Wikipedia. Real web text, however, is not limited to the clean, well-formatted writing found in Wikipedia. It includes blogs, news, forums, diverse writing styles, and noise.</p>
<p><strong>JFWIR (Japanese FineWeb Information Retrieval)</strong> is a large dataset of about 64 million Japanese document-query pairs created to address that gap. It is based on <a href="https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese">fineweb-2-edu-japanese</a>, a web-crawl dataset containing high-quality educational Japanese content.</p>
<ul>
<li><a href="https://huggingface.co/datasets/hotchpotch/JFWIR">https://huggingface.co/datasets/hotchpotch/JFWIR</a></li>
</ul>
<h2>Characteristics of JFWIR</h2>
<h3>1. Large and Diverse</h3>
<p>JFWIR has the following characteristics:</p>
<ul>
<li><strong>More than 64 million document-query pairs</strong>: seven different query types are generated for each document: <code>keywords</code>, <code>synonym_keywords</code>, <code>query</code>, <code>alt_query</code>, <code>title</code>, <code>faq</code>, and <code>summary</code></li>
<li><strong>Real web text</strong>: educationally valuable web content beyond Wikipedia</li>
<li><strong>Hard negatives</strong>: similar but incorrect documents for effective training</li>
</ul>
<h3>2. Benchmark Results</h3>
<p>I evaluated reranking models trained with JFWIR on major Japanese information retrieval benchmarks:</p>
<table>
<thead>
<tr>
<th>Benchmark</th>
<th>Without JFWIR</th>
<th>With 10M JFWIR records</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://huggingface.co/datasets/SkelterLabsInc/JQaRA">JQaRA</a></td>
<td>0.7621</td>
<td>0.7633</td>
</tr>
<tr>
<td><a href="https://huggingface.co/datasets/miracl/miracl">MIRACL(ja)</a></td>
<td>0.8332</td>
<td>0.8385</td>
</tr>
<tr>
<td><a href="https://techblog.yahoo.co.jp/entry/2022122030379907/">jsquad</a></td>
<td>0.9801</td>
<td>0.9821</td>
</tr>
<tr>
<td><a href="https://huggingface.co/datasets/hotchpotch/JaCWIR">JaCWIR</a></td>
<td>0.9339</td>
<td><strong>0.9586</strong></td>
</tr>
</tbody>
</table>
<p>The improvement on JaCWIR, which targets web text, was especially clear: 0.9339 to 0.9586.</p>
<h2>Usage</h2>
<p>JFWIR can be used easily from Hugging Face Datasets:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> datasets <span class="hljs-keyword">import</span> load_dataset

<span class="hljs-comment"># Load the main dataset.</span>
train_ds = load_dataset(<span class="hljs-string">"hotchpotch/JFWIR"</span>, split=<span class="hljs-string">"train"</span>, name=<span class="hljs-string">"small_tokens_cleaned"</span>)

<span class="hljs-comment"># Inspect sample data.</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">3</span>):
    sample = train_ds[i]
    <span class="hljs-built_in">print</span>(<span class="hljs-string">f"Query: <span class="hljs-subst">{sample[<span class="hljs-string">'query'</span>]}</span>"</span>)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">f"Document: <span class="hljs-subst">{sample[<span class="hljs-string">'text'</span>][:<span class="hljs-number">100</span>]}</span>..."</span>)

<span class="hljs-comment"># Load the hard-negative dataset.</span>
hard_negatives_ds = load_dataset(<span class="hljs-string">"hotchpotch/JFWIR"</span>, split=<span class="hljs-string">"train"</span>, name=<span class="hljs-string">"hard_negatives"</span>)

<span class="hljs-comment"># Example hard-negative usage.</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">3</span>):
    hn_sample = hard_negatives_ds[i]
    pos_id = hn_sample[<span class="hljs-string">'pos_id'</span>]
    pos_doc = train_ds[pos_id]
    
    <span class="hljs-built_in">print</span>(<span class="hljs-string">f"Query: <span class="hljs-subst">{pos_doc[<span class="hljs-string">'query'</span>]}</span>"</span>)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">f"Positive (score: <span class="hljs-subst">{hn_sample[<span class="hljs-string">'pos_score'</span>]:<span class="hljs-number">.3</span>f}</span>): <span class="hljs-subst">{pos_doc[<span class="hljs-string">'text'</span>][:<span class="hljs-number">100</span>]}</span>..."</span>)
    
    <span class="hljs-comment"># Sort negative documents by score.</span>
    neg_pairs = <span class="hljs-built_in">list</span>(<span class="hljs-built_in">zip</span>(hn_sample[<span class="hljs-string">'neg_ids'</span>], hn_sample[<span class="hljs-string">'neg_scores'</span>]))
    neg_pairs.sort(key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-number">1</span>])
    
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Negatives (lowest scores):"</span>)
    <span class="hljs-keyword">for</span> neg_id, score <span class="hljs-keyword">in</span> neg_pairs[:<span class="hljs-number">2</span>]:
        <span class="hljs-built_in">print</span>(<span class="hljs-string">f"  Score <span class="hljs-subst">{score:<span class="hljs-number">.3</span>f}</span>: <span class="hljs-subst">{train_ds[neg_id][<span class="hljs-string">'text'</span>][:<span class="hljs-number">80</span>]}</span>..."</span>)
</code></pre>
<h2>Dataset Creation Process</h2>
<h3>1. Collecting High-Quality Japanese Web Text</h3>
<p>First, I extracted educationally valuable Japanese content from <a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb-2">FineWeb-2</a> to create <a href="https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese">fineweb-2-edu-japanese</a>. I then created the <code>small_tokens_cleaned</code> subset by removing web-specific noise and adjusting the text length.</p>
<h3>2. Generating Diverse Queries</h3>
<p>To generate queries for 64 million records, I used the lightweight query generation model <a href="/articles/query-crafter-japanese">query-crafter-japanese</a>. To increase diversity, I combined three models:</p>
<ul>
<li><a href="https://huggingface.co/hotchpotch/query-crafter-japanese-Qwen3-1.7B">hotchpotch/query-crafter-japanese-Qwen3-1.7B</a></li>
<li><a href="https://huggingface.co/hotchpotch/query-crafter-japanese-Qwen3-4B">hotchpotch/query-crafter-japanese-Qwen3-4B</a></li>
<li><a href="https://huggingface.co/hotchpotch/query-crafter-japanese-sarashina2.2-3b-instruct-v0.1">hotchpotch/query-crafter-japanese-sarashina2.2-3b-instruct-v0.1</a></li>
</ul>
<p>By generating seven query types for each document, <code>keywords</code>, <code>synonym_keywords</code>, <code>query</code>, <code>alt_query</code>, <code>title</code>, <code>faq</code>, and <code>summary</code>, the dataset can support a wider range of retrieval needs.</p>
<h3>3. Creating Hard Negatives</h3>
<p>To improve retrieval model performance, I also created a dataset containing hard negatives, documents that are similar to the query but not correct:</p>
<ol>
<li><strong>Similar document retrieval with an embedding model</strong>: I vectorized 64 million documents with <a href="https://huggingface.co/cl-nagoya/ruri-v3-30m">ruri-v3-30m</a> and retrieved similar documents for each document.</li>
<li><strong>Selecting suitable negatives</strong>: I randomly sampled from similarity ranks top 10-50 and top 50-200.</li>
<li><strong>Assigning reranker scores</strong>: I scored documents with <a href="https://huggingface.co/hotchpotch/japanese-reranker-xsmall-v2">japanese-reranker-xsmall-v2</a>. For example, by excluding positives that are unsuitable, such as score&#x3C;0.6, and negatives that are unsuitable, such as score>0.4, you can select more appropriate positive and negative examples.</li>
</ol>
<h2>Future Work</h2>
<p>JFWIR is published to contribute to Japanese information retrieval. However, <a href="/articles/query-crafter-japanese">query-crafter-japanese</a> currently generates relatively simple queries from text. I think retrieval accuracy can improve further by creating more diverse and valuable questions.</p>
<h2>Summary</h2>
<p>JFWIR takes a different approach from previous Japanese IR datasets that were heavily biased toward Wikipedia. It targets real web text and includes about 64 million records, seven query types, and hard negatives for contrastive learning. These elements should be useful for developing information retrieval systems.</p>
<p>The dataset is published on Hugging Face and can be used freely under the ODC-By license. I hope it contributes, even a little, to the development of Japanese information retrieval.</p>
<hr>
<h2>Related Links</h2>
<h3>Datasets</h3>
<ul>
<li><strong>JFWIR dataset</strong>: <code>hotchpotch/JFWIR</code> on Hugging Face Datasets</li>
<li><strong>fineweb-2-edu-japanese</strong>: <a href="https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese">https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese</a></li>
<li><strong>FineWeb-2</strong>: <a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb-2">https://huggingface.co/datasets/HuggingFaceFW/fineweb-2</a></li>
</ul>
<h3>Models</h3>
<ul>
<li><strong>ruri-v3-30m embedding model</strong>: <a href="https://huggingface.co/cl-nagoya/ruri-v3-30m">https://huggingface.co/cl-nagoya/ruri-v3-30m</a></li>
<li><strong>ruri-v3-pt-70m pretraining model</strong>: <a href="https://huggingface.co/cl-nagoya/ruri-v3-pt-70m">https://huggingface.co/cl-nagoya/ruri-v3-pt-70m</a></li>
<li><strong>japanese-reranker-xsmall-v2</strong>: <a href="https://huggingface.co/hotchpotch/japanese-reranker-xsmall-v2">https://huggingface.co/hotchpotch/japanese-reranker-xsmall-v2</a></li>
<li><strong>query-crafter-japanese query generation models</strong>:
<ul>
<li><a href="https://huggingface.co/hotchpotch/query-crafter-japanese-Qwen3-1.7B">hotchpotch/query-crafter-japanese-Qwen3-1.7B</a></li>
<li><a href="https://huggingface.co/hotchpotch/query-crafter-japanese-Qwen3-4B">hotchpotch/query-crafter-japanese-Qwen3-4B</a></li>
<li><a href="https://huggingface.co/hotchpotch/query-crafter-japanese-sarashina2.2-3b-instruct-v0.1">hotchpotch/query-crafter-japanese-sarashina2.2-3b-instruct-v0.1</a></li>
</ul>
</li>
</ul>
<h3>Articles and Papers</h3>
<ul>
<li><a href="/articles/query-crafter-japanese">query-crafter-japanese article</a></li>
<li><a href="https://techblog.yahoo.co.jp/entry/2022122030379907/">Building JGLUE, a Japanese language understanding benchmark</a></li>
<li><a href="https://arxiv.org/abs/2210.09984">MIRACL: A Multilingual Retrieval Dataset Covering 18 Diverse Languages</a></li>
<li><a href="https://arxiv.org/abs/2406.17557">FineWeb-Edu: The Finest Collection of Educational Content the Web Has to Offer</a></li>
<li><a href="https://arxiv.org/abs/2409.07737">Ruri: Japanese General Text Embeddings</a></li>
</ul>
<p><strong>Author</strong>: <a href="https://secon.dev/">Yuichi Tateno (@hotchpotch)</a></p>
<h2>License</h2>
<p>This dataset is released under the <strong>Open Data Commons Attribution License (ODC-By) v1.0</strong>, the same as the original FineWeb2. The <a href="https://commoncrawl.org/terms-of-use">Common Crawl terms of use</a> also apply.</p>
<h2>Citation Information</h2>
<p>If you use the JFWIR dataset in research or development, please use the following citation:</p>
<pre><code>@misc{tateno2025jfwir,
  author = {Yuichi Tateno},
  title = {JFWIR: Japanese FineWeb Information Retrieval Dataset},
  year = {2025},
  url = {https://huggingface.co/datasets/hotchpotch/JFWIR},
  note = {A large-scale Japanese information retrieval dataset with 60+ million document-query pairs}
}
</code></pre>]]></content:encoded>
  <pubDate>Thu, 19 Jun 2025 00:00:00 GMT</pubDate>
  <lastBuildDate>Thu, 19 Jun 2025 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Evaluating the Japanese Performance of Qwen3 Embedding with JMTEB</title>
  <link>https://hotchpotch.dev/articles/qwen3-embedding-jmteb/</link>
  <guid>https://hotchpotch.dev/articles/qwen3-embedding-jmteb/</guid>
  <description>I benchmarked Qwen3-Embedding-0.6B on Japanese JMTEB tasks and compared it with Japanese embedding models and OpenAI embeddings.</description>
  <content:encoded><![CDATA[<p>The open-weight, high-performance multilingual embedding and reranker series <a href="https://qwenlm.github.io/blog/qwen3-embedding/">Qwen3 Embedding</a> has been released. It includes 8B, 4B, and 0.6B model sizes, performs well for text embeddings and reranking, and currently ranks at the top of the <a href="https://huggingface.co/spaces/mteb/leaderboard">Multilingual MTEB leaderboard</a>.</p>
<p>However, multilingual models often do not place much emphasis on Japanese, so I measured Qwen3-Embedding-0.6B with <a href="https://github.com/sbintuitions/JMTEB">JMTEB: Japanese Massive Text Embedding Benchmark</a>. <code>jsick</code> and <code>jsts</code> errored, so STS tasks are excluded.</p>
<h2>JMTEB Results</h2>
<table>
<thead>
<tr>
<th align="left">Model</th>
<th align="center">Retrieval</th>
<th align="center">STS</th>
<th align="center">Classification</th>
<th align="center">Reranking</th>
<th align="center">Clustering</th>
<th align="left">PairClassification</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><strong>Qwen3-Embedding-0.6B</strong></td>
<td align="center">72.81</td>
<td align="center">--</td>
<td align="center">66.09</td>
<td align="center">93.10</td>
<td align="center">48.84</td>
<td align="left">62.42</td>
</tr>
<tr>
<td align="left">ruri-v3-310m</td>
<td align="center"><strong>81.89</strong></td>
<td align="center">81.22</td>
<td align="center"><strong>78.66</strong></td>
<td align="center">93.43</td>
<td align="center"><strong>55.69</strong></td>
<td align="left"><strong>62.60</strong></td>
</tr>
<tr>
<td align="left">ruri-v3-130m</td>
<td align="center"><strong>81.89</strong></td>
<td align="center">79.25</td>
<td align="center">77.16</td>
<td align="center">93.31</td>
<td align="center">55.36</td>
<td align="left">62.26</td>
</tr>
<tr>
<td align="left">ruri-v3-70m</td>
<td align="center">79.96</td>
<td align="center">79.82</td>
<td align="center">76.97</td>
<td align="center">93.27</td>
<td align="center">52.70</td>
<td align="left">61.75</td>
</tr>
<tr>
<td align="left">PLaMo-Embedding-1B</td>
<td align="center">79.94</td>
<td align="center"><strong>83.14</strong></td>
<td align="center">77.20</td>
<td align="center">93.57</td>
<td align="center">53.47</td>
<td align="left">62.37</td>
</tr>
<tr>
<td align="left">ruri-v3-30m</td>
<td align="center">78.08</td>
<td align="center">82.48</td>
<td align="center">74.80</td>
<td align="center">93.00</td>
<td align="center">52.12</td>
<td align="left">62.40</td>
</tr>
<tr>
<td align="left">sbintuitions/sarashina-embedding-v1-1b</td>
<td align="center">77.61</td>
<td align="center">82.71</td>
<td align="center">78.37</td>
<td align="center"><strong>93.74</strong></td>
<td align="center">53.86</td>
<td align="left">62.00</td>
</tr>
<tr>
<td align="left">jinaai/jina-embeddings-v3</td>
<td align="center">75.22</td>
<td align="center">80.05</td>
<td align="center">76.39</td>
<td align="center">92.71</td>
<td align="center">51.46</td>
<td align="left">62.37</td>
</tr>
<tr>
<td align="left">OpenAI/text-embedding-3-large</td>
<td align="center">74.48</td>
<td align="center">82.52</td>
<td align="center">77.58</td>
<td align="center">93.58</td>
<td align="center">53.32</td>
<td align="left">62.35</td>
</tr>
<tr>
<td align="left">pkshatech/GLuCoSE-base-ja-v2</td>
<td align="center">73.36</td>
<td align="center">82.96</td>
<td align="center">74.21</td>
<td align="center">93.01</td>
<td align="center">48.65</td>
<td align="left">62.37</td>
</tr>
<tr>
<td align="left">pkshatech/RoSEtta-base-ja</td>
<td align="center">73.21</td>
<td align="center">81.39</td>
<td align="center">72.41</td>
<td align="center">92.69</td>
<td align="center">53.23</td>
<td align="left">61.74</td>
</tr>
<tr>
<td align="left">intfloat/multilingual-e5-large</td>
<td align="center">70.98</td>
<td align="center">79.70</td>
<td align="center">72.89</td>
<td align="center">92.96</td>
<td align="center">51.24</td>
<td align="left">62.15</td>
</tr>
<tr>
<td align="left">OpenAI/text-embedding-3-small</td>
<td align="center">66.39</td>
<td align="center">79.46</td>
<td align="center">73.06</td>
<td align="center">92.92</td>
<td align="center">51.06</td>
<td align="left">62.27</td>
</tr>
</tbody>
</table>
<p>These are the results. Perhaps because Japanese tasks were not trained heavily, the Japanese results were not strong. The <code>ruri-v3</code> series is smaller and clearly much stronger for Japanese.</p>
<p>For Retrieval and Reranking tasks, I added the query prefix <code>Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery:</code>.</p>
<p>The JMTEB configuration, summary JSON, and execution commands used for this measurement are available here. The Qwen3-Embedding-0.6B score feels low, so if I made a mistake, please let me know.</p>
<ul>
<li><a href="https://gist.github.com/hotchpotch/f6be186010e70d6eb6e46447cea258f9">https://gist.github.com/hotchpotch/f6be186010e70d6eb6e46447cea258f9</a></li>
</ul>
<h2>Extra: Reading the Qwen3 Embedding Paper</h2>
<p><a href="https://arxiv.org/abs/2506.05176">Qwen3 Embedding: Advancing Text Embedding and Reranking Through Foundation Models</a> was published, so I read it briefly. I found the synthetic dataset creation process especially interesting.</p>
<p>These are notes from the parts that interested me:</p>
<ul>
<li>It does not convert a decoder to an encoder like LLM2Vec; it uses causal attention as-is.</li>
<li>The embedding model obtains the final embedding from the hidden state of the final layer's <code>[EOS]</code> token.
<ul>
<li>Queries are built as Instruction + Query. Documents are used as-is.</li>
<li>The score improves on InfoNCE rather than using simple contrastive learning, including multiple hard negatives and adjustments for false negatives by tuning positive and negative similarity.</li>
</ul>
</li>
<li>Reranking uses the chat template directly and computes a relevance score from the probabilities of the "yes" and "no" tokens.
<ul>
<li>It applies the usual decoder-model label-classification approach, looking at the probability of the target label token.</li>
<li>It can be trained with SFT.</li>
</ul>
</li>
<li>In the first stage, training uses a synthetic dataset created with Qwen3-32B.
<ul>
<li>It creates four types: information retrieval, bitext mining, semantic similarity, and classification.</li>
<li>For the information retrieval synthetic dataset, it creates detailed settings and generates queries from documents in the Qwen3 pretraining corpus.</li>
</ul>
</li>
<li>In the second stage, training uses 7 million existing datasets such as MS MARCO and MIRACL, plus 12 million records filtered by cosine similarity from the first stage.</li>
<li>Finally, it uses model merging with diversity in mind.
<ul>
<li>The details are not written, so this is an inference, but multiple second-stage checkpoints could include task-specialized checkpoints or checkpoints focused on particular languages.</li>
<li>If you have many checkpoints, model merging seems worth trying. Even with limited compute, you can often observe benchmark improvements by merging checkpoints and evaluating the result.</li>
</ul>
</li>
</ul>]]></content:encoded>
  <pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 11 Jun 2025 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Releasing Small, Fast, and Practical Japanese Rerankers: tiny, xsmall, small, and base v2</title>
  <link>https://hotchpotch.dev/articles/japanese-reranker-v2/</link>
  <guid>https://hotchpotch.dev/articles/japanese-reranker-v2/</guid>
  <description>I released tiny, xsmall, small, and base v2 Japanese rerankers designed for practical CPU and Apple silicon latency with competitive quality.</description>
  <content:encoded><![CDATA[<p>I released very small Japanese reranker models, <a href="https://huggingface.co/hotchpotch/japanese-reranker-tiny-v2">japanese-reranker-tiny-v2</a> and <a href="https://huggingface.co/hotchpotch/japanese-reranker-xsmall-v2">japanese-reranker-xsmall-v2</a>. In information retrieval systems, rerankers improve the precision of search results, but model size and compute cost are practical challenges.</p>
<p>🆕 Update, 2025-07-10: I also added moderately small rerankers, <a href="https://huggingface.co/hotchpotch/japanese-reranker-small-v2">japanese-reranker-small-v2</a> and <a href="https://huggingface.co/hotchpotch/japanese-reranker-base-v2">japanese-reranker-base-v2</a>.</p>
<p>These models are built with minimal layer counts and parameter counts, and they run at practical speed even on CPU and Apple silicon. This makes it possible to improve RAG system accuracy without expensive GPU resources, and should make them useful for edge deployment and production environments that require low latency. In evaluation, they achieve competitive scores even compared with larger models.</p>
<ul>
<li><a href="https://huggingface.co/hotchpotch/japanese-reranker-tiny-v2">https://huggingface.co/hotchpotch/japanese-reranker-tiny-v2</a></li>
<li><a href="https://huggingface.co/hotchpotch/japanese-reranker-xsmall-v2">https://huggingface.co/hotchpotch/japanese-reranker-xsmall-v2</a></li>
<li><a href="https://huggingface.co/hotchpotch/japanese-reranker-small-v2">https://huggingface.co/hotchpotch/japanese-reranker-small-v2</a></li>
<li><a href="https://huggingface.co/hotchpotch/japanese-reranker-base-v2">https://huggingface.co/hotchpotch/japanese-reranker-base-v2</a></li>
</ul>
<p><img src="https://storage.googleapis.com/secons-site-images/other/blog_images/20250508-japanese-reranker-v2-scores.png" alt="Reranker Benchmark"></p>
<h1>What Rerankers Are, and Why Small Rerankers Matter</h1>
<p>A reranker is a model that evaluates the relevance between a question, or query, and documents, then reorders the documents by relevance. Its strength is that it can evaluate complex relationships that ordinary embedding search may miss. In particular, CrossEncoder architectures take the query and document as one input pair, allowing finer-grained nuance and contextual understanding.</p>
<ul>
<li><a href="/articles/japanese-reranker-release">Releasing High-Performance Japanese Rerankers, and What Rerankers Are</a></li>
</ul>
<p>Small rerankers matter for several reasons. First, a reranker must evaluate every combination of a query and candidate document. Reranking 100 candidate documents requires 100 model inferences. Smaller models therefore directly improve throughput and reduce latency.</p>
<p>Small models can also run in resource-limited environments. They can run at realistic speed on CPU-only environments, edge devices, and mobile devices, improving the practicality of RAG systems. In server environments, they also reduce GPU memory usage and make it easier to share GPU resources, improving cost efficiency.</p>
<ul>
<li><a href="https://speakerdeck.com/hotchpotch/ask-nikkei-ragjian-suo-ji-shu-noshen-ceng">Ask! NIKKEI RAG Search Technology Deep Dive</a></li>
</ul>
<p>Small rerankers therefore provide important benefits in speed, cost, and resource efficiency, and can play a useful role in practical RAG systems.</p>
<h2>Benchmark Performance</h2>
<p>The benchmark results are below. Considering their model size, the tiny and xsmall v2 models perform quite well. Among larger models, <a href="https://huggingface.co/cl-nagoya/ruri-v3-reranker-310m">ruri-v3-reranker-310m</a> is clearly strong. The fact that these high-performing models are based on ModernBERT likely contributes to the improvement.</p>
<p>Japanese models have learned the tendencies of JQaRA, a quiz-style dataset, which puts <code>bge-reranker-v2-m3</code> at a disadvantage. This is also an example of how much a reranker score can improve when the domain task is learned appropriately.</p>
<table>
<thead>
<tr>
<th>Model name</th>
<th>avg</th>
<th>JQaRA</th>
<th>JaCWIR</th>
<th>MIRACL</th>
<th>JSQuAD</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-tiny-v2">japanese-reranker-tiny-v2</a></td>
<td>0.8138</td>
<td>0.6455</td>
<td>0.9287</td>
<td>0.7201</td>
<td>0.9608</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-xsmall-v2">japanese-reranker-xsmall-v2</a></td>
<td>0.8699</td>
<td>0.7403</td>
<td>0.9409</td>
<td>0.8206</td>
<td>0.9776</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-small-v2">japanese-reranker-small-v2</a></td>
<td>0.8856</td>
<td>0.7633</td>
<td>0.9586</td>
<td>0.8385</td>
<td>0.9821</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-base-v2">japanese-reranker-base-v2</a></td>
<td>0.8930</td>
<td>0.7845</td>
<td>0.9603</td>
<td>0.8425</td>
<td>0.9845</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-xsmall-v1">japanese-reranker-cross-encoder-xsmall-v1</a></td>
<td>0.8131</td>
<td>0.6136</td>
<td>0.9376</td>
<td>0.7411</td>
<td>0.9602</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-small-v1">japanese-reranker-cross-encoder-small-v1</a></td>
<td>0.8254</td>
<td>0.6247</td>
<td>0.9390</td>
<td>0.7776</td>
<td>0.9604</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-base-v1">japanese-reranker-cross-encoder-base-v1</a></td>
<td>0.8484</td>
<td>0.6711</td>
<td>0.9337</td>
<td>0.8180</td>
<td>0.9708</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-large-v1">japanese-reranker-cross-encoder-large-v1</a></td>
<td>0.8661</td>
<td>0.7099</td>
<td>0.9364</td>
<td>0.8406</td>
<td>0.9773</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-bge-reranker-v2-m3-v1">japanese-bge-reranker-v2-m3-v1</a></td>
<td>0.8584</td>
<td>0.6918</td>
<td>0.9372</td>
<td>0.8423</td>
<td>0.9624</td>
</tr>
<tr>
<td><a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">bge-reranker-v2-m3</a></td>
<td>0.8512</td>
<td>0.6730</td>
<td>0.9343</td>
<td>0.8374</td>
<td>0.9599</td>
</tr>
<tr>
<td><a href="https://huggingface.co/cl-nagoya/ruri-v3-reranker-310m">ruri-v3-reranker-310m</a></td>
<td>0.9171</td>
<td>0.8688</td>
<td>0.9506</td>
<td>0.8670</td>
<td>0.9820</td>
</tr>
</tbody>
</table>
<h2>Inference Speed</h2>
<p>The table below shows inference time for reranking about 150,000 pairs with the Hugging Face Transformers library. Tokenization time is excluded, so this is pure model inference time. I used an M4 Max for MPS and CPU measurements, an RTX 5090 for GPU, and FlashAttention 2 for ModernBERT-family models on GPU.</p>
<p><code>japanese-reranker-tiny-v2</code> and <code>xsmall-v2</code> are clearly fast. <code>ruri-v3-reranker-310m</code> is also fast for its size, likely because FlashAttention 2 is effective. Other models can also use FlashAttention 2 through tools such as <a href="https://github.com/huggingface/text-embeddings-inference/">text-embeddings-inference</a>, and may run faster than in this evaluation.</p>
<table>
<thead>
<tr>
<th>Model name</th>
<th>Layers</th>
<th>Hidden size</th>
<th>Speed (GPU)</th>
<th>Speed (MPS)</th>
<th>Speed (CPU)</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-tiny-v2">japanese-reranker-tiny-v2</a></td>
<td>3</td>
<td>256</td>
<td>2.1s</td>
<td>82s</td>
<td>702s</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-xsmall-v2">japanese-reranker-xsmall-v2</a></td>
<td>10</td>
<td>256</td>
<td>6.5s</td>
<td>303s</td>
<td>2300s</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-small-v2">japanese-reranker-small-v2</a></td>
<td>13</td>
<td>384</td>
<td>15.2s</td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-base-v2">japanese-reranker-base-v2</a></td>
<td>19</td>
<td>512</td>
<td>32.5s</td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-xsmall-v1">japanese-reranker-cross-encoder-xsmall-v1</a></td>
<td>6</td>
<td>384</td>
<td>20.5s</td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-small-v1">japanese-reranker-cross-encoder-small-v1</a></td>
<td>12</td>
<td>384</td>
<td>40.3s</td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-base-v1">japanese-reranker-cross-encoder-base-v1</a></td>
<td>12</td>
<td>768</td>
<td>96.8s</td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-large-v1">japanese-reranker-cross-encoder-large-v1</a></td>
<td>24</td>
<td>1024</td>
<td>312.2s</td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-bge-reranker-v2-m3-v1">japanese-bge-reranker-v2-m3-v1</a></td>
<td>24</td>
<td>1024</td>
<td>310.6s</td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">bge-reranker-v2-m3</a></td>
<td>24</td>
<td>1024</td>
<td>310.7s</td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="https://huggingface.co/cl-nagoya/ruri-v3-reranker-310m">ruri-v3-reranker-310m</a></td>
<td>25</td>
<td>768</td>
<td>81.4s</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>The <a href="https://gist.github.com/hotchpotch/bab03c7d4399aa13beb2702600ad9371">benchmark script is here</a>.</p>
<p>I also publish models converted to ONNX for CPU use, so with ONNX and ARM quantized models, they should be usable even in edge environments such as Raspberry Pi.</p>
<h1>Short Technical Report</h1>
<p>The training data for <code>japanese-reranker-tiny-v2</code>, <code>xsmall-v2</code>, <code>small-v2</code>, and <code>base-v2</code> is based on the dataset used to train <a href="https://huggingface.co/hotchpotch/japanese-splade-v2">hotchpotch/japanese-splade-v2</a>, plus hard negatives and some additional private data. The large improvement over v1 likely comes from using <a href="https://huggingface.co/cl-nagoya/ruri-v3-pt-30m">ruri-v3-pt-30m</a>, a ModernBERT-based model pretrained for the target task, using several times more data than v1, and extracting higher-quality data with hard negatives, including filtering positives and negatives with scores from various rerankers.</p>
<p>For the Tiny model's parameter extraction source, I evaluated <a href="https://huggingface.co/sbintuitions/modernbert-ja-30m">sbintuitions/modernbert-ja-30m</a> and <a href="https://huggingface.co/cl-nagoya/ruri-v3-pt-30m">cl-nagoya/ruri-v3-pt-30m</a>. ModernBERT alternates global attention and local attention layers. For example, <code>modernbert-ja-30m</code> has 10 layers, where <code>[0,3,6,9]</code> are global attention layers and the others are local attention layers.</p>
<p>At first I expected all global attention layers to work best, but including layers 3, 6, and 9 generally made results worse. Including layers close to the output also made results worse. The table below shows reranking evaluation results for models trained on the same dataset. Results including layers close to the output, such as 6 and 9, were much worse and training was stopped early, so they are not included. Layer 0 alone did not produce useful performance.</p>
<table>
<thead>
<tr>
<th>name</th>
<th>JQaRA</th>
<th>miracl</th>
<th>jsquad</th>
<th>JaCWIR</th>
</tr>
</thead>
<tbody>
<tr>
<td>modernbert-ja-30m + full layers</td>
<td>0.7261</td>
<td>0.8095</td>
<td>0.9752</td>
<td>0.9420</td>
</tr>
<tr>
<td>modernbert-ja-30m + layer 0,2,4</td>
<td>0.6455</td>
<td>0.7185</td>
<td>0.9588</td>
<td>0.9265</td>
</tr>
<tr>
<td>modernbert-ja-30m + layer 0,2</td>
<td>0.6171</td>
<td>0.6784</td>
<td>0.9516</td>
<td>0.9155</td>
</tr>
<tr>
<td>modernbert-ja-30m + layer 0</td>
<td>0.2515</td>
<td>0.4416</td>
<td>0.3172</td>
<td>0.0738</td>
</tr>
<tr>
<td>ruri-v3-pt-30m + full layers (= xsmall-v2)</td>
<td>0.7403</td>
<td>0.8206</td>
<td>0.9776</td>
<td>0.9409</td>
</tr>
<tr>
<td>ruri-v3-pt-30m + layer 0,2,4  (= tiny-v2)</td>
<td>0.6455</td>
<td>0.7201</td>
<td>0.9608</td>
<td>0.9287</td>
</tr>
<tr>
<td>ruri-v3-pt-30m + layer 0,1,3</td>
<td>0.6405</td>
<td>0.7124</td>
<td>0.9552</td>
<td>0.9211</td>
</tr>
<tr>
<td>ruri-v3-pt-30m + layer 0,3</td>
<td>0.6177</td>
<td>0.6619</td>
<td>0.9482</td>
<td>0.9076</td>
</tr>
</tbody>
</table>
<p>From these results, I published <code>ruri-v3-pt-30m</code> as xsmall, and <code>ruri-v3-pt-30m + layer 0,2,4</code> as tiny. <code>small-v2</code> and <code>base-v2</code> are based on <a href="https://huggingface.co/cl-nagoya/ruri-v3-pt-70m">ruri-v3-pt-70m</a> and <a href="https://huggingface.co/cl-nagoya/ruri-v3-pt-130m">ruri-v3-pt-130m</a>, respectively. Model merging slightly improves performance, but I did not use it this time.</p>
<h1>Closing</h1>
<p>This article introduced the small, lightweight, and practical Japanese reranker models <a href="https://huggingface.co/hotchpotch/japanese-reranker-tiny-v2">japanese-reranker-tiny-v2</a>, <a href="https://huggingface.co/hotchpotch/japanese-reranker-xsmall-v2">japanese-reranker-xsmall-v2</a>, <a href="https://huggingface.co/hotchpotch/japanese-reranker-small-v2">japanese-reranker-small-v2</a>, and <a href="https://huggingface.co/hotchpotch/japanese-reranker-base-v2">japanese-reranker-base-v2</a>. The tiny and xsmall models run at practical speed on CPU and Apple silicon, and can improve search accuracy for local RAG systems without requiring expensive GPU resources. Running them on GPU also enables fast responses.</p>
<p>Recent high-performance encoder models such as ModernBERT make it easier to build practical models with stronger performance. I hope this article contributes to the further development of Japanese language processing technology.</p>]]></content:encoded>
  <pubDate>Thu, 08 May 2025 00:00:00 GMT</pubDate>
  <lastBuildDate>Thu, 08 May 2025 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>query-crafter-japanese: A Model for Generating Queries for Information Retrieval</title>
  <link>https://hotchpotch.dev/articles/query-crafter-japanese/</link>
  <guid>https://hotchpotch.dev/articles/query-crafter-japanese/</guid>
  <description>I released query-crafter-japanese, small Apache-2.0 models that generate retrieval queries from documents for synthetic IR datasets.</description>
  <content:encoded><![CDATA[<p>Training neural retrieval models such as vector search models and rerankers requires pairs of questions and answer documents. The answer document can be almost any text, though higher-quality text and domain-specific data naturally help produce better models. For training, however, we also need questions that are related to those answers. Recent LLMs have improved substantially, and we can use them to generate questions automatically from answer documents, then use those pairs for training. Datasets created this way are often called synthetic datasets.</p>
<p>However, when you want to create and publish a synthetic dataset broadly, commercial LLMs such as OpenAI and Gemini can create licensing issues because of their terms of use. Processing a large number of documents also takes significant time and cost.</p>
<p>For that reason, I created and released <code>query-crafter-japanese</code>, a family of small 1.7B to 4B models that run quickly, can generate retrieval questions at roughly the same level as questions generated by DeepSeek-R1, and do not impose restrictions on output licensing. The models are released under the Apache 2.0 license.</p>
<ul>
<li><a href="https://huggingface.co/hotchpotch/query-crafter-japanese-Qwen3-1.7B">query-crafter-japanese-Qwen3-1.7B</a>
<ul>
<li>Recommended for speed and performance</li>
</ul>
</li>
<li><a href="https://huggingface.co/hotchpotch/query-crafter-japanese-Qwen3-4B">query-crafter-japanese-Qwen3-4B</a></li>
<li><a href="https://huggingface.co/hotchpotch/query-crafter-japanese-sarashina2.2-3b-instruct-v0.1">query-crafter-japanese-sarashina2.2-3b-instruct-v0.1</a></li>
</ul>
<hr>
<p><img src="https://storage.googleapis.com/secons-site-images/other/blog_images/20250507-query-crafter-japanese-desc.jpg" alt="query-crafter-japanese-desc"></p>
<hr>
<p><code>query-crafter</code> can generate seven categories:</p>
<ul>
<li><code>keywords</code>: space-separated keywords</li>
<li><code>synonym_keywords</code>: distinctive keywords using synonyms</li>
<li><code>query</code>: a question based on the content of the text</li>
<li><code>alt_query</code>: a question phrased in a way that does not match well with BM25</li>
<li><code>title</code>: a title representing the whole text</li>
<li><code>faq</code>: a question when treating the text as an FAQ answer</li>
<li><code>summary</code>: a short summary of the text</li>
</ul>
<p>Let's generate each category from the following text.</p>
<ul>
<li><a href="https://gist.github.com/hotchpotch/8b9c9c43e6aacc14b4b47801de063d64">query-crafter-japanese-example.py</a></li>
</ul>
<pre><code>In the evening, we had the results presentation for a development retreat. Everyone except me worked on proper AI-related themes, and the quality was high. It was interesting. Person I is not even an engineer, but they made a Figma plugin and deployed it to Vercel, mostly written by Cursor. It was a close-up example of how AI can greatly expand what someone can do. I did not work on a particular theme. Instead, because I had never tried vibe coding, I tested how far I could build something in Cursor without touching or reading the code.

I made a tool that summarizes these yearly diary entries and posts them to Discord, adding new features based on a specification I had written before. I also made a tool that automatically gives titles to diary entries that do not have one. Vibe coding worked about as I expected. It is convenient.

Because I developed in a black-box way without looking at the code and only checked the output artifacts, the generated code was not production-ready when I looked at it later. Still, it was enough for quick one-off tools. I only need to give the specification, and I also make sure the specification is updated along the way. If I want to change a feature, I only need to change or add to the specification, which is easy.
</code></pre>
<p>Here is the result of generating queries by category with <code>query-crafter-japanese-Qwen3-1.7B</code>. <code>keywords</code>, <code>query</code>, <code>title</code>, and <code>summary</code> show clear differences. <code>synonym_keywords</code> is not always a perfect synonym, and <code>alt_query</code> and <code>faq</code> may sometimes be close to <code>query</code>.</p>
<pre><code>keywords: Vibe Cording ブラックボックス開発 仕様変更
synonym_keywords: AI活用開発プロジェクト 発表会 仕様変更追加
query: 開発合宿で作成したツールの具体的な機能は？
alt_query: 開発者向けツール開発でコード見ない開発手法の利点は？
title: AI活用で拓く開発の新領域：Vibe Cordingとブラックボックス開発の可能性
faq: 開発合宿で実現した新機能や成果は？
summary: AI活用の開発成果発表会で、Vibe Cordingや日記ツール開発、コード見ずに開発を実施
</code></pre>
<p>The model is also fast. In a vLLM + RTX 5090 environment, it runs at about <code>48,000 toks/s</code> for input tokens and <code>2,200 toks/s</code> for output tokens. If you generate 10,000 questions from 10,000 texts of around 1,000 Japanese characters each, it takes a little under 100 seconds. Even if there were 100 million target documents, processing all of them would take about 140 hours.</p>
<p>For comparison, when I processed 100,000 documents with DeepSeek-R1 during the nighttime discount window, with input at 0.135 USD per 1M tokens and output at 0.55 USD per 1M tokens, using 100 parallel API requests took about 7 hours and cost around 40 USD. Processing 100 million documents with the DeepSeek-R1 API would cost around 40,000 USD and take about 7,000 hours. In practice it would take longer if you try to use only nighttime discount periods, and the maximum parallel request count also depends on DeepSeek's available resources.</p>
<p>In this way, <code>query-crafter</code> has large advantages in both speed and cost when you want to generate questions from a large number of documents.</p>
<h2>Training query-crafter-japanese</h2>
<p>For training, I used DeepSeek-R1, which does not restrict output use, to create supervised question data as a synthetic dataset from <a href="https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese">fineweb-2-edu-japanese</a>.</p>
<p>For example, for <code>title</code>, I used an instruction like: "Think of and create a title that represents the whole text well. Output the title within 30 Japanese characters. The output must be strict JSON in the form <code>{"query": "title"}</code>. Do not output anything else."</p>
<ul>
<li><a href="https://huggingface.co/datasets/hotchpotch/japanese-query-crafter-reasoning-80k">https://huggingface.co/datasets/hotchpotch/japanese-query-crafter-reasoning-80k</a></li>
</ul>
<p>I then used this data as supervised data for SFT, supervised fine-tuning, on Qwen3-4B, Qwen3-1.7B, sarashina2.2-3b-instruct-v0.1, and TinySwallow-1.5B-Instruct.</p>
<p>The SFT format was simple:</p>
<pre><code>{
  "system": "{category名}",
  "user": "{text}",
  "assistant": "{query}",
}
</code></pre>
<p>The system prompt contains the instruction category such as <code>title</code>, the user input contains the document text, and the model output contains <code>query</code>. For SFT specialized to a particular use case, a verbose prompt is not necessary. A short instruction, in this case the category, can train the behavior well.</p>
<h2>Evaluation</h2>
<p>I evaluated <code>query-crafter</code> using the <a href="https://huggingface.co/datasets/hotchpotch/japanese-query-crafter-reasoning-80k/viewer/default/test">test split of japanese-query-crafter-reasoning-80k</a>. I generated questions from the text in this data using each SFT-trained query-crafter model.</p>
<p>Then I paired those generated questions with the original text and scored them with the reranker <a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">BAAI/bge-reranker-v2-m3</a>. The reranker score is 1.0 when the document and text are highly related, and 0.0 when they are not related. It is therefore a rough measure of whether the generated question is related to the text.</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Mean</th>
<th>Std. dev.</th>
</tr>
</thead>
<tbody>
<tr>
<td>query-crafter-jp-Qwen3-1.7B</td>
<td>0.8701</td>
<td>0.2592</td>
</tr>
<tr>
<td>query-crafter-jp-Qwen3-4B</td>
<td>0.8712</td>
<td>0.2652</td>
</tr>
<tr>
<td>query-crafter-jp-TinySwallow-1.5B</td>
<td>0.7526</td>
<td>0.3611</td>
</tr>
<tr>
<td>query-crafter-jp-sarashina2.2-3b</td>
<td>0.8670</td>
<td>0.2646</td>
</tr>
<tr>
<td>deepseek-r1</td>
<td>0.8507</td>
<td>0.2875</td>
</tr>
</tbody>
</table>
<p>The percentile plot is below.</p>
<p><img src="https://storage.googleapis.com/secons-site-images/other/blog_images/20250507-query-crafter-jp-percentile.jpg" alt="query-crafter-jp-percentile"></p>
<p>Except for TinySwallow-1.5B, the models scored higher than DeepSeek-R1 in most cases. In particular, Qwen3-1.7B is a multilingual model not specialized for Japanese, but after SFT its score is almost the same as Qwen3-4B. Its performance is impressive. Unless you have a specific reason to choose otherwise, <code>query-crafter-japanese-Qwen3-1.7B</code> is a good choice.</p>
<p>A lower reranker score than DeepSeek-R1 does not necessarily mean the DeepSeek-R1 question is worse. There are cases where it creates correct but difficult questions that are hard even for a reranker to judge. TinySwallow-1.5B sometimes generated questions that were completely unrelated, which lowered its score compared with the other models. TinySwallow-1.5B-Instruct was distilled with <a href="https://arxiv.org/abs/2501.16937">TAID</a>, so it may be less suitable for subsequent SFT.</p>
<h2>Closing</h2>
<p>I created and released <code>query-crafter-japanese</code>, a model with significant speed and cost advantages when generating a large number of questions. Since the release of high-performing DeepSeek-R1, which does not restrict output use, it has become easier to create and publish datasets and then build models using them as supervised data. The emergence and improvement of open-weight LLMs with practical licenses, such as smaller Qwen models, also makes it easier to create and publish fine-tuned small models specialized for specific use cases. I feel that the range of possible applications has widened considerably. Half a year earlier, creating this model as an individual would probably have been impossible for resource reasons.</p>
<p>I hope this model helps people who need to generate questions.</p>]]></content:encoded>
  <pubDate>Wed, 07 May 2025 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 07 May 2025 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>FineWeb2 Edu Japanese: A High-Quality Educational Japanese Dataset</title>
  <link>https://hotchpotch.dev/articles/fineweb-2-edu-japanese/</link>
  <guid>https://hotchpotch.dev/articles/fineweb-2-edu-japanese/</guid>
  <description>FineWeb2 Edu Japanese is a filtered Japanese educational web dataset with 120M records and about 89.3B tokens, built from FineWeb2.</description>
  <content:encoded><![CDATA[<p>I published FineWeb2 Edu Japanese, a high-quality educational Japanese dataset.</p>
<ul>
<li><a href="https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese">https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese</a></li>
</ul>
<p>The following is an English version of the content on that page.</p>
<hr>
<p><img src="https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese/resolve/main/assets/fw2.png" alt="FineWeb2 Edu Japanese image"></p>
<p>This dataset filters the Japanese portion of FineWeb2, 376 million records, down to 120 million records, about 89.3B tokens, that were judged to be educational content. It also provides the following subsets.</p>
<ul>
<li><strong>default</strong>: about 120M records and about 89.3B tokens</li>
<li><strong>sample_10BT</strong>: about 10B tokens randomly sampled from <code>default</code></li>
<li><strong>small_tokens</strong>: only short texts with 512 tokens or fewer</li>
<li><strong>small_tokens_cleaned</strong>: <code>small_tokens</code> with web-specific text noise removed</li>
</ul>
<h2>Background</h2>
<p><a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb">FineWeb</a>, which is English-only, was created to deduplicate web data and extract high-quality text. <a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb-edu">FineWeb-Edu</a>, which extracts higher-quality educational text, makes efficient training possible with fewer tokens.</p>
<p><a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb-2">FineWeb2</a>, released in December 2024, is a high-quality multilingual dataset that includes Japanese. As of February 2025, however, an "Edu" dataset that extracts educationally valuable Japanese text had not been released. For that reason, I created and published <a href="https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese">FineWeb2 Edu Japanese</a>.</p>
<h2>Filtering Educational Data</h2>
<p>To build this dataset, I filtered FineWeb2 Japanese data with <a href="https://huggingface.co/hotchpotch/fineweb-2-edu-japanese-classifier">fineweb-2-edu-japanese-classifier</a>, a model for judging whether text is educational. The supervised data for the scoring model comes from <a href="https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese-scores">fineweb-2-edu-japanese-scores</a>, which was evaluated with DeepSeek-API (<code>deepseek-chat</code>). This dataset extracts only texts with a score of 2.5 or higher, and the score is included in the <code>score</code> column.</p>
<h2>Token Counts</h2>
<p>Token counts computed with the <a href="https://huggingface.co/sbintuitions/modernbert-ja-130m">ModernBERT-Ja-130M</a> tokenizer are included in the <code>token_count</code> column.</p>
<h2>Removing Web-Specific Noise</h2>
<p>FineWeb2 Japanese data can contain web-specific boilerplate and unnecessary noise. For example, text like the following can appear.</p>
<pre><code>This text is displayed on a site that has not been updated for more than 90 days.
Login Logout

Besides the text that is actually needed, various kinds of noise may be included. This sentence is one such example. Unnecessary text can be inserted in this way.

50% off now! Click to view the linked product

Especially when the text is short, most of it may contain noise. Removing such text may allow higher-quality text to be extracted.

Previous page  Next page
</code></pre>
<p>To remove this kind of unnecessary text, I developed <a href="https://huggingface.co/hotchpotch/fineweb-2-japanese-text-cleaner">fineweb-2-japanese-text-cleaner</a>. The supervised data for noise detection is <a href="https://huggingface.co/datasets/hotchpotch/fineweb-2-japanese-noise-spans">fineweb-2-japanese-noise-spans</a>. That supervised data was created using <a href="https://huggingface.co/cyberagent/DeepSeek-R1-Distill-Qwen-32B-Japanese">cyberagent/DeepSeek-R1-Distill-Qwen-32B-Japanese</a>.</p>
<p>The model detects noisy spans as follows.</p>
<pre><code>[NOISE]This text is displayed on a site that has not been updated for more than 90 days.[/NOISE]
[NOISE]Login[/NOISE] [NOISE]Logout[/NOISE]

Besides the text that is actually needed, various kinds of noise may be included. This sentence is one such example. Unnecessary text can be inserted in this way.
[NOISE]
50% off now! Click to view the linked product[/NOISE]

Especially when the text is short, most of it may contain noise. Removing such text may allow higher-quality text to be extracted.

[NOISE]Previous page[/NOISE]  [NOISE]Next page[/NOISE]
</code></pre>
<p>The <code>small_tokens_cleaned</code> subset applies <a href="https://huggingface.co/hotchpotch/fineweb-2-japanese-text-cleaner">fineweb-2-japanese-text-cleaner</a> to <code>small_tokens</code> and removes detected noise. The raw data produced by running noise detection with the model is also published as <a href="https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese-noise-detect-raw">fineweb-2-edu-japanese-noise-detect-raw</a>.</p>
<p>Noise detection is not perfect, so in some cases parts of valid text may have been mistakenly removed.</p>
<h2>Notes</h2>
<p>I have not run a comparative experiment between this dataset, FineWeb2 Edu Japanese, and the original FineWeb2 dataset without Edu filtering. Therefore, the actual difference in effect during LLM training has not been verified.</p>
<p>The classification of whether text is educational is also not perfect, and some non-educational text is included.</p>
<h2>License</h2>
<p>This dataset is released under the <strong>Open Data Commons Attribution License (ODC-By) v1.0</strong>, the same as the original FineWeb2. The <a href="https://commoncrawl.org/terms-of-use">Common Crawl terms of use</a> also apply.</p>
<h2>Citation Information</h2>
<pre><code>@software{yuichi2025fineweb-2-edu-japanese,
  author = {Yuichi Tateno},
  title = {FineWeb2 Edu Japanese},
  month = feb,
  year = 2025,
  url = {https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese/}
}
</code></pre>]]></content:encoded>
  <pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate>
  <lastBuildDate>Thu, 20 Feb 2025 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Releasing a Japanese StaticEmbedding Model for Practical 100x Faster Text Embeddings</title>
  <link>https://hotchpotch.dev/articles/static-embedding-japanese/</link>
  <guid>https://hotchpotch.dev/articles/static-embedding-japanese/</guid>
  <description>I released static-embedding-japanese, a fast non-Transformer embedding model for Japanese and English text, and evaluated it on JMTEB.</description>
  <content:encoded><![CDATA[<p>Dense text vectors can be used for many tasks, including information retrieval, text classification, and similar-text extraction. However, even small recent Transformer models can be slow, especially on CPU, and that often makes them impractical.</p>
<p>A recently released non-Transformer <a href="https://huggingface.co/blog/static-embeddings">StaticEmbedding model</a> offers a new approach. In benchmark comparisons with <a href="https://huggingface.co/intfloat/multilingual-e5-small">intfloat/multilingual-e5-small</a>, or mE5-small, it achieved at least reasonable performance, around 85% of the score, while creating sentence vectors 126 times faster on CPU. That speed is impressive.</p>
<p>I therefore trained and released a Japanese and English model, <code>static-embedding-japanese</code>.</p>
<ul>
<li><a href="https://huggingface.co/hotchpotch/static-embedding-japanese">https://huggingface.co/hotchpotch/static-embedding-japanese</a></li>
</ul>
<p>The JMTEB results for Japanese text embeddings are below. The overall score is slightly below mE5-small, but it wins on some tasks and is sometimes stronger than other Japanese base-size BERT models. Before training it, I was not sure a model this simple would really perform this well, so the result was surprising.</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Avg(micro)</th>
<th>Retrieval</th>
<th>STS</th>
<th>Classification</th>
<th>Reranking</th>
<th>Clustering</th>
<th>PairClassification</th>
</tr>
</thead>
<tbody>
<tr>
<td>text-embedding-3-small</td>
<td>69.18</td>
<td>66.39</td>
<td>79.46</td>
<td>73.06</td>
<td>92.92</td>
<td>51.06</td>
<td>62.27</td>
</tr>
<tr>
<td>multilingual-e5-small</td>
<td>67.71</td>
<td>67.27</td>
<td>80.07</td>
<td>67.62</td>
<td>93.03</td>
<td>46.91</td>
<td>62.19</td>
</tr>
<tr>
<td><strong>static-embedding-japanese</strong></td>
<td>67.17</td>
<td><strong>67.92</strong></td>
<td><strong>80.16</strong></td>
<td><strong>67.96</strong></td>
<td>91.87</td>
<td>40.39</td>
<td><strong>62.37</strong></td>
</tr>
</tbody>
</table>
<p>Technical notes on training the Japanese StaticEmbedding model are in the latter half of this article.</p>
<h2>Usage</h2>
<p>Usage is simple. You can create sentence vectors with SentenceTransformer as usual. This example runs on CPU without a GPU. I tested with SentenceTransformer 3.3.1.</p>
<pre><code>pip install "sentence-transformers>=3.3.1"
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> sentence_transformers <span class="hljs-keyword">import</span> SentenceTransformer

model_name = <span class="hljs-string">"hotchpotch/static-embedding-japanese"</span>
model = SentenceTransformer(model_name, device=<span class="hljs-string">"cpu"</span>)

query = <span class="hljs-string">"美味しいラーメン屋に行きたい"</span>
docs = [
    <span class="hljs-string">"素敵なカフェが近所にあるよ。落ち着いた雰囲気でゆっくりできるし、窓際の席からは公園の景色も見えるんだ。"</span>,
    <span class="hljs-string">"新鮮な魚介を提供する店です。地元の漁師から直接仕入れているので鮮度は抜群ですし、料理人の腕も確かです。"</span>,
    <span class="hljs-string">"あそこは行きにくいけど、隠れた豚骨の名店だよ。スープが最高だし、麺の硬さも好み。"</span>,
    <span class="hljs-string">"おすすめの中華そばの店を教えてあげる。とりわけチャーシューが手作りで柔らかくてジューシーなんだ。"</span>,
]

embeddings = model.encode([query] + docs)
<span class="hljs-built_in">print</span>(embeddings.shape)
similarities = model.similarity(embeddings[<span class="hljs-number">0</span>], embeddings[<span class="hljs-number">1</span>:])
<span class="hljs-keyword">for</span> i, similarity <span class="hljs-keyword">in</span> <span class="hljs-built_in">enumerate</span>(similarities[<span class="hljs-number">0</span>].tolist()):
    <span class="hljs-built_in">print</span>(<span class="hljs-string">f"<span class="hljs-subst">{similarity:<span class="hljs-number">.04</span>f}</span>: <span class="hljs-subst">{docs[i]}</span>"</span>)
</code></pre>
<pre><code>(5, 1024)
0.1040: 素敵なカフェが近所にあるよ。落ち着いた雰囲気でゆっくりできるし、窓際の席からは公園の景色も見えるんだ。
0.2521: 新鮮な魚介を提供する店です。地元の漁師から直接仕入れているので鮮度は抜群ですし、料理人の腕も確かです。
0.4835: あそこは行きにくいけど、隠れた豚骨の名店だよ。スープが最高だし、麺の硬さも好み。
0.3199: おすすめの中華そばの店を教えてあげる。とりわけチャーシューが手作りで柔らかくてジューシーなんだ。
</code></pre>
<p>The document matching the query gets a higher score. In this example, BM25 would have difficulty because direct words such as "ramen" in the query do not appear in the documents.</p>
<p>Here is an example of a similar-sentence task:</p>
<pre><code class="hljs language-python">sentences = [
    <span class="hljs-string">"明日の午後から雨が降るみたいです。"</span>,
    <span class="hljs-string">"来週の日曜日は天気が良いそうだ。"</span>,
    <span class="hljs-string">"あしたの昼過ぎから傘が必要になりそう。"</span>,
    <span class="hljs-string">"週末は晴れるという予報が出ています。"</span>,
]

embeddings = model.encode(sentences)
similarities = model.similarity(embeddings, embeddings)

<span class="hljs-built_in">print</span>(similarities)

<span class="hljs-comment"># Show similarity between the first sentence and the others.</span>
<span class="hljs-keyword">for</span> i, similarity <span class="hljs-keyword">in</span> <span class="hljs-built_in">enumerate</span>(similarities[<span class="hljs-number">0</span>].tolist()):
    <span class="hljs-built_in">print</span>(<span class="hljs-string">f"<span class="hljs-subst">{similarity:<span class="hljs-number">.04</span>f}</span>: <span class="hljs-subst">{sentences[i]}</span>"</span>)
</code></pre>
<pre><code>tensor([[1.0000, 0.2814, 0.3620, 0.2818],
        [0.2814, 1.0000, 0.2007, 0.5372],
        [0.3620, 0.2007, 1.0000, 0.1299],
        [0.2818, 0.5372, 0.1299, 1.0000]])
1.0000: 明日の午後から雨が降るみたいです。
0.2814: 来週の日曜日は天気が良いそうだ。
0.3620: あしたの昼過ぎから傘が必要になりそう。
0.2818: 週末は晴れるという予報が出ています。
</code></pre>
<p>The similar sentence receives a higher score here as well.</p>
<p>Many people have experienced that creating sentence vectors with Transformer models on CPU takes a long time even for a small amount of text. With StaticEmbedding, it should finish almost instantly if the CPU is reasonably fast.</p>
<h3>Reducing Output Dimensions</h3>
<p>The default sentence vector has 1024 dimensions, but you can reduce it further. For example, here is <code>truncate_dim=128</code>.</p>
<pre><code class="hljs language-python"><span class="hljs-comment"># truncate_dim can be 32, 64, 128, 256, 512, or 1024.</span>
model = SentenceTransformer(model_name, device=<span class="hljs-string">"cpu"</span>, truncate_dim=<span class="hljs-number">128</span>)
</code></pre>
<p>This produces 128-dimensional vectors. The score changes slightly because reducing dimensions lowers performance a little. On the other hand, reducing from 1024 to 128 dimensions reduces storage size and makes similarity computation about 8 times cheaper, so lower dimensions can be preferable depending on the use case.</p>
<h2>Why Is CPU Inference Fast?</h2>
<p>StaticEmbedding is not a Transformer model. It has no attention computation, the core of "Attention Is All You Need." It stores token embeddings in a 1024-dimensional table and creates a sentence vector by averaging the token vectors that appear in the sentence. Because there is no attention, it does not understand context in the same way a Transformer does.</p>
<p>Internally, it uses PyTorch's <code>nn.EmbeddingBag</code>, passing concatenated tokens and offsets so that PyTorch can use optimized CPU parallel processing and memory access.</p>
<p><img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/static-embeddings/similarity_speed.png" alt=""></p>
<p>According to the <a href="https://huggingface.co/blog/static-embeddings#multilingual-similarity-4">speed evaluation in the original article</a>, it is 126 times faster than mE5-small on CPU.</p>
<h2>Evaluation</h2>
<p>All JMTEB results are in <a href="https://huggingface.co/hotchpotch/static-embedding-japanese/blob/main/JMTEB/summary.json">this JSON file</a>. Comparing with other models on the <a href="https://github.com/sbintuitions/JMTEB/blob/main/leaderboard.md">JMTEB leaderboard</a> shows the relative difference. Considering the model size, the overall JMTEB result is very good. The Mr. TyDi task in JMTEB requires vectorizing 7 million documents and usually takes a long time, around 1 to 4 hours on an RTX 4090 depending on the model. StaticEmbeddings processed it very quickly, finishing in about 4 minutes on an RTX 4090.</p>
<h3>Can It Replace BM25 for Retrieval?</h3>
<p>Looking at the <a href="https://huggingface.co/hotchpotch/static-embedding-japanese/blob/main/JMTEB/summary.json#L21-L39">Retrieval results</a>, StaticEmbedding performs very poorly on Mr. TyDi. Mr. TyDi has far more documents than the other tasks, 7 million documents, so results may be poor for tasks that search over very large document collections. Since the model simply averages tokens without considering context, the more documents there are, the more likely similar averages may appear.</p>
<p>For large document collections, it may perform much worse than BM25. For smaller collections where exact keyword matches are rare, it may often perform better than BM25.</p>
<p>The JAQKET retrieval score is unusually good compared with other models. This may be because the model trained on JQaRA dev and unused data, which includes JAQKET-style questions, but the score still feels high. I do not think the test data leaked, but I am not fully sure why the score is this good.</p>
<h3>Clustering Is Weak</h3>
<p>I have not investigated this in detail, but the clustering score is clearly worse than other models. Classification is not bad, so this is somewhat surprising. It may be related to the embedding space being created with Matryoshka Representation Learning.</p>
<h2>JQaRA and JaCWIR Reranking Evaluation</h2>
<p><a href="https://huggingface.co/datasets/hotchpotch/JQaRA">JQaRA</a>:</p>
<table>
<thead>
<tr>
<th align="left">model_names</th>
<th align="right">ndcg@10</th>
<th align="right">mrr@10</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><a href="https://huggingface.co/hotchpotch/static-embedding-japanese">static-embedding-japanese</a></td>
<td align="right">0.4704</td>
<td align="right">0.6814</td>
</tr>
<tr>
<td align="left">bm25</td>
<td align="right">0.458</td>
<td align="right">0.702</td>
</tr>
<tr>
<td align="left"><a href="https://huggingface.co/intfloat/multilingual-e5-small">multilingual-e5-small</a></td>
<td align="right">0.4917</td>
<td align="right">0.7291</td>
</tr>
</tbody>
</table>
<p><a href="https://huggingface.co/datasets/hotchpotch/JaCWIR">JaCWIR</a>:</p>
<table>
<thead>
<tr>
<th align="left">model_names</th>
<th align="right">map@10</th>
<th align="right">hits@10</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><a href="https://huggingface.co/hotchpotch/static-embedding-japanese">static-embedding-japanese</a></td>
<td align="right">0.7642</td>
<td align="right">0.9266</td>
</tr>
<tr>
<td align="left">bm25</td>
<td align="right">0.8408</td>
<td align="right">0.9528</td>
</tr>
<tr>
<td align="left"><a href="https://huggingface.co/intfloat/multilingual-e5-small">multilingual-e5-small</a></td>
<td align="right">0.869</td>
<td align="right">0.97</td>
</tr>
</tbody>
</table>
<p>On JQaRA it is slightly better than BM25 and slightly worse than mE5-small. On JaCWIR it is much lower than BM25 and mE5-small.</p>
<p>JaCWIR asks the model to find web article titles and summaries from queries, and those texts are often not clean. Transformer models are robust to noise, so it makes sense that a simple token-average StaticEmbedding model falls behind. BM25 matches distinctive words, so noisy words in documents often do not match the query in the first place, which helps it remain competitive with Transformer models on JaCWIR.</p>
<p>This suggests StaticEmbedding may score poorly compared with Transformer models or BM25 when texts contain a lot of noise.</p>
<h2>Reducing Output Dimensions</h2>
<p>The model created here outputs 1024 dimensions. Higher dimensionality increases computation cost for downstream tasks such as clustering and retrieval. Because the model is trained with <a href="https://arxiv.org/abs/2205.13147">Matryoshka Representation Learning (MRL)</a>, however, the 1024-dimensional vector can be easily truncated to smaller dimensions.</p>
<p>MRL encourages earlier dimensions to hold more important information, so using only the first 32, 64, 128, or 256 dimensions can still produce reasonable results.</p>
<p><img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/static-embeddings/nano_beir_matryoshka.png" alt=""></p>
<p>According to the <a href="https://huggingface.co/blog/static-embeddings#matryoshka-evaluation">StaticEmbedding article</a>, the model retains 91.87% performance at 128 dimensions, 95.79% at 256 dimensions, and 98.53% at 512 dimensions. This is useful when accuracy requirements are not too strict and downstream computation should be reduced.</p>
<h3>Dimension Reduction Results for static-embedding-japanese</h3>
<p>JMTEB can pass <code>truncate_dim</code>, making it easy to benchmark dimension-reduced outputs.</p>
<table>
<thead>
<tr>
<th>Dimensions</th>
<th>Avg(micro)</th>
<th>Score ratio (%)</th>
<th>Retrieval</th>
<th>STS</th>
<th>Classification</th>
<th>Reranking</th>
<th>Clustering</th>
<th>PairClassification</th>
</tr>
</thead>
<tbody>
<tr>
<td>1024</td>
<td>67.17</td>
<td>100.00</td>
<td>67.92</td>
<td>80.16</td>
<td>67.96</td>
<td>91.87</td>
<td>40.39</td>
<td>62.37</td>
</tr>
<tr>
<td>512</td>
<td>66.57</td>
<td>99.10</td>
<td>67.63</td>
<td>80.11</td>
<td>65.66</td>
<td>91.54</td>
<td>41.25</td>
<td>62.37</td>
</tr>
<tr>
<td>256</td>
<td>65.94</td>
<td>98.17</td>
<td>66.99</td>
<td>79.93</td>
<td>63.53</td>
<td>91.73</td>
<td>42.55</td>
<td>62.37</td>
</tr>
<tr>
<td>128</td>
<td>64.25</td>
<td>95.65</td>
<td>64.87</td>
<td>79.56</td>
<td>60.52</td>
<td>91.62</td>
<td>41.81</td>
<td>62.33</td>
</tr>
<tr>
<td>64</td>
<td>61.79</td>
<td>91.98</td>
<td>61.15</td>
<td>78.34</td>
<td>58.23</td>
<td>91.50</td>
<td>39.11</td>
<td>62.35</td>
</tr>
<tr>
<td>32</td>
<td>57.93</td>
<td>86.24</td>
<td>53.35</td>
<td>76.51</td>
<td>55.95</td>
<td>91.15</td>
<td>38.20</td>
<td>62.37</td>
</tr>
</tbody>
</table>
<p>I had previously measured the 512-dimensional score incorrectly and corrected it. Matryoshka Representation Learning appears to work: reducing dimensions causes a small score drop, but the reduced dimensions should lower downstream cost.</p>
<p>Interestingly, clustering improves over 1024 dimensions even when reduced to 128 dimensions. Normally, keeping more information should help, so this is unexpected. It may mean that, for clustering, using only the earlier dimensions that capture more global features works better than using later dimensions, depending on the clustering algorithm.</p>
<p>For this model, 512, 256, and 128 dimensions seem like reasonable tradeoffs between performance and dimensionality reduction.</p>
<h2>Impressions After Building a StaticEmbedding Model</h2>
<p>I was honestly skeptical that a simple average of token embeddings could perform this well, but after training it, I was surprised by the performance of such a simple architecture. In an era dominated by Transformers, it is interesting to see a practical model based on a more traditional word-embedding style approach.</p>
<p>A fast CPU sentence embedding model should be useful for converting large amounts of text locally, edge devices, and environments with slow networks where calling a remote inference server is difficult.</p>
<hr>
<h1>Technical Notes on Training the Japanese StaticEmbedding Model</h1>
<h2>Why Training Works</h2>
<p>StaticEmbedding is very simple. It tokenizes a sentence, obtains N-dimensional word embeddings from an <code>EmbeddingBag</code> table, 1024 dimensions in this model, and averages them.</p>
<p>Traditional word embeddings such as word2vec and GloVe learn from word context with Skip-gram or CBOW. StaticEmbedding instead trains with entire sentences. It uses contrastive learning with large batches over many kinds of text, which can learn useful word embeddings.</p>
<p>Contrastive learning treats everything except the positive as a negative. With batch size 2048, one positive is compared against 2047 negatives for 2048 examples, about 4 million comparisons. This allows the model to update weights appropriately over the original word space.</p>
<h2>Training Datasets</h2>
<p>For the Japanese model, I created and used datasets suitable for contrastive learning:</p>
<ul>
<li><a href="https://huggingface.co/datasets/hotchpotch/sentence_transformer_japanese">hotchpotch/sentence_transformer_japanese</a>
<ul>
<li>This is arranged with column names and structures easy to use with <a href="https://sbert.net/docs/sentence_transformer/loss_overview.html">SentenceTransformer training</a>, such as <code>(anchor, positive)</code>, <code>(anchor, positive, negative)</code>, and <code>(anchor, positive, negative_1, ..., negative_n)</code>.</li>
<li>It is based on datasets including <code>hpprc/emb</code>, <code>hpprc/llmjp-kaken</code>, <code>hpprc/msmarco-ja</code>, <code>hpprc/mqa-ja</code>, and <code>hpprc/llmjp-warp-html</code>. For <code>hpprc/emb</code> and <code>msmarco-ja</code>, I filtered positives and negatives with reranker scores, using positive(>=0.7) and negative(&#x3C;=0.3).</li>
<li>I used many subsets from the constructed dataset, with augmentation to increase the amount of information retrieval-oriented data.</li>
</ul>
</li>
<li>For English data, I used datasets such as <code>sentence-transformers/msmarco-co-condenser-margin-mse-sym-mnrl-mean-v1</code>, <code>sentence-transformers/squad</code>, <code>sentence-transformers/all-nli</code>, <code>sentence-transformers/trivia-qa</code>, <code>nthakur/swim-ir-monolingual</code>, <code>sentence-transformers/miracl</code>, and <code>sentence-transformers/mr-tydi</code>.</li>
</ul>
<p>As always, I am grateful to the dataset authors, especially hpprc.</p>
<h2>Japanese Tokenizer</h2>
<p>To train StaticEmbedding, it seemed easiest to use a tokenizer that can be processed in Hugging Face's <code>tokenizer.json</code> format, so I created <a href="https://huggingface.co/hotchpotch/xlm-roberta-japanese-tokenizer">hotchpotch/xlm-roberta-japanese-tokenizer</a>, with a vocabulary size of 32,768.</p>
<p>This tokenizer was trained by segmenting Japanese Wikipedia data with UniDic and training SentencePiece unigram. I originally thought it also used sampled English Wikipedia and Japanese CC-100, but after checking the creation code, it used only Japanese Wikipedia. It also works as an XLM-Roberta-style Japanese tokenizer. I used this tokenizer for the model.</p>
<h2>Hyperparameters</h2>
<p>Notes and changes from the <a href="https://huggingface.co/blog/static-embeddings#english-retrieval-2">original training code</a>:</p>
<ul>
<li>Batch size was changed from 2048 to 6072.
<ul>
<li>In large-batch contrastive learning, having positives and negatives in the same batch can hurt training. <code>BatchSamplers.NO_DUPLICATES</code> avoids this, but sampling can become slow with huge batches.</li>
<li>I used <code>BatchSamplers.NO_DUPLICATES</code> and set the batch size to 6072, which fit in 24 GB on an RTX 4090. Larger batches may produce better results.</li>
</ul>
</li>
<li>Epochs were changed from 1 to 2.
<ul>
<li>2 epochs performed better than 1, though with a larger dataset, 1 might be better.</li>
</ul>
</li>
<li>Scheduler:
<ul>
<li>Changed from the default linear scheduler to cosine, which has often worked better in my experience.</li>
</ul>
</li>
<li>Optimizer:
<ul>
<li>Kept the default AdamW. Switching to Adafactor made convergence worse.</li>
</ul>
</li>
<li>Learning rate:
<ul>
<li>Kept <code>2e-1</code>. I wondered whether it was too large, but lower values worsened results.</li>
</ul>
</li>
<li><code>dataloader_prefetch_factor=4</code></li>
<li><code>dataloader_num_workers=15</code>
<ul>
<li>Tokenization and batch sampler sampling take time, so I set this relatively high.</li>
</ul>
</li>
</ul>
<h2>Training Resources</h2>
<ul>
<li>CPU: Ryzen 9 7950X</li>
<li>GPU: RTX 4090</li>
<li>Memory: 64 GB</li>
</ul>
<p>With these resources, full-scratch training took about 4 hours. GPU core load was very low, often near 0%, unlike Transformer training where it stays around 90%. Most of the time appears to be spent transferring huge batches into GPU memory. Faster GPU memory bandwidth may improve training speed further.</p>
<h2>Further Improvements</h2>
<p>The tokenizer used here is not specialized for StaticEmbedding, so a more suitable tokenizer may improve performance. Larger batch sizes may also stabilize training and improve performance.</p>
<p>Using broader text resources, including various domains and synthetic datasets, may further improve performance.</p>
<h2>Training Code</h2>
<p>The training code is published under the MIT license. Running the script should reproduce the model.</p>
<ul>
<li><a href="https://huggingface.co/hotchpotch/static-embedding-japanese/blob/main/trainer.py">https://huggingface.co/hotchpotch/static-embedding-japanese/blob/main/trainer.py</a></li>
</ul>
<h2>License</h2>
<p><code>static-embedding-japanese</code> publishes model weights and training code under the MIT license.</p>]]></content:encoded>
  <pubDate>Tue, 21 Jan 2025 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 21 Jan 2025 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Looking Back on 2024</title>
  <link>https://hotchpotch.dev/articles/furikaeri-2024/</link>
  <guid>https://hotchpotch.dev/articles/furikaeri-2024/</guid>
  <description>A personal look back at 2024, covering life, building a house, work on AI and information retrieval, and technical projects.</description>
  <content:encoded><![CDATA[<p>The year is coming to an end, so here is a short look back at 2024.</p>
<p><img src="https://i.imgur.com/0zZSHkA.jpeg" alt="2025 is the year of the snake"></p>
<hr>
<h2>Life</h2>
<p>In 2024 I was able to live at my own pace. Because we were building a house, there were many meetings and site visits, so I could not take a long overseas trip. Still, I traveled a bit both in Japan and abroad, and overall it was an active enough year.</p>
<p>On the health side, I hit my highest body weight so far. I started going to a personal gym partly to diet, lost 5 kg, and got back to a weight from a while ago. Ideally I would like to lose another 5 kg and gain more muscle, but that probably requires exercising properly rather than only going to the gym once a week.</p>
<p>And the house is finally built. We moved in right at the end of the year, so we have only been living here for about six days, but so far it has been very comfortable. The wood stove has been running almost all the time except when we are sleeping, and it is warm and excellent.</p>
<h2>Work</h2>
<p>At work, I was able to do a lot of what I wanted as a product manager and data scientist, mainly in information retrieval, on an AI-related product that started this year. I also worked with teammates who had expertise I did not have, and it was an enjoyable and fulfilling year. I think we built a fairly good product. It has had a quiet release, and next year I think it will probably be used more widely. More precisely, I want to help make that happen.</p>
<h2>Technology and Hobbies</h2>
<p>Since the second half of 2023, information retrieval has become much more interesting to me. I started working on it professionally as well, and as a hobby I created datasets, built rerankers and retrieval models, and published some useful outputs. Training LLMs is difficult with my machine resources, but it is possible to build models that fill a missing piece for a specific task. In Japanese, relatively few people publish this kind of work, so if the task is specific enough it is still possible to build a strong model. That has been fun.</p>
<p>In information retrieval, NLP, and machine learning, the more I work on them, the more knowledge keeps accumulating. There is still so much I want to do and learn. In 2025 I want to keep this area as a technical focus, build things as a hobby, and apply what I learn at work.</p>
<p>At the same time, I may have spent too much private time on work-adjacent technology. Most mornings and evenings were spent building models as a hobby. I also feel that life might be richer if I enjoyed a wider range of things outside work, so I want to pay attention to that balance in 2025.</p>
<h2>Summary</h2>
<p>Looking back, the year went by quickly. It feels like I did quite a lot and also like there is still a lot I did not do. I think I recovered the pace I lost in 2023, and I was able to do various things with a balance that suits me.</p>
<p>As always, I am grateful to my wife, who supports me in many ways. With that, I hope to see you all in 2025.</p>
<ul>
<li><a href="/articles/furikaeri-2023">Looking Back on 2023</a></li>
<li><a href="https://secon.dev/entry/2022/12/31/070000/">Looking Back on 2022</a></li>
<li><a href="https://secon.dev/entry/2021/12/31/070000/">Looking Back on 2021</a></li>
<li><a href="https://secon.dev/entry/2020/12/31/080000-hurikaeri-2020/">Looking Back on 2020</a></li>
</ul>]]></content:encoded>
  <pubDate>Tue, 31 Dec 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 31 Dec 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Releasing Japanese SPLADE v2, a Strong Retrieval Model for Texts Under 512 Tokens</title>
  <link>https://hotchpotch.dev/articles/japanese-splade-v2-release/</link>
  <guid>https://hotchpotch.dev/articles/japanese-splade-v2-release/</guid>
  <description>I released japanese-splade-v2, an improved Japanese SPLADE model with strong JMTEB retrieval scores for documents up to 512 tokens.</description>
  <content:encoded><![CDATA[<p>In 2024 I became interested in information retrieval and have been building retrieval-related models as a hobby as <a href="https://huggingface.co/hotchpotch">@hotchpotch</a>. Transformers are enjoyable because they often learn reasonably well even when the setup is not overly elaborate.</p>
<p>I have been running consumer GPUs at home and released <a href="https://huggingface.co/hotchpotch/japanese-splade-v2">Japanese SPLADE v2, japanese-splade-v2</a>, an improved version of the Japanese SPLADE v1 retrieval model I published earlier. On <a href="https://huggingface.co/datasets/sbintuitions/JMTEB">JMTEB</a> retrieval benchmarks, it achieves very strong scores for document lengths up to 512 tokens, which is common in RAG. Considering the model size and performance, I think it is a well-balanced retrieval model.</p>
<p><img src="https://i.imgur.com/JVEu0zP.png" alt="Characteristics of Japanese SPLADE v2"></p>
<p>This article is also day 24 of the <a href="https://qiita.com/advent-calendar/2024/search">Information Retrieval / Search Technology Advent Calendar 2024</a>.</p>
<h2>What Is SPLADE?</h2>
<p>SPLADE is probably unfamiliar to many people, so before explaining SPLADE, I will briefly discuss dense vector search and sparse vector search.</p>
<p>When people talk about natural language search today, the popular approach is dense retrieval, also called text embeddings or embedding search. However, sparse retrieval is still actively used in many places. Keyword-based methods such as TF-IDF and BM25 are representative examples.</p>
<p>Suppose you search for "Tell me a good cafe." Sparse vector search, such as TF-IDF or BM25, scores how important keywords are and returns results. In this case, the results depend on the frequency and rarity of words such as "good" and "cafe". Documents with matching distinctive keywords tend to rank higher.</p>
<p>Dense retrieval represents the meaning of words and phrases as vectors. For "good cafe", it can also retrieve contextually related phrases such as "popular coffee shop" or "pleasant cafe". This is because the neural network model, usually a Transformer, has learned broad semantic representations of words and sentences.</p>
<p>In short, sparse vectors such as BM25 emphasize the keywords themselves, while dense vectors emphasize the meaning and nuance of the keywords. Which one to use depends on whether exact keyword matching or semantic breadth is more important.</p>
<h3>Difference in Dimensions</h3>
<p>Dense and sparse vectors also differ in the number of dimensions used to represent information.</p>
<p>Dense vectors typically have large dimensionality, often from 384 to 3072 dimensions, and sometimes more depending on the model. For example, OpenAI's <code>text-embedding-3-large</code> uses 3072 dimensions by default. Higher dimensionality means that vector computations, such as dot products or cosine similarity, become more expensive and require more storage and memory. This is one of the challenges of dense vectors.</p>
<p>In real search systems, searching all high-dimensional vectors exactly is too inefficient, so algorithms such as <a href="https://www.elastic.co/jp/blog/understanding-ann">approximate nearest neighbor search, ANN</a>, are used to trade a little accuracy for better computational efficiency.</p>
<p>Sparse vectors, when keyword-based, theoretically use the vocabulary of the whole document collection as dimensions, creating a large vector space. In practice, however, most dimensions are zero, and only a small number of elements are non-zero. A query such as "Tell me a good cafe" uses only the dimensions corresponding to words like "good", "cafe", and "tell". The other tens or hundreds of thousands of possible dimensions remain zero. This greatly reduces storage, memory, and computation in production and enables fast search.</p>
<p>Sparse vectors also have the advantage that it is easy to understand what each non-zero dimension means. It is clear which dimensions correspond to keywords such as "good" and "cafe", making results easier to interpret.</p>
<p>Here is an example of a dense vector:</p>
<pre><code>dense_vector = [
 0.0023, -0.0008, 0.0017, 0.0009, -0.0025,
 ... # elements continue for the number of dimensions
]
</code></pre>
<p>All dimensions in a dense vector have meaning, but it is hard to understand what each value specifically represents.</p>
<p>A sparse vector, on the other hand, has an easier-to-understand structure:</p>
<pre><code>sparse_vector = {
  33721: 1.5, # dimension 33721 corresponds to "good"
  1191: 2.3, # dimension 1191 corresponds to "cafe"
  997: 0.2 # dimension 997 corresponds to "tell"; frequent words have lower scores
  # all other dimensions are zero and do not need to be written
}
</code></pre>
<p>In this example, it is clear which dimensions correspond to "good", "cafe", and "tell". This makes it easier to interpret which words contributed to the search result.</p>
<p>Dense vectors are good at capturing broad meaning, but their many dimensions make them computationally expensive. Sparse vectors are efficient and especially useful for precise keyword search.</p>
<h3>Weaknesses of Sparse Vector Search</h3>
<p>Sparse vectors use relatively few active dimensions and make it easy to understand which words matched. That may sound like an obvious win.</p>
<p>However, dense retrieval is popular for natural language search because of accuracy. Algorithms such as BM25 basically match predefined keywords and manually maintained synonym dictionaries. If you search for "good cafe", BM25 will usually not match "tasty coffee shop" unless the keywords align. Dense vectors use fuzzier semantic representations and can match texts like "tasty coffee shop" as similar.</p>
<h2>SPLADE: Sparse Vectors That Understand and Expand Context</h2>
<p>Sparse vector search is well suited to exact keyword matching, such as e-commerce search where similar but different products can be wrong. For natural language queries, dense retrieval often seems more suitable.</p>
<p>As people increasingly want systems to find target documents from casual natural language, similar to talking with AI, dense retrieval models have become popular.</p>
<p>This is where SPLADE, Sparse Lexical and Expansion Model, comes in. SPLADE's key feature is that it understands context and proposes multiple appropriate words or tokens. For example, for the query "What time of day has the highest household TV rating in Japan?", SPLADE can output related terms inferred from context, not only words directly included in the query.</p>
<ul>
<li>Words directly included in the query:
<ul>
<li>Japan</li>
<li>viewing</li>
<li>household</li>
<li>time</li>
</ul>
</li>
<li>Related words inferred from context:
<ul>
<li>TV and broadcasting: broadcast, program, slot</li>
<li>Metrics: rate, rise, high</li>
<li>Time-related: time, period</li>
</ul>
</li>
</ul>
<p>Traditional sparse vector search could only find documents where the entered keywords matched exactly. SPLADE can understand context and search with related words as well, while preserving the fast retrieval performance of sparse vectors.</p>
<h3>Efficient Retrieval</h3>
<p>SPLADE can perform this kind of advanced search efficiently.</p>
<pre><code>For example, for the query "What time of day has the highest household TV rating in Japan?"
sparse_vector = {
    1423: 1.71,  # corresponds to "Japan"
    5891: 1.59,  # corresponds to "viewing"
    8754: 1.57,  # corresponds to "household"
    2341: 1.33,  # corresponds to "time"
    9876: 0.96,  # corresponds to "broadcast"
    # ...other related dimensions
}
</code></pre>
<p>Only the necessary information is stored as a sparse vector, and matching uses a small number of dimensions. The important point is that SPLADE scores are not simple frequencies; they represent contextual importance.</p>
<h3>Why SPLADE?</h3>
<p>Compared with other retrieval approaches:</p>
<ul>
<li>Traditional sparse retrieval such as BM25
<ul>
<li>Depends on keyword combinations such as "rating" + "time slot"</li>
<li>Weak against paraphrases such as "broadcast peak time"</li>
<li>Strong for exact keyword matches</li>
<li>Easy to explain results</li>
</ul>
</li>
<li>Dense retrieval
<ul>
<li>Represents queries and documents with dense vectors, so stronger accuracy often requires larger models and higher vector dimensions
<ul>
<li>This affects inference speed and search speed</li>
</ul>
</li>
<li>Results are harder to interpret</li>
</ul>
</li>
<li>SPLADE, context-aware sparse retrieval
<ul>
<li>Can search with contextual understanding</li>
<li>Maintains fast search performance
<ul>
<li>Queries are often around 20-40 dimensions and documents around 150-400 dimensions</li>
<li>Runtime tradeoffs between accuracy and speed are possible by not searching or indexing low-importance words</li>
</ul>
</li>
<li>Results are easy to interpret because you can see which word tokens matched</li>
</ul>
</li>
</ul>
<p>SPLADE balances many requirements of modern search systems.</p>
<h2>How Good Is the Actual Performance?</h2>
<p>Let's look at performance, especially the ability to retrieve appropriate documents for natural language questions.</p>
<p><img src="https://i.imgur.com/pnHIvoi.png" alt="Japanese SPLADE v2 scores"></p>
<p>This is the JMTEB retrieval benchmark result, nDCG@10. For texts of 512 tokens or fewer, Japanese SPLADE v2 achieves the best score on most tasks. The benchmark tasks <code>nlp_journal_abs_intro</code> and <code>nlp_journal_title_intro</code> contain documents longer than 512 tokens, so models with shorter maximum input lengths score lower across the board.</p>
<p>In practical use cases such as retrieval for RAG, documents are often split into smaller chunks, so depending on the use case, handling only up to 512 tokens may not be a problem.</p>
<p>The JMTEB retrieval datasets are roughly:</p>
<ul>
<li>JaGovFaqs_22k
<ul>
<li>QA dataset based on Japanese government agency FAQs</li>
<li>Queries: 3,420</li>
<li>Documents: 22,794</li>
<li>Mostly 512 tokens or fewer</li>
</ul>
</li>
<li>Mr. TyDi
<ul>
<li>Retrieval benchmark of manually created questions and related Wikipedia passages</li>
<li>Queries: 720</li>
<li>Documents: 7,000,027</li>
<li>Mostly 512 tokens or fewer</li>
</ul>
</li>
<li>JAQKET
<ul>
<li>Dataset from the AI-Ou quiz competition, containing quiz questions and Wikipedia articles with the answers</li>
<li>Queries: 997</li>
<li>Documents: 114,229</li>
<li>Mostly 512 tokens or fewer</li>
</ul>
</li>
<li>NLP Journal
<ul>
<li>Dataset built from the Japanese NLP Journal LaTeX Corpus, combining titles, abstracts, and introductions</li>
<li>Many introductions exceed 512 tokens</li>
</ul>
</li>
</ul>
<p>Japanese SPLADE v2 did not use the train, dev, or test data from Mr. TyDi, MIRACL, JAQKET, or JQaRA as training sources. Using those as training data can improve performance on that domain, but I avoided doing so in order to measure generalization.</p>
<h3>Model Size and Dimensions</h3>
<p>The model parameter counts and output dimensions are shown in the original table. Parameter counts are roughly computed from layer weights. Larger models usually cost more for training and inference. Larger document output dimensions also require more memory and storage.</p>
<p>Because SPLADE output dimensionality, the number of non-zero elements, depends on the text, I included rough numbers for JMTEB queries and documents.</p>
<h2>License</h2>
<p>Japanese SPLADE v2 has no special usage restrictions and is released under the MIT license. You can use it freely.</p>
<h1>Using It from Code</h1>
<p>Sample code is available on <a href="https://huggingface.co/hotchpotch/japanese-splade-v2">huggingface.co/hotchpotch/japanese-splade-v2</a>.</p>
<h2>FAQ</h2>
<h3>Can sparse vector search be used in production?</h3>
<p>Yes. Classic search technologies such as TF-IDF and BM25 are sparse vector search methods, and many search systems, including Elasticsearch, Vespa, and Qdrant, support sparse vector search and hybrid search combining dense and sparse vectors.</p>
<h3>Is SPLADE better than dense vector models?</h3>
<p>On benchmarks it can be better, but it depends on the use case. Even in relatively simple search systems, such as finding a corresponding document from a natural language question, the best method depends on what kinds of questions and documents you expect and what requirements you need to satisfy. Simple BM25 may be best in some cases.</p>
<p>Dense vector models and SPLADE often return results with different characteristics, so hybrid search that combines both is also recommended.</p>
<p>For hybrid search, another useful approach is to train either the dense or sparse model on domain-specific data while keeping the other model more general. The <a href="https://github.com/hotchpotch/yast">Trainer implementation for Japanese SPLADE v2, YAST</a>, and the <a href="https://github.com/hotchpotch/yast/tree/main/examples/japanese-splade">training data and settings</a> are published. By creating query-document training data from your own domain and adding it to the training data, retrieval accuracy may improve significantly. Recently, if you have text, it has also become easier to create synthetic supervised data with LLMs, expanding the ways data can be used.</p>
<h1>Closing</h1>
<p>SPLADE uses context-aware word expansion to cover some weaknesses of keyword-based methods such as BM25, and it is gaining attention as one practical neural search option.</p>
<p>Japanese SPLADE v2, trained properly on Japanese, is likely one of the strongest current models for natural language question tasks such as Mr. TyDi. It is also a high-performing and well-balanced retrieval model that should be practical in production.</p>
<p>I hope this model and article are useful to people working on AI development, natural language processing, and information retrieval.</p>
<h2>Related Reading</h2>
<ul>
<li><a href="/articles/splade">Releasing a High-Performance Japanese SPLADE Sparse Retrieval Model</a></li>
<li><a href="/articles/japanese-splade-tech-report">How to Build a SPLADE Model: Japanese SPLADE Technical Report</a></li>
</ul>]]></content:encoded>
  <pubDate>Thu, 19 Dec 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Thu, 19 Dec 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Releasing Japanese BERT RetroMAE Models and Evaluating Them on Downstream Retrieval Tasks</title>
  <link>https://hotchpotch.dev/articles/japanese-retromae/</link>
  <guid>https://hotchpotch.dev/articles/japanese-retromae/</guid>
  <description>I pretrained Japanese BERT models with RetroMAE, released the weights, and evaluated how the pretraining affects JMTEB retrieval tasks.</description>
  <content:encoded><![CDATA[<p>Neural retrieval models that capture semantic similarity between queries and documents are important for search tasks. However, conventional language models such as BERT are mainly pretrained on token-level tasks, so their sentence-level representations are not always well developed. <a href="https://arxiv.org/abs/2205.12035">RetroMAE: Pre-Training Retrieval-oriented Language Models Via Masked Auto-Encoder</a> was proposed as a new pretraining method specialized for retrieval.</p>
<p>In this article, I created and released Japanese BERT models pretrained with RetroMAE and evaluated them on downstream retrieval tasks, JMTEB.</p>
<table>
<thead>
<tr>
<th>model_name</th>
<th>Avg.</th>
<th>jagovfaqs22k</th>
<th>jaqket</th>
<th>mrtydi</th>
<th>nlp_journalabs_intro</th>
<th>nlp_journaltitle_abs</th>
<th>nlp_journaltitle_intro</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://huggingface.co/tohoku-nlp/bert-base-japanese-v3">bert-base-japanese-v3</a></td>
<td>0.7266</td>
<td>0.6532</td>
<td>0.6236</td>
<td>0.4521</td>
<td>0.8774</td>
<td><strong>0.9732</strong></td>
<td>0.7803</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/bert-base-japanese-v3-retromae">bert-base-japanese-v3retromae</a></td>
<td>0.7352</td>
<td>0.6631</td>
<td>0.6632</td>
<td>0.4526</td>
<td>0.8893</td>
<td>0.9722</td>
<td>0.7708</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/ruri-pt-base-retromae">ruri-pt-baseretromae</a></td>
<td><strong>0.7397</strong></td>
<td><strong>0.6678</strong></td>
<td><strong>0.6691</strong></td>
<td><strong>0.4667</strong></td>
<td><strong>0.8931</strong></td>
<td>0.9605</td>
<td><strong>0.7812</strong></td>
</tr>
</tbody>
</table>
<p>The results show improvements in almost all scores, confirming the usefulness of RetroMAE. The training method is also practical because it is unsupervised and only requires text.</p>
<h2>About RetroMAE</h2>
<p>RetroMAE uses a masked auto-encoder approach with three main design choices:</p>
<ol>
<li>A new workflow that applies different masks to the input sentence</li>
<li>An asymmetric encoder-decoder structure</li>
<li>Different mask ratios for the encoder and decoder</li>
</ol>
<p>These choices allow the model to learn representations that understand document meaning more deeply and support effective retrieval. RetroMAE also performs well on benchmarks such as BEIR and MS MARCO. The high-performing multilingual dense embedding model <a href="https://huggingface.co/BAAI/bge-m3">BAAI/bge-m3</a> also <a href="https://arxiv.org/abs/2402.03216">uses RetroMAE pretraining</a>.</p>
<p>There is also a later method, <a href="https://arxiv.org/abs/2211.08769">RetroMAE v2: Duplex Masked Auto-Encoder For Pre-Training Retrieval-Oriented Language Models</a>, also called DupMAE. This article covers <a href="https://arxiv.org/abs/2205.12035">RetroMAE</a>.</p>
<h3>Workflow with Different Masks</h3>
<p>RetroMAE applies two different masks to the input sentence. The encoder generates a sentence embedding from the first masked input. The decoder then combines the second masked input with that sentence embedding to reconstruct the original sentence.</p>
<p><img src="https://i.imgur.com/xoUY9C8.png" alt="RetroMAE encoder and decoder"></p>
<h3>Asymmetric Encoder and Decoder</h3>
<p>RetroMAE intentionally uses an asymmetric structure. The encoder uses BERT's 12-layer Transformer to capture the meaning of the input sentence sufficiently. The decoder, on the other hand, is only a very simple one-layer Transformer. This simple decoder makes the reconstruction task harder, encouraging the encoder to learn higher-quality sentence embeddings.</p>
<p>The one-layer decoder also introduces a special mechanism called enhanced decoding. It prepares two inputs: a query that combines the sentence embedding and position embedding, and a context that combines the sentence embedding, token embedding, and position embedding. It then applies an attention mask according to position. This allows all input tokens to be reconstruction targets while each token is reconstructed from its own context, enabling efficient training even with a shallow decoder.</p>
<h3>Different Mask Ratios</h3>
<p>The encoder uses a moderate mask ratio, 15-30%, so it can retain most of the information in the input sentence. The decoder uses a more aggressive mask ratio, 50-70%. With this high mask ratio, the decoder cannot easily reconstruct the input from the masked input alone, so it must rely heavily on the sentence embedding produced by the encoder. This forces the encoder to learn deeper semantic understanding.</p>
<h2>Pretraining Japanese RetroMAE Models</h2>
<p>The original paper trains on English Wikipedia, BookCorpus, and MS MARCO. For Japanese, I used the following datasets, which contain similar tasks:</p>
<ul>
<li>(A) Japanese Wikipedia: <a href="https://huggingface.co/datasets/hpprc/jawiki-paragraphs">hpprc/jawiki-paragraphs</a></li>
<li>(A) jawiki-books: <a href="https://huggingface.co/datasets/hpprc/jawiki-books-paragraphs">hpprc/jawiki-books-paragraphs</a></li>
<li>(B) Japanese MQA: <a href="https://huggingface.co/datasets/hpprc/mqa-ja">hpprc/mqa-ja</a></li>
<li>(B) JSNLI: <a href="https://huggingface.co/datasets/shunk031/jsnli">shunk031/jsnli</a></li>
</ul>
<p>For Wikipedia and jawiki-books, I used only paragraphs and did not include titles. For MQA, I concatenated query and document. For JSNLI, I removed spaces.</p>
<p>Instead of training from zero weights, I used <a href="https://huggingface.co/tohoku-nlp/bert-base-japanese-v3">tohoku-nlp/bert-base-japanese-v3</a> and <a href="https://huggingface.co/cl-nagoya/ruri-pt-base">cl-nagoya/ruri-pt-base</a> as the base models for RetroMAE training. <a href="https://arxiv.org/abs/2409.07737">ruri-pt-base is a pretrained model based on bert-base-japanese-v3 and trained with contrastive learning</a>. Because the MLM decoder layer is lost in that process, I used a model where the decoder layer weights were copied from bert-base-japanese-v3.</p>
<p>For the training script, I used the MIT-licensed OSS <a href="https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/baai_general_embedding/retromae_pretrain">retromae_pretrain</a>. The encoder mask ratio was 30%, and the decoder mask ratio was 50%. Other Trainer hyperparameters were:</p>
<pre><code>  "learning_rate": 1e-4,
  "num_train_epochs": 2,
  "per_device_train_batch_size": 16,
  "gradient_accumulation_steps": 32,
  "warmup_ratio": 0.05,
  "lr_scheduler_type": "cosine",
  "bf16": true,
  "dataloader_drop_last": true,
  "dataloader_num_workers": 12
</code></pre>
<p>Using these settings, I created RetroMAE pretrained models using only dataset group (A), and using (A) + (B).</p>
<h2>Evaluation on Downstream Retrieval Tasks</h2>
<p>For downstream retrieval evaluation, I trained Japanese SPLADE models using only the mMARCO dataset. The settings are based on <a href="examples/japanese-splade-v1/japanese-splade-base-v1-mmarco-only.yaml">japanese-splade-base-v1-mmarco-only</a>, with the model epochs reduced from 12 to 10 and <code>model_name</code> replaced with the model being evaluated.</p>
<p>For evaluation, I used my <a href="https://github.com/hotchpotch/JMTEB/tree/add_splade">fork of JMTEB</a>, modified to evaluate sparse vectors, and ran retrieval tasks.</p>
<p>The evaluation scores are:</p>
<table>
<thead>
<tr>
<th>model_name</th>
<th>Avg.</th>
<th>jagovfaqs22k</th>
<th>jaqket</th>
<th>mrtydi</th>
<th>nlp_journalabs_intro</th>
<th>nlp_journaltitle_abs</th>
<th>nlp_journaltitle_intro</th>
</tr>
</thead>
<tbody>
<tr>
<td>bert-base-japanese-v3</td>
<td>0.7266</td>
<td>0.6532</td>
<td>0.6236</td>
<td>0.4521</td>
<td>0.8774</td>
<td>0.9732</td>
<td>0.7803</td>
</tr>
<tr>
<td>bert-base-japanese-v3retromae(A)</td>
<td>0.7361</td>
<td>0.6655</td>
<td>0.6621</td>
<td>0.4557</td>
<td>0.888</td>
<td>0.9604</td>
<td><strong>0.7848</strong></td>
</tr>
<tr>
<td>ruri-pt-base retromae(A)</td>
<td>0.737</td>
<td>0.6657</td>
<td>0.6541</td>
<td>0.4608</td>
<td>0.8823</td>
<td><strong>0.9768</strong></td>
<td>0.7821</td>
</tr>
<tr>
<td>bert-base-japanese-v3 retromae(A+B)</td>
<td>0.7352</td>
<td>0.6631</td>
<td>0.6632</td>
<td>0.4526</td>
<td>0.8893</td>
<td>0.9722</td>
<td>0.7708</td>
</tr>
<tr>
<td>ruri-pt-base retromae(A+B)</td>
<td><strong>0.7397</strong></td>
<td><strong>0.6678</strong></td>
<td><strong>0.6691</strong></td>
<td><strong>0.4667</strong></td>
<td><strong>0.8931</strong></td>
<td>0.9605</td>
<td>0.7812</td>
</tr>
</tbody>
</table>
<p>In almost all evaluations, the models trained with RetroMAE scored higher than models not trained with RetroMAE. The best model, <code>ruri-pt-base retromae(A+B)</code>, improved by about 2% compared with <code>bert-base-japanese-v3</code>.</p>
<p>The datasets also show that training on both (A) and (B) generally produced higher scores than training only on (A). This suggests that adding more datasets or training on domain-specific text may further improve performance.</p>
<p>The RetroMAE models trained on (A+B) are published on Hugging Face:</p>
<ul>
<li><a href="https://huggingface.co/hotchpotch/ruri-pt-base-retromae">https://huggingface.co/hotchpotch/ruri-pt-base-retromae</a></li>
<li><a href="https://huggingface.co/hotchpotch/bert-base-japanese-v3-retromae">https://huggingface.co/hotchpotch/bert-base-japanese-v3-retromae</a></li>
</ul>
<h2>Closing</h2>
<p>This article applied RetroMAE, a retrieval-oriented pretraining method, to Japanese BERT models and evaluated its effect. In downstream SPLADE evaluation, models trained with RetroMAE improved over the baseline <code>bert-base-japanese-v3</code> on almost all retrieval tasks. In particular, the model based on contrastively trained <code>ruri-pt-base</code> and trained on multiple datasets such as Wikipedia, books, and question-answer data achieved an average improvement of about 2%.</p>
<p>Another advantage of RetroMAE is that it can be trained in an unsupervised way using only text data. This makes it useful for customizing models for specific domains or business tasks. Further improvements may be possible by adding more training data or continuing training on domain-specific text.</p>
<p>The RetroMAE models are published on Hugging Face and can be used. I hope this article helps improve Japanese retrieval task performance.</p>]]></content:encoded>
  <pubDate>Wed, 30 Oct 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 30 Oct 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>How to Build a SPLADE Model: Japanese SPLADE Technical Report</title>
  <link>https://hotchpotch.dev/articles/japanese-splade-tech-report/</link>
  <guid>https://hotchpotch.dev/articles/japanese-splade-tech-report/</guid>
  <description>How I built a Japanese SPLADE sparse retrieval model, including tokenizer issues, training implementation, evaluation, and the YAST trainer.</description>
  <content:encoded><![CDATA[<p>In recent years, the rise of large language models has made information retrieval increasingly important. Applications such as Retrieval-Augmented Generation, or RAG, need search systems that are both efficient and accurate.</p>
<p>In neural retrieval, dense retrievers have become mainstream, and strong multilingual models such as <code>multilingual-e5</code> and <code>bge-m3</code> are available. At the same time, sparse retrieval models represented by SPLADE, Sparse Lexical and Expansion Model with Contextualized Embeddings, have shown strong performance in English.</p>
<p>However, SPLADE depends heavily on lexical features, and tokenization depends on the model tokenizer. This made multilingual SPLADE difficult. Multilingual tokenizers often split many languages at the character level, making meaningful word-level tokenization difficult. I therefore developed and evaluated a SPLADE model specialized for Japanese.</p>
<p>The original SPLADE implementation, <a href="https://github.com/naver/splade">naver/splade</a>, is released under CC-BY-NC and has commercial-use restrictions. I implemented a Trainer based on the papers and released it as MIT-licensed open source software.</p>
<ul>
<li><a href="https://github.com/hotchpotch/yast">YAST - Yet Another SPLADE or Sparse Trainer</a></li>
</ul>
<p>This report covers implementation details, evaluation results, and future directions for the Japanese SPLADE model.</p>
<h2>SPLADE Algorithm</h2>
<p>SPLADE learns sparse document and query representations for information retrieval. This section describes how it is trained.</p>
<h3>Word Importance and Output Tokens</h3>
<p>SPLADE uses the per-token output of a model pretrained with Masked Language Modeling, or MLM, to compute context-dependent word importance. More concretely, it uses the vocabulary space of a pretrained model such as BERT and applies max pooling over token scores at each input position. Applying a log-saturation function suppresses extreme values while emphasizing important features. These operations produce sparse and efficient document and query representations that capture salient features.</p>
<p>This operation is called SPLADE Max. A Python implementation is below.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">splade_max_pooling</span>(<span class="hljs-params">logits, attention_mask</span>):
    <span class="hljs-comment"># Step 1: apply log saturation, log(1 + x)</span>
    <span class="hljs-comment"># - torch.relu() clamps negative values to 0</span>
    <span class="hljs-comment"># - torch.log(1 + x) converts values to log scale and suppresses large values</span>
    relu_log = torch.log(<span class="hljs-number">1</span> + torch.relu(logits))
    
    <span class="hljs-comment"># Step 2: mask scores at padded positions with attention_mask</span>
    <span class="hljs-comment"># unsqueeze(-1) aligns dimensions: (batch_size, seq_len, 1)</span>
    weighted_log = relu_log * attention_mask.unsqueeze(-<span class="hljs-number">1</span>)
    
    <span class="hljs-comment"># Step 3: apply max pooling</span>
    <span class="hljs-comment"># torch.max() takes the maximum over sequence length (dim=1)</span>
    <span class="hljs-comment"># This selects the most important score for each vocabulary item</span>
    max_val, _ = torch.<span class="hljs-built_in">max</span>(weighted_log, dim=<span class="hljs-number">1</span>)
    
    <span class="hljs-keyword">return</span> max_val
</code></pre>
<h3>Predicting Document-Query Relevance</h3>
<p>The word importance scores from SPLADE Max are used to predict the relevance between documents and queries, mainly with an inner product. The difference between the prediction and the training data is defined as the loss.</p>
<p>Loss functions such as KL divergence, MarginMSE, and cross entropy can be used to measure differences between the model's predicted vocabulary distribution and the target distribution. These can be used alone or in combination. <a href="https://arxiv.org/abs/2403.06789">SPLADE-v3</a> combines KL divergence and MarginMSE.</p>
<h3>Sparsity and Regularization</h3>
<p>Regularization is included in the loss to make the output word-importance distribution sparse. The main algorithms are:</p>
<ul>
<li><strong>L1 regularization</strong>: minimizes the sum of absolute parameter values, pushing many values toward zero and encouraging sparse representations.</li>
<li><strong>FLOPs regularization</strong>: in high-dimensional sparse representation learning, distributes non-zero elements across dimensions to reduce matrix-operation FLOPs quadratically. See <a href="https://arxiv.org/abs/2004.05665">Minimizing FLOPs to Learn Efficient Sparse Representations</a>.</li>
</ul>
<p>Different losses and regularization coefficients can be applied to queries and documents. Applying strong regularization from the beginning of training can harm importance prediction, so a warmup period that gradually increases the regularization loss weight is also used.</p>
<h3>Training and Relevance Computation</h3>
<p>By training with these methods, SPLADE can increase query-document relevance while encouraging sparsity. It combines sparse representations with neural contextual vocabulary information, enabling strong information retrieval.</p>
<h2>Training Method for the Japanese Model</h2>
<h3>Dataset Preparation</h3>
<p>For the final <code>japanese-splade-base-v1</code> model, I used several subsets from <a href="https://huggingface.co/datasets/hpprc/emb">hpprc/emb</a>, which contains Japanese questions, answers, and hard negatives. The subsets include <code>auto-wiki-qa</code>, <code>mmarco</code>, <code>jsquad</code>, <code>jaquad</code>, <code>auto-wiki-qa-nemotron</code>, <code>quiz-works</code>, <code>quiz-no-mori</code>, <code>miracl</code>, <code>jqara</code>, <code>mr-tydi</code>, <code>baobab-wiki-retrieval</code>, and <code>mkqa</code>.</p>
<p>I also created <a href="https://huggingface.co/datasets/hotchpotch/hpprc_emb-scores">hotchpotch/hpprc_emb-scores</a>, a scored dataset using high-performance Japanese cross-encoder rerankers, <a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">BAAI/bge-reranker-v2-m3</a> and <a href="https://huggingface.co/cl-nagoya/ruri-reranker-large">cl-nagoya/ruri-reranker-large</a>. For English data, I used <a href="https://huggingface.co/datasets/microsoft/ms_marco">MS MARCO</a> and data scored with BAAI/bge-reranker-v2-m3.</p>
<p>For filtering, I used the average score of the rerankers: positives with scores of 0.7 or higher, and negatives with scores of 0.3 or lower. This removes passages that the rerankers judge to be inappropriate for the query.</p>
<p>For datasets with small proportions, I increased the amount of training per epoch so the model would not forget their characteristics.</p>
<p>For an mMARCO-only training dataset, I created and used <a href="https://huggingface.co/datasets/hotchpotch/mmarco-hard-negatives-reranker-score">hotchpotch/mmarco-hard-negatives-reranker-score</a>, based on <a href="https://huggingface.co/datasets/unicamp-dl/mmarco">mMARCO</a> and scored with BAAI/bge-reranker-v2-m3. It uses the same filtering rule: positives at 0.7 or higher and negatives at 0.3 or lower.</p>
<h3>Training Settings and Hyperparameters</h3>
<p>I used simple cross-entropy loss as the training loss. I tried KL divergence and MarginMSE as well, but cross entropy produced the best result. The goal was to let the model learn the scores from high-performance rerankers.</p>
<p>For sparsity regularization, I used L1 regularization. Compared with FLOPs loss, L1 regularization encouraged sparsity more effectively for Japanese.</p>
<p>The learning rate was <code>5.0e-2</code>, a common value for a 110M-parameter model in this setting. I used a cosine learning-rate scheduler and set 10% of the total steps as warmup.</p>
<p>Each batch contains one positive and seven negatives, for eight examples total. The batch size was 32 for <code>japanese-splade-base-v1</code> and 128 for <code>japanese-splade-base-v1-mmarco-only</code>. For mMARCO-only training, query and document sparsity converged quickly even with a large batch. For <code>japanese-splade-base-v1</code>, which trains on diverse datasets, larger batch sizes slowed sparsity convergence, so smaller batches worked better. If more training time and resources are available, larger batches might still improve <code>japanese-splade-base-v1</code>.</p>
<p>Detailed parameters are available in the <a href="https://github.com/hotchpotch/yast/tree/main/examples/japanese-splade-v1">actual training configuration files</a>.</p>
<h3>Removing Noise Tokens</h3>
<p>In Japanese training, punctuation and symbols such as <code>、</code>, <code>。</code>, <code>「</code>, and <code>：</code> appeared prominently as noisy features. When these tokens remained in SPLADE Max output, I added their scores to the loss as a penalty. I extracted symbolic words with fugashi and unidic-lite.</p>
<p>By treating these as noise tokens and including them in the loss, the trained model almost stopped outputting them. Training also became more stable and converged faster.</p>
<h3>Base Model</h3>
<p>The base model was <a href="https://huggingface.co/tohoku-nlp/bert-base-japanese-v3">tohoku-nlp/bert-base-japanese-v3</a>, which has lexical semantic features in its output layer from MLM pretraining and is based on the Japanese BERT architecture.</p>
<h3>Training</h3>
<p>Using these settings, I fine-tuned and created <code>japanese-splade-base-v1</code> and <code>japanese-splade-base-v1-mmarco-only</code>. On an RTX 4090, training took about 33 hours for <code>japanese-splade-base-v1</code> and about 24 hours for <code>japanese-splade-base-v1-mmarco-only</code>.</p>
<p><code>japanese-splade-base-v1</code> was trained for 2 epochs because the dataset was large. <code>japanese-splade-base-v1-mmarco-only</code> was trained for 12 epochs because the dataset was smaller and contained only mMARCO. Increasing the number of epochs for <code>japanese-splade-base-v1</code> lowered training loss but reduced retrieval performance during evaluation, probably because of overfitting.</p>
<p>The trained models are published on Hugging Face:</p>
<ul>
<li><a href="https://huggingface.co/hotchpotch/japanese-splade-base-v1">https://huggingface.co/hotchpotch/japanese-splade-base-v1</a></li>
<li><a href="https://huggingface.co/hotchpotch/japanese-splade-base-v1-mmarco-only">https://huggingface.co/hotchpotch/japanese-splade-base-v1-mmarco-only</a></li>
</ul>
<h2>Evaluation Results</h2>
<h3>JMTEB Retrieval</h3>
<p>The JMTEB results are below. I used my <a href="https://github.com/hotchpotch/JMTEB/tree/add_splade">fork modified to evaluate sparse vectors</a>.</p>
<table>
<thead>
<tr>
<th>model_name</th>
<th>Avg.</th>
<th>jagovfaqs</th>
<th>jaqket</th>
<th>mrtydi</th>
<th>nlp_journalabs_intro</th>
<th>nlp_journaltitle_abs</th>
<th>nlp_journaltitle_intro</th>
</tr>
</thead>
<tbody>
<tr>
<td>japanese-splade-base-v1</td>
<td><strong>0.7465</strong></td>
<td>0.6499</td>
<td><strong>0.6992</strong></td>
<td>0.4365</td>
<td>0.8967</td>
<td><strong>0.9766</strong></td>
<td>0.8203</td>
</tr>
<tr>
<td>japanese-splade-base-v1-mmarco-only</td>
<td>0.7313</td>
<td>0.6513</td>
<td>0.6518</td>
<td><strong>0.4467</strong></td>
<td>0.8893</td>
<td>0.9736</td>
<td>0.7751</td>
</tr>
<tr>
<td>text-embedding-3-large</td>
<td>0.7448</td>
<td>0.7241</td>
<td>0.4821</td>
<td>0.3488</td>
<td><strong>0.9933</strong></td>
<td>0.9655</td>
<td><strong>0.9547</strong></td>
</tr>
<tr>
<td>GLuCoSE-base-ja-v2</td>
<td>0.7336</td>
<td>0.6979</td>
<td>0.6729</td>
<td>0.4186</td>
<td>0.9029</td>
<td>0.9511</td>
<td>0.7580</td>
</tr>
<tr>
<td>multilingual-e5-large</td>
<td>0.7098</td>
<td>0.7030</td>
<td>0.5878</td>
<td>0.4363</td>
<td>0.8600</td>
<td>0.9470</td>
<td>0.7248</td>
</tr>
<tr>
<td>multilingual-e5-small</td>
<td>0.6727</td>
<td>0.6411</td>
<td>0.4997</td>
<td>0.3605</td>
<td>0.8521</td>
<td>0.9526</td>
<td>0.7299</td>
</tr>
<tr>
<td>ruri-large</td>
<td>0.7302</td>
<td><strong>0.7668</strong></td>
<td>0.6174</td>
<td>0.3803</td>
<td>0.8712</td>
<td>0.9658</td>
<td>0.7797</td>
</tr>
</tbody>
</table>
<p>On average, <code>japanese-splade-base-v1</code> performed best, though it trained on some domain tasks such as Mr. TyDi and JAQKET, not the test data used in JMTEB evaluation. <code>japanese-splade-base-v1-mmarco-only</code> trained only on mMARCO but was best on Mr. TyDi and competitive on other tasks.</p>
<p>SPLADE models perform relatively poorly on <code>jagovfaqs</code>. This may be because the queries are FAQ-like and often resemble summarization or contextual similarity tasks. Other models learn semantic similarity, while <code>japanese-splade-base-v1</code> does not. Strong Japanese models such as <code>ruri-large</code> and <code>GLuCoSE-base-ja-v2</code> may also benefit from training on Japanese data from <a href="https://huggingface.co/datasets/clips/mqa">MQA</a>, a multilingual FAQ and CQA dataset.</p>
<p>JAQKET contains many quiz-style questions with distinctive Japanese phrasing. Models that learn these expressions score well, and because answer documents contain the correct answer words, SPLADE's lexical features likely help.</p>
<p>The Mr. TyDi result is counterintuitive: <code>japanese-splade-base-v1</code>, which should have learned the domain, is worse than <code>japanese-splade-base-v1-mmarco-only</code>, which did not. I have not fully analyzed this.</p>
<p>For the three NLP Journal tasks, SPLADE models perform well on <code>title_abs</code>, while <code>text-embedding-3-large</code> is much stronger on <code>abs_intro</code> and <code>title_intro</code>. This is because <code>title_abs</code> documents average 442 tokens, while <code>abs_intro</code> and <code>title_intro</code> average 2052 tokens. All models except <code>text-embedding-3-large</code> have a maximum input length of 512 tokens, while <code>text-embedding-3-large</code> supports 8191, so the other models evaluate only the beginning of long documents.</p>
<h3>Reranking Evaluation</h3>
<p>For reranking, I used <a href="https://github.com/hotchpotch/JQaRA">JQaRA</a> and <a href="https://github.com/hotchpotch/JaCWIR">JaCWIR</a>.</p>
<table>
<thead>
<tr>
<th>model_name</th>
<th>JaCWIR map@10</th>
<th>JaCWIR HR@10</th>
<th>JQaRA ndcg@10</th>
<th>JQaRA mrr@10</th>
</tr>
</thead>
<tbody>
<tr>
<td>japanese-splade-base-v1</td>
<td><strong>0.9122</strong></td>
<td><strong>0.9854</strong></td>
<td><strong>0.6441</strong></td>
<td><strong>0.8616</strong></td>
</tr>
<tr>
<td>japanese-splade-base-v1-mmarco-only</td>
<td>0.8953</td>
<td>0.9746</td>
<td>0.5740</td>
<td>0.8176</td>
</tr>
<tr>
<td>text-embedding-3-small</td>
<td>0.8168</td>
<td>0.9506</td>
<td>0.3881</td>
<td>0.6107</td>
</tr>
<tr>
<td>GLuCoSE-base-ja-v2</td>
<td>0.8567</td>
<td>0.9676</td>
<td>0.6060</td>
<td>0.8359</td>
</tr>
<tr>
<td>bge-m3+dense</td>
<td>0.8642</td>
<td>0.9684</td>
<td>0.5390</td>
<td>0.7854</td>
</tr>
<tr>
<td>multilingual-e5-large</td>
<td>0.8759</td>
<td>0.9726</td>
<td>0.5540</td>
<td>0.7988</td>
</tr>
<tr>
<td>multilingual-e5-small</td>
<td>0.8690</td>
<td>0.9700</td>
<td>0.4917</td>
<td>0.7291</td>
</tr>
<tr>
<td>ruri-large</td>
<td>0.8291</td>
<td>0.9594</td>
<td>0.6287</td>
<td>0.8418</td>
</tr>
</tbody>
</table>
<p>Although <code>japanese-splade-base-v1</code> learned the JQaRA domain, it achieved the best results across these evaluations.</p>
<h3>English Evaluation</h3>
<p><code>japanese-splade-base-v1</code> includes English MS MARCO data in training, so I evaluated it on MS MARCO dev with the script from <a href="https://github.com/naver/splade">naver/splade</a>.</p>
<table>
<thead>
<tr>
<th>model_name</th>
<th>MRR@10 (MS MARCO dev)</th>
</tr>
</thead>
<tbody>
<tr>
<td>japanese-splade-base-v1</td>
<td>0.047</td>
</tr>
<tr>
<td>japanese-splade-base-v1-mmarco-only</td>
<td>0.036</td>
</tr>
<tr>
<td>naver/splade_v2_max</td>
<td><strong>0.340</strong></td>
</tr>
</tbody>
</table>
<p>There is a small improvement compared with the model that did not train on English data, but the score is far below <code>naver/splade_v2_max</code>, which is trained for English. The model has little English retrieval capability.</p>
<h3>Sparsity Evaluation</h3>
<p>I measured sparsity with the number of non-zero elements, the L0 norm, for queries and documents. The following results were measured on JMTEB retrieval tasks, top 1000, with <a href="https://github.com/hotchpotch/yast/blob/main/utils/JMTEB_L0.py">JMTEB_L0.py</a>.</p>
<table>
<thead>
<tr>
<th>JMTEB tasks</th>
<th>v1</th>
<th>v1-mmarco-only</th>
</tr>
</thead>
<tbody>
<tr>
<td>jagovfaqs_22k-query</td>
<td>27.9</td>
<td>43.4</td>
</tr>
<tr>
<td>jaqket-query</td>
<td>23.3</td>
<td>38.9</td>
</tr>
<tr>
<td>mrtydi-query</td>
<td>13.8</td>
<td>20.5</td>
</tr>
<tr>
<td>nlp_journal_abs_intro-query</td>
<td>75.3</td>
<td>127.2</td>
</tr>
<tr>
<td>nlp_journal_title_abs-query</td>
<td>19</td>
<td>26.4</td>
</tr>
<tr>
<td>nlp_journal_title_intro-query</td>
<td>19</td>
<td>26.4</td>
</tr>
<tr>
<td>jagovfaqs_22k-docs</td>
<td>73.2</td>
<td>97.9</td>
</tr>
<tr>
<td>jaqket-docs</td>
<td>146.2</td>
<td>231.8</td>
</tr>
<tr>
<td>mrtydi-docs</td>
<td>89.3</td>
<td>100.4</td>
</tr>
<tr>
<td>nlp_journal_abs_intro-docs</td>
<td>95.7</td>
<td>182</td>
</tr>
<tr>
<td>nlp_journal_title_abs-docs</td>
<td>75.2</td>
<td>126.9</td>
</tr>
<tr>
<td>nlp_journal_title_intro-docs</td>
<td>95.7</td>
<td>182</td>
</tr>
</tbody>
</table>
<p>The L0 norms show that <code>v1-mmarco-only</code> generally has more non-zero elements and is less sparse. Query and document sparsity are both important, but they have different requirements.</p>
<p>For search speed, higher query sparsity is especially valuable. Document sparsity also matters for memory and disk usage, but in production, millions to tens of millions of documents can often be searched in memory on one machine, so document sparsity may not need to be managed as strictly as query sparsity. At the same time, if documents have too few non-zero elements, retrieval quality can suffer. Tuning query and document sparsity is important for balancing search quality and efficiency.</p>
<h3>Summary of Evaluation</h3>
<p>These results suggest that <code>japanese-splade-base-v1</code> is competitive with recent models for Japanese retrieval, especially on tasks where lexical features are important. Query and document sparsity are also sufficient for practical use.</p>
<p>Other models in the comparison are dense vector models, while SPLADE is a sparse vector model that emphasizes lexical features. Combining different models can produce more diverse search results than using dense models alone. This is important in real systems where diverse retrieval results are useful, such as passing varied search information to an LLM.</p>
<h2>Future Work</h2>
<p><code>japanese-splade-base-v1</code> has been released as a first artifact, but there is still room for improvement. The original SPLADE papers improve performance with self-distillation, multiple loss scores, and hard-negative sampling using SPLADE itself.</p>
<p>I have also not fully explored selecting or training pretrained models suited to retrieval tasks. Methods such as <a href="https://arxiv.org/abs/2108.05540">Unsupervised Corpus Aware Language Model Pre-training for Dense Passage Retrieval</a> and <a href="https://arxiv.org/abs/2205.12035">RetroMAE</a> may improve performance.</p>
<p>Other possibilities include adding FAQ-style task data, supporting longer context, and adding more diverse datasets. Current data tends to be Wikipedia-heavy.</p>
<p>Recent models such as Llama 3.1 have licenses that allow their outputs to be used for training, making it easier to create retrieval datasets without licensing issues. The <code>hpprc/emb</code> dataset used here provides high-quality data using LLM outputs, as described in <a href="https://arxiv.org/abs/2409.07737">Ruri: Japanese General Text Embeddings</a>.</p>
<p>Creating retrieval-suitable queries from documents used to require significant manual effort. LLMs now make it possible to generate large numbers of queries at low cost. Training on specific domains often improves generalization to those domains, so richer datasets should further improve retrieval models.</p>
<h2>Closing</h2>
<p>This report described <code>japanese-splade-base-v1</code>, a SPLADE model specialized for Japanese, and evaluated it. The results show that it performs strongly compared with recent models for Japanese information retrieval.</p>
<p>Future work includes methods for further performance improvement, selecting pretrained models better suited to retrieval, and using more diverse datasets.</p>
<p>I hope releasing the Japanese SPLADE model and SPLADE training Trainer contributes to the development of information retrieval technology.</p>
<h3>References</h3>
<ul>
<li><a href="https://arxiv.org/abs/2107.05720">SPLADE: Sparse Lexical and Expansion Model for First Stage Ranking</a></li>
<li><a href="https://arxiv.org/abs/2109.10086">SPLADE v2: Sparse Lexical and Expansion Model for Information Retrieval</a></li>
<li><a href="http://arxiv.org/abs/2205.04733">From Distillation to Hard Negative Sampling: Making Sparse Neural IR Models More Effective</a></li>
<li><a href="https://dl.acm.org/doi/10.1145/3477495.3531833">An Efficiency Study for SPLADE Models</a></li>
<li><a href="https://arxiv.org/abs/2304.12702">A Static Pruning Study on Sparse Neural Retrievers</a></li>
<li><a href="https://arxiv.org/abs/2403.06789">SPLADE-v3: New baselines for SPLADE</a></li>
<li><a href="https://arxiv.org/abs/2004.05665">Minimizing FLOPs to Learn Efficient Sparse Representations</a></li>
<li><a href="https://arxiv.org/abs/2409.07737">Ruri: Japanese General Text Embeddings</a></li>
<li><a href="https://arxiv.org/abs/2407.20750v1">JaColBERTv2.5: Optimising Multi-Vector Retrievers to Create State-of-the-Art Japanese Retrievers with Constrained Resources</a></li>
<li><a href="https://www.sbintuitions.co.jp/blog/entry/2024/05/16/130848">Building JMTEB, a Japanese text embedding benchmark</a></li>
<li><a href="https://arxiv.org/abs/2108.13897">mMARCO: A Multilingual Version of the MS MARCO Passage Ranking Dataset</a></li>
<li><a href="https://arxiv.org/abs/2108.08787">Mr. TyDi: A Multi-lingual Benchmark for Dense Retrieval</a></li>
<li><a href="https://huggingface.co/datasets/hotchpotch/JaCWIR">JaCWIR: Japanese Casual Web IR</a></li>
<li><a href="https://huggingface.co/datasets/hotchpotch/JQaRA">JQaRA: Japanese Question Answering with Retrieval Augmentation</a></li>
<li><a href="https://www.anlp.jp/proceedings/annual_meeting/2020/pdf_dir/P2-24.pdf">JAQKET: Building a Japanese QA Dataset Based on Quiz Questions</a></li>
<li><a href="/articles/splade">Releasing a High-Performance Japanese SPLADE Sparse Retrieval Model</a></li>
</ul>
<hr>
<pre><code>@article{tateno2024splade,
    title={SPLADE モデルの作り方・日本語SPLADEテクニカルレポート},
    author={TatenoYuichi},
    year={2024},
    url={/articles/japanese-splade-tech-report}
}
</code></pre>]]></content:encoded>
  <pubDate>Wed, 23 Oct 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 23 Oct 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Releasing a High-Performance Japanese SPLADE Sparse Retrieval Model</title>
  <link>https://hotchpotch.dev/articles/splade/</link>
  <guid>https://hotchpotch.dev/articles/splade/</guid>
  <description>I released a Japanese SPLADE sparse retrieval model and compare its retrieval and reranking performance with dense embedding models.</description>
  <content:encoded><![CDATA[<p>I created and released a Japanese SPLADE sparse vector model for text retrieval. On retrieval tasks over large text collections and reranking tasks that reorder documents related to a query, it achieves strong competitive results compared with recent dense vector models such as <code>multilingual-e5-large</code>, <code>ruri-large</code>, <code>GLuCoSE-base-ja-v2</code>, and OpenAI text embeddings.</p>
<ul>
<li><a href="https://huggingface.co/hotchpotch/japanese-splade-base-v1">https://huggingface.co/hotchpotch/japanese-splade-base-v1</a></li>
</ul>
<p><img src="https://i.imgur.com/mRrO2G0.png" alt="JMTEB retrieval"></p>
<p>For technical details on building the Japanese SPLADE model, see <a href="/articles/japanese-splade-tech-report">How to Build a SPLADE Model: Japanese SPLADE Technical Report</a>.</p>
<h2>What Is SPLADE?</h2>
<p><a href="https://github.com/naver/splade">SPLADE</a>, Sparse Lexical and Expansion Model, is a retrieval model that uses sparse vectors. BM25 is the representative sparse retrieval algorithm and has been widely used for many years because of its strong performance. However, BM25 depends on exact word matches between queries and documents, so it can miss documents that contain related words or synonyms.</p>
<p>SPLADE uses a Transformer architecture and can include contextually related words in the vector representation. This allows words beyond exact matches to become retrieval candidates, enabling more flexible and effective search.</p>
<h2>Characteristics and Benefits</h2>
<p>SPLADE uses a pretrained Transformer model, such as BERT, to understand the context of the input text. It does not depend only on exact word matches and can effectively extract contextually related words. Each word is assigned an importance score, making it clear which words matter for retrieval. It also produces sparse vectors, where many dimensions are zero, which keeps computation manageable and enables efficient search.</p>
<p>These characteristics make SPLADE suitable for flexible retrieval needs involving related terms and synonyms. Sparse vectors allow fast search with lower computation, improving the efficiency of the overall system. Because each word has an explicit importance score, the retrieval result is also easier to interpret. Finally, SPLADE is relatively easy to introduce into existing search engines, so it can be integrated smoothly into current systems.</p>
<h2>A Concrete Example</h2>
<p>To understand how SPLADE works, here is a concrete example from the actual <code>japanese-splade-base-v1</code> model. You can also get outputs easily from the <a href="https://huggingface.co/spaces/hotchpotch/japanese-splade-demo-streamlit">Japanese SPLADE demo</a>.</p>
<h3>Example of Word Expansion</h3>
<p>SPLADE output for "How can I improve my car's fuel efficiency?"</p>
<table>
<thead>
<tr>
<th>Score</th>
<th>Word (vocab)</th>
</tr>
</thead>
<tbody>
<tr>
<td>2.1797</td>
<td>車</td>
</tr>
<tr>
<td>2.1465</td>
<td>燃費</td>
</tr>
<tr>
<td>1.7344</td>
<td>向上</td>
</tr>
<tr>
<td>1.5586</td>
<td>方法</td>
</tr>
<tr>
<td>1.3291</td>
<td>燃料</td>
</tr>
<tr>
<td>1.1377</td>
<td>効果</td>
</tr>
<tr>
<td>0.8716</td>
<td>良い</td>
</tr>
<tr>
<td>0.8452</td>
<td>改善</td>
</tr>
<tr>
<td>0.8340</td>
<td>アップ</td>
</tr>
<tr>
<td>0.7065</td>
<td>いう</td>
</tr>
<tr>
<td>0.6450</td>
<td>理由</td>
</tr>
<tr>
<td>0.4355</td>
<td>価格</td>
</tr>
<tr>
<td>0.3184</td>
<td>は</td>
</tr>
<tr>
<td>0.2510</td>
<td>家</td>
</tr>
<tr>
<td>0.2417</td>
<td>せる</td>
</tr>
<tr>
<td>0.2286</td>
<td>目的</td>
</tr>
<tr>
<td>0.1735</td>
<td>店</td>
</tr>
<tr>
<td>0.1627</td>
<td>手段</td>
</tr>
<tr>
<td>0.0851</td>
<td>用</td>
</tr>
<tr>
<td>0.0752</td>
<td>率</td>
</tr>
<tr>
<td>0.0734</td>
<td>上昇</td>
</tr>
</tbody>
</table>
<p>As shown here, the model understands the context of the query and extracts related words such as "fuel" and "effect", even though they are not present in the original sentence. Each word also has an importance score. Some words that look unrelated or noisy, such as Japanese particles, are also included. Because such words appear in many outputs, they often become noise that can be mostly ignored, and search can still work well.</p>
<p>The same process can be applied to documents. By taking the dot product between the sparse vector for a query and the sparse vector for a document, we can compute how related they are.</p>
<h2>Performance</h2>
<p>As noted above, the SPLADE model performs well on many Japanese information retrieval tasks. Benchmark results on <a href="https://github.com/sbintuitions/JMTEB">JMTEB</a> retrieval, <a href="https://huggingface.co/datasets/hotchpotch/JQaRA">JQaRA</a>, and <a href="https://huggingface.co/datasets/hotchpotch/JaCWIR">JaCWIR</a> are shown below. It performs strongly on tasks where lexical features matter. On the other hand, it is weaker on tasks such as <code>jagovfaqs</code>, where understanding similar sentence meaning appears to be more important.</p>
<h3>JMTEB Retrieval</h3>
<p><img src="https://i.imgur.com/mRrO2G0.png" alt="JMTEB retrieval"></p>
<h3>JQaRA and JaCWIR Reranking</h3>
<p><img src="https://i.imgur.com/ArEt770.png" alt="JaCWIR, JQaRA reranking"></p>
<p>Most open source search engines, including Elasticsearch, OpenSearch, Qdrant, and Vespa, support sparse retrieval, so adoption is relatively easy. Sparse vector search has also existed for a long time and is fast, similarly to BM25.</p>
<p>SPLADE and BM25 strongly reflect lexical features, so their results often differ from dense vector models such as <code>multilingual-e5</code>. Combining both sets of results as hybrid search can produce better and more diverse results. Most of the search engines mentioned above also support hybrid search, and many make it easy to use.</p>
<h2>Is It Hard to Run in Production?</h2>
<p>SPLADE can be operated almost the same way as a dense vector model, so it is not especially difficult. As mentioned above, most search engines support sparse search.</p>
<p>Obtaining a SPLADE sparse vector is also not complicated. It passes token scores through a combination of max pooling, often called SPLADE max, and a log-saturation function.</p>
<ul>
<li>Example: <a href="https://huggingface.co/hotchpotch/japanese-splade-base-v1#transformers">obtaining sparse vectors with the Transformers library</a></li>
</ul>
<p>It can also be used from <code>text-embedding-inference</code>, a fast inference server that is convenient for production operation.</p>
<ul>
<li><a href="https://huggingface.co/hotchpotch/japanese-splade-base-v1-dummy-fast-tokenizer-for-tei">https://huggingface.co/hotchpotch/japanese-splade-base-v1-dummy-fast-tokenizer-for-tei</a></li>
</ul>
<h2>Closing</h2>
<p>At first I was not sure whether SPLADE would really perform well. However, <a href="https://arxiv.org/abs/2403.06789">SPLADE-v3</a>, trained only on the English MS MARCO dataset, performs well across a variety of retrieval tasks. That made me interested in what would happen if it were trained properly for Japanese.</p>
<p>SPLADE also depends on the tokenizer vocabulary. That makes it a poor fit with multilingual model tokenizers that often split Japanese at the character level, so specialized training for Japanese is needed. This was another reason the project seemed interesting. High-performance multilingual dense vector models that support Japanese are already being pursued by many companies.</p>
<p>As a result of training, even though some known-domain tasks such as JAQKET and Mr.TyDi are included, I was able to create a base-size 110M-parameter sparse retrieval model that outperforms large OpenAI models on some benchmarks.</p>
<p>Training took about 33 hours on an RTX 4090. Because SPLADE can be trained with relatively modest compute and time, creating a model trained on domain-specific data with SPLADE seems like a useful approach for teams that need retrieval results adapted to their own domain.</p>
<p>I expect Japanese sparse retrieval performance with SPLADE to continue improving, and I think it remains an interesting research area.</p>]]></content:encoded>
  <pubDate>Mon, 07 Oct 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 07 Oct 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Running Japanese Tokenizer Models with text-embeddings-inference</title>
  <link>https://hotchpotch.dev/articles/tei/</link>
  <guid>https://hotchpotch.dev/articles/tei/</guid>
  <description>How to run Japanese embedding models that lack tokenizer.json on Hugging Face text-embeddings-inference by adding a dummy fast tokenizer.</description>
  <content:encoded><![CDATA[<p><a href="https://github.com/huggingface/text-embeddings-inference">text-embeddings-inference</a>, or TEI, is an inference server provided by Hugging Face. It is written in Rust, provides Docker containers for various GPU architectures, and when the GPU architecture supports FlashAttention 2, it is often about 1.5 to almost 2 times faster than running inference with Python's Transformers library. I find it useful as a high-performance production inference server.</p>
<p>One problem in Japanese environments is that TEI requires a Rust-based FastTokenizer, in other words a model with <code>tokenizer.json</code>. Many Japanese Transformer models use morphological analysis dictionaries and libraries such as UniDic or MeCab that run in Python, so many of them cannot be used through the <code>tokenizer.json</code> path.</p>
<p>This was a serious issue for me at first, but I found that some APIs such as <code>/embed</code> and <code>/embed_sparse</code>, though unfortunately not <code>/rerank</code>, can be used with a workaround. As an example, I will record the method using <a href="https://huggingface.co/cl-nagoya/ruri-base">cl-nagoya/ruri-base</a>.</p>
<h2>Prepare a Dummy tokenizer.json</h2>
<p>TEI checks for <code>tokenizer.json</code> when starting the model, and it will not start without one. Therefore we prepare a dummy <code>tokenizer.json</code>. You can create one yourself or use one from a public model. For this example I used the <a href="https://huggingface.co/hotchpotch/mMiniLMv2-L6-H384/blob/main/tokenizer.json"><code>tokenizer.json</code> from hotchpotch/mMiniLMv2-L6-H384</a>.</p>
<p>I created a version of <code>ruri-base</code> with this <code>tokenizer.json</code> added as <a href="https://huggingface.co/hotchpotch/ruri-base-dummy-fast-tokenizer-for-tei">ruri-base-dummy-fast-tokenizer-for-tei</a>.</p>
<h2>Start the Server with the Dummy tokenizer.json Model</h2>
<p>Prepare a <code>docker-compose.yaml</code> like this:</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">ruri-base:</span>
    <span class="hljs-comment"># Change the image to one that matches your architecture.</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">ghcr.io/huggingface/text-embeddings-inference:86-1.5</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8080:80"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/tmp/docker-tei-data:/data</span>
    <span class="hljs-comment"># Change pooling to match the model architecture.</span>
    <span class="hljs-attr">command:</span> [ <span class="hljs-string">"--model-id"</span>, <span class="hljs-string">"hotchpotch/ruri-base-dummy-fast-tokenizer-for-tei"</span>, <span class="hljs-string">"--dtype"</span>, <span class="hljs-string">"float16"</span>, <span class="hljs-string">"--pooling"</span>, <span class="hljs-string">"mean"</span>, <span class="hljs-string">"--max-batch-tokens"</span>, <span class="hljs-string">"131072"</span>, <span class="hljs-string">"--max-client-batch-size"</span>, <span class="hljs-string">"16"</span> ]
    <span class="hljs-attr">deploy:</span>
      <span class="hljs-attr">resources:</span>
        <span class="hljs-attr">reservations:</span>
          <span class="hljs-attr">devices:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">driver:</span> <span class="hljs-string">nvidia</span>
              <span class="hljs-attr">count:</span> <span class="hljs-number">1</span>
              <span class="hljs-attr">capabilities:</span> [ <span class="hljs-string">gpu</span> ]
</code></pre>
<p>Start it:</p>
<pre><code>$ docker compose up
...
ruri-base-1  | 2024-09-30T06:51:45.266929Z  INFO text_embeddings_router::http::server: router/src/http/server.rs:1778: Starting HTTP server: 0.0.0.0:80
ruri-base-1  | 2024-09-30T06:51:45.266940Z  INFO text_embeddings_router::http::server: router/src/http/server.rs:1779: Ready
</code></pre>
<p>It should now be running on port 8080.</p>
<h2>Convert to token_ids Locally and Call the API</h2>
<p>Next, tokenize locally and call the API with <code>token_ids</code>.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> AutoTokenizer
<span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np

tokenizer = AutoTokenizer.from_pretrained(<span class="hljs-string">"hotchpotch/ruri-base-dummy-fast-tokenizer-for-tei"</span>, use_fast=<span class="hljs-literal">False</span>)

sentences = [
    <span class="hljs-string">"クエリ: 瑠璃色はどんな色？"</span>,
    <span class="hljs-string">"文章: 瑠璃色（るりいろ）は、紫みを帯びた濃い青。名は、半貴石の瑠璃（ラピスラズリ、英: lapis lazuli）による。JIS慣用色名では「こい紫みの青」（略号 dp-pB）と定義している[1][2]。"</span>,
    <span class="hljs-string">"クエリ: ワシやタカのように、鋭いくちばしと爪を持った大型の鳥類を総称して「何類」というでしょう?"</span>,
    <span class="hljs-string">"文章: ワシ、タカ、ハゲワシ、ハヤブサ、コンドル、フクロウが代表的である。これらの猛禽類はリンネ前後の時代(17~18世紀)には鷲類・鷹類・隼類及び梟類に分類された。ちなみにリンネは狩りをする鳥を単一の目(もく)にまとめ、vultur(コンドル、ハゲワシ)、falco(ワシ、タカ、ハヤブサなど)、strix(フクロウ)、lanius(モズ)の4属を含めている。"</span>,
]

token_ids = tokenizer(sentences, padding=<span class="hljs-literal">False</span>, truncation=<span class="hljs-literal">False</span>, return_tensors=<span class="hljs-string">"np"</span>)[<span class="hljs-string">"input_ids"</span>]
token_ids = [t.tolist() <span class="hljs-keyword">for</span> t <span class="hljs-keyword">in</span> token_ids]

url = <span class="hljs-string">"http://127.0.0.1:8080/embed"</span>
payload = {<span class="hljs-string">"inputs"</span>: token_ids, <span class="hljs-string">"normalize"</span>: <span class="hljs-literal">False</span>, <span class="hljs-string">"truncate"</span>: <span class="hljs-literal">True</span>}
headers = {<span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>}

response = requests.post(url, json=payload, headers=headers)
embeddings_data = response.json()
embeddings = np.array(embeddings_data)
<span class="hljs-built_in">print</span>(embeddings.shape)

<span class="hljs-comment"># calc cosine similarity</span>
normalized_embeddings = embeddings / np.linalg.norm(embeddings, axis=<span class="hljs-number">1</span>, keepdims=<span class="hljs-literal">True</span>)
similarities = np.dot(normalized_embeddings, normalized_embeddings.T)

<span class="hljs-built_in">print</span>(similarities)
</code></pre>
<p>Result:</p>
<pre><code>(4, 768)

array([[1.        , 0.94194159, 0.68661375, 0.71621216],
       [0.94194159, 1.        , 0.66622363, 0.68591373],
       [0.68661375, 0.66622363, 1.        , 0.87196226],
       [0.71621216, 0.68591373, 0.87196226, 1.        ]])
</code></pre>
<p>This successfully obtains dense vectors, with cosine similarities almost the same as the values shown in the <code>ruri-base</code> model card. With this approach, TEI can be used with Japanese tokenizers for APIs other than reranking. Of course, if you send ordinary text instead of tokenized <code>token_ids</code>, you will get results that are completely off, so be careful.</p>
<hr>
<p>The real solution would be to send pull requests so TEI can start without <code>tokenizer.json</code> and the <code>/rerank</code> API also works properly. I have not done that because implementing it in Rust and communicating through the PR process feels like more work than I currently want to take on. I would be grateful if someone did.</p>]]></content:encoded>
  <pubDate>Mon, 30 Sep 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 30 Sep 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Releasing High-Performance Japanese Rerankers, and What Rerankers Are</title>
  <link>https://hotchpotch.dev/articles/japanese-reranker-release/</link>
  <guid>https://hotchpotch.dev/articles/japanese-reranker-release/</guid>
  <description>I released Japanese reranker models trained for search reranking and explain how rerankers improve retrieval quality after initial vector or keyword search.</description>
  <content:encoded><![CDATA[<p>💡 A newer version is available: <a href="/articles/japanese-reranker-v2">Releasing very small, fast, and practical Japanese rerankers: japanese-reranker-tiny and xsmall v2</a></p>
<p>There were very few rerankers trained specifically for Japanese, so I created a family of reranker models that properly learn Japanese. The family includes models from small to large sizes.</p>
<p>The evaluation results are below. As of early April 2024, I think these are among the strongest publicly available models on Japanese reranking tasks, partly because almost no rerankers trained on Japanese had been published.</p>
<table>
<thead>
<tr>
<th>Model name</th>
<th>layers</th>
<th>hidden_size</th>
<th><a href="https://huggingface.co/datasets/hotchpotch/JQaRA">JQaRA</a></th>
<th><a href="https://huggingface.co/datasets/hotchpotch/JaCWIR">JaCWIR</a></th>
<th><a href="https://huggingface.co/datasets/miracl/miracl">MIRACL</a></th>
<th><a href="https://github.com/yahoojapan/JGLUE">JSQuAD</a></th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-xsmall-v1">japanese-reranker-cross-encoder-xsmall-v1</a></td>
<td>6</td>
<td>384</td>
<td>0.6136</td>
<td>0.9376</td>
<td>0.7411</td>
<td>0.9602</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-small-v1">japanese-reranker-cross-encoder-small-v1</a></td>
<td>12</td>
<td>384</td>
<td>0.6247</td>
<td>0.939</td>
<td>0.7776</td>
<td>0.9604</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-base-v1">japanese-reranker-cross-encoder-base-v1</a></td>
<td>12</td>
<td>768</td>
<td>0.6711</td>
<td>0.9337</td>
<td>0.818</td>
<td>0.9708</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-large-v1">japanese-reranker-cross-encoder-large-v1</a></td>
<td>24</td>
<td>1024</td>
<td>0.7099</td>
<td>0.9364</td>
<td>0.8406</td>
<td>0.9773</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-bge-reranker-v2-m3-v1">japanese-bge-reranker-v2-m3-v1</a></td>
<td>24</td>
<td>1024</td>
<td>0.6918</td>
<td>0.9372</td>
<td>0.8423</td>
<td>0.9624</td>
</tr>
</tbody>
</table>
<p>Technical details about how these rerankers were created are in <a href="/articles/japanese-reranker-tech-report">Japanese Reranker Technical Report</a>.</p>
<h1>What Is a Reranker?</h1>
<p>A reranker, as the name suggests, reranks documents. Given a query, it reorders documents by relevance. You might wonder how this differs from sorting by similarity between text embeddings. In practice, embedding similarity can also be used for ranking, but there are two important differences.</p>
<p><img src="https://i.imgur.com/dQsNajT.png" alt="embeddings and reranker"></p>
<h2>Rerankers Have Strong Reranking Performance</h2>
<p>Text embeddings represent the query and document in the same vector space and measure similarity. This makes them efficient at large scale because document vectors can be computed offline in advance.</p>
<p>Rerankers, however, are specialized for reranking. The models I created use a CrossEncoder architecture that evaluates a query and document as one pair. This allows the model to understand finer nuances and contextual relationships between the query and document. As a result, relevant documents are more likely to move higher in the ranking.</p>
<h2>Rerankers Cannot Be Precomputed and Are Slow</h2>
<p>If rerankers are more accurate, one might ask why not evaluate everything with a reranker instead of using embeddings. The issue is that rerankers use both the query and document as input. With text embeddings, document vectors can be computed offline in advance. At search time, only the query vector needs to be computed.</p>
<p>A reranker, or CrossEncoder, cannot precompute document-only representations in the same way. If there are only 100 candidate documents, evaluating all of them online may be fine. As the number of documents grows, however, evaluating every candidate at search time becomes impractical.</p>
<h1>Where Rerankers Fit</h1>
<p>This does not mean rerankers are unusable in real-world search. A common approach is to first retrieve the top 100 related documents using an efficient offline-computable method such as text embeddings, and then use a reranker to reorder those 100 candidates more accurately.</p>
<p><img src="https://raw.githubusercontent.com/UKPLab/sentence-transformers/master/docs/img/InformationRetrieval.png" alt="Retrieve &#x26; Re-Rank Pipeline"></p>
<p>Quoted from <a href="https://www.sbert.net/examples/applications/retrieve_rerank/README.html#retrieve-re-rank-pipeline" title="Permalink to this headline">Retrieve &#x26; Re-Rank Pipeline</a>.</p>
<p>The same idea is also described in articles such as Google Cloud's <a href="https://cloud.google.com/blog/products/ai-machine-learning/rags-powered-by-google-search-technology-part-2?hl=en">Your RAGs powered by Google Search technology, part 2</a>, in the section on <strong>Deep re-ranking</strong>. Reranking is an important technique.</p>
<h2>How Much Can Reranking Improve Retrieval Scores?</h2>
<p>In a previous article, <a href="/articles/vector-search-ai-ou-comp">Taking on the First AI-Ou Quiz Competition with Vector Search Only: Evaluating Multiple Japanese Embeddings on a Q&#x26;A Task</a>, I converted about 5.5 million documents into vectors with several text embedding models and evaluated retrieval with approximate nearest neighbor search using IVFPQ. The accuracy results for those embedding models are below.</p>
<ul>
<li><a href="https://docs.google.com/spreadsheets/d/1eSYzxzIfN3uMIpFKDGCTQsIxuWYELBtD49LQbl88GUE/edit?usp=sharing">JAQKET v1 vector search: Japanese embeddings evaluation</a></li>
</ul>
<p>Now let's take the top 100 results from that approximate nearest neighbor search and rerank them with the smallest and fastest model in this family, the xsmall reranker.</p>
<p><img src="https://i.imgur.com/dr8Ahy1.png" alt="rerank - japanese-reranker-cross-encoder-small-v1"></p>
<p>The results improve substantially. Because the task searches about 5.5 million documents at practical speed with approximate nearest neighbor search, accuracy is lower than exhaustive embedding similarity search. Reranking those results with a reranker leads to a large score improvement.</p>
<p>OpenAI text embeddings, for example, often do not score especially well on Japanese information retrieval tasks, but reranking improves their scores considerably.</p>
<p>Next, let's rerank with the larger <code>large</code> reranker model.</p>
<p><img src="https://i.imgur.com/Rk8bmIy.png" alt="rerank - japanese-reranker-cross-encoder-large-v1"></p>
<p>The score improves further. If you have enough compute, using a larger model is reasonable, but reranking latency increases with model size. The time required to evaluate <a href="https://huggingface.co/datasets/hotchpotch/JaCWIR">JaCWIR</a> on an RTX 3090 was as follows.</p>
<table>
<thead>
<tr>
<th>Model name</th>
<th>layers</th>
<th>hidden_size</th>
<th>Runtime (sec)</th>
</tr>
</thead>
<tbody>
<tr>
<td>japanese-reranker-cross-encoder-xsmall-v1</td>
<td>6</td>
<td>384</td>
<td>196</td>
</tr>
<tr>
<td>japanese-reranker-cross-encoder-small-v1</td>
<td>12</td>
<td>384</td>
<td>265</td>
</tr>
<tr>
<td>japanese-reranker-cross-encoder-base-v1</td>
<td>12</td>
<td>768</td>
<td>481</td>
</tr>
<tr>
<td>japanese-reranker-cross-encoder-large-v1</td>
<td>24</td>
<td>1024</td>
<td>1253</td>
</tr>
<tr>
<td>japanese-bge-reranker-v2-m3-v1</td>
<td>24</td>
<td>1024</td>
<td>1173</td>
</tr>
</tbody>
</table>
<p>The xsmall and large models differ by about 6x in speed. There is a clear tradeoff between performance and latency, so it is important to choose a reranker according to the needed accuracy and speed. In runtime search systems, reranker latency often matters.</p>
<p>For evaluation results against many other models, see <a href="/articles/japanese-reranker-tech-report">Japanese Reranker Technical Report</a>. Also note that although the reranking evaluation in this article uses the test data from the AI-Ou Quiz competition and was not directly trained on it, these models did train on <a href="https://huggingface.co/datasets/hotchpotch/JQaRA">JQaRA</a>, a dataset made from the competition's dev and unused data. This may make scores easier to improve.</p>
<h1>Rerankers Are Surprisingly Important</h1>
<p>I decided to build Japanese rerankers because, when searching over millions of documents, combining embeddings plus approximate nearest neighbor search with a reranker produced much better results than embeddings plus ANN alone. At that time I was using the multilingual reranker <a href="https://huggingface.co/corrius/cross-encoder-mmarco-mMiniLMv2-L12-H384-v1">cross-encoder-mmarco-mMiniLMv2-L12-H384-v1</a>. If a multilingual model could improve accuracy that much, I thought a model properly trained on Japanese might improve it further.</p>
<p>Rerankers require online computation, which is both a drawback and an advantage. The drawback is computational cost. A benefit other than accuracy is that you do not need to recompute precomputed data. If you want to replace a text embedding model with a better one, existing document vectors stored in a database must be changed carefully in production, and recomputing hundreds of millions of vectors can be expensive. A reranker is more like replacing the sorting algorithm. It can often be swapped in without changing precomputed data.</p>
<p>I have also observed that rerankers can improve performance substantially when trained on domain data for the task being solved. This suggests a useful split: use a general-purpose model for text embeddings, and use a domain-specific model for reranking.</p>
<p>This article introduced Japanese reranker models and explained what rerankers are. Much of the current attention is on training and using LLMs, but as LLM usage expands, I think search will increasingly be optimized for AI rather than humans, and information retrieval will become even more important.</p>
<p>Rerankers will likely become an essential tool for improving retrieval. I hope this article helps more people become interested in rerankers and information retrieval.</p>
<hr>
<p>This article was lightly edited from text generated by <a href="https://www.anthropic.com/news/claude-3-family">Claude 3 Opus</a> based on my draft.</p>]]></content:encoded>
  <pubDate>Tue, 02 Apr 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 02 Apr 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Technical Report on Building Japanese Rerankers</title>
  <link>https://hotchpotch.dev/articles/japanese-reranker-tech-report/</link>
  <guid>https://hotchpotch.dev/articles/japanese-reranker-tech-report/</guid>
  <description>A technical report on training Japanese CrossEncoder rerankers, including data construction, hard negatives, model variants, and evaluation results.</description>
  <content:encoded><![CDATA[<p>This is a technical report on building Japanese reranker, or CrossEncoder, models. For an explanation of what rerankers are, see <a href="/articles/japanese-reranker-release">Releasing High-Performance Japanese Rerankers, and What Rerankers Are</a>.</p>
<p>The models created are:</p>
<table>
<thead>
<tr>
<th>Model name</th>
<th>layers</th>
<th>hidden_size</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-xsmall-v1">hotchpotch/japanese-reranker-cross-encoder-xsmall-v1</a></td>
<td>6</td>
<td>384</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-small-v1">hotchpotch/japanese-reranker-cross-encoder-small-v1</a></td>
<td>12</td>
<td>384</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-base-v1">hotchpotch/japanese-reranker-cross-encoder-base-v1</a></td>
<td>12</td>
<td>768</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-large-v1">hotchpotch/japanese-reranker-cross-encoder-large-v1</a></td>
<td>24</td>
<td>1024</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-bge-reranker-v2-m3-v1">hotchpotch/japanese-bge-reranker-v2-m3-v1</a></td>
<td>24</td>
<td>1024</td>
</tr>
</tbody>
</table>
<h2>How CrossEncoders Are Trained</h2>
<p>A CrossEncoder can be trained as a simple regression task. Text in the form <code>query text[SEP]passage text</code>, separated by a SEP token or similar, is labeled as <code>1.0</code> for positives and <code>0.0</code> for negatives. For concrete training code, the <a href="https://github.com/UKPLab/sentence-transformers/tree/master/examples/training/cross-encoder">SentenceTransformers CrossEncoder training examples</a> are easy to understand.</p>
<p>Performance improves significantly when multiple negatives, or hard negatives, are trained in the same batch as the positive. <a href="https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/reranker">FlagEmbedding's reranker trainer</a> is a useful reference for this approach.</p>
<h2>Training Datasets</h2>
<p>Training requires datasets of questions, positives, and negatives. I used one positive and 15 hard negatives per item, for a group of 16 examples. The datasets were:</p>
<ul>
<li><a href="https://huggingface.co/datasets/hotchpotch/JQaRA">JQaRA</a>: 7,270 records from <code>dev</code> and <code>unused</code></li>
<li><a href="https://github.com/yahoojapan/JGLUE#jsquad">JSQuAD</a>:
<ul>
<li>62,859 records from <code>train</code></li>
<li>Additional Wikipedia passages for hard-negative mining</li>
</ul>
</li>
<li><a href="https://huggingface.co/datasets/miracl/miracl">miracl</a>: 6,984 Japanese records from <code>train</code></li>
<li><a href="https://huggingface.co/datasets/unicamp-dl/mmarco">mmarco</a>: 346,413 filtered Japanese records from <code>train</code></li>
<li><a href="https://huggingface.co/datasets/castorini/mr-tydi">mr_tydi</a>:
<ul>
<li>3,697 Japanese records from <code>train</code></li>
<li>The Japanese MIRACL data contains many records overlapping with this mr_tydi data</li>
</ul>
</li>
<li>Wikipedia lead sections:
<ul>
<li>40,130 pairs of Wikipedia titles and lead paragraphs</li>
<li>Hard-negative mining also used only Wikipedia lead paragraphs</li>
</ul>
</li>
</ul>
<h2>Evaluation Datasets</h2>
<p>The models were evaluated with:</p>
<ul>
<li><a href="https://huggingface.co/datasets/hotchpotch/JQaRA">JQaRA</a>:
<ul>
<li>2,000 <code>test</code> records</li>
<li>Metric: <code>NDCG@10</code>, as defined for JQaRA evaluation</li>
</ul>
</li>
<li>JSQuAD:
<ul>
<li>4,442 <code>validation</code> records</li>
<li>19 negatives added by hard-negative mining from Wikipedia, evaluated with <code>MAP@10</code> over 20 total candidates</li>
</ul>
</li>
<li>miracl:
<ul>
<li>704 records from <code>dev</code>, filtered to records with at least 9 negatives</li>
<li>1 positive and 9 negatives, evaluated with <code>MAP@10</code></li>
<li>Japanese MIRACL has some overlap between <code>dev</code> and <code>train</code>, so training more on <code>train</code> tends to raise <code>dev</code> evaluation</li>
</ul>
</li>
<li><a href="https://huggingface.co/datasets/hotchpotch/JaCWIR">JaCWIR</a>:
<ul>
<li>5,000 <code>eval</code> records</li>
<li>Metric: <code>MAP@10</code>, as defined for JaCWIR reranker evaluation</li>
</ul>
</li>
</ul>
<h3>Hard-Negative Mining</h3>
<p>Hard negatives are examples that a model is likely to mistakenly judge as positives, even though they are actually negative. Actively mining them increases the diversity and difficulty of the training data and can improve model accuracy.</p>
<p>For these models, I mined hard negatives with BM25 and multiple SentenceTransformer models. Using semantic textual similarity tasks, I extracted texts semantically similar to positives but actually negative, and randomly sampled from high-similarity candidates.</p>
<h2>Pretrained Base Models</h2>
<p>The following pretrained models were used as bases. For <code>BAAI/bge-reranker-v2-m3</code>, training on all data reduced generalization, so I randomly sampled 10,000 records each from mMARCO, JSQuAD, and Wikipedia lead sections, while using all records from the other datasets.</p>
<ul>
<li><code>japanese-reranker-cross-encoder-xsmall-v1</code>
<ul>
<li>Microsoft <a href="https://huggingface.co/hotchpotch/mMiniLMv2-L6-H384">mMiniLMv2-L6-H384</a></li>
<li>6 layers, 384 hidden size</li>
</ul>
</li>
<li><code>japanese-reranker-cross-encoder-small-v1</code>
<ul>
<li>Microsoft <a href="https://huggingface.co/hotchpotch/mMiniLMv2-L12-H384">mMiniLMv2-L12-H384</a></li>
<li>12 layers, 384 hidden size</li>
</ul>
</li>
<li><code>japanese-reranker-cross-encoder-base-v1</code>
<ul>
<li><a href="https://huggingface.co/cl-nagoya/sup-simcse-ja-base">cl-nagoya/sup-simcse-ja-base</a></li>
<li><a href="https://huggingface.co/tohoku-nlp/bert-base-japanese-v3">tohoku-nlp/bert-base-japanese-v3</a></li>
<li>A merged model from models trained from both sources</li>
<li>12 layers, 768 hidden size</li>
</ul>
</li>
<li><code>japanese-reranker-cross-encoder-large-v1</code>
<ul>
<li><a href="https://huggingface.co/cl-nagoya/sup-simcse-ja-large">cl-nagoya/sup-simcse-ja-large</a></li>
<li><a href="https://huggingface.co/tohoku-nlp/bert-large-japanese-v2">tohoku-nlp/bert-large-japanese-v2</a></li>
<li>A merged model from models trained from both sources</li>
<li>24 layers, 1024 hidden size</li>
</ul>
</li>
<li><code>japanese-bge-reranker-v2-m3-v1</code>
<ul>
<li><a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">BAAI/bge-reranker-v2-m3</a></li>
<li>24 layers, 1024 hidden size</li>
</ul>
</li>
</ul>
<h2>Handling Overfitting</h2>
<p>During CrossEncoder training, I found that because Wikipedia passages were used as hard negatives, evaluation on Wikipedia-related tasks such as JQaRA, JSQuAD, and Japanese MIRACL improved, while generalization outside Wikipedia domains degraded as training continued. To balance this, I created <a href="https://huggingface.co/datasets/hotchpotch/JaCWIR">JaCWIR</a>, an out-of-domain dataset not included in the training data, and used it for evaluation while training.</p>
<p>Training beyond <code>1 epoch</code> caused overfitting, so training was limited to <code>1 epoch</code>.</p>
<h2>Training Parameters</h2>
<p>The main model training used roughly the following parameters:</p>
<ul>
<li><code>batch_size</code>: <code>512</code> with gradient accumulation
<ul>
<li>Since 16 examples form one group, the actual batch contains <code>512 * 16 = 8192</code> positive and negative examples</li>
</ul>
</li>
<li><code>warmup_ratio</code>: <code>0.25</code></li>
<li>Scheduler: <code>cosine</code></li>
<li>Optimizer: <code>paged_adamw_32bit</code></li>
<li><code>learning_rate</code>:
<ul>
<li><code>xsmall</code> = <code>2e-04</code></li>
<li><code>small</code> = <code>5e-04</code></li>
<li><code>base</code> = <code>8e-05</code></li>
<li><code>large</code> = <code>3e-05</code></li>
</ul>
</li>
<li>Loss:
<ul>
<li>Cross entropy</li>
</ul>
</li>
</ul>
<h2>Using Large Models as Teachers</h2>
<p>For <code>xsmall</code> and <code>small</code>, I also used inference outputs from <code>japanese-reranker-cross-encoder-large-v1</code> and <code>japanese-bge-reranker-v2-m3-v1</code> as teacher labels. Teacher outputs are continuous inference values, such as <code>pos=0.98</code> and <code>negs=[0.02, 0.07, ...]</code>, so they can be used as regression targets rather than only <code>0</code> and <code>1</code>. Using teacher outputs gave a small score improvement. MSE loss was used for this training.</p>
<h2>Creating Mix Models</h2>
<p>Changing datasets, score parameters, and seeds produces diverse training results. Linearly combining separately trained models can improve performance by adding diversity. I confirmed score improvements by combining multiple trained models. I used <a href="https://github.com/FlagOpen/FlagEmbedding/tree/master/LM_Cocktail">LM_Cocktail</a> for model merging.</p>
<p>One caveat is that the merged model has a smaller output standard deviation, so there may be some performance degradation during quantization or similar processing.</p>
<h2>Evaluation Results</h2>
<p>The CrossEncoder evaluation results are below. <code>BAAI/bge-reranker-v2-m3</code> already has strong multilingual generalization and high Japanese performance. If model size is not an issue, I think it is a good base model to fine-tune for reranker training, even with only a few thousand examples.</p>
<p>Scores on these evaluation datasets tend to increase when training on the corresponding public train data. The models created here learned the tendencies of train data for all datasets except JaCWIR, so that should be considered when reading the scores.</p>
<table>
<thead>
<tr>
<th>Model Name</th>
<th><a href="https://huggingface.co/datasets/hotchpotch/JQaRA">JQaRA</a></th>
<th><a href="https://huggingface.co/datasets/hotchpotch/JaCWIR">JaCWIR</a></th>
<th><a href="https://huggingface.co/datasets/miracl/miracl">MIRACL</a></th>
<th><a href="https://github.com/yahoojapan/JGLUE">JSQuAD</a></th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-xsmall-v1">japanese-reranker-cross-encoder-xsmall-v1</a></td>
<td>0.6136</td>
<td>0.9376</td>
<td>0.7411</td>
<td>0.9602</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-small-v1">japanese-reranker-cross-encoder-small-v1</a></td>
<td>0.6247</td>
<td>0.939</td>
<td>0.7776</td>
<td>0.9604</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-base-v1">japanese-reranker-cross-encoder-base-v1</a></td>
<td>0.6711</td>
<td>0.9337</td>
<td>0.818</td>
<td>0.9708</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-reranker-cross-encoder-large-v1">japanese-reranker-cross-encoder-large-v1</a></td>
<td>0.7099</td>
<td>0.9364</td>
<td>0.8406</td>
<td>0.9773</td>
</tr>
<tr>
<td><a href="https://huggingface.co/hotchpotch/japanese-bge-reranker-v2-m3-v1">japanese-bge-reranker-v2-m3-v1</a></td>
<td>0.6918</td>
<td>0.9372</td>
<td>0.8423</td>
<td>0.9624</td>
</tr>
<tr>
<td><a href="https://huggingface.co/BAAI/bge-reranker-v2-m3">bge-reranker-v2-m3</a></td>
<td>0.673</td>
<td>0.9343</td>
<td>0.8374</td>
<td>0.9599</td>
</tr>
<tr>
<td><a href="https://huggingface.co/BAAI/bge-reranker-large">bge-reranker-large</a></td>
<td>0.4718</td>
<td>0.7332</td>
<td>0.7666</td>
<td>0.7081</td>
</tr>
<tr>
<td><a href="https://huggingface.co/BAAI/bge-reranker-base">bge-reranker-base</a></td>
<td>0.2445</td>
<td>0.4905</td>
<td>0.6792</td>
<td>0.5757</td>
</tr>
<tr>
<td><a href="https://huggingface.co/corrius/cross-encoder-mmarco-mMiniLMv2-L12-H384-v1">cross-encoder-mmarco-mMiniLMv2-L12-H384-v1</a></td>
<td>0.5588</td>
<td>0.9211</td>
<td>0.7158</td>
<td>0.932</td>
</tr>
<tr>
<td><a href="https://huggingface.co/cl-nagoya/shioriha-large-reranker">shioriha-large-reranker</a></td>
<td>0.5775</td>
<td>0.8458</td>
<td>0.8084</td>
<td>0.9262</td>
</tr>
<tr>
<td><a href="https://huggingface.co/BAAI/bge-m3">bge-m3+all</a></td>
<td>0.576</td>
<td>0.904</td>
<td>0.7926</td>
<td>0.9226</td>
</tr>
<tr>
<td><a href="https://huggingface.co/BAAI/bge-m3">bge-m3+dense</a></td>
<td>0.539</td>
<td>0.8642</td>
<td>0.7753</td>
<td>0.8815</td>
</tr>
<tr>
<td><a href="https://huggingface.co/BAAI/bge-m3">bge-m3+colbert</a></td>
<td>0.5656</td>
<td>0.9064</td>
<td>0.7902</td>
<td>0.9297</td>
</tr>
<tr>
<td><a href="https://huggingface.co/BAAI/bge-m3">bge-m3+sparse</a></td>
<td>0.5088</td>
<td>0.8944</td>
<td>0.6941</td>
<td>0.9184</td>
</tr>
<tr>
<td><a href="https://huggingface.co/bclavie/JaColBERTv2">JaColBERTv2</a></td>
<td>0.5847</td>
<td>0.9185</td>
<td>0.6861</td>
<td>0.9247</td>
</tr>
<tr>
<td><a href="https://huggingface.co/intfloat/multilingual-e5-large">multilingual-e5-large</a></td>
<td>0.554</td>
<td>0.8759</td>
<td>0.7722</td>
<td>0.8892</td>
</tr>
<tr>
<td><a href="https://huggingface.co/intfloat/multilingual-e5-small">multilingual-e5-small</a></td>
<td>0.4917</td>
<td>0.869</td>
<td>0.7025</td>
<td>0.8565</td>
</tr>
<tr>
<td>bm25</td>
<td>0.458</td>
<td>0.8408</td>
<td>0.4387</td>
<td>0.9002</td>
</tr>
</tbody>
</table>
<hr>
<p>This article was lightly edited from text generated by <a href="https://www.anthropic.com/news/claude-3-family">Claude 3 Opus</a> based on my notes and instructions.</p>]]></content:encoded>
  <pubDate>Tue, 02 Apr 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 02 Apr 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>After reading Human-in-the-Loop Machine Learning: a data-centric and suggestive book</title>
  <link>https://hotchpotch.dev/articles/human-in-the-loop-ml/</link>
  <guid>https://hotchpotch.dev/articles/human-in-the-loop-ml/</guid>
  <description>A review of the Japanese translation of Human-in-the-Loop Machine Learning, focusing on why its data-centric view of active learning, annotation, and collaboration with annotators is valuable in the current AI era.</description>
  <content:encoded><![CDATA[<p>This is a review after reading <a href="https://amzn.to/4bS31eK">Human-in-the-Loop Machine Learning: Active learning and annotation for human-centered AI</a>, which I received from Junya Ueda, one of the translators.</p>
<p><img src="https://storage.googleapis.com/secons-site-images/photo/large/20240129_L1007411.webp" alt=""></p>
<hr>
<p>Recently, with the rise of generative AI and LLMs, I hear more often about how to collect and create "high-quality data". Not only for data used to train LLMs, but also for solving familiar problems, it has become natural to define the task needed for the problem, analyze data for it, and create data in order to solve social problems.</p>
<p>For many of these problems, you do not need to invent a new model yourself. Defining the task, collecting data, and training can often produce enough performance. This is the so-called data-centric way of thinking, focused on data.</p>
<p>There are many model-centric discussions in the world, about models and algorithms, while data is often treated as something evaluated on an already published dataset. Human-in-the-Loop Machine Learning is rare because it focuses on data and explains it in depth.</p>
<p>For what the book contains, it is best to read the <a href="https://shunyaueta.com/posts/2023-11-14-2336/">table of contents and reaction summary</a>, so please look there. Personally, the first part that strongly interested me was data sampling methods for active learning. It is easy to think that data near a clear linear decision boundary, where confidence is low, should be annotated. But the book discusses from many perspectives how to interpret uncertainty and diversity, and what strategies to use when deciding which data should be annotated. It is full of ideas, and implementations, that made me stop and think.</p>
<p>The second was collaboration with annotators. As the book says, "people management is essential." It is not at all a matter of saying "label it like this, thanks" and leaving the rest to them. The book strongly argues that you should treat collaboration with annotators like ordinary people management: how to make requests, what skills are needed, how to give feedback, how to remove annotator bias, how to handle uncertainty for each annotator, and how to communicate and provide feedback. Of course, it also contains many hints beyond people management, such as bias.</p>
<p>There were useful points everywhere, and because I read it carefully, it took about two months to finish. It was that interesting, and as someone who works with machine learning, I am truly glad I encountered this book.</p>
<p>Now that the LLM-driven AI era has begun, it will probably become normal to have AI evaluate data, use that feedback to create high-quality training datasets, and perform reinforcement learning. The original Human-in-the-Loop Machine Learning was written before GPT-4 appeared and before LLMs drew as much attention as they do now, but I think the book's viewpoint will be very useful when combined with LLMs too.</p>]]></content:encoded>
  <pubDate>Mon, 26 Feb 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 26 Feb 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>ColBERT reaches e5-large-level performance on a Japanese RAG task</title>
  <link>https://hotchpotch.dev/articles/colbert/</link>
  <guid>https://hotchpotch.dev/articles/colbert/</guid>
  <description>I evaluated JaColBERT, a Japanese pretrained ColBERT model, on my usual AI-Ou Q&amp;A RAG benchmark and found performance only slightly below multilingual-e5-large.</description>
  <content:encoded><![CDATA[<p>The recently released Japanese pretrained ColBERT model, <a href="https://huggingface.co/bclavie/JaColBERT">JaColBERT</a>, seems to perform well. I evaluated it on the <a href="/articles/vector-search-ai-ou-comp">AI-Ou Q&#x26;A RAG task</a> that I usually use for evaluation.</p>
<p><img src="https://i.imgur.com/m3aDIu0.png" alt="Evaluation"></p>
<ul>
<li><a href="https://docs.google.com/spreadsheets/d/1eSYzxzIfN3uMIpFKDGCTQsIxuWYELBtD49LQbl88GUE/edit#gid=140790548">https://docs.google.com/spreadsheets/d/1eSYzxzIfN3uMIpFKDGCTQsIxuWYELBtD49LQbl88GUE/edit#gid=140790548</a></li>
</ul>
<p>The result was only slightly below multilingual-e5-large. Considering that the training data is small and the model size is almost the same as a 12-layer BERT, which is the same size as multilingual-e5-small, this is impressive.</p>
<h2>Reading the ColBERT implementation and papers</h2>
<p>That made me interested in ColBERT, so I read the papers and implementation.</p>
<ul>
<li>Papers I read
<ul>
<li><a href="https://arxiv.org/abs/2004.12832">ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT</a></li>
<li><a href="https://arxiv.org/abs/2112.01488">ColBERTv2: Effective and Efficient Retrieval via Lightweight Late Interaction</a></li>
<li><a href="https://arxiv.org/abs/2312.16144">JaColBERT and Hard Negatives, Towards Better Japanese-First Embeddings for Retrieval: Early Technical Report</a></li>
</ul>
</li>
<li>Implementations
<ul>
<li><a href="https://github.com/stanford-futuredata/ColBERT">https://github.com/stanford-futuredata/ColBERT</a></li>
<li><a href="https://github.com/bclavie/RAGatouille">https://github.com/bclavie/RAGatouille</a></li>
</ul>
</li>
</ul>
<p>ColBERT is not the usual method of outputting a sentence embedding with SentenceTransformer or similar and searching by similarity comparison. It is token-based similarity search. The final hidden layer of a sentence has contextual information for each token, so ColBERT uses token-level representations, not only a single sentence vector, to calculate similarity.</p>
<p><img src="https://i.imgur.com/ADtjJJB.png" alt="MaxSim"></p>
<p>The similarity calculation uses a method called MaxSim. It takes cosine similarities between query token outputs and document token outputs, then sums the maximum values. The MaxSim calculation itself is simple.</p>
<p>The query and document must be encoded separately, but the model used is BERT with 12 layers plus a custom head, a 128-dimensional linear layer. BERT's 12-layer hidden output is 768 dimensions, so it is converted to 128 dimensions through the linear layer.</p>
<p>In the actual implementation, the query and document are distinguished only by adding custom tokens after CLS: for a query, a prefix like <code>[CLS][unused0]</code>; for a document, <code>[CLS][unused1]</code>. The encoder itself is the same.</p>
<p>After encoding them, MaxSim is computed, and the document with the largest score is judged to be similar to the query. In the ColBERT implementation, symbols and padding tokens in document tokens are masked and ignored during calculation.</p>
<h3>Solving search-time performance issues</h3>
<p>With ordinary sentence-vector search, approximate nearest neighbor search, or ANN, can search quickly even from hundreds of millions of documents. However, ColBERT uses MaxSim over token similarities rather than sentence vectors, so that approach cannot be used as-is.</p>
<p>ColBERTv2 describes how to create an index that can solve this problem with fast nearest neighbor search. It seems to compress vectors in several ways, calculate centroids with KMeans, and search from there. The implementation imports FAISS, so I first thought it used FAISS indexes directly. But FAISS was only used to calculate cluster centroids with KMeans. Once the index is created, later search is quick.</p>
<p>Creating an index for the 5.5 million passages in the AI-Ou Q&#x26;A RAG task above took around five hours on a Ryzen 3900 + RTX 3090 environment. Be careful that FAISS is quite slow unless you use the GPU version, <code>faiss-gpu</code>.</p>
<h3>Trainable with little data</h3>
<p>According to the JaColBERT report, it was made from <code>bert-base-japanese-v3</code> by training for 10 hours with 10 million triplet examples and 8 NVIDIA L4 GPUs. If the data amount is small and training time is short, the possibilities are exciting.</p>
<h2>ColBERT's problems and practical difficulty</h2>
<p>Reading the implementation made me realize that ColBERT is not easy to use casually because both the processing itself and the implementation code are complex. <a href="https://github.com/bclavie/ragatouille">RAGatouille</a> addresses that with an approach that can be used quickly even with zero configuration. I used RAGatouille for this evaluation too.</p>
<p>RAGatouille can create indexes and search them, of course, and can also train models with a Trainer. It also supports modern integrations such as becoming a LangChain retriever.</p>
<h2>Can ColBERT be used on a production search server?</h2>
<p>One concern with ColBERT is whether it can be operated in production. As of early February 2024, it seems to require implementing and operating your own search API server, which is not easy to run casually.</p>
<p>However, the RAGatouille documentation says that the search engine <a href="https://vespa.ai/">Vespa</a> will support it soon. If that happens, operation should become much easier. Adding data to an index also still seems experimental, but if that works properly, it should become reasonable to consider for production.</p>
<p>Above all, if you can train on your own domain data at low cost, it may be usable as high-quality RAG retrieval for your own data. For that kind of use case, I would actively consider it.</p>
<h2>Closing</h2>
<p>So this was a note saying that ColBERT is impressive. People researching information retrieval probably already know ColBERT, but I did not, so learning about it felt fresh. I probably would not have become interested without JaColBERT, so I am grateful to <a href="https://ben.clavie.eu/">Benjamin Clavié</a>, its author. He is also the author of RAGatouille, which is very helpful.</p>]]></content:encoded>
  <pubDate>Fri, 02 Feb 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Fri, 02 Feb 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Evaluating OpenAI&apos;s new text-embedding-3-small on a RAG task</title>
  <link>https://hotchpotch.dev/articles/text-embedding-3-small/</link>
  <guid>https://hotchpotch.dev/articles/text-embedding-3-small/</guid>
  <description>I evaluated OpenAI&apos;s text-embedding-3-small on a Japanese Wikipedia Q&amp;A RAG retrieval task and compared it with multilingual-e5 models, using 512-dimensional reduced embeddings.</description>
  <content:encoded><![CDATA[<p>OpenAI recently released new embedding models, <a href="https://openai.com/blog/new-embedding-models-and-api-updates">text-embedding-3-small and text-embedding-3-large</a>. According to OpenAI, text-embedding-3-small costs one fifth as much as the older ada-v2 embedding model while improving performance.</p>
<p>According to OpenAI's article, MTEB scores improved slightly, and MIRACL scores improved substantially. MIRACL, Multilingual Information Retrieval Across a Continuum of Languages, is an information retrieval task across multiple languages. A large score improvement there suggests we can also expect better accuracy for Japanese information retrieval tasks.</p>
<h2>Evaluation on a Wikipedia Q&#x26;A RAG task</h2>
<p>So I evaluated it right away. I used the same method as <a href="/articles/vector-search-ai-ou-comp">Solving the first AI-Ou quiz competition with vector search only</a>: vector search over about 5.5 million passages and checking whether the retrieved results contain the answer. In other words, this tests whether RAG retrieval can find text containing the appropriate answer. For text-embedding-3-small, I passed an option to the OpenAI API to reduce the embedding dimension to 512, and used those dimension-reduced results. If the original 1536-dimensional data were used, the score would probably improve slightly.</p>
<p>The results are below.</p>
<ul>
<li><a href="https://docs.google.com/spreadsheets/d/1eSYzxzIfN3uMIpFKDGCTQsIxuWYELBtD49LQbl88GUE/edit#gid=140790548">https://docs.google.com/spreadsheets/d/1eSYzxzIfN3uMIpFKDGCTQsIxuWYELBtD49LQbl88GUE/edit#gid=140790548</a></li>
</ul>
<p><img src="https://i.imgur.com/lVlua6x.png" alt="Evaluation result"></p>
<p>The text-embedding-3-small result was low, and the multilingual-e5 series remained overwhelmingly strong for Q&#x26;A-style information retrieval tasks. That said, one strength of OpenAI embeddings may be their ability to embed long token inputs. The passages used here were under 400 Japanese characters, and the score might also be a little higher without dimension reduction.</p>
<h3>Data and code used</h3>
<ul>
<li><a href="https://github.com/hotchpotch/wikipedia-passages-jawiki-embeddings-utils">https://github.com/hotchpotch/wikipedia-passages-jawiki-embeddings-utils</a></li>
<li><a href="https://huggingface.co/datasets/hotchpotch/wikipedia-passages-jawiki-embeddings">https://huggingface.co/datasets/hotchpotch/wikipedia-passages-jawiki-embeddings</a></li>
</ul>
<h3>Extra: cost</h3>
<p>The total was <code>1,490,618,785</code> tokens, and cost about 30 USD. With the old model this would have cost five times as much, so I would not have felt like trying it. At this level, it feels roughly acceptable even for an individual.</p>]]></content:encoded>
  <pubDate>Mon, 29 Jan 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 29 Jan 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Summarizing same-date diary entries from secon.dev and sending them to Discord</title>
  <link>https://hotchpotch.dev/articles/n-diaries-summary/</link>
  <guid>https://hotchpotch.dev/articles/n-diaries-summary/</guid>
  <description>I built a small system that summarizes years of same-date diary entries from secon.dev and posts them to a private family Discord channel, using OpenAI and an LCEL-based implementation.</description>
  <content:encoded><![CDATA[<p>I have accumulated four or five years of old diary entries on <a href="https://secon.dev/">secon.dev</a>. Sometimes I look back at past entries from the same date, but there are many days when I skip them. I think occasionally touching past records helps keep memories fresh, so skipping them feels wasteful.</p>
<p>The reason I skip them is that accessing old diaries is troublesome. They are shown in the footer of same-date diary pages, but I still do not go look very often. Also, if a diary entry is long, reading all of it is a bother. I want someone to tell me the gist.</p>
<p>So I wrote an implementation that reduces the access friction by posting to a family Discord channel, used only by my wife and me, while summarizing past diary entries from the same date. Posting to Discord is easy with a webhook. I also made the Discord message use as little screen space as possible, since ordinary links expand and consume too much area, so it can be read quickly.</p>
<p>Summarizing old diaries is also easy because it just sends them to OpenAI. To get used to <a href="https://python.langchain.com/docs/expression_language/">LCEL</a>, I <a href="https://github.com/hotchpotch/secon-dev-n-year-summary/blob/main/secon_d_summary/chain.py">wrote the implementation in LCEL even though it is verbose</a>. LCEL is easy once you get used to it, and being able to use <a href="https://www.langchain.com/langsmith">LangSmith</a> for debugging is convenient.</p>
<ul>
<li><a href="https://github.com/hotchpotch/secon-dev-n-year-summary/">https://github.com/hotchpotch/secon-dev-n-year-summary/</a></li>
</ul>
<p><img src="https://i.imgur.com/cjIaTBg.png" alt="Discord example"></p>]]></content:encoded>
  <pubDate>Mon, 15 Jan 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 15 Jan 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Understanding LangChain Expression Language (LCEL)</title>
  <link>https://hotchpotch.dev/articles/lcel/</link>
  <guid>https://hotchpotch.dev/articles/lcel/</guid>
  <description>LCEL is LangChain&apos;s recommended way to build chains. This article explains the basic behavior of Runnable, RunnableSequence, RunnableParallel, dict syntax, invoke, and RunnablePassthrough step by step.</description>
  <content:encoded><![CDATA[<p>LCEL is a way to build LangChain chains easily. Development became active in the second half of 2023, and as of January 2024, writing LangChain code with LCEL is generally recommended, although the older style can still be used. For LCEL's benefits, the <a href="https://python.langchain.com/docs/expression_language/">official LCEL documentation</a> is a good reference.</p>
<p>However, when I started writing LCEL, code worked if I followed the official documentation exactly, but small changes often broke it. This was simply because I did not understand LCEL's behavior. The official documentation and tutorials contain many examples showing how simply LLM + RAG code can be written with LCEL, but they do not explain much about LCEL's behavior itself. Even when they do, the examples are often combined with ChatGPT or templates, so I had trouble finding an explanation of "how LCEL behaves in the first place."</p>
<p>So I wrote a notebook article using LangChain 0.1.0 that explains only the basics of LCEL behavior and helps understanding progress step by step. The Colab notebook is here:</p>
<ul>
<li><a href="https://colab.research.google.com/drive/1hC8aBzoo3p7psGiRM1IXiBMBk3Wj2ntQ">https://colab.research.google.com/drive/1hC8aBzoo3p7psGiRM1IXiBMBk3Wj2ntQ</a></li>
</ul>
<h2>LCEL basics</h2>
<p>The basic idea of LCEL in LangChain is simple. An object receives an input value and passes its output value to the next object. This is the same as ordinary implementation.</p>
<p>First, define a function that doubles a value.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">double</span>(<span class="hljs-params">x</span>):
    <span class="hljs-keyword">return</span> x * <span class="hljs-number">2</span>

double(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">4
</code></pre>
<p>Next, define a function that prints the argument to standard output and returns the argument unchanged.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">tap_print</span>(<span class="hljs-params">x</span>):
    <span class="hljs-string">"""
    引数の値を標準出力に出力して、引数の値をそのまま返す
    """</span>
    <span class="hljs-built_in">print</span>(<span class="hljs-string">f"tap_print: <span class="hljs-subst">{x}</span>"</span>)
    <span class="hljs-keyword">return</span> x
</code></pre>
<p>Now run the two functions together. Give an argument to <code>double</code>, run it, and pass the result to <code>tap_print</code>.</p>
<pre><code class="hljs language-python">tap_print(double(<span class="hljs-number">2</span>))
</code></pre>
<pre><code class="hljs language-text">tap_print: 4

4
</code></pre>
<p>That produced the expected result.</p>
<p>Next, convert these functions into <code>RunnableLambda</code>, a subclass of <code>Runnable</code>, which is LCEL's execution class.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> langchain_core.runnables <span class="hljs-keyword">import</span> RunnableLambda

r_double = RunnableLambda(double)
</code></pre>
<p>After conversion, you can use the Runnable interface. Let's call <code>invoke</code>, which runs a Runnable.</p>
<pre><code class="hljs language-python">r_double.invoke(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">4
</code></pre>
<p>You can also define a RunnableLambda-like function with the <code>@chain</code> decorator. A variable named <code>chain</code> appears later and would be confusing, so here I import it as <code>chain_decorator</code>.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> langchain_core.runnables <span class="hljs-keyword">import</span> chain <span class="hljs-keyword">as</span> chain_decorator

<span class="hljs-meta">@chain_decorator</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">r_double</span>(<span class="hljs-params">x</span>):
    <span class="hljs-keyword">return</span> x * <span class="hljs-number">2</span>

r_double.invoke(<span class="hljs-number">2</span>)  <span class="hljs-comment"># r_double は RunnableLambda になるので、invoke で実行できる</span>
</code></pre>
<pre><code class="hljs language-text">4
</code></pre>
<p>Convert <code>tap_print</code> into a Runnable as well.</p>
<pre><code class="hljs language-python">r_tap_print = RunnableLambda(tap_print)
r_tap_print.invoke(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">tap_print: 2

2
</code></pre>
<p>Now finally, let's connect and run them with <code>|</code>, the core of LCEL.</p>
<pre><code class="hljs language-python">chain = r_double | r_tap_print
chain.invoke(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">tap_print: 4

4
</code></pre>
<p>Good. <code>r_double</code> returns a result, and <code>r_tap_print</code> prints that result while returning it. What exactly is this <code>chain</code>?</p>
<pre><code class="hljs language-python">chain.__class__
</code></pre>
<pre><code class="hljs language-text">langchain_core.runnables.base.RunnableSequence
</code></pre>
<p>The chain is a <code>RunnableSequence</code>, a Runnable that runs serially. Let's write the same implementation without the <code>|</code> syntax sugar.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> langchain_core.runnables <span class="hljs-keyword">import</span> RunnableSequence

chain = RunnableSequence(r_double, r_tap_print)
chain.invoke(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">tap_print: 4

4
</code></pre>
<p>That produced the same result.</p>
<p>Now let's display the execution flow, or execution graph, for this Runnable.</p>
<pre><code class="hljs language-python">chain.get_graph().print_ascii()
</code></pre>
<pre><code class="hljs language-text">  +----------------+   
  | r_double_input |   
  +----------------+   
          *            
          *            
          *            
+------------------+   
| Lambda(r_double) |   
+------------------+   
          *            
          *            
          *            
+-------------------+  
| Lambda(tap_print) |  
+-------------------+  
          *            
          *            
          *            
+------------------+   
| tap_print_output |   
+------------------+   
</code></pre>
<p><code>double</code> receives the input, passes it to <code>tap_print</code>, and the final output becomes <code>tap_print_output</code>.</p>
<p>Now look at this code:</p>
<pre><code class="hljs language-python">chain = r_double | tap_print  <span class="hljs-comment"># tap_print は RunnableLambda ではない!</span>
chain.invoke(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">tap_print: 4

4
</code></pre>
<p>Why does this work even though <code>tap_print</code> is not a Runnable? It works because Runnable objects use Python's bitwise <code>|</code> operator behavior: if either the left or right side is a Runnable, the other side is automatically converted into a Runnable. The actual Runnable code looks like this:</p>
<pre><code class="hljs language-python">    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__or__</span>(<span class="hljs-params">
        self,
        other: <span class="hljs-type">Union</span>[
            Runnable[<span class="hljs-type">Any</span>, Other],
            <span class="hljs-type">Callable</span>[[<span class="hljs-type">Any</span>], Other],
            <span class="hljs-type">Callable</span>[[Iterator[<span class="hljs-type">Any</span>]], Iterator[Other]],
            Mapping[<span class="hljs-built_in">str</span>, <span class="hljs-type">Union</span>[Runnable[<span class="hljs-type">Any</span>, Other], <span class="hljs-type">Callable</span>[[<span class="hljs-type">Any</span>], Other], <span class="hljs-type">Any</span>]],
        ],
    </span>) -> RunnableSerializable[Input, Other]:
        <span class="hljs-string">"""Compose this runnable with another object to create a RunnableSequence."""</span>
        <span class="hljs-keyword">return</span> RunnableSequence(<span class="hljs-variable language_">self</span>, coerce_to_runnable(other))

    <span class="hljs-comment"># 同様に __ror__ も定義されている</span>
</code></pre>
<p>It wraps the result in <code>RunnableSequence</code>, and passes the other argument given to the <code>|</code> operator, <code>tap_print</code> in this example, through <code>coerce_to_runnable</code>.</p>
<p><code>coerce_to_runnable</code> is also important, so let's look at it.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">coerce_to_runnable</span>(<span class="hljs-params">thing: RunnableLike</span>) -> Runnable[Input, Output]:
    <span class="hljs-string">"""Coerce a runnable-like object into a Runnable.

    Args:
        thing: A runnable-like object.

    Returns:
        A Runnable.
    """</span>
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">isinstance</span>(thing, Runnable):
        <span class="hljs-keyword">return</span> thing
    <span class="hljs-keyword">elif</span> inspect.isasyncgenfunction(thing) <span class="hljs-keyword">or</span> inspect.isgeneratorfunction(thing):
        <span class="hljs-keyword">return</span> RunnableGenerator(thing)
    <span class="hljs-keyword">elif</span> <span class="hljs-built_in">callable</span>(thing):
        <span class="hljs-keyword">return</span> RunnableLambda(cast(<span class="hljs-type">Callable</span>[[Input], Output], thing))
    <span class="hljs-keyword">elif</span> <span class="hljs-built_in">isinstance</span>(thing, <span class="hljs-built_in">dict</span>):
        <span class="hljs-keyword">return</span> cast(Runnable[Input, Output], RunnableParallel(thing))
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">raise</span> TypeError(
            <span class="hljs-string">f"Expected a Runnable, callable or dict."</span>
            <span class="hljs-string">f"Instead got an unsupported type: <span class="hljs-subst">{<span class="hljs-built_in">type</span>(thing)}</span>"</span>
        )
</code></pre>
<p>This function does the following conversions: if the object is already a Runnable, do nothing; if it looks like a generator, convert it to <code>RunnableGenerator</code>; if it is callable, such as a function, convert it to <code>RunnableLambda</code>; if it is a dict, convert it to <code>RunnableParallel</code>; otherwise, raise an exception.</p>
<p>From this, you can see that in LCEL, if either side connected by <code>|</code> is a Runnable, the other side is converted into a Runnable, and a <code>RunnableSequence</code> connecting them is returned.</p>
<p>Now run the next code.</p>
<pre><code class="hljs language-python">chain = double | r_tap_print  <span class="hljs-comment"># double は　RunnableLambda ではない！</span>

chain.invoke(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">tap_print: 4

4
</code></pre>
<p>This time, <code>double</code> is not a Runnable, but <code>r_tap_print.__ror__</code> converts it into a RunnableSequence, so it can run.</p>
<p>So far, we have seen:</p>
<ul>
<li>A Runnable basically receives an input value with <code>invoke</code>, processes it, and returns an output value.</li>
<li>Connecting Runnables with <code>|</code> creates code that runs serially through <code>RunnableSequence</code>.</li>
<li>If either side of <code>|</code> is not a Runnable, it is automatically converted into a Runnable.</li>
</ul>
<p>Seen this way, Runnable feels simple and understandable.</p>
<h2>Dict syntax converted into RunnableParallel</h2>
<p>Next is the syntax using <code>|</code> and dict, which confused me a lot at first. Consider this implementation:</p>
<ul>
<li>Pass a number as the argument.
<ul>
<li>Keep the first value in <code>original_value</code>.</li>
<li>Put the doubled value in <code>double_value</code>.</li>
</ul>
</li>
<li>Pass those results to <code>tap_print</code>.</li>
</ul>
<p>Let's write code that does this.</p>
<pre><code class="hljs language-python">chain = {
    <span class="hljs-string">"original_value"</span>: <span class="hljs-keyword">lambda</span> x: x,
    <span class="hljs-string">"double_value"</span>: double,
} | r_tap_print

chain.invoke(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">tap_print: {'original_value': 2, 'double_value': 4}

{'original_value': 2, 'double_value': 4}
</code></pre>
<p>It worked, though it is not obvious why. If you use this by intuition without understanding the behavior, it gradually becomes confusing. That happened to me.</p>
<p>Let's check what this is doing by displaying the execution graph.</p>
<pre><code class="hljs language-python">chain.get_graph().print_ascii()
</code></pre>
<pre><code class="hljs language-text">+--------------------------------------------+    
| Parallel&#x3C;original_value,double_value>Input |    
+--------------------------------------------+    
               **              **                 
            ***                  ***              
          **                        **            
+-------------+               +----------------+  
| Lambda(...) |               | Lambda(double) |  
+-------------+               +----------------+  
               **              **                 
                 ***        ***                   
                    **    **                      
+---------------------------------------------+   
| Parallel&#x3C;original_value,double_value>Output |   
+---------------------------------------------+   
                        *                         
                        *                         
                        *                         
             +-------------------+                
             | Lambda(tap_print) |                
             +-------------------+                
                        *                         
                        *                         
                        *                         
              +------------------+                
              | tap_print_output |                
              +------------------+                
</code></pre>
<p>It suddenly becomes parallel execution, branches, aggregates the parallel results, and then passes them to <code>tap_print</code>.</p>
<p>This is the confusing point: when you connect a dict with <code>|</code>, <code>coerce_to_runnable</code> is called, it is automatically converted into <code>RunnableParallel</code>, and the values of the dict are run in parallel. When <code>RunnableParallel</code> receives a dict, it automatically converts the dict values into Runnables, runs them in parallel, and returns the results under the corresponding keys.</p>
<p>Let's use <code>coerce_to_runnable</code> directly and see the result of the type conversion that happened through <code>|</code>.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> langchain_core.runnables.base <span class="hljs-keyword">import</span> coerce_to_runnable

parallel = coerce_to_runnable(
    {
        <span class="hljs-string">"original_value"</span>: <span class="hljs-keyword">lambda</span> x: x,
        <span class="hljs-string">"double_value"</span>: double,
    }
)

parallel.invoke(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">{'original_value': 2, 'double_value': 4}
</code></pre>
<pre><code class="hljs language-python">parallel.__class__
</code></pre>
<pre><code class="hljs language-text">langchain_core.runnables.base.RunnableParallel
</code></pre>
<p>Declaratively writing code with the same behavior looks like this:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> langchain_core.runnables <span class="hljs-keyword">import</span> RunnableParallel

parallel = RunnableParallel(
    {
        <span class="hljs-string">"original_value"</span>: coerce_to_runnable(<span class="hljs-keyword">lambda</span> x: x),
        <span class="hljs-string">"double_value"</span>: coerce_to_runnable(double),
    }
)
parallel.invoke(<span class="hljs-number">2</span>)
</code></pre>
<pre><code class="hljs language-text">{'original_value': 2, 'double_value': 4}
</code></pre>
<p>So in LCEL, connecting a dict with <code>|</code> produces code that runs in parallel with <code>RunnableParallel</code> and returns values.</p>
<h2>invoke and dict</h2>
<p>Next, call <code>invoke</code> with a dict. Note that this is completely different from the dict that is converted into <code>RunnableParallel</code> above. This is an ordinary call with a dict as the argument.</p>
<pre><code class="hljs language-python">data = {
    <span class="hljs-string">"input_value"</span>: <span class="hljs-number">2</span>,
    <span class="hljs-string">"input_do_nothing"</span>: <span class="hljs-number">100</span>,
}
chain = r_double | r_tap_print
<span class="hljs-keyword">try</span>:
    chain.invoke(data)
<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Error:"</span>, e)
</code></pre>
<pre><code class="hljs language-text">Error: unsupported operand type(s) for *: 'dict' and 'int'
</code></pre>
<p><code>r_double</code> expects an int, but it received a dict, so it cannot process it. You may say, "Just write <code>chain.invoke(data['input_value'])</code>", and that is true. But if <code>r_double</code> is in the middle of a chain and a dict reaches it, it cannot handle it.</p>
<p>In that case, insert a function that extracts only <code>input_value</code>.</p>
<pre><code class="hljs language-python">data = {
    <span class="hljs-string">"input_value"</span>: <span class="hljs-number">2</span>,
    <span class="hljs-string">"input_do_nothing"</span>: <span class="hljs-number">100</span>,
}
chain = (<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">"input_value"</span>]) | r_double | r_tap_print
chain.invoke(data)
</code></pre>
<pre><code class="hljs language-text">tap_print: 4

4
</code></pre>
<p>That worked.</p>
<p>Now let's write code that passes <code>input_do_nothing</code> onward unchanged, passes the calculation result of <code>input_value</code> onward as <code>double_value</code>, and also passes the original <code>input_value</code>.</p>
<pre><code class="hljs language-python">data = {
    <span class="hljs-string">"input_value"</span>: <span class="hljs-number">2</span>,
    <span class="hljs-string">"input_do_nothing"</span>: <span class="hljs-number">100</span>,
}
chain = {
    <span class="hljs-string">"double_value"</span>: (<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">"input_value"</span>]) | r_double,
    <span class="hljs-string">"input_value"</span>: <span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">"input_value"</span>],
    <span class="hljs-string">"input_do_nothing"</span>: <span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">"input_do_nothing"</span>],
} | r_tap_print
chain.invoke(data)
</code></pre>
<pre><code class="hljs language-text">tap_print: {'double_value': 4, 'input_value': 2, 'input_do_nothing': 100}

{'double_value': 4, 'input_value': 2, 'input_do_nothing': 100}
</code></pre>
<p>The intended values were passed to <code>r_tap_print</code>.</p>
<p>In the initial dict definition of this chain, the next <code>|</code> operator calls <code>r_tap_print.__ror__</code>, passes the dict to <code>coerce_to_runnable</code>, and automatically converts it into <code>RunnableParallel</code>, which produces the intended behavior.</p>
<p>But this is very verbose. If <code>data</code> had many more keys and you wanted to pass all of them as input values to later Runnables, it would be painful. For that situation, <code>RunnablePassthrough</code> exists. Let's rewrite this using <code>RunnablePassthrough</code>.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> langchain_core.runnables <span class="hljs-keyword">import</span> RunnablePassthrough

data = {
    <span class="hljs-string">"input_value"</span>: <span class="hljs-number">2</span>,
    <span class="hljs-string">"input_do_nothing"</span>: <span class="hljs-number">100</span>,
}
chain = (
    RunnablePassthrough().assign(
        double_value=(<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">"input_value"</span>]) | r_double,
    )
    | r_tap_print
)
chain.invoke(data)
</code></pre>
<pre><code class="hljs language-text">tap_print: {'input_value': 2, 'input_do_nothing': 100, 'double_value': 4}

{'input_value': 2, 'input_do_nothing': 100, 'double_value': 4}
</code></pre>
<p>Let's also add a result that triples the value.</p>
<pre><code class="hljs language-python">chain = (
    RunnablePassthrough().assign(
        double_value=(<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">"input_value"</span>]) | r_double,
        triple_value=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">"input_value"</span>] * <span class="hljs-number">3</span>,  <span class="hljs-comment"># 暗黙的に RunnableLambda に変換される</span>
    )
    | r_tap_print
)
chain.invoke(data)
</code></pre>
<pre><code class="hljs language-text">tap_print: {'input_value': 2, 'input_do_nothing': 100, 'double_value': 4, 'triple_value': 6}

{'input_value': 2,
 'input_do_nothing': 100,
 'double_value': 4,
 'triple_value': 6}
</code></pre>
<p>As you can see, <code>RunnablePassthrough</code> is very convenient when you want to modify only part of an input dict, add keys, and pass it to the next Runnable.</p>
<p>Be careful not to confuse passing a dict as the argument to <code>invoke</code> with writing LCEL by connecting a dict with <code>|</code>, which effectively converts it into <code>RunnableParallel</code>. They have completely different intentions and behavior.</p>
<p><code>RunnablePassthrough</code> can also be used declaratively to turn a non-dict input value into a dict.</p>
<pre><code class="hljs language-python">value_format = <span class="hljs-string">"value is {value}, double value is {double_value}"</span>

<span class="hljs-keyword">def</span> <span class="hljs-title function_">template</span>(<span class="hljs-params">data</span>):
    <span class="hljs-keyword">return</span> value_format.<span class="hljs-built_in">format</span>(**data)

r_template = RunnableLambda(template)

chain = (
    {
        <span class="hljs-string">"value"</span>: RunnablePassthrough(),
        <span class="hljs-string">"double_value"</span>: RunnablePassthrough() | double,
    }  <span class="hljs-comment"># r_double でなく double でも、自動で RunnableLambda に型変換される</span>
    | r_template
    | r_tap_print
)
chain.invoke(<span class="hljs-number">100</span>)
</code></pre>
<pre><code class="hljs language-text">tap_print: value is 100, double value is 200

'value is 100, double value is 200'
</code></pre>
<p>You now understand why this behaves this way.</p>
<h2>Next steps</h2>
<p>If you understand this much, you should be able to read LangChain LCEL sample code and most LCEL code written by others. Looking back, if you understand how automatic conversion into Runnable works and that writing a dict around <code>|</code> turns it into <code>RunnableParallel</code>, you should run into far fewer implementation problems.</p>
<p>If you want to know why something behaves a certain way in practice, reading the Runnable source code may be the fastest way to understand it. In that case, I recommend reading the code.</p>
<ul>
<li><a href="https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html">https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html</a></li>
<li><a href="https://github.com/langchain-ai/langchain/tree/master/libs/core/langchain_core/runnables">https://github.com/langchain-ai/langchain/tree/master/libs/core/langchain_core/runnables</a></li>
</ul>
<p>I hope this article and notebook help someone understand LCEL.</p>]]></content:encoded>
  <pubDate>Thu, 11 Jan 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Thu, 11 Jan 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Looking back on 2023</title>
  <link>https://hotchpotch.dev/articles/furikaeri-2023/</link>
  <guid>https://hotchpotch.dev/articles/furikaeri-2023/</guid>
  <description>A personal 2023 retrospective covering work, travel, daily life, technology, social networks, and how I want to spend 2024 at my own pace.</description>
  <content:encoded><![CDATA[<p>Several days have already passed since the new year began. I did not write a retrospective at the end of last year because I was traveling in Taiwan, though that is just an excuse.</p>
<p><img src="https://i.imgur.com/Vye097U.jpg" alt="A New Year-themed illustration in a low-polygon art style, featuring a majestic dragon. The dragon, symbolizing good luck and prosperity, is created with geometric shapes, giving it a modern, digital look. The background includes fireworks and traditional Japanese decorations, all rendered in low-polygon style. The scene is set at night with a polygonal full moon, and the dragon is in vibrant colors like red, gold, and green, capturing the festive spirit of the New Year in a unique, contemporary art form."></p>
<h2>Work</h2>
<p>For a short period from May 2023 until my term ended in December, I served for the second time as an executive officer of a listed company. From my own point of view, my output was not very good, roughly at the level of being better there than not there. I thought continuing like that would not be good for either the company or myself, so I decided to stop when the term ended. I am not going to write publicly about why it was like that, but I did not work freely at my own pace as I did from 2020 to 2022, and that is probably why I did not feel like writing this 2023 retrospective either.</p>
<p>Going forward, I want to live at my own pace and without being swayed.</p>
<h2>Travel</h2>
<p>I went to South America for two months in February and March, and to Taiwan for one week at the end of the year. South America especially felt unknown to me, so I am glad I was able to go while I am alive. I should have more time this year, so I want to travel around Japan and abroad from time to time. Short trips of about a week sound good, and staying for about a month somewhere with a good internet environment also sounds good.</p>
<h2>Life</h2>
<p>I neglected daily life because of work. Even though I live in the countryside, if I do not leave the house, it is no different from city life. At night there is not much entertainment in the countryside, so if I only work during the day, that is really all the day becomes. I regret that.</p>
<h2>Technology</h2>
<p>With the appearance of GPT-4, many things started changing dramatically, not only around data science but across many areas. In software development, GPT-4 is now the first move, and I do not really want to develop in an editor without Copilot. My development style changed a lot and became easier.</p>
<h2>Social networks</h2>
<p>I thought I should do some publicity, so I started using social networks a little again. Basically I do not use them, and only log in when needed for posting. The password is stored somewhere very hard to access, so I only look a little. Even at this distance, social networks still take time away, so I want to keep my distance while using them only slightly.</p>
<h2>2024</h2>
<p>I want to put an end to the way I worked in 2023 and return to the 2020-2022 style: living while doing work, daily life, and the things I want to do. On the work side, I will be involved in a development project that applies recent AI-related technologies in society and has enough potential to become a product that makes people take notice. Whether it can be built depends to some extent on my own development ability, so I would be happy if, in next year's retrospective, I can say that I built that thing.</p>
<p>On the technology side, machine learning, data science, and surrounding technologies are still too interesting in every area, so I want to keep learning. I also want to win at least one Kaggle gold medal. In 2023 I joined only one competition and got one silver. Travel and daily life are as written above.</p>
<p>This year I want to do things at my own pace, enjoy myself, and stay healthy. Also, as always, I am grateful to my wife for supporting me in many ways.</p>
<p>With that, I look forward to another year with everyone.</p>
<ul>
<li><a href="https://secon.dev/entry/2022/12/31/070000/">Looking back on 2022</a></li>
<li><a href="https://secon.dev/entry/2021/12/31/070000/">Looking back on 2021</a></li>
<li><a href="https://secon.dev/entry/2020/12/31/080000-hurikaeri-2020/">Looking back on 2020</a></li>
</ul>]]></content:encoded>
  <pubDate>Mon, 08 Jan 2024 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 08 Jan 2024 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Solving the first AI-Ou quiz competition with vector search only</title>
  <link>https://hotchpotch.dev/articles/vector-search-ai-ou-comp/</link>
  <guid>https://hotchpotch.dev/articles/vector-search-ai-ou-comp/</guid>
  <description>I tried solving the first AI-Ou Japanese quiz competition using only vector search over Japanese Wikipedia passages, and compared several Japanese embedding models on a Q&amp;A retrieval task.</description>
  <content:encoded><![CDATA[<p>This article was written for day 21 of the <a href="https://qiita.com/advent-calendar/2023/kaggle">Kaggle Advent Calendar 2023</a>.</p>
<hr>
<p>With the appearance of LLMs that can handle long token contexts, I feel that Retrieval-Augmented Generation, or RAG, has become increasingly important as a way to improve LLM output accuracy. For example, in the Kaggle competition <a href="https://www.kaggle.com/competitions/kaggle-llm-science-exam">LLM Science Exam</a>, all top solutions used RAG. One core element of RAG is a search method that retrieves text well representing the target text, such as a question. Major approaches include keyword-based search such as BM25 and vector search using sentence features, or embeddings.</p>
<p>In this article, I use only Japanese vector search to solve the task from the already-finished <a href="https://sites.google.com/view/project-aio/competition1?authuser=0">AI-Ou: Quiz AI Japan Championship, First Competition</a>, and check what score can be achieved. I also evaluate several Japanese embedding conversion models.</p>
<p><img src="https://i.imgur.com/qYwHz4l.jpg" alt="A wide image depicting an origami-crafted anime-style scene, with a color palette based primarily on light blue tones. It features a round robot, crafted in origami style, answering a quiz and holding a stick with the answer. The atmosphere is cute and fun. The quiz is about vectors, so vector arrows are displayed in the scene, complementing the light blue color scheme."></p>
<h2>What is the first AI-Ou competition?</h2>
<p>The first AI-Ou, or Quiz AI Japan Championship, competition asks systems to choose one correct answer from about 20 candidates for each question. About 13,000 examples were published for training and about 2,000 for validation. The quiz answers are always included in Japanese Wikipedia. A dataset example looks like this:</p>
<pre><code>## 質問
1868年に化石が発見された南フランスの地名から名が付いた、現在の人類の直接的な祖先とされる化石人類は何でしょう?

## 回答候補
['ホモ・ハイデルベルゲンシス', 'ホモ・サピエンス・イダルトゥ', 'クロマニョン人', 'ホモ・エルガステル', 'ジャワ原人', 'オロリン', 'サヘラントロ プス', 'アウストラロピテクス・アフリカヌス', 'ホモ・アンテセッサー', '猿人', 'ネアンデルタール人', 'ホモ・ ゲオルギクス', 'ホモ・エレクトス', '元謀原人', 'アウストラロピテクス', 'ホモ・フローレシエンシス', 'ホモ・ローデシエンシス', 'アウストラロピテクス・アファレンシス', 'ホモ・サピエンス', 'ホモ・ハビリス']

## 正解
クロマニョン人
</code></pre>
<h2>Predicting the answer with only vector search</h2>
<p>Because the quiz answers are always included in Japanese Wikipedia, I convert the question text into embeddings, search over embeddings of Japanese Wikipedia passages, and extract the top-N passages and Wikipedia titles with high similarity. Then I search that text for the first occurrence position of each of the 20 candidate answer strings, and predict the answer whose first occurrence is earliest. For Wikipedia search, I use the roughly 5.5 million passage dataset from <a href="/articles/wikipedia-ja-embeddings">Building Japanese Wikipedia embeddings and a FAISS index for RAG</a>.</p>
<p>For example, for the question above, I convert "1868年に化石が発見された南フランスの地名から名が付いた、現在の人類の直接的な祖先とされる化石人類は何でしょう?" into embeddings, retrieve top-N by vector search, and concatenate the results into one text. The example below uses top-3.</p>
<pre><code>南アフリカの人類化石遺跡群 クロマニョン人 化石人類 そのため、180万年前から150万年前と推測されるその時期、東アフリカではヒト属が優勢になっていたのに対し、南アフリカで優勢だったのはパラントロプス属の方だったのだろうと考えられている。グラディスヴェール はスタルクフォンテインから8 km ほどの場所にある遺跡で、1948年には探索が行われていたが、化石人骨の出土は1992年になってのことだった。この地で調査に当たっていた古人類学者リー・バーガー(英語版)は、アウストラロピテクス・アフリカヌスの断片を見つけるにとど まっていたという。しかし、バーガーは2008年8月にヨハネスブルグからグラディスヴェールに向かう大きな道を数 km 手前で脇に逸れ、グーグル・アースで見当をつけていた近隣の石灰石採掘場跡に赴いた。その場所で彼は9歳の息子マシューとともに、新種の猿人化石を発見した。 クロマニョン人(クロマニョンじん、Cro-Magnon man)とは、南フランスで発見された人類化石に付けられた名称である。1868年、クロマニョン (Cro-Magnon) 洞窟で、鉄道工事に際して5体の人骨化石が出土し、古生物学者ルイ・ラルテ(フランス語版、英語版)によって研究された。その後、ヨーロッパ、北アフリカ各地でも発見された。現在ではクロマニョン人を、現世人類と合わせて解剖学的現代人(英語: anatomically modern human) (AMH) と呼ぶことがある。またネアンデルタール人を、従来の日本語では旧人と呼ぶのに対し(ネアンデルター ル人以外にも、25万年前に新人段階に達する前の、現代型サピエンスの直接の祖先である古代型サピエンス等も旧人段階の人類とみなすこ とがある)、クロマニョン人に代表される現代型ホモ・サピエンスを、従来の日本語では新人と呼ぶこともある。 化石人類(かせきじんるい、英語: fossil hominidまたはfossil man)は、現在ではすでに化石化してその人骨が発見される過去の人類。人類の進化を考察していくうえで重要な化石資料となる。資料そのものは化石人骨(かせきじんこつ)とも称する。また、主に第四紀更新世(洪積世)の地層で発見される ので更新世人類ないし洪積世人類とも称する。
</code></pre>
<p>From this text, I search for the answer candidates above and choose the one that appears first. The candidates include <code>'ホモ・ハイデルベルゲンシス', 'ホモ・サピエンス・イダルトゥ', 'クロマニョン人', ...</code>, so the predicted answer is "クロマニョン人". The true answer is also "クロマニョン人", so this case is correct.</p>
<h2>Japanese embedding models and accuracy</h2>
<p>For data, I used the roughly 2,000 validation examples, dev1 and dev2, provided by the first AI-Ou competition, and evaluated with accuracy. As Japanese embedding conversion models, I used:</p>
<ul>
<li><a href="https://huggingface.co/intfloat/multilingual-e5-small">intfloat/multilingual-e5-small</a></li>
<li><a href="https://huggingface.co/intfloat/multilingual-e5-base">intfloat/multilingual-e5-base</a></li>
<li><a href="https://huggingface.co/intfloat/multilingual-e5-large">intfloat/multilingual-e5-large</a></li>
<li><a href="https://huggingface.co/pkshatech/GLuCoSE-base-ja">pkshatech/GLuCoSE-base-ja</a></li>
<li><a href="https://huggingface.co/cl-nagoya/sup-simcse-ja-base">cl-nagoya/sup-simcse-ja-base</a></li>
</ul>
<p>For the e5 series, different embeddings are generated by adding <code>passage:</code> to the original text for retrieval, or <code>query:</code> otherwise, so I tried both. Also, because search uses a FAISS IVFPQ-compressed index, based on the values from <a href="/articles/vector-search-params">Measuring speed, data size, and accuracy for vector search algorithms and quantization parameters</a>, answer accuracy may fluctuate by about plus or minus 2% for top-3 and plus or minus 0.5% for top-5 compared with an uncompressed index.</p>
<p>The results are below. As a reference, scores for the 13,000 training examples are also written in a separate sheet. <code>acc@N</code> is accuracy calculated from top-N data, and <code>NMR@N</code> is the no match rate, where none of the 20 answer candidate keywords were found from top-N.</p>
<ul>
<li>Evaluation code
<ul>
<li><a href="https://github.com/hotchpotch/wikipedia-passages-jawiki-embeddings-utils/blob/main/eval_jaqket_v1/eval_jacket_v1.py">https://github.com/hotchpotch/wikipedia-passages-jawiki-embeddings-utils/blob/main/eval_jaqket_v1/eval_jacket_v1.py</a></li>
</ul>
</li>
<li>Score summary
<ul>
<li><a href="https://docs.google.com/spreadsheets/d/1eSYzxzIfN3uMIpFKDGCTQsIxuWYELBtD49LQbl88GUE/edit?usp=sharing">https://docs.google.com/spreadsheets/d/1eSYzxzIfN3uMIpFKDGCTQsIxuWYELBtD49LQbl88GUE/edit?usp=sharing</a></li>
</ul>
</li>
</ul>
<p><img src="https://i.imgur.com/v6p5kkw.png" alt=""></p>
<p>For top-1, 3, 5, 10, 20, and so on, <code>multilingual-e5-large</code> wins decisively. I had assumed, with some bias, that <code>multilingual-e5-small</code> and <code>multilingual-e5-large</code> might not differ that much. But a 7% difference in accuracy between small and large changes the picture a lot. Another surprise was that for e5 embeddings, the difference between using <code>passage:</code> and <code>query:</code> as the prefix was almost nonexistent except for base. In fact, outside base, the results were almost reversed. For retrieval tasks that retrieve answer text for a question, I expected <code>passage:</code> to score better, but that was not the case here. Looking only at this result, it feels like for RAG search with e5 embeddings, using the more general <code>query:</code> prefix, which also works for similar sentence tasks, may be fine.</p>
<p>The reason many results are worse at acc@100 than acc@10 is the keyword search order. I concatenate text in the order <code>title@1, title@2, ..., title@N, passage@1, passage@2</code>, so as N grows, the chance of matching a wrong title increases.</p>
<h2>Difference from top competition teams</h2>
<p>According to <a href="https://www.ai-shift.co.jp/techblog/1781">the AI-Ou retrospective</a>, top teams in the competition, including the LB first-place team, had CV scores over 0.95 on the dev dataset. Even the best e5-large score of <code>0.7791</code> is nowhere close.</p>
<p>Still, for only vector search and simple string search with no training, preprocessing, or postprocessing, I think the score is fairly good. I cannot confirm it now, but the originally published BERT fine-tuning baseline was reportedly around 0.8. If no-training search reaches 0.78, that does not feel bad.</p>
<h1>Closing</h1>
<p>This time I tried the first AI-Ou quiz competition using only vector search. In question-answering systems and similar applications, techniques that insert knowledge an LLM does not have and produce desired output through RAG + in-context learning will probably continue until LLMs can cheaply add external knowledge through training and hallucinations are almost eliminated. In Kaggle too, RAG and in-context learning may appear again in NLP tasks.</p>
<p>For this competition task, where the goal is to search for likely answer-containing Japanese sentences for a question, <code>multilingual-e5-large</code> performed well as an embedding conversion model. But for similar sentence search tasks, other models may perform better, as shown by <a href="https://github.com/oshizo/JapaneseEmbeddingEval">JSTS, JSICK, and related evaluations</a>. It seems necessary to evaluate performance appropriately for the task and data you want to handle.</p>
<p>I hope this article helps with Kaggle tasks or with using and choosing Japanese embeddings.</p>]]></content:encoded>
  <pubDate>Thu, 21 Dec 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Thu, 21 Dec 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Training a Q&amp;A + RAG-focused LLM with SFT, making 4-bit quantized models, and exceeding GPT-3.5 with a 7B model</title>
  <link>https://hotchpotch.dev/articles/qa-rag-llm-sft/</link>
  <guid>https://hotchpotch.dev/articles/qa-rag-llm-sft/</guid>
  <description>I fine-tuned rinna&apos;s youri-7b-instruction with SFT for Japanese Q&amp;A over RAG context, quantized it with 4-bit methods, and compared exact match, partial match, speed, and GPU memory against GPT-3.5 and GPT-4.</description>
  <content:encoded><![CDATA[<p>This article was written for December 15 of the <a href="https://qiita.com/advent-calendar/2023/llm">LLM Advent Calendar 2023</a>.</p>
<hr>
<p>Recently I wrote <a href="/articles/wikipedia-ja-embeddings">Building Japanese Wikipedia embeddings and a FAISS index for RAG</a>, where I used GPT-3.5 or GPT-4 to extract answers to questions. Since I had the data, I wanted to avoid using a huge LLM such as OpenAI's models and instead train a rapidly improving local LLM with <a href="https://huggingface.co/docs/trl/main/en/sft_trainer">Supervised Fine-tuning Trainer, or SFT</a>, making an LLM specialized for Q&#x26;A + RAG tasks. As the base LLM, I used <a href="https://rinna.co.jp/news/2023/10/20231031.html">youri7b-instruction</a>, published by rinna.</p>
<p>For example, if I give the trained model an input like this:</p>
<pre><code>以下は、タスクを説明する指示と、文脈のある入力の組み合わせです。要求を適切に満たす応答を書きなさい。

### 指示:
楽曲『約束はいらない』でデビューした、声優は誰?

### 入力:
Suara 大阪府立豊中高等学校を経て大阪外国語大学でインドネシア語を専攻中にバンド・ユニットを組んで音楽活動を始めた。普段はお笑い番組が大好きなこともあってよく喋るほうだが、東京の仕事で標準語の喋りをする時は、
早見沙織 声優デビュー時より、数多くの主題歌やキャラクターソングを担当し、バラードからポップス、ヒットソングのカバー曲や英語の楽曲など、様々な曲を歌いこなす。2009年には吉田仁美とのユニット「blue dro
約束はいらない 「約束はいらない」(やくそくはいらない)は、坂本真綾のデビューシングル。
約束はいらない 坂本真綾の歌手デビュー作品。当時坂本はまだ無名の声優であったが、同曲がテーマソングとなったアニメ『天空のエスカフローネ』とともに知名度を上げることとなる。後に「指輪」が同アニメの劇場版映画の主題歌とな
坂本真綾 本格的な歌手活動は、1996年にテレビアニメ『天空のエスカフローネ』へ出演したことがきっかけで始めており、同作のオープニングテーマソングである「約束はいらない」(岩里祐穂作詞、菅野よう子作曲)をシング

### 応答:
</code></pre>
<p>It outputs only the answer to the question:</p>
<pre><code>坂本真綾
</code></pre>
<p>I also created a <a href="https://colab.research.google.com/drive/1mZfAPcKJzAlOe2Cb1qq692KoGjOv3LH7?usp=sharing">notebook that runs on a Google Colab T4 GPU</a>, so please try it if you are interested.</p>
<h2>Performance comparison with GPT-3.5</h2>
<p>Let's compare the model I made and its quantized versions with GPT-3.5 and GPT-4. For the comparison dataset, I used 980 validation examples from <a href="https://huggingface.co/datasets/hotchpotch/jaqket_v1_qa_wikija_context">hotchpotch/jaqket_v1_qa_wikija_context</a>. For questions that have context containing the answer, I evaluated whether the answer could be extracted correctly using exact match and partial match accuracy.</p>
<p>The results are below. After training, every model exceeded GPT-3.5 accuracy by a large margin. Execution time was also faster, especially for the AutoGPTQ quantized model, which was about twice as fast. I discuss the numbers later in the article.</p>
<p><img src="https://i.imgur.com/vkxQZld.png" alt=""></p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Exact match</th>
<th>Partial match</th>
<th>Time</th>
<th>GPU memory (MB)</th>
</tr>
</thead>
<tbody>
<tr>
<td>GPT3.5</td>
<td>0.5949</td>
<td>0.799</td>
<td>405</td>
<td></td>
</tr>
<tr>
<td>GPT4.0</td>
<td>0.8786</td>
<td>0.9173</td>
<td>1152</td>
<td></td>
</tr>
<tr>
<td>fp16 before training</td>
<td>0.5908</td>
<td>0.7327</td>
<td>4218</td>
<td>11122</td>
</tr>
<tr>
<td>fp16 after training</td>
<td>0.7582</td>
<td>0.8939</td>
<td>4146</td>
<td>9964</td>
</tr>
<tr>
<td>BnB 4bit</td>
<td>0.7602</td>
<td>0.8867</td>
<td>397</td>
<td>3774</td>
</tr>
<tr>
<td>AutoGPTQ</td>
<td>0.7969</td>
<td>0.8867</td>
<td>211</td>
<td>4695</td>
</tr>
<tr>
<td>AutoAWQ</td>
<td>0.7316</td>
<td>0.8847</td>
<td>301</td>
<td>5933</td>
</tr>
</tbody>
</table>
<p>The evaluation code is in the <code>eval_xxx</code> files here:</p>
<ul>
<li><a href="https://github.com/hotchpotch/youri-7b-stf-qa-context-jaqket/">https://github.com/hotchpotch/youri-7b-stf-qa-context-jaqket/</a></li>
</ul>
<h2>Training with Supervised Fine-tuning Trainer</h2>
<p>SFT is an easy way to train a model to output a specific format in response to instructions, or instruction tuning. The way to train is simple. Prepare examples like this:</p>
<pre><code>### 指示:
今日の天気は何ですか？

### 入力:
本日は大雨ですね。

### 応答:
大雨
</code></pre>
<p>In this example, the data after <code>### 応答:</code> is what we want the model to output well. If you provide the example sentence and <code>### 応答:</code>, it trains appropriately. During actual training, the model predicts after <code>応答:</code> and learns the token probability score, or cross entropy loss, for the desired answer. In other words, once examples can be created, this is an easy training method that handles the rest nicely. I have heard that around 1000 examples can be enough to train reasonably well, though a citation is needed.</p>
<h3>Training dataset</h3>
<p>For training, I used 2939 train examples from <a href="https://huggingface.co/datasets/hotchpotch/jaqket_v1_qa_wikija_context">jaqket_v1_qa_wikija_context</a>. This dataset extracts the CC BY-SA 4.0 DEED licensed portion of the AI Quiz King dataset and adds context usable for RAG.</p>
<h3>Training</h3>
<p>Training was done with this implementation. On an RTX 4090, one epoch, or 91 steps, took a little over two hours.</p>
<ul>
<li><a href="https://github.com/hotchpotch/youri-7b-sft-qa-context-jaqket/blob/main/train.py">https://github.com/hotchpotch/youri-7b-sft-qa-context-jaqket/blob/main/train.py</a></li>
</ul>
<p>I omit the details here, but the training loads youri7b-instruction with BnB 4-bit quantization and FlashAttention 2, then trains with LoRA. I also used <a href="https://github.com/neelsjain/NEFTune">NEFTune</a> to improve performance.</p>
<h3>Looking at training results</h3>
<p>The training process is recorded in this wandb run:</p>
<ul>
<li><a href="https://wandb.ai/hotchpotch/youri-7b-stf-qa-context-jaqket/runs/beelrst5/workspace">https://wandb.ai/hotchpotch/youri-7b-stf-qa-context-jaqket/runs/beelrst5/workspace</a></li>
</ul>
<p>Train loss flattened fairly quickly, and eval loss stopped decreasing around 40% of the training. Forty percent means around 1200 training examples, so the claim that around 1000 examples can train reasonably well feels fairly plausible.</p>
<p><img src="https://i.imgur.com/tgCNePk.png" alt="loss"></p>
<p>Let's also look at wrong results at the end of training where exact match did not match. wandb is convenient because it can display dataframes as tables.</p>
<p><img src="https://i.imgur.com/p4fvWj7.png" alt="Wrong data"></p>
<p>Many results were close: an extra <code>』</code> at the end, or differences between full-width <code>＝</code> and half-width <code>=</code>.</p>
<h1>Model quantization</h1>
<p>As of December 2023, according to <a href="https://huggingface.co/docs/transformers/main_classes/quantization">Quantize 🤗 Transformers models</a>, Hugging Face Transformers lists the following three quantization methods as easy to use from Python:</p>
<ul>
<li>AWQ
<ul>
<li>Implementation of <a href="https://arxiv.org/abs/2306.00978">Activation-aware Weight Quantization for LLM Compression and Acceleration</a></li>
</ul>
</li>
<li>GPTQ
<ul>
<li>Implementation of <a href="https://arxiv.org/abs/2210.17323">GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers</a></li>
</ul>
</li>
<li>BnB (bitsandbytes)
<ul>
<li>Implementation related to <a href="https://arxiv.org/abs/2110.02861">8-bit Optimizers via Block-wise Quantization</a> and others</li>
</ul>
</li>
</ul>
<p>BnB is a relatively older quantization method, and I also used it during training. GPTQ appeared in 2022, and AWQ appeared in 2023. This time I quantized with each method at 4 bits and evaluated on the validation data of <code>jaqket_v1_qa_wikija_context</code>. For AWQ and GPTQ, I provided Wikipedia text and training data as calibration sample text during quantization so that quantization would be better.</p>
<p>The result is the same as the table shown earlier. The measurement environment is CPU Ryzen 9 5950X and GPU RTX 4090. Every model exceeded GPT-3.5 on partial match and exact match, and all quantized models were faster. AutoGPTQ was about twice as fast as GPT-3.5. Comparing the quantized models, it is understandable that non-quantized fp16 had the best exact match, but unexpectedly, AutoGPTQ had the best partial match and exceeded fp16. Because I passed training data as samples during AutoGPTQ quantization, that bias may have made the result better than fp16. Against GPT-4.0, the models lose clearly on accuracy, which is unavoidable.</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Exact match</th>
<th>Partial match</th>
<th>Time</th>
<th>GPU memory (MB)</th>
</tr>
</thead>
<tbody>
<tr>
<td>GPT3.5</td>
<td>0.5949</td>
<td>0.799</td>
<td>405</td>
<td></td>
</tr>
<tr>
<td>GPT4.0</td>
<td>0.8786</td>
<td>0.9173</td>
<td>1152</td>
<td></td>
</tr>
<tr>
<td>fp16 before training</td>
<td>0.5908</td>
<td>0.7327</td>
<td>4218</td>
<td>11122</td>
</tr>
<tr>
<td>fp16 after training</td>
<td>0.7582</td>
<td>0.8939</td>
<td>4146</td>
<td>9964</td>
</tr>
<tr>
<td>BnB 4bit</td>
<td>0.7602</td>
<td>0.8867</td>
<td>397</td>
<td>3774</td>
</tr>
<tr>
<td>AutoGPTQ</td>
<td>0.7969</td>
<td>0.8867</td>
<td>211</td>
<td>4695</td>
</tr>
<tr>
<td>AutoAWQ</td>
<td>0.7316</td>
<td>0.8847</td>
<td>301</td>
<td>5933</td>
</tr>
</tbody>
</table>
<p>I did not tune GPTQ, AWQ, or BnB in detail, so results may differ with better optimization. For example, AWQ can speed up inference by changing the algorithm version depending on token length and batch size for the use case. Also, GPU memory is the memory at model load time, and inference probably uses more GPU memory.</p>
<h1>Closing</h1>
<p>This time I used easy SFT training to fine-tune a 7B local LLM so that it can answer appropriately for Q&#x26;A + RAG tasks. As a result, although general ability was lost, the quantized models achieved better speed and accuracy than GPT-3.5 on a home machine. With SFT, if you have around 1000 training examples, it seems possible to make a model follow many output formats, and in that case training may take less than one hour on an RTX 4090. That makes training LLMs specialized for specific uses feel casual.</p>
<p>Local LLM performance will continue improving, and smaller high-performance local LLMs such as <a href="https://github.com/jzhang38/TinyLlama">TinyLlama-1.1B</a> will likely continue to be developed. I look forward to local LLM progress next year.</p>
<h2>Implementations, notebooks, and public models used for training and inference</h2>
<ul>
<li><a href="https://github.com/hotchpotch/youri-7b-sft-qa-context-jaqket/">https://github.com/hotchpotch/youri-7b-sft-qa-context-jaqket/</a></li>
<li><a href="https://huggingface.co/hotchpotch/youri-7b-sft-qa-context-jaqket-gptq">hotchpotch/youri-7b-sft-qa-context-jaqket-gptq</a>
<ul>
<li>Model quantized with AutoGPTQ</li>
</ul>
</li>
<li><a href="https://huggingface.co/hotchpotch/youri-7b-sft-qa-context-jaqket-awq">hotchpotch/youri-7b-sft-qa-context-jaqket-awq</a>
<ul>
<li>Model quantized with AutoAWQ</li>
</ul>
</li>
</ul>]]></content:encoded>
  <pubDate>Fri, 15 Dec 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Fri, 15 Dec 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Building Japanese Wikipedia embeddings and a FAISS index for RAG</title>
  <link>https://hotchpotch.dev/articles/wikipedia-ja-embeddings/</link>
  <guid>https://hotchpotch.dev/articles/wikipedia-ja-embeddings/</guid>
  <description>I created embeddings for about 5.5 million Japanese Wikipedia passages and published FAISS indexes that can be used easily for RAG-style retrieval and question answering experiments.</description>
  <content:encoded><![CDATA[<p>This article was written for December 4 of the <a href="https://adventar.org/calendars/8678">Information Retrieval and Search Technology Advent Calendar 2023</a>.</p>
<hr>
<p>With the recent rise of LLMs, I feel that RAG, or Retrieval-Augmented Generation, has become increasingly important as a way to improve LLM outputs by using external information as in-context learning. But when building a system with RAG, it is not very interesting if the data is too small. So I created embeddings for about 5.5 million Japanese Wikipedia passages that can be searched easily and used as RAG input data, along with FAISS indexes for fast vector search.</p>
<p>For example, you can search for relevant Wikipedia sentences like this:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> datasets.download <span class="hljs-keyword">import</span> DownloadManager
<span class="hljs-keyword">from</span> datasets <span class="hljs-keyword">import</span> load_dataset
<span class="hljs-keyword">from</span> sentence_transformers <span class="hljs-keyword">import</span> SentenceTransformer
<span class="hljs-keyword">import</span> faiss

<span class="hljs-comment"># wikipedia 日本語データセットのロード</span>
wikija_dataset = load_dataset(
    path=<span class="hljs-string">"singletongue/wikipedia-utils"</span>,
    name=<span class="hljs-string">"passages-c400-jawiki-20230403"</span>,
    split=<span class="hljs-string">"train"</span>,
)
<span class="hljs-comment"># faiss index のダウンロード</span>
dm = DownloadManager()
index_local_path = dm.download(
    <span class="hljs-string">f"https://huggingface.co/datasets/hotchpotch/wikipedia-passages-jawiki-embeddings/resolve/main/faiss_indexes/passages-c400-jawiki-20230403/multilingual-e5-small-passage/index_IVF2048_PQ96.faiss"</span>
)
<span class="hljs-comment"># faiss index のロード</span>
faiss_index = faiss.read_index(index_local_path)

<span class="hljs-comment"># embeddings へ変換するモデルのロード</span>
model = SentenceTransformer(<span class="hljs-string">"intfloat/multilingual-e5-small"</span>)
model.max_seq_length = <span class="hljs-number">512</span>

<span class="hljs-comment"># embeddings へ変換</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">to_emb</span>(<span class="hljs-params">model, text, prefix=<span class="hljs-string">"query: "</span></span>):
    <span class="hljs-keyword">return</span> model.encode([prefix + text], normalize_embeddings=<span class="hljs-literal">True</span>)

emb = to_emb(model, <span class="hljs-string">"楽曲『約束はいらない』でデビューした、声優は誰?"</span>)

<span class="hljs-comment"># faiss で検索して、関連 Top-5 を取り出す</span>
TOP_K = <span class="hljs-number">5</span>
scores, indexes = faiss_index.search(emb, TOP_K)
<span class="hljs-keyword">for</span> idx, (<span class="hljs-built_in">id</span>, score) <span class="hljs-keyword">in</span> <span class="hljs-built_in">enumerate</span>(<span class="hljs-built_in">zip</span>(indexes[<span class="hljs-number">0</span>], scores[<span class="hljs-number">0</span>])):
    data = wikija_dataset[<span class="hljs-built_in">int</span>(<span class="hljs-built_in">id</span>)]
    <span class="hljs-built_in">print</span>((score, data[<span class="hljs-string">"title"</span>], data[<span class="hljs-string">"text"</span>][:<span class="hljs-number">100</span>]))
</code></pre>
<p>The result is below. For the question asking "who?", the appropriate person's name appears in the top 3.</p>
<pre><code>(0.21018645, '約束はいらない', '「約束はいらない」(やくそくはいらない)は、坂本真綾のデビューシングル。')
(0.24241784, '約束はいらない', '坂本真綾の歌手デビュー作品。当時坂本はまだ無名の声優であったが、同曲がテーマソングとなったアニメ『天空のエスカフローネ』とともに知名度を上げることとなる。後に「指輪」が同アニメの劇場版映画の主題歌とな')
(0.2441598, '坂本真綾', '本格的な歌手活動は、1996年にテレビアニメ『天空のエスカフローネ』へ出演したことがきっかけで始めており、同作のオープニングテーマソングである「約束はいらない」(岩里祐穂作詞、菅野よう子作曲)をシング')
(0.26043588, 'Suara', '大阪府立豊中高等学校を経て大阪外国語大学でインドネシア語を専攻中にバンド・ユニットを組んで音楽活動を始めた。普段はお笑い番組が大好きなこともあってよく喋るほうだが、東京の仕事で標準語の喋りをする時は、')
(0.2690969, '早見沙織', '声優デビュー時より、数多くの主題歌やキャラクターソングを担当し、バラードからポップス、ヒットソングのカバー曲や英語の楽曲など、様々な曲を歌いこなす。2009年には吉田仁美とのユニット「blue dro')
</code></pre>
<p>Because the full Wikipedia text and the search index can be loaded from Hugging Face datasets, this can be used easily without doing difficult setup. The FAISS index used for search is memory-efficient and fast. For about 5.5 million vectors, it can search quickly on CPU without requiring large compute resources.</p>
<p>I also made a Hugging Face Spaces demo for RAG, so you can try RAG search and QA interaction from a Web UI.</p>
<ul>
<li><a href="https://huggingface.co/spaces/hotchpotch/wikipedia-japanese-rag-qa">https://huggingface.co/spaces/hotchpotch/wikipedia-japanese-rag-qa</a></li>
</ul>
<p><img src="https://i.imgur.com/M3zfxcA.png" alt="Screenshot"></p>
<h2>Using Wikipedia sentences as RAG context</h2>
<p>Next, let's use the answer data for the question "楽曲『約束はいらない』でデビューした、声優は誰？" as in-context learning for RAG. First, asking ChatGPT 3.5 directly produced this result:</p>
<blockquote>
<p>楽曲『約束はいらない』でデビューした、声優は誰？</p>
<p>「約束はいらない」は、声優の悠木碧（ゆうき あおい、英: Aoi Yuki）によって歌われた楽曲です。悠木碧は、日本の声優として知られており、多くのアニメ作品やゲームで声優として活躍しています。この曲は彼女のデビューシングルの一つとして知られています。</p>
</blockquote>
<p>ChatGPT 3.5 gave a wrong answer. Next, I injected the Wikipedia search results above and used them for generation, in other words as RAG search results for in-context learning.</p>
<blockquote>
<p>楽曲『約束はいらない』でデビューした、声優は誰？以下のデータにヒントが含まれる場合は参考にして答えよ。また以下のデータ自体が間違っている場合もあるため、その場合はデータを利用せずに答えよ。</p>
<p>(0.21018645, '約束はいらない', '「約束はいらない」(やくそくはいらない)は、坂本真綾のデビューシングル。')
(0.24241784, '約束はいらない', '坂本真綾の歌手デビュー作品。当時坂本はまだ無名の声優であったが、同曲がテーマソングとなったアニメ『天空のエスカフローネ』とともに知名度を上げることとなる。後に「指輪」が同アニメの劇場版映画の主題歌とな')
(0.2441598, '坂本真綾', '本格的な歌手活動は、1996年にテレビアニメ『天空のエスカフローネ』へ出演したことがきっかけで始めており、同作のオープニングテーマソングである「約束はいらない」(岩里祐穂作詞、菅野よう子作曲)をシング')
(0.26043588, 'Suara', '大阪府立豊中高等学校を経て大阪外国語大学でインドネシア語を専攻中にバンド・ユニットを組んで音楽活動を始めた。普段はお笑い番組が大好きなこともあってよく喋るほうだが、東京の仕事で標準語の喋りをする時は、')
(0.2690969, '早見沙織', '声優デビュー時より、数多くの主題歌やキャラクターソングを担当し、バラードからポップス、ヒットソングのカバー曲や英語の楽曲など、様々な曲を歌いこなす。2009年には吉田仁美とのユニット「blue dro')</p>
<p>提供されたデータによれば、楽曲「約束はいらない」は坂本真綾のデビューシングルであり、坂本真綾が歌手デビューした作品であることが確認できます。したがって、この楽曲でデビューした声優は坂本真綾（さかもと まあや）です。</p>
</blockquote>
<p>This answered correctly. <a href="https://chat.openai.com/share/31090d36-93ff-4e1a-a652-321ebbde662e">The test is here</a>. GPT-4 answered correctly from the start even without adding RAG information.</p>
<h1>How to create Wikipedia sentence and passage data</h1>
<p>There are several ways to obtain Wikipedia data, such as <a href="https://dumps.wikimedia.org/">full dump data</a>, <a href="https://www.mediawiki.org/wiki/Help:CirrusSearch/ja">CirrusSearch dump data</a>, and <a href="https://www.mediawiki.org/wiki/Wikimedia_REST_API">calling the API</a>. Each has advantages and disadvantages. Personally, I think calling the API is the best way to get clean text. However, calling the API for every article is hard, so at first I used the easier CirrusSearch dump data and <a href="https://huggingface.co/datasets/hotchpotch/wikipedia-ja-20231030">split it into chunks for testing</a>.</p>
<p>Then I found <a href="https://huggingface.co/datasets/singletongue/wikipedia-utils">singletongue/wikipedia-utils</a>, created by <a href="https://singletongue.github.io/">Masatoshi Suzuki</a>. Looking inside, it contains clean text data including article titles and section titles. It also publishes passages of useful lengths, such as 200, 300, and 400 characters, so I created embeddings based on that data.</p>
<h1>Which model to use for sentence embeddings</h1>
<p>Embedding models have different performance directions depending on the use case, roughly corresponding to how they were trained. In RAG systems, the most important type is a model strong at retrieval or reranking tasks, trained so that questions and matching passages score highly. The next best direction is a model strong at similar sentence search.</p>
<p>Among models that seemed likely to perform well and could be used freely for Japanese, I converted data into embeddings with the following models. OpenAI's embeddings API, ada-v2, has the advantage of long input token length, but I excluded it because the output dimension is large at 1536 dimensions, the license situation for converted data is unclear, and above all it costs API fees.</p>
<ul>
<li><a href="https://huggingface.co/intfloat/multilingual-e5-small">multilingal-e5</a> series
<ul>
<li>You need to be aware of prefixes: use <code>"passage: "</code> for retrieval passages and <code>"query: "</code> otherwise. It feels quite strong.</li>
</ul>
</li>
<li><a href="https://huggingface.co/pkshatech/GLuCoSE-base-ja">pkshatech/GLuCoSE-base-ja</a></li>
<li><a href="https://huggingface.co/cl-nagoya/sup-simcse-ja-base">cl-nagoya/sup-simcse-ja-base</a></li>
</ul>
<p>I converted the passage-400 data above into embeddings with these models. For e5, using <code>"passage: "</code> or <code>"query: "</code> at the beginning of the target text produces different vectors, so I published embeddings generated with each prefix.</p>
<ul>
<li><a href="https://huggingface.co/datasets/hotchpotch/wikipedia-passages-jawiki-embeddings/tree/main/embs/passages-c400-jawiki-20230403">https://huggingface.co/datasets/hotchpotch/wikipedia-passages-jawiki-embeddings/tree/main/embs/passages-c400-jawiki-20230403</a></li>
</ul>
<p>In addition to embeddings, I published FAISS indexes for search. For index parameters, based on <a href="/articles/vector-search-params">this evaluation</a>, I used IVF <code>nlist=2048</code>, and set the PQ quantization option to one quarter of the vector dimension, such as 96 for 384 dimensions. If you are interested in how Wikipedia sentence embedding search behaves with each model, please try them.</p>
<ul>
<li><a href="https://huggingface.co/datasets/hotchpotch/wikipedia-passages-jawiki-embeddings/tree/main/faiss_indexes/passages-c400-jawiki-20230403">https://huggingface.co/datasets/hotchpotch/wikipedia-passages-jawiki-embeddings/tree/main/faiss_indexes/passages-c400-jawiki-20230403</a></li>
</ul>
<p>The implementation used for conversion is published as <a href="https://github.com/hotchpotch/wikipedia-passages-jawiki-embeddings-utils">hotchpotch/wikipedia-passages-jawiki-embeddings-utils</a>. <a href="https://github.com/hotchpotch/wikipedia-passages-jawiki-embeddings-utils/blob/main/datasets_to_embs.py">datasets_to_embs.py</a> converts datasets to embeddings, and <a href="https://github.com/hotchpotch/wikipedia-passages-jawiki-embeddings-utils/blob/main/embs_to_faiss.py">embs_to_faiss.py</a> creates FAISS indexes from embeddings.</p>
<h1>Closing</h1>
<p>This article introduced and described creating Japanese Wikipedia embeddings and search indexes for RAG that are easy to try in a general way. In this year when LLMs rose rapidly, search use cases are likely to change greatly, and I feel this was the year when search technology started to be rediscovered and redefined. RAG, though this article only touched one element of RAG, vector search, suggests expansion into many search use cases. I look forward to the development and use of search technology next year and beyond.</p>
<hr>
<h2>Reference links</h2>
<ul>
<li><a href="https://github.com/singletongue/wikipedia-utils">Wikipedia-Utils: Preprocessing Wikipedia Texts for NLP</a></li>
<li><a href="https://yag-ays.github.io/project/cirrus/">Using WIKIPEDIA CIRRUSSEARCH dump data</a></li>
<li><a href="https://github.com/attardi/wikiextractor">WikiExtractor</a></li>
<li><a href="https://github.com/oshizo/JapaneseEmbeddingEval">JapaneseEmbeddingEval</a></li>
</ul>]]></content:encoded>
  <pubDate>Mon, 04 Dec 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 04 Dec 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Measuring speed, data size, and accuracy for vector search algorithms and quantization parameters</title>
  <link>https://hotchpotch.dev/articles/vector-search-params/</link>
  <guid>https://hotchpotch.dev/articles/vector-search-params/</guid>
  <description>A benchmark of FAISS vector search settings, including IVF, HNSW, and product quantization, with a focus on recall@1, @3, and @5 for RAG systems where top-N retrieval quality matters.</description>
  <content:encoded><![CDATA[<p>Recently, more use cases convert sentences into feature vectors such as embeddings. When searching for similar vectors, a few thousand vectors usually require almost no special thought. From tens of thousands of vectors, you often use approximate nearest neighbor algorithms such as HNSW to speed up search. From millions of vectors, you often combine optimization techniques such as quantization to keep the data size practical.</p>
<p><img src="https://i.imgur.com/BCEjD7d.jpg" alt="A simplified and wide image representing the concepts of Artificial Neural Networks (ANN) and quantum compression. The image should feature a minimalist design, with clean and clear lines. For the ANN part, depict a few interconnected nodes and simple pathways in a subtle color scheme. For the quantum compression aspect, include basic quantum-inspired symbols and patterns, focusing on a more straightforward and less complex design. The overall composition should be elegant, with a balance between simplicity and the representation of these advanced technological concepts."></p>
<p>These optimizations for similar vector search, such as HNSW, IVF, and quantization, create tradeoffs among search speed, data size, and accuracy. When thinking about optimization strategies under those tradeoffs, I often see recall@10 or recall@100 used as the reported accuracy metric. For example, <a href="https://aws.amazon.com/jp/blogs/big-data/choose-the-k-nn-algorithm-for-your-billion-scale-use-case-with-opensearch/">Choose the k-NN algorithm for your billion-scale use case with OpenSearch</a> evaluates with recall@10, and <a href="https://opensearch.org/blog/byte-quantized-vectors-in-opensearch/">Byte-quantized vectors in OpenSearch</a> uses recall@100.</p>
<p>If search results are combined with information other than embeddings, or if a reranker re-sorts the results, recall@10 or recall@100 may be fine. But when using retrieval for RAG, I do not often put top-10 results into the LLM or Reader in-context. In my own use, top-3 or top-5 is more common. So I evaluated recall@1, @2, @3, and @5 with FAISS, representative algorithms, and quantization, and measured search speed, data size, and accuracy, or recall. I used FAISS as the library. Even outside FAISS, major vector search engines usually implement the main algorithms and quantization methods, and <a href="https://opensearch.org/docs/latest/search-plugins/knn/approximate-knn/">OpenSearch can also use FAISS internally as a vector search engine</a>.</p>
<h2>Algorithms and parameters in vector search databases</h2>
<p>Before measuring, here is a review of algorithms and parameters used when creating indexes. Major examples include IVF and HNSW search algorithms, compression with PQ, or Product Quantization, and each of their parameters. The following descriptions are GPT-4 output with some edits. For nearest neighbor search itself, I recommend reading Professor Matsui's <a href="https://speakerdeck.com/matsui_528/gurahuwoyong-itajin-si-zui-jin-bang-tan-suo-noli-lun-toying-yong">Theory and Applications of Approximate Nearest Neighbor Search Using Graphs</a>, which explains it thoroughly.</p>
<ul>
<li>HNSW (Hierarchical Navigable Small World)
<ul>
<li>Approach: HNSW uses a graph-based approach. Each node, or data point, has links to neighboring nodes, and search is performed efficiently through those links.</li>
<li>Characteristics:</li>
</ul>
</li>
</ul>
<pre><code class="hljs language-text">- Fast search: The graph's hierarchical structure enables very fast approximate nearest neighbor search.
- Dynamic addition: New data points can be added dynamically.
- High accuracy: It provides high accuracy compared with many other approximate algorithms.
- Memory usage: Because of the graph structure, memory usage may be relatively large.
</code></pre>
<ul>
<li>Parameter: M</li>
</ul>
<pre><code class="hljs language-text">- The maximum number of neighboring nodes each data point has.
</code></pre>
<ul>
<li>IVF (Inverted File Index)
<ul>
<li>Approach: IVF divides data into multiple clusters and creates a separate index for each cluster. During search, it identifies the clusters closest to the query and searches only inside those clusters.</li>
<li>Characteristics:</li>
</ul>
</li>
</ul>
<pre><code class="hljs language-text">- Efficient large-scale search: It can make search efficient for large datasets.
- Scalability: It can be applied to large datasets and is scalable.
- Customizable: Many parameters can be customized, such as the number of clusters, nlist, and the quantization level.
- Memory usage: It is often more memory-efficient than HNSW, though this depends on the number of clusters and quantization level.
</code></pre>
<ul>
<li>Parameter: nlist</li>
</ul>
<pre><code class="hljs language-text">- The number of clusters.
</code></pre>
<ul>
<li>Comparison of HNSW and IVF
<ul>
<li>Accuracy: HNSW generally provides higher accuracy than IVF, but memory usage tends to increase.</li>
<li>Speed: HNSW enables fast search, while IVF is more scalable and memory-efficient for large datasets.</li>
<li>Use cases: HNSW is suitable when accuracy is important or for realtime search. IVF is suitable for large datasets or limited memory resources.</li>
</ul>
</li>
<li>Product Quantization (PQ)
<ul>
<li>Product Quantization is a technique for efficiently compressing high-dimensional vectors. It includes the following steps:</li>
</ul>
</li>
</ul>
<pre><code class="hljs language-text">- Vector splitting: Each vector is split into multiple lower-dimensional subvectors.
- Subvector quantization: Each subvector is quantized using a small separate codebook, or predefined set of values. Each subvector is mapped to the nearest value in that codebook.
- Compressed representation: Finally, the original vector is represented as a combination of these quantized subvectors.
</code></pre>
<p>The main parameter for HNSW is <code>M</code>; for IVF it is <code>nlist</code>, and also <code>mbit</code>, though that did not appear above. When using PQ, the parameter is the number of subvectors. The number of subvectors must divide the original vector dimension. For example, if the original dimension is 384, possible values include 32, 64, and 96. These parameters are needed before training, but the number of graph nodes or clusters searched can be decided at search execution time with parameters such as <code>efSearch</code> for HNSW and <code>nprobe</code> for IVF.</p>
<p>In a FAISS + Python environment, <code>index_factory()</code> lets you create indexes from strings like this, which is convenient:</p>
<pre><code>faiss.index_factory(d, "IVF2048,PQ64") # nlist = 2048, PQ = 64
faiss.index_factory(d, "HNSW32,PQ64") # M = 32, PQ = 64
</code></pre>
<h2>Dataset and code</h2>
<p>This time I used <a href="http://corpus-texmex.irisa.fr/">ANN_SIFT1M</a>, which is often used in FAISS code. As the name says, it is a dataset of 1M, or one million, 128-dimensional vectors. I used 10,000 search queries from it and measured recall@N. FAISS can also use GPU for search, but most searches will probably run on CPU, so I used a CPU, Ryzen 9 5950X.</p>
<p>The benchmark code is <a href="https://gist.github.com/hotchpotch/57d508beb09bbf1e18bee79293ac2ee5">bench_gpu_sift1m_ivf_hnsw.py</a>. If you put this source file into the <a href="https://github.com/facebookresearch/faiss/tree/main/benchs">benchs directory of a cloned FAISS repository</a>, place the dataset appropriately, and run it, it should reproduce the benchmark.</p>
<h1>Measurement results</h1>
<ul>
<li><a href="https://docs.google.com/spreadsheets/d/1ZsMJZf-4tgKgfSa4zvLcmZrBZpoy6Op_EOgY17GPOfY/edit?usp=sharing">https://docs.google.com/spreadsheets/d/1ZsMJZf-4tgKgfSa4zvLcmZrBZpoy6Op_EOgY17GPOfY/edit?usp=sharing</a></li>
</ul>
<p><img src="https://i.imgur.com/xdPO4Im.png" alt="Results"></p>
<p>The result spreadsheet has two sheets. Showing everything would be too large, so one sheet contains extracted data.</p>
<p>As described at the beginning, IVF, HNSW, and PQ, or quantization, show tradeoffs. IVF is outstandingly memory-efficient with small data size, but its speed and accuracy are worse than HNSW. However, once the data no longer fits in memory, HNSW speed will likely degrade too. The recall@1 to recall@100 values are interesting. Recall@100 and recall@10 approach 1.0 fairly quickly, but if recall@3 or recall@5, meaning whether the same data appears in the top 3 or 5 results, is important for your system, you need to choose parameters carefully. Also, when using PQ, the probability that top-1 matches under recall@1 can be fairly low. If top-1 is important, you need to think carefully about what to do.</p>
<h1>Summary</h1>
<p>For systems where top-3 or top-5 matters in RAG, judging that everything is fine based on a larger recall metric without using a reranker may diverge from the results you actually want. The point is obvious, but choose metrics based on what you want to do, and set optimal parameters that fit those metrics. These results are only for the SIFT1M dataset, so different datasets should produce different results.</p>
<p>There is no best parameter that works without thought. But for vectors beyond one million, especially if they may reach tens of millions, I feel that IVF + quantization is a good direction. <code>nlist</code> around 1024 seems good because it is near the square root of the actual count, so for ten million items something like 4096 may be appropriate. PQ should be as large as possible; for example, for 128-dimensional vectors, 64 seems good. If accuracy and speed are important and the scale is up to a few million vectors, HNSW + PQ can also be handled with a practical data size, so it should be considered too.</p>
<p>The <a href="https://github.com/facebookresearch/faiss/tree/main/benchs">FAISS benchs directory</a> includes measurements beyond IVF and HNSW, such as a <a href="https://github.com/facebookresearch/faiss/blob/main/benchs/bench_all_ivf/bench_kmeans.py">benchmark for dimensionality reduction with PCA</a>. It contains quite a few measurements for things you may want to do in vector search, so it is worth a look.</p>
<h2>Extra: a Kaggle competition mistake</h2>
<p>In the already-finished Kaggle competition <a href="https://www.kaggle.com/competitions/kaggle-llm-science-exam">LLM Science Exam</a>, many solutions improved scores by putting top 1 to 5 RAG results into the prompt. I did that too. At the time, I chose FAISS parameters fairly casually, so the data size became quite small, but I did not notice that accuracy had actually dropped a fair amount. I would like to tell my past self to measure properly and think about parameters when accuracy matters.</p>]]></content:encoded>
  <pubDate>Mon, 27 Nov 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 27 Nov 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Making Transformers inference 1.6 to almost 2 times faster with CTranslate2</title>
  <link>https://hotchpotch.dev/articles/ctranslate2-embeddings/</link>
  <guid>https://hotchpotch.dev/articles/ctranslate2-embeddings/</guid>
  <description>I tried CTranslate2 through hf_hub_ctranslate2 for SentenceTransformer-style embedding inference and found it easy to get about 1.6x faster GPU inference and 1.9x faster CPU inference with almost no accuracy change.</description>
  <content:encoded><![CDATA[<p>There is a fast inference library written in Python and C++ called <a href="https://github.com/OpenNMT/CTranslate2">CTranslate2</a>. I had wanted to try it someday, but because it required converting models, I had put it off. Then I learned about <a href="https://github.com/michaelfeil/hf-hub-ctranslate2">hf_hub_ctranslate2</a>, a library that transparently converts Hugging Face models into a format usable with CTranslate2 and runs inference with them. I tried it and very easily got 1.6x faster inference on GPU and 1.9x faster inference on CPU, with almost no change in accuracy. I should have used it earlier, so here is a note.</p>
<p><img src="https://i.imgur.com/jtDuZ4W.jpg" alt="fast GPU"></p>
<h2>What is CTranslate2?</h2>
<p>CTranslate2, or CT2 below, is, as the <a href="https://github.com/OpenNMT/CTranslate2">GitHub project overview</a> says, "a C++ and Python library for efficient inference with Transformer models." It is a library that makes Transformer model inference efficient through various optimizations. Libraries for efficient inference such as <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> basically support only decoder models, but CT2 supports not only decoder models, but also encoder-decoder models and some encoder models. Since supported encoder models include BERT, BERT-family models can also run inference efficiently.</p>
<p>You may think, "BERT? Do we still use such an old architecture?" But for example, <a href="https://huggingface.co/intfloat/multilingual-e5-small">multilingual-e5-small</a>, the model I use daily to generate embeddings, is also a BERT-family model. There are still many chances to use it.</p>
<h2>Embedding inference with CTranslate2 and SentenceTransformer</h2>
<p>Using CTranslate2 as a SentenceTransformer-compatible model is very easy. For example, change this SentenceTransformer code:</p>
<pre><code>from sentence_transformers import SentenceTransformer
model = SentenceTransformer(model_name, device=device)
embs = model.encode(texts)
</code></pre>
<p>to this:</p>
<pre><code>from hf_hub_ctranslate2 import CT2SentenceTransformer
model = CT2SentenceTransformer(
    model_name, device=device, compute_type=compute_type
)
embs = model.encode(texts)
</code></pre>
<p>That alone basically makes inference 1.6 to 2 times faster. It also uses less memory.</p>
<p><code>CT2SentenceTransformer</code> is implemented as a subclass of SentenceTransformer, so it can be used in almost the same way. I describe <code>compute_type</code> later.</p>
<h2>Actual inference speed and output differences</h2>
<p>Let's look at the measured speed difference. I used 20,000 Japanese Wikipedia samples, taking the first 512 tokens and converting them into embeddings with <a href="https://huggingface.co/intfloat/multilingual-e5-small">multilingual-e5-small</a> for a similarity search task by adding the <code>"query: "</code> prefix. I compared inference with the original SentenceTransformer and with CT2 under several <code>compute_type</code> settings. The notebook is <a href="https://colab.research.google.com/drive/181RqvUP5MRtNTK0_mFDo5pJW0XO_hQVI">here</a>. The GPU is RTX 4090, and the CPU is Ryzen 9 5950X. <code>speed</code> is relative to SentenceTransformer as 1.0.</p>
<table>
<thead>
<tr>
<th>device</th>
<th>type</th>
<th>speed</th>
<th>time</th>
<th>rps</th>
<th>mAP@100</th>
<th>MSE</th>
</tr>
</thead>
<tbody>
<tr>
<td>cuda</td>
<td>sentence_transformer</td>
<td>1.00</td>
<td>38.99</td>
<td>512.94</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>cuda</td>
<td>CT2 + int8</td>
<td>0.94</td>
<td>41.44</td>
<td>482.63</td>
<td>1.0</td>
<td>0.000004</td>
</tr>
<tr>
<td>cuda</td>
<td>CT2 + int8_float32</td>
<td>0.93</td>
<td>41.89</td>
<td>477.43</td>
<td>1.0</td>
<td>0.000004</td>
</tr>
<tr>
<td>cuda</td>
<td>CT2 + int8_float16</td>
<td>1.45</td>
<td>26.98</td>
<td>741.30</td>
<td>1.0</td>
<td>0.000004</td>
</tr>
<tr>
<td>cuda</td>
<td>CT2 + float16</td>
<td>1.66</td>
<td>23.54</td>
<td>849.53</td>
<td>1.0</td>
<td>0.0</td>
</tr>
<tr>
<td>cuda</td>
<td>CT2 + auto</td>
<td>1.48</td>
<td>26.36</td>
<td>758.69</td>
<td>1.0</td>
<td>0.000004</td>
</tr>
<tr>
<td>cpu</td>
<td>sentence_transformer</td>
<td>1.00</td>
<td>1389.80</td>
<td>14.39</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>cpu</td>
<td>CT2 + auto</td>
<td>1.89</td>
<td>737.07</td>
<td>27.13</td>
<td>1.0</td>
<td>0.000004</td>
</tr>
</tbody>
</table>
<p>In this result, CT2 with quantization such as int8 was actually slower, and CT2 + float16 was the fastest. On GPU, <code>compute_type="float16"</code> gave 1.66x speed. As an evaluation metric for the inference results, mAP@100 was 1.0, meaning the ranking did not change. To look at a finer accuracy difference, I also measured MSE, and it was almost unchanged too. It is displayed as 0.0, but the actual value was around 3e-09. For GPU, <code>compute_type="auto"</code> seems to be <code>int8_float</code>.</p>
<p>On CPU, <code>compute_type="auto"</code> was about 1.9x faster. mAP@100 remained 1.0, and MSE was only 0.000004, a tiny difference that should be almost no problem in real operation. There are many cases where inference is run on CPU, so a 1.9x speedup for CPU inference is quite valuable. I did not measure memory this time, but CT2 also advertises lower memory use and it did seem memory-efficient, so it should be even more useful in environments with tight compute resources.</p>
<h1>CTranslate2 deserves more attention</h1>
<p>CTranslate2 can be used for encoder models, and with hf_hub_ctranslate2, Hugging Face models can be used easily. This time I used it as a replacement for SentenceTransformer, but BERT-family models are still used for many tasks, so I feel its range of use is broad.</p>
<p>However, CTranslate2 currently has 2.2k GitHub stars, while llama.cpp has 44.6k. In this LLM boom, many projects receive a lot of stars, so its popularity feels modest. The name CTranslate2, perhaps because it was originally used to speed up machine translation models, also does not make it easy to infer what the library can do. That feels like a waste. I hope more people try it.</p>]]></content:encoded>
  <pubDate>Thu, 23 Nov 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Thu, 23 Nov 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Embedding conversion performance on Apple Silicon GPU (MPS)</title>
  <link>https://hotchpotch.dev/articles/apple-silicon-embs/</link>
  <guid>https://hotchpotch.dev/articles/apple-silicon-embs/</guid>
  <description>I measured how fast Apple Silicon MPS can convert text into embeddings, comparing a MacBook Air M2 with RTX 4090, Colab T4, and CPU execution for multilingual-e5-small.</description>
  <content:encoded><![CDATA[<p>With the announcement of the M3 Max with 128 GB unified memory, people were excited that even huge-parameter LLMs might run locally. For my own use case, I wanted to convert text into embeddings on my local Mac, so I measured how fast text-to-sentence-vector, or embedding, conversion actually is.</p>
<p><img src="https://i.imgur.com/qjBMgyJ.jpg" alt="Apple Silicon"></p>
<h2>Environment</h2>
<p>I ran the benchmark on a local Mac, Linux through WSL2, and Colab.</p>
<ul>
<li>Mac: 2022 MacBook Air / M2, 8 CPU cores, 10 GPU cores, 24 GB memory</li>
<li>Linux (WSL2): Ryzen 9 5950X / NVIDIA RTX 4090</li>
<li>Google Colab: T4 instance</li>
</ul>
<p>On each CPU and GPU, I measured the time to convert the first 512 tokens of 1000 Japanese Wikipedia samples with <a href="https://huggingface.co/intfloat/multilingual-e5-small">multilingual-e5-small</a>. The notebook used for measurement is here:</p>
<ul>
<li><a href="https://colab.research.google.com/drive/14_oeZrN5v7Potq5_a8UXvaOGCUJ4I1m8?usp=sharing">https://colab.research.google.com/drive/14_oeZrN5v7Potq5_a8UXvaOGCUJ4I1m8?usp=sharing</a></li>
</ul>
<h2>Results</h2>
<table>
<thead>
<tr>
<th>Device</th>
<th>Method</th>
<th>Total Time (sec)</th>
<th>RPS</th>
</tr>
</thead>
<tbody>
<tr>
<td>RTX 4090</td>
<td>CUDA (GPU)</td>
<td>2.58</td>
<td>388.07</td>
</tr>
<tr>
<td>Colab T4</td>
<td>CUDA (GPU)</td>
<td>19.92</td>
<td>50.21</td>
</tr>
<tr>
<td>MacBook Air M2</td>
<td>MPS (GPU)</td>
<td>33.16</td>
<td>30.15</td>
</tr>
<tr>
<td>Ryzen 5950X</td>
<td>CPU</td>
<td>73.18</td>
<td>13.66</td>
</tr>
<tr>
<td>MacBook Air M2</td>
<td>CPU</td>
<td>104.89</td>
<td>9.53</td>
</tr>
<tr>
<td>Colab</td>
<td>CPU</td>
<td>710.72</td>
<td>1.41</td>
</tr>
</tbody>
</table>
<p>The RTX 4090 wins overwhelmingly, which is expected. But the 10-core M2 GPU reaches about 60% of the T4's speed. M3 Max also has a 40-core GPU model. If speed scales linearly, an M3 Max 40-core GPU would reach around 130 RPS. That is about one third of an RTX 4090 and quite fast for a laptop GPU. It also looks likely to be more than twice as fast as a T4.</p>
<p>Whether M2 embedding conversion is practical depends on the use case. It is not terribly slow, but it is not fast either. Still, the GPU is about three times faster than the M2 CPU, and with Hugging Face Transformers you can use it just by setting the device to <code>"mps"</code>. If you use a Mac, you should naturally use the GPU. With M3 Max, I think many use cases would get reasonably practical speed.</p>
<p>Even so, the M2 CPU with 8 cores is surprisingly fast. Ryzen 5950X uses all 16 cores for this run, yet on single CPU core speed the M2 seems faster than the Ryzen 5950X. Library optimization may also be involved, but in a simple comparison without thinking too hard, it is fast.</p>
<h2>Update</h2>
<p>I received a message from <a href="https://www.ey-office.com/blog/">yuumi3</a> saying that a Mac mini M2 Pro with 10 CPU cores and 16 GPU cores produced the following speeds. Thank you. The GPU score seems to have increased roughly with the move from 10 GPU cores on M2 to 16 GPU cores on M2 Pro.</p>
<pre><code>[mps] convert 1000 embs, total time: 13.59 sec  / rps: 73.60
[cpu] convert 1000 embs, total time: 68.12 sec  / rps: 14.68
</code></pre>]]></content:encoded>
  <pubDate>Fri, 10 Nov 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Fri, 10 Nov 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Starting Weekly AI News: automated summaries with clustering and GPT</title>
  <link>https://hotchpotch.dev/articles/weekly-ai-news/</link>
  <guid>https://hotchpotch.dev/articles/weekly-ai-news/</guid>
  <description>I started a weekly Substack newsletter that automatically summarizes AI-related news. This note explains how I cluster articles with multilingual-e5-small and generate topic titles and summaries with GPT.</description>
  <content:encoded><![CDATA[<p>AI-related topics continue to be numerous, and I thought it would be useful to know roughly what became a topic each week. So I started a weekly newsletter on Substack. The content is created fully automatically. For example, the AI news summary for the week going back from July 28, 2023 looked like this:</p>
<ul>
<li><a href="https://ainewsdev.substack.com/p/weekly-ai-news-1">https://ainewsdev.substack.com/p/weekly-ai-news-1</a></li>
</ul>
<p>I would not call it perfect, but I think it gathers reasonably notable topics in a decent way. If you are interested, please subscribe or read it through a feed reader.</p>
<p><a href="https://ainewsdev.substack.com/embed">Substack signup form</a></p>
<p>The newsletter title is a tribute to <a href="https://weeklykagglenews.substack.com/">Weekly Kaggle News</a>.</p>
<hr>
<p>That would be only publicity, so I will also write about the internal implementation. It has not changed drastically from the <a href="https://p0q.notion.site/AIAD-0-LT-0e22457243394175b06e0cf2fd29aa49">material I linked before</a>, but roughly:</p>
<ul>
<li>Generate features, or sentence vectors, from title + summary using <a href="https://huggingface.co/intfloat/multilingual-e5-small">multilingual-e5-small</a>.
<ul>
<li>I use small so that it runs on the low-spec VPS environment. Subjectively, small did not feel much less accurate.</li>
</ul>
</li>
<li>Add a standardized article timestamp vector to the 384 dimensions from e5-small, making a 385-dimensional representation.</li>
<li>Run KMeans without dimensionality reduction. The number of clusters is total article count divided by 8, chosen roughly. With about 250 target articles, this gives around 30 clusters.
<ul>
<li>Reducing dimensions with UMAP or PCA did not produce very good results.</li>
</ul>
</li>
<li>Look at overall distances, extract only articles near each cluster center, and use clusters where at least N articles remain.</li>
</ul>
<p>This extracts clusters that look meaningful as groups of articles from the week. Then I generate titles and summaries for those clusters with <code>gpt-3.5-turbo</code>. It is basically ordinary BERTopic-like clustering plus GPT-based topic representation. In other words, a topic model implementation.</p>
<hr>
<p>Recent <a href="https://github.com/MaartenGr/BERTopic">BERTopic implementations</a> on GitHub also seem to include OpenAI and LLM-based features, such as creating sentence vectors with OpenAI embeddings, or ada-v2, in addition to sentence-transformers, and <a href="https://maartengr.github.io/BERTopic/getting_started/representation/llm.html">creating topic representations with ChatGPT or GPT-4</a>. By default, it seems to include c-TF-IDF keyword extraction in the prompt for generation. If you want to try this quickly with a library, BERTopic may be a good option.</p>
<hr>
<p>Incidentally, newsletter article creation is fully automatic, but Substack itself does not seem to have a mechanism that lets me send the newsletter by calling an API. The final delivery flow is manual through the Web UI, which is unfortunate for my own workload.</p>]]></content:encoded>
  <pubDate>Mon, 31 Jul 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 31 Jul 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Implementing and trying gzip + kNN text classification from the paper that beats BERT</title>
  <link>https://hotchpotch.dev/articles/ncd-classifier/</link>
  <guid>https://hotchpotch.dev/articles/ncd-classifier/</guid>
  <description>I implemented the gzip-based NCD + kNN classifier from the paper “Low-Resource” Text Classification and tried it on Japanese and English datasets, including livedoor news, MARC-ja, and AGNews.</description>
  <content:encoded><![CDATA[<p>The recently published paper <a href="https://aclanthology.org/2023.findings-acl.426">“Low-Resource” Text Classification: A Parameter-Free Classification Method with Compressors</a> (Jiang et al., Findings 2023) says that it uses the length of data compressed with gzip and performs better than BERT on text classification tasks. That sounded interesting, so I implemented the method myself and tried it. As a result, for text category classification using the <a href="https://www.rondhuit.com/download.html">livedoor news corpus</a>, it actually achieved a better result than Japanese BERT.</p>
<h2>What kind of method is it?</h2>
<p>What it does is simple. First, calculate <a href="https://en.wikipedia.org/wiki/Normalized_compression_distance">NCD, or Normalized Compression Distance</a>. The examples use gzip as the compression algorithm.</p>
<ul>
<li>Compress individual data <code>x</code> and <code>y</code>, and call their compressed lengths <code>C(x)</code> and <code>C(y)</code>.</li>
<li>Concatenate the two pieces of data into <code>xy</code>, compress it, and call the compressed length <code>C(xy)</code>.</li>
<li>Calculate NCD:
<ul>
<li><code>NCD(x, y) = [C(xy) - min(C(x), C(y))] / max(C(x), C(y))</code></li>
</ul>
</li>
</ul>
<p>If the information is similar, NCD becomes small. <code>"Hello world!"</code> concatenated with <code>"Hello!"</code> should compress well and have a small NCD, while <code>"Hello world!"</code> and <code>"Good!"</code> should be farther apart.</p>
<p>Then sort the training data by this distance, take the top-k items, and use the most common category among those top-k items as the prediction. In other words, it decides from the k nearest training items under NCD distance, so it is kNN.</p>
<h2>Implementation</h2>
<p>The original paper's implementation is available at <a href="https://github.com/bazingagin/npc_gzip">https://github.com/bazingagin/npc_gzip</a>. The method should be simple, but the code was hard to use, so I implemented a version that can be used quickly with sklearn-like <code>fit</code> and <code>predict</code> interfaces.</p>
<ul>
<li><a href="https://github.com/hotchpotch/ncd_classifier/">https://github.com/hotchpotch/ncd_classifier/</a></li>
</ul>
<h2>Trying it on the livedoor news corpus</h2>
<p>Using this implementation, let's classify categories in the livedoor news corpus. In <a href="https://qiita.com/sugulu_Ogawa_ISID/items/697bd03499c1de9cf082">this Japanese BERT implementation article</a>, 1475 items, about 20% of the whole dataset, were used as test data, and the accuracy was <code>0.9261</code>.</p>
<p>This time I split the dataset into train and test at 8:2, with 5894 train items and 1473 test items.</p>
<ul>
<li><a href="https://github.com/hotchpotch/ncd_classifier/blob/main/examples/livedoor_news_ja.py">https://github.com/hotchpotch/ncd_classifier/blob/main/examples/livedoor_news_ja.py</a></li>
</ul>
<p>The accuracy was <code>0.9457</code>, which appears to beat Japanese BERT. It is impressive for such a simple mechanism. You may wonder why I did not prepare validation data, but this method only calculates distance, or NCD, and does not pretrain anything, so validation data would not be meaningful here.</p>
<pre><code># Accuracy
0.9456890699253224
</code></pre>
<pre><code># Confusion matrix
[[150   0   0   0   1   1   0   0   0]
 [  0 166   2   2   3   0   0   0   2]
 [  0   1 164   0   0   2   0   0   0]
 [  0   0   1 156   0   0   5   0   0]
 [  5   1   3   2 103   5   0   3   4]
 [  5   0   1   1   5 148   0   4   3]
 [  0   0   0   5   1   0 182   0   0]
 [  1   0   1   0   3   7   0 151   0]
 [  0   0   0   0   0   0   0   0 173]]
</code></pre>
<h2>Trying it on MARC-ja</h2>
<p>Next, let's evaluate MARC-ja from the JGLUE dataset. MARC-ja has about 190,000 items with positive and negative labels, with roughly 90% positive and 10% negative. Japanese BERT reportedly gets accuracy <code>0.958</code>. If everything were predicted as positive, accuracy would be around <code>0.9</code>.</p>
<p>With NCD Classifier, the accuracy was <code>0.802</code>. Very bad. At first I thought the implementation must be wrong and checked it repeatedly, but the dataset distribution is heavily skewed, and many texts are too short. Livedoor news articles, for example, have a reasonable length. It seems this method does not perform well under these conditions.</p>
<ul>
<li><a href="https://github.com/hotchpotch/ncd_classifier/blob/main/examples/JGLUE_MARC_ja.py">https://github.com/hotchpotch/ncd_classifier/blob/main/examples/JGLUE_MARC_ja.py</a></li>
</ul>
<pre><code># Accuracy
0.8020870180403255
</code></pre>
<pre><code># Confusion matrix
[[4077  755]
 [ 364  458]]
</code></pre>
<h2>Trying it on AGNews</h2>
<p>The paper reports a score of 0.937 on AGNews, but my implementation only reached about 0.898. I do not know the cause of this difference. It may come from implementation differences, the data used, or some difference in data processing. My implementation may be wrong, so please let me know if you notice a problem.</p>
<ul>
<li><a href="https://github.com/hotchpotch/ncd_classifier/blob/main/examples/ag_news.py">https://github.com/hotchpotch/ncd_classifier/blob/main/examples/ag_news.py</a></li>
</ul>
<pre><code># Accuracy
0.8976315789473684
</code></pre>
<pre><code># Confusion matrix
[[1718   47   83   52]
 [  20 1838   23   19]
 [  72   31 1635  162]
 [  81   37  151 1631]]
</code></pre>
<h2>Rough summary</h2>
<p>I confirmed that simple text classification with NCD using gzip plus kNN, without training, can outperform BERT in some cases. As the paper says, it seems likely to work well on small datasets of a few hundred to a few thousand items where the text length is reasonably long, such as news articles. Among the datasets I tried, the livedoor news corpus is exactly that kind of dataset.</p>
<p>This paper's approach looks worth trying as one classifier for casual text classification that does not require pretraining. The implementation is also simple.</p>
<p>However, as the paper says, computational cost grows as the data grows. At prediction time, if <code>M</code> is the number of train items and <code>N</code> is the number of items to predict, the cost is roughly <code>O(M*N)</code>. The more data you have, the more prediction cost you pay. It is not learning features; it is doing a direct full calculation, so it is slow. For example, the MARC-ja dataset has <code>M = 187528</code> and <code>N = 5654</code>, so the computation cost is large. Even using 32 virtual cores on a Ryzen 7950X CPU fully, predicting 5654 items takes about 30 minutes.</p>
<p>Still, it was an interesting paper showing that a simple approach without pretraining can produce enough performance depending on the use case.</p>]]></content:encoded>
  <pubDate>Thu, 20 Jul 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Thu, 20 Jul 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Generating answers from images with ChatGPT 3.5 and extracting information through BLIP-2 prompts</title>
  <link>https://hotchpotch.dev/articles/gpt-image/</link>
  <guid>https://hotchpotch.dev/articles/gpt-image/</guid>
  <description>A note on using BLIP-2 with ChatGPT 3.5 for image-based answer generation when the task fits, and more importantly, on extracting image information through prompts to BLIP-2.</description>
  <content:encoded><![CDATA[<p>This is about how image-based answer generation, similar to what can be done with the ChatGPT Web UI and GPT-4, can also be useful with ChatGPT 3.5 and BLIP-2 if the requirements match. What I really want to talk about is information extraction through prompts using BLIP-2.</p>
<p><img src="https://i.imgur.com/40DTXp6.png" alt="recipe"></p>
<hr>
<p>The reason I tried this was that I wondered how GPT-4 generates answers from images. While looking into it, I found BLIP-2 explained as an example of incorporating VQA tasks into an LLM in <a href="https://speakerdeck.com/sgnm/vision-and-languagenoxian-zhuang-tozhan-wang-gpt-4?slide=73">Current Status and Prospects of Vision and Language (GPT-4)</a>.</p>
<p><img src="https://i.imgur.com/xMm0Pwb.png" alt="vqa_blip"></p>
<p>I had roughly thought of BLIP as something for generating captions from images. BLIP-2, however, trains with an open LLM behind it, which lets it learn information that cannot be represented by image-caption pairs alone and improves accuracy.</p>
<p><img src="https://i.imgur.com/UGs7iUq.png" alt="blip-2"></p>
<p>What is useful for users is that it is not limited to ordinary caption generation. You can extract information by making it solve a Q&#x26;A task about an image.</p>
<p><img src="https://i.imgur.com/Daa2sH3.png" alt="tokyo_tower"></p>
<p>For example, "a night view of Tokyo Tower is shown" is generated by having GPT-3.5 answer based on a simple caption. The flow for the opening example, "please suggest several recipes", is as follows:</p>
<ul>
<li>Use <code>image_question_prompt</code> with "What ingredients are there?" to extract information from the image with BLIP-2.
<ul>
<li>The QA task only accepts English, so translate with <a href="https://ai.facebook.com/blog/nllb-200-high-quality-machine-translation/ja/">Meta's NLLB-200</a>, which is for non-commercial use only.</li>
<li>The prompt becomes <code>Question: What are the ingredients? Answer:</code>.</li>
</ul>
</li>
<li>The extracted text becomes "Vegetables, carrots, cabbage, cauliflower, broccoli, and potatoes".</li>
<li>After that it is easy. Use that text and have GPT-3.5 answer with few-shot prompting.</li>
</ul>
<p>That is the simple mechanism. If you write the information-extraction prompt directly in English for <code>image_question_prompt</code>, you can process it without the translation step.</p>
<hr>
<p>This cannot handle advanced context in the way ChatGPT with GPT-4 can. But if the task can be combined with information extraction that BLIP-2 can perform, it can work reasonably well.</p>
<p>What I really want to say in this article is what I wrote above: with an appropriate prompt, BLIP-2 can perform a certain amount of information extraction. ChatGPT's own API will probably become able to handle image-based information extraction tasks, but BLIP-2 plus open LLM models should also continue to improve. I expect zero-shot information extraction with open models to become more accurate, and I am looking forward to that.</p>
<h2>References</h2>
<ul>
<li><a href="https://gist.github.com/hotchpotch/9080889f59f4e889f2066391d2627ce8">Notebook code for this example</a></li>
<li><a href="https://huggingface.co/blog/blip-2">Zero-shot image-to-text generation with BLIP-2</a></li>
<li><a href="https://arxiv.org/abs/2301.12597">BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models</a></li>
<li><a href="https://blog.shikoan.com/blip-2/">Paper summary: BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models</a></li>
</ul>]]></content:encoded>
  <pubDate>Wed, 05 Jul 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 05 Jul 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Enjoying Stable Diffusion again from a technical perspective</title>
  <link>https://hotchpotch.dev/articles/sd-tech/</link>
  <guid>https://hotchpotch.dev/articles/sd-tech/</guid>
  <description>After using Stable Diffusion again through stable-diffusion-webui, I wrote notes on the surrounding techniques I had not followed closely: ControlNet, LoRA, textual inversion embeddings, and checkpoint merging.</description>
  <content:encoded><![CDATA[<p>Recently I used Stable Diffusion again through <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui">stable-diffusion-webui</a>, and there were several technical things I did not know. These are my notes.</p>
<h2>ControlNet</h2>
<ul>
<li><a href="https://arxiv.org/abs/2302.05543">https://arxiv.org/abs/2302.05543</a></li>
<li><a href="https://github.com/lllyasviel/ControlNet">https://github.com/lllyasviel/ControlNet</a></li>
</ul>
<p>ControlNet is an extremely powerful neural network for controlling generated images so that they follow specific conditions. It works properly with any base model.</p>
<p>As of v1.1, it supports conditions such as depth, semantic segmentation, human pose, fake scribbles, HED boundary, M-LSD lines, and Canny edge. From a source image, it can use composition, pose, segmentation, edge extraction, masked-region completion, and many other conditions for image generation. The way it combines existing datasets and architectures is also exciting. The range of applications is wide.</p>
<p>It is innovative enough that anyone who has not used ControlNet image generation should try it. In the illustration-generation context, people often focus only on pose control, but it can reproduce many kinds of composition. It is seriously impressive. More types of conditional image generation will probably become possible from here.</p>
<p>There is also a ControlNet WebUI extension for SD-WebUI, so it can be used easily from sd-web.</p>
<ul>
<li><a href="https://github.com/Mikubill/sd-webui-controlnet">https://github.com/Mikubill/sd-webui-controlnet</a></li>
</ul>
<p><img src="https://github.com/lllyasviel/ControlNet/raw/main/github_page/p3.png" alt=""></p>
<p><img src="https://user-images.githubusercontent.com/20929282/246632890-400b2e0d-b064-4505-b31d-49375216ca98.png" alt=""></p>
<p>Clear explanations and related material:</p>
<ul>
<li><a href="https://blog.shikoan.com/controlnet_lora/">Trying ControlNet + LoRA in Diffusers: theory and practice</a></li>
<li><a href="https://ascii.jp/elem/000/004/137/4137393/">ControlNet, the second revolution in image generation AI</a></li>
<li><a href="https://speakerdeck.com/koharite/lun-wen-jie-shuo-controlnet">Paper explanation: ControlNet</a></li>
</ul>
<h2>LoRA: Low-rank Adaptation for Fast Text-to-Image Diffusion Fine-tuning</h2>
<ul>
<li><a href="https://github.com/cloneofsimo/lora">https://github.com/cloneofsimo/lora</a></li>
<li><a href="https://github.com/kohya-ss/sd-scripts/blob/main/docs/train_network_README-ja.md">https://github.com/kohya-ss/sd-scripts/blob/main/docs/train_network_README-ja.md</a> (LoRA training based on the implementation above)</li>
</ul>
<p>The original LoRA paper, <a href="https://arxiv.org/abs/2106.09685">LoRA: Low-Rank Adaptation of Large Language Models</a>, is about LLMs, or Transformers. The LoRA used with Stable Diffusion often uses these implementations. The cloneofsimo/lora implementation can train for high-quality output based on <a href="https://arxiv.org/abs/2106.05744">Pivotal Tuning Inversion</a>, or PTI.</p>
<p>LoRA adds low-rank matrices to a base model and trains only those parts. This reduces training cost. Because the resulting parameters are small, both the parameter file size and memory usage are smaller.</p>
<p>From the point of view of someone using LoRA for image generation, LoRA is easy to layer. You can apply LoRA-trained parameter B and LoRA-trained parameter C on top of base model A, and generate images with characteristics from both B and C without major changes. In SD-WebUI, you can quickly specify which LoRA to apply and at what strength from a text prompt, such as <code>&#x3C;lora:model_a:1.0>, &#x3C;lora:model_b:0.7></code>.</p>
<p>A clear Japanese deck about LoRA for LLMs is <a href="https://speakerdeck.com/hpprc/lun-jiang-zi-liao-lora-low-rank-adaptation-of-large-language-models">here</a>.</p>
<h2>Textual Inversion embeddings</h2>
<ul>
<li><a href="https://huggingface.co/docs/diffusers/training/text_inversion">https://huggingface.co/docs/diffusers/training/text_inversion</a></li>
<li><a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Textual-Inversion">https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Textual-Inversion</a></li>
</ul>
<p>In Stable Diffusion, one of the generation inputs is the embedding output, or sentence vector, from the CLIP text encoder. My understanding, although it is a little vague, is that textual inversion adds a new word that has learned a specific feature when creating these embeddings, and adjusts generation toward the intended output.</p>
<p>Normally, humans adjust output by putting strings into the text prompt. With textual inversion, you can directly insert text embeddings that have learned a specific expression, so you can make finer adjustments than ordinary text can express. After training, the embedding data has the same rank as a sentence vector, so the file size is extremely small.</p>
<p>Well-known examples include <a href="https://huggingface.co/datasets/gsdf/EasyNegative">EasyNegative</a>, which learns characteristics of words used in negative prompts and lets you add a good negative prompt easily, and <a href="https://civitai.com/models/16993/badhandv4-animeillustdiffusion">badhandv4</a> and <a href="https://huggingface.co/datasets/Nerfgun3/bad_prompt">bad_prompt</a>, which suppress strange arms and fingers.</p>
<h2>Merging checkpoint models</h2>
<p>Checkpoint merging combines checkpoint models to create another checkpoint. What it does is simple: linearly combine model parameters, essentially adding them with weights. It is surprisingly simple. This alone can create a model C that has characteristics of both models A and B. The many models named XxxMix that you see around are merged models made from multiple models. That said, not all merged models are only simple linear combinations. Some seem to use techniques such as changing the ratio by network layer, though that is still a kind of linear combination.</p>
<ul>
<li><a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#checkpoint-merger">https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#checkpoint-merger</a></li>
<li><a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/How-to-make-your-own-Inpainting-model">https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/How-to-make-your-own-Inpainting-model</a></li>
</ul>
<hr>
<p>SD-WebUI keeps making these new techniques easy to use. For example, LoRA files, embeddings, and base models work by putting them in directories, and extensions such as sd-webui-controlnet can add features. The UI is not exactly approachable, but the system is well made. There is also a wiki page that roughly explains all features. If you use plain Stable Diffusion, you have to gather information yourself about what is possible, but SD-WebUI usually includes current trends, so it is also useful for understanding what is popular.</p>
<ul>
<li><a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features">https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features</a></li>
</ul>
<p>For image generation AI, I mostly use Midjourney. But touching Stable Diffusion again made me feel the evolution and interest of the ecosystem that comes from being open source and having published checkpoints. Looking closely, many parts are interesting, and it feels like I could get pulled deeply into it, so I am not digging too far. Still, it seems like an interesting area to get absorbed in.</p>
<p>After writing this much, I remembered that <a href="https://koshian2.booth.pm/items/4820110">A New Era of AI Art: Image Generation Technology and Applications Using CLIP and Stable Diffusion</a> had been sitting unread, so I started reading it. It covers the topics in this note, of course, and also explores many experimental approaches around the CLIP encoder as well as Stable Diffusion. It includes results showing what happens when each approach is applied, so you can understand how generated images change. Reading it is definitely enjoyable.</p>]]></content:encoded>
  <pubDate>Tue, 04 Jul 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 04 Jul 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Another major benefit of LoRA: switching task models instantly while sharing GPU memory</title>
  <link>https://hotchpotch.dev/articles/lora-share-gpu/</link>
  <guid>https://hotchpotch.dev/articles/lora-share-gpu/</guid>
  <description>LoRA is usually discussed as a cheaper training method, but it also lets multiple task-specific adapters share one LLM base model in memory. This article shows how to switch adapters with Hugging Face PEFT.</description>
  <content:encoded><![CDATA[<p>LoRA, or Low-Rank Adaptation of Large Language Models, adds low-rank matrices so that you can train at low cost while keeping the original model intact. Recently, large Japanese models such as <a href="https://huggingface.co/cyberagent/open-calm-7b">cyberagent/open-calm-7b</a> and <a href="https://huggingface.co/rinna/japanese-gpt-neox-3.6b">rinna/japanese-gpt-neox-3.6b</a> were released, and Hugging Face released <a href="https://github.com/huggingface/peft">peft</a>, a library that makes LoRA easy to use with Transformers. Many people have probably tried it.</p>
<p>Most explanations of LoRA's benefits focus on training. I had not seen much discussion of another major benefit: handling multiple tasks while sharing memory for the base LLM. This article explains how to do that with peft.</p>
<p>For an explanation of what LoRA is, <a href="https://speakerdeck.com/hpprc/lun-jiang-zi-liao-lora-low-rank-adaptation-of-large-language-models">this study group deck on LoRA: Low-Rank Adaptation of Large Language Models</a> is very clear.</p>
<h2>What problem does this solve?</h2>
<p><img src="https://i.imgur.com/Kj58m4Q.png" alt="LoRA"></p>
<p>As the name says, LLMs are large language models. For example, if you load the open-calm-7b model onto a GPU in fp16, it alone uses about 13 GB of memory. If you fully fine-tune it for a task, running that task needs 13 GB. If you then load another model for another task, it needs another 13 GB. A total of 26 GB is a harsh amount of memory, especially for a home GPU.</p>
<p>However, if you train open-calm-7b with LoRA by adding low-rank matrices with parameter <code>r=8</code>, the additional memory needed is only 17 MB. Not 17 GB, but 17 MB. You get a neural network that has learned task-specific characteristics and can solve some task with only that extra size.</p>
<p>That means you can handle another task with the 13 GB base LLM plus 17 MB. And not just one task. If you have LoRA data trained from ten different tasks or datasets, you can handle those tasks with 13 GB + 170 MB of memory. That is extremely powerful.</p>
<p>To be honest, for batch processing where you run the same process over lots of data, repeatedly loading and unloading GPU memory is often acceptable if you can wait. But for realtime sequential processing, such as responding to user input, being able to share memory on one GPU while handling multiple tasks is much better for performance.</p>
<p>For example, this seems useful for cases like:</p>
<ul>
<li>Changing the expression style of chatbot responses</li>
<li>Running an article hosting service and switching models per user after learning each user's writing characteristics</li>
<li>Switching models to evaluate which training worked better in an A/B test</li>
<li>Switching LangChain Agents quickly when running them locally
<ul>
<li>Each Agent may have different capabilities, and you may want to switch Agents depending on the content. If each Agent is a huge model, frequent loading and unloading from memory becomes very slow.</li>
</ul>
</li>
</ul>
<p>One caveat is that the base LLM must be the same.</p>
<h2>How to switch in practice</h2>
<p>When using LoRA models trained with peft, switching is very easy. I prepared the following notebook as an example:</p>
<ul>
<li><a href="https://gist.github.com/hotchpotch/e99a70a6864c76f5638010537d535a33">https://gist.github.com/hotchpotch/e99a70a6864c76f5638010537d535a33</a></li>
</ul>
<p><code>PeftModel</code> can switch the active model using a feature called <code>adapter</code>. The model loaded by default is named <code>default</code>, and you can load another model with a name using <code>load_adapter(model_name, adapter_name)</code>.</p>
<p>For example, load <code>peft_model</code> like this:</p>
<pre><code>from peft import PeftConfig, PeftModel

peft_model_open2ch = "hotchpotch/open-calm-7b_lora_open2ch"
peft_config_open2ch = PeftConfig.from_pretrained(peft_model_open2ch)

model = AutoModelForCausalLM.from_pretrained(peft_config_open2ch.base_model_name_or_path, device_map="auto", torch_dtype=torch.float16)

peft_model = PeftModel.from_pretrained(model, peft_model_open2ch)
</code></pre>
<p>Then add a model with a different capability:</p>
<pre><code># https://note.com/masuidrive/n/n0e2a11fc5bfa
peft_model_instruct = "masuidrive/open-calm-instruct-lora-20230525-r4-alpha16-batch32-epoch1"

# Load it into peft_model with the adapter name "instruct"
peft_model.load_adapter(peft_model_instruct, "instruct")
</code></pre>
<p>After that, just switch adapters according to the task:</p>
<pre><code># The trained model hotchpotch/open-calm-7b_lora_open2ch
peft_model.set_adapter("default")
# The trained model masuidrive/open-calm-instruct-lora-20230525-r4-alpha16-batch32-epoch1
peft_model.set_adapter("instruct")
</code></pre>
<p>With this, the base LLM <code>cyberagent/open-calm-7b</code> should be loaded into about 13 GB of memory, while the 2ch-style text generation model is loaded as the <code>default</code> adapter and the QA answering model is loaded as the <code>instruct</code> adapter. Together, those adapters add only about 34 MB of memory.</p>
<p>So by switching with <code>set_adapter</code> for the task you want to run, you can use the models well without loading and freeing the huge LLM again. In the notebook example, I switch between two capabilities: generating 2ch-style text and answering questions.</p>
<h2>LLMs plus many adapters open up possibilities</h2>
<p>Training huge models that previously required full fine-tuning can now be done efficiently with LoRA, with small saved data size. In inference, multiple tasks can also share memory and run with lower memory usage.</p>
<p>This area is evolving quickly day by day. It is very interesting, the things we can do are expanding, and I am looking forward to the future.</p>
<p>The title says "GPU memory", but this memory sharing should not be limited to GPUs.</p>]]></content:encoded>
  <pubDate>Wed, 31 May 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 31 May 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Analyzing the Iris dataset with ChatGPT&apos;s Noteable plugin</title>
  <link>https://hotchpotch.dev/articles/noteable-iris/</link>
  <guid>https://hotchpotch.dev/articles/noteable-iris/</guid>
  <description>After trying Noteable on a tiny OpenCALM dataset, I asked it to analyze the classic Iris dataset. It quickly generated plots, model comparisons, clustering, and dimensionality reduction notebooks.</description>
  <content:encoded><![CDATA[<p>The OpenCALM data I wrote about earlier was far too small, so I tried asking <a href="https://noteable.io/">Noteable</a> to analyze the Iris dataset, the classic dataset that everyone loves and that has probably been analyzed a hundred million times. The results were about what I expected, but it also quickly wrote code for plotting graphs and trying several algorithms, which felt very convenient. This is a short note about that.</p>
<ul>
<li><a href="https://chat.openai.com/share/e99aaf30-08a8-4941-ad3e-c52e8c432c40">https://chat.openai.com/share/e99aaf30-08a8-4941-ad3e-c52e8c432c40</a></li>
<li><a href="https://app.noteable.io/f/c3246857-4f24-4204-b499-5aeca55321b9/iris_dataset_analysis.ipynb">https://app.noteable.io/f/c3246857-4f24-4204-b499-5aeca55321b9/iris_dataset_analysis.ipynb</a></li>
</ul>
<hr>
<p>The standard statistics output was unsurprising, but the pair plot was nice. I usually end up reading the documentation while writing pair plots, but it generated a clean seaborn plot and colored it by target, or species.</p>
<p><img src="https://i.imgur.com/4EeBHeB.png" alt=""></p>
<p>I only gave it an instruction like: "I want to build a model that predicts target using data other than target. What algorithms would be good for building the prediction model? Please answer using Noteable." From that, it split the data into train and test at 8:2, wrote implementations for five sklearn algorithms, and displayed the actual results. That was convenient because writing this by hand each time is bothersome. This time all models achieved 100% accuracy, but if the accuracy had differed by algorithm, I could probably ask why and get an explanation.</p>
<p>The notebook code automatically created by Noteable looked like this:</p>
<pre><code>from sklearn.model_selection import train_test_split

X = iris_df.drop('target', axis=1)
y = iris_df['target']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
X_train.shape, X_test.shape, y_train.shape, y_test.shape
</code></pre>
<pre><code>from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

models = [
    ('Logistic Regression', LogisticRegression()),
    ('Decision Tree', DecisionTreeClassifier()),
    ('Random Forest', RandomForestClassifier()),
    ('SVM', SVC()),
    ('KNN', KNeighborsClassifier())
]

results = []

for name, model in models:
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    results.append((name, accuracy))

results
</code></pre>
<pre><code>[('Logistic Regression', 1.0),
 ('Decision Tree', 1.0),
 ('Random Forest', 1.0),
 ('SVM', 1.0),
 ('KNN', 1.0)]
</code></pre>
<p>ChatGPT then returned an easy-to-understand explanation of these results.</p>
<hr>
<p>Next was clustering and visualization with dimensionality reduction. This is another thing that is quietly annoying to write yourself because you end up checking the documentation, but it generated the code quickly. It first used PCA for dimensionality reduction, and when I asked what would happen with t-SNE, the graph appeared right away.</p>
<p>The clustering and dimensionality-reduction plots with PCA and t-SNE looked like this:</p>
<p><img src="https://i.imgur.com/1zlrPmb.png" alt=""></p>
<p><img src="https://i.imgur.com/xfcpaaD.png" alt=""></p>
<hr>
<p>ChatGPT cannot properly analyze unknown data it has never seen, although few-shot examples can help. A service that lets it execute a notebook, observe the results, and then continue the conversation through ChatGPT complements that weakness well. It made me feel again that Noteable is impressive.</p>
<p>People who do data analysis probably see Iris and think, "Ah, another Iris tutorial", and do not feel like analyzing it again. I was surprised that a day came when I voluntarily wanted to analyze the Iris dataset again.</p>]]></content:encoded>
  <pubDate>Sat, 27 May 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Sat, 27 May 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Using ChatGPT&apos;s Noteable and WebPilot plugins to build a notebook that predicts OpenCALM 14B performance</title>
  <link>https://hotchpotch.dev/articles/noteable/</link>
  <guid>https://hotchpotch.dev/articles/noteable/</guid>
  <description>I tried the Noteable plugin with ChatGPT and WebPilot to scrape OpenCALM model data, build a notebook, plot parameter counts and perplexity, and estimate the performance of a hypothetical 14B model.</description>
  <content:encoded><![CDATA[<p>I saw people saying that the <a href="https://noteable.io/">Noteable</a> plugin, which lets you create notebooks through conversation on ChatGPT, was impressive. I tried it with ChatGPT, or GPT-4, and it was better than I expected. We have reached a convenient time where AI can write the notebook for small analyses, and you can check the results in ChatGPT without opening the notebook itself.</p>
<p>For example, I asked it to fetch the number of parameters and PPL for OpenCALM models from the Hugging Face page and plot them. The result looked like this:</p>
<p><img src="https://i.imgur.com/m9709ke.png" alt=""></p>
<p>The PPL performance graph per parameter looked like this:</p>
<p><img src="https://i.imgur.com/xrFVUQz.png" alt=""></p>
<p>It also built a model using the parameters from 1B onward, where a linear regression seemed plausible. The predicted graph for a hypothetical OpenCALM-14B model looked like this. Since the 1B, 3B, and 7B models decrease linearly, it feels like performance will continue improving as larger models appear.</p>
<p><img src="https://i.imgur.com/J9EMJIj.png" alt=""></p>
<pre><code>先ほど作成した線形回帰モデルの切片と係数は以下の通りです：

切片（Intercept）: 10.7928
係数（Coefficient）: -0.000383
これは、モデルが以下の形式で表されることを意味します：

Dev ppl = 10.7928 - 0.000383 * Params

つまり、Paramsが1増えると、Dev pplは約0.000383減少すると予測されます。
</code></pre>
<p>The ChatGPT conversation looked like this. It is not written in this exchange, but if I ask it to model the data with something nonlinear, such as SVM, a neural network, or a polynomial instead of only linear regression, it will express it that way too. It seems very convenient.</p>
<ul>
<li><a href="https://chat.openai.com/share/e3f754bb-0a9f-4bc9-a04e-399296f55884">https://chat.openai.com/share/e3f754bb-0a9f-4bc9-a04e-399296f55884</a>
<ul>
<li>ChatGPT recently added share URLs. Noteable images seem to be cached only for a few dozen minutes during the conversation, so they disappear after a while.</li>
</ul>
</li>
</ul>
<p>The notebook created by Noteable looked like this. The data scraped by WebPilot is placed in the first cell.</p>
<ul>
<li><a href="https://app.noteable.io/f/71f35e5a-171f-4d70-8428-62c42bf7193d/params_dev_ppl_graph-(1).ipynb">https://app.noteable.io/f/71f35e5a-171f-4d70-8428-62c42bf7193d/params_dev_ppl_graph-(1).ipynb</a></li>
</ul>
<p>Until now, the natural flow was to collect data from a web page, shape it into CSV or Python code, and then analyze it in a notebook using familiar steps. Being able to do that quickly by writing natural language is extremely convenient. If I want to do something more complex, the notebook already exists, so I can continue the analysis by adding a little myself. Having the usual notebook workflow become "mostly let AI do it, then have a human make the final adjustments" is a strong point.</p>
<p>Tedious work keeps disappearing, which feels good.</p>]]></content:encoded>
  <pubDate>Sat, 27 May 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Sat, 27 May 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Quantizing fastText to build a practical 1.7 MB text classifier</title>
  <link>https://hotchpotch.dev/articles/fasttext-quantize/</link>
  <guid>https://hotchpotch.dev/articles/fasttext-quantize/</guid>
  <description>I built a text classifier for AI News with fastText and quantization, reducing the model to 1.7 MB while keeping practical accuracy and recall for filtering AI-related English articles.</description>
  <content:encoded><![CDATA[<p>This is a note about my surprise after building a text classifier that decides whether English articles are AI-related or not with fastText and quantization. The resulting model was practical and only <code>1.7MB</code>. <code>1.7MB</code>!</p>
<hr>
<p>As I wrote in <a href="/articles/ainews">Launching AI News and how I used OpenAI behind it</a>, <a href="https://ai-news.dev/">AI News</a> currently classifies text as AI-related or not by converting articles into 1536-dimensional vectors with OpenAI's <code>text-embedding-ada-002</code> and training lightGBM on those vectors. The problem with this approach is that every article must go through the OpenAI API. On days with many long articles, this can cost several tens of yen per day. Monthly, it probably costs 500 to 1000 yen. Small costs add up.</p>
<p>The data had started to accumulate, so I wanted to classify articles without spending money on the OpenAI API. The data source is about 1,100 English article titles and bodies, with a ratio of about 2 AI articles to 8 non-AI articles. I split it 7:2:1 into train, validation, and test. Also, because I want to avoid mistakenly classifying AI articles as non-AI as much as possible, I look not only at accuracy but also at recall.</p>
<h2>Current classifier: OpenAI embeddings + lightGBM</h2>
<p>Accuracy was <code>0.9636</code>, and recall was <code>0.777</code>. For embeddings without fine-tuning, this is quite high.</p>
<h2>Transformer: deberta-v3-xsmall</h2>
<p>Accuracy was <code>0.9636</code>, and recall was <code>0.888</code>. This is a properly fine-tuned transformer, <a href="https://huggingface.co/microsoft/deberta-v3-xsmall">deberta-v3-xsmall</a>. The test set is small, about 110 items, so it is hard to say too much, but it is roughly the same performance as the current classifier. That is expected, since it is fine-tuned.</p>
<p>I also tried deberta-v3-large, but the score actually dropped. The training data may be too small for fine-tuning a larger model to fit well.</p>
<h2>fastText: cc.en.300</h2>
<p>Accuracy was <code>0.9454</code>, and recall was <code>1.0</code>. Accuracy dropped, but recall was 1.0. Because the test data is small, I cannot strongly claim the recall is excellent, but the balance looks good.</p>
<p>The trained model size at this point was <code>4.6GB</code>. That makes sense because the original <a href="https://fasttext.cc/docs/en/crawl-vectors.html">cc.en.300</a> is large.</p>
<h2>fastText: ag news</h2>
<p>Accuracy was <code>0.9454</code>, and recall was <code>1.0</code>. The result was the same as <code>cc.en.300</code>. The trained model size was <code>88MB</code>, already much smaller.</p>
<p>The ag news model came from <a href="https://fasttext.cc/docs/en/supervised-models.html">Supervised models</a>. <code>cc.en.300</code> is 300-dimensional data trained from <a href="https://commoncrawl.org/">Common Crawl</a> and Wikipedia, while <a href="https://paperswithcode.com/dataset/ag-news">ag news</a> is 10-dimensional data trained from a corpus of news article titles and descriptions. Since this dataset matches my use case fairly well, I think the compatibility was good.</p>
<p>At this point the model was already reasonably small and practical, but looking at <a href="https://fasttext.cc/docs/en/supervised-models.html">Supervised models</a>, I noticed that very lightweight quantized models are also provided. Their scores do not degrade much; for example, ag news goes from <code>0.924</code> to <code>0.92</code>. The model size goes from <code>387MB</code> to <code>1.6MB</code>.</p>
<p>So I tested performance with quantization too.</p>
<h2>fastText: ag news + quantization</h2>
<p>Accuracy was <code>0.9363</code>, and recall was <code>1.0</code>. Accuracy dropped slightly, but the model size shrank dramatically from <code>88MB</code> to <code>1.7MB</code>.</p>
<p>With the fastText command, quantization can be done quickly like this. Inference with the quantized model also worked without any special handling.</p>
<pre><code>fasttext  quantize -output ./trained.ag_news -input ./trained.ag_news.bin -qnorm -retrain -cutoff 100000
</code></pre>
<h2>Extra: fastText cc.en.300 + quantization</h2>
<p>Accuracy was <code>0.9181</code>, and recall was <code>0.714</code>. The score dropped a lot. The model size also dropped dramatically from <code>4.6G</code> to <code>16MB</code>, though. The 300-dimensional size of cc.en.300 may have worked against it here.</p>
<h2>Performance summary</h2>
<p>The classifiers I tried performed as follows. OpenAI embeddings are clearly strong. If cost does not matter, it feels like they are good enough. DeBERTa v3 is also good if the machine has enough resources. But AI News data processing runs on a VPS with 1 GB of memory, so it is overkill there.</p>
<p>So I decided to use the memory-efficient, quantized, and still practical fastText ag news model as the first-stage classifier, and then use OpenAI embeddings + lightGBM as the second stage. The first stage should filter out about 80% of articles, so the number of calls to OpenAI in the second stage should drop sharply.</p>
<ul>
<li>OpenAI embeddings + lightGBM
<ul>
<li>acc 0.9636, recall: 0.777</li>
</ul>
</li>
<li>deberta-v3-xsmall
<ul>
<li>acc 0.9636, recall: 0.8888</li>
</ul>
</li>
<li>fastText cc.en.300, after fine-tuning: 4.6G
<ul>
<li>acc 0.9454, recall: 1.0</li>
</ul>
</li>
<li>fastText cc.en.300, after fine-tuning and quantization: 16MB
<ul>
<li>acc 0.9181, recall: 0.7142</li>
</ul>
</li>
<li>fastText ag news, after fine-tuning: 88M
<ul>
<li>acc 0.9454, recall 1.0</li>
</ul>
</li>
<li>fastText ag news, after fine-tuning and quantization: 1.7M
<ul>
<li>acc 0.9363, recall 1.0</li>
</ul>
</li>
</ul>
<h1>fastText + quantization as an option</h1>
<p>Until now I did not know that quantizing a fastText model could reduce the model size this much. Going forward, when I want to run text classification inference on a low-spec machine, I will consider fastText + quantization as one strong option.</p>
<h2>Extra note: bin to vec</h2>
<pre><code>fasttext print-word-vectors ag_news.bin
</code></pre>
<p>This outputs a <code>.vec</code> file containing only word vectors. Specify this <code>.vec</code> file for a pretrained model.</p>]]></content:encoded>
  <pubDate>Tue, 09 May 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 09 May 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>After reading Kaggle ni Idomu Deep Learning Programming no Gokui</title>
  <link>https://hotchpotch.dev/articles/kaggle-book-review/</link>
  <guid>https://hotchpotch.dev/articles/kaggle-book-review/</guid>
  <description>A review of the Japanese book Kaggle ni Idomu Deep Learning Programming no Gokui, which works well as a compact index of practical machine learning ideas for both Kaggle beginners and people already working with machine learning.</description>
  <content:encoded><![CDATA[<p>I finished reading the book in the title, so here are my impressions. In one sentence, it is a good, easy-to-understand book that compresses the essence of machine learning into a short "index", whether or not you care about Kaggle. I recommend it both to machine learning beginners and Kaggle beginners. I am technically a Kaggle Competitions Master, and even so it helped me understand several things that I had only understood shallowly, and it introduced techniques I did not know. Machine learning engineers and people familiar with Kaggle should also get a lot from it.</p>
<p><a href="https://amzn.to/3n71bm5"><img src="https://i.imgur.com/TAncM6U.jpg" alt="Kaggle ni Idomu Deep Learning Programming no Gokui image"></a></p>
<p>The book, <a href="https://amzn.to/40PYaV1">Kaggle ni Idomu Deep Learning Programming no Gokui</a>, was given to me by one of its authors, <a href="https://upura.github.io/">Shotaro Ishihara</a>. Thank you.</p>
<hr>
<p>One of the good things about this book is that it is short. Excluding the index, it is about 200 pages. Many machine learning books are thick, so this one is nicely compact. Books that explain theory properly tend to become long because they need careful explanation, and bad books often become long because they explain difficult material in a confusing and redundant way.</p>
<p>This book explains things accurately and simply, so you can get a broad overview. Looking at the <a href="https://www.kspub.co.jp/book/detail/5305133.html">book's table of contents</a>, it covers many techniques needed not only for Kaggle but also for practical machine learning in general, and it explains why each technique is needed and when to use it. From chapter 3 onward, the book applies techniques and ways of thinking to actual Kaggle competition tasks and improves the score, so it is easy to see that the methods really have an effect. If you want more detail, you can follow the URLs and papers scattered throughout the text. It also gives you important keywords, so you can deepen your knowledge through search or other books.</p>
<p>The main models it covers are also models that are strong enough to consider early in practical work: gradient boosted decision trees, neural networks such as CNNs, RNNs, and Transformers, and linear models for ensembles. This focused selection is probably one reason the book does not become redundant.</p>
<p>If I had encountered this book when I first started learning machine learning, I could have learned about models and algorithms that are good in terms of performance without wandering too far off course, and I could have learned simply how to look at data and how to validate models. Beginners will not understand everything just by reading it lightly, but at the beginning you often do not even know which keywords matter. This book exposes you to many keywords that feel important and worth looking up when needed. That alone is valuable.</p>
<p>If I had encountered it when I started Kaggle, it would also have answered many of the questions that come up when beginning Kaggle competitions. Fortunately, my first Kaggle competition was a team effort, and much stronger teammates taught me the basics, so I was able to understand the overall flow. With this book, I think I could have grasped the flow of Kaggle and started my first competition with more understanding.</p>
<p>As I wrote at the beginning, this is a good, easy-to-understand book that compresses the essence of machine learning into a short index, beyond Kaggle itself. I hope many people read it.</p>]]></content:encoded>
  <pubDate>Sat, 29 Apr 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Sat, 29 Apr 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Similar embedding search with SVM: an alternative to kNN</title>
  <link>https://hotchpotch.dev/articles/svm-embedding-retriever/</link>
  <guid>https://hotchpotch.dev/articles/svm-embedding-retriever/</guid>
  <description>LangChain added an SVM Retriever implementation that searches for embeddings similar to a query embedding using SVM. I looked into how it works and compared kNN, SVM, and a hybrid search on AI News data.</description>
  <content:encoded><![CDATA[<p>LangChain v0.0.141 added an implementation called <a href="https://python.langchain.com/en/latest/modules/indexes/retrievers/examples/svm_retriever.html">SVM Retriever</a>. It finds the top-K embeddings similar to a single query embedding from a set of embeddings by using SVM. I wondered how that worked, looked into it, and found it interesting because it used an idea I did not know. This is a note about that.</p>
<h2>kNN vs SVM</h2>
<p>There is a notebook called <a href="https://github.com/karpathy/randomfun/blob/master/knn_vs_svm.ipynb">knn_vs_svm.ipynb</a>, which this implementation is based on. Machine-translating the beginning gives the following:</p>
<blockquote>
<p>A common workflow is to index some data based on embeddings, then, given a new query embedding, use k-Nearest Neighbor search to retrieve the most similar examples. For example, you could imagine embedding a large collection of papers based on their abstracts, then giving a new paper of interest and retrieving the most similar papers.</p>
<p>In my experience, if you have a little extra compute budget, using SVM instead of kNN always works better. Here is an example:</p>
</blockquote>
<p>k-nearest neighbors computes using Euclidean distance, but this approach uses SVM. The way SVM is used is interesting. Quoting from the notebook:</p>
<pre><code class="hljs language-python"><span class="hljs-comment"># Wired: use an SVM</span>
<span class="hljs-keyword">from</span> sklearn <span class="hljs-keyword">import</span> svm

<span class="hljs-comment"># create the "Dataset"</span>
x = np.concatenate([query[<span class="hljs-literal">None</span>,...], embeddings]) <span class="hljs-comment"># x is (1001, 1536) array, with query now as the first row</span>
y = np.zeros(<span class="hljs-number">1001</span>)
y[<span class="hljs-number">0</span>] = <span class="hljs-number">1</span> <span class="hljs-comment"># we have a single positive example, mark it as such</span>

<span class="hljs-comment"># train our (Exemplar) SVM</span>
<span class="hljs-comment"># docs: https://scikit-learn.org/stable/modules/generated/sklearn.svm.LinearSVC.html</span>
clf = svm.LinearSVC(class_weight=<span class="hljs-string">'balanced'</span>, verbose=<span class="hljs-literal">False</span>, max_iter=<span class="hljs-number">10000</span>, tol=<span class="hljs-number">1e-6</span>, C=<span class="hljs-number">0.1</span>)
clf.fit(x, y) <span class="hljs-comment"># train</span>

<span class="hljs-comment"># infer on whatever data you wish, e.g. the original data</span>
similarities = clf.decision_function(x)
sorted_ix = np.argsort(-similarities)
<span class="hljs-built_in">print</span>(<span class="hljs-string">"top 10 results:"</span>)
<span class="hljs-keyword">for</span> k <span class="hljs-keyword">in</span> sorted_ix[:<span class="hljs-number">10</span>]:
  <span class="hljs-built_in">print</span>(<span class="hljs-string">f"row <span class="hljs-subst">{k}</span>, similarity <span class="hljs-subst">{similarities[k]}</span>"</span>)
</code></pre>
<p>It labels only the target single embedding as <code>1</code> and all others as <code>0</code>, then trains LinearSVC as a classification task. The confidence score is obtained as a value around <code>1</code> to <code>-1</code>, and the top-K items closest to <code>1</code> are treated as the most similar items.</p>
<p>Instead of simple Euclidean distance, the score is computed while considering a space learned by SVM, so it does seem likely to produce better results. Using LinearSVC to compute that made me think, "I see." SVM Retriever is an abstraction that makes this method convenient to use in LangChain.</p>
<h2>Comparing kNN and SVM results</h2>
<p>Let's use 450 Japanese items from <a href="https://ai-news.dev/">AI News</a> and compare search results from kNN and SVM for a specific query.</p>
<pre><code>query:  生成AIと著作権
=== kNN ===
0.886: 生成AIの猛烈な進化と著作権制度～技術発展と著作権者の利益のバランスをとるには～ | STORIA法律事務所
0.880: スター・ウォーズやハリポタの人気キャラと話せるAIの「著作権問題」をどう考えるべきか | シリコンバレーの「生き字引」がズバリ指摘 | クーリエ・ジャポン
0.876: 生成AIの利用ガイドライン作成のための手引き | STORIA法律事務所
0.876: ダブスタクソイナゴは生成AIの法的議論に参加してくるんじゃねえ！！
0.874: 画像生成AI “クリエーターの権利脅かされる” 法整備など提言 | NHK | AI（人工知能）
0.870: 【AI】生成AIを利用する場合に気を付けなければならない著作権の知識｜福岡真之介｜note
0.868: AIイラストに規制を求める団体の理事「木目百二」氏が二次創作のガイドライン違反で支援サイトの作品全消し、謝罪に追い込まれる - Togetter
0.865: 生成AI「開発規制、望ましくない」　松本総務相 - 日本経済新聞
=== SVM ===
-0.305: 生成ＡＩで作品、それって著作権侵害？　福井健策弁護士に聞く：朝日新聞デジタル
-0.384: 生成AIの猛烈な進化と著作権制度～技術発展と著作権者の利益のバランスをとるには～ | STORIA法律事務所
-0.402: ダブスタクソイナゴは生成AIの法的議論に参加してくるんじゃねえ！！
-0.408: AIイラストに規制を求める団体の理事「木目百二」氏が二次創作のガイドライン違反で支援サイトの作品全消し、謝罪に追い込まれる - Togetter
-0.436: 画像生成AIによる作品の無許可使用を主張した写真家が逆に損害賠償を請求される - GIGAZINE
-0.479: アーティストのGrimes、生成AIで自分の声を自由に使っていいとツイート - ITmedia NEWS
-0.482: 生成AIの利用ガイドライン作成のための手引き | STORIA法律事務所
-0.483: スター・ウォーズやハリポタの人気キャラと話せるAIの「著作権問題」をどう考えるべきか | シリコンバレーの「生き字引」がズバリ指摘 | クーリエ・ジャポン
</code></pre>
<p>For the result above, both look reasonable at a glance. Let's try a slightly more difficult query.</p>
<pre><code>query:  大規模言語モデルを低スペックのマシンで動かしたい
=== kNN ===
0.872: RWKV14Bを日本語AlpacaデータセットでLoRAして公開しました(ご家庭で動く!?)｜shi3z｜note
0.861: チャットAIをブラウザのWebGPUだけで実行でき日本語も使用できる「Web LLM」、実際に試してみる方法はこんな感じ - GIGAZINE
0.855: LLMをアプリ開発に統合するSDK「Semantic Kernel」がPythonに対応、TypeScriptへの対応も検討中|CodeZine（コードジン）
0.853: ChatGPT対抗のオープンソース言語モデル「StableLM」。日本語版も？  - PC Watch
0.851: “画像の面白さ”を解説できるAI「MiniGPT-4」　写真からラップや詩、料理レシピ作成　デモサイトも公開中：Innovative Tech（1/2 ページ） - ITmedia NEWS
0.850: チャットAI「StableLM」発表　オープンソースモデルで商用可　「Stable Diffusion」開発元から - ITmedia NEWS
0.849: Googleの大規模言語モデル「Bard」、日本でも利用可能に。英語のみだが、改良されたPaLMベース | テクノエッジ TechnoEdge
0.849: Stability AIがオープンソースで商用利用も可能な大規模言語モデル「StableLM」をリリース - GIGAZINE
=== SVM ===
-0.359: 大規模言語モデルを自社でトレーニング＆活用する方法｜mah_lab / 西見 公宏｜note
-0.366: 大規模言語モデル間の性能比較まとめ｜mah_lab / 西見 公宏｜note
-0.451: 深層学習コンパイラスタックと最適化
-0.456: LLMをアプリ開発に統合するSDK「Semantic Kernel」がPythonに対応、TypeScriptへの対応も検討中|CodeZine（コードジン）
-0.471: dolly-v2-12bという120億パラメータの言語モデルを使ってみた!｜Masayuki Abe｜note
-0.490: Googleの大規模言語モデル「Bard」、日本でも利用可能に。英語のみだが、改良されたPaLMベース | テクノエッジ TechnoEdge
-0.504: RWKV14Bを日本語AlpacaデータセットでLoRAして公開しました(ご家庭で動く!?)｜shi3z｜note
-0.510: Webブラウザ上で3D/2Dモデルをぬるぬる動かせる「Babylon.js 6」正式版に。レンダリング性能が最大50倍、WASM化した物理演算エンジン搭載、液体のレンダリングも － Publickey
</code></pre>
<p>Depending on the query, the results can differ quite a bit. I also implemented a hybrid search that ensembles kNN and SVM results, so let's look at that.</p>
<pre><code>query:  大規模言語モデルを低スペックのマシンで動かしたい
=== kNN ===
-3.816: RWKV14Bを日本語AlpacaデータセットでLoRAして公開しました(ご家庭で動く!?)｜shi3z｜note
-3.527: チャットAIをブラウザのWebGPUだけで実行でき日本語も使用できる「Web LLM」、実際に試してみる方法はこんな感じ - GIGAZINE
-2.920: LLMをアプリ開発に統合するSDK「Semantic Kernel」がPythonに対応、TypeScriptへの対応も検討中|CodeZine（コードジン）
-2.591: ChatGPT対抗のオープンソース言語モデル「StableLM」。日本語版も？  - PC Watch
-2.436: “画像の面白さ”を解説できるAI「MiniGPT-4」　写真からラップや詩、料理レシピ作成　デモサイトも公開中：Innovative Tech（1/2 ページ） - ITmedia NEWS
=== SVM ===
-3.923: 大規模言語モデルを自社でトレーニング＆活用する方法｜mah_lab / 西見 公宏｜note
-3.865: 大規模言語モデル間の性能比較まとめ｜mah_lab / 西見 公宏｜note
-3.140: 深層学習コンパイラスタックと最適化
-3.097: LLMをアプリ開発に統合するSDK「Semantic Kernel」がPythonに対応、TypeScriptへの対応も検討中|CodeZine（コードジン）
-2.962: dolly-v2-12bという120億パラメータの言語モデルを使ってみた!｜Masayuki Abe｜note
=== Hybrid ===
-3.869: 大規模言語モデルを自社でトレーニング＆活用する方法｜mah_lab / 西見 公宏｜note
-3.102: RWKV14Bを日本語AlpacaデータセットでLoRAして公開しました(ご家庭で動く!?)｜shi3z｜note
-2.913: 大規模言語モデル間の性能比較まとめ｜mah_lab / 西見 公宏｜note
-2.844: LLMをアプリ開発に統合するSDK「Semantic Kernel」がPythonに対応、TypeScriptへの対応も検討中|CodeZine（コードジン）
-2.558: チャットAIをブラウザのWebGPUだけで実行でき日本語も使用できる「Web LLM」、実際に試してみる方法はこんな感じ - GIGAZINE
</code></pre>
<p>This looks better to me. Since it is easy to try, using SVM in addition to kNN search or similarity search seems like a reasonable option. Of course kNN is overwhelmingly faster, but if SVM can be used at practical speed, it seems useful.</p>
<h2>Extra code</h2>
<p><code>embs</code> must be created separately as an array of embeddings. <code>texts</code> is paired data for <code>embs</code>. LangChain's SVM Retriever makes this easier, but it does not expose scores, so I implemented it myself.</p>
<pre><code class="hljs language-python"><span class="hljs-comment"># base: https://github.com/karpathy/randomfun/blob/master/knn_vs_svm.ipynb</span>
<span class="hljs-keyword">from</span> sklearn <span class="hljs-keyword">import</span> svm
<span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np
<span class="hljs-keyword">from</span> langchain.embeddings <span class="hljs-keyword">import</span> OpenAIEmbeddings

<span class="hljs-keyword">def</span> <span class="hljs-title function_">knn_top_k</span>(<span class="hljs-params">query_emb, embs, k=<span class="hljs-number">10</span></span>):
    l2_embs = embs / np.sqrt((embs**<span class="hljs-number">2</span>).<span class="hljs-built_in">sum</span>(<span class="hljs-number">1</span>, keepdims=<span class="hljs-literal">True</span>))
    l2_query = query_emb / np.sqrt((query_emb**<span class="hljs-number">2</span>).<span class="hljs-built_in">sum</span>())
    similarities = l2_embs.dot(l2_query)
    sorted_index = np.argsort(-similarities)
    res_index = sorted_index[<span class="hljs-number">1</span>:k+<span class="hljs-number">1</span>]
    <span class="hljs-keyword">return</span> res_index, similarities[res_index], -similarities

<span class="hljs-keyword">def</span> <span class="hljs-title function_">svm_top_k</span>(<span class="hljs-params">query_emb, embs, k=<span class="hljs-number">10</span></span>):
    X = np.concatenate([query_emb[<span class="hljs-literal">None</span>, ...], embs])
    y = np.zeros(X.shape[<span class="hljs-number">0</span>])
    y[<span class="hljs-number">0</span>] = <span class="hljs-number">1</span>
    clf = svm.LinearSVC(class_weight=<span class="hljs-string">'balanced'</span>, verbose=<span class="hljs-literal">False</span>, max_iter=<span class="hljs-number">10000</span>, tol=<span class="hljs-number">1e-6</span>, C=<span class="hljs-number">0.1</span>)
    clf.fit(X, y)
    similarities = clf.decision_function(X)
    sorted_index = np.argsort(-similarities)
    res_index = sorted_index[<span class="hljs-number">1</span>:k+<span class="hljs-number">1</span>] - <span class="hljs-number">1</span>
    <span class="hljs-keyword">return</span> res_index, similarities[res_index + <span class="hljs-number">1</span>], -similarities[<span class="hljs-number">1</span>:]

<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_query_emb</span>(<span class="hljs-params">text</span>):
    emb = OpenAIEmbeddings().embed_query(text) <span class="hljs-comment"># type: ignore</span>
    <span class="hljs-keyword">return</span> np.array(emb)

<span class="hljs-keyword">def</span> <span class="hljs-title function_">join_colon</span>(<span class="hljs-params">num_list_a, list_b</span>):
    <span class="hljs-keyword">return</span> [<span class="hljs-string">f'<span class="hljs-subst">{a:<span class="hljs-number">.3</span>f}</span>: <span class="hljs-subst">{b}</span>'</span> <span class="hljs-keyword">for</span> a, b <span class="hljs-keyword">in</span> <span class="hljs-built_in">zip</span>(num_list_a, list_b)]

<span class="hljs-keyword">def</span> <span class="hljs-title function_">knn_svm</span>(<span class="hljs-params">text, embs, texts, k=<span class="hljs-number">5</span></span>):
    query_emb = get_query_emb(text)
    knn_index, knn_similarities, _ = knn_top_k(query_emb, embs, k)
    svm_index, svm_similarities, _ = svm_top_k(query_emb, embs, k)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'query: '</span>, text)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'=== kNN ==='</span>)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"\n"</span>.join(join_colon(knn_similarities, texts[knn_index])))
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'=== SVM ==='</span>)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"\n"</span>.join(join_colon(svm_similarities, texts[svm_index])))

<span class="hljs-keyword">def</span> <span class="hljs-title function_">hyblid_knn_svm</span>(<span class="hljs-params">text_or_emb, embs, texts, k=<span class="hljs-number">5</span></span>):
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">isinstance</span>(text_or_emb, <span class="hljs-built_in">str</span>):
        query_emb = get_query_emb(text_or_emb)
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'query: '</span>, text_or_emb) <span class="hljs-comment"># type: ignore</span>
    <span class="hljs-keyword">else</span>:
        query_emb = text_or_emb
    <span class="hljs-comment"># 全件取得する</span>
    knn_index, knn_similarities, knn_all_scores = knn_top_k(query_emb, embs, embs.shape[<span class="hljs-number">0</span>])
    svm_index, svm_similarities, svm_all_scores = svm_top_k(query_emb, embs, embs.shape[<span class="hljs-number">0</span>])
    
    <span class="hljs-comment"># score を正規化する</span>
    knn_score_normalized = (knn_all_scores - np.mean(knn_all_scores)) / np.std(knn_all_scores)
    svm_score_normalized = (svm_all_scores - np.mean(svm_all_scores)) / np.std(svm_all_scores)

    <span class="hljs-comment"># それぞれのスコアを足し合わせて、ハイブリッドなスコアを作る</span>
    hybrid_similarities = (knn_score_normalized + svm_score_normalized) / <span class="hljs-number">2</span>
    hybrid_index = np.argsort(hybrid_similarities)[:k]

    <span class="hljs-built_in">print</span>(<span class="hljs-string">'=== kNN ==='</span>)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"\n"</span>.join(join_colon(np.sort(knn_score_normalized)[:k], texts[knn_index][:k])))
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'=== SVM ==='</span>)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"\n"</span>.join(join_colon(np.sort(svm_score_normalized)[:k], texts[svm_index][:k])))
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'=== Hybrid ==='</span>)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"\n"</span>.join(join_colon(hybrid_similarities[hybrid_index][:k], texts[hybrid_index][:k])))
</code></pre>]]></content:encoded>
  <pubDate>Sat, 29 Apr 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Sat, 29 Apr 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Launching AI News and how I used OpenAI behind it</title>
  <link>https://hotchpotch.dev/articles/ainews/</link>
  <guid>https://hotchpotch.dev/articles/ainews/</guid>
  <description>I launched AI News, a site that collects AI, data science, and machine learning topics and summarizes them into three lines with AI. This article describes why I built it and how I used OpenAI APIs for classification and summarization.</description>
  <content:encoded><![CDATA[<p>I <a href="https://ai-news.dev/">launched</a> a site called AI News. It collects topics related to AI, data science, and machine learning, summarizes them into three lines with AI, and publishes them. It is also available through Twitter <a href="https://twitter.com/AINewsDev">@AINewsDev</a> and an <a href="https://ai-news.dev/feeds/">Atom feed</a>. I have been running it for a few days, and although it is my own site, it has already been useful for collecting information. English articles are summarized in Japanese too, which is convenient.</p>
<p><a href="https://ai-news.dev/"><img src="https://i.imgur.com/8DjLzeE.png" alt="ainews"></a></p>
<h2>Why I built it</h2>
<p>I used to read ML-related information conveniently through <a href="https://www.machine-learning.news/">ML-News</a>, made by <a href="https://twitter.com/syou6162">@syou6162</a>. Around the time Twitter API pricing became an issue, it seems to have become unavailable.</p>
<p>That made it harder to follow data science and machine learning topics, and I had been thinking about building a similar site someday. Then ChatGPT, or GPT-4, appeared as an actually useful LLM. As everyone knows, topics around AI then exploded. There was too much information to read, but I still wanted to read the things I cared about properly. It would be useful to look at a reliable overview first and then decide whether to read the full article. The <code>description</code> written in an article's HTML is often only the first hundred characters or so, so it is not enough for that judgment. Then I realized that this was exactly the kind of thing an LLM such as ChatGPT could do, so I built it.</p>
<h2>Implementation notes</h2>
<p>Web scraping, article body extraction, and the website implementation are ordinary web development topics, so I will leave them aside for now and focus first on ChatGPT and the OpenAI API.</p>
<h3>Article classification</h3>
<p>I needed an implementation that takes scraped web articles and decides whether each article is related to AI. If you do machine learning, this sounds easy when correct labels exist, especially for binary classification of AI-related or not. The problem is that creating correct labels is tedious. I wanted AI itself to judge the articles instead of doing all of it by hand.</p>
<p>So I first asked GPT-3.5 to label the data. However, asking it to score how AI-like a topic is on a numeric scale from 0.0 to 1.0 was surprisingly unstable. I tried hard to write prompts that would make the output look like a probability distribution, but my prompting ability was not enough. What I wanted was a softmax-like probability distribution, so instead of describing it in prose, I wrote more directly that the values should be as if passed through a softmax function. That worked better. The final prompt is here:</p>
<ul>
<li><a href="https://gist.github.com/hotchpotch/8cb74d7a2ed1730faf1ec1ba089f93cf">https://gist.github.com/hotchpotch/8cb74d7a2ed1730faf1ec1ba089f93cf</a></li>
</ul>
<p>I made it evaluate multiple AI-like categories and an "Others" category. When I fed it the roughly 400-character summaries described later, I often got output like this. Each value is between 0.0 and 1.0, and the total is 1.0, so it feels softmax-like.</p>
<pre><code>{
    "AI": 0.0,
    "Machine Learning": 0.0,
    "Data Science": 0.0,
    "Data Analysis": 0.0,
    "Statistics": 0.8,
    "Deep Learning": 0.0,
    "kaggle": 0.0,
    "ChatGPT": 0.0,
    "MLOps": 0.0,
    "Generative AI": 0.0,
    "LLM": 0.0,
    "Others": 0.2
}
</code></pre>
<p>But sometimes it produced output like this. The values are between 0.0 and 1.0, but the total is greater than 1.0. What happened to softmax?</p>
<pre><code>{
    "AI": 0.5,
    "Machine Learning": 1.0,
    "Data Science": 1.0,
    "Data Analysis": 1.0,
    "Statistics": 0.5,
    "Deep Learning": 0.0,
    "kaggle": 0.0,
    "ChatGPT": 0.8,
    "MLOps": 0.0,
    "Generative AI": 0.0,
    "LLM": 0.0,
    "Others": 0.2
}
</code></pre>
<p>If I pass that output through an actual softmax function, I get this. The values form a 0.0 to 1.0 distribution and sum to 1.0, so I can use this.</p>
<pre><code>{
    "AI": 0.08285351386643752,
    "Machine Learning": 0.13660235066384355,
    "Data Science": 0.13660235066384355,
    "Data Analysis": 0.13660235066384355,
    "Statistics": 0.08285351386643752,
    "Deep Learning": 0.05025319642492017,
    "kaggle": 0.05025319642492017,
    "ChatGPT": 0.11184054543123119,
    "MLOps": 0.05025319642492017,
    "Generative AI": 0.05025319642492017,
    "LLM": 0.05025319642492017,
    "Others": 0.06137939271976228
}
</code></pre>
<p>Using this data, I treated items where "Others" was the highest score as non-AI and then checked the labels by hand. After correcting wrong labels by hand, I had N=550 labels: 200 AI-related and 350 others. Compared with the labels before manual correction, the accuracy was about 94%. That is quite high. The labeling is biased because I checked likely mistakes based on the AI output, but even so, the accuracy was good. Many of the mistakes were also borderline cases. I could probably improve it further by tuning the prompt or using GPT-4, but the goal was to create correct labels for training a classifier, and that was achieved, so I considered this good enough for now.</p>
<p>I listed many categories because when I wrote the task as something like "machine learning, AI, data science, or anything else", GPT-3.5's output felt less stable.</p>
<p>I corrected labels by hand while looking at a screen like this. It was much easier than labeling everything from scratch, although still tedious.</p>
<p><img src="https://i.imgur.com/UZx5s6S.png" alt="Manual correction"></p>
<h3>Building a classifier</h3>
<p>Creating 550 correct labels was manageable, so next I built a classifier to decide whether an article is AI-related. For feature generation, I used OpenAI's Embeddings API, <code>text-embedding-ada-002</code>, to convert article bodies into 1536-dimensional vectors. The price per 1K tokens is also 20% of <code>gpt-3.5-turbo</code>, which is nice.</p>
<p>According to <a href="https://openai.com/blog/new-and-improved-embedding-model">OpenAI's blog</a>, <code>text-similarity-davinci-001</code> seems to be more accurate for classification prediction. Still, I want to use embeddings for various things in the future, so I chose <code>text-embedding-ada-002</code> for its generality.</p>
<p>Now I had 1536-dimensional features, so I split the labeled data into train, validation, and test sets and built a classifier. This time I used lightGBM, familiar to Kagglers. It had been several months since I last used lightGBM, and reading the documentation while implementing it felt bothersome, so I asked ChatGPT. It quickly produced working code, which surprised me. I was able to use it almost as-is.</p>
<ul>
<li><a href="https://gist.github.com/hotchpotch/81cf130279f4df9aeccd20e51678cff4">https://gist.github.com/hotchpotch/81cf130279f4df9aeccd20e51678cff4</a></li>
</ul>
<p>The code in that gist splits the data into 80% train, 10% validation, and 10% test, but because the final amount of data was not large, I adjusted it to 60% train, 30% validation, and 10% test. The trained model achieved validation accuracy <code>0.987</code> and test accuracy <code>1.0</code>. Test accuracy of 100%! Of course the test data is only about 55 items, so it may be chance. When I changed the random seed casually, accuracy ranged from 0.96 to 1.0. Even with non-fine-tuned <code>text-embedding-ada-002</code> features, the score was very good for a classification task. For NLP classification with only 330 training samples, that is impressive.</p>
<p>This completed the classifier for deciding whether an article is AI-related. Since then I have added various data sources, so at the moment some non-AI articles occasionally slip through and get displayed. I plan to retrain the classifier later and make it smarter.</p>
<h2>Creating article summaries</h2>
<p>For article summaries, if money were no issue, asking <code>gpt-4</code> to summarize the whole article would be the most accurate. But the token cost is 15 times higher than <code>gpt-3.5-turbo</code>. Fifteen times. That is a lot for a hobby project, so I wanted to keep the cost as low as possible.</p>
<p>I asked GPT-4 and GPT-3.5 to summarize roughly the first 4K tokens of article text into about 400 Japanese characters and compared the results subjectively. GPT-4 produced better summaries, but they did not feel overwhelmingly better than GPT-3.5. Considering cost and processing time, I first use GPT-3.5 to summarize the first roughly 4K tokens into about 400 characters.</p>
<p>The difference between GPT-4 and GPT-3.5 became clearer when compressing the information further. When I reduced summaries to around 80 characters for Twitter posts, GPT-4 was much better. When the prompt specified a constraint such as "around 80 characters in Japanese", GPT-4 followed the constraint much more closely. GPT-3.5 sometimes produced much longer text, so GPT-4's ability to respect the character-limit constraint was valuable for Twitter posting.</p>
<p>GPT-4 is also much better at handling several tasks in one prompt. If I ask it in one request to create "around 80 characters", "around 80 characters in a casual style", "a three-emoji summary", and "a three-line summary", GPT-4 almost always does it. GPT-3.5 seems to struggle with doing multiple tasks in a single run.</p>
<p>At the moment I use this prompt:</p>
<ul>
<li><a href="https://gist.github.com/hotchpotch/427b2c24a1368a6f54d79d3f282c9445">https://gist.github.com/hotchpotch/427b2c24a1368a6f54d79d3f282c9445</a></li>
</ul>
<p>Running that prompt through GPT-4 gives results for multiple tasks like this:</p>
<pre><code>{
  "Bullets": ["約2650チーム中15位で金メダル獲得", "Kaggle Competitions Masterの称号取得", "CV・LB相関が観測できず、最終結果は大幅なshake予想"],
  "Summary": "Kaggleのコンペティションで15位の金メダルを獲得し、Kaggle Competitions Masterの称号を手に入れた。",
  "SummaryEmojis": "🏆Kaggleのコンペで15位の金メダル🥇を獲得し、Kaggle Competitions Master👑の称号を手に入れた🎉",
  "Emojis": "🥇👑🎉"
}
</code></pre>
<p>So summary generation is split into two steps:</p>
<ul>
<li>First, summarize to about 400 characters with GPT-3.5 to save money and time.</li>
<li>Then, create multiple shorter summaries from that 400-character summary with GPT-4. This costs more money and time, but the quality is higher.</li>
</ul>
<p>As people often say, prompts written in English generally produced better output than prompts written in Japanese. I am not good at English, so I translated my prompts with DeepL, but even that produced better results in English.</p>
<h2>Ordinary web development</h2>
<p>In addition to the OpenAI and machine learning work described above, I also implemented the following. This part took around 70% of the total development time, which is about what I expected.</p>
<ul>
<li>Scrapers for various sites in Python</li>
<li>Saving data to the backend database and related systems in Python</li>
<li>Batch job implementation</li>
<li>Website implementation with Next.js, TypeScript, and Chakra UI</li>
</ul>
<p>I wrote much of this implementation based on code generated by the ChatGPT 3.5 and 4 web UI. For Python code, which I also write in normal work, I sometimes felt it would have been faster to write it myself. Still, it was very useful for small pieces of implementation, such as writing a simple function or a regular expression.</p>
<p>It had been a long time since I used Next.js and TypeScript, and Chakra UI was new to me, so ChatGPT-generated code was especially useful there because I had less knowledge. However, the Next.js code was probably based on version 11 or 12 from ChatGPT's training data rather than the current version 13, so it sometimes produced deprecated structures. That is part of the charm.</p>
<p>ChatGPT 4.0 produced higher-quality code, but it was slower, so I mostly used 3.5 for small code generation. I used 4.0 when 3.5 looked suspicious or when I needed to include many conditions in the code. Use the right tool for the job.</p>
<p>Even with the current GPT-3.5 and 4.0, better VS Code integration alone would make development much more convenient. If code generation gets smarter over the next one or two years, development where I define requirements, review diffs, press y/N, and occasionally give feedback starts to feel realistic.</p>
<h2>The future of AI News</h2>
<p>For now I have only built the minimum necessary pieces, so I plan to keep improving it bit by bit. It is a personal sandbox, and it is also a website I can use conveniently myself, so maintaining it is fun in the way tending a bonsai might be, although I have never actually tended one. The article embeddings are currently used only for binary classification, but they should be useful for many other things too. I expect I will keep tinkering with it for a while.</p>]]></content:encoded>
  <pubDate>Mon, 24 Apr 2023 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 24 Apr 2023 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>RAPIDS SVR and SVC: fast training without fine-tuning, evaluated on MARC-ja</title>
  <link>https://hotchpotch.dev/articles/rapids-svr-svc-marc-ja/</link>
  <guid>https://hotchpotch.dev/articles/rapids-svr-svc-marc-ja/</guid>
  <description>An introduction to RAPIDS SVR and SVC, using neural-network embeddings as features without fine-tuning and evaluating the approach on the Japanese MARC-ja classification dataset.</description>
  <content:encoded><![CDATA[<p>I learned about RAPIDS SVR and SVC in the Kaggle competition <a href="https://www.kaggle.com/competitions/feedback-prize-english-language-learning">Feedback Prize - English Language Learning</a>. They train quickly, and I felt they were useful methods for regression and classification tasks, so I will introduce what they are. In fact, top solutions in that competition used RAPIDS SVR.</p>
<p>I will also use RAPIDS SVC to evaluate <a href="https://github.com/yahoojapan/JGLUE#marc-ja">MARC-ja</a>, the classification dataset in the Japanese evaluation benchmark <a href="https://github.com/yahoojapan/JGLUE">JGLUE</a>. The implementation used for the evaluation is available on <a href="https://github.com/hotchpotch/rapids-svr-svc-marc_ja">GitHub</a>.</p>
<p>This article was written for day 13 of the <a href="https://qiita.com/advent-calendar/2022/kaggle">Kaggle Advent Calendar 2022</a>.</p>
<h2>What are SVR and SVC?</h2>
<p>SVR is Support Vector Regression, and SVC is Support Vector Classification. The algorithm behind them is SVM, or Support Vector Machine, which is known for strong accuracy and was apparently very popular at one point. sklearn also has an implementation, so many people have probably used it.</p>
<p>However, as the <a href="https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVR.html">sklearn documentation</a> says:</p>
<blockquote>
<p>The implementation is based on libsvm. The fit time complexity is more than quadratic with the number of samples which makes it hard to scale to datasets with more than a couple of 10000 samples.</p>
</blockquote>
<p>In other words, with sklearn's libsvm-based implementation, scaling past around ten thousand samples is not very realistic.</p>
<h2>What are RAPIDS SVR and SVC?</h2>
<p>RAPIDS SVR and SVC are SVM implementations in <a href="https://github.com/rapidsai/cuml">cuML</a>, which is part of <a href="https://rapids.ai/">RAPIDS</a>, NVIDIA's project for GPU-accelerated data science. Roughly speaking, cuML implements general-purpose machine learning algorithms similar to those in sklearn, follows sklearn's <a href="https://scikit-learn.org/stable/developers/develop.html">estimator API</a> such as <code>fit()</code> and <code>transform()</code>, and optimizes them to run on CUDA. <a href="https://github.com/rapidsai/cuml/blob/branch-0.14/notebooks/tools/cuml_benchmarks.ipynb">According to its benchmarks, it is 10 to 50 times faster than sklearn</a>. That means algorithms that are difficult to run at practical speed in sklearn can become practical with cuML. RAPIDS also includes other CUDA-based tools, such as <a href="https://github.com/rapidsai/cudf">cuDF</a> for fast DataFrame operations, so it is worth looking at the rest of the project if you are interested.</p>
<p>What becomes useful when SVR can run quickly? One answer is that training on the embedding representation from a neural network output layer becomes practical. You can take an existing public model, use it only for feature extraction without fine-tuning, and train SVR on those features. It is also easy to combine features from multiple models and train on the concatenated features. You can use non-fine-tuned models this way, but fine-tuned models can also be used as feature extractors.</p>
<blockquote>
<p><img src="https://raw.githubusercontent.com/cdeotte/Kaggle_Images/main/Sep-2022/svr.png" alt="RAPIDS SVR">
--Quoted from <a href="https://www.kaggle.com/competitions/feedback-prize-english-language-learning/discussion/351577">RAPIDS SVR starter kit</a></p>
</blockquote>
<h2>Extracting features from neural networks</h2>
<p>How should we extract features from a neural network? As an example, I will describe encoder models from Hugging Face Transformers. For most encoder models, you can either take the CLS token from <code>last_hidden_state</code> or apply mean pooling. The resulting vectors are then normalized before use.</p>
<pre><code>class MeanPooling(nn.Module):
    def __init__(self, eps=1e-6):
        super(MeanPooling, self).__init__()
        self.eps = eps

    def forward(
        self, outputs: torch.Tensor, attention_mask: torch.Tensor
    ) -> torch.Tensor:
        last_hidden_state = outputs[0]
        input_mask_expanded = (
            attention_mask.unsqueeze(-1).expand(last_hidden_state.size()).float()
        )
        sum_embeddings = torch.sum(last_hidden_state * input_mask_expanded, 1)
        sum_mask = input_mask_expanded.sum(1)
        sum_mask = torch.clamp(sum_mask, min=self.eps)
        mean_embeddings = sum_embeddings / sum_mask
        return mean_embeddings

class ClsPooling(nn.Module):
    # 実際は Pooling ではなくただの CLS を取り出しているだけなので、このクラス名は良くない…
    def __init__(self):
        super(ClsPooling, self).__init__()

    def forward(
        self, outputs: torch.Tensor, attention_mask: torch.Tensor
    ) -> torch.Tensor:
        last_hidden_state = outputs[0]
        return last_hidden_state[:, 0, :]

POOLING_CLASSES = {
    "mean": MeanPooling,
    "cls": ClsPooling,
}

class TransformerEmbsModel(torch.nn.Module):
    def __init__(self, model_name: str, pooling: str = "mean"):
        super().__init__()
        self.model = AutoModel.from_pretrained(model_name)
        self.pool = POOLING_CLASSES[pooling]()

    def feature(self, inputs: dict[str, torch.Tensor]) -> torch.Tensor:
        outputs = self.model(**inputs)
        sentence_embeddings = self.pool(outputs, inputs["attention_mask"])
        # Normalize the embeddings
        sentence_embeddings = F.normalize(sentence_embeddings, p=2, dim=1)
        sentence_embeddings = sentence_embeddings.squeeze(0)
        return sentence_embeddings

    def forward(self, inputs: dict[str, torch.Tensor]) -> torch.Tensor:
        embs = self.feature(inputs)
        return embs
</code></pre>
<p>This is enough to extract features.</p>
<h2>Training with RAPIDS SVC</h2>
<p>After that, we only need to train with SVC. SVR works with almost the same code.</p>
<pre><code>from cuml.svm import SVC
import numpy as np

DEFAULT_SVC_PARAMS = {
    "C": 3.0,  # Penalty parameter C of the error term.
    "kernel": "rbf",  # Possible options: ‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’.
    "degree": 3,
    "gamma": "scale",  # auto or scale
    "coef0": 0.0,
    "tol": 0.001,  # 0.001 = 1e-3
}

def train_svc(
    X: np.ndarray,
    y: np.ndarray,
    svc_params: dict[str, object] = DEFAULT_SVC_PARAMS,
    probability: bool = True,
) -> SVC:
    svc = SVC(**svc_params)
    svc.probability = probability
    svc.fit(X, y)
    return svc
</code></pre>
<p>The core is almost just this.</p>
<h2>Measuring the score on MARC-ja</h2>
<p>Now let's evaluate <a href="https://github.com/yahoojapan/JGLUE#marc-ja">MARC-ja</a>, the classification dataset in the Japanese evaluation benchmark <a href="https://github.com/yahoojapan/JGLUE">JGLUE</a>. MARC-ja is a binary positive/negative sentiment classification dataset built from Japanese Amazon reviews. It has 187,528 train samples and 5,654 dev, or validation, samples. That is a reasonably large dataset. The test data does not seem to be publicly available at the moment.</p>
<p>JGLUE's GitHub page lists dev accuracy scores. For example, <a href="https://huggingface.co/cl-tohoku/bert-base-japanese-v2">cl-tohoku/bert-base-japanese-v2</a> gets <code>0.958</code> after four epochs. The top score shown there is <code>0.964</code> from XLM-RoBERTa large.</p>
<p>Training time is also interesting. When I ran training casually on <a href="https://colab.research.google.com/drive/1Y3AmgAzypLEmGLGyp7lheg1td5RtEOpN">Colab with a T4 GPU</a>, one epoch of bert-base-japanese-v2 took about 100 minutes, with <code>0.9573</code> accuracy after the first epoch. On my local RTX 4090, one epoch took about 30 minutes.</p>
<h3>Feature extraction and SVC training</h3>
<ul>
<li><a href="https://github.com/hotchpotch/rapids-svr-svc-marc_ja">https://github.com/hotchpotch/rapids-svr-svc-marc_ja</a></li>
</ul>
<p>In the repository above, I implemented feature extraction from a neural network on MARC-ja and training with RAPIDS SVC. Let's first train and evaluate SVC using <code>cl-tohoku/bert-base-japanese-v2</code> without fine-tuning. The execution times below are from my local RTX 4090.</p>
<pre><code>$ python lib/runner.py bert-base-ja-v2-cls
[create cache] tmp/embs_cache/bert-base-ja-v2-cls.pkl.gz
100%|███████████████████████████████████████████████████████████████████████| 5861/5861 [06:04&#x3C;00:00, 16.09it/s]
100%|█████████████████████████████████████████████████████████████████████████| 177/177 [00:10&#x3C;00:00, 16.45it/s]
exec time: 394.05 sec
shape: (187528, 768) (5654, 768)
concat embs: (187528, 768) (5654, 768)
[train svc]
svc exec time: 17.83 sec
==================================================
bert-base-ja-v2-cls
valid acc score: 0.927661832331093
==================================================
              precision    recall  f1-score   support

    positive    0.89788   0.56691   0.69500       822
    negative    0.93067   0.98903   0.95896      4832

    accuracy                        0.92766      5654
   macro avg    0.91428   0.77797   0.82698      5654
weighted avg    0.92590   0.92766   0.92059      5654
</code></pre>
<p>Feature extraction took 394 seconds, SVC training took about 18 seconds, and the accuracy was <code>0.92766</code>. Once features are extracted, my implementation reuses them as a cache, so the second run costs almost only the SVC training time.</p>
<p>Next, let's look at the same model with mean pooling instead of CLS.</p>
<pre><code>$ python lib/runner.py bert-base-ja-v2-mean
[load cache] tmp/embs_cache/bert-base-ja-v2-mean.pkl.gz
shape: (187528, 768) (5654, 768)
concat embs: (187528, 768) (5654, 768)
[train svc]
svc exec time: 18.44 sec
==================================================
bert-base-ja-v2-mean
valid acc score: 0.9324372125928546
==================================================
              precision    recall  f1-score   support

    positive    0.91667   0.58881   0.71704       822
    negative    0.93406   0.99089   0.96164      4832

    accuracy                        0.93244      5654
   macro avg    0.92536   0.78985   0.83934      5654
weighted avg    0.93153   0.93244   0.92608      5654
</code></pre>
<p>I had already run this before, so the features were loaded from cache and only SVC training was needed. Accuracy was <code>0.93244</code>, so mean pooling worked better than CLS. What happens if we train on both sets of features?</p>
<pre><code>$ python lib/runner.py bert-base-ja-v2-cls bert-base-ja-v2-mean
[load cache] tmp/embs_cache/bert-base-ja-v2-cls.pkl.gz
shape: (187528, 768) (5654, 768)
[load cache] tmp/embs_cache/bert-base-ja-v2-mean.pkl.gz
shape: (187528, 768) (5654, 768)
concat embs: (187528, 1536) (5654, 1536)
[train svc]
svc exec time: 30.04 sec
==================================================
bert-base-ja-v2-cls + bert-base-ja-v2-mean
valid acc score: 0.9334984082065794
==================================================
              precision    recall  f1-score   support

    positive    0.90545   0.60584   0.72595       822
    negative    0.93652   0.98924   0.96216      4832

    accuracy                        0.93350      5654
   macro avg    0.92099   0.79754   0.84405      5654
weighted avg    0.93200   0.93350   0.92782      5654
</code></pre>
<p>Because both feature sets were already cached, loading was almost instant, and SVC training took about 30 seconds. The result was <code>0.93350</code>. Even with the same neural network model, extracting CLS and mean-pooled features separately and training on them together improved the score by about <code>0.001</code>.</p>
<p>How about classic TF-IDF? TF-IDF features have too many dimensions as-is, so I reduced them to 1000 dimensions with SVD and then trained and evaluated SVC.</p>
<pre><code>$ python lib/runner.py tfidf
[load cache] tmp/embs_cache/tfidf.pkl.gz
shape: (187528, 1000) (5654, 1000)
concat embs: (187528, 1000) (5654, 1000)
[train svc]
svc exec time: 55.78 sec
==================================================
tfidf
valid acc score: 0.8924655111425539
==================================================
              precision    recall  f1-score   support

    positive    0.81657   0.33577   0.47586       822
    negative    0.89729   0.98717   0.94009      4832

    accuracy                        0.89247      5654
   macro avg    0.85693   0.66147   0.70797      5654
weighted avg    0.88556   0.89247   0.87260      5654
</code></pre>
<p>Accuracy was <code>0.89247</code>, which is not very good. For text with many unseen words, this is probably about what we should expect. Then what happens if we combine TF-IDF with BERT features?</p>
<pre><code>$ python lib/runner.py bert-base-ja-v2-cls bert-base-ja-v2-mean tfidf
[load cache] tmp/embs_cache/bert-base-ja-v2-cls.pkl.gz
shape: (187528, 768) (5654, 768)
[load cache] tmp/embs_cache/bert-base-ja-v2-mean.pkl.gz
shape: (187528, 768) (5654, 768)
[load cache] tmp/embs_cache/tfidf.pkl.gz
shape: (187528, 1000) (5654, 1000)
concat embs: (187528, 2536) (5654, 2536)
[train svc]
svc exec time: 53.41 sec
==================================================
bert-base-ja-v2-cls + bert-base-ja-v2-mean + tfidf
valid acc score: 0.9379200565970994
==================================================
              precision    recall  f1-score   support

    positive    0.92280   0.62530   0.74547       822
    negative    0.93957   0.99110   0.96465      4832

    accuracy                        0.93792      5654
   macro avg    0.93119   0.80820   0.85506      5654
weighted avg    0.93713   0.93792   0.93278      5654
</code></pre>
<p>The result was <code>0.93792</code>, much higher than BERT alone. TF-IDF points in a different direction as a feature source, so combining it likely added diversity and improved the score. It is also interesting that SVC training became slightly faster than TF-IDF alone, perhaps because convergence was better.</p>
<p>In the same way, I tried combining features from several Japanese models published on Hugging Face.</p>
<pre><code>$ python lib/runner.py bert-base-ja-v2-cls bert-base-ja-v2-mean rinna-ja-roberta-base-cls rinna-ja-roberta-base-mean tfidf bert-base-ja-sentiment-cls bert-base-ja-sentiment-mean
[load cache] tmp/embs_cache/bert-base-ja-v2-cls.pkl.gz
shape: (187528, 768) (5654, 768)
...中略
[load cache] tmp/embs_cache/bert-base-ja-sentiment-mean.pkl.gz
shape: (187528, 768) (5654, 768)
concat embs: (187528, 5608) (5654, 5608)
[train svc]
svc exec time: 89.47 sec
==================================================
bert-base-ja-v2-cls + bert-base-ja-v2-mean + rinna-ja-roberta-base-cls + rinna-ja-roberta-base-mean + tfidf + bert-base-ja-sentiment-cls + bert-base-ja-sentiment-mean
valid acc score: 0.9432260346657234
==================================================
              precision    recall  f1-score   support

    positive    0.93717   0.65328   0.76989       822
    negative    0.94391   0.99255   0.96762      4832

    accuracy                        0.94323      5654
   macro avg    0.94054   0.82292   0.86876      5654
weighted avg    0.94293   0.94323   0.93887      5654
</code></pre>
<p>Training SVC on <code>187528x5608</code> features took 90 seconds. The accuracy was <code>0.94323</code>, the best result in this trial. Compared with the <code>0.958</code> score from properly fine-tuned BERT, it is still not enough. Still, it is good enough to consider as one model in an ensemble, and there is still plenty of room to improve the score by adding more features.</p>
<p>The training speed is high. Once the neural network features, which take the most time, have been extracted, I can freely combine features and observe results. That also means using a large number of folds should still be practical.</p>
<h2>Use in real Kaggle competitions</h2>
<p>In the competition I recently joined, <a href="https://www.kaggle.com/competitions/feedback-prize-english-language-learning">Feedback Prize - English Language Learning</a>, which predicted scores for text, the <a href="https://www.kaggle.com/competitions/feedback-prize-english-language-learning/discussion/370605">summary of the 1st through 8th place solutions</a> says that the 1st, 3rd, and 4th place solutions used RAPIDS SVR models in their ensembles. I also tried SVR. Because it did not improve my Public LB score when added to my ensemble, I did not include it in my final submission. However, it scored higher on both Public and Private LB than the early public fine-tuned DeBERTa v3 base model. After the competition ended, I was able to confirm on the Private LB that adding it to the ensemble improved the score, so knowing the result now, I should have included it.</p>
<p>I also heard that SVR was used in the first-place solution for the image competition <a href="https://www.kaggle.com/c/petfinder-pawpularity-score/discussion/300938">PetFinder.my - Pawpularity Contest</a>.</p>
<p>Another possible use is near the end of a competition, when deciding which additional models to include in an ensemble. It may be useful to first pass candidate model features through SVR and prioritize fine-tuning the models with higher scores. <a href="https://www.kaggle.com/competitions/feedback-prize-english-language-learning/discussion/365926">Pretrained Embeddings are all you need (sort of ...)</a> lists SVR results for extracted features, and I think the scores would correlate with the scores obtained by actually fine-tuning those models.</p>
<hr>
<h2>Closing</h2>
<p>This article introduced RAPIDS SVR and SVC, which can train directly on extracted features without fine-tuning. Fine-tuning often takes tens of minutes to several hours depending on the amount of data, and real-world datasets can be much larger. SVR and SVC, which can run in a "RAPID" way with a few minutes for feature extraction and seconds to tens of seconds for training, seem useful not only for Kaggle but also for ordinary work and research.</p>
<p>Until now, when I did not train a neural network for regression or classification tasks, I usually only tried gradient boosted decision trees. RAPIDS SVR and SVC make it possible to run SVM quickly, so they look like methods worth adding to the list of things to try.</p>]]></content:encoded>
  <pubDate>Tue, 13 Dec 2022 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 13 Dec 2022 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>How secon.dev was implemented, December 2022 edition</title>
  <link>https://hotchpotch.dev/articles/website/</link>
  <guid>https://hotchpotch.dev/articles/website/</guid>
  <description>A snapshot of how secon.dev worked in late 2022: Markdown files synced through Dropbox, static builds with Next.js, related-article generation, image processing on GCS, and the parts I wanted to improve next.</description>
  <content:encoded><![CDATA[<p>It has been a while since I wrote <a href="https://secon.dev/entry/2020/09/26/194600-website-setup/">the 2020 edition of how secon.dev was implemented</a>, and I have been thinking that it might be time to touch secon.dev again. I may only think about it and never actually do it, but I will write down the state as of late 2022.</p>
<h2>Core implementation</h2>
<p>This part is almost unchanged from the 2020 version. I write files in Markdown, sync them through Dropbox, and a build server detects changes, builds static HTML with Next.js, and deploys it to Firebase Hosting.</p>
<h2>Related entries</h2>
<p>For <a href="https://secon.dev/entry/other/">entries other than diaries</a>, the site shows related entries at the bottom of the page. This article should probably show them too. The approach is the same as in <a href="/articles/similar-documents-cli">I made a CLI that outputs similar documents for static site generators</a>: calculate TF-IDF for the text, use cosine similarity to find similar articles, and have Next.js read that data and include it at build time. It is a very plain mechanism.</p>
<p>Related entries work reasonably well for non-diary articles. Diaries, though, tend to contain many unrelated notes instead of a single topic. Treating a whole diary entry as one document and computing similarity from TF-IDF word occurrence does not work very well, so I do not use it there.</p>
<h2>Articles with similar images</h2>
<p>When an article contains a photo, the site shows articles that contain similar photos at the bottom of the individual article page. I use this navigation a lot myself. It is one of my favorite features.</p>
<p>During the image upload flow described below, I extract metadata and, at the same time, image features. At the moment I use EfficientNetB0. I then use those features to compute cosine similarity and pick similar images. This is also a plain mechanism, and it is almost the same as what I described in the <a href="https://secon.dev/entry/2021/12/18/200000-image-similarity/">similar image search</a> article.</p>
<p>At the time I did not really understand image features. I still cannot say I understand them properly, but I probably know more than I did then. So I simply chose EfficientNetB0 because it was both performant and small. If I were choosing now, I would start from what kind of "similar image" would actually be useful and choose the model from there.</p>
<p><img src="https://i.imgur.com/DEAaA3H.png" alt="Similar image article"></p>
<h2>Diaries from the same date</h2>
<p>This is an old feature in web diary systems. It lets you look back at what happened on the same date in previous years, and it becomes very good navigation when you are writing "diaries" rather than "articles". The more content accumulates, the more useful it becomes.</p>
<p><img src="https://i.imgur.com/Pitboar.png" alt="Diary entries from the same date"></p>
<h2>Image upload</h2>
<p>In the 2020 version I used Hatena Fotolife as the image upload destination. Later, because Hatena Fotolife removed its paid upload option, I changed the storage to GCS.</p>
<p>When I upload an image file to a specific GCS bucket, a Cloud Function converts JPEG to WebP, resizes it to frequently used sizes, and extracts metadata. The image feature extraction mentioned above also happens there.</p>
<p>Uploading files to a specific GCS bucket sounds troublesome at first, but I mount the bucket as a Windows local filesystem using the method described in <a href="https://scrapbox.io/hotchpotch/GCS_%E3%81%AE%E3%83%90%E3%82%B1%E3%83%83%E3%83%88%E3%82%92_windows_%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%81%A8%E3%81%97%E3%81%A6%E3%83%9E%E3%82%A6%E3%83%B3%E3%83%88">Mount a GCS bucket as a Windows filesystem</a>. That means I can develop a photo in Lightroom, save it, and quickly get the various image sizes needed by the site.</p>
<p>Because all files are on GCS, it is also convenient when I want to bring photos back to my local machine and do something with them. I can fetch them with something like <code>gsutil -m rsync ...</code>. I am glad I got around to building this setup.</p>
<h2>Cost</h2>
<p>secon.dev does not get a lot of traffic, so Firebase Hosting for the website, GCS for image hosting, and Cloud Functions together cost 49 yen including tax for November 2022.</p>
<p>In reality there is also the build server cost, since it runs on a VPS that I use for many other things, so the total cost is probably a little higher.</p>
<hr>
<h2>Future implementation direction</h2>
<p>secon.dev is currently a static build, and the data is filesystem-based: Markdown text plus JSON metadata. This approach has become inefficient enough that I now want a database where I can build data while keeping references between pieces of information, whether with GraphQL, an RDB, or something else. It is not strictly necessary, though, so I am still not sure what to do.</p>
<p>For machine learning features, if more things could be calculated dynamically, I could do more with the site, such as building my own search. I am also thinking about that area.</p>
<p>I am still interested in machine learning these days, and I would like to combine experiments in that area with secon.dev. I will probably choose technologies and architecture that fit that direction.</p>]]></content:encoded>
  <pubDate>Sun, 11 Dec 2022 00:00:00 GMT</pubDate>
  <lastBuildDate>Sun, 11 Dec 2022 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Kaggle Feedback Prize - English Language Learning: team gold medal, 15th place, and Kaggle Master</title>
  <link>https://hotchpotch.dev/articles/kaggle-fp3-comp/</link>
  <guid>https://hotchpotch.dev/articles/kaggle-fp3-comp/</guid>
  <description>Our team finished 15th in Feedback Prize - English Language Learning, earning a gold medal and giving me the medals needed to become a Kaggle Competitions Master.</description>
  <content:encoded><![CDATA[<p>The Kaggle competition <a href="https://www.kaggle.com/competitions/feedback-prize-english-language-learning">Feedback Prize - English Language Learning</a> ended, and we received a gold medal with 15th place out of about 2,650 teams. With this, I had two gold medals and one silver medal in total, meeting the requirements for Kaggle Competitions Master, the title I had aimed for when I first started competitions. I do not think I could have reached this result alone, so I am grateful to my teammates <a href="https://www.kaggle.com/masakiaota">@masakiaota</a> and <a href="https://www.kaggle.com/olivineryo">@olivineryo</a>.</p>
<p>At the end of the competition we were 8th on the public LB among about 2,700 teams, inside the gold-medal range. But the public LB used only 26% of the full LB data. Scores were shown only to the second decimal place, so the display was coarse. Also, some submissions that were quite bad on our CV ranked oddly high on the public LB, so we could not observe a reliable CV-LB correlation. Because of that, we did not trust the public LB and expected a large shake in the final private LB ranking.</p>
<p>For final submission, we submitted three versions: the best CV model, the best LB model, and the best CV model without pseudo labels. Pseudo labels made CV overwhelmingly better, but there were concerns about over-optimizing CV or leaking information. This competition had another prize category, so we had three submission slots and chose those three. If only two submissions had been allowed, the choice would have been very painful.</p>
<p>The result was that although we were 8th on public LB, we dropped to 17th on private LB and unfortunately missed the gold medal range, which was 15th or higher at the time. Missing gold by two places was extremely frustrating. Still, some teams dropped tens or hundreds of ranks, and the public LB first-place team dropped 175 places, so a nine-place drop could have been much worse. It reconfirmed how hard it is to win gold.</p>
<p><img src="https://i.imgur.com/iqlARTi.png" alt="15th place"></p>
<p>Update: after invalid users were removed and the ranking was finalized, we moved up to 15th and received a gold medal. I am very happy.</p>
<hr>
<h2>Competition Task</h2>
<p>This was the third competition in the Feedback Prize, or FP, series, which evaluates English written by students. The task was to infer scores, in 0.5-point increments from 1.0 to 5.0, across six columns such as grammar, vocabulary, and syntax. The evaluation metric was MCRMSE, mean columnwise root mean squared error, so the mean of RMSE for each column. Since data from the previous FP1 and FP2 competitions existed, we tried various approaches while thinking about whether they could be used as pseudo labels and how to optimize the six evaluation axes.</p>
<h2>Solution</h2>
<p>We submitted three ensemble models combining the ideas below. The best private LB result came from an ensemble of 11 models that did not use pseudo labels.</p>
<h3>Backbone model selection</h3>
<p>On the public LB, <code>deberta-v3-base</code> scored better than <code>deberta-v3-large</code>, even though <code>deberta-v3-large</code> has the stronger generalization performance in general. I accepted that result and trained and optimized around it. I thought that for this competition, <code>v3-base</code>, with 12 layers, might score better than the more complex <code>v3-large</code>, with 24 layers. Since the essays were written by students and were not complex prose, perhaps the simpler model was better.</p>
<p>In the final private LB, however, <code>v3-large</code> was indeed strong. We should not have trusted LB and should have built our strategy around CV and <code>v3-large</code>.</p>
<ul>
<li><code>deberta-v3-base</code>
<ul>
<li>Overwhelmingly strong on this competition's public LB.</li>
</ul>
</li>
<li><code>deberta-v1-large</code>
<ul>
<li>Somewhat effective on both CV and LB.</li>
</ul>
</li>
<li><code>deberta-v3-large</code>
<ul>
<li>Effective on CV, but mixing it in caused public LB to drop considerably.</li>
<li>On private LB, that was not the case.</li>
</ul>
</li>
</ul>
<h3>Our team's own approaches</h3>
<p>These were things not discussed publicly, or at least not widely adopted as "this is effective, let's use it". Most of these ideas came from my teammates, and I did not contribute much to them. I am grateful to have had excellent teammates.</p>
<ul>
<li>Attention pooling separately for the six columns
<ul>
<li>Improved the score substantially compared with mean pooling.</li>
</ul>
</li>
<li>Applying LayerNorm after pooling
<ul>
<li>This also improved the score substantially.</li>
</ul>
</li>
<li><code>maxlen: 640</code>
<ul>
<li>Truncating to 640 improved scores compared with longer <code>maxlen</code>.</li>
<li>For <code>deberta-v3-large</code> only, using a longer <code>maxlen</code> and training with splits using <a href="https://paperswithcode.com/method/sliding-window-attention">Sliding Window Attention</a> gave better scores.</li>
</ul>
</li>
<li>Pseudo labeling
<ul>
<li>Very effective for CV, but not very effective for LB.</li>
<li>Using pseudo labels with leaked information improved CV even more, so we carefully removed, or tried to remove, information leaks.</li>
<li>Pseudo-label data used past FP1 and FP2 competition data.</li>
<li>Pseudo-label scores were created by ensembling predictions from trained models.</li>
<li>A single-model pseudo label was learned too quickly, but using ensembled pseudo labels kept improving CV.</li>
<li>A single model trained with pseudo labels, not the strongest on CV but moderately improved, was best on private LB. It hurt that we did not choose it for final submission.</li>
</ul>
</li>
<li>Post-processing
<ul>
<li>Clamp values below 1.0 to 1.0 and values above 5.0 to 5.0. This slightly improved the score.</li>
<li>When ensembling, find optimal weights per each of the six columns as an optimization problem that minimizes CV score. For example, with four ensemble members, solve for and apply 4 x 6 = 24 weights. This slightly improved the score.</li>
<li>We used <a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html">scipy.optimize.minimize</a> to define and solve the optimization problem. I had not known that SciPy could solve it so easily; it was convenient.</li>
</ul>
</li>
</ul>
<h3>Approaches discussed publicly</h3>
<ul>
<li>Layer reinitialization
<ul>
<li>Reinitialize the final n layers of the pretrained model. Reinitializing only the final layer worked best.</li>
<li>This helped a lot.</li>
</ul>
</li>
<li>LLRD, or Layer-wise Learning Rate Decay
<ul>
<li>Gradually decrease the learning rate by layer.</li>
<li>We decayed LR by 0.8 for large models and by 0.7225 for base models.</li>
<li>This helped a lot.</li>
</ul>
</li>
<li>Layer freezing
<ul>
<li>Do not train the first n layers. When LLRD is applied, the early layers of large models, 24 layers, are barely trained anyway, so freezing the first 12 layers of large models sped up training.</li>
</ul>
</li>
<li>AWP
<ul>
<li>Team member Aota has a clear explanation: <a href="https://speakerdeck.com/masakiaota/kaggledeshi-yong-sarerudi-dui-xue-xi-fang-fa-awpnolun-wen-jie-shuo-toshi-zhuang-jie-shuo-adversarial-weight-perturbation-helps-robust-generalization">Kaggleで使用される敵対学習方法AWPの論文解説と実装解説</a></li>
<li>This helped a lot.</li>
</ul>
</li>
<li>Multi-sample dropout
<ul>
<li>The score barely changed, but training became more stable, so we used it.</li>
<li>Added <code>0.2 * 5</code> dropout.</li>
</ul>
</li>
</ul>
<h3>Useful to learn, even though it did not improve our score</h3>
<p><a href="https://www.kaggle.com/code/cdeotte/rapids-svr-cv-0-450-lb-0-44x">RAPIDS SVR</a>. In the RAPIDS SVR approach, neural networks are used only to extract feature embeddings, and then those embeddings are trained with SVR, Support Vector Regression, on CUDA using cuML. SVR training itself finishes instantly or within a few seconds, depending on the GPU. I wondered whether such a method could work, but it produced a score better than my early baseline. Some top solutions also seem to have used RAPIDS SVR, so it is quite possible that I simply failed to use it effectively for performance improvement.</p>
<p>SVR was useful not only because it quickly produced reasonably good scores, but also because it correlated to some extent with the performance of NN backbone models. NN models that scored poorly with SVR often also scored poorly after full NN training. That made SVR useful as a guide when choosing backbone models, especially near the end when we had to consider many models for the final ensemble.</p>
<p>I also had not known about NVIDIA's <a href="https://github.com/rapidsai/cuml">cuML</a> library itself, so learning about it was valuable. cuML can run basic machine learning algorithms similar to those in scikit-learn on CUDA, and some algorithms become extremely fast. Its interface is usually sklearn-compatible, so it is easy to understand. In the future, if a machine learning task takes minutes or more on CPU, I would like to consider using cuML.</p>
<hr>
<h2>After the Competition</h2>
<p>After this competition, I should become a long-awaited Kaggle Competitions Master. Getting gold or silver medals in three medal competitions in a row from my first competition was possible largely because of teammates, except for the <a href="/articles/kaggle-feedback-prize-solo-silver">solo competition</a>, and even there the previous team experience helped. I am especially grateful to <a href="https://www.kaggle.com/masakiaota">Aota</a>, who invited me into Kaggle competitions.</p>
<p>That said, I felt this during my previous solo competition, and this competition made it clear again: I was not the person who came up with the idea that won gold. At my current level, I do not have the skill needed to win a solo gold medal, which is required to become a Kaggle Competitions Grandmaster. There is a large wall in front of me. If I know the approach for a problem domain, I feel I might be able to win solo silver after trying a few times, but I cannot yet imagine winning solo gold. People who have won solo gold medals are truly impressive.</p>
<p>So far, I have only joined competitions solvable with natural language processing Transformer encoder approaches. If I join another competition, rather than prioritizing medals by solving a similar task, I would like to try a task where my score may be weaker but I can gain different knowledge. I am still a beginner, and most machine learning algorithms and problem-solving approaches stimulate my curiosity. I would like to keep enjoying the process while broadening my knowledge.</p>]]></content:encoded>
  <pubDate>Wed, 30 Nov 2022 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 30 Nov 2022 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Solo silver medal, 43rd place, in Kaggle Feedback Prize - Predicting Effective Arguments</title>
  <link>https://hotchpotch.dev/articles/kaggle-feedback-prize-solo-silver/</link>
  <guid>https://hotchpotch.dev/articles/kaggle-feedback-prize-solo-silver/</guid>
  <description>I joined Feedback Prize - Predicting Effective Arguments solo, finished 43rd out of 1,566 teams, and wrote down what worked, what failed, and what solo participation felt like.</description>
  <content:encoded><![CDATA[<p>I joined the Kaggle competition <a href="https://www.kaggle.com/competitions/feedback-prize-effectiveness/">Feedback Prize - Predicting Effective Arguments</a> solo and finished 43rd out of 1,566 teams, earning a silver medal. This was still a provisional ranking at the time, so the final rank could change slightly.</p>
<p><img src="https://i.imgur.com/Kmr1QZR.png" alt="43rd place screenshot"></p>
<p>In my previous competition, my first Kaggle competition, I was <a href="/articles/kaggle-uspppm">blessed with a team and happened to win a gold medal</a>. Through that experience I learned how fun Kaggle can be, and I wanted to join another competition. I also thought I would prefer a team, because staying motivated alone seemed hard. This time, however, I started without a team, or more precisely, I did not know many people and could not form one. The previous competition also taught me that team participation can sometimes lead to a gold medal even without enough individual skill, as in my case, so I wanted to see what result I could leave as a solo participant.</p>
<p>At first I worried that my motivation would not last. But my first baseline was already in the silver-medal range, so I started with the feeling that maybe the gold range was not impossible, and that kept me motivated through the end. In the result, I was nowhere near the gold range, and I recognized both my lack of skill and the narrowness of my toolbox. Still, solo participation gave me its own lessons. Unlike a team, I had to write every piece of code myself, understand the intent of that code, and try a wide range of methods considered good in similar competitions. That broadened my knowledge. On the other hand, I did not get the sense of unity that comes from team participation, nor the unexpected knowledge that teammates can bring. Both styles have tradeoffs.</p>
<p>The PPPM competition I joined previously also used Transformer encoder models for NLP, and this competition likely required similar models. A good part of the knowledge from the previous competition carried over. I think that was one reason I could stay motivated as a solo participant: I was not starting from a place where I knew nothing at all.</p>
<p>Near the end, before the team-merge deadline, several teams invited me to merge. That gave me a real sense that these things happen when you participate solo. By that point, leaving a result as a solo participant had also become one of my goals, so although I appreciated the invitations, I declined them this time.</p>
<h2>What Kind of Competition Was It?</h2>
<p>This competition was a variant of the earlier <a href="https://www.kaggle.com/competitions/feedback-prize-2021">Feedback Prize - Evaluating Student Writing</a>. The task was a classification problem: given part of an essay written by a U.S. student, predict whether it was <code>Ineffective</code>, <code>Adequate</code>, or <code>Effective</code>.</p>
<p><img src="https://i.imgur.com/8uBX7j5.png" alt="Data"></p>
<p>The actual data had about 37,000 rows. Each <code>essay_id</code> had a separate long essay text associated with it, and <code>discourse_text</code> was one part of that essay. Many public baselines created inputs like <code>discourse_type + discourse_text + [SEP] + essay_text</code> and trained on them. The problem with this approach is that <code>essay_text</code> appears repeatedly, causing overfitting very quickly. It also seemed that rows from the same essay were written by the same student, so each essay had its own tendency across <code>Ineffective</code>, <code>Adequate</code>, and <code>Effective</code>. That tendency seemed useful, because a student who writes a good essay probably writes generally good discourse segments.</p>
<h2>Solution Notes</h2>
<p>This was not a gold-medal solution and may not be broadly useful, but these are the methods I tried, what worked, what improved training efficiency, and what did not work well for me.</p>
<h3>Look at essays, not rows</h3>
<p>Instead of treating the problem row by row, I looked at it by <code>essay_id</code>. There are about 37,000 rows, but only about 4,200 essays, around one eighth as many, which improves training speed. Since each target <code>discourse_text</code> is contained in the essay, the task can be seen as classifying specific spans that appear within one text. In that form, a model similar to NER can classify the spans.</p>
<p>For example, I created essay text with special tokens <code>[TAR_START]</code> and <code>[TAR_END]</code> around target spans, like this, and turned the region between those tokens into the classification target.</p>
<pre><code>[TAR_START]Lead so you want to take all the cars out of the city ok cool[TAR_END]. [TAR_START]Claim this will save the pepol like 1,000,000 dolers a year[TAR_END] and will [TAR_START]Claim reduce polution[TAR_END] and stuf i dont know. [TAR_START]Counterclaim i gess its a good idea but on the other hand nah i mean lookif pepal want to ruin the world with gas thats there choce man[TAR_END]. but i just relised that the thing seid to agrewith the pasige or whatever so yah. ummmmmmm i dont know [TAR_START]Position the eirth is cool so why distroit with gasis or something[TAR_END]. look to be honist...
</code></pre>
<p>For the classification representation, I tried averaging between <code>[TAR_START]</code> and <code>[TAR_END]</code>, using only the <code>[TAR_START]</code> and <code>[TAR_END]</code> tokens, using only <code>[TAR_START]</code>, and other variants. The best was using only <code>[TAR_START]</code>. I also tried not using special tokens, using only CLS and SEP, removing <code>TAR_END</code>, replacing it with SEP, and so on. The custom <code>[TAR_START]</code> and <code>[TAR_END]</code> special tokens worked better.</p>
<p>The tokenizer <code>max_length</code> was 1024. Even at that length, some essays overflowed, so I added some ad hoc processing to pack the <code>discourse_text</code> appropriately.</p>
<p>In the end, I trained a 4-fold <code>deberta-v3-large</code> model with CV <code>0.5911</code>, LB <code>0.585</code>, private LB <code>0.586</code>; a <code>deberta-large</code> model with CV <code>0.6034</code>, LB <code>0.595</code>, private LB <code>0.597</code>; and earlier row-based models such as <code>deberta-v3-large</code> with CV <code>0.6179</code>, LB <code>0.608</code>, private LB <code>0.604</code>. I ensembled these. The result was LB <code>0.582</code> and private LB <code>0.583</code>.</p>
<h3>Methods that worked</h3>
<ul>
<li>AWP, or Adversarial Weight Perturbation
<ul>
<li>Improved the score by about 0.05 to 0.1.</li>
<li>AWP has perturbation width and range parameters. Increasing the perturbation width by epoch improved the score a little. If I had more time, I would have liked to try changing it with a scheduler.</li>
</ul>
</li>
<li>Lowercasing and removing symbols in text processing
<ul>
<li>Improved the score by about 0.03.</li>
</ul>
</li>
<li>Back-translation augmentation
<ul>
<li>I ran retranslated data through the model and used only essays where the model's accuracy exceeded 90%.</li>
<li>Improved the score by about 0.03.</li>
<li>Using only retranslated text caused overfitting. When I used data with about 20% <code>[MASK]</code>, training progressed stably.</li>
</ul>
</li>
</ul>
<h3>Methods that did not work well</h3>
<p>This only means I could not make them work. Someone with different knowledge might well make them effective.</p>
<ul>
<li>LSTM or Bi-LSTM before the final output.</li>
<li>Pseudo-labeling using the previous Feedback Prize - Evaluating Student Writing data.
<ul>
<li>This still seems useful if done properly.</li>
<li><a href="https://www.kaggle.com/code/rolianklay/pseudo-labeling-how-to-get-pseudo-labels">https://www.kaggle.com/code/rolianklay/pseudo-labeling-how-to-get-pseudo-labels</a></li>
</ul>
</li>
<li>Adding class weights to <code>torch.nn.CrossEntropyLoss</code>.
<ul>
<li>The data was imbalanced, so I tried weights, but it did not work.</li>
</ul>
</li>
<li>Removing essays with very few <code>discourse_id</code> values.
<ul>
<li>This removes data like one discourse ID per essay.</li>
</ul>
</li>
<li><code>deberta-xlarge</code> and <code>deberta-xxlarge</code>.
<ul>
<li>Training did not progress.</li>
</ul>
</li>
<li><code>allenai/longformer-large-4096</code>.
<ul>
<li>Gradients became NaN and training did not progress.</li>
<li><code>allenai/longformer-base-4096</code> trained, but the score was poor.</li>
</ul>
</li>
</ul>
<h3>Training-efficiency methods</h3>
<p>The text was fairly long in this competition, so training speed and memory efficiency were important. <a href="https://www.kaggle.com/code/vad13irt/optimization-approaches-for-transformers">Optimization approaches for Transformers</a> summarizes these techniques. The linked article has details, but here is a rough summary.</p>
<ul>
<li>8-bit optimizers
<ul>
<li>Use an 8-bit optimizer instead of a 16-bit one to save more memory.</li>
<li>Specifically, <a href="https://github.com/TimDetmers/bitsandbytes">bitsandbytes</a> can replace AdamW and worked smoothly.</li>
<li>I did not notice a score drop, and memory usage really decreased.</li>
<li>Replacing <code>torch.nn.Embedding</code> with <code>bnb.nn.StableEmbedding</code> did not work smoothly for me. If I had replaced it well, the result might have improved further.</li>
</ul>
</li>
<li>Gradient checkpointing
<ul>
<li>Computes while discarding unnecessary gradients. If discarded values are needed again, the backward graph is rebuilt, so speed decreases.</li>
<li>With Transformers, it can be enabled with <code>model.gradient_checkpointing_enable()</code>.</li>
<li>The score barely changed. Training slowed down, but memory usage decreased greatly.</li>
</ul>
</li>
<li>Automatic mixed precision, or AMP
<ul>
<li>Places and computes safe parts in fp16 instead of fp32.</li>
<li>The important point is to use it with <code>GradScaler</code> to avoid gradient overflow.</li>
</ul>
</li>
<li>Gradient accumulation
<ul>
<li>When memory is limited, batch size becomes small. By splitting loss application, you can train similarly to using a larger batch size.</li>
</ul>
</li>
<li>Freezing
<ul>
<li>Layers near the input have lower learning rates, and sometimes not training them gives better results.</li>
<li>So those layers can be frozen.</li>
</ul>
</li>
<li>Fast tokenizers
<ul>
<li>Use Transformers tokenizers written in Rust.</li>
<li>Recently, Rust implementations are used by default when available, so there is often nothing special to do.</li>
</ul>
</li>
<li>Uniform dynamic padding
<ul>
<li>Dynamic padding is pulled up by the longest token length in each batch. If examples are sorted by token length beforehand, awkwardly long token lengths are less likely to appear.</li>
<li>It is hard to use during training because you usually want random ordering, but it can speed up inference.</li>
</ul>
</li>
</ul>
<h2>After the Competition</h2>
<p>At first I thought maybe I had a chance at the gold range. In the end, I was in the middle of the silver range and nowhere near gold. That helped me reconfirm both my current position and my lack of skill. Small incremental improvements were not enough to reach a gold-medal score; a more drastic improvement would have been needed. I am looking forward to reading the top solutions.</p>
<p>Even though this was an NLP competition similar to the previous one, using a Transformer encoder model, I finished in the middle of the silver range. For Kaggle competitions in other topics, whether I can win a medal at all is still uncertain.</p>
<p>Trying it individually instead of as a team was good. I was able to leave some result on my own, stay motivated by myself, and enjoy the work. One more silver medal would make me a Kaggle Competitions Master, so I would like to keep joining competitions that interest me, whether as a team or solo. Most competitions are probably tasks I have never done before, so even if I do not win a medal, I expect to gain a lot of knowledge from any of them.</p>]]></content:encoded>
  <pubDate>Wed, 24 Aug 2022 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 24 Aug 2022 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Was the end of Japan&apos;s 2022 rainy season unusually early?</title>
  <link>https://hotchpotch.dev/articles/rainy-season/</link>
  <guid>https://hotchpotch.dev/articles/rainy-season/</guid>
  <description>Using Japan Meteorological Agency data, I checked the standard deviation of rainy-season ending dates to see how unusual the 2022 Kanto-Koshin date really was.</description>
  <content:encoded><![CDATA[<p>Yesterday, the rainy season was considered to have ended in the Kanto-Koshin region. News reports said it was unusually early, 22 days earlier than normal, where "normal" means the statistics from the past 30 years. News reports usually avoid talking about standard deviation because they need to communicate clearly to the general public, but I wanted to check how unusual "unusually early" actually was.</p>
<p>For rainy-season start and end dates, I used <a href="https://www.data.jma.go.jp/cpd/baiu/kako_baiu09.html">data published by the Japan Meteorological Agency from 1951 onward</a>. There is even a year, 1993, where the start and end could not be identified. The data was inconvenient to handle directly, so I pasted it into a spreadsheet and cleaned it up roughly.</p>
<ul>
<li><a href="https://docs.google.com/spreadsheets/d/1-1JZitpvKu_MDDoK0bRaYFGOMaMJuVc9s1GusthlvPc/edit?usp=sharing">https://docs.google.com/spreadsheets/d/1-1JZitpvKu_MDDoK0bRaYFGOMaMJuVc9s1GusthlvPc/edit?usp=sharing</a></li>
</ul>
<p>Looking at about 70 years of data, the standard deviation of the rainy-season end date is 8.6 days. The 2022 date is 21 days earlier than the 70-year average. If we assume the data is normally distributed, it falls outside the 95% confidence interval, so it is fair to call it unusually early.</p>
<p>However, looking only at the past 10 years, the rainy season has already been ending earlier. The 70-year average end date is July 18, while the 10-year average is July 14. The average start date differs by only one day, so the end date has moved noticeably earlier. The standard deviation of the 10-year average end date is 12.59 days. Compared with the 10-year average, the 2022 end date differs by 17.2 days. If we look only at the narrow window of the past 10 years, it probably falls somewhere around a 75-85% confidence interval, so it feels like something that happens from time to time. The 2022 date is also not that different from 2018, only two days earlier.</p>
<p>In other words, viewed across the past 70 years, it is unusually early. Viewed across the past 10 years, it is only something that occasionally happens. The data makes me feel that changes in the global environment, including environmental issues, may already be affecting the timing of the rainy season's end.</p>]]></content:encoded>
  <pubDate>Tue, 28 Jun 2022 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 28 Jun 2022 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Finding optimal weighted-ensemble coefficients with constrained least squares</title>
  <link>https://hotchpotch.dev/articles/ensemble-linear/</link>
  <guid>https://hotchpotch.dev/articles/ensemble-linear/</guid>
  <description>For a Kaggle ensemble, I used non-negative least squares to compute model blending weights automatically instead of tuning them by hand.</description>
  <content:encoded><![CDATA[<p>In the Kaggle competition <a href="https://www.kaggle.com/competitions/us-patent-phrase-to-phrase-matching/overview">U.S. Patent Phrase to Phrase Matching</a>, I created about 20 ensemble candidates near the final stage by combining several pretrained models and multiple promising parameter settings. We then tried different ways to mix those models. The final submissions were a 6-model ensemble and a 9-model ensemble.</p>
<p>At that point, asking a human to choose the optimal ensemble weights from model performance and intuition is not very realistic. Of course, a domain expert may sometimes make better choices, but I wanted the machine to calculate the weights automatically. Changing the mixing ratio to minimize MSE is just a linear combination, so I thought the optimum should be computable.</p>
<h2>A Simple Example</h2>
<p>Suppose we have data like this. <code>y</code> is the true value, and <code>X</code> contains the predictions from each ensemble model.</p>
<pre><code>import numpy as np
y = np.array([0.5, 0.75, 0.25, 1.0, 0.5])
X = np.array([
    [0.52, 0.9, 0.41, 0.99, 0.51],
    [0.52, 0.7, 0.41, 0.99, 0.51],
    [0.48, 0.73, 0.12, 0.97, 0.47],
    [0.45, 0.35, 0.25, 0.9, 0.49],
])
</code></pre>
<p>First, look at the MSE for each row of <code>X</code>.</p>
<pre><code>np.square(X - y).mean(axis=1)
=> array([0.00974, 0.00574, 0.0039 , 0.03452])
</code></pre>
<p>If we simply average the predictions, the MSE is below. It is worse than the best single model.</p>
<pre><code>np.square(X - y).mean(axis=0).mean(axis=0)
=> 0.013475
</code></pre>
<h2>Least Squares</h2>
<p>One way to find optimal coefficients is least squares, or linear regression. Let's try it.</p>
<pre><code>from sklearn.linear_model import LinearRegression
reg = LinearRegression().fit(X.T, y)
reg.coef_
=> array([ 0.43575566, -0.05397578,  0.46076883,  0.21063718])
</code></pre>
<p>Ignoring the negative coefficient for a moment, using these parameters as weights gives the least-squares prediction.</p>
<pre><code>X.T @ reg.coef_
=>array([0.51448131, 0.76448131, 0.26448131, 1.01448131, 0.51448131])
</code></pre>
<p>The values look close to the target, so calculate the MSE. It is good.</p>
<pre><code>np.square(X.T @ reg.coef_ - y).mean(axis=0)
=> 0.00020970822203200185
</code></pre>
<p>But should an ensemble model really receive a negative coefficient? I want the coefficients to be positive.</p>
<h2>Solving with Constrained Least Squares</h2>
<p>So we can treat this as a least-squares problem with the constraint that coefficients must be non-negative. If there is an implementation of a constrained least-squares solver, this should be easy. Searching for <a href="https://www.google.com/search?q=restricted+least+square+method+scipy">restricted least square method scipy</a> led me directly to one in SciPy. <a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.nnls.html#scipy.optimize.nnls">scipy.optimize.nnls</a> solves non-negative least squares, so I used it.</p>
<pre><code>weights, rnorm = scipy.optimize.nnls(X.T, y)
weights
=> array([0.29260857, 0.08404164, 0.52487508, 0.12761238])
</code></pre>
<p>Now we have positive coefficients. Use them as ensemble weights.</p>
<pre><code>X.T @ weights
=> array([0.50522372, 0.75      , 0.24931469, 0.99686367, 0.50131296])
np.square(X.T @ weights - y).mean(axis=0)
=> 7.863453999510499e-06
</code></pre>
<p>This gives a minimum MSE with positive coefficients. For simple linear blending of ensemble results like this, solving with <code>scipy.optimize.nnls</code> looks easy and useful.</p>
<hr>
<p>The ensemble predictions calculated with these weights correlated well between CV and both public and private LB. Being able to quickly compute the optimal blending ratio without hand-tuning weights was helpful in the final stage of a Kaggle competition, when time was short and everyone was mentally tired. This example uses simple linear-combination weights. There are many other ways to weight ensembles, so the implementation of "optimal weighting" will depend on the purpose.</p>
<p>After the competition ended, I learned that this method is a way of solving the linear problem in what is called stacking. I also tried optimizing ensembles by context with LightGBM, or GBDT. It optimized CV but did not work well for LB score. <a href="https://www.kaggle.com/competitions/us-patent-phrase-to-phrase-matching/discussion/332420#1828334">According to this discussion</a>, neural networks can overfit easily, so GBDT is not well suited here. That makes sense.</p>
<hr>
<p>People who know linear algebra can probably derive these coefficients naturally. In my case, I had recently read <a href="https://www.kspub.co.jp/book/detail/5161968.html">Optimization Mathematics Starting from Vectors and Matrices</a>, and that was the first time I understood, at least a little, what linearity means. If I had not read that book, I probably would not have thought of keywords such as "constrained non-negative least squares". Fundamentals matter.</p>]]></content:encoded>
  <pubDate>Tue, 21 Jun 2022 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 21 Jun 2022 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>My first Kaggle competition ended with a team gold medal, 8th place</title>
  <link>https://hotchpotch.dev/articles/kaggle-uspppm/</link>
  <guid>https://hotchpotch.dev/articles/kaggle-uspppm/</guid>
  <description>I joined my first Kaggle competition through a strong team, learned how collaborative competition work is organized, and ended up with a gold medal in the U.S. Patent Phrase to Phrase Matching competition.</description>
  <content:encoded><![CDATA[<p>The Kaggle competition ended today, June 21, 2022, at 9:00 in the morning. Results appear immediately, so I watched them with my team members. We finished 8th out of 1,975 teams, the same as our public LB rank, and received a gold medal. It was my first Kaggle competition, and I was fortunate to be on an excellent team.</p>
<p><img src="https://i.imgur.com/3Cbwh4z.png" alt="8th place"></p>
<p><a href="https://www.kaggle.com/competitions/us-patent-phrase-to-phrase-matching/discussion/332492">The team's solution is published in English</a>, and I expect a Japanese version will be published separately. So here I will write mainly about my impressions as a first-time participant, rather than the solution itself. About 40 days earlier, <a href="https://www.kaggle.com/masakiaota">Aota</a> invited me to try <a href="https://www.kaggle.com/competitions/us-patent-phrase-to-phrase-matching/overview">U.S. Patent Phrase to Phrase Matching</a>, a patent and natural language processing competition. My Kaggle knowledge was only at the level of having <a href="https://amzn.to/3Oute6U">read a beginner book and tried the Titanic practice competition</a>, so he taught me more concrete ways to proceed and how to think when working as a team.</p>
<p>For tools, we used Notion for information sharing, mainly a kanban board, and shared implementations on GitHub. Scores were recorded in a spreadsheet together with implementation serial-number files. Implementations were branched by plain copying. At first I wondered whether a notebook-style workflow, like Colab plus diff management on save, would be better. In practice, the copy-based approach made each file easier to compare, and because many variants were created, more than 100 training implementations in the end, copy-paste implementation was convenient. File-level diffs were also easy to take.</p>
<p><img src="https://i.imgur.com/w799Vta.png" alt="Score management table"></p>
<p>Asynchronous flow-style information sharing happened in chat, while synchronous discussions happened in a weekly regular meeting. When the team reached consensus on a method, we updated the baseline implementation. Aota mostly grew the baseline into something strong. Deciding the best CV split method early also paid off more and more as data accumulated. Running the baseline saved not only scores but also metadata needed to derive those scores, such as preprocessed data, which made it easier to investigate why a score appeared.</p>
<p>That is how the competition work began. From then on, I spent almost all the time I had previously spent on games and other things on Kaggle. Once I started, Kaggle had a game-like addictiveness. To get a higher score, I wanted to try one idea after another. Because it was my first competition, I also gained a lot of knowledge along the way, which made the process intellectually satisfying and fun.</p>
<p>That said, Kaggle is not easy enough that a beginner can get high scores just by imitating what they read in <a href="https://www.kaggle.com/competitions/us-patent-phrase-to-phrase-matching/code">Code</a> or <a href="https://www.kaggle.com/competitions/us-patent-phrase-to-phrase-matching/discussion">Discussion</a>. Around the middle of the competition, there were periods where improving local CV produced the opposite movement on LB. My motivation dropped a little. Because I was on a team, though, I still felt that we were moving forward together, which gave me enough motivation not to quit midway. At least for me, if I had been doing it alone, I probably would have stopped working on it partway through. The chart below is from the period when CV and LB were completely negatively correlated.</p>
<p><img src="https://i.imgur.com/iTF9LCZ.png" alt="Negative correlation"></p>
<p>Ten days before the deadline, we merged with <a href="https://www.kaggle.com/skraiii">Raiii</a> and <a href="https://www.kaggle.com/taromasuda">Masuda</a>, who had been participating as another team. We shared methods and ideas, built a reliable model that was the best at that point, and started creating ensembles for final submission. Then Raiii implemented a brilliant training idea three days before the deadline, thinking it might work. A single model with that method suddenly entered the 30s, and when we ensembled it, we climbed to 4th on the LB at once. The idea initially considered a graph-network solution, but the problem could be treated as a simpler 1:N problem, so perhaps it would train well if fed as a sequence. In hindsight that sounds plausible, but I would not have come up with the idea, and I was impressed by the skill needed to implement it in such a short time.</p>
<p><img src="https://i.imgur.com/1pZFDmp.png" alt="LB 4th place"></p>
<p>From this point the competition became too interesting. On the weekend right before the deadline, I spent almost all my time on Kaggle: creating various ensembles from new training methods, incorporating a way to <a href="/articles/ensemble-linear">automatically compute optimal blending weights for a weighted ensemble</a>, because it is a linear combination with a unique optimum, and analyzing score data repeatedly. My small contribution was probably around there. It felt like an intense three-day festival. In the end, CV and LB scores became almost correlated, so we trusted the best CV and chose the final submission. On the morning of the deadline, we watched the result together while doing KPT and chatting. That gave the team a real sense of unity and achievement.</p>
<p><img src="https://i.imgur.com/IBwxIY0.png" alt="Scores near the end"></p>
<p>As written at the beginning, the final result was 8th place and a gold medal. It was simply very interesting. I was lucky to have strong team members, gain a lot of knowledge, and experience an exciting situation. This was an NLP competition, and because I went through the tutorials and beginner course for <a href="https://huggingface.co/docs/transformers/index">Transformers</a>, I also gained practical knowledge that feels useful for work. Kaggle has competitions in many other domains too, and in unfamiliar domains everything seems fresh and full of things to learn.</p>
<p>My first Kaggle competition went very well because I was blessed with the team. I am grateful to the team members. But if you take a competition seriously, I think you should expect to spend at least about 50 hours on one competition. If you cannot make that time, joining may be difficult. This time, over about 40 days, I think I spent around 80 to 100 hours. Conversely, if you have the time, I felt that you can gain a lot from it, as described above. It is also simply fun if you think of it as a game. I would like to join another competition on a topic that interests me, but personally I may give up midway if I do it solo, so I would like to participate with at least one teammate for continuity and motivation.</p>]]></content:encoded>
  <pubDate>Tue, 21 Jun 2022 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 21 Jun 2022 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Reading Basic Statistics by Kimio Miyakawa: statistics before machine learning</title>
  <link>https://hotchpotch.dev/articles/basic-statistics/</link>
  <guid>https://hotchpotch.dev/articles/basic-statistics/</guid>
  <description>After several months of studying machine learning, I realized I was missing the statistical foundations needed to understand data, experiments, estimation, testing, and model evaluation.</description>
  <content:encoded><![CDATA[<p><img src="https://storage.googleapis.com/secons-site-images/photo/large/20211213125609.webp" alt=""></p>
<p>I started learning machine learning this year. For roughly the first six months, I learned mainly from typical machine learning books: basic supervised learning such as regression and classification, unsupervised learning such as clustering and dimensionality reduction, how to read results, how to process well-formed data, simple neural networks such as perceptrons, fully connected layers, CNNs, and RNNs implemented from scratch, and model building with TensorFlow and Keras. When abstracted libraries existed, I used them while thinking about which model was appropriate for the problem.</p>
<p>In practice, however, before building the model you need, you first have to look at the data and think. Explanations of exploratory data analysis often skip, especially for beginners, the question of what can be understood from data in the first place. They start from ideas such as correlation and distribution, assuming that background knowledge already exists. I could do something that looked like EDA, but in reality I was doing it without really understanding what I was looking at.</p>
<p>The same thing happens when building a model and validating it with an A/B test. Many explanations say something like "use a chi-square test and check statistical significance", and you end up validating things without understanding them well. What are degrees of freedom? What is a t statistic? Can you ignore degrees of freedom because internet data has a large sample size? Why is variance divided by n - 1? I did not understand even these basic points. And in reality, you are not always looking at one A/B test once. You may want to know whether repeated results are significant, where degrees of freedom matter because the number of trials is small, what you want to call significant, and what result you expected before running the test.</p>
<p>These are only examples, but I lacked the underlying premises. Because of that, my understanding was shallow and I could not always choose an appropriate method. I often did not understand basic terms that appeared in library documentation. It took me about half a year to realize that this "background knowledge I was missing" was statistics. The foundations needed for machine learning are calculus, linear algebra, and statistics. I had at least a minimal handle on calculus and linear algebra because I studied their basics in high school, and linear algebra also appeared in 3D programming, where I had implemented related code before.</p>
<p>Statistics, on the other hand, was almost absent from my working knowledge. I may have taken a university credit for it, but I had forgotten it completely. I did not even understand basic ideas such as looking at the mean and variance of data, standardizing a value, and knowing that a normal distribution falls within -1.96 to 1.96 about 95% of the time.</p>
<p>So I decided to learn the basics of statistics properly. At first, though, I did not know where to begin. Looking around bookstores, I found many all-in-one books combined with SQL or Python, but I could not tell which books would let me actually learn statistics. I tried O'Reilly's <a href="https://www.oreilly.co.jp/books/9784873119267/">Practical Statistics for Data Scientists</a>, but because I did not understand the underlying basics of statistics, I could not really get started.</p>
<p>Returning to the basics, I skimmed textbook-style books. <a href="http://www.utp.or.jp/book/b300857.html">Basic Statistics from University of Tokyo Press</a> honestly felt too difficult for me, and I could not imagine finishing it. Around that time, I happened to see a video that recommended <a href="http://www.yuhikaku.co.jp/books/detail/9784641164550">Basic Statistics, 4th Edition by Kimio Miyakawa</a>. I tried it without much expectation, but it was extremely clear, and the example problems were excellent. I read it almost every day, worked through exercises with a pen, notebook, and scientific calculator, and finished it over a little under three months. I almost never finish this kind of textbook, so it must have matched me very well.</p>
<p>The explanations are concise and easy to understand. When enough time has passed that you might have forgotten something, the book gives page references and supplementary explanations, so it does not leave you behind. The exercises are also easy to imagine in real-world terms, for example: "If the defect rate of a product is 2%, what is the probability that 2 defective items are included among 200 products?" or "In an experiment, the average time until 10 fuses blew under a 25% overload was 9.2 minutes, with a standard deviation of 2.5 minutes. Estimate the mean time until this type of fuse blows under a 25% overload with a 99% confidence coefficient."</p>
<p>As you can see from the <a href="https://honto.jp/netstore/pd-contents_0626580538.html">table of contents</a>, the book covers mean and variance, frequency distributions, regression and correlation analysis, probability, random variables and probability distributions, major probability distributions, sampling distributions, estimation, hypothesis testing, and the statistical logic of regression. It teaches probability and regression, which are important foundations for machine learning algorithms, probability distributions that matter when looking at data, and estimation and testing for checking whether hypotheses hold. As I learned these topics gradually, I think my practical ability to look at data and form hypotheses improved substantially.</p>
<p>Looking back, for me the efficient timing would have been about three months after I started machine learning, once I had become able to use machine learning tools such as scikit-learn and TensorFlow at a basic level. In short, statistics is one of the foundations of machine learning, and it is better to learn at least the minimum basics early. I recommend Kimio Miyakawa's Basic Statistics as a clear way to learn those foundations. I am grateful to Professor Miyakawa for writing such a good book.</p>]]></content:encoded>
  <pubDate>Mon, 13 Dec 2021 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 13 Dec 2021 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Adding type hints to a Python project and getting value from type checking</title>
  <link>https://hotchpotch.dev/articles/python-typing/</link>
  <guid>https://hotchpotch.dev/articles/python-typing/</guid>
  <description>I added Python type hints and pyright to a machine learning project, and found that the setup cost was low while editor support and static checks were immediately useful.</description>
  <content:encoded><![CDATA[<p>Recently I started writing Python for a machine learning project I am helping with. I remembered that Python has type hints, tried adding them to the project, and found that they were easy to introduce and immediately useful because I could get the benefits of static type checking.</p>
<p>When I first started writing type hints, though, I was not sure which documents I should read or how I should introduce type hints into a project. This article explains those points and shows one way to start writing types in a Python project.</p>
<h2>Which Documents Should You Read?</h2>
<p>If you have written another statically typed language before, these two documents should be enough to get started.</p>
<ul>
<li><a href="https://github.com/microsoft/pyright/blob/main/docs/type-concepts.md">Understanding Typing</a>
<ul>
<li>Part of the documentation for pyright, a type checker implementation. It summarizes the important points concisely.</li>
</ul>
</li>
<li><a href="https://docs.python.org/ja/3/library/typing.html">typing - Support for type hints</a>
<ul>
<li>The official Python documentation. It feels more like a reference manual than an easy tutorial, so I recommend reading it after Understanding Typing.</li>
</ul>
</li>
</ul>
<p>After reading those two, you should have a rough sense of Python type hints and may want to start writing them yourself. The sample code I used in an internal study session to check type behavior is here. It may not be exemplary code, but it is useful for observing behavior.</p>
<ul>
<li><a href="https://gist.github.com/hotchpotch/f0253edcc1d403ffb38740cf2e0ecc74">https://gist.github.com/hotchpotch/f0253edcc1d403ffb38740cf2e0ecc74</a></li>
</ul>
<p><img src="https://i.imgur.com/Tlbs8x0.png" alt="vscode-1"></p>
<h2>How to Introduce Type Hints</h2>
<p>When adding type hints to a project, the first thing to choose is the type checker implementation. Python type hints are specified across several <a href="https://www.python.org/dev/peps/">PEPs</a>, and there are multiple tools that implement them. Major options include:</p>
<ul>
<li><a href="https://github.com/python/mypy">mypy</a>
<ul>
<li>A long-standing implementation that can also <a href="https://mypy.readthedocs.io/en/latest/python2.html">type-check Python 2 code</a>.</li>
</ul>
</li>
<li><a href="https://github.com/google/pytype">pytype</a>
<ul>
<li>Google's implementation. It can also type-check Python 2.7.</li>
</ul>
</li>
<li><a href="https://pyre-check.org/">pyre</a>
<ul>
<li>Meta's implementation. It supports Python 3 and later.</li>
</ul>
</li>
<li><a href="https://github.com/microsoft/pyright">pyright</a>
<ul>
<li>Microsoft's implementation. It supports only Python 3 and later, and is written for Node.js.</li>
</ul>
</li>
</ul>
<p>All of these can be used through the Language Server Protocol, although I have not checked pytype myself, so they can be used from LSP-compatible IDEs and editors. If Python 3 or later is enough, my personal recommendation is pyright. It is fast, and GitHub issues and PRs seem to get quick responses, perhaps because Microsoft employees are assigned to them. In VS Code, it is easy to use through the <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance">Pylance extension</a>.</p>
<p>One Pylance trap is that type checking is off by default. Really. Pyright's default is <code>basic</code>, so this surprised me. You should change it in VS Code's <code>settings.json</code>. Otherwise, you may think "I installed Pylance, now I can write type hints, and there are no errors, so everything is fine", when in fact type checking is simply not running. That is exactly what happened to me at first.</p>
<pre><code>  // settings.json
  // 現在初期値は "off" となっている🤣なんでや～
  "python.analysis.typeCheckingMode": "basic",
  // workspace 全体に対して型チェック。
  "python.analysis.diagnosticMode": "workspace"
</code></pre>
<p><img src="https://i.imgur.com/C9G6jAV.png" alt="The default is off!?"></p>
<p>Pylance includes pyright, which is open source, but Pylance itself is not open source because it includes other features as well. If you use something other than VS Code, you can use pyright directly instead of Pylance, so the absence of Pylance should not be a serious problem.</p>
<h2>Adding pyright to a Project</h2>
<p>If you only want to install pyright as a CLI, install it through npm and run the <code>pyright</code> command.</p>
<pre><code>$ npm install --global pyright
$ pyright
No configuration file found.
pyproject.toml file found at C:\Users\hotch\src\github.com\....
Loading pyproject.toml file at C:\Users\hotch\src\github.com\...\pyproject.toml
Assuming Python platform Windows
No include entries specified; assuming C:\Users\hotch\src\github.com\...
Auto-excluding **/node_modules
Auto-excluding **/__pycache__
Auto-excluding .git
stubPath C:\Users\hotch\src\github.com\...\typings is not a valid directory.
Searching for source files
Found 62 source files
0 errors, 0 warnings, 0 infos
Completed in 2.591sec
</code></pre>
<p>Type errors are detected when they exist, and <code>pyright -w</code> can watch files and keep running. In normal use, however, VS Code or another editor will run type checks through pyright, so there are not many occasions to invoke the command directly.</p>
<p>You can apply project-wide settings by placing either <code>pyproject.toml</code> or <code>pyrightconfig.json</code> in the project root.</p>
<pre><code># pyproject.toml
# https://github.com/microsoft/pyright/blob/main/docs/configuration.md
[tool.pyright]
pythonVersion = "3.7"
typeCheckingMode = "basic"
</code></pre>
<p>Personally, I wanted to pin the pyright version and install it quickly with <code>npm install</code>, so I put a Node package-management <code>package.json</code> in the project.</p>
<pre><code>{
  "name": "pyright-exec",
  "version": "1.0.0",
  "description": "",
  "main": "",
  "scripts": {
    "pyright": "pyright"
  },
  "author": "",
  "license": "",
  "dependencies": {
    "pyright": "^1.1.155"
  }
}
</code></pre>
<p>If you want to run it in CI, you can configure GitHub Actions like this <a href="https://github.com/hotchpotch/similar-documents-cli/blob/main/.github/workflows/test.yaml">example</a>.</p>
<pre><code>      - uses: actions/setup-node@v1
        with:
          node-version: 14.x
      - name: Install node dependencies
        run: npm install
      - name: Typecheck
        run: npm run pyright
</code></pre>
<p>At first I wondered whether it was strange to add pyright, which is written for Node.js, to a Python project. But it has zero dependencies, while many tools pull in a lot of packages, so installation is immediate. That also makes it comfortable to use.</p>
<h2>Python Versions and Type Hints</h2>
<p>Once you start writing type hints in Python, the next thing you may hit is that available typing features differ by Python version. For example, list type hints behave differently depending on the version.</p>
<pre><code class="hljs language-python"><span class="hljs-comment"># py 3.9～ 何もしないで builtins のものとして書ける</span>
l: <span class="hljs-built_in">list</span>[<span class="hljs-built_in">str</span>] = []
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-comment"># py 3.7～は future import でバックポートを実現</span>
<span class="hljs-keyword">from</span> __future__ <span class="hljs-keyword">import</span> annotations
l: <span class="hljs-built_in">list</span>[<span class="hljs-built_in">str</span>] = []
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-comment"># py 3.5～ は typing モジュールを使う</span>
<span class="hljs-comment"># なおこの書き方は 3.9 ～非推奨に…</span>
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> <span class="hljs-type">List</span>
l: <span class="hljs-type">List</span>[<span class="hljs-built_in">str</span>] = []
</code></pre>
<p>Another commonly used type is <a href="https://docs.python.org/ja/3/library/typing.html#typing.TypedDict">TypedDict</a>, which is supported from Python 3.8.</p>
<pre><code># py3.8～
from typing import TypedDict
</code></pre>
<p>If the <code>typing</code> module in the Python version you need does not include a type you want, you can usually install <a href="https://github.com/python/typing/tree/master/typing_extensions">typing_extensions</a> and use its backport.</p>
<pre><code># py3.7以前の場合。もちろん3.8以降でも動く。
from typing_extensions import TypedDict
</code></pre>
<p>Until you get used to it, you need to check the reference documentation to see which Python version supports which typing feature. For a new project without constraints, using the newest possible Python version is best. But the runtime environment may be older. For example, as of July 2021, <a href="https://colab.research.google.com/">Google Colab</a> uses Python 3.7. If your code must run on Colab, you need to write it so it works on 3.7. I initially wrote code for 3.8, found that it did not run on Colab, and had to rewrite it for 3.7.</p>
<h2>Type Stubs: Providing Types from Separate Files</h2>
<p>Next, you may run into a library that does not provide type hints. In that case, you can use the type stub mechanism defined in <a href="https://www.python.org/dev/peps/pep-0561/">PEP 561</a>.</p>
<p>In pyright, the default <code>stubPath</code> is <code>./typings</code>. <code>.pyi</code> type stub files placed under that directory are loaded. To see concrete examples of how to write <code>.pyi</code>, it is useful to look at <a href="https://github.com/python/typeshed">typeshed</a>, which collects type stubs for the standard library and well-known packages. Typeshed's stubs are included by default in tools such as pyright and mypy, so you normally do not need to install them separately.</p>
<p>pyright also has a <code>pyright --createstub packagename</code> command that generates a type stub template, and Pylance can create stubs through its UI. See pyright's <a href="https://github.com/microsoft/pyright/blob/main/docs/type-stubs.md">Type Stub Files</a> documentation for details.</p>
<h2>Start Writing Type Hints</h2>
<p>Writing type hints brings many benefits: better IDE completion, easier refactoring, improved development efficiency, more confidence from static type checks, and fewer runtime errors. Python's type hints can do far less than, for example, TypeScript's type system, and sometimes I wish I could manipulate types more expressively. But because of that limitation, most Python type annotations stay simple. That keeps the learning cost low and makes the types easy for most readers to understand.</p>
<p>The cost of introducing type hints into a project is also low, especially for a new project. If you are unsure whether to write type hints, I think it is worth trying them first.</p>
<hr>
<h2>Other Notes</h2>
<p>As of July 2021, I help with machine learning projects as a software engineer at <a href="https://nilab.nikkei.co.jp/">Nikkei Innovation Lab</a> three to four days a week. I am not an employee; I help on a project basis. Most of the information in this article comes from that work, and this post is based on material I presented at an internal Nikkei study session.</p>
<p>Nikkei is an interesting environment for machine learning. It naturally has newspapers and other text suited to natural language processing, and it also has access to various large-scale datasets through the Nikkei electronic edition. If that sounds interesting, take a look at the <a href="https://hack.nikkei.com/">hiring site</a>. At the time of writing, Nikkei is also recruiting machine learning interns for a <a href="https://hack.nikkei.com/internJobs/2021_summer/">summer internship</a>.</p>]]></content:encoded>
  <pubDate>Wed, 07 Jul 2021 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 07 Jul 2021 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>A CLI for finding similar documents in static site generators</title>
  <link>https://hotchpotch.dev/articles/similar-documents-cli/</link>
  <guid>https://hotchpotch.dev/articles/similar-documents-cli/</guid>
  <description>I published similar-documents-cli, a small tool that computes TF-IDF and cosine similarity over Markdown or HTML files so static sites can generate related-article data at build time.</description>
  <content:encoded><![CDATA[<p>I recently wrote a small <a href="https://secon.dev/entry/2021/04/22/200000/">CLI for outputting related entries</a> for secon.dev. While doing that, I realized that most static site generators produce HTML from article files written in Markdown or HTML, such as <code>.md</code> and <code>.html</code>, located somewhere on disk. That means the same idea could be useful outside my own site, so I released it as a CLI for finding similar documents.</p>
<ul>
<li><a href="https://github.com/hotchpotch/similar-documents-cli">https://github.com/hotchpotch/similar-documents-cli</a>
<ul>
<li>Install it with <code>pip install -U similar-documents</code></li>
</ul>
</li>
</ul>
<p>If you pass the files whose related entries you want to infer, the CLI outputs, in JSON, the most related files for each input file. The article files for this site, secon.dev, are not public, so as an example I tried inferring related entries from the Markdown articles in <a href="https://github.com/r7kamura/r7kamura.com/">the source code for r7kamura.com</a>, which <a href="https://r7kamura.com/">r7kamura</a> publishes.</p>
<pre><code>$ time similar-documents --debug -k 3 -t japanese ~/src/github.com/r7kamura/r7kamura.com/articles/*.md > r7kamura_com_similar_articles.json
files to texts 951 documents
calc tfidf...
calc similarity...
assign similarity score
similar-documents --debug -k 3 -t japanese  >   8.03s user 3.98s system 383% cpu 3.131 total
</code></pre>
<p>It took about 3.1 seconds to infer related articles for 951 posts on my Ryzen 3900X environment. The JSON includes file paths from my machine, so I cleaned it up a little. The hash keys are article paths, and each <code>entry</code> array contains related articles in descending score order.</p>
<pre><code>cat r7kamura_com_similar_articles.json | jq . | sd '/home/yu1/src/github.com/r7kamura/' 'https://' |sd '.md"' '"' > converted.json
cat converted.json
</code></pre>
<p>Here are a few excerpts from the JSON. For an article about duct rails, other duct-rail-related articles appear with high scores.</p>
<pre><code>  "https://r7kamura.com/articles/2021-02-05-switchbot-hub-mini-on-rails": [
    [
      "https://r7kamura.com/articles/2020-12-19-google-home-mini-on-rails",
      0.6502251932677562
    ],
    [
      "https://r7kamura.com/articles/2021-01-18-nature-remo-on-rails",
      0.6088665752039284
    ],
    [
      "https://r7kamura.com/articles/2016-12-12-h",
      0.33070364498269256
    ]
  ],
</code></pre>
<p>For an article about the game Atelier Ryza, another Ryza article and an FF13 article appear. Looking at the scores, the other Ryza article is clearly the closest one.</p>
<pre><code>  "https://r7kamura.com/articles/2021-02-13-atelier-ryza": [
    [
      "https://r7kamura.com/articles/2020-01-19-atelier-ryza",
      0.4632359977711961
    ],
    [
      "https://r7kamura.com/articles/2020-12-31-games-2020",
      0.17984491640184092
    ],
    [
      "https://r7kamura.com/articles/2021-01-30-final-fantasy-13",
      0.15056225381780178
    ]
  ],
</code></pre>
<p>For an article about bathtub cleaning, articles about bathtub detergent and drain cleaning are inferred as related.</p>
<pre><code>  "https://r7kamura.com/articles/2021-02-19-laundry-cleaning": [
    [
      "https://r7kamura.com/articles/2020-11-02-lookplus",
      0.39103024934082137
    ],
    [
      "https://r7kamura.com/articles/2020-10-12-ember-restored",
      0.3759286934329018
    ],
    [
      "https://r7kamura.com/articles/2014-08-31-h",
      0.33743028929351304
    ]
  ],
</code></pre>
<p>With output like this, a single command can generate JSON for related entries. If a static site build reads that JSON, related-article features should be fairly easy to add to static site generators.</p>
<p>If you have persistent compute resources, something like Elasticsearch's <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-mlt-query.html">More like this</a> should produce more accurate related entries. But for static site generators, there is value in a command that can run casually at build time without maintaining any external state.</p>
<h3>Technical Notes</h3>
<p>Nothing complicated is happening. It uses the kind of document similarity method that appears in introductory machine learning material: count terms, calculate TF-IDF, and find similar documents by cosine similarity. For Japanese tokenization it uses MeCab through <a href="https://github.com/polm/fugashi">fugashi</a>, a Python wrapper that is easy to use and makes dictionaries easy to install. TF-IDF and cosine similarity are handled entirely by <a href="https://github.com/hotchpotch/similar-documents-cli/blob/main/similar_documents/similar.py">scikit-learn</a>. It is a classical method, but in practice it gives fairly reasonable related articles.</p>
<p>At the moment, <code>.md</code> and <code>.html</code> files are converted to text through parsers for their formats, and all other files are treated as plain text. In TF-IDF, terms that appear across many documents receive lower scores. So if every file uses the same particular format, words specific to that format should have limited effect on the score, even though converting to clean text is still preferable. That is why this simple approach seems to work reasonably well.</p>]]></content:encoded>
  <pubDate>Tue, 27 Apr 2021 00:00:00 GMT</pubDate>
  <lastBuildDate>Tue, 27 Apr 2021 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>When sharp fails to install on WSL</title>
  <link>https://hotchpotch.dev/articles/wsl-sharp/</link>
  <guid>https://hotchpotch.dev/articles/wsl-sharp/</guid>
  <description>On WSL2, sharp tried to fall back to a local build because APPDATA pointed at a Windows npm cache path. Clearing APPDATA let the Linux prebuilt binary install correctly.</description>
  <content:encoded><![CDATA[<p>This was a small but annoying trap. When I ran <code>yarn add sharp</code> on Linux inside WSL2, sharp started trying to build itself locally and failed with:</p>
<pre><code>"/usr/include/vips/vips8:35:10: fatal error: glib-object.h:"
</code></pre>
<p>The confusing part was that the libvips-related packages existed, and <code>/usr/include/glib-2.0/glib-object.h</code> existed too. I tried several things, but none of them fixed it.</p>
<p>Looking more carefully at the other error messages, I found that it first failed here:</p>
<pre><code>sharp: Installation error: ENOENT: no such file or directory, mkdir 'C:\Users\hotch\AppData\Roaming/npm-cache'
</code></pre>
<p>After that failure, it tried to start a local build. But why was Linux inside WSL referring to a Windows path in the first place? Looking at the environment, I found that the <code>APPDATA</code> environment variable was being passed into WSL as-is, and that path was coming from there.</p>
<p>In sharp's install code, <a href="https://github.com/lovell/sharp/blob/43a085d1ae126016bd7c9be74881983554214394/lib/libvips.js#L32-L33">the cache path includes <code>env.APPDATA</code></a>, which made it refer to the odd Windows path.</p>
<p>When I set <code>APPDATA</code> to an empty string, which evaluates as false in Node.js, sharp used the prebuilt Linux binary from the cache and installed successfully.</p>
<pre><code>APPDATA='' yarn add sharp
</code></pre>
<p>This does not fix the underlying local-build problem, but sharp became usable, so that was good enough for now.</p>]]></content:encoded>
  <pubDate>Sun, 18 Apr 2021 00:00:00 GMT</pubDate>
  <lastBuildDate>Sun, 18 Apr 2021 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Fixing WSL clock drift after sleep from the Windows side</title>
  <link>https://hotchpotch.dev/articles/wsl-clock-sync/</link>
  <guid>https://hotchpotch.dev/articles/wsl-clock-sync/</guid>
  <description>When WSL2&apos;s clock drifted after waking from sleep, I fixed it by registering a Windows Task Scheduler command that runs hwclock inside WSL.</description>
  <content:encoded><![CDATA[<p>After developing in a WSL2 environment for the first time in a while, I noticed that the time was off. A problem I had not seen before seemed to have appeared: the WSL clock drifted after waking from sleep.</p>
<ul>
<li><a href="https://github.com/microsoft/WSL/issues/5324">https://github.com/microsoft/WSL/issues/5324</a></li>
</ul>
<p>This is an old issue, and it was <a href="https://devblogs.microsoft.com/commandline/servicing-the-windows-subsystem-for-linux-wsl-2-linux-kernel/#bug-fix-clock-sync">fixed in yesterday's Insider Preview Linux kernel</a>, so it will probably be fixed in Windows 10 21H1. At the moment, though, the problem still occurs.</p>
<p>On the Linux side, <code>hwclock --hctosys</code> fixes it. But running that command manually every time is annoying, and setting up periodic execution inside WSL2, for example with <code>cron.d</code>, is more troublesome than it first looks. So I prepared a batch file that runs the WSL-side command from Windows, then ran it from Windows Task Scheduler.</p>
<pre><code>wsl.exe -u root --exec /usr/sbin/hwclock --hctosys
</code></pre>
<p>There are several examples online for running a Task Scheduler job when the machine wakes from sleep, so I will skip that part. This solved the issue. After writing that much, I found <a href="https://github.com/microsoft/WSL/issues/5324#issuecomment-821666831">a comment on the issue that does the same thing in one line</a>.</p>
<pre><code>schtasks /Create /TN wsl-clock /TR "wsl.exe -u root sh -c hwclock -s" /SC ONEVENT /EC System /MO "*[System[Provider[@Name='Microsoft-Windows-Kernel-Power'] and (EventID=107 or EventID=507)]]" /F
</code></pre>
<p>Right, <a href="https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks">schtasks</a> can register tasks in Task Scheduler. With this command, the WSL clock drift after waking from sleep can be fixed fairly easily.</p>]]></content:encoded>
  <pubDate>Sat, 17 Apr 2021 00:00:00 GMT</pubDate>
  <lastBuildDate>Sat, 17 Apr 2021 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>NumPy cast overflow behavior can vary by environment and array size</title>
  <link>https://hotchpotch.dev/articles/numpy-cast/</link>
  <guid>https://hotchpotch.dev/articles/numpy-cast/</guid>
  <description>I ran into a NumPy casting issue where overflowing float32 values cast to uint8 behaved differently depending on the environment and array length.</description>
  <content:encoded><![CDATA[<p>I got caught by exactly what the title says: overflow behavior when casting in NumPy differed depending on the environment and the size of the data. It seems to take a different code path depending on the array length, and it took me a while to identify the cause. The correct answer is probably "do not pass overflowing data into a cast", but if behavior changes like this, I would at least appreciate a warning that an overflow happened.</p>
<p>This feels like the kind of bug that can look fine during development on a Mac, while already being broken, and then behave differently in production. I did not dig far enough to know whether this is specific to the Mac environment or whether it depends on the BLAS implementation, such as Intel MKL.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> platform
<span class="hljs-built_in">print</span>(platform.system())
<span class="hljs-comment"># オーバーフローして1になる</span>
<span class="hljs-built_in">print</span>(np.array([<span class="hljs-number">257.0</span>], dtype=<span class="hljs-string">"float32"</span>).astype(<span class="hljs-string">'uint8'</span>))
<span class="hljs-comment"># オーバーフローして1になる</span>
<span class="hljs-built_in">print</span>(np.array([<span class="hljs-number">257.0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>], dtype=<span class="hljs-string">"float32"</span>).astype(<span class="hljs-string">'uint8'</span>))
<span class="hljs-comment"># オーバーフローして1になるが正しい、と思いきや、環境によっては丸め込まれて255になる</span>
<span class="hljs-built_in">print</span>(np.array([<span class="hljs-number">257.0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>], dtype=<span class="hljs-string">"float32"</span>).astype(<span class="hljs-string">'uint8'</span>))
</code></pre>
<pre><code>Linux
[1]
[1 0 0 0 0 0 0]
[1 0 0 0 0 0 0 0]
</code></pre>
<pre><code>Windows
[1]
[1 0 0 0 0 0 0]
[1 0 0 0 0 0 0 0]
</code></pre>
<p>On my Intel Mac, the value is clamped instead.</p>
<pre><code>Darwin
[1]
[1 0 0 0 0 0 0]
[255   0   0   0   0   0   0   0]
</code></pre>]]></content:encoded>
  <pubDate>Wed, 31 Mar 2021 00:00:00 GMT</pubDate>
  <lastBuildDate>Wed, 31 Mar 2021 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Building a simple fully connected neural network with TensorFlow 2 without Keras</title>
  <link>https://hotchpotch.dev/articles/tensorflow-without-keras/</link>
  <guid>https://hotchpotch.dev/articles/tensorflow-without-keras/</guid>
  <description>A hands-on note implementing a simple feed-forward neural network with only TensorFlow APIs, without Keras, to understand layers, activation functions, losses, automatic differentiation, and manual training.</description>
  <content:encoded><![CDATA[<p>In TensorFlow, using the high-level Keras API makes it easy to create and train neural network models and do many other things you want to do with neural networks. But as a neural network beginner, I had been using it without really understanding what was happening. So I implemented a simple feed-forward neural network without Keras, using only TensorFlow APIs.</p>
<p>This article is implemented with reference to <a href="https://www.oreilly.co.jp/books/9784873117584/">Deep Learning from Scratch</a>. It is also a personal note for my own understanding. If you want to understand this properly, you should probably read Deep Learning from Scratch and the <a href="https://www.tensorflow.org/guide">TensorFlow guide</a>. The original <a href="https://gist.github.com/hotchpotch/a80e204d36943a6733284e42090a2340">Jupyter notebook is here</a>.</p>
<hr>
<p>By moving my hands and building everything once, I understood more about TensorFlow and neural network basics: which function affects what, what manual training feels like, how automatic differentiation works and how to use it, and why Keras is useful. Guide pages that I had not understood before became mostly readable.</p>
<p>Some very capable people I have seen can learn the logic and implement it in a program without much trouble. Even many ordinary capable people can read a book in an unfamiliar field and understand and implement it. In my case, that is often not enough. I often understand only after actually moving my hands and observing behavior. This reminded me of that again.</p>
<hr>
<p>The simple neural network implemented here looks like this:</p>
<ul>
<li>Create a layer with two weight parameters:
<ul>
<li>weights of shape <code>(input count, unit count)</code></li>
<li>bias weights of shape <code>(unit count,)</code></li>
<li>During forward propagation, apply an activation function to the input multiplied by weights plus bias.</li>
</ul>
</li>
<li>Create a network that manages layers.
<ul>
<li>During inference, apply layers in order, or forward propagation, and output the result.</li>
<li>Apply a loss function as a metric for how correct inference is.</li>
<li>During learning, which obtains optimal weight parameters from training data and applies them with a learning rate, calculate gradients so that the loss function becomes smaller, and update layer parameters little by little in reverse order with backpropagation. Backpropagation uses <a href="https://www.tensorflow.org/guide/autodiff">TensorFlow autodiff</a>.</li>
</ul>
</li>
<li>Give training data to this network and train it.</li>
</ul>
<p>First, implement a simple layer.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np
<span class="hljs-keyword">import</span> matplotlib.pyplot <span class="hljs-keyword">as</span> plt
<span class="hljs-keyword">import</span> tensorflow <span class="hljs-keyword">as</span> tf

<span class="hljs-comment"># GPU 使わない設定</span>
<span class="hljs-keyword">import</span> os
os.environ[<span class="hljs-string">'CUDA_VISIBLE_DEVICES'</span>] = <span class="hljs-string">'-1'</span>

<span class="hljs-comment"># seed の固定</span>
np.random.seed(<span class="hljs-number">42</span>)
tf.random.set_seed(<span class="hljs-number">42</span>)
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SimpleLayer</span>():
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, input_dim, units, activation</span>):
        <span class="hljs-comment"># ウェイトを正規分布で初期化。Variable で更新可能な値として作る。</span>
        <span class="hljs-variable language_">self</span>.w = tf.Variable(tf.random.normal([input_dim, units]) * <span class="hljs-number">0.01</span>, name=<span class="hljs-string">'weight'</span>)
        <span class="hljs-comment"># バイアスを 0 で初期化</span>
        <span class="hljs-variable language_">self</span>.b = tf.Variable(tf.zeros([units]), name=<span class="hljs-string">"bais"</span>)
        <span class="hljs-comment"># 活性化関数</span>
        <span class="hljs-variable language_">self</span>.activation = activation
    
<span class="hljs-meta">    @property</span>
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">weights</span>(<span class="hljs-params">self</span>):
        <span class="hljs-keyword">return</span> [<span class="hljs-variable language_">self</span>.w, <span class="hljs-variable language_">self</span>.b]

    <span class="hljs-keyword">def</span> <span class="hljs-title function_">forward</span>(<span class="hljs-params">self, x</span>):
        y = x @ <span class="hljs-variable language_">self</span>.w + <span class="hljs-variable language_">self</span>.b <span class="hljs-comment"># y = tf.matmul(x, self.w) + self.b # と同等</span>
        <span class="hljs-keyword">return</span> <span class="hljs-variable language_">self</span>.activation(y)

    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__call__</span>(<span class="hljs-params">self, x</span>):
        <span class="hljs-keyword">return</span> <span class="hljs-variable language_">self</span>.forward(x)
</code></pre>
<pre><code class="hljs language-python">identify_function = <span class="hljs-keyword">lambda</span> x: x
zero_function = <span class="hljs-keyword">lambda</span> x: x * <span class="hljs-number">0</span>

l1 = SimpleLayer(<span class="hljs-number">2</span>, <span class="hljs-number">2</span>, identify_function)
l2 = SimpleLayer(<span class="hljs-number">2</span>, <span class="hljs-number">1</span>, zero_function)

<span class="hljs-built_in">print</span>(<span class="hljs-string">f'l1 weights: <span class="hljs-subst">{l1.weights}</span>'</span>)
a1 = l1([[<span class="hljs-number">10</span>, <span class="hljs-number">20</span>]]) <span class="hljs-comment"># SimpleLayer.__call__ を呼び出す</span>
<span class="hljs-built_in">print</span>(<span class="hljs-string">f'a1: <span class="hljs-subst">{a1}</span>'</span>)
a2 = l2(a1)
<span class="hljs-built_in">print</span>(<span class="hljs-string">f'a2: <span class="hljs-subst">{a2}</span>'</span>)

<span class="hljs-built_in">print</span>(l2(l1([[<span class="hljs-number">10</span>, <span class="hljs-number">20</span>]])))
</code></pre>
<p>Next, implement several simple activation functions.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">step_function</span>(<span class="hljs-params">x:tf.Tensor</span>):
    <span class="hljs-keyword">return</span> tf.cast(x > <span class="hljs-number">0</span>, tf.uint8)

step_function(tf.constant([<span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">3</span>, -<span class="hljs-number">3</span>]))
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">sigmoid</span>(<span class="hljs-params">x:tf.Tensor</span>):
    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span> / (<span class="hljs-number">1</span> + tf.exp(-x))

sigmoid(tf.constant([<span class="hljs-number">0</span>, <span class="hljs-number">1.0</span>, -<span class="hljs-number">2.0</span>]))
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">relu</span>(<span class="hljs-params">x: tf.Tensor</span>):
    <span class="hljs-keyword">return</span> tf.maximum(<span class="hljs-number">0.0</span>, x)

relu(tf.constant([-<span class="hljs-number">2.0</span>, -<span class="hljs-number">1.0</span>, <span class="hljs-number">1.0</span>, <span class="hljs-number">2.0</span>]))
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">tanh</span>(<span class="hljs-params">x: tf.Tensor</span>):
    <span class="hljs-keyword">return</span> (tf.exp(x) - tf.exp(-x)) / (tf.exp(x) + tf.exp(-x))

tanh(tf.constant([-<span class="hljs-number">3.0</span>,-<span class="hljs-number">1.0</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">1.0</span>,<span class="hljs-number">3.0</span>]))
</code></pre>
<p>For output layer activation functions, implement the identity function, which does nothing, and softmax, which is used for classification problems.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">identity</span>(<span class="hljs-params">x: tf.Tensor</span>):
    <span class="hljs-keyword">return</span> x

identity(tf.constant([<span class="hljs-number">1.0</span>, <span class="hljs-number">0.0</span>, -<span class="hljs-number">1.0</span>, -<span class="hljs-number">3.0</span>]))
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">softmax</span>(<span class="hljs-params">x:tf.Tensor</span>):
    e = tf.exp(x -tf.reduce_max(x))
    s = tf.reduce_sum(e)
    <span class="hljs-keyword">return</span> e / s

<span class="hljs-built_in">print</span>(softmax(tf.constant([<span class="hljs-number">0.3</span>,<span class="hljs-number">2.9</span>,<span class="hljs-number">4.0</span>])))
<span class="hljs-built_in">print</span>(softmax(tf.constant([<span class="hljs-number">1010.0</span>, <span class="hljs-number">1000</span>, <span class="hljs-number">990</span>])))
</code></pre>
<p>Next, implement loss functions: sum of squared error and cross entropy error for classification models, and root mean squared error for regression models.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">sum_squared_error</span>(<span class="hljs-params">x:tf.Tensor, y: tf.Tensor</span>):
    <span class="hljs-keyword">return</span> tf.reduce_mean(<span class="hljs-number">0.5</span> * tf.reduce_sum((x-y) ** <span class="hljs-number">2</span>, axis=tf.rank(x)-<span class="hljs-number">1</span>))

y1 = [<span class="hljs-number">0.0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>]
y2 = [<span class="hljs-number">0.0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>]
x1 = [<span class="hljs-number">0.1</span>, <span class="hljs-number">0.05</span>, <span class="hljs-number">0.6</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.05</span>, <span class="hljs-number">0.1</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.1</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.0</span>]

<span class="hljs-built_in">print</span>(sum_squared_error(tf.constant(x1), tf.constant(y1)))
<span class="hljs-built_in">print</span>(sum_squared_error(tf.constant(x1), tf.constant(y2)))
<span class="hljs-built_in">print</span>(sum_squared_error(tf.constant([x1, x1]), tf.constant([y1, y2])))
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">cross_entropy_error</span>(<span class="hljs-params">x:tf.Tensor, y: tf.Tensor</span>):
    delta = tf.constant(<span class="hljs-number">1e-7</span>)
    <span class="hljs-keyword">if</span> tf.rank(x) == <span class="hljs-number">1</span>:
        x = tf.reshape(x, (<span class="hljs-number">1</span>, tf.size(x)))
        y = tf.reshape(y, (<span class="hljs-number">1</span>, tf.size(y)))
    batch_size = x.shape[<span class="hljs-number">0</span>]
    <span class="hljs-keyword">return</span> -tf.reduce_sum(y * tf.math.log(x + <span class="hljs-number">1e-7</span>)) / batch_size

<span class="hljs-built_in">print</span>(cross_entropy_error(tf.constant(x1), tf.constant(y1)))
<span class="hljs-built_in">print</span>(cross_entropy_error(tf.constant(x1), tf.constant(y2)))
<span class="hljs-built_in">print</span>(cross_entropy_error(tf.constant([x1, x1]), tf.constant([y1, y2])))
</code></pre>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">root_mean_squared_error</span>(<span class="hljs-params">x:tf.Tensor, y: tf.Tensor</span>):
    diff = y - x
    <span class="hljs-keyword">return</span> tf.sqrt(tf.reduce_mean((diff)**<span class="hljs-number">2</span>))

y = [[<span class="hljs-number">100.0</span>], [<span class="hljs-number">160</span>], [<span class="hljs-number">60</span>]]
x = [[<span class="hljs-number">80.0</span>], [<span class="hljs-number">100</span>], [<span class="hljs-number">100</span>]]

<span class="hljs-built_in">print</span>(root_mean_squared_error(tf.constant(x), tf.constant(y)))
</code></pre>
<p>Next, check TensorFlow behavior for gradient calculation with automatic differentiation.</p>
<p>For the function <code>f1</code> below, the derivative at <code>x=3</code> is 40.</p>
<pre><code class="hljs language-python">f1 = <span class="hljs-keyword">lambda</span> x: x**<span class="hljs-number">3</span> + <span class="hljs-number">2</span>*x**<span class="hljs-number">2</span> + x

x = tf.Variable(<span class="hljs-number">3.0</span>)
<span class="hljs-keyword">with</span> tf.GradientTape() <span class="hljs-keyword">as</span> tape:
    z = f1(x)
tape.gradient(z, [x])
</code></pre>
<p>Next, partial differentiation. For the function <code>f2</code> below, when <code>w1</code> and <code>w2</code> are 5 and 3, the partial derivatives with respect to <code>w1</code> and <code>w2</code> are 36 and 10. This code is from <a href="https://github.com/ageron/handson-ml2/blob/master/12_custom_models_and_training_with_tensorflow.ipynb">Chapter 12 - Custom Models and Training with TensorFlow</a>.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">f2</span>(<span class="hljs-params">w1, w2</span>):
    <span class="hljs-keyword">return</span> <span class="hljs-number">3</span> * w1**<span class="hljs-number">2</span> + <span class="hljs-number">2</span>*w1 * w2

w1, w2 = tf.Variable(<span class="hljs-number">5.0</span>), tf.Variable(<span class="hljs-number">3.0</span>)

<span class="hljs-keyword">with</span> tf.GradientTape() <span class="hljs-keyword">as</span> tape:
    z = f2(w1, w2)
<span class="hljs-built_in">print</span>(tape.gradient(z, [w1, w2]))

<span class="hljs-keyword">try</span>:
    <span class="hljs-built_in">print</span>(tape.gradient(z, [w1, w2]))
<span class="hljs-keyword">except</span> RuntimeError:
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'二回目の呼び出し時には、リソースが削除されていてエラーになる'</span>)

<span class="hljs-keyword">with</span> tf.GradientTape(persistent=<span class="hljs-literal">True</span>) <span class="hljs-keyword">as</span> tape:
    z = f2(w1, w2)

<span class="hljs-built_in">print</span>(tape.gradient(z, [w1]))
<span class="hljs-built_in">print</span>(tape.gradient(z, [w2]))
<span class="hljs-keyword">del</span> tape <span class="hljs-comment"># 開放する</span>
</code></pre>
<p>Now check whether the functions and layers created so far behave as intended by trying a simple linear-function prediction. Create data based on <code>2x + 10</code> with small random noise.</p>
<pre><code class="hljs language-python">x = np.arange(-<span class="hljs-number">50</span>, <span class="hljs-number">50</span>, <span class="hljs-number">2</span>)
line_2x_1 = <span class="hljs-number">2</span> * x  + <span class="hljs-number">10</span>
noise = -<span class="hljs-number">10</span> * np.random.rand(<span class="hljs-built_in">len</span>(x)) + <span class="hljs-number">5</span>
dots_2x_1 = line_2x_1 + noise
plt.plot(x, line_2x_1)
plt.plot(x, dots_2x_1, <span class="hljs-string">'o'</span>)
plt.show()

y = tf.expand_dims(tf.constant(dots_2x_1, dtype=tf.float32), axis=<span class="hljs-number">1</span>)
X = tf.expand_dims(tf.constant(x, dtype=tf.float32), axis=<span class="hljs-number">1</span>)
</code></pre>
<p>First, without using a neural network, check whether it works well with sklearn.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> sklearn.linear_model <span class="hljs-keyword">import</span> LinearRegression
reg = LinearRegression().fit(X, y)
<span class="hljs-built_in">print</span>(reg.score(X, y))
reg.predict([[-<span class="hljs-number">50</span>], [<span class="hljs-number">0</span>], [<span class="hljs-number">50</span>], [<span class="hljs-number">100</span>]])
</code></pre>
<pre><code class="hljs language-text">0.9975753493086111

array([[-89.75652361],
       [ 10.54682827],
       [110.85018015],
       [211.15353203]])
</code></pre>
<p>Next, train a two-layer neural network as a regression model. If it can predict regression similarly to sklearn, that is fine.</p>
<pre><code class="hljs language-python">layer1 = SimpleLayer(<span class="hljs-number">1</span>, <span class="hljs-number">32</span>, relu)
layer2 = SimpleLayer(<span class="hljs-number">32</span>, <span class="hljs-number">1</span>, identify_function)

loss_function = root_mean_squared_error
lr = <span class="hljs-number">0.003</span>

predict = <span class="hljs-keyword">lambda</span> x: layer2(layer1(x))
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">10000</span>):
    <span class="hljs-comment"># 勾配を求める</span>
    <span class="hljs-keyword">with</span> tf.GradientTape() <span class="hljs-keyword">as</span> tape:
        y_pred = predict(X)
        z = loss_function(y_pred, y)
    (l1_w_grads, l1_b_grads), (l2_w_grads, l2_b_grads) = tape.gradient(z, [layer1.weights, layer2.weights])
    <span class="hljs-comment"># SDG で勾配を学習する</span>
    layer1.w.assign_sub(lr * l1_w_grads)
    layer1.b.assign_sub(lr * l1_b_grads)
    layer2.w.assign_sub(lr * l2_w_grads)
    layer2.b.assign_sub(lr * l2_b_grads)
    <span class="hljs-keyword">if</span> (i % <span class="hljs-number">1000</span> == <span class="hljs-number">0</span>):
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'iter {} / train loss: {:.3}'</span>.<span class="hljs-built_in">format</span>(i, z.numpy()))

<span class="hljs-built_in">print</span>(<span class="hljs-string">'train loss: {:.3}'</span>.<span class="hljs-built_in">format</span>(loss_function(predict(X), y)))
<span class="hljs-built_in">print</span>(predict(tf.constant([[-<span class="hljs-number">50</span>], [<span class="hljs-number">0</span>], [<span class="hljs-number">50</span>], [<span class="hljs-number">100</span>]], dtype=tf.float32)))
</code></pre>
<pre><code class="hljs language-text">iter 0 / train loss: 58.6
iter 1000 / train loss: 5.08
iter 2000 / train loss: 3.71
iter 3000 / train loss: 3.79
iter 4000 / train loss: 3.5
iter 5000 / train loss: 3.3
iter 6000 / train loss: 3.18
iter 7000 / train loss: 3.09
iter 8000 / train loss: 3.03
iter 9000 / train loss: 2.98
train loss: 2.94
tf.Tensor(
[[-92.20102  ]
 [ 10.1016445]
 [109.65451  ]
 [209.20737  ]], shape=(4, 1), dtype=float32)
</code></pre>
<p>It seems to work. Next, implement a network that handles the training above more conveniently.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SimpleSequenceNetwork</span>:
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, layers, loss_function, lr=<span class="hljs-number">0.01</span></span>):
        <span class="hljs-variable language_">self</span>.layers = layers
        <span class="hljs-variable language_">self</span>.loss_function = loss_function
        <span class="hljs-variable language_">self</span>.lr = lr
    
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">predict</span>(<span class="hljs-params">self, x</span>):
        <span class="hljs-keyword">for</span> layer <span class="hljs-keyword">in</span> <span class="hljs-variable language_">self</span>.layers:
            x = layer(x)
        <span class="hljs-keyword">return</span> x

    <span class="hljs-keyword">def</span> <span class="hljs-title function_">loss</span>(<span class="hljs-params">self, x, target</span>):
        y = <span class="hljs-variable language_">self</span>.predict(x)
        <span class="hljs-keyword">return</span> <span class="hljs-variable language_">self</span>.loss_function(y, target)

    <span class="hljs-keyword">def</span> <span class="hljs-title function_">accuracy</span>(<span class="hljs-params">self, x, target</span>):
        y = <span class="hljs-variable language_">self</span>.predict(x)
        y = tf.argmax(y, axis=<span class="hljs-number">1</span>)
        target = tf.argmax(target, axis=<span class="hljs-number">1</span>)

        accuracy = tf.math.count_nonzero(y == target) / x.shape[<span class="hljs-number">0</span>]
        <span class="hljs-keyword">return</span> accuracy
    
<span class="hljs-meta">    @property</span>
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">all_weights</span>(<span class="hljs-params">self</span>):
        <span class="hljs-keyword">return</span> tf.nest.flatten([layer.weights <span class="hljs-keyword">for</span> layer <span class="hljs-keyword">in</span> <span class="hljs-variable language_">self</span>.layers])

    <span class="hljs-comment"># 損失関数の、重みパラメータに対する勾配を求める</span>
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">gradient</span>(<span class="hljs-params">self, x, target</span>):
        <span class="hljs-keyword">with</span> tf.GradientTape() <span class="hljs-keyword">as</span> tape:
            tape.watch(x)
            z = <span class="hljs-variable language_">self</span>.loss(x, target)
        <span class="hljs-keyword">return</span> tape.gradient(z, <span class="hljs-variable language_">self</span>.all_weights)

    <span class="hljs-comment"># 単純な勾配下降法(SDG)で、レイヤーの重みパラメータを更新する</span>
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">update_variables_by_sdg</span>(<span class="hljs-params">self, grads</span>):
        <span class="hljs-keyword">for</span> (grad, val) <span class="hljs-keyword">in</span> <span class="hljs-built_in">zip</span>(grads, <span class="hljs-variable language_">self</span>.all_weights):
            val.assign_sub(<span class="hljs-variable language_">self</span>.lr * grad)

    <span class="hljs-comment"># 勾配を求め、パラメータを更新する</span>
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">training</span>(<span class="hljs-params">self, x, target</span>):
        grads = <span class="hljs-variable language_">self</span>.gradient(x, target)
        <span class="hljs-variable language_">self</span>.update_variables_by_sdg(grads)
</code></pre>
<p>Load the dataset for training. Use the familiar MNIST digit data from 0 to 9.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> tensorflow_datasets <span class="hljs-keyword">as</span> tfds

ds = tfds.load(<span class="hljs-string">"mnist"</span>, as_supervised=<span class="hljs-literal">True</span>)
test_ds = ds[<span class="hljs-string">'test'</span>]
train_ds = ds[<span class="hljs-string">'train'</span>]

<span class="hljs-keyword">for</span> (i, (image, label)) <span class="hljs-keyword">in</span> <span class="hljs-built_in">enumerate</span>(train_ds.take(<span class="hljs-number">12</span>)):
    plt.subplot(<span class="hljs-number">3</span>, <span class="hljs-number">4</span>, i+<span class="hljs-number">1</span>)
    plt.imshow(image, cmap=<span class="hljs-string">'gray'</span>)
    plt.subplots_adjust(wspace=<span class="hljs-number">0</span>, hspace=<span class="hljs-number">1</span>)
    plt.title(label.numpy())
    plt.axis(<span class="hljs-string">'off'</span>)
plt.show()

<span class="hljs-keyword">def</span> <span class="hljs-title function_">preprocess</span>(<span class="hljs-params">image, label</span>):
    <span class="hljs-comment"># 画像は (28,28,1) を (784,) にして、0.～1. の範囲へ</span>
    image = tf.cast(tf.reshape(image, (-<span class="hljs-number">1</span>,)), tf.float32) / <span class="hljs-number">255.0</span>
    <span class="hljs-comment"># ラベルはワンホットベクトルに</span>
    label = tf.one_hot(label, <span class="hljs-number">10</span>, dtype=tf.float32)
    <span class="hljs-keyword">return</span> image, label

train_ds = train_ds.<span class="hljs-built_in">map</span>(preprocess, num_parallel_calls=tf.data.AUTOTUNE).cache()
test_ds = test_ds.<span class="hljs-built_in">map</span>(preprocess, num_parallel_calls=tf.data.AUTOTUNE).cache()

<span class="hljs-built_in">print</span>((<span class="hljs-built_in">len</span>(train_ds), <span class="hljs-built_in">len</span>(test_ds)))
</code></pre>
<pre><code class="hljs language-text">(60000, 10000)
</code></pre>
<p>Create a function that trains the network based on the dataset.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">fit</span>(<span class="hljs-params">network, train_ds: tf.data.Dataset, test_ds: tf.data.Dataset, epochs=<span class="hljs-number">20</span>, batch_size=<span class="hljs-number">32</span></span>):
    history_train_loss, history_train_accuracy, history_test_accuracy = [], [], []
    <span class="hljs-keyword">for</span> epoch <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">1</span>, epochs + <span class="hljs-number">1</span>):
        train_loss, train_accuracy, test_accuracy = [], [], []
        <span class="hljs-keyword">for</span> (X_batch, y_batch) <span class="hljs-keyword">in</span> train_ds.shuffle(<span class="hljs-number">1000</span>).batch(batch_size).prefetch(<span class="hljs-number">1</span>):
            network.training(X_batch, y_batch)
            train_loss.append(network.loss(X_batch, y_batch))
            train_accuracy.append(network.accuracy(X_batch, y_batch))
        <span class="hljs-keyword">for</span> (X_batch, y_batch) <span class="hljs-keyword">in</span> test_ds.shuffle(<span class="hljs-number">1000</span>).batch(batch_size).prefetch(<span class="hljs-number">1</span>):
            test_accuracy.append(network.accuracy(X_batch, y_batch))
        
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"train acc, test acc, train loss | {:.4}, {:.4}, {:.4}"</span>.<span class="hljs-built_in">format</span>(
            tf.reduce_mean(train_accuracy).numpy(),
            tf.reduce_mean(test_accuracy).numpy(),
            tf.reduce_mean(train_loss).numpy()
        ))
        history_train_loss.extend(train_loss)
        history_train_accuracy.extend(train_accuracy)
        history_test_accuracy.extend(test_accuracy)
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">'train_loss'</span>: np.array(history_train_loss),
        <span class="hljs-string">'train_accuracy'</span>: np.array(history_train_accuracy),
        <span class="hljs-string">'test_accuracy'</span>: np.array(history_test_accuracy)
    }
</code></pre>
<p>Actually create layers and a neural network, then train it.</p>
<pre><code class="hljs language-python">%%time
input_layer = SimpleLayer(<span class="hljs-number">784</span>, <span class="hljs-number">100</span>, relu)
hidden_layer = SimpleLayer(<span class="hljs-number">100</span>, <span class="hljs-number">50</span>, relu)
output_layer = SimpleLayer(<span class="hljs-number">50</span>, <span class="hljs-number">10</span>, softmax) 
network = SimpleSequenceNetwork([input_layer, hidden_layer, output_layer], cross_entropy_error, lr=<span class="hljs-number">0.1</span>)

history = fit(network, train_ds, test_ds, epochs=<span class="hljs-number">5</span>, batch_size=<span class="hljs-number">32</span>)
plt.plot(history[<span class="hljs-string">'train_loss'</span>])
plt.show()
</code></pre>
<pre><code class="hljs language-text">train acc, test acc, train loss | 0.7391, 0.9364, 4.336
train acc, test acc, train loss | 0.9798, 0.9572, 3.604
train acc, test acc, train loss | 0.9897, 0.9674, 3.547
train acc, test acc, train loss | 0.9936, 0.9695, 3.524
train acc, test acc, train loss | 0.9954, 0.9706, 3.511
</code></pre>
<p>Try replacing the activation function.</p>
<pre><code class="hljs language-python">%%time
input_layer = SimpleLayer(<span class="hljs-number">784</span>, <span class="hljs-number">100</span>, tanh)
hidden_layer = SimpleLayer(<span class="hljs-number">100</span>, <span class="hljs-number">50</span>, tanh)
output_layer = SimpleLayer(<span class="hljs-number">50</span>, <span class="hljs-number">10</span>, softmax) 
network = SimpleSequenceNetwork([input_layer, hidden_layer, output_layer], cross_entropy_error, lr=<span class="hljs-number">0.1</span>)

history = fit(network, train_ds, test_ds, epochs=<span class="hljs-number">5</span>, batch_size=<span class="hljs-number">32</span>)
plt.plot(history[<span class="hljs-string">'train_loss'</span>])
plt.show()
</code></pre>
<pre><code class="hljs language-text">train acc, test acc, train loss | 0.774, 0.9139, 4.245
train acc, test acc, train loss | 0.953, 0.9455, 3.687
train acc, test acc, train loss | 0.973, 0.9567, 3.607
train acc, test acc, train loss | 0.9815, 0.9625, 3.57
train acc, test acc, train loss | 0.9869, 0.9663, 3.548
</code></pre>
<p>With this simple neural network, MNIST digit label classification also worked reasonably well. The hardest part, updating weights through backpropagation, can be done easily with TensorFlow autodiff, so I did not need to write that process myself.</p>]]></content:encoded>
  <pubDate>Sun, 28 Mar 2021 00:00:00 GMT</pubDate>
  <lastBuildDate>Sun, 28 Mar 2021 00:00:00 GMT</lastBuildDate>
</item>
<item>
  <title>Inferring Hiragana in the Browser with TensorFlow.js</title>
  <link>https://hotchpotch.dev/articles/hiragana-tensorflow-js/</link>
  <guid>https://hotchpotch.dev/articles/hiragana-tensorflow-js/</guid>
  <description>I built a small TensorFlow.js demo that recognizes handwritten hiragana in the browser, then looked at model size, conversion from Keras, and the limits of importing Python-trained models into JavaScript.</description>
  <content:encoded><![CDATA[<p>While trying out TensorFlow.js, I made a demo that recognizes hiragana using only the browser.</p>
<ul>
<li><a href="https://tfjs-hiragana.surge.sh/">https://tfjs-hiragana.surge.sh/</a></li>
</ul>
<p><img src="https://i.imgur.com/H7sGPcr.gif" alt="Hiragana inference"></p>
<p>The model gets about 99.0% accuracy on the original dataset. In the browser, though, it makes a fair number of mistakes. I suspect this is because the training data contains handwriting from pens and brushes, while browser input has a different feel. Characters such as "か" are especially hard.</p>
<p>The trained weights loaded by the browser are 437 KB, and the model metadata is 49 KB. I trained a simple fully connected network in Python TensorFlow: 48x48 grayscale hiragana images, about 700,000 characters, 20 layers, 100 neurons, and 71 output classes. The original Keras model was about 5 MB. After converting it for JavaScript with <code>tensorflowjs_converter --quantize_uint8 --input_format keras input.h5 output_dir</code>, the model metadata became about 49 KB of JSON and the trained data about 437 KB. gzip reduces it by another 20% or so.</p>
<p>The source data is the <a href="https://github.com/ndl-lab/hiragana_mojigazo">Character Image Dataset: 73 Hiragana Characters</a>. Its image license is <a href="https://creativecommons.org/publicdomain/mark/1.0/deed.ja">Public Domain Mark 1.0</a>. I used 71 characters from it, excluding ゑ and ゐ.</p>
<p>Converting from Keras to TensorFlow.js is easy if you follow the <a href="https://www.tensorflow.org/js/tutorials/conversion/import_keras">documentation</a>, but only officially supported APIs can be imported.</p>
<blockquote>
<p>Models using unsupported ops or layers, e.g. custom layers, Lambda layers, custom losses, or custom metrics, cannot be automatically imported, because they depend on Python code that cannot be reliably translated into JavaScript.</p>
</blockquote>
<p>Custom loss functions and similar pieces are common, so models that are not fairly plain Keras or Python TensorFlow models may not import cleanly. If the final target is TensorFlow.js, it may be better to design the Python model with that in mind, or to build and train the model in TensorFlow.js from the beginning. Node + GPU support seems to exist now, and the <a href="https://js.tensorflow.org/api/latest/">API is also fairly complete</a>.</p>
<hr>
<p>TensorFlow.js and <a href="https://www.tensorflow.org/lite?hl=ja">TensorFlow Lite</a> are interesting because, despite their constraints, they let you run inference on browsers, smartphones, and other edge devices. I was impressed by how casually this kind of inference can now be done.</p>]]></content:encoded>
  <pubDate>Mon, 15 Mar 2021 00:00:00 GMT</pubDate>
  <lastBuildDate>Mon, 15 Mar 2021 00:00:00 GMT</lastBuildDate>
</item>
</channel>
</rss>
