Tutorial

รู้จักกับ Pickle โหลหมัก Object ใน Python

By Arnon Puitrakul - 08 มกราคม 2020 - 1 min read min(s)

รู้จักกับ Pickle โหลหมัก Object ใน Python

เมื่อก่อน ตอนเราทำงานกับข้อมูลใน Python อย่างเช่นการทำ ETL Process (Extract, Transform and Load) หรือ การ Train Machine Learning Model ทั้งหมด ทั้งปวงนั้นกินเวลานานแสนนาน แต่ความชิบหายก็เกิดขึ้นได้เสมอ เช่น Script ที่เขียนไปมีปัญหา ทำให้เราต้องรันใหม่ทั้งหมด หรือ เราอยากจะเก็บ Object บางอย่างไว้ใช้ในครั้งต่อไป จะได้ไม่ต้องรันใหม่ทั้งหมด

เมื่อก่อนทำยังไง ?

ถ้าเราเจอปัญหานี้จะทำยังไงดี วิธีแรก น่าจะง่าย ๆ เลยคือ เราก็เก็บค่าอะไรบางอย่างที่เราใช้เป็น Parameter ลงไปใน Text File แล้วพอเราจะโหลดอีกครั้ง เราก็เปิด Text File แล้วป้อน Parameter จาก Text File ลงไปในโปรแกรม เราก็ น่าจะ ได้ State เดิมกลับคืนมาแล้ว

ที่เราใช้ตัวหนา เพราะ มันอาจจะไม่ได้เป็นเหมือนเดิมก็ได้ บาง Process เราอาศัยการ Random ทำให้ผลลัพธ์ในแต่ละครั้ง มันก็จะไม่เหมือนเดิมซะทีเดียว อาจจะนึกภาพไม่ออก ยกตัวอย่างเช่น เราจะแบ่ง Train:Test สำหรับการทดสอบ Machine Learning Model ด้วยอัตราส่วน 80:20 ถ้าเราบอกว่า เราไม่ Shuffle เลย การเก็บ 80 และ 20 ก็ดูจะ Valid อยู่นะ ยังไงข้อมูลมันก็เรียงเหมือนเดิม ตัดที่เดิม เราก็จะได้ข้อมูลเหมือนเดิม แต่ถ้าเราบอกว่า เราอยากจะ Shuffle นี่สิงานงอก

ถึงเราจะเก็บเลข 80 และ 20 ไว้ รันอีกครั้ง มันก็ได้ไม่เหมือนเดิม เพราะข้อมูลมันสลับที่กันหมดแล้ว แต่ละครั้งไม่เหมือนกันเลย ทำให้นำไปสู่วิธีที่ 2 นั่นคือการเก็บลง CSV File

จริง ๆ มันก็ไม่ต่างจากวิธีแรกเท่าไหร่แหละ แค่เราเก็บแบบมี Format ทำให้เวลาเราเรียกกลับขึ้นมา มันทำได้ง่ายขึ้น อย่าง CSV หรือ TSV Format เอง ก็มีคำสั่งหลายอย่างที่รองรับการอ่านไฟล์ประเภทนี้ เช่นใน Pandas ก็มีคำสั่งสำหรับอ่านอยู่แล้ว ทำให้ เราไม่ต้องมานั่งเขียนเองทั้งหมด

ปัญหามันก็เกิดอีกครั้ง เมื่อข้อมูลเรามี Format แปลก ๆ หรือ เรียกมาจากที่อื่นที่ไม่ได้อ่านจาาก CSV File อื่น ยกตัวอย่างเคสเราเลยคือ ตอนนั้นเราดึงพวก Post และ Comment มาจาก Social Media แห่งนึงด้วย API ของมัน แล้วเราก็จะเก็บเป็น CSV ไว้คราวหน้าเผื่อเรียกมาทำงานต่อ ตอนเก็บก็ไม่มีอะไร ปกติดีเก็บได้ชิว ๆ เพราะมันมีคำสั่งให้เราหมดแล้ว เราไม่ต้องมานั่งเขียนเอง

ความบรรลัยมันเกิดขึ้น ตอนเราจะเรียกมันกลับขึ้นมา เพราะ Pandas ที่เราใช้อ่านมัน Error ว่า Column เรามันไม่เท่ากัน เช่น Row แรกมี 5 Column แต่อีก Row มี 200 Columns อะไรแบบนั้น นั่งหาอยู่นาน สรุปคือ ใน Post และ Comment มันมี Comma อยู่ในนั้นด้วย ทำให้เวลา Script มันอ่านขึ้นมา มันก็นึกว่าเป็น Column นั่นเอง ตอนนั้นคือ ก็ทำใหม่อะ

ต้อง Encode พวก Comma และ อัขระพิเศษ เป็นอย่างอื่นแทน แล้วตอนเรียก เราก็สั่ง Replece มันกลับเป็นเหมือนเดิม ซึ่งแน่นอน มันเสียเวลามาก ๆ ทั้งคนเขียนที่ต้องมานั่งเรียกอะไรแบบนี้ทุกครั้ง และ เครื่องเองที่ต้องมา Tranform กลับทุกครั้ง ยิ่งถ้าเรามี Data ที่ใหญ่มาก ๆ รันทีต้องมาเสียเวลาอะไรแบบนี้ ก็ไม่โอเคเท่าไหร่

หรืออีกตัวอย่างคือ โปรแกรมของเราต้อง Predict อะไรบางอย่าง แน่นอนว่า Model นั้นเกิดจาก Data ขนาดใหญ่มาก ถ้าเราไม่ได้ทำอะไรเลย เราก็ต้องเก็บ Hyperparameter เป็น Text File และ Data เป็น CSV แล้วพอเราเปิดโปรแกรม เราก็ต้องดึงข้อมูลเหล่านี้มา Train Model ทุกครั้ง มันก็เสียเวลา เปลือง Resource ทั้งในแง่ Space และ Time เป็นอย่างมาก

Pickle come to rescue

จากปัญหาทั้งหมด ทั้งมวล ที่เรายกตัวอย่างมา เราต้องการอะไรที่มาช่วยเราเก็บ Object ใน Python และ โหลดกลับเข้ามาแล้วได้ State เดิมแน่นอน ไม่เอาแล้ว ต้องมานั่ง Transform หรือ Train ใหม่ทุกครั้ง ไม่ใช่เรื่องตลกเลย

ทำให้เราได้ไปรู้จักกับ Pickle ใน Python มา มันเข้ามาช่วยเราแก้ปัญหานี้ได้อย่างสุดยอดไปเลย เพราะมันทำให้เราสามารถเก็บ Object ใน Python เป็น File และ เรียกมันกลับขึ้นมา ก็จะได้ State เดิมเลย

ถ้าสงสัยว่าทำไมมันเรียกว่า Pickle ลองไปหาความหมายของคำนี้ดู มันแปลว่า การหมัก การดอง อะไรพวกนั้น มันคือการ Preserve อะไรบางอย่าง ในที่นี้เราก็จะ Preserve Object มันเลยเรียกแบบนี้แหละ

เบื้องหลังการทำงานของมันคือ การทำ Object Serialisation โดยมันจะแปลง Object ต่าง ๆ ใน Python ให้อยู่ในรูปแบบของ Byte Stream พอเราเรียกมันกลับขึ้นมา มันก็จะทำกลับกัน เพื่อสร้าง Object อันเดิมกลับมาเท่านั้นเอง

อีกความงามการทำ Serialisation คือ การที่ เราสามารถย่อขนาดของข้อมูลบางอย่างได้ด้วย เพราะเบื้องหลังของมันก็คือการแปลงให้ข้อมูลมันอยู่ในรูปแบบของ Bit Byte นั่นเอง นั่นแปลว่า ถ้า Set ของข้อมูลเรามัน Finite มาก ๆ เราก็สามารถ Encode มันด้วย Scheme บางอย่างแล้วเก็บ โดยใช้ขนาดที่เล็กลงนั่นเอง

อ่านแล้วอาจจะ งง ว่าอีนี่มันเล่าอะไรของมัน เรายกตัวอย่างที่เราเคยทำละกันคือ เราทำงานกับข้อมูลจำพวก DNA Sequence ซึ่งเราก็ได้เรียนกันมาตอนม.ปลายกันแล้วว่าใน DNA มันจะประกอบด้วย Base เพียง 4 ชนิดเรียงกันเท่านั้นคือ A,T,C และ G นั่นเอง

ถ้าในมุมมองของเครื่องคอมพิวเตอร์ A,T,C และ G มันก็เป็นอัขระนึงใน ACSII เท่านั้นแหละ ซึ่งเราก็รู้กันอยู่แล้วว่า 1 ASCII Character มันจะกิน 1 Byte หรือ 8 Bits หรือ ถ้าเราขยายไปเล่นกับ Unicode อีก มันก็จะทะลุไป 2 Byte หรือ 12 Bits กันเลยทีเดียว

แต่เรารู้อยู่แล้วว่า DNA มีแค่ A,T,C และ G เท่านั้นเอง แล้วทำไมเราต้องใช้ 8 Bits ต่อ Base ในการเก็บด้วยละ ถ้าเรามีแค่ 4 Combination เราก็ใช้แค่ 2 Bits ในการเก็บก็พอแล้วนิน่า เราก็แค่สร้าง Scheme ในการ Encode ขึ้นมา เช่น 00 คือ A, 01 คือ T จนถึง G จะเห็นว่า แค่น้ีเราก็ลดพื้นที่การจัดเก็บไปได้มากแล้วถึง 4 เท่ากันไปเลย

อันนี้เราไม่ได้ใช้ Pickle ในการเก็บนะ อันนี้เราเขียน Scheme และ Script ในการเก็บและเรียกเองนะ ถ้า Pickle เราไม่ต้องไปรู้ก็ได้ว่ามันทำยังไง เดี๋ยวจะยาวไป เอาเป็นนว่า มันหด Object เป็นไฟล์ แล้วเรียกกลับขึ้นมาได้เหมือนเดิมเสมอแค่นั้นพอ

Pickle และ Unpickle

บอกเลยว่า มันง่ายมาก ๆ แต่ก่อนที่เราจะไป Pickle เราจะต้องเรียกมันมาก่อน ซึ่งก็ใช้คำสั่ง Import ทั่ว ๆ ไปได้เลย มันมากับ Python อยู่แล้ว ไม่ต้องไปลงเพิ่ม

import pickle

ถ้าจะจำก็ง่าย ๆ เลย คำสั่งที่เราใช้งานกันจะมีอยู่แค่ 2 คำสั่งเท่ากันคือ dump และ load

เริ่มจากการเก็บ Object ลงไปในไฟล์ก่อน เราจะใช้คำสั่ง dump แล้วเติมของ 2 อย่างลงไปคือ ของที่จะเก็บ และที่เก็บ

a = [1,2,3,4,5]

a_pickle = open('a.pkl', 'wb')

pickle.dump(a, 'a.pkl')

a_pickle.close()

ในตัวอย่างนี้เราลองสร้าง List ขึ้นมาง่าย ๆ แล้วเราจะ Pickle List นี้กัน เริ่มจากการเปิดไฟล์ผ่านคำสั่ง Open อันนี้น่าจะปกติอยู่แล้ว แต่ข้อสังเกตคือ Operation Mode แทนที่เราจะใส่แค่ w หรือก็คือ Write เราเติม b ลงไปด้วย มันย่อมาจาก Binary นั่นเอง เพราะอย่าลืมว่า Pickle มันใช้ Object Serialisation ซึ่งมันเก็บข้อมูลเป็น Binary Based ทำให้เราต้องเติม b เสมอ ส่วนชื่อไฟล์ และ สกุลไฟล์ ก็เอาที่ต้องการได้เลย หรือจะไม่ใส่สกุลไฟล์ก็ได้

a_file = open('a.pkl', 'rb')
a = pickle.load(a)
a.close()

และเวลาเราจะอ่านคืน เราก็เปิดไฟล์ และแน่นอนว่า ตอนเปิด Operation Mode อย่าลืมใส่ b ด้วย สำหรับการเรียกคืน เราก็จะใช้คำสั่ง load โดยใส่ไฟล์ที่เราเปิดลงไป ก็ได้คืนมาแล้ว จากตรงนี้ ถ้าเราเรียก a ขึ้นมา เราก็จะได้ List ที่ประกอบด้วยเลข 1,2,3,4 และ 5 เหมือนเดิมเลย

ติด Speed ไปให้สุด ด้วย cPickle

เมื่อ Object ที่เราต้องการทำ Serialisation มันใหญ่มาก ๆ อย่าง Pandas Dataframe ที่มีข้อมูลอยู่เยอะมาก ๆ การใช้ Pickle มันจะช้ามาก ทำให้มันมี Library อีกตัวขึ้นมาเพื่อช่วยให้เราทำ Object Serialisation และ Deserialisation ได้เร็วขึ้นนั่นคือ cPickle

ที่มันมีชื่อแบบนี้เพราะมันถูกเขียนขึ้นมาด้วย C Lang ทำให้ความเร็วนั่นบอกเลยว่า อย่างเด็ด !! เราลองโหลด Pandas Dataframe ขนาด 200 Columns, 100M Records ขึ้นมา คือเร็วขึ้นหลายสิบเท่าจริง ๆ

แต่ถ้าเราไปใช้กับ Object เล็ก ๆ ก็จะไม่เจอความแตกต่างอะไรเลย มันเร็วอยู่แล้วมันไปกว่านี้ไม่ได้แล้วละ ฮ่า ๆ

เมื่อไหร่ที่เราใช้ Pickle ไม่ได้ ?

เป็นคำถามที่ดีเลย ขอบคุณตัวเองที่ถามขึ้นมาตอนเขียน ฮ่า ๆ เคสนึงที่เรานึกออก ไม่สิ เราเจออยู่ตอนนี้แหละ คือ Object นั่นมีขนาดใหญ่มาก ๆ ใหญ่โคตร ๆ ใหญ่กว่า Memory ของเรา

ตอนนี้เรามี Data ขนาด 200 GB ที่เก็บเป็น ASCII Format มา แล้วเราคลายมันออกมาเป็นตัวเลขเลขแล้ว นั่นแปลว่า ข้อมูลนั้นมีขนาดใหญ่ว่า 200 GB แน่นอน เพราะมันเป็นตัวเลข ซึ่งตัวเลข เครื่องมันไม่ได้มองเป็น ตัวเลข มันมองเป็น Character 2 ตัว ทำให้มันกินเข้าไปเลย 2 เท่าเป็นอย่างน้อยจัดไป !!! เครื่องบ้าน ๆ ไม่น่ามี RAM ถึง 200 GB หรอก

ดังนั้นการ Process ข้อมูลใหญ่ ๆ แบบนี้ เราไม่สามารถ ที่จะโหลดข้อมูลทั้งหมดขึ้นมาได้แน่นอน ไม่งั้น RAM เต็ม Paging เต็มก็ยังไม่พอเลยมั่ง เราก็ต้องเลือกโหลดมาทำทีละ Chunk ไป แน่นอนว่า Pickle มันทำแบบนั้นไม่ได้

วิธีการ Work Around คือ ตอนเราเก็บ แทนที่เราจะเก็บเป็นไฟล์เดียวเลย เราก็ล่อเก็บมันเป็น Chunk ไปสิ นั่นก็ทำได้เหมือนกัน แต่ไฟล์มันก็จะเยอะตามที่เราเก็บนั่นแหละ ปวดหัวเข้าไปอีก ไหนตอนเรียกกลับขึ้นมา ถ้าเราก๊อปไฟล์ไม่ครบ อ้าวข้อมูลหายอีก ปัญหาตามมาเยอะมาก นั่นแหละ เป็น Case ที่เราว่าการใช้ Pickle อาจจะรอดยาก

หรืออีกเคสที่เราว่า การใช้ Pickle อาจจะไม่ใช่ทางเลือกคือ เมื่อเราต้องการที่จะใช้ค่าบางอย่างใน Object นั่นเพียงแค่ไม่กี่ค่า ยกตัวอย่างเช่น เราบอกว่า เราเก็บ Personal Information ของคนหลาย ๆ คนเลย มันก็น่าจะมีชื่อ สกุล และข้อมูลอื่น ๆ อีกมากมาย

แต่บังเอิญว่า ส่วนใหญ่เราต้องการที่จะดึงข้อมูลบางอย่างเช่น จังหวัดที่คนอาศัยอยู่ขึ้นมา ถ้าเราใช้ Pickle นั่นแปลว่า เราจะต้อง Deserialise ข้อมูลทั้งหมดกลับขึ้นมาเป็น Object เต็ม ๆ ก่อน แล้วเราค่อยเอาจังหวัดออกมา โอเคแหละ มันก็ดูเป็นวิธีที่มันก็ไม่ผิด ได้จังหวัดกลับมาเหมือนกัน แต่ถามว่า มันต้องขนาดนั้นเลยเหรอ ทำไมเราจะดึงจังหวัดออกมาเลยไม่ได้เหรอ ข้อมูลเรามันก็เป็นโครงอยู่แล้ว เรารู้ว่าชื่อ จังหวัด มันอยู่ตรงไหนแน่ ๆ ของแต่ละคน ทำไมเราไม่เก็บด้วยวิธีอื่นที่ดีกว่านี้

ใช่ฮ่ะ Back to basic เลย คือ การเก็บเป็น JSON ง่ายกว่าเดิมมาก เพราะตอนที่เราจะเรียกข้อมูลคืนมา เราก็รู้อยู่แล้วว่ามันอยู่ตรงไหนใน JSON เราก็เรียกขึ้นมาได้เลย ไม่ต้องมานั่ง Deserialise ข้อมูลทั้งหมดก็ได้ ทำให้ความเร็วแน่นอนว่ามันย่อมดีกว่า และการใช้ Resource ก็ย่อมน้อยกว่าเป็นธรรมดา

จริง ๆ เราว่ามันยังมีอีกหลายเคสมาก ที่ Pickle อาจจะไม่ตอบโจทย์ หลัก ๆ เราว่ามันไม่เหมาะด้วย 2 เหตุผลใหญ่ ๆ คือ ข้อมูลใหญ่โคตร ๆ ใหญ่วัวตายควายล้ม อันนี้เราแนะนำให้ไปใช้พวก hdf5 format น่าจะรอดมากกว่ามั่งนะ และอีกเหตุผลคือ Object มันมีความซับซ้อนมาก ๆ อย่างถ้าเราจะเก็บ Database อย่าง sqlite ลงไปเลยก็น่าจะไม่ไหวนะ อลังเกิ้นนนนน

สรุป

Pickle เป็น ชุดคำสั่งสำหรับการเก็บ Object ลงเป็น Binary File และ สามารถเรียกกลับมาเป็น Object ได้อย่างง่ายดาย โดยผ่านคำสั่ง dump และ load ซึ่ง Pickle เองก็ไม่ต้องไปโหลดเพิ่มที่ไหน มันมากับ Standard Package อยู่แล้ว แต่ถ้าใครต้องทำงานกับข้อมูลขนาดใหญ่มาก ๆ Pickle อาจจะช้าไป ลองไปใช้ cPickle ดูจะเห็นเลยว่า ความเร็วสูงกว่าเยอะมาก เพราะมันเขียนจาก C Lang

แต่แน่นอนว่าเหรียญมันก็มีสองด้าน บางเคส เราอาจจะใช้ Pickle ไม่ได้ เช่น ข้อมูลเรามีขนาดใหญ่มาก และ ข้อมูลเราซับซ้อนมาก ๆ ถ้าเจอแบบนนั้น เราอาจจะต้องไปพิจารณาใช้ Format อื่นในการเก็บ