CasRel关系抽取模型从零开始:基于HuggingFace Transformers重训微调流程

张开发
2026/6/14 22:46:24 15 分钟阅读
CasRel关系抽取模型从零开始:基于HuggingFace Transformers重训微调流程
CasRel关系抽取模型从零开始基于HuggingFace Transformers重训微调流程你是不是经常面对一堆文档需要手动整理出“谁在什么时候做了什么”这类信息比如从新闻里找出公司间的收购关系或者从技术报告中提取产品与功能之间的关联。这个过程不仅枯燥还容易出错。今天我们就来聊聊如何让机器自动帮你完成这个任务。我将带你从零开始手把手教你使用CasRel模型并基于HuggingFace Transformers库训练一个属于你自己的关系抽取模型。无论你是想构建知识图谱还是为智能问答系统提供数据支持这篇文章都能给你一套清晰、可落地的方案。1. 认识CasRel关系抽取的“高效捕手”在开始动手之前我们先花几分钟搞明白CasRel到底是什么以及它为什么比传统方法更厉害。1.1 关系抽取在做什么简单来说关系抽取就是从一段文本里自动找出实体之间的关系。这里的实体可以是人、地点、组织关系则是连接它们的纽带比如“出生于”、“就职于”、“位于”。传统的方法有点像“先抓人再问关系”。它先识别出文本里所有的实体比如“查尔斯·阿兰基斯”和“智利圣地亚哥”然后再判断每两个实体之间可能存在什么关系。这种方法在遇到复杂情况时比如一句话里提到同一个实体和多个其他实体的不同关系时就容易“卡壳”或出错。1.2 CasRel的巧妙设计级联二元标记CasRel的全称是“Cascade Binary Tagging Framework”翻译过来就是“级联二元标记框架”。它的核心思路非常巧妙可以概括为“先定主角再找关系和配角”。想象一下侦探破案第一步锁定嫌疑人主体。模型先扫描整个句子找出所有可能作为“主体”的实体。比如在“查尔斯·阿兰基斯出生于智利圣地亚哥”这句话里它先锁定“查尔斯·阿兰基斯”。第二步针对每个嫌疑人列出其所有可能罪行关系和共犯客体。对于锁定的“查尔斯·阿兰基斯”模型不再去匹配所有其他实体而是直接问在这个句子背景下“查尔斯·阿兰基斯”可能涉及“出生地”这个关系吗如果涉及那么对应的“客体”地点是句子里的哪几个词这个过程是“级联”的即步骤二依赖于步骤一的结果。同时它对每个关系都做一次“是/否”二元的判断和对应客体的定位标记。这种设计让它能非常优雅地处理一句话里包含多个关系的情况效率高准确度也更好。2. 环境搭建与数据准备理论清楚了我们开始动手。第一步是把需要的工具和“食材”数据准备好。2.1 创建你的工作环境我强烈建议使用Conda或Venv来创建一个独立的Python环境避免包版本冲突。# 使用Conda创建新环境推荐 conda create -n casrel-re python3.9 conda activate casrel-re # 或者使用venv python -m venv casrel-env source casrel-env/bin/activate # Linux/Mac # casrel-env\Scripts\activate # Windows2.2 安装核心依赖接下来安装必要的Python库。核心是transformers和datasets我们也会用到torch进行模型训练。pip install transformers datasets torch # 如果需要使用GPU加速请根据你的CUDA版本安装对应的torch # 例如pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install seqeval # 用于评估序列标注任务 pip install tqdm # 用于显示进度条2.3 准备你的数据集模型需要数据来学习。关系抽取的数据通常需要标注出文本中的实体以及实体之间的关系。格式有很多种今天我们使用一种比较常见的JSON格式。假设我们有一个简单的数据集文件train.json内容如下[ { text: 马云是阿里巴巴集团的创始人。, spo_list: [ { subject: 马云, predicate: 创始人, object: 阿里巴巴集团 } ] }, { text: 北京大学坐落于北京市海淀区。, spo_list: [ { subject: 北京大学, predicate: 坐落于, object: 北京市海淀区 } ] } ]你需要根据自己的领域如金融、医疗、科技新闻去收集和标注数据。数据量越大、质量越高最终训练出的模型效果就越好。初期可以从几百条数据开始实验。3. 数据预处理把文本变成模型能懂的“数字”模型不能直接理解文字我们需要把文本数据转换成数字Token ID并按照CasRel任务的需求构造出对应的标签。3.1 理解任务与标签构造对于CasRel模型我们需要为每个训练样本生成两种标签主体识别标签一个序列标注出句子中哪些词属于某个主体Subject。关系-客体识别标签对于上一步识别出的每个主体以及预定义的关系集合中的每一种关系生成一个序列标注出在该关系下对应的客体Object是哪些词。这听起来有点绕我们通过代码来直观感受。下面的CasRelDataset类会完成主要的预处理工作。3.2 构建数据预处理类创建一个名为data_processor.py的文件。import json from typing import List, Dict, Any from transformers import BertTokenizerFast import torch class CasRelDataset(torch.utils.data.Dataset): def __init__(self, data_path: str, tokenizer: BertTokenizerFast, max_len: int 128, relation_list: List[str] None): 初始化数据集 Args: data_path: 训练数据JSON文件路径 tokenizer: 分词器 max_len: 最大序列长度 relation_list: 预定义的关系列表。如果为None则从数据中收集所有关系。 self.tokenizer tokenizer self.max_len max_len # 加载数据 with open(data_path, r, encodingutf-8) as f: self.raw_data json.load(f) # 获取或定义关系列表 if relation_list is None: self.relation_list self._collect_relations() else: self.relation_list relation_list self.relation2id {rel: idx for idx, rel in enumerate(self.relation_list)} self.id2relation {idx: rel for idx, rel in enumerate(self.relation_list)} # 预处理所有数据 self.processed_data self._preprocess_all_data() def _collect_relations(self) - List[str]: 从原始数据中收集所有出现的关系类型 relations set() for item in self.raw_data: for spo in item[spo_list]: relations.add(spo[predicate]) return sorted(list(relations)) # 排序以保证顺序固定 def _preprocess_all_data(self) - List[Dict[str, Any]]: 预处理所有数据样本 processed [] for item in self.raw_data: encoded self._encode_one_sample(item) if encoded: # 过滤掉因长度等问题处理失败的数据 processed.append(encoded) return processed def _encode_one_sample(self, sample: Dict) - Dict[str, Any]: 处理单个样本分词、构造标签 text sample[text] spo_list sample[spo_list] # 1. 分词 encoding self.tokenizer( text, max_lengthself.max_len, truncationTrue, paddingmax_length, return_tensorspt ) # 移除batch维度因为我们是一个样本一个样本处理的 input_ids encoding[input_ids].squeeze(0) attention_mask encoding[attention_mask].squeeze(0) token_type_ids encoding.get(token_type_ids, torch.zeros_like(input_ids)).squeeze(0) # 获取分词后的token列表和原始字符到token的映射用于对齐 tokens self.tokenizer.convert_ids_to_tokens(input_ids) # 这里简化处理实际生产环境需要更精细的字符-token对齐 # 2. 构造主体识别标签 (subject tagging) # 标签序列0表示非主体1表示主体开始2表示主体内部采用BIO格式简化 subj_labels torch.zeros(self.max_len, dtypetorch.long) # 在实际复杂实现中这里需要根据原始文本中主体的位置映射到token序列上并设置B-I-O标签。 # 为简化示例我们假设一个句子只有一个主体且位置已知实际应从spo_list中解析。 # 此处省略复杂的对齐代码仅作流程演示。 # 假设我们通过某种方式得到了主体在tokens中的起始和结束位置 start_idx, end_idx # subj_labels[start_idx] 1 # B-SUBJ # subj_labels[start_idx1:end_idx] 2 # I-SUBJ # 3. 构造关系-客体识别标签 (relation-object tagging) # 这是一个三维张量 [num_relations, max_len, 2] # 对于每一种关系都有一个[max_len, 2]的标签其中第一列是客体开始标签第二列是客体结束标签。 # 采用指针网络思想对每个位置预测它是否是某个客体的开始或结束。 num_relations len(self.relation_list) obj_head_labels torch.zeros((num_relations, self.max_len), dtypetorch.long) # 客体开始位置 obj_tail_labels torch.zeros((num_relations, self.max_len), dtypetorch.long) # 客体结束位置 # 遍历样本中的所有SPO三元组填充标签 for spo in spo_list: subj spo[subject] rel spo[predicate] obj spo[object] rel_id self.relation2id.get(rel) if rel_id is None: continue # 关系不在预定义列表中跳过 # 同样这里需要将客体obj在原文中的位置映射到token序列的起始和结束位置。 # obj_start_idx, obj_end_idx find_token_indices_for_span(text, obj, tokenizer) # obj_head_labels[rel_id, obj_start_idx] 1 # obj_tail_labels[rel_id, obj_end_idx] 1 # 此处省略映射代码... # 由于对齐映射代码较为复杂且非本文核心本例中我们返回一个标志性的数据结构。 # 在实际应用中你需要实现 find_token_indices_for_span 函数来完成精确的字符到token的索引映射。 return { input_ids: input_ids, attention_mask: attention_mask, token_type_ids: token_type_ids, subj_labels: subj_labels, # 示例用零张量 obj_head_labels: obj_head_labels, # 示例用零张量 obj_tail_labels: obj_tail_labels, # 示例用零张量 text: text, spo_list: spo_list } def __len__(self): return len(self.processed_data) def __getitem__(self, idx): return self.processed_data[idx] # 注意以上代码中的 find_token_indices_for_span 函数是实现关键它需要处理分词器如BERT WordPiece带来的子词切分问题 # 确保原始文本中的实体边界能准确对应到token序列的位置。这通常通过比较字符偏移量来实现。这个类完成了数据加载和格式转换的框架。最关键也是最繁琐的一步就是实现字符级别实体位置到token索引位置的精确映射。这需要你仔细处理分词器如BERT的WordPiece可能将一个字拆成多个子词token的情况。4. 构建CasRel模型数据准备好了接下来我们搭建模型本身。我们将基于一个预训练的BERT模型来构建CasRel的编码器和解码头。4.1 模型结构解析CasRel模型主要包含三部分编码器Encoder通常使用BERT将输入文本编码成一系列富含语义信息的向量。主体标记器Subject Tagger一个简单的线性分类层基于编码器的输出预测每个token是否是主体的开始或结束。关系-客体标记器Relation-specific Object Tagger对于上一步识别出的每个主体以及预定义的每一种关系都有一个独立的二元标记器用于预测在该关系下客体的开始和结束位置。4.2 使用Transformers库实现模型创建一个名为model.py的文件。import torch import torch.nn as nn from transformers import BertPreTrainedModel, BertModel from torch.nn import CrossEntropyLoss class CasRelModel(BertPreTrainedModel): 基于BERT的CasRel关系抽取模型实现。 简化版本用于展示核心结构。 def __init__(self, config, num_relations): super().__init__(config) self.num_relations num_relations self.bert BertModel(config) # 主体标记器预测每个token是主体开始、主体内部还是其他BIO self.subj_classifier nn.Linear(config.hidden_size, 3) # 0:O, 1:B-SUBJ, 2:I-SUBJ # 关系特定的客体标记器每个关系对应两个分类器开始、结束 # 输入是BERT输出向量和主体向量的融合表示 self.obj_start_classifiers nn.ModuleList([ nn.Linear(config.hidden_size * 2, 1) for _ in range(num_relations) ]) self.obj_end_classifiers nn.ModuleList([ nn.Linear(config.hidden_size * 2, 1) for _ in range(num_relations) ]) # 初始化权重 self.init_weights() def forward( self, input_idsNone, attention_maskNone, token_type_idsNone, subj_labelsNone, obj_head_labelsNone, obj_tail_labelsNone, subj_positionsNone, # 训练时可以提供真实主体位置以辅助客体预测 ): # 1. 通过BERT编码器获取文本表示 outputs self.bert( input_idsinput_ids, attention_maskattention_mask, token_type_idstoken_type_ids, return_dictTrue ) sequence_output outputs.last_hidden_state # [batch_size, seq_len, hidden_size] # 2. 主体识别 subj_logits self.subj_classifier(sequence_output) # [batch_size, seq_len, 3] loss 0 losses {} # 计算主体识别损失如果提供了标签 if subj_labels is not None: loss_fct CrossEntropyLoss() # 只对有效位置attention_mask1计算损失 active_loss attention_mask.view(-1) 1 active_logits subj_logits.view(-1, 3)[active_loss] active_labels subj_labels.view(-1)[active_loss] subj_loss loss_fct(active_logits, active_labels) loss subj_loss losses[subj_loss] subj_loss # 3. 关系-客体识别训练时使用真实主体推理时使用预测主体 # 为简化示例我们假设训练时传入了真实主体的起始位置 subj_positions # subj_positions: [batch_size, 2] 每行是(start_idx, end_idx) obj_loss 0 if subj_positions is not None and (obj_head_labels is not None or obj_tail_labels is not None): batch_size, seq_len, hidden_size sequence_output.shape # 获取每个样本的主体向量表示通常取主体起始和结束位置向量的平均 subj_representations [] for i in range(batch_size): start, end subj_positions[i] # 处理padding等情况 if start seq_len or end seq_len: subj_vec torch.zeros(hidden_size, devicesequence_output.device) else: subj_vec sequence_output[i, start:end1].mean(dim0) # 平均池化 subj_representations.append(subj_vec) subj_representation torch.stack(subj_representations, dim0) # [batch_size, hidden_size] # 将主体表示与每个token的表示拼接作为客体标记器的输入 # 扩展subj_representation以便与每个token拼接 subj_repr_expanded subj_representation.unsqueeze(1).expand(-1, seq_len, -1) # [batch_size, seq_len, hidden_size] combined_repr torch.cat([sequence_output, subj_repr_expanded], dim-1) # [batch_size, seq_len, hidden_size*2] # 对每一种关系计算客体开始和结束的logits all_rel_obj_start_logits [] all_rel_obj_end_logits [] for rel_id in range(self.num_relations): start_logits self.obj_start_classifiers[rel_id](combined_repr).squeeze(-1) # [batch_size, seq_len] end_logits self.obj_end_classifiers[rel_id](combined_repr).squeeze(-1) # [batch_size, seq_len] all_rel_obj_start_logits.append(start_logits) all_rel_obj_end_logits.append(end_logits) # 计算客体识别损失如果提供了标签 if obj_head_labels is not None and obj_tail_labels is not None: loss_fct_bce nn.BCEWithLogitsLoss() # 二分类问题 for rel_id in range(self.num_relations): # 获取当前关系的真实标签 gold_start obj_head_labels[:, rel_id, :] # [batch_size, seq_len] gold_end obj_tail_labels[:, rel_id, :] # 获取当前关系的预测logits pred_start all_rel_obj_start_logits[rel_id] pred_end all_rel_obj_end_logits[rel_id] # 只对有效位置计算损失 active_mask attention_mask.float() start_loss loss_fct_bce(pred_start * active_mask, gold_start.float() * active_mask) end_loss loss_fct_bce(pred_end * active_mask, gold_end.float() * active_mask) obj_loss (start_loss end_loss) loss obj_loss losses[obj_loss] obj_loss losses[total_loss] loss return losses if loss ! 0 else (subj_logits, all_rel_obj_start_logits, all_rel_obj_end_logits)这个模型类定义了CasRel的前向计算过程。在训练时我们同时计算主体识别和客体识别的损失。在推理时模型会先预测主体然后对于每个预测出的主体和每一种关系再去预测客体的位置。5. 训练与评估你的模型模型和数据都准备好了现在是时候让模型开始学习了。5.1 编写训练循环创建一个名为train.py的脚本。import torch from torch.utils.data import DataLoader from transformers import BertTokenizerFast, AdamW, get_linear_schedule_with_warmup from data_processor import CasRelDataset from model import CasRelModel import os def train(): # 超参数设置 MODEL_NAME bert-base-chinese # 使用中文BERT预训练模型 TRAIN_DATA_PATH ./data/train.json DEV_DATA_PATH ./data/dev.json # 验证集 OUTPUT_DIR ./output BATCH_SIZE 8 EPOCHS 10 LEARNING_RATE 3e-5 MAX_LEN 128 # 创建输出目录 os.makedirs(OUTPUT_DIR, exist_okTrue) # 1. 加载分词器和数据 print(Loading tokenizer and data...) tokenizer BertTokenizerFast.from_pretrained(MODEL_NAME) # 假设我们有一个预定义的关系列表可以从训练数据统计得来 # 这里为了示例手动定义几个关系 RELATION_LIST [创始人, 坐落于, 出生于, 国籍] train_dataset CasRelDataset(TRAIN_DATA_PATH, tokenizer, MAX_LEN, RELATION_LIST) dev_dataset CasRelDataset(DEV_DATA_PATH, tokenizer, MAX_LEN, RELATION_LIST) if os.path.exists(DEV_DATA_PATH) else None train_loader DataLoader(train_dataset, batch_sizeBATCH_SIZE, shuffleTrue) dev_loader DataLoader(dev_dataset, batch_sizeBATCH_SIZE, shuffleFalse) if dev_dataset else None # 2. 初始化模型 print(Initializing model...) from transformers import BertConfig config BertConfig.from_pretrained(MODEL_NAME) model CasRelModel.from_pretrained(MODEL_NAME, configconfig, num_relationslen(RELATION_LIST)) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) # 3. 设置优化器和学习率调度器 optimizer AdamW(model.parameters(), lrLEARNING_RATE) total_steps len(train_loader) * EPOCHS scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(0.1 * total_steps), num_training_stepstotal_steps ) # 4. 训练循环 print(Start training...) for epoch in range(EPOCHS): model.train() total_loss 0 for step, batch in enumerate(train_loader): # 将数据移动到设备 input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) # 注意这里需要从batch中获取真实的主体位置用于训练客体标记器。 # 由于我们之前的Dataset示例返回的是零张量这里需要你根据真实标注计算出来。 # subj_positions calculate_true_subject_positions(batch) # 为简化此处假设batch中已有subj_positions字段 subj_positions batch.get(subj_positions, None) if subj_positions is not None: subj_positions subj_positions.to(device) # 前向传播计算损失 losses model( input_idsinput_ids, attention_maskattention_mask, subj_positionssubj_positions, # 其他标签... ) loss losses[total_loss] # 反向传播 loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) # 梯度裁剪 optimizer.step() scheduler.step() optimizer.zero_grad() total_loss loss.item() if (step 1) % 10 0: print(fEpoch [{epoch1}/{EPOCHS}], Step [{step1}/{len(train_loader)}], Loss: {loss.item():.4f}) avg_train_loss total_loss / len(train_loader) print(fEpoch [{epoch1}/{EPOCHS}] finished. Average Train Loss: {avg_train_loss:.4f}) # 5. 验证可选 if dev_loader: model.eval() dev_loss 0 with torch.no_grad(): for dev_batch in dev_loader: # ... 类似训练步骤计算验证集损失 ... pass # 打印验证集损失并保存性能最好的模型 print(fValidation Loss: {dev_loss/len(dev_loader):.4f}) # 6. 保存模型 print(Saving model...) model.save_pretrained(OUTPUT_DIR) tokenizer.save_pretrained(OUTPUT_DIR) print(fModel saved to {OUTPUT_DIR}) if __name__ __main__: train()5.2 模型推理与使用训练完成后我们可以加载模型进行预测。创建一个predict.py脚本。from transformers import BertTokenizerFast from model import CasRelModel import torch def predict(): MODEL_PATH ./output # 你训练好的模型路径 SENTENCE 苹果公司由史蒂夫·乔布斯在美国加州创立。 # 加载模型和分词器 tokenizer BertTokenizerFast.from_pretrained(MODEL_PATH) model CasRelModel.from_pretrained(MODEL_PATH) model.eval() # 预处理输入句子 encoding tokenizer( SENTENCE, max_length128, truncationTrue, paddingmax_length, return_tensorspt ) input_ids encoding[input_ids] attention_mask encoding[attention_mask] # 推理 with torch.no_grad(): # 首先进行主体识别 subj_logits, all_rel_obj_start_logits, all_rel_obj_end_logits model( input_idsinput_ids, attention_maskattention_mask ) # 1. 解码主体 subj_preds torch.argmax(subj_logits, dim-1).squeeze(0).tolist() # [seq_len] tokens tokenizer.convert_ids_to_tokens(input_ids.squeeze(0)) # 简单的BIO解码找出所有主体 subjects [] i 0 while i len(subj_preds): if subj_preds[i] 1: # B-SUBJ start i i 1 while i len(subj_preds) and subj_preds[i] 2: # I-SUBJ i 1 end i - 1 subject_tokens tokens[start:end1] # 将token组合成字符串去掉特殊标记如[CLS], [SEP], ##前缀等 subject_text tokenizer.convert_tokens_to_string(subject_tokens).replace( ##, ).strip() if subject_text and subject_text not in [[CLS], [SEP]]: subjects.append((subject_text, start, end)) else: i 1 # 2. 对于每个识别出的主体解码其可能的关系和客体 spo_triplets [] for subj_text, subj_start, subj_end in subjects: # 这里需要模拟获取该主体的向量表示并与每个token表示拼接 # 为简化我们假设有一个函数能根据主体位置获取其表示并计算所有关系的客体logits # 然后对每种关系解码出客体开始和结束位置例如取logits 0的位置 # 最后将 (subj_text, relation, obj_text) 加入 spo_triplets pass # 具体解码逻辑需要根据模型输出和标签构造的逆过程实现 print(f输入句子: {SENTENCE}) print(f预测的三元组: {spo_triplets}) if __name__ __main__: predict()请注意上面的训练和推理代码是高度简化的框架特别是数据预处理中的标签对齐和解码部分需要你根据实际的数据格式和任务需求进行完整实现。核心挑战在于精确处理从原始文本字符到BERT分词后token序列的索引映射。6. 总结与下一步通过上面的步骤我们完成了一个基于HuggingFace Transformers的CasRel关系抽取模型从零开始的微调流程。我们来回顾一下关键点理解模型CasRel通过“先抽主体再针对主体和关系抽客体”的级联方式高效解决了复杂关系抽取问题。准备数据你需要一个包含(文本, SPO列表)标注格式的数据集。数据质量决定模型上限。处理数据这是最繁琐但至关重要的一步需要将字符级的实体标注准确映射到分词后的token索引上并构造出模型需要的三种标签。构建模型基于预训练BERT增加主体标记头和一系列关系特定的客体标记头。训练与评估设计合理的损失函数主体分类损失各关系客体识别损失进行模型训练并在验证集上评估性能。推理应用实现一个解码函数将模型输出的logits转换回(主体关系客体)三元组。6.1 可能遇到的挑战与优化方向数据对齐字符到token的映射是最大的工程难点务必仔细测试。多关系处理一个句子可能有多个主体一个主体可能对应多个关系和客体解码逻辑需要妥善处理这些情况。负样本关系抽取中负样本不存在某关系远多于正样本需要考虑采样策略或损失函数加权。预训练模型选择根据你的文本领域中文、英文、专业领域选择合适的预训练模型底座如bert-base-chinese,roberta-large等。评估指标关系抽取常用精确率Precision、召回率Recall和F1值来评估需要编写相应的评估脚本。6.2 开始你的实践最好的学习方式是动手。建议你从一个小的、标注好的数据集开始比如几百条。先实现一个简化版本确保数据流和训练循环能跑通。逐步完善数据预处理和解码部分。在验证集上观察效果进行调优。关系抽取是NLP中一项非常实用且富有挑战的任务成功部署后能极大提升信息处理的自动化程度。希望这篇指南能为你提供一个坚实的起点。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

更多文章