Show HN: Outlines - เครื่องมือที่รับประกันว่า LLM จะสร้างเอาต์พุตแบบมีโครงสร้าง เช่น JSON ที่ถูกต้อง
(github.com/normal-computing)- Outlines เป็นเครื่องมือสำหรับ Structured Output ของ LLM ที่ให้กำหนดชนิดเอาต์พุตที่ต้องการระหว่างการสร้าง เพื่อรับประกันว่าข้อมูลจะตรงกับโครงสร้างนั้นอย่างถูกต้อง
- แทนที่จะใช้การ parse, regular expression หรือโค้ดที่เปราะบางเพื่อคอยแก้เอาต์พุตที่ผิดหลังสร้างเสร็จ เครื่องมือนี้บังคับใช้ โครงสร้างที่ถูกต้อง ได้โดยตรงตั้งแต่ขั้นตอนการสร้าง
- วิธีใช้งานอยู่ในรูป
model(prompt, output_type)และสามารถกำหนดเอาต์พุตให้สอดคล้องกับระบบชนิดของ Python ได้ เช่นLiteral["Yes", "No"],int, หรือ Pydantic model - ประเภทเอาต์พุตที่รองรับ ได้แก่ ตัวเลือกที่กำหนดไว้ล่วงหน้า, Function Calls ที่อิงจาก function signature, JSON/Pydantic schema, รูปแบบ regular expression และการบังคับโครงสร้างตามไวยากรณ์ (grammars)
- การเชื่อมต่อกับโมเดลแบ่งเป็น vLLM·Ollama สำหรับเซิร์ฟเวอร์, transformers·llama.cpp สำหรับโมเดลในเครื่อง และ OpenAI·Gemini·Dottxt สำหรับ API
- เวิร์กโฟลว์ตัวอย่าง ได้แก่ การแปลงอีเมลลูกค้าเป็น service ticket, การแปลงคำอธิบายสินค้าเป็นข้อมูลหมวดหมู่, การดึงข้อมูลแบบมีโครงสร้างจากคำอธิบายอีเวนต์ที่ไม่สมบูรณ์หรือคืนค่า
"I don't know", การจัดหมวดหมู่เอกสารตามหมวดที่กำหนดไว้ล่วงหน้า และการแปลงคำขอประชุมภาษาธรรมชาติเป็นพารามิเตอร์ของฟังก์ชัน - พรอมป์ตเทมเพลตรองรับ
outlines.Template.from_stringที่อิง Jinja และการโหลดจากไฟล์ เพื่อแยกพรอมป์ตที่ซับซ้อนออกจากโค้ดและนำกลับมาใช้ซ้ำ - ติดตั้งได้ด้วย
pip install outlinesและ.txt APIถูกระบุว่าอยู่ในสถานะ early access ในขณะนี้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ไลบรารีนี้ดูเหมือนใช้ไอเดียเรียบง่ายตามกลไก คือ มาสก์บางส่วนของพื้นที่คำศัพท์ แล้วดำเนินไปอย่างมีประสิทธิภาพทีละขั้นเวลา ซึ่งยอดเยี่ยมมาก
แต่ในฐานะคนที่เคยใช้ไลบรารีที่ครอบโครงสร้างเอาต์พุตให้กับ LLM พื้นฐาน ผมสงสัยว่าโมเดลพื้นฐานอย่าง Llama2 จะทำงานได้ดีจริงหรือไม่
จากประสบการณ์ของผม มันใกล้เคียงกับคำว่า “ไม่เลย” และถ้าจะให้ใช้งานได้จริง จำเป็นต้องมี instruction tuning ที่ปรับให้เข้ากับงานเฉพาะพอสมควร
ยิ่งไปกว่านั้น การมาสก์ state space ภายหลังระหว่างการสร้างกับโมเดลที่ผ่าน instruction tuning แล้ว สุดท้ายก็เท่ากับเปลี่ยนการแจกแจงของการสร้าง ซึ่งดูขัดกับสัญชาตญาณ เพราะอาจส่งผลเสียต่อ instruction tuning ได้
สำหรับประเด็นที่สอง ถ้าเป้าหมายคือ เช่น ให้โมเดล สร้างเฉพาะ JSON ก็ทำได้ 100% ด้วยวิธีจำกัดว่าโทเคนเอาต์พุตใดใช้ได้หรือใช้ไม่ได้
ผมใช้ GPU บน M2 Mac ได้ เลยใช้เวอร์ชัน MLC ผ่านปลั๊กอิน https://github.com/simonw/llm-mlc
ในงานวิจัย “Guiding Language Models of Code with Global Context using Monitors”(https://arxiv.org/abs/2306.10763) ได้เสนอ Monitor Guided Decoding ซึ่งเชื่อม LLM เข้ากับการวิเคราะห์แบบสถิต เพื่อชี้นำให้สร้างโค้ดที่มีความสอดคล้องด้านชนิดข้อมูล
โดยไม่ต้องทำ fine-tuning ใด ๆ แค่ใช้การวิเคราะห์แบบสถิตชี้นำการสร้างระดับโทเคน ณ จุดเฉพาะ ก็ทำให้คุณภาพโค้ดที่สร้างดีขึ้นมาก ทั้งในแง่การคอมไพล์ได้และความตรงกับคำตอบ แม้แต่โมเดลขนาดเล็กมาก (1.1B) ก็สร้างโค้ดที่คอมไพล์ได้มากกว่าโมเดลที่ใหญ่กว่ามาก (175B) พร้อมทั้งทำให้ความตรงกับคำตอบดีขึ้นด้วย
ความต่างหลักมีเพียงฟังก์ชันที่สร้างผลลัพธ์ไม่ใช่ LLM แต่เป็นมนุษย์เท่านั้น ผมไม่เห็นเหตุผลว่าทำไมจึงไม่ควรตัดคนกลางออกไป
ในโค้ดแบบดั้งเดิม เงื่อนไขขอบเขตเป็นกรณีพิเศษเล็ก ๆ ตามตัวอักษร แต่ใน LLM เราไม่รู้ว่าอะไรจะทำให้มันออกนอกลู่นอกทาง และโค้ดพาร์สต้องรับมือกับความโกลาหลนั้น
พูดอีกอย่างคือ รู้สึกว่าสัดส่วนของเคสที่ต้องถือเป็นเงื่อนไขขอบเขตเพิ่มขึ้นอย่างมาก
GPT-4 สามารถทำให้คืนค่า JSON ที่ถูกต้อง ได้เพียงใส่ตัวอย่างใน system message และใช้งานได้ประมาณเก้าครั้งจากสิบครั้ง
แต่มันยังคงเป็นเชิงความน่าจะเป็น และเก้าครั้งจากสิบครั้งยังไม่เพียงพอ
บางครั้งมันก็สร้างคำตอบหลอน ๆ เช่น
{"key1": "value1", "key2": "value2" for i in range(n)}หาก prompt อีกครั้งพร้อมข้อความข้อผิดพลาดจากการพาร์ส ปกติก็จะแก้ได้ในครั้งที่สอง
แต่การ escape เครื่องหมายอัญประกาศคู่และอักขระขึ้นบรรทัดใหม่ยังไม่ค่อยเสถียร แม้ให้ตัวอย่างหลายตัวอย่าง ก็ escape ได้ถูกต้องเพียงประมาณครึ่งหนึ่ง และแม้ prompt ซ้ำด้วยข้อผิดพลาดเรื่องการ escape อัตราสำเร็จก็ยังอยู่ราว 50%
prompt ดังกล่าวอยู่ที่นี่: https://github.com/microsoft/TypeChat/blob/c45460f4030938da3...
ผมมองว่าแนวทางอิงไวยากรณ์ที่เห็นที่นี่ หรือวิธีแบบ https://github.com/ggerganov/llama.cpp/pull/1773 เป็นทางออกที่สง่างามกว่ามาก
ผมเข้าใจว่าผู้คนต้องการ JSON แต่สำหรับผมมันคล้ายกับการพยายามพาแมวไปว่ายน้ำ สุดท้ายอาจทำสำเร็จได้ แต่ไม่ใช่ธรรมชาติของมัน
ความผิดพลาดหลักคือการไม่เตรียมทางออกไว้ LLM พยายามหาคำตอบที่ถูกต้อง ดังนั้นเมื่อเราใส่ข้อความเข้าไปแล้วสั่งให้คืนข้อมูลแบบมีโครงสร้าง แต่มีข้อความบางส่วนว่างเปล่า มันจะตัดสินคำตอบที่ถูกต้องได้ยากและเกิดอาการหลอน
ทางแก้คือสร้างทางออกไว้ เช่นใส่บูลีนอย่าง
textIsMissingเป็นหนึ่งในอาร์กิวเมนต์ ถ้าคำนึงถึงรูปแบบความล้มเหลวแบบนี้ไว้ ก็ทำงานได้ไร้ที่ติลองใหม่หลายครั้งและให้แก้ไขต่อเนื่องแล้วก็ยังไม่ผ่านการตรวจสอบ และไม่เคยสร้าง JSON ที่ถูกต้อง 100% ได้แม้แต่ครั้งเดียว สุดท้ายจึงยอมแพ้
ข้อแรก ไม่ต้องใส่ตัวอย่างจำนวนมากใน prompt จึงใช้โทเคนน้อยลง
ข้อสอง ได้รับผลกระทบจากปัญหาการลืมน้อยลง
ข้อดีเล็ก ๆ คือสามารถควบคุมได้อย่างแม่นยำว่าเอาต์พุตที่ต้องการควรเริ่มตรงไหน อย่างไรก็ตาม โดยรวมแล้วผมมองว่าเป็นฟีเจอร์เสริมที่ดี ไม่ใช่สิ่งที่เป็นแก่นสารมากนัก
พลังส่วนใหญ่ของ LLM อยู่ที่ การแจกแจงความน่าจะเป็นที่ปรับเทียบแล้ว ของคำตอบ แต่เทคนิคนี้ดูเหมือนอาจทิ้งความสามารถนั้นไป ผมสงสัยว่าทำไมแค่นี้ถึงเพียงพอ
ตัวอย่างง่าย ๆ สมมติว่าเอาต์พุตที่เป็นไปได้ของ LLM มีแค่ “hello world”, “food”, “hello”, “good day” และเมื่อไม่มีพรอมป์ ทุกอันมีความน่าจะเป็นเท่ากัน สมมติว่าไวยากรณ์กำหนดข้อจำกัดแค่ว่าต้องมีช่องว่างอยู่ที่ไหนสักแห่งในเอาต์พุต
ถ้าสุ่มเอาต์พุตจาก LLM จนกว่าจะผ่านไวยากรณ์ ก็จะได้ “hello world” และ “good day” ด้วยความน่าจะเป็นเท่ากัน แต่ถ้าใช้เทคนิคบนเว็บไซต์ “hello world” จะออกมาบ่อยกว่า “good day” สองเท่า
ปัญหาหลักคือ แม้คำนำหน้าของคำตอบบางแบบอาจมีโอกาสน้อยมากที่จะนำไปสู่คำตอบที่ถูกต้อง แต่เทคนิคนี้กลับสร้างคำตอบที่ถูกต้องจากคำนำหน้านั้นได้ โดยถือว่าสุดท้ายจะสำเร็จ ถ้ามันเป็นอิสระต่อกันพอในจุดที่เหมาะสมก็คงไม่เป็นไร แต่ในโมเดลแบบ autoregressive ข้อผิดพลาดที่สัมพันธ์กันจะสะสมอย่างรวดเร็ว
ถ้าถามจำกัดเฉพาะ JSON: เวลาที่ LLM สร้างคำตอบที่รักษา schema ไม่ได้ มันจะสร้างข้อผิดพลาดด้านข้อเท็จจริง ภาพหลอน สตริงที่ถูกตัดขาด การตกหล่นตัวละครหลัก ฯลฯ มากขึ้นหรือน้อยลง? ถ้าอัตราข้อผิดพลาดด้านข้อเท็จจริงเกี่ยวข้องกับอัตราข้อผิดพลาดของ schema อย่างไม่เป็นเรื่องง่าย เส้นทางนี้ก็อันตรายกว่าที่เห็น เมื่อเห็นว่าคำบางคำหรือวลีที่ติดกันส่งผลอย่างมากต่อเอาต์พุตของ LLM ก็ดูมีความเป็นไปได้สูงว่ารายละเอียดอย่างการปฏิบัติตาม schema จะลามไปถึงคุณลักษณะอื่น ๆ ของเอาต์พุตด้วย
ผมลองคิดตัวอย่างของ “คำนำหน้าคำตอบมีโอกาสน้อยมากที่จะนำไปสู่คำตอบที่ถูกต้อง แต่เทคนิคนี้สร้างคำตอบที่ถูกต้องขึ้นมาได้ด้วยวิธีใดวิธีหนึ่งจนก่อปัญหา” แต่ยังนึกไม่ออก ถ้ามีตัวอย่างดี ๆ ก็น่าจะเป็นคำถามวิจัยที่น่าสนใจ
ที่เกี่ยวข้องคือ LLama.cpp เพิ่ง implement การสุ่มตัวอย่างตามไวยากรณ์เมื่อเดือนที่แล้ว
https://news.ycombinator.com/item?id=36819906
https://github.com/ggerganov/llama.cpp/pull/1773
วิธีของเรามีประสิทธิภาพกว่ามาก llama.cpp จะวนผ่านคำศัพท์ทั้งหมดราว 50,000 โทเค็นในแต่ละขั้นเพื่อสร้าง mask
เราสร้าง index ตอน initialize และในแต่ละขั้นตอนเมื่อประกอบ mask ก็แค่ lookup ใน dictionary เป็นการแลกความเร็วกับหน่วยความจำ และการสุ่มตัวอย่างเร็วพอ ๆ กับการสุ่มตัวอย่างมาตรฐาน
จากที่มีงานวิจัยที่เกี่ยวข้องจำนวนมาก ก็น่าจะมีที่อื่นอีกหลายแห่งเช่นกัน ประเด็นของที่นี่และงานปัจจุบันคือการให้การชี้นำที่มีต้นทุนต่ำมาก และสำหรับกรณี regex เรามี implementation อยู่มาพักหนึ่งแล้ว ก่อนจะขยายไปเป็น JSON ภายหลัง
ขอบคุณที่ทำสิ่งนี้ขึ้นมา และหลักการทำงานก็เป็นไอเดียที่ชัดเจนมากจนแปลกใจที่ แพลตฟอร์มระดับเฟิร์สต์ปาร์ตี้ ยังไม่ได้ทำ
นอกจาก JSON แล้ว ผมสงสัยว่าจะเอาไปใช้กับงานอื่น ๆ ที่ต้องการอินพุตแบบมีโครงสร้างได้อย่างไร
อาจเป็นผลจากทรัพยากรนักพัฒนาที่จำกัดก็ได้ ถ้าเทคโนโลยีอายุ 10 ปีขาดฟีเจอร์พื้นฐานแบบนี้ก็คงน่าแปลกใจ แต่สำหรับเทคโนโลยี AI ที่กำลังอยู่ในช่วงแข่งขันสะสมอาวุธ การที่ยังขาดฟีเจอร์อำนวยความสะดวกบางอย่างก็ดูเข้าใจได้
ผมไม่ค่อยแน่ใจว่าสิ่งนี้ต่างจากโปรเจกต์ต่อไปนี้อย่างไร
https://github.com/1rgs/jsonformer
https://github.com/newhouseb/clownfish
https://github.com/mkuchnik/relm
https://github.com/ggerganov/llama.cpp/pull/1773
https://github.com/Shopify/torch-grammar
โดยรวมแล้ว ระบบชี้นำตาม logits แบบนี้มีอยู่เยอะมาก และเหตุผลที่มันไม่ได้รับ traction มากนักก็คือโมเดลประสิทธิภาพสูงสุดอยู่หลัง REST API ที่ไม่เปิดให้ใช้แนวทางละเอียดระดับนี้
โมเดลเหล่านั้นทำงานได้ดีกว่ามาก ผู้คนจึงมักพอใจกับการขอใหม่จนกว่าจะได้รูปแบบที่ถูกต้อง จากประสบการณ์ของผมกับ GPT-4 ความล้มเหลวแบบนั้นก็ค่อนข้างพบได้น้อยด้วย
เราวนผ่านคำศัพท์หนึ่งครั้งตอน initialize เพื่อสร้าง index แล้วหลังจากนั้นการสร้างก็เร็วพอ ๆ กับการสร้างมาตรฐาน
อธิบายอีกแบบคือ ทุกครั้งที่ LLM สร้างโทเค็นหนึ่งตัว จะอัปเดต “มาสก์” logit bias เพื่อให้โทเค็นถัดไปเป็นได้เฉพาะ โทเค็น JSON ที่ถูกต้อง เท่านั้นใช่ไหม? เจ๋งมาก
ยังไม่มั่นใจว่าวิธีนี้จะรับประกันได้ 100% จริง ๆ อย่างไร
แก้ไข: ในเปเปอร์อธิบายส่วนนี้ไว้อย่างระมัดระวังกว่านี้
Brandon Willard คนนี้คือ Brandon Willard นักเบรกแดนซ์จาก Detroit หรือเปล่า?
แก้ไข: ใช่! https://brandonwillard.github.io/
remilouf จากที่บอกว่าพื้นหลังของตัวเองคือ “การเขียนโปรแกรมเชิงความน่าจะเป็น เชิงสัมพันธ์ และเชิงสัญลักษณ์” ก็น่าจะเข้าใจว่าการสร้างข้อความด้วยไวยากรณ์ปกติหรือไวยากรณ์ไม่พึ่งบริบท หรือจริง ๆ แล้วไวยากรณ์แทบทุกระดับนั้นไม่ใช่ปัญหา
ตัวอย่างเช่น ในภาษาเชิงสัมพันธ์อย่าง Prolog ถ้ามีเพียงไวยากรณ์ในรูปแบบ Definite Clause Grammars ก็ทำได้ง่ายมาก
เท่าที่ผมมอง แนวทางนี้ต้องให้ผู้ใช้จัดเตรียมไวยากรณ์ให้ ถ้าอย่างนั้นก็สงสัยว่าข้อดีของการใช้ LLM เพื่อสร้างข้อความคืออะไร
ทำไมไม่รันไวยากรณ์เป็นตัวสร้างเพื่อผลิตข้อความที่ต้องการไปเลย? แบบนั้นจะลดความพยายามและต้นทุนมหาศาลในการฝึก LLM ตั้งแต่แรกได้ด้วย ถ้าต้องการสร้างแค่ข้อความที่มีโครงสร้าง ก็ยังสงสัยว่าทำไมถึงต้องใช้ LLM ซึ่งเป็นโมเดลภาษาธรรมชาติ
เหตุผลที่ยังต้องใช้ LLM แม้ต้องการแค่ข้อความที่มีโครงสร้าง คือเพื่อพาร์สข้อความไม่เป็นโครงสร้างที่มนุษย์เขียน แล้วคืนข้อมูลมีโครงสร้างที่เครื่องใช้ได้
ถ้าคำขอคือ “เลข 10 หลักแรกของพายคืออะไร?” และจำกัดคำตอบด้วย regex
"[0-9]+\.[0-9]+"เป้าหมายคือการได้คำตอบจริงคือ 3.1415926535 ไม่ใช่สตริงสุ่มอย่าง “1.2346789” ที่แค่ตรงแพตเทิร์นน่าสนใจ และเมื่อไม่นานมานี้พวกเราก็ทำเครื่องมือคล้าย ๆ กันที่จำกัดเอาต์พุตของ llama ให้ตรงกับ TypeScript interface[1]
ผมเชื่ออย่างหนักแน่นว่าการรับประกันรูปแบบเอาต์พุตจะสำคัญตลอดหลายทศวรรษข้างหน้า เมื่อ LLM ถูกนำไปใช้งานจริงที่ไม่ใช่แค่ของเล่น
[1] https://github.com/ggerganov/llama.cpp/discussions/2494