尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

医学搜索Query相关性判断:从TextCNN到BERT的NLP课设实战

发布时间:2026/9/23 17:02:40

资讯中心
01
ARTICLE

医学搜索Query相关性判断:从TextCNN到BERT的NLP课设实战

医学搜索Query相关性判断:从TextCNN到BERT的NLP课设实战
简介这份资源面向计算机、人工智能及相关专业的在校学生与教师提供天池比赛「NLP医学搜索Query相关性判断」的完整课设与毕设方案帮助读者理解医学搜索场景下的文本匹配与相关性建模思路。压缩包共30个文件约370KB以8个Python源码为核心配合7个JSON配置、5个XML工程文件、3个日志、2个Markdown说明文档及CSV数据等覆盖训练、评估与推理各环节。项目基于ERNIE、RoBERTa-wwm-large-ext、RoBERTa-large-pair等预训练模型包含数据增强、工具函数、训练评估脚本与多组模型运行入口目录按code、data、pretrain_models、logging等模块划分结构清晰便于按需查阅。目前已有120人学习适合作为课程设计、毕业设计或项目初期立项的参考也可在现有代码基础上修改扩展实现其他NLP任务。仅供学习参考请勿用于商业用途。1. 医学搜索里的 Query 相关性判断为什么值得单独做一遍电商搜索里“苹果手机”和“iPhone”算不算同一件事大家凭直觉就能判但医学搜索完全是另一套逻辑。用户输入“心梗”召回的文档标题写的是“急性心肌梗死”这俩在字面上几乎不重叠语义上却是同一件事反过来“糖尿病”和“糖尿病足”字面高度相似但一个是内科慢病管理一个是血管外科并发症相关性判断上必须区分开。天池这类 NLP 医学搜索 Query 相关性比赛考的就是这种“字面相似但语义不等价、字面不相似但语义等价”的细粒度判断能力。这份课设资源给的是完整可跑的 Python 源码加文档说明任务本质是一个三分类或二分类的文本匹配问题给定 (Query, 文档标题/摘要) 对输出相关性等级。它适合三类人正在做深度学习课设、需要一份能讲清楚原理又能跑通的学生想入门 NLP 文本匹配、但不想从零搭数据管道的工程师以及需要一份医学领域文本对 baseline 做对比实验的人。下面按“数据怎么读、模型怎么搭、指标怎么算、坑在哪”的顺序拆开讲。2. 数据读取与医学文本预处理从原始 Query-Doc 对到可训练张量2.1 先看清数据长什么样医学搜索相关性数据通常是 CSV 或 JSON 行格式字段大致是query、title、abstract或content、label。label 常见是 0/1/2 三档0 不相关、1 部分相关、2 完全相关。动手前第一件事不是写模型而是用 pandas 把分布打出来确认类别是否极度不平衡——医学数据里“不相关”往往占七成以上直接训练模型会退化成全预测 0。import pandas as pd # 读取训练集注意医学文本里可能有全角标点和换行 df pd.read_csv(train.csv, encodingutf-8) print(df.columns.tolist()) print(df[label].value_counts(normalizeTrue)) # 检查空值医学摘要字段经常有缺失 print(df[[query, title, abstract]].isnull().sum()) # 看一眼长度分布决定后面 max_len 设多少 df[q_len] df[query].str.len() df[d_len] (df[title].fillna() df[abstract].fillna()).str.len() print(df[[q_len, d_len]].describe(percentiles[0.5, 0.9, 0.99]))这段代码做了三件事确认字段名、看类别比例、看文本长度分位数。normalizeTrue输出的是占比而不是计数方便判断是否需要加权。长度分位数直接决定后面max_len的取值——如果 99 分位是 180那把max_len设成 512 纯属浪费显存。2.2 中文医学文本的清洗边界医学文本清洗有个反直觉的点不要无脑去停用词。像“无”“未”“不”这类否定词在医学语境里是强信号“未见异常”和“见异常”意思完全相反一旦被停用词表干掉模型就学反了。常见做法是只做全角转半角、去多余空白、统一大小写针对英文缩写保留所有中文实词和否定词。import re def clean_text(s): if not isinstance(s, str): return s s.strip() # 全角转半角 s .join(chr(ord(c) - 0xFEE0) if 0xFF01 ord(c) 0xFF5E else c for c in s) # 合并连续空白 s re.sub(r\s, , s) # 去掉 HTML 残留标签 s re.sub(r[^], , s) return s for col in [query, title, abstract]: df[col] df[col].apply(clean_text)0xFEE0是 Unicode 里全角与半角的固定偏移量这个转换对医学缩写如“”转“ACEI”很关键否则同一个药名会被当成两个 token。HTML 标签清理是因为部分医学语料是从网页抓的摘要里混着br之类的东西。2.3 构造模型输入与标签文本匹配模型一般把 Query 和 Doc 拼成一句送进编码器用[SEP]分隔。这里要注意医学 Query 通常很短几个词Doc 很长直接拼接后 Doc 会主导注意力。常见做法是给 Query 加一个前缀标记或者用双塔结构分别编码。课设级别用单塔拼接就够但拼接顺序建议 Query 在前。from sklearn.model_selection import train_test_split df[text_a] df[query] df[text_b] (df[title].fillna() df[abstract].fillna()).str.strip() train_df, dev_df train_test_split( df, test_size0.2, random_state42, stratifydf[label] ) print(train_df.shape, dev_df.shape)stratifydf[label]保证训练集和验证集的类别比例一致医学数据类别不平衡时这一步不能省否则验证集可能一个“完全相关”样本都没有指标完全失真。参数建议值说明max_len128~256按 99 分位长度取别盲目 512batch_size16~32显存 8G 以下用 16类别权重按频次反比缓解不平衡验证集比例0.15~0.2分层抽样提示清洗后一定要人工抽 20 条看一遍医学文本里“阴性”“阳性”这种词一旦被误处理模型学到的就是错的。3. 相关性建模从 TextCNN 到 BERT 微调的选型与实现3.1 为什么课设里 TextCNN 和 BERT 都值得写TextCNN 是深度学习课设的常客因为它结构简单、训练快、能在 CPU 上跑通适合讲清楚“卷积核提取 n-gram 特征”这件事。但医学搜索相关性判断的 SOTA 基本被预训练模型占据BERT 类模型在语义等价判断上优势明显。这份源码如果同时给了两条路线价值就在于能对比TextCNN 在字面重叠高的样本上不差但遇到“心梗 vs 急性心肌梗死”这种同义改写就明显掉点。选型建议课设报告里把 TextCNN 作为 baseline 讲原理把 BERT 微调作为主方案讲效果两者用同一套数据管道对比才有意义。3.2 TextCNN 的实现要点import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, num_classes3, filter_sizes(2, 3, 4), num_filters128): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) # 多尺度卷积核分别捕捉 2/3/4 元词组特征 self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, (fs, embed_dim)) for fs in filter_sizes ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(num_filters * len(filter_sizes), num_classes) def forward(self, x): # x: [batch, seq_len] emb self.embedding(x).unsqueeze(1) # [B, 1, L, D] feats [] for conv in self.convs: c torch.relu(conv(emb)).squeeze(3) # [B, F, L-fs1] c torch.max_pool1d(c, c.size(2)).squeeze(2) # 最大池化 feats.append(c) out self.dropout(torch.cat(feats, dim1)) return self.fc(out)filter_sizes(2,3,4)对应二元、三元、四元词组医学里“心肌梗死”是四字词四元卷积核能直接命中。padding_idx0让 padding 不参与梯度。最大池化取每个卷积核的最强响应这是 TextCNN 能处理变长文本的关键。注意unsqueeze(1)是为了把词向量当成单通道图像这是 Conv2d 的输入要求。3.3 BERT 微调的关键参数from transformers import BertTokenizer, BertForSequenceClassification from torch.optim import AdamW tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels3 ) enc tokenizer( train_df[text_a].tolist(), train_df[text_b].tolist(), max_length192, truncationTrue, paddingmax_length, return_tensorspt ) optimizer AdamW(model.parameters(), lr2e-5, weight_decay0.01)lr2e-5是 BERT 微调的经典学习率比从头训练小一到两个数量级因为预训练权重已经很好了大学习率会把它冲垮。max_length192是 Query 加 Doc 拼接后的长度医学摘要长截断时优先保 Query 和标题。weight_decay0.01抑制过拟合课设数据量通常不大这个参数别省。模型学习率batchepoch适用场景TextCNN1e-36410~20CPU、快速 baselineBERT-base2e-5163~5GPU、追求效果BERT 对抗训练1e-5163数据少、防过拟合注意BERT 微调 epoch 不是越多越好医学小样本上 3 个 epoch 后验证集 F1 往往就开始掉早停比调大 epoch 更有效。4. 训练循环、评估指标与医学场景下的排错4.1 训练循环里必须有的三件事from torch.utils.data import DataLoader, TensorDataset from sklearn.metrics import f1_score dataset TensorDataset(enc[input_ids], enc[attention_mask], torch.tensor(train_df[label].values)) loader DataLoader(dataset, batch_size16, shuffleTrue) for epoch in range(3): model.train() for ids, mask, y in loader: optimizer.zero_grad() out model(input_idsids, attention_maskmask, labelsy) loss out.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() # 验证 model.eval() preds, golds [], [] with torch.no_grad(): for ids, mask, y in dev_loader: logits model(input_idsids, attention_maskmask).logits preds.extend(logits.argmax(-1).tolist()) golds.extend(y.tolist()) print(fepoch {epoch} macro-F1: {f1_score(golds, preds, averagemacro):.4f})clip_grad_norm_(..., 1.0)是梯度裁剪医学文本里偶尔有超长样本导致梯度爆炸这一步能稳住训练。评估用macro-F1而不是准确率因为类别不平衡时准确率会骗人——全预测“不相关”也能有 70% 准确率但 macro-F1 会暴露问题。4.2 医学场景特有的坑第一个坑是否定词被截断。如果max_length设太小Query 里的“无”“未”可能被截掉模型判断直接反转。解决办法是截断时优先保 Query 全文Doc 从尾部截。第二个坑是同义词未归一。“心梗”和“心肌梗死”、“CT”和“计算机断层扫描”在词表里是不同 tokenTextCNN 学不到它们等价。常见做法是加一份医学同义词词典做替换或者直接用 BERT 的语义能力兜底。第三个坑是标签噪声。医学相关性标注主观性强同一对样本不同标注员可能给不同等级。如果发现验证集 F1 卡在某个值上不去先抽样看标签别急着调模型。# 快速定位错分样本 import numpy as np wrong np.array(preds) ! np.array(golds) for i in np.where(wrong)[0][:10]: print(Q:, dev_df.iloc[i][text_a]) print(D:, dev_df.iloc[i][text_b][:80]) print(gold:, golds[i], pred:, preds[i]) print(- * 40)这段代码把错分样本的 Query、Doc 前 80 字、真实标签和预测标签打出来人工扫一遍就能看出是数据问题还是模型问题。医学场景里错分往往集中在“部分相关”这一档因为它的边界最模糊。现象可能原因排查动作验证 F1 远低于训练过拟合加 dropout、减 epoch全预测同一类类别不平衡加类别权重、换 macro-F1否定句全错截断或停用词检查 max_len、停用词表同义词对全错词表未归一加同义词替换或换 BERT5. 把课设做成能复现的实验配置管理与结果对比技巧课设最容易翻车的地方不是模型而是“跑过一次就再也复现不出来”。把超参数、随机种子、数据路径全部收进一个 YAML 或 argparse是让这份源码从“能跑”变成“能交”的关键。我一般会用一个config.yaml管住所有变量训练脚本只读配置不写死任何数字。import yaml, random, numpy as np, torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) cfg yaml.safe_load(open(config.yaml, encodingutf-8)) set_seed(cfg[seed])set_seed要同时设 Python、NumPy、PyTorch 和 CUDA 四层种子少一层结果就可能飘。config.yaml里至少放seed、max_len、batch_size、lr、epochs、model_name六项换实验只改配置不改代码。结果对比建议固定一张表把 TextCNN 和 BERT 在同一验证集上的 macro-F1、各类别 F1、训练耗时都记下来。课设报告里这张表比任何文字描述都有说服力。如果时间够再补一个消融去掉同义词替换看 F1 掉多少去掉梯度裁剪看训练是否发散。这些细节才是拉开课设分数的部分。提示交作业前把requirements.txt和README里的运行命令对齐跑一遍transformers和torch版本不匹配是复现失败的头号原因。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。