自然语言处理:基于BERT预训练模型的中文命名实体识别(使用PyTorch)

news2024/12/26 19:44:47

命名实体识别(NER)

命名实体识别(Named Entity Recognition, NER)是自然语言处理(NLP)中的一个关键任务,其目标是从文本中识别出具有特定意义的实体,并将其分类到预定义的类别中。这些实体可以是人名、地名、组织机构名、日期时间、货币金额等。

  • 主要功能:
    • 实体识别:从文本中找出所有可能的命名实体。
    • 实体分类:将识别出来的实体归类到预先定义好的类别中,如人名、地名、组织名等。
    • 边界检测:确定每个实体在文本中的起始和结束位置。
  • 应用场景:
    • 信息检索:帮助搜索引擎理解查询意图,提供更精准的搜索结果。
    • 问答系统:辅助解析用户问题,提高答案的准确性。
    • 机器翻译:保留原文中的专有名词不被翻译,或根据上下文正确翻译。
    • 数据挖掘:从大量文本数据中提取有价值的信息,如市场分析、舆情监控等。
    • 个性化推荐:通过分析用户的兴趣点,提供个性化的服务和内容。

更多细节可以参考:命名实体识别综述。

本文目标

  • 从公开的新闻报道标题中提取地名,这里的地名主要是一些国家名称
  • 使用预训练的中文Bert模型,bert-base-chinese
  • 数据集的标注方式为BIO。

获取数据集

我们直接抓取漂亮国的中文发布网站的数据。
这里,我把数据存在PostgreSQL数据库里面,我建议大家安装一个数据库,非常方便数据抓取。

import time
import requests
import pandas as pd
from sqlalchemy import create_engine
from tqdm import tqdm
from bs4 import BeautifulSoup

user = 'postgres'
password = '你的密码'
db_name = '你的数据库名称'
db_url = f'postgresql://{user}:{password}@localhost:5432/{db_name}'
engine = create_engine(db_url)

def get_title(url):
    res = requests.get(url, headers=headers)

    try:
        txt = res.content.decode('gbk')
    except UnicodeDecodeError as e:
        txt = res.content.decode('utf-8')
    soup = BeautifulSoup(txt, 'lxml')   

    data = []
    for li in soup.find_all('li', class_='collection-result'):
        try:
            href = li.find('a')['href']
        except:
            href = '无数据'
        try:
            title = li.find('a').text.replace('\n','').replace('\t','')
        except:
            title = '无数据'
        try:
            date = li.find('div').text.replace('\n','').replace('\t','')
        except:
            date = '无数据'
        data.append([href, title, date])

    return pd.DataFrame(data, columns=['href','title','date'])

def get_news(url):
    res = requests.get(url, headers=headers)
    
    try:
        txt = res.content.decode('gbk')
    except UnicodeDecodeError as e:
        txt = res.content.decode('utf-8')
    soup = BeautifulSoup(txt, 'lxml')
    
    data = []
    for div in soup.find_all('div', class_='entry-content'):
        try:
            text = '\n'.join([p.get_text(strip=True) for p in div.find_all('p')[:-2]])
        except:
            text = '无数据'
        data.append({'href': url, 'text': text})

    return pd.DataFrame(data)

headers = {
  'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
  'cookie':'自己去网站找'
}

# 这里是抓取对应标题和url
for i in range(53):  # 页数
    url = f'https://www.state.gov/translations/chinese/page/{i}/'
    df = get_title(url)
    print(f'正在抓取: {url}, 数据长度: {len(df)}')
    df.to_sql('mfa_usa', con=engine, if_exists='append', index=False)
    time.sleep(30)

# 这里是抓取完整的报道
df = pd.read_sql('select * from mfa_usa', con=engine)
pbar = tqdm(list(df.href)[10:])
for url in pbar:
    pbar.set_description('Processing %s')
    df0 = get_news(url)
    df0.to_sql('mfa_usa_news', con=engine, if_exists='append', index=False)
    time.sleep(4)
  • 标题在这里插入图片描述
  • 全文
    在这里插入图片描述
  • 一共是500+的数据,差不多了,标注也挺麻烦的。

标注数据集

因为我的任务是提取地名,所以使用比较简单的BIO进行:

  • B-NP:开头
  • I-NP:中间
  • O:不是需要识别的词/字

这里推荐一个开源的NLP标注工具:MarkStudio。

第一步,转换数据格式

下载好之后,打开exe就可以导入自己的数据开始标注,但是数据必须以txt的形式导入,如下图所示。下面是简单的处理脚本

import pandas as pd

df = pd.read_csv('data/data.csv')

# 将每一行数据写入txt文件
txt_file = 'data/ner_label_in.txt'
with open(txt_file, 'w', encoding='utf-8') as f:
    for index, row in df.iterrows():
        f.write(row['text'] + '\n')  #
print(f"数据已成功写入 {txt_file} 文件!")

在这里插入图片描述

第二步,定义标签组

待标注数据准备好之后,我们打开标注工具,然后自定义标签(你也可以使用该工具自带的标签),如下图。
在这里插入图片描述

第三步,创建标注工程

回到工程管理,新建工程,然后导入待标注的txt文件,如下图。

  • 建工程
    在这里插入图片描述
  • 导数据
    在这里插入图片描述

第四步,标注实体

切换到工作台,就可以开始标注数据。
鼠标选中需要标的字或词,他会自动弹出我们预先选择的实体类型,如下图。
在这里插入图片描述

第五步,导出标注数据

该工具导出的标注数据为json格式。所以我后面在进行实验时,进行了预处理。
回到工程管理,点击导出数据即可,如下图。
在这里插入图片描述
我们就导出已经标注的数据。
在这里插入图片描述

微调Bert

数据预处理

import json
from sklearn.model_selection import train_test_split
from datasets import Dataset, DatasetDict

# 来自标注好的JSON文件
with open(LABEL_DATA_PATH, 'r', encoding='utf-8') as f:
    data = json.load(f)

texts = []
labels = []

for entry in data:
    text = entry['content']
    label_sequence = ['O'] * len(text)  # 初始化所有字符的标签为 'O'

    for tag in entry['tags']:
        if tag['name'] == 'PLACE':
            start = tag['start']
            end = tag['end']

            # 将开始位置标记为 'B-PLACE'
            label_sequence[start] = 'B-PLACE'

            # 将后续位置标记为 'I-PLACE'
            for i in range(start + 1, end):
                label_sequence[i] = 'I-PLACE'

    # 将标签转换为标签索引
    label_indices = [label2id[label] for label in label_sequence]

    texts.append(text)
    labels.append(label_indices)

# 检查转换后的格式
print("Texts:", texts[-2:])
print("Labels:", labels[-2:])

# 划分数据集--训练测试和验证
texts_train, texts_temp, labels_train, labels_temp = train_test_split(
    texts, labels, test_size=0.2, random_state=42
)
texts_val, texts_test, labels_val, labels_test = train_test_split(
    texts_temp, labels_temp, test_size=0.5, random_state=42
)

# 构造字典形式的数据
def create_dataset(texts, labels):
    ids = list(range(len(texts)))
    tokens_list = [list(text) for text in texts]
    return {'id': ids, 'tokens': tokens_list, 'ner_tags': labels}

train_data = create_dataset(texts_train, labels_train)
val_data = create_dataset(texts_val, labels_val)
test_data = create_dataset(texts_test, labels_test)

# 创建 Dataset 和 DatasetDict
train_dataset = Dataset.from_dict(train_data)
val_dataset = Dataset.from_dict(val_data)
test_dataset = Dataset.from_dict(test_data)

# 最终的数据集
ner_data = DatasetDict({
    'train': train_dataset,
    'validation': val_dataset,
    'test': test_dataset
})

编码文本

from transformers import BertTokenizerFast

def tokenize_and_align_labels(examples, label_all_tokens=True):
    tokenized_inputs = tokenizer(examples["tokens"], truncation=True, is_split_into_words=True)
    labels = []
    for i, label in enumerate(examples["ner_tags"]):
        word_ids = tokenized_inputs.word_ids(batch_index=i)
        # word_ids() => Return a list mapping the tokens
        # to their actual word in the initial sentence.
        # It Returns a list indicating the word corresponding to each token.
        previous_word_idx = None
        label_ids = []
        # Special tokens like `` and `<\s>` are originally mapped to None
        # We need to set the label to -100 so they are automatically ignored in the loss function.
        for word_idx in word_ids:
            if word_idx is None:
                # set –100 as the label for these special tokens
                label_ids.append(-100)
            # For the other tokens in a word, we set the label to either the current label or -100, depending on
            # the label_all_tokens flag.
            elif word_idx != previous_word_idx:
                # if current word_idx is != prev then its the most regular case
                # and add the corresponding token
                label_ids.append(label[word_idx])
            else:
                # to take care of sub-words which have the same word_idx
                # set -100 as well for them, but only if label_all_tokens == False
                label_ids.append(label[word_idx] if label_all_tokens else -100)
                # mask the subword representations after the first subword

            previous_word_idx = word_idx
        labels.append(label_ids)
    tokenized_inputs["labels"] = labels
    return tokenized_inputs

tokenizer = BertTokenizerFast.from_pretrained(MODEL_PATH+MODEL_NAME)  # 自己下载的中文 BERT 模型
# 应用于整个数据
tokenized_datasets = ner_data.map(tokenize_and_align_labels, batched=True)

定义模型

from torch.optim import AdamW
from transformers import Trainer, TrainingArguments
from transformers import DataCollatorForTokenClassification

# 初始化模型
model = AutoModelForTokenClassification.from_pretrained(MODEL_PATH+MODEL_NAME, num_labels=NUM_LABELS)

构建Trainer

from torch.optim import AdamW
from transformers import Trainer, TrainingArguments
from transformers import DataCollatorForTokenClassification

def calculate_ner_metrics(true_labels, pred_labels):
    """
    自定义评估函数,输入为二维列表,输出为各指标
    """
    assert len(true_labels) == len(pred_labels), "true_labels 和 pred_labels 的长度必须一致"
    
    # 初始化统计变量
    total_true = 0  # 总的真实实体数
    total_pred = 0  # 总的预测实体数
    total_correct = 0  # 预测正确的实体数
    total_tokens = 0  # 总的标注的token数
    correct_tokens = 0  # 预测正确的token数
    
    # 遍历每个序列
    for true_seq, pred_seq in zip(true_labels, pred_labels):
        assert len(true_seq) == len(pred_seq), "每个序列的长度必须一致"
        
        for true, pred in zip(true_seq, pred_seq):
            # 统计 token-level 准确性
            total_tokens += 1
            if true == pred:
                correct_tokens += 1
            
            # 如果是实体标签,更新统计
            if true != "O":  # 真实标签为实体
                total_true += 1
                if true == pred:  # 预测正确的实体
                    total_correct += 1
            
            if pred != "O":  # 预测标签为实体
                total_pred += 1
    
    # 计算指标
    accuracy = correct_tokens / total_tokens if total_tokens > 0 else 0.0
    precision = total_correct / total_pred if total_pred > 0 else 0.0
    recall = total_correct / total_true if total_true > 0 else 0.0
    f1 = (2 * precision * recall) / (precision + recall) if (precision + recall) > 0 else 0.0
    
    metrics = {
        "accuracy": accuracy,
        "precision": precision,
        "recall": recall,
        "f1_score": f1
    }
    return metrics

def compute_metrics(pred):
    pred_logits, labels = pred
    pred_logits = pred_logits.argmax(-1)
    # 取去除 padding 的部分
    predictions = [
        [id2label[eval_preds] for (eval_preds, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(pred_logits, labels)
    ]

    true_labels = [
        [id2label[l] for (eval_preds, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(pred_logits, labels)
   ]
    result = calculate_ner_metrics(
        true_labels,
        predictions
    )
    return result

# 重写 Trainer 类
class CustomTrainer(Trainer):
    def create_optimizer(self):
        if self.optimizer is None:
            # 获取模型参数
            decay_parameters = [
                p for n, p in self.model.named_parameters() if n.endswith("weight")
            ]
            no_decay_parameters = [
                p for n, p in self.model.named_parameters() if n.endswith("bias")
            ]
            # 将参数分组
            optimizer_grouped_parameters = [
                {"params": decay_parameters, "weight_decay": self.args.weight_decay},
                {"params": no_decay_parameters, "weight_decay": 0.0},
            ]
            # 使用 AdamW 作为优化器
            self.optimizer = AdamW(
                optimizer_grouped_parameters, lr=self.args.learning_rate
            )
        return self.optimizer


# 创建训练参数
training_args = TrainingArguments(
    output_dir=OUT_DIR,
    eval_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=BATCH_SIZE,
    per_device_eval_batch_size=BATCH_SIZE,
    num_train_epochs=3,
    weight_decay=0.01,
    load_best_model_at_end=True,
    logging_dir=LOG_DIR,
    save_total_limit=1,
)

# 数据收集器,用于将数据转换为模型可接受的格式
data_collator = DataCollatorForTokenClassification(tokenizer)  

# 定义 Trainer
trainer = CustomTrainer(
    model=model,  # 替换为你的模型
    args=training_args,
    train_dataset=tokenized_datasets['train'],
    eval_dataset=tokenized_datasets['validation'],
    data_collator=data_collator,
    tokenizer=tokenizer,
    compute_metrics=compute_metrics,
)

训练

# 训练 model
trainer.train()

# 保存模型
best_ckpt_path = trainer.state.best_model_checkpoint
best_ckpt_path

评估

trainer.evaluate(eval_dataset=tokenized_datasets['test'])

结果

  • 训练过程
    在这里插入图片描述
  • 测试集
    在这里插入图片描述
  • 预测
    在这里插入图片描述
    完整代码和数据集发布在Github:chinese_ner_place

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

掌握 Spring Boot 中的缓存:技术和最佳实践

缓存是一种用于将经常访问的数据临时存储在更快的存储层&#xff08;通常在内存中&#xff09;中的技术&#xff0c;以便可以更快地满足未来对该数据的请求&#xff0c;从而提高应用程序的性能和效率。在 Spring Boot 中&#xff0c;缓存是一种简单而强大的方法&#xff0c;可以…

Unity 模拟百度地图,使用鼠标控制图片在固定区域内放大、缩小、鼠标左键拖拽移动图片

效果展示&#xff1a; 步骤流程&#xff1a; 1.使用的是UGUI&#xff0c;将下面的脚本拖拽到图片上即可。 using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems;public class CheckImage : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragH…

基础入门-Web应用OSS存储负载均衡CDN加速反向代理WAF防护部署影响

知识点&#xff1a; 1、基础入门-Web应用-防护产品-WAF保护 2、基础入门-Web应用-加速服务-CDN节点 3、基础入门-Web应用-文件托管-OSS存储 4、基础入门-Web应用-通讯服务-反向代理 5、基础入门-Web应用-运维安全-负载均衡 一、演示案例-Web-拓展架构-WAF保护-拦截攻击 原理&a…

指针(上)

目录 内存和地址 指针变量和地址 取地址&#xff08;&&#xff09; 解引用&#xff08;*&#xff09; 大小 类型 意义 const修饰 修饰变量 修饰指针 指针运算 指针- 整数 指针-指针 指针的关系运算 野指针 概念 成因 避免 assert断言 指针的使用 strl…

警惕开源信息成为泄密源头

文章目录 前言一、信息公开需谨慎1、警惕采购招标泄密。2、警惕信息公开泄密。3、警惕社交媒体泄密。 二、泄密风险需严防1、健全制度&#xff0c;明确责任。2、加强管控&#xff0c;严格审查。3、提高意识&#xff0c;谨言慎行。 前言 大数据时代&#xff0c;信息在网络空间发…

LearnOpenGL学习(光照 -- 颜色,基础光照,材质,光照贴图)

光照 glm::vec3 lightColor(0.0f, 1.0f, 0.0f); glm::vec3 toyColor(1.0f, 0.5f, 0.31f); glm::vec3 result lightColor * toyColor; // (0.0f, 0.5f, 0.0f); 说明&#xff1a;当我们把光源的颜色与物体的颜色值相乘&#xff0c;所得到的就是这个物体所反射的颜色。 创建…

面向对象(二)——类和对象(上)

1 类的定义 做了关于对象的很多介绍&#xff0c;终于进入代码编写阶段。 本节中重点介绍类和对象的基本定义&#xff0c;属性和方法的基本使用方式。 【示例】类的定义方式 // 每一个源文件必须有且只有一个public class&#xff0c;并且类名和文件名保持一致&#xff01; …

3GPP R18 LTM(L1/L2 Triggered Mobility)是什么鬼?(三) RACH-less LTM cell switch

这篇看下RACH-less LTM cell switch。 相比于RACH-based LTM,RACH-less LTM在进行LTM cell switch之前就要先知道target cell的TA信息,进而才能进行RACH-less过程,这里一般可以通过UE自行测量或者通过RA过程获取,而这里的RA一般是通过PDCCH order过程触发。根据38.300中的描…

实验13 使用预训练resnet18实现CIFAR-10分类

1.数据预处理 首先利用函数transforms.Compose定义了一个预处理函数transform&#xff0c;里面定义了两种操作&#xff0c;一个是将图像转换为Tensor&#xff0c;一个是对图像进行标准化。然后利用函数torchvision.datasets.CIFAR10下载数据集&#xff0c;这个函数有四个常见的…

P1226 快速幂

【STUACM-算法入门-快速幂】https://www.bilibili.com/video/BV1Hi4y1L7qB?p2&vd_sourcee583d26dc0028b3e6ea220aadf5bc7fe 想先把a的b次方算出来再对p取模是不可能的&#xff0c;因为肯定超出long long 范围。 需要知道&#xff1a;(x*y)mod p (x mod p)*(y mod p) mo…

【力扣热题100】—— Day3.反转链表

你不会永远顺遂&#xff0c;更不会一直年轻&#xff0c;你太安静了&#xff0c;是时候出发了 —— 24.12.2 206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&…

containerd安装

containerd安装 参考资料前置准备Installing containerdInstalling runcInstalling CNI plugins containerd is available as a daemon for Linux and Windows. It manages the complete container lifecycle of its host system, from image transfer and storage to containe…

底部导航栏新增功能按键

场景需求&#xff1a; 在底部导航栏添加power案件&#xff0c;单击息屏&#xff0c;长按 关机 如下实现图 借此需求&#xff0c;需要掌握技能&#xff1a; 底部导航栏如何实现新增、修改、删除底部导航栏流程对底部导航栏部分样式如何修改。 比如放不下、顺序排列、坑点如…

Python 入门教程(2)搭建环境 | 2.4、VSCode配置Node.js运行环境

文章目录 一、VSCode配置Node.js运行环境1、软件安装2、安装Node.js插件3、配置VSCode4、创建并运行Node.js文件5、调试Node.js代码 一、VSCode配置Node.js运行环境 1、软件安装 安装下面的软件&#xff1a; 安装Node.js&#xff1a;Node.js官网 下载Node.js安装包。建议选择L…

火语言RPA流程组件介绍--键盘按键

&#x1f6a9;【组件功能】&#xff1a;模拟键盘按键 配置预览 配置说明 按键 点击后,在弹出的软键盘上选择需要的按键 执行后等待时间(ms) 默认值300,执行该组件后等待300毫秒后执行下一个组件. 输入输出 输入类型 万能对象类型(System.Object)输出类型 万能对象类型…

【人工智能-基础】SVM中的核函数到底是什么

文章目录 支持向量机(SVM)中的核函数详解1. 什么是核函数?核函数的作用:2. 核技巧:从低维到高维的映射3. 常见的核函数类型3.1 线性核函数3.2 多项式核函数3.3 高斯径向基函数(RBF核)4. 总结支持向量机(SVM)中的核函数详解 支持向量机(SVM,Support Vector Machine)…

万字长文解读深度学习——多模态模型BLIP2

&#x1f33a;历史文章列表&#x1f33a; 深度学习——优化算法、激活函数、归一化、正则化 深度学习——权重初始化、评估指标、梯度消失和梯度爆炸 深度学习——前向传播与反向传播、神经网络&#xff08;前馈神经网络与反馈神经网络&#xff09;、常见算法概要汇总 万字长…

大数据开发治理--大数据AI公共数据集分析

本文以分析公共数据集的数据示例&#xff0c;为您展示如何使用DataWorks进行简单数据分析工作。本教程以申请免费资源为例为您展示详细操作步骤&#xff0c;您也可以使用付费资源&#xff0c;操作类似。 教程简介 阿里云DataWorks基于多种大数据引擎&#xff0c;为数据仓库、…

ESP32-S3模组上跑通ES8388(13)

接前一篇文章&#xff1a;ESP32-S3模组上跑通ES8388&#xff08;12&#xff09; 二、利用ESP-ADF操作ES8388 2. 详细解析 上一回解析了es8388_init函数中的第6段代码&#xff0c;本回继续往下解析。为了便于理解和回顾&#xff0c;再次贴出es8388_init函数源码&#xff0c;在…

【Mac】安装Gradle

1、说明 Gradle 运行依赖 JVM&#xff0c;需要先安装JDK&#xff0c;Gradle 与 JDK的版本对应参见&#xff1a;Java Compatibility IDEA的版本也是有要求Gradle版本的&#xff0c;二者版本对应关系参见&#xff1a;Third-Party Software and Licenses 本次 Gradle 安装版本为…