此分类用于记录吴恩达深度学习课程的学习笔记。
课程相关信息链接如下:
- 原课程视频链接:[双语字幕]吴恩达深度学习deeplearning.ai
- github课程资料,含课件与笔记:吴恩达深度学习教学资料
- 课程配套练习(中英)与答案:吴恩达深度学习课后习题与答案
本篇为第二课的第三周内容,3.10的内容。
本周为第二课的第三周内容,你会发现这周的题目很长,实际上,作为第二课的最后一周内容,这一周是对基础部分的最后补充。
在整个第一课和第二课部分,我们会了解到最基本的全连接神经网络的基本结构和一个完整的模型训练,验证的各个部分。
之后几课就会进行更多的实践和进阶内容介绍,从"通用"走向"特化"。
总的来说这周的难度不高,但也有需要理解的内容,我仍会在相对较难理解的部分增加更多基础和例子,以及最后的"人话版"总结。
本篇的内容关于编程框架 ,之前本想在这篇写完后面的Tensorflow结束本周内容,但想了一想,关于框架本身还有很多可以展开的地方。
因此,这篇在课程内容上拓展了很多计算机基础,来简单讲解一下搭建框架的基本逻辑,是对后面引入成熟框架的一个简单补充。
1. 什么叫"框架"?
如果你的基础不太好,我们先简单解释一下,什么叫"框架",它并没有看起来那么高大上,
用专业点的话来概括,它就是别人已经写好的一整套代码 ,你只需要调用,不用从零开始写。
如果打个比方,它更像一个帮我们编程的工具箱 。
但当我们可以用"框架"来形容一套代码时,就已经表明这套代码的完整性,成熟性以及延展性。
这部分我们就简单看看搭建框架的基本逻辑。
1.1 类与对象
如果我们养宠物狗,那一般我们都会给它起一个名字 ,而一般的狗都会汪汪叫 。
我们把这件事略微抽象化一点:
- 每一条宠物狗都有一个名字,这是它固有的东西 ,是它的属性。
- 每一条宠物狗都会汪汪叫,但是它可以主动选择是否进行这种行为 ,我们管这叫它的方法 。
这样,我们就定义了一个"类"------宠物狗:有名字,可以汪汪叫。
你会发现,在类里,我们知道狗有名字,但并不知道它具体叫什么,我们无法由类定位到具体某条狗。
因此,类只是一个"具体的概念",就像一个模具,只规定形状,我们并不知道模具里到底是什么。
现在,我真的养了一条狗,给它起名叫"小黑"------我填充了它的固有属性。
那么这个从宠物狗的概念到具体的狗的转变,就是类和对象的关系。
"小黑"是"宠物狗类"下一个真实对象 ,我们也叫它实例 。
现在,我可以让小黑叫或者不叫,即实例化后自由选择是否用实例调用类里的方法。
于此同时,你会发现宠物狗属于宠物的一种:
- 每个宠物都有自己的名字。
- 每个宠物都会移动。
- 但只有狗会"汪汪叫"
这时,"宠物" 这个类比 "宠物狗"这个类更广,而我的狗"小黑"只要是宠物狗,就一定是宠物 。
这种关系就像大模具里的小模具 ,我们可以把共性放在大模具里,在小模具里只留下个性------小模具一定在大模具里,大模具有的小模具都能用。
这在代码里就叫继承。
再补充一点,一些类的某些属性往往在定义时就已经固定了。就像某个型号的手机的尺寸大小,这时我们实例化时就不用在填充这些属性。
这就是类和对象的逻辑,我们把上面的过程代码化:
python
# -------------------------------
# 大模具:宠物(Pet)
# 说明:
# - 每个宠物都有"名字"这个固有属性
# - 每个宠物都会"移动"这个通用行为
# -------------------------------
class Pet:
#python中定义类属性的固定方法
def __init__(self, name):
# "名字"是所有宠物都天生具有的属性(固有属性)
self.name = name
def move(self):
# "移动"是所有宠物共有的方法
print(self.name, "在移动中...")
# -------------------------------
# 小模具:宠物狗(Dog)
# 说明:
# - 宠物狗是一种宠物,因此"继承"Pet
# - 宠物狗在拥有 Pet 的全部属性和方法外,
# 还额外具有"汪汪叫"这一专属能力
# -------------------------------
class Dog(Pet): # Pet 是大模具,Dog 是小模具
def __init__(self, name):
# 使用 super() 调用父类的构造方法,继承"名字"属性
super().__init__(name)
def bark(self):
# "汪汪叫"是狗独有的方法
print(self.name, "在汪汪叫!")
# -------------------------------
# 从模具中创建具体对象(实例)
# "小黑""大黄"是 Dog 模具的真实物体
# -------------------------------
dog1 = Dog("小黑")
dog2 = Dog("大黄")
# 调用从父类继承的"移动"方法
dog1.move()
dog2.move()
# 调用狗自己独有的"汪汪叫"方法
dog1.bark()
dog2.bark()
现在,把这种逻辑推广到深度学习领域,你脑海里第一个出现的类是什么?
我想的是模型结构 ,模型结构就像一个大盒子,我们可以主动选择初始化模型成什么样子,定义类的传播顺序。
就像我们之前做的一样:
python
# 这个类就是"神经网络"的模具(模型结构)
# 我们在这里规定它由哪些层组成、将按怎样的顺序传播
class NeuralNetwork(nn.Module): # 继承自 nn.Module ------ 大模具里套小模具
def __init__(self):
# super() 表示先初始化大模具 nn.Module,让我们的模型具有 PyTorch 模型的基本能力
super().__init__()
# 各个个性化零件
self.flatten = nn.Flatten()
self.hidden1 = nn.Linear(128 * 128 * 3, 1024)
self.hidden2 = nn.Linear(1024, 512)
self.hidden3 = nn.Linear(512, 128)
self.hidden4 = nn.Linear(128, 32)
self.hidden5 = nn.Linear(32, 8)
self.hidden6 = nn.Linear(8, 3)
self.relu = nn.ReLU()
self.output = nn.Linear(3, 1)
self.sigmoid = nn.Sigmoid()
init.xavier_uniform_(self.output.weight)
# forward 就相当于这个模型的"方法"
def forward(self, x):
x = self.flatten(x)
x = self.relu(self.hidden1(x))
x = self.relu(self.hidden2(x))
x = self.relu(self.hidden3(x))
x = self.relu(self.hidden4(x))
x = self.relu(self.hidden5(x))
x = self.relu(self.hidden6(x))
x = self.sigmoid(self.output(x))
return x
model = NeuralNetwork() # model 就像"小黑"一样,是 NeuralNetwork 的真实对象
1.2 模块化
前面我们讲"类"时,你可以把它理解成一个模具,但如果我们继续往下想,就会遇到一个现实问题:如果一个模具太大,把所有东西全塞在一起,会不会难以维护?
答案当然是会的。
我们再做个生活比喻:
假如你要装修一间房子,你不会把电线、水管、墙面、家具全部混在一起施工。相反,你会把房屋拆成不同的"模块":
- 水电是一个模块
- 墙体结构是一个模块
- 家具布置又是一个模块
这样,每个模块里只解决一种具体问题,分工明确,这样: - 需要改线路,只动水电模块
- 需要换沙发,只改家具模块
- 不会搞乱整个家
编程也是一样的,我们也来实现一下这个逻辑:
python
# 模块1.水电模块
class Electrical:
def __init__(self):
self.lights_on = False
def switch_on(self):
self.lights_on = True
print("灯已打开,电流正常。")
def switch_off(self):
self.lights_on = False
print("灯已关闭。")
# 模块2.墙体模块
class Walls:
def __init__(self):
self.paint_color = "白色"
def paint(self, color):
self.paint_color = color
print(f"墙面已刷成 {color} 色。")
# 模块3.家具模块
class Furniture:
def __init__(self):
self.has_sofa = False
self.has_table = False
def add_sofa(self):
self.has_sofa = True
print("沙发已摆好。")
def add_table(self):
self.has_table = True
print("餐桌已摆好。")
# -------------------------------
# 房子类 ------ 把3个模块组合成完整房子
# -------------------------------
class House:
def __init__(self):
# 房子里固定三个功能模块:水电、墙体、家具
# 模块就是房子的"固有属性"
self.electrical = Electrical()
self.walls = Walls()
self.furniture = Furniture()
# 个性方法:整合操作
def setup_house(self):
# 模块化操作,每个模块只做自己的事
self.electrical.switch_on()
self.walls.paint("浅蓝色")
self.furniture.add_sofa()
self.furniture.add_table()
# -------------------------------
# 实例化房子对象
# -------------------------------
my_house = House()
# 单独调整水电模块
my_house.electrical.switch_on()
# 单独换墙颜色
my_house.walls.paint("浅灰色")
# 单独布置家具
my_house.furniture.add_sofa()
# 一次性完成全部装修
my_house.setup_house()
加深理解,我们单独展开一下这一句的结构:
python
my_house.electrical.switch_on()
my_house是实例化的房子类
我们可以通过my_house.调用房子类的属性和方法,如:my_house.setup_house()electrical是房子类的一个属性
但是self.electrical = Electrical()把这个属性初始化为一个水电类的实例。
所以,我们可以用属性调用类方法。my_house.electrical.switch_on()
你会发现,水电类不是房子类的父类,但是房子类却可以通过属性调用水电类的方法。
这种情况下,水电就成了房子的一个模块,父模块和子模块不像继承,它们没有共性,更像是局部与整体。
根据这个逻辑,你现在已经知道了"类"是模具,那什么是模块?
显然,模块就是"装着很多模具"的更大工具箱,其内部分工明确,责任分明。
比如我们前面的神经网络代码:
python
self.flatten = nn.Flatten()
self.hidden1 = nn.Linear(128 * 128 * 3, 1024)
self.hidden2 = nn.Linear(1024, 512)
...
self.sigmoid = nn.Sigmoid()
你会发现:
Flatten是一个工具箱Linear是另一个工具箱ReLU是第三个工具箱- ......
这就是模块化。
1.3 大型封装
承接上面的逻辑,我们由小到大,介绍了类继承和模块化的概念。
要注意一点:
一般来说,我们会把最上层的功能划分成模块,但在实际的逻辑里,模块化并非一直在继承之上 。
就像这里:
python
class NeuralNetwork(nn.Module):# 继承
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()# 模块化
......
你会发现,神经网络类拥有自己的子模块,但它本身也是一个子类。
类可能拥有子模块, 但他本身也可能是大型模块的一部分;子模块类下甚至也可能有其他子类。
二者根据需求彼此包容,层层嵌套,这才是继承和模块化的关系。
而这种层层嵌套,不断复用的逻辑就是封装。
再看个例子:
- 打开水龙头、关闭水龙头都是一个步骤,我们分别用一行代码来编写。
- 拿杯子,用杯子接水,喝水也是一个步骤,我们分别用一行代码来编写。
- 现在我是一个代码人,我要喝水。在我的逻辑里:如果不封装,那么每次喝水都要复制这五行代码。
- 于是我把这五行代码封装成一个函数。之后每次喝水,只调用这一个函数,实现代码复用。
- 同理,我可以把洗脸,刷牙也各自封装成一个函数来每天使用。
- 不仅可以封装步骤,也可以封装函数:我再把洗脸和刷牙封装成洗漱。
- 把洗漱和喝水封装为起床······
用代码来看就是:
python
# -------------------------------
# 基础动作函数
# -------------------------------
def turn_on_tap():
print("打开水龙头")
def turn_off_tap():
print("关闭水龙头")
def take_cup():
print("拿起杯子")
def pour_water():
print("用杯子接水")
def drink_water():
print("喝水")
def wash_face():
print("洗脸")
def brush_teeth():
print("刷牙")
# -------------------------------
# 封装喝水流程
# -------------------------------
def have_water():
turn_on_tap()
take_cup()
pour_water()
drink_water()
turn_off_tap()
# -------------------------------
# 封装洗漱流程
# -------------------------------
def wash_up():
wash_face()
brush_teeth()
# -------------------------------
# 封装起床流程
# -------------------------------
def morning_routine():
wash_up() # 洗漱
have_water() # 喝水
# ------------------------------
# 执行起床流程
# -------------------------------
morning_routine()
因此,继续按之前的例子来说,那大型封装就是:
统合所有工具箱,组合大型机器。
最后展开一下:
就像我们生活中会买"整套厨房",而不是一个人从零搭建炉灶、管道、排风系统。
深度学习里的框架做的事情,就是把无数模块组合起来,再封装成一个"大型机器"。
例如:
- 数据加载器
DataLoader - 自动求导系统
autograd - 优化器
optim.Adam - 卷积网络
torchvision.models.resnet50 - Transformer 编码器
nn.TransformerEncoder
你不需要知道它内部是几十层什么结构、多少个循环、什么数学公式 。
你只需要:
python
model = torchvision.models.resnet50(pretrained=True)
这句话就像:
"我买了一个已经装好的厨房。"
听起来很霸气吧,这就是最终经过大型封装的框架。
梳理一下:
类是模具,模块是工具箱,封装是组合流程,大型封装就是成熟框架。
从小到大、从零件到流程,再到整套系统,这就是编程逻辑与框架设计的核心思路。
2.总结
| 概念 | 原理 | 比喻 |
|---|---|---|
| 类与对象 | 类是模具,定义属性和方法;对象是类的实例,拥有具体属性并可调用方法。继承用于复用父类共性。 | 类是模具(宠物狗),对象是实例(小黑);大模具(宠物)里套小模具(宠物狗)。 |
| 模块化 | 模块是装着多个功能的工具箱,每个模块只负责自己的功能,方便维护和复用。 | 房子装修拆成水电、墙体、家具模块;改动某个模块不影响其他模块。 |
| 封装 | 把零散步骤组合成一个流程或函数,提高代码复用性,可以层层嵌套。 | 起床流程:洗脸、刷牙、喝水;每个动作是函数,组合成流程函数。 |
| 大型封装 | 将模块组合、封装成成熟框架,用户无需关心内部实现即可直接使用。 | 买整套厨房,而不是自己从零组装炉灶、管道、排风系统;深度学习框架如ResNet。 |