目录
美团的强化学习应用场景和分析
场景举例
使用原因
强化学习的六大要素
智能体
环境
行动
奖励
目标
状态
美团强化学习模型设计
美团强化学习工程落地
总体的数据结构关系图
实现步骤
1. 日志收集与实时处理(Log Collector, Online Joiner)
2. 经验回放(Experience Collector)
3. 离线训练(Offline Training)
4. 在线学习(Online Training)
5. 模型服务(Model Serving)
补充说明
1. 特征数据(FeatureData)
2. 状态表示(State)
3. 在线服务中的模型计算
4. 模型更新与版本管理
5.为什么要离线和在线训练相结合
相关资料
美团的强化学习应用场景和分析
场景举例
使用原因
传统的推荐算法,如协同过滤、矩阵分解等,在很多场景下已经可以取得不错的效果。那么,我们为什么还要引入强化学习呢?主要有以下几点原因:
-
强化学习能够建模多轮交互场景。传统推荐算法通常是根据用户的长期历史行为和兴趣来进行建模,给出一次性的推荐结果。但在美团"猜你喜欢"这样的场景下,用户可以与推荐列表进行多轮交互,如翻页、点击等。每一轮交互后,用户的即时反馈实际上反映了用户当前的意图和偏好,需要推荐系统及时捕捉并调整策略。强化学习通过奖励机制可以很好地建模这种动态变化的过程。
-
强化学习可以优化长期收益。传统算法通常是针对眼前的指标(如预测准确率)进行优化。但推荐系统的最终目标是提升用户的长期参与度、留存率和满意度。强化学习的目标恰恰是最大化长期累积奖励,与这个目标更加一致。通过折扣因子等机制,强化学习能权衡当前和未来收益,避免短视行为。
-
强化学习可以灵活纳入各种额外信息。用户行为是在一个复杂多变的真实环境中产生的,与用户自身的动机和场景高度相关。强化学习可以将场景特征、时间段、用户属性等各种额外信息都建模到状态表征中,捕捉用户行为的背景,给出更精准、个性化的推荐。传统算法对场景建模的能力相对有限。
-
强化学习能够平衡探索和利用。推荐系统需要在向用户推荐他们已知的感兴趣的物品(利用),和探索用户可能感兴趣的新物品(探索)之间权衡。过度利用会让用户觉得推荐单一乏味,过度探索又会损害用户体验。强化学习通过ε-贪心等策略,可以在二者间找到平衡,并随时调整。
-
当然,强化学习也不是万能的。它对样本数据的要求很高,训练过程不够稳定,落地的工程复杂度高。因此现实中需要根据具体的场景特点,来权衡是否值得引入强化学习。像美团这样有足够数据和工程能力的大型推荐系统,使用强化学习可以进一步提升推荐的效果。但对于很多中小型系统,传统算法可能依然是更现实的选择。
强化学习的六大要素
智能体
美团的推荐系统
环境
美团App的"猜你喜欢"推荐场景
行动
生成推荐商品列表
奖励
用户在推荐列表上的点击、下单行为的加权和
奖励”指的是推荐系统把推荐列表推送给用户之后,用户在这个列表之上的反馈。对于美团来说,“猜你喜欢”展位的核心优化指标是点击率和下单率,这里的奖励就可以被定义成推荐列表中物品的点击次数和下单次数的加权和。如下面的公式所示,其中的 wc 和 wp 分别是点 击次数和下单次数的权重,这两个超参数可以根据你对它们的重视程度进行调整。
目标
最大化多轮交互后的累积奖励
它的形式化表达如下所示:
- s 表示当前状态(state)
- a 表示在状态 s 下采取的动作(action)
- t 表示当前所处的时间步(time step)或决策阶段
- r_{t+k} 表示在时间步 t+k 时获得的奖励(reward)
- γ (gamma) 是折扣因子(discount factor),0 <= γ <= 1,用于控制未来奖励的重要程度。γ 越大,则表示越重视长远的奖励
状态
通过深度学习网络生成的综合表征用户意图、兴趣、场景的embedding向量
这里比较有意思的是第一个部分,它采用了单层 CNN,也就是卷积神经网络来学习用户的实 时意图表达,这是我们之前没有遇到过的处理手法。那它是怎么做的呢?它首先把用户交互获得的 Item Embedding 组成了一个矩阵,然后在这 个矩阵之上使用了两个不同尺度的卷积层,再对卷积层中的每个行向量进行池化操作,生成两 个代表了用户实时意图的 Embedding,把它们与第二部分的场景特征向量连接后,再经过全连接层,生成最终代表状态的 State Embedding。
美团强化学习模型设计
- 采用一个类似DQN的actor-critic架构作为核心的强化学习模型:
- Critic网络(Advantage函数):评估在某状态下采取某行动的优劣
- Actor网络(Value函数):评估某状态下的期望long-term收益
- 线上线下结合的工程架构:
- 离线进行深度模型的预训练,如状态embedding网络
- 线上进行实时的数据处理和模型finetune,动态优化模型
- 针对性地优化线上serving的效率,如embedding层剥离、预热等
- 整个推荐流程与数据流、训练、serving等环节紧密结合,形成一个完整的闭环
美团强化学习工程落地
总体的数据结构关系图
原始日志数据(String,JSON 格式)
|
v
解析后的日志事件(LogEvent)
|
v
样本数据(Sample)<--(在线特征 Join)-->特征数据(FeatureData)
|
v
经验数据(Experience)(经过会话窗口和序列处理)
|
v
离线训练和在线学习(使用 Sample 和 Experience 进行训练)
|
v
模型参数(State Network、Advantage Network、Value Network、Embedding Layer)
|
v
模型服务(加载模型参数,提供在线推荐服务)
实现步骤
1. 日志收集与实时处理(Log Collector, Online Joiner)
使用 Flink 作为流处理框架,从 Kafka 中消费日志数据,进行实时数据处理和特征合并,然后将处理后的样本数据写回 Kafka 或下游存储。
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer// 创建 Flink 流执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment// 配置 Kafka 消费者参数
val kafkaConsumerProps = new Properties()
kafkaConsumerProps.setProperty("bootstrap.servers", "localhost:9092")
kafkaConsumerProps.setProperty("group.id", "log_consumer_group")// 创建 Kafka Source,用于消费日志数据
val logSource = new FlinkKafkaConsumer[String]("log_topic",new SimpleStringSchema(),kafkaConsumerProps
)// 从 Kafka 中读取日志数据
val logStream: DataStream[String] = env.addSource(logSource)// 对日志数据进行解析和预处理
val parsedLogStream: DataStream[LogEvent] = logStream.map(parseLog) // 自定义的日志解析函数,将字符串解析为 LogEvent 对象// 配置 Kafka 生产者参数
val kafkaProducerProps = new Properties()
kafkaProducerProps.setProperty("bootstrap.servers", "localhost:9092")// 创建 Kafka Sink,用于输出处理后的样本数据
val sampleSink = new FlinkKafkaProducer[Sample]("sample_topic",new SampleSerializationSchema(),kafkaProducerProps,FlinkKafkaProducer.Semantic.AT_LEAST_ONCE
)// 进行在线特征 Join(假设 featureStream 是已存在的特征数据流)
val featureStream: DataStream[FeatureData] = ...// 在线 Join,将日志数据与特征数据合并
val sampleStream: DataStream[Sample] = parsedLogStream.keyBy(_.userId).connect(featureStream.keyBy(_.userId)).process(new OnlineJoiner()) // 自定义的 ProcessFunction,实现在线 Join// 将合并后的样本数据写入 Kafka
sampleStream.addSink(sampleSink)// 启动 Flink 作业
env.execute("Log Collector and Online Joiner")
注意:
parseLog
是自定义的函数,用于解析原始日志字符串为LogEvent
对象。OnlineJoiner
是自定义的CoProcessFunction
,实现了两个流的在线 Join 操作。SampleSerializationSchema
是自定义的序列化方案,用于将Sample
对象序列化为字节数组写入 Kafka。数据流动过程:
- 原始日志数据从 Kafka 输入,类型为
String
。- 使用
parseLog
函数将原始日志字符串解析为LogEvent
对象。- 与特征数据流(
FeatureData
)进行在线 Join,生成Sample
数据。- 将
Sample
数据写入下游系统(如 Kafka、存储系统等)。
case class LogEvent(userId: String, // 用户IDitemId: String, // 物品IDaction: String, // 用户行为类型,例如点击、购买timestamp: Long // 行为发生的时间戳// 其他可能的字段,例如客户端信息、位置信息等
)case class Sample(userId: String, // 用户IDitemId: String, // 物品IDuserFeatures: Map[String, Double], // 用户特征,键值对形式itemFeatures: Map[String, Double], // 物品特征,键值对形式contextFeatures: Map[String, Double], // 上下文特征,键值对形式action: String, // 用户行为label: Double, // 标签,例如点击为1,未点击为0timestamp: Long // 行为发生的时间戳
)
2. 经验回放(Experience Collector)
使用 Flink,对在线 Join 的样本数据进行会话窗口划分,生成强化学习模型训练所需的序列数据。
import org.apache.flink.streaming.api.windowing.assigners.EventTimeSessionWindows
import org.apache.flink.streaming.api.windowing.time.Time// 假设 sampleStream 是上一步产生的样本数据流
val sampleStream: DataStream[Sample] = ...// 设置时间特征为事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)// 对样本数据进行水位线设置,以支持基于事件时间的窗口操作
val timestampedSampleStream = sampleStream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[Sample](Time.seconds(10)) {override def extractTimestamp(element: Sample): Long = element.eventTime})// 按照用户 ID 进行会话窗口划分
val sessionWindows = timestampedSampleStream.keyBy(_.userId).window(EventTimeSessionWindows.withGap(Time.minutes(30)))// 在会话窗口内收集经验数据
val experienceStream: DataStream[Experience] = sessionWindows.process(new ExperienceCollector())// 将经验数据输出到下游(如写入 Kafka、存储到文件系统等)
experienceStream.addSink(....)
注意:
ExperienceCollector
是自定义的ProcessWindowFunction
,用于在会话窗口内收集用户交互序列,生成Experience
对象。- 需要确保样本数据流中包含事件时间戳,并正确设置水位线(Watermarks)以支持事件时间窗口。
数据流动过程:
- 样本数据按照
userId
进行会话窗口划分,形成用户的行为序列。- 在会话窗口内,使用
ExperienceCollector
将样本数据组装为强化学习的序列数据,即Experience
对象。- 将
Experience
数据发送到下游系统(如用于在线训练的 Kafka 主题或存储系统)
case class Experience(userId: String, // 用户IDstateSeq: Seq[State], // 状态序列actionSeq: Seq[String], // 动作序列rewardSeq: Seq[Double], // 奖励序列nextStateSeq: Seq[State], // 下一个状态序列doneSeq: Seq[Boolean], // Episode 是否结束的标记序列timestampSeq: Seq[Long] // 时间戳序列
)// 定义状态
case class State(userFeatures: Map[String, Double], // 用户特征itemFeatures: Map[String, Double], // 物品特征contextFeatures: Map[String, Double] // 上下文特征
)
3. 离线训练(Offline Training)
使用 TensorFlow 离线训练相对稳定的模型部分,如状态网络(State Network)和 Embedding 层。
import tensorflow as tf# 定义状态网络结构
def build_state_network(input_shape):inputs = tf.keras.Input(shape=input_shape)x = tf.keras.layers.Dense(128, activation='relu')(inputs)x = tf.keras.layers.Dense(64, activation='relu')(x)outputs = tf.keras.layers.Dense(state_dim)(x)model = tf.keras.Model(inputs, outputs, name='StateNetwork')return model# 定义 Embedding 层
def build_embedding_layer(vocab_size, embedding_dim):embedding_layer = tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=embedding_dim, name='ItemEmbedding')return embedding_layer# 建立模型
state_network = build_state_network(input_shape=(feature_dim,))
embedding_layer = build_embedding_layer(vocab_size=item_count, embedding_dim=embedding_dim)# 定义优化器和损失函数
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
loss_fn = tf.keras.losses.MeanSquaredError()# 加载训练数据(假设数据存储在 HDFS 上)
def data_generator():for sample_batch in read_hdfs_samples(hdfs_path):yield sample_batch['features'], sample_batch['labels']# 创建数据集
train_dataset = tf.data.Dataset.from_generator(data_generator,output_types=(tf.float32, tf.float32),output_shapes=((None, feature_dim), (None, state_dim))
).batch(batch_size)# 训练模型
for epoch in range(num_epochs):for step, (features, labels) in enumerate(train_dataset):with tf.GradientTape() as tape:embeddings = embedding_layer(features['item_ids'])inputs = tf.concat([features['user_features'], embeddings], axis=1)predictions = state_network(inputs)loss = loss_fn(labels, predictions)gradients = tape.gradient(loss, state_network.trainable_variables + embedding_layer.trainable_variables)optimizer.apply_gradients(zip(gradients, state_network.trainable_variables + embedding_layer.trainable_variables))print(f'Epoch {epoch + 1}, Loss: {loss.numpy()}')# 保存模型
state_network.save('models/state_network')
embedding_layer.save_weights('models/embedding_layer_weights')# 将训练好的 Embedding 参数保存到在线存储,如 Redis 或 Tair
item_embeddings = embedding_layer.get_weights()[0]
save_embeddings_to_tair(item_embeddings)
注意:
read_hdfs_samples
是自定义的函数,用于读取 HDFS 上的样本数据。- 需要将训练好的模型和 Embedding 参数保存,以供在线服务使用。
数据流动过程:
- 从存储系统读取训练数据,解析出模型所需的特征和标签。
- 使用 TensorFlow 构建模型,输入为用户特征和物品特征,目标为预测用户的行为。
- 训练模型,调整模型参数。
- 训练完成后,保存模型结构和参数。
- 提取 Embedding 层的参数(物品 Embedding 向量),存储到在线存储系统(如 Tair、Redis)。
# 假设输入的数据是 TFRecord 格式,包含以下字段
{'user_features': tf.train.Feature(float_list=tf.train.FloatList(value=[...])), # 用户特征向量'item_features': tf.train.Feature(float_list=tf.train.FloatList(value=[...])), # 物品特征向量'label': tf.train.Feature(float_list=tf.train.FloatList(value=[...])), # 标签# 其他可能的字段
}
4. 在线学习(Online Training)
对需要在线更新的网络(如 Advantage Network 和 Value Network),实现流式的在线训练。
import tensorflow as tf# 定义 Advantage Network 和 Value Network
def build_advantage_network(input_shape):inputs = tf.keras.Input(shape=input_shape)x = tf.keras.layers.Dense(64, activation='relu')(inputs)outputs = tf.keras.layers.Dense(action_dim)(x)model = tf.keras.Model(inputs, outputs, name='AdvantageNetwork')return modeldef build_value_network(input_shape):inputs = tf.keras.Input(shape=input_shape)x = tf.keras.layers.Dense(64, activation='relu')(inputs)outputs = tf.keras.layers.Dense(1)(x)model = tf.keras.Model(inputs, outputs, name='ValueNetwork')return modeladv_network = build_advantage_network(input_shape=(state_dim,))
value_network = build_value_network(input_shape=(state_dim,))# 定义优化器和损失函数
adv_optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
value_optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
loss_fn = tf.keras.losses.MeanSquaredError()# 从流中读取经验数据(假设使用 tf.data 从 Kafka 读取)
def experience_data_generator():for exp_batch in read_experience_stream(kafka_topic):yield exp_batch['states'], exp_batch['actions'], exp_batch['rewards'], exp_batch['next_states']# 创建数据集
experience_dataset = tf.data.Dataset.from_generator(experience_data_generator,output_types=(tf.float32, tf.int32, tf.float32, tf.float32),output_shapes=((None, state_dim), (None,), (None,), (None, state_dim))
).batch(batch_size)# 在线训练循环
for step, (states, actions, rewards, next_states) in enumerate(experience_dataset):# 计算目标值with tf.GradientTape() as tape_adv, tf.GradientTape() as tape_value:advantages = adv_network(states)values = value_network(states)next_values = value_network(next_states)td_targets = rewards + gamma * tf.stop_gradient(next_values)advantages_selected = tf.gather_nd(advantages, tf.stack([tf.range(tf.shape(actions)[0]), actions], axis=1))# 计算损失adv_loss = tf.reduce_mean(tf.square(advantages_selected - (td_targets - values)))value_loss = loss_fn(td_targets, values)# 计算梯度并更新参数adv_gradients = tape_adv.gradient(adv_loss, adv_network.trainable_variables)value_gradients = tape_value.gradient(value_loss, value_network.trainable_variables)adv_optimizer.apply_gradients(zip(adv_gradients, adv_network.trainable_variables))value_optimizer.apply_gradients(zip(value_gradients, value_network.trainable_variables))if step % 100 == 0:print(f'Step {step}, Adv Loss: {adv_loss.numpy()}, Value Loss: {value_loss.numpy()}')# 定期保存模型参数if step % 1000 == 0:adv_network.save_weights('models/adv_network_weights')value_network.save_weights('models/value_network_weights')
注意:
read_experience_stream
是自定义的函数,用于从 Kafka 中读取 Experience 数据流。- 在线训练需要注意数据的稳定性和训练速度,可以考虑使用分布式训练或异步更新等优化策略。
数据流动过程:
- 从经验数据流中读取
Experience
数据(与经验回放模块的输出相同)。- 将经验数据解析为用于训练的张量,包括状态、动作、奖励、下一状态等。
- 使用在线学习算法(如策略梯度、Q-learning 等)计算损失和梯度。
- 更新 Advantage 网络和 Value 网络的模型参数。
- 定期保存模型参数,供模型服务模块加载。
5. 模型服务(Model Serving)
用 TensorFlow Serving 部署模型服务,解决模型热加载和多版本管理的问题。
模型配置:
# 创建模型配置文件 models.config
model_config_list: {config: {name: 'recommendation_model',base_path: '/models/recommendation_model',model_platform: 'tensorflow',model_version_policy: { all: {} }}
}
启动 TensorFlow Serving:
tensorflow_model_server --port=8500 --rest_api_port=8501 \--model_config_file=/path/to/models.config \--file_system_poll_wait_seconds=60
模型热加载和预热:
为了避免模型加载时影响请求处理,可以采用预热策略:
import requests
import json# 模型预热函数
def warm_up_model():# 构造一个 dummy 请求dummy_request = {"signature_name": "serving_default","instances": [dummy_input_data]}# 发送请求到 TensorFlow Servingresponse = requests.post('http://localhost:8501/v1/models/recommendation_model:predict', data=json.dumps(dummy_request))if response.status_code == 200:print('Model warm-up successful.')else:print('Model warm-up failed.')# 在模型部署后进行预热
warm_up_model()
请求处理时的线程池优化:
需要对 TensorFlow Serving 的源码进行修改,分离模型加载与请求处理的线程池。这需要一定的 C++ 开发能力和对 TensorFlow Serving 内部架构的了解。
版本控制和灰度发布:
可以通过在模型路径下放置不同版本的模型,并使用 TensorFlow Serving 的模型版本策略进行控制。
--model_version_policy="{\"specific\": {\"versions\": [1, 2]}}"
模型监控和自动化部署:
- 集成 Prometheus 或其他监控工具,监控模型的性能和请求情况。
- 使用 CI/CD 工具(如 Jenkins、GitLab CI)实现模型的自动化构建、测试和部署。
补充说明
1. 特征数据(FeatureData)
在在线 Join 阶段,需要将日志事件与用户特征和物品特征进行合并,特征数据可能来自于缓存、数据库或实时计算。
用户特征(UserFeature)
case class UserFeature(userId: String,featureMap: Map[String, Double] // 用户特征键值对
)
物品特征(ItemFeature)
case class ItemFeature(itemId: String,featureMap: Map[String, Double] // 物品特征键值对
)
2. 状态表示(State)
状态通常是用户特征、物品特征和上下文特征的组合,用于表示在某一时刻用户的"状态"。
状态(State)
case class State(userEmbedding: Vector[Double], // 用户特征的嵌入向量itemEmbedding: Vector[Double], // 物品特征的嵌入向量contextFeatures: Map[String, Double] // 上下文特征
)
3. 在线服务中的模型计算
在线服务需要根据输入的数据结构,使用加载的模型参数进行计算。
计算步骤:
状态表示计算
使用状态网络(State Network)对用户特征、物品特征和上下文特征进行处理,生成状态表示。
动作价值评估
使用 Advantage 网络和 Value 网络,对当前状态下的各个可能的动作(物品)计算价值。
动作选择
根据计算的价值,选择排序得分最高的物品作为推荐结果。
4. 模型更新与版本管理
模型服务需要支持模型的热更新和多版本管理,以确保服务的连续性和稳定性。
模型版本目录结构示例:
/models/recommendation_model/1saved_model.pbvariables//2saved_model.pbvariables/
模型服务可以配置自动加载最新版本的模型,并在后台完成模型的加载和预热。
5.为什么要离线和在线训练相结合
模型的稳定性:
有些模型组件,如状态表征网络(State Network)、物品 Embedding 等,其参数相对稳定,不需要频繁更新。这些组件可以在离线通过大规模历史数据进行充分训练,得到一个性能不错且稳定的模型。离线训练通常批量大、迭代充分,模型更加鲁棒。实时性和适应性:
另一些组件,如 Advantage 网络、Value 网络等,其作用是对当前环境下的行为策略做出及时反馈和评估,以指导模型做出调整。这就需要基于用户的实时反馈数据进行更新。在线训练可以让模型快速适应用户偏好的变化,提供更加个性化的推荐。计算资源的平衡:
端到端的深度学习模型通常计算量很大,若完全在线训练,对计算资源和响应时间都是巨大的挑战。合理的划分离线和在线训练的边界,可以在保证一定实时性的同时,最大化利用离线计算资源,减轻在线系统的压力。
相关资料
强化学习在美团“猜你喜欢”的实践_文化 & 方法_美团技术团队_InfoQ精选文章