小建儿的小站


  • 首页

  • 关于

  • 标签

  • 分类

  • 搜索

Term-1-p4-advanced-lane-lines

发表于 2018-03-26 | 分类于 无人驾驶 |
字数统计: | 阅读时长 ≈

简介

  p4也是车道检测,可以说是p1的升级版。p1只是使用了简单的边缘检测和直线检测进行直线车道信息提取,并未考虑摄像头造成的畸变以及车道不是直线的时候如何进行车道检测,这节课在之前的基础上进行了延伸,过程中会用到更多计算机视觉处理技术。

处理流程

  处理流程如下所示:



棋盘标定

  在使用摄像设备进行图像拍摄的时候往往会有畸变误差,这些畸变误差分为两类:

  • 径向畸变:由于透镜形状等原因造成,距离透镜光学中心越近畸变越小,越靠近透镜边缘畸变越严重。

  • 切向畸变:由于摄像设备安装时不完全平行于图像平面造成的;

  消除畸变可以采用棋盘标定来实现,通过检测棋盘的角点,获取校正系数,将其保存,用于后续实际拍摄图像的畸变校正。
  代码如下

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
#coding:utf-8
import cv2
import numpy as np
import glob
import matplotlib.pyplot as plt
import pickle

#摄像头校正
def camera_calibration():
#横向角点数量
nx = 9
#纵向角点数量
ny = 6
#获取角点坐标
objp = np.zeros((nx*ny,3),np.float32)
#赋值x,y,默认z=0
objp[:,:2] = np.mgrid[0:nx,0:ny].transpose(2,1,0).reshape(-1,2)

objpoints = []
imgpoints = []

images = glob.glob("./camera_cal/calibration*.jpg")
count = 0
plt.figure(figsize=(12,8))
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#标记棋盘角点
ret,corners = cv2.findChessboardCorners(gray,(nx,ny),None)

if ret == True:
objpoints.append(objp)
imgpoints.append(corners)
#角点图
img_cor = cv2.drawChessboardCorners(img,(nx,ny),corners,ret)
#画图
plt.subplot(4,5,count+1)
plt.axis('off')
plt.title(fname.split('/')[-1])
plt.imshow(img_cor)
count += 1
write_name = './corners_found/corners_'+fname.split('/')[-1];
cv2.imwrite(write_name,img)
plt.show()
return objpoints,imgpoints

#保存校正系数等
def save_parameters(objpoints,imgpoints):
img = cv2.imread('./camera_cal/calibration1.jpg')
img_size = (img.shape[1],img.shape[0])
ret,mtx,dist,rvecs,tvecs = cv2.calibrateCamera(objpoints,imgpoints,img_size,None,None)
dist_pickle = {}
dist_pickle['mtx'] = mtx
dist_pickle['dist'] = dist
pickle.dump(dist_pickle,open('./output_images/camera_mtx_dist.p','wb'))
print 'parameters saved'


def main():
objpoints,imgpoints = camera_calibration()
save_parameters(objpoints,imgpoints)

if __name__=='__main__':
main()

  角点标记的效果如下



径向畸变校正

  先展示一下未处理过的照片和畸变校正后的照片效果



  可以看到左图边缘被弯曲的线条,在右图中被修正为平行线条。对于实际道路的图片,效果如下:



  代码示例如下:

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
#coding:utf-8
import cv2
import numpy as np
import pickle
import matplotlib.pyplot as plt
#读取校正系数
def load_undistort_parameter(path = './output_images/camera_mtx_dist.p'):
with open(path) as f:
data = pickle.load(f)
return data['mtx'],data['dist']
#消除畸变
def undistort(img,mtx,dist):
return cv2.undistort(img,mtx,dist,None,mtx)
#展示效果图
def show_undistorted_image(mtx,dist,path = './camera_cal/calibration1.jpg'):
img = cv2.imread(path)
undst = undistort(img,mtx,dist)
#为了显示把BGR格式转为RGB格式
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
undst = cv2.cvtColor(undst,cv2.COLOR_BGR2RGB)
plt.figure(figsize=(200,200))
plt.subplot(1,2,1)
plt.title('origin image')
plt.imshow(img)
plt.subplot(1,2,2)
plt.title('undistort image')
plt.imshow(undst)
plt.show()

def main():
mtx,dist = load_undistort_parameter()
show_undistorted_image(mtx,dist)
show_undistorted_image(mtx,dist,'./test_images/test1.jpg')

if __name__=='__main__':
main()

透视畸变校正

  车载摄像头是固定在车上的,由于拍摄视角问题(摄像头与道路不垂直),拍摄出的照片有透视畸变,即近大远小,和美术的透视原理类似。

  透视变换可以消除透视畸变,简而言之就是转化为鸟瞰图。其原理如下:取得畸变图像的4个点坐标和目标图像的4个点坐标,通过两组坐标计算出透视变换的变换矩阵,之后对整个图像执行变换,就实现了对图像的校正。

  畸变图像需要进行透视变换的区域坐标分别为(580,460),(740,460),(280,680),(1050,680),在图上的区域如下


透视变换区域

透视变换代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

def perspective_transform(path = './test_images/test6.jpg'):
image = cv2.imread(path)
img_h = image.shape[0]
img_w = image.shape[1]
print img_h,img_w
#原图像待变换区域顶点坐标
src = np.float32([left_up,left_down,right_up,right_down])
#目标区域顶点坐标
dst = np.float32([[200,0],[200,680],[1000,0],[1000,680]])
#求得变换矩阵
M = cv2.getPerspectiveTransform(src,dst)
#进行透视变换
warped = cv2.warpPerspective(image,M,(img_w,img_h),flags=cv2.INTER_NEAREST)
return img_as_ubyte(warped),M

经过透视变换,图像转换为



边缘检测

  与p1类似若想提取车道信息,需要二值化处理、边缘检测、roi区域提取,但是p4的处理过程会有不同。

  对于p1,直接读入灰度图像然后进行canny边缘检测,这样做有两个问题。首先是颜色检测,车道线条往往有不同的颜色,对于RGB格式来说,某些颜色在特定通道下检测效果不好,比如黄色车道线在Blue通道下是这样的



  黄色分量在blue通道下辨识度不是很高。为了更好的进行颜色检测,可以把RGB格式转化为HLS格式,即使用色相(Hue)、饱和度(Saturation)、亮度(Lightness)表示颜色,可以获得更好的效果。

  第二个问题是canny检测虽然能够获得很好的效果,但是它会把车道外其他部分的边缘提取出来,这些信息对于我们来说并没有用。考虑到车在行驶过程中,车道基本是垂直于车子,所以可以采用sobel算子提取x方向上的梯度,实现垂直方向上车道的检测。

  代码如下

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
def edge_detection(image,sobel_kernel=3,sc_threshold=(110, 255), sx_threshold=(20, 100)):

img = np.copy(image)
#转换为hsv格式
hsv = cv2.cvtColor(img,cv2.COLOR_RGB2HLS).astype(np.float)
h_channel = hsv[:,:,0]
l_channel = hsv[:,:,1]
s_channel = hsv[:,:,2]
#使用s通道
channel = s_channel
#x方向梯度
sobel_x = cv2.Sobel(channel,cv2.CV_64F,1,0,ksize = sobel_kernel)
#二值化处理
scaled_sobel_x = cv2.convertScaleAbs(255*sobel_x/np.max(sobel_x))
sx_binary = np.zeros_like(scaled_sobel_x)
#进行边缘检测
sx_binary[(scaled_sobel_x >= sx_threshold[0]) & (scaled_sobel_x<=sx_threshold[1])] = 1
s_binary = np.zeros_like(s_channel)
#进行颜色检测
s_binary[(channel>=sc_threshold[0]) & (channel<=sc_threshold[1])]=1
flat_binary = np.zeros_like(sx_binary)
#颜色和边缘叠加
flat_binary[(sx_binary == 1) | (s_binary ==1)] =1

return flat_binary

  检测效果如下:



  接下来进行roi区域提取

1
2
3
4
5
6
7
8
9
10
11
12
def roi(img,vertices):
mask = np.zeros_like(img)

if len(img.shape) >2:
channel_count = image.shape[2]
ignore_mask_color=(255,)*channel_count
else:
ignore_mask_color = 255

cv2.fillPoly(mask,vertices,ignore_mask_color)
masked_image = cv2.bitwise_and(img,mask)
return masked_image

  最后得到的车道图像为



车道标记

  上个步骤结束后,我们找到了车道的大致范围,下面需要对这些点进行过滤,然后将其连成车道线。

确定中线范围

  首先找到车道的中线作为搜索的起始点,由于车道有一定的弯曲,我们可以使用图片的下半部分画出直方图,查找其最大值位于x轴的位置。



1
2
3
4
5
6
7
8
9
10
11
#获取直方图,得到车道大致位置
histogram = np.sum(roi_image[int(roi_image.shape[0]/2):,:],axis=0)
#输出图像
out_image = np.dstack((roi_image,roi_image,roi_image)) * 255

#图像中线x轴的坐标
midpoint = np.int(histogram.shape[0]/2)
#图像中线左侧车道x轴基准
leftx_base = np.argmax(histogram[:midpoint])
#图像中线右侧车道x轴基准
rightx_base = np.argmax(histogram[midpoint:]) + midpoint

大致能确定左车道中心线大概在x=250,右车道中心线大概在x=1000的位置。

定义搜索框

  在y轴方向上使用9个搜索框从图片底部向图片上部进行搜索,其宽度为80像素。

1
2
3
4
5
6
#设置滑动窗口个数
nwindows = 9
#窗口高度
window_height = np.int(roi_image.shape[0]/nwindows)
#所有非零点坐标
nonzero = roi_image.nonzero()

过滤非零像素点

  把不符合要求的点过滤掉,只保留窗口范围内的非零像素点,并调节搜索中线位置。整个过程如下



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
#左车道搜索起始点
leftx_current = leftx_base
#右车道搜索起始点
rightx_current = rightx_base
#窗口长度2*margin,即搜索中线左右80像素范围内的点
margin = 80
#窗口内超过50个点才会改变搜索中线
minpix = 50
#左车道窗口内像素点坐标
left_lane_indices = []
#右车道窗口内像素点坐标
right_lane_indices = []


for window in range(nwindows):
#窗口上边线y值
win_y_low = roi_image.shape[0] - (window + 1) * window_height
#窗口下边线y值
win_y_high = roi_image.shape[0] - window * window_height
#窗口左下顶点坐标
win_xleft_low = leftx_current - margin
#窗口左上顶点坐标
win_xleft_high = leftx_current + margin
#窗口右下顶点坐标
win_xright_low = rightx_current - margin
#窗口右上顶点坐标
win_xright_high = rightx_current + margin
#画图
cv2.rectangle(out_image,(win_xleft_low,win_y_low),(win_xleft_high,win_y_high),(255,0,0),2)
cv2.rectangle(out_image,(win_xright_low,win_y_low),(win_xright_high,win_y_high),(255,0,0),2)
#挑出左窗口范围内的点
good_left_indices = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high)
& (nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]
#挑出右窗口范围内的点
good_right_indices = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high)
& (nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]

left_lane_indices.append(good_left_indices)
right_lane_indices.append(good_right_indices)

if len(good_left_indices) > minpix:
#将左车道搜索中线置为窗口内像素点x坐标的平均值
leftx_current = np.int(np.mean(nonzerox[good_left_indices]))
if len(good_right_indices) > minpix:
#将右车道搜索中线置为窗口内像素点x坐标的平均值
rightx_current = np.int(np.mean(nonzerox[good_right_indices]))

车道线拟合

  拿到了车道像素点后,使用数学方法对其进行二次函数拟合。为了后续处理,还需要计算拟合曲线上的坐标。效果如下



1
2
3
4
5
6
7
8
9
10
#拟合左车道曲线
left_fit = np.polyfit(lefty,leftx,2)
#拟合右车道曲线
right_fit = np.polyfit(righty,rightx,2)
#y轴的坐标都是确定的,待确定的是x的坐标
ploty = np.linspace(0,roi_image.shape[0]-1,roi_image.shape[0])
#根据左车道y的坐标计算x的坐标
left_fitx = left_fit[0] * ploty ** 2 + left_fit[1] * ploty + left_fit[2]
#根据右车道y的坐标计算x的坐标
right_fitx = right_fit[0] * ploty ** 2 + right_fit[1] * ploty + right_fit[2]

标记车道范围

  先在鸟瞰图上标记车道范围,然后映射到透视变换前的图像上。



1
2
3
4
5
6
7
8
9
10
11
# 复制一份输出图像
color_warp = np.zeros_like(out_image).astype(np.uint8)
pts_left = np.array([np.transpose(np.vstack([left_fitx,ploty]))])
pts_right = np.array([np.flipud(np.transpose(np.vstack([right_fitx,ploty])))])
pts = np.hstack((pts_left,pts_right))
#标记不规则区域
cv2.fillPoly(color_warp,np.int_([pts]),(0,255,0))
#还原到原图像,需要用到透视变换矩阵
newwarp = cv2.warpPerspective(color_warp,np.linalg.inv(M),(original.shape[1],original.shape[0]))
#叠加图像
result = cv2.addWeighted(original,1,newwarp,0.3,0)

处理视频

  处理完单张图像,处理视频,效果对比如下
效果如下

  • 原始视频


  • 处理后视频


总结

   这节课难度比前几节课略大,需要用到的图像处理手段有些复杂,需要一定时间消化理解,部分细节性技术后续会单开文章专门介绍,这部分代码参考Term-1-p4-advanced-lane-lines

反射机制性能测试

发表于 2018-03-23 | 分类于 java |
字数统计: | 阅读时长 ≈

背景

  目前项目中用到了反射,但是java反射机制会造成一定的性能损失,需要初步确定其造成的损失程度。这次测试场景较为粗糙,有待进一步详细测试。

测试场景

硬件设备

  • 电脑型号:MacBook Pro

  • 系统版本:macOS High Sierra 10.13.1

  • 处理器:2.7 GHz Intel Core i5

  • 内存:8G

  • jvm版本:1.8.0_144

  • 堆最大内存:2G

方法调用机制

对比以下三种调用机制执行同一操作相同次数的耗时

  • 直接调用
  • Java反射,这里先反射需要调用的方法,只记录方法执行耗时
  • ReflectAsm反射

待执行方法

  • 简单方法,get一个int行变量
  • 稍复杂方法,add两个long型变量

测试用例

待反射类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InvokeClass {


private int a = 1;

public int getA() {
return a;
}

public long add(long a, long b) {
return a + b;
}

}

反射执行get方法

执行次数:Integer.MAX_VALUE,即(2^31 - 1)次

直接调用

1
2
3
4
5
6
7
InvokeClass invokeClass = new InvokeClass();
long start = System.currentTimeMillis();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
invokeClass.getA();
}
long end = System.currentTimeMillis();
System.out.println(end - start);

平均耗时5ms

Java反射

1
2
3
4
5
6
7
8
Method method = InvokeClass.class.getMethod("getA");
set.setAccessible(true); //此处很重要,不设置的话每次反射会去判断方法是否能访问,造成性能损失,大约耗时5000ms
long start = System.currentTimeMillis();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
method.invoke(invokeClass);
}
long end = System.currentTimeMillis();
System.out.println(end - start);

平均耗时3000ms

ReflectAsm反射

1
2
3
4
5
6
7
8
9

MethodAccess methodAccess = MethodAccess.get(InvokeClass.class);
int index = methodAccess.getIndex("getA");
long start = System.currentTimeMillis();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
methodAccess.invoke(invokeClass, index);
}
long end = System.currentTimeMillis();
System.out.println(end - start);

平均耗时30ms

反射执行Add方法

直接调用

1
2
3
4
5
6
7
InvokeClass invokeClass = new InvokeClass();
long start = System.currentTimeMillis();
for (long i = 0; i < Integer.MAX_VALUE; i++) {
invokeClass.add(i, i);
}
long end = System.currentTimeMillis();
System.out.println(end - start);

平均耗时1000ms

java反射

1
2
3
4
5
6
7
8
9

Method method = InvokeClass.class.getMethod("add", long.class, long.class);
set.setAccessible(true);
long start = System.currentTimeMillis();
for (long i = 0; i < Integer.MAX_VALUE; i++) {
method.invoke(invokeClass, i, i);
}
long end = System.currentTimeMillis();
System.out.println(end - start);

平均耗时18000ms

ReflectAsm反射

1
2
3
4
5
6
7
8
MethodAccess methodAccess = MethodAccess.get(InvokeClass.class);
int index = methodAccess.getIndex("add", long.class, long.class);
long start = System.currentTimeMillis();
for (long i = 0; i < Integer.MAX_VALUE; i++) {
methodAccess.invoke(invokeClass, index, i, i);
}
long end = System.currentTimeMillis();
System.out.println(end - start);

平均耗时2700ms

对比及结论

直接调用 Java反射 ReflectASM反射
get方法 5 3000 30
add方法 1000 18000 2700

经过简单测试,可以发现

  1. 反射确实会带来性能损失,ReflectAsm相比Java原生反射机制能提升一定性能
  2. 待反射方法的复杂程度会对反射耗时带来一定影响,需要进一步详细测试

Term-1-p2-traffic-sign-classifier

发表于 2018-03-22 | 分类于 无人驾驶 |
字数统计: | 阅读时长 ≈

简介

  Term-1第二节课是进行交通标志分类,数据集主要来自于German Traffic Sign,包含了42种交通标志,通过深度学习网络进行分类。

环境准备

  • python 2.7
  • numpy
  • scikit-learn
  • tensorflow
  • keras

处理流程

  处理流程如下图所示



数据读取

  我们拿到的数据集是一系列交通标志图像,每个类别的交通标志放在了同一个文件夹下,并且有一个csv文件用于描述每个交通标志图片的ROI区域和该标志所属类别。下载图像的时候网站提供了一份用于数据处理的python程序(Python code for GTSRB文件夹下),在这里可以用到。
  这里按csv描述的图像信息进行ROI部分的提取,并使用pickle保存为.p文件方便后续模型训练使用。代码示例如下:

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
import numpy as np
import pickle
import os
import cv2
import csv

//处理训练数据
def process_train_data(path):
file = os.listdir(path)
classes = len(file)
train_data = []
train_labels = []
for i in range(0,classes):
dir_name = file[i]
if dir_name=='.DS_Store':
continue
full_dir_path = path + dir_name
csv_file_path = full_dir_path + '/' + 'GT-{0}.csv'.format(dir_name)
with open(csv_file_path) as f:
csv_reader = csv.reader(f,delimiter=';')
# pass header
csv_reader.next()
for (filename,width,height,x1,y1,x2,y2,classid) in csv_reader:
train_labels.append(classid)
image_file_path = full_dir_path+'/'+filename
resized_image = resize_image(image_file_path,(x1,y1,x2,y2))
train_data.append(resized_image)
f.close()
print 'train data process done'
return train_data,train_labels

//提取roi区域,并调整大小,统一为32*32像素
def resize_image(path,index):
image = cv2.imread(path)
image = image[int(index[0]):int(index[2]),int(index[1]):int(index[3])]
res = cv2.resize(image,(32,32),interpolation = cv2.INTER_CUBIC)
return res

//处理测试数据
def process_test_data(path):
test_data = []
test_labels = []
csv_file_path = path + '/' + 'GT-final_test.csv'
with open(csv_file_path) as f:
csv_reader = csv.reader(f,delimiter=';')
csv_reader.next()
for (filename,width,height,x1,y1,x2,y2,classid) in csv_reader:
test_labels.append(classid)
image_file_path = path+'/'+filename
resized_image = resize_image(image_file_path,(x1,y1,x2,y2))
test_data.append(resized_image)
print 'test data process done'
return test_data,test_labels

def main():
train_data_path = '../data/GTSRB/Final_Training/Images/'
test_data_path = '../data/GTSRB2/Final_Test/Images'
train_data,train_labels = process_train_data(train_data_path)
test_data,test_labels = process_test_data(test_data_path)
with open('./train.p','wb') as f:
pickle.dump({"data":np.array(train_data),"labels":np.array(train_labels)},f)
with open('./test.p','wb') as f:
pickle.dump({"data":np.array(test_data),"labels":np.array(test_labels)},f)

if __name__=="__main__":
main()

  放几张效果图



数据处理

  目前已经得到了训练集和测试集,需要对数据进行shuffle以及对label进行one-hot编码。

1
2
3
4
5
6
7
8
9
def shuffle_data(X_train,Y_train,X_test,Y_test):
X_train,Y_train = shuffle(X_train,Y_train,random_state = 0)
X_test,Y_test = shuffle(X_test,Y_test,random_state=0)
return X_train,Y_train,X_test,Y_test

def encode_label(Y_train,Y_test):
Y_train_encoded = to_categorical(Y_train,num_classes = 43)
Y_test_encode = to_categorical(Y_test,num_classes = 43)
return Y_train_encoded,Y_test_encode

模型构建

  下面使用keras构建卷积神经网络,keras的使用方法具体参见Keras中文文档。

  模型构建部分代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def build_model():
//使用sequential模型
model = Sequential()
//创建卷积层,输入为32*32*3的图像数据,卷积核大小为3*3
model.add(Conv2D(32,(3,3),input_shape = (32,32,3),activation = 'relu'))
//添加最大池化层
model.add(MaxPooling2D(pool_size=(2,2)))
//dropout修正过拟合
model.add(Dropout(0.5))
//将输出展平
model.add(Flatten())
//普通全连接层
model.add(Dense(512,activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(128,activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(43,activation='softmax'))
//定义损失函数,优化器、评估标准
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
return model

模型训练

  模型定义完成后开始进行模型训练

1
2
//指定训练集、测试机,迭代次数等
model.fit(X_train,Y_train,validation_data=(x_dev,y_dev),epochs=40,batch_size=200,verbose =1)

效果评估

  经过40轮迭代,模型准确率基本能达到99%左右,为了验证模型效果,特意挑选了10张图像进行比对。
  最终结果如下图所示




  其中第6张图类别应该是“Priority road”,其他分类全部正确。

总结

  这节课主要是使用深度学习模型进行图像分类,在没有进行深入调优的情况下,在测试集上获得了90%的正确率,如果想进一步提高分类准确率可以采用效果更好的模型进行轮数更多的训练。这部分的代码详见Term-1-p2-traffic-sign-classifier。

Term-1 p1 lane-detection

发表于 2018-03-19 | 分类于 无人驾驶 |
字数统计: | 阅读时长 ≈

简介

  Udacity是硅谷的一个在线教育网站,主要以介绍AI技术为主,除了基本的课程录像还会有专业老师进行代码review,通过课程会颁发nano degree。其中最为火爆的一门课程就是自动驾驶课程CarNd,这门课程由简入繁分了三个term,第一个term主要是以机器视觉识别道路、交通标志为主,需要掌握基本的深度学习和机器视觉知识;第二个term是介绍传感器以及使用卡尔曼滤波进行定位和速度控制;第三个term主要是路径规划以及系统集成,据说最后会把学员的代码放入真车进行实际测试。Ucacity的课程质量很高,但是有些小贵,出于无奈只好从网上搜集资料,开源的资料包含了前两个term,学完可以对无人驾驶有基本的了解,感兴趣的同学可以试试。

处理流程

  无人驾驶车辆的基本功能之一就是检测车道,之前参加过飞思卡尔智能车竞赛的同学对这个项目应该并不陌生,与摄像头组识别赛道的原理类似,可惜当时我在红外组,没有深入了解相关原理。

  先简单说下利用图像处理技术识别车道的基本流程


车道检测流程

  p1相对来说比较简单,使用opencv的一些操作函数即可实现检测功能。下面详细介绍每个步骤的实现

读入图像

  首先读入图像

1
2
3
4
5
6
7
8
9
10
11
//导入opencv
import cv2

//读取图像
image = cv2.imread("./test_images/solidYellowCurve.jpg")
//展示图像
cv2.imshow('lane',image)
//等待键盘按键
cv2.waitKey()
//销毁图片展示窗口
cv2.destroyAllWindows()

效果如下


车道原图

二值化处理

  opencv对于彩色图像的存储使用GBR格式,与一般RGB格式顺序不太一样,据说是为了做硬件兼容。二值化处理是把读入的彩色图像转成灰度图,方便后续计算。

1
2
//彩色图像转化为灰度图
gray_image = cv2.cvtColor(image,cv2.COLOR_GBR2GRAY)

效果如下


二值化图像

高斯模糊

  高斯模糊是一种图像平滑技术,基本原理是在以中心像素为原点取一定半径内像素点值求加权平均值,权重根据高斯函数求得,结果就是高斯模糊处理后的图片。从数值上看,整体上更加平缓,从图片效果上看变得模糊了。

1
blured_image = cv2.GaussianBlur(gray_image,(5,5),0) //第二个参数表示模糊半径,第三个参数代表高斯函数标准差,半径越大标准差越大,图片越模糊

高斯模糊图像

Canny边缘检测

  由于车道具有明显的边缘,所以使用边缘检测可以提取车道信息,opencv提供了canny边缘检测函数可以实现该功能。

1
canny = cv2.cv2.Canny(blur,250,300)

效果如下


canny边缘检测

ROI区域提取

  在理想情况下(直路且行驶平稳),车道的位置在拍摄的图片中的相对位置不变,我们并不关心除车道以外的其他位置,所以可以通过ROI区域提取获取车道所在区域。针对p1所用到的图片,其ROI区域顶点坐标分别为(0,540),(465,320),(475,320),(960,320),其所形成的不规则区域如下所示


ROI区域

与边缘检测结果进行叠加

1
2
3
4
roi_range = np.array([[(0,540),(465,320),(475,320),(960,320)]],dtype = np.int32)
mask = np.zeros_like(canny) //复制一个和canny图像大小一样的叠加矩阵
cv2.fillPoly(mask,roi_range,255) //设置roi区域的像素值为255,其他区域为0
img_masked = cv2.bitwise_and(canny,mask) //将canny图像和叠加图像求并,ROI区域外的部分都变为0

效果如下


ROI区域提取

Hough直线检测

  现在经过处理后的图像基本只包含ROI区域内的车道信息以及其他干扰信息,这两种信息的区别在于车道由多条直线构成,而干扰信息则不具备这样明显的条件,所以直线检测可以提取ROI区域内的车道。
  opencv提供了霍夫变换函数HoughLines和HoughLinesP用于直线检测,目前先不去深究其原理,后续我再补充。其函数定义如下:

HoughLinesP(image, rho, theta, threshold, minLineLength=None, maxLineGap=None)

  • image:必须是二值图像,推荐使用canny边缘检测的结果图像;
  • rho: 线段以像素为单位的距离精度,double类型的,推荐用1.0
  • theta: 线段以弧度为单位的角度精度,推荐用numpy.pi/180
  • threshod: 累加平面的阈值参数,int类型,超过设定阈值才被检测出线段,值越大,基本上意味着检出的线段越长,检出的线段个数越少。根据情况推荐先用100试试
  • minLineLength:线段以像素为单位的最小长度,根据应用场景设置
  • maxLineGap:同一方向上两条线段判定为一条线段的最大允许间隔(断裂),超过了设定值,则把两条线段当成一条线段,值越大,允许线段上的断裂越大,越有可能检出潜在的直线段

    1
    2
    3
    4
    5
    6
    7
    lines = cv2.HoughLinesP(img_mashed,1,np.pi/180,15,25,20)
    for line in lines:
    for x1,y1,x2,y2 in line:
    cv2.line(img_masked,(x1,y1),(x2,y2),255,12)
    cv2.imshow('img_masked',img_masked)
    cv2.waitKey()
    cv2.destroyAllWindows()

效果如下


直线检测

车道标记

  直线检测完成后,需要对检测后的直线进行过滤,把误差较大的点移除,并且还要将这些点连成直线,进行车道标记。

车道左右边线检测

  首先需要区分车道左边线和右边线,我们已经拿到了直线检测后的直线端点,可以利用斜率进行区分。由于opencv默认原点位于图像左上角顶点,所以左边线斜率为负,右边线斜率为正,并计算每段线段的斜率和截距。

1
2
3
4
5
6
7
8
9
positive_slop_intercept = []    //左边线点构成直线的斜率和截距
negative_slop_intercept = [] //右边线点构成直线的斜率和截距
for line in lines:
for x1,y1,x2,y2 in line:
slop = np.float((y2-y1))/np.float((x2-x1)) //计算斜率
if slop > 0:
positive_slop_intercept.append([slop,y1-slop*x1]) //根据点的坐标和斜率计算截距
elif slop < 0 :
negative_slop_intercept.append([slop,y1-slop*x1])

计算左右边线斜率和截距

  上一步完成了斜率和截距的计算,还要对其进行筛选,把误差较大的线段移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
legal_slop=[]
legal_intercept=[]
slopes=[pair[0] for pair in slop_intercept]
slop_mean = np.mean(slopes) //斜率的均值
slop_std = np.std(slopes) //斜率的标准差
for pair in slop_intercept:
if pair[0] - slop_mean < 3 * slop_std: //挑选出平均值3个标准差误差范围内的斜率和截距
legal_slop.append(pair[0])
legal_intercept.append(pair[1])
if not legal_slop: //如果没有合理范围内的斜率,则使用原始斜率,最终的斜率就是均值
legal_slop = slopes
legal_intercept = [pair[1] for pair in slop_intercept]
slop = np.mean(legal_slop)
intercept = np.mean(legal_intercept)

计算车道两端端点

  计算除了车道的斜率和截距,还需要两个端点才能确定车道的范围。由于我们只关注ROI区域范围内的车道,所以车道左右边线的上顶点的值为ymin=320、下顶点的值为ymax=540,从而求得每条边线的两端顶点。

1
2
xmin = int((ymin - intercept)/slop)
xmax = int((ymax-intercept)/slop)

标记车道

  把标记过车道的图片与原始图片叠加,实现车道的标记。图像叠加通过opencv的addWeight函数实现,该函数定义如下:
cv2.addWeighted(src1, alpha, src2, beta, gamma[, dst[, dtype]])

  • alpha : 第一幅图片中元素的权重
  • beta : 是第二个的权重
  • gamma : 加到最后结果上的值
1
2
//imgae为原始图像,line_image为车道标记图像
res_image = cv2.addWeighted(image,0.8,line_image,1,0)

最终结果如下


车道标记

处理视频

  处理视频需要使用moviepy的VideoFileClip函数

1
2
3
video = VideoFileClip(path) //待处理图像路径
video_after_process = video.fl_image(process_picture) //处理每张图片的函数作为参数
video_after_process.write_videofile("./line_detection.mp4",audio=False) //生成新视频

效果如下

  • 原始视频


  • 处理后视频


代码参考

详见Term-1_lane_detection_demo

ANTLR示例

发表于 2018-03-16 | 分类于 java |
字数统计: | 阅读时长 ≈

目标

  通过antlr把short数组转化为字符串,如data = {1,2,3}转化为data=”\u0001\u0002\u0003”
  大致流程如下:定义语法文件,生成解析类,解析数组

环境准备

  • intellij编辑器
  • maven

下载antlr插件

  intellij支持antlr4插件,可以方便地定义语法文件,并生成解析类。安装过程如下:

  1. 下载ANTLR v4 grammar plugin
  2. 进入Intellij IDEA “Preferences”选项,找到“Plugin”标签,点击“Install plugin from disk…”选项进行安装
  3. 安装结束后重启编辑器,若有如下显示说明安装成功

安装成功

定义.g4文件

  在resources文件夹下新建名为ArrayInit.g4的语法文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 语法文件通常以grammar开头
* grammar后面的语法名必须和文件名一致
*/
grammar ArrayInit;

/**
* 一条名为init的规则,匹配一堆花括号中的逗号分隔的value
*/
init : '{' value (',' value)* '}';
/**
* 一个value可以是嵌套的花括号结构,也可以是一个简单的整数
*/
value : init
| INT
;
/**
* 语法解析器的规则必须以小写字母开头,词法分析器的规则必须以大写字母开头
*/
INT : [0-9]+ ; //定义词法符号INIT,表示一个或多个数字
WS : [\t\r\n]+ -> skip ; //定义词法符号WS,表示空白符号

测试语法规则

  ANTLR v4 grammar plugin提供了ANTLR preview功能,可以时时查看语法规则,并展示结果,如下图所示


安装成功

生成解析类

  右击.g4文件,点击“ANTLR Configure”进行输出路径设置,点击“Generate ANTLR Recognizer”选项,会在目标路径下看到对应的解析类,如下图所示


安装成功

  在生成解析类之后,可能因为没有导入包名报错,需要手动在java文件上方加上“package”包名。

编写监听类

  到现在为止antlr已经完成解析工作,下面要进行转换,需要继承生成的监听类,根据对应的规则进行相应的转换,如把“{“转换成“””,把数字转换成unicode。监听类实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 public class ShortToUnicodeString extends ArrayInitBaseListener {


@Override
public void enterInit(ArrayInitParser.InitContext ctx) {
System.out.print('"');
}

@Override
public void exitInit(ArrayInitParser.InitContext ctx) {
System.out.print('"');
}

@Override
public void enterValue(ArrayInitParser.ValueContext ctx) {
int value = Integer.valueOf(ctx.INT().getText());
System.out.printf("\\u%04x",value);
}
}

执行代码

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws IOException {

String script = "{1,2,3}";
CodePointCharStream charStreams = CharStreams.fromString(script);
ArrayInitLexer lexer = new ArrayInitLexer(charStreams);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ArrayInitParser parser = new ArrayInitParser(tokens);
ParseTree tree = parser.init();

ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(new ShortToUnicodeString(),tree);
}

  第一个antlr小程序就实现了,功能很简单,后续再放高级用法。

ANTLR简介

发表于 2018-03-16 | 分类于 java |
字数统计: | 阅读时长 ≈

  目前AI领域很多开源框架都朝着低门槛的方向发展,以keras为例,极大地简化了tensorflow的使用。我们的项目同样面临这个问题,为了降低使用门槛,需要使用解析器把易于常人理解的配置转化为复杂的后端执行代码。在调研过程中,遇到了有很多表达式解析工具,如ik-expression、aviator等,最后我学习spark-sql的时候了解了spark2.0底层的sql解析器使用了ANTLR ,后来发现ANTLR 是一款优秀的工业级语法分析器,所以决定用到我们的项目中。

基本概念

  • 语言:一门语言是一个有效语句的集合,语句(sentence)由词组组成,词组(phrase)由子词组组成,子词组(subphrase)又由更小的词组组成,依次类推;
  • 语法:语法定义了语言的语义规则,语法中的每条规则定义了一种词组结构;
  • 语义树或语法分析树:代表了语句的结构,其中的每个子树的根节点都使用一个抽象的名字给包含的元素命名。即子树的根节点对应了语法规则的名字。树的叶子结点是语句的符号或者词法的符号;
  • 词法符号:词法符号是一门语言的基本词汇符号,他们可以代表像是“标志符”这样的一类符号,也可以代表一个单一的运算符,或者代表一个关键字;
  • 词法分析器或者词法符号生成器:将字符聚集为单词或者符号(token)的过程称为词法分析(lexical analysis)或者词法符号化(tokenzing)。我们可以把输入文本转换为词法符号的程序称为词法分析器(lexer)。词法符号包含至少两部分信息:词法符号类型和词法符号对应的文本;
  • 语法分析器:识别语言的程序叫做语法分析器(parser)或者句法分析器(syntax analyzer)。句法(syntax)是指约束语言中的各个组成部分之间关系的规则。语法(grammar)是一系列规则的集合,每条规则表述出一种词汇结构。语法分析器通过检查语句的结构是否符合语法规则的定义来验证该语句在特定语言中是否合法;
  • 递归下降的语法分析器:这事自顶向下的语法分析器的一种实现,每条规则都对应语法分析器中的一个函数;
  • 前向预测:语法分析器使用前向预测来进行决策,具体方法是:将输入符号与每个备选分支的起始符号进行比较;
  • 解释器:如果一个程序能够分析计算或者“执行”语句,我们就称之为“解释器”(interpreter),例子有计算器、读取配置文件的程序或者python解释器;
  • 翻译器:如果一个程序能够将一门语言的语句转换为另一门语言的语句,我们称之为翻译器(translator),这样的例子包括java到C#的转换器和普通编译器;

raspberry初始篇

发表于 2018-01-07 | 分类于 raspberry |
字数统计: | 阅读时长 ≈

  之前就在一些论坛上看到过树莓派相关的技术贴,一直没机会玩,碰巧在今年公司圣诞节活动上得到了一块raspberry pi 3 model b开发版,配齐了显示器、摄像头、云台等配件,想先从机器视觉入手做些小项目。配置raspberry的时候踩到了一些坑,下面详细介绍一下从拆箱到能运行opencv获取视频流的过程。

##安装系统
  raspberry pi 3 model b很小巧,只有巴掌大小,最基本的套餐只有板子,其他的配件都不带。电源部分需要自己配一个安卓充电器和数据线,用之前安卓手机的就可以;存储部分需要准备一张sd卡,一样从原来的安卓手机上拆。接下来就是烧录系统,跟着官网的document做,首先从官网下载RASPBIAN STRETCH WITH DESKTOP系统,文件1.6G,建议用utorrent下载。下载好系统后,制作启动盘,我用的是mac,所以下载Etcher。Etcher的使用很简单,跟着官方使用方法3步就可以把系统烧到sd卡中,然后上电启动,raspberry就可以正常运行了。

##触屏校正
  建议买一块带触摸功能的屏幕,没有键盘的时候还能进行简单的点击操作,装个虚拟键盘可以打字。屏幕是淘宝的3.5寸HDMI电阻屏,附上淘宝链接,里面可以找到具体的屏幕参数。在没有安装驱动之前,显示的分辨率和触屏功能都是有问题的,首先要安装驱动,链接里也有详细说明,这里贴一下。

1
2
3
4
5
6
7
8
9
sudo rm -rf LCD-show

git clone https://github.com/goodtft/LCD-show.git

chmod -R 755 LCD-show

cd LCD-show/

sudo ./MPI3508_480_320-show

  这步搞完,屏幕的触摸功能依然不好用,左右是颠倒的,点击左边选择的却是右边,需要进行校正。运行xinput_calibrator工具,屏幕四个角会出现十字,按顺序点击,就搞定了触屏校正。具体的参考见给树莓派装上触摸屏,跟着指示做,亲测有效。
  到此,你的树莓派已经可以正常的显示桌面,和正常操作触摸功能了。

##摄像头安装及监控程序安装
  摄像头也是淘宝的,附上链接摄像头,为了之后能跟踪运动物体,建议连云台一起买了。这款摄像头是usb接口的,即插即用,可以使用lsusb查看设备,如果有“Bus 001 Device 004: ID 1908:2310 GEMBIRD”就说明设备已经连接了。

  接下来就是装一款获取视频流的软件,试试摄像头好不好用,最开始我安装的motion,非常卡!!建议安装另一款软件mjpg-streamer,安装这玩意也是费了九牛二虎之力。说说中间遇到的问题,首先去github复制mjpg-streamer项目,然后make,目前一切正常。然而执行./start.sh就报Init v4L2 failed !! exit fatal错误,开始查了半天说是不支持YUV编码,那改启动脚本,加上-y参数使用YUV编码,改完之后是这样

1
2
3
#start.sh

./mjpg_streamer -i "./input_uvc.so -y" -o "./output_http.so -w ./www

此时应该好了吧?!再执行还他么不行,继续报Init v4L2 failed !! exit fatal i: init_VideoIn failed。又查了半天才知道是v4l2没有加载到linux内核,执行

1
sudo modprobe bcm2835-v4l2

再次执行启动文件,就ok了。后来又修改了一下/etc/modules-load.d/modules.conf文件,把 bcm2835-v4l2加上去,这样就保险了。

  程序启动后出现下面的命令说明启动正常,访问ip:8080/stream.html,就可以获取实时图像了,速度很快。

1
2
3
4
5
6
7
8
9
10
11
12
MJPG Streamer Version.: 2.0
i: Using V4L2 device.: /dev/video0
i: Desired Resolution: 640 x 480
i: Frames Per Second.: -1
i: Format............: YUYV
i: JPEG Quality......: 80
i: TV-Norm...........: DEFAULT
o: www-folder-path......: ./www/
o: HTTP TCP port........: 8080
o: HTTP Listen Address..: (null)
o: username:password....: disabled
o: commands.............: enabled

##opencv安装
  接下来安装opencv,还是先去github下载opencv和opencv_contrib,然后新建个build文件夹,执行cmake编译

1
2
3
4
5
6
cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D INSTALL_C_EXAMPLES=ON \
-D INSTALL_PYTHON_EXAMPLES=ON \
-D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \
-D BUILD_EXAMPLES=ON ..

完事正式编译opencv,使用make -j4命令,然后执行到80%多就死机了,每次都是执行了个把小时,真是无语。。。后来发现4核一起会死机!直接make毛事没有,最终编译了过去。附上参考贴。

  摄像头和opencv都准备好了,可以用opencv试试获取实时视频流,写个python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import cv2
clicked = False
def onMouse(event,x,y,flags,param):
global clicked
if event == cv2.EVENT_LBUTTONUP:
clicked = True
cameraCapture=cv2.VideoCapture(0)
cv2.namedWindow('My Window')
cv2.setMouseCallback('My Window',onMouse)
print 'showing camera feed, Click window or press any key to stop'
success,frame = cameraCapture.read()
while success and cv2.waitKey(1)==-1 and not clicked:
cv2.imshow('My Window',frame)
success,frame = cameraCapture.read()
cv2.destroyWindow('My Window')
cameraCapture.release()

执行之后就可以获取实时视频流了。

##总结
  环境准备是个大坑,每个人遇到的问题都不一样,这时候就知道了google的好处。不过好歹是把基本环境搭建了起来,之后看看用opencv做点小项目玩玩,有新进展将继续更新。

机器学习策略-1

发表于 2017-12-03 | 分类于 机器学习 |
字数统计: | 阅读时长 ≈

  本周视频讲解了在机器学习实践中会用到的一些基本的策略,整理了个人比较重要的一些内容。

正交化

  正交化是一种系统设计属性,是指确保修改算法的指令或者组件时,不会产生或传播副作用到系统的其他组件中。正交系统模块间互相依赖较小,可以相互独立地进行算法验证,能够有效减少开发和测试时间。
  当设计一个监督学习系统时,需要做到下面的4个假设并且是相互正交的:

  1. 模型在训练集上表现良好
  2. 模型在验证集上表现良好
  3. 模型在测试集上表现良好
  4. 模型在实际应用中表现良好

  当发现训练集上表现不够好时,可以采用更大的神经网络或者换一种更好的优化算法;当在验证集上表现不够好时,可以进行正则化处理或者加入更多的训练数据;在测试集上效果不够好时,可以采用更大的验证集进行验证;当在实际应用中表现不好时,可能是因为没有正确设置测试集或者代价函数评估出现问题。

单一数字评估指标

  在构建机器学习系统时,通过设置单一数字评估指标,可以更快更好地评估模型。对于二分类问题,常用的评价指标是精准率(Precision)和召回率(Recall)。将所关注的类作为正类(Positive),其他类作为负类(Negative),并根据分类器在数据集上预测的正确与否,有以下4种情况:

  • TP(True Positive) –正类预测为正类
  • FN(False Negative) –正类预测为负类
  • FP(False Positive) –负类预测为正类
  • TN(True Positive) –负类预测为负类

所以有,精准率为
$$P=\frac{TP}{TP+FP}$$

召回率为

$$R=\frac{TP}{TP+FN}$$

  当遇到下面的情况时,难以分辨哪个模型更优,就需要使用F1 Score指标。



F1 Score定义为
$$F_1=\frac{2}{\frac{1}{P}+\frac{1}{R}}=\frac{2TP}{2TP+FP+FN} $$
F1 Score是精准率和召回率的调和平均数,比简单的去平均数效果要好。

满足和优化指标

  有时候,判断的标准不限于一个单一数字的评估指标。假设有三个不同的分类器性能表现如下:




如要求模型准确率尽可能的高,运行时间在100 ms以内。这里以Accuracy为优化指标,以Running time为满足指标,可以从中选出B是满足条件的最好的分类器。

训练、开发、测试集

   一般把收集到的现有数据分为训练集、验证集和测试集。构建机器学习系统时,在训练集上训练出不同的模型,随后使用验证集对模型的好坏进行评估,确信某个模型效果足够好时再用测试集进行测试。
   验证集和测试集的来源应该是相同的,并且是从所有数据中随机抽取;其次注意数据集大小的划分,原来的分配经验是将数据的70%作为训练集30%作为测试集,或者60%作为训练集20%作为验证集20%作为测试集。当数据量较少时,这种划分方式是合理的。但是在大数据量的场景下,一般不遵守该原则,测试集的大小应该设置得足够提高系统整体性能得可信度,开发集的大小也要设置得足够用于评估几个不同的模型。

改变开发、测试集和评估指标

  在针对某一问题设置好验证集和评估指标后,不是一成不变的,有以后会发现目标设置错误,所以需要改动开发、测试集或评估指标。
  假设有两个猫的图片分类器,评估指标为错误率,模型A的错误了为3%,模型B的错误率为5%。表面上看A的效果更好。但是在实际应用中,A可能会将很多色情图片分类成了猫。所以当在线上部署的时候,算法A会给爱猫人士推送更多更准确的猫的图片,但同时也会给用户推送一些色情图片,这是不能忍受的(为啥我觉得A更好,可能是因为我不是爱猫人士。。。。)。所以,虽然算法A的错误率很低,但是它却不是一个好的算法。这时我们需要改变验证集和测试集或者评估指标。
假设开始的评估指标如下:
$$E=\frac{1}{m_{dev}}\sum_{i=1}^{m_{dev}}I({y_{pred}^{(i)}\not=y^{(i)}})$$
该指标没有区分普通非猫图片和色情图片的误差,但是实际中,我们会希望当把色情图片标记为猫的时候误差更大一些,于是加入权重项$w^{(i)}$
$$E=\frac{1}{\sum w^{(i)}}\sum_{i=1}^{m_{dev}}w^{(i)}I({y_{pred}^{(i)}\not=y^{(i)}})$$
当$x^{(i)}$是色情图片时,$w^{(i)}=10$,否则$w^{(i)}=1$,以此来区分色情图片及其他误识别的图片。所以,要根据实际情况,正确确定一个评判指标,确保这个评判指标最优。

改善深层神经网络:超参数调试、正则化以及优化

发表于 2017-11-24 | 分类于 机器学习 |
字数统计: | 阅读时长 ≈

  这周的视频主要讲了超参数搜索方法、Batch Norm归一化处理、SoftMax回归以及TensorFlow计算框架的使用。

搜索超参数

  在之前的课程中出现了很多超参数,如学习率$a$、神经网络层数$l$,在求解过程中需要对超参数的取值进行调整。超参数的调整可以遵循以下几个规则

  1. 随机搜索优于网格搜索,在进行网格搜索时,同一个纬度取的值较少,如下图,同样是取25个值,在进行网格搜索时超参数1只使用了5个值,而进行随机搜索时参数1和参数2都使用了25个值。


  1. 由粗略搜索到精细搜索,即先在大范围内寻找效果好的区域范围,再在该范围内进行精细搜索
  2. 第1条中说的随机搜索并不是在有效范围内均匀随机取值,而是需要选择合适的坐标系。对于隐藏层数来说,可以在规定范围内进行均匀随机取值,如范围时2到4层,取值时选取2、3、4三个值。但是对于学习率α,其范围在0.0001到1之间,如果α有很大可能在0.0001到0.1之间,若随机均匀取值,那么在0.1到1之间将用去90%的资源,这是不合理的。这时候要是取对数坐标,就能在0.0001到0.1之间取得更多的值。

  为了优化模型效果,通常有两种方式进行训练。第一种是选择一个模型进行训练,观察其代价函数,在其变化的过程中,不断调节超参数,使曲线不上升,这种方法适用于数据量大、时间充足的情况;第二种是同时建立多个模型,观察所有模型的代价函数,选择表现较好的模型,这种方法适用于计算能力较强的情况。

Batch Norm归一化处理

  在视频中Ng把输入层归一化推广到隐含层归一化,并且从前向传播角度说明了Batch Norm的好处。其实可以从公式推导角度证明Batch Norm为什么可以加快训练速度。
  当使用sigmoid函数作为激活函数时,其导数在x=0时取得最大值,当x远离0时其导数基本为0。而对于正态分布而言,有95%的数据在[-2,2]的范围内,这样在进行反向传播时出现梯度消失的数据较少,使得整体上保持较快速度收敛。如果不进行Batch Norm处理,数据就会落在激活函数的饱和区,这样梯度越来越小,甚至出现梯度消失现象。




  下面说说Batch Norm的处理过程
$$\mu=\frac{1}{m}\sum_{i}^{}z^{(i)}$$
$$\sigma^2=\frac{1}{m}\sum_{i}^{} (z^{(i)}-\mu)^2$$
$$z^{(i)}_{norm}=\frac{z^{(i)-\mu}}{\sqrt{\sigma^2+\varepsilon}}$$
$${z^{(i)}_{norm}}^{‘} = \gamma z^{(i)}_{norm}+\beta$$
最后又加入了$\gamma$、$\beta$参数,同样可以通过梯度下降进行求解。

SoftMax回归

  当进行二分类时,最后一层的激活函数可以采用sigmoid函数,当涉及多分类时就要用到softmax函数。先给出softmax函数的形式

$$h_{\theta}(x^{(i)})=
\begin{bmatrix}
p(y^{(i)}=1|x^{(i)};\theta)\\
p(y^{(i)}=2|x^{(i)};\theta)\\
\vdots\\
p(y^{(i)}=3|x^{(i)};\theta)\\
\end{bmatrix}=
\frac{1}{\sum_{j=1}^{k}e^{\theta^{T}_{j}x^{(i)}}}
\begin{bmatrix}
e^{\theta^{T}_{1}x^{(i)}}\\
e^{\theta^{T}_{2}x^{(i)}}\\
\vdots\\
e^{\theta^{T}_{k}x^{(i)}}\\
\end{bmatrix}
$$
举个视频的例子就是




  softmax回归做了两件事情,第一步是将模型的每个输出加上了指数映射,将其取值范围定义在正实数,第二步是把经过转换的输出进行归一化,得到概率分布。在网上给出的推导版本中都是直接给出了softmax的指数形式,其实线性回归、logistic回归、softmax回归都是从广义线性模型推导出来的,当y的分布是正态分布时得到的就是线性模型,当y是两点分布时得到的是logistic回归模型,当y是多项式分布时得到的是softmax回归模型。

TensorFlow的使用

  在进行神经网络搭建事,个人觉得还是keras比较好用,接口抽象的比较友好,不过既然视频提到了TensorFlow,那也研究一下吧,贴上代码

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
#coding=utf-8
import tensorflow as tf
import numpy as np

#准备数据
train_X = np.linspace(-1,1,100)
train_Y = 2 * train_X + np.random.randn(*(train_X.shape))*0.33 + 10

#定义模型
X = tf.placeholder("float")
Y = tf.placeholder("float")
w = tf.Variable(0.0,name="weight")
b = tf.Variable(0.0,name="bias")
#定义均方误差损失函数
loss = tf.square(Y - tf.multiply(X,w) -b)
tran_op = tf.train.GradientDescentOptimizer(0.01).minimize(loss)

#模型训练
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
epoch =1
for i in range(10):
for (x,y) in zip(train_X,train_Y):
_,w_value,b_value = sess.run([tran_op,w,b],feed_dict={X:x,Y:y})
print "Epoch:{}, w:{}, b:{}".format(epoch,w_value,b_value)
epoch+=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
Epoch:1, w:-0.157984390855, b:0.157984390855

Epoch:2, w:-0.311834096909, b:0.315006256104

Epoch:3, w:-0.45063239336, b:0.459648668766

Epoch:4, w:-0.587551951408, b:0.605401754379

Epoch:5, w:-0.719821453094, b:0.749299347401

Epoch:6, w:-0.839450538158, b:0.882369875908

Epoch:7, w:-0.967249691486, b:1.02779650688

Epoch:8, w:-1.08310699463, b:1.16273617744

Epoch:9, w:-1.18981790543, b:1.29001784325
...
Epoch:990, w:1.9911210537, b:10.0346708298

Epoch:991, w:1.99132072926, b:10.0349149704

Epoch:992, w:1.9895298481, b:10.0327787399

Epoch:993, w:1.99043321609, b:10.0338306427

Epoch:994, w:1.99787294865, b:10.0422964096

Epoch:995, w:1.98872160912, b:10.03211689

Epoch:996, w:1.99850010872, b:10.042755127

Epoch:997, w:1.99492764473, b:10.0389518738

Epoch:998, w:1.99669957161, b:10.0407981873

Epoch:999, w:1.99851024151, b:10.0426464081

Epoch:1000, w:2.01060414314, b:10.0547399521

优化算法

发表于 2017-11-18 | 分类于 机器学习 |
字数统计: | 阅读时长 ≈

  这周的视频主要讲了在使用梯度下降求解$w$和$b$时用到的优化方法,主要包括Mini-batch、指数加权平均、动量梯度下降法、RMSprop、Adam优化算法、学习率衰减。这些都是比较实用的方法,在《机器学习实战》中也都有介绍。

Mini-batch

  说到Mini-batch有必要提一下梯度下降和随机梯度下降。以线性回归为例进行说明,假设$(x_{j},y_{j})$是输入输出向量,参数个数为$n$,样本个数为$m$,$h(x)$是预测函数,$J(\theta)$为损失函数,这里取平方误差函数,则有
$$h(\theta)=\sum_{j=0}^{n}\theta_{j}x_{j}$$
$$J(\theta)=\frac{1}{2m}\sum_{i=1}^{m}(y^{i}-h_{\theta}(x^{i}))^2$$

批量梯度下降(Batch Gradient Descent)

  批量梯度下降首先对损失函数$J(\theta)$求导
$$\frac{\partial{J(\theta)}}{\partial{\theta_{j}}}=-\frac{1}{m}\sum_{i=1}^{m}(y^{i}-h_{\theta}(x^{i}))x^{i}_{j}$$
要最小化损失函数,所以每个参数都要沿着负梯度的方向进行更新,有
$$\theta_{j}=\theta_{j}+\eta*\frac{1}{m}\sum_{i=1}^{m}(y^{i}-h_{\theta}(x^{i}))x^{i}_{j}$$
从上面的公式中可以看出,每进行一次更新都需要用到$m$个样本,即最小化所有训练样本的损失函数,最后求得的解是全局最优解。但是当$m$很大时$\theta$更新速度会很慢。

随机梯度下降(Stochastic Gradient Descent)

  为了避免$m$很大时$\theta$更新速度慢的情况,每次更新只使用一个样本,最小化每条样本的损失函数,当样本量很大时也能利用少量数据求得$\theta$最优解。但是不是每次迭代都能向着全局最优解的方向,若遇上噪声容易陷入局部最优。

  而Mini-batch是在BGD和SGD中取折中,一次取一部分数据进行梯度更新,既保证了速度,又能让迭代方向与全局最优解的方向保持一致。

指数加权平均

  在实际应用中有时候会对原始数据进行处理,降低一些奇异值的影响,达到平滑的目的。Ng用温度的例子比较名确的说明了指数加权平均的处理过程,假设$\theta_{t}$代表第$t$天的温度,$v_{t}$代表第$t$天经过指数加权平均处理的温度,则有
$$v_{t} = \betav_{t-1}+(1-\beta)\theta_{t}$$
其中$\beta$是可调节的超参数。乍看上去这个公式和指数没什么关系,展开来看,让$、beta=0.9$
$$v_{100}=0.9v_{99}+0.1\theta_{100}$$
$$v_{99}=0.9v_{98}+0.1\theta_{99}$$
$$v_{98}=0.9v_{97}+0.1\theta_{98}$$
$$……$$
所以有$$v_{100}=0.1\theta_{100}+0.1
0.9\theta_{99}+0.10.9^2\theta_{98}+……+0.10.9^{99}*\theta_{1}$$
不难看出本质上就是以指数形式的递减加权的移动平均,各数值的加权随着时间指数级递减。

Momentum

  在上面提到的梯度下降算法中,$w$和$b$的更新公式是这样的
$$w = w-\eta dw$$
$$b = b-\eta db$$
而动量梯度下降是按照下面的方式进行更新的
$$v_{dw}=\beta v_{dw}+(1-\beta) dw$$
$$w=w-\eta v_{dw}$$
$$v_{db}=\beta v_{db}+(1-\beta)
db$$
$$b=w-\eta v_{db}$$
动量梯度下降是将梯度下降的过程加了一个指数加权平均,这样能更好的控制梯度更新的方向。

RMSprop

   RMSprop与动量梯度下降算法很想,其$w$和$b$的更新公式如下
$$v_{dw}=\beta v_{dw}+(1-\beta)(dw)^2$$
$$w=w-\eta \frac{dw}{\sqrt{v_{dw}}}$$
$$v_{db}=\beta v_{db}+(1-\beta)(db)^2$$
$$b=w-\eta \frac{db}{\sqrt{v_{db}}}$$
这样做是防止在对$w$进行迭代时,$w$的变化幅度很小,而$b$的变化很大。当$w$很小时,$\frac{dw}{\sqrt{v_{dw}}}$这项会将$w$变化幅度增大。对于$b$的迭代道理一样。

Adam优化算法

  Adam算法是把Momentum和RMSprop结合,其$w$和$b$的更新公式如下
$$v_{dw}=\beta_{1} v_{dw}+(1-\beta_{1})dw$$
$$s_{dw}=\beta_{2} s_{dw}+(1-\beta_{2})(dw)^2$$
$$v_{dw}^{correct}=\frac{v_{dw}}{(1-\beta_{1}^{t})}$$
$$s_{dw}^{correct}=\frac{s_{dw}}{(1-\beta_{2}^{t})}$$
$$w=w-\eta \frac{v_{dw}^{correct}}{\sqrt{s_{dw}^{correct}}+\varepsilon}$$
$$v_{db}=\beta_{1} v_{db}+(1-\beta_{1})db$$
$$s_{db}=\beta_{2} s_{db}+(1-\beta_{2})(db)^2$$
$$v_{db}^{correct}=\frac{v_{db}}{(1-\beta_{1}^{t})}$$
$$s_{db}^{correct}=\frac{s_{db}}{(1-\beta_{2}^{t})}$$
$$b=b-\eta \frac{v_{db}^{correct}}{\sqrt{s_{db}^{correct}}+\varepsilon}$$

学习率衰减

  在算法迭代过程中,学习率较大会造成震荡,这时候需要降低学习率。一般会这样设置学习率
$$\eta = \eta \frac{1}{1+decayepoch_num}$$
其中,decay的范围是[0,1],epoch_num是迭代的次数,所以$\eta$会随着迭代次数的增加而减小。

1…456
小建儿

小建儿

码农小白成长记

54 日志
8 分类
18 标签
RSS
© 2019 小建儿
本站访客数: |
| 博客全站共字