Skip to content

YoutubeDNN Tutorial

1. Model Overview and Use Cases

YoutubeDNN is a deep neural retrieval model proposed by Google at RecSys 2016 and is one of the core components of the YouTube recommendation system. Unlike DSSM, YoutubeDNN uses list-wise training (global ranking with Softmax). In the original paper, the item tower directly uses an embedding rather than passing it through a DNN.

Paper: Deep Neural Networks for YouTube Recommendations

Model Architecture

YoutubeDNN Model Architecture
  • User Tower: maps user attributes and behavior sequences to a user embedding through a DNN
  • Item Tower: directly uses item embeddings (without a DNN)
  • Training: list-wise training with Softmax and negative sampling
  • Negative Sampling: this tutorial uses generate_seq_feature_match(..., mode=2) to generate an explicit neg_items list; the model applies sampled softmax to the positive item and these negatives

Suitable Scenarios

  • Retrieval from a large candidate set
  • Scenarios with rich user behavior sequences
  • Video and content recommendation systems
  • Scenarios that require list-wise ranking optimization

2. Data Preparation and Preprocessing

This tutorial uses the MovieLens-1M dataset. YoutubeDNN uses mode=2 to generate list-wise training data containing one positive item and multiple negative items.

python
import os
import numpy as np
import pandas as pd
import torch
from sklearn.preprocessing import LabelEncoder

from torch_rechub.basic.features import SparseFeature, SequenceFeature
from torch_rechub.utils.data import MatchDataGenerator, df_to_dict
from torch_rechub.utils.match import gen_model_input, generate_seq_feature_match

data = pd.read_csv("examples/matching/data/ml-1m/ml-1m_sample.csv")
data["cate_id"] = data["genres"].apply(lambda x: x.split("|")[0])

sparse_features = ['user_id', 'movie_id', 'gender', 'age', 'occupation', 'zip', 'cate_id']
user_col, item_col = "user_id", "movie_id"

feature_max_idx = {}
for feature in sparse_features:
    lbe = LabelEncoder()
    data[feature] = lbe.fit_transform(data[feature]) + 1
    feature_max_idx[feature] = data[feature].max() + 1

user_profile = data[["user_id", "gender", "age", "occupation", "zip"]].drop_duplicates("user_id")
item_profile = data[["movie_id", "cate_id"]].drop_duplicates("movie_id")

# mode=2: list-wise negative sampling; negative items are stored in the "neg_items" column
df_train, df_test = generate_seq_feature_match(
    data, user_col, item_col,
    time_col="timestamp",
    item_attribute_cols=[],
    sample_method=1,
    mode=2,           # list-wise mode
    neg_ratio=3,
    min_item=0
)

x_train = gen_model_input(df_train, user_profile, user_col, item_profile, item_col, seq_max_len=50)
# In list-wise training, label 0 means that the first position contains the positive item
y_train = np.array([0] * df_train.shape[0])
x_test = gen_model_input(df_test, user_profile, user_col, item_profile, item_col, seq_max_len=50)

Define Features

python
user_cols = ['user_id', 'gender', 'age', 'occupation', 'zip']

# User features = user attributes + historical behavior sequence
user_features = [
    SparseFeature(name, vocab_size=feature_max_idx[name], embed_dim=16)
    for name in user_cols
]
user_features += [
    SequenceFeature("hist_movie_id", vocab_size=feature_max_idx["movie_id"],
                    embed_dim=16, pooling="mean", shared_with="movie_id")
]

# Item features (movie_id embedding only)
item_features = [
    SparseFeature("movie_id", vocab_size=feature_max_idx["movie_id"], embed_dim=16)
]

# Negative-item feature
neg_item_feature = [
    SequenceFeature("neg_items", vocab_size=feature_max_idx["movie_id"],
                    embed_dim=16, pooling="concat", shared_with="movie_id")
]

all_item = df_to_dict(item_profile)
test_user = x_test

# Create DataLoaders
dg = MatchDataGenerator(x=x_train, y=y_train)
train_dl, test_dl, item_dl = dg.generate_dataloader(test_user, all_item, batch_size=2048, num_workers=0)

3. Model Configuration and Parameter Reference

3.1 Create the Model

python
from torch_rechub.models.matching import YoutubeDNN

model = YoutubeDNN(
    user_features=user_features,
    item_features=item_features,
    neg_item_feature=neg_item_feature,
    user_params={"dims": [128, 64, 16]},
    temperature=0.02
)

3.2 Parameter Details

ParameterTypeDescriptionRecommended Value
user_featureslist[Feature]User-side featuresUser attributes + sequence
item_featureslist[Feature]Positive-item featuresItem ID
neg_item_featurelist[Feature]Negative-item featuresSequenceFeature + pooling="concat"
user_params.dimslist[int]User Tower MLP dimensions[128, 64, 16]
temperaturefloatTemperature coefficient0.02–0.1

Note: The shared_with value of neg_item_feature must match the item ID feature name in item_features, ensuring that positive and negative items share the same embedding.


4. Training Process and Code Example

python
import os
from torch_rechub.trainers import MatchTrainer

torch.manual_seed(2022)
save_dir = "./saved/youtube_dnn/"
os.makedirs(save_dir, exist_ok=True)

trainer = MatchTrainer(
    model,
    mode=2,                     # list-wise training; YoutubeDNN normally does not use point-wise mode
    optimizer_params={
        "lr": 1e-4,
        "weight_decay": 1e-6
    },
    n_epoch=10,
    device="cpu",
    model_path=save_dir
)

trainer.fit(train_dl)

DSSM vs. YoutubeDNN Training Modes

ItemDSSM (mode=0)YoutubeDNN (mode=2)
Training objectivePoint-wise (BCE)List-wise (Softmax)
Negative samplingIndependent negative samplesA list containing one positive and multiple negative items
Labels0/1Always 0 (the first item in the list is positive)

5. Model Evaluation and Result Analysis

python
# Generate embeddings
user_embedding = trainer.inference_embedding(
    model=model, mode="user",
    data_loader=test_dl,
    model_path=save_dir
)
item_embedding = trainer.inference_embedding(
    model=model, mode="item",
    data_loader=item_dl,
    model_path=save_dir
)

print(f"User Embedding: {user_embedding.shape}")
print(f"Item Embedding: {item_embedding.shape}")

6. Tuning Recommendations

  1. User Tower dimensions: YoutubeDNN's User Tower dimensions should decrease layer by layer (for example, [128, 64, 16]); the final dimension determines the embedding size
  2. Number of negative samples: neg_ratio=3~10; more negatives can often improve quality but increase training time
  3. Sequence length: seq_max_len=50 is a good starting point and can be adjusted according to the actual user-behavior distribution
  4. Temperature: as with DSSM, 0.02 is a recommended starting point

6.1 Vector Retrieval and Deployment

As with DSSM, after training YoutubeDNN you need to insert its embeddings into a vector index for ANN retrieval.

python
from torch_rechub.utils.match import Annoy, Faiss

# Option 1: Annoy (fast prototyping)
annoy = Annoy(n_trees=10)
annoy.fit(item_embedding)
indices, distances = annoy.query(user_embedding[0], n=10)

# Option 2: Faiss (high performance)
import numpy as np
item_emb_np = item_embedding.cpu().numpy().astype(np.float32)
faiss_index = Faiss(dim=item_emb_np.shape[1], index_type='flat', metric='l2')
faiss_index.fit(item_emb_np)
indices, distances = faiss_index.query(user_embedding[0].cpu().numpy().astype(np.float32), n=10)

# Save the index for online serving
faiss_index.save_index("youtube_dnn_item.index")

For more vector-retrieval details, see section “6.2 Vector Retrieval and Deployment” in the DSSM tutorial, which explains each backend's dependencies and implementation boundaries.


7. Model Visualization

python
from torch_rechub.utils.visualization import visualize_model

# Automatically generate inputs and visualize the model
graph = visualize_model(model, depth=4)

# Save the image
visualize_model(model, save_path="youtube_dnn_arch.png", dpi=300)

YoutubeDNN Architecture

YoutubeDNN model architecture

Install the dependencies with pip install torch-rechub[visualization], and install Graphviz on the system (Ubuntu: apt-get install graphviz / macOS: brew install graphviz / Windows: choco install graphviz).


8. ONNX Export

First install the optional ONNX dependencies:

bash
pip install "torch-rechub[onnx]"
python
from torch_rechub.utils.onnx_export import ONNXExporter

exporter = ONNXExporter(model, device="cpu")

# Export the User and Item Towers separately
exporter.export("youtube_user_tower.onnx", mode="user")
exporter.export("youtube_item_tower.onnx", mode="item")

9. FAQ and Troubleshooting

Q1: What are the main differences between YoutubeDNN and DSSM?

  • Different training objectives: DSSM is point-wise, whereas YoutubeDNN is list-wise
  • Different Item Towers: DSSM has a DNN in its Item Tower, whereas YoutubeDNN only uses an embedding
  • Different loss functions: DSSM uses BCE, whereas YoutubeDNN uses Softmax

Q2: Why does y_train contain only zeros?

In list-wise (mode=2) training, label 0 means that the item in the first position of the list is positive. The model must learn to rank that positive item above the negatives.

Q3: Why does neg_item_feature use pooling="concat"?

The negative samples form a list of multiple items. pooling="concat" preserves them in a [batch_size, n_neg, embed_dim] tensor for the list-wise computation.

Q4: How can I export ONNX models for online deployment?

Use ONNXExporter to export the User and Item Towers separately. Run the User Tower with ONNX Runtime online, then retrieve items from a vector index such as Faiss or Milvus.


Complete Example

python
import os
import numpy as np
import pandas as pd
import torch
from sklearn.preprocessing import LabelEncoder

from torch_rechub.basic.features import SparseFeature, SequenceFeature
from torch_rechub.models.matching import YoutubeDNN
from torch_rechub.trainers import MatchTrainer
from torch_rechub.utils.data import MatchDataGenerator, df_to_dict
from torch_rechub.utils.match import gen_model_input, generate_seq_feature_match, Annoy


def main():
    torch.manual_seed(2022)
    save_dir = "./saved/youtube_dnn/"
    os.makedirs(save_dir, exist_ok=True)

    data = pd.read_csv("examples/matching/data/ml-1m/ml-1m_sample.csv")
    data["cate_id"] = data["genres"].apply(lambda x: x.split("|")[0])
    sparse_features = ['user_id', 'movie_id', 'gender', 'age', 'occupation', 'zip', 'cate_id']
    user_col, item_col = "user_id", "movie_id"

    feature_max_idx = {}
    for feature in sparse_features:
        lbe = LabelEncoder()
        data[feature] = lbe.fit_transform(data[feature]) + 1
        feature_max_idx[feature] = data[feature].max() + 1

    user_profile = data[["user_id", "gender", "age", "occupation", "zip"]].drop_duplicates("user_id")
    item_profile = data[["movie_id", "cate_id"]].drop_duplicates("movie_id")

    df_train, df_test = generate_seq_feature_match(
        data, user_col, item_col, time_col="timestamp",
        item_attribute_cols=[], sample_method=1, mode=2, neg_ratio=3, min_item=0
    )
    x_train = gen_model_input(df_train, user_profile, user_col, item_profile, item_col, seq_max_len=50)
    y_train = np.array([0] * df_train.shape[0])
    x_test = gen_model_input(df_test, user_profile, user_col, item_profile, item_col, seq_max_len=50)

    user_cols = ['user_id', 'gender', 'age', 'occupation', 'zip']
    user_features = [SparseFeature(name, vocab_size=feature_max_idx[name], embed_dim=16) for name in user_cols]
    user_features += [SequenceFeature("hist_movie_id", vocab_size=feature_max_idx["movie_id"], embed_dim=16, pooling="mean", shared_with="movie_id")]
    item_features = [SparseFeature("movie_id", vocab_size=feature_max_idx["movie_id"], embed_dim=16)]
    neg_item_feature = [SequenceFeature("neg_items", vocab_size=feature_max_idx["movie_id"], embed_dim=16, pooling="concat", shared_with="movie_id")]

    all_item = df_to_dict(item_profile)
    test_user = x_test

    dg = MatchDataGenerator(x=x_train, y=y_train)
    model = YoutubeDNN(user_features, item_features, neg_item_feature, user_params={"dims": [128, 64, 16]}, temperature=0.02)

    trainer = MatchTrainer(model, mode=2, optimizer_params={"lr": 1e-4, "weight_decay": 1e-6},
                           n_epoch=10, device="cpu", model_path=save_dir)

    train_dl, test_dl, item_dl = dg.generate_dataloader(test_user, all_item, batch_size=2048, num_workers=0)
    trainer.fit(train_dl)

    user_embedding = trainer.inference_embedding(model=model, mode="user", data_loader=test_dl, model_path=save_dir)
    item_embedding = trainer.inference_embedding(model=model, mode="item", data_loader=item_dl, model_path=save_dir)
    print(f"User Embedding: {user_embedding.shape}, Item Embedding: {item_embedding.shape}")

    # Vector retrieval
    annoy = Annoy(n_trees=10)
    annoy.fit(item_embedding)
    for i in range(min(5, len(user_embedding))):
        indices, distances = annoy.query(user_embedding[i], n=10)
        print(f"User {i} -> Top-10 Items: {indices}")


if __name__ == "__main__":
    main()