眾所周知,人工智能的測試具有相當大的難度,不論是以前在互聯(lián)網(wǎng)就經(jīng)??吹降膬?nèi)容推薦系統(tǒng),還是金融領(lǐng)域常用的信息分類系統(tǒng)。測試介入的都不多。那么如何測試人工智能相關(guān)的產(chǎn)品。
我們最常做的測試其實就是直接看模型效果,效果不好就慢慢調(diào)。 為了改變這種現(xiàn)狀,我們希望測試人員也能做一些事情,參與到人工智能服務的質(zhì)量保證當中來。在日常工作中總結(jié)了一些經(jīng)驗跟大家討論一下。
什么是人工智能
簡而言之,現(xiàn)階段的人工智能就是:人工智能=大數(shù)據(jù)+機器學習。
現(xiàn)階段的人工智能是使用機器學習算法在大量的歷史數(shù)據(jù)下進行訓練,從歷史數(shù)據(jù)中找到一定的規(guī)律并對未來做出的預測行為。舉例說明,就好比銀行做過的反欺詐項目。
在銀行里有一群專家,他們的工作就是根據(jù)經(jīng)驗向系統(tǒng)中輸入一些規(guī)則。例如某一張卡在一個城市有了一筆交易,之后1小時內(nèi)在另一個城市又有了一筆交易。這些專家根據(jù)以前的經(jīng)驗判斷這種情況是有盜刷的風險的。
他們在系統(tǒng)中輸入了幾千條這樣的規(guī)則,組成了一個專家系統(tǒng)。 這個專家系統(tǒng)是建立在人類對過往的數(shù)據(jù)所總結(jié)出的經(jīng)驗下建立的。 后來我們引入了機器學習算法,對過往所有的歷史數(shù)據(jù)進行訓練,最后在25億個特征中抽取出8000萬條有效特征。
我們把這些特征就暫且當成是專家系統(tǒng)中的規(guī)則。8000萬對不到1萬,效果是可以預想的。當時對第一版模型上線后的數(shù)據(jù)做統(tǒng)計,反欺詐效果提升了7倍, 這就是二分類算法典型的業(yè)務場景。
為什么叫機器學習呢,因為它給人一種感覺,機器能像人類一樣從過去的數(shù)據(jù)中學習到經(jīng)驗,只不過機器的能力更強。 如果想再稍微深究一下機器學習訓練出來的模型到底是什么,那大家可以暫且理解為一個二分類的模型主要是就是一個key,value的數(shù)據(jù)庫。
key就是在數(shù)據(jù)中抽取出來的特征,value就是這個特征的權(quán)重。 當我們想要預測一個用戶的行為的時候,就會從用戶的數(shù)據(jù)中提取特征并在模型中查找對應的權(quán)重。 最后根據(jù)這些特征的權(quán)重算出一個分,也可以說是一個概率。 如果大家看過最強大腦這個節(jié)目的話。
應該記得第一次人機大戰(zhàn)項目是人臉識別,第三回合的時候機器給出了兩個答案。因為當時做人臉識別項目的志愿者中有一對雙胞胎。所以機器計算出的這兩個人的概率只差了0.1%,最后為了謹慎起見機器輸出兩個答案出來。以至于吳恩達先生也很糾結(jié)到底該選哪一個答案。
再之后的人機對戰(zhàn)項目里我們也能看到雖然小度一路高歌,毫無敗績。但是其中是有選錯過幾次的。所以大家可以看到我們得到的答案其實是一個概率,是一個根據(jù)以往的數(shù)據(jù)進行總結(jié)并作出預測的一個行為。并不是100%準確的。即便是阿爾法狗當初也是輸過一局的。所以其實我們測試的痛點也就來了,你怎么測試一個概率呢。
這個圖就是一個人工智能服務的略縮圖。 在歷史數(shù)據(jù)上訓練出模型,并發(fā)布一個預測服務,這個預測服務可能就是一個http的接口。 然后新的數(shù)據(jù)過來以后,根據(jù)模型算出一個預測值。經(jīng)過剛才的說明,我們看到數(shù)據(jù)是人工智能的根本。擁有的數(shù)據(jù)越多,越豐富,越真實,那么訓練出的模型效果越好。
測試思路
·數(shù)據(jù)測試
·分層測試
·訓練集與測試集對比
數(shù)據(jù)測試
根據(jù)我們之前對人工智能的定義,我們發(fā)現(xiàn)數(shù)據(jù)是人工智能的根據(jù)。 保證數(shù)據(jù)的正確定是非常必要的。 而且?guī)缀跛械臋C器學習算法對數(shù)據(jù)的容錯能力都很強,即便數(shù)據(jù)稍有偏差,它們也能通過一次一次的迭代和對比來減少誤差。 所以即便我們的數(shù)據(jù)有一點問題最后得出的模型效果可能還不差, 但是這個時候我們不能認為模型就沒問題了。 因為很可能在某些特定場景下就會出現(xiàn)雪崩效應。
再舉個例子。當初阿爾法狗與李世石一戰(zhàn)成名。如果說前三盤的結(jié)果令各路專家大跌眼鏡的話。那第四盤可能是讓所有人都大跌眼鏡了。阿爾法狗連出昏招,幾乎是將這一局拱手相讓。那阿爾法狗出bug了?DeepMind團隊說,這是一個系統(tǒng)問題。那我們來看看這個系統(tǒng)到底有什么問題。根據(jù)當時公布出來的數(shù)據(jù)我們發(fā)現(xiàn)阿爾法狗的養(yǎng)成方法是這樣的。
阿爾法狗如何養(yǎng)成
1.整理過去人類對弈的80多萬盤棋局
2.拿1的棋譜,訓練一只狗狗,能夠預測人類下一步會下在哪里
3.拿2得到的狗狗,每天自己和自己下100萬盤棋
4.拿1和3的棋譜,再訓練一只狗狗,這就是阿爾法狗。
阿爾法狗是基于1億+盤機器棋局和80萬人類棋局訓練出來的狗狗。問題出在哪?我們看到,訓練阿爾法狗所用的棋譜,只有80萬是人類棋局,總數(shù)上億的棋局是機器對弈的。它下的每一步,都是將局面導向歷史上(也就是80萬人類棋局和1億自己對弈的棋局)勝率最大的局面。
問題就恰恰出在這里,80萬和1億。相差甚多。那么阿爾法狗選擇的所謂勝率最大,一定是贏自己的概率最大,而不是贏人類的概率最大。這樣的標準在順豐棋尚且不容易出問題,一旦遇到逆風局,它的選擇就變成了,選擇對手犯錯概率最大的棋,而這個對手恰恰就是它自己。
這就是為什么當初阿爾法狗在逆風局中下出一些匪夷所思的棋。當然這些都只是行業(yè)內(nèi)的人的猜測。但從這個例子就可以看到數(shù)據(jù)的重要性。數(shù)據(jù)出現(xiàn)偏差會直接導致不可估計的后果。
所以我們總結(jié)出的第一個突破口就是數(shù)據(jù)測試。保證我們用來建模的數(shù)據(jù)是正確的。在這里的數(shù)據(jù)測試會跟我們以往的測試有些不一樣。
首先是數(shù)據(jù)規(guī)模的增長。數(shù)據(jù)大了里面的有些數(shù)據(jù)可能會發(fā)生一些很詭異的事情。然后是存儲介質(zhì)和技術(shù)棧的變化,我們用來訓練模型的數(shù)據(jù)都會存放在hadoop集群上,所以一般都會用spark或者Hbase來編寫測試腳本。最后是驗證方式的變化,之前我們測試的時候都是精確的測試每條數(shù)據(jù)的正確性。 但是現(xiàn)在我們需要根據(jù)業(yè)務設(shè)定每一個字段的規(guī)則。然后掃描每一行數(shù)據(jù),如果這條數(shù)據(jù)超出了這個字段的規(guī)定范圍,會發(fā)出報警。 假如用戶年齡這個字段,正常值可能是0~100歲。如果突然出現(xiàn)個300歲,那么這條數(shù)據(jù)肯定是有問題的。我們之前曾經(jīng)碰見過這樣的數(shù)據(jù)。本來這條數(shù)據(jù)應該是30歲的,但卻多了個0。
針對這方面的測試,一般都是使用spark或者Hbase這種大數(shù)據(jù)處理框架把任務提交到集群上去掃描每一張表。
分層測試
雖然我們能測試用來做訓練的數(shù)據(jù)是正確的。但是一個人工智能系統(tǒng)是很復雜的, 我們只是保證了作為源頭的數(shù)據(jù)貌似是不夠的。以前我們測試的時候都知道把系統(tǒng)拆解開做分層測試,所以我們的第二個突破口就是基于這個思路。我們先來了解一下,一個模型的誕生都經(jīng)理了哪些步驟。
模型誕生的步驟:
·數(shù)據(jù)引入
·數(shù)據(jù)處理(清洗,拆分,拼接等)
·特征工程
·模型訓練
·模型上線
數(shù)據(jù)引入:將歷史數(shù)據(jù)引入到系統(tǒng)中,并做第一步的預處理。例如處理一些明顯的異常的行為。
數(shù)據(jù)處理: 其中又包含很多種操作。 例如對數(shù)據(jù)進行清洗,拆分。 把兩張表進行拼接等。
特征工程:我們從數(shù)據(jù)中按一定規(guī)律提取一些特征出來。之后需要將提取的樣本表傳遞給機器學習算法進行訓練。
可以看到我們把數(shù)據(jù)引入到系統(tǒng)后,先是用SQL算子對數(shù)據(jù)做了拼接,然后清洗一些無效數(shù)據(jù)。 再把數(shù)據(jù)拆分為訓練集和測試集。分別對兩個數(shù)據(jù)集做特征抽取。之后訓練集傳遞給邏輯回歸這個機器學習算法進行訓練。訓練之后使用測試集來測試一下模型的效果。大家看到這個圖了,這是創(chuàng)建一個模型比較常見的流程。
圖中的邏輯歸回就是一種機器學習算法,也是一種最簡單的二分類算法,其他的算法諸如 GBDT,SVM,DNN等算法的模型都是這個流程。我們可以看到算法上面的流程。全部與機器學習無關(guān)。他們都屬于大數(shù)據(jù)處理范疇。而且一個成型的系統(tǒng)在每一個模塊都會提供一些固定的接口。
例如我們公司在特征抽取算法上就提供了近百個特征抽取的接口,可以根據(jù)不同的情況使用不同的方式提取數(shù)據(jù)中的特征。 數(shù)據(jù)拆分也有很多種不同的拆分方法,按隨機拆分,分層拆分,規(guī)則拆分。
每個子模塊都會提供一些接口供上層調(diào)用。 所以既然提到接口層面的東西了,大家應該都知道怎么測了吧。 只不過有些接口并不是http或者RPC協(xié)議的。 有時候需要我們在產(chǎn)品的repo里寫測試用例。
訓練集與測試集對比


這是我們的第三種測試思路。 我們剛才一直用來舉例的分類算法是一種監(jiān)督學習。 什么是監(jiān)督學習呢,就是我們的歷史數(shù)據(jù)中是有答案的。還拿剛才的反欺詐的例子說,就是我們的數(shù)據(jù)中都有一個字段標明了這條數(shù)據(jù)是否是欺詐場景。 所以我們完全可以把歷史數(shù)據(jù)拆分為訓練集和測試集。將測試集輸入到模型中以評價模型預測出的結(jié)果的正確率如何。所以每次版本迭代都使用同樣的數(shù)據(jù),同樣的參數(shù)配置。 統(tǒng)計模型效果并進行對比。當然這種測試方式是一種模糊的方式。就如我再剛開始說的一樣,這種方式無法判斷問題出在哪里。是bug,還是參數(shù)設(shè)置錯了?我們無法判斷。
常見的測試場景
1.自學習


幾乎所有的人工智能服務都必須要支持自學習場景。就像阿爾法狗一樣,它輸了一局,就會從輸?shù)倪@一局中學習到經(jīng)驗,以后他就不會那么下了,這也是機器學習恐怖的地方,它會變的越來越無懈可擊,以前人類還能贏上一局,但是未來可能人類再也贏不了阿爾法狗了。 做法就是我們的數(shù)據(jù)每天都是在更新的,用戶行為也是一直在變化的。所以我們的模型要有從最新的數(shù)據(jù)中進行學習的能力。
上面是常見的自學習場景流程圖。假如我們用歷史上n天的數(shù)據(jù)訓練出一個模型并發(fā)布成了一個預測的服務。 那么到了隔天的時候。我們拋棄之前第一天的數(shù)據(jù),使用第二天到第n+1天的數(shù)據(jù)重新訓練一個模型并代替之前的模型發(fā)布一個預測服務。這樣不停的循環(huán),每一天都收集到最新的數(shù)據(jù)參與模型訓練。 這時候大家應該明白該測試什么了。每天收集到的新數(shù)據(jù),就是測試重點。就是我們剛才說的第一種測試思路,使用spark,Hbase這些技術(shù),根據(jù)業(yè)務指定規(guī)則,掃描這些數(shù)據(jù)。一旦有異常就要報警。
2.預測服務
下面一個場景是預測服務的。預測服務的架構(gòu)一般都滿復雜的,為了實現(xiàn)高可用,負載均衡等目的,所以一般都是標準的服務發(fā)現(xiàn)架構(gòu)。以etcd這種分布式存儲機制為載體。 所有的預測服務分別以自注冊的方式來提供服務。

上面的一個圖是一個比較流行的預測服務的架構(gòu)。當然我做了相應的簡化,隱去了一些細節(jié)。所有的部署任務由master寫入ETCD。 所有agent以自注冊的方式將自己的信息寫入ETCD以接受master的管理并執(zhí)行部署任務。 而router也同樣讀取etcd獲取所有agent提供的預測服務的信息并負責負載均衡。 有些公司為了做高可用和彈性伸縮甚至將agent納入了kubernetes的HPA中進行管理。由此我們需要測試這套機制能實現(xiàn)他該有的功能。例如:
·router會按規(guī)則把壓力分發(fā)到各個agent上。
·把某個agent的預測服務被kill掉后,router會自動切換。
·預估服務掛掉,agent會自動感知并重新拉起服務。
·agent被kill掉后,也會被自動拉起。
·如果做了彈性伸縮,需要將預測服務壓到臨界點后觀察系統(tǒng)是否做了擴容等等。
性能測試
我們要接觸的性能測試跟互聯(lián)網(wǎng)的不太一樣。我們知道預測服務仍然還是訪問密集型業(yè)務。但是模型調(diào)研的過程是屬于計算密集型業(yè)務。我們要模擬的情況不再是高并發(fā)。而是不同的數(shù)據(jù)規(guī)模,數(shù)據(jù)分布和數(shù)據(jù)類型。我們?nèi)粘5男阅軠y試都是需要在各種不同的數(shù)據(jù)下跑各種不同的算子和參數(shù)。所以我們首先需要一種造數(shù)機制,能幫助我們按需求生成大規(guī)模的數(shù)據(jù)。我們選擇的是spark,利用分布式計算在hadoop集群上生成大量的數(shù)據(jù)。
原理也很簡單,接觸過spark的同學肯定都知道在spark中生成一個RDD有兩種方式, 一種是從文件中讀取,另一種是從內(nèi)存中的一個list種解析。第一種方式肯定不是我們想要的, 所以從內(nèi)存中的list解析就是我們選擇的方式。假如我們想生成一個10億行的數(shù)據(jù)。就可以先使用python 的xrange造一個生成器以防止內(nèi)存被撐爆。然后用這個生成器初始化一個有著10億行的空的RDD,定義并操作RDD的每一行去生成我們想要的數(shù)據(jù),然后設(shè)置RDD的分片以及消耗的container,內(nèi)存,cpu等參數(shù)。提交到集群上利用集群龐大的計算資源幫助我們在段時間內(nèi)生成我們需要的數(shù)據(jù)。 前兩天我再一個3個節(jié)點的集群上造過一個1.5T的數(shù)據(jù),大概用了5個小時。這樣一開始的時候我們是寫spark腳本來完成這些事。后來需求越來越多,我們發(fā)現(xiàn)可以造數(shù)做成一個工具。把表和字段都提取到配置文件中進行定義。就這樣我們成立了shannon這個項目。慢慢的從造數(shù)腳本到造數(shù)工具再到造數(shù)平臺。
它的架構(gòu)特別簡單,就是對原生spark的應用,這里我就不展示spark的架構(gòu)是什么樣了。就貼一下造數(shù)工具的設(shè)計圖吧。

簡單來說shannon分了3層。最底層是基本數(shù)據(jù)類型層。負責字段實例化,定義并實現(xiàn)了shannon支持的所有數(shù)據(jù)類型。例如,隨機,枚舉,主鍵,unique key,控制分布,大小,范圍等等。
測試環(huán)境管理
常見的測試場景我們基本上都說完了。 我們再說一說測試環(huán)境管理的問題。 為了能夠保證研發(fā)和測試效率,一個能夠支撐大規(guī)模測試環(huán)境的基礎(chǔ)設(shè)施是十分必要的。為什么這么說呢?
首先但凡是涉及到機器學習的業(yè)務,運行時間都非常慢。 有時候做測試的時候跑一個模型要幾個小時甚至一天都是有的。也就是說,我們運行測試的成本比較高。如果在運行測試的途中環(huán)境出了什么問題那么損失還是很大的。多人共用一套環(huán)境難免會有互相踩踏的情況,例如一個RD在測試自己的模塊,另一個人上來把服務重啟了。這時候我們心里一般就是一萬頭某種動物飄過。所以我們一般希望每個人都能擁有一套獨立的環(huán)境甚至一個人多套環(huán)境。這就增加了測試環(huán)境的數(shù)量。尤其是團隊越來越大的時候,測試環(huán)境的數(shù)量已經(jīng)到達了一個恐怖的量級。
其次如果各位所在的公司也像我們一樣做TO B的業(yè)務,那么我們的測試環(huán)境就需要多版本管理,要有能力隨時快速的搭建起特定版本的產(chǎn)品環(huán)境供開發(fā),產(chǎn)品,測試,以及技術(shù)支持人員使用。所以這無疑又增加了環(huán)境管理設(shè)置的復雜度。
再有就是隨著環(huán)境數(shù)量的擴張,我們的環(huán)境從單節(jié)點走向集群,這時候我們對環(huán)境調(diào)度能力的要求會比較高,例如我們要對環(huán)境的資源進行計算和限制,保證最大化利用資源的同時不會撐爆系統(tǒng)。例如我們要保證系統(tǒng)有足夠的冗余,在某些環(huán)境出現(xiàn)故障的時候能夠自動檢測出來并在冗余節(jié)點進行恢復。例如我們需要能夠?qū)崿F(xiàn)多租戶管理,執(zhí)行資源管控,限制超售行為. 例如我們希望系統(tǒng)有一定能力的無人值守運維能力等等。
所以我們經(jīng)過一段時間的討論和實驗,引入了k8s+docker來完成這個目標。docker的優(yōu)勢大家應該都知道,快速,標準化,隔離性,可遷移性都不錯。 通過鏡像我們可以迅速的將測試環(huán)境的數(shù)量提升一個量級,鏡像的版本管理正適合TO B業(yè)務的多版本管理。 之所以選擇k8s,是因為k8s相較于swarm和mesos 都擁有著更強大的功能和更簡單的部署方式。剛才說的預測服務需要部署很多個agent,使用k8s的話只需要設(shè)置一下replica set的數(shù)量,k8s就會自動幫我們維護好這個數(shù)量的實例了,很方便 。k8s的調(diào)度機制能很輕松的滿足我們剛才說的對于災難恢復,冗余,多租戶,高可用,負載均衡,資源管理等要求。所以我們當初懷揣著對google莫名的憧憬走上了k8s的踩坑之旅。

首先說一下我們都用容器做什么。主要三大類,第一種是諸如testlink,jenkins這種基礎(chǔ)服務。第二種是產(chǎn)品的測試環(huán)境,這是占比最多的。然后就是我們的測試執(zhí)行機器了。例如UI自動化,我們采取的是分別將selenium hub和node docker化的做法。如下圖:

當UI自動化的case增多的時候,分布式運行往往是最好的解決方案。 目前我們通過這種方式容器化了20個瀏覽器進行并發(fā)測試。這些鏡像都有官方的版本,使用起來還是蠻方面的。
然后說一下比較關(guān)鍵的網(wǎng)絡(luò)解決方案,我們從單機到集群,中途歷經(jīng)了集中網(wǎng)絡(luò)模型的變化。從一開始的端口映射,到利用路由規(guī)則給容器分配真實的ip,再到給每個容器在DHCP和DNS服務器上注冊和續(xù)租。到最后我們演進出了下面這個k8s的網(wǎng)絡(luò)模型。
我們知道每個docker宿主機都會自己維護一個私有網(wǎng)絡(luò)。如果想讓容器跨主機通訊或者外部訪問容器。一般就是通過三種方式: 端口映射,路由規(guī)則以及overlay網(wǎng)絡(luò)。我們選擇在k8s中引入的overlay網(wǎng)絡(luò)是weave,以解決夸主機通信問題。安裝kube-dns實現(xiàn)服務發(fā)現(xiàn)。之后為了能讓外部訪問容器服務, 使用了k8s提供的ingress機制來實現(xiàn)。這個ingress網(wǎng)絡(luò)其實就是在集群中啟動一個容器,這個容器既能訪問容器網(wǎng)絡(luò)的同是還監(jiān)聽了宿主機的80端口。容器里是一個nginx,它會負責幫忙轉(zhuǎn)發(fā)請求。nginx負責轉(zhuǎn)發(fā)的有servicename和path,這里我們是無法使用路徑進行轉(zhuǎn)發(fā)的。所以我們在公司內(nèi)部的DNS上做了泛域名解析。所有testenv為后綴的域名都會解析成集群的master節(jié)點的ip。這樣我們的請求就能命中nginx中固定的servicename并做轉(zhuǎn)發(fā)了。通過這種機制我們就可以很方面的訪問容器提供的服務。當然ingress的缺點是暫時還無法做4層轉(zhuǎn)發(fā)。如果要訪問4層協(xié)議的服務暫時還是只能暴露node port。
我們這個測試環(huán)境的管理平臺主要的架構(gòu)是這樣的:
集群中所有的鏡像都過公司內(nèi)部搭建的鏡像倉庫進行共享,我們在集群之上安裝了各種服務來滿足測試環(huán)境的需要。例如使用NFS做數(shù)據(jù)持久化,Heapster+Grafana+InfluxDB做性能監(jiān)控,kube-DNS做服務發(fā)現(xiàn),dashboard提供web管理界面,weave做集群網(wǎng)絡(luò),ingress做服務的轉(zhuǎn)發(fā)。并且我們在這個整體上針對k8s的APIserver做了一層cli的封裝。我們嘗試過腳本管理,web服務管理,但是發(fā)現(xiàn)大家對這些方式的接受度都不高。我們面對的大多數(shù)都是一幫做夢都在寫代碼的人,所以我們換做提供一個cli的方式可以讓使用者更靈活來定制自己需要的服務。通過這種形式,我們在公司內(nèi)部搭建了一個可以提供測試資源的私有云,配合jenkins我們可以很方便的一鍵部署我們需要的環(huán)境并執(zhí)行UT,接口,UI自動化測試等等,并提供一個詳細的測試報告。下面是我們的部署一個環(huán)境后所提供的測試報告。