Browse Source

[UPD] 0.0.1

wingedox 8 months ago
parent
commit
c244af684b

+ 25 - 37
bio.py

@@ -1,22 +1,22 @@
 import re
-from collections import defaultdict
-from datetime import datetime
-
+import pandas as pd
 import yaml
 import jieba
 import jieba.posseg as pseg
 import polars as pl
 
+from collections import defaultdict
+from datetime import datetime
 from pathlib import Path
-from data_preparation import demo_file_name, columns, read_ner_result
-from deepseek import ds_ner
-import pandas as pd
-
-import bio
 from scel2text import get_words_from_sogou_cell_dict
 
-building_dict_file_name_sogo = '~/Documents/论文/建筑词汇大全【官方推荐】.scel'
-bio_file_name = '~/Documents/论文/上市公司-建筑工业化-专利摘要数据-样本-BIO.csv'
+from data_preparation import sample_file_name, columns
+from deepseek import ds_ner
+from env import env
+
+ds_sample_ner_file_name = 'ds_sample_ner_result.yml'
+building_dict_file_name_sogo = '建筑词汇大全【官方推荐】.scel'
+bio_file_name = '上市公司-专利摘要数据-筛选-样本-BIO.csv'
 entity_types = {
     "结构部件类": 'COM',
     "材料类": 'MAT',
@@ -37,14 +37,12 @@ class FlowList(list):
     ...
 
 # 自定义处理 hobbies 字段为流格式
-def custom_hobbies_representer(dumper, data):
+def flow_list_representer(dumper, data):
     return dumper.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=True)
 
 # 注册自定义 representer
-yaml.add_representer(FlowList, custom_hobbies_representer)
+yaml.add_representer(FlowList, flow_list_representer)
 
-# "CN200910091292.3"
-all_words = defaultdict(list)
 
 def process(row):
     jieba.load_userdict("word_dict.txt")
@@ -65,9 +63,9 @@ def process(row):
     return origin_words, none_words
 
 
-def ner_demo_by_deepseek():
+def ner_sample_by_deepseek():
     patents = []
-    df = pl.read_csv(str(Path(demo_file_name).expanduser()), columns=columns, encoding="utf-8")
+    df = pl.read_csv(str(env.resolve_output(sample_file_name)), columns=columns, encoding="utf-8")
     df = df.sort('专利申请号')
     results = []
     count = 1
@@ -81,8 +79,8 @@ def ner_demo_by_deepseek():
                 result['结果'][t] = FlowList(result['结果'][t])
             results.append(result)
             patents.append(row[1])
-            Path('ds_patents_result.yml').write_text(yaml.dump(results, allow_unicode=True), encoding='utf-8')
-            Path('ds_patents.yml').write_text(yaml.dump(patents), encoding='utf-8')
+            env.resolve_output(ds_sample_ner_file_name).write_text(yaml.dump(results, allow_unicode=True), encoding='utf-8')
+            env.resolve_output('ds_ner_patents.yml').write_text(yaml.dump(patents), encoding='utf-8')
             print(datetime.now(), row[1], count)
             count += 1
         except Exception as e:
@@ -91,13 +89,10 @@ def ner_demo_by_deepseek():
     print('All ok')
 
 
-def resave_ds_patents_result():
-    ner_result = read_ner_result()
-    for ns in ner_result:
-        for t in ns['结果']:
-            ns['结果'][t] = FlowList(ns['结果'][t])
-    result_str = yaml.dump(ner_result, allow_unicode=True)
-    Path('ds_patents_result_resave.yml').write_text(result_str, encoding='utf-8')
+def read_ner_result():
+    yaml_str = env.resolve_output(ds_sample_ner_file_name).read_text(encoding='utf-8')
+    ner_result = yaml.safe_load(yaml_str)
+    return ner_result
 
 
 def annotate_bio(text, ner_words):
@@ -145,28 +140,21 @@ def add_bio():
             ner_word['not_found'] = FlowList(not_found)
             print(ner_word['专利号'], not_found)
 
-    bio_file = str(Path(bio_file_name).expanduser())
-
-    # for ns in ner_results:
-    #     for t in ns['ner']:
-    #         ns['ner'][t] = FlowList(ns['ner'][t])
-    # result_str = yaml.dump(ner_results, allow_unicode=True)
-    # Path(bio_file_name).expanduser().write_text(result_str, encoding='utf-8')
-
+    bio_file = str(env.resolve_output(bio_file_name))
     df = pd.DataFrame.from_dict(ner_results)  # 转换为 DataFrame
     df.to_csv(bio_file, index=False, encoding="utf-8")
     print('All done.')
 
 
 def get_bio():
-    df = pd.read_csv(bio_file_name, encoding='utf-8')
+    df = pd.read_csv(str(env.resolve_output(bio_file_name)), encoding='utf-8')
 
     labels = []
     for label in df['bio']:
         label = eval(label)
         labels.append([l[1] for l in label])
 
-    return df['摘要'].tolist(), labels
+    return df['专利号'].tolist(), df['摘要'].tolist(), labels
 
 
 def check_entity_name():
@@ -187,7 +175,7 @@ def check_entity_name():
 
 def building_dict():
     # 搜狗建筑词汇库
-    records = get_words_from_sogou_cell_dict(Path(building_dict_file_name_sogo).expanduser())
+    records = get_words_from_sogou_cell_dict(env.resolve_data(building_dict_file_name_sogo))
     build_words = [r[1] for r in records]
 
     ner_result = read_ner_result()
@@ -202,7 +190,7 @@ def building_dict():
 
 if __name__ == '__main__':
     # building_dict()
-    # ner_demo_by_deepseek()
+    # ner_sample_by_deepseek()
     # resave_ds_patents_result()
     # add_bio()
     # check_entity_name()

+ 0 - 44
chinese_vocab_data.yaml

@@ -1,44 +0,0 @@
-- text: 我喜欢在北京吃苹果,北京的天气很好。你好,世界!谢谢你。
-  vocab:
-    问候语 (Greetings):
-    - 你好
-    - 谢谢
-    - 再见
-    地点 (Places):
-    - 北京
-    - 上海
-    - 公园
-    水果 (Fruits):
-    - 苹果
-    - 香蕉
-    - 橘子
-    颜色 (Colors):
-    - 红色
-    - 蓝色
-- text: 今天天气不错,我们去公园散步吧。上海的公园很漂亮。
-  vocab:
-    活动 (Activities):
-    - 散步
-    - 去
-    地点 (Places):
-    - 公园
-    - 上海
-    时间 (Time):
-    - 今天
-    天气 (Weather):
-    - 天气不错
-- text: 小猫喜欢吃鱼,小狗喜欢玩球。红色和蓝色的球。
-  vocab:
-    动物 (Animals):
-    - 小猫
-    - 小狗
-    - 鱼
-    动作 (Actions):
-    - 吃
-    - 玩
-    - 喜欢
-    物品 (Items):
-    - 球
-    颜色 (Colors):
-    - 红色
-    - 蓝色

+ 0 - 0
building_ipc.yml → conf/building_ipc.yml


+ 0 - 0
data/hit_stopwords.txt → conf/hit_stopwords.txt


+ 0 - 0
data/patent_stopwords.txt → conf/patent_stopwords.txt


+ 0 - 0
word_dict.txt → conf/word_dict.txt


+ 0 - 0
bio.yml → data/bio.yml


+ 0 - 0
ds_patents.yml → data/ds_patents.yml


+ 0 - 0
ds_patents_result.yml → data/ds_patents_result.yml


+ 0 - 0
ds_patents_result_resave.yml → data/ds_patents_result_resave.yml


+ 0 - 0
non_include.yml → data/non_include.yml


+ 0 - 0
patents.yml → data/patents.yml


+ 62 - 20
data_preparation.py

@@ -3,14 +3,15 @@ import polars as pl
 import yaml
 
 from pathlib import Path
+from env import env
 
 columns = ["专利申请号", "IPC分类号", "专利申请日", "摘要"]
-patent_file_name = '~/Documents/论文/上市公司-专利明细数据.csv'
-desc_file_name = '~/Documents/论文/上市公司-建筑工业化-专利摘要数据.csv'
-demo_file_name = '~/Documents/论文/上市公司-建筑工业化-专利摘要数据-样本.csv'
+patent_file_name = '上市公司-专利明细数据.csv'
+filerted_file_name = '上市公司-专利摘要数据-筛选.csv'
+sample_file_name = '上市公司-专利摘要数据-筛选-样本.csv'
 
 def read_csv():
-    # 逐块读取(每次读取 100,000 行)
+    # 逐块读取(每次读取 10 行)
     chunk_size = 10
 
     for chunk in pd.read_csv(patent_file_name, chunksize=chunk_size, encoding="utf-8"):
@@ -20,38 +21,79 @@ def read_csv():
 # read_csv()
 
 
-def get_ipc_df(ipcs):
+def get_stop_words():
+    with open(str(env.resolve('conf/hit_stopwords.txt'))) as f:
+        stopwords = set(line.strip() for line in f)
+    with open(str(env.resolve('conf/patent_stopwords.txt'))) as f:
+        stopwords |= set(line.strip() for line in f)
+    return stopwords
+
+stopwords = get_stop_words()
+def clean_raw_text(raw_text):
+    raw = raw_text.replace(r'\r', '').replace(r'\n', '').replace(r'\t', '').replace(' ', '')
+    # for stop in stopwords:
+    #     raw = raw.replace(stop, '')
+    return raw
+
+
+def get_ipc_df(ipcs, start=None, end=None):
     # 根据给定的ipc列表读取专利数据
 
     # 读取专利文件指定的列
-    df = pl.read_csv(str(Path(patent_file_name).expanduser()), columns=columns, encoding="utf-8")
+    df = pl.read_csv(str(env.resolve_data(patent_file_name)), columns=columns, encoding="utf-8")
 
     # 构造IPC正则表达式
     regex_pattern = "|".join(ipcs)
 
     # 按IPC正则表达式筛选数据
     df = df.filter(pl.col('IPC分类号').str.contains(regex_pattern))
+
+    # 按申请日期过滤
+    if start is not None:
+        df = df.filter(pl.col('专利申请日') >= start)
+    if end is not None:
+        df = df.filter(pl.col('专利申请日') <= end)
+
     return df
 
 
-def save_building_csv():
+def save_building_csv(ipcs=None, start=None, end=None):
     # 筛选并保存跟建筑工业化相关IPC的专利数据
-    ipc_yaml = Path('building_ipc.yml').read_text()
-    ipc_data = yaml.safe_load(ipc_yaml)
-    keywords_list = list(ipc_data.keys())
-    df = get_ipc_df(keywords_list)
-    df.write_csv(desc_file_name)
+    if ipcs is None:
+        ipc_yaml = env.resolve('conf/building_ipc.yml').read_text()
+        ipc_data = yaml.safe_load(ipc_yaml)
+        ipcs = list(ipc_data.keys())
+    df = get_ipc_df(ipcs, start, end)
+    df.write_csv(str(env.resolve_output(filerted_file_name)))
+
+
+def clean_pl_df(df, dirty_column):
+    cleaned_df = df.with_columns(
+        pl.col(dirty_column)
+        # .str.to_lowercase()  # 转为小写
+        # .str.replace_all(r"[^a-z]", "")  # 删除非字母字符
+        .replace('关注公众号“马 克 数 据 网”', '')
+        .str.strip_chars()  # 去除首尾空格
+        .alias("fully_cleaned")
+    )
+    return cleaned_df
+
+
+def save_patent_description():
+    df = pl.read_csv(str(env.resolve_data(patent_file_name)), columns=["摘要"], encoding="utf-8")
+    df.write_csv(str(env.resolve_data('上市公司-专利明细数据-摘要.csv')))
 
 
-def read_ner_result():
-    yaml_str = Path('ds_patents_result.yml').read_text(encoding='utf-8')
-    ner_result = yaml.safe_load(yaml_str)
-    return ner_result
+def save_sample_csv(fraction=0.05):
+    df = pl.read_csv(str(env.resolve_output(filerted_file_name)), columns=columns, encoding="utf-8")
+    # 抽取样本并保存
+    df = df.sample(fraction=fraction)
+    df.write_csv(str(env.resolve_output(sample_file_name)))
 
 
 if __name__ == '__main__':
+    env.data_folder = '/Users/gaojun/dev/gxy-gd/论文'
+    env.output_folder = '/Users/gaojun/dev/gxy-gd/论文'
+    save_patent_description()
     # save_building_csv()
-    df = pl.read_csv(str(Path(desc_file_name).expanduser()), columns=columns, encoding="utf-8")
-    # 抽取5%样本并保存
-    df = df.sample(fraction=0.20)
-    df.write_csv(demo_file_name)
+    # save_sample_csv()

+ 159 - 0
doc/data_flow_1.md

@@ -0,0 +1,159 @@
+好的,我们来介绍如何使用 Prefect(特指 Prefect 2.x 版本)在本地进行涉及多个参数组合的数据流(Flow)试验。
+
+Prefect 非常适合这种场景,因为它能自动跟踪每次运行的参数、状态和结果,并通过其 UI 提供清晰的可视化。
+
+**核心思路:**
+
+1.  **定义参数化的 Flow:** 创建一个接受不同参数作为输入的 Prefect Flow 函数。
+2.  **定义参数组合:** 准备好多组你想要测试的参数。
+3.  **循环调用 Flow:** 在一个普通的 Python 脚本中,循环遍历你的参数组合,并为每一组参数调用(执行)一次你的 Flow 函数。
+4.  **使用 Prefect UI 观察结果:** 启动 Prefect UI 来查看每次 Flow 运行的详情,包括传入的参数、任务状态、日志和最终结果(如果 Flow 有返回值)。
+
+**具体步骤和示例代码:**
+
+**1. 安装 Prefect**
+
+如果你还没有安装 Prefect,请先安装:
+
+```bash
+pip install prefect
+```
+
+**2. 定义参数化的 Flow 和 Tasks**
+
+创建一个 Python 文件(例如 `experiment.py`),并定义你的任务(Tasks)和数据流(Flow)。Flow 函数的参数就是你想要试验的变量。
+
+```python
+import prefect
+from prefect import flow, task
+import time
+import random
+
+# 定义一些任务 (Tasks)
+@task
+def load_data(source: str) -> list:
+    """模拟加载数据的任务"""
+    print(f"加载数据来源: {source}...")
+    time.sleep(random.uniform(0.5, 1.5))
+    # 假设返回一些数据
+    return list(range(10))
+
+@task
+def process_data(data: list, processing_param: float) -> dict:
+    """模拟数据处理任务,使用一个参数"""
+    print(f"处理数据,参数: {processing_param}...")
+    processed_count = int(len(data) * processing_param)
+    processed_data = data[:processed_count]
+    time.sleep(random.uniform(0.5, 1.0))
+    print(f"处理了 {len(processed_data)} 条数据.")
+    # 假设返回处理结果的摘要
+    return {"processed_count": len(processed_data), "processing_param": processing_param}
+
+@task
+def generate_report(result: dict, report_format: str) -> str:
+    """模拟生成报告的任务,使用另一个参数"""
+    print(f"生成报告,格式: {report_format}...")
+    time.sleep(0.5)
+    report = f"--- 报告 ({report_format}) ---\n处理数量: {result['processed_count']}\n处理参数: {result['processing_param']}\n--- 结束 ---"
+    print(report)
+    return report
+
+# 定义参数化的 Flow
+@flow(name="Multi-Parameter Experiment Flow", log_prints=True) # log_prints=True 会自动捕获 print 语句到 Prefect 日志
+def run_experiment(
+    data_source: str = "default_source", # 参数1: 数据源
+    process_level: float = 0.5,        # 参数2: 处理级别
+    report_type: str = "txt"           # 参数3: 报告格式
+):
+    """
+    这是一个接收多个参数的实验性数据流。
+    """
+    run_id = prefect.runtime.flow_run.id # 获取当前 Flow Run 的 ID
+    print(f"开始 Flow Run: {run_id}")
+    print(f"参数: data_source='{data_source}', process_level={process_level}, report_type='{report_type}'")
+
+    # 调用 Tasks
+    raw_data = load_data(source=data_source)
+    processing_result = process_data(data=raw_data, processing_param=process_level)
+    final_report = generate_report(result=processing_result, report_format=report_type)
+
+    print(f"Flow Run {run_id} 完成.")
+    return final_report # Flow 可以返回值,这也会被记录
+```
+
+**3. 定义参数组合并循环执行 Flow**
+
+在同一个文件 `experiment.py` 的末尾(或者一个单独的 `run_local.py` 文件中),添加执行逻辑。
+
+```python
+# ... (上面的 flow 和 task 定义) ...
+
+if __name__ == "__main__":
+    # 定义你想要试验的参数组合
+    parameter_combinations = [
+        {"data_source": "source_A", "process_level": 0.3, "report_type": "json"},
+        {"data_source": "source_A", "process_level": 0.8, "report_type": "csv"},
+        {"data_source": "source_B", "process_level": 0.5, "report_type": "txt"},
+        {"data_source": "source_C", "process_level": 0.9, "report_type": "json"},
+        {"data_source": "source_C", "process_level": 0.2, "report_type": "txt"},
+    ]
+
+    print(f"准备执行 {len(parameter_combinations)} 组实验...")
+
+    # 循环遍历参数组合,为每一组参数执行一次 Flow
+    results = []
+    for i, params in enumerate(parameter_combinations):
+        print(f"\n--- 开始第 {i+1} 次实验: {params} ---")
+        # 直接调用 flow 函数,并传入参数
+        # Prefect 会自动捕获这次调用为一个 Flow Run
+        try:
+            # 使用 **params 将字典解包为关键字参数传递给 flow 函数
+            result = run_experiment(**params)
+            results.append({"params": params, "result": result, "status": "Success"})
+            print(f"--- 第 {i+1} 次实验完成 ---")
+        except Exception as e:
+            results.append({"params": params, "result": str(e), "status": "Failed"})
+            print(f"--- 第 {i+1} 次实验失败: {e} ---")
+
+    print("\n所有本地实验执行完毕。")
+    # 你可以在这里对 results 列表进行分析
+    # print("实验结果汇总:", results)
+```
+
+**4. 启动 Prefect UI 并观察结果**
+
+a.  **运行 Python 脚本:**
+    在你的终端中,导航到包含 `experiment.py` 的目录,然后运行它:
+
+    ```bash
+    python experiment.py
+    ```
+    你会看到脚本打印出每次实验开始和结束的信息,以及 Flow 内部任务的 `print` 输出。每次调用 `run_experiment(**params)` 都会启动一个新的 Prefect Flow Run。
+
+b.  **启动 Prefect UI:**
+    打开一个新的终端窗口(或在脚本运行结束后在同一个终端),运行:
+
+    ```bash
+    prefect ui
+    ```
+    这会启动一个本地的 Web 服务器,并通常会自动在你的浏览器中打开 Prefect UI(地址通常是 `http://127.0.0.1:4200`)。
+
+c.  **在 UI 中查看:**
+    *   **Flow Runs 页面:** 你会看到一个列表,其中包含了你刚刚通过脚本执行的每一次 `run_experiment` 调用。每一次调用都对应列表中的一行(一个 Flow Run)。
+    *   **区分运行:** 你可以通过 "Parameters" 列(可能需要添加到视图中)或者点击进入每个 Flow Run 的详情页面来查看该次运行具体使用了哪些参数。
+    *   **查看详情:** 点击任意一个 Flow Run,你可以看到:
+        *   **Overview/概览:** 运行的基本信息,状态,持续时间等。
+        *   **Parameters/参数:** 清晰地列出了这次运行传入 `run_experiment` 的所有参数及其值。
+        *   **Logs/日志:** 包含了 Flow 和 Tasks 中的 `print` 语句(因为我们设置了 `log_prints=True`)以及 Prefect 自身的日志信息。
+        *   **Task Runs/任务运行:** 显示了该 Flow Run 中所有 Task 的执行情况,包括它们的输入、输出(如果配置了结果持久化或较小)、状态和持续时间。
+        *   **Results/结果:** 如果 Flow 函数有返回值,这里会显示返回的结果。
+
+**关键优势总结:**
+
+*   **自动跟踪:** 无需手动记录每次实验的参数和状态,Prefect 自动完成。
+*   **清晰的可视化:** Prefect UI 提供了一个集中的地方来比较不同参数组合下的运行表现。
+*   **代码简洁:** 定义 Flow 和 Task 的方式很自然,参数化也很直接。
+*   **本地优先:** 这种方法完全在本地运行,不需要配置复杂的服务器或云环境,非常适合初期探索和试验。
+*   **可扩展性:** 当你准备好进行更正式的部署、调度或并行执行时,可以基于这个本地 Flow 定义创建 Prefect Deployments。
+
+这种简单的循环调用方法是进行多参数本地试验的最直接方式。对于更复杂的场景,例如需要并行执行这些独立的 Flow Runs(而不是按顺序),或者在单个 Flow Run 内部并行处理参数列表(使用 `Task.map`),则需要更进阶的设置,但这超出了基本的本地试验范围。

+ 60 - 0
env.py

@@ -0,0 +1,60 @@
+
+from pathlib import Path
+
+
+class Env:
+    envs = {}
+
+    def __new__(cls, context=None):
+        if context not in cls.envs:
+            obj = object.__new__(cls)
+            cls.envs[context] = obj
+        return cls.envs[context]
+
+    def __init__(self, context=None):
+        self._context = context or {}
+        self._data_folder = ''
+        self._output_folder = ''
+
+    @property
+    def data_folder(self):
+        return self._data_folder
+
+    @data_folder.setter
+    def data_folder(self, folder):
+        path = Path(folder)
+        if path.is_absolute():
+            self._data_folder = str(path)
+        else:
+            self._data_folder = str(self.resolve(folder))
+
+    @property
+    def output_folder(self):
+        return self._output_folder
+
+    @output_folder.setter
+    def output_folder(self, folder):
+        path = Path(folder)
+        if path.is_absolute():
+            self._output_folder = str(path)
+        else:
+            self._output_folder = str(self.resolve(folder))
+
+    @classmethod
+    def _resolve(cls, folder, file_name):
+        file = Path(file_name).expanduser()
+        if file.is_absolute():
+            return file
+        return Path(folder).joinpath(file_name).resolve()
+
+    def resolve(self, file_name):
+        return self._resolve(str(Path(__file__).parent), file_name)
+
+    def resolve_data(self, file_name):
+        return self._resolve(self._data_folder, file_name)
+
+    def resolve_output(self, file_name):
+        return self._resolve(self._output_folder, file_name)
+
+
+env = Env()

+ 73 - 64
lda.py

@@ -25,63 +25,64 @@ def cut(text):
     return seg_list
 
 
-def stop_words():
-    with open('hit_stopwords.txt') as f:
-        stopwords = set(line.strip() for line in f)
-    with open('patent_stopwords.txt') as f:
-        stopwords |= set(line.strip() for line in f)
-    return stopwords
-
+def process():
+    file_name = str(Path(building_dict_file_name).absolute())
+    seg = pkuseg(user_dict=file_name)
+    stop_words = get_stop_words()
+
+    def preprocess_text_with_ner(patent_ner):
+        entities = []
+        entity_texts = set()  # 用set去重
+        patent_entities = [ent for categ_ent in patent_ner['结果'].values() for ent in categ_ent]
+        for ent in patent_entities:
+            # 清理实体文本,去除内部多余空格,并转小写(可选)
+            clean_ent = ent.strip().lower()
+            if clean_ent and clean_ent not in stop_words and len(clean_ent) > 1:  # 过滤掉空实体、停用词实体和单字实体
+                entities.append(clean_ent)
+                entity_texts.add(clean_ent)  # 记录已被识别为实体的文本
+
+        # 对非实体部分进行分词 (可选策略)
+        # 策略 A: 只使用实体 (如果实体覆盖度足够)
+        # processed_tokens = entities
+
+        # 策略 B: 结合分词,但避免重复切分实体
+        # (这个策略较复杂,这里采用一个简化版:对全文分词,然后替换回实体,再过滤)
+        # 使用jieba分词
+        text = patent_ner['摘要']
+        # words = seg.cut(text)
+        words = []
+
+        processed_tokens = []
+        # 先加入识别出的实体
+        processed_tokens.extend(entities)
+
+        # 再处理分词结果,过滤停用词、单字词,并确保不是实体的一部分
+        current_text = text.lower()  # 用于检查是否是实体的一部分
+        for word in words:
+            word = word.strip().lower()
+            # 检查这个词是否已经是实体的一部分,或者是否是停用词/单字词
+            is_part_of_entity = False
+            for ent_text in entity_texts:
+                if word in ent_text:  # 简化判断,可能不够精确
+                    is_part_of_entity = True
+                    break
+
+            if word and word not in stop_words and len(word) > 1 and not is_part_of_entity:
+                processed_tokens.append(word)
+
+        # 去重 (如果需要)
+        processed_tokens = list(dict.fromkeys(processed_tokens))
+
+        return processed_tokens
 
-def preprocess_text_with_ner():
     ner_result = data_preparation.read_ner_result()
-    entity_texts = set()  # 用set去重
-    for ent in doc.ents:
-        # 清理实体文本,去除内部多余空格,并转小写(可选)
-        clean_ent = ent.text.strip().lower()
-        if clean_ent and clean_ent not in stop_words and len(clean_ent) > 1:  # 过滤掉空实体、停用词实体和单字实体
-            entities.append(clean_ent)
-            entity_texts.add(clean_ent)  # 记录已被识别为实体的文本
-
-    # 对非实体部分进行分词 (可选策略)
-    # 策略 A: 只使用实体 (如果实体覆盖度足够)
-    # processed_tokens = entities
-
-    # 策略 B: 结合分词,但避免重复切分实体
-    # (这个策略较复杂,这里采用一个简化版:对全文分词,然后替换回实体,再过滤)
-    # 使用jieba分词
-    words = jieba.lcut(text, cut_all=False)
-
-    processed_tokens = []
-    # 先加入识别出的实体
-    processed_tokens.extend(entities)
-
-    # 再处理分词结果,过滤停用词、单字词,并确保不是实体的一部分
-    current_text = text.lower()  # 用于检查是否是实体的一部分
-    for word in words:
-        word = word.strip().lower()
-        # 检查这个词是否已经是实体的一部分,或者是否是停用词/单字词
-        is_part_of_entity = False
-        for ent_text in entity_texts:
-            if word in ent_text:  # 简化判断,可能不够精确
-                is_part_of_entity = True
-                break
-
-        if word and word not in stop_words and len(word) > 1 and not is_part_of_entity:
-            processed_tokens.append(word)
-
-    # 去重 (如果需要)
-    processed_tokens = list(dict.fromkeys(processed_tokens))
-
-    return processed_tokens
-
-
-def run_lda():
+    processed_docs = [preprocess_text_with_ner(patent_ner) for patent_ner in ner_result]
+
+
     print("\n开始构建LDA模型...")
     start_time = time.time()
 
     # 创建Gensim字典
-    processed_docs = preprocess_text_with_ner()
     id2word = corpora.Dictionary(processed_docs)
     print(f"创建字典完成,字典大小: {len(id2word)}")
 
@@ -91,15 +92,30 @@ def run_lda():
 
     # 确定主题数量 (这是一个超参数,通常需要尝试不同的值)
     # 可以通过计算困惑度(Perplexity)或一致性分数(Coherence Score)来辅助选择
-    num_topics = 3  # 假设我们想挖掘3个主题 (根据你的数据量和领域知识调整)
+    num_topics = 30  # 假设我们想挖掘3个主题 (根据你的数据量和领域知识调整)
     print(f"设置主题数量为: {num_topics}")
 
+    # import multiprocessing
+    # cpu_count = multiprocessing.cpu_count()
+
     # 训练LDA模型 (使用多核版本 LdaMulticore 加速)
     # passes 控制训练遍数,iterations 控制每次迭代的最大次数
     # alpha 和 eta 是先验参数,'auto' 让gensim自动学习
     # random_state 保证结果可复现
     try:
-        lda_model = models.LdaMulticore(
+        # lda_model = models.LdaMulticore(
+        #     corpus=corpus,
+        #     id2word=id2word,
+        #     num_topics=num_topics,
+        #     random_state=42,
+        #     chunksize=100,  # 每次处理的文档数
+        #     passes=15,  # 整个语料库的训练遍数
+        #     iterations=100,  # 对每个文档的迭代次数
+        #     alpha='auto',  # 或者设置为一个浮点数 e.g., 0.1
+        #     eta='auto',  # 或者设置为一个浮点数 e.g., 0.01
+        #     workers=max(1, cpu_count - 1)  # 使用CPU核心数-1
+        # )
+        lda_model = models.LdaModel(
             corpus=corpus,
             id2word=id2word,
             num_topics=num_topics,
@@ -109,15 +125,9 @@ def run_lda():
             iterations=100,  # 对每个文档的迭代次数
             alpha='auto',  # 或者设置为一个浮点数 e.g., 0.1
             eta='auto',  # 或者设置为一个浮点数 e.g., 0.01
-            workers=max(1, spacy.util.cpu_count() - 1)  # 使用CPU核心数-1
         )
         print("LDA模型训练成功。")
 
-        end_time = time.time()
-        print(f"LDA模型训练耗时: {end_time - start_time:.2f} 秒。")
-
-        # --- 5. 结果展示与评估 ---
-
         print("\n--- LDA 主题结果 ---")
         # 打印每个主题的代表性词语 (实体优先)
         # num_words 控制每个主题显示多少个词
@@ -138,17 +148,16 @@ def run_lda():
         # doc_lda = lda_model[corpus[0]] # 获取第一篇文档的主题分布
         # print(doc_lda) # 输出格式为 [(topic_id, probability), ...]
 
+        end_time = time.time()
+        print(f"LDA模型训练耗时: {end_time - start_time:.2f} 秒。")
     except Exception as e:
         print(f"LDA模型训练或评估过程中发生错误: {e}")
         print("可能的原因:数据量过少、文本预处理后内容为空、参数设置问题等。")
 
-    print("\n--- 流程结束 ---")
-
-
 if __name__ == '__main__':
     # save_user_dict()
     txt = '''
 本发明提供一种陶瓷坯体及其成型方法,所述成型方法包括下述步骤:步骤1、配料:称取陶瓷粉料,量取溶剂,并制备环氧体系粉末,以100重量份的陶瓷粉料为基准,所述环氧体系粉末的含量为1-5重量份,所述溶剂的含量为50-100重量份;步骤2、球磨:将上述陶瓷粉料、环氧体系粉末、溶剂进行球磨,得到浆料;步骤3、成型:将上述浆料注入模具中,低温脱除溶剂;然后在模具上方进行加压,同时对模具进行加热,冷却后得到坯件。本发明还涉及采用所述陶瓷坯体制作的陶瓷产品。本发明通过在体系中加入溶剂,能够有效降低粘结剂在整个体系中的含量比,所制作的陶瓷坯件具有较高的硬度、较低的收缩率,并且适用于于大尺寸的陶瓷产品的制备。    
     '''
-    cut(txt)
-    # preprocess_text_with_ner()
+    # cut(txt)
+    process()

+ 133 - 0
lda_flow.py

@@ -0,0 +1,133 @@
+import prefect
+from prefect import flow, task
+import time
+import random
+
+import data_preparation
+import bio
+import ner
+
+from env import env
+
+
+# 定义任务 (Tasks)
+@task
+def filter_patent(ipcs: list, start=None, end=None):
+    """
+    从文件 上市公司-专利明细数据.csv 中筛选专利数据
+    :param ipcs: 需要过滤的 ipc 分类列表,
+    :param start: 开始日期,'2014-01-01'
+    :param end: 结束日期
+    :return:
+    """
+    data_preparation.save_building_csv(ipcs, start, end)
+
+
+def sample_patent(fraction=0.05):
+    """
+    从筛选的专利数据中随机取样,结果保存在文件:上市公司-专利摘要数据-筛选.csv
+    :param fraction: 取样率
+    :return:
+    """
+    data_preparation.save_building_csv(fraction)
+
+
+def deepseek_ner_sample():
+    """
+    将取样专利数据交由deepseek进行命名实体识别,结果保存在文件:ds_sample_ner_result.yml
+    :return:
+    """
+    bio.ner_sample_by_deepseek()
+
+
+@task
+def cut_words():
+    ...
+
+
+def add_bio():
+    """
+    根据 ds_sample_ner_result.yml 文件中识别出的命名实体进行BIO标注,结果保存在文件:上市公司-专利摘要数据-筛选-样本-BIO.csv
+    :return:
+    """
+    bio.add_bio()
+
+
+def train(model_name):
+    """
+    训练模型
+    :return:
+    """
+    ner.train(model_name)
+
+
+def model_test():
+    ...
+
+def _ner():
+    ...
+
+
+def lda():
+    ...
+
+
+if __name__ == '__main__':
+    env.data_folder = '/Users/gaojun/dev/gxy-gd/论文'
+    env.output_folder = '/Users/gaojun/dev/gxy-gd/论文'
+    # ner.predict_test('hfl/chinese-roberta-wwm-ext-large-building-building')
+    ner.train('hfl/chinese-roberta-wwm-ext-large-building-building')
+
+#
+# # 定义参数化的 Flow
+# @flow(name="Multi-Parameter Experiment Flow", log_prints=True) # log_prints=True 会自动捕获 print 语句到 Prefect 日志
+# def run_experiment(
+#     data_source: str = "default_source", # 参数1: 数据源
+#     process_level: float = 0.5,        # 参数2: 处理级别
+#     report_type: str = "txt"           # 参数3: 报告格式
+# ):
+#     """
+#     这是一个接收多个参数的实验性数据流。
+#     """
+#     run_id = prefect.runtime.flow_run.id # 获取当前 Flow Run 的 ID
+#     print(f"开始 Flow Run: {run_id}")
+#     print(f"参数: data_source='{data_source}', process_level={process_level}, report_type='{report_type}'")
+#
+#     # 调用 Tasks
+#     raw_data = load_data(source=data_source)
+#     processing_result = process_data(data=raw_data, processing_param=process_level)
+#     final_report = generate_report(result=processing_result, report_format=report_type)
+#
+#     print(f"Flow Run {run_id} 完成.")
+#     return final_report # Flow 可以返回值,这也会被记录
+#
+# if __name__ == "__main__":
+#     # 定义你想要试验的参数组合
+#     parameter_combinations = [
+#         {"data_source": "source_A", "process_level": 0.3, "report_type": "json"},
+#         {"data_source": "source_A", "process_level": 0.8, "report_type": "csv"},
+#         {"data_source": "source_B", "process_level": 0.5, "report_type": "txt"},
+#         {"data_source": "source_C", "process_level": 0.9, "report_type": "json"},
+#         {"data_source": "source_C", "process_level": 0.2, "report_type": "txt"},
+#     ]
+#
+#     print(f"准备执行 {len(parameter_combinations)} 组实验...")
+#
+#     # 循环遍历参数组合,为每一组参数执行一次 Flow
+#     results = []
+#     for i, params in enumerate(parameter_combinations):
+#         print(f"\n--- 开始第 {i+1} 次实验: {params} ---")
+#         # 直接调用 flow 函数,并传入参数
+#         # Prefect 会自动捕获这次调用为一个 Flow Run
+#         try:
+#             # 使用 **params 将字典解包为关键字参数传递给 flow 函数
+#             result = run_experiment(**params)
+#             results.append({"params": params, "result": result, "status": "Success"})
+#             print(f"--- 第 {i+1} 次实验完成 ---")
+#         except Exception as e:
+#             results.append({"params": params, "result": str(e), "status": "Failed"})
+#             print(f"--- 第 {i+1} 次实验失败: {e} ---")
+#
+#     print("\n所有本地实验执行完毕。")
+#     # 你可以在这里对 results 列表进行分析
+#     # print("实验结果汇总:", results)

+ 90 - 20
ner.py

@@ -1,16 +1,15 @@
-from pathlib import Path
-
-from transformers import AutoTokenizer, AutoModelForTokenClassification
 import torch
+from sympy.physics.units import current
 
 import bio
-
 import polars as pl
-from data_preparation import demo_file_name, columns, desc_file_name
-# entity_types = ["Material", "Component", "Equipment", "Process", "Organization", "Standard"]
-
 
+from transformers import AutoTokenizer, AutoModelForTokenClassification
+from torch.utils.data import Dataset
+from torch.nn import CrossEntropyLoss # 需要导入损失函数
+from data_preparation import columns, sample_file_name, clean_raw_text
 from bio import entity_types
+from env import env
 
 label_map = {"O": 0}
 index = 1
@@ -27,11 +26,9 @@ label_index = {v: k for k, v in label_map.items()}
 # model = AutoModelForTokenClassification.from_pretrained(model_name, num_labels=len(set(label_map)) + 1)  # include:“O”
 device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
-from torch.utils.data import Dataset
-
-
 class PatentNERDataset(Dataset):
-    def __init__(self, texts, labels, tokenizer, max_length=512):
+    def __init__(self, index, texts, labels, tokenizer, max_length=512):
+        self.index = index
         self.texts = texts
         self.labels = labels
         self.tokenizer = tokenizer
@@ -51,7 +48,32 @@ class PatentNERDataset(Dataset):
 
         # 转换标签
         label_ids = [label_map[label] for label in labels] + [0] * (self.max_length - len(labels))
-        return {"input_ids": input_ids, "attention_mask": attention_mask, "labels": torch.tensor(label_ids)}
+        return {"input_ids": input_ids,
+                "attention_mask": attention_mask,
+                "labels": torch.tensor(label_ids),
+                "text": text,
+                "index": self.index[idx]
+                }
+
+def parse_entity(text, predict):
+    entities = []
+    bios = []
+    entity = []
+
+    current_categ = None
+    for word, tensor in zip(text, predict):
+        label_id = tensor.item()
+        label = label_index[label_id]
+        if label == 'O' or (current_categ and not label[2:] == current_categ):
+            if entity:
+                entities.append(''.join(entity))
+                bios.append(current_categ)
+                entity = []
+                current_categ = None
+            continue
+        current_categ = label[2:]
+        entity.append(word)
+    return entities, bios
 
 
 def train(model_name):
@@ -63,10 +85,10 @@ def train(model_name):
     #                                                         num_labels=len(set(label_map)) + 1)  # include:“O”
 
     # 训练数据
-    train_texts, train_labels = bio.get_bio()
+    train_index, train_texts, train_labels = bio.get_bio()
 
     # 创建数据集
-    dataset = PatentNERDataset(train_texts, train_labels, tokenizer)
+    dataset = PatentNERDataset(train_index, train_texts, train_labels, tokenizer)
 
 
     from torch.utils.data import DataLoader
@@ -77,6 +99,11 @@ def train(model_name):
     optimizer = AdamW(model.parameters(), lr=5e-5)
     train_loader = DataLoader(dataset, batch_size=2, shuffle=True)
 
+    # 创建一个损失函数实例,用于单独计算样本损失
+    # reduction='none' 表示不进行聚合,返回每个元素的损失
+    # reduction='mean' 在这里用于计算单个样本所有 token 的平均损失
+    loss_fct = CrossEntropyLoss(ignore_index=-100, reduction='mean')  # 'mean' 计算单个样本的平均loss
+
     # 训练循环
     for epoch in range(4):
         for batch in train_loader:
@@ -88,16 +115,59 @@ def train(model_name):
             optimizer.step()
             print(f"Epoch {epoch}, Loss: {loss.item()}")
 
+            original_texts = batch["text"]
+            original_bios = batch["labels"]
+
+            # 获取模型的 logits (预测分数)
+            logits = outputs.logits  # Shape: (batch_size, sequence_length, num_labels)
+
+            # --- 单独计算每个样本的损失 ---
+            sample_losses = []
+            for i in range(logits.size(0)):  # 遍历批次中的每个样本
+                # 提取单个样本的 logits 和 labels
+                sample_logits = logits[i]  # Shape: (sequence_length, num_labels)
+                sample_labels = labels[i]  # Shape: (sequence_length)
+
+                # 检查是否存在有效标签,避免除以零或 NaN
+                if (sample_labels != -100).sum() > 0:
+                    # 使用 loss_fct 计算该样本的平均损失
+                    # CrossEntropyLoss 需要 (N, C) 和 (N) 格式,这里 N=sequence_length, C=num_labels
+                    individual_loss = loss_fct(sample_logits, sample_labels)
+                    sample_losses.append((individual_loss.item(), i))
+                else:
+                    # 如果样本没有有效标签(可能全是padding或特殊标记),损失设为0
+                    sample_losses.append((0.0, i))
+
+            # --- 按损失值降序排序 ---
+            sample_losses.sort(key=lambda x: x[0], reverse=True)
+
+            # --- 打印损失最高的样本信息 ---
+            for individual_loss_value, index in sample_losses:
+                # 只打印损失大于某个值的样本,或者打印前 N 个
+                # if individual_loss_value > 0.1: # 可以加一个过滤条件
+                print(f"    - Sample Index in Batch: {batch['index'][index]}")
+                print(f"      Individual Avg Loss: {individual_loss_value:.4f}")
+                print(f"      Original Text: {original_texts[index]}")  # 打印部分文本
+                # print(f"      Original BIO : {' '.join(original_bios[index][:20])}...")  # 打印部分BIO
+                # 可选:进行预测并打印对比,帮助分析错误
+                with torch.no_grad():
+                    pred_labels_ids = torch.argmax(logits[index], dim=-1)
+
+                    pred_labels, _ = parse_entity(original_texts[index], pred_labels_ids)
+                    true_labels, _ = parse_entity(original_texts[index], labels[index])
+                    print(f"      Predicted BIO: {' '.join(pred_labels)}")
+                    print(f"      True BIO     : {' '.join(true_labels)}")
+
         # 保存训练模型
         # model.save_pretrained("building_ner_model")
         # tokenizer.save_pretrained("building_ner_model")
         # model.save_pretrained("building_ner_model_bert_wwm")
         # tokenizer.save_pretrained("building_ner_model_bert_wwm")
-        model.save_pretrained(f"{model_name}-building")
+        model.save_pretrained(env.resolve_output(f"{model_name}-building"))
         tokenizer.save_pretrained(f"{model_name}-building")
 
 
-def test(model_name):
+def predict_test(model_name):
     # 加载训练模型
     # model = AutoModelForTokenClassification.from_pretrained("building_ner_model")
     # tokenizer = AutoTokenizer.from_pretrained("building_ner_model")
@@ -137,14 +207,14 @@ def test(model_name):
         print("\n")
         return predictions
 
-    df = pl.read_csv(str(Path(desc_file_name).expanduser()), columns=columns, encoding="utf-8")
+    df = pl.read_csv(str(env.resolve_data(sample_file_name)), columns=columns, encoding="utf-8")
 
     for description in df['摘要']:
-        description = description.replace(r'\r', ' ').replace(r'\n', ' ').replace(r'\t', '').replace(' ', '')
+        description = clean_raw_text(description)
         predict_distilbert(description)
 
 if __name__ == '__main__':
-    train('hfl/chinese-roberta-wwm-ext-large-building')
+    # train('hfl/chinese-roberta-wwm-ext-large-building')
     # train('hfl/chinese-roberta-wwm-ext-large')
-    # test('hfl/chinese-roberta-wwm-ext-large-building')
+    predict_test('hfl/chinese-roberta-wwm-ext-large-building-building')
     # test()

+ 3 - 2
requirements.txt

@@ -1,3 +1,5 @@
+jieba~=0.42.1
+gensim~=4.3.3
 pip~=25.0.1
 attrs~=25.3.0
 zlib~=1.3.1
@@ -5,17 +7,16 @@ distro~=1.9.0
 wheel~=0.45.1
 pysocks~=1.7.1
 openssl~=3.4.1
+jinja2~=3.1.6
 setuptools~=78.1.0
 typing_extensions~=4.13.1
 packaging~=24.2
 yaml~=0.2.5
 pyyaml~=6.0.2
-jieba~=0.42.1
 polars~=1.26.0
 pandas~=2.2.3
 scipy~=1.15.2
 numpy~=2.2.4
-gensim~=4.3.3
 transformers~=4.50.0
 openai~=1.70.0
 pyqt6~=6.8.1

+ 14 - 0
tests/test.py

@@ -0,0 +1,14 @@
+
+def test_env():
+    from env import env
+
+    env.data_folder = '~/dev/gxy-gd/论文'
+
+    folder = env.resolve('tests/test.py')
+    assert folder.exists()
+
+    folder = env.resolve_data('论文框架.txt')
+    assert folder.exists()
+
+    folder = env.resolve('~/dev/gxy-gd/论文/上市公司-专利明细数据.csv')
+    assert folder.exists()