背景
药品对码
不同机构,存储药品信息的格式不同,合作时需要对齐双方的药品信息,双方药品编码建立关联关系; 例如: 对方的药品
id | 通用名 | 批准文号 | 规格 | 生成厂商 |
---|---|---|---|---|
1024 | 维生素E软胶囊 | H35020242 | 100mg/粒x60粒/瓶/盒 | 国药控股星鲨制药(厦门)有限公司 |
我方药品库内批准文号、通用名与之匹配的有
id | 通用名 | 批准文号 | 规格 | 生成厂商 |
---|---|---|---|---|
5364 | 维生素E软胶囊 | 国药准字H35020242 | 0.1gx15粒x2板/盒 | 国药控股星鲨制药(厦门)有限公司 |
5368 | 维生素E软胶囊 | 国药准字H35020242 | 0.1gx60粒/瓶 | 国药控股星鲨制药(厦门)有限公司 |
5388 | 维生素E软胶囊 | 国药准字H35020242 | 0.1gx20粒x2板/盒 | 国药控股星鲨制药(厦门)有限公司 |
5394 | 维生素E软胶囊 | 国药准字H35020242 | 0.1gx16粒x2板/盒 | 国药控股星鲨制药(厦门)有限公司 |
对码后建立关联关系
id | 对方机构ID | 对方药品ID | 我方药品ID |
---|---|---|---|
1 | 1000 | 1024 | 5368 |
肉眼很容易看出,第二条匹配,但是用传统的程序并不好写,只能穷举对方可能的千奇百怪的写法,还可能遗漏;所以,这里适合用AI代替;
向AI发问
向本地部署的 DeepSeek-R1-Distill-Qwen-7B 提问
- 问
从输入中找出属于国药控股星鲨制药(厦门)有限公司的,与规格: 100mg/粒x60粒/瓶/盒, 最匹配的一项, 并返回其序号, 如果没有找到则返回0
1. 国药控股星鲨制药(厦门)有限公司, 0.1gx15粒x2板/盒
2. 国药控股星鲨制药(厦门)有限公司, 0.1gx60粒/瓶
3. 国药控股星鲨制药(厦门)有限公司, 0.1gx20粒x2板/盒
4. 国药控股星鲨制药(厦门)有限公司, 0.1gx16粒x2板/盒
- 答
<details open><summary class='thinking-summary'><span>✅ 思考完成</span></summary>
<div class='thinking-container'>
好的,我现在要解决这个问题。用户给了四个选项,都是国药控股星鲨制药(厦门)有限公司的,但规格不同。我需要找出与用户提供的规格最匹配的一项,然后返回它的序号,如果没有就返回0。
首先,用户给的规格是100mg/粒 × 60粒/瓶/盒。我需要仔细分析每个选项,看看哪个最接近。
看选项1:0.1gx15粒x2板/盒。这里的0.1g等于100mg,粒数是15,而用户的是60粒。所以粒数不符,但包装是2板/盒,可能指的是两盒装,但数量不够,所以这个选项不匹配。
选项2:0.1gx60粒/瓶。0.1g是100mg,粒数正好是60,但包装是每瓶,没有提到每盒,所以可能缺少了部分信息,但包装方式可能与用户的不同,所以这个选项可能部分匹配。
选项3:0.1gx20粒x2板/盒。0.1g是100mg,粒数20,包装是2板/盒。粒数不够,所以这个选项也不匹配。
选项4:0.1gx16粒x2板/盒。同样,0.1g是100mg,粒数16,也不够,所以也不匹配。
现在,我需要比较这些选项中哪个最接近。用户的需求是100mg/粒,60粒/瓶/盒。选项2正好有100mg,60粒,但包装是每瓶,而用户可能需要每盒。选项1有15粒,不够,但包装是2板/盒,可能意味着两盒装,但数量太少。选项3和4都是更少的粒数,所以都不符合。
所以,最接近的是选项2,因为它有正确的粒数和正确的重量,虽然包装方式不同,但其他选项都不符合。因此,我应该选择选项2,序号是2。
</div>
</details>
根据以上分析,最匹配的是选项2,返回其序号。
答案:2
微调
可以看到,AI实际上已经有足够的智力回答对码问题,只需要稍加调整就可以用了
- 提供多样的训练数据,使AI能使用更多的写法,比如:0.58gx12sx2板/盒
- 去除think部分
- 约束输出,仅输出序号
节省资源考虑,我选择1.5B的模型做LoRA微调,1.5B对于这个简单任务,已经够用了
步骤
初始环境准备
- 安装好Python环境,我的版本是 3.12.3,可通过命令 python --version查看版本
- 安装显卡驱动,我的显卡是RTX 4090,可通过命令 nvidia-smi 查看取得是否安装好
- 下载模型 DeepSeek-R1-Distill-Qwen-1.5B
安装LLaMA-Factory
- 下载LLaMA-Factory
- 创建python虚拟环境并安装依赖
cd LLaMA-Factory/ python/python3 -m venv .venv source .venv/bin/activate # windows下用 .venv\Scripts\activate # 安装依赖,建议用国内镜像 pip install -e ".[torch,metrics]" # 启动web ui,默认端口:7860 llamafactory-cli webui
准备训练数据并训练
训练数据格式
[
{
"system": "你是一位资深药师和药品数据管理员,精通药品和医疗器械的标准规格格式以及人们的各种手写写法。",
"instruction": "从输入中找出属于陕西利君现代中药有限公司的,与规格: 2x15片/板/盒, 最匹配的一项, 并返回其序号, 如果没有找到则返回0",
"input": "1. 陕西利君现代中药有限公司, 12片x3板/盒\n2. 陕西利君现代中药有限公司, 15片x2板/盒\n3. 陕西利君现代中药有限公司, 12片x2板/盒\n",
"output": "2"
},
{
"system": "你是一位资深药师和药品数据管理员,精通药品和医疗器械的标准规格格式以及人们的各种手写写法。",
"instruction": "从输入中找出属于辉瑞制药有限公司的,与规格: 100mgx1片/板/盒, 最匹配的一项, 并返回其序号, 如果没有找到则返回0",
"input": "1. 晖致制药(大连)有限公司, 0.1gx1片/盒\n2. 晖致制药(大连)有限公司, 0.1gx5片/盒\n3. 晖致制药(大连)有限公司, 0.1gx10片/盒\n",
"output": "1"
}
]
我准备了近3万条
注册数据集
编辑 /LLaMA-Factory/data/dataset_info.json 文件添加你的数据集
配置基础模型和数据集
其他参数保持默认
训练
点击开始,等待训练完成
加载基础模型和Adapter查看
加载
测试
合并导出模型并评估
导出
评估
LLaMA-Factory的 Evaluate&Predict 功能并不适用于当前任务,我们需要准备评估数据并计算模型回答的准确率
评估数据集
格式和训练数据集一致,我准备了5000条
评估代码
predict.py
import json
import time
from typing import (Any, Dict, List, Literal)
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
AutoConfig,
LogitsProcessorList,
InfNanRemoveLogitsProcessor,
GenerationConfig)
class Response:
def __init__(self, response_text: str, response_length: int, prompt_length: int, finish_reason: Literal["stop", "length"]):
self.response_text = response_text
self.response_length = response_length
self.prompt_length = prompt_length
self.finish_reason = finish_reason
# 合并导出的微调模型路径
BASE_MODEL_PATH = "/home/xuyd/yb_medicine_match"
# 评估数据的路径
EVAL_DATA_PATH = "/home/xuyd/LLaMA-Factory/data/yb_medicine/standard_check.json"
# 错误输出路径
ERROR_OUTPUT_PATH = "/home/xuyd/LLaMA-Factory/errors.json"
# 加载模型配置
config = AutoConfig.from_pretrained(BASE_MODEL_PATH, trust_remote_code=True, cache_dir=None, revision='main', token=None)
# 加载模型
model = AutoModelForCausalLM.from_pretrained(
pretrained_model_name_or_path=BASE_MODEL_PATH,
trust_remote_code=True,
device_map="auto",
torch_dtype=torch.bfloat16,
config=config,
)
model.requires_grad_(False)
model.eval()
# 加载 tokenizer
tokenizer = AutoTokenizer.from_pretrained(
pretrained_model_name_or_path=BASE_MODEL_PATH,
use_fast_tokenizer=True,
skip_special_tokens=False,
padding_side="right")
# 设置生成参数
generating_args : Dict[str,Any] = {
'do_sample' : True,
'temperature' : 0.5,
'top_p' : 0.7,
'top_k' : 50,
'num_return_sequences' : 1,
'repetition_penalty' : 1.0,
'length_penalty' : 1.0,
'eos_token_id' : tokenizer.eos_token_id,
'pad_token_id' : tokenizer.pad_token_id,
'max_length' : 1024,
'max_new_tokens' : 1024
}
def build_token_ids(item) -> List[int]:
"""构建token_ids"""
token_ids = []
token_ids += [tokenizer.bos_token_id]
token_ids += tokenizer.encode(item["system"], add_special_tokens=False)
token_ids += tokenizer.encode(f"<|User|>{item['instruction']}\n\n{item['input']}\n\n<|Assistant|>", add_special_tokens=False)
return token_ids
def get_logits_processor() -> LogitsProcessorList:
"""创建logits处理器,防止无效输出"""
logits_processor = LogitsProcessorList()
logits_processor.append(InfNanRemoveLogitsProcessor())
return logits_processor
def extract_answer(text):
"""从文本中提取答案"""
for char in text:
if char.isdigit():
return char
return "0" # 未找到数字时返回0
def clean_output(text: str) -> str:
"""清理模型输出,只保留最后一行"""
lines = text.strip().splitlines() # 按行分割并去掉首尾空格
last_line = lines[-1] if lines else "" # 取最后一行
return last_line.strip()
def process_item(item):
"""处理单个评估项"""
# 构建prompt
prompt_ids = build_token_ids(item)
prompt_length = len(prompt_ids)
inputs = torch.tensor([prompt_ids], device=model.device)
attention_mask = torch.ones_like(inputs, dtype=torch.bool)
gen_kwargs = dict(
inputs=inputs,
attention_mask=attention_mask,
generation_config=GenerationConfig(**generating_args),
logits_processor=get_logits_processor(),
)
generate_output = model.generate(**gen_kwargs)
response_ids = generate_output[:, prompt_length:]
response = tokenizer.batch_decode(
response_ids, clean_up_tokenization_spaces=True, skip_special_tokens=True
)
results = []
for i in range(len(response)):
eos_index = (response_ids[i] == tokenizer.eos_token_id).nonzero()
response_length = (eos_index[0].item() + 1) if len(eos_index) else len(response_ids[i])
results.append(
Response(
response_text=clean_output(response[i]),
response_length=response_length,
prompt_length=prompt_length,
finish_reason="stop" if len(eos_index) else "length",
)
)
# 解析答案
first = results[0]
print(f"原始输出:{first.response_text}")
model_answer = extract_answer(first.response_text)
# 构建结果
result = item.copy()
result["model_output"] = model_answer
result["is_correct"] = (model_answer == str(item["output"]))
return result
def main():
# 加载评估数据
with open(EVAL_DATA_PATH, "r", encoding="utf-8") as f:
eval_data = json.load(f)
errors = []
# 处理数据
start_time = time.time()
for item in eval_data:
processed = process_item(item)
if not processed["is_correct"]:
print(f"错误:{processed}")
errors.append(processed)
end_time = time.time()
print(f"评估完成,耗时:{end_time - start_time:.2f}s")
# 保存错误记录
with open(ERROR_OUTPUT_PATH, "w", encoding="utf-8") as f:
json.dump(errors, f, ensure_ascii=False, indent=2)
print(f"评估完成,错误数量:{len(errors)}/{len(eval_data)}")
print(f"错误记录已保存至:{ERROR_OUTPUT_PATH}")
if __name__ == "__main__":
main()
执行评估
python predict.py
结果
速度很快,大约23ms执行一次,但正确率并不理想,准确率只有95%,我希望达到99%,我觉得还需要以下调整
- 默认的LoRA秩为8,太小了,可以调大一点
- 加强提示词,查看评估错误的条目,发现有的规格中,用字母d代替了中文袋,但是模型并未分辨出来