把 OpenAI 的 ChatGPT 搬到 iOS 上
目錄
⦿ ChatGPT
⦿ OpenAI API key
⦿ Curl
⦿ Playground
⦿ Parameter
⦿ Xcode
⦿ 解析json
⦿ UI設定
ChatGPT
搭上 ML 模型的熱潮,前陣子研究了一下 Glicko,這幾天玩了一下大家都在討論的 ChatGPT,也從 彼得潘的 iOS App Neverland 那裡看到測試文章驚奇不斷,最後決定搬到手機上來體驗。
ChatGPT 是 OpenAI 開發的人工智慧聊天機器人的程式,使用 GPT-3 這個神經網路模型,GPT 是 Generative Pre-trained Transformer;OpenAI 創始人之一是 Elon Musk,那天,朋友 Peggy Tsai 看到馬斯克的新聞,問我是不是臉書的創始人,我想了想,他一定是在跟我開玩笑吧。
當你連上網站並註冊後,就可以開始提問了,把它想像成是雲端情人的 Samantha 的聲音(Scarlett Johansson),人生頓時光明了起來,事實上,在 iOS 中,我們也可用 AVFoundation、AVSpeechSynthesizer 來朗讀結果,但這篇文章沒有要做這樣的事。先看到下方網頁:
許多人測試時會希望 AI 做到現階段它還不能做到的事,而不是關注在它能做到的事,但模型是有條理的,瞭解它,會比有一天來不及瞭解它還來得好,其實在人與人之間也是這樣。
若希望把 ChatGPT 搬到 iOS 上,剛好 OpenAI 也開了 API 給我們,於是就可用 Restful API 的方式去呈現,先看結果吧:
看起來很醜,但很可愛吧?
繼續閱讀|回目錄
OpenAI API key
當你申請完帳號,就必須到這個網頁去 Create new secret key,如下:
Curl
接著,我們可以從這個網頁中,瞭解 curl 指令,如下:
curl https://api.openai.com/v1/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{
"model": "text-davinci-003",
"prompt": "Say this is a test",
"max_tokens": 7,
"temperature": 0
}'
可以看到它的 HTTP header 中,Content-Type 是 json,Authorization 的 Bearer 後面則要放入你剛才產生的 API key;而 HTTP body 中 data 帶了 model、prompt、max_tokens、temperature 等訊息,是 json 格式。
在你放入 API key 後,將這段 curl 指令複製起來,貼到 CLI 中,就會得到下面的回傳結果:
{
"id":"cmpl-6Khe2owgvY4i8TIaJZPK1tPHK9zAA",
"object":"text_completion",
"created":1670392350,
"model":"text-davinci-003",
"choices":[{"text":"\n\nThis is indeed a test",
"index":0,
"logprobs":null,
"finish_reason":"length"}],
"usage":{"prompt_tokens":5,
"completion_tokens":7,
"total_tokens":12}
}
我們看到 choices 這個 key 的值中的 text 的值為,兩個空行,This is indeed a test
,這個就是我們要的回傳結果。
等等要在 Xcode 中,用 JSONDecoder 去轉成我們要的資料。
Playground
我們也可到這個網頁使用它的 Playground,點擊 View code 後,同樣選擇 curl,結果如下:
我們看到對 https://api.openai.com/v1/completions
request,有同樣的請求頭,不過夾帶的 data 跟前段比,多了一些參數。
max_tokens
max_tokens
關乎最後回應的字數,它的計算方式不是規律地依照字元,有興趣的可以到這個網頁去測試看看,如下:
不過不同的 model,有不同的 max_tokens,我們也可以連到這個網頁去看看,如下:
若用 token 來簡單判斷,text-davinci-003
算是裡面最厲害的 model 吧。
temperature、top_p
這兩個參數,官方建議不要都改變,只改變其中一個,他們關乎最後得到的答案是制式的答案,還是具創造性的,就像設定 AI 的性格一樣,在統計學上,像是樣本的信賴區間,改變值會改變答案的精確度。
Xcode
回到正題,我們在 Xcode 創建一個新的專案,建立一個新的 swift 檔,在裡面加入下面的程式碼:
struct ChatGPTAPIKey {
static let key = "你的 API key"
}
struct AIModel {
static let model = "text-davinci-003"
}
struct OpenAIBody: Encodable {
let model: String
let prompt: String
let temperature = 0.0
let max_tokens = 256
let top_p = 1.0
let frequency_penalty = 0.0
let presence_penalty = 0.0
}
在 ViewController 中,另外寫一個方法,再放到 viewDidLoad( ) 中呼叫,如下:
private let urlString = "https://api.openai.com/v1/completions"
override func viewDidLoad() {
super.viewDidLoad()
callChatGPTAPI()
}
private func callChatGPTAPI() {
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.setValue("application/json",
forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(ChatGPTAPIKey.key)",
forHTTPHeaderField: "Authorization")
let openAIBody = OpenAIBody(model: AIModel.model,
prompt: "How’s going?")
request.httpBody = try? JSONEncoder().encode(openAIBody)
request.httpMethod = "post"
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let content = String(data: data, encoding: .utf8) {
print(content)
}
}.resume()
}
在 request 中,把請求頭該有的東西放進去後,我們還要在 httpBody 裡放入 openAIBody,在前段中,我們知道要用 JSONEncoder()
,httpMethod 是 “post”
。
接著將 request 放入 URLSession 中,記得 .resume()
,印出轉成 string 的 data(即 content),如果你只印 data,會得到 data 的大小。
最後會得到這樣的結果:
{
"id":"cmpl-6Kj3NlV8nESELoTbWn7iSUWtp9hjO",
"object":"text_completion",
"created":1670397765,
"model":"text-davinci-003",
"choices":[{"text":"\n\nIt's going well, thank you. How about you?",
"index":0,
"logprobs":null,
"finish_reason":"stop"}],
"usage":{"prompt_tokens":6,
"completion_tokens":14,
"total_tokens":20}
}
跟前段 curl 的結果是不是一樣呢?那麼,剩下的就是我們熟悉的解析 json 了。
繼續閱讀|回目錄
解析json
我們再開一個 swift 檔案,加入下面程式碼:
struct JsonData: Codable {
let id: String
let object: String
let created: Int
let model: String
let choices: [ChoiceSet]
}
struct ChoiceSet: Codable {
let text: String
}
由於需要的只是 choices 裡的 text,所以盡可能簡化程式碼,最後再把下面的程式碼,放到剛剛的 URLSession 中:
do {
let data = try JSONDecoder().decode(JsonData.self, from: data)
DispatchQueue.main.async {
self.answerTextView.text = data.choices[0].text
}
} catch {
print(error.localizedDescription)
}
解析出 data 後,讓 textView 顯示,結果如下:
是不是很棒呢?
繼續閱讀|回目錄
UI設定
如果你的 UI 長這樣:
程式裡可能會有這樣的宣告與設定:
@IBOutlet weak var questionTextView: UITextView!
@IBOutlet weak var answerTextView: UITextView!
private let urlString = "https://api.openai.com/v1/completions"
private let answerHint = "\n\n 讓我思考一下..."
@IBAction func clearBtnTapped(_ sender: UIButton) {
// 自己寫
}
@IBAction func sendBtnTapped(_ sender: UIButton) {
// 自己寫
}
在按下清空按鈕的動作裡,兩個 textView 要清空,接著,通常還希望鍵盤會收回,於是會加上 view.endEditing(true)
。
而 answerTextView 不希望被編輯,所以在 Attributed Inspector 中取消 editable
,如下:
// 點擊旁邊鍵盤收回
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
questionTextView.resignFirstResponder()
}
extension ViewController: UITextViewDelegate {
// 按下 return 後,鍵盤收回
func textView(_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String) -> Bool {
if text == "\n" {
view.endEditing(false)
}
return true
}
}
再來,我們也希望 textView 輸入時,在按下 return 後,鍵盤會收回,就會遵循 UITextViewDelegate,利用 textView 的 function 來做到;如果希望按到旁邊時,鍵盤也會收回,就會覆寫 touchesEnded 這個 function。
回到 callChatGPTAPI()
,這時候你已經不希望在 viewDidLoad() 中呼叫它,而是希望在按下送出按鈕時呼叫它。
但我們要改掉 OpenAIBody 的 prompt 為 text,即是你輸入的問題,如下:
// 新問題等待答案時的提示
DispatchQueue.main.async {
self.answerTextView.text = self.answerHint
}
// 把新問題放到 httpBody 裡
let text = questionTextView.text!
let openAIBody = OpenAIBody(model: AIModel.model,
prompt: text)
完成了!這次分享就到這,感謝您的閱讀。
繼續閱讀|回目錄
Reference: