從軟體開發到 AI 領域工程師:模型訓練篇
前言
4 月熱播的韓劇《王國》,不知道大家有沒有看?我一集不落地看完了。王子元子出生時,正逢宮內殭屍作亂,元子也被咬了一口,但是由於大腦神經元尚未形成,寄生蟲無法控制神經元,所以醫女在做了簡單處理後,判斷不會影響大腦。這裡提到了人腦神經元,它也是 AI 神經網路的研究起源,具體展開講講。
人腦中總共有 860 億個神經元,其中大腦皮層有 160 億個神經元。大腦皮層的神經元數量決定了動物的智力水平,人的大腦皮層中神經元數量遠高於其他物種,所以人類比其他物種更聰明。大象的腦子總共有 2570 億個神經元,但是其中 98% 的神經元都存在於大象的小腦中,而大象的大腦皮層只有 56 億個神經元,無法與人類相比。大腦皮層中的神經元數量越大,能耗也越大。人腦每天消耗的能量占人體全部耗能的 25%,這也就是為什麼我們每天都要吃多餐,很容易餓的原因。人之所以能夠很快超越其他物種,主要是因為人類掌握了烹飪技術,能夠在短時間內攝入大量卡路里以支援大腦運轉,其他物種則將攝入的卡路里用於維護身體運轉,不得不犧牲大腦皮層的神經元數量。
之所以先談大腦神經元原理,也是為了引出本文的重點–現代 AI 技術。在正式進入 AI 技術前,我先講講軟體工程師這份工作,因為現在有很多軟體工程師準備轉入 AI 行業。
軟體工程師
我是軟體工程師出身,2004 年剛畢業時我寫的是 JSP 程式碼(一種將 Java 語言嵌入在 HTML 程式碼中的編寫方式),工作幾年後轉入分散式軟體技術,再後來進入大資料技術領域,最近的 4 年時間我一直在從事 AI 平臺研發工作。
軟體工程師的要求,我總體分為基礎編碼和系統架構兩方面,因此我對於軟體工程師的考察,特別是校招學生時,為了進一步考察他們的綜合能力,我每次都會自己準備面試題,這些題目包括了程式設計基本概念、演算法程式設計題、作業系統、資料庫程式設計、開原始碼閱讀、垃圾回收機制、系統架構描述等。
編碼的話題展開來可以講很久,發展歷史很悠久,我 15 歲學程式設計時用的是 Basic 語言,讀大學時學的是 C 語言,大學畢業參加工作後第一門用的語言是 Java,其中的各種故事和理解可以寫幾篇文章,這裡不展開談。
我覺得談到軟體工程師工作,避不開軟體架構設計。大眾談軟體架構,很多人會認為軟體架構就是一堆框架的組合,其實不對,軟體架構本身是對於軟體實體的組織形式的闡述,使用框架的意義是快速完成軟體架構設計,而不是取代軟體架構設計,兩者本質上不是一類事物,更像是設計圖紙和所使用的原材料。軟體架構就是通過對軟體生命週期的拆分,在符合業務架構的前提下,以達到軟體本身訪問增長目的的方式。這個增長需要軟體開發的增長,也需要軟體執行的增長,由此達到所支撐業務的增長。
市面上也確實有很多例如“分散式系統架構”、“微服務架構”等等跟隨著潮流的書籍,但是看完後只停留在會採用一些開源框架進行整體框架搭建,我說的是搭建,而不是設計。確實是搭建,你所擁有的能力就好像小孩子搭積木,只會採用固定討論,或者學得差點,連固定套路都沒學會,這樣對你的個人能力發展其實沒有多大好處,這也是為什麼很多程式設計師在完成了程式設計師 - 架構師的轉型後,沒過多久就轉為純管理,或者徹底離開了技術界,因為從來沒有大徹大悟理解系統架構。
之所以談了這麼多系統架構相關的工作理解,是因為我認為系統架構師系統化的思維,我們搞 AI 系統也是系統化的思維,從有較強程式設計能力的系統架構師轉 AI 技術,相對容易一些。
AI 工程師
為什麼要從軟體工程師轉行到 AI 產品研發?因為 AI 產品研發有更大的吸引力,因為它更難,難到我們並不確定什麼時候才能真正做出來,做出來真正能夠可複製的 AI 產品。表面上看它也是一個門檻—一個“可用”且“可複製”的 AI 技術,但因為難度足夠大,所以有挑戰性,必須不斷地改善技術,做全球範圍內還沒有做出來的技術。搞軟體開發時處理的一些問題可能是其他公司已經解決的,並非“人類”都還沒有解決的問題。
AI 的研究最早可以被追溯到亞里士多德的三段論,然後萊布尼茨創立了處理邏輯,布林在布林代數上的貢獻,弗雷德在近代邏輯上的貢獻,羅素在邏輯主義方面的貢獻,這些工作都是在資料邏輯上的。一般認為,現代 AI 技術討論,起源於 1956 年在達特茅斯學院召開的夏季研討會,而這門學科的源頭可能是 Alan Turing(阿蘭. 圖靈) 1948 年在英國國家物理實驗室(NPL)寫過的一份內部報告,這份報告中提到了肉體智慧和無肉體智慧,從某種意義上預示了後來符號派和統計派之爭,或是 Turing 在 1950 年在哲學雜誌《心》(Mind)上發表的文章“計算機與智慧”,反正都是 Turing。
可以這麼認為,現代 AI 是一系列通用目的技術的總稱。現代 AI 技術,主要指基於機器學習(Machine Learning,簡稱 ML)/ 深度學習(Deep Learning,簡稱 DL)的一系列方法和應用,這只是 AI 領域的一個分支,也是目前發展最快、應用最廣的分支。
機器學習 / 深度學習的原理可以這樣理解:建立一個模型,給一個輸入,通過模型的運算,得到一個輸出。可以用於解決一個簡單問題,例如識別圖片是不是狗,也可以用來解決複雜問題,例如下棋、開車、醫療診斷、交通治理等等,也可以理解為,模型就是一個函式 f(x),上述過程,可以表達為:f(一張圖片)= 狗 / 不是狗。
一個 AI 應用開發,大概分為三個階段:
- 第一階段,識別問題、構建模型、選擇模型。AI 的開發和培養一個小孩子類似,不同的孩子有不同的愛好和特長,同樣地,AI 也有很多模型 / 演算法,不同的模型 / 演算法適合解決不同的問題。所以,首先要識別你要解決的是個什麼問題,然後選擇一個合適的模型 / 演算法;
- 第二階段,訓練模型。和培養小孩子一樣,即使你發現小孩子有音樂天分,他也不可能天生就是鋼琴家,他需要專業的訓練。AI 開發也一樣,選定模型 / 演算法後,即使演算法再好,也不能馬上有效工作,你需要用大量的資料訓練這個模型,訓練過程中不斷優化引數,讓模型能夠更為有效地工作。這個階段 AI 模型的工作狀態,叫做“訓練”;
- 第三階段,模型部署。模型訓練結束後就可以部署了。比如一個人臉識別的模型,你可以把它部署在手機上,用於開機鑑權,也可以把它部署在園區閘機上,用於出入管理,還可以把它部署在銀行的客戶端上,用於業務鑑權,等等。如同一個孩子成長為鋼琴家後,既可以在音樂會上演奏,也可以在家庭聚會上表演。AI 模型部署之後的工作狀態,專業的說法,叫做“推理”。
通過上述 AI 開發過程的簡述,可以發現,演算法、資料和算力,是驅動 AI 發展的三大動力,三者缺一不可。
- 演算法相當於是基因。基因不好,再努力也白搭。如何識別問題,並根據問題選擇演算法,甚至開發新的演算法,是高階 AI 專家的核心競爭力;
- 資料相當於是學習材料。光基因好,沒有好的教材,也教不出大師。AI 的訓練,需要海量的、高質量的資料作為輸入。AlhpaGo 通過自己和自己下棋,下了幾千萬盤,人類一輩子最多也就下幾千盤。沒有這樣的訓練量,AlhpaGo 根本不可戰勝人類。自動駕駛,Google 已經搞了 10 年,訓練了幾十萬小時,遠遠超過一個專業賽車手的訓練量,但離真正的無人駕駛還差很遠。另外,資料的質量也很重要,如果你給 AI 輸入的資料是錯的,那麼訓練出來的 AI,也會做出錯誤的結果。簡單的說,如果你把貓的圖片當做狗的圖片去訓練 AI,那麼訓練出來的 AI,就一定會把貓當做狗。資料的重要性直接導致了中國湧現了大批以資料標註為生存手段的公司和個人;
- 一個小孩,光有天分和好的學習材料,自己如果不努力,不投入時間和精力好好學習,絕對不可能成為大師。同樣的道理,一個 AI 模型,演算法再好、資料再多,如果沒有足夠的算力,支撐它持續不斷的訓練,這個模型永遠也不能成為一個真正好用的模型。這就是為什麼英偉達崛起的原因,這家公司的 GPU 晶片提供了最為適配於人腦神經網路的計算算力,現在國內工業界也有了類似的公司產品 - 華為的達芬奇晶片。
訓練 AI 應用模型
動手實踐前
接下來,我們通過對一個 AI 應用模型的訓練和推理過程介紹,開始動手實踐。
訓練模型需要算力,對於算力的獲取,訓練和推理可以根據自己的業務需求,選擇使用公有云或自己購買帶算力晶片的伺服器,本文案我選擇的是某花廠的 AI 開發平臺,因為近期他們剛推出一個免費算力的推廣活動,可以省下一筆訓練費用。為了便於除錯,我首先在自己的 CPU 個人電腦上編寫程式碼、訓練模型,這樣做的缺點是模型收斂的時間長了一些。
疫情期間,對於民眾來說,佩戴口罩是最有效防止被傳染新冠病毒的方式,保護自己的同時也保護他人。所以本文的案例是佩戴口罩的識別模型訓練。識別演算法離不開目標檢測。目標檢測(Object Detection)的任務是找出影象中所有感興趣的目標(物體),確定它們的位置和大小。由於各類物體有不同的形狀、大小和數量,加上物體間還會相互遮擋, 因此目標檢測一直都是機器視覺領域中最具挑戰性的難題之一。
基於深度學習的人臉檢測演算法,多數都是基於深度學習目標檢測演算法進行的改進,或者說是把通用的目標檢測模型,為適應人臉檢測任務而進行的特定配置。而眾多的目標檢測模型(Faster RCNN、SSD、YOLO)中,人臉檢測演算法最常用的是 SSD 演算法(Single Shot MultiBox Detector,“Single Shot”指的是單目標檢測,“MultiBox”中的“Box”就像是我們平時拍攝時用到的取景框,只關注框內的畫面,遮蔽框外的內容。建立“Multi”個 "Box",將每個 "Box" 的單目標檢測結果彙總起來就是多目標檢測。換句話說,SSD 將影象切分為 N 片,並對每片進行獨立的單目標檢測,最後彙總每片的檢測結果。),其他如 SSH 模型、S3FD 模型、RetinaFace 演算法,都是受 SSD 演算法的啟發,或者基於 SSD 進行的任務定製化改進, 例如將定位層提到更靠前的位置,Anchor 大小調整、Anchor 標籤分配規則的調整,在 SSD 基礎上加入 FPN 等。本文訓練口罩識別模型採用了 YOLO。
目標檢測過程都可以分解為兩個獨立的操作:
- 定位(location): 用一個矩形(bounding box)來框定物體,bounding box 一般由 4 個整陣列成,分別表示矩形左上角和右下角的 x 和 y 座標,或矩形的左上角座標以及矩形的長和高。
- 分類(classification): 識別 bounding box 中的(最大的)物體。
我選擇採用 keras-yolo3-Mobilenet 方案,開源專案地址:
https://github.com/Adamdad/keras-YOLOv3-mobilenet。
MobileNet 的創新亮點是 Depthwise Separable Convolution(深度可分離卷積),與 VGG16 相比,在很小的精度損失情況下,將運算量減小了 30 倍。YOLOv3 的創新亮點是 DarkNet-53、Prediction Across Scales、多標籤多分類的邏輯迴歸層。
基於開源資料集的實驗結果:
動手訓練模型
訓練模型自然需要訓練資料集和測試資料集,大家可以在這裡下載:
https://modelarts-labs-bj4.obs.cn-north-4.myhuaweicloud.com/casezoo/maskdetect/datasets/maskdetectdatasets.zip
Yolo v3-MobileNet 目標檢測工程的目錄結構如下:
|--model_data |--voc_classes.txt |--yolo_anchors.txt |--yolo3 |--model.py |--model_Mobilenet.py |--utiles.py |--convert.py |--kmeans.py |--train.py |--train_Mobilenet.py |--train_bottleneck.py |--voc_annotation.py |--yolo.py |--yolo_Mobilenet.py |--yolo_video.py
開源專案的好處是已經幫你封裝了流程,例如涉及的 Yolo 程式碼不用修改,本次訓練過程需要修改的程式碼主要是以下三個:
1.train_Mobilenet.py:模型訓練程式碼;
2.yolo/model_Mobilenet.py:基於 mobilenet 的 yolo 的模型程式碼,如果相對模型程式碼仔細研究的人,可以研究這個程式碼;
3.yolo_Mobilenet.py:模型推理程式碼。
接下來具體介紹我們需要修改的程式碼,按照功能分為資料類、模型類、視覺化類、遷移上雲準備類。
•資料類:
仿照 modeldata/vocclasses.txt 寫一個是否有戴口罩的類別的 txt,內容只有 yes_mask、no_mask 兩個字元。
如果你下載我給出的資料集,你會發現,口罩資料集中給出的 xml 標註格式是 VOC 的標準的,仿照 convert.py 和 voc_annotation.py 寫一個數據轉換檔案,程式碼如下所示:
import xml.etree.ElementTree as ET import os def convert_annotation(classes, label_path): in_file = open(label_path) tree=ET.parse(in_file) root = tree.getroot() output_list = [] for obj in root.iter('object'): difficult = obj.find('difficult').text cls = obj.find('name').text if cls not in classes or int(difficult)==1: continue cls_id = classes.index(cls) xmlbox = obj.find('bndbox') b = (int(xmlbox.find('xmin').text), int(xmlbox.find('ymin').text), int(xmlbox.find('xmax').text), int(xmlbox.find('ymax').text)) output_list.append(" " + ",".join([str(a) for a in b]) + ',' + str(cls_id)) return (' '.join(output_list)) def mask_convert(data_path, classes): img_list = [] for i in list(os.listdir(data_path)): if i.split('.')[1] == 'jpg': img_list.append(i.split('.')[0]) output_list = [] for image_id in img_list: img_path = (data_path + '/%s.jpg' % (image_id)) label_path = (data_path + '/%s.xml' % (image_id)) annotation = convert_annotation(classes, label_path) output_list.append(img_path + annotation) return output_list
•模型類:
訓練過程中會有一個 tensor 對不上的錯誤,需要修改 model_data/model.py 這個程式碼中的 140-141 行,如下所示:
box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[..., ::-1], K.dtype(feats)) box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[..., ::-1], K.dtype(feats))
•視覺化類:
為了直觀判斷模型效果,增加了一個在圖片上直接標註的視覺化程式碼,也就是在圖片上列印輸出結果(yes_mask 或 no_mask),程式碼如下所示:
#!/usr/bin/env python # coding: utf- img_path = "D:/Code/mask_detection/data/test" save_path = "D:/Code/mask_detection/data/test_result/" import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.patches as patches import matplotlib.image as mpimg import random import json # 推理輸出路徑 with open('D:/Code/mask_detection/keras-YOLOv3-mobilenet-master/annotation_YOLOv3.json') as json_file: data = json.load(json_file) imgs = list(data.keys()) def parse_json(json): bbox = [] for item in json['annotations']: name = item['label'] xmin = item['x'] ymin = item['y'] xmax = item['x']+item['width'] ymax = item['y']+item['height'] bbox_i = [name, xmin, ymin, xmax, ymax] bbox.append(bbox_i) return bbox def visualize_bbox(image, bbox, name): fig, ax = plt.subplots() plt.imshow(image) colors = dict()#指定標註某個物件的邊框的顏色 for bbox_i in bbox: cls_name = bbox_i[0] #得到 object 的 name if cls_name not in colors: colors[cls_name] = (random.random(), random.random(), random.random()) #隨機生成標註 name 為 cls_name 的 object 的邊框顏色 xmin = bbox_i[1] ymin = bbox_i[2] xmax = bbox_i[3] ymax = bbox_i[4] #指明對應位置和大小的邊框 rect = patches.Rectangle(xy=(xmin, ymin), width=xmax-xmin, height=ymax-ymin, edgecolor=colors[cls_name],facecolor='None',linewidth=3.5) plt.text(xmin, ymin-2, '{:s}'.format(cls_name), bbox=dict(facecolor=colors[cls_name], alpha=0.5)) ax.add_patch(rect) plt.axis('off') plt.savefig(save_path+'{}_gt.png'.format(name)) #將該圖片儲存下來 plt.close() for item in imgs: img = mpimg.imread(img_path+item) bbox = parse_json(data[item]) visualize_bbox(img, bbox, item.split('.')[0])
•上雲準備類:
開原始碼寫的比較隨意,直接就是在訓練程式碼 trian_Mobilenet.py 程式碼中一開頭指定所有的引數。華為雲中訓練作業是需要指定 OBS 的輸入路徑和輸出路徑的,最好使用 argparse 的形式將路徑引數傳進去。其他引數可以按照自己需求做增加,修改樣例如下:
import argparse parser = argparse.ArgumentParser(description="training a maskmodel in modelarts") # 訓練輸出路徑 parser.add_argument("--train_url", default='logs/maskMobilenet/003_Mobilenet_finetune/', type=str) # 資料輸入路徑 parser.add_argument("--data_url", default="D:/code/mask_detection/data/MASK_MERGE/", type=str) # GPU 數量 parser.add_argument("--num_gpus", default=0, type=int) args = parser.parse_args()
開原始碼中,資料處理的部分是將 xml 轉換成 yolo 讀的 txt 文件,這樣導致資料輸入需要有一個寫入到 txt 檔案,然後訓練工程讀取這個 txt 檔案和圖片的過程。上雲後,這種流程不太方便,需要將資料處理,資料轉換和訓練程式碼打通。這裡我使用快取將資料直接傳到訓練程式碼中,這樣改起來比較方便,但是當資料量較大的時候並不科學,有興趣的人可以自己修改。
遷移公有云
我使用某廠商公有云的 AI 訓練平臺,用的是 OBS 桶上傳已經除錯好的程式碼(建議大家體驗 Notebook 方式,線上程式設計、編譯),如下圖所示:
接著啟動 Notebook,不過我沒有用 jupyter 方式寫程式碼,而是採用同步 OBS 桶的資源,通過 Notebook 啟動一個 GPU 映象:
建立一個 Notebook 環境:
確認計算資源規格:
建立 Notebook 環境成功:
從 OBS 桶同步相關檔案:
接下來進入該 Notebook 的終端環境,執行以下程式碼,啟動訓練任務:
訓練過程輸出片段如下所示:
2020-04-07 18:58:14.497319: I tensorflow/stream_executor/dso_loader.cc:152] successfully opened CUDA library libcublas.so.10.0 locally 7/7 [==============================] - 17s 2s/step - loss: 4226.4421 - val_loss: 22123.3750 Epoch 2/50 7/7 [==============================] - 6s 855ms/step - loss: 1083.1558 - val_loss: 1734.1427 Epoch 3/50 7/7 [==============================] - 6s 864ms/step - loss: 521.8567 - val_loss: 455.0971 Epoch 4/50 7/7 [==============================] - 6s 851ms/step - loss: 322.8907 - val_loss: 193.3107 Epoch 5/50 7/7 [==============================] - 6s 841ms/step - loss: 227.7257 - val_loss: 150.8902 Epoch 6/50 7/7 [==============================] - 6s 851ms/step - loss: 179.0605 - val_loss: 154.9351 Epoch 7/50 7/7 [==============================] - 6s 868ms/step - loss: 150.4297 - val_loss: 147.3101 Epoch 8/50 7/7 [==============================] - 8s 1s/step - loss: 129.5681 - val_loss: 144.8283
模型生成後,建立一個 python 指令碼,程式碼如下,實現了模型檔案拷貝到 OBS 桶:
from modelarts.session import Session session = Session() session.upload_data(bucket_path="/mask-detection-modelarts-test/run/log/", path="/home/ma-user/work/log/trained_weights_final.h5")
執行推理指令碼,我把推測結果列印在了測試圖片上,如下圖所示,識別出了口罩:
後記
AI 技術的興起,已經帶動了科技行業的革命,而每一次業界的革命,都會讓一些公司落寞而讓另一些公司崛起,程式設計師也一樣,每一次技術換代也都會讓一些程式設計師沒落而讓另一些程式設計師崛起。抓住目前正在流行的 AI 技術趨勢,使用雲端的免費計算資源,上手學習並實踐 AI 技術,會是相當一部分軟體工程師、資料科學家的選擇。此外,由於在影象識別、文字識別、語音識別等技術領域,演算法的精度已經給有大幅度的提升,在很多場景下已經達到可商用級別,也進一步讓自動機器學習技術(模型的自動設計和訓練)的發展成為可能。因此,在上述幾個技術領域的很多應用場景下,公有云已經可以做到根據使用者自定義資料進行 AI 模型的自動訓練。
點選關注,第一時間瞭解華為雲新鮮技術~