将ebook-GPT-translator项目改造为使用本地LLM

一、网络环境与硬件准备

准备科学上网的条件,因为无论是下载ollama,还是从huggingface下载模型,都需要它。

至少需要1块8G以上显存的显卡。

二、第一种方式:使用ollama

1、安装第一个ollama模型

对于Windows用户来说,这是相对简单的方法。

访问ollama.com,下载ollama安装包(大约600+M,使用科学下载会很快),安装ollama。安装成功之后,在系统托盘会出现一只白色的小羊。

默认所有的模型会保存在C盘——这显然不合适。修改环境变量,将系统变量中的“OLLAMA_MODELS”修改为你指定的目录,例如:D\AI\ollama\models\

浏览器访问https://ollama.com/library,可以看到ollama当前支持的模型列表。最热门的有llama3.1(来自meta公司)、gemma2(来自Google)、Mistral(来自Nvidia)、phi3.5(来自Microsoft)、Qwen2.5(来自Alibaba)等等。进入任意模型,选择合适的大小(例如3b),网页上就已经直接列出了安装模型的命令行。以llama3.2-3b为例,只需在命令行中执行:

ollama run llama3.2:3b

这将会启动一个下载进程,耐心等待。下载完成后,它会自动加载并运行,在cmd终端显示出一个输入光标:此时就可以开始聊天了。这其实是一个测试过程,如果测试正常,输入/exit可以结束聊天。

此时此刻,你的电脑已经在端口11434运行着一个可用的本地API了。

2、调用ollama api

ollama的api默认运行在本地:http://localhost:11434/api/generate,可以通过post的方式来发送json格式的数据并获得响应。为此,python中通常需要导入两个库:requests、json。

import requests
import json

使用它超级简单:

url = "http://localhost:11434/api/generate"
headers = {
    "Content-Type": "application/json"
}
prompt = "hi, nice to meet my first llm!"
data = {
    "model": "llama3.2-3b",
    "prompt": prompt,
    "options": {
        "num_predict": 512
    },
    "steam": False
}
response = request.post(url, headers, data=json.dumps(data))
if response.status_code == 200:
    result = response.json()
    return result.get("response", "")
else:
    raise Exception(f"Error! Status Code: {response.status_code}")

这就完成了一个基本的输入prompt,返回response的过程。

可以从ollama的Github库了解详细文档:https://github.com/ollama/ollama/blob/main/docs/api.md。上述示例中,应用场景是批量翻译字幕,所以使用的是非流式响应的方式。可以根据需要,采用不同的方式来使用ollama api。

三、第二种方式:使用Transformers直接调用

1、下载llm

浏览器访问:https://huggingface.co/models,找到想要的模型。假设这次下载的是Qwen2.5-1.5B,点击进入模型页面后,首先是Model Card,提示你如何使用它;然后是Files and versions,这里可以看到全部的模型文件。

逐个将它们全部下载,假设保存在:D\AI\llm\models\Qwen2.5-1.5b目录中。

2、安装依赖和加载llm

首先安装通常用到的依赖:torch、transformers、pathlib等。简单写出它们的名字,每个一行,保存为requirements.txt,就可以用命令行批量安装:

pip install -r requirements.txt

对于torch需要尤其注意,为了充分利用Nvidia显卡,必须安装与显卡型号相对应的torch版本。为了确认这一点,可以访问:https://pytorch.org/get-started/locally/,在页面中选择操作系统版本和显卡的CUDA驱动版本,它会生成一个命令行让你执行。

在python中,导入这些库:

import json
from pathlib import Path
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

然后定义一下模型的路径,实际加载llm只需要两行:

BASE_DIR = os.path.abspath(os.path.dirname(__file__))
MODEL_BASE = os.path.join(BASE_DIR, 'models', 'Qwen2.5-1.5B')
tokenizer = AutoTokenizer.from_pretrained(MODEL_BASE, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(MODEL_BASE, torch_dtype="auto", device_map="auto", trust_remote_code=True, attn_implementation="flash_attention_2").eval()

需要注意的是,attn_implementation="flash_attention_2"是显示开启了flash_attention_2的算法,这是一种“注意力优化算法”,为了支持它,需要首先安装一些依赖:

set USE_FLASH_ATTENTION=1
pip install build
pip install cmake
pip install ninja
pip install wheel
pip install flash-attn --no-build-isolation 

最后一步是手动编译flash-attn,通常需要很长的时间,耐心等待。或者,其实也可以直接删掉这个设置:

model = AutoModelForCausalLM.from_pretrained(MODEL_BASE, torch_dtype="auto", device_map="auto", trust_remote_code=True).eval()

实测影响也没有那么大。

3、使用llm

注意,不同的llm,其调用的方式会有一些差别,需要仔细查看huggingface模型主页上的model card。

下面是适用于Qwen2.5的调用方式:

prompt = "ANY SYSTEM PROMPTS"
text = “USER PROMPTS”
def askLLM(text):
    messages = [
        {"role": "system", "content": prompt},
        {"role": "user", "content": text}
    ]
    chat_text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True
    )
    model_inputs = tokenizer([chat_text], return_tensors="pt").to(model.device)
    with torch.no_grad():
        generated_ids = model.generate(
            **model_inputs,
            max_new_tokens=512
        )
    generated_ids = [
        output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
    ]
    generated_text = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
    return generated_text

补充说明:

1、torch.no_grad()方法是取消梯度计算,可以显著降低显存占用这在推理中是合适的设置,但不适用于训练。

2、max_new_tokens和前面ollama api中的num_predict一样,都是设置输出文本的长度的。注意,输出文本的长度与计算量基本成正比,对于个人电脑来说,可能不适合使用超过2048的值。在特定场景下,建议设置为合理的最小值,以加快批量任务的执行速度。

四、改造ebook-gpt-translator项目

项目地址:https://github.com/jesselau76/ebook-GPT-translator

这是一个之前用过的项目,当初用GPT-4的在线api进行翻译测试,发现其成本与讯飞文档翻译基本相当,翻译质量也相当,但速度太慢,一本英文书通常需要翻译6-8个小时。随着openai对在线api的严厉管控(嗯,也就炸了5个号)之后,在线api不再成为一种首选的途径,该项目的作者也已经一年多没有更新它了。

现在有了本地llm,当然就可以重新复活它了。

1、将GPT在线api更换为本地llm api

如前,此处不再赘述。

2、优化pdf解析

替换原作者使用的pdfminer库,改为效率更高的PyMuPDF,它的导入名称是fitz。这一步将原来PDF的解析工作效率提高了100倍,从需要若干分钟,变成一瞬间即可完成。

3、优化epub文件的处理

原作者对epub文件情有独钟,是唯一支持翻译后还能保留图片的文件格式。然而,作者非常偷懒地将每一章的所有图片全部堆在翻译后文件的每一章最前面,这显然离完美相差很远。作者的理由是:如果每次遇到图片就分段,那可能会导致很小的分段,就影响翻译质量啦。其实还是有办法的。

我们只需要在原文中,每次遇到图片标签就自动放一个占位符,并加上序号,那么在翻译完成之后,就可以通过逐个替换这些序号,将图片放回原处。

soup = BeautifulSoup(item.get_content(), 'html.parser')
img_count = 0
for img_tag in soup.find_all('img'):
    img_count += 1
    img_tag.replace_with("[11_22_" + str(img_count) + "]") 
text = soup.get_text().strip()

用img_count加上一个序号,就可以有效避免每一片段中如果存在多个图片的情形。占位符强烈建议使用数字和符号,例如本例中的[11_22_{imgcount}],在翻译的场景下,任何llm都会自动原封不动地保留它。如果使用类似[PRE_IMG_{img_count}]这样的占位符,实测仍会有被“翻译”的风险。

全部翻译完之后,再将图片替换回来:

soup = BeautifulSoup(item.get_content(), 'html.parser')
img_count = 0
for img_tag in soup.find_all('img'):
    img_count += 1
    img_html = str(img_tag)
    if bilingual_output.lower() == 'true':
        translated_text = translated_text.replace("[11_22_" + str(img_count) + "]", img_html + "<br>", 2)
    else:
        translated_text = translated_text.replace("[11_22_" + str(img_count) + "]", img_html + "<br>", 1)
item.set_content(translated_text.replace('\n', '<br>').encode('utf-8'))
translated_all += translated_text

注意,这里的bilingual_output是一个可设置的参数,当设置为双语输出时,每个图片占位符会出现两次,因此需要替换两次。

OK!从此实现了翻译自由——与GPT api或讯飞文档翻译相比,速度基本相当,使用7B模型的情况下质量基本相当,而成本只剩下不到1/100。这里是修改后的py文件:点击下载,感谢原作者jesselau76!

五、ollama与transformers的对比

实测发现,使用ollama api,它默认情况下并不会充分使用你的GPU,占用的显存也较小。而transformers直接调用的方式会毫不客气地吃掉你的显存,并将GPU直接拉满。所以ollama api的速度会慢很多:大概只有后者的1/3~1/2。

但ollama绝非一无是处,它默认情况下,会将一部分显存占用改为系统内存占用,并适当地让CPU也介入分担一些GPU的工作,这当然会拖慢速度,但好处是ollama因此获得很大的弹性:24G显存的显卡甚至可以运行70B的模型。与之对比,在同样硬件条件下,transformers运行14B的模型都经常失败。所以,ollama在多并发、高质量、低速度要求的对外服务场景下更有优势;而transformers则适合个人单机执行特定任务,会更有效率。