Files
wehub-resource-sync 2aaeece67c
Codestyle Check / Lint (push) Has been cancelled
Codestyle Check / Check bypass (push) Has been cancelled
Pipelines-Test / Pipelines-Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:37:14 +08:00

370 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# coding:utf-8
# Copyright (c) 2021 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 os
from typing import Any, Dict, List, Union
import numpy as np
import paddle
import paddle.nn.functional as F
from scipy.special import expit as np_sigmoid
from scipy.special import softmax as np_softmax
from ..data import DataCollatorWithPadding
from ..prompt import (
AutoTemplate,
PromptDataCollatorWithPadding,
PromptModelForSequenceClassification,
SoftVerbalizer,
)
from ..transformers import (
AutoModelForMaskedLM,
AutoModelForSequenceClassification,
AutoTokenizer,
)
from ..utils.env import CONFIG_NAME, LEGACY_CONFIG_NAME
from ..utils.log import logger
from .task import Task
from .utils import static_mode_guard
usage = r"""
from paddlenlp import Taskflow
text_cls = Taskflow(
"text_classification",
mode="finetune",
problem_type="multi_class",
task_path=<local_saved_dynamic_model>,
id2label={0: "negative", 1: "positive"}
)
text_cls('房间依然很整洁,相当不错')
'''
[
{
'text': '房间依然很整洁,相当不错',
'predictions: [{
'label': 'positive',
'score': 0.80
}]
}
]
'''
text_cls = Taskflow(
"text_classification",
mode="prompt",
problem_type="multi_label",
is_static_model=True,
task_path=<local_saved_static_model>,
static_model_prefix=<static_model_prefix>,
plm_model_path=<local_saved_plm_model>,
id2label={ 0: "体育", 1: "经济", 2: "娱乐"}
)
text_cls(['这是一条体育娱乐新闻的例子',
'这是一条经济新闻'])
'''
[
{
'text': '这是一条体育娱乐新闻的例子',
'predictions: [
{
'label': '体育',
'score': 0.80
},
{
'label': '娱乐',
'score': 0.90
}
]
},
{
'text': '这是一条经济新闻',
'predictions: [
{
'label': '经济',
'score': 0.80
}
]
}
]
"""
def softmax(x, axis=None):
x_max = np.amax(x, axis=axis, keepdims=True)
exp_x_shifted = np.exp(x - x_max)
return exp_x_shifted / np.sum(exp_x_shifted, axis=axis, keepdims=True)
class TextClassificationTask(Task):
"""
The text classification model to classify text.
NOTE: This task is different from all other tasks that it has no out-of-box zero-shot capabilities.
Instead, it's used as a simple inference pipeline.
Args:
task (string): The name of task.
model (string): Mode of the classification, Supports ["prompt", "finetune"].
kwargs (dict, optional): Additional keyword arguments passed along to the specific task.
task_path (string): The local file path to the model path or a pre-trained model.
is_static_model (string): Whether the model in task path is a static model.
problem_type (str, optional): Select among ["multi_class", "multi_label"] based on the nature of your problem. Default to "multi_class".
multilabel_threshold (float): The probability threshold used for the multi_label setup. Only effective if model = "multi_label". Defaults to 0.5.
max_length (int): Maximum number of tokens for the model.
precision (int): Select among ["fp32", "fp16"]. Default to "fp32".
plm_model_name (str): Pretrained language model name for PromptModel.
input_spec [list]: Specify the tensor information for each input parameter of the forward function.
id2label(dict(int,string)): The dictionary to map the predictions from class ids to class names.
batch_size(int): The sample number of a mini-batch.
"""
def __init__(self, task: str, model: str = "finetune", **kwargs):
super().__init__(task=task, model=model, **kwargs)
self.problem_type = self.kwargs.get("problem_type", "multi_class")
self.multilabel_threshold = self.kwargs.get("multilabel_threshold", 0.5)
self._max_length = self.kwargs.get("max_length", 512)
self._construct_tokenizer()
if self.model == "prompt":
self._initialize_prompt()
self._check_predictor_type()
self._get_inference_model()
self._construct_id2label()
def _initialize_prompt(self):
if "plm_model_name" in self.kwargs:
self._plm_model = AutoModelForMaskedLM.from_pretrained(self.kwargs["plm_model_name"])
elif os.path.isdir(os.path.join(self._task_path, "plm")):
self._plm_model = AutoModelForMaskedLM.from_pretrained(os.path.join(self._task_path, "plm"))
logger.info(f"Load pretrained language model from {self._plm_model}")
else:
raise NotImplementedError(
"Please specify the pretrained language model name ex. plm_model_name='ernie-3.0-medium-zh'."
)
self._template = AutoTemplate.load_from(self._task_path, self._tokenizer, self._max_length, self._plm_model)
with open(os.path.join(self._task_path, "verbalizer_config.json"), "r", encoding="utf-8") as fp:
self._label_words = json.load(fp)
self._verbalizer = SoftVerbalizer(self._label_words, self._tokenizer, self._plm_model)
def _construct_input_spec(self):
"""
Construct the input spec for the convert dygraph model to static model.
"""
if "input_spec" in self.kwargs:
self._input_spec = self.kwargs["input_spec"]
elif self.model == "finetune":
if os.path.exists(os.path.join(self._task_path, LEGACY_CONFIG_NAME)):
with open(os.path.join(self._task_path, LEGACY_CONFIG_NAME)) as fb:
init_class = json.load(fb)["init_class"]
elif os.path.exists(os.path.join(self._task_path, CONFIG_NAME)):
with open(os.path.join(self._task_path, CONFIG_NAME)) as fb:
init_class = json.load(fb)["architectures"].pop()
else:
raise IOError(
f"Model configuration file doesn't exist.[task_path] should include {LEGACY_CONFIG_NAME} or {CONFIG_NAME}"
)
if init_class in ["ErnieMForSequenceClassification"]:
self._input_spec = [paddle.static.InputSpec(shape=[None, None], dtype="int64", name="input_ids")]
else:
self._input_spec = [
paddle.static.InputSpec(shape=[None, None], dtype="int64", name="input_ids"),
paddle.static.InputSpec(shape=[None, None], dtype="int64", name="token_type_ids"),
]
elif self.model == "prompt":
self._input_spec = self._model.get_input_spec()
else:
raise NotImplementedError(
f"'{self.model}' is not a supported model_type. Please select among ['finetune', 'prompt']"
)
def _construct_model(self, model: str):
"""
Construct the inference model for the predictor.
"""
if model == "finetune":
model_instance = AutoModelForSequenceClassification.from_pretrained(self._task_path)
elif model == "prompt":
model_instance = PromptModelForSequenceClassification(self._plm_model, self._template, self._verbalizer)
state_dict = paddle.load(os.path.join(self._task_path, "model_state.pdparams"), return_numpy=True)
model_instance.set_state_dict(state_dict)
# release memory
del state_dict
else:
raise NotImplementedError(
f"'{model}' is not a supported model_type. Please select among ['finetune', 'prompt']"
)
# Load the model parameter for the predict
model_instance.eval()
self._model = model_instance
def _construct_tokenizer(self):
"""
Construct the tokenizer for the predictor.
"""
self._tokenizer = AutoTokenizer.from_pretrained(self._task_path)
def _construct_id2label(self):
if "id2label" in self.kwargs:
id2label = self.kwargs["id2label"]
elif os.path.exists(os.path.join(self._task_path, "id2label.json")):
id2label_path = os.path.join(self._task_path, "id2label.json")
with open(id2label_path) as fb:
id2label = json.load(fb)
logger.info(f"Load id2label from {id2label_path}.")
elif self.model == "prompt" and os.path.exists(os.path.join(self._task_path, "verbalizer_config.json")):
label_list = sorted(list(self._verbalizer.label_words.keys()))
id2label = {}
for i, l in enumerate(label_list):
id2label[i] = l
logger.info("Load id2label from verbalizer.")
elif self.model == "finetune" and os.path.exists(os.path.join(self._task_path, CONFIG_NAME)):
config_path = os.path.join(self._task_path, CONFIG_NAME)
with open(config_path) as fb:
config = json.load(fb)
if "id2label" in config:
id2label = config["id2label"]
logger.info(f"Load id2label from {config_path}.")
else:
id2label = None
else:
id2label = None
if id2label is None:
self.id2label = id2label
else:
self.id2label = {}
for i in id2label:
self.id2label[int(i)] = id2label[i]
def _preprocess(self, inputs: Union[str, List[str]]) -> Dict[str, Any]:
"""
Transform the raw text to the model inputs, two steps involved:
1) Transform the raw text to token ids.
2) Generate the other model inputs from the raw text and token ids.
"""
inputs = self._check_input_text(inputs)
# Get the config from the kwargs
batch_size = self.kwargs["batch_size"] if "batch_size" in self.kwargs else 1
if self.model == "finetune":
collator = DataCollatorWithPadding(self._tokenizer, return_tensors="np")
tokenized_inputs = [self._tokenizer(i, max_length=self._max_length, truncation=True) for i in inputs]
batches = [tokenized_inputs[idx : idx + batch_size] for idx in range(0, len(tokenized_inputs), batch_size)]
elif self.model == "prompt":
collator = PromptDataCollatorWithPadding(
self._tokenizer, padding=True, return_tensors="np", return_attention_mask=True
)
part_text = "text"
for part in self._template.prompt:
if "text" in part:
part_text = part["text"]
template_inputs = [self._template({part_text: x}) for x in inputs]
batches = [template_inputs[idx : idx + batch_size] for idx in range(0, len(template_inputs), batch_size)]
else:
raise NotImplementedError(
f"'{self.model}' is not a supported model_type. Please select among ['finetune', 'prompt']"
)
outputs = {}
outputs["text"] = inputs
outputs["batches"] = [collator(batch) for batch in batches]
return outputs
def _run_model(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
"""
Run the task model from the outputs of the `_tokenize` function.
"""
# TODO: support hierarchical classification
outputs = {}
outputs["text"] = inputs["text"]
outputs["batch_logits"] = []
dtype_dict = {
"input_ids": "int64",
"token_type_ids": "int64",
"position_ids": "int64",
"attention_mask": "float32",
"masked_positions": "int64",
"soft_token_ids": "int64",
"encoder_ids": "int64",
}
with static_mode_guard():
for batch in inputs["batches"]:
if "attention_mask" in batch:
input_name = "attention_mask"
if batch[input_name].ndim == 2:
batch[input_name] = (1 - batch[input_name][:, np.newaxis, np.newaxis, :]) * -1e4
elif batch[input_name].ndim != 4:
raise ValueError(
"Expect attention mask with ndim=2 or 4, but get ndim={}".format(batch[input_name].ndim)
)
if self._predictor_type == "paddle-inference":
for i, input_name in enumerate(self.predictor.get_input_names()):
self.input_handles[i].copy_from_cpu(batch[input_name].astype(dtype_dict[input_name]))
self.predictor.run()
logits = self.output_handle[0].copy_to_cpu().tolist()
else:
input_dict = {}
for input_name in self.input_handler:
input_dict[input_name] = batch[input_name].astype(dtype_dict[input_name])
logits = self.predictor.run(None, input_dict)[0].tolist()
outputs["batch_logits"].append(logits)
return outputs
def _postprocess(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
"""
This function converts the model logits output to class score and predictions
"""
# TODO: support hierarchical classification
postprocessed_outputs = []
for logits in inputs["batch_logits"]:
if self.problem_type == "multi_class":
if isinstance(logits, paddle.Tensor): # dygraph
scores = F.softmax(logits, axis=-1).numpy()
labels = paddle.argmax(logits, axis=-1).numpy()
else: # static graph
scores = np_softmax(logits, axis=-1)
labels = np.argmax(logits, axis=-1)
for score, label in zip(scores, labels):
postprocessed_output = {}
if self.id2label is None:
postprocessed_output["predictions"] = [{"label": label, "score": score[label]}]
else:
postprocessed_output["predictions"] = [{"label": self.id2label[label], "score": score[label]}]
postprocessed_outputs.append(postprocessed_output)
elif self.problem_type == "multi_label": # multi_label
if isinstance(logits, paddle.Tensor): # dygraph
scores = F.sigmoid(logits).numpy()
else: # static graph
scores = np_sigmoid(logits)
for score in scores:
postprocessed_output = {}
postprocessed_output["predictions"] = []
for i, class_score in enumerate(score):
if class_score > self.multilabel_threshold:
if self.id2label is None:
postprocessed_output["predictions"].append({"label": i, "score": class_score})
else:
postprocessed_output["predictions"].append(
{"label": self.id2label[i], "score": class_score}
)
postprocessed_outputs.append(postprocessed_output)
else:
raise NotImplementedError(
f"'{self.problem_type}' is not a supported problem type. Please select among ['multi_class', 'multi_label']"
)
for i, postprocessed_output in enumerate(postprocessed_outputs):
postprocessed_output["text"] = inputs["text"][i]
return postprocessed_outputs