基于 BERT 实现的情感分析(文本分类)----概念与应用

news2024/11/18 17:34:44

文章目录

    • 基于 BERT 的情感分析(文本分类)
      • 基本概念理解
        • 简便的编码方式: One-Hot 编码
        • 突破: Word2Vec编码方式
        • 新的开始: Attention 与 Transformer 模型
        • 四方来降: 超一流预处理模型 BERT 诞生
      • BERT 实现情感分析
        • 数据预处理并创建数据集
        • 定义网络模型
        • 定义训练函数与评估函数
        • 设置损失函数、优化方法、BertTokenizer词嵌入
      • 训练模型并预测结果
      • 小结

基于 BERT 的情感分析(文本分类)

2018年的10月11日,Google发布一篇论文《Pre-training of Deep Bidirectional Transformers for Language Understanding》,成功在 11 项 NLP 任务中取得 SOTA(最优) 结果,赢得自然语言处理学界的一片赞誉之声。

从此各种类BERT模型如雨后春笋般不断破土而出,创造了 NLP 领域一项又一项新的记录,本次我将带领大家在 18 多万条数据集上微调BERT实现情感分析(文本分类)。

基本概念理解

是什么导致了 BERT 的诞生?

BERT 有哪些优势?

简便的编码方式: One-Hot 编码

我们都知道,大家说的话都是人类语言,而计算机本质只认识数字信息,那计算机理解人类语言信息又是必须过程,那么我们该如何设计数字编码表示语言信息呢?

此时,最初的编码方式诞生了,即One-Hot编码,又称独热编码方式。假设现在有四个词 ['钢琴 ', ‘绘画’, '舞蹈 ', ‘篮球’] ,则编码方式如下:

钢琴 → [1, 0, 0, 0]

绘画 → [0, 1, 0, 0]

舞蹈 → [0, 0, 1, 0]

篮球 → [0, 0, 0, 1]

每个词在向量中都会有一个 1 与之对应,代表该词元的编码信息。最初很多文本信息都是这种编码方式,但是久而久之,这种编码方式的两种缺陷便显露出来:

1、可能产生维度爆炸的问题,即当词元的个数超级多的时候,每个词元的向量长度均为词元个数 n ,导致计算机无法存储,运算速度极慢。

2、无法有效表示词元的相似关系,每个词元只能独立表示。

突破: Word2Vec编码方式

为了改进 One-Hot 编码的缺陷,一种新的编码方式应运而生。2013年,Google团队发布了论文《Efficient Estimation of Word Representations inVector Space》,Word2Vec编码方式由此诞生,很大程度上推动了 NLP 领域的发展。其编码方式有以下两种:

1、Skip-Gram模型,即跳元模型。其原理是根据中心词预测上下文词

2、CBOW模型,即连续词袋模型。其原理是根据上下文词预测中心词

在这里插入图片描述

通过计算 中心词与上下文词的余弦相似度,来确定不同词元直接的关系。最终将所有词元都转化为长度为 embedding_dim(50~300)的向量。

这样不仅能够很大程度上缩减词元的存储空间,又能获得词元与词元之间的相似度。但是这种方法也有一些缺陷:

1、由于词和向量是一对一的关系,所以多义词的问题无法解决。

2、Word2vec 是一种静态的方式,虽然通用性强,但是无法针对特定任务做动态优化

关于Word2Vec的原理与代码实现,请看这几部分内容Word2Vec的详情原理。

新的开始: Attention 与 Transformer 模型

2017年6月,Google团队再次发布论文 《Attention Is All You Need》 关于注意力机制的 Transformer模型。

该论文主张使用注意力的机制,完全抛弃CNN,RNN等网络模型结构。起初主要应用在自然语言处理NLP中,后面也逐渐应用到了计算机视觉中。

仅仅通过 注意力机制(self-attention)前馈神经网络(Feed Forward Neural Network),不需要使用序列对齐的循环架构就实现了较好的效果。

模型结构如下图:

在这里插入图片描述

其存在两方面优点:

1、摒弃了RNN的网络结构模式,其能够很好的并行运算

2、其注意力机制能够帮助当前词获取较好的上下文信息

由于Transformer部分的原理内容较多,所以暂时跳过,有兴趣的朋友可阅读此篇文章。

四方来降: 超一流预处理模型 BERT 诞生

Bert基于Transformer编码器块架构进行设计,同时采用了两种方法来提升 NLP 处理水平:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2FtBqBgS-1668581805429)(attachment:QQ%E6%88%AA%E5%9B%BE20221116091839.png)]

1、MLM(Maked Language Model)掩蔽语言模型,Bert通过MLM方法来随机掩蔽句子中的一个词元,设计模型根据上下文信息去预测该词元。通过此种方法,使模型能够很好地解决多义词匹配上下文语义理解问题。

2、NSP(Next Sentence Prediction)下一句预测,随机选取多组 两个连接或两个不连接的句子,设置它们是否连续(为上下文)的标记,从而进行训练,得到文本对之间的关系,能够解决文本对之间的关系问题,即上下文关系问题

另外,Bert将普遍模型处理的 特定NLP任务 转化为了在Bert模型下的 不可知NLP任务,也就是说,Bert只是一个语料库的预处理模型,和One-Hot、Word2Vec一致,均是语言预训练模型。用户可以在Bert预训练模型下作 微调(fine-tune) 操作,使其能够处理多种NLP任务,如:

(1)单一文本分类(如情感分析)

(2)文本对分类(如自然语言推断)

(3)问答

(4)文本标记(如命名实体识别)。

比如文本分类,也就是我们这次的任务(情感分析),只需得到整个评论语句的上下文综合信息(‘<cls>’),再接上一层全连接层,便可实现分类任务。
其它不可知任务类似。

本模型的详情参考这篇文章。

BERT 实现情感分析

前提准备:

  1. 准备18多万条手机评论信息和对应的情感标签, 0, 1, 2 代表 差中好 评。
  2. 因为个人计算机资源无法训练出Bert预训练模型,我们使用 transformer 库中的 BertTokenizer、 BertModel模型。BertTokenizer将输入的评论语句转化为输入Bert模型的向量信息,BertModel根据输入信息输出结果。

数据预处理并创建数据集

读取文件中的评论信息,并对数据进行去重。

import csv
import pandas as pd
import random
import torch
from transformers import BertTokenizer, BertModel
from torch import nn
from d2l import torch as d2l
from tqdm import tqdm

"""
读取评论文件的评论信息
"""
def read_file(file_name):
    
    comments_data = None
    
    # 读取评论信息
    with open(file_name, 'r', encoding='UTF-8') as f:
        reader = csv.reader(f)
        # 读取评论数据和对应的标签信息
        comments_data = [[line[0], int(line[1])] for line in reader if len(line[0]) > 0]                      
    
    # 打乱数据集
    random.shuffle(comments_data)
    
    data = pd.DataFrame(comments_data)
    
    same_sentence_num = data.duplicated().sum()                                           # 统计重复的评论内容个数
    
    if same_sentence_num > 0:
        data = data.drop_duplicates()                                                     # 删除重复的样本信息
    
    
    f.close()
    
    return data

读取数据集信息,并输出样本的长度

comments_data = read_file('./file/comments.csv')
len(comments_data)
181945

查看所有样本信息

comments_data
01
0外形外观:时尚精美拍照效果:华为拍照效果杠杠的2
1本来我是想买苹果13,后来想了想还是支持下华为毕竟是国产之光,但拿到手机用起来感觉系统有时候...1
2手机毕竟是处理器流畅的很手机还比较轻薄还有微颜功能和功能第一感觉很棒流行一些大型游戏十分流畅...2
3京东送货非常快辛苦快递小哥啦手机看着挺好的选了黑色的毕竟年纪大了各种彩色已经不适合我了的屏幕...2
4外形外观在京东买手机好几台了这次是最差的手机像是二手的包装简陋一看就是拆封的装手机的盒子都布...0
.........
186528手机赠品一起寄回去了也显示收货了到你们哪边就说我没退回赠品扣我赠品的钱一天两天就算了能理解慢...0
186529手机已经拿到了。使用感觉很棒!手机的颜值可以的。特别是屏幕,采用了3D曲屏设计,感觉很棒。总...2
186530从开始一直都是用苹果好用是好用就是电池真的不想说1
186531第一次在京东买手机手机刚收到包装很好完整手机颜色很漂亮手感不错才开始使用看看效果怎么样总体感...2
186532好不容易抢到的,但是信号真的太差了,惆怅!天天卡的要命!1

181945 rows × 2 columns

以 6:4 的比例拆分训练集与测试集,设定切分线。

split = 0.6
split_line = int(len(comments_data) * split)
split_line
109167

划分训练集 train_comments, train_lables 与测试集 test_comments,test_lables 并输出它们的长度

# 划分训练集与测试集,并将pandas数据类型转化为列表类型
train_comments, train_labels = list(comments_data[: split_line][0]), list(comments_data[: split_line][1])
test_comments, test_labels = list(comments_data[split_line:][0]), list(comments_data[split_line:][1])

len(train_comments),len(train_labels), len(test_comments), len(test_labels)
(109167, 109167, 72778, 72778)

定义网络模型

现在我们来微调Bert,使用Bert来实现情感分析(文本分类)的效果。

默认这里使用基本模型Bert_base(bert-base-chinese),使用12层Transformer编码器块,768个隐藏单元和12个自注意头。

只需要在Bert的输出信息中提取出综合上下文信息 ‘<cls>’,并外接一层全连接层,即可完成情感分析(文本分类)效果。如下图

> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QXSHVXkM-1668581805431)(attachment:QQ%E6%88%AA%E5%9B%BE20221116144257.png)]

仍不太懂的小伙伴可以参考transform_bert官方文档,对于Bert的参数和返回有详细说明。

"""
定义BERTClassifier分类器模型
"""
class BERTClassifier(nn.Module):

    # 初始化加载 bert-base-chinese 原型,即Bert中的Bert-Base模型
    def __init__(self, output_dim, pretrained_name='bert-base-chinese'):

        super(BERTClassifier, self).__init__()
        
        # 定义 Bert 模型
        self.bert = BertModel.from_pretrained(pretrained_name)

        # 外接全连接层
        self.mlp = nn.Linear(768, output_dim)


    def forward(self, tokens_X):

        # 得到最后一层的 '<cls>' 信息, 其标志全部上下文信息
        res = self.bert(**tokens_X)

        # res[1]代表序列的上下文信息'<cls>',外接全连接层,进行情感分析 
        return self.mlp(res[1])

定义训练函数与评估函数

设计以下的评估函数和训练函数,用以对模型进行训练测试

"""
评估函数,用以评估数据集在神经网络下的精确度
"""
def evaluate(net, comments_data, labels_data):
    
    sum_correct, i = 0, 0
    
    while i <= len(comments_data):
        
        comments = comments_data[i: min(i + 8, len(comments_data))]
        
        tokens_X = tokenizer(comments, padding=True, truncation=True, return_tensors='pt').to(device=device)

        res = net(tokens_X)                                          # 获得到预测结果

        y = torch.tensor(labels_data[i: min(i + 8, len(comments_data))]).reshape(-1).to(device=device)

        sum_correct += (res.argmax(axis=1) == y).sum()              # 累加预测正确的结果
        i += 8

    return sum_correct/len(comments_data)                           # 返回(总正确结果/所有样本),精确率


"""
训练bert_classifier分类器

"""
def train_bert_classifier(net, tokenizer, loss, optimizer, train_comments, train_labels, test_comments, test_labels, device, epochs):
    
    max_acc = 0.5                                 # 初始化模型最大精度为0.5
    
    # 累计训练18万条数据 epochs 次,优化模型
    for epoch in tqdm(range(epochs)):
        
        i, sum_loss = 0, 0                           # 每次开始训练时, i 为0 表示从第一条数据开始训练
        
        # 计算训练集与测试集的精度
        train_acc = evaluate(net, train_comments, train_labels)
        test_acc = evaluate(net, test_comments, test_labels)
        
        # 输出精度
        print('\n--epoch', epoch, '\t--loss:', sum_loss / (len(train_comments) / 8), '\t--train_acc:', train_acc, '\t--test_acc', test_acc)
        
        
        
        # 如果测试集精度 大于 之前保存的最大精度,保存模型参数,并重设最大值
        if test_acc > max_acc:
            
            # 更新历史最大精确度
            max_acc = test_acc
            
            # 保存模型
            torch.save(net.state_dict(), 'bert.parameters')
        
        
        # 开始训练模型
        while i < len(comments_data):
            comments = train_comments[i: min(i+8, len(train_comments))]             # 批量训练,每次训练8条样本数据

            # 通过 tokenizer 数据化输入的评论语句信息,准备输入bert分类器
            # 输入的8个评论语句长度很可能不一致,这时取长度为最长的那个句子,padding=True代表对短句子进行填充操作
            # 当输入的某个句子过长时,使用truncation=True进行截断操作
            # return_tensors='pt' 代表返回的数据类型为 python 的 torch 类型
            tokens_X = tokenizer(comments, padding=True, truncation=True, return_tensors='pt').to(device=device)

            # 将数据输入到bert分类器模型中,获得结果
            res = net(tokens_X)

            # 批量获取实际结果信息
            y = torch.tensor(train_labels[i: min(i+8, len(train_comments))]).reshape(-1).to(device=device)

            optimizer.zero_grad()                  # 清空梯度
            l = loss(res, y)                       # 计算损失
            l.backward()                           # 后向传播
            optimizer.step()                      # 更新梯度

            sum_loss += l.detach()                # 累加损失
            i += 8                                # 样本下标累加

设置损失函数、优化方法、BertTokenizer词嵌入

本次实验中,我们这里使用交叉熵损失函数、小批量随机梯度下降,并定义 BertTokenizer 将输入的评论语句(次元序列)转化为输入Bert的数据。

device = d2l.try_gpu()                                  # 获取GPU

net = BERTClassifier(output_dim=3)                      # BERTClassifier分类器,因为最终结果为3分类,所以输出维度为3,代表概率分布
net = net.to(device)                                    # 将模型存放到GPU中,加速计算

# 定义tokenizer对象,用于将评论语句转化为BertModel的输入信息
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')

loss = nn.CrossEntropyLoss()                                # 损失函数
optimizer = torch.optim.SGD(net.parameters(), lr=1e-4)      # 小批量随机梯度下降算法

训练模型并预测结果

train_bert_classifier(net, tokenizer, loss, optimizer, train_comments, train_labels, test_comments, test_labels, device, 20)

运行结果如下

在这里插入图片描述

经过5个小时左右的模型训练,在测试集上的精确度最高能够达到82.38%,相对于之前的RNN系列的结果(75%左右),提升了7个百分比左右,而且我们并没有作任何调整优化操作,所以Bert预训练模型还是很强大的。

现在,我们再次可视化测试一下模型预测的准确度(测试集上),如下

# 定义模型
net = BERTClassifier(output_dim=3)
net = net.to(device)

# 加载训练好的模型参数
net.load_state_dict(torch.load('./bert.parameters'))

start = 0
while start < 20:

    comment = test_comments[start]
    token_X = tokenizer(comment, padding=True, truncation=True, return_tensors='pt').to(device)
    label = test_labels[start]                          # 实际结果
    result = net(token_X).argmax(axis=1).item()         # 得到预测结果
	
	# 打印评论语句
    print(comment)
    
    # 输出预测结果
    if result == 0:
        print('预测结果: ', 0, '----》差评', end='\t')
    elif result == 1:
        print('预测结果: ', 1, '----》中评', end='\t')
    else:
        print('预测结果: ', 2, '----》好评', end='\t')

	# 输出实际结果
    if label == 0:
        print('实际结果: ', 0, '----》差评', end='\t')
    elif label == 1:
        print('实际结果: ', 1, '----》中评', end='\t')
    else:
        print('实际结果: ', 2, '----》好评', end='\t')

    if result == label:
        print('预测正确')
    else:
        print('预测错误')

    start += 1

运行结果如下:
在这里插入图片描述

小结

本次使用Bert实现情感分类总体上得到了不错的效果,但是仍有值得改进的地方,如未对评论数据作预处理操作,也未改善优化Bert分类器的基本结构,仅仅是外接了一个全连接层,未采用更佳的优化算法、未支持动态学习率变化等。

另外,Bert模型也存在一些缺陷,Bert是基于字粒度对文本数据进行划分的,我们也可以采用Bert的改进模型RoBERTa模型,或者中文语义更强的ERNIE模型来构建预训练模型。

希望本次分享可以帮助到大家。

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

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

相关文章

AVL树的插入(C++实现)

1. 概念 AVL树&#xff08;Adelson-Velsky and Landis Tree&#xff09;于1962年被提出&#xff0c;是计算机科学中最早被发明的平衡二叉查找树。AVL树得名于它的发明者G. M. Adelson-Velsky和Evgenii Landis。 在AVL树中&#xff0c;任一节点对应的两棵子树的最大高度差为1&…

大一新生HTML期末作业 个人网页王嘉尔明星介绍网页设计与制作

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

领悟《信号与系统》之 信号与系统的描述-上节

信号与系统的描述-上节一、信号分类1. 一维信号和多维信号2. 确定信号和随机信号3. 连续时间信号和离散时间信号4. 周期信号和非周期信号5. 能量信号和功率信号二、常见工程信号1. 正弦信号2. 指数信号3. 取样信号4. 单位矩形脉冲信号5. 符号函数在真实的物理世界中&#xff0c…

Redis的优惠券秒杀问题(五)全局唯一ID 以及 秒杀下单

Redis的优惠券秒杀问题&#xff08;五&#xff09;全局唯一ID 以及 秒杀下单 关于优惠秒杀问题的Redis实现章节总览 全局唯一ID 场景分析 不能用自增的原因 id的规律性太明显 受单表数据量的限制 全局唯一ID的条件 全局唯一ID的Redis实现 代码实现 单元测试 其它…

【FPGA】FPGA实现SPI协议读写FLASH(一)----- M25P16操作概述

文章目录一、FLASH介绍&#xff08;M25P16&#xff09;1、M25P16概述2、SPI模式3、存储结构4、指令集5、时间参数二、M25P16工作原理三、M25P16指令操作1、页编程 (PP)2、扇区擦除和整块擦除 (SE and BE)3、写使能 (WREN)4、读ID&#xff08;RDID&#xff09;5、读状态寄存器&a…

使用c#将aj-report桌面化:1.winform嵌入浏览器

说到底,aj-report是个工具,我想大多数人还是想快速使用它来创建一个可以展示的工具。通过之前的章节,你应该可以制作自己的报表页面了,下面我们来看看怎么把aj-report包装成一个桌面能够运行的软件。 当然作为扩展开发,受开源协议限制,我们不能大规模修改aj-report的源代…

【毕业设计】深度学习图像修复算法研究与实现 - python

文章目录1 前言2 什么是图像内容填充修复3 原理分析3.1 第一步&#xff1a;将图像理解为一个概率分布的样本3.2 补全图像3.3 快速生成假图像3.4 生成对抗网络(Generative Adversarial Net, GAN) 的架构3.5 使用G(z)生成伪图像4 在Tensorflow上构建DCGANs5 最后1 前言 &#x1…

PC_OS中断/中断屏蔽字

文章目录程序中断&#x1f383;中断概念中断功能(作用)中断请求中断源中断分类外中断非屏蔽中断和可屏蔽中断陷入(内中断)硬件中断和软件中断关系整理&#x1f388;中断判优&#x1f388;中断优先级CPU响应中断的条件外中断实现思路&#x1f388;中断隐指令 及其工作①关中断②…

下一个倒下的是不是Genesis

今日&#xff0c;一个关于“Genesis今晚破产”的传言在各个社交平台传播&#xff0c;包括行业的KOL也在讨论这个事情&#xff0c;认为Genesis或存在偿付能力问题&#xff0c;该公司将于美国东部时间11月17日8&#xff1a;00am与债权人通话以解释情况。若消息属实&#xff0c;Ge…

【AGC】flutter之agconnect_crash在ios上崩溃

问题背景 flutter agconnect_crash-1.2.0300 运行在ios平台上&#xff0c;出现了如下这个崩溃 NSInvalidArgumentException: *** [NSJSONSerialization dataWithJSONObject:options:error:]: value parameter is nil 0 CoreFoundation 0x00000001830d005c 0x183037000 62678…

视频讲解vue2基础之渲染v-if/v-show/v-for/v-html

大家好&#xff0c;我是你们的老朋友lqj_本人&#xff0c;最近一周没有更新文章了&#xff0c;是因为最近学校有一些活动比赛&#xff0c;也有一部分原因就是我在录制一些关于前端方面的视频&#xff0c;涉及到的领域主要一前端&#xff0c;比如&#xff1a;H5开发&#xff0c;…

项目经理如何搞懂难缠的客户【静说】

作为乙方的项目经理&#xff0c;是否经常遇见难缠的客户&#xff0c;现环境下&#xff0c;大部分都是甲方强势&#xff0c;乙方弱势&#xff0c;双方处于一种不对等的基础上&#xff0c;项目经理如何生产&#xff0c;成功交付项目呢&#xff1f; 流程机制上如何应对&#xff1…

计算机毕业设计jsp教师课堂教学评价系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 教师课堂教学评价系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql&#xff0c…

智慧公交解决方案-最新全套文件

智慧公交解决方案-最新全套文件一、建设背景二、思路架构三、建设方案3大能力&#xff1a;1、数据驱动的智慧公交全息感知能力2、精细化精准化的公交健康诊断能力3、高品质的公交运营组织能力6大系统&#xff1a;1、公交线网健康诊断系统2、职能部门指挥决策支持系统3、公共出行…

Spring读取.xml和通过Java类配置对比

Spring读取配置文件获取容器,通过容器获得javaBean演示 1.创建一个空项目 配置项目JDK 新建module 选择Maven项目 注意路径 pom.xml文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"…

C实现扫雷小游戏(简易版)

你知道&#xff0c;有些鸟儿是注定不会被关在牢笼里的&#xff0c;它们的每一片羽毛都闪耀着自由的光辉。——《肖申克的救赎》 目录 1、设计框架 2、设计流程 2.1菜单 2.2初始化雷阵 2.3生成雷 2.4玩家输入坐标 2.5显示有多少个雷 3、所有程序的源码 3.1game.h 3.2…

Slimming剪枝方法

本文参考&#xff1a;5-剪枝后模型参数赋值_哔哩哔哩_bilibiliz https://github.com/foolwood/pytorch-slimming 一、模型剪枝理论说明 论文&#xff1a;Learning Efficient Convolutional Networks through Network Slimming &#xff08;1&#xff09;卷积后得到多个特征图…

通过逻辑回归和感知器算法对乳腺癌数据集breastCancer和鸢尾花数据集iris进行线性分类

逻辑回归和感知器算法进行线性分类 代码使用了LogisticRegression和Perceptron两种分类方法 # 使用LogisticRegreeion分类器学习和测试 lr LogisticRegression() lr.fit(X_train_scaler, y_train) y_pred_lr lr.predict(X_test_scaler)#定义感知机 perceptron Perceptron(…

N3-PEG-ALD,Azide-PEG-Aldehyde,醛基-聚乙二醇-叠氮

1、名称 英文&#xff1a;N3-PEG-ALD&#xff0c;Azide-PEG-Aldehyde 中文&#xff1a;叠氮-聚乙二醇-醛基 2、CAS编号&#xff1a;N/A 3、所属分类&#xff1a;Aldehyde / Acetal PEG Azide PEG 4、分子量&#xff1a;可定制&#xff0c;5000 N3-PEG-ALD、10000 叠氮-PEG…

用html做一个漂亮的网站【茶文化12页】期末网页制作 HTML+CSS网页设计实例 企业文化网站制作

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…