ASCII×マイクロソフトの生成AIコンテスト AI Challenge Dayに参戦

## はじめに

こんにちは、(株) 日立製作所 [Lumada Data Science Lab.](https://www.hitachi.co.jp/products/it/lumada/about/ai/ldsl/index.html) の植田碩瞭(うえだ　みちあき)です。
2024年4月18,19日にASCII×Microsoft主催の「AI Challenge Day」という生成AIに寄せられた質問に対してどう答えるべきかを競うイベントが開催されました。

https://www.youtube.com/watch?v=AzhOi14CLTA

私はこのイベントに日立製作所Lumada Data Science Lab.のメンバ6人(1人はDay1のみ)で参加してきました。
このような社外イベントは初参戦でとても緊張しましたが、チームで協力して検討や実装を進め、**グランプリを受賞**しました！

この記事では、
- [私たちが検討したソリューションのアーキテクチャ](#ソリューションのアーキテクチャ)
- [生成AIの回答精度向上に向けた工夫(本記事では数ある工夫のうち2つ)](#作業分担)
    - [スキャナーでPDF化されたファイルの前処理](#文書データの前処理スキャンデータのテキスト化)
    - [マルチモーダルへの対応に向けた画像データの前処理](#画像データの前処理画像データのベクトル化)

をご紹介できればと思います。

### AI Challenge Dayとは
本イベントは、生成AIの力を活用した課題に取り組むことで新しいことへチャレンジし、学ぶコンテストです。
テーマは「世界遺産トラベルアシスタント」で下記２つの課題に対してRAG（Retrieval Augmented Generation）という生成AIの活用技術を用いて精度を競うコンテストでした。
(精度評価は運営の用意したプロンプトを用いたChatGPTによる類似度評価)



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777252/481d25e5-d016-7c33-4ab4-2cc0d7df82ee.png)

**ただし**、RAGの回答精度が高いだけでは意味がなく、カスタマーストーリーやセキュリティを考慮しているかなども審査基準として設定されており、高い評価を得るには本番運用を想定した検討や実装が必要でした。(回答精度評価点：25点、審査員評価点：15点　合計40点)

## ソリューションのアーキテクチャ
まずは、本イベントのメインの技術となるRAGについて説明します。RAGを使うことで言語モデルが持っていない情報を回答したり、正確さを上げることができます。

### RAGの基本的な流れ
1. UI(ユーザインタフェース)画面で a.質問文を受け取る
2. UIで受け取った b.質問文を基に c.検索エンジンで知識DB（データセットをベクトルなどの形式にEmbeddingしたデータベース）から関連する文書を検索する
3. そして d.検索結果を前提知識として質問に加えてLarge Language Model (LLM)を用いて e.回答を生成する


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777252/3333ab50-7681-8d87-5006-6d75b17cae37.png)


### イベント開始時
私たちは、最初に上記RAGの実装とデータの確認に着手しました。精度向上のベースラインとしてデータの前処理をほぼ行わず、まずはどの程度の精度が出るのかを確認しました。

結果は散々で、13.258点/25点(課題①のみで評価)。多くの回答で`"申し訳ありませんが、提供された情報の中には○○に関する情報は含まれておりません。"`と出力されてしまいました。
このベースラインを基に**関連情報がなぜ検索できていないか**や**関連情報があるのになぜ上手く回答できていないか**に注目してデータ、質問文、回答文を確認し、必要な前処理や改良案を検討していきました。

### 作業分担
ベースラインとなるRAGの実装が完了したことで、完成度向上に向けて実施すべき作業・工夫点が見えてきました。

- [文書データの前処理(本記事で紹介)](#文書データの前処理スキャンデータのテキスト化)
- [画像データの前処理(本記事で紹介)](#画像データの前処理画像データのベクトル化)
- RAGの改良
- RAGを操作するUIの開発

これらの作業を各自の興味(今回のコンテストで学びたい内容)に合わせて分担し、詰まった時にはお互いにコードを共有したり、結果を見せ合ったりしながら協力して進めました。
それらの工夫の一部を以下で紹介します。

## 文書データの前処理(スキャンデータのテキスト化)
精度向上に向けた工夫の1つ目として文書データの前処理のうち、効果の大きかった**スキャンデータのテキスト化**について説明します。
今回の提供されたデータにはスキャナーでPDF化されたファイルが含まれており、[LangChainのLoader](https://python.langchain.com/v0.1/docs/modules/data_connection/document_loaders/pdf/)を使用すると下記のように空文字となり、文字が取り出せていませんでした。
```python:LangChainのLoaderによるPDFのテキスト抽出
from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("./清水寺.pdf")
print(loader.load()[0].page_content)
```

```text:出力
" "
```
そのため、OCRによるPDFファイルのテキスト化を実施しました。
今回は[tesseract](https://github.com/tesseract-ocr/tesseract)のようなOSSのOCRツールよりも日本語の読み取り精度が高いAzure Document Intelligence(旧Azure Form Recognizer)を利用しました。

https://learn.microsoft.com/ja-jp/python/api/overview/azure/ai-formrecognizer-readme?view=azure-python

OCRを行うと文字が存在する範囲を特定し、それぞれの文字を文字認識によって文章データ化してくれます。
本記事では(イベント時のデータは使えないので)[Wikipediaの清水寺ページ(1ページ目のみ)](https://ja.wikipedia.org/wiki/%E6%B8%85%E6%B0%B4%E5%AF%BA)を印刷し、スキャンした画像でOCRを実行しました。
(CC BY-SAの下で二次利用を許諾。https://creativecommons.org/licenses/by-sa/4.0/)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777252/8adeffaa-da48-1a95-3950-fd34d9c08fd6.png)

それでは、どのようにOCRを実装したか説明します。
まずはAzure Document Intellegenceを使う準備をします。
Azure Document Intellegence用のAPIキーとエンドポイントを環境変数に書き込み、
```.env
DOCUMENT_INTELLIGENCE_API_KEY = XXX
DOCUMENT_INTELLIGENCE_ENDPOINT = XXX
```
pip でazure-ai-formrecognizerをインストールします。
```shell-session
$ pip install azure-ai-formrecognizer
```
上記の準備後、公式のドキュメントを参考に下記コードでOCRを実装しました。

```python:OCRの実行
from azure.core.credentials import AzureKeyCredential
from azure.ai.formrecognizer import DocumentAnalysisClient

# OCR用クラスの定義
document_analysis_client = DocumentAnalysisClient(
    endpoint=os.environ['DOCUMENT_INTELLIGENCE_ENDPOINT'],
    credential=AzureKeyCredential(os.environ['DOCUMENT_INTELLIGENCE_API_KEY'])
    )
# fileのパスからOCRの実行
with open(path_to_sample_documents, "rb") as f:
    poller = document_analysis_client.begin_analyze_document(
        "prebuilt-layout", document=f
    )
result = poller.result()
print(result.content)
```
```text:出力
ウィキペディア
フリー百科事典
清水寺
出典:フリー百科事典『ウィキペディア(Wikipedia)』
清水寺(きよみずでら、英: Kiyomizu-dera Temple[1]) は、
京都市東山区清水1丁目にある北法相宗の大本山の寺院。山
号は音羽山。本尊は十一面千手観世音菩薩。正式には音羽山
清水寺(おとわさんきよみずでら)と号する。もとは法相宗
に属していたが、現在は独立して北法相宗を名乗る。西国三
十三所第16番札所。洛陽三十三所観音霊場第10から14番札
所。境内(敷地面積)は約13万平方メートル[2]。
概要
清水寺は法相宗(南都六宗の1つ)系の寺院で、広隆寺、鞍
馬寺とともに、平安京遷都以前からの歴史をもつ京都では数
少ない寺院の1つである。また、石山寺(滋賀県大津市)、長
谷寺(奈良県桜井市)などと並び、日本でも有数の観音霊場
である。鹿苑寺(金閣寺)、嵐山などと並ぶ京都市内でも有
数の観光地として有名であり[3]、季節を問わず多くの参詣
者が訪れる。また、修学旅行で多くの学生が訪れる。古都京
都の文化財としてユネスコ世界遺産に登録されている。
清水寺の宗旨は、当初は法相宗で、平安時代中期からは真言
宗を兼宗していた。明治時代初期に一時真言宗醍醐派に属す
るが、1885年(明治18年)に法相宗に復す。1965年(昭和
40年)に住職であった大西良慶が北法相宗を立宗して法相
宗から独立した[4]。
歴史
創建伝承
清水寺の創建については、『群書類従』所収の藤原明衡撰の
『清水寺縁起』、永正17年(1520年)制作の『清水寺縁起絵
巻』(東京国立博物館)『今昔物語集』、『扶桑略記』の延暦17
年(798年)記などにも清水寺草創伝承が載せられている。
これらによれば、草創縁起は大略次の通りである。
宝亀9年(778年)に、大和国の興福寺の僧で当時子島寺
(奈良県高市郡高取町に現存)で修行していた賢心(後に延
鎮と改名)は、夢のお告げで北へ向かい、山城国愛宕郡八坂
郷の東山、今の清水寺の地である音羽山に至った。金色の水
流を見出した賢心がその源をたどっていくと、そこにはこの
山に篭って滝行を行い、千手観音を念じ続けている行叡居士
(ぎょうえいこじ)という白衣の修行者がいた。年齢200歳
になるという行叡居士は賢心に「私はあなたが来るのを長年
```
出力を見ると記載内容として取り出したい文章をうまく抽出できているかと思います。

これらをLangChainのLoaderで読み込めなかったファイル全てに適用するだけで**精度が格段に向上しました！** (この段階で21.400点/25点(課題①のみで評価)を達成)
Azure Document Intelligenceはこれまで利用したことがなかったのですが、かなりの精度向上効果があり、この機会に試してよかったです！

**余談**
終了10分前にうまくテキストが読み込めていないPDFファイルが残っていたことに気づき断念したものがありました。それらのファイルもAzure Document Intelligenceなどで前処理できていればもう少し精度を上げることができた、、、かも？

## 画像データの前処理(画像データのベクトル化)
精度向上に向けた工夫の2つ目として**画像データのベクトル化**について説明します。
課題②のマルチモーダルへの対応に向けて、チームメンバと検討を進め、実装方針を下記としました。(<font color="Crimson">**赤字**</font>が通常のRAGに追加した部分)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777252/7c27ab42-4d13-de73-5bbd-df31320f3457.png)

このうち、本節で紹介する画像データのベクトル化は、画像用の知識DBを作成するための前処理である**画像データのEmbedding**(上図のz'部分)時に実施しました。
ベクトル化の手法は様々ありますが、マルチモーダルへ対応することができる(画像も文書も同様にベクトル化可能な)CLIPというモデルを採用しました。
実装は下記を参考に進めていきました。

https://python.langchain.com/v0.1/docs/integrations/text_embedding/open_clip/

pipで必要なライブラリをインストールします。

```shell-session
$ pip install langchain-experimental
$ pip install open_clip_torch
```
その後、CLIPモデルによる画像の埋め込みを実装しました。
(検証用画像は[Unsplash](https://unsplash.com/ja)で取得)

```python:CLIPモデルによるベクトル化
from langchain_experimental.open_clip import OpenCLIPEmbeddings
import numpy as np

#モデル名定義(open_clip.list_pretrained()で学習済みモデルを確認可能)
model_name = "ViT-B-32"
checkpoint = "laion2b_s34b_b79k"

#Embeddingモデル定義
clip_embd = OpenCLIPEmbeddings(model_name=model_name, checkpoint=checkpoint)

#画像をファイルパスからEmbedding
image1_path = "./images/厳島神社1.jpg"
image2_path = "./images/清水寺1.jpg"
image_test_path = "./test_厳島神社.jpg"

image1_feat = clip_embd.embed_image([image1_path])
image2_feat = clip_embd.embed_image([image2_path])
image_test_feat = clip_embd.embed_image([image_test_path])

```
上記コードでベクトル化した画像の類似度(ベクトルの内積を計算)を比較してみます。
```python:画像ベクトルの類似度比較
# 厳島神社 vs テスト用厳島神社
similarity1 = np.matmul(np.array(image1_feat[0]), np.array(image_test_feat[0]).T)
# 清水寺　 vs テスト用厳島神社
similarity2 = np.matmul(np.array(image2_feat[0]), np.array(image_test_feat[0]).T)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777252/ebee3a56-1ef8-5714-a7ab-b66c004ad5d9.png)

このCLIPモデルのライブラリでEmbeddingしたベクトルのノルムは1になるので、ベクトルの内積を計算した際には1に近いほど似ていることになります。実際に清水寺よりも厳島神社の方がテスト用厳島神社に近い画像であることが確認できました。

Embeddingしたベクトルを知識DBとして検索できるようにするために、SentenceTransformersを利用しました。

https://www.sbert.net/

pipで必要なライブラリをインストールします。

```shell-session
$ pip install sentence-transformers
```
知識DBへ格納する対象の画像フォルダを指定し、Embedding、検索するまでを以下のコードで実装しました。
```python:画像データのベクトル化と知識DBからの検索
# 知識DBへ格納する画像のパスを指定
import glob
list_fnames = glob.glob("./images/*.jpg")

# 画像をEmbeddingし、リストに格納
img_feats = []
for list_fname in list_fnames:
    img_feat = clip_embd.embed_image([list_fname])
    img_feats.append(img_feat[0])

# Sentence Transformersで検索
import torch
from sentence_transformers import SentenceTransformer, models, util, InputExample

# 検索用関数
def get_similarity_images(image_path : str):
    # 引数で指定された画像をEmbedding
    embed_query = clip_embd.embed_image([image_path])[0]
    # 知識DB内でコサイン類似度を計算
    cos_sim = util.cos_sim(embed_query, img_feats)
    # SentenceTransformersにより最も似ている画像を検索
    cos_val, cos_idx = torch.topk(cos_sim, k=1)

    # 似ている画像のファイルパスを返す
    images = []
    for tens in cos_idx[0]:
        images.append(list_fnames1[tens.item()])
    return images
    
# 検索したい画像のパスを引数に与えて検索
print(get_similarity_images("./test_厳島神社.jpg"))
```
```text:出力
['./images/厳島神社2.jpg']
```
これにより、課題②のマルチモーダル問題**全5問中4問**に対して、回答できるように！
ここを実装するまでは課題②が精度の足を引っ張っていたので、一安心しました。

**残課題**
残りの１問はこのアプローチでは回答できない問題だったため、タスクを分岐するなど検討しましたが、実装の時間がなく断念しました。
他のチームではこの問題に対してセグメンテーション＋GPT4Vというアプローチで精度向上を目指していてとても勉強になりました。
また、2024/5/13に発表された[GPT-4o](https://openai.com/index/hello-gpt-4o/)を使えば完答できたりするのかなとか妄想してます。

## おわりに

最終スコアは全10チームの中でトップの**22.125点/25点**(課題①、②合わせて評価)を達成しました。また、「世界遺産トラベルアシスタント」を誰に向けて提供するかのストーリー、非機能要件に配慮した本番実装の検討なども高く評価して頂き、グランプリを受賞出来たのかなと思っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777252/2aa9f9b9-bd94-0170-01ca-ec2d53243e41.png)
左から奥田さん、[諸橋さん](https://qiita.com/m-morohashi)、片渕さん、植田、山根さん

### 感想
私が担当したOCRが大きく回答精度の向上に寄与したこともあり、達成感がすごくありました。作業部屋と発表順のくじ引きで運が良かったことも私の(最大の？)功績です。(部屋にホワイトボードがあったおかげで作業の整理や分担がうまくいきました。発表順も大トリで「全部持ってったな」などのコメントを頂けました。)
また、普段別のチームで業務を行っているメンバでの参加でしたが、作業を分担して適宜連携しながら進めることでクイックに成果を出すことができました。

多くの学びを得ることができ、普段の業務と違う場所、メンバでの取組はとても楽しいイベントでした。機会があればまた参加したいです！
主催いただいたASCII様、日本マイクロソフト様、本当にありがとうございました。


**商標**
- "日立製作所"、"Lumada"は株式会社日立製作所の登録商標です。
- "ASCII"は株式会社ＫＡＤＯＫＡＷＡの登録商標です。
- "Microsoft"、"Azure"、"マイクロソフト"はマイクロソフト　コーポレーションの登録商標です。
- "Wikipedia"はWikimedia Foundation, Inc.の登録商標です。
- "GPT"はオープンエイアイ　オプコ　エルエルシーの登録商標です。