Onwaier's Blog

大步后退亦或停止不前,不如匍匐前进

0%

参考资料

  1. 官方文档
  2. 10分钟教程掌握Python调试器pdb
  3. Python 必备 debug 神器:pdb
  4. PDB——Python调试利器详解

简介

pdb是python的内置模块,类似c++中的gdb的存在,可用于在命令行中对代码进行调试。

进入调试

  1. 修改代码

The typical usage to break into the debugger from a running program is to insert at the location you want to break into the debugger.

在需要调试的位置插入以下代码,运行的时候,会在插入位置停止进入调试模式。

1
2
import pdb
pdb.set_trace()
  1. 脚本执行调试

pdb.py can also be invoked as a script to debug other scripts

1
2
# myscript.py为要调试的程序
python3 -m pdb myscript.py

推荐使用方法2,重要的是不会添加额外的代码污染代码,更加灵活。

常用命令

命令

功能

_断点有关的命令_

b(break)

查看已设的所有断点

b no

设置断点,no是断点所在的行号

b filename:no

将断点添加到某个文件中

b funcname

为函数funcname的第一行设置断点

tbreak(break)

同b,临时断点,执行一次自动清除

tbreak no

tbeak filename:no

tbreak funcname

disable bno

禁用指定序号断点,但未删除

enable bno

启用指定序号断点

cl

清除所有断点(包括临时断点)

cl bno1[bno2 ……]

清除指定序号的断点,多个用空格分隔

cl filename:no

清除某文件所在行号的断点

_调试相关的命令_

s(step)

执行下一句,遇到函数会进入到函数内部执行

n(next)

执行下一句,不会进入函数

r(return)

执行当前所在函数的返回处

c(continue)

执行到下一个断点处

unt(until)

退出当前循环或堆栈,遇到断点会停止

unt no

执行到指定行号处停止

_查看代码或变量值_

l(list)

列出当前执行行周围的11行代码

l no

列出指定行号周围的11行代码

l no1 no2

列出no1 no2间的行号

ll

列出所有代码

p exp

打印某变量或表达式的值

pp exp

好看一点打印某变量或表达式的值

a

查看所在函数的参数与参数值

what is var

查看变量的类型

_其它_

restart

重新运行

run

类似restart

q(quit)

退出调试

interact

启动一个python的交互式解释器,使用当前代码的全局命名空间 ctrl + d退出

参考资料

  1. pytorch 模型部分参数的加载
  2. Pytorch中,只导入部分模型参数的做法
  3. Correct way to freeze layers
  4. Pytorch自由载入部分模型参数并冻结
  5. pytorch冻结部分参数训练另一部分
  6. PyTorch更新部分网络,其他不更新
  7. Pytorch固定部分参数(只训练部分层)

加载部分参数

如果加载现有模型的所有参数,我们常使用的是代码如下:

1
torch.load(model.state_dict())

在训练过程中,我们常常会使用预训练模型,有时我们是在自己的模型中加入别人的某些模块,或者对别人的模型进行局部修改,这个时候再使用torch.load(model.state_dict()),就会出现类似这些的错误:RuntimeError: Error(s) in loading state_dict for Net:Missing key(s) in state_dict:xxx。出现这个错误就是某些参数缺失或者不匹配。

保持原来网络层的名称和结构不变

现有模型中引入的那部分网络结构的网络层的名称和结构保持不变,这时候加载参数的代码很简单。

1
2
3
4
5
6
7
8
9
10
11
12
# 加载引入的网络模型
model_path = "xxx"
checkpoint = torch.load(os.path.join(model_path, map_location=torch.device('cpu'))
pretrained_dict = checkpoint['net']
# 获取现有模型的参数字典
model_dict = model.state_dict()
# 获取两个模型相同网络层的参数字典
state_dict = {k:v for k,v in pretrained_dict.items() if k in model_dict.keys()}
# update必不可少,实现相同key的value同步
model_dict.update(state_dict)
# 加载模型部分参数
model.load_state_dict(model_dict)

引入的网络层名称发生修改

这个时候再直接使用上面的加载方法,会导致部分key的value无法实现更新。 我就曾在这个位置犯过很严重的错误。首先我定义了AttentionResNet,这是一个UNet来实现图像分割,然后在另一个模型中我使用了这个模型self.attention_map = AttentionResNet(XXX)。因为我在引用的过程中并没有对AttentionResNet那部分代码进行修改,所以本能的觉得这部分网络层的名称是相同的,所以加载这部分参数时,我直接使用了上面的方法。这个错误隐藏了差不多一个星期。直到我开始冻结这部分参数进行训练时,发现情况不对。因为我在输出attention_map的特征图时,我发现它是一张全黑图(像素全为0),这表示加载的参数不对,然后我尝试输出pretrained_dict时,它是一个空字典。然后继续输出pretrained_dict.keys()(未修改之前的pretrained_dict)和model_dict.keys()发现预期相同的那部分key中都多了一部分attention_map.。问题主要出在self.attention_map = AttentionResNet(XXX)这一句,它使原有的网络层名称都加了个前缀attention_map.,知道了错误,修改起来很简单。

1
2
3
4
5
6
7
8
9
10
11
12
# 加载引入的网络模型
model_path = "xxx"
checkpoint = torch.load(os.path.join(model_path, map_location=torch.device('cpu'))
pretrained_dict = checkpoint['net']
# 获取现有模型的参数字典
model_dict = model.state_dict()
# 获取两个模型相同网络层的参数字典
state_dict = {'attention_map.' + k:v for k,v in pretrained_dict.items() if 'attention_map.' + k in model_dict.keys()}
# update必不可少,实现相同key的value同步
model_dict.update(state_dict)
# 加载模型部分参数
model.load_state_dict(model_dict)

其实我这个位置的修改有点投机,更加常规的方法是: 引用自Pytorch自由载入部分模型参数并冻结

我们看出只要构建一个字典,使得字典的keys和我们自己创建的网络相同,我们在从各种预训练网络把想要的参数对着新的keys填进去就可以有一个新的state_dict了,这样我们就可以load这个新的state_dict,这是最普适的方法适用于所有的网络变化。

先输出两个模型的参数字典,观察需要加载的那部分参数所处的位置,然后利用for循环构建新的字典。

冻结参数

  1. 将需要固定的那部分参数的requires_grad置为False.
  2. 在优化器中加入filter根据requires_grad进行过滤.

ps: 解决AttributeError: ‘NoneType’ object has no attribute ‘data’问题的一种思路就是冻结参数,参考博客 代码如下:

1
2
3
4
5
6
# requires_grad置为False
for p in net.XXX.parameters():
p.requires_grad = False

# filter
optimizer.SGD(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3)

当需要冻结的那部分参数的网络层名称不太明确时,可以采用pytorch冻结部分参数训练另一部分的思路,打印出所有网络层,通过参数名称进行冻结。

5340. 统计有序矩阵中的负数

题目描述

给你一个 m * n 的矩阵 grid,矩阵中的元素无论是按行还是按列,都以非递增顺序排列。 请你统计并返回 grid负数 的数目。 示例 1:

输入:grid = [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]]
输出:8
解释:矩阵中共有 8 个负数。

示例 2:

输入:grid = [[3,2],[1,0]]
输出:0

示例 3:

输入:grid = [[1,-1],[-1,-1]]
输出:3

示例 4:

输入:grid = [[-1]]
输出:1

题解

遍历二维数组,统计负数的个数即可。 时间复杂度为:$O(n^2)$ 空间复杂度为:$O(1)$

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int countNegatives(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
int cnt = 0;
for(int i = 0; i < m; ++i){
for(int j = 0; j < n; ++j){
if(grid[i][j] < 0){//统计负数的个数
++cnt;
}
}
}
return cnt;
}
};

5341. 最后 K 个数的乘积

题目描述

请你实现一个「数字乘积类」ProductOfNumbers,要求支持下述两种方法: 1. add(int num)

  • 将数字 num 添加到当前数字列表的最后面。
  1. getProduct(int k)
  • 返回当前数字列表中,最后 k 个数字的乘积。
  • 你可以假设当前列表中始终 至少 包含 k 个数字。

题目数据保证:任何时候,任一连续数字序列的乘积都在 32-bit 整数范围内,不会溢出。 示例:

输入:
[“ProductOfNumbers”,”add”,”add”,”add”,”add”,”add”,”getProduct”,”getProduct”,”getProduct”,”add”,”getProduct”]
[[],[3],[0],[2],[5],[4],[2],[3],[4],[8],[2]]

输出:
[null,null,null,null,null,null,20,40,0,null,32]

解释:
ProductOfNumbers productOfNumbers = new ProductOfNumbers();
productOfNumbers.add(3); // [3]
productOfNumbers.add(0); // [3,0]
productOfNumbers.add(2); // [3,0,2]
productOfNumbers.add(5); // [3,0,2,5]
productOfNumbers.add(4); // [3,0,2,5,4]
productOfNumbers.getProduct(2); // 返回 20 。最后 2 个数字的乘积是 5 * 4 = 20
productOfNumbers.getProduct(3); // 返回 40 。最后 3 个数字的乘积是 2 * 5 * 4 = 40
productOfNumbers.getProduct(4); // 返回 0 。最后 4 个数字的乘积是 0 * 2 * 5 * 4 = 0
productOfNumbers.add(8); // [3,0,2,5,4,8]
productOfNumbers.getProduct(2); // 返回 32 。最后 2 个数字的乘积是 4 * 8 = 32

题解1

周赛的时候没有多想,上来直接用vector来存储所有的数,add函数用push_back来实现。最后k个元素乘积直接从vector的最后一个元素开始,将k个元素乘起来即可。 时间复杂度为:O(n) 空间复杂度为:O(n)

代码1

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
class ProductOfNumbers {
public:
vector<int>vec;
ProductOfNumbers() {

}

void add(int num) {
vec.push_back(num);
}

int getProduct(int k) {
int len = vec.size(), res = 1;
for(int i = len - 1; i >= len - k; --i){
res = res * vec[i];
}
return res;
}
};

/**
* Your ProductOfNumbers object will be instantiated and called as such:
* ProductOfNumbers* obj = new ProductOfNumbers();
* obj->add(num);
* int param_2 = obj->getProduct(k);
*/

题解2

参照LeetCode - 圈子,对于第二题给出了许多优化的算法。题目中给出的限制条件需要注意。

题目数据保证:任何时候,任一连续数字序列的乘积都在 32-bit 整数范围内,不会溢出。 0 <= num <= 100

对于任一连续序列的乘积都小于32位整数范围,表示序列中最多只有31个数大于1($2^{31} - 1$),其它数字都是0和1。所以可以用前缀和来统计0-100这101个数字在序列中出现的个数,这里空间开销是101 * n(n是序列的总长度)。计算最后K个数的乘积,只需要先找出每个数字num出现的次数cnt,$cnt_{num} = vec[n][num] - vec[n - k][num]$,出现0,最后乘积就为0,出现1,则跳过1不算。然后再求其它数字乘积即可。 这里我介绍前缀积的思路。数组存储连续数字的乘积,遇到0,清除数组,重新统计。当最后数组长度不足K时,表示乘积为0,否则就用总乘积除以前面元素的乘积。 时间复杂度为:O(1) 空间复杂度为:O(n)

代码2

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
class ProductOfNumbers {
public:
vector<int>vec;
ProductOfNumbers() {
vec.clear();
vec.push_back(1);
}

void add(int num) {
if(num == 0){//遇到0,清空前缀积数组
vec.clear();
vec.push_back(1);
}
else{
vec.push_back(vec.back() * num);
}
}
int getProduct(int k) {
if(vec.size() - 1 < k){//数组长度不足K表示,最后K个数中存在0
return 0;
}
else{
return vec[vec.size() - 1] / vec[vec.size() - 1 - k];
}
}
};

/**
* Your ProductOfNumbers object will be instantiated and called as such:
* ProductOfNumbers* obj = new ProductOfNumbers();
* obj->add(num);
* int param_2 = obj->getProduct(k);
*/

1353. 最多可以参加的会议数目

题目描述

给你一个数组 events,其中 events[i] = [startDayi, endDayi] ,表示会议 i 开始于 startDayi ,结束于 endDayi 。 你可以在满足 startDayi <= d <= endDayi 中的任意一天 d 参加会议 i 。注意,一天只能参加一个会议。 请你返回你可以参加的 最大 会议数目。 示例 1:

输入:events = [[1,2],[2,3],[3,4]]
输出:3
解释:你可以参加所有的三个会议。
安排会议的一种方案如上图。
第 1 天参加第一个会议。
第 2 天参加第二个会议。
第 3 天参加第三个会议。

示例 2:

输入:events= [[1,2],[2,3],[3,4],[1,2]]
输出:4

示例 3:

输入:events = [[1,4],[4,4],[2,2],[3,4],[1,1]]
输出:4

示例 4:

输入:events = [[1,100000]]
输出:1

示例 5:

输入:events = [[1,1],[1,2],[1,3],[1,4],[1,5],[1,6],[1,7]]
输出:7

题解

贪心+优先队列 首先将区间按开始时间进行排序,然后结束时间早的先安排。还需要鼗安排冲突,不合理的去掉。 时间复杂度为:O(nlogn) 空间复杂度为:O(n)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int maxEvents(vector<vector<int>>& events) {
priority_queue<int, vector<int>, greater<int>>que;
int cnt = 0, idx = 1, i = 0;
//先排序
sort(events.begin(), events.end());
while(i < events.size() que.size() > 0){
while(i < events.size() && events[i][0] == idx){//开始时间为idx的,均添加到优先队列
que.push(events[i][1]);
++i;
}
while(que.size() > 0 && que.top() < idx){//去除安排冲突的
que.pop();
}
if(que.size() > 0){//优先安排结束时间早的
++cnt;
que.pop();
}
++idx;
}
return cnt;
}
};

5343. 多次求和构造目标数组

题目描述

给你一个整数数组 target 。一开始,你有一个数组 A ,它的所有元素均为 1 ,你可以执行以下操作:

  • x 为你数组里所有元素的和
  • 选择满足 0 <= i < target.size 的任意下标 i ,并让 A 数组里下标为 i 处的值为 x
  • 你可以重复该过程任意次

如果能从 A 开始构造出目标数组 target ,请你返回 True ,否则返回 False 。 示例 1:

输入:target = [9,3,5]
输出:true
解释:从 [1, 1, 1] 开始
[1, 1, 1], 和为 3 ,选择下标 1
[1, 3, 1], 和为 5, 选择下标 2
[1, 3, 5], 和为 9, 选择下标 0
[9, 3, 5] 完成

示例 2:

输入:target = [1,1,1,2]
输出:false
解释:不可能从 [1,1,1,1] 出发构造目标数组。

示例 3:

输入:target = [8,5]
输出:true

题解

正向思考,不知道用所有的数字之和去替换哪一个数字,尝试的话可能性太多,实现难度太大。反向思考的话,数组中最大的数字显然是上一个数组中所有数字之和,而它所替换的数字等于最大的数字减去其它数字之和。如此替换下去,如果最后所有的数字都为1,表示可以构造目标数组,一旦出现数字0或负数,表示不能构造。

代码

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
class Solution {
public:
bool isPossible(vector<int>& target) {
long long int sum = 0;
int rep_num = 0;
bool flag = true;
priority_queue<int>que;
for(int i = 0; i < target.size(); ++i){
sum += target[i];
que.push(target[i]);
}
while(que.top() != 1){
sum = sum - que.top();//除去最大数字其它数字之和
if(sum == 1){
flag = true;
break;
}
/*
这里用除法而不用减法是为了处理像[100000, 1]这样的例子超时的问题
用除法,直接使当前的最大数字小于其它数字之和,避免多轮最大数字都是同一个。
*/
if(que.top() / sum == 0 (que.top() % sum == 0)){//出现负数和0
flag = false;
break;
}
else{
que.push(que.top() % sum);
sum = sum + que.top() % sum;//上一个数组的所有元素之和
que.pop();
}
}
if(flag){
return true;
}
else{
return false;
}
}
};

参考资料

  1. 个人博客 - Pytorch同时对输入输出使用transform
  2. 知乎 - 4个例子让你的pytorch数据增强过程不随机
  3. CSDN - Tensorflow如何对两幅图像做同样的数据增广操作
  4. Pytorch官方文档 - torchvision.transforms.functional

问题描述

原始的人脸表情识别训练过程中,对于每张输入的图片,都有一个表情标签与之对应(这里只考虑单标签)。可以直接使用torchvision.transform中的transforms.RandomCroptransforms.RandomHorizontalFliptransforms.RandomRotation等对图片进行随机操作,从而实现对数据的增广操作,提高模型的泛化能力。 但在人脸表情识别中引入Attention机制后,对于每张输入图片,则对应一个脸部分割的概率图。在对原始图片进行transform的变换时,与之对应的脸部分割图mask应与之进行相同的变化。但是上面提到的所有操作都是随机操作,涉及一个随机值。所以,无法做到输入与mask进行相同的transform。所以,决定放弃使用数据增广的操作,而是直接使用的是原图,训练过程中出现train的准确率不断上升,最后达到100%,而test的准确率则开始是上升,中间出现波动。最后开始下降。很显然,模型过拟化。为了解决这个问题,相当于解决如何用Pytorch对两张图片进行相同的数据增广操作的问题?

解决方法

在pytorch文档上可以看到,除了torchvision.transform 还有torchvision.transforms.functional。 相比transform ,transforms.functional 更加灵活,该方法只提供了图像的增强变换功能,而并没有随机部分

引用自资料1 实际上是用torchvision.transforms.functional中提供的函数(都没有引入随机性),来自定义transform函数。

示例

原图及脸部分割mask如图所求 然后对图片同时进行随机旋转和翻转,代码如下

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
import random
import cv2
from torchvision import transforms
import torchvision.transforms.functional as tf
from PIL import Image

def my_transform1(image, mask):

# 拿到角度的随机数。angle是一个-180到180之间的一个数
angle = transforms.RandomRotation.get_params([-180, 180])
# 对image和mask做相同的旋转操作,保证他们都旋转angle角度
image = image.rotate(angle)
mask = mask.rotate(angle)

image = tf.to_tensor(image)
mask = tf.to_tensor(mask)
return image, mask

def my_transform2(image, mask):
# 50%的概率应用垂直,水平翻转。
if random.random() > 0.5:
image = tf.hflip(image)
mask = tf.hflip(mask)
if random.random() > 0.5:
image = tf.vflip(image)
mask = tf.vflip(mask)
image = tf.to_tensor(image)
mask = tf.to_tensor(mask)
return image, mask

def my_transform3(image, mask):
# 随机裁剪
i, j, h, w = transforms.RandomResizedCrop.get_params(
image, scale=(0.7, 1.0), ratio=(1, 1))
image = tf.resized_crop(image, i, j, h, w, 48)
mask = tf.resized_crop(mask, i, j, h, w, 48)
image = tf.to_tensor(image)
mask = tf.to_tensor(mask)
return image, mask

# transform处理对象一般都是 PIL Image

image = Image.open('./0.jpg') # 原图
mask = Image.open('./0_seg_face.jpg') # mask

image_tensor, mask_tensor = my_transform1(image, mask)

#此时image, mask为tensor,需转成PIL Image
image_rotate = transforms.ToPILImage()(image_tensor).convert('L').save('0_rotate.jpg')
mask_rotate = transforms.ToPILImage()(mask_tensor).convert('L').save('0_seg_face_rotate.jpg')

image_tensor, mask_tensor = my_transform2(image, mask)

#此时image, mask为tensor,需转成PIL Image
image_flip = transforms.ToPILImage()(image_tensor).convert('L').save('0_flip.jpg')
mask_flip = transforms.ToPILImage()(mask_tensor).convert('L').save('0_seg_face_flip.jpg')

image_tensor, mask_tensor = my_transform3(image, mask)

#此时image, mask为tensor,需转成PIL Image
image_crop = transforms.ToPILImage()(image_tensor).convert('L').save('0_crop.jpg')
mask_crop = transforms.ToPILImage()(mask_tensor).convert('L').save('0_seg_face_crop.jpg')

旋转后图片为(每次旋转结果可能都不一样,角度随机) 50%可能性翻转后图片为 随机裁剪,裁剪后大小为原来的[0.8,1],长宽比例为1:1,最后并将resize为(48, 48),图片为:

参考资料

  1. Python TypeError: No conversion path for dtype: dtype(‘
  2. TypeError: Object dtype dtype(‘O’) has no native HDF5 equivalent
  3. python - 使用不同大小的h5py数组进行保存

错误描述

在对h5文件写的过程中,首先遇到了错误OSError: Cannot write data (no appropriate function for conversion path),网上搜索之后,与之相关的问题很少,大部分提到的是字符串编码问题,参照资料1,对字符串的编码修改,但错误依旧。在这个地方卡了很长时间,一直检查数据类型哪里是不是有问题?最后尝试性,将最后创建数据集中的dtype去掉,即置为None。即将

1
datafile.create_dataset("PrivateTest_gt", dtype = 'uint8', data=PrivateTest_z)

改为

1
datafile.create_dataset("PrivateTest_gt", data=PrivateTest_z)

问题出现了转机,提示的错误不一样了,TypeError: Object dtype dtype('O') has no native HDF5 equivalent,再查询这个问题上,发现很多博客,如资料2资料3提到出现错误的原因是:

要存储的数据中存在维度不一致的数据

具体datafile.create_dataset("PrivateTest_gt", data=PrivateTest_z)一行中,PrivateTest_z列表中存储着两种shape的图片数据,一种是48 * 48,而另一种是128 * 128。这种情况h5py无法统一处理。

解决方法

参照资料,解决这个问题常见有两种。

  • 散装

将相同维度的数据放在同一个dataset中,即把原始数据拆分成多个dataset存储

  • 统装

统一数据的维度。我用的就是这种方法,我将所有图片的数据都统一成128 * 128(对于48的resize),即解决上述问题。

参考资料

  1. Python迭代DataLoader时出现TypeError: Caught TypeError in DataLoader worker process 0.错误。

问题描述

用构造的数据集去训练网络过程,迭代DataLoader出现TypeError的错误,Caught TypeError in DataLoader worker process 0

解决问题

参考资料1),出错原因是数据集的标签缺失,不完整。 查看自己的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
files = os.listdir(anger_path)
files.sort()
for filename in files:
I = skimage.io.imread(os.path.join(anger_path,filename))
image = cv2.imread(os.path.join(anger_path,filename))
res, flag = get_mask(image)
data_x.append(I.tolist())
data_y.append(0)


if flag == True:
res = res * 255
seg_image_data = res.tolist()
data_z.append(seg_image_data)
imageCount = imageCount + 1
print(imageCount)

data_x与data_y的数据是直接添加的,而data_z是经过if判断的,False时,就不会添加。这样就导致某一条记录里面没有data_z这个标签,为了解决这个问题,需将data_x和data_y的数据添加移到if条件中,实现data_x,data_y和data_z三个同步添加。即

1
2
3
4
5
6
7
8
9
10
11
12
13
14
files = os.listdir(anger_path)
files.sort()
for filename in files:
I = skimage.io.imread(os.path.join(anger_path,filename))
image = cv2.imread(os.path.join(anger_path,filename))
res, flag = get_mask(image)
if flag == True:
data_x.append(I.tolist())
data_y.append(0)
res = res * 255
seg_image_data = res.tolist()
data_z.append(seg_image_data)
imageCount = imageCount + 1
print(imageCount)

参考资料

  1. issue
  2. pytorch: grad is None while training
  3. pytorch冻结部分参数训练另一部分

问题描述

将别人模型替换成自己的模型,训练过程中出现错误param.grad.data.clamp_(-grad_clip, grad_clip) AttributeError: 'NoneType' object has no attribute 'data'

解决方法

上网查询这个问题,大部分回答都是模型中定义的某个层没有参与到前向传播,所以反向传播,计算loss时,grad is None。主要是要找到未参与计算的层,并注释掉。 这个问题困扰我很久,因为始终觉得没有多余的层。 下面的代码定义一个AttentionResNet模型。后来才发现罪魁祸首是self.encoder,这所以前面一直没有注意到它,是因为后面的层的定义用到self.encoder中的某个层或某几个层组合。

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
class AttentionResNet(nn.Module):

def __init__(self, in_channels=3, out_channels=1, num_filters=32, encoder_depth=34, pretrained=True):
super(AttentionResNet, self).__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.num_filters = num_filters

if encoder_depth == 34:
self.encoder = torchvision.models.resnet34(pretrained=pretrained)
bottom_channel_nr = 512
elif encoder_depth == 101:
self.encoder = torchvision.models.resnet101(pretrained=pretrained)
bottom_channel_nr = 2048
elif encoder_depth == 152:
self.encoder = torchvision.models.resnet152(pretrained=pretrained)
bottom_channel_nr = 2048
else:
raise NotImplementedError('only 34, 101, 152 version of Resnet are implemented')

#attention module
self.pool = nn.MaxPool2d(2, 2)
self.relu = nn.ReLU(inplace=True)
self.conv1 = nn.Sequential(self.encoder.conv1, self.encoder.bn1, self.encoder.relu, self.pool)
self.conv2 = self.encoder.layer1
self.conv3 = self.encoder.layer2
self.conv4 = self.encoder.layer3
self.conv5 = self.encoder.layer4

self.center = DecoderBlockV2(bottom_channel_nr, num_filters * 8 * 2, num_filters * 8)
self.dec5 = DecoderBlockV2(bottom_channel_nr + num_filters * 8, num_filters * 8 * 2, num_filters * 8)
self.dec4 = DecoderBlockV2(bottom_channel_nr // 2 + num_filters * 8, num_filters * 8 * 2, num_filters * 8)
self.dec3 = DecoderBlockV2(bottom_channel_nr // 4 + num_filters * 8, num_filters * 4 * 2, num_filters * 2)
self.dec2 = DecoderBlockV2(bottom_channel_nr // 8 + num_filters * 2, num_filters * 2 * 2, num_filters * 2 * 2)
self.dec1 = DecoderBlockV2(num_filters * 2 * 2, num_filters * 2 * 2, num_filters)

self.attention_map = nn.Sequential(
ConvRelu(num_filters, num_filters),
nn.Conv2d(num_filters, 1, kernel_size=1)
)



def forward(self, x):

#attention module
conv1 = self.conv1(x)
conv2 = self.conv2(conv1)
conv3 = self.conv3(conv2)
conv4 = self.conv4(conv3)
conv5 = self.conv5(conv4)

pool = self.pool(conv5)
center = self.center( pool )
dec5 = self.dec5(torch.cat([center, conv5], 1))
dec4 = self.dec4(torch.cat([dec5, conv4], 1))
dec3 = self.dec3(torch.cat([dec4, conv3], 1))
dec2 = self.dec2(torch.cat([dec3, conv2], 1))
dec1 = self.dec1(dec2)

#attention map
x = self.attention_map( dec1 )
return x

找到了出错的位置,如何修改呢?参照资料3,首先将self.encoder中的参数的requires_grad都置为False,然后在优化器中加入过滤器,只更新requires_grad = True的参数。

1
2
3
4
for param in net.encoder.parameters():
param.requires_grad = False

optimizer = optim.SGD(filter(lambda p: p.requires_grad, net.parameters()), lr=opt.lr, momentum=0.9, weight_decay=5e-4)

找了2天,终于找到错误的位置,太难了。

参考资料

  1. Floyd判圈算法
  2. Brent’s Cycle Detection Algorithm
  3. Floyd判圈算法(龟兔赛跑算法, Floyd’s cycle detection)及其证明
  4. 算法-floyd判环(圈)算法

算法解读

Floyd判圈算法(Floyd Cycle Detection Algorithm),又称龟兔赛跑算法(Tortoise and Hare Algorithm),是一个可以在有限状态机、迭代函数或者链表上判断是否存在环,求出该环的起点与长度的算法。该算法据高德纳称由美国科学家罗伯特·弗洛伊德发明。

引用自维基百科-Floyd判圈算法

问题引入

如何检测一个链表是否有环(循环节),如果有,那么如何确定环的起点以及环的长度。

引用自博客 上述问题是一个经典问题,经常会在面试中被问到。我之前在杭州一家网络公司的电话面试中就很不巧的问到,当时是第一次遇到那个问题(毕竟太菜,没有专门准备过算法面试),我思考片刻,问答的是用一个哈希表存储访问的节点的地址,当访问某节点时,发现哈希表中已存在,表明链表中存在环。面试官听了我的回答就反问了我一句:如果链表的环很大,那么哈希表的空间消耗就很大,你的方法并不实用。你能在不消耗额外空间的情况下,找到链表的环吗?当时,想了很久没想到,面试官就说可以这样做,balabala…

算法描述

  • 形象化解释

解决上述问题的方法就是我们常说的快慢指针。乌龟与兔子在一个含环的跑道上(如图所求)进行比赛,乌龟在单位时间内移动一步,而兔子则在单位时间内移动两步,其移动速度是乌龟的两倍。乌龟与兔子同时从A点出发,兔子移动的快先行进入环形跑道,但那以后,一直在转圈。所以如果存在环的话,乌龟总能与兔子相遇(首次相遇在C点),甚至还能跑到兔子前面:joy:。

  • 判断是否有环

定义两个指针p1与p2,起始时,都指向链表的起点A,p1每次移动1个长度,p2每次移动2个长度。如果p2在移到链表的尾端时,并未与p1相遇,表明链表中不存在环。如果p1与p2相遇在环上的某一点C,表明链表有环。

  • 环的长度

将指针p1固定在相遇位置C,移动p2,每次移动1个长度,并用变量cnt计数。当p2再次与p1相遇时,此时cnt的值就是链表的长度。

  • 环的起点

环的起点即图中点B,将指针p1指向链表的起始位置A,指针p2仍在位置C,指针p1与p2每次均移动一个单位,p1与p2再次相遇的位置就是环的起点位置点B。

代码

  • 伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
t := &S
h := &S //令指针t和h均指向起始节点S。
repeat
t := t->next
h := h->next
if h is not NULL //要注意这一判断一般不能省略
h := h->next
until t = h or h = NULL
if h != NULL //如果存在环的話
n := 0
repeat //求环的度
t := t->next
n := n+1
until t = h
t := &S //求环的一个起点
while t != h
t := t->next
h := h->next
P := *t

引用自维基百科

  • c++代码
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
#include <bits/stdc++.h>

struct Node//链表节点
{
int data;
Node * next;
};

bool findCircle(Node * list){
Node* p1 = list->next, *p2;//p1移动1个单位
bool flag;
if(p1 == NULL){//链表只有1个节点
return false;
}
p2 = p1 -> next;//p2移动两个单位
while(p1 != p2 && p2 != NULL){
p1 = p1->next;
p2 = p2->next;
if(p2 == NULL){
break;
}
p2 = p2->next;//p2移动两个单位
}
if(p2 == NULL){
return false;
}
else{
//存在环,首先求环的长度
int cnt = 1, p2 = p2->next;
while(p2 != p1){
++cnt;
p2 = p2->next;
}
//此时cnt就是环的长度

//求环的起点
p1 = list;
while(p1 != p2){
p1 = p1->next;
p2 = p2->next;
}
//此时p1与p2都指向环的起点
return true;
}
}

简单证明

借鉴于博客 上面求链表是否存在环及求环的长度的思路都很好理解,主要是为什么p1与p2再次相遇就是环的起点呢?这里假设从跑道的起始点A到环的起点B的路程为m,从B到相遇点C的路程为k,环的长度为n,相遇时乌龟的爬行路程为$S_1 = m + k + t_1 * n$,兔子的奔跑距离为$S_2 = m + k + t_2 * n(t_2 > t_1)$,兔子的速度是乌龟的两倍即$S_2 = 2 * S_1$,则$S_1 = S_2 - S_1 = (t_2 - t_1) * n$,$S_2 = 2 * (t_2 - t_1) * n$即兔子和乌龟相遇时的奔跑距离为环的整数倍,而$m + k = (t_2 - 2 * t_1) * n$也为环的整数倍。当兔子回到跑道的起始位置,乌龟从相遇点B出发时,这时,两人的速度均为单位时间内爬行1个长度,当兔子到达环的起点B即爬行了m距离时,乌龟则是k+m,此时刚好爬行环的整数倍,也处于环的起点B,即乌龟与兔子再次相遇的位置即为环的起点位置B。

复杂度分析

  • 空间复杂度

空间复杂度为:$O(1)$。使用的p1,p2,cnt均为常量空间。

  • 时间复杂度

假设链表中存在环,则p1移动m个长度即可到达环的起点,p2与p1间的最大距离为n-1。而p2移动速度是p1的两倍,每个单位时间内可以将其与p1的距离缩短1,则最多n-1时间,即可与p1相遇。时间复杂度为$O(m + n)$,即线性时间。

Brent的移动的兔子和传送的乌龟

简介

How do you determine if your singly-linked list has a cycle? In 1980, Brent invented an algorithm that not only worked in linear time, but required less stepping than Floyd’s Tortoise and the Hare algorithm (however it is slightly more complex). Although stepping through a ‘regular’ linked list is computationally easy, these algorithms are also used for factorization and pseudorandom number generators, linked lists are implicit and finding the next member is computationally difficult.

引用自网站 1980年,Brent提出了一种算法,不仅能在线性时间内找到环,并且使用的步数比Floyd的判圈算法要少。

算法思路

Brent’s algorithm features a moving rabbit and a stationary, then teleporting, turtle. Both turtle and rabbit start at the top of the list. The rabbit takes one step per iteration. If it is then at the same position as the stationary turtle, there is obviously a loop. If it reaches the end of the list, there is no loop. Of course, this by itself will take infinite time if there is a loop. So every once in a while, we teleport the turtle to the rabbit’s position, and let the rabbit continue moving. We start out waiting just 2 steps before teleportation, and we double that each time we move the turtle.

相比Floyd的兔子与乌龟,Brent的兔子仍然移动,但乌龟静止,达到传送时间t时,乌龟直接移动到兔子当前的位置,然后传送时间t翻倍(t = 2 * t),如此下去,如果兔子到达终点,则表示不存在环,如果兔子与乌龟相遇(兔子回到自己曾去达的位置)表示有环。

算法效率

Note that like Floyd’s Tortoise and Hare algorithm, this one runs in O(N). However you’re doing less stepping than with Floyd’s (in fact the upper bound for steps is the number you would do with Floyd’s algorithm). According to Brent’s research, his algorithm is 24-36% faster on average for implicit linked list algorithms.

兔子仍在不断移动,乌龟的传送使得其位置瞬移,缩短了它与兔子相遇时间。并且传送等待时间的不断翻倍,保证了兔子与乌龟在有限的时间内一定能相遇。

代码

  • 伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
turtle = top
rabbit = top

steps_taken = 0
step_limit = 2

forever:
if rabbit == end:
return 'No Loop Found'
rabbit = rabbit.next

steps_taken += 1

if rabbit == turtle:
return 'Loop found'

if steps_taken == step_limit:
steps_taken = 0
step_limit *= 2
// teleport the turtle
turtle = rabbit

引用自网站

  • c++代码
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
#include <bits/stdc++.h>

struct Node//链表节点
{
int data;
Node * next;
};

bool findCircle(Node * list){
Node* p1 = list->next, *p2 = list;//p1移动1个单位
int steps_taken = 1, step_limit = 2;
bool flag;
if(p1 == NULL){//链表只有1个节点
return false;
}

while(p1 != p2 && p1 != NULL){
p1 = p1->next;
++steps_taken;
if(p1 == NULL){
break;
}
if(steps_taken == step_limit){//达到传送时间
steps_taken = 0;//步数清0
step_limit = step_limit * 2;//传送时间翻倍
p2 = p1;
}
}
if(p1 == NULL){
return false;
}
else{
//存在环,首先求环的长度
int cnt = 1, p2 = p2->next;
while(p2 != p1){
++cnt;
p2 = p2->next;
}
//此时cnt就是环的长度

//求环的起点
p1 = list;
while(p1 != p2){
p1 = p1->next;
p2 = p2->next;
}
//此时p1与p2都指向环的起点
return true;
}
}

参考资料

  1. transforms.ToPILImage(): pic should be Tensor or ndarray

问题描述

对UNet网络完成训练,需要输入一张图片,测试输出的概率图是否接近分割出的人脸。网络的输入为(num, 3, w, h)4维度tensor。输出仍为(num, 1, w, h)4维度的tensor,我的目标是将输出的tensor转为图像,代码如下:

1
2
3
4
5
6
7
inputs = inputs.view(-1, c, h, w)
#inputs = inputs.cuda()

inputs = Variable(inputs, volatile=True)
outputs = net(inputs)
print(outputs.shape)
transforms.ToPILImage()(outputs).convert('L').save('test2.jpg')

运行出现错误transforms.ToPILImage(): pic should be Tensor or ndarray

解决问题

查阅资料1,出错是因为

All images in torchvision have to be represented as 3-dimensional tensors of the form [Channel, Height, Width]. I’m guessing your float tensor is a 2d tensor (height x width). For example, this works:

即torchvision中的所有图像必须是三维的tensor表示的,而我代码中的outputs未经处理时,是四维的。所以需要进行维度转换。修改后代码如下:

1
2
3
4
5
6
7
8
9
inputs = inputs.view(-1, c, h, w)
#inputs = inputs.cuda()

inputs = Variable(inputs, volatile=True)
outputs = net(inputs)
print(outputs.shape)
outputs = outputs.view(1, h, w) # 转成3维的 很重要!!!
print(type(outputs))
transforms.ToPILImage()(outputs).convert('L').save('test2.jpg')

参考资料

  1. issue

问题描述

原始网络用来训练一个表情分类的网络,对输入的图像进行了变换;我将其换成了一个图像分割的网络,目标值从原来的表情标签变成了01概率图的ground truth,此时不能再对输入图像进行变换,因为ground truth不能随之变换。所以我将transform赋值为None,再次训练网络时,出现了错误TypeError: default_collate: batch must contain tensors, numpy arrays, numbers, dicts or lists; found

问题解决

查阅相关问题的回答。

The error states that the DataLoader receives a PIL image. This is because there are no transforms made (transform=None) on the image. The getitem method of MyDataset passes an unprocessed PIL image to the DataLoader, whereas it should receive a tensor. You can add a transform that creates a tensor from the PIL image by adding transform:

PIL image交给DataLoader,必须要将PIL image先进行处理,即将其转为tensor,所以对图片仍需进行transform,将其它随机裁剪注释掉,仅保留PIP image转为tensor的代码。

1
2
3
4
5
6
7
8
9
10
transform_train = transforms.Compose([
#transforms.RandomCrop(44),
#transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
])

transform_test = transforms.Compose([
#transforms.TenCrop(cut_size),
transforms.Lambda(lambda crops: torch.stack([transforms.ToTensor()(crop) for crop in crops])),
])