Dataikuで実施するRAG構築 3 - DeepSeek-VL2-tinyでPDFをOCR処理 -
ローカル VisonLLM
前回の記事
Windowsのwsl2上で、Dataikuアプリを使用した環境となります。
クラウド版の場合はGPUがどうなるか分からないため、実施が不可能な可能性が高いです。
下記では、Qwen2.5-VL-7B-instructでOCR処理など試してみました。
(vllmインストール箇所など)
しかし、レンタルGPU先が埋まってしまったり、自分のPCでは量子化しないとVRAM的に辛い+速度が遅いため、せっかくなのでDataikuさん環境にて、MoE構造のdeepseek-vl2を試してみます。
画像を見る感じだと、Qwen2-VLシリーズよりも性能が良さそうです。
【現状(2025/02/08)ではQwen2.5シリーズもあるため、そちらとは比較できていません】
一番小さいモデルのdeepseek-vl2-tinyでは、16Bパラメータでアクティブ活性化が1.0Bであるため、24VRAMあるRTX 3090や、RTX 4090であれば、なんとか量子化をせずに動くかと思います。
※ 簡単に説明すると、全部で16Bのパラメータあるけど、良い感じに使う部分だけを選択するから1.0Bしか動作しないから早いよ!というイメージですかね?
(16Bでsmallとは一体!?)
0 環境構築
【アドミニストレーション】→【Conde Envs】→【NEW PYTHON ENV】
まずは、使用する仮想環境を構築します。
なお、Conda環境では、インストール系で上手くいかなかったため、pipバージョンで作成しました。
pythonのバージョンは、DeepSeek-VL2のgithubを参考に、3.9以上あれば大丈夫だとは思います。
githubのrequirements.txtを参考にpipのところに貼り付けました。
git+https://github.com/deepseek-ai/DeepSeek-VL2.git
setuptools>=40.6.0
torch==2.0.1
torchvision==0.15.2
torchaudio==2.0.2
transformers==4.38.2
xformers>=0.0.21
timm>=0.9.16
accelerate
sentencepiece
attrdict
einops
PyMuPDF
※ Condaで色々試した結果
DATA_DIRのconda環境を起動させてpip listするとdeepseek_vl2が入っているが、Notebookから見ると入っていない。無理やりconda環境からwsl2上でpip installしてもNotebookのKernelでは反映されてなかったりで、諦めてcondaを使用しない事にしました。
後で気づきますが、deepseek_vl2の間違いでした。。。
1 python準備
前回の記事などと同じく、pdfフォルダを準備し、pythonコードを適用させます。
また、同じくNotebookを選択し、上記で作成したpython環境を選択します。
ハマったポイント
HuggingFaceのdeepseek-vl2-tinyのサンプルコードでは、
from deepseek_vl.models import DeepseekVLV2Processor, DeepseekVLV2ForCausalLM
from deepseek_vl.utils.io import load_pil_images
との記載がありますが、実際は
from deepseek_vl2.models import DeepseekVLV2Processor, DeepseekVLV2ForCausalLM
from deepseek_vl2.utils.io import load_pil_images
と、vl2を使用します。
ここに気づかず、Dataikuの環境構築で失敗していると思い、数時間溶かしてしまいました。。。
※ HuggingFaceにアップロードされるコードやライセンスなどは、わりかし適当だったり、後で変更なども多いため、githubの方も見るのが良いでしょう。
2 pythonコード
前回の記事と似たり寄ったりなので、deepseekコード部分のみ記載します。
全コードは次の3をご覧ください。
2.1 deepseekライブラリーインポート
import dataiku
import pandas as pd, numpy as np
from dataiku import pandasutils as pdu
import os
import re
import glob
import pymupdf
from PIL import Image
from io import BytesIO
import json
import torch
from transformers import AutoModelForCausalLM
from deepseek_vl2.models import DeepseekVLV2Processor, DeepseekVLV2ForCausalLM
from deepseek_vl2.utils.io import load_pil_images
結構な量の警告が出ますが、気にしないで進めました。
MODEL_NAME = "deepseek-ai/deepseek-vl2-tiny" # DeepSeek VL2 モデル tinyを選択
# DeepSeek VL2 の初期化
vl_chat_processor: DeepseekVLV2Processor = DeepseekVLV2Processor.from_pretrained(MODEL_NAME)
tokenizer = vl_chat_processor.tokenizer
vl_gpt: DeepseekVLV2ForCausalLM = AutoModelForCausalLM.from_pretrained(MODEL_NAME, trust_remote_code=True)
vl_gpt = vl_gpt.to(torch.bfloat16).cuda().eval()
モデルを読み込みます、tinyでも、そこそこ時間がかかります。
2.2 deepseek処理の部分
# ファイル1と2のそれぞれ先頭2ページだけを処理するためのループ
all_data = []
global_index = 1 # グローバルインデックス
for file_index, pdf_file in enumerate(pdf_files[:2]): # 最初の2つのファイルのみ処理
doc = pymupdf.open(pdf_file)
for page_index in range(min(2, len(doc))): # 各ファイルの先頭2ページのみ処理
page = doc[page_index]
# 画像を取得する
pix = page.get_pixmap()
# BytesIOオブジェクトを作成し、Pixmapデータを書き込む
image_data = BytesIO(pix.tobytes())
# PIL Imageオブジェクトとして読み込む
image = Image.open(image_data)
try:
# DeepSeek VL2 での推論
conversation = [
{
"role": "<|User|>",
"content": "<image>\nOCR処理し、markdown形式で返してください。",
"images": [image], # PIL image を直接渡す
},
{"role": "<|Assistant|>", "content": ""},
]
# load images and prepare for inputs
pil_images = [image] # PIL image をリストに格納
prepare_inputs = vl_chat_processor(
conversations=conversation,
images=pil_images,
force_batchify=True,
system_prompt=""
).to(vl_gpt.device)
# run image encoder to get the image embeddings
inputs_embeds = vl_gpt.prepare_inputs_embeds(**prepare_inputs)
# run the model to get the response
outputs = vl_gpt.language.generate(
inputs_embeds=inputs_embeds,
attention_mask=prepare_inputs.attention_mask,
pad_token_id=tokenizer.eos_token_id,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
max_new_tokens=512,
do_sample=False,
use_cache=True
)
answer = tokenizer.decode(outputs[0].cpu().tolist(), skip_special_tokens=False)
response_text = answer.replace("<|endoftext|>", "").strip() # EOSトークンを除去
metadata = {"file": file_index + 1, "page": page_index + 1} # file_indexとpage_indexを使用
all_data.append({
'index': global_index, # グローバルインデックスを使用
# 返ってきたテキスト
'text': response_text,
'metadata': json.dumps(metadata), #metadata列を追加 json形式
# 使用したモデル
'model_version': MODEL_NAME, # DeepSeekのモデルパス
})
global_index += 1 # グローバルインデックスを増加
except Exception as e:
# print(f"Error processing file {file_index+1}, page {page_index+1}: {e}")
metadata = {"file": file_index + 1, "page": page_index + 1}
all_data.append({
'index': global_index,
'text': "error",
'metadata': json.dumps(metadata),
'model_version': MODEL_NAME,
})
global_index += 1
continue # エラーが発生した場合でも次のページに進む
githubのコードと、前回のコードを参考に、ほとんどgeminiに考えてもらいました。
2.3 VRAM使用量
モデルが6.74GB程度であるため、使用量が7GVRAM程度で済んでいます。
さすがはtinyですね。
※ 他のプログラムも使用しているため、もっと小さいのかも?
3 全コード
# -------------------------------------------------------------------------------- NOTEBOOK-CELL: CODE
import dataiku
import pandas as pd, numpy as np
from dataiku import pandasutils as pdu
import os
import re
import glob
import pymupdf
from PIL import Image
from io import BytesIO
import json
import torch
from transformers import AutoModelForCausalLM
from deepseek_vl2.models import DeepseekVLV2Processor, DeepseekVLV2ForCausalLM
from deepseek_vl2.utils.io import load_pil_images
# -------------------------------------------------------------------------------- NOTEBOOK-CELL: CODE
# Read recipe inputs
main_pdf_datasets = dataiku.Folder("LkS11ZMy") # 入力データフォルダのIDを選択
main_pdf_datasets_info = main_pdf_datasets.get_info()
# -------------------------------------------------------------------------------- NOTEBOOK-CELL: CODE
def sort_paths(paths):
"""リスト内のファイルパスを、pdfファイル名に含まれる数値に基づいてソートします。"""
return sorted(paths, key=lambda path: int(re.search(r'(\d+)\.pdf$', path).group(1)))
# -------------------------------------------------------------------------------- NOTEBOOK-CELL: CODE
# データセットのパスを取得
path = main_pdf_datasets.get_path()
# PDFファイル名のリストをglobで取得し、1~という順番に並び替える
pdf_files = glob.glob(os.path.join(path, "*.pdf"))
# ファイルを順番に並べ変える
pdf_files = sort_paths(pdf_files)
# -------------------------------------------------------------------------------- NOTEBOOK-CELL: CODE
MODEL_NAME = "deepseek-ai/deepseek-vl2-tiny" # DeepSeek VL2 モデル tinyを選択
# DeepSeek VL2 の初期化
vl_chat_processor: DeepseekVLV2Processor = DeepseekVLV2Processor.from_pretrained(MODEL_NAME)
tokenizer = vl_chat_processor.tokenizer
vl_gpt: DeepseekVLV2ForCausalLM = AutoModelForCausalLM.from_pretrained(MODEL_NAME, trust_remote_code=True)
vl_gpt = vl_gpt.to(torch.bfloat16).cuda().eval()
# -------------------------------------------------------------------------------- NOTEBOOK-CELL: CODE
# ファイル1と2のそれぞれ先頭2ページだけを処理するためのループ
all_data = []
global_index = 1 # グローバルインデックス
for file_index, pdf_file in enumerate(pdf_files[:2]): # 最初の2つのファイルのみ処理
doc = pymupdf.open(pdf_file)
for page_index in range(min(2, len(doc))): # 各ファイルの先頭2ページのみ処理
page = doc[page_index]
# 画像を取得する
pix = page.get_pixmap()
# BytesIOオブジェクトを作成し、Pixmapデータを書き込む
image_data = BytesIO(pix.tobytes())
# PIL Imageオブジェクトとして読み込む
image = Image.open(image_data)
try:
# DeepSeek VL2 での推論
conversation = [
{
"role": "<|User|>",
"content": "<image>\nOCR処理し、markdown形式で返してください。",
"images": [image], # PIL image を直接渡す
},
{"role": "<|Assistant|>", "content": ""},
]
# load images and prepare for inputs
pil_images = [image] # PIL image をリストに格納
prepare_inputs = vl_chat_processor(
conversations=conversation,
images=pil_images,
force_batchify=True,
system_prompt=""
).to(vl_gpt.device)
# run image encoder to get the image embeddings
inputs_embeds = vl_gpt.prepare_inputs_embeds(**prepare_inputs)
# run the model to get the response
outputs = vl_gpt.language.generate(
inputs_embeds=inputs_embeds,
attention_mask=prepare_inputs.attention_mask,
pad_token_id=tokenizer.eos_token_id,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
max_new_tokens=512,
do_sample=False,
use_cache=True
)
answer = tokenizer.decode(outputs[0].cpu().tolist(), skip_special_tokens=False)
response_text = answer.replace("<|endoftext|>", "").strip() # EOSトークンを除去
metadata = {"file": file_index + 1, "page": page_index + 1} # file_indexとpage_indexを使用
all_data.append({
'index': global_index, # グローバルインデックスを使用
# 返ってきたテキスト
'text': response_text,
'metadata': json.dumps(metadata), #metadata列を追加 json形式
# 使用したモデル
'model_version': MODEL_NAME, # DeepSeekのモデルパス
})
global_index += 1 # グローバルインデックスを増加
except Exception as e:
# print(f"Error processing file {file_index+1}, page {page_index+1}: {e}")
metadata = {"file": file_index + 1, "page": page_index + 1}
all_data.append({
'index': global_index,
'text': "error",
'metadata': json.dumps(metadata),
'model_version': MODEL_NAME,
})
global_index += 1
continue # エラーが発生した場合でも次のページに進む
# -------------------------------------------------------------------------------- NOTEBOOK-CELL: CODE
# DataFrameの作成
df = pd.DataFrame(all_data, index=[d['index'] for d in all_data])
# -------------------------------------------------------------------------------- NOTEBOOK-CELL: CODE
# Write recipe outputs
main_pdf_deepseek_ocr = dataiku.Dataset("main_pdf_deepseek_ocr")
main_pdf_deepseek_ocr.write_with_schema(df)
Dataiku上で実施した全コードです。
コードを反映させて、実行する場合は、Advancedから作成したpython環境を忘れずに適用させましょう。ライブラリーがインストールされてない!と、焦ってしまいます。
4 結果
ぱっと見は良さそうに見えましたが、tinyモデルでは中国語が大量にあったり、繰り返しがあったりで、雑なプロンプトではOCR処理としては厳しそうな感じでした。。。
通常使用するのであれば、smallモデル以上の物を使う方が良さそうです。
※ プロンプトや設定がgithubそのままであるため、そのせいかも?
5 番外
※ ダウンロードしたモデルの保存先
初期設定ではHFのモデルは$HF_HOME先にダウンロードされます。
WindowsのWSLであれば、エクスプローラーからは
\wsl.localhost\Ubuntu-22.04
というような、形で表示する事で、wsl内のファイルにアクセスする事ができます。
私の場合、Cドライブ容量圧迫を回避するため、別ドライブ(Bドライブ)に格納しています。
export HF_HOME = /mnt/b/hf
モデルが不要になったばあい、こちらから削除してください。
Discussion