MiniCPM-o

对标题的评论会显示在这里

MiniCPM-o 2.6 FastApi部署调用

对这一段的评论会显示在这里

环境配置

对这一段的评论会显示在这里

基础环境如下:

对这一段的评论会显示在这里
----------------
ubuntu 22.04
Python 3.12.3
cuda 12.1
pytorch 2.3.0
----------------
对这一段的评论会显示在这里

打开终端或新建 Jupyter.ipynb 文件,换源加速及安装魔搭依赖

对这一段的评论会显示在这里
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

pip install modelscope==1.20.0
对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

官方环境打包文件传送门:https://github.com/OpenBMB/MiniCPM-o/blob/main/requirements_o2.6.txt

对这一段的评论会显示在这里

下载到所属路径下,一键pip安装

对这一段的评论会显示在这里
pip install -r requirements_o2.6.txt
对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

或手动pip安装

对这一段的评论会显示在这里
pip install Pillow==10.1.0 torch==2.3.1 torchaudio==2.3.1 torchvision==0.18.1 transformers==4.44.2 sentencepiece==0.2.0 vector-quantize-pytorch==1.18.5 vocos==0.1.0 accelerate==1.2.1 timm==0.9.10 soundfile==0.12.1 librosa==0.9.0 decord moviepy fastapi uvicorn python-multipart
对这一段的评论会显示在这里

检查是否有漏装或者报错

对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

考虑到部分同学配置环境可能会遇到一些问题,我们在 AutoDL 平台准备了 InternLM3-8b-Instruct 的环境镜像,点击下方链接并直接创建 AutoDL 示例即可。 https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-minicpm-o

对这一段的评论会显示在这里

模型下载

对这一段的评论会显示在这里

下载 MiniCPM-o 2.6模型文件

对这一段的评论会显示在这里

使用 modelscope 中的 snapshot_download 函数下载模型,第一个参数为模型名称,参数 cache_dir 为模型的下载路径。

对这一段的评论会显示在这里
from modelscope import snapshot_download
# cache_dir记得修改为自己的目录路径
model_dir = snapshot_download('OpenBMB/MiniCPM-o-2_6', cache_dir='/root/autodl-tmp', revision='master')
对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

魔搭社区传送门:MiniCPM-o-2_6 · 模型库

对这一段的评论会显示在这里

API Server 部署

对这一段的评论会显示在这里

新建 api_server.py 文件并粘贴以下代码,均已详细注释,检查是否有将主程序入口的 modeltokenizer 中的模型路径替换为自己刚下载的模型文件具体路径:

对这一段的评论会显示在这里
# 导入必要的库
from fastapi import FastAPI, Request  # FastAPI框架相关
from transformers import AutoModel, AutoTokenizer  # Hugging Face transformers模型相关
from PIL import Image  # 图像处理
from decord import VideoReader, cpu  # 视频处理
import uvicorn  # ASGI服务器
import json  # JSON数据处理
import datetime  # 时间处理
import torch  # PyTorch深度学习框架
import base64  # Base64编解码
import io  # IO操作
import os  # 操作系统接口

# 设置GPU相关参数
DEVICE = "cuda"  # 使用CUDA设备
DEVICE_ID = "0"  # CUDA设备ID
CUDA_DEVICE = f"{DEVICE}:{DEVICE_ID}" if DEVICE_ID else DEVICE  # 组合CUDA设备信息
MAX_NUM_FRAMES = 64  # 视频处理的最大帧数,如果显存不足可以设置更小的值

def torch_gc():
    """清理GPU内存的函数"""
    if torch.cuda.is_available():
        with torch.cuda.device(CUDA_DEVICE):
            torch.cuda.empty_cache()  # 清空CUDA缓存
            torch.cuda.ipc_collect()  # 收集CUDA IPC内存

def decode_base64_to_image(base64_string):
    """将base64字符串转换为PIL Image对象
    Args:
        base64_string: base64编码的图片字符串
    Returns:
        PIL.Image: 解码后的图片对象,如果解码失败返回None
    """
    try:
        # 移除可能的data URI前缀(如 "data:image/jpeg;base64,")
        if ',' in base64_string:
            base64_string = base64_string.split(',', 1)[1]
        
        # 解码base64并转换为图片对象
        image_data = base64.b64decode(base64_string)
        image = Image.open(io.BytesIO(image_data))
        return image.convert('RGB')  # 转换为RGB格式
    except Exception as e:
        print(f"图片解码错误: {str(e)}")
        return None

def encode_video(video_path):
    """处理视频文件,提取帧
    Args:
        video_path: 视频文件路径
    Returns:
        list: 提取的视频帧列表,每一帧为PIL Image对象
    """
    def uniform_sample(lst, n):
        """均匀采样函数
        Args:
            lst: 要采样的列表
            n: 需要的样本数
        Returns:
            list: 采样后的列表
        """
        gap = len(lst) / n
        idxs = [int(i * gap + gap / 2) for i in range(n)]
        return [lst[i] for i in idxs]

    # 使用decord读取视频
    vr = VideoReader(video_path, ctx=cpu(0))
    sample_fps = round(vr.get_avg_fps() / 1)  # 获取采样帧率
    frame_idx = [i for i in range(0, len(vr), sample_fps)]  # 生成帧索引
    
    # 如果帧数超过最大限制,进行均匀采样
    if len(frame_idx) > MAX_NUM_FRAMES:
        frame_idx = uniform_sample(frame_idx, MAX_NUM_FRAMES)
    
    # 获取视频帧并转换为PIL Image格式
    frames = vr.get_batch(frame_idx).asnumpy()
    frames = [Image.fromarray(v.astype('uint8')) for v in frames]
    print('num frames:', len(frames))
    return frames

# 创建FastAPI应用
app = FastAPI()

@app.post("/")
async def create_item(request: Request):
    """处理POST请求的端点
    Args:
        request: FastAPI请求对象
    Returns:
        dict: 包含模型响应的JSON对象
    """
    global model, tokenizer
    try:
        # 解析请求数据
        json_post_raw = await request.json()
        json_post = json.dumps(json_post_raw)
        json_post_list = json.loads(json_post)
        
        # 获取请求参数
        prompt = json_post_list.get('prompt')  # 提示文本
        video_path = json_post_list.get('video_path')  # 视频路径
        history = json_post_list.get('history', [])  # 对话历史
        
        if video_path:
            # 处理视频输入
            frames = encode_video(video_path)
            msgs = [{'role': 'user', 'content': frames + [prompt]}]
        else:
            # 处理图片或其他输入
            current_msgs = []
            for msg in history:
                if msg['role'] == 'user' and isinstance(msg['content'], list):
                    new_content = []
                    for item in msg['content']:
                        # 处理base64编码的图片
                        if isinstance(item, str) and (item.startswith('data:image') or ';base64,' in item):
                            image = decode_base64_to_image(item)
                            if image:
                                new_content.append(image)
                        else:
                            new_content.append(item)
                    msg['content'] = new_content
                current_msgs.append(msg)
            msgs = current_msgs
            
        # 设置模型参数
        params = {
            "use_image_id": False,
            "max_slice_nums": 2  # 如果显存不足且视频分辨率>448*448时使用1
        }
        
        # 调用模型生成回答
        responds = model.chat(
            msgs=msgs,
            tokenizer=tokenizer,
            **params
        )
        
        # 生成响应
        now = datetime.datetime.now()
        time = now.strftime("%Y-%m-%d %H:%M:%S")
        
        answer = {
            "response": responds,
            "status": 200,
            "time": time
        }
        
        # 记录日志
        log = "[" + time + "] " + '", prompt:"' + prompt + '", response:"' + repr(responds) + '"'
        print(log)
        
        # 清理GPU内存
        torch_gc()
        return answer
        
    except Exception as e:
        print(f"Error: {str(e)}")
        return {"error": str(e), "status": 500}

# 主程序入口
if __name__ == '__main__':
    # 设置随机种子
    torch.manual_seed(1000)
    
    # 加载模型和分词器
    model = AutoModel.from_pretrained(
        'autodl-tmp/OpenBMB/MiniCPM-o-2_6',  # 替换为自己的具体路径
        trust_remote_code=True,
        attn_implementation='sdpa', 
        torch_dtype=torch.bfloat16
    )
    model = model.eval().cuda()  # 将模型设置为评估模式并移到GPU
    
    tokenizer = AutoTokenizer.from_pretrained(
        'autodl-tmp/OpenBMB/MiniCPM-o-2_6',  # 替换为自己的具体路径
        trust_remote_code=True
    )
    
    # 启动FastAPI服务器
    uvicorn.run(app, host='0.0.0.0', port=6006, workers=1)
对这一段的评论会显示在这里

在终端输入以下命令启动fastapi服务

对这一段的评论会显示在这里
# cd到自己存放代码的文件的路径,如 cd /root/autodl-tmp
python api_server.py
对这一段的评论会显示在这里

出现如下信息说明启动成功,默认部署在6006 端口

对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

单张图片输入-单轮对话

对这一段的评论会显示在这里

新建 1image_1conv.py 文件并粘贴以下代码,注意在注释位置修改自己具体输入图片的存放路径:

对这一段的评论会显示在这里
import requests
import base64, os

def send_request(data):
    """发送请求并处理响应"""
    response = requests.post("http://localhost:6006", json=data)
    try:
        result = response.json()
        if result.get('status') == 200:
            print(f"MiniCPM-o: {result.get('response')}")
        else:
            print(f"错误: {result.get('error')}")
    except Exception as e:
        print(f"解析响应时出错: {str(e)}")
        print(f"原始响应: {response.text}")

print("\n=== 测试单张图片分析 ===")
with open("autodl-tmp/input/1-01.jpg", 'rb') as f: # 注意修改为自己输入图片的存放路径
    image_base64 = f"data:image/jpeg;base64,{base64.b64encode(f.read()).decode('utf-8')}"

send_request({
    "prompt": "这张图片里有什么?",
    "history": [
        {
            'role': 'user', 
            'content': [image_base64, "这张图片里有什么?"]
        }
    ]
})
对这一段的评论会显示在这里

样例图片如下图:

对这一段的评论会显示在这里
image.jpg
image.jpg
对这一段的评论会显示在这里

运行命令进行单张图片单轮对话:

对这一段的评论会显示在这里
# cd到自己存放代码的文件的路径,如 cd /root/autodl-tmp
python 1image_1conv.py
对这一段的评论会显示在这里

调用结果如下所示:

对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

单张照片输入-多次对话

对这一段的评论会显示在这里

新建 1image_mconv.py 文件并粘贴以下代码,同样注意修改为自己具体输入图片的存放路径:

对这一段的评论会显示在这里
import requests
import base64

with open("autodl-tmp/input/1-01.jpg", 'rb') as f: # 在这里修改为自己图片的具体路径
    image_base64 = f"data:image/jpeg;base64,{base64.b64encode(f.read()).decode('utf-8')}"

question = "这张图片里有什么?"
msgs = [{'role': 'user', 'content': [image_base64, question]}]

response = requests.post(
    "http://localhost:6006",
    json={
        "prompt": question, 
        "history": msgs
    }
)

if response.status_code == 200:
    first_response = response.json().get('response')
    print(first_response)
    msgs.append({'role': 'assistant', 'content': [first_response]})

    
    question = "你觉得它下一步会做什么"
    msgs.append({'role': 'user', 'content': [question]})
    
    response = requests.post(
        "http://localhost:6006",
        json={
            "prompt": question,
            "history": msgs
        }
    )
    
    if response.status_code == 200:
        print(response.json().get('response')) 
对这一段的评论会显示在这里

样例图片同上图的猫猫:

对这一段的评论会显示在这里
image.jpg
image.jpg
对这一段的评论会显示在这里

运行命令进行单张图片多轮对话:

对这一段的评论会显示在这里
python 1image_mconv.py
对这一段的评论会显示在这里

调用结果如下所示:

对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

多张图片输入-对话

对这一段的评论会显示在这里

新建 mimage.py 文件并粘贴以下代码,同样注意修改为自己具体输入图片的存放路径:

对这一段的评论会显示在这里
import requests
import base64, os

def send_request(data):
    """发送请求并处理响应"""
    response = requests.post("http://localhost:6006", json=data)
    try:
        result = response.json()
        if result.get('status') == 200:
            print(f"MiniCPM-o: {result.get('response')}")
        else:
            print(f"错误: {result.get('error')}")
    except Exception as e:
        print(f"解析响应时出错: {str(e)}")
        print(f"原始响应: {response.text}")
        
# 多张图片比较
print("\n=== 测试多张图片比较 ===")
with open("autodl-tmp/input/1-01.jpg", 'rb') as f: # 在这里修改
    image1_base64 = f"data:image/jpeg;base64,{base64.b64encode(f.read()).decode('utf-8')}"
with open("autodl-tmp/input/1-02.jpg", 'rb') as f: # 在这里修改
    image2_base64 = f"data:image/jpeg;base64,{base64.b64encode(f.read()).decode('utf-8')}"

send_request({
    "prompt": "比较这两张图片的区别",
    "history": [
        {
            'role': 'user', 
            'content': [
                image1_base64,
                image2_base64,
                "比较这两张图片的区别"
            ]
        }
    ]
})
对这一段的评论会显示在这里

样例图片同上的猫猫和新的勾勾:

对这一段的评论会显示在这里
image.jpg
image.jpg
对这一段的评论会显示在这里

运行命令进行单张图片多轮对话:

对这一段的评论会显示在这里
python mimage.py
对这一段的评论会显示在这里

调用结果如下所示:

对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

视频输入-对话

对这一段的评论会显示在这里

新建 1video.py 文件并粘贴以下代码,同样注意修改为自己具体输入图片的存放路径:

对这一段的评论会显示在这里
import requests
import base64, os

def send_request(data):
    """发送请求并处理响应"""
    response = requests.post("http://localhost:6006", json=data)
    try:
        result = response.json()
        if result.get('status') == 200:
            print(f"MiniCPM-o: {result.get('response')}")
        else:
            print(f"错误: {result.get('error')}")
    except Exception as e:
        print(f"解析响应时出错: {str(e)}")
        print(f"原始响应: {response.text}")

print("\n=== 测试视频分析 ===")
video_path = os.path.join(os.path.expanduser("~"), "autodl-tmp", "input", "1-04.mp4") # 在这里修改
if os.path.exists(video_path):
    send_request({
        "prompt": "描述这个视频的内容",
        "video_path": video_path
    })
else:
    print(f"错误: 找不到视频文件 {video_path}") 
对这一段的评论会显示在这里

样例视频:

对这一段的评论会显示在这里
对这一段的评论会显示在这里

运行命令进行单张图片多轮对话:

对这一段的评论会显示在这里
python 1video.py
对这一段的评论会显示在这里

调用结果如下所示:

对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

InternLM3-8B-Instruct WebDemo 部署

对这一段的评论会显示在这里

环境配置

对这一段的评论会显示在这里

基础环境如下:

对这一段的评论会显示在这里
----------------
ubuntu 22.04
Python 3.12.3
cuda 12.1
pytorch 2.3.0
----------------
对这一段的评论会显示在这里

打开终端或新建 Jupyter.ipynb 文件,换源加速及安装魔搭依赖

对这一段的评论会显示在这里
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

pip install modelscope==1.20.0
对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

官方环境打包文件传送门:https://github.com/OpenBMB/MiniCPM-o/blob/main/requirements_o2.6.txt

对这一段的评论会显示在这里

下载到所属路径下,一键pip安装

对这一段的评论会显示在这里
pip install -r requirements_o2.6.txt
对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

或手动pip安装

对这一段的评论会显示在这里
pip install Pillow==10.1.0 torch==2.3.1 torchaudio==2.3.1 torchvision==0.18.1 transformers==4.44.2 sentencepiece==0.2.0 vector-quantize-pytorch==1.18.5 vocos==0.1.0 accelerate==1.2.1 timm==0.9.10 soundfile==0.12.1 librosa==0.9.0 decord moviepy fastapi uvicorn python-multipart
对这一段的评论会显示在这里

检查是否有漏装或者报错

对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

考虑到部分同学配置环境可能会遇到一些问题,我们在 AutoDL 平台准备了 InternLM3-8b-Instruct 的环境镜像,点击下方链接并直接创建 AutoDL 示例即可。 https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-minicpm-o

对这一段的评论会显示在这里

模型下载

对这一段的评论会显示在这里

下载 MiniCPM-o 2.6模型文件

对这一段的评论会显示在这里

使用 modelscope 中的 snapshot_download 函数下载模型,第一个参数为模型名称,参数 cache_dir 为模型的下载路径。

对这一段的评论会显示在这里
from modelscope import snapshot_download
# cache_dir记得修改为自己的目录路径
model_dir = snapshot_download('OpenBMB/MiniCPM-o-2_6', cache_dir='/root/autodl-tmp', revision='master')
对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

魔搭社区传送门:MiniCPM-o-2_6 · 模型库

对这一段的评论会显示在这里

在终端运行 python /root/autodl-tmp/model_download.py 执行下载,模型大小为 18GB 左右,下载模型大概需要5-30分钟。

对这一段的评论会显示在这里

代码准备

对这一段的评论会显示在这里

在本文件下, 提供了02minicpm-o-2.6WebDemo_streamlit.py 文件,请大家参考, 有比较详细的注释,下面的代码供大家简单了解代码的逻辑结构,可以参考完整代码内容和注释来理解使用和修改,如有不理解的地方欢迎提出 issue 。

对这一段的评论会显示在这里
# ... 导入必要的库和常量定义 ...

# 加载模型和分词器(使用缓存以提高性能)
@st.cache_resource
def load_model_and_tokenizer():
    print(f"load_model_and_tokenizer from {model_path}")
    model = (AutoModel.from_pretrained(model_path, 
                                       trust_remote_code=True, 
                                       attn_implementation='sdpa').
             to(dtype=torch.bfloat16))
    tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
    return model, tokenizer

# 初始化模型和会话状态
if 'model' not in st.session_state:
    st.session_state.model, st.session_state.tokenizer = load_model_and_tokenizer()
    st.session_state.model.eval().cuda()

# 初始化聊天历史
if 'chat_history' not in st.session_state:
    # ... 初始化聊天历史和媒体追踪 ...

# 侧边栏配置
with st.sidebar:
    # ... 侧边栏配置项 ...

# 主界面配置
st.title("💬 MiniCPM-V-2_6 聊天机器人")

# 模式选择
selected_mode = st.sidebar.selectbox("选择模式", ["文本", "单图片", "多图片", "视频"])

# 不同模式的处理逻辑
if selected_mode == "单图片":
    # ... 单图片模式处理 ...

elif selected_mode == "多图片":
    # ... 多图片模式处理 ...

elif selected_mode == "视频":
    # ... 视频模式处理 ...

# 聊天输入处理
user_text = st.chat_input("请输入您的问题")
if user_text is not None:
    # 显示用户消息
    with st.chat_message(U_NAME, avatar="user"):
        # ... 显示用户消息 ...

    # 模型响应生成
    with st.chat_message(A_NAME, avatar="assistant"):
        # 根据不同模式处理输入
        if selected_mode == "单图片":
            # ... 单图片处理 ...
        elif selected_mode == "多图片":
            # ... 多图片处理 ...
        elif selected_mode == "视频":
            # ... 视频处理 ...

        # 生成模型响应
        with st.spinner('AI正在思考...'):
            response = model.chat(image=None, msgs=msgs, context=None, tokenizer=tokenizer, **params)
            # ... 处理响应 ...
对这一段的评论会显示在这里

运行 demo

对这一段的评论会显示在这里

在终端中运行以下命令,启动 streamlit 服务,server.port 可以更换端口

对这一段的评论会显示在这里
streamlit run 02minicpm-o-2.6WebDemo_streamlit.py --server.address 127.0.0.1 --server.port 6006
对这一段的评论会显示在这里

运行成功后可以在terminal中看到如下信息,

对这一段的评论会显示在这里
image.png
image.png
对这一段的评论会显示在这里

在本地浏览器(推荐使用 Chrome 浏览器)中打开链接 http://localhost:6006/ ,即可查看部署的 WebDemo 聊天界面。运行效果如下:

对这一段的评论会显示在这里
03-1
03-1
对这一段的评论会显示在这里

左侧为侧边栏,可以设置不同的参数和切换不同的模式(文本、单图片、多图片、视频),右侧为聊天界面,可以输入问题,点击发送后,模型会根据输入的问题和上传的图片或视频,生成回答。

对这一段的评论会显示在这里

生成参数说明

对这一段的评论会显示在这里

这里简单说下几个可调参数的含义:

对这一段的评论会显示在这里

temperature(温度系数)

对这一段的评论会显示在这里

范围:0.0-1.0
控制采样随机性:值越大,生成越随机;值越小,生成越确定
建议值:0.7-0.9

对这一段的评论会显示在这里

top_p(核采样)

对这一段的评论会显示在这里

范围:0.0-1.0
只保留累积概率超过top_p的词来采样
截断式控制,动态概率阈值

对这一段的评论会显示在这里

top_k(前k采样)

对这一段的评论会显示在这里

范围:正整数
只从概率最高的k个词中采样
固定数量截断

对这一段的评论会显示在这里

max_new_tokens(生成长度)

对这一段的评论会显示在这里

控制新生成的token数量上限
对话建议:1024-2048
长文建议:2048-4096

对这一段的评论会显示在这里

最佳实践

对这一段的评论会显示在这里

创意写作:temperature=0.8, top_p=0.9 (或 top_k=50)
事实回答:temperature=0.3, top_p=0.1 (或 top_k=10)
代码生成:temperature=0.2, top_p=0.9 (或 top_k=20)
建议
top_k与top_p选择其一使用
top_k建议范围:10-50,值越小生成越保守

对这一段的评论会显示在这里

接下来我将演示下如何使用这个WebDemo,分为文本、单图片、多图片和视频四种模式。

对这一段的评论会显示在这里

文本模式

对这一段的评论会显示在这里

这里采用了自问自答的方式,大家可以自行尝试

对这一段的评论会显示在这里
03-1
03-1
对这一段的评论会显示在这里

单图片模式

对这一段的评论会显示在这里

单图片模式的测试采用了一个 MS-CAM 的结构图, 询问了图片内容和改进建议,效果如下

对这一段的评论会显示在这里
03-1
03-1
对这一段的评论会显示在这里
03-1
03-1
对这一段的评论会显示在这里

多图片模式

对这一段的评论会显示在这里

这里上传了来自mmyolo中的 yolov5 和 yolov8 视觉检测模型的结构图, 同时展示如下

对这一段的评论会显示在这里
03-1
03-1
对这一段的评论会显示在这里

询问两者的异同,测试结果如下

对这一段的评论会显示在这里
03-1
03-1
对这一段的评论会显示在这里

视频模式

对这一段的评论会显示在这里

这里采用的是一个网上的视频, 大家可以测试手头的视频, 建议大家测试的视频不要太大, 否则可能会上传或者解码失败(推荐20秒以内, 10M以内的视频)。

对这一段的评论会显示在这里
03-1
03-1
对这一段的评论会显示在这里

大家也可以尝试下项目里提供的测试视频 01-11.mp4,效果如下

对这一段的评论会显示在这里
03-1
03-1
对这一段的评论会显示在这里

MiniCPM-o-2.6 多模态语音能力

对这一段的评论会显示在这里

环境准备

对这一段的评论会显示在这里

本文基础环境如下:

对这一段的评论会显示在这里
----------------
ubuntu 22.04
python 3.12
cuda 12.1
pytorch 2.5.1
----------------
对这一段的评论会显示在这里

本文默认学习者已配置好以上 Pytorch (cuda) 环境,如未配置请先自行安装。

对这一段的评论会显示在这里

首先 pip 换源加速下载并安装依赖包

对这一段的评论会显示在这里
python -m pip install --upgrade pip
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

pip install modelscope==1.20.0
pip install transformers==4.44.2
pip install torch==2.5.1 torchvision==0.20.1 --index-url https://download.pytorch.org/whl/cu124
pip install accelerate==1.2.1 timm==0.9.10 soundfile==0.12.1 librosa==0.9.0 vector-quantize-pytorch==1.18.5 vocos==0.1.0
对这一段的评论会显示在这里

考虑到部分同学配置环境可能会遇到一些问题,我们在 AutoDL 平台准备了 InternLM3-8b-Instruct 的环境镜像,点击下方链接并直接创建 AutoDL 示例即可。 https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-minicpm-o

对这一段的评论会显示在这里

模型下载

对这一段的评论会显示在这里

使用 modelscope 中的 snapshot_download 函数下载模型,第一个参数为模型名称,参数 cache_dir为模型的下载路径。

对这一段的评论会显示在这里

先切换到 autodl-tmp 目录,cd /root/autodl-tmp

对这一段的评论会显示在这里

然后新建名为 model_download.pypython 脚本,并在其中输入以下内容并保存

对这一段的评论会显示在这里
from modelscope import snapshot_download

snapshot_download('OpenBMB/MiniCPM-o-2_6', local_dir='/root/autodl-tmp/MiniCPM-o-2_6')
对这一段的评论会显示在这里

然后在终端中输入 python model_download.py 执行下载,这里需要耐心等待一段时间直到模型下载完成。

对这一段的评论会显示在这里

注意:记得修改 local_dir 为你的模型本地下载路径哦~

对这一段的评论会显示在这里

多模态语音能力

对这一段的评论会显示在这里

MiniCPM-o-2.6具有一定的多模态语音理解能力,在新的语音模式中,MiniCPM-o 2.6支持可配置声音的中英双语语音对话,还具备情感/语速/风格控制、端到端声音克隆、角色扮演等进阶能力。所以接下来一起来探索一下MiniCPM多模态的语音能力。可以准备一个jupyter或者python脚本运行以下代码。

对这一段的评论会显示在这里

模型初始化

对这一段的评论会显示在这里

MiniCPM-o 默认加载是 omni 模型,初始化视觉,音频和TTS模块,也可以根据需求选择初始化

对这一段的评论会显示在这里
import torch
from PIL import Image
from modelscope import AutoModel, AutoTokenizer
import librosa

# 加载 omni 模型,默认会初始化视觉、音频和 TTS 模块
# 如果只加载视觉模型,设置 init_audio=False 和 init_tts=False
# 如果只加载音频模型,设置 init_vision=False
model = AutoModel.from_pretrained(
    '/root/autodl-tmp/MiniCPM-o-2_6/',
    trust_remote_code=True,
    attn_implementation='sdpa', # 使用 sdpa 或 flash_attention_2
    torch_dtype=torch.bfloat16,
    init_vision=True,
    init_audio=True,
    init_tts=True
)

model = model.eval().cuda()
tokenizer = AutoTokenizer.from_pretrained('/root/autodl-tmp/MiniCPM-o-2_6/', trust_remote_code=True)

# 除了视觉模式,TTS 处理器和 vocos 也需要初始化
model.init_tts()
对这一段的评论会显示在这里

语音模仿

对这一段的评论会显示在这里

语音模仿任务反映了模型的端到端语音建模能力。模型接收音频输入,输出 ASR 转录,然后重建原始音频,高度相似。重建的音频与原始音频之间的相似度越高,模型在端到端语音建模方面的基础能力就越强。

对这一段的评论会显示在这里
mimick_prompt = "请重复每个用户的讲话内容,包括语音风格和内容。"
audio_input, _ = librosa.load('/root/autodl-tmp/MiniCPM-o-2_6/assets/mimick.wav', sr=16000, mono=True)
msgs = [{'role': 'user', 'content': [mimick_prompt,audio_input]}]

res = model.chat(
    msgs=msgs,
    tokenizer=tokenizer,
    sampling=True,
    max_new_tokens=128,
    use_tts_template=True,
    temperature=0.3,
    generate_audio=True,
    output_audio_path='output.wav', # 将 TTS 结果保存到 output_audio_path
)
print(res)
对这一段的评论会显示在这里
OmniOutput(text='全民制作人们大家好,我是练习时长两年半的个人练习生蔡徐坤,喜欢唱跳rap、篮球music.', spk_embeds=None, audio_wav=tensor([-5.5982e-03, -4.1898e-03, -4.9984e-03,  ..., -3.4108e-05,
         2.6977e-05,  1.9374e-05]), sampling_rate=24000)
对这一段的评论会显示在这里
对这一段的评论会显示在这里

音频角色扮演/助手

对这一段的评论会显示在这里

同时还可以设置mode参数为audio_roleplay或者audio_assistant来进行音频角色扮演或者助手,一般对话推荐audio_assistant

对这一段的评论会显示在这里
ref_audio, _ = librosa.load('/root/autodl-tmp/MiniCPM-o-2_6/assets/demo.wav', sr=16000, mono=True) # 加载参考音频

# 音频角色扮演:  # 在此模式下,模型会根据音频提示进行角色扮演。 (更自然的对话,但不稳定)
# sys_prompt = model.get_sys_prompt(ref_audio=ref_audio, mode='audio_roleplay', language='en')
# user_question = {'role': 'user', 'content': [librosa.load('xxx.wav', sr=16000, mono=True)[0]]}

# 音频助手: # 在此模式下,模型会以参考音频中的语音作为 AI 助手进行对话。 (稳定,更适合一般对话)
sys_prompt = model.get_sys_prompt(ref_audio=ref_audio, mode='audio_assistant', language='zh') 
user_question = {'role': 'user', 'content': [librosa.load('/root/autodl-tmp/MiniCPM-o-2_6/assets/qa.wav', sr=16000, mono=True)[0]]}

msgs = [sys_prompt, user_question]
# 第一轮
res = model.chat(
    msgs=msgs,
    tokenizer=tokenizer,
    sampling=True,
    max_new_tokens=128,
    use_tts_template=True,
    generate_audio=True,
    temperature=0.3,
    output_audio_path='result.wav',
)

# # 第二轮
# history = msgs.append({'role': 'assistant', 'content': res})
# user_question = {'role': 'user', 'content': [librosa.load('xxx.wav', sr=16000, mono=True)[0]]}
# msgs = history.append(user_question)
# res = model.chat(
#     msgs=msgs,
#     tokenizer=tokenizer,
#     sampling=True,
#     max_new_tokens=128,
#     use_tts_template=True,
#     generate_audio=True,
#     temperature=0.3,
#     output_audio_path='result_round_2.wav',
# )
print(res)
对这一段的评论会显示在这里
OmniOutput(text='大家好,我是面壁智能小钢炮。祝大家新年快乐!', spk_embeds=None, audio_wav=tensor([-4.9051e-05, -4.5966e-05, -7.2269e-05,  ...,  1.0666e-03,
         1.5180e-03,  1.1057e-03]), sampling_rate=24000)
对这一段的评论会显示在这里
对这一段的评论会显示在这里

多种音频任务

对这一段的评论会显示在这里

除此之外,还能做以下的音频任务,包括音频理解,语音生成,语音克隆等

对这一段的评论会显示在这里

音频理解

对这一段的评论会显示在这里
# 音频理解任务Prompt
# 语音:
#     ASR with ZH(same as AST en2zh): 请仔细听这段音频片段,并将其内容逐字记录。
#     ASR with EN(same as AST zh2en): Please listen to the audio snippet carefully and transcribe the content.
#     Speaker Analysis 说话人分析: Based on the speaker's content, speculate on their gender, condition, age range, and health status.
# 一般音频:
#     音频总结: Summarize the main content of the audio. / 总结音频的主要内容。
#     声音场景标记: Utilize one keyword to convey the audio's content or the associated scene. / 使用一个关键词表达音频内容或相关场景。

task_prompt = "Summarize the main content of the audio. \n" # 选择上面的任务Prompt
audio_input, _ = librosa.load('/root/autodl-tmp/MiniCPM-o-2_6/assets/audio_understanding.mp3', sr=16000, mono=True)

msgs = [{'role': 'user', 'content': [task_prompt,audio_input]}]

res = model.chat(
    msgs=msgs,
    tokenizer=tokenizer,
    sampling=True,
    max_new_tokens=128,
    use_tts_template=True,
    generate_audio=True,
    temperature=0.3,
    output_audio_path='result.wav',
)
print(res)
对这一段的评论会显示在这里
OmniOutput(text='Birds are chirping and singing, with a distant sound of an emergency vehicle siren.', spk_embeds=None, audio_wav=tensor([-0.0009, -0.0009, -0.0008,  ..., -0.0018,  0.0007, -0.0008]), sampling_rate=24000)
对这一段的评论会显示在这里
对这一段的评论会显示在这里

语音生成

对这一段的评论会显示在这里
'''
语音生成任务 Speech Generation Task Prompt:
    Human Instruction-to-Speech: see https://voxinstruct.github.io/VoxInstruct/
    Example:
        # 在新闻中,一个年轻男性兴致勃勃地说:“祝福亲爱的祖国母亲美丽富强!”他用低音调和低音量,慢慢地说出了这句话。
        # Delighting in a surprised tone, an adult male with low pitch and low volume comments:"One even gave my little dog a biscuit" This dialogue takes place at a leisurely pace, delivering a sense of excitement and surprise in the context. 

    Voice Cloning or Voice Conversion: With this mode, model will act like a TTS model. 
'''
# Human Instruction-to-Speech:
task_prompt = '在新闻中,一个年轻男性兴致勃勃地说:“祝福亲爱的祖国母亲美丽富强!”他用低音调和低音量,慢慢地说出了这句话。' #Try to make some Human Instruction-to-Speech prompt (Voice Creation)
msgs = [{'role': 'user', 'content': [task_prompt]}] # you can also try to ask the same audio question

res = model.chat(
    msgs=msgs,
    tokenizer=tokenizer,
    sampling=True,
    max_new_tokens=128,
    use_tts_template=True,
    generate_audio=True,
    temperature=0.3,
    output_audio_path='result.wav',
)
print(res)
# IPython.display.Audio('result.wav')
对这一段的评论会显示在这里
OmniOutput(text='祝福亲爱的祖国母亲美丽富强', spk_embeds=None, audio_wav=tensor([-4.8119e-03, -3.6808e-03, -2.6445e-03,  ..., -6.3839e-05,
         6.8862e-06, -1.4487e-04]), sampling_rate=24000)
对这一段的评论会显示在这里
对这一段的评论会显示在这里

语音克隆

对这一段的评论会显示在这里
ref_audio, _ = librosa.load('/root/autodl-tmp/MiniCPM-o-2_6/assets/mimick.wav', sr=16000, mono=True) # 加载参考音频

# 声音克隆模式: 
sys_prompt = model.get_sys_prompt(ref_audio=ref_audio, mode='voice_cloning', language='zh')
text_prompt = f"Please read the text below."
user_question = {'role': 'user', 'content': [text_prompt, "全名制作人们大家好,我是小黑子,鸡你太美"]}
msgs = [sys_prompt, user_question]
res = model.chat(
    msgs=msgs,
    tokenizer=tokenizer,
    sampling=True,
    max_new_tokens=128,
    use_tts_template=True,
    generate_audio=True,
    temperature=0.3,
    output_audio_path='result.wav',
)

print(res)
对这一段的评论会显示在这里
OmniOutput(text='全名制作人们大家好,我是小黑子,鸡你太美', spk_embeds=None, audio_wav=tensor([-0.0197,  0.0294,  0.0064,  ..., -0.0006, -0.0006, -0.0005]), sampling_rate=24000)
对这一段的评论会显示在这里
对这一段的评论会显示在这里

MiniCPM-o-2.6 Lora 微调

对这一段的评论会显示在这里

MiniCPM-o-2.6是OpenBMB(面壁智能)团队最近开源的多模态大语言模型。以MiniCPM-o-2.6作为基座多模态大模型,通过指令微调的方式实现特定场景下的OCR,是学习多模态LLM微调的入门任务。

对这一段的评论会显示在这里
04-1
04-1
对这一段的评论会显示在这里

本文我们将简要介绍基于 transformers、peft 等框架,使用 MiniCPM-O-2.6 模型在LaTeX_OCR 上进行Lora微调训练,同时使用 SwanLab 监控训练过程与评估模型效果。

对这一段的评论会显示在这里

LoRA 是一种高效微调方法,深入了解其原理可参见博客:知乎|深入浅出 LoRA

对这一段的评论会显示在这里

训练过程:ZeyiLin/minicpm-o-2-6-latexcor
代码:见此文档同目录下文件夹04-MiniCPM-0-2.6 Lora微调 参考代码
数据集:LaTeX_OCR
模型:MiniCPM-o-2.6
在线LaTex公式预览网站:latexlive
显存占用:约25GB,建议租A100(40GB显存)进行微调

对这一段的评论会显示在这里

目录

对这一段的评论会显示在这里

视觉大模型是指能够支持图片/视频输入的大语言模型,能够极大丰富与LLM的交互方式。

对这一段的评论会显示在这里

对视觉大模型做微调的一个典型场景,是让它特化成一个更强大、更智能的计算机视觉模型,执行图像分类、目标检测、语义分割、OCR、图像描述任务等等。

对这一段的评论会显示在这里

并且由于视觉大模型强大的基础能力,所以训练流程变得非常统一——无论是分类、检测还是分割,只需要构建好数据对(图像 -> 文本),都可以用同一套代码完成,相比以往针对不同任务就要构建迥异的训练代码而言,视觉大模型微调要简单粗暴得多,而且效果还更好。

对这一段的评论会显示在这里

当然,硬币的另一面是要承担更高的计算开销,但在大模型逐渐轻量化的趋势下,可以预想这种训练范式将逐渐成为主流。

对这一段的评论会显示在这里

👋 SwanLab简介

对这一段的评论会显示在这里
04-2
04-2
对这一段的评论会显示在这里

SwanLab 是一个开源的模型训练记录工具,常被称为"中国版 Weights&Biases + Tensorboard"。SwanLab面向AI研究者,提供了训练可视化、自动日志记录、超参数记录、实验对比、多人协同等功能。在SwanLab上,研究者能基于直观的可视化图表发现训练问题,对比多个实验找到研究灵感,并通过在线链接的分享与基于组织的多人协同训练,打破团队沟通的壁垒。

对这一段的评论会显示在这里

为什么要记录训练?

对这一段的评论会显示在这里

相较于软件开发,模型训练更像一个实验科学。一个品质优秀的模型背后,往往是成千上万次实验。研究者需要不断尝试、记录、对比,积累经验,才能找到最佳的模型结构、超参数与数据配比。在这之中,如何高效进行记录与对比,对于研究效率的提升至关重要。

对这一段的评论会显示在这里

可视化的价值在哪里?

对这一段的评论会显示在这里

机器学习模型训练往往伴随着大量的超参数、指标、日志等数据,很多关键信息往往存在于实验的中间而非结尾,如果不对连续的指标通过图表进行可视化,往往会错失发现问题的最佳时机,甚至错过关键信息。同时不进行可视化,也难以对比多个实验之间的差异。 可视化也为AI研究者提供了良好的交流基础,研究者们可以基于图表进行沟通、分析与优化,而非以往看着枯燥的终端打印。这打破了团队沟通的壁垒,提高了整体的研发效率。

对这一段的评论会显示在这里

🌍 环境配置

对这一段的评论会显示在这里

环境配置分为三步:

对这一段的评论会显示在这里

确保你的电脑上至少有一张英伟达显卡,并已安装好了CUDA环境。
安装Python(版本>=3.8)以及能够调用CUDA加速的PyTorch
安装与MiniCPM-O-2.6微调相关的第三方库,可以使用以下命令:

对这一段的评论会显示在这里
python -m pip install --upgrade pip
# 更换 pypi 源,加速库的安装
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

pip install modelscope==1.20.1
pip install transformers==4.44.2
pip install sentencepiece==0.2.0
pip install accelerate==1.0.1
pip install datasets==2.18.0
pip install peft==0.12.0
pip install swanlab==0.4.5
pip install pandas==2.2.2
pip install vocos==0.1.0
pip install vector-quantize-pytorch==1.21.2
pip install timm==1.0.7
pip install soundfile==0.12.1
pip install numpy==1.26.4
pip install oss2
pip install addict
pip install decord
pip install moviepy
pip install librosa
对这一段的评论会显示在这里

考虑到部分同学配置环境可能会遇到一些问题,我们在 AutoDL 平台准备了 InternLM3-8b-Instruct 的环境镜像,点击下方链接并直接创建 AutoDL 示例即可。 https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-minicpm-o

对这一段的评论会显示在这里

📚 准备数据集

对这一段的评论会显示在这里

本节使用的是 LaTex_OCR 数据集,这个数据集包含了大量的数学公式图片,以及对应的LaTex语法字符串。可以看到,下图中的image就是学术公式图,text就是对应的LaTex语法字符串:

对这一段的评论会显示在这里
04-3
04-3
对这一段的评论会显示在这里

将这些LaTex语法字符串粘贴到latexlive中,可以预览对应的数学公式:

对这一段的评论会显示在这里
04-4
04-4
对这一段的评论会显示在这里

了解了数据集结构之后,我们需要做的是将这些数据整理成MiniCPM-O-2.6需要的json格式,下面是目标的格式:

对这一段的评论会显示在这里
[
  {
    "id": "identity_1",
    "image": "图片路径",
    "conversations": [
      {
        "role": "user",
        "content": "<image>\n这张图对应的LaTex公式是什么?"
      },
      {
        "role": "assistant",
        "content": "LaTex公式"
      }
    ]  
  },
...
]
对这一段的评论会显示在这里

我们来解读一下这个json:

对这一段的评论会显示在这里

id:数据对的编号
image:输入的图像文件路径
conversations:人类与LLM的对话,类型是列表
role:角色,user代表人类,assistant代表模型
content:对话发送的内容,其中user的content是图片标签``和提示词,assistant的回复是LaTex公式

对这一段的评论会显示在这里

对这一段的评论会显示在这里

接下来让我们下载数据集并进行处理:

对这一段的评论会显示在这里

我们需要做四件事情:
通过Modelscope下载LaTex_OCR数据集
加载数据集,将图像保存到本地
将图像路径和对应的LaTex公式转换为一个csv文件
将csv文件转换为json文件,并拆分为1个训练集和验证集
运行下面的代码完成从数据下载到生成csv的过程:

对这一段的评论会显示在这里
python data2csv.py
python csv2json.py
对这一段的评论会显示在这里

此时目录下会多出3个文件:

对这一段的评论会显示在这里
|———— latex_ocr_train.csv
|———— latex_ocr_train.json
|———— latex_ocr_val.json
对这一段的评论会显示在这里

至此,我们完成了数据集的准备。

对这一段的评论会显示在这里

🤖 模型下载与加载

对这一段的评论会显示在这里

这里我们使用modelscope下载MiniCPM-O-2.6模型,将其保存到本地路径:

对这一段的评论会显示在这里
from modelscope import snapshot_download

snapshot_download('OpenBMB/MiniCPM-o-2_6', local_dir='/root/autodl-tmp/MiniCPM-o-2_6')
对这一段的评论会显示在这里

注意:记得修改 local_dir 为你自己的模型本地下载路径哦~

对这一段的评论会显示在这里

🐦‍ 集成SwanLab

对这一段的评论会显示在这里

SwanLab与Transformers已经做好了集成,用法是在Trainer的callbacks参数中添加SwanLabCallback实例,就可以自动记录超参数和训练指标,简化代码如下:

对这一段的评论会显示在这里
from swanlab.integration.transformers import SwanLabCallback
from transformers import Trainer

swanlab_callback = SwanLabCallback()

trainer = Trainer(
    ...
    callbacks=[swanlab_callback],
)
对这一段的评论会显示在这里

首次使用SwanLab,需要先在官网注册一个账号,然后在用户设置页面复制你的API Key,然后在训练开始提示登录时粘贴即可,后续无需再次登录:

对这一段的评论会显示在这里
04-5
04-5
对这一段的评论会显示在这里
04-6
04-6
对这一段的评论会显示在这里

SwanLab API Key的位置:

对这一段的评论会显示在这里
04-7
04-7
对这一段的评论会显示在这里

更多用法可参考快速开始Transformers集成

对这一段的评论会显示在这里

🚀 开始微调

对这一段的评论会显示在这里

查看可视化训练过程:ZeyiLin/MiniCPM-o-ft-latexocr

对这一段的评论会显示在这里

代码在本文同目录下的04-MiniCPM-0-2.6 Lora微调 参考代码文件夹中,共有6个py文件,请将他们保存到同一目录下,想直接开始微调,请在完成 准备数据集 后,运行python train.py

对这一段的评论会显示在这里

本节代码做了以下几件事:

对这一段的评论会显示在这里

下载并加载MiniCPM-O-2.6模型
加载数据集,取前996条数据参与训练,4条数据进行主观评测
配置Lora,参数为r=64, lora_alpha=16, lora_dropout=0.05
使用SwanLab记录训练过程,包括超参数、指标和最终的模型输出结果
训练2个epoch

对这一段的评论会显示在这里

开始执行代码时的目录结构应该是:

对这一段的评论会显示在这里
|———— train.py
|———— minicpm_datasets.py
|———— trainer.py
|———— data2csv.py
|———— csv2json.py
|———— latex_ocr_train.csv
|———— latex_ocr_train.json
|———— latex_ocr_val.json
对这一段的评论会显示在这里

train.py完整代码如下

对这一段的评论会显示在这里

train.py:

对这一段的评论会显示在这里
import json
from functools import partial
from typing import Dict
from torchvision import transforms
import torch
import transformers
from transformers import AutoModel, AutoTokenizer, TrainingArguments
from minicpm_datasets import SupervisedDataset, data_collator
from trainer import CPMTrainer
from peft import LoraConfig, get_peft_model, PeftModel
from modelscope import snapshot_download
import swanlab
from swanlab.integration.transformers import SwanLabCallback
import os
from PIL import Image

def make_supervised_data_module(
    tokenizer: transformers.PreTrainedTokenizer,
    data_path,
    transform,
    data_collator=None,
    llm_type="qwen",
    slice_config=None,
    patch_size=14,
    query_nums=64,
    batch_vision=False,
    max_length=2048,
) -> Dict:
    """Make dataset and collator for supervised fine-tuning."""
    print("Loading data...")

    train_json = json.load(open(data_path, "r"))
    train_dataset = SupervisedDataset(
        train_json,
        transform,
        tokenizer,
        slice_config=slice_config,
        llm_type=llm_type,
        patch_size=patch_size,
        query_nums=query_nums,
        batch_vision=batch_vision,
        max_length=max_length,
    )

    return dict(
        train_dataset=train_dataset,
        eval_dataset=None,
        data_collator= partial(data_collator, max_length=max_length),
    )


model_id = "OpenBMB/MiniCPM-o-2_6"
data_path="./latex_ocr_train.json"
output_dir="./output/minicpm-o-2-6-latexocr"

llm_type: str = "qwen"
tune_vision: bool = True
tune_llm: bool = False
use_lora: bool = True

max_steps: int = 1000
model_max_length: int = 2048
max_slice_nums: int = 9

lora_rank: int = 64
lora_alpha: int = 16
lora_dropout: float = 0.1

# 设置Transformers训练参数
training_args = TrainingArguments(
    output_dir=output_dir,
    bf16=True,
    logging_strategy="steps",
    per_device_train_batch_size=1,
    per_device_eval_batch_size=1,
    gradient_accumulation_steps=1,
    save_strategy="steps",
    save_steps=500,
    max_steps=max_steps,
    save_total_limit=10,
    learning_rate=1e-6,
    weight_decay=0.1,
    adam_beta2=0.95,
    warmup_ratio=0.01,
    lr_scheduler_type="cosine",
    logging_steps=10,
    gradient_checkpointing=True,
    label_names="labels",
    remove_unused_columns=False,
    gradient_checkpointing_kwargs={"use_reentrant":False},
    report_to="none",
)

# 下载模型
model_dir = snapshot_download(model_id, cache_dir="/root/autodl-tmp/", revision="master")

# 加载模型
model = AutoModel.from_pretrained(
    model_dir,
    trust_remote_code=True,
    torch_dtype=torch.bfloat16,
    device_map=None,
    init_vision=True,
    init_audio=False,
    init_tts=False,
)

# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)

# 参数冻结
if not tune_vision:
    model.vpm.requires_grad_(False)
if not tune_llm:
    model.llm.requires_grad_(False)
    
# 配置Lora
if use_lora:
    # 如果同时微调llm和使用lora,则报错
    if use_lora and tune_llm:
        raise ValueError("The model cannot simultaneously adjust LLM parameters and apply LoRA.")
    
    print("Currently using LoRA for fine-tuning the MiniCPM-V model.")
    # 冻结llm参数
    for name, param in model.llm.named_parameters():
        param.requires_grad = False
    # 设置需要保存的模块
    modules_to_save = ['embed_tokens','resampler']
    if tune_vision:
        modules_to_save.append('vpm')
        
    # 设置lora配置
    lora_config = LoraConfig(
        r=lora_rank,
        lora_alpha=lora_alpha,
        target_modules="llm\..*layers\.\d+\.self_attn\.(q_proj|k_proj|v_proj|o_proj)",
        lora_dropout=lora_dropout,
    )
    # 将模型转换为peft模型
    model = get_peft_model(model, lora_config)
    # 启用输入梯度
    model.enable_input_require_grads()

    model.config.slice_config.max_slice_nums = max_slice_nums
    slice_config = model.config.slice_config.to_dict()
    batch_vision = model.config.batch_vision_input

# 设置数据集预处理
transform_func = transforms.Compose(
        [
            transforms.ToTensor(),
            transforms.Normalize(
                mean=(0.5, 0.5, 0.5) , std=(0.5, 0.5, 0.5)
            ),
        ]
    )

# 数据集模块
data_module = make_supervised_data_module(
    tokenizer=tokenizer,
    data_path=data_path,
    transform=transform_func,
    data_collator=data_collator,
    slice_config=slice_config,
    llm_type=llm_type,
    patch_size=model.config.patch_size,
    query_nums=model.config.query_num,
    batch_vision=batch_vision,
    max_length=model_max_length,
)

# 集成SwanLab训练可视化工具
swanlab_callback = SwanLabCallback(
    project="minicpm-o-2-6-latexcor",
    experiment_name="minicpm-o-2-6",
    config={
        "github_repo": "self-llm",
        "model": "https://modelscope.cn/models/OpenBMB/MiniCPM-o-2_6",
        "dataset": "https://modelscope.cn/datasets/AI-ModelScope/LaTeX_OCR/summary",
        "model_id": model_id,
        "train_dataset_json_path": data_path,
        "output_dir": "output/output__lora",
        "token_max_length": model_max_length,
        "lora_rank": lora_rank,
        "lora_alpha": lora_alpha,
        "lora_dropout": lora_dropout,
    }
)

trainer = CPMTrainer(
    model=model,
    tokenizer=tokenizer,
    args=training_args,
    use_lora=use_lora,
    callbacks=[swanlab_callback],
    **data_module,
)

trainer.train()


# ========== 主观测试 ==========

# 释放trainer中的model显存
trainer.model.cpu()
del trainer.model
if torch.cuda.is_available():
    torch.cuda.empty_cache()

# 获取测试模型,从output_dir中获取最新的checkpoint
load_model_path = f"{output_dir}/checkpoint-{max([int(d.split('-')[-1]) for d in os.listdir(output_dir) if d.startswith('checkpoint-')])}"
print(f"load_model_path: {load_model_path}")

origin_model = AutoModel.from_pretrained(model_dir, trust_remote_code=True)
val_lora_model = PeftModel.from_pretrained(
    origin_model,
    load_model_path,
    device_map="auto",
    trust_remote_code=True
).eval().cuda()

# 读取测试数据
with open("./latex_ocr_val.json", "r") as f:
    test_dataset = json.load(f)

test_image_list = []
for item in test_dataset:
    image_file_path = item["image"]
    label = item["conversations"][1]["content"]
    
    image = Image.open(image_file_path).convert('RGB')

    question = "这张图对应的LaTex公式是什么?"
    msgs = [{'role': 'user', 'content': [image, question]}]

    answer = val_lora_model.chat(
        msgs=msgs,
        tokenizer=tokenizer
    )

    print(f"predict:{answer}")
    print(f"gt:{label}\n")

    test_image_list.append(swanlab.Image(image_file_path, caption=answer))

swanlab.log({"Prediction": test_image_list})

# 在Jupyter Notebook中运行时要停止SwanLab记录,需要调用swanlab.finish()
swanlab.finish()
对这一段的评论会显示在这里

我们运行python train.py,可以看到下面的进度条即代表训练开始:

对这一段的评论会显示在这里
04-8
04-8
对这一段的评论会显示在这里

💻 训练结果演示

对这一段的评论会显示在这里

详细训练过程请看这里:ZeyiLin/MiniCPM-o-2-6-latexcor

对这一段的评论会显示在这里
04-9
04-9
对这一段的评论会显示在这里

从SwanLab图表中我们可以看到,学习率的下降策略是Cosine,loss随step逐渐下降。

对这一段的评论会显示在这里

Prediction图表中记录着模型最终的输出结果,可以看到模型在回答的风格已经是标准的LaTex语法。

对这一段的评论会显示在这里
04-10
04-10
对这一段的评论会显示在这里

我们来对结果进行验证。我们选择图片LaTeX_OCR/998.jpg

对这一段的评论会显示在这里
04-12
04-12
对这一段的评论会显示在这里

让lora微调前后的模型进行回答。

对这一段的评论会显示在这里

没有微调模型的回答:

对这一段的评论会显示在这里
图像中的LaTeX公式为:

\[ \mathcal{L}_{YM}^{(1)}(D\rightarrow 4,2) = \frac{\hbar g^2}{32\pi^2(4-D)} \left( \frac{11}{3}C + \frac{1}{6}T_s - \frac{4}{3}T_f \right) F^\mu_{\mu\nu}F^a_{\mu\nu} \]

解释:
- \(\mathcal{L}_{YM}\) 表示规范场理论的拉格朗日量。
- \(g\) 和 \(\hbar\) 分别是耦合常数和约化普朗克常数。
- \(C\)、\(T_s\) 和 \(T_f\) 是与规范场相关的张量。
- \(F^\mu_{\mu\nu}\) 和 \(F^a_{\mu\nu}\) 分别表示标量场和规范场的场强度。

这个公式描述了从 \(D\) 维到 \(4\) 维的规范场理论的拉格朗日量,考虑了 \(D\) 维空间中规范场的行为。
对这一段的评论会显示在这里

Lora微调后模型的回答:

对这一段的评论会显示在这里
\mathrm{tr}\mathrm{i}\mathrm{r}_{s} \left( \bar{\Phi } _{A} ^{(3)} \right) = (g h_{1} \left( \Phi ^{A} \right) + 1, g h_{2} \left( \Phi ^{A} \right) + 1, g h_{3} \left( \Phi ^{A} \right) ) ,
对这一段的评论会显示在这里

可以看到没有微调的模型,对于输出的风格不是我们想要的,并且公式有存在一些错误(比如在右边第二个F的上标应该是a,但原模型给了u)

对这一段的评论会显示在这里

而微调后的模型,有着非常完美表现:

对这一段的评论会显示在这里
04-13
04-13
对这一段的评论会显示在这里

🧐 推理LoRA微调后的模型

对这一段的评论会显示在这里

加载lora微调后的模型,并进行推理:

对这一段的评论会显示在这里
from peft import PeftModel
from transformers import AutoModel, AutoTokenizer
from PIL import Image

model_path=  "/root/autodl-tmp/OpenBMB/MiniCPM-o-2_6"
path_to_adapter="./output/minicpm-o-2-6-latexocr/checkpoint-1000"

model =  AutoModel.from_pretrained(
        model_path,
        trust_remote_code=True,
        )

lora_model = PeftModel.from_pretrained(
    model,
    path_to_adapter,
    device_map="auto",
    trust_remote_code=True
).eval().cuda()

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

image = Image.open('./LaTeX_OCR/998.jpg').convert('RGB')

question = "这张图对应的LaTex公式是什么?"
msgs = [{'role': 'user', 'content': [image, question]}]

answer = lora_model.chat(
    msgs=msgs,
    tokenizer=tokenizer
)
print(answer)
对这一段的评论会显示在这里

对这一段的评论会显示在这里

补充

对这一段的评论会显示在这里

注意

对这一段的评论会显示在这里

在微调脚本中,path_to_adapter加载的是一共固定的checkpoint文件,如果你添加了数据或超参数,请根据实际情况修改checkpoint文件路径。

对这一段的评论会显示在这里