แนะนำราคารูปคอมมิชชันอนิเมะด้วย fastai ML

Prad Rattanakijsoonthorn
5 min readJun 29, 2021

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

นอกจากนี้ปัญหาการกดราคาก็ทำให้ประเทศของเราต้องสูญเสียเหล่าศิลปินที่จะเป็นกำลังสำคัญในการพัฒนาวงการศิลปะของประเทศไทย เรามั่นใจว่าคนไทยเองก็มีความสามารถทางด้านศิลปะไม่แพ้ชาติอื่นใด หากได้รับการซัพพอร์ทจนสามารถทำเป็นอาชีพประจำที่มีรายได้เพียงพอเหมือนกับอาชีพอื่น เราอาจได้เห็นชื่อศิลปินชาวไทยหลายคนในงานระดับโลกก็ได้

ด้วยเหตุนี้เราจึงทำ machine learning นี้ขึ้นมา เพื่อเป็นตัวช่วยในการแนะนำราคาเบื้องต้นให้กับผู้ที่เป็นมือใหม่ในวงการซึ่งอาจจะไม่มั่นใจในฝีมือตัวเองจึงไม่กล้าตั้งราคาตามคุณภาพงานอย่างที่ควรจะเป็น (ทำให้กลายเป็นการดันบาร์ราคาให้ต่ำลงแบบไม่รู้ตัว) โดยเราหวังว่านี่จะเป็นส่วนหนึ่งในการดันบาร์ราคาคอมมิชชันขึ้นมาให้สูงขึ้น ซึ่งจะเป็นกำลังให้กับศิลปินในการสร้างสรรค์ผลงานต่อไปได้

สารบัญ
· รวบรวมและจัดการ Dataset
· โมเดลที่ 0: Image Classification
· โมเดลที่ 1: Image Regression
· โมเดลที่ 2: Tabular Regression
· รวมผลการทำนายจากสองโมเดล
· เช็ค Overfit ด้วย Test Set
· นำไปใช้งานด้วยการ Deploy
· ปัญหาและข้อเสนอแนะ

รวบรวมและจัดการ Dataset

dataset ของเราได้มาจากการทำ web scraping จากเว็บไซต์ Artists & Clients ซึ่งเป็นเว็บไซต์สำหรับซื้อ-ขายรูปวาดคอมมิชชัน

Artists & Clients

เราทำการดึงรูปวาดคอมมิชชันอนิเมะที่ตัวละครมีขนาดต่าง ๆ ได้แก่ chibi, bust up, half body และ full body พร้อมข้อมูลอื่น ๆ มาเก็บไว้ในไฟล์ csv ประกอบด้วย

  • price: ราคา
  • day: ระยะเวลาที่ใช้ในการวาด
  • like: จำนวนไลก์ของรูป
  • completed: จำนวนรูปที่ศิลปินเคยวาด
  • rate: คะแนนรีวิว
  • size: ไซส์ของตัวละคร

จากนั้นเราได้ทำการใส่ข้อมูลเพิ่มเติมอีกสองอย่างในไฟล์เดิม ได้แก่

  • color: รูปสีหรือ grayscale (ตรวจสอบด้วยโค้ดของ Noah Whitman จาก stackoverflow)
  • bg: รูปมีหรือไม่มีพื้นหลัง (ตรวจสอบจากนามสกุลไฟล์ เราคาดว่ารูปส่วนใหญ่ที่ไม่มีพื้นหลังจะเป็นไฟล์ png)

ข้อมูลที่ได้มาไม่สามารถใช้ได้ทั้งหมด เช่น รูปภาพที่ดึงมาเป็นรูปพื้นหลังเฉย ๆ หรือบางข้อมูลก็ไม่ถูกต้อง เช่น ในโพสต์ระบุราคาของไซส์ bust up แต่รูปจริงเป็นไซส์ full body เป็นต้น เราจึงทำการคัดออกคร่าว ๆ ด้วยมือทำให้เหลือชุดข้อมูลที่ใช้ได้จริง 8,239 ชุด ซึ่งมีเลเบล (ราคา) ตั้งแต่ 5–620 $

เรานำชุดข้อมูลทั้งหมดไปลองเทรนเบื้องต้นด้วยการทำ classification โดยใช้ fastai แต่พบว่าการใช้ข้อมูลทั้งหมดไปเทรนทำให้ได้ผลลัพธ์ที่ไม่ดีเท่าที่ควร เราจึงต้องหากลยุทธ์เพื่อจัดการปัญหานี้

  • อย่างแรก เราสังเกตเห็นจากการดูผลลัพธ์จากการทำ image classification ครั้งแรก มีหลายรูปที่ค่อนข้างแปลกในเซนส์ของการตั้งราคา เช่น เป็นภาพร่างขาวดำแต่ราคาที่ตั้งสูงกว่าราคาโดยทั่วไปมาก เราจึงคาดการณ์ว่าอาจจะมีศิลปินหลายคนที่เป็นมือใหม่ในวงการทำให้ตั้งราคาที่สูงหรือต่ำกว่าปกติ เราจึงตัดสินใจเลือกใช้เฉพาะข้อมูลของศิลปินที่เคยวาดตั้งแต่ 5 รูปขึ้นไป
  • หลังจากนั้น เราพล็อตกราฟการกระจายของราคาทำให้เห็นว่ารูปวาดที่มีราคาสูงมีจำนวนน้อยกว่าราคาต่ำ-กลางมาก เราคาดว่าการแบ่งคลาสแบบเดิมมีจำนวนข้อมูลไม่สมดุลและมีช่วงราคาที่ถี่เกินไป เราจึงใช้ k-means ในการแบ่งข้อมูลเป็นกลุ่มแบบ clustering แทน เพื่อผลลัพธ์ที่ดีขึ้นในการทำ classification
distribution
k-means
  • เมื่อการทำ classification ยังได้ผลลัพธ์ไม่ดีพอ (จะกล่าวถึงรายละเอียดในหัวข้อถัดไป) เราจึงตัดสินใจทำโมเดล regression ซึ่งเมื่อดูจากกราฟการกระจายของราคา จะเห็นว่ารูปภาพที่มีราคาสูงมากมีน้อยมากจนอาจส่งผลต่อความแม่นยำของโมเดล เราจึงใช้ boxplot ในการคัดข้อมูลที่เป็น outliers ออก
boxplot
  • เราสังเกตเห็นความผิดปกติของข้อมูลคะแนนรีวิว เราเห็นว่าถ้าโพสต์ไหนมีรีวิวจะได้คะแนน 5 ดาวทั้งหมด ซึ่งเราไม่แน่ใจว่าเป็นความผิดพลาดของเว็บไซต์หรือความผิดพลาดจากขั้นตอนการดึงข้อมูล จึงตัดสินใจไม่นำข้อมูลจากคอลัมน์ rate ไปใช้เทรนโมเดล tabular regression

ท้ายที่สุด เราเทรนโมเดลด้วยการทำ image และ tabular regression โดยมีชุดข้อมูลที่นำไปใช้จริง 2,028 ชุด ซึ่งมีเลเบลตั้งแต่ 5–90 $ แบ่งออกเป็น train set, validation set และ test set ในอัตราส่วนประมาณ 80:10:10 โดยแบ่งจากการพิจารณาขนาดของตัวละคร ดังตารางนี้

เราเซฟข้อมูลตารางไว้ในไฟล์ anime.csv และเซฟข้อมูลรูปภาพในโฟลเดอร์ img

- anime
- anime.csv
- img
- train (1624 images)
- valid (204 images)
- test (200 images)

โมเดลที่ 0: Image Classification

ในตอนแรกเรานำข้อมูลทั้งหมดไปเทรนโมเดลด้วยการ classification โดยมีคลาสเป็นช่วงราคาที่มีความกว้างอันตรภาคชั้นเท่ากับ 5 $ และสิ้นสุดที่ช่วง 51 $ ขึ้นไป (5–10, 11–15, 16–20, … , 51+) ผลลัพธ์ที่ได้มี accuracy ต่ำมาก (ประมาณ 25%) เราจึงสันนิษฐานว่า

  • ศิลปินบางคนอาจเป็นมือใหม่จึงยังตั้งราคาไม่สมเหตุสมผล
  • จำนวนข้อมูลของแต่ละคลาสมีปริมาณที่ไม่สมดุลกันและมีคลาสจำนวนมากเกินไป อาจเป็นเหตุให้โมเดลมีประสิทธิภาพการทำนายต่ำ

ด้วยข้อสันนิษฐานนี้เราจึงคัดข้อมูลจากศิลปินที่เคยวาดน้อยกว่า 5 รูปออก แล้วใช้ k-means ในการแบ่งข้อมูลแทนการกำหนดช่วงเดิม โดยแบ่งคลาสเป็นช่วง 5–16, 17–27, 28–39, 40–57 และ 58+ $

เราทำการเทรนโมเดลด้วย architecture หลาย ๆ แบบ รวมถึงการทำ data augmentation แต่โมเดลของเราก็ไม่สามารถทำนายได้ดีมากเท่าที่หวังไว้ สุดท้ายเราลองใช้ densenet121 เทรนเป็นจำนวน 5 epochs ได้โมเดลที่มี accuracy ประมาณ 41.1% ซึ่งยังไม่ดีพอสำหรับการนำไปใช้งานจริง อีกทั้งยังไม่สามารถทายผลรูปให้มีราคาสูงกว่า 58 $ ได้ เราจึงเปลี่ยนแผนไปทำ image regression แทน

learn_img = cnn_learner(dls_img, densenet121, metrics=[accuracy, error_rate])
learn_img.fine_tune(epochs=5, base_lr=2.7e-3)

โมเดลที่ 1: Image Regression

เมื่อเราเปลี่ยนแผนมาทำ regression หลังจากตัด outliers ออกด้วยการใช้ boxplot ทำให้เหลือเฉพาะข้อมูลที่มีราคาตั้งแต่ 5–95 $ เราจะทำการ resize รูปภาพและทำ data augmentation แล้วกำหนดค่าไว้ใน DataBlock

Resizing

ก่อนจะทำ data augmentation เราได้ทำการแปลงขนาดรูปภาพให้เป็นสี่เหลี่ยมจัตุรัสโดยการกำหนด item_tfms ด้วย Resize โดยเราได้ทดลองทำ ResizeMethod ทั้งสามแบบ เมื่อนำไปเทรนเป็นจำนวน 5 epochs ได้ผลลัพธ์ดังนี้

  • Squish (ปรับด้วยการยืด-หดรูปภาพ)
  • Pad: (ปรับด้วยการเติมขอบรูปภาพด้วยการสะท้อนภาพเดิม)
  • Crop: (ปรับด้วยการตัดรูปภาพ)

Data Augmentation

เราทำ data augmentation เพื่อเพิ่มจำนวนรูปภาพที่ใช้เทรน ทำให้โมเดลได้เรียนรู้รูปภาพในมุมมองที่หลากหลาย เรากำหนด batch_tfms ด้วย aug_transforms โดยเราได้ทดลองทำการพลิกรูปด้วยการกำหนดค่า True/False ที่ flip_vert

data augmentation
  • flip_vert=False (ไม่พลิกรูป)
  • flip_vert=True (พลิกรูป)

สุดท้าย เมื่อพิจารณาผลจากการ resize รูปภาพและทำ data augmentation เราเลือกแปลงขนาดรูปภาพให้เป็นสี่เหลี่ยมจัตุรัสขนาด 400*400 pixel เพื่อให้เหมาะกับขนาด gpu ที่เราใช้เทรนใน google colaboratory เลือก resize รูปภาพด้วย ResizeMethod.Squish และทำ data augmentation แบบไม่พลิกรูป

def img_price(o): #คืนค่าราคาของรูปโดยดูจากคอลัมน์ price
for i, row in df.iterrows():
if row.path==os.path.basename(o): return row.price
dblock = DataBlock(
blocks=(ImageBlock, RegressionBlock), #(รูป, ราคา)
get_items=get_image_files,
splitter=FuncSplitter(lambda o: Path(o).parent.name=='valid'), #แยก train/valid set ด้วยชื่อ parent folder
get_y=img_price,
item_tfms=Resize(400, method=ResizeMethod.Squish),
batch_tfms=aug_transforms(size=400, pad_mode=PadMode.Reflection),
)
dls_img = dblock.dataloaders(path_img, bs=32) #batch size = 32

เราทำการเทรนโมเดลด้วย architecture densenet121 และเทรนเป็นจำนวน 10 epochs ได้โมเดลที่มี mean absolute error (mae) ประมาณ 11.1

learn_img = cnn_learner(dls_img, densenet121, metrics=[mae, mse, rmse])
learn_img.fine_tune(epochs=5, base_lr=1.2e-2)

โมเดลที่ 2: Tabular Regression

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

ข้อมูลตารางของเรามีทั้งที่เป็นข้อมูลต่อเนื่อง (continuous) ได้แก่ ระยะเวลาที่ใช้วาด จำนวนไลก์ จำนวนรูปที่เคยวาด และข้อมูลที่เป็นประเภท (categorical) ได้แก่ ไซส์ของตัวละคร การมีหรือไม่มีสี การมีหรือไม่มีพื้นหลัง เราสามารถบอกข้อมูลนี้กับ fastai ด้วยการกำหนด cat_names และ cont_names ใน TabularDataLoaders

dls_csv = TabularDataLoaders.from_df(
df_train,
splits="dataset", #แยก train/valid set ด้วยคอลัมน์ dataset
cat_names=['size', 'color', 'bg'], #ข้อมูลต่อเนื่อง (continuous)
cont_names=['day', 'like', 'completed'], #ข้อมูลประเภท (categorical)
y_names='price', #ราคาของรูปโดยดูจากคอลัมน์ price
procs=[Categorify, FillMissing, Normalize],
bs=64
)

เราทำการเทรนโมเดลเป็นจำนวน 10 epochs ได้โมเดลที่มี mae ประมาณ 11.5

รวมผลการทำนายจากสองโมเดล

เราหาผลลัพธ์สุดท้ายโดยใช้วิธีการหาค่าเฉลี่ยจากผลการทำนายของทั้งสองโมเดล ดังโค้ดด้านล่าง

for i, row in df_valid.iterrows():
valid_img = Path('/content/img/valid/'+row.path)
valid_csv = df_test[df_valid.path==row.path].drop(columns=['path', 'dataset']).iloc[0]
predict_img = float(learn_img.predict(valid_img)[1]) #ทำนายราคาจากรูปภาพ
predict_csv = float(learn_csv.predict(valid_csv)[1]) #ทำนายราคาจากตาราง
predict_price.append((predict_csv+predict_img)/2) #ทำนายราคาโดยเฉลี่ยจากสองโมเดล
true_price.append(float(row.price))
mean_absolute_error(predict_price, true_price) #หาค่า mae

ซึ่งเราได้ผลลัพธ์ที่มี mae ประมาน 10.4 จะเห็นได้ว่าดีขึ้นกว่าการใช้โมเดลเดียวทำนายเล็กน้อย

เช็ค Overfit ด้วย Test Set

ในขั้นตอนการเตรียม dataset เราแบ่ง test set ไว้ประมาณ 10% ของข้อมูลทั้งหมดสำหรับใช้ในการตรวจสอบเพื่อป้องกันการเกิด overfit ของโมเดล เราใช้โค้ดเดียวกันกับการรวมโมเดลในหัวข้อที่แล้วแต่เปลี่ยนข้อมูลอินพุทเป็น test set แทน ผลลัพธ์ที่ได้มี mae ประมาณ 10.7 ซึ่งพอ ๆ กันกับตอนที่ใช้ validation set จึงสันนิษฐานได้ว่าโมเดลของเราไม่ overfit และสามารถนำไปใช้งานได้

และเราได้ตรวจสอบประสิทธิภาพของโมเดลเพิ่มเติมด้วยการลองทำนายผลข้อมูลทั้งหมดด้วยค่าเฉลี่ยราคา เราได้ผลลัพธ์ว่าการทำนายที่มี mae ประมาณ 14.6 ซึ่งหมายความว่าการทำนายผลทั้งหมดด้วยค่าเฉลี่ยจะมีความผิดพลาดมากกว่าทำนายผลด้วยโมเดลประมาณ 4 $

(ราคาเฉลี่ย, mae) เมื่อทายผลเป็นราคาเฉลี่ยทั้งหมด

นำไปใช้งานด้วยการ Deploy

การจะนำโมเดลไปใช้งานจริง เราจำเป็นต้องทำการ deploy เป็นเว็บแอปพลิเคชันซึ่งจะช่วยให้ผู้ใช้งานสามารถกรอกข้อมูลและดูผลลัพธ์ได้ง่ายขึ้น โดยเราเลือกใช้ heroku ในการ deploy

heroku

ขั้นแรก เราทำการ export โมเดลไปเป็นไฟล์ pkl

learn_img.export('img_reg.pkl')
learn_img.export('img_csv.pkl')

ต่อมาเราเขียนโปรแกรมเพื่อแสดงผลบนเว็บ (ดูโค้ดเพิ่มเติมได้ที่ github) เนื่องจากโมเดลของเรามี mae ที่ 10.4 เราจึงให้เว็บของเราแสดงผลต่อผู้ใช้เป็นช่วงราคาบวกลบ 10 เช่น โมเดลทำนายว่ารูปมีราคา 15 $ เราก็จะแสดงผลออกไปเป็น 5–25 $

price = (predict_csv+predict_img)/2
min = math.ceil(price)-10 if math.ceil(price)-10>=5 else 5 #ลบ 10
max = math.ceil(price)+10 #บวก 10
price_range = str(min) + ' - ' + str(max)

เราเขียนโปรแกรมให้ผู้ใช้สามารถเลือกได้ว่าจะกรอกข้อมูลเพิ่มเติมหรือไม่ ถ้ากรอกข้อมูลเพิ่มก็จะสามารถใช้โมเดล tabular regression มาร่วมประเมินราคาร่วมเพื่อผลลัพธ์ที่ดีขึ้นได้

และนี่คือหน้าตาเว็บแอปของเราที่ deploy เสร็จแล้ว

anime-commission-price

ปัญหาและข้อเสนอแนะ

  • เนื่องจากเราคัดเลือกเฉพาะข้อมูลของศิลปินที่เคยวาดตั้งแต่ 5 ครั้งขึ้นไป ทำให้ต้องตัดข้อมูลออกจำนวนมากจนเหลือ dataset สำหรับเทรนน้อย เราคาดว่าผลลัพธ์จากการเทรนน่าจะดีขึ้น (mae<10) ได้ถ้าหากมี dataset มากกว่านี้
  • การแบ่งไซส์ของตัวละครในข้อมูลตารางอาจทำให้ละเอียดกว่านี้ได้ เช่น แยกรูปไซส์ headshot ออกมาจาก bust up
  • อาจเพิ่มข้อมูลในตาราง เช่น เพิ่มข้อมูลจำนวนตัวละครในรูปโดยใช้โมเดลที่สามารถตรวจจับใบหน้าตัวละครอนิเมะ
  • อาจพัฒนาต่อให้โมเดลสามารถทำนายได้ทั้งราคาคอมมิชชันธรรมดา และคอมมิชชันเชิงพาณิชย์ (โดยทั่วไปเชิงพาณิชย์จะคิดราคาสูงกว่า)

--

--