UIMA CAS processing library in Python
Project description
DKPro cassis (pronunciation: [ka.sis]) provides a pure-Python implementation of the Common Analysis System (CAS) as defined by the UIMA framework. The CAS is a data structure representing an object to be enriched with annotations (the co-called Subject of Analysis, short SofA).
This library enables the creation and manipulation of annotated documents (CAS objects) and their associated type systems as well as loading and saving them in the CAS XMI XML representation or the CAS JSON representation in Python programs. This can ease in particular the integration of Python-based Natural Language Processing (e.g. spacy or NLTK) and Machine Learning librarys (e.g. scikit-learn or Keras) in UIMA-based text analysis workflows.
An example of cassis in action is the spacy recommender for INCEpTION, which wraps the spacy NLP library as a web service which can be used in conjunction with the INCEpTION text annotation platform to automatically generate annotation suggestions.
Features
Currently supported features are:
Text SofAs
Deserializing/serializing UIMA CAS from/to XMI
Deserializing/serializing UIMA CAS from/to JSON
Deserializing/serializing type systems from/to XML
Selecting annotations, selecting covered annotations, adding annotations
Type inheritance
Multiple SofA support
Type system can be changed after loading
Primitive and reference features and arrays of primitives and references
Some features are still under development, e.g.
Proper type checking
XML/XMI schema validation
Installation
To install the package with pip
, just run
pip install dkpro-cassis
Usage
Example CAS XMI and types system files can be found under tests\test_files
.
Reading a CAS file
From XMI: A CAS can be deserialized from the UIMA CAS XMI (XML 1.0) format either
by reading from a file or string using load_cas_from_xmi
.
from cassis import *
with open('typesystem.xml', 'rb') as f:
typesystem = load_typesystem(f)
with open('cas.xmi', 'rb') as f:
cas = load_cas_from_xmi(f, typesystem=typesystem)
From JSON: The UIMA JSON CAS format is also supported and can be loaded using load_cas_from_json
.
Most UIMA JSON CAS files come with an embedded typesystem, so it is not necessary to specify one.
from cassis import *
with open('cas.json', 'rb') as f:
cas = load_cas_from_json(f)
Writing a CAS file
To XMI: A CAS can be serialized to XMI either by writing to a file or be
returned as a string using cas.to_xmi()
.
from cassis import *
# Returned as a string
xmi = cas.to_xmi()
# Written to file
cas.to_xmi("my_cas.xmi")
To JSON: A CAS can also be written to JSON using cas.to_json()
.
from cassis import *
# Returned as a string
xmi = cas.to_json()
# Written to file
cas.to_json("my_cas.json")
Creating a CAS
A CAS (Common Analysis System) object typically represents a (text) document. When using cassis, you will likely most often reading existing CAS files, modify them and then writing them out again. But you can also create CAS objects from scratch, e.g. if you want to convert some data into a CAS object in order to create a pre-annotated text. If you do not have a pre-defined typesystem to work with, you will have to define one.
typesystem = TypeSystem()
cas = Cas(
sofa_string = "Joe waited for the train . The train was late .",
document_language = "en",
typesystem = typesystem)
print(cas.sofa_string)
print(cas.sofa_mime)
print(cas.document_language)
Adding annotations
Note: type names used below are examples only. The actual CAS files you will be
dealing with will use other names! You can get a list of the types using
cas.typesystem.get_types()
.
Given a type system with a type cassis.Token
that has an id
and
pos
feature, annotations can be added in the following:
from cassis import *
with open('typesystem.xml', 'rb') as f:
typesystem = load_typesystem(f)
with open('cas.xmi', 'rb') as f:
cas = load_cas_from_xmi(f, typesystem=typesystem)
Token = typesystem.get_type('cassis.Token')
tokens = [
Token(begin=0, end=3, id='0', pos='NNP'),
Token(begin=4, end=10, id='1', pos='VBD'),
Token(begin=11, end=14, id='2', pos='IN'),
Token(begin=15, end=18, id='3', pos='DT'),
Token(begin=19, end=24, id='4', pos='NN'),
Token(begin=25, end=26, id='5', pos='.'),
]
for token in tokens:
cas.add(token)
Selecting annotations
from cassis import *
with open('typesystem.xml', 'rb') as f:
typesystem = load_typesystem(f)
with open('cas.xmi', 'rb') as f:
cas = load_cas_from_xmi(f, typesystem=typesystem)
for sentence in cas.select('cassis.Sentence'):
for token in cas.select_covered('cassis.Token', sentence):
print(token.get_covered_text())
# Annotation values can be accessed as properties
print('Token: begin={0}, end={1}, id={2}, pos={3}'.format(token.begin, token.end, token.id, token.pos))
Getting and setting (nested) features
If you want to access a variable but only have its name as a string or have nested feature structures,
e.g. a feature structure with feature a
that has a
feature b
that has a feature c
, some of which can be None
, then you can use the
following:
fs.get("var_name") # Or
fs["var_name"]
Or in the nested case,
fs.get("a.b.c")
fs["a.b.c"]
If a
or b
or c
are None
, then this returns instead of
throwing an error.
Another example would be a StringList containing ["Foo", "Bar", "Baz"]
:
assert lst.get("head") == "foo"
assert lst.get("tail.head") == "bar"
assert lst.get("tail.tail.head") == "baz"
assert lst.get("tail.tail.tail.head") == None
assert lst.get("tail.tail.tail.tail.head") == None
The same goes for setting:
# Functional
lst.set("head", "new_foo")
lst.set("tail.head", "new_bar")
lst.set("tail.tail.head", "new_baz")
assert lst.get("head") == "new_foo"
assert lst.get("tail.head") == "new_bar"
assert lst.get("tail.tail.head") == "new_baz"
# Bracket access
lst["head"] = "newer_foo"
lst["tail.head"] = "newer_bar"
lst["tail.tail.head"] = "newer_baz"
assert lst["head"] == "newer_foo"
assert lst["tail.head"] == "newer_bar"
assert lst["tail.tail.head"] == "newer_baz"
Creating types and adding features
from cassis import *
typesystem = TypeSystem()
parent_type = typesystem.create_type(name='example.ParentType')
typesystem.create_feature(domainType=parent_type, name='parentFeature', rangeType=TYPE_NAME_STRING)
child_type = typesystem.create_type(name='example.ChildType', supertypeName=parent_type.name)
typesystem.create_feature(domainType=child_type, name='childFeature', rangeType=TYPE_NAME_INTEGER)
annotation = child_type(parentFeature='parent', childFeature='child')
When adding new features, these changes are propagated. For example, adding a feature to a parent type makes it available to a child type. Therefore, the type system does not need to be frozen for consistency. The type system can be changed even after loading, it is not frozen like in UIMAj.
Sofa support
A Sofa represents some form of an unstructured artifact that is processed in a UIMA pipeline. It contains for instance the document text. Currently, new Sofas can be created. This is automatically done when creating a new view. Basic properties of the Sofa can be read and written:
cas = Cas(
sofa_string = "Joe waited for the train . The train was late .",
document_language = "en")
print(cas.sofa_string)
print(cas.sofa_mime)
print(cas.document_language)
Array support
Array feature values are not simply Python arrays, but they are wrapped in a feature structure of
a UIMA array type such as uima.cas.FSArray
.
from cassis import *
from cassis.typesystem import TYPE_NAME_FS_ARRAY, TYPE_NAME_ANNOTATION
typesystem = TypeSystem()
ArrayHolder = typesystem.create_type(name='example.ArrayHolder')
typesystem.create_feature(domainType=ArrayHolder, name='array', rangeType=TYPE_NAME_FS_ARRAY)
cas = Cas(typesystem=typesystem)
Annotation = cas.typesystem.get_type(TYPE_NAME_ANNOTATION)
FSArray = cas.typesystem.get_type(TYPE_NAME_FS_ARRAY)
ann = Annotation(begin=0, end=1)
cas.add(ann1)
holder = ArrayHolder(array=FSArray(elements=[ann, ann, ann]))
cas.add(holder)
Managing views
A view into a CAS contains a subset of feature structures and annotations. One view corresponds to exactly one Sofa. It
can also be used to query and alter information about the Sofa, e.g. the document text. Annotations added to one view
are not visible in another view. A view Views can be created and changed. A view has the same methods and attributes
as a Cas
.
from cassis import *
with open('typesystem.xml', 'rb') as f:
typesystem = load_typesystem(f)
Token = typesystem.get_type('cassis.Token')
# This creates automatically the view `_InitialView`
cas = Cas()
cas.sofa_string = "I like cheese ."
cas.add_all([
Token(begin=0, end=1),
Token(begin=2, end=6),
Token(begin=7, end=13),
Token(begin=14, end=15)
])
print([x.get_covered_text() for x in cas.select_all()])
# Create a new view and work on it.
view = cas.create_view('testView')
view.sofa_string = "I like blackcurrant ."
view.add_all([
Token(begin=0, end=1),
Token(begin=2, end=6),
Token(begin=7, end=19),
Token(begin=20, end=21)
])
print([x.get_covered_text() for x in view.select_all()])
Merging type systems
Sometimes, it is desirable to merge two type systems. With cassis, this can be
achieved via the merge_typesystems
function. The detailed rules of merging can be found
here.
from cassis import *
with open('typesystem.xml', 'rb') as f:
typesystem = load_typesystem(f)
ts = merge_typesystems([typesystem, load_dkpro_core_typesystem()])
Type checking
When adding annotations, no type checking is performed for simplicity reasons.
In order to check types, call the cas.typecheck()
method. Currently, it only
checks whether elements in uima.cas.FSArray are
adhere to the specified elementType
.
DKPro Core Integration
A CAS using the DKPro Core Type System can be created via
from cassis import *
cas = Cas(typesystem=load_dkpro_core_typesystem())
for t in cas.typesystem.get_types():
print(t)
Miscellaneous
If feature names clash with Python magic variables
If your type system defines a type called self
or type
, then it will be made
available as a member variable self_
or type_
on the respective type:
from cassis import *
from cassis.typesystem import *
typesystem = TypeSystem()
ExampleType = typesystem.create_type(name='example.Type')
typesystem.create_feature(domainType=ExampleType, name='self', rangeType=TYPE_NAME_STRING)
typesystem.create_feature(domainType=ExampleType, name='type', rangeType=TYPE_NAME_STRING)
annotation = ExampleType(self_="Test string1", type_="Test string2")
print(annotation.self_)
print(annotation.type_)
Leniency
If the type for a feature structure is not found in the typesystem, it will raise an exception by default.
If you want to ignore these kind of errors, you can pass lenient=True
to the Cas
constructor or
to load_cas_from_xmi
.
Large XMI files
If you try to parse large XMI files and get an error message like XMLSyntaxError: internal error: Huge input lookup
,
then you can disable this security check by passing trusted=True
to your calls to load_cas_from_xmi
.
Development
The required dependencies are managed by pip. A virtual environment containing all needed packages for development and production can be created and activated by
virtualenv venv --python=python3 --no-site-packages source venv/bin/activate pip install -e ".[test, dev, doc]"
The tests can be run in the current environment by invoking
make test
or in a clean environment via
tox
Release
Make sure all issues for the milestone are completed, otherwise move them to the next
Checkout the main branch
Bump the version in cassis/__version__.py to a stable one, e.g. __version__ = "0.6.0", commit and push, wait until the build completed. An example commit message would be No issue. Release 0.6.0
Create a tag for that version via e.g. git tag v0.6.0 and push the tags via git push --tags. Pushing a tag triggers the release to pypi
Bump the version in cassis/__version__.py to the next development version, e.g. 0.7.0-dev, commit and push that. An example commit message would be No issue. Bump version after release
Once the build has completed and pypi accepted the new version, go to the Github release and write the changelog based on the issues in the respective milestone
Create a new milestone for the next version
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file dkpro-cassis-0.9.1.tar.gz
.
File metadata
- Download URL: dkpro-cassis-0.9.1.tar.gz
- Upload date:
- Size: 77.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.11.8
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4ec1e8ac2fa84522e9657587a36b9cc68b5f0609703c7f3196133f462e41bf84 |
|
MD5 | 2a69b535a00b71874c69291e81d01a21 |
|
BLAKE2b-256 | 989d43129ff1e33821ccd450746918e55b1dae9066b0d555d947cb457abfd2fb |