华为云ModelArts实战:从训练作业到在线服务的完整部署流程

发布时间:2026/10/11 8:07:49

华为云ModelArts实战:从训练作业到在线服务的完整部署流程 这两年AI平台层出不穷但真正让我觉得训练到部署一条龙这件事能在一个页面里闭环完成的华为云ModelArts算是一个典型。之前在本地用GPU跑实验光是配环境、装驱动、管依赖就能耗掉大半天更别提训练完还要自己写服务端推理代码、搞镜像、调并发。我自己也是从零开始啃ModelArts的文档和实际操作踩了不少坑这篇笔记就是把整个训练-部署链路里我觉得最值得记录的东西沉淀下来包括平台到底解决了什么问题、一个训练作业从数据准备到日志排查怎么走通、模型部署成在线服务有哪些隐藏细节以及费用和调优的权衡。1. 上手前的核心认知ModelArts到底帮你省掉了什么说实话第一次打开ModelArts控制台时我的第一反应是东西太多了。里面有ModelArts Studio、训练作业、推理平台、AI应用管理、开发环境等等好几个大板块每个板块点进去还有一堆子菜单确实容易懵。但用了一段时间之后我梳理出一个很核心的理解ModelArts本质上做的事情是把一条原本散落在不同系统里的AI项目流水线收拢到一个平台上。这条流水线大概是四个环节数据准备、算法开发、模型训练、模型部署。在没有平台的情况下数据要自己写脚本处理算法要在本地IDE里写训练要在自己的GPU机器上跑部署要自己搞服务器和API框架。中间任何一步出了问题都要自己排查环境、版本、依赖这些工程细节而不是算法本身的逻辑。ModelArts的省力点在于训练资源是弹性的。你不是买一台物理机而是按需申请一个训练作业的资源池。用完就释放不用养机器。数据和代码分离通过OBS对象存储来中转。数据集、训练脚本、输出模型都放在OBS里训练作业读取和写回都在云端完成。部署环节变成配置化操作。训练出来的模型文件注册到AI应用管理中然后一键创建在线服务平台负责拉镜像、起容器、做负载均衡和监控。对我来说理解到这一层训练作业和在线服务这两个概念就不再是孤立的功能按钮而是整个流水线上两个关键的枢纽节点。另外需要提前说清楚一点ModelArts并不是一个傻瓜式平台——不是说你把数据传上去它就能自动帮你调参、自动训练出一个完美模型。它更准确的说法是托管式平台训练脚本还是你自己写数据处理逻辑还是你自己定义ModelArts负责的是把你写的这些东西放到一个稳定、可控、可复现的运行环境里去执行并且管理好输入输出和日志。所以上一篇笔记里我写过一句话ModelArts是给真正要做模型训练的人准备的加速器而不是给完全不懂AI的人准备的玩具。想清楚这一点后面对平台各种设置的理解就会顺畅很多。2. 从OBS数据集到训练脚本一次训练作业是怎么跑起来的2.1 训练作业的输入输出模型一切围绕OBS转我第一次在ModelArts上创建训练作业时比较困惑的是代码从哪里来。不像本地跑实验那样直接打开IDE写代码然后运行在ModelArts里训练代码需要先放到一个OBS桶里然后在创建训练作业时指定代码的路径。这里有一个很重要的设计逻辑训练运行的机器是无状态的临时容器。你可以把训练作业想象成一个加工车间它启动时从仓库OBS拿原料代码、数据加工完成后再把产品模型权重、日志文件放回仓库。车间本身不保留任何东西作业一结束这台机器就被回收了。因此准备训练作业需要三样东西训练脚本文件夹包含你的入口脚本比如train.py以及项目依赖的Python模块。数据集放在另一个OBS路径下训练脚本通过参数接收数据路径。输出路径训练完成后模型文件、checkpoint会保存到这个路径下。创建训练作业的时候核心配置项大概是这几个训练任务名称比如我用的是bert-finetune-test-v1这种格式方便后续区分版本。算法来源可以选择常用框架里的预置引擎也可以用自己的镜像。预置引擎的好处是不用手动装PyTorch、TensorFlow这些框架平台已经帮你装好了。训练输入和训练输出对应前面说的数据路径和输出路径以参数名OBS路径的形式配置。比如代码里定义了一个参数叫data_url那么训练作业配置里就会有一个data_url的输入。这个参数名路径的映射关系是训练脚本和数据之间的桥梁也是新手比较容易忽略的地方——脚本里用的参数名和作业配置里填的参数名对不上运行的时候就会报找不到数据的错误。2.2 资源池和规格怎么选计算固然重要但别忽视环境一致性资源池这块ModelArts提供两种选择按需计费的公共资源池以及包周期的专属资源池。公共资源池适合实验和短期任务按实际使用时长计费。我一开始用的就是这种好处是灵活跑完就释放。但有个点要注意公共资源池的底层机器是共享的如果赶上平台上别人也大量提交作业资源调度可能会有排队。专属资源池的效果会好很多因为资源是独享的但你得包周期购买费用就高了。一般来说如果训练任务很频繁、对训练时间敏感可以租一个专属资源池。如果是学习、验证想法先用公共资源池把流程跑通再考虑上专属资源。规格选择上ModelArts给了很多种CPU版、GPU版、多卡NPU版等。我个人的经验是代码调试阶段用CPU版或最小规格的GPU版便宜主要是验证逻辑正确。正式训练如果需要几个小时甚至更久再选高配GPU或NPU规格。另外一个特别容易踩坑的地方是换了规格之后代码运行的环境可能有差异。比如有的规格里CUDA版本是11.8有的可能是12.0同一份PyTorch代码在小规格机器上跑没问题换到另一个规格可能因为GPU架构不同或者驱动版本问题报奇怪的错误。这不是ModelArts特有的问题但因为是云端托管平台你无法直接SSH到机器上看环境排查起来会比本地更麻烦。所以我的习惯是确定一个常用规格后尽量保持稳定不要频繁切换。2.3 从创建到完成我实测的一次训练作业全流程我用一个具体的例子来说明整个流程。假设我需要用一份自定义数据集微调一个文本分类模型。首先我把训练数据和脚本组织成下面的目录结构传到OBSobs://my-bucket/ ├── code/ │ ├── train.py │ └── requirements.txt ├── data/ │ ├── train.csv │ └── dev.csv └── output/train.py的入口部分大致是这样接收参数的import argparse import os if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--data_url, typestr, default/tmp/data) parser.add_argument(--train_url, typestr, default/tmp/output) args parser.parse_args() print(Reading data from:, args.data_url) # 实际训练逻辑... print(Model saved to:, args.train_url)然后在ModelArts的训练作业页面点击创建训练作业配置如下算法来源选择PyTorch预置框架版本我选了比较稳定的那个。训练输入参数名data_url路径指向obs://my-bucket/data/。训练输出参数名train_url路径指向obs://my-bucket/output/。规格选了一个单卡GPU的配置。提交之后平台会拉取镜像、启动容器然后执行训练脚本。训练过程里日志会实时打印出来。这里有一个比较实用的技巧在代码里多加print输出比如打印当前epoch、loss值、学习率、显存占用这样在训练作业的日志页面就能直观看到训练是否正常推进。不少初学者喜欢闷头训练代码里没有几个print结果作业跑了半小时日志里全是框架的初始化信息不知道自己代码执行到哪里排查起来特别吃力。整个训练过程如果顺利大概几分钟到几十分钟不等取决于数据量和模型大小。完成后输出路径下就会出现模型权重文件和训练日志。2.4 训练失败怎么办日志排查的三板斧训练作业失败是几乎每个人都会碰到的事。我刚入门的时候在一个模型上连续失败了好几次后来总结出三条排查路径现在基本能快速定位问题。第一先看作业日志的开头部分。很多时候失败信息就在最开始的地方比如没找到指定路径、依赖库缺失、CUDA不可用、数据集文件解压失败。这些错误在日志前几十行里就能看到。第二看退出码。如果模型代码抛了异常退出码往往能反映出来类型比如内存不足、执行权限、文件不存在等。第三看有没有stderr输出。有时候代码本身没有崩溃但某些底层库会往stderr打印警告这些警告可能是关键线索——比如某个算子不兼容当前GPU架构性能会骤降再比如OBS读取速度异常缓慢说明网络或桶权限配置有问题。还有一个排查技巧是在代码的入口处用print把传入参数原样打印一遍。这样在日志里能看到平台实际传入的数据路径、输出路径是否都正确映射能排除掉参数对不上这一类问题。我的建议是不要一失败就盲目重试先花5分钟把日志完整看一遍。第一次失败往往能省下后面十次失败的时间。3. 训练完成后模型要经历哪些加工才能对外提供服务3.1 训练输出和模型注册的差异不只是上传一个文件很多人以为训练完了直接把权重文件传到一个模型库里服务就算部署好了——这个理解不太准确至少在ModelArts上不是这样。训练作业的output里是一个或者一组模型文件比如pytorch_model.bin、config.json。这些文件只是原料在线服务需要的是标准化产物。ModelArts提供了一个专门的模型管理模块它支持两种做法一种是直接用平台自带的推理镜像配合模型文件组成一个AI应用。另一种是把你自定义的推理代码比如处理输入数据、调用模型、拼接输出结果的Python脚本和模型文件打包放到指定目录结构里再构建成AI应用。我平时用的是比较多的方式自己写一个config.json和推理脚本放到模型目录的固定位置然后让ModelArts根据这套结构启动一个支持预测的服务。这个模型推理脚本运行环境的组合体在ModelArts里叫做AI应用。AI应用是可以有版本的比如同一个模型你优化了推理逻辑就可以发布一个新版本。所以从训练产出到可部署中间其实多了一步把一个模型文件组织成一个规范的、可被平台识别加载的AI应用。这一步很容易被跳过但也是新手最容易卡住的地方。3.2 模型转换和AI应用创建的注意事项创建AI应用的时候有几个关键细节值得单独记一下模型路径和文件必须符合平台规则。比如如果你用系统预置的推理引擎那么模型文件通常要放在指定的model/目录下推理脚本要按照规定的格式写。如果路径不对构建AI应用时就会报模型文件缺失或配置文件格式错误。推理脚本的输入输出是JSON结构。ModelArts在线服务对外暴露的是HTTP接口请求体是一个JSON内部会把这个JSON解析后传给推理脚本里的处理函数。你需要在脚本里定义好如何解析请求体里的字段、如何传给模型做推理、如何把结果整理成JSON返回。推理脚本里的依赖也要声明清楚。比如用了哪个版本的transformers、numpy都要在推理配置或requirements文件里申明否则线上服务启动时会因为缺依赖直接失败。创建AI应用之后通常还会有一个部署的过程。体系中会先提供测试调用你可以用一个模拟请求去看看返回结构是否正常。3.3 从AI应用到在线服务的部署流程部署这一步是把AI应用真正变成一个可以实时调用的HTTP接口。ModelArts里的入口是在线服务或推理服务。配置内容如下选择要部署的AI应用和版本。选择服务资源规格比如CPU规格、GPU规格、实例数量。实例数越多能承担的并发请求越高但费用也越高。设置环境变量比如模型路径、超时时间、打日志级别等。开放公网访问或仅内网访问。如果是学习测试可以用公网方式快速验证如果是生产环境建议放到内网通过API网关转发。提交部署后平台会构建服务。这个过程可能需要几分钟期间会有启动中状态。服务成功运行后会给你一个调用地址以及一个API鉴权的token。用curl就可以做一次完整的预测请求。我当时测试的时候因为请求体里有个字段名字写错了服务返回了一个比较神秘的错误码一度怀疑是部署配置有问题。后来仔细看推理脚本的日志才发现是解析请求体时KeyError很快定位到问题核心在输入格式约定上而不是平台部署本身。4. 推理服务实际调用与常见问题别让最后一公里翻车4.1 成功的请求长什么样一次在线预测的完整报文服务部署成功后我建议先用一个最简单的请求验证通链路。比如我的文本分类模型请求大概是这样的{ texts: [今天天气真不错适合出门走走], top_k: 1 }返回结果{ results: [ { label: positive, score: 0.98 } ] }这里有个小经验定义请求和返回结构时尽量把字段设计得和你的业务语义一致而不是直接暴露模型的推理内部参数。比如模型内部的logits你可以在推理脚本里把它处理成label和score这样调用方不需要了解模型细节接口更容易被其他系统对接。4.2 推理性能慢怎么查从请求时延到规格调整在线服务的调用时延受很多因素影响。我自己实测下来的经验是模型本身的计算量是根本瓶颈。一个BERT模型做一次推理可能只要几十毫秒但如果并发很高单实例吞吐不够就会有排队现象。推理脚本里有没有做推理结果缓存有没有做批量处理如果每个请求都重新加载模型那速度一定很慢。规范做法是在初始化阶段加载模型参数到内存后面每次请求只做前向计算。网络传输和请求体大小也会影响感知时延。JSON字段多、数据量大时传输耗时不可忽略。如果服务时延变高第一步先看监控面板里的CPU使用率GPU利用率请求失败率这些指标。如果GPU利用率很高说明计算饱和如果GPU利用率低但时延高很可能是网络或推理脚本解析逻辑的问题需要从代码层面排查。4.3 服务加载模型失败最典型的部署翻车现场部署在线服务时经常遇到的一个问题是模型加载失败。日志里会提示找不到某个文件、内存不够、或者某个Python库版本冲突。这类问题之所以常见是因为你在训练时依赖的环境和在线推理时的环境往往不完全一致。训练时你可能用的是一个包含很多开发依赖的镜像推理时平台用的是裁剪过的基础镜像很多包没有装。解决办法有两种一是尽量在推理脚本里做最小化依赖少用不必要的库降低环境不一致带来的风险。二是在创建AI应用时明确指定推理环境所需的依赖清单把版本号写清楚别用latest这种模糊版本。还有一个被很多人忽略的问题模型文件太大导致加载超时。平台对模型加载一般会有一个超时时间如果你加载的模型超过10GB而实例规格内存又不够可能会启动失败或者反复重启。这种情况要么换更大内存的实例规格要么想办法把模型裁剪、量化后再部署。5. 用ModelArts做项目的成本观算清楚钱再提交训练5.1 计费方式按需、包周期、套餐到底选哪个ModelArts的费用构成并不复杂主要就两块训练资源和推理资源。训练资源这块按需计费是按秒计费的——你启动一个训练作业跑多久算多久。规格越高每分钟价格越高。如果是实验验证用按需更划算如果是长期每天训练包周期会有一定折扣。我个人建议在没有明确预算需求之前先用按需跑通流程统计一下一天大概需要多少小时再决定是否转包周期。推理资源这块在线服务是按实例运行的时长计费的。部署一个GPU实例挂在那里即使没有请求进来也在收费。这一点和训练不一样——训练跑完作业就结束推理服务是常驻的。所以如果你的功能还处在联调测试阶段别急着用很高配的GPU实例等真正要对线上流量做压力测试时再升级规格。5.2 省钱技巧与实际项目中的权衡目前我在学习阶段能分享的省钱策略有这么几条训练作业结束后确认释放资源。如果是按需计费作业结束资源自动回收但如果你用了开发环境Notebook记得关闭或释放Notebook实例因为它也是一直在计费的。测试推理服务时可以先部署一个CPU版本用少量数据验证功能和调用链路正确等确认无误后再改为GPU版本。CPU实例比GPU实例便宜很多。如果一些任务可以批处理完成就不需要部署常驻服务可以改用批量推理作业的方式用完即止也能省不少钱。定期清理不再使用的AI应用和旧版本模型。虽然存储费用一般不高但积累多了也是额外的开销。另外ModelArts还有免费额度和新手体验资源新用户可以先利用这些额度把基础流程跑通不用一上来就充值。不过要注意免费资源的规格和时间限制超了就会自动扣费建议在控制台里设置费用提醒。6. 调试训练作业的一些小技巧从日志到代码的协作习惯6.1 用开发环境Notebook做断点调试别直接提交训练我一开始不太习惯在云端开发总觉得不如本地IDE顺手。后来发现ModelArts提供的Notebook开发环境其实很适合做调试。流程上我一般在Notebook里把数据加载、模型定义、训练循环的每一步都用单元格跑一遍确认每一步输出正常后再把整个训练逻辑封装成train.py提交为训练作业。这样做的最大好处是调试阶段能看到中间变量的真实值遇到报错可以直接修改不用反复提交训练作业等几十分钟。用Notebook还有一个隐藏优势它自带一些可视化能力比如把loss曲线用简单的绘图库画出来看一眼就能判断模型收敛趋势比在训练日志里翻数字直观得多。6.2 日志规范结构化日志帮你快速定位训练问题在云平台上训练你的眼睛就是日志。代码里的print最终会出现在ModelArts的日志面板上。我的习惯是训练脚本里把关键信息用统一格式打印出来比如[INFO] epoch1 step100 loss2.34 lr5e-5这样打印出来的日志有规律、可搜索。排查问题时在日志面板里按关键词搜索loss或者ERROR能快速定位到问题发生的epoch和step。我还试过把关键指标比如训练集/验证集准确率、显存占用率、机器负载等定期打印出来这些对了解训练状况、判断是否要提前终止作业都有帮助。6.3 输出模型文件时要注意的目录权限问题训练脚本里最常见的保存模型代码如下model.save_pretrained(args.train_url)但有一点要注意args.train_url接收到的路径可能是平台挂载的一个存储路径也可能是一个OBS路径的映射。你在代码里直接往这个路径里写文件平台会自动同步到OBS。如果路径不对或者没有写权限代码不会报错但文件保存不到预期位置后续部署就找不到模型。我的习惯是在保存模型前用print打印出当前保存路径的具体值然后再保存。训练完成后去OBS控制台看一下模型文件是否真的出现在目标目录确认后再进行下一步操作。7. 进阶方向流程化、自动化和更贴近生产的配置7.1 从手动提交到自动训练工作流的价值跑通一次训练、部署一个服务之后很多人会问每次修改代码、数据更新后是不是都要手动重新创建训练作业、重新部署服务答案是没必要。ModelArts提供了工作流Pipeline编排能力可以把数据处理训练模型评估串联成一个流程。设定好触发条件和输入路径之后数据的更新可以自动触发新一轮训练训练产生的模型可以按规则自动注册成新版本。刚开始我并没有马上用工作流因为手动流程已经能满足学习需求。但如果你是在团队里做项目迭代频率高那么自动化的收益会非常明显。它还能保证每次训练的可复现性——因为流程固定下来了参数、数据、代码版本都保持一致。7.2 模型评估与版本管理别让最好模型找不到来源在多次实验后模型文件会非常多。我见过有同学把所有的输出模型都堆在同一个OBS目录文件命名为model_v1、model_v2、model_final、model_final2……到最后根本分不清哪个是基于哪个数据和参数训练出来的。我现在的做法是每次训练作业的输出路径都包含实验标识比如obs://my-bucket/output/bert-128-5e5-20250110/。同时在训练作业的描述字段里写清楚这次实验用了什么数据集、什么参数、目标是什么。这样后续回看时每个模型的历史和调参意图都一目了然。如果要让模型之间做对比可以在AI应用管理里给每个版本加上标签比如baseline-v1dropout-0.3-v2部署时直接选择对应版本即可。7.3 把ModelArts能力拆解到自己的项目里最后补充一个思路ModelArts不只是单独使用的平台它可以和很多外部工具联动。比如数据清洗放在更熟悉的数据处理脚本里做处理完成后把干净的数据集传到OBS再触发ModelArts训练。CI/CD系统可以在达到某个条件后比如代码合并后自动调用ModelArts的API创建训练作业实现持续训练。训练完成并部署服务后业务系统通过API网关统一调用在线服务这样做权限管理、限流、日志审计都更方便。这些能力不需要一次性掌握可以按需逐步实践。学习阶段先跑通一个最小闭环后面遇到的问题会逼你不断扩展认知。我自己最近想做的事情是给这套流程接入一个简单的数据版本管理机制把每次训练所用数据的哈希值也记下来这样模型复现会更靠谱——虽然在ModelArts里数据本身存在OBS但数据的版本追溯还是需要自己在项目层面做设计。这属于工程习惯的范畴但能大大减少后续交接和回溯时的混乱。
延伸阅读

更多相关文章

2026/10/11 8:07:49

风电随机性动态经济调度的Matlab建模与求解实践

做风电随机性动态经济调度这个课题,一开始我是被"随机性"三个字折腾得够呛。单看"动态经济调度",无非是多时段滚动优化机组出力,把煤耗曲线、爬坡约束、功率平衡一股脑塞进求解器。但一旦把风电扯进来,问题性…

2026/10/11 9:02:52

镀锌桥架采购常见问题解答 新明电气 大厂直供 降低采购成本

镀锌桥架作为电缆敷设体系中的基础支撑构件,凭借热镀锌工艺带来的防锈防腐能力与较高的经济性,长期占据工业与基建项目线缆配套市场的重要位置。然而在实际采购过程中,不少项目采购人员由于对产品工艺、规格体系、供货周期缺乏系统了解&#…

2026/10/11 9:02:52

自研GEMM内核DeepGEMM:从性能剖析到算子级调优实战

我在做推理优化的时候,用性能分析工具看了下整个计算图,发现一个很扎心的事实:一个普通的矩阵乘法算子,就能吃掉单次迭代接近四成的时间。当时第一反应是换参数、调库、换格式,折腾一圈之后发现,通用数学库…

2026/10/11 9:02:52

DeepGEMM:GPU矩阵乘法算子级极致优化实战指南

1. 项目概述:DeepGEMM不是新模型,而是GPU计算底层的“肌肉强化术”如果你最近在高性能计算、AI训练加速或CUDA开发相关的技术社区里刷到“DeepGEMM”这个词,第一反应可能是——又一个大模型?还是某家新出的推理框架?其…

2026/10/11 9:02:52

基于YOLOv8的植物健康状态二分类系统实战

1. 项目概述:为什么一个“健康/患病”二分类检测系统值得花两周时间重做三遍?去年在某高校实验室带一个植物图像分析的模拟项目X时,我第一次接到需求:“用YOLOv8做个植物病害识别”。当时想得很简单——网上搜个预训练权重、换掉最…

2026/10/11 8:57:52

什么是 SRC?手把手教新手在 SRC 提交漏洞,拿第一个证书 / 奖金

什么是 SRC?手把手教新手在 SRC 提交漏洞,拿第一个证书 / 奖金 免责声明:本文仅用于网络安全知识科普、白帽漏洞挖掘合规学习。**所有漏洞挖掘操作,仅能在厂商 SRC 明确授权范围内开展测试,严禁对未授权网站、系统、AP…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑