7 คะแนน โดย GN⁺ 2025-03-06 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

วิธีที่ดีที่สุดในการใช้ text embeddings แบบพกพาได้คือ Parquet และ Polars

  • text embeddings คือเวกเตอร์ที่สร้างจากโมเดลภาษาขนาดใหญ่ ซึ่งใช้แทนคำ ประโยค และเอกสารในรูปแบบตัวเลข
  • ณ เดือนกุมภาพันธ์ 2025 ได้สร้าง embeddings ของการ์ด "Magic: The Gathering" ทั้งหมด 32,254 ใบ
  • ทำให้สามารถวิเคราะห์ความคล้ายคลึงกันทางคณิตศาสตร์ได้จากการออกแบบและคุณสมบัติเชิงกลไกของการ์ด
  • embeddings ที่สร้างขึ้นสามารถนำไปแสดงผลด้วยการลดมิติแบบ 2D UMAP
  • โมเดล embedding ที่ใช้คือ gte-modernbert-base และกระบวนการโดยละเอียดถูกรวบรวมไว้ใน GitHub repository
  • ชุดข้อมูล embedding นี้เผยแพร่บน Hugging Face

ทบทวนความจำเป็นของ vector database

  • โดยทั่วไปมักใช้ vector database (faiss, qdrant, Pinecone) เพื่อจัดเก็บและค้นหา embeddings
  • แต่ vector database ต้องการการตั้งค่าที่ซับซ้อน และบริการคลาวด์อาจมีค่าใช้จ่ายสูง
  • หากเป็นข้อมูลขนาดเล็กในระดับหลายหมื่นรายการ ก็สามารถใช้ numpy เพื่อค้นหาความคล้ายคลึงได้อย่างรวดเร็วโดยไม่ต้องใช้ vector database
  • หากใช้การคำนวณ dot product ของ numpy ก็สามารถคำนวณ cosine similarity แบบง่าย ๆ ได้ โดยใช้เวลาเฉลี่ย 1.08ms สำหรับ embeddings 32,254 รายการ
def fast_dot_product(query, matrix, k=3):  
    dot_products = query @ matrix.T  
  
    idx = np.argpartition(dot_products, -k)[-k:]  
    idx = idx[np.argsort(dot_products[idx])[::-1]]  
  
    score = dot_products[idx]  
  
    return idx, score  
  • การใช้ vector database มักเพิ่มความเสี่ยงที่จะผูกติดกับไลบรารีหรือบริการเฉพาะ
  • หากสร้าง embeddings บนเซิร์ฟเวอร์ GPU แล้วดาวน์โหลดกลับมาไว้ในเครื่อง ก็จำเป็นต้องมีวิธีจัดเก็บและส่งผ่านข้อมูลที่มีประสิทธิภาพ

วิธีจัดเก็บ embeddings ที่แย่ที่สุด

  • ไฟล์ CSV
    • หากเก็บข้อมูลทศนิยมแบบลอยตัว (float32) เป็นข้อความ ขนาดจะเพิ่มขึ้นมากกว่า 6 เท่า
    • แม้แต่ บทช่วยสอนทางการ ของ OpenAI ก็แนะนำให้ใช้ CSV เฉพาะกับชุดข้อมูลขนาดเล็ก
    • หากบันทึกด้วย .savetxt() ของ numpy ขนาดไฟล์จะเพิ่มเป็น 631.5MB
  • ไฟล์ pickle
    • บันทึกและโหลดได้รวดเร็ว แต่มีความเสี่ยงด้านความปลอดภัยและความเข้ากันได้ระหว่างเวอร์ชันต่ำ
    • ขนาดไฟล์คือ 94.49MB เท่ากับขนาดหน่วยความจำต้นฉบับ แต่มีความสามารถในการพกพาต่ำ

วิธีจัดเก็บที่ไม่แย่ แต่ยังไม่เหมาะที่สุด

  • รูปแบบ .npy ของ numpy
    • สามารถป้องกันการจัดเก็บแบบ pickle ได้ด้วยการตั้งค่า allow_pickle=False
    • ขนาดไฟล์และความเร็วเท่ากับวิธี pickle แต่ยากต่อการจัดเก็บ metadata รายรายการร่วมกัน
  • ปัญหาของโครงสร้างการจัดเก็บที่แยก metadata ออกจากกัน
    • หากเก็บเป็นอาร์เรย์ numpy (.npy) ข้อมูลการ์ด เช่น ชื่อ ข้อความ ฯลฯ จะถูกแยกออกจาก embeddings
    • เมื่อข้อมูลมีการเปลี่ยนแปลง (เพิ่ม/ลบ) จะจับคู่ metadata กับ embeddings ได้ยาก
    • ใน vector database นั้น metadata และเวกเตอร์จะถูกเก็บร่วมกันและมีความสามารถด้านการกรอง

วิธีจัดเก็บ embeddings ที่เหมาะที่สุด: Parquet + polars

แนะนำรูปแบบไฟล์ Parquet

  • Apache Parquet คือรูปแบบจัดเก็บข้อมูลแบบคอลัมน์ ซึ่งสามารถกำหนดชนิดข้อมูลของแต่ละคอลัมน์ได้อย่างชัดเจน
  • สามารถเก็บข้อมูลแบบลิสต์ (อาร์เรย์ float32) ได้ จึงเหมาะกับการเก็บ embeddings
  • ให้ประสิทธิภาพในการบันทึกและโหลดที่ดีกว่า CSV และสามารถเลือกโหลดเฉพาะบางส่วนของข้อมูลได้
  • แม้จะมีความสามารถในการบีบอัด แต่ข้อมูล embedding มีความซ้ำต่ำจึงได้ประโยชน์จากการบีบอัดไม่มาก

การใช้ไฟล์ Parquet ใน Python

  • การบันทึกและโหลดไฟล์ Parquet ด้วย pandas:
    df = pd.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])  
    df  
    
    • pandas จัดการข้อมูลแบบ nested (ลิสต์) ได้ไม่ดีนัก และจะถูกแปลงเป็น numpy object
    • เมื่อต้องแปลงเป็นอาร์เรย์ numpy จะต้องมีการประมวลผลเพิ่ม (np.vstack()) ซึ่งอาจทำให้ประสิทธิภาพลดลง
  • การบันทึกและโหลดไฟล์ Parquet ด้วย polars:
    df = pl.read_parquet("mtg-embeddings.parquet", columns=["name", "embedding"])  
    df  
    
    • polars จะคงอาร์เรย์ float32 ไว้ตามเดิม และเมื่อเรียก to_numpy() ก็สามารถคืนค่าเป็นอาร์เรย์ numpy แบบ 2 มิติได้ทันที
    • สามารถป้องกันการคัดลอกข้อมูลที่ไม่จำเป็นได้ด้วยการตั้งค่า allow_copy=False
    embeddings = df["embedding"].to_numpy(allow_copy=False)  
    
  • เมื่อต้องการเพิ่ม embeddings ใหม่ ก็สามารถเพิ่มคอลัมน์แล้วบันทึกได้อย่างง่ายดาย
    df = df.with_columns(embedding=embeddings)  
    df.write_parquet("mtg-embeddings.parquet")  
    

การค้นหาความคล้ายคลึงและการกรองด้วย Parquet + polars

  • สามารถกรองเฉพาะข้อมูลที่ตรงตามเงื่อนไขก่อน แล้วค่อยทำ similarity search ได้
  • ตัวอย่าง: ค้นหาการ์ดที่คล้ายกับการ์ดหนึ่ง (query_embed) แต่จำกัดเฉพาะการ์ดประเภท 'Sorcery' และมีสี 'Black'
    df_filter = df.filter(  
        pl.col("type").str.contains("Sorcery"),  
        pl.col("manaCost").str.contains("B"),  
    )  
    
    embeddings_filter = df_filter["embedding"].to_numpy(allow_copy=False)  
    idx, _ = fast_dot_product(query_embed, embeddings_filter, k=4)  
    related_cards = df_filter[idx]  
    
  • เวลาเฉลี่ยในการทำงานคือ 1.48ms ช้ากว่าการค้นหาทั้งชุดข้อมูล 37% แต่ก็ยังเร็วอยู่มาก

ทางเลือกสำหรับการจัดการข้อมูลเวกเตอร์ขนาดใหญ่

  • วิธีแบบ Parquet และ dot product สามารถรองรับได้สบายจนถึงระดับ embeddings หลายแสนรายการ
  • หากต้องจัดการกับชุดข้อมูลที่ใหญ่กว่านั้น อาจจำเป็นต้องใช้ vector database
  • อีกทางเลือกหนึ่งคือใช้ sqlite-vec ที่ทำงานบน SQLite เพื่อเพิ่มความสามารถด้านการค้นหาและกรองเวกเตอร์

บทสรุป

  • vector database ไม่ได้เป็นสิ่งจำเป็นเสมอไป
  • การใช้ Parquet + polars เป็นทางเลือกที่ทรงพลังสำหรับการจัดเก็บ ค้นหา และกรอง embeddings อย่างมีประสิทธิภาพ
  • โดยเฉพาะในโปรเจ็กต์ขนาดเล็ก การใช้ไฟล์ Parquet อาจเร็วกว่าและคุ้มค่ากว่า
  • สิ่งสำคัญคือการเลือกโซลูชันที่เหมาะสมระหว่าง Parquet และ vector database ตามลักษณะของโปรเจ็กต์
  • สามารถดูโค้ดและข้อมูลได้ที่ GitHub repository

1 ความคิดเห็น

 
GN⁺ 2025-03-06
ความคิดเห็นจาก Hacker News
  • ปัญหาของ Parquet คือมันเป็นแบบสถิต จึงไม่เหมาะหากต้องการการเขียนและอัปเดตอย่างต่อเนื่อง แต่เมื่อใช้ไฟล์ Parquet ของ DuckDB กับ object storage ก็ได้ผลลัพธ์ที่ดี เวลาโหลดเร็ว

    • หากโฮสต์โมเดล embedding เอง ก็สามารถส่งอาร์เรย์บีบอัด numpy float32 เป็นไบต์ แล้วถอดรหัสกลับเป็นอาร์เรย์ numpy ได้
    • โดยส่วนตัวแล้วชอบใช้ SQLite กับส่วนขยาย usearch มากกว่า ใช้เวกเตอร์แบบไบนารีก่อน แล้วค่อยจัดอันดับใหม่ 100 อันดับแรกด้วย float32 ใช้เวลาประมาณ 2ms สำหรับข้อมูลราว 20,000 รายการ ซึ่งเร็วกว่า LanceDB สำหรับคอลเลกชันที่ใหญ่กว่านี้ Lance อาจชนะได้ แต่ในกรณีการใช้งานของฉัน ผู้ใช้แต่ละคนมีไฟล์ SQLite เฉพาะของตัวเอง จึงทำงานได้ดี
    • สำหรับความสามารถในการพกพา มี Litestream ให้ใช้
  • บทความนี้ยอดเยี่ยมมาก ฉันติดตามและชื่นชอบงานของคุณมานาน สำหรับคนที่กำลังจะลองทำบน SQLite อยากเสริมว่า DuckDB เริ่มมีฟีเจอร์ด้าน vector similarity บางอย่างที่อ่าน Parquet ได้ และรองรับกรณีการใช้งานนี้ได้อย่างพอดี

  • ฉันยังไม่ชอบ data frame อยู่ดี แต่ Polars ดีกว่า pandas มาก

    • ฉันกำลังคำนวณ time series โดยหลัก ๆ คือทำการปรับราคาหุ้นแบบง่าย ๆ
    • น่าประหลาดใจที่โค้ดอ่านและทดสอบได้จริง
    • มันรันเร็วมากจนดูเหมือนพังไปแล้ว
  • ลองดู usearch ของ Unum มันชนะเกือบทุกอย่างและใช้งานง่ายมาก ทำสิ่งที่ต้องการได้ตรงเป๊ะ

  • ถ้าอยากลอง คุณสามารถโหลดแบบ lazy จาก HF และใช้ตัวกรองได้

    • Polars ใช้งานได้ยอดเยี่ยมและแนะนำอย่างมาก มันเก่งมากในการใช้ CPU ของโหนดเดียวให้เต็มที่ และถ้าจำเป็นต้องกระจายงาน ก็สามารถใช้ POLARS_MAX_THREADS กับ Ray Actor เพื่อปรับตามระดับการใช้ทรัพยากรของโหนดเดียวได้
  • มีข้อค้นพบดี ๆ มากมาย

    • ฉันสงสัยว่าการส่งข้อมูลแบบมีโครงสร้างเข้า embedding API ดีกว่า หรือส่งข้อมูลแบบไม่มีโครงสร้างดีกว่า ถ้าถาม ChatGPT มันบอกว่าส่งข้อมูลแบบไม่มีโครงสร้างดีกว่า
    • กรณีใช้งานของฉันคือสำหรับ jsonresume ตอนนี้ฉันส่ง json เวอร์ชันเต็มเป็นสตริงเพื่อสร้าง embedding แต่กำลังทดลองใช้โมเดลที่แปลง resume.json เป็นข้อความเต็มก่อน แล้วค่อยสร้าง embedding ดูเหมือนว่าผลลัพธ์จะดีกว่า แต่ยังไม่ค่อยเห็นความเห็นที่ชัดเจนเกี่ยวกับเรื่องนี้
    • เหตุผลที่ข้อมูลแบบไม่มีโครงสร้างอาจดีกว่าคือมันมีความหมายเชิงข้อความ/เชิงความหมายจากภาษาธรรมชาติอยู่ด้วย
  • ในเอกสารของ Vespa มีทริกที่เรียบร้อยดี คือแปลงเวกเตอร์เป็นเลขฐานสองแล้วใช้รูปแบบแทนค่าแบบฐานสิบหก

    • ทริกนี้ใช้เพื่อลดขนาด payload ได้ ใน Vespa รองรับรูปแบบนี้ และมีประโยชน์เป็นพิเศษเมื่อเวกเตอร์เดียวกันถูกอ้างอิงหลายครั้งในเอกสาร ในกรณีอย่าง ColBERT หรือ ColPaLi (ที่มี embedding vector หลายตัว) สามารถลดขนาดของเวกเตอร์ที่เก็บบนดิสก์ได้มาก
  • Polars + Parquet ยอดเยี่ยมมากทั้งในด้านความสามารถในการพกพาและประสิทธิภาพ โพสต์นี้เน้นเรื่องความสามารถในการพกพาของ Python แต่ Polars ก็มี Rust API ที่ใช้งานง่ายสำหรับฝังเอนจินไว้ในหลาย ๆ ที่ได้ด้วย

  • ฉันเป็นแฟนตัวยงของ Polars แต่ไม่เคยคิดจะใช้มันเก็บ embedding มาก่อน (ก่อนหน้านี้กำลังทดลองกับ sqlite-vec อยู่) ฟังดูเป็นไอเดียที่น่าสนใจมาก

  • ขอแนะนำ lancedb เป็นอีกไลบรารีหนึ่งที่มีประสิทธิภาพและฟีเจอร์ยอดเยี่ยม เช่น full-text indexing และการทำเวอร์ชันของการเปลี่ยนแปลง

    • มันเป็น vector database และซับซ้อนกว่า แต่ก็สามารถใช้ได้โดยไม่ต้องสร้างดัชนีก่อน และยังมีการรองรับ arrow แบบ zero-copy ที่ยอดเยี่ยมสำหรับทั้ง polars และ pandas ด้วย