100分钟吃掉DIN深度兴趣网络

3 种序列建模方案对比:BST vs DIN vs DIEN,电商场景 CTR 效果实测 本文深度对比了电商推荐系统中三大序列建模方案BST、DIN与DIEN的CTR预测效果。通过Transformer架构的BST模型在长序列处理上表现最优,AUC指标领先DIN约2%,而DIEN虽效果优异但计算成本较高。文章提供了详细的工业级评测数据和实战部署建议,帮助开发者在推荐系统建设中做出合理技术选型。 阅读详情

阿里巴巴在CTR预估领域有3篇比较有名的文章。

2017年的深度兴趣网络, DIN(DeepInterestNetwork)。

2018年的深度兴趣演化网络, DIEN(DeepInterestEvolutionNetWork)。

2019年的深度会话兴趣网络, DSIN(DeepSessionInterestNetWork)。

这3篇文章的主要思想和相互关系用一句话分别概括如下:

第1篇DIN说,用户的行为日志中只有一部分和当前候选广告有关。可以利用Attention机制从用户行为日志中建模出和当前候选广告相关的用户兴趣表示。我们试过涨点了嘻嘻嘻。

第2篇DIEN说,用户最近的行为可能比较远的行为更加重要。可以用循环神经网络GRU建模用户兴趣随时间的演化。我们试过也涨点了嘿嘿嘿。

第3篇DSIN说,用户在同一次会话中的行为高度相关,在不同会话间的行为则相对独立。可以把用户行为日志按照时间间隔分割成会话并用SelfAttention机制建模它们之间的相互作用。我们试过又涨点了哈哈哈。

参考材料:

  • DIN论文:https://arxiv.org/pdf/1706.06978.pdf

  • 推荐系统中的注意力机制:https://zhuanlan.zhihu.com/p/51623339

  • 阿里经典兴趣网络:https://zhuanlan.zhihu.com/p/429433768

  • 从DIN到DIEN看阿里CTR算法的进化脉络:https://zhuanlan.zhihu.com/p/78365283

  • DIN+DIEN,机器学习唯一指定涨点技Attention:https://zhuanlan.zhihu.com/p/431131396

  • Attention机制简单总结:https://zhuanlan.zhihu.com/p/46313756

  • 代码实现参考:https://github.com/GitHub-HongweiZhang/prediction-flow

本篇文章我们主要介绍DIN,下一篇文章我们介绍DIEN。

公众号后台回复关键词:DIN,获取本文源代码和数据集下载链接。

〇,Attention原理概述

众所周知,Attention机制在深度学习领域是非常通用的涨分技巧。其主要作用是提升模型的自适应能力。

Attention机制的一些常用功能和典型范例总结如下:

  • 1,动态特征选择,根据样本不同动态地赋予特征以不同的权重,典型范例如SENet中的SEAttention,DIN中的Attention.

  • 2,动态特征交互,动态地构建特征之间的交互强弱关系,提取高阶特征。典型范例如Transformer中的的Attention。

  • 3,动态模块集成,类似多模型融合集成,但是不同子模块的权重是动态的。典型范例如MOE中的门控注意力机制。

在许多Attention机制的应用场景中,输入分成Query(Q)和Key(K)。Query是当前关注项的Embedding向量,Key是待和当前关注项进行匹配的Embedding向量。

例如在广告CTR领域,Query就是当前待预估的广告,Key就是用户历史上点击过的广告,通过Attention机制建立当前待预估的广告和用户历史上点击过的广告的相关性强弱。

又比如在NLP翻译领域,Query就是当前正在解码的译文单词词向量,Key就是原文单词序列的词向量,通过Attention机制可以建立译文单词和原文单词的对应关系。

Attention机制的核心实现是计算注意力权重,一些的常用实现形式如下:

1,多层感知机方法

先将Query和Key进行拼接,然后接一个多层感知机。

这种方法不需要Query和Key的向量长度相等,Query和Key之间的交互方式是通过学习获得的。

2,Bilinear方法

通过一个权重矩阵直接建立Query和Key的关系映射,计算速度较快,但是需要Query和Key的向量长度相同。

3,Scaled-Dot Product

这种方式直接求Query和Key的内积相似度,没有需要学习的参数,计算速度极快,需要Query和Key的向量长度相同。考虑到随着向量维度的增加,最后得到的权重也会增加,对其进行scaling。

402 Payment Required

一,DIN原理解析

阿里的展示广告系统主要用到了如下4类特征

  • (1) 用户画像特征。

  • (2) 用户行为特征,即用户点击过的商品。

  • (3) 待曝光的广告特征,广告其实也是商品。

  • (4) 上下文特征。

c7af9139547d765ab442703e24a0a426.jpeg

DIN、DIEN和DSIN主要聚焦在对用户行为日志的建模。

用户行为日志反应的是用户的兴趣,如何从行为日志中建模出一个好的用户兴趣的表示?

最基础的建模方法是 Embedding+SumPooling. 把用户的过去所有点击行为做Embedding, 然后求和。

e20e64c3e5d89cf915bd183c2932d921.jpeg

这个SumPooling的实现不要太简单。

import torch
import torch.nn as nn

class SumPooling(nn.Module):
    def __init__(self, dim):
        super(SumPooling, self).__init__()
        self.dim = dim

    def forward(self, x):
        return torch.sum(x, self.dim)

这种建模方式存在着一个巨大的缺陷,那就是用户的兴趣表示是确定的,和候选广告无关。

不管来个啥候选广告,用户过去的所有行为日志全部一把梭哈丢进去求和。

很显然,如果我们如果建模出和候选广告相关的用户兴趣表示,效果应该会好很多。

那么,如何做到这一点呢?我们可以用候选广告来和用户历史行为日志求相关性,用相关性对历史行为日志做加权。

这是很自然的,我们主要聚焦(Attention)用户历史行为日志中那些和候选广告相关的部分。

于是,duang的一下,DIN模型的模型架构就出来了。

b9bf351c6eb0426d3b4dfe8640d2f646.jpeg

这里注意力机制比较值得玩味,它是一种mlp形式的注意力结构,但在输入端不是简单地拼接了和,而是将都一起打包拼接了,这样模型更加容易学习Q和K之间的相似性关系。

此外,这里用mask技巧将keys中填充的的部分的注意力赋值为0,以及维度变换等一些实现上的细节,也是很值得揣摩的。

import torch
from torch import nn 


class MLP(nn.Module):
    def __init__(self, input_size, hidden_layers,
                 dropout=0.0, batchnorm=True):
        super(MLP, self).__init__()
        modules = OrderedDict()
        previous_size = input_size
        for index, hidden_layer in enumerate(hidden_layers):
            modules[f"dense{index}"] = nn.Linear(previous_size, hidden_layer)
            if batchnorm:
                modules[f"batchnorm{index}"] = nn.BatchNorm1d(hidden_layer)
            modules[f"activation{index}"] = nn.PReLU() 
            if dropout:
                modules[f"dropout{index}"] = nn.Dropout(dropout)
            previous_size = hidden_layer
        self.mlp = nn.Sequential(modules)

    def forward(self, x):
        return self.mlp(x)
    
    
class Attention(nn.Module):
    def __init__(
            self,
            input_size,
            hidden_layers,
            dropout=0.0,
            batchnorm=True,
            return_scores=False):
        
        super().__init__()
        self.return_scores = return_scores
        
        self.mlp = MLP(
            input_size=input_size * 4,
            hidden_layers=hidden_layers,
            dropout=dropout,
            batchnorm=batchnorm,
            activation=activation)
        self.fc = nn.Linear(hidden_layers[-1], 1)

    def forward(self, query, keys, keys_length):
        """
        Parameters
        ----------
        query: 2D tensor, [Batch, Hidden]
        keys: 3D tensor, [Batch, Time, Hidden]
        keys_length: 1D tensor, [Batch]

        Returns
        -------
        outputs: 2D tensor, [Batch, Hidden]
        """
        
        batch_size, max_length, dim = keys.size()

        query = query.unsqueeze(1).expand(-1, max_length, -1)

        din_all = torch.cat(
            [query, keys, query - keys, query * keys], dim=-1)

        din_all = din_all.view(batch_size * max_length, -1)

        outputs = self.mlp(din_all)

        outputs = self.fc(outputs).view(batch_size, max_length)  # [B, T]

        # Scale
        outputs = outputs / (dim ** 0.5)

        # Mask
        mask = (torch.arange(max_length, device=keys_length.device).repeat(
            batch_size, 1) < keys_length.view(-1, 1))
        outputs[~mask] = -np.inf

        # Activation
        outputs = torch.sigmoid(outputs)  # [B, T]

        if not self.return_scores:
            # Weighted sum
            outputs = torch.matmul(
                outputs.unsqueeze(1), keys).squeeze()  # [B, H]
            
        return outputs

我们期待的效果是这样的,和候选广告(query)越相关的用户历史浏览记录(keys),其注意力权重值越高😋。

7ec38aa0996442629683b2581cdf01a9.jpeg

除了用Attention机制从用户行为日志中建模出和当前候选广告相关的用户兴趣表示这个主要创新外,DIN这篇文章还有一些其他的微创新。

  • 引入转折点可以学习的Dice激活函数代替PReLU激活函数

  • 介绍一种Mini-batch Aware 的L2正则化方法

二,DIN的pytorch实现

下面是一个DIN模型的完整pytorch实现。

这里的AttentionGroup类比较特别,是为了建立候选广告属性和历史广告属性的pair关系。

import torch
import torch.nn as nn
import torch.nn.functional as F 
from collections import OrderedDict

class MaxPooling(nn.Module):
    def __init__(self, dim):
        super(MaxPooling, self).__init__()
        self.dim = dim

    def forward(self, x):
        return torch.max(x, self.dim)[0]


class SumPooling(nn.Module):
    def __init__(self, dim):
        super(SumPooling, self).__init__()
        self.dim = dim

    def forward(self, x):
        return torch.sum(x, self.dim)

class Dice(nn.Module):
    """
    The Data Adaptive Activation Function in DIN, a generalization of PReLu.
    """
    def __init__(self, emb_size, dim=2, epsilon=1e-8):
        super(Dice, self).__init__()
        assert dim == 2 or dim == 3

        self.bn = nn.BatchNorm1d(emb_size, eps=epsilon)
        self.sigmoid = nn.Sigmoid()
        self.dim = dim
        
        # wrap alpha in nn.Parameter to make it trainable
        self.alpha = nn.Parameter(torch.zeros((emb_size,))) if self.dim == 2 else nn.Parameter(
            torch.zeros((emb_size, 1)))


    def forward(self, x):
        assert x.dim() == self.dim
        if self.dim == 2:
            x_p = self.sigmoid(self.bn(x))
            out = self.alpha * (1 - x_p) * x + x_p * x
        else:
            x = torch.transpose(x, 1, 2)
            x_p = self.sigmoid(self.bn(x))
            out = self.alpha * (1 - x_p) * x + x_p * x
            out = torch.transpose(out, 1, 2)
        return out


class Identity(nn.Module):
    def __init__(self):
        super().__init__()
    def forward(self, x):
        return x
    
def get_activation_layer(name, hidden_size=None, dice_dim=2):
    name = name.lower()
    name_dict = {x.lower():x for x in dir(nn) if '__' not in x and 'Z'>=x[0]>='A'}
    if name=="linear":
        return Identity()
    elif name=="dice":
        assert dice_dim
        return Dice(hidden_size, dice_dim)
    else:
        assert name in name_dict, f'activation type {name} not supported!'
        return getattr(nn,name_dict[name])()
    
def init_weights(model):
    if isinstance(model, nn.Linear):
        if model.weight is not None:
            nn.init.kaiming_uniform_(model.weight.data)
        if model.bias is not None:
            nn.init.normal_(model.bias.data)
    elif isinstance(model, (nn.BatchNorm1d,nn.BatchNorm2d,nn.BatchNorm3d)):
        if model.weight is not None:
            nn.init.normal_(model.weight.data, mean=1, std=0.02)
        if model.bias is not None:
            nn.init.constant_(model.bias.data, 0)
    else:
        pass


class MLP(nn.Module):
    def __init__(self, input_size, hidden_layers,
                 dropout=0.0, batchnorm=True, activation='relu'):
        super(MLP, self).__init__()
        modules = OrderedDict()
        previous_size = input_size
        for index, hidden_layer in enumerate(hidden_layers):
            modules[f"dense{index}"] = nn.Linear(previous_size, hidden_layer)
            if batchnorm:
                modules[f"batchnorm{index}"] = nn.BatchNorm1d(hidden_layer)
            if activation:
                modules[f"activation{index}"] = get_activation_layer(activation,hidden_layer,2)
            if dropout:
                modules[f"dropout{index}"] = nn.Dropout(dropout)
            previous_size = hidden_layer
        self.mlp = nn.Sequential(modules)

    def forward(self, x):
        return self.mlp(x)
import numpy as np
class Attention(nn.Module):
    def __init__(
            self,
            input_size,
            hidden_layers,
            dropout=0.0,
            batchnorm=True,
            activation='prelu',
            return_scores=False):
        
        super().__init__()
        self.return_scores = return_scores
        
        self.mlp = MLP(
            input_size=input_size * 4,
            hidden_layers=hidden_layers,
            dropout=dropout,
            batchnorm=batchnorm,
            activation=activation)
        self.fc = nn.Linear(hidden_layers[-1], 1)

    def forward(self, query, keys, keys_length):
        """
        Parameters
        ----------
        query: 2D tensor, [Batch, Hidden]
        keys: 3D tensor, [Batch, Time, Hidden]
        keys_length: 1D tensor, [Batch]

        Returns
        -------
        outputs: 2D tensor, [Batch, Hidden]
        """
        
        batch_size, max_length, dim = keys.size()

        query = query.unsqueeze(1).expand(-1, max_length, -1)

        din_all = torch.cat(
            [query, keys, query - keys, query * keys], dim=-1)

        din_all = din_all.view(batch_size * max_length, -1)

        outputs = self.mlp(din_all)

        outputs = self.fc(outputs).view(batch_size, max_length)  # [B, T]

        # Scale
        outputs = outputs / (dim ** 0.5)

        # Mask
        mask = (torch.arange(max_length, device=keys_length.device).repeat(
            batch_size, 1) < keys_length.view(-1, 1))
        outputs[~mask] = -np.inf

        # Activation
        outputs = torch.sigmoid(outputs)  # [B, T]

        if not self.return_scores:
            # Weighted sum
            outputs = torch.matmul(
                outputs.unsqueeze(1), keys).squeeze()  # [B, H]
            
        return outputs
class AttentionGroup(object):
    def __init__(self, name, pairs,
                 hidden_layers, activation='dice', att_dropout=0.0):
        self.name = name
        self.pairs = pairs
        self.hidden_layers = hidden_layers
        self.activation = activation
        self.att_dropout = att_dropout

        self.related_feature_names = set()
        for pair in pairs:
            self.related_feature_names.add(pair['ad'])
            self.related_feature_names.add(pair['pos_hist'])

    def is_attention_feature(self, feature_name):
        if feature_name in self.related_feature_names:
            return True
        return False

    @property
    def pairs_count(self):
        return len(self.pairs)
class DIN(nn.Module):
    def __init__(self, num_features,cat_features,seq_features, 
                 cat_nums,embedding_size, attention_groups,
                 mlp_hidden_layers, mlp_activation='prelu', mlp_dropout=0.0,
                 d_out = 1
                 ):
        super().__init__()
        self.num_features = num_features
        self.cat_features = cat_features
        self.seq_features = seq_features
        self.cat_nums = cat_nums 
        self.embedding_size = embedding_size
        
        self.attention_groups = attention_groups
        
        self.mlp_hidden_layers = mlp_hidden_layers
        self.mlp_activation = mlp_activation
        self.mlp_dropout = mlp_dropout
        
        self.d_out = d_out
        
        #embedding
        self.embeddings = OrderedDict()
        for feature in self.cat_features+self.seq_features:
            self.embeddings[feature] = nn.Embedding(
                self.cat_nums[feature], self.embedding_size, padding_idx=0)
            self.add_module(f"embedding:{feature}",self.embeddings[feature])

        self.sequence_poolings = OrderedDict()
        self.attention_poolings = OrderedDict()
        total_embedding_sizes = 0
        for feature in self.cat_features:
            total_embedding_sizes += self.embedding_size
        for feature in self.seq_features:
            total_embedding_sizes += self.embedding_size
        
        #sequence_pooling
        for feature in self.seq_features:
            if not self.is_attention_feature(feature):
                self.sequence_poolings[feature] = MaxPooling(1)
                self.add_module(f"pooling:{feature}",self.sequence_poolings[feature])

        #attention_pooling
        for attention_group in self.attention_groups:
            self.attention_poolings[attention_group.name] = (
                self.create_attention_fn(attention_group))
            self.add_module(f"attention_pooling:{attention_group.name}",
                self.attention_poolings[attention_group.name])

        total_input_size = total_embedding_sizes+len(self.num_features)
        
        self.mlp = MLP(
            total_input_size,
            mlp_hidden_layers,
            dropout=mlp_dropout, batchnorm=True, activation=mlp_activation)
        
        self.final_layer = nn.Linear(mlp_hidden_layers[-1], self.d_out)
        self.apply(init_weights)

    def forward(self, x):
        
        final_layer_inputs = list()

        number_inputs = list()
        for feature in self.num_features:
            number_inputs.append(x[feature].view(-1, 1))

        embeddings = OrderedDict()
        for feature in self.cat_features:
            embeddings[feature] = self.embeddings[feature](x[feature])

        for feature in self.seq_features:
            if not self.is_attention_feature(feature):
                embeddings[feature] = self.sequence_poolings[feature](
                    self.embeddings[feature](x[feature]))
                
        for attention_group in self.attention_groups:
            query = torch.cat(
                [embeddings[pair['ad']]
                 for pair in attention_group.pairs],
                dim=-1)
            keys = torch.cat(
                [self.embeddings[pair['pos_hist']](
                    x[pair['pos_hist']]) for pair in attention_group.pairs],
                dim=-1)
            #hist_length = torch.sum(hist>0,axis=1)
            keys_length = torch.min(torch.cat(
                [torch.sum(x[pair['pos_hist']]>0,axis=1).view(-1, 1)
                 for pair in attention_group.pairs],
                dim=-1), dim=-1)[0]
            
            embeddings[attention_group.name] = self.attention_poolings[
                attention_group.name](query, keys, keys_length)

        emb_concat = torch.cat(number_inputs + [
            emb for emb in embeddings.values()], dim=-1)

        final_layer_inputs = self.mlp(emb_concat)
        output = self.final_layer(final_layer_inputs)
        if  self.d_out==1:
            output = output.squeeze() 

        return output

    def create_attention_fn(self, attention_group):
        return Attention(
            attention_group.pairs_count * self.embedding_size,
            hidden_layers=attention_group.hidden_layers,
            dropout=attention_group.att_dropout,
            activation=attention_group.activation)
    
    def is_attention_feature(self, feature):
        for group in self.attention_groups:
            if group.is_attention_feature(feature):
                return True
        return False

三,Movielens数据集完整范例

下面是一个基于Movielens评价数据集的完整范例,根据用户过去对一些电影的评价结果,来预测用户对候选电影是否会给好评。

这个数据集不大,用CPU就能跑。😁

1,准备数据

import numpy as np 
import pandas as pd 
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.preprocessing import QuantileTransformer
from sklearn.pipeline import Pipeline, FeatureUnion 
from sklearn.impute import SimpleImputer 
from collections import Counter

#类别特征预处理
class CategoryEncoder(BaseEstimator, TransformerMixin):
    
    def __init__(self, min_cnt=5, word2idx=None, idx2word=None):
        super().__init__() 
        self.min_cnt = min_cnt
        self.word2idx = word2idx if word2idx else dict()
        self.idx2word = idx2word if idx2word else dict()

    def fit(self, x, y=None):
        if not self.word2idx:
            counter = Counter(np.asarray(x).ravel())

            selected_terms = sorted(
                list(filter(lambda x: counter[x] >= self.min_cnt, counter)))

            self.word2idx = dict(
                zip(selected_terms, range(1, len(selected_terms) + 1)))
            self.word2idx['__PAD__'] = 0
            if '__UNKNOWN__' not in self.word2idx:
                self.word2idx['__UNKNOWN__'] = len(self.word2idx)

        if not self.idx2word:
            self.idx2word = {
                index: word for word, index in self.word2idx.items()}

        return self

    def transform(self, x):
        transformed_x = list()
        for term in np.asarray(x).ravel():
            try:
                transformed_x.append(self.word2idx[term])
            except KeyError:
                transformed_x.append(self.word2idx['__UNKNOWN__'])

        return np.asarray(transformed_x, dtype=np.int64)

    def dimension(self):
        return len(self.word2idx)

#序列特征预处理(类别序列) 
class SequenceEncoder(BaseEstimator, TransformerMixin):
    def __init__(self, sep=' ', min_cnt=5, max_len=None,
                 word2idx=None, idx2word=None):
        super().__init__() 
        self.sep = sep
        self.min_cnt = min_cnt
        self.max_len = max_len

        self.word2idx = word2idx if word2idx else dict()
        self.idx2word = idx2word if idx2word else dict()

    def fit(self, x, y=None):
        if not self.word2idx:
            counter = Counter()

            max_len = 0
            for sequence in np.array(x).ravel():
                words = sequence.split(self.sep)
                counter.update(words)
                max_len = max(max_len, len(words))

            if self.max_len is None:
                self.max_len = max_len

            # drop rare words
            words = sorted(
                list(filter(lambda x: counter[x] >= self.min_cnt, counter)))

            self.word2idx = dict(zip(words, range(1, len(words) + 1)))
            self.word2idx['__PAD__'] = 0
            if '__UNKNOWN__' not in self.word2idx:
                self.word2idx['__UNKNOWN__'] = len(self.word2idx)

        if not self.idx2word:
            self.idx2word = {
                index: word for word, index in self.word2idx.items()}

        if not self.max_len:
            max_len = 0
            for sequence in np.array(x).ravel():
                words = sequence.split(self.sep)
                max_len = max(max_len, len(words))
            self.max_len = max_len

        return self

    def transform(self, x):
        transformed_x = list()

        for sequence in np.asarray(x).ravel():
            words = list()
            for word in sequence.split(self.sep):
                try:
                    words.append(self.word2idx[word])
                except KeyError:
                    words.append(self.word2idx['__UNKNOWN__'])

            transformed_x.append(
                np.asarray(words[0:self.max_len], dtype=np.int64))

        return np.asarray(transformed_x, dtype=object)
    
    def dimension(self):
        return len(self.word2idx)

    def max_length(self):
        return self.max_len
from sklearn.preprocessing import QuantileTransformer
from sklearn.pipeline import Pipeline 
from sklearn.impute import SimpleImputer 
from tqdm import tqdm 

dftrain = pd.read_csv("../data/ml_1m/train.csv")
dfval = pd.read_csv("../data/ml_1m/test.csv")

for col in ["movieId","histHighRatedMovieIds","negHistMovieIds","genres"]:
    dftrain[col] = dftrain[col].astype(str)
    dfval[col] = dfval[col].astype(str)

num_features = ['age']
cat_features = ['gender', 'movieId', 'occupation', 'zipCode']
seq_features = ['genres', 'histHighRatedMovieIds', 'negHistMovieIds']

num_pipe = Pipeline(steps = [('impute',SimpleImputer()),('quantile',QuantileTransformer())])

encoders = {}

print("preprocess number features...")
dftrain[num_features] = num_pipe.fit_transform(dftrain[num_features]).astype(np.float32)
dfval[num_features] = num_pipe.transform(dfval[num_features]).astype(np.float32)

print("preprocess category features...")
for col in tqdm(cat_features):
    encoders[col] = CategoryEncoder(min_cnt=5)
    dftrain[col]  = encoders[col].fit_transform(dftrain[col])
    dfval[col] =  encoders[col].transform(dfval[col])
    
print("preprocess sequence features...")
for col in tqdm(seq_features):
    encoders[col] = SequenceEncoder(sep="|",min_cnt=5)
    dftrain[col]  = encoders[col].fit_transform(dftrain[col])
    dfval[col] =  encoders[col].transform(dfval[col])
    
from collections import OrderedDict
from itertools import chain
from torch.utils.data import Dataset,DataLoader 

class Df2Dataset(Dataset):
    def __init__(self, dfdata, num_features, cat_features,
                 seq_features, encoders, label_col="label"):
        self.dfdata = dfdata
        self.num_features = num_features
        self.cat_features = cat_features 
        self.seq_features = seq_features
        self.encoders = encoders
        self.label_col = label_col
        self.size = len(self.dfdata)

    def __len__(self):
        return self.size

    @staticmethod
    def pad_sequence(sequence,max_length):
        #zero is special index for padding
        padded_seq = np.zeros(max_length, np.int32)
        padded_seq[0: sequence.shape[0]] = sequence
        return padded_seq

    def __getitem__(self, idx):
        record = OrderedDict()
        for col in self.num_features:
            record[col] = self.dfdata[col].iloc[idx].astype(np.float32)
            
        for col in self.cat_features:
            record[col] = self.dfdata[col].iloc[idx].astype(np.int64)
            
        for col in self.seq_features:
            seq = self.dfdata[col].iloc[idx]
            max_length = self.encoders[col].max_length()
            record[col] = Df2Dataset.pad_sequence(seq,max_length)

        if self.label_col is not None:
            record['label'] = self.dfdata[self.label_col].iloc[idx].astype(np.float32)
        return record

    def get_num_batches(self, batch_size):
        return np.ceil(self.size / batch_size)
    
ds_train = Df2Dataset(dftrain, num_features, cat_features, seq_features, encoders)
ds_val = Df2Dataset(dfval,num_features, cat_features, seq_features, encoders)
dl_train = DataLoader(ds_train, batch_size=128,shuffle=True)
dl_val = DataLoader(ds_val,batch_size=128,shuffle=False)

cat_nums = {k:v.dimension() for k,v in encoders.items()}
for batch in dl_train:
    break

2,定义模型

def create_net():
    din_attention_groups = [
        AttentionGroup(
            name='group1',
            pairs=[{'ad': 'movieId', 'pos_hist': 'histHighRatedMovieIds'}],
            activation='dice',
            hidden_layers=[16, 8], att_dropout=0.1)
    ]

    net = DIN(num_features=num_features,
               cat_features=cat_features,
               seq_features=seq_features,
               cat_nums = cat_nums,
               embedding_size=16,
               attention_groups=din_attention_groups,
               mlp_hidden_layers=[32,16],
               mlp_activation="prelu",
               mlp_dropout=0.25,
               d_out=1
               )
    return net 

net = create_net() 

out = net.forward(batch)
from torchkeras.summary import summary 
summary(net,input_data=batch);

3,训练模型

import os,sys,time
import numpy as np
import pandas as pd
import datetime 
from tqdm import tqdm 

import torch
from torch import nn 
from accelerate import Accelerator
from copy import deepcopy


def printlog(info):
    nowtime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print("\n"+"=========="*8 + "%s"%nowtime)
    print(str(info)+"\n")
    
class StepRunner:
    def __init__(self, net, loss_fn,stage = "train", metrics_dict = None, 
                 optimizer = None, lr_scheduler = None,
                 accelerator = None
                 ):
        self.net,self.loss_fn,self.metrics_dict,self.stage = net,loss_fn,metrics_dict,stage
        self.optimizer,self.lr_scheduler = optimizer,lr_scheduler
        self.accelerator = accelerator
    
    def __call__(self, batch):
        #loss
        preds = self.net(batch)
        loss = self.loss_fn(preds,batch["label"])

        #backward()
        if self.optimizer is not None and self.stage=="train":
            if self.accelerator is  None:
                loss.backward()
            else:
                self.accelerator.backward(loss)
            self.optimizer.step()
            if self.lr_scheduler is not None:
                self.lr_scheduler.step()
            self.optimizer.zero_grad()
            
        #metrics
        step_metrics = {self.stage+"_"+name:metric_fn(preds, batch["label"]).item() 
                        for name,metric_fn in self.metrics_dict.items()}
        return loss.item(),step_metrics
    
    
class EpochRunner:
    def __init__(self,steprunner):
        self.steprunner = steprunner
        self.stage = steprunner.stage
        self.steprunner.net.train() if self.stage=="train" else self.steprunner.net.eval()
        
    def __call__(self,dataloader):
        total_loss,step = 0,0
        loop = tqdm(enumerate(dataloader), total =len(dataloader))
        for i, batch in loop:
            if self.stage=="train":
                loss, step_metrics = self.steprunner(batch)
            else:
                with torch.no_grad():
                    loss, step_metrics = self.steprunner(batch)

            step_log = dict({self.stage+"_loss":loss},**step_metrics)

            total_loss += loss
            step+=1
            if i!=len(dataloader)-1:
                loop.set_postfix(**step_log)
            else:
                epoch_loss = total_loss/step
                epoch_metrics = {self.stage+"_"+name:metric_fn.compute().item() 
                                 for name,metric_fn in self.steprunner.metrics_dict.items()}
                epoch_log = dict({self.stage+"_loss":epoch_loss},**epoch_metrics)
                loop.set_postfix(**epoch_log)

                for name,metric_fn in self.steprunner.metrics_dict.items():
                    metric_fn.reset()
        return epoch_log

class KerasModel(torch.nn.Module):
    def __init__(self,net,loss_fn,metrics_dict=None,optimizer=None,lr_scheduler = None):
        super().__init__()
        self.accelerator = Accelerator()
        self.history = {}
        
        self.net = net
        self.loss_fn = loss_fn
        self.metrics_dict = nn.ModuleDict(metrics_dict) 
        
        self.optimizer = optimizer if optimizer is not None else torch.optim.Adam(
            self.parameters(), lr=1e-2)
        self.lr_scheduler = lr_scheduler
        
        self.net,self.loss_fn,self.metrics_dict,self.optimizer = self.accelerator.prepare(
            self.net,self.loss_fn,self.metrics_dict,self.optimizer)

    def forward(self, x):
        if self.net:
            return self.net.forward(x)
        else:
            raise NotImplementedError


    def fit(self, train_data, val_data=None, epochs=10, ckpt_path='checkpoint.pt', 
            patience=5, monitor="val_loss", mode="min"):
        
        train_data = self.accelerator.prepare(train_data)
        val_data = self.accelerator.prepare(val_data) if val_data else []

        for epoch in range(1, epochs+1):
            printlog("Epoch {0} / {1}".format(epoch, epochs))
            
            # 1,train -------------------------------------------------  
            train_step_runner = StepRunner(net = self.net,stage="train",
                    loss_fn = self.loss_fn,metrics_dict=deepcopy(self.metrics_dict),
                    optimizer = self.optimizer, lr_scheduler = self.lr_scheduler,
                    accelerator = self.accelerator)
            train_epoch_runner = EpochRunner(train_step_runner)
            train_metrics = train_epoch_runner(train_data)
            
            for name, metric in train_metrics.items():
                self.history[name] = self.history.get(name, []) + [metric]

            # 2,validate -------------------------------------------------
            if val_data:
                val_step_runner = StepRunner(net = self.net,stage="val",
                    loss_fn = self.loss_fn,metrics_dict=deepcopy(self.metrics_dict),
                    accelerator = self.accelerator)
                val_epoch_runner = EpochRunner(val_step_runner)
                with torch.no_grad():
                    val_metrics = val_epoch_runner(val_data)
                val_metrics["epoch"] = epoch
                for name, metric in val_metrics.items():
                    self.history[name] = self.history.get(name, []) + [metric]
            
            # 3,early-stopping -------------------------------------------------
            arr_scores = self.history[monitor]
            best_score_idx = np.argmax(arr_scores) if mode=="max" else np.argmin(arr_scores)
            if best_score_idx==len(arr_scores)-1:
                torch.save(self.net.state_dict(),ckpt_path)
                print("<<<<<< reach best {0} : {1} >>>>>>".format(monitor,
                     arr_scores[best_score_idx]),file=sys.stderr)
            if len(arr_scores)-best_score_idx>patience:
                print("<<<<<< {} without improvement in {} epoch, early stopping >>>>>>".format(
                    monitor,patience),file=sys.stderr)
                self.net.load_state_dict(torch.load(ckpt_path))
                break 
            
        return pd.DataFrame(self.history)

    @torch.no_grad()
    def evaluate(self, val_data):
        val_data = self.accelerator.prepare(val_data)
        val_step_runner = StepRunner(net = self.net,stage="val",
                    loss_fn = self.loss_fn,metrics_dict=deepcopy(self.metrics_dict),
                    accelerator = self.accelerator)
        val_epoch_runner = EpochRunner(val_step_runner)
        val_metrics = val_epoch_runner(val_data)
        return val_metrics
        
       
    @torch.no_grad()
    def predict(self, dataloader):
        dataloader = self.accelerator.prepare(dataloader)
        self.net.eval()
        result = torch.cat([self.forward(t) for t in dataloader])
        return result.data
from torchkeras.metrics import AUC

loss_fn = nn.BCEWithLogitsLoss()

metrics_dict = {"auc":AUC()}

optimizer = torch.optim.Adam(net.parameters(), lr=0.002, weight_decay=0.001) 


model = KerasModel(net,
                   loss_fn = loss_fn,
                   metrics_dict= metrics_dict,
                   optimizer = optimizer,
                  )
dfhistory = model.fit(train_data=dl_train,val_data=dl_val,epochs=100, patience=5,
                      monitor = "val_auc",mode="max",ckpt_path='checkpoint.pt')
45d73b85a14abbc6c912ccca7377ad27.jpeg

4,评估模型

%matplotlib inline
%config InlineBackend.figure_format = 'svg'

import matplotlib.pyplot as plt

def plot_metric(dfhistory, metric):
    train_metrics = dfhistory["train_"+metric]
    val_metrics = dfhistory['val_'+metric]
    epochs = range(1, len(train_metrics) + 1)
    plt.plot(epochs, train_metrics, 'bo--')
    plt.plot(epochs, val_metrics, 'ro-')
    plt.title('Training and validation '+ metric)
    plt.xlabel("Epochs")
    plt.ylabel(metric)
    plt.legend(["train_"+metric, 'val_'+metric])
    plt.show()
plot_metric(dfhistory,"loss")
77a8346e20d9fbe96f140b8b423ebfef.jpeg
plot_metric(dfhistory,"auc")
0029ea9288128ebacafa5c750af3a51c.jpeg
model.evaluate(dl_val)

{'val_loss': 0.6842133283615113, 'val_auc': 0.6392135620117188}

4,使用模型

labels = torch.tensor([x["label"] for x in ds_val])
preds = model.predict(dl_val)
val_auc = roc_auc_score(labels.cpu().numpy(),preds.cpu().numpy())
print(val_auc)

0.6392135469811272

5, 保存模型

torch.save(model.net.state_dict(),"best_din.pt")
net_clone = create_net()
net_clone.load_state_dict(torch.load("best_din.pt"))
net_clone.eval()
labels = torch.tensor([x["label"] for x in ds_val])
preds = torch.cat([net_clone(x).data for x in dl_val]) 
val_auc = roc_auc_score(labels.cpu().numpy(),preds.cpu().numpy())
print(val_auc)

0.6392135469811272

公众号后台回复关键词:DIN,获取本文源代码和数据集链接。

以上。万水千山总是情,点个在看行不行?

36f5f43ea587f84cc989f423fbb9e562.png

别再死记硬背模型结构了!从Deep Crossing到DIEN,手把手拆解推荐系统经典模型的演进逻辑 本文深入解析了推荐系统从Deep Crossing到DIEN的经典模型演进逻辑,揭示了深度学习在推荐算法中的关键突破。通过分析特征工程自动化、记忆与泛化统一、注意力机制等核心技术,帮助开发者理解推荐系统的设计哲学与实战应用,提升模型选型与优化能力。 阅读详情

相关推荐

推荐系统核心逻辑结构解析:从召回、排序到深度学习模型实战

推荐系统作为信息过滤与分发的核心技术,其核心逻辑在于通过算法模型连接用户与内容。其基本原理通常遵循“召回-排序-重排”的三层漏斗架构,旨在从海量候选中高效筛选并精准呈现个性化结果。这一技术架构的价值在于平衡了覆盖率、精准度与多样性,是提升用户体验与平台商业价值的关键引擎,广泛应用于电商、内容资讯、视频流媒体等场景。在深度学习时代,以DeepFM为代表的模型通过融合线性模型的记忆能力与深度网络的泛化能力,有效捕捉特征间的高阶交互;而DIN模型则引入注意力机制,动态激活与当前候选相关的用户历史兴趣,实现了更精细

weixin_34248705的博客 406

机器学习——特征工程(实战)

onehot编码——类别字段 机器学习中,对于一些类别字段,计算机是不能直接进行识别的。例如某个数据集中,动物类别分为cat、dog、turtle、fish等,可以通过onehot编码将其转换成计算机所能识别的数字。 pet cat dog turtle fish cat cat dog turtle fish 1 0 0 0 0 1 0 0 0 0 1 0 0 0

qq_38029916的博客 1582

网络小说推荐系统实战:从特征工程到混合模型架构设计

个性化推荐系统是现代互联网应用的核心技术之一,其核心原理是通过分析用户历史行为与内容特征,构建模型来预测用户兴趣并进行精准匹配。这项技术的核心价值在于提升用户体验、增加内容分发效率与平台粘性。在电商、视频、资讯等广泛应用场景中,推荐系统已成为标配。本文聚焦于网络小说这一垂直领域,深入探讨了如何针对其用户行为深度、内容消耗周期长、序列依赖性强等特点,构建高效的推荐系统。文中重点阐述了以多维度动态用户特征和深度内容理解为燃料,融合协同过滤、语义向量、序列模型等多路召回,并结合GBDT与深度学习混合排序模型的完整

weixin_30587927的博客 342

DIN模型pytorch代码逐行细讲

​ din代码逐行解析 使用的模型:din​ 模型功能:预测用户是否想要购买目标商品y​ 模型特点:该模型一般用于分析用户历史行为与预测目标之前的关系​ 数据集:用户的id,亚马逊的用户购买商品的记录,以及要预测的目标商品(注:一般更好的数据集会带有用户的个人信息,也可以作为模型输入进行分析,但是作为一个模板,不搞这么复制)​ 模型输入:用户的历史购买商品类别序列​ 模型输出:用户想购买商品的概率​

qq_36760793的博客 6744

ESP32+MAX98357音频模块实战:5分钟搞定Micropython播放网络音乐(附避坑指南)

本文详细介绍了如何使用ESP32和MAX98357音频模块,通过Micropython快速搭建网络音乐播放系统。内容涵盖硬件连接、I2S驱动配置、网络音频流处理等核心步骤,并提供了完整的实战代码和常见问题避坑指南,帮助开发者高效实现物联网音频应用。

weixin_29193359的博客 206

泰迪杯实战案例学习资料:电商用户行为分析与个性化推荐系统设计

在电商场景中,用户行为数据(点击、加购、下单)的深度挖掘是提升转化率的核心。:训练需32GB内存 + GPU(如RTX 3090),推理可使用CPU集群。:为不同用户生成Top-N商品推荐列表,要求点击率(CTR)提升20%以上。:利用类目相似性进行流量扶持(“同类目老商品”的受众用户优先曝光)。:多路召回(协同过滤+热门商品+类目偏好)→ 生成1000候选集。:需兼顾平台GMV(成交总额)与用户体验(推荐多样性)。:长尾商品(80%商品点击量<10次)难以建模。

qq_63961628的博客 1595

【Pytorch】expand()用法==》扩展某个维度

有两点需要注意,无论是expand() 还是 expand_as(): 只能在第0维扩展一个维数,比如原来是是(1,3,4)==》(2,1,3,4),而在其他维度扩展不可以(1,3,4)==》(1,2,3,4)【错误】 如果不增加维数,只是增加维度,要增加的原维度必须是1才可以在该维度增加维度,其他值均不可以 import torch #1 x = torch.randn(2, 1, 1)#为1可以扩展为3和4 x = x.expand(2, 3, 4) print('x :', x.size()..

Drug discovery 1万+

prediction-flow:由PyTorch实施的基于深度学习的点击率模型

预测流 预测流是一个Python软件包,提供了基于现代深度学习的CTR模型。 模型由PyTorch实现。 如何使用 使用pip安装。 pip install prediction-flow 特征 如何定义特征 所有要素类型都有两个参数,名称和column_flow。 name参数用于索引来自输入数据帧的列原始数据。 column_flow参数是变压器列表中的单个变压器。 变压器用于训练模型之前对列数据进行预处理。 密集数特征 Number('age', StandardScaler()) Number('ctr', None) 稀疏类别特征 Category('movieId', CategoryEncoder(min_cnt=1)) 可变长度序列特征 Sequence('genres', SequenceEncoder(sep='|', min_cnt=1)) 变压器 现在提供以下变

Prediction-Flow 开源项目教程

Prediction-Flow 开源项目教程 1. 项目介绍 Prediction-Flow 是一个基于 PyTorch 实现的深度学习 CTR(点击率预测)模型库。该项目提供了多种现代深度学习模型,如 Wide & Deep、DeepFM、DIN 等,旨在帮助开发者快速构建和训练 CTR 模型。Prediction-Flow 支持多种特征类型,包括密集数值特征、稀疏类别特征和变长序列特征...

gitblog_01128的博客 1081

可乐学习NVMe之一:为什么SSD需要NVMe

1. NVMe SSD 结构简图 整体来看,NVMe SSD可以分为三部分,host端的驱动NVMe Driver、PCIe+NVMe实现的控制器以及NAND Flash+DDR4/DDR5存储介质。 芯片回片后Host侧软件或者驱动开发主要在应用层,通过一些测试工具进行benchmark跑分测试,performance性能分析,latency分析、stress-testing等等: 那么,接下来阐述为什么SSD需要NVMe,并再后续的文章介绍NVMe协议分析,个人针对协议的理解,如果有不当之处,还请各

Lucky 3173

一文帮你彻底搞懂ARM Debug Interface之SWD(转)

ARM的文档写的已经很好了,但是关于上电以后的第一时间应该怎么操作,依然写的不够清晰,导致我第一次用的时候还是费了一些周折。今天做一个详细的梳理,希望能够帮助到更多的朋友。BTW, ARM_Cortex-M0/3 DesignStart系列会持续更新,只是中间可能会穿插其他知识点。SWD是Serial Wire Debug的简称,翻译成中文是”串行线调试”。

2302_79847220的博客 6863

【DNN】DIN原理-代码解读-应用过程中可能存在的难点

DIN(Deep Interest Network)很火,已经成功吸引了我的注意力,落地时,究竟难在什么地方?????本文的整体思路是:先熟悉原理,再看代码,然后在结合实际,猜想下可能难在什么地方。 原理 参考各位前辈的博客,结合论文,按照自己的理解习惯,汇总如下。论文下载点击这里。 DIN要解决的问题: 针对互联网电子商务领域,数据特点:Diversity、Local Activation。 DIN给出的解决方案: 使用用户兴趣分布来表示用户多种多样的兴趣爱好 使用Attention机制来实

凝眸伏笔的博客 2488

海上AI导航系统:技术架构与行业应用解析

计算机视觉与强化学习技术在海上导航系统中的应用正逐步改变传统航运业。通过多模态传感器融合技术,现代AI导航系统能够实现高精度的物体识别与实时路径规划,有效解决能见度依赖、决策延迟等核心痛点。以Orca AI的SeaPod系统为例,其结合了NVIDIA GeForce RTX 4070的边缘计算能力与多光谱协同识别技术,在恶劣环境下仍能保持稳定性能。这类系统不仅提升了航行安全性,还通过优化航线显著降低燃油消耗,在东京湾、马六甲海峡等繁忙水域展现出巨大价值。随着毫米波雷达融合方案的测试推进,未来海上导航将突破极

weixin_33597080的博客 356

3分钟搞懂深度学习AI:参数量与形状推导:算力预算

处理4K超清图还是小头像,AI模型大小竟然一样?本文用“手电筒照墙”的直观比喻,3分钟带你零基础搞懂深度学习核心:卷积层参数量。揭示AI“脑容量”只取决于探测器结构,而与输入图片尺寸完全无关的奥秘。

ldxldz的博客 751

CTFHUB------XSS反射型(WP)

文章目录前言XSS测试平台 前言 一开始我直接在上面写js脚本发现出错,最后发现只能通过下面的输入框来执行XSS测试,于是我们只能用XSS测试平台解决问题 XSS测试平台 注册一个XSS测试平台 这里随便填写 勾选默认模块就够了,点击下一步 这时会出现一个找个将代码复制 拼接到网站后缀name后面,点击send 这时平台会收到一个记录点开即可 得到flag ...

Y4tacker的博客 1万+

AD5700 HART芯片实战笔记:从时钟检测到数据收发,一个STM32工程师的踩坑实录

本文详细记录了STM32工程师在AD5700 HART芯片开发中的实战经验,涵盖时钟信号检测、RTS切换时序优化及HART数据解析等关键问题。通过硬件设计检查、定时器配置优化和大小端转换方案对比,解决了工业级芯片开发中的典型挑战,为嵌入式开发者提供宝贵参考。

weixin_42715608的博客 164

告别手绘线束图:5分钟掌握WireViz自动化设计终极指南

还在为复杂线束图的绘制而烦恼吗?传统的手工绘图不仅耗时耗力,还容易出错。今天,我将向您介绍一款革命性的线束图自动生成工具——WireViz。这款免费开源工具能够将您的线束设计效率提升10倍以上,让专业级电气图纸制作变得前所未有的简单。 ## 为什么你需要立即放弃传统线束图绘制方法 传统线束图绘制面临三大痛点:**设计周期长**、**错误率高**、**维护困难**。工程师们常常需要花费数小时甚至

gitblog_01187的博客 310

CIFAR-100模型性能对比:Top1与Top5错误率深度解析

本文深度解析了CIFAR-100数据集上Top1与Top5错误率的核心意义与差异。通过对比MobileNet、ResNet、DenseNet等主流模型的性能数据,揭示了模型架构、训练策略及数据集特性如何共同影响这两个关键指标,并为不同应用场景下的模型选择提供了实战指南。

weixin_28224217的博客 339

PaddleRec环境搭建与配置:10分钟完成推荐系统开发环境部署

想要快速掌握推荐系统开发,却苦于环境配置复杂?PaddleRec作为基于飞桨生态的推荐算法一站式工具,让您只需10分钟就能完成完整的推荐系统开发环境部署!🚀 ## 什么是PaddleRec推荐系统? PaddleRec是飞桨生态中的大规模推荐算法库,包含推荐系统经典及最新算法LR、Wide&Deep、DSSM、TDM、MIND、Word2Vec、Bert4Rec等70+模型,覆盖内容理解、匹

gitblog_00233的博客 903

什么是 SAP PCE?

SAP Private Cloud Edition 是一种基于云计算的部署模式,它结合了公有云的灵活性和私有云的安全性,为企业提供了量身定制的云服务。在 PCE 环境中,SAP 托管和维护客户的应用系统(如 SAP S/4HANA、SAP SuccessFactors 或 SAP BW/4HANA),同时客户拥有更高的定制化权限。专属环境:每个客户在云中拥有独立的资源,这保障了更高的安全性和性能。全面托管:SAP 提供基础架构、系统管理和更新服务,降低了客户的运维成本。灵活扩展。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 2252

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

如何用WireViz实现专业级线束图自动化设计:5分钟掌握电气工程高效工作流

WireViz是一款革命性的线束图自动生成工具,通过简单的YAML配置文件即可快速创建专业级的线束连接图。这个开源项目彻底改变了电气工程师、电子爱好者和项目管理者手动绘制线束图的传统方式,让线束设计从繁琐的手工绘图转变为高效的文本化配置。无论您需要设计简单的电缆连接还是复杂的多分支线束系统,WireViz都能在几分钟内生成包含完整连接关系、物料清单和详细标注的专业图纸。 ## 🔥 价值主张:为

gitblog_01138的博客 454

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

推荐系统的特征工程

推荐系统中的刀工

清风徐来 3333

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

推荐系统十年演进

过去十年的演进,是将推荐系统从**“为了点击率而不择手段的算法”重塑为“赋能全球数字化决策、具备内核级隐私保护与复杂逻辑理解能力的智慧引擎”**。你在纠结如何让模型分清“用户买过一次尿布,不代表他这辈子都要看尿布广告”。你在利用 eBPF 审计下的生成式推荐系统,看着它在内核层静默地保护着你的隐私,同时为你精准生成了一份未来一周的健康饮食方案。

jzwspace的专栏 1388

终极指南:如何用YAML配置文件3分钟生成专业线束图

还在为绘制复杂的线束图而烦恼吗?传统的CAD软件操作复杂,手绘图纸耗时费力,物料清单统计容易出错...这些问题在电气设计和硬件开发中屡见不鲜。今天我要向你介绍一个革命性的工具——**WireViz线束图绘制工具**,它能让你用简单的YAML配置文件,在几分钟内生成专业的电气连接图和物料清单。 WireViz是一个基于Python的开源工具,专为电气工程师、硬件开发者和项目管理者设计。它通过纯文本

gitblog_00177的博客 514
上一篇: 60分钟吃掉嘎嘣脆的DeepCross模型
下一篇: 120分钟吃掉DIEN深度兴趣演化网络
算法美食屋
博客等级 码龄9年 2万+粉丝 154原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值