簡單的卷積神經網絡,實作手寫英文字母識别
1 搭建Python運作環境(建議用Anaconda),自學Python程式設計
安裝Tensorflow、再安裝Pycharm等環境。(也可用Pytorch)
1.1 Anaconda的安裝及工具包下載下傳方法總結
參考文章:
手把手教你在Windows系統下安裝Anaconda
在官網上下載下傳Anaconda
Anaconda官網
進入官網:
點選Download
選擇對應的版本
以下在windows系統中進行示範
安裝流程
下載下傳完畢後,直接安裝即可,我這裡習慣把所有的程式都放在一個環境中
環境變量請必須勾選,如果忘了,建議解除安裝重裝
至此,anaconda就安裝完成了
打開Anaconda檢查python版本
打開Anaconda prompt,輸入python --version即可。
如果打不開,或者點選之後沒反應,但可以用管理者身份打開軟體,那可能是anaconda指向的java jdk路徑不對,以後有時間我再寫個教程更改一下。
1.2 Windows下安裝教程
鏡像的好處:滿速下載下傳,不能直接上網的伺服器也可以下載下傳相應的包(采用ipv6)等
一下以安裝pytorch為例
在Anaconda安裝Pytorch等一系列包
準備工作
- 安裝好anaconda,具體參考1.1
- 打開Anaconda Prompt,即可
- 為了更好的管理安裝pytorch、tensorflow等一系列包的路徑,建議建立對應的環境,而不是直接安裝在base中,用以下代碼語句即可:
conda create -n xxx python3.8 #建立名為python3.8的xxx虛拟環境
conda env list #檢視所有虛拟環境
conda activate xxx #進入xxx環境
conda deactivate xxx #退出xxx環境
其他與環境相關的conda語句可以點選這裡檢視。
首先建立名為"jh"的環境;
建立好後,檢視已有的環境,此時“jh”環境已經出現
最後,進入(激活)“jh”環境
從base進入到jh環境,之後将所有需要安裝的包全部安裝在jh環境中。至于為什麼要建立“jh”環境?因為如果你之後不再使用jh環境的時候,直接用一行conda語句删除掉這個環境,安裝在jh裡的包就全部删除掉了,管理起來很友善。
之後安裝各種包就在“jh”環境中進行。
鏡像源網站
我經常使用清華鏡像源,但最近北外的鏡像源速度更快,是以就選用了北外開源鏡像源。
點選下方紅框的“使用幫助”、“anaconda”。
找到中間選中的channels的内容,即
channels:
- defaults
show_channel_urls: true
default_channels:
- https://mirrors.bfsu.edu.cn/anaconda/pkgs/main
- https://mirrors.bfsu.edu.cn/anaconda/pkgs/free
- https://mirrors.bfsu.edu.cn/anaconda/pkgs/r
- https://mirrors.bfsu.edu.cn/anaconda/pkgs/pro
- https://mirrors.bfsu.edu.cn/anaconda/pkgs/msys2
custom_channels:
conda-forge: https://mirrors.bfsu.edu.cn/anaconda/cloud
msys2: https://mirrors.bfsu.edu.cn/anaconda/cloud
bioconda: https://mirrors.bfsu.edu.cn/anaconda/cloud
menpo: https://mirrors.bfsu.edu.cn/anaconda/cloud
pytorch: https://mirrors.bfsu.edu.cn/anaconda/cloud
simpleitk: https://mirrors.bfsu.edu.cn/anaconda/cloud
一般有兩種方法将此内容配置給anaconda,一般采用第一種方式:
第①種方式: 修改.condarc檔案,該檔案儲存了anaconda的配置資訊,此檔案存儲在 C:\Users\xxxx.conda\路徑中,可用記事本打開,将channels内容全部粘貼在.condarc檔案即可。
【注意!】如果
C:\Users\xxxx\.conda\
路徑中沒有.condarc檔案,可先在anaconda prompt中生成一個,代碼如下:
conda config --add channels https://xxx ##這裡随便寫一個channel的https即可。
運作後就可在
C:\Users\xxxx\
路徑中找到.condarc檔案
第②種方式: 用conda語句,一句一句的輸入,例如需要安裝pytorch
conda config --add channels https://mirrors.bfsu.edu.cn/anaconda/cloud
...
#https的内容對應于channels中的https;
#将channels中的https一行一行的用conda語句配置給anaconda;
安裝pytorch
- 打開pytorch官網(https://pytorch.org/),進度條往下拖,可以找到對應的安裝語句。
此處conda安裝pytorch的語句(日期2021/05/14)為
conda install pytorch torchvision torchaudio cudatoolkit=10.2 -c pytorch
- 在anaconda prompt中進入你建好的環境中(如“jh”環境),并進行pytorch的安裝即可。
- 檢視版本資訊
退出python可以在運作quit()或快捷鍵Ctrl+Z即可。
此外,還可以檢視“jh”環境中所有安裝包情況,conda語句如下(需在“jh”環境下運作該語句):
conda list #檢視該環境下的所有安裝包情況
可以發現安裝pytorch的時候,常用的numpy也一同被安裝了。
如果要删除此環境,可以點選這裡檢視conda語句。
删除安裝過程中下載下傳的安裝包,釋放空間
建議定期删除安裝包,不删除的話就會一直積累在那裡。
相關conda語句如下:
conda clean -p #删除沒有用的安裝包
conda clean -y --all #删除所有安裝包和cache
總結說明
對于相關部落格中介紹的方法個人覺得不是最友善的。
綜合檢視的一些資料将我個人覺得比較簡單的方法應該是直接在Anaconda Navigator (anaconda3)面闆上安裝。
具體流程如下:
- 打開Anaconda Navigator (anaconda3)
- 打擊Environments
- 選中自己的執行檔案夾
- 通過搜尋框查詢需要的工具包進行安裝即可。
通過以上的方式安裝工具包,可以避免因為指令問題或版本問題導緻的工具包和Anaconda不相容的問題。
通過上圖可以看到我這邊已經安裝了pytorch和Tensorflow
1.3 Pycharm安裝
Pycharm安裝教程
安裝配置
PyCharm 是一款功能強大的 Python 編輯器,具有跨平台性,鑒于目前最新版 PyCharm 使用教程較少,為了節約時間,來介紹一下 PyCharm 在 Windows下是如何安裝的。
這是 PyCharm 的下載下傳位址
下載下傳位址
進入該網站後,我們會看到如下界面:
professional 表示專業版,community 是社群版,推薦安裝社群版,因為是免費使用的。
1、當下載下傳好以後,點選安裝,記得修改安裝路徑,我這裡放的是E盤,修改好以後,Next
2、接下來是
我們可以根據自己的電腦選擇32位還是64位,目前應該基本都是64位系統
3、如下
點選Install,然後就是靜靜的等待安裝了。
4、我們進入該軟體
5、點選Create New Project,接下來是重點
Location是我們存放工程的路徑,點選
這個三角符号,可以看到pycharm已經自動擷取了Python3.5
點選第一個
我們可以選擇Location的路徑,比如
記住,我們選擇的路徑需要為空,不然無法建立,第二個Location不用動它,是自動預設的,其餘不用點,然後點選Create。出現如下界面,這是Pycharm在配置環境,靜靜等待。最後點選close關掉提示就好了。
6、建立編譯環境
右鍵
點選New,選擇Python File
給file取個名字,點選OK
系統會預設生成hello.py
好了,至此,我們的初始工作基本完成。
7、我們來編譯一下
8、因為之前已經添加過了,是以可以直接編譯,還有很重要的一步沒說,不然pycharm無法找到解釋器,将無法編譯。
點選File,選擇settings,點選
添加解釋器
最後點選Apply。等待系統配置。
如果我們需要添加新的子產品,點選綠色+号
然後直接搜尋pymysql
然後點安裝
以上就是pycharm的安裝過程以及初始化,還有Python解釋器的安裝配置。
注意點
Pycharm的安裝基本按照對應的教程就行。需要注意的是對應的python.exe一定要和自己的運作路徑相對應,不然會出現我們安裝了相應的工具包,但卻無法導入的問題
2 自學卷積神經網絡的基礎知識
參考的學習視訊
卷積神經網絡百度解析
Lenet-5模型+代碼實作
卷積神經網絡
2.1 卷積
卷積是兩個變量在某範圍内相乘後求和的結果。在卷積神經網絡中就是指卷積核與指定視圖某塊區域的數值乘積之和,即對于給定的一幅圖像來說,給定一個卷積核,卷積就是根據卷積視窗,進行像素的權重求和。
2.2 池化
剛開始學習CNN的時候,看到這個詞,好像高大上的樣子,于是查了很多資料,理論一大堆?
池化,就是圖檔下采樣。每一層通過卷積,然後卷積後進行下采樣,而CNN也是同樣的過程。CNN的池化主要要兩種方法(肯定有很多,但在學習過程中,我主要接觸到的就是最大值池化和平均值池化)。
其中最經典的是最大池化,是以我就解釋一下最大池化的實作:
上圖用到的池化2 * 2 的filter(類似卷積層的卷積核,隻不過這裡更加容易了解),在filter掃描範圍中選取最大值作為這個區域的代表,就是最大化池化方法。
2.3 Lenet模型
Lenet-5卷積神經網絡的整體架構如下:
LeNet-5共有8層,包含輸入層,每層都包含可訓練參數;每個層有多個特征映射(Feature Map),每個特征映射通過一種卷積核(或者叫濾波器)提取輸入的一種特征,然後每個特征映射有多個神經元。C層代表的是卷積層,通過卷積操作,可以使原信号特征增強,并且降低噪音。 S層是一個降采樣層,利用圖像局部相關性的原理,對圖像進行子抽樣,可以減少資料處理量同時保留有用資訊。
計算公式:輸入圖像的大小為nxn,卷積核的大小為mxm,步長為s, 為輸入圖像兩端填補p個零(zero padding),那麼卷積操作之後輸出的大小為(n-m+2p)/s + 1。
結合前面的學習視訊來說,這個公式是很好了解的。
3 下載下傳一個手寫英文字母資料集
The Chars74K dataset
由于隻需要訓練識别英文字母,選擇其中的Sample011到Sample062複制到任意目錄下
這裡為了便于實驗處理,将資料中的同一個字母的大小寫放在了同一個檔案夾
4 下載下傳LeNet5,或更複雜的卷積神經網絡代碼
參考部落格
5 将上述代碼調試運作通過,并實作手寫英文字母的識别
5.1 訓練效果圖
input(手寫體):
卷積層1:
卷積層2:
全連接配接層:
OUTPUT:
predict character: G or g
代碼說明
import torch
import torch.nn as nn
# torchvision已經預先實作了常用的Datast
from torchvision.datasets import ImageFolder # ImageFolder是一個經常用到的Dataset
import torchvision.models as models
from torchvision import utils
import torchvision.transforms as T
import torch.utils.data as Data
from PIL import Image
import numpy as np
import torch.optim as optim
import os
import matplotlib.pyplot as plt
#使用tensorboardX進行可視化
from tensorboardX import SummaryWriter
# 建立一個write執行個體,自動生成的檔案夾路徑為:./EMNIST_log
SumWriter = SummaryWriter(log_dir = "./EMNIST_log")
# 資料預處理
# 首先定義超參數
EPOCH = 10 # 訓練的批次,下載下傳的資料集每個字母都有5000張左右的圖檔,由于電腦性能的原因,對于每個字母的訓練我隻保留了1000張圖檔,同時為了保證訓練準确度,将訓練的次數調得比較多
BATCH_SIZE = 128 # 訓練的最小規模(一次反向傳播需要更新權值)
LR = 1e-4 # 學習率
# 轉為tensor 以及 标準化
transform = T.Compose([
#轉為灰階圖像,這部分是便于圖像識别的:
T.Grayscale(num_output_channels=1),
#将圖檔轉換為Tensor,歸一化至(0,1),在實驗中發現如果沒有歸一化的過程,最後的預測效果會很差:
T.ToTensor(),
])
#資料集要作為一個檔案夾讀入:
# #讀取訓練集:
# ImageFolder(root, transform=None, target_transform=None, loader=default_loader)
# 它主要有四個參數:
# root:在root指定的路徑下尋找圖檔
# transform:對PIL Image進行的轉換操作,transform的輸入是使用loader讀取圖檔的傳回對象
# target_transform:對label的轉換
# loader:給定路徑後如何讀取圖檔,預設讀取為RGB格式的PIL Image對象
train_data = ImageFolder(root="./Emnist_letters_png/Train_png",
transform=transform)
# 訓練集資料的加載器,自動将資料分割成batch,順序随機打亂
# shuffle這個參數代表是否在建構批次時随機選取資料
train_loader = torch.utils.data.DataLoader(dataset=train_data,
batch_size=BATCH_SIZE,
shuffle=True)
#讀取測試集:
test_data = ImageFolder(root="./Emnist_letters_png/Test_png",
transform=transform)
#之是以要将test_data轉換為loader是因為網絡不支援原始的ImageFolder類資料,到時候直接使用批訓練,便是tensor類。
#batch_size為全部10000張testdata,在全測試集上測試精度
test_loader = torch.utils.data.DataLoader(dataset=test_data,
batch_size=test_data.__len__())
label_num = len(train_data.class_to_idx)
#資料可視化:
to_img = T.ToPILImage()
a=to_img(test_data[0][0]) #size=[1, 28, 28]
plt.imshow(a)
plt.axis('off')
plt.show()
# 卷積網絡搭建:兩層卷積網絡(卷積+池化)+ 三層全連接配接層
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# Sequantial()把不同的函數組合成一個子產品使用
# 定義網絡架構
# 卷積層1(卷積核=16)
self.Conv1 = nn.Sequential(
# 5個參數依次是:
# in_channels:輸入圖像的通道數,這裡為1表示隻有一層圖像
# out_channels:定義了16個卷積核,即輸出高度為16
# kernel_size:卷積核大小為5 * 5
# stride: 步長,卷積核每次掃描的跨度
# padding: 邊界填充0(如步長為1時,若要保證輸出尺寸像和原尺寸一緻,
# 計算公式為:padding = (kernel_size-1)/2)
nn.Conv2d(1, 16, 5, 1, 2),
nn.BatchNorm2d(16),
#激活函數層
nn.ReLU(),
#最大池化層 通過最大值進行池化
nn.MaxPool2d(kernel_size=2)
)
# 卷積層2
self.Conv2 = nn.Sequential(
nn.Conv2d(16, 32, 5, 1, 2),
nn.BatchNorm2d(32),
#激活函數層
nn.Dropout(p=0.2),
nn.ReLU(),
#最大池化層
nn.MaxPool2d(kernel_size=2)
)
#最後接上一個全連接配接層(将圖像變為1維)
#為什麼是32*7*7:
# (1,28,28)->(16,28,28)(conv1)
# ->(16,14,14)(pool1)
# ->(32,14,14)(conv2)
# ->(32,7,7)(pool2)->output
self.Linear = nn.Sequential(
nn.Linear(32*7*7,400),
# Dropout按指定機率随機舍棄部分的神經元
nn.Dropout(p = 0.5),
# 全連接配接層激活函數
nn.ReLU(),
nn.Linear(400,80),
# nn.Dropout(p=0.5),
nn.ReLU(),
nn.Linear(80,label_num),
)
# 前向傳播
def forward(self, input):
input = self.Conv1(input)
input = self.Conv2(input)
#input.size() = [100, 32, 7, 7], 100是每批次的數量,32是厚度,圖檔尺寸為7*7
#當某一維是-1時,會自動計算它的大小(原則是總資料量不變):
input = input.view(input.size(0), -1) #(batch=100, 1568), 最終效果便是将二維圖檔壓縮為一維(資料量不變)
#最後接上一個全連接配接層,輸出為10:[100,1568]*[1568,10]=[100,10]
output = self.Linear(input)
return output
# 讀取網絡架構
cnn = CNN()
# 僅儲存訓練好的參數
torch.save(cnn.state_dict(), 'EMNIST_CNN.pkl')
# 加載訓練好的參數
cnn.load_state_dict(torch.load('EMNIST_CNN.pkl'))
# 進行訓練
cnn.train()
# 顯示網絡層結構
# print(cnn)
#定義優化器
optimizer = torch.optim.Adam(cnn.parameters(), lr=LR)
#定義損失函數,因為是分類問題,是以使用交叉熵損失
loss_func = nn.CrossEntropyLoss()
# 訓練與模式儲存
# 根據EPOCH自動更新學習率,2次EPOCH學習率減少為原來的一半:
# scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 2, gamma = 0.6, last_epoch = -1)
for epoch in range(EPOCH):
# enumerate() 函數用于将一個可周遊的資料對象組合為一個索引序列。例:['A','B','C']->[(0,'A'),(1,'B'),(2,'C')],
# 這裡是為了将索引傳給step輸出
for step, (x, y) in enumerate(train_loader):
output = cnn(x)
loss = loss_func(output, y)
loss.backward()
optimizer.step()
optimizer.zero_grad()
if step % 100 == 0:
# enumerate() 函數用于将一個可周遊的資料對象組合為一個索引序列。例:['A','B','C']->[(0,'A'),(1,'B'),(2,'C')]
for (test_x, test_y) in test_loader:
# print(test_y.size())
# 在所有資料集上預測精度:
# 預測結果 test_output.size() = [10000,10],其中每一列代表預測為每一個數的機率(softmax輸出),而不是0或1
test_output = cnn(test_x)
# torch.max()則将預測結果轉化對應的預測結果,即機率最大對應的數字:[10000,10]->[10000]
pred_y = torch.max(test_output,1)[1].squeeze() # squeeze()預設是将a中所有為1的次元删掉
# pred_size() = [10000]
accuracy = sum(pred_y == test_y) / test_data.__len__()
print('Eopch:',
epoch,
' | train loss: %.6f' % loss.item(),
' | test accracy:%.5f' % accuracy,
' | step: %d' % step)
-
INPUT層-輸入層
首先是資料 INPUT 層,輸入圖像的尺寸統一歸一化為28 * 28
-
C1層-卷積層
輸入圖檔:28 * 28
卷積核大小:5*5
卷積核種類:16
步長為:1
邊界填充0
輸出的特征映射大小為 28 * 28 (因為使用了邊界填充,是以圖像大小沒有發生變化),因為有16個卷積核,是以此階段有16個特征映射圖
-
第一次池化
采樣區域 2 * 2
方式:最大值池化
輸出的特征映射大小 14 * 14
這是也是16份特征映射圖
-
C2卷積層
輸入圖檔:14 * 14
卷積核大小:5*5
卷積核種類:32
步長為:1
邊界填充0
輸出的特征映射大小為 14 * 14 (因為使用了邊界填充,是以圖像大小沒有發生變化),因為有32個卷積核,是以此階段有32個特征映射圖
-
第二次池化
采樣區域 2 * 2
方式:最大值池化
輸出的特征映射大小 7 * 7
這時也是32份特征映射圖
可視化測試
import torch
import torch.nn as nn
import torchvision.transforms as transforms
from torchvision.datasets import ImageFolder
import torchvision.models as models
from torchvision import utils
import torchvision.transforms as T
import torch.utils.data as Data
from PIL import Image
import numpy as np
import torch.optim as optim
import cv2
import matplotlib.pyplot as plt
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.Conv1 = nn.Sequential(
#卷積層1
nn.Conv2d(1, 16, 5, 1, 2),
nn.BatchNorm2d(16),
#激活函數層
nn.ReLU(),
#最大池化層
nn.MaxPool2d(kernel_size=2)
)
self.Conv2 = nn.Sequential(
#卷積層2
nn.Conv2d(16, 32, 5, 1, 2),
nn.BatchNorm2d(32),
nn.Dropout(p=0.2),
#激活函數層
nn.ReLU(),
#最大池化層
nn.MaxPool2d(kernel_size=2)
)
#最後接上一個全連接配接層(将圖像變為1維)
#為什麼是32*7*7:(1,28,28)->(16,28,28)(conv1)->(16,14,14)(pool1)->(32,14,14)(conv2)->(32,7,7)(pool2)->output
self.Linear = nn.Sequential(
nn.Linear(32*7*7,400),
nn.Dropout(p=0.2),
nn.ReLU(),
nn.Linear(400,80),
nn.ReLU(),
nn.Linear(80,26),
)
def forward(self, input):
input = self.Conv1(input)
input = self.Conv2(input) #view可了解為resize
#input.size() = [100, 32, 7, 7], 100是每批次的數量,32是厚度,圖檔尺寸為7*7
#當某一維是-1時,會自動計算他的大小(原則是總資料量不變):
input = input.view(input.size(0), -1) #(batch=100, 1568), 最終效果便是将二維圖檔壓縮為一維(資料量不變)
#最後接上一個全連接配接層,輸出為10:[100,1568]*[1568,10]=[100,10]
output = self.Linear(input)
return output
#讀取網絡架構
cnn = CNN()
#讀取權重:
cnn.load_state_dict(torch.load('EMNIST_CNN.pkl'))
#test_x:(10000行1列,每列元素為28*28矩陣)
# 提供指定的資料進行測試:
my_img = plt.imread("Emnist_letters_png/Pre_jpg/C.jpg")
my_img = my_img[:,:,0] # 轉換為單通道
my_img = cv2.resize(my_img,(28,28)) # 轉換為28*28尺寸
my_img = torch.from_numpy(my_img) # 轉換為張量
my_img = torch.unsqueeze(my_img, dim=0) # 添加一個次元
my_img = torch.unsqueeze(my_img, dim=0)/255. # 再添加一個次元并把灰階映射在(0,1之間)
#可視化部分:
#輸入原圖像:
plt.imshow(my_img.squeeze())
plt.show()
#Conv1:
cnt = 1
my_img = cnn.Conv1(my_img)
img = my_img.squeeze()
for i in img.squeeze():
plt.axis('off')
fig = plt.gcf()
fig.set_size_inches(5,5) # 輸出width*height像素
plt.margins(0,0)
plt.imshow(i.detach().numpy())
plt.subplot(4, 4, cnt)
plt.axis('off')
plt.imshow(i.detach().numpy())
cnt += 1
plt.subplots_adjust(top=1,bottom=0,left=0,right=1,hspace=0,wspace=0)
plt.show()
#Conv2:
cnt = 1
my_img = cnn.Conv2(my_img)
img = my_img.squeeze()
for i in img.squeeze():
plt.axis('off')
fig = plt.gcf()
fig.set_size_inches(5,5)#輸出width*height像素
plt.margins(0,0)
plt.imshow(i.detach().numpy())
plt.subplot(4, 8, cnt)
plt.axis('off')
plt.imshow(i.detach().numpy())
cnt += 1
#plt.subplots_adjust(top=1,bottom=0,left=0,right=1,hspace=0,wspace=0)
plt.show()
#全連接配接層:
my_img = my_img.view(my_img.size(0), -1)
fig = plt.gcf()
fig.set_size_inches(10000,4) # 輸出width*height像素
plt.subplots_adjust(top=1,bottom=0,left=0,right=1,hspace=0,wspace=0)
plt.margins(0,0)
my_img = cnn.Linear[0](my_img)
plt.subplot(3, 1, 1)
plt.imshow(my_img.detach().numpy())
my_img = cnn.Linear[1](my_img)
my_img = cnn.Linear[2](my_img)
my_img = cnn.Linear[3](my_img)
plt.subplot(3, 1, 2)
plt.imshow(my_img.detach().numpy())
my_img = cnn.Linear[4](my_img)
my_img = cnn.Linear[5](my_img)
plt.subplot(3, 1, 3)
plt.imshow(my_img.detach().numpy())
plt.show()
#輸出預測結果:
pred_y = int(torch.max(my_img,1)[1])
print('\n %s : %s' % ("待預測字母對應字母表可能位置的機率為" , my_img))
#chr()将數字轉為對應的的ASCAII字元
print('\n predict character: %c or %c' % (chr(pred_y+65), chr(pred_y+97)))