前言
本文主要是针对陈云的PyTorch入门与实践的第八章的内容进行复现,准确地说,是看着他写的代码,自己再实现一遍,所以更多地是在讲解实现过程中遇到的问题或者看到的好的方法,而不是针对论文的原理的进行讲解。对于原理,也只是会一笔带过。原理篇暂时不准备留坑,因为原理是个玄学。
这是我的代码
大神链接:https://github.com/anishathalye/neural-style
这是论文作者写的
问题以及思考
这一次感觉写起来很顺利,数据的处理+基本模型的走读基本只用了两天,剩下的两天主要是耗在了beam_searching上,原理的解析和代码的思考。
现在记录一下这次走读的过程中学习到的东西,如果是和之前的记录有联系,那么则尽量记在一起。
局部反向传播管理
部分参考第八章,基本来自官网文档
一共是四种
- @torch.no_grad()
- with torch.no_grad():
- torch.set_grad_enabled(bool)
- with torch.set_grad_enabled(False):
1 | # 第一种:固定上下文管理器,torch.no_grad()和torch.enable_grad() |
预训练模型的修改
备注:感觉这一块应该是很条理才对,但是没有找到类似的说明
只能等以后见得多了,再做补充,网上有一些对特定模型的修改,但是都不全面,也没有具体说明各个方法的优劣。
应该是这样的,层必须和forward对应,参数的加载可以放在模型定义时,也可以放在模型定义之后。
不修改原模型的forward流程
常用于对特定层的修改1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import torchvision.models as models
model = models.resnet50(pretrained=True)
# 只修改最后一层
# 第一种
fc_features = model.fc.in_features
model.fc = nn.Linear(fc, 9)
# 第二种
resnet50 = tv.models.resnet50(pretrained=True)
del resnet50.fc
resnet50.fc = lambda x: x
# 如果直接修改out_features是没有用的
model.fc.out_features = 9
resnet50.fc.weight.shape
torch.Size([1000, 2048])
即如果修改某一层,要重新定义这一层
在模型内修改forward流程
常用于中间层的增加1
2
3
4
5
6
7
8
9
10
11# 需要自己先定义类似的网络,注意定义的名字必须一致和方式需要一致,利用state_dict来更新参数
import torchvision.models as models
resnet50 = models.resnet50(pretrained=True)
cnn = CNN(Bottleneck, [3, 4, 6, 3])
pretrained_dict = resnet50.state_dict()
model_dict = cnn.state_dict()
# 选取相同名字参数
pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict}
model_dict.update(pretrained_dict)
cnn.load_state_dict(model_dict)
print(cnn)
在模型外增加forward流程
常用与开头或者末尾层的增加1
2
3
4model.add_module('layer_name',layer)
可以理解成
self.layer_name = layer
x = model.layer_name(x)
取特定模块,利用children()和nn.Sequential()也可以实现特定层的修改
这个方法比较啰嗦,不是很推荐,或者不如第一种方法,或者不如最后一种方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
model = models.vgg16(pretrained=True)
removed = list(model.classifier.children())[:-1]
model.classifier = torch.nn.Sequential(*removed)
model.add_module('fc', torch.nn.Linear(4096, out_num)) # out_num是你希望输出的数量
# 直接list(model)是不行的,但是list(model.children())就可以
list(ResNet34.children())
In [23]: for i in ResNet34.children():
...: print(type(i))
...:
<class 'torch.nn.modules.container.Sequential'>
<class 'torch.nn.modules.container.Sequential'>
<class 'torch.nn.modules.container.Sequential'>
<class 'torch.nn.modules.container.Sequential'>
<class 'torch.nn.modules.container.Sequential'>
<class 'torch.nn.modules.linear.Linear'>
取特定模块
利用list和modulelist,可用于对于特定模块的特定操作,可修改forward流程1
2
3
4
5
6
7
8# 第八章的方法 定义新模型, 在模型定义时,加载原模型参数, 修改forward 对于单向的还好,对于有分支的还没有尝试 用了list和modulelist 直接在定义模型的地方取
features = list(vgg16(pretrained=True).features)[:23]
self.features = nn.ModuleList(features).eval()
for ii, model in enumerate(self.features):
x = model(x)
if ii in {3,8,15,22}:
results.append(x)
1 | for k,v in resnet34.named_children(): |
tensor.new和fill_和copy_
在第九章,创建同类型的tensor用new,保证类型和cuda一致,不保证requires_grad,保证了和源类型一致,不共享内存
1 | import torch |
在第十章,创建同类型同样大小同cuda的tensor,用fill,fill也保证了类型和cuda一致,保证了和目标类型一致
1 | In [36]: x = t.Tensor(3,4).cuda() |
第十章的copy_,类型不变,
可以作为计算图进行保留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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86# 不共享内存
In [49]: x = t.Tensor(2,2).fill_(0)
In [50]: x
Out[50]:
tensor([[0., 0.],
[0., 0.]])
In [51]: y = t.Tensor(1,2).fill_(1)
In [52]: y
Out[52]: tensor([[1., 1.]])
In [53]: x[0].copy_(y[0])
Out[53]: tensor([1., 1.])
In [54]: x
Out[54]:
tensor([[1., 1.],
[0., 0.]])
In [55]: y
Out[55]: tensor([[1., 1.]])
In [56]: y[0][0]=2
In [57]: y
Out[57]: tensor([[2., 1.]])
In [58]: x
Out[58]:
tensor([[1., 1.],
[0., 0.]])
# 类型不变
In [60]: y = t.IntTensor(1,2).fill_(1)
In [61]: y
Out[61]: tensor([[1, 1]], dtype=torch.int32)
In [62]: x = t.Tensor(2,2).fill_(0)
In [63]: x
Out[63]:
tensor([[0., 0.],
[0., 0.]])
In [64]: x[0].copy_(y[0])
Out[64]: tensor([1., 1.])
In [65]: x
Out[65]:
tensor([[1., 1.],
[0., 0.]])
# requires_grad,会作为一个计算图保留
In [66]: x
Out[66]:
tensor([[1., 1.],
[0., 0.]])
In [67]: x.requires_grad=True
In [68]: x
Out[68]:
tensor([[1., 1.],
[0., 0.]], requires_grad=True)
In [69]: x[1].copy_(y[0])
Out[69]: tensor([1., 1.], grad_fn=<AsStridedBackward>)
# cuda,可以保留
In [76]: y[0]=2
In [77]: y
Out[77]: tensor([[2, 2]], dtype=torch.int32)
In [78]: x[1].copy_(y[0])
Out[78]: tensor([2., 2.], device='cuda:0', grad_fn=<AsStridedBackward>)
In [79]: x
Out[79]:
tensor([[1., 1.],
[2., 2.]], device='cuda:0', grad_fn=<CopySlices>)
tensor赋值操作 只复制值,不共享内存
1 | # 第一种 利用tensor 只复制值 |
t.save
1 | # 单个变量 不保留名字 |
第十章的诡异装饰器
作者在这里实现了batcha_size的拼接的方式。
具体的函数闭包可以参考python1
2
3
4# def create_collate_fn():
# def collate_fn():
# pass
# return collate_fn
来,猜一下这里为什么这么写,函数闭包,根据昨天看的,函数闭包和类函数有的一拼,或者说可以用于创建多个类似的函数,暂时先这么理解,因为还没有太多的用到,在这里的函数闭包是为了实现对作为函数的参数进行传递变量,也就是把函数作为变量传递,这种思想要注意一下。
设想几种情况。
假设函数h的定义是这样的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15In [1]: def h(x, f):
...: """
...: Args:
...: x: int
...: f: function
...: """
...: out = f(x)
...: return out
In [2]: def f(x):
...: return 2*x
In [3]: h(2,f)
Out[3]: 4
第一种情况,函数f的所有输入都是h可以给的,那么这时候如上所示,直接定义一个函数,然后把函数名或者其他等于函数的变量传进去就可以。
第二种情况,函数f的有一部分变量,需要是外界给的,即f的定义中,引用到了不属于h的输入的变量。就像这样。1
2
3
4
5
6
7
8
9
10
11In [4]: def g(i):
...: def f(x):
...: return i*x
...: return f
...:
...:
In [5]: ff = g(3)
In [6]: h(4,ff)
Out[6]: 12
那么这个时候函数闭包就可以很好地实现这种想法。
这是暂时对于函数闭包的理解,但我知道这种想法肯定是有问题的。
rnn的pack和pad
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence1
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
li_ = [[3,4,2,1],[3,4,2,1],[3,4,2,1],[3,4,2,0],[3,4,0,0]]
ten = t.Tensor(li_).long()
pad_variable = ten
embedding = nn.Embedding(5,2)
pad_embeddings = embedding(pad_variable)
lengths = [5,5,4,3]
pad_embeddings
pad_embeddings
tensor([[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[-0.7936, 0.9621]],
[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[-0.7936, 0.9621]],
[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[-0.7936, 0.9621]],
[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[ 0.5581, 0.7382]],
[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[ 0.5581, 0.7382],
[ 0.5581, 0.7382]]])
packed_variable = pack_padded_sequence(pad_embeddings, lengths)
PackedSequence(data=tensor([[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[-0.7936, 0.9621],
[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[-0.7936, 0.9621],
[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[-0.7936, 0.9621],
[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[ 0.0256, -1.6445],
[-0.0939, -0.4070]]), batch_sizes=tensor([ 4, 4, 4, 3, 2]))
packed_variable.data.shape
torch.Size([17, 2])
rnn = nn.LSTM(2,3)
output, hn = rnn(packed_variable)
output
PackedSequence(data=tensor([[-0.1698, -0.1311, 0.2030],
[-0.0984, -0.0693, 0.1601],
[-0.0791, -0.1195, 0.2111],
[-0.0175, 0.0069, 0.0978],
[-0.2580, -0.1868, 0.3193],
[-0.1392, -0.0959, 0.2441],
[-0.1221, -0.1489, 0.3270],
[-0.0223, 0.0109, 0.1334],
[-0.3011, -0.2100, 0.3821],
[-0.1544, -0.1061, 0.2877],
[-0.1452, -0.1551, 0.3886],
[-0.0232, 0.0129, 0.1460],
[-0.3222, -0.2195, 0.4168],
[-0.1593, -0.1098, 0.3109],
[-0.1575, -0.1556, 0.4222],
[-0.3325, -0.2233, 0.4370],
[-0.1603, -0.1111, 0.3235]]), batch_sizes=tensor([ 4, 4, 4, 3, 2]))
hn[1].shape
torch.Size([1, 4, 3])
pad_packed_sequence(packed_variable)
(tensor([[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[-0.7936, 0.9621]],
[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[-0.7936, 0.9621]],
[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[-0.7936, 0.9621]],
[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[-1.4719, -0.4871],
[ 0.0000, 0.0000]],
[[ 0.0256, -1.6445],
[-0.0939, -0.4070],
[ 0.0000, 0.0000],
[ 0.0000, 0.0000]]]), tensor([ 5, 5, 4, 3]))
1 | # embedding_dim=3, seq_len=4,3 batch_size=2 即把两句话a,b作为一个batch,空余补0 |
1 | # a,b两句话,分别有3,2个词,batch_size=2, 共有3个batch_size,大小分别是2,2,1 |
beam_searching
[参考链接]https://blog.csdn.net/xljiulong/article/details/51554780
[参考链接]http://jhave.org/algorithms/graphs/beamsearch/beamsearch.shtml
网上讲的大部分都有各自的问题,不是很清晰,只有那篇英文才是标准的,这哥们应该是翻译的,还不错
作者使用的是beam_searching的变种,原理类似,但是条件不一致,具体的在代码注释中,不再陈述。
第十章和第九章关于生成语句的流程的区别
第十章和第九章在模型生成的地方有两个点不一样,
第九章的模型本身可以进行正常的输入与输出,所以第九章也写成这个样子
输入(LongTensor) 11 输出 tensor 1vocabsize1
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
33results = list(start_words)
start_word_len = len(start_words)
for i in range(opt.max_gen_len):
output, hidden = model(input, hidden)
if i < start_word_len:
w = results[i]
input = input.data.new([word2ix[w]]).view(1,1)
else:
# output size 1×vocab_size [[1,2,3,...]]
# 这里应该看一下,输出output是个什么东西
top_index = output.data[0].topk(1)[1][0].item()
w = ix2word[top_index]
results.append(w)
input = input.data.new([word2ix[w]]).view(1,1)
if w == '<EOP>':
del results[-1]
break
return results
# 简写为
# model: embedder rnn classifier
results = []
for i in range(opt.max_gen_len):
output, hidden = model(input, hidden) # input(tensor) 1*1 output(tensor) 1*vocabsize hidden(tensor) 1*1*hidden_dim
top_index = output.data[0].topk(1)[1][0].item()
w = ix2word(top_index)
results.append(w)
input = input.data.new(word2ix(w)).view(1,1)
return results
第十章因为使用了pack_padded_sequence来加速训练,那么训练的模型就不能直接拿来像第九章进行生成,另外第一个字是图片特征的转化而成的,不需要embedding层,而是需要fc层,其实也可以直接拿来用,把captions设置为空就好了,在这里作者没有直接用,直接用好像比较麻烦。而是采用beam_search中把rnn和classifier层传进去,写了一个标准的beam_search函数,即输入是第一个字,输出是beam_size句话,因为设计到其他选词保留的问题,所以直接传入的的是各个分函数,进行自行拼接。也可能是为了复用logprobs = nn.functional.log_softmax(output, dim=1) ## 暂时不清楚这里为什么用log_softmax,是负数啊,大哥,不过大小好像不变
在rnn中有一个问题,就是能不能用t.no_grad,会不会影响其向前传播。
对数据的预处理
第九章是把对数据的预处理写在了data里面,但事实上,这个数据预处理应该与主模型分开,是属于前一个过程。有什么需要交互的,也是通过文件进行,包括配置。
新建立的数据结构的对比大小
1 | class Caption(object): |
作者在固定长度的列表中,使用了小顶堆的方式,来保证每次取出去的都是最小的。
存在问题
现在还有一个问题就是当一个.py文件里的函数或者类超过2、3个时,应该以什么的方式注释才能更好地让别人知道这个文件里的函数和怎么干的。
总结
第十章的代码在难度上其实已经感觉下降了好多,当然自己又忘了写requires.txt。但是在调试改bug自己就用了三天。其中的bug有的时候自己已经忘记当初是怎么写的了,尴尬。
自己训练出来的模型也没有作者声称的那么好,暂时不知道