资讯动态

歌曲标签与用户画像双通道音乐推荐系统:从Spark计算到虚拟化部署实践

发布时间:2026/10/20 5:29:01 来源:尧图企业网站定制
不知道你有没有发现市面上讲音乐推荐的文章不少但大多数停留在“ALS协同过滤”或者“TF-IDF算相似度”这种单点技术上。真正把一个推荐系统从数据清洗、歌曲标签建模、用户画像构建到Spark分布式计算、虚拟化环境部署最后再拿离线指标评估一轮的完整链路文章其实很稀缺。这个项目当初给我的最大启发也在这里它不是在演示某个算法而是在完整回答一个工程问题——我有海量听歌行为数据和歌曲元数据怎么在有限的机器资源里跑出一个能让用户觉得“有点懂我”的推荐系统。这篇文章我会把这套基于歌曲标签加用户画像双通道再叠加Spark分布式计算能力的设计思路完整写出来包括每个模块为什么要这样做、踩过的坑、以及虚拟化部署阶段最容易忽略的资源和内存问题。1. 为什么音乐推荐比电商推荐更“吃”用户画像和标签1.1 音乐消费场景的特殊性音乐推荐和商品推荐的底层逻辑有本质区别。买一件商品用户主动搜索、比较、下单意图很明确推荐只需要在用户已有明确目标时做“顺水推舟”。但听歌是典型的“无目的消费”——大多数人打开音乐App时并不知道自己今天想听什么更多是“随便听听”。这种场景决定了推荐系统必须主动出击而不是等用户明确表达需求。还有一个很关键的问题歌曲本身不像商品那样有“功能属性”。一件外套可以说“保暖”“防风”这些属性可以直接对应购买理由但一首歌的“属性”是主观、多面、模糊的。同一首歌有人觉得是“治愈”有人觉得是“悲伤”甚至同一首歌曲在不同情绪状态下的听觉体验完全不同。这意味着纯靠歌曲自带的结构化字段歌手、语种、年代来做推荐精度很快就会撞到天花板。这也是这个项目把“歌曲标签”和“用户画像”作为双核心的原因。歌曲标签解决的是把主观感受尽量转译成可计算的特征用户画像解决的是把用户的零散行为转译成一个稳定的、个性化的偏好结构。两者叠加系统才能摆脱“只推荐一样东西”的尴尬做到“我知道你可能想要什么调性的歌”。1.2 项目要解决的问题边界动手前一定要先划定问题边界。这个项目最开始很容易陷入“什么都想做”的泥潭比如实时推荐、流式计算、深度学习召回结果大概率是半年都出不了效果。当时我们收敛后的核心目标其实只有三个第一构建一套基于歌曲标签的“内容侧”推荐基线。系统能根据用户的历史行为从标签维度反推用户当下最偏好的音乐风格组合。第二基于用户行为数据构建立体画像用协同过滤的方式捕捉“和自己品味相似的人都在听什么”。第三把这两条通道的结果做加权融合兼顾推荐的精准度和惊喜度并跑通“Spark处理全量数据、离线周期性更新”的完整工程链路。配套文档1.3万字骨架也就是围绕这三条展开的。如果你现在也准备做类似的推荐项目我强烈建议先把这三个环节想清楚再动代码。目标越清晰后面写到论文或项目文档时反而越省力。2. 歌曲标签建模把“听感”变成可计算的特征2.1 标签体系怎么设计才不会乱歌曲标签是整套推荐的地基。地基如果只有“好听”“喜欢”这种词后面所有的相似度计算都是笑话。标签体系需要满足三个要求有区分度、有层次、覆盖全面且不过度稀疏。我们当时把标签拆成了四个维度风格摇滚、民谣、电子、情绪治愈、燃、丧、场景通勤、深夜、运动、年代80/90/00年代。这样设计的好处是一首歌的标签向量天然具备了“多面性”不管是做内容召回还是做相似度计算都能从不同角度切入。标签的选择优先级也很重要。不是所有标签权重都一样风格和情绪是核心权重场景和年代作为辅助修正。举例来说一首歌标了“摇滚”和“治愈”两个标签系统在做用户今日推荐时应该优先命中用户画像里的“摇滚”偏好而“治愈”这种情绪标签更适合在晚间场景下放大权重。有人会问标签从哪来最靠谱的是用歌曲平台的公开元数据做主集再用歌词文本和评论数据做扩充。这个项目里我们对每首歌最终保留的标签控制在5到15个之间太少了区分度不够太多了向量稀疏会让计算开销成倍上涨。2.2 从单曲标签到用户-标签偏好矩阵有了歌曲标签不能直接用推荐系统的第一大步要把“用户-歌曲”的历史行为转化成“用户-标签”的偏好强度。这个过程本质上是在做一个矩阵变换先统计每个用户对每首歌的行为得分播放权重、收藏权重、跳过惩罚等再把歌曲得分按照标签维度累加。举个例子某用户本月听了十首歌其中六首带“摇滚”标签、三首带“电子”标签那么他在这两个标签上的原始偏好分就是行为得分的累加值。这里有个特别需要注意的细节直接累加会让热门标签霸榜。全网热门标签永远是“流行”“情歌”如果按原始累加值排序几乎所有用户都会被推成“情歌爱好者”这显然不合理。所以必须做归一化和逆文档频率修正——我把标签权重设计成语义级TF-IDF某个标签越普遍它在用户偏好向量里的权重越低这样反而是“爵士”“后摇”这种小众标签能为用户画像贡献更高的区分度。偏好向量成型后每个用户就变成了一条结构化画像记录维度就是标签体系里的几百个标签项。后续无论是找相似用户、还是直接做标签召回用的都是这个“用户-标签”偏好矩阵。2.3 标签相似度计算的一个实用坑标签建立后最常规的用途是算歌曲间相似度以做“听了A就推B”的关联推荐。很多人在这一步会直接上余弦相似度跑出来结果却让人摸不着头脑——比如一首《公路摇滚》和一首《放学路上的风》相似度居然高达0.8只因为两首歌都标了“夏天”和“奔跑”。问题出在余弦相似度对稀疏向量的匹配方式太“宽泛”。它只看维度上是否有共同非零项不看共同项本身的重要性。我们最终的解决办法是加了一个权重修正两个标签之间的共现频率要乘一个惩罚项共现越频繁的标签组合说明它越基础、越不具区分度权重就要下调。改完之后类似“夏天奔跑”这种泛化标签组合带来的虚高相似度被压了下去推荐结果一下子“懂行”了很多。3. 用户画像构建逻辑行为数据才是第一生产力3.1 事件行为如何拆解成可用的画像素材用户画像听起来高大上落到数据层面就是“把行为事件序列加工成统计特征”。原始数据通常长这样用户名、歌曲ID、行为类型播放/收藏/跳过/循环、时间戳、设备类型。这套项目里我们把这些行为全部转成了带权重的偏好信号。具体权重分配是加收藏“喜欢”行为权重最高权重系数设为5完整播放播放时长达到整首歌的80%以上权重系数为3仅播放但没听完的权重系数为1秒跳的直接记-2作为明确的负反馈信号。这套规则看起来简单实际测试下来比直接套算法有效得多因为用户行为天然带有“喜欢程度”的连续差异只看“是否播放”会丢失太多信号。时间段也是一个常被忽略的维度。如果你发现某个用户凌晨2点到5点之间高频听歌那他的历史歌单大概率偏向安静、氛围类的音乐。我们把时间窗划成早高峰、工作时间、晚间、深夜四段画像里多存一组“时段-标签”偏好这样白天推荐系统会主动避开那些太“躁”的歌深夜则多推点舒缓内容。3.2 画像维度的优先级排序画像不是维度越多越好维度过高会直接给计算链路带来压力而且很多维度对推荐结果的质量贡献极低。项目最终定版的画像主维度是音乐风格偏好向量、情绪偏好向量、歌手偏好Top20、语种偏好、近7日活跃时段分布、平均歌曲新鲜度容量判断用户是“单曲循环型”还是“探索尝鲜型”。这个设计里有一个维度最容易被忽略平均歌曲新鲜度容量。如果用户最近一周听的新歌占比很少说明这个用户更偏好稳定、熟悉的歌单系统给他的推荐就不应该太多新面孔反之则要加大探索类的推荐比例。这个维度对“惊喜度”的调节特别有效它本质上是在给推荐系统上一根用户侧的“探索刹车”。我个人的体会是歌手偏好这一项的价值比很多算法教程里说的要高得多。音乐平台里存在大量“因为喜欢某歌手所以整张专辑循环”的用户把这个维度单独提出来做一层歌手召回实际评测中点击率提升非常明显而且代码就那么几行。3.3 画像更新的频率设计画像的更新频率是个典型的工程权衡题。白天改得太频繁Spark任务会一直跑资源被吃满更新太慢用户最近两三天的兴趣迁移完全捕捉不到。最后的方案是“双频更新”全量画像每天凌晨两点跑一次用时大约40分钟增量画像每六小时跑一次只处理最近6小时的高价值行为数据收藏、循环、完整播放用小体量Spark任务完成计算。增量更新的效果立竿见影——某个用户周一听了大量“拉丁语系”歌曲当天下午就能在首页看到对应风格的推荐。全量重算则负责把用户长期的偏好稳定性校准回来避免增量数据过度放大短期偶然行为。这两种节奏的组合我建议所有做推荐系统的项目都模仿一下成本不高但体验边际收益很大。4. 双通道融合标签召回与ALS协同过滤怎么协同4.1 为什么不用单通道做完全部推荐市面上很多教程会把“协同过滤”当成唯一解实际项目中单用ALS会遇到两个很难解决的短板冷启动新歌曲没有任何行为数据协同过滤完全推不动和覆盖度热门歌曲的交互数据太庞大ALS很容易把推荐结果固化在流行区用户看到的时候会觉得“怎么又是这些热门歌”。而标签通道天然能冷启动——一首歌只要被人工或机器打了标签马上可以进入召回池参与推荐同时因为标签分布天然拥有长尾性反而更容易把小众歌推出去。所以双通道是结构性的必要不是锦上添花。标签通道负责“兜底 多样性”协通过滤负责“精准 品味捕捉”。4.2 融合打分的权重设计两条通道都会产出一个用户对候选歌曲的打分问题是怎么融合。最简单的是线性加权最终得分 0.6 × ALS得分 0.4 × 标签得分但实测效果一般。后来改成得分先做标准归一化再按用户活跃度动态调权——高活跃老用户的行为数据多ALS信号更可靠ALS权重升到0.7低活跃新用户行为稀疏协同过滤结果不可信权重降到0.3完全交给标签通道主导。这个动态调权的思路其实就是根据“数据充足度”来分配信任度非常符合直觉。如果你要在这个项目上做升级我建议往上下文方向上再挖一步晚间和通勤时段加大标签权重因为用户更偏情绪导向工作时段加大协同过滤权重因为用户更多在主动检索内容。4.3 多路召回加精排的Pipeline结构项目跑通的最终Pipeline是这个结构多路召回标签召回 相似用户协同召回 歌手召回 热门兜底 - 融合打分 - 规则过滤去重、去除已听、下架歌曲屏蔽 - TopN排序输出多路召回的好处是每一条通道都足够简单、可单独排查问题而且每路召回几千条候选几路加起来不过几万条排序阶段的压力完全可控。这个结构在实际生产环境也是主流做法远比单靠一个巨型模型直接出TopN更稳定、更好排查问题。规则过滤这一步不要觉得它“不智能”就小看它。用户已经听过的歌、连续推送过三次都没点击的醉酒歌手、版权状态不可用的歌如果不过滤掉再好的算法都会被体验拖垮。这个项目里我们专门维护了一张“用户负面反馈表”一旦用户对某个歌手点了三次“不喜欢”这个歌手未来两周都会对他的候选列表进行屏蔽。5. Spark在整套链路里到底扮演什么角色5.1 不用分布式行不行很多人的第一反应是这套推荐系统看起来数据量也没多大为什么要用Spark这个问题的答案要先搞清楚项目的数据规模。在完整使用场景下行为日志表每天新增约一千万条播放记录用户量超过三百万歌曲元数据加上标签映射也接近百万级。这种数据量级单机Python直接在这个规模上算相似度矩阵内存早就撑爆了一次性全量模型训练动辄十几个小时根本不实用。用Spark核心解决的就是两件事内存中分布式计算的高吞吐以及把特征工程、模型训练、评估的整个Pipeline统一跑在一套计算框架下不需要为了不同环节去搭各种独立服务。5.2 具体用到Spark的哪些能力和模块我在Spark的使用上做了一次三次演进这也是项目从能跑到跑稳的关键。第一阶段直接用Spark Core加RDD做数据清洗当时把日志数据从JSON解析出来、过滤异常字段、简单聚合逻辑很好写但代码冗长性能也很一般。第二阶段迁移到Spark SQL上了用DataFrame做清洗和特征宽表构建.groupBy().agg()写起来的效率比RDD高出好几倍而且Catalyst优化器自动做了谓词下推和列裁剪同样一批数据清洗任务执行时间直接缩短了40%左右。第三阶段才引入MLlib做ALS模型训练以及用Spark的分布式计算能力来跑召回阶段的批量相似度矩阵计算。给一个参考的ALS训练伪代码结构from pyspark.ml.recommendation import ALS from pyspark.ml.evaluation import RegressionEvaluator als ALS( userColuser_id, itemColsong_id, ratingColrating, rank50, maxIter12, regParam0.08, coldStartStrategydrop, implicitPrefsTrue, alpha30.0 ) model als.fit(train_df) pred model.transform(test_df) evaluator RegressionEvaluator( metricNamermse, labelColrating, predictionColprediction ) rmse evaluator.evaluate(pred) print(fRMSE: {rmse})需要特别说明的是implicitPrefsTrue这个参数。我们是拿用户的行为事件转出来的评分喂给模型的并不是用户真实的显式打分所以必须用隐式反馈矩阵。这个参数很多人第一次用经常漏掉结果模型输出的分数会严重偏离合理区间。alpha控制的是隐式反馈置信度默认值往往偏高对行为数据比较稀疏的项目我建议往下调到20到35之间再结合离线指标观察。5.3 训练数据和评估数据切分里的坑模型训练最不经意的坑是怎么划分训练集和测试集。很多教程直接随机切分这在时间序列型数据里是大忌。用户今天的听歌行为从时间上天然依赖昨天以前的所有行为随机切分会把未来的信息泄漏进训练集评测结果虚高得离谱实际上线效果却很差。我们把数据按时间切分前80%时间范围内的行为做训练最后20%时间范围内的行为做验证集。除此之外还要做一次用户粒度去重切分验证集时要把训练集里已经出现过的用户与验证集重叠的交互项剔除确保每个验证集的评分记录对应的用户和歌曲组合在训练集里没有出现过完全相同的一条交互。这一步做完后模型的离线指标大概掉了15%到20%但线上效果反而稳定了——这说明之前完全是虚高。5.4 全流程任务编排的基本心得整个推荐流程涉及数据清洗、特征构建、画像更新、模型训练、召回计算、融合排序等多个Spark作业需要一套工作流把这些任务串起来。项目用的是最轻量的一套方案——用Crontab定时触发脚本脚本按顺序调用spark-submit。顺序必须严格安排先做增量数据清洗合并再更新用户画像和歌曲标签宽表然后训练ALS模型并得到协同过滤召回池标签召回池是直接从宽表算的可以跟模型训练并行最后做融合打分和结果写库。每一步输出都落地成Parquet格式存到HDFS这样每一步可以独立重跑不会一个环节出错全链路推倒。还有一点Spark UI的日志一定要定期查看尤其是Executor的Shuffle读写量。如果你发现某个Stage的Shuffle Write高达几个GB就该检查是不是在某个环节做了不合理的宽依赖操作这时候在数据源就做预处理聚合比在Spark任务里硬扛要快得多。6. 虚拟化部署环境里的资源规划和避坑记录6.1 为什么要把整套环境跑在虚拟机上作为一套要提交文档和做演示的完整项目直接在物理机或云服务器上部署一个多节点Hadoop加Spark集群成本太高而且演示完环境就闲置了。我们的选择是在单台物理机上创建三台虚拟机分别作为主节点和两个工作节点构建一个小型但完整的分布式环境。这样跑通的数据链路、代码逻辑、部署脚本都能直接平滑迁移到更大的集群节省成本的同时又不牺牲技术栈完整性。虚拟机创建时有一个绝对不要省的操作提前拍好快照快照必须在不安装复杂环境时拍即新建完虚拟机、安装好CentOS最小化系统后就拍一个干净快照。后面配置Hadoop、Spark、MySQL、HBase每一个环节都可能把系统折腾坏一个干净快照能让你随时回到一小时前的状态。初期我因为没做快照调Spark配置时改坏了系统重装了三次虚拟机整整浪费了一个下午。6.2 三台虚拟机的资源切分方案物理机是32GB内存、8核的配置如果是16GB内存的机器资源切分要更保守。最终分配方案如下虚拟机角色操作系统内存CPU核心磁盘主节点CentOS 7.914GB480GB工作节点1CentOS 7.910GB260GB工作节点2CentOS 7.98GB260GB主节点要同时跑NameNode、ResourceManager、Spark的Driver还要做历史服务器内存压力最大所以给最多。每台虚拟机至少预留了2GB内存给操作系统本身确保不会因为主机内存不足导致OOM连环崩。系统盘之外我专门给每台虚拟机挂了一块独立数据盘磁盘部分用来放HDFS数据目录和Spark的临时目录系统和数据分离配置出问题时排查范围小很多。6.3 搭建完Spark集群后最容易忽视的四个检查项第一主机名和IP映射。Hadoop和Spark极度依赖主机名解析如果/etc/hosts里没配好DataNode会随机连不上NameNodeSpark Executor会反复注册失败。搭建完成第一步就是三台机器互相ping主机名通了再启动服务。第二SSH免密登录。主节点需要免密登录到两个工作节点以启动从属进程。很多人配置了免密但忘了检查authorized_keys的权限后来发现SSH证书绕过、无法建立连接问题不在密码就是权限太开放。目录和文件权限必须设置成700和600。第三Spark历史日志目录。默认情况下Spark在程序跑完后日志就清空了调参时根本看不到之前的执行日志。在spark-defaults.conf里开启spark.history.fs.logDirectory并启动HistoryServer之后每次的Executor日志、Shuffle信息、GC时间才有迹可循。第四虚拟机的CPU核心数不建议超分。工作节点看起来分配了2核但如果宿主机本身已经在跑很多任务这两个虚拟核心的实际计算能力会很弱。要把虚拟机的虚拟化引擎的硬件辅助虚拟化勾上否则运行速度会明显偏慢模型训练时间翻倍。6.4 内存不足和OOM的实战排障Spark作业最常见的崩溃就一句话Container killed by YARN for exceeding memory limits。做这个项目时第一次跑全量ALS工作节点直接爆内存当时第一反应是调大spark.executor.memory结果越调越崩。真正的病根在YARN的yarn.nodemanager.resource.memory-mb配置和Spark Executor内存的关系。Executor默认会额外申请堆外内存开销这个开销大约是内存设置值的10%到20%。如果单台NodeManager总内存定为10GBExecutor申请了8GB加上1.6GB的堆外开销会直接超过NodeManager上限被YARN杀掉。正确做法是控制单节点Executor数量、Executor内存以及堆外占比这三者的乘积严格小于NodeManager可分配内存。同时打开spark.memory.offHeap.enabled并给堆外内存一个预留值这样Shuffle和RDD缓存能走到堆外不会一有压力就往堆内塞导致Full GC。如果你跑的任务体量不大最省心的策略其实是直接调成单Executor模式每台工作节点只跑一个Executor内存设成NodeManager上限的70%堆外预留25%。虽然并发度低一点但稳定度非常高。对绝大多数演示和文档型项目来说保稳定比拼并发更重要。6.5 角色分配不均衡的调整三节点集群最容易出现的一个现象是主节点CPU跑满两个工作节点闲着没事干。这不是网络问题而是Spark默认把Driver和Executors的任务分配对主节点压力过大尤其是spark.default.parallelism和HDFS块分布不合理导致大量任务在主节点本地读取数据。做了一轮调整后我在代码里显式增加repartition次数让任务尽量打散到不同节点同时调整了HDFS的副本系数让数据块均匀分布。优化之后同样的ALS训练任务从28分钟降到了17分钟而且整台主节点不再像以前那样一直卡在99%的占用率。7. 效果评估与冷启动离线指标只能信一半7.1 离线评估到底看什么指标推荐系统的离线评估不能只看RMSE或MAE。RMSE低只能说明模型对历史交互的拟合度高不代表用户真的会点。我们同时看了四个指标RMSE评分预测误差、召回率验证集合中被召回命中的比例、精确率推荐列表里被点击的比例以及覆盖率推荐结果里的歌曲种类覆盖了多少全量歌曲。这里有一个经验覆盖率低是最值得警惕的信号。第一次全量训练出来的模型覆盖率只有13%也就是说推荐来推荐去永远是那一小撮热门歌。后来加入标签召回通道和多路召回覆盖率一下子提升到36%精确率不降反升——因为这个项目里“推荐点没点”高度依赖“推没推过”热门歌用户早就看见了覆盖率代表着系统触达长尾的潜力。7.2 冷启动场景怎么处理冷启动拆成用户冷启动和歌曲冷启动两个问题。用户新注册没有任何行为数据我们直接采用“标签问卷 热门分档”的组合策略新用户注册时选三到五个风格标签系统按这些标签从歌曲池里抽取种子候选同时把歌曲分成“热门主推”“中腰部”“小众潜力”三个档位每个档位按稀疏比例混合出最终推荐列表保证新用户不至于只看到一堆全网大热歌。新用户一旦产生三条以上播放行为系统马上切换成正式的双通道推荐逻辑。歌曲冷启动更简单也更重要新歌没有交互数据协同过滤直接失效于是新歌先强制进入标签通道的候选池依靠标签相似度参与召回并有三个自然日的“新歌保量期”——这段时间内只要用户画像匹配度达到阈值就会以一定概率出现在用户推荐流里。保量期内积累的行为数据达到十次后新歌正式进入协同过滤的候选里。7.3 ALS参数调优的地图ALS本身的四个参数我把实际调参后的结论整理如下参数作用本项目最终值调参方向rank分解矩阵的隐含维度50过小欠拟合偏大训练慢且容易过拟合maxIter最大迭代次数12超过20次基本无提升只拖慢速度regParam正则化系数0.08偏大有欠拟合风险偏小验证集波动大alpha隐式反馈置信度30数据稀疏时下调到20数据密集时上调到40调参方法强烈不建议网格搜索硬跑。网格搜索看起来自动化实际上每个组合都要一次全量训练几个小时就进去了。更聪明的做法是先固定其他参数只扫rank为20/50/80三档找到最优档位后再扫alpha的15/25/35三档最后微调regParam。三轮扫描加起来最多十次训练就能接近最优组合。7.4 效果评测不只是调一个参数的事如果发现离线指标一直上不去绝大多数问题不在调参而在数据质量或特征设计不合理。比如用户画像里“平均歌曲新鲜度容量”这个维度一开始设计得过于粗糙没能区分单曲循环型用户和探索型用户后来拆细到“七日听歌去重率”和“新歌占比”两个子指标整个模型分数才有了明显起色。还有一个最容易被忽视的杀手级因素——标签噪声。标签来自不同渠道往往同一首歌被标成“民谣”又标成“电音”互相矛盾。这种冲突标签会直接污染“用户-标签”偏好矩阵。我们专门做了一个标签冲突检测环节同一首歌里若两个标签之间历史上从未共现超过3次就要人工审核或直接降权处理。做完这套评测和调优整个项目从最初的离线RMSE 1.42、召回率21%最终收敛到RMSE 1.18、召回率37%、覆盖率36%也算达到了一个可以演示和作为作业交付的水平。8. 复盘做完这个项目我最后想说的三点第一标签、画像和Spark本身都不神秘这套项目最大的价值在于让你看清“一个生产可用的推荐系统”到底由多少环节共同兜底数据质量、标签体系、行为建模、算法选型、资源规划、评测闭环缺一环系统都会显得很“飘”。第二虚拟机搭建集群的过程虽然琐碎但它逼着你把分布式计算里最容易出问题的网络通信、内存分配、日志排查都过了一遍这些技能在直接在云服务器上“一键建集群”的快捷方式里反而学不到。第三如果你也想复刻这个项目我的建议是按“标签召回先跑通、画像维度先建准、ALS最后再优化”的顺序来推进不要一上来就急着调一个豪华模型。最后分享一个小技巧给项目写配套文档时把每个模块的“为什么这样设计”单独列一节不要只贴代码和参数。这个文档之所以能写到1.3万字很大程度上就是因为每一个模块的选型、参数、踩坑点都被记录下来后来复盘时翻起来效率非常高这也是这个项目留给我最有价值的产物。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑