langchainとDatabricksで(私が)学ぶRAG :日本語でセマンティックチャンキング(もどき)

データの処理は大事です。

# 導入

LLMにおけるRAGは様々な問題を抱えつつ、かなり一般化してきている感があります。
その中でも適切なデータの取得・加工、チャンキングは非常に重要な処理だと思います。

セマンティックチャンキングは文章の類似度を基にチャンク分けする手法です。
以下記事のLevel4として紹介されています。

https://medium.com/@anuragmishra_27746/five-levels-of-chunking-strategies-in-rag-notes-from-gregs-video-7b735895694d

すごく雑な理解で言えば、連続する文章についてコサイン距離を計算し、その差が大きいところをチャンクの切れ目としてチャンク分けするという考え方です。

LangChainやLlamaIndexでも実装されています。

https://python.langchain.com/docs/modules/data_connection/document_transformers/semantic-chunker/

https://docs.llamaindex.ai/en/stable/examples/node_parsers/semantic_chunking/

ただ、標準だと日本語に対応していないようなので、LangChainの```SemanticChunker```を雑に日本語対応してセマンティックチャンキングしてみます。

検証はDatabricksを利用しました。

# Step1. パッケージのインストール

langchain等をインストール。
LangChainのセマンティックチャンキング機能は```langchan_experimental```パッケージ側に含まれるため、合わせてインストールします。

```python
%pip install -U -qq langchain langchain_experimental transformers

dbutils.library.restartPython()
```

# Step2. チャンキング用サンプルデータの準備

Wikipediaから適当にチャンキング用のサンプルデータを作成します。

```python
import requests


def get_wikipedia_page(title: str):
    """
    Retrieve the full text content of a Wikipedia page.

    :param title: str - Title of the Wikipedia page.
    :return: str - Full text content of the page as raw string.
    """
    # Wikipedia API endpoint
    URL = "https://ja.wikipedia.org/w/api.php"

    # Parameters for the API request
    params = {
        "action": "query",
        "format": "json",
        "titles": title,
        "prop": "extracts",
        "explaintext": True,
    }

    # Custom User-Agent header to comply with Wikipedia's best practices
    headers = {"User-Agent": "tutorial/0.0.1"}

    response = requests.get(URL, params=params, headers=headers)
    data = response.json()

    # Extracting page content
    page = next(iter(data["query"]["pages"].values()))
    return page["extract"] if "extract" in page else None

full_document = get_wikipedia_page("葬送のフリーレン")

full_document
```

```:出力
'『葬送のフリーレン』（そうそうのフリーレン）は、山田鐘人（原作）、アベツカサ（作画）による日本の漫画。『週刊少年サンデー』（小学館）にて、2020年22・23合併号より連載中。
（以下省略）
```

# Step3. 埋め込みモデルの準備

埋め込みモデルをロード。
以下でも紹介した事前ダウンロード済みのBGE-M3を利用します。

https://qiita.com/isanakamishiro2/items/e4f67586b4cb5f171ea9

```python
import torch
from langchain_community.embeddings import HuggingFaceBgeEmbeddings

device = "cuda" if torch.cuda.is_available() else "cpu"

model_path = "/Volumes/training/llm/model_snapshots/models--BAAI--bge-m3"
model_kwargs = {"device": device}
encode_kwargs = {"normalize_embeddings": True}  # Cosine Similarity

embeddings = HuggingFaceBgeEmbeddings(
    model_name=model_path,
    model_kwargs=model_kwargs,
    encode_kwargs=encode_kwargs,
)
```

# Step4. 日本語用セマンティックチャンカーを準備・実行

今回のポイント。

日本語対応のSemanticChunkerを用意します。

LangChainのオリジナルSemanticChunkerは与えられたテキストを最初に正規表現で文に分割するのですが、分割に使う記号が「., !, ?」の3種なので、日本語はほぼ分割されません。
セマンティックチャンキングは「文などの単位に分割されたテキストをコサイン距離に基づいてグループ化する」処理に近いため、最初に文として分割できない日本語では正しく処理できないことになります。

というわけで、オリジナルSemanticChunkerの文分割部分(```split_text```メソッド)をオーバーライドして、日本語も文分割できるように修正します。
そこ意外は、以下のソースほぼそのままです。

https://api.python.langchain.com/en/latest/_modules/langchain_experimental/text_splitter.html#SemanticChunker

また、作成したクラスを使ってStep2で取得したテキストをチャンク分割します。

```python
import copy
import re
from typing import Any, Dict, Iterable, List, Literal, Optional, Sequence, Tuple, cast
from langchain_experimental.text_splitter import SemanticChunker

class JapaneseSemanticChunker(SemanticChunker):
    def split_text(
        self,
        text: str,
    ) -> List[str]:
        # ここが変更点。日本語句読点と改行コードでも文の終わりとして分割
        single_sentences_list = re.split(r"(?<=[.?!。、])\s+|\n", text)

        # having len(single_sentences_list) == 1 would cause the following
        # np.percentile to fail.
        if len(single_sentences_list) == 1:
            return single_sentences_list
        distances, sentences = self._calculate_sentence_distances(single_sentences_list)
        if self.number_of_chunks is not None:
            breakpoint_distance_threshold = self._threshold_from_clusters(distances)
        else:
            breakpoint_distance_threshold = self._calculate_breakpoint_threshold(
                distances
            )

        indices_above_thresh = [
            i for i, x in enumerate(distances) if x > breakpoint_distance_threshold
        ]

        chunks = []
        start_index = 0

        # Iterate through the breakpoints to slice the sentences
        for index in indices_above_thresh:
            # The end index is the current breakpoint
            end_index = index

            # Slice the sentence_dicts from the current start index to the end index
            group = sentences[start_index : end_index + 1]
            combined_text = " ".join([d["sentence"] for d in group])
            chunks.append(combined_text)

            # Update the start index for the next group
            start_index = index + 1

        # The last group, if any sentences remain
        if start_index < len(sentences):
            combined_text = " ".join([d["sentence"] for d in sentences[start_index:]])
            chunks.append(combined_text)
        return chunks

# 上記クラスのText Splitterを作成
text_splitter = JapaneseSemanticChunker(embeddings)

# Wikipediaから取得したテキストをチャンクに分割
docs = text_splitter.create_documents([full_document])
```

# Step5. チャンキング結果の確認

チャンク分けした結果を確認します。

```python
from pprint import pprint

for d in docs:
    print(d.page_content)
    print("---" * 30)
```

一部だけ抜粋。
特段チャンク数などを指定していないため、割と大きい塊で固まっています。
※　ちなみに正規表現分割直後は516に分割されました。最終的には27のチャンクに集約されています。


```:出力
『葬送のフリーレン』（そうそうのフリーレン）は、山田鐘人（原作）、アベツカサ（作画）による日本の漫画。『週刊少年サンデー』（小学館）にて、2020年22・23合併号より連載中。 2021年に第14回マンガ大賞および第25回手塚治虫文化賞新生賞を、2023年に第69回小学館漫画賞受賞。
------------------------------------------------------------------------------------------
== 概要 == 魔王を倒した勇者一行の後日譚を描くファンタジー。 原作担当の山田の前作である「ぼっち博士とロボット少女の絶望的ユートピア」の連載終了後、いくつかの読切のネームを描くもうまくいかず、担当編集者から、最初の受賞作が勇者・魔王物のコメディーだったことから、その方向でギャグを描いてみてはと提案したところ、いきなり「葬送のフリーレン」の第1話のネームが上がってきた。その後、作画担当をつけることになり、同じく担当していたアベにネームを見せたところ「描いてみたい」と反応があり、フリーレンのキャラ絵を描いてもらったところ、山田からも「この方ならお願いしたい」と返答をもらったため、アベが作画担当になった。ちなみにマンガ大賞を受賞した2021年3月現在、山田とアベは一度も会ったことがないという。 本作のタイトルの由来は、山田が考えたタイトル案がありながら、編集部でも検討をし、編集部会議で「いいタイトルが決まったら自腹で賞金1万円出します」と担当編集者が募ったところ、副編集長が出したタイトル案の中に「葬送のフリーレン」があり、最終的に山田、アベに決めてもらい現在の題名となった。 2022年9月にアニメ化が発表され、同年11月に展開媒体がテレビアニメであることが発表された。2023年9月から2024年3月まで、日本テレビ系列ほかにて連続2クールで放送された。 == あらすじ == 魔王討伐の偉業を成し遂げ王都に凱旋した勇者ヒンメル、僧侶ハイター、戦士アイゼン、魔法使いフリーレンら勇者パーティー4人は、10年間もの旅路を振り返り感慨にふけっていたが、1000年は軽く生きる長命種のエルフであるフリーレンにとって、その旅はきわめて短いものであった。そして、50年に一度降るという「半世紀（エーラ）流星」を見た4人は、次回もそれを見る約束を交わしてパーティーを解散する。 それから50年後、すっかり年老いたヒンメルと再会したフリーレンは、ハイターやアイゼンとも連れ立って再び流星群を観賞する。まもなくヒンメルは亡くなるが、彼の葬儀でフリーレンは自身がヒンメルについて何も知らず、知ろうともしなかったことに気付いて涙する。その悲しみに困惑したフリーレンは、人間を知るためと魔法収集のために旅に出る。 さらに20年後、フリーレンはもうひとりの仲間であったハイターを訪ねる。ヒンメルと同じく老い先短い身であったハイターは、魔導書の解読と戦災孤児の少女フェルンを弟子にすることを依頼。その4年後に魔導書の解読を終えたフリーレンと、一人前の魔法使いに成長したフェルンは、ハイターの最期を看取ったあとに諸国をめぐる旅に出る。 その後フリーレンたちは、最後に訪ねた仲間であるアイゼンの助力を受けて、フリーレンの師匠にして伝説の大魔法使いフランメの手記を入手。その手記には、かつての魔王城があった大陸北端の地エンデにあるという、死者の魂と対話できる場所・オレオールの存在が記されていた。オレオールで亡きヒンメルと再会するという新たな目的ができたフリーレンは、アイゼンの弟子である少年戦士シュタルクや、行方不明の親友との再会を望む僧侶ザインという新たな仲間たちを加えて北方を目指す。 == 登場人物 ==
------------------------------------------------------------------------------------------
声の項はテレビアニメ版の声優。 === フリーレン一行（主要人物） === フリーレン (Frieren) 声 - 種﨑敦美 本作の主人公。魔王を討伐した勇者パーティーの魔法使い。長命なエルフ族の出身で、少女のような外見に反して1000年以上の歳月を生き続けている。人間とは時間の感覚が大きく異なるため、数か月から数年単位の作業をまったく苦にせず、ヒンメルらかつての仲間たちとの再会も50年の月日が経ってからのことだった。ヒンメルが天寿を全うして他界したのを機に、自身にとってはわずか10年足らずの旅の中でヒンメルの人となりを詳しく知ろうともしなかったことを深く後悔し、趣味の魔法収集を兼ねて人間を知るための旅を始める。生前時のヒンメルに対する意識は希薄であったが、幻影鬼（アインザーム）との遭遇時や、奇跡のグラオザームに「楽園へと導く魔法（アンシレーシエ
(中略)
不機嫌になったフェルンに謝罪したり、デートのように連れ歩いたりするさまから、ザインからは「もう付き合っちゃえよ」などと漏らされている。男性の象徴に対する評価は芳しくなく、「服が透けて見える魔法」で自身の下半身を見たフェルンからは「ちっさ」と漏らされて傷つく場面がある。好物は自身の誕生日にアイゼンがふるまってくれるハンバーグ。 ザイン (Sein) 声 - 中村悠一、川井田夏海（幼少期） アルト森林近くの村に住んでいた僧侶。フリーレンを除いたパーティーでは最年長で、酒・タバコ・ギャンブル・年上のお姉さんといった俗なものを愛好する破戒僧であるが、シュタルクが蛇から媒介された脳が数時間で溶けて死ぬという不治の毒を一瞬で治療するほどの技能をもつ。 底なし沼にはまり動けなくなっていたところをフリーレンに助けられる。少年時代から冒険者となることにあこがれていたが、10年前に親友の「戦士ゴリラ」が先に旅立ってからも、兄への配慮から村にとどまり、友の帰りを待ち続けていた。その真意を知った兄から叱責されて、フリーレンの仲間に加わる。旅中では大人の立場としてフェルンとシュタルクの仲を取り持つこともあり、もう互いに二人が付き合うべきではないかと悩んだりする。探す友が交易都市テューアに向かったことを知ると、彼を追いかけるため一行から離脱。のちにフリーレンがメトーデから僧侶枠として同行しようかと聞かれた際は、「このパーティーの僧侶の席は（ザインのために）空けておきたいから」と断っている。 === ヒンメル一行（勇者パーティー） ===
------------------------------------------------------------------------------------------
ヒンメル (Himmel) 声 - 岡本信彦 フリーレンたちとともに魔王を討伐した人間の勇者。旅の始めから10年をかけて目的を完遂し、その偉業や数々の功績から各地に銅像が建てられ、英雄として広く存在を知られている。人格者ではあるがナルシストな一面もあり、旅始めに謁見した王様にため口を利いて危うく処刑されそうになったり、銅像のモデルとなった際は細かく注文を出すなどしている。 これからのフリーレンの人生を「想像もできないほど、長いものになる」といい、後日の再会を約束するも、彼女が次に訪れたのは自身の晩年である50年後だった。「たった10年一緒に旅をしただけ」の彼の死がフリーレンに与えた影響は大きく、彼のことを知ろうとしなかったことで大きな後悔の念を抱かせることとなる。 初対面時から
(中略)
魔王討伐後はハイターと文通をしたり、ヒンメルの葬儀で見せたフリーレンの姿に二人の関係を哀れんだりするなど仲間思いの一面を見せており、オレオール（魂の眠る地）でフリーレンとヒンメルが再会できることを願っている。 ヒンメルの死後にシュタルクを弟子とし育て上げるも、とある理由でけんか別れしたとされている。その真相は、魔物との戦いが嫌で反抗してきたシュタルクの力の片鱗におののき、反射的に殴り飛ばしてしまったとのこと。 === 北側諸国 ===

（略）

------------------------------------------------------------------------------------------
 === 関連番組 === 葬送のフリーレン×ZIP!待望のアニメ化！魅力解剖SP テレビアニメ本放送前の2023年9月24日に日本テレビにて放送された『ZIP!』とコラボした特別番組。その後一部地域を除く日本テレビ系列各局でも順次放送された。同番組のコーナー「？よミトく！」特別版として、作品の内容や制作現場の模様が公開された。出演は水卜麻美（日本テレビアナウンサー）。インタビュー出演はかまいたち、若月佑美、種﨑敦美（フリーレン役）、市ノ瀬加那（フェルン役）、小林千晃（シュタルク役）、YOASOBI（OPアーティスト）、milet（EDアーティスト）。 フラアニ特別編 『葬送のフリーレン』⼤感謝祭 〜⼈の⼼を知る軌跡 2024年3月29日に日本テレビ系列にて放送された特別番組。公式サイトで募集した「もう⼀度⾒たい名シーン」のアンケートを元に振り返る名場面集。ナレーションはmilet。 葬送のフリーレン 〜トークの魔法〜 2023年9月27日よりYouTube「TOHO animation チャンネル」にて不定期で配信中のWeb番組。「ラジオ風番組」と銘打たれており、各回異なる出演者が作品についてラジオ形式で語っていく。特別版として、生放送の特番が配信される場合もある。 == 脚注 ==
------------------------------------------------------------------------------------------
  === 注釈 ===   === 出典 ===   == 外部リンク ==
------------------------------------------------------------------------------------------
漫画 『葬送のフリーレン』 原作:山田鐘人／作画:アベツカサ | 少年サンデー 『葬送のフリーレン』公式 (@FRIEREN_PR) - X（旧Twitter）テレビアニメ アニメ『葬送のフリーレン』公式サイト 『葬送のフリーレン』アニメ公式 (@Anime_Frieren) - X（旧Twitter） 葬送のフリーレン - マッドハウス - アニメ制作会社公式サイト
------------------------------------------------------------------------------------------
```

チャンク分け結果は・・・うーん、妥当な部分もありますが、なぜそこで固めるのか？という部分もありますね。
初期の文分割含めて見直しの余地が多々ありあそうです。

# まとめ

なんとなくですが、セマンティックチャンキングの前に可能な限り構造的なチャンキングを実施した上でそれでもチャンクが大きい場合にセマンティックチャンキングを行うなど、組み合わせをするのがよいように思います。

また、今回はWikipediaのような内容も構造化された文書を利用しましたが、小説などの場合はセマンティックチャンキングが重要な手法になるかもしれません。

いずれにせよ、ユースケースやコンテキストごとに適したチャンキング手法は変わると思いますので、いろんなパターンを理解しておくことが大事ですね。