「转载」北京大学计算机基础能力手册
本网站仅做转载，最终解释权仍在原作者。点击下载main.pdf北京大学计算机基础能力手册
前言：本文基于数学与大数据学院2024级数据科学与大数据技术专业课程《分布式数据库原理及技术》。该课程考核难度较大，需特别用心认真听课！我将个人笔记开源，旨在为后续的新同学提供复习参考，欢迎交流与讨论（虽然我也可能忘记了（（Chiale2026.8.1资料下载： https://pan.baidu.com/s/1k6hAS7tVlPAeOwbevS7b7A?pwd=t2uv 提取码: t2uv第一篇 概论与基础第一章 分布式数据库概述1.1 前沿技术科普以下技术作为通识了解即可，构成本课程的学习背景：技术理解数据中台把企业内分散的数据资产汇聚、加工、服务化，向业务统一供给数据湖仓（Lakehouse）同时具备数据湖的灵活存储与数据仓库的事务/Schema 能力数据脱敏对敏感字段（如身份证、手机号）做变形或替换以保护隐私分布式数据库的雏形（ICBC Bank）工商银行早期基于大型机的分布式账务系统区块链多方参与的链式账本，依赖共识算法与不可篡改结构TCP 三次握手四次挥手面向连接的传输层建连与断连流程联邦学习 / 深度学习多方在不共享原始数据的前提下联合训练模型结论：以上内容了解即可，作为科普知识看待，本课程重点在分布式数据库系统（Hive / Hadoop）。1.2 分布式数据库与传统 RDBMS 的对比Q1：传统的三级模式 / 两级映像在分布式数据库中是否改变？A：保留原有结构，但增加了新内容——引入了 全局模式（Global Schema） 和 分片模式（Fragmentation Schema）。全局模式：从用户视角看，"整库"长什么样分片模式：说明全局关系如何被切分到不同节点原有"外模式 / 模式 / 内模式 + 映像"仍在，但被扩展以适配分布式存储Q2：分布式数据库在存储/检索效率上通过何种指标与传统 RDBMS 进行对比？A：两个核心指标——存储空间大小：分布式能否减少单点膨胀检索效率：是否仍能满足查询延迟与吞吐Q3：HDFS（GFS）在存储体系上如何实现共享服务？课堂问答留作思考。补充资料：02 Hadoop与HIve.pptx、0 hdfs shell操作.txtQ4：HIVE 的三种部署方式及使用环境部署方式metastore 存储适用场景内嵌模式（Derby）嵌入式 Derby 数据库单人本地调试；只允许一个会话本地模式（Local）MySQL/PostgreSQL 等独立 RDBMS单人开发 / 教学实验，HIVE 与 metastore 在同一进程远程模式（Remote）独立的 MySQL 服务多客户端并发访问同一 metastore，生产环境图中标注：内模式对应"存储"，模式对应"放表的"，外模式对应"逻辑独立"；映像体现了 create view as select ... 的位置——逻辑独立、存储独立。1.3 数据分类类型典型代表处理方式结构化MySQL、Oracle、关系表传统 SQL 数据库半结构化JSON、XML可用 Hive JSON SerDe、Spark 解析非结构化视频、图片YOLO 等模型识别 → 输出 JSON/CSV → 落入数据库第二章 Hadoop 与 HIVE 体系结构2.1 HDFS（GFS）存储体系HDFS（Hadoop Distributed File System）：源自 Google GFS 论文核心特性：分块存储、多副本容错、一次写多次读默认存储路径：/warehousedir/home/xxx.db/表名/（Hive on HDFS 默认行为）2.2 HIVE 三种工作模式模式metastore 位置是否允许多会话Derby嵌入式（每个客户端独立）否本地（Local）同节点 MySQL/PG单客户端远程（Remote）远程独立 MySQL是2.3 集群环境与启停流程集群环境：角色IP用户master192.168.5.134root / 123123slave1192.168.5.135root / 123123slave2192.168.5.136root / 123123MySQL 安装在 slave2，HIVE 启用本地模式；统一数据库名 bigdata2024；本地数据目录 /data。集群启动流程：关闭防火墙（三台虚拟机）systemctl stop firewalld启动 Zookeeper（三台虚拟机）cd $ZOOKEEPER_HOME bin/zkServer.sh start bin/zkServer.sh status # 1 个 leader，2 个 follower启动 Hadoop（master）cd $HADOOP_HOME sbin/start-all.sh在三台机器上 jps 查看进程。启动 HIVE（slave2 启动 MySQL，slave1 或 master 启动 HIVE）# slave2 systemctl start mysqld # slave1 或 master cd $HIVE_HOME bin/hive集群关闭流程（与启动对称）：hive> quit;master：sbin/stop-all.sh三台机器：cd $ZOOKEEPER_HOME && bin/zkServer.sh stopshutdown -h now2.4 HIVE 基础配置与指令配置分类：类别方式持久性临时配置hive> set key=value;仅当前会话永久配置修改 hive-site.xml持久化常用配置（来自 执行命令集合.txt）：-- 在命令行中显示当前数据库名 set hive.cli.print.current.db=true; -- 查询结果显示列名 set hive.cli.print.header=true; -- 启用桶表 set hive.enforce.bucketing=true; -- 压缩 HIVE 中间结果 set hive.exec.compress.intermediate=true; -- 让 HIVE 尽量尝试 local 模式查询 set hive.exec.mode.local.auto=true; -- 动态分区非严格模式 set hive.exec.dynamic.partition.mode=nonstrict; -- ORC 表事务相关 set hive.support.concurrency=true; set hive.txn.manager=org.apache.hadoop.hive.ql.lockmgr.DbTxnManager; set hive.compactor.initiator.on=true; set hive.compactor.worker.threads=1;最常用的元数据查询指令：-- 查看表字段 desc 表名; -- 查看表详细信息（包括存储位置、参数） desc formatted 表名; -- 查看建表语句 show create table 表名;关键知识点：HIVE metastore 除 Derby 外采取文件夹存储管理——即 HIVE 表对应 HDFS 上的目录，而非文件。metastore 存储在 HDFS：/warehousedir/home/xxx.db/表名（xxx 是数据库名）。2.5 HDFS Shell 常用命令# 目录与文件 hadoop fs -mkdir /usr hadoop fs -touchz /emptyfile # 创建空白文件 hadoop fs -ls / hadoop fs -lsr /usr # 递归列出 # 大小统计 hadoop fs -du /usr # 各文件大小 hadoop fs -dus /usr # 汇总大小 hadoop fs -count /usr # 文件(夹)数量 # 移动 / 复制 / 删除 hadoop fs -mv /usr/opt/data/student.txt /usr hadoop fs -cp /usr/opt/data/student.txt /usr hadoop fs -rm /usr # 删除文件/空目录 hadoop fs -rm -r /dir # 递归删除 # 上传 / 下载 / 查看 hadoop fs -put /local/student.txt /hdfs/data # 本地 → HDFS（本地原文件保留） hadoop fs -get /hdfs/student.txt /local/data # HDFS → 本地 hadoop fs -cat /student.txt hadoop fs -text /student.txt # 权限 hadoop fs -chmod 777 /emptyfile # 思考：-put 后本地文件是否还有？-mv 呢？-cp 呢？ # -put：本地保留；-mv：本地删除；-cp：本地保留（HDFS 内复制）第二篇 HIVE 数据管理第三章 HIVE 数据类型与表创建3.1 数据库管理创建/查看/删除：-- 创建（指定存储位置） create database owndb; create database owndb location '/opt'; -- 查看 describe database owndb; describe database extended owndb; set hive.cli.print.current.db=true; -- 在提示符显示当前 DB -- 删除 drop database owndb; drop database if exists owndb; drop database if exists owndb cascade; -- 级联删除库内所有表 -- 使用 use owndb; show databases; show databases in owndb;默认存储路径：/user/hive/warehouse/xxx.db/，通过 location 指定后自定义。3.2 表的创建与存储位置创建基本表（资料 1 HIVE指令实际操练1.txt、2 表的创建与数据装载操作.txt）：create table customers(customerid int, firstname string, lastname string) comment 'description of the table' tblproperties('creator'='me', 'create-at'='2020-11-27 10:00') location '/warehouse/home/customer' stored as orcfile; -- 仅复制表结构（不复制数据） create table if not exists customer2 like owndb.customer;查看表信息：desc customer; desc extended customer; desc formatted customer; desc owndb.customer.lastname; -- 查看某字段元数据3.3 数据导入方式create table 默认存储位置：HDFS:/warehousedir/home/xxxx.db/表名（Directory）。三种数据导入方式：方式命令示例HDFS 实际文件INSERT 语句insert into ... select ...Directory/000000_0HDFS puthadoop fs -put 本地文件 Directory直接落入指定目录LOAD DATAload data local inpath '/data/st1.txt' into table student;Directory/xxx文件名Q1（Lesson 4）：create table xxx location 对整个命名有什么要求？A：location 最好与默认文件管理体系一致，精确到表名级别；若含约束，location 必须放在最后一行。create table stu_test(sid int, sname string, sage int, addr string) location '/testdb'; -- 结果：发现并没有 xxx.db/表名 这层目录，因为 location 是人为规定的。load data 的两种形式：load data local inpath '/data/st1.txt' overwrite into table student; -- 本地文件保留，不会被删除 load data inpath '/cloud/path' overwrite into table student; -- 加载后会删除云端路径下的文件（HDFS 内移动）3.4 基本数据类型与字段约束-- 行/字段分隔符 row format delimited fields terminated by ',' -- 字段分隔符 lines terminated by '\n' -- 行分隔符 stored as textfile; -- 存储格式 -- 首位字段字符串机制 create table stu_test(sid string, sname string, sage int, addr string) row delimited fields terminated by ',' lines terminated by '\n' store as textfile;3.5 复杂数据类型3.5.1 Array（数组）create table student1(sid int, sname string, grade array<float>) row format delimited fields terminated by ',' collection items terminated by '#' lines terminated by '\n';示例数据 student1.txt：1,zhangsan,80#90.5#35 2,lisi,90# #88 3,wangwu,87#87 4,mary, #60# 5,tom,60# # 6,jemy,78#查询 sid=101 学生的第二个成绩（数组下标从 0 开始）：select grade[1] as second_grade from student1 where sid = 101;解析规则（# 之间的空值解析为 NULL）：sidsnamegrade（数组）说明1zhangsan[80.0, 90.5, 35.0]3 个有效浮点数2lisi[90.0, NULL, 88.0]中间 # 之间为空 → NULL3wangwu[87.0, 87.0]2 个有效浮点数4mary[NULL, 60.0, NULL]首尾 # 为空 → NULL5tom[60.0, NULL, NULL]后两个 # 为空 → NULL数据仓库6jemy[78.0]单个元素，结尾 # 忽略装载：load data local inpath '/opt/data/student1.txt' into table student1;3.5.2 Map（键值对）create table student2(sid int, sname string, grade map<string, float>) row format delimited fields terminated by ',' collection items terminated by '#' map keys terminated by ':' lines terminated by '\n';示例数据 student2.txt：1,zhangsan,语文:80#数学:90.5#英语:35 2,lisi,语文:90#数学:95#英语:88 3,wangwu,语文:87#数学:87#英语:56查询 sid=101 学生的数学成绩：select grade['数学'] as math_score from student3 where sid = 101;示例数据：101,张三,数学:90.5#语文:88.0#英语:92.3 102,李四,数学:85.0#语文:90.2#英语:89.7注意：若将 collection items terminated by '#' 删掉，则 Map 集合分割不存在，Map 只能有一个键值对。装载：load data local inpath '/data/student2.txt' into table student2;3.5.3 Struct（结构体）create table student4(sid int, info struct<name:string, age:int, sex:string>) row format delimited fields terminated by ',' collection items terminated by '#' lines terminated by '\n';示例数据 student4.txt：1,name:zhangsan#age:19#sex:Female 2,name:lisi#age:20#sex:male注意：Struct 不必写 map keys terminated by（教材中的写法是教学示意，标准 Struct 写法见上方）。装载：load data local inpath '/opt/data/student4.txt' into table student4;3.5.4 Array / Map / Struct 嵌套Array 嵌套 Map（需要 JSON SerDe，普通 delimited 无法实现）：create table student3(sid int, sname string, grade array<map<string,float>>) row format serde 'org.apache.hive.hcatalog.data.JsonSerDe'; load data local inpath '/opt/data/student3.txt' into table student3;JSON 数据格式 student3.txt：{"sid":1,"sname":"zhangsan","grade":{"语文":80,"数学":90.5,"英语":35}} {"sid":2,"sname":"lisi","grade":{"语文":80,"数学":90.5,"英语":35}} {"sid":3,"sname":"wangwu","grade":{"语文":80,"数学":90.5,"英语":35}}Map 嵌套 Array——同样无法实现。总结：Array、Map、Struct 不能嵌套使用（标准 delimited 模式下）Array、Struct 属于 collection itemsMap 若要实现多组数据并发，隐含包括了 collection items想做嵌套 → 用 JSON SerDe 或 ORC 复杂类型3.6 表的复制方式命令复制结构复制数据深拷贝create table student3_1_copy as select * from student3_1;✔✔浅拷贝create table student3_2_copy like student3_1;✔✘图中数据仓库（DW）将数据复制到子分析模块，"如果结果正确"——深拷贝是数据也一并复制；浅拷贝仅复制表结构。第四章 HIVE 存储模式4.1 内部表与外部表内部表 vs 外部表核心差异：维度内部表（Managed Table）外部表（External Table）创建关键字create table ...create external table ...DROP 表行为数据连同元数据一起删除仅删除元数据，location 数据保留适用场景中间表、临时计算结果共享数据源、防止误删外部表创建示例：create external table student_cp(sid int, sname string) row format delimited fields terminated by ',' location "/tmpdb/student_cp";DROP 外部表 → 位置 location 仍存在，仅引用切断；当下次仍有同名表（结构相同）时，仍会继续读取里面的数据。原理：仅切断元数据对 location 的引用，存储位置仍存在。查看表的位置：desc formatted tableName; show create table test_table;4.2 分区表4.2.1 分区表的概念分区表将数据按某个字段值分散到不同 HDFS 子目录，便于剪裁扫描。常见误解：分区是数据行的字段？错——分区字段是伪列，不参与业务字段存储，但出现在目录名里。4.2.2 分区表创建与数据导入静态分区：create table part_table(sid int, sname string, sage int) partitioned by(addr string) row format delimited fields terminated by ','; load data local inpath '/home/student100' into table part_table partition(addr='guangxi'); -- 字符串，addr 必须写清楚，严格管控装载后目录结构：/warehousedir/home/exam.db/part_table/addr=guangxi/student100原数据未发生变化，分区字段是附加在路径上的伪列。重复装载：存储位置 addr=xxx 不变，但子文件会多出 student100_copy_1。分区的增删：alter table part_table drop partition(addr='guangxi'); -- 删除该分区的子文件 alter table part_table add partition(addr='guangxi'); -- 新增分区 alter table part_table add partition(sage=20, addr='jiangsu'); -- 多分区需指定明确字段注意：show create table part_table; 的输出 不能用于新表的创建——它把 partitioned by 当字符串，不带分区值，无法直接复用。4.2.3 多分区字段create table part_table2(sid int, sname string) partitioned by(sage int, addr string) row format delimited fields terminated by ','; load data local inpath '/home/student100' into table part_table2 partition(sage=20, addr='guangxi');注意：alter table part_table add partition(sage=20, addr='jiangsu'); 需要同时指定两个明确分区的字段。4.2.4 分区的常见问题问题答案无分区表能否 add partition？新增分区字段是已有还是新增？不能；分区字段必须是建表时 partitioned by 中声明的伪列已分区的表能否 add partition 新字段？不能直接 add 新分区字段；只能新增已有分区字段的值非分区表能否变成（alter 成）分区表？不能alter table ... add 操作本质是什么？虽然语法像 DDL 的 schema 操作，本质是对元数据的修改 + HDFS 目录的创建错误示范：-- ❌ 非分区表不能 add 分区 create table student00(sid int, sname string); alter table student00 add partition (gender string); -- 报错 -- ❌ 这样也无法 load data create table student00(sid int, sname string) partitioned by(data string); load data local inpath '/data/x' into table student00; -- 必须指定分区值 -- ✔ 正确 alter table student00 add partition (data = '20251011');4.2.5 静态分区 vs 动态分区维度静态分区动态分区Schema相同相同分区值指定装载时手动 partition(addr='xxx')由 SELECT 子句字段决定数据控制精度精准（可配合 where 过滤）宽松（依赖源数据）适用场景不干净数据、需要精准把控大量分区批量写入动态分区装载（先创建含 partitioned by 的表，再 insert）：create table student3(sid int, sname string, grade map<string,float>) partitioned by(addr string) row format delimited fields terminated by ','; -- 静态 insert 也可指定具体分区 insert into table student3 partition (addr="anhui") select * from ...;4.2.6 经典实操：ah16 学生体检数据重要：ah16 是贯穿多个 Lesson 的核心实操表，按要求不删除。普通表 ah16（资料：学生体检数据普通表和分区表导入.txt、3 静态分区表的管理.txt）：create table ah16( sname string, sex int, minzu string, sge int, college string, major string, height int, weight int, breathlen float, breathweight int, physcore int, bloodtype string ) row format delimited fields terminated by ',' lines terminated by '\n' stored as textfile; load data local inpath '/data/16-1.csv' into table ah16; -- Loading data to table bigdata20.ah16ah16 的分区表 ah16_part（按血型分区）：create table ah16_part( sname string, sex int, minzu string, sge int, college string, major string, height int, weight int, breathlen float, breathweight int, physcore int ) partitioned by (bloodtype string) row format delimited fields terminated by ',' lines terminated by '\n' stored as textfile; set mapreduce.job.reduces=4; -- 按 bloodtype 分区装载 insert into ah16_part partition (bloodtype='A') select sname, sex, minzu, sge, college, major, height, weight, breathlen, breathweight, physcore from ah16 where bloodtype='A'; insert into ah16_part partition (bloodtype='B') select sname, sex, minzu, sge, college, major, height, weight, breathlen, breathweight, physcore from ah16 where bloodtype='B'; insert into ah16_part partition (bloodtype='AB') select sname, sex, minzu, sge, college, major, height, weight, breathlen, breathweight, physcore from ah16 where bloodtype='AB'; insert into ah16_part partition (bloodtype='O') select sname, sex, minzu, sge, college, major, height, weight, breathlen, breathweight, physcore from ah16 where bloodtype='O'; select count(*) from ah16_part;4.3 桶表（Bucket Table）4.3.1 桶表概念桶：在表的数据存储层面，按某字段哈希分散到若干文件与分区的区别：分区按值目录切分；桶按哈希文件切分桶表三大问题（Lesson 9）：对应的 schema 创建方法怎样将外部数据（CSV、跨 DB 表、insert 插入若干数据）导入到创建的表中select * 与 HDFS 文件查看指令 对应的数据4.3.2 桶表创建（ah16 桶表实操）跨 DB 引用 ah16：-- ah16 位于 owndb；当前在 bigdata24 库 -- 跨库语法 insert into table bucket_ah16 select * from owndb.ah16;桶表创建：set hive.enforce.bucketing=true; set mapreduce.job.reduces=4; create table bucket_ah16( sname string, sex int, minzu string, sge int, college string, major string, height int, weight int, breathlen float, breathweight int, physcore int, bloodtype string ) clustered by(sge) sorted by (sname desc) into 5 buckets row format delimited fields terminated by ','; -- 跨 DB 装载 insert into table bucket_ah16 select * from owndb.ah16; -- 分桶采样（Tablesample） select * from bucket_ah16 tablesample(bucket 1 out of 5 on sge); -- 查看建表语句（用于参考） show create table bucket_ah16;4.3.3 桶表的两个关键陷阱buck.txt 数据：10,name10 1,name1 9,name9 3,name3 2,name2 8,name8 4,name4 12,name12 5,name5 15,name15 14,name14 38,name8 7,name7 11,name11陷阱 1：使用 load data local 装载到桶表（4 桶），会形成四个桶，但 不会形成分区目录。create table buck_table(sid int, sname string) clustered by(sid) sorted by(sname desc) into 4 buckets row format delimited fields terminated by ','; load data local inpath '/data/buck.txt' overwrite into buck_table; select * from buck_table; -- 看不到分桶痕迹 hadoop fs -ls /warehousedir/home/bigdata24.db/buck_table -- 没有分区目录陷阱 2：sorted by + load data 不会按姓名排序；只有通过 INSERT 语句插入数据时，每个桶内的数据才会按 sname 排序。关键：仅定义 sorted by + load data 操作不会按姓名排序；只有通过 INSERT 语句插入数据时，每个桶内的数据才会按 sname 排序。对比：load data 与 insert into 的差异：方式HDFS 是否分桶文件命名Tablesample 行为load data ... into buck_table不形成分桶原始文件名仅逻辑分桶，标号从 1 开始insert into buck_table select * from ...形成分桶00000_0（标号从 0 开始）物理分桶可采样-- 用 insert 触发真正的分桶 insert into table buck_table select * from bigdata241.buck; -- 采样（4 桶） select * from buck_table tablesample(bucket 1 out of 4 on sid);4.3.4 桶表的完整操作流程set hive.enforce.bucketing=true; set mapreduce.job.reduces=4; -- 第一步：创建桶表 create table bucktable(sid int, sname string) clustered by(sid) into 4 buckets row format delimited fields terminated by ',' lines terminated by '\n'; create table bucktable2(sid int, sname string) clustered by(sid) sorted by(sid desc) into 4 buckets row format delimited fields terminated by ',' lines terminated by '\n'; -- 第三步：load data（不会触发分桶） load data local inpath '/data/buck.txt' overwrite into table bucktable; select * from bucktable; hadoop fs -ls /datawarehouse/home/; -- 第四步：建临时表（用于 insert 触发分桶） create table tmptable(sid int, sname string) row format delimited fields terminated by ',' lines terminated by '\n'; -- 第五步：load data 进临时表 load data local inpath '/data/buck.txt' overwrite into table tmptable; -- 第六步：通过 insert 触发分桶 insert into bucktable select * from tmptable; insert into bucktable2 select * from tmptable; -- 第七步：查看桶内容 hadoop fs -cat /warehouse/home/owndb.db/bucktable2/000002_0 select * from bucktable2 tablesample(bucket 3 out of 4 on sid); select * from bucktable tablesample(bucket 3 out of 4 on sid);4.4 四种表类型对比类型数据管理方式关键语句内部表（Table）数据与表结构绑定，drop 时级联删除create table t(...)外部表（External Table）数据独立于表，drop 时仅删除元数据create external table t(...) location '...'分区表（Partitioned Table）按分区字段值目录切分partitioned by(field type)桶表（Bucket Table）按哈希分散到固定数量文件clustered by(x) into n buckets核心问题（Lesson 8 Review）：四张表的数据管理方式有什么不同？内部表 ↔ 外部表：删除时是否级联处理存储位置分区表：按字段值目录切分（便于剪裁）桶表：按哈希文件切分（便于采样与高效 JOIN）第三篇 HQL 数据操作第五章 数据查询与连接5.1 数据导出方式导出到 HDFS：insert overwrite directory '/hdfs_path' row format delimited fields terminated by '#' select * from xxx;取回本地：hadoop fs -get /hdfs_path 本地路径导出到基本表（实表）：create table xxx as select ... from ...;导出到视图（虚表）：create view xx as select ... from ...; select * from xx; -- 触发 MR清理 HDFS 目录：hadoop fs -rm -r /myin # 里头有东西也能删除5.2 JOIN 操作详解实验表（来自 5 hive连接的案例设计.txt）：create table student100(sno int, sname string, sage int, saddr string) row format delimited fields terminated by ',' lines terminated by '\n'; create table course100(cno int, cname string, ccredit int) row format delimited fields terminated by ',' lines terminated by '\n'; create table SC100(sno int, cno int, grade int) row format delimited fields terminated by ',' lines terminated by '\n';实验数据：-- student100.txt 1,aa,17,anhui 2,bb,38,henan 3,cc,23,jiangsu 4,dd,56,beijing 5,ee,22,jiangsu 6,ff,23,shanghai 7,gg,24,fujian 8,hh,43,hubei 9,jj,25,jiangxi 10,kk,20,guangdong -- course100.txt 1,java,3 2,c++,2 3,dephi,3 4,vc++,3 5,python,2 6,spark,2 7,hive,1 -- SC100.txt 1,2,98 2,1,90 5,4,67 10,7,56 8,6,78 5,12,78 6,15,76 20,6,89 30,5,68 5,6, 6,3,5.2.1 五种连接方式总览连接方式关键字本质自然连接JOIN仅保留两表都有匹配的记录左外连接LEFT JOIN以左表为主，右表无匹配补 NULL右外连接RIGHT JOIN以右表为主，左表无匹配补 NULL全外连接FULL OUTER JOIN左右并集，无匹配补 NULL左半连接LEFT SEMI JOIN本质是嵌套查询，只输出左表字段笛卡尔连接：from A, B（不带 on）——"最废物的连接"，行数 = |A| × |B|，不推荐。5.2.2 笛卡尔连接（最废物）select student100.sno, sname, cno, grade from student100, SC100;结果展示（按 sno 升序，共 100 行 = 10 × 10）：5.2.3 自然连接（JOIN）select student100.sno, sname, cno, grade from student100 join SC100 on student100.sno=SC100.sno;含义：学生必须存在 student100 里，且选了课（但不必有有效成绩，NULL 也会保留）。结果（9 行）：5.2.4 左外连接（LEFT JOIN）select student100.sno, sname, cno, grade from student100 left join SC100 on student100.sno=SC100.sno;含义：以 student100 为主出发，看 SC100 中有谁和它等值，返回左边的全部内容（无匹配则右字段补 NULL）。结果（13 行）：5.2.5 右外连接（RIGHT JOIN）select student100.sno, sname, cno, grade from student100 right join SC100 on student100.sno=SC100.sno;含义：以 SC100 为主出发，看 student100 中有谁和它等值，返回右边的全部内容（左表无匹配补 NULL）。结果（11 行）：5.2.6 全外连接（FULL OUTER JOIN）select student100.sno, sname, cno, grade from student100 full outer join SC100 on student100.sno=SC100.sno;含义：同时执行 LEFT JOIN 和 RIGHT JOIN（已经包含了 JOIN NOT NULL & NOT NULL 的情况）。结果（15 行）：5.2.7 左半连接（LEFT SEMI JOIN）select student100.sno, sname, sage, saddr from student100 left semi join SC100 on (student100.sno = SC100.sno);核心思想："检查左表的每一行是否能在右表中找到匹配项，如果能，就保留左表的这一行；否则，就丢弃它。"LEFT SEMI JOIN 的关键特性：本质是嵌套查询，不是真正的 JOIN只输出左表字段，右表字段不可引用HIVE 不支持多层 LEFT SEMI JOIN 嵌套（仅一次嵌套）5.2.8 多层嵌套的两种实现方式场景：选课人数 > 2 的有效选课学生名单方式一：临时表（推荐）-- 步骤 1：建立中间表（统计每门课的选课人数） create table tempsc100 as select cno, count(*) as cnt from SC100 group by cno; -- 查看 tempsc100-- 步骤 2：选出选课人数 > 2 的课程对应的学生 create table tmpst100 as select SC100.sno from SC100 join tempsc100 on SC100.cno = tempsc100.cno where cnt > 2; -- 步骤 3：LEFT SEMI JOIN 锁定最终学生名单 select sno, sname from student100 left semi join tmpst100 on student100.sno = tmpst100.sno;结论：HIVE 不支持多层嵌套，只支持 LEFT SEMI JOIN 一次嵌套。因此只能通过中间表的方式实现多层嵌套。方式二：子查询（不需要创建中间表）select * from student100 inner join ( select * from tempsc100 join sc100 on tempsc100.cno = sc100.cno where cnt > 2 ) a on student100.sno = a.sno;注意：嵌套子查询中两个 SC100 都有 cno 字段，会出现二义性，必须用别名 a 隔离。5.3 ah16 血型统计复习重要：ah16 实操不删除。【复习 ah16】一次性统计出所有血型的指令 Group by：select bloodtype, count(*) as cnt from owndb.ah16 group by bloodtype;第六章 函数与视图6.1 条件函数：if / case whenif 函数（二分类）：if(condition, TrueValue, DefaultValue)case when（多分支）：case <field> when 'value1' then 'result1' when 'value2' then 'result2' ... else 'default_result' end as alias经典案例（tb_case 表）：悟空 A 男 娜娜 A 男 宋宋 B 男 凤姐 A 女 热巴 B 女 慧慧 B 女目标结果：dname男女A21B12方法一：行转列后再 sum：-- 第一步：分组计数（4 行） select dname, gender, count(*) as cnt from tb_case group by dname, gender; -- 第二步：行转列 select dname, count(*) as count_numbe, sum(if(gender == '男', 1, 0)) as M, sum(if(gender == '女', 1, 0)) as F from tb_case group by dname;方法二：直接用 case when 行转列：-- 写法一 select dname, sum(case gender when '男' then 1 else 0 end) as m, sum(case gender when '女' then 1 else 0 end) as f from tb_case group by dname; -- 写法二 select dname, sum(case when gender == '男' then 1 else 0 end) as m, sum(case when gender == '女' then 1 else 0 end) as f from tb_case group by dname;附加应用：性别缩写转换select name, dname, gender, case gender when '男' then 'm' when '女' then 'f' end from tb_case;6.2 聚合函数与分组汇总sum 的两种写法对比：写法含义是否正确sum(jb + jj + tc)把三列相加后求和✔ 正确sum(jb, jj, tc)把三列作为独立参数求和❌ 错误max vs greatest：函数作用范围max(col)返回列中最大的值greatest(col1, col2, col3, ...)返回行内多个字段中最大的值经典案例：每位员工收入最高的类型（资料 8. case_when.txt）-- 表 gz(uid, jb, jj, tc, deptno) select uid, greatest(jb, jj, tc) as greatest_sal, case when jb == greatest(jb, jj, tc) then 'jb' when jj == greatest(jb, jj, tc) then 'jj' when tc == greatest(jb, jj, tc) then 'tc' end as greatest_sal_type from gz;配套员工-部门关联分析（gz, bm, yg 三表连接）：-- 求每部门总薪资 select deptno, sum(jb + jj + tc) as sal_sum_dept from gz group by deptno; -- 求每部门不同性别员工薪资总和 select deptno, gender, sum(jb + jj + tc) as sum_sal_dept_and_gender from ( select gz.*, yg.gender from yg join gz on yg.uid = gz.uid ) t group by deptno, gender;6.3 视图视图 vs 表：维度视图（虚表）表（实表）创建语句create view v as select ...create table t as select ...是否执行 MR不执行 MR执行 MR查询时行为select * from 视图 → 自动 MRselect * from table → 不启动 MR数据存储不存储数据，仅保存定义存储物理数据视图的特点：create view as select ... 不执行 MRcreate table as select ... 执行 MRcreate view as (select ...) 保持原来数据的统一，虚实结合，真正的查询对象是实表实操：create view viewdb as select dname, count(*) as count_numbe, sum(if(gender == '男', 1, 0)) as M, sum(if(gender == '女', 1, 0)) as F from tb_case group by dname; -- 视图与表的查询对比 select * from tmp_case; -- 不启动 MR select * from viewdb; -- 自动 MR6.4 查询输出方式总结输出方式命令存储位置临时输出select * from ...控制台输出到 HDFSinsert overwrite directory '目录' row format delimited fields terminated by '#' select ...HDFS 目录HDFS → 本地hadoop fs -get HDFS目录 本地目录本地输出到基本表create table xxx as select ...实表输出到视图create view xx as select ...; select * from xxview;虚表（启动 MR）临时表查询（不创建表）with t as (select ...)内存中的临时结果集6.5 常用内置函数清单字符串函数：函数作用示例length(s)字符数length('hello world') → 11concat(a, b, ...)拼接concat('hello', 'world')trim(s)只去首尾空格（注意：不是去全部）trim(' hello ')lpad(s, n, c)左填充至 n 位lpad('abcd', 10, '*')rpad(s, n, c)右填充至 n 位rpad('abcd', 10, '*')lower(s)转小写lower('Hello WORLD!')upper(s)转大写upper('Hello WORLD!')substr(s, start)从 start 开始截到末尾（1-based）substr('hello world', 5) → ' world'substr(s, start, len)截取 len 个字符substr('hello world', 5, 3) → ' wo'instr(s, sub)子串位置instr('hello', 'll') → 3数值函数：函数作用示例round(x, d)四舍五入（d 可为负数）round(45.836, -1) → 50.0；round(45.836, -2) → 0.0；round(95.836, -2) → 100.0ceil(x)向上取整ceil(45.926)floor(x)向下取整floor(45.926)收集/转换函数：函数作用示例size(map(1,'yom',2,'ff'))返回集合大小2cast(x as type)类型转换cast('2019-11-21' as date)日期函数：函数作用示例to_date(s)转日期to_date('2019-11-28 11:23:45')year(s) / month(s) / day(s)取年/月/日year('2019-11-28') → 2019weekofyear(s)周数 datediff(d1, d2)日期差（天）datediff('2019-11-29','2014-11-12')date_add(d, n)加 n 天（n 可负）date_add('2019-12-2', 2)date_sub(d, n)减 n 天 from_unixtime(unix_timestamp(s, fmt), fmt)时间戳格式化from_unixtime(unix_timestamp('20200101','yyyyMMdd'),'yyyy-MM-dd') → 2020-01-01current_date当前日期 条件函数：函数作用coalesce(a, b, c, ...)从左到右返回第一个不为 NULL 的值case ... when ... then ... end多分支条件聚合函数：count、sum、min、max、avg表生成函数：函数作用explode(map) / explode(array)行转列查询优化开关（Lesson 12）：set hive.fetch.task.conversion=more; -- 或 set hive.exec.mode.local.auto=true; -- 简单 select 不转换为 MR第四篇 综合实战第七章 综合案例分析7.1 肥胖等级数据分析（obesity_level.csv）建表：drop table if exists feipang; create table feipang( gender string, age int, height float, fam_his int, FAVC int, FCVC int, NCP int, CAEC string, SMOKE int, CH2O float, SCC int, FAF int, TUE float, CALC string, MTRANS string, fat_level string ) row format delimited fields terminated by ',' tblproperties('skip.header.line.count'='1'); -- 跳过 CSV 首行表头 load data local inpath '/home/obesity_level.csv' into table feipang;查询基础统计：select count(*) as cnt from feipang; select distinct(fat_level) from feipang group by fat_level;实操题目：查询 fat_level 等级数量及占比：with TOTAL as (select count(*) as total from feipang) select distinct(fat_level), round(count(*) / TOTAL.total, 3) as percentage from feipang group by fat_level;查询 MTRANS 各类样本数：select distinct(MTRANS), count(*) as cnt_MTRANS from feipang group by MTRANS;将 CAEC 中 '0' 替换为 'Never'（不增加新字段）：-- 先观察 CAEC 取值 select distinct(CAEC) from feipang; -- 思路：先建临时表新增 CAEC_1 列，再覆盖回原表 create table feipang_new as select *, case CAEC when '0' then 'Never' end as CAEC_1 from feipang; -- 注意观察 CAEC=sometimes/always/frequently 的数据会不会一起 -- 携带到 CAEC_1 的字段中 → CAEC_1 中只含 Never -- 覆盖原表（必须列出全部列） insert overwrite table feipang select gender, age, height, fam_his, FAVC, FCVC, NCP, CAEC_1, -- 替换后的列 SMOKE, CH2O, SCC, FAF, TUE, CALC, MTRANS, fat_level from feipang_new;ORC 表事务权限（ORC 表可 update / delete，textfile 不行）：create table feipang_new (stored as ORC) as select *, case CAEC when '0' then 'Never' end as CAEC_1 from feipang;7.2 身份证信息处理建表：create table cid_info(id int, cid string) row format delimited fields terminated by '#'; load data local inpath '/home/cid.txt' into table cid_info; -- 观察 cid 长度（会有空格，要确定从哪开始计算） select distinct(length(cid)) from cid_info;题目 1：截取 cid 的 8 位出生年月日作为 birthdayselect cid, substr(cid, 8, 8) as birthday from cid_info; alter table cid_info add columns(birthday string); -- 只插一列（不用中间表）——没有 into insert overwrite table cid_info select id, cid, substr(cid, 8, 8) as birthday from cid_info;题目 2：将截取的 birthday 转换为 yyyy-MM-dd 格式alter table cid_info add columns(birth_date string); insert overwrite table cid_info select id, cid, birthday, from_unixtime(unix_timestamp(substr(cid, 8, 8), 'yyyyMMdd'), 'yyyy-MM-dd') as birth_date from cid_info;题目 3：将 birthday 拆分为 year、month、dayalter table cid_info add columns(birth_year int, birth_month int, birth_day int); insert overwrite table cid_info select id, cid, birthday, birth_date, year(birth_date) as birth_year, month(birth_date) as birth_month, day(birth_date) as birth_day from cid_info;题目 4：计算实岁和虚岁alter table cid_info add columns(age_shi float, age_xu int); insert overwrite table cid_info select id, cid, birthday, birth_date, birth_year, birth_month, birth_day, round(datediff(current_date, birth_date) / 365, 1) as age_shi, year(current_date) - birth_year as age_xu from cid_info;7.3 结婚离婚数据查询综合实训资料 7 结婚离婚数据查询综合实训.txt建表（70+ 字段，按季度拆分每省年度数据）：create table marriage( province string, M07Q1 int, D07Q1 int, M07Q2 int, D07Q2 int, M07Q3 int, D07Q3 int, M07Q4 int, D07Q4 int, -- ... 省略 M08Q1..M20Q3 共 14 年的所有季度字段 M20Q1 int, D20Q1 int, M20Q2 int, D20Q2 int, M20Q3 int, D20Q3 int ) row format delimited fields terminated by ',' collection items terminated by '-' map keys terminated by ':' tblproperties('skip.header.line.count'='2'); load data local inpath '/opt/data/marriageclean.txt' overwrite into table marriage;查询：set hive.cli.print.header=true; select province, M07Q1, D07Q1, M07Q2, D07Q2, M07Q3, D07Q3, M07Q4, D07Q4 from marriage limit 10; select count(*) from marriage; -- 2007 年各省结婚/离婚季度合计 select province, M07Q1+M07Q2+M07Q3+M07Q4 from marriage;典型分析：07 年结婚数最高的省份create table Y07MD as select province, avg(M07Q1+M07Q2+M07Q3+M07Q4) as M07, avg(D07Q1+D07Q2+D07Q3+D07Q4) as D07 from marriage group by province order by M07; select max(M07) from Y07MD; -- 1806787.0 select province from Y07MD where M07 = 1806787.0; -- 山东省多年汇总：create table allyearM as select province, sum(M07Q1+M07Q2+M07Q3+M07Q4) as M07, sum(M08Q1+M08Q2+M08Q3+M08Q4) as M08, -- ... 14 年 sum(M20Q1+M20Q2+M20Q3) as M20 from marriage group by province order by M07; select * from allyearM limit 10;错误示例：select max(M07, M08, M09, M10, M11) from allyearM; ❌ max 不能接受多列参数。第八章 半结构化数据处理8.1 JSON 数据处理JSON 解析的两种方式：JAR 包方式：自定义 RegexSerDe 解析get_json_object 函数：函数式字段提取应用案例：影评库 users.dat / movies.dat / ratings.dat（详见第九章）。8.2 字符串函数与数据清洗字符串函数复习（见 §6.5）。特别注意：trim 只去首尾空格length 计算字符数（含空格），如 length(' hello') → 6concat 拼接时空格会保留，必要时先 trim第九章 影评综合案例资料 第11章 影评案例综合应用.txt9.1 数据说明数据文件格式行数字段users.dat2::M::56::16::700726040UserID BigInt, Gender String, Age Int, Occupation String, Zipcode Stringmovies.dat`2::Jumanji (1995)::AdventureChildren'sFantasy`3883MovieID BigInt, Title String, Genres Stringratings.dat1::1193::5::9783007601000209UserID BigInt, MovieID BigInt, Rating Double, Timestamped String问题：HIVE 不支持解析多字节分隔符 '::'，必须用 RegexSerDe 或先 shell 清洗成单分隔符。9.2 建表（RegexSerDe）drop database if exists movie; create database if not exists movie; use movie; -- 用户表 create table t_user( userid bigint, sex string, age int, occupation string, zipcode string) row format serde 'org.apache.hadoop.hive.serde2.RegexSerDe' with serdeproperties( 'input.regex'='(.*)::(.*)::(.*)::(.*)::(.*)', 'output.format.string'='%1$s %2$s %3$s %4$s %5$s') stored as textfile; -- 电影表 create table t_movie( movieid bigint, moviename string, movietype string) row format serde 'org.apache.hadoop.hive.serde2.RegexSerDe' with serdeproperties( 'input.regex'='(.*)::(.*)::(.*)', 'output.format.string'='%1$s %2$s %3$s') stored as textfile; -- 评分表 create table t_rating( userid bigint, movieid bigint, rate double, times string) row format serde 'org.apache.hadoop.hive.serde2.RegexSerDe' with serdeproperties( 'input.regex'='(.*)::(.*)::(.*)::(.*)', 'output.format.string'='%1$s %2$s %3$s %4$s') stored as textfile; -- 装载 load data local inpath '/warehouse/users.dat' into table t_user; load data local inpath '/warehouse/movies.dat' into table t_movie; load data local inpath '/warehouse/ratings.dat' into table t_rating; select * from t_user limit 5; select * from t_movie limit 5; select * from t_rating limit 5;9.3 十大分析需求(1) 被评分次数最多的 10 部电影create table answer2 as select a.moviename as moviename, count(a.moviename) as total from t_movie a join t_rating b on a.movieid = b.movieid group by a.moviename order by total desc limit 10; select * from answer2;(2) 男性/女性评分最高的 10 部电影-- 女性 create table answer3_F as select 'F' as sex, c.moviename as name, avg(a.rate) as avgrate, count(c.moviename) as total from t_rating a join t_user b on a.userid = b.userid join t_movie c on a.movieid = c.movieid where b.sex = 'F' group by c.moviename having total >= 50 order by avgrate desc limit 10; -- 男性 create table answer3_M as select 'M' as sex, c.moviename as name, avg(a.rate) as avgrate, count(c.moviename) as total from t_rating a join t_user b on a.userid = b.userid join t_movie c on a.movieid = c.movieid where b.sex = 'M' group by c.moviename having total >= 50 order by avgrate desc limit 10;(3) movieid = 2116 各年龄段平均影评create table answer4 as select a.age as age, avg(b.rate) as avgrate from t_user a join t_rating b on a.userid = b.userid where b.movieid = 2116 group by a.age;(4) 影评最多的女性评分最高的 10 部电影的平均影评分-- A：最喜欢看电影的女性 select a.userid, count(a.userid) as total from t_rating a join t_user b on a.userid = b.userid where b.sex = 'F' group by a.userid order by total desc limit 1; -- 假设 userid = 1150 -- B：该女性评分最高的 10 部电影 create table answer5_B as select a.movieid as movieid, a.rate as rate from t_rating a where a.userid = 1150 order by rate desc limit 10; -- C：求出 B 中 10 部电影的平均影评分 create table answer5_C as select b.movieid as movieid, c.moviename as moviename, avg(b.rate) as avgrate from answer5_B a join t_rating b on a.movieid = b.movieid join t_movie c on b.movieid = c.movieid group by b.movieid, c.moviename;(5) 好片（评分 ≥ 4.0）最多的年份中最好看的 10 部电影-- A：截取年份 create table answer6_A as select a.movieid as movieid, a.moviename as moviename, substr(a.moviename, -5, 4) as years, avg(b.rate) as avgrate from t_movie a join t_rating b on a.movieid = b.movieid group by a.movieid, a.moviename; -- B：好片最多的年份 select years, count(years) as total from answer6_A a where avgrate >= 4.0 group by years order by total desc limit 1; -- 假设 = 1998 -- C：该年评分最高的 10 部电影 create table answer6_C as select a.moviename as name, a.avgrate as rate from answer6_A a where a.years = 1998 order by rate desc limit 10;(6) 1997 年评分最高的 10 部 Comedy 类电影-- A：联合电影类型 create table answer7_A as select b.movieid as id, b.moviename as name, b.years as years, b.avgrate as rate, a.movietype as type from t_movie a join answer6_A b on a.movieid = b.movieid; -- B：筛选 + 排序 create table answer7_B as select t.id as id, t.name as name, t.rate as rate from answer7_A t where t.years = 1997 and instr(lcase(t.type), 'comedy') > 0 order by rate desc limit 10;(7) 各类型电影中评价最高的 5 部（TopN）-- A：裂变电影类型 create table answer8_A as select a.id as id, a.name as name, a.years as years, a.rate as rate, tv.type as type from answer7_A a lateral view explode(split(a.type, '\\|')) tv as type; -- B：按类型分组编号 create table answer8_B as select id, name, years, rate, type, row_number() over (distribute by type sort by rate desc) as num from answer8_A; -- C：取每组前 5 select a.* from answer8_B a where a.num <= 5;(8) 各年评分最高的电影类型-- A：按 (年份, 类型) 分组计算平均评分 create table answer9_A as select a.years as years, a.type as type, avg(a.rate) as rate from answer8_A a group by a.years, a.type order by rate desc; -- B：按年份编号 create table answer9_B as select years, type, rate, row_number() over (distribute by years sort by rate) as num from answer9_A; -- C：取每年最高类型 select * from answer9_B where num = 1;(9) 每个地区最高评分的电影名（存入 HDFS）-- A：三表联合，计算每个 (地区, 电影) 平均评分 create table answer10_A as select c.movieid, c.moviename, avg(b.rate) as avgrate, a.zipcode from t_user a join t_rating b on a.userid = b.userid join t_movie c on b.movieid = c.movieid group by a.zipcode, c.movieid, c.moviename; -- B：按地区分组编号 create table answer10_B as select movieid, moviename, avgrate, zipcode, row_number() over (distribute by zipcode sort by avgrate) as num from answer10_A; -- C：取 num = 1 的行写入 HDFS insert overwrite directory '/movie/answer10/' select t.* from answer10_B t where t.num = 1; hadoop fs -ls /movie/answer10/ hadoop fs -cat /movie/answer10/000000_0第五篇 总复习第十章 知识体系总结10.1 表的创建与存储知识点关键点数据库存储位置默认 /user/hive/warehouse/xxx.db/，可 location 指定表/外部表 location写在表级，location 最好精确到表名级，含约束时必须放最后表/外部表/分区表/桶表数据装载与表的内容匹配分区表/桶表查看内容partition 目录查看 vs 桶号文件 00000_010.2 四种表类型类型关键字数据管理普通表create tabledrop 级联删除外部表create external table ... locationdrop 不级联（原理：引用切断，存储位置仍存在）分区表partitioned by按字段值目录切分桶表clustered by ... into n buckets按哈希分散到固定数量文件10.3 数据输入与输出方法输入（数据导入）：方法命令HDFS puthadoop fs -put 本地文件 DirectoryLOAD DATAload data local inpath '/data/st1.txt' into table student;INSERTinsert into ... select ...HDFS cp/mvhadoop fs -cp /hdfs/path /target 等输出（查询导出）：方法命令控制台临时输出select * from ...输出到 HDFSinsert overwrite directory '目录' row format delimited fields terminated by '#' select ...输出到视图create view xx as select ...输出到实表create table xx as select ...WITH 临时查询with t as (select ...) select * from t10.4 HQL 增删改查ORC 表开放事务权限后，可执行 update、delete；textfile 不行insert 没有问题（任何存储格式）查询思路：distinct、count、group by、order by、limit函数应用：max、min、avg、greatest、if、case when、length、concat、trim、lower、upper、instr、substr 等10.5 笛卡尔连接 / 五种 JOIN 总结连接方式关键字特性笛卡尔连接from A, B（不带 on）行数 =自然连接JOIN ... ON仅保留匹配左外连接LEFT JOIN左表为主右外连接RIGHT JOIN右表为主全外连接FULL OUTER JOIN左右并集左半连接LEFT SEMI JOIN仅输出左表字段，本质是嵌套查询10.6 静态分区 vs 动态分区维度静态分区动态分区Schema相同相同数据装载速度较快（直接挂目录）较慢（需 MR）数据控制精度精准（配合 where 过滤不干净数据）宽松（依赖源数据）典型语句load data ... partition(addr='xx')insert ... partition(addr)附录：参考资料清单类别文件集群与 HDFS00 集群启动.txt、0 hdfs shell操作.txtHIVE 基础指令1 HIVE指令实际操练1.txt、2 表的创建与数据装载操作.txt复杂类型3 三种复杂结构的数据输入操作实践.txt、3 静态分区表的管理.txt桶表4 桶表的数据装载及分桶显示.txt、4 桶表的数据装载及分桶显示(1).txtJOIN 案例5 hive连接的案例设计.txt函数与查询6 查询软件实践.txt、8. case_when.txt、8.1 source.txt综合实训7 结婚离婚数据查询综合实训.txt、第11章 影评案例综合应用.txt、json.txt实操数据ah16（体检数据）、obesity_level.csv、学生体检数据普通表和分区表导入.txtHIVE 配置执行命令集合.txt教材 PPT02 Hadoop与HIve.pptx、03 HIVE的体系结构与安装.pptx、第4-10章 Hive 系列附录：原始笔记 Lesson 索引Lesson主题详见本文档Lesson 1前沿技术科普§1.1Lesson 2三级模式、数据分类、HIVE 三种部署§1.2、§1.3、§2.2Lesson 3集群启停、HIVE 配置、metastore§2.3、§2.4、§3.1Lesson 4数据导入、location 规范、复杂数据类型§3.3、§3.4、§3.5Lesson 5表复制、Array/Map/Struct 嵌套§3.6、§3.5.4Lesson 6外部表、load data、ah16 实操§4.1、§4.2.6Lesson 7静态分区表§4.2.1-§4.2.4Lesson 8动态分区表、桶表、五种 JOIN§4.2.5、§4.3、§5.2.1Lesson 9桶表陷阱、数据导出§4.3.3、§5.1Lesson 10JOIN 五种连接、LEFT SEMI JOIN§5.2Lesson 11视图、case when、聚合函数§6.1、§6.3Lesson 12查询输出、肥胖数据实战§6.4、§7.1Lesson 13ORC 表、CAEC 字段替换§7.1Lesson 14身份证信息处理§7.2Lesson 15JSON 处理§8.1、§9Lesson 16字符串函数、知识体系总结§6.5、第十章
深度学习（DL）-经典卷积神经网络-测试(model_test)模板代码
一、导入依赖模块import torch import torch.utils.data as Data from torchvision import transforms from torchvision.datasets import FashionMNIST from model import LeNet代码行逻辑与作用关键补充import torch导入PyTorch核心库提供张量计算、设备管理（CPU/GPU）、模型前向传播等基础能力import torch.utils.data as Data导入PyTorch数据加载工具封装数据集为可迭代的DataLoader，简化批次读取、打乱、多进程加载等逻辑from torchvision import transforms导入图像预处理工具用于对FashionMNIST的图像做标准化、尺寸调整、格式转换（如PIL→Tensor）from torchvision.datasets import FashionMNIST导入FashionMNIST数据集服装分类数据集（10类，如T恤、裤子），替代MNIST的手写数字分类，更贴近实际场景from model import LeNet导入自定义的LeNet模型model.py中定义的LeNet卷积神经网络，是本次要测试的模型二、测试数据处理函数 test_data_process作用：加载并预处理测试集，返回可迭代的DataLoader（数据加载器）def test_data_process(): test_data = FashionMNIST(root='./data', train=False, transform=transforms.Compose([transforms.Resize(size=28), transforms.ToTensor()]), download=True) test_dataloader = Data.DataLoader(dataset=test_data, batch_size=1, shuffle=True, num_workers=0) return test_dataloader代码行逻辑与作用关键补充def test_data_process():定义测试数据处理函数（无入参）封装数据加载逻辑，提高代码复用性test_data = FashionMNIST(...)加载FashionMNIST测试集+ root='./data'：数据集保存到当前目录的data文件夹 + train=False：指定加载测试集（train=True为训练集） + transform=...：图像预处理管道（组合多个变换）： - transforms.Resize(size=28)：强制将图像缩放到28×28（确保输入模型的尺寸统一） - transforms.ToTensor()：将PIL图像转为PyTorch张量，同时把像素值从0-255归一化到0-1 + download=True：如果data文件夹无数据集，自动从官网下载test_dataloader = Data.DataLoader(...)封装测试集为DataLoader+ dataset=test_data：指定要加载的数据集 + batch_size=1：批次大小为1（测试时批次不影响准确率，设1方便逐样本验证） + shuffle=True：打乱测试集顺序（测试时shuffle不影响最终准确率，仅改变遍历顺序） + num_workers=0：数据加载的子进程数（Windows系统推荐设0，避免多进程冲突）return test_dataloader返回构建好的测试数据加载器供后续模型测试函数调用三、模型测试函数 test_model_process作用：用测试集评估模型的预测准确率，核心是仅前向传播、不计算梯度def test_model_process(model, test_dataloader): device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") model = model.to(device) #初始化参数 test_correct = 0.0 test_num = 0 #只进行前向传播计算，不计算梯度，从而节省内存，加快运行速度 with torch.no_grad(): for test_data_x, test_data_y in test_dataloader: # 将特征放入到测试设备中 test_data_x = test_data_x.to(device) # 将标签放入到测试设备中 test_data_y = test_data_y.to(device) # 设置模型为评估模式 model.eval() # 前向传播过程，输入为测试数据集，输出为对每个样本的预测值 output = model(test_data_x) # 查找每一行中最大值对应的行标 pre_lab = torch.argmax(output, dim=1) #沿着第一维度 (列)找最大值 # 如果预测正确，则准确度test_corrects加1 test_correct += torch.sum(pre_lab == test_data_y.data) # 将所有的测试样本进行累加 test_num += test_data_x.size(0) # 计算测试准确率 test_acc = test_correct.double().item() / test_num print("测试的准确率为：", test_acc)代码行逻辑与作用关键补充def test_model_process(model, test_dataloader):定义模型测试函数入参：model（待测试的LeNet模型）、test_dataloader（测试数据加载器）device = torch.device(...)定义计算设备优先使用GPU（cuda:0），无GPU则用CPU；保证模型和数据在同一设备上计算（否则报错）model = model.to(device)将模型移动到指定设备模型的所有参数/计算都会在GPU/CPU上执行test_correct = 0.0初始化“正确预测数”浮点型避免整数溢出，方便后续除法计算test_num = 0初始化“测试样本总数”累加所有遍历过的样本数with torch.no_grad():禁用梯度计算的上下文管理器测试阶段不需要反向传播更新参数，禁用梯度可大幅节省内存、提升计算速度for test_data_x, test_data_y in test_dataloader:遍历测试集的每个批次test_data_x：图像特征张量（shape=[1,1,28,28]，对应batch_size=1、单通道、28×28）；test_data_y：真实标签（shape=[1]）test_data_x = test_data_x.to(device)将特征张量移到指定设备必须和模型同设备，否则无法计算test_data_y = test_data_y.to(device)将标签张量移到指定设备保证和预测标签（pre_lab）同设备，才能比较model.eval()将模型设为评估模式关键：关闭训练时的特殊层行为（如Dropout随机失活、BatchNorm的均值/方差更新），确保测试时模型行为一致output = model(test_data_x)模型前向传播输入测试特征，输出output（shape=[1,10]）：10个类别的预测得分（logits，未归一化）pre_lab = torch.argmax(output, dim=1)取预测得分的最大值索引dim=1：沿“类别维度”（第1维，列方向）找最大值，结果是预测的类别标签（如0=T恤、1=裤子）test_correct += torch.sum(pre_lab == test_data_y.data)累加本批次正确数pre_lab == test_data_y.data：逐元素比较（True=1，False=0）；torch.sum()求和得到本批次正确数test_num += test_data_x.size(0)累加本批次样本数test_data_x.size(0)=batch_size（这里是1），最终累加为测试集总样本数test_acc = test_correct.double().item() / test_num计算整体准确率+ test_correct.double()：转为双精度浮点型 + .item()：将张量转为Python数值（避免张量运算） + 除以总样本数得到准确率（0~1之间）print("测试的准确率为：", test_acc)打印准确率直观展示模型测试效果四、主程序入口if __name__=="__main__": # 加载模型 model = LeNet() model.load_state_dict(torch.load('model.pth')) # 加载测试数据 test_dataloader = test_data_process() # 加载模型测试的函数 test_model_process(model, test_dataloader)代码行逻辑与作用关键补充if __name__=="__main__":脚本入口判断仅当脚本被直接运行时执行（被导入为模块时不执行），避免代码被复用时报错model = LeNet()实例化LeNet模型初始化模型的网络结构（此时参数是随机的）model.load_state_dict(torch.load('model.pth'))加载训练好的模型权重model.pth是训练阶段保存的模型参数文件，加载后模型才有“训练好的能力”test_dataloader = test_data_process()调用函数获取测试数据加载器准备测试数据test_model_process(model, test_dataloader)调用测试函数执行模型评估，最终打印测试准确率总结测试阶段的核心优化：torch.no_grad() + model.eval() 是测试的“黄金组合”——禁用梯度+关闭训练层行为，保证效率和结果准确；设备一致性：模型、特征、标签必须在同一设备（CPU/GPU），否则会报张量设备不匹配错误；准确率计算逻辑：通过torch.argmax取预测类别，和真实标签比较后累加正确数，最终除以总样本数。完整代码如下import torch import torch.utils.data as Data from torchvision import transforms from torchvision.datasets import FashionMNIST from model import LeNet def test_data_process(): test_data = FashionMNIST(root='./data', train=False, transform=transforms.Compose([transforms.Resize(size=28), transforms.ToTensor()]), download=True) test_dataloader = Data.DataLoader(dataset=test_data, batch_size=1, shuffle=True, num_workers=0) return test_dataloader def test_model_process(model, test_dataloader): device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") model = model.to(device) #初始化参数 test_correct = 0.0 test_num = 0 #只进行前向传播计算，不计算梯度，从而节省内存，加快运行速度 with torch.no_grad(): for test_data_x, test_data_y in test_dataloader: # 将特征放入到测试设备中 test_data_x = test_data_x.to(device) # 将标签放入到测试设备中 test_data_y = test_data_y.to(device) # 设置模型为评估模式 model.eval() # 前向传播过程，输入为测试数据集，输出为对每个样本的预测值 output = model(test_data_x) # 查找每一行中最大值对应的行标 pre_lab = torch.argmax(output, dim=1) #沿着第一维度 (列)找最大值 # 如果预测正确，则准确度test_corrects加1 test_correct += torch.sum(pre_lab == test_data_y.data) # 将所有的测试样本进行累加 test_num += test_data_x.size(0) # 计算测试准确率 test_acc = test_correct.double().item() / test_num print("测试的准确率为：", test_acc) if __name__=="__main__": # 加载模型 model = LeNet() model.load_state_dict(torch.load('model.pth')) # 加载测试数据 test_dataloader = test_data_process() # 加载模型测试的函数 test_model_process(model, test_dataloader)
一、核心逻辑先明确整体训练框架：数据准备→模型 / 优化器 / 损失函数初始化→多轮次训练（训练 + 验证）→保存最优模型→可视化，以下按函数拆分讲解。（一）数据处理函数 train_val_data_processdef train_val_data_process(): train_data = FashionMNIST(root='./data', train=True, transform=transforms.Compose([transforms.Resize(size=28), transforms.ToTensor()]), download=True) # 划分训练/验证集（8:2） train_data, val_data = Data.random_split(train_data, [round(0.8*len(train_data)), round(0.2*len(train_data))]) # 构建训练集DataLoader train_dataloader = Data.DataLoader(dataset=train_data, batch_size=32, shuffle=True, num_workers=2) # 构建验证集DataLoader val_dataloader = Data.DataLoader(dataset=val_data, batch_size=32, shuffle=True, num_workers=2) return train_dataloader, val_dataloader逻辑：加载 FashionMNIST 训练集：transform将图片转为模型可接受的 Tensor 格式，download=True自动下载数据集；拆分训练 / 验证集：避免用测试集验证，保证评估客观性；封装 DataLoader：核心是batch_size=32（批次加载）、shuffle=True（打乱数据）、num_workers=2（多线程加速）。（二）核心训练函数 train_model_process1. 初始化核心组件def train_model_process(model, train_dataloader, val_dataloader, num_epochs): # 自动选择GPU/CPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Adam优化器（自适应学习率，更新模型参数） optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 分类损失函数：交叉熵 criterion = nn.CrossEntropyLoss() # 模型迁移到指定设备（GPU/CPU） model = model.to(device) # 保存最优模型参数（避免过拟合覆盖） best_model_wts = copy.deepcopy(model.state_dict())关键：所有计算（模型 / 数据 / 损失）必须在同一设备，否则报错。2. 训练过程参数初始化best_acc = 0.0 # 最优验证准确率 train_loss_all = [] # 训练损失记录 val_loss_all = [] # 验证损失记录 train_acc_all = [] # 训练准确率记录 val_acc_all = [] # 验证准确率记录 since = time.time() # 训练开始时间（统计耗时）逻辑：每轮 epoch 重置累加变量，避免跨轮污染；记录指标用于后续可视化。3. 多轮次（epoch）训练循环for epoch in range(num_epochs): print("Epoch {}/{}".format(epoch, num_epochs-1)) print("-"*10) # 每轮重置累加变量 train_loss = 0.0 train_corrects = 0 val_loss = 0.0 val_corrects = 0 train_num = 0 val_num = 0epoch：每轮完整遍历一次训练集 + 验证集，是训练的基本单位。4. 训练阶段（核心：前向→反向→参数更新）for step, (b_x, b_y) in enumerate(train_dataloader): # 数据迁移到设备（和模型同设备） b_x = b_x.to(device) b_y = b_y.to(device) # 模型设为训练模式（启用Dropout/BatchNorm训练行为） model.train() # 前向传播：模型输出原始得分（logits，未做softmax） output = model(b_x) # 预测类别：取logits最大值对应的索引 pre_lab = torch.argmax(output, dim=1) # 计算批次损失（交叉熵内置softmax，无需手动加） loss = criterion(output, b_y) # 梯度清零（PyTorch梯度累加，必须手动清空） optimizer.zero_grad() # 反向传播：计算参数梯度 loss.backward() # 参数更新：Adam根据梯度调整权重 optimizer.step() # 累加训练损失（批次平均损失×批次大小=总损失） train_loss += loss.item() * b_x.size(0) # 累加正确数（预测=真实标签） train_corrects += torch.sum(pre_lab == b_y.data) # 累加训练样本数 train_num += b_x.size(0)核心闭环：前向传播（算预测）→ 损失计算（衡量误差）→ 反向传播（算梯度）→ 参数更新（降低误差）；model.train()：启用 Dropout（随机失活神经元）、BatchNorm（更新均值 / 方差），仅训练阶段生效。5. 验证阶段（仅前向传播，无参数更新）for step, (b_x, b_y) in enumerate(val_dataloader): b_x = b_x.to(device) b_y = b_y.to(device) # 模型设为评估模式（关闭Dropout，固定BatchNorm） model.eval() output = model(b_x) pre_lab = torch.argmax(output, dim=1) loss = criterion(output, b_y) # 仅累加指标，无反向传播/参数更新 val_loss += loss.item() * b_x.size(0) val_corrects += torch.sum(pre_lab == b_y.data) val_num += b_x.size(0)model.eval()：避免验证阶段的随机性，保证评估结果稳定；无优化器操作：验证仅评估模型性能，不更新参数。6. 每轮指标计算与最优模型保存# 计算每轮平均损失/准确率 train_loss_all.append(train_loss / train_num) train_acc_all.append(train_corrects.double().item() / train_num) val_loss_all.append(val_loss / val_num) val_acc_all.append(val_corrects.double().item() / val_num) # 打印轮次指标 print("{} train loss:{:.4f} train acc: {:.4f}".format(epoch, train_loss_all[-1], train_acc_all[-1])) print("{} val loss:{:.4f} val acc: {:.4f}".format(epoch, val_loss_all[-1], val_acc_all[-1])) # 保存最优模型（按验证准确率） if val_acc_all[-1] > best_acc: best_acc = val_acc_all[-1] best_model_wts = copy.deepcopy(model.state_dict()) # 统计耗时 time_use = time.time() - since print("训练和验证耗费的时间{:.0f}m{:.0f}s".format(time_use//60, time_use%60))最优模型保存：用copy.deepcopy避免浅拷贝导致参数覆盖；耗时统计：评估训练效率，调整超参数（如 batch_size、epoch 数）。7. 训练完成后保存模型 + 返回指标# 保存最优模型参数 torch.save(best_model_wts, "C:/Users/86159/Desktop/LeNet/best_model.pth") # 封装训练过程为DataFrame（用于可视化） train_process = pd.DataFrame(data={"epoch":range(num_epochs), "train_loss_all":train_loss_all, "val_loss_all":val_loss_all, "train_acc_all":train_acc_all, "val_acc_all":val_acc_all,}) return train_process（三）可视化函数 matplot_acc_lossdef matplot_acc_loss(train_process): plt.figure(figsize=(12, 4)) # 损失曲线 plt.subplot(1, 2, 1) plt.plot(train_process['epoch'], train_process.train_loss_all, "ro-", label="Train loss") plt.plot(train_process['epoch'], train_process.val_loss_all, "bs-", label="Val loss") plt.legend() plt.xlabel("epoch") plt.ylabel("Loss") # 准确率曲线 plt.subplot(1, 2, 2) plt.plot(train_process['epoch'], train_process.train_acc_all, "ro-", label="Train acc") plt.plot(train_process['epoch'], train_process.val_acc_all, "bs-", label="Val acc") plt.xlabel("epoch") plt.ylabel("acc") plt.legend() plt.show()作用：直观观察训练趋势（如训练损失下降、验证损失上升→过拟合）。（四）主入口if __name__ == '__main__': LeNet = LeNet() # 加载模型 train_data, val_data = train_val_data_process() # 加载数据 train_process = train_model_process(LeNet, train_data, val_data, num_epochs=20) # 启动训练 matplot_acc_loss(train_process) # 可视化二、重点问题问题 1：分类转回归的修改（代码 + 损失函数）分类任务预测离散类别（如 0-9），回归任务预测连续值（如房价、温度），核心修改如下：维度分类任务回归任务模型输出层维度 = 类别数（10）维度 = 1（单值回归）损失函数nn.CrossEntropyLoss()nn.MSELoss() （MSE）/nn.L1Loss() （MAE）标签类型整数（0-9）浮点型（如 3.14）评估指标准确率MAE/RMSE（平均绝对误差 / 均方根误差）预测逻辑torch.argmax(output, 1)output.squeeze() （直接取输出值）核心修改示例：# 1. 回归损失函数 criterion = nn.MSELoss() # 替换交叉熵 # 2. 模型输出层修改（LeNet） # 分类：self.fc3 = nn.Linear(84, 10) # 回归：self.fc3 = nn.Linear(84, 1) # 3. 训练阶段（回归） for step, (b_x, b_y) in enumerate(train_dataloader): b_x = b_x.to(device) b_y = b_y.float().to(device) # 回归标签转浮点型 model.train() output = model(b_x) # 匹配维度：output.squeeze()去掉1维，和b_y对齐 loss = criterion(output.squeeze(), b_y) optimizer.zero_grad() loss.backward() optimizer.step() # 回归指标：MAE（替换准确率） train_mae += torch.sum(torch.abs(output.squeeze() - b_y.data)) train_loss += loss.item() * b_x.size(0) train_num += b_x.size(0)问题 2：交叉熵损失的内置逻辑nn.CrossEntropyLoss() 是 PyTorch 分类任务的核心损失，内置 2 个关键步骤，无需手动实现：Step1：对模型输出（logits）做 Softmax将 “原始得分” 转为概率分布（所有类别概率和为 1）。Step2：对 Softmax 输出做负对数似然损失（NLLLoss）惩罚错误预测（正确类别概率越低，损失越大）。关键注意：模型输出无需手动加 Softmax，否则会导致 Softmax 被应用两次，损失计算错误；等价于：nn.CrossEntropyLoss() = nn.LogSoftmax() + nn.NLLLoss()。完整代码如下：import copy import time import torch from torchvision.datasets import FashionMNIST from torchvision import transforms import torch.utils.data as Data import numpy as np import matplotlib.pyplot as plt from model import LeNet import torch.nn as nn import pandas as pd def train_val_data_process(): train_data = FashionMNIST(root='./data', train=True, transform=transforms.Compose([transforms.Resize(size=28), transforms.ToTensor()]), download=True) train_data, val_data = Data.random_split(train_data, [round(0.8*len(train_data)), round(0.2*len(train_data))]) train_dataloader = Data.DataLoader(dataset=train_data, batch_size=32, shuffle=True, num_workers=2) val_dataloader = Data.DataLoader(dataset=val_data, batch_size=32, shuffle=True, num_workers=2) return train_dataloader, val_dataloader def train_model_process(model, train_dataloader, val_dataloader, num_epochs): # 设定训练所用到的设备，有GPU用GPU没有GPU用CPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 使用Adam优化器，学习率为0.001 optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 损失函数为交叉熵函数 criterion = nn.CrossEntropyLoss() # 将模型放入到训练设备中 model = model.to(device) # 复制当前模型的参数 best_model_wts = copy.deepcopy(model.state_dict()) # 初始化参数 # 最高准确度 best_acc = 0.0 # 训练集损失列表 train_loss_all = [] # 验证集损失列表 val_loss_all = [] # 训练集准确度列表 train_acc_all = [] # 验证集准确度列表 val_acc_all = [] # 当前时间 since = time.time() for epoch in range(num_epochs): print("Epoch {}/{}".format(epoch, num_epochs-1)) print("-"*10) # 初始化参数 # 训练集损失函数 train_loss = 0.0 # 训练集准确度 train_corrects = 0 # 验证集损失函数 val_loss = 0.0 # 验证集准确度 val_corrects = 0 # 训练集样本数量 train_num = 0 # 验证集样本数量 val_num = 0 # 对每一个mini-batch训练和计算 for step, (b_x, b_y) in enumerate(train_dataloader): # 将特征放入到训练设备中 b_x = b_x.to(device) # 将标签放入到训练设备中 b_y = b_y.to(device) # 设置模型为训练模式 model.train() # 前向传播过程，输入为一个batch，输出为一个batch中对应的预测 output = model(b_x) # 查找每一行中最大值对应的行标 pre_lab = torch.argmax(output, dim=1) # 计算每一个batch的损失函数 loss = criterion(output, b_y) # 将梯度初始化为0 optimizer.zero_grad() # 反向传播计算 loss.backward() # 根据网络反向传播的梯度信息来更新网络的参数，以起到降低loss函数计算值的作用 optimizer.step() # 对损失函数进行累加 train_loss += loss.item() * b_x.size(0) # 如果预测正确，则准确度train_corrects加1 train_corrects += torch.sum(pre_lab == b_y.data) # 当前用于训练的样本数量 train_num += b_x.size(0) for step, (b_x, b_y) in enumerate(val_dataloader): # 将特征放入到验证设备中 b_x = b_x.to(device) # 将标签放入到验证设备中 b_y = b_y.to(device) # 设置模型为评估模式 model.eval() # 前向传播过程，输入为一个batch，输出为一个batch中对应的预测 output = model(b_x) # 查找每一行中最大值对应的行标 pre_lab = torch.argmax(output, dim=1) # 计算每一个batch的损失函数 loss = criterion(output, b_y) # 对损失函数进行累加 val_loss += loss.item() * b_x.size(0) # 如果预测正确，则准确度train_corrects加1 val_corrects += torch.sum(pre_lab == b_y.data) # 当前用于验证的样本数量 val_num += b_x.size(0) # 计算并保存每一次迭代的loss值和准确率 # 计算并保存训练集的loss值 train_loss_all.append(train_loss / train_num) # 计算并保存训练集的准确率 train_acc_all.append(train_corrects.double().item() / train_num) # 计算并保存验证集的loss值 val_loss_all.append(val_loss / val_num) # 计算并保存验证集的准确率 val_acc_all.append(val_corrects.double().item() / val_num) print("{} train loss:{:.4f} train acc: {:.4f}".format(epoch, train_loss_all[-1], train_acc_all[-1])) print("{} val loss:{:.4f} val acc: {:.4f}".format(epoch, val_loss_all[-1], val_acc_all[-1])) if val_acc_all[-1] > best_acc: # 保存当前最高准确度 best_acc = val_acc_all[-1] # 保存当前最高准确度的模型参数 best_model_wts = copy.deepcopy(model.state_dict()) # 计算训练和验证的耗时 time_use = time.time() - since print("训练和验证耗费的时间{:.0f}m{:.0f}s".format(time_use//60, time_use%60)) # 选择最优参数，保存最优参数的模型 torch.save(best_model_wts, "./model.pth") train_process = pd.DataFrame(data={"epoch":range(num_epochs), "train_loss_all":train_loss_all, "val_loss_all":val_loss_all, "train_acc_all":train_acc_all, "val_acc_all":val_acc_all,}) return train_process def matplot_acc_loss(train_process): # 显示每一次迭代后的训练集和验证集的损失函数和准确率 plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(train_process['epoch'], train_process.train_loss_all, "ro-", label="Train loss") plt.plot(train_process['epoch'], train_process.val_loss_all, "bs-", label="Val loss") plt.legend() plt.xlabel("epoch") plt.ylabel("Loss") plt.subplot(1, 2, 2) plt.plot(train_process['epoch'], train_process.train_acc_all, "ro-", label="Train acc") plt.plot(train_process['epoch'], train_process.val_acc_all, "bs-", label="Val acc") plt.xlabel("epoch") plt.ylabel("acc") plt.legend() plt.show() if __name__ == '__main__': # 加载需要的模型 LeNet = LeNet() # 加载数据集 train_data, val_data = train_val_data_process() # 利用现有的模型进行模型的训练 train_process = train_model_process(LeNet, train_data, val_data, num_epochs=20) matplot_acc_loss(train_process)
深度学习（DL）-经典卷积神经网络-输入（输出）通道数与卷积核的关系
输入通道数 → 决定「每个卷积核的通道数」（硬性匹配，没得选）人为设定的输出通道数 → 决定「卷积核的总个数」（超参数，自己定）简单说，卷积核有两个关键维度，输入通道数只管其中一个（每个卷积核的深度），不管另一个（卷积核的总数）先明确：卷积核不是 “单个小方块”，而是 “多层小方块叠成的小积木”你可以把一个卷积核想象成「叠起来的多层滤镜」，层数就是这个卷积核的通道数—— 这个层数必须和输入特征图的通道数完全一样，这是卷积运算的硬性规则（维度对不上，根本没法计算）。而卷积层的卷积核总个数，是另一回事，它和输入通道数无关，完全由你想得到的输出通道数决定（就是我们之前说的人为设定的超参数）。举最直观的例子（VGG16 首层，输入 3 通道 RGB 图）输入特征图：224×224×3（3 是输入通道数）我们要的输出通道数：64（人为设定）对应的卷积核设计：卷积核总个数：64 个（和输出通道数一致，人为定的）；每个卷积核的规格：3×3×3（最后一个 3 是卷积核的通道数，和输入通道数 3 硬性匹配，没得选）。卷积的运算过程（为什么必须匹配）：每个 3×3×3 的卷积核，会把自己的 3 层滤镜，分别和输入特征图的 3 个通道做卷积（逐元素相乘求和），然后把 3 个通道的卷积结果加起来，得到1 张单通道的特征图；64 个这样的卷积核，就会得到 64 张单通道特征图，叠在一起就是输出的 224×224×64 特征图（64 个输出通道）。如果输入通道数是 64（比如 VGG16 第一层池化后），要的输出通道数是 128（人为定），那卷积核就是：128 个 3×3×64 的卷积核（64 是卷积核通道数，匹配输入；128 是个数，匹配输出）。核心关系总结维度由什么决定？性质例子（输入 64 通道→输出 128 通道）单个卷积核的通道数输入通道数硬性匹配64（和输入通道数一致）卷积层的卷积核总个数输出通道数（人为定）可调整超参数128（人为设定的输出通道数）再补一个反例，彻底打消疑问如果输入是 64 通道，你想输出 256 通道，那卷积核就是256 个 3×3×64—— 输入通道数还是 64，但卷积核个数直接跳到 256，和输入通道数完全无关，只看你要的输出通道数。反之，如果输入通道数变了（比如从 3 变成 1，黑白图），那卷积核的个数可以不变（还是 64），但每个卷积核的通道数必须从 3 变成 1（3×3×3→3×3×1）。总结输入通道数管 “单个卷积核的深度”（必须匹配），输出通道数管 “卷积核的总数”（人为设定），二者毫无决定关系，只是卷积核的两个不同属性
深度学习（DL）-经典卷积神经网络-以VGG16理解合理设置通道数的原理
VGG16 设计通道数（64→128→256→512，后两段保持 512）的核心初衷，是围绕「视觉特征提取的天然规律」，适配自身「小卷积核 + 深度堆叠」的核心结构，同时在「特征表达能力、计算量、训练稳定性、泛化能力」之间做最优的工程平衡—— 这个通道数序列不是单纯的 “拍脑袋实验值”，而是把视觉认知逻辑、网络结构特点、硬件工程限制三者结合后的精准设计，每一个通道数的梯度和上限，都对应着明确的设计目标。初衷 1贴合视觉特征「从简到繁、从少到多」的天然提取规律这是最根本的初衷，所有卷积网络的通道数设计都基于此，VGG16 把这个规律做到了极致简洁的「翻倍式适配」。图片的视觉特征提取是层层组合的过程：「浅层」只能提取低级基础特征（竖线、横线、斜纹、颜色块、明暗交界），这些特征的种类极少、模式固定，根本不需要多通道 ——64 个通道足以把所有基础特征 “抓全”，再多就是计算量浪费（比如用 128 个通道抓竖线，大部分通道会学到重复的特征）；「深层」会把浅层的低级特征拼接、融合成高级语义特征（比如 “竖线 + 横线” 拼成角、“多个角” 拼成眼睛、“眼睛 + 鼻子” 拼成人脸，还有车轮、树叶、桌腿等无数物体部件），这些高级特征的种类呈指数级暴增，必须用更多通道来「分门别类编码」——256→512 的通道数，就是为了给每一种复杂的高级特征分配一个 “专属编码通道”，保证网络能精准区分不同的语义特征，不会把 “眼睛” 和 “车轮” 的特征混在一起。简单说：VGG16 的通道数翻倍，就是跟着特征的 “复杂程度 / 种类数” 翻倍走，让通道数刚好匹配特征的实际需求，不多余、不不足。初衷 2支撑 VGG16「小卷积核 + 深度堆叠」的核心结构（专属设计初衷）VGG16 的标志性设计是放弃大卷积核（如 7×7、5×5），用多个 3×3 小卷积核堆叠（比如 2 层 3×3 卷积代替 1 层 5×5，3 层 3×3 代替 1 层 7×7），这个设计的目的是用更少的参数，获得更多的非线性激活（ReLU），提升特征表达的灵活性。但这个结构有个「硬需求」：必须有足够的通道数，才能避免 “信息瓶颈”——如果深层通道数和浅层一样（比如全程 64），那么多层 3×3 卷积堆叠后，浅层的低级特征会在传递中不断 “挤压”，根本无法充分融合成高级特征，就像用一根细管子传大量水，水会不断溢出，深度堆叠的优势会完全消失。而 64→128→256→512 的通道数递增，就是为深度堆叠的小卷积核 “铺好信息通道”：每一层卷积堆叠前，先增加通道数，让特征融合有足够的 “空间”，保证 VGG16 的深度堆叠能真正发挥作用（更多非线性、更好的特征提取）。初衷 3精准平衡「模型容量」和「计算量 / 硬件限制」深度学习中，通道数是提升模型容量（学习能力）的核心手段（比单纯加深网络更高效），但通道数越多，计算量、显存占用就越大 —— 而 VGG16 诞生时（2014 年），硬件算力和显存还远不如现在（当时主流显卡显存只有 2-4G），通道数设计必须兼顾 “能训、能跑”。VGG16 用 「通道数翻倍 + 池化层尺寸减半」的组合 ，实现了计算量的「动态平衡」，这是最精妙的工程设计：池化层把特征图的长、宽各减半，特征图面积变为原来的 1/4，计算量会大幅下降；紧接着的卷积层把通道数翻倍，计算量会小幅上升；一降一升后，整体计算量基本保持稳定，不会呈指数级爆炸。比如从 64 通道→池化→128 通道：池化让面积减 1/4，卷积核翻倍让通道数 ×2，最终计算量只有原来的2/4=1/2，显存占用也可控；而把深层通道数定在 512，不是不想升到 1024，而是512 是当时硬件能支撑的 “性价比上限”——1024 通道会让显存溢出、训练无法进行，而 512 通道已经能让网络学到足够的高级特征，模型精度和工程实现达到最优平衡。初衷 4保证网络「训练稳定性」，避免梯度消失 / 特征重复如果通道数设计混乱（比如浅层 128、深层 64，或突然从 64 跳到 512），会出现两个问题：梯度消失：深层通道数过少，特征信息在反向传播时会不断丢失，梯度无法有效传回到浅层，网络学不到东西；特征重复：浅层通道数过多，有限的低级特征会让大量通道学到重复的内容，网络出现 “冗余学习”，不仅浪费计算量，还会导致过拟合。而 VGG16缓慢、均匀的翻倍式通道数递增（64→128→256→512），让特征信息在正向传播时能「平稳融合」，反向传播时梯度能「平稳传递」，既避免了梯度消失，又减少了特征重复，保证了网络在深度堆叠的情况下，依然能稳定训练、收敛。同时，深层保持 512 通道不变（最后两段卷积都是 512），也是为了让高级特征的提取更 “聚焦”—— 到了网络最深层，特征已经是最复杂的全局语义特征，不需要再增加通道数，保持 512 通道能让网络集中精力对这些高级特征做精细化编码，而不是继续增加冗余通道。总结：整个通道数序列（64→128→256→512）的设计，本质是 “让通道数服务于网络的核心目标”：以视觉特征的天然规律为根本，以自身小卷积核 + 深度堆叠的结构为支撑，以计算量 / 硬件的工程限制为边界，最终在「特征表达能力、模型容量、计算量、训练稳定性」之间，找到了一个适配 2014 年技术背景、又能最大化模型性能的最优解 。而这个 “通道数递增 + 池化尺寸减半” 的设计范式，也因为 VGG16 的成功，成为后续所有卷积网络（ResNet、DenseNet 等）的通用设计思路，直到今天依然被沿用。
深度学习（DL）-经典卷积神经网络-概念理解-通道数到底是什么
通道数就是特征图的「特征种类数」，也可以理解成网络看图片的「专属视角数」—— 每个通道只盯着图片里的一种特定特征，通道数越多，网络能同时捕捉的特征种类就越全。再往简单说，把卷积网络想象成一群 “特征侦探”，通道数就是「侦探的人数」，每个侦探只负责找一种特定线索，所有侦探的结果合在一起，就是网络对图片的完整特征理解。以「输入图片通道」为例平时看的 RGB 彩色图，就是3 个通道：红色通道（R）：只保留图片里红色的明暗信息（不管绿、蓝）；绿色通道（G）：只保留绿色的明暗信息；蓝色通道（B）：只保留蓝色的明暗信息。这 3 个通道叠在一起，就是你看到的彩色画面。如果是黑白图，就是1 个通道，只有明暗信息，没有颜色种类。通道的本质就是「把信息按种类分开存」，方便后续针对性处理。以VGG16为例，卷积的「网络通道数」是什么卷积层的输出通道数 = 卷积核个数，其实就是给网络新增了对应数量的 “专属侦探”：比如 VGG16 首层 64 个卷积核→64 个通道，就是 64 个侦探，每个侦探只找一种简单特征（有的专找竖线、有的专找横线、有的专找斜纹、有的专找颜色块，都是最基础的视觉线索）；到了深层 512 个卷积核→512 个通道，就是 512 个侦探，每个侦探只找一种复杂特征（有的专找眼睛、有的专找车轮、有的专找树叶轮廓、有的专找桌面边缘，都是浅层简单特征拼出来的 “高级线索”）。每个通道最终会输出一张「特征图」，这张图里越亮的地方，代表图片对应位置越符合这个通道的特征（比如 “竖线通道” 的特征图，图片里竖线的位置会特别亮，其他地方暗）。通道数的「核心作用」：1. 「分门别类存特征」，不混不乱，方便后续融合如果把所有特征都揉在 1 个通道里，网络根本分不清 “这里是竖线”“那里是纹理”，就像把所有侦探的线索堆在一个盒子里，越往后越乱。而通道就是给每个特征配一个 “专属文件夹”，竖线放竖线的文件夹，眼睛放眼睛的文件夹，后续网络可以轻松把这些文件夹里的信息拼起来，还原出图片里的物体（比如 “眼睛 + 鼻子 + 嘴巴” 拼出人脸）。2. 「通道越多，能抓的特征越全」，网络看得越 “明白”浅层用少通道（64/128）：因为图片里的简单特征就那么几种（线、纹、色块），多了也是浪费，相当于没必要找 100 个侦探都去查竖线；深层用多通道（256/512）：因为深层要抓的复杂特征特别多（不同的物体部件、不同的组合方式），需要更多侦探各司其职，才能把图片里的细节都查清楚，相当于查一个复杂案件，需要不同领域的侦探（法医、痕迹、刑侦）一起上。VGG16 里池化层不改变通道数，就是因为池化只是 “把侦探的线索图缩小（去掉无用细节）”，但不会删掉任何侦探（还是原来的 64/128/512 个），线索的种类数没变，只是线索图更简洁了；而卷积层增加通道数，就是新增一批更专业的侦探，从查简单线索升级为查复杂线索。终极大白话总结通道数 = 网络的特征种类数 / 侦探人数；每个通道管一种特征，通道越多，网络能捕捉的特征越全，对图片的理解就越深刻；VGG16 逐层加通道，就是让网络从 “看简单线条” 慢慢升级为 “看复杂物体”。
一、基础准备（Anaconda Prompt 中）# 1. 清屏，让界面更整洁 cls二、环境管理（base 环境下执行）# 2. 查看当前系统中所有已创建的conda环境（确认环境列表） conda env list # 3. 方式1：创建指定名称的虚拟环境（推荐，简单易记） # 替换【你的环境名】为自定义名称，比如data_analysis conda create -n 你的环境名 python=3.9 # 3. 方式2：创建指定安装路径的虚拟环境（按需选择） # 替换【你的安装路径】和【你的环境名】，比如D:\conda_env\data_analysis conda create --prefix=你的安装路径\你的环境名 python=3.9 # 4. 激活创建好的虚拟环境（方式1创建的环境） conda activate 你的环境名 # （可选）如果是方式2创建的路径型环境，激活命令如下 # conda activate 你的安装路径\你的环境名三、虚拟环境内的库安装与验证# 5. 查看当前环境已安装的库（激活环境后执行） conda list # 6. 安装指定版本的NumPy（使用清华源加速） pip install numpy==1.21.5 -i https://pypi.tuna.tsinghua.edu.cn/simple # 7. 安装指定版本的Pandas pip install Pandas==1.2.4 -i https://pypi.tuna.tsinghua.edu.cn/simple # 8. 安装指定版本的Matplotlib pip install Matplotlib==3.5.1 -i https://pypi.tuna.tsinghua.edu.cn/simple # 9. 验证NumPy版本（确认安装成功） pip show numpy # （可选）验证其他库版本，替换numpy即可 # pip show pandas # pip show matplotlib四、环境退出与删除（按需执行）# 10. 退出当前虚拟环境，回到base环境 conda deactivate # 11. 删除指定名称的虚拟环境（需先退出该环境） conda remove -n 你的环境名 --all # （可选）删除路径型环境 # conda remove --prefix=你的安装路径\你的环境名 --all
深度学习（DL）-代码探究-张量怎么变成二维矩阵
在深度学习代码中出现了张量如何转化为二维矩阵的例子，用AI整理过后形成了本文。一、先理解：为什么要把张量展平成二维矩阵？全连接层（仿射层）的核心运算是矩阵乘法：y = X · W + b，而矩阵乘法有严格的维度要求：输入X必须是二维矩阵（形状：[样本数, 输入特征数]）权重W是二维矩阵（形状：[输入特征数, 输出特征数]）但实际场景中，输入往往不是二维的：比如批量图片：形状是 [批量数, 通道数, 高, 宽]（例：(100, 3, 28, 28) → 100 张 3 通道 28×28 的图片）比如批量序列数据：形状是 [批量数, 序列长度, 特征维度]（例：(50, 10, 128)）这些高维张量无法直接和权重矩阵做乘法，必须先展平成二维——固定 “样本数” 为第一维，把剩下的所有维度合并成 “特征数” 一维。二、核心操作：X.reshape(X.shape[0], -1) 详解reshape是 NumPy/PyTorch 中重塑数组 / 张量形状的函数，这里的关键是X.shape[0]和-1的组合：部分作用X.shape[0]固定第一维为样本数（批量大小），保证每个样本的特征独立，不混淆。-1NumPy 的 “自动计算维度” 标记：让程序自动计算这一维的大小，等于「总元素数 ÷ 样本数」。例子假设输入是 100 张 3 通道 28×28 的图片，张量形状为 X.shape = (100, 3, 28, 28)：import numpy as np # 模拟输入：100张3通道28×28的图片 X = np.random.randn(100, 3, 28, 28) print("原始形状：", X.shape) # 输出：(100, 3, 28, 28) # 展平成二维矩阵 X_flat = X.reshape(X.shape[0], -1) print("展平后形状：", X_flat.shape) # 输出：(100, 2352) # 2352 = 3 × 28 × 28，刚好是单张图片的所有像素数（特征数）再比如输入是 50 条序列数据（形状(50, 10, 128)）：X = np.random.randn(50, 10, 128) X_flat = X.reshape(X.shape[0], -1) print(X_flat.shape) # 输出：(50, 1280) → 1280 = 10 × 128三、original_x_shape 的关键作用：恢复梯度形状反向传播时，我们计算出的输入梯度dX一开始是展平后的二维形状（比如(100, 2352)），但前一层的输出是高维的（比如卷积层输出是(100, 3, 28, 28)），如果直接把二维的梯度传给前一层，维度会不匹配，导致计算错误。所以original_x_shape的作用就是保存输入的原始形状，反向传播时用它把梯度恢复成原始维度：python运行# 反向传播时恢复梯度形状 dX = np.dot(dy, self.W.T) # 计算出的梯度是二维：(100, 2352) dX = dX.reshape(*self.original_x_shape) # 恢复成(100, 3, 28, 28)这里的*self.original_x_shape是 Python 的 “解包” 操作，把保存的形状元组(100, 3, 28, 28)拆解成参数传给reshape，等价于dX.reshape(100, 3, 28, 28)。总结X.reshape(X.shape[0], -1) 是高维张量适配全连接层的核心：固定样本数维度，自动展平剩余维度为一维，得到 “样本数 × 总特征数” 的二维矩阵。original_x_shape 是反向传播的 “维度桥梁”：保存原始形状，确保梯度能恢复成前一层需要的维度，避免维度不匹配错误。-1 在 reshape 中是 “懒人神器”：无需手动计算展平后的特征数，由程序自动推导，适配任意高维输入。
深度学习（DL）-原理探究-从ReLu函数理解何为反向传播
前排提醒：阅读本文时记得要动脑子和动笔，不要想当然。一、反向传播是什么“反向传播”（Backpropagation） 是深度学习中训练神经网络的核心算法神经网络训练时，从损失函数（最终误差）出发，沿着神经网络的层反向计算每个参数的梯度，并利用梯度下降法更新参数，最终让损失函数变小（模型预测更准）的过程。 前向传播：“输入数据→经过各层计算→输出预测值→计算损失（误差）”（从输入到输出）； 反向传播：“从损失出发→反向算出每一层参数该怎么调整→更新参数”（从输出回到输入）；一般而言，需要做一次前向传播才能做一次反向传播。核心目的：求参数的梯度神经网络的参数（权重 W、偏置 b）需要不断调整，而调整的依据是梯度（参数变化对损失的影响程度）。 反向传播的本质就是用链式法则（微积分里的复合函数求导），把 “损失对最终输出的梯度” 一步步传递回每一层，算出 “损失对每一个参数的梯度”。二、结合ReLu函数假设我们有一个极简网络：输入x → 全连接层（W,b） → ReLU层 → 输出y → 损失L步骤 1：前向传播（正向算结果） 全连接层：z = x*W + bReLU 层：y = max(z, 0)（就是我们写的 ReLU.forward）损失：L = (y - 真实值)²（比如均方误差损失）步骤 2：反向传播（反向算梯度） 我们的目标是算dL/dW（损失对权重 W 的梯度）和dL/db（损失对偏置 b 的梯度），用链式法则拆解： 最终，反向传播就是把这些梯度 “从后往前” 乘起来，得到每个参数的梯度，再用W = W - 学习率*dL/dW更新参数。 逐个拆解（结合 ReLU 类）：dL/dy：损失对 ReLU 输出 y 的梯度（比如L=(y-t)²，则dL/dy=2*(y-t)）；dy/dz：ReLU 的导数（z>0 时 = 1，z≤0 时 = 0）—— 这就是后续 ReLU.backward 里用mask清零梯度的逻辑；dz/dW：全连接层对 W 的导数（=x），dz/db=1；最终，反向传播就是把这些梯度 “从后往前” 乘起来，得到每个参数的梯度，再用W = W - 学习率*dL/dW更新参数。 结合我们写的 ReLU.backward 理解我们的 ReLU 类的backward函数，就是反向传播中 “ReLU 层” 的梯度计算步骤：输入dout：其实就是dL/dy（损失对 ReLU 输出的梯度）；dout[self.mask] = 0：就是计算dy/dz * dout（因为 z≤0 时dy/dz=0，所以梯度清零；z>0 时dy/dz=1，梯度不变）；返回的dx：就是dL/dz（损失对 ReLU 输入 z 的梯度），会传递给前一层（全连接层）继续反向计算。反向传播的关键特点链式法则是核心：把复杂的复合函数求导拆解成多个简单导数的乘积，从后往前逐层计算；效率高：一次反向传播就能算出所有参数的梯度，不用重复计算；和前向传播配套：前向传播时会记录中间结果（比如 ReLU 的mask），反向传播时复用这些结果快速算梯度。三、具体简单案例指定的数值（x=2、W=3、b=1、真实值 = 5），搭建一个极简神经网络（输入→全连接层→ReLU 层→损失）第一步：先明确网络结构我们的极简网络只有 3 层：输入x → 全连接层（W=3, b=1）→ ReLU层 → 输出y → 损失L（均方误差）全连接层公式：z=x∗W+bReLU 层公式：y=max(z,0)损失函数（均方误差）：L=1/2(y−t)2（加 1/2 是为了求导时消去系数，不影响梯度方向）第二步：前向传播（正向算结果）我们从输入 x 开始，一步步算到损失 L：步骤公式数值计算结果1. 全连接层计算z=x∗W+bz=2∗3+1=7z=72. ReLU 层计算y=max(z,0)y=max(7,0)=7y=73. 损失计算L=1/2(y−t)2L=1/2(7−5)2=1/2∗4=2L=2前向传播结论：输入 x=2 经过网络后输出 y=7，和真实值 5 的损失是 2。第三步：反向传播（反向算梯度）反向传播的核心是链式法则，从损失 L 出发，反向算每一层的梯度，最终得到损失对 W、b、x 的梯度（我们的目标是更新 W 和 b）。先明确反向传播的目标我们需要算 3 个梯度：dL/dy：损失对 ReLU 输出 y 的梯度dL/dz：损失对全连接层输出 z 的梯度（ReLU 的输入）dL/dW、dL/db：损失对全连接层参数 W、b 的梯度（最终要用来更新参数）逐步骤计算梯度（纯数值）步骤 1：计算dL/dy（损失对 ReLU 输出 y 的梯度）公式：dL/dy=dyd[1/2(y−t)2]=y−t数值计算：dL/dy=7−5=2结论：dL/dy=2步骤 2：计算dL/dz（损失对全连接层 z 的梯度，对应 ReLU 反向）ReLU 的导数规则：根据链式法则：dL/dz=dL/dy∗dy/dz先判断 z 的取值：z=7>0 → dy/dz=1数值计算：dL/dz=2∗1=2结论：dL/dz=2（这一步对应我们 ReLU 类的 backward 函数：因为 z>0，mask 为 False，所以 dout 不变，直接返回 2）步骤 3：计算dL/dW（损失对权重 W 的梯度）全连接层 z 对 W 的导数：dz/dW=x根据链式法则：dL/dW=dL/dz∗dz/dW数值计算：dL/dW=2∗2=4结论：dL/dW=4（W 的梯度是 4，更新时 W 要减去 “学习率 ×4”）步骤 4：计算dL/db（损失对偏置 b 的梯度）全连接层 z 对 b 的导数：dz/db=1根据链式法则：dL/db=dL/dz∗dz/db数值计算：dL/db=2∗1=2结论：dL/db=2（b 的梯度是 2，更新时 b 要减去 “学习率 ×2”）额外：计算dL/dx（损失对输入 x 的梯度）全连接层 z 对 x 的导数：dz/dx=W根据链式法则：dL/dx=dL/dz∗dz/dx数值计算：dL/dx=2∗3=6结论：dL/dx=6第四步：梯度更新示例（验证逻辑）假设学习率 η=0.1（超参数，控制更新幅度），更新 W 和 b：新 W = 原 W - η×dL/dW = 3 - 0.1×4 = 2.6新 b = 原 b - η×dL/db = 1 - 0.1×2 = 0.8更新后再做一次前向传播：z = 2×2.6 + 0.8 = 6y = max(6, 0) = 6L = 1/2×(6-5)² = 0.5可以看到：损失从 2 降到了 0.5，说明参数更新有效。总结（核心数值）梯度名称公式数值结果dL/dyy - t2dL/dzdL/dy × dy/dz2dL/dWdL/dz × x4dL/dbdL/dz × 12dL/dxdL/dz × W6这个过程完美对应我们后续写的 ReLU 类：前向传播：z=7>0 → ReLU 输出 y=7（mask=False，无位置置 0）；反向传播：dout=dL/dy=2 → 因为 mask=False，所以 dx=dout=2（即 dL/dz=2）。四、代码示例import numpy as np # 导入numpy，支持数组的布尔索引、copy等操作 class Relu: """ 基于NumPy实现的ReLU激活层类 核心功能： - 前向传播：y = max(x, 0)，记录x<=0的位置 - 反向传播：根据记录的位置清零梯度（ReLU导数特性） """ def __init__(self): # 初始化mask，用于标记前向传播中x<=0的位置（布尔数组） self.mask = None def forward(self, x): """ ReLU前向传播计算 参数： x (np.ndarray): 输入数组（神经网络前一层输出） 返回： np.ndarray: ReLU激活后的输出（x>0保留，x<=0置0） """ # 生成布尔掩码：x<=0的位置为True，x>0为False self.mask = (x <= 0) # 复制输入避免修改原数据 out = x.copy() # 把mask为True的位置置0（实现ReLU核心逻辑） out[self.mask] = 0 return out def backward(self, dout): """ ReLU反向传播计算梯度 参数： dout (np.ndarray): 后一层传递过来的输出梯度 返回： np.ndarray: 当前层输入的梯度（传递给前一层） """ # 利用mask清零x<=0位置的梯度（对应ReLU导数为0） dout[self.mask] = 0 # x>0位置梯度不变（对应ReLU导数为1），直接赋值 dx = dout return dx # 测试代码（仅在直接运行该文件时执行） if __name__ == "__main__": # 1. 创建ReLU实例 relu = Relu() # 2. 前向传播测试 x = np.array([3.0, -2.0, 0.0, 5.0, -1.5]) out_forward = relu.forward(x) print("前向传播输出：", out_forward) # 预期输出：[3. 0. 0. 5. 0. ] # 3. 反向传播测试 dout = np.array([0.5, 1.2, 0.8, 0.3, 0.7]) dx_backward = relu.backward(dout) print("反向传播梯度：", dx_backward) # 预期输出：[0.5 0. 0. 0.3 0. ]