当前位置:   article > 正文

山东大学软件学院2024创新实训项目VCR个人博客_qwen2 72b 硬件要求

qwen2 72b 硬件要求

        本博客为山东大学软件学院2024创新项目实训,可视化课程知识问答系统(VCR)的个人博客。VCR即Visualization(可视化)、Chat(对话)、Retrieval&Recommend(检索和推荐),因此本博客将围绕以上功能点展开工作记录。

2024.3.31 数据范围的确定和数据来源的调研

        本项目主要需要实现知识图谱的构建和大模型对话功能,本周针对知识图谱构建进行数据集调研。

数据特征分析

        由于本项目是在课程知识库上建立的大模型项目,我们希望选择一些特定的软件工程相关专业科目进行知识库的建立。由于大语言模型具有强大的自然语言理解、自然语言推理、自然语言表示和文本生成能力,我们理所应当地选取了计算机网络、软件工程、面向对象等文本数据丰富的课程作为数据采样领域。

        课程知识具有文本数据量大但描述性文字较多,章节与章节、知识点与知识点之间的关系错综复杂的特点。因此在从文本中获取知识时往往面临难以建立一个描述知识点间关系的框架以及难以定位某个知识点在大量文本中的位置的困难。

        通过学习经验来看,知识点间往往存在大量的相互关联关系包含关系,因此可以通过从关联关系网的角度入手建立知识框架,从包含关系树的角度来建立知识索引,加速知识的检索和定位,因此,我们需要获取大量具有以上两种特征的数据以进行模型的训练和微调。

课本原文

        使用课本原文的概念定义和大段描述、关键概念词表、目录结构可以提取出大量的(概念-描述)对作为实体数据作为命名实体识别的训练数据

课本的描述文本以及概念实体(粗体)样例

        使用课本中的附录索引可以获取大量高质量实体清单

课本附录索引

 百度百科

        百度百科是类似于wiki pedia的中文词条数据库,存储了大量的概念词条信息,针对每个词条都有详细的定义、描述、相关概念、分类等标签信息,可以作为一大数据来源

计算机网络的词条页面

        其中词条标题和第一段落的文本描述可以构成(实体-描述)对,目录栏列举了相关的词条实体并给出了一些实体间关系可以作为(实体-关系-实体)三元组用于训练实体关系提取模型。

计算机网络的词条目录栏

         由于词条还包含详细的描述文本,其中使用超链接标注和定位了相关实体,可以借此作为判断两个实体间关系强弱的依据,以及以特定词条为原点进行递归式的数据爬取。

详细的词条文本与其中的超链接

复习笔记&资料

        在网络和校园资源库可以获取大量的课程知识笔记和复习资料,这些文档对课程知识进行了一定程度的整理和精简,已经具有部分知识结构。

智库复习笔记

         通过观察可以发现笔记类资料整理的知识往往是(Q-A)和(实体-包含-实体)形式的,通俗的说就是问题+答案或某个问题/实体包含哪些实体和概念,通过这些结构化的数据我们可以提取出大量的问答数据和目录关系数据,可以在训练大语言模型问答和构建知识图谱中发挥作用。

2024.4.7 pdf数据获取和数据清洗

        本周的主要工作是研究如何将pdf文件进行文本提取和数据清洗

环境部署

安装rapidocr_pdf库

pip install rapidocr_pdf

pdf文本提取

将计算机网络第五版pdf中的文字提取到result.json

  1. from rapidocr_pdf import PDFExtracter
  2. import json
  3. pdf_extracter = PDFExtracter(print_verbose=True, use_cuda=True)
  4. pdf_path = "computer_networks_5.pdf"
  5. texts = pdf_extracter(pdf_path, force_ocr=False)
  6. print(texts)
  7. with open('./result.json', 'w', encoding='utf-8') as f:
  8. json.dump(texts, f)

注意如果提取的是中文数据,在保存到json时需要设置ensure_ascii为False

  1. import json
  2. src_f = open('./result.json')
  3. out_f = open('./processed_result.json', 'w', encoding='utf-8')
  4. result = json.load(src_f)
  5. # print(json.dumps(result))
  6. json.dump(result, out_f, ensure_ascii=False)

原始的pdf格式

提取后的json数据

        rapidocr_pdf会将提取的每页数据放到一个list中,其中每个元素都是一个list,形如["页码", "文本", "置信度"]

数据清洗

        初步提取的文档数据中存在大量的换行符、空格、特殊字符、公式、页码、等影响数据集构建和模型训练的噪声数据,因此需要额外步骤进行数据清洗。

        我采用的数据清洗手段有:去除特殊字符、分割段落和句子并去除长句、去除连续的数字字符、对特殊文本进行正则匹配、使用大模型进行数据过滤。

去除页码

        观察发现rapidocr_pdf会将页码进行识别提取,因此需要根据json的页码将对应页面中的页码数据去除。

  1. import json
  2. def remove_numbers_from_text(data_list):
  3. result = []
  4. for item in data_list:
  5. number = str(item[0])
  6. text = item[1]
  7. processed_text = text.replace(number, '')
  8. result.append(processed_text)
  9. return result
  10. # 从json文件中读取数据
  11. with open('data.json', 'r') as file:
  12. data = json.load(file)
  13. # 处理数据
  14. processed_data = remove_numbers_from_text(data)
  15. # 将处理后的文本拼接在一起
  16. combined_text = ' '.join(processed_data)
  17. # 输出到文件
  18. with open('output.txt', 'w') as file:
  19. file.write(combined_text)

去除特殊字符

        首先读入json文件,将文本中的\n,\t,连续的空格等字符去除,

  1. import re
  2. def remove_special_chars(text):
  3. text = re.sub(r'\n', '', text)
  4. text = re.sub(r'\t', '', text)
  5. text = re.sub(r'\s+', ' ', text)
  6. return text

特殊文本正则匹配

将“(比如......)”、“图 2-35”这样的无意义文本去除

  1. import re
  2. def remove_data(string):
  3. # 匹配形如"(比如...)"的数据
  4. pattern1 = r'\(.*?\)'
  5. # 匹配形如"图 2-35"的数据
  6. pattern2 = r'图 \d+-\d+'
  7. # 去除"(比如...)"的数据
  8. string = re.sub(pattern1, '', string)
  9. # 去除"图 2-35"的数据
  10. string = re.sub(pattern2, '', string)
  11. return string
  12. # 测试示例
  13. string = '这是一个示例(比如...), 这是另一个示例(比如...), 这是图 2-35的示例'
  14. result = remove_data(string)
  15. print(result)

使用大模型进行数据清洗

我们部署了ChatGLM3-6B和Qwen-72B-Chat-Int4模型进行数据集的清洗工作

1. 环境准备

1.1 硬件需求
  • GPU: 至少一块支持 CUDA 的 NVIDIA GPU(我们使用了8张GTX3090显卡,每张有24GB的显存,足以支持72B的千问模型)
  • 内存: 至少 64 GB
  • 存储: SSD, 至少 500 GB 可用空间
1.2 软件需求
  • 操作系统: Ubuntu 20.04 或以上
  • CUDA: 11.3 或以上
  • NVIDIA 驱动: 最新版本
  • Python: 3.8 或以上
  • Pip: 最新版本

2. 部署 ChatGLM3-6B 模型

2.1 安装依赖

首先,确保系统中安装了必要的软件和驱动程序。然后,创建一个虚拟环境并激活它:

  1. sudo apt update
  2. sudo apt install -y python3-venv
  3. python3 -m venv chatglm3_env
  4. source chatglm3_env/bin/activate

安装必要的 Python 库:

  1. pip install torch torchvision torchaudio
  2. pip install transformers
  3. pip install accelerate
2.2 下载和加载 ChatGLM3-6B 模型

使用 transformers 库下载和加载模型:

  1. from transformers import AutoTokenizer, AutoModel
  2. tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b")
  3. model = AutoModel.from_pretrained("THUDM/chatglm-6b", torch_dtype=torch.float16).cuda()
2.3 推理示例

运行简单的推理测试以确保模型正常工作:

  1. input_text = "你好,今天的天气怎么样?"
  2. input_ids = tokenizer.encode(input_text, return_tensors="pt").cuda()
  3. with torch.no_grad():
  4. outputs = model.generate(input_ids, max_length=50)
  5. response = tokenizer.decode(outputs[0], skip_special_tokens=True)
  6. print(response)

3. 部署 Qwen-72B-Chat-Int4 模型

3.1 安装依赖

创建另一个虚拟环境并激活它:

  1. python3 -m venv qwen72_env
  2. source qwen72_env/bin/activate

安装必要的 Python 库:

  1. pip install torch torchvision torchaudio
  2. pip install transformers
  3. pip install accelerate
3.2 下载和加载 Qwen-72B-Chat-Int4 模型

使用 transformers 库下载和加载模型:

  1. from transformers import AutoTokenizer, AutoModelForCausalLM
  2. tokenizer = AutoTokenizer.from_pretrained("qwen/Qwen-72B-Chat-Int4")
  3. model = AutoModelForCausalLM.from_pretrained("qwen/Qwen-72B-Chat-Int4", torch_dtype=torch.float16).cuda()
3.3 推理示例

运行简单的推理测试以确保模型正常工作:

  1. input_text = "请介绍一下自己。"
  2. input_ids = tokenizer.encode(input_text, return_tensors="pt").cuda()
  3. with torch.no_grad():
  4. outputs = model.generate(input_ids, max_length=50)
  5. response = tokenizer.decode(outputs[0], skip_special_tokens=True)
  6. print(response)

4. 使用大模型+任务提示的方式进行数据清洗

4.1 代码展示
  1. from transformers import AutoTokenizer, AutoModel, AutoModelForCausalLM
  2. from modelscope import AutoTokenizer, AutoModelForCausalLM
  3. import time
  4. import json
  5. from tqdm import tqdm
  6. import re
  7. def split_chinese_text(text):
  8. """
  9. 将中文文本按句子划分成列表
  10. :param text: 中文文本
  11. :return: 划分后的句子列表
  12. """
  13. # 去除原句中的换行符
  14. text = text.replace('\n', '')
  15. # 使用正则表达式划分句子,以句号、问号和感叹号作为句子分隔符
  16. sentences = re.split(r'([。?!])', text)
  17. # 去除空白句子
  18. sentences = [sentence.strip() for sentence in sentences if sentence.strip()]
  19. # 将分隔符与前一句合并
  20. merged_sentences = []
  21. for i in range(0, len(sentences), 2):
  22. if i + 1 < len(sentences):
  23. merged_sentences.append(sentences[i] + sentences[i+1])
  24. else:
  25. merged_sentences.append(sentences[i])
  26. return merged_sentences
  27. # 1.保留内容为描述、陈述事实、定义、解释的句子。
  28. # 2、去除大段公式、无意义符号、其他语义信息不明的文本。
  29. # 3、保留句子格式和标点。
  30. # 4、除处理后的文本不要输出任何其他内容。
  31. # 输入:比特填充还确保了转换的最小密度,这将有助于物理层保持同步。正是由于这个原因,USB(通用串行总线)采用了比特填充技术。当接收方看到5 个连续入境比特l ,并且后面紧跟一个比特0,它就自动剔除(即删除)比特0。比特填充和字节填充一样,对两台计算机上的网络层是完全透明的。如果用户数据中包含了标志模式。1111110,这个标志传输出去的是011111010,但在接收方内存中存储还是01111110 。图3-5 给出了一个比特填充的例子。0 I ro I I I I I I I I I I I I I I I I 0 0 I 0 (a) 飞---....... L ... ./’ 填充比特(b) 0110 I JI JI I I I JI I I JI I I 00 I 0 (c) 图3-5比特填充(a )原始数据g(b )出现在线路上的数据:(c )存储在接收方内存中的数据。有了比特填充技术,两帧之间的边界可以由标志模式明确区分。
  32. # 输出:比特填充还确保了转换的最小密度,这将有助于物理层保持同步。正是由于这个原因,USB(通用串行总线)采用了比特填充技术。当接收方看到5 个连续入境比特l ,并且后面紧跟一个比特0,它就自动剔除(即删除)比特0。比特填充和字节填充一样,对两台计算机上的网络层是完全透明的。如果用户数据中包含了标志模式。1111110,这个标志传输出去的是011111010,但在接收方内存中存储还是01111110 。有了比特填充技术,两帧之间的边界可以由标志模式明确区分。
  33. # 你的任务是将给定的文本做以下处理并输出处理后的句子:保留内容为描述、陈述事实、定义、解释的句子。去除大段公式、无意义符号、其他语义信息不明的文本。保留句子格式和标点。除了原始文本不要输出任何其他内容。
  34. def process_batch_sentence(model, tokenizer, batch_sentence, history):
  35. simple_prompt = f"""
  36. 将不严谨的语言和乱码去除:
  37. """
  38. task__example_prompt = f"""
  39. 你的任务是将输入文本中语义不明的文字和符号去除并输出,不要输出除原文外的任何其他内容:
  40. 以下是两个例子:
  41. 样例输入:比特填充还确保了转换的最小密度,这将有助于物理层保持同步。
  42. 样例输出:比特填充还确保了转换的最小密度,这将有助于物理层保持同步。
  43. 样例输入:图3-5 给出了一个比特填充的例子。0 I ro II I I I 0 (a) 飞---....... L ... ./’ 填充比特(b) 0110 I JI I 00 I 0 有了比特填充技术,两帧之间的边界可以由标志模式明确区分。
  44. 样例输出:有了比特填充技术,两帧之间的边界可以由标志模式明确区分。
  45. 你的输入:
  46. """
  47. dialog_prompt = f"""
  48. <|system|>
  49. 你的任务是将给定的文本做以下处理并输出处理后的句子:保留内容为描述、陈述事实、定义、解释的句子。去除大段公式、无意义符号、其他语义信息不明的文本。保留句子格式和标点。除处理后的文本不要输出任何其他内容。
  50. <|user|>
  51. 比特填充还确保了转换的最小密度,这将有助于物理层保持同步。正是由于这个原因,USB(通用串行总线)采用了比特填充技术。当接收方看到5 个连续入境比特l ,并且后面紧跟一个比特0,它就自动剔除(即删除)比特0。比特填充和字节填充一样,对两台计算机上的网络层是完全透明的。如果用户数据中包含了标志模式。
  52. <|assistant|>
  53. 比特填充还确保了转换的最小密度,这将有助于物理层保持同步。正是由于这个原因,USB(通用串行总线)采用了比特填充技术。当接收方看到5 个连续入境比特l ,并且后面紧跟一个比特0,它就自动剔除(即删除)比特0。比特填充和字节填充一样,对两台计算机上的网络层是完全透明的。如果用户数据中包含了标志模式。
  54. <|user|>
  55. 1111110,这个标志传输出去的是011111010,但在接收方内存中存储还是01111110 。图3-5 给出了一个比特填充的例子。0 I ro I I I I I I I I I I I I I I I I 0 0 I 0 (a) 飞---....... L ... ./’ 填充比特(b) 0110 I JI JI I I I JI I I JI I I 00 I 0 (c) 图3-5比特填充(a )原始数据g(b )出现在线路上的数据:(c )存储在接收方内存中的数据。有了比特填充技术,两帧之间的边界可以由标志模式明确区分。
  56. <|assistant|>
  57. 1111110,这个标志传输出去的是011111010,但在接收方内存中存储还是01111110 。有了比特填充技术,两帧之间的边界可以由标志模式明确区分。
  58. <|user|>
  59. """
  60. system_prompt = f"""
  61. 将接下来输入文本中的乱码删除,其余内容不变直接输出,不要输出任何多余信息
  62. """
  63. prompt_history = [
  64. ('另外还有一种可能,将NAT 盒子集成到路由器或者ADSL 调制解调器中。转换之后的数据包IP=l 98.60.42.12 端口号=3344客户路由,、器和LAN//、NAT盒子/防火墙『------·,,,,、\客户办公室边界图5-55NAT 盒子的放置和操作过程(逼向Internet)ISP路由器。至此,我们掩盖了一个微小但至关重要的细节z 当应答数据包返回时(比如从Web 服务器返回的应答包〉,本质上它的目标地址是198.60.42.12 ,那么,NAT 盒子如何知道该用哪一个地址来替代呢', '另外还有一种可能,将NAT 盒子集成到路由器或者ADSL 调制解调器中。至此,我们掩盖了一个微小但至关重要的细节z 当应答数据包返回时(比如从Web 服务器返回的应答包〉,本质上它的目标地址是198.60.42.12 ,那么,NAT 盒子如何知道该用哪一个地址来替代呢'),
  65. ('1111110,这个标志传输出去的是011111010,但在接收方内存中存储还是01111110 。图3-5 给出了一个比特填充的例子。0 I ro I I I I I I I I I I I I I I I I 0 0 I 0 (a) 飞---....... L ... ./’ 填充比特(b) 0110 I JI JI I I I JI I I JI I I 00 I 0 (c) 图3-5比特填充(a )原始数据g(b )出现在线路上的数据:(c )存储在接收方内存中的数据。有了比特填充技术,两帧之间的边界可以由标志模式明确区分。', '1111110,这个标志传输出去的是011111010,但在接收方内存中存储还是01111110 。有了比特填充技术,两帧之间的边界可以由标志模式明确区分。')
  66. ]
  67. print("input:", simple_prompt + batch_sentence)
  68. response, history = model.chat(tokenizer, simple_prompt + batch_sentence, history=[])
  69. print("output:\n", response)
  70. return response, history
  71. if __name__ == "__main__":
  72. #chat-glm3-6b
  73. # tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)
  74. # model = AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True, device='cuda')
  75. # model = model.eval()
  76. #Qwen-72B-Chat-Int4
  77. tokenizer = AutoTokenizer.from_pretrained("qwen/Qwen-72B-Chat-Int4", revision='master', trust_remote_code=True)
  78. model = AutoModelForCausalLM.from_pretrained(
  79. "qwen/Qwen-72B-Chat-Int4", revision='master',
  80. device_map="auto",
  81. trust_remote_code=True
  82. ).eval()
  83. # src_f = open('./data/sample.json', 'r')
  84. src_f = open('./data/computer_network_5_origin.json', 'r')
  85. out_f = open('./data/computer_network_5_filted.json', 'w')
  86. # read pages content from file
  87. pages = json.load(src_f)
  88. full_content = ''
  89. for page in tqdm(pages):
  90. full_content += page[1]
  91. # split sentences and filted
  92. sentences = split_chinese_text(full_content)
  93. batch_sentence = ''
  94. history = []
  95. filted_sentence = []
  96. max_len = 512
  97. for sentence in tqdm(sentences):
  98. if len(sentence) > max_len:
  99. continue
  100. if len(batch_sentence) + len(sentence) > max_len:
  101. response, history = process_batch_sentence(model, tokenizer, batch_sentence, history)
  102. filted_sentence.extend(split_chinese_text(response))
  103. batch_sentence = sentence
  104. else:
  105. batch_sentence += sentence
  106. if batch_sentence != '':
  107. response, history = process_batch_sentence(model, tokenizer, batch_sentence, history)
  108. filted_sentence.extend(split_chinese_text(response))
  109. out_f.write(json.dumps(filted_sentence, ensure_ascii=False))
4.2效果分析

        在使用ChatGLM3的时候出现了文档清洗不完全、有多余输出信息、修改了原文内容等问题,

因此我们换用了Qwen72B模型进行清洗,部署Qwen72B模型需要过多的显存,我们为了在本地部署,选用了INT4参数的版本大概需要50GB显存,在三张3090显卡上运行。

最终的清洗效果如下:

原始的纯文本数据
清洗后的文本数据(按句子分割)

文本数据提取和清洗总结

        我们对pdf的文本进行了提取并使用多种手段进行数据清洗,最终得到了富含语义信息和概念知识的纯文本,并以句子为单位分割,去除了过长的句子以防止对后续的模型训练产生影响。最终我们通过统计文本中的特殊字符和连续数字、字符数量以评判清洗效果,达到了令人满意的程度。

2024.4.14 词条数据爬取和格式转换

        本周我对之前分析的百度百科词条编写了爬虫进行数据爬取,获得了大量的词条和描述文档,并且同时获得了一些词条间的关系。

爬虫编写

网站分析

https://baike.baidu.com/item/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/18763

百度百科是类似于wiki pedia的百科网站,数据库里存储了大量的概念词条和相关描述,同时在每个概念的描述文档中还使用超链接连接到相关的概念。

该网站不需要登录即可游客访问,使用浏览器可以在未登录的情况下快速多次访问不受限制。

 使用技术和工具

        本次爬虫使用Python作为编程语言,使用了requests模块发送http请求,使用time进行随机等待请求时间间隔防止被反爬机制识别,使用re模块对爬取的html格式数据进行正则匹配提取有用信息,使用json模块进行数据的格式化和写入存储

        另外本次爬虫还使用了企业提供的IP池,支持爬虫程序进行多线程并发的快速请求,提高爬取效率,支持大量数据爬取。

代码实现

        首先设计爬虫机制,对指定的词条进行爬取,通过文档中的超链接和相关词条标题加入待爬取队列,进行递归爬取,对规模较大的数据集爬取3层,对规模较小的数据集爬取4层。

  1. if __name__ == '__main__':
  2. que = []
  3. vis = set()
  4. que.append(('面向对象', 2262089, 1))
  5. vis.add('面向对象')
  6. # que.append(('网络环境', 4422188, 1))
  7. # vis.add('网络环境')
  8. cur = 0
  9. docs = []
  10. mention_list = []
  11. doc_f = open('./docs.json', 'w', encoding='utf-8')
  12. mention_f = open('./mentions.json', 'w', encoding='utf-8')
  13. while True:
  14. if cur >= len(que):
  15. break
  16. print(f"cur/sum: {cur}/{len(que)}")
  17. entity_name, entity_id, depth = que[cur]
  18. cur += 1
  19. print("request:", 'https://baike.baidu.com/item/' + entity_name + '/' + str(entity_id))
  20. url = 'https://baike.baidu.com/item/' + urllib.parse.quote(entity_name) + '/' + str(entity_id)
  21. text = query(url)
  22. doc = {
  23. 'title': entity_name,
  24. 'id': entity_id,
  25. 'content': ''
  26. }
  27. pattern = r"<span class=\".*?\" data-text=\"true\">(.*?)</span>"
  28. matches = re.findall(pattern, text)
  29. for match in matches:
  30. # print("match:", match)
  31. pattern = r'href="/item/([^"]+)/(\d+).*?>(.*?)</a>'
  32. sentence = re.search(pattern, match)
  33. if sentence:
  34. match_entity_name, match_entity_id, match_mention = re.findall(pattern, match)[0]
  35. match_entity_id = int(match_entity_id)
  36. # print("a:", match_mention)
  37. if depth < 4 and match_entity_name not in vis:
  38. que.append((match_entity_name, match_entity_id, depth + 1))
  39. vis.add(match_entity_name)
  40. start_pos = len(doc['content'])
  41. doc['content'] += match_mention
  42. end_pos = len(doc['content'])
  43. mention = {
  44. 'doc_id': entity_id,
  45. 'entity_id': match_entity_id,
  46. 'entity_name': match_entity_name,
  47. 'mention': match_mention,
  48. 'start_pos': start_pos,
  49. 'end_pos': end_pos
  50. }
  51. mention_list.append(mention)
  52. mention_f.write(json.dumps(mention, ensure_ascii=False) + '\n')
  53. else:
  54. # print("span:", match)
  55. doc['content'] += match
  56. docs.append(doc)
  57. doc_f.write(json.dumps(doc, ensure_ascii=False) + '\n')
  58. # print("doc:", doc)
  59. time.sleep(1)
  60. # break

        首先直接使用requests进行http请求,请求间隔为1秒,发现爬取约30条网页信息后被百度反爬机制侦测并爬取到人机验证界面。

        尝试设置时间间隔为随机20-30秒,重新爬取,发现在爬取约半小时后仍然被人机验证。虽然爬取时间变长,但因为爬取间隔边长,总的爬取数量仍没有什么提升。

        发现浏览器请求可以不受限制,尝试携带agent请求头模拟chrome浏览器请求,并携带登录用户的cookie。

  1. headers = {
  2. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
  3. # 'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
  4. # 'Accept-Encoding': 'gzip, deflate, br, zstd',
  5. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
  6. # 'Cookie': 'zhishiTopicRequestTime=1714204958834; BIDUPSID=990AEC8E63B79ECEB2DCA3A7BEEA347E; PSTM=1688897620; BAIDUID=990AEC8E63B79ECE513E0A76649F94E7:FG=1; BDUSS=XY4cU5nUXU0NEZSRXFxRGM5UFdwUnBBRFQ5MlZUVFdWclM5Y2N5eHAtflhYTkprRVFBQUFBJCQAAAAAAAAAAAEAAAB~ktsyMjQ3MTExYQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANfPqmTXz6pkS; BDUSS_BFESS=XY4cU5nUXU0NEZSRXFxRGM5UFdwUnBBRFQ5MlZUVFdWclM5Y2N5eHAtflhYTkprRVFBQUFBJCQAAAAAAAAAAAEAAAB~ktsyMjQ3MTExYQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANfPqmTXz6pkS; Hm_lvt_55b574651fcae74b0a9f1cf9c8d7c93a=1700572499,1701322775; MCITY=-60949%3A288%3A; H_WISE_SIDS_BFESS=40416_40499_40446_40080_60129_60138; baikeVisitId=2efca0c8-7c1c-4797-8bb5-d03d9db4ab56; BAIDUID_BFESS=990AEC8E63B79ECE513E0A76649F94E7:FG=1; ZFY=hJ65HUmPh7OYnHjNKX2IUJefkgbBJUR9vLt4jjZ0hio:C; H_PS_PSSID=40499_40446_40080_60138; H_WISE_SIDS=40499_40446_40080_60138; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; BA_HECTOR=ag8k002g048l8420a0a0a180prrdan1j2pb881t; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; PSINO=1; delPer=0; channel=baidusearch; ab_sr=1.0.1_MjE4YTM5MzJkYmE0Njg1M2IxYmIwYTIyMWMzYjVmMjUwMThkZWE3NDExNzZjMjk0ODk1YWU4OGRhMjViNjRkYWYwNWYwZDUyMzhhMmY3ZjMzMGU1YWFlNjdmMjdhZTRjMjUyOTA5NDNhMzU3YjJkNDNkNTRlMTA1NjcyMTViN2MzYTQxOTE4OTQyOWZkNjMyZjU4OWE0N2M4MDJlYjkyYjU5Yjk5ZjBiYWQxMjkzNDNhZTJmYTVhMDA3YmU5YjFh; RT="z=1&dm=baidu.com&si=d50562e6-c279-4806-8422-2414ca533856&ss=lvhtdf0a&sl=9&tt=1yf&bcn=https%3A%2F%2Ffclog.baidu.com%2Flog%2Fweirwood%3Ftype%3Dperf&ld=gyo&ul=hla"'
  7. 'Connection': 'close'
  8. }

        在爬取半天,约爬取1300条网页后仍然被人机验证,约半小时到一小时内可以恢复访问,中间的爬取会被阻塞,效率太低。

        尝试配置IP池,使用IP代理的方式每次请求使用不同的IP,防止被反爬机制识别到同一IP的操作。

        使用IP池的结果可以正确避免被反爬机制识别,但存在部分IP连接超时,因此需要对每次代理访问进行多次尝试。

        使用IP池后,由于每次访问使用不同IP,即访问请求可以并发且时间间隔降低,因此时间间隔设置为随机1-5秒

爬虫结果

        我们对计算机网络、面向对象、软件工程、计算机组织与结构进行了相关词条数据爬取,总共获得了约1.5GB的文本数据,包括词条描述和词条间关系,大概占比1:1

词条描述数据
词条关系数据

 2024.4.21 知识图谱大模型调研和部署

        我们获得了富含大量语义信息和知识的文本数据,现在问题是如何通过这些数据构建知识图谱和训练大模型让其在相关领域具有更好的理解、表示、推理和生成能力。

一、模型选择

1. BERT-based Models (如 BERT, RoBERTa, SpanBERT)
  • 特点和优势

    • BERT:基于Transformer架构,双向编码,适合各种NLP任务。
    • RoBERTa:在BERT基础上优化了训练过程,效果更好。
    • SpanBERT:专门优化了span-level表示,提升了实体和关系识别的性能。
  • 训练方法

    • 使用预训练的BERT模型,然后在特定任务的数据集上进行微调(fine-tuning)。
    • 数据集通常需要标注实体和关系。
  • 部署方法

    • 使用 transformers 库加载预训练模型并微调。
    • 部署时可以使用 torchONNX 等进行推理。
  • 注意事项

    • BERT模型较大,推理时需要较多资源。
    • 微调需要高质量的标注数据。
2. GPT-based Models (如 GPT-3, GPT-4)
  • 特点和优势

    • 强大的生成能力,适用于从文本生成知识图谱的任务。
    • 通过自然语言提示(prompt)进行任务驱动,不需要专门微调。
  • 训练方法

    • 使用预训练的GPT模型,针对具体任务设计合适的提示(prompt)。
    • 可通过少量示例进行任务适应(少样本学习)。
  • 部署方法

    • 使用API(如OpenAI的API)或 transformers 库进行部署和调用。
    • 部署时需要注意模型的推理延迟和成本。
  • 注意事项

    • 模型较大,推理成本高。
    • 需要设计有效的提示来引导模型生成合适的输出。
3. Transformer-based Models 专为NLP任务设计 (如 T5, BART)
  • 特点和优势

    • T5:统一的文本到文本框架,可以处理多种NLP任务。
    • BART:结合了BERT和GPT的优点,适用于生成和序列标注任务。
  • 训练方法

    • 使用预训练的模型,并在特定任务的数据集上进行微调。
    • 数据集需要包含输入和输出对。
  • 部署方法

    • 使用 transformers 库进行加载和推理。
    • 可以通过 ONNXTorchScript 优化推理性能。
  • 注意事项

    • 数据集的质量和规模对微调效果影响很大。
    • 部署时需要注意内存和计算资源需求。
4. 专用模型 (如 SpaCy, Flair)
  • 特点和优势

    • SpaCy:高效且易用,适合生产环境。
    • Flair:灵活且易于扩展,支持多语言和多任务。
  • 训练方法

    • 使用内置的预训练模型,或在自定义数据集上进行训练。
    • 提供简单的API进行模型训练和评估。
  • 部署方法

    • 部署时直接调用库的API。
    • 可以通过容器化(如Docker)方便地进行部署。
  • 注意事项

    • 对大规模任务可能性能不如BERT-based模型。
    • 适合中小规模的数据和任务。

二、模型训练方法

1. 数据准备
  • 收集和标注高质量的数据集,包括实体和关系标签。
  • 数据集格式通常为CoNLL格式或JSON格式,包含文本、实体位置、实体类型和关系。
2. 预处理
  • 数据清洗和预处理,确保数据质量。
  • 对文本进行分词、标注等处理。
3. 模型微调
  • 使用 transformers 等库加载预训练模型。
  • 在准备好的数据集上进行微调,调整超参数(如学习率、批次大小等)。

三、PaddleNLP 的 UIE 模型分析

一、基本特点和功能

PaddleNLP 的 UIE(Universal Information Extraction)模型是一个强大的信息抽取模型,基于PaddlePaddle深度学习框架开发,设计用于处理多种信息抽取任务,包括命名实体识别(NER)、关系抽取(RE)、事件抽取等。UIE模型的核心特点和功能包括:

  1. 通用性:UIE模型可以通过统一的架构处理多种信息抽取任务,减少了为不同任务训练不同模型的需求。
  2. 预训练与微调:利用预训练技术,在大规模无监督数据上进行预训练,然后在特定任务上微调,提高模型的效果。
  3. 端到端训练:模型可以通过端到端训练方法,直接从原始文本到信息抽取结果,无需额外的手工特征工程。
  4. 高效性:PaddlePaddle框架提供了高效的训练和推理性能,支持多种硬件加速,包括GPU和CPU。
二、在实体识别和关系提取上的优劣势

优势

  1. 统一框架:UIE模型使用统一的框架进行训练和推理,能够处理不同类型的抽取任务(实体、关系、事件等),简化了开发和部署流程。
  2. 高准确性:得益于预训练技术和大规模数据训练,UIE模型在实体识别和关系提取任务上具有较高的准确性。
  3. 灵活性:模型可以方便地适应不同的领域和任务,只需少量的标注数据进行微调。
  4. 扩展性:基于PaddlePaddle框架,UIE模型可以方便地进行扩展和优化,适用于大规模工业应用。

劣势

  1. 资源需求高:与其他大型预训练模型类似,UIE模型的训练和推理过程需要较高的计算资源,尤其是显存和内存。
  2. 数据依赖:模型的效果高度依赖于训练数据的质量和数量,需要大量高质量的标注数据进行微调。
  3. 复杂性:对于初学者或小型团队,使用和调试大型预训练模型可能存在一定的复杂性和门槛。

三、模型训练和部署到本地的详细步骤

1. 环境准备

首先,安装PaddlePaddle和PaddleNLP库:

  1. pip install paddlepaddle-gpu
  2. pip install paddlenlp

2. 下载和准备数据

3. 数据预处理

使用PaddleNLP的工具进行数据预处理:

  1. from paddlenlp.datasets import load_dataset
  2. def read(data_path):
  3. with open(data_path, 'r', encoding='utf-8') as f:
  4. for line in f:
  5. data = json.loads(line)
  6. yield {'text': data['text'], 'entities': data['entities'], 'relations': data['relations']}
  7. dataset = load_dataset(read, data_path='path/to/your/data')

4. 模型训练

使用PaddleNLP的UIE模型进行训练:

  1. from paddlenlp.transformers import UIEModel, UIETokenizer
  2. from paddlenlp.data import DataCollatorForTokenClassification
  3. from paddlenlp.trainer import Trainer, TrainingArguments
  4. model = UIEModel.from_pretrained('uie-base')
  5. tokenizer = UIETokenizer.from_pretrained('uie-base')
  6. def tokenize_and_align_labels(examples):
  7. tokenized_inputs = tokenizer(examples['text'], truncation=True, padding='max_length', max_length=128)
  8. labels = []
  9. for i, label in enumerate(examples['entities']):
  10. label_ids = [0] * len(tokenized_inputs['input_ids'][i])
  11. for start, end, entity in label:
  12. label_ids[start:end+1] = [1] * (end - start + 1)
  13. labels.append(label_ids)
  14. tokenized_inputs['labels'] = labels
  15. return tokenized_inputs
  16. tokenized_datasets = dataset.map(tokenize_and_align_labels, batched=True)
  17. training_args = TrainingArguments(
  18. output_dir='./results',
  19. evaluation_strategy='epoch',
  20. learning_rate=2e-5,
  21. per_device_train_batch_size=16,
  22. per_device_eval_batch_size=16,
  23. num_train_epochs=3,
  24. weight_decay=0.01,
  25. )
  26. trainer = Trainer(
  27. model=model,
  28. args=training_args,
  29. train_dataset=tokenized_datasets['train'],
  30. eval_dataset=tokenized_datasets['eval'],
  31. tokenizer=tokenizer,
  32. data_collator=DataCollatorForTokenClassification(tokenizer)
  33. )
  34. trainer.train()

5. 模型评估

使用验证集评估模型性能:

  1. metrics = trainer.evaluate()
  2. print(metrics)

6. 模型部署

将训练好的模型导出并部署为API服务:

  1. from paddlehub.serving import application
  2. import paddlehub as hub
  3. model.save_pretrained('uie_model')
  4. app = application.Application()
  5. app.load('uie_model')
  6. @app.route('/predict', methods=['POST'])
  7. def predict():
  8. data = request.json
  9. inputs = tokenizer(data['text'], return_tensors='pd')
  10. outputs = model(**inputs)
  11. # 处理输出并返回结果
  12. return jsonify(outputs)
  13. if __name__ == '__main__':
  14. app.run()

四、模型选取和部署总结

        paddle社区提供了十分完整易用的同意信息抽取模型UIE,并且经过了优秀的预训练过程,在许多领域上都取得了良好的信息提取效果,我们选用这个模型可以大大降低模型训练的风险。而且该模型配有比较完善的训练和测试代码,可以将主要工作放在数据集的精炼方面,已取得更好的模型训练效果。

        该模型基于bert,部署所需的资源相对较少,运行速度快,十分适用于我们大数据量信息提取的问题背景,下周我的工作将针对于该模型的训练数据构建和模型训练与评估展开。

2024.4.28 UIE模型的训练和评估

UIE使用daccano进行数据标注,并提供了将daccano数据转化为训练数据格式的代码。

daccano标注数据

一、将爬虫获取的json数据转化为daccano数据的格式

        我们根据爬取的大量文档文本和其中的超链接实体关系,对原文本中的已标注实体转化成daccano标注的实体的格式

  1. import json
  2. import re
  3. def split_doc(doc):
  4. single_sentences = re.split(r"(?<=[。!?])", docs[doc_title])
  5. sentences = []
  6. merged_sentence = ''
  7. for sentence in single_sentences:
  8. if len(merged_sentence) + len(sentence) < 512:
  9. merged_sentence += sentence
  10. else:
  11. sentences.append(merged_sentence)
  12. merged_sentence = sentence
  13. if merged_sentence != '':
  14. sentences.append(merged_sentence)
  15. return sentences
  16. if __name__ == '__main__':
  17. domain_name = 'computer_network'
  18. doc_f = open('/home/yunpu/Data/codes/VCRS/data/wiki_data/' + domain_name + '_docs.json' ,'r')
  19. mention_f = open('/home/yunpu/Data/codes/VCRS/data/wiki_data/' + domain_name + '_mentions_with_title.json', 'r')
  20. out_f = open('/home/yunpu/Data/codes/VCRS/UIE/data/' + domain_name + '_daccano.jsonl' ,'w')
  21. # entitys = set()
  22. # docs = {}
  23. # for line in doc_f:
  24. # doc = json.loads(line)
  25. # assert doc['title'] not in entitys
  26. # entitys.add(doc['title'])
  27. # docs[doc['title']] = doc['content']
  28. doc_titles = set()
  29. docs = {}
  30. entity_id = {}
  31. cnt = 0
  32. mentions = {}
  33. for line in doc_f:
  34. doc = json.loads(line)
  35. assert doc['title'] not in doc_titles
  36. doc_titles.add(doc['title'])
  37. docs[doc['title']] = doc['content']
  38. entity_id[doc['title']] = cnt
  39. cnt += 1
  40. for line in mention_f:
  41. mention = json.loads(line)
  42. if mention['doc_title'] not in mentions.keys():
  43. mentions[mention['doc_title']] = [mention]
  44. else:
  45. mentions[mention['doc_title']].append(mention)
  46. sample_cnt = 0
  47. for doc_title in docs.keys():
  48. if doc_title not in mentions.keys():
  49. continue
  50. # sentences = re.split(r"(?<=[。!?])", docs[doc_title])
  51. sentences = split_doc(docs[doc_title])
  52. if sample_cnt == 0:
  53. print(sentences)
  54. prelen = 0
  55. cur = 0
  56. for sentence in sentences:
  57. if len(sentence) < 512:
  58. sample = {}
  59. sample['id'] = sample_cnt
  60. sample_cnt += 1
  61. sample['text'] = sentence
  62. sample['relations'] = []
  63. sample['entities'] = []
  64. while cur < len(mentions[doc_title]) and mentions[doc_title][cur]['start_pos'] >= prelen and mentions[doc_title][cur]['end_pos'] <= prelen + len(sentence):
  65. if mentions[doc_title][cur]['entity_name'] not in entity_id.keys():
  66. entity_id[mentions[doc_title][cur]['entity_name']] = cnt
  67. cnt += 1
  68. entity = {
  69. "id": entity_id[mentions[doc_title][cur]['entity_name']],
  70. "start_offset": mentions[doc_title][cur]['start_pos'] - prelen,
  71. "end_offset": mentions[doc_title][cur]['end_pos'] - prelen,
  72. "label": "实体"
  73. }
  74. sample['entities'].append(entity)
  75. cur += 1
  76. if len(sample['entities']) == 0:
  77. sample_cnt -= 1
  78. else:
  79. out_f.write(json.dumps(sample, ensure_ascii=False) + '\n')
  80. prelen += len(sentence)
  81. # if sample_cnt > 50:
  82. # break

代码标注后的daccano数据 

{"id": 0, "text": "计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路和通信设备连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。计算机网络主要是由一些通用的、可编程的硬件互连而成的。这些可编程的硬件能够用来传送多种不同类型的数据,并能支持广泛的和日益增长的应用。计算机网络Computer network计算机网络系统互联网信息的传输与共享网络操作系统计算机网络也称计算机通信网。关于计算机网络的最简单定义是:一些相互连接的、以共享资源为目的的、自治的计算机的集合。若按此定义,则早期的面向终端的网络都不能算是计算机网络,而只能称为联机系统(因为那时的许多终端不能算是自治的计算机)。但随着硬件价格的下降,许多终端都具有一定的智能,因而“终端”和“自治的计算机”逐渐失去了严格的界限。若用微型计算机作为终端使用,按上述定义,则早期的那种面向终端的网络也可称为计算机网络。另外,从逻辑功能上看,计算机网络是以传输信息为基础目的,用通信线路将多个计算机连接起来的计算机系统的集合,一个计算机网络组成包括传输介质和通信设备。", "relations": [], "entities": [{"id": 1, "start_offset": 8, "end_offset": 12, "label": "实体"}, {"id": 2, "start_offset": 29, "end_offset": 33, "label": "实体"}, {"id": 3, "start_offset": 36, "end_offset": 40, "label": "实体"}, {"id": 4, "start_offset": 51, "end_offset": 57, "label": "实体"}, {"id": 5, "start_offset": 58, "end_offset": 64, "label": "实体"}, {"id": 6, "start_offset": 65, "end_offset": 71, "label": "实体"}, {"id": 7, "start_offset": 81, "end_offset": 85, "label": "实体"}, {"id": 8, "start_offset": 86, "end_offset": 90, "label": "实体"}, {"id": 9, "start_offset": 91, "end_offset": 96, "label": "实体"}, {"id": 10, "start_offset": 216, "end_offset": 222, "label": "实体"}, {"id": 11, "start_offset": 247, "end_offset": 251, "label": "实体"}, {"id": 12, "start_offset": 299, "end_offset": 303, "label": "实体"}, {"id": 13, "start_offset": 377, "end_offset": 382, "label": "实体"}, {"id": 3, "start_offset": 447, "end_offset": 451, "label": "实体"}, {"id": 14, "start_offset": 482, "end_offset": 486, "label": "实体"}, {"id": 15, "start_offset": 487, "end_offset": 491, "label": "实体"}]}

二、人工标注

        因为百度词条中仅对一部分实体进行了标注,重复实体被忽略,因此我们首先使用代码对相同实体进行了补充标注,并人工对一些代码忽略的实体进行了标注

Doccano 是一个用于文本标注的开源工具,支持多种语言任务如命名实体识别、情感分析和文本分类。以下是详细的本地部署步骤。

一、环境准备
  • 操作系统:Windows
  • Python:3.8 或以上
  • Docker:最新版本
二、安装 Docker
  1. sudo apt-get update
  2. sudo apt-get install -y \
  3. ca-certificates \
  4. curl \
  5. gnupg \
  6. lsb-release
  7. sudo mkdir -p /etc/apt/keyrings
  8. curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
  9. echo \
  10. "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  11. $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  12. sudo apt-get update
  13. sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

启动 Docker 服务:

  1. sudo systemctl start docker
  2. sudo systemctl enable docker
三、下载并启动 Doccano

使用 Docker 部署 Doccano 非常简单。首先,拉取 Doccano 的 Docker 镜像:

docker pull doccano/doccano

 然后,运行 Doccano 容器:

docker run -d --name doccano -p 8000:8000 doccano/doccano

这会启动 Doccano,并在本地的 8000 端口上提供服务。

四、访问 Doccano

在浏览器中访问 http://localhost:8000,你会看到 Doccano 的登录页面。首次运行时需要创建一个超级用户来管理项目和用户。

五、数据标注

先导入刚刚代码标注的json文件

        可以看到我们刚刚使用代码进行的自动标注已经导入了daccano的系统之中,我们在这个基础上对遗落的实体进行了标注

 

最终我们取得了约50条500字长度的标注数据对UIE模型进行微调

 三、数据格式转换

使用paddle提供的数据格式转换代码

https://github.com/PaddlePaddle/PaddleNLP/tree/develop/model_zoo/uie

  1. python doccano.py \
  2. --doccano_file ./data/doccano_ext.json \
  3. --task_type ext \
  4. --save_dir ./data \
  5. --splits 0.8 0.2 0 \
  6. --schema_lang ch

转换后的数据格式 

{"id":0,"text":"计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路和通信设备连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。计算机网络主要是由一些通用的、可编程的硬件互连而成的。这些可编程的硬件能够用来传送多种不同类型的数据,并能支持广泛的和日益增长的应用。计算机网络Computer network计算机网络系统互联网信息的传输与共享网络操作系统计算机网络也称计算机通信网。关于计算机网络的最简单定义是:一些相互连接的、以共享资源为目的的、自治的计算机的集合。若按此定义,则早期的面向终端的网络都不能算是计算机网络,而只能称为联机系统(因为那时的许多终端不能算是自治的计算机)。但随着硬件价格的下降,许多终端都具有一定的智能,因而“终端”和“自治的计算机”逐渐失去了严格的界限。若用微型计算机作为终端使用,按上述定义,则早期的那种面向终端的网络也可称为计算机网络。另外,从逻辑功能上看,计算机网络是以传输信息为基础目的,用通信线路将多个计算机连接起来的计算机系统的集合,一个计算机网络组成包括传输介质和通信设备。","entities":[{"id":350447,"label":"实体","start_offset":8,"end_offset":12},{"id":350448,"label":"实体","start_offset":29,"end_offset":33},{"id":350449,"label":"实体","start_offset":36,"end_offset":40},{"id":350450,"label":"实体","start_offset":51,"end_offset":57},{"id":350451,"label":"实体","start_offset":58,"end_offset":64},{"id":350452,"label":"实体","start_offset":65,"end_offset":71},{"id":350453,"label":"实体","start_offset":81,"end_offset":85},{"id":350454,"label":"实体","start_offset":86,"end_offset":90},{"id":350455,"label":"实体","start_offset":91,"end_offset":96},{"id":350456,"label":"实体","start_offset":216,"end_offset":222},{"id":350457,"label":"实体","start_offset":247,"end_offset":251},{"id":350458,"label":"实体","start_offset":299,"end_offset":303},{"id":350459,"label":"实体","start_offset":377,"end_offset":382},{"id":350460,"label":"实体","start_offset":447,"end_offset":451},{"id":350461,"label":"实体","start_offset":482,"end_offset":486},{"id":350462,"label":"实体","start_offset":487,"end_offset":491},{"id":350885,"label":"实体","start_offset":0,"end_offset":5},{"id":350886,"label":"实体","start_offset":41,"end_offset":45},{"id":350887,"label":"实体","start_offset":24,"end_offset":27},{"id":350888,"label":"实体","start_offset":209,"end_offset":214},{"id":350889,"label":"实体","start_offset":473,"end_offset":478},{"id":350890,"label":"实体","start_offset":454,"end_offset":457}],"relations":[{"id":222,"from_id":350885,"to_id":350453,"type":"功能"},{"id":223,"from_id":350885,"to_id":350454,"type":"功能"},{"id":224,"from_id":350885,"to_id":350455,"type":"定义"},{"id":225,"from_id":350888,"to_id":350456,"type":"别名"},{"id":226,"from_id":350889,"to_id":350461,"type":"包含"},{"id":227,"from_id":350889,"to_id":350462,"type":"包含"},{"id":228,"from_id":350460,"to_id":350890,"type":"连接"},{"id":229,"from_id":350449,"to_id":350887,"type":"连接"},{"id":230,"from_id":350449,"to_id":350448,"type":"连接"},{"id":231,"from_id":350886,"to_id":350887,"type":"连接"},{"id":232,"from_id":350886,"to_id":350448,"type":"连接"},{"id":233,"from_id":350885,"to_id":350450,"type":"包含"},{"id":234,"from_id":350885,"to_id":350451,"type":"包含"},{"id":235,"from_id":350885,"to_id":350452,"type":"包含"}],"Comments":[]}

 四、模型微调

继续使用paddle提供的微调代码,设置微调参数

  1. export finetuned_model=./checkpoint/model_best
  2. python finetune.py \
  3. --device gpu \
  4. --logging_steps 10 \
  5. --save_steps 100 \
  6. --eval_steps 100 \
  7. --seed 42 \
  8. --model_name_or_path uie-base \
  9. --output_dir $finetuned_model \
  10. --train_path data/train.txt \
  11. --dev_path data/dev.txt \
  12. --max_seq_length 512 \
  13. --per_device_eval_batch_size 16 \
  14. --per_device_train_batch_size 16 \
  15. --num_train_epochs 20 \
  16. --learning_rate 1e-5 \
  17. --label_names "start_positions" "end_positions" \
  18. --do_train \
  19. --do_eval \
  20. --do_export \
  21. --export_model_dir $finetuned_model \
  22. --overwrite_output_dir \
  23. --disable_tqdm True \
  24. --metric_for_best_model eval_f1 \
  25. --load_best_model_at_end True \
  26. --save_total_limit 1

 五、模型效果评估

  1. python evaluate.py \
  2. --model_path ./checkpoint/model_best \
  3. --test_path ./data/dev.txt \
  4. --batch_size 16 \
  5. --max_seq_len 512 \
  6. --multilingual

        模型在实体识别任务上取得了Evaluation Precision: 0.61081 | Recall: 0.58020 | F1: 0.59511的成绩,我认为已经满足我们的实体识别标注需求。

2024.5.5

本周劳动节假期

2024.5.12 使用模型进行命名实体识别和知识图谱关系提取

1. 工作概述

本周的工作旨在利用上周训练的 PaddleNLP 的 UIE 模型对现有文档数据进行实体识别和关系提取,从而构建知识图谱。提取出来的数据将分别存储到 JSON 文件和 Neo4j 数据库中。

2. 环境准备

2.1 安装必要的软件和库

确保安装以下软件和库Neo4j

pip install neo4j
2.2 设置 Neo4j 数据库

下载并安装 Neo4j 社区版:https://neo4j.com/download/

启动 Neo4j 数据库,并设置用户名和密码(默认用户名为 neo4j,密码可以自定义)。

3. 数据准备与预处理

将我们的爬取的所有数据按照上周训练模型的方式进行格式转换(仅跳过人工标注阶段)

4. 实体识别与关系提取

使用 PaddleNLP 的 UIE 模型进行实体识别和关系提取。

5. 数据存储到 JSON 文件

5.1 定义 JSON 文件存储函数
  1. def save_to_json(data, filename='extracted_data.json'):
  2. with open(filename, 'w', encoding='utf-8') as file:
  3. json.dump(data, file, ensure_ascii=False, indent=4)
5.2 保存提取的数据到 JSON 文件
save_to_json(extracted_data)

6. 数据存储到 Neo4j 数据库

6.1 连接 Neo4j 数据库
  1. from neo4j import GraphDatabase
  2. uri = "bolt://localhost:7687"
  3. username = "neo4j"
  4. password = "your_password"
  5. driver = GraphDatabase.driver(uri, auth=(username, password))
 6.2 定义数据存储函数
  1. def save_to_neo4j(data):
  2. with driver.session() as session:
  3. for item in data:
  4. entities = item['entities']
  5. relations = item['relations']
  6. for entity in entities:
  7. session.run(
  8. "MERGE (e:Entity {name: $name, type: $type})",
  9. name=entity['name'],
  10. type=entity['type']
  11. )
  12. for relation in relations:
  13. session.run(
  14. """
  15. MATCH (e1:Entity {name: $start_name}), (e2:Entity {name: $end_name})
  16. MERGE (e1)-[r:RELATION {type: $type}]->(e2)
  17. """,
  18. start_name=relation['start_name'],
  19. end_name=relation['end_name'],
  20. type=relation['type']
  21. )

我们还直接使用大模型+任务提示的方式进行了实体和关系提取,并对比效果

  1. import modelscope
  2. import transformers
  3. prompt = """
  4. 你的任务是提取以下文本中所有的实体和他们之间的关系并按{"entity1": '', "relation": '', "entity2": ''}的格式输出。
  5. """
  6. class GlmModel:
  7. def __init__(self) -> None:
  8. self.tokenizer = transformers.AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)
  9. self.model = transformers.AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True, device='cuda')
  10. self.model = self.model.eval()
  11. def text2re(self, text: str):
  12. message = prompt + text
  13. print("input:", message)
  14. response, history = self.model.chat(self.tokenizer, message, history=None)
  15. print("output:", response)
  16. return response
  17. class QwenModel:
  18. def __init__(self) -> None:
  19. self.tokenizer = modelscope.AutoTokenizer.from_pretrained("qwen/Qwen-72B-Chat-Int4", revision='master', trust_remote_code=True)
  20. self.model = modelscope.AutoModelForCausalLM.from_pretrained(
  21. "qwen/Qwen-72B-Chat-Int4", revision='master',
  22. device_map="auto",
  23. trust_remote_code=True
  24. ).eval()
  25. def text2re(self, text: str):
  26. message = prompt + text
  27. print("input:", message)
  28. response, history = self.model.chat(self.tokenizer, message, history=None)
  29. print("output:", response)
  30. return response
  31. if __name__ == '__main__':
  32. re_model = GlmModel()
  33. f = open('./data/sample_text.txt', 'r')
  34. doc = f.readline()
  35. re = re_model.text2re(doc)

2024.5.19 可视化方面的探索

结合前端的需求,我需要将爬取的文档和关系数据进行一定的格式转化以便于可视化展示

知识图谱数据格式转换

1. 概述

该工作主要功能是处理一个包含文档标题和实体名称的 JSON 文件,生成一个包含节点(文档标题)和边(文档标题与实体名称关系)的知识图谱(KG)。生成的知识图谱按特定类别(如物理层、数据链路层等)进行分类,并将结果输出到一个 JSON 文件中。

2. 代码功能简介

代码执行以下主要任务:

  1. 从输入的 JSON 文件中读取数据。
  2. 创建节点和边,并构建一个图结构。
  3. 按照预定义的类别对节点进行分类。
  4. 将处理后的图结构数据存储到一个 JSON 文件中。

3. 数据格式转换分析

输入数据格式

输入数据来自一个名为 computer_network_mentions_with_title.json 的文件,其中每行是一个 JSON 对象,包含以下字段:

  • doc_title: 文档标题
  • entity_name: 实体名称
输出数据格式

输出是一个 JSON 对象,包含以下结构:

  • categories: 节点类别列表
  • nodes: 节点列表,每个节点包含名称、值和类别
  • links: 边列表,每个边包含源节点和目标节点

4. 实现思路

  1. 初始化类别和数据结构:首先定义类别列表,并初始化存储节点、节点ID映射、图结构和边的相关数据结构。
  2. 读取并解析输入文件:逐行读取输入文件,将每个文档标题和实体名称映射到节点和边的数据结构中。
  3. 构建图结构:根据文档标题和实体名称的关系构建图结构,并记录每个文档标题的实体名称。
  4. 分类节点:基于预定义的类别,通过广度优先搜索(BFS)方法,将相关节点进行分类,并构建节点之间的边。
  5. 输出结果:将最终构建的图结构数据写入到一个 JSON 文件中。
读取并解析输入文件
  1. f = open('./computer_network_mentions_with_title.json', 'r', encoding='utf-8')
  2. last_title = ''
  3. for line in f:
  4. item = json.loads(line)
  5. if item['doc_title'] not in nodes_id.keys():
  6. nodes_id[item['doc_title']] = len(nodes)
  7. nodes.append({
  8. "name": item['doc_title'],
  9. "value": 1,
  10. "category": 7
  11. })
  12. graph[item['doc_title']] = []
  13. edges.append((item['doc_title'], item['entity_name']))
  14. graph[item['doc_title']].append(item['entity_name'])
  15. if len(nodes) == 1000:
  16. break
  • 打开输入文件,逐行读取并解析为 JSON 对象。
  • 对每个文档标题,检查是否已经在 nodes_id 中,如不存在则添加到 nodesnodes_id 中,并初始化 graph 字典。
  • 添加文档标题与实体名称的关系到 edges 列表和 graph 字典中。
  • 当节点数量达到 1000 时,停止读取。
构建图结构
  1. for id, categorie in enumerate(tqdm(categories)):
  2. now_title = categorie['name']
  3. max_dis = 2
  4. que = [(now_title, 0)]
  5. vis = set()
  6. now = 0
  7. while now < len(que):
  8. now_title, now_dis = que[now]
  9. now += 1
  10. if now_title in nodes_id.keys():
  11. nodes[nodes_id[now_title]]['category'] = id
  12. if now_dis < max_dis:
  13. if now_title in graph.keys():
  14. for new_title in graph[now_title]:
  15. if new_title not in vis:
  16. que.append([new_title, now_dis + 1])
  17. vis.add(new_title)
  18. if now_title in nodes_id.keys() and new_title in nodes_id.keys():
  19. links.append({
  20. "source": nodes_id[now_title],
  21. "target": nodes_id[new_title]
  22. })
  • 使用广度优先搜索(BFS)方法,从每个类别的名称开始,遍历其相关的文档标题和实体名称,将节点分类并构建边。
  • max_dis 参数限制了搜索深度,确保仅搜索与类别名称相关的节点及其直接连接的节点。
输出结果
  1. kg = {
  2. "categories": categories,
  3. "nodes": nodes,
  4. "links": links
  5. }
  6. out_f = open("./kg_by_category.json", 'w', encoding='utf-8')
  7. out_f.write(json.dumps(kg, ensure_ascii=False))
  • 构建最终的知识图谱数据结构,包括 categoriesnodeslinks
  • 将知识图谱数据写入到 kg_by_category.json 文件中。

实现效果

树形结构的可视化

概述

读取一个包含文档标题和实体名称的 JSON 文件,将其转换成树形结构的数据,并将每个文档标题及其子树结构存储到单独的 JSON 文件中。输出的 JSON 文件适用于可视化工具,如 d3.js,用于展示树形结构图。

主要任务
  1. 读取并解析输入 JSON 文件。
  2. 维护一个顺序列表 order 和一个字典 sons,记录每个文档标题及其关联的实体名称。
  3. 生成树形结构数据。
  4. 将树形结构数据保存到单独的 JSON 文件中,每个文件对应一个文档标题。

 

数据格式转换分析
输入数据格式

输入数据来自一个名为 computer_network_mentions_with_title.json 的文件,其中每行是一个 JSON 对象,包含 doc_titleentity_name

输出数据格式

输出是树形结构的 JSON 对象,每个对象包含 namesizechildrenvalue 属性。输出文件以 id.json 命名存储在 tree_data 目录中。

实现思路
  1. 初始化必要的数据结构。
  2. 读取输入文件并解析数据,将每个文档标题和其关联的实体名称存储到 ordersons 中。
  3. 按照预定义的规则生成树形结构数据。
  4. 将每个文档标题及其树形结构数据存储到单独的 JSON 文件中。
生成树形结构数据
  1. id = 0
  2. for fa in order:
  3. item = {
  4. "name": '',
  5. "size": 0,
  6. "children": [],
  7. "value": 0
  8. }
  9. fasize = 0
  10. for s1 in sons[fa]:
  11. if len(item['children']) == 10:
  12. break
  13. if s1 in sons.keys():
  14. s1item = {
  15. "name": '',
  16. "size": 0,
  17. "children": [],
  18. "value": 0
  19. }
  20. s1size = 0
  21. for s2 in sons[s1]:
  22. if len(s1item['children']) == 10:
  23. break
  24. if s2 in sons.keys():
  25. s2item = {}
  26. s2size = random.randrange(1, 100)
  27. s1size += s2size
  28. s2item['name'] = s2
  29. s2item['size'] = s2size
  30. s2item['value'] = s2size
  31. s1item['children'].append(s2item)
  32. s1item['name'] = s1
  33. s1item['size'] = s1size
  34. s1item['value'] = s1size
  35. fasize += s1size
  36. item['children'].append(s1item)
  37. item['name'] = fa
  38. item['size'] = fasize
  39. item['value'] = fasize
  40. out_f = open("./tree_data/" + str(id) + ".json", "w", encoding='utf-8')
  41. out_f.write(json.dumps(item, ensure_ascii=False))
  42. id += 1
  43. print(id)
  44. if id == 2000:
  45. break
  • 初始化 id 计数器。
  • 对于每个文档标题 fa,创建一个包含 namesizechildrenvalue 的空字典 item
  • 对于每个与文档标题关联的实体名称 s1,如果它也是一个文档标题,则创建一个类似的字典 s1item
  • 对于每个与 s1 关联的实体名称 s2,生成一个随机大小 s2size,并将其添加到 s1itemchildren 中。
  • 更新 s1itemsizevalue,并将其添加到 itemchildren 中。
  • 更新 itemsizevalue,并将其写入到一个以 id 命名的 JSON 文件中。
数据展示
{"name": "计算机网络", "size": 5290, "children": [{"name": "地理位置", "size": 604, "children": [{"name": "地理事物", "size": 67, "value": 67}, {"name": "空间关系", "size": 58, "value": 58}, {"name": "确定", "size": 46, "value": 46}, {"name": "经纬度", "size": 83, "value": 83}, {"name": "经济地理位置", "size": 65, "value": 65}, {"name": "地理事物", "size": 99, "value": 99}, {"name": "空间关系", "size": 43, "value": 43}, {"name": "测绘科学与技术", "size": 81, "value": 81}, {"name": "地理事物", "size": 43, "value": 43}, {"name": "定性", "size": 19, "value": 19}], "value": 604}, {"name": "外部设备", "size": 543, "children": [{"name": "外设", "size": 9, "value": 9}, {"name": "输出设备", "size": 76, "value": 76}, {"name": "外存储器", "size": 78, "value": 78}, {"name": "作用", "size": 73, "value": 73}, {"name": "外围设备", "size": 72, "value": 72}, {"name": "主机", "size": 1, "value": 1}, {"name": "设备", "size": 49, "value": 49}, {"name": "系统", "size": 20, "value": 20}, {"name": "外设", "size": 99, "value": 99}, {"name": "计算机硬件", "size": 66, "value": 66}], "value": 543}, {"name": "通信线路", "size": 547, "children": [{"name": "有线通信", "size": 4, "value": 4}, {"name": "传输媒介", "size": 99, "value": 99}, {"name": "巴尔的摩", "size": 37, "value": 37}, {"name": "英吉利海峡", "size": 94, "value": 94}, {"name": "海底电缆", "size": 68, "value": 68}, {"name": "海底电缆", "size": 87, "value": 87}, {"name": "丹麦大北电报公司", "size": 44, "value": 44}, {"name": "多模光纤", "size": 68, "value": 68}, {"name": "单模光纤", "size": 3, "value": 3}, {"name": "传输媒介", "size": 43, "value": 43}], "value": 547}, {"name": "网络操作系统", "size": 515, "children": [{"name": "计算机", "size": 95, "value": 95}, {"name": "操作系统", "size": 4, "value": 4}, {"name": "服务器", "size": 49, "value": 49}, {"name": "客户端", "size": 26, "value": 26}, {"name": "服务器", "size": 63, "value": 63}, {"name": "资源", "size": 62, "value": 62}, {"name": "计算机", "size": 31, "value": 31}, {"name": "NOS", "size": 90, "value": 90}, {"name": "工作站", "size": 64, "value": 64}, {"name": "单用户操作系统", "size": 31, "value": 31}], "value": 515}, {"name": "网络管理软件", "size": 573, "children": [{"name": "网络管理", "size": 67, "value": 67}, {"name": "支撑软件", "size": 93, "value": 93}, {"name": "网络设备", "size": 22, "value": 22}, {"name": "网络系统", "size": 53, "value": 53}, {"name": "体系结构", "size": 13, "value": 13}, {"name": "应用程序", "size": 79, "value": 79}, {"name": "网络搜索", "size": 45, "value": 45}, {"name": "Windows NT", "size": 18, "value": 18}, {"name": "网络管理软件", "size": 93, "value": 93}, {"name": "网络管理协议", "size": 90, "value": 90}], "value": 573}, {"name": "网络通信协议", "size": 736, "children": [{"name": "操作系统", "size": 85, "value": 85}, {"name": "体系结构", "size": 84, "value": 84}, {"name": "互联网络", "size": 54, "value": 54}, {"name": "网络", "size": 81, "value": 81}, {"name": "操作系统", "size": 54, "value": 54}, {"name": "体系结构", "size": 90, "value": 90}, {"name": "互联网络", "size": 82, "value": 82}, {"name": "TCP/IP", "size": 86, "value": 86}, {"name": "传输控制协议", "size": 55, "value": 55}, {"name": "网际协议", "size": 65, "value": 65}], "value": 736}, {"name": "资源共享", "size": 437, "children": [{"name": "计算机", "size": 46, "value": 46}, {"name": "操作系统", "size": 53, "value": 53}, {"name": "共享空间", "size": 38, "value": 38}, {"name": "局域网", "size": 61, "value": 61}, {"name": "打印服务器", "size": 84, "value": 84}, {"name": "邮件服务器", "size": 66, "value": 66}, {"name": "局域网", "size": 28, "value": 28}, {"name": "集中存储", "size": 7, "value": 7}, {"name": "网络存储", "size": 47, "value": 47}, {"name": "工作组", "size": 7, "value": 7}], "value": 437}, {"name": "信息传递", "size": 407, "children": [{"name": "现代化管理", "size": 58, "value": 58}, {"name": "电码", "size": 66, "value": 66}, {"name": "购买行为", "size": 6, "value": 6}, {"name": "销售管理", "size": 21, "value": 21}, {"name": "商品信息", "size": 38, "value": 38}, {"name": "购买行为", "size": 32, "value": 32}, {"name": "信息管理", "size": 38, "value": 38}, {"name": "信息活动", "size": 64, "value": 64}, {"name": "有机体", "size": 31, "value": 31}, {"name": "主体要素", "size": 53, "value": 53}], "value": 407}, {"name": "计算机系统", "size": 354, "children": [{"name": "存储信息", "size": 53, "value": 53}, {"name": "结果信息", "size": 21, "value": 21}, {"name": "计算机", "size": 4, "value": 4}, {"name": "硬件", "size": 18, "value": 18}, {"name": "软件", "size": 27, "value": 27}, {"name": "中央处理机", "size": 62, "value": 62}, {"name": "存储器", "size": 61, "value": 61}, {"name": "外部设备", "size": 12, "value": 12}, {"name": "系统软件", "size": 83, "value": 83}, {"name": "应用软件", "size": 13, "value": 13}], "value": 354}, {"name": "计算机通信网", "size": 574, "children": [{"name": "通信设备", "size": 86, "value": 86}, {"name": "数据传输", "size": 58, "value": 58}, {"name": "领域", "size": 87, "value": 87}, {"name": "条件", "size": 76, "value": 76}, {"name": "通信技术", "size": 6, "value": 6}, {"name": "微电子", "size": 86, "value": 86}, {"name": "数据传输", "size": 19, "value": 19}, {"name": "处理器", "size": 50, "value": 50}, {"name": "功能", "size": 10, "value": 10}, {"name": "资源共享", "size": 96, "value": 96}], "value": 574}], "value": 5290}
实现结果

2024.5.26 知识图谱增强的大模型问答、检索调研

1. 知识图谱与语言模型融合的工作

1.1 K-BERT (Knowledge Enhanced BERT)

特点和思路:

  • K-BERT 将知识图谱中的三元组嵌入到 BERT 的输入中,使得模型能够利用结构化的知识进行问答和检索。
  • 通过在文本中插入实体及其关系节点,扩展了文本表示,从而在问答和信息检索任务中提供更丰富的语义信息。

优点:

  • 利用现有的 BERT 架构,容易进行迁移学习。
  • 能够增强模型对复杂问题的理解能力。

缺点:

  • 需要预处理文本以嵌入知识图谱信息,增加了数据处理复杂度。
  • 模型规模较大,训练和推理资源需求高。

模型和资源:

  • 需要预训练的 BERT 模型(如 bert-base-uncased)。
  • 需要额外的 GPU 资源用于训练和推理。
1.2 ERNIE (Enhanced Representation through Knowledge Integration)

特点和思路:

  • ERNIE 通过在训练过程中加入实体嵌入来增强语言模型的表示能力。
  • 将知识图谱中的实体信息集成到 Transformer 结构中,使模型能够更好地理解和生成与知识相关的文本。

优点:

  • 强化了模型对实体及其关系的理解能力,提高问答的准确性。
  • 在多个自然语言理解任务上表现出色。

缺点:

  • 需要复杂的预处理和知识集成步骤。
  • 模型训练时间较长,对硬件资源要求高。

模型和资源:

  • 预训练模型(如 ERNIE 1.0 或 ERNIE 2.0)。
  • 需要大规模的计算资源,特别是多卡 GPU 环境。
1.3 KEPLER (Knowledge Embedding Pre-trained LanguagE Representations)

特点和思路:

  • KEPLER 将知识图谱中的实体和关系嵌入到语言模型的预训练过程中。
  • 通过联合训练知识图谱嵌入和语言模型,提供更丰富的语义表示。

优点:

  • 在不显著增加模型复杂度的情况下增强语言模型的表现。
  • 适用于多种下游任务,如问答、文本分类等。

缺点:

  • 需要联合训练知识图谱和语言模型,训练过程复杂。
  • 对硬件资源要求较高。

模型和资源:

  • 基于 BERT 的预训练模型。
  • 需要大规模的知识图谱数据和 GPU 资源。

2. 知识图谱增强问答和检索的实现思路

2.1 实时知识检索与融合

思路:

  • 在用户查询时,实时从知识图谱中检索相关的实体和关系,融合到查询中以增强模型理解。
  • 结合 Elasticsearch 等工具实现高效的知识检索。

优点:

  • 能够动态响应用户查询,提供实时的知识增强。
  • 灵活性高,可以适应多种知识图谱数据源。

缺点:

  • 实时检索和融合会增加系统复杂度和响应时间。
  • 需要高效的索引和检索系统支持。

资源需求:

  • Elasticsearch 或其他搜索引擎。
  • 实时数据处理和融合的计算资源。
2.2 预先嵌入知识的文本生成

思路:

  • 在文本生成之前,通过查询知识图谱,将相关的实体和关系信息预先嵌入到生成模型的输入中。
  • 利用已增强的文本生成模型(如 T5,GPT-3)进行问答和检索。

优点:

  • 能够生成高质量的、知识丰富的回答。
  • 预处理过程相对简单,易于实现。

缺点:

  • 需要高质量的知识图谱数据和预处理步骤。
  • 生成模型需要额外的知识嵌入模块。

资源需求:

  • 预训练的文本生成模型。
  • 数据预处理和知识嵌入的计算资源。

部署和实现的具体建议

  1. 选择合适的模型:

    • 根据任务需求和资源情况选择合适的模型,例如选择 K-BERT 或 KEPLER 来进行问答任务。
    • 考虑使用开源的预训练模型(如 Hugging Face 提供的 BERT 变体)以节省训练时间和资源。
  2. 知识图谱的构建和使用:

    • 利用开源的知识图谱资源(如 DBpedia, Wikidata)。
    • 使用现有的知识图谱工具(如 RDFLib, NetworkX)进行数据处理和检索。
  3. 系统架构:

    • 部署基于 Elasticsearch 的知识检索系统,实现高效的知识查询。
    • 使用 RESTful API 或 gRPC 服务实现模型推理和知识融合,确保系统的灵活性和可扩展性。
  4. 硬件和计算资源:

    • 利用云服务(如 AWS, Google Cloud, Azure)提供的 GPU 实例进行模型训练和推理。
    • 配置高效的缓存和存储系统以支持大规模知识数据的快速访问和处理。

通过以上方法,可以有效地利用知识图谱增强大模型的问答和检索能力,提升系统的智能性和实用性。

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/950819
推荐阅读
相关标签
  

闽ICP备14008679号