林晗的AI笔记


  • 首页

  • 归档

001a_神经网络_基础

发表于 2020-01-17

神经网络初探

声明:本教程使用PyTorch>=1.3和Fastai v1以上版本。原教程来自Jeremy Howard,因其为全英文,同时笔者在自学过程中记了一些笔记,本着分离的精神遂将笔记分享。须知原作者为Jeremy Howard。

MNIST数据集设置

我们使用黑白手写数字数据集MNIST,其包含70000张从0到9的手写数字图片,图像尺寸为28*28像素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pathlib import Path
import requests

DATA_PATH = Path('data')
PATH = DATA_PATH/'mnist'

PATH.mkdir(parents=True, exist_ok=True)

URL='http://deeplearning.net/data/mnist/'
FILENAME='mnist.pkl.gz'

if not (PATH/FILENAME).exists():
content = requests.get(URL+FILENAME).content
(PATH/FILENAME).open('wb').write(content)

数据集是numpy数组格式,已经使用pickle序列化了,这里把它们导入内存的时候要反序列化。

1
2
3
4
5
import pickle, gzip

with gzip.open(PATH/FILENAME, 'rb') as f:
((x_train, y_train), (x_valid, y_valid), _) = \
pickle.load(f, encoding='latin-1')

每个图像大小为28x28,并被存储为长度为784 (=28x28)的向量而非矩阵。我们来看一眼:我们需要先将其重塑为二维数组。

1
2
3
4
5
6
7
%matplotlib inline

from matplotlib import pyplot
import numpy as np

pyplot.imshow(x_train[0].reshape((28,28)), cmap="gray")
x_train.shape

输出:

1
2
(50000, 784)
<class 'numpy.ndarray'>

image
PyTorch使用的数组格式是torch.tensor而非numpy数组,因此我们需要转化一下数据格式。

1
2
3
4
5
6
import torch 

x_train,y_train,x_valid,y_valid = map(torch.tensor, \
(x_train,y_train,x_valid,y_valid))
n,c = x_train.shape
x_train, x_train.shape, y_train.min(), y_train.max()

## 完全手撸的神经网络(不用torch.nn模块) 让我们先用PyTorch张量运算建立一个模型。我们假设你已经熟悉了神经网络的基础知识。(如果你没有,你可以在fast.ai课程中学习一下。) PyTorch提供了创建随机或零填充张量的方法,我们将使用这些方法创建一个简单线性模型的权重和偏差。这些只是普通的张量,有一个非常特殊的添加:我们告诉PyTorch它们需要一个梯度。这使得PyTorch记录了对张量做的所有操作,这样它就可以自动计算反向传播过程中的梯度了! 对于权重,我们在初始化之后设置requires_grad,因为我们不希望该步骤包含在梯度中。(注意,在PyTorch中标记_表示操作是执行后赋值回去。) 注:我们在这里初始化的权重与Xavier初始化(乘以`$\dfrac{1}{\sqrt{n}}$`)。
1
2
3
4
5
import math

weights = torch.randn(784,10)/math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)

由于PyTorch能够自动计算梯度,我们可以使用任何标准的Python函数(或可调用对象)作为模型!让我们写一个简单的矩阵乘法并加入加法来创建一个简单的线性模型。我们还需要一个激活函数,因此我们将编写log_softmax并使用它。请记住:虽然PyTorch提供了许多预先编写的损失函数、激活函数等,但是您可以使用普通python轻松地编写自己的函数。PyTorch甚至可以为你的函数自动创建快速的GPU或矢量化的CPU代码。
​

1
2
3
4
5
6
7
# 损失函数
def log_softmax(x):
return x - x.exp().sum(-1).log().unsqueeze(-1)

# 前面传播
def model(xb):
return log_softmax(xb @ weights + bias)

在上面,“@”代表点积操作。我们将对一批数据(在本例中为64张图像)调用函数。这是一次向前传播。注意,我们的预测在这个阶段未必比随机的好,因为我们从随机的权值开始。

1
2
3
4
5
bs=64                  # batch size

xb = x_train[0:bs] # a mini-batch from x
preds = model(xb) # predictions
preds[0], preds.shape

正如你所看到的,preds张量不仅包含张量值,还包含一个梯度。我们稍后会用它来做反向传播(back propagation)。
让我们实现负对数似然函数作为损失函数(同样,我们可以使用标准的Python):

1
2
3
4
def nll(input, target):
return -input[range(target.shape[0]), target].mean()

loss_func = nll

让我们用我们的随机模型来检查一下我们的损失,这样我们就可以看到我们是否会在反向传播后有所改善。

1
2
3
4
yb = y_train[0:bs]

# 用负对数似然函数衡量的损失
loss_func(preds, yb)

让我们实现一个函数来计算我们的模型的精度。对于每个预测,如果具有最大值的索引与目标值匹配,则预测是正确的。

1
2
3
def accuracy(out, yb):
preds = torch.argmax(out, dim=1)
return (preds==yb).float().mean()

让我们检查一下随机模型的准确性,这样我们就可以看到我们的准确性是否会随着损失的增加而提高。

1
accuracy(preds, yb)

我们现在可以运行一个训练循环。对于每个迭代,我们将:

  • 选择一小批数据(大小为bs)
  • 使用模型进行预测
  • 计算损失
  • loss.backward()更新模型的梯度(在本例中更新的是权重和偏差的梯度)。
  • 我们现在使用这些梯度来更新权重和偏差。我们在torch.no_grad()上下文管理器中执行此操作,因为我们不希望在下一次计算梯度时记录这些操作。
  • 然后,我们将梯度设置为零,以便为下一个循环做好准备。否则,我们的梯度将记录所有已经发生的操作的运行记录(例如,loss. backwards()将梯度添加到已经存储的内容中,而不是替换它们),这样的话下次更新参数就使用了错误的梯度了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    from IPython.core.debugger import set_trace

    lr = 0.5 # learning rate
    epochs = 2 # how many epochs to train for

    for epoch in range(epochs):
    for i in range((n-1)//bs + 1):
    # set_trace()
    start_i = i*bs
    end_i = start_i+bs
    xb = x_train[start_i:end_i]
    yb = y_train[start_i:end_i]
    pred = model(xb)
    loss = loss_func(pred, yb)

    loss.backward()
    with torch.no_grad():
    weights -= weights.grad * lr
    bias -= bias.grad * lr
    weights.grad.zero_()
    bias.grad.zero_()

好了:我们已经完全从零开始创建并训练了一个最小的神经网络(在本例中是逻辑回归,因为我们没有隐藏层)!
让我们检查一下损失和准确性,并与我们之前得到的进行比较。我们预计损失会减少,准确性会提高,他们确实做到了。

1
loss_func(model(xb), yb), accuracy(model(xb), yb)

## 使用torch.nn.functional 现在我们将重构我们的代码,使它做与以前相同的事情,只是我们将开始利用PyTorch的nn类使它更简洁和灵活。从这里开始的每一步,我们都应该使我们的代码变得更短、更容易理解和/或更灵活。 第一步,也是最简单的一步,是将我们手写的激活函数和损失函数替换为来自于torch.nn.functional的函数(通常按照惯例导入后临时命名为F),从而缩短代码。。这个模块包含了torch.nn库中的所有函数(而库的其他部分包含类)。除了各种各样的损失和激活函数之外,您还可以在这里找到一些用于创建神经网络的方便函数,比如池函数(pooling)。(也有一些函数用于执行卷积、线性层等,但是我们将看到,使用库的其他部分通常可以更好地处理这些函数。) 如果您使用的是负对数似然损失和对数softmax激活,那么Pytorch提供了一个单独的函数F.cross_entropy两者结合起来,即交叉熵。因此我们甚至可以从现在的模型中移除激活函数了。
1
2
3
4
5
import torch.nn.functional as F
loss_func = F.cross_entropy

def model(xb):
return xb @ weights + bias

注意,我们不再在模型函数中调用log_softmax。我们确认一下我们的损失和准确性和之前一样:
​

1
loss_func(model(xb), yb), accuracy(model(xb), yb)


## 使用nn.Module重构 接下来,我们将使用nn.Module和nn.Parameter进行更清晰,更简洁的训练循环。 我们将从nn.Module(它本身是一个类并且能够跟踪状态)继承并创建它的子类。在本例中,我们要创建一个类,该类包含前向传播的weights, bias和前向传播的函数。nn.Module具有许多我们将要使用的属性和方法(例如.parameters()和.zero_grad())。

注意:nn.Module(大写M)是PyTorch的特定概念,并且是我们将大量使用的类。 nn.Module不要与Python的module概念混淆,该概念是可以导入的Python代码文件。

1
2
3
4
5
6
7
8
9
10
from torch import nn

class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
self.weights = nn.Parameter(torch.randn(784,10)/\
math.sqrt(784))
self.bias = nn.Parameter(torch.zeros(10))

def forward(self, xb): return xb @ self.weights + self.bias

因为我们现在使用的是对象而不是函数,我们首先要实例化我们的模型:

1
model = Mnist_Logistic()

有意思的是,nn.Module()这个类里实现了call()这个方法,因此这个类实例出来的对象是可调用的(本博客内的文章有详细介绍),因此接下来我们可以直接把model作为函数来调用。
现在我们可以用以前的方法来计算损失。注意,nn.Module对象被当作函数来使用(但是在后台Pytorch会自动调用我们的forward方法)。

1
loss_func(model(xb), yb)

在之前的训练循环中,我们必须按名称更新每个参数的值,并手动将每个参数的梯度归零,如下所示:

1
2
3
4
5
with torch.no_grad():
weights -= weights.grad * lr
bias -= bias.grad * lr
weights.grad.zero_()
bias.grad.zero_()

现在,我们可以利用model.parameters()和model.zero_grad()(它们都是由PyTorch为n. module定义的)来简化这些步骤,减少忘记一些参数的错误,特别是当我们有一个更复杂的模型时:

1
2
3
with torch.no_grad():
for p in model.parameters(): p -= p.grad * lr
model.zero_grad()

我们将在’fit’函数中封装我们的训练循环,以便稍后再次运行它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def fit():
for epoch in range(epochs):
for i in range((n-1)//bs + 1):
start_i = i*bs
end_i = start_i+bs
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
pred = model(xb)
loss = loss_func(pred, yb)

loss.backward()
with torch.no_grad():
for p in model.parameters(): p -= p.grad * lr
model.zero_grad()

fit()

让我们再次检查一下我们的损失是否减少了:

1
loss_func(model(xb), yb)

## 使用nn.Linear重构 我们继续重构代码。而不是手动定义和初始化参数再手动运算。我们将使用Pytorch的类nn.Linear类来代替。一个线性层的线性,它为我们做了所有这些。Pytorch有许多类型的预定义层,这些层可以极大地简化我们的代码,并且常常使它更快。
1
2
3
4
5
6
class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
self.lin = nn.Linear(784,10)

def forward(self, xb): return self.lin(xb)

我们实例化我们的模型,并按照与之前相同的方式计算损失:
​

1
2
model = Mnist_Logistic()
loss_func(model(xb), yb)

然后运行一遍fit()函数来训练模型。最后对比一下损失。

1
2
fit()
loss_func(model(xb), yb)

使用optim重构

Pytorch还有一个包含各种优化算法的包,torch.optim。我们可以使用来自优化器的step()方法来执行前向传播,而不是手动更新每个参数。
这将让我们取代之前的手动编码优化步骤:

1
2
3
with torch.no_grad():
for p in model.parameters(): p -= p.grad * lr
model.zero_grad()

而是使用以下代码:

1
2
opt.step()
opt.zero_grad()

(optim.zero_grad()将梯度重置为0,我们需要在为下一个minibatch计算梯度之前调用它)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from torch import optim

def get_model():
model = Mnist_Logistic()
return model, optim.SGD(model.parameters(), lr=lr)

model,opt = get_model()
loss_func(model(xb), yb)

for epoch in range(epochs):
for i in range((n-1)//bs + 1):
start_i = i*bs
end_i = start_i+bs
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
pred = model(xb)
loss = loss_func(pred, yb)

loss.backward()
opt.step()
opt.zero_grad()

loss_func(model(xb), yb)

## 使用Dataset重构 PyTorch有一个抽象的数据集类Dataset。一个Dataset可以是任何具有一个__len__方法(有了它就可以被Python的标准len函数调用)和一个__getitem__方法(有了它就可索引)的类。本教程将介绍一个创建自定义FacialLandmarkDataset类作为Dataset子类的示例。 PyTorch的TensorDataset是一个包装张量的数据集。通过定义长度和索引的方式,这也给了我们一种方法来遍历、索引和切片一个张量的第一维。这将使我们更容易地同时访问解释变量和被解释变量。
1
2
3
4
5
from torch.utils.data import TensorDataset

'''x_train和y_train都可以组合在一个单独的TensorDataset中,
这样更容易遍历和切片。'''
train_ds = TensorDataset(x_train, y_train)

之前,我们必须分别迭代x和y值的小批量:
​

1
2
xb = x_train [start_i end_i):
yb = y_train [start_i end_i):

现在,我们可以一起做这两个步骤:

1
xb,yb = train_ds[i*bs: i*bs+bs]
1
2
3
4
5
6
7
8
9
10
11
12
13
model,opt = get_model()

for epoch in range(epochs):
for i in range((n-1)//bs + 1):
xb,yb = train_ds[i*bs : i*bs+bs]
pred = model(xb)
loss = loss_func(pred, yb)

loss.backward()
opt.step()
opt.zero_grad()

loss_func(model(xb), yb)

有点意思,对吧?一步一步来。

使用DataLoader来重构

Pytorch的DataLoader负责管理batches。你可以从任何数据集创建DataLoader。DataLoader简化了对batches的迭代。而不是必须使用$train_ds[i * bs: i * bs+bs]$, DataLoader自动为我们提供每个mini batch批处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)

model,opt = get_model()
for epoch in range(epochs):
for xb,yb in train_dl:
pred = model(xb)
loss = loss_func(pred, yb)

loss.backward()
opt.step()
opt.zero_grad()

多亏了Pytorch的 nn.Module, nn.Parameter, Dataset, and DataLoader,我们的训练循环现在大大缩小,更容易理解。现在让我们尝试添加现实中创建有效模型所需的基本特性。


# 添加验证集 在第1部分中,我们只是试图建立一个合理的训练循环,以用于我们的训练数据。实际上,你总是应该有一个验证集,以便确定是否进行了过度拟合。 对训练数据进行顺序洗牌对于防止批量之间的相关性和过度拟合非常重要。另一方面,无论是否重新洗牌验证集,验证损失都是相同的。由于重新洗牌需要额外的时间,因此没有必要重新洗牌验证数据。 我们将为验证集使用两倍于训练集的批处理大小。这是因为验证集不需要反向传播,因此占用的内存更少(它不需要存储梯度)。我们利用这一点来使用更大的批大小以便更快地计算损失。
1
2
3
4
5
train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)

valid_ds = TensorDataset(x_valid, y_valid)
valid_dl = DataLoader(valid_ds, batch_size=bs*2)

我们将在每个epoch结束时计算并打印验证损失。
(注意,我们总是在训练之前调用model.train(),在推断之前调用model.eval(),因为这些是由nn.BatchNorm2d 和 nn.Dropout,以确保为这些不同的阶段采取适当的行为,比如是否有dropout,是否shuffle。)
​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
model,opt = get_model()

for epoch in range(epochs):
model.train()
for xb,yb in train_dl:
pred = model(xb)
loss = loss_func(pred, yb)

loss.backward()
opt.step()
opt.zero_grad()

model.eval()
with torch.no_grad():
valid_loss = sum(loss_func(model(xb), yb)
for xb,yb in valid_dl)

print(epoch, valid_loss/len(valid_dl))

重构一下拟合和获取数据的代码

现在我们自己来做一点重构。因为我们要经历一个类似的过程,两次计算训练集和验证集的损失,所以让我们将其转换为自己的函数“loss_batch”,它计算一个批处理的损失。
我们为训练集传递一个优化器,并使用它来执行backprop。对于验证集,我们不传递优化器,因此该方法不执行backprop。

1
2
3
4
5
6
7
8
9
def loss_batch(model, loss_func, xb, yb, opt=None):
loss = loss_func(model(xb), yb)

if opt is not None:
loss.backward()
opt.step()
opt.zero_grad()

return loss.item(), len(xb)

定义fit()函数运行必要的操作来训练我们的模型,并计算每个epoch的训练和验证损失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np

def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
for epoch in range(epochs):
model.train()
for xb,yb in train_dl: loss_batch(model, loss_func, xb, yb, opt)

model.eval()
with torch.no_grad():
losses,nums = zip(*[loss_batch(model, loss_func, xb, yb)
for xb,yb in valid_dl])
val_loss = np.sum(np.multiply(losses,nums)) / np.sum(nums)

print(epoch, val_loss)

定义get_data()函数返回用于训练和验证集的dataloader。

1
2
3
def get_data(train_ds, valid_ds, bs):
return (DataLoader(train_ds, batch_size=bs, shuffle=True),
DataLoader(valid_ds, batch_size=bs*2))

现在,我们获取数据加载器并拟合模型的整个过程可以在3行代码中运行:

1
2
3
train_dl,valid_dl = get_data(train_ds, valid_ds, bs)
model,opt = get_model()
fit(epochs, model, loss_func, opt, train_dl, valid_dl)

您可以使用这三行代码来训练各种各样的模型。让我们看看是否可以用它们来训练一个卷积神经网络(CNN)!

切换到CNN

第一次尝试

我们现在要构建一个有三个卷积层的神经网络。因为前一节中的函数都没有假定任何关于模型形式的内容,所以我们可以使用它们来训练CNN,而无需进行任何修改。
我们将使用Pytorch预定义的Conv2d类作为卷积层。我们定义了一个3个卷积层的CNN。每个卷积后面都有一个ReLU。最后,我们执行一个平均池。(注意,view()方法是PyTorch版本的numpy reshape()方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Mnist_CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)

def forward(self, xb):
xb = xb.view(-1,1,28,28)
xb = F.relu(self.conv1(xb))
xb = F.relu(self.conv2(xb))
xb = F.relu(self.conv3(xb))
xb = F.avg_pool2d(xb, 4)
return xb.view(-1,xb.size(1))

lr=0.1

# 动量是随机梯度下降的一种变化,它也考虑了以前的更新,通常会使训练更快的收敛。
model = Mnist_CNN()
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)

nn.Sequential

torch.nn还有一个方便的类,我们可以用它来简化代码:Sequential。Sequential对象以顺序的方式运行其中包含的每个模块。这是一种更简单的神经网络的写法。
为了利用这一点,我们需要能够从给定的函数轻松地定义一个自定义层。例如,PyTorch没有视图层,我们需要为我们的网络创建一个视图层。Lambda将创建一个层,然后我们可以使用它来定义一个具有序列的网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Lambda(nn.Module):
def __init__(self, func):
super().__init__()
self.func=func

def forward(self, x):
return self.func(x)

def preprocess(x):
return x.view(-1,1,28,28)

# 使用‘Sequential’创建的模型很简单
model = nn.Sequential(
Lambda(preprocess),
nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1), nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1), nn.ReLU(),
nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1), nn.ReLU(),
nn.AvgPool2d(4),
Lambda(lambda x: x.view(x.size(0),-1))
)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)

包装DataLoader

我们的CNN相当简洁,但它只适用于MNIST,因为:

  • 它假设输入是一个28*28长的向量
  • 它假设最终的CNN网格大小是4*4(因为这是我们使用的平均池内核大小)

让我们摆脱这两个假设,以便我们的模型适用于任何二维单通道图像。首先,我们可以删除初始Lambda层,但将数据预处理移动到生成器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def preprocess(x,y):
return x.view(-1,1,28,28),y

class WrappedDataLoader():
def __init__(self, dl, func):
self.dl = dl
self.func = func

def __len__(self): return len(self.dl)

def __iter__(self):
batches = iter(self.dl)
for b in batches: yield(self.func(*b))

train_dl,valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)

接下来,我们可以用nn.AdaptiveAvgPool2d替换nn.AvgPool2d,它允许我们定义我们想要的输出张量的大小,而不是我们拥有的输入张量。因此,我们的模型可以处理任何大小的输入。

1
2
3
4
5
6
7
8
9
10
model = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1), nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1), nn.ReLU(),
nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1), nn.ReLU(),
nn.AdaptiveAvgPool2d(1),
Lambda(lambda x: x.view(x.size(0),-1))
)

opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)

使用GPU

实际训练中稍微略大一点的项目,几乎不会有人使用CPU来训练,那样太慢了。首先查看一下你的GPU是否能在PyTorch下工作:

1
2
3
4
5
torch.cuda.is_available()

# 然后为它创建一个设备对象:
dev = torch.device('cuda') if torch.cuda.is_available() \
else torch.device('cpu')

让我们更新预处理函数preprocess(),将batches移动到GPU:

1
2
3
4
5
6
7
8
9
10
11
def preprocess(x,y):
return x.view(-1,1,28,28).to(dev),y.to(dev)

train_dl,valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)

# 最后,我们可以将模型移动到GPU。
model.to(dev)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)

Python的魔法方法__call__()和__init__()拾遗

发表于 2019-11-04

__call__()

我们常听到:Python中一切都是对象。

这句话对Python中的函数也是同样适用的。事实上,函数在Python中是一级对象,即Python中的函数可以作为输入参数传递到其他的函数/方法中,并在其中被执行。

同时,对象(实例化的类)可以被当做函数对待。也就是说,可以给对象传递参数,达到函数的效果。比如这样:

1
this_is_call(arg1, arg2)

要把一个实例对象做为函数调用,需要在类中实现_call_()方法。也就是我们要在类中实现如下方法:

1
2
def __call__(self, *args):
pass

假设x是X类的一个实例。那么调用x.__call__(1,2)等同于调用x(1,2)。这个实例本身在这里相当于一个函数。

看到这里你可能会有一点疑惑,这哥们儿有点眼熟儿啊,和__init__()有啥区别?

没错,但后者是初始化实例时用的,具体区别如下:

  1. __init__()的作用是初始化某个类的一个实例。
  2. __call__()的作用是使实例能够像函数一样被调用,同时不影响实例本身的生命周期(__call__()不影响一个实例的构造和析构)。但是__call__()可以用来改变实例的内部成员的值。

代码对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
class X(object):
def __init__(self, a, b, range):
self.a = a
self.b = b
self.range = range
def __call__(self, a, b):
self.a = a
self.b = b
print('__call__ with ({}, {})'.format(self.a, self.b))
def __del__(self, a, b, range):
del self.a
del self.b
del self.range

调用结果:

1
2
3
>>> xInstance = X(1, 2, 3)	# 创建对象,用的__init__()
>>> xInstance(1,2) # 调用对象(这个对象已经是一个函数了),这时用的__call__()
__call__ with (1, 2)

opencv几种简单的转化

发表于 2019-11-02

1. 通道交换

读取图像,然后将BGR通道(这是opencv默认打开图像的方式)替换成RGB 通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import cv2

img = cv2.imread("../imori.jpg")
b = img[:, :, 0].copy()
g = img[:, :, 1].copy()
r = img[:, :, 2].copy()

# BGR -> RGB
img[:, :, 0] = r
img[:, :, 1] = g
img[:, :, 2] = b

# save result
cv2.imwrite("answer_1.jpg", img)
cv2.imshow("result", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
输入 输出
image image

2. 灰度化(Grayscale)

灰度是一种图像亮度的表示方法,通过下式计算:

Y = 0.2126 R + 0.7152 G + 0.0722 B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cv2
import numpy as np

img = cv2.imread("../imori.jpg").astype(np.float)
b = img[:, :, 0]
g = img[:, :, 1]
r = img[:, :, 2]

out = 0.2126 * r + 0.7152 * g + 0.0722 * b
out = out.astype(np.uint8)

cv2.imwrite("answer_2.jpg", out)
cv2.imshow("result", out)
cv2.waitKey(0)
cv2.destroyAllWindows()
输入 输出
image image

先写到这里,后续看到有意思的再继续补充。

opencv中的大津算法(最大类间差法/OTSU算法)

发表于 2019-11-02

大津二值化算法(Otsu’s Method)

要解释大津二值化算法,首先要解释一下二值化算法。

opencv通过cv2.imread(“图片路径”)读取图片后(假设是彩色BGR三通道的图片),每个通道的每个像素是从0到255的无符号整数,我们看到的颜色是通过三个通道对0到255的不同组合呈现出来的。二值化是首先把它们合并成一张灰度图,再设定一个阈值th,高于th的像素值设为255(即最白),低于th的则设为0(即最黑)。这是最简单的二值化,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cv2
import numpy as np

img = cv2.imread("../imori.jpg").astype(np.float)
b = img[:, :, 0].copy()
g = img[:, :, 1].copy()
r = img[:, :, 2].copy()

out = 0.2126 * r + 0.7152 * g + 0.0722 * b
out = out.astype(np.uint8)

th = 128

out[out < th] = 0
out[out >= th] = 255

cv2.imwrite("answer_3.jpg", out)
cv2.imshow("result", out)
cv2.waitKey(0)
cv2.destroyAllWindows()
输入 输出
image image

但这存在一个问题,如何设置这个阈值th呢?上面的代码是直接取0到255的中值128(其实也不是中值,中值是127.5,好吧,我钻牛角尖了),效果也还不错,这是因为刚好这张原图的灰度图的像素分布在128的两边,但如果是一张很灰暗的或者很亮的图呢?那可能得到的是一张全黑或全白的结果了。

一个解决办法是遍历0到255所有数值,把它们都作为阈值th,看在哪一个th下二值化效果最好。这里“效果最好”指的是两部分的类间方差最大——类间方差越大,就说明两部分之间的灰度差距越大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import cv2
import numpy as np

img = cv2.imread("../imori.jpg").astype(np.float)
H, W, C = img.shape

b = img[:, :, 0].copy()
g = img[:, :, 1].copy()
r = img[:, :, 2].copy()

out = 0.2126 * r + 0.7152 * g + 0.0722 * b
out = out.astype(np.uint8)

max_sigma = 0
max_t = 0

for _t in range(1, 255):
v0 = out[np.where(out < _t)]
m0 = np.mean(v0) if len(v0) > 0 else 0.
w0 = len(v0) / (H * W)
v1 = out[np.where(out >= _t)]
m1 = np.mean(v1) if len(v1) > 0 else 0.
w1 = len(v1) / (H * W)
sigma = w0 * w1 * ((m0 - m1)**2)
if sigma > max_sigma:
max_sigma = sigma
max_t = _t

# 下面对其进行二值化
print("二值化阈值:", max_t)
th = max_t
out[out < th] = 0
out[out >= th] = 255

# 保存结果
cv2.imwrite("answer_4.jpg", out)
cv2.imshow("result", out)
cv2.waitKey(0)
cv2.destroyAllWindows()
输入 输出
image image

numpy.where()函数用法:(np.where())

发表于 2019-11-02

首先看一下源码:

1
2
def where(condition, x=None, y=None):
'''忽略源码'''

所以实际上既可以只传第一个参数(后面两个默认为None),也可以传两个或三个参数。

用法1:

1
np.where(condition, x, y)

这里有一点像Excel中的if()函数。

1
2
3
4
5
>>> aa = np.arange(10)
>>> np.where(aa,1,-1)
array([-1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) # 0为False,所以第一个输出-1
>>> np.where(aa > 5,1,-1)
array([-1, -1, -1, -1, -1, -1, 1, 1, 1, 1])

用法2:

1
np.where(condition)

只有条件 (condition),没有x和y,则输出满足条件 (即非0) 元素的坐标(注意是坐标,不是值)。

坐标以tuple的形式给出,通常原数组有多少维,输出的tuple中就包含几个数组,分别对应符合条件元素的各维坐标。

1
2
3
4
5
6
7
8
9
10
11
>>> a = np.array([2,4,6,8,10])
>>> np.where(a > 5) # 返回索引
(array([2, 3, 4]),) # 这是6, 8, 10的坐标
>>> a[np.where(a > 5)] # 等价于 a[a>5]
array([ 6, 8, 10])

>>> np.where([[0, 1], [1, 0]])
(array([0, 1]), array([1, 0]))
''' 上面这个有点复杂,[[0,1],[1,0]]的真值为两个1,
各自的第一维坐标为[0,1],第二维坐标为[1,0] 。
'''

Anaconda 执行 conda create -n无法定位程序输入点 openssl_sk_new_reserve

发表于 2019-10-14

之前在本地电脑上配置cuda的时候可能把Anaconda的环境搞乱了,所以今天想要创建一个独立的python3.6环境的时候报了这个弹窗的错(当时没保存截图),内容为:conda create -n无法定位程序输入点openssl_sk_new_reserve

网上找了下资料,发现是.dll的锅。

打开Anaconda安装文件夹(根据个人的路径查找,你的可能和我的不一样):

image

image

这两个文件的修改日期必须是一致的,如果不一致,把DLLs文件夹中的替换掉bin文件夹中的那个。(上图是替换好后的)

再次尝试,已经OK了。

image

Django2.2 学习笔记第一篇

发表于 2019-10-01

Django是什么?

Django是是用 Python 写的一个自由和开放源码 web 应用程序框架。 web框架是一套组件,能帮助你更快、更容易地开发web站点。

编程一个重要原则就是不要重复造轮子。当你开始构建一个web站点时,你总需要一些相似的组件:处理用户认证(注册、登录、登出)的方式、一个管理站点的面板、表单、上传文件的方式,等等。

幸运的是,其他人很早就注意到web开发人员会面临一些共同的问题。所以他们联手创建了 web 框架(Django 是其中一个)来让你使用。


你为什么需要一个框架?

要理解什么是Django, 我们需要更仔细的看一下服务器。 服务器需要知道的第一件事就是你希望它为你的网页做什么。

想象一个用来监控接收邮件(请求)的邮箱(端口)。 这就是网站服务器做的事情。 网站服务器读这封信,然后将响应发送给网页。

但是当你想发送一些东西的时候,你必须要有一些内容。 而Django就是可以帮助您创建内容的工具。


当有人向您的服务器请求一个网站,会发生什么呢?

当一个请求到达网站服务器,它会被传递到Django,试图找到实际上什么是被请求的。 它首先会拿到一个网页的地址,然后试图去弄清该做什么。 这个部分是由Django的urlresolver(url解析器。注意一个网站的地址被叫做URL,统一资源定位器,所以url解析器是有意义的)。 它并不十分聪明,他接受一个模式列表,然后试图去匹配 URL。 Django从顶到底检查模式,如果有匹配上的,那么Django会将请求传递给相关的函数(这被称作视图)。).

想象一个邮递员拿着一封信。 她沿着街区走下去,检查每一个房号与信件地址是否对应。 如果匹配上了,她就把信投在那里。 这也是url解析器的工作方式!

在视图函数里做了很多有趣的事情:我们能在数据库中寻找到一些信息。 如果用户要求修改数据呢? 就像一封信里说,“请修改我的工作描述”,视图 将检查是否你允许它这么干,然后更新工作描述并发回一个消息:“做完了”。 然后视图生成响应,并且Django能够发送给用户的web浏览器。

当然,上述的描述是有一些简化的,但是你不需要去知道所有的技术的东西。有一个大概的想法就够了。

所以不要太过深入细节,我们会简单用Django创建一些东西,然后我们会在学习的路上学到所有重要的部分!


编写你的第一个 Django 应用,第 1 部分

首先查看一下Django版本号,在命令行输入:

1
$ python -m django --version

创建项目

在命令行敲入如下代码:

1
$ django-admin startproject mysite

看一下startproject创建了哪些东西:

1
2
3
4
5
6
7
mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
wsgi.py

lesson 1 fast.ai 图像分类

发表于 2019-09-30

Fast.ai Lesson 1 图像分类 上篇

大家好,我叫南林笑笑生,因为在学习fast.ai这个深度学习框架的过程中觉得很好玩,希望能和更多志同道合的朋友一起讨论,但发现中文网上的资料不多,因此我把我学习的笔记在这里分享给大家。

简单介绍一下,fast.ai是一个基于PyTorch(如果你看到这里,想必你已经听说过天下第二的PyTorch了吧)的高级封装,类似于Keras和Tensorflow的关系,但远比Keras要方便和易上手的多的框架。

  • 当然,后两者的关系要紧密得多,tf.keras甚至可以和keras中的绝大多数api通用了。

易上手往往意味着难于不灵活和不便于部署,不过对fast.ai不需要太担心,需要灵活调整的部分完全可以交给PyTorch来做;部署上,训练好的模型可以直接用torch.onnx来把模型转换成各种你想要的格式。

除此外还有两个准备工作建议大家先完成(预计花费20分钟)。

  1. 使用谷歌搜索,不要再用百度了。不需要翻墙,只需要为你的Chrome浏览器安装一个谷歌访问助手即可。

    具体操作请百度,哦不,谷歌一下,这里就不炒冷饭了。

  2. 注册一个kaggle账号,这有两个目的,一个是尽可能地参加kaggle比赛,按照Jeremy的观点,如果在一个kaggle竞赛中你进入了前10%的名次,那么就真的弄明白你在做什么了。另一个目的是使用kaggle kernel,这是一个免费的GPU计算资源,使用Nvidia K80显卡,而且它下载许多国外的开源数据集速度非常快,这一点即使在国内自己搭梯子也没办法做到。

    注册需要临时搭个梯子,注册好账号,又有谷歌访问助手,就不再需要梯子,随时可以用了。


闲话不多说了,我们这就开始吧。深度学习的往往从图像入门,我们也不例外。

开始

首先,只需要下面两行代码(其实只要第二行的就可以了),即可完成fast.ai图像处理的模块以及很多相关的第三方库的引入。

1
2
from fastai import*
from fastai.vision import *

Jupyter格式子的使用技巧

一般我们都是在Jupyter Notebook中来调试深度学习的代码,如果看教程的你也是这样的话,有一些骚操作可以带来极大的便利。

  1. ?functione-name: 在类名/函数名前加?,然后shift + enter,可以看到相关文档。

    1
    ?ImageDataBunch
  2. ??functione-name: 在类名/函数名前加??,然后shift + enter,可以看到源代码。

    1
    ??ImageDataBunch
  3. doc(function-name): 显示函数的定义、docstring和指向文档的链接。

    1
    doc(ImageDataBunch)

训练过程过视化

Tensorboard是tensorflow用来可视化训练过程的一个很好用的工具,遗憾的是之前PyTorch一系一直不能使用它。不过最新版本的fast.ai已经支持使用Tensorboard了,这里需要添加下面一行代码:

1
from fastai.callbacks.tensorboard import LearnerTensorboardWriter

Jupyter魔法操作符

魔法操作符是可以在单元格上运行的函数,并将调用它们的行其余部分作为参数。可以通过在命令前放置’%’符号来调用它们。我把最有用的几个列在下面:

1
%matplotlib inline

眼熟吧,如果你用Jupyter跑过numpy和pandas的数据分析,你肯定使用过它了。此命令确保所有matplotlib绘图将被绘制在笔记本的输出单元格中,并在保存时保存在笔记本中(下次打开同一个ipynb时也会看到)。

1
2
%reload_ext autoreload
%autoreload 2

在执行新行之前重新加载所有模块。如果模块被编辑,则无需重新运行导入命令,模块将自动重新加载。

记住你总是在敲代码前在第一格把这三兄弟都复制进去就可以了。

1
2
3
%matplotlib inline
%reload_ext autoreload
%autoreload 2

关于数据

Oxford-IIIT Pet Dataset是一个宠物图片数据集,这个数据集由牛津大学出品,里面有12种猫和25种狗的图片。我们的模型需要学会区分这37个不同的类别。根据他们的论文,他们在2012年获得的最佳准确率为59.21%,使用的是一种专门用于宠物检测的复杂模型,宠物照片分别使用“图像”、“头部”和“身体”模型。让我们看看使用深度学习的效果吧。

fastai内置了一个获取机器学习常用数据集的函数,untar_data,它会自动下载解压数据。通过开头第二行的import,我们已经可以直接使用它了。我们可以使用URLs.PETS这个名字来直接获得这个数据集。

1
2
path = untar_data(URLs.PETS)
path

这里要说明一下,Fast.ai是国外的框架,所以下载的数据都是从国外下载,由于众所周知的原因,下载速度十分感人,因此需要自己搭个梯子。

别急,还有一个更方便的办法,就是使用前面提到的kaggle kernel。你可以点击下面这个链接(前提是你已经按我前面要求的注册好了kaggle账号,否则点进来了也没法使用)

lesson 1 fast.ai v3: What’s your pet

你可以对path直接使用如下语法:

1
2
3
4
5
# 类似在linux命令行中执行: ls path
path.ls()
# 等效于: path_anno = os.path.join(path, 'annotations')
path_anno = path/'annotations'
path_img = path/'images'

做分析前我们深入了解一下数据,我们总是需要很好地理解问题是什么,数据是什么样子的,然后才能找出解决的办法。查看数据意味着理解数据目录的结构、标签是什么以及一些示例图像是什么样子。

这里推荐使用linux的第三方工具tree,查看文件树形结构,使用前需要安装。在Jupyter Notebook里安装Linux工具可以使用如下办法:

1
2
3
import os
os.system("apt install tree")
os.system("tree")
1
2
3
4
5
6
7
8
9
10
fnames = get_image_files(path_img)
fnames[:5]
'''
显示如下:
[PosixPath('/tmp/.fastai/data/oxford-iiit-pet/images/shiba_inu_11.jpg'),
PosixPath('/tmp/.fastai/data/oxford-iiit-pet/images/pug_74.jpg'),
PosixPath('/tmp/.fastai/data/oxford-iiit-pet/images/Maine_Coon_2.jpg'),
PosixPath('/tmp/.fastai/data/oxford-iiit-pet/images/Bombay_107.jpg'),
PosixPath('/tmp/.fastai/data/oxford-iiit-pet/images/leonberger_13.jpg')]
'''
1
2
3
4
5
6
# 设置随机种子,不用担心前面没有import numpy和re,fast.ai已经代劳了
np.random.seed(2)
pat = re.compile(r'/([^/]+)_\d+.jpg$')
'''
简单解释一下,这个正则表达式能从比如'/tmp/.fastai/data/oxford-iiit-pet/images/leonberger_13.jpg'字符串中取出leonberger这个名字,作为后面的分类的类名
'''

GPU处理数据的速度十分快,同时从硬盘把数据送入到GPU的过程很慢,因此为了减少GPU无效的等待时间,PyTorch提供了dataloader。fast.ai则封装了dataloader,高度集成之后,只需要使用如下代码即可准备好数据了。

1
data = ImageDataBunch.from_name_re(path_img, fnames, pat, ds_tfms=get_transforms(), size=224, bs=16, num_workers=0).normalize(imagenet_stats)

预览一下三行三列的图片

1
data.show_batch(rows=3, figsize=(7,6))
1
2
3
4
5
6
7
print(data.classes)
len(data.classes),data.c
'''
输出:
['Abyssinian', 'Bengal', 'Birman', 'Bombay', 'British_Shorthair', 'Egyptian_Mau', 'Maine_Coon', 'Persian', 'Ragdoll', 'Russian_Blue', 'Siamese', 'Sphynx', 'american_bulldog', 'american_pit_bull_terrier', 'basset_hound', 'beagle', 'boxer', 'chihuahua', 'english_cocker_spaniel', 'english_setter', 'german_shorthaired', 'great_pyrenees', 'havanese', 'japanese_chin', 'keeshond', 'leonberger', 'miniature_pinscher', 'newfoundland', 'pomeranian', 'pug', 'saint_bernard', 'samoyed', 'scottish_terrier', 'shiba_inu', 'staffordshire_bull_terrier', 'wheaten_terrier', 'yorkshire_terrier']
(37, 37)
'''

好了,我们可以开始训练了。限于篇幅,我把接下来的部分放在下篇。

南林笑笑生

南林笑笑生

无论如何,人生是美丽的。

8 日志
© 2020 南林笑笑生
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4