我們是如何測試人工智慧的(一)基礎效果篇(內含大模型的測試內容)

孙高飞發表於2023-12-19

前言

這個系列算是科普文吧,尤其這第一篇可能會比較長,因為我這 8 年裡一直在 AI 領域裡做測試,涉及到的場景有些多, 我希望能儘量把我經歷過的東西都介紹一下,算是給大家科普一下我們這些在 AI 領域內做測試的人,每天都在做什麼事情。 當然 AI 領域很龐雜,我涉及到的可能也僅僅是一小部分,這篇帖子算是拋磚引玉,歡迎大家一起來討論。

我打算先簡單講解一下人工智慧的原理,畢竟後面要圍繞這些去做測試, 並且包括了在測試大模型以及其他一些場景的時候,需要自己構建模型來輔助測試, 所以我覺得至少先講明白遷移學習的原理,這樣我們後面做模型微調定製自己的模型的時候才有據可依。

PS:由於是科普性質的,所以不會講的特別深, 很多地方我都簡化過並且翻譯成大白話方便大家理解。 要是每一個點都很詳細的深入的去講,那估計都能寫成本書了。

專家系統與機器學習

我們舉一個信用卡反欺詐的例子, 以前的時候在銀行裡有一群業務專家, 他們的工作就是根據自己的知識和經驗向系統中輸入一些規則。例如某一張卡在一個城市有了一筆交易,之後 1 小時內在另一個城市又有了一筆交易。這些專家根據以前的經驗判斷這種情況是有盜刷的風險的。他們在系統中輸入了 1 千多條這樣的規則,組成了一個專家系統。 這個專家系統是建立在人類對過往的資料所總結出的經驗下建立的。 我們可以把它就看成一個大腦,我們業務是受這個大腦控制的。但這個大腦是有極限的,我們要知道這種規則從 0 條建立到 1 條是很容易的,但是從幾千條擴充套件到幾千零一條是很難的。 因為你要保證新的規則有效,要保證它不會跟之前所有的規則衝突,所這很難,因為人的分析能力畢竟是有限的。 我聽說過的最大的專家系統是百度鳳巢的,好像是在 10 年的時候吧,廣告系統裡有 1W 條專家規則。但這是極限了,它們已經沒辦法往裡再新增了。 所以說這是人腦的一個極限。 後來呢大家引入機器學習, 給機器學習演算法中灌入大量的歷史資料進行訓練, 它跟人類行為很像的一點就是它可以從歷史資料中找到規律,而且它的分析能力更強。 以前人類做分析的時候,可能說在反欺詐的例子裡,一個小時之內的跨城市交易記錄是一個規則,但如果機器學習來做的話,它可能劃分的更細,例如 10 分鐘之內有個權重,10 分鐘到 20 分鐘有個權重,1 小時 10 分鐘也有個權重。 也就是說它把這些時間段拆的更細。 也可以跟其他的規則組合,例如雖然我是 1 小時內的交易記錄跨越城市了,但是我再哪個城市發生了這類情況也有個權重,發生的時間也有個權重,交易數額也有權重。也就是說機器學習能幫助我們找個更多更細隱藏的更深的規則。 以前銀行的專家系統有 1000 多條規則,引入了機器學習後我們生成了 8000W 條規則。 百度在引入機器學習後從 1w 條規則擴充套件到了幾十億還是幾百億條 (我記不清楚了)。 所以當時百度廣告推薦的利潤很輕易的提升了 4 倍。 我們可以把專家系統看成是一個比較小的大腦,而機器學習是更大的大腦。 所以說我們叫它機器學習,是因為它像人類一樣可以從歷史中庫刻畫出規律,只不過它比人類的分析能力更強。 所以在網上有個段子麼,說機器學習就是學出來海量的 if else, 其實想想也有道理,套路都是 if 命中了這條規則,就怎麼麼樣的,else if 命中了那個規則,就怎麼怎麼樣的。 人類的分析能力有限麼,專家系統里人類寫 1 千,1w 個 if else 就到頭了。 但是機器學習給你整出來幾百個億的 if else 出來。 我們就可以把機器學習想的 low 一點麼,不要把它想的那麼神秘。

那深度學習是什麼呢, 深度學習就是機器學習的一個分支,他把基礎的機器學習演算法(邏輯迴歸),擴充套件成了神經網路:

上面是一個神經網路的結構圖, 可以理解為這個神經網路裡, 除了輸入層外,每一個神經元(節點)都是一個邏輯迴歸, 這樣我們的演算法就更加的複雜,能夠計算更加複雜的情況。 所以我們說傳統的結構化資料比較少會用到深度學習, 因為結構化資料比較簡單,不需要那麼複雜的計算。 而為了能夠計算影像和 NLP 領域內的複雜場景,才需要用到神經網路。 而深度學習就是一個擁有很多個隱藏層的神經網路,科學家們把這種情況命名為深度學習。

什麼是模型

人類負責提供資料提取特徵, 而演算法負責學習這些特徵對應的權重, 比如我們有下面這張表:

還是以信用卡反欺詐為例子,假設這張表中 label 列就是表明該使用者是否是欺詐行為的列。 當我們告訴演算法, 性別,年齡,職業和收入是有助於判斷使用者行為是否為欺詐行為的重要特徵,那麼演算法就會去學習這些特徵對預測最終結果的權重。 並把這些儲存成一個模型。 我們可以理解為模型就是儲存這些特徵和權重的資料庫。 比如可以簡單理解為模型裡儲存的主要是:

當使用者的一條資料過來後, 就要到模型裡面去查詢對應特徵的權重了, 比如新來了一條資料,是男性,收入 2300,年齡 24,職業是程式設計師。那麼演算法的計算過程就是:y = 男性 *0.1 + 程式設計師 *0.22 + 2300*0.3 + 24 * 0.2 。 假設最終的結果是 0.8,代表著這條資料有 80% 的機率是信用卡欺詐行為。 然後我們自己設定一個閾值, 或者叫置信度, 比如這個值是 0.7,意思是如果模型預測這個資料是欺詐行為的機率是大於 0.7 的,那我們就讓系統認為這個使用者就是欺詐行為, 而如果模型預測的解僱歐式這個資料是欺詐行為的機率小於 0.7,那我們就讓系統判定它是非欺詐行為,這就是一個典型的二分類模型的原理 -- 演算法輸出的是機率,系統透過設定閾值來做最後判斷。 所以演算法本身的公式其實就是: y= w1*x1+ w2*x2 + w3*x3 ...... wn*xn +b 。 其中 x 是特徵, w 是特徵對應的權重,b 是偏移量。其實模型的原理換成大白話來說還是比較簡單的。

而我們說機器學習, 它學習的就是 w 和 b(因為我們特徵值的已知的),演算法要透過一種方式來學習什麼的 w 和 b 的值能讓最終的結果與真實的情況最接近, 而這個方法就叫梯度下降,但是我不打算在這裡講它的原理了, 因為畢竟我們這裡是講如何做測試的, 不懂這個梯度下降的原理也沒關係。 感興趣的同學可以查查我之前寫的深度學習的帖子。

什麼是遷移學習

還拿上面的例子來說明,我們已經訓練好了一個模型,在這個模型中針對職業分別儲存了程式設計師,測試人員和產品經理的特徵和權重,它在當前場景下工作的很好。 我們想把這個模型放到另外一個公司裡面使用,但是這個公司裡除了這 3 種職業外,還有財務人員和行政人員,這樣這個模型的效果就不能滿足新公司的需要了。 這時候怎麼辦呢?再根據新公司的資料重新訓練一個模型麼?可以是可以, 但是這個成本可能太高了, 因為模型是要基於很大的資料進行訓練的,這對資料質量和算力都有比較高的要求。 所以這時候我們可以換個思路,雖然舊的模型中沒有財務人員和行政人員的記錄,但是它裡面的程式設計師,測試人員和產品經理在新公司裡也有這些職位的,所以舊模型並不是完全沒用,起碼在這三個職位上我們還可以用舊模型工作起來。 那假如我們讓演算法去學習在財務和行政人員的權重後,再更新到舊的模型裡,可不可以呢:

答案是可以的, 這就是遷移學習,也叫模型微調。 在已有的模型的基礎上稍微進行調整來滿足新的場景。打個比喻,就好像我們有一本練習題, 之前有位學霸已經寫下了練習題的所有答案了, 後來這本練習題出了新的版本,但其實 90% 的題還是老的。 只有 10% 的題不一樣,那麼我們只需要把老的題的答案都照著抄下來, 我們自己去寫剩下 10% 就可以了。

當然在模型微調的時候也可以修改舊模型中的特徵, 比如我們覺得那位學霸在明朝歷史方面的造詣不如我們自己, 那我們就可以選擇在這方面更相信自己的判斷,並不會全盤照抄(引數凍結,後面講微調的時候我們會說)。

遷移學習對於人工智慧的發展至關重要, 我們可以使用權威的模型(這些模型經過了時間和各個專案的考驗)並進行微調以適應自己的場景。 這也為在某些場景下測試人員利用模型的能力來輔助測試場景帶來了可能性(畢竟從頭訓練一個新模型的成本太高)

模型評估指標

接下來終於要說到如何測試模型了, 在這個領域裡模型其實沒有 bug 一說, 我們透過會說一個模型的效果好或者不好, 不會說這個模型有 bug -- 因為世界上沒有 100% 滿足所有場景的模型。 那我們要如何評估模型呢。 下面以分類模型為主。分類模型就是需要模型幫我們判斷這條資料屬於哪些分類,比如是信用卡欺詐行為或者不是,這就是二分類。 也可以是判斷目標是貓,還是狗,還是老鼠或者是人, 這種就是多分類。 一般要評估這種分類模型,我們需要統計一些標準的評估方法。 首先說一下為什麼不能直接用模型預測的正確率。正確率越高說明模型效果越好, 這其實是一個普遍的誤區。 舉一個例子把,假設我們有一個預測癌症的場景,健康的人有 99 個 (y=0),得癌症的病人有 1 個 (y=1)。我們用一個特別糟糕的模型,永遠都輸出 y=0,就是讓所有的病人都是健康的。這個時候我們的 “準確率” accuracy=99%,判斷對了 99 個,判斷錯了 1 個,但是很明顯地這個模型相當糟糕,在真實生活中我們大部分場景都是正/負向本相差十分懸殊的,一旦分佈十分懸殊那麼準確率這種簡單粗暴的方法就難以表達模型真正的效果, 所以準確率往往是我們最不常用的評估方法。因此需要一種很好的評測方法,來把這些 “作弊的” 模型給揪出來。

混淆矩陣

混淆矩陣是一個用於描述分類模型效能的矩陣,它顯示了模型對於每個類別的預測結果與實際結果的對比情況。

以分類模型中最簡單的二分類為例,對於這種問題,我們的模型最終需要判斷樣本的結果是 0 還是 1,或者說是 positive 還是 negative。

我們透過樣本的採集,能夠直接知道真實情況下,哪些資料結果是 positive,哪些結果是 negative。同時,我們透過用樣本資料跑出分型別模型的結果,也可以知道模型認為這些資料哪些是 positive,哪些是 negative。

因此,我們就能得到這樣四個基礎指標,我稱他們是一級指標(最底層的):


真實值是positive,模型認為是positive的數量(True Positive=TP)
真實值是positive,模型認為是negative的數量(False Negative=FN)
真實值是negative,模型認為是positive的數量(False Positive=FP)
真實值是negative,模型認為是negative的數量(True Negative=TN)

將這四個指標一起呈現在表格中,就能得到如下這樣一個矩陣,我們稱它為混淆矩陣(Confusion Matrix):

多分類的混淆矩陣
當分類的結果多於兩種的時候,混淆矩陣同時適用。

下面的混淆矩陣為例,我們的模型目的是為了預測樣本是什麼動物,這是我們的結果:

二級指標

但是,混淆矩陣裡面統計的是個數,有時候面對大量的資料,光憑算個數,很難衡量模型的優劣。 所以我們需要在混淆矩陣的基礎上引入其他幾個指標

因此混淆矩陣在基本的統計結果上又延伸瞭如下幾個個指標:

  • 召回率:recall,召回率就是說,所有得了癌症的病人中,有多少個被查出來得癌症。公式是:TP/TP+FN。 意思是真正類在所有正樣本中的比率,也就是真正類率
  • 精準率:precision,還是拿剛才的癌症的例子說。精準率 (precision) 就是說,所有被查出來得了癌症的人中,有多少個是真的癌症病人。公式是 TP/TP+FP。

召回和精準理解起來可能比較繞,我多解釋一下,我們說要統計召回率,因為我們要知道所有得了癌症中的人中,我們預測出來多少。因為預測癌症是我們這個模型的主要目的, 我們希望的是所有得了癌症的人都被查出來。不能說得了癌症的我預測說是健康的,這樣耽誤人家的病情是不行的。 但同時我們也要統計精準率, 為什麼呢, 假如我們為了追求召回率,我又輸入一個特別糟糕的模型,永遠判斷你是得了癌症的,這樣真正得了癌症的患者肯定不會漏掉了。但明顯這也是不行的對吧, 人家明明是健康的你硬說人家得了癌症,結果人家回去悲憤欲絕,生無可戀,自殺了。或者回去以後散盡家財,出家為僧。結果你後來跟人說我們誤診了, 那人家砍死你的心都有。 所以在統計召回的同時我們也要加入精準率, 計算所有被查出來得了癌症的人中,有多少是真的癌症病人。 說到這大家可能已經看出來召回和精準在某稱程度下是互斥的, 因為他們追求的是幾乎相反的目標。 有些時候召回高了,精準就會低。精準高了召回會變低。 所以這時候就要根據我們的業務重心來選擇到底選擇召回高的模型還是精準高的模型。 有些業務比較看重召回,有些業務比較看重精準。 當然也有兩樣都很看重的業務,就例如我們說的這個預測癌症的例子。或者說銀行的反欺詐場景。 反欺詐追求高召回率,不能讓真正的欺詐場景漏過去,在一定程度上也注重精準率,不能隨便三天兩頭的判斷錯誤把使用者的卡給凍結了對吧,來這麼幾次使用者就該換銀行了。 所以我們還有一個指標叫 F1 score, 大家可以理解為是召回和精準的平均值,在同時關注這兩種指標的場景下作為評估維度。F1 = 2PR/(P+R) 其中,P 代表 Precision,R 代表 Recall。

多分類評估舉例

將多分類混淆矩陣二分化,以貓為例,我們可以將上面的圖合併為二分問題:

Accuracy

在總共 66 個動物中,我們一共預測對了 10 + 15 + 20=45 個樣本,所以準確率(Accuracy)=45/66 = 68.2%。

Precision

所以,以貓為例,模型的結果告訴我們,66 只動物裡有 13 只是貓,但是其實這 13 只貓只有 10 只預測對了。模型認為是貓的 13 只動物裡,有 1 條狗,兩隻豬。所以,Precision(貓)= 10/13 = 76.9%

Recall

以貓為例,在總共 18 只真貓中,我們的模型認為裡面只有 10 只是貓,剩下的 3 只是狗,5 只都是豬。這 5 只八成是橘貓,能理解。所以,Recall(貓)= 10/18 = 55.6%

F1-Score

透過公式,可以計算出,對貓而言,F1-Score=(2 * 0.769 * 0.556)/( 0.769 + 0.556)= 64.54%

ROA 與 AUC

ROC(Receiver Operating Characteristic Curve)和 AUC(Area Under Curve)是評價分類模型效能的指標,常用於二分類問題。
ROC 曲線是將真陽性率(True Positive Rate,TPR)和假陽性率(False Positive Rate,FPR)在不同閾值下的表現繪製成的曲線。TPR(也稱為敏感性)表示正類樣本中正確識別的正類樣本所佔的比例,而 FPR(1-特異性)表示負類樣本中錯誤識別為正類樣本的負類樣本所佔的比例。

AUC 則是 ROC 曲線下的面積,用來量化模型的分類能力。AUC 越大,表示模型的分類效能越好。

在二分類問題中,ROC 曲線一般是從左上角到右下角,曲線下的面積(AUC)為 0.5,表示模型對正負類的預測機率相等。如果 AUC 大於 0.5,表示模型對正類預測較好,如果 AUC 小於 0.5,表示模型對負類預測較好。

ROC 曲線和 AUC 可以作為評估分類模型效能的參考指標,並且可以幫助選擇合適的分類閾值。

一般在一些機器學習的庫中,會有相關的演算法去統計 AUC 指標,比如在 spark 庫中:

from pyspark.ml.evaluation import BinaryClassificationEvaluator
from pyspark.ml.feature import VectorAssembler
from pyspark.sql import SparkSession
from pyspark.sql.types import StructType, StructField, DoubleType, IntegerType


spark = SparkSession.builder \
    .appName("AUC Calculation") \
    .getOrCreate()

data = [(0, 1.0), (1, 0.9), (0, 0.8), (1, 0.7), (0, 0.6), (1, 0.5), (0, 0.4), (1, 0.3), (0, 0.2), (1, 0.1)]

# 定義資料模式
schema = StructType([
    StructField("label", IntegerType(), True),
    StructField("probability", DoubleType(), True)
])

# 將資料轉換為 DataFrame
df = spark.createDataFrame(data, schema=schema)


# 計算 AUC
evaluator = BinaryClassificationEvaluator(rawPredictionCol="probability", labelCol="label")
auc = evaluator.evaluate(df)

print("AUC:", auc)

分組指標的統計

我們現在有了一些常用的評估指標。 但這些往往還是不夠,因為我們每一個模型都是有業務場景的,業務場景有很複雜的邏輯和不同的側重點。 以前在測試模型的時候經常的漏測場景就是忽略了業務含義而做的測試。 舉個例子,我們拿某影片網站的影片推薦系統來說。 我們使用二分類模型來做推薦問題, 怎麼做呢, 我們有一個使用者, 同時有 2000 個影片候選集,我們分別用這些影片和使用者分別用模型算出 2000 個機率,意思是每個影片使用者會點選的機率。然後從高到低排序。把排行前面的影片推薦給使用者。 所以我們就把一個推薦系統的問題轉換為一個分類問題。只不過這裡我們不設定閾值了而已。如果按照我們剛才講的評估指標,我們計算了 AUC,召回,精準。 就可以了麼,大家想一想這樣做有沒有什麼問題? 問題是這些指標的統計都是建立在所有測試集下進行的統計的,粒度是非常粗的。 也就是說我們並沒有對測試資料進行分類。 舉個例子, 在影片網站中, 新使用者和新影片的點選情況可能也是比較看重的指標。 尤其是影片網站上每日新增的影片是很多的,對於新增的熱點影片的曝光率也就是被推薦的機率也是很重要的維度。 假如我們有一個模型,我們測試之後發現各項指標都很好,但其實可能它對新使用者或者新影片的預測並不好,只是由於新增的使用者和影片在整個資料集中的佔比太小了,所以從整體的評估指標上來看是比較好的。 所以一般我們測試一個模型的時候,要根據業務引入分組指標的統計。並且要保證每一個分組都有足夠的樣本,如果某個分組的樣本數量太少,那麼不足以表達它的真實效果。所以在資料採集的時候就需要注意。

其實就是需要測試人員要理解你的業務, 熟悉你的資料。 你需要了解產品的使用者畫像, 需要知道使用者要切分成多少個分類。 比如按職業分類, 按性別分類, 按愛好分類, 按時間段分類,按年齡分分類, 按行為分類等等等等。 只要足夠了解了使用者和資料,測試人員才知道都需要從哪些維度評估模型的效果。 我們說搞演算法測試的人 8 成時間都是在跟資料打交道, 就是因為這樣。 所以比起對 AI 演算法的理解, 資料處理技術可能對測試人員來說才是更重要的。比如處理結構化資料需要學習 spark,hive 這樣的分散式計算框架,處理影像資料需要學習 opencv 和 ffmpeg。 因為需要對資料進行採集,分析,統計。 畢竟符合場景要求的資料不會自己飛到你面前。下一篇文章會詳細介紹資料處理相關的內容, 這一次只介紹效果測試的方法。

計算機視覺下的模型效果測試

上面我們聊的所有的場景都是在結構化資料下的,通常在金融領域或者在某些網際網路系統中(比如推薦系統)。 但在計算機視覺場景裡,模型的評估指標會稍微有些不一樣。 但其大部分的基本方法論仍然是通用的,比如召回,精準, FIcore 依然是通用的。只是在一些特殊場景裡會有不同。 比如在目標檢測場景中:

這是我週末帶老婆孩子去體育場玩時拍下來的照片。 我使用這張照片輸入到 yolo 模型中(計算機視覺中非常著名的演算法),希望模型可以識別出圖片中的人類並畫出人類所在位置的長方形的框(框上面的數字是這個目標屬於人類的機率)。 而這就是目標檢測,同時目標檢測也是計算機視覺中的基礎演算法,很多其他場景都需要依賴目標檢測。 比如 OCR 要識別文字, 就需要先定位到文字所在的位置,所以需要先經過目標檢測。

那麼在這樣的場景中,我們同樣需要使用召回率,精準率這些指標來統計目標識別的效果,用來評估有多少真正的目標被識別出來了,又有多少非目標物體被錯誤的識別出來了。 但也需要額外再去評估這個框的準確性, 就是雖然目標識別出來了, 但是你畫框的位置有偏差,這樣就不好了。 就像我們剛才說 OCR 的流程是先跑一個目標檢測把目標的位置識別出來, 然後再把目標位置的影像資訊輸入到另一個模型(負責文字識別),所以如果位置識別的有偏差, 可能會造成最終結果的南轅北轍。而評估這個位置的指標,我們叫 IOU。直接上程式碼:



# IoU是目標檢測中常用的一種重疊度量,用於衡量檢測框和真實標註框之間的重疊程度。IoU值越大,表示檢測結果越準確。它的達標標準取決於具體的應用場景和需求。
# 在目標檢測任務中,常用的IoU閾值為0.5或0.7,即當檢測框與真實標註框的IoU值大於等於0.5或0.7時,認為檢測結果正確。在一些特定的應用場景中,IoU閾值可能會更高或更低,比如如果用於OCR, 那麼準確度要求是非常高的。

def compute_iou(box1, box2):
    # 計算兩個矩形框的面積
    area1 = (box1[2] - box1[0] + 1) * (box1[3] - box1[1] + 1)
    area2 = (box2[2] - box2[0] + 1) * (box2[3] - box2[1] + 1)

    # 計算交集的座標範圍
    x1 = max(box1[0], box2[0])
    y1 = max(box1[1], box2[1])
    x2 = min(box1[2], box2[2])
    y2 = min(box1[3], box2[3])

    # 計算交集的面積
    inter_area = max(0, x2 - x1 + 1) * max(0, y2 - y1 + 1)

    # 計算並集的面積
    union_area = area1 + area2 - inter_area

    # 計算IoU值
    iou = inter_area / union_area

    return iou


box1 = [50, 50, 150, 150]  # 左上角座標為(50, 50),右下角座標為(150, 150)
box2 = [100, 100, 200, 200]  # 左上角座標為(100, 100),右下角座標為(200, 200)

# 計算兩個矩形框之間的IoU值
iou = compute_iou(box1, box2)

# 輸出結果
print('IoU:', iou)

NLP 的評估指標

剛才我們提到了 OCR, 其實 OCR 本身也算是沾著 NLP 的邊的,畢竟他有個需要理解人類語言的過程。 那一般在 NLP 領域裡我們會如何評估它的效果呢。 就拿 OCR 來說:

  • 字元識別準確率,即識別對的字元數佔總識別出來字元數的比例,可以反應識別錯和多識別的情況,但無法反應漏識別的情況
  • 字元識別召回率,即識別對的字元數佔實際字元數的比例,可以反應識別錯和漏識別的情況,但是沒辦法反應多識別的情況,可以配套字元識別準確率一起使用。
  • 整行準確率:一個欄位算一個整體,假如 100 個字分為 20 個欄位,裡面錯了 5 個字,分佈在 4 個欄位裡,那麼識別率是 16/20=80%。
  • 平均編輯距離:是一種衡量兩個字串之間相似度的度量。在 OCR 中,我們可以使用平均編輯距離來評估 OCR 系統的效果。具體來說,我們可以將 OCR 系統識別的文字與實際的文字進行比較,計算它們之間的平均編輯距離。較低的平均編輯距離表示 OCR 系統的效果較好,因為它能更準確地識別和轉換影像中的文字。

可以看出精準和召回仍然是比較主要的評估方法,下面給出計算平均編輯距離的 python 程式碼


import Levenshtein

def average_edit_distance(predicted_text, ground_truth_text):
    return Levenshtein.distance(predicted_text, ground_truth_text) / max(len(predicted_text), len(ground_truth_text))

# 示例
predicted_text = "Hello world"
ground_truth_text = "Hello, world!"

aed = average_edit_distance(predicted_text, ground_truth_text)
print(f"Average Edit Distance: {aed:.2f}")

上面的評估方法在 OCR 裡比較常用, 相對來說是比較簡單的,因為它有 1 對 1 的答案, ocr 識別出來的內容在評估的時候不存在歧義行, 圖片裡的文字是什麼,你識別出來的文字就應該是什麼,所以可以在字元級別很容易統計出指標。 但在其他 NLP 領域裡就沒有那麼簡單了。 比如對一個翻譯模型來說。它翻譯出來的內容往往是沒有唯一的標準答案的, 比如 my name is frank 和 frank is my name 意思是基本一樣的,在一個語言裡,你可以用完全不同的表達形式來表述同一個意思,所以有很多時候評估是很主觀的, 需要這個語言的專業人員來進行評估,並且也不再是字元級別的,而是針對語句和段落級別來統計翻譯的正確率了。

那有沒有一種普通的測試人員可以使用的方法呢, 也是有的。 比如 BLEU,ROUGE,METEOR, TER。 這些指標其實都是用一些演算法來計算文字之間的相似程度的。

下面以 METEOR 為例子:

from nltk.translate.meteor_score import meteor_score

# 生成文字
generated_text = ['my', 'name', 'is', 'frank']

# 參考文字列表
reference_texts = [['my', 'name', 'is', 'frank'],['frank', 'is', 'my', 'name']]

# 計算 METEOR 指標
meteor = meteor_score(reference_texts, generated_text)

# 列印結果
print("The METEOR score is:", meteor)

操作也比較簡單, 我們一般會準備幾個參考的答案,把模型生成的本文和參考文字一起輸入到演算法中,讓演算法去挨個計算他們之間的相似程度。 這也為自動化測試提供了基礎。 這些演算法的具體原理就不講了, 一個是講不完, 再一個我自己其實也糊里糊塗的, 並且其實完全沒必要知道原理, 只需要知道他們是計算文字相似度的就可以了。

其實我更喜歡用 bert 模型來計算文字相似度, bert 是 nlp 領域內非常著名的預訓練模型, 它本質上其實是個詞向量模型, 使用者可以基於 bert 去做模型的微調來完成各種任務。 現在有一個開源專案叫 bert-score,它是專門計算文字相似度的,並且給出相關的精準,召回和 FI score 的指標,要使用 bert-score,需要下載 bert-base-chinese 模型, 這是 bert 針對中文的詞向量模型。 然後準備下面的指令碼:

from bert_score import score

cands = ['我們都曾經年輕過,雖然我們都年少,但還是懂事的','我們都曾經年輕過,雖然我們都年少,但還是懂事的']
refs = ['雖然我們都年少,但還是懂事的','我們都曾經年輕過,雖然我們都年少,但還是懂事的']

P, R, F1 = score(cands, refs,model_type="bert-base-chinese",lang="zh", verbose=True)
print(F1)
print(f"System level F1 score: {F1.mean():.3f}")
print(f"System level Precison: {P.mean():.3f}")
print(f"System level Recall: {R.mean():.3f}")


輸入結果:
System level F1 score: 0.957
System level Precison: 0.940
System level Recall: 0.977

R(召回率 Recall):

  • 評估生成文字中有多少與參考文字相匹配的內容。
  • 召回率越高,說明生成文字覆蓋了更多的參考文字內容。

P(精確度 Precision):

  • 評估生成文字中與參考文字匹配的內容所佔的比例。
  • 精確度越高,說明生成文字中的內容與參考文字的匹配度越高。

F(F1 Score):

  • 是 Precision 和 Recall 的調和平均數。
  • F1 Score 能夠平衡精確度和召回率,提供單一的效能衡量指標。

相比之前的演算法,我更喜歡用 bert 來評估文字之間的相似程度。 尤其在大模型的測試中,通常會評估大模型生成的答案,和預期的答案之間的相似程度。 實際上這種用模型測試模型的方法,在大模型場景中也算是不少見了。 那接下來我們聊聊大模型的測試場景。

大模型

其實大模型是典型的生成式模型(用來生成內容的),並且多數能力屬於 NLP 領域。 事實上大模型的訓練原理也是 NLP 的。 transformer 原本就是用於訓練 NLP 模型的, 後來才應用在了大模型中。所以其中的很多的測試方法可以參考 NLP,比如文字相似度的計算。 但這種生成式模型,我們需要考慮幾個問題。

資料收集

首先是資料問題, 測試資料的收集向來都是繁雜和枯燥的, 只能一點點按部就班的收集資料並進行評估。由於自然語言的複雜和多樣性,這也導致了我們需要
評估的內容非常多。 所以需要建立起一套或多套的問卷來進行評估。 當然也可以用行業公開的資料集和指標。 比如在語言安全方面(內容稽核)可以使用 Safety-Prompts,
中文安全 prompts,用於評測和提升大模型的安全性,將模型的輸出與人類的價值觀對齊。

也可以使用中文通用大模型評測標準 SuperCLUE,23 年 5 月在國內剛推出, 它主要回答的問題是:中文大模型的效果情況,包括但不限於"這些模型不同任務的效果情況"、"相較於國際上的代表性模型做到了什麼程度"、 "這些模型與人類的效果對比如何"。
該標準可透過多個層面,考驗市面上主流的中文 GPT 大模型的能力。一是基礎能力,包括常見的有代表性的模型能力,如語義理解、對話、邏輯推理、角色模擬、程式碼、生成與創作等 10 項能力;二是專業能力,包括中學、大學與專業考試,涵蓋從數學、物理、地理到社會科學等 50 多項能力;三是中文特效能力,針對有中文特點的任務,包括中文成語、詩歌、文學、字形等 10 項能力。

或者 C-Eval:

C-Eval 也支援為使用者的大模型進行打分, 所以我們可以讓自己的大模型去回答 C-Eval 的問題,然後把結果上傳回 C-Eval 的官方網站, 就可以得到一個評分結果, 讓你知道你的大模型在全球的排名。

但即便有這樣的資料測試人員也會遇到如下的問題:

  • 一個問題的解答方式是可以有不同的方法的(一題多解)並且在很多領域十分的專業,測試人員沒有專業的知識來判斷準確性。(這個沒有辦法解決,沒有專業知識是不行的,只能找專業人員評估,所以一個大模型或者生成式模型,需要很多不同職業不同角色的人來進行評估,不是測試人員自己可以搞定的。 在初期的時候需要這些專業人員準備問題和參考答案。 測試人員針對這些問題和答案進行測試,計算文字相似度。)
  • 很多問題的答案帶有很大的主觀理解,比如生成一張圖片到底美觀不美觀,一首詩寫的好或不好,是非常看個人的主觀意願的。
  • 現實的情況太多了, 測試人員是永遠沒有辦法窮舉出使用者所有的 prompt 了, 甚至連相當比例的 prompt 都無法窮舉出來,這個工作量太過於龐大了。

第一個問題不過多說明了,文字相似度的計算已經在上面說過了。 我們來看看如何解決下面的兩個問題。

怎樣解決主觀差異問題

  • 多人打分取平均:假設取 100 分為滿分, 別被找 n 個人為結果打分, 然後取平均值。
  • 兩人打分,並由第三人仲裁:提前設定一個可接受的分差範圍,如最大接受 2 個人打分分差為 20 分,對每篇作文,由 2 個人分別獨立打分;若分差大於可接受範圍,則由第 3 個人參與打分。

主觀問題一般只能投票解決了, 我們暫時還沒有更好的辦法來解決, 如果各位有什麼好的方法,也請說出來讓我學習參考。

用模型來測試模型

對於這種大模型場景或者生成式場景來說, 測試人員不可能列舉出所有的問題和答案,這個太不現實了。 雖然我們可以爬取到線上使用者提的所有問題, 但是我們沒辦法把他們作為測試資料, 因為這些問題的答案需要人工來判斷。所以測試人員能列舉的情況是有限的。 老實講這個難點幾乎無解, 只能是投入大量的人力去收集測試資料。 在 AI 領域裡不管是演算法人員還是測試人員,都要面臨巧婦難為無米之炊的窘境, 而資料就是那個米。 但是測試人員在這裡還是可以做一些事情的。 我們可以去總結一些特定的場景。 比如我在將大模型資料的時候,一開始的那個內容稽核的場景或者叫安全場景。 大家看當使用者輸入了辱罵,違法,歧視,黃色,暴力等 prompt 候,我們的模型必須是要明確反對,勸導和拒絕的。 所以我們可以用自己收集的,或者公開的安全場景的資料集輸入到模型中, 只要模型明確的輸出了帶有反對,勸導和拒絕的帶有這種感情色彩的答案,就是對的,沒有返回就是錯的。 這種測試型別也是很重要的(參考之前網路上流傳的各種模型翻車的案例,如果嚴重了會被下架)。所以如果我們的程式可以判斷模型輸出的答案是否有這種感情色彩就可以了。 於是乎,我們把這樣的問題抽象成了一個文字分類,或者叫文字的情感分類場景。 這時我們就可以自己訓練一個模型來完成這種判斷。 當然從頭訓練一個模型基本是不現實的,這個成本太高了。 所以我們要利用遷移學習的思路,用一個成熟的模型經過微調後來滿足我們的場景。 這也是為什麼我在開篇要先講遷移學習的原因之一。 我這裡先給出一段 demo, 這是用 bert 來進行微調的文字分類模型的訓練程式碼,程式碼有點長,需要安裝 pytorch 環境以及下載 bert 中文的預訓練模型:

import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')


from transformers import BertTokenizer

# 載入 BERT 分詞器
print('Loading BERT tokenizer...')
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese', do_lower_case=True)

sentences = ['我是誰', '我在哪', '你是誰', '你在哪', '我知道我是誰', '我知道你是誰']
labels = [1, 1, 1, 1, 0, 0]

# 輸出原始句子
print(' Original: ', sentences[0])

# 將分詞後的內容輸出
print('Tokenized: ', tokenizer.tokenize(sentences[0]))

# 將每個詞對映到詞典下標
print('Token IDs: ', tokenizer.convert_tokens_to_ids(tokenizer.tokenize(sentences[0])))

input_ids = []
attention_masks = []

# 對文字進行預處理, bert要求文字需要CLSSEP token並且需要固定的長度,tokenizer.encode_plus能夠幫助我們自動的完成這些預處理操作
for sent in sentences:
    encoded_dict = tokenizer.encode_plus(
        sent,  # 輸入文字
        add_special_tokens=True,  # 新增 '[CLS]'  '[SEP]'
        max_length=64,  # 填充 & 截斷長度
        pad_to_max_length=True,
        return_attention_mask=True,  # 返回 attn. masks.
        return_tensors='pt',  # 返回 pytorch tensors 格式的資料
    )

    # 將編碼後的文字加入到列表
    input_ids.append(encoded_dict['input_ids'])

    # 將文字的 attention mask 也加入到 attention_masks 列表
    attention_masks.append(encoded_dict['attention_mask'])

# 將列表轉換為 tensor
input_ids = torch.cat(input_ids, dim=0)
attention_masks = torch.cat(attention_masks, dim=0)
labels = torch.tensor(labels)

# 輸出第 1 行文字的原始和編碼後的資訊
print('Original: ', sentences[0])
print('Token IDs:', input_ids[0])



from torch.utils.data import TensorDataset, random_split

# 將輸入資料合併為 TensorDataset 物件
dataset = TensorDataset(input_ids, attention_masks, labels)

# 計算訓練集和驗證集大小
train_size = int(0.9 * len(dataset))
val_size = len(dataset) - train_size

# 按照資料大小隨機拆分訓練集和測試集
train_dataset, val_dataset = random_split(dataset, [train_size, val_size])

print('{:>5,} training samples'.format(train_size))
print('{:>5,} validation samples'.format(val_size))



from torch.utils.data import DataLoader, RandomSampler, SequentialSampler

#  fine-tune 的訓練中,BERT 作者建議小批次大小設為 16  32
batch_size = 2

# 為訓練和驗證集建立 Dataloader,對訓練樣本隨機洗牌
train_dataloader = DataLoader(
            train_dataset,  # 訓練樣本
            sampler = RandomSampler(train_dataset), # 隨機小批次
            batch_size = batch_size # 以小批次進行訓練
        )

# 驗證集不需要隨機化,這裡順序讀取就好
validation_dataloader = DataLoader(
            val_dataset, # 驗證樣本
            sampler = SequentialSampler(val_dataset), # 順序選取小批次
            batch_size = batch_size
        )




from transformers import BertForSequenceClassification, AdamW, BertConfig

# 載入 BertForSequenceClassification, 預訓練 BERT 模型 + 頂層的線性分類層
model = BertForSequenceClassification.from_pretrained(
    "bert-base-chinese", # 預訓練模型
    num_labels = 2, # 分類數 --2 表示二分類
                    # 你可以改變這個數字,用於多分類任務
    output_attentions = False, # 模型是否返回 attentions weights.
    output_hidden_states = False, # 模型是否返回所有隱層狀態.
)


optimizer = AdamW(model.parameters(),
                  lr = 2e-5, # args.learning_rate - default is 5e-5
                  eps = 1e-8 # args.adam_epsilon  - default is 1e-8
                )

from transformers import get_linear_schedule_with_warmup

# 訓練 epochs BERT 作者建議在 2  4 之間,設大了容易過擬合
epochs = 4

# 總的訓練樣本數
total_steps = len(train_dataloader) * epochs

# 建立學習率排程器
scheduler = get_linear_schedule_with_warmup(optimizer,
                                            num_warmup_steps = 0,
                                            num_training_steps = total_steps)







import numpy as np

# 根據預測結果和標籤資料來計算準確率
def flat_accuracy(preds, labels):
    pred_flat = np.argmax(preds, axis=1).flatten()
    labels_flat = labels.flatten()
    return np.sum(pred_flat == labels_flat) / len(labels_flat)


import time
import datetime


def format_time(elapsed):
    '''
    Takes a time in seconds and returns a string hh:mm:ss
    '''
    # 四捨五入到最近的秒
    elapsed_rounded = int(round((elapsed)))

    # 格式化為 hh:mm:ss
    return str(datetime.timedelta(seconds=elapsed_rounded))






import random
import numpy as np

# 以下訓練程式碼

# 設定隨機種子值,以確保輸出是確定的
seed_val = 42

random.seed(seed_val)
np.random.seed(seed_val)
torch.manual_seed(seed_val)
torch.cuda.manual_seed_all(seed_val)

# 儲存訓練和評估的 loss、準確率、訓練時長等統計指標,
training_stats = []

# 統計整個訓練時長
total_t0 = time.time()

for epoch_i in range(0, epochs):

    #開始訓練

    print("")
    print('======== Epoch {:} / {:} ========'.format(epoch_i + 1, epochs))
    print('Training...')

    # 統計單次 epoch 的訓練時間
    t0 = time.time()

    # 重置每次 epoch 的訓練總 loss
    total_train_loss = 0

    # 將模型設定為訓練模式。這裡並不是呼叫訓練介面的意思
    # dropoutbatchnorm 層在訓練和測試模式下的表現是不同的
    model.train()

    # 訓練集小批次迭代
    for step, batch in enumerate(train_dataloader):

        # 每經過40次迭代,就輸出進度資訊
        if step % 40 == 0 and not step == 0:
            elapsed = format_time(time.time() - t0)
            print('  Batch {:>5,}  of  {:>5,}.    Elapsed: {:}.'.format(step, len(train_dataloader), elapsed))

        # 準備輸入資料,並將其複製到 cpu 
        b_input_ids = batch[0].to(device)
        b_input_mask = batch[1].to(device)
        b_labels = batch[2].to(device)

        # 每次計算梯度前,都需要將梯度清 0,因為 pytorch 的梯度是累加的
        model.zero_grad()

        # 前向傳播
        # 該函式會根據不同的引數,會返回不同的值。 本例中, 會返回 loss  logits -- 模型的預測結果



        # loss, logits = model(b_input_ids,
        #                      token_type_ids=None,
        #                      attention_mask=b_input_mask,
        #                      labels=b_labels)

        output = model(b_input_ids,
              token_type_ids=None,
              attention_mask=b_input_mask,
              labels=b_labels)
        loss = output.loss
        logits = output.logits

        print(loss)
        print(logits)

        # 累加 loss
        total_train_loss += loss.item()

        # 反向傳播
        loss.backward()

        # 梯度裁剪,避免出現梯度爆炸情況
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)

        # 更新引數
        optimizer.step()

        # 更新學習率
        scheduler.step()

    # 平均訓練誤差
    avg_train_loss = total_train_loss / len(train_dataloader)

    # 單次 epoch 的訓練時長
    training_time = format_time(time.time() - t0)

    print("")
    print("  Average training loss: {0:.2f}".format(avg_train_loss))
    print("  Training epcoh took: {:}".format(training_time))

    # 開始用驗證集評估效果
    # 完成一次 epoch 訓練後,就對該模型的效能進行驗證

    print("")
    print("Running Validation...")

    t0 = time.time()

    # 設定模型為評估模式
    model.eval()

    # Tracking variables
    total_eval_accuracy = 0
    total_eval_loss = 0
    nb_eval_steps = 0

    # Evaluate data for one epoch
    for batch in validation_dataloader:
        # 將輸入資料載入到 gpu 
        b_input_ids = batch[0].to(device)
        b_input_mask = batch[1].to(device)
        b_labels = batch[2].to(device)

        # 評估的時候不需要更新引數、計算梯度
        with torch.no_grad():
            output = model(b_input_ids,
                                   token_type_ids=None,
                                   attention_mask=b_input_mask,
                                   labels=b_labels)
            loss = output.loss
            logits = output.logits

        # 累加 loss
        total_eval_loss += loss.item()

        # 將預測結果和 labels 載入到 cpu 中計算
        logits = logits.detach().cpu().numpy()
        label_ids = b_labels.to('cpu').numpy()

        # 計算準確率
        total_eval_accuracy += flat_accuracy(logits, label_ids)

    # 列印本次 epoch 的準確率
    avg_val_accuracy = total_eval_accuracy / len(validation_dataloader)
    print("  Accuracy: {0:.2f}".format(avg_val_accuracy))

    # 統計本次 epoch  loss
    avg_val_loss = total_eval_loss / len(validation_dataloader)

    # 統計本次評估的時長
    validation_time = format_time(time.time() - t0)

    print("  Validation Loss: {0:.2f}".format(avg_val_loss))
    print("  Validation took: {:}".format(validation_time))

    # 記錄本次 epoch 的所有統計資訊
    training_stats.append(
        {
            'epoch': epoch_i + 1,
            'Training Loss': avg_train_loss,
            'Valid. Loss': avg_val_loss,
            'Valid. Accur.': avg_val_accuracy,
            'Training Time': training_time,
            'Validation Time': validation_time
        }
    )

print("")
print("Training complete!")
print("Total training took {:} (h:mm:ss)".format(format_time(time.time() - total_t0)))




# 這裡走一下模型推理,用一個文字資料輸入到模型中讓他分類
# 要分類的文字
text = "我知道我是誰"

# 對文字進行編碼
inputs = tokenizer(text, return_tensors="pt")

# 進行預測
with torch.no_grad():
    outputs = model(**inputs)
    logits = outputs.logits

# 獲取預測結果,因為走的是多分類的介面, 所以需要取機率最高的那個作為最終的預測結果
predictions = torch.softmax(logits, dim=-1)
predicted_class_idx = torch.argmax(predictions, dim=-1).item()

print(f"Predicted class: {predictions}")
print(f"Predicted class index: {predicted_class_idx}")



上面的程式碼需要各位有一定的 pytorch 的知識儲備,也需要了解 bert 模型的一些原理才能知道怎麼預處理資料(bert 要求資料處理成它要求的格式,尤其是需要在文字中新增 CLS 和 SEP,還需要把文字都處理成固定長度,長度不夠的要 padding 補全等)。 這個比較遺憾的可能不是能在特別短的時間內速成的。 不過上次跟螞蟻的老師交流的時候,他介紹給我一個名字叫 EasyNLP 的開源專案, 這個介面用起來非常簡單:

easynlp 的安裝:

git clone https://github.com/alibaba/EasyNLP.git
$ cd EasyNLP
$ python setup.py install

程式碼:

from easynlp.appzoo import ClassificationDataset
from easynlp.appzoo import get_application_model, get_application_evaluator
from easynlp.core import Trainer
from easynlp.utils import initialize_easynlp, get_args
from easynlp.utils.global_vars import parse_user_defined_parameters
from easynlp.utils import get_pretrain_model_path

initialize_easynlp()
args = get_args()
user_defined_parameters = parse_user_defined_parameters(args.user_defined_parameters)
pretrained_model_name_or_path = get_pretrain_model_path(user_defined_parameters.get('pretrain_model_name_or_path', None))

train_dataset = ClassificationDataset(
    pretrained_model_name_or_path=pretrained_model_name_or_path,
    data_file=args.tables.split(",")[0],
    max_seq_length=args.sequence_length,
    input_schema=args.input_schema,
    first_sequence=args.first_sequence,
    second_sequence=args.second_sequence,
    label_name=args.label_name,
    label_enumerate_values=args.label_enumerate_values,
    user_defined_parameters=user_defined_parameters,
    is_training=True)

valid_dataset = ClassificationDataset(
    pretrained_model_name_or_path=pretrained_model_name_or_path,
    data_file=args.tables.split(",")[-1],
    max_seq_length=args.sequence_length,
    input_schema=args.input_schema,
    first_sequence=args.first_sequence,
    second_sequence=args.second_sequence,
    label_name=args.label_name,
    label_enumerate_values=args.label_enumerate_values,
    user_defined_parameters=user_defined_parameters,
    is_training=False)

model = get_application_model(app_name=args.app_name,
    pretrained_model_name_or_path=pretrained_model_name_or_path,
    num_labels=len(valid_dataset.label_enumerate_values),
    user_defined_parameters=user_defined_parameters)

trainer = Trainer(model=model, train_dataset=train_dataset,user_defined_parameters=user_defined_parameters,
    evaluator=get_application_evaluator(app_name=args.app_name, valid_dataset=valid_dataset,user_defined_parameters=user_defined_parameters,
    eval_batch_size=args.micro_batch_size))

trainer.train()

執行:


easynlp \
   --mode=train \
   --worker_gpu=1 \
   --tables=train.tsv,dev.tsv \
   --input_schema=label:str:1,sid1:str:1,sid2:str:1,sent1:str:1,sent2:str:1 \
   --first_sequence=sent1 \
   --label_name=label \
   --label_enumerate_values=0,1 \
   --checkpoint_dir=./classification_model \
   --epoch_num=1  \
   --sequence_length=128 \
   --app_name=text_classify \
   --user_defined_parameters='pretrain_model_name_or_path=bert-small-uncased'

EasyNLP 的使用明顯簡單很多了, 因為它把複雜的東西都封裝了起來, 不過折騰環境有點費盡, 中間遇到過各種問題。 並且文件也是不太好,我研究了好一段時間才知道怎麼訓練並儲存中文的文字分類模型。

AIGC

大模型除了生成文字外, 還有生成影像的 AIGC, 其實我本人還沒有開始參與 AIGC 的測試工作。 但跟其他同行交流過後,也大概知道了要怎麼來利用 yolo 模型和 blip 模型來做這種測試,同樣我也借鑑這個思路開發了一個資料探勘工具(這個我們下篇講資料的帖子裡再詳細講解)。 我們假設去測試生成人有關的圖片, 這裡面常見的問題有:

  • 肢體異常:多肢體,多頭
  • 手部異常:多指,殘指
  • 性別錯誤
  • 錯誤特徵:錯誤生成鬍子,人種偏差
  • 色情: 低俗,露點
  • 其他業務問題

所以其實我們可以訓練一個 yolo 模型,專門去識別人類的手部(或者直接找公開的手部識別模型),識別到手部的位置後,把手摳出來,然後再用一個 blip 模型(或者也是個 yolo 也可以)去識別這個手是否是殘指,多指。 其實就是一個目標檢測目標先把目標摳出來, 然後一個分類模型去識別這個目標是不是有缺陷的(跟 OCR 的思路很像。 具體怎麼微調 YOLO,怎麼使用 blip 我下篇講資料探勘工具的時候再說,總之這種自動化測試的思路差不多是這樣的。)

模型測試模型的總結

總結一下這種測試方法之所以會出現,就是因為收集海量的標準問 - 答資料的成本是非常高的, 所以我們可以從線上拉到使用者的輸入的文案, 然後簡單的透過簡單的切詞來匹配場景,比如使用者的文字中包含了:小孩,女孩,男人,男孩,老人等等明顯是跟人有關係的關鍵詞,這時候就可以知道這是一個生成人的場景, 所以就可以把這些資料輸入到大模型中, 然後我們用自己訓練的模型去判斷生成的圖片中有沒有殘指,多頭等已知的特別容易出現的缺陷(在 AIGC 領域中手指和頭是比較出名的不容易處理,容易出問題的場景)。

所以其實我們的思路就是在測試場景中抽象出已知的明顯容易出現問題的場景, 然後告訴演算法什麼樣的圖片是好的,什麼樣的是不好的(就是收集這些不好的圖片作為訓練資料),由於遷移學習的存在,可能我們只需要 1 千張圖片就有比較好的場景(文字也是一樣的)。 當然我們自己調整的模型,它的效果肯定沒有那麼好。 但我們用量來補充這個缺陷。 比如可以測試 1W 甚至 10W,用量來彌補準確率的不足。 只要樣本的量足夠的多, 我們就能挖掘到更多的問題。

需要注意的是這種方式不能代替人工測試的,它只能是一種輔助手段,用模型來幫助我們挖掘潛在的問題(畢竟人的精力有限,不可能測試到那麼多的樣本),所以人工測試,仍然是非常重要的手段。 我們一般可能人工測試 1000~5000 個樣本, 機器測試 1W 個或者 10w 個樣本甚至更多,用這樣的策略讓人和機器一起去挖掘潛在的問題。

自學習與線上的效果監控

在一些實際的業務場景裡,會更加偏向線上的監控和 A/B Test 而非離線的測試。 我們先看一下什麼是自學習。

自學習是比較好理解的,在一些場景裡,一個模型是不能永遠都生效的,因為使用者在變化, 資料也在變化。 我們說機器學習是你給他什麼樣的資料,他就會學習出什麼樣的效果。 所以當使用者和資料變化後,我們也需要用最新的資料來更新模型以保證它的效果。 通常我們說這種場景的特徵是隨著時間發生鉅變的, 所以我們也需要一定頻率的自學習系統來更新模型。 但這個更新模型的過程其實是比較嚴謹的。 它可能需要經過一個資料閉環的流程:

  • 資料迴流:也可以說是資料採集, 需要把資料採集下來才能加入到後續的模型訓練中。
  • 資料預警:需要對採集的資料進行測試, 因為資料本身可能也會出現質量問題, 畢竟它經過了很多道程式,每一道程式可能都會讓資料出現偏差, 一旦出現偏差就會直接影響後面的模型。 這裡透過會透過 flink 或者 spark 這類的手段來對資料進行掃描。
  • 模型訓練:自學習的過程, 利用最新的資料訓練出新的模型。
  • A/B Test:新模型雖然已經訓練出來並且可能也經過了效果的評估, 但仍然需要 A/B Test 來慢慢的讓新模型取代舊模型。比如可以先把 10% 的流量打到新模型上,而其餘的 90% 仍然打到老模型中, 持續對比新老模型的效果。 原則上新模型的效果不能衰退才可以。 就這樣慢慢的切換流量到新模型上, 最終實現完全的取代。
  • 模型上線:模型上線,對接流量, 使用者的資料透過模型的預測又產生了新的資料。
  • 資料迴流:上一步的資料在拼接好 label 後(就是之前說的答案,也是資料標註的過程, 一個資料需要告訴演算法這個資料到底是否是欺詐行為, 演算法才能去學習相關的規律。所以這個資料標註過程是所有監督學習都繞不過去的坎,因為大部分標註行為都需要人力介入,無法自動化完成)經過資料採集系統重新進入迴圈產生新的價值。 這樣我們就實現了資料閉環。

凡是應用了人工智慧的團隊,都在追求構建出上面的資料閉環系統,形成良性迴圈。 而在這個資料閉環系統裡,根據業務形態的不同, 它的週期可能也是不同的。比如在反欺詐系統裡,最後一步的拼接 label 不是可以自動化完成的, 需要人工介入(一條交易記錄最終是否是欺詐行為,需要人來判斷,也就是資料標註的過程還是人來完成的,這就導致資料標註的成本比較高), 並且騙子們的欺詐手段也不會天天都更新,所以它做不到也沒有必要做到高頻率的自學習。 而推薦系統就完全相反, 首先它的資料標註過程是完全可以自動化的,因為只要使用者點選這個廣告/影片/文章/買了產品 了那就算使用者對它感興趣了,所以不需要人來介入標記資料。 再一個是推薦系統必須高頻的自學習, 因為它就是之前說的典型的資料隨著時間發生鉅變的場景,使用者的興趣會隨著時間,社會熱點發生急劇的變化。 所以自學習必須高頻, 高頻到什麼程度呢, 可能高頻到根本來不及做離線測試的程度。 對的,沒有時間給測試人員線上下做效果測試了。 模型的時效性很短,等測試人員磨磨唧唧去測試完後,模型的時效已經過了。 所以 A/B Test 才會顯得那麼重要。 同樣對線上模型的效果監控也變的額外的重要。尤其往往大的推薦系統裡,模型是非常多的。 可能會為每一個客戶都定製一個模型, 比如給阿迪定製一個廣告推薦模型, 耐克又一個, 李寧也有一個, 每個大的廣告客戶都會有一個獨立的模型為他們服務。 所以要線上上組建一個比較龐大的實時效果監控系統, 一旦效果發生衰退, 需要及時告警並處理, 因為這背後都是錢, 跟客戶都簽了合同,要求要保證轉化率。所以假設我用當前的預算投放了一波廣告後,沒有達到預期的效果客戶是不答應的,那麼就要及時對效果進行監控。

結尾

這一篇寫到這就差不多了, 想了想應該覆蓋到我這些年裡跟效果測試有關的大部分場景了,但 AI 場景其實還有很多其他的,只是我都沒有解除大了 ,畢竟 AI 這個領域太大了, 我能接觸到的東西還是有限。 而且其實這次主要也是講了方法論, 實操的東西並不多, 尤其是在資料處理上的實操不多。 做 AI 測試大部分時間都是跟資料打交道的, 只有解決了資料問題, 才輪得到今天講的這些方法論。 這些資料相關的東西就放到下一次寫吧, 想想要寫的東西也挺多的, 得講資料標註的事情, 得講 spark,得講 ffmpeg,得講 opencv,想想就感覺有點累。。。

如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援

相關文章