open-interpretability 0.0.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- openinterp/wordnet.py +362 -0
openinterp/syntax.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Label the tokens of source code from its syntax tree, for the scan's syntax label sets.
|
|
2
|
+
|
|
3
|
+
A document with a known programming language (a corpus's ``code_language`` field) goes
|
|
4
|
+
through tree-sitter. Tree-sitter tolerates the broken code that a cut at ``max_chars``
|
|
5
|
+
leaves. Every character of the document then has one label in each of these label sets.
|
|
6
|
+
|
|
7
|
+
* ``syntax node``: identifier, keyword, string, comment, number, operator, bracket,
|
|
8
|
+
punctuation, whitespace or other. A string or a comment counts as one node, so its
|
|
9
|
+
contents are not read as code.
|
|
10
|
+
* ``bracket depth``: how many round, square and curly brackets are open. A bracket has
|
|
11
|
+
the depth of what it holds. Angle brackets don't count, because most grammars do not
|
|
12
|
+
tell them from comparison operators. Depths of ``MAX_DEPTH`` or more share one label.
|
|
13
|
+
* ``round depth``: the same count for round brackets only. GPT-2's L2 N676+ follows this
|
|
14
|
+
count, and the count of all brackets hides it (papers/syntax-positive-controls/README.md).
|
|
15
|
+
* ``indent depth``: the indentation of the character's line, in levels. A blank line has
|
|
16
|
+
the level of the line before it.
|
|
17
|
+
* ``syntax construct``: the innermost function, type definition, parameter list, call's
|
|
18
|
+
arguments, import, loop or conditional around the character, or ``top level``.
|
|
19
|
+
|
|
20
|
+
The grammars name their nodes differently. So this module sorts node names into shared
|
|
21
|
+
labels by pattern. The sort is approximate and is not a language's own specification.
|
|
22
|
+
A language with no grammar in :data:`GRAMMARS` has no spans, so its tokens are unlabelled.
|
|
23
|
+
Tree-sitter gives offsets in UTF-8 bytes, and this module converts them to characters.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import importlib
|
|
29
|
+
import re
|
|
30
|
+
from collections import Counter
|
|
31
|
+
from collections.abc import Callable, Sequence
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from functools import cache
|
|
34
|
+
from importlib.metadata import version
|
|
35
|
+
from itertools import accumulate, groupby
|
|
36
|
+
|
|
37
|
+
from openinterp.core.confounds import (
|
|
38
|
+
BRACKET_DEPTH,
|
|
39
|
+
INDENT_DEPTH,
|
|
40
|
+
ROUND_DEPTH,
|
|
41
|
+
SYNTAX_CONSTRUCT,
|
|
42
|
+
SYNTAX_NODE,
|
|
43
|
+
)
|
|
44
|
+
from openinterp.harness.annotations import Span
|
|
45
|
+
|
|
46
|
+
# The github-code preset's `language` values and their grammars: (package, module function).
|
|
47
|
+
# These are its most frequent programming languages. Markup and build files have no entry.
|
|
48
|
+
GRAMMARS = {
|
|
49
|
+
"C": ("tree-sitter-c", "language"),
|
|
50
|
+
"C#": ("tree-sitter-c-sharp", "language"),
|
|
51
|
+
"C++": ("tree-sitter-cpp", "language"),
|
|
52
|
+
"GO": ("tree-sitter-go", "language"),
|
|
53
|
+
"Haskell": ("tree-sitter-haskell", "language"),
|
|
54
|
+
"Java": ("tree-sitter-java", "language"),
|
|
55
|
+
"JavaScript": ("tree-sitter-javascript", "language"),
|
|
56
|
+
"Lua": ("tree-sitter-lua", "language"),
|
|
57
|
+
"PHP": ("tree-sitter-php", "language_php"),
|
|
58
|
+
"Python": ("tree-sitter-python", "language"),
|
|
59
|
+
"Ruby": ("tree-sitter-ruby", "language"),
|
|
60
|
+
"Rust": ("tree-sitter-rust", "language"),
|
|
61
|
+
"Scala": ("tree-sitter-scala", "language"),
|
|
62
|
+
"Shell": ("tree-sitter-bash", "language"),
|
|
63
|
+
"TypeScript": ("tree-sitter-typescript", "language_typescript"),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
IDENTIFIER = "identifier"
|
|
67
|
+
KEYWORD = "keyword"
|
|
68
|
+
STRING = "string"
|
|
69
|
+
COMMENT = "comment"
|
|
70
|
+
NUMBER = "number"
|
|
71
|
+
OPERATOR = "operator"
|
|
72
|
+
BRACKET = "bracket"
|
|
73
|
+
PUNCTUATION = "punctuation"
|
|
74
|
+
WHITESPACE = "whitespace"
|
|
75
|
+
OTHER = "other"
|
|
76
|
+
|
|
77
|
+
TOP_LEVEL = "top level"
|
|
78
|
+
MAX_DEPTH = 8 # depths from here share a label: few tokens are deeper
|
|
79
|
+
DEFAULT_INDENT = 4 # the columns of one level, in a document whose lines don't show it
|
|
80
|
+
|
|
81
|
+
CLOSER = {"(": ")", "[": "]", "{": "}"}
|
|
82
|
+
OPENING, CLOSING = set(CLOSER), set(CLOSER.values())
|
|
83
|
+
# What can end a comment or a string that a cut leaves open: a line comment, a block
|
|
84
|
+
# comment, then the quotes. The first entry adds nothing.
|
|
85
|
+
_STARTS = ("", "\n", "*/", '"', "'", "`")
|
|
86
|
+
END_KEYWORD_LANGUAGES = {"Lua", "Ruby"} # a block closes with `end`
|
|
87
|
+
MAX_END_KEYWORDS = 12 # more blocks than a cut document leaves open
|
|
88
|
+
_SEPARATORS = {",", ";", ":", "::", "."}
|
|
89
|
+
|
|
90
|
+
_ATOMS = (
|
|
91
|
+
(COMMENT, re.compile(r"comment")),
|
|
92
|
+
(STRING, re.compile(r"(^|_)(string|char|character|heredoc|regex)(_|$)")),
|
|
93
|
+
)
|
|
94
|
+
_NUMBER = re.compile(r"(^|_)(number|integer|int|float|floating|decimal|hex|octal|binary|real)(_|$)")
|
|
95
|
+
_NAMED_KEYWORD = re.compile(
|
|
96
|
+
r"^(true|false|null|nil|none|this|self|super|primitive_type|predefined_type|void_type"
|
|
97
|
+
r"|boolean_type|boolean_literal|null_literal|integral_type|floating_point_type)$"
|
|
98
|
+
)
|
|
99
|
+
_NAME = re.compile(r"identifier|name|word|variable|constant|symbol|field|type")
|
|
100
|
+
# Innermost wins, because the walk paints a child's characters after its parent's.
|
|
101
|
+
_CONSTRUCTS = (
|
|
102
|
+
("import", re.compile(r"import|include|using_directive|use_declaration|namespace_use")),
|
|
103
|
+
("call arguments", re.compile(r"^(argument_list|arguments|actual_parameters)$")),
|
|
104
|
+
("parameters", re.compile(r"parameter")),
|
|
105
|
+
(
|
|
106
|
+
"function",
|
|
107
|
+
re.compile(
|
|
108
|
+
r"^(function|method|lambda|closure_expression|func_literal|arrow_function"
|
|
109
|
+
r"|generator_function|anonymous_function)"
|
|
110
|
+
r"(_definition|_declaration|_item|_expression|_creation_expression)?$"
|
|
111
|
+
r"|^constructor_declaration$"
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
(
|
|
115
|
+
"type definition",
|
|
116
|
+
re.compile(
|
|
117
|
+
r"^(class|struct|interface|enum|trait|union|impl|object|module|namespace|type)"
|
|
118
|
+
r"_(definition|declaration|specifier|item)$|^(class|module)$"
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
(
|
|
122
|
+
"loop",
|
|
123
|
+
re.compile(
|
|
124
|
+
r"^(for|while|do|foreach|enhanced_for|for_in|for_range|repeat|loop|until)"
|
|
125
|
+
r"(_statement|_expression)?$"
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
("conditional", re.compile(r"^(if|unless|match|case)(_statement|_expression)?$|switch_")),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def detector() -> str:
|
|
133
|
+
"""Name the parser and grammar versions, which fix what the syntax label sets say."""
|
|
134
|
+
packages = ("tree-sitter", *sorted(package for package, _ in GRAMMARS.values()))
|
|
135
|
+
return f"openinterp.syntax ({', '.join(f'{p} {version(p)}' for p in packages)})"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@cache
|
|
139
|
+
def _parser(language: str):
|
|
140
|
+
"""Return the parser of ``language``, a key of :data:`GRAMMARS`."""
|
|
141
|
+
import tree_sitter
|
|
142
|
+
|
|
143
|
+
package, function = GRAMMARS[language]
|
|
144
|
+
module = importlib.import_module(package.replace("-", "_"))
|
|
145
|
+
return tree_sitter.Parser(tree_sitter.Language(getattr(module, function)()))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _char_offsets(text: str) -> list[int]:
|
|
149
|
+
"""Return the map from each UTF-8 byte offset of ``text`` to its character offset."""
|
|
150
|
+
if text.isascii():
|
|
151
|
+
return list(range(len(text) + 1))
|
|
152
|
+
widths = (1 if c < "\x80" else 2 if c < "ࠀ" else 3 if c < "\U00010000" else 4 for c in text)
|
|
153
|
+
out = []
|
|
154
|
+
for i, width in enumerate(widths):
|
|
155
|
+
out += [i] * width
|
|
156
|
+
return [*out, len(text)]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _encode(text: str) -> bytes:
|
|
160
|
+
"""UTF-8 of ``text``. A corpus's JSON can hold a lone surrogate, so this keeps it."""
|
|
161
|
+
return text.encode("utf-8", "surrogatepass")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _node_kind(node_type: str, named: bool, text: str) -> str:
|
|
165
|
+
"""Return the ``syntax node`` label of a tree leaf (or of a string or comment)."""
|
|
166
|
+
if not named:
|
|
167
|
+
if text in OPENING or text in CLOSING:
|
|
168
|
+
return BRACKET
|
|
169
|
+
if text in _SEPARATORS:
|
|
170
|
+
return PUNCTUATION
|
|
171
|
+
return KEYWORD if text[:1].isalpha() or text[:1] == "_" else OPERATOR
|
|
172
|
+
for label, pattern in _ATOMS:
|
|
173
|
+
if pattern.search(node_type):
|
|
174
|
+
return label
|
|
175
|
+
if _NUMBER.search(node_type):
|
|
176
|
+
return NUMBER
|
|
177
|
+
if _NAMED_KEYWORD.match(node_type):
|
|
178
|
+
return KEYWORD
|
|
179
|
+
return IDENTIFIER if _NAME.search(node_type) else OTHER
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _atom(node) -> bool:
|
|
183
|
+
"""Is this node a string or a comment, whose contents don't count as code?"""
|
|
184
|
+
return node.is_named and any(pattern.search(node.type) for _, pattern in _ATOMS)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _construct(node) -> str | None:
|
|
188
|
+
"""Return the ``syntax construct`` label of a node, or ``None`` if it opens none."""
|
|
189
|
+
if node.is_named:
|
|
190
|
+
for label, pattern in _CONSTRUCTS:
|
|
191
|
+
if pattern.search(node.type):
|
|
192
|
+
return label
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _paint(text: str, language: str) -> tuple[list[str], list[str]]:
|
|
197
|
+
"""Return each character's ``syntax node`` and ``syntax construct`` labels, from one parse."""
|
|
198
|
+
chars = _char_offsets(text)
|
|
199
|
+
nodes: list[str | None] = [None] * len(text)
|
|
200
|
+
constructs = [TOP_LEVEL] * len(text)
|
|
201
|
+
root = _parser(language).parse(_encode(text)).root_node
|
|
202
|
+
todo = [root]
|
|
203
|
+
while todo: # not recursive, because deep code would pass Python's recursion limit
|
|
204
|
+
node = todo.pop()
|
|
205
|
+
start, end = chars[node.start_byte], chars[node.end_byte]
|
|
206
|
+
# The root is the file. Python calls it `module`, which is not a Ruby `module`.
|
|
207
|
+
if node != root and (construct := _construct(node)) is not None:
|
|
208
|
+
constructs[start:end] = [construct] * (end - start)
|
|
209
|
+
if _atom(node) or not node.children:
|
|
210
|
+
kind = _node_kind(node.type, node.is_named, text[start:end])
|
|
211
|
+
nodes[start:end] = [kind] * (end - start)
|
|
212
|
+
else:
|
|
213
|
+
todo.extend(reversed(node.children))
|
|
214
|
+
return [_gap_kind(c) if n is None else n for n, c in zip(nodes, text, strict=True)], constructs
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _open_brackets(text: str, nodes: Sequence[str]) -> str:
|
|
218
|
+
"""Return the closing brackets of the brackets still open at the end of ``text``."""
|
|
219
|
+
stack: list[str] = []
|
|
220
|
+
for char, node in zip(text, nodes, strict=True):
|
|
221
|
+
if node == BRACKET and char in OPENING:
|
|
222
|
+
stack.append(CLOSER[char])
|
|
223
|
+
elif node == BRACKET and char in CLOSING and stack:
|
|
224
|
+
stack.pop()
|
|
225
|
+
return "".join(reversed(stack))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _missing_closers(text: str, nodes: Sequence[str]) -> str:
|
|
229
|
+
"""Return what closes the brackets still open at the end of ``text``."""
|
|
230
|
+
# A cut also leaves a statement unfinished, and a block needs its statements to end.
|
|
231
|
+
return _open_brackets(text, nodes).replace("}", ";}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _error_chars(text: str, limit: int, language: str) -> int:
|
|
235
|
+
"""Count the characters before ``limit`` that the parser puts in an error node.
|
|
236
|
+
|
|
237
|
+
A node that the parser had to make up (a missing token) counts as one character more.
|
|
238
|
+
A parse with no error gives 0.
|
|
239
|
+
"""
|
|
240
|
+
root = _parser(language).parse(_encode(text)).root_node
|
|
241
|
+
if not root.has_error:
|
|
242
|
+
return 0
|
|
243
|
+
chars, count, todo = _char_offsets(text), 0, [root]
|
|
244
|
+
while todo:
|
|
245
|
+
node = todo.pop()
|
|
246
|
+
if node.type == "ERROR" or node.is_missing:
|
|
247
|
+
start, end = chars[node.start_byte], chars[node.end_byte]
|
|
248
|
+
count += max(0, min(end, limit) - min(start, limit)) + node.is_missing
|
|
249
|
+
elif node.has_error:
|
|
250
|
+
todo.extend(node.children)
|
|
251
|
+
return count
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _endings(text: str, nodes: Sequence[str], language: str) -> list[str]:
|
|
255
|
+
"""Return the texts that can end a cut ``text``, shortest first.
|
|
256
|
+
|
|
257
|
+
A cut can stop inside a comment or a string. So each ending starts with nothing, a line
|
|
258
|
+
break, the end of a block comment or a quote. Then come the closing brackets that are
|
|
259
|
+
open after that start, with and without a ``;`` before each ``}``. In
|
|
260
|
+
:data:`END_KEYWORD_LANGUAGES`, a block closes with ``end`` and not with a bracket. So
|
|
261
|
+
there, endings also add up to ``MAX_END_KEYWORDS`` of them, before or after the brackets.
|
|
262
|
+
"""
|
|
263
|
+
out: set[str] = set()
|
|
264
|
+
for start in _STARTS:
|
|
265
|
+
start_nodes = nodes if not start else _paint(text + start, language)[0][: len(text)]
|
|
266
|
+
brackets = _open_brackets(text, start_nodes)
|
|
267
|
+
tails = {brackets, brackets.replace("}", ";}")}
|
|
268
|
+
if language in END_KEYWORD_LANGUAGES:
|
|
269
|
+
for n in range(1, MAX_END_KEYWORDS + 1):
|
|
270
|
+
tails |= {brackets + "\nend" * n, "\nend" * n + brackets}
|
|
271
|
+
out |= {start + tail for tail in tails}
|
|
272
|
+
return sorted(out - {""}, key=lambda ending: (len(ending), ending))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def parse_labels(text: str, language: str) -> tuple[list[str], list[str]]:
|
|
276
|
+
"""Return each character's ``syntax node`` and ``syntax construct`` labels.
|
|
277
|
+
|
|
278
|
+
The corpus cuts a document at ``max_chars``, usually inside a class or a function. The
|
|
279
|
+
parser then has no node for that class or function, so its contents read as top level.
|
|
280
|
+
So when the parse has errors, this parses again with an ending added, and labels only
|
|
281
|
+
``text``. The ending is the one of :func:`_endings` that leaves the fewest characters
|
|
282
|
+
of ``text`` in error nodes, and the shortest of those. If no ending lowers that count,
|
|
283
|
+
the ending closes the open brackets as the first parse reads them.
|
|
284
|
+
"""
|
|
285
|
+
nodes, constructs = _paint(text, language)
|
|
286
|
+
errors = _error_chars(text, len(text), language)
|
|
287
|
+
if not errors:
|
|
288
|
+
return nodes, constructs
|
|
289
|
+
scored = (
|
|
290
|
+
(_error_chars(text + e, len(text), language), e) for e in _endings(text, nodes, language)
|
|
291
|
+
)
|
|
292
|
+
fewest, ending = min(scored, key=lambda pair: pair[0], default=(errors, ""))
|
|
293
|
+
if fewest >= errors:
|
|
294
|
+
ending = _missing_closers(text, nodes)
|
|
295
|
+
if not ending:
|
|
296
|
+
return nodes, constructs
|
|
297
|
+
nodes, constructs = _paint(text + ending, language)
|
|
298
|
+
return nodes[: len(text)], constructs[: len(text)]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _gap_kind(char: str) -> str:
|
|
302
|
+
"""Return the ``syntax node`` label of a character that no tree leaf covers."""
|
|
303
|
+
return WHITESPACE if char.isspace() else OTHER
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _depth_label(depth: int) -> str:
|
|
307
|
+
return f"{MAX_DEPTH}+" if depth >= MAX_DEPTH else str(depth)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def bracket_depths(text: str, nodes: Sequence[str], kinds: str = "([{") -> list[str]:
|
|
311
|
+
"""Return each character's depth of the brackets that ``kinds`` opens.
|
|
312
|
+
|
|
313
|
+
``nodes`` holds the characters' ``syntax node`` labels. So a bracket in a string or
|
|
314
|
+
a comment doesn't count. A closing bracket with no opening bracket leaves the depth at 0.
|
|
315
|
+
"""
|
|
316
|
+
openers, closers = set(kinds), {CLOSER[k] for k in kinds}
|
|
317
|
+
out, depth = [], 0
|
|
318
|
+
for char, node in zip(text, nodes, strict=True):
|
|
319
|
+
if node == BRACKET and char in openers:
|
|
320
|
+
depth += 1
|
|
321
|
+
out.append(_depth_label(depth))
|
|
322
|
+
if node == BRACKET and char in closers:
|
|
323
|
+
depth = max(0, depth - 1)
|
|
324
|
+
return out
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _indent_unit(widths: Sequence[int]) -> int:
|
|
328
|
+
"""Return the columns of one level: the most frequent step between indented lines.
|
|
329
|
+
|
|
330
|
+
Lines' first indents can mislead. A `` * `` comment line has one space, and is not a
|
|
331
|
+
level. So this takes the step that the lines repeat most. ``DEFAULT_INDENT`` if there
|
|
332
|
+
is no step.
|
|
333
|
+
"""
|
|
334
|
+
steps = Counter(b - a for a, b in zip(widths, widths[1:], strict=False) if b > a)
|
|
335
|
+
return steps.most_common(1)[0][0] if steps else DEFAULT_INDENT
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def indent_depths(text: str) -> list[str]:
|
|
339
|
+
"""Return each character's ``indent depth`` label, from its line's leading whitespace."""
|
|
340
|
+
lines = text.splitlines(keepends=True)
|
|
341
|
+
tabbed = [len(line) - len(line.lstrip(" \t")) for line in lines]
|
|
342
|
+
blank = [not line.strip() for line in lines]
|
|
343
|
+
tabs = [line[:n].count("\t") for line, n in zip(lines, tabbed, strict=True)]
|
|
344
|
+
spaces = [n - t for n, t in zip(tabbed, tabs, strict=True)]
|
|
345
|
+
solid = [s for s, b, t in zip(spaces, blank, tabs, strict=True) if not b and not t]
|
|
346
|
+
unit = _indent_unit(solid)
|
|
347
|
+
out, level = [], 0
|
|
348
|
+
for line, is_blank, tab, space in zip(lines, blank, tabs, spaces, strict=True):
|
|
349
|
+
if not is_blank:
|
|
350
|
+
level = tab + space // unit
|
|
351
|
+
out += [_depth_label(level)] * len(line)
|
|
352
|
+
return out
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _runs(labels: Sequence[str]) -> list[Span]:
|
|
356
|
+
"""Join neighbouring characters of one label into spans."""
|
|
357
|
+
lengths = [(label, len(list(run))) for label, run in groupby(labels)]
|
|
358
|
+
ends = accumulate(n for _, n in lengths)
|
|
359
|
+
return [Span(end - n, end, label) for (label, n), end in zip(lengths, ends, strict=True)]
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@dataclass(frozen=True)
|
|
363
|
+
class SyntaxTagger:
|
|
364
|
+
"""Label a document's characters with ``read``, for documents that have a grammar."""
|
|
365
|
+
|
|
366
|
+
name: str
|
|
367
|
+
read: Callable[[int, str], list[Span]]
|
|
368
|
+
|
|
369
|
+
def spans(self, doc: int, text: str) -> list[Span]:
|
|
370
|
+
"""The document's spans in this label set. A document without a grammar has none."""
|
|
371
|
+
return self.read(doc, text)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def syntax_taggers(languages: Sequence[str | None]) -> list[SyntaxTagger]:
|
|
375
|
+
"""Return one tagger per syntax label set, over documents of these ``languages``.
|
|
376
|
+
|
|
377
|
+
The taggers share one parse of each document. ``languages`` holds one value per
|
|
378
|
+
document, or ``None``. A document with a language outside :data:`GRAMMARS` has no spans.
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
@cache
|
|
382
|
+
def parsed(doc: int, text: str) -> tuple[list[str], list[str]] | None:
|
|
383
|
+
language = languages[doc]
|
|
384
|
+
return parse_labels(text, language) if language in GRAMMARS else None
|
|
385
|
+
|
|
386
|
+
def tagger(name: str, labels: Callable[[str, list[str], list[str]], Sequence[str]]):
|
|
387
|
+
def read(doc: int, text: str) -> list[Span]:
|
|
388
|
+
found = parsed(doc, text)
|
|
389
|
+
return [] if found is None else _runs(labels(text, *found))
|
|
390
|
+
|
|
391
|
+
return SyntaxTagger(name, read)
|
|
392
|
+
|
|
393
|
+
return [
|
|
394
|
+
tagger(SYNTAX_NODE, lambda _, nodes, __: nodes),
|
|
395
|
+
tagger(BRACKET_DEPTH, lambda text, nodes, _: bracket_depths(text, nodes)),
|
|
396
|
+
tagger(ROUND_DEPTH, lambda text, nodes, _: bracket_depths(text, nodes, "(")),
|
|
397
|
+
tagger(INDENT_DEPTH, lambda text, _, __: indent_depths(text)),
|
|
398
|
+
tagger(SYNTAX_CONSTRUCT, lambda _, __, constructs: constructs),
|
|
399
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Part of check 5 of ``open-interpretability check``: the takedown file.
|
|
2
|
+
|
|
3
|
+
The file's format is in :mod:`openinterp.core.takedowns`.
|
|
4
|
+
|
|
5
|
+
The file parses and has unique ids. A key in the allowed signers (from ``base_ref`` if
|
|
6
|
+
given) signed it, so a branch can't add a takedown of its own. With ``base_ref`` the file
|
|
7
|
+
is append-only, and must not be deleted.
|
|
8
|
+
|
|
9
|
+
:func:`page_redaction_ok` is the one exception to write-once experiment pages. A page may
|
|
10
|
+
change outside its Discussion section when the change only removes what the takedowns
|
|
11
|
+
cover.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from openinterp.core.takedowns import all_hashes, load_takedowns, parse_takedowns, redact
|
|
21
|
+
from openinterp.core.takedowns import takedowns_path as path_of
|
|
22
|
+
from openinterp.errata_checks import changed_file, check_signature
|
|
23
|
+
from openinterp.signing import allowed_signers_text, signature_path
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from openinterp.checks import CheckReport
|
|
27
|
+
|
|
28
|
+
WHAT = "takedown file"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_takedowns(
|
|
32
|
+
wiki_dir: Path,
|
|
33
|
+
report: CheckReport,
|
|
34
|
+
base_ref: str | None,
|
|
35
|
+
changes: list[tuple[str, Path]],
|
|
36
|
+
allowed_signers: Path,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Add a problem to ``report`` for each way the takedown file is not valid."""
|
|
39
|
+
path = path_of(wiki_dir)
|
|
40
|
+
if any(p == path and status == "D" for status, p in changes):
|
|
41
|
+
report.add(path, f"the {WHAT} is append-only and must not be deleted")
|
|
42
|
+
if not path.exists():
|
|
43
|
+
if signature_path(path).exists():
|
|
44
|
+
report.add(signature_path(path), f"signature of a {WHAT} that does not exist")
|
|
45
|
+
return
|
|
46
|
+
text = path.read_text(encoding="utf-8")
|
|
47
|
+
try:
|
|
48
|
+
takedowns = parse_takedowns(text)
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
report.add(path, f"invalid takedown: {exc}")
|
|
51
|
+
return
|
|
52
|
+
ids = [t.id for t in takedowns]
|
|
53
|
+
for dup in sorted({i for i in ids if ids.count(i) > 1}):
|
|
54
|
+
report.add(path, f"takedown id {dup!r} is used more than once")
|
|
55
|
+
changed = changed_file(path, base_ref, changes)
|
|
56
|
+
if base_ref and changed:
|
|
57
|
+
shown = subprocess.run(
|
|
58
|
+
["git", "show", f"{base_ref}:{path.as_posix()}"], capture_output=True, text=True
|
|
59
|
+
)
|
|
60
|
+
if shown.returncode == 0 and not text.startswith(shown.stdout):
|
|
61
|
+
report.add(path, f"the {WHAT} is append-only: add lines at the end, change none")
|
|
62
|
+
signers = allowed_signers_text(allowed_signers, base_ref)
|
|
63
|
+
check_signature(path, report, signers, changed, WHAT)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def page_redaction_ok(before: str, after: str, wiki_dir: Path) -> bool:
|
|
67
|
+
"""Whether ``after`` is ``before`` with exactly the text that the takedowns cover removed.
|
|
68
|
+
|
|
69
|
+
Both are a page's text before its Discussion section. The check that the takedown
|
|
70
|
+
file is signed is :func:`check_takedowns`, which runs in the same
|
|
71
|
+
``open-interpretability check``.
|
|
72
|
+
"""
|
|
73
|
+
from openinterp.builder.pii import REMOVED_TEXT
|
|
74
|
+
|
|
75
|
+
hashes = all_hashes(load_takedowns(wiki_dir))
|
|
76
|
+
return before != after and redact(before, hashes, REMOVED_TEXT) == after
|
openinterp/toxicity.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Find toxic text and hate speech in a document, for the scan's label sets.
|
|
2
|
+
|
|
3
|
+
Both labels are models' guesses, for experiments only. Nothing removes text because of
|
|
4
|
+
them. The site's spoiler guard uses a word list (``sensitive.is_offensive``), so that a
|
|
5
|
+
site build needs no model.
|
|
6
|
+
|
|
7
|
+
A model reads text, not tokens. So each sentence is read alone, and all of its tokens take
|
|
8
|
+
the sentence's label. A label therefore says where toxic text is, not which word made it so.
|
|
9
|
+
|
|
10
|
+
The two models do different work. We compared them on HateCheck (Röttger et al., 2021),
|
|
11
|
+
which neither trained on, at a score of at least 0.5:
|
|
12
|
+
|
|
13
|
+
- ``unitary/unbiased-toxic-roberta`` (Apache 2.0) gives ``toxicity``. It alone finds
|
|
14
|
+
obscenity, insults, threats and sexual text. Its identity outputs (``female``, ``black``)
|
|
15
|
+
say that a text mentions a group, so we leave them out. Built from them, a target
|
|
16
|
+
category found 0% to 76% of hateful cases, and 0% for immigrants, trans people and
|
|
17
|
+
disabled people.
|
|
18
|
+
- ``cardiffnlp/twitter-roberta-base-hate-multiclass-latest`` gives ``hate speech``. It
|
|
19
|
+
found 40% of hateful cases, and called 9% of non-hateful cases hate. It names the right
|
|
20
|
+
target in 13% to 62% of a group's cases, but in 1% for disabled people. Its model card
|
|
21
|
+
states no licence. It trained on tweets. We leave out its ``other`` output, because that
|
|
22
|
+
output called lines of Ruby code hate.
|
|
23
|
+
|
|
24
|
+
Both models are English models. Scoring runs on the CPU, at about 10 ms a sentence for each
|
|
25
|
+
model. Each document's result stays on disk. The models (about 500 MB each) are downloaded
|
|
26
|
+
once, pinned to a commit.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import hashlib
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
from collections.abc import Sequence
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from functools import cache
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
THRESHOLD = 0.5 # the score at which a sentence takes a label
|
|
41
|
+
_BATCH = 16
|
|
42
|
+
_MAX_CHARS = 1000 # a longer "sentence" (code, a table) is read in pieces of this length
|
|
43
|
+
_MAX_TOKENS = 512 # what the models accept
|
|
44
|
+
# On our corpus, the models called one-word lines hate or toxic ("Fluker.", "F.eks.").
|
|
45
|
+
_MIN_WORDS = 2
|
|
46
|
+
# Up to a stop followed by a space, else up to the end of the line.
|
|
47
|
+
_SENTENCE = re.compile(r"\S(?:[^\n]*?[.!?](?=\s)|[^\n]*)")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class _Classifier:
|
|
52
|
+
repo: str
|
|
53
|
+
revision: str # a commit, so a later scan reads the same weights
|
|
54
|
+
name: str # part of the cache's path
|
|
55
|
+
labels: tuple[str, ...] # the outputs that can label a sentence, most specific first
|
|
56
|
+
softmax: bool = False # the outputs compete; else each output is its own yes or no
|
|
57
|
+
fallback: str | None = None # the label of a sentence with no other label
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_TOXICITY = _Classifier(
|
|
61
|
+
"unitary/unbiased-toxic-roberta",
|
|
62
|
+
"36295dd80b422dc49f40052021430dae76241adc",
|
|
63
|
+
"toxicity",
|
|
64
|
+
("threat", "identity_attack", "sexual_explicit", "obscene", "insult"),
|
|
65
|
+
fallback="toxicity",
|
|
66
|
+
)
|
|
67
|
+
_HATE = _Classifier(
|
|
68
|
+
"cardiffnlp/twitter-roberta-base-hate-multiclass-latest",
|
|
69
|
+
"b9a303f920f8527ac4151e65953c04505fdf0587",
|
|
70
|
+
"hate",
|
|
71
|
+
("racism", "sexism", "religion", "sexual_orientation", "disability"),
|
|
72
|
+
softmax=True,
|
|
73
|
+
)
|
|
74
|
+
DETECTOR = "; ".join(f"{c.repo}@{c.revision[:12]}" for c in (_TOXICITY, _HATE))
|
|
75
|
+
DETECTOR = f"{DETECTOR} (score at least {THRESHOLD}, per sentence of {_MIN_WORDS}+ words)"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def sentences(text: str) -> list[tuple[int, int]]:
|
|
79
|
+
"""Return the ``(start, end)`` of each sentence in ``text``, split to ``_MAX_CHARS``."""
|
|
80
|
+
return [
|
|
81
|
+
(start, min(start + _MAX_CHARS, match.end()))
|
|
82
|
+
for match in _SENTENCE.finditer(text)
|
|
83
|
+
for start in range(match.start(), match.end(), _MAX_CHARS)
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _label(classifier: _Classifier, scores: dict[str, float]) -> str | None:
|
|
88
|
+
"""The strongest of a sentence's labels, else the fallback when its score passes."""
|
|
89
|
+
passed = [label for label in classifier.labels if scores[label] >= THRESHOLD]
|
|
90
|
+
if passed:
|
|
91
|
+
return max(passed, key=scores.__getitem__)
|
|
92
|
+
fallback = classifier.fallback
|
|
93
|
+
return fallback if fallback and scores[fallback] >= THRESHOLD else None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@cache
|
|
97
|
+
def _model(classifier: _Classifier):
|
|
98
|
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
|
99
|
+
|
|
100
|
+
tokenizer = AutoTokenizer.from_pretrained(classifier.repo, revision=classifier.revision)
|
|
101
|
+
model = AutoModelForSequenceClassification.from_pretrained(
|
|
102
|
+
classifier.repo, revision=classifier.revision
|
|
103
|
+
)
|
|
104
|
+
return tokenizer, model.eval()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _score(classifier: _Classifier, texts: Sequence[str]) -> list[dict[str, float]]:
|
|
108
|
+
"""Return each text's score per output of the model."""
|
|
109
|
+
import torch
|
|
110
|
+
|
|
111
|
+
tokenizer, model = _model(classifier)
|
|
112
|
+
outputs = [model.config.id2label[i] for i in range(model.config.num_labels)]
|
|
113
|
+
found: list[dict[str, float]] = []
|
|
114
|
+
for first in range(0, len(texts), _BATCH):
|
|
115
|
+
batch = tokenizer(
|
|
116
|
+
list(texts[first : first + _BATCH]),
|
|
117
|
+
truncation=True,
|
|
118
|
+
max_length=_MAX_TOKENS,
|
|
119
|
+
padding=True,
|
|
120
|
+
return_tensors="pt",
|
|
121
|
+
)
|
|
122
|
+
with torch.no_grad():
|
|
123
|
+
logits = model(**batch).logits
|
|
124
|
+
scores = logits.softmax(-1) if classifier.softmax else logits.sigmoid()
|
|
125
|
+
found += [dict(zip(outputs, row, strict=True)) for row in scores.tolist()]
|
|
126
|
+
return found
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _cache_dir() -> Path:
|
|
130
|
+
return Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "openinterp"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _matches(classifier: _Classifier, text: str) -> tuple[tuple[int, int, str], ...]:
|
|
134
|
+
"""Return ``(start, end, label)`` of each labelled sentence, kept on disk by the text."""
|
|
135
|
+
key = hashlib.sha256(text.encode()).hexdigest()
|
|
136
|
+
version = f"{classifier.revision[:12]}-{THRESHOLD}-{_MIN_WORDS}"
|
|
137
|
+
path = _cache_dir() / "toxicity" / classifier.name / version / f"{key}.json"
|
|
138
|
+
if path.exists():
|
|
139
|
+
return tuple((start, end, label) for start, end, label in json.loads(path.read_text()))
|
|
140
|
+
spans = [(a, b) for a, b in sentences(text) if len(text[a:b].split()) >= _MIN_WORDS]
|
|
141
|
+
scores = _score(classifier, [text[start:end] for start, end in spans]) if spans else []
|
|
142
|
+
labels = [_label(classifier, s) for s in scores]
|
|
143
|
+
found = [(*span, label) for span, label in zip(spans, labels, strict=True) if label]
|
|
144
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
partial = path.with_suffix(f".{os.getpid()}.part") # scans can run side by side
|
|
146
|
+
partial.write_text(json.dumps(found))
|
|
147
|
+
partial.replace(path)
|
|
148
|
+
return tuple(found)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def toxicity_matches(text: str) -> tuple[tuple[int, int, str], ...]:
|
|
152
|
+
"""Return ``(start, end, label)`` of each toxic sentence in ``text``, sorted."""
|
|
153
|
+
return _matches(_TOXICITY, text)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def hate_matches(text: str) -> tuple[tuple[int, int, str], ...]:
|
|
157
|
+
"""Return ``(start, end, label)`` of each sentence of hate speech in ``text``, sorted."""
|
|
158
|
+
return _matches(_HATE, text)
|