# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import math import random import numpy as np import paddle from tqdm import tqdm from paddlenlp.utils.log import logger prompt_format = """你是一个阅读理解专家,请提取所给句子与问题,提取实体。请注意,如果存在实体,则一定在原句中逐字出现,请输出对应实体的原文,不要进行额外修改;如果无法提取,请输出“无相应实体”。 **句子开始** {sentence} **句子结束** **问题开始** {prompt} **问题结束** **回答开始** """ def set_seed(seed): paddle.seed(seed) random.seed(seed) np.random.seed(seed) def create_data_loader(dataset, mode="train", batch_size=1, trans_fn=None): """ Create dataloader. Args: dataset(obj:`paddle.io.Dataset`): Dataset instance. mode(obj:`str`, optional, defaults to obj:`train`): If mode is 'train', it will shuffle the dataset randomly. batch_size(obj:`int`, optional, defaults to 1): The sample number of a mini-batch. trans_fn(obj:`callable`, optional, defaults to `None`): function to convert a data sample to input ids, etc. Returns: dataloader(obj:`paddle.io.DataLoader`): The dataloader which generates batches. """ if trans_fn: dataset = dataset.map(trans_fn) shuffle = True if mode == "train" else False if mode == "train": sampler = paddle.io.DistributedBatchSampler(dataset=dataset, batch_size=batch_size, shuffle=shuffle) else: sampler = paddle.io.BatchSampler(dataset=dataset, batch_size=batch_size, shuffle=shuffle) dataloader = paddle.io.DataLoader(dataset, batch_sampler=sampler, return_list=True) return dataloader def add_entity_negative_example(examples, texts, prompts, label_set, negative_ratio): negative_examples = [] positive_examples = [] with tqdm(total=len(prompts)) as pbar: for i, prompt in enumerate(prompts): redundants = list(set(label_set) ^ set(prompt)) redundants.sort() num_positive = len(examples[i]) if num_positive != 0: actual_ratio = math.ceil(len(redundants) / num_positive) else: # Set num_positive to 1 for text without positive example num_positive, actual_ratio = 1, 0 if actual_ratio <= negative_ratio or negative_ratio == -1: idxs = [k for k in range(len(redundants))] else: idxs = random.sample(range(0, len(redundants)), negative_ratio * num_positive) for idx in idxs: src = prompt_format.format_map({"sentence": texts[i], "prompt": redundants[idx]}) negative_result = {"src": src, "tgt": "无相应实体\n**回答结束**\n\n"} # negative_result = {"content": texts[i], "result_list": [], "prompt": redundants[idx]} negative_examples.append(negative_result) positive_examples.extend(examples[i]) pbar.update(1) return positive_examples, negative_examples def add_relation_negative_example(redundants, text, num_positive, ratio): added_example = [] rest_example = [] if num_positive != 0: actual_ratio = math.ceil(len(redundants) / num_positive) else: # Set num_positive to 1 for text without positive example num_positive, actual_ratio = 1, 0 all_idxs = [k for k in range(len(redundants))] if actual_ratio <= ratio or ratio == -1: idxs = all_idxs rest_idxs = [] else: idxs = random.sample(range(0, len(redundants)), ratio * num_positive) rest_idxs = list(set(all_idxs) ^ set(idxs)) for idx in idxs: src = prompt_format.format_map({"sentence": text, "prompt": redundants[idx]}) negative_result = {"src": src, "tgt": "无相应实体\n**回答结束**\n\n"} added_example.append(negative_result) for rest_idx in rest_idxs: src = prompt_format.format_map({"sentence": text, "prompt": redundants[rest_idx]}) negative_result = {"src": src, "tgt": "无相应实体\n**回答结束**\n\n"} rest_example.append(negative_result) return added_example, rest_example def add_full_negative_example(examples, texts, relation_prompts, predicate_set, subject_goldens, schema_lang="ch"): with tqdm(total=len(relation_prompts)) as pbar: for i, relation_prompt in enumerate(relation_prompts): negative_sample = [] for subject in subject_goldens[i]: for predicate in predicate_set: # The relation prompt is constructed as follows: # subject + "的" + predicate -> Chinese # predicate + " of " + subject -> English if schema_lang == "ch": prompt = subject + "的" + predicate else: prompt = predicate + " of " + subject if prompt not in relation_prompt: src = prompt_format.format_map({"sentence": texts[i], "prompt": prompt}) negative_result = {"src": src, "tgt": "无相应实体\n**回答结束**\n\n"} negative_sample.append(negative_result) examples[i].extend(negative_sample) pbar.update(1) return examples def convert_llm_examples( raw_examples, negative_ratio, is_train=True, schema_lang="ch", ): """ Convert labeled data export from doccano for extraction and aspect-level classification task. """ texts = [] entity_examples = [] relation_examples = [] entity_prompts = [] relation_prompts = [] entity_label_set = [] entity_name_set = [] predicate_set = [] subject_goldens = [] inverse_relation_list = [] predicate_list = [] logger.info("Converting doccano data...") with tqdm(total=len(raw_examples)) as pbar: for line in raw_examples: items = json.loads(line) # Export file in JSONL format which doccano >= 1.7.0 # Export file in JSONL (relation) format # e.g. {"text": "", "relations": [ {"id": 0, "start_offset": 0, "end_offset": 6, "label": "ORG"}, ... ], "entities": [ {"id": 0, "from_id": 0, "to_id": 1, "type": "foundedAt"}, ... ]} text, relations, entities = items["text"], items["relations"], items["entities"] texts.append(text) entity_example = [] entity_prompt = [] entity_example_map = {} entity_map = {} # id to entity name for entity in entities: entity_name = text[entity["start_offset"] : entity["end_offset"]] entity_label = entity["label"] entity_map[entity["id"]] = { "name": entity_name, "start": entity["start_offset"], "end": entity["end_offset"], } src = prompt_format.format_map({"sentence": text, "prompt": entity_label}) if entity_label not in entity_example_map.keys(): entity_example_map[entity_label] = {"src": src, "tgt": [entity_name]} else: entity_example_map[entity_label]["tgt"].append(entity_name) if entity_label not in entity_label_set: entity_label_set.append(entity_label) if entity_name not in entity_name_set: entity_name_set.append(entity_name) entity_prompt.append(entity_label) for label, v in entity_example_map.items(): v["tgt"] = ",".join(v["tgt"]) + "\n**回答结束**\n\n" entity_example.append(v) entity_examples.append(entity_example) entity_prompts.append(entity_prompt) subject_golden = [] # Golden entity inputs relation_example = [] relation_prompt = [] relation_example_map = {} inverse_relation = [] predicates = [] for relation in relations: predicate = relation["type"] subject_id = relation["from_id"] object_id = relation["to_id"] # The relation prompt is constructed as follows: # subject + "的" + predicate -> Chinese # predicate + " of " + subject -> English if schema_lang == "ch": prompt = entity_map[subject_id]["name"] + "的" + predicate inverse_negative = entity_map[object_id]["name"] + "的" + predicate else: prompt = predicate + " of " + entity_map[subject_id]["name"] inverse_negative = predicate + " of " + entity_map[object_id]["name"] if entity_map[subject_id]["name"] not in subject_golden: subject_golden.append(entity_map[subject_id]["name"]) src = prompt_format.format_map({"sentence": text, "prompt": prompt}) inverse_relation.append(inverse_negative) predicates.append(predicate) if prompt not in relation_example_map.keys(): relation_example_map[prompt] = {"src": src, "tgt": [entity_map[object_id]["name"]]} else: relation_example_map[prompt]["tgt"].append(entity_map[object_id]["name"]) if predicate not in predicate_set: predicate_set.append(predicate) relation_prompt.append(prompt) for v in relation_example_map.values(): v["tgt"] = ",".join(v["tgt"]) + "\n**回答结束**\n\n" relation_example.append(v) relation_examples.append(relation_example) relation_prompts.append(relation_prompt) subject_goldens.append(subject_golden) inverse_relation_list.append(inverse_relation) predicate_list.append(predicates) pbar.update(1) logger.info("Adding negative samples for first stage prompt...") positive_examples, negative_examples = add_entity_negative_example( entity_examples, texts, entity_prompts, entity_label_set, negative_ratio ) if len(positive_examples) == 0: all_entity_examples = [] else: all_entity_examples = positive_examples + negative_examples all_relation_examples = [] if len(predicate_set) != 0: logger.info("Adding negative samples for second stage prompt...") if is_train: positive_examples = [] negative_examples = [] per_n_ratio = negative_ratio // 3 with tqdm(total=len(texts)) as pbar: for i, text in enumerate(texts): negative_example = [] collects = [] num_positive = len(relation_examples[i]) # 1. inverse_relation_list redundants1 = inverse_relation_list[i] # 2. entity_name_set ^ subject_goldens[i] redundants2 = [] if len(predicate_list[i]) != 0: nonentity_list = list(set(entity_name_set) ^ set(subject_goldens[i])) nonentity_list.sort() if schema_lang == "ch": redundants2 = [ nonentity + "的" + predicate_list[i][random.randrange(len(predicate_list[i]))] for nonentity in nonentity_list ] else: redundants2 = [ predicate_list[i][random.randrange(len(predicate_list[i]))] + " of " + nonentity for nonentity in nonentity_list ] # 3. entity_label_set ^ entity_prompts[i] redundants3 = [] if len(subject_goldens[i]) != 0: non_ent_label_list = list(set(entity_label_set) ^ set(entity_prompts[i])) non_ent_label_list.sort() if schema_lang == "ch": redundants3 = [ subject_goldens[i][random.randrange(len(subject_goldens[i]))] + "的" + non_ent_label for non_ent_label in non_ent_label_list ] else: redundants3 = [ non_ent_label + " of " + subject_goldens[i][random.randrange(len(subject_goldens[i]))] for non_ent_label in non_ent_label_list ] redundants_list = [redundants1, redundants2, redundants3] for redundants in redundants_list: added, rest = add_relation_negative_example( redundants, texts[i], num_positive, per_n_ratio, ) negative_example.extend(added) collects.extend(rest) num_sup = num_positive * negative_ratio - len(negative_example) if num_sup > 0 and collects: if num_sup > len(collects): idxs = [k for k in range(len(collects))] else: idxs = random.sample(range(0, len(collects)), num_sup) for idx in idxs: negative_example.append(collects[idx]) positive_examples.extend(relation_examples[i]) negative_examples.extend(negative_example) pbar.update(1) all_relation_examples = positive_examples + negative_examples else: relation_examples = add_full_negative_example( relation_examples, texts, relation_prompts, predicate_set, subject_goldens, schema_lang=schema_lang ) all_relation_examples = [r for relation_example in relation_examples for r in relation_example] return all_entity_examples, all_relation_examples