Frame Semantic Parser based on T5 and FrameNet
Project description
Frame Semantic Transformer
Frame-based semantic parsing library trained on FrameNet and built on HuggingFace's T5 Transformer
Live Demo: chanind.github.io/frame-semantic-transformer
About
This library draws heavily on Open-Sesame (paper) for inspiration on training and evaluation on FrameNet 1.7, and uses ideas from the paper Open-Domain Frame Semantic Parsing Using Transformers for using T5 as a frame-semantic parser. SimpleT5 was also used as a base for the initial training setup.
More details: FrameNet Parsing with Transformers Blog Post
Performance
This library uses the same train/dev/test documents and evaluation methodology as Open-Sesame, so that the results should be comparable between the 2 libraries. There are 2 pretrained models available, base
and small
, corresponding to t5-base
and t5-small
in Huggingface, respectively.
Task | Sesame F1 (dev/test) | Small Model F1 (dev/test) | Base Model F1 (dev/test) |
---|---|---|---|
Trigger identification | 0.80 / 0.73 | 0.74 / 0.70 | 0.78 / 0.71 |
Frame classification | 0.90 / 0.87 | 0.83 / 0.81 | 0.89 / 0.87 |
Argument extraction | 0.61 / 0.61 | 0.68 / 0.70 | 0.74 / 0.72 |
The base model performs similarly to Open-Sesame on trigger identification and frame classification tasks, but outperforms it by a significant margin on argument extraction. The small pretrained model has lower F1 than base across the board, but is 1/4 the size and still outperforms Open-Sesame at argument extraction.
Installation
pip install frame-semantic-transformer
Usage
Inference
The main entry to interacting with the library is the FrameSemanticTransformer
class, as shown below. For inference the detect_frames()
method is likely all that is needed to perform frame parsing.
from frame_semantic_transformer import FrameSemanticTransformer
frame_transformer = FrameSemanticTransformer()
result = frame_transformer.detect_frames("The hallway smelt of boiled cabbage and old rag mats.")
print(f"Results found in: {result.sentence}")
for frame in result.frames:
print(f"FRAME: {frame.name}")
for element in frame.frame_elements:
print(f"{element.name}: {element.text}")
The result returned from detect_frames()
is an object containing sentence
, a parsed version of the original sentence text, trigger_locations
, the indices within the sentence where frame triggers were detected, and frames
, a list of all detected frames in the sentence. Within frames
, each object containes name
which corresponds to the FrameNet name of the frame, trigger_location
corresponding to which trigger in the text this frame this frame uses, and frame_elements
containing a list of all relevant frame elements found in the text.
Loading Models
There are currently 2 available pre-trained models for inference, called base
and small
, fine-tuned from HuggingFace's t5-base and t5-small model respectively. If a local fine-tuned t5 model exists that can be loaded as well. If no model is specified, the base
model will be used.
base_transformer = FrameSemanticTransformer("base") # this is also the default
small_transformer = FrameSemanticTransformer("small") # a smaller pretrained model which is faster to run
custom_transformer = FrameSemanticTransformer("/path/to/model") # load a custom t5 model
By default, models are lazily loaded when detect_frames()
is first called. If you want to load the model sooner, you can call setup()
on a FrameSemanticTransformer
instance to load models immediately.
frame_transformer = FrameSemanticTransformer()
frame_transformer.setup() # load models immediately
Training
If you want to train a new model on the Framenet 1.7 dataset yourself, you can run the training script like below:
python -m frame_semantic_transformer.train \
--base-model t5-base \
--use-gpu \
--batch-size 8 \
--epochs 10 \
--learning-rate 5e-5 \
--output-dir ./outputs
Training uses Pytorch Lightning behind the scenes, and will place tensorboard logs into ./lightning_logs
as it trains.
If you need more control, you can also directly import the train()
method from frame_semantic_transformer.train
and run training directly in code.
Training on different datasets
By default FrameSemanticTransformer assumes you want to train on the framenet 1.7 dataset, and will download this dataset during training and inference. If you'd like to train on a different dataset, for example a different language version of framenet, or a custom frame dataset, you'll need to provide custom data loaders to load the data for your frames. Specifically, this requires extending an instance of TrainingLoader
to load your training data, and InstanceLoader
to load all the frames and lexical units from your custom dataset.
These loaders have the following signatures:
class InferenceLoader(ABC):
def setup(self) -> None:
"""
Perform any setup required, e.g. downloading needed data
"""
pass
@abstractmethod
def load_frames(self) -> list[Frame]:
"""
Load the full list of frames to be used during inference
"""
pass
@abstractmethod
def normalize_lexical_unit_text(self, lu: str) -> str:
"""
Normalize a lexical unit like "takes.v" to "take".
"""
pass
def prioritize_lexical_unit(self, lu: str) -> bool:
"""
Check if the lexical unit is relatively rare, so that it should be considered "high information"
"""
return len(self.normalize_lexical_unit_text(lu)) >= 6
class TrainingLoader(ABC):
def setup(self) -> None:
"""
Perform any setup required, e.g. downloading needed data.
"""
pass
@abstractmethod
def get_augmentations(self) -> list[DataAugmentation]:
"""
Get a list of augmentations to apply to the training data
"""
pass
@abstractmethod
def load_training_data(self) -> list[FrameAnnotatedSentence]:
"""
Load the training data
"""
pass
@abstractmethod
def load_validation_data(self) -> list[FrameAnnotatedSentence]:
"""
Load the validation data
"""
pass
@abstractmethod
def load_test_data(self) -> list[FrameAnnotatedSentence]:
"""
Load the test data
"""
pass
The most difficult part of this is returning instances of Frame
for the load_frames
method of InstanceLoader
, and FrameAnnotatedSentence
from the TrainingLoader
. These are simple Python dataclasses with the following signatures:
@dataclass
class Frame:
"""
Representation of a FrameNet frame
For training on your own data, you can use this class to represent your own frames
"""
name: str
core_elements: list[str]
non_core_elements: list[str]
lexical_units: list[str]
@dataclass
class FrameAnnotatedSentence:
"""
Representation of a sentence with annotations for use in training
If training on your own data, you'll need to create instances of this class for your training sentences
"""
text: str
annotations: list[FrameAnnotation]
@dataclass
class FrameAnnotation:
"""
A single frame occuring in a sentence
"""
frame: str
trigger_locs: list[int]
frame_elements: list[FrameElementAnnotation]
@dataclass
class FrameElementAnnotation:
"""
A single frame element in a frame annotation.
Includes the name of the frame element and the start and end locations of the frame element in the sentence
"""
name: str
start_loc: int
end_loc: int
Hopefully the meaning of the fields in the Frame
dataclass should be obvious when looking at a sample FrameNet Frame.
The FrameAnnotatedSentence
class is a bit trickier, as this represents an annotated training sample. The text
field should be a single sentence, and all start_loc
, end_loc
, and trigger_locs
are indices which refer to positions in the text.
FrameAnnotation
refers to a single frame inside of the sentence. There may be multiple frames in a sentence, which is why the annotations
field on FrameAnnotatedSentence
is a list of FrameAnnotation
s. The trigger_locs
field in FrameAnnotation
is just the start locations of any triggers in the sentence for the frame. End locations of triggers are not used currently by FrameSemanticTransformer as it makes the labeling more complicated. There is an implicit assumptions here, which is that a single location in a sentence can only be a trigger for 1 frame.
FrameElement
refers to the location of a frame element in the sentence for the frame being annotated. Frame elements do require both start and end locations in the sentence.
For instance, for the sentence "It was no use trying the lift", we have 2 frames "Attempt_means" at index 14 (the word "trying"), and "Connecting_architecture" at index 25 (the word "lift"). "Attempt_means" has a single frame element "Means" with text "the lift" (index 21 - 29), and "Connecting_architecture" likewise also has a single frame element "Part" with text "lift" (index 25 - 29). This would look like the following when turned into a FrameAnnotatedSentence
instance:
annotated_sentence = FrameAnnotatedSentence(
text="It was no use trying the lift",
annotations=[
FrameAnnotation(
frame="Attempt_means",
trigger_locs=[14],
frame_elements=[
FrameElementAnnotation(
name="Means",
start_loc=21,
end_loc=29,
)
]
),
FrameAnnotation(
frame="Connecting_architecture",
trigger_locs=[25],
frame_elements=[
FrameElementAnnotation(
name="Part",
start_loc=25,
end_loc=29,
)
]
)
]
)
After creating custom TrainingLoader
and InferenceLoader
classes, you'll need to pass these classes in when training a new model and when running inference after training. An example of this is shown below:
from frame_semantic_transformer import TrainingLoader, InferenceLoader, FrameSemanticTransformer
from frame_semantic_transformer.training import train
class MyCustomInferenceLoader(InferenceLoader):
...
class MyCustomTrainingLoader(TrainingLoader):
...
my_inference_loader = MyCustomInferenceLoader()
my_training_loader = MyCustomTrainingLoader()
my_model, my_tokenizer = train(
base_model=f"t5-small",
batch_size=32,
max_epochs=16,
lr=5e-5,
inference_loader=my_inference_loader,
training_loader=my_training_loader,
)
my_model.save_pretrained('./my_model')
my_tokenizer.save_pretrained('./my_model')
# after training...
frame_transformer = FrameSemanticTransformer('./my_model', inference_loader=my_inference_loader)
frame_transformer.detect_frames(...)
You can see examples of how these classes are implemented for the default framenet 1.7 by looking at Framenet17InferenceLoader.py and Framenet17TrainingLoader.py. There's also an example of creating custom loaders for Swedish in the following Colab notebook:
If you have trouble creating and using custom loader classes please don't hesitate to open an issue!
Contributing
Any contributions to improve this project are welcome! Please open an issue or pull request in this repo with any bugfixes / changes / improvements you have!
This project uses Black for code formatting, Flake8 for linting, and Pytest for tests. Make sure any changes you submit pass these code checks in your PR. If you have trouble getting these to run feel free to open a pull-request regardless and we can discuss further in the PR.
License
The code contained in this repo is released under a MIT license, however the pretrained models are released under an Apache 2.0 license in accordance with FrameNet training data and HuggingFace's T5 base models.
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
Built Distribution
File details
Details for the file frame_semantic_transformer-0.5.0.tar.gz
.
File metadata
- Download URL: frame_semantic_transformer-0.5.0.tar.gz
- Upload date:
- Size: 28.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.9.6 readme-renderer/37.3 requests/2.28.2 requests-toolbelt/0.10.1 urllib3/1.26.14 tqdm/4.64.1 importlib-metadata/6.0.0 keyring/23.13.1 rfc3986/2.0.0 colorama/0.4.6 CPython/3.10.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | aa36bf00521716108eeceb34e83defa668a8bd7996c86c3a2e9df59a4ecb890a |
|
MD5 | 1007521cb52222266fb28e1a39c9db79 |
|
BLAKE2b-256 | 79bff16102309850f664578c4e063f0ce0fe6e290c3e5dcf794d18717ac0c2f4 |
File details
Details for the file frame_semantic_transformer-0.5.0-py3-none-any.whl
.
File metadata
- Download URL: frame_semantic_transformer-0.5.0-py3-none-any.whl
- Upload date:
- Size: 38.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.9.6 readme-renderer/37.3 requests/2.28.2 requests-toolbelt/0.10.1 urllib3/1.26.14 tqdm/4.64.1 importlib-metadata/6.0.0 keyring/23.13.1 rfc3986/2.0.0 colorama/0.4.6 CPython/3.10.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 5397ab189c41d96e2f85786ed93fa077dc5c9b3eb7bcaffb75c3b8735e09087f |
|
MD5 | 1e137cee4db94a6e28222c18b83a4448 |
|
BLAKE2b-256 | 5fcad1585f5f402b19ebddae99ca5b60f25875e1d5f5ee75af660bf20992cc1b |