run_stage7.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Stage 7 独立运行脚本
  5. 从 Stage 6 结果开始,进行深度解构分析
  6. 支持指定 feature 和数量限制
  7. """
  8. import os
  9. import json
  10. import logging
  11. import argparse
  12. import webbrowser
  13. from pathlib import Path
  14. from stage7_analyzer import Stage7DeconstructionAnalyzer
  15. from stage8_similarity_analyzer import Stage8SimilarityAnalyzer
  16. import visualize_stage78_with_deconstruction
  17. # 配置日志
  18. logging.basicConfig(
  19. level=logging.INFO,
  20. format='%(asctime)s - %(levelname)s - %(message)s',
  21. datefmt='%Y-%m-%d %H:%M:%S',
  22. handlers=[
  23. logging.FileHandler('stage7_standalone.log', encoding='utf-8'),
  24. logging.StreamHandler()
  25. ]
  26. )
  27. logger = logging.getLogger(__name__)
  28. def main():
  29. """主函数"""
  30. parser = argparse.ArgumentParser(
  31. description='Stage 7 深度解构分析(独立运行,支持流水线执行)',
  32. formatter_class=argparse.RawDescriptionHelpFormatter,
  33. epilog='''
  34. 基础用法示例:
  35. # 只处理"墨镜"特征的前10个高分帖子
  36. python3 run_stage7.py --feature "墨镜" --max-notes 10
  37. # 处理"墨镜"和"耳环"两个特征,每个最多5个
  38. python3 run_stage7.py --feature "墨镜" "耳环" --max-notes 5
  39. # 处理所有特征,按时间排序,前20个
  40. python3 run_stage7.py --sort-by time --max-notes 20
  41. # 只处理"墨镜",按互动量排序,跳过前3个
  42. python3 run_stage7.py --feature "墨镜" --sort-by engagement --skip 3
  43. # 降低分数阈值,处理更多帖子
  44. python3 run_stage7.py --feature "墨镜" --min-score 6.0 --max-notes 30
  45. 流水线执行示例(推荐):
  46. # 完整流水线: Stage 7 → Stage 8 → 可视化 → 自动打开浏览器
  47. python3 run_stage7.py --feature "墨镜" --max-notes 10 --run-stage8 --visualize
  48. # Stage 7 → Stage 8(不生成可视化)
  49. python3 run_stage7.py --feature "墨镜" --max-notes 10 --run-stage8
  50. # Stage 7 → 可视化(跳过 Stage 8)
  51. python3 run_stage7.py --feature "墨镜" --max-notes 10 --visualize
  52. # 完整流水线,不自动打开浏览器
  53. python3 run_stage7.py --feature "墨镜" --run-stage8 --visualize --no-open
  54. # 自定义 Stage 8 相似度权重
  55. python3 run_stage7.py --feature "墨镜" --run-stage8 --visualize \\
  56. --stage8-weight-embedding 0.7 --stage8-weight-semantic 0.3
  57. # 过滤低相似度特征
  58. python3 run_stage7.py --feature "墨镜" --run-stage8 --visualize \\
  59. --stage8-min-similarity 0.3
  60. 配置文件示例:
  61. # 使用配置文件(支持所有参数)
  62. python3 run_stage7.py --config pipeline_config.json
  63. # 配置文件示例内容(pipeline_config.json):
  64. {
  65. "feature": ["墨镜"],
  66. "max_notes": 10,
  67. "timeout": 600,
  68. "run_stage8": true,
  69. "visualize": true,
  70. "stage8_weight_embedding": 0.5,
  71. "stage8_weight_semantic": 0.5
  72. }
  73. '''
  74. )
  75. # 输入输出配置
  76. parser.add_argument(
  77. '--input',
  78. default='output_v2/stage6_with_evaluations.json',
  79. help='Stage 6 结果文件路径(默认: output_v2/stage6_with_evaluations.json)'
  80. )
  81. parser.add_argument(
  82. '--output',
  83. default='output_v2/stage7_with_deconstruction.json',
  84. help='Stage 7 输出文件路径(默认: output_v2/stage7_with_deconstruction.json)'
  85. )
  86. # Feature 过滤(新增)
  87. parser.add_argument(
  88. '--feature',
  89. nargs='+',
  90. default=None,
  91. help='指定要处理的原始特征名称(可指定多个),如: --feature "墨镜" "耳环"。不指定则处理所有特征'
  92. )
  93. # 过滤参数
  94. parser.add_argument(
  95. '--min-score',
  96. type=float,
  97. default=8.0,
  98. help='最低分数阈值,只处理 >= 此分数的帖子(默认: 8.0)'
  99. )
  100. parser.add_argument(
  101. '--skip',
  102. type=int,
  103. default=0,
  104. help='跳过前 N 个帖子(默认: 0)'
  105. )
  106. parser.add_argument(
  107. '--max-notes',
  108. type=int,
  109. default=None,
  110. help='最多处理多少个帖子(默认: None 不限制)'
  111. )
  112. parser.add_argument(
  113. '--sort-by',
  114. choices=['score', 'time', 'engagement'],
  115. default='score',
  116. help='排序方式: score(评分), time(时间), engagement(互动量)(默认: score)'
  117. )
  118. # API 配置
  119. parser.add_argument(
  120. '--api-url',
  121. default='http://192.168.245.150:7000/what/analysis/single',
  122. help='解构 API 地址(默认: http://192.168.245.150:7000/what/analysis/single)'
  123. )
  124. parser.add_argument(
  125. '--timeout',
  126. type=int,
  127. default=600,
  128. help='API 超时时间(秒)(默认: 600,即10分钟)'
  129. )
  130. parser.add_argument(
  131. '--max-retries',
  132. type=int,
  133. default=3,
  134. help='API 最大重试次数(默认: 3)'
  135. )
  136. # 并发配置
  137. parser.add_argument(
  138. '--max-workers',
  139. type=int,
  140. default=5,
  141. help='并发处理数(默认: 5)'
  142. )
  143. # 从配置文件加载
  144. parser.add_argument(
  145. '--config',
  146. default=None,
  147. help='从 JSON 配置文件加载参数'
  148. )
  149. # 流水线控制参数
  150. parser.add_argument(
  151. '--run-stage8',
  152. action='store_true',
  153. help='Stage 7 完成后自动运行 Stage 8'
  154. )
  155. parser.add_argument(
  156. '--visualize',
  157. action='store_true',
  158. help='生成可视化结果'
  159. )
  160. parser.add_argument(
  161. '--open-browser',
  162. action='store_true',
  163. default=True,
  164. help='自动在浏览器中打开可视化结果(默认: True)'
  165. )
  166. parser.add_argument(
  167. '--no-open',
  168. action='store_true',
  169. help='禁用自动打开浏览器'
  170. )
  171. # Stage 8 输出配置
  172. parser.add_argument(
  173. '--stage8-output',
  174. default='output_v2/stage8_similarity_scores.json',
  175. help='Stage 8 输出文件路径(默认: output_v2/stage8_similarity_scores.json)'
  176. )
  177. # Stage 8 相似度配置
  178. parser.add_argument(
  179. '--stage8-weight-embedding',
  180. type=float,
  181. default=0.5,
  182. help='Stage 8 向量模型权重(默认: 0.5)'
  183. )
  184. parser.add_argument(
  185. '--stage8-weight-semantic',
  186. type=float,
  187. default=0.5,
  188. help='Stage 8 LLM 模型权重(默认: 0.5)'
  189. )
  190. parser.add_argument(
  191. '--stage8-min-similarity',
  192. type=float,
  193. default=0.0,
  194. help='Stage 8 最小相似度阈值(默认: 0.0)'
  195. )
  196. parser.add_argument(
  197. '--stage8-max-workers',
  198. type=int,
  199. default=5,
  200. help='Stage 8 最大并发数(默认: 5)'
  201. )
  202. # 可视化输出配置
  203. parser.add_argument(
  204. '--viz-output',
  205. default=None,
  206. help='可视化输出目录(默认: visualization/)'
  207. )
  208. args = parser.parse_args()
  209. # 如果提供了配置文件,加载配置
  210. if args.config:
  211. logger.info(f"从配置文件加载参数: {args.config}")
  212. with open(args.config, 'r', encoding='utf-8') as f:
  213. config = json.load(f)
  214. # 配置文件中的参数会覆盖命令行参数
  215. for key, value in config.items():
  216. setattr(args, key.replace('-', '_'), value)
  217. # 检查输入文件是否存在
  218. if not os.path.exists(args.input):
  219. logger.error(f"输入文件不存在: {args.input}")
  220. return
  221. # 加载 Stage 6 结果
  222. logger.info(f"加载 Stage 6 结果: {args.input}")
  223. with open(args.input, 'r', encoding='utf-8') as f:
  224. stage6_results = json.load(f)
  225. # 打印配置
  226. logger.info("=" * 60)
  227. logger.info("运行配置:")
  228. logger.info(f" 输入文件: {args.input}")
  229. logger.info(f" 输出文件: {args.output}")
  230. if args.feature:
  231. logger.info(f" 指定特征: {', '.join(args.feature)}")
  232. else:
  233. logger.info(f" 指定特征: 全部")
  234. logger.info(f" API 地址: {args.api_url}")
  235. logger.info(f" 最低分数阈值: {args.min_score}")
  236. logger.info(f" 跳过前 N 个: {args.skip}")
  237. logger.info(f" 最多处理数: {args.max_notes if args.max_notes else '不限制'}")
  238. logger.info(f" 排序方式: {args.sort_by}")
  239. logger.info(f" 并发数: {args.max_workers}")
  240. logger.info(f" API 超时: {args.timeout}秒")
  241. logger.info(f" 最大重试: {args.max_retries}次")
  242. logger.info("=" * 60)
  243. # 创建分析器
  244. analyzer = Stage7DeconstructionAnalyzer(
  245. api_url=args.api_url,
  246. max_workers=args.max_workers,
  247. max_notes=args.max_notes,
  248. min_score=args.min_score,
  249. skip_count=args.skip,
  250. sort_by=args.sort_by,
  251. timeout=args.timeout,
  252. max_retries=args.max_retries,
  253. output_dir=os.path.dirname(args.output) or 'output_v2',
  254. target_features=args.feature # 传递 feature 过滤参数
  255. )
  256. # 运行分析
  257. try:
  258. stage7_results = analyzer.run(
  259. stage6_results=stage6_results,
  260. output_path=args.output
  261. )
  262. # 打印结果摘要
  263. logger.info("\n" + "=" * 60)
  264. logger.info("Stage 7 执行完成!")
  265. logger.info(f" 总匹配帖子数: {stage7_results['metadata']['total_matched_notes']}")
  266. logger.info(f" 实际处理数: {stage7_results['metadata']['processed_notes']}")
  267. logger.info(f" 成功: {stage7_results['metadata']['success_count']}")
  268. logger.info(f" 失败: {stage7_results['metadata']['failed_count']}")
  269. logger.info(f" 总耗时: {stage7_results['metadata']['processing_time_seconds']}秒")
  270. logger.info(f" 结果已保存: {args.output}")
  271. logger.info("=" * 60)
  272. # Stage 8: 相似度分析
  273. stage8_results = None
  274. if args.run_stage8:
  275. logger.info("\n" + "=" * 60)
  276. logger.info("开始执行 Stage 8 相似度分析...")
  277. logger.info("=" * 60)
  278. try:
  279. # 创建 Stage 8 分析器
  280. stage8_analyzer = Stage8SimilarityAnalyzer(
  281. weight_embedding=args.stage8_weight_embedding,
  282. weight_semantic=args.stage8_weight_semantic,
  283. max_workers=args.stage8_max_workers,
  284. min_similarity=args.stage8_min_similarity,
  285. target_features=args.feature
  286. )
  287. # 运行 Stage 8 分析
  288. stage8_results = stage8_analyzer.run(
  289. stage7_results=stage7_results,
  290. output_path=args.stage8_output
  291. )
  292. # 打印 Stage 8 结果摘要
  293. logger.info("\n" + "=" * 60)
  294. logger.info("Stage 8 执行完成!")
  295. metadata = stage8_results['metadata']
  296. overall_stats = metadata['overall_statistics']
  297. logger.info(f" 处理帖子数: {overall_stats['total_notes']}")
  298. logger.info(f" 提取特征总数: {overall_stats['total_features_extracted']}")
  299. logger.info(f" 平均特征数/帖子: {overall_stats['avg_features_per_note']:.2f}")
  300. logger.info(f" 平均最高相似度: {overall_stats['avg_max_similarity']:.3f}")
  301. logger.info(f" 包含高相似度特征的帖子: {overall_stats['notes_with_high_similarity']}")
  302. logger.info(f" 总耗时: {metadata['processing_time_seconds']:.2f}秒")
  303. logger.info(f" 结果已保存: {args.stage8_output}")
  304. logger.info("=" * 60)
  305. # 打印 Top 5 高相似度特征示例
  306. if stage8_results['results']:
  307. logger.info("\nTop 5 高相似度特征示例:")
  308. all_features = []
  309. for result in stage8_results['results']:
  310. for feat in result['deconstructed_features'][:5]:
  311. all_features.append({
  312. 'note_id': result['note_id'],
  313. 'feature_name': feat['feature_name'],
  314. 'dimension': feat['dimension'],
  315. 'similarity': feat['similarity_score']
  316. })
  317. # 按相似度排序,取 Top 5
  318. all_features.sort(key=lambda x: x['similarity'], reverse=True)
  319. for i, feat in enumerate(all_features[:5], 1):
  320. logger.info(f" {i}. [{feat['note_id'][:12]}...] "
  321. f"{feat['feature_name']} ({feat['dimension']}) "
  322. f"- 相似度: {feat['similarity']:.3f}")
  323. except Exception as e:
  324. logger.error(f"Stage 8 执行失败: {e}", exc_info=True)
  325. logger.warning("继续执行后续步骤...")
  326. # 可视化生成
  327. viz_path = None
  328. if args.visualize:
  329. logger.info("\n" + "=" * 60)
  330. logger.info("开始生成可视化结果...")
  331. logger.info("=" * 60)
  332. try:
  333. # 准备可视化所需的数据文件路径
  334. viz_args = [
  335. '--stage6', args.input,
  336. '--stage7', args.output
  337. ]
  338. # 如果有 Stage 8 结果,添加到参数中
  339. if stage8_results and args.stage8_output:
  340. viz_args.extend(['--stage8', args.stage8_output])
  341. # 如果指定了可视化输出目录
  342. if args.viz_output:
  343. viz_args.extend(['--output-dir', args.viz_output])
  344. # 调用可视化模块
  345. import sys
  346. original_argv = sys.argv
  347. try:
  348. sys.argv = ['visualize_stage78_with_deconstruction.py'] + viz_args
  349. viz_path = visualize_stage78_with_deconstruction.main()
  350. finally:
  351. sys.argv = original_argv
  352. if viz_path:
  353. logger.info("\n" + "=" * 60)
  354. logger.info("可视化生成完成!")
  355. logger.info(f" 可视化文件: {viz_path}")
  356. logger.info("=" * 60)
  357. # 自动打开浏览器
  358. if args.open_browser and not args.no_open:
  359. logger.info("\n正在打开浏览器...")
  360. try:
  361. # 使用 Path.as_uri() 来正确处理包含中文和特殊字符的路径
  362. file_url = Path(viz_path).resolve().as_uri()
  363. webbrowser.open(file_url)
  364. logger.info("浏览器已打开")
  365. except Exception as e:
  366. logger.warning(f"无法自动打开浏览器: {e}")
  367. logger.info(f"请手动打开: {os.path.abspath(viz_path)}")
  368. else:
  369. logger.warning("可视化生成返回了空路径")
  370. except Exception as e:
  371. logger.error(f"可视化生成失败: {e}", exc_info=True)
  372. logger.warning("跳过可视化步骤")
  373. # 流水线执行完成
  374. logger.info("\n" + "=" * 60)
  375. logger.info("流水线执行完成!")
  376. logger.info("=" * 60)
  377. except Exception as e:
  378. logger.error(f"执行失败: {e}", exc_info=True)
  379. raise
  380. if __name__ == '__main__':
  381. main()