NLP实战命名实体识别

文章目录

  • 一、导入相关包
  • 二、加载数据集
  • 三、数据预处理
  • 四、创建模型
  • 五、创建评估函数
  • 六、配置训练参数
  • 七、创建训练器
  • 八、模型训练
  • 九、模型预测

一、导入相关包

  • DataCollatorForTokenClassification 用于 Token 级别的分类任务
import evaluate
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForTokenClassification, 
	TrainingArguments, Trainer, DataCollatorForTokenClassification

二、加载数据集

# 如果可以联网,直接使用load_dataset进行加载,cache_dir将数据缓冲到该位置
ner_datasets = load_dataset("peoples_daily_ner", cache_dir="./data")
# 如果无法联网,则使用下面的方式加载数据集
# from datasets import DatasetDict
# ner_datasets = DatasetDict.load_from_disk("ner_data")
ner_datasets

'''
DatasetDict({
    train: Dataset({
        features: ['id', 'tokens', 'ner_tags'],
        num_rows: 20865
    })
    validation: Dataset({
        features: ['id', 'tokens', 'ner_tags'],
        num_rows: 2319
    })
    test: Dataset({
        features: ['id', 'tokens', 'ner_tags'],
        num_rows: 4637
    })
})
'''

  • 查看一条数据
ner_datasets["train"][0]
'''
{'id': '0',
 'tokens': ['海',
  '钓',
  '比',
  '赛',
  '地',
  '点',
  '在',
  '厦',
  '门',
  '与',
  '金',
  '门',
  '之',
  '间',
  '的',
  '海',
  '域',
  '。'],
 'ner_tags': [0, 0, 0, 0, 0, 0, 0, 5, 6, 0, 5, 6, 0, 0, 0, 0, 0, 0]}
'''
  • 查看特征信息
ner_datasets["train"].features
'''
{'id': Value(dtype='string', id=None),
 'tokens': Sequence(feature=Value(dtype='string', id=None), length=-1, id=None),
 'ner_tags': Sequence(feature=ClassLabel(names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC'], id=None), length=-1, id=None)}
'''
  • 获取标签信息
label_list = ner_datasets["train"].features["ner_tags"].feature.names
label_list
'''
['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC']
'''

三、数据预处理

tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-macbert-base")

tokenizer(ner_datasets["train"][0]["tokens"], is_split_into_words=True)   
# 对于已经做好tokenize的数据,要指定is_split_into_words参数为True
'''
{'input_ids': [101, 3862, 7157, 3683, 6612, 1765, 4157, 1762, 1336, 7305, 680, 7032, 7305, 722, 7313, 4638, 3862, 1818, 511, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
'''

否则
tokenizer(ner_datasets["train"][0]["tokens"], is_split_into_words=False) 
'''
{'input_ids': [[101, 3862, 102], [101, 7157, 102], [101, 3683, 102], [101, 6612, 102], [101, 1765, 102], [101, 4157, 102], [101, 1762, 102], [101, 1336, 102], [101, 7305, 102], [101, 680, 102], [101, 7032, 102], [101, 7305, 102], [101, 722, 102], [101, 7313, 102], [101, 4638, 102], [101, 3862, 102], [101, 1818, 102], [101, 511, 102]], 'token_type_ids': [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 'attention_mask': [[1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1]]}
'''

- attention_mask取值为1指的是不需要做mask

  • 字词分词的时候(例如下面的英文数据)会存在一个 token 对应若干 id,所以不能简单的一一对应
res = tokenizer("interesting word")
res
'''
{'input_ids': [101, 10673, 12865, 12921, 8181, 8681, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1]}
'''
  • 解决方案:word_ids(),实现标签映射
res.word_ids()
'''
[None, 0, 0, 0, 0, 1, None] # None代表的是特殊token
'''

  • 实现label和token的一一对应
# 借助word_ids 实现标签映射
def process_function(examples):
    tokenized_exmaples = tokenizer(examples["tokens"], max_length=128, truncation=True, is_split_into_words=True)
    # 实现label和token的一一对应
    labels = []
    for i, label in enumerate(examples["ner_tags"]):
        word_ids = tokenized_exmaples.word_ids(batch_index=i)
        label_ids = []
        for word_id in word_ids:
            if word_id is None:
                label_ids.append(-100) # -100 softmax后会置为0
            else:
                label_ids.append(label[word_id])
        labels.append(label_ids)
    tokenized_exmaples["labels"] = labels
    return tokenized_exmaples
tokenized_datasets = ner_datasets.map(process_function, batched=True)
tokenized_datasets
'''
DatasetDict({
    train: Dataset({
        features: ['id', 'tokens', 'ner_tags', 'input_ids', 'token_type_ids', 'attention_mask', 'labels'],
        num_rows: 20865
    })
    validation: Dataset({
        features: ['id', 'tokens', 'ner_tags', 'input_ids', 'token_type_ids', 'attention_mask', 'labels'],
        num_rows: 2319
    })
    test: Dataset({
        features: ['id', 'tokens', 'ner_tags', 'input_ids', 'token_type_ids', 'attention_mask', 'labels'],
        num_rows: 4637
    })
})
'''

print(tokenized_datasets["train"][0])
'''
{'id': '0', 
'tokens': ['海', '钓', '比', '赛', '地', '点', '在', '厦', '门', '与', '金', '门', '之', '间', '的', '海', '域', '。'], 
'ner_tags': [0, 0, 0, 0, 0, 0, 0, 5, 6, 0, 5, 6, 0, 0, 0, 0, 0, 0], 'input_ids': [101, 3862, 7157, 3683, 6612, 1765, 4157, 1762, 1336, 7305, 680, 7032, 7305, 722, 7313, 4638, 3862, 1818, 511, 102], 
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 
'labels': [-100, 0, 0, 0, 0, 0, 0, 0, 5, 6, 0, 5, 6, 0, 0, 0, 0, 0, 0, -100]}
'''

四、创建模型

  • 对于所有的非二分类任务,切记要指定num_labels,否则就会device错误
# 对于所有的非二分类任务,切记要指定num_labels,否则就会device错误
model = AutoModelForTokenClassification.from_pretrained("hfl/chinese-macbert-base", 
                                                        num_labels=len(label_list))



model.config.num_labels
'''
7
'''

五、创建评估函数

# seqeval = evaluate.load("seqeval_metric.py") # 本地的加载方式
!pip install seqeval
seqeval = evaluate.load("seqeval")
seqeval

'''
Args:
    predictions: List of List of predicted labels (Estimated targets as returned by a tagger)
    references: List of List of reference labels (Ground truth (correct) target values)
    suffix: True if the IOB prefix is after type, False otherwise. default: False
    scheme: Specify target tagging scheme. Should be one of ["IOB1", "IOB2", "IOE1", "IOE2", "IOBES", "BILOU"].
        default: None
    mode: Whether to count correct entity labels with incorrect I/B tags as true positives or not.
        If you want to only count exact matches, pass mode="strict". default: None.
    sample_weight: Array-like of shape (n_samples,), weights for individual samples. default: None
    zero_division: Which value to substitute as a metric value when encountering zero division. Should be on of 0, 1,
        "warn". "warn" acts as 0, but the warning is raised.

Returns:
    'scores': dict. Summary of the scores for overall and per type
        Overall:
            'accuracy': accuracy,
            'precision': precision,
            'recall': recall,
            'f1': F1 score, also known as balanced F-score or F-measure,
        Per type:
            'precision': precision,
            'recall': recall,
            'f1': F1 score, also known as balanced F-score or F-measure
Examples:

    >>> predictions = [['O', 'O', 'B-MISC', 'I-MISC', 'I-MISC', 'I-MISC', 'O'], ['B-PER', 'I-PER', 'O']]
    >>> references = [['O', 'O', 'O', 'B-MISC', 'I-MISC', 'I-MISC', 'O'], ['B-PER', 'I-PER', 'O']]
    >>> seqeval = evaluate.load("seqeval")
    >>> results = seqeval.compute(predictions=predictions, references=references)
    >>> print(list(results.keys()))
    ['MISC', 'PER', 'overall_precision', 'overall_recall', 'overall_f1', 'overall_accuracy']
    >>> print(results["overall_f1"])
    0.5
    >>> print(results["PER"]["f1"])
    1.0
""", stored examples: 0)
'''
import numpy as np

def eval_metric(pred):
    # 返回的predictions只是logits
    predictions, labels = pred
    predictions = np.argmax(predictions, axis=-1)

    # 将id转换为原始的字符串类型的标签
    true_predictions = [
        [label_list[p] for p, l in zip(prediction, label) if l != -100] # simple
        for prediction, label in zip(predictions, labels) # batch
    ]

    true_labels = [
        [label_list[l] for p, l in zip(prediction, label) if l != -100] # simple
        for prediction, label in zip(predictions, labels) # batch
    ]

    result = seqeval.compute(predictions=true_predictions, references=true_labels, mode="strict", scheme="IOB2")

    return {
        "f1": result["overall_f1"]
    }

六、配置训练参数

huggingface transformers使用指南之二——方便的trainer
详解Hugging Face Transformers的TrainingArguments_若石之上的博客-CSDN博客
LLM大模型之Trainer以及训练参数

!pip install accelerate # Using the `Trainer` with `PyTorch` requires `accelerate>=0.20.1`
args = TrainingArguments(
    output_dir="models_for_ner", # 断点和运行日志记录保存文件夹
    per_device_train_batch_size=64, 
    per_device_eval_batch_size=128,
    evaluation_strategy="epoch", # 评价策略,需要和保存策略保持一致
    save_strategy="epoch",
    metric_for_best_model="f1", # 选取最好模型的指标,这里是评价函数返回的字典的key f1
    load_best_model_at_end=True, # 训练完后加载最好的模型
    logging_steps=50, # 日志打印步数
    num_train_epochs=3
)


# 如果使用evaluation_strategy="steps",
# 则需要指定eval_steps参数,否则eval_steps=logging_steps
args = TrainingArguments(
    output_dir="models_for_ner", # 断点和运行记录保存文件夹
    per_device_train_batch_size=64, 
    per_device_eval_batch_size=128,
    evaluation_strategy="steps", # 评价策略,需要和保存策略保持一致
    save_strategy="steps",
    metric_for_best_model="f1", # 选取最好模型的指标,这里是评价函数返回的字典的key f1
    load_best_model_at_end=True, # 训练完后加载最好的模型
    logging_steps=50, # 日志打印步数
    num_train_epochs=1
)
  • 采用第二种参数:

image.png


七、创建训练器

  • 如果只有训练集还想实现在训练的时候评估的效果,则只需要将eval_dataset=tokenized_datasets[“train”] 即可
trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    compute_metrics=eval_metric,
    data_collator=DataCollatorForTokenClassification(tokenizer=tokenizer)
)

八、模型训练

trainer.train()
  • 模型评估
trainer.evaluate() # 默认使用trainer中指定的eval_dataset

# 也可以更换其他数据集
trainer.evaluate(eval_dataset=tokenized_datasets["test"])

九、模型预测

res = trainer.predict(tokenized_datasets["test"])
res.predictions.argmax(axis=-1)
'''
array([[0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ...,
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0]])
'''
res.predictions.argmax(axis=-1)[0]
'''
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 5, 5, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
'''
from transformers import pipeline

# 使用pipeline进行推理,要指定id2label
model.config.id2label = {idx: label for idx, label in enumerate(label_list)}
model.config
'''
BertConfig {
  "_name_or_path": "hfl/chinese-macbert-base",
  "architectures": [
    "BertForTokenClassification"
  ],
  "attention_probs_dropout_prob": 0.1,
  "classifier_dropout": null,
  "directionality": "bidi",
  "gradient_checkpointing": false,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "id2label": {
    "0": "O",
    "1": "B-PER",
    "2": "I-PER",
    "3": "B-ORG",
    "4": "I-ORG",
    "5": "B-LOC",
    "6": "I-LOC"
  },
  "initializer_range": 0.02,
  "intermediate_size": 3072,
  "label2id": {
    "LABEL_0": 0,
    "LABEL_1": 1,
    "LABEL_2": 2,
    "LABEL_3": 3,
    "LABEL_4": 4,
    "LABEL_5": 5,
    "LABEL_6": 6
  },
  "layer_norm_eps": 1e-12,
  "max_position_embeddings": 512,
  "model_type": "bert",
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "pad_token_id": 0,
  "pooler_fc_size": 768,
  "pooler_num_attention_heads": 12,
  "pooler_num_fc_layers": 3,
  "pooler_size_per_head": 128,
  "pooler_type": "first_token_transform",
  "position_embedding_type": "absolute",
  "torch_dtype": "float32",
  "transformers_version": "4.35.0",
  "type_vocab_size": 2,
  "use_cache": true,
  "vocab_size": 21128
}
'''
# 如果模型是基于GPU训练的,那么推理时要指定device
# 对于NER任务,可以指定aggregation_strategy为simple,得到具体的实体的结果,而不是token的结果
ner_pipe = pipeline("token-classification", 
                    model=model, 
                    tokenizer=tokenizer, 
                    device=0, 
                    aggregation_strategy="simple")
res = ner_pipe("小明在北京上班")
res
'''
Asking to truncate to max_length but no maximum length is provided and the model has no predefined maximum length. Default to no truncation.
[{'entity_group': 'PER',
  'score': 0.44049227,
  'word': '明',
  'start': 1,
  'end': 2},
 {'entity_group': 'LOC',
  'score': 0.9994525,
  'word': '北 京',
  'start': 3,
  'end': 5}]
'''


# 可以用model_max_length参数解决Asking to truncate to max_length but no maximum length is provided and the model has no predefined maximum length. Default to no truncation.
tokenizer = AutoTokenizer.from_pretrained('google/bert_uncased_L-4_H-256_A-4', 
                                          model_max_length=512)


# 指定aggregation_strategy为simple
ner_pipe = pipeline("token-classification", model=model, tokenizer=tokenizer, device=0)
res = ner_pipe("小明在北京上班")
res
'''
[{'entity': 'I-PER',
  'score': 0.44049227,
  'index': 2,
  'word': '明',
  'start': 1,
  'end': 2},
 {'entity': 'B-LOC',
  'score': 0.99940526,
  'index': 4,
  'word': '北',
  'start': 3,
  'end': 4},
 {'entity': 'I-LOC',
  'score': 0.9994997,
  'index': 5,
  'word': '京',
  'start': 4,
  'end': 5}]
'''
# 根据start和end取实际的结果
ner_result = {}
x = "小明在北京上班"
for r in res:
    if r["entity_group"] not in ner_result:
        ner_result[r["entity_group"]] = []
    ner_result[r["entity_group"]].append(x[r["start"]: r["end"]])

ner_result
'''
{'PER': ['明'], 'LOC': ['北京']}
'''

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

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

相关文章

tensorboard报错解决:No dashboards are active for the current data set

版本:tensorboard 2.10.0 问题:文件夹下明明有events文件,但用tensorboard命令却无法显示。 例如: 原因:有可能是文件路径太长了,导致系统无法读取文件。在win系统中规定,目录的绝对路径不得超…

WebSocket网络协议

二十六、WebSocket 26.1 介绍 WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。 HHTP协议和WebSocket协议对比&#xff…

零基础算法还原01以及使用python和JS还原C++部分细节

题目一 使用jadx 打开algorithmbase_10.apk JAVA层 使用Frida获取先生成的随机字符串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // 定义一个名为hook_js的JavaScript函数 function hook_js(){ // 使用Java.perform()函数来…

微信支付服务商消费者投诉及时处理与商户违规及时通知,支持多服务商

大家好,我是小悟 微信直连商户处理消费者投诉的功能解决了很多商户对于投诉处理不及时而导致商户号出现异常的问题,可以说解决了实实在在的问题。 很多小伙伴私信说自己是服务商角色,也需要微信支付服务商处理消费者投诉的功能,…

JS移动端触屏事件

在我们PC端中有许多的事件,那我们在移动端有没有事件呢?让我为大家介绍一下移动端常用的事件,触屏事件 触屏事件 touch (也称触摸事件),Android 和IOS 都有 touch 对象代表一个触摸点。触摸点可能是一根手指,也可能是一…

加班把数据库重构完毕

加班把数据库重构完毕 本文的数据库重构是基于 clickhouse 时序非关系型的数据库。该数据库适合存储股票数据,速度快,一般查询都是 ms 级别,不需要异步查询更新界面 ui。 达到目标效果:数据表随便删除,重新拉数据以及指…

Matter学习笔记(2)——数据模型和设备类型

一、设备数据模型 Matter 中的设备具有明确定义的 数据模型(DM),它是设备功能的分层建模。使用 属性(Attribute)、命令(Command) 和 事件(Event) 的概念描述 Matter 节点支持的远程操作,并分组为称为集群的逻辑块。Matter 应用集群规范中包含的集群具有…

POJ 3254 Corn Fields 状态压缩DP(铺砖问题)

一、题目大意 我们要在N * M的田地里种植玉米,有如下限制条件: 1、对已经种植了玉米的位置,它的四个相邻位置都无法继续种植玉米。 2、题目中有说一些块无论如何,都无法种植玉米。 求所有种植玉米的方案数(不种植也…

【Java 进阶篇】JQuery DOM操作:轻松驾驭网页内容的魔法

在前端开发的舞台上,DOM(文档对象模型)是我们与网页内容互动的关键。而JQuery作为一个轻量级的JavaScript库,为我们提供了便捷而强大的DOM操作工具。在本篇博客中,我们将深入探讨JQuery的DOM内容操作,揭开这…

外星人笔记本键盘USB协议逆向

前言 我朋友一台 dell g16 购买时直接安装了linux系统,但是linux上没有官方的键盘控制中心,所以无法控制键盘灯光,于是我就想着能不能逆向一下键盘的协议,然后自己写一个控制键盘灯光的程序。我自己的外星人笔记本是m16&#xff…

阿里系APP崩了?回应来了!

最近,阿里云遭遇了一场可怕的疑似故障,引起了广泛的关注和热议。各种消息纷传,阿里云盘崩了,淘宝又崩了,闲鱼也崩了,连钉钉也不幸中招。这一系列故障让人不禁发问:阿里系的APP都崩了&#xff0c…

【Unity每日一记】“调皮的协程”,协程和多线程的区别在哪里

👨‍💻个人主页:元宇宙-秩沅 👨‍💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍💻 本文由 秩沅 原创 👨‍💻 收录于专栏:uni…

msvcp120.dll丢失的6种解决方法,教你如何修复dll文件丢失

“找不到msvcp120dll,无法继续执行代码的6个修复方案”。我相信很多朋友在运行某些程序时,可能会遇到这样的错误提示:“找不到msvcp120dll,无法继续执行代码”。那么,msvcp120dll究竟是什么?为什么会丢失呢&#xff1f…

发布订阅者模式(观察者模式)

目录 应用场景 1.结构 2.效果 3.代码 3.1.Main方法的类【ObserverPatternExample】 3.2.主题(接口)【Subject】 3.3.观察者(接口)【Observer】 3.4.主题(实现类)【ConcreteSubject】 3.5.观察者&a…

qemu 之 uboot、linux 启动

目录 编译uboot、kernel 编译启动从 uboot 中引导启动 linux注参考 本文主要说明 arm64 在 qemu 上的相关启动。 编译 使用的是 qemu-8.1.1 版本,编译命令如下: ../configure --cc/usr/local/bin/gcc --prefix/home/XXX/qemu_out --enable-virtfs --enable-slir…

Three.js——基于原生WebGL封装运行的三维引擎

文章目录 前言一、什么是WebGL?二、Three.js 特性 前言 Three.js中文官网 Three.js是基于原生WebGL封装运行的三维引擎,在所有WebGL引擎中,Three.js是国内文资料最多、使用最广泛的三维引擎。既然Threejs是一款WebGL三维引擎,那么…

RAG相关内容介绍

本文记录在查找RAG相关内容时所整合的一些资料与内容,还有一个组会报告的PPT 文章目录 定义LLM的知识更新难题 RAG是什么?-“开卷考试”RAG原理与技术RAG技术细节一、数据索引• 数据提取• 分块(Chunking)分块方式确定应用程序的…

摊牌 了,我不藏了,上线了一年多的网站还是广而告之吧!

大家好,我是大明哥,一个专注「死磕 Java」系列文章创作的程序员。 本文已收录到我的小站:https://skjava.com。 从去年开始一直有小伙伴问我,大明哥,你的网站怎么打不开了?我只能苦涩地跟他说,没…

DevChat智能编程助手:小白也能轻松上手的开发利器

DevChat智能编程助手:小白也能轻松上手的开发利器 一、DevChat介绍1.1 DevChat简介1.2 DevChat特点1.3 DevChat官网 二、注册DevChat账号2.1 访问DevChat官网2.2 注册账号2.3 复制Access Key2.4 登录DevChat 三、安装DevChat3.1 打开VS Code软件3.2 安装DevChat3.3 …

(免费领源码)java#ssm#mysql在线学习平台85204-计算机毕业设计项目选题推荐

摘 要 随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,在线学习平台当然也不能排除在外。在线学习平台是以实际运用为开发背景,运用软件工程原理和开发方法&#x…