1.引言今天我们来看一个OCR相关的文档扫描项目。首先我们先来介绍一些相关理论1.1 什么是光学字符识别 (OCR)OCR(即光学字符识别)是识别图像中的文本并将其转换为电子形式的过程。这些图像可以是手写文本、打印文本(如文档、收据、名片等),甚至是自然场景照片。简单来说,OCR 有两个部分。第一部分是文本检测,确定图像内的文本部分。第二部分文本识别,从图像中提取文本。 结合使用这些技术可以从任何图像中提取文本。具体的流程如下图所示OCR 在各个行业都有广泛的应用(主要目的是减少人工操作)。它已经融入我们的日常生活,并且有很多的应用。1.2 应用领域OCR 越来越多地被各行业用于数字化,以减少人工工作量。这使得从商业文档、收据、发票、护照等中提取和存储信息变得非常容易和高效,几十年前,OCR 系统的构建非常昂贵且繁琐。但计算机视觉和深度学习领域的进步使得我们现在自己就可以构建一个OCR 系统。但构建 OCR 系统需要利用到我们之前介绍的一系列方法。2.项目背景介绍背景:我们有一张随手拍的发票照片如下,我们希望识别出文档信息并扫描思考:我们如何实现上述需求呢?思考:我们如何实现上述需求呢?首先,我们的算法应该能够正确的对齐文档,检测图像的边界,获得目标文本图像其次,我们能对目标文本图像的文档进行扫描下面我们来看一下具体如何在Opencv中处理这里一共需要四大步第一步,边缘检测,第二步,提取轮廓。第三步,透视变换,使得图像对齐,从上图可以看出,我们的图片是一个倾斜的,我们需要通过各种转换方法将其放平。第四步,OCR识别3.边缘检测3.1 原始图像读取首先,我们读取要扫描的图像。下述代码我们计算了一个ratio比例,这是因为我们后续要对图像进行resize操作,里面每一个点的坐标也会有相同的一个变化,因此,我们先算出来这样一个比例,可以推导出resize完之后图像的坐标变化,然后方便我们后续在原图上进行修改。# 读取输入 image = cv2.imread('images/receipt.jpg') #坐标也会相同变化 ratio = image.shape[0] / 500.0 #这里我们首先得到一个比例,方便后续操作 orig = image.copy() 3.2 预处理下面我们对图形进行一些基本的预处理工作,包含resize、灰度处理、二值处理。第一,我们定义了一个resize()函数,它的基本逻辑是根据输入的高度或宽度,自动的计算出宽度或高度。第二,我们将图形进行灰度处理第三,我们使用gaussian滤波器去除噪音点 def resize(image, width=None, height=None, inter=cv2.INTER_AREA): dim = None (h, w) = image.shape[:2] if width is None and height is None: return image if width is None: r = height / float(h) dim = (int(w * r), height) else: r = width / float(w) dim = (width, int(h * r)) resized = cv2.resize(image, dim, interpolation=inter) return resized image = resize(orig, height = 500) # # 预处理 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 灰度处理 gray = cv2.GaussianBlur(gray, (5, 5), 0) # 高斯滤波器 3.3 结果展示经过上述得到预处理后的图片,经过canny边缘检测。# 展示预处理结果 edged = cv2.Canny(gray, 75, 200) # canny边缘检测,得到边缘 print("STEP 1: 边缘检测") cv2.imshow("Image", image) #原始图像 cv2.imshow("Edged", edged) #边缘结果 cv2.waitKey(0) cv2.destroyAllWindows() 现在我们得到边缘检测的结果,可以看到有很多个边缘,我们做文档扫描,需要的是最外面的结果,接下来我们来具体如何实现。3.轮廓检测我们先来思考一下最外面这个轮廓它有什么特点。首先,它是最大的,因此,我们可以根据它的面积或者周长进行排序,这里我们对面积进行排序。然后我们要去找轮廓,这里我们遍历每一个轮廓,然后去计算轮廓的一个近似,因为直接算轮廓的时候不太好算,往往是一个不规则形状,我们做一个矩形近似,然后此时就只需要确定四个点就行。具体代码如下: # 轮廓检测 cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[1] cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:5] #按照面积排序 # 遍历轮廓 for c in cnts: # 计算轮廓近似 peri = cv2.arcLength(c, True) # C表示输入的点集 # epsilon表示从原始轮廓到近似轮廓的最大距离,它是一个准确度参数 # True表示封闭的 approx = cv2.approxPolyDP(c, 0.02 * peri, True) #轮廓近似 # 4个点的时候就拿出来 if len(approx) == 4: screenCnt = approx break # 展示结果 print("STEP 2: 获取轮廓") cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2) cv2.imshow("Outline", image) cv2.waitKey(0) cv2.destroyAllWindows() 4.透视变换透视变换(Perspective Transformation) ,也称为投影变换,它可以用于纠正图像畸变、实现视角变换和图像合成等应用。借助透视变换,我们可以从不同视角获得准确的图像数据,并进行更精确的分析、处理和识别。它的基本原理是基于相机的投影模型,通过处理图像中的四个控制点,将原始图像上的任意四边形区域映射到新的位置和形状上,我们需要得到四个输入坐标和四个输出坐标。通常情况下,透视变换会改变图像中的视角、缩放和旋转等属性。它有几个关键步骤如下:控制点选择:为了进行透视变换,我们需要选择原始图像中的四个控制点(例如四个角点),以定义目标区域的形状和位置。这些控制点应该在原始图像和目标图像之间有明确的对应关系,然后通过高度和宽度信息,我们计算出目标图像的四个控制点透视变换矩阵:通过使用控制点的坐标,可以计算出透视变换矩阵,透视变换矩阵是一个3x3的矩阵。它包含了图像变换所需的所有信息。这里需要输入坐标和输出坐标,然后利用cv2.getPerspectiveTransform函数获取变换矩阵。通过将透视变换矩阵应用于原始图像上的点,可以得到它们在目标图像中的对应位置。接下来我们来看一下具体是如何实现的,首先我们定义了一个ordr_points()函数来获取坐标点,然后我们定义four_point_transform函数来实现透视变换。具体代码如下,# 获取坐标点 def order_points(pts): # 一共4个坐标点 rect = np.zeros((4, 2), dtype = "float32") # 按顺序找到对应坐标0123分别是 左上,右上,右下,左下 # 计算左上,右下 s = pts.sum(axis = 1) rect[0] = pts[np.argmin(s)] rect[2] = pts[np.argmax(s)] # 计算右上和左下 diff = np.diff(pts, axis = 1) rect[1] = pts[np.argmin(diff)] rect[3] = pts[np.argmax(diff)] return rect def four_point_transform(image, pts): # 获取输入坐标点 rect = order_points(pts) (tl, tr, br, bl) = rect # 计算输入的w和h值 widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2)) widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2)) maxWidth = max(int(widthA), int(widthB)) heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2)) heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2)) maxHeight = max(int(heightA), int(heightB)) # 变换后对应坐标位置 dst = np.array([ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype = "float32") # 计算变换矩阵 M = cv2.getPerspectiveTransform(rect, dst) #通过输入和输出坐标,可以计算出M矩阵 warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) # 返回变换后结果 return warped 定义好上述函数之后,接下来看一下经过同时变换之后的结果,为了方便展示,我们再进行二值化处理 # 透视变换 warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio) #这里乘ratio是为了恢复我们原始图像坐标 # 二值处理 warped = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) ref = cv2.threshold(warped, 100, 255, cv2.THRESH_BINARY)[1] cv2.imwrite('scan.jpg', ref) # 展示结果 print("STEP 3: 变换") cv2.imshow("Scanned", resize(ref, height = 650)) cv2.waitKey(0) cv2.destroyAllWindows() 可以看到我们现在就得到了扫描之后得到的结果,并且我们保存为scan.jpg操作5.OCR识别得到扫描后的文档之后,我们需要对其中的字符进行识别,这里我们要用到tesseract工具包,我们先来看一下如何安装相关环境。5.1 tesseract安装安装地址:digi.bib.uni-mannheim.de/tesseract/首先选择一个合适的版本进行安装就行,我这里选择最新的w64版本,如何安装时一直点击下一步就行,但是我们要记住安装的路径。注意:我们需要进行环境变量配置把刚刚安装的路径添加到环境变量中即可接下来我们希望在python中使用它,因此要下载对应的python工具包。安装命令如下:pip install pytesseract5.2 字符识别我们刚刚已经得到了扫描后的图像,并保存为scan.jpg如下所示接下来我们希望把其中的文本字符全部提取出来,我们来看一下具体代码吧from PIL import Image import pytesseract import cv2 import os # 读取图片 image = cv2.imread('scan.jpg') # 灰度处理 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 二值处理 gray = cv2.threshold(gray, 0, 255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] filename = "{}.png".format(os.getpid()) cv2.imwrite(filename, gray) # OCR识别,提取字符 text = pytesseract.image_to_string(Image.open(filename)) print(text) x * KK KK K KR KH KR RK KK WHOLE FOODS TM AR K CE T) WHOLE FOODS MARKET ~ WESTPORT, CT 06880 399 POST RD WEST - (203) 227-6858 365 BACUN LS NP 499 $65 BACON LS NP 4.99 365 BACON LS NP 4.99 365 BACON LS NP 4.99 BROTH CHTC NP 2.19 FLOUR ALMUND NP 11.99 CHKN BRST BNLSS SK NP 18.80 HEAVY CREAM NP 3.39 BALSMC REDUCT NP 6.49 BEEF GRND 85/15 NP 5.04 JUICE COF CASHEW L NP = 8.99 DOCS PINT ORGANIC NF 14.49 HNY ALMOND BUTTER NP 9.99 xeee TAX = 00 9 BAL 101.33 TTA AATDA ABH HH oy 对比一下可以看到识别的字符都比较准确。
0.Opencv环境配置这一环节我们主要介绍Opencv图形处理框架,首先我们要配置好对应的环境。大家在网上可以搜到各种各样的教程,这里我的环境如下python3.6, opencv-contrib-python3.4.1.15,opencv-python3.4.1.15大家可以根据自己的python环境自习安装对应的版本。以我的环境为例,安装命令如下pip install opencv-python==3.4.1.15pip install opencv-contrib-python==3.4.1.151.图像读取接下来我们来看在opencv当中,图像最基本的操作。首先,我们得先知道计算机是如何看一张图像的我们来观察这个图,我们把它叫做丽娜,我们把图片分成了很多很多小方格,然后我们拿出其中的一个方格观察一下。在这个方格当中,我们的每一个就是一个大区域,它是有很多个小块儿所组成的,其中的每一个小格,它叫做一个像素点,计算机当就是由这些像素点来构成一张图像的,那像素点又是什么呢?其实,它就是一个值,我们来看最右边,每一个矩阵里边儿的组成,各种值,例如81,116,…,133,170。它们就是构成像素点的每一个值了,而这个数值大小意味着什么呢?在计算机当中,每一个像素点的值是在0-255之间进行浮动的,表示该点亮度,0表示很亮,255表示很暗然后我们再来看R、G、B三个通道,每一个彩色图都是由RGB三颜色通道组成的,然后每一个矩阵分别对应每一个通道的亮度。对于灰度图而言,就只有一个通道来表示亮度。这些像素点组成了一个矩阵,这个矩阵就表示图像的大小,例如我们假设矩阵是300×300,那相应的rgb三个通道都是300×300,因此整个图形的维度就是(300,300,3)1.1彩色图像读取当我们要分析一张图片时,首先就是读取它转换成一个矩阵的格式,下面我们正式看一下如何读取图片import cv2 #opencv读取的格式是BGR import matplotlib.pyplot as plt import numpy as np %matplotlib inline img=cv2.imread('yangqi.jpg') 在这里我们先导入cv2,然后找到你要读取的图片地址,我这里用的是我们家小狗的图片,他的名字叫洋气,‘yangqi.jpg’,使用cv.imread()函数就会把这张图像读进来。我们来看一下它的结果是什么img array([[[ 99, 141, 148], [ 95, 137, 144], [ 78, 120, 127], ..., [ 69, 88, 103], [ 81, 100, 113], [ 88, 107, 120]], [[ 93, 137, 144], [ 49, 93, 100], [ 86, 130, 137], ..., [175, 197, 209], [195, 217, 229], [200, 222, 234]], [[ 30, 76, 84], [ 23, 66, 75], [ 72, 115, 124], ..., [ 43, 65, 77], [ 42, 64, 76], [ 43, 65, 77]], ..., [[178, 209, 232], [158, 189, 212], [154, 185, 206], ..., [ 68, 78, 85], [ 54, 64, 71], [ 83, 93, 100]], [[164, 195, 216], [138, 169, 190], [168, 197, 218], ..., [ 84, 94, 101], [ 92, 102, 109], [ 87, 97, 104]], [[167, 196, 217], [129, 158, 179], [102, 131, 152], ..., [101, 111, 118], [124, 134, 141], [ 96, 106, 113]]], dtype=uint8) img.shape (238, 218, 3) 可以看到,它返回的是一个ndarray的结构,纬度为(238, 218, 3),因为我们这里是一张彩色图片,所以有三个通道。现在我们已经把图像数据给读进来了,有些时候就是随着我们对图像进行处理,例如边缘检测,或者是一些更复杂的操作。在做操作过程当中,我们想观察一下,这个图像它变换成什么样子了,因此我们需要展示图像,我们可以用matplotlib或者cv2,但是需要注意的是openCV默认读取是BGR的格式,所以在读取的时候,如果我们要用matplotlib的话需要进行相应的转变。我们这里全部都用openCV自带的函数来展示。这里我们利用cv.imshow()函数,传入两个参数,第一个参数表示窗口的名称,第二个函数就是我们要读取的图像数据,也就是我们刚刚得到的那个矩阵#图像展示 cv2.imshow('cat',img) cv2.waitKey(0) # 等待时间,0表示按任意键结束 cv2.destroyAllWindows() 为了方便使用,我们定义函数cv_show(),以后就直接调用这个函数来进行图像读取def cv_show(name,img): cv2.imshow(name,img) cv2.waitKey(0) cv2.destroyAllWindows() 1.2 灰色图像读取在opencv中,默认都是进行彩色图像读取,有的时候我们想要读取灰度图,在这里我们指定cv2.IMREAD_GRAYSCALEimg=cv2.imread('yangqi.jpg',cv2.IMREAD_GRAYSCALE) img array([[138, 134, 117, ..., 90, 102, 109], [134, 90, 127, ..., 198, 218, 223], [ 73, 64, 113, ..., 66, 65, 66], ..., [212, 192, 188, ..., 79, 65, 94], [198, 172, 200, ..., 95, 103, 98], [199, 161, 134, ..., 112, 135, 107]], dtype=uint8) img.shape (238, 218) 可以看到现在的数据变成了一个二维矩阵了,此时是一个单通道,我们再次读取cv2.imshow('cat2',img) cv2.waitKey(0) cv2.destroyAllWindows() 可以看出现在这个图片就变成灰白了2.视频读取视频读取其实和我们图像读取类似,我们先把视频进行拆分,拆分成其中的每一帧,然后基于每一帧图像去做。下面我们具体来看一下如何使用opencv读取视频,首先找到你要读取的视频路径,然后调用cv2.VideoCapture()函数得到视频流,然后我们使用isOpened()来判断是否能打开,如果能打开,使用vc.read()一帧一帧的读取视频信息,然后返回两个值,open为布尔值,frame为读取的每一帧的图像数据vc = cv2.VideoCapture('test.mp4') # 检查是否打开正确 if vc.isOpened(): oepn, frame = vc.read() else: open = False while open: ret, frame = vc.read() if frame is None: break if ret == True: gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) #转换成黑白图 cv2.imshow('result', gray)#显示每一帧图像 if cv2.waitKey(100) & 0xFF == 27:#等待时间 break vc.release() cv2.destroyAllWindows()3.ROI读取3.1 图形切片处理接下来我们来看,当我拿到一张图像时,有的时候我只对某一个区域感兴趣,例如我们刚刚的cat图片,我可能想要观察的是中间的某一个特定区域,这就是region of interest(ROI)读取,如下所示,我们对图形进行切片处理,得到它的左上部分结果。img=cv2.imread('yangqi.jpg') dog=img[0:50,0:200] cv_show('yangqi',dog) 3.2 提取颜色通道有的时候我们需要对图像进行一些特殊的分析,假设一个彩色图像,它是由三个颜色通道组成,BGR(Opencv默认顺序)。在openCV当中,我们可以使用cv2.split()把三个通道分别提取出来看一下b,g,r=cv2.split(img) 这样我们得到了每一个通道的数据r.shape,b.shape,g.shape ((238, 218), (238, 218), (238, 218)) 可以看到每一个通道都是一个二维矩阵。在我们切片完处理之后,我们希望再将其组合在一起,我们可以直接调用cv2.merge(),注意我们的顺序是BGRimg=cv2.merge((b,g,r))#顺序为bgr img.shape 我们前面一直说彩色图片有三个通道,那么每个通道的图像是怎样的呢?我们接下来分别来看看。首先,我们只看R通道的图像,那么我们设置B,G通道对于的像素点值全为0tmp_img = img.copy() tmp_img[:,:,0] = 0#第一个通道B全部设为0 tmp_img[:,:,1] = 0#第二个通道G全部设为0 cv_show('Red',tmp_img) 可以看到得到了一张红色图片,下面类似的操作可以分别得到蓝色和绿色的结果tmp_img = img.copy() tmp_img[:,:,0] = 0 tmp_img[:,:,2] = 0 cv_show('Green',tmp_img) tmp_img = img.copy() tmp_img[:,:,1] = 0 tmp_img[:,:,2] = 0 cv_show('Blue',tmp_img) 4.图像填充接下来我们讨论一下图像填充,这个也比较常见,我们之间在介绍卷积的时候说了padding,它就是在图像在边界进行填充。下面我们就介绍一下使用Opencv来填充图像的一些方法top_size,bottom_size,left_size,right_size = (100,100,100,100)#要填充的大小 replicate = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, borderType=cv2.BORDER_REPLICATE) #复制法 reflect = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size,cv2.BORDER_REFLECT) #反射法 reflect101 = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_REFLECT_101) #反射法101 wrap = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_WRAP) #外包装法 constant = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size,cv2.BORDER_CONSTANT, value=0) #常数填充,0表示黑色 BORDER_REPLICATE:复制法,直接复制最边缘的像素BORDER_REFLECT:反射法,对感兴趣的图像中的像素在两边进行复制,例如,原始为1234,那么我们复制的方法为321|1234|432BORDER_REFLECT_101:反射法101,以最边缘像素为轴进行对称复制,例如,原始为1234,那么我们复制的方法为432|1234|321BORDER_WRAP:外包装法,例如原始为1234,复制为234|1234|123BORDER_CONSTANT:常量法,常数值填充。查看结果import matplotlib.pyplot as plt plt.subplot(231), plt.imshow(img, 'gray'), plt.title('ORIGINAL') plt.subplot(232), plt.imshow(replicate, 'gray'), plt.title('REPLICATE') plt.subplot(233), plt.imshow(reflect, 'gray'), plt.title('REFLECT') plt.subplot(234), plt.imshow(reflect101, 'gray'), plt.title('REFLECT_101') plt.subplot(235), plt.imshow(wrap, 'gray'), plt.title('WRAP') plt.subplot(236), plt.imshow(constant, 'gray'), plt.title('CONSTANT') plt.show() 5.数值运算与图像融合接下来我们来看一下在Opencv中的基本数值运算,我们先读取两张图像,yangqi和另一只猫的图片catimg_dog=cv2.imread('yangqi.jpg') img_cat=cv2.imread('cat.jpg') 5.1加法运算img_cat2= img_cat +10 img_cat[:5,:,0] array([[142, 146, 151, ..., 156, 155, 154], [107, 112, 117, ..., 155, 154, 153], [108, 112, 118, ..., 154, 153, 152], [139, 143, 148, ..., 156, 155, 154], [153, 158, 163, ..., 160, 159, 158]], dtype=uint8) 然后我们对cat加10,这相当于在每个像素点都加10.img_cat2[:5,:,0] array([[152, 156, 161, ..., 166, 165, 164], [117, 122, 127, ..., 165, 164, 163], [118, 122, 128, ..., 164, 163, 162], [149, 153, 158, ..., 166, 165, 164], [163, 168, 173, ..., 170, 169, 168]], dtype=uint8) 在opencv中,我们有两种方法实现两个矩阵相加# 方法一 (img_cat + img_cat2)[:5,:,0] array([[ 38, 46, 56, ..., 66, 64, 62], [224, 234, 244, ..., 64, 62, 60], [226, 234, 246, ..., 62, 60, 58], [ 32, 40, 50, ..., 66, 64, 62], [ 60, 70, 80, ..., 74, 72, 70]], dtype=uint8) # 方法二 cv2.add(img_cat,img_cat2)[:5,:,0] array([[255, 255, 255, ..., 255, 255, 255], [224, 234, 244, ..., 255, 255, 255], [226, 234, 246, ..., 255, 255, 255], [255, 255, 255, ..., 255, 255, 255], [255, 255, 255, ..., 255, 255, 255]], dtype=uint8) 方法一:直接如上所示,用a+b,这样得出来的结果大家可以看第一个为38,我们知道刚刚两个矩阵的第一个元素相加为142+152=294,之前我们说每一个像素的范围为0-255,在这里超过了255,因此我们这里实际上是294%256=38,进行了一个取余数的操作方法二:cv2.add(),这种方法会将超过255的值定义为2555.2 图像融合首先我们来看一下两张图片的维度img_cat.shape,img_dog.shape ((414, 500, 3), (238, 218, 3)) 由于维度不同,不能直接相加,这里我们需要使用cv.resize将dog的图片维度转换,这在我们之前CNN中也用过类似的操作。img_dog = cv2.resize(img_dog, (500, 414)) img_dog.shape (414, 500, 3) 现在两张图片的纬度一样了,我们可以调用cv2.addWeighted()将两张图片融合。这里有5个参数,分别是图片1数据、图片1权重、图片2数据、图片2权重、常数项。具体操作就是0.4*cat+0.6*dog+0res = cv2.addWeighted(img_cat, 0.4, img_dog, 0.6, 0) 可以看到这个结果当中,左边像猫,右边像狗,这样就好像两张图像就融合在一起了。6. 总结我们介绍了Opencv环境配置、基本的图像和视频读取、读取感兴趣的部分图像、图像填充、以及在Opencv中的基本数值计算和图像融合。
1.腐蚀操作腐蚀操作是图像处理中常用的一种形态学操作,我们通常用于去除图像中的噪声、分割连通区域、减小目标物体的尺寸等。腐蚀操作的原理是,在给定的结构元素下,遍历图像的每个像素,并将其值替换为该像素周围邻域内像素的最小值。结构元素控制了腐蚀的邻域范围和形状。邻域内的任何一个像素为黑色(0),则中心像素也将被置为黑色(0)。这样可以缩小或消除二值图像中的前景目标。在OpenCV中,我们可以使用cv2.erode()来实现腐蚀操作语法如下cv2.erode(src, kernel, iterations) 参数说明:src:输入的二值图像,通常为单通道灰度图像。kernel:腐蚀操作的结构元素,用于定义腐蚀的邻域大小和形状。可以使用 cv2.getStructuringElement() 函数创建不同形状的结构元素。iterations:腐蚀操作的迭代次数,表示应用腐蚀的重复次数。接下来我们先来看一张图:img = cv2.imread('JOJO.png') cv2.imshow('img', img) cv2.waitKey(0) cv2.destroyAllWindows() 大家可以看到,除了JOJO之外,它的周围还有很多斜线,我们可以利用腐蚀操作来消除。# 创建结构元素 (3x3 方框形) kernel = np.ones((3, 3), dtype=np.uint8) # 执行腐蚀操作 eroded = cv2.erode(img, kernel, iterations=1) # 显示结果 cv2.imshow('Eroded Image', eroded) cv2.waitKey(0) cv2.destroyAllWindows() 可以看到,这些斜线变得很浅,因为被他周围的黑色所影响,并且原始图像的字母也变得更小了,因为腐蚀操作减少了一部分信息。我们可以更改iterations的值,来增加迭代次数,迭代的次数越多,则腐蚀的越严重,具体结果如下erosion_1 = cv2.erode(img,kernel,iterations = 1)#1次迭代 erosion_2 = cv2.erode(img,kernel,iterations = 2)#2次迭代 erosion_3 = cv2.erode(img,kernel,iterations = 3)#3次迭代 res = np.hstack((erosion_1,erosion_2,erosion_3))#水平堆砌 # 显示结果 cv2.imshow('res', res) cv2.waitKey(0) cv2.destroyAllWindows() 腐蚀操作和膨胀(Dilation)操作相对应,二者经常组合使用以实现更复杂的形态学图像处理任务,接下来我们来看看膨胀操作。2.膨胀操作说完了腐蚀操作之后,我们再来看一下它的逆操作,膨胀操作。我们在上面的腐蚀操作中,在消除噪声的同时,把有价值的信息也减少了。因此我们希望将这些有价值的信息增大,这样就要利用到膨胀操作。在 OpenCV 中,膨胀操作是通过 cv2.dilate() 函数实现的。该函数接受三个参数:输入图像、结构元素和迭代次数。结构元素可以通过 cv2.getStructuringElement() 函数创建,它定义了膨胀操作的邻域大小和形状。常见的结构元素形状包括矩形、椭圆和十字形。我们还是用刚刚的例子# 原始图像 img = cv2.imread('JOJO.png') cv2.imshow('img', img) cv2.waitKey(0) cv2.destroyAllWindows() 接下来我们使用腐蚀操作消除细线# 腐蚀操作 kernel = np.ones((5,5),np.uint8) dige_erosion = cv2.erode(img,kernel,iterations = 1) cv2.imshow('erosion', erosion) cv2.waitKey(0) cv2.destroyAllWindows() 我们可以看见其中字母也变小了,我们想恢复其原始信息。# 膨胀操作 kernel = np.ones((3,3),np.uint8) dige_dilate = cv2.dilate(dige_erosion,kernel,iterations = 1) cv2.imshow('dilate', dige_dilate) cv2.waitKey(0) cv2.destroyAllWindows() 膨胀操作的效果取决于结构元素的形状和大小,以及迭代次数。增加迭代次数会使目标物体区域更大,边界更粗糙。通常情况下,一个或两个迭代次数就足够了。膨胀操作通常与腐蚀操作结合使用,以在图像中执行形态学处理。这种组合的方法称为开运算(Opening)和闭运算(Closing),接下来我们来看看如何实现3.开运算和闭运算开运算是先进行腐蚀操作,再进行膨胀操作。它主要用于去除图像中的噪点、小的干扰物或者分离连通的对象。闭运算是先进行膨胀操作,再进行腐蚀操作。它主要用于填充图像中的小洞孔或者连接分离的对象。==开运算==:在opencv中,通过调用cv2.morphologyEx()函数,并指定操作类型为cv2.MORPH_OPEN,实现开运算# 开:先腐蚀,再膨胀 img = cv2.imread('dige.png') kernel = np.ones((5,5),np.uint8) opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) cv2.imshow('opening', opening) cv2.waitKey(0) cv2.destroyAllWindows() 可以看到开运算可以让我们先去除边缘细线,然后再增加信息恢复到原始结果。==闭运算==:在opencv中,通过调用cv2.morphologyEx()函数,并指定操作类型为cv2.MORPH_CLOSE,实现闭运算# 闭:先膨胀,再腐蚀 img = cv2.imread('JOJO.png') kernel = np.ones((5,5),np.uint8) closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) cv2.imshow('closing', closing) cv2.waitKey(0) cv2.destroyAllWindows()在闭运算中,由于一开始我们加粗了边缘细线,导致后续我们即使做了腐蚀操作也不能完全去除。这两种方法可以用来改善图像的质量、去除噪声或者填充空洞,具体根据我们的需求进行更改。4.礼帽与黑帽礼帽 = 原始输入-开运算结果黑帽 = 闭运算-原始输入通过使用礼帽和黑帽操作,可以突出图像中细微的亮或暗结构,或者检测背景中的亮或暗区域。==礼帽操作==在opencv中,我们通过调用 cv2.morphologyEx() 函数,并指定操作类型为 cv2.MORPH_TOPHAT,执行礼帽操作#礼帽 img = cv2.imread('JOJO.png') tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel) cv2.imshow('tophat', tophat) cv2.waitKey(0) cv2.destroyAllWindows() ==黑帽操作==:黑帽操作是礼帽操作的相反过程,用于突出图像中微小结构或背景中的暗区域。在Opencv中,通过调用 cv2.morphologyEx() 函数,并指定操作类型为cv2.MORPH_BLACKHAT,执行黑帽操作。#黑帽 img = cv2.imread('JOJO.png') blackhat = cv2.morphologyEx(img,cv2.MORPH_BLACKHAT, kernel) cv2.imshow('blackhat ', blackhat ) cv2.waitKey(0) cv2.destroyAllWindows() 5.梯度运算梯度 = 膨胀-腐蚀首先,我们分别实现膨胀和腐蚀操作。# 梯度=膨胀-腐蚀 img = cv2.imread('JOJO.png') kernel = np.ones((5,5),np.uint8) dilate = cv2.dilate(img,kernel,iterations = 1) erosion = cv2.erode(img,kernel,iterations = 1) res = np.hstack((dilate,erosion)) cv2.imshow('res', res) cv2.waitKey(0) cv2.destroyAllWindows() 接下来我们实现==梯度运算==# 梯度运算 gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel) cv2.imshow('gradient', gradient) cv2.waitKey(0) cv2.destroyAllWindows() 大家可以看出这个图片就是用膨胀操作-腐蚀操作的结果。
0.引言在图像处理中,梯度是指图像中像素灰度变化的速率或幅度,我们先来看下面这张图假设我们想要计算出A点的梯度,我们可以发现A点位于边缘点,A点左边为黑色,右边为白色,而计算图像的梯度可以提取出图像中的边缘信息,我们常用的方法是使用Sobel算子或Scharr算子进行梯度计算。接下来我们分别来看看具体是如何做的1. Sobel算子和我们之前介绍的各种图像计算的方法类似,我们利用某一个大小的卷积核来进行计算,我们这里也一样,Sobel算子有两个核,一个用于计算图像在水平方向上的差异(x方向梯度),另一个用于计算图像在垂直方向上的差异(y方向梯度)。这两个核可以在水平和垂直方向上检测出图像中的边缘信息。下面是Sobel算子在x和y方向上的核矩阵:我们来看他这个是如何来识别边缘的,以x方向为例,如果两边相差太大了,那么结果的绝对值也会比较大,说明应该在边缘点附近,如果两边值非常接近,则结果也会趋于0,此时说明不在边缘地附近。y方向也是同理。接下来我们看一下如何在Opencv中实现,我们调用cv2.Sobel()函数,dst = cv2.Sobel(src, ddepth, dx, dy, ksize) ddepth:输出图像的深度(数据类型),一般我们指定为64位浮点数型,设为CV_64Fdx和dy分别表示水平和竖直方向ksize是Sobel算子的大小我们以下面这张图为例计算梯度,# 导入原始图 img = cv2.imread('pie.png',cv2.IMREAD_GRAYSCALE) cv2.imshow("img",img) cv2.waitKey() cv2.destroyAllWindows() x方向计算梯度# 定义图像展示函数 def cv_show(img,name): cv2.imshow(name,img) cv2.waitKey() cv2.destroyAllWindows() sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3) cv_show(sobelx,'sobelx') 我们发现只有一半。我们来思考一个问题哈,从黑到白是正数,白到黑就是负数了,所有的负数会被截断成0,所以导致我们右半边的边缘无法显示,因此我们要取绝对值来解决这个问题。我们调用cv2.convertScaleAbs(sobelx) 实现将结果转换为无符号8位整数sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3) sobelx = cv2.convertScaleAbs(sobelx) #实现将结果转换为无符号8位整数 cv_show(sobelx,'sobelx') 现在我们基本找到了边缘,接下来我们还需要看y方向的情况计算y方向的梯度sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3) sobely = cv2.convertScaleAbs(sobely) cv_show(sobely,'sobely') 现在我们分别得到了x方向和y方向的边缘,接下来我们进行求和处理。求和调用cv2.addWeighted函数sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0) cv_show(sobelxy,'sobelxy') 接下来,我们以之前小狗洋气的图片来看一下它的梯度结果# 原始图像 img = cv2.imread('yangqi.jpg',cv2.IMREAD_GRAYSCALE) cv_show(img,'img') 接下来我们来看一下求梯度后的结果img = cv2.imread('yangqi.jpg',cv2.IMREAD_GRAYSCALE) sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3) sobelx = cv2.convertScaleAbs(sobelx) sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3) sobely = cv2.convertScaleAbs(sobely) sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0) cv_show(sobelxy,'sobelxy') 可以看到,我们把图片所有的轮廓都给提取出来了。Sobel算子具有简单且易于实现的优点,它对噪声有一定的抑制作用,并可以快速检测出图像中的边缘。然而,Sobel算子也存在一些局限性,如对于较弱的边缘响应不敏感,并且可能会产生较粗的边缘。对于更复杂的场景,可能需要结合其他的边缘检测算法或采用更高级的技术。2. Scharr算子Scharr算子和Sobel算子很像,但在边缘检测方面具有更好的性能。Scharr算子也是基于一阶导数的近似,和Sobel算子一样,Scharr算子也有两个3x3的核、具体核矩阵如下:在Opencv中,我们调用cv2.Scharr()函数实现。scharrx = cv2.Scharr(img,cv2.CV_64F,1,0) scharry = cv2.Scharr(img,cv2.CV_64F,0,1) scharrx = cv2.convertScaleAbs(scharrx) scharry = cv2.convertScaleAbs(scharry) scharrxy = cv2.addWeighted(scharrx,0.5,scharry,0.5,0) cv_show(scharrxy,'scharr') 从结果来看,与Sobel算子相比,检测出来的边缘更多,因为Scharr算子具有更高的方向敏感性和更好的旋转不变性,能够更准确地检测到边缘,并且在边缘方向变化较大的情况下效果更好。因此,在很多应用中,Scharr算子常常被用作替代Sobel算子的选择。3.Laplacian算子Laplacian算子常用于检测图像中的边缘和纹理,但是它计算图像的二阶导数,以此捕捉到图像中的灰度变化,它只有一个核在Opencv中,我们调用cv2.Laplacian()函数实现,因为这里只有一个核,因此不用分别计算x方向和y方向,直接计算一个即可laplacian = cv2.Laplacian(img,cv2.CV_64F) laplacian = cv2.convertScaleAbs(laplacian) 从结果来看,Laplacian单独使用对边缘检测的效果一般,因为它是一个二阶导数运算,所以图像中的噪声会被放大。因此,在应用Laplacian算子之前,可能需要对图像进行预处理,例如平滑/模糊来降低噪声的影响,我们一般不会单独使用Laplacian算子,而是结合其他的方法使用。
GoogleNet(Inception v1)是由Google团队在2014年提出的深度卷积神经网络架构。它是为解决图像分类任务而设计的,并在ImageNet图像分类挑战赛中取得了很好的成绩。与VGGNet、LeNet、AlexNet有较大不同。在之前我们介绍的架构中VGG大量使用了3×3卷积,AlexNet使用了5×5,而NiN使用了1×1。因此,在构建卷积层时,我们要决定过滤器的大小究竟是1×1,3×3还是5×5,或者要不要添加池化层。而GoogleNet网络的想法就是**我全都要!**GoogleNet最显著的特点是采用了一系列并行连接的Inception模块。原文作者提到,“Inception”这个名字的想法来自于电影《盗梦空间》一个著名的网络梗:WE NEED TO GO DEEPER。虽然网络架构因此变得更加复杂,但网络表现却非常好,下面我们来看一下GoogleNet具体组成架构:1×1卷积1×1 卷积由 NiN引入。1×1卷积与ReLU一起使用。因此,NiN最初使用它来引入更多的非线性,以提高网络的表示能力,因为NiN的作者认为数据是非线性形式的。在GoogLeNet中,1×1卷积被用作降维模块,以减少计算量。通过减少计算瓶颈,可以增加深度和宽度。我举一个简单的例子来说明这一点。假设我们需要执行 5×5 卷积而不使用 1×1 卷积,如下所示:运算次数 = (14×14×48)×(5×5×480) = 112.9M使用 1×1 卷积:1×1的运算次数 = (14×14×16)×(1×1×480) = 1.5M5×5的运算次数 = (14×14×48)×(5×5×16) = 3.8M总操作次数 = 1.5M + 3.8M = 5.3M,这比 112.9M 小得多!!!事实上,上面的例子是在inception时 5×5 卷积的计算。(我们可以认为,当降维时,实际上我们正在以非线性的方式进行从高维到低维的映射。相反,对于PCA,它执行线性降维。)因此,与没有 1×1 卷积的情况相比,我们可以在不增加操作数量的情况下构建 inception 模块。1×1卷积可以帮助减小模型大小,这在某种程度上也有助于减少过拟合问题2. Inception模块与之前介绍的lexNet 和 VGGNet,每层的 conv 大小都是固定的相比。Inception模块将输入特征图分别进行多个不同尺寸的卷积和池化操作,并将它们的结果进行拼接。这样可以同时捕捉到不同尺度上的特征,从而提高了特征的表达能力。具体而言,Inception模块包含了1×1、3×3和5×5的卷积层,以及1×1卷积和3×3最大池化层。通过使用不同尺寸的卷积和池化操作,网络能够有效地捕捉到局部和全局的特征。3. 全局平均池化层:在网络的最后,GoogleNet使用全局平均池化层将特征图的大小降为1×1。这种操作可以将特征图中每个通道的特征合并为一个标量值,从而减少参数数量,并且有助于提取更加全局的特征。之前的网络以全连接(FC)层用于网络末端,所有输入都连接到每个输出。上面的权重(连接)数量 = 7×7×1024×1024 = 51.3M在GoogLeNet中,全局平均池化在网络末端使用,通过对每个特征图从7×7到1×1进行平均权重数量 = 04.训练辅助分类器在模型中间的一部分有softmax分支,它们仅用于训练。这些分支是辅助分类器,包括:5×5 平均池化(步幅 3)1×1 卷积(128 个过滤器)1024 FC1000FCSoftmax5.基本架构了解了上面介绍的基本单元之后,我们就可以谈谈整体的网络架构了。总共有22层!与之前的AlexNet、LeNet和VGGNet相比,它已经是一个非常深的模型了。 而且我们可以看到有很多inception模块连接在一起,甚至可以更深入。以下是各层参数的详细信息。我们其实可以扩展1×1卷积的例子来自己计算运算次数。6.Pytorch代码实现import warnings warnings.filterwarnings('ignore') # 导入相关库 import torch import torch.nn as nn import torch.optim as optim from torch.nn import functional as F import torchvision import torchvision.transforms as transforms import matplotlib.pyplot as plt # inception块 class Inception(nn.Module): # c1--c4是每条路径的输出通道数 def __init__(self, in_channels, c1, c2, c3, c4, **kwargs): super(Inception, self).__init__(**kwargs) # 线路1,单1x1卷积层 self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1) # 线路2,1x1卷积层后接3x3卷积层 self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1) self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1) # 线路3,1x1卷积层后接5x5卷积层 self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1) self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2) # 线路4,3x3最大汇聚层后接1x1卷积层 self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1) self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1) def forward(self, x): p1 = F.relu(self.p1_1(x)) p2 = F.relu(self.p2_2(F.relu(self.p2_1(x)))) p3 = F.relu(self.p3_2(F.relu(self.p3_1(x)))) p4 = F.relu(self.p4_2(self.p4_1(x))) # 在通道维度上连结输出 return torch.cat((p1, p2, p3, p4), dim=1) # 构建google-Net b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1), nn.ReLU(), nn.Conv2d(64, 192, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32), Inception(256, 128, (128, 192), (32, 96), 64), nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64), Inception(512, 160, (112, 224), (24, 64), 64), Inception(512, 128, (128, 256), (24, 64), 64), Inception(512, 112, (144, 288), (32, 64), 64), Inception(528, 256, (160, 320), (32, 128), 128), nn.MaxPool2d(kernel_size=3, stride=2, padding=1)) b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128), Inception(832, 384, (192, 384), (48, 128), 128), nn.AdaptiveAvgPool2d((1,1)), nn.Flatten()) net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10)) # Xavier初始化:防止梯度爆炸,这是CNN常用的操作,特别对于GoogleNet这种已经算比较深的网络而言,特别有效,之前我们也介绍过他的具体公式。 def init_weights(m): if type(m) == nn.Linear or type(m) == nn.Conv2d: #对全连接层和卷积层初始化 nn.init.xavier_uniform_(m.weight) net.apply(init_weights) # 检查是否有可用的GPU device = torch.device('cuda'if torch.cuda.is_available() else 'cpu') model = net.to(device) # 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.1) # 加载Fashion-MNIST数据集 transform = transforms.Compose([ transforms.ToTensor(), transforms.Resize((96,96)), transforms.Normalize((0.5,), (0.5,)) ]) trainset = torchvision.datasets.FashionMNIST(root='./data', train=True, download=True, transform=transform) trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2) testset = torchvision.datasets.FashionMNIST(root='./data', train=False, download=True, transform=transform) testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=False, num_workers=2) # 训练模型 num_epochs = 10 train_losses = [] test_losses = [] train_accs = [] test_accs = [] for epoch in range(num_epochs): train_loss = 0.0 train_total = 0 train_correct = 0 model.train() for images, labels in trainloader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() _, predicted = torch.max(outputs.data, 1) train_total += labels.size(0) train_correct += (predicted == labels).sum().item() train_loss /= len(trainloader) train_accuracy = 100*train_correct / train_total train_losses.append(train_loss) train_accs.append(train_accuracy) test_loss = 0.0 test_total = 0 test_correct = 0 model.eval() with torch.no_grad(): for images, labels in testloader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) test_loss += loss.item() _, predicted = torch.max(outputs.data, 1) test_total += labels.size(0) test_correct += (predicted == labels).sum().item() test_loss /= len(testloader) test_accuracy = 100*test_correct / test_total test_losses.append(test_loss) test_accs.append(test_accuracy) print(f"Epoch {epoch+1}/{num_epochs}: Train Loss: {train_loss:.4f}, Train Acc: {train_accuracy:.2f}%, Test Loss: {test_loss:.4f}, Test Acc: {test_accuracy:.2f}%") # 绘制训练误差和测试误差曲线 plt.figure(figsize=(10, 5)) plt.plot(range(1, num_epochs+1), train_losses, label='Train Loss') plt.plot(range(1, num_epochs+1), test_losses, label='Test Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.title('Training and Testing Loss') plt.legend() plt.show() # 绘制训练准确率和测试准确率曲线 plt.figure(figsize=(10, 5)) plt.plot(range(1, num_epochs+1), train_accs, label='Train Acc') plt.plot(range(1, num_epochs+1), test_accs, label='Test Acc') plt.xlabel('Epoch') plt.ylabel('Accuracy') plt.title('Training and Testing Accuracy') plt.legend() plt.show() Epoch 1/10: Train Loss: 2.0245, Train Acc: 0.24, Test Loss: 1.2393, Test Acc: 0.50 Epoch 2/10: Train Loss: 0.8974, Train Acc: 0.67, Test Loss: 1.9615, Test Acc: 0.29 Epoch 3/10: Train Loss: 0.6790, Train Acc: 0.75, Test Loss: 0.4919, Test Acc: 0.81 Epoch 4/10: Train Loss: 0.4275, Train Acc: 0.84, Test Loss: 0.4063, Test Acc: 0.85 Epoch 5/10: Train Loss: 0.3615, Train Acc: 0.86, Test Loss: 0.3978, Test Acc: 0.84 Epoch 6/10: Train Loss: 0.3253, Train Acc: 0.88, Test Loss: 0.3284, Test Acc: 0.88 Epoch 7/10: Train Loss: 0.2964, Train Acc: 0.89, Test Loss: 0.3079, Test Acc: 0.89 Epoch 8/10: Train Loss: 0.2726, Train Acc: 0.90, Test Loss: 0.2953, Test Acc: 0.89 Epoch 9/10: Train Loss: 0.2540, Train Acc: 0.90, Test Loss: 0.3061, Test Acc: 0.88 Epoch 10/10: Train Loss: 0.2380, Train Acc: 0.91, Test Loss: 0.2799, Test Acc: 0.90 从结果可以看出,GoogLeNet的精确度超过了0.9,比之前的AlexNet更好。总结GoogleNet的是通过Inception模块的并行连接和多尺度的卷积和池化操作来提高特征的表示能力。它相对于传统的深度网络具有更高的计算效率和更强大的特征学习能力。基本思想是Inception网络不需要人为决定使用哪个过滤器或者是否需要池化,而是由网络自行确定这些参数,你可以给网络添加这些参数的所有可能值,然后把这些输出连接起来,让网络自己学习它需要什么样的参数,采用哪些过滤器组合。简单来说,这些参数都是试出来的,Google太有钱了。GoogleNet的成功开创了一系列基于Inception架构的后续版本(如Inception v2、v3等),为深度学习在计算机视觉任务中的广泛应用奠定了基础。
1.线性回归基本概念==线性回归==是机器学习中非常常用的模型之一,特别在研究定量数据的问题中,它能分析变量之间的关系,并给出很好的解释。此外,它还是新方法的一个良好起点:许多有趣的统计学习方法可以被视为线性回归的推广或扩展。例如Lasso回归,岭回归,logistic regression,softmax回归。具体理论介绍部分大家可以看我这篇文章:统计学习方法之线性回归详解简单线性回归模型具体形式可以如下表示:写成向量的形式:在我们拿到一堆数据之后,我们要做就是找到最好的参数W,bW,bW,b,如何找到最好的参数呢?在那之前我们介绍一下==损失函数==和==梯度下降==2.损失函数损失函数是衡量一个模型拟合的重要指标,表示实际值和拟合值之间的差距,在线性回归中,损失函数也被称作==平方误差函数==。我们之所以要求出误差的平方和,是因为一般我们认为误差是非负的,而误差绝对值在求导时不太便利,而误差平方损失函数,对于大多数问题,特别是回归问题,都是一个合理的选择。具体定义如下:我们的目标便是选择出可以使得损失函数能够最小的模型参数,因为线性回归是一个很简单的问题,所有大部分情况下都存在解析解,对L求梯度为0的点。为了方便表示,这里我将bbb也放入到WWW中,则有:3.梯度下降==梯度下降==是一种==优化算法==,后续还会介绍一些其他的优化方法,例如Adam,SGD等。本章暂时用梯度下降来计算参数。梯度下降背后的思想是:开始时我们随机选择一个参数的组合,计算损失函数,然后我们寻找下一个能让损失函数值下降最多的参数组合。具体公式如下:1.初始化一组参数值2.在负梯度方向不断更新参数,其中η\etaη是==学习率==,是一个超参数,它决定了我们沿着能让损失函数下降程度最大的方向步长多大,在梯度下降中,我们每一次都同时让所有的参数减去学习速率乘以损失函数的导数。需要我们提前给定。我们持续这么做直到得到一个==局部最小值==(local minimum),因为我们并没有尝试完所有的参数组合,所以不能确定我们得到的局部最小值是否便是==全局最小值==(global minimum),选择不同的初始参数组合,可能会找到不同的局部最小值。此时如何设置合适的η\etaη值是需要我们考虑的,如果η\etaη太大,则可能到不了最低点,导致无法收敛,如果η\etaη太小,那收敛过程太慢,这些细节问题在之后再讨论,接下来我们来看看如何实现一个简单的线性回归模型。4.Pytorch实现线性回归导入相关库import random import torch import numpy as np import matplotlib.pyplot as plt %matplotlib inline 4.1 生成数据集为了方便举例,这里使用模拟数据集进行展示。假设样本来自标准正态分布,每个样本有两个特征,我们生成1000个数据集,w=[1,−1]Tw=[1,-1]^Tw=[1,−1]T,b=2b=2b=2,ϵ\epsilonϵ是一个均值为0,标准差为0.1的正态分布def simulation_data(w,b,n): X = torch.normal(0,1,(n,len(w)))#生成标准正态分布 y = torch.matmul(X, w)+b#计算回归拟合值 y += torch.normal(0,0.1,y.shape) #加上随机扰动项 return X,y.reshape((-1,1)) true_w = torch.tensor([-1,1],dtype=torch.float32) true_b = 2 features, target = simulation_data(true_w,true_b,1000) 此时已经生成好了模拟数据集,下面我们绘制图形观察一下plt.scatter(features[:,(1)].detach().numpy(),target.detach().numpy(),2) 从上图我可以看出,y和一个特征之间的关系呈现明显的线性关系。4.2 初始化参数下面我们开始初始化我们要求的参数,通常将www设置为均值为0的正态分布,bbb设置为0向量w = torch.normal(0,0.1,(2,1),requires_grad=True) b = torch.zeros(1,requires_grad=True) 4.3 定义回归模型初始化参数之后,我们下一步就是开始定义我们的线性回归模型def reg(X,w,b): return torch.matmul(X,w)+b 4.4 计算损失函数def loss_fun(y_hat,y): return(y_hat-y)**2/2/len(y) 4.5 使用梯度下降求解参数def gd(params,n): with torch.no_grad():#在外面不需要求解梯度,只有参数更新的时候求参数 for param in params: param -= n*param.grad#进行梯度下降 param.grad.zero_()#重新将梯度设置为0,这样就不会受到上一次影响 n = 0.01#学习率 num_epochs = 3#训练次数 net = reg loss = loss_fun X = features y = target for epoch in range(num_epochs): l = loss(reg(X,w,b),y)#计算损失函数 l.sum().backward()#反向传播求梯度 gd([w,b],n)#更新参数 with torch.no_grad(): train = loss(net(features,w,b),target) print(f'epoch{epoch+1},loss:{float(train.mean()):f}') epoch1,loss:0.001797 epoch2,loss:0.001763 epoch3,loss:0.001729 由于本例中是模拟数据集,我们知道真实的参数,因此可以计算出参数估计的误差print(f'w的误差:{true_w-w.reshape(true_w.shape)}') print(f'b的误差:{true_b-b}') w的误差:tensor([-0.8461, 0.8311], grad_fn=<SubBackward0>) b的误差:tensor([1.4857], grad_fn=<RsubBackward1>)
1.Lenet1.1理论介绍经过前面的介绍,我们已经了解了卷积神经网络的基本模块,接下来我们来讨论几个经典的神经网络结构,首先介绍LeNet-5。LeNet是最早的卷积神经网络之一,其被提出用于识别手写数字和机器印刷字符。1998年,Yann LeCun第一次将LeNet卷积神经网络应用到图像分类上,在手写数字识别任务中取得了巨大成功。首先看看LeNet-5的网络结构,下图是原论文放出来的架构假设你有一张32×32×1的图片,LeNet-5可以识别图中的手写数字。由于LeNet-5是针对灰度图片训练的,所以图片的大小只有32×32×1。LeNet的结构如下:**输入层:**接收输入图像的尺寸为32x32x1。卷积层部分: 卷积层1:6×5x5的卷积核,步长为1,填充为0,使用Sigmoid激活函数平均池化层1:2x2的池化窗口,步长为2。卷积层2:16×5x5的卷积核,步长为1,填充为0,使用Sigmoid激活函数。平均池化层2:2x2的池化窗口,步长为2。全连接层部分:全连接层1:120个神经元,使用Sigmoid激活函数。全连接层2:84个神经元,使用Sigmoid激活函数。全连接层3(输出层):10个神经元,对应10个手写数字类别,现在往往用softmax。总的来说,如果我们从左往右看,随着网络越来越深,图像的高度和宽度在缩小,从最初的32×32缩小到28×28,再到14×14、10×10,最后只有5×5。与此同时,随着网络层次的加深,通道数量一直在增加,从1增加到6个,再到16个。这个神经网络中还有一种模式至今仍然经常用到,就是一个或多个卷积层后面跟着一个池化层,然后又是若干个卷积层再接一个池化层,然后是全连接层,最后是输出,这种排列方式很常用。Fashion—MNIST数据集原始的LeNet是在MNIST数据集上实现的,但是MNIST数据集在今天来说实在太简单了,我们使用一个稍微复杂一点的数据集Fashion-MNIST,为了方便我们后续比较几个模型的性能。Fashion-MNIST数据集由Zalando Research创建,并且与经典的MNIST数据集具有相似的结构。它包含了来自10个不同类别的共计70000张灰度图像,每个类别包含7000张图像。这些类别分别是:T恤、裤子、套头衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包和短靴。每张图像的尺寸为28x28像素,并以灰度形式表示,像素值范围在0到255之间。Fashion-MNIST数据集已经被标记,因此每个图像都与其对应的类别标签相关联。这使得Fashion-MNIST成为评估机器学习模型在图像分类任务上表现的理想数据集。Fashion-MNIST的目标是提供一个更具挑战性的数据集,用于测试和比较不同算法的性能。与MNIST数据集相比,Fashion-MNIST涵盖更复杂、多样化的图像内容,更能反映现实世界中的图像分类问题。我们来简单的看一下数据集,我们可以利用torchvision来下载import torch import torchvision import matplotlib.pyplot as plt # 加载Fashion-MNIST数据集 train_set = torchvision.datasets.FashionMNIST(root='./data', train=True, download=True) test_set = torchvision.datasets.FashionMNIST(root='./data', train=False, download=True) # 查看数据集大小 print(f"训练集大小: {len(train_set)}") print(f"测试集大小: {len(test_set)}") # 获取类别标签 labels = train_set.classes print(f"类别标签: {labels}") # 随机显示几个样本图像 fig, axes = plt.subplots(2, 5, figsize=(10, 4)) for i, ax in enumerate(axes.flat): image, label = train_set[i] ax.imshow(image, cmap='gray') ax.set_title(labels[label]) ax.axis('off') plt.show() 可以看到上面10张示例图,相对于手写数字识别(MNIST)数据集而言,更复杂一些,下面我们正式使用LeNet来对Fashion-MNIST数据集进行识别。1.2代码实现1.导入相关库:import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms import matplotlib.pyplot as plt 2.定义LeNet框架# 定义 LeNet 模型 class LeNet(nn.Module): def __init__(self): super(LeNet, self).__init__() self.conv1 = nn.Conv2d(1, 6, kernel_size=5,padding=2) self.avgpool = nn.AvgPool2d(kernel_size=2, stride=2) self.conv2 = nn.Conv2d(6, 16, kernel_size=5) self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): out = self.avgpool(torch.relu(self.conv1(x))) out = self.avgpool(torch.relu(self.conv2(out))) out = out.view(out.size(0), -1) out = torch.sigmoid(self.fc1(out)) out = torch.sigmoid(self.fc2(out)) out = self.fc3(out) return out 请注意,在整个卷积块中,与上一层相比,每一层特征的高度和宽度都减小了,因此高度和宽度都减少了4个像素。 随着层叠的上升,通道的数量从输入时的1个,增加到第一个卷积层之后的6个,再到第二个卷积层之后的16个。 同时,每个池化层层的高度和宽度都减半。最后,每个全连接层减少维数,最终输出一个维数与结果分类数相匹配的输出。设置gpu# 设置gpu device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 3.导入Fashion-MINIST数据集# 加载 Fashion-MNIST 数据集 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) trainset = torchvision.datasets.FashionMNIST(root='./data', train=True, download=True, transform=transform) trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True, num_workers=2) testset = torchvision.datasets.FashionMNIST(root='./data', train=False, download=True, transform=transform) testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False, num_workers=2) 4.初始化模型# 初始化模型、损失函数和优化器 model = LeNet().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.9) 这里我们使用交叉熵损失函数和小批量梯度下降5.模型训练和评估num_epochs = 10 train_losses = [] test_losses = [] for epoch in range(num_epochs): train_loss = 0.0 test_loss = 0.0 correct = 0 total = 0 # 训练模型 model.train() for images, labels in trainloader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() # 测试模型 model.eval() with torch.no_grad(): for images, labels in testloader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) test_loss += loss.item() _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() avg_train_loss = train_loss / len(trainloader) avg_test_loss = test_loss / len(testloader) train_losses.append(avg_train_loss) test_losses.append(avg_test_loss) print(f"Epoch [{epoch+1}/{num_epochs}], Train Loss: {avg_train_loss:.4f}, Test Loss: {avg_test_loss:.4f}, Acc: {correct/total*100:.2f}%") # 绘制测试误差和训练误差曲线 plt.plot(train_losses, label='Training Loss') plt.plot(test_losses, label='Testing Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.legend() plt.show() Epoch [1/10], Train Loss: 2.2963, Test Loss: 2.2134, Acc: 30.00% Epoch [2/10], Train Loss: 0.9418, Test Loss: 0.6950, Acc: 75.43% Epoch [3/10], Train Loss: 0.5754, Test Loss: 0.5239, Acc: 80.05% Epoch [4/10], Train Loss: 0.4852, Test Loss: 0.4512, Acc: 83.23% Epoch [5/10], Train Loss: 0.4302, Test Loss: 0.4255, Acc: 84.22% Epoch [6/10], Train Loss: 0.3905, Test Loss: 0.3730, Acc: 85.98% Epoch [7/10], Train Loss: 0.3644, Test Loss: 0.3640, Acc: 86.68% Epoch [8/10], Train Loss: 0.3424, Test Loss: 0.3370, Acc: 87.41% Epoch [9/10], Train Loss: 0.3253, Test Loss: 0.3261, Acc: 87.83% Epoch [10/10], Train Loss: 0.3107, Test Loss: 0.3042, Acc: 88.74% 2. AlexNet2.1 理论介绍AlexNet,是以论文的第一作者Alex Krizhevsky的名字命名的,另外两位合著者是ilya Sutskever和Geoffery Hinton。AlexNet在2012年在ImageNet图像分类挑战赛上取得了突破性的成果,其本质上和LeNet没有区别,可以看做是一个更深的LeNet,拥有更多的参数。AlexNet首先用一张227×227×3图像,论文中实际用的是224×224×3,实践中往往227×227×3更有效,我们来看一下ALexNet的基本框架**输入层:**接收输入图像的尺寸为227x227x3。卷积层部分:全连接层部分:实际上,AlexNet神经网络与LeNet有很多相似之处,不过AlexNet要大得多。正如前面讲到的LeNet大约有6万个参数,而AlexNet模型总共有5个卷积层,3个池化层和3个全连接层,参数量较大,约6000万个参数。同时,通过大输入图像尺寸和大尺寸的卷积核,使得网络能够更好地捕捉图像中的细节信息。此外,AlexNet比LeNet表现更为出色的另一个原因是它使用了ReLu激活函数,以及在池化层中使用了maxpooling。于此同时,AlexNet引入了深度学习中的一些重要概念和技术,如使用ReLU激活函数、局部响应归一化(LRN)和Dropout正则化等。2.2 代码实现原文中AlexNet是在ImageNet上进行训练的,但是这里为了方便比较,以及节约训练时间,我们依旧在Fashion—MNIST数据集上进行训练。1.定义AlexNet模型#定义AlexNet class AlexNet(nn.Module): def __init__(self, num_classes=10): super(AlexNet, self).__init__() self.features = nn.Sequential( nn.Conv2d(1, 96, kernel_size=11, stride=4), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2) ) self.classifier = nn.Sequential( nn.Dropout(p=0.5), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(4096, 4096), nn.ReLU(inplace=True), nn.Linear(4096, num_classes) ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), 256 * 6 * 6) x = self.classifier(x) return x 2.加载数据集 # 加载 Fashion-MNIST 数据集 transform = transforms.Compose([ transforms.ToTensor(), transforms.Resize((227,227)),#将原始图像扩宽到227×227 transforms.Normalize((0.5,), (0.5,)) ]) trainset = torchvision.datasets.FashionMNIST(root='./data', train=True, download=True, transform=transform) train_loader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True, num_workers=2) testset = torchvision.datasets.FashionMNIST(root='./data', train=False, download=True, transform=transform) test_loader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False, num_workers=2) 3.初始化AlexNet模型# 初始化AlexNet模型 model = AlexNet().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.01) 由于Fashion-MNINST图像默认是28×28的,我们需要将其增加到227×227,在实际中我们一般不会这样做。4.模型训练和评估# 训练AlexNet模型 num_epochs = 10 train_losses = [] test_losses = [] for epoch in range(num_epochs): train_loss = 0.0 test_loss = 0.0 correct = 0 total = 0 # 训练模型 model.train() for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() # 测试模型 model.eval() with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) test_loss += loss.item() _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() avg_train_loss = train_loss / len(train_loader) avg_test_loss = test_loss / len(test_loader) train_losses.append(avg_train_loss) test_losses.append(avg_test_loss) print(f"Epoch [{epoch+1}/{num_epochs}], Train Loss: {avg_train_loss:.4f}, Test Loss: {avg_test_loss:.4f}, Acc: {correct/total*100:.2f}%") # 绘制测试误差和训练误差曲线 plt.plot(train_losses, label='Training Loss') plt.plot(test_losses, label='Testing Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.legend() plt.show() Epoch [1/10], Train Loss: 0.8024, Test Loss: 1.3865, Acc: 55.48% Epoch [2/10], Train Loss: 0.4890, Test Loss: 0.4372, Acc: 83.66% Epoch [3/10], Train Loss: 0.3692, Test Loss: 0.3832, Acc: 85.42% Epoch [4/10], Train Loss: 0.3307, Test Loss: 0.3728, Acc: 86.03% Epoch [5/10], Train Loss: 0.3030, Test Loss: 0.3281, Acc: 87.72% Epoch [6/10], Train Loss: 0.2829, Test Loss: 0.3285, Acc: 87.78% Epoch [7/10], Train Loss: 0.2697, Test Loss: 0.3515, Acc: 87.47% Epoch [8/10], Train Loss: 0.2560, Test Loss: 0.3193, Acc: 88.24% Epoch [9/10], Train Loss: 0.2466, Test Loss: 0.3005, Acc: 89.02% Epoch [10/10], Train Loss: 0.2373, Test Loss: 0.3068, Acc: 89.00% 可以看出AlexNet在Fashion-MNIST数据集上测试精度有所提升,突破了89%,这是因为AlexNet使用了更深更大的网络。3.VGG3.1 理论介绍经过Lenet和AlexNet的介绍,我们可以发现使用更深更大的神经网络,能够带来更好的效果,这也是目前深度学习领域一直在做的事情,包括现在热门gpt4。但是AlexNet有一个问题是框架的设置太不规则,因此如何更好的设计更深更大的神经网络值得我们去思考。为了实现这一目的,VGG模型(Visual Geometry Group)产生了,VGG由牛津大学的研究团队开发的深度卷积神经网络模型。VGG模型在2014年的ImageNet图像分类挑战赛中取得了很大的成功,并且在计算机视觉领域被广泛应用。VGG模型的主要特点是它采用了非常小的卷积核(3x3)和最大池化层(2x2),以及多个卷积和池化层的叠加。模型的深度可变,通过调整卷积和全连接层的数量来改变模型的深度。最常用的VGG模型有VGG16和VGG19。VGG模型的主要优势是它具有非常好的表达能力和一致性,以及相对简单的结构。它通过多层卷积和池化层来逐渐提取图像的特征,并通过全连接层进行分类。这种结构使得模型能够捕获不同尺度下的图像特征,从而提高了模型的准确性,下面我们具体讲讲这种网络结构,如下图所示(VGG16)VGG模型主要由VGG块和全连接层组成,通过多次叠加这些层来逐渐提取图像特征并进行分类。其中每一个VGG块都是由3×3的卷积层和2×2的池化层组成下面是VGG16模型的详细结构:输入层:接收输入图像的尺寸为224x224x3。VGG块部分: 卷积层1-1:64个3x3的卷积核,填充为1,ReLU激活函数。 卷积层1-2:64个3x3的卷积核,填充为1,ReLU激活函数。 最大池化层1:2x2的池化窗口,步长为2 卷积层2-1:128个3x3的卷积核,填充为1,ReLU激活函数。 卷积层2-2:128个3x3的卷积核,填充为1,ReLU激活函数。 最大池化层2:2x2的池化窗口,步长为2。 卷积层3-1:256个3x3的卷积核,填充为1,ReLU激活函数。 卷积层3-2:256个3x3的卷积核,填充为1,ReLU激活函数。 卷积层3-3:256个3x3的卷积核,填充为1,ReLU激活函数。 最大池化层3:2x2的池化窗口,步长为2。 卷积层4-1:512个3x3的卷积核,填充为1,ReLU激活函数。 卷积层4-2:512个3x3的卷积核,填充为1,ReLU激活函数。 卷积层4-3:512个3x3的卷积核,填充为1,ReLU激活函数。 最大池化层4:2x2的池化窗口,步长为2。 卷积层5-1:512个3x3的卷积核,填充为1,ReLU激活函数。 卷积层5-2:512个3x3的卷积核,填充为1,ReLU激活函数。 卷积层5-3:512个3x3的卷积核,填充为1,ReLU激活函数。 最大池化层5:2x2的池化窗口,步长为2。全连接层部分:全连接层1:4096个神经元,ReLU激活函数。 Dropout层1:以0.5的概率随机将输入置为0。 全连接层2:4096个神经元,ReLU激活函数。 Dropout层2:以0.5的概率随机将输入置为0。 全连接层3(输出层):1000个神经元,对应ImageNet的1000个类别。VGG16模型总共有13个卷积层和3个全连接层,参数量较大。该模型的设计思想是通过多层的小卷积核和池化层来逐渐缩小宽度,并提取出更高级别的图像特征。同时,使用ReLU激活函数来增强网络的非线性表达能力。最后通过全连接层进行分类。随着网络的加深,图像的高度和宽度都在以一定的规律不断缩小,每次池化后刚好缩小一半,而通道数量在不断增加,而且刚好也是在每组卷积操作后增加一倍。也就是说,图像缩小的比例和通道数增加的比例是有规律的。VGG使用可重复使用的卷积块构建深度卷积神经网络不同的卷积块个数和超参数可以得到不同系列的VGG(如:VGG16、VGG19)3.2代码实现1.VGG模型定义原文VGG16模型定义如下# 定义VGG16模型 class VGG16(nn.Module): def __init__(self, num_classes=10): super(VGG16, self).__init__() self.features = nn.Sequential( nn.Conv2d(1, 64, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.Conv2d(64, 64, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(64, 128, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.Conv2d(128, 128, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(128, 256, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(256, 512, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.Conv2d(512, 512, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.Conv2d(512, 512, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(512, 512, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.Conv2d(512, 512, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.Conv2d(512, 512, kernel_size=3, padding=1),nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2) ) self.classifier = nn.Sequential( nn.Linear(512 * 7 * 7, 4096),nn.ReLU(inplace=True), nn.Dropout(), nn.Linear(4096, 4096),nn.ReLU(inplace=True), nn.Dropout(), nn.Linear(4096, num_classes) ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), 512 * 7 * 7) x = self.classifier(x) return x 这样看上去有点冗余,为了方便更改架构,我们可以设置VGG块,然后根据VGG块来生成网络,后续的很多网络都用类似的想法。 VGG块定义如下import torch import torch.nn as nn def vgg_block(num_convs, in_channels, out_channels): layers = [] for _ in range(num_convs): layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)) layers.append(nn.ReLU()) in_channels = out_channels layers.append(nn.MaxPool2d(kernel_size=2,stride=2)) return nn.Sequential(*layers) 由于这里使用的数据集数量较小,考虑到性能问题,这里我们使用VGG-11,共有8个卷积层和3个全连接层。conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512)) def vgg(conv_arch): conv_blks = [] in_channels = 1 # 卷积层部分 for (num_convs, out_channels) in conv_arch: conv_blks.append(vgg_block(num_convs, in_channels, out_channels)) in_channels = out_channels return nn.Sequential( *conv_blks, nn.Flatten(), # 全连接层部分 nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 10)) net = vgg(conv_arch) 2.加载Fashion-MNIST数据集import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms import matplotlib.pyplot as plt # 设置使用的设备为GPU,如果没有GPU则使用CPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 加载Fashion-MNIST数据集 transform = transforms.Compose([ transforms.ToTensor(), transforms.Resize((224,224)),# transforms.Normalize((0.5,), (0.5,)) ]) trainset = torchvision.datasets.FashionMNIST(root='./data', train=True,download=True, transform=transform) trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,shuffle=True, num_workers=2) testset = torchvision.datasets.FashionMNIST(root='./data', train=False,download=True, transform=transform) testloader = torch.utils.data.DataLoader(testset, batch_size=64,shuffle=False, num_workers=2) 由于VGG输入图像要求为224×224,这里需要将Fashion—MNIST的图像大小更改,使用transforms.Resize函数。3.初始化模型model = net.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.05) 4.模型训练和评估# 训练模型 num_epochs = 10 train_losses = [] test_losses = [] train_accs = [] test_accs = [] for epoch in range(num_epochs): train_loss = 0.0 train_total = 0 train_correct = 0 model.train() for images, labels in trainloader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() _, predicted = torch.max(outputs.data, 1) train_total += labels.size(0) train_correct += (predicted == labels).sum().item() train_loss /= len(trainloader) train_accuracy = 100.0 * train_correct / train_total train_losses.append(train_loss) train_accs.append(train_accuracy) test_loss = 0.0 test_total = 0 test_correct = 0 model.eval() with torch.no_grad(): for images, labels in testloader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) test_loss += loss.item() _, predicted = torch.max(outputs.data, 1) test_total += labels.size(0) test_correct += (predicted == labels).sum().item() test_loss /= len(testloader) test_accuracy = 100.0 * test_correct / test_total test_losses.append(test_loss) test_accs.append(test_accuracy) print(f"Epoch {epoch+1}/{num_epochs}: Train Loss: {train_loss:.4f}, Train Acc: {train_accuracy:.2f}%, Test Loss: {test_loss:.4f}, Test Acc: {test_accuracy:.2f}%") # 绘制训练误差和测试误差曲线 plt.figure(figsize=(10, 5)) plt.plot(range(1, num_epochs+1), train_losses, label='Train Loss') plt.plot(range(1, num_epochs+1), test_losses, label='Test Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.title('Training and Testing Loss') plt.legend() plt.show() # 绘制训练准确率和测试准确率曲线 plt.figure(figsize=(10, 5)) plt.plot(range(1, num_epochs+1), train_accs, label='Train Acc') plt.plot(range(1, num_epochs+1), test_accs, label='Test Acc') plt.xlabel('Epoch') plt.ylabel('Accuracy') plt.title('Training and Testing Accuracy') plt.legend() plt.show() Epoch 1/10: Train Loss: 2.2078, Train Acc: 15.18%, Test Loss: 0.9984, Test Acc: 65.97% Epoch 2/10: Train Loss: 0.5435, Train Acc: 79.75%, Test Loss: 0.4182, Test Acc: 84.12% Epoch 3/10: Train Loss: 0.3391, Train Acc: 87.61%, Test Loss: 0.3074, Test Acc: 88.30% Epoch 4/10: Train Loss: 0.2872, Train Acc: 89.33%, Test Loss: 0.2830, Test Acc: 89.32% Epoch 5/10: Train Loss: 0.2521, Train Acc: 90.65%, Test Loss: 0.2747, Test Acc: 90.11% Epoch 6/10: Train Loss: 0.2228, Train Acc: 91.58%, Test Loss: 0.2585, Test Acc: 90.44% Epoch 7/10: Train Loss: 0.1985, Train Acc: 92.61%, Test Loss: 0.2545, Test Acc: 91.10% Epoch 8/10: Train Loss: 0.1767, Train Acc: 93.42%, Test Loss: 0.2654, Test Acc: 90.92% Epoch 9/10: Train Loss: 0.1535, Train Acc: 94.28%, Test Loss: 0.2362, Test Acc: 91.81% Epoch 10/10: Train Loss: 0.1324, Train Acc: 94.98%, Test Loss: 0.2662, Test Acc: 91.24%我们对比三个架构在Fashion-MNIST数据集上的结果,发现测试集的Accuracy,VGG-11表现最好,突破了0.91,AlexNet次之,LeNet最低,这说明使用更深的网络是能够提升图像识别性能的。总结我们介绍了三种经典的卷积神经网络架构:LeNet,AlexNet,VGG。他们的共同思想都是使用卷积层来学习图片的空间信息,提取特征,最后使用全连接层转换到我们要的分类空间。LeNet是首个成功应用在手写数字识别数据集上的深度卷积神经网络,只有2个卷积层、两个池化层和三个全连接层AlexNet在LeNet基础上使用了更多更深的卷积层,在2012年的ImageNet比赛上一战成名,从此引领了深度学习的浪潮VGG在AlexNet的基础上构建了一个非常深的卷积神经网络,通过堆叠多个小尺寸的卷积核和池化层来逐步提取图像特征。它的设计简单一致,具有较好的性能和可迁移性,成为了深度学习研究中的重要里程碑之一。从LeNet到AlexNet再到VGG,网络在不断的变深变大,模型参数也在不断增加,包括现在很多模型都是上亿个参数,这对数据集和硬件都有很高的要求,后续我们再介绍一些能够减少模型参数的方法。
1.数据操作# 导入torch import torch import numpy as np1.1 张量创建x = torch.arange(12) y = np.arange(12)x,y (tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])) tensor(张量)表示一个数值组成的数组,可以有多个维度,类似numpy中的n维数组,因此很多n维数组有的方法张量也有,下面我们来测试一下有哪些numpy中的方法可以在这里使用。==要了解numpy可以看这篇文章==:Python数据分析大杀器之Numpy详解# 查看形状 x.shape torch.Size([12]) # 查看数量长度 len(x) 12 同样可以使用reshape函数转换数组x = x.reshape(3,4) x tensor([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) zeros创建全为0的元素x = torch.zeros(3,4) x tensor([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]]) ones创建全为1的元素x = torch.ones(3,4) x tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]]) eye创建对角矩阵l = torch.eye(5) l tensor([[1., 0., 0., 0., 0.], [0., 1., 0., 0., 0.], [0., 0., 1., 0., 0.], [0., 0., 0., 1., 0.], [0., 0., 0., 0., 1.]]) ones_like创建形状一致的全为1的元素矩阵x = torch.ones_like(l) x tensor([[1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.]]) randn创建随机矩阵x = torch.randn((2,4)) x tensor([[-0.2102, -1.5580, -1.0650, -0.2689], [-0.5349, 0.6057, 0.7164, 0.4334]]) 可以有多个维度,如下所示,创建两个维度的tensor,其中0表示外面的一层,1表示内部的一层x = torch.tensor([[1,1,1,1],[1,2,3,4],[4,3,2,1]]) x tensor([[1, 1, 1, 1], [1, 2, 3, 4], [4, 3, 2, 1]]) 张量还可以和numpy的数组之间相互转换,具体如下所示y = x.numpy() type(x),type(y) (torch.Tensor, numpy.ndarray) 1.2 基本运算在创建完张量之后,我们对如何计算这些张量感兴趣。和多维数组一样,张量也可以进行一些基本运算,具体代码如下所示x = torch.tensor([1,2,3,4]) y = torch.tensor([2,3,4,5]) x+y,x-y,x*y,x/y (tensor([3, 5, 7, 9]), tensor([-1, -1, -1, -1]), tensor([ 2, 6, 12, 20]), tensor([0.5000, 0.6667, 0.7500, 0.8000])) 可以看出和numpy数组一样,也是对元素进行运算。下面看看求和操作x = torch.arange(12).reshape(3,4) x.sum(dim=0)#按行求和 tensor([12, 15, 18, 21]) y = np.arange(12).reshape((3,4)) y.sum(axis=0)#按行求和 array([12, 15, 18, 21]) 从上面可以看出,tensor和array都可以按行或案列进行操作,但是在torch中,指定dim参数,numpy中,指定axis参数1.3 广播机制我们之前的numpy中介绍过广播机制,在两个数组纬度不同时,可以适当的复制元素来拓展一个纬度或者两个纬度的元素,我们接下来看看torch中是不是也支持广播机制x = torch.tensor([[1,2,3],[4,5,6]]) y = torch.tensor([1,1,1]) z = x + y print('x:',x) print('y:',y) print('z:',z) x: tensor([[1, 2, 3], [4, 5, 6]]) y: tensor([1, 1, 1]) z: tensor([[2, 3, 4], [5, 6, 7]]) 通过上述代码可以发现,torch中也支持广播机制,并且和numpy中的使用基本一致1.4 索引和切片接下来我们来看看如何对tensor结果进行切片和索引,用法和numpy基本一致x tensor([[1, 2, 3], [4, 5, 6]]) # 选取第一列和第二列的数据 x[:,[0,1]] tensor([[1, 2], [4, 5]]) 2.自动微分(求导)线性代数部分大家可以看我的numpy文章,有具体的介绍,这里着重看一下如何求导数。在深度学习中,对于很多层的神经网络而言,人工求导是一件很复杂的事情,因此在如何自动求导是一件很work的事情这里我们假设要对y=xTxy=x^Txy=xTx进行求导。首先我们先初始化一个x值x = torch.arange(4.0) x tensor([0., 1., 2., 3.]) 下面我们在计算梯度之前,需要一个地方来存储梯度,就像我们在进行一些循环时,需要一个空列表来存储内容。下面我们来看如何使用requires_grad_来存储x.requires_grad_(True) print(x.grad)#默认是None,相当于这个时候是一个空列表 None 下面我们来计算yy = torch.dot(x,x) y tensor(14., grad_fn=<DotBackward0>) # 通过反向传播函数计算梯度 y.backward(retain_graph=False) x.grad tensor([0., 2., 4., 6.]) 这里默认情况下,pytorch会保存梯度,因此当我们需要重新计算梯度时,首先要进行初始化,使用grad.zero_x.grad.zero_() # 重新计算y=x的梯度 y = x.sum() y.backward() x.grad tensor([1., 1., 1., 1.]) 上面我们都是先将y变为一个标量再求梯度,如果y不是标量呢?可以先将y求和转换为标量x.grad.zero_() y = x*x y.sum().backward() x.grad tensor([0., 2., 4., 6.]) 2.3 分离微分计算这里沐神给出了一个这样的场景,y是关于x的函数,而z是关于y和x的函数,在我们对z求x偏导时,我们希望将y看做一个常数。这种方法在有的复杂的神经网络模型会很有效,具体通过detach()实现,将u为y的常量具体代码如下:x.grad.zero_()#初始化梯度 y = x * x#y对x的函数 u = y.detach()#将y分离处理 z = u * x#z对x的函数 z.sum().backward()#通过反向传播函数求梯度 x.grad tensor([0., 1., 4., 9.]) 上述结果是什么呢?我们根据求导法则:下面我们来看看u是多少u tensor([0., 1., 4., 9.]) 2.4 控制流梯度计算使用自动微分有一个好处是,当我们的函数是分段的,其也会自动计算得到相应的梯度。下面我们来看一个线性控制流梯度计算案例:def f(a): if a.sum() > 0: b = a else: b = 100 * a return b 首先我们定义一个线性分段函数,如上所示:下面我们来看如何进行自动求导a = torch.randn(12, requires_grad=True) d = f(a) d.backward(torch.ones_like(a)) a.grad == d / a tensor([True, True, True, True, True, True, True, True, True, True, True, True]) 练习与总结1.重新设计一个求控制流梯度的例子,运行并分析结果。在上面的案例中,沐神给了一个线性分段函数的例子,假设不是线性的呢,下面我们假设一个分段函数是这样的具体控制流代码如下:def f(x): if x.norm() > 10: y = x else: y = x*x return y x = torch.randn(12,requires_grad=True) y = f(x) y.backward(torch.ones_like(x)) x.grad tensor([ 0.3074, -2.0289, 0.5950, 1.2339, -2.2543, 0.5834, -2.3040, -1.9097, 0.9255, 1.6837, -1.4464, -0.3131]) 2.绘制微分图import numpy as np import matplotlib.pyplot as plt %matplotlib inline x = torch.linspace(-2*torch.pi, 2*torch.pi, 100) x.requires_grad_(True) y = torch.sin(x) y.sum().backward() y = torch.detach(y) plt.plot(y,'r--',label='$sin(x)$') plt.plot(x.grad,'g',label='$cos(x)$') plt.legend(loc='best') plt.grid()
0.背景介绍接下来我们正式进入项目实战部分,这一章要介绍的是一个信用卡号识别的项目。首先,我们来明确一下研究的问题,假设我们有一张信用卡如下所示,我们要做的就是识别出这上面卡号信息,然后会输出一个序列,第一个序列就是4020,第二序列是3400,第三个序列0234,第四个序列5678,也就是说此时我们不光是把这个数输出来,我们还要知道对应的位置。之前我们已经介绍了Opencv的各种图像基本操作,例如形态学操作、模板匹配,我们现在要做的就是把这些方法全部应用到一起,相当于把我们以前所学的知识点全部穿插到咱们这个项目当中了。我们先来看一下要完成这个项目的基本思想。思考一: 首先最核心的问题是我们如何判断一个数字是几呢?这里我们要用到模板匹配假设我们有一个数字模板如下:现在我们要做的就是将信用卡上每一个数字和模板上的数字进行匹配,看一下它与模板上的哪一个数字最接近,我们就把这个数字输出。因此我们第一步需要得到一个与目标信用卡数字字体非常接近的一个模板。*思考二: *如何每一个数字单独拿出来?我们之前介绍轮廓检测,但是直接得到的轮廓各个数字之间非常不规则,我们可以利用轮廓的外接矩形或者外接圆来进行操作。总体就是分为以上两个步骤,具体过程我们还需要对图像进行各种预处理操作,我们在后面在代码中细致介绍。以下是项目的主要框架,想要源码的可以私信我获取。1.模板处理1.1模板读取首先我们将目标模板读取# 导入工具包 from imutils import contours import numpy as np import argparse import cv2 import myutils # 指定模板和目标图像位置 target = 'images/credit_card_02.png' template = 'images/ocr_a_reference.png' # 定义图像展示函数 def cv_show(name,img): cv2.imshow(name, img) cv2.waitKey(0) cv2.destroyAllWindows() # 读取模板图像 img = cv2.imread(template) cv_show(im) 1.2预处理接下来对模板进行预处理,转换为二值图,因为我们后续轮廓检测时只接受二值图输入。# 灰度图 ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) cv_show('ref',ref) # 二值图像 ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1] #阈值设为10 cv_show('ref',ref) 现在得到了二值图像之后我们就可以进行图像轮廓检测了。1.3轮廓计算在这里我们使用cv2.findContours()函数,其只接收一个二值图像,cv2.RETR_EXTERNAL只检测外轮廓,cv2.CHAIN_APPROX_SIMPLE只保留终点坐标。返回参数我们只需要用refCnts即可,它返回的是我们的轮廓信息# 计算轮廓 #返回的list中每个元素都是图像中的一个轮廓 ref_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(img,refCnts,-1,(0,0,255),3) # -1表示绘制所有轮廓 cv_show('img',img) #展示轮廓 现在我们得到了0-9每个数字的外轮廓信息,但是直接返回的轮廓顺序不一定是按照我们模板从左到右排序的,接下来我们需要对轮廓进行排序,让它按照从左到右0,1,2,3,4…9的顺序排列,这里我们定义了函数,sort_contours,我们直接根据我们的坐标排序,就可以得到按照0-9排列的轮廓import cv2 def sort_contours(cnts, method="left-to-right"): reverse = False i = 0 if method == "right-to-left" or method == "bottom-to-top": reverse = True if method == "top-to-bottom" or method == "bottom-to-top": i = 1 boundingBoxes = [cv2.boundingRect(c) for c in cnts] #计算外接矩形,用一个最小的矩形,返回x,y,h,w,分别表示坐标和高宽 (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes), key=lambda b: b[1][i], reverse=reverse))#排序 return cnts, boundingBoxes 现在得到了排序好后的轮廓,接下来我们需要把模板中每个数字轮廓单独拿出来放到一个字典中方便我们后续进行匹配,通过cv.boundingRect()得到轮廓坐标和长宽信息,然后利用我们之前的ROI读取方法即可,最后我们更改一下轮廓的大小。refCnts = sort_contours(refCnts, method="left-to-right")[0] #排序,从左到右 digits = {} # 遍历每一个轮廓 for (i, c) in enumerate(refCnts): # 计算外接矩形并且resize成合适大小 (x, y, w, h) = cv2.boundingRect(c) roi = ref[y:y + h, x:x + w] roi = cv2.resize(roi, (57, 88)) # 每一个数字对应每一个模板 digits[i] = roi 接下来我们就得到了每个数字模板的轮廓信息,并保留在digits字典中,接下来我们需要对输入图像进行处理。2.输入图像处理2.1图形读取这里我们初始化了两个卷积核,分别为9×3和5×5的,这里大家可以根据自己的任务更换别的卷积核大小。然后我们把目标图像读取进来并转换为灰度图# 初始化卷积核 rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3)) sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) #读取输入图像,预处理 image = cv2.imread(target) cv_show('image',image) image = myutils.resize(image, width=300) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 转换为灰度图 cv_show('gray',gray) 2.2预处理得到灰度图之后,我们需要进行更细节的预处理,因为我们想要检测的是银行卡号,我们关注的是这样的数字部分,也就是更亮的区域,因此我们在这里进行了礼帽操作,来突出我们想要研究的信息,在实际应用中,可以根据具体想要研究的任务来选择其他的处理方式#礼帽操作,突出更明亮的区域 tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel) cv_show('tophat',tophat) 接下来我们进一步的利用sobel算子来进行边缘检测gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, #ksize=-1相当于用3*3的 ksize=-1) # 使用Sobel算子处理 gradX = np.absolute(gradX) (minVal, maxVal) = (np.min(gradX), np.max(gradX)) gradX = (255 * ((gradX - minVal) / (maxVal - minVal))) # 归一化处理 gradX = gradX.astype("uint8") print (np.array(gradX).shape) cv_show('gradX',gradX) 得到边缘之后,我们希望将这些数字分块放到一起,每四个数字为一个小方块。我们可以利用之前介绍过的先膨胀,再腐蚀的操作。#通过闭操作(先膨胀,再腐蚀)将数字连在一起 gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel) cv_show('gradX',gradX) #二值化处理:THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0 thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] cv_show('thresh',thresh) #再来一个闭操作 thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel) #再来一个闭操作 cv_show('thresh',thresh) 首先经过一个闭操作后,得到下列结果然后我们进一步使用二值化处理,将图片转换为二值图像现在得到的结果中,还有一部分空隙,我们再进行一次闭操作,得到结果如下:现在得到的结果是一个完全闭合的状态了,此时我们再检测它的外轮廓,会更准确一些。2.3轮廓计算接下来我们计算轮廓,和之前在处理模板一样,我们调用cv2.findContours()函数计算轮廓信息。# 计算轮廓 thresh_, threshCnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# cnts = threshCnts #轮廓信息 cur_img = image.copy() cv2.drawContours(cur_img,cnts,-1,(0,0,255),3)#在原始图像上绘制轮廓 cv_show('img',cur_img) 这里需要说明的是,我们这里是用经过一切预处理后得到的轮廓信息,然后绘制在原始图像中。但是我们得到的轮廓有点多,且有一些不太规则的形状,有些可能不是我们想要轮廓,我们需要把这些轮廓过滤,我们只要四组数字轮廓。我们可以根据他们的坐标位置进行筛选,具体的筛选范围大家要根据自己的实际任务选择.locs = [] # 遍历轮廓 for (i, c) in enumerate(cnts): # 计算矩形 (x, y, w, h) = cv2.boundingRect(c) ar = w / float(h) # 选择合适的区域,根据实际任务来,这里的基本都是四个数字一组 if ar > 2.5 and ar < 4.0: #这里需要根据具体的任务更改,我这里是经过几次尝试测试出来的 if (w > 40 and w < 55) and (h > 10 and h < 20): #符合的留下来 locs.append((x, y, w, h)) # 将符合的轮廓从左到右排序 locs = sorted(locs, key=lambda x:x[0]) locs [(34, 111, 47, 14), (95, 111, 48, 14), (157, 111, 47, 14), (219, 111, 48, 14)] 可以看到我们现在得到了四个轮廓,并且进行了排序,接下来我们怎么进行模板匹配呢?我们不是拿这四个大轮廓去匹配,而是在每一个大轮廓中,再去分隔成小轮廓,然后去和我们之前保存的10个数的模板进行匹配。2.4计算匹配得分接下来我们要做的是去遍历每一个轮廓当中的数字,然后将其与模板中的10个数字计算匹配得分,从而识别出对于的数字,我们先来看第一个轮廓:有了这个之后,就和我们最开始处理模板一样,先进行二值化处理,得到下图然后计算每一组的轮廓,并按照从左到右的顺序排列,以第一个数字为例,得到结果如下然后我们就是将这每一个数字与模板上的10个数字进行对比,看一下和哪一个最相似。具体做法跟之前都是一样的吧,先去找到外接矩形,然后对外接矩形进行resize。然后我们就要计算得分了,我们利用模板匹配中的方法,使用cv2.TM_CCOEFF计算得分,然后找到最匹配的数字,这样就完成了我们所有的步骤了output = [] # 遍历每一个轮廓中的数字 for (i, (gX, gY, gW, gH)) in enumerate(locs): # initialize the list of group digits groupOutput = [] # 根据坐标提取每一个组 group = gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5] # 扩张一下轮廓 cv_show('group',group) # 预处理 group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] cv_show('group',group) # 计算每一组的轮廓 group_,digitCnts,hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) digitCnts = contours.sort_contours(digitCnts, method="left-to-right")[0] # 计算每一组中的每一个数值 for c in digitCnts: # 找到当前数值的轮廓,resize成合适的的大小 (x, y, w, h) = cv2.boundingRect(c) roi = group[y:y + h, x:x + w] roi = cv2.resize(roi, (57, 88)) cv_show('roi',roi) # 计算匹配得分 scores = [] # 在模板中计算每一个得分 for (digit, digitROI) in digits.items(): # 模板匹配 result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF) (_, score, _, _) = cv2.minMaxLoc(result) scores.append(score) # 得到最合适的数字 groupOutput.append(str(np.argmax(scores))) # 绘制结果 cv2.rectangle(image, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1) cv2.putText(image, "".join(groupOutput), (gX, gY - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2) # 得到结果 output.extend(groupOutput) # 打印结果 print("Credit Card: {}".format("".join(output))) cv2.imshow("Image", image) cv2.waitKey(0) Credit Card: 4020340002345678 3.小结我们这个项目主要分两步。第一步,定位到目标数字在什么位置。第二步,基于定位好的区域,在模板当中去匹配它到底是一个什么样的值。中间利用了我们之前介绍过的各种图像处理方法。今天我们这个项目是做一个信用卡号识别,如果说大家想做车牌识别、学生卡、身份证识别,都是一个类似的做法,我们主需要更改一下对于的照片模板以及其中的一些参数即可。
1.理论部分在进行卷积层计算时候,有一个问题就是边缘的地方容易忽略,并且对位置是非常敏感的。池化层的做法是为了降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。 因此,除了卷积层,卷积网络也经常使用池化层来缩减模型的大小,提高计算速度,同时提高所提取特征的鲁棒性。与卷积层不同的是,池化层运算是确定性的,我们通常计算池化窗口中所有元素的最大值或平均值。这些操作分别称为最大池化层(maximum pooling)和平均池化层(average pooling)。先举一个池化层的例子假如输入是一个4×4矩阵,用到的池化类型是最大池化(max pooling)。执行最大池化是一个2×2矩阵。执行过程非常简单,把4×4的输入拆分成不同的区域,我把这个区域用不同颜色来标记。对于2×2的输出,输出的每个元素都是其对应颜色区域中的最大元素值。左上区域的最大值是9,右上区域的最大元素值是2,左下区域的最大值是6,右下区域的最大值是3。为了计算出右侧这4个元素值,我们需要对输入矩阵的2×2区域做最大值运算。这里相当于是一个2×2的kernel,步幅为2。因为我们使用的过滤器为2×2,最后输出是9。然后向右移动2个步幅,计算出最大值2。然后是第二行,向下移动得到最大值6。最后向右移动,得到最大值3。这是一个2×2矩阵,即f=2f=2f=2,步幅是2,即s=2s=2s=2。这是对最大池化功能的直观理解,你可以把这个4×4区域看作是某些特征的集合,也就是神经网络中某一层的非激活值集合。数字大意味着可能探测到了某些特定的特征,左上象限具有的特征可能是一个垂直边缘,最大化操作的功能就是只要在任何一个象限内提取到某个特征,它都会保留在最大化的池化输出里。所以最大化运算的实际作用就是,如果在过滤器中提取到某个特征,那么保留其最大值。如果没有提取到这个特征,可能在右上象限中不存在这个特征,那么其中的最大值也还是很小。其中一个有意思的特点就是,它有一组超参数,但并没有参数需要学习。实际上,梯度下降没有什么可学的,一旦确定了fff和sss,它就是一个固定运算,梯度下降无需改变任何值。目前来说,最大池化比平均池化更常用。但也有例外,就是深度很深的神经网络,你可以用平均池化来分解规模为7×7×1000的网络的表示层,在整个空间内求平均值,得到1×1×1000。但在神经网络中,最大池化要比平均池化用得更多。2.代码实现池化层代码非常简单,因为这里没有卷积核,输出为输入中每个区域的最大值或平均值,我们可以很简单的自定义一个池化层函数import torch from torch import nn def pool2d(X, pool_size, mode='max'): p_h, p_w = pool_size #池化层大小 Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1)) for i in range(Y.shape[0]): for j in range(Y.shape[1]): if mode == 'max': Y[i, j] = X[i: i + p_h, j: j + p_w].max() elif mode == 'avg': Y[i, j] = X[i: i + p_h, j: j + p_w].mean() return Y 现在我们测试一下,先生成一个张量XX = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]]) X # 测试我们刚刚的函数,默认最大池化层 pool2d(X, (2, 2)) tensor([[4., 5.], [7., 8.]]) 3.填充与步幅与卷积层一样,池化层也可以改变输出形状。和以前一样,我们可以通过填充(padding)和步幅(stride)以获得所需的输出形状。 这里我们直接使用pytorch内置的方法来演示,首先生成输入张量X,它是一个四维张量,其中样本数和通道数都是1。X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4)) X tensor([[[[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [12., 13., 14., 15.]]]]) 默认条件下,步幅和池化层的窗口大小一致。例如,我们使用3×33×33×3的池化层,那么步幅sss默认是3pool2d = nn.MaxPool2d(3) pool2d(X) tensor([[[[10.]]]]) 我们可以根据需要指定相应的步幅和填充,这里我们设置padding=1,stride=2通常情况下,我们不会使用填充(padding),因为这一部分可以在卷积层进行。pool2d = nn.MaxPool2d(3, padding=1, stride=2) pool2d(X) 4.多通道在处理多通道输入数据时,池化层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着汇聚层的输出通道数与输入通道数相同。和我们之前创建多通道数据一样,我们使用cat函数X = torch.cat((X, X + 1), 1) X 此时X的纬度是1×2×4×4,1个样本,两个通道接下来我们使用一个2×2的池化层,步幅sss为2pool2d = nn.MaxPool2d(2, stride=2) pool2d(X) tensor([[[[ 5., 7.], [13., 15.]], [[ 6., 8.], [14., 16.]]]]) 可以看到结果仍然是一个两通道张量,这一点和卷积层完全不同。总结特点:输入通道数等于输出通道数没有可以学习的参数池化层的主要优点之一是减轻卷积层对位置的过度敏感最大池化层:返回窗口内的最大值,提取的是每个窗口中最强的信号,更常用平均池化层:平均汇聚层会输出该窗口内的平均值。