此分類用於記錄吳恩達深度學習課程的學習筆記。
課程相關信息鏈接如下:
- 原課程視頻鏈接:[雙語字幕]吳恩達深度學習deeplearning.ai
- github課程資料,含課件與筆記:吳恩達深度學習教學資料
- 課程配套練習(中英)與答案:吳恩達深度學習課後習題與答案
本篇為第二課的第三週內容,3.10的內容。
本週為第二課的第三週內容,你會發現這周的題目很長,實際上,作為第二課的最後一週內容,這一週是對基礎部分的最後補充。
在整個第一課和第二課部分,我們會了解到最基本的全連接神經網絡的基本結構和一個完整的模型訓練,驗證的各個部分。
之後幾課就會進行更多的實踐和進階內容介紹,從“通用”走向“特化”。
總的來説這周的難度不高,但也有需要理解的內容,我仍會在相對較難理解的部分增加更多基礎和例子,以及最後的“人話版”總結。
本篇的內容關於編程框架,之前本想在這篇寫完後面的Tensorflow結束本週內容,但想了一想,關於框架本身還有很多可以展開的地方。
因此,這篇在課程內容上拓展了很多計算機基礎,來簡單講解一下搭建框架的基本邏輯,是對後面引入成熟框架的一個簡單補充。
1. 什麼叫“框架”?
如果你的基礎不太好,我們先簡單解釋一下,什麼叫“框架”,它並沒有看起來那麼高大上,
用專業點的話來概括,它就是別人已經寫好的一整套代碼,你只需要調用,不用從零開始寫。
如果打個比方,它更像一個幫我們編程的工具箱。
但當我們可以用“框架”來形容一套代碼時,就已經表明這套代碼的完整性,成熟性以及延展性。
這部分我們就簡單看看搭建框架的基本邏輯。
1.1 類與對象
如果我們養寵物狗,那一般我們都會給它起一個名字,而一般的狗都會汪汪叫。
我們把這件事略微抽象化一點:
- 每一條寵物狗都有一個名字,這是它固有的東西,是它的屬性。
- 每一條寵物狗都會汪汪叫,但是它可以主動選擇是否進行這種行為,我們管這叫它的方法。
這樣,我們就定義了一個“類”——寵物狗:有名字,可以汪汪叫。
你會發現,在類裏,我們知道狗有名字,但並不知道它具體叫什麼,我們無法由類定位到具體某條狗。
因此,類只是一個“具體的概念”,就像一個模具,只規定形狀,我們並不知道模具裏到底是什麼。
現在,我真的養了一條狗,給它起名叫“小黑”——我填充了它的固有屬性。
那麼這個從寵物狗的概念到具體的狗的轉變,就是類和對象的關係。
“小黑”是“寵物狗類”下一個真實對象,我們也叫它實例。
現在,我可以讓小黑叫或者不叫,即實例化後自由選擇是否用實例調用類裏的方法。
於此同時,你會發現寵物狗屬於寵物的一種:
- 每個寵物都有自己的名字。
- 每個寵物都會移動。
- 但只有狗會“汪汪叫”
這時,“寵物” 這個類比 “寵物狗“這個類更廣,而我的狗”小黑“只要是寵物狗,就一定是寵物。
這種關係就像大模具裏的小模具,我們可以把共性放在大模具裏,在小模具裏只留下個性——小模具一定在大模具裏,大模具有的小模具都能用。
這在代碼裏就叫繼承。
再補充一點,一些類的某些屬性往往在定義時就已經固定了。就像某個型號的手機的尺寸大小,這時我們實例化時就不用在填充這些屬性。
這就是類和對象的邏輯,我們把上面的過程代碼化:
# -------------------------------
# 大模具:寵物(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()
現在,把這種邏輯推廣到深度學習領域,你腦海裏第一個出現的類是什麼?
我想的是模型結構,模型結構就像一個大盒子,我們可以主動選擇初始化模型成什麼樣子,定義類的傳播順序。
就像我們之前做的一樣:
# 這個類就是“神經網絡”的模具(模型結構)
# 我們在這裏規定它由哪些層組成、將按怎樣的順序傳播
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 模塊化
前面我們講“類”時,你可以把它理解成一個模具,但如果我們繼續往下想,就會遇到一個現實問題:如果一個模具太大,把所有東西全塞在一起,會不會難以維護?
答案當然是會的。
我們再做個生活比喻:
假如你要裝修一間房子,你不會把電線、水管、牆面、傢俱全部混在一起施工。相反,你會把房屋拆成不同的“模塊”:
- 水電是一個模塊
- 牆體結構是一個模塊
- 傢俱佈置又是一個模塊
這樣,每個模塊裏只解決一種具體問題,分工明確,這樣: - 需要改線路,只動水電模塊
- 需要換沙發,只改傢俱模塊
- 不會搞亂整個家
編程也是一樣的,我們也來實現一下這個邏輯:
# 模塊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()
加深理解,我們單獨展開一下這一句的結構:
my_house.electrical.switch_on()
my_house是實例化的房子類
我們可以通過my_house.調用房子類的屬性和方法,如:my_house.setup_house()electrical是房子類的一個屬性
但是self.electrical = Electrical()把這個屬性初始化為一個水電類的實例。
所以,我們可以用屬性調用類方法。my_house.electrical.switch_on()
你會發現,水電類不是房子類的父類,但是房子類卻可以通過屬性調用水電類的方法。
這種情況下,水電就成了房子的一個模塊,父模塊和子模塊不像繼承,它們沒有共性,更像是局部與整體。
根據這個邏輯,你現在已經知道了“類”是模具,那什麼是模塊?
顯然,模塊就是“裝着很多模具”的更大工具箱,其內部分工明確,責任分明。
比如我們前面的神經網絡代碼:
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 大型封裝
承接上面的邏輯,我們由小到大,介紹了類繼承和模塊化的概念。
要注意一點:
一般來説,我們會把最上層的功能劃分成模塊,但在實際的邏輯裏,模塊化並非一直在繼承之上。
就像這裏:
class NeuralNetwork(nn.Module):# 繼承
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()# 模塊化
……
你會發現,神經網絡類擁有自己的子模塊,但它本身也是一個子類。
類可能擁有子模塊, 但他本身也可能是大型模塊的一部分;子模塊類下甚至也可能有其他子類。
二者根據需求彼此包容,層層嵌套,這才是繼承和模塊化的關係。
而這種層層嵌套,不斷複用的邏輯就是封裝。
再看個例子:
- 打開水龍頭、關閉水龍頭都是一個步驟,我們分別用一行代碼來編寫。
- 拿杯子,用杯子接水,喝水也是一個步驟,我們分別用一行代碼來編寫。
- 現在我是一個代碼人,我要喝水。在我的邏輯裏:如果不封裝,那麼每次喝水都要複製這五行代碼。
- 於是我把這五行代碼封裝成一個函數。之後每次喝水,只調用這一個函數,實現代碼複用。
- 同理,我可以把洗臉,刷牙也各自封裝成一個函數來每天使用。
- 不僅可以封裝步驟,也可以封裝函數:我再把洗臉和刷牙封裝成洗漱。
- 把洗漱和喝水封裝為起牀······
用代碼來看就是:
# -------------------------------
# 基礎動作函數
# -------------------------------
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
你不需要知道它內部是幾十層什麼結構、多少個循環、什麼數學公式。
你只需要:
model = torchvision.models.resnet50(pretrained=True)
這句話就像:
“我買了一個已經裝好的廚房。”
聽起來很霸氣吧,這就是最終經過大型封裝的框架。
梳理一下:
類是模具,模塊是工具箱,封裝是組合流程,大型封裝就是成熟框架。
從小到大、從零件到流程,再到整套系統,這就是編程邏輯與框架設計的核心思路。
2.總結
| 概念 | 原理 | 比喻 |
|---|---|---|
| 類與對象 | 類是模具,定義屬性和方法;對象是類的實例,擁有具體屬性並可調用方法。繼承用於複用父類共性。 | 類是模具(寵物狗),對象是實例(小黑);大模具(寵物)裏套小模具(寵物狗)。 |
| 模塊化 | 模塊是裝着多個功能的工具箱,每個模塊只負責自己的功能,方便維護和複用。 | 房子裝修拆成水電、牆體、傢俱模塊;改動某個模塊不影響其他模塊。 |
| 封裝 | 把零散步驟組合成一個流程或函數,提高代碼複用性,可以層層嵌套。 | 起牀流程:洗臉、刷牙、喝水;每個動作是函數,組合成流程函數。 |
| 大型封裝 | 將模塊組合、封裝成成熟框架,用户無需關心內部實現即可直接使用。 | 買整套廚房,而不是自己從零組裝爐灶、管道、排風系統;深度學習框架如ResNet。 |