OpenVINO を使用して LLM による RAG システムを作成

この Jupyter ノートブックは、ローカルへのインストール後にのみ起動できます。

GitHub

検索拡張生成 (RAG) は、多くの場合プライベートまたはリアルタイムのデータを追加して LLM 知識を拡張する手法です。LLM は幅広いトピックについて推論できますが、その知識はトレーニングを受けた特定の時点までの公開データに限定されます。プライベート・データやモデルのカットオフ日以降に導入されたデータについて推論できる AI アプリケーションを構築するには、モデルの知識に必要な情報を追加する必要があります。適切な情報を取得してモデルプロンプトに挿入するプロセスは、検索拡張生成 (RAG) と呼ばれます。

LangChain は、言語モデルを活用したアプリケーションを開発するフレームワークです。 RAG アプリケーションの構築を支援するために設計されたコンポーネントが多数あります。このチュートリアルでは、Markdown または CSV データソースを使用して簡単な質問応答アプリケーションを構築します。

このチュートリアルは次のステップで構成されます。

  • 前提条件をインストールします。

  • OpenVINO と Hugging Face Optimum の統合を使用して、パブリックソースからモデルをダウンロードして変換します。

  • NNCF を使用してモデルの重みを 4 ビットまたは 8 ビットのデータタイプに圧縮します。

  • RAG チェーン・パイプラインを作成します。

  • チャット・パイプラインを実行します。

目次

必要条件

必要な依存関係をインストールします。

%pip uninstall -q -y openvino-dev openvino openvino-nightly
%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu\
"git+https://github.com/huggingface/optimum-intel.git"\
"nncf>=2.8.0"\
"datasets"\
"accelerate"\
"openvino-nightly"\
"gradio"\
"onnx" "chromadb" "sentence_transformers" "langchain" "langchainhub" "transformers>=4.34.0" "unstructured" "scikit-learn" "python-docx" "pdfminer.six"
WARNING: Skipping openvino-dev as it is not installed.
WARNING: Skipping openvino as it is not installed.
Note: you may need to restart the kernel to use updated packages.

[notice] A new release of pip is available: 23.3.1 -> 23.3.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

推論用のモデルを選択

このチュートリアルではさまざまなモデルがサポートされており、提供されたオプションから 1 つを選択して、オープンソース LLM ソリューションの品質を比較できます。

注: 一部のモデルの変換には、ユーザーによる追加アクションが必要になる場合があり、変換には少なくとも 64GB RAM が必要です。

利用可能な埋め込みモデルのオプションは次のとおりです。

  • all-mpnet-base-v2(すべて) - これは文変換モデルです。文と段落を 768 次元の密なベクトル空間にマッピングし、クラスタリングやセマンティック検索などのタスクに使用できます。モデルの詳細については、モデルカードをご覧ください。

  • text2vec-large-chinese(中国語) - これは CoSENT モデルです。文の埋め込み、テキストのマッチング、セマンティック検索などのタスクに使用できます。モデルの詳細については、モデルカードをご覧ください。

利用可能な LLM モデルのオプションには以下があります。

  • tiny-llama-1b-chat - これは、TinyLlama/TinyLlama-1.1B-intermediate-step-955k-2T をベースに微調整されたチャットモデルです。TinyLlama プロジェクトは、Llama 2 と同じアーキテクチャーとトークナイザーを採用し、3 兆個のトークンで 11 億個の Llama モデルを事前トレーニングすることを目的としています。これは、TinyLlama を、Llama 上に構築された多くのオープンソース・プロジェクトにプラグインできることを意味します。さらに、TinyLlama はコンパクトで、パラメーターは 1.1B のみです。このコンパクトさにより、制限された計算量とメモリー使用量を要求する多くのアプリケーションに対応できます。モデルの詳細については、モデルカードをご覧ください。

  • red-pajama-3b-chat - GPT-NEOX アーキテクチャーに基づいた 2.8B パラメーターの事前トレーニング済み言語モデル。これは、Togetter Computer とオープンソース AI コミュニティーのリーダーによって開発されました。このモデルは、チャット機能を強化するため OASST1 および Dolly2 データセットに基づいて微調整されています。モデルの詳細については、Hugging Face モデルカードを参照してください。

  • llama-2-7b-chat - LLama 2 は、Meta によって開発された LLama モデルの第 2 世代です。Llama 2 は、事前トレーニングされ、微調整された生成テキストモデルのコレクションであり、その規模は 70 億から 700 億のパラメーターに及びます。 llama-2-7b-chat は、対話の使用例向けに微調整および最適化された LLama 2 の 70 億パラメーターのバージョンです。モデルの詳細については、論文リポジトリーおよび Hugging Face モデルカードを参照してください。

    注: デモでモデルを実行するには、ライセンス契約に同意する必要があります。Hugging Face Hub の登録ユーザーである必要があります。Hugging Face モデルカードにアクセスし、利用規約をよく読み、同意ボタンをクリックしてください。以下のコードを実行するには、アクセストークンを使用する必要があります。アクセストークンの詳細については、ドキュメントのこのセクションを参照してください。次のコードを使用して、ノートブック環境の Hugging Face Hub にログインできます。

    ## login to huggingfacehub to get access to pretrained model
    
    from huggingface_hub import notebook_login, whoami
    
    try:
        whoami()
        print('Authorization token already provided')
    except OSError:
        notebook_login()
    
  • mpt-7b-chat - MPT-7B は MosaicPretrainedTransformer (MPT) モデルファミリーの一部であり、効率的なトレーニングと推論のために最適化された修正されたトランスフォーマー・アーキテクチャーを使用します。これらのアーキテクチャー変更には、パフォーマンスが最適化されたレイヤーの実装と、位置埋め込みを線形バイアスによるアテンション (ALiBi) に置き換えることによるコンテキストの長さの制限の排除が含まれます。これらの変更により、MPT モデルは高いスループット効率と安定した収束でトレーニングできます。MPT-7B-chat は、対話生成のためのチャットボットのようなモデルです。これは、ShareGPT-VicunaHC3AlpacaHH-RLHF、 および Evol-Instruct データセット上で MPT-7B を微調整することによって構築されました。モデルの詳細については、ブログの投稿リポジトリーおよび Hugging Face モデルカードを参照してください。

  • qwen-7b-chat - Qwen-7B は、Alibaba Cloud が提案する大規模言語モデルシリーズ Qwen (略称 Tongyi Qianwen) の 7B パラメーター・バ―ションです。Qwen-7B は、ウェブテキスト、書籍、コードなどを含む大量のデータで事前トレーニングされた Transformer ベースの大規模言語モデルです。Qwen の詳細については、GitHub コード・リポジトリーを参照してください。

  • chatglm3-6b - ChatGLM3-6B は、ChatGLM シリーズの最新のオープンソース・モデルです。hatGLM3-6B は、前の 2 世代のスムーズな対話や低い導入しきい値など多くの優れた機能を維持しながら、多様なトレーニング・データセット、十分なトレーニング・ステップ、合理的なトレーニング戦略を採用しています。ChatGLM3-6Bでは、通常のマルチターン・ダイアログに加え、新たに設計されたプロンプト形式を採用しています。モデルの詳細については、モデルカードを参照してください。

  • mistral-7b - Mistral-7B-v0.1 大規模言語モデル (LLM) は、70 億のパラメーターを備えた事前トレーニング済みの生成テキストモデルです。モデルの詳細については、モデルカード論文およびリリースに関するブログの投稿を参照してください。

  • zephyr-7b-beta - Zephyr は、有用なアシスタントとして機能するように訓練された一連の言語モデルです。Zephyr-7B-beta はシリーズの 2 番目のモデルであり、直接優先最適化 (DPO) を使用して公開されている合成データセットの組み合わせでトレーニングされた、mistralai/Mistral-7B-v0.1 の微調整されたバージョンです。モデルの詳細については、技術レポートおよび Hugging Face モデルカードを参照してください。

  • neural-chat-7b-v3-1 - インテル Gaudi を使用して微調整された Mistral-7b モデル。モデルはオープンソース・データセット Open-Orca/SlimOrca に基づいて微調整され、直接優先最適化 (DPO) アルゴリズムに合わせて調整されています。詳細については、モデルカードブログの投稿をご覧ください

  • notus-7b-v1 - Notus は、直接優先最適化 (DPO) および関連する RLHF 技術を使用して微調整されたモデルのコレクションです。このモデルは、zephyr-7b-sft を DPO で微調整した最初のバージョンです。データファーストのアプローチに従って、Notus-7B-v1 と Zephyr-7B-beta の唯一の違いは、dDPO に使用される優先データセットです。データセットの作成に提案されたアプローチは、AlpacaEval 上の Zephyr-7B-beta や Claude 2 を超える Notus-7b を効果的に微調整するのに役立ちます。モデルの詳細については、モデルカードをご覧ください。

from pathlib import Path
from optimum.intel import OVQuantizer
from optimum.intel.openvino import OVModelForCausalLM
import openvino as ov
import torch
import nncf
import logging
import shutil
import gc
import ipywidgets as widgets
from transformers import (
    AutoModelForCausalLM,
    AutoModel,
    AutoTokenizer,
    AutoConfig,
    TextIteratorStreamer,
    pipeline,
    StoppingCriteria,
    StoppingCriteriaList,
)
INFO:nncf:NNCF initialized successfully. Supported frameworks detected: torch, tensorflow, onnx, openvino
2023-12-25 07:58:21.310297: I tensorflow/core/util/port.cc:111] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable TF_ENABLE_ONEDNN_OPTS=0.
2023-12-25 07:58:21.312367: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.
2023-12-25 07:58:21.337757: E tensorflow/compiler/xla/stream_executor/cuda/cuda_dnn.cc:9342] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2023-12-25 07:58:21.337778: E tensorflow/compiler/xla/stream_executor/cuda/cuda_fft.cc:609] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2023-12-25 07:58:21.337798: E tensorflow/compiler/xla/stream_executor/cuda/cuda_blas.cc:1518] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2023-12-25 07:58:21.343045: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.
2023-12-25 07:58:21.343941: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI AVX512_BF16 AVX_VNNI AMX_TILE AMX_INT8 AMX_BF16 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2023-12-25 07:58:21.912373: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT

モデルの変換

LLM モデルの変換

from config import SUPPORTED_EMBEDDING_MODELS, SUPPORTED_LLM_MODELS

llm_model_id = list(SUPPORTED_LLM_MODELS)

llm_model_id = widgets.Dropdown(
    options=llm_model_id,
    value=llm_model_id[0],
    description="LLM Model:",
    disabled=False,
)

llm_model_id
Dropdown(description='LLM Model:', options=('tiny-llama-1b-chat', 'red-pajama-3b-chat', 'llama-2-chat-7b', 'mp…
llm_model_configuration = SUPPORTED_LLM_MODELS[llm_model_id.value]
print(f"Selected LLM model {llm_model_id.value}")
Selected LLM model chatglm3-6b

Optimum Intel を使用すると、Hugging Face Hub から最適化されたモデルをロードし、Hugging Face API を使用して OpenVINO ランタイムで推論を実行するパイプラインを作成できます。Optimum 推論モデルは、Hugging Face Transformers モデルと API の互換性があります。つまり、AutoModelForXxx クラスを対応する OVModelForXxx クラスに置き換えるだけで済みます。

以下は RedPajama モデルの例です。

-from transformers import AutoModelForCausalLM
+from optimum.intel.openvino import OVModelForCausalLM
from transformers import AutoTokenizer, pipeline

model_id = "togethercomputer/RedPajama-INCITE-Chat-3B-v1"
-model = AutoModelForCausalLM.from_pretrained(model_id)
+model = OVModelForCausalLM.from_pretrained(model_id, export=True)

モデルクラスの初期化は、from_pretrained メソッドの呼び出しから始まります。Transformers モデルをダウンロードして変換する場合は、パラメーター export=True を追加する必要があります。save_pretrained メソッドを使用して、変換されたモデルを次回に使用するため保存できます。トークナイザー・クラスとパイプライン API は Optimum モデルと互換性があります。

生成プロセスを最適化し、メモリーをより効率的に使用するには、use_cache=True オプションを有効にします。出力側は自動回帰であるため、出力トークンの非表示状態は、その後の生成ステップごとに計算されると同じままになります。したがって、新しいトークンを生成するたびに再計算するのは無駄であるように思えます。キャッシュを使用すると、モデルは計算後に非表示の状態を保存します。モデルは各タイムステップで最後に生成された出力トークンのみを計算し、保存された出力トークンを非表示のトークンに再利用します。これにより、変圧器モデルの生成の複雑さが \(O(n^3)\) から \(O(n^2)\) に軽減されます。仕組みの詳細については、この記事を参照してくださいこのオプションを使用すると、モデルは前のステップの非表示状態 (キャッシュされたアテンション・キーと値) を入力として取得し、さらに現在のステップの非表示状態を出力として提供します。これは、次のすべての反復では、前のステップから取得した新しいトークンと、次のトークン予測を取得するためのキャッシュされたキー値のみを提供するだけで十分であることを意味します。

ここでは、MPT、Qwen、ChatGLM モデルは現在 Optimum Intel の対象になっていないため、手動で変換して Optimum Intel と互換性のあるラッパーを作成します。

モデルの重みを圧縮

重み圧縮アルゴリズムは、モデルの重みを圧縮することを目的としており、大規模言語モデル (LLM) など、重みのサイズがアクティベーションのサイズよりも相対的に大きい大規模モデルのモデル・フットプリントとパフォーマンスを最適化するために使用できます。INT8 圧縮と比較して、INT4 圧縮はパフォーマンスをさらに向上させますが、予測品質は若干低下します。

Optimum Intel を使用した重み圧縮

Optimum Intel の OVQuantizer クラスでサポートされるモデルの NNCF を介した重み圧縮を有効にするには、OVModelForCausalLM モデルに使用する必要があります。OVQuantizer.quantize(save_directory=save_dir, weights_only=True) 重みの圧縮を有効にします。RedPajama、LLAMA、Zephyr の例で行う方法を検討します。

注: 現在、Optimum Intel 適用を使用した重み圧縮は INT8 圧縮のみをサポートしています。以下で説明する NNCF API を使用して、これらのモデルに INT4 圧縮を適用します。

注: dGPU 上の INT4/INT8 圧縮モデルではスピードアップされない可能性があります。

NNCF を使用した重み圧縮

NNCF を直接使用して、OpenVINO モデルの重み圧縮を実行することもできます。nncf.compress_weights 関数は、OpenVINO モデル・インスタンスを受け入れ、線形レイヤーと埋め込みレイヤーの重みを圧縮します。MPT モデルに基づいてこのバリアントを検討します。

注: このチュートリアルには、FP16 および INT4/INT8 重み圧縮シナリオの変換モデルが含まれます。最初の実行ではメモリーを消費し時間がかかる可能性があります。以下で圧縮精度を手動で制御できます。

from IPython.display import display

prepare_int4_model = widgets.Checkbox(
    value=True,
    description="Prepare INT4 model",
    disabled=False,
)
prepare_int8_model = widgets.Checkbox(
    value=False,
    description="Prepare INT8 model",
    disabled=False,
)
prepare_fp16_model = widgets.Checkbox(
    value=False,
    description="Prepare FP16 model",
    disabled=False,
)

display(prepare_int4_model)
display(prepare_int8_model)
display(prepare_fp16_model)
Checkbox(value=True, description='Prepare INT4 model')
Checkbox(value=False, description='Prepare INT8 model')
Checkbox(value=False, description='Prepare FP16 model')
from converter import converters

nncf.set_log_level(logging.ERROR)

pt_model_id = llm_model_configuration["model_id"]
pt_model_name = llm_model_id.value.split("-")[0]
model_type = AutoConfig.from_pretrained(pt_model_id, trust_remote_code=True).model_type
fp16_model_dir = Path(llm_model_id.value) / "FP16"
int8_model_dir = Path(llm_model_id.value) / "INT8_compressed_weights"
int4_model_dir = Path(llm_model_id.value) / "INT4_compressed_weights"


def convert_to_fp16():
    if (fp16_model_dir / "openvino_model.xml").exists():
        return
    if not llm_model_configuration["remote"]:
        ov_model = OVModelForCausalLM.from_pretrained(
            pt_model_id, export=True, compile=False, load_in_8bit=False
        )
        ov_model.half()
        ov_model.save_pretrained(fp16_model_dir)
        del ov_model
    else:
        model_kwargs = {}
        if "revision" in llm_model_configuration:
            model_kwargs["revision"] = llm_model_configuration["revision"]
        model = AutoModelForCausalLM.from_pretrained(
            llm_model_configuration["model_id"],
            torch_dtype=torch.float32,
            trust_remote_code=True,
            **model_kwargs
        )
        converters[pt_model_name](model, fp16_model_dir)
        del model
    gc.collect()


def convert_to_int8():
    if (int8_model_dir / "openvino_model.xml").exists():
        return
    int8_model_dir.mkdir(parents=True, exist_ok=True)
    if not llm_model_configuration["remote"]:
        if fp16_model_dir.exists():
            ov_model = OVModelForCausalLM.from_pretrained(fp16_model_dir, compile=False, load_in_8bit=False)
        else:
            ov_model = OVModelForCausalLM.from_pretrained(
                pt_model_id, export=True, compile=False
            )
            ov_model.half()
        quantizer = OVQuantizer.from_pretrained(ov_model)
        quantizer.quantize(save_directory=int8_model_dir, weights_only=True)
        del quantizer
        del ov_model
    else:
        convert_to_fp16()
        ov_model = ov.Core().read_model(fp16_model_dir / "openvino_model.xml")
        shutil.copy(fp16_model_dir / "config.json", int8_model_dir / "config.json")
        configuration_file = fp16_model_dir / f"configuration_{model_type}.py"
        if configuration_file.exists():
            shutil.copy(
                configuration_file, int8_model_dir / f"configuration_{model_type}.py"
            )
        compressed_model = nncf.compress_weights(ov_model)
        ov.save_model(compressed_model, int8_model_dir / "openvino_model.xml")
        del ov_model
        del compressed_model
    gc.collect()


def convert_to_int4():
    compression_configs = {
        "zephyr-7b-beta": {
            "mode": nncf.CompressWeightsMode.INT4_SYM,
            "group_size": 64,
            "ratio": 0.6,
        },
        "mistral-7b": {
            "mode": nncf.CompressWeightsMode.INT4_SYM,
            "group_size": 64,
            "ratio": 0.6,
        },
        "notus-7b-v1": {
            "mode": nncf.CompressWeightsMode.INT4_SYM,
            "group_size": 64,
            "ratio": 0.6,
        },"PERFORMANCE_HINT": "LATENCY", "NUM_STREAMS": "1",
        "neural-chat-7b-v3-1": {
            "mode": nncf.CompressWeightsMode.INT4_SYM,
            "group_size": 64,
            "ratio": 0.6,
        },
        "llama-2-chat-7b": {
            "mode": nncf.CompressWeightsMode.INT4_SYM,
            "group_size": 128,
            "ratio": 0.8,
        },
        "chatglm2-6b": {
            "mode": nncf.CompressWeightsMode.INT4_SYM,
            "group_size": 128,
            "ratio": 0.72
        },
        "qwen-7b-chat": {
            "mode": nncf.CompressWeightsMode.INT4_SYM,
            "group_size": 128,
            "ratio": 0.6
        },
        'red-pajama-3b-chat': {
            "mode": nncf.CompressWeightsMode.INT4_ASYM,
            "group_size": 128,
            "ratio": 0.5,
        },
        "default": {
            "mode": nncf.CompressWeightsMode.INT4_ASYM,
            "group_size": 128,
            "ratio": 0.8,
        },
    }

    model_compression_params = compression_configs.get(
        llm_model_id.value, compression_configs["default"]
    )
    if (int4_model_dir / "openvino_model.xml").exists():
        return
    int4_model_dir.mkdir(parents=True, exist_ok=True)
    if not llm_model_configuration["remote"]:
        if not fp16_model_dir.exists():
            model = OVModelForCausalLM.from_pretrained(
                pt_model_id, export=True, compile=False, load_in_8bit=False
            ).half()
            model.config.save_pretrained(int4_model_dir)
            ov_model = model._original_model
            del model
            gc.collect()
        else:
            ov_model = ov.Core().read_model(fp16_model_dir / "openvino_model.xml")
            shutil.copy(fp16_model_dir / "config.json", int4_model_dir / "config.json")

    else:
        convert_to_fp16()
        ov_model = ov.Core().read_model(fp16_model_dir / "openvino_model.xml")
        shutil.copy(fp16_model_dir / "config.json", int4_model_dir / "config.json")
        configuration_file = fp16_model_dir / f"configuration_{model_type}.py"
        if configuration_file.exists():
            shutil.copy(
                configuration_file, int4_model_dir / f"configuration_{model_type}.py"
            )
    compressed_model = nncf.compress_weights(ov_model, **model_compression_params)
    ov.save_model(compressed_model, int4_model_dir / "openvino_model.xml")
    del ov_model
    del compressed_model
    gc.collect()


if prepare_fp16_model.value:
    convert_to_fp16()
if prepare_int8_model.value:
    convert_to_int8()
if prepare_int4_model.value:
    convert_to_int4()

さまざまな圧縮タイプのモデルサイズを比較してみましょう。

fp16_weights = fp16_model_dir / "openvino_model.bin"
int8_weights = int8_model_dir / "openvino_model.bin"
int4_weights = int4_model_dir / "openvino_model.bin"

if fp16_weights.exists():
    print(f"Size of FP16 model is {fp16_weights.stat().st_size / 1024 / 1024:.2f} MB")
for precision, compressed_weights in zip([8, 4], [int8_weights, int4_weights]):
    if compressed_weights.exists():
        print(
            f"Size of model with INT{precision} compressed weights is {compressed_weights.stat().st_size / 1024 / 1024:.2f} MB"
        )
    if compressed_weights.exists() and fp16_weights.exists():
        print(
            f"Compression rate for INT{precision} model: {fp16_weights.stat().st_size / compressed_weights.stat().st_size:.3f}"
        )
Size of FP16 model is 11909.69 MB
Size of model with INT4 compressed weights is 3890.41 MB
Compression rate for INT4 model: 3.061

埋め込みモデルの変換

一部の埋め込みモデルは限られた言語しかサポートできないため、選択した LLM に応じてそれらを除外できます。

embedding_model_id = list(SUPPORTED_EMBEDDING_MODELS)

if "qwen" not in llm_model_id.value and "chatglm" not in llm_model_id.value:
    embedding_model_id = [x for x in embedding_model_id if "chinese" not in x]

embedding_model_id = widgets.Dropdown(
    options=embedding_model_id,
    value=embedding_model_id[0],
    description="Embedding Model:",
    disabled=False,
)

embedding_model_id
Dropdown(description='Embedding Model:', options=('all-mpnet-base-v2', 'text2vec-large-chinese'), value='all-m…
embedding_model_configuration = SUPPORTED_EMBEDDING_MODELS[embedding_model_id.value]
print(f"Selected {embedding_model_id.value} model")
Selected all-mpnet-base-v2 model
embedding_model_dir = Path(embedding_model_id.value)

if not (embedding_model_dir / "openvino_model.xml").exists():
    model = AutoModel.from_pretrained(embedding_model_configuration["model_id"])
    converters[embedding_model_id.value](model, embedding_model_dir)
    tokenizer = AutoTokenizer.from_pretrained(embedding_model_configuration["model_id"])
    tokenizer.save_pretrained(embedding_model_dir)
    del model

推論用のデバイスとモデルバリアントを選択

注: dGPU 上の INT4/INT8 圧縮モデルではスピードアップされない可能性があります。

モデル推論を埋め込むデバイスを選択

core = ov.Core()
embedding_device = widgets.Dropdown(
    options=core.available_devices + ["AUTO"],
    value="CPU",
    description="Device:",
    disabled=False,
)

embedding_device
Dropdown(description='Device:', options=('CPU', 'GPU', 'AUTO'), value='CPU')
print(f"Embedding model will be loaded to {embedding_device.value} device for response generation")
Embedding model will be loaded to CPU device for response generation

LLM モデル推論用のデバイスを選択

llm_device = widgets.Dropdown(
    options=core.available_devices + ["AUTO"],
    value="CPU",
    description="Device:",
    disabled=False,
)

llm_device
Dropdown(description='Device:', options=('CPU', 'GPU', 'AUTO'), value='CPU')
print(f"LLM model will be loaded to {llm_device.value} device for response generation")
LLM model will be loaded to CPU device for response generation

モデルのロード

埋め込みモデルのロード

テキストを埋め込みに変換するために使用される、LangChain のテキスト埋め込みモデルのラッパーです。

from ov_embedding_model import OVEmbeddings

embedding = OVEmbeddings.from_model_id(
    embedding_model_dir,
    do_norm=embedding_model_configuration["do_norm"],
    ov_config={
        "device_name": embedding_device.value,
        "config": {"PERFORMANCE_HINT": "THROUGHPUT"},
    },
    model_kwargs={
        "model_max_length": 512,
    },
)

LLM モデルのロード

以下のセルは、OVModelForCausalLM モデルに基づいて OVMPTModelOVQWENModel、および OVCHATGLM2Model ラッパーを作成します。

from ov_llm_model import model_classes
available_models = []
if int4_model_dir.exists():
    available_models.append("INT4")
if int8_model_dir.exists():
    available_models.append("INT8")
if fp16_model_dir.exists():
    available_models.append("FP16")

model_to_run = widgets.Dropdown(
    options=available_models,
    value=available_models[0],
    description="Model to run:",
    disabled=False,
)

model_to_run
Dropdown(description='Model to run:', options=('INT4', 'FP16'), value='INT4')
from langchain.llms import Hugging FacePipeline

if model_to_run.value == "INT4":
    model_dir = int4_model_dir
elif model_to_run.value == "INT8":
    model_dir = int8_model_dir
else:
    model_dir = fp16_model_dir
print(f"Loading model from {model_dir}")

ov_config = {"PERFORMANCE_HINT": "LATENCY", "NUM_STREAMS": "1", "CACHE_DIR": ""}

# On a GPU device a model is executed in FP16 precision. For red-pajama-3b-chat model there known accuracy
# issues caused by this, which we avoid by setting precision hint to "f32".
if llm_model_id.value == "red-pajama-3b-chat" and "GPU" in core.available_devices and llm_device.value in ["GPU", "AUTO"]:
    ov_config["INFERENCE_PRECISION_HINT"] = "f32"

model_name = llm_model_configuration["model_id"]
stop_tokens = llm_model_configuration.get("stop_tokens")
class_key = llm_model_id.value.split("-")[0]
tok = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

class StopOnTokens(StoppingCriteria):
    def __init__(self, token_ids):
        self.token_ids = token_ids

    def __call__(
        self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs
    ) -> bool:
        for stop_id in self.token_ids:
            if input_ids[0][-1] == stop_id:
                return True
        return False

if stop_tokens is not None:
    if isinstance(stop_tokens[0], str):
        stop_tokens = tok.convert_tokens_to_ids(stop_tokens)

    stop_tokens = [StopOnTokens(stop_tokens)]

model_class = (
    OVModelForCausalLM
    if not llm_model_configuration["remote"]
    else model_classes[class_key]
)
ov_model = model_class.from_pretrained(
    model_dir,
    device=llm_device.value,
    ov_config=ov_config,
    config=AutoConfig.from_pretrained(model_dir, trust_remote_code=True),
    trust_remote_code=True,
)
Loading model from chatglm3-6b/INT4_compressed_weights
The argument trust_remote_code is to be used along with export=True. It will be ignored.
Compiling the model to CPU ...

応答テキストの生成に使用される、LangChain の LLM/チャットモデルのラッパーです。OpenVINO でコンパイルされたモデルは、Hugging FacePipeline クラスを通じてローカルで実行できます。

streamer = TextIteratorStreamer(
    tok, timeout=30.0, skip_prompt=True, skip_special_tokens=True
)
generate_kwargs = dict(
    model=ov_model,
    tokenizer=tok,
    max_new_tokens=256,
    streamer=streamer,
    # temperature=1,
    # do_sample=True,
    # top_p=0.8,
    # top_k=20,
    # repetition_penalty=1.1,
)
if stop_tokens is not None:
    generate_kwargs["stopping_criteria"] = StoppingCriteriaList(stop_tokens)

pipe = pipeline("text-generation", **generate_kwargs)
llm = Hugging FacePipeline(pipeline=pipe)

ドキュメントの QA を実行

モデルが作成されたら、Gradio を使用してチャットボット・インターフェイスをセットアップできます。

一般的な RAG アプリケーションには、次の 2 つの主要コンポーネントがあります。

  • インデックス作成: ソースからデータを取り込み、インデックスを作成するパイプライン。これは通常、オフラインで発生します。

  • 取得と生成: 実際の RAG チェーンは、実行時にユーザークエリーを受け取り、インデックスから関連データを取得して、それをモデルに渡します。

生データから回答までの最も一般的な完全なシーケンスは次のようになります。

インデックス作成

  1. Load: まずデータをロードする必要があります。これには DocumentLoaders を使用します。
  2. Split: テキスト分割ツールは、大きなドキュメントを小さなチャンクに分割します。これは、データのインデックス作成とモデルへのデータの受け渡しの両方に役立ちます。大きなチャンクは検索が困難であり、モデルの有限コンテキスト・ウィンドウでは検索できないためです。
  3. Store: 後で検索できるように、分割を保存してインデックスを作成する場所が必要です。これは、多くの場合、VectorStore と Embeddings モデルを使用して行われます。
Indexing pipeline

インデックス・パイプライン

取得と生成

  1. Retrieve: ユーザー入力があれば、Retriever を使用して関連する分割がストレージから取得されます。
  2. Generate: LLM は、質問と取得したデータを含むプロンプトを使用して回答を生成します。
Retrieval and generation pipeline

検索と生成パイプライン

from typing import List
from langchain.text_splitter import CharacterTextSplitter, RecursiveCharacterTextSplitter, MarkdownTextSplitter
from langchain.document_loaders import (
    CSVLoader,
    EverNoteLoader,
    PDFMinerLoader,
    TextLoader,
    UnstructuredEPubLoader,
    UnstructuredHTMLLoader,
    UnstructuredMarkdownLoader,
    UnstructuredODTLoader,
    UnstructuredPowerPointLoader,
    UnstructuredWordDocumentLoader, )


class ChineseTextSplitter(CharacterTextSplitter):
    def __init__(self, pdf: bool = False, **kwargs):
        super().__init__(**kwargs)
        self.pdf = pdf

    def split_text(self, text: str) -> List[str]:
        if self.pdf:
            text = re.sub(r"\n{3,}", "\n", text)
            text = text.replace("\n\n", "")
        sent_sep_pattern = re.compile(
            '([﹒﹔﹖﹗.。!?]["’”」』]{0,2}|(?=["‘“「『]{1,2}|$))')
        sent_list = []
        for ele in sent_sep_pattern.split(text):
            if sent_sep_pattern.match(ele) and sent_list:
                sent_list[-1] += ele
            elif ele:
                sent_list.append(ele)
        return sent_list


TEXT_SPLITERS = {
    "Character": CharacterTextSplitter,
    "RecursiveCharacter": RecursiveCharacterTextSplitter,
    "Markdown": MarkdownTextSplitter,
    "Chinese": ChineseTextSplitter,
}


LOADERS = {
    ".csv": (CSVLoader, {}),
    ".doc": (UnstructuredWordDocumentLoader, {}),
    ".docx": (UnstructuredWordDocumentLoader, {}),
    ".enex": (EverNoteLoader, {}),
    ".epub": (UnstructuredEPubLoader, {}),
    ".html": (UnstructuredHTMLLoader, {}),
    ".md": (UnstructuredMarkdownLoader, {}),
    ".odt": (UnstructuredODTLoader, {}),
    ".pdf": (PDFMinerLoader, {}),
    ".ppt": (UnstructuredPowerPointLoader, {}),
    ".pptx": (UnstructuredPowerPointLoader, {}),
    ".txt": (TextLoader, {"encoding": "utf8"}),
}
from langchain.prompts import PromptTemplate
from langchain.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.docstore.document import Document
from threading import Event, Thread
import gradio as gr
import re
from uuid import uuid4


def load_single_document(file_path: str) -> List[Document]:
    """
    helper for loading a single document

    Params:
      file_path: document path
    Returns:
      documents loaded

    """
    ext = "." + file_path.rsplit(".", 1)[-1]
    if ext in LOADERS:
        loader_class, loader_args = LOADERS[ext]
        loader = loader_class(file_path, **loader_args)
        return loader.load()

    raise ValueError(f"File does not exist '{ext}'")


def default_partial_text_processor(partial_text: str, new_text: str):
    """
    helper for updating partially generated answer, used by default

    Params:
      partial_text: text buffer for storing previosly generated text
      new_text: text update for the current step
    Returns:
      updated text string

    """
    partial_text += new_text
    return partial_text


text_processor = llm_model_configuration.get(
    "partial_text_processor", default_partial_text_processor
)


def build_chain(docs, spliter_name, chunk_size, chunk_overlap, vector_search_top_k):
    """
    Initialize a QA chain

    Params:
      doc: orignal documents provided by user
      chunk_size:  size of a single sentence chunk
      chunk_overlap: overlap size between 2 chunks
      vector_search_top_k: Vector search top k

    """
    documents = []
    for doc in docs:
        documents.extend(load_single_document(doc.name))

    text_splitter = TEXT_SPLITERS[spliter_name](
        chunk_size=chunk_size, chunk_overlap=chunk_overlap
    )

    texts = text_splitter.split_documents(documents)

    db = Chroma.from_documents(texts, embedding)
    retriever = db.as_retriever(search_kwargs={"k": vector_search_top_k})

    global rag_chain
    prompt = PromptTemplate.from_template(llm_model_configuration["prompt_template"])
    chain_type_kwargs = {"prompt": prompt}
    rag_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=retriever,
        chain_type_kwargs=chain_type_kwargs,
    )

    return "Retriever is Ready"


def user(message, history):
    """
    callback function for updating user messages in interface on submit button click

    Params:
      message: current message
      history: conversation history
    Returns:
      None
    """
    # Append the user's message to the conversation history
    return "", history + [[message, ""]]


def bot(history, conversation_id):
    """
    callback function for running chatbot on submit button click

    Params:
      history: conversation history.
      conversation_id: unique conversation identifier.

    """
    stream_complete = Event()

    def infer(question):
        rag_chain.run(question)
        stream_complete.set()

    t1 = Thread(target=infer, args=(history[-1][0],))
    t1.start()

    # Initialize an empty string to store the generated text
    partial_text = ""
    for new_text in streamer:
        partial_text = text_processor(partial_text, new_text)
        history[-1][1] = partial_text
        yield history


def get_uuid():
    """
    universal unique identifier for thread
    """
    return str(uuid4())


with gr.Blocks(
    theme=gr.themes.Soft(),
    css=".disclaimer {font-variant-caps: all-small-caps;}",
) as demo:
    conversation_id = gr.State(get_uuid)
    gr.Markdown("""<h1><center>QA over Document</center></h1>""")
    gr.Markdown(f"""<center>Powered by OpenVINO and {llm_model_id.value} </center>""")
    with gr.Row():
        with gr.Column(scale=1):
            docs = gr.File(
                label="Load text files",
                file_count="multiple",
                file_types=[
                    ".csv",
                    ".doc",
                    ".docx",
                    ".enex",
                    ".epub",
                    ".html",
                    ".md",
                    ".odt",
                    ".pdf",
                    ".ppt",
                    ".pptx",
                    ".txt",
                ],
            )
            load_docs = gr.Button("Build Retriever")
            retriever_argument = gr.Accordion("Retriever Configuration", open=False)
            with retriever_argument:
                spliter = gr.Dropdown(
                    ["Character", "RecursiveCharacter", "Markdown", "Chinese"],
                    value="RecursiveCharacter",
                    label="Text Spliter",
                    info="Method used to splite the documents",
                    multiselect=False,
                )

                chunk_size = gr.Slider(
                    label="Chunk size",
                    value=1000,
                    minimum=100,
                    maximum=2000,
                    step=50,
                    interactive=True,
                    info="Size of sentence chunk",
                )

                chunk_overlap = gr.Slider(
                    label="Chunk overlap",
                    value=200,
                    minimum=0,
                    maximum=400,
                    step=10,
                    interactive=True,
                    info=("Overlap between 2 chunks"),
                )

                vector_search_top_k = gr.Slider(
                    1,
                    10,
                    value=6,
                    step=1,
                    label="Vector search top k",
                    interactive=True,
                )
            langchain_status = gr.Textbox(
                label="Status", value="Retriever is Not ready", interactive=False
            )
        with gr.Column(scale=4):
            chatbot = gr.Chatbot(height=600)
            with gr.Row():
                with gr.Column():
                    msg = gr.Textbox(
                        label="Chat Message Box",
                        placeholder="Chat Message Box",
                        show_label=False,
                        container=False,
                    )
                with gr.Column():
                    with gr.Row():
                        submit = gr.Button("Submit")
                        clear = gr.Button("Clear")
    load_docs.click(
        build_chain,
        inputs=[docs, spliter, chunk_size, chunk_overlap, vector_search_top_k],
        outputs=[langchain_status],
        queue=False,
    )
    submit_event = msg.submit(
        user, [msg, chatbot], [msg, chatbot], queue=False, trigger_mode="once"
    ).then(bot, [chatbot, conversation_id], chatbot, queue=True)
    submit_click_event = submit.click(
        user, [msg, chatbot], [msg, chatbot], queue=False, trigger_mode="once"
    ).then(bot, [chatbot, conversation_id], chatbot, queue=True)
    clear.click(lambda: None, None, chatbot, queue=False)

demo.queue(max_size=2)
# if you are launching remotely, specify server_name and server_port
#  demo.launch(server_name='your server name', server_port='server port in int')
# if you have any issue to launch on your platform, you can pass share=True to launch method:
# demo.launch(share=True)
# it creates a publicly shareable link for the interface. Read more in the docs: https://gradio.app/docs/
demo.launch()
Running on local URL:  http://10.3.233.70:4888

To create a public link, set share=True in launch().
/home/ethan/intel/openvino_notebooks/openvino_env/lib/python3.10/site-packages/optimum/intel/openvino/modeling_decoder.py:388: FutureWarning: shared_memory is deprecated and will be removed in 2024.0. Value of shared_memory is going to override share_inputs value. Please use only share_inputs explicitly.
  self.request.start_async(inputs, shared_memory=True)
/home/ethan/intel/openvino_notebooks/openvino_env/lib/python3.10/site-packages/optimum/intel/openvino/modeling_decoder.py:388: FutureWarning: shared_memory is deprecated and will be removed in 2024.0. Value of shared_memory is going to override share_inputs value. Please use only share_inputs explicitly.
  self.request.start_async(inputs, shared_memory=True)
/home/ethan/intel/openvino_notebooks/openvino_env/lib/python3.10/site-packages/optimum/intel/openvino/modeling_decoder.py:388: FutureWarning: shared_memory is deprecated and will be removed in 2024.0. Value of shared_memory is going to override share_inputs value. Please use only share_inputs explicitly.
  self.request.start_async(inputs, shared_memory=True)
# please run this cell for stopping gradio interface
demo.close()
del rag_chain
Closing server running on port: 4888