PaddleNLP本文分类及docker部署流程

本文记录使用PaddleNLP进行文本分类的全流程

参考:https://github.com/PaddlePaddle/PaddleNLP/tree/develop/legacy/applications/text_classification/multi_class

文章目录

    • 1. 数据准备
    • 2. 模型训练
      • 2.1 准备关键库
      • 2.2 模型训练&验证
      • 2.3 模型测试
      • 2.4 结果分析
    • 3. 模型部署
      • 3.1 编写预测脚本
      • 3.2 Flask搭建服务
      • 3.3 Docker包装环境
        • gpu版本
    • 4. 小结

1. 数据准备

【STEP1】:拿到用来训练的数据 train.xlsx 和用来测试的数据 test.xlsx,确定训练集测试集数据来源一致。

【STEP2】:PaddleNLP要求训练过程中有三个文件:train.txt,val.txt,class.txt

  • 准备train.txt和val.txt: 将用来训练的数据划分训练集和验证集,PaddleNLP要求训练和验证集采用’.txt’文件格式,并且每一行数据为:content + ‘\t’ + label 的形式

  • 准备class.txt: 将类别标签那一列去重后保存,每一行是一个类别

def prepare_txt(data_fp,tar_fp,rate):"""准备训练、验证数据集以及标签文件:param data_fp:训练数据路径:param tar_fp:保存处理好的数据文件夹路径:param rate:训练集比率:return:"""data = pd.read_excel(data_fp, rate)# 保存类别标签数据class.txtdata['label'].drop_duplicates().to_csv(os.path.join(tar_fp, 'class.txt'),index=False,header=None)data_shuffle = data.sample(frac=1).reset_index(drop=True)print(f"处理后:{data.shape[0]}")length = data.shape[0]train_num = int(length * rate)test_num = length - train_numtrain_data = data_shuffle.iloc[: train_num, :]test_data = data_shuffle.iloc[train_num:, :]train_data_txt = train_data[['content', 'label']]test_data_txt = test_data[['content', 'label']]# 保存训练集和验证集with open(os.path.join(tar_fp,'train_data.txt'),'w',encoding='utf-8') as f:for i in tqdm(range(len(train_data_txt))):f.write(str(train_data_txt.iloc[i,0]) + '\t' + str(train_data_txt.iloc[i,1]) + '\n')with open(os.path.join(tar_fp,'test_data.txt'),'w',encoding='utf-8') as f:for i in tqdm(range(len(test_data_txt))):f.write(str(test_data_txt.iloc[i,0]) + '\t' + str(test_data_txt.iloc[i,1]) + '\n')

设置好路径运行后得到三个文件:

image-20240713174240011.png

2. 模型训练

2.1 准备关键库

安装关键库:paddlepaddle-gpu建议根据官网安装教程安装,选择适配的版本(安装教程:https://www.paddlepaddle.org.cn/install/old?docurl=/documentation/docs/zh/install/pip/windows-pip.html)

python==3.9.19
paddlenlp==2.5.2
paddlepaddle-gpu==2.5.2.post120
pandas==1.5.2
sklearn==1.0.2
numpy==1.23.5

把PaddleNLP目录下的train.py和utils.py粘贴到本地项目中:(PaddleNLP版本为2.5.2好像把evaluate.py合并到train.py中,设置参数do_eval就可以完成验证)
image-20240715105525090.png

2.2 模型训练&验证

主要调整:(各个参数含义在参考网站下有说明)

  • batch_size:越大越好占满显存
  • model_name_or_path:选择需要使用的模型,综合考虑运行时间和精度
  • early_stopping和early_stopping_patience:早停策略:n个epoch没提升就停止训练,节省时间,同时可以把num_train_epochs设大一点
  • train_path / dev_path / label_path:替换训练集和验证集路径
python ./train.py \--do_train \--do_eval \--do_export \--dataloader_num_workers 8 \--model_name_or_path ernie-3.0-medium-zh \--output_dir ./dispose_model_2024712 \--overwrite_output_dir True \--load_best_model_at_end True \--early_stopping True \--early_stopping_patience 3 \--device gpu \--num_train_epochs 100 \--logging_steps 5 \--evaluation_strategy epoch \--save_strategy epoch \--per_device_train_batch_size 128 \--per_device_eval_batch_size 128 \--max_length 128 \--save_total_limit 1 \--train_path ./train_data.txt \ # 替换准备好的训练数据集路径--dev_path ./test_data.txt \    # 替换准备好的验证数据集路径--label_path ./class.txt        # 替换准备好的标签集合路径

终端有训练日志输出即为开启训练,loss有下降说明正常训练,根据设置的参数每个epoch结束会在验证集上验证结果:

image-20240715110607930.png

2.3 模型测试

【注】老版本可以用evaluate.py来预测测试集得到模型预测结果,新版本需要将模型导出为pdmodel格式后采用Taskflow进行预测,流程如下:

【Step1】模型导出:训练保存的模型(以及相关文件) VS 导出的模型(以及相关文件)差别对比:(主要多了一个.pdmodel文件)

image-20240715111340159.png

模仿train.py中的模型导出写法,编写模型导出脚本,修改export_model_dir、model_name_or_path、class_txt路径

from paddlenlp.transformers import (AutoModelForSequenceClassification,AutoTokenizer,export_model,
)
import paddle
import json
import os
from paddlenlp.utils.log import logger
export_model_dir = './export' # 模型导出路径
model_name_or_path = './model_fp'      # 训练好的模型路径
class_txt = './model_fp/class.txt'     # 类别标签txt
cls_list = []
with open(class_txt,'r',encoding='utf-8') as f:for line in f.readlines():cls_list.append(line.strip())
id2label = {}
label2id = {}
with open(class_txt, 'r', encoding='utf-8') as f:for i,line in enumerate(f.readlines()):id2label[f"{i}"] = line.strip()label2id[line.strip()] = i
input_spec = [paddle.static.InputSpec(shape=[None, None], dtype="int64", name="input_ids")]
# input_spec = [
#     paddle.static.InputSpec(shape=[None, None], dtype="int64", name="input_ids"),
#     paddle.static.InputSpec(shape=[None, None], dtype="int64", name="token_type_ids"),
# ]
model = AutoModelForSequenceClassification.from_pretrained(model_name_or_path, label2id=label2id, id2label=id2label)
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
export_model(model=model, input_spec=input_spec, path=export_model_dir)
tokenizer.save_pretrained(export_model_dir)
id2label_file = os.path.join(export_model_dir, "id2label.json")
with open(id2label_file, "w", encoding="utf-8") as f:json.dump(id2label, f, ensure_ascii=False)logger.info(f"id2label file saved in {id2label_file}")

【Step2】模型预测:基于Taskflow编写预测脚本

from paddlenlp import Taskflow
import pandas as pd
from tqdm import tqdm
import os
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score,classification_report
import json
import numpy as np
def predict(model_fp, content_fp, save_fp):"""预测结果Args:model_fp (str): 导出的模型路径content_fp (str): 待预测的测试集路径(写的read_table读取txt)save_fp (str): 保存路径"""# 模型预测model = Taskflow("text_classification", task_path=model_fp, is_static_model=True)content = pd.read_table(content_fp, header=None, encoding='utf_8_sig')content.columns = ['data', 'label']# 初始化 predict 和 scorecontent['predict'] = content['label']content['score'] = content['label']for i in tqdm(range(content.shape[0])):tmp = content.loc[i, 'data']pred = model([content.loc[i, 'data']])[0] # listcontent.loc[i, 'predict'] = pred['predictions'][0]['label']content.loc[i, 'score'] = pred['predictions'][0]['score']content.to_excel(os.path.join(save_fp, 'test_data_predict.xlsx'))
if __name__ == '__main__':# 预测model_fp = './dispose_model_2024712/model_2024712/export'content_fp = './dispose_data_2024712/data_2024712/test_data.txt'save_fp = './dispose_data_2024712/report_valid'predict(model_fp, content_fp, save_fp)

预测完成后打开test_data_predict.xlsx查看结果:存放了输入文本、标签、预测结果、置信度得分

image-20240715112252328.png

2.4 结果分析

计算各个类别的指标,思路:

  • 编写Metric类,存放数据读取、预处理和指标计算的方法
  • preprocess():测试数据预处理:例如清洗没有标签的测试数据等,根据不同的数据和业务需求来定义
  • cal_metrics_class()和cal_metrics_all():分别用来计算各个类的指标和总体指标并保存
  • compute():预处理 => 计算指标 => 保存
import pandas as pd
import os
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score,classification_report
import json
import numpy as np
id2label = None
with open('./id2label.json', 'r', encoding='utf-8') as file:id2label = json.load(file)
label2id = {value: int(key) for key, value in id2label.items()}
class Metric:def __init__(self, pred_fp, true_label_col, pred_label_col, save_fp, score_thresh=0.9):self.pred_fp = pred_fpself.true_label_col = true_label_colself.pred_label_col = pred_label_colself.save_fp = save_fpself.score_thresh = score_threshself.df = pd.read_excel(pred_fp)# 确保输入的列名存在assert self.true_label_col in self.df.columns, f"{self.true_label_col} not in DataFrame columns"assert self.pred_label_col in self.df.columns, f"{self.pred_label_col} not in DataFrame columns"self.label2id = label2idself.id2label = id2labelprint(f"测试集数量:{self.df.shape[0]}")def compute(self):self.preprocess()# self.cal_metrics_all()self.cal_metrics_class()def preprocess(self):print(f"所有测试数据条数为:{self.df.shape[0]}")# 删除没有预测的项(label中包含测试的项目)self.df['label'] = self.df['label'].replace(['',' '], np.nan)self.df.dropna(axis=0, subset = ["label"], how='any', inplace=True)print(f"删除未测试项后数据条数为:{self.df.shape[0]}")# 根据 score_thresh 筛选数据if self.score_thresh is not None:self.df = self.df[self.df['score'] > self.score_thresh]print(f"筛选score>{self.score_thresh}的数据有: {self.df.shape[0]}")def cal_metrics_class(self):# 生成分类报告report = classification_report(self.df[self.true_label_col],self.df[self.pred_label_col],output_dict=True)report_df = pd.DataFrame(report).Tcls_id = list(report.keys())cls_name = [self.id2label[id.split('.')[0]] for id in cls_id[:-3]] # 去掉'accuracy', 'macro avg', 'weighted avg'这三个report_df['category'] = cls_name + cls_id[-3:] # 加上'accuracy', 'macro avg', 'weighted avg'这三个report_df = report_df[['category','precision','recall','f1-score','support']] # 调整一下顺序,把类别放第一个# 重命名列以更清晰地表示指标if self.save_fp is not None:report_df.to_excel(os.path.join(self.save_fp, f'metrics_class_{self.score_thresh}.xlsx'), index=False)def cal_metrics_all(self):# 计算各项指标accuracy = accuracy_score(self.df[self.true_label_col], self.df[self.pred_label_col])precision = precision_score(self.df[self.true_label_col], self.df[self.pred_label_col], average='weighted')recall = recall_score(self.df[self.true_label_col], self.df[self.pred_label_col], average='weighted')f1 = f1_score(self.df[self.true_label_col], self.df[self.pred_label_col], average='weighted')report_dict = {'Accuracy': accuracy,'Precision': precision,'Recall': recall,'F1 Score': f1}report_list = [value for value in report_dict.values()]# 然后,创建DataFramereport_df = pd.DataFrame(report_list, index=list(report_dict.keys())).Treport_df.columns = list(report_dict.keys())if self.save_fp is not None:report_df.to_excel(os.path.join(self.save_fp, f'metrics_all_{self.score_thresh}.xlsx'), index=False)# 返回指标字典return report_df

【注】分析过程中出现的问题:

  • 模型精度过低排除参数设置的问题外,基本都是数据问题,例如:测试集和训练集标签差别很大、
  • 一般加载ERNIE模型后训练10-20个epoch左右基本就可以稳定
  • 数据增强的作用在项目中微乎其微,不如清洗脏数据

3. 模型部署

模型部署主要分为以下步骤:

  • 编写预测脚本:调用上一步中训练好的模型,通过接收’POST’请求的方式封装成预测函数
  • Flask搭建服务:使用Flask给预测函数搭建搭建一个微服务
  • Docker包装环境:编写Dockerfile构建镜像(如果服务器有gpu可以构建gpu镜像)
    • 编写dockerfile:选择基础镜像构建环境(docker build)
    • 运行镜像形成容器:docker run
    • 镜像确认无误后移植到服务器上运行,将服务器端口号与容器端口号对应上

3.1 编写预测脚本

  • 将模型和相关文件保存到对应文件夹
  • 编写调用训练好的模型进行预测的函数predict(),根据业务需求设定判断条件
  • 保存日志并以json的形式返回结果
def predict():if request.method == 'POST':start_time = time.time()s1 = request.jsonif "content" not in s1:return jsonify({"success":False, "data":[], "message":"missing content"})if "streetName" not in s1:return jsonify({"success":False, "data":[], "message":"missing streetName"})if "topK" not in s1:return jsonify({"success":False, "data":[], "message":"missing topK"})data = s1['content'].replace(' ', '').replace('\n', '').replace('\t', '').replace('\r', '')street_name=s1['streetName']topn=int(s1['topK'])itext_all=data.replace('\n', '').replace('\r', '').replace('\t', '').replace(' ', '')+street_nameresult = predict_dispose(itext_all, topn)logger.info(f"{result} ----time cost: {(time.time() - start_time):.4f}")return jsonify({"success":True,"data":result})

3.2 Flask搭建服务

基于Flask搭建微服务

  • 给函数加上修饰器,指定路由
  • 在主函数中启动服务
from flask import Flask, request, jsonify
import time
from config.utils import get_logger, get_config
from predict.load_model import init_model
from predict.load_model import predict_dispose
@app.route('/predict/dispose', methods=['GET', 'POST'])
def predict():pass
if __name__ == '__main__':config = get_config()logger = get_logger("dispose")init_model("dispose", config) # 加载模型参数app.run(host='0.0.0.0', port=config.getint("service", "dispose_port")) # 根据配置文件获取端口号

3.3 Docker包装环境

将环境打包成requirements.txt(或者自己写一下requirements)

conda list -e > requirements.txt

下载并安装docker,编写Dockerfile:

FROM python:3.9.19 # 基础镜像
RUN mkdir -p /app/${MODEL_PATH} # 新建app文件夹
# 把代码和模型COPY至镜像中
COPY config /app/config 
COPY ./models/dispose/pingshan app/models/dispose/pingshan/
COPY ./predict app/predict
COPY dispose_api.py ./app/dispose_api.py
COPY utils.py ./app/utils.py
COPY requirements.txt ./app
# 设置工作路径
WORKDIR /app
# 根据requirements.txt安装库
RUN pip install -r requirements.txt -i https://mirrors.ustc.edu.cn/pypi/web/simple
# 启动服务
ENTRYPOINT ["python", "dispose_api.py"]

使用docker build创建镜像:用-ip进行端口映射

docker build -t hs_classification_service . 

创建成功后通过命令行输入 docker images 可以看到
image-20240715204555539.png
本地启动服务,使用postman请求该服务确保没问题:docker run IMAGE ID

docker run -it --gpus all -ip <服务器端口>:<容器端口> 5c # -it显示终端结果  --gpus all:调用gpu,能在容器内用nvidia-smi

image-20240715204414446.png

把镜像保存到本地.tar文件

docker save -o D:\work\codes\proj\dockerfiles\docker_images\image1.tar 5ce023c786f6

image-20240716152859887.png
把镜像文件传到服务器上(可以通过MobaXterm传输),通过docker load -i .tar来加载镜像,加载成功后用docker images查看

docker load -i .tar

在服务器上用docker run启动服务,并在本地电脑上使用postman向服务发送请求,正确返回预测结果即部署成功(cpu版本)

gpu版本

如果服务器有显卡则需要用docker部署gpu版本的paddle:

docker中配置cuda的环境变量:

echo "export LD_LIBRARY_PATH=/dmdbms_x86/bin:/usr/local/cuda-11.0/lib64:\$LD_LIBRARY_PATH" >> /root/.bashrc 

4. 小结

应用过程中一般使用现有模型就能满足大多数需求,如果精度差距很远多半是数据原因
模型部署需要知道的框架:flask\docker\nginx\

  • flask:轻量级python服务框架
  • docker:容器、镜像服务,模型部署必备
  • nginx:网络服务,在对方服务器只能访问我方唯一端口时需要

部署流程:

  1. 本地编写Dockerfile成功build一个镜像
  2. 在flask run处指定host=0.0.0.0,端口号=指定端口号,并将docker中的端口号暴露出来
  3. 将该镜像传到服务器上,使用docker run

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

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

相关文章

分布式中间件-redis相关概念介绍

文章目录 什么是redis?示意图Redis的主要特点Redis的主要用途Redis的工作原理Redis的持久化与备份 redis 6.x新增特性多线程数据加载客户端缓存新的 RESP 3 协议支持ACL&#xff08;Access Control List&#xff09;功能新增数据类型性能改进配置文件的改进其他改进 redis数据…

02 基于STM32的按键控制继电器驱动电机

本专栏所有源资料都免费获取&#xff0c;没有任何隐形消费。 注意事项&#xff1a;STM32仿真会存在各种各样BUG&#xff0c;且尽量按照同样仿真版本使用。本专栏所有的仿真都采用PROTEUS8.15。 本文已经配置好STM32F103C8T6系列&#xff0c;在PROTUES仿真里&#xff0c;32单片…

Doker学习笔记--黑马

介绍&#xff1a;快速构建、运行、管理应用的工具 在不同的服务器上部署多个应用&#xff0c;但是往往不同应用之间会有冲突&#xff0c;因为它们所依赖的环境&#xff0c;函数库&#xff0c;配置都不一样&#xff0c;此时docker在运行时形成了一个隔离环境&#xff08;容器&am…

【C++篇】C++类与对象深度解析(三):类的默认成员函数详解

文章目录 【C篇】C类与对象深度解析&#xff08;三&#xff09;前言4. 运算符重载基本概念4.1 运算符重载的基本概念4.2 重载运算符的规则4.3 成员函数重载运算符4.4 运算符重载的优先级与结合性4.5 运算符重载中的限制与特殊情况4.5.1 不能创建新的操作符4.5.2 无法重载的运算…

QT 带箭头的控件QPolygon

由于对当前项目需要绘制一个箭头控件&#xff0c;所以使用了QPainter和QPolygon来进行绘制&#xff0c;原理就是计算填充&#xff0c;下面贴出代码和效果图 这里简单介绍下QPolygon QPolygon是继承自 QVector<QPoint>那么可以很简单的理解为&#xff0c;他就是一个点的…

Leetcode面试经典150题-138.随机链表的复制

题目比较简单&#xff0c;重点是理解思想&#xff0c;random不管&#xff0c;copy一定要放在next 而且里面的遍历过程不能省略 解法都在代码里&#xff0c;不懂就留言或者私信 /* // Definition for a Node. class Node {int val;Node next;Node random;public Node(int val…

springboot-创建连接池

操作数据库 代码开发步骤&#xff1a; pom.xml文件配置依赖properties文件配置连接数据库信息&#xff08;连接池用的是HikariDataSource&#xff09;数据库连接池开发 configurationproperties和value注解从properties文件中取值bean方法开发 service层代码操作数据库 步骤&am…

数据分析师的得力助手:vividime Desktop让数据分析变得更简单高效

在数据驱动决策的今天&#xff0c;数据分析已成为企业不可或缺的一部分。面对海量的数据和复杂的业务需求&#xff0c;一款高效、易用的报表工具显得尤为重要。本文将深入解析为何一款优秀的报表工具对于数据分析至关重要&#xff0c;并以市场上备受好评的免费BI工具——vividi…

集成学习详细介绍

以下内容整理于&#xff1a; 斯图尔特.罗素, 人工智能.现代方法 第四版(张博雅等译)机器学习_温州大学_中国大学MOOC(慕课)XGBoost原理介绍------个人理解版_xgboost原理介绍 个人理解-CSDN博客 集成学习(ensemble)&#xff1a;选择一个由一系列假设h1, h2, …, hn构成的集合…

YOLOv10改进系列,YOLOv10损失函数更换为Powerful-IoU(2024年最新IOU),助力高效涨点

改进前训练结果: 改进后的结果: 摘要 边界框回归(BBR)是目标检测中的核心任务之一,BBR损失函数显著影响其性能。然而,观察到现有基于IoU的损失函数存在不合理的惩罚因子,导致回归过程中锚框扩展,并显著减缓收敛速度。为了解决这个问题,深入分析了锚框扩展的原因。针…

【网络】详解HTTP协议的CGI机制和CGI进程

目录 引言 CGI机制模型 伪代码示例 个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 引言 CGI机制是HTTP协议提供的偏底层的一套机制&#xff0c;也是非常重要的机制——它让大量的业务进程和HTPP协议解耦。而CGI进程是业务层的&#xff0c;用来处理各种数据&#xff0c;比…

OpenCV结构分析与形状描述符(24)检测两个旋转矩形之间是否相交的一个函数rotatedRectangleIntersection()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 测两个旋转矩形之间是否存在交集。 如果存在交集&#xff0c;则还返回交集区域的顶点。 下面是一些交集配置的例子。斜线图案表示交集区域&#…

孙怡带你深度学习(2)--PyTorch框架认识

文章目录 PyTorch框架认识1. Tensor张量定义与特性创建方式 2. 下载数据集下载测试展现下载内容 3. 创建DataLoader&#xff08;数据加载器&#xff09;4. 选择处理器5. 神经网络模型构建模型 6. 训练数据训练集数据测试集数据 7. 提高模型学习率 总结 PyTorch框架认识 PyTorc…

Vue2电商平台项目 (三) Search模块、面包屑(页面自己跳自己)、排序、分页器!

文章目录 一、Search模块1、Search模块的api2、Vuex保存数据3、组件获取vuex数据并渲染(1)、分析请求数据的数据结构(2)、getters简化数据、渲染页面 4、Search模块根据不同的参数获取数据(1)、 派发actions的操作封装为函数(2)、设置带给服务器的参数(3)、Object.assign整理参…

如何通过OceanBase的多级弹性扩缩容能力应对业务洪峰

每周四晚上的10点&#xff0c;都有近百万的年轻用户进入泡泡玛特的抽盒机小程序&#xff0c;共同参与到抢抽盲盒新品的活动中。瞬间的并发流量激增对抽盒机小程序的系统构成了巨大的挑战&#xff0c;同时也对其数据库的扩容能力也提出了更高的要求。 但泡泡玛特的工程师们一点…

Redhat 7,8,9系(复刻系列) 一键部署Oracle19c rpm

Oracle19c前言 Oracle 19c 是甲骨文公司推出的一款企业级关系数据库管理系统,它带来了许多新的功能和改进,使得数据库管理更加高效、安全和可靠。以下是关于 Oracle 19c 的详细介绍: 主要新特性 多租户架构:支持多租户架构,允许多个独立的数据库实例在同一个物理服务器上…

JDBC API详解一

DriverManager 驱动管理类&#xff0c;作用&#xff1a;1&#xff0c;注册驱动&#xff1b;2&#xff0c;获取数据库连接 1&#xff0c;注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); 查看Driver类源码 static{try{DriverManager.registerDriver(newDrive…

java十进制码、六进制码和字符码的转换

一、字符转换为ASCII码&#xff1a; int i(int)1; 二、ASCII码转换为字符&#xff1a; char ch (char)40; 三、十六进制码转换为字符&#xff1a; char charValue (char)\u0040; package week3;public class check_point4_8 {public static void main(String[] args) {S…

谷歌怎么像图里这样多开贴吧号??

&#x1f3c6;本文收录于《CSDN问答解惑-专业版》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收…

数据库三范式和ER图详解

数据库设计三范式 第一范式&#xff1a;要求数据表中的字段&#xff08;列&#xff09;不可再分(原子性) 第二范式&#xff1a;不存在非关键字段(非主键)对关键字段(主键)的部分依赖 ps: 主要是针对联合主键,非主键不能只依赖联合主键的一部分 联合主键,即多个列组成的主键 第…