pai_flow_operator_v3.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  1. # -*- coding: utf-8 -*-
  2. import functools
  3. import os
  4. import re
  5. import sys
  6. import time
  7. import json
  8. import pandas as pd
  9. from alibabacloud_paistudio20210202.client import Client as PaiStudio20210202Client
  10. from alibabacloud_tea_openapi import models as open_api_models
  11. from alibabacloud_paistudio20210202 import models as pai_studio_20210202_models
  12. from alibabacloud_tea_util import models as util_models
  13. from alibabacloud_tea_util.client import Client as UtilClient
  14. from alibabacloud_eas20210701.client import Client as eas20210701Client
  15. from alibabacloud_paiflow20210202 import models as paiflow_20210202_models
  16. from alibabacloud_paiflow20210202.client import Client as PAIFlow20210202Client
  17. from datetime import datetime, timedelta
  18. from odps import ODPS
  19. from ad_monitor_util import _monitor
  20. import alibabacloud_oss_v2 as oss
  21. target_names = {
  22. '样本shuffle',
  23. '模型训练-样本shufle',
  24. '模型训练-自定义',
  25. '模型导出-2',
  26. '更新EAS服务(Beta)-1',
  27. '虚拟起始节点',
  28. '二分类评估-扫码1',
  29. '二分类评估-加微1',
  30. '二分类评估-转化1',
  31. '二分类评估-扫码2',
  32. '二分类评估-加微2',
  33. '二分类评估-转化2',
  34. '预测结果对比-扫码',
  35. '预测结果对比-加微',
  36. '预测结果对比-转化',
  37. }
  38. EXPERIMENT_ID = "draft-pcxrc35xr0mw9zgmg5"
  39. ACCESS_KEY_ID = "LTAI5tFGqgC8f3mh1fRCrAEy"
  40. ACCESS_KEY_SECRET = "XhOjK9XmTYRhVAtf6yii4s4kZwWzvV"
  41. MAX_RETRIES = 3
  42. def retry(func):
  43. @functools.wraps(func)
  44. def wrapper(*args, **kwargs):
  45. retries = 0
  46. while retries < MAX_RETRIES:
  47. try:
  48. result = func(*args, **kwargs)
  49. if result is not False:
  50. return result
  51. except Exception as e:
  52. print(f"函数 {func.__name__} 执行时发生异常: {e},重试第 {retries + 1} 次")
  53. retries += 1
  54. print(f"函数 {func.__name__} 重试 {MAX_RETRIES} 次后仍失败。")
  55. return False
  56. return wrapper
  57. def get_odps_instance(project):
  58. odps = ODPS(
  59. access_id=ACCESS_KEY_ID,
  60. secret_access_key=ACCESS_KEY_SECRET,
  61. project=project,
  62. endpoint='http://service.cn.maxcompute.aliyun.com/api',
  63. )
  64. return odps
  65. def get_data_from_odps(project, table, num):
  66. odps = get_odps_instance(project)
  67. try:
  68. # 要查询的 SQL 语句
  69. sql = f'select * from {table} limit {num}'
  70. # 执行 SQL 查询
  71. with odps.execute_sql(sql).open_reader() as reader:
  72. df = reader.to_pandas()
  73. # 查询数量小于目标数量时 返回空
  74. if len(df) < num:
  75. return None
  76. return df
  77. except Exception as e:
  78. print(f"发生错误: {e}")
  79. def get_dict_from_odps(project, table):
  80. odps = get_odps_instance(project)
  81. try:
  82. # 要查询的 SQL 语句
  83. sql = f'select * from {table}'
  84. # 执行 SQL 查询
  85. with odps.execute_sql(sql).open_reader() as reader:
  86. data = {}
  87. for record in reader:
  88. record_list = list(record)
  89. key = record_list[0][1]
  90. value = record_list[1][1]
  91. data[key] = value
  92. return data
  93. except Exception as e:
  94. print(f"发生错误: {e}")
  95. def get_dates_between(start_date_str, end_date_str):
  96. start_date = datetime.strptime(start_date_str, '%Y%m%d')
  97. end_date = datetime.strptime(end_date_str, '%Y%m%d')
  98. dates = []
  99. current_date = start_date
  100. while current_date <= end_date:
  101. dates.append(current_date.strftime('%Y%m%d'))
  102. current_date += timedelta(days=1)
  103. return dates
  104. def read_file_to_list():
  105. try:
  106. current_dir = os.getcwd()
  107. file_path = os.path.join(current_dir, 'ad', 'holidays.txt')
  108. with open(file_path, 'r', encoding='utf-8') as file:
  109. content = file.read()
  110. return content.split('\n')
  111. except FileNotFoundError:
  112. raise Exception(f"错误:未找到 {file_path} 文件。")
  113. except Exception as e:
  114. raise Exception(f"错误:发生了一个未知错误: {e}")
  115. return []
  116. def get_previous_days_date(days):
  117. current_date = datetime.now()
  118. previous_date = current_date - timedelta(days=days)
  119. return previous_date.strftime('%Y%m%d')
  120. def remove_elements(lst1, lst2):
  121. return [element for element in lst1 if element not in lst2]
  122. def process_list(lst, append_str):
  123. # 给列表中每个元素拼接相同的字符串
  124. appended_list = [append_str + element for element in lst]
  125. # 将拼接后的列表元素用逗号拼接成一个字符串
  126. result_str = ','.join(appended_list)
  127. return result_str
  128. def get_train_data_list(date_begin):
  129. end_date = get_previous_days_date(2)
  130. date_list = get_dates_between(date_begin, end_date)
  131. filter_date_list = read_file_to_list()
  132. date_list = remove_elements(date_list, filter_date_list)
  133. return date_list
  134. # 只替换第一次匹配的'where dt in ()'中的日期
  135. def update_data_date_range(old_str, date_begin='20250605'):
  136. date_list = get_train_data_list(date_begin)
  137. train_list = ["'" + item + "'" for item in date_list]
  138. result = ','.join(train_list)
  139. start_index = old_str.find('where dt in (')
  140. if start_index != -1:
  141. equal_sign_index = start_index + len('where dt in (')
  142. # 找到下一个双引号的位置
  143. next_quote_index = old_str.find(')', equal_sign_index)
  144. if next_quote_index != -1:
  145. # 进行替换
  146. new_value = old_str[:equal_sign_index] + result + old_str[next_quote_index:]
  147. return new_value
  148. return None
  149. def compare_timestamp_with_today_start(time_str):
  150. # 解析时间字符串为 datetime 对象
  151. time_obj = datetime.fromisoformat(time_str)
  152. # 将其转换为时间戳
  153. target_timestamp = time_obj.timestamp()
  154. # 获取今天开始的时间
  155. today_start = datetime.combine(datetime.now().date(), datetime.min.time())
  156. # 将今天开始时间转换为时间戳
  157. today_start_timestamp = today_start.timestamp()
  158. return target_timestamp > today_start_timestamp
  159. def update_train_table(old_str, table):
  160. address = 'odps://pai_algo/tables/'
  161. train_table = address + table
  162. start_index = old_str.find('-Dtrain_tables="')
  163. if start_index != -1:
  164. # 确定等号的位置
  165. equal_sign_index = start_index + len('-Dtrain_tables="')
  166. # 找到下一个双引号的位置
  167. next_quote_index = old_str.find('"', equal_sign_index)
  168. if next_quote_index != -1:
  169. # 进行替换
  170. new_value = old_str[:equal_sign_index] + train_table + old_str[next_quote_index:]
  171. return new_value
  172. return None
  173. class PAIClient:
  174. def __init__(self):
  175. pass
  176. @staticmethod
  177. def create_client() -> PaiStudio20210202Client:
  178. """
  179. 使用AK&SK初始化账号Client
  180. @return: Client
  181. @throws Exception
  182. """
  183. # 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
  184. # 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html。
  185. config = open_api_models.Config(
  186. access_key_id=ACCESS_KEY_ID,
  187. access_key_secret=ACCESS_KEY_SECRET
  188. )
  189. # Endpoint 请参考 https://api.aliyun.com/product/PaiStudio
  190. config.endpoint = f'pai.cn-hangzhou.aliyuncs.com'
  191. return PaiStudio20210202Client(config)
  192. @staticmethod
  193. def create_eas_client() -> eas20210701Client:
  194. """
  195. 使用AK&SK初始化账号Client
  196. @return: Client
  197. @throws Exception
  198. """
  199. # 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
  200. # 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html。
  201. config = open_api_models.Config(
  202. access_key_id=ACCESS_KEY_ID,
  203. access_key_secret=ACCESS_KEY_SECRET
  204. )
  205. # Endpoint 请参考 https://api.aliyun.com/product/PaiStudio
  206. config.endpoint = f'pai-eas.cn-hangzhou.aliyuncs.com'
  207. return eas20210701Client(config)
  208. @staticmethod
  209. def create_flow_client() -> PAIFlow20210202Client:
  210. """
  211. 使用AK&SK初始化账号Client
  212. @return: Client
  213. @throws Exception
  214. """
  215. # 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
  216. # 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html。
  217. config = open_api_models.Config(
  218. # 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。,
  219. access_key_id=ACCESS_KEY_ID,
  220. # 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。,
  221. access_key_secret=ACCESS_KEY_SECRET
  222. )
  223. # Endpoint 请参考 https://api.aliyun.com/product/PAIFlow
  224. config.endpoint = f'paiflow.cn-hangzhou.aliyuncs.com'
  225. return PAIFlow20210202Client(config)
  226. @staticmethod
  227. def get_work_flow_draft_list(workspace_id: str):
  228. client = PAIClient.create_client()
  229. list_experiments_request = pai_studio_20210202_models.ListExperimentsRequest(
  230. workspace_id=workspace_id
  231. )
  232. runtime = util_models.RuntimeOptions()
  233. headers = {}
  234. try:
  235. resp = client.list_experiments_with_options(list_experiments_request, headers, runtime)
  236. return resp.body.to_map()
  237. except Exception as error:
  238. raise Exception(f"get_work_flow_draft_list error {error}")
  239. @staticmethod
  240. def get_work_flow_draft(experiment_id: str):
  241. client = PAIClient.create_client()
  242. runtime = util_models.RuntimeOptions()
  243. headers = {}
  244. try:
  245. # 复制代码运行请自行打印 API 的返回值
  246. resp = client.get_experiment_with_options(experiment_id, headers, runtime)
  247. return resp.body.to_map()
  248. except Exception as error:
  249. raise Exception(f"get_work_flow_draft error {error}")
  250. @staticmethod
  251. def get_describe_service(service_name: str):
  252. client = PAIClient.create_eas_client()
  253. runtime = util_models.RuntimeOptions()
  254. headers = {}
  255. try:
  256. # 复制代码运行请自行打印 API 的返回值
  257. resp = client.describe_service_with_options('cn-hangzhou', service_name, headers, runtime)
  258. return resp.body.to_map()
  259. except Exception as error:
  260. raise Exception(f"get_describe_service error {error}")
  261. @staticmethod
  262. def update_experiment_content(experiment_id: str, content: str, version: int):
  263. client = PAIClient.create_client()
  264. update_experiment_content_request = pai_studio_20210202_models.UpdateExperimentContentRequest(content=content,
  265. version=version)
  266. runtime = util_models.RuntimeOptions()
  267. headers = {}
  268. try:
  269. # 复制代码运行请自行打印 API 的返回值
  270. resp = client.update_experiment_content_with_options(experiment_id, update_experiment_content_request,
  271. headers, runtime)
  272. print(resp.body.to_map())
  273. except Exception as error:
  274. raise Exception(f"update_experiment_content error {error}")
  275. @staticmethod
  276. def create_job(experiment_id: str, node_id: str, execute_type: str):
  277. client = PAIClient.create_client()
  278. create_job_request = pai_studio_20210202_models.CreateJobRequest()
  279. create_job_request.experiment_id = experiment_id
  280. create_job_request.node_id = node_id
  281. create_job_request.execute_type = execute_type
  282. runtime = util_models.RuntimeOptions()
  283. headers = {}
  284. try:
  285. # 复制代码运行请自行打印 API 的返回值
  286. resp = client.create_job_with_options(create_job_request, headers, runtime)
  287. return resp.body.to_map()
  288. except Exception as error:
  289. raise Exception(f"create_job error {error}")
  290. @staticmethod
  291. def get_jobs_list(experiment_id: str, order='DESC'):
  292. client = PAIClient.create_client()
  293. list_jobs_request = pai_studio_20210202_models.ListJobsRequest(
  294. experiment_id=experiment_id,
  295. order=order
  296. )
  297. runtime = util_models.RuntimeOptions()
  298. headers = {}
  299. try:
  300. # 复制代码运行请自行打印 API 的返回值
  301. resp = client.list_jobs_with_options(list_jobs_request, headers, runtime)
  302. return resp.body.to_map()
  303. except Exception as error:
  304. raise Exception(f"get_jobs_list error {error}")
  305. @staticmethod
  306. def get_job_detail(job_id: str, verbose=False):
  307. client = PAIClient.create_client()
  308. get_job_request = pai_studio_20210202_models.GetJobRequest(
  309. verbose=verbose
  310. )
  311. runtime = util_models.RuntimeOptions()
  312. headers = {}
  313. try:
  314. # 复制代码运行请自行打印 API 的返回值
  315. resp = client.get_job_with_options(job_id, get_job_request, headers, runtime)
  316. return resp.body.to_map()
  317. except Exception as error:
  318. # 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
  319. # 错误 message
  320. print(error.message)
  321. # 诊断地址
  322. print(error.data.get("Recommend"))
  323. UtilClient.assert_as_string(error.message)
  324. @staticmethod
  325. def get_flow_out_put(pipeline_run_id: str, node_id: str, depth: int):
  326. client = PAIClient.create_flow_client()
  327. runtime = util_models.RuntimeOptions()
  328. headers = {}
  329. page_number = 1
  330. page_size = 100
  331. all_outputs = []
  332. total_count = None
  333. request_id = None
  334. try:
  335. while True:
  336. list_pipeline_run_node_outputs_request = paiflow_20210202_models.ListPipelineRunNodeOutputsRequest(
  337. depth=depth,
  338. page_number=page_number,
  339. page_size=page_size,
  340. )
  341. resp = client.list_pipeline_run_node_outputs_with_options(
  342. pipeline_run_id, node_id,
  343. list_pipeline_run_node_outputs_request, headers, runtime
  344. )
  345. body = resp.body.to_map()
  346. request_id = body.get('RequestId', request_id)
  347. total_count = body.get('TotalCount', total_count)
  348. outputs = body.get('Outputs') or []
  349. all_outputs.extend(outputs)
  350. if not outputs:
  351. break
  352. if total_count is not None and len(all_outputs) >= int(total_count):
  353. break
  354. if len(outputs) < page_size:
  355. break
  356. page_number += 1
  357. return {
  358. 'Outputs': all_outputs,
  359. 'TotalCount': total_count if total_count is not None else len(all_outputs),
  360. 'RequestId': request_id,
  361. }
  362. except Exception as error:
  363. # 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
  364. # 错误 message
  365. print(error.message)
  366. # 诊断地址
  367. print(error.data.get("Recommend"))
  368. UtilClient.assert_as_string(error.message)
  369. def extract_date_yyyymmdd(input_string):
  370. pattern = r'\d{8}'
  371. matches = re.findall(pattern, input_string)
  372. if matches:
  373. return matches[0]
  374. return None
  375. def get_online_model_config(service_name: str):
  376. model_config = {}
  377. model_detail = PAIClient.get_describe_service(service_name)
  378. service_config_str = model_detail['ServiceConfig']
  379. service_config = json.loads(service_config_str)
  380. model_path = service_config['model_path']
  381. model_config['model_path'] = model_path
  382. online_date = extract_date_yyyymmdd(model_path)
  383. model_config['online_date'] = online_date
  384. return model_config
  385. def update_shuffle_flow(table):
  386. draft = PAIClient.get_work_flow_draft(EXPERIMENT_ID)
  387. print(json.dumps(draft, ensure_ascii=False))
  388. content = draft['Content']
  389. version = draft['Version']
  390. content_json = json.loads(content)
  391. nodes = content_json.get('nodes')
  392. for node in nodes:
  393. name = node['name']
  394. if name == '模型训练-样本shufle':
  395. properties = node['properties']
  396. for property in properties:
  397. if property['name'] == 'sql':
  398. value = property['value']
  399. new_value = update_train_table(value, table)
  400. if new_value is None:
  401. print("error")
  402. property['value'] = new_value
  403. new_content = json.dumps(content_json, ensure_ascii=False)
  404. PAIClient.update_experiment_content(EXPERIMENT_ID, new_content, version)
  405. def wait_job_end(job_id: str, check_interval=300):
  406. while True:
  407. job_detail = PAIClient.get_job_detail(job_id)
  408. print(job_detail)
  409. statue = job_detail['Status']
  410. # Initialized: 初始化完成 Starting:开始 WorkflowServiceStarting:准备提交 Running:运行中 ReadyToSchedule:准备运行(前序节点未完成导致)
  411. if (statue == 'Initialized' or statue == 'Starting' or statue == 'WorkflowServiceStarting'
  412. or statue == 'Running' or statue == 'ReadyToSchedule'):
  413. time.sleep(check_interval)
  414. continue
  415. # Failed:运行失败 Terminating:终止中 Terminated:已终止 Unknown:未知 Skipped:跳过(前序节点失败导致) Succeeded:运行成功
  416. if statue == 'Failed' or statue == 'Terminating' or statue == 'Unknown' or statue == 'Skipped' or statue == 'Succeeded':
  417. return job_detail
  418. def get_node_dict():
  419. draft = PAIClient.get_work_flow_draft(EXPERIMENT_ID)
  420. content = draft['Content']
  421. content_json = json.loads(content)
  422. nodes = content_json.get('nodes')
  423. node_dict = {}
  424. for node in nodes:
  425. name = node['name']
  426. # 检查名称是否在目标名称集合中
  427. if name in target_names:
  428. node_dict[name] = node['id']
  429. return node_dict
  430. def get_job_dict():
  431. job_dict = {}
  432. jobs_list = PAIClient.get_jobs_list(EXPERIMENT_ID)
  433. for job in jobs_list['Jobs']:
  434. # 解析时间字符串为 datetime 对象
  435. if not compare_timestamp_with_today_start(job['GmtCreateTime']):
  436. break
  437. job_id = job['JobId']
  438. job_detail = PAIClient.get_job_detail(job_id, verbose=True)
  439. for name in target_names:
  440. if job_detail['Status'] != 'Succeeded':
  441. continue
  442. if name in job_dict:
  443. continue
  444. if name in job_detail['RunInfo']:
  445. job_dict[name] = job_detail['JobId']
  446. return job_dict
  447. @retry
  448. def update_online_flow():
  449. try:
  450. online_model_config = get_online_model_config('ad_rank_dnn_v11_easyrec_v2')
  451. draft = PAIClient.get_work_flow_draft(EXPERIMENT_ID)
  452. print(json.dumps(draft, ensure_ascii=False))
  453. content = draft['Content']
  454. version = draft['Version']
  455. print(content)
  456. content_json = json.loads(content)
  457. nodes = content_json.get('nodes')
  458. global_params = content_json.get('globalParams')
  459. bizdate = get_previous_days_date(1)
  460. for global_param in global_params:
  461. try:
  462. if global_param['name'] == 'bizdate':
  463. global_param['value'] = bizdate
  464. if global_param['name'] == 'online_version_dt':
  465. global_param['value'] = online_model_config['online_date']
  466. if global_param['name'] == 'eval_date':
  467. global_param['value'] = bizdate
  468. if global_param['name'] == 'online_model_path':
  469. global_param['value'] = online_model_config['model_path']
  470. except KeyError:
  471. raise Exception("在处理全局参数时,字典中缺少必要的键")
  472. for node in nodes:
  473. try:
  474. name = node['name']
  475. if name in ('样本shuffle',):
  476. date_begin = get_previous_days_date(90) if name == '样本shuffle' else get_previous_days_date(10)
  477. properties = node['properties']
  478. for property in properties:
  479. if property['name'] == 'sql':
  480. value = property['value']
  481. new_value = update_data_date_range(value, date_begin)
  482. if new_value is None:
  483. print("error")
  484. property['value'] = new_value
  485. except KeyError:
  486. raise Exception("在处理节点属性时,字典中缺少必要的键")
  487. new_content = json.dumps(content_json, ensure_ascii=False)
  488. PAIClient.update_experiment_content(EXPERIMENT_ID, new_content, version)
  489. return True
  490. except json.JSONDecodeError:
  491. raise Exception("JSON 解析错误,可能是草稿内容格式不正确")
  492. except Exception as e:
  493. raise Exception(f"发生未知错误: {e}")
  494. @retry
  495. def shuffle_table():
  496. try:
  497. node_dict = get_node_dict()
  498. train_node_id = node_dict['样本shuffle']
  499. execute_type = 'EXECUTE_FROM_HERE'
  500. validate_res = PAIClient.create_job(EXPERIMENT_ID, train_node_id, execute_type)
  501. validate_job_id = validate_res['JobId']
  502. validate_job_detail = wait_job_end(validate_job_id, 10)
  503. if validate_job_detail['Status'] == 'Succeeded':
  504. return True
  505. return False
  506. except Exception as e:
  507. error_message = f"在执行 shuffle_table 函数时发生异常: {str(e)}"
  508. print(error_message)
  509. raise Exception(error_message)
  510. @retry
  511. def shuffle_train_model():
  512. try:
  513. node_dict = get_node_dict()
  514. job_dict = get_job_dict()
  515. job_id = job_dict['样本shuffle']
  516. validate_job_detail = wait_job_end(job_id)
  517. if validate_job_detail['Status'] == 'Succeeded':
  518. pipeline_run_id = validate_job_detail['RunId']
  519. node_id = validate_job_detail['PaiflowNodeId']
  520. flow_out_put_detail = PAIClient.get_flow_out_put(pipeline_run_id, node_id, 2)
  521. outputs = flow_out_put_detail['Outputs']
  522. table = None
  523. for output in outputs:
  524. if output["Producer"] == node_dict['样本shuffle'] and output["Name"] == "outputTable":
  525. value1 = json.loads(output["Info"]['value'])
  526. table = value1['location']['table']
  527. if table is not None:
  528. update_shuffle_flow(table)
  529. node_dict = get_node_dict()
  530. train_node_id = node_dict['模型训练-样本shufle']
  531. execute_type = 'EXECUTE_ONE'
  532. train_res = PAIClient.create_job(EXPERIMENT_ID, train_node_id, execute_type)
  533. train_job_id = train_res['JobId']
  534. train_job_detail = wait_job_end(train_job_id)
  535. if train_job_detail['Status'] == 'Succeeded':
  536. return True
  537. return False
  538. except Exception as e:
  539. error_message = f"在执行 shuffle_train_model 函数时发生异常: {str(e)}"
  540. print(error_message)
  541. raise Exception(error_message)
  542. @retry
  543. def export_model():
  544. try:
  545. node_dict = get_node_dict()
  546. export_node_id = node_dict['模型导出-2']
  547. execute_type = 'EXECUTE_ONE'
  548. export_res = PAIClient.create_job(EXPERIMENT_ID, export_node_id, execute_type)
  549. export_job_id = export_res['JobId']
  550. export_job_detail = wait_job_end(export_job_id)
  551. if export_job_detail['Status'] == 'Succeeded':
  552. return True
  553. return False
  554. except Exception as e:
  555. error_message = f"在执行 export_model 函数时发生异常: {str(e)}"
  556. print(error_message)
  557. raise Exception(error_message)
  558. def update_online_model():
  559. try:
  560. node_dict = get_node_dict()
  561. train_node_id = node_dict['更新EAS服务(Beta)-1']
  562. execute_type = 'EXECUTE_ONE'
  563. train_res = PAIClient.create_job(EXPERIMENT_ID, train_node_id, execute_type)
  564. train_job_id = train_res['JobId']
  565. train_job_detail = wait_job_end(train_job_id)
  566. if train_job_detail['Status'] == 'Succeeded':
  567. return True
  568. return False
  569. except Exception as e:
  570. error_message = f"在执行 update_online_model 函数时发生异常: {str(e)}"
  571. print(error_message)
  572. raise Exception(error_message)
  573. @retry
  574. def get_validate_model_data():
  575. try:
  576. node_dict = get_node_dict()
  577. train_node_id = node_dict['虚拟起始节点']
  578. execute_type = 'EXECUTE_FROM_HERE'
  579. validate_res = PAIClient.create_job(EXPERIMENT_ID, train_node_id, execute_type)
  580. validate_job_id = validate_res['JobId']
  581. validate_job_detail = wait_job_end(validate_job_id)
  582. if validate_job_detail['Status'] == 'Succeeded':
  583. return True
  584. return False
  585. except Exception as e:
  586. error_message = f"在执行 get_validate_model_data 函数时出现异常: {e}"
  587. print(error_message)
  588. raise Exception(error_message)
  589. def validate_model_data_accuracy():
  590. try:
  591. table_dict = {}
  592. node_dict = get_node_dict()
  593. job_dict = get_job_dict()
  594. if '虚拟起始节点' not in job_dict:
  595. raise Exception(f"未找到今日成功的虚拟起始节点任务,当前 job_dict={job_dict}")
  596. job_id = job_dict['虚拟起始节点']
  597. validate_job_detail = wait_job_end(job_id)
  598. if validate_job_detail['Status'] != 'Succeeded':
  599. raise Exception(f"虚拟起始节点任务未成功,status={validate_job_detail['Status']}")
  600. pipeline_run_id = validate_job_detail['RunId']
  601. node_id = validate_job_detail['PaiflowNodeId']
  602. # 扫码/加微/转化三路评估与对比节点共 5 层
  603. flow_out_put_detail = PAIClient.get_flow_out_put(pipeline_run_id, node_id, 5)
  604. if not flow_out_put_detail:
  605. raise Exception('获取工作流输出失败,flow_out_put_detail 为空')
  606. print(flow_out_put_detail)
  607. outputs = flow_out_put_detail.get('Outputs') or []
  608. metric_names = {
  609. '二分类评估-扫码1', '二分类评估-加微1', '二分类评估-转化1',
  610. '二分类评估-扫码2', '二分类评估-加微2', '二分类评估-转化2',
  611. }
  612. compare_names = {
  613. '预测结果对比-扫码', '预测结果对比-加微', '预测结果对比-转化',
  614. }
  615. required_names = metric_names | compare_names
  616. producer_to_name = {node_dict[name]: name for name in required_names if name in node_dict}
  617. print(f'node_dict keys: {list(node_dict.keys())}')
  618. print(f'producer_to_name: {producer_to_name}')
  619. for output in outputs:
  620. producer = output.get('Producer')
  621. name = producer_to_name.get(producer)
  622. if not name:
  623. continue
  624. output_name = output.get('Name')
  625. if name in metric_names and output_name == 'outputMetricTable':
  626. value = json.loads(output['Info']['value'])
  627. table_dict[name] = value['location']['table']
  628. elif name in compare_names and output_name == 'outputTable':
  629. value = json.loads(output['Info']['value'])
  630. table_dict[name] = value['location']['table']
  631. print(f'table_dict: {table_dict}')
  632. # 兜底:起始节点 depth 仍可能漏表,按各节点自身 Job 再取一次输出
  633. for name in required_names:
  634. if name in table_dict or name not in job_dict or name not in node_dict:
  635. continue
  636. node_job_detail = PAIClient.get_job_detail(job_dict[name])
  637. if node_job_detail.get('Status') != 'Succeeded':
  638. continue
  639. node_outputs = PAIClient.get_flow_out_put(
  640. node_job_detail['RunId'], node_job_detail['PaiflowNodeId'], 1
  641. ).get('Outputs') or []
  642. for output in node_outputs:
  643. if output.get('Producer') != node_dict[name]:
  644. continue
  645. output_name = output.get('Name')
  646. if name in metric_names and output_name == 'outputMetricTable':
  647. value = json.loads(output['Info']['value'])
  648. table_dict[name] = value['location']['table']
  649. elif name in compare_names and output_name == 'outputTable':
  650. value = json.loads(output['Info']['value'])
  651. table_dict[name] = value['location']['table']
  652. print(f'table_dict after fallback: {table_dict}')
  653. required_tables = [
  654. '预测结果对比-扫码', '预测结果对比-加微', '预测结果对比-转化',
  655. '二分类评估-扫码1', '二分类评估-加微1', '二分类评估-转化1',
  656. '二分类评估-扫码2', '二分类评估-加微2', '二分类评估-转化2',
  657. ]
  658. missing_tables = [k for k in required_tables if k not in table_dict]
  659. if missing_tables:
  660. missing_nodes = [k for k in required_tables if k not in node_dict]
  661. raise Exception(
  662. f"未获取到评估产物表: {missing_tables};"
  663. f"工作流中缺失节点: {missing_nodes};"
  664. f"当前 table_dict={table_dict}"
  665. )
  666. num = 10
  667. bizdate = get_previous_days_date(1)
  668. categories = [
  669. ('扫码', '预测结果对比-扫码', '二分类评估-扫码1', '二分类评估-扫码2'),
  670. ('加微', '预测结果对比-加微', '二分类评估-加微1', '二分类评估-加微2'),
  671. ('转化', '预测结果对比-转化', '二分类评估-转化1', '二分类评估-转化2'),
  672. ]
  673. msg = f'DNN广告模型评估{bizdate}'
  674. top10_msg = ''
  675. auc_decline_threshold = 0.0005
  676. auc_check_passed = True
  677. for name, compare_key, new_eval_key, old_eval_key in categories:
  678. df = get_data_from_odps('pai_algo', table_dict[compare_key], num)
  679. if df is None:
  680. raise Exception(f'【{name}】预测结果对比表数据不足 {num} 条: {table_dict[compare_key]}')
  681. old_abs_avg = df['old_error'].abs().sum() / num
  682. new_abs_avg = df['new_error'].abs().sum() / num
  683. new_auc_dict = get_dict_from_odps('pai_algo', table_dict[new_eval_key])
  684. old_auc_dict = get_dict_from_odps('pai_algo', table_dict[old_eval_key])
  685. if not new_auc_dict or not old_auc_dict:
  686. raise Exception(f'【{name}】评估指标表读取失败: new={table_dict[new_eval_key]}, old={table_dict[old_eval_key]}')
  687. new_auc = float(new_auc_dict['AUC'])
  688. old_auc = float(old_auc_dict['AUC'])
  689. if name in ('扫码', '加微') and old_auc - new_auc > auc_decline_threshold:
  690. auc_check_passed = False
  691. msg += (
  692. f'\n\n**{name}**'
  693. f'\n- 老模型AUC: {old_auc:.6f}'
  694. f'\n- 新模型AUC: {new_auc:.6f}'
  695. f'\n- 老模型Top10差异平均值: {old_abs_avg:.6f}'
  696. f'\n- 新模型Top10差异平均值: {new_abs_avg:.6f}'
  697. )
  698. section_msg = f'\n### {name}\n| CID | 老模型相对真实CTCVR的变化 | 新模型相对真实CTCVR的变化 |'
  699. section_msg += '\n| --- | --- | --- |'
  700. for _, row in df.iterrows():
  701. section_msg += (
  702. f"\n| {int(row['cid'])} "
  703. f"| {float(row['old_error']):.6f} "
  704. f"| {float(row['new_error']):.6f} |"
  705. )
  706. top10_msg += section_msg
  707. print(section_msg)
  708. result = auc_check_passed
  709. level = 'info' if result else 'error'
  710. return result, msg, level, top10_msg
  711. except Exception as e:
  712. error_message = f"在执行 validate_model_data_accuracy 函数时出现异常: {str(e)}"
  713. print(error_message)
  714. raise Exception(error_message)
  715. def update_trained_cids_pointer(model_name=None, dt_version=None):
  716. # 如均为空,则从工作流中获取
  717. if not model_name and not dt_version:
  718. draft = PAIClient.get_work_flow_draft(EXPERIMENT_ID)
  719. content = draft['Content']
  720. content_json = json.loads(content)
  721. global_params = content_json.get('globalParams', [])
  722. model_name = None
  723. dt_version = None
  724. for param in global_params:
  725. if param.get('name') == 'model_name':
  726. model_name = param.get('value')
  727. if param.get('name') == 'bizdate':
  728. dt_version = param.get('value')
  729. if not model_name or not dt_version:
  730. raise Exception("globalParams 中未找到 model_name 或 bizdate")
  731. elif not (model_name and dt_version):
  732. # 不允许其中一个为空
  733. raise Exception("model_name 和 dt_version 必须同时提供")
  734. model_version = {}
  735. model_version['modelName'] = f"model_name={model_name}"
  736. model_version['dtVersion'] = f"dt_version={dt_version}"
  737. model_version['timestamp'] = int(time.time())
  738. print(json.dumps(model_version, ensure_ascii=False, indent=4).encode('utf-8'))
  739. bucket_name = "art-recommend"
  740. object_key = "fengzhoutian/pai_model_trained_cids/model_version.json"
  741. oss_config = oss.config.load_default()
  742. oss_config.credentials_provider = oss.credentials.StaticCredentialsProvider(
  743. access_key_id=ACCESS_KEY_ID, access_key_secret=ACCESS_KEY_SECRET
  744. )
  745. oss_config.region = "cn-hangzhou"
  746. client = oss.Client(oss_config)
  747. ret = client.put_object(oss.PutObjectRequest(
  748. bucket=bucket_name,
  749. key=object_key,
  750. body=json.dumps(model_version, ensure_ascii=False, indent=4).encode('utf-8')
  751. ))
  752. print(f'oss put status code: {ret.status_code},'
  753. f' request id: {ret.request_id},'
  754. f' content md5: {ret.content_md5},'
  755. f' etag: {ret.etag},'
  756. f' hash crc64: {ret.hash_crc64},'
  757. f' version id: {ret.version_id},'
  758. f' content: {model_version}'
  759. )
  760. if __name__ == '__main__':
  761. start_time = int(time.time())
  762. functions = [update_online_flow, shuffle_table, shuffle_train_model, export_model, get_validate_model_data]
  763. function_names = [func.__name__ for func in functions]
  764. start_function = None
  765. if len(sys.argv) > 1:
  766. start_function = sys.argv[1]
  767. if start_function not in function_names:
  768. print(f"指定的起始函数 {start_function} 不存在,请选择以下函数之一:{', '.join(function_names)}")
  769. sys.exit(1)
  770. start_index = 0
  771. if start_function:
  772. start_index = function_names.index(start_function)
  773. for func in functions[start_index:]:
  774. if not func():
  775. print(f"{func.__name__} 执行失败,后续函数不再执行。")
  776. step_end_time = int(time.time())
  777. elapsed = step_end_time - start_time
  778. _monitor('error', f"DNN模型更新,{func.__name__} 执行失败,后续函数不再执行,请检查", start_time, elapsed, None)
  779. break
  780. else:
  781. print("所有函数都成功执行,可以继续下一步操作。")
  782. result, msg, level, top10_msg = validate_model_data_accuracy()
  783. if result:
  784. update_online_res = update_online_model()
  785. if update_online_res:
  786. # update_trained_cids_pointer()
  787. print("success")
  788. step_end_time = int(time.time())
  789. elapsed = step_end_time - start_time
  790. print(level, msg, start_time, elapsed, top10_msg)
  791. _monitor(level, msg, start_time, elapsed, top10_msg)