RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part10 完

RAG開発の一旦終了と成果物の共有 とりあえず目標としていたものが完成したので開発はこれで一旦終わりとします。 完成して動いたものを動画にして3つほどはります。 動画の内容とモデルの読み込み ファイル数別に動画を取ってみました。ベクトルモデルの読み込みと読み込ませるテキスト量によって時間が変わってきます。 モデルの読み込みとかはOpenAIに変わったぐらいで大きく変更したところはないですが一部お見せします。 def generate_response(self, prompt: str, max_tokens: int = 3000) -> str: try: client = OpenAI( # This is the default and can be omitted api_key=os.environ.get("OPENAI_API_KEY"), ) response = client.chat.completions.create( model="gpt-4o", # 使用するモデルを指定 messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": prompt} ], max_tokens=max_tokens, n=1, stop=None, temperature=0.7 ) print(response) return response.choices[0].message.content except Exception as e: logging.error(f"エラーが発生しました: {e}") return "申し訳ありませんが、応答を生成する際にエラーが発生しました。" この辺をGPT-4oに聞いてみたのですが最近のコードを提示してくれなかったので、chatを作成する部分はgit-hubを参考に作りました。こちらです。 Flaskの設定と変更点 それからFlaskの部分ですね。 ...

2024年5月31日

RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part9

一旦行き詰ったのでここまでを記録として残しておきます。 行き詰った内容としてはローカルで実行できないこと、無料枠のcolabを利用してもダメだったことです。 今回実施した内容としては以下 ベクトルデータベースの完成 生成モデルを利用して質問+ファイルの内容を分割してプロンプトを投げる ‘paraphrase-multilingual-mpnet-base-v2’を利用したベクトルデータベースの作成が完了したのでGithubにあげようとしたのですが、jsonファイルは2GB以上、zip化しても500MB以上あったので断念しました。100MB未満なら簡単にあげられるのですが、それ以上はもう少し手順が必要みたいです。 このファイル作るのに1週間近くはかかるんだなと思いました。ファイル数が多かったのもあるかもしれませんが、こんなに大変なんだなと実感しました。この記事でベクトルデータベースの容量削減について書かれていますが、精度とかはまだ分かってないのでなるほどぐらいで終わりました。 生成モデルを利用したプロンプトの作成と推論コードはザックリと以下になります。 def load_or_download_genmodel(model_name: str, local_dir: str) -> tuple[PreTrainedTokenizer | PreTrainedTokenizerFast, PreTrainedModel|SentenceTransformer]: # ローカルディレクトリが存在しない場合は作成 if not os.path.exists(local_dir): os.makedirs(local_dir) # モデルがローカルに存在するかをチェック if not (os.path.exists(os.path.join(local_dir, 'config.json')) and os.path.exists(os.path.join(local_dir, 'tokenizer_config.json'))): # モデルが存在しない場合はダウンロードして保存 print(f"Model not found locally. Downloading {model_name}...") tokenizer = AutoTokenizer.from_pretrained(model_name, force_download=True) model = AutoModelForCausalLM.from_pretrained(model_name, force_download=True) print(f"Saving model and tokenizer to {local_dir}...") tokenizer.save_pretrained(local_dir) model.save_pretrained(local_dir) else: # モデルが存在する場合はローカルからロード print(f"Loading generative model and tokenizer from {local_dir}...") tokenizer = AutoTokenizer.from_pretrained(local_dir) model = AutoModelForCausalLM.from_pretrained(local_dir) return tokenizer, model def process_text_in_chunks(self, question, text, model, tokenizer): for chunk in chunks: prompt = f"{question}\n\n{chunk}" inputs = tokenizer(prompt, return_tensors="pt", max_length=50, truncation=True) input_ids = inputs["input_ids"] attention_mask = inputs["attention_mask"] output = self.generative_model.generate(input_ids=input_ids, attention_mask=attention_mask, max_new_tokens=100, num_beams=3) result_text = tokenizer.decode(output[0], skip_special_tokens=True) combined_result += result_text + "\n" return combined_result def main(self, question): # コサイン類推度から近いファイルを取得し、テキストを抽出→ここでは省く result = self.process_text_in_chunks(question, text, self.generative_model, self.tokenizer) return result if __name__ == '__main__': # 生成モデルをロードまたはダウンロード tokenizer, gen_model = load_or_download_genmodel(gen_model_name, gen_local_dir) cos_sim_calc = CosSimCalc(gen_model, tokenizer, emb_model) cos_sim_calc.main(question = '著作権について教えて下さい。') いろいろ試して見たんですがローカルだと動かないですし、高性能GCPを買うまではいかないので、クラウドで実行するかOpenAIなどのAPIに頼ることになりそうです。 ...

2024年5月24日

RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part8

前回問題としていたベクトル化についてですが解決して、ようやく2つ目の目標が終わりました! まずはこちらをご覧ください。 こんな感じになりました。 今はまだファイル数は20~30なのでまだまだ足りませんが、前回に比べるとかなりましになりました。 今回やったこととしては OpenAIのベクトル化からSentence Transformerを使用したベクトル化に変更 全ファイルをベクトルデータベースとしてjsonファイルに格納 になります。そのため一旦別プログラムでファイルを全てベクトル化し、画面に表示をさせるときはそのファイルを読み込んで類似度の計算を行っています。 前のコードとそこまでは変わってないので変わった部分で言うと class PDFToVector: def __init__(self, model_name: str): # モデルを読み込み self.model = SentenceTransformer(model_name) def split_text2vector(self, text: str, max_tokens=200) -> List[float]: vectors = [] for i in range(0, len(text), max_tokens): segment = text[i:i+max_tokens] vector = self.model.encode(segment, convert_to_tensor=True) vectors.append(vector) average_vector = np.mean(vectors, axis=0).tolist() def embed_sentence(self, text: str, vector_fol: str, filename: str, vector_database: List) -> List: embedding = self.split_text2vector(text) vector = { 'title': filename, 'body': text, 'embedding': embedding } vector_database.append(vector) def main(self): # ベクトルデータベースを格納 with open(vector_db_json, 'w', encoding='utf-8') as file: json.dump(vector_database, file, ensure_ascii=False, indent=4) こんなとこだと思います。一部処理を省略していますが大体の変わったとこはここになります。 ...

2024年5月14日

RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part7

目標としていた部分が完成したのでデモ版という形でお見せしようと思います。 想定していた形が取れたので割と嬉しいですね!ただ、上記はファイルが2つの場合でこのくらい時間がかかっています。 次はファイルが5つの場合です。 かなり時間がかかってますね。というのも実行するたびファイルをベクトル化しているので時間がかかる仕組みになっています。 実行するたびopenaiのAPIもたたいているので料金も増えていきます。ということで次の課題は openaiのベクトル化ではない代替手段を見つける ベクトルデータベースを作成する(すべてのファイルをベクトル化して、どこかに保存) ひとまずはこの2つですかね? 代替手段は見つければ何とかなりそうなので時間はかからないと思いますが、ベクトルデータベースの作成は異なるプログラムを作る必要がありそうですし、質問をベクトル化するプログラムの改修も入りそうなので少し時間がかかるかもですね。 第二目標ほど時間はかからないと思いますが… 今回修正した箇所はflaskを使用した場所と応答画面の作成になります。flaskは微修正だったので割愛しますが、画面のほうは以下のようになりました。 'use client'; import { useState } from 'react'; export default function QuestionForm() { // サーバーからの結果を保持する状態変数 const [result, setResult] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); const question = e.target.elements.question.value; // サーバーにPOSTリクエストを送る const response = await fetch('http://localhost:5000/process', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: question }) }); // サーバーからのレスポンスをJSON形式で取得 const data = await response.json(); // 結果を状態変数に保存 setResult({ title: data.title, body: data.body, similarity: data.similarity } || 'No result returned'); // デフォルトで"結果がない"と表示 e.target.reset(); } return ( <div> <form onSubmit={handleSubmit}> <textarea name="question" placeholder="質問を入力してください" rows={5} /> <button type="submit">送信</button> </form> {/* サーバーからの結果を表示 */} <div> <h3>回答:</h3> <p><strong>タイトル:</strong> {result.title}</p> <p><strong>類似度:</strong> {result.similarity}</p> <p><strong>内容:</strong> {result.body}</p> </div> </div> ) } 画面にタイトルと類似度と内容が表示されるようになってます。必要ないかもしれませんが、デフォルトでは結果が表示されない文言を出しています。多分出ることはないですが… ...

2024年5月9日

RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part6

PCが故障したという話をしたのですが、コンセントから電源が抜けてたというオチでした。 なのでRAGの開発を再開しました! 前回開発した部分は 質疑応答画面の作成 今回開発する部分は 画面(Next.js)から送られるデータを受け取るエンドポイントをFlaskに設定 ベクトル化するプログラムをクラス化 Flaskも触ったことないのでChat-GPTに質問しながら触ってました。一旦中身は以下の内容にしています。 from flask import Flask, request, jsonify from flask_cors import CORS import pdf_to_model import logging logging.basicConfig(level=logging.DEBUG) app = Flask(__name__) CORS(app) @app.route('/process', methods=['POST']) def process_question(): data = request.get_json() query = data.get('query') logging.debug("Received query: %s", query) try: vector = pdf_to_model.PDFToVector() result = vector.main(query) # 結果を JSON 形式で返す return jsonify(result[0]) # return jsonify({'result': 'Processed result of ' + query}) except Exception as e: logging.error("Error processing query: %s", str(e)) return jsonify({'error': str(e)}), 500 if __name__ == "__main__": app.run(debug=True) ログの設定はする必要はないですが、まだ開発段階なので一旦設定しています。それから"CORS(app)“ですね。少し調べたのですが異なるオリジン間でのリソース共有を可能にするために必要な設定みたいです。そもそも同一のオリジンとは以下の3つが同じであることが条件です。 ...

2024年5月4日

RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part5

さて前回までやったこととしては 全データのベクトル化(途中で断念) 画面の作成(Next.jsのインストールと起動) というところまでやってみました。今回は 質疑応答画面の作成 というところまでやってみます。Next.jsは初めて触るので進みが遅々としていますがご了承ください。 さてインストールするタイミングでこのようなことを聞かれました。 Would you like to use TypeScript? » No / Yes Would you like to use ESLint? » No / Yes Would you like to use src/ directory? » No / Yes 他にもいくつか聞かれたので一応Claudeにどんな意味があって導入することのメリットデメリットを聞きながら質問に答えました。大体Yesにしましたが… さて前回の画面表示まで行けたのでここからどう作業を進めればいいのかわかってなかったので聞いてみました。 ということで質疑応答画面を作っていきます。本格的なデザインは気にせずに簡易的なものを作っていきます。 と言ってもからコード書くのは大変ですし、学ぶのにも時間がかかるので今回もClaudeに手伝ってもらおうと思います。それで最初書いてもらったコードがこちら page.jsx import QuestionForm from './components/QuestionForm'; const HomePage = () => { return ( <div> <h1>RAGシステム</h1> <QuestionForm /> </div> ) } export default HomePage; QuestionForm.jsx import { useState } from 'react'; const QuestionForm = () => { const [question, setQuestion] = useState(''); const handleSubmit = (e) => { e.preventDefault(); // 質問をサブミットする処理をここに書く console.log('Submitted question:', question); setQuestion(''); } return ( <form onSubmit={handleSubmit}> <textarea value={question} onChange={(e) => setQuestion(e.target.value)} placeholder="質問を入力してください" rows={5} /> <button type="submit">送信</button> </form> ) } export default QuestionForm; ちなみに上記の書いてる内容はよくわかってないのでこっちはChat-GPTに解説してもらってました。新卒の時の研修で書いたHTMLやJSを思い出します。 ...

2024年4月22日

RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part4

前回で一通り作りたいものが作れたのでこの先はWebアプリ化できないかな~?という緩い感じで進めていこうと思います。 前回行ったのは 検索クエリのベクトル化 コサイン類似度の計算 今回行ったのは pdfファイルの全データベクトル化(途中で断念) Web画面の作成(一部) pdfファイルのベクトル化に関しては前回やったのでそれをfor文で回してます。pdfは1つ1つ出力するようにして、後ほど全体を結合しようとしていました。 file_path_list = glob.glob(input_pdf+'/*.pdf') for pdf_path in file_path_list: # 前回やったベクトル化 ・ ・ ・ # JSON形式でベクトルデータベースを保存 with open(json_save_name, 'w', encoding='utf-8') as file: json.dump(vector_database, file, ensure_ascii=False, indent=4) 大体2500ファイルぐらいベクトル化したのですが、9$くらいになってました。残り12000ファイルほどあるので少し時間がかかるのと料金も計50$近くになりそうだったのでいったん中断しました。 別の作業としてWeb画面を作っていこうと思ったのでまずは技術の選定からですね。ここに関してはChat-GPTとClaude両方に聞いてみましたが、Claudeのほうを採用しました。こんな感じです。Googleストアと書いたんですが1ミリも予定はないです(笑) 返ってきた回答がこちら ということでおすすめにしたがってNext.jsにしようと思います。ユーザーのストレスを考えるのは今じゃなくてもいいんですがせっかく自身のスキルにもつながるので選びました。 ちなみにおススメした理由とかわかってなかったのでいくつか聞いてみました。 なぜNext.js(React)+Tailwind CSS や Vue.js はreactに比べてユーザーにストレスがかかりにくいのですか? 単一ページアプリケーション(SPA)とはどういうものでしょうか? Flash Of Unstyled Content(FOUC)や、SPAでよくみられるバンク画面の発生とはどういったものでしょうか? バックエンドではpythonを使用しているのでpythonフレームワークのflaskやdjangoという選択肢もあったと思いますが、なぜ候補から外れたのでしょうか? フロントエンドフレームワークとバックエンドWebフレームワークの違いは何ですか? そこからいろいろ聞いていた結果、Next.jsとFlask(python)の組み合わせにしようと思います。Webについては全く分からなかったので色々聞いてみました。流れとしては以下の感じです。 Web画面から入力を受け取る 画面から受け取った入力を引数としてpythonに渡す pythonで引数をベクトル化しコサイン類似度の計算を行う pdfファイルを全てベクトル化して結合したものを取得して、入力に類似しているファイルのタイトルと内容を取得する 取得したタイトルと内容を画面に表示する 1と5がNext.js、2がflask、3と4がpythonという流れでやる予定で、今回は1をやっていきます。 というわけでまたClaudeに使い方を聞いて進めていきます。 これでインストールとプロジェクトの作成が完了して、デフォルトの画面が表示されるようになりました。こんな感じです。 私はvscodeを使っているのですがターミナルからうまく実行できてないので少し調べる必要がありますね。それから"AppData/Roaming"の中にnpmというファイルが必要なのですが、インストールで作成されなかったので手動で作成したりと少し導入で手間取ってしまいました。 今回はここまでにして次回は簡単な画面を作成していこうと思います。恐らく稚拙な画面になりそうですがまずは作るとこらからです。ではでは。

2024年4月19日

RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part3

このpartで目標としていた基本形は作れたことになります。簡単な質問で"タイトル"と"内容"が返ってくるものになります。 前回やったのは 特殊文字の削除 大文字小文字統一 ベクトル変換 今回行ったのは 検索クエリのベクトル化 コサイン類似度の計算 になります。ベクトル化に関しては前回も行ったので同じものになります。 # これが検索用の文字列 QUERY = '保険金が欲しい' # 検索用の文字列をベクトル化 query = openai.embeddings.create( model='text-embedding-3-large', input=QUERY ) # ベクトル取得 query = query.data[0].embedding ちなみにモデルの部分に関しては’text-embedding-3-large’の他に’text-embedding-3-small’もありますのでお好きな方を使ってください。精度はlargeのほうがいいですが料金は下がると思います。 次はコサイン類似度の計算になります。こちらは昔OpenAIでも提供されていたみたいですが、前のバージョンで提供されていたみたいなので他のもので代用します。簡単に自作できますがめんどくさい方はsklearnでも提供されているのでそちらを使ってください。 import numpy as np def cosine_similarity(v1, v2): # ベクトルのドット積 dot_product = np.dot(v1, v2) # ベクトルのノルム norm_v1 = np.linalg.norm(v1) norm_v2 = np.linalg.norm(v2) # コサイン類似度 similarity = dot_product / (norm_v1 * norm_v2) return similarity # 総当りで類似度を計算 results = map( lambda i: { 'title': i['title'], 'body': i['body'], # ここでクエリと各文章のコサイン類似度を計算 'similarity': cosine_similarity(i['embedding'], query) }, vector_database ) # コサイン類似度で降順(大きい順)にソート results = sorted(results, key=lambda i: i['similarity'], reverse=True) これで関連するファイルと内容が取得できます。‘result[0]‘であれば最も関連するもの、‘result[-1]‘であれば関連しないものあるいは反対のものになります。反対かどうかはコサイン類似度の値が負の値になってるかで確認できます。 ...

2024年4月14日

RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part2

このpartで基本が8割がた作れました。内容としては簡単な質問をすると"タイトル"と"内容"が返ってくる程度です。まだまだ理想には遠いですね。 前回やったのは PDFファイル読み込み ページ番号取得と削除 色んな空白を削除 今回行ったのは 特殊文字の削除 大文字小文字統一 ベクトル変換 この辺も全てChat-GPT任せでした。特殊文字に関してはURLとメールアドレス、特殊文字の削除になります。まあ私が扱ってるものの中にURLやメールアドレスはあまりなさそうですが、一般的には必要だと思います。 text = re.sub(r'http[s]?://\S+', '', text) text = re.sub(r'\S*@\S*\s?', '', text) text = re.sub(r'[!#\$%&\'()*+,\-./:;<=>?@\[\\\]^_`{|}~]', '', text) 次は大文字小文字の統一ですね。これが少し特殊ですが例えば “123abcアイウエオ①②③¹²³"という文字があった時に"123abcアイウエオ123123"となるように変換しています。これもあまり使われてないと思いますが、一応変換しています。 normalized_text = ''.join([unicodedata.normalize('NFKC', char) if not unicodedata.east_asian_width(char) in ('F', 'W', 'A') else char for char in text]) それからベクトル変換ですね。OpenAIの埋め込みベクトルに変換していきます。課金が必要なので他のやり方がないか考えたいところですが、簡単に実装するために使用します。いつかはTF-IDFかWord2Vecを使ってみたいです。 openai.api_key = os.environ['OPENAI_API_KEY'] vector_database.append({ 'title': filename, 'body': text, 'embedding': vector }) 課金といっても10000字くらい埋め込みベクトルに使っても0.01$もかかりませんので数円くらいならお試しで触ってみてください。 一つだけ注意点を話すと8192トークンまでしか入力として受け取れないので、もし文字数が多くなる場合は分割して、それぞれの平均を取るといいかと思います。私の場合はとりあえず7000文字くらいで分割しています。 for i in range(0, len(text), max_tokens): # max_tokens=7000 segment = text[i:i+max_tokens] res = openai.embeddings.create( model='text-embedding-3-large', input=segment ) vectors.append(res.data[0].embedding) average_vector = np.mean(vectors, axis=0).tolist() そういえばいつからかはわかりませんが埋め込みベクトルの数値を取り出す方法が変わってたみたいで、‘res[data][0][embedding]’ から ‘res.data[0].embedding’に変わっています。前の方法が良ければバージョンを下げていただければ大丈夫です。 ...

2024年4月12日

RAGを開発してみたい!ので簡単なものでもいいので作ってみよう!part1

大規模言語モデルが出てきてChat-GPTやGemini、Claudeが出てきましたが、“ハルシネーション"は中々解決できない問題ですね。人間でも勘違いや嘘を堂々と言うことはあるのでどっこいどっこいですが、なるべくなら減らしていきたいですね。 そこで出てくるのが"RAG"になります。RAG(Retrieval-Augmented Generation)は、情報検索を組み込んだ生成モデルで、特に自然言語処理タスクでの応用が注目されています。RAGは、大量のテキストデータから関連情報を検索し、その情報を基に関連するテキストを生成します。 というわけで開発手順をChat-GPTとClaudeに聞いてみました。 大体似たような回答になりました。 データセットの準備と前処理 検索エンジンの構築 学習済みモデルの構築 RAGモデルの構築(検索エンジンと学習済みモデルを統合) モデルのトレーニング モデルの評価と調整 という流れになりました。 データセットの準備のほうはできています。私は昔作ったこちらを再利用しようと思いますが、論文やら書籍をダウンロードしたほうが楽だと思います。 というわけでデータセットの準備が終わったので今度は前処理をしていこうと思います。私のデータはPDFになってるので文字を抽出して、不要な文字を削除するところまでやります。 ということでgithubには登録しましたが一部コードをピックアップして # PDFPage.create_pages()を使用してページのリストを取得し、その長さを数える page_count = len(list(PDFPage.create_pages(document))) # スペース、タブ、改行を削除 text = text.replace(' ', '').replace('\t', '').replace('\n', '') # 正規表現を使って他の空白文字を削除 text = re.sub(r'\s+', '', text) 簡単ではありますがページ番号の削除と空白削除を行いましたので、とりあえず次のステップに進もうと思います。何をしたらいいかChat-GPTとClaudeに聞いてみました。 Claude曰く形態素解析や正規化は行わなくてもいいみたいなので、一旦インデックスの作成をやっていこうと思います。ただ色々試行錯誤してやった方が精度が上がりそうであればやろうと思います。 ところでClaudeに聞いたときこのような解答が返ってきたことがありました。野球って何でしょうか?(笑) ということで引き続き開発を進めていこうと思います。この先llamaindeを使うのか他のAPIを使うのか、ベクトルデータベースはどうするのか調べながら考えてみようと思います。ではでは

2024年4月7日