ディープラーニングレコメンドシステム(4) Wide&DeepモデルとCriteoデータセットへの応用
2016年には、MicrosoftのDeep Crossing、Googleの Wide&Deep、FNN、PNNなどの優れたディープラーニングモデルが多数導入され、レコメンドシステムは本格的にディープラーニングの時代に入り、現在でも主流となっています。レコメンデーション モデルには主に次の 2 つの展開があります。
-
従来の機械学習モデルと比較して、深層学習モデルはより強力な表現力を備えており、データ内のより多くの隠れたパターンを掘り出すことができます。
-
深層学習モデルの構造は非常に柔軟であり、ビジネス シナリオやデータの特性に応じて柔軟に調整して、モデルをアプリケーション シナリオに完全に適合させることができます。
深層学習レコメンデーションモデルは、多層パーセプトロン(MLP)を核とし、ニューラルネットワーク構造を変更することで進化します。
1 Wide&Deepモデルの原則
1.1 Wide&Deepモデルの背景
-
協調フィルタリングやロジスティック回帰などの単純なモデルは、履歴データから高頻度で共起する特徴の組み合わせを学習できますが、一般化機能は不十分です。
-
また、行列分解と同様に、埋め込みと深層学習ネットワークでは、相関の推移性を使用して履歴データに現れない特徴の組み合わせを調査し、データの潜在的な相関パターンをマイニングできますが、これは一部の特定のシナリオ (データ分布のロングテール、 -出現行列が疎で高順位であるため、低次元表現を効果的に学習することが難しく、その結果、推奨事項が過度に一般化されてしまいます。
そこで、2016 年に Google は、線形モデルと DNN を完全に組み合わせた Wide&Deep モデルを提案しました提高模型泛化能力的同时,兼顾模型的记忆性
。線形モデルと DNN 間の並列接続モードである Wide&Deep は、後にレコメンデーション分野の古典的なモードとなり、その後の深層学習モデルの基礎を築きました。
1.2 モデルの記憶能力と汎化能力
1.2.1 記憶力の理解
记忆能力
これは、履歴データ内の項目と特徴を直接学習して利用するモデルの共现频率
機能として理解できます。
如果点击A, 就推荐B
一般に、協調フィルタリングやロジスティック回帰はいずれも強力な「記憶力」を持っており、このようなモデルは比較的単純であるため、元のデータがレコメンデーション結果に直接影響を与えることが多く、モデルと直接同等のルールに似たレコメンデーションを生成することができることを覚えておいてください。履歴データの分布特性を把握し、これらの記憶を使用して推奨事項を作成します。
以谷歌APP推荐场景为例理解一下:
Google Play レコメンデーション モデルのトレーニング プロセス中に、次の組み合わせ機能が設定されているとします。 AND(user_installed_app=netflix,
impression_app=pandora)。これは、ユーザーが Netflix アプリをインストールし、アプリ内で Pandora アプリを閲覧したことを表します。ストア。
「最終的にpandoraをインストールするかどうか」をラベルにすると、netfilx&pandora機能とpandoraインストールラベルの共起頻度を簡単にカウントできます。たとえば、この 2 つの共起頻度は 10% にも達するため、モデルを設計する際には、モデルがこの特徴を見つけさえすれば、pandora アプリケーションを推奨することを期待します (深い知識のように脳に刻み込まれます)。メモリポイント)、いわゆる「メモリ容量」です。
ロジスティック回帰のようなモデルは、そのような強力な特徴を見つけると、重みを増やしてこの特徴を直接記憶します。
ただし、ニューラル ネットワークなどのモデルでは、特徴が多層で処理され、常に他の特徴と交差するため、モデルの強力な特殊記憶は単純なモデルほど深くありません。
1.2.2 汎化能力の理解
泛化能力
として理解できます模型传递特征的相关性, 以及发掘稀疏甚至从未出现过的稀有特征与最终标签相关性的能力
。
たとえば、行列分解や埋め込みなどにより、スパース データを持つユーザーまたはアイテムが潜在ベクトルを生成できるようになり、それによってデータによってサポートされる推奨スコアが取得され、グローバル データがスパース アイテムに渡され、汎化機能が向上します。
もう 1 つの例はニューラル ネットワークです。これは、データ内の潜在的なパターンを深く調査し、機能の自動組み合わせによって一般化を向上させることができます。
したがって、 Wide&Deep モデルの直接の動機は、この 2 つを統合して、モデルが単純なモデルの「記憶能力」と、記憶と一般化の組み合わせでもあるニューラル ネットワークの「汎化能力」の両方を備えるようにすることです。 . 素晴らしいパターンへの最初の試み。
1.3 Wide&Deepモデルの構造
古典的な W&D モデルを次の図に示します。
-
左側の部分は単純な線形モデルである広い部分であり、右側の部分は古典的な DNN モデルである深い部分です。
-
W&D モデルは、単一の入力層の合計を結合し
Wide部分
、Embedding+多层的全连接的部分(deep部分)
最終出力層に一緒に入力して予測結果を取得します。 -
单层的wide层善于处理大量的稀疏的id类特征
, ディープ パートでは、ディープ フィーチャ交差を使用して、フィーチャの背後にあるデータ パターンをマイニングします。最後に、ロジスティック回帰を使用して、出力層部分を Deep と組み合わせて、統合モデルを形成します。
1.3.1 幅広部
-
ワイド パート トレーニング中に使用されるオプティマイザーの場合
带正则的FTRL算法(Follow-the-regularized-leader)
、FTRL は、優れたスパース性と優れた精度を備えた確率的勾配降下法とみなすことができ、このアルゴリズムはモデルのスパースな性質に細心の注意を払っています。 -
つまり、W&D モデルは L1 FTRL を使用し
想让Wide部分变得更加的稀疏,即Wide部分的大部分参数都为0
、モデルの重みと特徴ベクトルの次元を大幅に圧縮します。 -
モデルのワイド部分がトレーニングされた後に残る特徴は非常に重要です
模型的“记忆能力”可以理解为发现"直接的",“暴力的”,“显然的”关联规则的能力
。
1.3.2 深部
-
主にディープ部分
一个Embedding+MLP的神经网络模型
。 -
大規模な疎な特徴は、埋め込みによって低次元の密な特徴に変換されます。次に、特徴が結合されて MLP に入力され、特徴の背後に隠されたデータ パターンがマイニングされます。
-
入力特徴には 2 種類あり、1 つは数値特徴、もう 1 つはカテゴリ特徴 (埋め込み後) です。
-
DNN モデルの層の数が増えると、中間の特徴がより抽象化され、モデルの汎化能力が向上します。
-
DNN モデルの深層部分では、著者は一般的に使用されている深層学習手法を使用しています
优化器AdaGrad
。これは、モデルがより正確なソリューションを取得できるようにするためでもあります。
1.3.3 Wide&Deepの詳細モデル構造
上の図から、Google Play レコメンデーション チームが設計した Wide&Deep モデルのディープ部分への入力としてどの特徴が使用され、ワイド部分への入力としてどの特徴が使用されているかを詳細に知ることができます。
-
Wide 部分への入力は
已安装应用和曝光应用
2 種類の機能だけです。インストールされたアプリケーションはユーザーの過去の行動を表し、公開されたアプリケーションは推奨される現在のアプリケーションを表します。この 2 種類の機能を選択した理由は、记忆能力
ワイド部分の強力な利点を最大限に発揮するためです。 -
Deep 部分の入力は、ユーザーの年齢 (Age)、インストールされているアプリケーションの数 (#App Installs)、デバイスの種類 (Device Class)、インストールされているアプリケーション (User Installed App)、露出アプリケーション (Impression) を含む特徴ベクトルの完全なセットです。アプリ)などの機能があります。インストールされたアプリケーションや公開されたアプリケーションなどのカテゴリ機能は、Embedding 層を介して Concatenated Embedding 層に入力され、1200 次元に結合され、その後 3 層の ReLU 全結合層を通過し、最後に LogLoss 出力層に入力される必要があります。埋め込みベクトル。
1.4 Wide&Deepモデルコード
import torch.nn as nn
import torch.nn.functional as F
import torch
class Linear(nn.Module):
"""
Linear part
"""
def __init__(self, input_dim):
super(Linear, self).__init__()
self.linear = nn.Linear(in_features=input_dim, out_features=1)
def forward(self, x):
return self.linear(x)
class Dnn(nn.Module):
"""
Dnn part
"""
def __init__(self, hidden_units, dropout=0.5):
"""
hidden_units: 列表, 每个元素表示每一层的神经单元个数, 比如[256, 128, 64], 两层网络, 第一层神经单元128, 第二层64, 第一个维度是输入维度
dropout: 失活率
"""
super(Dnn, self).__init__()
self.dnn_network = nn.ModuleList(
[nn.Linear(layer[0], layer[1]) for layer in list(zip(hidden_units[:-1], hidden_units[1:]))])
self.dropout = nn.Dropout(p=dropout)
def forward(self, x):
for linear in self.dnn_network:
x = linear(x)
x = F.relu(x)
x = self.dropout(x)
return x
'''
WideDeep模型:
主要包括Wide部分和Deep部分
'''
class WideDeep(nn.Module):
def __init__(self, feature_info, hidden_units, embed_dim=8):
"""
DeepCrossing:
feature_info: 特征信息(数值特征, 类别特征, 类别特征embedding映射)
hidden_units: 列表, 隐藏单元
dropout: Dropout层的失活比例
embed_dim: embedding维度
"""
super(WideDeep, self).__init__()
self.dense_features, self.sparse_features, self.sparse_features_map = feature_info
# embedding层, 这里需要一个列表的形式, 因为每个类别特征都需要embedding
self.embed_layers = nn.ModuleDict(
{
'embed_' + str(key): nn.Embedding(num_embeddings=val, embedding_dim=embed_dim)
for key, val in self.sparse_features_map.items()
}
)
# 统计embedding_dim的总维度
# 一个离散型(类别型)变量 通过embedding层变为8纬
embed_dim_sum = sum([embed_dim] * len(self.sparse_features))
# 总维度 = 数值型特征的纬度 + 离散型变量经过embedding后的纬度
dim_sum = len(self.dense_features) + embed_dim_sum
hidden_units.insert(0, dim_sum)
# dnn网络
self.dnn_network = Dnn(hidden_units)
# 线性层
self.linear = Linear(input_dim=len(self.dense_features))
# 最终的线性层
self.final_linear = nn.Linear(hidden_units[-1], 1)
def forward(self, x):
# 1、先把输入向量x分成两部分处理、因为数值型和类别型的处理方式不一样
dense_input, sparse_inputs = x[:, :len(self.dense_features)], x[:, len(self.dense_features):]
# 2、转换为long形
sparse_inputs = sparse_inputs.long()
# 2、不同的类别特征分别embedding
sparse_embeds = [
self.embed_layers['embed_' + key](sparse_inputs[:, i]) for key, i in
zip(self.sparse_features_map.keys(), range(sparse_inputs.shape[1]))
]
# 3、把类别型特征进行拼接,即emdedding后,由3行转换为1行
sparse_embeds = torch.cat(sparse_embeds, axis=-1)
# 4、数值型和类别型特征进行拼接
dnn_input = torch.cat([sparse_embeds, dense_input], axis=-1)
# Wide部分,使用的特征为数值型类型
wide_out = self.linear(dense_input)
# Deep部分,使用全部特征
deep_out = self.dnn_network(dnn_input)
deep_out = self.final_linear(deep_out)
# out 将Wide部分的输出和Deep部分的输出进行合并
outputs = F.sigmoid(0.5 * (wide_out + deep_out))
return outputs
if __name__ == '__main__':
x = torch.rand(size=(1, 5), dtype=torch.float32)
feature_info = [
['I1', 'I2'], # 连续性特征
['C1', 'C2', 'C3'], # 离散型特征
{
'C1': 20,
'C2': 20,
'C3': 20
}
]
# 建立模型
hidden_units = [256, 128, 64]
net = WideDeep(feature_info, hidden_units)
print(net)
print(net(x))
WideDeep(
(embed_layers): ModuleDict(
(embed_C1): Embedding(20, 8)
(embed_C2): Embedding(20, 8)
(embed_C3): Embedding(20, 8)
)
(dnn_network): Dnn(
(dnn_network): ModuleList(
(0): Linear(in_features=26, out_features=256, bias=True)
(1): Linear(in_features=256, out_features=128, bias=True)
(2): Linear(in_features=128, out_features=64, bias=True)
)
(dropout): Dropout(p=0.5, inplace=False)
)
(linear): Linear(
(linear): Linear(in_features=2, out_features=1, bias=True)
)
(final_linear): Linear(in_features=64, out_features=1, bias=True)
)
tensor([[0.6531]], grad_fn=<SigmoidBackward0>)
2 Criteo データセットにおける Wide&Deep モデルの適用
データの前処理と一部の関数またはクラスについては、以下を参照してください。
ディープラーニングレコメンドシステム (2) Deep Crossing と Criteo データセットへの応用
2.1 学習データの準備
import pandas as pd
import torch
from torch.utils.data import TensorDataset, Dataset, DataLoader
import torch.nn as nn
from sklearn.metrics import auc, roc_auc_score, roc_curve
import warnings
warnings.filterwarnings('ignore')
# 封装为函数
def prepared_data(file_path):
# 读入训练集,验证集和测试集
train_set = pd.read_csv(file_path + 'train_set.csv')
val_set = pd.read_csv(file_path + 'val_set.csv')
test_set = pd.read_csv(file_path + 'test.csv')
# 这里需要把特征分成数值型和离散型
# 因为后面的模型里面离散型的特征需要embedding, 而数值型的特征直接进入了stacking层, 处理方式会不一样
data_df = pd.concat((train_set, val_set, test_set))
# 数值型特征直接放入stacking层
dense_features = ['I' + str(i) for i in range(1, 14)]
# 离散型特征需要需要进行embedding处理
sparse_features = ['C' + str(i) for i in range(1, 27)]
# 定义一个稀疏特征的embedding映射, 字典{key: value},
# key表示每个稀疏特征, value表示数据集data_df对应列的不同取值个数, 作为embedding输入维度
sparse_feas_map = {
}
for key in sparse_features:
sparse_feas_map[key] = data_df[key].nunique()
feature_info = [dense_features, sparse_features, sparse_feas_map] # 这里把特征信息进行封装, 建立模型的时候作为参数传入
# 把数据构建成数据管道
dl_train_dataset = TensorDataset(
# 特征信息
torch.tensor(train_set.drop(columns='Label').values).float(),
# 标签信息
torch.tensor(train_set['Label'].values).float()
)
dl_val_dataset = TensorDataset(
# 特征信息
torch.tensor(val_set.drop(columns='Label').values).float(),
# 标签信息
torch.tensor(val_set['Label'].values).float()
)
dl_train = DataLoader(dl_train_dataset, shuffle=True, batch_size=16)
dl_vaild = DataLoader(dl_val_dataset, shuffle=True, batch_size=16)
return feature_info,dl_train,dl_vaild,test_set
file_path = './preprocessed_data/'
feature_info,dl_train,dl_vaild,test_set = prepared_data(file_path)
2.2 Wide&Deepモデルの確立
from _01_wide_deep import WideDeep
hidden_units = [256, 128, 64]
net = WideDeep(feature_info, hidden_units)
2.3 モデルのトレーニング
from AnimatorClass import Animator
from TimerClass import Timer
# 模型的相关设置
def metric_func(y_pred, y_true):
pred = y_pred.data
y = y_true.data
return roc_auc_score(y, pred)
def try_gpu(i=0):
if torch.cuda.device_count() >= i + 1:
return torch.device(f'cuda:{
i}')
return torch.device('cpu')
def train_ch(net, dl_train, dl_vaild, num_epochs, lr, device):
"""⽤GPU训练模型"""
print('training on', device)
net.to(device)
# 二值交叉熵损失
loss_func = nn.BCELoss()
# 注意:这里没有使用原理提到的优化器
optimizer = torch.optim.Adam(params=net.parameters(), lr=lr)
animator = Animator(xlabel='epoch', xlim=[1, num_epochs],legend=['train loss', 'train auc', 'val loss', 'val auc']
,figsize=(8.0, 6.0))
timer, num_batches = Timer(), len(dl_train)
log_step_freq = 10
for epoch in range(1, num_epochs + 1):
# 训练阶段
net.train()
loss_sum = 0.0
metric_sum = 0.0
for step, (features, labels) in enumerate(dl_train, 1):
timer.start()
# 梯度清零
optimizer.zero_grad()
# 正向传播
predictions = net(features)
loss = loss_func(predictions, labels.unsqueeze(1) )
try: # 这里就是如果当前批次里面的y只有一个类别, 跳过去
metric = metric_func(predictions, labels)
except ValueError:
pass
# 反向传播求梯度
loss.backward()
optimizer.step()
timer.stop()
# 打印batch级别日志
loss_sum += loss.item()
metric_sum += metric.item()
if step % log_step_freq == 0:
animator.add(epoch + step / num_batches,(loss_sum/step, metric_sum/step, None, None))
# 验证阶段
net.eval()
val_loss_sum = 0.0
val_metric_sum = 0.0
for val_step, (features, labels) in enumerate(dl_vaild, 1):
with torch.no_grad():
predictions = net(features)
val_loss = loss_func(predictions, labels.unsqueeze(1))
try:
val_metric = metric_func(predictions, labels)
except ValueError:
pass
val_loss_sum += val_loss.item()
val_metric_sum += val_metric.item()
if val_step % log_step_freq == 0:
animator.add(epoch + val_step / num_batches, (None,None,val_loss_sum / val_step , val_metric_sum / val_step))
print(f'final: loss {
loss_sum/len(dl_train):.3f}, auc {
metric_sum/len(dl_train):.3f},'
f' val loss {
val_loss_sum/len(dl_vaild):.3f}, val auc {
val_metric_sum/len(dl_vaild):.3f}')
print(f'{
num_batches * num_epochs / timer.sum():.1f} examples/sec on {
str(device)}')
lr, num_epochs = 0.001, 10
# 其实发生了过拟合
train_ch(net, dl_train, dl_vaild, num_epochs, lr, try_gpu())
2.4 モデルの予測
y_pred_probs = net(torch.tensor(test_set.values).float())
y_pred = torch.where(
y_pred_probs>0.5,
torch.ones_like(y_pred_probs),
torch.zeros_like(y_pred_probs)
)
y_pred.data[:10]