ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Outlines 实战:用正则约束生成从财报(Earnings Reports)中直接提取金融数据到 CSV

Outlines 实战:用正则约束生成从财报(Earnings Reports)中直接提取金融数据到 CSV Outlines 实战用正则约束生成从财报Earnings Reports中直接提取金融数据到 CSV【免费下载链接】outlinesStructured Outputs项目地址: https://gitcode.com/GitHub_Trending/ou/outlines本篇技术指南演示如何基于 Outlines 的正则表达式约束解码能力让 LLM 直接从结构化程度极低的上市公司 10-K 收益报告中提取年度财务数据并百分之百保证输出是合法 CSV。读完本文你将掌握用正则描述列类型、动态拼接 CSV 语法正则、通过outlines.Generator与Regex组合运行模型以及把提取结果直接载入pandasDataFrame 进行后续分析。文章以仓库中的 cookbook 示例 earnings-reports.md 为骨架并结合 src/outlines 下的源码剖析其底层实现原理。背景为什么财报数据提取如此困难金融领域一个非常常见的任务是从收益报告earnings reports中提取财务数据。问题的根源在于SEC美国证券交易委员会并不要求上市公司提供机器可读的文档因此收益报告普遍格式混乱、极不规范。现实中的收益报告多以 HTML 文档形式提供直接解析非常困难。投资者往往依赖复杂的解析系统甚至需要人工逐条核对才能完成数据提取——行业内甚至有专门的公司在做用自动化方案替代人工解析这件事。本 cookbook 是一个概念验证proof of concept用 LLM 把财务数据直接提取成 CSV。为什么选 CSV因为逗号分隔值Comma-Separated Values结构规整可以用一条正则表达式完整描述而 Outlines 恰好能用正则去约束guideLLM 的输出。本文示例是完整 demo 的一个精简子集——完整版 demo 包含把原始 HTML 转换为结构化 CSV 所需的全部预处理步骤并在三家公司的 10-K 报告上做了结果验证。环境准备与安装安装 Outlines 及其依赖# 较新版本的 torch 可能与某些 CUDA 驱动存在兼容性问题。 # 这里推荐先使用 2.4.0你也可以自行尝试其他版本。 pip install outlines pandas transformers torch2.4.0 accelerate依赖清单里每个包的职责如下包用途outlines结构化生成框架提供正则约束解码与Generator接口transformers加载 Hugging Face 上的预训练因果语言模型与分词器torch2.4.0模型推理后端文档建议暂时锁定该版本以避免 CUDA 驱动问题accelerate支持device_mapcuda等设备调度策略pandas把提取出的 CSV 字符串载入 DataFrame 做后续分析加载模型我们选择一个足够小、能在普通机器上运行的模型Phi-3 mini。import outlines import torch import transformers model_name microsoft/Phi-3-mini-4k-instruct tf_model transformers.AutoModelForCausalLM.from_pretrained( model_name, device_mapcuda, torch_dtypetorch.bfloat16 ) tf_tokenizer transformers.AutoTokenizer.from_pretrained(model_name) model outlines.from_transformers(tf_model, tf_tokenizer)这里的关键一步是outlines.from_transformers(tf_model, tf_tokenizer)。从源码看from_transformers 接受一个transformers.PreTrainedModel实例和PreTrainedTokenizer/ProcessorMixin实例若传入的是普通分词器则返回Transformers模型实例若传入的是多模态 processor 则返回TransformersMultiModal实例文档中支持的模型范围包括PreTrainedModelForCausalLM、PreTrainedMambaForCausalLM、PreTrainedModelForSeq2Seq以及任何实现 transformers 模型 API 的模型。在本文场景中model随即具备 Outlines 的可约束生成能力。数据准备从原始 HTML 到 Markdown 表格为简洁起见我们直接把 Nvidia 10-K 报告的 markdown 版本贴在代码里。完整版 demo 会先把原始 HTML 处理成如下所示的 markdown 表格按页面是否包含利润表income statement进行过滤再把筛选出的页面压缩成一个字符串。income_statement Table of ContentsNVIDIA Corporation and SubsidiariesConsolidated Statements of Income(In millions, except per share data) | | | | | | | | | | | | | | | | | | | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | | | | Year Ended | | | | | | | | | | | | | | | | | | | Jan 28, 2024 | | | | | | Jan 29, 2023 | | | | | | Jan 30, 2022 | | | | Revenue | | | $ | 60,922 | | | | | $ | 26,974 | | | | | $ | 26,914 | | | Cost of revenue | | | 16,621 | | | | | | 11,618 | | | | | | 9,439 | | | | Gross profit | | | 44,301 | | | | | | 15,356 | | | | | | 17,475 | | | | Operating expenses | | | | | | | | | | | | | | | | | | | Research and development | | | 8,675 | | | | | | 7,339 | | | | | | 5,268 | | | | Sales, general and administrative | | | 2,654 | | | | | | 2,440 | | | | | | 2,166 | | | | Acquisition termination cost | | | | | | | | | 1,353 | | | | | | | | | | Total operating expenses | | | 11,329 | | | | | | 11,132 | | | | | | 7,434 | | | | Operating income | | | 32,972 | | | | | | 4,224 | | | | | | 10,041 | | | | Interest income | | | 866 | | | | | | 267 | | | | | | 29 | | | | Interest expense | | | (257) | | | | | | (262) | | | | | | (236) | | | | Other, net | | | 237 | | | | | | (48) | | | | | | 107 | | | | Other income (expense), net | | | 846 | | | | | | (43) | | | | | | (100) | | | | Income before income tax | | | 33,818 | | | | | | 4,181 | | | | | | 9,941 | | | | Income tax expense (benefit) | | | 4,058 | | | | | | (187) | | | | | | 189 | | | | Net income | | | $ | 29,760 | | | | | $ | 4,368 | | | | | $ | 9,752 | | | | | | | | | | | | | | | | | | | | | | Net income per share: | | | | | | | | | | | | | | | | | | | Basic | | | $ | 12\.05 | | | | | $ | 1\.76 | | | | | $ | 3\.91 | | | Diluted | | | $ | 11\.93 | | | | | $ | 1\.74 | | | | | $ | 3\.85 | | | | | | | | | | | | | | | | | | | | | | Weighted average shares used in per share computation: | | | | | | | | | | | | | | | | | | | Basic | | | 2,469 | | | | | | 2,487 | | | | | | 2,496 | | | | Diluted | | | 2,494 | | | | | | 2,507 | | | | | | 2,535 | | | 从不同收益报告提取出的 markdown 表格在行名、列数、数据类型上差异极大。LLM 的优势恰恰在此我们只需按数据类型定义想要的数据模型就会以我们指定的格式输出——无需为每家公司的排版写专属解析器。作为对比下图展示的是该利润表在原始 HTML 中的样子定义我们想要的数据为每一列声明正则类型Outlines 常被用于 JSON 输出但同样适用于 CSV。我们明确知道要提取哪些列也明确知道各列的数据类型例如Year 永远是四位数字revenue 是带逗号的数字……于是可以为每种列类型定义一个正则模式# Define the column type regex patterns column_types { # Year is always a four-digit number year: r\d{4}, # Revenue, operating income, and net income are always numbers with commas. # This regex permits integers that may begin with a minus sign, and may have # commas separating the thousands, millions, etc. integer_comma: r((-?\d),?\d|(-?\d)), # Number is currently not used, but it represents a number with up to two decimal places. number: r(-?\d(?:\.\d{1,2})?), }三个类型的语义拆解year\d{4}精确匹配四位数年份integer_comma((-?\d),?\d|(-?\d))允许可选的负号开头千/百万位之间允许出现逗号例如60,922与-257都能匹配number(-?\d(?:\.\d{1,2})?)最多两位小数的数字当前示例未使用但保留用于每股收益等场景。接下来选择要提取的列Year四位数、Revenue带逗号数字、Operating income带逗号数字、Net income带逗号数字。# Define the columns to extract, and their data types. columns_to_extract { year: year, revenue: integer_comma, operating_income: integer_comma, net_income: integer_comma, }你可以修改column_type_regex来适配你关心的列的数据类型。要新增一个财务指标只需在columns_to_extract中加一个键值对columns_to_extract[diluted_earnings_per_share] number需要提醒的是新增列在准确率上没有经过充分测试请谨慎使用。构造描述目标数据的 CSV 正则有了列与类型定义就可以动态拼接出完整正则# Create the header line. This is the requested column names # separated by commas, i.e. year,revenue,... header ,.join(columns_to_extract.keys()) # Create the data capture patterns. These are the regex patterns # that will be used to capture the data in each column data_patterns [column_types[dtype] for dtype in columns_to_extract.values()] data_line ,.join(data_patterns) # Our final regex pattern. max_rows 3 # We expect 3 rows of data, firms usually report 3 years of income statements csv_regex f{header}(\n{data_line}){{,{max_rows}}}\n\n print(csv_regex)打印出的正则如下year,revenue,operating_income,net_income,basic_earnings_per_share( \d{4},((-?\d),?\d|(-?\d)),((-?\d),?\d|(-?\d)),((-?\d),?\d|(-?\d)),(-?\d(?:\.\d{1,2})?)){,3}注上面这段打印输出来自包含五个列含basic_earnings_per_share的旧版示例若只按本文的columns_to_extract四个列构造打印结果中对应会只有四个模式结构完全相同。看起来相当狰狞对吧其实它的结构很清晰先是表头行header随后是每个数据行data_line数据行通过{,3}量词重复最多 3 次——因为公司通常报告最近三年的利润表数据。正则结尾的两个换行\n\n用于标记 CSV 的终止与下文提示词中的两个换行结束 CSV约定一一对应。把它传给outlines.Generator得到的函数将始终产出与该正则一致consistent的 CSV 字符串——这是 Outlines 约束解码的承诺。构造提示词让模型知道任务是什么Outlines默认不会添加 system 或 instruction token因此我们需要用transformers.AutoTokenizer为当前模型手动添加from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(model_name) def add_instruction(prompt): return tokenizer.apply_chat_template([{role: user, content: prompt}], tokenizeFalse, add_generation_promptTrue) print(add_instruction(Howdy))输出结果|user| Howdy|end| |assistant|我们的提示词大致描述了要模型执行的任务以及理解利润表所需的一些背景知识def extract_financial_data_prompt(columns_to_extract, income_statement): user_prompt f Extract annual financial data from this set of pages. Pages are from a 10k filing and were chosen because they may contain a comprehensive income statement. Note that selected pages may be incorrectly extracted, so you should verify that you are extracting from the comprehensive income statement and not some other financial statement. Create a row for each year available in the income statement with the following columns: {, .join(columns_to_extract.keys())}. Firms typically report the most recent 3 years of data, but this can vary. Each column has types: {, .join(columns_to_extract.values())}. # Relevant pages: {income_statement} # Key instructions: 1. Look ONLY at the Consolidated Statements of Income table 2. For operating income, look for Income from operations or Operating income 3. For net income, use the TOTAL net income figure, not amounts allocated to specific share classes 4. Use NULL for missing values 5. Operating income must be less than revenue 6. Net income must be less than operating income 7. Ignore segment breakdowns, quarterly data, or per-share amounts # Output format: - CSV format with headers: {,.join(columns_to_extract.keys())} - Use NULL for missing values - If no data are found, do not create a row. - Enter two newline characters to terminate the CSV when no more data are found. # Definitions: - Revenue: Total sales of goods and services. Usually this is at the top of the income statement. - Operating income: Revenue minus operating expenses for the entire company. This is revenue minus costs. Operating income is also called operating profit, EBIT, or income from operations. - Net income: Operating income minus taxes. This is the bottom line of the income statement. return add_instruction(user_prompt)这份提示词中的几条关键指令值得注意它们与正则约束互补共同保证数据质量只查看 Consolidated Statements of Income 表格——因为预过滤的页面可能包含其他报表明确 operating income / net income 的口径总净利润而非分摊到特定股份类别的金额缺失值用NULL施加领域约束operating income 必须小于 revenue、net income 必须小于 operating income忽略分部segment拆分、季度数据与每股金额两个换行符终止 CSV——与正则里的\n\n严格对应未找到数据时不创建行对应正则中的{,3}允许 0 到 3 行的设计。运行模型并验证结果有了提示词和正则就可以运行模型了。先构造正则提取器from outlines.types import Regex csv_extractor outlines.Generator(model, Regex(csv_regex))把提示词交给模型执行csv_data csv_extractor( extract_financial_data_prompt(columns_to_extract, income_statement), max_new_tokens1024, ) print(csv_data)输出year,revenue,operating_income,net_income 2024,60922,32972,29760 2023,26974,4224,4368 2022,26914,10041,9752对照 Nvidia 财报原始数据Revenue 60,922 / Operating income 32,972 / Net income 29,760 等提取结果经人工核对完全正确——这就是 LLM 做信息抽取与正则约束做格式保证相结合的典型效果。你甚至可以把它直接载入pandasDataFrame 做进一步分析import pandas as pd from io import StringIO df pd.read_csv(StringIO(csv_data)) print(df)输出year revenue operating_income net_income 0 2024 60922 32972 29760 1 2023 26974 4224 4368 2 2022 26914 10041 9752底层原理从正则到约束解码理解outlines.Generator(model, Regex(csv_regex))内部发生了什么有助于你把这套方案迁移到其他任务上。Generator 工厂与 SteerableGeneratorGenerator是一个工厂函数定义在 generator.py它会根据模型类型与参数分发到不同的生成器类。本地可约束模型如 transformers 模型对应SteerableGenerator其构造逻辑见 generator.py如下通过python_types_to_terms(output_type)把输出类型转换成 DSL 术语Term若术语是CFG调用get_cfg_logits_processor若术语是JsonSchema调用get_json_schema_logits_processor其余情况包括Regex统一走to_regex(term)取出正则字符串再调用get_regex_logits_processor编译成 logits processor。由于编译 logits processor 可能比较昂贵SteerableGenerator会在构造时构建并缓存它每次调用时仅做一次reset()再传给模型生成见 generator.py。output_type与processor两个参数互斥同时传入会抛ValueError。正则 DSLRegex 与 to_regexRegex是 types/dsl.py 中定义的一个Term子类仅封装一个正则模式字符串。to_regextypes/dsl.py负责把任意Term递归展开成最终的正则字符串——对Regex就是把其模式包一层捕获括号。仓库还在 types/init.py 中预置了大量开箱即用的正则类型例如integer、number、date、uuid4、ipv4、semver等它们内部都是Regex(...)实例可作为拼正则的积木。正则如何约束每一步 token 采样最终的正则字符串被交给后端编译。以默认的outlines_core后端为例backends/outlines_core.py 中的get_regex_logits_processor会构造一个Index(regex, self.vocabulary)把正则编译为以模型词表为索引的有限状态机并返回OutlinesCoreLogitsProcessor。在生成过程中logits processor 会在每一步解码前依据当前已生成的 token 序列与正则状态把不可能导致合法 CSV的候选 token 的 logits 直接屏蔽掉。这也解释了为什么Generator产出的结果始终符合正则——不是靠提示词碰运气而是靠解码期硬约束。完整的后端路由在 backends/init.pyget_regex_logits_processor会根据后端名称如outlines_core、xgrammar、llguidance分发到对应实现这些后端均声明于 backends/base.py 的Backend抽象接口中。扩展与注意事项新增指标加一个columns_to_extract键值对即可例如columns_to_extract[gross_profit] integer_comma但新列未经充分准确率测试生产使用前务必自行验证。缺失值约定提示词要求用NULL表示缺失值与列正则并不冲突若你的数据允许缺失可把列正则改为(NULL|...实际模式...)。行数上限max_rows 3假设公司报告三年数据若某公司报告更多年度可调大该值或改成无上限形式如*但会放宽对输出的约束。提示词中的领域约束operating income revenue、net income operating income、只看综合利润表、忽略分部/季度/每股数据等规则是抑制 LLM幻觉出错误报表的关键不应省略。token 预算示例使用max_new_tokens1024足够容纳表头 三行数据若提取更多列或更多年份需要相应增大。模型选择Phi-3 mini 只是示例该方案与模型解耦任何受支持的本地或远程模型详见 docs/features/models 下的模型接入文档都可替换只需保持提示词模板与正则约束不变。小结本文展示了一条完整的LLM 正则约束解码的财报数据提取流水线正则负责格式LLM 负责语义。得益于 Outlines 的Regex输出类型与Generator抽象CSV 这类结构规整但非 JSON的文本同样能被严格约束生成从而把过去依赖复杂解析系统或人工核对的工作压缩为一次高置信度的模型调用并且提取结果可以直接进入pandas等下游分析管线。【免费下载链接】outlinesStructured Outputs项目地址: https://gitcode.com/GitHub_Trending/ou/outlines创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表