การใช้คุณสมบัติด้านข้าง: การประมวลผลคุณสมบัติล่วงหน้า

ดูบน TensorFlow.org ทำงานใน Google Colab ดูแหล่งที่มาบน GitHub ดาวน์โหลดโน๊ตบุ๊ค

ข้อดีอย่างหนึ่งของการใช้เฟรมเวิร์กการเรียนรู้เชิงลึกเพื่อสร้างโมเดลผู้แนะนำคืออิสระในการสร้างการนำเสนอฟีเจอร์ที่หลากหลายและยืดหยุ่น

ขั้นตอนแรกในการทำเช่นนั้นคือการเตรียมคุณสมบัติ เนื่องจากคุณสมบัติดิบมักจะไม่สามารถใช้งานได้ในแบบจำลองทันที

ตัวอย่างเช่น:

  • รหัสผู้ใช้และรายการอาจเป็นสตริง (ชื่อ ชื่อผู้ใช้) หรือจำนวนเต็มขนาดใหญ่ที่ไม่ต่อเนื่องกัน (ID ฐานข้อมูล)
  • คำอธิบายรายการอาจเป็นข้อความดิบ
  • การประทับเวลาการโต้ตอบอาจเป็นการประทับเวลา Unix แบบดิบ

สิ่งเหล่านี้จำเป็นต้องได้รับการเปลี่ยนแปลงอย่างเหมาะสมเพื่อให้เป็นประโยชน์ในการสร้างแบบจำลอง:

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

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

ในการกวดวิชานี้เราจะมุ่งเน้นไปที่ recommenders และ preprocessing ที่เราต้องทำใน ชุดข้อมูลที่ MovieLens หากคุณกำลังสนใจในการกวดวิชาที่มีขนาดใหญ่ได้โดยไม่ต้องโฟกัสระบบ recommender, มีลักษณะที่เต็ม Keras คู่มือ preprocessing

ชุดข้อมูล MovieLens

ก่อนอื่นมาดูว่าเราสามารถใช้คุณลักษณะใดบ้างจากชุดข้อมูล MovieLens:

pip install -q --upgrade tensorflow-datasets
import pprint

import tensorflow_datasets as tfds

ratings = tfds.load("movielens/100k-ratings", split="train")

for x in ratings.take(1).as_numpy_iterator():
  pprint.pprint(x)
2021-10-02 11:59:46.956587: E tensorflow/stream_executor/cuda/cuda_driver.cc:271] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected
{'bucketized_user_age': 45.0,
 'movie_genres': array([7]),
 'movie_id': b'357',
 'movie_title': b"One Flew Over the Cuckoo's Nest (1975)",
 'raw_user_age': 46.0,
 'timestamp': 879024327,
 'user_gender': True,
 'user_id': b'138',
 'user_occupation_label': 4,
 'user_occupation_text': b'doctor',
 'user_rating': 4.0,
 'user_zip_code': b'53211'}
2021-10-02 11:59:47.327679: W tensorflow/core/kernels/data/cache_dataset_ops.cc:768] The calling iterator did not fully read the dataset being cached. In order to avoid unexpected truncation of the dataset, the partially cached contents of the dataset  will be discarded. This can happen if you have an input pipeline similar to `dataset.cache().take(k).repeat()`. You should use `dataset.take(k).cache().repeat()` instead.

มีคุณสมบัติที่สำคัญสองสามประการที่นี่:

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

สองรายการแรกเป็นลักษณะเฉพาะ การประทับเวลาเป็นคุณลักษณะที่ต่อเนื่อง

เปลี่ยนคุณสมบัติที่เป็นหมวดหมู่เป็นการฝัง

คุณลักษณะเด็ดขาด เป็นคุณลักษณะที่ไม่ได้แสดงปริมาณอย่างต่อเนื่อง แต่จะใช้เวลาในหนึ่งชุดของค่าคงที่

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

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

การนำคุณสมบัติการจัดหมวดหมู่แบบดิบๆ และเปลี่ยนให้เป็นแบบฝังนั้นเป็นกระบวนการสองขั้นตอน:

  1. ประการแรก เราต้องแปลค่าดิบเป็นช่วงของจำนวนเต็มที่ต่อเนื่องกัน โดยปกติโดยการสร้างการแมป (เรียกว่า "คำศัพท์") ที่จับคู่ค่าดิบ ("Star Wars") เป็นจำนวนเต็ม (เช่น 15)
  2. ประการที่สอง เราจำเป็นต้องนำจำนวนเต็มเหล่านี้มาแปลงเป็นการฝัง

การกำหนดคำศัพท์

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

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

ตัวเลเยอร์เองยังไม่มีคำศัพท์ แต่เราสามารถสร้างมันได้โดยใช้ข้อมูลของเรา

movie_title_lookup.adapt(ratings.map(lambda x: x["movie_title"]))

print(f"Vocabulary: {movie_title_lookup.get_vocabulary()[:3]}")
Vocabulary: ['[UNK]', 'Star Wars (1977)', 'Contact (1997)']

เมื่อเราได้สิ่งนี้แล้ว เราก็สามารถใช้เลเยอร์นี้เพื่อแปลโทเค็นดิบเป็นรหัสการฝังได้:

movie_title_lookup(["Star Wars (1977)", "One Flew Over the Cuckoo's Nest (1975)"])
<tf.Tensor: shape=(2,), dtype=int64, numpy=array([ 1, 58])>

โปรดทราบว่าคำศัพท์ของเลเยอร์ประกอบด้วยโทเค็นที่ไม่รู้จัก (หรือ "หมดคำศัพท์", OOV) หนึ่งโทเค็น (หรือมากกว่า!) สิ่งนี้มีประโยชน์จริง ๆ : หมายความว่าเลเยอร์สามารถจัดการกับค่าหมวดหมู่ที่ไม่ได้อยู่ในคำศัพท์ ในทางปฏิบัติ นี่หมายความว่าโมเดลสามารถเรียนรู้และให้คำแนะนำต่อไปได้ แม้จะใช้คุณลักษณะที่ไม่เคยเห็นระหว่างการสร้างคำศัพท์

การใช้ฟีเจอร์แฮช

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

เราสามารถใช้สิ่งนี้ให้สุดขั้วเชิงตรรกะและพึ่งพาการแฮชฟีเจอร์ทั้งหมดโดยไม่ต้องใช้คำศัพท์เลย นี้จะดำเนินการใน tf.keras.layers.Hashing ชั้น

# We set up a large number of bins to reduce the chance of hash collisions.
num_hashing_bins = 200_000

movie_title_hashing = tf.keras.layers.Hashing(
    num_bins=num_hashing_bins
)

เราทำการค้นหาได้เหมือนเมื่อก่อนโดยไม่จำเป็นต้องสร้างคำศัพท์:

movie_title_hashing(["Star Wars (1977)", "One Flew Over the Cuckoo's Nest (1975)"])
<tf.Tensor: shape=(2,), dtype=int64, numpy=array([101016,  96565])>

การกำหนดการฝัง

ตอนนี้เรามีรหัสจำนวนเต็มเราสามารถใช้ Embedding ชั้นเพื่อเปิดเหล่านั้นลงใน embeddings

เลเยอร์การฝังมีสองมิติ: มิติแรกบอกเราว่าเราสามารถฝังหมวดหมู่ที่แตกต่างกันได้กี่หมวดหมู่ อันที่สองบอกเราว่าเวกเตอร์ที่แทนแต่ละอันนั้นมีขนาดใหญ่เพียงใด

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

movie_title_embedding = tf.keras.layers.Embedding(
    # Let's use the explicit vocabulary lookup.
    input_dim=movie_title_lookup.vocab_size(),
    output_dim=32
)
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.

เราสามารถรวมทั้งสองเข้าด้วยกันเป็นชั้นเดียวซึ่งนำข้อความดิบเข้ามาและทำให้เกิดการฝัง

movie_title_model = tf.keras.Sequential([movie_title_lookup, movie_title_embedding])

ในทำนองเดียวกัน เราสามารถฝังเนื้อหาสำหรับชื่อภาพยนตร์ของเราได้โดยตรง:

movie_title_model(["Star Wars (1977)"])
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'list'> input: ['Star Wars (1977)']
Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'list'> input: ['Star Wars (1977)']
Consider rewriting this model with the Functional API.
<tf.Tensor: shape=(1, 32), dtype=float32, numpy=
array([[-0.00255408,  0.00941082,  0.02599109, -0.02758816, -0.03652344,
        -0.03852248, -0.03309812, -0.04343383,  0.03444691, -0.02454401,
         0.00619583, -0.01912323, -0.03988413,  0.03595274,  0.00727529,
         0.04844356,  0.04739804,  0.02836904,  0.01647964, -0.02924066,
        -0.00425701,  0.01747661,  0.0114414 ,  0.04916174,  0.02185034,
        -0.00399858,  0.03934855,  0.03666003,  0.01980535, -0.03694187,
        -0.02149243, -0.03765338]], dtype=float32)>

เราสามารถทำเช่นเดียวกันกับการฝังผู้ใช้:

user_id_lookup = tf.keras.layers.StringLookup()
user_id_lookup.adapt(ratings.map(lambda x: x["user_id"]))

user_id_embedding = tf.keras.layers.Embedding(user_id_lookup.vocab_size(), 32)

user_id_model = tf.keras.Sequential([user_id_lookup, user_id_embedding])
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.

ทำให้คุณสมบัติต่อเนื่องเป็นปกติ

คุณสมบัติต่อเนื่องยังต้องทำให้เป็นมาตรฐาน ยกตัวอย่างเช่น timestamp คุณลักษณะอยู่ไกลขนาดใหญ่เกินไปที่จะนำมาใช้โดยตรงในรูปแบบลึก:

for x in ratings.take(3).as_numpy_iterator():
  print(f"Timestamp: {x['timestamp']}.")
Timestamp: 879024327.
Timestamp: 875654590.
Timestamp: 882075110.

เราต้องประมวลผลก่อนจึงจะสามารถใช้งานได้ แม้ว่าจะมีหลายวิธีที่เราสามารถทำได้ แต่การแยกส่วนและการกำหนดมาตรฐานเป็นสองวิธีทั่วไป

มาตรฐาน

มาตรฐาน rescales ให้บริการปกติช่วงของพวกเขาโดยการลบคุณลักษณะของค่าเฉลี่ยและหารด้วยส่วนเบี่ยงเบนมาตรฐาน เป็นการเปลี่ยนแปลงก่อนการประมวลผลทั่วไป

นี้สามารถทำได้อย่างง่ายดายโดยใช้ tf.keras.layers.Normalization ชั้น:

timestamp_normalization = tf.keras.layers.Normalization(
    axis=None
)
timestamp_normalization.adapt(ratings.map(lambda x: x["timestamp"]).batch(1024))

for x in ratings.take(3).as_numpy_iterator():
  print(f"Normalized timestamp: {timestamp_normalization(x['timestamp'])}.")
Normalized timestamp: [-0.84293723].
Normalized timestamp: [-1.4735204].
Normalized timestamp: [-0.27203268].

ดุลยพินิจ

การเปลี่ยนแปลงทั่วไปอีกประการหนึ่งคือการเปลี่ยนคุณลักษณะที่ต่อเนื่องเป็นคุณลักษณะตามหมวดหมู่จำนวนหนึ่ง สิ่งนี้สมเหตุสมผลดีหากเรามีเหตุผลที่จะสงสัยว่าเอฟเฟกต์ของฟีเจอร์นั้นไม่ต่อเนื่อง

ในการทำเช่นนี้ ก่อนอื่นเราต้องกำหนดขอบเขตของที่เก็บข้อมูลที่เราจะใช้สำหรับการแยกส่วน วิธีที่ง่ายที่สุดคือการระบุค่าต่ำสุดและสูงสุดของคุณลักษณะ และแบ่งช่วงเวลาผลลัพธ์เท่าๆ กัน:

max_timestamp = ratings.map(lambda x: x["timestamp"]).reduce(
    tf.cast(0, tf.int64), tf.maximum).numpy().max()
min_timestamp = ratings.map(lambda x: x["timestamp"]).reduce(
    np.int64(1e9), tf.minimum).numpy().min()

timestamp_buckets = np.linspace(
    min_timestamp, max_timestamp, num=1000)

print(f"Buckets: {timestamp_buckets[:3]}")
Buckets: [8.74724710e+08 8.74743291e+08 8.74761871e+08]

ด้วยขอบเขตของบัคเก็ต เราสามารถเปลี่ยนการประทับเวลาเป็นการฝังได้:

timestamp_embedding_model = tf.keras.Sequential([
  tf.keras.layers.Discretization(timestamp_buckets.tolist()),
  tf.keras.layers.Embedding(len(timestamp_buckets) + 1, 32)
])

for timestamp in ratings.take(1).map(lambda x: x["timestamp"]).batch(1).as_numpy_iterator():
  print(f"Timestamp embedding: {timestamp_embedding_model(timestamp)}.")
Timestamp embedding: [[-0.02532113 -0.00415025  0.00458465  0.02080876  0.03103903 -0.03746337
   0.04010465 -0.01709593 -0.00246077 -0.01220842  0.02456966 -0.04816503
   0.04552222  0.03535838  0.00769508  0.04328252  0.00869263  0.01110227
   0.02754457 -0.02659499 -0.01055292 -0.03035731  0.00463334 -0.02848787
  -0.03416766  0.02538678 -0.03446608 -0.0384447  -0.03032914 -0.02391632
   0.02637175 -0.01158618]].

กำลังประมวลผลคุณสมบัติข้อความ

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

แม้ว่าชุดข้อมูล MovieLens ไม่ได้ให้คุณสมบัติข้อความที่สมบูรณ์ แต่เรายังคงใช้ชื่อภาพยนตร์ได้ ซึ่งอาจช่วยให้เราเข้าใจข้อเท็จจริงที่ว่าภาพยนตร์ที่มีชื่อคล้ายกันมากมักจะอยู่ในซีรีส์เดียวกัน

การแปลงรูปแบบแรกที่เราต้องใช้กับข้อความคือการสร้างโทเค็น (แบ่งเป็นคำหรือชิ้นส่วนคำ) ตามด้วยการเรียนรู้คำศัพท์ ตามด้วยฝัง

Keras tf.keras.layers.TextVectorization ชั้นสามารถทำสองขั้นตอนแรกเรา:

title_text = tf.keras.layers.TextVectorization()
title_text.adapt(ratings.map(lambda x: x["movie_title"]))

มาลองดูกัน:

for row in ratings.batch(1).map(lambda x: x["movie_title"]).take(1):
  print(title_text(row))
tf.Tensor([[ 32 266 162   2 267 265  53]], shape=(1, 7), dtype=int64)

แต่ละชื่อได้รับการแปลเป็นลำดับของโทเค็น หนึ่งรายการสำหรับแต่ละชิ้นที่เราสร้างโทเค็น

เราสามารถตรวจสอบคำศัพท์ที่เรียนรู้เพื่อตรวจสอบว่าเลเยอร์นั้นใช้โทเค็นที่ถูกต้องหรือไม่:

title_text.get_vocabulary()[40:45]
['first', '1998', '1977', '1971', 'monty']

ดูเหมือนถูกต้อง: เลเยอร์กำลังแปลงชื่อเป็นคำแต่ละคำ

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

วางมันทั้งหมดเข้าด้วยกัน

ด้วยองค์ประกอบเหล่านี้ เราจึงสามารถสร้างแบบจำลองที่รวมการประมวลผลล่วงหน้าทั้งหมดเข้าด้วยกัน

รุ่นผู้ใช้

โมเดลผู้ใช้แบบเต็มอาจมีลักษณะดังนี้:

class UserModel(tf.keras.Model):

  def __init__(self):
    super().__init__()

    self.user_embedding = tf.keras.Sequential([
        user_id_lookup,
        tf.keras.layers.Embedding(user_id_lookup.vocab_size(), 32),
    ])
    self.timestamp_embedding = tf.keras.Sequential([
      tf.keras.layers.Discretization(timestamp_buckets.tolist()),
      tf.keras.layers.Embedding(len(timestamp_buckets) + 2, 32)
    ])
    self.normalized_timestamp = tf.keras.layers.Normalization(
        axis=None
    )

  def call(self, inputs):

    # Take the input dictionary, pass it through each input layer,
    # and concatenate the result.
    return tf.concat([
        self.user_embedding(inputs["user_id"]),
        self.timestamp_embedding(inputs["timestamp"]),
        tf.reshape(self.normalized_timestamp(inputs["timestamp"]), (-1, 1))
    ], axis=1)

มาลองดูกัน:

user_model = UserModel()

user_model.normalized_timestamp.adapt(
    ratings.map(lambda x: x["timestamp"]).batch(128))

for row in ratings.batch(1).take(1):
  print(f"Computed representations: {user_model(row)[0, :3]}")
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
Computed representations: [-0.04705765 -0.04739009 -0.04212048]

โมเดลภาพยนตร์

เราสามารถทำเช่นเดียวกันกับโมเดลภาพยนตร์:

class MovieModel(tf.keras.Model):

  def __init__(self):
    super().__init__()

    max_tokens = 10_000

    self.title_embedding = tf.keras.Sequential([
      movie_title_lookup,
      tf.keras.layers.Embedding(movie_title_lookup.vocab_size(), 32)
    ])
    self.title_text_embedding = tf.keras.Sequential([
      tf.keras.layers.TextVectorization(max_tokens=max_tokens),
      tf.keras.layers.Embedding(max_tokens, 32, mask_zero=True),
      # We average the embedding of individual words to get one embedding vector
      # per title.
      tf.keras.layers.GlobalAveragePooling1D(),
    ])

  def call(self, inputs):
    return tf.concat([
        self.title_embedding(inputs["movie_title"]),
        self.title_text_embedding(inputs["movie_title"]),
    ], axis=1)

มาลองดูกัน:

movie_model = MovieModel()

movie_model.title_text_embedding.layers[0].adapt(
    ratings.map(lambda x: x["movie_title"]))

for row in ratings.batch(1).take(1):
  print(f"Computed representations: {movie_model(row)[0, :3]}")
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
Computed representations: [-0.01670959  0.02128791  0.04631067]

ขั้นตอนถัดไป

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