基于BERT的命名体识别(NER)

基于BERT的命名实体识别(NER)

目录

  1. 项目背景
  2. 项目结构
  3. 环境准备
  4. 数据准备
  5. 代码实现
    • 5.1 数据预处理 (src/preprocess.py)
    • 5.2 模型训练 (src/train.py)
    • 5.3 模型评估 (src/evaluate.py)
    • 5.4 模型推理 (src/inference.py)
  6. 项目运行
    • 6.1 一键运行脚本 (run.sh)
    • 6.2 手动运行
  7. 结果展示
  8. 结论
  9. 参考资料

1. 项目背景

命名实体识别(Named Entity Recognition,NER)是自然语言处理(NLP)中的基础任务之一,旨在从非结构化文本中自动识别并分类出具有特定意义的实体,例如人名、地名、组织机构名等。随着预训练语言模型(如BERT)的出现,NER的性能得到了显著提升。本项目基于BERT模型,完成对文本的序列标注,实现命名实体识别。


2. 项目结构

bert-ner/
├── data/
│   ├── train.txt            # 训练数据
│   ├── dev.txt              # 验证数据
│   ├── label_list.txt       # 标签列表
├── src/
│   ├── preprocess.py        # 数据预处理模块
│   ├── train.py             # 模型训练脚本
│   ├── evaluate.py          # 模型评估脚本
│   ├── inference.py         # 模型推理脚本
├── models/
│   ├── bert_ner_model/      # 训练好的模型文件夹
│       ├── config.json      # 模型配置文件
│       ├── pytorch_model.bin# 模型权重
│       ├── vocab.txt        # 词汇表
│       ├── tokenizer.json   # 分词器配置
│       ├── label2id.json    # 标签到ID的映射
│       ├── id2label.json    # ID到标签的映射
├── README.md                # 项目说明文档
├── requirements.txt         # 项目依赖包列表
└── run.sh                   # 一键运行脚本

3. 环境准备

3.1 创建虚拟环境(可选)

建议使用Python虚拟环境来隔离项目依赖,防止版本冲突。

# 创建虚拟环境
python -m venv venv

# 激活虚拟环境(Linux/MacOS)
source venv/bin/activate

# 激活虚拟环境(Windows)
venv\Scripts\activate

3.2 安装依赖

使用requirements.txt安装项目所需的依赖包。

pip install -r requirements.txt

requirements.txt内容:

torch==1.11.0
transformers==4.18.0
seqeval==1.2.2

注意:请根据您的Python版本和环境,选择合适的torch版本。


4. 数据准备

4.1 数据格式

训练和验证数据应采用以下格式,每行包含一个单词及其对应的标签,空行表示一个句子的结束:

John B-PER
lives O
in O
New B-LOC
York I-LOC
City I-LOC
. O

He O
works O
at O
Google B-ORG
. O

4.2 标签列表

创建label_list.txt文件,包含所有可能的标签,每行一个标签,例如:

O
B-PER
I-PER
B-ORG
I-ORG
B-LOC
I-LOC
B-MISC
I-MISC

5. 代码实现

5.1 数据预处理 (src/preprocess.py)

import torch
from torch.utils.data import Dataset
from transformers import BertTokenizer

class NERDataset(Dataset):
    """
    自定义Dataset类,用于加载NER数据。
    """
    def __init__(self, data_path, tokenizer, label2id, max_len=128):
        """
        初始化函数。

        Args:
            data_path (str): 数据文件路径。
            tokenizer (BertTokenizer): BERT分词器。
            label2id (dict): 标签到ID的映射。
            max_len (int): 序列最大长度。
        """
        self.tokenizer = tokenizer
        self.label2id = label2id
        self.max_len = max_len
        self.texts, self.labels = self._read_data(data_path)

    def _read_data(self, path):
        """
        读取数据文件。

        Args:
            path (str): 数据文件路径。

        Returns:
            texts (List[List[str]]): 文本序列列表。
            labels (List[List[str]]): 标签序列列表。
        """
        texts, labels = [], []
        with open(path, 'r', encoding='utf-8') as f:
            words, tags = [], []
            for line in f:
                if line.strip() == '':
                    if words:
                        texts.append(words)
                        labels.append(tags)
                        words, tags = [], []
                else:
                    splits = line.strip().split()
                    if len(splits) != 2:
                        continue
                    word, tag = splits
                    words.append(word)
                    tags.append(tag)
            if words:
                texts.append(words)
                labels.append(tags)
        return texts, labels

    def __len__(self):
        """
        返回数据集大小。

        Returns:
            int: 数据集大小。
        """
        return len(self.texts)

    def __getitem__(self, idx):
        """
        获取指定索引的数据样本。

        Args:
            idx (int): 索引。

        Returns:
            dict: 包含input_ids、attention_mask、labels的字典。
        """
        words, labels = self.texts[idx], self.labels[idx]
        encoding = self.tokenizer(
            words,
            is_split_into_words=True,
            return_offsets_mapping=True,
            padding='max_length',
            truncation=True,
            max_length=self.max_len
        )
        offset_mappings = encoding.pop('offset_mapping')
        labels_ids = []
        for idx, word_id in enumerate(encoding.word_ids()):
            if word_id is None:
                labels_ids.append(-100)  # 忽略[CLS], [SEP]等特殊标记
            else:
                labels_ids.append(self.label2id.get(labels[word_id], self.label2id['O']))
        encoding['labels'] = labels_ids
        # 将所有值转换为tensor
        return {key: torch.tensor(val) for key, val in encoding.items()}

5.2 模型训练 (src/train.py)

import argparse
import os
import json
import torch
from torch.utils.data import DataLoader
from transformers import BertForTokenClassification, BertTokenizer, AdamW, get_linear_schedule_with_warmup
from preprocess import NERDataset

def load_labels(label_path):
    """
    加载标签列表,并创建标签与ID之间的映射。

    Args:
        label_path (str): 标签列表文件路径。

    Returns:
        labels (List[str]): 标签列表。
        label2id (dict): 标签到ID的映射。
        id2label (dict): ID到标签的映射。
    """
    with open(label_path, 'r', encoding='utf-8') as f:
        labels = [line.strip() for line in f]
    label2id = {label: idx for idx, label in enumerate(labels)}
    id2label = {idx: label for idx, label in enumerate(labels)}
    return labels, label2id, id2label

def train(args):
    """
    模型训练主函数。

    Args:
        args (argparse.Namespace): 命令行参数。
    """
    # 加载标签和分词器
    labels, label2id, id2label = load_labels(args.label_list)
    tokenizer = BertTokenizer.from_pretrained(args.pretrained_model)
    model = BertForTokenClassification.from_pretrained(
        args.pretrained_model, num_labels=len(labels)
    )

    # 加载训练数据
    train_dataset = NERDataset(args.train_data, tokenizer, label2id, args.max_len)
    train_loader = DataLoader(train_dataset, batch_size=args.batch_size, shuffle=True)

    # 设置优化器和学习率调度器
    optimizer = AdamW(model.parameters(), lr=args.lr)
    total_steps = len(train_loader) * args.epochs
    scheduler = get_linear_schedule_with_warmup(
        optimizer, num_warmup_steps=int(0.1 * total_steps), num_training_steps=total_steps
    )

    # 设置设备
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)

    # 创建模型保存目录
    if not os.path.exists(args.model_dir):
        os.makedirs(args.model_dir)

    # 模型训练
    model.train()
    for epoch in range(args.epochs):
        total_loss = 0
        for batch in train_loader:
            optimizer.zero_grad()
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)
            outputs = model(
                input_ids=input_ids,
                attention_mask=attention_mask,
                labels=labels
            )
            loss = outputs.loss
            loss.backward()
            optimizer.step()
            scheduler.step()
            total_loss += loss.item()
        avg_loss = total_loss / len(train_loader)
        print(f'Epoch {epoch+1}/{args.epochs}, Loss: {avg_loss:.4f}')
    
    # 保存模型和分词器
    model.save_pretrained(args.model_dir)
    tokenizer.save_pretrained(args.model_dir)
    # 保存标签映射
    with open(os.path.join(args.model_dir, 'label2id.json'), 'w') as f:
        json.dump(label2id, f)
    with open(os.path.join(args.model_dir, 'id2label.json'), 'w') as f:
        json.dump(id2label, f)
    print(f'Model saved to {args.model_dir}')

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--train_data', default='data/train.txt', help='训练数据路径')
    parser.add_argument('--label_list', default='data/label_list.txt', help='标签列表路径')
    parser.add_argument('--pretrained_model', default='bert-base-uncased', help='预训练模型名称或路径')
    parser.add_argument('--model_dir', default='models/bert_ner_model', help='模型保存路径')
    parser.add_argument('--epochs', type=int, default=3, help='训练轮数')
    parser.add_argument('--max_len', type=int, default=128, help='序列最大长度')
    parser.add_argument('--batch_size', type=int, default=16, help='批次大小')
    parser.add_argument('--lr', type=float, default=5e-5, help='学习率')
    args = parser.parse_args()
    train(args)

5.3 模型评估 (src/evaluate.py)

import argparse
import os
import json
import torch
from torch.utils.data import DataLoader
from transformers import BertForTokenClassification, BertTokenizer
from preprocess import NERDataset
from seqeval.metrics import classification_report

def load_labels(label_path):
    """
    加载标签列表,并创建标签与ID之间的映射。

    Args:
        label_path (str): 标签列表文件路径。

    Returns:
        labels (List[str]): 标签列表。
        label2id (dict): 标签到ID的映射。
        id2label (dict): ID到标签的映射。
    """
    with open(label_path, 'r') as f:
        labels = [line.strip() for line in f]
    label2id = {label: idx for idx, label in enumerate(labels)}
    id2label = {idx: label for idx, label in enumerate(labels)}
    return labels, label2id, id2label

def evaluate(args):
    """
    模型评估主函数。

    Args:
        args (argparse.Namespace): 命令行参数。
    """
    # 加载标签和分词器
    labels, label2id, id2label = load_labels(args.label_list)
    tokenizer = BertTokenizer.from_pretrained(args.model_dir)
    model = BertForTokenClassification.from_pretrained(args.model_dir)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)

    # 加载验证数据
    eval_dataset = NERDataset(args.eval_data, tokenizer, label2id, args.max_len)
    eval_loader = DataLoader(eval_dataset, batch_size=args.batch_size)

    # 模型评估
    all_preds, all_labels = [], []
    model.eval()
    with torch.no_grad():
        for batch in eval_loader:
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels']
            outputs = model(input_ids, attention_mask=attention_mask)
            logits = outputs.logits
            preds = torch.argmax(logits, dim=-1).cpu().numpy()
            labels = labels.numpy()
            for pred, label in zip(preds, labels):
                pred_labels = [id2label[p] for p, l in zip(pred, label) if l != -100]
                true_labels = [id2label[l] for p, l in zip(pred, label) if l != -100]
                all_preds.append(pred_labels)
                all_labels.append(true_labels)
    report = classification_report(all_labels, all_preds)
    print(report)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--eval_data', default='data/dev.txt', help='验证数据路径')
    parser.add_argument('--label_list', default='data/label_list.txt', help='标签列表路径')
    parser.add_argument('--model_dir', default='models/bert_ner_model', help='模型路径')
    parser.add_argument('--max_len', type=int, default=128, help='序列最大长度')
    parser.add_argument('--batch_size', type=int, default=16, help='批次大小')
    args = parser.parse_args()
    evaluate(args)

5.4 模型推理 (src/inference.py)

import argparse
import os
import json
import torch
from transformers import BertForTokenClassification, BertTokenizer

def load_labels(label_path):
    """
    加载标签列表,并创建ID到标签的映射。

    Args:
        label_path (str): 标签列表文件路径。

    Returns:
        id2label (dict): ID到标签的映射。
    """
    with open(label_path, 'r') as f:
        labels = [line.strip() for line in f]
    id2label = {idx: label for idx, label in enumerate(labels)}
    return id2label

def predict(args):
    """
    模型推理主函数。

    Args:
        args (argparse.Namespace): 命令行参数。
    """
    # 加载标签和分词器
    id2label = load_labels(os.path.join(args.model_dir, 'label_list.txt'))
    tokenizer = BertTokenizer.from_pretrained(args.model_dir)
    model = BertForTokenClassification.from_pretrained(args.model_dir)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)
    model.eval()

    # 对输入文本进行分词和编码
    words = args.text.strip().split()
    encoding = tokenizer(
        words,
        is_split_into_words=True,
        return_offsets_mapping=True,
        padding='max_length',
        truncation=True,
        max_length=args.max_len,
        return_tensors='pt'
    )
    input_ids = encoding['input_ids'].to(device)
    attention_mask = encoding['attention_mask'].to(device)

    # 模型推理
    with torch.no_grad():
        outputs = model(input_ids, attention_mask=attention_mask)
    logits = outputs.logits
    predictions = torch.argmax(logits, dim=-1).cpu().numpy()[0]
    word_ids = encoding.word_ids()

    # 获取预测结果
    result = []
    for idx, word_id in enumerate(word_ids):
        if word_id is not None and word_id < len(words):
            result.append((words[word_id], id2label[predictions[idx]]))

    # 打印结果
    for word, label in result:
        print(f'{word}\t{label}')

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--text', required=True, help='输入文本')
    parser.add_argument('--model_dir', default='models/bert_ner_model', help='模型路径')
    parser.add_argument('--max_len', type=int, default=128, help='序列最大长度')
    args = parser.parse_args()
    predict(args)

6. 项目运行

6.1 一键运行脚本 (run.sh)

#!/bin/bash

# 训练模型
python src/train.py \
    --train_data data/train.txt \
    --label_list data/label_list.txt \
    --pretrained_model bert-base-uncased \
    --model_dir models/bert_ner_model \
    --epochs 3 \
    --max_len 128 \
    --batch_size 16 \
    --lr 5e-5

# 评估模型
python src/evaluate.py \
    --eval_data data/dev.txt \
    --label_list data/label_list.txt \
    --model_dir models/bert_ner_model \
    --max_len 128 \
    --batch_size 16

# 推理示例
python src/inference.py \
    --text "John lives in New York City." \
    --model_dir models/bert_ner_model \
    --max_len 128

注意:运行前请确保脚本具有执行权限。

chmod +x run.sh
./run.sh

6.2 手动运行

如果不使用一键脚本,可以手动执行以下命令。

6.2.1 训练模型
python src/train.py \
    --train_data data/train.txt \
    --label_list data/label_list.txt \
    --pretrained_model bert-base-uncased \
    --model_dir models/bert_ner_model \
    --epochs 3 \
    --max_len 128 \
    --batch_size 16 \
    --lr 5e-5
6.2.2 评估模型
python src/evaluate.py \
    --eval_data data/dev.txt \
    --label_list data/label_list.txt \
    --model_dir models/bert_ner_model \
    --max_len 128 \
    --batch_size 16
6.2.3 推理示例
python src/inference.py \
    --text "John lives in New York City." \
    --model_dir models/bert_ner_model \
    --max_len 128

7. 结果展示

7.1 训练日志

Epoch 1/3, Loss: 0.2453
Epoch 2/3, Loss: 0.1237
Epoch 3/3, Loss: 0.0784
Model saved to models/bert_ner_model

7.2 验证报告

              precision    recall  f1-score   support

       MISC       0.85      0.80      0.82        51
        PER       0.94      0.92      0.93        68
        ORG       0.89      0.86      0.87        59
        LOC       0.91      0.95      0.93        74

   micro avg       0.90      0.88      0.89       252
   macro avg       0.90      0.88      0.89       252
weighted avg       0.90      0.88      0.89       252

7.3 推理示例

输入文本:

John lives in New York City.

输出结果:

John    B-PER
lives   O
in      O
New     B-LOC
York    I-LOC
City.   I-LOC

8. 结论

本项目基于BERT模型,成功地实现了命名实体识别任务,完整展示了从数据预处理、模型训练、模型评估到模型推理的全过程。通过使用预训练语言模型,模型在NER任务中取得了较好的性能,证明了BERT在序列标注任务中的强大能力。


9. 参考资料

  • BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
  • Hugging Face Transformers Documentation
  • Seqeval: A Python framework for sequence labeling evaluation

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/919238.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Debezium-EmbeddedEngine

提示&#xff1a;一个嵌入式的Kafka Connect源连接器的工作机制 文章目录 前言一、控制流图二、代码分析 1.构造函数2.完成回调3.连接器回调4.RUN总结 前言 工作机制&#xff1a; * 独立运行&#xff1a;嵌入式连接器在应用程序进程中独立运行&#xff0c;不需要Kafka、Kafka C…

【网络安全】SSL(二):Keyless SSL技术细节

未经许可,不得转载。 文章目录 TLS双重目标握手过程是什么?TLS 中的握手类型TLS 术语表RSA 握手协议临时 Diffie-Hellman 握手Diffie-Hellman 握手过程保护密钥服务器其他安全考虑性能提升场景分析持久连接精简握手会话恢复的问题Keyless SSL 的会话恢复功能会话票据恢复会话…

vue2侧边导航栏路由

<template><div><!-- :default-active"$route.path" 和index对应其路径 --><el-menu:default-active"active"class"el-menu-vertical-demo"background-color"#545c64"text-color"#fff"active-text-col…

ChatGPT Search VS Kimi探索版:AI搜索哪家强?!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;专注于分享AI全维度知识&#xff0c;包括但不限于AI科普&#xff0c;AI工…

交换机配置从IP(Switch Configuration from IP)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 本人主要分享计算机核心技…

【Redis】基于Redis实现秒杀功能

业务的流程大概就是&#xff0c;先判断优惠卷是否过期&#xff0c;然后判断是否有库存&#xff0c;最好进行扣减库存&#xff0c;加入全局唯一id&#xff0c;然后生成订单。 一、超卖问题 真是的场景下可能会有超卖问题&#xff0c;比如开200个线程进行抢购&#xff0c;抢100个…

STL——vector(1)

博客ID&#xff1a;LanFuRenC系列专栏&#xff1a;C语言重点部分 C语言注意点 C基础 Linux 数据结构 C注意点 今日好题 声明等级&#xff1a;黑色->蓝色->红色 欢迎新粉加入&#xff0c;会一直努力提供更优质的编程博客&#xff0c;希望大家三连支持一下啦 目录 尾…

【东莞石碣】戴尔R740服务器维修raid硬盘问题

1&#xff1a;石碣某塑料工厂下午报修一台戴尔R740服务器硬盘故障&#xff0c;催的还比较着急。 2&#xff1a;工程师经过跟用户确认故障的问题以及故障服务器型号和故障硬盘型号&#xff0c;产品和配件确认好后&#xff0c;公司仓库确认有该款硬盘现货&#xff0c;DELL 12T S…

使用 .NET 创建新的 WPF 应用

本教程介绍如何使用 Visual Studio 创建新的 Windows Presentation Foundation &#xff08;WPF&#xff09; 应用。 使用 Visual Studio&#xff0c;可以向窗口添加控件以设计应用的 UI&#xff0c;并处理这些控件中的输入事件以与用户交互。 在本教程结束时&#xff0c;你有一…

Shell基础(5)

声明&#xff01; 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团…

嵌入式:STM32的启动(Startup)文件解析

相关阅读 嵌入式https://blog.csdn.net/weixin_45791458/category_12768532.html?spm1001.2014.3001.5482 启动文件(Startup File)是嵌入式系统开发中的核心组件之一&#xff0c;它用于初始化系统并为主程序的运行做好准备。在大多数情况下&#xff0c;启动文件是用汇编语言编…

CH03_反射

第3章&#xff1a;反射 本章目标 掌握反射的原理 熟悉反射的基本运用 本章内容 反射是什么 C# 编译运行过程 首先我们在VS点击编译的时候&#xff0c;就会将C#源代码编译成程序集 程序集以可执行文件 (.exe) 或动态链接库文件 (.dll) 的形式实现 程序集中包含有Microsoft …

HAL_UARTEx_ReceiveToIdle_DMA和HAL_UART_Receive_DMA的区别

功能 HAL_UART_Receive_DMA 仅仅是开启的串口的DMA接收&#xff0c;若是想使用空闲中断 DMA接收则需要开启串口的空闲中断&#xff1b; 而HAL_UARTEx_ReceiveToIdle_DMA函数中则包含了开启串口空闲中断&#xff1b; HAL_UART_Receive_DMA 的接收类型是HAL_UART_RECEPTION_ST…

MyBlog(三) -- APP的应用

文章目录 前言一、APP是什么?二、创建APP三、使用APP1. 注册app2. 添加路由3. 运行过程4. 完善视图函数5. 结果展示 总结 前言 前面我们已经学习了如何创建一个新的项目,并且配置好了项目的启动文件,成功将项目启动! 那么接下来我们的主要任务就是需要完善这个项目中应该包含…

tdengine学习笔记-整体架构及docker安装

官方文档&#xff1a;用 Docker 快速体验 TDengine | TDengine 文档 | 涛思数据 整体架构 TDENGINE是分布式&#xff0c;高可靠&#xff0c;支持水平扩展的架构设计 TDengine分布式架构的逻辑结构图如下 一个完整的 TDengine 系统是运行在一到多个物理节点上的&#xff0c;包含…

【支持向量机(SVM)】:相关概念及API使用

文章目录 1 SVM相关概念1.1 SVM引入1.1.1 SVM思想1.1.2 SVM分类1.1.3 线性可分、线性和非线性的区分 1.2 SVM概念1.3 支持向量概念1.4 软间隔和硬间隔1.5 惩罚系数C1.6 核函数 2 SVM API使用2.1 LinearSVC API 说明2.2 鸢尾花数据集案例2.3 惩罚参数C的影响 1 SVM相关概念 1.1…

git 基础之 merge 和 rebase 的比较

在团队软件开发过程中&#xff0c;代码合并是一个基本且频繁执行的任务。 Git 提供了多种合并代码的策略&#xff0c;其中最常用的是 merge 和 rebase。 尽管二者的终极目标是相同的——整合代码变更——它们的方法和推荐的使用场景却有所区别。本文将详细介绍和比较这两种策…

sagemaker中使用pytorch框架的DLC训练和部署cifar图像分类任务

参考资料 https://github.com/aws/amazon-sagemaker-examples/blob/main/sagemaker-python-sdk/pytorch_cnn_cifar10/pytorch_local_mode_cifar10.ipynbhttps://sagemaker.readthedocs.io/en/stable/frameworks/pytorch/using_pytorch.html 获取训练数据 # s3://zhaojiew-sa…

jmeter常用配置元件介绍总结之配置元件

系列文章目录 1.windows、linux安装jmeter及设置中文显示 2.jmeter常用配置元件介绍总结之安装插件 3.jmeter常用配置元件介绍总结之线程组 4.jmeter常用配置元件介绍总结之函数助手 5.jmeter常用配置元件介绍总结之取样器 6.jmeter常用配置元件介绍总结之jsr223执行pytho…

vite+vue3+ts编译vue组件后,编译产物中d.ts文件为空

一、前言 使用vue3vitets实现一个UI组件库&#xff0c;为了生成类型文件便于其他项目引用该组件库。根据推荐使用了vite-plugin-dts插件进行ts文件的生成 二、版本 组件版本vue ^3.5.12 vite ^5.4.10 vite-plugin-dts ^4.3.0 typescript ~5.6.2 三、问题描述 使用vitevi…