摘要
本周在深度学习方面,学习了权重衰退、丢弃法、数值稳定性等方面的相关知识,主要是去实现了一个关于Kaggle房价预测实例的模型,使用了之前所有学习到深度学习的各种方法,如何对数据集预处理、如何写激活函数、对模型进行训练、以及使用K折交叉验证来检验模型等操作。在OpenCV方面,学习了Canny边缘检测、图像金字塔的图像处理操作。
This week, I’ve been learning about various aspects of deep learning, such as weight decay, dropout, and numerical stability. I mainly focused on implementing a model for a Kaggle house price prediction example. I applied various deep learning techniques I’ve learned, including data preprocessing, activation function implementation, model training, and using K-fold cross-validation to evaluate the model.In the context of OpenCV, I delved into image processing techniques like Canny edge detection and image pyramid operations.
深度学习
训练误差和泛化误差的区别
验证数据集:一个用来评估模型好坏的数据集,验证数据上的精度还不是最能反映模型真实精度的,因为有可能调超参也是从验证数据集上调的。
测试数据集 :只用一次的数据集
K折交叉验证,初始采样分割成K个子样本,一个单独的子样本被保留作为验证模型的数据,其他K-1个样本用来训练。交叉验证重复K次,每个子样本验证一次,平均K次的结果,最终得到一个单一估测。「一般 k 为 5或者10」. 损失为 这 k 个的损失之和求平均值。
就是首先要保证泛化误差最小,然后在一定程度上缩小训练误差,可能会承受一下训练误差增大。
VC维
平面上vc维就是3,因为三个点时,出现的情况都可以分。
支持N维输入的感知机的VC维是N+1,一些多层感知机的VC维O(Nlog2N)
数据集只包含三个点时永远是线性可分的,即假设空间H能够shatter数据集,但当数据集包含四个点时则存在线性不可分的情况(xor)
模型容量需要匹配数据复杂度,否则可能导致欠拟合和过拟合,统计机器学习提供数学工具来衡量模型复杂度,实际中一般靠观察训练误差和验证误差。(模拟考和高考形容)
权重衰退
把模型容量控制比较小有两种方法,方法一:模型控制的比较小,使得模型中参数比较少。方法二:控制参数选择范围来控制参数容量。
③ 如下图所示,w向量中每一个元素的值都小于θ的根号。
④ 约束就是正则项。每个特征的权重都大会导致模型复杂,从而导致过拟合。控制权重矩阵范数可以使得减少一些特征的权重,甚至使他们权重为0,从而导致模型简单,减轻过拟合。
拉格朗日乘子法原本是用于解决约束条件下的多元函数极值问题。举例,求f(x,y)的最小值,但是有约束C(x,y) = 0。乘子法给的一般思路是,构造一个新的函数g(x,y,λ) = f(x,y) +λC(x,y),当同时满足g’x = g’y = 0时,函数取到最小值。这件结论的几何含义是,当f(x,y)与C(x,y)的等高线相切时,取到最小值。
⑦ 具体到机器学习这里,C(x,y) = w^2 -θ。所以视频中的黄色圆圈,代表不同θ下的约束条件。θ越小,则最终的parameter离原点越近。
① 绿色的线就是原始损失函数l的等高线,优化原始损失l的最优解(波浪号即最优解)在中心位置。
② 当原始损失加入二分之λ的项后,这个项是一个二次项,假如w就两个值,x1(横轴)、x2(纵轴),那么在图上这个二次项的损失以原点为中心的等高线为橙色的图所示。所以合并后的损失为绿色的和黄色的线加一起的损失。
③ 当加上损失项后,可以知道原来最优解对应的二次项的损失特别大,因此原来的最优解不是加上二次项后的公式的最优解了。若沿着橙色的方向走,原有l损失值会大一些,但是二次项罚的损失会变小,当拉到平衡点以内时,惩罚项减少的值不足以原有l损失增大的值,这样w * 就是加惩罚项后的最优解。
④ 损失函数加上正则项成为目标函数,目标函数最优解不是损失函数最优解。正则项就是防止达到损失函数最优导致过拟合,把损失函数最优点往外拉一拉。鼓励权重分散,将所有额特征运用起来,而不是依赖其中的少数特征,并且权重分散的话它的内积就小一些。
⑤ l2正则项会对大数值的权值进行惩罚
丢弃法
一层神经元到下一层时,随机舍去一些神经元,但是要保证总的数学期望不变。
正则项、dropout只在训练中使用,推理中不使用。
mask = (torch.randn(X.shape)>dropout).float()
这个代码写的确实秒,一句话,就让mask里变成随机0,1的值了
return mask * X / (1.0 - dropout) 然后正好和x相乘,把其中一些元素舍去了
在深度学习中,可以把隐藏层设大一些,然后再用dropout、正则化、缩小模型。
数值稳定性
往往在神经网络中,存在着梯度爆炸和梯度消失的数据问题。
产生这两个情况的原因是:
在反向传播的时候,我们做了太多的矩阵乘法:
∂
ℓ
∂
W
t
=
∂
ℓ
∂
h
d
∂
h
d
∂
h
d
−
1
…
∂
h
t
+
1
∂
h
t
∂
h
t
∂
W
t
\frac{\partial \ell}{\partial \mathbf{W}^t}=\frac{\partial \ell}{\partial \mathbf{h}^d} \frac{\partial \mathbf{h}^d}{\partial \mathbf{h}^{d-1}} \ldots \frac{\partial \mathbf{h}^{t+1}}{\partial \mathbf{h}^t} \frac{\partial \mathbf{h}^t}{\partial \mathbf{W}^t}
∂Wt∂ℓ=∂hd∂ℓ∂hd−1∂hd…∂ht∂ht+1∂Wt∂ht
因为对
σ
(
x
)
\sigma(x)
σ(x)求导后的结果就是1或0,因此在diag()里面就是一堆0、1元素,与w转置相乘后,就是那些与1相乘的元素的和,再做连乘,那如果这个模型成数比较大,最后的值就可能会很大。
⋅
∏
i
=
t
d
−
1
∂
h
i
+
1
∂
h
i
=
∏
i
=
t
d
−
1
diag
(
σ
′
(
W
i
h
i
−
1
)
)
(
W
i
)
T
\cdot \prod_{i=t}^{d-1} \frac{\partial \mathbf{h}^{i+1}}{\partial \mathbf{h}^i}=\prod_{i=t}^{d-1} \operatorname{diag}\left(\sigma^{\prime}\left(\mathbf{W}^i \mathbf{h}^{i-1}\right)\right)\left(W^i\right)^T
⋅i=t∏d−1∂hi∂hi+1=i=t∏d−1diag(σ′(Wihi−1))(Wi)T
如果激活函数是sigmoid函数的话,最后的梯度是d-t个小数值的乘积
如何使梯度值在合理的范围内?
1)乘法变加法
2)梯度归一化、梯度裁剪
合理的权重初始值和激活函数的选取可以提升数值稳定性。
让我们看看某些没有非线性的全连接层输出(例如,隐藏变量)
o
i
o_{i}
oi的尺度分布。
对于该层
n
i
n
n_\mathrm{in}
nin输入
x
j
x_j
xj及其相关权重
w
i
j
w_{ij}
wij,输出由下式给出
o i = ∑ j = 1 n i n w i j x j . o_{i} = \sum_{j=1}^{n_\mathrm{in}} w_{ij} x_j. oi=j=1∑ninwijxj.
权重
w
i
j
w_{ij}
wij都是从同一分布中独立抽取的。
此外,让我们假设该分布具有零均值和方差
σ
2
\sigma^2
σ2。
请注意,这并不意味着分布必须是高斯的,只是均值和方差需要存在。
现在,让我们假设层
x
j
x_j
xj的输入也具有零均值和方差
γ
2
\gamma^2
γ2,
并且它们独立于
w
i
j
w_{ij}
wij并且彼此独立。
则得到以下公式:
E
[
h
i
t
]
=
E
[
∑
j
w
i
,
j
t
h
j
t
−
1
]
=
∑
j
E
[
w
i
,
j
t
]
E
[
h
j
t
−
1
]
=
0
\mathbb{E}\left[h_i^t\right]=\mathbb{E}\left[\sum_j w_{i, j}^t h_j^{t-1}\right]=\sum_j \mathbb{E}\left[w_{i, j}^t\right] \mathbb{E}\left[h_j^{t-1}\right]=0
E[hit]=E[j∑wi,jthjt−1]=j∑E[wi,jt]E[hjt−1]=0
对于离散性随机变量hi均值:平方的均值-均值的平方
D(X)=E(X2)-E(X)2
而E(hi)也是等于0的,于是这个式子可以化简。
最后推的
n
t
−
1
γ
t
=
1
n_{t-1} \gamma_t=1
nt−1γt=1
由上述公式推的,需要同时满足
n
t
−
1
γ
t
=
1
n_{t-1} \gamma_t=1
nt−1γt=1
n
t
γ
t
=
1
n_{t} \gamma_t=1
ntγt=1,但在实际情况中,难以同时满足这两个式子,因为不能保证输入维度正好等于输出维度,于是采取折中的方式:Xavier初始化。
这个方法见下图:
在同样输入值的均值为0的情况下,讨论假设线性的激活函数:
有如下推到:
让我们看看某些没有非线性的全连接层输出(例如,隐藏变量)
o
i
o_{i}
oi的尺度分布。
对于该层
n
i
n
n_\mathrm{in}
nin输入
x
j
x_j
xj及其相关权重
w
i
j
w_{ij}
wij,输出由下式给出
o i = ∑ j = 1 n i n w i j x j . o_{i} = \sum_{j=1}^{n_\mathrm{in}} w_{ij} x_j. oi=j=1∑ninwijxj.
权重
w
i
j
w_{ij}
wij都是从同一分布中独立抽取的。
此外,让我们假设该分布具有零均值和方差
σ
2
\sigma^2
σ2。
请注意,这并不意味着分布必须是高斯的,只是均值和方差需要存在。
现在,让我们假设层
x
j
x_j
xj的输入也具有零均值和方差
γ
2
\gamma^2
γ2,
并且它们独立于
w
i
j
w_{ij}
wij并且彼此独立。
同理在上有假设的前提下,继续对激活函数进行一个假设线性的条件:
σ
(
x
)
=
α
x
+
β
h
′
=
W
t
h
t
−
1
and
h
t
=
σ
(
h
′
)
E
[
h
i
t
]
=
E
[
α
h
i
′
+
β
]
=
β
⟹
β
=
0
Var
[
h
i
t
]
=
E
[
(
h
i
t
)
2
]
−
E
[
h
i
t
]
2
=
E
[
(
α
h
i
′
+
β
)
2
]
−
β
2
=
E
[
α
2
(
h
i
′
)
2
+
2
α
β
h
i
′
+
β
2
]
−
β
2
α
2
Var
[
h
i
′
]
\begin{aligned} \sigma(x)= & \alpha x+\beta \\ & \mathbf{h}^{\prime}=\mathbf{W}^t \mathbf{h}^{t-1} \quad \text { and } \quad \mathbf{h}^t=\sigma\left(\mathbf{h}^{\prime}\right) \\ \mathbb{E}\left[h_i^t\right]= & \mathbb{E}\left[\alpha h_i^{\prime}+\beta\right]=\beta \quad \Longrightarrow \quad \beta=0 \\ \operatorname{Var}\left[h_i^t\right]= & \mathbb{E}\left[\left(h_i^t\right)^2\right]-\mathbb{E}\left[h_i^t\right]^2 \\ = & \mathbb{E}\left[\left(\alpha h_i^{\prime}+\beta\right)^2\right]-\beta^2 \\ = & \mathbb{E}\left[\alpha^2\left(h_i^{\prime}\right)^2+2 \alpha \beta h_i^{\prime}+\beta^2\right]-\beta^2 \\ & \alpha^2 \operatorname{Var}\left[h_i^{\prime}\right] \end{aligned}
σ(x)=E[hit]=Var[hit]===αx+βh′=Wtht−1 and ht=σ(h′)E[αhi′+β]=β⟹β=0E[(hit)2]−E[hit]2E[(αhi′+β)2]−β2E[α2(hi′)2+2αβhi′+β2]−β2α2Var[hi′]
由此可知,如果不希望激活函数改变输入值和输出值的方差的话,那么就经可能希望激活函数为f(x)=x。
检查常用的激活函数
由图像可知,需要对sigmoid进行调整,使其在一定条件下,近似sigmoid(x)≈x。
Kaggle房价预测实例
深度学习中的优化器
随机梯度下降(Stochastic Gradient Descent,SGD)是一种常见的优化器,也是深度学习模型中最基础的优化算法之一。它是对梯度下降算法的一种实现方式,常被用于神经网络中的权重更新。
SGD的基本思路是在每个训练样本上计算梯度并更新权重,因此也被称为在线学习。相比于批量梯度下降(Batch Gradient Descent, BGD),SGD更加高效,尤其是当数据集较大时。
在SGD中,模型的参数向负梯度方向更新,使得损失函数的值逐渐减少。具体来说,每个训练样本的误差对每个参数的偏导数被计算,并且应用于参数的当前值以更新它。在迭代过程中,每次更新后,下一个样本的误差被计算,参数再次更新。这个过程重复多次,直到达到一定的收敛条件或达到事先设定的最大迭代次数。
这段代码是声明了数据下载的相关三个函数,实现了从网站下载对应数据集并解压保存在本地路径的功能。
import hashlib
import os
import tarfile
import zipfile
import requests
import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l
#@save
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'
def download(name, cache_dir=os.path.join('..', 'data')): #@save
"""下载一个DATA_HUB中的文件,返回本地文件名"""
assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}"
url, sha1_hash = DATA_HUB[name]
os.makedirs(cache_dir, exist_ok=True)
fname = os.path.join(cache_dir, url.split('/')[-1])
if os.path.exists(fname):
sha1 = hashlib.sha1()
with open(fname, 'rb') as f:
while True:
data = f.read(1048576)
if not data:
break
sha1.update(data)
if sha1.hexdigest() == sha1_hash:
return fname # 命中缓存
print(f'正在从{url}下载{fname}...')
r = requests.get(url, stream=True, verify=True)
with open(fname, 'wb') as f:
f.write(r.content)
return fname
def download_extract(name, folder=None): #@save
"""下载并解压zip/tar文件"""
fname = download(name)
base_dir = os.path.dirname(fname)
data_dir, ext = os.path.splitext(fname)
if ext == '.zip':
fp = zipfile.ZipFile(fname, 'r')
elif ext in ('.tar', '.gz'):
fp = tarfile.open(fname, 'r')
else:
assert False, '只有zip/tar文件可以被解压缩'
fp.extractall(base_dir)
return os.path.join(base_dir, folder) if folder else data_dir
def download_all(): #@save
"""下载DATA_HUB中的所有文件"""
for name in DATA_HUB:
download(name)
实现数据预处理操作,先删除数据集中第一列特征,因为是ID,然后将所有缺失的值替换成相应特征的平均值,通过将特征重新缩放到零均值和单位方差来标准化数据。对于离散值的列,使用独热编码的方式来处理,因此数据集的维度大大增加了。
DATA_HUB['kaggle_house_train'] = (DATA_URL + 'kaggle_house_pred_train.csv','585e9cc9370b9160e7921475fbcd7d31219ce')
DATA_HUB['kaggle_house_test'] = (DATA_URL + 'kaggle_house_pred_test.csv', 'fal9780a7b011d9b009e8bff8e99922a8ee2eb90')
train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))
print(train_data.shape) # 1460个样本,80个te特征,1个标号label
print(test_data.shape) # 测试样本没有标号label
print(train_data.iloc[0:4,[0,1,2,3,-3,-2,-1]]) # 前面四行的某些列特征
# 在每个样本中,第一个特征是ID,将其从数据集中删除
all_features = pd.concat((train_data.iloc[:,1:-1],test_data.iloc[:,1:])) # 从第2列开始,到倒数第2列,然后再纵向合并。
print(all_features.iloc[0:4,[0,1,2,3,-3,-2,-1]])
# 将所有缺失的值替换成相应特征的平均值
# 通过将特征重新缩放到零均值和单位方差来标准化数据
print(all_features.dtypes) # 可以知道每一列分别为什么类型特征
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index # 当值的类型不是object的话,就是一个数值
print(numeric_features)
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std())) # 对数值数据变为总体为均值为0,方差为1的分布的数据
all_features[numeric_features] = all_features[numeric_features].fillna(0) # 将数值数据中not number的数据用0填充
# 处理离散值。用一次独热编码替换它们
# 若一列里面有五个不同的值,则创建五个features,如果该列中为该feature则为1,不为该feature则为0
all_features = pd.get_dummies(all_features,dummy_na=True) #对于字符串的列,把他们拆开,形成新的维度,并用独热编码来赋值。因此这个数据集维度就变的非常大了。
all_features.shape
# 从pandas格式中提取Numpy格式,并将其转换为张量表示
print(train_data.shape)
n_train = train_data.shape[0] # 样本个数
train_features = torch.tensor(all_features[:n_train].values,
dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].values,
dtype=torch.float32)
# train_data的SalePrice列是label值
train_labels = torch.tensor(train_data.SalePrice.values.reshape(-1,1),
dtype=torch.float32)
训练
loss = nn.MSELoss()
print(train_features.shape[1]) # 所有特征个数
in_features = train_features.shape[1]
def get_net():
net = nn.Sequential(nn.Linear(in_features,1)) # 单层线性回归
return net
def log_rmse(net, features, labels):
clipped_preds = torch.clamp(net(features),1,float('inf')) # 把模型输出的值限制在1和inf之间,inf代表无穷大(infinity的缩写)
rmse = torch.sqrt(loss(torch.log(clipped_preds),torch.log(labels))) # 预测做log,label做log,然后丢到MSE损失函数里
return rmse.item()
def train(net, train_features, train_labels, test_features, test_labels,
num_epochs, learning_rate, weight_decay, batch_size):
train_ls, test_ls = [], []
train_iter = d2l.load_array((train_features, train_labels), batch_size)
optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate, weight_decay=weight_decay) #权重衰退的超参数
for epoch in range(num_epochs):
for X, y in train_iter:
optimizer.zero_grad() #清零参数的梯度
l = loss(net(X),y)
l.backward()
optimizer.step() #优化器对参数进行更新
train_ls.append(log_rmse(net,train_features,train_labels)) #添加均方根误差实例
if test_labels is not None:
test_ls.append(log_rmse(net, test_features, test_labels))
return train_ls, test_ls
使用K折交叉验证的方式来对权重进行优化
先定义一个将数据集处理为交叉验证集的函数:
def get_k_fold_data(k,i,X,y): # 给定k折,给定第几折,返回相应的训练集、测试集
assert k > 1
fold_size = X.shape[0] // k # 每一折的大小为样本数除以k
X_train, y_train = None, None
for j in range(k): # 每一折
idx = slice(j * fold_size, (j+1)*fold_size) # 每一折的切片索引间隔
X_part, y_part = X[idx,:], y[idx] # 把每一折对应部分取出来
if j == i: # i表示第几折,把它作为验证集
X_valid, y_valid = X_part, y_part
elif X_train is None: # 第一次看到X_train,则把它存起来
X_train, y_train = X_part, y_part
else: # 后面再看到,除了第i外,其余折也作为训练数据集,用torch.cat将原先的合并
X_train = torch.cat([X_train, X_part],0)
y_train = torch.cat([y_train, y_part],0)
return X_train, y_train, X_valid, y_valid # 返回训练集和验证集
返回训练和验证误差的平均值
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay, batch_size):
train_l_sum, valid_l_sum = 0, 0
for i in range(k):
data = get_k_fold_data(k, i, X_train, y_train) # 把第i折对应分开的数据集、验证集拿出来
net = get_net()
# *是解码,变成前面返回的四个数据
train_ls, valid_ls = train(net, *data, num_epochs, learning_rate, weight_decay, batch_size) # 训练集、验证集丢进train函数
train_l_sum += train_ls[-1]
valid_l_sum += valid_ls[-1]
if i == 0:
d2l.plot(list(range(1, num_epochs + 1)), [train_ls, valid_ls],
xlabel='epoch', ylabel='rmse', xlim=[1, num_epochs],
legend=['train', 'valid'], yscale='log')
print(f'fold{i + 1},train log rmse {float(train_ls[-1]):f},'
f'valid log rmse {float(valid_ls[-1]):f}')
return train_l_sum / k, valid_l_sum / k # 求和做平均
最后是模型选择
选择了一组未调优的超参数,看其根据K折交叉验证测试的效果。
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr, weight_decay, batch_size)
print(f'{k}-折验证:平均训练log rmse:{float(train_l):f},'f'平均验证log rmse:{float(valid_l):f}')
输出训练和验证的具体图像,对比其差异。
最后输出的结果,随着迭代次数增大,log rmse误差越来越小。并且将生成submission.csv的文件
def train_and_pred(train_features, test_feature, train_labels, test_data, num_epochs, lr, weight_decay, batch_size):
net = get_net()
train_ls, _ = train(net, train_features, train_labels, None, None, num_epochs, lr, weight_decay, batch_size)
d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',
ylabel='log rmse', xlim=[1, num_epochs], yscale='log')
print(f'train log rmse {float(train_ls[-1]):f}')
preds = net(test_features).detach().numpy()
test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
submission.to_csv('submission.cvs', index=False)
train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size)
神经网络基础
nn.Sequential 定义了一种特殊的Module
# 回顾一下多层感知机
import torch
from torch import nn
from torch.nn import functional as F
net = nn.Sequential(nn.Linear(20,256),nn.ReLU(),nn.Linear(256,10))
X = torch.rand(2,20)
net(X)
自定义块
class MLP(nn.Module):
def __init__(self):
super().__init__() # 调用父类的__init__函数
self.hidden = nn.Linear(20,256)
self.out = nn.Linear(256,10)
def forward(self, X):
return self.out(F.relu(self.hidden(X)))
# 实例化多层感知机的层,然后在每次调用正向传播函数调用这些层
net = MLP()
X = torch.rand(2,20)
net(X)
顺序块
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for block in args:
self._modules[block] = block # block 本身作为它的key,存在_modules里面的为层,以字典的形式
def forward(self, X):
for block in self._modules.values():
print(block)
X = block(X)
return X
net = MySequential(nn.Linear(20,256),nn.ReLU(),nn.Linear(256,10))
X = torch.rand(2,20)
net(X)
正向传播
# 在正向传播函数中执行代码
class FixedHiddenMLP(nn.Module):
def __init__(self):
super().__init__()
self.rand_weight = torch.rand((20,20),requires_grad=False)
self.linear = nn.Linear(20,20)
def forward(self, X):
X = self.linear(X)
X = F.relu(torch.mm(X, self.rand_weight + 1))
X = self.linear(X)
while X.abs().sum() > 1:
X /= 2
return X.sum()
net = FixedHiddenMLP()
X = torch.rand(2,20)
net(X)
混合组合块
# 混合代培各种组合块的方法
class NestMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(20,64),nn.ReLU(),
nn.Linear(64,32),nn.ReLU())
self.linear = nn.Linear(32,16)
def forward(self, X):
return self.linear(self.net(X))
chimear = nn.Sequential(NestMLP(),nn.Linear(16,20),FixedHiddenMLP())
X = torch.rand(2,20)
chimear(X)
参数管理
# 首先关注具有单隐藏层的多层感知机
import torch
from torch import nn
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))
X = torch.rand(size=(2,4))
print(net(X))
print(net[2].state_dict()) # 访问参数,net[2]就是最后一个输出层
print(type(net[2].bias)) # 目标参数
print(net[2].bias)
print(net[2].bias.data)
print(net[2].weight.grad == None) # 还没进行反向计算,所以grad为None
print(*[(name, param.shape) for name, param in net[0].named_parameters()]) # 一次性访问所有参数
print(*[(name, param.shape) for name, param in net.named_parameters()]) # 0是第一层名字,1是ReLU,它没有参数
print(net.state_dict()['2.bias'].data) # 通过名字获取参数
嵌套块
# 从嵌套块收集参数
def block1():
return nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,4),nn.ReLU())
def block2():
net = nn.Sequential()
for i in range(4):
net.add_module(f'block{i}',block1()) # f'block{i}' 可以传一个字符串名字过来,block2可以嵌套四个block1
return net
rgnet = nn.Sequential(block2(), nn.Linear(4,1))
print(rgnet(X))
print(rgnet)
内置初始化
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))
def init_normal(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01) # 下划线表示把m.weight的值替换掉
nn.init.zeros_(m.bias)
net.apply(init_normal) # 会递归调用 直到所有层都初始化
print(net[0].weight.data[0])
print(net[0].bias.data[0])
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight,1)
nn.init.zeros_(m.bias)
net.apply(init_constant)
print(net[0].weight.data[0])
print(net[0].bias.data[0])
# 对某些块应用不同的初始化
def xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net[0].apply(xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
参数替换
# 自定义初始化
def my_init(m):
if type(m) == nn.Linear:
print("Init",*[(name, param.shape) for name, param in m.named_parameters()][0]) # 打印名字是啥,形状是啥
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5 # 这里*=的代码相当于先计算一个布尔矩阵(先判断>=),然后再用布尔矩阵的对应元素去乘以原始矩阵的每个元素。保留绝对值大于5的权重,不是的话就设为0
net.apply(my_init)
print(net[0].weight[:2])
net[0].weight.data[:] += 1 # 参数替换
net[0].weight.data[0,0] = 42
print(net[0].weight.data[0])
参数绑定
# 参数绑定
shared = nn.Linear(8,8)
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),shared,nn.ReLU(),shared,nn.ReLU(),nn.Linear(8,1)) # 第2个隐藏层和第3个隐藏层是share权重的,第一个和第四个是自己的
net(X)
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0,0] = 100
print(net[2].weight.data[0] == net[4].weight.data[0])
构造一个没有任何参数的自定义层
# 构造一个没有任何参数的自定义层
import torch
import torch.nn.functional as F
from torch import nn
class CenteredLayer(nn.Module):
def __init__(self):
super().__init__()
def forward(self, X):
return X - X.mean()
layer = CenteredLayer()
print(layer(torch.FloatTensor([1,2,3,4,5])))
# 将层作为组件合并到构建更复杂的模型中
net = nn.Sequential(nn.Linear(8,128),CenteredLayer())
Y = net(torch.rand(4,8))
print(Y.mean())
# 带参数的图层
class MyLinear(nn.Module):
def __init__(self, in_units, units):
super().__init__()
self.weight = nn.Parameter(torch.randn(in_units,units)) # nn.Parameter使得这些参数加上了梯度
self.bias = nn.Parameter(torch.randn(units,))
def forward(self, X):
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)
dense = MyLinear(5,3)
print(dense.weight)
# 使用自定义层直接执行正向传播计算
print(dense(torch.rand(2,5)))
# 使用自定义层构建模型
net = nn.Sequential(MyLinear(64,8),MyLinear(8,1))
print(net(torch.rand(2,64)))
读取文件
# 加载和保存张量
import torch
from torch import nn
from torch.nn import functional as F
x = torch.arange(4)
torch.save(x, 'x-file')
x2 = torch.load("x-file")
print(x2)
#存储一个张量列表,然后把它们读回内存
y = torch.zeros(4)
torch.save([x,y],'x-files')
x2, y2 = torch.load('x-files')
print(x2)
print(y2)
# 写入或读取从字符串映射到张量的字典
mydict = {'x':x,'y':y}
torch.save(mydict,'mydict')
mydict2 = torch.load('mydict')
print(mydict2)
# 加载和保存模型参数
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(20,256)
self.output = nn.Linear(256,10)
def forward(self, x):
return self.output(F.relu(self.hidden(x)))
net = MLP()
X = torch.randn(size=(2,20))
Y = net(X)
# 将模型的参数存储为一个叫做"mlp.params"的文件
torch.save(net.state_dict(),'mlp.params')
# 实例化了原始多层感知机模型的一个备份。直接读取文件中存储的参数
clone = MLP() # 必须要先声明一下,才能导入参数
clone.load_state_dict(torch.load("mlp.params"))
print(clone.eval()) # eval()是进入测试模式
Y_clone = clone(X)
print(Y_clone == Y)
卷积层
假设用手机拍摄了一张1200万像素的照片,而且是RGB图片,有R、G、B三个通道(channel),也就是有3600万个像素,每个像素对应有一个值
假设用一个单隐藏层的MLP来进行训练,隐藏层的大小是100,那么这个模型就有36亿个参数,这个数量远多于世界上所有的猫和狗的总数,还不如将所有的猫和狗全部记下来
所以这就是当使用MLP,特别是比较大的图片的时候所遇到的问题。
MLP
平移不变形:在任何一个地方,识别器不会因为图片像素出现的位置而发生改变
局部性:只需要看到局部的信息就可以了,而不需要看到全局的信息
从全连接层出发,应用上面两个原则,得到卷积
但是之前的全连接层的输入输出是一维的向量,这里为什么又还原成了矩阵?
因为要考虑空间的信息,所以将输入和输出变成矩阵,它有宽度和高度这两个维度对应的可以将权重变成一个4维的张量,之前是输入的长度到输出的长度的变化,现在变成了输入的高宽到输出的高宽的变化,所以可以将它reshape成为一个4D的张量
w表示全连接层的权重,之前是2维的现在变成了4维,求和的是k和l两个坐标,遍历两个维度再求和,其实和二维张量表示的权重求和是类似的如下图所示
然后对W进行重新索引,将W中的元素进行重新排列,组成V,将W的下标变化一下从而引出卷积
总的来说就是将全连接层的输入输出变成二维的,然后将权重做一些变换。
平移不变性使得对权重做了一定的限制,去掉了权重的i、j维度,只剩下a、b维度,最后直接得到了二维卷积交叉相关的计算,所以可以认为二维卷积就是全连接或者说是矩阵的乘法,但是改变了一些权重,使得他里面的一些东西是重复的,也就是说,他不是每一个元素都是可以自由变换的(当把一个模型的取值范围限制之后,模型的复杂度就降低了,同样也就意味着不需要存取大量的元素)
局部性是说假设需要计算hij的输出的话,输入以ij为中心所有的地方都会去看,但是实际上不应该看得太远,hij的结果只应该由输入xij附近的点决定就可以了。
总结:所以说卷积是一个特殊的全连接层
将a和b限制在一个很小的值
交叉相关和卷积是没有太多区别的,唯一的区别是在卷积上a、b的值有负号
这里虽然是卷积,但是实际的计算实现做的是交叉相关,严格定义上卷积应该是反过来的交叉相关
超参数就是卷积核的大小,卷积核的大小控制了局部性,卷积核越大看到的范围越广,卷积核越小看到的范围也就越小
卷积层可以看成一个特殊的全连接层
卷积解决了权重参数随着输入规模的增大而增大的问题,通过不变性减小了权重参数的数量
OpenCV
Canny边缘检测流程:
使用高斯滤波器,以平滑图像,滤除噪声。
计算图像中每个像素点的梯度强度和方向。
应用非极大值(Non-Maximum Suppression)抑制,以消除边缘检测带来的杂散响应。
应用双阈值(Double-Threshold)检测来确定真实的和潜在的边缘。
通过抑制孤立的弱边缘最终完成边缘检测。
高斯滤波器
高斯滤波器靠近的中心点的权重比较大,较远中心点的权重比较小。
梯度和方向
非极大值抑制
① C 点的梯度和方向可以通过前一步算出来。
② C 点的梯度是不是一个极大值点,应该是去跟它的临近点去比较。
③ 利用 C 点梯度的方向,可以得到上面有一个交点 Q,下面有一个交点 Z,如果 C 点的梯度比 Q 和 Z 都大,那么 C 就是极大值点,其中 Q 和 Z 的梯度值通过线性差值法来计算。
④ 如果 C 的梯度是极大值点,那么 C 就是边缘点。否则 C 不是极大值点,就会被抑制。
① 简单计算将像素点周围固定为八个像素,当梯度角度相交的点与哪个方向近,就哪个方向的两个点。
② 例如,梯度方向是 43° 就取上下两个像素来做极大值判断,如果梯度方向是 46°,就取左下、右上两个像素来做极大值判断。
③ 如下图所示,如果 A 的梯度值比 B 和 C 都要大,那么 A 就是边界,由于边界与梯度方向垂直,所以如下图所示黑色为边界。
双阈值检测
① C 在 minVal 与 maxVal 之间,是候选边界,若 C 的左右或上下两边连有 A,而 A 是边界,那么定义 C 也是边界。
② B 在 minVal 与 maxVal 之间,是候选边界,若B的左右或上下像素都不是边界,那么 B 就被舍弃,不定义为边界。
代码实现:
import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np # numpy数值计算工具包
def cv_show(img,name):
cv2.imshow(name,img)
cv2.waitKey()
cv2.destroyAllWindows()
img = cv2.imread('01_Picture/07_Lena.jpg',cv2.IMREAD_GRAYSCALE)
v1 = cv2.Canny(img,80,150) # 第二个参数为minVal,第三个参数为maxVal
v2 = cv2.Canny(img,50,100)
res = np.hstack((v1,v2))
cv_show(res,'res')
img = cv2.imread('01_Picture/08_Car.png',cv2.IMREAD_GRAYSCALE)
v1 = cv2.Canny(img,120,250) # 第二个参数为minVal,第三个参数为maxVal
v2 = cv2.Canny(img,50,100)
res = np.hstack((v1,v2))
cv_show(res,'res')
图像金字塔
图像金字塔可以做图像特征提取,做特征提取时有时可能不光对原始输入做特征提取,可能还会对好几层图像金字塔做特征提取。可能每一层特征提取的结果是不一样的,再把特征提取的结果总结在一起。
高斯金字塔
分成两种一种是向下采样方法(缩小)
将Gi与高斯内核卷积,然后将所有偶数行和列去除。
向上采样方法是放大:1,将图像在每个方向扩大为原来的两倍,新增的行和列以0来填充。
2.使用先前同样的内核(乘以4)与放大后的图像卷积,获得近似值。
import cv2 #opencv的缩写为cv2
import matplotlib.pyplot as plt # matplotlib库用于绘图展示
import numpy as np # numpy数值计算工具包
def cv_show(img,name):
cv2.imshow(name,img)
cv2.waitKey()
cv2.destroyAllWindows()
img = cv2.imread('01_Picture/09_AM.png')
cv_show(img,'img')
print(img.shape)
img = cv2.imread('01_Picture/09_AM.png')
up = cv2.pyrUp(img)
cv_show(up,'up')
print(up.shape)
img = cv2.imread('01_Picture/09_AM.png')
down = cv2.pyrDown(img)
cv_show(down,'down')
print(down.shape)
up = cv2.pyrUp(up) # 上采样之后再上采样
cv_show(up,'up')
print(up.shape)
img = cv2.imread('01_Picture/09_AM.png')
up = cv2.pyrUp(img)
up_down = cv2.pyrDown(up) # 先上采样再下采样
cv_show(np.hstack((img,up_down)),'up_down')
拉普拉斯金字塔
1 拉普拉斯金字塔的每一层图像尺寸不变。
2拉普拉斯金字塔的每一层操作都是上一层处理后作为输入,该输入减去该输入缩小放大后的图像,获得该层的输出。
img = cv2.imread('01_Picture/09_AM.png')
domn = cv2.pyrDown(img)
down_up = cv2.pyrUp(down)
L_1 = img - down_up
cv_show(L_1,'L_1')
print(L_1.shape)
总结
深度学习方面的进展太慢,下周开始看各类神经网络的相关模型。代码实现上面,先不需要太仔细过,掌握原理、大概模块功能即可。