---
title: "從測試 Stable Diffusion 結果反思軟體測試"
date: 2023-03-08T00:00:00+08:00
publishDate: 2023-03-08T00:00:00+08:00
lastmod: 2023-03-01T13:32:00+08:00
tags: ["測試","心得","AI","人工智慧","Stable Diffusion"]
toc: true
permalink: "https://blog.aotoki.me/posts/2023/03/08/rethink-software-test-by-test-stable-diffusion-result/"
language: "zh-tw"
---


因為已經非常習慣寫軟體測試在[用工程師的方式入門生成式 AI - Stable Diffusion](https://blog.aotoki.me/posts/2023/03/01/learn-generative-ai-stable-diffusion-in-programmer-way/) 這篇文章實作的過程中，我開始思考「Generative AI 的產出能被測試嗎？」這樣的問題。

<!--more-->

## 亂數種子{#random-seed}

如果有在關注 Stable Diffusion 相關的作品，可能會注意到分享的人會附上一個叫做 Seed（種子）的資訊，只要使用相同的 Prompt（提示）和參數，那麼就可以得到相同的結果。

假設排除 Seed 的參數，其他輸入的數值都是固定的但每次產生出來的結果都會非常不一樣，在這樣的狀況下似乎就變得無法進行測試。然而，如果加入 Seed 的參數，就可以解決掉這樣的問題。

> 寫這篇文章剛好朋友傳來網路上的教學影片，發生用了相同 Seed 得到不同結果的差異，在 [PyTorch](https://pytorch.org/docs/stable/notes/randomness.html) 的文件上有說明，理論上只要環境相同應該是能重現的。

實際上這個 Seed 的參數，對大多數的測試框架都可以設定，以 [RSpec](https://rubydoc.info/gems/rspec-core/RSpec%2FCore%2FConfiguration:seed) 為例子，我們可以在跑測試的時候給予 `--seed` 的選項。

```bash
rspec --seed 1000
```

這樣一來，所有 Ruby 中使用到像是 `rand(1000)` 的結果，就會被固定，這是由[亂數表](https://en.wikipedia.org/wiki/Random_number_table)這類機制來產生效果的，因此某些對隨機性非常要求的服務（如：遊戲）還會透過時間、玩家編號、伺服器編號等等資訊，隨時調整 Seed 來確保更好的隨機性。

> 實務上是不推薦使用 `--seed` 來控制隨機結果的，下一個段落會討論這個問題。

## 不確定的軟體{#uncertainty}

從我的角度來看，設計軟體時應該要避免「不確定」的情況發生，像是隨機、當下時間等等，這類資訊通常都應該視為輸入（Input）也就是來自使用者或者環境的資訊。

也許你碰過像是測試隨機失敗，原因是因為「以某個時間點當條件」之類的情境，或者在做遊戲時，因為要有一個隨機的傷害數值，最後放棄驗證這個情況之類的狀況。從我的角度來看，這其實是沒有釐清這類數值的職責。

我們用隨機傷害數值作為例子，以「功能測試」的角度來看，假設最少會有 `1` 點傷害，同時每一點 `STR` 都會等比例換算成傷害，我們是可以這樣驗證的。

```gherkin
Feature: Normal Attack
  Scenario: Player use normal attack and damage enemy
    Given there is a player "Aotoki" with these attributes
      | attribute | value |
      | HP        | 100   |
      | STR       | 10    |
    And there is a monster "Goblin" with these attributes
      | attribute | value |
      | HP        | 100   |
      | STR       | 10    |
    When "Aotoki" click "Normal Attack" button
    Then I can see "Goblin is damaged" between 1 to 10
```

根據邏輯推導出來，我們只需要驗證 `MAX(1, RANDOM() * STR)` 的條件符合，最後就是 `1` 到 `10` 這個範圍就可以，實際上是沒有不確定因素的。

> 在遊戲業中，會用 DPS（Damage Per Second）來評估這類遊戲單一角色的強度來控制平衡，如果把 `1 ~ 10` 大量的重複統計後，應該會接近某個數值，實際上也就「不那麼隨機」如果無法做到，那麼遊戲的體驗就變得不可控。

假設我們繼續拆分功能變成「單元測試」那麼連隨機也就不會出現在這個狀況中，因為我們應該要能有一組固定的公式，我們可以思考以下兩個方法哪一個是更為合理的。

```ruby
# A: Random in method
def calculate_damage(str)
  rand(str)
end

# B: Random in parameter
def calculate_damage(str, ratio)
  str * ratio
end
```

實際上 B 方案是更合理的，那麼我們的 `rand()` 方法應該在哪裡被呼叫呢？取得當下日期、隨機數等等「不確定」的部分，都視為輸入的狀況下，就是在 Use Case（使用案例）的情境下處理。

```ruby
class AttackController < ApplicationController
  # ...
  def normal
    target_id = params[:target_id]
    ratio = rand()
    # ...

    amount = attack_service.calculate_damage(player.str, ratio)
    target.damanged(amount)
    target.save!
  end
end
```

像這樣子我們盡可能把「不確定」的資訊往使用者端靠近就可以讓單元測試不需要去處理「不確定」的狀況，這也讓我們的「商業邏輯」是有邏輯的，畢竟我們不會預期一套系統的結果永遠是「隨機」的，即使是遊戲中的所有數值也都能夠「統計」出來。

那麼，為什麼 Stable Diffusion 會提供一個 Seed 的選項，是不是把「隨機」這件事情從 Use Case 繼續往外推，作為一個輸入的選項讓使用者來決定，來增加可控的程度。

## 脈絡的影響{#the-context}

解決了隨機的問題後，我們來看 Prompt 的效果是受到 Context（脈絡）有的影響。我們能產生怎樣的圖像，是由 Model（模型）加上 Prompt 來決定的，因此使用不同的 Model 來生成圖像，能發揮作用的 Prompt 也不盡相同，這就跟我們在描述需求一樣，越完善的脈絡越能被正確測試。

我們可以先來看一下這兩張圖片，我使用了相同的 Prompt 和參數，但是是基於不同的 Model 來生成（下方是加入 Negative Prompt 排除後的效果）

![結果差異](images/01.png)

差異是肉眼可見的明顯，造成影響的就是 Model 不同，提供給 AI 的 Context 就完全不一樣，那麼後續對相同的 Prompt 得出的結論就會不同，最後得到不一樣的結果。

更多人知道的可能是 ChatGPT 的 AI，實際上目前我們使用的都是叫做 Completion（補完）的機制，常見的使用做法是會先給予一些樣板作為 Context 然後再給予 Prompt 來「補齊缺少的語句」

舉一個有趣的例子是，如果你問 ChatGPT「今天是幾月幾號」他是無法回答你的，因此用「今天有什麼新聞」會無法使用，然而如果改為「假設今天是 2021 年 1 月 1 日，發生了什麼事？」那麼就相對精準的得到當天的一些事件，這是因為我們把「時間點」作為輸入提供進去的關係。

> 詢問當天日期在 ChatGPT 上來說可能是能回答的，因為 ChatGPT 很可能不是單純的利用模型生成，可能會根據詢問日期推導出「查詢」的意圖，並且呼叫一些輔助工具來解決問題（Meta 提出的語言模型就具備這樣的能力）

這些模型我們可以把它視為 Domain Knowledge 的概念，所以 OpenAI 公司針對 ChatGPT 類型的語言模型，提供了 Fine Turning（微調）的機制，讓我們可以預先加入相關的知識來符合特定情境。

我們以前面戰鬥的例子來看，直接讓 ChatGPT 回答會要求給予更多上下文（Context 的另一種翻譯）然而給予功能測試後，就能根據上下文推斷出可能的結果。

![脈絡影響](images/02.png)

這其實也是影響我們在撰寫測試的品質的另一個關鍵，我們的軟體是建構在某個特定領域下的，這個產業會有很多預設的 Context 存在，如果沒有正確的獲取這些資訊，我們是很撰寫出正確的測試，會有過多不確定的條件限制在裡面。

回到對 Stable Diffusion 這類生成式 AI 的結果做測試這件事情上，我們會發現測試本身其實並不困難，反而是現在要還原能夠產生相同結果的環境（有 GPU 的測試伺服器，而且使用的模型還要相同）才是最為困難的地方。

