Introduzione di un Pythonista al kernel semantico |  di Chris Hughes |  Settembre 2023

 | Intelligenza-Artificiale

Possiamo vedere che questo equivale al nostro approccio manuale.

Creazione di plugin personalizzati

Ora che sappiamo come creare funzioni semantiche e come utilizzare i plugin, abbiamo tutto ciò di cui abbiamo bisogno per iniziare a creare i nostri plugin!

I plugin possono contenere due tipi di funzioni:

  • Funzioni semantiche: utilizzare il linguaggio naturale per eseguire azioni
  • Funzioni native: usa il codice Python per eseguire azioni

che possono essere combinati all’interno di un unico plugin.

La scelta se utilizzare una funzione semantica o nativa dipende dall’attività che stai eseguendo. Per compiti che coinvolgono la comprensione o la generazione del linguaggio, le funzioni semantiche sono la scelta ovvia. Tuttavia, per compiti più deterministici, come eseguire operazioni matematiche, scaricare dati o accedere all’ora, le funzioni native sono più adatte.

Esploriamo come possiamo creare ciascun tipo. Innanzitutto, creiamo una cartella per archiviare i nostri plugin.

from pathlib import Path

plugins_path = Path("Plugins")
plugins_path.mkdir(exist_ok=True)

Creazione di un plugin per il generatore di poesie

Per il nostro esempio, creiamo un plugin che generi poesie; per questo, utilizzare una funzione semantica sembra una scelta naturale. Possiamo creare una cartella per questo plugin nella nostra directory.

poem_gen_plugin_path = plugins_path / "PoemGeneratorPlugin"
poem_gen_plugin_path.mkdir(exist_ok=True)

Ricordando che i plugin sono solo una raccolta di funzioni e che stiamo creando una funzione semantica, la parte successiva dovrebbe risultare abbastanza familiare. La differenza fondamentale è che, invece di definire il nostro prompt e la configurazione in linea, creeremo file individuali per questi; per facilitare il caricamento.

Creiamo una cartella per la nostra funzione semantica, che chiameremo write_poem.

poem_sc_path = poem_gen_plugin_path / "write_poem"
poem_sc_path.mkdir(exist_ok=True)

Successivamente, creiamo il nostro prompt, salvandolo come skprompt.txt.

Ora creiamo la nostra configurazione e memorizziamola in un file json.

Anche se è sempre buona pratica impostare descrizioni significative nella nostra configurazione, questo diventa più importante quando definiamo i plugin; i plugin dovrebbero fornire descrizioni chiare che descrivano come si comportano, quali sono i loro input e output e quali sono i loro effetti collaterali. La ragione di ciò è che questa è l’interfaccia presentata dal nostro kernel e, se vogliamo essere in grado di utilizzare un LLM per orchestrare le attività, deve essere in grado di comprendere la funzionalità del plugin e come chiamarlo in modo che può selezionare le funzioni appropriate.

config_path = poem_sc_path / "config.json"
%%writefile {config_path}

{
"schema": 1,
"type": "completion",
"description": "A poem generator, that writes a short poem based on user input",
"default_services": ("azure_gpt35_chat_completion"),
"completion": {
"temperature": 0.0,
"top_p": 1,
"max_tokens": 250,
"number_of_responses": 1,
"presence_penalty": 0,
"frequency_penalty": 0
},
"input": {
"parameters": ({
"name": "input",
"description": "The topic that the poem should be written about",
"defaultValue": ""
})
}
}

Tieni presente che, poiché stiamo salvando la nostra configurazione come file JSON, dobbiamo rimuovere i commenti per rendere questo JSON valido.

Ora possiamo importare il nostro plugin:

poem_gen_plugin = kernel.import_semantic_skill_from_directory(
plugins_path, "PoemGeneratorPlugin"
)

Ispezionando il nostro plugin, possiamo vedere che espone our write_poem funzione semantica.

Possiamo chiamare direttamente la nostra funzione semantica:

result = poem_gen_plugin("write_poem")("Munich")

oppure possiamo usarlo in un’altra funzione semantica:

chat_config_dict = {
"schema": 1,
# The type of prompt
"type": "completion",
# A description of what the semantic function does
"description": "Wraps a plugin to write a poem",
# Specifies which model service(s) to use
"default_services": ("azure_gpt35_chat_completion"),
# The parameters that will be passed to the connector and model service
"completion": {
"temperature": 0.0,
"top_p": 1,
"max_tokens": 500,
"number_of_responses": 1,
"presence_penalty": 0,
"frequency_penalty": 0,
},
# Defines the variables that are used inside of the prompt
"input": {
"parameters": (
{
"name": "input",
"description": "The input given by the user",
"defaultValue": "",
},
)
},
}

prompt = """
{{PoemGeneratorPlugin.write_poem $input}}
"""

write_poem_wrapper = kernel.register_semantic_function(
skill_name="PoemWrapper",
function_name="poem_wrapper",
function_config=create_semantic_function_chat_config(
prompt, chat_config_dict, kernel
),
)

result = write_poem_wrapper("Munich")

Creazione di un plugin per la classificazione delle immagini

Ora che abbiamo visto come utilizzare una funzione semantica in un plugin, diamo un’occhiata a come possiamo utilizzare una funzione nativa.

Qui creiamo un plugin che accetta l’URL di un’immagine, quindi scarica e classifica l’immagine. Ancora una volta, creiamo una cartella per il nostro nuovo plugin.

image_classifier_plugin_path = plugins_path / "ImageClassifierPlugin"
image_classifier_plugin_path.mkdir(exist_ok=True)

download_image_sc_path = image_classifier_plugin_path / "download_image.py"
download_image_sc_path.mkdir(exist_ok=True)

Ora possiamo creare il nostro modulo Python. All’interno del modulo possiamo essere abbastanza flessibili. Qui abbiamo creato una classe con due metodi, il passaggio chiave è utilizzare il file sk_function decoratore per specificare quali metodi dovrebbero essere esposti come parte del plugin.

In questo esempio, la nostra funzione richiede solo un singolo input. Per le funzioni che richiedono più input, il file sk_function_context_parameter può essere utilizzato, come dimostrato nella documentazione.

import requests
from PIL import Image
import timm
from timm.data.imagenet_info import ImageNetInfo

from semantic_kernel.skill_definition import (
sk_function,
)
from semantic_kernel.orchestration.sk_context import SKContext

class ImageClassifierPlugin:
def __init__(self):
self.model = timm.create_model("convnext_tiny.in12k_ft_in1k", pretrained=True)
self.model.eval()
data_config = timm.data.resolve_model_data_config(self.model)
self.transforms = timm.data.create_transform(**data_config, is_training=False)
self.imagenet_info = ImageNetInfo()

@sk_function(
description="Takes a url as an input and classifies the image",
name="classify_image",
input_description="The url of the image to classify",
)
def classify_image(self, url: str) -> str:
image = self.download_image(url)
pred = self.model(self.transforms(image)(None))
return self.imagenet_info.index_to_description(pred.argmax())

def download_image(self, url):
return Image.open(requests.get(url, stream=True).raw).convert("RGB")

Per questo esempio, ho utilizzato l’eccellente Modelli di immagini Pytorch libreria per fornire il nostro classificatore. Per ulteriori informazioni su come funziona questa libreria, dai un’occhiata a questo post sul blog.

Ora possiamo semplicemente importare il nostro plugin come mostrato di seguito.

image_classifier = ImageClassifierPlugin()

classify_plugin = kernel.import_skill(image_classifier, skill_name="classify_image")

Ispezionando il nostro plugin, possiamo vedere che è esposta solo la nostra funzione decorata.

Possiamo verificare che il nostro plugin funzioni utilizzando un file immagine di un gatto da Pixabay.

url = "https://cdn.pixabay.com/photo/2016/02/10/16/37/cat-1192026_1280.jpg"
response = classify_plugin("classify_image")(url)

Chiamando manualmente la nostra funzione, possiamo vedere che la nostra immagine è stata classificata correttamente! Allo stesso modo di prima, potremmo anche fare riferimento a questa funzione direttamente da un prompt. Tuttavia, poiché lo abbiamo già dimostrato, proviamo qualcosa di leggermente diverso nella sezione seguente.

Concatenamento di più plugin

È anche possibile concatenare più plugin insieme utilizzando il kernel, come dimostrato di seguito.

context = kernel.create_new_context()
context("input") = url

answer = await kernel.run_async(
classify_plugin("classify_image"),
poem_gen_plugin("write_poem"),
input_context=context,
)

Possiamo vedere che, utilizzando entrambi i plugin in sequenza, abbiamo classificato l’immagine e scritto una poesia al riguardo!

A questo punto, abbiamo esplorato a fondo le funzioni semantiche, compreso come le funzioni possono essere raggruppate e utilizzate come parte di un plugin e abbiamo visto come possiamo concatenare manualmente i plugin. Ora esploriamo come possiamo creare e orchestrare flussi di lavoro utilizzando LLM. Per fare ciò, Semantic Kernel fornisce Pianificatore oggetti, che possono creare dinamicamente catene di funzioni per cercare di raggiungere un obiettivo.

Un pianificatore è una classe che prende un prompt dell’utente e un kernel e utilizza i servizi del kernel per creare un piano su come eseguire l’attività, utilizzando le funzioni e i plugin che sono stati resi disponibili al kernel. Poiché i plugin sono gli elementi costitutivi principali di questi piani, il pianificatore fa molto affidamento sulle descrizioni fornite; se plugin e funzioni non hanno descrizioni chiare, il pianificatore non sarà in grado di utilizzarli correttamente. Inoltre, poiché un pianificatore può combinare le funzioni in vari modi diversi, è importante assicurarsi di esporre solo le funzioni che siamo felici che il pianificatore utilizzi.

Poiché il pianificatore fa affidamento su un modello per generare un piano, possono essere introdotti errori; questi di solito si verificano quando il pianificatore non capisce correttamente come utilizzare la funzione. In questi casi, ho scoperto che fornire istruzioni esplicite – come descrivere gli input e gli output e indicare se gli input sono richiesti – nelle descrizioni può portare a risultati migliori. Inoltre, ho ottenuto risultati migliori utilizzando modelli ottimizzati per le istruzioni rispetto ai modelli base; i modelli di completamento del testo base tendono ad allucinare funzioni che non esistono o a creare più piani. Nonostante queste limitazioni, quando tutto funziona correttamente, i pianificatori possono essere incredibilmente potenti!

Esploriamo come possiamo farlo esaminando se possiamo creare un piano per scrivere una poesia su un’immagine, in base al suo URL; utilizzando i plugin che abbiamo creato in precedenza. Poiché abbiamo definito molte funzioni di cui non abbiamo più bisogno, creiamo un nuovo kernel, in modo da poter controllare quali funzioni sono esposte.

kernel = sk.Kernel()

Per creare il nostro piano, utilizziamo il nostro servizio di chat OpenAI.

kernel.add_chat_service(
service_id="azure_gpt35_chat_completion",
service=AzureChatCompletion(
OPENAI_DEPLOYMENT_NAME, OPENAI_ENDPOINT, OPENAI_API_KEY
),
)

Esaminando i nostri servizi registrati, possiamo vedere che il nostro servizio può essere utilizzato sia per il completamento del testo che per le attività di completamento della chat.

Ora importiamo i nostri plugin.

classify_plugin = kernel.import_skill(
ImageClassifierPlugin(), skill_name="classify_image"
)
poem_gen_plugin = kernel.import_semantic_skill_from_directory(
plugins_path, "PoemGeneratorPlugin"
)

Possiamo vedere a quali funzioni ha accesso il nostro kernel come dimostrato di seguito.

Ora importiamo il nostro oggetto pianificatore.

from semantic_kernel.planning.basic_planner import BasicPlanner

planner = BasicPlanner()

Per utilizzare il nostro pianificatore, tutto ciò di cui abbiamo bisogno è un prompt. Spesso dovremo modificarlo a seconda dei piani generati. Qui ho cercato di essere il più esplicito possibile riguardo all’input richiesto.

ask = f"""
I would like you to write poem about what is contained in this image with this url: {url}. This url should be used as input.

"""

Successivamente, possiamo utilizzare il nostro pianificatore per creare un piano su come risolverà l’attività.

plan = await planner.create_plan_async(ask, kernel)

Esaminando il nostro piano, possiamo vedere che il modello ha identificato correttamente gli input e le funzioni corrette da utilizzare!

Infine, tutto ciò che resta da fare è eseguire il nostro piano.

poem = await planner.execute_plan_async(plan, kernel)

Wow, ha funzionato! Per un modello addestrato a prevedere la parola successiva, questo è piuttosto potente!

Come avvertimento, sono stato abbastanza fortunato nel fare questo esempio che il piano generato ha funzionato la prima volta. Tuttavia, eseguendo l’operazione più volte con lo stesso prompt, possiamo vedere che non è sempre così, quindi è importante ricontrollare il tuo piano prima di eseguirlo! Personalmente, in un sistema di produzione, mi sentirei molto più a mio agio nel creare manualmente il flusso di lavoro da eseguire, piuttosto che lasciarlo al LLM! Poiché la tecnologia continua a migliorare, soprattutto al ritmo attuale, si spera che questa raccomandazione diventi obsoleta!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *