LLM=(Large Language Models) 大規模言語モデル。大規模なテキストデータを学習し、単語の出現確率を統計的に分析し、次に来る単語を予測できるようにしたもの。ChatGPTなどテキストの生成AIの一種。
LLM はTransformerという仕組みを使い、テキストを生成する
以下はTransformerをシミュレートしたサイト。上部のExampleに「This」と入れてみる。
すると、次に来る単語の確率を予測する。
このようにLLMは、どの単語が次に気やすいかというのを確率的に予測する(確率モデル)。確率は、大規模な文章(ネット上の文章など)を学習して計算されている。
LLMは通常ChatGPTのようにネット上のサービスとしてサーバ上で実行され、それと通信を行うのみである。
しかし、このサーバ部分を自分のPCで動作させることが出来るLLMも存在する。このようなLLMをローカルLLMと呼ぶ。
OllamaはさまざまなLLMをパソコン上で動作させることが出来るツール
まず、Windows用をダウンロードしインストールする。
モデル名 | 開発元 | パラメータ | ファイルサイズ | 特徴 |
---|---|---|---|---|
Llama-3-ELYZA-JP-8B | ELYZA | 80億 | 4.58GB | Llama3を元に日本語でファインチューニング(追加学習) |
Phi-3-mini | Microsoft | 38億 | 2GB | 小型。日本語は不得意 |
gemma | 20億 | 1GB | 最も小型 | |
Llama3 | Meta | 80億 | 16GB | 一般的モデル |
Llama3.2 3B | Meta | 30億 | 3GB | 最新 |
※参考:GPT-3.5 :パラメータ数1750億
llama3.2を実行する場合、以下のように入力。モデルがインストールしていない場合、自動インストールされる。
ollama run llama3.2
OllamaはWebのAPIを内蔵しており、localhost:11434 にpostでアクセスすることで問い合わせが可能。
import requests import json question = "人工知能とは何ですか?" response = requests.post("http://localhost:11434/api/generate", json={"model": "llama3.2", "prompt": question}) for r in response.iter_lines() : res = json.loads(r) print(res['response'], end='');
OllamaのPythonライブラリを利用するとより簡単にOllamaをPythonで利用できる。
pip install ollama
import ollama # 使用例 query = "人工知能とは何ですか?" response = ollama.generate(model='llama3.2', prompt=query) print( response['response'] )
LLMとのやりとりでは原則としてその一度きりの内容で判断した会話になる。これを過去の会話を反映させるには過去のやりとりも毎回LLMに送信する必要がある。
このときに利用するのがmessagesである。これは過去の会話を含めたリストであり、roleがuserのときにはユーザの入力、assistantのときにはLLMの解答を表す。
messages=[ {"role": "user", "content": "今日は金曜です。明日は?"}, {"role": "assistant", "content": "土曜です"}, {"role": "user", "content": "では明後日は?"}, ]
messagesを利用して問い合わせを行うには、ollama.chat()を使用する。このとき、引数streamをTrueにすることで、解答を順次得られるので、それを少しずつ画面に出すようにする。
# LLMに問い合わせ stream = ollama.chat( model='llama3.2', messages=messages, stream=True ) # 内容の表示 for chunk in stream: content = chunk['message']['content'] print(content, end='', flush=True)
ユーザが入力した内容を元にチャットを行うコードを作成する。会話内容をmessagesに保存していき、過去の会話内容を元に現在の会話を行えるようにする。
import ollama messages = [] while True: user_input = input("\nあなた: ") if user_input == 'bye': print("チャットを終了します。") break messages.append({'role': 'user', 'content': user_input}) stream = ollama.chat( model='llama3.2', messages=messages, stream=True ) print("\nAI: ", end='', flush=True) ai_response = "" # 最終的な解答文字列を保存 for chunk in stream: content = chunk['message']['content'] print(content, end='', flush=True) ai_response += content messages.append({'role': 'assistant', 'content': ai_response})
Streamlitを利用し、WebでOllamaを利用する。
import streamlit as st import ollama st.title("Ollama Chat サンプル") # セッション状態の初期化 if "messages" not in st.session_state: st.session_state.messages = [] # チャット履歴の表示 for message in st.session_state.messages: with st.chat_message(message["role"]): st.markdown(message["content"]) # ユーザー入力 if prompt := st.chat_input("メッセージを入力してください"): # ユーザーメッセージの追加 st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) # Ollama API呼び出しの準備 with st.chat_message("assistant"): message_placeholder = st.empty() full_response = "" # Ollamaクライアントを使用してストリーミング応答を取得 stream = ollama.chat( model='llama3.2', messages=st.session_state.messages, stream=True ) for chunk in stream: content = chunk['message']['content'] message_placeholder.markdown(full_response + "┃") full_response += content message_placeholder.markdown(full_response) # アシスタントの応答をメッセージ履歴に追加 st.session_state.messages.append({"role": "assistant", "content": full_response})
LLMが元々学習した内容には限りがあり、専門知識や組織内のローカルな内容、最新の情報は学習していない。これらの情報をプロンプトに付加することで適切な回答を得ることが出来る。
ただし、プロンプトの最大長には限界がある(Llama3で8kトークン、GPT-4で128Kトークンなど)。また、プロンプトが長くなることで応答に時間がかかるようになり、また、応答の精度も落ちる。
RAG(Retrieval Augmented Generation:検索拡張生成)とは、LLMに問い合わせを行う際に、情報源から別途検索・抽出した内容をプロンプトに付加すること。これらの情報を付加することで適切な回答を得ることが出来る。
テキストファイルを読み込み、そのテキストを文章単位に分割する。分割した文章をチャンクと呼ぶ。
まず、テキストファイルを読み込む関数 load_document を作成する。
def load_document(file_path): with open(file_path, 'r', encoding='utf-8') as file: return file.read()
文章を読み込みチャンクに分割する関数 load_chunk を作成する。
import spacy nlp = spacy.load("ja_ginza") def load_chunks(text, max_length=300, overlap=30): doc = nlp(text) chunks = [] current_chunk = "" for sent in doc.sents: if len(current_chunk) + len(sent.text) <= max_length: current_chunk += sent.text else: chunks.append(current_chunk) current_chunk = sent.text[-overlap:] if current_chunk: chunks.append(current_chunk) return chunks
続いて、クエリーとチャンクのベクトル化を行い、そのコサイン類似度が高いチャンクを抽出する。
import numpy as np from sklearn.metrics.pairwise import cosine_similarity def get_similar_chunks(query, chunks, top_n=10): # クエリと全チャンクをベクトル化 query_embedding = [nlp(query).vector] chunk_embeddings = [nlp(chunk).vector for chunk in chunks] # コサイン類似度を計算 similarities = cosine_similarity(query_embedding, chunk_embeddings)[0] # 類似度が高いtop_n件を取得 top_indices = np.argsort(similarities)[-top_n:][::-1] return [chunks[i] for i in top_indices]
最後にメイン処理を作成。
import ollama if __name__ == "__main__": text = load_document('kousoku.txt') chunks = load_chunks(text) while(True): query = input('質問:') if query == 'q': break # 質問と似たチャンクを取得 similar_chunks = get_similar_chunks(query, chunks) context = "\n\n".join(similar_chunks) # 各行を改行で挟んで結合 # プロンプトの作成 prompt = f"以下の情報を参考にして質問に答えてください:\n\n{context}\n\n質問: {query}" response = ollama.generate(model='llama3.2', prompt=prompt) print( response['response'] )
上記の例だと全チャンクをベクトル化することを問い合わせのたびに毎回行っている。これは一度だけ行えば良い処理であり、その結果を保存しておいて、問い合わせ時に読み込んで使用すれば良い。
そのために利用できるのがベクトルデータベースと呼ばれるものである。これを利用すれば文章とそのベクトルを保存し、類似するベクトルの検索も出来る。
ここではFacebookが開発したオープンソースのベクトルデータベースであるFAISS(Facebook AI Similarity Search)を利用し、ベクトルデータを保存し、問い合わせ時に活用する。
インストールはlangchainをインストールすることでFAISSが使用できる。
pip install langchain
まずは先ほどと同様にload_documentとload_chunksの関数を定義し、以下のように呼び出す。
text = load_document('kousoku.txt') chunks = load_chunks(text)
次にベクトル化を行うモデルを生成する。これはOllamaに備わっているOllamaEmbeddingsを利用する。
from langchain_community.embeddings import OllamaEmbeddings embeddings = OllamaEmbeddings(model="llama3.2")
次にベクトルデータベース FAISSのオブジェクトを生成する。これはFAISS.from_texts でチャンクとベクトルかを行うモデルを指定するだけである。
faiss = FAISS.from_texts(chunks, embeddings)
最後に保存を行う。これはフォルダを指定する。これを後で読み込む。
faiss.save_local('./kousoku.faiss')
実行すると、指定したフォルダにベクトル化したデータが保存される。
別ファイルで検索を行う。まず、ベクトルデータの読み込みを行う関数load_vectorstoreを作成する。
import ollama from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import FAISS # ファイルからベクトルストアを読み込む embeddings = OllamaEmbeddings(model='llama3.2') vectorstore = FAISS.load_local(f'./kousoku.faiss', embeddings,allow_dangerous_deserialization=True)
これを使い、検索を行う。
while(True): query = input('質問:') if query == 'q': break # 質問と似たチャンクを取得 similar_chunks = vectorstore.similarity_search(query, k=8) context = "\n".join([doc.page_content for doc in similar_chunks]) # プロンプトの作成 prompt = f"以下の情報を参考にして質問に答えてください:\n\n{context}\n\n質問: {query}" response = ollama.generate(model='llama3.2', prompt=prompt) print( response['response'] )