「转载」北京大学计算机基础能力手册
本网站仅做转载，最终解释权仍在原作者。点击下载main.pdf北京大学计算机基础能力手册
本文章基于安徽理工大学数据科学与大数据技术2024级《分布式数据库原理与技术》课程授课内容整合重构，搭建条理清晰、体系完整的知识框架，可供同学们复习查阅。Chiale2026.6.23一、大数据领域科普概念（Lesson 1，仅作了解）数据中台数据湖仓数据脱敏分布式数据库雏形（ICBC Bank）区块链TCP 三次握手、四次挥手联邦学习 / 深度学习二、分布式数据库理论基础（Lesson 2）三级模式/两级映像在分布式数据库中仍然保留，并增加了新内容：全局模式和分片模式。性能对比指标与传统 RDBMS 对比的指标：存储空间大小、检索效率。HDFS（GFS）的共享存储通过何种机制实现共享服务（问题提出，未在笔记中展开答案）。数据分类结构化数据：MySQL、Oracle 等半结构化数据：JSON、XML非结构化数据：视频等（处理路径：视频 → YOLO → JSON）三、Hive 基础环境与工作模式（Lesson 2 - Lesson 3）Hive 的三种部署方式（Lesson 2 Q4）嵌入式 Derby、远程 Metastore、本地 MetastoreHive 的三种工作模式（Lesson 3 Q1）Derby 模式、远程模式、本地模式环境操作要点启动顺序：关闭防火墙 → 启动 Zookeeper → 检查状态 → 在 master 上 sbin/start-all.sh 启动 HadoopMySQL 安装在 slave2，Hive 启用本地模式关闭顺序：Hive 中 exit → master sbin/stop-all.sh → 所有机器 zkServer.sh stop → 全部关机统一数据库名：bigdata2024，本地数据目录：/dataMetastore 存储位置存储在 HDFS 上的 /warehousedir/home（除 Derby 模式外）四、Hive 表与数据管理（Lesson 3 - Lesson 9）4.1 基本操作查看表结构：desc 表名 （展示字段名）查看详细参数：desc formatted 表名显示建表语句：show create table 表名永久配置：修改 hive-site.xml；临时配置：Hive 控制台直接使用 set命令，例如：set hive.cli.print.current.db=true; set hive.cli.print.header=true;4.2 表的默认存储与数据导入方式默认存储位置：/warehousedir/home/xxxx.db/表名（即 Directory）数据导入的三种方式：insert 语句：insert into table 表名 values(...); 或从其他表查询插入，会在表目录下生成文件 000000_0hdfs 上传：hdfs dfs -put 本地文件 表对应的HDFS目录，文件会直接成为表数据的一部分load data 命令：load data local inpath '本地文件路径' into table 表名;会将本地文件复制到表目录下，若文件与表目录中已有文件重名，会自动生成带序号的副本（如 xxx_copy_1）。若使用 overwrite 关键字则会先清空表目录下的所有文件再装载。4.3 自定义存储位置（LOCATION）可以使用 location 子句指定表在 HDFS 上的存储路径，此时不会自动创建 数据库.db/表名 的层级目录，而是直接使用给定路径。要求：location 路径最好与默认管理体系一致，且精确到表名级别；建表时若有其他约束，location 必须放在最后一行。示例：create table stu_test(sid int, sname string, sage int, addr string) location '/testdb';此时 HDFS 上只有 /testdb 目录，不会出现 xxx.db/stu_test。通常推荐的做法是让 location 的末级目录与表名一致，并放在数据库目录下，以便管理。4.4 复杂数据类型（Array、Map、Struct）详细过程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#解析过程：Hive 读取文件时先按行分割，再按逗号切分出 sid、sname 和整个数组字符串 "80#90.5#35"，然后用 # 切分数组元素，每个元素转换为 float。若某元素为空字符串（如 90# #88 中的第二个 # 之间为空），则解析为 NULL。末位的空元素（如 78#）会被忽略，因此 jemy 的数组只含一个元素 [78.0]。装载数据：load data local inpath '/opt/data/student1.txt' into table student1;查询数组元素（索引从 0 开始）：select grade[1] as second_grade from student1 where sid = 101;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';分隔符说明：字段间用逗号；Map 中的键值对之间用 # 分隔；键和值之间用 : 分隔。数据文件student2.txt 示例：1,zhangsan,语文:80#数学:90.5#英语:35 2,lisi,语文:90#数学:95#英语:88装载方式同 Array。查询 Map 中指定键的值：select grade['数学'] as math_score from student2 where sid = 101;注意：如果建表时去掉 collection items terminated by '#'，则 Hive 无法识别多个键值对之间的分隔，整个 grade 列会被当作一个单一字符串，无法正常解析为 Map。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,zhangsan#19#Female 2,lisi#20#male解析说明：Struct 内部成员之间用 # 分隔，成员顺序与建表时定义的 struct 属性顺序严格一致。可以使用 info.name、info.age 等方式访问成员。4. 复杂类型嵌套限制如 array<map<string,float>> 或 map<string,array<float>> 等嵌套结构，无法直接使用分隔符方式定义。若要实现此类嵌套，必须使用 JSON SerDe（如 'org.apache.hive.hcatalog.data.JsonSerDe'），并将数据文件组织为标准 JSON 格式。示例：create table student3(sid int, sname string, grade array<map<string,float>>) row format serde 'org.apache.hive.hcatalog.data.JsonSerDe';数据文件内容需类似：{"sid":1,"sname":"zhangsan","grade":[{"语文":80},{"数学":90.5}]}核心结论：Array、Map、Struct 三种原生集合类型不能互相嵌套使用；Array 和 Struct 内部元素靠 collection items 分隔，Map 由于自带键值对分隔，隐含了 collection items 的功能。4.5 表的类型与管理1. 管理表（内部表）与外部表创建外部表：create external table student_cp(sid int, sname string) row format delimited fields terminated by ',' location '/tmpdb/student_cp';区别过程：使用 drop table 删除表时：内部表：元数据与 HDFS 上的数据文件一并删除。外部表：仅删除元数据，location 指向的文件夹及数据全部保留。下次创建同名、同结构的外部表并指向同一 location 时，可直接读取原有数据。数据加载区别：load data inpath '云端路径' into table 表名; 会将 HDFS 上的文件移动到表目录，原路径文件消失。load data local inpath '本地路径' 是复制操作，本地文件不删除。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');HDFS 存储结构：数据文件会被放置于 /warehousedir/home/exam.db/part_table/addr=guangxi/ 目录下，文件名保持为 student100。原有数据内容并不包含addr 列，该列的值完全由分区目录名提供，查询时自动作为虚拟列出现。若重复执行相同分区的装载，同一目录下会出现 student100_copy_1 等带编号的副本文件。管理分区：alter table part_table add partition(addr='guangxi'); -- 添加分区（新建目录） alter table part_table drop partition(addr='guangxi'); -- 删除分区（删除对应目录及所有文件）多级分区：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');目录结构变为 .../sage=20/addr=guangxi/。非分区表无法转换为分区表，只能重建。动态分区：建表语法与静态分区完全相同，只是在插入数据时由查询语句决定分区值（例如通过 insert ... select addr from ...），装载速度更快，但对数据质量的控制不如静态分区精准。3. 桶表建表时指定分桶字段和排序字段：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 ... 直接装载文件，HDFS 上不会生成分桶目录，仅有一个原始文件，且数据未按 sid 分桶，也未按 sname 排序。必须通过 insert into 从其他表查询导入，才会真正执行分桶和排序：insert into table buck_table select * from source_table;此时 HDFS 表目录下会出现 000000_0、000001_0 等文件，编号从 0 开始，每个文件对应一个桶。采样查询：select * from buck_table tablesample(bucket 1 out of 4 on sid);无论数据是通过 load data 还是 insert 进入的，采样语法都可执行。但对于 load data 进入的未分桶数据，采样可能返回空或不符合预期结果；只有经过 insert 分桶后的数据才能正确按桶采样。五、数据查询与连接（Lesson 10）5.1 连接类型详解笛卡尔连接：select * from student100, sc100; 无连接条件，返回两表行数的乘积，通常无实际意义。内连接（自然连接）：join ... on ...，只返回两表中满足等值条件的行，学生必须同时出现在 student100 和 sc100 中（不要求一定有成绩）。左连接：left join ... on ...，以左表（student100）为主，返回左表所有行。右表无匹配时，sc100 的字段为 NULL。右连接：right join ... on ...，以右表（sc100）为主，返回右表所有行。全外连接：full outer join ... on ...，同时包含左右连接的结果，相当于 left join 和 right join 的并集（已去重）。左半连接：left semi join ... on ...，本质是嵌套查询，只返回左表中能够在右表找到匹配键的行，且查询结果中不允许出现右表的字段。它在 Hive 中相当于 where exists (select 1 from 右表 where 条件) 但性能更优。限制：Hive 的 left semi join 只支持一层嵌套。若要实现多层筛选（如选课人数大于2的学生），必须借助中间表或派生表：-- 先找出人数>2的课程对应的学号，作为派生表 select * from student100 inner join ( select sc100.sno from sc100 join tempsc100 on sc100.cno = tempsc100.cno where cnt > 2 ) a on student100.sno = a.sno;这样就能避免直接使用多层 left semi join 的限制，且不需要创建物理中间表。5.2 数据导出方式输出到 HDFS 目录：insert overwrite directory '/hdfs路径' row format delimited fields terminated by '#' select * from 表名;从 HDFS 下载到本地：hadoop fs -get /hdfs路径 本地路径拷贝表：深拷贝：create table new_table as select * from old_table; —— 执行 MapReduce，复制表结构和数据。浅拷贝：create table new_table like old_table; —— 仅复制表结构，数据为空。六、视图、临时表与行列转换（Lesson 11 - Lesson 12）6.1 视图与物理表的选择创建视图：create view viewdb as select ... from tb_case group by dname;创建过程不启动 MapReduce，视图只是存储查询定义。当执行 select * from viewdb 时会启动 MapReduce 计算。创建物理表：create table tmp_case as select ... from tb_case group by dname;会在执行时启动 MapReduce，并将结果持久化到 HDFS，后续查询不重复计算。临时性结果集：with t as (select ...) 可用于复杂查询的分解，提升可读性。6.2 条件函数与行列转换if(条件, 真值, 假值) 用于二分类。case when ... then ... else ... end 用于多条件转换，例如：select name, dname, case gender when '男' then 'm' when '女' then 'f' end from tb_case;行转列（透视）示例：统计各部门男女数量。select dname, count(*) as cnt, sum(if(gender='男',1,0)) as M, sum(if(gender='女',1,0)) as F from tb_case group by dname;聚合注意事项：sum(jb+jj+tc) 不能简写为 sum(jb, jj, tc)。greatest() 是行内最大值函数（取一行多列的最大值），max() 是列聚合函数（取多行某一列的最大值）。七、常用函数与实战案例（Lesson 13 - Lesson 16）7.1 字符串函数length(), concat(), trim()（只去除开头和结尾空格），upper(), lower(), substr(), instr()示例：select length('你好'); -- 返回字符数 select trim(concat(' hello ', ' world ')); -- 返回去掉首尾空格的结果7.2 日期与数值函数（身份证号实战）提取出生日期并格式化：alter table cid_info add columns(birthday string); -- 使用 insert overwrite 实现只更新新增列（需要包含所有旧字段） insert overwrite table cid_info select id, cid, substr(cid,8,8) as birthday from cid_info; 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;计算实岁与虚岁：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;实岁：通过当前日期与出生日期相差天数除以365，保留一位小数。虚岁：当前年份减去出生年份。round 函数取整规则：round(45.836, -1) 对个位四舍五入，结果为 50.0。round(45.836, -2) 对十位四舍五入，结果为 0.0。round(95.836, -2) 对十位四舍五入，结果为 100.0。7.3 Hive 查询核心思路与限制查询语句结构：select → distinct → from → join/on → where → group by → order by →limitORC 表在开启事务后可执行 update 和 delete，而 TextFile 格式不支持行级更新，只能进行 insert 或 insert overwrite 覆盖。当需要对已有表增加列并填充数据时，不能直接 update 某一列，通常采用 alter table add columns 添加列，然后通过insert overwrite table 全量回填所有列的方式实现。八、JSON 数据处理（Lesson 15）解析 JSON 需借助外部 jar 包或内置函数 get_json_object。对于复杂嵌套类型（如 array<map<string,float>>），必须使用 JSON SerDe，并在建表时指定：create table student3(sid int, sname string, grade array<map<string,float>>) row format serde 'org.apache.hive.hcatalog.data.JsonSerDe';数据文件每行必须为标准 JSON，例如：{"sid":1,"sname":"zhangsan","grade":[{"语文":80},{"数学":90.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. ]