97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
332 lines
10 KiB
Markdown
332 lines
10 KiB
Markdown
---
|
|
title: Text Classification with OpenAI and Pydantic
|
|
description: Learn to implement single-label and multi-label text classification using OpenAI API and Pydantic models in Python.
|
|
---
|
|
|
|
# Text Classification using OpenAI and Pydantic
|
|
|
|
This tutorial showcases how to implement text classification tasks-specifically, single-label and multi-label classifications-using the OpenAI API and Pydantic models. For complete examples, check out our [single classification](./bulk_classification.md) and [multi-label classification](./bulk_classification.md) examples in the cookbook.
|
|
|
|
!!! tips "Motivation"
|
|
|
|
Text classification is a common problem in many NLP applications, such as spam detection or support ticket categorization. The goal is to provide a systematic way to handle these cases using OpenAI's GPT models in combination with Python data structures.
|
|
|
|
## Single-Label Classification
|
|
|
|
### Defining the Structures
|
|
|
|
For single-label classification, we define a Pydantic model with a [Literal](../concepts/prompting.md#literals) field for the possible labels.
|
|
|
|
!!! note "Literals vs Enums"
|
|
|
|
We prefer using `Literal` types over `enum` for classification labels. Literals provide better type checking and are more straightforward to use with Pydantic models.
|
|
|
|
!!! important "Few-Shot Examples"
|
|
|
|
Including few-shot examples in the model's docstring is crucial for improving the model's classification accuracy. These examples guide the AI in understanding the task and expected outputs.
|
|
|
|
If you want to learn more prompting tips check out our [prompting guide](../prompting/index.md)
|
|
|
|
!!! note "Chain of Thought"
|
|
|
|
Using [Chain of Thought](../concepts/prompting.md#chain-of-thought) has been shown to improve the quality of the predictions by ~ 10%
|
|
|
|
```python
|
|
from pydantic import BaseModel, Field
|
|
from typing import Literal
|
|
import instructor
|
|
|
|
# Apply the patch to the OpenAI client
|
|
# enables response_model keyword
|
|
client = instructor.from_provider("openai/gpt-5-nano")
|
|
|
|
|
|
class ClassificationResponse(BaseModel):
|
|
"""
|
|
A few-shot example of text classification:
|
|
|
|
Examples:
|
|
- "Buy cheap watches now!": SPAM
|
|
- "Meeting at 3 PM in the conference room": NOT_SPAM
|
|
- "You've won a free iPhone! Click here": SPAM
|
|
- "Can you pick up some milk on your way home?": NOT_SPAM
|
|
- "Increase your followers by 10000 overnight!": SPAM
|
|
"""
|
|
|
|
chain_of_thought: str = Field(
|
|
...,
|
|
description="The chain of thought that led to the prediction.",
|
|
)
|
|
label: Literal["SPAM", "NOT_SPAM"] = Field(
|
|
...,
|
|
description="The predicted class label.",
|
|
)
|
|
```
|
|
|
|
### Classifying Text
|
|
|
|
The function **`classify`** will perform the single-label classification.
|
|
|
|
```python
|
|
# <%hide%>
|
|
from pydantic import BaseModel, Field
|
|
from typing import Literal
|
|
import instructor
|
|
|
|
|
|
class ClassificationResponse(BaseModel):
|
|
"""
|
|
A few-shot example of text classification:
|
|
|
|
Examples:
|
|
- "Buy cheap watches now!": SPAM
|
|
- "Meeting at 3 PM in the conference room": NOT_SPAM
|
|
- "You've won a free iPhone! Click here": SPAM
|
|
- "Can you pick up some milk on your way home?": NOT_SPAM
|
|
- "Increase your followers by 10000 overnight!": SPAM
|
|
"""
|
|
|
|
chain_of_thought: str = Field(
|
|
...,
|
|
description="The chain of thought that led to the prediction.",
|
|
)
|
|
label: Literal["SPAM", "NOT_SPAM"] = Field(
|
|
...,
|
|
description="The predicted class label.",
|
|
)
|
|
|
|
|
|
# Apply the patch to the OpenAI client
|
|
# enables response_model keyword
|
|
client = instructor.from_provider("openai/gpt-5-nano")
|
|
|
|
|
|
# <%hide%>
|
|
def classify(data: str) -> ClassificationResponse:
|
|
"""Perform single-label classification on the input text."""
|
|
return client.create(
|
|
model="gpt-4o-mini",
|
|
response_model=ClassificationResponse,
|
|
messages=[
|
|
{
|
|
"role": "user",
|
|
"content": f"Classify the following text: <text>{data}</text>",
|
|
},
|
|
],
|
|
)
|
|
```
|
|
|
|
### Testing and Evaluation
|
|
|
|
Let's run examples to see if it correctly identifies spam and non-spam messages.
|
|
|
|
```python
|
|
# <%hide%>
|
|
from pydantic import BaseModel, Field
|
|
from typing import Literal
|
|
import instructor
|
|
|
|
client = instructor.from_provider("openai/gpt-5-nano")
|
|
|
|
|
|
class ClassificationResponse(BaseModel):
|
|
"""
|
|
A few-shot example of text classification:
|
|
|
|
Examples:
|
|
- "Buy cheap watches now!": SPAM
|
|
- "Meeting at 3 PM in the conference room": NOT_SPAM
|
|
- "You've won a free iPhone! Click here": SPAM
|
|
- "Can you pick up some milk on your way home?": NOT_SPAM
|
|
- "Increase your followers by 10000 overnight!": SPAM
|
|
"""
|
|
|
|
chain_of_thought: str = Field(
|
|
...,
|
|
description="The chain of thought that led to the prediction.",
|
|
)
|
|
label: Literal["SPAM", "NOT_SPAM"] = Field(
|
|
...,
|
|
description="The predicted class label.",
|
|
)
|
|
|
|
|
|
def classify(data: str) -> ClassificationResponse:
|
|
"""Perform single-label classification on the input text."""
|
|
return client.create(
|
|
model="gpt-4o-mini",
|
|
response_model=ClassificationResponse,
|
|
messages=[
|
|
{
|
|
"role": "user",
|
|
"content": f"Classify the following text: <text>{data}</text>",
|
|
},
|
|
],
|
|
)
|
|
|
|
|
|
# <%hide%>
|
|
if __name__ == "__main__":
|
|
for text, label in [
|
|
("Hey Jason! You're awesome", "NOT_SPAM"),
|
|
("I am a nigerian prince and I need your help.", "SPAM"),
|
|
]:
|
|
prediction = classify(text)
|
|
assert prediction.label == label
|
|
print(f"Text: {text}, Predicted Label: {prediction.label}")
|
|
#> Text: Hey Jason! You're awesome, Predicted Label: NOT_SPAM
|
|
#> Text: I am a nigerian prince and I need your help., Predicted Label: SPAM
|
|
```
|
|
|
|
## Multi-Label Classification
|
|
|
|
### Defining the Structures
|
|
|
|
For multi-label classification, we'll update our approach to use Literals instead of enums, and include few-shot examples in the model's docstring.
|
|
|
|
```python
|
|
from typing import List
|
|
from pydantic import BaseModel, Field
|
|
from typing import Literal
|
|
|
|
|
|
class MultiClassPrediction(BaseModel):
|
|
"""
|
|
Class for a multi-class label prediction.
|
|
|
|
Examples:
|
|
- "My account is locked": ["TECH_ISSUE"]
|
|
- "I can't access my billing info": ["TECH_ISSUE", "BILLING"]
|
|
- "When do you close for holidays?": ["GENERAL_QUERY"]
|
|
- "My payment didn't go through and now I can't log in": ["BILLING", "TECH_ISSUE"]
|
|
"""
|
|
|
|
chain_of_thought: str = Field(
|
|
...,
|
|
description="The chain of thought that led to the prediction.",
|
|
)
|
|
|
|
class_labels: List[Literal["TECH_ISSUE", "BILLING", "GENERAL_QUERY"]] = Field(
|
|
...,
|
|
description="The predicted class labels for the support ticket.",
|
|
)
|
|
```
|
|
|
|
### Classifying Text
|
|
|
|
The function **`multi_classify`** is responsible for multi-label classification.
|
|
|
|
```python
|
|
# <%hide%>
|
|
from typing import List
|
|
from pydantic import BaseModel, Field
|
|
from typing import Literal
|
|
|
|
|
|
class MultiClassPrediction(BaseModel):
|
|
"""
|
|
Class for a multi-class label prediction.
|
|
|
|
Examples:
|
|
- "My account is locked": ["TECH_ISSUE"]
|
|
- "I can't access my billing info": ["TECH_ISSUE", "BILLING"]
|
|
- "When do you close for holidays?": ["GENERAL_QUERY"]
|
|
- "My payment didn't go through and now I can't log in": ["BILLING", "TECH_ISSUE"]
|
|
"""
|
|
|
|
chain_of_thought: str = Field(
|
|
...,
|
|
description="The chain of thought that led to the prediction.",
|
|
)
|
|
|
|
class_labels: List[Literal["TECH_ISSUE", "BILLING", "GENERAL_QUERY"]] = Field(
|
|
...,
|
|
description="The predicted class labels for the support ticket.",
|
|
)
|
|
|
|
|
|
# <%hide%>
|
|
import instructor
|
|
|
|
client = instructor.from_provider("openai/gpt-5-nano")
|
|
|
|
|
|
def multi_classify(data: str) -> MultiClassPrediction:
|
|
"""Perform multi-label classification on the input text."""
|
|
return client.create(
|
|
model="gpt-4o-mini",
|
|
response_model=MultiClassPrediction,
|
|
messages=[
|
|
{
|
|
"role": "user",
|
|
"content": f"Classify the following support ticket: <ticket>{data}</ticket>",
|
|
},
|
|
],
|
|
)
|
|
```
|
|
|
|
### Testing and Evaluation
|
|
|
|
Finally, we test the multi-label classification function using a sample support ticket.
|
|
|
|
```python
|
|
# <%hide%>
|
|
from typing import List
|
|
from pydantic import BaseModel, Field
|
|
from typing import Literal
|
|
import instructor
|
|
|
|
|
|
class MultiClassPrediction(BaseModel):
|
|
"""
|
|
Class for a multi-class label prediction.
|
|
|
|
Examples:
|
|
- "My account is locked": ["TECH_ISSUE"]
|
|
- "I can't access my billing info": ["TECH_ISSUE", "BILLING"]
|
|
- "When do you close for holidays?": ["GENERAL_QUERY"]
|
|
- "My payment didn't go through and now I can't log in": ["BILLING", "TECH_ISSUE"]
|
|
"""
|
|
|
|
chain_of_thought: str = Field(
|
|
...,
|
|
description="The chain of thought that led to the prediction.",
|
|
)
|
|
|
|
class_labels: List[Literal["TECH_ISSUE", "BILLING", "GENERAL_QUERY"]] = Field(
|
|
...,
|
|
description="The predicted class labels for the support ticket.",
|
|
)
|
|
|
|
|
|
client = instructor.from_provider("openai/gpt-5-nano")
|
|
|
|
|
|
def multi_classify(data: str) -> MultiClassPrediction:
|
|
"""Perform multi-label classification on the input text."""
|
|
return client.create(
|
|
model="gpt-4o-mini",
|
|
response_model=MultiClassPrediction,
|
|
messages=[
|
|
{
|
|
"role": "user",
|
|
"content": f"Classify the following support ticket: <ticket>{data}</ticket>",
|
|
},
|
|
],
|
|
)
|
|
|
|
|
|
# <%hide%>
|
|
# Test multi-label classification
|
|
ticket = "My account is locked and I can't access my billing info."
|
|
prediction = multi_classify(ticket)
|
|
assert "TECH_ISSUE" in prediction.class_labels
|
|
assert "BILLING" in prediction.class_labels
|
|
print(f"Ticket: {ticket}")
|
|
#> Ticket: My account is locked and I can't access my billing info.
|
|
print(f"Predicted Labels: {prediction.class_labels}")
|
|
#> Predicted Labels: ['TECH_ISSUE', 'BILLING']
|
|
```
|
|
|
|
By using Literals and including few-shot examples, we've improved both the single-label and multi-label classification implementations. These changes enhance type safety and provide better guidance for the AI model, potentially leading to more accurate classifications.
|