前言¶
本章將介紹如何使用PaddlePaddle訓練自己的圖片數據集,在之前的圖像數據集中,我們都是使用PaddlePaddle自帶的數據集,本章我們就來學習如何讓PaddlePaddle訓練我們自己的圖片數據集。
爬取圖像¶
在本章中,我們使用的是自己的圖片數據集,所以我們需要弄一堆圖像來製作訓練的數據集。下面我們就編寫一個爬蟲程序,讓其幫我們從百度圖片中爬取相應類別的圖片。
創建一個download_image.py文件用於編寫爬取圖片程序。首先導入所需的依賴包。
import re
import uuid
import requests
import os
import numpy
import imghdr
from PIL import Image
然後編寫一個下載圖片的函數,這個是程序核心代碼。參數是下載圖片的關鍵、保存的名字、下載圖片的數量。關鍵字是百度搜索圖片的關鍵。
# 獲取百度圖片下載圖片
def download_image(key_word, save_name, download_max):
download_sum = 0
# 把每個類別的圖片存放在單獨一個文件夾中
save_path = 'images' + '/' + save_name
if not os.path.exists(save_path):
os.makedirs(save_path)
while download_sum < download_max:
download_sum += 1
str_pn = str(download_sum)
# 定義百度圖片的路徑
url = 'http://image.baidu.com/search/flip?tn=baiduimage&ie=utf-8&' \
'word=' + key_word + '&pn=' + str_pn + '&gsm=80&ct=&ic=0&lm=-1&width=0&height=0'
try:
s = requests.session()
s.headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'
# 獲取當前頁面的源碼
result = s.get(url).content.decode('utf-8')
# 獲取當前頁面的圖片URL
img_urls = re.findall('"objURL":"(.*?)",', result, re.S)
if img_urls is None or len(img_urls) < 1:
break
# 開始下載圖片
for img_url in img_urls:
# 獲取圖片內容
img = requests.get(img_url, timeout=30)
# 保存圖片
with open(save_path + '/' + str(uuid.uuid1()) + '.jpg', 'wb') as f:
f.write(img.content)
print('正在下載 %s 的第 %d 張圖片' % (key_word, download_sum))
download_sum += 1
# 下載次數超過指定值就停止下載
if download_sum >= download_max:
break
except Exception as e:
print(e)
continue
print('下載完成')
圖片下載完成之後,需要刪除一家損壞的圖片,因爲在下載的過程中,由於圖片本身的問題或者下載過程造成的圖片損壞,需要把這些已經損壞的圖片上傳。下面的函數就是刪除所有損壞的圖片,根據圖像數據集的目錄讀取獲取所有圖片文件的路徑,然後使用imghdr工具獲取圖片的類型是否爲png或者jpg來判斷圖片文件是否完整,最後再刪除根據圖片的通道數據來刪除灰度圖片。
# 刪除不是JPEG或者PNG格式的圖片
def delete_error_image(father_path):
# 獲取父級目錄的所有文件以及文件夾
try:
image_dirs = os.listdir(father_path)
for image_dir in image_dirs:
image_dir = os.path.join(father_path, image_dir)
# 如果是文件夾就繼續獲取文件夾中的圖片
if os.path.isdir(image_dir):
images = os.listdir(image_dir)
for image in images:
image = os.path.join(image_dir, image)
try:
# 獲取圖片的類型
image_type = imghdr.what(image)
# 如果圖片格式不是JPEG同時也不是PNG就刪除圖片
if image_type is not 'jpeg' and image_type is not 'png':
os.remove(image)
print('已刪除:%s' % image)
continue
# 刪除灰度圖
img = numpy.array(Image.open(image))
if len(img.shape) is 2:
os.remove(image)
print('已刪除:%s' % image)
except:
os.remove(image)
print('已刪除:%s' % image)
except:
pass
最後在main入口中通過調用兩個函數來完成下載圖像數據集,使用中文進行百度搜索圖片,使用英文是爲了出現中文路徑導致圖片讀取錯誤。
if __name__ == '__main__':
# 定義要下載的圖片中文名稱和英文名稱,ps:英文名稱主要是爲了設置文件夾名
key_words = {'西瓜': 'watermelon', '哈密瓜': 'cantaloupe',
'櫻桃': 'cherry', '蘋果': 'apple', '黃瓜': 'cucumber', '胡蘿蔔': 'carrot'}
# 每個類別下載一千個
max_sum = 500
for key_word in key_words:
save_name = key_words[key_word]
download_image(key_word, save_name, max_sum)
# 刪除錯誤圖片
delete_error_image('images/')
輸出信息:
正在下載 哈密瓜 的第 0 張圖片.....
【錯誤】當前圖片無法下載,HTTPConnectionPool(host='www.boyingsj.com', port=80): Read timed out.
正在下載 哈密瓜 的第 10 張圖片.....
注意: 下載處理完成之後,還可能存在其他雜亂的圖片,所以還需要我們手動刪除這些不屬於這個類別的圖片,這纔算完成圖像數據集的製作。
創建圖像列表¶
創建一個create_data_list.py文件,在這個程序中,我們只要把爬取保存圖片的路徑的文件夾路徑傳進去就可以了,生成固定格式的列表,格式爲圖片的路徑 <Tab> 圖片類別的標籤:
import json
import os
def create_data_list(data_root_path):
with open(data_root_path + "test.list", 'w') as f:
pass
with open(data_root_path + "train.list", 'w') as f:
pass
# 所有類別的信息
class_detail = []
# 獲取所有類別
class_dirs = os.listdir(data_root_path)
# 類別標籤
class_label = 0
# 獲取總類別的名稱
father_paths = data_root_path.split('/')
while True:
if father_paths[len(father_paths) - 1] == '':
del father_paths[len(father_paths) - 1]
else:
break
father_path = father_paths[len(father_paths) - 1]
all_class_images = 0
other_file = 0
# 讀取每個類別
for class_dir in class_dirs:
if class_dir == 'test.list' or class_dir == "train.list" or class_dir == 'readme.json':
other_file += 1
continue
print('正在讀取類別:%s' % class_dir)
# 每個類別的信息
class_detail_list = {}
test_sum = 0
trainer_sum = 0
# 統計每個類別有多少張圖片
class_sum = 0
# 獲取類別路徑
path = data_root_path + "/" + class_dir
# 獲取所有圖片
img_paths = os.listdir(path)
for img_path in img_paths:
# 每張圖片的路徑
name_path = class_dir + '/' + img_path
# 如果不存在這個文件夾,就創建
if not os.path.exists(data_root_path):
os.makedirs(data_root_path)
# 每10張圖片取一個做測試數據
if class_sum % 10 == 0:
test_sum += 1
with open(data_root_path + "test.list", 'a') as f:
f.write(name_path + "\t%d" % class_label + "\n")
else:
trainer_sum += 1
with open(data_root_path + "train.list", 'a') as f:
f.write(name_path + "\t%d" % class_label + "\n")
class_sum += 1
all_class_images += 1
# 說明的json文件的class_detail數據
class_detail_list['class_name'] = class_dir
class_detail_list['class_label'] = class_label
class_detail_list['class_test_images'] = test_sum
class_detail_list['class_trainer_images'] = trainer_sum
class_detail.append(class_detail_list)
class_label += 1
# 獲取類別數量
all_class_sum = len(class_dirs) - other_file
# 說明的json文件信息
readjson = {}
readjson['all_class_name'] = father_path
readjson['all_class_sum'] = all_class_sum
readjson['all_class_images'] = all_class_images
readjson['class_detail'] = class_detail
jsons = json.dumps(readjson, sort_keys=True, indent=4, separators=(',', ': '))
with open(data_root_path + "readme.json", 'w') as f:
f.write(jsons)
print('圖像列表已生成')
最後執行就可以生成圖像的列表。
if __name__ == '__main__':
# 把生產的數據列表都放在自己的總類別文件夾中
data_root_path = "images/"
create_data_list(data_root_path)
輸出信息:
正在讀取類別:apple
正在讀取類別:cantaloupe
正在讀取類別:carrot
正在讀取類別:cherry
正在讀取類別:cucumber
正在讀取類別:watermelon
圖像列表已生成
運行這個程序之後,會生成在data文件夾中生成一個單獨的大類文件夾,比如我們這次是使用到蔬菜類,所以我生成一個vegetables文件夾,在這個文件夾下有3個文件:
|文件名|作用|
|:—:|:—:|
|trainer.list|用於訓練的圖像列表|
|test.list|用於測試的圖像列表|
|readme.json|該數據集的json格式的說明,方便以後使用|
readme.json文件的格式如下,可以很清楚看到整個數據的圖像數量,總類別名稱和類別數量,還有每個類對應的標籤,類別的名字,該類別的測試數據和訓練數據的數量:
{
"all_class_images": 2200,
"all_class_name": "images",
"all_class_sum": 2,
"class_detail": [
{
"class_label": 1,
"class_name": "watermelon",
"class_test_images": 110,
"class_trainer_images": 990
},
{
"class_label": 2,
"class_name": "cantaloupe",
"class_test_images": 110,
"class_trainer_images": 990
}
]
}
定義模型¶
創建一個mobilenet_v1.py文件,在本章我們使用的是MobileNet神經網絡,MobileNet是Google針對手機等嵌入式設備提出的一種輕量級的深層神經網絡,它的核心思想就是卷積核的巧妙分解,可以有效減少網絡參數,從而達到減小訓練時網絡的模型。因爲太大的模型模型文件是不利於移植到移動設備上的,比如我們把模型文件遷移到Android手機應用上,那麼模型文件的大小就直接影響應用安裝包的大小。以下就是使用PaddlePaddle定義的MobileNet神經網絡:
import paddle.fluid as fluid
def conv_bn_layer(input, filter_size, num_filters, stride,
padding, channels=None, num_groups=1, act='relu', use_cudnn=True):
conv = fluid.layers.conv2d(input=input,
num_filters=num_filters,
filter_size=filter_size,
stride=stride,
padding=padding,
groups=num_groups,
act=None,
use_cudnn=use_cudnn,
bias_attr=False)
return fluid.layers.batch_norm(input=conv, act=act)
def depthwise_separable(input, num_filters1, num_filters2, num_groups, stride, scale):
depthwise_conv = conv_bn_layer(input=input,
filter_size=3,
num_filters=int(num_filters1 * scale),
stride=stride,
padding=1,
num_groups=int(num_groups * scale),
use_cudnn=False)
pointwise_conv = conv_bn_layer(input=depthwise_conv,
filter_size=1,
num_filters=int(num_filters2 * scale),
stride=1,
padding=0)
return pointwise_conv
def net(input, class_dim, scale=1.0):
# conv1: 112x112
input = conv_bn_layer(input=input,
filter_size=3,
channels=3,
num_filters=int(32 * scale),
stride=2,
padding=1)
# 56x56
input = depthwise_separable(input=input,
num_filters1=32,
num_filters2=64,
num_groups=32,
stride=1,
scale=scale)
input = depthwise_separable(input=input,
num_filters1=64,
num_filters2=128,
num_groups=64,
stride=2,
scale=scale)
# 28x28
input = depthwise_separable(input=input,
num_filters1=128,
num_filters2=128,
num_groups=128,
stride=1,
scale=scale)
input = depthwise_separable(input=input,
num_filters1=128,
num_filters2=256,
num_groups=128,
stride=2,
scale=scale)
# 14x14
input = depthwise_separable(input=input,
num_filters1=256,
num_filters2=256,
num_groups=256,
stride=1,
scale=scale)
input = depthwise_separable(input=input,
num_filters1=256,
num_filters2=512,
num_groups=256,
stride=2,
scale=scale)
# 14x14
for i in range(5):
input = depthwise_separable(input=input,
num_filters1=512,
num_filters2=512,
num_groups=512,
stride=1,
scale=scale)
# 7x7
input = depthwise_separable(input=input,
num_filters1=512,
num_filters2=1024,
num_groups=512,
stride=2,
scale=scale)
input = depthwise_separable(input=input,
num_filters1=1024,
num_filters2=1024,
num_groups=1024,
stride=1,
scale=scale)
feature = fluid.layers.pool2d(input=input,
pool_size=0,
pool_stride=1,
pool_type='avg',
global_pooling=True)
net = fluid.layers.fc(input=feature,
size=class_dim,
act='softmax')
return net
定義數據讀取¶
創建一個reader.py文件,這個程序就是用戶訓練和測試的使用讀取數據的。訓練的時候,通過這個程序從本地讀取圖片,然後通過一系列的預處理操作,最後轉換成訓練所需的Numpy數組。
首先導入所需的包,其中cpu_count是獲取當前計算機有多少個CPU,然後使用多線程讀取數據。
import os
import random
from multiprocessing import cpu_count
import numpy as np
import paddle
from PIL import Image
首先定義一個train_mapper()函數,這個函數是根據傳入進來的圖片路徑來對圖片進行預處理,比如訓練的時候需要統一圖片的大小,同時也使用多種的數據增強的方式,如水平翻轉、垂直翻轉、角度翻轉、隨機裁剪,這些方式都可以讓有限的圖片數據集在訓練的時候成倍的增加。最後因爲PIL打開圖片存儲順序爲H(高度),W(寬度),C(通道),PaddlePaddle要求數據順序爲CHW,所以需要轉換順序。最後返回的是處理後的圖片數據和其對應的標籤。
# 訓練圖片的預處理
def train_mapper(sample):
img_path, label, crop_size, resize_size = sample
try:
img = Image.open(img_path)
# 統一圖片大小
img = img.resize((resize_size, resize_size), Image.ANTIALIAS)
# 隨機水平翻轉
r1 = random.random()
if r1 > 0.5:
img = img.transpose(Image.FLIP_LEFT_RIGHT)
# 隨機垂直翻轉
r2 = random.random()
if r2 > 0.5:
img = img.transpose(Image.FLIP_TOP_BOTTOM)
# 隨機角度翻轉
r3 = random.randint(-3, 3)
img = img.rotate(r3, expand=False)
# 隨機裁剪
r4 = random.randint(0, int(resize_size - crop_size))
r5 = random.randint(0, int(resize_size - crop_size))
box = (r4, r5, r4 + crop_size, r5 + crop_size)
img = img.crop(box)
# 把圖片轉換成numpy值
img = np.array(img).astype(np.float32)
# 轉換成CHW
img = img.transpose((2, 0, 1))
# 轉換成BGR
img = img[(2, 1, 0), :, :] / 255.0
return img, int(label)
except:
print("%s 該圖片錯誤,請刪除該圖片並重新創建圖像數據列表" % img_path)
這個train_reader()函數是根據已經創建的圖像列表解析得到每張圖片的路徑和其他對應的標籤,然後使用paddle.reader.xmap_readers()把數據傳遞給上面定義的train_mapper()函數進行處理,最後得到一個訓練所需的reader。
# 獲取訓練的reader
def train_reader(train_list_path, crop_size, resize_size):
father_path = os.path.dirname(train_list_path)
def reader():
with open(train_list_path, 'r') as f:
lines = f.readlines()
# 打亂圖像列表
np.random.shuffle(lines)
# 開始獲取每張圖像和標籤
for line in lines:
img, label = line.split('\t')
img = os.path.join(father_path, img)
yield img, label, crop_size, resize_size
return paddle.reader.xmap_readers(train_mapper, reader, cpu_count(), 102400)
這是一個測試數據的預處理函數test_mapper(),這個沒有做太多處理,因爲測試的數據不需要數據增強操作,只需統一圖片大小和設置好圖片的通過順序和數據類型即可。
# 測試圖片的預處理
def test_mapper(sample):
img, label, crop_size = sample
img = Image.open(img)
# 統一圖像大小
img = img.resize((crop_size, crop_size), Image.ANTIALIAS)
# 轉換成numpy值
img = np.array(img).astype(np.float32)
# 轉換成CHW
img = img.transpose((2, 0, 1))
# 轉換成BGR
img = img[(2, 1, 0), :, :] / 255.0
return img, int(label)
這個是測試的reader函數test_reader(),這個跟訓練的reader函數定義一樣。
# 測試的圖片reader
def test_reader(test_list_path, crop_size):
father_path = os.path.dirname(test_list_path)
def reader():
with open(test_list_path, 'r') as f:
lines = f.readlines()
for line in lines:
img, label = line.split('\t')
img = os.path.join(father_path, img)
yield img, label, crop_size
return paddle.reader.xmap_readers(test_mapper, reader, cpu_count(), 1024)
訓練模型¶
萬事俱備,只等訓練了。關於PaddlePaddle訓練流程,我們已經非常熟悉了,那麼我們就簡單地過一遍。
創建train.py文件,首先導入所需的包,其中包括我們定義的MobileNet模型和數據讀取程序:
import os
import shutil
import mobilenet_v1
import paddle as paddle
import reader
import paddle.fluid as fluid
然後定義數據輸入層,這次我們使用的是圖片大小是224,這比之前使用的CIFAR數據集的32大小要大很多,所以訓練其他會慢不少。至於resize_size是用於統一縮放到這個大小,然後再隨機裁剪成crop_size大小,crop_size纔是最終訓練圖片的大小。
crop_size = 224
resize_size = 250
# 定義輸入層
image = fluid.layers.data(name='image', shape=[3, crop_size, crop_size], dtype='float32')
label = fluid.layers.data(name='label', shape=[1], dtype='int64')
接着獲取MobileNet網絡的分類器,傳入的第一個參數就是上面定義的輸入層,第二個是分類的類別大小,比如我們這次爬取的圖像類別數量是6個。
# 獲取分類器,因爲這次只爬取了6個類別的圖片,所以分類器的類別大小爲6
model = mobilenet_v1.net(image, 6)
再接着是獲取損失函數和平均準確率函數,還有測試程序和優化方法,這個優化方法我加了正則,因爲爬取的圖片數量太少,在訓練容易過擬合,所以加上正則一定程度上可以抑制過擬合。
# 獲取損失函數和準確率函數
cost = fluid.layers.cross_entropy(input=model, label=label)
avg_cost = fluid.layers.mean(cost)
acc = fluid.layers.accuracy(input=model, label=label)
# 獲取訓練和測試程序
test_program = fluid.default_main_program().clone(for_test=True)
# 定義優化方法
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=1e-3,
regularization=fluid.regularizer.L2DecayRegularizer(1e-4))
opts = optimizer.minimize(avg_cost)
這裏就是獲取訓練測試是所以想的數據讀取reader,通過使用paddle.batch()函數可以把多條數據打包成一個批次,訓練的時候是按照一個個批次訓練的。
# 獲取自定義數據
train_reader = paddle.batch(reader=reader.train_reader('images/train.list', crop_size, resize_size), batch_size=32)
test_reader = paddle.batch(reader=reader.test_reader('images/test.list', crop_size), batch_size=32)
執行訓練之前,還需要創建一個執行器,建議使用GPU進行訓練,因爲我們訓練的圖片比較大,所以使用CPU訓練速度會相當的慢。
# 定義一個使用GPU的執行器
place = fluid.CUDAPlace(0)
# place = fluid.CPUPlace()
exe = fluid.Executor(place)
# 進行參數初始化
exe.run(fluid.default_startup_program())
# 定義輸入數據維度
feeder = fluid.DataFeeder(place=place, feed_list=[image, label])
最後終於可以執行訓練了,這裏跟在前些章節都幾乎一樣,就不重複介紹了。
# 訓練100次
for pass_id in range(100):
# 進行訓練
for batch_id, data in enumerate(train_reader()):
train_cost, train_acc = exe.run(program=fluid.default_main_program(),
feed=feeder.feed(data),
fetch_list=[avg_cost, acc])
# 每100個batch打印一次信息
if batch_id % 100 == 0:
print('Pass:%d, Batch:%d, Cost:%0.5f, Accuracy:%0.5f' %
(pass_id, batch_id, train_cost[0], train_acc[0]))
# 進行測試
test_accs = []
test_costs = []
for batch_id, data in enumerate(test_reader()):
test_cost, test_acc = exe.run(program=test_program,
feed=feeder.feed(data),
fetch_list=[avg_cost, acc])
test_accs.append(test_acc[0])
test_costs.append(test_cost[0])
# 求測試結果的平均值
test_cost = (sum(test_costs) / len(test_costs))
test_acc = (sum(test_accs) / len(test_accs))
print('Test:%d, Cost:%0.5f, Accuracy:%0.5f' % (pass_id, test_cost, test_acc))
訓練的過程中可以保存預測模型,用於之後的預測。筆者一般是每一個pass保存一次模型。
# 保存預測模型
save_path = 'infer_model/'
# 刪除舊的模型文件
shutil.rmtree(save_path, ignore_errors=True)
# 創建保持模型文件目錄
os.makedirs(save_path)
# 保存預測模型
fluid.io.save_inference_model(save_path, feeded_var_names=[image.name], target_vars=[model], executor=exe)
訓練輸出的信息:
Pass:0, Batch:0, Cost:1.84754, Accuracy:0.15625
Test:0, Cost:4.66276, Accuracy:0.17857
Pass:1, Batch:0, Cost:1.04008, Accuracy:0.59375
Test:1, Cost:1.23828, Accuracy:0.54464
Pass:2, Batch:0, Cost:1.04778, Accuracy:0.65625
Test:2, Cost:0.99189, Accuracy:0.64286
Pass:3, Batch:0, Cost:1.21555, Accuracy:0.65625
Test:3, Cost:1.01552, Accuracy:0.57589
Pass:4, Batch:0, Cost:0.64620, Accuracy:0.81250
Test:4, Cost:1.19264, Accuracy:0.63393
預測圖片¶
經過上面訓練後,得到了一個預測模型,下面我們就使用一個預測模型來預測一些圖片。
創建一個infer.py文件作爲預測程序。首先導入所需的依賴包。
import paddle.fluid as fluid
from PIL import Image
import numpy as np
創建一個執行器,這些不需要訓練,所以可以使用CPU進行預測,速度不會太慢,當然,使用GPU的預測速度會更快一些。
# 創建執行器
place = fluid.CPUPlace()
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
然後加載預測模型,獲取預測程序和輸入層的名字,還有網絡的分類器。
# 保存預測模型路徑
save_path = 'infer_model/'
# 從模型中獲取預測程序、輸入數據名稱列表、分類器
[infer_program, feeded_var_names, target_var] = fluid.io.load_inference_model(dirname=save_path, executor=exe)
預測圖片之前,還需要對圖片進行預處理,處理的方式跟測試的時候處理的方式一樣。
# 預處理圖片
def load_image(file):
img = Image.open(file)
# 統一圖像大小
img = img.resize((224, 224), Image.ANTIALIAS)
# 轉換成numpy值
img = np.array(img).astype(np.float32)
# 轉換成CHW
img = img.transpose((2, 0, 1))
# 轉換成BGR
img = img[(2, 1, 0), :, :] / 255.0
img = np.expand_dims(img, axis=0)
return img
最後獲取經過預處理的圖片數據,再使用這些圖像數據進行預測,得到分類結果。
# 獲取圖片數據
img = load_image('images/apple/0fdd5422-31e0-11e9-9cfd-3c970e769528.jpg')
# 執行預測
result = exe.run(program=infer_program,
feed={feeded_var_names[0]: img},
fetch_list=target_var)
我們可以通過解析分類的結果,獲取概率最大類別標籤。關於預測輸出的result是數據,它是3維的,第一層是輸出本身就是一個數組,第二層圖片的數量,因爲PaddlePaddle支持多張圖片同時預測,最後一層就是每個類別的概率,這個概率的總和爲1,概率最大的標籤就是預測結果。
# 顯示圖片並輸出結果最大的label
lab = np.argsort(result)[0][0][-1]
names = ['蘋果', '哈密瓜', '胡蘿蔔', '櫻桃', '黃瓜', '西瓜']
print('預測結果標籤爲:%d, 名稱爲:%s, 概率爲:%f' % (lab, names[lab], result[0][0][lab]))
預測輸出的結果:
預測結果標籤爲:0, 名稱爲:蘋果, 概率爲:0.948698
GitHub地址:https://github.com/yeyupiaoling/LearnPaddle2/tree/master/note11
上一章:《PaddlePaddle從入門到煉丹》十——VisualDL 訓練可視化¶
下一章:《PaddlePaddle從入門到煉丹》十二——自定義文本數據集分類¶
參考資料¶
- https://yeyupiaoling.blog.csdn.net/article/details/79095265
- https://yeyupiaoling.blog.csdn.net/article/details/79127017