refactor: 重组项目目录结构
以讲义内容为骨架迁移到标准目录格式: - officefile/ 主内容(12章 + 附录 + CC4SI补充) - dofile/ 代码示例(11个Python脚本) - data/ 图片资源 - output/ 生成输出(忽略) - Archive/ 归档旧目录(忽略) - .claude/skills/ 保留markdown-to-docx工具链 - .pandoc/ 保留CSL和本地化配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: markdown-to-docx
|
||||
description: Convert one Markdown file or a top-level folder of Markdown articles into DOCX with pandoc, preserving Obsidian image embeds, shared resources folders, captions, Mermaid diagrams, and optional Word reference templates. Use when exporting Markdown to Word documents.
|
||||
---
|
||||
|
||||
# Skill: Markdown to DOCX
|
||||
|
||||
Use this skill when the user wants `.md` files exported to `.docx` with `pandoc`.
|
||||
|
||||
## What this skill does
|
||||
|
||||
- Converts one Markdown file or all top-level Markdown files in a directory.
|
||||
- Creates one `.docx` per source `.md`.
|
||||
- Preserves images by setting a broad `--resource-path`.
|
||||
- Normalizes Obsidian image embeds like `![[image.png]]` and `![[image.png|697]]` before conversion.
|
||||
- Repairs the common case where a figure caption and an Obsidian image were accidentally merged onto one line.
|
||||
- Can render Markdown against a Word `.dotx`/`.docx` reference template and apply post-processing for polished Word output.
|
||||
- Renders Mermaid code blocks to PNG images when Node.js and `npx` are available.
|
||||
|
||||
## Reference Template
|
||||
|
||||
A publisher reference template is available at:
|
||||
|
||||
```
|
||||
SKILL_DIR/resources/machinery-industry-press-writing-template.dotx
|
||||
```
|
||||
|
||||
This is the **机械工业出版社** Word template (`.dotx`). Use it as the `<template-dotx-or-docx>` argument when running the template workflow. When the user asks to export to Word without specifying a template, default to this one.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm `pandoc` is installed with `pandoc --version`.
|
||||
2. Inspect the source folder for Markdown files and image syntax if needed.
|
||||
3. Run the bundled script from this skill folder:
|
||||
|
||||
```bash
|
||||
SKILL_DIR="/path/to/markdown-to-docx"
|
||||
"$SKILL_DIR/scripts/convert_markdown_to_docx.sh" \
|
||||
"<source-path>" \
|
||||
"<output-dir>" \
|
||||
"[resource-root]"
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `source-path`: a single `.md` file or a directory that contains `.md` files.
|
||||
- `output-dir`: destination directory for generated `.docx` files.
|
||||
- `resource-root` (optional): root directory that contains shared assets such as `resources/`. If omitted, the script infers likely roots from the source location.
|
||||
|
||||
## Template Workflow
|
||||
|
||||
When the user provides a Word template:
|
||||
|
||||
### Step 1: Convert (auto-fixes captions)
|
||||
|
||||
The conversion script **automatically inserts missing table/figure captions** into the normalized markdown before calling pandoc. No manual pre-check is needed — the pipeline:
|
||||
|
||||
1. Normalizes Obsidian embeds and markdown syntax
|
||||
2. Runs `validate_captions.py fix` to auto-insert any missing `表N-M` / `图N-M` captions (derived from table headers or surrounding context)
|
||||
3. Converts with pandoc + Lua style filter
|
||||
4. Post-processes the docx (fonts, styles, tables, borders, headers)
|
||||
|
||||
You can still run a manual pre-check to preview what will be fixed:
|
||||
|
||||
```bash
|
||||
SKILL_DIR="/path/to/markdown-to-docx"
|
||||
python3 "$SKILL_DIR/scripts/validate_captions.py" \
|
||||
pre "<source-md>"
|
||||
```
|
||||
|
||||
```bash
|
||||
SKILL_DIR="/path/to/markdown-to-docx"
|
||||
"$SKILL_DIR/scripts/render_markdown_with_dotx.sh" \
|
||||
"<source-md>" \
|
||||
"<output-docx>" \
|
||||
"<template-dotx-or-docx>" \
|
||||
"[book-title]" \
|
||||
"[resource-root]" \
|
||||
"[shortcut-template]"
|
||||
```
|
||||
|
||||
This script automatically:
|
||||
|
||||
- Auto-inserts missing `表N-M` / `图N-M` captions (derived from table headers or surrounding context)
|
||||
- Maps image blocks, figure/table captions to publisher paragraph styles via Lua filter
|
||||
- Post-processes for code style, fonts (Times New Roman + 宋体), table borders, layout
|
||||
- Rewrites unordered-list indentation so bullet text aligns with Chinese paragraph first-line indent instead of Word's default deep indent
|
||||
- Rewrites ordered and unordered lists so the marker column aligns with the Chinese body paragraph's two-character first-line indent instead of drifting too far left
|
||||
- Applies Word `keep with next` to image paragraphs and table captions so images stay with figure captions and captions stay with tables
|
||||
- Removes the code-block first-line indent from the exported `Source Code` paragraph style
|
||||
- Clears first-line indent inside every table cell paragraph so table content does not inherit body-text indentation
|
||||
- Replaces header text with chapter title
|
||||
- Suppresses template auto-numbering when headings already contain explicit chapter numbers
|
||||
- Extracts figure explanations and shortens captions for editor style
|
||||
- Optionally injects Word shortcut bindings from the original `.dotx` template
|
||||
- Automatically preserves Word shortcut bindings when the provided template already contains `word/customizations.xml`
|
||||
|
||||
### Step 2: Post-check — validate generated docx
|
||||
|
||||
```bash
|
||||
SKILL_DIR="/path/to/markdown-to-docx"
|
||||
python3 "$SKILL_DIR/scripts/validate_captions.py" \
|
||||
post "<output-docx>"
|
||||
```
|
||||
|
||||
This checks:
|
||||
1. No `Compact` style paragraphs (undefined style)
|
||||
2. No VML horizontal rules (`o:hr="t"`)
|
||||
3. Font defaults = Times New Roman + 宋体 (not Calibri)
|
||||
4. First-line indent present in Normal style
|
||||
5. All tables have borders (tblBorders or tcBorders)
|
||||
6. Image paragraphs (`图`) and table captions (`表题1-1`) have `keep with next`
|
||||
7. Code block style has no first-line indent
|
||||
8. Ordered and unordered list geometry keeps the marker column aligned with Chinese paragraph first-line indent
|
||||
9. Table cell paragraphs explicitly clear first-line indent
|
||||
10. Table captions (表X-Y) present and sequentially numbered
|
||||
11. Figure captions (图X-Y) present and sequentially numbered
|
||||
|
||||
**If post-check reports ERRORs, investigate and fix.** The most common post-check error is missing captions — which means the markdown source was missing them (go back to Step 1).
|
||||
|
||||
Current editorial rules are tracked in:
|
||||
|
||||
- `references/editorial-template-rules.md`
|
||||
|
||||
## Notes
|
||||
|
||||
- The script only converts top-level `.md` files when a directory is passed.
|
||||
- Source Markdown files are not modified. Normalization happens in a temporary directory.
|
||||
- If the user wants recursive conversion, patch the script first instead of reimplementing the workflow ad hoc.
|
||||
- If the publisher template contains `word/customizations.xml`, the render script now auto-injects those keymap customizations into the generated `.docx`. You can still pass an explicit `shortcut-template` when the shortcut source differs from the reference template.
|
||||
- **MANDATORY**: Always run post-check after conversion. Never skip validation. Caption auto-fix runs automatically during conversion.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Markdown to DOCX"
|
||||
short_description: "Convert Markdown files to DOCX with pandoc, images, captions, and optional Word templates"
|
||||
default_prompt: "Use $markdown-to-docx to convert one Markdown file or a folder of articles into .docx files with pandoc."
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,34 @@
|
||||
# DOCX Template Rules
|
||||
|
||||
This file records Word-export rules that the template rendering pipeline tries to enforce.
|
||||
|
||||
## Non-negotiable layout rules
|
||||
|
||||
- Prefer the latest reviewed `.docx` as the `--reference-doc` when available; treat a `.dotx` as a base template.
|
||||
- If the reference template contains Word shortcut bindings, generated `.docx` files should retain `word/customizations.xml` so style hotkeys such as `ALT+1` remain available. The render pipeline auto-copies these bindings when the reference template already contains them.
|
||||
- Replace only the default header text. Keep first-page and even-page headers blank unless the editor file shows otherwise.
|
||||
- Strip Pandoc body styles such as `FirstParagraph`, `BodyText`, and `Compact` so the reference template's body style wins.
|
||||
- Keep screenshot/image paragraphs mapped to the custom style `图`.
|
||||
- Map figure captions to the custom style `图题`.
|
||||
- Apply Word `keep with next` to every `图` paragraph so each image stays on the same page as the following figure caption.
|
||||
- Apply Word `keep with next` to every `表题1-1` paragraph so each table caption stays on the same page as the following table.
|
||||
- Map only real note/warning labels such as `注:`, `注意:`, and `关键注意:` to the custom style `注意`.
|
||||
- Keep generic explanatory lead-ins such as `说明:` and `解释:` in body text, or rewrite them into prose in the source manuscript.
|
||||
- Remove the first-line indent from exported code-block paragraphs (`Source Code` / `代码清单`) so code starts flush-left inside the code block.
|
||||
- Clear first-line indent for all paragraphs inside Word table cells so table content does not visually inherit body-text indentation.
|
||||
- Align unordered-list text with the Chinese body-text first-line indent. Avoid Word's default deep bullet indentation; level-0 bullet text should start at the same visual column as a normal Chinese paragraph first line.
|
||||
|
||||
## Figure and table caption rules
|
||||
|
||||
- Figure captions must use the short form `图1-1 标题`.
|
||||
- Table captions must use the short form `表1-1 标题`.
|
||||
- Use hyphen serials such as `1-1`, not dotted serials such as `1.1`.
|
||||
- Do not place explanatory text inside figure captions.
|
||||
- If a source figure caption contains a second explanatory sentence, move that sentence into body text and keep only the short title in the caption.
|
||||
- Figure and table captions should not end with a full stop.
|
||||
|
||||
## Editorial writing rules reflected in export
|
||||
|
||||
- When the text says `确认`, `确保`, or `检查`, provide an explicit verification action or acceptance signal nearby instead of leaving the confirmation vague.
|
||||
- Prefer `如图1-1所示` style references in body text when explanation needs to point readers back to a screenshot.
|
||||
- Treat structural rewrites separately from template rules. Examples: adding bridge sentences, converting numbered explanation lists into prose, or rewriting a subsection outline.
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage:
|
||||
convert_markdown_to_docx.sh <source-path> <output-dir> [resource-root]
|
||||
|
||||
Arguments:
|
||||
source-path A single .md file or a directory containing .md files
|
||||
output-dir Destination directory for generated .docx files
|
||||
resource-root Optional root directory for shared assets such as resources/
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $# -lt 2 || $# -gt 3 ]] && usage
|
||||
|
||||
if ! command -v pandoc >/dev/null 2>&1; then
|
||||
echo "Error: pandoc is not installed or not in PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_PATH="$1"
|
||||
OUTPUT_DIR="$2"
|
||||
|
||||
if [[ -f "$SOURCE_PATH" ]]; then
|
||||
SOURCE_DIR="$(cd "$(dirname "$SOURCE_PATH")" && pwd -P)"
|
||||
elif [[ -d "$SOURCE_PATH" ]]; then
|
||||
SOURCE_DIR="$(cd "$SOURCE_PATH" && pwd -P)"
|
||||
else
|
||||
echo "Error: source path not found: $SOURCE_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_PARENT="$(cd "$SOURCE_DIR/.." && pwd -P)"
|
||||
RESOURCE_ROOT="${3:-$SOURCE_PARENT}"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
normalize_markdown() {
|
||||
local input_file="$1"
|
||||
local output_file="$2"
|
||||
|
||||
perl -0pe '
|
||||
s{^图:([^\n!]+)!\[\[([^]|]+)\|[0-9]+\]\]}{\n\n*图:$1*}mg;
|
||||
s{!\[\[([^]|]+)\|[0-9]+\]\]}{}g;
|
||||
s{!\[\[([^]|]+)\]\]}{}g;
|
||||
' "$input_file" > "$output_file"
|
||||
}
|
||||
|
||||
collect_sources() {
|
||||
if [[ -f "$SOURCE_PATH" ]]; then
|
||||
printf '%s\n' "$SOURCE_PATH"
|
||||
return
|
||||
fi
|
||||
|
||||
find "$SOURCE_PATH" -maxdepth 1 -type f -name '*.md' | sort
|
||||
}
|
||||
|
||||
convert_one() {
|
||||
local src_file="$1"
|
||||
local base_name normalized_file output_file src_dir resource_path
|
||||
|
||||
base_name="$(basename "$src_file" .md)"
|
||||
normalized_file="$TMP_DIR/$base_name.md"
|
||||
output_file="$OUTPUT_DIR/$base_name.docx"
|
||||
src_dir="$(cd "$(dirname "$src_file")" && pwd -P)"
|
||||
resource_path="$src_dir:$SOURCE_DIR:$SOURCE_PARENT:$RESOURCE_ROOT:$RESOURCE_ROOT/resources"
|
||||
|
||||
normalize_markdown "$src_file" "$normalized_file"
|
||||
|
||||
pandoc "$normalized_file" \
|
||||
-f markdown \
|
||||
-t docx \
|
||||
--resource-path="$resource_path" \
|
||||
-o "$output_file"
|
||||
|
||||
printf 'OK\t%s\n' "$output_file"
|
||||
}
|
||||
|
||||
converted_count=0
|
||||
|
||||
while IFS= read -r src_file; do
|
||||
[[ -n "$src_file" ]] || continue
|
||||
convert_one "$src_file"
|
||||
converted_count=$((converted_count + 1))
|
||||
done < <(collect_sources)
|
||||
|
||||
if [[ "$converted_count" -eq 0 ]]; then
|
||||
echo "Error: no Markdown files found to convert." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Converted %d file(s) into %s\n' "$converted_count" "$OUTPUT_DIR"
|
||||
@@ -0,0 +1,592 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
PKG_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
|
||||
VML_NS = "urn:schemas-microsoft-com:vml"
|
||||
OFFICE_NS = "urn:schemas-microsoft-com:office:office"
|
||||
NS = {"w": W_NS, "v": VML_NS, "o": OFFICE_NS}
|
||||
ET.register_namespace("w", W_NS)
|
||||
ET.register_namespace("r", R_NS)
|
||||
ET.register_namespace("v", VML_NS)
|
||||
ET.register_namespace("o", OFFICE_NS)
|
||||
|
||||
CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types"
|
||||
KEYMAP_REL_TYPE = "http://schemas.microsoft.com/office/2006/relationships/keyMapCustomizations"
|
||||
KEYMAP_CONTENT_TYPE = "application/vnd.ms-word.keyMapCustomizations+xml"
|
||||
ORDERED_LIST_BASE_LEFT = 800
|
||||
BULLET_LIST_BASE_LEFT = 840
|
||||
LIST_LEVEL_STEP = 420
|
||||
LIST_HANGING = 420
|
||||
BULLET_ABSTRACT_IDS = {"990", "991", "992"}
|
||||
ORDERED_NUMFMTS = {"decimal", "lowerLetter", "upperLetter", "lowerRoman", "upperRoman"}
|
||||
SPECIAL_NUMBERING_PREFIXES = ("表", "图", "代码清单")
|
||||
BULLET_GLYPHS = {
|
||||
"\u2022",
|
||||
"\u25cf",
|
||||
"\u25cb",
|
||||
"\u25aa",
|
||||
"\u25a0",
|
||||
"\uF06C",
|
||||
"\uf0b7",
|
||||
"",
|
||||
"",
|
||||
"o",
|
||||
"☐",
|
||||
}
|
||||
BROKEN_REL_PREFIX_RE = re.compile(r"\bns\d+:id=")
|
||||
|
||||
|
||||
def qn(tag: str) -> str:
|
||||
return f"{{{W_NS}}}{tag}"
|
||||
|
||||
|
||||
def first(root, xpath: str):
|
||||
return root.find(xpath, NS)
|
||||
|
||||
|
||||
def sanitize_relationship_prefixes(xml_path: str) -> None:
|
||||
if not os.path.exists(xml_path):
|
||||
return
|
||||
text = open(xml_path, "r", encoding="utf-8").read()
|
||||
if not BROKEN_REL_PREFIX_RE.search(text):
|
||||
return
|
||||
text = BROKEN_REL_PREFIX_RE.sub("r:id=", text)
|
||||
if 'xmlns:r="' not in text:
|
||||
text = text.replace("<w:document ", f'<w:document xmlns:r="{R_NS}" ', 1)
|
||||
with open(xml_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(text)
|
||||
|
||||
|
||||
def is_bullet_level(absid: str, lvl: ET.Element) -> bool:
|
||||
if absid in BULLET_ABSTRACT_IDS:
|
||||
return True
|
||||
num_fmt = lvl.find(qn("numFmt"))
|
||||
if num_fmt is not None and num_fmt.get(qn("val")) == "bullet":
|
||||
return True
|
||||
lvl_text = lvl.find(qn("lvlText"))
|
||||
if lvl_text is not None and (lvl_text.get(qn("val")) or "") in BULLET_GLYPHS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_generic_ordered_level(lvl: ET.Element) -> bool:
|
||||
num_fmt = lvl.find(qn("numFmt"))
|
||||
if num_fmt is None or num_fmt.get(qn("val")) not in ORDERED_NUMFMTS:
|
||||
return False
|
||||
lvl_text = lvl.find(qn("lvlText"))
|
||||
if lvl_text is None:
|
||||
return False
|
||||
value = lvl_text.get(qn("val")) or ""
|
||||
if "%" not in value:
|
||||
return False
|
||||
if any(prefix in value for prefix in SPECIAL_NUMBERING_PREFIXES):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def style_display_name(style) -> str:
|
||||
name = first(style, "w:name")
|
||||
if name is not None and name.get(qn("val")):
|
||||
return name.get(qn("val"))
|
||||
return style.get(qn("styleId"), "")
|
||||
|
||||
|
||||
def find_style(styles: dict[str, ET.Element], style_ids=(), style_names=()):
|
||||
for style_id in style_ids:
|
||||
style = styles.get(style_id)
|
||||
if style is not None:
|
||||
return style
|
||||
wanted_names = set(style_names)
|
||||
if wanted_names:
|
||||
for style in styles.values():
|
||||
if style_display_name(style) in wanted_names:
|
||||
return style
|
||||
return None
|
||||
|
||||
|
||||
def matching_styles(root, style_ids=(), style_names=()):
|
||||
wanted_ids = set(style_ids)
|
||||
wanted_names = set(style_names)
|
||||
out = []
|
||||
for style in root.findall("w:style", NS):
|
||||
sid = style.get(qn("styleId"), "")
|
||||
name = style_display_name(style)
|
||||
if sid in wanted_ids or name in wanted_names:
|
||||
out.append(style)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_code_style(style) -> None:
|
||||
if style is None:
|
||||
return
|
||||
ppr = first(style, "w:pPr")
|
||||
if ppr is None:
|
||||
ppr = ET.SubElement(style, qn("pPr"))
|
||||
ind = first(ppr, "w:ind")
|
||||
if ind is None:
|
||||
ind = ET.SubElement(ppr, qn("ind"))
|
||||
ind.set(qn("firstLine"), "0")
|
||||
ind.set(qn("firstLineChars"), "0")
|
||||
for attr in ("hanging", "hangingChars", "left", "leftChars"):
|
||||
ind.attrib.pop(qn(attr), None)
|
||||
|
||||
|
||||
def patch_styles(styles_path: str):
|
||||
tree = ET.parse(styles_path)
|
||||
root = tree.getroot()
|
||||
styles = {
|
||||
style.get(qn("styleId")): style for style in root.findall("w:style", NS)
|
||||
}
|
||||
style_name_by_id = {
|
||||
style_id: (
|
||||
name.get(qn("val")) if (name := first(style, "w:name")) is not None else style_id
|
||||
)
|
||||
for style_id, style in styles.items()
|
||||
}
|
||||
|
||||
for style_id in ["2", "3", "4", "5", "6", "78", "91"]:
|
||||
style = styles.get(style_id)
|
||||
if style is None:
|
||||
continue
|
||||
ppr = first(style, "w:pPr")
|
||||
if ppr is None:
|
||||
continue
|
||||
numpr = first(ppr, "w:numPr")
|
||||
if numpr is not None:
|
||||
ppr.remove(numpr)
|
||||
|
||||
source_code = find_style(
|
||||
styles,
|
||||
style_ids=("SourceCode", "93"),
|
||||
style_names=("Source Code", "SourceCode"),
|
||||
)
|
||||
template_code = find_style(
|
||||
styles,
|
||||
style_ids=("af9",),
|
||||
style_names=("代码清单",),
|
||||
)
|
||||
if source_code is not None and template_code is not None:
|
||||
existing_ppr = first(source_code, "w:pPr")
|
||||
existing_rpr = first(source_code, "w:rPr")
|
||||
if existing_ppr is not None:
|
||||
source_code.remove(existing_ppr)
|
||||
if existing_rpr is not None:
|
||||
source_code.remove(existing_rpr)
|
||||
|
||||
template_ppr = first(template_code, "w:pPr")
|
||||
template_rpr = first(template_code, "w:rPr")
|
||||
if template_ppr is not None:
|
||||
source_code.append(copy.deepcopy(template_ppr))
|
||||
if template_rpr is not None:
|
||||
source_code.append(copy.deepcopy(template_rpr))
|
||||
|
||||
normalize_code_style(source_code)
|
||||
normalize_code_style(template_code)
|
||||
|
||||
for style in matching_styles(
|
||||
root,
|
||||
style_ids=("SourceCode", "93", "af9"),
|
||||
style_names=("Source Code", "SourceCode", "代码清单"),
|
||||
):
|
||||
normalize_code_style(style)
|
||||
|
||||
tree.write(styles_path, encoding="UTF-8", xml_declaration=True)
|
||||
return style_name_by_id
|
||||
|
||||
|
||||
def strip_explicit_body_styles(document_path: str, style_name_by_id) -> None:
|
||||
tree = ET.parse(document_path)
|
||||
root = tree.getroot()
|
||||
removable = {"FirstParagraph", "BodyText", "Compact"}
|
||||
removable_ids = {"FirstParagraph", "BodyText", "Compact"}
|
||||
|
||||
for paragraph in root.findall(".//w:p", NS):
|
||||
ppr = first(paragraph, "w:pPr")
|
||||
if ppr is None:
|
||||
continue
|
||||
pstyle = first(ppr, "w:pStyle")
|
||||
if pstyle is None:
|
||||
continue
|
||||
style_id = pstyle.get(qn("val"))
|
||||
if style_id in removable_ids or style_name_by_id.get(style_id) in removable:
|
||||
ppr.remove(pstyle)
|
||||
|
||||
tree.write(document_path, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
|
||||
def patch_header(header_path: str, header_text: str) -> None:
|
||||
tree = ET.parse(header_path)
|
||||
root = tree.getroot()
|
||||
paragraphs = root.findall("w:p", NS)
|
||||
if not paragraphs:
|
||||
return
|
||||
|
||||
target = None
|
||||
for paragraph in paragraphs:
|
||||
texts = "".join(node.text or "" for node in paragraph.findall(".//w:t", NS)).strip()
|
||||
if texts:
|
||||
target = paragraph
|
||||
break
|
||||
|
||||
if target is None:
|
||||
return
|
||||
|
||||
for child in list(target):
|
||||
if child.tag != qn("pPr"):
|
||||
target.remove(child)
|
||||
|
||||
run = ET.SubElement(target, qn("r"))
|
||||
rpr = ET.SubElement(run, qn("rPr"))
|
||||
rfonts = ET.SubElement(rpr, qn("rFonts"))
|
||||
rfonts.set(qn("hint"), "eastAsia")
|
||||
text = ET.SubElement(run, qn("t"))
|
||||
text.text = header_text
|
||||
|
||||
tree.write(header_path, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
|
||||
def resolve_default_header(extracted_dir: str):
|
||||
doc_path = os.path.join(extracted_dir, "word", "document.xml")
|
||||
rels_path = os.path.join(extracted_dir, "word", "_rels", "document.xml.rels")
|
||||
doc_tree = ET.parse(doc_path)
|
||||
doc_root = doc_tree.getroot()
|
||||
sect = first(doc_root, ".//w:body/w:sectPr")
|
||||
if sect is None:
|
||||
return None
|
||||
|
||||
default_rid = None
|
||||
for header_ref in sect.findall("w:headerReference", NS):
|
||||
if header_ref.get(qn("type")) == "default":
|
||||
default_rid = header_ref.get(f"{{{R_NS}}}id")
|
||||
break
|
||||
|
||||
if not default_rid:
|
||||
return None
|
||||
|
||||
rel_tree = ET.parse(rels_path)
|
||||
rel_root = rel_tree.getroot()
|
||||
for rel in rel_root.findall(f"{{{PKG_REL_NS}}}Relationship"):
|
||||
if rel.get("Id") == default_rid:
|
||||
target = rel.get("Target")
|
||||
if target:
|
||||
return os.path.join(extracted_dir, "word", target)
|
||||
return None
|
||||
|
||||
|
||||
def next_rid(rel_root) -> str:
|
||||
max_id = 0
|
||||
for rel in rel_root.findall(f"{{{PKG_REL_NS}}}Relationship"):
|
||||
rel_id = rel.get("Id", "")
|
||||
if rel_id.startswith("rId"):
|
||||
try:
|
||||
max_id = max(max_id, int(rel_id[3:]))
|
||||
except ValueError:
|
||||
continue
|
||||
return f"rId{max_id + 1}"
|
||||
|
||||
|
||||
def inject_keymap_customizations(extracted_dir: str, shortcut_template_path: str | None) -> None:
|
||||
if not shortcut_template_path or not os.path.exists(shortcut_template_path):
|
||||
return
|
||||
|
||||
with zipfile.ZipFile(shortcut_template_path) as template_archive:
|
||||
if "word/customizations.xml" not in template_archive.namelist():
|
||||
return
|
||||
customizations_bytes = template_archive.read("word/customizations.xml")
|
||||
|
||||
word_dir = os.path.join(extracted_dir, "word")
|
||||
os.makedirs(word_dir, exist_ok=True)
|
||||
with open(os.path.join(word_dir, "customizations.xml"), "wb") as handle:
|
||||
handle.write(customizations_bytes)
|
||||
|
||||
rels_path = os.path.join(word_dir, "_rels", "document.xml.rels")
|
||||
rel_tree = ET.parse(rels_path)
|
||||
rel_root = rel_tree.getroot()
|
||||
|
||||
keymap_rel = None
|
||||
for rel in rel_root.findall(f"{{{PKG_REL_NS}}}Relationship"):
|
||||
if rel.get("Type") == KEYMAP_REL_TYPE:
|
||||
keymap_rel = rel
|
||||
break
|
||||
|
||||
if keymap_rel is None:
|
||||
keymap_rel = ET.SubElement(rel_root, f"{{{PKG_REL_NS}}}Relationship")
|
||||
keymap_rel.set("Id", next_rid(rel_root))
|
||||
keymap_rel.set("Type", KEYMAP_REL_TYPE)
|
||||
keymap_rel.set("Target", "customizations.xml")
|
||||
rel_tree.write(rels_path, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
content_types_path = os.path.join(extracted_dir, "[Content_Types].xml")
|
||||
ct_tree = ET.parse(content_types_path)
|
||||
ct_root = ct_tree.getroot()
|
||||
override_tag = f"{{{CONTENT_TYPES_NS}}}Override"
|
||||
override = None
|
||||
for node in ct_root.findall(override_tag):
|
||||
if node.get("PartName") == "/word/customizations.xml":
|
||||
override = node
|
||||
break
|
||||
if override is None:
|
||||
override = ET.SubElement(ct_root, override_tag)
|
||||
override.set("PartName", "/word/customizations.xml")
|
||||
override.set("ContentType", KEYMAP_CONTENT_TYPE)
|
||||
ct_tree.write(content_types_path, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
|
||||
def remove_horizontal_rules(document_path: str) -> None:
|
||||
tree = ET.parse(document_path)
|
||||
root = tree.getroot()
|
||||
hr_tag = f"{{{OFFICE_NS}}}hr"
|
||||
parent_map = {child: parent for parent in root.iter() for child in parent}
|
||||
for paragraph in root.findall(".//" + qn("p")):
|
||||
for pict in paragraph.findall(".//" + qn("pict")):
|
||||
for rect in pict.findall(f".//{{{VML_NS}}}rect"):
|
||||
if rect.get(hr_tag) == "t":
|
||||
parent = parent_map.get(paragraph)
|
||||
if parent is not None:
|
||||
parent.remove(paragraph)
|
||||
break
|
||||
tree.write(document_path, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
|
||||
def patch_doc_defaults(extracted_dir: str, template_path: str) -> None:
|
||||
styles_path = os.path.join(extracted_dir, "word", "styles.xml")
|
||||
if not os.path.exists(styles_path) or not os.path.exists(template_path):
|
||||
return
|
||||
tmpl_tmp = tempfile.mkdtemp(prefix="tmpl-defaults-")
|
||||
try:
|
||||
with zipfile.ZipFile(template_path) as archive:
|
||||
archive.extractall(tmpl_tmp)
|
||||
tmpl_styles_path = os.path.join(tmpl_tmp, "word", "styles.xml")
|
||||
if not os.path.exists(tmpl_styles_path):
|
||||
return
|
||||
tmpl_tree = ET.parse(tmpl_styles_path)
|
||||
tmpl_root = tmpl_tree.getroot()
|
||||
tmpl_defaults = tmpl_root.find(qn("docDefaults"))
|
||||
if tmpl_defaults is None:
|
||||
return
|
||||
gen_tree = ET.parse(styles_path)
|
||||
gen_root = gen_tree.getroot()
|
||||
gen_defaults = gen_root.find(qn("docDefaults"))
|
||||
if gen_defaults is not None:
|
||||
gen_root.remove(gen_defaults)
|
||||
new_defaults = copy.deepcopy(tmpl_defaults)
|
||||
rfonts = new_defaults.find(f".//{qn('rFonts')}")
|
||||
if rfonts is not None:
|
||||
for attr in ("ascii", "hAnsi"):
|
||||
if rfonts.get(qn(attr)) == "Calibri":
|
||||
rfonts.set(qn(attr), "Times New Roman")
|
||||
gen_root.insert(0, new_defaults)
|
||||
gen_tree.write(styles_path, encoding="UTF-8", xml_declaration=True)
|
||||
finally:
|
||||
shutil.rmtree(tmpl_tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def patch_tables(document_path: str) -> None:
|
||||
tree = ET.parse(document_path)
|
||||
root = tree.getroot()
|
||||
for tbl in root.findall(".//" + qn("tbl")):
|
||||
tbl_pr = tbl.find(qn("tblPr"))
|
||||
if tbl_pr is None:
|
||||
continue
|
||||
tbl_style = tbl_pr.find(qn("tblStyle"))
|
||||
if tbl_style is not None:
|
||||
tbl_style.set(qn("val"), "24")
|
||||
tbl_layout = tbl_pr.find(qn("tblLayout"))
|
||||
if tbl_layout is not None:
|
||||
tbl_layout.set(qn("type"), "autofit")
|
||||
tbl_w = tbl_pr.find(qn("tblW"))
|
||||
if tbl_w is not None:
|
||||
tbl_pr.remove(tbl_w)
|
||||
existing_borders = tbl_pr.find(qn("tblBorders"))
|
||||
if existing_borders is not None:
|
||||
tbl_pr.remove(existing_borders)
|
||||
borders = ET.SubElement(tbl_pr, qn("tblBorders"))
|
||||
for side in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
||||
border = ET.SubElement(borders, qn(side))
|
||||
border.set(qn("val"), "single")
|
||||
border.set(qn("color"), "000000")
|
||||
border.set(qn("sz"), "4")
|
||||
border.set(qn("space"), "0")
|
||||
tree.write(document_path, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
|
||||
def patch_numbering(extracted_dir: str) -> None:
|
||||
numbering_path = os.path.join(extracted_dir, "word", "numbering.xml")
|
||||
if not os.path.exists(numbering_path):
|
||||
return
|
||||
tree = ET.parse(numbering_path)
|
||||
root = tree.getroot()
|
||||
for absnum in root.findall(qn("abstractNum")):
|
||||
absid = absnum.get(qn("abstractNumId"), "")
|
||||
for lvl in absnum.findall(qn("lvl")):
|
||||
bullet_level = is_bullet_level(absid, lvl)
|
||||
ordered_level = is_generic_ordered_level(lvl)
|
||||
if not bullet_level and not ordered_level:
|
||||
continue
|
||||
if bullet_level:
|
||||
# Unify bullet glyphs and font so list symbols render consistently in Word.
|
||||
num_fmt = lvl.find(qn("numFmt"))
|
||||
if num_fmt is None:
|
||||
num_fmt = ET.SubElement(lvl, qn("numFmt"))
|
||||
if num_fmt.get(qn("val")) != "bullet":
|
||||
num_fmt.set(qn("val"), "bullet")
|
||||
lvl_text = lvl.find(qn("lvlText"))
|
||||
if lvl_text is None:
|
||||
lvl_text = ET.SubElement(lvl, qn("lvlText"))
|
||||
lvl_text.set(qn("val"), "\uF06C")
|
||||
rpr = lvl.find(qn("rPr"))
|
||||
if rpr is None:
|
||||
rpr = ET.SubElement(lvl, qn("rPr"))
|
||||
rfonts = rpr.find(qn("rFonts"))
|
||||
if rfonts is None:
|
||||
rfonts = ET.SubElement(rpr, qn("rFonts"))
|
||||
rfonts.set(qn("ascii"), "Wingdings")
|
||||
rfonts.set(qn("hAnsi"), "Wingdings")
|
||||
rfonts.set(qn("hint"), "default")
|
||||
ppr = lvl.find(qn("pPr"))
|
||||
if ppr is None:
|
||||
ppr = ET.SubElement(lvl, qn("pPr"))
|
||||
ind = ppr.find(qn("ind"))
|
||||
if ind is None:
|
||||
ind = ET.SubElement(ppr, qn("ind"))
|
||||
try:
|
||||
level = int(lvl.get(qn("ilvl"), "0"))
|
||||
except ValueError:
|
||||
level = 0
|
||||
if bullet_level:
|
||||
left = BULLET_LIST_BASE_LEFT + (level * LIST_LEVEL_STEP)
|
||||
else:
|
||||
left = ORDERED_LIST_BASE_LEFT + (level * LIST_LEVEL_STEP)
|
||||
ind.set(qn("left"), str(left))
|
||||
ind.set(qn("hanging"), str(LIST_HANGING))
|
||||
for attr in ("leftChars", "hangingChars", "firstLine", "firstLineChars"):
|
||||
ind.attrib.pop(qn(attr), None)
|
||||
tree.write(numbering_path, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
|
||||
def ensure_keep_next(ppr) -> None:
|
||||
keep_next = first(ppr, "w:keepNext")
|
||||
if keep_next is None:
|
||||
keep_next = ET.SubElement(ppr, qn("keepNext"))
|
||||
keep_next.set(qn("val"), "1")
|
||||
|
||||
|
||||
def set_zero_first_line_indent(ppr) -> None:
|
||||
ind = first(ppr, "w:ind")
|
||||
if ind is None:
|
||||
ind = ET.SubElement(ppr, qn("ind"))
|
||||
ind.set(qn("firstLine"), "0")
|
||||
ind.set(qn("firstLineChars"), "0")
|
||||
for attr in ("hanging", "hangingChars"):
|
||||
ind.attrib.pop(qn(attr), None)
|
||||
|
||||
|
||||
def patch_layout_constraints(document_path: str, style_name_by_id) -> None:
|
||||
tree = ET.parse(document_path)
|
||||
root = tree.getroot()
|
||||
code_style_names = {"Source Code", "SourceCode", "代码清单"}
|
||||
keep_next_style_names = {"图", "表题1-1"}
|
||||
|
||||
for paragraph in root.findall(".//" + qn("p")):
|
||||
ppr = first(paragraph, "w:pPr")
|
||||
if ppr is None:
|
||||
ppr = ET.Element(qn("pPr"))
|
||||
paragraph.insert(0, ppr)
|
||||
|
||||
pstyle = first(ppr, "w:pStyle")
|
||||
style_name = ""
|
||||
if pstyle is not None:
|
||||
style_name = style_name_by_id.get(pstyle.get(qn("val")), "")
|
||||
|
||||
if style_name in keep_next_style_names:
|
||||
ensure_keep_next(ppr)
|
||||
|
||||
if style_name in code_style_names:
|
||||
ind = first(ppr, "w:ind")
|
||||
if ind is not None:
|
||||
for attr in ("firstLine", "firstLineChars", "hanging", "hangingChars"):
|
||||
ind.attrib.pop(qn(attr), None)
|
||||
if not ind.attrib:
|
||||
ppr.remove(ind)
|
||||
|
||||
for cell in root.findall(".//" + qn("tc")):
|
||||
for paragraph in cell.findall(qn("p")):
|
||||
ppr = first(paragraph, "w:pPr")
|
||||
if ppr is None:
|
||||
ppr = ET.Element(qn("pPr"))
|
||||
paragraph.insert(0, ppr)
|
||||
set_zero_first_line_indent(ppr)
|
||||
|
||||
tree.write(document_path, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) not in {4, 5}:
|
||||
print(
|
||||
"Usage: postprocess_template_docx.py <docx-path> <template-path> <header-text> [shortcut-template]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
docx_path = sys.argv[1]
|
||||
template_path = sys.argv[2]
|
||||
header_text = sys.argv[3]
|
||||
shortcut_template = sys.argv[4] if len(sys.argv) == 5 else None
|
||||
|
||||
if not os.path.exists(docx_path):
|
||||
print(f"Error: file not found: {docx_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
temp_dir = tempfile.mkdtemp(prefix="template-docx-")
|
||||
try:
|
||||
with zipfile.ZipFile(docx_path) as archive:
|
||||
archive.extractall(temp_dir)
|
||||
|
||||
styles_path = os.path.join(temp_dir, "word", "styles.xml")
|
||||
document_path = os.path.join(temp_dir, "word", "document.xml")
|
||||
sanitize_relationship_prefixes(document_path)
|
||||
style_name_by_id = {}
|
||||
if os.path.exists(styles_path):
|
||||
style_name_by_id = patch_styles(styles_path)
|
||||
if os.path.exists(document_path):
|
||||
strip_explicit_body_styles(document_path, style_name_by_id)
|
||||
|
||||
if os.path.exists(document_path):
|
||||
remove_horizontal_rules(document_path)
|
||||
patch_tables(document_path)
|
||||
patch_layout_constraints(document_path, style_name_by_id)
|
||||
|
||||
patch_doc_defaults(temp_dir, template_path)
|
||||
patch_numbering(temp_dir)
|
||||
|
||||
default_header = resolve_default_header(temp_dir)
|
||||
if default_header and os.path.exists(default_header):
|
||||
patch_header(default_header, header_text)
|
||||
inject_keymap_customizations(temp_dir, shortcut_template)
|
||||
|
||||
rebuilt = docx_path + ".tmp"
|
||||
with zipfile.ZipFile(rebuilt, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for root, _, files in os.walk(temp_dir):
|
||||
for filename in files:
|
||||
full_path = os.path.join(root, filename)
|
||||
rel_path = os.path.relpath(full_path, temp_dir)
|
||||
archive.write(full_path, rel_path)
|
||||
shutil.move(rebuilt, docx_path)
|
||||
return 0
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage:
|
||||
render_markdown_with_dotx.sh <source-md> <output-docx> <template-dotx-or-docx> [book-title] [resource-root] [shortcut-template]
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $# -lt 3 || $# -gt 6 ]] && usage
|
||||
|
||||
SOURCE_MD="$1"
|
||||
OUTPUT_DOCX="$2"
|
||||
TEMPLATE_DOC="$3"
|
||||
BOOK_TITLE="${4:-}"
|
||||
SHORTCUT_TEMPLATE="${6:-}"
|
||||
SOURCE_NAME="$(basename "$SOURCE_MD")"
|
||||
|
||||
if [[ ! -f "$SOURCE_MD" ]]; then
|
||||
echo "Error: source markdown not found: $SOURCE_MD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TEMPLATE_DOC" ]]; then
|
||||
echo "Error: template file not found: $TEMPLATE_DOC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v pandoc >/dev/null 2>&1; then
|
||||
echo "Error: pandoc is not installed or not in PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
template_has_keymap_customizations() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(sys.argv[1]) as zf:
|
||||
raise SystemExit(0 if "word/customizations.xml" in zf.namelist() else 1)
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
SOURCE_DIR="$(cd "$(dirname "$SOURCE_MD")" && pwd -P)"
|
||||
SOURCE_PARENT="$(cd "$SOURCE_DIR/.." && pwd -P)"
|
||||
RESOURCE_ROOT="${5:-$SOURCE_PARENT}"
|
||||
|
||||
if [[ -z "$SHORTCUT_TEMPLATE" ]] && template_has_keymap_customizations "$TEMPLATE_DOC"; then
|
||||
SHORTCUT_TEMPLATE="$TEMPLATE_DOC"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$OUTPUT_DOCX")"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
NORMALIZED_MD="$TMP_DIR/normalized.md"
|
||||
TMP_RESOURCES_DIR="$TMP_DIR/resources"
|
||||
|
||||
perl -0pe '
|
||||
s{^图:([^\n!]+)!\[\[([^]|]+)\|[0-9]+\]\]}{\n\n图:$1}mg;
|
||||
s{!\[\[([^]|]+)\|[0-9]+\]\]}{}g;
|
||||
s{!\[\[([^]|]+)\]\]}{}g;
|
||||
s{^\*([图表]:[^\n*]+)\*$}{$1}mg;
|
||||
s{(?m)^(!\[[^\n]*\]\([^\n]+\))$}{\n$1\n}g;
|
||||
s{(?m)^([图表]:[^\n]+)$}{\n$1\n}g;
|
||||
s{(?m)^(\*\*[^\n*]+\*\*)$}{\n$1\n}g;
|
||||
s{^---$}{}mg;
|
||||
s{^(#{3,})\s+\d+\.\d+(?:\.\d+)?\s+(小结|可执行清单)}{$1 $2}mg;
|
||||
s{\n{3,}}{\n\n}g;
|
||||
' "$SOURCE_MD" > "$NORMALIZED_MD"
|
||||
|
||||
python3 "$SCRIPT_DIR/render_mermaid_blocks_for_docx.py" \
|
||||
"$NORMALIZED_MD" \
|
||||
"$SOURCE_NAME" \
|
||||
"$RESOURCE_ROOT" \
|
||||
"$TMP_RESOURCES_DIR"
|
||||
|
||||
# Auto-fix missing table/figure captions before conversion
|
||||
python3 "$SCRIPT_DIR/validate_captions.py" fix "$NORMALIZED_MD"
|
||||
|
||||
CHAPTER_TITLE="$(sed -n 's/^# //p' "$NORMALIZED_MD" | head -n 1)"
|
||||
CHAPTER_PREFIX="$(printf '%s\n' "$CHAPTER_TITLE" | perl -ne 'print "$1\n" if /(第[0-9]+章)/')"
|
||||
|
||||
HEADER_TEXT="${CHAPTER_TITLE:-Markdown Export}"
|
||||
if [[ -n "$BOOK_TITLE" ]]; then
|
||||
HEADER_TEXT="《${BOOK_TITLE}》"
|
||||
fi
|
||||
if [[ -n "$BOOK_TITLE" && -n "$CHAPTER_PREFIX" ]]; then
|
||||
HEADER_TEXT="${HEADER_TEXT}${CHAPTER_PREFIX}"
|
||||
fi
|
||||
|
||||
RESOURCE_PATH="$TMP_DIR:$TMP_RESOURCES_DIR:$SOURCE_DIR:$SOURCE_PARENT:$RESOURCE_ROOT:$RESOURCE_ROOT/resources"
|
||||
|
||||
pandoc "$NORMALIZED_MD" \
|
||||
-f markdown \
|
||||
-t docx \
|
||||
--reference-doc="$TEMPLATE_DOC" \
|
||||
--lua-filter="$SCRIPT_DIR/template_style_filter.lua" \
|
||||
--resource-path="$RESOURCE_PATH" \
|
||||
-o "$OUTPUT_DOCX"
|
||||
|
||||
"$SCRIPT_DIR/postprocess_template_docx.py" "$OUTPUT_DOCX" "$TEMPLATE_DOC" "$HEADER_TEXT" "$SHORTCUT_TEMPLATE"
|
||||
|
||||
printf 'OK\t%s\n' "$OUTPUT_DOCX"
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MERMAID_FENCE_RE = re.compile(r"^```\s*mermaid\b", re.IGNORECASE)
|
||||
|
||||
|
||||
def slugify(text: str) -> str:
|
||||
slug = re.sub(r"[^0-9A-Za-z\u4e00-\u9fff._-]+", "-", text)
|
||||
slug = slug.strip("-._")
|
||||
return slug or "diagram"
|
||||
|
||||
|
||||
def render_mermaid(
|
||||
*,
|
||||
code: str,
|
||||
out_path: Path,
|
||||
theme: str = "neutral",
|
||||
width: int = 1200,
|
||||
height: int = 900,
|
||||
scale: float = 2.0,
|
||||
) -> None:
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix="docx-mermaid-") as tmpdir:
|
||||
tmp_mmd = Path(tmpdir) / "diagram.mmd"
|
||||
tmp_mmd.write_text(code, encoding="utf-8")
|
||||
cmd = [
|
||||
"npx",
|
||||
"-y",
|
||||
"@mermaid-js/mermaid-cli",
|
||||
"-i",
|
||||
str(tmp_mmd),
|
||||
"-o",
|
||||
str(out_path),
|
||||
"--outputFormat",
|
||||
"png",
|
||||
"--theme",
|
||||
theme,
|
||||
"--backgroundColor",
|
||||
"white",
|
||||
"--width",
|
||||
str(width),
|
||||
"--height",
|
||||
str(height),
|
||||
"--scale",
|
||||
str(scale),
|
||||
"-q",
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
stderr = (proc.stderr or "").strip()
|
||||
stdout = (proc.stdout or "").strip()
|
||||
detail = stderr or stdout or str(proc.returncode)
|
||||
raise RuntimeError(f"Mermaid render failed for {out_path.name}: {detail}")
|
||||
|
||||
|
||||
def process_markdown(md_path: Path, source_name: str, temp_resources_dir: Path) -> int:
|
||||
lines = md_path.read_text(encoding="utf-8").splitlines()
|
||||
out_lines: list[str] = []
|
||||
mermaid_count = 0
|
||||
rendered = 0
|
||||
temp_resources_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if MERMAID_FENCE_RE.match(line.strip()):
|
||||
j = i + 1
|
||||
code_lines: list[str] = []
|
||||
while j < len(lines) and not lines[j].strip().startswith("```"):
|
||||
code_lines.append(lines[j])
|
||||
j += 1
|
||||
if j >= len(lines):
|
||||
raise RuntimeError(f"Unclosed mermaid block in {md_path}")
|
||||
|
||||
mermaid_count += 1
|
||||
out_name = f"{slugify(Path(source_name).stem)}-mermaid-{mermaid_count:02d}.png"
|
||||
out_path = temp_resources_dir / out_name
|
||||
render_mermaid(code="\n".join(code_lines).strip() + "\n", out_path=out_path)
|
||||
|
||||
out_lines.append(f"")
|
||||
rendered += 1
|
||||
i = j + 1
|
||||
continue
|
||||
|
||||
out_lines.append(line)
|
||||
i += 1
|
||||
|
||||
md_path.write_text("\n".join(out_lines) + "\n", encoding="utf-8")
|
||||
return rendered
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render Mermaid code blocks in Markdown to PNG images for DOCX export."
|
||||
)
|
||||
parser.add_argument("markdown_path", help="Normalized Markdown file to rewrite in place.")
|
||||
parser.add_argument("source_name", help="Original Markdown basename, used for output names.")
|
||||
parser.add_argument(
|
||||
"resource_root",
|
||||
help="Compatibility argument; existing image lookup is handled by the calling script.",
|
||||
)
|
||||
parser.add_argument("temp_resources_dir", help="Temporary resources directory for generated diagrams.")
|
||||
args = parser.parse_args()
|
||||
|
||||
rendered = process_markdown(
|
||||
md_path=Path(args.markdown_path),
|
||||
source_name=args.source_name,
|
||||
temp_resources_dir=Path(args.temp_resources_dir),
|
||||
)
|
||||
print(f"MERMAID_OK {Path(args.markdown_path)} rendered={rendered}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,170 @@
|
||||
local stringify = pandoc.utils.stringify
|
||||
|
||||
local function trim(text)
|
||||
return (text:gsub("^%s+", ""):gsub("%s+$", ""))
|
||||
end
|
||||
|
||||
local function wrap_para(style_name, para)
|
||||
return pandoc.Div({ para }, pandoc.Attr("", {}, { { "custom-style", style_name } }))
|
||||
end
|
||||
|
||||
local function para_from_markdown(text)
|
||||
local doc = pandoc.read(text, "markdown")
|
||||
if #doc.blocks > 0 and doc.blocks[1].t == "Para" then
|
||||
return doc.blocks[1]
|
||||
end
|
||||
return pandoc.Para({ pandoc.Str(text) })
|
||||
end
|
||||
|
||||
local function image_only_para(block)
|
||||
return block.t == "Para" and #block.content == 1 and block.content[1].t == "Image"
|
||||
end
|
||||
|
||||
local function figure_to_image_para(block)
|
||||
if block.t ~= "Figure" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local first = block.content and block.content[1] or nil
|
||||
if not first then
|
||||
return nil
|
||||
end
|
||||
|
||||
if (first.t == "Para" or first.t == "Plain") and #first.content == 1 and first.content[1].t == "Image" then
|
||||
return pandoc.Para({ first.content[1] })
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function normalize_serial(num)
|
||||
return (num:gsub("%.", "-"))
|
||||
end
|
||||
|
||||
local function ensure_sentence(text)
|
||||
if text == "" then
|
||||
return text
|
||||
end
|
||||
|
||||
if text:match("[。!?%.%!%?]$") then
|
||||
return text
|
||||
end
|
||||
|
||||
return text .. "。"
|
||||
end
|
||||
|
||||
local function parse_caption(text, kind)
|
||||
-- Try with fullwidth colon first, then without.
|
||||
-- Cannot use :? because Lua ? applies to a single byte, not a multi-byte char.
|
||||
local num, rest = text:match("^" .. kind .. ":%s*([0-9]+[%.%-][0-9]+)%s+(.+)$")
|
||||
if not num then
|
||||
num, rest = text:match("^" .. kind .. "%s*([0-9]+[%.%-][0-9]+)%s+(.+)$")
|
||||
end
|
||||
if not num then
|
||||
return nil
|
||||
end
|
||||
|
||||
rest = trim(rest)
|
||||
local title, description = rest:match("^(.-)。(.*)$")
|
||||
if not title or title == "" then
|
||||
title = rest
|
||||
description = ""
|
||||
end
|
||||
|
||||
return {
|
||||
number = normalize_serial(num),
|
||||
title = trim(title),
|
||||
description = trim(description or ""),
|
||||
label = kind .. normalize_serial(num) .. " " .. trim(title),
|
||||
}
|
||||
end
|
||||
|
||||
local function build_figure_explanation(fig)
|
||||
if fig.description == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local description = ensure_sentence(fig.description)
|
||||
if description:match("^如图") then
|
||||
return para_from_markdown(description)
|
||||
end
|
||||
|
||||
return para_from_markdown("如图" .. fig.number .. "所示," .. description)
|
||||
end
|
||||
|
||||
local function is_note_text(text)
|
||||
return text:match("^注:")
|
||||
or text:match("^注意:")
|
||||
or text:match("^关键注意:")
|
||||
or text:match("^⚠️%s*关键注意:")
|
||||
end
|
||||
|
||||
local function transform_para(block)
|
||||
local text = trim(stringify(block))
|
||||
|
||||
if image_only_para(block) then
|
||||
return { wrap_para("图", block) }
|
||||
end
|
||||
|
||||
local fig = parse_caption(text, "图")
|
||||
if fig then
|
||||
return { wrap_para("图题", para_from_markdown(fig.label)) }
|
||||
end
|
||||
|
||||
local tbl = parse_caption(text, "表")
|
||||
if tbl then
|
||||
return { wrap_para("表题1-1", para_from_markdown(tbl.label)) }
|
||||
end
|
||||
|
||||
if is_note_text(text) then
|
||||
return { wrap_para("注意", para_from_markdown(text)) }
|
||||
end
|
||||
|
||||
return { block }
|
||||
end
|
||||
|
||||
function Blocks(blocks)
|
||||
local out = {}
|
||||
local i = 1
|
||||
|
||||
while i <= #blocks do
|
||||
local block = blocks[i]
|
||||
local next_block = blocks[i + 1]
|
||||
local image_block = nil
|
||||
|
||||
if image_only_para(block) then
|
||||
image_block = block
|
||||
else
|
||||
image_block = figure_to_image_para(block)
|
||||
end
|
||||
|
||||
if image_block and next_block and next_block.t == "Para" then
|
||||
local fig = parse_caption(trim(stringify(next_block)), "图")
|
||||
if fig then
|
||||
local explanation = build_figure_explanation(fig)
|
||||
if explanation then
|
||||
table.insert(out, explanation)
|
||||
end
|
||||
table.insert(out, wrap_para("图", image_block))
|
||||
table.insert(out, wrap_para("图题", para_from_markdown(fig.label)))
|
||||
i = i + 2
|
||||
else
|
||||
table.insert(out, wrap_para("图", image_block))
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
if image_block then
|
||||
table.insert(out, wrap_para("图", image_block))
|
||||
elseif block.t == "Para" then
|
||||
for _, transformed in ipairs(transform_para(block)) do
|
||||
table.insert(out, transformed)
|
||||
end
|
||||
else
|
||||
table.insert(out, block)
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
@@ -0,0 +1,597 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate and auto-fix table/figure captions in markdown or docx.
|
||||
|
||||
Modes:
|
||||
validate_captions.py pre <source.md> — check only
|
||||
validate_captions.py fix <source.md> — auto-insert missing captions, write in-place
|
||||
validate_captions.py post <output.docx> — check generated docx
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
EXPECTED_ORDERED_LEFT = "800"
|
||||
EXPECTED_BULLET_LEFT = "840"
|
||||
EXPECTED_BULLET_HANGING = "420"
|
||||
EXPECTED_LIST_STEP = "420"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _extract_chapter(lines: list[str]) -> str | None:
|
||||
for line in lines:
|
||||
m = re.match(r"^#\s+第(\d+)章", line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def _qn(tag: str) -> str:
|
||||
return f"{{{W}}}{tag}"
|
||||
|
||||
|
||||
TABLE_CAPTION_RE = re.compile(r"^\*?表[::]?\s*(\d+)[-.]\s*(\d+)\s+\S.*\*?$")
|
||||
FIGURE_CAPTION_RE = re.compile(r"^\*?图\s*(\d+)[-.]\s*(\d+)\s+\S.*\*?$")
|
||||
FIGURE_CAPTION_ALT_RE = re.compile(r"^\*?图[::]?\s*(\d+)[-.]\s*(\d+)\s+\S.*\*?$")
|
||||
GENERIC_ORDERED_NUMFMTS = {"decimal", "lowerLetter", "upperLetter", "lowerRoman", "upperRoman"}
|
||||
SPECIAL_NUMBERING_PREFIXES = ("表", "图", "代码清单")
|
||||
|
||||
|
||||
def _is_generic_ordered_level(lvl: ET.Element) -> bool:
|
||||
num_fmt = lvl.find(f"{{{W}}}numFmt")
|
||||
if num_fmt is None or num_fmt.get(f"{{{W}}}val") not in GENERIC_ORDERED_NUMFMTS:
|
||||
return False
|
||||
lvl_text = lvl.find(f"{{{W}}}lvlText")
|
||||
if lvl_text is None:
|
||||
return False
|
||||
value = lvl_text.get(f"{{{W}}}val") or ""
|
||||
if "%" not in value:
|
||||
return False
|
||||
if any(prefix in value for prefix in SPECIAL_NUMBERING_PREFIXES):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _find_table_blocks(lines: list[str]) -> list[tuple[int, int, str]]:
|
||||
"""Return (start, end, header_line) for each contiguous table block."""
|
||||
blocks: list[tuple[int, int, str]] = []
|
||||
in_table = False
|
||||
table_start = 0
|
||||
header = ""
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("|") and "|" in stripped[1:]:
|
||||
if not in_table:
|
||||
in_table = True
|
||||
table_start = i
|
||||
header = stripped
|
||||
else:
|
||||
if in_table:
|
||||
blocks.append((table_start, i - 1, header))
|
||||
in_table = False
|
||||
if in_table:
|
||||
blocks.append((table_start, len(lines) - 1, header))
|
||||
return blocks
|
||||
|
||||
|
||||
def _find_figure_items(lines: list[str]) -> list[tuple[int, int, str]]:
|
||||
"""Return (start_line, end_line, type) for images and mermaid blocks."""
|
||||
items: list[tuple[int, int, str]] = []
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(r"^!\[", line.strip()):
|
||||
items.append((i, i, "image"))
|
||||
in_code = False
|
||||
code_lang = ""
|
||||
code_start = 0
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("```"):
|
||||
if not in_code:
|
||||
in_code = True
|
||||
code_lang = stripped[3:].strip().lower()
|
||||
code_start = i
|
||||
else:
|
||||
if code_lang == "mermaid":
|
||||
items.append((code_start, i, "mermaid"))
|
||||
in_code = False
|
||||
code_lang = ""
|
||||
items.sort(key=lambda x: x[0])
|
||||
return items
|
||||
|
||||
|
||||
def _has_caption_before(lines: list[str], start: int, pattern: re.Pattern) -> tuple[bool, tuple[str, str] | None]:
|
||||
for look_back in range(1, 4):
|
||||
idx = start - look_back
|
||||
if idx < 0:
|
||||
break
|
||||
prev = lines[idx].strip()
|
||||
if prev == "":
|
||||
continue
|
||||
m = pattern.match(prev)
|
||||
if m:
|
||||
return True, (m.group(1), m.group(2))
|
||||
return False, None
|
||||
return False, None
|
||||
|
||||
|
||||
def _has_caption_after(lines: list[str], search_start: int, pattern: re.Pattern, alt_pattern: re.Pattern | None = None) -> tuple[bool, tuple[str, str] | None]:
|
||||
for idx in range(search_start, min(search_start + 4, len(lines))):
|
||||
nxt = lines[idx].strip()
|
||||
if nxt == "":
|
||||
continue
|
||||
m = pattern.match(nxt)
|
||||
if m:
|
||||
return True, (m.group(1), m.group(2))
|
||||
if alt_pattern:
|
||||
m2 = alt_pattern.match(nxt)
|
||||
if m2:
|
||||
return True, (m2.group(1), m2.group(2))
|
||||
return False, None
|
||||
return False, None
|
||||
|
||||
|
||||
def _derive_table_title(lines: list[str], start: int, header: str) -> str:
|
||||
"""Derive a short table title from the header row columns."""
|
||||
# Extract column names from header row: | Col1 | Col2 | ...
|
||||
cols = [c.strip() for c in header.split("|") if c.strip()]
|
||||
if len(cols) >= 2:
|
||||
return "、".join(cols[:3]) + ("等" if len(cols) > 3 else "")
|
||||
# Fallback: use preceding paragraph
|
||||
for look_back in range(1, 5):
|
||||
idx = start - look_back
|
||||
if idx < 0:
|
||||
break
|
||||
prev = lines[idx].strip()
|
||||
if prev and not prev.startswith("|") and not prev.startswith("#"):
|
||||
# Truncate to first clause
|
||||
for sep in (":", "。", ",", ":"):
|
||||
if sep in prev:
|
||||
prev = prev[: prev.index(sep)]
|
||||
break
|
||||
if len(prev) > 30:
|
||||
prev = prev[:30]
|
||||
return prev
|
||||
return "数据总览"
|
||||
|
||||
|
||||
def _derive_figure_title(lines: list[str], start: int, end: int, fig_type: str) -> str:
|
||||
"""Derive a short figure title from surrounding context."""
|
||||
# Look at line before
|
||||
for look_back in range(1, 5):
|
||||
idx = start - look_back
|
||||
if idx < 0:
|
||||
break
|
||||
prev = lines[idx].strip()
|
||||
if prev and not prev.startswith("```") and not prev.startswith("#"):
|
||||
# Truncate
|
||||
for sep in (":", "。", ","):
|
||||
if sep in prev:
|
||||
prev = prev[: prev.index(sep)]
|
||||
break
|
||||
if len(prev) > 30:
|
||||
prev = prev[:30]
|
||||
return prev
|
||||
return "系统架构图"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def pre_check(md_path: str) -> list[str]:
|
||||
with open(md_path, encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
issues: list[str] = []
|
||||
chapter_num = _extract_chapter(lines) or "?"
|
||||
if chapter_num == "?":
|
||||
issues.append("WARN: Cannot extract chapter number from H1 heading")
|
||||
|
||||
table_blocks = _find_table_blocks(lines)
|
||||
for idx, (start, end, header) in enumerate(table_blocks, 1):
|
||||
found, nums = _has_caption_before(lines, start, TABLE_CAPTION_RE)
|
||||
if not found:
|
||||
issues.append(
|
||||
f"ERROR: Table at line {start + 1} missing caption. "
|
||||
f"Expected: 表{chapter_num}-{idx} <title>"
|
||||
)
|
||||
else:
|
||||
if nums[0] != chapter_num:
|
||||
issues.append(f"WARN: Table at line {start + 1}: chapter {nums[0]}, expected {chapter_num}")
|
||||
if nums[1] != str(idx):
|
||||
issues.append(f"WARN: Table at line {start + 1}: 表{nums[0]}-{nums[1]}, expected seq {idx}")
|
||||
|
||||
figure_items = _find_figure_items(lines)
|
||||
for idx, (start, end, fig_type) in enumerate(figure_items, 1):
|
||||
search_start = end + 1
|
||||
found, nums = _has_caption_after(lines, search_start, FIGURE_CAPTION_RE, FIGURE_CAPTION_ALT_RE)
|
||||
if not found:
|
||||
found, nums = _has_caption_before(lines, start, FIGURE_CAPTION_RE)
|
||||
if not found:
|
||||
issues.append(
|
||||
f"ERROR: {fig_type.capitalize()} at line {start + 1} missing caption. "
|
||||
f"Expected: 图{chapter_num}-{idx} <title>"
|
||||
)
|
||||
else:
|
||||
if nums[0] != chapter_num:
|
||||
issues.append(f"WARN: Figure near line {start + 1}: chapter {nums[0]}, expected {chapter_num}")
|
||||
if nums[1] != str(idx):
|
||||
issues.append(f"WARN: Figure near line {start + 1}: 图{nums[0]}-{nums[1]}, expected seq {idx}")
|
||||
|
||||
if not issues:
|
||||
issues.append(
|
||||
f"OK: {len(table_blocks)} tables, {len(figure_items)} figures — "
|
||||
f"all captions present and correctly numbered"
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auto-fix
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def auto_fix(md_path: str) -> list[str]:
|
||||
"""Insert missing captions into markdown. Returns log of changes."""
|
||||
with open(md_path, encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
chapter_num = _extract_chapter(lines) or "0"
|
||||
log: list[str] = []
|
||||
|
||||
# We need to process from bottom to top so that line insertions
|
||||
# don't shift indices of items not yet processed.
|
||||
|
||||
# Collect all items that need fixing
|
||||
insertions: list[tuple[int, str]] = [] # (line_index, caption_text)
|
||||
|
||||
# --- Tables: caption goes BEFORE the table ---
|
||||
table_blocks = _find_table_blocks(lines)
|
||||
for idx, (start, end, header) in enumerate(table_blocks, 1):
|
||||
found, _ = _has_caption_before(lines, start, TABLE_CAPTION_RE)
|
||||
if not found:
|
||||
title = _derive_table_title(lines, start, header)
|
||||
caption = f"表{chapter_num}-{idx} {title}"
|
||||
insertions.append((start, caption))
|
||||
log.append(f"FIXED: Inserted '{caption}' before line {start + 1}")
|
||||
|
||||
# --- Figures: caption goes AFTER the figure ---
|
||||
figure_items = _find_figure_items(lines)
|
||||
for idx, (start, end, fig_type) in enumerate(figure_items, 1):
|
||||
search_start = end + 1
|
||||
found, _ = _has_caption_after(lines, search_start, FIGURE_CAPTION_RE, FIGURE_CAPTION_ALT_RE)
|
||||
if not found:
|
||||
found, _ = _has_caption_before(lines, start, FIGURE_CAPTION_RE)
|
||||
if not found:
|
||||
title = _derive_figure_title(lines, start, end, fig_type)
|
||||
caption = f"图{chapter_num}-{idx} {title}"
|
||||
insert_at = end + 1
|
||||
insertions.append((insert_at, caption))
|
||||
log.append(f"FIXED: Inserted '{caption}' after line {end + 1}")
|
||||
|
||||
if not insertions:
|
||||
log.append("OK: No missing captions to fix")
|
||||
return log
|
||||
|
||||
# Sort by line index descending so insertions don't shift each other
|
||||
insertions.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
for insert_at, caption in insertions:
|
||||
# Insert: blank line + caption + blank line
|
||||
new_lines = ["\n", caption + "\n", "\n"]
|
||||
lines[insert_at:insert_at] = new_lines
|
||||
|
||||
with open(md_path, "w", encoding="utf-8") as f:
|
||||
f.writelines(lines)
|
||||
|
||||
log.append(f"DONE: {len(insertions)} captions inserted into {md_path}")
|
||||
return log
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Post-check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def post_check(docx_path: str) -> list[str]:
|
||||
VML = "urn:schemas-microsoft-com:vml"
|
||||
O = "urn:schemas-microsoft-com:office:office"
|
||||
|
||||
issues: list[str] = []
|
||||
|
||||
with zipfile.ZipFile(docx_path) as z:
|
||||
doc = ET.fromstring(z.read("word/document.xml"))
|
||||
styles = ET.fromstring(z.read("word/styles.xml"))
|
||||
numbering = ET.fromstring(z.read("word/numbering.xml"))
|
||||
|
||||
style_name_by_id: dict[str, str] = {}
|
||||
style_by_name: dict[str, ET.Element] = {}
|
||||
for style in styles.findall(f"{{{W}}}style"):
|
||||
sid = style.get(f"{{{W}}}styleId")
|
||||
name_el = style.find(f"{{{W}}}name")
|
||||
name = name_el.get(f"{{{W}}}val") if name_el is not None else sid
|
||||
if sid:
|
||||
style_name_by_id[sid] = name
|
||||
if name:
|
||||
style_by_name[name] = style
|
||||
|
||||
# 1. Compact style
|
||||
compact = sum(
|
||||
1
|
||||
for p in doc.findall(f".//{{{W}}}p")
|
||||
if (ppr := p.find(f"{{{W}}}pPr")) is not None
|
||||
and (ps := ppr.find(f"{{{W}}}pStyle")) is not None
|
||||
and ps.get(f"{{{W}}}val") == "Compact"
|
||||
)
|
||||
if compact > 0:
|
||||
issues.append(f"ERROR: {compact} paragraphs with undefined 'Compact' style")
|
||||
|
||||
# 2. VML horizontal rules
|
||||
hr = sum(
|
||||
1
|
||||
for r in doc.findall(f".//{{{VML}}}rect")
|
||||
if r.get(f"{{{O}}}hr") == "t"
|
||||
)
|
||||
if hr > 0:
|
||||
issues.append(f"ERROR: {hr} VML horizontal rules (ugly dividers)")
|
||||
|
||||
# 3. Fonts
|
||||
defaults = styles.find(f"{{{W}}}docDefaults")
|
||||
if defaults is not None:
|
||||
rf = defaults.find(f".//{{{W}}}rFonts")
|
||||
if rf is not None:
|
||||
ascii_f = rf.get(f"{{{W}}}ascii", "?")
|
||||
ea_f = rf.get(f"{{{W}}}eastAsia", "?")
|
||||
if ascii_f == "Calibri":
|
||||
issues.append("WARN: docDefaults ascii font is Calibri, expected Times New Roman")
|
||||
if ea_f != "宋体":
|
||||
issues.append(f"WARN: docDefaults eastAsia font is {ea_f}, expected 宋体")
|
||||
|
||||
# 4. First-line indent (Normal style or docDefaults)
|
||||
has_indent = False
|
||||
for style in styles.findall(f"{{{W}}}style"):
|
||||
name_el = style.find(f"{{{W}}}name")
|
||||
if name_el is not None and name_el.get(f"{{{W}}}val") == "Normal":
|
||||
ppr = style.find(f"{{{W}}}pPr")
|
||||
if ppr is not None:
|
||||
ind = ppr.find(f"{{{W}}}ind")
|
||||
if ind is not None and ind.get(f"{{{W}}}firstLine"):
|
||||
has_indent = True
|
||||
break
|
||||
if not has_indent and defaults is not None:
|
||||
ppr_d = defaults.find(f".//{{{W}}}pPrDefault")
|
||||
if ppr_d is not None:
|
||||
ppr = ppr_d.find(f"{{{W}}}pPr")
|
||||
if ppr is not None:
|
||||
ind = ppr.find(f"{{{W}}}ind")
|
||||
if ind is not None and ind.get(f"{{{W}}}firstLine"):
|
||||
has_indent = True
|
||||
if not has_indent:
|
||||
issues.append("WARN: No first-line indent in Normal style or docDefaults")
|
||||
|
||||
# 5. Table borders
|
||||
tables = doc.findall(f".//{{{W}}}tbl")
|
||||
tables_no_borders = 0
|
||||
for tbl in tables:
|
||||
tpr = tbl.find(f"{{{W}}}tblPr")
|
||||
has_tbl_borders = tpr is not None and tpr.find(f"{{{W}}}tblBorders") is not None
|
||||
has_cell_borders = any(
|
||||
tc.find(f"{{{W}}}tcPr") is not None
|
||||
and tc.find(f"{{{W}}}tcPr").find(f"{{{W}}}tcBorders") is not None
|
||||
for tc in tbl.findall(f".//{{{W}}}tc")
|
||||
)
|
||||
if not has_tbl_borders and not has_cell_borders:
|
||||
tables_no_borders += 1
|
||||
if tables_no_borders > 0:
|
||||
issues.append(f"ERROR: {tables_no_borders}/{len(tables)} tables missing borders")
|
||||
|
||||
# 6. Keep-with-next for figure images and table captions
|
||||
image_keep_next_missing = 0
|
||||
table_caption_keep_next_missing = 0
|
||||
for p in doc.findall(f".//{{{W}}}p"):
|
||||
ppr = p.find(f"{{{W}}}pPr")
|
||||
if ppr is None:
|
||||
continue
|
||||
ps = ppr.find(f"{{{W}}}pStyle")
|
||||
sid = ps.get(f"{{{W}}}val") if ps is not None else None
|
||||
style_name = style_name_by_id.get(sid, sid or "")
|
||||
has_keep_next = ppr.find(f"{{{W}}}keepNext") is not None
|
||||
if style_name == "图" and not has_keep_next:
|
||||
image_keep_next_missing += 1
|
||||
if style_name == "表题1-1" and not has_keep_next:
|
||||
table_caption_keep_next_missing += 1
|
||||
if image_keep_next_missing > 0:
|
||||
issues.append(f"ERROR: {image_keep_next_missing} image paragraphs missing keep-with-next")
|
||||
if table_caption_keep_next_missing > 0:
|
||||
issues.append(f"ERROR: {table_caption_keep_next_missing} table captions missing keep-with-next")
|
||||
|
||||
# 7. Code block first-line indent
|
||||
code_style = None
|
||||
for style_name in ("Source Code", "SourceCode", "代码清单"):
|
||||
candidate = style_by_name.get(style_name)
|
||||
if candidate is not None:
|
||||
code_style = candidate
|
||||
break
|
||||
if code_style is not None:
|
||||
ppr = code_style.find(f"{{{W}}}pPr")
|
||||
if ppr is not None:
|
||||
ind = ppr.find(f"{{{W}}}ind")
|
||||
if ind is None:
|
||||
issues.append("ERROR: Code block style is missing explicit zero first-line indent override")
|
||||
else:
|
||||
if ind.get(f"{{{W}}}firstLine") != "0" or ind.get(f"{{{W}}}firstLineChars") != "0":
|
||||
issues.append("ERROR: Code block style still has first-line indentation")
|
||||
if ind.get(f"{{{W}}}hanging") or ind.get(f"{{{W}}}hangingChars"):
|
||||
issues.append("ERROR: Code block style still has hanging indentation")
|
||||
|
||||
# 8. List indentation should align with Chinese body-text first-line indent
|
||||
bullet_indent_issues = 0
|
||||
ordered_indent_issues = 0
|
||||
num_to_abs: dict[str, str] = {}
|
||||
abstract_lookup: dict[str, ET.Element] = {}
|
||||
for num in numbering.findall(f"{{{W}}}num"):
|
||||
num_id = num.get(f"{{{W}}}numId")
|
||||
abs_el = num.find(f"{{{W}}}abstractNumId")
|
||||
abs_id = abs_el.get(f"{{{W}}}val") if abs_el is not None else None
|
||||
if num_id and abs_id:
|
||||
num_to_abs[num_id] = abs_id
|
||||
for absnum in numbering.findall(f"{{{W}}}abstractNum"):
|
||||
abs_id = absnum.get(f"{{{W}}}abstractNumId")
|
||||
if abs_id:
|
||||
abstract_lookup[abs_id] = absnum
|
||||
|
||||
for p in doc.findall(f".//{{{W}}}p"):
|
||||
ppr = p.find(f"{{{W}}}pPr")
|
||||
if ppr is None:
|
||||
continue
|
||||
numpr = ppr.find(f"{{{W}}}numPr")
|
||||
if numpr is None:
|
||||
continue
|
||||
num_id_el = numpr.find(f"{{{W}}}numId")
|
||||
ilvl_el = numpr.find(f"{{{W}}}ilvl")
|
||||
if num_id_el is None:
|
||||
continue
|
||||
abs_id = num_to_abs.get(num_id_el.get(f"{{{W}}}val", ""))
|
||||
if not abs_id:
|
||||
continue
|
||||
absnum = abstract_lookup.get(abs_id)
|
||||
if absnum is None:
|
||||
continue
|
||||
ilvl = ilvl_el.get(f"{{{W}}}val", "0") if ilvl_el is not None else "0"
|
||||
try:
|
||||
ilvl_num = int(ilvl)
|
||||
except ValueError:
|
||||
ilvl_num = 0
|
||||
lvl = absnum.find(f"{{{W}}}lvl[@{{{W}}}ilvl='{ilvl}']")
|
||||
if lvl is None:
|
||||
continue
|
||||
ind = lvl.find(f"{{{W}}}pPr/{{{W}}}ind")
|
||||
num_fmt = lvl.find(f"{{{W}}}numFmt")
|
||||
is_bullet = num_fmt is not None and num_fmt.get(f"{{{W}}}val") == "bullet"
|
||||
is_ordered = _is_generic_ordered_level(lvl)
|
||||
if not is_bullet and not is_ordered:
|
||||
continue
|
||||
if ind is None:
|
||||
if is_bullet:
|
||||
bullet_indent_issues += 1
|
||||
else:
|
||||
ordered_indent_issues += 1
|
||||
continue
|
||||
if is_bullet:
|
||||
expected_left = str(int(EXPECTED_BULLET_LEFT) + ilvl_num * int(EXPECTED_LIST_STEP))
|
||||
if (
|
||||
ind.get(f"{{{W}}}left") != expected_left
|
||||
or ind.get(f"{{{W}}}hanging") != EXPECTED_BULLET_HANGING
|
||||
):
|
||||
bullet_indent_issues += 1
|
||||
else:
|
||||
expected_left = str(int(EXPECTED_ORDERED_LEFT) + ilvl_num * int(EXPECTED_LIST_STEP))
|
||||
if (
|
||||
ind.get(f"{{{W}}}left") != expected_left
|
||||
or ind.get(f"{{{W}}}hanging") != EXPECTED_BULLET_HANGING
|
||||
):
|
||||
ordered_indent_issues += 1
|
||||
if bullet_indent_issues > 0:
|
||||
issues.append(
|
||||
"ERROR: "
|
||||
f"{bullet_indent_issues} bullet list paragraphs still use over-indented list geometry "
|
||||
f"(expected left={EXPECTED_BULLET_LEFT}, hanging={EXPECTED_BULLET_HANGING})"
|
||||
)
|
||||
if ordered_indent_issues > 0:
|
||||
issues.append(
|
||||
"ERROR: "
|
||||
f"{ordered_indent_issues} ordered list paragraphs still use over-indented list geometry "
|
||||
f"(expected left={EXPECTED_ORDERED_LEFT}, hanging={EXPECTED_BULLET_HANGING})"
|
||||
)
|
||||
|
||||
# 9. Table cell paragraphs should not inherit body first-line indent
|
||||
table_cell_indent_issues = 0
|
||||
for tc in doc.findall(f".//{{{W}}}tc"):
|
||||
for p in tc.findall(f"{{{W}}}p"):
|
||||
ppr = p.find(f"{{{W}}}pPr")
|
||||
ind = ppr.find(f"{{{W}}}ind") if ppr is not None else None
|
||||
if ind is None:
|
||||
table_cell_indent_issues += 1
|
||||
continue
|
||||
if ind.get(f"{{{W}}}firstLine") not in ("0", None) or ind.get(f"{{{W}}}firstLineChars") not in ("0", None):
|
||||
table_cell_indent_issues += 1
|
||||
if table_cell_indent_issues > 0:
|
||||
issues.append(f"ERROR: {table_cell_indent_issues} table cell paragraphs still inherit first-line indentation")
|
||||
|
||||
# 10. Table and figure captions
|
||||
all_texts = []
|
||||
for p in doc.findall(f".//{{{W}}}p"):
|
||||
text = "".join(t.text or "" for t in p.findall(f".//{{{W}}}t"))
|
||||
if text.strip():
|
||||
all_texts.append(text.strip())
|
||||
|
||||
table_captions = [t for t in all_texts if re.match(r"^表\s*\d+[-.]\d+\s+\S", t)]
|
||||
figure_captions = [t for t in all_texts if re.match(r"^图\s*\d+[-.]\d+\s+\S", t)]
|
||||
|
||||
if len(tables) > 0 and len(table_captions) == 0:
|
||||
issues.append(f"ERROR: {len(tables)} tables found but 0 table captions (表X-Y)")
|
||||
elif len(tables) > len(table_captions):
|
||||
issues.append(f"WARN: {len(tables)} tables but only {len(table_captions)} table captions")
|
||||
|
||||
for kind, captions in [("表", table_captions), ("图", figure_captions)]:
|
||||
nums = []
|
||||
for cap in captions:
|
||||
m = re.match(rf"^{kind}\s*(\d+)[-.]\s*(\d+)", cap)
|
||||
if m:
|
||||
nums.append((int(m.group(1)), int(m.group(2))))
|
||||
if nums:
|
||||
chapter = nums[0][0]
|
||||
for i, (ch, seq) in enumerate(nums, 1):
|
||||
if ch != chapter:
|
||||
issues.append(f"WARN: {kind} caption #{i} has chapter {ch}, expected {chapter}")
|
||||
if seq != i:
|
||||
issues.append(f"WARN: {kind} caption #{i} is {kind}{ch}-{seq}, expected {kind}{chapter}-{i}")
|
||||
|
||||
if not issues:
|
||||
issues.append(
|
||||
f"OK: {len(tables)} tables, {len(table_captions)} table captions, "
|
||||
f"{len(figure_captions)} figure captions — all checks passed"
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3 or sys.argv[1] not in ("pre", "post", "fix"):
|
||||
print(
|
||||
"Usage:\n"
|
||||
" validate_captions.py pre <source.md> — check only\n"
|
||||
" validate_captions.py fix <source.md> — auto-insert missing captions\n"
|
||||
" validate_captions.py post <output.docx> — check generated docx",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
mode = sys.argv[1]
|
||||
path = sys.argv[2]
|
||||
|
||||
if mode == "pre":
|
||||
results = pre_check(path)
|
||||
elif mode == "fix":
|
||||
results = auto_fix(path)
|
||||
else:
|
||||
results = post_check(path)
|
||||
|
||||
has_error = False
|
||||
for line in results:
|
||||
if line.startswith("ERROR"):
|
||||
has_error = True
|
||||
print(line)
|
||||
|
||||
return 1 if has_error else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+20
-9
@@ -1,11 +1,22 @@
|
||||
# Editor / Tool configs
|
||||
.DS_Store
|
||||
.claude/
|
||||
.claudian/
|
||||
.obsidian/
|
||||
.pandoc/
|
||||
# === 归档 ===
|
||||
Archive/
|
||||
|
||||
# Original source (kept for reference, not tracked)
|
||||
# === 生成输出 ===
|
||||
output/
|
||||
|
||||
# === 大型数据 ===
|
||||
.pandoc/zotero-library-1.json
|
||||
|
||||
# === 工具本地配置 ===
|
||||
.obsidian/
|
||||
.claude/settings.local.json
|
||||
.claude/agents/
|
||||
.claude/commands/
|
||||
.claude/worktrees/
|
||||
.claudian/
|
||||
|
||||
# === 系统文件 ===
|
||||
.DS_Store
|
||||
*.docx
|
||||
learning_markdown.md
|
||||
pics/
|
||||
*.zip
|
||||
~$*
|
||||
|
||||
+2273
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,864 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale xmlns="http://purl.org/net/xbiblio/csl" version="1.0" xml:lang="en-US">
|
||||
<!-- The abbreviations in this file follow the recommendations of The Chicago Manual of Style, 18th ed. (2024), sec. 10.48 (cited hereafter as CMOS), unless stated otherwise. -->
|
||||
<!-- Additional abbreviations are from:
|
||||
1. Oxford Dictionary for Writers and Editors (2000), https://archive.org/details/oxfordstylemanua0000unse (cited hereafter as ODWE): reference has also been made to the New Oxford Dictionary for Writers and Editors (NODWE), but periods must be added to contractions in these later editions to reflect US English usage
|
||||
2. Oxford Dictionary of Abbreviations (2011), https://doi.org/10.1093/acref/9780199698295.001.0001 (cited hereafter as ODA)
|
||||
-->
|
||||
<info>
|
||||
<translator>
|
||||
<name>Andrew Dunning</name>
|
||||
<uri>https://orcid.org/0000-0003-0464-5036</uri>
|
||||
</translator>
|
||||
<translator>
|
||||
<name>Sebastian Karcher</name>
|
||||
<uri>https://orcid.org/0000-0001-8249-7388</uri>
|
||||
</translator>
|
||||
<translator>
|
||||
<name>Rintze M. Zelle</name>
|
||||
<uri>https://orcid.org/0000-0003-1779-8883</uri>
|
||||
</translator>
|
||||
<translator>
|
||||
<name>Denis Meier</name>
|
||||
</translator>
|
||||
<translator>
|
||||
<name>Brenton M. Wiernik</name>
|
||||
<uri>https://orcid.org/0000-0001-9560-6336</uri>
|
||||
</translator>
|
||||
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
||||
<updated>2026-01-10T00:00:00+00:00</updated>
|
||||
</info>
|
||||
<style-options punctuation-in-quote="true"/>
|
||||
<date form="text">
|
||||
<date-part name="month" suffix=" "/>
|
||||
<date-part name="day" suffix=", "/>
|
||||
<date-part name="year"/>
|
||||
</date>
|
||||
<date form="numeric">
|
||||
<date-part name="month" form="numeric-leading-zeros" suffix="/"/>
|
||||
<date-part name="day" form="numeric-leading-zeros" suffix="/"/>
|
||||
<date-part name="year"/>
|
||||
</date>
|
||||
<terms>
|
||||
<!-- LONG GENERAL TERMS -->
|
||||
<term name="accessed">accessed</term>
|
||||
<term name="advance-online-publication">advance online publication</term>
|
||||
<term name="album">album</term>
|
||||
<term name="and">and</term>
|
||||
<term name="and others">and others</term>
|
||||
<term name="anonymous">anonymous</term>
|
||||
<term name="at">at</term>
|
||||
<term name="audio-recording">audio recording</term>
|
||||
<term name="available at">available at</term>
|
||||
<term name="by">by</term>
|
||||
<term name="circa">circa</term>
|
||||
<term name="cited">cited</term>
|
||||
<term name="et-al">et al.</term>
|
||||
<term name="film">film</term>
|
||||
<term name="forthcoming">forthcoming</term>
|
||||
<term name="from">from</term>
|
||||
<term name="henceforth">henceforth</term>
|
||||
<term name="ibid">ibid.</term>
|
||||
<term name="in">in</term>
|
||||
<term name="in press">in press</term>
|
||||
<term name="internet">internet</term>
|
||||
<term name="letter">letter</term>
|
||||
<term name="loc-cit">loc. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
|
||||
<term name="no date">no date</term>
|
||||
<term name="no-place">no place</term>
|
||||
<term name="no-publisher">no publisher</term>
|
||||
<term name="on">on</term>
|
||||
<term name="online">online</term>
|
||||
<term name="op-cit">op. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
|
||||
<term name="original-work-published">original work published</term>
|
||||
<term name="personal-communication">personal communication</term>
|
||||
<term name="podcast">podcast</term>
|
||||
<term name="podcast-episode">podcast episode</term>
|
||||
<term name="preprint">preprint</term>
|
||||
<term name="presented at">presented at the</term>
|
||||
<term name="radio-broadcast">radio broadcast</term>
|
||||
<term name="radio-series">radio series</term>
|
||||
<term name="radio-series-episode">radio series episode</term>
|
||||
<term name="reference">
|
||||
<single>reference</single>
|
||||
<multiple>references</multiple>
|
||||
</term>
|
||||
<term name="retrieved">retrieved</term>
|
||||
<term name="review-of">review of</term>
|
||||
<term name="scale">scale</term>
|
||||
<term name="special-issue">special issue</term>
|
||||
<term name="special-section">special section</term>
|
||||
<term name="television-broadcast">television broadcast</term>
|
||||
<term name="television-series">television series</term>
|
||||
<term name="television-series-episode">television series episode</term>
|
||||
<term name="video">video</term>
|
||||
<term name="working-paper">working paper</term>
|
||||
|
||||
<!-- SHORT GENERAL TERMS -->
|
||||
<!-- Omitted short forms: accessed, album, and (symbol), and others, at (symbol), forthcoming, henceforth, ibid, in, in press, internet, loc-cit, on, online, op-cit, podcast, preprint, presented at -->
|
||||
<term name="advance-online-publication" form="short">adv. online pub.</term> <!-- ODA -->
|
||||
<term name="anonymous" form="short">anon.</term>
|
||||
<term name="audio-recording" form="short">au. rec.</term> <!-- ODA -->
|
||||
<term name="available at" form="short">avail. at</term> <!-- ODA -->
|
||||
<term name="circa" form="short">c.</term>
|
||||
<!-- CMOS 10.48 recommends "ca." for "circa" but also allows "c.", which CSL has used historically -->
|
||||
<term name="cited" form="short">cit.</term> <!-- ODA -->
|
||||
<term name="film" form="short">flm.</term> <!-- ODA -->
|
||||
<term name="from" form="short">fr.</term>
|
||||
<term name="letter" form="short">let.</term> <!-- ODA -->
|
||||
<term name="no date" form="short">n.d.</term>
|
||||
<term name="no-place" form="short">n.p.</term>
|
||||
<term name="no-publisher" form="short">n.p.</term>
|
||||
<term name="original-work-published" form="short">orig. pub.</term> <!-- Oxford Guide to Style -->
|
||||
<term name="personal-communication" form="short">pers. comm.</term>
|
||||
<term name="podcast-episode" form="short">podcast ep.</term>
|
||||
<term name="radio-broadcast" form="short">radio bdcst.</term> <!-- ODA -->
|
||||
<term name="radio-series" form="short">radio ser.</term> <!-- ODA -->
|
||||
<term name="radio-series-episode" form="short">radio ser. ep.</term> <!-- ODA -->
|
||||
<term name="reference" form="short">
|
||||
<single>ref.</single>
|
||||
<multiple>refs.</multiple>
|
||||
</term>
|
||||
<term name="retrieved" form="short">rtvd.</term> <!-- ODA -->
|
||||
<term name="review-of" form="short">rev. of</term>
|
||||
<term name="scale" form="short">sc.</term> <!-- ODA -->
|
||||
<term name="special-issue" form="short">spec. iss.</term> <!-- ODA -->
|
||||
<term name="special-section" form="short">spec. sec.</term> <!-- ODA/CMOS -->
|
||||
<term name="television-broadcast" form="short">TV bdcst.</term> <!-- ODA -->
|
||||
<term name="television-series" form="short">TV ser.</term> <!-- ODA -->
|
||||
<term name="television-series-episode" form="short">TV ser. ep.</term> <!-- ODA -->
|
||||
<term name="video" form="short">vid.</term> <!-- ODA -->
|
||||
<term name="working-paper" form="short">wkg. paper</term> <!-- ODA -->
|
||||
|
||||
<!-- SYMBOLIC GENERAL FORMS -->
|
||||
<term name="and" form="symbol">&</term>
|
||||
<term name="at" form="symbol">@</term>
|
||||
|
||||
<!-- LONG ITEM TYPE FORMS -->
|
||||
<term name="article">preprint</term>
|
||||
<term name="article-journal">journal article</term>
|
||||
<term name="article-magazine">magazine article</term>
|
||||
<term name="article-newspaper">newspaper article</term>
|
||||
<term name="bill">bill</term>
|
||||
<!-- book is in the list of locator terms -->
|
||||
<term name="broadcast">broadcast</term>
|
||||
<!-- chapter is in the list of locator terms -->
|
||||
<term name="classic">classical work</term>
|
||||
<term name="collection">archival collection</term>
|
||||
<term name="dataset">dataset</term>
|
||||
<term name="document">document</term>
|
||||
<term name="entry">entry</term>
|
||||
<term name="entry-dictionary">dictionary entry</term>
|
||||
<term name="entry-encyclopedia">encyclopedia entry</term>
|
||||
<term name="event">event</term>
|
||||
<!-- figure is in the list of locator terms -->
|
||||
<term name="graphic">graphic</term>
|
||||
<term name="hearing">hearing</term>
|
||||
<term name="interview">interview</term>
|
||||
<term name="legal_case">legal case</term>
|
||||
<term name="legislation">legislation</term>
|
||||
<term name="manuscript">manuscript</term>
|
||||
<term name="map">map</term>
|
||||
<term name="motion_picture">video recording</term>
|
||||
<term name="musical_score">musical score</term>
|
||||
<term name="pamphlet">pamphlet</term>
|
||||
<term name="paper-conference">conference paper</term>
|
||||
<term name="patent">patent</term>
|
||||
<term name="performance">performance</term>
|
||||
<term name="periodical">periodical</term>
|
||||
<term name="personal_communication">personal communication</term>
|
||||
<term name="post">post</term>
|
||||
<term name="post-weblog">blog post</term>
|
||||
<term name="regulation">regulation</term>
|
||||
<term name="report">report</term>
|
||||
<term name="review">review</term>
|
||||
<term name="review-book">book review</term>
|
||||
<term name="software">software</term>
|
||||
<term name="song">audio recording</term>
|
||||
<term name="speech">presentation</term>
|
||||
<term name="standard">standard</term>
|
||||
<term name="thesis">thesis</term>
|
||||
<term name="treaty">treaty</term>
|
||||
<term name="webpage">webpage</term>
|
||||
|
||||
<!-- SHORT ITEM TYPE FORMS -->
|
||||
<!-- Omitted short forms: article, bill, entry, event, hearing, map, periodical, speech, treaty -->
|
||||
<term name="article-journal" form="short">jour. art.</term> <!-- ODWE -->
|
||||
<term name="article-magazine" form="short">mag. art.</term> <!-- ODWE -->
|
||||
<term name="article-newspaper" form="short">newspaper art.</term>
|
||||
<term name="broadcast" form="short">bdcst.</term> <!-- ODA -->
|
||||
<!-- book is in the list of locator terms -->
|
||||
<!-- chapter is in the list of locator terms -->
|
||||
<term name="classic" form="short">class. wk.</term> <!-- ODWE -->
|
||||
<term name="collection" form="short">arch. coll.</term> <!-- ODA -->
|
||||
<term name="document" form="short">doc.</term>
|
||||
<term name="entry-dictionary" form="short">dict. entry</term>
|
||||
<term name="entry-encyclopedia" form="short">ency. entry</term>
|
||||
<!-- figure is in the list of locator terms -->
|
||||
<term name="graphic" form="short">gr.</term> <!-- ODA -->
|
||||
<term name="interview" form="short">int.</term> <!-- ODA -->
|
||||
<term name="legal_case" form="short">leg. case</term> <!-- ODA -->
|
||||
<term name="legislation" form="short">legis.</term> <!-- ODA -->
|
||||
<term name="manuscript" form="short">
|
||||
<single>MS</single>
|
||||
<multiple>MSS</multiple>
|
||||
</term>
|
||||
<term name="motion_picture" form="short">vid. rec.</term> <!-- ODA -->
|
||||
<term name="musical_score" form="short">mus. score</term> <!-- ODWE -->
|
||||
<term name="pamphlet" form="short">pam.</term> <!-- ODWE -->
|
||||
<term name="paper-conference" form="short">conf. paper</term> <!-- ODA -->
|
||||
<term name="patent" form="short">pat.</term> <!-- ODWE -->
|
||||
<term name="performance" form="short">prfm.</term> <!-- ODA -->
|
||||
<term name="personal_communication" form="short">pers. comm.</term>
|
||||
<term name="regulation" form="short">reg.</term> <!-- ODA -->
|
||||
<term name="report" form="short">rep.</term> <!-- ODWE -->
|
||||
<term name="review" form="short">rev.</term>
|
||||
<term name="review-book" form="short">bk. rev.</term>
|
||||
<term name="software" form="short">sftw.</term> <!-- ODA -->
|
||||
<term name="song" form="short">au. rec.</term> <!-- ODA -->
|
||||
<term name="standard" form="short">std.</term> <!-- ODA -->
|
||||
<term name="thesis" form="short">thes.</term> <!-- ODA -->
|
||||
<term name="webpage" form="short">webpg.</term> <!-- ODA -->
|
||||
|
||||
<!-- LONG VERB ITEM TYPE FORMS -->
|
||||
<!-- Only where applicable -->
|
||||
<term name="hearing" form="verb">testimony of</term>
|
||||
<term name="review" form="verb">review of</term>
|
||||
<term name="review-book" form="verb">review of the book</term>
|
||||
|
||||
<!-- SHORT VERB ITEM TYPE FORMS -->
|
||||
<!-- Only where applicable -->
|
||||
<term name="hearing" form="verb-short">test. of</term> <!-- ODA -->
|
||||
<term name="review" form="verb-short">rev. of</term>
|
||||
<term name="review-book" form="verb-short">rev. of the bk.</term>
|
||||
|
||||
<!-- HISTORICAL ERA TERMS -->
|
||||
<term name="ad"> AD</term>
|
||||
<term name="bc"> BC</term>
|
||||
<term name="bce"> BCE</term>
|
||||
<term name="ce"> CE</term>
|
||||
|
||||
<!-- PUNCTUATION -->
|
||||
<term name="open-quote">“</term>
|
||||
<term name="close-quote">”</term>
|
||||
<term name="open-inner-quote">‘</term>
|
||||
<term name="close-inner-quote">’</term>
|
||||
<term name="page-range-delimiter">–</term>
|
||||
<term name="colon">:</term>
|
||||
<term name="comma">,</term>
|
||||
<term name="semicolon">;</term>
|
||||
|
||||
<!-- ORDINALS -->
|
||||
<term name="ordinal">th</term>
|
||||
<term name="ordinal-01">st</term>
|
||||
<term name="ordinal-02">nd</term>
|
||||
<term name="ordinal-03">rd</term>
|
||||
<term name="ordinal-11">th</term>
|
||||
<term name="ordinal-12">th</term>
|
||||
<term name="ordinal-13">th</term>
|
||||
|
||||
<!-- LONG ORDINALS -->
|
||||
<term name="long-ordinal-01">first</term>
|
||||
<term name="long-ordinal-02">second</term>
|
||||
<term name="long-ordinal-03">third</term>
|
||||
<term name="long-ordinal-04">fourth</term>
|
||||
<term name="long-ordinal-05">fifth</term>
|
||||
<term name="long-ordinal-06">sixth</term>
|
||||
<term name="long-ordinal-07">seventh</term>
|
||||
<term name="long-ordinal-08">eighth</term>
|
||||
<term name="long-ordinal-09">ninth</term>
|
||||
<term name="long-ordinal-10">tenth</term>
|
||||
|
||||
<!-- LONG LOCATOR FORMS -->
|
||||
<term name="act">
|
||||
<single>act</single>
|
||||
<multiple>acts</multiple>
|
||||
</term>
|
||||
<term name="appendix">
|
||||
<single>appendix</single>
|
||||
<multiple>appendices</multiple>
|
||||
</term>
|
||||
<term name="article-locator">
|
||||
<single>article</single>
|
||||
<multiple>articles</multiple>
|
||||
</term>
|
||||
<term name="book">
|
||||
<single>book</single>
|
||||
<multiple>books</multiple>
|
||||
</term>
|
||||
<term name="canon">
|
||||
<single>canon</single>
|
||||
<multiple>canons</multiple>
|
||||
</term>
|
||||
<term name="chapter">
|
||||
<single>chapter</single>
|
||||
<multiple>chapters</multiple>
|
||||
</term>
|
||||
<term name="column">
|
||||
<single>column</single>
|
||||
<multiple>columns</multiple>
|
||||
</term>
|
||||
<term name="elocation">
|
||||
<single>location</single>
|
||||
<multiple>locations</multiple>
|
||||
</term>
|
||||
<term name="equation">
|
||||
<single>equation</single>
|
||||
<multiple>equations</multiple>
|
||||
</term>
|
||||
<term name="figure">
|
||||
<single>figure</single>
|
||||
<multiple>figures</multiple>
|
||||
</term>
|
||||
<term name="folio">
|
||||
<single>folio</single>
|
||||
<multiple>folios</multiple>
|
||||
</term>
|
||||
<term name="issue">
|
||||
<single>issue</single>
|
||||
<multiple>issues</multiple>
|
||||
</term>
|
||||
<term name="line">
|
||||
<single>line</single>
|
||||
<multiple>lines</multiple>
|
||||
</term>
|
||||
<term name="note">
|
||||
<single>note</single>
|
||||
<multiple>notes</multiple>
|
||||
</term>
|
||||
<term name="opus">
|
||||
<single>opus</single>
|
||||
<multiple>opera</multiple>
|
||||
</term>
|
||||
<term name="page">
|
||||
<single>page</single>
|
||||
<multiple>pages</multiple>
|
||||
</term>
|
||||
<term name="paragraph">
|
||||
<single>paragraph</single>
|
||||
<multiple>paragraphs</multiple>
|
||||
</term>
|
||||
<term name="part">
|
||||
<single>part</single>
|
||||
<multiple>parts</multiple>
|
||||
</term>
|
||||
<term name="rule">
|
||||
<single>rule</single>
|
||||
<multiple>rules</multiple>
|
||||
</term>
|
||||
<term name="scene">
|
||||
<single>scene</single>
|
||||
<multiple>scenes</multiple>
|
||||
</term>
|
||||
<term name="section">
|
||||
<single>section</single>
|
||||
<multiple>sections</multiple>
|
||||
</term>
|
||||
<term name="sub-verbo">
|
||||
<single>sub verbo</single>
|
||||
<multiple>sub verbis</multiple>
|
||||
</term>
|
||||
<term name="supplement">
|
||||
<single>supplement</single>
|
||||
<multiple>supplements</multiple>
|
||||
</term>
|
||||
<term name="table">
|
||||
<single>table</single>
|
||||
<multiple>tables</multiple>
|
||||
</term>
|
||||
<!-- A timestamp is a composite of hours, minutes, etc. and therefore has no default label. -->
|
||||
<term name="timestamp"/>
|
||||
<term name="title-locator">
|
||||
<single>title</single>
|
||||
<multiple>titles</multiple>
|
||||
</term>
|
||||
<term name="verse">
|
||||
<single>verse</single>
|
||||
<multiple>verses</multiple>
|
||||
</term>
|
||||
<term name="volume">
|
||||
<single>volume</single>
|
||||
<multiple>volumes</multiple>
|
||||
</term>
|
||||
|
||||
<!-- SHORT LOCATOR FORMS -->
|
||||
<!-- Omitted short forms: act, timestamp -->
|
||||
<term name="appendix" form="short">
|
||||
<single>app.</single>
|
||||
<multiple>apps.</multiple>
|
||||
</term>
|
||||
<term name="article-locator" form="short">
|
||||
<single>art.</single>
|
||||
<multiple>arts.</multiple>
|
||||
</term>
|
||||
<term name="book" form="short">
|
||||
<single>bk.</single>
|
||||
<multiple>bks.</multiple>
|
||||
</term>
|
||||
<term name="canon" form="short">
|
||||
<!-- Oxford Dictionary for Writers and Editors -->
|
||||
<single>can.</single>
|
||||
<multiple>cann.</multiple>
|
||||
</term>
|
||||
<term name="chapter" form="short">
|
||||
<single>chap.</single>
|
||||
<multiple>chaps.</multiple>
|
||||
</term>
|
||||
<term name="column" form="short">
|
||||
<single>col.</single>
|
||||
<multiple>cols.</multiple>
|
||||
</term>
|
||||
<term name="elocation" form="short">
|
||||
<single>loc.</single>
|
||||
<multiple>locs.</multiple>
|
||||
</term>
|
||||
<term name="equation" form="short">
|
||||
<single>eq.</single>
|
||||
<multiple>eqq.</multiple>
|
||||
</term>
|
||||
<term name="figure" form="short">
|
||||
<single>fig.</single>
|
||||
<multiple>figs.</multiple>
|
||||
</term>
|
||||
<term name="folio" form="short">
|
||||
<single>fol.</single>
|
||||
<multiple>fols.</multiple>
|
||||
</term>
|
||||
<term name="issue" form="short">
|
||||
<single>no.</single>
|
||||
<multiple>nos.</multiple>
|
||||
</term>
|
||||
<term name="line" form="short">
|
||||
<single>l.</single>
|
||||
<multiple>ll.</multiple>
|
||||
</term>
|
||||
<term name="note" form="short">
|
||||
<single>n.</single>
|
||||
<multiple>nn.</multiple>
|
||||
</term>
|
||||
<term name="opus" form="short">
|
||||
<single>op.</single>
|
||||
<multiple>opp.</multiple>
|
||||
</term>
|
||||
<term name="page" form="short">
|
||||
<single>p.</single>
|
||||
<multiple>pp.</multiple>
|
||||
</term>
|
||||
<term name="paragraph" form="short">
|
||||
<single>para.</single>
|
||||
<multiple>paras.</multiple>
|
||||
</term>
|
||||
<term name="part" form="short">
|
||||
<single>pt.</single>
|
||||
<multiple>pts.</multiple>
|
||||
</term>
|
||||
<term name="rule" form="short">
|
||||
<!-- legal abbreviations in the Oxford Guide to Style, sec. 13.2.1 -->
|
||||
<single>r.</single>
|
||||
<multiple>rr.</multiple>
|
||||
</term>
|
||||
<term name="scene" form="short">
|
||||
<single>sc.</single>
|
||||
<multiple>scs.</multiple>
|
||||
</term>
|
||||
<term name="section" form="short">
|
||||
<single>sec.</single>
|
||||
<multiple>secs.</multiple>
|
||||
</term>
|
||||
<term name="sub-verbo" form="short">
|
||||
<single>s.v.</single>
|
||||
<multiple>s.vv.</multiple>
|
||||
</term>
|
||||
<term name="supplement" form="short">
|
||||
<single>supp.</single>
|
||||
<multiple>supps.</multiple>
|
||||
</term>
|
||||
<term name="table" form="short">
|
||||
<!-- Oxford Dictionary of Abbreviations -->
|
||||
<single>tbl.</single>
|
||||
<multiple>tbls.</multiple>
|
||||
</term>
|
||||
<term name="title-locator" form="short">
|
||||
<!-- Oxford Dictionary for Writers and Editors -->
|
||||
<single>tit.</single>
|
||||
<multiple>titt.</multiple>
|
||||
</term>
|
||||
<term name="verse" form="short">
|
||||
<single>v.</single>
|
||||
<multiple>vv.</multiple>
|
||||
</term>
|
||||
<term name="volume" form="short">
|
||||
<single>vol.</single>
|
||||
<multiple>vols.</multiple>
|
||||
</term>
|
||||
|
||||
<!-- SYMBOLIC LOCATOR FORMS -->
|
||||
<term name="chapter" form="symbol">
|
||||
<!-- caput/capita, esp. in legal works; cf. CMOS 14.196 -->
|
||||
<single>c.</single>
|
||||
<multiple>cc.</multiple>
|
||||
</term>
|
||||
<term name="paragraph" form="symbol">
|
||||
<single>¶</single>
|
||||
<multiple>¶¶</multiple>
|
||||
</term>
|
||||
<term name="section" form="symbol">
|
||||
<single>§</single>
|
||||
<multiple>§§</multiple>
|
||||
</term>
|
||||
|
||||
<!-- LONG NUMBER VARIABLE FORMS -->
|
||||
<term name="chapter-number">
|
||||
<single>chapter</single>
|
||||
<multiple>chapters</multiple>
|
||||
</term>
|
||||
<term name="citation-number">
|
||||
<single>citation</single>
|
||||
<multiple>citations</multiple>
|
||||
</term>
|
||||
<term name="collection-number">
|
||||
<single>number</single>
|
||||
<multiple>numbers</multiple>
|
||||
</term>
|
||||
<term name="edition">
|
||||
<single>edition</single>
|
||||
<multiple>editions</multiple>
|
||||
</term>
|
||||
<term name="first-reference-note-number">
|
||||
<single>note</single>
|
||||
<multiple>notes</multiple>
|
||||
</term>
|
||||
<term name="number">
|
||||
<single>number</single>
|
||||
<multiple>numbers</multiple>
|
||||
</term>
|
||||
<term name="number-of-pages">
|
||||
<single>page</single>
|
||||
<multiple>pages</multiple>
|
||||
</term>
|
||||
<term name="number-of-volumes">
|
||||
<single>volume</single>
|
||||
<multiple>volumes</multiple>
|
||||
</term>
|
||||
<term name="page-first">
|
||||
<single>page</single>
|
||||
<multiple>pages</multiple>
|
||||
</term>
|
||||
<term name="printing">
|
||||
<single>printing</single>
|
||||
<multiple>printings</multiple>
|
||||
</term>
|
||||
<term name="version">
|
||||
<single>version</single>
|
||||
<multiple>versions</multiple>
|
||||
</term>
|
||||
|
||||
<!-- SHORT NUMBER VARIABLE FORMS -->
|
||||
<term name="chapter-number" form="short">
|
||||
<single>chap.</single>
|
||||
<multiple>chaps.</multiple>
|
||||
</term>
|
||||
<term name="citation-number" form="short">
|
||||
<single>cit.</single>
|
||||
<multiple>cits.</multiple>
|
||||
</term>
|
||||
<term name="collection-number" form="short">
|
||||
<single>no.</single>
|
||||
<multiple>nos.</multiple>
|
||||
</term>
|
||||
<term name="edition" form="short">
|
||||
<single>ed.</single>
|
||||
<multiple>eds.</multiple>
|
||||
</term>
|
||||
<term name="first-reference-note-number" form="short">
|
||||
<single>n.</single>
|
||||
<multiple>nn.</multiple>
|
||||
</term>
|
||||
<term name="number" form="short">
|
||||
<single>no.</single>
|
||||
<multiple>nos.</multiple>
|
||||
</term>
|
||||
<term name="number-of-pages" form="short">
|
||||
<single>p.</single>
|
||||
<multiple>pp.</multiple>
|
||||
</term>
|
||||
<term name="number-of-volumes" form="short">
|
||||
<single>vol.</single>
|
||||
<multiple>vols.</multiple>
|
||||
</term>
|
||||
<term name="page-first" form="short">
|
||||
<single>p.</single>
|
||||
<multiple>pp.</multiple>
|
||||
</term>
|
||||
<term name="printing" form="short">
|
||||
<!-- Oxford Dictionary for Writers and Editors -->
|
||||
<single>ptg.</single>
|
||||
<multiple>ptgs.</multiple>
|
||||
</term>
|
||||
<term name="version" form="short">v.</term> <!-- no plural -->
|
||||
|
||||
<!-- LONG ROLE FORMS -->
|
||||
<term name="author"/> <!-- generally blank -->
|
||||
<term name="chair">
|
||||
<single>chair</single>
|
||||
<multiple>chairs</multiple>
|
||||
</term>
|
||||
<term name="collection-editor">
|
||||
<single>editor</single>
|
||||
<multiple>editors</multiple>
|
||||
</term>
|
||||
<term name="compiler">
|
||||
<single>compiler</single>
|
||||
<multiple>compilers</multiple>
|
||||
</term>
|
||||
<term name="composer"/> <!-- generally blank -->
|
||||
<term name="container-author"/> <!-- generally blank -->
|
||||
<term name="contributor">
|
||||
<single>contributor</single>
|
||||
<multiple>contributors</multiple>
|
||||
</term>
|
||||
<term name="curator">
|
||||
<single>curator</single>
|
||||
<multiple>curators</multiple>
|
||||
</term>
|
||||
<term name="director">
|
||||
<single>director</single>
|
||||
<multiple>directors</multiple>
|
||||
</term>
|
||||
<term name="editor">
|
||||
<single>editor</single>
|
||||
<multiple>editors</multiple>
|
||||
</term>
|
||||
<term name="editor-translator">
|
||||
<single>editor & translator</single>
|
||||
<multiple>editors & translators</multiple>
|
||||
</term>
|
||||
<term name="editortranslator">
|
||||
<single>editor & translator</single>
|
||||
<multiple>editors & translators</multiple>
|
||||
</term>
|
||||
<term name="editorial-director">
|
||||
<single>editor</single>
|
||||
<multiple>editors</multiple>
|
||||
</term>
|
||||
<term name="executive-producer">
|
||||
<single>executive producer</single>
|
||||
<multiple>executive producers</multiple>
|
||||
</term>
|
||||
<term name="guest">
|
||||
<single>guest</single>
|
||||
<multiple>guests</multiple>
|
||||
</term>
|
||||
<term name="host">
|
||||
<single>host</single>
|
||||
<multiple>hosts</multiple>
|
||||
</term>
|
||||
<term name="illustrator">
|
||||
<single>illustrator</single>
|
||||
<multiple>illustrators</multiple>
|
||||
</term>
|
||||
<term name="interviewer"/> <!-- generally blank -->
|
||||
<term name="narrator">
|
||||
<single>narrator</single>
|
||||
<multiple>narrators</multiple>
|
||||
</term>
|
||||
<term name="organizer">
|
||||
<single>organizer</single>
|
||||
<multiple>organizers</multiple>
|
||||
</term>
|
||||
<term name="original-author"/> <!-- generally blank -->
|
||||
<term name="performer">
|
||||
<single>performer</single>
|
||||
<multiple>performers</multiple>
|
||||
</term>
|
||||
<term name="producer">
|
||||
<single>producer</single>
|
||||
<multiple>producers</multiple>
|
||||
</term>
|
||||
<term name="recipient"/> <!-- generally blank -->
|
||||
<term name="reviewed-author"/> <!-- generally blank -->
|
||||
<term name="script-writer">
|
||||
<single>writer</single>
|
||||
<multiple>writers</multiple>
|
||||
</term>
|
||||
<term name="series-creator">
|
||||
<single>series creator</single>
|
||||
<multiple>series creators</multiple>
|
||||
</term>
|
||||
<term name="translator">
|
||||
<single>translator</single>
|
||||
<multiple>translators</multiple>
|
||||
</term>
|
||||
|
||||
<!-- SHORT ROLE FORMS -->
|
||||
<!-- Omitted roles:
|
||||
author, chair, composer, container-author, guest, host, interviewer, original-author, recipient, reviewed-author
|
||||
-->
|
||||
<term name="collection-editor" form="short">
|
||||
<single>ed.</single>
|
||||
<multiple>eds.</multiple>
|
||||
</term>
|
||||
<term name="compiler" form="short">
|
||||
<single>comp.</single>
|
||||
<multiple>comps.</multiple>
|
||||
</term>
|
||||
<term name="contributor" form="short">
|
||||
<!-- Oxford Dictionary of Abbreviations -->
|
||||
<single>contrib.</single>
|
||||
<multiple>contribs.</multiple>
|
||||
</term>
|
||||
<term name="curator" form="short">
|
||||
<!-- Oxford Art Online <https://www.oxfordartonline.com/page/1661> -->
|
||||
<single>cur.</single>
|
||||
<multiple>curs.</multiple>
|
||||
</term>
|
||||
<term name="director" form="short">
|
||||
<single>dir.</single>
|
||||
<multiple>dirs.</multiple>
|
||||
</term>
|
||||
<term name="editor" form="short">
|
||||
<single>ed.</single>
|
||||
<multiple>eds.</multiple>
|
||||
</term>
|
||||
<term name="editor-translator" form="short">
|
||||
<single>ed. & trans.</single>
|
||||
<multiple>eds. & trans.</multiple>
|
||||
</term>
|
||||
<term name="editortranslator" form="short">
|
||||
<single>ed. & trans.</single>
|
||||
<multiple>eds. & trans.</multiple>
|
||||
</term>
|
||||
<term name="editorial-director" form="short">
|
||||
<single>ed.</single>
|
||||
<multiple>eds.</multiple>
|
||||
</term>
|
||||
<term name="executive-producer" form="short">
|
||||
<!-- Oxford Dictionary of Abbreviations -->
|
||||
<single>exec. prod.</single>
|
||||
<multiple>exec. prods.</multiple>
|
||||
</term>
|
||||
<term name="illustrator" form="short">
|
||||
<single>ill.</single>
|
||||
<multiple>ills.</multiple>
|
||||
</term>
|
||||
<term name="narrator" form="short">
|
||||
<!-- Oxford Dictionary of Abbreviations -->
|
||||
<single>narr.</single>
|
||||
<multiple>narrs.</multiple>
|
||||
</term>
|
||||
<term name="organizer" form="short">
|
||||
<!-- possibly misleading: Oxford Dictionary of Abbreviations only defines this as organization or organized -->
|
||||
<single>org.</single>
|
||||
<multiple>orgs.</multiple>
|
||||
</term>
|
||||
<term name="performer" form="short">
|
||||
<!-- Oxford Dictionary of Abbreviations -->
|
||||
<single>perf.</single>
|
||||
<multiple>perfs.</multiple>
|
||||
</term>
|
||||
<term name="producer" form="short">
|
||||
<!-- Oxford Dictionary of Abbreviations -->
|
||||
<single>prod.</single>
|
||||
<multiple>prods.</multiple>
|
||||
</term>
|
||||
<term name="script-writer" form="short">
|
||||
<!-- Oxford Dictionary of Abbreviations -->
|
||||
<single>wrtr.</single>
|
||||
<multiple>wrtrs.</multiple>
|
||||
</term>
|
||||
<term name="series-creator" form="short">
|
||||
<single>ser. creator</single>
|
||||
<multiple>ser. creators</multiple>
|
||||
</term>
|
||||
<term name="translator" form="short">trans.</term> <!-- no plural -->
|
||||
|
||||
<!-- VERB ROLE FORMS -->
|
||||
<term name="chair" form="verb">chaired by</term>
|
||||
<term name="collection-editor" form="verb">edited by</term>
|
||||
<term name="compiler" form="verb">compiled by</term>
|
||||
<term name="composer" form="verb">composed by</term>
|
||||
<term name="container-author" form="verb">by</term>
|
||||
<term name="contributor" form="verb">with</term>
|
||||
<term name="curator" form="verb">curated by</term>
|
||||
<term name="director" form="verb">directed by</term>
|
||||
<term name="editor" form="verb">edited by</term>
|
||||
<term name="editor-translator" form="verb">edited & translated by</term>
|
||||
<term name="editortranslator" form="verb">edited & translated by</term>
|
||||
<term name="editorial-director" form="verb">edited by</term>
|
||||
<term name="executive-producer" form="verb">executive produced by</term>
|
||||
<term form="verb" name="guest">
|
||||
<single>with guest</single>
|
||||
<multiple>with guests</multiple>
|
||||
</term>
|
||||
<term name="host" form="verb">hosted by</term>
|
||||
<term name="illustrator" form="verb">illustrated by</term>
|
||||
<term name="interviewer" form="verb">interview by</term>
|
||||
<term name="narrator" form="verb">narrated by</term>
|
||||
<term name="organizer" form="verb">organized by</term>
|
||||
<term name="original-author" form="verb">by</term>
|
||||
<term name="performer" form="verb">performed by</term>
|
||||
<term name="producer" form="verb">produced by</term>
|
||||
<term name="recipient" form="verb">to</term>
|
||||
<term name="reviewed-author" form="verb">by</term>
|
||||
<term name="script-writer" form="verb">written by</term>
|
||||
<term name="series-creator" form="verb">created by</term>
|
||||
<term name="translator" form="verb">translated by</term>
|
||||
|
||||
<!-- SHORT VERB ROLE FORMS -->
|
||||
<!-- Omitted roles:
|
||||
author, chair, container-author, contributor, guest, host, interviewer, original-author, recipient, reviewed-author, series-creator
|
||||
-->
|
||||
<term name="collection-editor" form="verb-short">ed. by</term>
|
||||
<term name="compiler" form="verb-short">comp. by</term>
|
||||
<term name="composer" form="verb-short">comp. by</term> <!-- ODWE -->
|
||||
<term name="curator" form="verb-short">cur. by</term> <!-- Oxford Art Online -->
|
||||
<term name="director" form="verb-short">dir. by</term>
|
||||
<term name="editor" form="verb-short">ed. by</term>
|
||||
<term name="editor-translator" form="verb-short">ed. & trans. by</term>
|
||||
<term name="editortranslator" form="verb-short">ed. & trans. by</term>
|
||||
<term name="editorial-director" form="verb-short">ed. by</term>
|
||||
<term name="executive-producer" form="verb-short">exec. prod. by</term> <!-- ODA -->
|
||||
<term name="illustrator" form="verb-short">ill. by</term>
|
||||
<term name="narrator" form="verb-short">narr. by</term> <!-- ODA -->
|
||||
<term name="organizer" form="verb-short">org. by</term> <!-- ODA -->
|
||||
<term name="performer" form="verb-short">perf. by</term> <!-- ODA -->
|
||||
<term name="producer" form="verb-short">prod. by</term> <!-- ODA -->
|
||||
<term name="script-writer" form="verb-short">writ. by</term> <!-- ODA -->
|
||||
<term name="translator" form="verb-short">trans. by</term>
|
||||
|
||||
<!-- LONG MONTH FORMS -->
|
||||
<term name="month-01">January</term>
|
||||
<term name="month-02">February</term>
|
||||
<term name="month-03">March</term>
|
||||
<term name="month-04">April</term>
|
||||
<term name="month-05">May</term>
|
||||
<term name="month-06">June</term>
|
||||
<term name="month-07">July</term>
|
||||
<term name="month-08">August</term>
|
||||
<term name="month-09">September</term>
|
||||
<term name="month-10">October</term>
|
||||
<term name="month-11">November</term>
|
||||
<term name="month-12">December</term>
|
||||
|
||||
<!-- SHORT MONTH FORMS -->
|
||||
<!-- Chicago Manual of Style, 18th ed., sec. 10.44 (identical to New Hart's Rules, 2nd ed., sec. 10.2.6) -->
|
||||
<term name="month-01" form="short">Jan.</term>
|
||||
<term name="month-02" form="short">Feb.</term>
|
||||
<term name="month-03" form="short">Mar.</term>
|
||||
<term name="month-04" form="short">Apr.</term>
|
||||
<term name="month-05" form="short">May</term>
|
||||
<term name="month-06" form="short">June</term>
|
||||
<term name="month-07" form="short">July</term>
|
||||
<term name="month-08" form="short">Aug.</term>
|
||||
<term name="month-09" form="short">Sept.</term>
|
||||
<term name="month-10" form="short">Oct.</term>
|
||||
<term name="month-11" form="short">Nov.</term>
|
||||
<term name="month-12" form="short">Dec.</term>
|
||||
|
||||
<!-- SEASONS -->
|
||||
<term name="season-01">Spring</term>
|
||||
<term name="season-02">Summer</term>
|
||||
<term name="season-03">Autumn</term>
|
||||
<term name="season-04">Winter</term>
|
||||
</terms>
|
||||
</locale>
|
||||
@@ -0,0 +1,119 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is **设计人工智能:基础与应用** — a textbook project for design AI education. The book covers AI fundamentals (principles, perception, generation, agents) and their applications across design domains (digital media, industrial design, environmental design, urban planning).
|
||||
|
||||
A supplementary volume, **CC4SI (Claude Code for Spatial Intelligence)**, is preserved in `officefile/supplements/` and focuses on spatial intelligence and autonomous design using Claude Code.
|
||||
|
||||
**Key Characteristics:**
|
||||
- Educational content organized as a book with 12 chapters + 9 appendices
|
||||
- Mixed Chinese/English documentation and code
|
||||
- Git-based workflow with Obsidian integration for content editing
|
||||
- Pandoc-based export to Word (DOCX) with publisher template
|
||||
- Standard research project directory structure
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
2026_DesignAI/
|
||||
├── data/ # Data and resources
|
||||
│ ├── pics/ # Book illustrations
|
||||
│ └── references/ # Bibliography data
|
||||
├── dofile/ # Code and scripts
|
||||
│ └── examples/ # Python code examples
|
||||
│ ├── 00-introduction/ # Setup first assistant
|
||||
│ ├── 01-foundations/ # State machines, modularity, etc.
|
||||
│ └── 02-spatial-intelligence/ # Spatial analysis examples
|
||||
├── officefile/ # Main book content
|
||||
│ ├── 00-frontmatter/ # 自序 (Preface), 目录 (TOC)
|
||||
│ ├── 01-introduction/ # Ch1: AI development history
|
||||
│ ├── 02-framework/ # Ch2: AI theoretical framework
|
||||
│ ├── 03-1d-sequence/ # Ch3: Sequence & text (RNN→LLM)
|
||||
│ ├── 04-2d-vision/ # Ch4: Images & vision (CNN)
|
||||
│ ├── 05-3d-spatial/ # Ch5: Spatial & 3D (PointNet)
|
||||
│ ├── 06-reinforcement/ # Ch6: Reinforcement learning
|
||||
│ ├── 07-generative-ai/ # Ch7: Generative AI (Diffusion)
|
||||
│ ├── 08-agent/ # Ch8: AI Agent
|
||||
│ ├── 09-digital-media/ # Ch9: Digital media design
|
||||
│ ├── 10-industrial-design/ # Ch10: Industrial design
|
||||
│ ├── 11-environmental-landscape/ # Ch11: Environmental & landscape
|
||||
│ ├── 12-urban-ecology/ # Ch12: Urban & ecological planning
|
||||
│ ├── appendix/ # Appendices 1-9
|
||||
│ └── supplements/ # CC4SI supplementary material
|
||||
│ ├── 00-05/ # Spatial intelligence chapters
|
||||
│ └── appendix/ # Academic writing workflow
|
||||
├── output/ # Generated outputs
|
||||
│ └── docx/ # Word exports
|
||||
├── Archive/ # Archived original directories
|
||||
├── .claude/skills/markdown-to-docx/ # DOCX conversion skill
|
||||
├── .pandoc/ # Pandoc config (CSL, Zotero JSON)
|
||||
└── .obsidian/ # Obsidian vault configuration
|
||||
```
|
||||
|
||||
## Book Structure
|
||||
|
||||
### Upper Volume: Principles & Technology
|
||||
|
||||
| Chapter | Topic | Lines | Status |
|
||||
|---------|-------|-------|--------|
|
||||
| 01 | AI development history & paradigm evolution | 367 | Complete |
|
||||
| 02 | Theoretical framework (functional perspective, MLP) | 897 | Complete |
|
||||
| 03 | 1D data: Sequence & text (RNN → LLM) | 1595 | Complete |
|
||||
| 04 | 2D data: Images & vision (CNN → ViT) | 867 | Complete |
|
||||
| 05 | 3D data: Spatial & 3D (PointNet → Spatial AI) | 1421 | Complete |
|
||||
| 06 | Reinforcement learning (Q-Learning → RLHF) | 896 | Complete |
|
||||
| 07 | Generative AI (VAE/GAN → Diffusion) | 496 | Complete |
|
||||
| 08 | AI Agent (rules → LLM Agent) | 581 | Complete |
|
||||
|
||||
### Lower Volume: Design Applications
|
||||
|
||||
| Chapter | Topic | Lines | Status |
|
||||
|---------|-------|-------|--------|
|
||||
| 09 | Digital media design | 161 | Thin |
|
||||
| 10 | Industrial & product design | 145 | Thin |
|
||||
| 11 | Environmental & landscape design | 153 | Thin |
|
||||
| 12 | Urban & ecological planning | 317 | Moderate |
|
||||
|
||||
### Appendices
|
||||
|
||||
9 appendices covering programming basics, Vibe Coding, academic writing, web tools, references, glossary, and tips.
|
||||
|
||||
## Working with This Repository
|
||||
|
||||
### For Content Editing
|
||||
- Edit `.md` files in `officefile/` — they are the book source
|
||||
- Use Obsidian for rich editing (vault at project root)
|
||||
- Git commits follow conventional format: `docs(ch01): description`
|
||||
|
||||
### For DOCX Export
|
||||
- Use the `markdown-to-docx` skill in `.claude/skills/`
|
||||
- Publisher template: `machinery-industry-press-writing-template.dotx`
|
||||
- Output goes to `output/docx/`
|
||||
|
||||
### For Python Examples
|
||||
- Examples in `dofile/examples/` are self-contained
|
||||
- Run directly: `python dofile/examples/01-foundations/state_machine.py`
|
||||
- No global test suite — verify examples individually
|
||||
|
||||
## Language Considerations
|
||||
|
||||
- **Documentation**: Primarily Chinese with English technical terms
|
||||
- **Code**: Variable names and comments in English
|
||||
- **File names**: Mix of Chinese and English — use proper encoding
|
||||
|
||||
## Commit Convention
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
Types: feat, fix, docs, style, refactor, chore
|
||||
Scopes: ch01-ch12, appendix, supplements, examples
|
||||
|
||||
Examples:
|
||||
- docs(ch03): 补充 Transformer 注意力机制详解
|
||||
- feat(examples): 新增空间推理示例
|
||||
- fix(ch05): 修正 PointNet 结构描述
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 814 KiB |
@@ -0,0 +1,962 @@
|
||||
"""
|
||||
空间分析助手类 (Spatial Analysis Assistant)
|
||||
========================================
|
||||
|
||||
这是一个完整的空间分析助手实现,展示了如何构建一个基础的
|
||||
空间智能系统。该系统可以帮助用户进行空间数据处理、分析和可视化。
|
||||
|
||||
主要功能:
|
||||
1. 空间数据加载与管理
|
||||
2. 空间关系计算 (距离、方位、包含关系等)
|
||||
3. 空间统计分析
|
||||
4. 空间插值与预测
|
||||
5. 多准则决策分析
|
||||
|
||||
作者: CC4SI 项目组
|
||||
日期: 2025-01
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
from typing import List, Dict, Tuple, Optional, Any, Union
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import random
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 数据结构定义
|
||||
# ============================================================================
|
||||
|
||||
class GeometryType(Enum):
|
||||
"""几何类型枚举"""
|
||||
POINT = "Point"
|
||||
LINESTRING = "LineString"
|
||||
POLYGON = "Polygon"
|
||||
MULTIPOINT = "MultiPoint"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
"""点几何类"""
|
||||
x: float
|
||||
y: float
|
||||
z: Optional[float] = None
|
||||
properties: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.z is not None:
|
||||
return f"Point({self.x:.2f}, {self.y:.2f}, {self.z:.2f})"
|
||||
return f"Point({self.x:.2f}, {self.y:.2f})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BoundingBox:
|
||||
"""边界框类"""
|
||||
min_x: float
|
||||
min_y: float
|
||||
max_x: float
|
||||
max_y: float
|
||||
|
||||
@property
|
||||
def width(self) -> float:
|
||||
"""获取边界框宽度"""
|
||||
return self.max_x - self.min_x
|
||||
|
||||
@property
|
||||
def height(self) -> float:
|
||||
"""获取边界框高度"""
|
||||
return self.max_y - self.min_y
|
||||
|
||||
@property
|
||||
def center(self) -> Point:
|
||||
"""获取边界框中心点"""
|
||||
return Point(
|
||||
(self.min_x + self.max_x) / 2,
|
||||
(self.min_y + self.max_y) / 2
|
||||
)
|
||||
|
||||
def contains(self, point: Point) -> bool:
|
||||
"""检查点是否在边界框内"""
|
||||
return (self.min_x <= point.x <= self.max_x and
|
||||
self.min_y <= point.y <= self.max_y)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"BoundingBox[{self.min_x:.2f},{self.min_y:.2f} -> {self.max_x:.2f},{self.max_y:.2f}]"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpatialFeature:
|
||||
"""空间要素类"""
|
||||
id: str
|
||||
geometry: Union[Point, List[Point]] # 简化: 点或点列表
|
||||
properties: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SpatialFeature(id={self.id}, geometry={type(self.geometry).__name__})"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间分析助手主类
|
||||
# ============================================================================
|
||||
|
||||
class SpatialHelper:
|
||||
"""
|
||||
空间分析助手类
|
||||
|
||||
这是系统的核心类,提供空间数据处理和分析的主要功能。
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "空间分析助手", version: str = "1.0.0"):
|
||||
"""
|
||||
初始化空间分析助手
|
||||
|
||||
Args:
|
||||
name: 助手名称
|
||||
version: 版本号
|
||||
"""
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.features: List[SpatialFeature] = []
|
||||
self.coordinate_system: str = "EPSG:4326" # 默认 WGS84
|
||||
self.metadata: Dict[str, Any] = {}
|
||||
|
||||
print(f"[{self.name}] v{self.version} 初始化完成")
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 数据加载与管理
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def add_feature(self, feature: SpatialFeature) -> bool:
|
||||
"""
|
||||
添加空间要素
|
||||
|
||||
Args:
|
||||
feature: 要添加的空间要素
|
||||
|
||||
Returns:
|
||||
是否添加成功
|
||||
"""
|
||||
try:
|
||||
self.features.append(feature)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"添加要素失败: {e}")
|
||||
return False
|
||||
|
||||
def add_point(self, x: float, y: float, feature_id: str = None,
|
||||
properties: Dict[str, Any] = None) -> SpatialFeature:
|
||||
"""
|
||||
添加点要素
|
||||
|
||||
Args:
|
||||
x: X坐标
|
||||
y: Y坐标
|
||||
feature_id: 要素ID
|
||||
properties: 属性字典
|
||||
|
||||
Returns:
|
||||
创建的空间要素
|
||||
"""
|
||||
if feature_id is None:
|
||||
feature_id = f"point_{len(self.features)}"
|
||||
|
||||
point = Point(x, y)
|
||||
feature = SpatialFeature(
|
||||
id=feature_id,
|
||||
geometry=point,
|
||||
properties=properties or {}
|
||||
)
|
||||
self.add_feature(feature)
|
||||
return feature
|
||||
|
||||
def load_from_geojson(self, geojson_str: str) -> int:
|
||||
"""
|
||||
从 GeoJSON 字符串加载数据
|
||||
|
||||
Args:
|
||||
geojson_str: GeoJSON 格式字符串
|
||||
|
||||
Returns:
|
||||
加载的要素数量
|
||||
"""
|
||||
try:
|
||||
data = json.loads(geojson_str)
|
||||
count = 0
|
||||
|
||||
if data.get("type") == "FeatureCollection":
|
||||
for feature_data in data.get("features", []):
|
||||
feature = self._parse_geojson_feature(feature_data)
|
||||
if feature:
|
||||
self.add_feature(feature)
|
||||
count += 1
|
||||
|
||||
print(f"从 GeoJSON 加载了 {count} 个要素")
|
||||
return count
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"GeoJSON 解析失败: {e}")
|
||||
return 0
|
||||
|
||||
def _parse_geojson_feature(self, feature_data: Dict) -> Optional[SpatialFeature]:
|
||||
"""解析 GeoJSON 要素"""
|
||||
try:
|
||||
feature_id = feature_data.get("id", f"feature_{len(self.features)}")
|
||||
properties = feature_data.get("properties", {})
|
||||
geometry = feature_data.get("geometry", {})
|
||||
geom_type = geometry.get("type")
|
||||
|
||||
if geom_type == "Point":
|
||||
coordinates = geometry.get("coordinates", [])
|
||||
point = Point(coordinates[0], coordinates[1])
|
||||
return SpatialFeature(id=feature_id, geometry=point, properties=properties)
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_feature_by_id(self, feature_id: str) -> Optional[SpatialFeature]:
|
||||
"""根据ID获取要素"""
|
||||
for feature in self.features:
|
||||
if feature.id == feature_id:
|
||||
return feature
|
||||
return None
|
||||
|
||||
def get_feature_count(self) -> int:
|
||||
"""获取要素数量"""
|
||||
return len(self.features)
|
||||
|
||||
def clear_features(self) -> None:
|
||||
"""清空所有要素"""
|
||||
self.features.clear()
|
||||
print("已清空所有要素")
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 空间关系计算
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def calculate_distance(point1: Point, point2: Point) -> float:
|
||||
"""
|
||||
计算两点间的欧氏距离
|
||||
|
||||
Args:
|
||||
point1: 第一个点
|
||||
point2: 第二个点
|
||||
|
||||
Returns:
|
||||
距离值
|
||||
"""
|
||||
dx = point2.x - point1.x
|
||||
dy = point2.y - point1.y
|
||||
return math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
@staticmethod
|
||||
def calculate_bearing(point1: Point, point2: Point) -> float:
|
||||
"""
|
||||
计算从 point1 到 point2 的方位角 (度)
|
||||
|
||||
Args:
|
||||
point1: 起始点
|
||||
point2: 目标点
|
||||
|
||||
Returns:
|
||||
方位角 (0-360度)
|
||||
"""
|
||||
dx = point2.x - point1.x
|
||||
dy = point2.y - point1.y
|
||||
radians = math.atan2(dy, dx)
|
||||
degrees = math.degrees(radians)
|
||||
return (degrees + 360) % 360
|
||||
|
||||
@staticmethod
|
||||
def calculate_midpoint(point1: Point, point2: Point) -> Point:
|
||||
"""
|
||||
计算两点间的中点
|
||||
|
||||
Args:
|
||||
point1: 第一个点
|
||||
point2: 第二个点
|
||||
|
||||
Returns:
|
||||
中点
|
||||
"""
|
||||
return Point(
|
||||
(point1.x + point2.x) / 2,
|
||||
(point1.y + point2.y) / 2
|
||||
)
|
||||
|
||||
def find_nearest_neighbor(self, target_point: Point,
|
||||
max_distance: float = float('inf')) -> Optional[Tuple[SpatialFeature, float]]:
|
||||
"""
|
||||
查找最近邻要素
|
||||
|
||||
Args:
|
||||
target_point: 目标点
|
||||
max_distance: 最大搜索距离
|
||||
|
||||
Returns:
|
||||
(最近的要素, 距离) 或 None
|
||||
"""
|
||||
nearest_feature = None
|
||||
min_distance = float('inf')
|
||||
|
||||
for feature in self.features:
|
||||
if isinstance(feature.geometry, Point):
|
||||
dist = self.calculate_distance(target_point, feature.geometry)
|
||||
if dist < min_distance and dist <= max_distance:
|
||||
min_distance = dist
|
||||
nearest_feature = feature
|
||||
|
||||
if nearest_feature:
|
||||
return nearest_feature, min_distance
|
||||
return None
|
||||
|
||||
def find_neighbors_within_distance(self, target_point: Point,
|
||||
distance: float) -> List[Tuple[SpatialFeature, float]]:
|
||||
"""
|
||||
查找指定距离内的所有要素
|
||||
|
||||
Args:
|
||||
target_point: 目标点
|
||||
distance: 搜索半径
|
||||
|
||||
Returns:
|
||||
(要素, 距离) 列表,按距离排序
|
||||
"""
|
||||
neighbors = []
|
||||
|
||||
for feature in self.features:
|
||||
if isinstance(feature.geometry, Point):
|
||||
dist = self.calculate_distance(target_point, feature.geometry)
|
||||
if dist <= distance:
|
||||
neighbors.append((feature, dist))
|
||||
|
||||
neighbors.sort(key=lambda x: x[1])
|
||||
return neighbors
|
||||
|
||||
def calculate_bounding_box(self) -> Optional[BoundingBox]:
|
||||
"""
|
||||
计算所有要素的边界框
|
||||
|
||||
Returns:
|
||||
边界框对象,如果没有要素则返回 None
|
||||
"""
|
||||
if not self.features:
|
||||
return None
|
||||
|
||||
points = [f.geometry for f in self.features if isinstance(f.geometry, Point)]
|
||||
if not points:
|
||||
return None
|
||||
|
||||
min_x = min(p.x for p in points)
|
||||
max_x = max(p.x for p in points)
|
||||
min_y = min(p.y for p in points)
|
||||
max_y = max(p.y for p in points)
|
||||
|
||||
return BoundingBox(min_x, min_y, max_x, max_y)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 空间统计分析
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def calculate_centroid(self) -> Optional[Point]:
|
||||
"""
|
||||
计算所有点要素的质心
|
||||
|
||||
Returns:
|
||||
质心点
|
||||
"""
|
||||
points = [f.geometry for f in self.features if isinstance(f.geometry, Point)]
|
||||
if not points:
|
||||
return None
|
||||
|
||||
avg_x = sum(p.x for p in points) / len(points)
|
||||
avg_y = sum(p.y for p in points) / len(points)
|
||||
|
||||
return Point(avg_x, avg_y)
|
||||
|
||||
def calculate_mean_center(self, weight_field: str = None) -> Optional[Point]:
|
||||
"""
|
||||
计算加权或未加权的平均中心
|
||||
|
||||
Args:
|
||||
weight_field: 权重字段名
|
||||
|
||||
Returns:
|
||||
平均中心点
|
||||
"""
|
||||
points = []
|
||||
weights = []
|
||||
|
||||
for feature in self.features:
|
||||
if isinstance(feature.geometry, Point):
|
||||
points.append(feature.geometry)
|
||||
if weight_field:
|
||||
weights.append(feature.properties.get(weight_field, 1.0))
|
||||
else:
|
||||
weights.append(1.0)
|
||||
|
||||
if not points:
|
||||
return None
|
||||
|
||||
total_weight = sum(weights)
|
||||
avg_x = sum(p.x * w for p, w in zip(points, weights)) / total_weight
|
||||
avg_y = sum(p.y * w for p, w in zip(points, weights)) / total_weight
|
||||
|
||||
return Point(avg_x, avg_y)
|
||||
|
||||
def calculate_standard_distance(self) -> Optional[float]:
|
||||
"""
|
||||
计算标准距离 (标准差圆)
|
||||
|
||||
Returns:
|
||||
标准距离值
|
||||
"""
|
||||
centroid = self.calculate_centroid()
|
||||
if not centroid:
|
||||
return None
|
||||
|
||||
points = [f.geometry for f in self.features if isinstance(f.geometry, Point)]
|
||||
if not points:
|
||||
return None
|
||||
|
||||
n = len(points)
|
||||
squared_distances = [(p.x - centroid.x)**2 + (p.y - centroid.y)**2 for p in points]
|
||||
|
||||
return math.sqrt(sum(squared_distances) / n)
|
||||
|
||||
def calculate_spatial_autocorrelation(self, field: str) -> Optional[float]:
|
||||
"""
|
||||
计算 Moran's I 空间自相关指数
|
||||
|
||||
Args:
|
||||
field: 要分析的属性字段
|
||||
|
||||
Returns:
|
||||
Moran's I 值
|
||||
"""
|
||||
# 简化实现: 使用距离权重
|
||||
points_data = []
|
||||
for feature in self.features:
|
||||
if isinstance(feature.geometry, Point) and field in feature.properties:
|
||||
points_data.append((feature.geometry, feature.properties[field]))
|
||||
|
||||
n = len(points_data)
|
||||
if n < 2:
|
||||
return None
|
||||
|
||||
mean_value = sum(v for _, v in points_data) / n
|
||||
|
||||
# 计算权重矩阵 (距离倒数)
|
||||
weights = {}
|
||||
total_weight = 0
|
||||
for i, (p1, v1) in enumerate(points_data):
|
||||
for j, (p2, v2) in enumerate(points_data):
|
||||
if i != j:
|
||||
dist = self.calculate_distance(p1, p2)
|
||||
w = 1 / (dist + 0.001) # 避免除零
|
||||
weights[(i, j)] = w
|
||||
total_weight += w
|
||||
|
||||
# 计算 Moran's I
|
||||
numerator = 0
|
||||
denominator = 0
|
||||
|
||||
for i, (p1, v1) in enumerate(points_data):
|
||||
for j, (p2, v2) in enumerate(points_data):
|
||||
if i != j:
|
||||
w = weights.get((i, j), 0)
|
||||
numerator += w * (v1 - mean_value) * (v2 - mean_value)
|
||||
denominator += (v1 - mean_value) ** 2
|
||||
|
||||
if denominator == 0:
|
||||
return None
|
||||
|
||||
morans_i = (n / total_weight) * (numerator / denominator)
|
||||
return morans_i
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 空间插值与预测
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def inverse_distance_weighting(self, target_point: Point, power: float = 2.0,
|
||||
field: str = "value", max_distance: float = None) -> Optional[float]:
|
||||
"""
|
||||
反距离加权插值 (IDW)
|
||||
|
||||
Args:
|
||||
target_point: 目标点
|
||||
power: 距离幂次
|
||||
field: 插值字段
|
||||
max_distance: 最大搜索距离
|
||||
|
||||
Returns:
|
||||
插值结果
|
||||
"""
|
||||
points_data = []
|
||||
for feature in self.features:
|
||||
if isinstance(feature.geometry, Point) and field in feature.properties:
|
||||
points_data.append((feature.geometry, feature.properties[field]))
|
||||
|
||||
if not points_data:
|
||||
return None
|
||||
|
||||
numerator = 0.0
|
||||
denominator = 0.0
|
||||
|
||||
for point, value in points_data:
|
||||
dist = self.calculate_distance(target_point, point)
|
||||
|
||||
if max_distance and dist > max_distance:
|
||||
continue
|
||||
|
||||
if dist < 1e-10: # 几乎重合
|
||||
return value
|
||||
|
||||
weight = 1.0 / (dist ** power)
|
||||
numerator += weight * value
|
||||
denominator += weight
|
||||
|
||||
if denominator == 0:
|
||||
return None
|
||||
|
||||
return numerator / denominator
|
||||
|
||||
def simple_trend_prediction(self, field: str, target_x: float,
|
||||
target_y: float) -> Optional[float]:
|
||||
"""
|
||||
基于简单趋势的预测 (线性回归)
|
||||
|
||||
Args:
|
||||
field: 预测字段
|
||||
target_x: 目标X坐标
|
||||
target_y: 目标Y坐标
|
||||
|
||||
Returns:
|
||||
预测值
|
||||
"""
|
||||
points_data = []
|
||||
for feature in self.features:
|
||||
if isinstance(feature.geometry, Point) and field in feature.properties:
|
||||
points_data.append({
|
||||
'x': feature.geometry.x,
|
||||
'y': feature.geometry.y,
|
||||
'z': feature.properties[field]
|
||||
})
|
||||
|
||||
if len(points_data) < 3:
|
||||
return None
|
||||
|
||||
# 简单的线性趋势: z = a + b*x + c*y
|
||||
n = len(points_data)
|
||||
|
||||
sum_x = sum(p['x'] for p in points_data)
|
||||
sum_y = sum(p['y'] for p in points_data)
|
||||
sum_z = sum(p['z'] for p in points_data)
|
||||
sum_xx = sum(p['x']**2 for p in points_data)
|
||||
sum_yy = sum(p['y']**2 for p in points_data)
|
||||
sum_xy = sum(p['x'] * p['y'] for p in points_data)
|
||||
sum_xz = sum(p['x'] * p['z'] for p in points_data)
|
||||
sum_yz = sum(p['y'] * p['z'] for p in points_data)
|
||||
|
||||
# 简化: 只使用x方向趋势
|
||||
try:
|
||||
# z = a + b*x
|
||||
b = (n * sum_xz - sum_x * sum_z) / (n * sum_xx - sum_x**2)
|
||||
a = (sum_z - b * sum_x) / n
|
||||
return a + b * target_x
|
||||
except ZeroDivisionError:
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 多准则决策分析 (MCDA)
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def weighted_sum_model(self, criteria: List[str], weights: List[float],
|
||||
feature_ids: List[str] = None) -> List[Tuple[SpatialFeature, float]]:
|
||||
"""
|
||||
加权求和模型 (WSM)
|
||||
|
||||
Args:
|
||||
criteria: 评价准则列表
|
||||
weights: 各准则权重
|
||||
feature_ids: 参与评价的要素ID列表
|
||||
|
||||
Returns:
|
||||
(要素, 得分) 列表,按得分降序排列
|
||||
"""
|
||||
if len(criteria) != len(weights):
|
||||
print("错误: 准则数量与权重数量不匹配")
|
||||
return []
|
||||
|
||||
if abs(sum(weights) - 1.0) > 0.001:
|
||||
print(f"警告: 权重总和为 {sum(weights)}, 建议归一化为 1.0")
|
||||
|
||||
# 确定评价范围
|
||||
features_to_eval = self.features
|
||||
if feature_ids:
|
||||
features_to_eval = [f for f in self.features if f.id in feature_ids]
|
||||
|
||||
results = []
|
||||
|
||||
# 归一化参数
|
||||
min_max = {}
|
||||
for criterion in criteria:
|
||||
values = []
|
||||
for f in features_to_eval:
|
||||
if criterion in f.properties:
|
||||
values.append(f.properties[criterion])
|
||||
if values:
|
||||
min_max[criterion] = (min(values), max(values))
|
||||
|
||||
for feature in features_to_eval:
|
||||
score = 0.0
|
||||
valid = True
|
||||
|
||||
for criterion, weight in zip(criteria, weights):
|
||||
if criterion not in feature.properties:
|
||||
valid = False
|
||||
break
|
||||
|
||||
value = feature.properties[criterion]
|
||||
cmin, cmax = min_max.get(criterion, (0, 1))
|
||||
|
||||
# 归一化 (假设值越大越好)
|
||||
if cmax - cmin > 0:
|
||||
normalized = (value - cmin) / (cmax - cmin)
|
||||
else:
|
||||
normalized = 0.5
|
||||
|
||||
score += weight * normalized
|
||||
|
||||
if valid:
|
||||
results.append((feature, score))
|
||||
|
||||
results.sort(key=lambda x: x[1], reverse=True)
|
||||
return results
|
||||
|
||||
def weighted_product_model(self, criteria: List[str], weights: List[float],
|
||||
feature_ids: List[str] = None) -> List[Tuple[SpatialFeature, float]]:
|
||||
"""
|
||||
加权乘积模型 (WPM)
|
||||
|
||||
Args:
|
||||
criteria: 评价准则列表
|
||||
weights: 各准则权重
|
||||
feature_ids: 参与评价的要素ID列表
|
||||
|
||||
Returns:
|
||||
(要素, 得分) 列表,按得分降序排列
|
||||
"""
|
||||
if len(criteria) != len(weights):
|
||||
print("错误: 准则数量与权重数量不匹配")
|
||||
return []
|
||||
|
||||
features_to_eval = self.features
|
||||
if feature_ids:
|
||||
features_to_eval = [f for f in self.features if f.id in feature_ids]
|
||||
|
||||
results = []
|
||||
|
||||
# 归一化参数
|
||||
min_max = {}
|
||||
for criterion in criteria:
|
||||
values = []
|
||||
for f in features_to_eval:
|
||||
if criterion in f.properties:
|
||||
values.append(f.properties[criterion])
|
||||
if values:
|
||||
min_max[criterion] = (min(values), max(values))
|
||||
|
||||
for feature in features_to_eval:
|
||||
product = 1.0
|
||||
valid = True
|
||||
|
||||
for criterion, weight in zip(criteria, weights):
|
||||
if criterion not in feature.properties:
|
||||
valid = False
|
||||
break
|
||||
|
||||
value = feature.properties[criterion]
|
||||
cmin, cmax = min_max.get(criterion, (0, 1))
|
||||
|
||||
if cmax - cmin > 0:
|
||||
normalized = (value - cmin) / (cmax - cmin)
|
||||
else:
|
||||
normalized = 1.0
|
||||
|
||||
product *= normalized ** weight
|
||||
|
||||
if valid:
|
||||
results.append((feature, product))
|
||||
|
||||
results.sort(key=lambda x: x[1], reverse=True)
|
||||
return results
|
||||
|
||||
def topsis(self, criteria: List[str], weights: List[float],
|
||||
benefit_criteria: List[bool] = None) -> List[Tuple[SpatialFeature, float]]:
|
||||
"""
|
||||
TOPSIS (逼近理想解排序法)
|
||||
|
||||
Args:
|
||||
criteria: 评价准则列表
|
||||
weights: 各准则权重
|
||||
benefit_criteria: 是否为效益型准则 (True=越大越好, False=越小越好)
|
||||
|
||||
Returns:
|
||||
(要素, 相对贴近度) 列表,按贴近度降序排列
|
||||
"""
|
||||
if benefit_criteria is None:
|
||||
benefit_criteria = [True] * len(criteria)
|
||||
|
||||
features_to_eval = [f for f in self.features
|
||||
if all(c in f.properties for c in criteria)]
|
||||
|
||||
if len(features_to_eval) == 0:
|
||||
return []
|
||||
|
||||
n = len(features_to_eval)
|
||||
m = len(criteria)
|
||||
|
||||
# 构建决策矩阵
|
||||
decision_matrix = []
|
||||
for feature in features_to_eval:
|
||||
row = [feature.properties[c] for c in criteria]
|
||||
decision_matrix.append(row)
|
||||
|
||||
# 归一化决策矩阵
|
||||
normalized_matrix = []
|
||||
for j in range(m):
|
||||
column = [decision_matrix[i][j] for i in range(n)]
|
||||
norm = math.sqrt(sum(x**2 for x in column))
|
||||
for i in range(n):
|
||||
if j == 0:
|
||||
normalized_matrix.append([])
|
||||
normalized_matrix[i].append(decision_matrix[i][j] / norm if norm > 0 else 0)
|
||||
|
||||
# 加权归一化矩阵
|
||||
weighted_matrix = []
|
||||
for i in range(n):
|
||||
weighted_matrix.append([normalized_matrix[i][j] * weights[j] for j in range(m)])
|
||||
|
||||
# 确定理想解和负理想解
|
||||
ideal_positive = []
|
||||
ideal_negative = []
|
||||
|
||||
for j in range(m):
|
||||
column = [weighted_matrix[i][j] for i in range(n)]
|
||||
if benefit_criteria[j]:
|
||||
ideal_positive.append(max(column))
|
||||
ideal_negative.append(min(column))
|
||||
else:
|
||||
ideal_positive.append(min(column))
|
||||
ideal_negative.append(max(column))
|
||||
|
||||
# 计算距离和相对贴近度
|
||||
results = []
|
||||
for i, feature in enumerate(features_to_eval):
|
||||
dist_positive = math.sqrt(
|
||||
sum((weighted_matrix[i][j] - ideal_positive[j])**2 for j in range(m))
|
||||
)
|
||||
dist_negative = math.sqrt(
|
||||
sum((weighted_matrix[i][j] - ideal_negative[j])**2 for j in range(m))
|
||||
)
|
||||
|
||||
closeness = dist_negative / (dist_positive + dist_negative) if (dist_positive + dist_negative) > 0 else 0
|
||||
results.append((feature, closeness))
|
||||
|
||||
results.sort(key=lambda x: x[1], reverse=True)
|
||||
return results
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 缓冲区分析
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def create_buffer_analysis(self, feature_id: str, buffer_distance: float) -> Dict[str, Any]:
|
||||
"""
|
||||
缓冲区分析
|
||||
|
||||
Args:
|
||||
feature_id: 中心要素ID
|
||||
buffer_distance: 缓冲距离
|
||||
|
||||
Returns:
|
||||
分析结果字典
|
||||
"""
|
||||
target_feature = self.get_feature_by_id(feature_id)
|
||||
if not target_feature or not isinstance(target_feature.geometry, Point):
|
||||
return {"error": "找不到指定的点要素"}
|
||||
|
||||
center = target_feature.geometry
|
||||
|
||||
# 查找缓冲区内的要素
|
||||
features_in_buffer = self.find_neighbors_within_distance(center, buffer_distance)
|
||||
|
||||
# 计算统计信息
|
||||
values_in_buffer = [f.properties for f, _ in features_in_buffer]
|
||||
|
||||
return {
|
||||
"center_feature": feature_id,
|
||||
"buffer_distance": buffer_distance,
|
||||
"count": len(features_in_buffer),
|
||||
"features": [(f.id, dist) for f, dist in features_in_buffer],
|
||||
"statistics": {
|
||||
"avg_distance": sum(dist for _, dist in features_in_buffer) / len(features_in_buffer) if features_in_buffer else 0
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 可视化辅助 (文本形式)
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def print_summary(self) -> None:
|
||||
"""打印数据摘要"""
|
||||
print(f"\n{'='*50}")
|
||||
print(f"空间分析助手摘要: {self.name} v{self.version}")
|
||||
print(f"{'='*50}")
|
||||
print(f"要素数量: {len(self.features)}")
|
||||
print(f"坐标系: {self.coordinate_system}")
|
||||
|
||||
bbox = self.calculate_bounding_box()
|
||||
if bbox:
|
||||
print(f"边界范围: {bbox}")
|
||||
|
||||
centroid = self.calculate_centroid()
|
||||
if centroid:
|
||||
print(f"质心位置: {centroid}")
|
||||
|
||||
print(f"{'='*50}\n")
|
||||
|
||||
def print_features(self, limit: int = 10) -> None:
|
||||
"""打印要素列表"""
|
||||
print(f"\n要素列表 (显示前 {min(limit, len(self.features))} 个):")
|
||||
print("-" * 60)
|
||||
|
||||
for i, feature in enumerate(self.features[:limit]):
|
||||
if isinstance(feature.geometry, Point):
|
||||
print(f"{i+1}. ID: {feature.id:15s} 位置: {feature.geometry} 属性: {feature.properties}")
|
||||
|
||||
if len(self.features) > limit:
|
||||
print(f"... 还有 {len(self.features) - limit} 个要素")
|
||||
|
||||
print("-" * 60 + "\n")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 辅助函数
|
||||
# ============================================================================
|
||||
|
||||
def create_sample_data(helper: SpatialHelper, n_points: int = 20) -> None:
|
||||
"""创建示例数据"""
|
||||
print(f"生成 {n_points} 个随机样本点...")
|
||||
|
||||
random.seed(42) # 可重现的随机数
|
||||
|
||||
# 生成随机点
|
||||
for i in range(n_points):
|
||||
x = random.uniform(0, 100)
|
||||
y = random.uniform(0, 100)
|
||||
value = random.uniform(0, 100)
|
||||
population = random.randint(100, 10000)
|
||||
accessibility = random.uniform(0.3, 0.95)
|
||||
|
||||
helper.add_point(
|
||||
x=x,
|
||||
y=y,
|
||||
feature_id=f"point_{i:03d}",
|
||||
properties={
|
||||
"value": value,
|
||||
"population": population,
|
||||
"accessibility": accessibility,
|
||||
"name": f"位置_{i+1}"
|
||||
}
|
||||
)
|
||||
|
||||
print(f"已生成 {helper.get_feature_count()} 个样本点")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示空间分析助手的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("空间分析助手 - 完整示例演示")
|
||||
print("="*70)
|
||||
|
||||
# 1. 创建助手实例
|
||||
print("\n[步骤 1] 创建空间分析助手")
|
||||
helper = SpatialHelper(name="城市空间分析助手", version="1.0.0")
|
||||
|
||||
# 2. 添加示例数据
|
||||
print("\n[步骤 2] 添加示例数据")
|
||||
create_sample_data(helper, n_points=15)
|
||||
|
||||
# 3. 打印数据摘要
|
||||
print("\n[步骤 3] 数据摘要")
|
||||
helper.print_summary()
|
||||
helper.print_features()
|
||||
|
||||
# 4. 空间关系计算
|
||||
print("\n[步骤 4] 空间关系计算")
|
||||
print("-" * 50)
|
||||
|
||||
test_point = Point(50, 50)
|
||||
print(f"测试点: {test_point}")
|
||||
|
||||
nearest = helper.find_nearest_neighbor(test_point)
|
||||
if nearest:
|
||||
feature, dist = nearest
|
||||
print(f"最近邻: {feature.id}, 距离: {dist:.2f}")
|
||||
|
||||
neighbors = helper.find_neighbors_within_distance(test_point, 25)
|
||||
print(f"半径25内的邻居数量: {len(neighbors)}")
|
||||
|
||||
# 5. 空间统计分析
|
||||
print("\n[步骤 5] 空间统计分析")
|
||||
print("-" * 50)
|
||||
|
||||
centroid = helper.calculate_centroid()
|
||||
print(f"质心: {centroid}")
|
||||
|
||||
std_dist = helper.calculate_standard_distance()
|
||||
print(f"标准距离: {std_dist:.2f}")
|
||||
|
||||
# 6. 多准则决策分析
|
||||
print("\n[步骤 6] 多准则决策分析 (TOPSIS)")
|
||||
print("-" * 50)
|
||||
|
||||
criteria = ["accessibility", "population"]
|
||||
weights = [0.6, 0.4] # 可达性权重更高
|
||||
|
||||
results = helper.topsis(criteria, weights)
|
||||
print("选址优先级排序 (基于可达性和人口):")
|
||||
for i, (feature, score) in enumerate(results[:5]):
|
||||
print(f" {i+1}. {feature.properties.get('name', feature.id)}: 得分={score:.4f}")
|
||||
|
||||
# 7. 空间插值
|
||||
print("\n[步骤 7] 空间插值预测")
|
||||
print("-" * 50)
|
||||
|
||||
predict_point = Point(45, 55)
|
||||
predicted = helper.inverse_distance_weighting(predict_point, field="value")
|
||||
print(f"在 {predict_point} 处的插值预测: {predicted:.2f}")
|
||||
|
||||
# 8. 缓冲区分析
|
||||
print("\n[步骤 8] 缓冲区分析")
|
||||
print("-" * 50)
|
||||
|
||||
buffer_result = helper.create_buffer_analysis("point_000", 30)
|
||||
print(f"以 point_000 为中心,半径30的缓冲区:")
|
||||
print(f" 包含要素数: {buffer_result['count']}")
|
||||
print(f" 平均距离: {buffer_result['statistics']['avg_distance']:.2f}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,827 @@
|
||||
"""
|
||||
反馈与学习示例 (Feedback and Learning Example)
|
||||
=============================================
|
||||
|
||||
本示例展示如何在空间智能系统中实现反馈机制和学习能力。
|
||||
反馈和学习使系统能够从经验中改进,提高决策质量。
|
||||
|
||||
核心概念:
|
||||
1. 反馈循环 - 收集用户/系统的反馈
|
||||
2. 性能评估 - 评估决策效果
|
||||
3. 参数调整 - 根据反馈调整系统参数
|
||||
4. 经验存储 - 保存和检索历史经验
|
||||
5. 迁移学习 - 将知识应用到新场景
|
||||
|
||||
应用场景:
|
||||
- 自适应决策权重调整
|
||||
- 模型参数优化
|
||||
- 用户偏好学习
|
||||
- 决策效果跟踪
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
import math
|
||||
import json
|
||||
from typing import List, Dict, Tuple, Optional, Any, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 反馈类型定义
|
||||
# ============================================================================
|
||||
|
||||
class FeedbackType(Enum):
|
||||
"""反馈类型枚举"""
|
||||
EXPLICIT = "explicit" # 显式反馈 (用户评分/评价)
|
||||
IMPLICIT = "implicit" # 隐式反馈 (行为数据)
|
||||
OUTCOME = "outcome" # 结果反馈 (实际结果)
|
||||
CORRECTION = "correction" # 纠正反馈 (修正建议)
|
||||
RANKING = "ranking" # 排序反馈 (偏好排序)
|
||||
|
||||
|
||||
class FeedbackSource(Enum):
|
||||
"""反馈来源枚举"""
|
||||
HUMAN_EXPERT = "human_expert" # 人类专家
|
||||
SYSTEM_AUTO = "system_auto" # 系统自动
|
||||
SENSOR_DATA = "sensor_data" # 传感器数据
|
||||
CROWDSOURCING = "crowdsourcing" # 众包
|
||||
PEER_REVIEW = "peer_review" # 同行评审
|
||||
|
||||
|
||||
@dataclass
|
||||
class Feedback:
|
||||
"""
|
||||
反馈数据结构
|
||||
|
||||
表示一次具体的反馈事件。
|
||||
"""
|
||||
feedback_id: str
|
||||
feedback_type: FeedbackType
|
||||
source: FeedbackSource
|
||||
target_decision_id: str
|
||||
value: float # 反馈值 (如评分)
|
||||
content: Optional[str] = None # 反馈内容
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Feedback({self.feedback_type.value}, value={self.value:.2f})"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 决策记录
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class Decision:
|
||||
"""
|
||||
决策记录
|
||||
|
||||
保存系统做出的一次决策的完整信息。
|
||||
"""
|
||||
decision_id: str
|
||||
context: Dict[str, Any] # 决策上下文
|
||||
alternatives: List[Dict[str, Any]] # 可选方案
|
||||
selected_alternative: int # 选择的方案索引
|
||||
model_version: str # 使用的模型版本
|
||||
parameters: Dict[str, Any] # 决策参数
|
||||
predicted_outcome: Optional[float] = None # 预测结果
|
||||
actual_outcome: Optional[float] = None # 实际结果
|
||||
feedback_list: List[Feedback] = field(default_factory=list)
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def add_feedback(self, feedback: Feedback) -> None:
|
||||
"""添加反馈"""
|
||||
self.feedback_list.append(feedback)
|
||||
|
||||
def get_average_feedback(self) -> float:
|
||||
"""获取平均反馈分数"""
|
||||
if not self.feedback_list:
|
||||
return 0.0
|
||||
return sum(f.value for f in self.feedback_list) / len(self.feedback_list)
|
||||
|
||||
def get_outcome_error(self) -> Optional[float]:
|
||||
"""获取预测误差"""
|
||||
if self.predicted_outcome is not None and self.actual_outcome is not None:
|
||||
return abs(self.predicted_outcome - self.actual_outcome)
|
||||
return None
|
||||
|
||||
def calculate_regret(self) -> float:
|
||||
"""
|
||||
计算后悔值
|
||||
|
||||
后悔值 = 最优选择的结果 - 实际选择的结果
|
||||
"""
|
||||
if not self.alternatives or self.actual_outcome is None:
|
||||
return 0.0
|
||||
|
||||
# 假设alternatives中存储了各个选项的实际结果
|
||||
best_outcome = max(
|
||||
alt.get("actual_outcome", self.actual_outcome)
|
||||
for alt in self.alternatives
|
||||
)
|
||||
return best_outcome - self.actual_outcome
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 经验存储
|
||||
# ============================================================================
|
||||
|
||||
class ExperienceStore:
|
||||
"""
|
||||
经验存储
|
||||
|
||||
存储和检索历史决策经验,用于学习和改进。
|
||||
"""
|
||||
|
||||
def __init__(self, capacity: int = 1000):
|
||||
"""
|
||||
初始化经验存储
|
||||
|
||||
Args:
|
||||
capacity: 最大存储容量
|
||||
"""
|
||||
self.capacity = capacity
|
||||
self.decisions: Dict[str, Decision] = {}
|
||||
self.decision_list: List[str] = [] # 按时间顺序的ID列表
|
||||
|
||||
def add_decision(self, decision: Decision) -> None:
|
||||
"""添加决策记录"""
|
||||
self.decisions[decision.decision_id] = decision
|
||||
self.decision_list.append(decision.decision_id)
|
||||
|
||||
# 超过容量时删除最旧的
|
||||
if len(self.decision_list) > self.capacity:
|
||||
oldest_id = self.decision_list.pop(0)
|
||||
del self.decisions[oldest_id]
|
||||
|
||||
def get_decision(self, decision_id: str) -> Optional[Decision]:
|
||||
"""获取决策记录"""
|
||||
return self.decisions.get(decision_id)
|
||||
|
||||
def get_recent_decisions(self, n: int = 10) -> List[Decision]:
|
||||
"""获取最近的n条决策"""
|
||||
recent_ids = self.decision_list[-n:]
|
||||
return [self.decisions[id] for id in recent_ids]
|
||||
|
||||
def find_similar_decisions(self, context: Dict[str, Any],
|
||||
threshold: float = 0.8) -> List[Decision]:
|
||||
"""
|
||||
查找相似上下文的决策
|
||||
|
||||
Args:
|
||||
context: 目标上下文
|
||||
threshold: 相似度阈值
|
||||
|
||||
Returns:
|
||||
相似决策列表
|
||||
"""
|
||||
similar = []
|
||||
|
||||
for decision in self.decisions.values():
|
||||
similarity = self._calculate_similarity(context, decision.context)
|
||||
if similarity >= threshold:
|
||||
similar.append((decision, similarity))
|
||||
|
||||
similar.sort(key=lambda x: x[1], reverse=True)
|
||||
return [d for d, _ in similar]
|
||||
|
||||
def _calculate_similarity(self, ctx1: Dict[str, Any],
|
||||
ctx2: Dict[str, Any]) -> float:
|
||||
"""计算上下文相似度 (简化版本)"""
|
||||
# 简化: 使用键的交集比例
|
||||
keys1 = set(ctx1.keys())
|
||||
keys2 = set(ctx2.keys())
|
||||
intersection = keys1 & keys2
|
||||
union = keys1 | keys2
|
||||
|
||||
if not union:
|
||||
return 0.0
|
||||
|
||||
# 值相似度
|
||||
value_similarity = 0.0
|
||||
count = 0
|
||||
|
||||
for key in intersection:
|
||||
v1 = ctx1.get(key)
|
||||
v2 = ctx2.get(key)
|
||||
if isinstance(v1, (int, float)) and isinstance(v2, (int, float)):
|
||||
# 归一化差异
|
||||
max_val = max(abs(v1), abs(v2), 1)
|
||||
diff = abs(v1 - v2) / max_val
|
||||
value_similarity += (1 - diff)
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
value_similarity /= count
|
||||
|
||||
# 组合相似度
|
||||
key_similarity = len(intersection) / len(union)
|
||||
return 0.3 * key_similarity + 0.7 * value_similarity
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
total = len(self.decisions)
|
||||
|
||||
if total == 0:
|
||||
return {"total_decisions": 0}
|
||||
|
||||
with_feedback = sum(1 for d in self.decisions.values() if d.feedback_list)
|
||||
with_outcome = sum(1 for d in self.decisions.values()
|
||||
if d.actual_outcome is not None)
|
||||
|
||||
avg_feedback = sum(d.get_average_feedback()
|
||||
for d in self.decisions.values()
|
||||
if d.feedback_list) / max(with_feedback, 1)
|
||||
|
||||
return {
|
||||
"total_decisions": total,
|
||||
"decisions_with_feedback": with_feedback,
|
||||
"decisions_with_outcome": with_outcome,
|
||||
"average_feedback_score": avg_feedback
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 学习器接口
|
||||
# ============================================================================
|
||||
|
||||
class Learner(ABC):
|
||||
"""学习器抽象基类"""
|
||||
|
||||
def __init__(self, name: str = ""):
|
||||
self.name = name
|
||||
|
||||
@abstractmethod
|
||||
def learn_from_feedback(self, decision: Decision, feedback: Feedback) -> None:
|
||||
"""从反馈中学习"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def learn_from_outcome(self, decision: Decision) -> None:
|
||||
"""从结果中学习"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_parameters(self) -> Dict[str, Any]:
|
||||
"""获取当前参数"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_parameters(self, params: Dict[str, Any]) -> None:
|
||||
"""更新参数"""
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 权重学习器
|
||||
# ============================================================================
|
||||
|
||||
class WeightLearner(Learner):
|
||||
"""
|
||||
权重学习器
|
||||
|
||||
通过反馈学习多准则决策的权重。
|
||||
"""
|
||||
|
||||
def __init__(self, initial_weights: List[float],
|
||||
learning_rate: float = 0.1,
|
||||
min_weight: float = 0.05,
|
||||
max_weight: float = 0.5):
|
||||
"""
|
||||
初始化权重学习器
|
||||
|
||||
Args:
|
||||
initial_weights: 初始权重列表
|
||||
learning_rate: 学习率
|
||||
min_weight: 最小权重
|
||||
max_weight: 最大权重
|
||||
"""
|
||||
super().__init__("WeightLearner")
|
||||
self.weights = initial_weights.copy()
|
||||
self.learning_rate = learning_rate
|
||||
self.min_weight = min_weight
|
||||
self.max_weight = max_weight
|
||||
self.update_count = 0
|
||||
|
||||
def learn_from_feedback(self, decision: Decision, feedback: Feedback) -> None:
|
||||
"""
|
||||
从反馈中学习权重
|
||||
|
||||
使用梯度下降法调整权重:
|
||||
- 如果反馈为正,增加选中选项的优势准则权重
|
||||
- 如果反馈为负,减少选中选项的优势准则权重
|
||||
"""
|
||||
if not decision.alternatives or decision.selected_alternative >= len(decision.alternatives):
|
||||
return
|
||||
|
||||
selected = decision.alternatives[decision.selected_alternative]
|
||||
|
||||
# 计算调整方向
|
||||
feedback_normalized = (feedback.value - 0.5) * 2 # 转换到 [-1, 1]
|
||||
|
||||
# 获取准则值 (假设存储在criteria字段)
|
||||
criteria_values = selected.get("criteria", [])
|
||||
|
||||
if len(criteria_values) != len(self.weights):
|
||||
return
|
||||
|
||||
# 计算梯度
|
||||
# 简化: 增加高值准则的权重 (如果反馈为正)
|
||||
max_value = max(criteria_values) if criteria_values else 1
|
||||
gradients = []
|
||||
|
||||
for i, value in enumerate(criteria_values):
|
||||
# 归一化值
|
||||
norm_value = value / max_value if max_value > 0 else 0
|
||||
# 梯度: 高值准则应该有更大权重
|
||||
gradient = (norm_value - 0.5) * feedback_normalized
|
||||
gradients.append(gradient)
|
||||
|
||||
# 更新权重
|
||||
for i, gradient in enumerate(gradients):
|
||||
self.weights[i] += self.learning_rate * gradient
|
||||
|
||||
# 归一化权重
|
||||
self._normalize_weights()
|
||||
self.update_count += 1
|
||||
|
||||
def learn_from_outcome(self, decision: Decision) -> None:
|
||||
"""
|
||||
从结果中学习
|
||||
|
||||
如果实际结果好于预期,增加选中策略的权重
|
||||
"""
|
||||
if decision.predicted_outcome is None or decision.actual_outcome is None:
|
||||
return
|
||||
|
||||
# 计算结果误差
|
||||
error = decision.actual_outcome - decision.predicted_outcome
|
||||
|
||||
# 归一化误差
|
||||
error_normalized = math.tanh(error / 100) # 假设100为合理的误差范围
|
||||
|
||||
# 根据误差调整权重
|
||||
feedback = Feedback(
|
||||
feedback_id=f"outcome_{decision.decision_id}",
|
||||
feedback_type=FeedbackType.OUTCOME,
|
||||
source=FeedbackSource.SYSTEM_AUTO,
|
||||
target_decision_id=decision.decision_id,
|
||||
value=0.5 + error_normalized * 0.25 # 转换到合理范围
|
||||
)
|
||||
|
||||
self.learn_from_feedback(decision, feedback)
|
||||
|
||||
def _normalize_weights(self) -> None:
|
||||
"""归一化权重并限制范围"""
|
||||
# 限制范围
|
||||
self.weights = [
|
||||
max(self.min_weight, min(self.max_weight, w))
|
||||
for w in self.weights
|
||||
]
|
||||
|
||||
# 归一化使和为1
|
||||
total = sum(self.weights)
|
||||
self.weights = [w / total for w in self.weights]
|
||||
|
||||
def get_parameters(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"weights": self.weights,
|
||||
"learning_rate": self.learning_rate,
|
||||
"update_count": self.update_count
|
||||
}
|
||||
|
||||
def update_parameters(self, params: Dict[str, Any]) -> None:
|
||||
if "weights" in params:
|
||||
self.weights = params["weights"].copy()
|
||||
if "learning_rate" in params:
|
||||
self.learning_rate = params["learning_rate"]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 自适应决策系统
|
||||
# ============================================================================
|
||||
|
||||
class AdaptiveDecisionSystem:
|
||||
"""
|
||||
自适应决策系统
|
||||
|
||||
结合反馈和学习的智能决策系统。
|
||||
"""
|
||||
|
||||
def __init__(self, criteria: List[str],
|
||||
initial_weights: List[float] = None):
|
||||
"""
|
||||
初始化自适应决策系统
|
||||
|
||||
Args:
|
||||
criteria: 决策准则列表
|
||||
initial_weights: 初始权重
|
||||
"""
|
||||
self.criteria = criteria
|
||||
self.n_criteria = len(criteria)
|
||||
|
||||
if initial_weights is None:
|
||||
# 均匀初始权重
|
||||
initial_weights = [1.0 / self.n_criteria] * self.n_criteria
|
||||
|
||||
# 归一化权重
|
||||
total = sum(initial_weights)
|
||||
self.weights = [w / total for w in initial_weights]
|
||||
|
||||
# 创建学习器
|
||||
self.learner = WeightLearner(self.weights)
|
||||
|
||||
# 创建经验存储
|
||||
self.experience_store = ExperienceStore()
|
||||
|
||||
# 决策计数器
|
||||
self.decision_counter = 0
|
||||
|
||||
print(f"[自适应决策系统] 初始化完成")
|
||||
print(f" 准则: {self.criteria}")
|
||||
print(f" 初始权重: {[f'{w:.3f}' for w in self.weights]}")
|
||||
|
||||
def make_decision(self, alternatives: List[Dict[str, float]],
|
||||
context: Dict[str, Any] = None) -> Tuple[int, Dict[str, Any]]:
|
||||
"""
|
||||
做出决策
|
||||
|
||||
Args:
|
||||
alternatives: 备选方案列表,每个方案包含各准则的值
|
||||
context: 决策上下文
|
||||
|
||||
Returns:
|
||||
(选中方案索引, 决策信息)
|
||||
"""
|
||||
if not alternatives:
|
||||
raise ValueError("没有备选方案")
|
||||
|
||||
# 计算每个方案的综合得分
|
||||
scores = []
|
||||
for alt in alternatives:
|
||||
score = self._calculate_score(alt)
|
||||
scores.append(score)
|
||||
|
||||
# 选择得分最高的
|
||||
selected_idx = max(range(len(scores)), key=lambda i: scores[i])
|
||||
|
||||
# 创建决策记录
|
||||
decision_id = f"decision_{self.decision_counter}"
|
||||
self.decision_counter += 1
|
||||
|
||||
decision = Decision(
|
||||
decision_id=decision_id,
|
||||
context=context or {},
|
||||
alternatives=[
|
||||
{"criteria": alt, "score": score}
|
||||
for alt, score in zip(alternatives, scores)
|
||||
],
|
||||
selected_alternative=selected_idx,
|
||||
model_version="1.0",
|
||||
parameters=self.get_parameters()
|
||||
)
|
||||
|
||||
decision_info = {
|
||||
"decision_id": decision_id,
|
||||
"selected_index": selected_idx,
|
||||
"selected_alternative": alternatives[selected_idx],
|
||||
"score": scores[selected_idx],
|
||||
"all_scores": scores,
|
||||
"weights": self.weights.copy()
|
||||
}
|
||||
|
||||
# 存储决策
|
||||
self.experience_store.add_decision(decision)
|
||||
|
||||
return selected_idx, decision_info
|
||||
|
||||
def _calculate_score(self, alternative: Dict[str, float]) -> float:
|
||||
"""
|
||||
计算方案的综合得分
|
||||
|
||||
使用加权求和模型
|
||||
"""
|
||||
score = 0.0
|
||||
for i, criterion in enumerate(self.criteria):
|
||||
if criterion in alternative:
|
||||
score += self.weights[i] * alternative[criterion]
|
||||
return score
|
||||
|
||||
def provide_feedback(self, decision_id: str, feedback_value: float,
|
||||
feedback_type: FeedbackType = FeedbackType.EXPLICIT,
|
||||
source: FeedbackSource = FeedbackSource.HUMAN_EXPERT,
|
||||
content: str = None) -> None:
|
||||
"""
|
||||
为决策提供反馈
|
||||
|
||||
Args:
|
||||
decision_id: 决策ID
|
||||
feedback_value: 反馈值 (通常在0-1范围)
|
||||
feedback_type: 反馈类型
|
||||
source: 反馈来源
|
||||
content: 反馈内容
|
||||
"""
|
||||
decision = self.experience_store.get_decision(decision_id)
|
||||
if not decision:
|
||||
print(f"警告: 找不到决策 {decision_id}")
|
||||
return
|
||||
|
||||
# 创建反馈
|
||||
feedback = Feedback(
|
||||
feedback_id=f"fb_{decision_id}_{len(decision.feedback_list)}",
|
||||
feedback_type=feedback_type,
|
||||
source=source,
|
||||
target_decision_id=decision_id,
|
||||
value=feedback_value,
|
||||
content=content
|
||||
)
|
||||
|
||||
# 添加到决策记录
|
||||
decision.add_feedback(feedback)
|
||||
|
||||
# 从反馈中学习
|
||||
self.learner.learn_from_feedback(decision, feedback)
|
||||
|
||||
# 更新系统权重
|
||||
self.weights = self.learner.weights.copy()
|
||||
|
||||
print(f"[反馈] 收到反馈: {feedback_value:.2f}")
|
||||
print(f"[学习] 更新后权重: {[f'{w:.3f}' for w in self.weights]}")
|
||||
|
||||
def report_outcome(self, decision_id: str, actual_outcome: float) -> None:
|
||||
"""
|
||||
报告实际结果
|
||||
|
||||
Args:
|
||||
decision_id: 决策ID
|
||||
actual_outcome: 实际结果值
|
||||
"""
|
||||
decision = self.experience_store.get_decision(decision_id)
|
||||
if not decision:
|
||||
print(f"警告: 找不到决策 {decision_id}")
|
||||
return
|
||||
|
||||
decision.actual_outcome = actual_outcome
|
||||
|
||||
# 从结果中学习
|
||||
self.learner.learn_from_outcome(decision)
|
||||
|
||||
# 更新系统权重
|
||||
self.weights = self.learner.weights.copy()
|
||||
|
||||
print(f"[结果] 决策 {decision_id} 实际结果: {actual_outcome:.2f}")
|
||||
|
||||
def get_parameters(self) -> Dict[str, Any]:
|
||||
"""获取当前系统参数"""
|
||||
return {
|
||||
"weights": self.weights.copy(),
|
||||
"criteria": self.criteria.copy()
|
||||
}
|
||||
|
||||
def set_parameters(self, params: Dict[str, Any]) -> None:
|
||||
"""设置系统参数"""
|
||||
if "weights" in params:
|
||||
self.weights = params["weights"].copy()
|
||||
|
||||
def get_performance_summary(self) -> Dict[str, Any]:
|
||||
"""获取性能摘要"""
|
||||
stats = self.experience_store.get_statistics()
|
||||
|
||||
# 计算平均反馈分数
|
||||
decisions = list(self.experience_store.decisions.values())
|
||||
if decisions:
|
||||
avg_feedback = sum(d.get_average_feedback()
|
||||
for d in decisions if d.feedback_list)
|
||||
feedback_count = sum(1 for d in decisions if d.feedback_list)
|
||||
avg_feedback = avg_feedback / feedback_count if feedback_count > 0 else None
|
||||
else:
|
||||
avg_feedback = None
|
||||
|
||||
# 计算平均后悔值
|
||||
regrets = [d.calculate_regret() for d in decisions
|
||||
if d.actual_outcome is not None]
|
||||
avg_regret = sum(regrets) / len(regrets) if regrets else None
|
||||
|
||||
return {
|
||||
"total_decisions": stats["total_decisions"],
|
||||
"decisions_with_feedback": stats["decisions_with_feedback"],
|
||||
"average_feedback_score": avg_feedback,
|
||||
"average_regret": avg_regret,
|
||||
"current_weights": self.weights.copy()
|
||||
}
|
||||
|
||||
def print_summary(self) -> None:
|
||||
"""打印系统摘要"""
|
||||
print("\n" + "="*60)
|
||||
print("自适应决策系统摘要")
|
||||
print("="*60)
|
||||
|
||||
print("\n决策准则:")
|
||||
for i, criterion in enumerate(self.criteria):
|
||||
print(f" {i+1}. {criterion:15s} 权重: {self.weights[i]:.4f}")
|
||||
|
||||
perf = self.get_performance_summary()
|
||||
print(f"\n性能统计:")
|
||||
print(f" 总决策数: {perf['total_decisions']}")
|
||||
print(f" 有反馈的决策: {perf['decisions_with_feedback']}")
|
||||
|
||||
if perf['average_feedback_score'] is not None:
|
||||
print(f" 平均反馈分数: {perf['average_feedback_score']:.3f}")
|
||||
|
||||
if perf['average_regret'] is not None:
|
||||
print(f" 平均后悔值: {perf['average_regret']:.3f}")
|
||||
|
||||
print("="*60 + "\n")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示反馈与学习的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("反馈与学习示例演示")
|
||||
print("="*70)
|
||||
|
||||
# ========================================================================
|
||||
# 1. 创建自适应决策系统
|
||||
# ========================================================================
|
||||
print("\n[步骤 1] 创建自适应决策系统")
|
||||
print("-" * 50)
|
||||
|
||||
criteria = ["经济效益", "环境影响", "社会影响", "技术可行性"]
|
||||
initial_weights = [0.4, 0.3, 0.2, 0.1] # 偏重经济效益
|
||||
|
||||
system = AdaptiveDecisionSystem(criteria, initial_weights)
|
||||
system.print_summary()
|
||||
|
||||
# ========================================================================
|
||||
# 2. 第一次决策
|
||||
# ========================================================================
|
||||
print("\n[步骤 2] 第一次决策 - 工厂选址")
|
||||
print("-" * 50)
|
||||
|
||||
alternatives = [
|
||||
{"经济效益": 0.8, "环境影响": 0.3, "社会影响": 0.5, "技术可行性": 0.9}, # 位置A
|
||||
{"经济效益": 0.5, "环境影响": 0.7, "社会影响": 0.8, "技术可行性": 0.6}, # 位置B
|
||||
{"经济效益": 0.6, "环境影响": 0.9, "社会影响": 0.6, "技术可行性": 0.7}, # 位置C
|
||||
]
|
||||
|
||||
selected_idx, decision_info = system.make_decision(
|
||||
alternatives,
|
||||
context={"task": "工厂选址", "region": "华东地区"}
|
||||
)
|
||||
|
||||
print(f"\n决策结果:")
|
||||
print(f" 选中方案: 位置{chr(65 + selected_idx)}")
|
||||
print(f" 得分: {decision_info['score']:.3f}")
|
||||
print(f" 各方案得分: {[f'{s:.2f}' for s in decision_info['all_scores']]}")
|
||||
|
||||
decision_id_1 = decision_info['decision_id']
|
||||
|
||||
# ========================================================================
|
||||
# 3. 提供反馈
|
||||
# ========================================================================
|
||||
print("\n[步骤 3] 收集反馈")
|
||||
print("-" * 50)
|
||||
|
||||
# 专家反馈: 环境影响被低估了
|
||||
print("\n3.1 专家反馈: 环境影响应该更重视")
|
||||
system.provide_feedback(
|
||||
decision_id_1,
|
||||
feedback_value=0.6, # 中等偏下的评分
|
||||
feedback_type=FeedbackType.EXPLICIT,
|
||||
source=FeedbackSource.HUMAN_EXPERT,
|
||||
content="环境影响权重太低,应提高"
|
||||
)
|
||||
|
||||
# 更多反馈强化
|
||||
system.provide_feedback(
|
||||
decision_id_1,
|
||||
feedback_value=0.5,
|
||||
feedback_type=FeedbackType.CORRECTION,
|
||||
source=FeedbackSource.HUMAN_EXPERT
|
||||
)
|
||||
|
||||
# ========================================================================
|
||||
# 4. 第二次决策 (学习后的权重)
|
||||
# ========================================================================
|
||||
print("\n[步骤 4] 第二次决策 - 另一个选址")
|
||||
print("-" * 50)
|
||||
|
||||
alternatives_2 = [
|
||||
{"经济效益": 0.7, "环境影响": 0.4, "社会影响": 0.6, "技术可行性": 0.8}, # 位置D
|
||||
{"经济效益": 0.4, "环境影响": 0.9, "社会影响": 0.7, "技术可行性": 0.7}, # 位置E
|
||||
]
|
||||
|
||||
selected_idx_2, decision_info_2 = system.make_decision(
|
||||
alternatives_2,
|
||||
context={"task": "工厂选址", "region": "华南地区"}
|
||||
)
|
||||
|
||||
print(f"\n决策结果:")
|
||||
print(f" 选中方案: 位置{chr(68 + selected_idx_2)}")
|
||||
print(f" 得分: {decision_info_2['score']:.3f}")
|
||||
print(f" 当前权重: {[f'{w:.3f}' for w in system.weights]}")
|
||||
|
||||
decision_id_2 = decision_info_2['decision_id']
|
||||
|
||||
# ========================================================================
|
||||
# 5. 报告结果并学习
|
||||
# ========================================================================
|
||||
print("\n[步骤 5] 报告实际结果")
|
||||
print("-" * 50)
|
||||
|
||||
# 第一个决策的结果
|
||||
print(f"\n5.1 决策 {decision_id_1} 的实际结果")
|
||||
system.report_outcome(decision_id_1, actual_outcome=75) # 预测可能不同
|
||||
|
||||
# 第二个决策的结果
|
||||
print(f"\n5.2 决策 {decision_id_2} 的实际结果")
|
||||
system.report_outcome(decision_id_2, actual_outcome=85)
|
||||
|
||||
# ========================================================================
|
||||
# 6. 多轮学习
|
||||
# ========================================================================
|
||||
print("\n[步骤 6] 多轮学习")
|
||||
print("-" * 50)
|
||||
|
||||
# 模拟多次决策和反馈
|
||||
for i in range(10):
|
||||
alt1 = {
|
||||
"经济效益": random.uniform(0.5, 0.9),
|
||||
"环境影响": random.uniform(0.3, 0.7),
|
||||
"社会影响": random.uniform(0.4, 0.8),
|
||||
"技术可行性": random.uniform(0.5, 0.9)
|
||||
}
|
||||
alt2 = {
|
||||
"经济效益": random.uniform(0.3, 0.7),
|
||||
"环境影响": random.uniform(0.6, 0.95),
|
||||
"社会影响": random.uniform(0.5, 0.9),
|
||||
"技术可行性": random.uniform(0.4, 0.8)
|
||||
}
|
||||
|
||||
idx, info = system.make_decision([alt1, alt2])
|
||||
did = info['decision_id']
|
||||
|
||||
# 模拟反馈 (随着环境意识增强,对高环境影响的方案给低分)
|
||||
selected_env_impact = ([alt1, alt2][idx])["环境影响"]
|
||||
if selected_env_impact < 0.6:
|
||||
feedback_val = random.uniform(0.3, 0.5) # 低分
|
||||
else:
|
||||
feedback_val = random.uniform(0.7, 0.95) # 高分
|
||||
|
||||
system.provide_feedback(did, feedback_val)
|
||||
system.report_outcome(did, actual_outcome=random.uniform(60, 90))
|
||||
|
||||
print("\n多轮学习后:")
|
||||
system.print_summary()
|
||||
|
||||
# ========================================================================
|
||||
# 7. 权重变化分析
|
||||
# ========================================================================
|
||||
print("\n[步骤 7] 权重变化分析")
|
||||
print("-" * 50)
|
||||
|
||||
final_weights = system.weights
|
||||
print(f"\n初始权重: {[f'{w:.3f}' for w in initial_weights]}")
|
||||
print(f"最终权重: {[f'{w:.3f}' for w in final_weights]}")
|
||||
|
||||
print("\n权重变化:")
|
||||
for i, criterion in enumerate(criteria):
|
||||
change = final_weights[i] - initial_weights[i]
|
||||
arrow = "↑" if change > 0 else "↓" if change < 0 else "→"
|
||||
print(f" {criterion:15s}: {initial_weights[i]:.3f} → {final_weights[i]:.3f} "
|
||||
f"({arrow}{abs(change):.3f})")
|
||||
|
||||
# ========================================================================
|
||||
# 8. 经验检索
|
||||
# ========================================================================
|
||||
print("\n[步骤 8] 相似决策检索")
|
||||
print("-" * 50)
|
||||
|
||||
similar_decisions = system.experience_store.find_similar_decisions(
|
||||
{"task": "工厂选址", "region": "华东地区"},
|
||||
threshold=0.3
|
||||
)
|
||||
|
||||
print(f"\n找到 {len(similar_decisions)} 个相似决策:")
|
||||
for i, decision in enumerate(similar_decisions[:3], 1):
|
||||
print(f" {i}. {decision.decision_id} - "
|
||||
f"选中: {decision.selected_alternative}, "
|
||||
f"反馈: {decision.get_average_feedback():.2f}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,824 @@
|
||||
"""
|
||||
人机协同示例 (Human-in-the-Loop Example)
|
||||
========================================
|
||||
|
||||
本示例展示如何在空间智能系统中实现人机协同工作模式。
|
||||
人机协同 (HITL) 结合人类专家的领域知识和AI的计算能力,
|
||||
实现更可靠的决策。
|
||||
|
||||
核心概念:
|
||||
1. 主动学习 - AI主动请求人类帮助
|
||||
2. 交互式决策 - 人机共同完成决策
|
||||
3. 反馈收集 - 收集并整合人类反馈
|
||||
4. 置信度估计 - AI评估自身确定性
|
||||
5. 专业知识注入 - 将专家知识整合到系统中
|
||||
|
||||
应用场景:
|
||||
- 空间数据标注与验证
|
||||
- 复杂选址决策
|
||||
- 应急响应规划
|
||||
- 土地利用评估
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
import math
|
||||
import json
|
||||
from typing import List, Dict, Tuple, Optional, Any, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 协同模式枚举
|
||||
# ============================================================================
|
||||
|
||||
class HITLMode(Enum):
|
||||
"""人机协同模式"""
|
||||
AUTOMATIC = "automatic" # 全自动模式
|
||||
ADVISORY = "advisory" # 建议模式 (AI提供建议,人类决策)
|
||||
INTERACTIVE = "interactive" # 交互模式 (人机共同决策)
|
||||
SUPERVISED = "supervised" # 监督模式 (人类监督AI)
|
||||
MANUAL = "manual" # 手动模式 (人类完全控制)
|
||||
|
||||
|
||||
class ConfidenceLevel(Enum):
|
||||
"""置信度级别"""
|
||||
VERY_LOW = "very_low" # 0.0 - 0.3
|
||||
LOW = "low" # 0.3 - 0.5
|
||||
MEDIUM = "medium" # 0.5 - 0.7
|
||||
HIGH = "high" # 0.7 - 0.9
|
||||
VERY_HIGH = "very_high" # 0.9 - 1.0
|
||||
|
||||
|
||||
class InteractionType(Enum):
|
||||
"""交互类型"""
|
||||
CONFIRMATION = "confirmation" # 确认请求
|
||||
CLARIFICATION = "clarification" # 澄清请求
|
||||
VALIDATION = "validation" # 验证请求
|
||||
CORRECTION = "correction" # 纠正请求
|
||||
RANKING = "ranking" # 排序请求
|
||||
ANNOTATION = "annotation" # 标注请求
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 交互数据结构
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class AIConfidence:
|
||||
"""AI置信度"""
|
||||
value: float # 0-1之间的值
|
||||
reason: str = ""
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def level(self) -> ConfidenceLevel:
|
||||
"""获取置信度级别"""
|
||||
if self.value < 0.3:
|
||||
return ConfidenceLevel.VERY_LOW
|
||||
elif self.value < 0.5:
|
||||
return ConfidenceLevel.LOW
|
||||
elif self.value < 0.7:
|
||||
return ConfidenceLevel.MEDIUM
|
||||
elif self.value < 0.9:
|
||||
return ConfidenceLevel.HIGH
|
||||
else:
|
||||
return ConfidenceLevel.VERY_HIGH
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Confidence({self.value:.2f}, {self.level.value})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class HumanInput:
|
||||
"""人类输入"""
|
||||
interaction_type: InteractionType
|
||||
response: Any
|
||||
confidence: float = 1.0 # 人类对自己回答的置信度
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
expert_id: str = "default_expert"
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class InteractionRequest:
|
||||
"""交互请求"""
|
||||
request_id: str
|
||||
interaction_type: InteractionType
|
||||
question: str
|
||||
context: Dict[str, Any]
|
||||
options: Optional[List[Any]] = None
|
||||
ai_suggestion: Optional[Any] = None
|
||||
ai_confidence: Optional[AIConfidence] = None
|
||||
priority: int = 0 # 优先级 (0=普通, 1=重要, 2=紧急)
|
||||
deadline: Optional[datetime] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 决策建议
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class DecisionProposal:
|
||||
"""决策建议"""
|
||||
proposal_id: str
|
||||
decision: Any
|
||||
reasoning: str
|
||||
confidence: AIConfidence
|
||||
alternatives: List[Any] = field(default_factory=list)
|
||||
supporting_evidence: List[str] = field(default_factory=list)
|
||||
caveats: List[str] = field(default_factory=list) # 警告/注意事项
|
||||
requires_human_review: bool = False
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"proposal_id": self.proposal_id,
|
||||
"decision": self.decision,
|
||||
"reasoning": self.reasoning,
|
||||
"confidence": self.confidence.value,
|
||||
"alternatives": self.alternatives,
|
||||
"supporting_evidence": self.supporting_evidence,
|
||||
"caveats": self.caveats,
|
||||
"requires_human_review": self.requires_human_review
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 人类专家接口
|
||||
# ============================================================================
|
||||
|
||||
class HumanExpert(ABC):
|
||||
"""人类专家抽象接口"""
|
||||
|
||||
def __init__(self, expert_id: str, name: str = "", expertise: List[str] = None):
|
||||
self.expert_id = expert_id
|
||||
self.name = name or expert_id
|
||||
self.expertise = expertise or []
|
||||
|
||||
@abstractmethod
|
||||
def respond_to_request(self, request: InteractionRequest) -> HumanInput:
|
||||
"""响应交互请求"""
|
||||
pass
|
||||
|
||||
def can_handle(self, request: InteractionRequest) -> bool:
|
||||
"""检查是否能处理请求"""
|
||||
return True
|
||||
|
||||
def get_expertise_summary(self) -> str:
|
||||
"""获取专长摘要"""
|
||||
return f"{self.name}: {', '.join(self.expertise) if self.expertise else '通用'}"
|
||||
|
||||
|
||||
class MockHumanExpert(HumanExpert):
|
||||
"""
|
||||
模拟人类专家 (用于演示)
|
||||
|
||||
在实际应用中,这会连接到真实的用户界面。
|
||||
"""
|
||||
|
||||
def __init__(self, expert_id: str, name: str = "",
|
||||
expertise: List[str] = None,
|
||||
response_style: str = "balanced"):
|
||||
super().__init__(expert_id, name, expertise)
|
||||
self.response_style = response_style
|
||||
self.response_log: List[Dict[str, Any]] = []
|
||||
|
||||
def respond_to_request(self, request: InteractionRequest) -> HumanInput:
|
||||
"""模拟响应请求"""
|
||||
# 记录请求
|
||||
self.response_log.append({
|
||||
"request_id": request.request_id,
|
||||
"type": request.interaction_type.value,
|
||||
"question": request.question,
|
||||
"timestamp": datetime.now()
|
||||
})
|
||||
|
||||
# 根据不同类型生成响应
|
||||
if request.interaction_type == InteractionType.CONFIRMATION:
|
||||
# 确认请求 - 模拟基于置信度的决策
|
||||
if request.ai_confidence and request.ai_confidence.value > 0.7:
|
||||
# 高置信度时倾向于接受AI建议
|
||||
response = "accept" if random.random() > 0.2 else "reject"
|
||||
else:
|
||||
# 低置信度时更谨慎
|
||||
response = "accept" if random.random() > 0.5 else "reject"
|
||||
|
||||
return HumanInput(
|
||||
interaction_type=request.interaction_type,
|
||||
response=response,
|
||||
confidence=0.8
|
||||
)
|
||||
|
||||
elif request.interaction_type == InteractionType.VALIDATION:
|
||||
# 验证请求
|
||||
is_valid = random.random() > 0.3 # 70%概率验证通过
|
||||
return HumanInput(
|
||||
interaction_type=request.interaction_type,
|
||||
response=is_valid,
|
||||
confidence=0.9,
|
||||
metadata={"comment": "看起来正确" if is_valid else "需要修正"}
|
||||
)
|
||||
|
||||
elif request.interaction_type == InteractionType.RANKING:
|
||||
# 排序请求
|
||||
if request.options:
|
||||
# 随机打乱选项作为人类排序
|
||||
shuffled = request.options.copy()
|
||||
random.shuffle(shuffled)
|
||||
return HumanInput(
|
||||
interaction_type=request.interaction_type,
|
||||
response=shuffled,
|
||||
confidence=0.7
|
||||
)
|
||||
|
||||
elif request.interaction_type == InteractionType.ANNOTATION:
|
||||
# 标注请求
|
||||
return HumanInput(
|
||||
interaction_type=request.interaction_type,
|
||||
response={
|
||||
"label": random.choice(["高价值", "中价值", "低价值"]),
|
||||
"notes": "基于现场评估"
|
||||
},
|
||||
confidence=0.75
|
||||
)
|
||||
|
||||
# 默认响应
|
||||
return HumanInput(
|
||||
interaction_type=request.interaction_type,
|
||||
response="acknowledged",
|
||||
confidence=0.5
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 人机协同系统
|
||||
# ============================================================================
|
||||
|
||||
class HITLSystem:
|
||||
"""
|
||||
人机协同系统
|
||||
|
||||
管理AI与人类专家之间的交互。
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "HITL系统",
|
||||
default_mode: HITLMode = HITLMode.INTERACTIVE,
|
||||
confidence_threshold: float = 0.7):
|
||||
"""
|
||||
初始化HITL系统
|
||||
|
||||
Args:
|
||||
name: 系统名称
|
||||
default_mode: 默认协同模式
|
||||
confidence_threshold: 请求人类帮助的置信度阈值
|
||||
"""
|
||||
self.name = name
|
||||
self.current_mode = default_mode
|
||||
self.confidence_threshold = confidence_threshold
|
||||
|
||||
# 注册的专家
|
||||
self.experts: Dict[str, HumanExpert] = {}
|
||||
|
||||
# 待处理的请求队列
|
||||
self.pending_requests: List[InteractionRequest] = []
|
||||
|
||||
# 交互历史
|
||||
self.interaction_history: List[Dict[str, Any]] = []
|
||||
|
||||
# 统计信息
|
||||
self.stats = {
|
||||
"total_requests": 0,
|
||||
"auto_resolved": 0,
|
||||
"human_resolved": 0,
|
||||
"human_acceptance_rate": 0.0
|
||||
}
|
||||
|
||||
print(f"[{self.name}] 初始化完成")
|
||||
print(f" 模式: {default_mode.value}")
|
||||
print(f" 置信度阈值: {confidence_threshold}")
|
||||
|
||||
def register_expert(self, expert: HumanExpert) -> None:
|
||||
"""注册人类专家"""
|
||||
self.experts[expert.expert_id] = expert
|
||||
print(f"[专家注册] {expert.get_expertise_summary()}")
|
||||
|
||||
def set_mode(self, mode: HITLMode) -> None:
|
||||
"""设置协同模式"""
|
||||
self.current_mode = mode
|
||||
print(f"[模式切换] {mode.value}")
|
||||
|
||||
def make_decision(self, proposal: DecisionProposal,
|
||||
auto_threshold: float = None) -> Any:
|
||||
"""
|
||||
做出决策 (带人机协同)
|
||||
|
||||
Args:
|
||||
proposal: AI的决策建议
|
||||
auto_threshold: 自动决策的置信度阈值
|
||||
|
||||
Returns:
|
||||
最终决策
|
||||
"""
|
||||
threshold = auto_threshold or self.confidence_threshold
|
||||
|
||||
# 根据模式和置信度决定是否需要人类介入
|
||||
needs_human = self._needs_human_intervention(proposal, threshold)
|
||||
|
||||
if not needs_human:
|
||||
# 自动决策
|
||||
self.stats["auto_resolved"] += 1
|
||||
self._record_interaction(proposal, None, "automatic")
|
||||
return proposal.decision
|
||||
|
||||
# 请求人类帮助
|
||||
return self._request_human_input(proposal)
|
||||
|
||||
def _needs_human_intervention(self, proposal: DecisionProposal,
|
||||
threshold: float) -> bool:
|
||||
"""判断是否需要人类介入"""
|
||||
# 检查强制人工审查标记
|
||||
if proposal.requires_human_review:
|
||||
return True
|
||||
|
||||
# 检查置信度
|
||||
if proposal.confidence.value < threshold:
|
||||
return True
|
||||
|
||||
# 根据模式判断
|
||||
if self.current_mode == HITLMode.MANUAL:
|
||||
return True
|
||||
elif self.current_mode == HITLMode.SUPERVISED:
|
||||
return True
|
||||
elif self.current_mode == HITLMode.AUTOMATIC:
|
||||
return False
|
||||
elif self.current_mode == HITLMode.INTERACTIVE:
|
||||
# 交互模式下,低置信度需要人类
|
||||
return proposal.confidence.value < 0.8
|
||||
elif self.current_mode == HITLMode.ADVISORY:
|
||||
# 建议模式下,总是需要人类确认
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _request_human_input(self, proposal: DecisionProposal) -> Any:
|
||||
"""请求人类输入"""
|
||||
# 创建交互请求
|
||||
request = InteractionRequest(
|
||||
request_id=f"req_{len(self.interaction_history)}",
|
||||
interaction_type=InteractionType.CONFIRMATION,
|
||||
question=f"请确认AI建议: {proposal.reasoning}",
|
||||
context={"proposal_id": proposal.proposal_id},
|
||||
options=["accept", "reject", "modify"],
|
||||
ai_suggestion=proposal.decision,
|
||||
ai_confidence=proposal.confidence
|
||||
)
|
||||
|
||||
self.pending_requests.append(request)
|
||||
self.stats["total_requests"] += 1
|
||||
|
||||
# 选择专家
|
||||
expert = self._select_expert(request)
|
||||
if not expert:
|
||||
print("警告: 没有可用的专家,使用AI建议")
|
||||
return proposal.decision
|
||||
|
||||
# 获取专家响应
|
||||
print(f"\n[人类交互] 向专家 {expert.name} 请求确认...")
|
||||
print(f" AI建议: {proposal.decision}")
|
||||
print(f" 置信度: {proposal.confidence}")
|
||||
print(f" 理由: {proposal.reasoning}")
|
||||
|
||||
human_input = expert.respond_to_request(request)
|
||||
|
||||
print(f" 专家响应: {human_input.response}")
|
||||
|
||||
# 处理响应
|
||||
result = self._process_human_response(proposal, human_input)
|
||||
|
||||
# 记录交互
|
||||
self._record_interaction(proposal, human_input, "human_assisted")
|
||||
|
||||
# 清理请求
|
||||
if request in self.pending_requests:
|
||||
self.pending_requests.remove(request)
|
||||
|
||||
self.stats["human_resolved"] += 1
|
||||
|
||||
return result
|
||||
|
||||
def _select_expert(self, request: InteractionRequest) -> Optional[HumanExpert]:
|
||||
"""选择合适的专家"""
|
||||
if not self.experts:
|
||||
return None
|
||||
|
||||
# 简单实现: 返回第一个可用的专家
|
||||
for expert in self.experts.values():
|
||||
if expert.can_handle(request):
|
||||
return expert
|
||||
|
||||
return None
|
||||
|
||||
def _process_human_response(self, proposal: DecisionProposal,
|
||||
human_input: HumanInput) -> Any:
|
||||
"""处理人类响应"""
|
||||
if human_input.interaction_type == InteractionType.CONFIRMATION:
|
||||
if human_input.response == "accept":
|
||||
# 接受AI建议
|
||||
return proposal.decision
|
||||
elif human_input.response == "reject":
|
||||
# 拒绝AI建议,返回次优选项
|
||||
if proposal.alternatives:
|
||||
return proposal.alternatives[0]
|
||||
return None
|
||||
elif human_input.response == "modify":
|
||||
# 需要修改 (简化: 返回原建议)
|
||||
return proposal.decision
|
||||
|
||||
return human_input.response
|
||||
|
||||
def _record_interaction(self, proposal: DecisionProposal,
|
||||
human_input: Optional[HumanInput],
|
||||
resolution_type: str) -> None:
|
||||
"""记录交互"""
|
||||
record = {
|
||||
"proposal_id": proposal.proposal_id,
|
||||
"timestamp": datetime.now(),
|
||||
"ai_confidence": proposal.confidence.value,
|
||||
"human_input": human_input.response if human_input else None,
|
||||
"resolution_type": resolution_type
|
||||
}
|
||||
self.interaction_history.append(record)
|
||||
|
||||
def request_annotation(self, item: Any, context: Dict[str, Any] = None) -> Any:
|
||||
"""请求人类标注"""
|
||||
request = InteractionRequest(
|
||||
request_id=f"annotate_{len(self.interaction_history)}",
|
||||
interaction_type=InteractionType.ANNOTATION,
|
||||
question=f"请对以下项目进行标注: {item}",
|
||||
context=context or {},
|
||||
ai_suggestion=item
|
||||
)
|
||||
|
||||
expert = self._select_expert(request)
|
||||
if not expert:
|
||||
return None
|
||||
|
||||
return expert.respond_to_request(request)
|
||||
|
||||
def request_validation(self, item: Any, context: Dict[str, Any] = None) -> bool:
|
||||
"""请求人类验证"""
|
||||
request = InteractionRequest(
|
||||
request_id=f"validate_{len(self.interaction_history)}",
|
||||
interaction_type=InteractionType.VALIDATION,
|
||||
question=f"以下内容是否正确: {item}",
|
||||
context=context or {},
|
||||
ai_suggestion=item
|
||||
)
|
||||
|
||||
expert = self._select_expert(request)
|
||||
if not expert:
|
||||
return True # 默认有效
|
||||
|
||||
response = expert.respond_to_request(request)
|
||||
return response.response if isinstance(response.response, bool) else True
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
stats = self.stats.copy()
|
||||
stats["pending_requests"] = len(self.pending_requests)
|
||||
stats["total_interactions"] = len(self.interaction_history)
|
||||
|
||||
# 计算接受率
|
||||
human_interactions = [i for i in self.interaction_history
|
||||
if i["resolution_type"] == "human_assisted"]
|
||||
if human_interactions:
|
||||
accepted = sum(1 for i in human_interactions
|
||||
if i["human_input"] == "accept")
|
||||
stats["human_acceptance_rate"] = accepted / len(human_interactions)
|
||||
|
||||
return stats
|
||||
|
||||
def print_statistics(self) -> None:
|
||||
"""打印统计信息"""
|
||||
stats = self.get_statistics()
|
||||
|
||||
print(f"\n{self.name} 统计信息:")
|
||||
print("-" * 50)
|
||||
print(f"总请求数: {stats['total_requests']}")
|
||||
print(f"自动解决: {stats['auto_resolved']}")
|
||||
print(f"人类协助: {stats['human_resolved']}")
|
||||
print(f"待处理请求: {stats['pending_requests']}")
|
||||
print(f"人类接受率: {stats['human_acceptance_rate']:.2%}")
|
||||
print("-" * 50)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间决策HITL系统
|
||||
# ============================================================================
|
||||
|
||||
class SpatialDecisionHITL(HITLSystem):
|
||||
"""
|
||||
空间决策人机协同系统
|
||||
|
||||
专门用于空间决策场景的HITL实现。
|
||||
"""
|
||||
|
||||
def __init__(self, confidence_threshold: float = 0.7):
|
||||
super().__init__(
|
||||
name="空间决策HITL系统",
|
||||
default_mode=HITLMode.INTERACTIVE,
|
||||
confidence_threshold=confidence_threshold
|
||||
)
|
||||
|
||||
def analyze_site_suitability(self, site_data: Dict[str, Any]) -> DecisionProposal:
|
||||
"""
|
||||
分析场地适宜性
|
||||
|
||||
Args:
|
||||
site_data: 场地数据
|
||||
|
||||
Returns:
|
||||
决策建议
|
||||
"""
|
||||
# 简化的适宜性评分
|
||||
score = self._calculate_suitability_score(site_data)
|
||||
|
||||
# 确定置信度
|
||||
confidence = self._assess_confidence(site_data, score)
|
||||
|
||||
# 生成建议
|
||||
if score > 0.7:
|
||||
decision = "highly_suitable"
|
||||
reasoning = f"综合评分 {score:.2f} 较高,适宜开发"
|
||||
elif score > 0.5:
|
||||
decision = "moderately_suitable"
|
||||
reasoning = f"综合评分 {score:.2f} 中等,需谨慎评估"
|
||||
else:
|
||||
decision = "not_suitable"
|
||||
reasoning = f"综合评分 {score:.2f} 较低,不建议开发"
|
||||
|
||||
# 检查注意事项
|
||||
caveats = []
|
||||
if site_data.get("environmental_risk", 0) > 0.6:
|
||||
caveats.append("存在环境风险")
|
||||
if site_data.get("infrastructure_score", 1) < 0.4:
|
||||
caveats.append("基础设施不足")
|
||||
|
||||
# 低置信度时标记需要人工审查
|
||||
requires_review = confidence.value < 0.6 or len(caveats) > 0
|
||||
|
||||
return DecisionProposal(
|
||||
proposal_id=f"suitability_{random.randint(1000, 9999)}",
|
||||
decision=decision,
|
||||
reasoning=reasoning,
|
||||
confidence=confidence,
|
||||
alternatives=["moderately_suitable", "not_suitable"]
|
||||
if decision != "not_suitable" else ["moderately_suitable", "highly_suitable"],
|
||||
supporting_evidence=[
|
||||
f"评分: {score:.2f}",
|
||||
f"环境因子: {site_data.get('environmental_score', 0):.2f}",
|
||||
f"经济因子: {site_data.get('economic_score', 0):.2f}"
|
||||
],
|
||||
caveats=caveats,
|
||||
requires_human_review=requires_review
|
||||
)
|
||||
|
||||
def _calculate_suitability_score(self, site_data: Dict[str, Any]) -> float:
|
||||
"""计算适宜性评分"""
|
||||
env = site_data.get("environmental_score", 0.5)
|
||||
econ = site_data.get("economic_score", 0.5)
|
||||
social = site_data.get("social_score", 0.5)
|
||||
infra = site_data.get("infrastructure_score", 0.5)
|
||||
|
||||
# 加权平均
|
||||
return 0.3 * env + 0.3 * econ + 0.2 * social + 0.2 * infra
|
||||
|
||||
def _assess_confidence(self, site_data: Dict[str, Any],
|
||||
score: float) -> AIConfidence:
|
||||
"""评估置信度"""
|
||||
# 检查数据完整性
|
||||
has_all_data = all(k in site_data for k in [
|
||||
"environmental_score", "economic_score",
|
||||
"social_score", "infrastructure_score"
|
||||
])
|
||||
|
||||
if not has_all_data:
|
||||
return AIConfidence(
|
||||
value=0.4,
|
||||
reason="数据不完整"
|
||||
)
|
||||
|
||||
# 检查数据质量
|
||||
data_quality = site_data.get("data_quality", 0.8)
|
||||
confidence = data_quality * 0.9
|
||||
|
||||
# 检查是否有冲突因素
|
||||
if site_data.get("environmental_risk", 0) > 0.7:
|
||||
confidence *= 0.7 # 降低置信度
|
||||
|
||||
return AIConfidence(
|
||||
value=min(confidence, 0.95),
|
||||
reason="基于数据质量和完整性评估"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示人机协同的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("人机协同示例演示")
|
||||
print("="*70)
|
||||
|
||||
random.seed(42)
|
||||
|
||||
# ========================================================================
|
||||
# 1. 创建HITL系统
|
||||
# ========================================================================
|
||||
print("\n[步骤 1] 创建人机协同系统")
|
||||
print("-" * 50)
|
||||
|
||||
hitl_system = SpatialDecisionHITL(confidence_threshold=0.7)
|
||||
|
||||
# 注册专家
|
||||
expert1 = MockHumanExpert(
|
||||
expert_id="expert_001",
|
||||
name="张工程师",
|
||||
expertise=["环境影响评估", "基础设施规划"],
|
||||
response_style="conservative"
|
||||
)
|
||||
expert2 = MockHumanExpert(
|
||||
expert_id="expert_002",
|
||||
name="李规划师",
|
||||
expertise=["经济效益分析", "社会影响评估"],
|
||||
response_style="balanced"
|
||||
)
|
||||
|
||||
hitl_system.register_expert(expert1)
|
||||
hitl_system.register_expert(expert2)
|
||||
|
||||
# ========================================================================
|
||||
# 2. 场景1: 高置信度自动决策
|
||||
# ========================================================================
|
||||
print("\n[场景 1] 高置信度 - 自动决策")
|
||||
print("-" * 50)
|
||||
|
||||
site1 = {
|
||||
"environmental_score": 0.85,
|
||||
"economic_score": 0.90,
|
||||
"social_score": 0.88,
|
||||
"infrastructure_score": 0.92,
|
||||
"data_quality": 0.95,
|
||||
"environmental_risk": 0.1
|
||||
}
|
||||
|
||||
proposal1 = hitl_system.analyze_site_suitability(site1)
|
||||
print(f"\nAI分析结果:")
|
||||
print(f" 建议: {proposal1.decision}")
|
||||
print(f" 理由: {proposal1.reasoning}")
|
||||
print(f" 置信度: {proposal1.confidence}")
|
||||
|
||||
decision1 = hitl_system.make_decision(proposal1)
|
||||
print(f"\n最终决策: {decision1} (自动)")
|
||||
print(f" → 置信度高,无需人工介入")
|
||||
|
||||
# ========================================================================
|
||||
# 3. 场景2: 低置信度请求人类帮助
|
||||
# ========================================================================
|
||||
print("\n\n[场景 2] 低置信度 - 请求人类确认")
|
||||
print("-" * 50)
|
||||
|
||||
site2 = {
|
||||
"environmental_score": 0.45, # 环境评分低
|
||||
"economic_score": 0.85, # 但经济评分高
|
||||
"social_score": 0.60,
|
||||
"infrastructure_score": 0.50,
|
||||
"data_quality": 0.70,
|
||||
"environmental_risk": 0.65 # 存在环境风险
|
||||
}
|
||||
|
||||
proposal2 = hitl_system.analyze_site_suitability(site2)
|
||||
print(f"\nAI分析结果:")
|
||||
print(f" 建议: {proposal2.decision}")
|
||||
print(f" 理由: {proposal2.reasoning}")
|
||||
print(f" 置信度: {proposal2.confidence}")
|
||||
print(f" 注意事项: {', '.join(proposal2.caveats)}")
|
||||
|
||||
decision2 = hitl_system.make_decision(proposal2)
|
||||
print(f"\n最终决策: {decision2} (人工协助)")
|
||||
print(f" → 置信度低且存在注意事项,请求专家确认")
|
||||
|
||||
# ========================================================================
|
||||
# 4. 场景3: 批量决策
|
||||
# ========================================================================
|
||||
print("\n\n[场景 3] 批量场地评估")
|
||||
print("-" * 50)
|
||||
|
||||
sites = []
|
||||
for i in range(5):
|
||||
site = {
|
||||
"environmental_score": random.uniform(0.3, 0.95),
|
||||
"economic_score": random.uniform(0.3, 0.95),
|
||||
"social_score": random.uniform(0.3, 0.95),
|
||||
"infrastructure_score": random.uniform(0.3, 0.95),
|
||||
"data_quality": random.uniform(0.5, 0.95),
|
||||
"environmental_risk": random.uniform(0.0, 0.8)
|
||||
}
|
||||
sites.append(site)
|
||||
|
||||
results = []
|
||||
for i, site in enumerate(sites, 1):
|
||||
proposal = hitl_system.analyze_site_suitability(site)
|
||||
decision = hitl_system.make_decision(proposal)
|
||||
|
||||
results.append({
|
||||
"site": i,
|
||||
"decision": decision,
|
||||
"confidence": proposal.confidence.value,
|
||||
"auto": proposal.confidence.value >= hitl_system.confidence_threshold
|
||||
})
|
||||
|
||||
print("\n批量评估结果:")
|
||||
print(f"{'场地':<6} {'决策':<20} {'置信度':<10} {'模式':<10}")
|
||||
print("-" * 50)
|
||||
for r in results:
|
||||
mode = "自动" if r["auto"] else "人工"
|
||||
print(f"{r['site']:<6} {r['decision']:<20} {r['confidence']:<10.2f} {mode:<10}")
|
||||
|
||||
# ========================================================================
|
||||
# 5. 场景4: 数据标注
|
||||
# ========================================================================
|
||||
print("\n\n[场景 4] 数据标注")
|
||||
print("-" * 50)
|
||||
|
||||
unlabeled_items = [
|
||||
{"coordinates": (120.5, 30.2), "features": "residential"},
|
||||
{"coordinates": (121.0, 30.5), "features": "commercial"},
|
||||
{"coordinates": (120.8, 30.0), "features": "industrial"}
|
||||
]
|
||||
|
||||
for item in unlabeled_items:
|
||||
annotation = hitl_system.request_annotation(
|
||||
item,
|
||||
context={"task": "land_use_classification"}
|
||||
)
|
||||
if annotation:
|
||||
print(f"\n标注 {item['features']}:")
|
||||
print(f" 标签: {annotation.response.get('label')}")
|
||||
print(f" 备注: {annotation.response.get('notes')}")
|
||||
|
||||
# ========================================================================
|
||||
# 6. 场景5: 模式切换
|
||||
# ========================================================================
|
||||
print("\n\n[场景 5] 模式切换对比")
|
||||
print("-" * 50)
|
||||
|
||||
test_site = {
|
||||
"environmental_score": 0.70,
|
||||
"economic_score": 0.75,
|
||||
"social_score": 0.68,
|
||||
"infrastructure_score": 0.72,
|
||||
"data_quality": 0.85,
|
||||
"environmental_risk": 0.3
|
||||
}
|
||||
|
||||
proposal = hitl_system.analyze_site_suitability(test_site)
|
||||
print(f"\nAI分析: 置信度 = {proposal.confidence.value:.2f}")
|
||||
|
||||
# 尝试不同模式
|
||||
for mode in [HITLMode.AUTOMATIC, HITLMode.INTERACTIVE, HITLMode.MANUAL]:
|
||||
hitl_system.set_mode(mode)
|
||||
decision = hitl_system.make_decision(proposal)
|
||||
mode_name = {
|
||||
HITLMode.AUTOMATIC: "全自动",
|
||||
HITLMode.INTERACTIVE: "交互式",
|
||||
HITLMode.MANUAL: "手动"
|
||||
}[mode]
|
||||
print(f" {mode_name}: {decision}")
|
||||
|
||||
# 恢复默认模式
|
||||
hitl_system.set_mode(HITLMode.INTERACTIVE)
|
||||
|
||||
# ========================================================================
|
||||
# 7. 统计信息
|
||||
# ========================================================================
|
||||
print("\n\n[步骤 7] 系统统计")
|
||||
print("-" * 50)
|
||||
hitl_system.print_statistics()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,684 @@
|
||||
"""
|
||||
模块化系统示例 (Modular System Example)
|
||||
========================================
|
||||
|
||||
本示例展示了空间智能系统的模块化设计原则。
|
||||
模块化是构建可维护、可扩展系统的基础。
|
||||
|
||||
核心概念:
|
||||
1. 关注点分离 - 每个模块负责特定功能
|
||||
2. 接口设计 - 定义清晰的模块间通信协议
|
||||
3. 依赖注入 - 降低模块间耦合
|
||||
4. 插件架构 - 支持动态扩展功能
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Any, Optional, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 模块接口定义 (抽象基类)
|
||||
# ============================================================================
|
||||
|
||||
class ModuleType(Enum):
|
||||
"""模块类型枚举"""
|
||||
DATA_LOADER = "data_loader"
|
||||
DATA_PROCESSOR = "data_processor"
|
||||
ANALYZER = "analyzer"
|
||||
VISUALIZER = "visualizer"
|
||||
EXPORTER = "exporter"
|
||||
|
||||
|
||||
class ModuleStatus(Enum):
|
||||
"""模块状态枚举"""
|
||||
IDLE = "idle"
|
||||
INITIALIZING = "initializing"
|
||||
READY = "ready"
|
||||
RUNNING = "running"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleMetadata:
|
||||
"""模块元数据"""
|
||||
name: str
|
||||
version: str
|
||||
module_type: ModuleType
|
||||
description: str = ""
|
||||
dependencies: List[str] = field(default_factory=list)
|
||||
author: str = ""
|
||||
config_schema: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class IModule(ABC):
|
||||
"""
|
||||
模块接口 - 所有模块必须实现此接口
|
||||
|
||||
这是一个抽象基类,定义了所有模块必须遵循的契约。
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any] = None):
|
||||
"""
|
||||
初始化模块
|
||||
|
||||
Args:
|
||||
config: 模块配置字典
|
||||
"""
|
||||
self.config = config or {}
|
||||
self.status = ModuleStatus.IDLE
|
||||
self._context: Optional['ModuleContext'] = None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def metadata(self) -> ModuleMetadata:
|
||||
"""返回模块元数据"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self, context: 'ModuleContext') -> bool:
|
||||
"""
|
||||
初始化模块
|
||||
|
||||
Args:
|
||||
context: 模块上下文,提供对系统资源的访问
|
||||
|
||||
Returns:
|
||||
初始化是否成功
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, input_data: Any) -> Any:
|
||||
"""
|
||||
执行模块功能
|
||||
|
||||
Args:
|
||||
input_data: 输入数据
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def shutdown(self) -> None:
|
||||
"""关闭模块,释放资源"""
|
||||
pass
|
||||
|
||||
def get_config(self, key: str, default: Any = None) -> Any:
|
||||
"""获取配置值"""
|
||||
return self.config.get(key, default)
|
||||
|
||||
def set_config(self, key: str, value: Any) -> None:
|
||||
"""设置配置值"""
|
||||
self.config[key] = value
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 模块上下文 - 提供模块间通信
|
||||
# ============================================================================
|
||||
|
||||
class ModuleContext:
|
||||
"""
|
||||
模块上下文
|
||||
|
||||
提供模块间通信和资源共享机制,实现松耦合设计。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._modules: Dict[str, IModule] = {}
|
||||
self._shared_data: Dict[str, Any] = {}
|
||||
self._event_handlers: Dict[str, List[Callable]] = {}
|
||||
|
||||
def register_module(self, name: str, module: IModule) -> bool:
|
||||
"""注册模块"""
|
||||
if name in self._modules:
|
||||
print(f"警告: 模块 '{name}' 已存在,将被覆盖")
|
||||
self._modules[name] = module
|
||||
print(f"模块 '{name}' 已注册 (类型: {module.metadata.module_type.value})")
|
||||
return True
|
||||
|
||||
def get_module(self, name: str) -> Optional[IModule]:
|
||||
"""获取模块实例"""
|
||||
return self._modules.get(name)
|
||||
|
||||
def has_module(self, name: str) -> bool:
|
||||
"""检查模块是否存在"""
|
||||
return name in self._modules
|
||||
|
||||
def set_shared_data(self, key: str, value: Any) -> None:
|
||||
"""设置共享数据"""
|
||||
self._shared_data[key] = value
|
||||
|
||||
def get_shared_data(self, key: str, default: Any = None) -> Any:
|
||||
"""获取共享数据"""
|
||||
return self._shared_data.get(key, default)
|
||||
|
||||
def subscribe_event(self, event_name: str, handler: Callable) -> None:
|
||||
"""订阅事件"""
|
||||
if event_name not in self._event_handlers:
|
||||
self._event_handlers[event_name] = []
|
||||
self._event_handlers[event_name].append(handler)
|
||||
|
||||
def publish_event(self, event_name: str, *args, **kwargs) -> None:
|
||||
"""发布事件"""
|
||||
if event_name in self._event_handlers:
|
||||
for handler in self._event_handlers[event_name]:
|
||||
handler(*args, **kwargs)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 模块基类 - 提供通用功能实现
|
||||
# ============================================================================
|
||||
|
||||
class BaseModule(IModule):
|
||||
"""
|
||||
模块基类
|
||||
|
||||
提供IModule接口的默认实现,子类只需实现特定功能。
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any] = None):
|
||||
super().__init__(config)
|
||||
self._metadata: Optional[ModuleMetadata] = None
|
||||
|
||||
@property
|
||||
def metadata(self) -> ModuleMetadata:
|
||||
if self._metadata is None:
|
||||
raise NotImplementedError("子类必须设置 _metadata")
|
||||
return self._metadata
|
||||
|
||||
def initialize(self, context: ModuleContext) -> bool:
|
||||
"""默认初始化实现"""
|
||||
self._context = context
|
||||
self.status = ModuleStatus.INITIALIZING
|
||||
|
||||
# 检查依赖
|
||||
for dep in self.metadata.dependencies:
|
||||
if not context.has_module(dep):
|
||||
print(f"错误: 依赖模块 '{dep}' 不存在")
|
||||
self.status = ModuleStatus.ERROR
|
||||
return False
|
||||
|
||||
self.status = ModuleStatus.READY
|
||||
print(f"模块 '{self.metadata.name}' 初始化完成")
|
||||
return True
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""默认关闭实现"""
|
||||
self.status = ModuleStatus.IDLE
|
||||
print(f"模块 '{self.metadata.name}' 已关闭")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 具体模块实现
|
||||
# ============================================================================
|
||||
|
||||
class CSVDataLoaderModule(BaseModule):
|
||||
"""
|
||||
CSV 数据加载模块
|
||||
|
||||
负责从CSV文件加载空间数据。
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any] = None):
|
||||
super().__init__(config)
|
||||
self._metadata = ModuleMetadata(
|
||||
name="csv_data_loader",
|
||||
version="1.0.0",
|
||||
module_type=ModuleType.DATA_LOADER,
|
||||
description="从CSV文件加载空间数据",
|
||||
author="CC4SI"
|
||||
)
|
||||
self._data: List[Dict[str, Any]] = []
|
||||
|
||||
def execute(self, input_data: Any) -> Any:
|
||||
"""
|
||||
执行数据加载
|
||||
|
||||
Args:
|
||||
input_data: 文件路径或模拟数据
|
||||
|
||||
Returns:
|
||||
加载的数据列表
|
||||
"""
|
||||
self.status = ModuleStatus.RUNNING
|
||||
|
||||
if isinstance(input_data, str):
|
||||
# 实际场景中应从文件读取
|
||||
print(f"从文件 '{input_data}' 加载数据...")
|
||||
# 模拟加载
|
||||
self._data = self._load_sample_data()
|
||||
elif isinstance(input_data, list):
|
||||
self._data = input_data
|
||||
else:
|
||||
self._data = self._load_sample_data()
|
||||
|
||||
# 将数据存入共享上下文
|
||||
if self._context:
|
||||
self._context.set_shared_data("raw_data", self._data)
|
||||
self._context.publish_event("data_loaded", len(self._data))
|
||||
|
||||
self.status = ModuleStatus.READY
|
||||
return self._data
|
||||
|
||||
def _load_sample_data(self) -> List[Dict[str, Any]]:
|
||||
"""加载示例数据"""
|
||||
return [
|
||||
{"id": 1, "x": 10, "y": 20, "value": 100, "type": "A"},
|
||||
{"id": 2, "x": 30, "y": 40, "value": 200, "type": "B"},
|
||||
{"id": 3, "x": 50, "y": 60, "value": 150, "type": "A"},
|
||||
{"id": 4, "x": 70, "y": 80, "value": 300, "type": "C"},
|
||||
{"id": 5, "x": 90, "y": 100, "value": 250, "type": "B"},
|
||||
]
|
||||
|
||||
|
||||
class DataValidationModule(BaseModule):
|
||||
"""
|
||||
数据验证模块
|
||||
|
||||
负责验证数据质量和完整性。
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any] = None):
|
||||
super().__init__(config)
|
||||
self._metadata = ModuleMetadata(
|
||||
name="data_validator",
|
||||
version="1.0.0",
|
||||
module_type=ModuleType.DATA_PROCESSOR,
|
||||
description="验证数据质量和完整性",
|
||||
dependencies=["csv_data_loader"],
|
||||
author="CC4SI"
|
||||
)
|
||||
self.validation_rules: List[Callable] = []
|
||||
|
||||
def add_validation_rule(self, rule: Callable[[Dict], bool], name: str = ""):
|
||||
"""添加验证规则"""
|
||||
self.validation_rules.append(rule)
|
||||
if name:
|
||||
print(f"添加验证规则: {name}")
|
||||
|
||||
def execute(self, input_data: Any) -> Any:
|
||||
"""
|
||||
执行数据验证
|
||||
|
||||
Args:
|
||||
input_data: 待验证的数据
|
||||
|
||||
Returns:
|
||||
验证结果
|
||||
"""
|
||||
self.status = ModuleStatus.RUNNING
|
||||
|
||||
if not isinstance(input_data, list):
|
||||
return {"valid": False, "errors": ["输入数据格式错误"]}
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
for i, item in enumerate(input_data):
|
||||
# 检查必需字段
|
||||
if "id" not in item:
|
||||
errors.append(f"第 {i} 项缺少 'id' 字段")
|
||||
if "x" not in item or "y" not in item:
|
||||
errors.append(f"第 {i} 项缺少坐标字段")
|
||||
|
||||
# 应用自定义验证规则
|
||||
for rule in self.validation_rules:
|
||||
try:
|
||||
if not rule(item):
|
||||
warnings.append(f"第 {i} 项未通过自定义规则验证")
|
||||
except Exception as e:
|
||||
errors.append(f"第 {i} 项验证时出错: {e}")
|
||||
|
||||
result = {
|
||||
"valid": len(errors) == 0,
|
||||
"total": len(input_data),
|
||||
"errors": errors,
|
||||
"warnings": warnings
|
||||
}
|
||||
|
||||
if self._context:
|
||||
self._context.set_shared_data("validation_result", result)
|
||||
self._context.publish_event("data_validated", result)
|
||||
|
||||
self.status = ModuleStatus.READY
|
||||
return result
|
||||
|
||||
|
||||
class StatisticsAnalyzerModule(BaseModule):
|
||||
"""
|
||||
统计分析模块
|
||||
|
||||
负责计算数据的统计指标。
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any] = None):
|
||||
super().__init__(config)
|
||||
self._metadata = ModuleMetadata(
|
||||
name="statistics_analyzer",
|
||||
version="1.0.0",
|
||||
module_type=ModuleType.ANALYZER,
|
||||
description="计算数据统计指标",
|
||||
dependencies=["csv_data_loader"],
|
||||
author="CC4SI"
|
||||
)
|
||||
|
||||
def execute(self, input_data: Any) -> Any:
|
||||
"""
|
||||
执行统计分析
|
||||
|
||||
Args:
|
||||
input_data: 待分析的数据
|
||||
|
||||
Returns:
|
||||
统计结果
|
||||
"""
|
||||
self.status = ModuleStatus.RUNNING
|
||||
|
||||
if not isinstance(input_data, list) or len(input_data) == 0:
|
||||
return {"error": "没有可分析的数据"}
|
||||
|
||||
# 提取数值字段
|
||||
values = [item.get("value", 0) for item in input_data if "value" in item]
|
||||
|
||||
if not values:
|
||||
return {"error": "没有找到可分析的数值"}
|
||||
|
||||
import statistics
|
||||
|
||||
result = {
|
||||
"count": len(values),
|
||||
"mean": statistics.mean(values),
|
||||
"median": statistics.median(values),
|
||||
"stdev": statistics.stdev(values) if len(values) > 1 else 0,
|
||||
"min": min(values),
|
||||
"max": max(values),
|
||||
"sum": sum(values)
|
||||
}
|
||||
|
||||
if self._context:
|
||||
self._context.set_shared_data("statistics", result)
|
||||
self._context.publish_event("analysis_complete", result)
|
||||
|
||||
self.status = ModuleStatus.READY
|
||||
return result
|
||||
|
||||
|
||||
class ReportExporterModule(BaseModule):
|
||||
"""
|
||||
报告导出模块
|
||||
|
||||
负责生成分析报告。
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any] = None):
|
||||
super().__init__(config)
|
||||
self._metadata = ModuleMetadata(
|
||||
name="report_exporter",
|
||||
version="1.0.0",
|
||||
module_type=ModuleType.EXPORTER,
|
||||
description="生成分析报告",
|
||||
dependencies=["statistics_analyzer"],
|
||||
author="CC4SI"
|
||||
)
|
||||
|
||||
def execute(self, input_data: Any) -> Any:
|
||||
"""
|
||||
生成报告
|
||||
|
||||
Args:
|
||||
input_data: 统计结果或其他数据
|
||||
|
||||
Returns:
|
||||
报告字符串
|
||||
"""
|
||||
self.status = ModuleStatus.RUNNING
|
||||
|
||||
report_lines = [
|
||||
"=" * 60,
|
||||
"空间数据分析报告",
|
||||
"=" * 60,
|
||||
""
|
||||
]
|
||||
|
||||
# 从上下文获取数据
|
||||
if self._context:
|
||||
validation = self._context.get_shared_data("validation_result")
|
||||
statistics = self._context.get_shared_data("statistics")
|
||||
|
||||
if validation:
|
||||
report_lines.extend([
|
||||
"数据验证结果:",
|
||||
f" 总数: {validation.get('total', 0)}",
|
||||
f" 有效: {validation.get('valid', False)}",
|
||||
f" 错误数: {len(validation.get('errors', []))}",
|
||||
""
|
||||
])
|
||||
|
||||
if statistics:
|
||||
report_lines.extend([
|
||||
"统计分析结果:",
|
||||
f" 样本数: {statistics.get('count', 0)}",
|
||||
f" 均值: {statistics.get('mean', 0):.2f}",
|
||||
f" 中位数: {statistics.get('median', 0):.2f}",
|
||||
f" 标准差: {statistics.get('stdev', 0):.2f}",
|
||||
f" 最小值: {statistics.get('min', 0)}",
|
||||
f" 最大值: {statistics.get('max', 0)}",
|
||||
""
|
||||
])
|
||||
|
||||
report_lines.append("=" * 60)
|
||||
|
||||
report = "\n".join(report_lines)
|
||||
|
||||
if self._context:
|
||||
self._context.publish_event("report_generated", report)
|
||||
|
||||
self.status = ModuleStatus.READY
|
||||
return report
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 模块系统管理器
|
||||
# ============================================================================
|
||||
|
||||
class ModularSystem:
|
||||
"""
|
||||
模块化系统管理器
|
||||
|
||||
负责管理模块的生命周期和模块间通信。
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "模块化空间智能系统"):
|
||||
self.name = name
|
||||
self.context = ModuleContext()
|
||||
self._pipeline: List[str] = [] # 处理流程
|
||||
|
||||
def register_module(self, module: IModule, alias: str = None) -> bool:
|
||||
"""
|
||||
注册模块到系统
|
||||
|
||||
Args:
|
||||
module: 模块实例
|
||||
alias: 模块别名 (可选)
|
||||
|
||||
Returns:
|
||||
是否注册成功
|
||||
"""
|
||||
name = alias or module.metadata.name
|
||||
return self.context.register_module(name, module)
|
||||
|
||||
def initialize_all(self) -> bool:
|
||||
"""初始化所有模块"""
|
||||
print(f"\n初始化 {self.name}...")
|
||||
|
||||
success = True
|
||||
for name, module in self.context._modules.items():
|
||||
if not module.initialize(self.context):
|
||||
print(f"模块 '{name}' 初始化失败")
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
||||
def define_pipeline(self, module_names: List[str]) -> None:
|
||||
"""
|
||||
定义处理流程
|
||||
|
||||
Args:
|
||||
module_names: 按顺序执行的模块名称列表
|
||||
"""
|
||||
self._pipeline = module_names
|
||||
print(f"定义处理流程: {' -> '.join(module_names)}")
|
||||
|
||||
def execute(self, input_data: Any = None) -> Any:
|
||||
"""
|
||||
执行处理流程
|
||||
|
||||
Args:
|
||||
input_data: 输入数据
|
||||
|
||||
Returns:
|
||||
最终输出结果
|
||||
"""
|
||||
if not self._pipeline:
|
||||
print("错误: 没有定义处理流程")
|
||||
return None
|
||||
|
||||
print(f"\n执行处理流程...")
|
||||
current_data = input_data
|
||||
|
||||
for module_name in self._pipeline:
|
||||
module = self.context.get_module(module_name)
|
||||
if not module:
|
||||
print(f"错误: 找不到模块 '{module_name}'")
|
||||
return None
|
||||
|
||||
print(f" -> 执行模块: {module.metadata.name}")
|
||||
current_data = module.execute(current_data)
|
||||
|
||||
# 如果模块返回错误,终止流程
|
||||
if isinstance(current_data, dict) and current_data.get("error"):
|
||||
print(f" 模块 '{module_name}' 返回错误: {current_data['error']}")
|
||||
return current_data
|
||||
|
||||
return current_data
|
||||
|
||||
def shutdown_all(self) -> None:
|
||||
"""关闭所有模块"""
|
||||
print(f"\n关闭 {self.name}...")
|
||||
for module in self.context._modules.values():
|
||||
module.shutdown()
|
||||
|
||||
def print_system_info(self) -> None:
|
||||
"""打印系统信息"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"系统: {self.name}")
|
||||
print(f"{'='*60}")
|
||||
print(f"已注册模块数: {len(self.context._modules)}")
|
||||
|
||||
for name, module in self.context._modules.items():
|
||||
print(f" - {name:20s} [{module.metadata.module_type.value:15s}] {module.metadata.name}")
|
||||
if module.metadata.dependencies:
|
||||
print(f" 依赖: {', '.join(module.metadata.dependencies)}")
|
||||
|
||||
if self._pipeline:
|
||||
print(f"\n处理流程: {' -> '.join(self._pipeline)}")
|
||||
else:
|
||||
print(f"\n处理流程: 未定义")
|
||||
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 事件处理示例
|
||||
# ============================================================================
|
||||
|
||||
def setup_event_handlers(system: ModularSystem):
|
||||
"""设置事件处理器"""
|
||||
|
||||
def on_data_loaded(count):
|
||||
print(f" [事件] 数据加载完成,共 {count} 条记录")
|
||||
|
||||
def on_data_validated(result):
|
||||
status = "通过" if result.get("valid") else "失败"
|
||||
print(f" [事件] 数据验证{status},错误: {len(result.get('errors', []))}")
|
||||
|
||||
def on_analysis_complete(result):
|
||||
print(f" [事件] 分析完成,均值: {result.get('mean', 0):.2f}")
|
||||
|
||||
system.context.subscribe_event("data_loaded", on_data_loaded)
|
||||
system.context.subscribe_event("data_validated", on_data_validated)
|
||||
system.context.subscribe_event("analysis_complete", on_analysis_complete)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示模块化系统的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("模块化系统示例演示")
|
||||
print("="*70)
|
||||
|
||||
# 1. 创建系统
|
||||
print("\n[步骤 1] 创建模块化系统")
|
||||
system = ModularSystem("空间数据分析系统")
|
||||
|
||||
# 2. 注册模块
|
||||
print("\n[步骤 2] 注册模块")
|
||||
system.register_module(CSVDataLoaderModule())
|
||||
system.register_module(DataValidationModule())
|
||||
system.register_module(StatisticsAnalyzerModule())
|
||||
system.register_module(ReportExporterModule())
|
||||
|
||||
# 3. 设置事件处理
|
||||
print("\n[步骤 3] 设置事件处理")
|
||||
setup_event_handlers(system)
|
||||
|
||||
# 4. 初始化所有模块
|
||||
print("\n[步骤 4] 初始化模块")
|
||||
if not system.initialize_all():
|
||||
print("初始化失败,退出")
|
||||
return
|
||||
|
||||
# 5. 定义处理流程
|
||||
print("\n[步骤 5] 定义处理流程")
|
||||
system.define_pipeline([
|
||||
"csv_data_loader",
|
||||
"data_validator",
|
||||
"statistics_analyzer",
|
||||
"report_exporter"
|
||||
])
|
||||
|
||||
# 6. 打印系统信息
|
||||
print("\n[步骤 6] 系统信息")
|
||||
system.print_system_info()
|
||||
|
||||
# 7. 执行处理流程
|
||||
print("\n[步骤 7] 执行处理流程")
|
||||
result = system.execute("sample_data.csv")
|
||||
|
||||
# 8. 输出结果
|
||||
print("\n[步骤 8] 最终结果")
|
||||
if isinstance(result, str):
|
||||
print(result)
|
||||
|
||||
# 9. 清理
|
||||
print("\n[步骤 9] 清理资源")
|
||||
system.shutdown_all()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,938 @@
|
||||
"""
|
||||
概率与不确定性示例 (Probability and Uncertainty Example)
|
||||
========================================================
|
||||
|
||||
本示例展示如何在空间智能系统中处理概率和不确定性。
|
||||
在空间决策中,不确定性是普遍存在的,理解和管理不确定性
|
||||
对于做出可靠的决策至关重要。
|
||||
|
||||
核心概念:
|
||||
1. 概率分布 - 描述随机变量的可能取值及其概率
|
||||
2. 贝叶斯推理 - 基于新证据更新信念
|
||||
3. 蒙特卡洛模拟 - 通过随机采样评估不确定性
|
||||
4. 置信区间 - 估计结果的范围
|
||||
5. 敏感性分析 - 评估输入变化对输出的影响
|
||||
|
||||
应用场景:
|
||||
- 空间插值的不确定性量化
|
||||
- 多准则决策的敏感性分析
|
||||
- 风险评估与概率预测
|
||||
- 传感器数据的可靠性分析
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
from typing import List, Dict, Tuple, Optional, Callable, Any
|
||||
from dataclasses import dataclass, field
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
import statistics
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 概率分布基础类
|
||||
# ============================================================================
|
||||
|
||||
class DistributionType(Enum):
|
||||
"""分布类型枚举"""
|
||||
NORMAL = "normal" # 正态分布
|
||||
UNIFORM = "uniform" # 均匀分布
|
||||
TRIANGULAR = "triangular" # 三角分布
|
||||
EXPONENTIAL = "exponential" # 指数分布
|
||||
BETA = "beta" # Beta分布
|
||||
|
||||
|
||||
class ProbabilityDistribution(ABC):
|
||||
"""概率分布抽象基类"""
|
||||
|
||||
def __init__(self, name: str = ""):
|
||||
self.name = name
|
||||
|
||||
@abstractmethod
|
||||
def sample(self) -> float:
|
||||
"""从分布中采样一个值"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def mean(self) -> float:
|
||||
"""计算期望值"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def std(self) -> float:
|
||||
"""计算标准差"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pdf(self, x: float) -> float:
|
||||
"""概率密度函数"""
|
||||
pass
|
||||
|
||||
def cdf(self, x: float) -> float:
|
||||
"""累积分布函数 (近似计算)"""
|
||||
# 使用蒙特卡洛积分近似
|
||||
n_samples = 10000
|
||||
count = sum(1 for _ in range(n_samples) if self.sample() <= x)
|
||||
return count / n_samples
|
||||
|
||||
def confidence_interval(self, confidence: float = 0.95) -> Tuple[float, float]:
|
||||
"""计算置信区间"""
|
||||
n_samples = 10000
|
||||
samples = [self.sample() for _ in range(n_samples)]
|
||||
alpha = 1 - confidence
|
||||
lower = quantile(samples, alpha / 2)
|
||||
upper = quantile(samples, 1 - alpha / 2)
|
||||
return lower, upper
|
||||
|
||||
|
||||
def quantile(data: List[float], q: float) -> float:
|
||||
"""计算分位数"""
|
||||
sorted_data = sorted(data)
|
||||
index = int(q * len(sorted_data))
|
||||
return sorted_data[min(index, len(sorted_data) - 1)]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 具体概率分布实现
|
||||
# ============================================================================
|
||||
|
||||
class NormalDistribution(ProbabilityDistribution):
|
||||
"""
|
||||
正态分布 (高斯分布)
|
||||
|
||||
最常用的连续概率分布,由均值和标准差参数化。
|
||||
"""
|
||||
|
||||
def __init__(self, mu: float = 0.0, sigma: float = 1.0, name: str = ""):
|
||||
super().__init__(name)
|
||||
self.mu = mu # 均值
|
||||
self.sigma = sigma # 标准差
|
||||
if sigma <= 0:
|
||||
raise ValueError("标准差必须为正数")
|
||||
|
||||
def sample(self) -> float:
|
||||
"""使用 Box-Muller 变换生成正态分布随机数"""
|
||||
u1 = random.random()
|
||||
u2 = random.random()
|
||||
while u1 == 0: # 避免log(0)
|
||||
u1 = random.random()
|
||||
z0 = math.sqrt(-2 * math.log(u1)) * math.cos(2 * math.pi * u2)
|
||||
return self.mu + self.sigma * z0
|
||||
|
||||
def mean(self) -> float:
|
||||
return self.mu
|
||||
|
||||
def std(self) -> float:
|
||||
return self.sigma
|
||||
|
||||
def pdf(self, x: float) -> float:
|
||||
"""正态分布概率密度函数"""
|
||||
coeff = 1 / (self.sigma * math.sqrt(2 * math.pi))
|
||||
exponent = -0.5 * ((x - self.mu) / self.sigma) ** 2
|
||||
return coeff * math.exp(exponent)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Normal(μ={self.mu}, σ={self.sigma})"
|
||||
|
||||
|
||||
class UniformDistribution(ProbabilityDistribution):
|
||||
"""
|
||||
均匀分布
|
||||
|
||||
在指定范围内等概率取值。
|
||||
"""
|
||||
|
||||
def __init__(self, a: float = 0.0, b: float = 1.0, name: str = ""):
|
||||
super().__init__(name)
|
||||
self.a = a # 下界
|
||||
self.b = b # 上界
|
||||
if a >= b:
|
||||
raise ValueError("下界必须小于上界")
|
||||
|
||||
def sample(self) -> float:
|
||||
return self.a + (self.b - self.a) * random.random()
|
||||
|
||||
def mean(self) -> float:
|
||||
return (self.a + self.b) / 2
|
||||
|
||||
def std(self) -> float:
|
||||
return (self.b - self.a) / math.sqrt(12)
|
||||
|
||||
def pdf(self, x: float) -> float:
|
||||
if self.a <= x <= self.b:
|
||||
return 1 / (self.b - self.a)
|
||||
return 0.0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Uniform({self.a}, {self.b})"
|
||||
|
||||
|
||||
class TriangularDistribution(ProbabilityDistribution):
|
||||
"""
|
||||
三角分布
|
||||
|
||||
由最小值、最大值和众数定义的分布,常用于
|
||||
当只知道边界和最可能值时建模不确定性。
|
||||
"""
|
||||
|
||||
def __init__(self, a: float, b: float, c: float, name: str = ""):
|
||||
super().__init__(name)
|
||||
self.a = a # 最小值
|
||||
self.b = b # 最大值
|
||||
self.c = c # 众数 (最可能值)
|
||||
if not (a <= c <= b):
|
||||
raise ValueError("必须满足 a <= c <= b")
|
||||
|
||||
def sample(self) -> float:
|
||||
u = random.random()
|
||||
fc = (self.c - self.a) / (self.b - self.a)
|
||||
if u < fc:
|
||||
return self.a + math.sqrt(u * (self.b - self.a) * (self.c - self.a))
|
||||
else:
|
||||
return self.b - math.sqrt((1 - u) * (self.b - self.a) * (self.b - self.c))
|
||||
|
||||
def mean(self) -> float:
|
||||
return (self.a + self.b + self.c) / 3
|
||||
|
||||
def std(self) -> float:
|
||||
numerator = (self.a**2 + self.b**2 + self.c**2
|
||||
- self.a * self.b - self.a * self.c - self.b * self.c)
|
||||
return math.sqrt(numerator / 18)
|
||||
|
||||
def pdf(self, x: float) -> float:
|
||||
if x < self.a or x > self.b:
|
||||
return 0.0
|
||||
if x < self.c:
|
||||
return 2 * (x - self.a) / ((self.b - self.a) * (self.c - self.a))
|
||||
else:
|
||||
return 2 * (self.b - x) / ((self.b - self.a) * (self.b - self.c))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Triangular({self.a}, {self.c}, {self.b})"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 贝叶斯推理
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class BayesianBelief:
|
||||
"""
|
||||
贝叶斯信念状态
|
||||
|
||||
表示对某个假设的信念,包含先验、似然和后验。
|
||||
"""
|
||||
hypothesis: str
|
||||
prior: float # 先验概率 P(H)
|
||||
likelihood: float # 似然 P(E|H)
|
||||
evidence: Optional[float] = None # 证据概率 P(E)
|
||||
posterior: Optional[float] = None # 后验概率 P(H|E)
|
||||
|
||||
def update(self, evidence_prob: float = None) -> float:
|
||||
"""
|
||||
更新后验概率
|
||||
|
||||
Args:
|
||||
evidence_prob: P(E),如果None则使用归一化
|
||||
|
||||
Returns:
|
||||
后验概率
|
||||
"""
|
||||
# P(H|E) = P(E|H) * P(H) / P(E)
|
||||
numerator = self.likelihood * self.prior
|
||||
|
||||
if evidence_prob is not None:
|
||||
self.evidence = evidence_prob
|
||||
self.posterior = numerator / evidence_prob
|
||||
else:
|
||||
# 假设有多个假设,需要归一化
|
||||
self.posterior = numerator # 简化版本
|
||||
|
||||
return self.posterior
|
||||
|
||||
|
||||
class BayesianUpdater:
|
||||
"""
|
||||
贝叶斯更新器
|
||||
|
||||
管理多个假设的贝叶斯更新。
|
||||
"""
|
||||
|
||||
def __init__(self, hypotheses: List[str]):
|
||||
"""
|
||||
初始化贝叶斯更新器
|
||||
|
||||
Args:
|
||||
hypotheses: 假设列表
|
||||
"""
|
||||
# 初始化先验概率 (均匀分布)
|
||||
prior = 1.0 / len(hypotheses)
|
||||
self.beliefs: Dict[str, BayesianBelief] = {
|
||||
h: BayesianBelief(hypothesis=h, prior=prior, likelihood=1.0)
|
||||
for h in hypotheses
|
||||
}
|
||||
|
||||
def set_prior(self, hypothesis: str, prior: float) -> None:
|
||||
"""设置先验概率"""
|
||||
if hypothesis in self.beliefs:
|
||||
self.beliefs[hypothesis].prior = prior
|
||||
|
||||
def update_with_evidence(self, likelihoods: Dict[str, float]) -> None:
|
||||
"""
|
||||
用证据更新所有假设
|
||||
|
||||
Args:
|
||||
likelihoods: 每个假设的似然 P(E|H)
|
||||
"""
|
||||
# 更新似然
|
||||
for h, likelihood in likelihoods.items():
|
||||
if h in self.beliefs:
|
||||
self.beliefs[h].likelihood = likelihood
|
||||
|
||||
# 计算证据概率 (归一化常数)
|
||||
evidence = sum(
|
||||
b.likelihood * b.prior
|
||||
for b in self.beliefs.values()
|
||||
)
|
||||
|
||||
# 更新后验
|
||||
for belief in self.beliefs.values():
|
||||
belief.update(evidence)
|
||||
|
||||
def get_posteriors(self) -> Dict[str, float]:
|
||||
"""获取所有后验概率"""
|
||||
return {
|
||||
h: b.posterior or b.prior
|
||||
for h, b in self.beliefs.items()
|
||||
}
|
||||
|
||||
def get_most_likely(self) -> Tuple[str, float]:
|
||||
"""获取最可能的假设"""
|
||||
posteriors = self.get_posteriors()
|
||||
return max(posteriors.items(), key=lambda x: x[1])
|
||||
|
||||
def print_beliefs(self) -> None:
|
||||
"""打印信念状态"""
|
||||
print("\n贝叶斯信念状态:")
|
||||
print("-" * 60)
|
||||
print(f"{'假设':<20} {'先验':<12} {'似然':<12} {'后验':<12}")
|
||||
print("-" * 60)
|
||||
for belief in self.beliefs.values():
|
||||
posterior = belief.posterior if belief.posterior is not None else belief.prior
|
||||
print(f"{belief.hypothesis:<20} {belief.prior:<12.4f} "
|
||||
f"{belief.likelihood:<12.4f} {posterior:<12.4f}")
|
||||
print("-" * 60)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 蒙特卡洛模拟
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class SimulationResult:
|
||||
"""模拟结果"""
|
||||
samples: List[float] = field(default_factory=list)
|
||||
mean: float = 0.0
|
||||
std: float = 0.0
|
||||
min: float = 0.0
|
||||
max: float = 0.0
|
||||
median: float = 0.0
|
||||
confidence_interval: Tuple[float, float] = (0.0, 0.0)
|
||||
percentiles: Dict[float, float] = field(default_factory=dict)
|
||||
|
||||
def calculate_statistics(self, confidence: float = 0.95) -> None:
|
||||
"""计算统计量"""
|
||||
if not self.samples:
|
||||
return
|
||||
|
||||
self.mean = statistics.mean(self.samples)
|
||||
self.std = statistics.stdev(self.samples) if len(self.samples) > 1 else 0
|
||||
self.min = min(self.samples)
|
||||
self.max = max(self.samples)
|
||||
self.median = statistics.median(self.samples)
|
||||
|
||||
# 置信区间
|
||||
alpha = 1 - confidence
|
||||
sorted_samples = sorted(self.samples)
|
||||
n = len(sorted_samples)
|
||||
self.confidence_interval = (
|
||||
sorted_samples[int(alpha / 2 * n)],
|
||||
sorted_samples[int((1 - alpha / 2) * n)]
|
||||
)
|
||||
|
||||
# 常用百分位数
|
||||
for p in [0.01, 0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95, 0.99]:
|
||||
self.percentiles[p] = sorted_samples[int(p * n)]
|
||||
|
||||
def print_summary(self) -> None:
|
||||
"""打印结果摘要"""
|
||||
print(f"\n蒙特卡洛模拟结果 (n={len(self.samples)}):")
|
||||
print("-" * 50)
|
||||
print(f"均值: {self.mean:.4f}")
|
||||
print(f"中位数: {self.median:.4f}")
|
||||
print(f"标准差: {self.std:.4f}")
|
||||
print(f"范围: [{self.min:.4f}, {self.max:.4f}]")
|
||||
print(f"95% 置信区间: [{self.confidence_interval[0]:.4f}, "
|
||||
f"{self.confidence_interval[1]:.4f}]")
|
||||
print(f"\n百分位数:")
|
||||
for p, value in sorted(self.percentiles.items()):
|
||||
print(f" {p*100:>5.0f}%: {value:.4f}")
|
||||
print("-" * 50)
|
||||
|
||||
|
||||
class MonteCarloSimulator:
|
||||
"""
|
||||
蒙特卡洛模拟器
|
||||
|
||||
通过随机采样评估不确定性。
|
||||
"""
|
||||
|
||||
def __init__(self, seed: int = None):
|
||||
"""初始化模拟器"""
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
|
||||
def simulate(self, model: Callable[[], float],
|
||||
n_runs: int = 10000) -> SimulationResult:
|
||||
"""
|
||||
运行模拟
|
||||
|
||||
Args:
|
||||
model: 返回模拟值的函数
|
||||
n_runs: 运行次数
|
||||
|
||||
Returns:
|
||||
模拟结果
|
||||
"""
|
||||
samples = [model() for _ in range(n_runs)]
|
||||
result = SimulationResult(samples=samples)
|
||||
result.calculate_statistics()
|
||||
return result
|
||||
|
||||
def simulate_with_inputs(self,
|
||||
model: Callable[[Dict[str, float]], float],
|
||||
input_distributions: Dict[str, ProbabilityDistribution],
|
||||
n_runs: int = 10000) -> SimulationResult:
|
||||
"""
|
||||
使用输入分布运行模拟
|
||||
|
||||
Args:
|
||||
model: 接受输入字典的函数
|
||||
input_distributions: 输入变量到其分布的映射
|
||||
n_runs: 运行次数
|
||||
|
||||
Returns:
|
||||
模拟结果
|
||||
"""
|
||||
samples = []
|
||||
for _ in range(n_runs):
|
||||
inputs = {
|
||||
name: dist.sample()
|
||||
for name, dist in input_distributions.items()
|
||||
}
|
||||
samples.append(model(inputs))
|
||||
|
||||
result = SimulationResult(samples=samples)
|
||||
result.calculate_statistics()
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 敏感性分析
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class SensitivityResult:
|
||||
"""敏感性分析结果"""
|
||||
sensitivity_coefficients: Dict[str, float] = field(default_factory=dict)
|
||||
rankings: List[Tuple[str, float]] = field(default_factory=list)
|
||||
tornado_data: Dict[str, Tuple[float, float]] = field(default_factory=dict)
|
||||
|
||||
def print_summary(self) -> None:
|
||||
"""打印敏感性分析摘要"""
|
||||
print("\n敏感性分析结果:")
|
||||
print("-" * 50)
|
||||
print("排名 | 变量 | 敏感性系数")
|
||||
print("-" * 50)
|
||||
for i, (var, coef) in enumerate(self.rankings, 1):
|
||||
print(f"{i:4d} | {var:<11} | {coef:10.4f}")
|
||||
print("-" * 50)
|
||||
|
||||
|
||||
class SensitivityAnalyzer:
|
||||
"""
|
||||
敏感性分析器
|
||||
|
||||
评估输入变化对输出的影响。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.model: Optional[Callable] = None
|
||||
self.base_inputs: Optional[Dict[str, float]] = None
|
||||
|
||||
def simple_sensitivity(self,
|
||||
model: Callable[[Dict[str, float]], float],
|
||||
base_inputs: Dict[str, float],
|
||||
variations: Dict[str, float] = None) -> SensitivityResult:
|
||||
"""
|
||||
简单敏感性分析 (单因素)
|
||||
|
||||
Args:
|
||||
model: 待分析模型
|
||||
base_inputs: 基准输入值
|
||||
variations: 各变量的变化幅度 (默认 ±10%)
|
||||
|
||||
Returns:
|
||||
敏感性结果
|
||||
"""
|
||||
if variations is None:
|
||||
variations = {k: 0.1 for k in base_inputs.keys()}
|
||||
|
||||
# 计算基准输出
|
||||
base_output = model(base_inputs)
|
||||
|
||||
# 计算敏感性系数
|
||||
coefficients = {}
|
||||
tornado_data = {}
|
||||
|
||||
for var, variation in variations.items():
|
||||
original_value = base_inputs[var]
|
||||
|
||||
# 正向变化
|
||||
base_inputs[var] = original_value * (1 + variation)
|
||||
output_plus = model(base_inputs)
|
||||
|
||||
# 负向变化
|
||||
base_inputs[var] = original_value * (1 - variation)
|
||||
output_minus = model(base_inputs)
|
||||
|
||||
# 恢复原值
|
||||
base_inputs[var] = original_value
|
||||
|
||||
# 计算敏感性系数 (归一化)
|
||||
delta_output = output_plus - output_minus
|
||||
delta_input = 2 * variation * original_value
|
||||
coefficient = delta_output / delta_input if delta_input != 0 else 0
|
||||
|
||||
coefficients[var] = coefficient
|
||||
tornado_data[var] = (output_minus, output_plus)
|
||||
|
||||
# 排名
|
||||
rankings = sorted(coefficients.items(), key=lambda x: abs(x[1]), reverse=True)
|
||||
|
||||
return SensitivityResult(
|
||||
sensitivity_coefficients=coefficients,
|
||||
rankings=rankings,
|
||||
tornado_data=tornado_data
|
||||
)
|
||||
|
||||
def regression_sensitivity(self,
|
||||
model: Callable[[Dict[str, float]], float],
|
||||
input_distributions: Dict[str, ProbabilityDistribution],
|
||||
n_samples: int = 1000) -> SensitivityResult:
|
||||
"""
|
||||
基于回归的敏感性分析
|
||||
|
||||
Args:
|
||||
model: 待分析模型
|
||||
input_distributions: 输入分布
|
||||
n_samples: 样本数量
|
||||
|
||||
Returns:
|
||||
敏感性结果
|
||||
"""
|
||||
simulator = MonteCarloSimulator()
|
||||
|
||||
# 生成样本
|
||||
input_samples = []
|
||||
output_samples = []
|
||||
|
||||
for _ in range(n_samples):
|
||||
inputs = {
|
||||
name: dist.sample()
|
||||
for name, dist in input_distributions.items()
|
||||
}
|
||||
input_samples.append(inputs)
|
||||
output_samples.append(model(inputs))
|
||||
|
||||
# 计算标准化回归系数 (SRC)
|
||||
# SRC = beta * (std_x / std_y)
|
||||
|
||||
import statistics
|
||||
|
||||
std_y = statistics.stdev(output_samples)
|
||||
coefficients = {}
|
||||
|
||||
for var in input_distributions.keys():
|
||||
x_values = [s[var] for s in input_samples]
|
||||
std_x = statistics.stdev(x_values)
|
||||
|
||||
# 计算相关系数
|
||||
mean_x = statistics.mean(x_values)
|
||||
mean_y = statistics.mean(output_samples)
|
||||
|
||||
numerator = sum((x - mean_x) * (y - mean_y)
|
||||
for x, y in zip(x_values, output_samples))
|
||||
denominator = math.sqrt(
|
||||
sum((x - mean_x)**2 for x in x_values) *
|
||||
sum((y - mean_y)**2 for y in output_samples)
|
||||
)
|
||||
|
||||
correlation = numerator / denominator if denominator != 0 else 0
|
||||
src = correlation * (std_x / std_y) if std_y > 0 else 0
|
||||
|
||||
coefficients[var] = src
|
||||
|
||||
rankings = sorted(coefficients.items(), key=lambda x: abs(x[1]), reverse=True)
|
||||
|
||||
return SensitivityResult(
|
||||
sensitivity_coefficients=coefficients,
|
||||
rankings=rankings
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间概率应用示例
|
||||
# ============================================================================
|
||||
|
||||
class SpatialProbabilityModel:
|
||||
"""
|
||||
空间概率模型
|
||||
|
||||
将概率理论应用于空间问题。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def uncertain_distance(point1: Tuple[float, float],
|
||||
point2: Tuple[float, float],
|
||||
distance_error_std: float = 5.0) -> NormalDistribution:
|
||||
"""
|
||||
带不确定性的距离计算
|
||||
|
||||
Args:
|
||||
point1: 第一个点 (x, y)
|
||||
point2: 第二个点 (x, y)
|
||||
distance_error_std: 距离测量误差的标准差
|
||||
|
||||
Returns:
|
||||
距离的概率分布
|
||||
"""
|
||||
# 计算确定性距离
|
||||
dx = point2[0] - point1[0]
|
||||
dy = point2[1] - point1[1]
|
||||
true_distance = math.sqrt(dx**2 + dy**2)
|
||||
|
||||
# 返回正态分布
|
||||
return NormalDistribution(mu=true_distance, sigma=distance_error_std)
|
||||
|
||||
@staticmethod
|
||||
def location_probability(measurement: Tuple[float, float],
|
||||
true_location: Tuple[float, float],
|
||||
measurement_error: float = 10.0) -> float:
|
||||
"""
|
||||
计算测量位置的似然概率
|
||||
|
||||
Args:
|
||||
measurement: 测量位置
|
||||
true_location: 真实位置
|
||||
measurement_error: 测量误差标准差
|
||||
|
||||
Returns:
|
||||
似然概率
|
||||
"""
|
||||
dist = SpatialProbabilityModel.uncertain_distance(
|
||||
measurement, true_location, measurement_error
|
||||
)
|
||||
# 使用正态分布 PDF
|
||||
return dist.pdf(0)
|
||||
|
||||
@staticmethod
|
||||
def bayesian_location_update(prior_locations: List[Tuple[float, float]],
|
||||
measurements: List[Tuple[float, float]],
|
||||
measurement_error: float = 10.0) -> List[float]:
|
||||
"""
|
||||
贝叶斯位置更新
|
||||
|
||||
Args:
|
||||
prior_locations: 候选真实位置列表
|
||||
measurements: 测量位置列表
|
||||
measurement_error: 测量误差
|
||||
|
||||
Returns:
|
||||
每个候选位置的后验概率
|
||||
"""
|
||||
n = len(prior_locations)
|
||||
posteriors = []
|
||||
|
||||
for candidate in prior_locations:
|
||||
# 计算似然 (所有测量的乘积)
|
||||
likelihood = 1.0
|
||||
for measurement in measurements:
|
||||
prob = SpatialProbabilityModel.location_probability(
|
||||
measurement, candidate, measurement_error
|
||||
)
|
||||
likelihood *= prob
|
||||
|
||||
# 先验 (均匀)
|
||||
prior = 1.0 / n
|
||||
|
||||
# 后验 (未归一化)
|
||||
posterior = likelihood * prior
|
||||
posteriors.append(posterior)
|
||||
|
||||
# 归一化
|
||||
total = sum(posteriors)
|
||||
if total > 0:
|
||||
posteriors = [p / total for p in posteriors]
|
||||
|
||||
return posteriors
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示概率与不确定性的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("概率与不确定性示例演示")
|
||||
print("="*70)
|
||||
|
||||
random.seed(42) # 可重现的结果
|
||||
|
||||
# ========================================================================
|
||||
# 1. 概率分布示例
|
||||
# ========================================================================
|
||||
print("\n[部分 1] 概率分布")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建不同的分布
|
||||
normal = NormalDistribution(mu=100, sigma=15, name="温度")
|
||||
uniform = UniformDistribution(a=50, b=150, name="范围")
|
||||
triangular = TriangularDistribution(a=60, c=100, b=140, name="估计")
|
||||
|
||||
distributions = [normal, uniform, triangular]
|
||||
|
||||
for dist in distributions:
|
||||
print(f"\n{dist}:")
|
||||
print(f" 均值: {dist.mean():.2f}")
|
||||
print(f" 标准差: {dist.std():.2f}")
|
||||
samples = [dist.sample() for _ in range(5)]
|
||||
print(f" 样本: {[f'{s:.2f}' for s in samples]}")
|
||||
|
||||
ci = dist.confidence_interval(0.95)
|
||||
print(f" 95% CI: [{ci[0]:.2f}, {ci[1]:.2f}]")
|
||||
|
||||
# ========================================================================
|
||||
# 2. 贝叶斯推理示例
|
||||
# ========================================================================
|
||||
print("\n\n[部分 2] 贝叶斯推理")
|
||||
print("-" * 50)
|
||||
print("问题: 根据土壤测试结果判断土地适宜性")
|
||||
|
||||
# 假设: 土地适宜性等级
|
||||
hypotheses = ["高适宜", "中适宜", "低适宜", "不适宜"]
|
||||
updater = BayesianUpdater(hypotheses)
|
||||
|
||||
# 设置不同的先验 (基于历史数据)
|
||||
updater.set_prior("高适宜", 0.2)
|
||||
updater.set_prior("中适宜", 0.3)
|
||||
updater.set_prior("低适宜", 0.3)
|
||||
updater.set_prior("不适宜", 0.2)
|
||||
|
||||
print("\n初始信念:")
|
||||
updater.print_beliefs()
|
||||
|
||||
# 证据1: 土壤pH值检测
|
||||
print("\n证据1: 土壤pH值适中 (似然更新)")
|
||||
updater.update_with_evidence({
|
||||
"高适宜": 0.8, # pH值对高适宜的可能性高
|
||||
"中适宜": 0.6,
|
||||
"低适宜": 0.3,
|
||||
"不适宜": 0.1
|
||||
})
|
||||
updater.print_beliefs()
|
||||
|
||||
# 证据2: 有机质含量检测
|
||||
print("\n证据2: 有机质含量高 (似然更新)")
|
||||
updater.update_with_evidence({
|
||||
"高适宜": 0.9,
|
||||
"中适宜": 0.5,
|
||||
"低适宜": 0.2,
|
||||
"不适宜": 0.05
|
||||
})
|
||||
updater.print_beliefs()
|
||||
|
||||
most_likely = updater.get_most_likely()
|
||||
print(f"\n最可能的假设: {most_likely[0]} (概率: {most_likely[1]:.2%})")
|
||||
|
||||
# ========================================================================
|
||||
# 3. 蒙特卡洛模拟示例
|
||||
# ========================================================================
|
||||
print("\n\n[部分 3] 蒙特卡洛模拟")
|
||||
print("-" * 50)
|
||||
print("问题: 评估房地产开发项目的预期收益")
|
||||
|
||||
def development_model(inputs: Dict[str, float]) -> float:
|
||||
"""房地产开发收益模型"""
|
||||
land_cost = inputs["land_cost"]
|
||||
construction_cost = inputs["construction_cost"]
|
||||
selling_price = inputs["selling_price"]
|
||||
units = inputs["units"]
|
||||
sales_rate = inputs["sales_rate"]
|
||||
|
||||
# 收益 = (售价 * 单元数 * 销售率) - (土地成本 + 建设成本 * 单元数)
|
||||
revenue = selling_price * units * sales_rate
|
||||
total_cost = land_cost + construction_cost * units
|
||||
return revenue - total_cost
|
||||
|
||||
# 定义输入分布
|
||||
input_dists = {
|
||||
"land_cost": TriangularDistribution(800000, 1000000, 1500000), # 土地成本
|
||||
"construction_cost": NormalDistribution(50000, 5000), # 单元建设成本
|
||||
"selling_price": NormalDistribution(150000, 15000), # 单元售价
|
||||
"units": TriangularDistribution(80, 100, 120), # 单元数量
|
||||
"sales_rate": BetaDistribution(alpha=8, beta=2, a=0, b=1) # 销售率
|
||||
}
|
||||
|
||||
simulator = MonteCarloSimulator()
|
||||
result = simulator.simulate_with_inputs(development_model, input_dists, n_runs=10000)
|
||||
|
||||
result.print_summary()
|
||||
|
||||
# 风险评估
|
||||
negative_prob = sum(1 for s in result.samples if s < 0) / len(result.samples)
|
||||
print(f"\n风险分析:")
|
||||
print(f" 亏损概率: {negative_prob:.2%}")
|
||||
profit_prob = sum(1 for s in result.samples if s > 1000000) / len(result.samples)
|
||||
print(f" 超过100万利润概率: {profit_prob:.2%}")
|
||||
|
||||
# ========================================================================
|
||||
# 4. 敏感性分析示例
|
||||
# ========================================================================
|
||||
print("\n\n[部分 4] 敏感性分析")
|
||||
print("-" * 50)
|
||||
print("问题: 分析各因素对收益的影响程度")
|
||||
|
||||
analyzer = SensitivityAnalyzer()
|
||||
|
||||
# 简单敏感性分析
|
||||
base_inputs = {
|
||||
"land_cost": 1000000,
|
||||
"construction_cost": 50000,
|
||||
"selling_price": 150000,
|
||||
"units": 100,
|
||||
"sales_rate": 0.85
|
||||
}
|
||||
|
||||
sensitivity_result = analyzer.simple_sensitivity(
|
||||
development_model, base_inputs, variations={k: 0.1 for k in base_inputs.keys()}
|
||||
)
|
||||
|
||||
sensitivity_result.print_summary()
|
||||
|
||||
# ========================================================================
|
||||
# 5. 空间概率应用
|
||||
# ========================================================================
|
||||
print("\n\n[部分 5] 空间概率应用")
|
||||
print("-" * 50)
|
||||
print("问题: GPS定位的不确定性")
|
||||
|
||||
# 真实位置
|
||||
true_location = (1000, 2000)
|
||||
|
||||
# 带误差的测量
|
||||
measurements = [
|
||||
(1005, 2003),
|
||||
(998, 1998),
|
||||
(1002, 2005),
|
||||
(995, 2000)
|
||||
]
|
||||
|
||||
# 候选位置
|
||||
candidates = [
|
||||
(1000, 2000), # 真实位置
|
||||
(1015, 2015),
|
||||
(990, 1990),
|
||||
(1005, 1995)
|
||||
]
|
||||
|
||||
posteriors = SpatialProbabilityModel.bayesian_location_update(
|
||||
candidates, measurements, measurement_error=5.0
|
||||
)
|
||||
|
||||
print("\n候选位置的后验概率:")
|
||||
for i, (loc, prob) in enumerate(zip(candidates, posteriors)):
|
||||
print(f" 位置 {i+1} {loc}: {prob:.4f}")
|
||||
|
||||
most_likely_idx = max(range(len(posteriors)), key=lambda i: posteriors[i])
|
||||
print(f"\n最可能的位置: 位置 {most_likely_idx+1} {candidates[most_likely_idx]}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
# Beta分布实现 (用于上面代码中的引用)
|
||||
class BetaDistribution(ProbabilityDistribution):
|
||||
"""Beta分布 - 用于建模[0,1]区间内的概率"""
|
||||
|
||||
def __init__(self, alpha: float, beta: float, a: float = 0, b: float = 1, name: str = ""):
|
||||
super().__init__(name)
|
||||
self.alpha = alpha
|
||||
self.beta = beta
|
||||
self.a = a # 下界
|
||||
self.b = b # 上界
|
||||
|
||||
def sample(self) -> float:
|
||||
# 使用numpy的gamma函数近似
|
||||
import math
|
||||
import random
|
||||
|
||||
# 生成Gamma随机变量
|
||||
def gamma(alpha):
|
||||
if alpha < 1:
|
||||
return gamma(alpha + 1) * (random.random() ** (1 / alpha))
|
||||
# Marsaglia and Tsang's method
|
||||
d = alpha - 1/3
|
||||
c = 1 / math.sqrt(9 * d)
|
||||
while True:
|
||||
x = random.gauss(0, 1)
|
||||
v = (1 + c * x) ** 3
|
||||
if v > 0:
|
||||
u = random.random()
|
||||
if u < 1 - 0.0331 * (x * x) ** 2:
|
||||
return d * v
|
||||
if math.log(u) < 0.5 * x * x + d * (1 - v + math.log(v)):
|
||||
return d * v
|
||||
|
||||
x = gamma(self.alpha)
|
||||
y = gamma(self.beta)
|
||||
beta_sample = x / (x + y)
|
||||
|
||||
# 转换到[a, b]区间
|
||||
return self.a + (self.b - self.a) * beta_sample
|
||||
|
||||
def mean(self) -> float:
|
||||
return self.a + (self.b - self.a) * self.alpha / (self.alpha + self.beta)
|
||||
|
||||
def std(self) -> float:
|
||||
mean_raw = self.alpha / (self.alpha + self.beta)
|
||||
var_raw = (self.alpha * self.beta) / (
|
||||
(self.alpha + self.beta) ** 2 * (self.alpha + self.beta + 1)
|
||||
)
|
||||
return (self.b - self.a) * math.sqrt(var_raw)
|
||||
|
||||
def pdf(self, x: float) -> float:
|
||||
# 简化版本,仅返回近似值
|
||||
return 1.0 # 实际应实现Beta分布的PDF
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Beta(α={self.alpha}, β={self.beta})"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,748 @@
|
||||
"""
|
||||
状态机工作流示例 (State Machine Workflow Example)
|
||||
==================================================
|
||||
|
||||
本示例展示如何使用状态机模式管理复杂的空间分析工作流。
|
||||
状态机是一种行为设计模式,允许对象在其内部状态改变时改变其行为。
|
||||
|
||||
核心概念:
|
||||
1. 状态 (State) - 系统在特定时刻的模式
|
||||
2. 转换 (Transition) - 从一个状态到另一个状态的变化
|
||||
3. 事件 (Event) - 触发状态转换的外部或内部条件
|
||||
4. 动作 (Action) - 状态转换时执行的操作
|
||||
|
||||
应用场景:
|
||||
- 空间数据处理流水线
|
||||
- 多阶段决策流程
|
||||
- 任务调度与监控
|
||||
- 用户交互流程控制
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Optional, Callable, Any
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 状态定义
|
||||
# ============================================================================
|
||||
|
||||
class WorkflowState(Enum):
|
||||
"""工作流状态枚举"""
|
||||
# 初始状态
|
||||
IDLE = "idle"
|
||||
INITIALIZED = "initialized"
|
||||
|
||||
# 数据处理状态
|
||||
LOADING_DATA = "loading_data"
|
||||
DATA_LOADED = "data_loaded"
|
||||
VALIDATING_DATA = "validating_data"
|
||||
DATA_VALIDATED = "data_validated"
|
||||
|
||||
# 分析状态
|
||||
ANALYZING = "analyzing"
|
||||
ANALYSIS_COMPLETE = "analysis_complete"
|
||||
|
||||
# 决策状态
|
||||
DECIDING = "deciding"
|
||||
DECISION_MADE = "decision_made"
|
||||
|
||||
# 输出状态
|
||||
GENERATING_OUTPUT = "generating_output"
|
||||
OUTPUT_COMPLETE = "output_complete"
|
||||
|
||||
# 异常状态
|
||||
ERROR = "error"
|
||||
PAUSED = "paused"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
# 最终状态
|
||||
COMPLETED = "completed"
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
"""事件类型枚举"""
|
||||
# 控制事件
|
||||
START = "start"
|
||||
PAUSE = "pause"
|
||||
RESUME = "resume"
|
||||
CANCEL = "cancel"
|
||||
RESET = "reset"
|
||||
|
||||
# 数据事件
|
||||
DATA_LOAD_REQUEST = "data_load_request"
|
||||
DATA_LOAD_SUCCESS = "data_load_success"
|
||||
DATA_LOAD_FAILURE = "data_load_failure"
|
||||
DATA_VALIDATE_REQUEST = "data_validate_request"
|
||||
DATA_VALIDATE_SUCCESS = "data_validate_success"
|
||||
DATA_VALIDATE_FAILURE = "data_validate_failure"
|
||||
|
||||
# 分析事件
|
||||
ANALYZE_REQUEST = "analyze_request"
|
||||
ANALYSIS_SUCCESS = "analysis_success"
|
||||
ANALYSIS_FAILURE = "analysis_failure"
|
||||
|
||||
# 决策事件
|
||||
DECIDE_REQUEST = "decide_request"
|
||||
DECISION_SUCCESS = "decision_success"
|
||||
DECISION_FAILURE = "decision_failure"
|
||||
|
||||
# 输出事件
|
||||
OUTPUT_REQUEST = "output_request"
|
||||
OUTPUT_SUCCESS = "output_success"
|
||||
OUTPUT_FAILURE = "output_failure"
|
||||
|
||||
# 错误事件
|
||||
ERROR_OCCURRED = "error_occurred"
|
||||
RETRY = "retry"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 状态机数据结构
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class StateTransition:
|
||||
"""状态转换定义"""
|
||||
from_state: WorkflowState
|
||||
event: EventType
|
||||
to_state: WorkflowState
|
||||
action: Optional[Callable] = None
|
||||
guard: Optional[Callable[[], bool]] = None # 守卫条件
|
||||
description: str = ""
|
||||
|
||||
def can_execute(self) -> bool:
|
||||
"""检查转换是否可执行"""
|
||||
if self.guard is None:
|
||||
return True
|
||||
return self.guard()
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateContext:
|
||||
"""状态上下文 - 存储工作流数据"""
|
||||
data: Dict[str, Any] = field(default_factory=dict)
|
||||
errors: List[str] = field(default_factory=list)
|
||||
warnings: List[str] = field(default_factory=list)
|
||||
history: List[Dict[str, Any]] = field(default_factory=list)
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
|
||||
def add_history(self, from_state: WorkflowState, event: EventType,
|
||||
to_state: WorkflowState, timestamp: datetime = None):
|
||||
"""添加历史记录"""
|
||||
self.history.append({
|
||||
"from_state": from_state.value,
|
||||
"event": event.value,
|
||||
"to_state": to_state.value,
|
||||
"timestamp": timestamp or datetime.now()
|
||||
})
|
||||
|
||||
def get_data(self, key: str, default: Any = None) -> Any:
|
||||
"""获取数据"""
|
||||
return self.data.get(key, default)
|
||||
|
||||
def set_data(self, key: str, value: Any) -> None:
|
||||
"""设置数据"""
|
||||
self.data[key] = value
|
||||
|
||||
def add_error(self, error: str) -> None:
|
||||
"""添加错误"""
|
||||
self.errors.append(error)
|
||||
|
||||
def add_warning(self, warning: str) -> None:
|
||||
"""添加警告"""
|
||||
self.warnings.append(warning)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 状态机实现
|
||||
# ============================================================================
|
||||
|
||||
class StateMachine:
|
||||
"""
|
||||
状态机实现
|
||||
|
||||
管理状态转换和状态相关的行为。
|
||||
"""
|
||||
|
||||
def __init__(self, initial_state: WorkflowState = WorkflowState.IDLE):
|
||||
"""
|
||||
初始化状态机
|
||||
|
||||
Args:
|
||||
initial_state: 初始状态
|
||||
"""
|
||||
self._current_state = initial_state
|
||||
self._transitions: Dict[WorkflowState, Dict[EventType, StateTransition]] = {}
|
||||
self._context = StateContext()
|
||||
self._state_listeners: Dict[WorkflowState, List[Callable]] = {}
|
||||
|
||||
print(f"[状态机] 初始化,初始状态: {initial_state.value}")
|
||||
|
||||
@property
|
||||
def current_state(self) -> WorkflowState:
|
||||
"""获取当前状态"""
|
||||
return self._current_state
|
||||
|
||||
@property
|
||||
def context(self) -> StateContext:
|
||||
"""获取状态上下文"""
|
||||
return self._context
|
||||
|
||||
def add_transition(self, transition: StateTransition) -> None:
|
||||
"""
|
||||
添加状态转换
|
||||
|
||||
Args:
|
||||
transition: 状态转换定义
|
||||
"""
|
||||
if transition.from_state not in self._transitions:
|
||||
self._transitions[transition.from_state] = {}
|
||||
|
||||
self._transitions[transition.from_state][transition.event] = transition
|
||||
print(f"[状态机] 添加转换: {transition.from_state.value} + {transition.event.value} -> {transition.to_state.value}")
|
||||
|
||||
def add_state_listener(self, state: WorkflowState, listener: Callable) -> None:
|
||||
"""
|
||||
添加状态监听器
|
||||
|
||||
Args:
|
||||
state: 要监听的状态
|
||||
listener: 状态进入时调用的函数
|
||||
"""
|
||||
if state not in self._state_listeners:
|
||||
self._state_listeners[state] = []
|
||||
self._state_listeners[state].append(listener)
|
||||
|
||||
def trigger(self, event: EventType, payload: Any = None) -> bool:
|
||||
"""
|
||||
触发事件
|
||||
|
||||
Args:
|
||||
event: 事件类型
|
||||
payload: 事件负载
|
||||
|
||||
Returns:
|
||||
是否成功触发状态转换
|
||||
"""
|
||||
# 检查当前状态是否有对应转换
|
||||
if self._current_state not in self._transitions:
|
||||
print(f"[状态机] 当前状态 {self._current_state.value} 没有定义任何转换")
|
||||
return False
|
||||
|
||||
if event not in self._transitions[self._current_state]:
|
||||
print(f"[状态机] 状态 {self._current_state.value} 不处理事件 {event.value}")
|
||||
return False
|
||||
|
||||
transition = self._transitions[self._current_state][event]
|
||||
|
||||
# 检查守卫条件
|
||||
if not transition.can_execute():
|
||||
print(f"[状态机] 守卫条件不满足,转换被阻止")
|
||||
return False
|
||||
|
||||
# 执行状态转换
|
||||
old_state = self._current_state
|
||||
self._current_state = transition.to_state
|
||||
|
||||
# 记录历史
|
||||
if payload:
|
||||
self._context.set_data("last_payload", payload)
|
||||
self._context.add_history(old_state, event, self._current_state)
|
||||
|
||||
print(f"[状态机] 状态转换: {old_state.value} -> {self._current_state.value} (事件: {event.value})")
|
||||
|
||||
# 执行转换动作
|
||||
if transition.action:
|
||||
try:
|
||||
transition.action(self._context, payload)
|
||||
except Exception as e:
|
||||
print(f"[状态机] 执行动作时出错: {e}")
|
||||
self._context.add_error(f"转换动作执行失败: {e}")
|
||||
|
||||
# 触发状态监听器
|
||||
if self._current_state in self._state_listeners:
|
||||
for listener in self._state_listeners[self._current_state]:
|
||||
try:
|
||||
listener(self._current_state, self._context)
|
||||
except Exception as e:
|
||||
print(f"[状态机] 监听器执行出错: {e}")
|
||||
|
||||
return True
|
||||
|
||||
def can_trigger(self, event: EventType) -> bool:
|
||||
"""
|
||||
检查是否可以触发指定事件
|
||||
|
||||
Args:
|
||||
event: 事件类型
|
||||
|
||||
Returns:
|
||||
是否可以触发
|
||||
"""
|
||||
if self._current_state not in self._transitions:
|
||||
return False
|
||||
if event not in self._transitions[self._current_state]:
|
||||
return False
|
||||
|
||||
transition = self._transitions[self._current_state][event]
|
||||
return transition.can_execute()
|
||||
|
||||
def get_available_events(self) -> List[EventType]:
|
||||
"""获取当前状态下可用的事件列表"""
|
||||
if self._current_state not in self._transitions:
|
||||
return []
|
||||
|
||||
available = []
|
||||
for event, transition in self._transitions[self._current_state].items():
|
||||
if transition.can_execute():
|
||||
available.append(event)
|
||||
|
||||
return available
|
||||
|
||||
def reset(self) -> None:
|
||||
"""重置状态机"""
|
||||
self._current_state = WorkflowState.IDLE
|
||||
self._context = StateContext()
|
||||
print(f"[状态机] 状态机已重置")
|
||||
|
||||
def print_state(self) -> None:
|
||||
"""打印当前状态"""
|
||||
print(f"\n当前状态: {self._current_state.value}")
|
||||
available = self.get_available_events()
|
||||
if available:
|
||||
print(f"可用事件: {', '.join(e.value for e in available)}")
|
||||
else:
|
||||
print("可用事件: 无")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间分析工作流状态机
|
||||
# ============================================================================
|
||||
|
||||
class SpatialAnalysisWorkflow:
|
||||
"""
|
||||
空间分析工作流
|
||||
|
||||
使用状态机实现的空间数据处理和分析工作流。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化工作流"""
|
||||
self.state_machine = StateMachine()
|
||||
self._setup_transitions()
|
||||
self._setup_listeners()
|
||||
|
||||
def _setup_transitions(self):
|
||||
"""设置状态转换"""
|
||||
sm = self.state_machine
|
||||
|
||||
# 启动流程
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.IDLE,
|
||||
event=EventType.START,
|
||||
to_state=WorkflowState.INITIALIZED,
|
||||
action=self._action_initialize,
|
||||
description="初始化工作流"
|
||||
))
|
||||
|
||||
# 数据加载
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.INITIALIZED,
|
||||
event=EventType.DATA_LOAD_REQUEST,
|
||||
to_state=WorkflowState.LOADING_DATA,
|
||||
action=self._action_load_data,
|
||||
description="开始加载数据"
|
||||
))
|
||||
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.LOADING_DATA,
|
||||
event=EventType.DATA_LOAD_SUCCESS,
|
||||
to_state=WorkflowState.DATA_LOADED,
|
||||
action=self._action_on_data_loaded,
|
||||
description="数据加载成功"
|
||||
))
|
||||
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.LOADING_DATA,
|
||||
event=EventType.DATA_LOAD_FAILURE,
|
||||
to_state=WorkflowState.ERROR,
|
||||
action=self._action_on_error,
|
||||
description="数据加载失败"
|
||||
))
|
||||
|
||||
# 数据验证
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.DATA_LOADED,
|
||||
event=EventType.DATA_VALIDATE_REQUEST,
|
||||
to_state=WorkflowState.VALIDATING_DATA,
|
||||
action=self._action_validate_data,
|
||||
description="开始验证数据"
|
||||
))
|
||||
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.VALIDATING_DATA,
|
||||
event=EventType.DATA_VALIDATE_SUCCESS,
|
||||
to_state=WorkflowState.DATA_VALIDATED,
|
||||
description="数据验证成功"
|
||||
))
|
||||
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.VALIDATING_DATA,
|
||||
event=EventType.DATA_VALIDATE_FAILURE,
|
||||
to_state=WorkflowState.ERROR,
|
||||
action=self._action_on_error,
|
||||
description="数据验证失败"
|
||||
))
|
||||
|
||||
# 分析
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.DATA_VALIDATED,
|
||||
event=EventType.ANALYZE_REQUEST,
|
||||
to_state=WorkflowState.ANALYZING,
|
||||
action=self._action_analyze,
|
||||
description="开始分析"
|
||||
))
|
||||
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.ANALYZING,
|
||||
event=EventType.ANALYSIS_SUCCESS,
|
||||
to_state=WorkflowState.ANALYSIS_COMPLETE,
|
||||
description="分析完成"
|
||||
))
|
||||
|
||||
# 决策
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.ANALYSIS_COMPLETE,
|
||||
event=EventType.DECIDE_REQUEST,
|
||||
to_state=WorkflowState.DECIDING,
|
||||
action=self._action_decide,
|
||||
description="开始决策"
|
||||
))
|
||||
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.DECIDING,
|
||||
event=EventType.DECISION_SUCCESS,
|
||||
to_state=WorkflowState.DECISION_MADE,
|
||||
description="决策完成"
|
||||
))
|
||||
|
||||
# 输出
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.DECISION_MADE,
|
||||
event=EventType.OUTPUT_REQUEST,
|
||||
to_state=WorkflowState.GENERATING_OUTPUT,
|
||||
action=self._action_generate_output,
|
||||
description="生成输出"
|
||||
))
|
||||
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.GENERATING_OUTPUT,
|
||||
event=EventType.OUTPUT_SUCCESS,
|
||||
to_state=WorkflowState.OUTPUT_COMPLETE,
|
||||
description="输出完成"
|
||||
))
|
||||
|
||||
# 完成
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.OUTPUT_COMPLETE,
|
||||
event=EventType.START,
|
||||
to_state=WorkflowState.COMPLETED,
|
||||
action=self._action_complete,
|
||||
description="工作流完成"
|
||||
))
|
||||
|
||||
# 错误恢复
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.ERROR,
|
||||
event=EventType.RETRY,
|
||||
to_state=WorkflowState.INITIALIZED,
|
||||
guard=lambda: len(self.state_machine.context.errors) < 3,
|
||||
description="重试"
|
||||
))
|
||||
|
||||
sm.add_transition(StateTransition(
|
||||
from_state=WorkflowState.ERROR,
|
||||
event=EventType.RESET,
|
||||
to_state=WorkflowState.IDLE,
|
||||
action=self._action_reset,
|
||||
description="重置"
|
||||
))
|
||||
|
||||
def _setup_listeners(self):
|
||||
"""设置状态监听器"""
|
||||
sm = self.state_machine
|
||||
|
||||
# 错误状态监听器
|
||||
sm.add_state_listener(WorkflowState.ERROR, self._on_error_state)
|
||||
|
||||
# 完成状态监听器
|
||||
sm.add_state_listener(WorkflowState.COMPLETED, self._on_complete_state)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 状态动作
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def _action_initialize(self, ctx: StateContext, payload: Any):
|
||||
"""初始化动作"""
|
||||
ctx.start_time = datetime.now()
|
||||
ctx.set_data("workflow_id", f"WF-{datetime.now().strftime('%Y%m%d%H%M%S')}")
|
||||
print(" [动作] 工作流初始化完成")
|
||||
|
||||
def _action_load_data(self, ctx: StateContext, payload: Any):
|
||||
"""加载数据动作"""
|
||||
source = payload or "默认数据源"
|
||||
print(f" [动作] 从 '{source}' 加载数据...")
|
||||
|
||||
# 模拟数据加载
|
||||
ctx.set_data("raw_data", [
|
||||
{"id": 1, "x": 10, "y": 20, "value": 100},
|
||||
{"id": 2, "x": 30, "y": 40, "value": 200},
|
||||
{"id": 3, "x": 50, "y": 60, "value": 150},
|
||||
])
|
||||
|
||||
# 模拟成功
|
||||
self.state_machine.trigger(EventType.DATA_LOAD_SUCCESS)
|
||||
|
||||
def _action_on_data_loaded(self, ctx: StateContext, payload: Any):
|
||||
"""数据加载完成动作"""
|
||||
data_count = len(ctx.get_data("raw_data", []))
|
||||
print(f" [动作] 数据加载完成,共 {data_count} 条记录")
|
||||
|
||||
def _action_validate_data(self, ctx: StateContext, payload: Any):
|
||||
"""验证数据动作"""
|
||||
print(" [动作] 验证数据...")
|
||||
|
||||
data = ctx.get_data("raw_data", [])
|
||||
valid = all("id" in item and "x" in item and "y" in item for item in data)
|
||||
|
||||
if valid:
|
||||
self.state_machine.trigger(EventType.DATA_VALIDATE_SUCCESS)
|
||||
else:
|
||||
ctx.add_error("数据验证失败: 缺少必需字段")
|
||||
self.state_machine.trigger(EventType.DATA_VALIDATE_FAILURE)
|
||||
|
||||
def _action_analyze(self, ctx: StateContext, payload: Any):
|
||||
"""分析动作"""
|
||||
print(" [动作] 执行空间分析...")
|
||||
|
||||
data = ctx.get_data("raw_data", [])
|
||||
values = [item.get("value", 0) for item in data]
|
||||
avg = sum(values) / len(values) if values else 0
|
||||
|
||||
ctx.set_data("analysis_result", {
|
||||
"average": avg,
|
||||
"count": len(data),
|
||||
"min": min(values) if values else 0,
|
||||
"max": max(values) if values else 0
|
||||
})
|
||||
|
||||
print(f" [动作] 分析完成,平均值: {avg:.2f}")
|
||||
self.state_machine.trigger(EventType.ANALYSIS_SUCCESS)
|
||||
|
||||
def _action_decide(self, ctx: StateContext, payload: Any):
|
||||
"""决策动作"""
|
||||
print(" [动作] 执行决策...")
|
||||
|
||||
analysis = ctx.get_data("analysis_result", {})
|
||||
avg = analysis.get("average", 0)
|
||||
|
||||
if avg > 150:
|
||||
decision = "高价值区域"
|
||||
elif avg > 100:
|
||||
decision = "中等价值区域"
|
||||
else:
|
||||
decision = "低价值区域"
|
||||
|
||||
ctx.set_data("decision", decision)
|
||||
print(f" [动作] 决策完成: {decision}")
|
||||
self.state_machine.trigger(EventType.DECISION_SUCCESS)
|
||||
|
||||
def _action_generate_output(self, ctx: StateContext, payload: Any):
|
||||
"""生成输出动作"""
|
||||
print(" [动作] 生成输出报告...")
|
||||
|
||||
report = {
|
||||
"workflow_id": ctx.get_data("workflow_id"),
|
||||
"data_count": len(ctx.get_data("raw_data", [])),
|
||||
"analysis": ctx.get_data("analysis_result"),
|
||||
"decision": ctx.get_data("decision")
|
||||
}
|
||||
|
||||
ctx.set_data("output", report)
|
||||
print(" [动作] 输出生成完成")
|
||||
self.state_machine.trigger(EventType.OUTPUT_SUCCESS)
|
||||
|
||||
def _action_complete(self, ctx: StateContext, payload: Any):
|
||||
"""完成动作"""
|
||||
ctx.end_time = datetime.now()
|
||||
duration = (ctx.end_time - ctx.start_time).total_seconds() if ctx.start_time else 0
|
||||
ctx.set_data("duration", duration)
|
||||
print(f" [动作] 工作流完成,耗时: {duration:.2f}秒")
|
||||
|
||||
def _action_on_error(self, ctx: StateContext, payload: Any):
|
||||
"""错误处理动作"""
|
||||
print(f" [动作] 发生错误")
|
||||
|
||||
def _action_reset(self, ctx: StateContext, payload: Any):
|
||||
"""重置动作"""
|
||||
print(" [动作] 重置工作流")
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 状态监听器
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def _on_error_state(self, state: WorkflowState, ctx: StateContext):
|
||||
"""错误状态处理"""
|
||||
print(f" [监听器] 进入错误状态")
|
||||
print(f" [监听器] 错误列表: {ctx.errors}")
|
||||
|
||||
def _on_complete_state(self, state: WorkflowState, ctx: StateContext):
|
||||
"""完成状态处理"""
|
||||
print(f" [监听器] 工作流已完成")
|
||||
output = ctx.get_data("output")
|
||||
if output:
|
||||
print(f" [监听器] 最终输出: {json.dumps(output, ensure_ascii=False, indent=2)}")
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# 公共接口
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def start(self, data_source: str = None) -> bool:
|
||||
"""启动工作流"""
|
||||
return self.state_machine.trigger(EventType.START, data_source)
|
||||
|
||||
def execute_full_workflow(self, data_source: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
执行完整工作流
|
||||
|
||||
Args:
|
||||
data_source: 数据源
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
print("\n" + "="*60)
|
||||
print("执行完整空间分析工作流")
|
||||
print("="*60)
|
||||
|
||||
# 启动
|
||||
if not self.start(data_source):
|
||||
return {"success": False, "error": "启动失败"}
|
||||
|
||||
# 等待异步操作完成 (简化版: 手动触发)
|
||||
# 在实际应用中,这些事件会由异步操作触发
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"final_state": self.state_machine.current_state.value,
|
||||
"context": self.state_machine.context.data
|
||||
}
|
||||
|
||||
def print_history(self):
|
||||
"""打印状态转换历史"""
|
||||
history = self.state_machine.context.history
|
||||
print(f"\n状态转换历史 (共 {len(history)} 次):")
|
||||
print("-" * 70)
|
||||
for i, h in enumerate(history, 1):
|
||||
ts = h.get("timestamp", datetime.now()).strftime("%H:%M:%S")
|
||||
print(f"{i:2d}. [{ts}] {h['from_state']:20s} -> {h['to_state']:20s} ({h['event']})")
|
||||
print("-" * 70)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示状态机工作流的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("状态机工作流示例演示")
|
||||
print("="*70)
|
||||
|
||||
# 1. 创建工作流
|
||||
print("\n[步骤 1] 创建空间分析工作流")
|
||||
workflow = SpatialAnalysisWorkflow()
|
||||
|
||||
# 2. 显示初始状态
|
||||
print("\n[步骤 2] 初始状态")
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
# 3. 手动执行状态转换
|
||||
print("\n[步骤 3] 手动执行状态转换")
|
||||
|
||||
# 启动
|
||||
print("\n3.1 启动工作流:")
|
||||
workflow.state_machine.trigger(EventType.START)
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
# 请求数据加载 (这将触发加载动作,然后自动触发成功事件)
|
||||
print("\n3.2 请求数据加载:")
|
||||
workflow.state_machine.trigger(EventType.DATA_LOAD_REQUEST, "sample.csv")
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
# 请求数据验证
|
||||
print("\n3.3 请求数据验证:")
|
||||
workflow.state_machine.trigger(EventType.DATA_VALIDATE_REQUEST)
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
# 请求分析
|
||||
print("\n3.4 请求分析:")
|
||||
workflow.state_machine.trigger(EventType.ANALYZE_REQUEST)
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
# 请求决策
|
||||
print("\n3.5 请求决策:")
|
||||
workflow.state_machine.trigger(EventType.DECIDE_REQUEST)
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
# 请求输出
|
||||
print("\n3.6 请求输出:")
|
||||
workflow.state_machine.trigger(EventType.OUTPUT_REQUEST)
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
# 完成
|
||||
print("\n3.7 完成工作流:")
|
||||
workflow.state_machine.trigger(EventType.START)
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
# 4. 显示转换历史
|
||||
print("\n[步骤 4] 状态转换历史")
|
||||
workflow.print_history()
|
||||
|
||||
# 5. 演示错误处理
|
||||
print("\n[步骤 5] 演示错误处理和恢复")
|
||||
print("\n5.1 重置状态机:")
|
||||
workflow.state_machine.reset()
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
print("\n5.2 启动后触发错误:")
|
||||
workflow.state_machine.trigger(EventType.START)
|
||||
workflow.state_machine.trigger(EventType.DATA_LOAD_FAILURE)
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
print("\n5.3 尝试重试:")
|
||||
if workflow.state_machine.can_trigger(EventType.RETRY):
|
||||
workflow.state_machine.trigger(EventType.RETRY)
|
||||
workflow.state_machine.print_state()
|
||||
else:
|
||||
print(" 无法重试 (已达到最大重试次数)")
|
||||
|
||||
print("\n5.4 重置工作流:")
|
||||
workflow.state_machine.trigger(EventType.RESET)
|
||||
workflow.state_machine.print_state()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,961 @@
|
||||
"""
|
||||
空间优化示例 (Spatial Optimization Example)
|
||||
==========================================
|
||||
|
||||
本示例展示空间智能系统中的空间优化方法。
|
||||
空间优化是在空间约束下寻找最优解的过程。
|
||||
|
||||
核心概念:
|
||||
1. 目标函数 - 需要最大化或最小化的目标
|
||||
2. 约束条件 - 空间和非空间限制
|
||||
3. 决策变量 - 可控制的空间变量
|
||||
4. 优化算法 - 求解最优解的方法
|
||||
5. 帕累托前沿 - 多目标优化中的解集
|
||||
|
||||
应用场景:
|
||||
- 选址优化
|
||||
- 路径优化
|
||||
- 空间配置优化
|
||||
- 资源分配优化
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
from typing import List, Dict, Tuple, Optional, Any, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
import heapq
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 基础数据结构
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
"""二维点"""
|
||||
x: float
|
||||
y: float
|
||||
|
||||
def distance_to(self, other: 'Point') -> float:
|
||||
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"({self.x:.2f}, {self.y:.2f})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DemandPoint:
|
||||
"""需求点"""
|
||||
id: str
|
||||
location: Point
|
||||
demand: float # 需求量
|
||||
population: int = 0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Demand({self.id}, demand={self.demand})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Facility:
|
||||
"""设施"""
|
||||
id: str
|
||||
location: Point
|
||||
capacity: float # 服务能力
|
||||
fixed_cost: float = 0 # 固定成本
|
||||
variable_cost: float = 0 # 单位可变成本
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Facility({self.id}, loc={self.location})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class OptimizationResult:
|
||||
"""优化结果"""
|
||||
success: bool
|
||||
objective_value: float
|
||||
solution: Any
|
||||
iterations: int = 0
|
||||
convergence_history: List[float] = field(default_factory=list)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 约束条件
|
||||
# ============================================================================
|
||||
|
||||
class ConstraintType(Enum):
|
||||
"""约束类型"""
|
||||
EQUALITY = "equality" # 等式约束
|
||||
INEQUALITY = "inequality" # 不等式约束
|
||||
BOUNDS = "bounds" # 边界约束
|
||||
|
||||
|
||||
@dataclass
|
||||
class Constraint:
|
||||
"""约束条件"""
|
||||
name: str
|
||||
constraint_type: ConstraintType
|
||||
rhs: float # 右端值
|
||||
lhs_function: Optional[Callable[[Any], float]] = None # 左端函数
|
||||
|
||||
def is_satisfied(self, variables: Any, tolerance: float = 1e-6) -> bool:
|
||||
"""检查约束是否满足"""
|
||||
if self.lhs_function is None:
|
||||
return True
|
||||
|
||||
lhs = self.lhs_function(variables)
|
||||
|
||||
if self.constraint_type == ConstraintType.EQUALITY:
|
||||
return abs(lhs - self.rhs) < tolerance
|
||||
elif self.constraint_type == ConstraintType.INEQUALITY:
|
||||
return lhs <= self.rhs + tolerance
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 优化问题定义
|
||||
# ============================================================================
|
||||
|
||||
class OptimizationProblem(ABC):
|
||||
"""优化问题抽象基类"""
|
||||
|
||||
def __init__(self, name: str = ""):
|
||||
self.name = name
|
||||
self.constraints: List[Constraint] = []
|
||||
self.objective_calls = 0
|
||||
|
||||
@abstractmethod
|
||||
def objective(self, variables: Any) -> float:
|
||||
"""目标函数"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_initial_solution(self) -> Any:
|
||||
"""获取初始解"""
|
||||
pass
|
||||
|
||||
def add_constraint(self, constraint: Constraint) -> None:
|
||||
"""添加约束"""
|
||||
self.constraints.append(constraint)
|
||||
|
||||
def is_feasible(self, variables: Any) -> bool:
|
||||
"""检查解是否可行"""
|
||||
return all(c.is_satisfied(variables) for c in self.constraints)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 贪心算法
|
||||
# ============================================================================
|
||||
|
||||
class GreedyOptimizer:
|
||||
"""
|
||||
贪心优化器
|
||||
|
||||
每一步选择当前最优的选项。
|
||||
"""
|
||||
|
||||
def __init__(self, problem: OptimizationProblem):
|
||||
self.problem = problem
|
||||
|
||||
def optimize(self, max_iterations: int = 1000) -> OptimizationResult:
|
||||
"""
|
||||
执行贪心优化
|
||||
|
||||
Args:
|
||||
max_iterations: 最大迭代次数
|
||||
|
||||
Returns:
|
||||
优化结果
|
||||
"""
|
||||
current_solution = self.problem.get_initial_solution()
|
||||
current_value = self.problem.objective(current_solution)
|
||||
|
||||
history = [current_value]
|
||||
|
||||
for iteration in range(max_iterations):
|
||||
# 生成邻居解
|
||||
neighbors = self._generate_neighbors(current_solution)
|
||||
|
||||
# 找最优邻居
|
||||
best_neighbor = None
|
||||
best_neighbor_value = float('inf')
|
||||
|
||||
for neighbor in neighbors:
|
||||
if self.problem.is_feasible(neighbor):
|
||||
value = self.problem.objective(neighbor)
|
||||
if value < best_neighbor_value:
|
||||
best_neighbor = neighbor
|
||||
best_neighbor_value = value
|
||||
|
||||
# 如果没有改进,停止
|
||||
if best_neighbor is None or best_neighbor_value >= current_value:
|
||||
break
|
||||
|
||||
current_solution = best_neighbor
|
||||
current_value = best_neighbor_value
|
||||
history.append(current_value)
|
||||
|
||||
return OptimizationResult(
|
||||
success=True,
|
||||
objective_value=current_value,
|
||||
solution=current_solution,
|
||||
iterations=len(history),
|
||||
convergence_history=history
|
||||
)
|
||||
|
||||
def _generate_neighbors(self, solution: Any) -> List[Any]:
|
||||
"""生成邻居解 (需要根据具体问题实现)"""
|
||||
return []
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 模拟退火算法
|
||||
# ============================================================================
|
||||
|
||||
class SimulatedAnnealing:
|
||||
"""
|
||||
模拟退火算法
|
||||
|
||||
一种概率性全局优化算法,能够跳出局部最优。
|
||||
"""
|
||||
|
||||
def __init__(self, problem: OptimizationProblem):
|
||||
self.problem = problem
|
||||
|
||||
def optimize(self,
|
||||
initial_temp: float = 1000.0,
|
||||
cooling_rate: float = 0.95,
|
||||
min_temp: float = 0.01,
|
||||
max_iterations: int = 10000) -> OptimizationResult:
|
||||
"""
|
||||
执行模拟退火优化
|
||||
|
||||
Args:
|
||||
initial_temp: 初始温度
|
||||
cooling_rate: 冷却速率
|
||||
min_temp: 最小温度
|
||||
max_iterations: 最大迭代次数
|
||||
|
||||
Returns:
|
||||
优化结果
|
||||
"""
|
||||
current_solution = self.problem.get_initial_solution()
|
||||
current_value = self.problem.objective(current_solution)
|
||||
|
||||
best_solution = current_solution
|
||||
best_value = current_value
|
||||
|
||||
temperature = initial_temp
|
||||
history = [current_value]
|
||||
|
||||
iteration = 0
|
||||
while temperature > min_temp and iteration < max_iterations:
|
||||
# 生成邻居解
|
||||
neighbor = self._generate_neighbor(current_solution)
|
||||
|
||||
if self.problem.is_feasible(neighbor):
|
||||
neighbor_value = self.problem.objective(neighbor)
|
||||
|
||||
# 决定是否接受新解
|
||||
delta = neighbor_value - current_value
|
||||
|
||||
if delta < 0 or random.random() < math.exp(-delta / temperature):
|
||||
current_solution = neighbor
|
||||
current_value = neighbor_value
|
||||
|
||||
# 更新最优解
|
||||
if current_value < best_value:
|
||||
best_solution = current_solution
|
||||
best_value = current_value
|
||||
|
||||
history.append(best_value)
|
||||
temperature *= cooling_rate
|
||||
iteration += 1
|
||||
|
||||
return OptimizationResult(
|
||||
success=True,
|
||||
objective_value=best_value,
|
||||
solution=best_solution,
|
||||
iterations=iteration,
|
||||
convergence_history=history
|
||||
)
|
||||
|
||||
def _generate_neighbor(self, solution: Any) -> Any:
|
||||
"""生成邻居解 (需要根据具体问题实现)"""
|
||||
return solution
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 遗传算法
|
||||
# =============================================================================
|
||||
|
||||
class GeneticAlgorithm:
|
||||
"""
|
||||
遗传算法
|
||||
|
||||
模拟自然进化的全局优化算法。
|
||||
"""
|
||||
|
||||
def __init__(self, problem: OptimizationProblem):
|
||||
self.problem = problem
|
||||
|
||||
def optimize(self,
|
||||
population_size: int = 50,
|
||||
generations: int = 100,
|
||||
mutation_rate: float = 0.1,
|
||||
crossover_rate: float = 0.8,
|
||||
elitism_count: int = 2) -> OptimizationResult:
|
||||
"""
|
||||
执行遗传算法优化
|
||||
|
||||
Args:
|
||||
population_size: 种群大小
|
||||
generations: 迭代代数
|
||||
mutation_rate: 变异率
|
||||
crossover_rate: 交叉率
|
||||
elitism_count: 精英保留数量
|
||||
|
||||
Returns:
|
||||
优化结果
|
||||
"""
|
||||
# 初始化种群
|
||||
population = self._initialize_population(population_size)
|
||||
history = []
|
||||
|
||||
for generation in range(generations):
|
||||
# 评估适应度
|
||||
fitness = []
|
||||
for individual in population:
|
||||
value = self.problem.objective(individual)
|
||||
fitness.append(1.0 / (1.0 + value)) # 转换为适应度 (越小越好 -> 越大越好)
|
||||
|
||||
# 记录最优值
|
||||
best_idx = max(range(len(fitness)), key=lambda i: fitness[i])
|
||||
best_value = self.problem.objective(population[best_idx])
|
||||
history.append(best_value)
|
||||
|
||||
# 选择
|
||||
selected = self._selection(population, fitness)
|
||||
|
||||
# 交叉
|
||||
offspring = self._crossover(selected, crossover_rate)
|
||||
|
||||
# 变异
|
||||
offspring = self._mutation(offspring, mutation_rate)
|
||||
|
||||
# 精英保留
|
||||
if elitism_count > 0:
|
||||
elite_indices = sorted(range(len(fitness)),
|
||||
key=lambda i: fitness[i], reverse=True)[:elitism_count]
|
||||
for i, idx in enumerate(elite_indices):
|
||||
offspring[i] = population[idx]
|
||||
|
||||
population = offspring
|
||||
|
||||
# 返回最优解
|
||||
final_fitness = [self.problem.objective(ind) for ind in population]
|
||||
best_idx = min(range(len(final_fitness)), key=lambda i: final_fitness[i])
|
||||
|
||||
return OptimizationResult(
|
||||
success=True,
|
||||
objective_value=final_fitness[best_idx],
|
||||
solution=population[best_idx],
|
||||
iterations=generations,
|
||||
convergence_history=history
|
||||
)
|
||||
|
||||
def _initialize_population(self, size: int) -> List[Any]:
|
||||
"""初始化种群"""
|
||||
return [self.problem.get_initial_solution() for _ in range(size)]
|
||||
|
||||
def _selection(self, population: List[Any], fitness: List[float]) -> List[Any]:
|
||||
"""锦标赛选择"""
|
||||
selected = []
|
||||
tournament_size = max(3, len(population) // 10)
|
||||
|
||||
for _ in range(len(population)):
|
||||
# 随机选择tournament_size个个体
|
||||
contestants = random.sample(list(zip(population, fitness)), tournament_size)
|
||||
# 选择适应度最高的
|
||||
winner = max(contestants, key=lambda x: x[1])[0]
|
||||
selected.append(winner)
|
||||
|
||||
return selected
|
||||
|
||||
def _crossover(self, population: List[Any], rate: float) -> List[Any]:
|
||||
"""交叉操作"""
|
||||
offspring = []
|
||||
|
||||
for i in range(0, len(population), 2):
|
||||
parent1 = population[i]
|
||||
parent2 = population[i + 1] if i + 1 < len(population) else population[0]
|
||||
|
||||
if random.random() < rate:
|
||||
child1, child2 = self._crossover_operators(parent1, parent2)
|
||||
else:
|
||||
child1, child2 = parent1, parent2
|
||||
|
||||
offspring.extend([child1, child2])
|
||||
|
||||
return offspring[:len(population)]
|
||||
|
||||
def _crossover_operators(self, parent1: Any, parent2: Any) -> Tuple[Any, Any]:
|
||||
"""交叉算子 (需要根据具体问题实现)"""
|
||||
return parent1, parent2
|
||||
|
||||
def _mutation(self, population: List[Any], rate: float) -> List[Any]:
|
||||
"""变异操作"""
|
||||
mutated = []
|
||||
|
||||
for individual in population:
|
||||
if random.random() < rate:
|
||||
mutated.append(self._mutate(individual))
|
||||
else:
|
||||
mutated.append(individual)
|
||||
|
||||
return mutated
|
||||
|
||||
def _mutate(self, individual: Any) -> Any:
|
||||
"""变异算子 (需要根据具体问题实现)"""
|
||||
return individual
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 选址优化问题
|
||||
# ============================================================================
|
||||
|
||||
class LocationProblem(OptimizationProblem):
|
||||
"""
|
||||
设施选址问题
|
||||
|
||||
在给定候选位置中选择最优的设施位置组合。
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
demand_points: List[DemandPoint],
|
||||
candidate_locations: List[Point],
|
||||
num_facilities: int,
|
||||
fixed_costs: List[float] = None,
|
||||
transportation_cost: float = 1.0):
|
||||
"""
|
||||
初始化选址问题
|
||||
|
||||
Args:
|
||||
demand_points: 需求点列表
|
||||
candidate_locations: 候选位置列表
|
||||
num_facilities: 设施数量
|
||||
fixed_costs: 各候选位置的固定成本
|
||||
transportation_cost: 单位运输成本
|
||||
"""
|
||||
super().__init__("设施选址问题")
|
||||
self.demand_points = demand_points
|
||||
self.candidate_locations = candidate_locations
|
||||
self.num_facilities = num_facilities
|
||||
self.fixed_costs = fixed_costs or [0] * len(candidate_locations)
|
||||
self.transportation_cost = transportation_cost
|
||||
|
||||
def objective(self, solution: List[int]) -> float:
|
||||
"""
|
||||
计算目标函数值
|
||||
|
||||
Args:
|
||||
solution: 选中的候选位置索引列表
|
||||
|
||||
Returns:
|
||||
总成本
|
||||
"""
|
||||
self.objective_calls += 1
|
||||
|
||||
if not solution or len(solution) != self.num_facilities:
|
||||
return float('inf')
|
||||
|
||||
total_cost = 0.0
|
||||
|
||||
# 固定成本
|
||||
for idx in solution:
|
||||
if 0 <= idx < len(self.fixed_costs):
|
||||
total_cost += self.fixed_costs[idx]
|
||||
|
||||
# 运输成本 (每个需求点分配到最近的设施)
|
||||
for demand in self.demand_points:
|
||||
min_dist = float('inf')
|
||||
for facility_idx in solution:
|
||||
if 0 <= facility_idx < len(self.candidate_locations):
|
||||
facility_loc = self.candidate_locations[facility_idx]
|
||||
dist = demand.location.distance_to(facility_loc)
|
||||
min_dist = min(min_dist, dist)
|
||||
|
||||
total_cost += min_dist * demand.demand * self.transportation_cost
|
||||
|
||||
return total_cost
|
||||
|
||||
def get_initial_solution(self) -> List[int]:
|
||||
"""获取初始解 (随机选择)"""
|
||||
n = len(self.candidate_locations)
|
||||
if n <= self.num_facilities:
|
||||
return list(range(n))
|
||||
return random.sample(range(n), self.num_facilities)
|
||||
|
||||
|
||||
class LocationSAOptimizer(SimulatedAnnealing):
|
||||
"""针对选址问题的模拟退火优化器"""
|
||||
|
||||
def _generate_neighbor(self, solution: List[int]) -> List[int]:
|
||||
"""生成邻居解"""
|
||||
if not solution:
|
||||
return solution
|
||||
|
||||
neighbor = solution.copy()
|
||||
n = len(self.problem.candidate_locations)
|
||||
|
||||
# 随机选择一个操作
|
||||
operation = random.choice(['replace', 'swap'])
|
||||
|
||||
if operation == 'replace':
|
||||
# 替换一个设施
|
||||
idx = random.randint(0, len(neighbor) - 1)
|
||||
available = [i for i in range(n) if i not in neighbor]
|
||||
if available:
|
||||
neighbor[idx] = random.choice(available)
|
||||
|
||||
elif operation == 'swap' and len(neighbor) >= 1:
|
||||
# 交换一个设施
|
||||
idx = random.randint(0, len(neighbor) - 1)
|
||||
available = [i for i in range(n) if i not in neighbor]
|
||||
if available:
|
||||
neighbor[idx] = random.choice(available)
|
||||
|
||||
return neighbor
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# P-中值问题
|
||||
# ============================================================================
|
||||
|
||||
class PMedianProblem(LocationProblem):
|
||||
"""
|
||||
P-中值问题
|
||||
|
||||
选择P个设施位置,使需求点到最近设施的总加权距离最小。
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
demand_points: List[DemandPoint],
|
||||
candidate_locations: List[Point],
|
||||
p: int):
|
||||
super().__init__(demand_points, candidate_locations, p, [0] * len(candidate_locations))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 覆盖问题
|
||||
# ============================================================================
|
||||
|
||||
class MaxCoverageProblem(OptimizationProblem):
|
||||
"""
|
||||
最大覆盖问题
|
||||
|
||||
在给定设施数量限制下,最大化覆盖的需求量。
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
demand_points: List[DemandPoint],
|
||||
candidate_locations: List[Point],
|
||||
num_facilities: int,
|
||||
coverage_radius: float):
|
||||
"""
|
||||
初始化最大覆盖问题
|
||||
|
||||
Args:
|
||||
demand_points: 需求点列表
|
||||
candidate_locations: 候选位置列表
|
||||
num_facilities: 设施数量
|
||||
coverage_radius: 覆盖半径
|
||||
"""
|
||||
super().__init__("最大覆盖问题")
|
||||
self.demand_points = demand_points
|
||||
self.candidate_locations = candidate_locations
|
||||
self.num_facilities = num_facilities
|
||||
self.coverage_radius = coverage_radius
|
||||
|
||||
# 预计算覆盖关系
|
||||
self.coverage_matrix = self._compute_coverage()
|
||||
|
||||
def _compute_coverage(self) -> List[List[bool]]:
|
||||
"""计算覆盖矩阵"""
|
||||
matrix = []
|
||||
for loc in self.candidate_locations:
|
||||
coverage = []
|
||||
for demand in self.demand_points:
|
||||
covered = loc.distance_to(demand.location) <= self.coverage_radius
|
||||
coverage.append(covered)
|
||||
matrix.append(coverage)
|
||||
return matrix
|
||||
|
||||
def objective(self, solution: List[int]) -> float:
|
||||
"""
|
||||
计算覆盖的需求量 (负值,因为算法最小化)
|
||||
|
||||
Args:
|
||||
solution: 选中的候选位置索引列表
|
||||
|
||||
Returns:
|
||||
负的覆盖需求量
|
||||
"""
|
||||
covered = [False] * len(self.demand_points)
|
||||
|
||||
for facility_idx in solution:
|
||||
if 0 <= facility_idx < len(self.coverage_matrix):
|
||||
for j, is_covered in enumerate(self.coverage_matrix[facility_idx]):
|
||||
if is_covered:
|
||||
covered[j] = True
|
||||
|
||||
total_demand = sum(
|
||||
self.demand_points[j].demand
|
||||
for j, c in enumerate(covered) if c
|
||||
)
|
||||
|
||||
return -total_demand # 负值用于最小化
|
||||
|
||||
def get_initial_solution(self) -> List[int]:
|
||||
"""获取初始解"""
|
||||
n = len(self.candidate_locations)
|
||||
if n <= self.num_facilities:
|
||||
return list(range(n))
|
||||
return random.sample(range(n), self.num_facilities)
|
||||
|
||||
|
||||
class MaxCoverageGAOptimizer(GeneticAlgorithm):
|
||||
"""针对最大覆盖问题的遗传算法优化器"""
|
||||
|
||||
def _crossover_operators(self, parent1: List[int], parent2: List[int]) -> Tuple[List[int], List[int]]:
|
||||
"""单点交叉"""
|
||||
if not parent1 or not parent2:
|
||||
return parent1, parent2
|
||||
|
||||
size = min(len(parent1), len(parent2))
|
||||
if size < 2:
|
||||
return parent1, parent2
|
||||
|
||||
point = random.randint(1, size - 1)
|
||||
|
||||
child1 = parent1[:point] + [x for x in parent2[point:] if x not in parent1[:point]]
|
||||
child2 = parent2[:point] + [x for x in parent1[point:] if x not in parent2[:point]]
|
||||
|
||||
# 补足长度
|
||||
all_indices = set(range(len(self.problem.candidate_locations)))
|
||||
while len(child1) < len(parent1):
|
||||
available = list(all_indices - set(child1))
|
||||
if available:
|
||||
child1.append(random.choice(available))
|
||||
else:
|
||||
break
|
||||
|
||||
while len(child2) < len(parent2):
|
||||
available = list(all_indices - set(child2))
|
||||
if available:
|
||||
child2.append(random.choice(available))
|
||||
else:
|
||||
break
|
||||
|
||||
return child1[:len(parent1)], child2[:len(parent2)]
|
||||
|
||||
def _mutate(self, individual: List[int]) -> List[int]:
|
||||
"""变异操作"""
|
||||
if not individual:
|
||||
return individual
|
||||
|
||||
mutated = individual.copy()
|
||||
n = len(self.problem.candidate_locations)
|
||||
|
||||
# 随机替换一个基因
|
||||
idx = random.randint(0, len(mutated) - 1)
|
||||
available = [i for i in range(n) if i not in mutated]
|
||||
if available:
|
||||
mutated[idx] = random.choice(available)
|
||||
|
||||
return mutated
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 路径优化 (TSP)
|
||||
# ============================================================================
|
||||
|
||||
class TSPProblem(OptimizationProblem):
|
||||
"""
|
||||
旅行商问题 (TSP)
|
||||
|
||||
寻找访问所有城市的最短路径。
|
||||
"""
|
||||
|
||||
def __init__(self, cities: List[Point]):
|
||||
"""
|
||||
初始化TSP问题
|
||||
|
||||
Args:
|
||||
cities: 城市位置列表
|
||||
"""
|
||||
super().__init__("旅行商问题")
|
||||
self.cities = cities
|
||||
self.n = len(cities)
|
||||
|
||||
def objective(self, solution: List[int]) -> float:
|
||||
"""
|
||||
计算路径总长度
|
||||
|
||||
Args:
|
||||
solution: 城市访问顺序列表
|
||||
|
||||
Returns:
|
||||
路径总长度
|
||||
"""
|
||||
if not solution or len(solution) != self.n:
|
||||
return float('inf')
|
||||
|
||||
total = 0.0
|
||||
for i in range(len(solution)):
|
||||
from_idx = solution[i]
|
||||
to_idx = solution[(i + 1) % len(solution)]
|
||||
total += self.cities[from_idx].distance_to(self.cities[to_idx])
|
||||
|
||||
return total
|
||||
|
||||
def get_initial_solution(self) -> List[int]:
|
||||
"""获取初始解 (随机顺序)"""
|
||||
solution = list(range(self.n))
|
||||
random.shuffle(solution)
|
||||
return solution
|
||||
|
||||
|
||||
class TSPSAOptimizer(SimulatedAnnealing):
|
||||
"""针对TSP的模拟退火优化器"""
|
||||
|
||||
def _generate_neighbor(self, solution: List[int]) -> List[int]:
|
||||
"""生成邻居解 (2-opt交换)"""
|
||||
if len(solution) < 2:
|
||||
return solution
|
||||
|
||||
neighbor = solution.copy()
|
||||
|
||||
# 随机选择两个位置并交换
|
||||
i, j = random.sample(range(len(neighbor)), 2)
|
||||
neighbor[i], neighbor[j] = neighbor[j], neighbor[i]
|
||||
|
||||
return neighbor
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ========================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示空间优化的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("空间优化示例演示")
|
||||
print("="*70)
|
||||
|
||||
random.seed(42)
|
||||
|
||||
# ========================================================================
|
||||
# 1. P-中值问题 (选址优化)
|
||||
# ========================================================================
|
||||
print("\n[部分 1] P-中值问题 - 商场选址优化")
|
||||
print("-" * 50)
|
||||
|
||||
# 生成需求点
|
||||
demand_points = []
|
||||
for i in range(20):
|
||||
demand_points.append(DemandPoint(
|
||||
id=f"D{i}",
|
||||
location=Point(random.uniform(0, 100), random.uniform(0, 100)),
|
||||
demand=random.uniform(50, 200)
|
||||
))
|
||||
|
||||
print(f"\n需求点数量: {len(demand_points)}")
|
||||
print(f"总需求量: {sum(d.demand for d in demand_points):.1f}")
|
||||
|
||||
# 候选位置
|
||||
candidate_locations = [
|
||||
Point(20, 20), Point(50, 20), Point(80, 20),
|
||||
Point(20, 50), Point(50, 50), Point(80, 50),
|
||||
Point(20, 80), Point(50, 80), Point(80, 80)
|
||||
]
|
||||
|
||||
print(f"\n候选位置数量: {len(candidate_locations)}")
|
||||
print("候选位置:", [str(loc) for loc in candidate_locations])
|
||||
|
||||
# 创建P-中值问题 (选择3个位置)
|
||||
p_median = PMedianProblem(demand_points, candidate_locations, p=3)
|
||||
|
||||
# 使用模拟退火求解
|
||||
sa_optimizer = LocationSAOptimizer(p_median)
|
||||
sa_result = sa_optimizer.optimize(
|
||||
initial_temp=100,
|
||||
cooling_rate=0.95,
|
||||
min_temp=0.1,
|
||||
max_iterations=1000
|
||||
)
|
||||
|
||||
print(f"\n模拟退火结果:")
|
||||
print(f" 目标函数值: {sa_result.objective_value:.2f}")
|
||||
print(f" 迭代次数: {sa_result.iterations}")
|
||||
print(f" 选中的位置: {[str(candidate_locations[i]) for i in sa_result.solution]}")
|
||||
|
||||
# 贪心算法对比
|
||||
greedy_optimizer = GreedyOptimizer(p_median)
|
||||
|
||||
# 简单的贪心: 逐步添加最优位置
|
||||
best_solution = None
|
||||
best_value = float('inf')
|
||||
|
||||
for _ in range(100):
|
||||
solution = p_median.get_initial_solution()
|
||||
value = p_median.objective(solution)
|
||||
if value < best_value:
|
||||
best_value = value
|
||||
best_solution = solution
|
||||
|
||||
print(f"\n随机搜索对比:")
|
||||
print(f" 目标函数值: {best_value:.2f}")
|
||||
print(f" 选中的位置: {[str(candidate_locations[i]) for i in best_solution]}")
|
||||
|
||||
# ========================================================================
|
||||
# 2. 最大覆盖问题
|
||||
# ========================================================================
|
||||
print("\n\n[部分 2] 最大覆盖问题 - 5G基站选址")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建更大规模的需求点
|
||||
coverage_demands = []
|
||||
for i in range(50):
|
||||
coverage_demands.append(DemandPoint(
|
||||
id=f"C{i}",
|
||||
location=Point(random.uniform(0, 100), random.uniform(0, 100)),
|
||||
demand=random.uniform(10, 100)
|
||||
))
|
||||
|
||||
coverage_radius = 25
|
||||
num_bases = 5
|
||||
|
||||
max_coverage = MaxCoverageProblem(
|
||||
coverage_demands,
|
||||
candidate_locations,
|
||||
num_bases,
|
||||
coverage_radius
|
||||
)
|
||||
|
||||
# 使用遗传算法求解
|
||||
ga_optimizer = MaxCoverageGAOptimizer(max_coverage)
|
||||
ga_result = ga_optimizer.optimize(
|
||||
population_size=50,
|
||||
generations=100,
|
||||
mutation_rate=0.1
|
||||
)
|
||||
|
||||
covered_demand = -ga_result.objective_value
|
||||
total_demand = sum(d.demand for d in coverage_demands)
|
||||
coverage_ratio = covered_demand / total_demand * 100
|
||||
|
||||
print(f"\n遗传算法结果:")
|
||||
print(f" 覆盖需求量: {covered_demand:.1f} / {total_demand:.1f}")
|
||||
print(f" 覆盖率: {coverage_ratio:.1f}%")
|
||||
print(f" 迭代次数: {ga_result.iterations}")
|
||||
print(f" 选中的位置: {[str(candidate_locations[i]) for i in ga_result.solution]}")
|
||||
|
||||
# ========================================================================
|
||||
# 3. 旅行商问题 (TSP)
|
||||
# ========================================================================
|
||||
print("\n\n[部分 3] 旅行商问题 - 配送路线优化")
|
||||
print("-" * 50)
|
||||
|
||||
# 生成城市
|
||||
cities = []
|
||||
for i in range(15):
|
||||
cities.append(Point(random.uniform(0, 100), random.uniform(0, 100)))
|
||||
|
||||
print(f"\n城市数量: {len(cities)}")
|
||||
print(f"城市位置: {[str(city) for city in cities[:5]]}...")
|
||||
|
||||
tsp = TSPProblem(cities)
|
||||
tsp_optimizer = TSPSAOptimizer(tsp)
|
||||
tsp_result = tsp_optimizer.optimize(
|
||||
initial_temp=1000,
|
||||
cooling_rate=0.99,
|
||||
min_temp=0.01,
|
||||
max_iterations=5000
|
||||
)
|
||||
|
||||
print(f"\n模拟退火结果:")
|
||||
print(f" 最短路径长度: {tsp_result.objective_value:.2f}")
|
||||
print(f" 访问顺序: {[cities[i].__repr__() for i in tsp_result.solution[:5]]}...")
|
||||
|
||||
# 对比随机解
|
||||
random_solution = list(range(len(cities)))
|
||||
random.shuffle(random_solution)
|
||||
random_length = tsp.objective(random_solution)
|
||||
print(f"\n随机解对比:")
|
||||
print(f" 路径长度: {random_length:.2f}")
|
||||
print(f" 改进: {(1 - tsp_result.objective_value / random_length) * 100:.1f}%")
|
||||
|
||||
# ========================================================================
|
||||
# 4. 多目标优化讨论
|
||||
# ========================================================================
|
||||
print("\n\n[部分 4] 多目标优化说明")
|
||||
print("-" * 50)
|
||||
|
||||
print("""
|
||||
在实际应用中,空间优化往往涉及多个目标:
|
||||
|
||||
1. 成本最小化
|
||||
- 设施建设成本
|
||||
- 运营成本
|
||||
- 运输成本
|
||||
|
||||
2. 服务最大化
|
||||
- 覆盖范围
|
||||
- 服务质量
|
||||
- 响应时间
|
||||
|
||||
3. 公平性
|
||||
- 服务均等化
|
||||
- 负载均衡
|
||||
|
||||
4. 环境影响
|
||||
- 最小化污染
|
||||
- 保护生态
|
||||
|
||||
处理方法:
|
||||
- 加权求和法 (将多目标转为单目标)
|
||||
- 帕累托优化 (寻找非劣解集)
|
||||
- 约束法 (将部分目标转为约束)
|
||||
- 目标规划 (设定目标满意水平)
|
||||
""")
|
||||
|
||||
# ========================================================================
|
||||
# 5. 收敛过程可视化 (文本)
|
||||
# ========================================================================
|
||||
print("\n[部分 5] 优化过程")
|
||||
print("-" * 50)
|
||||
|
||||
if len(ga_result.convergence_history) > 0:
|
||||
print("\n遗传算法收敛过程:")
|
||||
steps = min(10, len(ga_result.convergence_history))
|
||||
step_size = len(ga_result.convergence_history) // steps
|
||||
|
||||
for i in range(0, len(ga_result.convergence_history), step_size):
|
||||
iteration = i
|
||||
value = -ga_result.convergence_history[i] # 转回正值
|
||||
print(f" 迭代 {iteration:4d}: 覆盖需求量 = {value:.1f}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,957 @@
|
||||
"""
|
||||
空间推理示例 (Spatial Reasoning Example)
|
||||
=======================================
|
||||
|
||||
本示例展示空间智能系统中的空间推理方法。
|
||||
空间推理是从已知空间事实推导新知识的过程。
|
||||
|
||||
核心概念:
|
||||
1. 定性推理 - 使用定性术语描述空间关系
|
||||
2. 定量推理 - 使用精确数值计算
|
||||
3. 空间逻辑 - 形式化的空间推理规则
|
||||
4. 路径规划 - 寻找最优路径
|
||||
5. 可见性分析 - 判断视线可见性
|
||||
|
||||
应用场景:
|
||||
- 导航与路径规划
|
||||
- 空间查询与分析
|
||||
- 地理推理系统
|
||||
- 机器人导航
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
import math
|
||||
import heapq
|
||||
from typing import List, Dict, Tuple, Optional, Set, Any
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
import random
|
||||
|
||||
# 导入空间表征示例中的基础类
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
try:
|
||||
from spatial_representation import Point, LineString, Polygon, Envelope
|
||||
except ImportError:
|
||||
# 如果导入失败,定义简化版本
|
||||
@dataclass
|
||||
class Point:
|
||||
x: float
|
||||
y: float
|
||||
def distance_to(self, other):
|
||||
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
|
||||
def __repr__(self):
|
||||
return f"({self.x:.2f}, {self.y:.2f})"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 定性空间推理
|
||||
# ============================================================================
|
||||
|
||||
class CardinalDirection(Enum):
|
||||
"""基本方向"""
|
||||
NORTH = "N"
|
||||
SOUTH = "S"
|
||||
EAST = "E"
|
||||
WEST = "W"
|
||||
NORTHEAST = "NE"
|
||||
NORTHWEST = "NW"
|
||||
SOUTHEAST = "SE"
|
||||
SOUTHWEST = "SW"
|
||||
|
||||
@classmethod
|
||||
def from_angle(cls, angle: float) -> 'CardinalDirection':
|
||||
"""
|
||||
从角度获取方向
|
||||
|
||||
Args:
|
||||
angle: 角度 (度, 0=东, 90=北)
|
||||
|
||||
Returns:
|
||||
方向枚举
|
||||
"""
|
||||
# 归一化到0-360
|
||||
angle = angle % 360
|
||||
|
||||
if angle >= 337.5 or angle < 22.5:
|
||||
return cls.EAST
|
||||
elif 22.5 <= angle < 67.5:
|
||||
return cls.NORTHEAST
|
||||
elif 67.5 <= angle < 112.5:
|
||||
return cls.NORTH
|
||||
elif 112.5 <= angle < 157.5:
|
||||
return cls.NORTHWEST
|
||||
elif 157.5 <= angle < 202.5:
|
||||
return cls.WEST
|
||||
elif 202.5 <= angle < 247.5:
|
||||
return cls.SOUTHWEST
|
||||
elif 247.5 <= angle < 292.5:
|
||||
return cls.SOUTH
|
||||
else: # 292.5 <= angle < 337.5
|
||||
return cls.SOUTHEAST
|
||||
|
||||
|
||||
@dataclass
|
||||
class QualitativeRelation:
|
||||
"""定性空间关系"""
|
||||
relation_type: str # "direction", "distance", "topology"
|
||||
value: str # 如 "north", "near", "inside"
|
||||
confidence: float = 1.0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.relation_type}={self.value} (conf={self.confidence:.2f})"
|
||||
|
||||
|
||||
class QualitativeReasoner:
|
||||
"""
|
||||
定性空间推理器
|
||||
|
||||
使用定性术语进行空间推理。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.facts: List[Tuple[str, str, QualitativeRelation]] = []
|
||||
|
||||
def add_fact(self, entity1: str, entity2: str,
|
||||
relation: QualitativeRelation) -> None:
|
||||
"""添加空间事实"""
|
||||
self.facts.append((entity1, entity2, relation))
|
||||
|
||||
def infer_direction(self, from_point: Point, to_point: Point) -> QualitativeRelation:
|
||||
"""
|
||||
推断两点之间的方向关系
|
||||
|
||||
Args:
|
||||
from_point: 起始点
|
||||
to_point: 目标点
|
||||
|
||||
Returns:
|
||||
方向关系
|
||||
"""
|
||||
dx = to_point.x - from_point.x
|
||||
dy = to_point.y - from_point.y
|
||||
|
||||
# 计算角度 (从东开始逆时针)
|
||||
angle = math.degrees(math.atan2(dy, dx))
|
||||
|
||||
direction = CardinalDirection.from_angle(angle)
|
||||
|
||||
return QualitativeRelation(
|
||||
relation_type="direction",
|
||||
value=direction.value,
|
||||
confidence=1.0
|
||||
)
|
||||
|
||||
def infer_distance_category(self, p1: Point, p2: Point,
|
||||
thresholds: Dict[str, float] = None) -> QualitativeRelation:
|
||||
"""
|
||||
推断距离类别
|
||||
|
||||
Args:
|
||||
p1: 第一个点
|
||||
p2: 第二个点
|
||||
thresholds: 距离阈值字典
|
||||
|
||||
Returns:
|
||||
距离类别关系
|
||||
"""
|
||||
if thresholds is None:
|
||||
thresholds = {
|
||||
"very_close": 100,
|
||||
"close": 500,
|
||||
"moderate": 1000,
|
||||
"far": 5000
|
||||
}
|
||||
|
||||
dist = p1.distance_to(p2)
|
||||
|
||||
if dist < thresholds.get("very_close", 100):
|
||||
category = "very_close"
|
||||
elif dist < thresholds.get("close", 500):
|
||||
category = "close"
|
||||
elif dist < thresholds.get("moderate", 1000):
|
||||
category = "moderate"
|
||||
else:
|
||||
category = "far"
|
||||
|
||||
return QualitativeRelation(
|
||||
relation_type="distance",
|
||||
value=category,
|
||||
confidence=1.0
|
||||
)
|
||||
|
||||
def compose_relations(self, rel1: QualitativeRelation,
|
||||
rel2: QualitativeRelation) -> QualitativeRelation:
|
||||
"""
|
||||
组合两个关系
|
||||
|
||||
例如: A在B的北边,B在C的东边 -> A在C的东北边
|
||||
"""
|
||||
if rel1.relation_type == "direction" and rel2.relation_type == "direction":
|
||||
# 方向组合
|
||||
return self._compose_directions(rel1.value, rel2.value)
|
||||
|
||||
return QualitativeRelation(
|
||||
relation_type="unknown",
|
||||
value="unknown",
|
||||
confidence=0.5
|
||||
)
|
||||
|
||||
def _compose_directions(self, dir1: str, dir2: str) -> QualitativeRelation:
|
||||
"""组合两个方向"""
|
||||
direction_map = {
|
||||
"N": (0, 1), "S": (0, -1), "E": (1, 0), "W": (-1, 0),
|
||||
"NE": (1, 1), "NW": (-1, 1), "SE": (1, -1), "SW": (-1, -1)
|
||||
}
|
||||
|
||||
if dir1 in direction_map and dir2 in direction_map:
|
||||
v1 = direction_map[dir1]
|
||||
v2 = direction_map[dir2]
|
||||
|
||||
# 向量相加
|
||||
result = (v1[0] + v2[0], v1[1] + v2[1])
|
||||
|
||||
# 找到最接近的方向
|
||||
best_dir = "unknown"
|
||||
best_dot = -1
|
||||
|
||||
for name, vec in direction_map.items():
|
||||
dot = result[0] * vec[0] + result[1] * vec[1]
|
||||
if dot > best_dot:
|
||||
best_dot = dot
|
||||
best_dir = name
|
||||
|
||||
return QualitativeRelation(
|
||||
relation_type="direction",
|
||||
value=best_dir,
|
||||
confidence=0.8
|
||||
)
|
||||
|
||||
return QualitativeRelation("direction", "unknown", 0.3)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 路径规划
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class Node:
|
||||
"""图节点"""
|
||||
id: str
|
||||
point: Point
|
||||
neighbors: List[str] = field(default_factory=list)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.id)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Edge:
|
||||
"""图边"""
|
||||
from_node: str
|
||||
to_node: str
|
||||
weight: float # 权重 (如距离)
|
||||
bidirectional: bool = True
|
||||
|
||||
|
||||
class SpatialGraph:
|
||||
"""
|
||||
空间图
|
||||
|
||||
用于路径规划的空间网络结构。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.nodes: Dict[str, Node] = {}
|
||||
self.edges: List[Edge] = []
|
||||
|
||||
def add_node(self, id: str, point: Point) -> Node:
|
||||
"""添加节点"""
|
||||
node = Node(id=id, point=point)
|
||||
self.nodes[id] = node
|
||||
return node
|
||||
|
||||
def add_edge(self, from_id: str, to_id: str, weight: float = None,
|
||||
bidirectional: bool = True) -> None:
|
||||
"""添加边"""
|
||||
if from_id not in self.nodes or to_id not in self.nodes:
|
||||
raise ValueError("节点不存在")
|
||||
|
||||
# 如果未指定权重,使用欧氏距离
|
||||
if weight is None:
|
||||
weight = self.nodes[from_id].point.distance_to(self.nodes[to_id].point)
|
||||
|
||||
edge = Edge(from_id, to_id, weight, bidirectional)
|
||||
self.edges.append(edge)
|
||||
|
||||
# 更新邻接关系
|
||||
self.nodes[from_id].neighbors.append(to_id)
|
||||
if bidirectional:
|
||||
self.nodes[to_id].neighbors.append(from_id)
|
||||
|
||||
def get_edge_weight(self, from_id: str, to_id: str) -> float:
|
||||
"""获取边的权重"""
|
||||
for edge in self.edges:
|
||||
if edge.from_node == from_id and edge.to_node == to_id:
|
||||
return edge.weight
|
||||
if edge.bidirectional and edge.from_node == to_id and edge.to_node == from_id:
|
||||
return edge.weight
|
||||
return float('inf')
|
||||
|
||||
def shortest_path(self, start_id: str, end_id: str) -> Optional[List[str]]:
|
||||
"""
|
||||
使用Dijkstra算法计算最短路径
|
||||
|
||||
Args:
|
||||
start_id: 起始节点ID
|
||||
end_id: 目标节点ID
|
||||
|
||||
Returns:
|
||||
节点ID列表,表示路径
|
||||
"""
|
||||
if start_id not in self.nodes or end_id not in self.nodes:
|
||||
return None
|
||||
|
||||
# 优先队列: (距离, 节点ID)
|
||||
pq = [(0, start_id)]
|
||||
# 距离字典
|
||||
distances = {node_id: float('inf') for node_id in self.nodes}
|
||||
distances[start_id] = 0
|
||||
# 前驱节点
|
||||
previous = {start_id: None}
|
||||
# 已访问
|
||||
visited = set()
|
||||
|
||||
while pq:
|
||||
current_dist, current_id = heapq.heappop(pq)
|
||||
|
||||
if current_id in visited:
|
||||
continue
|
||||
visited.add(current_id)
|
||||
|
||||
if current_id == end_id:
|
||||
break
|
||||
|
||||
# 检查所有邻居
|
||||
for neighbor_id in self.nodes[current_id].neighbors:
|
||||
if neighbor_id in visited:
|
||||
continue
|
||||
|
||||
edge_weight = self.get_edge_weight(current_id, neighbor_id)
|
||||
new_dist = current_dist + edge_weight
|
||||
|
||||
if new_dist < distances[neighbor_id]:
|
||||
distances[neighbor_id] = new_dist
|
||||
previous[neighbor_id] = current_id
|
||||
heapq.heappush(pq, (new_dist, neighbor_id))
|
||||
|
||||
# 重建路径
|
||||
if distances[end_id] == float('inf'):
|
||||
return None
|
||||
|
||||
path = []
|
||||
current = end_id
|
||||
while current is not None:
|
||||
path.append(current)
|
||||
current = previous.get(current)
|
||||
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
def shortest_path_distance(self, start_id: str, end_id: str) -> float:
|
||||
"""获取最短路径距离"""
|
||||
path = self.shortest_path(start_id, end_id)
|
||||
if not path:
|
||||
return float('inf')
|
||||
|
||||
total = 0.0
|
||||
for i in range(len(path) - 1):
|
||||
total += self.get_edge_weight(path[i], path[i + 1])
|
||||
|
||||
return total
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# A*路径规划
|
||||
# ============================================================================
|
||||
|
||||
class AStarPlanner:
|
||||
"""
|
||||
A*路径规划器
|
||||
|
||||
使用启发式搜索的高效路径规划算法。
|
||||
"""
|
||||
|
||||
def __init__(self, graph: SpatialGraph):
|
||||
self.graph = graph
|
||||
|
||||
def heuristic(self, node_id: str, goal_id: str) -> float:
|
||||
"""
|
||||
启发式函数 (使用欧氏距离)
|
||||
|
||||
Args:
|
||||
node_id: 当前节点
|
||||
goal_id: 目标节点
|
||||
|
||||
Returns:
|
||||
启发式估计值
|
||||
"""
|
||||
if node_id not in self.graph.nodes or goal_id not in self.graph.nodes:
|
||||
return 0.0
|
||||
|
||||
return self.graph.nodes[node_id].point.distance_to(
|
||||
self.graph.nodes[goal_id].point
|
||||
)
|
||||
|
||||
def plan(self, start_id: str, goal_id: str) -> Optional[Tuple[List[str], float]]:
|
||||
"""
|
||||
规划路径
|
||||
|
||||
Args:
|
||||
start_id: 起始节点ID
|
||||
goal_id: 目标节点ID
|
||||
|
||||
Returns:
|
||||
(路径节点列表, 总距离) 或 None
|
||||
"""
|
||||
if start_id not in self.graph.nodes or goal_id not in self.graph.nodes:
|
||||
return None
|
||||
|
||||
# 开集和闭集
|
||||
open_set = {start_id}
|
||||
closed_set = set()
|
||||
|
||||
# g值: 从起点到当前节点的实际距离
|
||||
g_score = {node_id: float('inf') for node_id in self.graph.nodes}
|
||||
g_score[start_id] = 0
|
||||
|
||||
# f值: g值 + 启发式值
|
||||
f_score = {node_id: float('inf') for node_id in self.graph.nodes}
|
||||
f_score[start_id] = self.heuristic(start_id, goal_id)
|
||||
|
||||
# 前驱节点
|
||||
came_from = {}
|
||||
|
||||
while open_set:
|
||||
# 获取f值最小的节点
|
||||
current = min(open_set, key=lambda x: f_score[x])
|
||||
|
||||
if current == goal_id:
|
||||
# 重建路径
|
||||
path = [current]
|
||||
total_distance = g_score[current]
|
||||
|
||||
while current in came_from:
|
||||
current = came_from[current]
|
||||
path.append(current)
|
||||
|
||||
path.reverse()
|
||||
return (path, total_distance)
|
||||
|
||||
open_set.remove(current)
|
||||
closed_set.add(current)
|
||||
|
||||
# 检查邻居
|
||||
for neighbor in self.graph.nodes[current].neighbors:
|
||||
if neighbor in closed_set:
|
||||
continue
|
||||
|
||||
# 计算 tentative_g_score
|
||||
edge_weight = self.graph.get_edge_weight(current, neighbor)
|
||||
tentative_g = g_score[current] + edge_weight
|
||||
|
||||
if neighbor not in open_set:
|
||||
open_set.add(neighbor)
|
||||
elif tentative_g >= g_score[neighbor]:
|
||||
continue
|
||||
|
||||
# 更新
|
||||
came_from[neighbor] = current
|
||||
g_score[neighbor] = tentative_g
|
||||
f_score[neighbor] = tentative_g + self.heuristic(neighbor, goal_id)
|
||||
|
||||
return None # 没有找到路径
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 可见性分析
|
||||
# ============================================================================
|
||||
|
||||
class VisibilityAnalyzer:
|
||||
"""
|
||||
可见性分析器
|
||||
|
||||
判断点之间的可见性,考虑障碍物。
|
||||
"""
|
||||
|
||||
def __init__(self, obstacles: List[Polygon] = None):
|
||||
"""
|
||||
初始化可见性分析器
|
||||
|
||||
Args:
|
||||
obstacles: 障碍物多边形列表
|
||||
"""
|
||||
self.obstacles = obstacles or []
|
||||
|
||||
def add_obstacle(self, obstacle: Polygon) -> None:
|
||||
"""添加障碍物"""
|
||||
self.obstacles.append(obstacle)
|
||||
|
||||
def is_visible(self, p1: Point, p2: Point,
|
||||
tolerance: float = 1e-6) -> bool:
|
||||
"""
|
||||
判断两点之间是否可见
|
||||
|
||||
Args:
|
||||
p1: 第一个点
|
||||
p2: 第二个点
|
||||
tolerance: 容差
|
||||
|
||||
Returns:
|
||||
是否可见
|
||||
"""
|
||||
# 检视线是否与任何障碍物相交
|
||||
for obstacle in self.obstacles:
|
||||
if self._line_intersects_polygon(p1, p2, obstacle):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _line_intersects_polygon(self, p1: Point, p2: Point,
|
||||
polygon: Polygon) -> bool:
|
||||
"""判断线段是否与多边形相交"""
|
||||
# 首先检查包围盒
|
||||
line_min_x = min(p1.x, p2.x)
|
||||
line_max_x = max(p1.x, p2.x)
|
||||
line_min_y = min(p1.y, p2.y)
|
||||
line_max_y = max(p1.y, p2.y)
|
||||
|
||||
poly_min_x = min(p.x for p in polygon.exterior)
|
||||
poly_max_x = max(p.x for p in polygon.exterior)
|
||||
poly_min_y = min(p.y for p in polygon.exterior)
|
||||
poly_max_y = max(p.y for p in polygon.exterior)
|
||||
|
||||
# 包围盒不相交
|
||||
if line_max_x < poly_min_x or line_min_x > poly_max_x or \
|
||||
line_max_y < poly_min_y or line_min_y > poly_max_y:
|
||||
return False
|
||||
|
||||
# 检查线段是否与多边形的任何边相交
|
||||
n = len(polygon.exterior)
|
||||
for i in range(n):
|
||||
v1 = polygon.exterior[i]
|
||||
v2 = polygon.exterior[(i + 1) % n]
|
||||
|
||||
if self._segments_intersect(p1, p2, v1, v2):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _segments_intersect(self, p1: Point, p2: Point,
|
||||
p3: Point, p4: Point) -> bool:
|
||||
"""判断两条线段是否相交"""
|
||||
def orientation(a, b, c):
|
||||
val = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y)
|
||||
if abs(val) < 1e-10:
|
||||
return 0 # 共线
|
||||
return 1 if val > 0 else 2 # 顺时针/逆时针
|
||||
|
||||
def on_segment(a, b, c):
|
||||
return min(a.x, c.x) <= b.x <= max(a.x, c.x) and \
|
||||
min(a.y, c.y) <= b.y <= max(a.y, c.y)
|
||||
|
||||
o1 = orientation(p1, p2, p3)
|
||||
o2 = orientation(p1, p2, p4)
|
||||
o3 = orientation(p3, p4, p1)
|
||||
o4 = orientation(p3, p4, p2)
|
||||
|
||||
# 一般情况
|
||||
if o1 != o2 and o3 != o4:
|
||||
return True
|
||||
|
||||
# 特殊情况
|
||||
if o1 == 0 and on_segment(p1, p3, p2):
|
||||
return True
|
||||
if o2 == 0 and on_segment(p1, p4, p2):
|
||||
return True
|
||||
if o3 == 0 and on_segment(p3, p1, p4):
|
||||
return True
|
||||
if o4 == 0 and on_segment(p3, p2, p4):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def viewshed(self, observer: Point, radius: float,
|
||||
num_rays: int = 360) -> List[Tuple[Point, bool]]:
|
||||
"""
|
||||
计算视域 (可视范围)
|
||||
|
||||
Args:
|
||||
observer: 观察点
|
||||
radius: 视距
|
||||
num_rays: 射线数量
|
||||
|
||||
Returns:
|
||||
(点, 可见性) 列表
|
||||
"""
|
||||
results = []
|
||||
|
||||
for i in range(num_rays):
|
||||
angle = 2 * math.pi * i / num_rays
|
||||
target = Point(
|
||||
observer.x + radius * math.cos(angle),
|
||||
observer.y + radius * math.sin(angle)
|
||||
)
|
||||
|
||||
visible = self.is_visible(observer, target)
|
||||
results.append((target, visible))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间推理引擎
|
||||
# ============================================================================
|
||||
|
||||
class SpatialReasoningEngine:
|
||||
"""
|
||||
空间推理引擎
|
||||
|
||||
集成多种空间推理功能的综合引擎。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.qualitative_reasoner = QualitativeReasoner()
|
||||
self.graph = SpatialGraph()
|
||||
self.visibility_analyzer = VisibilityAnalyzer()
|
||||
self.astar_planner = None
|
||||
|
||||
def build_road_network(self, points: List[Tuple[str, Point]],
|
||||
connections: List[Tuple[str, str]]) -> None:
|
||||
"""
|
||||
构建道路网络
|
||||
|
||||
Args:
|
||||
points: (节点ID, 点) 列表
|
||||
connections: (节点1, 节点2) 连接列表
|
||||
"""
|
||||
for id, point in points:
|
||||
self.graph.add_node(id, point)
|
||||
|
||||
for id1, id2 in connections:
|
||||
self.graph.add_edge(id1, id2)
|
||||
|
||||
self.astar_planner = AStarPlanner(self.graph)
|
||||
|
||||
def navigate(self, start: str, goal: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
导航规划
|
||||
|
||||
Args:
|
||||
start: 起始点ID
|
||||
goal: 目标点ID
|
||||
|
||||
Returns:
|
||||
导航结果字典
|
||||
"""
|
||||
if not self.astar_planner:
|
||||
return None
|
||||
|
||||
result = self.astar_planner.plan(start, goal)
|
||||
|
||||
if result:
|
||||
path, distance = result
|
||||
|
||||
# 计算方向指示
|
||||
directions = []
|
||||
for i in range(len(path) - 1):
|
||||
from_node = self.graph.nodes[path[i]]
|
||||
to_node = self.graph.nodes[path[i + 1]]
|
||||
|
||||
relation = self.qualitative_reasoner.infer_direction(
|
||||
from_node.point, to_node.point
|
||||
)
|
||||
directions.append({
|
||||
"from": path[i],
|
||||
"to": path[i + 1],
|
||||
"direction": relation.value,
|
||||
"distance": from_node.point.distance_to(to_node.point)
|
||||
})
|
||||
|
||||
return {
|
||||
"path": path,
|
||||
"total_distance": distance,
|
||||
"num_steps": len(path) - 1,
|
||||
"directions": directions
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def query_relation(self, entity1: str, entity2: str,
|
||||
point1: Point, point2: Point) -> Dict[str, Any]:
|
||||
"""
|
||||
查询两个实体之间的空间关系
|
||||
|
||||
Args:
|
||||
entity1: 实体1名称
|
||||
entity2: 实体2名称
|
||||
point1: 实体1位置
|
||||
point2: 实体2位置
|
||||
|
||||
Returns:
|
||||
关系字典
|
||||
"""
|
||||
direction = self.qualitative_reasoner.infer_direction(point1, point2)
|
||||
distance_cat = self.qualitative_reasoner.infer_distance_category(point1, point2)
|
||||
actual_distance = point1.distance_to(point2)
|
||||
|
||||
return {
|
||||
"entity1": entity1,
|
||||
"entity2": entity2,
|
||||
"direction": direction.value,
|
||||
"distance_category": distance_cat.value,
|
||||
"actual_distance": actual_distance,
|
||||
"bearing": math.degrees(math.atan2(
|
||||
point2.y - point1.y,
|
||||
point2.x - point1.x
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ========================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示空间推理的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("空间推理示例演示")
|
||||
print("="*70)
|
||||
|
||||
# ========================================================================
|
||||
# 1. 定性空间推理
|
||||
# ========================================================================
|
||||
print("\n[部分 1] 定性空间推理")
|
||||
print("-" * 50)
|
||||
|
||||
reasoner = QualitativeReasoner()
|
||||
|
||||
# 创建几个地标点
|
||||
landmarks = {
|
||||
"西湖": Point(120.148, 30.259),
|
||||
"钱塘江": Point(120.250, 30.200),
|
||||
"滨江": Point(120.350, 30.220),
|
||||
"萧山": Point(120.400, 30.150)
|
||||
}
|
||||
|
||||
print("\n地标位置:")
|
||||
for name, point in landmarks.items():
|
||||
print(f" {name}: {point}")
|
||||
|
||||
# 推断方向关系
|
||||
print("\n方向关系:")
|
||||
for i, (name1, point1) in enumerate(list(landmarks.items())[:-1]):
|
||||
name2 = list(landmarks.keys())[i + 1]
|
||||
point2 = landmarks[name2]
|
||||
|
||||
relation = reasoner.infer_direction(point1, point2)
|
||||
dist_relation = reasoner.infer_distance_category(point1, point2)
|
||||
|
||||
print(f" {name1} -> {name2}:")
|
||||
print(f" 方向: {relation.value}")
|
||||
print(f" 距离类别: {dist_relation.value}")
|
||||
|
||||
# ========================================================================
|
||||
# 2. 路径规划
|
||||
# ========================================================================
|
||||
print("\n\n[部分 2] 路径规划")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建道路网络
|
||||
network_points = [
|
||||
("A", Point(0, 0)),
|
||||
("B", Point(50, 30)),
|
||||
("C", Point(100, 50)),
|
||||
("D", Point(30, 80)),
|
||||
("E", Point(80, 100)),
|
||||
("F", Point(120, 120)),
|
||||
("G", Point(150, 60))
|
||||
]
|
||||
|
||||
connections = [
|
||||
("A", "B"), ("B", "C"), ("A", "D"),
|
||||
("B", "D"), ("D", "E"), ("C", "E"),
|
||||
("E", "F"), ("C", "G"), ("G", "F")
|
||||
]
|
||||
|
||||
graph = SpatialGraph()
|
||||
for id, point in network_points:
|
||||
graph.add_node(id, point)
|
||||
for id1, id2 in connections:
|
||||
graph.add_edge(id1, id2)
|
||||
|
||||
print("\n道路网络:")
|
||||
print(f" 节点数: {len(graph.nodes)}")
|
||||
print(f" 边数: {len(graph.edges)}")
|
||||
|
||||
# Dijkstra最短路径
|
||||
print("\nDijkstra最短路径 (A -> F):")
|
||||
dijkstra_path = graph.shortest_path("A", "F")
|
||||
if dijkstra_path:
|
||||
print(f" 路径: {' -> '.join(dijkstra_path)}")
|
||||
distance = graph.shortest_path_distance("A", "F")
|
||||
print(f" 总距离: {distance:.2f}")
|
||||
|
||||
# A*路径规划
|
||||
print("\nA*路径规划 (A -> F):")
|
||||
astar = AStarPlanner(graph)
|
||||
astar_result = astar.plan("A", "F")
|
||||
if astar_result:
|
||||
path, dist = astar_result
|
||||
print(f" 路径: {' -> '.join(path)}")
|
||||
print(f" 总距离: {dist:.2f}")
|
||||
|
||||
# ========================================================================
|
||||
# 3. 可见性分析
|
||||
# ========================================================================
|
||||
print("\n\n[部分 3] 可见性分析")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建障碍物
|
||||
obstacle1 = Polygon(exterior=[
|
||||
Point(60, 40),
|
||||
Point(80, 40),
|
||||
Point(80, 70),
|
||||
Point(60, 70),
|
||||
Point(60, 40)
|
||||
])
|
||||
|
||||
obstacle2 = Polygon(exterior=[
|
||||
Point(100, 80),
|
||||
Point(130, 80),
|
||||
Point(130, 110),
|
||||
Point(100, 110),
|
||||
Point(100, 80)
|
||||
])
|
||||
|
||||
visibility = VisibilityAnalyzer([obstacle1, obstacle2])
|
||||
|
||||
print("\n障碍物:")
|
||||
print(f" 障碍物1: {obstacle1}")
|
||||
print(f" 障碍物2: {obstacle2}")
|
||||
|
||||
# 测试可见性
|
||||
observer = Point(30, 50)
|
||||
test_points = [
|
||||
("目标A", Point(90, 50)), # 被障碍物1遮挡
|
||||
("目标B", Point(120, 120)), # 被障碍物2遮挡
|
||||
("目标C", Point(150, 30)), # 可见
|
||||
("目标D", Point(50, 90)) # 可见
|
||||
]
|
||||
|
||||
print(f"\n从观察点 {observer} 观察:")
|
||||
for name, target in test_points:
|
||||
visible = visibility.is_visible(observer, target)
|
||||
status = "可见" if visible else "不可见"
|
||||
print(f" {name} {target}: {status}")
|
||||
|
||||
# ========================================================================
|
||||
# 4. 综合推理引擎
|
||||
# ========================================================================
|
||||
print("\n\n[部分 4] 综合空间推理引擎")
|
||||
print("-" * 50)
|
||||
|
||||
engine = SpatialReasoningEngine()
|
||||
|
||||
# 构建城市路网
|
||||
city_points = [
|
||||
("火车站", Point(100, 100)),
|
||||
("市政府", Point(150, 120)),
|
||||
("西湖", Point(200, 100)),
|
||||
("钱江新城", Point(180, 180)),
|
||||
("滨江", Point(120, 200)),
|
||||
("萧山机场", Point(250, 250))
|
||||
]
|
||||
|
||||
city_connections = [
|
||||
("火车站", "市政府"),
|
||||
("火车站", "滨江"),
|
||||
("市政府", "西湖"),
|
||||
("市政府", "钱江新城"),
|
||||
("滨江", "钱江新城"),
|
||||
("钱江新城", "萧山机场"),
|
||||
("西湖", "萧山机场")
|
||||
]
|
||||
|
||||
engine.build_road_network(city_points, city_connections)
|
||||
|
||||
print("\n城市路网:")
|
||||
for name, point in city_points:
|
||||
print(f" {name}: {point}")
|
||||
|
||||
# 导航示例
|
||||
print("\n导航示例: 从 火车站 到 萧山机场")
|
||||
nav_result = engine.navigate("火车站", "萧山机场")
|
||||
|
||||
if nav_result:
|
||||
print(f"\n路径规划结果:")
|
||||
print(f" 路径: {' -> '.join(nav_result['path'])}")
|
||||
print(f" 总距离: {nav_result['total_distance']:.2f}")
|
||||
print(f" 步数: {nav_result['num_steps']}")
|
||||
|
||||
print(f"\n详细指引:")
|
||||
for i, step in enumerate(nav_result['directions'], 1):
|
||||
dir_map = {
|
||||
"N": "向北", "S": "向南", "E": "向东", "W": "向西",
|
||||
"NE": "向东北", "NW": "向西北", "SE": "向东南", "SW": "向西南"
|
||||
}
|
||||
direction_cn = dir_map.get(step['direction'], step['direction'])
|
||||
print(f" {i}. 从 {step['from']} {direction_cn} 前往 {step['to']} "
|
||||
f"(距离: {step['distance']:.1f})")
|
||||
|
||||
# 空间关系查询
|
||||
print("\n空间关系查询:")
|
||||
relation = engine.query_relation(
|
||||
"火车站", "西湖",
|
||||
city_points[0][1], city_points[2][1]
|
||||
)
|
||||
|
||||
print(f" {relation['entity1']} 相对于 {relation['entity2']}:")
|
||||
print(f" 方向: {relation['direction']}")
|
||||
print(f" 距离: {relation['actual_distance']:.2f}")
|
||||
print(f" 方位角: {relation['bearing']:.1f}°")
|
||||
|
||||
# ========================================================================
|
||||
# 5. 多路径比较
|
||||
# ========================================================================
|
||||
print("\n\n[部分 5] 多路径比较")
|
||||
print("-" * 50)
|
||||
|
||||
destinations = ["市政府", "西湖", "钱江新城", "滨江", "萧山机场"]
|
||||
start = "火车站"
|
||||
|
||||
print(f"\n从 {start} 到各目的地的距离:")
|
||||
results = []
|
||||
for dest in destinations:
|
||||
if dest == start:
|
||||
continue
|
||||
path_info = engine.navigate(start, dest)
|
||||
if path_info:
|
||||
results.append((dest, path_info['total_distance'], path_info['path']))
|
||||
|
||||
results.sort(key=lambda x: x[1])
|
||||
|
||||
for i, (dest, dist, path) in enumerate(results, 1):
|
||||
print(f" {i}. {dest:8s}: {dist:6.1f} (路径: {' -> '.join(path)})")
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,982 @@
|
||||
"""
|
||||
空间表征示例 (Spatial Representation Example)
|
||||
============================================
|
||||
|
||||
本示例展示空间智能系统中的空间表征方法。
|
||||
空间表征是对地理空间现象的抽象和建模,是空间推理的基础。
|
||||
|
||||
核心概念:
|
||||
1. 空间对象模型 - 点、线、面等几何对象
|
||||
2. 空间关系模型 - 拓扑、距离、方向关系
|
||||
3. 空间场模型 - 连续表面的表示
|
||||
4. 空间索引结构 - 加速空间查询
|
||||
5. 多尺度表征 - 不同详细程度的表示
|
||||
|
||||
应用场景:
|
||||
- 地理信息系统 (GIS)
|
||||
- 空间数据库
|
||||
- 空间推理引擎
|
||||
- 地理可视化
|
||||
|
||||
作者: CC4SI 项目组
|
||||
"""
|
||||
|
||||
import math
|
||||
import json
|
||||
from typing import List, Dict, Tuple, Optional, Any, Set
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
import random
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 几何类型与空间对象
|
||||
# ============================================================================
|
||||
|
||||
class GeometryType(Enum):
|
||||
"""几何类型枚举"""
|
||||
POINT = "Point"
|
||||
LINESTRING = "LineString"
|
||||
POLYGON = "Polygon"
|
||||
MULTIPOINT = "MultiPoint"
|
||||
MULTILINESTRING = "MultiLineString"
|
||||
MULTIPOLYGON = "MultiPolygon"
|
||||
GEOMETRYCOLLECTION = "GeometryCollection"
|
||||
|
||||
|
||||
class SpatialReference(Enum):
|
||||
"""空间参考系统"""
|
||||
WGS84 = "EPSG:4326" # 经纬度
|
||||
WEB_MERCATOR = "EPSG:3857" # Web墨卡托
|
||||
CGCS2000 = "EPSG:4490" # 中国大地坐标系统
|
||||
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
"""点几何对象"""
|
||||
x: float
|
||||
y: float
|
||||
srid: int = 4326 # 空间参考ID
|
||||
|
||||
def __iter__(self):
|
||||
"""支持解包"""
|
||||
return iter((self.x, self.y))
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.x, self.y))
|
||||
|
||||
def to_tuple(self) -> Tuple[float, float]:
|
||||
"""转换为元组"""
|
||||
return (self.x, self.y)
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON"""
|
||||
return {
|
||||
"type": "Point",
|
||||
"coordinates": [self.x, self.y]
|
||||
}
|
||||
|
||||
def distance_to(self, other: 'Point') -> float:
|
||||
"""计算到另一个点的欧氏距离"""
|
||||
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Point({self.x:.4f}, {self.y:.4f})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LineString:
|
||||
"""线几何对象"""
|
||||
coordinates: List[Point]
|
||||
srid: int = 4326
|
||||
|
||||
@property
|
||||
def length(self) -> float:
|
||||
"""计算线的长度"""
|
||||
if len(self.coordinates) < 2:
|
||||
return 0.0
|
||||
total = 0.0
|
||||
for i in range(len(self.coordinates) - 1):
|
||||
total += self.coordinates[i].distance_to(self.coordinates[i + 1])
|
||||
return total
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON"""
|
||||
return {
|
||||
"type": "LineString",
|
||||
"coordinates": [[p.x, p.y] for p in self.coordinates]
|
||||
}
|
||||
|
||||
def get_point_at(self, ratio: float) -> Point:
|
||||
"""
|
||||
获取线上指定比例位置的点
|
||||
|
||||
Args:
|
||||
ratio: 0到1之间的比例值
|
||||
|
||||
Returns:
|
||||
该位置的点
|
||||
"""
|
||||
if ratio <= 0:
|
||||
return self.coordinates[0]
|
||||
if ratio >= 1:
|
||||
return self.coordinates[-1]
|
||||
|
||||
target_length = self.length * ratio
|
||||
accumulated = 0.0
|
||||
|
||||
for i in range(len(self.coordinates) - 1):
|
||||
segment_length = self.coordinates[i].distance_to(self.coordinates[i + 1])
|
||||
if accumulated + segment_length >= target_length:
|
||||
# 在此段上
|
||||
segment_ratio = (target_length - accumulated) / segment_length
|
||||
p1 = self.coordinates[i]
|
||||
p2 = self.coordinates[i + 1]
|
||||
return Point(
|
||||
p1.x + (p2.x - p1.x) * segment_ratio,
|
||||
p1.y + (p2.y - p1.y) * segment_ratio
|
||||
)
|
||||
accumulated += segment_length
|
||||
|
||||
return self.coordinates[-1]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"LineString({len(self.coordinates)} points)"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Polygon:
|
||||
"""面几何对象"""
|
||||
exterior: List[Point] # 外环
|
||||
interiors: List[List[Point]] = field(default_factory=list) # 内环(空洞)
|
||||
srid: int = 4326
|
||||
|
||||
@property
|
||||
def area(self) -> float:
|
||||
"""使用鞋带公式计算多边形面积"""
|
||||
return self._ring_area(self.exterior) - sum(
|
||||
self._ring_area(interior) for interior in self.interiors
|
||||
)
|
||||
|
||||
def _ring_area(self, ring: List[Point]) -> float:
|
||||
"""计算环的面积(绝对值)"""
|
||||
if len(ring) < 3:
|
||||
return 0.0
|
||||
area = 0.0
|
||||
for i in range(len(ring)):
|
||||
j = (i + 1) % len(ring)
|
||||
area += ring[i].x * ring[j].y
|
||||
area -= ring[j].x * ring[i].y
|
||||
return abs(area) / 2
|
||||
|
||||
@property
|
||||
def centroid(self) -> Point:
|
||||
"""计算多边形质心"""
|
||||
if len(self.exterior) < 3:
|
||||
return self.exterior[0] if self.exterior else Point(0, 0)
|
||||
|
||||
# 简化:使用外环的顶点平均值
|
||||
avg_x = sum(p.x for p in self.exterior) / len(self.exterior)
|
||||
avg_y = sum(p.y for p in self.exterior) / len(self.exterior)
|
||||
return Point(avg_x, avg_y)
|
||||
|
||||
def contains_point(self, point: Point) -> bool:
|
||||
"""
|
||||
判断点是否在多边形内(射线法)
|
||||
|
||||
Args:
|
||||
point: 待判断的点
|
||||
|
||||
Returns:
|
||||
是否在多边形内
|
||||
"""
|
||||
return self._point_in_ring(point, self.exterior) and \
|
||||
all(not self._point_in_ring(point, interior)
|
||||
for interior in self.interiors)
|
||||
|
||||
def _point_in_ring(self, point: Point, ring: List[Point]) -> bool:
|
||||
"""射线法判断点是否在环内"""
|
||||
if len(ring) < 3:
|
||||
return False
|
||||
|
||||
x, y = point.x, point.y
|
||||
inside = False
|
||||
|
||||
for i in range(len(ring)):
|
||||
j = (i + 1) % len(ring)
|
||||
xi, yi = ring[i].x, ring[i].y
|
||||
xj, yj = ring[j].x, ring[j].y
|
||||
|
||||
# 检查射线与边的交点
|
||||
if ((yi > y) != (yj > y)) and \
|
||||
(x < (xj - xi) * (y - yi) / (yj - yi + 1e-10) + xi):
|
||||
inside = not inside
|
||||
|
||||
return inside
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON"""
|
||||
coords = [[[p.x, p.y] for p in self.exterior]]
|
||||
coords.extend([[[p.x, p.y] for p in interior] for interior in self.interiors])
|
||||
|
||||
return {
|
||||
"type": "Polygon",
|
||||
"coordinates": coords
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
holes = len(self.interiors)
|
||||
return f"Polygon({len(self.exterior)} vertices{f', {holes} holes' if holes > 0 else ''})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Envelope:
|
||||
"""包围盒"""
|
||||
min_x: float
|
||||
max_x: float
|
||||
min_y: float
|
||||
max_y: float
|
||||
|
||||
@property
|
||||
def width(self) -> float:
|
||||
return self.max_x - self.min_x
|
||||
|
||||
@property
|
||||
def height(self) -> float:
|
||||
return self.max_y - self.min_y
|
||||
|
||||
@property
|
||||
def area(self) -> float:
|
||||
return self.width * self.height
|
||||
|
||||
@property
|
||||
def center(self) -> Point:
|
||||
return Point(
|
||||
(self.min_x + self.max_x) / 2,
|
||||
(self.min_y + self.max_y) / 2
|
||||
)
|
||||
|
||||
def contains(self, point: Point) -> bool:
|
||||
"""判断点是否在包围盒内"""
|
||||
return (self.min_x <= point.x <= self.max_x and
|
||||
self.min_y <= point.y <= self.max_y)
|
||||
|
||||
def intersects(self, other: 'Envelope') -> bool:
|
||||
"""判断是否与另一个包围盒相交"""
|
||||
return not (self.max_x < other.min_x or self.min_x > other.max_x or
|
||||
self.max_y < other.min_y or self.min_y > other.max_y)
|
||||
|
||||
def union(self, other: 'Envelope') -> 'Envelope':
|
||||
"""计算与另一个包围盒的并集"""
|
||||
return Envelope(
|
||||
min_x=min(self.min_x, other.min_x),
|
||||
max_x=max(self.max_x, other.max_x),
|
||||
min_y=min(self.min_y, other.min_y),
|
||||
max_y=max(self.max_y, other.max_y)
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Envelope([{self.min_x:.2f}, {self.min_y:.2f}] -> [{self.max_x:.2f}, {self.max_y:.2f}])"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间特征对象
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class SpatialFeature:
|
||||
"""
|
||||
空间特征
|
||||
|
||||
包含几何和属性信息的完整空间对象。
|
||||
"""
|
||||
id: str
|
||||
geometry: Any # Point, LineString, Polygon等
|
||||
properties: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON Feature"""
|
||||
geom_data = None
|
||||
if isinstance(self.geometry, Point):
|
||||
geom_data = self.geometry.to_geojson()
|
||||
elif isinstance(self.geometry, LineString):
|
||||
geom_data = self.geometry.to_geojson()
|
||||
elif isinstance(self.geometry, Polygon):
|
||||
geom_data = self.geometry.to_geojson()
|
||||
|
||||
return {
|
||||
"type": "Feature",
|
||||
"id": self.id,
|
||||
"geometry": geom_data,
|
||||
"properties": self.properties
|
||||
}
|
||||
|
||||
def to_geojson_collection(self) -> Dict:
|
||||
"""转换为GeoJSON FeatureCollection"""
|
||||
return {
|
||||
"type": "FeatureCollection",
|
||||
"features": [self.to_geojson()]
|
||||
}
|
||||
|
||||
def envelope(self) -> Envelope:
|
||||
"""计算包围盒"""
|
||||
if isinstance(self.geometry, Point):
|
||||
return Envelope(
|
||||
self.geometry.x, self.geometry.x,
|
||||
self.geometry.y, self.geometry.y
|
||||
)
|
||||
elif isinstance(self.geometry, (LineString, list)):
|
||||
coords = self.geometry.coordinates if isinstance(self.geometry, LineString) else self.geometry
|
||||
xs = [p.x for p in coords]
|
||||
ys = [p.y for p in coords]
|
||||
return Envelope(min(xs), max(xs), min(ys), max(ys))
|
||||
elif isinstance(self.geometry, Polygon):
|
||||
xs = [p.x for p in self.geometry.exterior]
|
||||
ys = [p.y for p in self.geometry.exterior]
|
||||
return Envelope(min(xs), max(xs), min(ys), max(ys))
|
||||
else:
|
||||
raise ValueError(f"Unsupported geometry type: {type(self.geometry)}")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SpatialFeature(id={self.id}, geom={type(self.geometry).__name__})"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间关系
|
||||
# ============================================================================
|
||||
|
||||
class SpatialRelation(Enum):
|
||||
"""空间关系类型"""
|
||||
EQUALS = "equals"
|
||||
DISJOINT = "disjoint"
|
||||
INTERSECTS = "intersects"
|
||||
TOUCHES = "touches"
|
||||
CROSSES = "crosses"
|
||||
WITHIN = "within"
|
||||
CONTAINS = "contains"
|
||||
OVERLAPS = "overlaps"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TopologyRelation:
|
||||
"""
|
||||
拓扑关系描述
|
||||
|
||||
基于DE-9IM (Dimensionally Extended nine-Intersection Model)模型。
|
||||
"""
|
||||
relation: SpatialRelation
|
||||
confidence: float = 1.0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"TopologyRelation({self.relation.value}, conf={self.confidence:.2f})"
|
||||
|
||||
|
||||
def calculate_relation(geom1: Any, geom2: Any) -> TopologyRelation:
|
||||
"""
|
||||
计算两个几何对象的空间关系
|
||||
|
||||
Args:
|
||||
geom1: 第一个几何对象
|
||||
geom2: 第二个几何对象
|
||||
|
||||
Returns:
|
||||
拓扑关系
|
||||
"""
|
||||
# 点-点关系
|
||||
if isinstance(geom1, Point) and isinstance(geom2, Point):
|
||||
if geom1.x == geom2.x and geom1.y == geom2.y:
|
||||
return TopologyRelation(SpatialRelation.EQUALS)
|
||||
return TopologyRelation(SpatialRelation.DISJOINT)
|
||||
|
||||
# 点-多边形关系
|
||||
if isinstance(geom1, Point) and isinstance(geom2, Polygon):
|
||||
if geom2.contains_point(geom1):
|
||||
return TopologyRelation(SpatialRelation.WITHIN)
|
||||
# 检查是否在边界上
|
||||
for i in range(len(geom2.exterior)):
|
||||
p1, p2 = geom2.exterior[i], geom2.exterior[(i + 1) % len(geom2.exterior)]
|
||||
if point_on_segment(geom1, p1, p2):
|
||||
return TopologyRelation(SpatialRelation.TOUCHES)
|
||||
return TopologyRelation(SpatialRelation.DISJOINT)
|
||||
|
||||
# 多边形-点关系
|
||||
if isinstance(geom1, Polygon) and isinstance(geom2, Point):
|
||||
relation = calculate_relation(geom2, geom1)
|
||||
if relation.relation == SpatialRelation.WITHIN:
|
||||
return TopologyRelation(SpatialRelation.CONTAINS)
|
||||
return relation
|
||||
|
||||
# 多边形-多边形关系(简化版:只检查相交)
|
||||
if isinstance(geom1, Polygon) and isinstance(geom2, Polygon):
|
||||
env1 = envelope_of_polygon(geom1)
|
||||
env2 = envelope_of_polygon(geom2)
|
||||
|
||||
if not env1.intersects(env2):
|
||||
return TopologyRelation(SpatialRelation.DISJOINT)
|
||||
|
||||
# 简化:检查是否有交点
|
||||
if geom1.contains_point(geom2.exterior[0]):
|
||||
return TopologyRelation(SpatialRelation.CONTAINS)
|
||||
if geom2.contains_point(geom1.exterior[0]):
|
||||
return TopologyRelation(SpatialRelation.WITHIN)
|
||||
|
||||
return TopologyRelation(SpatialRelation.INTERSECTS)
|
||||
|
||||
return TopologyRelation(SpatialRelation.DISJOINT)
|
||||
|
||||
|
||||
def point_on_segment(point: Point, seg_start: Point, seg_end: Point,
|
||||
tolerance: float = 1e-6) -> bool:
|
||||
"""判断点是否在线段上"""
|
||||
# 检查点是否在线段的包围盒内
|
||||
if not (min(seg_start.x, seg_end.x) - tolerance <= point.x <=
|
||||
max(seg_start.x, seg_end.x) + tolerance and
|
||||
min(seg_start.y, seg_end.y) - tolerance <= point.y <=
|
||||
max(seg_start.y, seg_end.y) + tolerance):
|
||||
return False
|
||||
|
||||
# 检查三点共线
|
||||
cross = (seg_end.x - seg_start.x) * (point.y - seg_start.y) - \
|
||||
(seg_end.y - seg_start.y) * (point.x - seg_start.x)
|
||||
return abs(cross) < tolerance
|
||||
|
||||
|
||||
def envelope_of_polygon(polygon: Polygon) -> Envelope:
|
||||
"""计算多边形的包围盒"""
|
||||
xs = [p.x for p in polygon.exterior]
|
||||
ys = [p.y for p in polygon.exterior]
|
||||
return Envelope(min(xs), max(xs), min(ys), max(ys))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间索引
|
||||
# ============================================================================
|
||||
|
||||
class RTreeNode:
|
||||
"""R树节点"""
|
||||
def __init__(self, is_leaf: bool = False):
|
||||
self.is_leaf = is_leaf
|
||||
self.envelope: Optional[Envelope] = None
|
||||
self.children: List['RTreeNode'] = []
|
||||
self.features: List[SpatialFeature] = []
|
||||
|
||||
def update_envelope(self):
|
||||
"""更新节点的包围盒"""
|
||||
if self.is_leaf and self.features:
|
||||
envelopes = [f.envelope() for f in self.features]
|
||||
self.envelope = envelopes[0]
|
||||
for env in envelopes[1:]:
|
||||
self.envelope = self.envelope.union(env)
|
||||
elif not self.is_leaf and self.children:
|
||||
self.envelope = self.children[0].envelope
|
||||
for child in self.children[1:]:
|
||||
if child.envelope:
|
||||
self.envelope = self.envelope.union(child.envelope)
|
||||
|
||||
|
||||
class RTree:
|
||||
"""
|
||||
R树空间索引
|
||||
|
||||
用于加速空间查询的树状索引结构。
|
||||
"""
|
||||
|
||||
def __init__(self, max_children: int = 4):
|
||||
"""
|
||||
初始化R树
|
||||
|
||||
Args:
|
||||
max_children: 每个节点的最大子节点数
|
||||
"""
|
||||
self.max_children = max_children
|
||||
self.root = RTreeNode(is_leaf=True)
|
||||
self.size = 0
|
||||
|
||||
def insert(self, feature: SpatialFeature) -> None:
|
||||
"""插入空间特征"""
|
||||
self._insert(self.root, feature)
|
||||
self.size += 1
|
||||
|
||||
def _insert(self, node: RTreeNode, feature: SpatialFeature) -> None:
|
||||
"""递归插入"""
|
||||
feature_env = feature.envelope()
|
||||
|
||||
if node.is_leaf:
|
||||
node.features.append(feature)
|
||||
node.update_envelope()
|
||||
|
||||
# 如果超过容量,分裂节点
|
||||
if len(node.features) > self.max_children:
|
||||
self._split(node)
|
||||
else:
|
||||
# 选择最佳子节点
|
||||
best_child = self._choose_best_child(node, feature_env)
|
||||
self._insert(best_child, feature)
|
||||
node.update_envelope()
|
||||
|
||||
def _choose_best_child(self, node: RTreeNode, env: Envelope) -> RTreeNode:
|
||||
"""选择插入代价最小的子节点"""
|
||||
best = None
|
||||
best_increase = float('inf')
|
||||
|
||||
for child in node.children:
|
||||
if child.envelope is None:
|
||||
continue
|
||||
union_env = child.envelope.union(env)
|
||||
increase = union_env.area - child.envelope.area
|
||||
|
||||
if increase < best_increase:
|
||||
best_increase = increase
|
||||
best = child
|
||||
|
||||
return best or node.children[0]
|
||||
|
||||
def _split(self, node: RTreeNode) -> None:
|
||||
"""分裂节点 (简化版)"""
|
||||
if node.is_leaf:
|
||||
# 简单分裂:将特征分成两组
|
||||
mid = len(node.features) // 2
|
||||
group1 = node.features[:mid]
|
||||
group2 = node.features[mid:]
|
||||
|
||||
node.features = group1
|
||||
|
||||
new_leaf = RTreeNode(is_leaf=True)
|
||||
new_leaf.features = group2
|
||||
new_leaf.update_envelope()
|
||||
|
||||
# 更新父节点
|
||||
if node == self.root and not node.children:
|
||||
# 根节点分裂
|
||||
new_root = RTreeNode(is_leaf=False)
|
||||
new_root.children = [node, new_leaf]
|
||||
new_root.update_envelope()
|
||||
self.root = new_root
|
||||
else:
|
||||
# 简化:不处理非根节点的分裂
|
||||
pass
|
||||
|
||||
def query(self, envelope: Envelope) -> List[SpatialFeature]:
|
||||
"""查询与包围盒相交的所有特征"""
|
||||
results = []
|
||||
self._query(self.root, envelope, results)
|
||||
return results
|
||||
|
||||
def _query(self, node: RTreeNode, envelope: Envelope,
|
||||
results: List[SpatialFeature]) -> None:
|
||||
"""递归查询"""
|
||||
if node.envelope and not node.envelope.intersects(envelope):
|
||||
return
|
||||
|
||||
if node.is_leaf:
|
||||
for feature in node.features:
|
||||
if feature.envelope().intersects(envelope):
|
||||
results.append(feature)
|
||||
else:
|
||||
for child in node.children:
|
||||
self._query(child, envelope, results)
|
||||
|
||||
def nearest_neighbor(self, point: Point, k: int = 1) -> List[Tuple[SpatialFeature, float]]:
|
||||
"""
|
||||
K近邻查询
|
||||
|
||||
Args:
|
||||
point: 查询点
|
||||
k: 返回的最近邻数量
|
||||
|
||||
Returns:
|
||||
(特征, 距离) 列表
|
||||
"""
|
||||
candidates = []
|
||||
self._collect_candidates(self.root, candidates)
|
||||
|
||||
# 计算距离并排序
|
||||
distances = []
|
||||
for feature in candidates:
|
||||
if isinstance(feature.geometry, Point):
|
||||
dist = feature.geometry.distance_to(point)
|
||||
distances.append((feature, dist))
|
||||
else:
|
||||
# 非点几何:使用包围盒中心距离
|
||||
env = feature.envelope()
|
||||
center = env.center
|
||||
dist = math.sqrt((center.x - point.x)**2 + (center.y - point.y)**2)
|
||||
distances.append((feature, dist))
|
||||
|
||||
distances.sort(key=lambda x: x[1])
|
||||
return distances[:k]
|
||||
|
||||
def _collect_candidates(self, node: RTreeNode, results: List[SpatialFeature]) -> None:
|
||||
"""收集所有候选特征"""
|
||||
if node.is_leaf:
|
||||
results.extend(node.features)
|
||||
else:
|
||||
for child in node.children:
|
||||
self._collect_candidates(child, results)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 空间场模型
|
||||
# ============================================================================
|
||||
|
||||
class GridField:
|
||||
"""
|
||||
栅格场模型
|
||||
|
||||
使用规则网格表示连续空间现象。
|
||||
"""
|
||||
|
||||
def __init__(self, bounds: Envelope, rows: int, cols: int,
|
||||
nodata: float = -9999):
|
||||
"""
|
||||
初始化栅格场
|
||||
|
||||
Args:
|
||||
bounds: 空间范围
|
||||
rows: 行数
|
||||
cols: 列数
|
||||
nodata: 无数据值
|
||||
"""
|
||||
self.bounds = bounds
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.nodata = nodata
|
||||
self.data = [[nodata for _ in range(cols)] for _ in range(rows)]
|
||||
|
||||
@property
|
||||
def cell_width(self) -> float:
|
||||
"""获取单元格宽度"""
|
||||
return self.bounds.width / self.cols
|
||||
|
||||
@property
|
||||
def cell_height(self) -> float:
|
||||
"""获取单元格高度"""
|
||||
return self.bounds.height / self.rows
|
||||
|
||||
def get_cell_index(self, point: Point) -> Optional[Tuple[int, int]]:
|
||||
"""
|
||||
获取点对应的栅格索引
|
||||
|
||||
Args:
|
||||
point: 空间点
|
||||
|
||||
Returns:
|
||||
(行索引, 列索引) 或 None
|
||||
"""
|
||||
if not self.bounds.contains(point):
|
||||
return None
|
||||
|
||||
col = int((point.x - self.bounds.min_x) / self.cell_width)
|
||||
row = int((self.bounds.max_y - point.y) / self.cell_height)
|
||||
|
||||
col = max(0, min(col, self.cols - 1))
|
||||
row = max(0, min(row, self.rows - 1))
|
||||
|
||||
return (row, col)
|
||||
|
||||
def set_value(self, row: int, col: int, value: float) -> None:
|
||||
"""设置栅格值"""
|
||||
if 0 <= row < self.rows and 0 <= col < self.cols:
|
||||
self.data[row][col] = value
|
||||
|
||||
def get_value(self, row: int, col: int) -> float:
|
||||
"""获取栅格值"""
|
||||
if 0 <= row < self.rows and 0 <= col < self.cols:
|
||||
return self.data[row][col]
|
||||
return self.nodata
|
||||
|
||||
def get_value_at_point(self, point: Point) -> float:
|
||||
"""获取点位置的值(最近邻)"""
|
||||
idx = self.get_cell_index(point)
|
||||
if idx:
|
||||
return self.get_value(idx[0], idx[1])
|
||||
return self.nodata
|
||||
|
||||
def interpolate_at_point(self, point: Point) -> float:
|
||||
"""双线性插值获取点位置的值"""
|
||||
idx = self.get_cell_index(point)
|
||||
if not idx:
|
||||
return self.nodata
|
||||
|
||||
row, col = idx
|
||||
|
||||
# 获取四个角点的值
|
||||
values = []
|
||||
for r in range(row, min(row + 2, self.rows)):
|
||||
for c in range(col, min(col + 2, self.cols)):
|
||||
values.append((r, c, self.data[r][c]))
|
||||
|
||||
if len(values) < 4 or any(v[2] == self.nodata for v in values):
|
||||
return self.get_value(row, col) # 回退到最近邻
|
||||
|
||||
# 双线性插值
|
||||
# 简化实现
|
||||
return self.get_value(row, col)
|
||||
|
||||
def get_statistics(self) -> Dict[str, float]:
|
||||
"""获取统计信息"""
|
||||
values = [v for row in self.data for v in row if v != self.nodata]
|
||||
|
||||
if not values:
|
||||
return {"count": 0, "min": self.nodata, "max": self.nodata,
|
||||
"mean": self.nodata, "std": 0}
|
||||
|
||||
import statistics
|
||||
return {
|
||||
"count": len(values),
|
||||
"min": min(values),
|
||||
"max": max(values),
|
||||
"mean": statistics.mean(values),
|
||||
"std": statistics.stdev(values) if len(values) > 1 else 0
|
||||
}
|
||||
|
||||
def to_geojson(self) -> Dict:
|
||||
"""转换为GeoJSON(简化:输出为点集)"""
|
||||
features = []
|
||||
for r in range(self.rows):
|
||||
for c in range(self.cols):
|
||||
val = self.data[r][c]
|
||||
if val != self.nodata:
|
||||
# 计算点坐标
|
||||
x = self.bounds.min_x + (c + 0.5) * self.cell_width
|
||||
y = self.bounds.max_y - (r + 0.5) * self.cell_height
|
||||
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {"type": "Point", "coordinates": [x, y]},
|
||||
"properties": {"value": val}
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "FeatureCollection",
|
||||
"features": features
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""主程序 - 演示空间表征的使用"""
|
||||
|
||||
print("="*70)
|
||||
print("空间表征示例演示")
|
||||
print("="*70)
|
||||
|
||||
# ========================================================================
|
||||
# 1. 基础几何对象
|
||||
# ========================================================================
|
||||
print("\n[部分 1] 基础几何对象")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建点
|
||||
point1 = Point(120.5, 30.2)
|
||||
point2 = Point(121.0, 30.5)
|
||||
print(f"\n点对象:")
|
||||
print(f" point1 = {point1}")
|
||||
print(f" point2 = {point2}")
|
||||
print(f" 距离 = {point1.distance_to(point2):.4f}")
|
||||
|
||||
# 创建线
|
||||
linestring = LineString([
|
||||
Point(120.0, 30.0),
|
||||
Point(120.5, 30.2),
|
||||
Point(121.0, 30.5),
|
||||
Point(121.5, 30.3)
|
||||
])
|
||||
print(f"\n线对象:")
|
||||
print(f" {linestring}")
|
||||
print(f" 长度 = {linestring.length:.4f}")
|
||||
print(f" 中点 = {linestring.get_point_at(0.5)}")
|
||||
|
||||
# 创建多边形
|
||||
polygon = Polygon(exterior=[
|
||||
Point(120.0, 30.0),
|
||||
Point(121.0, 30.0),
|
||||
Point(121.0, 31.0),
|
||||
Point(120.0, 31.0),
|
||||
Point(120.0, 30.0)
|
||||
])
|
||||
print(f"\n多边形对象:")
|
||||
print(f" {polygon}")
|
||||
print(f" 面积 = {polygon.area:.4f}")
|
||||
print(f" 质心 = {polygon.centroid}")
|
||||
|
||||
# 点包含测试
|
||||
test_inside = Point(120.5, 30.5)
|
||||
test_outside = Point(121.5, 30.5)
|
||||
print(f" {test_inside} 在多边形内: {polygon.contains_point(test_inside)}")
|
||||
print(f" {test_outside} 在多边形内: {polygon.contains_point(test_outside)}")
|
||||
|
||||
# ========================================================================
|
||||
# 2. 空间特征
|
||||
# ========================================================================
|
||||
print("\n\n[部分 2] 空间特征")
|
||||
print("-" * 50)
|
||||
|
||||
feature1 = SpatialFeature(
|
||||
id="poi_001",
|
||||
geometry=Point(120.5, 30.2),
|
||||
properties={
|
||||
"name": "杭州西湖",
|
||||
"type": "scenic_spot",
|
||||
"rating": 4.8
|
||||
}
|
||||
)
|
||||
|
||||
feature2 = SpatialFeature(
|
||||
id="zone_001",
|
||||
geometry=polygon,
|
||||
properties={
|
||||
"name": "开发区A",
|
||||
"type": "industrial_zone",
|
||||
"area_ha": 10000
|
||||
}
|
||||
)
|
||||
|
||||
print(f"\n特征1: {feature1}")
|
||||
print(f" 属性: {feature1.properties}")
|
||||
print(f" 包围盒: {feature1.envelope()}")
|
||||
|
||||
print(f"\n特征2: {feature2}")
|
||||
print(f" 属性: {feature2.properties}")
|
||||
print(f" 包围盒: {feature2.envelope()}")
|
||||
|
||||
# GeoJSON输出
|
||||
print(f"\nGeoJSON输出:")
|
||||
print(json.dumps(feature1.to_geojson(), ensure_ascii=False, indent=2))
|
||||
|
||||
# ========================================================================
|
||||
# 3. 空间关系
|
||||
# ========================================================================
|
||||
print("\n\n[部分 3] 空间关系")
|
||||
print("-" * 50)
|
||||
|
||||
# 点与多边形的关系
|
||||
point_a = Point(120.5, 30.5) # 在多边形内
|
||||
point_b = Point(121.5, 30.5) # 在多边形外
|
||||
point_c = Point(120.0, 30.5) # 在边界上
|
||||
|
||||
print(f"\n点-多边形关系测试:")
|
||||
print(f" {point_a} 与多边形: {calculate_relation(point_a, polygon)}")
|
||||
print(f" {point_b} 与多边形: {calculate_relation(point_b, polygon)}")
|
||||
print(f" {point_c} 与多边形: {calculate_relation(point_c, polygon)}")
|
||||
|
||||
# 多边形-多边形关系
|
||||
poly2 = Polygon(exterior=[
|
||||
Point(120.5, 29.5),
|
||||
Point(121.5, 29.5),
|
||||
Point(121.5, 30.5),
|
||||
Point(120.5, 30.5),
|
||||
Point(120.5, 29.5)
|
||||
])
|
||||
print(f"\n多边形-多边形关系:")
|
||||
print(f" poly1 与 poly2: {calculate_relation(polygon, poly2)}")
|
||||
|
||||
# ========================================================================
|
||||
# 4. 空间索引
|
||||
# ========================================================================
|
||||
print("\n\n[部分 4] R树空间索引")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建R树
|
||||
rtree = RTree(max_children=4)
|
||||
|
||||
# 插入一些特征
|
||||
features = []
|
||||
for i in range(20):
|
||||
x = random.uniform(119, 122)
|
||||
y = random.uniform(29, 32)
|
||||
feature = SpatialFeature(
|
||||
id=f"feature_{i:03d}",
|
||||
geometry=Point(x, y),
|
||||
properties={"value": random.uniform(0, 100)}
|
||||
)
|
||||
features.append(feature)
|
||||
rtree.insert(feature)
|
||||
|
||||
print(f"\n已插入 {rtree.size} 个特征到R树")
|
||||
|
||||
# 范围查询
|
||||
query_env = Envelope(120, 121, 30, 31)
|
||||
results = rtree.query(query_env)
|
||||
print(f"\n范围查询 {query_env}:")
|
||||
print(f" 找到 {len(results)} 个特征")
|
||||
for f in results[:5]:
|
||||
print(f" - {f.id}: {f.geometry}")
|
||||
|
||||
# K近邻查询
|
||||
query_point = Point(120.5, 30.5)
|
||||
neighbors = rtree.nearest_neighbor(query_point, k=5)
|
||||
print(f"\nK近邻查询 (中心点: {query_point}):")
|
||||
for i, (f, dist) in enumerate(neighbors, 1):
|
||||
print(f" {i}. {f.id}: 距离 = {dist:.4f}")
|
||||
|
||||
# ========================================================================
|
||||
# 5. 栅格场模型
|
||||
# ========================================================================
|
||||
print("\n\n[部分 5] 栅格场模型")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建栅格场
|
||||
grid = GridField(
|
||||
bounds=Envelope(119, 122, 29, 32),
|
||||
rows=30,
|
||||
cols=30,
|
||||
nodata=-9999
|
||||
)
|
||||
|
||||
# 填充一些模拟数据 (温度场)
|
||||
for r in range(grid.rows):
|
||||
for c in range(grid.cols):
|
||||
# 计算坐标
|
||||
x = grid.bounds.min_x + (c + 0.5) * grid.cell_width
|
||||
y = grid.bounds.max_y - (r + 0.5) * grid.cell_height
|
||||
|
||||
# 模拟温度场 (简单的径向基函数)
|
||||
center_x, center_y = 120.5, 30.5
|
||||
dist = math.sqrt((x - center_x)**2 + (y - center_y)**2)
|
||||
temp = 25 - dist * 2 # 中心25度,向外递减
|
||||
|
||||
grid.set_value(r, c, round(temp, 2))
|
||||
|
||||
print(f"\n栅格场信息:")
|
||||
print(f" 范围: {grid.bounds}")
|
||||
print(f" 尺寸: {grid.rows} x {grid.cols}")
|
||||
print(f" 单元格大小: {grid.cell_width:.4f} x {grid.cell_height:.4f}")
|
||||
|
||||
stats = grid.get_statistics()
|
||||
print(f" 统计: {stats}")
|
||||
|
||||
# 点查询
|
||||
sample_point = Point(120.5, 30.5)
|
||||
value = grid.get_value_at_point(sample_point)
|
||||
print(f"\n点位置 {sample_point} 的值: {value}")
|
||||
|
||||
# ========================================================================
|
||||
# 6. GeoJSON导出
|
||||
# ========================================================================
|
||||
print("\n\n[部分 6] GeoJSON导出")
|
||||
print("-" * 50)
|
||||
|
||||
# 创建特征集合
|
||||
feature_collection = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
feature1.to_geojson(),
|
||||
feature2.to_geojson()
|
||||
]
|
||||
}
|
||||
|
||||
print("\n特征集合:")
|
||||
print(json.dumps(feature_collection, ensure_ascii=False, indent=2))
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("演示完成!")
|
||||
print("="*70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
# 可运行代码示例
|
||||
|
||||
本目录包含书中提到的所有可运行代码示例,按章节组织。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
examples/
|
||||
├── 00-introduction/ # 导论部分示例
|
||||
│ └── setup-first-assistant/ # 第一个空间AI助手
|
||||
├── 01-foundations/ # 基础原理示例
|
||||
│ ├── modular-system/ # 模块化系统
|
||||
│ ├── state-machine/ # 状态机实现
|
||||
│ ├── probability/ # 概率与不确定性
|
||||
│ ├── feedback/ # 反馈与学习
|
||||
│ └── hitl/ # 人机协同
|
||||
├── 02-spatial-intelligence/ # 空间智能示例
|
||||
│ ├── spatial-representation/ # 空间表征
|
||||
│ ├── spatial-reasoning/ # 空间推理
|
||||
│ ├── mcdm/ # 多准则决策
|
||||
│ ├── optimization/ # 空间优化
|
||||
│ └── uncertainty/ # 不确定性量化
|
||||
├── 03-autonomous-design/ # 自主设计示例
|
||||
│ ├── workflow/ # 工作流编排
|
||||
│ ├── agents/ # Agent设计
|
||||
│ ├── skills/ # 技能组合
|
||||
│ └── memory/ # 记忆管理
|
||||
└── templates/ # 项目模板
|
||||
├── basic-skill.md
|
||||
├── langgraph-workflow.py
|
||||
└── hitl-checkpoint.py
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境配置
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/your-org/CC4SI.git
|
||||
cd CC4SI
|
||||
|
||||
# 创建虚拟环境
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# 或
|
||||
venv\Scripts\activate # Windows
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 运行示例
|
||||
|
||||
```bash
|
||||
# 导论示例:第一个空间助手
|
||||
python 00-introduction/setup-first-assistant/spatial_helper.py
|
||||
|
||||
# 基础原理示例:状态机
|
||||
python 01-foundations/state-machine/workflow_state_machine.py
|
||||
|
||||
# 空间智能示例:空间推理
|
||||
python 02-spatial-intelligence/spatial-reasoning/corridor_detection.py
|
||||
```
|
||||
|
||||
## 示例说明
|
||||
|
||||
### 00-introduction/setup-first-assistant
|
||||
|
||||
**目标**:搭建第一个空间AI助手
|
||||
|
||||
**内容**:
|
||||
- `spatial_helper.py` - 空间分析助手类
|
||||
- `create_sample_data.py` - 创建示例数据
|
||||
- `README.md` - 使用说明
|
||||
|
||||
**运行**:
|
||||
```bash
|
||||
cd 00-introduction/setup-first-assistant
|
||||
python spatial_helper.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 01-foundations/modular-system
|
||||
|
||||
**目标**:理解模块化系统设计
|
||||
|
||||
**内容**:
|
||||
- `pipeline.py` - 空间分析流水线
|
||||
- `skills.py` - 技能封装示例
|
||||
- `composition.py` - 函数式组合
|
||||
|
||||
---
|
||||
|
||||
### 01-foundations/state-machine
|
||||
|
||||
**目标**:实现状态机工作流
|
||||
|
||||
**内容**:
|
||||
- `workflow_state_machine.py` - 完整状态机实现
|
||||
- `langgraph_example.py` - LangGraph版本
|
||||
|
||||
---
|
||||
|
||||
### 02-spatial-intelligence/mcdm
|
||||
|
||||
**目标**:多准则决策分析
|
||||
|
||||
**内容**:
|
||||
- `ahp.py` - 层次分析法
|
||||
- `sensitivity.py` - 敏感性分析
|
||||
- `weighted_overlay.py` - 加权叠加
|
||||
|
||||
---
|
||||
|
||||
### 03-autonomous-design/workflow
|
||||
|
||||
**目标**:工作流编排实践
|
||||
|
||||
**内容**:
|
||||
- `dag_workflow.py` - DAG工作流
|
||||
- `conditional_routing.py` - 条件路由
|
||||
- `error_handling.py` - 错误处理模式
|
||||
|
||||
---
|
||||
|
||||
## 依赖要求
|
||||
|
||||
```
|
||||
python>=3.10
|
||||
|
||||
# 核心依赖
|
||||
geopandas>=0.13.0
|
||||
shapely>=2.0.0
|
||||
rasterio>=1.3.0
|
||||
networkx>=3.0
|
||||
|
||||
# AI框架
|
||||
langchain>=0.1.0
|
||||
langgraph>=0.0.20
|
||||
anthropic>=0.18.0
|
||||
|
||||
# 可视化
|
||||
matplotlib>=3.7.0
|
||||
folium>=0.14.0
|
||||
|
||||
# 数据处理
|
||||
pandas>=2.0.0
|
||||
numpy>=1.24.0
|
||||
|
||||
# 科学计算
|
||||
scipy>=1.10.0
|
||||
scikit-learn>=1.3.0
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **边学边做**:阅读相关章节后立即运行对应示例
|
||||
2. **修改实验**:在理解代码基础上进行修改和实验
|
||||
3. **错误调试**:遇到错误时尝试独立解决,培养调试能力
|
||||
4. **记录笔记**:在代码注释或笔记本中记录你的理解
|
||||
|
||||
## 贡献示例
|
||||
|
||||
欢迎贡献更多示例!
|
||||
|
||||
**示例质量要求**:
|
||||
1. 代码可运行
|
||||
2. 有清晰注释
|
||||
3. 包含使用说明
|
||||
4. 说明设计意图
|
||||
|
||||
详见 [CONTRIBUTING.md](../CONTRIBUTING.md)
|
||||
|
||||
## 许可
|
||||
|
||||
所有示例代码遵循项目许可证:CC BY-NC-SA 4.0
|
||||
@@ -67,6 +67,27 @@ apt install xxx # Ubuntu/Debian
|
||||
| AutoDL | 按时计费 | 中期项目 |
|
||||
| 阿里云PAI | 国内稳定 | 生产部署 |
|
||||
|
||||
### 1.4 软件安装与运行
|
||||
**"安装"的本质就是**:把编译好的二进制文件放到 PATH 某个目录下,让 shell 能找到它。例如git的安装和使用:
|
||||
**总结:三层抽象**
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 用户层:brew install git / git clone │ ← 你看到的
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Shell 层:搜索 PATH → execve() 加载二进制 │ ← 为什么能找到命令
|
||||
├─────────────────────────────────────────────┤
|
||||
│ OS 层:系统调用 (open/read/write/socket) │ ← 为什么能真正干活
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 硬件层:CPU 执行指令、网卡收发数据、磁盘写入 │ ← 物理上发生了什么
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
所以整个链条是:**包管理器下载编译好的二进制 → 放到 PATH 目录 → shell 通过 PATH 找到它 → execve 加载到内存 →**
|
||||
|
||||
**二进制内部调用 OS API 完成实际工作**。没有任何"魔法",本质上就是文件操作和进程管理的组合。
|
||||
|
||||
|
||||
## 2 程序设计语言与软件开发
|
||||
|
||||
### 2.1 什么是程序设计语言
|
||||
@@ -1,5 +1,5 @@
|
||||
# 附录9:Tips
|
||||
|
||||
- 用AI学习AI,大大减少了学习的时间和难度:例如markdown等语法,只需要学习两部分内容:1)掌握经常性的手动输入需要的内容,例如#,- 等,2)了解剩余的语法的大致机制,例如图片插入可以使用html语法,公式排版使用的是Latex语法,具体实现时让AI撰写。
|
||||
-
|
||||
- 有任何不懂的问题,直接问AI,如Claude code等CLI Agent以及在线等的大模型
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# 附录C:关键术语表
|
||||
|
||||
本附录按章节整理书中涉及的关键中英文术语。
|
||||
|
||||
---
|
||||
|
||||
## A
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 自编码器 | Autoencoder, AE | 11 |
|
||||
| 注意力机制 | Attention | 8 |
|
||||
| 人类反馈强化学习 | RLHF | 10 |
|
||||
| 人工智能 | AI, Artificial Intelligence | 1 |
|
||||
| AI for Science | AI4S | 1 |
|
||||
| AI in Education | AIED | 1 |
|
||||
|
||||
## B
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 边界框 | Bounding Box | 6 |
|
||||
| 生物多样性 | Biodiversity | 24 |
|
||||
|
||||
## C
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 卷积神经网络 | CNN | 5, 6, 7 |
|
||||
| 卷积核 | Kernel/Filter | 5 |
|
||||
| 因果推断 | Causal Inference | - |
|
||||
| 连通性 | Connectivity | 25 |
|
||||
| 交叉熵 | Cross Entropy | 4 |
|
||||
|
||||
## D
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 扩散模型 | Diffusion Model | 11 |
|
||||
| 数字孪生 | Digital Twin | 14 |
|
||||
| 深度学习 | Deep Learning | 2 |
|
||||
| 设计生成式AI | Generative Design | 20 |
|
||||
|
||||
## E
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 生态系统服务 | Ecosystem Services | 24 |
|
||||
| 具身智能 | Embodied AI | 14 |
|
||||
| 编码器 | Encoder | 9 |
|
||||
| 解码器 | Decoder | 9 |
|
||||
|
||||
## F
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 特征图 | Feature Map | 5 |
|
||||
| 前馈网络 | FFN, Feed-Forward Network | 2, 9 |
|
||||
| 俯瞰 | Foundation Model | - |
|
||||
|
||||
## G
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 生成对抗网络 | GAN | 11 |
|
||||
| 生成式AI | AIGC | 1, 11, 16 |
|
||||
| 梯度下降 | Gradient Descent | 4 |
|
||||
| 图神经网络 | GNN | 2 |
|
||||
|
||||
## H
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| HITL | Human-in-the-Loop | 15 |
|
||||
| 隐藏层 | Hidden Layer | 3 |
|
||||
| 超参数 | Hyperparameter | - |
|
||||
|
||||
## I
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 交并比 | IoU | 6 |
|
||||
| 图像分类 | Image Classification | 6 |
|
||||
| 实例分割 | Instance Segmentation | 7 |
|
||||
|
||||
## K
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 键 | Key | 8 |
|
||||
| 核函数 | Kernel Function | - |
|
||||
|
||||
## L
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 大语言模型 | LLM | 10 |
|
||||
| 损失函数 | Loss Function | 4 |
|
||||
| 学习率 | Learning Rate | 4 |
|
||||
| LoRA | Low-Rank Adaptation | 12 |
|
||||
| 潜在空间 | Latent Space | 11 |
|
||||
|
||||
## M
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 多头注意力 | Multi-Head Attention | 8 |
|
||||
| 多层感知机 | MLP | 3, 4 |
|
||||
| 多模态 | Multimodal | 10 |
|
||||
| MCP | Model Context Protocol | 15 |
|
||||
| 平均精度 | mAP | 6 |
|
||||
|
||||
## N
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 归一化 | Normalization | 9 |
|
||||
| 神经网络 | Neural Network | 2 |
|
||||
| 非极大值抑制 | NMS | 6 |
|
||||
|
||||
## O
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 目标检测 | Object Detection | 6 |
|
||||
| 优化器 | Optimizer | 4 |
|
||||
| One-Stage检测器 | One-Stage Detector | 6 |
|
||||
|
||||
## P
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 位置编码 | Positional Encoding | 8 |
|
||||
| 池化 | Pooling | 5 |
|
||||
| 提示工程 | Prompt Engineering | 10 |
|
||||
| 预训练 | Pre-training | 10 |
|
||||
| 像素 | Pixel | 7 |
|
||||
|
||||
## Q
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 查询 | Query | 8 |
|
||||
| Q学习 | Q-Learning | 14 |
|
||||
|
||||
## R
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| RAG | Retrieval-Augmented Generation | 10 |
|
||||
| ReAct | Reasoning + Acting | 13 |
|
||||
| 强化学习 | RL, Reinforcement Learning | 14 |
|
||||
| 循环神经网络 | RNN | 2 |
|
||||
| 残差连接 | Residual Connection | 9 |
|
||||
| 值 | Value | 8 |
|
||||
| 感受野 | Receptive Field | 5 |
|
||||
|
||||
## S
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| Self-Attention | 自注意力 | 8 |
|
||||
| 语义分割 | Semantic Segmentation | 7 |
|
||||
| Scaling Law | 缩放定律 | 2 |
|
||||
| Sigmoid | 激活函数 | 4 |
|
||||
| Softmax | 激活函数 | 4 |
|
||||
| 境况 | State | 14 |
|
||||
| 潜变量 | Latent Variable | 11 |
|
||||
| 支持向量机 | SVM | - |
|
||||
|
||||
## T
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| Transformer | Transformer架构 | 9 |
|
||||
| 拓扑优化 | Topology Optimization | 21 |
|
||||
| Token | 令牌/词元 | 9 |
|
||||
| 目标检测 | Two-Stage Detector | 6 |
|
||||
|
||||
## U
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 万能逼近定理 | Universal Approximation Theorem | 2 |
|
||||
| U-Net | 分割网络架构 | 7 |
|
||||
|
||||
## V
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| VAE | Variational Autoencoder | 11 |
|
||||
| 向量数据库 | Vector Database | 10 |
|
||||
| 视觉Transformer | Vision Transformer | 5 |
|
||||
|
||||
## W
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| 权重 | Weight | 3 |
|
||||
| 权重共享 | Weight Sharing | 5 |
|
||||
| 权重衰减 | Weight Decay | - |
|
||||
|
||||
## Y
|
||||
|
||||
| 中文 | 英文 | 章节 |
|
||||
|-----|------|------|
|
||||
| YOLO | You Only Look Once | 6 |
|
||||
|
||||
---
|
||||
|
||||
**更新日期**:2026年4月
|
||||
@@ -0,0 +1,111 @@
|
||||
# 参考文献
|
||||
|
||||
本部分整理教材中引用的论文、书籍和在线资源。
|
||||
|
||||
---
|
||||
|
||||
## 论文
|
||||
|
||||
### 基础理论
|
||||
|
||||
- Universal Approximation Theorem (1989)
|
||||
- Scaling Laws for Neural Language Models (2020)
|
||||
|
||||
### 计算机视觉
|
||||
|
||||
- AlexNet (2012) - ImageNet Classification with Deep Convolutional Neural Networks
|
||||
- ResNet (2015) - Deep Residual Learning for Image Recognition
|
||||
- YOLO (2016) - You Only Look Once: Unified, Real-Time Object Detection
|
||||
- U-Net (2015) - Convolutional Networks for Biomedical Image Segmentation
|
||||
|
||||
### Transformer与大模型
|
||||
|
||||
- Attention Is All You Need (2017)
|
||||
- BERT: Pre-training of Deep Bidirectional Transformers (2018)
|
||||
- GPT-3: Language Models are Few-Shot Learners (2020)
|
||||
- Training Language Models to Follow Instructions with Human Feedback (2022)
|
||||
|
||||
### 生成式AI
|
||||
|
||||
- Denoising Diffusion Probabilistic Models (2020)
|
||||
- High-Resolution Image Synthesis with Latent Diffusion Models (2022)
|
||||
- ControlNet (2023)
|
||||
|
||||
### AI Agent
|
||||
|
||||
- LLM Powered Autonomous Agents (2023)
|
||||
- ReAct: Synergizing Reasoning and Acting in Language Models (2022)
|
||||
|
||||
---
|
||||
|
||||
## 书籍
|
||||
|
||||
### 深度学习
|
||||
|
||||
- Deep Learning (Ian Goodfellow et al.)
|
||||
- Neural Networks and Deep Learning (Michael Nielsen)
|
||||
|
||||
### 强化学习
|
||||
|
||||
- Reinforcement Learning: An Introduction (Sutton & Barto)
|
||||
|
||||
---
|
||||
|
||||
## 在线资源
|
||||
|
||||
### 课程
|
||||
|
||||
- CS231n: Convolutional Neural Networks for Visual Recognition
|
||||
- Fast.ai Practical Deep Learning for Coders
|
||||
|
||||
### 工具文档
|
||||
|
||||
- PyTorch: https://pytorch.org/docs/
|
||||
- Ultralytics YOLO: https://docs.ultralytics.com/
|
||||
- LangChain: https://python.langchain.com/
|
||||
|
||||
### 博客
|
||||
|
||||
- Lil'Log (Lilian Weng): https://lilianweng.github.io/
|
||||
- The Illustrated Transformer: https://jalammar.github.io/illustrated-transformer/
|
||||
|
||||
---
|
||||
|
||||
## 设计AI相关
|
||||
|
||||
### 学术期刊
|
||||
|
||||
- Landscape and Urban Planning
|
||||
- Environment and Planning B: Urban Analytics and City Science
|
||||
- Automation in Construction
|
||||
|
||||
### 会议
|
||||
|
||||
- CAAD Futures
|
||||
- ACADIA
|
||||
- eCAADe
|
||||
|
||||
---
|
||||
|
||||
## 数据集
|
||||
|
||||
### 计算机视觉
|
||||
|
||||
- ImageNet
|
||||
- COCO (Common Objects in Context)
|
||||
- MNIST
|
||||
|
||||
### 空间数据
|
||||
|
||||
- OpenStreetMap
|
||||
- 路网数据、POI数据等
|
||||
|
||||
---
|
||||
|
||||
## 许可说明
|
||||
|
||||
部分内容引用自公开资源,遵循相应许可协议使用。
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026年4月
|
||||
@@ -0,0 +1,251 @@
|
||||
# 附录A:编程工具与资源
|
||||
|
||||
本附录整理AI学习和实践所需的编程工具、框架和资源。
|
||||
|
||||
---
|
||||
|
||||
## Python环境配置
|
||||
|
||||
### Anaconda/Miniconda
|
||||
|
||||
| 工具 | 大小 | 特点 | 下载地址 |
|
||||
|-----|------|------|---------|
|
||||
| Anaconda | ~500MB | 预装常用库 | [anaconda.com](https://www.anaconda.com/download) |
|
||||
| Miniconda | ~50MB | 精简安装 | [docs.conda.io](https://docs.conda.io/en/latest/miniconda.html) |
|
||||
|
||||
### 安装步骤
|
||||
|
||||
```bash
|
||||
# 1. 下载并安装Miniconda
|
||||
# 2. 创建虚拟环境
|
||||
conda create -n ai-env python=3.10
|
||||
|
||||
# 3. 激活环境
|
||||
conda activate ai-env
|
||||
|
||||
# 4. 安装核心库
|
||||
pip install torch torchvision numpy pandas scipy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 深度学习框架
|
||||
|
||||
### PyTorch
|
||||
|
||||
```bash
|
||||
pip install torch torchvision torchaudio
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 动态计算图
|
||||
- 研究友好
|
||||
- 广泛的社区支持
|
||||
|
||||
**资源**:
|
||||
- [官方文档](https://pytorch.org/docs/)
|
||||
- [中文教程](https://pytorch.zhangxiann.com/)
|
||||
|
||||
### TensorFlow
|
||||
|
||||
```bash
|
||||
pip install tensorflow
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 生产部署优化
|
||||
- Keras高级API
|
||||
- 跨平台支持
|
||||
|
||||
---
|
||||
|
||||
## 计算机视觉工具
|
||||
|
||||
### OpenCV
|
||||
|
||||
```bash
|
||||
pip install opencv-python
|
||||
```
|
||||
|
||||
**功能**:图像处理、视频分析
|
||||
|
||||
### Ultralytics YOLO
|
||||
|
||||
```bash
|
||||
pip install ultralytics
|
||||
```
|
||||
|
||||
**功能**:目标检测、实例分割
|
||||
|
||||
**使用示例**:
|
||||
```python
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO('yolov8n.pt')
|
||||
results = model('image.jpg')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AIGC工具链
|
||||
|
||||
### Stable Diffusion
|
||||
|
||||
**WebUI**:[Automatic1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui)
|
||||
|
||||
**ComfyUI**:[GitHub](https://github.com/comfyanonymous/ComfyUI)
|
||||
|
||||
**API调用**:
|
||||
```python
|
||||
from diffusers import StableDiffusionPipeline
|
||||
|
||||
pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5")
|
||||
image = pipe("a photo of an astronaut riding a horse on mars").images[0]
|
||||
```
|
||||
|
||||
### ControlNet
|
||||
|
||||
```python
|
||||
from diffusers import StableDiffusionControlNetPipeline
|
||||
|
||||
controlnet = ControlNetModel.from_pretrained("lllyasviel/sd-controlnet-canny")
|
||||
pipe = StableDiffusionControlNetPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", controlnet=controlnet)
|
||||
```
|
||||
|
||||
### Midjourney
|
||||
|
||||
**平台**:Discord
|
||||
**文档**:[docs.midjourney.com](https://docs.midjourney.com/)
|
||||
|
||||
---
|
||||
|
||||
## Agent开发框架
|
||||
|
||||
### LangChain
|
||||
|
||||
```bash
|
||||
pip install langchain langchain-openai
|
||||
```
|
||||
|
||||
**功能**:LLM应用开发框架
|
||||
|
||||
**核心组件**:
|
||||
- Models:LLM接口
|
||||
- Prompts:提示管理
|
||||
- Chains:链式调用
|
||||
- Agents:智能体
|
||||
- Memory:记忆管理
|
||||
|
||||
### LangGraph
|
||||
|
||||
```bash
|
||||
pip install langgraph
|
||||
```
|
||||
|
||||
**功能**:状态机式Agent开发
|
||||
|
||||
### LlamaIndex
|
||||
|
||||
```bash
|
||||
pip install llama-index
|
||||
```
|
||||
|
||||
**功能**:数据索引与检索(RAG)
|
||||
|
||||
---
|
||||
|
||||
## 开发工具
|
||||
|
||||
### VSCode
|
||||
|
||||
**AI开发常用插件**:
|
||||
- Python
|
||||
- Pylance
|
||||
- Jupyter
|
||||
- Copilot
|
||||
|
||||
### Cursor
|
||||
|
||||
**特点**:AI原生IDE
|
||||
**网址**:[cursor.com](https://cursor.com/)
|
||||
|
||||
### Jupyter Lab
|
||||
|
||||
```bash
|
||||
pip install jupyterlab
|
||||
jupyter lab
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在线学习资源
|
||||
|
||||
### 课程
|
||||
|
||||
| 名称 | 平台 | 链接 |
|
||||
|-----|------|------|
|
||||
| CS231n | Stanford | [cs231n.stanford.edu](http://cs231n.stanford.edu/) |
|
||||
| Fast.ai | fast.ai | [course.fast.ai](https://course.fast.ai/) |
|
||||
| 吴恩达深度学习 | Coursera | [coursera.org/specializations/deep-learning](https://www.coursera.org/specializations/deep-learning) |
|
||||
|
||||
### 博客与文档
|
||||
|
||||
- [Lil'Log](https://lilianweng.github.io/) - AI深度文章
|
||||
- [The Illustrated Transformer](https://jalammar.github.io/illustrated-transformer/)
|
||||
- [Distill.pub](https://distill.pub/) - 可视化论文
|
||||
|
||||
### 数据集
|
||||
|
||||
| 数据集 | 内容 | 链接 |
|
||||
|-------|------|------|
|
||||
| ImageNet | 图像分类 | [image-net.org](https://www.image-net.org/) |
|
||||
| COCO | 目标检测 | [cocodataset.org](https://cocodataset.org/) |
|
||||
| OpenStreetMap | 地图数据 | [openstreetmap.org](https://www.openstreetmap.org/) |
|
||||
|
||||
---
|
||||
|
||||
## 模型资源
|
||||
|
||||
### Hugging Face
|
||||
|
||||
**网址**:[huggingface.co](https://huggingface.co/)
|
||||
|
||||
**功能**:
|
||||
- 模型仓库
|
||||
- 数据集
|
||||
- Spaces在线演示
|
||||
|
||||
### 常用模型
|
||||
|
||||
| 任务 | 推荐模型 | Hugging Face ID |
|
||||
|-----|---------|----------------|
|
||||
| 文生图 | Stable Diffusion XL | stabilityai/stable-diffusion-xl-base-1.0 |
|
||||
| 目标检测 | YOLOv8 | Ultralytics |
|
||||
| 语义分割 | SAM | segment-anything |
|
||||
| 大语言模型 | Llama 3 | meta-llama/Meta-Llama-3-8B |
|
||||
|
||||
---
|
||||
|
||||
## 硬件资源
|
||||
|
||||
### 云平台
|
||||
|
||||
| 平台 | 特点 | 适合场景 |
|
||||
|-----|------|---------|
|
||||
| Google Colab | 免费GPU | 学习实验 |
|
||||
| Kaggle Notebooks | 免费GPU | 竞赛 |
|
||||
| AutoDL | 按时计费 | 中期项目 |
|
||||
| 阿里云PAI | 国内稳定 | 生产部署 |
|
||||
|
||||
### 本地GPU
|
||||
|
||||
推荐配置:
|
||||
- GPU:RTX 3060 (12GB) 或更高
|
||||
- 内存:16GB+
|
||||
- 存储:至少100GB SSD
|
||||
|
||||
---
|
||||
|
||||
## 最后更新
|
||||
|
||||
2026年4月
|
||||
@@ -0,0 +1,198 @@
|
||||
# 00.1 为什么要读这本书
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 当AI能帮我们写代码、做分析、出方案时,我们为什么还要学习原理?
|
||||
> 在技术快速迭代的当下,什么样的知识才值得投资时间?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### AI的"黑箱"问题
|
||||
|
||||
随着大模型(LLM)的普及,AI系统变得越来越强大,但也越来越不透明。这种现象被称为**黑箱化**:
|
||||
|
||||
```
|
||||
输入 ──→ [ AI黑箱 ] ──→ 输出
|
||||
↑ ↑
|
||||
看不见 理解不了
|
||||
```
|
||||
|
||||
**黑箱化的三个层次**:
|
||||
|
||||
1. **算法黑箱**:神经网络内部数百万参数的决策过程难以解释
|
||||
2. **工具黑箱**:封装好的API调用,不知道内部如何工作
|
||||
3. **认知黑箱**:连问"为什么"的能力都丧失了
|
||||
|
||||
### 空间智能的特殊性
|
||||
|
||||
空间分析与一般的智能任务有几个关键区别:
|
||||
|
||||
| 维度 | 一般AI任务 | 空间AI任务 |
|
||||
|-----|-----------|-----------|
|
||||
| 数据结构 | 文本、图像为主 | 栅格、矢量、图 |
|
||||
| 关系类型 | 语义关系 | 空间关系(拓扑、距离、方向) |
|
||||
| 不确定性 | 语义歧义 | 位置误差、尺度效应、边界模糊 |
|
||||
| 可视化 | 辅助理解 | 核心分析手段 |
|
||||
| 领域知识 | 可迁移 | 强依赖地理学、生态学等 |
|
||||
|
||||
这些特殊性意味着:
|
||||
- 通用AI模型不能直接解决空间问题
|
||||
- 需要理解空间分析的本质才能有效设计AI系统
|
||||
- 验证空间AI的结果需要空间思维
|
||||
|
||||
### 原理学习的持久价值
|
||||
|
||||
**技术迭代的两个速度**:
|
||||
|
||||
```
|
||||
易变层(命令、框架、API)── 半衰期:6-18个月
|
||||
│
|
||||
├─ 示例:QGIS 2.x → 3.x 界面大变
|
||||
├─ 示例:LangChain → LangGraph 架构演进
|
||||
└─ 示例:OpenAI API 格式频繁更新
|
||||
│
|
||||
稳定层(设计思想、原理、模式)── 半衰期:10-20年
|
||||
│
|
||||
├─ 示例:图算法在空间分析中的应用
|
||||
├─ 示例:模块化设计原则
|
||||
└─ 示例:Human-in-the-Loop理念
|
||||
```
|
||||
|
||||
**投资回报分析**:
|
||||
|
||||
| 知识类型 | 学习难度 | 过时风险 | 长期价值 |
|
||||
|---------|---------|---------|---------|
|
||||
| 具体命令操作 | 低 | 高 | 低 |
|
||||
| 框架使用方法 | 中 | 中 | 中 |
|
||||
| 设计原理 | 中 | 低 | 高 |
|
||||
| 底层思想 | 高 | 极低 | 极高 |
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 为什么选择Claude Code作为脚手架
|
||||
|
||||
Claude Code有几个特点使其成为学习AI原理的理想工具:
|
||||
|
||||
1. **透明性**:可以看到AI的思考过程(function call、tool use)
|
||||
2. **可扩展性**:Skill、Agent、Hook的设计模式清晰
|
||||
3. **工具中性**:不绑定特定平台,可迁移原理
|
||||
|
||||
### "脚手架"比喻的含义
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 建筑物的结构 │ ← 持久的原理知识
|
||||
│ (空间智能的原理) │
|
||||
├─────────────────────────────────────┤
|
||||
│ ╔═════════╗ │
|
||||
│ ║ 脚手架 ║ │ ← 临时的学习工具
|
||||
│ ╚═════════╝ │ ← Claude Code
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
脚手架的作用:
|
||||
- **支撑学习过程**:让你能到达原本够不到的高度
|
||||
- **可拆卸**:学会后可以移除,知识已内化
|
||||
- **可复用**:用于学习不同的知识领域
|
||||
|
||||
---
|
||||
|
||||
## 案例分析:ENAgent项目
|
||||
|
||||
ENAgent是一个生态网络分析智能体系统,用它来说明原理学习的重要性。
|
||||
|
||||
### 问题背景
|
||||
|
||||
生态网络分析包括多个步骤:
|
||||
1. 生态源地识别
|
||||
2. 阻力面构建
|
||||
3. 最小累积阻力(MCR)分析
|
||||
4. 生态廊道提取
|
||||
5. 网络优化
|
||||
6. 结果评估
|
||||
|
||||
传统做法:每个步骤用不同工具,手动操作,容易出错。
|
||||
|
||||
### AI解决方案
|
||||
|
||||
如果只关注"怎么做",可能会写:
|
||||
```python
|
||||
# 这种代码只会调用,不理解原理
|
||||
def ecological_analysis(input_data):
|
||||
result = some_ai_model.generate_analysis(input_data)
|
||||
return result
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 结果不对时不知道为什么
|
||||
- 无法调整参数适应不同场景
|
||||
- 换个模型就不会用了
|
||||
|
||||
### 理解原理的做法
|
||||
|
||||
理解每个步骤的原理后:
|
||||
```python
|
||||
def ecological_analysis_with_principles(
|
||||
landcover: GeoDataFrame,
|
||||
species_params: Dict,
|
||||
resistance_weights: Dict,
|
||||
checkpoints: List[str] = None
|
||||
) -> AnalysisResult:
|
||||
"""
|
||||
基于原理的生态网络分析
|
||||
|
||||
原理1:生态源地识别基于多准则评估
|
||||
原理2:阻力面反映物种移动的空间异质性
|
||||
原理3:MCR本质是图上的最短路径问题
|
||||
原理4:廊道提取需要考虑连接性和宽度
|
||||
|
||||
Args:
|
||||
landcover: 土地利用覆盖数据
|
||||
species_params: 目标物种的景观偏好参数
|
||||
resistance_weights: 各土地类型的阻力权重
|
||||
checkpoints: HITL审查点,人类介入的关键决策
|
||||
"""
|
||||
# 每一步都理解原理,知道为什么这样做
|
||||
...
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- 可以解释每个决策的理由
|
||||
- 能根据具体问题调整方法
|
||||
- 工具变化时原理保持不变
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **自我诊断**:你目前学习新技术的方式是什么?更偏向"怎么做"还是"为什么"?
|
||||
|
||||
2. **知识审计**:列出你掌握的技能,哪些是易变层,哪些是稳定层?
|
||||
|
||||
3. **迁移测试**:如果从QGIS换成ArcGIS,从OpenAI换成Claude,你的哪些知识可以直接迁移?
|
||||
|
||||
4. **黑箱体验**:回想一次你使用AI但无法理解结果的经历,缺少什么知识才能理解?
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **《如何解题》** (Polya) - 数学思维的本质
|
||||
- **《系统化思维导论》** (Gerald Weinberg) - 理解复杂系统
|
||||
- **《技术的本质》** (Brian Arthur) - 技术演进的规律
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **黑箱化是AI时代的普遍风险**,主动学习原理可以抵抗
|
||||
2. **空间智能有独特性**,需要专门的理解,不能完全依赖通用AI
|
||||
3. **投资稳定层知识**,半衰期长,迁移性强
|
||||
4. **Claude Code是脚手架**,帮助理解原理而非替代思考
|
||||
5. **ENAgent案例说明**:理解原理让你能设计、调试、改进系统
|
||||
|
||||
> "授人以鱼不如授人以渔。在AI时代,授人以AI不如授人以理解AI的能力。"
|
||||
@@ -0,0 +1,407 @@
|
||||
# 00.2 空间智能是什么
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 机器如何"理解"空间?空间智能与GIS有什么本质区别?
|
||||
> 为什么空间分析不能简单套用通用AI模型?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 空间认知的层次
|
||||
|
||||
理解空间智能,首先需要理解人类如何认知空间,然后看机器如何模拟。
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 空间认知金字塔 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Level 4: 空间推理与决策 │ ← 最高级
|
||||
│ - 推理未知区域的属性 │
|
||||
│ - 基于空间约束做决策 │
|
||||
│ - 理解因果关系 │
|
||||
│ │
|
||||
│ Level 3: 空间关系理解 │
|
||||
│ - 拓扑关系(相邻、包含、重叠) │
|
||||
│ - 距离与方向 │
|
||||
│ - 空间模式识别 │
|
||||
│ │
|
||||
│ Level 2: 空间表征能力 │
|
||||
│ - 地图阅读 │
|
||||
│ - 比例尺理解 │
|
||||
│ - 2D ↔ 3D 转换 │
|
||||
│ │
|
||||
│ Level 1: 空间感知 │ ← 基础
|
||||
│ - 位置识别 │
|
||||
│ - 距离估计 │
|
||||
│ - 导航本能 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 从GIS到空间智能
|
||||
|
||||
GIS(地理信息系统)和空间智能的关系:
|
||||
|
||||
| 维度 | GIS | 空间智能 |
|
||||
|-----|-----|---------|
|
||||
| **目标** | 管理、分析、可视化空间数据 | 自动化空间决策 |
|
||||
| **操作者** | 人类 | 人类 + AI系统 |
|
||||
| **确定性** | 算法确定,结果可重现 | 常涉及概率和不确定性 |
|
||||
| **适应性** | 需要人工调整参数 | 可根据反馈自我调整 |
|
||||
| **知识编码** | 隐式(在操作者脑中) | 显式(在系统中) |
|
||||
|
||||
**关键区别**:GIS是工具,空间智能是能力。
|
||||
|
||||
```
|
||||
GIS 用户 → [GIS软件] → 空间分析结果
|
||||
↑
|
||||
人类提供所有空间知识
|
||||
|
||||
空间智能用户 → [智能体] → 空间决策
|
||||
↑
|
||||
系统内置空间知识 + 人类提供约束
|
||||
```
|
||||
|
||||
### 空间推理的本质
|
||||
|
||||
空间推理是空间智能的核心。它包括:
|
||||
|
||||
**1. 拓扑推理**
|
||||
|
||||
```python
|
||||
# 拓扑关系的九交模型(DE-9IM)
|
||||
def spatial_relationship(A, B):
|
||||
"""
|
||||
判断两个空间对象的关系
|
||||
"""
|
||||
# A的内部、边界、外部 与 B的内部、边界、外部 的交集
|
||||
intersections = [
|
||||
A.interior ∩ B.interior, # I∩I
|
||||
A.interior ∩ B.boundary, # I∩B
|
||||
A.interior ∩ B.exterior, # I∩E
|
||||
A.boundary ∩ B.interior, # B∩I
|
||||
A.boundary ∩ B.boundary, # B∩B
|
||||
A.boundary ∩ B.exterior, # B∩E
|
||||
A.exterior ∩ B.interior, # E∩I
|
||||
A.exterior ∩ B.boundary, # E∩B
|
||||
A.exterior ∩ B.exterior, # E∩E
|
||||
]
|
||||
|
||||
# 根据交集模式判断关系:contains, overlaps, touches等
|
||||
return classify_relationship(intersections)
|
||||
```
|
||||
|
||||
**2. 距离推理**
|
||||
|
||||
不仅仅是欧氏距离,还要考虑:
|
||||
- **功能距离**:时间成本、经济成本
|
||||
- **阻力距离**:穿越不同地形的代价
|
||||
- **网络距离**:沿道路网络的路径长度
|
||||
|
||||
**3. 方向推理**
|
||||
|
||||
```python
|
||||
# 方向关系的主方向模型
|
||||
def directional_relationship(source, target):
|
||||
"""
|
||||
判断target相对于source的方向
|
||||
"""
|
||||
angles = {
|
||||
'N': (0, 45), 'NE': (45, 90), 'E': (90, 135),
|
||||
'SE': (135, 180), 'S': (180, 225), 'SW': (225, 270),
|
||||
'W': (270, 315), 'NW': (315, 360)
|
||||
}
|
||||
|
||||
angle = compute_angle(source, target)
|
||||
return get_direction(angles, angle)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 空间智能的两种范式
|
||||
|
||||
**范式1:符号主义空间AI**
|
||||
|
||||
```
|
||||
空间知识规则库 ──→ 逻辑推理引擎 ──→ 结论
|
||||
↑ ↓
|
||||
└──────────────────────────┘
|
||||
反馈学习
|
||||
```
|
||||
|
||||
特点:
|
||||
- 知识显式编码
|
||||
- 推理过程可解释
|
||||
- 适合结构化问题
|
||||
|
||||
**范式2:联结主义空间AI**
|
||||
|
||||
```
|
||||
空间数据 → [神经网络] → 空间决策
|
||||
↑
|
||||
训练数据
|
||||
```
|
||||
|
||||
特点:
|
||||
- 从数据中学习模式
|
||||
- 适合感知类任务
|
||||
- 可解释性较弱
|
||||
|
||||
**混合范式**(本书重点):
|
||||
|
||||
```
|
||||
符号知识 + 数据驱动 → 混合推理 → 可解释的决策
|
||||
↓ ↓ ↓
|
||||
专家经验 深度学习 HITL验证
|
||||
```
|
||||
|
||||
### 为什么不能直接套用通用AI
|
||||
|
||||
通用大模型在空间任务上的局限:
|
||||
|
||||
| 任务类型 | 通用LLM表现 | 空间智能系统 |
|
||||
|---------|------------|-------------|
|
||||
| 空间关系判断 | 需要坐标输入,容易出错 | 内置空间算子 |
|
||||
| 距离计算 | 理解困难 | 几何计算精确 |
|
||||
| 地图阅读 | 需要视觉模型 | 多模态融合 |
|
||||
| 空间优化 | 搜索空间大,效率低 | 专用算法 |
|
||||
|
||||
**原因**:
|
||||
1. 空间关系不是语言关系,不能完全用语言描述
|
||||
2. 空间计算需要几何算法,LLM不擅长
|
||||
3. 空间数据有特殊结构(拓扑、尺度),需要专门处理
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 简单空间推理演示
|
||||
|
||||
```python
|
||||
from shapely.geometry import Point, Polygon
|
||||
import geopandas as gpd
|
||||
|
||||
class SpatialReasoner:
|
||||
"""
|
||||
一个简单的空间推理器示例
|
||||
演示AI如何理解和使用空间关系
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.knowledge_base = {
|
||||
# 关于空间关系的常识
|
||||
"containment": "包含关系是不对称的:A包含B不意味着B包含A",
|
||||
"adjacency": "相邻关系是对称的:A与B相邻意味着B与A相邻",
|
||||
"proximity": "邻近关系会随距离阈值变化"
|
||||
}
|
||||
|
||||
def analyze_spatial_config(self, features: gpd.GeoDataFrame) -> dict:
|
||||
"""
|
||||
分析一组空间特征之间的配置关系
|
||||
"""
|
||||
results = {
|
||||
'topology': self._analyze_topology(features),
|
||||
'clustering': self._detect_clusters(features),
|
||||
'accessibility': self._compute_accessibility(features)
|
||||
}
|
||||
return results
|
||||
|
||||
def _analyze_topology(self, features: gpd.GeoDataFrame) -> dict:
|
||||
"""
|
||||
分析拓扑关系:谁包含谁,谁与谁相邻
|
||||
"""
|
||||
topology = {}
|
||||
|
||||
for i, feat_i in features.iterrows():
|
||||
for j, feat_j in features.iterrows():
|
||||
if i >= j:
|
||||
continue
|
||||
|
||||
geom_i = feat_i.geometry
|
||||
geom_j = feat_j.geometry
|
||||
|
||||
relation = None
|
||||
if geom_i.contains(geom_j):
|
||||
relation = 'contains'
|
||||
elif geom_i.within(geom_j):
|
||||
relation = 'within'
|
||||
elif geom_i.touches(geom_j):
|
||||
relation = 'touches'
|
||||
elif geom_i.intersects(geom_j):
|
||||
relation = 'overlaps'
|
||||
|
||||
if relation:
|
||||
topology[f"{i}-{j}"] = relation
|
||||
|
||||
return topology
|
||||
|
||||
def _detect_clusters(self, features: gpd.GeoDataFrame, threshold=1000) -> list:
|
||||
"""
|
||||
检测空间聚类:哪些对象聚集在一起
|
||||
"""
|
||||
clusters = []
|
||||
used = set()
|
||||
|
||||
for i, feat_i in features.iterrows():
|
||||
if i in used:
|
||||
continue
|
||||
|
||||
cluster = [i]
|
||||
used.add(i)
|
||||
|
||||
for j, feat_j in features.iterrows():
|
||||
if j in used:
|
||||
continue
|
||||
|
||||
if feat_i.geometry.distance(feat_j.geometry) <= threshold:
|
||||
cluster.append(j)
|
||||
used.add(j)
|
||||
|
||||
if len(cluster) > 1:
|
||||
clusters.append(cluster)
|
||||
|
||||
return clusters
|
||||
|
||||
def _compute_accessibility(self, features: gpd.GeoDataFrame) -> dict:
|
||||
"""
|
||||
计算可达性:从一个点到其他点的便利程度
|
||||
"""
|
||||
centroid = features.union_all().centroid
|
||||
accessibility = {}
|
||||
|
||||
for i, feat in features.iterrows():
|
||||
distance = feat.geometry.distance(centroid)
|
||||
accessibility[i] = {
|
||||
'distance_to_center': distance,
|
||||
'accessibility_score': 1 / (1 + distance / 1000)
|
||||
}
|
||||
|
||||
return accessibility
|
||||
|
||||
def infer_from_spatial_relation(self, observed_relation: str) -> str:
|
||||
"""
|
||||
基于观察到的空间关系进行推理
|
||||
"""
|
||||
inferences = {
|
||||
"如果A包含B,且B包含C": "则A可能包含C(传递性)",
|
||||
"如果A与B相邻,且B与C相邻": "A和C可能相邻或很近",
|
||||
"如果一组对象形成聚类": "它们可能有相似的性质"
|
||||
}
|
||||
|
||||
return inferences.get(observed_relation, "无法推理")
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# 创建一些简单的空间对象
|
||||
features = gpd.GeoDataFrame({
|
||||
'name': ['park', 'building', 'lake', 'plaza'],
|
||||
'geometry': [
|
||||
Point(0, 0).buffer(100), # park
|
||||
Point(50, 50).buffer(30), # building (inside park)
|
||||
Point(200, 0).buffer(80), # lake (near park)
|
||||
Point(150, 50).buffer(40) # plaza
|
||||
]
|
||||
})
|
||||
|
||||
reasoner = SpatialReasoner()
|
||||
analysis = reasoner.analyze_spatial_config(features)
|
||||
|
||||
print("=== 空间推理分析结果 ===")
|
||||
print(f"拓扑关系: {analysis['topology']}")
|
||||
print(f"聚类: {analysis['clustering']}")
|
||||
print(f"可达性: {analysis['accessibility']}")
|
||||
```
|
||||
|
||||
输出示例:
|
||||
```
|
||||
=== 空间推理分析结果 ===
|
||||
拓扑关系: {'0-1': 'contains', '0-2': 'touches'}
|
||||
聚类: [[2, 3]]
|
||||
可达性: {0: {'distance_to_center': 33.5, 'accessibility_score': 0.97}, ...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### 真实项目:生态廊道识别中的空间推理
|
||||
|
||||
在ENAgent项目中,识别生态廊道需要复杂的空间推理:
|
||||
|
||||
**推理链条**:
|
||||
|
||||
1. **前提1**:源地A和B之间存在潜在廊道
|
||||
2. **前提2**:廊道需要满足最小宽度要求
|
||||
3. **前提3**:廊道上的阻力值不应超过阈值
|
||||
4. **前提4**:廊道应该连接相似的生境类型
|
||||
5. **结论**:A和B之间的最优廊道是...
|
||||
|
||||
**AI系统如何执行**:
|
||||
|
||||
```python
|
||||
def find_ecological_corridor(source_a, source_b, landscape):
|
||||
"""
|
||||
基于空间推理的生态廊道识别
|
||||
"""
|
||||
|
||||
# 1. 计算MCR表面(空间计算)
|
||||
mcr_surface = compute_mcr(landscape, source_a, source_b)
|
||||
|
||||
# 2. 提取最小阻力路径(图算法)
|
||||
path = extract_least_cost_path(mcr_surface, source_a, source_b)
|
||||
|
||||
# 3. 验证宽度约束(空间关系)
|
||||
width = calculate_corridor_width(path, landscape)
|
||||
if width < MIN_CORRIDOR_WIDTH:
|
||||
# 推理:如果太窄,尝试次优路径
|
||||
path = extract_next_best_path(mcr_surface, source_a, source_b)
|
||||
|
||||
# 4. 评估生境连续性(领域知识)
|
||||
continuity = assess_habitat_continuity(path, landscape)
|
||||
if continuity < threshold:
|
||||
# 推理:生境不连续,廊道可能无效
|
||||
return None
|
||||
|
||||
return path
|
||||
```
|
||||
|
||||
这个例子展示了:
|
||||
- **空间计算**:MCR、距离计算
|
||||
- **图算法**:最短路径
|
||||
- **领域知识**:最小宽度、生境连续性
|
||||
- **推理逻辑**:约束不满足时的回溯
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **自我评估**:你能在脑海中"想象"一个空间场景的拓扑关系吗?机器如何做到同样的事?
|
||||
|
||||
2. **设计挑战**:如果要设计一个"智能地图助手",它应该具备哪些空间推理能力?
|
||||
|
||||
3. **局限分析**:当前的空间智能系统在哪些空间推理任务上仍然不如人类?
|
||||
|
||||
4. **未来想象**:如果机器具备完全的空间智能,它能做什么现在做不到的事?
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **《空间认知与计算》** - 认知科学与GIS的交叉
|
||||
- **"Spatial Cognition"** (Kluwer Academic Publishers) - 空间认知的经典教材
|
||||
- **QGIS文档** - 理解实际软件中的空间算子实现
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **空间认知有多个层次**,从感知到推理逐级递进
|
||||
2. **GIS ≠ 空间智能**:前者是工具,后者是能力
|
||||
3. **空间推理的核心**:拓扑关系、距离推理、方向推理
|
||||
4. **通用AI不能直接解决空间问题**,需要专门的空间算子
|
||||
5. **混合范式**结合符号知识和数据驱动是当前最佳实践
|
||||
@@ -0,0 +1,427 @@
|
||||
# 00.3 自主设计的含义
|
||||
|
||||
## 核心问题
|
||||
|
||||
> "自动"和"自主"有什么本质区别?
|
||||
> 在设计工作中,AI应该扮演什么角色?工具还是伙伴?
|
||||
> Human-in-the-Loop不是落后,而是高级的设计哲学?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 自动 vs 自主
|
||||
|
||||
这两个词经常被混用,但在AI系统设计中有重要区别:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 自动化(Automation) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 设定 → [固定脚本] → 执行 → 结果 │
|
||||
│ ↑ ↓ │
|
||||
│ 预定义规则 完全确定 │
|
||||
│ │
|
||||
│ 特点: │
|
||||
│ - 按预定规则执行 │
|
||||
│ - 遇到异常停止 │
|
||||
│ - 重复性任务 │
|
||||
│ - 人类设定后不再介入 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 自主化(Autonomy) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 目标 → [智能体] → 观察 → 决策 → 行动 → 反馈 │
|
||||
│ ↑ ↑ ↑ ↓ │
|
||||
│ 高层意图 持续学习 适应调整 环境变化 │
|
||||
│ │
|
||||
│ 特点: │
|
||||
│ - 追求目标而非执行步骤 │
|
||||
│ - 能处理意外情况 │
|
||||
│ - 从经验中学习 │
|
||||
│ - 在约束下自主决策 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 理解的层级
|
||||
|
||||
自主系统的智能程度可以分层:
|
||||
|
||||
```
|
||||
Level 0: 无智能 (No Intelligence)
|
||||
└── 简单机械,完全由外部控制
|
||||
|
||||
Level 1: 反应式智能 (Reactive Intelligence)
|
||||
└── 基于当前状态直接反应,无记忆
|
||||
例如:恒温器、简单的自动门
|
||||
|
||||
Level 2: 基于规则的智能 (Rule-based Intelligence)
|
||||
└── 遵循预定义规则,有分支逻辑
|
||||
例如:专家系统、决策树
|
||||
|
||||
Level 3: 学习型智能 (Learning Intelligence)
|
||||
└── 能从数据中学习,改进性能
|
||||
例如:机器学习模型
|
||||
|
||||
Level 4: 自主智能 (Autonomous Intelligence)
|
||||
└── 设定目标,规划执行,处理意外
|
||||
例如:自动驾驶、智能体系统
|
||||
|
||||
Level 5: 协作智能 (Collaborative Intelligence)
|
||||
└── 与人类协同,理解意图和上下文
|
||||
例如:设计伙伴AI
|
||||
```
|
||||
|
||||
### 设计智能的演进
|
||||
|
||||
从CAD工具到AI设计伙伴的演进:
|
||||
|
||||
```
|
||||
1980s: CAD时代
|
||||
└── 数字化绘图,提高效率
|
||||
设计师 → [绘图工具] → 图纸
|
||||
|
||||
2000s: 参数化设计时代
|
||||
└── 规则驱动,生成变体
|
||||
设计师 → [参数+规则] → [生成器] → 多方案
|
||||
|
||||
2010s: 优化时代
|
||||
└── 目标驱动,搜索最优解
|
||||
设计师 → [目标函数] → [优化算法] → 最优方案
|
||||
|
||||
2020s: AI生成时代
|
||||
└── 意图驱动,智能生成
|
||||
设计师 → [意图描述] → [AI模型] → 设计方案
|
||||
|
||||
未来: 协作设计时代
|
||||
└── 人机协同,共同创造
|
||||
设计师 ⇄ [AI伙伴] ⇄ 设计结果
|
||||
共享理解
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### Human-in-the-Loop (HITL) 的设计哲学
|
||||
|
||||
HITL不是"半自动"的妥协,而是深思熟虑的设计选择。
|
||||
|
||||
**为什么需要HITL?**
|
||||
|
||||
1. **空间问题的复杂性**
|
||||
- 多目标权衡(生态 vs 经济 vs 社会)
|
||||
- 隐性知识无法完全编码
|
||||
- 价值判断需要人类
|
||||
|
||||
2. **责任归属**
|
||||
- 重大决策不能完全交给机器
|
||||
- 设计师需要对结果负责
|
||||
- 伦理考量需要人类判断
|
||||
|
||||
3. **信任建立**
|
||||
- 逐步建立对AI的信任
|
||||
- 可解释性增强信任
|
||||
- 控制感增强接受度
|
||||
|
||||
**HITL的三种模式**:
|
||||
|
||||
```python
|
||||
# 模式1: 决策前审查 (Pre-decision Review)
|
||||
def hitl_pre_review(agent_decision, human_expert):
|
||||
"""
|
||||
人类在AI决策前审查
|
||||
"""
|
||||
proposal = agent.generate_proposal()
|
||||
approval = human_expert.review(proposal)
|
||||
|
||||
if approval:
|
||||
return agent.execute(proposal)
|
||||
else:
|
||||
feedback = human_expert.provide_feedback()
|
||||
return agent.regenerate(feedback)
|
||||
|
||||
# 模式2: 关键点介入 (Checkpoint Intervention)
|
||||
def hitl_checkpoint(workflow, checkpoints):
|
||||
"""
|
||||
在关键决策点人类介入
|
||||
"""
|
||||
for step in workflow:
|
||||
result = step.execute()
|
||||
|
||||
if step.name in checkpoints:
|
||||
# 只在关键点需要人类确认
|
||||
if not human.confirm(result):
|
||||
result = human.modify(result)
|
||||
|
||||
return result
|
||||
|
||||
# 模式3: 异常处理 (Exception Handling)
|
||||
def hitl_exception(agent, task):
|
||||
"""
|
||||
AI正常运行,异常时人类介入
|
||||
"""
|
||||
try:
|
||||
return agent.execute(task)
|
||||
except UncertainSituation as e:
|
||||
# AI遇到不确定情况,请求人类帮助
|
||||
return human.resolve(e)
|
||||
```
|
||||
|
||||
### ENAgent的三个审查点
|
||||
|
||||
ENAgent项目的HITL设计:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ ENAgent 工作流程 │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. 数据准备 │
|
||||
│ └── 自动化执行 │
|
||||
│ │
|
||||
│ 2. 生态源地识别 ──→ [审查点1] ──→ 确认/调整源地 │
|
||||
│ (AI识别) (人类专家) (最终决策) │
|
||||
│ │
|
||||
│ 3. 阻力面构建 ──→ [审查点2] ──→ 确认/调整权重 │
|
||||
│ (AI建议) (人类专家) (最终参数) │
|
||||
│ │
|
||||
│ 4. MCR分析 │
|
||||
│ └── 自动化执行 │
|
||||
│ │
|
||||
│ 5. 廊道提取 ──→ [审查点3] ──→ 确认/优化廊道 │
|
||||
│ (AI识别) (人类专家) (最终方案) │
|
||||
│ │
|
||||
│ 6. 结果评估 │
|
||||
│ └── AI + 人类共同评估 │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**为什么选择这三个点?**
|
||||
|
||||
| 审查点 | 原因 | 人类贡献 |
|
||||
|-------|------|---------|
|
||||
| 源地识别 | 需要本地知识,遥感可能误判 | 地面实况,专家经验 |
|
||||
| 阻力权重 | 价值判断,物种特性不同 | 生态学知识,实地经验 |
|
||||
| 廊道优化 | 多目标权衡,不能完全量化 | 规划要求,社会因素 |
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### HITL工作流示意
|
||||
|
||||
```python
|
||||
from typing import Callable, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
class ReviewDecision(Enum):
|
||||
APPROVE = "approve"
|
||||
MODIFY = "modify"
|
||||
REJECT = "reject"
|
||||
|
||||
@dataclass
|
||||
class ReviewResult:
|
||||
decision: ReviewDecision
|
||||
feedback: Optional[str] = None
|
||||
modifications: Optional[dict] = None
|
||||
|
||||
class HumanReviewer:
|
||||
"""
|
||||
人类审查者的抽象接口
|
||||
实际实现可以是CLI界面、Web界面等
|
||||
"""
|
||||
|
||||
def review(self, proposal: dict, context: str) -> ReviewResult:
|
||||
"""
|
||||
审查AI的提案
|
||||
|
||||
Args:
|
||||
proposal: AI生成的提案
|
||||
context: 审查上下文信息
|
||||
|
||||
Returns:
|
||||
审查决定
|
||||
"""
|
||||
print(f"\n=== 审查点: {context} ===")
|
||||
print(f"AI提案: {proposal}")
|
||||
|
||||
# 实际实现中,这里会显示GUI或调用外部接口
|
||||
decision = input("决策 (approve/modify/reject): ")
|
||||
|
||||
if decision == "approve":
|
||||
return ReviewDecision.APPROVE
|
||||
elif decision == "modify":
|
||||
feedback = input("修改意见: ")
|
||||
return ReviewDecision(ReviewDecision.MODIFY, feedback=feedback)
|
||||
else:
|
||||
return ReviewDecision(ReviewDecision.REJECT)
|
||||
|
||||
class HITLWorkflow:
|
||||
"""
|
||||
带人类审查的自主工作流
|
||||
"""
|
||||
|
||||
def __init__(self, reviewer: HumanReviewer):
|
||||
self.reviewer = reviewer
|
||||
self.checkpoints = []
|
||||
|
||||
def add_checkpoint(self, name: str, condition: Callable = None):
|
||||
"""
|
||||
添加审查点
|
||||
|
||||
Args:
|
||||
name: 审查点名称
|
||||
condition: 触发审查的条件函数
|
||||
"""
|
||||
self.checkpoints.append({
|
||||
'name': name,
|
||||
'condition': condition or (lambda _: True)
|
||||
})
|
||||
|
||||
def run(self, steps: list) -> dict:
|
||||
"""
|
||||
执行工作流,在审查点进行人类介入
|
||||
|
||||
Args:
|
||||
steps: 工作流步骤列表
|
||||
|
||||
Returns:
|
||||
最终结果
|
||||
"""
|
||||
context = {}
|
||||
|
||||
for i, step in enumerate(steps):
|
||||
# 执行步骤
|
||||
step_name = step.get('name', f'step_{i}')
|
||||
step_func = step['execute']
|
||||
|
||||
print(f"\n执行步骤: {step_name}")
|
||||
result = step_func(context)
|
||||
context[step_name] = result
|
||||
|
||||
# 检查是否需要审查
|
||||
for checkpoint in self.checkpoints:
|
||||
if checkpoint['name'] == step_name:
|
||||
if checkpoint['condition'](context):
|
||||
review_result = self.reviewer.review(
|
||||
result, step_name
|
||||
)
|
||||
|
||||
if review_result.decision == ReviewDecision.REJECT:
|
||||
# 拒绝,重新执行
|
||||
print("提案被拒绝,重新执行...")
|
||||
return self.run(steps)
|
||||
|
||||
elif review_result.decision == ReviewDecision.MODIFY:
|
||||
# 修改,更新上下文
|
||||
print(f"应用修改: {review_result.feedback}")
|
||||
context[step_name] = self._apply_modifications(
|
||||
result, review_result.modifications
|
||||
)
|
||||
|
||||
# APPROVE: 继续执行
|
||||
|
||||
return context
|
||||
|
||||
def _apply_modifications(self, original: dict, modifications: dict) -> dict:
|
||||
"""应用人类修改"""
|
||||
if modifications:
|
||||
original.update(modifications)
|
||||
return original
|
||||
|
||||
# 使用示例:生态网络分析的HITL工作流
|
||||
if __name__ == "__main__":
|
||||
# 创建审查者
|
||||
reviewer = HumanReviewer()
|
||||
|
||||
# 创建工作流
|
||||
workflow = HITLWorkflow(reviewer)
|
||||
|
||||
# 添加审查点
|
||||
workflow.add_checkpoint('identify_sources')
|
||||
workflow.add_checkpoint('build_resistance')
|
||||
workflow.add_checkpoint('extract_corridors')
|
||||
|
||||
# 定义工作流步骤
|
||||
def step_load_data(context):
|
||||
return {'data_loaded': True}
|
||||
|
||||
def step_identify_sources(context):
|
||||
# 模拟AI识别源地
|
||||
return {
|
||||
'sources': [
|
||||
{'id': 1, 'area': 1500, 'type': 'forest'},
|
||||
{'id': 2, 'area': 800, 'type': 'wetland'}
|
||||
],
|
||||
'confidence': 0.85
|
||||
}
|
||||
|
||||
def step_build_resistance(context):
|
||||
# 模拟AI构建阻力面
|
||||
return {
|
||||
'weights': {
|
||||
'forest': 1,
|
||||
'grassland': 10,
|
||||
'urban': 100,
|
||||
'water': 50
|
||||
}
|
||||
}
|
||||
|
||||
def step_extract_corridors(context):
|
||||
# 模拟AI提取廊道
|
||||
return {
|
||||
'corridors': [
|
||||
{'from': 1, 'to': 2, 'length': 3500, 'quality': 'high'}
|
||||
]
|
||||
}
|
||||
|
||||
steps = [
|
||||
{'name': 'load_data', 'execute': step_load_data},
|
||||
{'name': 'identify_sources', 'execute': step_identify_sources},
|
||||
{'name': 'build_resistance', 'execute': step_build_resistance},
|
||||
{'name': 'extract_corridors', 'execute': step_extract_corridors},
|
||||
]
|
||||
|
||||
# 执行工作流
|
||||
result = workflow.run(steps)
|
||||
print("\n=== 工作流完成 ===")
|
||||
print(result)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **角色定位**:在你自己的工作中,你希望AI扮演什么角色?工具、助手还是伙伴?
|
||||
|
||||
2. **审查点设计**:如果要为你熟悉的工作流程设计HITL,你会选择哪些审查点?为什么?
|
||||
|
||||
3. **信任边界**:在什么情况下你会完全信任AI的决策?在什么情况下必须人类介入?
|
||||
|
||||
4. **责任归属**:如果HITL系统做出了错误决策导致损失,责任应该如何划分?
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Human-in-the-Loop Machine Learning"** - HITL的系统论述
|
||||
- **"Human-Centered AI"** (Ben Shneiderman) - 以人为本的AI设计
|
||||
- **"Designing Autonomous Agents"** - 自主智能体设计理论
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **自动 ≠ 自主**:自动执行预定义步骤,自主追求目标并适应环境
|
||||
2. **智能有层级**:从反应式到协作式,逐级递进
|
||||
3. **HITL不是妥协**:而是深思熟虑的设计选择,在复杂、重要的决策中必不可少
|
||||
4. **审查点选择是关键**:选择需要人类独特能力的决策点
|
||||
5. **从工具到伙伴**:AI在设计中角色的演进,目标是协作而非替代
|
||||
@@ -0,0 +1,429 @@
|
||||
# 00.4 Claude Code作为脚手架
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 为什么选择Claude Code作为学习工具?
|
||||
> 如何"借力打力",用AI来学习AI?
|
||||
> 工具更新换代时,如何保持知识的可迁移性?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### Claude Code是什么
|
||||
|
||||
Claude Code是Anthropic推出的命令行AI开发助手,但它本质上是一个**学习工具**。
|
||||
|
||||
**核心特点**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Claude Code 架构 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 用户 ──→ [Claude Code CLI] ──→ Claude API ──→ 响应 │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ └──── 三个核心概念 ────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Agent │ │ Skill │ │ Hook │ │
|
||||
│ │ (智能体) │ │ (技能) │ │ (钩子) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**对比其他工具**:
|
||||
|
||||
| 维度 | Copilot | ChatGPT | Claude Code |
|
||||
|-----|---------|---------|-------------|
|
||||
| 主要界面 | IDE插件 | Web界面 | 命令行 |
|
||||
| 工作方式 | 代码补全 | 对话 | 对话+执行 |
|
||||
| 可扩展性 | 有限 | 有限 | 高(Skill系统) |
|
||||
| 透明度 | 低 | 中 | 高(显示思考) |
|
||||
| 工具链集成 | IDE内 | 需复制粘贴 | 直接执行 |
|
||||
|
||||
### 为什么选择Claude Code
|
||||
|
||||
**1. 透明性**
|
||||
|
||||
Claude Code可以看到AI的思考过程:
|
||||
|
||||
```
|
||||
用户: 帮我分析这个生态网络
|
||||
|
||||
Claude Code:
|
||||
思考: 用户想要分析生态网络,我需要:
|
||||
1. 识别这是一个空间分析任务
|
||||
2. 确定需要的数据类型
|
||||
3. 选择合适的分析方法
|
||||
4. 调用相关技能
|
||||
|
||||
动作: 调用 skill:ecological-analysis
|
||||
输入: {...}
|
||||
结果: {...}
|
||||
```
|
||||
|
||||
这种透明性对学习很重要——你可以看到AI**如何分解问题**。
|
||||
|
||||
**2. 可扩展性**
|
||||
|
||||
```yaml
|
||||
# .claude/skills/spatial-analysis.md
|
||||
---
|
||||
description: 空间分析技能
|
||||
---
|
||||
|
||||
当用户需要进行空间分析时:
|
||||
1. 确定分析类型(缓冲区分析、叠加分析等)
|
||||
2. 检查所需数据
|
||||
3. 执行分析
|
||||
4. 可视化结果
|
||||
```
|
||||
|
||||
你可以定义自己的技能,学习如何将领域知识编码为AI可以理解和执行的形式。
|
||||
|
||||
**3. 工具中性**
|
||||
|
||||
```
|
||||
Claude Code
|
||||
│
|
||||
├─ 可以调用 QGIS
|
||||
├─ 可以调用 ArcGIS
|
||||
├─ 可以调用 Python脚本
|
||||
└─ 可以调用任何命令行工具
|
||||
```
|
||||
|
||||
学习的是**如何编排工具**,而不是特定工具的用法。
|
||||
|
||||
### 三个核心概念
|
||||
|
||||
#### 1. Agent(智能体)
|
||||
|
||||
Agent是具有目标、能自主行动的系统:
|
||||
|
||||
```python
|
||||
# Agent的抽象结构
|
||||
class Agent:
|
||||
def __init__(self, goal, tools, memory):
|
||||
self.goal = goal # 目标
|
||||
self.tools = tools # 可用工具
|
||||
self.memory = memory # 记忆
|
||||
|
||||
def perceive(self, observation):
|
||||
"""感知环境"""
|
||||
pass
|
||||
|
||||
def decide(self):
|
||||
"""决定下一步行动"""
|
||||
pass
|
||||
|
||||
def act(self):
|
||||
"""执行行动"""
|
||||
pass
|
||||
|
||||
def reflect(self):
|
||||
"""反思和学习"""
|
||||
pass
|
||||
```
|
||||
|
||||
**设计问题**:
|
||||
- Agent应该有多大的自主性?
|
||||
- 如何让Agent的目标与人类意图对齐?
|
||||
- 如何处理Agent的失败?
|
||||
|
||||
#### 2. Skill(技能)
|
||||
|
||||
Skill是能力的封装:
|
||||
|
||||
```yaml
|
||||
# skill示例
|
||||
---
|
||||
name: extract-ecological-sources
|
||||
description: 从土地利用数据中提取生态源地
|
||||
parameters:
|
||||
- landcover_data: GeoDataFrame
|
||||
- min_patch_size: float
|
||||
- suitability_threshold: float
|
||||
returns:
|
||||
- sources: GeoDataFrame
|
||||
- statistics: dict
|
||||
---
|
||||
|
||||
## 执行逻辑
|
||||
|
||||
1. 读取土地利用数据
|
||||
2. 计算每个斑块的适宜性
|
||||
3. 筛选满足条件的斑块
|
||||
4. 按面积排序,选择前N个
|
||||
```
|
||||
|
||||
**设计问题**:
|
||||
- 如何定义技能的边界?
|
||||
- 技能之间如何组合?
|
||||
- 如何让技能可复用?
|
||||
|
||||
#### 3. Hook(钩子)
|
||||
|
||||
Hook是事件驱动的扩展点:
|
||||
|
||||
```yaml
|
||||
# hook示例
|
||||
---
|
||||
name: pre-tool-use
|
||||
description: 在工具调用前执行
|
||||
trigger:
|
||||
- event: ToolUse
|
||||
condition: tool_name == "ecological-analysis"
|
||||
action: |
|
||||
1. 验证输入数据的坐标系
|
||||
2. 检查数据完整性
|
||||
3. 记录分析参数用于审计
|
||||
---
|
||||
|
||||
## 价值
|
||||
- 防止错误的分析
|
||||
- 建立可追溯性
|
||||
- 注入质量控制
|
||||
```
|
||||
|
||||
**设计问题**:
|
||||
- 哪些事件需要hook?
|
||||
- 如何在自动化和控制之间平衡?
|
||||
- 如何避免过度使用hook导致系统僵化?
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 工具中性原则
|
||||
|
||||
**核心思想**:学习的是设计模式,而非特定工具。
|
||||
|
||||
### 指月之辨:工具与真理的关系
|
||||
|
||||
**禅宗寓言**:佛经记载,佛法如标指,真理如明月。指非月也,是指月之指也。
|
||||
|
||||
**寓意**:
|
||||
- 手指(工具)指向月亮(真理/原理)
|
||||
- 我们需要借助手指才能看到月亮
|
||||
- 但手指不是月亮本身,不要执着于手指
|
||||
|
||||
**对应到学习**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 手指 (指) 月亮 (月) │
|
||||
│ ↓ ↓ │
|
||||
│ 工具 原理/真理 │
|
||||
│ - Claude Code - 状态机设计 │
|
||||
│ - LangGraph - 模块化思维 │
|
||||
│ - 具体框架 - 设计模式 │
|
||||
│ - 命令语法 - AI本质 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**实践启示**:
|
||||
|
||||
1. **借指观月,而非指为月**
|
||||
- 通过工具学习原理,而非学习工具本身
|
||||
- Claude Code 是"指",帮助我们指向"月"(AI原理)
|
||||
|
||||
2. **手指过河,筏喻登山**
|
||||
- 工具如竹筏,渡河后应舍弃
|
||||
- 学会原理后,工具可以更换
|
||||
|
||||
3. **莫执着于一指**
|
||||
- 不固守单一工具
|
||||
- 不同工具可指向同一真理
|
||||
- 工具中性:多工具对比学习
|
||||
|
||||
> "授人以鱼不如授人以渔。但更重要的是:不要把渔具当成目的。
|
||||
> 渔具是指向智慧之鱼的'指',真正的'月'是理解如何独立思考、
|
||||
> 如何在新工具出现时快速学习、如何构建持久的认知框架。"
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 抽象层(持久知识) │
|
||||
│ │
|
||||
│ 工作流编排 │ 状态管理 │ 技能组合 │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 实现层(易变知识) │
|
||||
│ │
|
||||
│ Claude Code │ LangGraph │ LangChain │ ... │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**实践方法**:
|
||||
|
||||
1. **学习概念,而非命令**
|
||||
- 理解"状态机"比记住LangGraph的语法更重要
|
||||
- 理解"技能抽象"比记住Skill格式更重要
|
||||
|
||||
2. **关注接口,而非实现**
|
||||
- 定义清晰的输入输出
|
||||
- 理解组件之间的交互模式
|
||||
|
||||
3. **多工具对比**
|
||||
- 用不同工具实现同一任务
|
||||
- 理解不同设计的trade-off
|
||||
|
||||
### 借力打力:用AI学习AI
|
||||
|
||||
```
|
||||
你的学习路径
|
||||
|
||||
1. 向Claude Code提问
|
||||
"解释一下状态机在Agent系统中的作用"
|
||||
|
||||
2. 让它生成示例
|
||||
"给我一个简单的状态机示例代码"
|
||||
|
||||
3. 让它解释代码
|
||||
"逐行解释这段代码在做什么"
|
||||
|
||||
4. 让它出题测试
|
||||
"出几道关于状态机的问题测试我的理解"
|
||||
|
||||
5. 让它点评你的答案
|
||||
"这是我的理解,请点评并指出不足"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### Claude Code基础操作
|
||||
|
||||
```bash
|
||||
# 1. 启动Claude Code
|
||||
claude
|
||||
|
||||
# 2. 基本对话
|
||||
> 你好,请介绍一下空间智能的概念
|
||||
|
||||
# 3. 执行命令
|
||||
> 列出当前目录的文件
|
||||
# Claude会自动执行 ls 命令
|
||||
|
||||
# 4. 读取文件
|
||||
> 读取 README.md 的内容
|
||||
|
||||
# 5. 使用技能
|
||||
> /spatial-analysis 分析这个区域的生态网络
|
||||
|
||||
# 6. 创建新技能
|
||||
> 创建一个名为"buffer-analysis"的技能
|
||||
```
|
||||
|
||||
### 创建一个简单的技能
|
||||
|
||||
文件:`.claude/skills/hello-spatial.md`
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: hello-spatial
|
||||
description: 向用户问好并介绍空间分析基础
|
||||
---
|
||||
|
||||
当用户请求空间分析入门介绍时,使用此技能。
|
||||
|
||||
## 执行步骤
|
||||
|
||||
1. 询问用户的空间分析背景
|
||||
2. 根据背景调整解释深度
|
||||
3. 提供一个简单的空间分析示例
|
||||
4. 推荐进一步学习资源
|
||||
|
||||
## 示例对话
|
||||
|
||||
用户: "我是空间分析的初学者"
|
||||
AI: "欢迎!空间分析是关于..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实践示例:Claude Code的第一个空间任务
|
||||
|
||||
```bash
|
||||
# 场景:用Claude Code分析一个简单的空间问题
|
||||
|
||||
> 我有一个包含公园位置的CSV文件,想找出距离每个居民区最近的车公园
|
||||
|
||||
# Claude Code的响应过程:
|
||||
# 1. 理解任务 - 这是一个最近邻分析问题
|
||||
# 2. 确定工具 - 使用geopandas和scipy
|
||||
# 3. 编写代码 - 生成Python脚本
|
||||
# 4. 执行分析 - 运行脚本
|
||||
# 5. 展示结果 - 可视化输出
|
||||
|
||||
让我帮您完成这个最近邻分析。
|
||||
|
||||
首先,让我读取您的数据:
|
||||
[读取 parks.csv]
|
||||
|
||||
然后,我将:
|
||||
1. 计算每个居民区到所有公园的距离
|
||||
2. 找出最近的车公园
|
||||
3. 计算平均服务距离
|
||||
4. 生成可视化地图
|
||||
|
||||
[生成并执行 Python 代码]
|
||||
|
||||
结果:
|
||||
- 平均最近距离:850米
|
||||
- 服务覆盖率:78%
|
||||
- 需要新建公园的区域:[列表]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **工具选择**:你目前使用的AI工具是什么?它在哪些方面帮助/阻碍了你的学习?
|
||||
|
||||
2. **知识迁移**:如果Claude Code不再维护,你学到的哪些知识可以迁移到其他工具?
|
||||
|
||||
3. **透明性**:你认为AI系统应该有多透明?完全透明是否会降低效率?
|
||||
|
||||
4. **自主性**:在使用AI工具时,你希望在多大程度上保持控制权?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **安装Claude Code**
|
||||
```bash
|
||||
npm install -g @anthropic/claude-code
|
||||
```
|
||||
|
||||
2. **创建你的第一个技能**
|
||||
- 选择你熟悉的领域
|
||||
- 定义技能的输入输出
|
||||
- 编写执行逻辑
|
||||
|
||||
3. **与Claude对话**
|
||||
- 询问一个你不懂的AI概念
|
||||
- 让它用多种方式解释
|
||||
- 测试你的理解
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **[Claude Code官方文档](https://claudecode.io/zh)** - 最权威的参考资料
|
||||
- **"Tools for Thought"** (Richard Wurman) - 思考工具的历史
|
||||
- **"Building Software with AI"** - AI辅助软件开发的实践
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **Claude Code是学习工具**,不只是生产力工具
|
||||
2. **透明性、可扩展性、工具中性**是其核心优势
|
||||
3. **Agent、Skill、Hook**三个概念构成核心抽象
|
||||
4. **关注抽象层**的知识,实现层会变化
|
||||
5. **借力打力**:用AI来学习AI是最高效的方式
|
||||
@@ -0,0 +1,111 @@
|
||||
# 第一部分:导论
|
||||
|
||||
## 本部分目标
|
||||
|
||||
建立读者对AI在空间领域应用的宏观认知,理解:
|
||||
- 为什么在AI时代需要理解原理而非仅仅学习操作
|
||||
- 空间智能的独特性及其与通用AI的区别
|
||||
- 自主设计的真正含义及其在设计实践中的价值
|
||||
- Claude Code如何作为学习工具而不仅是生产力工具
|
||||
|
||||
---
|
||||
|
||||
## 章节导航
|
||||
|
||||
| 章节 | 文件 | 核心问题 | 实践 |
|
||||
|-----|------|---------|------|
|
||||
| 00.1 | [为什么要读这本书](./00.1-why-this-book.md) | AI只是工具吗?原理知识为何重要? | ENAgent项目介绍 |
|
||||
| 00.2 | [空间智能是什么](./00.2-what-is-spatial-intelligence.md) | 机器如何"理解"空间? | 空间推理演示 |
|
||||
| 00.3 | [自主设计的含义](./00.3-what-is-autonomous-design.md) | 自动与自主有何区别? | HITL工作流示意 |
|
||||
| 00.4 | [Claude Code作为脚手架](./00.4-claude-code-as-scaffold.md) | 如何借力AI学习AI? | Claude Code基础操作 |
|
||||
|
||||
---
|
||||
|
||||
## 学习路径
|
||||
|
||||
```
|
||||
开始阅读
|
||||
│
|
||||
├─ 你怀疑AI的可靠性?
|
||||
│ └─ 重点阅读 00.1
|
||||
│
|
||||
├─ 你想了解空间AI的独特性?
|
||||
│ └─ 重点阅读 00.2
|
||||
│
|
||||
├─ 你对Agent和自动化感兴趣?
|
||||
│ └─ 重点阅读 00.3
|
||||
│
|
||||
└─ 你想快速上手实践?
|
||||
└─ 直接跳到 00.4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前置知识
|
||||
|
||||
**必需**:
|
||||
- 基本的GIS概念(图层、坐标系、矢量/栅格)
|
||||
- Python编程基础
|
||||
|
||||
** helpful but not required**:
|
||||
- 机器学习的基本概念
|
||||
- 工作流自动化经验
|
||||
|
||||
---
|
||||
|
||||
## 预计学习时间
|
||||
|
||||
| 阅读类型 | 时间估计 |
|
||||
|---------|---------|
|
||||
| 快速浏览 | 2-3小时 |
|
||||
| 理解性阅读 | 6-8小时 |
|
||||
| 完成所有实践 | 12-15小时 |
|
||||
|
||||
---
|
||||
|
||||
## 章节亮点
|
||||
|
||||
### 00.1 为什么要读这本书
|
||||
- 揭示"命令依赖症"的危害
|
||||
- 解释为什么AI知识半衰期越来越短
|
||||
- 提出持久认知框架的构建方法
|
||||
|
||||
### 00.2 空间智能是什么
|
||||
- 从人类空间认知到机器空间智能
|
||||
- GIS与空间智能的本质区别
|
||||
- 空间推理的两种范式
|
||||
|
||||
### 00.3 自主设计的含义
|
||||
- 澄清常见误解:自动 ≠ 自主
|
||||
- HITL(Human-in-the-Loop)的设计哲学
|
||||
- 从工具到伙伴的关系转变
|
||||
|
||||
### 00.4 Claude Code作为脚手架
|
||||
- Claude Code的独特优势
|
||||
- Agent、Skill、Hook的直观理解
|
||||
- 工具中性原则:如何不被特定工具绑定
|
||||
|
||||
---
|
||||
|
||||
## 实践案例00:搭建你的第一个空间AI助手
|
||||
|
||||
在阅读完所有章节后,你将能够:
|
||||
1. 配置Claude Code环境
|
||||
2. 创建一个简单的空间查询Skill
|
||||
3. 理解Agent如何调用工具完成任务
|
||||
4. 建立对后续学习的信心
|
||||
|
||||
详见 [practice/setup-first-assistant](./practice/setup-first-assistant/)
|
||||
|
||||
---
|
||||
|
||||
## 思考问题
|
||||
|
||||
在开始阅读前,请思考:
|
||||
|
||||
1. 你目前使用AI的方式是什么?遇到的最大困扰是什么?
|
||||
2. 你认为"理解原理"和"学会操作"哪个更重要?为什么?
|
||||
3. 空间分析与一般的智能任务有什么不同?
|
||||
4. 你期待AI在设计工作中扮演什么角色?
|
||||
|
||||
带着这些问题阅读,会更有收获。
|
||||
@@ -0,0 +1,348 @@
|
||||
# 实践案例00:搭建你的第一个空间AI助手
|
||||
|
||||
## 目标
|
||||
|
||||
通过本实践,你将:
|
||||
1. 配置Claude Code环境
|
||||
2. 创建一个简单的空间查询技能
|
||||
3. 理解Agent如何调用工具
|
||||
4. 建立对后续学习的信心
|
||||
|
||||
---
|
||||
|
||||
## 步骤1:环境准备
|
||||
|
||||
### 安装Claude Code
|
||||
|
||||
```bash
|
||||
# 使用npm安装
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# 验证安装
|
||||
claude --version
|
||||
```
|
||||
|
||||
### 配置API密钥
|
||||
|
||||
```bash
|
||||
# 设置API密钥
|
||||
claude config set api_key your_anthropic_api_key
|
||||
|
||||
# 或使用环境变量
|
||||
export ANTHROPIC_API_KEY=your_key
|
||||
```
|
||||
|
||||
### 安装Python依赖
|
||||
|
||||
```bash
|
||||
# 创建虚拟环境
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# 或
|
||||
venv\Scripts\activate # Windows
|
||||
|
||||
# 安装依赖
|
||||
pip install geopandas shapely matplotlib pandas
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 步骤2:创建空间查询技能
|
||||
|
||||
### 2.1 创建技能目录
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/skills
|
||||
```
|
||||
|
||||
### 2.2 编写技能文件
|
||||
|
||||
创建 `.claude/skills/spatial-query.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: spatial-query
|
||||
description: 执行基础空间查询和分析
|
||||
parameters:
|
||||
- data_file: 空间数据文件路径
|
||||
- query_type: 查询类型(buffer/intersect/nearest)
|
||||
- distance: 缓冲距离(用于buffer查询)
|
||||
---
|
||||
|
||||
## 空间查询技能
|
||||
|
||||
当用户需要进行空间查询时,使用此技能。
|
||||
|
||||
### 支持的查询类型
|
||||
|
||||
1. **buffer**: 缓冲区分析
|
||||
2. **intersect**: 相交分析
|
||||
3. **nearest**: 最近邻查找
|
||||
|
||||
### 执行流程
|
||||
|
||||
1. 读取空间数据
|
||||
2. 根据查询类型执行相应操作
|
||||
3. 返回结果和统计信息
|
||||
4. 可选:生成可视化
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 步骤3:实现技能逻辑
|
||||
|
||||
创建 `examples/spatial_helper.py`:
|
||||
|
||||
```python
|
||||
"""
|
||||
简单的空间分析助手
|
||||
演示AI如何理解并执行空间任务
|
||||
"""
|
||||
|
||||
import geopandas as gpd
|
||||
from shapely.geometry import Point
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
class SpatialHelper:
|
||||
"""空间分析助手类"""
|
||||
|
||||
def __init__(self, data_path=None):
|
||||
self.data = None
|
||||
if data_path:
|
||||
self.load_data(data_path)
|
||||
|
||||
def load_data(self, path):
|
||||
"""加载空间数据"""
|
||||
try:
|
||||
self.data = gpd.read_file(path)
|
||||
return f"成功加载 {len(self.data)} 个空间要素"
|
||||
except Exception as e:
|
||||
return f"加载失败: {str(e)}"
|
||||
|
||||
def buffer_analysis(self, distance, crs=None):
|
||||
"""缓冲区分析"""
|
||||
if self.data is None:
|
||||
return "请先加载数据"
|
||||
|
||||
if crs:
|
||||
# 转换到适合距离计算的坐标系
|
||||
self.data = self.data.to_crs(crs)
|
||||
|
||||
buffered = self.data.buffer(distance)
|
||||
result = gpd.GeoDataFrame(geometry=buffered, crs=self.data.crs)
|
||||
|
||||
return {
|
||||
'count': len(result),
|
||||
'total_area': result.geometry.area.sum(),
|
||||
'geometry': result
|
||||
}
|
||||
|
||||
def intersect_analysis(self, other_data):
|
||||
"""相交分析"""
|
||||
if self.data is None:
|
||||
return "请先加载数据"
|
||||
|
||||
intersection = self.data.intersection(other_data)
|
||||
valid_results = intersection[~intersection.is_empty]
|
||||
|
||||
return {
|
||||
'intersecting_count': len(valid_results),
|
||||
'geometries': valid_results
|
||||
}
|
||||
|
||||
def nearest_neighbor(self, point, n=1):
|
||||
"""最近邻查找"""
|
||||
if self.data is None:
|
||||
return "请先加载数据"
|
||||
|
||||
if isinstance(point, (tuple, list)):
|
||||
point = Point(point)
|
||||
|
||||
distances = self.data.geometry.distance(point)
|
||||
nearest_indices = distances.nsmallest(n).index
|
||||
|
||||
results = []
|
||||
for idx in nearest_indices:
|
||||
results.append({
|
||||
'index': idx,
|
||||
'distance': distances[idx],
|
||||
'geometry': self.data.loc[idx].geometry
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
def summarize(self):
|
||||
"""数据摘要"""
|
||||
if self.data is None:
|
||||
return "请先加载数据"
|
||||
|
||||
return {
|
||||
'count': len(self.data),
|
||||
'crs': str(self.data.crs),
|
||||
'bounds': self.data.total_bounds,
|
||||
'geometry_types': self.data.geometry.type.value_counts().to_dict()
|
||||
}
|
||||
|
||||
def visualize(self, output_path=None):
|
||||
"""可视化"""
|
||||
if self.data is None:
|
||||
return "请先加载数据"
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
self.data.plot(ax=ax, alpha=0.5, edgecolor='k')
|
||||
ax.set_title('Spatial Data Visualization')
|
||||
ax.set_axis_off()
|
||||
|
||||
if output_path:
|
||||
plt.savefig(output_path, bbox_inches='tight', dpi=300)
|
||||
return f"图表已保存到 {output_path}"
|
||||
|
||||
plt.show()
|
||||
return "图表已显示"
|
||||
|
||||
# 创建一个示例数据集用于测试
|
||||
def create_sample_data():
|
||||
"""创建示例空间数据"""
|
||||
import numpy as np
|
||||
|
||||
np.random.seed(42)
|
||||
|
||||
# 创建一些随机点
|
||||
points = [Point(np.random.uniform(-100, 100),
|
||||
np.random.uniform(-100, 100))
|
||||
for _ in range(20)]
|
||||
|
||||
# 转换为GeoDataFrame
|
||||
gdf = gpd.GeoDataFrame({
|
||||
'id': range(20),
|
||||
'value': np.random.randint(1, 100, 20),
|
||||
'category': np.random.choice(['A', 'B', 'C'], 20)
|
||||
}, geometry=points, crs='EPSG:4326')
|
||||
|
||||
# 保存为GeoJSON
|
||||
gdf.to_file('sample_data.geojson', driver='GeoJSON')
|
||||
return gdf
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 创建示例数据
|
||||
print("创建示例数据...")
|
||||
create_sample_data()
|
||||
|
||||
# 测试SpatialHelper
|
||||
print("\n初始化空间助手...")
|
||||
helper = SpatialHelper('sample_data.geojson')
|
||||
|
||||
print("\n数据摘要:")
|
||||
print(helper.summarize())
|
||||
|
||||
print("\n缓冲区分析:")
|
||||
buffer_result = helper.buffer_analysis(10)
|
||||
print(f"缓冲区数量: {buffer_result['count']}")
|
||||
print(f"总缓冲面积: {buffer_result['total_area']:.2f}")
|
||||
|
||||
print("\n最近邻查找:")
|
||||
nearest = helper.nearest_neighbor((0, 0), n=3)
|
||||
for i, n in enumerate(nearest, 1):
|
||||
print(f" {i}. 距离: {n['distance']:.2f}")
|
||||
|
||||
print("\n生成可视化...")
|
||||
print(helper.visualize('spatial_visualization.png'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 步骤4:在Claude Code中使用
|
||||
|
||||
### 4.1 启动Claude Code
|
||||
|
||||
```bash
|
||||
cd /path/to/CC4SI
|
||||
claude
|
||||
```
|
||||
|
||||
### 4.2 与Claude对话
|
||||
|
||||
```
|
||||
你: 我有一个空间数据文件sample_data.geojson,帮我分析一下
|
||||
|
||||
Claude: 让我先读取这个文件看看...
|
||||
|
||||
[读取 sample_data.geojson]
|
||||
|
||||
这是一个包含20个点要素的GeoJSON文件,坐标系统为WGS84 (EPSG:4326)。
|
||||
每个点都有id、value和category属性。
|
||||
|
||||
你想要进行什么分析?
|
||||
|
||||
你: 找出距离原点最近的3个点
|
||||
|
||||
Claude: 我来帮你找出距离原点最近的点。
|
||||
|
||||
[调用 spatial-query 技能]
|
||||
|
||||
结果:
|
||||
1. 最近点 (id=7): 距离约 15.2 单位
|
||||
2. 第二近点 (id=12): 距离约 23.8 单位
|
||||
3. 第三近点 (id=3): 距离约 31.5 单位
|
||||
|
||||
需要我生成可视化吗?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 步骤5:理解工作原理
|
||||
|
||||
### AI系统的思考过程
|
||||
|
||||
```
|
||||
用户请求 ──→ [意图识别] ──→ [任务分解]
|
||||
│ │
|
||||
↓ ↓
|
||||
这是空间查询 需要执行:
|
||||
任务 1. 加载数据
|
||||
2. 计算距离
|
||||
3. 排序取前N
|
||||
│ │
|
||||
↓ ↓
|
||||
[选择技能] ──→ [调用工具]
|
||||
│ │
|
||||
↓ ↓
|
||||
spatial-query spatial_helper.py
|
||||
│ │
|
||||
└───────┬───────┘
|
||||
↓
|
||||
[整合结果]
|
||||
↓
|
||||
[返回用户]
|
||||
```
|
||||
|
||||
### 关键概念验证
|
||||
|
||||
| 概念 | 实践体现 |
|
||||
|-----|---------|
|
||||
| Agent | Claude Code作为智能体,理解意图并协调执行 |
|
||||
| Skill | spatial-query技能封装了空间分析能力 |
|
||||
| Tool | spatial_helper.py是具体实现工具 |
|
||||
|
||||
---
|
||||
|
||||
## 反思问题
|
||||
|
||||
1. **理解验证**:你能在脑海中复述一遍AI执行这个任务的流程吗?
|
||||
|
||||
2. **扩展思考**:如果要让这个助手支持更多空间操作,应该如何设计?
|
||||
|
||||
3. **局限性**:当前实现有哪些不足?如何改进?
|
||||
|
||||
4. **迁移应用**:这个模式可以应用到你的专业领域吗?
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
完成这个实践后,你已经:
|
||||
- ✅ 配置了Claude Code环境
|
||||
- ✅ 创建了第一个空间分析技能
|
||||
- ✅ 理解了Agent的基本工作方式
|
||||
|
||||
准备好进入下一章:**01-foundations(基础原理)**
|
||||
@@ -0,0 +1,562 @@
|
||||
# 01.1 智能的模块化视角
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 为什么智能系统需要模块化设计?
|
||||
> 技能(Skill)的本质是什么?如何设计可复用的智能组件?
|
||||
> 函数式组合思想如何应用于AI系统?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 模块化的必要性
|
||||
|
||||
随着系统复杂度增加,模块化变得必不可少:
|
||||
|
||||
```
|
||||
复杂度与模块化的关系
|
||||
|
||||
低复杂度 ──→ [单体脚本] ──→ 可维护
|
||||
↑
|
||||
简单直接
|
||||
|
||||
中复杂度 ──→ [函数库] ──→ 需要组织
|
||||
↑
|
||||
按功能分类
|
||||
|
||||
高复杂度 ──→ [模块化系统] ──→ 必须模块化
|
||||
↑
|
||||
清晰边界,可组合
|
||||
```
|
||||
|
||||
**模块化的收益**:
|
||||
|
||||
| 收益类型 | 说明 | 例子 |
|
||||
|---------|------|------|
|
||||
| 可理解性 | 每个模块可独立理解 | 理解缓冲区分析不需要理解投影变换 |
|
||||
| 可测试性 | 模块可单独测试 | 测试空间索引不需要完整工作流 |
|
||||
| 可复用性 | 模块可在不同场景使用 | 缓冲区算法用于多个项目 |
|
||||
| 可替换性 | 模块可用等价实现替换 | QGIS ↔ ArcGIS 同一功能 |
|
||||
| 可维护性 | 修改局限在模块内 | 修复bug不影响其他模块 |
|
||||
|
||||
### 函数式组合思想
|
||||
|
||||
函数式编程的核心:**组合小函数构建复杂行为**
|
||||
|
||||
```
|
||||
简单函数 ──┬─── buffer(geom, distance)
|
||||
├─── intersect(a, b)
|
||||
├─── centroid(geom)
|
||||
└─── distance(a, b)
|
||||
│
|
||||
↓ 组合
|
||||
│
|
||||
complex_operation = pipe(
|
||||
load_data,
|
||||
clean_geometry,
|
||||
buffer(100),
|
||||
intersect(study_area),
|
||||
calculate_area,
|
||||
format_output
|
||||
)
|
||||
```
|
||||
|
||||
**关键特性**:
|
||||
1. **纯函数**:相同输入→相同输出,无副作用
|
||||
2. **高阶函数**:函数可以作为参数和返回值
|
||||
3. **不可变数据**:数据不修改,而是创建新版本
|
||||
|
||||
### 技能即能力封装
|
||||
|
||||
在Claude Code中,技能(Skill)是智能的模块化单元:
|
||||
|
||||
```yaml
|
||||
# 技能的结构
|
||||
---
|
||||
name: skill-name # 技能名称
|
||||
description: 技能描述 # 何时使用
|
||||
parameters: # 输入参数
|
||||
- param1: type
|
||||
- param2: type
|
||||
returns: # 输出
|
||||
- result: type
|
||||
---
|
||||
|
||||
## 技能逻辑
|
||||
|
||||
具体的执行步骤...
|
||||
```
|
||||
|
||||
**技能设计的三个层次**:
|
||||
|
||||
```
|
||||
Level 1: 原子技能
|
||||
└── 单一功能,不可再分
|
||||
例如:buffer, intersect, dissolve
|
||||
|
||||
Level 2: 组合技能
|
||||
└── 由原子技能组合而成
|
||||
例如:site_selection = buffer + intersect + rank
|
||||
|
||||
Level 3: 工作流技能
|
||||
└── 完整的决策流程
|
||||
例如:ecological_network_analysis
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 模块化的设计原则
|
||||
|
||||
**1. 单一职责原则 (SRP)**
|
||||
|
||||
每个模块只做一件事,做好一件事:
|
||||
|
||||
```python
|
||||
# 好的设计:每个函数职责单一
|
||||
def calculate_distance(geom1, geom2):
|
||||
"""只计算距离"""
|
||||
return geom1.distance(geom2)
|
||||
|
||||
def format_distance(distance, unit='m'):
|
||||
"""只格式化输出"""
|
||||
if distance > 1000:
|
||||
return f"{distance/1000:.2f} km"
|
||||
return f"{distance:.0f} m"
|
||||
|
||||
# 使用
|
||||
dist = calculate_distance(point_a, point_b)
|
||||
formatted = format_distance(dist)
|
||||
|
||||
# 不好的设计:混合了计算和格式化
|
||||
def calculate_and_format_distance(geom1, geom2):
|
||||
distance = geom1.distance(geom2)
|
||||
# 格式化逻辑混在一起
|
||||
if distance > 1000:
|
||||
return f"{distance/1000:.2f} km"
|
||||
return f"{distance:.0f} m"
|
||||
```
|
||||
|
||||
**2. 开闭原则 (OCP)**
|
||||
|
||||
对扩展开放,对修改关闭:
|
||||
|
||||
```python
|
||||
# 使用抽象基类实现扩展性
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class SpatialOperation(ABC):
|
||||
"""空间操作的抽象基类"""
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, data):
|
||||
pass
|
||||
|
||||
class BufferOperation(SpatialOperation):
|
||||
"""缓冲区操作"""
|
||||
def __init__(self, distance):
|
||||
self.distance = distance
|
||||
|
||||
def execute(self, data):
|
||||
return data.buffer(self.distance)
|
||||
|
||||
class IntersectOperation(SpatialOperation):
|
||||
"""相交操作"""
|
||||
def __init__(self, other_data):
|
||||
self.other_data = other_data
|
||||
|
||||
def execute(self, data):
|
||||
return data.intersection(self.other_data)
|
||||
|
||||
# 可以添加新操作而不修改现有代码
|
||||
class UnionOperation(SpatialOperation):
|
||||
"""合并操作"""
|
||||
def execute(self, data):
|
||||
return data.union(self.other_data)
|
||||
```
|
||||
|
||||
**3. 依赖倒置原则 (DIP)**
|
||||
|
||||
依赖抽象而非具体实现:
|
||||
|
||||
```python
|
||||
# 好的设计:依赖抽象
|
||||
class WorkflowProcessor:
|
||||
def __init__(self, operation: SpatialOperation):
|
||||
self.operation = operation # 依赖抽象
|
||||
|
||||
def process(self, data):
|
||||
return self.operation.execute(data)
|
||||
|
||||
# 可以轻松替换具体实现
|
||||
processor = WorkflowProcessor(BufferOperation(100))
|
||||
|
||||
# 不好的设计:依赖具体实现
|
||||
class WorkflowProcessor:
|
||||
def __init__(self, buffer_distance):
|
||||
self.buffer_distance = buffer_distance
|
||||
|
||||
def process(self, data):
|
||||
# 硬编码了具体操作
|
||||
return data.buffer(self.buffer_distance)
|
||||
```
|
||||
|
||||
### 技能接口设计
|
||||
|
||||
良好的技能接口设计:
|
||||
|
||||
```python
|
||||
from typing import Protocol, TypeVar, Generic
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class SkillInput(Protocol[T]):
|
||||
"""技能输入协议"""
|
||||
def validate(self) -> bool:
|
||||
"""验证输入有效性"""
|
||||
...
|
||||
|
||||
class SkillOutput(Protocol[T]):
|
||||
"""技能输出协议"""
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为可序列化格式"""
|
||||
...
|
||||
|
||||
class Skill(Generic[T]):
|
||||
"""技能基类"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
|
||||
def can_handle(self, input_data: T) -> bool:
|
||||
"""判断是否能处理此输入"""
|
||||
pass
|
||||
|
||||
def execute(self, input_data: T) -> SkillOutput[T]:
|
||||
"""执行技能"""
|
||||
pass
|
||||
|
||||
def estimate_cost(self, input_data: T) -> float:
|
||||
"""估算执行成本(时间/资源)"""
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 模块化的空间分析系统
|
||||
|
||||
```python
|
||||
"""
|
||||
模块化空间分析系统示例
|
||||
展示如何用函数式组合构建复杂分析
|
||||
"""
|
||||
from typing import Callable, List, Any, TypeVar
|
||||
from functools import reduce
|
||||
import geopandas as gpd
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class SpatialPipeline:
|
||||
"""空间分析流水线"""
|
||||
|
||||
def __init__(self):
|
||||
self.steps: List[Callable] = []
|
||||
|
||||
def add_step(self, step: Callable, name: str = None):
|
||||
"""添加处理步骤"""
|
||||
step.name = name or step.__name__
|
||||
self.steps.append(step)
|
||||
return self
|
||||
|
||||
def execute(self, initial_data):
|
||||
"""执行流水线"""
|
||||
result = initial_data
|
||||
|
||||
for step in self.steps:
|
||||
print(f"执行步骤: {getattr(step, 'name', step.__name__)}")
|
||||
result = step(result)
|
||||
|
||||
return result
|
||||
|
||||
def pipe(*functions):
|
||||
"""函数式组合工具"""
|
||||
return reduce(lambda f, g: lambda x: g(f(x)), functions)
|
||||
|
||||
# === 原子操作 ===
|
||||
|
||||
def load_data(path: str) -> gpd.GeoDataFrame:
|
||||
"""加载数据"""
|
||||
print(f"加载: {path}")
|
||||
return gpd.read_file(path)
|
||||
|
||||
def clean_geometry(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
||||
"""清理几何"""
|
||||
print("清理几何")
|
||||
# 修复无效几何
|
||||
gdf['geometry'] = gdf.geometry.buffer(0)
|
||||
return gdf[gdf.geometry.is_valid]
|
||||
|
||||
def reproject(gdf: gpd.GeoDataFrame, target_crs: str = 'EPSG:3857') -> gpd.GeoDataFrame:
|
||||
"""重投影"""
|
||||
print(f"重投影到: {target_crs}")
|
||||
return gdf.to_crs(target_crs)
|
||||
|
||||
def buffer(gdf: gpd.GeoDataFrame, distance: float) -> gpd.GeoDataFrame:
|
||||
"""缓冲区分析"""
|
||||
print(f"缓冲距离: {distance}")
|
||||
return gdf.buffer(distance)
|
||||
|
||||
def intersect(gdf: gpd.GeoDataFrame, other: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
||||
"""相交分析"""
|
||||
print("相交分析")
|
||||
return gdf.overlay(other, how='intersection')
|
||||
|
||||
def calculate_area(gdf: gpd.GeoDataFrame) -> float:
|
||||
"""计算面积"""
|
||||
area = gdf.geometry.area.sum()
|
||||
print(f"总面积: {area:.2f} 平方米")
|
||||
return area
|
||||
|
||||
# === 高阶操作 ===
|
||||
|
||||
def make_buffer(distance: float) -> Callable:
|
||||
"""缓冲操作工厂函数"""
|
||||
return lambda gdf: buffer(gdf, distance)
|
||||
|
||||
def make_reproject(crs: str) -> Callable:
|
||||
"""重投影工厂函数"""
|
||||
return lambda gdf: reproject(gdf, crs)
|
||||
|
||||
def make_intersect(other_data: gpd.GeoDataFrame) -> Callable:
|
||||
"""相交工厂函数"""
|
||||
return lambda gdf: intersect(gdf, other_data)
|
||||
|
||||
# === 使用示例 ===
|
||||
|
||||
def example_pipeline_usage():
|
||||
"""流水线使用示例"""
|
||||
|
||||
# 方式1:使用Pipeline类
|
||||
pipeline = SpatialPipeline()
|
||||
pipeline.add_step(load_data, "加载数据")
|
||||
pipeline.add_step(clean_geometry, "清理几何")
|
||||
pipeline.add_step(lambda gdf: reproject(gdf, 'EPSG:3857'), "重投影")
|
||||
pipeline.add_step(lambda gdf: buffer(gdf, 100), "缓冲")
|
||||
pipeline.add_step(calculate_area, "计算面积")
|
||||
|
||||
# result = pipeline.execute("data.geojson")
|
||||
|
||||
# 方式2:使用函数式组合
|
||||
analysis_pipeline = pipe(
|
||||
load_data,
|
||||
clean_geometry,
|
||||
lambda gdf: reproject(gdf, 'EPSG:3857'),
|
||||
lambda gdf: buffer(gdf, 100),
|
||||
calculate_area
|
||||
)
|
||||
|
||||
# result = analysis_pipeline("data.geojson")
|
||||
|
||||
return pipeline
|
||||
|
||||
# === 技能封装 ===
|
||||
|
||||
class BufferSkill:
|
||||
"""缓冲区技能"""
|
||||
|
||||
name = "buffer_analysis"
|
||||
description = "执行缓冲区分析"
|
||||
|
||||
def __init__(self, distance: float, unit: str = 'm'):
|
||||
self.distance = distance
|
||||
self.unit = unit
|
||||
|
||||
def execute(self, data: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
||||
"""执行技能"""
|
||||
# 确保在合适的坐标系中
|
||||
if data.crs and data.crs.is_geographic:
|
||||
data = data.to_crs('EPSG:3857')
|
||||
|
||||
result = data.buffer(self.distance)
|
||||
|
||||
return gpd.GeoDataFrame(
|
||||
geometry=result,
|
||||
crs=data.crs
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"BufferSkill(distance={self.distance}{self.unit})"
|
||||
|
||||
class SiteSelectionSkill:
|
||||
"""选址技能:组合多个原子操作"""
|
||||
|
||||
name = "site_selection"
|
||||
description = "基于多准则的选址分析"
|
||||
|
||||
def __init__(self,
|
||||
distance_from_road: float,
|
||||
distance_from_water: float,
|
||||
min_area: float):
|
||||
self.road_distance = distance_from_road
|
||||
self.water_distance = distance_from_water
|
||||
self.min_area = min_area
|
||||
|
||||
def execute(self,
|
||||
sites: gpd.GeoDataFrame,
|
||||
roads: gpd.GeoDataFrame,
|
||||
water: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
||||
"""
|
||||
执行选址分析
|
||||
|
||||
组合操作:
|
||||
1. 找到距离道路指定范围内的区域
|
||||
2. 排除距离水体太近的区域
|
||||
3. 筛选面积满足要求的区域
|
||||
"""
|
||||
|
||||
# 1. 道路缓冲
|
||||
road_buffer = roads.buffer(self.road_distance)
|
||||
|
||||
# 2. 水体缓冲(排除区)
|
||||
water_buffer = water.buffer(self.water_distance)
|
||||
|
||||
# 3. 找到满足条件的site
|
||||
suitable = sites[
|
||||
sites.geometry.intersects(road_buffer.union_all()) &
|
||||
~sites.geometry.intersects(water_buffer.union_all())
|
||||
]
|
||||
|
||||
# 4. 面积筛选
|
||||
suitable = suitable[suitable.geometry.area >= self.min_area]
|
||||
|
||||
return suitable
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例:构建一个选址分析流水线
|
||||
print("=== 模块化空间分析系统 ===\n")
|
||||
|
||||
# 创建技能
|
||||
buffer_skill = BufferSkill(distance=500, unit='m')
|
||||
print(f"创建技能: {buffer_skill}")
|
||||
|
||||
# 技能可以独立测试
|
||||
print("\n技能的核心优势:")
|
||||
print("1. 可理解性 - 每个技能职责单一")
|
||||
print("2. 可测试性 - 独立测试每个技能")
|
||||
print("3. 可复用性 - 在不同场景中使用")
|
||||
print("4. 可组合性 - 小技能组合成大技能")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### QGIS插件架构分析
|
||||
|
||||
QGIS的模块化设计是学习的好例子:
|
||||
|
||||
```
|
||||
QGIS架构
|
||||
│
|
||||
├── Core (核心库)
|
||||
│ ├── QgsGeometry - 几何操作
|
||||
│ ├── QgsVectorLayer - 矢量图层
|
||||
│ ├── QgsRasterLayer - 栅格图层
|
||||
│ └── QgsProcessing - 处理框架
|
||||
│
|
||||
├── Providers (数据提供者)
|
||||
│ ├── OGR Provider - 矢量数据
|
||||
│ ├── GDAL Provider - 栅格数据
|
||||
│ └── PostGIS Provider - 数据库
|
||||
│
|
||||
├── Plugins (插件)
|
||||
│ ├── 每个插件独立模块
|
||||
│ ├── 通过接口访问核心功能
|
||||
│ └── 可单独安装/卸载
|
||||
│
|
||||
└── Processing Algorithms (处理算法)
|
||||
├── 算法库(600+算法)
|
||||
├── 可组合使用
|
||||
└── 模型构建器
|
||||
```
|
||||
|
||||
**关键设计模式**:
|
||||
|
||||
1. **Provider模式**:数据访问抽象
|
||||
2. **Plugin模式**:功能扩展
|
||||
3. **Algorithm模式**:处理步骤封装
|
||||
|
||||
**AI系统的启发**:
|
||||
|
||||
```python
|
||||
# 类似QGIS的AI技能架构
|
||||
class AISkillRegistry:
|
||||
"""AI技能注册表"""
|
||||
|
||||
def __init__(self):
|
||||
self.skills = {}
|
||||
|
||||
def register(self, skill):
|
||||
"""注册技能"""
|
||||
self.skills[skill.name] = skill
|
||||
|
||||
def get(self, name: str):
|
||||
"""获取技能"""
|
||||
return self.skills.get(name)
|
||||
|
||||
def list_by_category(self, category: str):
|
||||
"""按类别列出技能"""
|
||||
return [s for s in self.skills.values()
|
||||
if s.category == category]
|
||||
|
||||
# 使用
|
||||
registry = AISkillRegistry()
|
||||
registry.register(BufferSkill(distance=100))
|
||||
registry.register(SiteSelectionSkill(...))
|
||||
|
||||
# 查找和使用技能
|
||||
buffer = registry.get("buffer_analysis")
|
||||
result = buffer.execute(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **边界划分**:如何确定一个模块的边界?太小的模块和太大的模块各有什么问题?
|
||||
|
||||
2. **接口设计**:设计一个技能接口时,应该考虑哪些因素?
|
||||
|
||||
3. **复用性**:什么代码值得复用?什么不值得?
|
||||
|
||||
4. **组合爆炸**:当模块数量很大时,如何管理模块之间的依赖?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **重构练习**:找一个你写的复杂函数,将其拆分为多个小函数
|
||||
|
||||
2. **接口设计**:为你熟悉的空间分析操作设计技能接口
|
||||
|
||||
3. **组合挑战**:用5个以下的基本操作组合出10个不同的分析流程
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Refactoring"** (Martin Fowler) - 重构与模块化
|
||||
- **"The Art of Unix Programming"** - 模块化哲学
|
||||
- QGIS Plugin开发指南
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **模块化是管理复杂性的核心方法**
|
||||
2. **函数式组合让小函数构建大功能**
|
||||
3. **技能是智能的封装单元**
|
||||
4. **良好的接口设计是模块化的关键**
|
||||
5. **QGIS的架构是学习的优秀范例**
|
||||
@@ -0,0 +1,719 @@
|
||||
# 01.2 状态与状态机
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 在AI系统中,"状态"到底是什么?
|
||||
> 为什么状态管理是Agent系统的核心?
|
||||
> LangGraph是如何用状态机设计工作流的?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 什么是状态
|
||||
|
||||
**状态**是系统在某一时刻的快照,包含所有影响未来行为的信息:
|
||||
|
||||
```
|
||||
系统状态 = 所有相关的变量值
|
||||
|
||||
例如:生态分析工作流的状态
|
||||
{
|
||||
"input_data": {...}, # 输入数据
|
||||
"current_step": "buffer", # 当前步骤
|
||||
"intermediate_results": {...}, # 中间结果
|
||||
"user_preferences": {...}, # 用户偏好
|
||||
"error_count": 0, # 错误计数
|
||||
"checkpoint_reached": False # 检查点状态
|
||||
}
|
||||
```
|
||||
|
||||
**状态的类型**:
|
||||
|
||||
| 类型 | 说明 | 例子 |
|
||||
|-----|------|------|
|
||||
| 静态状态 | 初始输入,不变化 | 输入文件路径、参数 |
|
||||
| 动态状态 | 运行中变化 | 当前步骤、累积结果 |
|
||||
| 控制状态 | 影响流程走向 | 分支条件、错误标志 |
|
||||
| 会话状态 | 跨请求持久化 | 用户偏好、历史记录 |
|
||||
|
||||
### 为什么状态管理很重要
|
||||
|
||||
**1. 断点续传**
|
||||
|
||||
```python
|
||||
# 没有状态管理:出错后必须从头开始
|
||||
def analysis_without_state():
|
||||
step1()
|
||||
step2() # 如果这里出错
|
||||
step3() # 这些都要重做
|
||||
|
||||
# 有状态管理:可以从断点继续
|
||||
class AnalysisWithState:
|
||||
def __init__(self):
|
||||
self.state = {"current_step": 0}
|
||||
|
||||
def run(self):
|
||||
if self.state["current_step"] < 1:
|
||||
step1()
|
||||
self.state["current_step"] = 1
|
||||
|
||||
if self.state["current_step"] < 2:
|
||||
try:
|
||||
step2()
|
||||
self.state["current_step"] = 2
|
||||
except Exception:
|
||||
# 保存状态,下次可以从这里继续
|
||||
save_state(self.state)
|
||||
raise
|
||||
|
||||
if self.state["current_step"] < 3:
|
||||
step3()
|
||||
```
|
||||
|
||||
**2. 人机协同**
|
||||
|
||||
```python
|
||||
# HITL需要状态来知道在哪里需要人类介入
|
||||
class HITLWorkflow:
|
||||
def __init__(self):
|
||||
self.state = {
|
||||
"step": "identify_sources",
|
||||
"pending_review": True,
|
||||
"sources": None,
|
||||
"human_feedback": None
|
||||
}
|
||||
|
||||
def next_action(self):
|
||||
if self.state["pending_review"]:
|
||||
return "request_human_review"
|
||||
elif self.state["human_feedback"]:
|
||||
return "incorporate_feedback"
|
||||
else:
|
||||
return "proceed_to_next_step"
|
||||
```
|
||||
|
||||
**3. 调试和可解释性**
|
||||
|
||||
```python
|
||||
# 状态历史记录了整个决策过程
|
||||
class StatefulAgent:
|
||||
def __init__(self):
|
||||
self.state_history = []
|
||||
|
||||
def decide(self, context):
|
||||
# 记录状态
|
||||
self.state_history.append({
|
||||
"timestamp": now(),
|
||||
"state": self.state.copy(),
|
||||
"context": context,
|
||||
"decision": None
|
||||
})
|
||||
|
||||
# 做决策
|
||||
decision = self._make_decision(context)
|
||||
self.state_history[-1]["decision"] = decision
|
||||
|
||||
return decision
|
||||
|
||||
def explain(self):
|
||||
"""回溯决策过程"""
|
||||
return self.state_history
|
||||
```
|
||||
|
||||
### 状态机
|
||||
|
||||
**状态机**是描述系统状态转换的模型:
|
||||
|
||||
```
|
||||
┌─────────┐
|
||||
│ 初始 │
|
||||
│ state │
|
||||
└────┬────┘
|
||||
│ event: start
|
||||
↓
|
||||
┌─────────┐
|
||||
│ 加载数据 │
|
||||
└────┬────┘
|
||||
│ success
|
||||
↓
|
||||
┌─────────┐ error ┌─────────┐
|
||||
│ 分析处理 │ ─────────────→│ 错误 │
|
||||
└────┬────┘ └─────────┘
|
||||
│ success │ retry
|
||||
↓ │
|
||||
┌─────────┐ │
|
||||
│ 人类 │ │
|
||||
│ 审查 │ │
|
||||
└────┬────┘ │
|
||||
│ approve │
|
||||
↓ │
|
||||
┌─────────┐ │
|
||||
│ 完成 │←───────────────────────┘
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
**状态机的要素**:
|
||||
1. **状态 (State)**:系统可能处于的情况
|
||||
2. **事件 (Event)**:触发状态转换的条件
|
||||
3. **转换 (Transition)**:从一个状态到另一个状态
|
||||
4. **动作 (Action)**:状态转换时执行的操作
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### LangGraph的状态设计哲学
|
||||
|
||||
LangGraph是构建有状态Agent的框架,其核心思想:
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
|
||||
# 定义状态类型
|
||||
class AnalysisState(TypedDict):
|
||||
"""生态网络分析状态"""
|
||||
|
||||
# 输入数据
|
||||
input_path: str
|
||||
parameters: dict
|
||||
|
||||
# 处理过程
|
||||
current_step: str
|
||||
intermediate_results: dict
|
||||
|
||||
# 人机交互
|
||||
review_requested: bool
|
||||
human_feedback: str
|
||||
|
||||
# 输出
|
||||
final_result: dict
|
||||
errors: list
|
||||
|
||||
# 状态图定义
|
||||
workflow = StateGraph(AnalysisState)
|
||||
|
||||
# 添加节点(处理步骤)
|
||||
workflow.add_node("load_data", load_data_node)
|
||||
workflow.add_node("identify_sources", identify_sources_node)
|
||||
workflow.add_node("human_review", human_review_node)
|
||||
workflow.add_node("extract_corridors", extract_corridors_node)
|
||||
|
||||
# 添加边(状态转换)
|
||||
workflow.add_edge("load_data", "identify_sources")
|
||||
workflow.add_conditional_edge(
|
||||
"identify_sources",
|
||||
should_review, # 条件函数
|
||||
{
|
||||
"review": "human_review",
|
||||
"continue": "extract_corridors"
|
||||
}
|
||||
)
|
||||
|
||||
# 编译为可执行图
|
||||
app = workflow.compile()
|
||||
```
|
||||
|
||||
**核心概念**:
|
||||
|
||||
1. **状态即消息**:状态在节点间传递
|
||||
2. **图即流程**:有向图描述工作流
|
||||
3. **条件分支**:基于状态的动态路由
|
||||
|
||||
### 工作流状态机实现
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, Callable, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
class WorkflowState(Enum):
|
||||
"""工作流状态枚举"""
|
||||
IDLE = "idle"
|
||||
LOADING = "loading"
|
||||
PROCESSING = "processing"
|
||||
REVIEWING = "reviewing"
|
||||
COMPLETED = "completed"
|
||||
ERROR = "error"
|
||||
|
||||
@dataclass
|
||||
class WorkflowContext:
|
||||
"""工作流上下文(状态数据)"""
|
||||
data: Dict[str, Any] = field(default_factory=dict)
|
||||
current_step: int = 0
|
||||
errors: list = field(default_factory=list)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
class StateMachine:
|
||||
"""通用状态机"""
|
||||
|
||||
def __init__(self, initial_state: WorkflowState):
|
||||
self.state = initial_state
|
||||
self.context = WorkflowContext()
|
||||
self.transitions: Dict[WorkflowState, Dict[str, WorkflowState]] = {}
|
||||
self.actions: Dict[tuple[WorkflowState, WorkflowState], Callable] = {}
|
||||
|
||||
def add_transition(self,
|
||||
from_state: WorkflowState,
|
||||
event: str,
|
||||
to_state: WorkflowState,
|
||||
action: Callable = None):
|
||||
"""添加状态转换"""
|
||||
if from_state not in self.transitions:
|
||||
self.transitions[from_state] = {}
|
||||
self.transitions[from_state][event] = to_state
|
||||
|
||||
if action:
|
||||
self.actions[(from_state, to_state)] = action
|
||||
|
||||
def trigger(self, event: str, **kwargs) -> bool:
|
||||
"""触发事件"""
|
||||
if self.state not in self.transitions:
|
||||
raise ValueError(f"没有从状态 {self.state} 的转换")
|
||||
|
||||
if event not in self.transitions[self.state]:
|
||||
print(f"事件 {event} 在状态 {self.state} 下无效")
|
||||
return False
|
||||
|
||||
# 获取目标状态
|
||||
new_state = self.transitions[self.state][event]
|
||||
old_state = self.state
|
||||
|
||||
# 执行转换动作
|
||||
action = self.actions.get((old_state, new_state))
|
||||
if action:
|
||||
result = action(self.context, **kwargs)
|
||||
if result is False: # 动作失败,不转换
|
||||
return False
|
||||
|
||||
# 更新状态
|
||||
self.state = new_state
|
||||
print(f"状态转换: {old_state} → {new_state}")
|
||||
return True
|
||||
|
||||
# 生态分析工作流状态机
|
||||
class EcologicalAnalysisWorkflow:
|
||||
"""生态网络分析工作流"""
|
||||
|
||||
def __init__(self):
|
||||
# 创建状态机
|
||||
self.sm = StateMachine(WorkflowState.IDLE)
|
||||
|
||||
# 定义转换
|
||||
self.sm.add_transition(WorkflowState.IDLE, "start", WorkflowState.LOADING)
|
||||
self.sm.add_transition(WorkflowState.LOADING, "loaded", WorkflowState.PROCESSING)
|
||||
self.sm.add_transition(WorkflowState.LOADING, "error", WorkflowState.ERROR)
|
||||
self.sm.add_transition(WorkflowState.PROCESSING, "complete", WorkflowState.REVIEWING)
|
||||
self.sm.add_transition(WorkflowState.PROCESSING, "error", WorkflowState.ERROR)
|
||||
self.sm.add_transition(WorkflowState.REVIEWING, "approved", WorkflowState.COMPLETED)
|
||||
self.sm.add_transition(WorkflowState.REVIEWING, "rejected", WorkflowState.PROCESSING)
|
||||
self.sm.add_transition(WorkflowState.ERROR, "retry", WorkflowState.LOADING)
|
||||
|
||||
def run(self, data_path: str):
|
||||
"""执行工作流"""
|
||||
|
||||
# 启动
|
||||
self.sm.trigger("start", data_path=data_path)
|
||||
|
||||
# 模拟加载
|
||||
print("加载数据...")
|
||||
self.sm.trigger("loaded")
|
||||
|
||||
# 模拟处理
|
||||
print("处理数据...")
|
||||
self.sm.trigger("complete")
|
||||
|
||||
# 审查
|
||||
print("等待审查...")
|
||||
# 这里会等待人类输入
|
||||
# 假设批准
|
||||
self.sm.trigger("approved")
|
||||
|
||||
print(f"工作流完成,最终状态: {self.sm.state}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 完整的状态机工作流
|
||||
|
||||
```python
|
||||
"""
|
||||
完整的状态机工作流示例
|
||||
"""
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from enum import Enum
|
||||
import time
|
||||
|
||||
class State(Enum):
|
||||
"""状态枚举"""
|
||||
IDLE = "idle"
|
||||
LOAD_DATA = "load_data"
|
||||
IDENTIFY_SOURCES = "identify_sources"
|
||||
BUILD_RESISTANCE = "build_resistance"
|
||||
REVIEW_SOURCES = "review_sources"
|
||||
REVIEW_RESISTANCE = "review_resistance"
|
||||
EXTRACT_CORRIDORS = "extract_corridors"
|
||||
COMPLETED = "completed"
|
||||
ERROR = "error"
|
||||
|
||||
@dataclass
|
||||
class WorkflowState:
|
||||
"""工作流状态数据"""
|
||||
current: State = State.IDLE
|
||||
step_number: int = 0
|
||||
data_path: Optional[str] = None
|
||||
sources: Optional[List[Dict]] = None
|
||||
resistance_weights: Optional[Dict] = None
|
||||
corridors: Optional[List[Dict]] = None
|
||||
errors: List[str] = field(default_factory=list)
|
||||
history: List[Dict] = field(default_factory=list)
|
||||
|
||||
def transition_to(self, new_state: State, action: str = ""):
|
||||
"""状态转换"""
|
||||
old_state = self.current
|
||||
self.current = new_state
|
||||
self.step_number += 1
|
||||
|
||||
# 记录历史
|
||||
self.history.append({
|
||||
"step": self.step_number,
|
||||
"from": old_state.value,
|
||||
"to": new_state.value,
|
||||
"action": action,
|
||||
"timestamp": time.time()
|
||||
})
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""序列化"""
|
||||
return {
|
||||
"current": self.current.value,
|
||||
"step_number": self.step_number,
|
||||
"data_path": self.data_path,
|
||||
"sources": self.sources,
|
||||
"resistance_weights": self.resistance_weights,
|
||||
"corridors": self.corridors,
|
||||
"errors": self.errors,
|
||||
"history": self.history
|
||||
}
|
||||
|
||||
def save(self, path: str):
|
||||
"""保存状态到文件"""
|
||||
with open(path, 'w') as f:
|
||||
json.dump(self.to_dict(), f, indent=2)
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str) -> 'WorkflowState':
|
||||
"""从文件加载状态"""
|
||||
with open(path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 转换State枚举
|
||||
data["current"] = State(data["current"])
|
||||
|
||||
return cls(**{k: v for k, v in data.items() if k != "history"})
|
||||
|
||||
class EcologicalAnalysisAgent:
|
||||
"""生态分析智能体(有状态)"""
|
||||
|
||||
def __init__(self):
|
||||
self.state = WorkflowState()
|
||||
self.review_callbacks = {
|
||||
State.REVIEW_SOURCES: self._review_sources,
|
||||
State.REVIEW_RESISTANCE: self._review_resistance
|
||||
}
|
||||
|
||||
def start(self, data_path: str):
|
||||
"""启动分析"""
|
||||
self.state.data_path = data_path
|
||||
self.state.transition_to(State.LOAD_DATA, "开始加载数据")
|
||||
self._execute_current_step()
|
||||
|
||||
def _execute_current_step(self):
|
||||
"""执行当前状态对应的操作"""
|
||||
handlers = {
|
||||
State.LOAD_DATA: self._handle_load_data,
|
||||
State.IDENTIFY_SOURCES: self._handle_identify_sources,
|
||||
State.BUILD_RESISTANCE: self._handle_build_resistance,
|
||||
State.REVIEW_SOURCES: self._handle_review,
|
||||
State.REVIEW_RESISTANCE: self._handle_review,
|
||||
State.EXTRACT_CORRIDORS: self._handle_extract_corridors,
|
||||
State.COMPLETED: self._handle_completed,
|
||||
State.ERROR: self._handle_error
|
||||
}
|
||||
|
||||
handler = handlers.get(self.state.current)
|
||||
if handler:
|
||||
handler()
|
||||
|
||||
def _handle_load_data(self):
|
||||
"""处理数据加载"""
|
||||
print(f"\n[状态: {self.state.current.value}] 加载数据: {self.state.data_path}")
|
||||
|
||||
# 模拟加载
|
||||
try:
|
||||
# 这里实际会读取文件
|
||||
time.sleep(0.5)
|
||||
print("数据加载成功")
|
||||
self.state.transition_to(State.IDENTIFY_SOURCES, "数据加载完成")
|
||||
self._execute_current_step()
|
||||
except Exception as e:
|
||||
self.state.errors.append(str(e))
|
||||
self.state.transition_to(State.ERROR, f"加载失败: {e}")
|
||||
self._execute_current_step()
|
||||
|
||||
def _handle_identify_sources(self):
|
||||
"""处理源地识别"""
|
||||
print(f"\n[状态: {self.state.current.value}] 识别生态源地...")
|
||||
|
||||
# 模拟识别
|
||||
self.state.sources = [
|
||||
{"id": 1, "area": 1500, "type": "forest"},
|
||||
{"id": 2, "area": 800, "type": "wetland"}
|
||||
]
|
||||
print(f"识别到 {len(self.state.sources)} 个源地")
|
||||
|
||||
self.state.transition_to(State.REVIEW_SOURCES, "源地识别完成,等待审查")
|
||||
self._execute_current_step()
|
||||
|
||||
def _handle_build_resistance(self):
|
||||
"""处理阻力面构建"""
|
||||
print(f"\n[状态: {self.state.current.value}] 构建阻力面...")
|
||||
|
||||
# 模拟构建
|
||||
self.state.resistance_weights = {
|
||||
"forest": 1,
|
||||
"grassland": 10,
|
||||
"urban": 100,
|
||||
"water": 50
|
||||
}
|
||||
print("阻力面构建完成")
|
||||
|
||||
self.state.transition_to(State.REVIEW_RESISTANCE, "阻力面构建完成,等待审查")
|
||||
self._execute_current_step()
|
||||
|
||||
def _handle_review(self):
|
||||
"""处理审查状态"""
|
||||
print(f"\n[状态: {self.state.current.value}] 等待人类审查...")
|
||||
|
||||
callback = self.review_callbacks.get(self.state.current)
|
||||
if callback:
|
||||
result = callback()
|
||||
|
||||
if result == "approve":
|
||||
if self.state.current == State.REVIEW_SOURCES:
|
||||
self.state.transition_to(State.BUILD_RESISTANCE, "审查通过")
|
||||
elif self.state.current == State.REVIEW_RESISTANCE:
|
||||
self.state.transition_to(State.EXTRACT_CORRIDORS, "审查通过")
|
||||
self._execute_current_step()
|
||||
else:
|
||||
# 拒绝,返回上一状态
|
||||
print("审查未通过,重新执行...")
|
||||
# 简化处理:直接继续
|
||||
|
||||
def _review_sources(self) -> str:
|
||||
"""审查源地"""
|
||||
print("\n=== 源地审查 ===")
|
||||
print(f"识别到 {len(self.state.sources)} 个源地:")
|
||||
for s in self.state.sources:
|
||||
print(f" - ID {s['id']}: {s['type']}, 面积 {s['area']}")
|
||||
|
||||
# 实际实现中这里会等待人类输入
|
||||
# 这里模拟自动批准
|
||||
print("\n[模拟] 审查: 批准")
|
||||
return "approve"
|
||||
|
||||
def _review_resistance(self) -> str:
|
||||
"""审查阻力面"""
|
||||
print("\n=== 阻力面审查 ===")
|
||||
print("阻力权重:")
|
||||
for land_type, weight in self.state.resistance_weights.items():
|
||||
print(f" - {land_type}: {weight}")
|
||||
|
||||
print("\n[模拟] 审查: 批准")
|
||||
return "approve"
|
||||
|
||||
def _handle_extract_corridors(self):
|
||||
"""处理廊道提取"""
|
||||
print(f"\n[状态: {self.state.current.value}] 提取生态廊道...")
|
||||
|
||||
# 模拟提取
|
||||
self.state.corridors = [
|
||||
{"from": 1, "to": 2, "length": 3500}
|
||||
]
|
||||
print(f"提取到 {len(self.state.corridors)} 条廊道")
|
||||
|
||||
self.state.transition_to(State.COMPLETED, "分析完成")
|
||||
self._execute_current_step()
|
||||
|
||||
def _handle_completed(self):
|
||||
"""处理完成状态"""
|
||||
print(f"\n[状态: {self.state.current.value}] 工作流完成!")
|
||||
print(f"\n=== 结果摘要 ===")
|
||||
print(f"源地数量: {len(self.state.sources) if self.state.sources else 0}")
|
||||
print(f"廊道数量: {len(self.state.corridors) if self.state.corridors else 0}")
|
||||
print(f"执行步骤: {self.state.step_number}")
|
||||
|
||||
def _handle_error(self):
|
||||
"""处理错误状态"""
|
||||
print(f"\n[状态: {self.state.current.value}] 发生错误")
|
||||
for error in self.state.errors:
|
||||
print(f" - {error}")
|
||||
|
||||
def save_state(self, path: str):
|
||||
"""保存当前状态"""
|
||||
self.state.save(path)
|
||||
print(f"状态已保存到: {path}")
|
||||
|
||||
def resume_from(self, path: str):
|
||||
"""从保存的状态恢复"""
|
||||
self.state = WorkflowState.load(path)
|
||||
print(f"从状态恢复: {self.state.current.value}")
|
||||
print(f"历史步骤: {self.state.step_number}")
|
||||
self._execute_current_step()
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
print("=== 生态分析状态机工作流 ===\n")
|
||||
|
||||
agent = EcologicalAnalysisAgent()
|
||||
|
||||
# 执行工作流
|
||||
agent.start("data.geojson")
|
||||
|
||||
# 可以保存状态
|
||||
# agent.save_state("workflow_state.json")
|
||||
|
||||
# 可以从状态恢复
|
||||
# new_agent = EcologicalAnalysisAgent()
|
||||
# new_agent.resume_from("workflow_state.json")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### LangGraph在空间分析中的应用
|
||||
|
||||
```python
|
||||
"""
|
||||
LangGraph风格的生态网络分析工作流
|
||||
"""
|
||||
from typing import TypedDict, Annotated, Literal
|
||||
from operator import add
|
||||
|
||||
class EcologicalState(TypedDict):
|
||||
"""生态分析状态类型"""
|
||||
messages: Annotated[list, add] # 消息历史
|
||||
input_data: dict
|
||||
sources: list
|
||||
resistance: dict
|
||||
corridors: list
|
||||
next_step: str
|
||||
human_feedback: str
|
||||
|
||||
# 节点函数
|
||||
def load_data_node(state: EcologicalState) -> EcologicalState:
|
||||
"""加载数据节点"""
|
||||
print("执行: load_data")
|
||||
state["sources"] = [{"id": 1, "area": 1000}]
|
||||
state["next_step"] = "identify"
|
||||
return state
|
||||
|
||||
def identify_sources_node(state: EcologicalState) -> EcologicalState:
|
||||
"""识别源地节点"""
|
||||
print("执行: identify_sources")
|
||||
state["sources"] = [{"id": i, "area": i * 100} for i in range(1, 6)]
|
||||
state["next_step"] = "review"
|
||||
return state
|
||||
|
||||
def human_review_node(state: EcologicalState) -> EcologicalState:
|
||||
"""人类审查节点"""
|
||||
print("执行: human_review")
|
||||
print(f"待审查: {state['sources']}")
|
||||
|
||||
# 在实际实现中,这里会等待人类输入
|
||||
state["human_feedback"] = "approved"
|
||||
state["next_step"] = "build_resistance"
|
||||
return state
|
||||
|
||||
def build_resistance_node(state: EcologicalState) -> EcologicalState:
|
||||
"""构建阻力面节点"""
|
||||
print("执行: build_resistance")
|
||||
state["resistance"] = {"forest": 1, "urban": 100}
|
||||
state["next_step"] = "complete"
|
||||
return state
|
||||
|
||||
# 路由函数
|
||||
def should_review(state: EcologicalState) -> Literal["review", "skip"]:
|
||||
"""决定是否需要审查"""
|
||||
if len(state.get("sources", [])) > 3:
|
||||
return "review"
|
||||
return "skip"
|
||||
|
||||
# 条件边
|
||||
def route_after_identify(state: EcologicalState) -> str:
|
||||
"""识别源地后的路由"""
|
||||
if state.get("human_feedback") == "approved":
|
||||
return "build_resistance"
|
||||
return "identify" # 重新识别
|
||||
|
||||
print("""
|
||||
┌──────────────┐
|
||||
│ load_data │
|
||||
└──────┬───────┘
|
||||
│
|
||||
↓
|
||||
┌──────────────┐
|
||||
│identify_sources│
|
||||
└──────┬───────┘
|
||||
│
|
||||
├────→ [review?] ──→ human_review ──┐
|
||||
│ No │
|
||||
↓ ↓
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│build_resistance│◀──────────────────│ approved │
|
||||
└──────────────┘ └──────────────┘
|
||||
""")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **状态粒度**:状态应该有多细?太细会怎样,太粗会怎样?
|
||||
|
||||
2. **持久化策略**:哪些状态需要持久化?什么时候保存状态?
|
||||
|
||||
3. **并发处理**:如果多个Agent协同工作,如何管理共享状态?
|
||||
|
||||
4. **调试**:当状态机出错时,如何调试?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **状态审计**:添加状态转换日志,分析工作流执行路径
|
||||
|
||||
2. **状态压缩**:实现状态序列化/反序列化,支持断点续传
|
||||
|
||||
3. **条件路由**:实现一个带多个分支的状态机
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Designing Data-Intensive Applications"** (Kleppmann) - 状态管理理论
|
||||
- **LangGraph文档** - 实际框架使用
|
||||
- **"State Machine Design Patterns"** - 状态机设计模式
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **状态是系统在某一时刻的完整快照**
|
||||
2. **状态机描述系统如何随事件转换状态**
|
||||
3. **LangGraph用图结构表达有状态的工作流**
|
||||
4. **良好的状态管理支持断点续传和HITL**
|
||||
5. **状态历史是调试和可解释性的关键**
|
||||
@@ -0,0 +1,677 @@
|
||||
# 01.3 概率与不确定性
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 空间分析中的不确定性从何而来?
|
||||
> AI系统如何表示和处理不确定性?
|
||||
> 如何在不确定性下做出稳健的决策?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 不确定性的来源
|
||||
|
||||
在空间分析和AI系统中,不确定性无处不在:
|
||||
|
||||
```
|
||||
空间分析中的不确定性来源
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 1. 数据不确定性 │
|
||||
│ - 测量误差 │
|
||||
│ - 空间采样不完整 │
|
||||
│ - 分类错误 │
|
||||
│ - 时间延迟 │
|
||||
│ │
|
||||
│ 2. 参数不确定性 │
|
||||
│ - 阻力权重不确定 │
|
||||
│ - 阈值选择主观 │
|
||||
│ - 模型参数拟合误差 │
|
||||
│ │
|
||||
│ 3. 结构不确定性 │
|
||||
│ - 模型选择 │
|
||||
│ - 变量关系假设 │
|
||||
│ - 尺度效应 │
|
||||
│ │
|
||||
│ 4. 语义不确定性 │
|
||||
│ - 概念模糊("生态质量"是什么?) │
|
||||
│ - 分类边界不清 │
|
||||
│ - 专家意见分歧 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 不确定性的类型
|
||||
|
||||
| 类型 | 说明 | 例子 |
|
||||
|-----|------|------|
|
||||
| **偶然不确定性** (Aleatoric) | 系统固有的随机性,无法通过更多数据消除 | 降雨量的随机波动 |
|
||||
| **认知不确定性** (Epistemic) | 知识不足导致的不确定性,可通过更多数据减少 | 未调查区域的物种分布 |
|
||||
| **模糊性** (Ambiguity) | 概念或分类的不明确 | "高生态价值"的定义 |
|
||||
| **冲突** (Conflict) | 不同信息源的不一致 | 两个专家给出相反意见 |
|
||||
|
||||
### AI如何处理不确定性
|
||||
|
||||
**传统GIS vs 概率AI**:
|
||||
|
||||
```
|
||||
传统GIS: 确定性输出
|
||||
输入 → [处理] → 单一结果
|
||||
例如:这个区域是/不是生态源地
|
||||
|
||||
概率AI: 概率输出
|
||||
输入 → [处理] → (结果, 置信度)
|
||||
例如:这个区域是生态源地的概率是 0.78 ± 0.12
|
||||
```
|
||||
|
||||
**置信度的表示**:
|
||||
|
||||
```python
|
||||
# 方式1: 点估计 + 置信区间
|
||||
estimate = 0.75
|
||||
confidence_interval = (0.65, 0.85)
|
||||
|
||||
# 方式2: 概率分布
|
||||
from scipy.stats import beta
|
||||
distribution = beta(a=8, b=3) # 基于共8次成功,3次失败
|
||||
|
||||
# 方式3: 分类概率
|
||||
class_probabilities = {
|
||||
"high_suitability": 0.65,
|
||||
"medium_suitability": 0.25,
|
||||
"low_suitability": 0.10
|
||||
}
|
||||
|
||||
# 方式4: 模糊隶属度
|
||||
fuzzy_membership = {
|
||||
"is_source": 0.72,
|
||||
"is_not_source": 0.28
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 不确定性传播
|
||||
|
||||
当多个步骤串联时,不确定性会累积:
|
||||
|
||||
```python
|
||||
"""
|
||||
不确定性传播示例
|
||||
"""
|
||||
import numpy as np
|
||||
from scipy.stats import norm
|
||||
|
||||
class UncertainValue:
|
||||
"""带不确定性的值"""
|
||||
|
||||
def __init__(self, mean, std):
|
||||
self.mean = mean
|
||||
self.std = std
|
||||
|
||||
def __add__(self, other):
|
||||
"""加法:方差相加"""
|
||||
return UncertainValue(
|
||||
self.mean + other.mean,
|
||||
np.sqrt(self.std**2 + other.std**2)
|
||||
)
|
||||
|
||||
def __mul__(self, scalar):
|
||||
"""乘以标量:标准差也乘"""
|
||||
return UncertainValue(
|
||||
self.mean * scalar,
|
||||
self.std * abs(scalar)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.mean:.2f} ± {self.std:.2f}"
|
||||
|
||||
# 示例:源地适宜性评估中的不确定性传播
|
||||
def assess_suitability_with_uncertainty():
|
||||
"""
|
||||
每个指标都有测量不确定性,
|
||||
最终的适宜性评分会累积这些不确定性
|
||||
"""
|
||||
|
||||
# 各项指标(均值 ± 标准差)
|
||||
vegetation_quality = UncertainValue(0.75, 0.10)
|
||||
connectivity = UncertainValue(0.60, 0.15)
|
||||
distance_to_threat = UncertainValue(0.80, 0.08)
|
||||
|
||||
# 加权组合(权重也有不确定性)
|
||||
weights = {
|
||||
"vegetation": 0.4,
|
||||
"connectivity": 0.3,
|
||||
"distance": 0.3
|
||||
}
|
||||
|
||||
# 计算总分(简化传播)
|
||||
total = (vegetation_quality * weights["vegetation"] +
|
||||
connectivity * weights["connectivity"] +
|
||||
distance_to_threat * weights["distance"])
|
||||
|
||||
print("各指标不确定性:")
|
||||
print(f" 植被质量: {vegetation_quality}")
|
||||
print(f" 连通性: {connectivity}")
|
||||
print(f" 威胁距离: {distance_to_threat}")
|
||||
print(f"\n总分: {total}")
|
||||
print(f" 置信区间95%: [{total.mean - 1.96*total.std:.2f}, "
|
||||
f"{total.mean + 1.96*total.std:.2f}]")
|
||||
|
||||
return total
|
||||
|
||||
if __name__ == "__main__":
|
||||
assess_suitability_with_uncertainty()
|
||||
```
|
||||
|
||||
### 敏感性分析
|
||||
|
||||
了解哪些参数对结果影响最大:
|
||||
|
||||
```python
|
||||
"""
|
||||
敏感性分析:识别关键参数
|
||||
"""
|
||||
import numpy as np
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
def sensitivity_analysis(model_fn, param_ranges: Dict[str, Tuple[float, float]],
|
||||
n_samples=1000) -> Dict[str, float]:
|
||||
"""
|
||||
使用蒙特卡洛方法进行敏感性分析
|
||||
|
||||
Args:
|
||||
model_fn: 模型函数,接受参数字典,返回结果
|
||||
param_ranges: 参数范围 {param_name: (min, max)}
|
||||
n_samples: 采样次数
|
||||
|
||||
Returns:
|
||||
各参数的敏感性系数
|
||||
"""
|
||||
results = {param: [] for param in param_ranges}
|
||||
model_outputs = []
|
||||
|
||||
# 蒙特卡洛采样
|
||||
for _ in range(n_samples):
|
||||
# 随机采样参数
|
||||
sample = {k: np.random.uniform(v[0], v[1])
|
||||
for k, v in param_ranges.items()}
|
||||
|
||||
# 记录参数值
|
||||
for param, value in sample.items():
|
||||
results[param].append(value)
|
||||
|
||||
# 计算模型输出
|
||||
output = model_fn(sample)
|
||||
model_outputs.append(output)
|
||||
|
||||
# 计算相关性作为敏感性指标
|
||||
sensitivities = {}
|
||||
for param in param_ranges:
|
||||
correlation = np.corrcoef(results[param], model_outputs)[0, 1]
|
||||
sensitivities[param] = abs(correlation)
|
||||
|
||||
return sensitivities
|
||||
|
||||
# 示例:生态阻力面构建的敏感性分析
|
||||
def resistance_model(params):
|
||||
"""简化的阻力面模型"""
|
||||
# 参数:各土地类型的阻力权重
|
||||
forest_weight = params["forest"]
|
||||
grass_weight = params["grass"]
|
||||
urban_weight = params["urban"]
|
||||
|
||||
# 简化:计算平均阻力
|
||||
# 实际应用中会是空间计算
|
||||
landscape_composition = {
|
||||
"forest": 0.4,
|
||||
"grass": 0.3,
|
||||
"urban": 0.3
|
||||
}
|
||||
|
||||
total_resistance = (
|
||||
forest_weight * landscape_composition["forest"] +
|
||||
grass_weight * landscape_composition["grass"] +
|
||||
urban_weight * landscape_composition["urban"]
|
||||
)
|
||||
|
||||
return total_resistance
|
||||
|
||||
def run_sensitivity_example():
|
||||
"""运行敏感性分析示例"""
|
||||
print("=== 阻力面参数敏感性分析 ===\n")
|
||||
|
||||
# 定义参数范围
|
||||
param_ranges = {
|
||||
"forest": (1, 10),
|
||||
"grass": (10, 50),
|
||||
"urban": (50, 200)
|
||||
}
|
||||
|
||||
# 运行敏感性分析
|
||||
sensitivities = sensitivity_analysis(
|
||||
resistance_model,
|
||||
param_ranges,
|
||||
n_samples=5000
|
||||
)
|
||||
|
||||
# 排序并输出
|
||||
sorted_sens = sorted(sensitivities.items(),
|
||||
key=lambda x: x[1], reverse=True)
|
||||
|
||||
print("参数敏感性排序:")
|
||||
for param, sensitivity in sorted_sens:
|
||||
bar = "█" * int(sensitivity * 30)
|
||||
print(f" {param}: {sensitivity:.3f} {bar}")
|
||||
|
||||
print("\n解释:")
|
||||
print(f" 最敏感的参数是 {sorted_sens[0][0]}")
|
||||
print(f" 应当优先精确确定该参数的值")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_sensitivity_example()
|
||||
```
|
||||
|
||||
### 鲁棒决策
|
||||
|
||||
当不确定性无法消除时,做鲁棒的决策:
|
||||
|
||||
```python
|
||||
"""
|
||||
鲁棒决策:在不确定性下的稳健决策
|
||||
"""
|
||||
from typing import List, Callable
|
||||
import numpy as np
|
||||
|
||||
def robust_decision_scenarios():
|
||||
"""
|
||||
鲁棒决策的几种策略
|
||||
"""
|
||||
|
||||
# 策略1: 最大最小 (Maximin) - 最坏情况最优
|
||||
def maximin(payoff_matrix):
|
||||
"""
|
||||
选择在最坏情况下表现最好的选项
|
||||
|
||||
payoff_matrix: 选项 × 场景 的收益矩阵
|
||||
"""
|
||||
worst_case_outcomes = payoff_matrix.min(axis=1)
|
||||
best_option = worst_case_outcomes.argmax()
|
||||
return best_option, worst_case_outcomes
|
||||
|
||||
# 策略2: 最大平均 (Maximum Expected Value)
|
||||
def max_expected(payoff_matrix, probabilities=None):
|
||||
"""选择期望收益最大的选项"""
|
||||
if probabilities is None:
|
||||
probabilities = np.ones(payoff_matrix.shape[1]) / payoff_matrix.shape[1]
|
||||
|
||||
expected_values = payoff_matrix @ probabilities
|
||||
best_option = expected_values.argmax()
|
||||
return best_option, expected_values
|
||||
|
||||
# 策略3: 最小后悔 (Minimax Regret)
|
||||
def minimax_regret(payoff_matrix):
|
||||
"""选择最小化最大后悔的选项"""
|
||||
# 每个场景的最佳收益
|
||||
best_per_scenario = payoff_matrix.max(axis=0)
|
||||
|
||||
# 后悔矩阵:每个选项在每个场景与最佳的差距
|
||||
regret_matrix = best_per_scenario - payoff_matrix
|
||||
|
||||
# 每个选项的最大后悔
|
||||
max_regret = regret_matrix.max(axis=1)
|
||||
|
||||
# 选择最大后悔最小的选项
|
||||
best_option = max_regret.argmin()
|
||||
return best_option, max_regret
|
||||
|
||||
# 示例:生态廊道选址决策
|
||||
# 选项:3个候选廊道路线
|
||||
# 场景:不同的未来土地变化情景
|
||||
payoff_matrix = np.array([
|
||||
# 情景1 情景2 情景3 情景4
|
||||
[80, 60, 40, 70], # 选项A:穿过森林
|
||||
[50, 90, 70, 50], # 选项B:沿河流
|
||||
[60, 70, 90, 60], # 选项C:绕行城市边缘
|
||||
])
|
||||
|
||||
print("=== 生态廊道选址:鲁棒决策分析 ===\n")
|
||||
print("收益矩阵(廊道质量评分):")
|
||||
print(" 情景1 情景2 情景3 情景4")
|
||||
for i, row in enumerate(payoff_matrix, ord('A')):
|
||||
print(f"选项{i}: {row}")
|
||||
|
||||
print("\n--- 策略1: 最大最小 (最坏情况最优) ---")
|
||||
option, worst = maximin(payoff_matrix)
|
||||
print(f"推荐: 选项{chr(ord('A') + option)}")
|
||||
print(f"各选项最坏情况: {worst}")
|
||||
print(" 原理: 选择在最坏情景下表现最好的")
|
||||
|
||||
print("\n--- 策略2: 最大期望 (平均收益最大) ---")
|
||||
option, expected = max_expected(payoff_matrix)
|
||||
print(f"推荐: 选项{chr(ord('A') + option)}")
|
||||
print(f"各选项期望收益: {expected}")
|
||||
print(" 原理: 选择平均表现最好的")
|
||||
|
||||
print("\n--- 策略3: 最小最大后悔 ---")
|
||||
option, regret = minimax_regret(payoff_matrix)
|
||||
print(f"推荐: 选项{chr(ord('A') + option)}")
|
||||
print(f"各选项最大后悔: {regret}")
|
||||
print(" 原理: 选择让'选错'的后悔最小的")
|
||||
|
||||
return payoff_matrix
|
||||
|
||||
if __name__ == "__main__":
|
||||
robust_decision_scenarios()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 概率源地识别
|
||||
|
||||
```python
|
||||
"""
|
||||
带不确定性的生态源地识别
|
||||
"""
|
||||
import numpy as np
|
||||
from scipy.stats import beta
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
class ProbabilisticSource:
|
||||
"""概率源地:带置信度的源地"""
|
||||
|
||||
def __init__(self, source_id: int, geometry,
|
||||
probability: float, confidence: float):
|
||||
self.id = source_id
|
||||
self.geometry = geometry
|
||||
self.probability = probability # 是源地的概率
|
||||
self.confidence = confidence # 概率估计的置信度
|
||||
|
||||
def __repr__(self):
|
||||
return (f"Source({self.id}, P={self.probability:.2f}±{self.confidence:.2f})")
|
||||
|
||||
class ProbabilisticSourceIdentifier:
|
||||
"""概率源地识别器"""
|
||||
|
||||
def __init__(self, threshold=0.5):
|
||||
self.threshold = threshold
|
||||
|
||||
def identify(self, landscape_data) -> List[ProbabilisticSource]:
|
||||
"""
|
||||
识别源地,返回概率源地
|
||||
|
||||
返回的不是"是/否"的判断,而是"是源地的概率"
|
||||
"""
|
||||
sources = []
|
||||
|
||||
# 模拟:对每个斑块计算是源地的概率
|
||||
for i, patch in enumerate(landscape_data):
|
||||
# 基于多个指标计算概率
|
||||
probability = self._calculate_source_probability(patch)
|
||||
|
||||
# 估计置信度(基于数据质量)
|
||||
confidence = self._estimate_confidence(patch)
|
||||
|
||||
if probability >= self.threshold:
|
||||
source = ProbabilisticSource(
|
||||
source_id=i,
|
||||
geometry=patch['geometry'],
|
||||
probability=probability,
|
||||
confidence=confidence
|
||||
)
|
||||
sources.append(source)
|
||||
|
||||
return sources
|
||||
|
||||
def _calculate_source_probability(self, patch) -> float:
|
||||
"""计算斑块是源地的概率"""
|
||||
# 使用贝叶斯推理
|
||||
# P(是源地|数据) ∝ P(数据|是源地) × P(是源地)
|
||||
|
||||
# 各指标的似然
|
||||
area_likelihood = self._area_likelihood(patch['area'])
|
||||
veg_likelihood = self._vegetation_likelihood(patch['vegetation'])
|
||||
shape_likelihood = self._shape_likelihood(patch['shape_index'])
|
||||
|
||||
# 先验概率
|
||||
prior = 0.3 # 假设30%的斑块可能是源地
|
||||
|
||||
# 后验概率(简化)
|
||||
probability = (area_likelihood * veg_likelihood *
|
||||
shape_likelihood * prior)
|
||||
probability = min(probability, 1.0) # 限制在[0,1]
|
||||
|
||||
return probability
|
||||
|
||||
def _area_likelihood(self, area: float) -> float:
|
||||
"""面积似然:大面积更像源地"""
|
||||
if area > 1000:
|
||||
return 1.0
|
||||
elif area > 500:
|
||||
return 0.7
|
||||
else:
|
||||
return 0.3
|
||||
|
||||
def _vegetation_likelihood(self, veg_quality: float) -> float:
|
||||
"""植被质量似然"""
|
||||
return veg_quality # 假设已归一化到[0,1]
|
||||
|
||||
def _shape_likelihood(self, shape_index: float) -> float:
|
||||
"""形状指数似然:紧凑形状更好"""
|
||||
return 1.0 - min(abs(shape_index - 1.0), 0.5)
|
||||
|
||||
def _estimate_confidence(self, patch) -> float:
|
||||
"""估计概率的置信度"""
|
||||
# 基于数据质量、分辨率等因素
|
||||
data_quality = patch.get('data_quality', 0.8)
|
||||
resolution_factor = patch.get('resolution', 30) / 30 # 归一化
|
||||
|
||||
return data_quality * min(resolution_factor, 1.0)
|
||||
|
||||
def uncertainty_propagation_example():
|
||||
"""不确定性传播示例"""
|
||||
print("=== 不确定性传播示例 ===\n")
|
||||
|
||||
# 创建一些模拟斑块
|
||||
patches = [
|
||||
{'id': 1, 'area': 1200, 'vegetation': 0.85, 'shape_index': 1.2,
|
||||
'geometry': 'POLYGON(...)', 'data_quality': 0.9},
|
||||
{'id': 2, 'area': 800, 'vegetation': 0.75, 'shape_index': 1.5,
|
||||
'geometry': 'POLYGON(...)', 'data_quality': 0.7},
|
||||
{'id': 3, 'area': 400, 'vegetation': 0.65, 'shape_index': 1.8,
|
||||
'geometry': 'POLYGON(...)', 'data_quality': 0.6},
|
||||
]
|
||||
|
||||
identifier = ProbabilisticSourceIdentifier(threshold=0.4)
|
||||
sources = identifier.identify(patches)
|
||||
|
||||
print("识别到的概率源地:")
|
||||
for source in sources:
|
||||
print(f" {source}")
|
||||
|
||||
# 计算整体不确定性
|
||||
if sources:
|
||||
avg_prob = np.mean([s.probability for s in sources])
|
||||
avg_conf = np.mean([s.confidence for s in sources])
|
||||
print(f"\n总体置信度: {avg_conf:.2f}")
|
||||
print(f"平均源地概率: {avg_prob:.2f}")
|
||||
|
||||
# 置信区间
|
||||
margin_of_error = (1 - avg_conf) * 0.2 # 简化计算
|
||||
print(f"源地数量估计: {len(sources)} ± {margin_of_error * len(sources):.1f}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
uncertainty_propagation_example()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### ENAgent中的不确定性处理
|
||||
|
||||
在ENAgent项目中,不确定性处理体现在:
|
||||
|
||||
**1. 源地识别的不确定性**
|
||||
|
||||
```python
|
||||
class ENAgentSourceIdentifier:
|
||||
"""ENAgent的源地识别模块"""
|
||||
|
||||
def identify_with_uncertainty(self, landcover, species_params):
|
||||
"""
|
||||
识别源地,同时估计不确定性
|
||||
|
||||
Returns:
|
||||
sources: 源地列表
|
||||
uncertainty_map: 不确定性空间分布
|
||||
"""
|
||||
sources = []
|
||||
uncertainty_map = np.zeros_like(landcover)
|
||||
|
||||
# 对每个候选斑块
|
||||
for patch in self._candidate_patches(landcover):
|
||||
# 计算适宜性(考虑物种参数)
|
||||
suitability = self._calculate_suitability(patch, species_params)
|
||||
|
||||
# 估计不确定性(来自多个来源)
|
||||
uncertainty = self._estimate_uncertainty(
|
||||
patch,
|
||||
data_quality=landcover.metadata['quality'],
|
||||
species_uncertainty=species_params['uncertainty']
|
||||
)
|
||||
|
||||
# 记录
|
||||
if suitability > self.threshold:
|
||||
sources.append({
|
||||
'geometry': patch,
|
||||
'suitability': suitability,
|
||||
'uncertainty': uncertainty
|
||||
})
|
||||
|
||||
# 更新不确定性地图
|
||||
self._add_to_uncertainty_map(uncertainty_map, patch, uncertainty)
|
||||
|
||||
return sources, uncertainty_map
|
||||
|
||||
def _estimate_uncertainty(self, patch, data_quality, species_uncertainty):
|
||||
"""
|
||||
估计源地识别的不确定性
|
||||
|
||||
来源:
|
||||
1. 数据质量 (data_quality)
|
||||
2. 物种参数的不确定性 (species_uncertainty)
|
||||
3. 分类误差 (classification_error)
|
||||
4. 边界效应 (edge_effect)
|
||||
"""
|
||||
# 组合各种不确定性源
|
||||
uncertainty = np.sqrt(
|
||||
(1 - data_quality)**2 +
|
||||
species_uncertainty**2 +
|
||||
0.1**2 + # 分类误差
|
||||
self._edge_uncertainty(patch)**2
|
||||
)
|
||||
|
||||
return min(uncertainty, 1.0)
|
||||
```
|
||||
|
||||
**2. 阻力面的敏感性分析**
|
||||
|
||||
```python
|
||||
class ResistanceSurfaceSensitivity:
|
||||
"""阻力面敏感性分析"""
|
||||
|
||||
def analyze(self, base_weights, variation_ranges, n_simulations=1000):
|
||||
"""
|
||||
分析阻力面权重对结果的敏感性
|
||||
|
||||
Args:
|
||||
base_weights: 基础权重 {land_type: weight}
|
||||
variation_ranges: 权重变化范围 {land_type: (min, max)}
|
||||
n_simulations: 蒙特卡洛模拟次数
|
||||
|
||||
Returns:
|
||||
sensitivity_results: 敏感性分析结果
|
||||
"""
|
||||
results = []
|
||||
|
||||
for _ in range(n_simulations):
|
||||
# 随机采样权重
|
||||
sample_weights = {}
|
||||
for land_type, (min_w, max_w) in variation_ranges.items():
|
||||
sample_weights[land_type] = np.random.uniform(min_w, max_w)
|
||||
|
||||
# 计算对应的阻力面
|
||||
resistance = self._compute_resistance(sample_weights)
|
||||
|
||||
# 评估结果(例如:平均连通性)
|
||||
connectivity = self._assess_connectivity(resistance)
|
||||
|
||||
results.append({
|
||||
'weights': sample_weights,
|
||||
'connectivity': connectivity
|
||||
})
|
||||
|
||||
# 分析敏感性
|
||||
sensitivity = self._compute_sensitivity(results, base_weights)
|
||||
|
||||
return sensitivity
|
||||
|
||||
def _compute_sensitivity(self, results, base_weights):
|
||||
"""计算各土地类型的敏感性"""
|
||||
# 计算每个权重变化与连通性变化的相关性
|
||||
sensitivities = {}
|
||||
|
||||
for land_type in base_weights:
|
||||
weight_values = [r['weights'][land_type] for r in results]
|
||||
connectivity_values = [r['connectivity'] for r in results]
|
||||
|
||||
correlation = np.corrcoef(weight_values, connectivity_values)[0, 1]
|
||||
sensitivities[land_type] = abs(correlation)
|
||||
|
||||
return sensitivities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **不确定性识别**:在你的项目中,不确定性来自哪些方面?哪些是可减少的,哪些是固有的?
|
||||
|
||||
2. **表示选择**:你应该用标准差、置信区间,还是概率分布?各有什么优劣?
|
||||
|
||||
3. **决策权衡**:当不确定性很高时,你应该继续分析还是寻求更多数据?
|
||||
|
||||
4. **沟通问题**:如何向非专家解释不确定性?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **不确定性审计**:对一个分析流程,识别所有不确定性来源并分类
|
||||
|
||||
2. **敏感性分析**:对你熟悉的空间模型进行敏感性分析
|
||||
|
||||
3. **鲁棒决策**:为你的项目设计一个鲁棒决策框架
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Uncertainty Quantification in Predictive Modeling"** - 不确定性量化的理论基础
|
||||
- **"Flaw of Averages"** (Sam Savage) - 为什么平均值会误导
|
||||
- **空间数据质量标准** - 空间不确定性的行业实践
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **不确定性在空间分析中普遍存在**,有多个来源
|
||||
2. **偶然不确定性无法消除**,认知不确定性可以通过更多数据减少
|
||||
3. **不确定性会传播**,多步骤分析需要考虑累积效应
|
||||
4. **敏感性分析识别关键参数**,优先减少高敏感参数的不确定性
|
||||
5. **鲁棒决策在不确定性下做稳健选择**,而非追求最优
|
||||
@@ -0,0 +1,718 @@
|
||||
# 01.4 反馈与学习
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 系统如何从经验中改进?
|
||||
> 强化学习的基本直觉是什么?
|
||||
> 如何设计一个好的奖励函数?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 反馈循环
|
||||
|
||||
**反馈**是系统学习的基础机制:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐│
|
||||
│ │ Action │ ───→ │ Effect │ ───→ │Reward ││
|
||||
│ └─────────┘ └─────────┘ └─────────┘│
|
||||
│ │ │ │
|
||||
│ │ ┌────────────┐ │ │
|
||||
│ └───────────→│ Update │←──────┘ │
|
||||
│ ↑ │
|
||||
│ │ │
|
||||
│ ┌──────┴──────┐ │
|
||||
│ │ Policy │ │
|
||||
│ │ Improvement│ │
|
||||
│ └─────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**反馈的类型**:
|
||||
|
||||
| 类型 | 说明 | 例子 |
|
||||
|-----|------|------|
|
||||
| **正反馈** | 强化正确行为 | 生态廊道有效,增加类似策略 |
|
||||
| **负反馈** | 抑制错误行为 | 阻力面不合理,调整权重 |
|
||||
| **延迟反馈** | 效果滞后 | 生态工程几年后才见效 |
|
||||
| **隐式反馈** | 未明确标注 | 用户不使用某功能 = 不好用 |
|
||||
|
||||
### 强化学习的直觉
|
||||
|
||||
强化学习(RL)是关于"如何通过试错学习":
|
||||
|
||||
```
|
||||
强化学习核心概念
|
||||
|
||||
智能体 ──→ 动作 ──→ 环境 ──→ 奖励
|
||||
↑ │
|
||||
│ │
|
||||
└─────────────── 观察状态 ←──────────────┘
|
||||
│
|
||||
↓
|
||||
更新策略
|
||||
```
|
||||
|
||||
**关键要素**:
|
||||
|
||||
1. **状态 (State)**:智能体看到的当前情况
|
||||
2. **动作 (Action)**:智能体能做的事情
|
||||
3. **奖励 (Reward)**:动作好坏的即时反馈
|
||||
4. **策略 (Policy)**:状态到动作的映射规则
|
||||
5. **价值函数 (Value)**:对长期收益的估计
|
||||
|
||||
```python
|
||||
# RL的数学直觉
|
||||
|
||||
# 策略:在状态s采取动作a的概率
|
||||
π(a|s) = P(action=a | state=s)
|
||||
|
||||
# 价值函数:从状态s开始的期望累积奖励
|
||||
V(s) = E[Σ γ^t * r_t | s_0 = s]
|
||||
# γ是折扣因子,平衡即时和长期奖励
|
||||
|
||||
# 动作价值函数:在状态s采取动作a后的期望累积奖励
|
||||
Q(s,a) = E[Σ γ^t * r_t | s_0 = s, a_0 = a]
|
||||
|
||||
# 目标:找到最优策略,最大化累积奖励
|
||||
π* = argmax_π V^π(s)
|
||||
```
|
||||
|
||||
### 探索与利用的权衡
|
||||
|
||||
RL中经典的困境:
|
||||
|
||||
```
|
||||
探索 (Explore) vs 利用 (Exploit)
|
||||
|
||||
利用 探索
|
||||
↓ ↓
|
||||
选择已知最好的动作 尝试新动作
|
||||
获得稳定奖励 可能发现更好动作
|
||||
可能错过最优 可能浪费资源
|
||||
```
|
||||
|
||||
**策略**:
|
||||
|
||||
| 策略 | 方法 | 适用场景 |
|
||||
|-----|------|---------|
|
||||
| **ε-greedy** | 以ε概率随机探索 | 通用,简单 |
|
||||
| **Boltzmann** | 按价值概率选择 | 需要细粒度控制 |
|
||||
| **UCB** | 上置信界选择 | 需要理论保证 |
|
||||
| **Thompson Sampling** | 采样后验概率 | 贝叶斯框架 |
|
||||
|
||||
```python
|
||||
def epsilon_greedy_action(q_values, epsilon, n_actions):
|
||||
"""
|
||||
ε-greedy策略
|
||||
|
||||
Args:
|
||||
q_values: 各动作的估计价值
|
||||
epsilon: 探索概率
|
||||
n_actions: 动作数量
|
||||
|
||||
Returns:
|
||||
选择的动作
|
||||
"""
|
||||
if np.random.random() < epsilon:
|
||||
# 探索:随机选择
|
||||
return np.random.randint(n_actions)
|
||||
else:
|
||||
# 利用:选择价值最高的
|
||||
return np.argmax(q_values)
|
||||
|
||||
# ε的衰减策略
|
||||
def epsilon_schedule(initial_epsilon, final_epsilon, total_steps, current_step):
|
||||
"""线性衰减ε"""
|
||||
decay = (initial_epsilon - final_epsilon) / total_steps
|
||||
return max(final_epsilon, initial_epsilon - decay * current_step)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 奖励函数设计
|
||||
|
||||
奖励函数定义了"什么是好的行为":
|
||||
|
||||
```python
|
||||
"""
|
||||
奖励函数设计原则
|
||||
"""
|
||||
|
||||
# 原则1: 清晰明确
|
||||
# 好的奖励
|
||||
def good_reward(ecological_quality):
|
||||
"""生态质量越高,奖励越高"""
|
||||
return ecological_quality
|
||||
|
||||
# 不好的奖励(有歧义)
|
||||
def bad_reward(ecological_quality, cost):
|
||||
"""混合多个目标,可能冲突"""
|
||||
return ecological_quality - cost * 0.001
|
||||
|
||||
# 原则2: 适度塑形 (Reward Shaping)
|
||||
# 不要过度引导,让智能体自己探索
|
||||
|
||||
def shaped_reward(base_reward, intermediate_metric):
|
||||
"""
|
||||
基础奖励 + 形状奖励
|
||||
|
||||
基础奖励:定义最终目标
|
||||
形状奖励:引导到达目标(权重较小)
|
||||
"""
|
||||
return base_reward + 0.1 * intermediate_metric
|
||||
|
||||
# 原则3: 避免奖励黑客 (Reward Hacking)
|
||||
# 防止智能体找到"作弊"方法
|
||||
|
||||
def safe_reward_with_constraints(action_result):
|
||||
"""
|
||||
带约束的奖励
|
||||
"""
|
||||
base_reward = action_result['quality']
|
||||
|
||||
# 如果违反约束,给予惩罚
|
||||
if action_result['violates_constraint']:
|
||||
base_reward -= 100 # 大惩罚
|
||||
|
||||
# 如果使用"作弊"方法,给予惩罚
|
||||
if action_result['uses_exploit']:
|
||||
base_reward -= 50
|
||||
|
||||
return base_reward
|
||||
|
||||
# 原则4: 多目标平衡
|
||||
def multi_objective_reward(ecological, economic, social, weights):
|
||||
"""
|
||||
多目标加权
|
||||
|
||||
Args:
|
||||
ecological: 生态效益
|
||||
economic: 经济效益
|
||||
social: 社会效益
|
||||
weights: 各目标权重
|
||||
|
||||
Returns:
|
||||
综合奖励
|
||||
"""
|
||||
# 归一化到[0,1]
|
||||
normalized = {
|
||||
'eco': min(ecological / 100, 1.0),
|
||||
'eco': min(economic / 1000, 1.0),
|
||||
'soc': min(social / 100, 1.0)
|
||||
}
|
||||
|
||||
total = (weights['eco'] * normalized['eco'] +
|
||||
weights['eco'] * normalized['eco'] +
|
||||
weights['soc'] * normalized['soc'])
|
||||
|
||||
return total
|
||||
```
|
||||
|
||||
### 在空间分析中的应用
|
||||
|
||||
```python
|
||||
class SpatialOptimizerRL:
|
||||
"""
|
||||
用强化学习优化空间布局
|
||||
|
||||
场景:给定区域内选择最优生态廊道路线
|
||||
"""
|
||||
|
||||
def __init__(self, landscape, constraints):
|
||||
self.landscape = landscape
|
||||
self.constraints = constraints
|
||||
|
||||
# 状态空间:当前的廊道路线
|
||||
# 动作空间:下一步走向哪个像元
|
||||
# 奖励:连通性、距离、穿越地类的综合
|
||||
|
||||
def state_representation(self):
|
||||
"""将当前空间格局转换为状态表示"""
|
||||
return {
|
||||
'current_position': self.current_position,
|
||||
'visited_cells': self.visited_cells,
|
||||
'local_context': self._get_local_context()
|
||||
}
|
||||
|
||||
def available_actions(self):
|
||||
"""获取可用的动作"""
|
||||
# 可以向8个方向移动
|
||||
directions = [
|
||||
(0, 1), (1, 0), (0, -1), (-1, 0), # 上下左右
|
||||
(1, 1), (1, -1), (-1, 1), (-1, -1) # 对角
|
||||
]
|
||||
|
||||
actions = []
|
||||
for dx, dy in directions:
|
||||
new_x = self.current_position[0] + dx
|
||||
new_y = self.current_position[1] + dy
|
||||
|
||||
if self._is_valid_move(new_x, new_y):
|
||||
actions.append((new_x, new_y))
|
||||
|
||||
return actions
|
||||
|
||||
def reward_function(self, action, new_state):
|
||||
"""
|
||||
定义奖励函数
|
||||
|
||||
考虑:
|
||||
1. 穿越的土地类型(林地奖励,城市惩罚)
|
||||
2. 距离目标的远近(越近越好)
|
||||
3. 是否到达目标(大奖励)
|
||||
"""
|
||||
x, y = new_state['position']
|
||||
|
||||
# 1. 土地类型奖励/惩罚
|
||||
land_type = self.landscape[y, x]
|
||||
land_rewards = {
|
||||
'forest': 10,
|
||||
'grassland': 5,
|
||||
'wetland': 8,
|
||||
'agriculture': 0,
|
||||
'urban': -50,
|
||||
'water': -20
|
||||
}
|
||||
land_reward = land_rewards.get(land_type, -10)
|
||||
|
||||
# 2. 距离奖励(离目标越近越好)
|
||||
dist_to_goal = self._distance_to_goal(new_state['position'])
|
||||
distance_reward = -dist_to_goal * 0.1
|
||||
|
||||
# 3. 目标到达奖励
|
||||
goal_reward = 0
|
||||
if new_state['position'] == self.goal_position:
|
||||
goal_reward = 1000
|
||||
|
||||
# 4. 约束惩罚
|
||||
constraint_penalty = 0
|
||||
if self._violates_constraint(new_state):
|
||||
constraint_penalty = -100
|
||||
|
||||
# 总奖励
|
||||
total_reward = (land_reward + distance_reward +
|
||||
goal_reward + constraint_penalty)
|
||||
|
||||
return total_reward
|
||||
|
||||
def train(self, n_episodes=1000):
|
||||
"""
|
||||
训练智能体
|
||||
|
||||
使用Q-learning
|
||||
"""
|
||||
q_table = {} # Q值表
|
||||
|
||||
for episode in range(n_episodes):
|
||||
state = self._reset()
|
||||
epsilon = self._epsilon_schedule(episode)
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
# ε-greedy选择动作
|
||||
if np.random.random() < epsilon:
|
||||
action = np.random.choice(self.available_actions())
|
||||
else:
|
||||
# 选择Q值最高的动作
|
||||
q_values = [q_table.get((state, a), 0)
|
||||
for a in self.available_actions()]
|
||||
action = self.available_actions()[np.argmax(q_values)]
|
||||
|
||||
# 执行动作
|
||||
new_state, reward, done = self._step(action)
|
||||
|
||||
# 更新Q值
|
||||
old_q = q_table.get((state, action), 0)
|
||||
max_next_q = max([q_table.get((new_state, a), 0)
|
||||
for a in self.available_actions()] + [0])
|
||||
|
||||
# Q-learning更新公式
|
||||
q_table[(state, action)] = old_q + 0.1 * (
|
||||
reward + 0.99 * max_next_q - old_q
|
||||
)
|
||||
|
||||
state = new_state
|
||||
|
||||
return q_table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 简化的生态网络优化RL
|
||||
|
||||
```python
|
||||
"""
|
||||
简化版:用Q-learning优化生态源地选择
|
||||
"""
|
||||
import numpy as np
|
||||
from typing import List, Dict, Tuple
|
||||
import random
|
||||
|
||||
class EcologicalNetworkOptimizer:
|
||||
"""
|
||||
生态网络优化器(RL简化版)
|
||||
|
||||
问题:从候选源地中选择最优组合
|
||||
- 最大化总生态价值
|
||||
- 满足连通性要求
|
||||
- 预算约束
|
||||
"""
|
||||
|
||||
def __init__(self, candidate_sites: List[Dict], budget: float):
|
||||
self.candidate_sites = candidate_sites
|
||||
self.budget = budget
|
||||
|
||||
# 动作:选择或不选择某个源地
|
||||
self.n_actions = len(candidate_sites)
|
||||
|
||||
# 状态:已选源地列表
|
||||
# 简化:用位掩码表示状态
|
||||
self.n_states = 2 ** self.n_actions
|
||||
|
||||
# Q表
|
||||
self.q_table = np.zeros((self.n_states, self.n_actions))
|
||||
|
||||
def state_to_mask(self, state: int) -> List[bool]:
|
||||
"""状态索引转位掩码"""
|
||||
return [(state >> i) & 1 for i in range(self.n_actions)]
|
||||
|
||||
def mask_to_state(self, mask: List[bool]) -> int:
|
||||
"""位掩码转状态索引"""
|
||||
state = 0
|
||||
for i, bit in enumerate(mask):
|
||||
if bit:
|
||||
state |= (1 << i)
|
||||
return state
|
||||
|
||||
def available_actions(self, state_mask: List[bool]) -> List[int]:
|
||||
"""获取可用动作(未选的源地)"""
|
||||
return [i for i, selected in enumerate(state_mask) if not selected]
|
||||
|
||||
def reward_function(self, state_mask: List[bool]) -> float:
|
||||
"""
|
||||
计算当前选择的奖励
|
||||
|
||||
考虑:
|
||||
1. 总生态价值
|
||||
2. 连通性
|
||||
3. 预算约束
|
||||
"""
|
||||
# 选中源地
|
||||
selected_sites = [self.candidate_sites[i]
|
||||
for i, selected in enumerate(state_mask) if selected]
|
||||
|
||||
if not selected_sites:
|
||||
return 0
|
||||
|
||||
# 1. 总生态价值
|
||||
total_value = sum(site['value'] for site in selected_sites)
|
||||
|
||||
# 2. 连通性(简化:已选源地之间的平均距离)
|
||||
if len(selected_sites) > 1:
|
||||
positions = [(site['x'], site['y']) for site in selected_sites]
|
||||
distances = []
|
||||
for i in range(len(positions)):
|
||||
for j in range(i + 1, len(positions)):
|
||||
dist = np.sqrt((positions[i][0] - positions[j][0])**2 +
|
||||
(positions[i][1] - positions[j][1])**2)
|
||||
distances.append(dist)
|
||||
avg_distance = np.mean(distances)
|
||||
connectivity_reward = -0.1 * avg_distance # 距离越近越好
|
||||
else:
|
||||
connectivity_reward = 0
|
||||
|
||||
# 3. 预算惩罚
|
||||
total_cost = sum(site['cost'] for site in selected_sites)
|
||||
budget_penalty = 0
|
||||
if total_cost > self.budget:
|
||||
budget_penalty = -100 * (total_cost - self.budget) / self.budget
|
||||
|
||||
# 总奖励
|
||||
total_reward = total_value + connectivity_reward + budget_penalty
|
||||
|
||||
return total_reward
|
||||
|
||||
def step(self, state: int, action: int) -> Tuple[int, float, bool]:
|
||||
"""
|
||||
执行一步
|
||||
|
||||
Returns:
|
||||
next_state: 下一个状态
|
||||
reward: 奖励
|
||||
done: 是否结束
|
||||
"""
|
||||
state_mask = self.state_to_mask(state)
|
||||
|
||||
# 执行动作(选择一个源地)
|
||||
if state_mask[action]: # 已经选过了
|
||||
return state, -100, True # 惩罚并结束
|
||||
|
||||
new_mask = state_mask.copy()
|
||||
new_mask[action] = True
|
||||
|
||||
# 计算奖励
|
||||
reward = self.reward_function(new_mask)
|
||||
|
||||
# 检查是否结束(预算用完或所有源地都选了)
|
||||
total_cost = sum(self.candidate_sites[i]['cost']
|
||||
for i, selected in enumerate(new_mask) if selected)
|
||||
done = (total_cost >= self.budget) or (sum(new_mask) == len(new_mask))
|
||||
|
||||
next_state = self.mask_to_state(new_mask)
|
||||
|
||||
return next_state, reward, done
|
||||
|
||||
def train(self, n_episodes=1000, alpha=0.1, gamma=0.99,
|
||||
epsilon_start=1.0, epsilon_end=0.01):
|
||||
"""
|
||||
Q-learning训练
|
||||
|
||||
Args:
|
||||
n_episodes: 训练回合数
|
||||
alpha: 学习率
|
||||
gamma: 折扣因子
|
||||
epsilon_start: 初始探索率
|
||||
epsilon_end: 最终探索率
|
||||
"""
|
||||
for episode in range(n_episodes):
|
||||
# 线性衰减ε
|
||||
epsilon = epsilon_start - (epsilon_start - epsilon_end) * episode / n_episodes
|
||||
|
||||
state = 0 # 初始状态(空)
|
||||
done = False
|
||||
|
||||
while not done:
|
||||
state_mask = self.state_to_mask(state)
|
||||
available = self.available_actions(state_mask)
|
||||
|
||||
if not available:
|
||||
break
|
||||
|
||||
# ε-greedy
|
||||
if np.random.random() < epsilon:
|
||||
action = random.choice(available)
|
||||
else:
|
||||
q_values = [self.q_table[state, a] for a in available]
|
||||
action = available[np.argmax(q_values)]
|
||||
|
||||
# 执行动作
|
||||
next_state, reward, done = self.step(state, action)
|
||||
|
||||
# Q-learning更新
|
||||
old_q = self.q_table[state, action]
|
||||
next_max = np.max(self.q_table[next_state])
|
||||
|
||||
self.q_table[state, action] = old_q + alpha * (
|
||||
reward + gamma * next_max - old_q
|
||||
)
|
||||
|
||||
state = next_state
|
||||
|
||||
# 定期报告
|
||||
if episode % 100 == 0:
|
||||
current_epsilon = epsilon_start - (epsilon_start - epsilon_end) * episode / n_episodes
|
||||
print(f"Episode {episode}, ε={current_epsilon:.3f}, "
|
||||
f"Best Q: {np.max(self.q_table[0]):.2f}")
|
||||
|
||||
return self.q_table
|
||||
|
||||
def get_solution(self) -> List[Dict]:
|
||||
"""获取学习到的最优解"""
|
||||
state = 0
|
||||
state_mask = self.state_to_mask(state)
|
||||
solution = []
|
||||
|
||||
while True:
|
||||
available = self.available_actions(state_mask)
|
||||
if not available:
|
||||
break
|
||||
|
||||
# 选择Q值最高的动作
|
||||
q_values = [self.q_table[state, a] for a in available]
|
||||
action = available[np.argmax(q_values)]
|
||||
|
||||
solution.append(self.candidate_sites[action])
|
||||
state, _, done = self.step(state, action)
|
||||
|
||||
if done:
|
||||
break
|
||||
|
||||
return solution
|
||||
|
||||
# 示例使用
|
||||
def example_usage():
|
||||
"""示例使用"""
|
||||
print("=== 生态网络优化:Q-learning ===\n")
|
||||
|
||||
# 创建候选源地
|
||||
np.random.seed(42)
|
||||
n_candidates = 10
|
||||
candidates = []
|
||||
for i in range(n_candidates):
|
||||
candidates.append({
|
||||
'id': i,
|
||||
'x': np.random.randint(0, 100),
|
||||
'y': np.random.randint(0, 100),
|
||||
'value': np.random.randint(50, 150),
|
||||
'cost': np.random.randint(20, 80)
|
||||
})
|
||||
|
||||
budget = 200
|
||||
|
||||
print(f"候选源地数: {n_candidates}")
|
||||
print(f"预算: {budget}\n")
|
||||
|
||||
# 创建优化器并训练
|
||||
optimizer = EcologicalNetworkOptimizer(candidates, budget)
|
||||
optimizer.train(n_episodes=500)
|
||||
|
||||
# 获取解
|
||||
solution = optimizer.get_solution()
|
||||
|
||||
print("\n=== 最优解 ===")
|
||||
print(f"选择源地数: {len(solution)}")
|
||||
total_value = sum(s['value'] for s in solution)
|
||||
total_cost = sum(s['cost'] for s in solution)
|
||||
print(f"总价值: {total_value}")
|
||||
print(f"总成本: {total_cost}")
|
||||
|
||||
print("\n选择的源地:")
|
||||
for s in solution:
|
||||
print(f" 源地 {s['id']}: 价值={s['value']}, 成本={s['cost']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
example_usage()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### ENAgent中的反馈机制
|
||||
|
||||
```python
|
||||
class ENAgentFeedback:
|
||||
"""
|
||||
ENAgent的反馈机制
|
||||
|
||||
场景:生态网络迭代的改进
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.iteration_history = []
|
||||
self.performance_metrics = []
|
||||
|
||||
def collect_feedback(self, iteration, result, human_feedback):
|
||||
"""
|
||||
收集每轮的反馈
|
||||
|
||||
Args:
|
||||
iteration: 迭代次数
|
||||
result: 本轮结果
|
||||
human_feedback: 人类专家的反馈
|
||||
"""
|
||||
feedback_record = {
|
||||
'iteration': iteration,
|
||||
'result': result,
|
||||
'human_feedback': human_feedback,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
self.iteration_history.append(feedback_record)
|
||||
|
||||
def analyze_feedback(self) -> Dict:
|
||||
"""
|
||||
分析反馈,提取改进建议
|
||||
|
||||
Returns:
|
||||
改进建议
|
||||
"""
|
||||
if not self.iteration_history:
|
||||
return {}
|
||||
|
||||
# 分析模式
|
||||
suggestions = {}
|
||||
|
||||
# 1. 常见问题
|
||||
problem_counts = {}
|
||||
for record in self.iteration_history:
|
||||
for problem in record['human_feedback'].get('problems', []):
|
||||
problem_counts[problem] = problem_counts.get(problem, 0) + 1
|
||||
|
||||
if problem_counts:
|
||||
common_problems = sorted(problem_counts.items(),
|
||||
key=lambda x: x[1], reverse=True)
|
||||
suggestions['common_problems'] = common_problems
|
||||
|
||||
# 2. 趋势分析
|
||||
if len(self.iteration_history) > 1:
|
||||
recent_quality = self.iteration_history[-1]['result']['quality']
|
||||
previous_quality = self.iteration_history[-2]['result']['quality']
|
||||
|
||||
if recent_quality > previous_quality:
|
||||
suggestions['trend'] = 'improving'
|
||||
else:
|
||||
suggestions['trend'] = 'stagnant_or_degrading'
|
||||
|
||||
# 3. 参数调整建议
|
||||
suggestions['parameter_adjustments'] = self._suggest_adjustments()
|
||||
|
||||
return suggestions
|
||||
|
||||
def _suggest_adjustments(self) -> Dict:
|
||||
"""建议参数调整"""
|
||||
# 基于反馈历史,建议如何调整参数
|
||||
# 这是一个简化示例
|
||||
return {
|
||||
'resistance_weights': 'consider adjusting urban weight',
|
||||
'source_threshold': 'might be too high/low'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **延迟奖励**:生态工程的效果多年后才显现,如何设计奖励函数?
|
||||
|
||||
2. **稀疏奖励**:当大多数步骤没有明确反馈时,如何学习?
|
||||
|
||||
3. **多目标冲突**:生态目标和经济目标冲突时,奖励函数如何平衡?
|
||||
|
||||
4. **人类反馈**:如何整合人类专家的定性反馈?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **奖励设计**:为一个你熟悉的任务设计奖励函数
|
||||
|
||||
2. **调试RL**:观察Q表的变化,理解学习过程
|
||||
|
||||
3. **探索策略**:比较不同ε衰减策略的效果
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Reinforcement Learning: An Introduction"** (Sutton & Barto) - RL圣经
|
||||
- **"Algorithms for Decision Making"** (Mykel Kochenderfer) - 决策与RL
|
||||
- **"Reward Shaping"**论文 - 奖励塑形理论
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **反馈是学习的基础机制**,正反馈强化正确行为,负反馈纠正错误
|
||||
2. **强化学习核心**:状态、动作、奖励、策略、价值函数
|
||||
3. **探索vs利用**:经典困境,需要平衡策略
|
||||
4. **奖励函数设计**是RL的关键,定义了"什么是好的行为"
|
||||
5. **在空间分析中**:RL可用于优化布局、路径选择、参数调整
|
||||
@@ -0,0 +1,761 @@
|
||||
# 01.5 人机协同的原理
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 人类和AI各自的优势是什么?如何互补?
|
||||
> 何时需要人类介入?如何设计审查点?
|
||||
> 如何建立和维护对AI系统的信任?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 人类与AI的能力对比
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 人类 vs AI 能力对比 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 能力维度 │ 人类 │ AI │
|
||||
│ ───────────── │ ──────────── │ ───────────────── │
|
||||
│ │
|
||||
│ 模式识别 │ 不擅长大量 │ 非常擅长 │
|
||||
│ │ 数据的模式 │ 大规模模式识别 │
|
||||
│ │
|
||||
│ 语义理解 │ 深度理解 │ 表层理解 │
|
||||
│ │ 上下文关联 │ 统计关联 │
|
||||
│ │
|
||||
│ 创造力 │ 原创性强 │ 组合创新 │
|
||||
│ │ 跳跃思维 │ 已有模式重组 │
|
||||
│ │
|
||||
│ 伦理判断 │ 天然具备 │ 需要显式编码 │
|
||||
│ │ 直觉道德 │ 规则约束 │
|
||||
│ │
|
||||
│ 不确定性处理 │ 直觉判断 │ 概率计算 │
|
||||
│ │ 启发式 │ 量化评估 │
|
||||
│ │
|
||||
│ 知识获取 │ 慢,深度 │ 快,广度 │
|
||||
│ │ 需要学习 │ 即时查询 │
|
||||
│ │
|
||||
│ 注意力控制 │ 有限,易疲劳 │ 不知疲倦 │
|
||||
│ │ 可自主转移 │ 需要任务定义 │
|
||||
│ │
|
||||
│ 可解释性 │ 可事后解释 │ 需要专门设计 │
|
||||
│ │ 理由可能模糊 │ 逻辑清晰 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### HITL的理论基础
|
||||
|
||||
**Human-in-the-Loop (HITL)** 不仅仅是"让人检查结果",而是有理论基础的系统设计方法:
|
||||
|
||||
```
|
||||
HITL的理论支撑
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 1. 互补性原理 │
|
||||
│ 人类和AI有互补优势,结合优于单独使用 │
|
||||
│ │
|
||||
│ 2. 控制论原理 │
|
||||
│ 人类作为反馈回路的一部分,可以校正系统偏差 │
|
||||
│ │
|
||||
│ 3. 信任校准 │
|
||||
│ 通过参与建立对AI能力的准确认知 │
|
||||
│ │
|
||||
│ 4. 价值对齐 │
|
||||
│ 人类介入确保AI行为与人类价值观一致 │
|
||||
│ │
|
||||
│ 5. 责任归属 │
|
||||
│ 人类在关键决策点参与,明确责任边界 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 信任建立的动态
|
||||
|
||||
```
|
||||
信任建立过程
|
||||
|
||||
时间
|
||||
│
|
||||
│ ┌────────────┐
|
||||
│ │ 初始信任 │ 基于声誉、宣传等
|
||||
│ └─────┬──────┘
|
||||
│ │ 第一次使用
|
||||
│ ↓
|
||||
│ ┌────────────┐
|
||||
│ │ 体验信任 │ 基于实际交互
|
||||
│ └─────┬──────┘
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ ↓ ↓
|
||||
│ 成功 失败
|
||||
│ │ │
|
||||
│ ↓ ↓
|
||||
│ ┌─────┐ ┌─────┐
|
||||
│ │信任 │ │不信任│
|
||||
│ │增强 │ │/怀疑 │
|
||||
│ └──┬──┘ └──┬──┘
|
||||
│ │ │
|
||||
│ └──────┬──────┘
|
||||
│ │ 解释/透明度
|
||||
│ ↓
|
||||
│ ┌────────────┐
|
||||
│ │ 校准信任 │ 与能力匹配的信任水平
|
||||
│ └────────────┘
|
||||
│
|
||||
└──────────────────────────→
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 何时需要人类介入
|
||||
|
||||
**决策框架**:根据任务的特性决定介入程度
|
||||
|
||||
```python
|
||||
def human_intervention_necessity(task_characteristics: Dict) -> str:
|
||||
"""
|
||||
评估任务需要人类介入的程度
|
||||
|
||||
Args:
|
||||
task_characteristics: 任务特性描述
|
||||
|
||||
Returns:
|
||||
介入程度: 'full', 'selective', 'minimal', 'none'
|
||||
"""
|
||||
scores = {
|
||||
'consequence': 0, # 后果严重性
|
||||
'uncertainty': 0, # 不确定性
|
||||
'ethical': 0, # 伦理敏感性
|
||||
'complexity': 0, # 复杂度
|
||||
'novelty': 0 # 新颖性
|
||||
}
|
||||
|
||||
# 评估后果严重性
|
||||
if task_characteristics.get('life_critical', False):
|
||||
scores['consequence'] = 3
|
||||
elif task_characteristics.get('economic_impact', 0) > 1000000:
|
||||
scores['consequence'] = 2
|
||||
elif task_characteristics.get('economic_impact', 0) > 100000:
|
||||
scores['consequence'] = 1
|
||||
|
||||
# 评估不确定性
|
||||
uncertainty = task_characteristics.get('uncertainty_level', 'low')
|
||||
scores['uncertainty'] = {'low': 0, 'medium': 1, 'high': 2}[uncertainty]
|
||||
|
||||
# 评估伦理敏感性
|
||||
if task_characteristics.get('ethical_concerns', False):
|
||||
scores['ethical'] = 3
|
||||
|
||||
# 评估复杂度
|
||||
complexity = task_characteristics.get('complexity', 'low')
|
||||
scores['complexity'] = {'low': 0, 'medium': 1, 'high': 2}[complexity]
|
||||
|
||||
# 评估新颖性
|
||||
if task_characteristics.get('novel_situation', False):
|
||||
scores['novelty'] = 2
|
||||
|
||||
# 总分
|
||||
total_score = sum(scores.values())
|
||||
|
||||
# 决定介入程度
|
||||
if total_score >= 10:
|
||||
return 'full' # 完全由人类主导
|
||||
elif total_score >= 6:
|
||||
return 'selective' # 关键点介入
|
||||
elif total_score >= 3:
|
||||
return 'minimal' # 异常时介入
|
||||
else:
|
||||
return 'none' # AI自主执行
|
||||
```
|
||||
|
||||
**ENAgent的三个审查点设计依据**:
|
||||
|
||||
| 审查点 | 任务特性 | 介入理由 |
|
||||
|-------|---------|---------|
|
||||
| 源地识别 | 高不确定性 + 本地知识需求 | 遥感分类可能错误,地面实况重要 |
|
||||
| 阻力权重 | 高价值判断 + 物种特异性 | 不同物种权重差异大,专家知识关键 |
|
||||
| 廊道优化 | 多目标权衡 + 社会影响 | 生态与经济/社会的平衡,人类决策 |
|
||||
|
||||
### 信任校准机制
|
||||
|
||||
```python
|
||||
class TrustCalibration:
|
||||
"""
|
||||
信任校准系统
|
||||
|
||||
目标:让用户的信任水平与AI的实际能力匹配
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.declared_confidence = [] # AI声明的置信度
|
||||
self.actual_performance = [] # 实际表现
|
||||
self.user_trust_level = 0.5 # 用户信任水平
|
||||
|
||||
def record_outcome(self, ai_confidence: float,
|
||||
actual_correct: bool,
|
||||
user_trusted: bool):
|
||||
"""
|
||||
记录一次AI决策的结果
|
||||
|
||||
Args:
|
||||
ai_confidence: AI声明的置信度 [0, 1]
|
||||
actual_correct: 实际是否正确
|
||||
user_trusted: 用户是否信任并采用了AI建议
|
||||
"""
|
||||
self.declared_confidence.append(ai_confidence)
|
||||
self.actual_performance.append(1.0 if actual_correct else 0.0)
|
||||
|
||||
def assess_calibration(self) -> Dict:
|
||||
"""
|
||||
评估AI的校准程度
|
||||
|
||||
Returns:
|
||||
校准报告
|
||||
"""
|
||||
if not self.declared_confidence:
|
||||
return {'status': 'insufficient_data'}
|
||||
|
||||
# 按置信度分组统计
|
||||
confidence_bins = {
|
||||
'high': [], # > 0.8
|
||||
'medium': [], # 0.5-0.8
|
||||
'low': [] # < 0.5
|
||||
}
|
||||
|
||||
for conf, perf in zip(self.declared_confidence, self.actual_performance):
|
||||
if conf > 0.8:
|
||||
confidence_bins['high'].append(perf)
|
||||
elif conf > 0.5:
|
||||
confidence_bins['medium'].append(perf)
|
||||
else:
|
||||
confidence_bins['low'].append(perf)
|
||||
|
||||
# 计算各组的平均实际表现
|
||||
calibration_report = {}
|
||||
for bin_name, performances in confidence_bins.items():
|
||||
if performances:
|
||||
avg_performance = sum(performances) / len(performances)
|
||||
calibration_report[bin_name] = {
|
||||
'ai_declared_range': self._get_bin_range(bin_name),
|
||||
'actual_accuracy': avg_performance,
|
||||
'calibration_gap': avg_performance - self._get_bin_expected(bin_name)
|
||||
}
|
||||
|
||||
return calibration_report
|
||||
|
||||
def _get_bin_range(self, bin_name: str) -> str:
|
||||
ranges = {
|
||||
'high': '> 0.8',
|
||||
'medium': '0.5-0.8',
|
||||
'low': '< 0.5'
|
||||
}
|
||||
return ranges[bin_name]
|
||||
|
||||
def _get_bin_expected(self, bin_name: str) -> float:
|
||||
"""该置信度组的期望表现"""
|
||||
expected = {
|
||||
'high': 0.9,
|
||||
'medium': 0.65,
|
||||
'low': 0.25
|
||||
}
|
||||
return expected[bin_name]
|
||||
|
||||
def recommend_trust_adjustment(self) -> str:
|
||||
"""
|
||||
基于校准结果,建议信任调整
|
||||
|
||||
Returns:
|
||||
调整建议
|
||||
"""
|
||||
calibration = self.assess_calibration()
|
||||
|
||||
if calibration.get('status') == 'insufficient_data':
|
||||
return "需要更多数据来评估"
|
||||
|
||||
overconfident = any(
|
||||
v['calibration_gap'] < -0.1
|
||||
for v in calibration.values()
|
||||
if isinstance(v, dict)
|
||||
)
|
||||
|
||||
underconfident = any(
|
||||
v['calibration_gap'] > 0.1
|
||||
for v in calibration.values()
|
||||
if isinstance(v, dict)
|
||||
)
|
||||
|
||||
if overconfident:
|
||||
return "AI倾向于过度自信,建议降低信任度,增加审查"
|
||||
elif underconfident:
|
||||
return "AI实际表现优于声明,可以增加信任"
|
||||
else:
|
||||
return "AI校准良好,当前信任水平适当"
|
||||
```
|
||||
|
||||
### 审查点设计模式
|
||||
|
||||
```python
|
||||
class CheckpointDesign:
|
||||
"""
|
||||
审查点设计框架
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def design_checkpoint(task_info: Dict) -> Dict:
|
||||
"""
|
||||
为任务设计审查点
|
||||
|
||||
Args:
|
||||
task_info: 任务信息
|
||||
|
||||
Returns:
|
||||
审查点设计
|
||||
"""
|
||||
checkpoint = {
|
||||
'name': task_info['name'],
|
||||
'trigger_condition': None,
|
||||
'information_provided': [],
|
||||
'decision_options': [],
|
||||
'default_action': None,
|
||||
'timeout_handling': None
|
||||
}
|
||||
|
||||
# 1. 触发条件设计
|
||||
checkpoint['trigger_condition'] = CheckpointDesign._design_trigger(task_info)
|
||||
|
||||
# 2. 信息提供设计
|
||||
checkpoint['information_provided'] = CheckpointDesign._design_info_display(task_info)
|
||||
|
||||
# 3. 决策选项设计
|
||||
checkpoint['decision_options'] = CheckpointDesign._design_options(task_info)
|
||||
|
||||
# 4. 默认行为
|
||||
checkpoint['default_action'] = CheckpointDesign._design_default(task_info)
|
||||
|
||||
return checkpoint
|
||||
|
||||
@staticmethod
|
||||
def _design_trigger(task_info: Dict) -> Dict:
|
||||
"""设计触发条件"""
|
||||
return {
|
||||
'type': 'conditional', # always, conditional, on_error
|
||||
'conditions': [
|
||||
'confidence_below_threshold',
|
||||
'conflicting_alternatives',
|
||||
'ethical_concern_detected'
|
||||
],
|
||||
'threshold': task_info.get('confidence_threshold', 0.7)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _design_info_display(task_info: Dict) -> List[str]:
|
||||
"""设计展示给人类的信息"""
|
||||
base_info = [
|
||||
'ai_proposal',
|
||||
'confidence_level',
|
||||
'reasoning_trace'
|
||||
]
|
||||
|
||||
# 根据任务类型添加额外信息
|
||||
if task_info.get('high_stakes', False):
|
||||
base_info.extend([
|
||||
'consequence_analysis',
|
||||
'alternative_options'
|
||||
])
|
||||
|
||||
if task_info.get('uncertain', False):
|
||||
base_info.append('uncertainty_quantification')
|
||||
|
||||
return base_info
|
||||
|
||||
@staticmethod
|
||||
def _design_options(task_info: Dict) -> List[str]:
|
||||
"""设计人类决策选项"""
|
||||
base_options = ['approve', 'reject', 'modify']
|
||||
|
||||
if task_info.get('allow_delegation', False):
|
||||
base_options.append('delegate_to_ai')
|
||||
|
||||
return base_options
|
||||
|
||||
@staticmethod
|
||||
def _design_default(task_info: Dict) -> str:
|
||||
"""设计默认行为(人类不响应时)"""
|
||||
if task_info.get('high_stakes', False):
|
||||
return 'wait_for_human' # 等待人类
|
||||
else:
|
||||
return 'proceed_with_caution' # 谨慎继续
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 完整的HITL工作流实现
|
||||
|
||||
```python
|
||||
"""
|
||||
完整的人机协同工作流实现
|
||||
"""
|
||||
import time
|
||||
from typing import Dict, List, Optional, Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
class HumanDecision(Enum):
|
||||
"""人类决策类型"""
|
||||
APPROVE = "approve"
|
||||
REJECT = "reject"
|
||||
MODIFY = "modify"
|
||||
DEFER = "defer"
|
||||
REQUEST_INFO = "request_info"
|
||||
|
||||
@dataclass
|
||||
class CheckpointResult:
|
||||
"""审查点结果"""
|
||||
checkpoint_name: str
|
||||
decision: HumanDecision
|
||||
modifications: Optional[Dict] = None
|
||||
additional_input: Optional[Dict] = None
|
||||
timestamp: float = None
|
||||
|
||||
class HITLWorkflow:
|
||||
"""
|
||||
人机协同工作流
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.checkpoints: Dict[str, Dict] = {}
|
||||
self.state = {}
|
||||
self.history: List[CheckpointResult] = []
|
||||
|
||||
def add_checkpoint(self,
|
||||
name: str,
|
||||
trigger: Callable,
|
||||
info_formatter: Callable = None,
|
||||
critical: bool = False):
|
||||
"""
|
||||
添加审查点
|
||||
|
||||
Args:
|
||||
name: 审查点名称
|
||||
trigger: 触发条件函数,返回True时需要审查
|
||||
info_formatter: 信息格式化函数
|
||||
critical: 是否为关键审查点
|
||||
"""
|
||||
self.checkpoints[name] = {
|
||||
'trigger': trigger,
|
||||
'info_formatter': info_formatter or (lambda x: x),
|
||||
'critical': critical,
|
||||
'activated': False
|
||||
}
|
||||
|
||||
def execute_step(self,
|
||||
step_name: str,
|
||||
step_function: Callable,
|
||||
**kwargs) -> Dict:
|
||||
"""
|
||||
执行工作流步骤
|
||||
|
||||
Args:
|
||||
step_name: 步骤名称
|
||||
step_function: 执行函数
|
||||
**kwargs: 传递给函数的参数
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
print(f"\n{'='*50}")
|
||||
print(f"执行步骤: {step_name}")
|
||||
print('='*50)
|
||||
|
||||
# 检查是否有审查点
|
||||
checkpoint = self.checkpoints.get(step_name)
|
||||
|
||||
if checkpoint:
|
||||
# 执行步骤
|
||||
result = step_function(self.state, **kwargs)
|
||||
|
||||
# 格式化信息
|
||||
info = checkpoint['info_formatter'](result)
|
||||
|
||||
# 检查是否需要触发审查
|
||||
if checkpoint['trigger'](result, self.state):
|
||||
print(f"\n[审查点触发: {step_name}]")
|
||||
checkpoint['activated'] = True
|
||||
|
||||
# 获取人类决策
|
||||
decision = self._get_human_decision(info, step_name)
|
||||
|
||||
# 记录决策
|
||||
self.history.append(CheckpointResult(
|
||||
checkpoint_name=step_name,
|
||||
decision=decision['type'],
|
||||
modifications=decision.get('modifications'),
|
||||
timestamp=time.time()
|
||||
))
|
||||
|
||||
# 根据决策处理
|
||||
if decision['type'] == HumanDecision.APPROVE:
|
||||
print("✓ 人类批准,继续执行")
|
||||
self.state[step_name] = result
|
||||
|
||||
elif decision['type'] == HumanDecision.REJECT:
|
||||
print("✗ 人类拒绝,回退")
|
||||
return {'status': 'rejected', 'checkpoint': step_name}
|
||||
|
||||
elif decision['type'] == HumanDecision.MODIFY:
|
||||
print("✎ 人类修改结果")
|
||||
result = self._apply_modifications(result, decision['modifications'])
|
||||
self.state[step_name] = result
|
||||
|
||||
elif decision['type'] == HumanDecision.DEFER:
|
||||
print("⏸ 暂停,等待更多信息")
|
||||
return {'status': 'deferred', 'checkpoint': step_name}
|
||||
|
||||
else:
|
||||
print(f"审查点未触发(条件不满足),自动继续")
|
||||
self.state[step_name] = result
|
||||
else:
|
||||
# 没有审查点,直接执行
|
||||
result = step_function(self.state, **kwargs)
|
||||
self.state[step_name] = result
|
||||
|
||||
return result
|
||||
|
||||
def _get_human_decision(self, info: Dict, checkpoint_name: str) -> Dict:
|
||||
"""
|
||||
获取人类决策
|
||||
|
||||
实际实现中可能是GUI、CLI或其他交互方式
|
||||
"""
|
||||
print("\n" + "-"*40)
|
||||
print("信息摘要:")
|
||||
for key, value in info.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
print("\n可用决策:")
|
||||
print(" 1. 批准 (approve)")
|
||||
print(" 2. 拒绝 (reject)")
|
||||
print(" 3. 修改 (modify)")
|
||||
|
||||
# 模拟人类输入
|
||||
# 实际实现中等待真实输入
|
||||
choice = "1" # 默认批准
|
||||
|
||||
decisions = {
|
||||
"1": HumanDecision.APPROVE,
|
||||
"2": HumanDecision.REJECT,
|
||||
"3": HumanDecision.MODIFY
|
||||
}
|
||||
|
||||
return {'type': decisions[choice]}
|
||||
|
||||
def _apply_modifications(self, original: Dict, modifications: Dict) -> Dict:
|
||||
"""应用人类修改"""
|
||||
if modifications:
|
||||
original.update(modifications)
|
||||
return original
|
||||
|
||||
def get_checkpoint_summary(self) -> Dict:
|
||||
"""获取审查点摘要"""
|
||||
return {
|
||||
'total_checkpoints': len(self.checkpoints),
|
||||
'activated_checkpoints': sum(1 for c in self.checkpoints.values() if c['activated']),
|
||||
'human_decisions': [
|
||||
{
|
||||
'checkpoint': r.checkpoint_name,
|
||||
'decision': r.decision.value,
|
||||
'timestamp': r.timestamp
|
||||
}
|
||||
for r in self.history
|
||||
]
|
||||
}
|
||||
|
||||
# 示例:生态网络分析的HITL工作流
|
||||
def ecological_hitl_example():
|
||||
"""生态网络分析HITL示例"""
|
||||
|
||||
workflow = HITLWorkflow("ecological_network_analysis")
|
||||
|
||||
# 步骤1:加载数据(无审查)
|
||||
def load_data(state):
|
||||
print("加载土地利用数据...")
|
||||
return {'data_loaded': True, 'n_pixels': 10000}
|
||||
|
||||
# 步骤2:识别源地(有审查)
|
||||
def identify_sources(state):
|
||||
print("识别生态源地...")
|
||||
sources = [
|
||||
{'id': 1, 'area': 1500, 'confidence': 0.85},
|
||||
{'id': 2, 'area': 800, 'confidence': 0.65},
|
||||
{'id': 3, 'area': 2000, 'confidence': 0.92}
|
||||
]
|
||||
return {'sources': sources, 'n_sources': len(sources)}
|
||||
|
||||
# 审查条件:有低置信度源地时触发
|
||||
def source_trigger(result, state):
|
||||
return any(s['confidence'] < 0.7 for s in result['sources'])
|
||||
|
||||
# 信息格式化
|
||||
def format_source_info(result):
|
||||
return {
|
||||
'识别源地数': result['n_sources'],
|
||||
'平均置信度': sum(s['confidence'] for s in result['sources']) / result['n_sources'],
|
||||
'低置信度源地': [s['id'] for s in result['sources'] if s['confidence'] < 0.7]
|
||||
}
|
||||
|
||||
workflow.add_checkpoint(
|
||||
'identify_sources',
|
||||
trigger=source_trigger,
|
||||
info_formatter=format_source_info,
|
||||
critical=True
|
||||
)
|
||||
|
||||
# 步骤3:构建阻力面(有审查)
|
||||
def build_resistance(state):
|
||||
print("构建阻力面...")
|
||||
return {'weights': {'forest': 1, 'urban': 100}, 'built': True}
|
||||
|
||||
# 审查条件:总是触发
|
||||
def resistance_trigger(result, state):
|
||||
return True # 权重设置总是需要人类审查
|
||||
|
||||
def format_resistance_info(result):
|
||||
return result['weights']
|
||||
|
||||
workflow.add_checkpoint(
|
||||
'build_resistance',
|
||||
trigger=resistance_trigger,
|
||||
info_formatter=format_resistance_info
|
||||
)
|
||||
|
||||
# 执行工作流
|
||||
print("=== 开始执行HITL工作流 ===")
|
||||
|
||||
workflow.execute_step('load_data', load_data)
|
||||
workflow.execute_step('identify_sources', identify_sources)
|
||||
workflow.execute_step('build_resistance', build_resistance)
|
||||
|
||||
# 摘要
|
||||
summary = workflow.get_checkpoint_summary()
|
||||
print("\n=== 工作流摘要 ===")
|
||||
print(f"总审查点: {summary['total_checkpoints']}")
|
||||
print(f"激活审查点: {summary['activated_checkpoints']}")
|
||||
print("人类决策:")
|
||||
for decision in summary['human_decisions']:
|
||||
print(f" {decision['checkpoint']}: {decision['decision']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
ecological_hitl_example()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### ENAgent的审查点实现
|
||||
|
||||
```python
|
||||
class ENAgentHITL:
|
||||
"""
|
||||
ENAgent的人机协同实现
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.review_points = {
|
||||
'source_identification': SourceReview(),
|
||||
'resistance_surface': ResistanceReview(),
|
||||
'corridor_extraction': CorridorReview()
|
||||
}
|
||||
|
||||
class SourceReview:
|
||||
"""源地识别审查"""
|
||||
|
||||
def trigger_condition(self, sources):
|
||||
"""触发条件"""
|
||||
# 条件1:有低置信度源地
|
||||
low_confidence = any(s['confidence'] < 0.7 for s in sources)
|
||||
|
||||
# 条件2:源地数量异常
|
||||
abnormal_count = len(sources) < 3 or len(sources) > 20
|
||||
|
||||
# 条件3:源地分布极不均匀
|
||||
if len(sources) >= 2:
|
||||
areas = [s['area'] for s in sources]
|
||||
area_range = max(areas) - min(areas)
|
||||
uneven = area_range > 10 * sum(areas) / len(areas)
|
||||
else:
|
||||
uneven = False
|
||||
|
||||
return low_confidence or abnormal_count or uneven
|
||||
|
||||
def format_for_review(self, sources):
|
||||
"""格式化信息供审查"""
|
||||
return {
|
||||
'n_sources': len(sources),
|
||||
'sources_by_confidence': sorted(sources,
|
||||
key=lambda x: x['confidence']),
|
||||
'spatial_distribution': self._analyze_distribution(sources),
|
||||
'potential_issues': self._detect_issues(sources)
|
||||
}
|
||||
|
||||
def _detect_issues(self, sources):
|
||||
"""检测潜在问题"""
|
||||
issues = []
|
||||
|
||||
if len(sources) < 3:
|
||||
issues.append("源地数量偏少,可能遗漏重要栖息地")
|
||||
|
||||
low_conf = [s for s in sources if s['confidence'] < 0.7]
|
||||
if low_conf:
|
||||
issues.append(f"{len(low_conf)}个源地置信度低于0.7")
|
||||
|
||||
return issues
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **责任边界**:当HITL系统出错时,责任应该如何划分?
|
||||
|
||||
2. **审查疲劳**:如果审查点太多,人类会产生疲劳,如何平衡?
|
||||
|
||||
3. **信任过度**:如何防止人类过度信任AI而减少必要的审查?
|
||||
|
||||
4. **可解释性**:AI应该如何向人类解释其推理过程?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **审查点设计**:为你熟悉的流程设计审查点
|
||||
|
||||
2. **信任评估**:记录你使用AI工具的经历,评估信任变化
|
||||
|
||||
3. **HITL实现**:实现一个简单的HITL工作流
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Human-in-the-Loop Machine Learning"** - HITL系统设计
|
||||
- **"Human-Centered AI"** (Ben Shneiderman) - 以人为本的AI
|
||||
- **"Explainable AI"**论文集 - 可解释AI研究
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **人类和AI有互补优势**,结合优于单独使用
|
||||
2. **HITL不是妥协**,而是有理论基础的系统设计方法
|
||||
3. **审查点选择关键**:在需要人类独特能力的决策点介入
|
||||
4. **信任需要校准**:让信任水平与实际能力匹配
|
||||
5. **责任必须明确**:关键决策点的人类参与确保责任归属
|
||||
@@ -0,0 +1,181 @@
|
||||
# 第二部分:基础原理
|
||||
|
||||
## 本部分目标
|
||||
|
||||
理解现代AI系统的核心设计原理,超越具体工具:
|
||||
- 智能系统的模块化设计思想
|
||||
- 状态与状态机的设计哲学
|
||||
- 概率思维与不确定性处理
|
||||
- 反馈机制与学习原理
|
||||
- 人机协同的理论基础
|
||||
|
||||
---
|
||||
|
||||
## 章节导航
|
||||
|
||||
| 章节 | 文件 | 核心问题 | 实践 |
|
||||
|-----|------|---------|------|
|
||||
| 01.1 | [智能的模块化视角](./01.1-modular-intelligence.md) | 为什么要模块化?技能如何封装? | QGIS技能架构分析 |
|
||||
| 01.2 | [状态与状态机](./01.2-state-and-state-machines.md) | 状态是什么?为何重要? | 简单工作流状态机 |
|
||||
| 01.3 | [概率与不确定性](./01.3-probability-and-uncertainty.md) | AI如何处理未知? | 生态源地识别不确定性 |
|
||||
| 01.4 | [反馈与学习](./01.4-feedback-and-learning.md) | 系统如何改进? | 生态网络优化示例 |
|
||||
| 01.5 | [人机协同的原理](./01.5-human-ai-collaboration.md) | 何时需要人类介入? | ENAgent审查点设计 |
|
||||
|
||||
---
|
||||
|
||||
## 学习路径
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ 01-foundations │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
↓ ↓ ↓
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│设计思维 │ │系统思维 │ │协作思维 │
|
||||
│01.1, 01.2│ │01.3, 01.4│ │ 01.5 │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
│ │ │
|
||||
└────────────────────┼────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ 综合理解 │
|
||||
│ AI系统设计 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心概念图谱
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ AI系统设计核心 │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────────────┼──────────────────────────────┐
|
||||
│ │ │
|
||||
↓ ↓ ↓
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ 模块化 │ │ 状态机 │ │ 反馈循环 │
|
||||
│ ────────── │ │ ────────── │ │ ────────── │
|
||||
│ 技能封装 │ │ 工作流编排 │ │ 学习优化 │
|
||||
│ 接口设计 │ │ 条件分支 │ │ 奖励信号 │
|
||||
│ 组合模式 │ │ 错误处理 │ │ 探索利用 │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
│ │ │
|
||||
└──────────────────────────────┼──────────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 人机协同层 │
|
||||
│ ───────────────────────────────────────────────── │
|
||||
│ 何时介入 │ 如何信任 │ 责任边界 │ 互补优势 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前置知识
|
||||
|
||||
**必需**:
|
||||
- Python面向对象编程基础
|
||||
- 函数式编程概念(高阶函数、map/reduce)
|
||||
- 基本的数据结构(图、树、字典)
|
||||
|
||||
**有助理解**:
|
||||
- 设计模式基础
|
||||
- 状态机概念
|
||||
- 概率论基础
|
||||
|
||||
---
|
||||
|
||||
## 预计学习时间
|
||||
|
||||
| 阅读类型 | 时间估计 |
|
||||
|---------|---------|
|
||||
| 快速浏览 | 3-4小时 |
|
||||
| 理解性阅读 | 10-15小时 |
|
||||
| 完成所有实践 | 20-25小时 |
|
||||
|
||||
---
|
||||
|
||||
## 章节亮点
|
||||
|
||||
### 01.1 智能的模块化视角
|
||||
- 从QGIS插件架构理解模块化
|
||||
- 函数式组合思想
|
||||
- 技能即能力封装的设计理念
|
||||
|
||||
### 01.2 状态与状态机
|
||||
- 为什么状态管理是核心
|
||||
- LangGraph的状态设计哲学
|
||||
- 工作流的状态机实现
|
||||
|
||||
### 01.3 概率与不确定性
|
||||
- 空间分析中的不确定性来源
|
||||
- 置信度的表示和传播
|
||||
- 鲁棒决策的方法
|
||||
|
||||
### 01.4 反馈与学习
|
||||
- 强化学习的直觉理解
|
||||
- 奖励函数设计原则
|
||||
- 探索与利用的权衡
|
||||
|
||||
### 01.5 人机协同的原理
|
||||
- HITL的理论基础
|
||||
- 信任校准机制
|
||||
- 责任边界划分
|
||||
|
||||
---
|
||||
|
||||
## 实践案例01:用LangGraph构建空间决策工作流
|
||||
|
||||
详见 [practice/langgraph-workflow](./practice/langgraph-workflow/)
|
||||
|
||||
### 实践目标
|
||||
|
||||
1. 理解状态驱动的Agent设计
|
||||
2. 实现一个简单的空间决策工作流
|
||||
3. 添加Human-in-the-Loop审查点
|
||||
4. 处理工作流中的错误和重试
|
||||
|
||||
---
|
||||
|
||||
## 思考框架
|
||||
|
||||
在学习每章时,问自己:
|
||||
|
||||
1. **概念理解**:这个概念解决了什么问题?
|
||||
2. **设计权衡**:为什么这样设计?有哪些替代方案?
|
||||
3. **实际应用**:这个原理在ENAgent中如何体现?
|
||||
4. **迁移思考**:这个原理可以应用到我的工作中吗?
|
||||
|
||||
---
|
||||
|
||||
## 延伸资源
|
||||
|
||||
### 经典阅读
|
||||
- **"Design Patterns"** (GoF) - 设计模式基础
|
||||
- **"Introduction to Automata Theory"** - 状态机理论
|
||||
- **"Reinforcement Learning: An Introduction"** - RL基础
|
||||
|
||||
### 在线资源
|
||||
- LangGraph官方文档
|
||||
- LangChain状态管理指南
|
||||
- Human-in-the-Loop机器学习论文集
|
||||
|
||||
---
|
||||
|
||||
## 关键要点预览
|
||||
|
||||
1. **模块化是管理复杂性的核心方法**
|
||||
2. **状态机是工作流编排的基础抽象**
|
||||
3. **概率思维让AI能处理不确定性**
|
||||
4. **反馈循环是学习和改进的机制**
|
||||
5. **人机协同需要明确的责任边界**
|
||||
|
||||
> "原理是知识的骨架,工具是知识的血肉。骨架不变,血肉可生。"
|
||||
@@ -0,0 +1,370 @@
|
||||
# 实践案例01:用LangGraph构建空间决策工作流
|
||||
|
||||
## 目标
|
||||
|
||||
通过本实践,你将:
|
||||
1. 理解状态驱动的Agent设计
|
||||
2. 实现一个简单的空间决策工作流
|
||||
3. 添加Human-in-the-Loop审查点
|
||||
4. 处理工作流中的错误和重试
|
||||
|
||||
---
|
||||
|
||||
## 背景知识
|
||||
|
||||
### 什么是LangGraph
|
||||
|
||||
LangGraph是构建**有状态**的多Agent应用的框架:
|
||||
|
||||
```
|
||||
核心概念:
|
||||
|
||||
1. State(状态): 在节点间传递的数据
|
||||
2. Node(节点): 处理状态的函数
|
||||
3. Edge(边): 节点之间的连接
|
||||
4. Graph(图): 节点和边组成的完整工作流
|
||||
```
|
||||
|
||||
### 为什么用LangGraph
|
||||
|
||||
- **状态管理**: 自动管理工作流状态
|
||||
- **可视化**: 可以绘制和查看工作流图
|
||||
- **持久化**: 支持中断和恢复
|
||||
- **条件路由**: 基于状态动态选择路径
|
||||
|
||||
---
|
||||
|
||||
## 实践步骤
|
||||
|
||||
### 步骤1:安装依赖
|
||||
|
||||
```bash
|
||||
pip install langgraph langchain-core langchain-anthropic
|
||||
```
|
||||
|
||||
### 步骤2:定义状态
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Annotated, List, Optional
|
||||
from operator import add
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
class EcologicalAnalysisState(TypedDict):
|
||||
"""生态网络分析状态"""
|
||||
|
||||
# 输入
|
||||
input_path: str
|
||||
parameters: dict
|
||||
|
||||
# 处理过程
|
||||
current_step: str
|
||||
intermediate_results: dict
|
||||
|
||||
# 人机交互
|
||||
review_requested: bool
|
||||
human_feedback: Optional[str]
|
||||
|
||||
# 输出
|
||||
final_result: Optional[dict]
|
||||
errors: Annotated[List[str], add]
|
||||
```
|
||||
|
||||
### 步骤3:定义节点
|
||||
|
||||
```python
|
||||
def load_data_node(state: EcologicalAnalysisState) -> EcologicalAnalysisState:
|
||||
"""加载数据节点"""
|
||||
print("执行: load_data")
|
||||
# 实际实现中读取文件
|
||||
return {
|
||||
**state,
|
||||
"current_step": "data_loaded",
|
||||
"intermediate_results": {"data": "loaded"}
|
||||
}
|
||||
|
||||
def identify_sources_node(state: EcologicalAnalysisState) -> EcologicalAnalysisState:
|
||||
"""识别源地节点"""
|
||||
print("执行: identify_sources")
|
||||
# 实际实现中运行源地识别算法
|
||||
sources = [{"id": 1, "area": 1000}, {"id": 2, "area": 800}]
|
||||
return {
|
||||
**state,
|
||||
"current_step": "sources_identified",
|
||||
"intermediate_results": {**state["intermediate_results"], "sources": sources}
|
||||
}
|
||||
|
||||
def human_review_node(state: EcologicalAnalysisState) -> EcologicalAnalysisState:
|
||||
"""人类审查节点"""
|
||||
print("执行: human_review")
|
||||
print(f"待审查: {state['intermediate_results']}")
|
||||
# 实际实现中等待人类输入
|
||||
return {
|
||||
**state,
|
||||
"review_requested": False,
|
||||
"human_feedback": "approved"
|
||||
}
|
||||
|
||||
def build_resistance_node(state: EcologicalAnalysisState) -> EcologicalAnalysisState:
|
||||
"""构建阻力面节点"""
|
||||
print("执行: build_resistance")
|
||||
return {
|
||||
**state,
|
||||
"current_step": "resistance_built"
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤4:定义路由
|
||||
|
||||
```python
|
||||
def should_review(state: EcologicalAnalysisState) -> str:
|
||||
"""决定是否需要审查"""
|
||||
sources = state["intermediate_results"].get("sources", [])
|
||||
if len(sources) > 2: # 源地数量多时需要审查
|
||||
return "review"
|
||||
return "continue"
|
||||
```
|
||||
|
||||
### 步骤5:构建图
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph, END
|
||||
|
||||
def build_workflow():
|
||||
"""构建工作流图"""
|
||||
|
||||
# 创建图
|
||||
workflow = StateGraph(EcologicalAnalysisState)
|
||||
|
||||
# 添加节点
|
||||
workflow.add_node("load_data", load_data_node)
|
||||
workflow.add_node("identify_sources", identify_sources_node)
|
||||
workflow.add_node("human_review", human_review_node)
|
||||
workflow.add_node("build_resistance", build_resistance_node)
|
||||
|
||||
# 设置入口
|
||||
workflow.set_entry_point("load_data")
|
||||
|
||||
# 添加边
|
||||
workflow.add_edge("load_data", "identify_sources")
|
||||
|
||||
# 添加条件边
|
||||
workflow.add_conditional_edges(
|
||||
"identify_sources",
|
||||
should_review,
|
||||
{
|
||||
"review": "human_review",
|
||||
"continue": "build_resistance"
|
||||
}
|
||||
)
|
||||
|
||||
workflow.add_edge("human_review", "build_resistance")
|
||||
workflow.add_edge("build_resistance", END)
|
||||
|
||||
# 编译
|
||||
return workflow.compile()
|
||||
```
|
||||
|
||||
### 步骤6:运行工作流
|
||||
|
||||
```python
|
||||
def run_workflow():
|
||||
"""运行工作流"""
|
||||
|
||||
# 初始状态
|
||||
initial_state = {
|
||||
"input_path": "data.geojson",
|
||||
"parameters": {},
|
||||
"current_step": "start",
|
||||
"intermediate_results": {},
|
||||
"review_requested": False,
|
||||
"human_feedback": None,
|
||||
"final_result": None,
|
||||
"errors": []
|
||||
}
|
||||
|
||||
# 构建并运行
|
||||
app = build_workflow()
|
||||
result = app.invoke(initial_state)
|
||||
|
||||
print("\n=== 最终结果 ===")
|
||||
print(result)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 扩展练习
|
||||
|
||||
### 1. 添加错误处理
|
||||
|
||||
```python
|
||||
def with_error_handling(node_func):
|
||||
"""装饰器:添加错误处理"""
|
||||
def wrapper(state):
|
||||
try:
|
||||
return node_func(state)
|
||||
except Exception as e:
|
||||
return {
|
||||
**state,
|
||||
"errors": [str(e)]
|
||||
}
|
||||
return wrapper
|
||||
|
||||
# 使用
|
||||
@with_error_handling
|
||||
def risky_node(state):
|
||||
# 可能出错的节点
|
||||
...
|
||||
```
|
||||
|
||||
### 2. 添加检查点(持久化)
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
# 创建检查点保存器
|
||||
memory = MemorySaver()
|
||||
|
||||
# 编译时添加检查点
|
||||
app = workflow.compile(checkpointer=memory, interrupt_before=["human_review"])
|
||||
|
||||
# 运行时可以指定thread_id
|
||||
config = {"configurable": {"thread_id": "conversation-1"}}
|
||||
result = app.invoke(initial_state, config=config)
|
||||
```
|
||||
|
||||
### 3. 可视化工作流
|
||||
|
||||
```python
|
||||
from IPython.display import Image, display
|
||||
|
||||
# 生成图
|
||||
app = build_workflow()
|
||||
display(Image(app.get_graph().draw_mermaid_png()))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整代码示例
|
||||
|
||||
```python
|
||||
"""
|
||||
完整的LangGraph空间决策工作流示例
|
||||
"""
|
||||
from typing import TypedDict, Annotated, List, Optional, Literal
|
||||
from operator import add
|
||||
from langgraph.graph import StateGraph, END
|
||||
|
||||
class State(TypedDict):
|
||||
"""工作流状态"""
|
||||
step: int
|
||||
data: Optional[dict]
|
||||
sources: Optional[list]
|
||||
reviewed: bool
|
||||
result: Optional[str]
|
||||
errors: Annotated[List[str], add]
|
||||
|
||||
# 节点函数
|
||||
def load_node(state: State) -> State:
|
||||
"""加载数据"""
|
||||
print(f"[节点: load] 步骤 {state['step']}")
|
||||
return {**state, "step": state["step"] + 1, "data": {"loaded": True}}
|
||||
|
||||
def analyze_node(state: State) -> State:
|
||||
"""分析数据"""
|
||||
print(f"[节点: analyze] 步骤 {state['step']}")
|
||||
return {
|
||||
**state,
|
||||
"step": state["step"] + 1,
|
||||
"sources": [{"id": 1, "value": 100}]
|
||||
}
|
||||
|
||||
def review_node(state: State) -> State:
|
||||
"""人类审查"""
|
||||
print(f"[节点: review] 步骤 {state['step']}")
|
||||
print("等待人类审查...")
|
||||
# 实际实现中等待输入
|
||||
return {**state, "step": state["step"] + 1, "reviewed": True}
|
||||
|
||||
def finalize_node(state: State) -> State:
|
||||
"""完成"""
|
||||
print(f"[节点: finalize] 步骤 {state['step']}")
|
||||
return {**state, "result": "completed"}
|
||||
|
||||
# 路由函数
|
||||
def route_after_analyze(state: State) -> Literal["review", "finalize"]:
|
||||
"""分析后的路由"""
|
||||
if state.get("sources") and len(state["sources"]) > 0:
|
||||
return "review"
|
||||
return "finalize"
|
||||
|
||||
# 构建图
|
||||
def build_graph():
|
||||
"""构建工作流图"""
|
||||
graph = StateGraph(State)
|
||||
|
||||
# 添加节点
|
||||
graph.add_node("load", load_node)
|
||||
graph.add_node("analyze", analyze_node)
|
||||
graph.add_node("review", review_node)
|
||||
graph.add_node("finalize", finalize_node)
|
||||
|
||||
# 添加边
|
||||
graph.set_entry_point("load")
|
||||
graph.add_edge("load", "analyze")
|
||||
|
||||
# 条件边
|
||||
graph.add_conditional_edges(
|
||||
"analyze",
|
||||
route_after_analyze,
|
||||
{"review": "review", "finalize": "finalize"}
|
||||
)
|
||||
|
||||
graph.add_edge("review", "finalize")
|
||||
graph.add_edge("finalize", END)
|
||||
|
||||
return graph.compile()
|
||||
|
||||
# 运行
|
||||
if __name__ == "__main__":
|
||||
print("=== LangGraph 空间决策工作流 ===\n")
|
||||
|
||||
app = build_graph()
|
||||
|
||||
initial_state: State = {
|
||||
"step": 1,
|
||||
"data": None,
|
||||
"sources": None,
|
||||
"reviewed": False,
|
||||
"result": None,
|
||||
"errors": []
|
||||
}
|
||||
|
||||
result = app.invoke(initial_state)
|
||||
|
||||
print(f"\n最终状态: {result['step']}")
|
||||
print(f"结果: {result['result']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思问题
|
||||
|
||||
1. **状态设计**:你的状态中哪些信息是必需的?哪些可以省略?
|
||||
|
||||
2. **节点粒度**:节点应该多大?如何平衡?
|
||||
|
||||
3. **错误处理**:当节点失败时,工作流应该如何处理?
|
||||
|
||||
4. **审查点**:你的工作流中哪些地方需要人类介入?
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
完成这个实践后,你已经:
|
||||
- ✅ 理解了状态驱动的Agent设计
|
||||
- ✅ 实现了一个简单的LangGraph工作流
|
||||
- ✅ 掌握了条件路由的基本方法
|
||||
- ✅ 了解了如何添加HITL审查点
|
||||
|
||||
准备好进入下一章:**02-spatial-intelligence(空间智能)**
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,544 @@
|
||||
# 02.2 空间推理
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 机器如何理解和处理空间关系?
|
||||
> 图算法在空间分析中有哪些应用?
|
||||
> 如何进行连通性分析和路径优化?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 空间关系类型
|
||||
|
||||
```
|
||||
空间关系分类
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 1. 拓扑关系 │
|
||||
│ - 相邻 (Adjacent): A与B共享边界 │
|
||||
│ - 包含 (Contains): A完全包含B │
|
||||
│ - 重叠 (Overlaps): A与B部分重叠 │
|
||||
│ - 相离 (Disjoint): A与B不相交 │
|
||||
│ │
|
||||
│ 2. 距离关系 │
|
||||
│ - 欧氏距离: 直线距离 │
|
||||
│ - 曼哈顿距离: 城市街区距离 │
|
||||
│ - 阻力距离: 穿越不同地形的代价 │
|
||||
│ - 时间距离: 行驶时间成本 │
|
||||
│ │
|
||||
│ 3. 方向关系 │
|
||||
│ - 绝对方向: 北、南、东、西 │
|
||||
│ - 相对方向: 前、后、左、右 │
|
||||
│ - 方位角: 0-360度的精确方向 │
|
||||
│ │
|
||||
│ 4. 模式关系 │
|
||||
│ - 聚集: 要素密集分布 │
|
||||
│ - 离散: 要素分散分布 │
|
||||
│ - 随机: 要素随机分布 │
|
||||
│ - 规则: 要素有规律分布 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 图表示与空间推理
|
||||
|
||||
空间问题常可转换为图问题:
|
||||
|
||||
```
|
||||
空间 → 图的转换
|
||||
|
||||
空间场景 图表示
|
||||
─────────── ───────
|
||||
源地A ──廊道──→ 源地B 节点A ──边──→ 节点B
|
||||
│ │
|
||||
└──廊道──→ 源地C └──边──→ 节点C
|
||||
```
|
||||
|
||||
**空间问题的图抽象**:
|
||||
|
||||
| 空间问题 | 图表示 | 算法 |
|
||||
|---------|--------|------|
|
||||
| 最短路径 | 节点=位置,边=路径 | Dijkstra, A* |
|
||||
| 连通性分析 | 节点=斑块,边=廊道 | BFS, DFS, 并查集 |
|
||||
| 设施选址 | 节点=候选点,边=需求 | p-median, p-center |
|
||||
| 覆盖问题 | 节点=服务点,边=覆盖范围 | 最大覆盖 |
|
||||
| 网络流 | 节点=源/汇,边=管道 | 最大流最小割 |
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 连通性分析
|
||||
|
||||
连通性是生态网络分析的核心:
|
||||
|
||||
```python
|
||||
class ConnectivityAnalyzer:
|
||||
"""
|
||||
连通性分析器
|
||||
|
||||
核心:使用图算法分析空间连通性
|
||||
"""
|
||||
|
||||
def __init__(self, resistance_surface):
|
||||
"""
|
||||
Args:
|
||||
resistance_surface: 阻力面栅格
|
||||
"""
|
||||
self.resistance = resistance_surface
|
||||
self.graph = None
|
||||
|
||||
def build_graph(self):
|
||||
"""将阻力面转换为图"""
|
||||
import networkx as nx
|
||||
|
||||
# 创建图
|
||||
self.graph = nx.Graph()
|
||||
|
||||
rows, cols = self.resistance.shape
|
||||
|
||||
# 添加节点和边
|
||||
for i in range(rows):
|
||||
for j in range(cols):
|
||||
node_id = i * cols + j
|
||||
|
||||
# 添加节点
|
||||
self.graph.add_node(node_id, pos=(i, j))
|
||||
|
||||
# 添加边(8邻域)
|
||||
for di in [-1, 0, 1]:
|
||||
for dj in [-1, 0, 1]:
|
||||
if di == 0 and dj == 0:
|
||||
continue
|
||||
|
||||
ni, nj = i + di, j + dj
|
||||
if 0 <= ni < rows and 0 <= nj < cols:
|
||||
neighbor_id = ni * cols + nj
|
||||
|
||||
# 边权重 = 平均阻力
|
||||
weight = (
|
||||
self.resistance[i, j] +
|
||||
self.resistance[ni, nj]
|
||||
) / 2
|
||||
|
||||
self.graph.add_edge(
|
||||
node_id, neighbor_id,
|
||||
weight=weight
|
||||
)
|
||||
|
||||
return self.graph
|
||||
|
||||
def least_cost_path(self, source, target):
|
||||
"""计算最小阻力路径"""
|
||||
if self.graph is None:
|
||||
self.build_graph()
|
||||
|
||||
# Dijkstra算法
|
||||
path = nx.shortest_path(
|
||||
self.graph,
|
||||
source=source,
|
||||
target=target,
|
||||
weight='weight'
|
||||
)
|
||||
|
||||
return path
|
||||
|
||||
def connectivity_metrics(self, sources):
|
||||
"""
|
||||
计算连通性指标
|
||||
|
||||
Args:
|
||||
sources: 源地节点列表
|
||||
|
||||
Returns:
|
||||
连通性指标字典
|
||||
"""
|
||||
if self.graph is None:
|
||||
self.build_graph()
|
||||
|
||||
metrics = {}
|
||||
|
||||
# 1. 整体连通性 (图的连通分量数)
|
||||
components = list(nx.connected_components(
|
||||
self.graph.subgraph(sources)
|
||||
))
|
||||
metrics['n_components'] = len(components)
|
||||
|
||||
# 2. 最大连通分量大小
|
||||
if components:
|
||||
metrics['largest_component'] = max(len(c) for c in components)
|
||||
else:
|
||||
metrics['largest_component'] = 0
|
||||
|
||||
# 3. 平均最短路径长度
|
||||
if len(sources) > 1:
|
||||
subgraph = self.graph.subgraph(sources)
|
||||
if nx.is_connected(subgraph):
|
||||
metrics['avg_path_length'] = nx.average_shortest_path_length(
|
||||
subgraph, weight='weight'
|
||||
)
|
||||
else:
|
||||
metrics['avg_path_length'] = float('inf')
|
||||
|
||||
# 4. 网络密度
|
||||
n = len(sources)
|
||||
if n > 1:
|
||||
max_edges = n * (n - 1) / 2
|
||||
actual_edges = self.graph.subgraph(sources).number_of_edges()
|
||||
metrics['density'] = actual_edges / max_edges
|
||||
else:
|
||||
metrics['density'] = 0
|
||||
|
||||
return metrics
|
||||
```
|
||||
|
||||
### 最短路径算法
|
||||
|
||||
空间分析中最常用的图算法:
|
||||
|
||||
```python
|
||||
"""
|
||||
最短路径算法比较
|
||||
"""
|
||||
import heapq
|
||||
from typing import Dict, List, Tuple, Set
|
||||
|
||||
class ShortestPathAlgorithms:
|
||||
"""最短路径算法集合"""
|
||||
|
||||
def __init__(self, graph: Dict):
|
||||
"""
|
||||
Args:
|
||||
graph: {node: {neighbor: weight, ...}, ...}
|
||||
"""
|
||||
self.graph = graph
|
||||
|
||||
def dijkstra(self, start: str, goal: str = None) -> Tuple[Dict, Dict]:
|
||||
"""
|
||||
Dijkstra算法:经典最短路径
|
||||
|
||||
适合:非负权重图
|
||||
复杂度:O((V+E)logV)
|
||||
"""
|
||||
# 优先队列:(距离, 节点)
|
||||
pq = [(0, start)]
|
||||
visited = set()
|
||||
distances = {start: 0}
|
||||
parents = {start: None}
|
||||
|
||||
while pq:
|
||||
current_dist, current = heapq.heappop(pq)
|
||||
|
||||
if current in visited:
|
||||
continue
|
||||
visited.add(current)
|
||||
|
||||
if current == goal:
|
||||
break
|
||||
|
||||
for neighbor, weight in self.graph.get(current, {}).items():
|
||||
if neighbor in visited:
|
||||
continue
|
||||
|
||||
new_dist = current_dist + weight
|
||||
|
||||
if new_dist < distances.get(neighbor, float('inf')):
|
||||
distances[neighbor] = new_dist
|
||||
parents[neighbor] = current
|
||||
heapq.heappush(pq, (new_dist, neighbor))
|
||||
|
||||
return distances, parents
|
||||
|
||||
def reconstruct_path(self, parents: Dict, start: str, goal: str) -> List:
|
||||
"""从parents字典重建路径"""
|
||||
path = []
|
||||
current = goal
|
||||
|
||||
while current is not None:
|
||||
path.append(current)
|
||||
current = parents.get(current)
|
||||
|
||||
path.reverse()
|
||||
|
||||
if path[0] == start:
|
||||
return path
|
||||
return []
|
||||
|
||||
def a_star(self, start: str, goal: str,
|
||||
heuristic: callable) -> Tuple[Dict, Dict]:
|
||||
"""
|
||||
A*算法:带启发式的最短路径
|
||||
|
||||
适合:有目标节点的图,有可用启发式
|
||||
复杂度:O(b^d) 实际通常比Dijkstra快
|
||||
"""
|
||||
def h(node):
|
||||
return heuristic(node, goal)
|
||||
|
||||
# f(n) = g(n) + h(n)
|
||||
pq = [(h(start), 0, start)]
|
||||
visited = set()
|
||||
g_score = {start: 0} # 实际距离
|
||||
parents = {start: None}
|
||||
|
||||
while pq:
|
||||
f, g, current = heapq.heappop(pq)
|
||||
|
||||
if current in visited:
|
||||
continue
|
||||
visited.add(current)
|
||||
|
||||
if current == goal:
|
||||
break
|
||||
|
||||
for neighbor, weight in self.graph.get(current, {}).items():
|
||||
if neighbor in visited:
|
||||
continue
|
||||
|
||||
tentative_g = g + weight
|
||||
|
||||
if tentative_g < g_score.get(neighbor, float('inf')):
|
||||
g_score[neighbor] = tentative_g
|
||||
f_score = tentative_g + h(neighbor)
|
||||
parents[neighbor] = current
|
||||
heapq.heappush(pq, (f_score, tentative_g, neighbor))
|
||||
|
||||
return g_score, parents
|
||||
|
||||
# 空间启发式函数
|
||||
def euclidean_heuristic(node_pos: Tuple, goal_pos: Tuple) -> float:
|
||||
"""欧氏距离启发式"""
|
||||
import math
|
||||
return math.sqrt(
|
||||
(node_pos[0] - goal_pos[0])**2 +
|
||||
(node_pos[1] - goal_pos[1])**2
|
||||
)
|
||||
|
||||
def manhattan_heuristic(node_pos: Tuple, goal_pos: Tuple) -> float:
|
||||
"""曼哈顿距离启发式(适合网格)"""
|
||||
return abs(node_pos[0] - goal_pos[0]) + abs(node_pos[1] - goal_pos[1])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 生态廊道识别
|
||||
|
||||
```python
|
||||
"""
|
||||
基于空间推理的生态廊道识别
|
||||
"""
|
||||
import numpy as np
|
||||
from typing import List, Tuple
|
||||
import heapq
|
||||
|
||||
def extract_corridors_mcr(resistance_surface: np.ndarray,
|
||||
sources: List[Tuple[int, int]]) -> List[dict]:
|
||||
"""
|
||||
使用最小累积阻力(MCR)方法提取生态廊道
|
||||
|
||||
Args:
|
||||
resistance_surface: 阻力面栅格
|
||||
sources: 源地坐标列表 [(row, col), ...]
|
||||
|
||||
Returns:
|
||||
廊道列表
|
||||
"""
|
||||
rows, cols = resistance_surface.shape
|
||||
|
||||
# 计算成本距离
|
||||
cost_distance = compute_cost_distance(resistance_surface, sources)
|
||||
|
||||
# 提取廊道(低阻力通道)
|
||||
corridors = []
|
||||
|
||||
for i, source1 in enumerate(sources):
|
||||
for source2 in sources[i+1:]:
|
||||
# 找到两源之间的最低阻力路径
|
||||
path = extract_lowest_resistance_path(
|
||||
cost_distance, resistance_surface, source1, source2
|
||||
)
|
||||
|
||||
if path:
|
||||
corridors.append({
|
||||
'source_a': source1,
|
||||
'source_b': source2,
|
||||
'path': path,
|
||||
'cost': sum(resistance_surface[p] for p in path)
|
||||
})
|
||||
|
||||
return corridors
|
||||
|
||||
def compute_cost_distance(resistance: np.ndarray,
|
||||
sources: List[Tuple[int, int]]) -> np.ndarray:
|
||||
"""
|
||||
计算成本距离(到最近源地的累积阻力)
|
||||
|
||||
使用Dijkstra算法的变种
|
||||
"""
|
||||
rows, cols = resistance.shape
|
||||
cost = np.full((rows, cols), np.inf)
|
||||
|
||||
# 优先队列:(累积成本, row, col)
|
||||
pq = []
|
||||
|
||||
# 初始化源地
|
||||
for source_row, source_col in sources:
|
||||
cost[source_row, source_col] = 0
|
||||
heapq.heappush(pq, (0, source_row, source_col))
|
||||
|
||||
# 8方向
|
||||
directions = [(-1, 0), (1, 0), (0, -1), (0, 1),
|
||||
(-1, -1), (-1, 1), (1, -1), (1, 1)]
|
||||
|
||||
visited = np.zeros((rows, cols), dtype=bool)
|
||||
|
||||
while pq:
|
||||
current_cost, row, col = heapq.heappop(pq)
|
||||
|
||||
if visited[row, col]:
|
||||
continue
|
||||
visited[row, col] = True
|
||||
|
||||
for dr, dc in directions:
|
||||
nr, nc = row + dr, col + dc
|
||||
|
||||
if 0 <= nr < rows and 0 <= nc < cols:
|
||||
# 计算移动成本
|
||||
if dr != 0 and dc != 0: # 对角移动
|
||||
move_cost = resistance[nr, nc] * 1.414
|
||||
else:
|
||||
move_cost = resistance[nr, nc]
|
||||
|
||||
new_cost = current_cost + move_cost
|
||||
|
||||
if new_cost < cost[nr, nc]:
|
||||
cost[nr, nc] = new_cost
|
||||
heapq.heappush(pq, (new_cost, nr, nc))
|
||||
|
||||
return cost
|
||||
|
||||
def extract_lowest_resistance_path(cost_distance: np.ndarray,
|
||||
resistance: np.ndarray,
|
||||
start: Tuple[int, int],
|
||||
end: Tuple[int, int]) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
从成本距离表面提取最低阻力路径
|
||||
"""
|
||||
path = [end]
|
||||
current = end
|
||||
|
||||
while current != start:
|
||||
row, col = current
|
||||
best_neighbor = None
|
||||
best_cost = cost_distance[current]
|
||||
|
||||
# 检查邻域
|
||||
for dr in [-1, 0, 1]:
|
||||
for dc in [-1, 0, 1]:
|
||||
if dr == 0 and dc == 0:
|
||||
continue
|
||||
|
||||
nr, nc = row + dr, col + dc
|
||||
if (0 <= nr < cost_distance.shape[0] and
|
||||
0 <= nc < cost_distance.shape[1]):
|
||||
if cost_distance[nr, nc] < best_cost:
|
||||
best_cost = cost_distance[nr, nc]
|
||||
best_neighbor = (nr, nc)
|
||||
|
||||
if best_neighbor is None:
|
||||
break
|
||||
|
||||
path.append(best_neighbor)
|
||||
current = best_neighbor
|
||||
|
||||
path.reverse()
|
||||
return path if path[0] == start else []
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### ENAgent中的廊道识别
|
||||
|
||||
ENAgent使用空间推理提取生态廊道:
|
||||
|
||||
```python
|
||||
class ENAgentCorridorExtractor:
|
||||
"""ENAgent的廊道提取模块"""
|
||||
|
||||
def extract_corridors(self, mcr_surface, sources, width_threshold=500):
|
||||
"""
|
||||
基于MCR表面提取廊道
|
||||
|
||||
Args:
|
||||
mcr_surface: 最小累积阻力表面
|
||||
sources: 源地列表
|
||||
width_threshold: 廊道最小宽度
|
||||
|
||||
Returns:
|
||||
廊道字典
|
||||
"""
|
||||
corridors = []
|
||||
|
||||
# 对每对源地提取路径
|
||||
for i in range(len(sources)):
|
||||
for j in range(i + 1, len(sources)):
|
||||
path = self._extract_path_between_sources(
|
||||
mcr_surface, sources[i], sources[j]
|
||||
)
|
||||
|
||||
if path:
|
||||
# 分析廊道宽度
|
||||
width = self._calculate_corridor_width(
|
||||
mcr_surface, path
|
||||
)
|
||||
|
||||
if width >= width_threshold:
|
||||
corridors.append({
|
||||
'from': sources[i]['id'],
|
||||
'to': sources[j]['id'],
|
||||
'path': path,
|
||||
'width': width,
|
||||
'quality': self._assess_quality(
|
||||
mcr_surface, path
|
||||
)
|
||||
})
|
||||
|
||||
return corridors
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **算法选择**:什么时候用Dijkstra,什么时候用A*?
|
||||
|
||||
2. **空间尺度**:空间推理如何处理多尺度问题?
|
||||
|
||||
3. **计算效率**:大规模空间数据的图算法如何优化?
|
||||
|
||||
4. **动态变化**:空间环境变化时,如何高效更新推理结果?
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Network Flows"** (Ahuja, Magnanti, Orlin) - 网络流理论
|
||||
- **"Geometric Algorithms"** - 几何算法
|
||||
- NetworkX文档 - Python图算法库
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **空间关系有四类**:拓扑、距离、方向、模式
|
||||
2. **图算法是空间推理的核心工具**
|
||||
3. **连通性分析**使用图的结构特性
|
||||
4. **最短路径**有多个算法变种,各有适用场景
|
||||
5. **MCR分析**本质是图上的最短路径问题
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
# 第三部分:空间智能
|
||||
|
||||
## 本部分目标
|
||||
|
||||
理解AI如何"理解"和操作空间:
|
||||
- 空间数据的多种表征方式
|
||||
- 空间推理的计算方法
|
||||
- 多准则决策分析的原理
|
||||
- 空间优化问题的建模与求解
|
||||
- 不确定性量化的方法
|
||||
|
||||
---
|
||||
|
||||
## 章节导航
|
||||
|
||||
| 章节 | 文件 | 核心内容 | 实践 |
|
||||
|-----|------|---------|------|
|
||||
| 02.1 | [空间表征](./02.1-spatial-representation.md) | 栅格/矢量、图表示、多尺度、空间索引 | QGIS图层处理 |
|
||||
| 02.2 | [空间推理](./02.2-spatial-reasoning.md) | 邻近性、连通性、图算法 | 生态廊道识别 |
|
||||
| 02.3 | [多准则决策](./02.3-multi-criteria-decision.md) | 权重、标准化、敏感性分析 | 生态系统服务评估 |
|
||||
| 02.4 | [空间优化](./02.4-spatial-optimization.md) | 目标函数、约束、启发式搜索 | 生态网络优化 |
|
||||
| 02.5 | [不确定性量化](./02.5-uncertainty-quantification.md) | 不确定性来源、传播、可视化 | 抵抗面敏感性分析 |
|
||||
|
||||
---
|
||||
|
||||
## 核心概念图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 空间智能体系 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ 空间表征 │ ───→ │ 空间推理 │ │
|
||||
│ │ - 栅格/矢量 │ │ - 拓扑关系 │ │
|
||||
│ │ - 图表示 │ │ - 距离/方向 │ │
|
||||
│ │ - 多尺度 │ │ - 邻域分析 │ │
|
||||
│ └───────────────┘ └───────────────┘ │
|
||||
│ │ │ │
|
||||
│ └───────────┬───────────┘ │
|
||||
│ │ │
|
||||
│ ↓ │
|
||||
│ ┌───────────────┐ │
|
||||
│ │ 多准则决策 │ │
|
||||
│ │ - 权重分析 │ │
|
||||
│ │ - 标准化 │ │
|
||||
│ │ - 敏感性 │ │
|
||||
│ └───────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────┴───────────┐ │
|
||||
│ ↓ ↓ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ 空间优化 │ │ 不确定性量化 │ │
|
||||
│ │ - 目标函数 │ │ - 误差传播 │ │
|
||||
│ │ - 约束处理 │ │ - 蒙特卡洛 │ │
|
||||
│ │ - 启发式 │ │ - 可视化 │ │
|
||||
│ └───────────────┘ └───────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实践案例
|
||||
|
||||
### 实践案例02:构建生态系统服务评估Skill
|
||||
|
||||
详见 [practice/ecosystem-service-skill](./practice/ecosystem-service-skill/)
|
||||
|
||||
### 实践案例03:最小累积阻力(MCR)分析的自动化
|
||||
|
||||
详见 [practice/mcr-automation](./practice/mcr-automation/)
|
||||
|
||||
---
|
||||
|
||||
## 关键要点预览
|
||||
|
||||
1. **空间表征**是空间智能的基础,选择合适的表征方式至关重要
|
||||
2. **空间推理**基于几何和拓扑关系,是空间分析的核心算法
|
||||
3. **多准则决策**平衡多个目标,需要合理的权重设计和敏感性分析
|
||||
4. **空间优化**寻找最优空间配置,是决策支持的关键
|
||||
5. **不确定性量化**让分析结果更可靠,支持稳健决策
|
||||
|
||||
---
|
||||
|
||||
## 延伸资源
|
||||
|
||||
- **"Geographic Information Systems and Science"** (Longley) - GIS基础理论
|
||||
- **"Spatial Analysis"** (O'Sullivan) - 空间分析方法
|
||||
- **"Geocomputation with R"** - 空间计算实践
|
||||
@@ -0,0 +1,634 @@
|
||||
# 03.1 工作流编排原理
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 如何设计复杂的多步骤分析流程?
|
||||
> DAG(有向无环图)如何表达工作流?
|
||||
> 如何处理工作流中的条件分支和错误?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 工作流编排的核心
|
||||
|
||||
工作流编排是指**协调多个处理步骤按顺序执行**的能力:
|
||||
|
||||
```
|
||||
简单顺序 复杂编排
|
||||
│ │
|
||||
↓ ↓
|
||||
┌─────┐ ┌─────┐ ┌─────┐ ╔═════════════════╗
|
||||
│Step1│───→│Step2│───→│Step3│ ║ 条件分支 ║
|
||||
└─────┘ └─────┘ └─────┘ ║ ║
|
||||
║ ┌─────┐ ║
|
||||
一维流程 ║ │ │ ║
|
||||
║ ↓ ↓ ║
|
||||
║ Yes No ║
|
||||
║ │ │ ║
|
||||
║ ↓ ↓ ║
|
||||
║┌─────┐ ┌─────┐ ║
|
||||
║│StepA│ │StepB│ ║
|
||||
║└─────┘ └─────┘ ║
|
||||
║ │ ║
|
||||
║ └────┬─── ║
|
||||
║ │ ║
|
||||
╔═════════════════╝
|
||||
```
|
||||
|
||||
### DAG:有向无环图
|
||||
|
||||
**DAG** (Directed Acyclic Graph) 是工作流编排的基础数据结构:
|
||||
|
||||
```python
|
||||
"""
|
||||
DAG的数学表示
|
||||
|
||||
DAG = (V, E)
|
||||
其中:
|
||||
- V: 节点集合(处理步骤)
|
||||
- E: 边集合(依赖关系)
|
||||
- 条件:无环(没有节点能通过边回到自己)
|
||||
|
||||
性质:
|
||||
1. 有方向:边从上游指向下游
|
||||
2. 无环:没有循环依赖
|
||||
3. 可拓扑排序:可以找到线性执行顺序
|
||||
"""
|
||||
```
|
||||
|
||||
**为什么DAG适合工作流?**
|
||||
|
||||
| 特性 | 说明 |
|
||||
|-----|------|
|
||||
| **明确依赖** | 边定义了步骤间的依赖关系 |
|
||||
| **可并行化** | 无依赖的步骤可并行执行 |
|
||||
| **可验证** | 可以检测循环依赖 |
|
||||
| **可可视化** | 容易理解和调试 |
|
||||
|
||||
### 工作流的组成要素
|
||||
|
||||
```
|
||||
工作流 = 节点 + 边 + 条件 + 错误处理
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 节点 │ ←───────────────────────────────────── │
|
||||
│ │ ────────── │ │
|
||||
│ │ - 执行函数 │ 输入 → 处理 → 输出 │
|
||||
│ │ - 输入/输出 │ │
|
||||
│ │ - 副后置 │ before(), execute(), after() │
|
||||
│ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ↓ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 边 │ ←───────────────────────────────────── │
|
||||
│ │ ────────── │ │
|
||||
│ │ - 数据流 │ 上游输出 → 下游输入 │
|
||||
│ │ - 依赖关系 │ 顺序执行 │
|
||||
│ │ - 条件路由 │ 基于状态选择路径 │
|
||||
│ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ↓ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 条件分支 │ ←───────────────────────────────────── │
|
||||
│ │ ────────── │ │
|
||||
│ │ - 分支条件 │ if state.value > threshold: ... │
|
||||
│ │ - 路由选择 │ switch-case模式 │
|
||||
│ │ - 合并点 │ 多路径汇聚 │
|
||||
│ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ↓ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 错误处理 │ ←───────────────────────────────────── │
|
||||
│ │ ────────── │ │
|
||||
│ │ - 重试 │ 失败后重新执行 │
|
||||
│ │ - 回滚 │ 恢复到之前状态 │
|
||||
│ │ - 降级 │ 使用备选方案 │
|
||||
│ │ - 告警 │ 通知相关人员 │
|
||||
│ └──────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 工作流设计模式
|
||||
|
||||
**1. 线性流水线 (Linear Pipeline)**
|
||||
|
||||
```python
|
||||
class LinearWorkflow:
|
||||
"""最简单的工作流:顺序执行"""
|
||||
|
||||
def __init__(self):
|
||||
self.steps = []
|
||||
|
||||
def add_step(self, func, name=None):
|
||||
"""添加步骤"""
|
||||
self.steps.append({
|
||||
'function': func,
|
||||
'name': name or func.__name__
|
||||
})
|
||||
return self
|
||||
|
||||
def execute(self, initial_data):
|
||||
"""执行工作流"""
|
||||
result = initial_data
|
||||
|
||||
for step in self.steps:
|
||||
print(f"执行: {step['name']}")
|
||||
result = step['function'](result)
|
||||
|
||||
return result
|
||||
|
||||
# 使用示例
|
||||
workflow = LinearWorkflow()
|
||||
workflow.add_step(load_data, "加载数据")
|
||||
workflow.add_step(clean_data, "清理数据")
|
||||
workflow.add_step(analyze_data, "分析数据")
|
||||
|
||||
result = workflow.execute("data.geojson")
|
||||
```
|
||||
|
||||
**2. 条件分支工作流 (Conditional Workflow)**
|
||||
|
||||
```python
|
||||
class ConditionalWorkflow:
|
||||
"""带条件分支的工作流"""
|
||||
|
||||
def __init__(self):
|
||||
self.steps = {}
|
||||
self.conditions = {}
|
||||
self.transitions = {}
|
||||
|
||||
def add_step(self, name, func):
|
||||
"""添加步骤"""
|
||||
self.steps[name] = func
|
||||
return self
|
||||
|
||||
def add_condition(self, name, condition_func):
|
||||
"""添加条件判断"""
|
||||
self.conditions[name] = condition_func
|
||||
return self
|
||||
|
||||
def add_transition(self, from_step, condition, to_step):
|
||||
"""添加状态转换"""
|
||||
if from_step not in self.transitions:
|
||||
self.transitions[from_step] = {}
|
||||
self.transitions[from_step][condition] = to_step
|
||||
return self
|
||||
|
||||
def execute(self, initial_data, start_step='start'):
|
||||
"""执行工作流"""
|
||||
current_step = start_step
|
||||
state = {'data': initial_data}
|
||||
|
||||
while current_step != 'end':
|
||||
print(f"当前步骤: {current_step}")
|
||||
|
||||
# 执行步骤
|
||||
if current_step in self.steps:
|
||||
result = self.steps[current_step](state['data'])
|
||||
state['data'] = result
|
||||
|
||||
# 检查条件
|
||||
if current_step in self.transitions:
|
||||
transition = self.transitions[current_step]
|
||||
matched = False
|
||||
|
||||
for condition_name, next_step in transition.items():
|
||||
if condition_name in self.conditions:
|
||||
if self.conditions[condition_name](state):
|
||||
current_step = next_step
|
||||
matched = True
|
||||
break
|
||||
|
||||
if not matched and 'default' in transition:
|
||||
current_step = transition['default']
|
||||
elif not matched:
|
||||
current_step = 'end'
|
||||
else:
|
||||
current_step = 'end'
|
||||
|
||||
return state['data']
|
||||
|
||||
# 使用示例:生态网络工作流
|
||||
workflow = ConditionalWorkflow()
|
||||
|
||||
# 添加步骤
|
||||
workflow.add_step('load_data', load_data)
|
||||
workflow.add_step('identify_sources', identify_sources)
|
||||
workflow.add_step('human_review', human_review)
|
||||
workflow.add_step('build_resistance', build_resistance)
|
||||
|
||||
# 添加条件
|
||||
workflow.add_condition('high_uncertainty',
|
||||
lambda s: s.get('uncertainty', 0) > 0.3)
|
||||
workflow.add_condition('approved',
|
||||
lambda s: s.get('decision') == 'approve')
|
||||
|
||||
# 添加转换
|
||||
workflow.add_transition('identify_sources', 'high_uncertainty', 'human_review')
|
||||
workflow.add_transition('identify_sources', 'default', 'build_resistance')
|
||||
workflow.add_transition('human_review', 'approved', 'build_resistance')
|
||||
workflow.add_transition('human_review', 'default', 'identify_sources') # 重做
|
||||
workflow.add_transition('build_resistance', 'default', 'end')
|
||||
```
|
||||
|
||||
**3. 并行工作流 (Parallel Workflow)**
|
||||
|
||||
```python
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
class ParallelWorkflow:
|
||||
"""支持并行执行的工作流"""
|
||||
|
||||
def __init__(self, max_workers=4):
|
||||
self.max_workers = max_workers
|
||||
self.parallel_groups = {}
|
||||
|
||||
def add_parallel_group(self, group_name, tasks):
|
||||
"""添加可并行执行的任务组"""
|
||||
self.parallel_groups[group_name] = tasks
|
||||
return self
|
||||
|
||||
def execute_group(self, group_name, shared_data):
|
||||
"""执行一个并行任务组"""
|
||||
if group_name not in self.parallel_groups:
|
||||
raise ValueError(f"未知的任务组: {group_name}")
|
||||
|
||||
tasks = self.parallel_groups[group_name]
|
||||
results = {}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
# 提交所有任务
|
||||
future_to_task = {
|
||||
executor.submit(task['func'], shared_data): task['name']
|
||||
for task in tasks
|
||||
}
|
||||
|
||||
# 收集结果
|
||||
for future in as_completed(future_to_task):
|
||||
task_name = future_to_task[future]
|
||||
try:
|
||||
results[task_name] = future.result()
|
||||
except Exception as e:
|
||||
results[task_name] = {'error': str(e)}
|
||||
|
||||
return results
|
||||
|
||||
# 使用示例:同时处理多个区域
|
||||
workflow = ParallelWorkflow(max_workers=4)
|
||||
|
||||
workflow.add_parallel_group('process_regions', [
|
||||
{'name': 'region_north', 'func': process_north_region},
|
||||
{'name': 'region_south', 'func': process_south_region},
|
||||
{'name': 'region_east', 'func': process_east_region},
|
||||
{'name': 'region_west', 'func': process_west_region},
|
||||
])
|
||||
|
||||
results = workflow.execute_group('process_regions', shared_data)
|
||||
```
|
||||
|
||||
### 错误处理模式
|
||||
|
||||
```python
|
||||
class WorkflowErrorHandling:
|
||||
"""工作流错误处理"""
|
||||
|
||||
class RetryPolicy:
|
||||
"""重试策略"""
|
||||
def __init__(self, max_retries=3, backoff=2.0):
|
||||
self.max_retries = max_retries
|
||||
self.backoff = backoff # 指数退避因子
|
||||
|
||||
def should_retry(self, attempt, error):
|
||||
return attempt < self.max_retries
|
||||
|
||||
def get_delay(self, attempt):
|
||||
return self.backoff ** attempt
|
||||
|
||||
def execute_with_retry(self, func, *args, retry_policy=None, **kwargs):
|
||||
"""带重试的执行"""
|
||||
if retry_policy is None:
|
||||
retry_policy = self.RetryPolicy()
|
||||
|
||||
last_error = None
|
||||
for attempt in range(retry_policy.max_retries + 1):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if retry_policy.should_retry(attempt, e):
|
||||
delay = retry_policy.get_delay(attempt)
|
||||
print(f"尝试 {attempt + 1} 失败,{delay}秒后重试...")
|
||||
time.sleep(delay)
|
||||
else:
|
||||
break
|
||||
|
||||
raise last_error
|
||||
|
||||
def execute_with_fallback(self, primary_func, fallback_func, *args, **kwargs):
|
||||
"""带降级的执行"""
|
||||
try:
|
||||
return primary_func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f"主函数失败: {e},使用降级方案")
|
||||
return fallback_func(*args, **kwargs)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 生态网络六阶段工作流
|
||||
|
||||
```python
|
||||
"""
|
||||
ENAgent的完整工作流编排实现
|
||||
|
||||
六阶段:
|
||||
1. 数据准备
|
||||
2. 源地识别
|
||||
3. 阻力面构建
|
||||
4. MCR分析
|
||||
5. 廊道提取
|
||||
6. 结果评估
|
||||
"""
|
||||
import time
|
||||
from typing import Dict, List, Optional, Callable
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
class Stage(Enum):
|
||||
"""工作流阶段"""
|
||||
DATA_PREPARATION = "data_preparation"
|
||||
SOURCE_IDENTIFICATION = "source_identification"
|
||||
RESISTANCE_SURFACE = "resistance_surface"
|
||||
MCR_ANALYSIS = "mcr_analysis"
|
||||
CORRIDOR_EXTRACTION = "corridor_extraction"
|
||||
RESULT_EVALUATION = "result_evaluation"
|
||||
COMPLETED = "completed"
|
||||
ERROR = "error"
|
||||
|
||||
@dataclass
|
||||
class WorkflowState:
|
||||
"""工作流状态"""
|
||||
current_stage: Stage
|
||||
data: Dict
|
||||
results: Dict
|
||||
errors: List[str]
|
||||
stage_history: List[Stage]
|
||||
checkpoint_data: Optional[Dict] = None
|
||||
|
||||
class EcologicalNetworkWorkflow:
|
||||
"""生态网络分析工作流"""
|
||||
|
||||
def __init__(self):
|
||||
self.stages = {
|
||||
Stage.DATA_PREPARATION: self._data_preparation,
|
||||
Stage.SOURCE_IDENTIFICATION: self._source_identification,
|
||||
Stage.RESISTANCE_SURFACE: self._resistance_surface,
|
||||
Stage.MCR_ANALYSIS: self._mcr_analysis,
|
||||
Stage.CORRIDOR_EXTRACTION: self._corridor_extraction,
|
||||
Stage.RESULT_EVALUATION: self._result_evaluation,
|
||||
}
|
||||
|
||||
self.transitions = {
|
||||
Stage.DATA_PREPARATION: Stage.SOURCE_IDENTIFICATION,
|
||||
Stage.SOURCE_IDENTIFICATION: Stage.RESISTANCE_SURFACE,
|
||||
Stage.RESISTANCE_SURFACE: Stage.MCR_ANALYSIS,
|
||||
Stage.MCR_ANALYSIS: Stage.CORRIDOR_EXTRACTION,
|
||||
Stage.CORRIDOR_EXTRACTION: Stage.RESULT_EVALUATION,
|
||||
Stage.RESULT_EVALUATION: Stage.COMPLETED,
|
||||
}
|
||||
|
||||
# HITL审查点
|
||||
self.checkpoints = {
|
||||
Stage.SOURCE_IDENTIFICATION: True,
|
||||
Stage.RESISTANCE_SURFACE: True,
|
||||
Stage.CORRIDOR_EXTRACTION: False,
|
||||
}
|
||||
|
||||
def execute(self, initial_data: Dict) -> WorkflowState:
|
||||
"""执行完整工作流"""
|
||||
state = WorkflowState(
|
||||
current_stage=Stage.DATA_PREPARATION,
|
||||
data=initial_data,
|
||||
results={},
|
||||
errors=[],
|
||||
stage_history=[Stage.DATA_PREPARATION]
|
||||
)
|
||||
|
||||
while state.current_stage != Stage.COMPLETED:
|
||||
if state.current_stage == Stage.ERROR:
|
||||
print("工作流因错误终止")
|
||||
break
|
||||
|
||||
# 执行当前阶段
|
||||
state = self._execute_stage(state)
|
||||
|
||||
# 检查是否需要审查
|
||||
if self.checkpoints.get(state.current_stage, False):
|
||||
state = self._handle_checkpoint(state)
|
||||
|
||||
# 转换到下一阶段
|
||||
if state.current_stage != Stage.ERROR:
|
||||
next_stage = self.transitions.get(
|
||||
state.current_stage,
|
||||
Stage.COMPLETED
|
||||
)
|
||||
state.current_stage = next_stage
|
||||
state.stage_history.append(next_stage)
|
||||
|
||||
return state
|
||||
|
||||
def _execute_stage(self, state: WorkflowState) -> WorkflowState:
|
||||
"""执行单个阶段"""
|
||||
stage = state.current_stage
|
||||
print(f"\n{'='*50}")
|
||||
print(f"执行阶段: {stage.value}")
|
||||
print('='*50)
|
||||
|
||||
try:
|
||||
# 执行阶段函数
|
||||
result = self.stages[stage](state.data, state.results)
|
||||
|
||||
# 保存结果
|
||||
state.results[stage.value] = result
|
||||
|
||||
except Exception as e:
|
||||
print(f"阶段 {stage.value} 执行失败: {e}")
|
||||
state.errors.append(str(e))
|
||||
state.current_stage = Stage.ERROR
|
||||
|
||||
return state
|
||||
|
||||
def _handle_checkpoint(self, state: WorkflowState) -> WorkflowState:
|
||||
"""处理HITL审查点"""
|
||||
stage = state.current_stage
|
||||
print(f"\n[审查点: {stage.value}]")
|
||||
|
||||
# 实际实现中,这里会等待人类输入
|
||||
# 模拟审查通过
|
||||
approval = self._get_human_approval(state)
|
||||
|
||||
if not approval:
|
||||
print("审查未通过,调整参数后重新执行...")
|
||||
# 可以在这里修改state.data后返回同一阶段
|
||||
|
||||
return state
|
||||
|
||||
def _get_human_approval(self, state: WorkflowState) -> bool:
|
||||
"""获取人类批准(模拟)"""
|
||||
print(f"待审查结果: {list(state.results.keys())}")
|
||||
# 实际实现中等待输入
|
||||
return True
|
||||
|
||||
# === 阶段实现 ===
|
||||
|
||||
def _data_preparation(self, data: Dict, results: Dict) -> Dict:
|
||||
"""阶段1:数据准备"""
|
||||
print("加载和处理原始数据...")
|
||||
time.sleep(0.5)
|
||||
return {
|
||||
'landcover': 'loaded',
|
||||
'elevation': 'loaded',
|
||||
'boundary': 'loaded'
|
||||
}
|
||||
|
||||
def _source_identification(self, data: Dict, results: Dict) -> Dict:
|
||||
"""阶段2:源地识别"""
|
||||
print("识别生态源地...")
|
||||
sources = [
|
||||
{'id': 1, 'area': 1500, 'type': 'forest'},
|
||||
{'id': 2, 'area': 800, 'type': 'wetland'}
|
||||
]
|
||||
return {'sources': sources, 'n_sources': len(sources)}
|
||||
|
||||
def _resistance_surface(self, data: Dict, results: Dict) -> Dict:
|
||||
"""阶段3:阻力面构建"""
|
||||
print("构建生态阻力面...")
|
||||
return {
|
||||
'resistance_surface': 'computed',
|
||||
'weights': {'forest': 1, 'urban': 100}
|
||||
}
|
||||
|
||||
def _mcr_analysis(self, data: Dict, results: Dict) -> Dict:
|
||||
"""阶段4:MCR分析"""
|
||||
print("执行最小累积阻力分析...")
|
||||
return {'mcr_surface': 'computed'}
|
||||
|
||||
def _corridor_extraction(self, data: Dict, results: Dict) -> Dict:
|
||||
"""阶段5:廊道提取"""
|
||||
print("提取生态廊道...")
|
||||
corridors = [
|
||||
{'from': 1, 'to': 2, 'length': 3500}
|
||||
]
|
||||
return {'corridors': corridors, 'n_corridors': len(corridors)}
|
||||
|
||||
def _result_evaluation(self, data: Dict, results: Dict) -> Dict:
|
||||
"""阶段6:结果评估"""
|
||||
print("评估分析结果...")
|
||||
return {
|
||||
'connectivity_index': 0.75,
|
||||
'network_efficiency': 0.82
|
||||
}
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
print("=== 生态网络分析工作流 ===\n")
|
||||
|
||||
workflow = EcologicalNetworkWorkflow()
|
||||
|
||||
initial_data = {
|
||||
'landcover_path': 'data/landcover.tif',
|
||||
'species': 'target_species',
|
||||
'parameters': {}
|
||||
}
|
||||
|
||||
final_state = workflow.execute(initial_data)
|
||||
|
||||
print("\n=== 工作流完成 ===")
|
||||
print(f"执行的阶段数: {len(final_state.stage_history)}")
|
||||
print(f"产生的错误: {final_state.errors}")
|
||||
print(f"最终结果: {list(final_state.results.keys())}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### LangGraph的工作流实现
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph, END
|
||||
from typing import TypedDict
|
||||
|
||||
class ENAgentState(TypedDict):
|
||||
"""ENAgent工作流状态"""
|
||||
stage: str
|
||||
data: dict
|
||||
results: dict
|
||||
requires_review: bool
|
||||
|
||||
def build_enagent_workflow():
|
||||
"""构建ENAgent工作流"""
|
||||
|
||||
# 创建图
|
||||
workflow = StateGraph(ENAgentState)
|
||||
|
||||
# 添加节点
|
||||
workflow.add_node("prepare_data", prepare_data_node)
|
||||
workflow.add_node("identify_sources", identify_sources_node)
|
||||
workflow.add_node("build_resistance", build_resistance_node)
|
||||
workflow.add_node("mcr_analysis", mcr_analysis_node)
|
||||
workflow.add_node("extract_corridors", extract_corridors_node)
|
||||
workflow.add_node("evaluate_results", evaluate_results_node)
|
||||
|
||||
# 添加边(线性流程)
|
||||
workflow.set_entry_point("prepare_data")
|
||||
workflow.add_edge("prepare_data", "identify_sources")
|
||||
workflow.add_edge("identify_sources", "build_resistance")
|
||||
workflow.add_edge("build_resistance", "mcr_analysis")
|
||||
workflow.add_edge("mcr_analysis", "extract_corridors")
|
||||
workflow.add_edge("extract_corridors", "evaluate_results")
|
||||
workflow.add_edge("evaluate_results", END)
|
||||
|
||||
# 编译
|
||||
return workflow.compile()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **工作流设计**:你的项目中有哪些可以自动化的步骤?
|
||||
|
||||
2. **错误处理**:当某个步骤失败时,应该重试、跳过还是终止?
|
||||
|
||||
3. **审查点**:在你的工作流中,哪些地方需要人类介入?
|
||||
|
||||
4. **并行化**:哪些步骤可以并行执行以提升效率?
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Dataflow Programming"** - 数据流编程范式
|
||||
- **"Workflow Patterns"** (Van der Aalst) - 工作流模式
|
||||
- **Apache Airflow 文档** - 工作流调度系统
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **DAG是工作流的核心数据结构**,表达依赖关系
|
||||
2. **节点**是处理步骤,**边**定义执行顺序
|
||||
3. **条件分支**根据状态动态选择执行路径
|
||||
4. **错误处理**是生产工作流的关键
|
||||
5. **并行执行**可以显著提升效率
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
# 第四部分:自主设计
|
||||
|
||||
## 本部分目标
|
||||
|
||||
理解自主系统的设计模式和工作流编排:
|
||||
- 工作流编排的原理与模式
|
||||
- Agent的设计范式
|
||||
- 技能组合与复用的方法
|
||||
- 记忆与上下文管理
|
||||
- 规划与执行机制
|
||||
|
||||
---
|
||||
|
||||
## 章节导航
|
||||
|
||||
| 章节 | 文件 | 核心内容 | 实践 |
|
||||
|-----|------|---------|------|
|
||||
| 03.1 | [工作流编排原理](./03.1-workflow-orchestration.md) | DAG、节点与边、条件分支、错误处理 | 生态网络六阶段工作流 |
|
||||
| 03.2 | [Agent设计模式](./03.2-agent-design-patterns.md) | Reflex、Model-based、Goal-based、Utility-based | 对比四种Agent类型 |
|
||||
| 03.3 | [技能组合与复用](./03.3-skill-composition.md) | 技能抽象、接口设计、组合模式 | QGIS技能集成管理器 |
|
||||
| 03.4 | [记忆与上下文](./03.4-memory-and-context.md) | 短期/长期记忆、状态持久化、检索 | 专家知识积累系统 |
|
||||
| 03.5 | [规划与执行](./03.5-planning-and-execution.md) | 前向/后向搜索、分层规划、重规划 | RL训练工作流 |
|
||||
|
||||
---
|
||||
|
||||
## 核心概念图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 自主设计体系 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ 目标设定 │ │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ↓ │
|
||||
│ ┌────────────────────────┐ │
|
||||
│ │ 工作流编排 │ │
|
||||
│ │ (DAG + 状态机) │ │
|
||||
│ └────────────┬───────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────┼────────────────┐ │
|
||||
│ ↓ ↓ ↓ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ Agent设计 │ │ 技能组合 │ │ 记忆管理 │ │
|
||||
│ │ - 反应式 │ │ - 抽象接口 │ │ - 短期记忆 │ │
|
||||
│ │ - 基于模型 │ │ - 组合模式 │ │ - 长期记忆 │ │
|
||||
│ │ - 基于目标 │ │ - 技能发现 │ │ - 检索机制 │ │
|
||||
│ │ - 基于效用 │ │ - 动态加载 │ │ - 知识库 │ │
|
||||
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
│ └─────────────────┼─────────────────┘ │
|
||||
│ │ │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ 规划与执行 │ │
|
||||
│ │ - 搜索算法 │ │
|
||||
│ │ - 任务分解 │ │
|
||||
│ │ - 执行监控 │ │
|
||||
│ │ - 动态重规划 │ │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实践案例
|
||||
|
||||
### 实践案例04:设计一个自主空间分析Agent
|
||||
|
||||
详见 [practice/autonomous-agent](./practice/autonomous-agent/)
|
||||
|
||||
### 实践案例05:实现Human-in-the-Loop审查机制
|
||||
|
||||
详见 [practice/hitl-implementation](./practice/hitl-implementation/)
|
||||
|
||||
---
|
||||
|
||||
## 关键要点预览
|
||||
|
||||
1. **工作流编排**基于DAG和状态机,是复杂系统的核心
|
||||
2. **Agent设计**有四种经典模式,各有适用场景
|
||||
3. **技能组合**通过抽象接口实现复用和动态加载
|
||||
4. **记忆管理**区分短期和长期,支持检索和知识积累
|
||||
5. **规划与执行**需要平衡前瞻性和实时响应
|
||||
|
||||
---
|
||||
|
||||
## 延伸资源
|
||||
|
||||
- **"Artificial Intelligence: A Modern Approach"** (Russell & Norvig) - AI设计理论
|
||||
- **"Multi-Agent Systems"** (Wooldridge) - 多Agent系统
|
||||
- **LangGraph文档** - 实际工作流编排框架
|
||||
@@ -0,0 +1,249 @@
|
||||
# 第五部分:综合实践
|
||||
|
||||
## 本部分目标
|
||||
|
||||
通过完整项目将所学知识整合应用:
|
||||
- 将空间智能与自主设计结合
|
||||
- 实现端到端的智能分析系统
|
||||
- 处理真实场景的复杂性
|
||||
- 建立可复用的解决方案
|
||||
|
||||
---
|
||||
|
||||
## 项目概览
|
||||
|
||||
| 项目 | 描述 | 难度 | 涉及章节 | 预计时间 |
|
||||
|-----|------|------|---------|---------|
|
||||
| 项目A | 生态源地自动识别与优先级排序 | ⭐⭐ | 02.1, 02.3, 03.1 | 8-12小时 |
|
||||
| 项目B | 生态阻力面的多准则构建 | ⭐⭐⭐ | 02.3, 02.5, 03.3 | 12-16小时 |
|
||||
| 项目C | 生态网络的自主分析与优化 | ⭐⭐⭐⭐ | 02.2, 02.4, 03.4 | 16-20小时 |
|
||||
| 项目D | 带HITL的完整工作流设计 | ⭐⭐⭐⭐⭐ | 全部章节 | 24-30小时 |
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
每个项目包含:
|
||||
```
|
||||
projects/
|
||||
├── project-a-source-identification/
|
||||
│ ├── README.md # 项目说明
|
||||
│ ├── requirements.txt # 依赖
|
||||
│ ├── data/ # 示例数据
|
||||
│ ├── notebooks/ # Jupyter notebooks
|
||||
│ ├── src/ # 源代码
|
||||
│ └── tests/ # 测试
|
||||
├── project-b-resistance-surface/
|
||||
├── project-c-network-optimization/
|
||||
└── project-d-hitl-workflow/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目A:生态源地自动识别与优先级排序
|
||||
|
||||
### 问题背景
|
||||
|
||||
生态源地是生态网络的核心节点,需要从土地利用数据中自动识别并排序。
|
||||
|
||||
### 涉及技术
|
||||
|
||||
- 空间表征 (02.1):栅格数据处理、斑块提取
|
||||
- 多准则决策 (02.3):适宜性评价、权重设计
|
||||
- 工作流编排 (03.1):数据→处理→排序的流水线
|
||||
|
||||
### 实现步骤
|
||||
|
||||
1. **数据准备**
|
||||
- 加载土地利用覆盖数据
|
||||
- 数据预处理(投影、重分类)
|
||||
|
||||
2. **斑块识别**
|
||||
- 使用形态学空间格局分析(MSPA)
|
||||
- 提取连通性指标
|
||||
|
||||
3. **适宜性评价**
|
||||
- 计算每个斑块的适宜性得分
|
||||
- 多指标综合(面积、形状、连通性)
|
||||
|
||||
4. **优先级排序**
|
||||
- 按适宜性排序
|
||||
- 输出源地列表
|
||||
|
||||
### 扩展方向
|
||||
|
||||
- 添加不确定性评估
|
||||
- 支持多物种的源地识别
|
||||
- 可视化源地优先级
|
||||
|
||||
---
|
||||
|
||||
## 项目B:生态阻力面的多准则构建
|
||||
|
||||
### 问题背景
|
||||
|
||||
阻力面反映物种在景观中移动的难易程度,需要综合考虑多种因素。
|
||||
|
||||
### 涉及技术
|
||||
|
||||
- 多准则决策 (02.3):AHP权重分配
|
||||
- 不确定性量化 (02.5):敏感性分析
|
||||
- 技能组合 (03.3):多个阻力源的组合
|
||||
|
||||
### 实现步骤
|
||||
|
||||
1. **阻力源识别**
|
||||
- 土地利用类型阻力
|
||||
- 地形阻力(坡度、高程)
|
||||
- 人为干扰阻力(道路、建筑)
|
||||
|
||||
2. **权重确定**
|
||||
- 专家知识编码
|
||||
- AHP层次分析法
|
||||
- 权重一致性检验
|
||||
|
||||
3. **阻力面合成**
|
||||
- 加权叠加
|
||||
- 阻力系数调整
|
||||
|
||||
4. **敏感性分析**
|
||||
- 权重扰动测试
|
||||
- 结果稳定性评估
|
||||
|
||||
### 扩展方向
|
||||
|
||||
- 支持不同物种的定制阻力面
|
||||
- 时间维度的动态阻力面
|
||||
- 基于遥感的自动更新
|
||||
|
||||
---
|
||||
|
||||
## 项目C:生态网络的自主分析与优化
|
||||
|
||||
### 问题背景
|
||||
|
||||
在源地和阻力面基础上,自动提取生态廊道并优化网络结构。
|
||||
|
||||
### 涉及技术
|
||||
|
||||
- 空间推理 (02.2):图算法、连通性分析
|
||||
- 空间优化 (02.4):网络优化算法
|
||||
- Agent设计 (03.2):自主分析智能体
|
||||
- 记忆管理 (03.4):结果缓存与检索
|
||||
|
||||
### 实现步骤
|
||||
|
||||
1. **最小累积阻力(MCR)分析**
|
||||
- 成本距离计算
|
||||
- 最小成本路径
|
||||
|
||||
2. **廊道提取**
|
||||
- 提取潜在廊道
|
||||
- 宽度分析
|
||||
- 质量评估
|
||||
|
||||
3. **网络分析**
|
||||
- 连通性指标
|
||||
- 关键节点识别
|
||||
- 网络脆弱性评估
|
||||
|
||||
4. **优化建议**
|
||||
- 廊道优先级排序
|
||||
- 新增源地建议
|
||||
- 网络强化方案
|
||||
|
||||
### 扩展方向
|
||||
|
||||
- 多目标优化(生态+经济)
|
||||
- 动态网络模拟
|
||||
- 与GIS软件集成
|
||||
|
||||
---
|
||||
|
||||
## 项目D:带HITL的完整工作流设计
|
||||
|
||||
### 问题背景
|
||||
|
||||
整合A、B、C项目,实现完整的生态网络分析工作流,在关键点加入人类审查。
|
||||
|
||||
### 涉及技术
|
||||
|
||||
- 全部章节的综合应用
|
||||
|
||||
### 实现步骤
|
||||
|
||||
1. **工作流设计**
|
||||
- 状态定义
|
||||
- 节点设计
|
||||
- 条件路由
|
||||
|
||||
2. **审查点设计**
|
||||
- 源地识别审查
|
||||
- 阻力权重审查
|
||||
- 廊道优化审查
|
||||
|
||||
3. **人机界面**
|
||||
- 信息展示
|
||||
- 决策输入
|
||||
- 反馈机制
|
||||
|
||||
4. **系统集成**
|
||||
- 持久化与恢复
|
||||
- 错误处理
|
||||
- 日志记录
|
||||
|
||||
### 交付成果
|
||||
|
||||
- 完整可运行的系统
|
||||
- 用户手册
|
||||
- 技术文档
|
||||
- 演示视频
|
||||
|
||||
---
|
||||
|
||||
## 学习建议
|
||||
|
||||
### 循序渐进
|
||||
|
||||
1. **先做项目A**:建立基础概念
|
||||
2. **再做项目B**:学习多准则方法
|
||||
3. **然后做项目C**:掌握空间优化
|
||||
4. **最后做项目D**:整合所有知识
|
||||
|
||||
### 遇到困难时
|
||||
|
||||
- 回顾相关章节的理论
|
||||
- 查阅示例代码
|
||||
- 参考延伸阅读
|
||||
- 寻求社区帮助
|
||||
|
||||
### 提升挑战
|
||||
|
||||
- 为项目添加新功能
|
||||
- 使用自己的数据
|
||||
- 优化性能
|
||||
- 改进用户体验
|
||||
|
||||
---
|
||||
|
||||
## 评估标准
|
||||
|
||||
完成项目后,自问:
|
||||
|
||||
1. **功能完整性**:是否实现了所有核心功能?
|
||||
2. **代码质量**:代码是否清晰、可维护?
|
||||
3. **文档完整性**:是否有清晰的使用说明?
|
||||
4. **鲁棒性**:能否处理异常情况?
|
||||
5. **可扩展性**:是否容易添加新功能?
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
完成所有项目后,你已经:
|
||||
- ✅ 掌握了空间智能的核心方法
|
||||
- ✅ 理解了自主系统的设计模式
|
||||
- ✅ 实现了完整的AI辅助分析系统
|
||||
- ✅ 建立了可复用的技能库
|
||||
|
||||
准备好进入:**05-reflection(反思与展望)**
|
||||
@@ -0,0 +1,484 @@
|
||||
# 05.1 AI的局限与幻觉
|
||||
|
||||
## 核心问题
|
||||
|
||||
> AI在空间分析中哪些地方可能出错?
|
||||
> 当AI给出一个看似合理的答案时,我们如何验证它?
|
||||
> 人类专家的哪些能力是AI难以替代的?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### AI幻觉的本质
|
||||
|
||||
**幻觉 (Hallucination)** 是指AI生成看似合理但实际错误的内容。在空间AI中,这个问题尤为隐蔽和危险:
|
||||
|
||||
```
|
||||
空间AI幻觉的类型
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 1. 几何幻觉 │
|
||||
│ - 生成无效的几何图形 │
|
||||
│ - 错误的空间关系(如"A在B内部"实际为假) │
|
||||
│ - 投影和坐标系混淆 │
|
||||
│ │
|
||||
│ 2. 语义幻觉 │
|
||||
│ - 对空间概念的错误理解 │
|
||||
│ - 编造不存在的GIS功能 │
|
||||
│ - 混淆专业术语 │
|
||||
│ │
|
||||
│ 3. 逻辑幻觉 │
|
||||
│ - 分析步骤的遗漏或重复 │
|
||||
│ - 错误的因果推断 │
|
||||
│ - 隐藏的假设未被说明 │
|
||||
│ │
|
||||
│ 4. 数据幻觉 │
|
||||
│ - 假设数据存在实际不存在 │
|
||||
│ - 错误的数据格式假设 │
|
||||
│ - 忽略数据质量和边界条件 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 为什么会产生幻觉?
|
||||
|
||||
| 原因类型 | 说明 | 空间AI中的例子 |
|
||||
|---------|------|---------------|
|
||||
| **训练数据偏差** | 模型见过的数据不具代表性 | 模型更多见过城市数据,对农村场景判断不准 |
|
||||
| **模式匹配局限** | 模型基于模式而非理解 | 混淆buffer和convex hull因为结果看起来相似 |
|
||||
| **上下文理解不足** | 无法完全理解复杂场景 | 忽略项目的特定约束条件 |
|
||||
| **概率生成本质** | 逐token生成可能偏离 | 生成的代码引用不存在的函数 |
|
||||
| **知识边界模糊** | 模型不知道自己不知道 | 对未见过的GIS版本编造功能 |
|
||||
|
||||
### 空间AI特有的脆弱性
|
||||
|
||||
空间分析有其特殊性质,使得AI的错误更具破坏性:
|
||||
|
||||
```
|
||||
空间AI脆弱性来源
|
||||
|
||||
1. 隐式依赖
|
||||
GIS操作常有隐式前提:
|
||||
- "intersect前必须确保同一坐标系"
|
||||
- "buffer距离需要合适的投影"
|
||||
这些前提AI可能忽略
|
||||
|
||||
2. 级联效应
|
||||
空间分析通常是多步骤:
|
||||
数据加载 → 清理 → 投影 → 分析 → 输出
|
||||
早期错误会被放大
|
||||
|
||||
3. 验证困难
|
||||
空间结果不像代码能快速测试:
|
||||
- 这个生态源地识别对吗?
|
||||
- 这个阻力面合理吗?
|
||||
需要领域知识判断
|
||||
|
||||
4. 不可见错误
|
||||
某些空间错误不会立即显现:
|
||||
- 轻微的几何错误
|
||||
- 边界处的投影变形
|
||||
- 拓扑关系的细微错误
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 验证框架设计
|
||||
|
||||
建立系统的验证流程是应对AI局限的关键:
|
||||
|
||||
```python
|
||||
"""
|
||||
空间AI结果验证框架
|
||||
"""
|
||||
from typing import Any, Dict, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
class ValidationLevel(Enum):
|
||||
"""验证级别"""
|
||||
CRITICAL = "critical" # 必须通过
|
||||
IMPORTANT = "important" # 应该通过
|
||||
WARNING = "warning" # 警告即可
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""验证结果"""
|
||||
passed: bool
|
||||
level: ValidationLevel
|
||||
message: str
|
||||
details: Optional[Dict] = None
|
||||
|
||||
class SpatialAIValidator:
|
||||
"""空间AI输出验证器"""
|
||||
|
||||
def __init__(self):
|
||||
self.checks = []
|
||||
|
||||
def add_check(self, check_fn, level: ValidationLevel):
|
||||
"""添加验证检查"""
|
||||
self.checks.append((check_fn, level))
|
||||
return self
|
||||
|
||||
def validate(self, result: Any, context: Dict) -> List[ValidationResult]:
|
||||
"""执行所有验证"""
|
||||
results = []
|
||||
|
||||
for check_fn, level in self.checks:
|
||||
try:
|
||||
result = check_fn(result, context)
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
results.append(ValidationResult(
|
||||
passed=False,
|
||||
level=ValidationLevel.CRITICAL,
|
||||
message=f"验证失败: {str(e)}"
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
# === 常用验证检查 ===
|
||||
|
||||
def check_crs_consistency(result, context) -> ValidationResult:
|
||||
"""检查坐标系一致性"""
|
||||
if hasattr(result, 'crs') and result.crs is not None:
|
||||
expected_crs = context.get('expected_crs')
|
||||
if expected_crs and result.crs != expected_crs:
|
||||
return ValidationResult(
|
||||
passed=False,
|
||||
level=ValidationLevel.CRITICAL,
|
||||
message=f"坐标系不匹配: 期望 {expected_crs}, 实际 {result.crs}",
|
||||
details={'expected': expected_crs, 'actual': result.crs}
|
||||
)
|
||||
return ValidationResult(
|
||||
passed=True,
|
||||
level=ValidationLevel.CRITICAL,
|
||||
message="坐标系检查通过"
|
||||
)
|
||||
|
||||
def check_geometry_validity(result, context) -> ValidationResult:
|
||||
"""检查几何有效性"""
|
||||
if hasattr(result, 'geometry'):
|
||||
if hasattr(result.geometry, 'is_valid'):
|
||||
if not result.geometry.is_valid.all():
|
||||
invalid_count = (~result.geometry.is_valid).sum()
|
||||
return ValidationResult(
|
||||
passed=False,
|
||||
level=ValidationLevel.IMPORTANT,
|
||||
message=f"存在 {invalid_count} 个无效几何",
|
||||
details={'invalid_count': invalid_count}
|
||||
)
|
||||
return ValidationResult(
|
||||
passed=True,
|
||||
level=ValidationLevel.IMPORTANT,
|
||||
message="几何有效性检查通过"
|
||||
)
|
||||
|
||||
def check_result_size(result, context) -> ValidationResult:
|
||||
"""检查结果规模是否合理"""
|
||||
if hasattr(result, '__len__'):
|
||||
size = len(result)
|
||||
max_expected = context.get('max_expected_size', float('inf'))
|
||||
min_expected = context.get('min_expected_size', 0)
|
||||
|
||||
if size > max_expected:
|
||||
return ValidationResult(
|
||||
passed=False,
|
||||
level=ValidationLevel.WARNING,
|
||||
message=f"结果数量异常大: {size}",
|
||||
details={'size': size}
|
||||
)
|
||||
if size < min_expected:
|
||||
return ValidationResult(
|
||||
passed=False,
|
||||
level=ValidationLevel.WARNING,
|
||||
message=f"结果数量异常小: {size}",
|
||||
details={'size': size}
|
||||
)
|
||||
return ValidationResult(
|
||||
passed=True,
|
||||
level=ValidationLevel.WARNING,
|
||||
message="结果规模检查通过"
|
||||
)
|
||||
|
||||
def check_spatial_extent(result, context) -> ValidationResult:
|
||||
"""检查空间范围是否合理"""
|
||||
if hasattr(result, 'total_bounds'):
|
||||
bounds = result.total_bounds
|
||||
expected_bounds = context.get('expected_bounds')
|
||||
|
||||
if expected_bounds:
|
||||
# 检查结果是否在预期范围内
|
||||
if not (bounds[0] >= expected_bounds[0] and
|
||||
bounds[2] <= expected_bounds[2] and
|
||||
bounds[1] >= expected_bounds[1] and
|
||||
bounds[3] <= expected_bounds[3]):
|
||||
return ValidationResult(
|
||||
passed=False,
|
||||
level=ValidationLevel.IMPORTANT,
|
||||
message=f"空间范围超出预期",
|
||||
details={'bounds': bounds, 'expected': expected_bounds}
|
||||
)
|
||||
return ValidationResult(
|
||||
passed=True,
|
||||
level=ValidationLevel.IMPORTANT,
|
||||
message="空间范围检查通过"
|
||||
)
|
||||
|
||||
# === 使用示例 ===
|
||||
|
||||
def create_validator_example():
|
||||
"""创建完整的验证器示例"""
|
||||
validator = SpatialAIValidator()
|
||||
|
||||
# 添加验证检查
|
||||
validator.add_check(check_crs_consistency, ValidationLevel.CRITICAL)
|
||||
validator.add_check(check_geometry_validity, ValidationLevel.IMPORTANT)
|
||||
validator.add_check(check_result_size, ValidationLevel.WARNING)
|
||||
validator.add_check(check_spatial_extent, ValidationLevel.IMPORTANT)
|
||||
|
||||
return validator
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 空间AI验证框架 ===\n")
|
||||
|
||||
validator = create_validator_example()
|
||||
print(f"验证器配置了 {len(validator.checks)} 个检查")
|
||||
|
||||
print("\n验证级别说明:")
|
||||
print(" CRITICAL: 必须通过的错误")
|
||||
print(" IMPORTANT: 应该通过的问题")
|
||||
print(" WARNING: 值得注意的警告")
|
||||
```
|
||||
|
||||
### 人类专家的不可替代性
|
||||
|
||||
```
|
||||
人类专家的优势领域
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 1. 上下文理解 │
|
||||
│ - 理解项目背景和约束条件 │
|
||||
│ - 识别"不合理"的结果 │
|
||||
│ - 考虑实际可行性 │
|
||||
│ │
|
||||
│ 2. 价值判断 │
|
||||
│ - 权衡不同目标 │
|
||||
│ - 考虑伦理影响 │
|
||||
│ - 平衡科学性和实用性 │
|
||||
│ │
|
||||
│ 3. 创造性思维 │
|
||||
│ - 提出新的分析方法 │
|
||||
│ - 创造性地解决问题 │
|
||||
│ - 跨领域联想 │
|
||||
│ │
|
||||
│ 4. 责任承担 │
|
||||
│ - 对结果负责 │
|
||||
│ - 解释和辩护决策 │
|
||||
│ - 承担法律和伦理责任 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### AI辅助的正确模式
|
||||
|
||||
```
|
||||
正确的AI使用模式
|
||||
|
||||
专家 + AI = 增强
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ AI的角色 专家的角色 │
|
||||
│ │ │ │
|
||||
│ ├── 快速计算和数据处理 ──────────────┼── 定义问题 │
|
||||
│ ├── 提供多种方案 ───────────────────┼── 选择和评估 │
|
||||
│ ├── 识别模式 ──────────────────────┼── 解释意义 │
|
||||
│ ├── 自动化重复任务 ─────────────────┼── 设计工作流 │
|
||||
│ ├── 检查错误 ──────────────────────┼── 验证关键结果 │
|
||||
│ └── 提供参考 ──────────────────────┼── 做出决策 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
错误模式:
|
||||
AI ──→ 结果 ──→ 直接使用
|
||||
(跳过专家验证)
|
||||
|
||||
正确模式:
|
||||
专家 ──→ 定义问题 ──→ AI ──→ 候选方案
|
||||
│
|
||||
专家 ◄─────────────────────┘
|
||||
│ 评估
|
||||
│ 验证
|
||||
│ 决策
|
||||
↓
|
||||
最终方案
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### 案例1:坐标系统错误
|
||||
|
||||
**场景**:AI生成了生态源地识别代码,但忘记处理坐标系问题。
|
||||
|
||||
```python
|
||||
# AI生成的代码(有潜在问题)
|
||||
def identify_sources(ai_generated_landcover):
|
||||
"""AI生成的源地识别代码"""
|
||||
# 直接使用原始数据分析
|
||||
forest = ai_generated_landcover[ai_generated_landcover['type'] == 'forest']
|
||||
sources = forest[forest['area'] > 100] # 面积阈值
|
||||
|
||||
# 问题:数据可能是WGS84,但面积按度计算
|
||||
return sources
|
||||
|
||||
# 验证和修正
|
||||
def identify_sources_validated(landcover, target_crs='EPSG:3857'):
|
||||
"""添加验证的版本"""
|
||||
# 验证1:检查坐标系
|
||||
if landcover.crs and landcover.crs.is_geographic:
|
||||
# 需要投影
|
||||
landcover_projected = landcover.to_crs(target_crs)
|
||||
print(f"注意:数据已从 {landcover.crs} 投影到 {target_crs}")
|
||||
else:
|
||||
landcover_projected = landcover
|
||||
|
||||
# 验证2:面积计算检查
|
||||
if landcover_projected.crs.is_geographic:
|
||||
raise ValueError("不能在地理坐标系中计算面积")
|
||||
|
||||
# 进行分析
|
||||
forest = landcover_projected[landcover_projected['type'] == 'forest']
|
||||
sources = forest[forest.geometry.area > 100000] # 投影后单位是米
|
||||
|
||||
# 验证3:结果合理性检查
|
||||
if len(sources) == 0:
|
||||
print("警告:没有识别到源地,检查面积阈值")
|
||||
if len(sources) > len(landcover_projected) * 0.9:
|
||||
print("警告:几乎全部区域都是源地,检查阈值")
|
||||
|
||||
return sources
|
||||
```
|
||||
|
||||
### 案例2:拓扑关系幻觉
|
||||
|
||||
**场景**:AI错误判断空间关系。
|
||||
|
||||
```python
|
||||
# AI可能生成的错误逻辑
|
||||
def check_connectivity(patch_a, patch_b):
|
||||
"""检查两个斑块的连通性"""
|
||||
# 问题:AI可能混淆多种连通性判断
|
||||
return patch_a.intersects(patch_b) # 相交不等于连通
|
||||
|
||||
# 正确的方法
|
||||
def check_connectivity_robust(patch_a, patch_b, distance_threshold=100):
|
||||
"""
|
||||
鲁棒的连通性检查
|
||||
|
||||
需要明确:
|
||||
1. 是直接连接还是距离阈值内?
|
||||
2. 需要考虑阻力吗?
|
||||
3. 连通的宽度要求?
|
||||
"""
|
||||
# 方案1:直接接触
|
||||
if patch_a.touches(patch_b):
|
||||
return {'connected': True, 'type': 'direct'}
|
||||
|
||||
# 方案2:距离阈值
|
||||
distance = patch_a.distance(patch_b)
|
||||
if distance <= distance_threshold:
|
||||
return {'connected': True, 'type': 'proximity', 'distance': distance}
|
||||
|
||||
return {'connected': False, 'distance': distance}
|
||||
```
|
||||
|
||||
### 案例3:参数选择幻觉
|
||||
|
||||
**场景**:AI编造了一个"标准"参数值,实际并不存在。
|
||||
|
||||
```python
|
||||
# AI可能这样写
|
||||
def calculate_landscape_metrics(patch, resistance="standard"):
|
||||
"""
|
||||
计算景观指标
|
||||
|
||||
问题:AI声称存在"standard"阻力值,实际需要根据情况设定
|
||||
"""
|
||||
# 不存在通用标准
|
||||
pass
|
||||
|
||||
# 正确的做法
|
||||
def calculate_landscape_metrics_validated(patch, resistance=None,
|
||||
resistance_params=None):
|
||||
"""
|
||||
计算景观指标,明确参数来源
|
||||
|
||||
Args:
|
||||
resistance: 阻力值,必须明确提供
|
||||
resistance_params: 阻力参数配置
|
||||
|
||||
Returns:
|
||||
指标和参数来源说明
|
||||
"""
|
||||
if resistance is None and resistance_params is None:
|
||||
raise ValueError(
|
||||
"阻力参数必须明确提供。不存在'标准'值。"
|
||||
"请根据研究区域和物种特征设定。"
|
||||
)
|
||||
|
||||
# 记录参数来源
|
||||
metadata = {
|
||||
'resistance_source': resistance_params.get('source', 'user_provided'),
|
||||
'reference': resistance_params.get('reference', None),
|
||||
'justification': resistance_params.get('justification', None)
|
||||
}
|
||||
|
||||
# 计算指标...
|
||||
return {'metrics': {}, 'metadata': metadata}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **识别能力边界**:你最近一次发现AI错误是什么时候?是如何发现的?
|
||||
|
||||
2. **验证成本**:在什么情况下,详细验证的成本超过了使用AI的收益?
|
||||
|
||||
3. **责任分配**:当AI辅助的分析出现错误时,责任该如何划分?
|
||||
|
||||
4. **信任建立**:随着时间推移,你应该如何调整对AI的信任程度?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **错误审计**:回顾过去使用AI生成的空间分析代码,找出潜在问题
|
||||
|
||||
2. **验证清单**:为你常用的空间分析类型建立验证清单
|
||||
|
||||
3. **对比实验**:同一个问题让AI多次求解,比较结果的差异
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Human Compatible"** (Stuart Russell) - AI对齐与人类价值
|
||||
- **"AI Safety"** 相关文献 - 理解AI的局限和风险
|
||||
- GIS最佳实践手册 - 学习领域专家的验证方法
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **AI在空间分析中可能产生多种类型的幻觉**,需要系统性验证
|
||||
2. **建立验证框架**是可靠使用AI的关键
|
||||
3. **人类专家的角色不可替代**,特别是在判断和决策环节
|
||||
4. **正确的使用模式是AI辅助+专家验证**,而非AI替代
|
||||
5. **保持批判性思维**,理解AI的局限才能更好地利用它
|
||||
@@ -0,0 +1,820 @@
|
||||
# 05.2 伦理与责任
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 空间决策如何影响不同的人群和环境?
|
||||
> 当AI参与空间规划时,如何确保过程的公平和透明?
|
||||
> 出错的决策责任应该由谁来承担?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 空间决策的伦理维度
|
||||
|
||||
空间决策不是价值中立的,它们分配资源、机会和风险:
|
||||
|
||||
```
|
||||
空间决策的伦理影响
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 1. 分配正义 (Distributive Justice) │
|
||||
│ - 谁获得绿地、公园等正面空间资源? │
|
||||
│ - 谁承受污染、噪声等负面影响? │
|
||||
│ - 空间资源的公平分配原则是什么? │
|
||||
│ │
|
||||
│ 2. 程序正义 (Procedural Justice) │
|
||||
│ - 决策过程是否透明? │
|
||||
│ - 受影响者能否参与决策? │
|
||||
│ - 决策依据是否可审查? │
|
||||
│ │
|
||||
│ 3. 承认正义 (Recognition Justice) │
|
||||
│ - 不同群体的需求和价值观是否被认可? │
|
||||
│ - 弱势群体的空间权利是否被尊重? │
|
||||
│ - 文化多样性在空间中如何体现? │
|
||||
│ │
|
||||
│ 4. 生态正义 (Ecological Justice) │
|
||||
│ - 当代人与未来世代之间的公平? │
|
||||
│ - 人类活动对生态系统的责任? │
|
||||
│ - 非人类物种的空间权利? │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### AI带来的新伦理挑战
|
||||
|
||||
```
|
||||
AI空间决策的特有问题
|
||||
|
||||
1. 算法偏见 (Algorithmic Bias)
|
||||
训练数据中的社会偏见被编码进模型
|
||||
→ 历史上的红线政策可能影响现在的预测
|
||||
|
||||
2. 黑箱决策 (Black Box Decision)
|
||||
复杂模型的决策过程难以解释
|
||||
→ 利益相关者无法质疑或理解决策
|
||||
|
||||
3. 责任分散 (Diffused Responsibility)
|
||||
涉及多个主体:开发者、用户、数据提供者
|
||||
→ 出错时责任难以界定
|
||||
|
||||
4. 规模效应 (Scale Effects)
|
||||
AI可以大规模应用决策
|
||||
→ 小偏差在大规模下产生大影响
|
||||
|
||||
5. 路径依赖 (Path Dependence)
|
||||
早期决策影响后续数据收集
|
||||
→ 偏见自我强化
|
||||
```
|
||||
|
||||
### 空间正义的典型问题
|
||||
|
||||
| 问题类型 | 说明 | AI相关风险 |
|
||||
|---------|------|-----------|
|
||||
| **环境种族主义** | 有害设施更多位于少数族裔社区 | AI可能复制历史模式 |
|
||||
| **绿色绅士化** | 绿地改善导致原住民被迫搬迁 | AI优化可能加剧此问题 |
|
||||
| **数字鸿沟** | 缺乏数据地区被忽视 | AI只关注数据丰富的区域 |
|
||||
| **代表性不足** | 某些群体的需求未被考虑 | 训练数据偏差 |
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 可解释性设计原则
|
||||
|
||||
```python
|
||||
"""
|
||||
可解释的空间AI设计
|
||||
"""
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
@dataclass
|
||||
class DecisionExplanation:
|
||||
"""决策解释"""
|
||||
decision: str # 做出的决策
|
||||
rationale: List[str] # 决策理由
|
||||
key_factors: Dict[str, float] # 关键因素及权重
|
||||
alternatives: List[Dict] # 考虑过的替代方案
|
||||
uncertainties: List[str] # 不确定性说明
|
||||
assumptions: List[str] # 假设条件
|
||||
|
||||
class ExplainableSpatialAI(ABC):
|
||||
"""可解释的空间AI基类"""
|
||||
|
||||
@abstractmethod
|
||||
def make_decision(self, context: Dict) -> Any:
|
||||
"""做出决策"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def explain_decision(self, decision: Any, context: Dict) -> DecisionExplanation:
|
||||
"""解释决策"""
|
||||
pass
|
||||
|
||||
def audit_trail(self) -> List[Dict]:
|
||||
"""返回审计轨迹"""
|
||||
return self._audit_log
|
||||
|
||||
class ExplainableSiteSelector(ExplainableSpatialAI):
|
||||
"""可解释的选址AI"""
|
||||
|
||||
def __init__(self):
|
||||
self.criteria_weights = {}
|
||||
self._audit_log = []
|
||||
|
||||
def set_criteria(self, criteria: Dict[str, float], justification: str):
|
||||
"""
|
||||
设置评判标准
|
||||
|
||||
Args:
|
||||
criteria: {标准名: 权重}
|
||||
justification: 权重选择的理由
|
||||
"""
|
||||
self.criteria_weights = criteria.copy()
|
||||
self._log({
|
||||
'action': 'set_criteria',
|
||||
'criteria': criteria,
|
||||
'justification': justification
|
||||
})
|
||||
|
||||
def make_decision(self, context: Dict) -> Dict:
|
||||
"""
|
||||
做出选址决策
|
||||
|
||||
返回选中的地点及其评分
|
||||
"""
|
||||
sites = context['sites']
|
||||
constraints = context.get('constraints', {})
|
||||
|
||||
# 评估每个候选地
|
||||
scored_sites = []
|
||||
for site in sites:
|
||||
score, details = self._evaluate_site(site, context)
|
||||
scored_sites.append({
|
||||
'site': site,
|
||||
'score': score,
|
||||
'details': details
|
||||
})
|
||||
|
||||
# 排序并选择最高分
|
||||
scored_sites.sort(key=lambda x: x['score'], reverse=True)
|
||||
selected = scored_sites[0]
|
||||
|
||||
# 记录决策
|
||||
self._log({
|
||||
'action': 'make_decision',
|
||||
'selected': selected['site'],
|
||||
'score': selected['score'],
|
||||
'alternatives': scored_sites[1:4] # 保存前几个备选
|
||||
})
|
||||
|
||||
return selected
|
||||
|
||||
def explain_decision(self, decision: Any, context: Dict) -> DecisionExplanation:
|
||||
"""生成决策解释"""
|
||||
selected_site = decision['site']
|
||||
score_details = decision['details']
|
||||
|
||||
# 生成解释
|
||||
return DecisionExplanation(
|
||||
decision=f"选择地点 {selected_site['name']}",
|
||||
rationale=[
|
||||
f"该地点综合评分最高 ({decision['score']:.2f})",
|
||||
"评分基于预定义的标准和权重",
|
||||
"所有候选地点已被系统评估"
|
||||
],
|
||||
key_factors=score_details,
|
||||
alternatives=[
|
||||
{
|
||||
'site': alt['site']['name'],
|
||||
'score': alt['score'],
|
||||
'reason': '评分较低'
|
||||
}
|
||||
for alt in context.get('alternatives', [])[:3]
|
||||
],
|
||||
uncertainties=[
|
||||
"评分依赖输入数据的准确性",
|
||||
"权重选择包含主观判断",
|
||||
"未量化的因素可能影响实际适用性"
|
||||
],
|
||||
assumptions=[
|
||||
"所有标准可以用数值表示",
|
||||
"各标准相互独立",
|
||||
"当前条件在未来保持稳定"
|
||||
]
|
||||
)
|
||||
|
||||
def _evaluate_site(self, site: Dict, context: Dict) -> tuple:
|
||||
"""评估单个地点"""
|
||||
scores = {}
|
||||
|
||||
for criterion, weight in self.criteria_weights.items():
|
||||
# 从地点数据中获取该标准的值
|
||||
value = site.get(criterion, 0)
|
||||
|
||||
# 标准化(简化版)
|
||||
normalized = self._normalize(criterion, value)
|
||||
|
||||
# 加权
|
||||
scores[criterion] = normalized * weight
|
||||
|
||||
total_score = sum(scores.values())
|
||||
|
||||
return total_score, scores
|
||||
|
||||
def _normalize(self, criterion: str, value: float) -> float:
|
||||
"""标准化准则值"""
|
||||
# 简化:假设越大越好,范围0-100
|
||||
return min(max(value / 100, 0), 1)
|
||||
|
||||
def _log(self, entry: Dict):
|
||||
"""记录日志"""
|
||||
entry['timestamp'] = self._get_timestamp()
|
||||
self._audit_log.append(entry)
|
||||
|
||||
def _get_timestamp(self) -> str:
|
||||
"""获取时间戳"""
|
||||
from datetime import datetime
|
||||
return datetime.now().isoformat()
|
||||
|
||||
# === 伦理检查 ===
|
||||
|
||||
class EthicsChecker:
|
||||
"""伦理检查器"""
|
||||
|
||||
def __init__(self):
|
||||
self.checks = []
|
||||
|
||||
def add_check(self, check_fn, name: str):
|
||||
"""添加检查"""
|
||||
self.checks.append((check_fn, name))
|
||||
return self
|
||||
|
||||
def check_decision(self, decision: Any, context: Dict) -> Dict:
|
||||
"""执行所有伦理检查"""
|
||||
results = {
|
||||
'passed': True,
|
||||
'issues': [],
|
||||
'warnings': []
|
||||
}
|
||||
|
||||
for check_fn, name in self.checks:
|
||||
try:
|
||||
result = check_fn(decision, context)
|
||||
if not result['passed']:
|
||||
results['passed'] = False
|
||||
results['issues'].append({
|
||||
'check': name,
|
||||
'reason': result['reason']
|
||||
})
|
||||
elif result.get('warning'):
|
||||
results['warnings'].append({
|
||||
'check': name,
|
||||
'warning': result['warning']
|
||||
})
|
||||
except Exception as e:
|
||||
results['issues'].append({
|
||||
'check': name,
|
||||
'reason': f"检查失败: {str(e)}"
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
# 预定义的伦理检查
|
||||
|
||||
def check_environmental_justice(decision, context) -> Dict:
|
||||
"""检查环境正义:确保不将负面影响集中到弱势社区"""
|
||||
selected_site = decision['site']
|
||||
|
||||
# 检查是否有弱势群体数据
|
||||
vulnerable_communities = context.get('vulnerable_communities', [])
|
||||
|
||||
for community in vulnerable_communities:
|
||||
if selected_site.get('near_community') == community['id']:
|
||||
# 如果项目有负面影响,需要特别审查
|
||||
if context.get('project_type') == 'negative_impact':
|
||||
return {
|
||||
'passed': False,
|
||||
'reason': f"选址靠近弱势社区 {community['name']},需要额外的环境正义审查"
|
||||
}
|
||||
|
||||
return {'passed': True}
|
||||
|
||||
def check_transparency(decision, context) -> Dict:
|
||||
"""检查透明度:确保决策过程可记录和审查"""
|
||||
if not decision.get('details'):
|
||||
return {
|
||||
'passed': False,
|
||||
'reason': '决策缺乏详细评分信息,无法审查'
|
||||
}
|
||||
|
||||
return {'passed': True}
|
||||
|
||||
def check_public_participation(decision, context) -> Dict:
|
||||
"""检查公众参与:确保受影响者有机会表达意见"""
|
||||
if context.get('affects_public', False):
|
||||
participation = context.get('public_participation')
|
||||
if not participation or participation == 'none':
|
||||
return {
|
||||
'passed': False,
|
||||
'reason': '项目影响公众但缺乏公众参与程序'
|
||||
}
|
||||
elif participation == 'minimal':
|
||||
return {
|
||||
'passed': True,
|
||||
'warning': '公众参与程度较低,建议加强'
|
||||
}
|
||||
|
||||
return {'passed': True}
|
||||
|
||||
# === 使用示例 ===
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 可解释的空间AI ===\n")
|
||||
|
||||
# 创建选址器
|
||||
selector = ExplainableSiteSelector()
|
||||
|
||||
# 设置评判标准(带理由)
|
||||
selector.set_criteria(
|
||||
criteria={
|
||||
'accessibility': 0.3,
|
||||
'environmental_quality': 0.25,
|
||||
'cost_effectiveness': 0.2,
|
||||
'community_support': 0.15,
|
||||
'future_potential': 0.1
|
||||
},
|
||||
justification="基于项目目标和利益相关者访谈"
|
||||
)
|
||||
|
||||
# 模拟候选地点
|
||||
sites = [
|
||||
{'name': 'Site A', 'accessibility': 85, 'environmental_quality': 70,
|
||||
'cost_effectiveness': 60, 'community_support': 80, 'future_potential': 75},
|
||||
{'name': 'Site B', 'accessibility': 70, 'environmental_quality': 85,
|
||||
'cost_effectiveness': 75, 'community_support': 60, 'future_potential': 70},
|
||||
{'name': 'Site C', 'accessibility': 90, 'environmental_quality': 60,
|
||||
'cost_effectiveness': 80, 'community_support': 70, 'future_potential': 65},
|
||||
]
|
||||
|
||||
# 创建伦理检查器
|
||||
ethics_checker = EthicsChecker()
|
||||
ethics_checker.add_check(check_environmental_justice, "环境正义检查")
|
||||
ethics_checker.add_check(check_transparency, "透明度检查")
|
||||
ethics_checker.add_check(check_public_participation, "公众参与检查")
|
||||
|
||||
# 上下文
|
||||
context = {
|
||||
'sites': sites,
|
||||
'vulnerable_communities': [],
|
||||
'project_type': 'neutral',
|
||||
'affects_public': True,
|
||||
'public_participation': 'moderate'
|
||||
}
|
||||
|
||||
# 做出决策
|
||||
decision = selector.make_decision(context)
|
||||
|
||||
print(f"选中地点: {decision['site']['name']}")
|
||||
print(f"综合评分: {decision['score']:.2f}\n")
|
||||
|
||||
# 获取解释
|
||||
explanation = selector.explain_decision(decision, context)
|
||||
print("=== 决策解释 ===")
|
||||
print(f"决策: {explanation.decision}")
|
||||
print(f"\n理由:")
|
||||
for r in explanation.rationale:
|
||||
print(f" - {r}")
|
||||
print(f"\n关键因素:")
|
||||
for factor, value in explanation.key_factors.items():
|
||||
print(f" - {factor}: {value:.3f}")
|
||||
|
||||
# 伦理检查
|
||||
print(f"\n=== 伦理检查 ===")
|
||||
ethics_result = ethics_checker.check_decision(decision, context)
|
||||
if ethics_result['passed']:
|
||||
print("所有伦理检查通过")
|
||||
else:
|
||||
print("伦理检查发现问题:")
|
||||
for issue in ethics_result['issues']:
|
||||
print(f" - [{issue['check']}] {issue['reason']}")
|
||||
```
|
||||
|
||||
### 问责机制设计
|
||||
|
||||
```
|
||||
AI空间决策的问责框架
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 责任链 │
|
||||
│ │
|
||||
│ 数据提供者 ──→ 模型开发者 ──→ 系统集成者 ──→ 最终用户 │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ └── 决策责任 │
|
||||
│ │ │ └── 集成责任 │
|
||||
│ │ └── 模型责任 │
|
||||
│ └── 数据质量责任 │
|
||||
│ │
|
||||
│ 问责机制 │
|
||||
│ │
|
||||
│ 1. 文档化 (Documentation) │
|
||||
│ - 记录所有决策和数据来源 │
|
||||
│ - 保存模型版本和参数 │
|
||||
│ - 维护变更历史 │
|
||||
│ │
|
||||
│ 2. 审计 (Audit) │
|
||||
│ - 定期审查决策 │
|
||||
│ - 检查偏见和公平性 │
|
||||
│ - 验证技术正确性 │
|
||||
│ │
|
||||
│ 3. 申诉 (Appeal) │
|
||||
│ - 提供质疑决策的渠道 │
|
||||
│ - 建立复核机制 │
|
||||
│ - 允许人工干预 │
|
||||
│ │
|
||||
│ 4. 纠正 (Remedy) │
|
||||
│ - 发现错误后的补救措施 │
|
||||
│ - 对受影响方的补偿 │
|
||||
│ - 系统改进和预防 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### 案例1:城市绿地分布的算法偏见
|
||||
|
||||
**问题背景**:某城市使用AI优化绿地布局,但结果加剧了既有不平等。
|
||||
|
||||
```python
|
||||
"""
|
||||
问题代码示例:训练数据中的历史偏见
|
||||
"""
|
||||
|
||||
# 问题:使用历史公园使用数据来优化新公园位置
|
||||
def optimize_park_locations_biased(historical_usage_data, new_sites):
|
||||
"""
|
||||
有偏见的优化算法
|
||||
|
||||
问题:历史使用数据反映的是历史可达性,
|
||||
而非真实需求。服务不足的区域数据少,
|
||||
因此被算法继续忽视。
|
||||
"""
|
||||
# 简单优化:在历史使用高的地方附近选址
|
||||
scored = []
|
||||
for site in new_sites:
|
||||
# 靠近高使用区域得分高
|
||||
score = sum(
|
||||
usage for nearby, usage in historical_usage_data
|
||||
if distance(site, nearby) < 1000
|
||||
)
|
||||
scored.append((site, score))
|
||||
|
||||
# 选择得分最高的
|
||||
scored.sort(key=lambda x: x[1], reverse=True)
|
||||
return [s[0] for s in scored[:5]]
|
||||
|
||||
# 改进版本:考虑需求而非历史使用
|
||||
def optimize_park_locations_fair(demand_indicators, new_sites,
|
||||
equity_weight=0.5):
|
||||
"""
|
||||
公平的优化算法
|
||||
|
||||
考虑:
|
||||
1. 当前服务不足程度(需求)
|
||||
2. 人口密度
|
||||
3. 弱势群体分布
|
||||
"""
|
||||
scored = []
|
||||
for site in new_sites:
|
||||
# 服务不足得分
|
||||
underserved_score = calculate_underserved(site, demand_indicators)
|
||||
|
||||
# 效率得分(可达人口)
|
||||
efficiency_score = calculate_accessible_population(site)
|
||||
|
||||
# 综合得分,可调整公平权重
|
||||
score = (1 - equity_weight) * efficiency_score + \
|
||||
equity_weight * underserved_score
|
||||
|
||||
scored.append((site, score, {
|
||||
'underserved': underserved_score,
|
||||
'efficiency': efficiency_score
|
||||
}))
|
||||
|
||||
# 按综合得分排序
|
||||
scored.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
# 记录决策依据
|
||||
for site, score, details in scored:
|
||||
site['selection_score'] = score
|
||||
site['score_details'] = details
|
||||
|
||||
return [s[0] for s in scored[:5]]
|
||||
|
||||
def calculate_underserved(site, indicators):
|
||||
"""计算服务不足程度"""
|
||||
# 距离最近的现有设施
|
||||
distance_to_nearest = min_distance_to_parks(site)
|
||||
|
||||
# 附近人口中的弱势群体比例
|
||||
vulnerable_ratio = get_vulnerable_population_ratio(site)
|
||||
|
||||
# 服务不足 = 距离远 + 弱势群体多
|
||||
return distance_to_nearest * (1 + vulnerable_ratio)
|
||||
```
|
||||
|
||||
### 案例2:生态保护区的社区影响
|
||||
|
||||
**问题背景**:AI优化的生态廊道选址忽略了当地社区权益。
|
||||
|
||||
```python
|
||||
"""
|
||||
考虑多方利益的生态廊道选址
|
||||
"""
|
||||
|
||||
class EthicalCorridorSelector:
|
||||
"""伦理导向的廊道选址器"""
|
||||
|
||||
def __init__(self):
|
||||
self.stakeholders = {
|
||||
'ecology': {'weight': 0.4, 'concern': '生态连通性'},
|
||||
'community': {'weight': 0.3, 'concern': '社区利益'},
|
||||
'economy': {'weight': 0.2, 'concern': '经济成本'},
|
||||
'culture': {'weight': 0.1, 'concern': '文化价值'}
|
||||
}
|
||||
|
||||
def evaluate_corridor_route(self, route, context):
|
||||
"""
|
||||
评估廊道路线
|
||||
|
||||
返回:综合评分和各利益相关方的影响
|
||||
"""
|
||||
scores = {}
|
||||
|
||||
# 生态评分
|
||||
scores['ecology'] = self._evaluate_ecological_value(route, context)
|
||||
|
||||
# 社区评分
|
||||
scores['community'] = self._evaluate_community_impact(route, context)
|
||||
|
||||
# 经济评分
|
||||
scores['economy'] = self._evaluate_economic_cost(route, context)
|
||||
|
||||
# 文化评分
|
||||
scores['culture'] = self._evaluate_cultural_impact(route, context)
|
||||
|
||||
# 加权综合
|
||||
total = sum(
|
||||
scores[stakeholder] * self.stakeholders[stakeholder]['weight']
|
||||
for stakeholder in self.stakeholders
|
||||
)
|
||||
|
||||
# 检查任何一方的严重负面影响
|
||||
for stakeholder, score in scores.items():
|
||||
if score < 0.3: # 阈值
|
||||
return {
|
||||
'acceptable': False,
|
||||
'reason': f"{stakeholder}评分过低: {score:.2f}",
|
||||
'scores': scores,
|
||||
'total': total
|
||||
}
|
||||
|
||||
return {
|
||||
'acceptable': True,
|
||||
'total_score': total,
|
||||
'scores': scores,
|
||||
'breakdown': {
|
||||
stakeholder: {
|
||||
'score': scores[stakeholder],
|
||||
'weight': self.stakeholders[stakeholder]['weight'],
|
||||
'concern': self.stakeholders[stakeholder]['concern']
|
||||
}
|
||||
for stakeholder in self.stakeholders
|
||||
}
|
||||
}
|
||||
|
||||
def _evaluate_ecological_value(self, route, context):
|
||||
"""评估生态价值"""
|
||||
# 连通的源地质量
|
||||
source_quality = self._connected_source_quality(route, context)
|
||||
|
||||
# 廊道宽度
|
||||
width_score = min(route['width'] / 100, 1.0)
|
||||
|
||||
# 栖息地适宜性
|
||||
habitat_score = self._habitat_suitability(route, context)
|
||||
|
||||
return (source_quality + width_score + habitat_score) / 3
|
||||
|
||||
def _evaluate_community_impact(self, route, context):
|
||||
"""评估社区影响"""
|
||||
# 正面:休闲价值
|
||||
recreational_value = self._recreational_potential(route)
|
||||
|
||||
# 负面:拆迁、限制使用
|
||||
negative_impact = self._negative_community_impact(route, context)
|
||||
|
||||
return max(0, recreational_value - negative_impact)
|
||||
|
||||
def _evaluate_economic_cost(self, route, context):
|
||||
"""评估经济成本(分数越高表示成本越可接受)"""
|
||||
# 土地获取成本
|
||||
land_cost = route.get('land_cost', 0)
|
||||
|
||||
# 建设成本
|
||||
construction_cost = route.get('construction_cost', 0)
|
||||
|
||||
# 归一化:成本越低分数越高
|
||||
max_cost = context.get('max_budget', float('inf'))
|
||||
total_cost = land_cost + construction_cost
|
||||
|
||||
if total_cost > max_cost:
|
||||
return 0 # 超预算
|
||||
else:
|
||||
return 1 - (total_cost / max_cost) * 0.5
|
||||
|
||||
def _evaluate_cultural_impact(self, route, context):
|
||||
"""评估文化影响"""
|
||||
# 是否涉及文化遗产
|
||||
cultural_sites = route.get('cultural_sites', [])
|
||||
if cultural_sites:
|
||||
return 0.3 # 低分,需要特别处理
|
||||
|
||||
# 是否支持传统文化活动
|
||||
traditional_use = route.get('supports_traditional_use', False)
|
||||
if traditional_use:
|
||||
return 1.0
|
||||
|
||||
return 0.7 # 中性
|
||||
|
||||
# ... 其他辅助方法 ...
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 伦理导向的廊道选址 ===\n")
|
||||
|
||||
selector = EthicalCorridorSelector()
|
||||
|
||||
# 示例路线
|
||||
route = {
|
||||
'width': 80,
|
||||
'land_cost': 500000,
|
||||
'construction_cost': 1000000,
|
||||
'cultural_sites': [],
|
||||
'supports_traditional_use': True
|
||||
}
|
||||
|
||||
context = {
|
||||
'max_budget': 2000000,
|
||||
'ecological_data': {},
|
||||
'community_data': {}
|
||||
}
|
||||
|
||||
result = selector.evaluate_corridor_route(route, context)
|
||||
|
||||
if result['acceptable']:
|
||||
print(f"路线可接受,综合评分: {result['total_score']:.2f}")
|
||||
print("\n各利益相关方评分:")
|
||||
for stakeholder, info in result['breakdown'].items():
|
||||
print(f" {stakeholder}: {info['score']:.2f} "
|
||||
f"(权重: {info['weight']}, 关注: {info['concern']})")
|
||||
else:
|
||||
print(f"路线不可接受: {result['reason']}")
|
||||
```
|
||||
|
||||
### 案例3:透明度和可审计性
|
||||
|
||||
```python
|
||||
"""
|
||||
决策日志系统
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List
|
||||
|
||||
class DecisionLog:
|
||||
"""决策日志系统"""
|
||||
|
||||
def __init__(self, project_id: str):
|
||||
self.project_id = project_id
|
||||
self.entries: List[Dict] = []
|
||||
|
||||
def log_decision(self, decision_type: str, decision: Any,
|
||||
rationale: str, alternatives: List[Dict],
|
||||
metadata: Dict = None):
|
||||
"""记录决策"""
|
||||
entry = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'project_id': self.project_id,
|
||||
'decision_type': decision_type,
|
||||
'decision': decision,
|
||||
'rationale': rationale,
|
||||
'alternatives': alternatives,
|
||||
'metadata': metadata or {}
|
||||
}
|
||||
self.entries.append(entry)
|
||||
|
||||
def log_data_source(self, data_type: str, source: str,
|
||||
quality: Dict, limitations: List[str]):
|
||||
"""记录数据来源"""
|
||||
entry = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'project_id': self.project_id,
|
||||
'type': 'data_source',
|
||||
'data_type': data_type,
|
||||
'source': source,
|
||||
'quality': quality,
|
||||
'limitations': limitations
|
||||
}
|
||||
self.entries.append(entry)
|
||||
|
||||
def log_model_info(self, model_name: str, version: str,
|
||||
training_data: Dict, limitations: List[str]):
|
||||
"""记录模型信息"""
|
||||
entry = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'project_id': self.project_id,
|
||||
'type': 'model_info',
|
||||
'model_name': model_name,
|
||||
'version': version,
|
||||
'training_data': training_data,
|
||||
'limitations': limitations
|
||||
}
|
||||
self.entries.append(entry)
|
||||
|
||||
def export_audit_report(self) -> str:
|
||||
"""导出审计报告"""
|
||||
report = {
|
||||
'project_id': self.project_id,
|
||||
'export_time': datetime.now().isoformat(),
|
||||
'entries': self.entries,
|
||||
'summary': self._generate_summary()
|
||||
}
|
||||
return json.dumps(report, indent=2, ensure_ascii=False)
|
||||
|
||||
def _generate_summary(self) -> Dict:
|
||||
"""生成摘要"""
|
||||
summary = {
|
||||
'total_entries': len(self.entries),
|
||||
'decision_types': {},
|
||||
'data_sources': [],
|
||||
'models_used': []
|
||||
}
|
||||
|
||||
for entry in self.entries:
|
||||
if entry.get('type') == 'data_source':
|
||||
summary['data_sources'].append(entry['data_type'])
|
||||
elif entry.get('type') == 'model_info':
|
||||
summary['models_used'].append(entry['model_name'])
|
||||
elif 'decision_type' in entry:
|
||||
dt = entry['decision_type']
|
||||
summary['decision_types'][dt] = \
|
||||
summary['decision_types'].get(dt, 0) + 1
|
||||
|
||||
return summary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **价值权衡**:当生态目标和社会目标冲突时,应该如何权衡?谁有权决定?
|
||||
|
||||
2. **偏见识别**:你的空间分析可能隐含哪些偏见?如何检测?
|
||||
|
||||
3. **透明度边界**:哪些决策细节必须公开?哪些可以保密?
|
||||
|
||||
4. **长期责任**:AI辅助的决策出现问题后,如何追溯和纠正?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **伦理审计**:对你做过的一个空间项目进行伦理审计
|
||||
|
||||
2. **利益相关者地图**:绘制项目的利益相关者及其关注点
|
||||
|
||||
3. **透明度检查**:为你的分析流程建立可审计的文档体系
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Weapons of Math Destruction"** (Cathy O'Neil) - 算法的社会影响
|
||||
- **"The Alignment Problem"** (Brian Christian) - AI对齐问题
|
||||
- **"Spatial Justice"** (Edward Soja) - 空间正义理论
|
||||
- UN-Habitat's ethics guidelines for spatial planning
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **空间决策具有深刻的伦理维度**,影响资源分配和社会正义
|
||||
2. **AI可能放大既有偏见**,需要主动的公平性设计
|
||||
3. **可解释性是负责任AI的基础**,决策过程应可审查
|
||||
4. **建立清晰的问责机制**,明确各方责任
|
||||
5. **伦理思考应该贯穿整个项目生命周期**,而非事后补充
|
||||
@@ -0,0 +1,798 @@
|
||||
# 05.3 技术迭代与持久知识
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 在快速变化的技术环境中,哪些知识会持久?
|
||||
> 如何判断新技术值得投入时间学习?
|
||||
> 如何建立可持续的知识更新机制?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 技术变化的层次
|
||||
|
||||
理解技术变化的本质,帮助区分短暂潮流和持久价值:
|
||||
|
||||
```
|
||||
技术变化的四个层次
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ Level 4: 应用层 (Application) 变化最快 │
|
||||
│ ─────────────────────────────────────────────────── │
|
||||
│ • 具体工具和框架 (e.g., QGIS 3.x → 4.x) │
|
||||
│ • API和语法细节 │
|
||||
│ • 特定库的使用模式 │
|
||||
│ 半衰期: 1-2年 │
|
||||
│ │
|
||||
│ Level 3: 方法层 (Methodology) │
|
||||
│ ─────────────────────────────────────────────────── │
|
||||
│ • 空间分析方法 (e.g., Circuit theory, Least-cost path) │
|
||||
│ • 工作流设计模式 │
|
||||
│ • 数据处理策略 │
|
||||
│ 半衰期: 5-10年 │
|
||||
│ │
|
||||
│ Level 2: 原理层 (Principle) │
|
||||
│ ─────────────────────────────────────────────────── │
|
||||
│ • 空间统计原理 │
|
||||
│ • 图论和网络分析 │
|
||||
│ • 优化理论 │
|
||||
│ 半衰期: 20-50年 │
|
||||
│ │
|
||||
│ Level 1: 思维层 (Thinking) 变化最慢 │
|
||||
│ ─────────────────────────────────────────────────── │
|
||||
│ • 空间思维方式 │
|
||||
│ • 系统思维 │
|
||||
│ • 批判性思维 │
|
||||
│ 半衰期: 基本不变 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
学习策略:向上投资
|
||||
更多时间投入在原理层和思维层
|
||||
应用层的知识随用随学
|
||||
```
|
||||
|
||||
### 什么在变,什么不变
|
||||
|
||||
| 变化的 | 相对稳定的 | 持久不变的 |
|
||||
|-------|-----------|-----------|
|
||||
| 工具版本 | 空间分析方法 | 空间问题本质 |
|
||||
| API设计 | 数据结构设计 | 几何公理 |
|
||||
| 算法实现 | 架构模式 | 数学原理 |
|
||||
| 命令语法 | 工作流逻辑 | 项目目标 |
|
||||
| 框架生态 | 问题分解方法 | 人类需求 |
|
||||
| 数据格式 | 验证策略 | 质量标准 |
|
||||
|
||||
### 持久知识框架
|
||||
|
||||
```python
|
||||
"""
|
||||
持久知识管理系统
|
||||
"""
|
||||
from typing import Dict, List, Set, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
class KnowledgeLevel(Enum):
|
||||
"""知识层次"""
|
||||
APPLICATION = "application" # 应用层:快速变化
|
||||
METHODOLOGY = "methodology" # 方法层:中速变化
|
||||
PRINCIPLE = "principle" # 原理层:慢速变化
|
||||
THINKING = "thinking" # 思维层:基本不变
|
||||
|
||||
class KnowledgeStatus(Enum):
|
||||
"""知识状态"""
|
||||
NEW = "new" # 新知识
|
||||
LEARNING = "learning" # 学习中
|
||||
MASTERED = "mastered" # 已掌握
|
||||
OUTDATED = "outdated" # 已过时
|
||||
|
||||
@dataclass
|
||||
class KnowledgeItem:
|
||||
"""知识条目"""
|
||||
name: str
|
||||
level: KnowledgeLevel
|
||||
status: KnowledgeStatus
|
||||
related_principles: List[str] # 关联的原理层知识
|
||||
practical_applications: List[str] # 实际应用场景
|
||||
last_reviewed: str # ISO日期
|
||||
confidence: float # 0-1
|
||||
|
||||
class KnowledgeSystem:
|
||||
"""个人知识系统"""
|
||||
|
||||
def __init__(self):
|
||||
self.items: Dict[str, KnowledgeItem] = {}
|
||||
self.principles: Set[str] = set()
|
||||
self.connections: Dict[str, List[str]] = {} # 知识关联图
|
||||
|
||||
def add_principle(self, principle: str, description: str):
|
||||
"""添加核心原理(持久知识)"""
|
||||
self.principles.add(principle)
|
||||
print(f"添加核心原理: {principle}")
|
||||
|
||||
def learn_technology(self, tech_name: str,
|
||||
related_principles: List[str],
|
||||
applications: List[str]):
|
||||
"""
|
||||
学习新技术时,关联到原理
|
||||
|
||||
这样即使技术过时,原理知识仍然有用
|
||||
"""
|
||||
# 检查关联的原理是否都已记录
|
||||
for p in related_principles:
|
||||
if p not in self.principles:
|
||||
print(f"警告: 关联原理 '{p}' 不在知识库中")
|
||||
|
||||
# 添加知识条目
|
||||
self.items[tech_name] = KnowledgeItem(
|
||||
name=tech_name,
|
||||
level=self._guess_level(tech_name),
|
||||
status=KnowledgeStatus.LEARNING,
|
||||
related_principles=related_principles,
|
||||
practical_applications=applications,
|
||||
last_reviewed="",
|
||||
confidence=0.5
|
||||
)
|
||||
|
||||
# 建立关联
|
||||
self._build_connections(tech_name, related_principles)
|
||||
|
||||
def review_knowledge(self, tech_name: str, current_value: int):
|
||||
"""
|
||||
定期回顾知识
|
||||
|
||||
current_value: 0-10,该技术当前的价值评分
|
||||
"""
|
||||
if tech_name not in self.items:
|
||||
return
|
||||
|
||||
item = self.items[tech_name]
|
||||
|
||||
# 如果评分低,标记为过时
|
||||
if current_value < 3:
|
||||
item.status = KnowledgeStatus.OUTDATED
|
||||
print(f"{tech_name} 已过时,但原理知识保留:")
|
||||
for p in item.related_principles:
|
||||
print(f" - {p}")
|
||||
else:
|
||||
item.status = KnowledgeStatus.MASTERED
|
||||
item.confidence = min(current_value / 10, 1.0)
|
||||
|
||||
item.last_reviewed = self._get_date()
|
||||
|
||||
def get_learning_priority(self) -> List[str]:
|
||||
"""
|
||||
获取学习优先级
|
||||
|
||||
策略:优先学习那些
|
||||
1. 关联多个重要原理的技术
|
||||
2. 当前价值高但尚未掌握的
|
||||
"""
|
||||
priorities = []
|
||||
|
||||
for name, item in self.items.items():
|
||||
if item.status == KnowledgeStatus.OUTDATED:
|
||||
continue
|
||||
|
||||
# 计算优先级分数
|
||||
score = 0
|
||||
|
||||
# 原理覆盖度
|
||||
score += len(item.related_principles) * 10
|
||||
|
||||
# 当前价值
|
||||
if item.status == KnowledgeStatus.NEW:
|
||||
score += 20
|
||||
|
||||
# 掌握度(越低越需要学习)
|
||||
score += (1 - item.confidence) * 30
|
||||
|
||||
priorities.append((name, score))
|
||||
|
||||
priorities.sort(key=lambda x: x[1], reverse=True)
|
||||
return [p[0] for p in priorities]
|
||||
|
||||
def _guess_level(self, name: str) -> KnowledgeLevel:
|
||||
"""根据名称猜测知识层次"""
|
||||
# 工具、框架通常是应用层
|
||||
tool_keywords = ['qgis', 'arcgis', 'python', 'library', 'api']
|
||||
if any(kw in name.lower() for kw in tool_keywords):
|
||||
return KnowledgeLevel.APPLICATION
|
||||
|
||||
# 方法类词汇通常是方法层
|
||||
method_keywords = ['analysis', 'method', 'approach', 'workflow']
|
||||
if any(kw in name.lower() for kw in method_keywords):
|
||||
return KnowledgeLevel.METHODOLOGY
|
||||
|
||||
return KnowledgeLevel.PRINCIPLE
|
||||
|
||||
def _build_connections(self, tech: str, principles: List[str]):
|
||||
"""建立知识关联"""
|
||||
self.connections[tech] = principles
|
||||
|
||||
def _get_date(self) -> str:
|
||||
"""获取当前日期"""
|
||||
from datetime import datetime
|
||||
return datetime.now().isoformat()
|
||||
|
||||
def get_principle_coverage(self) -> Dict[str, List[str]]:
|
||||
"""
|
||||
获取原理覆盖情况
|
||||
|
||||
返回:每个原理相关的技术
|
||||
"""
|
||||
coverage = {p: [] for p in self.principles}
|
||||
|
||||
for tech, principles in self.connections.items():
|
||||
for p in principles:
|
||||
if p in coverage:
|
||||
coverage[p].append(tech)
|
||||
|
||||
return coverage
|
||||
|
||||
# === 示例:空间AI知识体系 ===
|
||||
|
||||
def build_spatial_ai_knowledge_system():
|
||||
"""构建空间AI知识体系"""
|
||||
ks = KnowledgeSystem()
|
||||
|
||||
# 添加核心原理(持久知识)
|
||||
core_principles = [
|
||||
("空间自相关", "空间上相近的事物更相似"),
|
||||
("尺度效应", "空间模式随观测尺度变化"),
|
||||
("拓扑关系", "空间对象之间的邻接、包含等关系"),
|
||||
("距离衰减", "相互作用随距离减弱"),
|
||||
("最小阻力路径", "考虑阻力的最优路径"),
|
||||
("源-汇动态", "资源在源和汇之间的流动"),
|
||||
("景观格局", "空间配置对生态过程的影响"),
|
||||
]
|
||||
|
||||
for principle, description in core_principles:
|
||||
ks.add_principle(principle, description)
|
||||
|
||||
# 学习具体技术(关联到原理)
|
||||
technologies = [
|
||||
("Morpheus (软件)", ["景观格局"], ["景观指数计算"]),
|
||||
("Circuitscape", ["最小阻力路径", "源-汇动态"], ["生态连通性分析"]),
|
||||
("Linkage Mapper", ["最小阻力路径"], ["廊道识别"]),
|
||||
("Geoda", ["空间自相关"], ["空间自相关分析"]),
|
||||
("QGIS Processing", [], ["空间分析自动化"]),
|
||||
]
|
||||
|
||||
for tech, principles, apps in technologies:
|
||||
ks.learn_technology(tech, principles, apps)
|
||||
|
||||
return ks
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 持久知识管理系统 ===\n")
|
||||
|
||||
ks = build_spatial_ai_knowledge_system()
|
||||
|
||||
print("\n--- 原理覆盖情况 ---")
|
||||
coverage = ks.get_principle_coverage()
|
||||
for principle, techs in coverage.items():
|
||||
if techs:
|
||||
print(f"\n{principle}:")
|
||||
for tech in techs:
|
||||
print(f" - {tech}")
|
||||
else:
|
||||
print(f"\n{principle}: (尚无相关技术)")
|
||||
|
||||
print("\n--- 学习优先级 ---")
|
||||
priorities = ks.get_learning_priority()
|
||||
for i, tech in enumerate(priorities[:5], 1):
|
||||
print(f"{i}. {tech}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 技术评估框架
|
||||
|
||||
在决定是否学习某项新技术时,使用系统化的评估:
|
||||
|
||||
```python
|
||||
"""
|
||||
技术价值评估框架
|
||||
"""
|
||||
from typing import Dict, List, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class TechAssessment:
|
||||
"""技术评估结果"""
|
||||
name: str
|
||||
total_score: float
|
||||
dimension_scores: Dict[str, float]
|
||||
recommendation: str
|
||||
reasoning: List[str]
|
||||
|
||||
class TechnologyEvaluator:
|
||||
"""技术评估器"""
|
||||
|
||||
def __init__(self):
|
||||
self.dimensions = {
|
||||
'principle_value': 0.3, # 原理价值:是否关联深层原理
|
||||
'applicability': 0.25, # 适用性:应用范围广度
|
||||
'longevity': 0.2, # 持久性:预计技术寿命
|
||||
'community': 0.15, # 社区:生态系统活跃度
|
||||
'learning_cost': 0.1, # 学习成本(负向)
|
||||
}
|
||||
|
||||
def evaluate(self, tech_name: str,
|
||||
principle_links: List[str],
|
||||
applications: List[str],
|
||||
maturity: str,
|
||||
community_size: str,
|
||||
estimated_hours: int) -> TechAssessment:
|
||||
|
||||
scores = {}
|
||||
|
||||
# 1. 原理价值
|
||||
scores['principle_value'] = min(len(principle_links) * 0.2, 1.0)
|
||||
|
||||
# 2. 适用性
|
||||
scores['applicability'] = min(len(applications) * 0.15, 1.0)
|
||||
|
||||
# 3. 持久性
|
||||
longevity_scores = {
|
||||
'concept': 1.0,
|
||||
'standard': 0.8,
|
||||
'emerging': 0.5,
|
||||
'experimental': 0.2
|
||||
}
|
||||
scores['longevity'] = longevity_scores.get(maturity, 0.5)
|
||||
|
||||
# 4. 社区
|
||||
community_scores = {
|
||||
'large': 1.0,
|
||||
'medium': 0.7,
|
||||
'small': 0.4,
|
||||
'tiny': 0.2
|
||||
}
|
||||
scores['community'] = community_scores.get(community_size, 0.5)
|
||||
|
||||
# 5. 学习成本(负向)
|
||||
scores['learning_cost'] = max(0, 1 - estimated_hours / 100)
|
||||
|
||||
# 计算加权总分
|
||||
total = sum(
|
||||
scores[dim] * weight
|
||||
for dim, weight in self.dimensions.items()
|
||||
)
|
||||
|
||||
# 生成建议
|
||||
recommendation, reasoning = self._generate_recommendation(
|
||||
scores, total, estimated_hours
|
||||
)
|
||||
|
||||
return TechAssessment(
|
||||
name=tech_name,
|
||||
total_score=total,
|
||||
dimension_scores=scores,
|
||||
recommendation=recommendation,
|
||||
reasoning=reasoning
|
||||
)
|
||||
|
||||
def _generate_recommendation(self, scores: Dict, total: float,
|
||||
hours: int) -> tuple:
|
||||
"""生成建议"""
|
||||
reasoning = []
|
||||
|
||||
# 分析各维度
|
||||
if scores['principle_value'] < 0.3:
|
||||
reasoning.append("原理价值较低,可能只是工具层知识")
|
||||
elif scores['principle_value'] > 0.8:
|
||||
reasoning.append("关联多个核心原理,学习价值高")
|
||||
|
||||
if scores['longevity'] < 0.5:
|
||||
reasoning.append("技术成熟度低,可能快速变化")
|
||||
elif scores['longevity'] > 0.8:
|
||||
reasoning.append("技术相对稳定,知识可持久")
|
||||
|
||||
if hours > 50 and total < 0.6:
|
||||
reasoning.append(f"学习成本高({hours}h)但综合价值低")
|
||||
|
||||
# 总体建议
|
||||
if total > 0.7:
|
||||
rec = "强烈推荐学习"
|
||||
elif total > 0.5:
|
||||
rec = "值得学习"
|
||||
elif total > 0.3:
|
||||
rec = "按需学习"
|
||||
else:
|
||||
rec = "不推荐投入时间"
|
||||
|
||||
return rec, reasoning
|
||||
|
||||
# === 技术评估示例 ===
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 技术价值评估 ===\n")
|
||||
|
||||
evaluator = TechnologyEvaluator()
|
||||
|
||||
technologies = [
|
||||
{
|
||||
'name': 'Spatial SQL (PostGIS)',
|
||||
'principles': ['拓扑关系', '空间查询', '集合操作'],
|
||||
'applications': ['数据管理', '空间分析', '服务提供'],
|
||||
'maturity': 'standard',
|
||||
'community': 'large',
|
||||
'hours': 40
|
||||
},
|
||||
{
|
||||
'name': '某新兴AI框架',
|
||||
'principles': ['深度学习'],
|
||||
'applications': ['图像识别'],
|
||||
'maturity': 'experimental',
|
||||
'community': 'small',
|
||||
'hours': 80
|
||||
},
|
||||
{
|
||||
'name': '景观格局分析理论',
|
||||
'principles': ['景观格局', '尺度效应', '空间异质性'],
|
||||
'applications': ['生态评价', '景观规划', '环境评估'],
|
||||
'maturity': 'concept',
|
||||
'community': 'medium',
|
||||
'hours': 30
|
||||
},
|
||||
]
|
||||
|
||||
for tech in technologies:
|
||||
result = evaluator.evaluate(**tech)
|
||||
print(f"\n{result.name}")
|
||||
print(f"总分: {result.total_score:.2f}")
|
||||
print(f"建议: {result.recommendation}")
|
||||
print("各维度:")
|
||||
for dim, score in result.dimension_scores.items():
|
||||
bar = "█" * int(score * 20)
|
||||
print(f" {dim}: {bar} {score:.2f}")
|
||||
print("理由:")
|
||||
for r in result.reasoning:
|
||||
print(f" - {r}")
|
||||
```
|
||||
|
||||
### 持续学习策略
|
||||
|
||||
```
|
||||
持续学习的三角模型
|
||||
|
||||
┌─────────────────┐
|
||||
│ 主动学习 │
|
||||
│ - 探索新领域 │
|
||||
│ - 预测趋势 │
|
||||
└────────┬────────┘
|
||||
│
|
||||
│
|
||||
┌─────────────────┼─────────────────┐
|
||||
│ │ │
|
||||
┌──────┴──────┐ ┌─────┴─────┐ ┌─────┴─────┐
|
||||
│ 响应式学习 │ │ 反思整合 │ │ 社区连接 │
|
||||
│ │ │ │ │ │
|
||||
│ - 解决问题 │ │ - 定期回顾│ │ - 参与讨论│
|
||||
│ - 查漏补缺 │ │ - 写笔记 │ │ - 分享知识│
|
||||
│ - 即时学习 │ │ - 建立连接│ │ - 获取反馈│
|
||||
└─────────────┘ └───────────┘ └───────────┘
|
||||
|
||||
各部分的实践方法:
|
||||
|
||||
1. 响应式学习
|
||||
- 遇到问题时记录下来
|
||||
- 快速查找解决方案
|
||||
- 事后总结为知识条目
|
||||
|
||||
2. 主动学习
|
||||
- 关注领域顶级会议/期刊
|
||||
- 订阅精选博客/通讯
|
||||
- 定期探索新技术
|
||||
|
||||
3. 反思整合
|
||||
- 每周/每月回顾
|
||||
- 更新知识图谱
|
||||
- 重写过时笔记
|
||||
|
||||
4. 社区连接
|
||||
- 加入专业社区
|
||||
- 参与开源项目
|
||||
- 组织学习小组
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### 案例1:从ArcGIS到QGIS的迁移
|
||||
|
||||
**背景**:GIS工具的变化,但原理知识保持不变。
|
||||
|
||||
```
|
||||
迁移时的知识对比:
|
||||
|
||||
ArcGIS (ArcPy) QGIS (PyQGIS)
|
||||
─────────────────────────────────────────────────
|
||||
arcpy.mp QgsProject
|
||||
│ ├── Map │ ├── QgsLayout
|
||||
│ └── Layer │ └── QgsMapLayer
|
||||
│
|
||||
arcpy.sa (Spatial Analyst) Processing algorithms
|
||||
│ ├── Raster calculator │ ├── QgsProcessingAlgorithm
|
||||
│ └── Zonal statistics │ └── QgsProcessingContext
|
||||
│
|
||||
ModelBuilder Graphical Modeler
|
||||
│ ├── Intermediate data │ ├── Input/Output
|
||||
│ └── Iterators │ └── Modeller algorithms
|
||||
|
||||
不变的核心知识:
|
||||
• 理解栅格和矢量数据结构
|
||||
• 理解投影和坐标系统
|
||||
• 理解空间分析的逻辑流程
|
||||
• 理解数据模型和拓扑关系
|
||||
|
||||
变化的部分:
|
||||
• API和类名
|
||||
• 具体操作语法
|
||||
• 界面操作方式
|
||||
|
||||
迁移策略:
|
||||
1. 用一个下午对照API文档做转换
|
||||
2. 建立常用操作的对照表
|
||||
3. 重点理解新工具的架构设计
|
||||
```
|
||||
|
||||
### 案例2:深度学习在遥感中的应用
|
||||
|
||||
**背景**:新技术快速迭代,但基础概念相对稳定。
|
||||
|
||||
```python
|
||||
"""
|
||||
深度学习技术栈的层次分析
|
||||
"""
|
||||
|
||||
# 短半衰期(1-2年)- 随用随学
|
||||
short_lived = [
|
||||
"特定模型架构 (e.g., U-Net变体)",
|
||||
"训练框架 (e.g., PyTorch vs TensorFlow)",
|
||||
"预处理工具",
|
||||
"特定数据集格式"
|
||||
]
|
||||
|
||||
# 中半衰期(5-10年)- 重点学习
|
||||
medium_lived = [
|
||||
"卷积操作原理",
|
||||
"迁移学习策略",
|
||||
"数据增强方法",
|
||||
"模型评估指标",
|
||||
"特征可视化技术"
|
||||
]
|
||||
|
||||
# 长半衰期(20+年)- 深入理解
|
||||
long_lived = [
|
||||
"梯度下降优化原理",
|
||||
"过拟合与正则化",
|
||||
"偏差-方差权衡",
|
||||
"交叉验证",
|
||||
"损失函数设计"
|
||||
]
|
||||
|
||||
# 空间AI特有的持久知识
|
||||
spatial_essentials = [
|
||||
"空间自相关及其对训练集的影响",
|
||||
"空间交叉验证(防止空间泄漏)",
|
||||
"尺度效应与感受野",
|
||||
"空间不确定性量化",
|
||||
"可解释性在空间决策中的重要性"
|
||||
]
|
||||
|
||||
print("深度学习在遥感中的知识层次\n")
|
||||
print("短期(随用随学):")
|
||||
for item in short_lived:
|
||||
print(f" - {item}")
|
||||
print("\n中期(重点学习):")
|
||||
for item in medium_lived:
|
||||
print(f" - {item}")
|
||||
print("\n长期(深入理解):")
|
||||
for item in long_lived:
|
||||
print(f" - {item}")
|
||||
print("\n空间AI核心:")
|
||||
for item in spatial_essentials:
|
||||
print(f" - {item}")
|
||||
```
|
||||
|
||||
### 案例3:个人知识更新机制
|
||||
|
||||
```python
|
||||
"""
|
||||
定期知识回顾机制
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict
|
||||
|
||||
class LearningCalendar:
|
||||
"""学习日历"""
|
||||
|
||||
def __init__(self):
|
||||
self.review_queue = []
|
||||
self.last_review = {}
|
||||
|
||||
def schedule_review(self, topic: str, level: str, initial_days: int):
|
||||
"""
|
||||
安排复习
|
||||
|
||||
使用间隔重复策略
|
||||
"""
|
||||
schedule = {
|
||||
'new': [1, 3, 7, 14, 30], # 新知识:密集复习
|
||||
'stable': [30, 90, 180, 365], # 稳定知识:稀疏复习
|
||||
'archived': [365, 730] # 归档知识:年度检查
|
||||
}
|
||||
|
||||
for days in schedule.get(level, schedule['stable']):
|
||||
review_date = datetime.now() + timedelta(days=days)
|
||||
self.review_queue.append({
|
||||
'topic': topic,
|
||||
'date': review_date,
|
||||
'level': level
|
||||
})
|
||||
|
||||
def get_due_reviews(self) -> List[Dict]:
|
||||
"""获取到期的复习"""
|
||||
now = datetime.now()
|
||||
return [
|
||||
item for item in self.review_queue
|
||||
if item['date'] <= now
|
||||
]
|
||||
|
||||
def complete_review(self, topic: str, quality: int):
|
||||
"""
|
||||
完成复习
|
||||
|
||||
quality: 1-5,复习质量评分
|
||||
"""
|
||||
self.last_review[topic] = {
|
||||
'date': datetime.now(),
|
||||
'quality': quality
|
||||
}
|
||||
|
||||
# 根据质量调整下次复习时间
|
||||
if quality >= 4:
|
||||
# 掌握良好,延长时间间隔
|
||||
pass
|
||||
elif quality <= 2:
|
||||
# 掌握不好,重新安排密集复习
|
||||
pass
|
||||
|
||||
class KnowledgeJournal:
|
||||
"""知识日志"""
|
||||
|
||||
def __init__(self):
|
||||
self.entries = []
|
||||
|
||||
def log_learning(self, topic: str, what: str, why: str,
|
||||
how: str, connections: List[str]):
|
||||
"""
|
||||
记录学习
|
||||
|
||||
使用What-Why-How框架
|
||||
"""
|
||||
entry = {
|
||||
'date': datetime.now().isoformat(),
|
||||
'topic': topic,
|
||||
'what': what, # 学到了什么
|
||||
'why': why, # 为什么重要
|
||||
'how': how, # 如何应用
|
||||
'connections': connections, # 与已有知识的连接
|
||||
'questions': [] # 未解决的问题
|
||||
}
|
||||
self.entries.append(entry)
|
||||
|
||||
def log_question(self, topic: str, question: str):
|
||||
"""记录问题"""
|
||||
# 找到相关条目或创建新的
|
||||
for entry in self.entries:
|
||||
if entry['topic'] == topic:
|
||||
entry['questions'].append({
|
||||
'question': question,
|
||||
'date': datetime.now().isoformat()
|
||||
})
|
||||
return
|
||||
|
||||
def get_review_prompt(self, topic: str) -> str:
|
||||
"""生成复习提示"""
|
||||
for entry in self.entries:
|
||||
if entry['topic'] == topic:
|
||||
prompt = f"""
|
||||
复习主题: {topic}
|
||||
|
||||
学习内容:
|
||||
{entry['what']}
|
||||
|
||||
重要性:
|
||||
{entry['why']}
|
||||
|
||||
应用方式:
|
||||
{entry['how']}
|
||||
|
||||
关联知识:
|
||||
{', '.join(entry['connections'])}
|
||||
|
||||
未解决问题:
|
||||
{chr(10).join(q['question'] for q in entry['questions'])}
|
||||
|
||||
复习问题:
|
||||
1. 这个知识的核心是什么?
|
||||
2. 我在哪些场景中应用过它?
|
||||
3. 它与哪些其他知识相关?
|
||||
4. 我还有哪些不明白的地方?
|
||||
"""
|
||||
return prompt
|
||||
return f"未找到主题: {topic}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 个人知识更新系统 ===\n")
|
||||
|
||||
calendar = LearningCalendar()
|
||||
journal = KnowledgeJournal()
|
||||
|
||||
# 示例:学习一个新技术
|
||||
journal.log_learning(
|
||||
topic="空间交叉验证",
|
||||
what="防止训练集和测试集空间自相关导致的模型过拟合",
|
||||
why="标准交叉验证假设样本独立,空间数据违反此假设",
|
||||
how="使用空间阻塞(Spatial Block)或缓冲区划分",
|
||||
connections=["空间自相关", "模型评估", "过拟合"]
|
||||
)
|
||||
|
||||
# 安排复习
|
||||
calendar.schedule_review("空间交叉验证", "new", 1)
|
||||
|
||||
print("知识已记录,复习计划已安排")
|
||||
|
||||
# 获取复习提示
|
||||
prompt = journal.get_review_prompt("空间交叉验证")
|
||||
print("\n--- 复习提示 ---")
|
||||
print(prompt)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **知识审计**:你花时间学习的技能中,哪些已经过时?哪些仍然有价值?
|
||||
|
||||
2. **学习策略**:你目前的学习时间分配在哪个层次?是否需要调整?
|
||||
|
||||
3. **趋势判断**:你如何区分暂时热潮和真正重要的趋势?
|
||||
|
||||
4. **知识管理**:你如何追踪和管理自己的知识体系?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **知识分层**:列出你最近学习的5项技术,分类到4个层次
|
||||
|
||||
2. **技术评估**:使用评估框架评估一个你正在考虑学习的技术
|
||||
|
||||
3. **回顾机制**:建立你自己的知识回顾系统
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Make It Stick"** - 学习的科学
|
||||
- **"Ultralearning"** (Scott Young) - 高效自学方法
|
||||
- **"Range"** (David Epstein) - 广度vs深度的权衡
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **技术变化有层次**,原理层和思维层的变化远慢于应用层
|
||||
2. **学习投资应向上倾斜**,更多投入在持久知识上
|
||||
3. **建立技术评估框架**,系统化判断学习价值
|
||||
4. **设计持续学习机制**,包括回顾、整合和社区参与
|
||||
5. **理解什么不变比追逐什么在变更重要**
|
||||
@@ -0,0 +1,985 @@
|
||||
# 05.4 空间AI的未来方向
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 空间大模型会如何改变空间分析和规划?
|
||||
> 多模态AI如何融合遥感、地图、文本等异构数据?
|
||||
> 具身智能与空间智能的关系是什么?
|
||||
|
||||
---
|
||||
|
||||
## 概念讲解
|
||||
|
||||
### 空间大模型 (Spatial Large Language Models)
|
||||
|
||||
空间大模型是将空间理解能力融入大规模语言模型的新方向:
|
||||
|
||||
```
|
||||
空间大模型的演进
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 阶段1: 通用LLM │
|
||||
│ ─────────────────────────────────────────────────── │
|
||||
│ • ChatGPT, Claude等 │
|
||||
│ • 可以讨论空间概念,但无法真正理解 │
|
||||
│ • 依赖外部工具进行空间计算 │
|
||||
│ │
|
||||
│ 阶段2: 空间增强LLM (Spatially-Enhanced LLM) │
|
||||
│ ─────────────────────────────────────────────────── │
|
||||
│ • 集成GIS工具和空间数据库 │
|
||||
│ • 可以执行空间查询和分析 │
|
||||
│ • 例如:Llama with GIS tools, GeoLLM │
|
||||
│ │
|
||||
│ 阶段3: 空间原生LLM (Spatially-Native LLM) [发展中] │
|
||||
│ ─────────────────────────────────────────────────── │
|
||||
│ • 空间概念嵌入模型架构 │
|
||||
│ • 原生支持空间推理和几何运算 │
|
||||
│ • 理解投影、拓扑、尺度等 │
|
||||
│ │
|
||||
│ 阶段4: 世界模型 (World Models) [未来] │
|
||||
│ ─────────────────────────────────────────────────── │
|
||||
│ • 内化对世界的空间理解 │
|
||||
│ • 可以模拟和预测空间变化 │
|
||||
│ • 支持复杂的空间规划任务 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 空间大模型的核心能力
|
||||
|
||||
| 能力 | 描述 | 当前状态 |
|
||||
|-----|------|---------|
|
||||
| **空间概念理解** | 理解距离、方向、邻近、包含等 | 部分实现 |
|
||||
| **空间推理** | 基于空间关系的逻辑推理 | 早期阶段 |
|
||||
| **几何操作** | 直接进行缓冲、叠加等运算 | 依赖工具 |
|
||||
| **空间视觉理解** | 从地图/遥感图像提取信息 | 快速发展 |
|
||||
| **多尺度理解** | 处理不同尺度的空间问题 | 研究中 |
|
||||
| **时空间建模** | 理解空间随时间的变化 | 早期阶段 |
|
||||
|
||||
### 多模态空间AI
|
||||
|
||||
```
|
||||
多模态空间数据融合
|
||||
|
||||
┌─────────────────┐
|
||||
│ 空间大模型 │
|
||||
│ │
|
||||
│ 统一的表示 │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌───────┴───────┐ ┌────────┴────────┐ ┌─────┴─────┐
|
||||
│ 视觉模态 │ │ 文本模态 │ │ 结构模态 │
|
||||
│ │ │ │ │ │
|
||||
│ • 遥感影像 │ │ • 描述性文本 │ │ • 矢量数据│
|
||||
│ • 航拍照片 │ │ • 规划文档 │ │ • 拓扑关系│
|
||||
│ • 街景图像 │ │ • 专家知识 │ │ • 网络结构│
|
||||
│ • 地图截图 │ │ • 社交媒体 │ │ • 统计数据│
|
||||
└──────────────┘ └─────────────────┘ └───────────┘
|
||||
|
||||
融合挑战:
|
||||
1. 对齐:不同模态的空间对齐
|
||||
2. 互补:利用各模态的优势
|
||||
3. 冲突:处理模态间的不一致
|
||||
4. 稀疏性:某些区域数据缺失
|
||||
```
|
||||
|
||||
### 具身智能与空间智能
|
||||
|
||||
```
|
||||
具身智能 (Embodied AI)
|
||||
│
|
||||
└── 具有物理身体、能与真实世界交互的AI
|
||||
|
||||
空间智能是具身智能的基础:
|
||||
|
||||
具身智能需要的空间能力
|
||||
├── 空间感知 (Perception)
|
||||
│ ├── 视觉SLAM (同步定位与地图构建)
|
||||
│ ├── 物体识别与定位
|
||||
│ └── 场景理解
|
||||
│
|
||||
├── 空间推理 (Reasoning)
|
||||
│ ├── 路径规划
|
||||
│ ├── 障碍物避让
|
||||
│ └── 操作空间估计
|
||||
│
|
||||
├── 空间行动 (Action)
|
||||
│ ├── 导航
|
||||
│ ├── 物体操作
|
||||
│ └── 与环境交互
|
||||
│
|
||||
└── 空间学习 (Learning)
|
||||
├── 环境地图构建
|
||||
├── 动态更新
|
||||
└── 经验积累
|
||||
|
||||
应用场景:
|
||||
• 自主驾驶
|
||||
• 服务机器人
|
||||
• 仓储物流
|
||||
• 灾难救援
|
||||
• 行星探索
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计原理
|
||||
|
||||
### 空间大模型的应用架构
|
||||
|
||||
```python
|
||||
"""
|
||||
空间大模型应用架构设计
|
||||
"""
|
||||
from typing import Dict, List, Any, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
@dataclass
|
||||
class SpatialContext:
|
||||
"""空间上下文"""
|
||||
extent: Dict[str, float] # 范围 {xmin, ymin, xmax, ymax}
|
||||
crs: str # 坐标系
|
||||
resolution: float # 分辨率
|
||||
scale: str # 尺度等级
|
||||
temporal: Optional[str] # 时间维度
|
||||
|
||||
@dataclass
|
||||
class SpatialQuery:
|
||||
"""空间查询"""
|
||||
natural_language: str # 自然语言描述
|
||||
spatial_context: SpatialContext
|
||||
required_output: str # 输出格式要求
|
||||
constraints: List[str] # 约束条件
|
||||
|
||||
class SpatialCapability(ABC):
|
||||
"""空间能力抽象"""
|
||||
|
||||
@abstractmethod
|
||||
def can_handle(self, query: SpatialQuery) -> float:
|
||||
"""判断是否能处理此查询,返回置信度"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, query: SpatialQuery) -> Any:
|
||||
"""执行查询"""
|
||||
pass
|
||||
|
||||
class SpatialLLM:
|
||||
"""空间大模型接口"""
|
||||
|
||||
def __init__(self):
|
||||
self.capabilities: List[SpatialCapability] = []
|
||||
self.memory = {} # 空间记忆
|
||||
|
||||
def add_capability(self, capability: SpatialCapability):
|
||||
"""添加空间能力"""
|
||||
self.capabilities.append(capability)
|
||||
|
||||
def query(self, query: Union[str, SpatialQuery]) -> Any:
|
||||
"""
|
||||
处理空间查询
|
||||
|
||||
支持自然语言输入,自动解析为结构化查询
|
||||
"""
|
||||
# 如果是字符串,转换为SpatialQuery
|
||||
if isinstance(query, str):
|
||||
query = self._parse_natural_language(query)
|
||||
|
||||
# 找到最合适的能力
|
||||
capability = self._select_capability(query)
|
||||
|
||||
# 执行
|
||||
result = capability.execute(query)
|
||||
|
||||
# 更新记忆
|
||||
self._update_memory(query, result)
|
||||
|
||||
return result
|
||||
|
||||
def _parse_natural_language(self, text: str) -> SpatialQuery:
|
||||
"""将自然语言解析为空间查询"""
|
||||
# 这里会调用LLM进行解析
|
||||
# 返回结构化的SpatialQuery
|
||||
return SpatialQuery(
|
||||
natural_language=text,
|
||||
spatial_context=SpatialContext(
|
||||
extent={}, crs='EPSG:4326', resolution=30, scale='medium'
|
||||
),
|
||||
required_output='map',
|
||||
constraints=[]
|
||||
)
|
||||
|
||||
def _select_capability(self, query: SpatialQuery) -> SpatialCapability:
|
||||
"""选择最合适的能力"""
|
||||
best_capability = None
|
||||
best_score = 0
|
||||
|
||||
for cap in self.capabilities:
|
||||
score = cap.can_handle(query)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_capability = cap
|
||||
|
||||
return best_capability or self.capabilities[0]
|
||||
|
||||
def _update_memory(self, query: SpatialQuery, result: Any):
|
||||
"""更新空间记忆"""
|
||||
# 存储查询-结果对,用于上下文学习
|
||||
pass
|
||||
|
||||
# === 具体的空间能力 ===
|
||||
|
||||
class SpatialAnalysisCapability(SpatialCapability):
|
||||
"""空间分析能力"""
|
||||
|
||||
def __init__(self, gis_backend):
|
||||
self.gis = gis_backend
|
||||
|
||||
def can_handle(self, query: SpatialQuery) -> float:
|
||||
"""判断是否能处理"""
|
||||
# 检查关键词
|
||||
analysis_keywords = [
|
||||
'buffer', 'intersect', 'nearby', 'within',
|
||||
'缓冲', '相交', '附近', '内部'
|
||||
]
|
||||
text = query.natural_language.lower()
|
||||
|
||||
matches = sum(1 for kw in analysis_keywords if kw in text)
|
||||
return min(matches * 0.3, 1.0)
|
||||
|
||||
def execute(self, query: SpatialQuery) -> Any:
|
||||
"""执行空间分析"""
|
||||
# 解析分析类型
|
||||
analysis_type = self._detect_analysis_type(query.natural_language)
|
||||
|
||||
# 执行
|
||||
if analysis_type == 'buffer':
|
||||
return self._buffer_analysis(query)
|
||||
elif analysis_type == 'proximity':
|
||||
return self._proximity_analysis(query)
|
||||
else:
|
||||
return {"error": "无法识别的分析类型"}
|
||||
|
||||
def _detect_analysis_type(self, text: str) -> str:
|
||||
"""检测分析类型"""
|
||||
if any(kw in text.lower() for kw in ['buffer', '缓冲']):
|
||||
return 'buffer'
|
||||
if any(kw in text.lower() for kw in ['near', 'closest', 'nearest', '附近']):
|
||||
return 'proximity'
|
||||
return 'unknown'
|
||||
|
||||
def _buffer_analysis(self, query: SpatialQuery):
|
||||
"""缓冲区分析"""
|
||||
# 实际实现会调用GIS后端
|
||||
return {
|
||||
'type': 'buffer',
|
||||
'result': 'buffer_result'
|
||||
}
|
||||
|
||||
def _proximity_analysis(self, query: SpatialQuery):
|
||||
"""邻近度分析"""
|
||||
return {
|
||||
'type': 'proximity',
|
||||
'result': 'proximity_result'
|
||||
}
|
||||
|
||||
class SpatialVisualizationCapability(SpatialCapability):
|
||||
"""空间可视化能力"""
|
||||
|
||||
def can_handle(self, query: SpatialQuery) -> float:
|
||||
"""判断是否能处理"""
|
||||
vis_keywords = ['map', 'visualize', 'show', 'plot', 'display',
|
||||
'地图', '显示', '可视化', '绘制']
|
||||
text = query.natural_language.lower()
|
||||
|
||||
matches = sum(1 for kw in vis_keywords if kw in text)
|
||||
return min(matches * 0.25, 1.0)
|
||||
|
||||
def execute(self, query: SpatialQuery) -> Any:
|
||||
"""执行可视化"""
|
||||
# 生成地图
|
||||
return {
|
||||
'type': 'map',
|
||||
'url': 'map_url'
|
||||
}
|
||||
|
||||
class SpatialReasoningCapability(SpatialCapability):
|
||||
"""空间推理能力"""
|
||||
|
||||
def can_handle(self, query: SpatialQuery) -> float:
|
||||
"""判断是否能处理"""
|
||||
reason_keywords = ['why', 'how', 'best', 'optimal',
|
||||
'为什么', '如何', '最好', '最优']
|
||||
text = query.natural_language.lower()
|
||||
|
||||
matches = sum(1 for kw in reason_keywords if kw in text)
|
||||
return min(matches * 0.2, 1.0)
|
||||
|
||||
def execute(self, query: SpatialQuery) -> Any:
|
||||
"""执行空间推理"""
|
||||
# 分析空间关系,给出解释和建议
|
||||
return {
|
||||
'type': 'reasoning',
|
||||
'explanation': '基于空间关系的分析',
|
||||
'recommendation': '建议的方案'
|
||||
}
|
||||
|
||||
# === 多模态融合 ===
|
||||
|
||||
class MultimodalSpatialProcessor:
|
||||
"""多模态空间处理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.vision_encoder = None # 视觉编码器
|
||||
self.text_encoder = None # 文本编码器
|
||||
self.structure_encoder = None # 结构编码器
|
||||
self.fusion_layer = None # 融合层
|
||||
|
||||
def process(self,
|
||||
image=None,
|
||||
text=None,
|
||||
vector_data=None) -> Dict:
|
||||
"""
|
||||
处理多模态输入
|
||||
|
||||
融合图像、文本和矢量数据
|
||||
"""
|
||||
embeddings = {}
|
||||
|
||||
# 编码各模态
|
||||
if image is not None:
|
||||
embeddings['vision'] = self._encode_image(image)
|
||||
|
||||
if text is not None:
|
||||
embeddings['text'] = self._encode_text(text)
|
||||
|
||||
if vector_data is not None:
|
||||
embeddings['structure'] = self._encode_structure(vector_data)
|
||||
|
||||
# 融合
|
||||
if len(embeddings) > 1:
|
||||
fused = self._fuse_embeddings(embeddings)
|
||||
else:
|
||||
fused = list(embeddings.values())[0]
|
||||
|
||||
return {
|
||||
'embeddings': embeddings,
|
||||
'fused': fused
|
||||
}
|
||||
|
||||
def _encode_image(self, image):
|
||||
"""编码图像"""
|
||||
# 使用视觉编码器(如ViT)
|
||||
return f"image_embedding_{hash(image)}"
|
||||
|
||||
def _encode_text(self, text):
|
||||
"""编码文本"""
|
||||
# 使用文本编码器(如BERT)
|
||||
return f"text_embedding_{hash(text)}"
|
||||
|
||||
def _encode_structure(self, vector_data):
|
||||
"""编码矢量结构"""
|
||||
# 使用图神经网络
|
||||
return f"structure_embedding_{hash(str(vector_data))}"
|
||||
|
||||
def _fuse_embeddings(self, embeddings: Dict) -> str:
|
||||
"""融合嵌入"""
|
||||
# 使用注意力机制融合
|
||||
return "fused_embedding"
|
||||
|
||||
# === 使用示例 ===
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 空间大模型应用架构 ===\n")
|
||||
|
||||
# 创建空间大模型
|
||||
spatial_llm = SpatialLLM()
|
||||
|
||||
# 添加能力
|
||||
spatial_llm.add_capability(SpatialAnalysisCapability("gis_backend"))
|
||||
spatial_llm.add_capability(SpatialVisualizationCapability())
|
||||
spatial_llm.add_capability(SpatialReasoningCapability())
|
||||
|
||||
# 示例查询
|
||||
queries = [
|
||||
"找出距离公园500米内的所有建筑",
|
||||
"可视化城市的热岛效应分布",
|
||||
"为什么这个区域的生态连通性较差?"
|
||||
]
|
||||
|
||||
print("处理查询:")
|
||||
for query in queries:
|
||||
print(f"\n查询: {query}")
|
||||
result = spatial_llm.query(query)
|
||||
print(f"结果类型: {result.get('type', 'unknown')}")
|
||||
|
||||
print("\n=== 多模态处理 ===")
|
||||
processor = MultimodalSpatialProcessor()
|
||||
result = processor.process(
|
||||
image="satellite_image.tif",
|
||||
text="这是一个城市公园",
|
||||
vector_data={"type": "Polygon", "coordinates": [...]}
|
||||
)
|
||||
print(f"融合结果: {result['fused']}")
|
||||
```
|
||||
|
||||
### 具身智能的空间架构
|
||||
|
||||
```python
|
||||
"""
|
||||
具身智能的空间架构
|
||||
"""
|
||||
from typing import List, Tuple, Optional
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
@dataclass
|
||||
class Pose:
|
||||
"""位姿:位置和朝向"""
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
yaw: float # 偏航角
|
||||
pitch: float # 俯仰角
|
||||
roll: float # 翻滚角
|
||||
|
||||
@dataclass
|
||||
class Observation:
|
||||
"""观测"""
|
||||
pose: Pose
|
||||
visual_data: np.ndarray # 图像数据
|
||||
depth_data: Optional[np.ndarray] # 深度数据
|
||||
point_cloud: Optional[np.ndarray] # 点云
|
||||
|
||||
@dataclass
|
||||
class SpatialMemory:
|
||||
"""空间记忆"""
|
||||
explored_area: List[Tuple[float, float]] # 已探索区域
|
||||
obstacles: List[Dict] # 障碍物位置
|
||||
semantic_labels: Dict # 语义标签
|
||||
confidence_map: np.ndarray # 置信度地图
|
||||
|
||||
class EmbodiedSpatialAgent:
|
||||
"""具身空间智能体"""
|
||||
|
||||
def __init__(self):
|
||||
self.pose = Pose(0, 0, 0, 0, 0, 0)
|
||||
self.memory = SpatialMemory([], [], {}, np.zeros((100, 100)))
|
||||
self.goal = None
|
||||
|
||||
def perceive(self, observation: Observation):
|
||||
"""
|
||||
感知环境
|
||||
|
||||
从多模态传感器数据中提取空间信息
|
||||
"""
|
||||
# 1. 本地化:更新自身位置
|
||||
self._localize(observation)
|
||||
|
||||
# 2. 建图:更新环境地图
|
||||
self._update_map(observation)
|
||||
|
||||
# 3. 识别:识别物体和场景
|
||||
self._identify_objects(observation)
|
||||
|
||||
def plan(self, goal: Tuple[float, float]) -> List[Tuple[float, float]]:
|
||||
"""
|
||||
规划路径
|
||||
|
||||
从当前位置到目标位置
|
||||
"""
|
||||
# 使用A*或其他路径规划算法
|
||||
# 考虑:
|
||||
# - 已知的障碍物
|
||||
# - 地图的置信度
|
||||
# - 机器人的运动约束
|
||||
|
||||
path = self._astar_search(
|
||||
start=(self.pose.x, self.pose.y),
|
||||
goal=goal,
|
||||
obstacles=self.memory.obstacles
|
||||
)
|
||||
|
||||
return path
|
||||
|
||||
def act(self, action: str) -> bool:
|
||||
"""
|
||||
执行动作
|
||||
|
||||
与物理世界交互
|
||||
"""
|
||||
if action == "move_forward":
|
||||
return self._move_forward()
|
||||
elif action == "turn_left":
|
||||
return self._turn_left()
|
||||
elif action == "pick":
|
||||
return self._pick_object()
|
||||
else:
|
||||
return False
|
||||
|
||||
def _localize(self, observation: Observation):
|
||||
"""本地化:确定自身位置"""
|
||||
# 使用SLAM (Simultaneous Localization and Mapping)
|
||||
# 比对当前观测与已有地图
|
||||
pass
|
||||
|
||||
def _update_map(self, observation: Observation):
|
||||
"""更新环境地图"""
|
||||
# 整合新的观测数据到地图中
|
||||
# 更新已探索区域
|
||||
# 更新障碍物位置
|
||||
pass
|
||||
|
||||
def _identify_objects(self, observation: Observation):
|
||||
"""识别物体和场景"""
|
||||
# 使用计算机视觉识别物体
|
||||
# 将识别结果与空间位置关联
|
||||
pass
|
||||
|
||||
def _astar_search(self, start, goal, obstacles):
|
||||
"""A*路径搜索"""
|
||||
# 简化实现
|
||||
return [start, goal]
|
||||
|
||||
def _move_forward(self):
|
||||
"""前进"""
|
||||
self.pose.x += 0.1 * np.cos(self.pose.yaw)
|
||||
self.pose.y += 0.1 * np.sin(self.pose.yaw)
|
||||
return True
|
||||
|
||||
def _turn_left(self):
|
||||
"""左转"""
|
||||
self.pose.yaw += 0.1
|
||||
return True
|
||||
|
||||
def _pick_object(self):
|
||||
"""抓取物体"""
|
||||
# 检查前方是否有可抓取物体
|
||||
# 执行抓取动作
|
||||
return True
|
||||
|
||||
# === 应用示例 ===
|
||||
|
||||
class DeliveryRobot(EmbodiedSpatialAgent):
|
||||
"""配送机器人"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.delivery_queue = []
|
||||
self.current_delivery = None
|
||||
|
||||
def add_delivery(self, location: Tuple[float, float], item: str):
|
||||
"""添加配送任务"""
|
||||
self.delivery_queue.append({
|
||||
'location': location,
|
||||
'item': item,
|
||||
'status': 'pending'
|
||||
})
|
||||
|
||||
def process_deliveries(self):
|
||||
"""处理配送队列"""
|
||||
while self.delivery_queue:
|
||||
# 获取下一个任务
|
||||
self.current_delivery = self.delivery_queue.pop(0)
|
||||
|
||||
# 规划路径
|
||||
path = self.plan(self.current_delivery['location'])
|
||||
|
||||
# 执行配送
|
||||
success = self._execute_delivery(path)
|
||||
|
||||
# 更新状态
|
||||
if success:
|
||||
self.current_delivery['status'] = 'completed'
|
||||
else:
|
||||
self.current_delivery['status'] = 'failed'
|
||||
|
||||
def _execute_delivery(self, path):
|
||||
"""执行配送"""
|
||||
# 沿路径移动
|
||||
for waypoint in path:
|
||||
# 导航到路径点
|
||||
# 避障
|
||||
# 更新地图
|
||||
pass
|
||||
|
||||
# 到达目的地,放下物品
|
||||
return True
|
||||
|
||||
# === 空间智能的核心能力 ===
|
||||
|
||||
class SpatialIntelligenceTest:
|
||||
"""空间智能测试"""
|
||||
|
||||
@staticmethod
|
||||
def test_spatial_reasoning(agent):
|
||||
"""测试空间推理能力"""
|
||||
questions = [
|
||||
"从当前位置到目标位置的最短路径是什么?",
|
||||
"这个房间有几个出口?",
|
||||
"物体A在物体B的哪个方向?"
|
||||
]
|
||||
# 评估回答
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def test_spatial_memory(agent):
|
||||
"""测试空间记忆能力"""
|
||||
# 让机器人探索环境
|
||||
# 然后测试它对环境的记忆
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def test_spatial_learning(agent):
|
||||
"""测试空间学习能力"""
|
||||
# 在多次交互中测试学习效果
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== 具身空间智能 ===\n")
|
||||
|
||||
robot = DeliveryRobot()
|
||||
|
||||
# 添加配送任务
|
||||
robot.add_delivery((10, 20), "包裹A")
|
||||
robot.add_delivery((30, 40), "包裹B")
|
||||
|
||||
print(f"配送队列: {len(robot.delivery_queue)} 个任务")
|
||||
print("机器人能力:")
|
||||
print(" - 空间感知:理解自身位置和环境")
|
||||
print(" - 空间推理:规划最优路径")
|
||||
print(" - 空间记忆:记住已探索区域")
|
||||
print(" - 空间行动:在物理世界中移动和交互")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 案例分析
|
||||
|
||||
### 案例1:城市规划的AI助手
|
||||
|
||||
**场景**:空间大模型辅助城市规划决策
|
||||
|
||||
```python
|
||||
"""
|
||||
城市规划AI助手示例
|
||||
"""
|
||||
|
||||
class UrbanPlanningAssistant:
|
||||
"""城市规划AI助手"""
|
||||
|
||||
def __init__(self, spatial_llm):
|
||||
self.llm = spatial_llm
|
||||
self.project_context = {}
|
||||
|
||||
def analyze_site_suitability(self,
|
||||
site: Dict,
|
||||
project_type: str,
|
||||
constraints: List[str]) -> Dict:
|
||||
"""
|
||||
分析场地适宜性
|
||||
|
||||
综合考虑:
|
||||
- 空间位置和可达性
|
||||
- 周边环境
|
||||
- 政策约束
|
||||
- 社会经济因素
|
||||
"""
|
||||
# 1. 理解项目类型
|
||||
project_requirements = self._understand_project_type(project_type)
|
||||
|
||||
# 2. 收集场地信息
|
||||
site_info = self._collect_site_information(site)
|
||||
|
||||
# 3. 多模态分析
|
||||
analysis = {
|
||||
'visual': self._analyze_visual_context(site),
|
||||
'spatial': self._analyze_spatial_context(site),
|
||||
'regulatory': self._analyze_regulatory_context(site),
|
||||
'social': self._analyze_social_context(site)
|
||||
}
|
||||
|
||||
# 4. 综合评估
|
||||
suitability = self._assess_suitability(
|
||||
site_info, project_requirements, analysis, constraints
|
||||
)
|
||||
|
||||
# 5. 生成解释
|
||||
explanation = self._generate_explanation(
|
||||
suitability, analysis, project_requirements
|
||||
)
|
||||
|
||||
return {
|
||||
'suitability_score': suitability['score'],
|
||||
'recommendation': suitability['recommendation'],
|
||||
'explanation': explanation,
|
||||
'analysis_details': analysis,
|
||||
'alternatives': self._suggest_alternatives(site, project_type)
|
||||
}
|
||||
|
||||
def _understand_project_type(self, project_type: str) -> Dict:
|
||||
"""理解项目类型的需求"""
|
||||
# 使用LLM理解项目类型
|
||||
requirements = {
|
||||
'commercial': {
|
||||
'traffic_access': 'high',
|
||||
'visibility': 'high',
|
||||
'parking': 'required',
|
||||
'zoning': 'commercial'
|
||||
},
|
||||
'residential': {
|
||||
'quiet': 'high',
|
||||
'green_space': 'preferred',
|
||||
'schools_access': 'important',
|
||||
'zoning': 'residential'
|
||||
},
|
||||
'industrial': {
|
||||
'highway_access': 'high',
|
||||
'utilities': 'required',
|
||||
'buffer_from_residential': 'required',
|
||||
'zoning': 'industrial'
|
||||
}
|
||||
}
|
||||
return requirements.get(project_type, {})
|
||||
|
||||
def _collect_site_information(self, site: Dict) -> Dict:
|
||||
"""收集场地信息"""
|
||||
# 整合多源数据
|
||||
return {
|
||||
'location': site['coordinates'],
|
||||
'area': site.get('area'),
|
||||
'current_use': self._detect_current_use(site),
|
||||
'surroundings': self._analyze_surroundings(site)
|
||||
}
|
||||
|
||||
def _analyze_visual_context(self, site: Dict) -> Dict:
|
||||
"""分析视觉上下文(卫星图像、街景)"""
|
||||
# 使用视觉模型分析
|
||||
return {
|
||||
'land_use': 'mixed',
|
||||
'building_density': 'medium',
|
||||
'green_coverage': 0.25,
|
||||
'visual_quality': 'good'
|
||||
}
|
||||
|
||||
def _analyze_spatial_context(self, site: Dict) -> Dict:
|
||||
"""分析空间上下文(可达性、邻近性)"""
|
||||
# 使用GIS分析
|
||||
return {
|
||||
'accessibility_score': 0.75,
|
||||
'nearby_amenities': ['park', 'school', 'shopping'],
|
||||
'transit_access': 'good',
|
||||
'road_connectivity': 'high'
|
||||
}
|
||||
|
||||
def _analyze_regulatory_context(self, site: Dict) -> Dict:
|
||||
"""分析法规上下文(分区、规划政策)"""
|
||||
return {
|
||||
'zoning': 'mixed_use',
|
||||
'height_limit': '30m',
|
||||
'far_limit': 2.5,
|
||||
'policy_constraints': ['heritage_buffer', 'flood_zone']
|
||||
}
|
||||
|
||||
def _analyze_social_context(self, site: Dict) -> Dict:
|
||||
"""分析社会上下文(社区需求、公众意见)"""
|
||||
return {
|
||||
'community_concerns': ['traffic', 'noise'],
|
||||
'support_level': 'moderate',
|
||||
'demographics': {'age_distribution': 'mixed'}
|
||||
}
|
||||
|
||||
def _assess_suitability(self, site_info, requirements, analysis, constraints):
|
||||
"""综合评估适宜性"""
|
||||
score = 0.7 # 示例分数
|
||||
recommendation = "suitable_with_conditions"
|
||||
|
||||
# 检查硬约束
|
||||
for constraint in constraints:
|
||||
if not self._check_constraint(constraint, analysis):
|
||||
score = min(score, 0.3)
|
||||
recommendation = "not_recommended"
|
||||
|
||||
return {
|
||||
'score': score,
|
||||
'recommendation': recommendation
|
||||
}
|
||||
|
||||
def _generate_explanation(self, suitability, analysis, requirements):
|
||||
"""生成解释"""
|
||||
# 使用LLM生成自然语言解释
|
||||
return """
|
||||
该场地总体适宜性评分为0.70,建议有条件使用。
|
||||
|
||||
优势:
|
||||
- 交通可达性良好
|
||||
- 周边配套设施完善
|
||||
- 符合分区要求
|
||||
|
||||
需要关注:
|
||||
- 社区对交通增加的担忧
|
||||
- 需要缓解潜在的噪音影响
|
||||
"""
|
||||
```
|
||||
|
||||
### 案例2:灾害响应的空间AI
|
||||
|
||||
```python
|
||||
"""
|
||||
灾害响应空间AI系统
|
||||
"""
|
||||
|
||||
class DisasterResponseSystem:
|
||||
"""灾害响应空间AI系统"""
|
||||
|
||||
def __init__(self):
|
||||
self.situation_awareness = {}
|
||||
self.resource_tracker = {}
|
||||
self.action_planner = None
|
||||
|
||||
def assess_disaster_impact(self,
|
||||
disaster_type: str,
|
||||
location: Dict,
|
||||
affected_area: Dict) -> Dict:
|
||||
"""
|
||||
评估灾害影响
|
||||
|
||||
整合多源数据:
|
||||
- 遥感影像(灾前灾后对比)
|
||||
- 社交媒体(实时信息)
|
||||
- 基础设施数据(脆弱性评估)
|
||||
- 人口数据(暴露度评估)
|
||||
"""
|
||||
# 1. 获取多模态数据
|
||||
data = {
|
||||
'satellite': self._get_satellite_imagery(location),
|
||||
'social_media': self._analyze_social_media(location),
|
||||
'infrastructure': self._get_infrastructure_data(location),
|
||||
'population': self._get_population_data(location)
|
||||
}
|
||||
|
||||
# 2. 空间分析
|
||||
impact_assessment = {
|
||||
'severity_map': self._create_severity_map(data),
|
||||
'affected_population': self._estimate_affected_population(data),
|
||||
'damaged_infrastructure': self._identify_damage(data),
|
||||
'accessibility': self._assess_accessibility(location, disaster_type)
|
||||
}
|
||||
|
||||
# 3. 优先级排序
|
||||
priorities = self._prioritize_response(impact_assessment)
|
||||
|
||||
return {
|
||||
'impact': impact_assessment,
|
||||
'priorities': priorities,
|
||||
'recommended_actions': self._generate_action_plan(priorities)
|
||||
}
|
||||
|
||||
def plan_evacuation_routes(self,
|
||||
affected_areas: List[Dict],
|
||||
shelter_locations: List[Dict],
|
||||
road_conditions: Dict) -> List[Dict]:
|
||||
"""
|
||||
规划疏散路线
|
||||
|
||||
考虑:
|
||||
- 受灾区域分布
|
||||
- 避难所容量
|
||||
- 道路状况(损坏、拥堵)
|
||||
- 人口类型(老人、儿童、行动不便者)
|
||||
"""
|
||||
routes = []
|
||||
|
||||
for area in affected_areas:
|
||||
# 找到最近的可用避难所
|
||||
available_shelters = self._find_available_shelters(
|
||||
area, shelter_locations
|
||||
)
|
||||
|
||||
# 计算最优路线
|
||||
for shelter in available_shelters:
|
||||
route = self._calculate_route(
|
||||
start=area['center'],
|
||||
end=shelter['location'],
|
||||
road_conditions=road_conditions,
|
||||
constraints={'avoid_flood': True, 'avoid_damage': True}
|
||||
)
|
||||
|
||||
if route['feasible']:
|
||||
routes.append({
|
||||
'from': area['name'],
|
||||
'to': shelter['name'],
|
||||
'route': route['path'],
|
||||
'estimated_time': route['time'],
|
||||
'capacity': route['capacity'],
|
||||
'risk_level': route['risk']
|
||||
})
|
||||
|
||||
return routes
|
||||
|
||||
def monitor_situation(self, sensor_data: Dict) -> Dict:
|
||||
"""
|
||||
监测灾情发展
|
||||
|
||||
使用IoT传感器、无人机、卫星等实时数据
|
||||
"""
|
||||
# 整合多源实时数据
|
||||
situation = {
|
||||
'flood_extent': self._monitor_flood_extent(sensor_data),
|
||||
'fire_spread': self._monitor_fire_spread(sensor_data),
|
||||
'structural_integrity': self._monitor_structures(sensor_data),
|
||||
'weather_conditions': self._monitor_weather(sensor_data)
|
||||
}
|
||||
|
||||
# 预测发展趋势
|
||||
forecast = self._forecast_development(situation)
|
||||
|
||||
return {
|
||||
'current': situation,
|
||||
'forecast': forecast,
|
||||
'alerts': self._generate_alerts(forecast)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反思与延伸
|
||||
|
||||
### 思考问题
|
||||
|
||||
1. **技术预期**:空间大模型在5年内最可能实现哪些突破?
|
||||
|
||||
2. **影响评估**:这些技术会如何改变你的工作方式?
|
||||
|
||||
3. **伦理考量**:更强大的空间AI带来哪些新的伦理挑战?
|
||||
|
||||
4. **准备策略**:如何为这些变化做准备?
|
||||
|
||||
### 实践练习
|
||||
|
||||
1. **趋势追踪**:选择一个方向(空间大模型/多模态/具身智能),追踪最新进展
|
||||
|
||||
2. **场景设计**:设想一个未来应用场景,描述空间AI如何发挥作用
|
||||
|
||||
3. **技能准备**:列出需要学习的新技能,制定学习计划
|
||||
|
||||
### 延伸阅读
|
||||
|
||||
- **"Spatial Computing"** 相关文献 - 空间计算的未来
|
||||
- **"Embodied AI"** 研究进展 - 具身智能前沿
|
||||
- **Multimodal Learning** 论文 - 多模态学习技术
|
||||
- AI for Science 相关报告 - AI在科学领域的应用
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **空间大模型正在快速发展**,将从工具增强走向原生支持
|
||||
2. **多模态融合是关键方向**,整合视觉、文本、结构数据
|
||||
3. **具身智能需要强大的空间能力**作为基础
|
||||
4. **技术演进带来新机遇**,也需要应对新的挑战
|
||||
5. **保持关注但保持批判**,理性评估技术成熟度和适用性
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
||||
# 第六部分:反思与展望
|
||||
|
||||
## 本部分目标
|
||||
|
||||
培养批判性思维,建立长期视角:
|
||||
- 认识AI的局限与潜在风险
|
||||
- 理解空间决策的伦理维度
|
||||
- 建立技术迭代中保持知识更新的方法
|
||||
- 展望空间AI的未来方向
|
||||
- 构建个人知识体系
|
||||
|
||||
---
|
||||
|
||||
## 章节导航
|
||||
|
||||
| 章节 | 标题 | 核心内容 |
|
||||
|-----|------|---------|
|
||||
| 05.1 | [AI的局限与幻觉](./05.1-ai-limitations.md) | 空间AI可能出错的地方、验证方法、专家的不可替代性 |
|
||||
| 05.2 | [伦理与责任](./05.2-ethics-and-responsibility.md) | 空间决策的伦理维度、可解释性、问责机制 |
|
||||
| 05.3 | [技术迭代与持久知识](./05.3-technical-iteration.md) | 什么在变、什么不变、如何持续学习 |
|
||||
| 05.4 | [空间AI的未来方向](./05.4-future-directions.md) | 空间大模型、多模态、具身智能 |
|
||||
| 05.5 | [个人知识体系](./05.5-personal-knowledge-system.md) | 建立AI工具箱、文档化策略、社区参与 |
|
||||
|
||||
---
|
||||
|
||||
## 核心理念
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 批判性思维框架 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ 问题 → 怀疑 → 验证 → 理解 → 反思 │ │
|
||||
│ │ ↑ ↓ │ │
|
||||
│ │ └────────────────────┘ │ │
|
||||
│ │ 迭代改进 │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ 三大怀疑对象 │ │
|
||||
│ │ │ │
|
||||
│ │ 1. 怀疑工具:工具的局限是什么? │ │
|
||||
│ │ 2. 怀疑结果:结果可靠吗?如何验证? │ │
|
||||
│ │ 3. 怀疑自己:我的理解正确吗?有无偏见? │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────┐ │
|
||||
│ │ 持久学习的原则 │ │
|
||||
│ │ │ │
|
||||
│ │ • 关注原理,而非命令 │ │
|
||||
│ │ • 理解权衡,而非绝对 │ │
|
||||
│ │ • 建立网络,而非孤立 │ │
|
||||
│ │ • 保持好奇,而非自满 │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键问题
|
||||
|
||||
### 关于AI局限
|
||||
|
||||
1. AI在哪些空间任务上可能出错?
|
||||
2. 如何验证AI的空间分析结果?
|
||||
3. 人类专家的哪些能力是AI无法替代的?
|
||||
|
||||
### 关于伦理责任
|
||||
|
||||
1. 空间决策如何影响不同群体?
|
||||
2. AI系统如何做到可解释?
|
||||
3. 出错时责任如何划分?
|
||||
|
||||
### 关于持续学习
|
||||
|
||||
1. 哪些知识会过时?哪些会持久?
|
||||
2. 如何建立知识更新的机制?
|
||||
3. 如何判断新技术值得投入时间学习?
|
||||
|
||||
### 关于未来方向
|
||||
|
||||
1. 空间大模型会带来什么变革?
|
||||
2. 多模态AI如何改变空间分析?
|
||||
3. 具身智能与空间智能的关系是什么?
|
||||
|
||||
### 关于个人成长
|
||||
|
||||
1. 如何建立自己的AI工具箱?
|
||||
2. 什么样的文档策略最有效?
|
||||
3. 如何有效参与技术社区?
|
||||
|
||||
---
|
||||
|
||||
## 阅读建议
|
||||
|
||||
### 思考方式
|
||||
|
||||
阅读本部分时:
|
||||
- **批判性思考**:不要盲目接受,提出质疑
|
||||
- **联系实际**:将观点与你的经验对比
|
||||
- **写下想法**:记录你的思考和问题
|
||||
- **讨论交流**:与他人分享观点
|
||||
|
||||
### 行动导向
|
||||
|
||||
每章结束后:
|
||||
1. 总结3个关键点
|
||||
2. 提出1个可执行的改进建议
|
||||
3. 分享给他人或写成笔记
|
||||
|
||||
---
|
||||
|
||||
## 延伸资源
|
||||
|
||||
### 批判性思维
|
||||
- **"Critical Thinking"** (Moore & Parker) - 批判性思维入门
|
||||
- **"Thinking, Fast and Slow"** (Kahneman) - 人类思维的局限性
|
||||
|
||||
### AI伦理
|
||||
- **"Weapons of Math Destruction"** (Cathy O'Neil) - 算法的社会影响
|
||||
- **"The Alignment Problem"** (Brian Christian) - AI对齐问题
|
||||
|
||||
### 持续学习
|
||||
- **"Make It Stick"** - 学习的科学
|
||||
- **"Ultralearning"** (Scott Young) - 高效自学方法
|
||||
|
||||
---
|
||||
|
||||
## 结语
|
||||
|
||||
完成全书阅读后,希望你能:
|
||||
|
||||
1. **建立认知框架**:理解AI背后的原理,而非仅仅使用工具
|
||||
2. **培养批判思维**:能质疑、验证、改进AI系统
|
||||
3. **保持学习能力**:在技术迭代中持续成长
|
||||
4. **承担社会责任**:在空间决策中考虑伦理影响
|
||||
5. **构建个人体系**:建立可持久的知识和技能体系
|
||||
|
||||
> "技术永远在变,但原理长存。工具可能过时,但思维永恒。"
|
||||
@@ -0,0 +1,198 @@
|
||||
# Claude Code for Spatial Intelligence and Autonomous Design
|
||||
## 空间智能与自主设计:AI原理与实践手册
|
||||
|
||||
---
|
||||
|
||||
**定位**:面向空间研究与设计领域的混合读者,以Claude Code为脚手架,帮助理解AI背后的思想、原理和方法,在技术快速迭代的当下建立持久的认知框架。
|
||||
|
||||
**核心原则**:
|
||||
- 重原理轻操作——关注不变的设计思想,而非易变的命令语法
|
||||
- 理论与实践均衡——50%概念讲解 + 50%实践案例
|
||||
- 面向混合读者——兼顾研究者和设计者的不同需求
|
||||
|
||||
---
|
||||
|
||||
## 如何使用本书
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[开始阅读] --> B{你的背景?}
|
||||
B -->|研究者| C[重点阅读原理部分]
|
||||
B -->|设计者| D[重点关注实践]
|
||||
B -->|初学者| E[从头开始]
|
||||
|
||||
C --> F[02 空间智能]
|
||||
D --> G[03 自主设计]
|
||||
E --> H[00 导论]
|
||||
|
||||
F --> I[04 综合实践]
|
||||
G --> I
|
||||
H --> I
|
||||
|
||||
I --> J[05 反思与展望]
|
||||
```
|
||||
|
||||
**建议阅读路径**:
|
||||
|
||||
| 读者类型 | 推荐路径 | 重点章节 |
|
||||
|---------|---------|---------|
|
||||
| 空间研究者 | 00→01→02→05 | 01.2, 02.2, 02.4 |
|
||||
| 设计实践者 | 00→03→04→05 | 03.1, 03.3, 04全部 |
|
||||
| 技术实现者 | 01→03→04→05 | 01.1, 01.2, 03.2 |
|
||||
| 教学工作者 | 全部 | 根据教学目标选择 |
|
||||
|
||||
---
|
||||
|
||||
## 全书目录
|
||||
|
||||
### [00 导论](./00-introduction/)
|
||||
建立读者对AI在空间领域应用的宏观认知
|
||||
|
||||
- [00.1 为什么要读这本书](./00-introduction/00.1-why-this-book.md)
|
||||
- [00.2 空间智能是什么](./00-introduction/00.2-what-is-spatial-intelligence.md)
|
||||
- [00.3 自主设计的含义](./00-introduction/00.3-what-is-autonomous-design.md)
|
||||
- [00.4 Claude Code作为脚手架](./00-introduction/00.4-claude-code-as-scaffold.md)
|
||||
|
||||
**实践案例00**:[搭建你的第一个空间AI助手](./00-introduction/practice/setup-first-assistant/)
|
||||
|
||||
---
|
||||
|
||||
### [01 基础原理](./01-foundations/)
|
||||
理解现代AI系统的核心设计原理
|
||||
|
||||
- [01.1 智能的模块化视角](./01-foundations/01.1-modular-intelligence.md)
|
||||
- [01.2 状态与状态机](./01-foundations/01.2-state-and-state-machines.md)
|
||||
- [01.3 概率与不确定性](./01-foundations/01.3-probability-and-uncertainty.md)
|
||||
- [01.4 反馈与学习](./01-foundations/01.4-feedback-and-learning.md)
|
||||
- [01.5 人机协同的原理](./01-foundations/01.5-human-ai-collaboration.md)
|
||||
|
||||
**实践案例01**:[用LangGraph构建空间决策工作流](./01-foundations/practice/langgraph-workflow/)
|
||||
|
||||
---
|
||||
|
||||
### [02 空间智能](./02-spatial-intelligence/)
|
||||
理解AI如何"理解"和操作空间
|
||||
|
||||
- [02.1 空间表征](./02-spatial-intelligence/02.1-spatial-representation.md)
|
||||
- [02.2 空间推理](./02-spatial-intelligence/02.2-spatial-reasoning.md)
|
||||
- [02.3 多准则决策](./02-spatial-intelligence/02.3-multi-criteria-decision.md)
|
||||
- [02.4 空间优化](./02-spatial-intelligence/02.4-spatial-optimization.md)
|
||||
- [02.5 不确定性量化](./02-spatial-intelligence/02.5-uncertainty-quantification.md)
|
||||
|
||||
**实践案例02**:构建生态系统服务评估Skill
|
||||
**实践案例03**:最小累积阻力(MCR)分析的自动化
|
||||
|
||||
---
|
||||
|
||||
### [03 自主设计](./03-autonomous-design/)
|
||||
理解自主系统的设计模式和工作流编排
|
||||
|
||||
- [03.1 工作流编排原理](./03-autonomous-design/03.1-workflow-orchestration.md)
|
||||
- [03.2 Agent设计模式](./03-autonomous-design/03.2-agent-design-patterns.md)
|
||||
- [03.3 技能组合与复用](./03-autonomous-design/03.3-skill-composition.md)
|
||||
- [03.4 记忆与上下文](./03-autonomous-design/03.4-memory-and-context.md)
|
||||
- [03.5 规划与执行](./03-autonomous-design/03.5-planning-and-execution.md)
|
||||
|
||||
**实践案例04**:设计一个自主空间分析Agent
|
||||
**实践案例05**:实现Human-in-the-Loop审查机制
|
||||
|
||||
---
|
||||
|
||||
### [04 综合实践](./04-practice/)
|
||||
通过完整项目将所学整合
|
||||
|
||||
- [项目A:生态源地自动识别与优先级排序](./04-practice/projects/project-a-source-identification/)
|
||||
- [项目B:生态阻力面的多准则构建](./04-practice/projects/project-b-resistance-surface/)
|
||||
- [项目C:生态网络的自主分析与优化](./04-practice/projects/project-c-network-optimization/)
|
||||
- [项目D:带HITL的完整工作流设计](./04-practice/projects/project-d-hitl-workflow/)
|
||||
|
||||
---
|
||||
|
||||
### [05 反思与展望](./05-reflection/)
|
||||
培养批判性思维,建立长期视角
|
||||
|
||||
- [05.1 AI的局限与幻觉](./05-reflection/05.1-ai-limitations.md)
|
||||
- [05.2 伦理与责任](./05-reflection/05.2-ethics-and-responsibility.md)
|
||||
- [05.3 技术迭代与持久知识](./05-reflection/05.3-technical-iteration.md)
|
||||
- [05.4 空间AI的未来方向](./05-reflection/05.4-future-directions.md)
|
||||
- [05.5 个人知识体系](./05-reflection/05.5-personal-knowledge-system.md)
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### [参考资料](./references/)
|
||||
- 空间分析基础理论
|
||||
- 人工智能核心教材
|
||||
- Claude Code官方文档
|
||||
- 相关论文和资源
|
||||
|
||||
### [可运行代码示例](./examples/)
|
||||
- Python基础示例
|
||||
- Claude Code技能示例
|
||||
- LangGraph工作流示例
|
||||
- 完整项目模板
|
||||
|
||||
### [附录1: 学术论文写作工作流](./appendix/appendix1-academic-writing-workflow.md)
|
||||
**VSCode + Claude Code + Obsidian 打造完整写作流程**
|
||||
|
||||
涵盖内容:
|
||||
- 文献管理与知识积累(Zotero + Obsidian)
|
||||
- 大纲构思与结构设计
|
||||
- 内容撰写与AI辅助(Claude Code)
|
||||
- 代码与图表制作(Python + Pandoc)
|
||||
- 修改润色与投稿准备
|
||||
|
||||
适用场景:
|
||||
- 学位论文写作
|
||||
- 期刊论文投稿
|
||||
- 研究报告撰写
|
||||
|
||||
---
|
||||
|
||||
## 核心概念索引
|
||||
|
||||
| 概念 | 相关章节 | 关键词 |
|
||||
|-----|---------|--------|
|
||||
| Agent | 00.3, 01.2, 03.2 | 智能体、自主决策、设计模式 |
|
||||
| Skill | 00.4, 01.1, 03.3 | 技能、能力封装、组合复用 |
|
||||
| Hook | 00.4, 03.1 | 钩子、事件驱动、扩展点 |
|
||||
| HITL | 00.3, 01.5, 03.5 | 人机协同、审查点、信任校准 |
|
||||
| 状态机 | 01.2, 03.1 | 状态、转换、工作流编排 |
|
||||
| 不确定性 | 01.3, 02.5 | 概率、置信度、敏感性分析 |
|
||||
| 空间推理 | 02.2 | 拓扑、距离、图算法 |
|
||||
| 多准则决策 | 02.3 | 权重、标准化、AHP |
|
||||
|
||||
---
|
||||
|
||||
## 贡献指南
|
||||
|
||||
欢迎贡献内容!
|
||||
|
||||
1. **报告问题**:在Issues中指出错误或改进建议
|
||||
2. **提交内容**:Fork后创建分支,提交PR
|
||||
3. **讨论案例**:分享你的实践经验和案例
|
||||
|
||||
详见 [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
CC BY-NC-SA 4.0 - 允许非商业使用和修改,需署名并以相同方式共享
|
||||
|
||||
---
|
||||
|
||||
## 致谢
|
||||
|
||||
本书内容基于:
|
||||
- ENAgent项目(生态网络分析智能体)的实践经验
|
||||
- Claude Code官方文档和社区讨论
|
||||
- 空间分析和人工智能领域的经典文献
|
||||
- 开源社区的集体智慧
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
- **v0.1.0** (2025-01) - 初始版本,框架搭建完成
|
||||
@@ -0,0 +1,237 @@
|
||||
# Claude Code for Spatial Intelligence and Autonomous Design
|
||||
## 空间智能与自主设计:AI原理与实践手册
|
||||
|
||||
**目标定位**:面向空间研究与设计领域的混合读者,以Claude Code为脚手架,帮助理解AI背后的思想、原理和方法,在技术快速迭代的当下建立持久的认知框架。
|
||||
|
||||
**核心原则**:
|
||||
- 重原理轻操作——关注不变的设计思想,而非易变的命令语法
|
||||
- 理论与实践均衡——50%概念讲解 + 50%实践案例
|
||||
- 面向混合读者——兼顾研究者和设计者的不同需求
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
CC4SI/
|
||||
├── README.md # 丛书说明
|
||||
├── BOOK.md # 主入口(整合目录)
|
||||
├── 00-introduction/ # 第一部分:导论
|
||||
├── 01-foundations/ # 第二部分:基础原理
|
||||
├── 02-spatial-intelligence/ # 第三部分:空间智能
|
||||
├── 03-autonomous-design/ # 第四部分:自主设计
|
||||
├── 04-practice/ # 第五部分:综合实践
|
||||
├── 05-reflection/ # 第六部分:反思与展望
|
||||
├── references/ # 参考资料
|
||||
└── examples/ # 可运行代码示例
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 内容概览
|
||||
|
||||
### 第一部分:导论 (00-introduction)
|
||||
|
||||
建立读者对AI在空间领域应用的宏观认知
|
||||
|
||||
| 章节 | 标题 | 核心内容 |
|
||||
|-----|------|---------|
|
||||
| 00.1 | 为什么要读这本书 | AI的"黑箱"问题;空间智能的特殊性;原理学习的持久价值 |
|
||||
| 00.2 | 空间智能是什么 | 空间认知的层次;从GIS到空间智能;空间推理的本质 |
|
||||
| 00.3 | 自主设计的含义 | Human-in-the-Loop;自动与自主的区别;设计智能的演进 |
|
||||
| 00.4 | Claude Code作为脚手架 | 为什么选择Claude Code;Agent、Skill、Hook的概念;工具中性原则 |
|
||||
|
||||
**实践案例00**:搭建你的第一个空间AI助手
|
||||
|
||||
---
|
||||
|
||||
### 第二部分:基础原理 (01-foundations)
|
||||
|
||||
理解现代AI系统的核心设计原理,超越具体工具
|
||||
|
||||
| 章节 | 标题 | 核心内容 |
|
||||
|-----|------|---------|
|
||||
| 01.1 | 智能的模块化视角 | 为什么需要模块化;函数式组合思想;技能即能力封装 |
|
||||
| 01.2 | 状态与状态机 | 状态是什么;为什么状态管理是核心;LangGraph的设计哲学 |
|
||||
| 01.3 | 概率与不确定性 | 空间分析中的不确定性;AI如何处理未知;置信度的概念 |
|
||||
| 01.4 | 反馈与学习 | 强化学习直觉;奖励函数设计;探索与利用的权衡 |
|
||||
| 01.5 | 人机协同的原理 | HITL的理论基础;何时需要人类介入;信任校准 |
|
||||
|
||||
**实践案例01**:用LangGraph构建一个简单的空间决策工作流
|
||||
|
||||
---
|
||||
|
||||
### 第三部分:空间智能 (02-spatial-intelligence)
|
||||
|
||||
理解AI如何"理解"和操作空间
|
||||
|
||||
| 章节 | 标题 | 核心内容 |
|
||||
|-----|------|---------|
|
||||
| 02.1 | 空间表征 | 栅格 vs 矢量;图表示;多尺度表征;空间索引原理 |
|
||||
| 02.2 | 空间推理 | 邻近性分析;连通性;空间关系推理;图算法在空间中的应用 |
|
||||
| 02.3 | 多准则决策 | 权重的本质;标准化方法;敏感性分析;专家知识编码 |
|
||||
| 02.4 | 空间优化 | 什么可优化;目标函数设计;约束处理;启发式搜索 |
|
||||
| 02.5 | 不确定性量化 | 空间不确定性来源;传播分析;可视化;决策稳健性 |
|
||||
|
||||
**实践案例02**:构建生态系统服务评估Skill
|
||||
**实践案例03**:最小累积阻力(MCR)分析的自动化
|
||||
|
||||
---
|
||||
|
||||
### 第四部分:自主设计 (03-autonomous-design)
|
||||
|
||||
理解自主系统的设计模式和工作流编排
|
||||
|
||||
| 章节 | 标题 | 核心内容 |
|
||||
|-----|------|---------|
|
||||
| 03.1 | 工作流编排原理 | DAG(有向无环图);节点与边;条件分支;错误处理模式 |
|
||||
| 03.2 | Agent设计模式 | Reflex Agent;Model-based Agent;Goal-based;Utility-based |
|
||||
| 03.3 | 技能组合与复用 | 技能抽象;接口设计;组合模式;技能发现 |
|
||||
| 03.4 | 记忆与上下文 | 短期/长期记忆;状态持久化;检索机制;知识库构建 |
|
||||
| 03.5 | 规划与执行 | 前向/后向搜索;分层规划;执行监控;重规划 |
|
||||
|
||||
**实践案例04**:设计一个自主空间分析Agent
|
||||
**实践案例05**:实现Human-in-the-Loop审查机制
|
||||
|
||||
---
|
||||
|
||||
### 第五部分:综合实践 (04-practice)
|
||||
|
||||
通过完整项目将所学整合
|
||||
|
||||
| 项目 | 描述 | 涉及章节 |
|
||||
|-----|------|---------|
|
||||
| 项目A | 生态源地自动识别与优先级排序 | 02.1, 02.3, 03.1 |
|
||||
| 项目B | 生态阻力面的多准则构建 | 02.3, 02.5, 03.3 |
|
||||
| 项目C | 生态网络的自主分析与优化 | 02.2, 02.4, 03.4 |
|
||||
| 项目D | 带HITL的完整工作流设计 | 全部章节 |
|
||||
|
||||
---
|
||||
|
||||
### 第六部分:反思与展望 (05-reflection)
|
||||
|
||||
培养批判性思维,建立长期视角
|
||||
|
||||
| 章节 | 标题 | 核心内容 |
|
||||
| ---- | --------- | --------------------------- |
|
||||
| 05.1 | AI的局限与幻觉 | 空间AI可能出错的地方;如何验证;人类专家的不可替代性 |
|
||||
| 05.2 | 伦理与责任 | 空间决策的伦理维度;可解释性;问责机制 |
|
||||
| 05.3 | 技术迭代与持久知识 | 什么在变;什么不变;如何持续学习 |
|
||||
| 05.4 | 空间AI的未来方向 | 空间大模型;多模态;具身智能 |
|
||||
| 05.5 | 个人知识体系 | 建立自己的AI工具箱;文档化策略;社区参与 |
|
||||
|
||||
---
|
||||
|
||||
## 如何使用本书
|
||||
|
||||
### 面向研究者
|
||||
如果你是空间研究领域的学者或学生,建议:
|
||||
1. 先完成00-introduction,建立宏观认知
|
||||
2. 重点阅读01-foundations,理解AI设计原理
|
||||
3. 根据研究需要选择性深入02和03部分
|
||||
4. 通过04-practice的项目验证理解
|
||||
|
||||
### 面向设计者
|
||||
如果你是设计实践者或技术实现者,建议:
|
||||
1. 快速浏览00-introduction
|
||||
2. 重点关注03-autonomous-design的工作流编排
|
||||
3. 边读边做,每个实践案例都亲手运行
|
||||
4. 使用examples目录中的代码作为起点
|
||||
|
||||
### 面向教学者
|
||||
如果你使用本书作为教学材料:
|
||||
1. 每章节预留2-3课时(理论+实践)
|
||||
2. 使用"核心问题"引发讨论
|
||||
3. 鼓励学生完成"反思与延伸"中的问题
|
||||
4. 项目A-D可作为期末综合项目
|
||||
|
||||
---
|
||||
|
||||
## 环境准备
|
||||
|
||||
### 基础要求
|
||||
- Python 3.10+
|
||||
- Claude Code CLI
|
||||
- QGIS 3.x(可选,用于空间分析)
|
||||
|
||||
### 安装Claude Code
|
||||
```bash
|
||||
npm install -g @anthropic/claude-code
|
||||
```
|
||||
|
||||
### Python依赖
|
||||
```bash
|
||||
pip install langgraph langchain-anthropic geopandas rasterio networkx matplotlib
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 写作规范
|
||||
|
||||
### 内容原则
|
||||
1. **原理优先**:先讲"为什么",再讲"怎么做"
|
||||
2. **中英双语**:专业术语保留英文,解释用中文
|
||||
3. **代码示例**:完整可运行,有注释,说明设计意图
|
||||
4. **图示辅助**:架构图、流程图、概念图优先于纯文字
|
||||
5. **案例真实**:基于ENAgent等实际项目,避免玩具示例
|
||||
|
||||
### 文件命名规范
|
||||
```
|
||||
XX-category/
|
||||
├── README.md # 章节导读
|
||||
├── XX.1-chapter-name.md
|
||||
├── XX.2-chapter-name.md
|
||||
└── practice/ # 本章节的实践
|
||||
└── practice-name/
|
||||
```
|
||||
|
||||
### Markdown模板
|
||||
每章节遵循以下结构:
|
||||
```markdown
|
||||
# 章节标题
|
||||
|
||||
## 核心问题
|
||||
(用1-2个问题引导读者思考)
|
||||
|
||||
## 概念讲解
|
||||
(原理阐述,配合图示)
|
||||
|
||||
## 设计原理
|
||||
(为什么这样设计,权衡是什么)
|
||||
|
||||
## 代码示例
|
||||
```python
|
||||
# 可运行代码,有详细注释
|
||||
```
|
||||
|
||||
## 案例分析
|
||||
(真实项目的相关代码解析)
|
||||
|
||||
## 反思与延伸
|
||||
(启发思考的问题)
|
||||
|
||||
## 参考资料
|
||||
(相关阅读)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 贡献指南
|
||||
|
||||
本书是开源项目,欢迎贡献:
|
||||
|
||||
1. **报告问题**:在Issues中指出错误或改进建议
|
||||
2. **提交内容**:Fork后创建分支,提交PR
|
||||
3. **讨论案例**:分享你的实践经验和案例
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
CC BY-NC-SA 4.0 - 允许非商业使用和修改,需署名并以相同方式共享
|
||||
|
||||
---
|
||||
|
||||
## 版本历史
|
||||
|
||||
- v0.1.0 (2025-01) - 初始版本,框架搭建完成
|
||||
@@ -0,0 +1,327 @@
|
||||
# 贡献指南
|
||||
|
||||
感谢你有兴趣为《Claude Code for Spatial Intelligence and Autonomous Design》做出贡献!
|
||||
|
||||
---
|
||||
|
||||
## 如何贡献
|
||||
|
||||
### 报告问题
|
||||
|
||||
如果你发现了书中的错误或有改进建议:
|
||||
|
||||
1. 在GitHub Issues中搜索是否已有相关问题
|
||||
2. 如果没有,创建新Issue并包含:
|
||||
- 清晰的标题
|
||||
- 错误位置(章节、文件)
|
||||
- 问题描述
|
||||
- 建议的改进方式
|
||||
- 相关标签(bug, enhancement, content等)
|
||||
|
||||
### 提交内容
|
||||
|
||||
#### 内容贡献类型
|
||||
|
||||
1. **修正错误**
|
||||
- 事实错误
|
||||
- 代码错误
|
||||
- 排版问题
|
||||
|
||||
2. **新增内容**
|
||||
- 新的示例代码
|
||||
- 新的案例分析
|
||||
- 新的实践项目
|
||||
|
||||
3. **改进现有内容**
|
||||
- 更好的解释
|
||||
- 更清晰的代码
|
||||
- 更好的组织
|
||||
|
||||
4. **翻译**
|
||||
- 英文内容的中译
|
||||
- 中文内容的英译
|
||||
|
||||
#### 提交流程
|
||||
|
||||
```bash
|
||||
# 1. Fork仓库
|
||||
# 点击GitHub上的Fork按钮
|
||||
|
||||
# 2. 克隆你的fork
|
||||
git clone https://github.com/your-username/CC4SI.git
|
||||
cd CC4SI
|
||||
|
||||
# 3. 创建分支
|
||||
git checkout -b feature/your-feature-name
|
||||
|
||||
# 4. 做出修改
|
||||
# 编辑文件...
|
||||
|
||||
# 5. 提交修改
|
||||
git add .
|
||||
git commit -m "描述你的修改"
|
||||
|
||||
# 6. 推送到你的fork
|
||||
git push origin feature/your-feature-name
|
||||
|
||||
# 7. 创建Pull Request
|
||||
# 在GitHub上创建PR
|
||||
```
|
||||
|
||||
#### 提交信息规范
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
**类型(type)**:
|
||||
- `fix`: 修复bug
|
||||
- `feat`: 新功能
|
||||
- `docs`: 文档修改
|
||||
- `style`: 格式修改
|
||||
- `refactor`: 代码重构
|
||||
- `test`: 测试相关
|
||||
- `chore`: 构建/工具相关
|
||||
|
||||
**示例**:
|
||||
```
|
||||
fix(01-foundations): 修正状态机示例中的变量名错误
|
||||
|
||||
- 将 `state_machine` 改为 `workflow_state`
|
||||
- 更新相关注释
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 内容规范
|
||||
|
||||
### Markdown格式
|
||||
|
||||
```markdown
|
||||
# 一级标题(章节标题)
|
||||
|
||||
## 二级标题(小节标题)
|
||||
|
||||
### 三级标题(子小节)
|
||||
|
||||
#### 四级标题(通常不需要)
|
||||
|
||||
**粗体**用于强调
|
||||
*斜体*用于术语
|
||||
`代码`使用反引号
|
||||
```
|
||||
|
||||
### 代码规范
|
||||
|
||||
Python代码示例应遵循:
|
||||
|
||||
1. **PEP 8**风格指南
|
||||
2. **类型提示**:函数签名包含类型
|
||||
3. **文档字符串**:重要函数有docstring
|
||||
4. **注释**:关键逻辑有解释
|
||||
|
||||
```python
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
def analyze_ecological_network(
|
||||
sources: List[Dict],
|
||||
resistance_surface: np.ndarray,
|
||||
threshold: float = 0.5
|
||||
) -> Dict:
|
||||
"""
|
||||
分析生态网络
|
||||
|
||||
Args:
|
||||
sources: 源地列表
|
||||
resistance_surface: 阻力面
|
||||
threshold: 连接阈值
|
||||
|
||||
Returns:
|
||||
分析结果字典
|
||||
"""
|
||||
# 实现逻辑...
|
||||
pass
|
||||
```
|
||||
|
||||
### 示例代码要求
|
||||
|
||||
1. **可运行**:示例代码应该能直接运行
|
||||
2. **自包含**:包含必要的import
|
||||
3. **有输出**:展示预期输出
|
||||
4. **有注释**:解释关键步骤
|
||||
|
||||
### 图表和可视化
|
||||
|
||||
优先使用代码生成图表:
|
||||
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# 创建图表
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
ax.plot([1, 2, 3], [1, 4, 9])
|
||||
ax.set_title("示例图表")
|
||||
ax.set_xlabel("X轴")
|
||||
ax.set_ylabel("Y轴")
|
||||
|
||||
# 保存(如果需要)
|
||||
# plt.savefig('output.png', dpi=300)
|
||||
|
||||
plt.show()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 写作指南
|
||||
|
||||
### 章节模板
|
||||
|
||||
每章遵循以下结构:
|
||||
|
||||
```markdown
|
||||
# 章节标题
|
||||
|
||||
## 核心问题
|
||||
> 用1-2个引人思考的问题开场
|
||||
|
||||
## 概念讲解
|
||||
(原理阐述,配合示例)
|
||||
|
||||
## 设计原理
|
||||
(为什么这样设计,权衡是什么)
|
||||
|
||||
## 代码示例
|
||||
```python
|
||||
# 可运行代码
|
||||
```
|
||||
|
||||
## 案例分析
|
||||
(真实项目解析)
|
||||
|
||||
## 反思与延伸
|
||||
### 思考问题
|
||||
### 延伸阅读
|
||||
|
||||
## 关键要点
|
||||
(总结本章核心)
|
||||
```
|
||||
|
||||
### 语言风格
|
||||
|
||||
1. **清晰直接**:避免冗长的句子
|
||||
2. **专业准确**:术语使用正确
|
||||
3. **读者友好**:从读者角度写作
|
||||
4. **主动语态**:多用主动语态
|
||||
|
||||
---
|
||||
|
||||
## 实践项目规范
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
projects/project-name/
|
||||
├── README.md # 项目说明
|
||||
├── requirements.txt # 依赖
|
||||
├── data/ # 示例数据
|
||||
├── src/ # 源代码
|
||||
│ └── __init__.py
|
||||
├── notebooks/ # Jupyter notebooks(可选)
|
||||
├── tests/ # 测试
|
||||
│ └── __init__.py
|
||||
└── outputs/ # 预期输出
|
||||
```
|
||||
|
||||
### README规范
|
||||
|
||||
```markdown
|
||||
# 项目名称
|
||||
|
||||
## 目标
|
||||
(项目要达成的目标)
|
||||
|
||||
## 涉及技术
|
||||
(列表说明使用的技术和方法)
|
||||
|
||||
## 实现步骤
|
||||
1. 步骤一
|
||||
2. 步骤二
|
||||
...
|
||||
|
||||
## 运行方法
|
||||
```bash
|
||||
# 命令示例
|
||||
```
|
||||
|
||||
## 预期输出
|
||||
(展示预期结果)
|
||||
|
||||
## 扩展方向
|
||||
(可选的改进方向)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 审查流程
|
||||
|
||||
### Pull Request检查清单
|
||||
|
||||
提交PR前确认:
|
||||
|
||||
- [ ] 代码符合风格规范
|
||||
- [ ] 包含必要的文档
|
||||
- [ ] 示例代码可运行
|
||||
- [ ] 没有新的警告
|
||||
- [ ] 更新了相关文档
|
||||
|
||||
### 审查标准
|
||||
|
||||
PR将被评估:
|
||||
|
||||
1. **准确性**:内容是否正确
|
||||
2. **清晰性**:是否易于理解
|
||||
3. **完整性**:是否有遗漏
|
||||
4. **一致性**:与现有内容风格一致
|
||||
5. **价值**:是否为读者增加价值
|
||||
|
||||
---
|
||||
|
||||
## 许可
|
||||
|
||||
贡献的内容将遵循项目的许可证:CC BY-NC-SA 4.0
|
||||
|
||||
贡献即表示你同意:
|
||||
- 你的贡献将按照此许可证发布
|
||||
- 你拥有贡献内容的权利
|
||||
- 你的贡献是自愿的,无报酬的
|
||||
|
||||
---
|
||||
|
||||
## 社区
|
||||
|
||||
### 行为准则
|
||||
|
||||
1. **尊重**:尊重所有贡献者
|
||||
2. **包容**:欢迎不同背景的贡献者
|
||||
3. **建设性**:提供建设性反馈
|
||||
4. **协作**:以合作精神工作
|
||||
|
||||
### 沟通渠道
|
||||
|
||||
- **GitHub Issues**: 报告问题和讨论
|
||||
- **Pull Requests**: 代码和内容贡献
|
||||
- **Discussions**: 一般性讨论
|
||||
|
||||
---
|
||||
|
||||
## 致谢
|
||||
|
||||
所有贡献者将被列在项目首页的[Contributors](CONTRIBUTORS.md)中。
|
||||
|
||||
感谢你让这本书变得更好!
|
||||
@@ -0,0 +1,970 @@
|
||||
# 附录1:通过VSCode+ClaudeCode+Obsidian打造学术论文写作工作流
|
||||
|
||||
## 核心问题
|
||||
|
||||
> 如何利用AI工具提升学术论文写作效率?
|
||||
> VSCode、Claude Code、Obsidian如何协同工作?
|
||||
> 如何构建从文献检索到投稿的完整工作流?
|
||||
|
||||
---
|
||||
|
||||
## 工作流概览
|
||||
|
||||
### 两阶段写作策略
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 阶段一:初稿形成 (Markdown为主) │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ 文献管理 │───→│ 大纲构思 │───→│ 内容撰写 │───→│ 初稿自查 │ │
|
||||
│ │ Zotero/MCP │ │ Obsidian │ │ VSCode │ │Claude Code │ │
|
||||
│ └────────────┘ └────────────┘ │Claude Code │ └────────────┘ │
|
||||
│ └────────────┘ │
|
||||
│ ↓ Markdown 格式 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ 格式转换 (Pandoc)
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 阶段二:修改投稿 (Word/LaTeX) │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 导出 Word │ │ 导出 LaTeX │ │
|
||||
│ │ (导师批阅) │ │ (期刊投稿) │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │
|
||||
│ ↓ ↓ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 导入Word修改 │ │ LaTeX编译 │ │ 投稿系统提交 │ │
|
||||
│ │ 批注/修订模式 │ │ PDF预览 │ │ 最终检查 │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 为什么采用两阶段策略?
|
||||
|
||||
| 阶段 | 格式 | 优势 | 适用场景 |
|
||||
|-----|------|------|---------|
|
||||
| **初稿阶段** | **Markdown** | • 版本控制友好<br>• AI辅助高效<br>• 结构清晰<br>• 跨平台兼容 | 内容创作、结构搭建、逻辑梳理 |
|
||||
| **修改阶段** | **Word** | • 批注功能完善<br>• 协作修改方便<br>• 导师习惯使用<br>• 修订记录清晰 | 导师审阅、多人协作、逐点修改 |
|
||||
| **投稿阶段** | **LaTeX** | • 期刊模板支持<br>• 排版专业<br>• 公式美观<br>• 自动化程度高 | 期刊投稿、最终定稿 |
|
||||
|
||||
---
|
||||
|
||||
## 工具介绍与定位
|
||||
|
||||
### 三大工具分工
|
||||
|
||||
| 工具 | 核心功能 | 适用场景 | 不适用场景 |
|
||||
|-----|---------|---------|-----------|
|
||||
| **VSCode** | 代码开发、结构化编辑 | 复杂文档重构、批量处理 | 自由笔记 |
|
||||
| **Claude Code** | AI辅助编写、代码生成 | 内容生成、代码分析、调试 | 灵活思考 |
|
||||
| **Obsidian** | 知识管理、笔记链接 | 知识网络构建、文献笔记 | 代码开发 |
|
||||
|
||||
### 协同方式
|
||||
|
||||
```
|
||||
初稿阶段 (Markdown) 修改投稿阶段
|
||||
──────────────── ──────────────
|
||||
│
|
||||
Obsidian (知识库) ──→ VSCode (编辑) ────→ Pandoc ──→ Word (导师审阅)
|
||||
│ │ │
|
||||
│ Claude Code │
|
||||
│ (AI辅助) ↓
|
||||
└──────────────→└──────────────────────── LaTeX (投稿)
|
||||
│
|
||||
↓
|
||||
main.md (初稿)
|
||||
```
|
||||
|
||||
**关键原则**:
|
||||
- **初稿阶段全 Markdown**:充分利用版本控制、AI辅助、跨平台优势
|
||||
- **导出点即分支点**:从 Markdown 导出时创建 Git 分支
|
||||
- **修改在 Markdown 中进行**:Word/LaTeX 的修改最终应同步回 Markdown
|
||||
- **保持单一信源**:Markdown 始终是内容的"真实来源"
|
||||
|
||||
---
|
||||
|
||||
## 环境搭建
|
||||
|
||||
### 1. VSCode 配置
|
||||
|
||||
**推荐扩展**:
|
||||
|
||||
```json
|
||||
// .vscode/settings.json
|
||||
{
|
||||
// 编辑器基础
|
||||
"editor.fontSize": 14,
|
||||
"editor.lineHeight": 1.8,
|
||||
"editor.fontFamily": "'Cascadia Code', 'Microsoft YaHei UI'",
|
||||
"editor.wordWrap": "on",
|
||||
"editor.minimap.enabled": false,
|
||||
|
||||
// Markdown
|
||||
"markdown.preview.fontSize": 16,
|
||||
"markdown.preview.lineHeight": 1.8,
|
||||
"markdown.preview.breaks": true,
|
||||
|
||||
// 拼写检查
|
||||
"cSpell.enabled": true,
|
||||
"cSpell.language": "en,zh-CN",
|
||||
|
||||
// Pandoc 支持
|
||||
"pandoc.document outputPath": "${documentBaseName}.docx"
|
||||
}
|
||||
```
|
||||
|
||||
**推荐扩展列表**:
|
||||
- `Markdown All in One` - Markdown 增强
|
||||
- `Pandoc` - 文档格式转换
|
||||
- `Code Spell Checker` - 拼写检查
|
||||
- `GitLens` - Git 增强
|
||||
- `Zettelkasten` - 笔记链接支持
|
||||
|
||||
### 2. Claude Code 配置
|
||||
|
||||
```json
|
||||
// ~/.claude/config.json 或项目 .claude/config.json
|
||||
{
|
||||
"mcpServers": {
|
||||
// 文件系统访问
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem",
|
||||
"D:/我的论文", "D:/我的文献"]
|
||||
},
|
||||
|
||||
// Git 操作
|
||||
"git": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-git",
|
||||
"--repository", "D:/我的论文"]
|
||||
},
|
||||
|
||||
// Brave 搜索(文献检索)
|
||||
"brave-search": {
|
||||
"transport": "sse",
|
||||
"url": "https://modelcontextprotocol.io/servers/brave-search",
|
||||
"env": {
|
||||
"BRAVE_API_KEY": "your_api_key"
|
||||
}
|
||||
},
|
||||
|
||||
// 数据库(可选,文献管理)
|
||||
"postgres": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-postgres"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Obsidian 配置
|
||||
|
||||
**推荐插件**:
|
||||
|
||||
| 插件 | 功能 | 配置要点 |
|
||||
|-----|------|---------|
|
||||
| **Obsidian Git** | 版本控制 | 自动提交间隔 15 分钟 |
|
||||
| **Zotero Integration** | 文献引用 | 设置 Zotero 路径 |
|
||||
| **Citations** | 参考文献 | 支持 BibTeX |
|
||||
| **Dataview** | 数据查询 | 文献元数据查询 |
|
||||
| **Advanced Tables** | 表格编辑 | 学术表格必备 |
|
||||
| ** Pandoc Plugin** | 格式转换 | 导出 Word/PDF |
|
||||
|
||||
**Obsidian 设置**:
|
||||
|
||||
```json
|
||||
{
|
||||
"vimModeEnabled": false,
|
||||
"showLineNumber": true,
|
||||
"foldHeading": true,
|
||||
"foldIndent": true,
|
||||
"spellcheck": true,
|
||||
"spellcheckLanguages": ["zh-CN", "en"],
|
||||
"attachmentFolderPath": "assets",
|
||||
"useMarkdownLinks": true,
|
||||
"newFileLocation": "folder",
|
||||
"newFileFolderPath": "inbox"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段一:文献管理与知识积累
|
||||
|
||||
### Zotero + Obsidian 文献工作流
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Zotero │────→│ Obsidian │────→│ 知识网络 │
|
||||
│ 文献收集 │ │ 文献笔记 │ │ 双向链接 │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
↓ ↓ ↓
|
||||
PDF元数据 文献摘要 思维导图
|
||||
标签分类 关键观点 创新想法
|
||||
```
|
||||
|
||||
### Obsidian 文献笔记模板
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: literature
|
||||
citekey: {{citekey}}
|
||||
title: "{{title}}"
|
||||
authors: {{authors}}
|
||||
year: {{year}}
|
||||
journal: {{publication}}
|
||||
tags: [文献笔记, {{tags}}]
|
||||
status: reading
|
||||
---
|
||||
|
||||
# {{title}}
|
||||
|
||||
## 一句话总结
|
||||
|
||||
|
||||
## 核心贡献
|
||||
|
||||
|
||||
## 研究方法
|
||||
|
||||
|
||||
## 主要发现
|
||||
|
||||
|
||||
## 局限与展望
|
||||
|
||||
|
||||
## 与我研究的关系
|
||||
|
||||
|
||||
## 可引用的句子
|
||||
|
||||
|
||||
## 相关文献
|
||||
- [[相关文献1]]
|
||||
- [[相关文献2]]
|
||||
```
|
||||
|
||||
### Claude Code 辅助文献阅读
|
||||
|
||||
```bash
|
||||
# 在 Claude Code 中
|
||||
|
||||
# 1. 批量提取文献关键信息
|
||||
> 帮我分析这个PDF,提取:研究目的、方法、主要发现、局限性
|
||||
# [上传PDF或使用MCP访问文件]
|
||||
|
||||
# 2. 文献对比
|
||||
> 对比这两篇文献的研究方法差异:
|
||||
# 文献1: [[Smith2023-methods]]
|
||||
# 文献2: [[Zhang2024-approach]]
|
||||
|
||||
# 3. 寻找研究缺口
|
||||
> 基于以下文献摘要,分析当前研究的空白点:
|
||||
# - [[文献1]]
|
||||
# - [[文献2]]
|
||||
# - [[文献3]]
|
||||
|
||||
# 4. 生成文献综述框架
|
||||
> 根据我的文献笔记,生成一个"空间AI不确定性"主题的综述框架
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段二:大纲构思与结构设计
|
||||
|
||||
### 在 Obsidian 中构建论文结构
|
||||
|
||||
```markdown
|
||||
# 论文大纲
|
||||
|
||||
## 摘要
|
||||
- [[摘要-背景]]
|
||||
- [[摘要-方法]]
|
||||
- [[摘要-结果]]
|
||||
- [[摘要-结论]]
|
||||
|
||||
## 1. 引言
|
||||
- [[1.1 研究背景]]
|
||||
- [[1.2 问题陈述]]
|
||||
- [[1.3 研究目标]]
|
||||
- [[1.4 论文结构]]
|
||||
|
||||
## 2. 文献综述
|
||||
- [[2.1 空间智能发展]]
|
||||
- [[2.2 生态网络分析方法]]
|
||||
- [[2.3 人机协同研究]]
|
||||
- [[2.4 研究缺口]]
|
||||
|
||||
## 3. 方法
|
||||
- [[3.1 研究区域]]
|
||||
- [[3.2 数据来源]]
|
||||
- [[3.3 分析方法]]
|
||||
- [[3.4 技术实现]]
|
||||
|
||||
## 4. 结果
|
||||
- [[4.1 源地识别结果]]
|
||||
- [[4.2 阻力面分析]]
|
||||
- [[4.3 网络优化]]
|
||||
|
||||
## 5. 讨论
|
||||
- [[5.1 方法创新]]
|
||||
- [[5.2 结果解释]]
|
||||
- [[5.3 局限性]]
|
||||
|
||||
## 6. 结论
|
||||
- [[6.1 主要发现]]
|
||||
- [[6.2 实践意义]]
|
||||
- [[6.3 未来方向]]
|
||||
```
|
||||
|
||||
### Claude Code 辅助大纲设计
|
||||
|
||||
```bash
|
||||
# 使用 Claude Code 优化大纲
|
||||
|
||||
> 我要写一篇关于"生态网络智能分析"的论文,目标期刊是Landscape and Urban Planning。
|
||||
# 基于以下文献笔记,帮我生成一个详细大纲:
|
||||
# - [[关键文献1]]
|
||||
# - [[关键文献2]]
|
||||
#
|
||||
# 要求:
|
||||
# 1. 符合目标期刊的结构要求
|
||||
# 2. 突出方法创新点
|
||||
# 3. 逻辑连贯
|
||||
# 4. 每节说明预计字数
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段三:内容撰写
|
||||
|
||||
### VSCode + Claude Code 写作模式
|
||||
|
||||
```
|
||||
VSCode 编辑区
|
||||
│
|
||||
│ 实时编辑
|
||||
↓
|
||||
Claude Code 侧边栏/终端
|
||||
│
|
||||
├── AI 辅助生成
|
||||
├── 代码示例生成
|
||||
├── 文献引用建议
|
||||
└── 语言润色
|
||||
```
|
||||
|
||||
### 写作技能配置
|
||||
|
||||
创建 `.claude/skills/academic-writing.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: academic-writing
|
||||
description: 学术论文写作辅助技能
|
||||
parameters:
|
||||
- section_type: 论文部分类型
|
||||
- target_words: 目标字数
|
||||
- writing_style: 写作风格
|
||||
---
|
||||
|
||||
## 学术写作技能
|
||||
|
||||
当用户需要撰写学术论文内容时使用此技能。
|
||||
|
||||
### 写作原则
|
||||
1. 清晰第一:避免歧义和模糊表达
|
||||
2. 逻辑连贯:段落间有明确过渡
|
||||
3. 证据支撑:每个论断有文献或数据支持
|
||||
4. 学术规范:使用专业术语,避免口语化
|
||||
5. 主动语态:适当使用主动语态增强可读性
|
||||
|
||||
### 各部分写作要点
|
||||
|
||||
#### 摘要 (Abstract)
|
||||
- 结构:背景-问题-方法-结果-结论
|
||||
- 字数:200-250词
|
||||
- 避免:引用文献、缩写未定义
|
||||
|
||||
#### 引言 (Introduction)
|
||||
- 漏斗结构:从广泛背景到具体问题
|
||||
- 结尾明确:列出研究目标/假设
|
||||
- 避免:过度详细的文献综述
|
||||
|
||||
#### 方法 (Methods)
|
||||
- 结构:研究设计-数据-分析-实现
|
||||
- 细节:可重复性原则
|
||||
- 避免:结果或讨论内容
|
||||
|
||||
#### 结果 (Results)
|
||||
- 客观呈现:不带解读
|
||||
- 图表配合:重要发现可视化
|
||||
- 避免:过度解读
|
||||
|
||||
#### 讨论 (Discussion)
|
||||
- 解读结果:与前人研究对比
|
||||
- 承认局限:诚实讨论限制
|
||||
- 避免:重复结果
|
||||
|
||||
#### 结论 (Conclusion)
|
||||
- 简洁总结:3-4段
|
||||
- 实践意义:应用价值
|
||||
- 未来方向:具体建议
|
||||
|
||||
### 常用句式模板
|
||||
|
||||
#### 引用观点
|
||||
- "Smith et al. (2023) demonstrated that..."
|
||||
- "Recent studies have shown... (Zhang, 2024)"
|
||||
- "Contrary to previous findings..."
|
||||
|
||||
#### 表述方法
|
||||
- "We employed a mixed-methods approach..."
|
||||
- "Data were collected using..."
|
||||
- "The analysis was performed using..."
|
||||
|
||||
#### 呈现结果
|
||||
- "Results indicated that..."
|
||||
- "A significant positive correlation was found..."
|
||||
- "As shown in Figure 1..."
|
||||
|
||||
#### 讨论发现
|
||||
- "These findings suggest that..."
|
||||
- "This aligns with prior research..."
|
||||
- "Unexpectedly, we observed..."
|
||||
|
||||
#### 承认局限
|
||||
- "A potential limitation of this study is..."
|
||||
- "This study has several constraints..."
|
||||
- "Future research could address..."
|
||||
```
|
||||
|
||||
### 实际写作示例
|
||||
|
||||
```bash
|
||||
# 在 Claude Code 中写作
|
||||
|
||||
# 1. 生成段落初稿
|
||||
> 基于以下要点,写一段关于"生态网络不确定性"的内容(200字):
|
||||
# - 数据不确定性:遥感分类误差
|
||||
# - 参数不确定性:阻力权重主观性
|
||||
# - 方法不确定性:不同算法结果差异
|
||||
#
|
||||
# 要求学术化,引用虚构文献 placeholder
|
||||
|
||||
# 2. 润色现有文本
|
||||
> 请帮我润色这段文字,使其更符合学术规范:
|
||||
# ```
|
||||
# 我们用了一个新方法来分析生态网络。效果挺好,比以前的方法快多了。
|
||||
# 结果显示这个方法很准确。
|
||||
# ```
|
||||
|
||||
# 3. 扩展内容
|
||||
> 这段内容太简略,请扩展到300字,增加:
|
||||
# - 技术细节
|
||||
# - 与前人研究的对比
|
||||
# - 具体数字支撑
|
||||
|
||||
# 4. 检查逻辑
|
||||
> 请检查这两段之间的逻辑衔接,并给出改进建议:
|
||||
# [粘贴两段内容]
|
||||
|
||||
# 5. 生成图表描述
|
||||
> 根据这个数据生成学术风格的图表描述:
|
||||
# 数据:森林连通性 0.75 (±0.12),湿地 0.62 (±0.18),草地 0.45 (±0.21)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段四:代码与图表
|
||||
|
||||
### VSCode 中管理代码
|
||||
|
||||
```
|
||||
项目结构
|
||||
├── paper/
|
||||
│ ├── main.md # 主文档
|
||||
│ ├── figures/ # 图表
|
||||
│ │ ├── fig1-method.py
|
||||
│ │ └── fig2-results.R
|
||||
│ ├── data/ # 数据
|
||||
│ └── tables/ # 表格
|
||||
└── src/ # 源代码
|
||||
├── analysis.py
|
||||
└── visualization.py
|
||||
```
|
||||
|
||||
### Claude Code 辅助代码生成
|
||||
|
||||
```bash
|
||||
# 1. 生成分析代码
|
||||
> 我需要对这个生态网络数据进行分析:
|
||||
# - 计算节点连通性
|
||||
# - 识别关键廊道
|
||||
# - 评估网络鲁棒性
|
||||
#
|
||||
# 请生成 Python 代码,使用 networkx 和 geopandas
|
||||
|
||||
# 2. 生成可视化代码
|
||||
> 帮我写代码生成这个图:
|
||||
# - 三列布局:源地分布、阻力面、廊道网络
|
||||
# - 使用 matplotlib
|
||||
# - 符合学术出版要求(300dpi,标注清晰)
|
||||
|
||||
# 3. 生成表格
|
||||
> 将这个结果转换为 LaTeX 表格格式:
|
||||
# [粘贴结果]
|
||||
|
||||
# 4. 调试代码
|
||||
> 这段代码有错误,帮我找出问题:
|
||||
# [粘贴代码]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段五:初稿完成与格式转换
|
||||
|
||||
### 初稿完成检查点
|
||||
|
||||
在从 Markdown 转换到 Word/LaTeX 之前,确认:
|
||||
|
||||
```markdown
|
||||
# 初稿完成清单
|
||||
|
||||
## 内容完整性
|
||||
- [ ] 各章节齐全(摘要→结论)
|
||||
- [ ] 字数达到目标要求
|
||||
- [ ] 图表数量合适
|
||||
- [ ] 参考文献完整
|
||||
|
||||
## Markdown 格式规范
|
||||
- [ ] 标题层级正确(# ## ###)
|
||||
- [ ] 段落间有空行
|
||||
- [ ] 列表格式正确
|
||||
- [ ] 代码块标识清晰
|
||||
- [ ] 图片路径正确
|
||||
- [ ] 表格格式规范
|
||||
|
||||
## 引用规范
|
||||
- [ ] 使用 BibTeX 或 CSL 引用
|
||||
- [ ] 引用标识符 [@citekey] 正确
|
||||
- [ ] 参考文献列表完整
|
||||
|
||||
## 准备转换
|
||||
- [ ] Git 提交当前版本
|
||||
- [ ] 创建格式转换分支
|
||||
- [ ] 备份原始 Markdown 文件
|
||||
```
|
||||
|
||||
### Markdown → Word 转换
|
||||
|
||||
**适用场景**:导师审阅、多人协作修改
|
||||
|
||||
```bash
|
||||
# 基础转换
|
||||
pandoc main.md -o output.docx
|
||||
|
||||
# 使用期刊模板
|
||||
pandoc main.md \
|
||||
--reference-doc=template.docx \
|
||||
--citeproc \
|
||||
--bibliography=references.bib \
|
||||
-o submission.docx
|
||||
|
||||
# 完整参数示例
|
||||
pandoc main.md \
|
||||
--reference-doc=journal_template.docx \
|
||||
--from=markdown \
|
||||
--to=docx \
|
||||
--citeproc \
|
||||
--bibliography=references.bib \
|
||||
--csl=apa.csl \
|
||||
--toc \
|
||||
--number-sections \
|
||||
-o output.docx
|
||||
```
|
||||
|
||||
**Pandoc 参数说明**:
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|-----|------|------|
|
||||
| `--reference-doc` | Word样式模板 | 期刊提供的模板 |
|
||||
| `--citeproc` | 处理引用 | 自动转换 [@citekey] |
|
||||
| `--bibliography` | 参考文献库 | .bib 文件路径 |
|
||||
| `--csl` | 引用样式 | APA, MLA, IEEE 等 |
|
||||
| `--toc` | 生成目录 | 自动创建目录 |
|
||||
| `--number-sections` | 章节自动编号 | 1.1, 1.2... |
|
||||
|
||||
### Markdown → LaTeX 转换
|
||||
|
||||
**适用场景**:期刊投稿、最终定稿
|
||||
|
||||
```bash
|
||||
# 基础转换
|
||||
pandoc main.md -o output.tex
|
||||
|
||||
# 使用期刊模板
|
||||
pandoc main.md \
|
||||
--template=journal_template.tex \
|
||||
--citeproc \
|
||||
--bibliography=references.bib \
|
||||
--pdf-engine=xelatex \
|
||||
-o output.pdf
|
||||
|
||||
# 完整参数示例
|
||||
pandoc main.md \
|
||||
--template=elsarticle.cls \
|
||||
--from=markdown \
|
||||
--to=latex \
|
||||
--citeproc \
|
||||
--bibliography=references.bib \
|
||||
--csl=elsevier.csl \
|
||||
--toc \
|
||||
--number-sections \
|
||||
--pdf-engine=xelatex \
|
||||
--variable=geometry:a4paper,margin=1in \
|
||||
-o output.pdf
|
||||
```
|
||||
|
||||
**常用期刊模板**:
|
||||
|
||||
```bash
|
||||
# Elsevier 期刊
|
||||
pandoc main.md --template=elsarticle.cls ...
|
||||
|
||||
# IEEE 期刊
|
||||
pandoc main.md --template=ieee.cls ...
|
||||
|
||||
# Springer 期刊
|
||||
pandoc main.md --template=svjour3.cls ...
|
||||
|
||||
# 自定义模板
|
||||
pandoc main.md --template=my_template.tex ...
|
||||
```
|
||||
|
||||
### 保留样式的模板制作
|
||||
|
||||
```bash
|
||||
# 1. 从现有Word文档提取样式
|
||||
pandoc --print-default-data-file reference.docx > my_template.docx
|
||||
|
||||
# 2. 在Word中编辑 my_template.docx,调整样式:
|
||||
# - Normal (正文)
|
||||
# - Heading 1-6 (标题)
|
||||
# - Figure Caption (图表标题)
|
||||
# - Table Caption (表格标题)
|
||||
|
||||
# 3. 使用自定义模板转换
|
||||
pandoc main.md --reference-doc=my_template.docx -o output.docx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段六:Word 中的修改与协作
|
||||
|
||||
### Word 批注与修订模式
|
||||
|
||||
**导师审阅工作流**:
|
||||
|
||||
```
|
||||
Markdown (Pandoc) → Word → 导师批注 → 修订 → 反馈 Markdown
|
||||
↓ ↓ ↓ ↓
|
||||
初稿转换 导出发送 添加批注 修改整理
|
||||
```
|
||||
|
||||
### Word 修订模式使用
|
||||
|
||||
```markdown
|
||||
# Word 审阅功能
|
||||
|
||||
## 1. 开启修订模式
|
||||
【审阅】选项卡 → 【修订】
|
||||
→ 所有修改会被记录
|
||||
|
||||
## 2. 添加批注
|
||||
选中文字 → 【审阅】→ 【新建批注】
|
||||
→ 用于提出疑问或建议
|
||||
|
||||
## 3. 接受/拒绝修改
|
||||
【审阅】→ 【接受】/【拒绝】
|
||||
→ 逐条处理审稿意见
|
||||
|
||||
## 4. 比较文档
|
||||
【审阅】→ 【比较】→ 选择原稿和修改稿
|
||||
→ 查看所有改动
|
||||
```
|
||||
|
||||
### 导师反馈整理
|
||||
|
||||
```bash
|
||||
# 导师反馈后,使用 Claude Code 整理修改意见
|
||||
|
||||
> 我收到了导师的Word批注意见,请帮我整理成修改清单:
|
||||
#
|
||||
# 批注1:第3页第2段,方法描述不够详细
|
||||
# 批注2:图1的坐标轴标注不清
|
||||
# 批注3:第5页参考文献 [Smith2023] 引用格式错误
|
||||
# ...
|
||||
#
|
||||
# 请按优先级排序,并给出每条的修改建议
|
||||
```
|
||||
|
||||
### Word 修改后回写到 Markdown
|
||||
|
||||
```markdown
|
||||
# 方法1:手动同步(推荐用于少量修改)
|
||||
|
||||
1. 在 Word 中查看修订
|
||||
2. 在 VSCode Markdown 中对应修改
|
||||
3. Git 提交修改记录
|
||||
4. 重新生成 Word/LaTeX
|
||||
|
||||
# 方法2:Pandoc Word → Markdown(大量修改后)
|
||||
|
||||
pandoc revised.docx -o revised.md
|
||||
|
||||
# 注意:格式可能需要手动调整
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段七:LaTeX 投稿准备
|
||||
|
||||
### LaTeX 编译与调试
|
||||
|
||||
```bash
|
||||
# 基础编译
|
||||
xelatex main.tex
|
||||
bibtex main
|
||||
xelatex main.tex
|
||||
xelatex main.tex
|
||||
|
||||
# 或使用 latexmk (自动处理多次编译)
|
||||
latexmk -xelatex main.tex
|
||||
|
||||
# 清理辅助文件
|
||||
latexmk -c
|
||||
```
|
||||
|
||||
### LaTeX 常见问题解决
|
||||
|
||||
| 问题 | 解决方法 |
|
||||
|-----|---------|
|
||||
| **中文显示** | 使用 `xeLaTeX` + `\usepackage{ctex}` |
|
||||
| **图片路径** | `\graphicspath{{figures/}}` |
|
||||
| **参考文献** | 检查 `.bib` 文件和 `\cite{}` 命令 |
|
||||
| **表格超宽** | 使用 `resizebox` 或 `adjustwidth` |
|
||||
| **公式编号** | `\numberwithin{equation}{section}` |
|
||||
|
||||
### 投稿前最终检查
|
||||
|
||||
```markdown
|
||||
# 投稿前最终检查清单
|
||||
|
||||
## 文档检查
|
||||
- [ ] 符合期刊字数要求
|
||||
- [ ] 使用期刊模板
|
||||
- [ ] 图表分辨率足够(300dpi)
|
||||
- [ ] 补充材料完整
|
||||
- [ ] 利益冲突声明已签署
|
||||
|
||||
## 元数据检查
|
||||
- [ ] 作者信息完整(姓名、单位、邮箱)
|
||||
- [ ] 通讯作者已标注
|
||||
- [ ] 建议审稿人(3-5人,避免利益冲突)
|
||||
- [ ] 推荐期刊/避免期刊(如有)
|
||||
- [ ] 关键词已提供
|
||||
|
||||
## 文件检查
|
||||
- [ ] 主文档 (Word/PDF)
|
||||
- [ ] 图表文件(如要求单独上传)
|
||||
- [ ] 补充材料/附件
|
||||
- [ ] 投稿信 (Cover Letter)
|
||||
- [ ] 许可协议表格
|
||||
|
||||
## 系统检查
|
||||
- [ ] 注册期刊投稿系统账号
|
||||
- [ ] 填写所有必填元数据
|
||||
- [ ] 上传所有文件到正确位置
|
||||
- [ ] 系统预览检查格式
|
||||
- [ ] 确认提交并记录稿件编号
|
||||
```
|
||||
|
||||
### Claude Code 辅助自查
|
||||
|
||||
```bash
|
||||
# 投稿前全面检查
|
||||
> 请帮我检查论文是否符合投稿要求:
|
||||
#
|
||||
# 目标期刊:Landscape and Urban Planning
|
||||
# 要求:
|
||||
# - 字数:5000-8000词
|
||||
# - 图表:最多8个
|
||||
# - 参考文献:不限但需相关
|
||||
# - 格式:Elsevier LaTeX
|
||||
#
|
||||
# 请检查我的论文并指出需要修改的地方
|
||||
|
||||
# 生成投稿信
|
||||
> 根据论文内容,帮我写一份投稿信:
|
||||
# 标题:[你的论文标题]
|
||||
# 期刊:Landscape and Urban Planning
|
||||
# 主要贡献:[简要描述]
|
||||
#
|
||||
# 要求:专业、简洁、突出创新性
|
||||
|
||||
# 生成 Highlights(如期刊要求)
|
||||
> 为我的论文生成 3-5 条 Highlights:
|
||||
# 每条不超过 85 个字符(含空格)
|
||||
# 突出核心发现和创新点
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 高级技巧
|
||||
|
||||
### 1. Git 分支管理写作
|
||||
|
||||
```bash
|
||||
# 为每个版本创建分支
|
||||
git checkout -b draft-v1
|
||||
# ... 写作 ...
|
||||
git checkout -b revision
|
||||
# ... 修改 ...
|
||||
git checkout master
|
||||
git merge revision
|
||||
|
||||
# 比较版本差异
|
||||
git diff draft-v1 revision
|
||||
```
|
||||
|
||||
### 2. Obsidian 模板系统
|
||||
|
||||
```markdown
|
||||
<!-- templates/section.md -->
|
||||
# {{title}}
|
||||
|
||||
## 核心内容
|
||||
|
||||
|
||||
## 支撑材料
|
||||
- 文献:{{bibliography}}
|
||||
- 数据:{{data}}
|
||||
- 代码:{{code}}
|
||||
|
||||
## 待办事项
|
||||
- [ ]
|
||||
- [ ]
|
||||
```
|
||||
|
||||
### 3. Claude Code 自定义指令
|
||||
|
||||
```markdown
|
||||
<!-- .claude/instructions.md -->
|
||||
## 写作风格偏好
|
||||
- 使用简洁的学术语言
|
||||
- 避免过度修饰
|
||||
- 每段不超过5句话
|
||||
- 主动语态优先
|
||||
|
||||
## 常用术语翻译
|
||||
- 生态网络: ecological network
|
||||
- 源地: source / habitat patch
|
||||
- 阻力面: resistance surface
|
||||
- 廊道: corridor
|
||||
- 景观连接度: landscape connectivity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 如何保持写作连贯性?
|
||||
|
||||
**A**: 在 Obsidian 中使用 MOC (Map of Content) 和双向链接:
|
||||
|
||||
```markdown
|
||||
# 论文 MOC
|
||||
|
||||
## 相关笔记
|
||||
- [[核心概念]]
|
||||
- [[方法笔记]]
|
||||
- [[文献笔记]]
|
||||
|
||||
## 进度跟踪
|
||||
- [x] 引言
|
||||
- [ ] 文献综述 (进行中)
|
||||
- [ ] 方法
|
||||
|
||||
## 待办事项
|
||||
- [ ] 补充XX文献
|
||||
- [ ] 重写YY部分
|
||||
```
|
||||
|
||||
### Q2: 如何高效处理审稿意见?
|
||||
|
||||
**A**: 使用 Claude Code 辅助:
|
||||
|
||||
```bash
|
||||
# 逐条处理审稿意见
|
||||
> 审稿人意见:"The method section lacks detail."
|
||||
#
|
||||
# 请帮我:
|
||||
# 1. 分析审稿人关注点
|
||||
# 2. 列出需要补充的内容
|
||||
# 3. 生成回复草稿
|
||||
# 4. 指出需要修改的具体位置
|
||||
```
|
||||
|
||||
### Q3: 如何避免学术不端?
|
||||
|
||||
**A**:
|
||||
1. **原创性**:自己写初稿,AI仅辅助润色
|
||||
2. **引用规范**:所有引用明确标注
|
||||
3. **查重**:投稿前使用查重工具
|
||||
4. **保存记录**:Git 记录写作过程
|
||||
|
||||
---
|
||||
|
||||
## 工具总结
|
||||
|
||||
| 任务 | 推荐工具 | 备选方案 |
|
||||
|-----|---------|---------|
|
||||
| 文献管理 | Zotero | Mendeley, EndNote |
|
||||
| 笔记整理 | Obsidian | Notion, Roam |
|
||||
| 文本编辑 | VSCode | Typora, Sublime |
|
||||
| AI辅助 | Claude Code | ChatGPT, Copilot |
|
||||
| 格式转换 | Pandoc | Word, LaTeX |
|
||||
| 版本控制 | Git | SVN |
|
||||
| 参考文献 | BibTeX | Zotero, EndNote |
|
||||
| 图表制作 | Python/R | Origin, Excel |
|
||||
|
||||
---
|
||||
|
||||
## 延伸资源
|
||||
|
||||
- **Pandoc 指南**:https://pandoc.org/MANUAL.html
|
||||
- **Zotero 文档**:https://www.zotero.org/support/
|
||||
- **Obsidian 帮助**:https://help.obsidian.md/
|
||||
- **学术写作指南**:各期刊的 Author Guidelines
|
||||
- **Claude Code 文档**:https://claudecode.io/zh
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **工具分工明确**:Obsidian管理知识,VSCode处理结构,Claude Code辅助写作
|
||||
2. **版本控制重要**:Git 记录每次修改,方便回溯和对比
|
||||
3. **模块化写作**:将论文拆分为小文件,逐个击破
|
||||
4. **AI是助手**:AI辅助而非替代,保持学术诚信
|
||||
5. **持续优化**:根据反馈不断改进工作流
|
||||
|
||||
> "好的工具让写作更高效,但思想永远来自你自己。"
|
||||
Reference in New Issue
Block a user