ASCII×Microsoft の生成AIコンテスト「AI Challenge Day」参加レポート

# 2日間のRAGハッカソンに参加しました

こんにちは，ワークス徳島人工知能NLP研究所の山村です．

本記事は，先日参加したASCII×Microsoft の生成AIコンテスト「AI Challenge Day」の参加報告記事です．
Microsoft のパートナー10社による二日間のハッカソンに弊社も参加し，結果的に「World Wide Learning賞」を受賞しました．
今回はそのイベントの様子や実装したコードの一部などを紹介します．

:::note info
AI Challenge Day
日時：2024年4月18-19日
趣旨：RAGがテーマのハッカソン
場所：Microsoft AI Co-Innovation Lab Kobe（神戸市）
主催：角川アスキー総合研究所 (ASCII)
協賛：日本Microsoft 
:::

発表会から授賞式までの様子は生中継で配信されており，以下でアーカイブが公開されています．
（ワークスアプリケーションズの発表は，再生時間 59:34 あたりから始まります）

<iframe width="560" height="315" src="https://www.youtube.com/embed/AzhOi14CLTA?si=ZuuuG0e7D1b5B47s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


# RAG (Retrieval-Augmented Generation) とは

[RAG と生成 AI - Azure AI Search | Microsoft Learn](https://learn.microsoft.com/ja-jp/azure/search/retrieval-augmented-generation-overview)
> 取得拡張生成 (RAG) は、グラウンディング データを提供する情報取得システムを追加することで、ChatGPT などの大規模言語モデル (LLM) の機能を拡張するアーキテクチャです。
> 情報取得システムを追加すると、応答を作成するときに LLM によって使用されるグラウンディング データを制御できます。

大規模言語モデル (LLM) はある程度の質問に対する応答ができる一方で，学習データには含まれていない独自データに対する内容を適切に扱うことは難しいです．
独自データを扱うために，独自データを使って LLM をファインチューニングする方法もありますが，一定の学習コストが必要になります．
ファインチューニングをせずに，独自データを扱う手法として RAG のような手法が提案されています．
RAG では回答を生成する際に，回答根拠になりそうな文書を検索し，それらの情報を入力に加えることで，学習データには含まれないような内容もある程度扱うことができます．

[[2302.00083] In-Context Retrieval-Augmented Language Models](https://arxiv.org/abs/2302.00083)

実際に，今回のハッカソンでも主催者側が作成した架空のデータに関する質問応答が求められるような内容となっており，RAG の実装が必須になっていました．


# ワークスのメンバー構成
チームメンバーは，今回のイベントのために所属部門や拠点（東京・シンガポール）を越えて即席で集まった有志の4人で参加しました．
うち2人は，生成AI開発の業務に普段は携わっていなかったため，イベントの約一か月前からリモートで数回のミーティングを行い，RAGについて勉強をしてイベント当日に臨みました．

![IMG_4139.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/225de193-bd4e-24ea-2e1c-8c5a5de918c7.jpeg)

イベント前日に，東京オフィスで初顔合わせをして，お互いの強みをうまく活かそうと一致団結しました．
また会社から，弊社製品のロゴがプリントされたユニフォーム「HUE Tシャツ」と激励の言葉を貰いました．

今回のイベントは，基本的に日本語のみで案内されていた（ハッカソンで扱うデータも日本語だった）ため，非日本語話者がいる自分たちのチームでは若干の懸念がありましたが，結果的に以下のような対応でイベントを乗り切ることができました．
* 非日本語話者のメンバーには他のメンバーが都度翻訳をしてサポートする
* 一方で，チーム内のミーティングは基本的に英語でコミュニケーションをとる


# Microsoft AI Co-Innovation Lab Kobe とは

[Microsoft AI Co-Innovation Labs](https://aiotlabs.microsoft.com/ja)

> [世界で 6 拠点目。イノベーションを創出する Microsoft AI Co-Innovation Lab を神戸市に開設 - News Center Japan](https://news.microsoft.com/ja-jp/2023/10/17/231017-sixth-location-in-the-world-microsoft-ai-co-innovation-lab-to-create-innovations-opens-in-kobe-japan/)
> Microsoft AI Co-Innovation Lab は、Microsoft および川崎重工業株式会社と神戸市が連携し、同市に開設。
> AI や IoT を活用したイノベーションの創出と、産業の振興を目指しており、AI を活用して、プロダクトやサービスの新たな可能性を探るスタートアップを含め様々な企業を支援する施設です。

現時点 (2024/05/10) で Microsoft のイノベーション拠点は世界に6拠点あり，最新の拠点がこの神戸ラボとのことです．
今回のイベントでは，Microsoft のスタッフにサポートをいただきながら開発するという貴重な機会になりました．


![PXL_20240418_002928916.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/78a08fe7-a245-48fa-1c7a-f6470f8eb5ce.jpeg)
> 神戸商工貿易センタービル 24F にある神戸ラボのエントランス


![IMG_4142.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/150f01e7-10b1-ed2b-a4f4-b59120e42162.jpeg)
> 神戸ラボから見た夕景がすごく綺麗でした

また，一日目のランチタイムには神戸ラボのラボツアーを開催していただきました．
ラボツアーでは，所長の平井氏にラボの展示エリアを案内していただき，企業のニーズに沿って多様な形で開発支援を行っているプロジェクトを紹介していただきました．
 Microsoft の強みとパートナー企業の強みを活かしたサービスの詳細や，ハードウェアまで作りこまれたプロトタイプに間近で接することができました．


![P1320285.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/b3644d77-64d8-dbff-3bc4-39110e2807f5.jpeg)
> 神戸ラボツアーの様子


# 課題発表
RAG がテーマということ以外に当日まで事前情報がほぼなく，緊張して迎えた初日．発表されたお題はこちらでした．
「日本の世界遺産」のドメインならば LLM 単体でもある程度対応できそうですが，なんと架空の観光地のデータも意図的に含められているとのこと．
また，チャレンジングな要素として，テキストファイル以外のリッチテキスト (pdf, pptx, docx) に加え，マルチモーダル (png, jpg, gif) なデータも用意されていました．

![スクリーンショット 2024-05-10 010222.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/b4183257-ea36-e1c3-47dc-2a5fa303da07.png)

![スクリーンショット 2024-05-10 005819.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/16a8d261-8a62-f7b4-8567-3ea178c29dd8.png)


# チームで取り組んだこと
Microsoft Copilot Studio 内の自動評価ツールの紹介が事前にあったので，それを活用して基本的な構成から出発し，個々の変更の効果を確認しながら進めるという基本方針を採用しました．

## テキストの前処理

まず（検索対象の）データセットを精査し，やるべき前処理を洗い出しました．

* リッチテキスト (pdf, pptx, docx) は [Azure AI Document Intelligence](https://azure.microsoft.com/ja-jp/products/ai-services/ai-document-intelligence) でマークダウンに変換し，不要タグを除去する
* [康煕部首](https://techracho.bpsinc.jp/hachi8833/2020_10_07/95257) の存在も確認されたので，ユニコード正規化 (NFKC) で文字種を揃える
  * エディタで作業していたメンバーが，一部の文字の表示が崩れていることに気づきました
* いくつかの表記揺れに対応するため，[Sudachi](https://github.com/WorksApplications/Sudachi) を活用した表記揺れの解消に取り組む
  * Sudachi 辞書の表記正規化を利用して，名詞に対してのみ正規化を適用しました．最終的なパフォーマンスにどれくらい寄与したかは時間の都合で十分に検証できませんでしたが，事前のミーティングで「Sudachi の活用余地も検討したい」というメンバーの案を採用しました．
    * 参考: [Sudachi辞書の紹介 Part 6 -正規化表記](https://qiita.com/sakamoto_mi/items/c711285811f0faf774ea)

以下が実際に SudachiPy v0.6.8 で実装した正規化処理のコードになります．
いくつか実装のポイントを紹介します．

1. ハッカソンで提供されたファイルの中には，とても長いテキストも含まれていました．ただし，Sudachi ではトークナイズ可能な入力長が制限されています．そのため，以下の実装ではナイーブに一定の文字数で入力テキストを区切る対応を取っています（適切な区切り文字などで区切った方がより望ましいです）．
 
 https://github.com/WorksApplications/sudachi.rs/blob/v0.6.8/sudachi/src/input_text/buffer/mod.rs#L125

2. SudachiPy 0.6.1以降では，品詞の判定に便利な PosMatcher の API が追加されています．大抵の場合，文字列一致で品詞を判定するよりも，より高速に品詞のチェックをすることができます．以下の実装では，この PosMatcher を使って名詞かどうか判定しています．
    - 参考: [SudachiPyのPosMatcher #自然言語処理 - Qiita](https://qiita.com/eiennohito/items/c98313c84f9a70ba5419)

3. SudachiPy 0.6.1以降で追加された out パラメータを利用してメモリを再利用することで，若干の高速化が期待されます．
    - [SudachiPyのoutパラメーター #Python - Qiita](https://qiita.com/eiennohito/items/0e6a7d136fce5e155bce)
    - [Output Parameters and Memory Reuse — SudachiPy 0.6.8 documentation](https://worksapplications.github.io/sudachi.rs/python/topics/out_param.html)

```python
class SudachiNounNormalizer:
    SPLIT_MODES = {
        "A": SplitMode.A,
        "B": SplitMode.B,
        "C": SplitMode.C
    }
    MAX_LENGTH = 18000

    def __init__(
            self,
            dict_type: Literal["small", "core", "full"] = "core",
            split_mode: Literal["A", "B", "C"] = "C"
    ):
        self._dictionary = Dictionary(dict=dict_type)
        self._tokenizer = self._dictionary.create(self.SPLIT_MODES[split_mode])
        self._morphemes = self.tokenizer.tokenize('')
        self.is_noun = self._dictionary.pos_matcher([("名詞",)])

    @property
    def dictionary(self) -> Dictionary:
        return self._dictionary

    @property
    def tokenizer(self) -> Tokenizer:
        return self._tokenizer

    def morpheme2norm(self, morpheme: Morpheme) -> str:
        return morpheme.normalized_form() if self.is_noun(morpheme) else morpheme.raw_surface()

    def normalize(self, document: str) -> str:
        # To avoid InputTooLong error, pre-split a document.
        sub_documents = (document[i:i+self.MAX_LENGTH] for i in range(0, len(document), self.MAX_LENGTH))

        return "".join(
            (
                "".join((
                    self.morpheme2norm(morpheme)
                    for morpheme in self.tokenizer.tokenize(sub_document, out=self._morphemes)
                ))
                for sub_document in sub_documents
            )
        )
```


## チャンキング

上記の前処理に加えて，テキストを一定の単位（検索単位）に分割するチャンキングは，既存の実装を用いつつパラメータの工夫を行いました．

以下の論文で， Overlap Size（直前のチャンクとどれくらいテキストを重複するかのパラメータ）は大きいほど概ね良い結果が確認できたと報告されていたため，Overlap Size を Chunk Size（1チャンクの全体の長さ）の半分くらいの値に設定しました．

[[阿部&新納@NLP2024] RAGにおける小説データベースのChunk SizeとOverlap SizeとEmbeddingモデルの効果](https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/P11-8.pdf)

```python
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base",
    separators=[
        "\n",
        "。"
    ],
    keep_separator=True,
    strip_whitespace=True,
    chunk_size=800,
    chunk_overlap=400,
)
```


## システムプロンプト

### クエリリライティング
質問として入力されたクエリの書き換え（クエリリライティング）によって，検索性能が向上する場合があります．
ハッカソン中は2種類のクエリリライティングを試しましたが，最終的には以下のシステムプロンプトを用いてクエリリライティングを行いました．
3-shot のサンプルを含めることで，より意図したキーワードベースのクエリに変換されることを期待しています．
また，今回扱ったデータは日本語のみだったため，日本語で書き換えるような指示も含めています．

```plaintext:クエリリライティングのシステムプロンプト
system:
Below is a history of the conversation so far, and a new question asked by the user that needs to be answered by searching in a knowledge base containing data of Japan's World heritage sites.
You have access to an Azure AI Search index with 100's of documents.
Generate a search query keywords based on the conversation and the new question.
Do not include cited source filenames and document names e.g info.txt or doc.pdf in the search query terms.
Do not include any text inside [] or <<>> in the search query terms.
Do not include any special characters like '+'.
The search query terms should be in ONLY Japanese.
Each Japanese word should be separated by space.
If you cannot generate a search query, return just the number 0.

[EXAMPLES]
user:
九州地方にある自然遺産を面積の広い順に並べて。
assistant:
九州、自然遺産、面積
user:
高尾山の観光スポットを教えて。
assistant:
高尾山 特徴 名所
user:
釧路 なぜ 人気
assistant:
釧路 観光 理由 シーズン
[END EXAMPLES]
```

### 回答生成
最終的な GPT-4 による回答生成のシステムプロンプトは以下のとおりです．
今回のハッカソンの評価には全く直結しませんが，根拠となるソースの情報を含めることや，入力言語と出力言語を揃えるような指示は実運用でも重要と考えられるため，これらの指示も追加しておきました．

```plaintext:回答生成のシステムプロンプト
system:
You are an expert travel assistant helps users with their questions about Japanese World's Heritage sites. Be brief in your answers.
For tabular information return it as an html table. Do not return markdown format. If the question is not in English, answer in the language used in the question.
Each source has a name followed by colon and the actual information, always include the source name for each fact you use in the response. Use square brackets to reference the source, for example [info1.txt]. Don't combine sources, list each source separately, for example [info1.txt][info2.pdf].
```

## 手法のまとめ

上記の前処理・チャンキングを適用して，[Azure AI Search](https://learn.microsoft.com/ja-jp/azure/search/search-what-is-azure-search) によるハイブリッド検索を行い，上記のプロンプトを用いて最終的な回答を生成しました．

限られた人数・時間の都合上，以下の要素は検討しましたが上記の実装を優先させて断念しました．
* マルチモーダルアシスタントの課題
* より具体的や限定的なカスタマーストーリーの定義
* UI の構築

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/78120919-2f11-c042-4f92-4b2708fcd4e7.png)
> ハッカソン中の開発の様子（テーブル中央にあるお菓子は Microsoft 様からの差し入れ）

# 結果：成果発表と表彰
一日目は，権限の関係でデプロイエラーが発生したり，インデキシングに異常に時間がかかる（二日目に docker コンテナの起動時間が原因と判明した）ということもあり，運営側から用意された評価スクリプトを動かすことまで到達できませんでした．
開発〆切は二日目の13:00だったのですが，〆切二時間前にようやく評価スクリプトを動かすことができ，〆切一分前に発表スライドをギリギリで滑り込んで提出しました．

14:00から 記事冒頭で紹介した YouTube でのライブ配信も開始され，各社順番に10分間の成果発表が行い，ASCIIとMicrosoftによる審査を経て表彰が行われました．

各社とも，技術力はもちろん，ビジネスモデルの構想からUIの工夫まで幅広く扱える総合力の高いチームばかりで，自由なアイデアと問題解決力にチーム一同感心しきりでした．
そんななか、ワークスは精度評価のスコアで3位に入ることができました．
また今回このRAGハッカソンの問題を設定した Microsoft の花ケ崎氏から「World Wide Learning賞」をいただきました．
丁寧な前処理によって精度が上がったことなどを評価していただきました．

![P1320850.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/fbd771ac-fde9-1df9-45cb-a77be507e50e.jpeg)

![P1320844.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/2e7dbad4-2a58-6c9a-5ee3-a9038bd87f48.jpeg)

![IMG_5409.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/6ae30dcf-5aff-e9ec-6305-f65ac171fd79.jpeg)
> 受賞後の記念撮影（パネルは頑張って持ち帰りました）

また，Youtube 配信終了後は同イベント会場で懇親会が行われました．
提供された軽食をいただきながら，他のチームや運営・スタッフの方とフランクに交流することができました．

![IMG_4140.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/d8a5126a-cdaa-927c-bee2-50998fd24b6b.jpeg)

![IMG_4143.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/d5cb133a-5669-acad-b2dc-95218f168db6.jpeg)

![IMG_4146.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394564/0e8b4996-3f69-00e7-0041-0176cf16fb2b.jpeg)


# 今後の展望
今回のイベントを通じて，事前の勉強，当日試したこと，他社のアプローチ，Microsoft によるヒント満載の講演など，いずれも大いに学ばせていただきました．
これらの新しい知見を，弊社ワークスアプリケーションズの製品やサービスに活用していきたいと思います．

もし第二回もあれば是非応募したいと思います．
貴重な機会をどうもありがとうございました．


# おわりに
ASCII様の記事にて2日間の詳細レポートが ~~近日公開予定です．~~ 公開されました↓
~~（後日リンクを追記予定）~~
[ASCII.jp：日本マイクロソフトのAIパートナー10社が神戸に集合　RAGとマルチモーダルに挑む (1/7)](https://ascii.jp/elem/000/004/199/4199213/)