袋熊的树洞

日拱一卒,功不唐捐

0%

介绍

本文主要是记录一些Numpy的使用方法以及注意事项。

Note: 如果没有特别说明,np 指的是 numpy,代表导入的 numpy

1
import numpy as np

PS: 网上找到一份Numpy的CheatSheet,内容不错,感兴趣的可以去下载:Numpy CheatSheet

转换ndarray数据类型

如果想转换ndarray的数据类型,可以使用ndarray的 astype 方法

1
2
3
4
5
6
>>> arr = np.array([1, 2, 3, 4, 5])
>>> arr.dtype
dtype('int64')
>>> float_arr = arr.astype(np.float64)
>>> float_arr.dtype
dtype('float64')

Note: 调用 astype 方法会生成新的数组,因此需要赋值到一个变量上。

切片不会生成新的数组

对数组进行切片后,返回的数组并不是原始数组的拷贝,只是一个对原始数组的引用,如果对切片后的数组进行数据修改,原始数组相应的位置数据会被修改。

1
2
3
4
5
6
7
8
9
10
11
12
>>> arr = np.arange(10)
>>> arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> new_arr = arr[5:8]
>>> new_arr
array([5, 6, 7])

>>> new_arr[:] = 12
>>> new_arr
array([12, 12, 12])
>>> arr
array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])

如果想要切片后的数组是原始数组的拷贝,需要调用 copy() 方法。

1
>>> new_arr = arr[5:8].copy()

Axis编号

调用一些Numpy函数时会遇到设置 axis 参数,该参数可以设为 01 ,对于这两个值表示的意义,可以参考下图

也就是说axis 0 代表行方向,axis 1 代表列方向。例如,我们使用 mean() 计算矩阵的平均值时,A.mean(axis=0) 代表每个平均值是沿着行方向计算可得,A.mean(axis=1) 代表每个平均值是沿着列方向计算可得。

1
2
3
4
5
6
7
8
>>> A = np.random.randn(2, 3)
>>> A
array([[-0.40797393, 0.24059956, -1.57582642],
[ 0.31626161, -0.07033558, 0.58346107]])
>>> A.mean(axis=0)
array([-0.04585616, 0.08513199, -0.49618267])
>>> A.mean(axis=1)
array([-0.58106693, 0.27646237])

A.mean(axis=0) 大小为矩阵A列的个数,A.mean(axis=1)大小为矩阵A行的个数。

统计Boolean数组中True的个数

Boolean值通常可以转换成 0 (False) 或 1 (True),因此可以使用 sum() 函数统计Boolean数组中 True 的个数

1
2
3
4
5
>>> bools = np.array([False, True, True, False, False])
>>> bools.sum()
2
>>> np.sum(bools)
2

既然可以知道数组中 True 的个数,自然也可以知道 True 所占的比例,此时可以用 mean() 函数进行计算

1
2
3
4
>>> bools.mean()
0.4
>>> np.mean(bools)
0.4

环境

OS: OS X 10.11.6

Shell: bash and zsh

Other: iTerm2

快捷键列表

快捷键 功能
Option + Left or Right 光标按单词前移或后移
Control + F or B 光标向前或向后移动一个字符
Control + D 删除当前光标的字符
Control + H 删除光标之前的字符
Control + W 删除光标之前的单词
Command + A 移动光标到行首
Command + E 移动光标到行尾巴
Control + U 清除整行内容
Control + K 删除光标之后的内容
Control + C Kill掉当前运行的程序
Control + Z 暂停当前运行程序,要恢复运行使用 fg process_name

快捷键详细信息

Option + Left or Right

如果使用iTerm2作为终端模拟器时,该快捷键不是所预想的功能,此时需要进行设置,这里贴出[4]提供的教程

Step 1: 在 Profiles-Keys 下设置 Left ⌥ key to act as Esc+

Step 2: 添加一个快捷键 ⌥ ←,快捷键设置内容为

  • Keyboard Shortcut: ⌥ ←
  • Action: Send Escape Sequence
  • Esc+: b

Step 3: 添加一个快捷键 ⌥ →,快捷键设置内容为

  • Keyboard Shortcut: ⌥ →
  • Action: Send Escape Sequence
  • Esc+: f

参考

  1. iTerm2 快捷键大全
  2. The Best Keyboard Shortcuts for Bash (aka the Linux and macOS Terminal)
  3. 20 Terminal shortcuts developers need to know
  4. Use ⌥ ← and ⌥→ to jump forwards / backwards words in iTerm 2, on OS X

1. 判断文件或目录是否存在

创建一个目录和一个文件

1
2
3
4
5
$ mkdir dir1 && touch file1.txt
$ ls -l
total 0
drwxr-xr-x 2 luowanqian wheel 68 5 2 23:21 dir1
-rw-r--r-- 1 luowanqian wheel 0 5 2 23:21 file1.txt

使用 os.path.exists() 可以判断文件或目录是否存在,但是不能判断是该路径是一个文件还是目录,要进一步判断,需要使用 os.path.isfile(),如果该路径是一个文件,则返回 True,否则返回 False,当然,也可以直接使用 os.path.isfile() 判断文件是否存在。测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> import os
>>> os.path.exists('dir1')
True
>>> os.path.exists('file1.txt')
True
>>> os.path.isfile('dir1')
False
>>> os.path.isfile('file1.txt')
True

>>> os.path.exists('dir2')
False
>>> os.path.exists('file2.txt')
False
>>> os.path.isfile('dir2')
False
>>> os.path.isfile('file2.txt')
False

目录 dir2 和文件 file2.txt 均不存在,所以函数 os.path.exists()os.path.isfile() 均返回 False。

2. ParameterGrid

机器学习算法最常见的调参方法是网格搜索,需要将多组参数进行组合,Scikit-learn提供了一个类 ParametGrid 可以生成所有的参数组合,这里提取其关键代码单独写成一个生成器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from itertools import product


def parameters_grid(parameter_map):
items = sorted(parameter_map.items())
if not items:
yield {}
else:
keys, values = zip(*items)
for v in product(*values):
params = dict(zip(keys, v))
yield params


if __name__ == '__main__':
parameter_map = {'a': [1, 2], 'b': [True, False]}
for params in parameters_grid(parameter_map):
print(params)

代码运行结果

1
2
3
4
{'a': 1, 'b': True}
{'a': 1, 'b': False}
{'a': 2, 'b': True}
{'a': 2, 'b': False}

3. 批量下载图片

这里使用requests库批量下载图片,为了加快下载速度,还实现了多线程下载,同时为了避免一次下载失败,脚本支持自动重试下载。没有处理具体的异常,只是捕获异常后输出异常信息。

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
import os
import requests
from retrying import retry
from io import BytesIO
from PIL import Image
import progressbar
import concurrent.futures as concurrent


@retry(stop_max_attempt_number=10)
def download(image_url, image_path):
response = requests.get(image_url)
img = Image.open(BytesIO(response.content))
img.save(image_path)


if __name__ == '__main__':
base_url = 'http://www.baidu.com'
num_images = 3
suffix = '.jpg'
image_dir = 'images'

num_workers = 4

if not os.path.isdir(image_dir):
os.mkdir(image_dir)

with concurrent.ThreadPoolExecutor(max_workers=num_workers) as executor:
image_urls = []
image_paths = []
for image_id in range(num_images):
url = base_url + '/' + str(image_id + 1) + suffix
file_path = os.path.join(image_dir, str(image_id + 1) + suffix)
image_urls.append(url)
image_paths.append(file_path)

tasks = {
executor.submit(download, url, file_path):
(url, file_path) for url, file_path in zip(image_urls, image_paths)
}

i = 0
total = len(image_urls)
pbar = progressbar.ProgressBar(max_value=total).start()
for task in concurrent.as_completed(tasks):
url, file_path = tasks[task]
try:
task.result()
i = i + 1
pbar.update(i)
except Exception as exc:
print('{} generated an exception: {}'.format(url, exc))
pbar.finish()

4. 遍历文件夹中所有文件

首先目录结构如下:

1
2
3
4
5
6
7
$ tree test
test
├── 1.txt
├── 2.txt
└── test2
├── 3.txt
└── 4.txt

使用os.walk()遍历test目录,代码如下:

1
2
3
4
5
6
root_dir = '/tmp/test'
for root, dirs, files in os.walk(root_dir, topdown=True):
for name in files:
print(os.path.join(root, name))
for name in dirs:
print(os.path.join(root, name))

得到结果如下:

1
2
3
4
5
/tmp/test/1.txt
/tmp/test/2.txt
/tmp/test/test2
/tmp/test/test2/3.txt
/tmp/test/test2/4.txt

如果设置topdown=False,得到结果如下:

1
2
3
4
5
/tmp/test/test2/3.txt
/tmp/test/test2/4.txt
/tmp/test/1.txt
/tmp/test/2.txt
/tmp/test/test2

5. 计算函数运行时间

这里使用装饰器来计算函数运行时间

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


def timethis(func):
"""
Decorator that reports the execution time
"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)

return result

return wrapper


@timethis
def countdown(n):
while n > 0:
n -= 1

6. 判断对象是否可迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def isiterable(obj):
try:
obj = iter(obj)
except:
return False
else:
return True


if __name__ == "__main__":
a = [1, 2]
b = 3
print(f"'a' is {'iterable' if isiterable(a) else 'not iterable'}")
print(f"'b' is {'iterable' if isiterable(b) else 'not iterable'}")

执行结果

1
2
'a' is iterable
'b' is not iterable

7. 判断操作系统类型

可以使用 sys.platform 判断当前是什么操作系统。常见的操作系统,其返回值如下

System platform value
AIX 'aix'
Linux 'linux'
Windows 'win32'
Windows/Cygwin 'cygwin'
macOS 'darwin'

官方推荐使用 startswith() 判断系统类型 (见 sys.platform),这里贴一段测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys


def identify_platform():
platform = sys.platform
if platform.startswith("freebsd"):
return "freebsd"
elif platform.startswith("linux"):
return "linux"
elif platform.startswith("aix"):
return "aix"
elif platform.startswith("win"):
return "windows"
elif platform.startswith("darwin"):
return "macos"
else:
return "unknown"


if __name__ == "__main__":
print(f"Platform: {identify_platform()}")

网络结构

关于 VGG 的详细内容,可以去看论文 Very Deep Convolutional Networks for Large-Scale Image Recognition,这里贴出网络结构图。

卷积层表示为 conv<receptive field size>-<number of channels>,卷积步长 (stride) 为 1,填充 (padding) 大小为 1,Pooling层的窗口大小为 2x2,步长 (stride) 为 2。为了显示简洁,图中未显示ReLU层。从图中可以看出网络输入的图片的大小为 224x224x3,经过每一层后,大小变化如下所示 (以下为VGG16网络,也就是图中的网络 D)

网络层 大小
输入层 224x224x3
conv3-64 224x224x64
conv3-64 224x224x64
maxpool 112x112x64
conv3-128 112x112x128
conv3-128 112x112x128
maxpool 56x56x128
conv3-256 56x56x256
conv3-256 56x56x256
conv3-256 56x56x256
maxpool 28x28x256
conv3-512 28x28x512
conv3-512 28x28x512
conv3-512 28x28x512
maxpool 14x14x512
conv3-512 14x14x512
conv3-512 14x14x512
conv3-512 14x14x512
maxpool 7x7x512
FC 1x1x4096
FC 1x1x4096
FC 1x1x1000

PyTorch实现

VGG 网络的 PyTorch 实现可以在 vgg.py 中找到,里面实现了网络 A, B, D, E 即 VGG11, VGG13, VGG16 以及 VGG19,同时还有相对应的 Batch Normalization 版本。如果要将 VGG 网络应用到其他大小输入的图片,主要修改的参数就是最后几个全连接层的大小即可,也就是只用修改类 VGG 中 classifier 属性即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class VGG(nn.Module):
def __init__(self, features, num_classes=1000, init_weights=True):
super(VGG, self).__init__()
self.features = features
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
if init_weights:
self._initialize_weights()

如果输入图片来自于CIFAR-10数据集,即大小为 32x32,类别数为10,经过网络的最后一个Pooling层后,输出大小为 1x1x512,因此修改类的 classifier 属性为

1
2
3
4
5
6
7
8
9
self.classifier = nn.Sequential(
nn.Linear(512 * 1 * 1, 512),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(512, 512),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(512, num_classes),
)

同时在使用函数生成对应版本的网络时,设置函数参数 num_classes=10

参考

  1. VGG论文翻译——中文版
  2. Pytorch实现的Vgg网络

环境

操作系统:OS X 10.11.6

Python版本:3.6.5

安装HDF5

使用 Homebrew 安装 HDF5,注意要开启 mpi 编译选项

1
$ brew install hdf5 --with-mpi

安装mpi4py

官网教程在使用 Parallel HDF5 时会用到 mpi4py 这个包,直接使用 pip 安装即可

1
$ pip3 install mpi4py

Note: 安装遇到一个问题是,如果之前安装过这个包,现在卸载再安装这个包时,pip 不会重新编译这个包,此时需要 --no-cache-dir 这个选项。

1
$ pip3 install --no-cache-dir mpi4py

安装h5py

直接使用 pip 安装 h5py 是不会开启 Parallel HDF5 的,需要添加一些编译选项,参考官网安装教程,运行命令

1
2
3
$ export CC=mpicc
$ export HDF5_MPI="ON"
$ pip3 install --no-binary=h5py h5py

同前面所诉,如果需要重新编译这个包,需要用到 --no-cache-dir 这个选项

1
$ pip3 install --no-cache-dir --no-binary=h5py h5py

介绍

在使用 Caffe 时,一个经常使用的数据输入来源就是LMDB数据库,通常我们手头的数据是一堆图片,此时需要将图片数据放入到LMDB数据库中。Caffe 有自个的转换程序,是一个用 C++ 编写的程序,需要编译,由于本文主要使用 Python语言,因此使用[3]提供的包来做数据转换。

Note: 相关的代码和图片数据在 GitHub

LMDB读写

数据描述

已有 10 张图片,放在目录 data 中,图片文件名列表为:

1
2
3
4
5
6
7
8
9
10
11
$ ls data | sort -n
1.png
2.png
3.png
4.png
5.png
6.png
7.png
8.png
9.png
10.png

每张图片都有一个类别,存在文件 labels.csv

1
2
3
4
5
6
7
8
9
10
11
12
$ cat labels.csv
id,label
1,frog
2,truck
3,truck
4,deer
5,automobile
6,automobile
7,bird
8,horse
9,ship
10,cat

文件中第一列是图片文件名 (不包含后缀名),第二列是图片的类别。

LMDB写入

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 numpy as np
import pandas as pd
from skimage import io
import matplotlib.pyplot as plt
import os
import lmdb
import caffe


def make_datum(image, label, channels, height, width):
datum = caffe.proto.caffe_pb2.Datum()
datum.channels = channels
datum.label = int(label)
datum.height = height
datum.width = width
datum.data = image.tobytes()

return datum


# data path and lmdb path
dataset_path = './data'
label_file = 'labels.csv'
lmdb_path = 'cifar10_lmdb'

# labels mapping
labels_mapping = {'airplane': 0, 'automobile': 1,
'bird': 2, 'cat': 3, 'deer': 4, 'dog': 5,
'frog': 6, 'horse': 7, 'ship': 8, 'truck': 9}
classes = {}
for key in labels_mapping:
classes[labels_mapping[key]] = key

# load data
df = pd.read_csv(label_file)
df['label'] = df['label'].map(labels_mapping)
images = list(df.id)
labels = list(df.label)

# write data to LMDB
map_size = 1e6
batch_size = 4

count = 0
lmdb_env = lmdb.open(lmdb_path, map_size=map_size)
lmdb_txn = lmdb_env.begin(write=True)

for image_id, label in zip(images, labels):
count = count + 1
image_file = os.path.join(dataset_path, str(image_id) + '.png')
image = io.imread(image_file)
height, width, channels = image.shape
datum = make_datum(image, label, channels, height, width)
str_id = '{:08}'.format(count)
lmdb_txn.put(str_id, datum.SerializeToString())

if count % batch_size == 0:
lmdb_txn.commit()
lmdb_txn = lmdb_env.begin(write=True)

lmdb_txn.commit()
lmdb_env.close()

LMDB读取

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
lmdb_env = lmdb.open(lmdb_path)
lmdb_txn = lmdb_env.begin()
lmdb_cursor = lmdb_txn.cursor()
datum = caffe.proto.caffe_pb2.Datum()

count = 0
for key, value in lmdb_cursor:
datum.ParseFromString(value)
label = datum.label
height = datum.height
width = datum.width
channels = datum.channels
data = datum.data
count = count + 1

if count == 2:
image = np.frombuffer(data, dtype=np.uint8)
image = np.reshape(image, (height, width, channels))
print('Label: {}, Class: {}'.format(label, classes[label]))
plt.imshow(image)
break

print('Number of items: {}'.format(count))

lmdb_env.close()

参考

  1. Creating an LMDB database in Python
  2. Write mnist image to lmdb with python
  3. lmdb

本文主要是记录一些h5py的使用方法。

Note: 如果没有特别说明,np 指的是 numpy,代表导入的 numpy

1
import numpy as np

存储浮点数采用单精度浮点数

在HDF5文件中存储浮点数时,可以选择单精度浮点数和双精度浮点数,常见是用单精度浮点数来存储浮点数,相比于双精度浮点数,单精度浮点数存储空间为双精度浮点数的一半,这样可以缩小一半的存储空间。不同于文件存储,在内存中需要使用双精度浮点数来保证计算的准确性,因此一个通常的操作是,在内存中使用双精度浮点数,然后存储到HDF5文件时,使用单精度浮点数,两者的使用只需要进行数据类型转换。

在内存中,我们使用Numpy创建一个数组,数据类型为双精度浮点数

1
2
3
4
5
6
>>> import numpy as np
>>> bigdata = np.ones((100, 1000))
>>> bigdata.dtype
dtype('float64')
>>> bigdata.shape
(100, 1000)

直接使用赋值方式将数组存储到HDF5文件中,存储的浮点数为双精度浮点数

1
2
3
4
5
>>> with h5py.File('big1.hdf5', 'w') as f1:
... f1['big'] = bigdata
>>> f1 = h5py.File('big1.hdf5')
>>> f1['big'].dtype
dtype('float64')

文件大小为 783K

1
2
$ ls -lh big1.hdf5
-rw-r--r-- 1 luowanqian staff 783K 4 29 10:44 big1.hdf5

我们可以使用 create_dataset 函数来指定存储单精度浮点数

1
2
3
4
5
>>> with h5py.File('big2.hdf5', 'w') as f2:
... f2.create_dataset('big', data=bigdata, dtype=np.float32)
>>> f2 = h5py.File('big2.hdf5')
>>> f2['big'].dtype
dtype('float32')

文件大小为 393K

1
2
$ ls -lh big2.hdf5
-rw-r--r-- 1 luowanqian staff 393K 4 29 11:02 big2.hdf5

读取数据时进行数据转换

假设有个存储单精度浮点数的HDF5文件,我们想将数据读入到内存时是双精度浮点数。HDF5文件的数据如下

1
2
3
4
5
6
7
8
9
10
11
>>> import numpy as np
>>> import h5py
>>> bigdata = np.ones((100, 1000))
>>> with h5py.File('big.hdf5', 'w') as f:
... f.create_dataset('big', data=bigdata, dtype=np.float32)
>>> f = h5py.File('big.hdf5')
>>> dset = f['big']
>>> dset.dtype
dtype('float32')
>>> dset.shape
(100, 1000)

方案1

使用 np.empty 创建一个空数组,然后使用 read_direct 函数

1
2
3
4
>>> out = np.empty((100, 1000), dtype=np.float64)
>>> dset.read_direct(out)
>>> out.dtype
dtype('float64')

如果不想读取全部数据,可以设置函数 read_directsource_seldest_sel 参数。假设要将 dset[0, :] 的数据读入到 out[50, :]

1
2
>>> out = np.empty((100, 1000), dtype=np.float64)
>>> dset.read_direct(out, source_sel=np.s_[0, :], dest_sel=np.s_[50, :])

其中使用 np.s_ 返回是 Numpy 的 slice 对象,该对象包含索引信息。如果省略 dset_sel 参数,则采用类似 Numpy 的广播规则进行赋值

1
2
>>> out = np.empty((100, 50), dtype=np.float32)
>>> dset.read_direct(out, source_sel=np.s_[:, 0:50])

优势

如果要读取多次同样大小的数据时,使用 read_direct 可以节省很多时间,因为只需要申请一次空间,后面数据读入直接覆盖到这个空间。做一个Benchmark:

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
import h5py
import numpy as np
from timeit import timeit


filename = 'test.hdf5'
n = 10000

f = h5py.File(filename, 'w')
dset = f.create_dataset('perftest', (n, n), dtype=np.float32)
dset[:] = np.random.random(n)
out = np.empty((n, 500), dtype=np.float32)


def time_simple():
dset[:, 0:500].mean(axis=1)


def time_direct():
dset.read_direct(out, np.s_[:, 0:500])
out.mean(axis=1)


print('Time simple: {}'.format(timeit(time_simple, number=500)))
print('Time direct: {}'.format(timeit(time_direct, number=500)))

f.close()

运行结果

1
2
Time simple: 41.33699381200131
Time direct: 39.005692337988876

方案2

使用 Dataset.astype 这个上下文管理器

1
2
3
4
5
6
>>> with dset.astype('float64'):
... out = dset[...]
>>> out.shape
(100, 1000)
>>> out.dtype
dtype('float64')

当然,也适合读取部分数据

1
2
3
4
>>> with dset.astype('float64'):
... out = dset[0, :]
>>> out.dtype
dtype('float64')

介绍

Non-Maximum Suppression,简称NMS,在计算视觉领域有着非常重要的应用,主要应用在冗余的检测框的去除,例如在人脸检测应用中,检测出多个人脸框,此时需要去除冗余的检测框,保留最好的一个,还有在目标检测算法中会遇到一个物体有多个检测框,此时也需要去除冗余的检测框。

原理

利用参考[1]中提供的例子来简单阐述算法的流程。已知图片中有一辆汽车

目标检测算法定位该汽车的位置时找出了一堆检测框,此时我们需要去除冗余的检测框。假设目标算法找到了6个检测框,而且算法还提供了每个框中内容属于汽车的概率或者得分 (在RCNN中,使用SVM计算检测框属于该类别的得分),NMS方法首先根据得分大小对检测框进行排序,假设从小到大的排序为 A, B, C, D, E, F

  1. 从最大得分的检测框 F 开始,分别判断 A~EF 的重叠度IoU是否大于某个设定的阈值。

  2. 假设检测框 B、DF 的重叠度超过阈值,那么就抛弃 B、D,并将检测框 F 标记为要保留的检测框。

  3. 第2步去掉 BD 后,剩余检测框 A、C、E,接着在剩下的检测框中选择得分最大检测框 E,然后判断 EA、C 的重叠度,如果重叠度大于设定阈值,那就抛弃该检测框,否则留到下一轮的筛选过程,并将检测框 E 标记为要保留的检测框。

重复步骤 3,直到剩余待筛选的框个数为 0。

实现

关于单类别的NMS的实现,网上已经有实现好的,这里贴出 Ross Girshick (RCNN提出者) 写的Python实现 py_cpu_nms.py,并且加了个人的标注。

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
import numpy as np


def py_cpu_nms(dets, thresh):
"""Pure Python NMS baseline."""
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
scores = dets[:, 4]

# 每个框 (bounding box) 的面积
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
# 根据得分 (score) 的大小进行降序排序
order = scores.argsort()[::-1]

keep = []
while order.size > 0:
# 保留剩余框中得分最高的那个
i = order[0]
keep.append(i)

# 计算相交区域位置,左上以及右下
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])

# 计算相交区域面积
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)

# 计算IoU,即 重叠面积 / (框1面积 + 框2面积 - 重叠面积)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)

# 保留IoU小于阈值的框
inds = np.where(ovr <= thresh)[0]

# 因为ovr数组的长度比order长度小1,所以这里要将所有下标后移一位
order = order[inds + 1]

return keep

关于该代码的使用,我写了一个简单的测试脚本,在 GitHub,测试了一张图片,实现效果如下:

参考

  1. 非极大值抑制(Non-Maximum Suppression,NMS)

Introduction

Shell Snippets

列出目录里面文件名并进行排序

在目录 train 里面有大量的图片 (扩展名为 png)

1
2
3
4
5
6
7
8
9
10
11
$ ls -l train/ | head
total 400000
-rw-r--r--@ 1 luowanqian staff 2455 10 19 2013 1.png
-rw-r--r--@ 1 luowanqian staff 2101 10 19 2013 10.png
-rw-r--r--@ 1 luowanqian staff 2466 10 19 2013 100.png
-rw-r--r--@ 1 luowanqian staff 2171 10 19 2013 1000.png
-rw-r--r--@ 1 luowanqian staff 2031 10 19 2013 10000.png
-rw-r--r--@ 1 luowanqian staff 2051 10 19 2013 10001.png
-rw-r--r--@ 1 luowanqian staff 2317 10 19 2013 10002.png
-rw-r--r--@ 1 luowanqian staff 2511 10 19 2013 10003.png
-rw-r--r--@ 1 luowanqian staff 2460 10 19 2013 10004.png

提取该目录下的所有图片的文件名并进行排序

方案1

1
2
3
4
5
6
7
8
9
10
11
12
$ ls -l train/ | grep ".png" | tr -s ' ' | cut -d ' ' -f 9 | sort -n > list.txt
$ head list.txt
1.png
2.png
3.png
4.png
5.png
6.png
7.png
8.png
9.png
10.png

其中

  • tr -s ' ' 是为了将多个space压缩成一个space
  • cut -d ' ' -f 9 是为了取出最后一列的文件名
  • sort -n 是将文件进行排序,由于文件名里面有数字,所以用-n选项

方案2

1
2
3
4
5
6
7
8
9
10
11
12
$ ls train/ | sort -n > list.txt
$ head list.txt
1.png
2.png
3.png
4.png
5.png
6.png
7.png
8.png
9.png
10.png

pip升级所有包

pip 升级所有Python包,命令如下

1
pip3 list --outdated --format=freeze | cut -d = -f 1 | xargs pip3 install -U'

其中 pip3 可以根据 pip 版本替换。

rsync断点续传

主要使用的是 rsync 的 -P 的选项,传输命令写为

1
$ rsync -P ubuntu/ubuntu-16.04.4-desktop-amd64.iso lab217server:/home/luowanqian/Downloads/

如果传输过程中网络中断或者使用了 Ctrl + C,此时可以再次使用该命令进行断点续传。

参考:

  1. How To Resume Partially Transferred Files Over SSH Using Rsync

介绍

关于ROC曲线的详细介绍,可以参考周志华的西瓜书 (《机器学习》),本文主要介绍如何使用Python绘制该曲线。ROC曲线的纵轴是“真正例率” (True Positive Rate,简称TPR),横轴是“假正例率” (False Positive Rate,简称FPR),两者定义为:
$$
\begin{align}
TPR & = \frac{TP}{TP + FN} \
FPR & = \frac{FP}{TN + FP}
\end{align}
$$
相关的符号定义为:

在现实任务中,我们获取有限个 (FPR, TPR) 坐标对来勾勒出ROC曲线。要获得多个坐标对需要多组二分类结果,而我们做二分类任务时,通常只能获取一组分类结果,此时我们利用这组结果生成多组分类结果,具体做法是:给定 $m$ 个正例和 $n$ 个反例,根据分类器的预测值对样例进行从大到小排序,然后把分类阈值设为样例预测值中最大的那个,将样例进行正反例分类,计算相应的TPR和FPR,然后令分类阈值依次设为每个样例的预测值,将样例进行正反例分类,接着计算TPR和FPR,重复这个操作,直到分类阈值取完所有样例的预测值。

关于分类器对样例的预测值,我们可以用分类器判定样例为正例的概率值,也可以用分类器对样例的评分值,预测值大于等于分类阈值时,分类器判定样例为正例,否则判定为负例。

例子

已知10样例的真实标签 (0: 反例,1: 正例) 以及分类器对该样例的评分值

1
2
y_true = [0, 1, 1, 0, 1, 0, 1, 1, 1, 0]
y_score = [0.505, 0.6, 0.8, 0.52, 0.55, 0.53, 0.54, 0.9, 0.51, 0.7]

根据评分值将样例进行从大到小排序,可以得到

1
2
y_true = [1, 1, 0, 1, 1, 1, 0, 0, 1, 0]
y_score = [0.9, 0.8, 0.7, 0.6, 0.55, 0.54, 0.53, 0.52, 0.51, 0.505]

将分类阈值设为 0.9,此时样例的分类为

1
2
y_true = [1, 1, 0, 1, 1, 1, 0, 0, 1, 0]
y_predict = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

可以得到 TP = 1,FP = 0,FN = 5,TN = 4,则可得到 TPR = 1/(1+5) = 1/6 以及 FPR = 0。接着将分类阈值设为 0.8,然后将样例进行正反例分类,接着计算相应的TPR和FPR,直到分类阈值取完所有评分值,最终可以得到下面的TPR和FPR列表

1
2
TPR = [0.16666667, 0.33333333, 0.33333333, 0.5, 0.66666667, 0.83333333, 0.83333333, 0.83333333, 1.0, 1.0]
FPR = [0.0, 0.0, 0.25, 0.25, 0.25, 0.25, 0.5, 0.75, 0.75, 1.0]

然后根据这一系列的 (FPR, TPR) 坐标对画出ROC曲线。

实现

相关代码以及运用可以参考:GitHub

Scikit-learn实现

Scikit-learn库提供了一个名为 roc_curve 函数来获取FPR以及TRP,函数原型如下

1
sklearn.metrics.roc_curve(y_true, y_score, pos_label=None, sample_weight=None, drop_intermediate=True)

要想得到上面例子的计算结果,需要把 drop_intermediate 设为 False。

个人实现

我这里直接照搬[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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def binary_clf_curve(y_true, y_score, pos_label=None):
if pos_label is None:
pos_label = 1.0

# make y_true a boolean vector
y_true = (y_true == pos_label)

# sort scores
desc_score_indices = np.argsort(y_score)[::-1]
thresholds = y_score[desc_score_indices]

fps = []
tps = []
for threshold in thresholds:
# 大于等于阈值判定为 1 (正类),否则为 0 (负类)
y_predict = [1 if i >= threshold else 0 for i in y_score]
# 预测值是否等于真实值
result = [i == j for i, j in zip(y_true, y_predict)]
# 预测值是否为 1 (正类)
positive = [i == 1 for i in y_predict]

# 预测为正类且预测错误
fp = [(not i) and j for i, j in zip(result, positive)].count(True)
# 预测为正类且预测正确
tp = [i and j for i, j in zip(result, positive)].count(True)

fps.append(fp)
tps.append(tp)
fps = np.array(fps)
tps = np.array(tps)

return fps, tps, thresholds


def roc_curve(y_true, y_score, pos_label=None):
fps, tps, thresholds = binary_clf_curve(y_true, y_score, pos_label)

fpr = fps / fps[-1]
tpr = tps / tps[-1]

return fpr, tpr, thresholds

参考

  1. 通过一个例子来绘制一条ROC曲线?
  2. roc-auc
  3. ROC和AUC介绍以及如何计算AUC