news 2026/10/16 12:18:57

TensorFlow 2.0函数式API实现Wide Deep多输入多输出模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TensorFlow 2.0函数式API实现Wide Deep多输入多输出模型

做推荐系统的朋友应该都遇到过同一个问题:用户特征的类型差异太大,类别特征动不动就几十上百个取值,连续特征又分布在完全不同的量纲上。把它们一股脑塞进同一个模型,往往会出现“记忆”和“泛化”两头不讨好的情况。业界很早就给出了一个经典方案——wide & deep 模型,直接把线性模型和深度神经网络拼在一起,各干各的专长。到了 TensorFlow 2.0 时代,用函数式 API 实现 wide & deep 模型的多输入多输出,可以说是每个做推荐、做广告、做风控的工程师都绕不开的基本功。

这篇文章,我结合一个完整的电影推荐示例,把 wide & deep 模型的原理、多输入多输出的实现方式、训练推理阶段的各种坑,一次说清楚。如果你是正在学 TensorFlow 的入门者,可以把它当一节完整的进阶课来读;如果你已经在业务里用过 wide & deep,但总在多输入、多输出上犯迷糊,那重点看第4章,那里全是我实测踩出来的经验。

1. wide & deep 模型到底长什么样:补上原理这一课

1.1 从名字说起:wide部分和deep部分各自干什么

wide & deep 最早是推荐系统领域提出来的,核心思路就一句话:让一个线性模型负责“记忆”,让一个深度神经网络负责“泛化”,最终把两边拼接起来输出。很多人第一次接触这个概念都会被“wide”这个词绕晕,其实你只需要把它当成一个最简单的线性层就行——wide部分接收的是原始稀疏特征和人工构造的交叉特征,比如“用户ID=12345 且 电影类别=科幻”,然后直接做线性加权求和,输出一个 logit。它的特点是对训练数据里出现频次高的组合非常敏感,几乎能做到“死记硬背”。

deep部分就完全是另一套逻辑了。它对类别特征做嵌入(Embedding),把高维稀疏的 one-hot 编码压成一个低维稠密向量,再和连续特征一起灌进多层全连接网络,通过非线性激活函数学习特征之间从未显式组合过的交互规律。也就是说,deep部分不具备“记住某个具体样本”的能力,但它能从数据的隐含结构里推断出“这个用户大概喜欢什么调性的电影”。

我举一个很典型的例子:假设训练数据里反复出现“深夜访问 + 学生身份 → 点击”这个模式,wide部分负责把它记住,下次见到类似组合直接高概率触发;而如果来了一个从没见过的组合“学生 + 科幻 + 悬疑”,wide部分完全没概念,但deep部分可以根据“学生群体对科幻题材的平均偏好”以及“悬疑和科幻的共现规律”给出一个合理推断。这种分工就是 wide & deep 的精华。如果你发现模型跑出来的效果还不如一个普通 MLP,那大概率是没把特征做合理分流——这件事后面实战部分会重点演示。

1.2 为什么“多输入多输出”是wide & deep的标配能力

理解了结构之后,“多输入”其实是水到渠成的。因为 wide 和 deep 吃的本来就是不同的特征源:wide侧拿的是精挑细选出来的稀疏类别特征和叉积特征,deep侧拿的是嵌入后的类别特征和连续特征。如果你硬把这两类特征拼成一个长向量再塞进同一个输入口,虽然模型也能跑,但你在工程上就失去了分别控制两侧结构的自由,比如你想给wide侧单独加一个特征筛选模块,或者调整deep侧嵌入维度,都会变得非常别扭。所以正规做法都是把 wide 输入、deep 输入拆成两个甚至多个 Input 层。

再说“多输出”。真实业务里几乎没有“一个模型只预测一个数”的需求。推荐系统要同时预估点击率、收藏率、播放时长;广告系统要同时预估点击率、转化率、ROI;风控要同时判断欺诈概率和逾期金额。这些目标不是独立的,它们共享一批用户行为特征。与其分别训练三个模型浪费算力、还丢失彼此相关性,不如在一个模型里共享底层特征表示,然后各自接一个输出头。这就是多任务学习,也是多输出存在的根本理由。wide & deep 结构天然支持这种设计:前面wide和deep特征都算好了,到最后一层分裂成多个输出头,每个输出头各学各的损失函数。

另外多说一句,多输出还有一层“特征正则化”的隐含好处:辅助任务会给共享层带来额外的梯度约束,从而减轻主任务的过拟合。以电影推荐例子来说,如果你只预测评分,模型很容易死盯评分分布;但额外加一个“是否喜欢”的二分类辅助输出,模型就不得不同时学会区分“高分段里有真喜欢和假喜欢”,共享层的表征往往会更鲁棒。原理讲到够用的程度,下面进入 TensorFlow 2.0 的工程实现。

2. 函数式API:多输入多输出唯一的正确打开方式

2.1 Sequential模型为什么做不了这件事

我见过不少初学者习惯用Sequential堆网络,堆得挺顺,一遇到多输入就傻眼。原因很简单:Sequential这个名字本身就是“顺序执行”的意思,你往里头 add 的每一层都必须严格串行,前一层输出接后一层输入,整个模型只能有唯一的输入口和唯一的输出口。

这就像一条单车道公路,你没法在中间突然再并进来一条车流,也没法在岔路口同时分出两个方向。wide & deep 结构在数学上是一个有向图而不是一条链:wide 输入和 deep 输入是两个起点,中间存在拼接操作,最后还得分成多个终点。用Sequential表达这种图结构,不是写法技巧问题,是工具就不支持。

所以到了 TensorFlow 2.0,凡是涉及多输入、多输出、共享层、残差连接这类结构,都用函数式 API(Functional API)来做。这不是一道选做题,是必做题。

2.2 函数式API的关键概念:Input、图层连接与Model容器

函数式 API 的核心写法只有三个动作,但很多人没把它理解透。

第一个动作是声明Input。keras.Input(shape=(...), name='xxx')创建的不是一个层,而是一个占位张量对象,它定义了后续网络的输入形状和名字。注意,这里的 shape 是不含 batch 维度的,你写 shape=(1,) 表示每条样本输入一个维度长度为 1 的向量,如果你想输入一条长度为 3 的连续特征向量,就写 shape=(3,)。

第二个动作是把图层当函数来调用。Dense(64, activation='relu')(prev_tensor)这句的本质是“用这一层给张量做一次变换,产生新的张量”。这个括号调用的语法是很多人上手时最不适应的地方,其实把它想成数学里的函数复合就行:f(x) = Dense(64)(x),一层吃一个张量,吐一个新张量。

第三个动作是Model容器。把所有 Input 放进一个列表inputs=[input_wide, input_deep],把所有输出张量放进列表outputs=[rating_output, like_output],然后Model(inputs=inputs, outputs=outputs)就组装成了一个带多个入口和出口的模型。到这里,“多输入多输出”的骨架就算搭出来了。

还有一个容易忽视的概念:在函数式 API 里,同一个 Input 张量可以被后续多个分支复用,这就是“共享层”的基础。比如 deep 部分的嵌入层可以被两个输入塔分别调用,只要参数相同它们就共享权重。后面实战代码里我会演示这个点。理解这三个动作,接下来就能直接写代码了。我个人建议初学者把Model(inputs=..., outputs=...)这行当成整个模型的“注册表”,每次搭建前先在纸上画清楚哪个输入流向哪条分支,最后汇聚到哪个输出,再动手写代码,比自己反复试错快得多。

3. 完整实战:电影推荐场景的多输入多输出模型

3.1 业务场景与模型设计

假定我们要为一个电影App做一个“评分 + 是否喜欢”的联合预估模型。业务上,我们希望首页推荐能把用户不喜欢的电影过滤掉,但只靠一个“是否喜欢”二分类又显得太粗暴——同样是“喜欢”,9分和8分的推荐优先级应该是不同的。因此我们让模型同时输出两个目标:

  • 输出1:rating,取值0~10,回归任务,告诉推荐系统精确的分数预期。
  • 输出2:like,取值0或1(二分类),判断用户是否点进详情页并收藏。

输入侧则按 wide & deep 的标准分工来设计:

  • wide侧输入(sparse_features):包括用户性别、用户等级、电影年代、用户职业等类别特征进行 one-hot 后的 0/1 向量。这里为了演示方便,我直接模拟成一个长度为10的 0/1 向量。
  • deep侧输入:
    • user_id:类别特征,做 Embedding,捕捉用户隐向量。
    • movie_id:类别特征,做 Embedding,捕捉电影隐向量。
    • dense_features:连续特征,比如用户年龄、电影时长、用户历史平均评分,直接拼进deep网络。

把问题拆到这个粒度,训练数据和模型结构就很清晰了。

3.2 数据准备与embedding维度说明

我先说清楚这个演示用的数据集:不需要真实的公开数据集,直接生成一份模拟数据即可。规模不大,控制在 5000 条左右,每条样本包含上述各字段。因为本文重点在模型结构而不是数据处理精度,模拟数据足够复现整个流程。

import numpy as np np.random.seed(42) num_samples = 5000 num_users = 1000 num_movies = 500 data = { 'user_id': np.random.randint(0, num_users, num_samples), 'movie_id': np.random.randint(0, num_movies, num_samples), 'user_age': np.random.normal(30, 8, num_samples).clip(10, 60), 'movie_duration': np.random.normal(110, 30, num_samples).clip(60, 200), 'history_rating': np.random.normal(7, 1.5, num_samples).clip(0, 10), 'sparse_features': np.random.randint(0, 2, size=(num_samples, 10)), } rating = ( 0.3 * data['user_age'] / 60 + 0.5 * data['movie_duration'] / 200 + 0.6 * data['history_rating'] / 10 + np.random.normal(0, 0.2, num_samples) ) * 5 rating = np.clip(rating, 0, 10) like = (rating >= 7).astype(int)

模拟数据的合理性我提一句:这种构造方式人为给标签与特征加了线性相关性,所以模型收敛会比较快,但演示多输入多输出的核心逻辑完全够用。真实数据里特征与标签的关系复杂得多,本质上不影响下文要讲的模型结构。

Embedding 维度的选择,这里给一个大多数场景下有效的经验公式:

embedding_dim = min(50, (num_ids + 1) // 2)

这是一条经验值,不是数学定律。对于 user_id 有1000个取值,按公式得到50;对于 movie_id 有500个取值,我手动降到32,避免演示模型参数过多。实际业务中,如果内存允许,嵌入维度一般不会超过100。

3.3 模型搭建完整代码

下面是多输入多输出 wide & deep 模型的完整代码,我加了详细注释。

import tensorflow as tf from tensorflow.keras.layers import ( Input, Embedding, Flatten, Dense, Dropout, Concatenate ) from tensorflow.keras.models import Model num_users = 1000 num_movies = 500 # ---------- deep侧输入:user_id, movie_id, dense特征 ---------- user_input = Input(shape=(1,), name='user_id') movie_input = Input(shape=(1,), name='movie_id') dense_input = Input(shape=(3,), name='dense_features') # deep侧嵌入与特征处理 user_embed = Embedding(input_dim=num_users, output_dim=32, name='user_emb')(user_input) user_vec = Flatten(name='user_vec')(user_embed) movie_embed = Embedding(input_dim=num_movies, output_dim=32, name='movie_emb')(movie_input) movie_vec = Flatten(name='movie_vec')(movie_embed) deep_features = Concatenate(name='deep_concat')([user_vec, movie_vec, dense_input]) deep_hidden = Dense(64, activation='relu', name='deep_hidden_1')(deep_features) deep_hidden = Dropout(0.3)(deep_hidden) deep_hidden = Dense(32, activation='relu', name='deep_hidden_2')(deep_hidden) # ---------- wide侧输入:稀疏0/1特征 ---------- wide_input = Input(shape=(10,), name='wide_features') wide_out_pre = Dense(32, activation='relu', name='wide_hidden')(wide_input) # ---------- 融合:wide和deep最后拼接 ---------- combined = Concatenate(name='combined')([deep_hidden, wide_out_pre]) # ---------- 多输出:评分回归 + 是否喜欢分类 ---------- rating_out = Dense(1, activation='linear', name='rating')(combined) like_out = Dense(1, activation='sigmoid', name='like')(combined) model = Model( inputs=[user_input, movie_input, dense_input, wide_input], outputs=[rating_out, like_out], name='wide_deep_multi_io' ) model.summary()

这里有一个设计选择值得展开讲:wide侧我并没有像论文里那样直接线性输出,而是先加了一个小隐藏层再接输出。为什么?因为真正的 wide 侧是线性模型的“记忆”作用,在 TensorFlow 里就是Dense(1, use_bias=True)直接收货。但如果你连这个隐藏层都不保留,最终拼接处会出现一个问题:deep侧输出至少32维,wide侧如果只有1维,wide侧的表达会被 deep 侧彻底淹没。所以做演示时我会保留一个小的隐藏层,让 wide 侧拥有一定的表达能力,又保证 wide 侧的“记忆”特点不被破坏。在实际业务中,你可以根据实验效果决定 wide 侧多深,不要盲目迷信论文原版结构。

3.4 多输出训练配置与loss权重

模型搭好了,接下来是 compile 和 fit。多输出的 compile 和单输出不同,关键在 loss 和 metrics 都要用字典或列表来对应不同输出。

model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss={ 'rating': 'mse', 'like': 'binary_crossentropy', }, loss_weights={ 'rating': 0.5, 'like': 0.5, }, metrics={ 'rating': ['mae'], 'like': ['accuracy'], }, )

这里两个输出头的loss_weights非常关键。如果不设置,TensorFlow 默认对每个输出都是1.0,那么你会发现训练过程中总loss几乎被 mse 主导,因为 mse 的数值通常在5~25之间,而 binary_crossentropy 的数值通常在 0.3~0.7 之间,两个量级差了一个数量还多。新手常犯的错是看到总loss一直在掉,但 accuracy 纹丝不动,其实就是回归loss盖过了分类loss,模型只顾着拟合评分去了。把两个权重调成 0.5 / 0.5,可以让两个任务相对均衡地更新参数。

训练时传数据也要用字典,索引一对应就能避免很多低级错误:

x = { 'user_id': data['user_id'], 'movie_id': data['movie_id'], 'dense_features': np.stack([data['user_age'], data['movie_duration'], data['history_rating']], axis=1), 'wide_features': data['sparse_features'], } y = { 'rating': rating, 'like': like, } history = model.fit( x, y, epochs=20, batch_size=256, validation_split=0.2, verbose=1, )

使用字典传参的好处是你不用关心 Input 在列表里的顺序,只要 name 对得上,框架会自动找到对应的输入口。这一点在接tf.data.Dataset时尤其重要。把训练跑完,如果 val_loss 下降、val_rating_mae 和 val_like_accuracy 同时变好,说明多输出联合训练基本跑通了。

4. 训练和推理阶段最容易踩的坑

4.1 输入顺序与dict传参

说实话,多输入多输出 90% 的 bug 出在“数据喂错口”上。有人喜欢用列表传参,x=[user_ids, movie_ids, dense_feats, wide_feats],而列表的顺序必须和Model(inputs=[...])声明时的顺序完全一致,一旦你在中间的某个步骤调换了一次,后面所有数据就全错位了。更要命的是,这种错误不会报错,模型照样训练,loss 照样下降,最后上线推断才发现输入顺序错位了,白白浪费几天时间。

我的建议很简单:从第一天开始就用字典传参。model.fit支持x={'user_id': ..., 'movie_id': ...},Inference 时model.predict({'user_id': ...})也一样。只要名字和 Input 层的 name 保持一致,顺序随便写,框架内部会做匹配。同样的道理也适用于 y,用{'rating': ..., 'like': ...}而不是[rating, like]。

使用tf.data.Dataset构造流水线时也建议一路用字典到底:

def gen(): for i in range(num_samples): yield { 'user_id': data['user_id'][i:i+1], 'movie_id': data['movie_id'][i:i+1], 'dense_features': np.stack([data['user_age'][i], data['movie_duration'][i], data['history_rating'][i]]), 'wide_features': data['sparse_features'][i], }, { 'rating': rating[i], 'like': like[i], } dataset = tf.data.Dataset.from_generator( gen, output_signature=( { 'user_id': tf.TensorSpec(shape=(1,), dtype=tf.int32), 'movie_id': tf.TensorSpec(shape=(1,), dtype=tf.int32), 'dense_features': tf.TensorSpec(shape=(3,), dtype=tf.float32), 'wide_features': tf.TensorSpec(shape=(10,), dtype=tf.float32), }, { 'rating': tf.TensorSpec(shape=(), dtype=tf.float32), 'like': tf.TensorSpec(shape=(), dtype=tf.float32), }, ), ) model.fit(dataset, epochs=20)

这里注意一个小细节:user_id 和 movie_id 的 TensorSpec 我写成了shape=(1,),和 Input 层声明保持一致,避免在数据管道里出现维度不一致的报错。dense_features 则是shape=(3,),这些 shape 一定要和模型定义对齐,否则 fit 阶段会报“输入维度不匹配”。

4.2 形状不匹配与Embedding的维度问题

我排查过不少报错,最典型的是这个:

Input 0 of layer "user_emb" is incompatible with the layer: expected min_ndim=2, found ndim=1.

原因通常是user_input定义的 shape 和实际喂进来的数据不一致。Input(shape=(1,))期望每条样本是(1,)形状,配合 batch 维度整体是(None, 1)。如果你从某个 dataframe 里直接取一列当成 numpy 数组,它的 shape 很可能是(num_samples,)而不是(num_samples, 1),喂进去就报错。解决办法很简单:reshape 一下,或者用np.expand_dims把维度补上。

Embedding 层输出形状也要心里有数。Embedding的输入要求是整数张量,输出形状是(batch, seq_len, embedding_dim)。在我们的例子里,seq_len 就是1(每个样本只有一个 user_id),所以 Embedding 输出是(batch, 1, 32),直接接到Dense层会报错,必须先Flatten成(batch, 32)。很多人第一次写多输入 demo 时就在这里栽跟头,记住一句话:Embedding 输出至少需要 Flatten 或 GlobalAveragePooling1D 降维之后,才能和普通 Dense 层连在一起。

另外还是要提醒一下:Input 里的 shape 统一不含 batch 维,设计模型时别自己加一个None进去。加了也不报错,但理解上容易绕,保持统一会省很多心。

4.3 loss值域差异与weights平衡

这个坑在第三章已经埋了个伏笔,这里展开细讲。多输出联合训练,本质上是在优化一个加权和:

total_loss = weight_rating * MSE(y_rating, y_pred_rating) + weight_like * BCE(y_like, y_pred_like)

这里最大的问题是量纲不平衡,看下面这个对比:

任务常用损失初始量级若不调权重的后果
评分回归MSE8.0 ~ 15.0主导梯度,分类任务被压制
是否喜欢binary_crossentropy0.6 ~ 0.8比例太小,训练信号微弱

如果权重都是1.0,梯度基本被回归任务带着跑,分类任务的 accuracy 可能一直在同一个水平来回抖动。你从训练日志里看到的现象就是总loss在降,但 like_accuracy 卡住不上不下。

排查方法:把两个输出头的 loss 单独打印出来对比量级。TensorBoard 里直接看 rating_loss 和 like_loss 两条曲线,如果你发现它们差一个数量级,立刻调整 loss_weights。实际调参时我一般先设成回归 0.3 / 分类 0.7,然后观察两个验证指标的变化,再逐步微调。

还有一种情况更隐蔽:当两个任务难度差太多时,即使你调平了 loss 量级,简单任务(通常指分类)的梯度仍然很小,共享层基本学不到分类相关的信息。这时候需要检查模型最后几层的梯度范数,或者给分类塔单独加一层容量,让辅助塔自己承担一部分特征提取压力,而不是全部压在共享层上。

4.4 保存加载模型时的命名问题

多输入多输出模型的保存和加载,我强烈建议直接model.save('model.keras'),把整个结构、权重、优化器状态一起存,然后tf.keras.models.load_model一把加载。不要自己去手动保存权重再重建结构,因为多输入的输入名、多输出的输出名只要错一个,load 的时候就装不上。

更隐蔽的问题是,当你保存模型时如果用了自定义 Loss 或自定义 Metric,加载时框架会尝试从配置里恢复这些类,恢复不了就会报类似Unknown loss function: xxx的错。解决方式是在load_model里传入custom_objects字典,把你的自定义类映射进去。

另外要注意 h5 和 SavedModel 格式的差异:h5 是老格式,对自定义逻辑的支持更弱;.keras或 SavedModel 格式反序列化更稳。现在 TensorFlow 2.x 里默认推荐.keras,如果项目中涉及跨版本部署,务必在代码里明确保存路径的扩展名。

给一个每次保存加载都适用的模板:

# 保存 model.save('wide_deep_multi.keras') # 加载 from tensorflow.keras.models import load_model loaded_model = load_model('wide_deep_multi.keras', custom_objects={'focal_loss': focal_loss}) loaded_model.predict({'user_id': x_user, 'movie_id': x_movie, 'dense_features': x_dense, 'wide_features': x_wide})

5. wide & deep 多输出的进阶玩法

5.1 多任务学习:CTR与观看时长联合预测

上面电影推荐的例子是“多输入多输出”的入门演示,实际业务里最常见的多输出是 CTR 和观看时长(或转化率)联合预估。如果你的推荐系统只用 CTR 排序,很容易被标题党、猎奇内容带偏;把观看时长加进来,模型才更接近“用户到底爱不爱这个内容”。

结构上,wide 侧照样记忆“某个用户群体 + 某个内容类目”的共现规则,deep 侧学习内容向量与用户向量的交互,然后在共享层后面分两个输出头:一个预测点击概率,一个预测观看时长。观看时长是正数偏态分布,一般不会直接用 MSE,而是先做 log1p 变换,或者直接用Huber损失来抵抗极端值的影响。

这里有一个工程细节:CTR 和观看时长的正样本定义往往不一致。比如“点击但只看了一秒”算不算正样本?这直接影响 label 怎么构造。多输出模型的好处就是你可以把“是否点击”和“观看时长”作为两个 label 分开建模,而不需要硬性定义唯一标签,模型自己会在共享层上学习两者之间的关联。

5.2 自定义loss分别处理不同输出

多输出的另一个进阶点是给不同输出指定不同的损失函数,乃至自定义损失。比如二分类输出正负样本极不均衡(点击率5%都算高),普通的 binary_crossentropy 会让模型倾向于全预测为负。这时候可以给 like 输出专门配一个加权交叉熵或者 Focal Loss:

def focal_loss(gamma=2.0, alpha=0.25): def loss(y_true, y_pred): epsilon = tf.keras.backend.epsilon() y_pred = tf.clip_by_value(y_pred, epsilon, 1.0 - epsilon) pt = tf.where(tf.equal(y_true, 1), y_pred, 1.0 - y_pred) return -alpha * tf.pow(1.0 - pt, gamma) * tf.math.log(pt) return loss

然后把 rating 输出的 loss 换成 Huber,让评分输出对异常打分不那么敏感:

model.compile( optimizer='adam', loss={ 'rating': tf.keras.losses.Huber(delta=1.0), 'like': focal_loss(gamma=2.0, alpha=0.3), }, loss_weights={'rating': 0.5, 'like': 0.5}, )

Focal Loss 的代码逻辑很多资料都讲得不清楚,其实核心就一点:让模型多关注那些“被预测错”的样本。参数 gamma 控制压低已经预测对了的样本的梯度贡献,alpha 控制正负样本权重。在 CTR 这种极度不均衡的任务上,比普通交叉熵好用很多。

5.3 从玩具模型到真实业务的扩展思考

演示代码跑通只是第一步,真正把 wide & deep 多输入多输出用于业务,还有几个点值得提前想清楚。

一是特征分流要刻意设计,不要随手把所有特征塞进 deep 侧。wide 侧一定要放那些高频、强相关的稀疏记忆特征,比如用户行为序列里的“最近点击过的类目”、推荐位上的“曝光位置”;deep 侧才放用户长期画像、物品内容属性和各类统计特征。如果不做分流,wide & deep 就退化成单纯的 MLP,“宽模型”的名字名存实亡。

二是 batch 大小和 embedding 表的规模问题。真实场景用户量可能是千万级,Embedding 表会非常大,内存和显存都可能成为瓶颈。常见做法是限制词表大小、对低频 ID 做哈希分桶,甚至把嵌入表放到 CPU 内存模拟。这些属于工程优化,动手前最好先测一下当前服务器能承受的参数规模。

三是数据流要用tf.data从头到尾打通,不要在model.fit里传 numpy 数组上线。原因不光是性能,更重要的是真实特征的更新和缓存策略。用真实数据做测试时,tf.data配合并行预取(AUTOTUNE)可以让训练吞吐量翻倍,这个优化在 demo 里体现不出来,但上线后收益非常明显。

这套代码我在多个模拟项目和实际数据上都跑过,最深的体会是:多输入多输出真正难的地方不在 API 本身,而在心态。很多人第一次把模型搭起来、能训练就开始加各种花活,结果每个输出都欠拟合,最后只能砍任务。我个人的建议是,先跑通一个“宽+深、双输出”的最简结构,把特征分流、loss 权重这些基本功调明白,再逐步增加复杂度和新任务。按这个节奏走,大概率不会踩到那些让我折腾了好几个晚上的坑。

最后再分享一个小技巧:当你想确认多输出模型是否正常工作,直接把训练日志里的 val_rating_mae 和 val_like_accuracy 打印成图表对比观察。如果两个指标同时变好,说明共享层学到的表征是有效的;如果一个好一个坏,优先检查 loss_weights 和数据对齐,十有八九是这两处。祝一次跑通。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 12:17:43

MySQL CRUD 详解:表数据插入、更新与删除,附生产避坑指南

一、前言 回顾 DDL 与 DML 区别 DDL:操作的对象:库、表结构例如:增create/删drop/改alter/查show数据表、对数据表的操作(添加字段…) DML:操作的对象:表里面的数据例如:增insert/…

作者头像 李华
网站建设 2026/10/16 12:17:29

CAD圆弧A指令与坐标定位全解:从画弧到精准绘图的实用指南

从一张图纸里画不圆的那条弧说起。很多人学CAD时,直线、矩形用得很顺,一到圆弧就露怯——要么弧的方向反了,要么弧的半径对不上,要么明明想和旁边两条线相切,画出来却差了十万八千里。更麻烦的是,图纸上的定…

作者头像 李华
网站建设 2026/10/16 12:16:48

ERROR 1227 权限报错排查:从SUPER权限到DEFINER机制与恢复实践

每次看到 ERROR 1227 (42000): Access denied; you need SUPER privilege ,我都知道对方已经和这份报错搏斗过一阵子了。明明账号能建库、能改表、能导数据,看起来权限已经拉满,结果恢复一个备份,或者执行一条 SET GLOBAL 就当…

作者头像 李华
网站建设 2026/10/16 12:16:42

纯HTML和CSS实现购物车静态页面:布局细节与踩坑记录

购物车页面,大概是前端练手项目里含金量被低估得最严重的一个。看起来平淡无奇,但真正动手去复刻一个接近主流电商App的购物车静态界面时,你会发现布局、语义、样式细节全都用得上。我最近的练习就是把这个页面从零写了一遍,没有框…

作者头像 李华
网站建设 2026/10/16 12:16:40

【软考信息安全】第二十四章 工业控制系统安全分析与防护

本文为软考信息安全工程师个人学习笔记,依据官方考试大纲与公开技术规范整理,仅用于学习交流,不作为商业用途。一、工业控制系统概念及组成 1.1 基本定义 工业控制系统是由各种控制组件、监测组件、数据处理与展示组件共同构成的对工业生产过…

作者头像 李华
网站建设 2026/10/16 12:16:30

JSP知识体系实战:从EL表达式、JSTL到分页查询的完整学习路径

开篇先说个真实感受:JSP 这门技术,在社区里的口碑一直很分裂。新项目里基本看不到它的影子,但在大量存量系统的维护、学校教学和传统企业级开发中,它依然是绕不开的一环。很多人学 JSP 时,要么被各种“过时了赶紧学 Sp…

作者头像 李华