<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從零開始編寫一個(gè)寵物識(shí)別系統(tǒng)(爬蟲、模型訓(xùn)練和調(diào)優(yōu)、模型部署、Web服務(wù))

          共 21517字,需瀏覽 44分鐘

           ·

          2022-05-27 09:59

          點(diǎn)擊下方卡片,關(guān)注“新機(jī)器視覺”公眾號(hào)

          重磅干貨,第一時(shí)間送達(dá)

          ?來源丨全棧工程師的自我修養(yǎng)


          心血來潮,想從零開始編寫一個(gè)相對(duì)完整的深度學(xué)習(xí)小項(xiàng)目。想到就做,那么首先要考慮的問題是,寫什么?

          思量再三,我決定寫一個(gè)寵物識(shí)別系統(tǒng),即給定一張圖片,判斷圖片上的寵物是什么。寵物種類暫定為四類——貓、狗、鼠、兔。之所以想到做這個(gè),是因?yàn)樵诓皇褂霉_數(shù)據(jù)集的情況下,寵物圖片數(shù)據(jù)集獲取的難度相對(duì)低一些。

          小項(xiàng)目分為如下幾個(gè)部分:

          • 爬蟲。從網(wǎng)絡(luò)上下載寵物圖片,構(gòu)建訓(xùn)練用的數(shù)據(jù)集。

          • 模型構(gòu)建、訓(xùn)練和調(diào)優(yōu)。鑒于我們的數(shù)據(jù)比較少,這部分需要做遷移學(xué)習(xí)。

          • 模型部署和 Web 服務(wù)。將訓(xùn)練好的模型部署成 Web 接口,并使用 Vue.js + Element UI 編寫測(cè)試頁面。

          好嘞,開搞吧!

          本文涉及到的所有代碼,均已上傳到 GitHub:

          pets_classifer?(https://github.com/AaronJny/pets_classifer)

          一、爬蟲

          訓(xùn)練模型肯定是需要數(shù)據(jù)集的,那么數(shù)據(jù)集從哪來?因?yàn)槭菑牧汩_始嘛,假設(shè)我們做的這個(gè)問題,業(yè)內(nèi)沒有公開的數(shù)據(jù)集,我們需要自己制作數(shù)據(jù)集。

          一個(gè)很簡(jiǎn)單的想法是,利用搜索引擎搜索相關(guān)圖片,使用爬蟲批量下載,然后人工去除不正確的圖片。舉個(gè)例子,我們先處理貓的圖片,步驟如下:

          • 1.使用搜索引擎搜索貓的圖片。

          • 2.使用爬蟲將搜索出的貓的圖片批量下載到本地,放到一個(gè)名為cats的文件夾里面。

          • 3.人工瀏覽一遍圖片,將“不包含貓”的圖片和“除貓外還包含其他寵物(狗、鼠、兔)”的圖片從文件夾中刪除。

          這樣,貓的圖片我們就搜集完成了,其他幾個(gè)類別的圖片也是類似的操作。不用擔(dān)心人工過濾圖片花費(fèi)的時(shí)間較長,全部過一遍也就二十多分鐘吧。

          然后是搜索引擎的選擇。搜索引擎用的比較多的無非兩種——Google 和百度。我分別使用 Google 和百度進(jìn)行了圖片搜索,發(fā)現(xiàn)百度的搜索結(jié)果遠(yuǎn)不如 Google 準(zhǔn)確,于是就選擇了 Google,所以我的爬蟲代碼是基于 Google 編寫的,運(yùn)行我的爬蟲代碼需要你的網(wǎng)絡(luò)能夠訪問 Google。

          如果你的網(wǎng)絡(luò)不能訪問 Google,可以考慮自行實(shí)現(xiàn)基于百度的爬蟲程序,邏輯都是相通的。

          因?yàn)橄胱岉?xiàng)目輕量級(jí)一些,故沒有使用 scrapy 框架。爬蟲使用 requests+beautifulsoup4 實(shí)現(xiàn),并發(fā)使用 gevent 實(shí)現(xiàn)。

          # -*- coding: utf-8 -*-
          # @File : spider.py
          # @Author : AaronJny
          # @Time : 2019/12/16
          # @Desc : 從谷歌下載指定圖片
          from gevent import monkey

          monkey.patch_all()
          import functools
          import logging
          import os
          from bs4 import BeautifulSoup
          from gevent.pool import Pool
          import requests
          import settings

          # 設(shè)置日志輸出格式
          logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',
          level=logging.INFO)

          # 搜索關(guān)鍵詞字典
          keywords_map = settings.IMAGE_CLASS_KEYWORD_MAP

          # 圖片保存根目錄
          images_root = settings.IMAGES_ROOT
          # 每個(gè)類別下載多少頁圖片
          download_pages = settings.SPIDER_DOWNLOAD_PAGES
          # 圖片編號(hào)字典,每種圖片都從0開始編號(hào),然后遞增
          images_index_map = dict(zip(keywords_map.keys(), [0 for _ in keywords_map]))
          # 圖片去重器
          duplication_filter = set()

          # 請(qǐng)求頭
          headers = {
          'accept-encoding': 'gzip, deflate, br',
          'accept-language': 'zh-CN,zh;q=0.9',
          'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36',
          'accept': '*/*',
          'referer': 'https://www.google.com/',
          'authority': 'www.google.com',
          }


          # 重試裝飾器
          def try_again_while_except(max_times=3):
          """
          當(dāng)出現(xiàn)異常時(shí),自動(dòng)重試。
          連續(xù)失敗max_times次后放棄。
          """

          def decorator(func):
          @functools.wraps(func)
          def wrapper(*args, **kwargs):
          error_cnt = 0
          error_msg = ''
          while error_cnt < max_times:
          try:
          return func(*args, **kwargs)
          except Exception as e:
          error_msg = str(e)
          error_cnt += 1
          if error_msg:
          logging.error(error_msg)

          return wrapper

          return decorator


          @try_again_while_except()
          def download_image(session, image_url, image_class):
          """
          從給定的url中下載圖片,并保存到指定路徑
          """
          # 下載圖片
          resp = session.get(image_url, timeout=20)
          # 檢查圖片是否下載成功
          if resp.status_code != 200:
          raise Exception('Response Status Code {}!'.format(resp.status_code))
          # 分配一個(gè)圖片編號(hào)
          image_index = images_index_map.get(image_class, 0)
          # 更新待分配編號(hào)
          images_index_map[image_class] = image_index + 1
          # 拼接圖片路徑
          image_path = os.path.join(images_root, image_class, '{}.jpg'.format(image_index))
          # 保存圖片
          with open(image_path, 'wb') as f:
          f.write(resp.content)
          # 成功寫入了一張圖片
          return True


          @try_again_while_except()
          def get_and_analysis_google_search_page(session, page, image_class, keyword):
          """
          使用google進(jìn)行搜索,下載搜索結(jié)果頁面,解析其中的圖片地址,并對(duì)有效圖片進(jìn)一步發(fā)起請(qǐng)求
          """
          logging.info('Class:{} Page:{} Processing...'.format(image_class, page + 1))
          # 記錄從本頁成功下載的圖片數(shù)量
          downloaded_cnt = 0
          # 構(gòu)建請(qǐng)求參數(shù)
          params = (
          ('q', keyword),
          ('tbm', 'isch'),
          ('async', '_id:islrg_c,_fmt:html'),
          ('asearch', 'ichunklite'),
          ('start', str(page * 100)),
          ('ijn', str(page)),
          )
          # 進(jìn)行搜索
          resp = requests.get('https://www.google.com/search', params=params, timeout=20)
          # 解析搜索結(jié)果
          bsobj = BeautifulSoup(resp.content, 'lxml')
          divs = bsobj.find_all('div', {'class': 'islrtb isv-r'})
          for div in divs:
          image_url = div.get('data-ou')
          # 只有當(dāng)圖片以'.jpg','.jpeg','.png'結(jié)尾時(shí)才下載圖片
          if image_url.endswith('.jpg') or image_url.endswith('.jpeg') or image_url.endswith('.png'):
          # 過濾掉相同圖片
          if image_url not in duplication_filter:
          # 使用去重器記錄
          duplication_filter.add(image_url)
          # 下載圖片
          flag = download_image(session, image_url, image_class)
          if flag:
          downloaded_cnt += 1
          logging.info('Class:{} Page:{} Done. {} images downloaded.'.format(image_class, page + 1, downloaded_cnt))


          def search_with_google(image_class, keyword):
          """
          通過google下載數(shù)據(jù)集
          """
          # 創(chuàng)建session對(duì)象
          session = requests.session()
          session.headers.update(headers)
          # 每個(gè)類別下載10頁數(shù)據(jù)
          for page in range(download_pages):
          get_and_analysis_google_search_page(session, page, image_class, keyword)


          def run():
          # 首先,創(chuàng)建數(shù)據(jù)文件夾
          if not os.path.exists(images_root):
          os.mkdir(images_root)
          for sub_images_dir in keywords_map.keys():
          # 對(duì)于每個(gè)圖片類別都創(chuàng)建一個(gè)單獨(dú)的文件夾保存
          sub_path = os.path.join(images_root, sub_images_dir)
          if not os.path.exists(sub_path):
          os.mkdir(sub_path)
          # 開始下載,這里使用gevent的協(xié)程池進(jìn)行并發(fā)
          pool = Pool(len(keywords_map))
          for image_class, keyword in keywords_map.items():
          pool.spawn(search_with_google, image_class, keyword)
          pool.join()


          if __name__ == '__main__':
          run()

          項(xiàng)目中涉及到的所有配置參數(shù),都提取到了settings.py中,內(nèi)容如下,以供查閱:

          # -*- coding: utf-8 -*-
          # @File : settings.py
          # @Author : AaronJny
          # @Time : 2019/12/16
          # @Desc :


          # ##########爬蟲############

          # 圖片類別和搜索關(guān)鍵詞的映射關(guān)系
          IMAGE_CLASS_KEYWORD_MAP = {
          'cats': '寵物貓',
          'dogs': '寵物狗',
          'mouses': '寵物鼠',
          'rabbits': '寵物兔'
          }
          # 圖片保存根目錄
          IMAGES_ROOT = './images'
          # 爬蟲每個(gè)類別下載多少頁圖片
          SPIDER_DOWNLOAD_PAGES = 20

          # #########數(shù)據(jù)###########

          # 每個(gè)類別選取的圖片數(shù)量
          SAMPLES_PER_CLASS = 345
          # 參與訓(xùn)練的類別
          CLASSES = ['cats', 'dogs', 'mouses', 'rabbits']
          # 參與訓(xùn)練的類別數(shù)量
          CLASS_NUM = len(CLASSES)
          # 類別->編號(hào)的映射
          CLASS_CODE_MAP = {
          'cats': 0,
          'dogs': 1,
          'mouses': 2,
          'rabbits': 3
          }
          # 編號(hào)->類別的映射
          CODE_CLASS_MAP = {
          0: '貓',
          1: '狗',
          2: '鼠',
          3: '兔'
          }
          # 隨機(jī)數(shù)種子
          RANDOM_SEED = 13 # 四個(gè)類別時(shí)樣本較為均衡的隨機(jī)數(shù)種子
          # RANDOM_SEED = 19 # 三個(gè)類別時(shí)樣本較為均衡的隨機(jī)數(shù)種子
          # 訓(xùn)練集比例
          TRAIN_DATASET = 0.6
          # 開發(fā)集比例
          DEV_DATASET = 0.2
          # 測(cè)試集比例
          TEST_DATASET = 0.2
          # mini_batch大小
          BATCH_SIZE = 16
          # imagenet數(shù)據(jù)集均值
          IMAGE_MEAN = [0.485, 0.456, 0.406]
          # imagenet數(shù)據(jù)集標(biāo)準(zhǔn)差
          IMAGE_STD = [0.299, 0.224, 0.225]

          # #########訓(xùn)練#########

          # 學(xué)習(xí)率
          LEARNING_RATE = 0.001
          # 訓(xùn)練epoch數(shù)
          TRAIN_EPOCHS = 30
          # 保存訓(xùn)練模型的路徑
          MODEL_PATH = './model.h5'

          # ########Web#########

          # Web服務(wù)端口
          WEB_PORT = 5000

          爬蟲使用 Google 進(jìn)行圖片搜索,每個(gè)寵物搜索 10 頁,下載其中的所有圖片。當(dāng)爬蟲運(yùn)行完成后,項(xiàng)目下會(huì)多出一個(gè)images文件夾,點(diǎn)進(jìn)去有四個(gè)子文件夾,分別為catsdogs、mouses、rabbits。每一個(gè)子文件夾里面是對(duì)應(yīng)類別的寵物圖片。




          其中貓圖片 600+ 張,狗圖片 600+ 張,鼠圖片 400+ 張,兔圖片 500+ 張?;ǘ喾昼姇r(shí)間,過一遍全部圖片,剔除其中不符合要求的圖片。注意,這一步是必做的,而且要認(rèn)真對(duì)待,我吃了虧的 = =

          進(jìn)行一輪篩選后,剩下圖片張數(shù):

          寵物圖片數(shù)量
          521
          526
          346
          345

          考慮各類別樣本均衡的問題,無非是過采樣和欠采樣。因?yàn)槭菆D片數(shù)據(jù),也可以使用數(shù)據(jù)增強(qiáng)的手段,為圖片數(shù)量較少的類別生成一些圖片,使樣本數(shù)量均衡。但出于如下原因考慮,我直接做了欠采樣,即每個(gè)類別只選取了 345 張樣本:

          • 使用數(shù)據(jù)增強(qiáng)的話,需要在原圖片的基礎(chǔ)上,重新生成一份數(shù)據(jù)集,嫌麻煩……

          • 使用數(shù)據(jù)增強(qiáng)后,樣本數(shù)量比較多,無法同時(shí)讀取到內(nèi)存里面,只能寫個(gè)生成器,處理哪一部分的時(shí)候,實(shí)時(shí)從硬盤讀取。弊端有倆:① 頻繁讀取硬盤,肯定比不上所有數(shù)據(jù)都放在內(nèi)存里面,會(huì)拖慢訓(xùn)練速度;② 還是嫌麻煩……

          說到底就是自己太懶了……當(dāng)然,可想而知,使用數(shù)據(jù)增強(qiáng)(在這里,數(shù)據(jù)增強(qiáng)可以作為一種過采樣的方式)使數(shù)據(jù)樣本都達(dá)到 526,訓(xùn)練的效果肯定會(huì)更好,能好多少就不知道了,有興趣的可以自行實(shí)現(xiàn),沒啥難點(diǎn),就是麻煩點(diǎn)。

          下面該對(duì)數(shù)據(jù)做預(yù)處理了。很多經(jīng)典的模型接收的輸入格式都為(None,224,224,3),由于我們的樣本較少,不可避免地需要用到遷移學(xué)習(xí),所以我們的數(shù)據(jù)格式與經(jīng)典模型保持一致,也使用(None,224,224,3),下面是預(yù)處理過程:

          # -*- coding: utf-8 -*-
          # @File : data.py
          # @Author : AaronJny
          # @Time : 2019/12/16
          # @Desc :
          import os
          import random
          import tensorflow as tf
          import settings

          # 每個(gè)類別選取的圖片數(shù)量
          samples_per_class = settings.SAMPLES_PER_CLASS
          # 圖片根目錄
          images_root = settings.IMAGES_ROOT
          # 類別->編碼的映射
          class_code_map = settings.CLASS_CODE_MAP

          # 我們準(zhǔn)備使用經(jīng)典網(wǎng)絡(luò)在imagenet數(shù)據(jù)集上的與訓(xùn)練權(quán)重,所以歸一化時(shí)也要使用imagenet的平均值和標(biāo)準(zhǔn)差
          image_mean = tf.constant(settings.IMAGE_MEAN)
          image_std = tf.constant(settings.IMAGE_STD)


          def normalization(x):
          """
          對(duì)輸入圖片x進(jìn)行歸一化,返回歸一化的值
          """
          return (x - image_mean) / image_std


          def train_preprocess(x, y):
          """
          對(duì)訓(xùn)練數(shù)據(jù)進(jìn)行預(yù)處理。
          注意,這里的參數(shù)x是圖片的路徑,不是圖片本身;y是圖片的標(biāo)簽值
          """
          # 讀取圖片
          x = tf.io.read_file(x)
          # 解碼成張量
          x = tf.image.decode_jpeg(x, channels=3)
          # 將圖片縮放到[244,244],比輸入[224,224]稍大一些,方便后面數(shù)據(jù)增強(qiáng)
          x = tf.image.resize(x, [244, 244])
          # 隨機(jī)決定是否左右鏡像
          if random.choice([0, 1]):
          x = tf.image.random_flip_left_right(x)
          # 隨機(jī)從x中剪裁出(224,224,3)大小的圖片
          x = tf.image.random_crop(x, [224, 224, 3])
          # 讀完上面的代碼可以發(fā)現(xiàn),這里的數(shù)據(jù)增強(qiáng)并不增加圖片數(shù)量,一張圖片經(jīng)過變換后,
          # 仍然只是一張圖片,跟我們前面說的增加圖片數(shù)量的邏輯不太一樣。
          # 這么做主要是應(yīng)對(duì)我們的數(shù)據(jù)集里可能會(huì)存在相同圖片的情況。

          # 將圖片的像素值縮放到[0,1]之間
          x = tf.cast(x, dtype=tf.float32) / 255.
          # 歸一化
          x = normalization(x)

          # 將標(biāo)簽轉(zhuǎn)成one-hot形式
          y = tf.cast(y, dtype=tf.int32)
          y = tf.one_hot(y, settings.CLASS_NUM)

          return x, y


          def dev_preprocess(x, y):
          """
          對(duì)驗(yàn)證集和測(cè)試集進(jìn)行數(shù)據(jù)預(yù)處理的方法。
          和train_preprocess的主要區(qū)別在于,不進(jìn)行數(shù)據(jù)增強(qiáng),以保證驗(yàn)證結(jié)果的穩(wěn)定性。
          """
          # 讀取并縮放圖片
          x = tf.io.read_file(x)
          x = tf.image.decode_jpeg(x, channels=3)
          x = tf.image.resize(x, [224, 224])
          # 歸一化
          x = tf.cast(x, dtype=tf.float32) / 255.
          x = normalization(x)
          # 將標(biāo)簽轉(zhuǎn)成one-hot形式
          y = tf.cast(y, dtype=tf.int32)
          y = tf.one_hot(y, settings.CLASS_NUM)

          return x, y


          # (圖片路徑,標(biāo)簽)的列表
          image_path_and_labels = []
          # 排序,保證每次拿到的順序都一樣
          sub_images_dir_list = sorted(list(os.listdir(images_root)))
          # 遍歷每一個(gè)子目錄
          for sub_images_dir in sub_images_dir_list:
          sub_path = os.path.join(images_root, sub_images_dir)
          # 如果給定路徑是文件夾,并且這個(gè)類別參與訓(xùn)練
          if os.path.isdir(sub_path) and sub_images_dir in settings.CLASSES:
          # 獲取當(dāng)前類別的編碼
          current_label = class_code_map.get(sub_images_dir)
          # 獲取子目錄下的全部圖片名稱
          images = sorted(list(os.listdir(sub_path)))
          # 隨機(jī)打亂(排序和置隨機(jī)數(shù)種子都是為了保證每次的結(jié)果都一樣)
          random.seed(settings.RANDOM_SEED)
          random.shuffle(images)
          # 保留前settings.SAMPLES_PER_CLASS個(gè)
          images = images[:samples_per_class]
          # 構(gòu)建(x,y)對(duì)
          for image_name in images:
          abs_image_path = os.path.join(sub_path, image_name)
          image_path_and_labels.append((abs_image_path, current_label))
          # 計(jì)算各數(shù)據(jù)集樣例數(shù)
          total_samples = len(image_path_and_labels) # 總樣例數(shù)
          train_samples = int(total_samples * settings.TRAIN_DATASET) # 訓(xùn)練集樣例數(shù)
          dev_samples = int(total_samples * settings.DEV_DATASET) # 開發(fā)集樣例數(shù)
          test_samples = total_samples - train_samples - dev_samples # 測(cè)試集樣例數(shù)
          # 打亂數(shù)據(jù)集
          random.seed(settings.RANDOM_SEED)
          random.shuffle(image_path_and_labels)
          # 將圖片數(shù)據(jù)和標(biāo)簽數(shù)據(jù)分開,此時(shí)它們?nèi)允且灰粚?duì)應(yīng)的
          x_data = tf.constant([img for img, label in image_path_and_labels])
          y_data = tf.constant([label for img, label in image_path_and_labels])
          # 開始劃分?jǐn)?shù)據(jù)集
          # 訓(xùn)練集
          train_db = tf.data.Dataset.from_tensor_slices((x_data[:train_samples], y_data[:train_samples]))
          # 打亂順序,數(shù)據(jù)預(yù)處理,設(shè)置批大小
          train_db = train_db.shuffle(10000).map(train_preprocess).batch(settings.BATCH_SIZE)
          # 開發(fā)集(驗(yàn)證集)
          dev_db = tf.data.Dataset.from_tensor_slices(
          (x_data[train_samples:train_samples + dev_samples], y_data[train_samples:train_samples + dev_samples]))
          # 數(shù)據(jù)預(yù)處理,設(shè)置批大小
          dev_db = dev_db.map(dev_preprocess).batch(settings.BATCH_SIZE)
          # 測(cè)試集
          test_db = tf.data.Dataset.from_tensor_slices(
          (x_data[train_samples + dev_samples:], y_data[train_samples + dev_samples:]))
          # 數(shù)據(jù)預(yù)處理,設(shè)置批大小
          test_db = test_db.map(dev_preprocess).batch(settings.BATCH_SIZE)

          二、模型構(gòu)建、訓(xùn)練和調(diào)優(yōu)

          數(shù)據(jù)已經(jīng)全部處理完畢,該考慮模型了。首先,我們數(shù)據(jù)集太小了,直接構(gòu)建自己的網(wǎng)絡(luò)并訓(xùn)練,并不是一個(gè)好方案。因?yàn)檫@幾種寵物其實(shí)挺難區(qū)分的,所以模型需要有一定復(fù)雜度,才能很好擬合這些數(shù)據(jù),但我們的數(shù)據(jù)又太少了,最后的結(jié)果一定是過擬合,而且還是救不回來的那種 = = 所以我們考慮從遷移學(xué)習(xí)入手。

          什么是遷移學(xué)習(xí)?懶得重新組織語言的我,默默地從之前寫的博文里面摘了一段:

          一般認(rèn)為,深度卷積神經(jīng)網(wǎng)絡(luò)的訓(xùn)練是對(duì)數(shù)據(jù)集特征的一步步抽取的過程,從簡(jiǎn)單的特征,到復(fù)雜的特征。
          訓(xùn)練好的模型學(xué)習(xí)到的是對(duì)圖像特征的抽取方法,所以在 imagenet 數(shù)據(jù)集上訓(xùn)練好的模型理論上來說,也可以直接用于抽取其他圖像的特征,這也是遷移學(xué)習(xí)的基礎(chǔ)。自然,這樣的效果往往沒有在新數(shù)據(jù)上重新訓(xùn)練的效果好,但能夠節(jié)省大量的訓(xùn)練時(shí)間,在特定情況下非常有用。

          上面說的特定情況也包括我們面臨的這一種——用于實(shí)際問題的數(shù)據(jù)集過小。

          說到遷移學(xué)習(xí),我最先想到的是 VGG16,就先用 VGG16 搞了一波。使用在 imagenet 數(shù)據(jù)集上預(yù)訓(xùn)練的 VGG16 網(wǎng)絡(luò),去除頂部的全連接層,凍結(jié)全部參數(shù),使它們?cè)诮酉聛淼挠?xùn)練中不會(huì)改變。然后加上自己的全連接層,最后的輸出層節(jié)點(diǎn)為 4,對(duì)應(yīng)于我們的四分類問題。開始訓(xùn)練。

          模型在訓(xùn)練集上的誤差很快降到 5% 以下,但是在驗(yàn)證集上的準(zhǔn)確率基本在 70+%,很明顯,過擬合了。好嘛,盤它!主要使用如下方法嘗試解決過擬合問題:

          • 調(diào)節(jié)全連接層的層數(shù)和每層的節(jié)點(diǎn)數(shù)

          • 添加 BN 層(雖說不是為了解決過擬合問題誕生的,但一定程度上是有效果的)

          • 添加 Dropout 層

          • 調(diào)節(jié) Dropout Rate

          • 添加 l2 正則

          一頓操作猛如虎,回頭一看 0-5。這些方法確實(shí)對(duì)過擬合有所緩解,驗(yàn)證集上的準(zhǔn)確率也確實(shí)有所提升,但只能達(dá)到 81% 左右。

          然后我嘗試了 Resnet50,當(dāng)然也過擬合了,盤它!最后驗(yàn)證集 accuracy 能達(dá)到 83% 左右。

          很明顯了,在全連接層的調(diào)整意義不大,究其根本,在于 VGG16 和 ResNet50 去除了全連接層之后,參數(shù)的數(shù)量也達(dá)到了 20M+。兩千萬的參數(shù)使得模型嚴(yán)重過擬合,所以我們需要換一個(gè)參數(shù)少一點(diǎn)的模型。

          于是,我盯上了 DenseNet121,它的參數(shù)數(shù)量只有 7M。繼續(xù)盤它!果然,在一段時(shí)間的調(diào)優(yōu)后,模型的性能有了明顯的提升,驗(yàn)證集上的 accuracy 達(dá)到了 87% 左右。雖然和 ResNet 相比,準(zhǔn)確率只高了 4%,但相比于 ResNet50 96% 的訓(xùn)練 accuracy 而言,DenseNet121 的訓(xùn)練 accuracy 只有 90% 左右。也就是說,對(duì)于 DenseNet121 而言,這個(gè)問題已經(jīng)不再是過擬合問題了(相差 3% 我是可以接受的),而是欠擬合了。

          然而淡騰的是,再怎么調(diào)參,模型都很難繼續(xù)擬合了,調(diào)小學(xué)習(xí)率也不行。模型本身沒啥問題的話,我開始懷疑數(shù)據(jù)集有沒有問題,畢竟這種無法擬合的問題有很大概率是數(shù)據(jù)導(dǎo)致的。于是我就去檢查了一下數(shù)據(jù)集……

          這就是我前面強(qiáng)調(diào)認(rèn)真過一遍數(shù)據(jù)集的原因了,我當(dāng)時(shí)只是花個(gè)幾分鐘粗略地過了一下,刪除掉一些明顯不對(duì)的圖片。我第二次認(rèn)真過數(shù)據(jù)集的時(shí)候才發(fā)現(xiàn),有很多異常圖片沒有過濾掉,比如貓的目錄下有狗的圖片,狗的目錄下有貓的圖片,還有一些不同動(dòng)物同框的圖片,以及我自己都認(rèn)不出來的圖片……




          文章第一部分中各類圖片數(shù)量的表格,其實(shí)就是我第二遍過濾后的結(jié)果統(tǒng)計(jì)。

          過濾完成后,模型的性能有了明顯的提升,訓(xùn)練 accuracy 約為 93%-94%,驗(yàn)證 accuracy 為 94%,測(cè)試 accuracy 為 92%.我們先來看一下代碼,后面會(huì)對(duì)這個(gè)結(jié)果再進(jìn)行分析。

          首先,是模型的構(gòu)建:

          # -*- coding: utf-8 -*-
          # @File : models.py
          # @Author : AaronJny
          # @Time : 2019/12/16
          # @Desc :
          import tensorflow as tf
          import settings


          def my_densenet():
          """
          創(chuàng)建并返回一個(gè)基于densenet的Model對(duì)象
          """
          # 獲取densenet網(wǎng)絡(luò),使用在imagenet上訓(xùn)練的參數(shù)值,移除頭部的全連接網(wǎng)絡(luò),池化層使用max_pooling
          densenet = tf.keras.applications.DenseNet121(include_top=False, weights='imagenet', pooling='max')
          # 凍結(jié)預(yù)訓(xùn)練的參數(shù),在之后的模型訓(xùn)練中不會(huì)改變它們
          densenet.trainable = False
          # 構(gòu)建模型
          model = tf.keras.Sequential([
          # 輸入層,shape為(None,224,224,3)
          tf.keras.layers.Input((224, 224, 3)),
          # 輸入到DenseNet121中
          densenet,
          # 將DenseNet121的輸出展平,以作為全連接層的輸入
          tf.keras.layers.Flatten(),
          # 添加BN層
          tf.keras.layers.BatchNormalization(),
          # 隨機(jī)失活
          tf.keras.layers.Dropout(0.5),
          # 第一個(gè)全連接層,激活函數(shù)relu
          tf.keras.layers.Dense(512, activation=tf.nn.relu),
          # BN層
          tf.keras.layers.BatchNormalization(),
          # 隨機(jī)失活
          tf.keras.layers.Dropout(0.5),
          # 第二個(gè)全連接層,激活函數(shù)relu
          tf.keras.layers.Dense(64, activation=tf.nn.relu),
          # BN層
          tf.keras.layers.BatchNormalization(),
          # 輸出層,為了保證輸出結(jié)果的穩(wěn)定,這里就不添加Dropout層了
          tf.keras.layers.Dense(settings.CLASS_NUM, activation=tf.nn.softmax)
          ])

          return model


          if __name__ == '__main__':
          model = my_densenet()
          model.summary()

          網(wǎng)絡(luò)的 summary:

          Model: "sequential"
          _________________________________________________________________
          Layer (type) Output Shape Param #
          =================================================================
          densenet121 (Model) (None, 1024) 7037504
          _________________________________________________________________
          flatten (Flatten) (None, 1024) 0
          _________________________________________________________________
          batch_normalization (BatchNo (None, 1024) 4096
          _________________________________________________________________
          dropout (Dropout) (None, 1024) 0
          _________________________________________________________________
          dense (Dense) (None, 512) 524800
          _________________________________________________________________
          batch_normalization_1 (Batch (None, 512) 2048
          _________________________________________________________________
          dropout_1 (Dropout) (None, 512) 0
          _________________________________________________________________
          dense_1 (Dense) (None, 64) 32832
          _________________________________________________________________
          batch_normalization_2 (Batch (None, 64) 256
          _________________________________________________________________
          dense_2 (Dense) (None, 4) 260
          =================================================================
          Total params: 7,601,796
          Trainable params: 561,092
          Non-trainable params: 7,040,704
          _________________________________________________________________

          參數(shù)總量 7601796 個(gè),其中可訓(xùn)練參數(shù) 561092 個(gè) 。

          模型和數(shù)據(jù)都已準(zhǔn)備完畢,可以開始訓(xùn)練了。讓我們編寫一個(gè)訓(xùn)練用的腳本:

          # -*- coding: utf-8 -*-
          # @File : train.py
          # @Author : AaronJny
          # @Time : 2019/12/17
          # @Desc :
          import tensorflow as tf
          from data import train_db, dev_db
          import models
          import settings

          # 從models文件中導(dǎo)入模型
          model = models.my_densenet()
          model.summary()

          # 配置優(yōu)化器、損失函數(shù)、以及監(jiān)控指標(biāo)
          model.compile(tf.keras.optimizers.Adam(settings.LEARNING_RATE), loss=tf.keras.losses.categorical_crossentropy,
          metrics=['accuracy'])

          # 在每個(gè)epoch結(jié)束后嘗試保存模型參數(shù),只有當(dāng)前參數(shù)的val_accuracy比之前保存的更優(yōu)時(shí),才會(huì)覆蓋掉之前保存的參數(shù)
          model_check_point = tf.keras.callbacks.ModelCheckpoint(filepath=settings.MODEL_PATH, monitor='val_accuracy',
          save_best_only=True)
          # 使用tf.keras的高級(jí)接口進(jìn)行訓(xùn)練
          model.fit_generator(train_db, epochs=settings.TRAIN_EPOCHS, validation_data=dev_db, callbacks=[model_check_point])

          現(xiàn)在,我們可以運(yùn)行腳本進(jìn)行訓(xùn)練了,最優(yōu)的參數(shù)將被保存在settings.MODEL_PATH。訓(xùn)練完成后,我們需要調(diào)用驗(yàn)證腳本,驗(yàn)證下模型在驗(yàn)證集和測(cè)試集上的表現(xiàn):

          # -*- coding: utf-8 -*-
          # @File : eval.py
          # @Author : AaronJny
          # @Time : 2019/12/17
          # @Desc :
          import tensorflow as tf
          from data import dev_db, test_db
          from models import my_densenet
          import settings

          # 創(chuàng)建模型
          model = my_densenet()
          # 加載參數(shù)
          model.load_weights(settings.MODEL_PATH)
          # 因?yàn)橄胗胻f.keras的高級(jí)接口做驗(yàn)證,所以還是需要編譯模型
          model.compile(tf.keras.optimizers.Adam(settings.LEARNING_RATE), loss=tf.keras.losses.categorical_crossentropy,
          metrics=['accuracy'])
          # 驗(yàn)證集accuracy
          print('dev', model.evaluate(dev_db))
          # 測(cè)試集accuracy
          print('test', model.evaluate(test_db))

          輸出如下:

          18/18 [==============================] - 5s 304ms/step - loss: 0.1936 - accuracy: 0.9457
          dev [0.19364455559601387, 0.9456522]
          18/18 [==============================] - 1s 64ms/step - loss: 0.2666 - accuracy: 0.9203
          test [0.26657224384446937, 0.9202899]

          能夠看到,模型在驗(yàn)證集上的準(zhǔn)確率為 94.57%,在測(cè)試集上的準(zhǔn)確率為 92.03%,已經(jīng)達(dá)到我的心里預(yù)期了,畢竟這么少的數(shù)據(jù),還要啥自行車?

          隨著訓(xùn)練 epoch 的增多,模型的訓(xùn)練 accuracy 始終在[0.92,0.95]左右徘徊不定,沒法繼續(xù)擬合。究其原因,應(yīng)該還是數(shù)據(jù)的鍋。我們看一下識(shí)別錯(cuò)的樣本,在 eval.py 腳本中,增加下面這一段程序:

          # 查看識(shí)別錯(cuò)誤的數(shù)據(jù)
          for x, y in test_db:
          y_pred = model(x)
          y_pred = tf.argmax(y_pred, axis=1).numpy()
          y_true = tf.argmax(y, axis=1).numpy()
          batch_size = y_pred.shape[0]
          for i in range(batch_size):
          if y_pred[i] != y_true[i]:
          print('{} 被錯(cuò)誤識(shí)別成 {}!'.format(settings.CODE_CLASS_MAP[y_true[i]], settings.CODE_CLASS_MAP[y_pred[i]]))

          重新跑一下 eval.py 腳本,輸出如下:

          18/18 [==============================] - 5s 291ms/step - loss: 0.1936 - accuracy: 0.9457
          dev [0.19364455559601387, 0.9456522]
          18/18 [==============================] - 1s 64ms/step - loss: 0.2666 - accuracy: 0.9203
          test [0.26657224384446937, 0.9202899]
          狗 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 兔!
          鼠 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 貓!
          鼠 被錯(cuò)誤識(shí)別成 貓!
          狗 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 鼠!
          鼠 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 兔!
          貓 被錯(cuò)誤識(shí)別成 兔!
          貓 被錯(cuò)誤識(shí)別成 鼠!
          貓 被錯(cuò)誤識(shí)別成 兔!
          鼠 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 貓!
          鼠 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 兔!
          鼠 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 貓!
          鼠 被錯(cuò)誤識(shí)別成 兔!
          狗 被錯(cuò)誤識(shí)別成 兔!

          來,跟我一起唱——都是兔子惹的禍 ~

          能夠看到,出錯(cuò)的大部分都是被誤識(shí)別成兔子了。對(duì)應(yīng)到數(shù)據(jù)集上,雖然已經(jīng)刪掉了部分問題比較大的圖片,但兔子的圖片確實(shí)不好認(rèn)。有很多兔子圖片我人工分辨都認(rèn)不出是兔子(捂臉.jpg)。然后,有些兔子圖片看起來很像貓,有些看起來很像狗,有些看起來很像鼠……

          如果我們把兔子圖片去掉,將系統(tǒng)改為三分類問題,準(zhǔn)確度將大幅度提高。當(dāng)然了,按理說識(shí)別的類別數(shù)量變了,除了調(diào)整輸出層的節(jié)點(diǎn)數(shù)量外,要想取得最佳效果,模型的其他參數(shù)也需要做相應(yīng)調(diào)整的。我自己已經(jīng)實(shí)測(cè)了,但限于篇幅,就不演示了,如果有興趣的話,可以直接在settings.py里進(jìn)行調(diào)整,將它變?yōu)槿诸悊栴}。改這兩個(gè)地方:

          # 參與訓(xùn)練的類別
          CLASSES = ['cats', 'dogs', 'mouses', 'rabbits']
          # 隨機(jī)數(shù)種子
          RANDOM_SEED = 13 # 四個(gè)類別時(shí)樣本較為均衡的隨機(jī)數(shù)種子

          改成:

          # 參與訓(xùn)練的類別
          CLASSES = ['cats', 'dogs', 'mouses']
          # 隨機(jī)數(shù)種子
          RANDOM_SEED = 19 # 三個(gè)類別時(shí)樣本較為均衡的隨機(jī)數(shù)種子

          然后重新訓(xùn)練和驗(yàn)證即可。這只是一個(gè)插曲,本文仍然以四分類問題繼續(xù)說明后續(xù)內(nèi)容。

          三、Web 接口編寫

          模型訓(xùn)練好了,我們要把它應(yīng)用起來。我準(zhǔn)備編寫一個(gè) Web 服務(wù),用戶可以通過瀏覽器上傳一張圖片,服務(wù)器判斷此圖片的類別后,返回相關(guān)數(shù)據(jù)給用戶。Web 后端使用 Flask,小而輕,前端則選用 Vue.js + Element-UI 實(shí)現(xiàn)。

          先寫后端:

          # -*- coding: utf-8 -*-
          # @File : app.py
          # @Author : AaronJny
          # @Time : 2019/12/18
          # @Desc :
          from flask import Flask
          from flask import jsonify
          from flask import request, render_template
          import tensorflow as tf
          from models import my_densenet
          import settings

          app = Flask(__name__)

          # 導(dǎo)入模型
          model = my_densenet()
          # 加載訓(xùn)練好的參數(shù)
          model.load_weights(settings.MODEL_PATH)


          @app.route('/', methods=['GET'])
          def index():
          """
          首頁,vue入口
          """
          return render_template('index.html')


          @app.route('/api/v1/pets_classify/', methods=['POST'])
          def pets_classify():
          """
          寵物圖片分類接口,上傳一張圖片,返回此圖片上的寵物是那種類別,概率多少
          """
          # 獲取用戶上傳的圖片
          img_str = request.files.get('file').read()
          # 進(jìn)行數(shù)據(jù)預(yù)處理
          x = tf.image.decode_image(img_str, channels=3)
          x = tf.image.resize(x, (224, 224))
          x = x / 255.
          x = (x - tf.constant(settings.IMAGE_MEAN)) / tf.constant(settings.IMAGE_STD)
          x = tf.reshape(x, (1, 224, 224, 3))
          # 預(yù)測(cè)
          y_pred = model(x)
          pet_cls_code = tf.argmax(y_pred, axis=1).numpy()[0]
          pet_cls_prob = float(y_pred.numpy()[0][pet_cls_code])
          pet_cls_prob = '{}%'.format(int(pet_cls_prob * 100))
          pet_class = settings.CODE_CLASS_MAP.get(pet_cls_code)
          # 將預(yù)測(cè)結(jié)果組織成json
          res = {
          'code': 0,
          'data': {
          'pet_cls': pet_class,
          'probability': pet_cls_prob,
          'msg': '

          {} '>概率{}'.format(pet_class, pet_cls_prob),
          }
          }
          # 返回json數(shù)據(jù)
          return jsonify(res)


          if __name__ == '__main__':
          app.run(port=settings.WEB_PORT)

          后端腳本app.py很簡(jiǎn)單,主要就兩個(gè)方法。其中index方法會(huì)返回首頁的 HTML 源碼,是用戶在瀏覽器端的訪問入口;另一個(gè)方法pets_classify則提供了計(jì)算給定圖片類別的功能。

          前端文件index.html主要是提供了一個(gè)照片墻,用戶上傳圖片到照片墻,服務(wù)器就會(huì)計(jì)算圖片類別并返回相關(guān)數(shù)據(jù)。代碼如下:












          寵物識(shí)別Demo



          action="http://localhost:5000/api/v1/pets_classify/"
          list-type="picture-card"
          :on-preview="handlePictureCardPreview"
          :on-success="handleUploadSuccess"
          :on-remove="handleRemove">















          讓我們?cè)囋囆ЧJ紫?,運(yùn)行app.py腳本,啟動(dòng) Web 服務(wù),當(dāng)你看到如下輸出時(shí),說明服務(wù)啟動(dòng)成功了:

           * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
          * Serving Flask app "app" (lazy loading)
          * Environment: production
          WARNING: This is a development server. Do not use it in a production deployment.
          Use a production WSGI server instead.
          * Debug mode: off

          因?yàn)橹皇情_發(fā)環(huán)境,這么啟動(dòng)就可以了。如果是生產(chǎn)環(huán)境,請(qǐng)不要這么做,可以選擇使用 nginx + gunicorn +uWSGI + gevent 進(jìn)行部署。

          四、測(cè)試

          打開瀏覽器,輸入 http://localhost:5000?進(jìn)入 index 頁面。頁面長這個(gè)樣子:




          點(diǎn)擊網(wǎng)頁中的上傳框,我們可以選擇圖片上傳并識(shí)別:





          當(dāng)然了,這里不選擇我們數(shù)據(jù)集里的圖片更好,哪怕是測(cè)試集里的。你可以去網(wǎng)上下載、或者通過其他渠道獲取這四種動(dòng)物的圖片來測(cè)試,這里我只做演示,就不搞那么麻煩了,直接從數(shù)據(jù)集里隨便選幾張照片。我們可以繼續(xù)上傳圖片給服務(wù)器識(shí)別:



          OK,演示到此為止,如果有興趣的話可以自行測(cè)試。

          結(jié)語

          文章到此結(jié)束,如果您喜歡的話,給我點(diǎn)個(gè)贊唄 ~

          菜鳥一只,歡迎大佬們拍磚 ~

          作者:AaronJny
          地址:https://www.aaronjny.com/articles/2019/12/17/1576592367309.html

          源碼:https://github.com/AaronJny/pets_classifer


          本文僅做學(xué)術(shù)分享,如有侵權(quán),請(qǐng)聯(lián)系刪文。

          —THE END—
          瀏覽 83
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  又色网站免费看 | 免费在线观看的黄片 | 影音先锋每日最新av | 日本一级黄色电影 | 国产无码精品黄色电影 |