La sintesi del codice è il processo di generazione di una descrizione in linguaggio naturale di frammenti di codice. Tra le attività più comuni di sintesi del codice vi sono l'esplorazione di un nuovo codice sorgente, l'apprendimento di un nuovo linguaggio di programmazione o la generazione di commenti al codice e spiegazioni delle funzioni. La generazione di una sintesi di un frammento di codice è simile alla generazione di una sintesi testuale di un documento in linguaggio naturale. La differenza sta nel fatto che il modello linguistico di grandi dimensioni (LLM) che genera la sintesi deve comprendere il linguaggio di programmazione che sta analizzando, identificando al contempo la logica sottostante di ciò che sta cercando di realizzare.
I riassunti di codice sono una parte preziosa dello sviluppo software, in quanto aiutano nella manutenzione del software eseguendo la sintesi automatica del codice sorgente, creando riassunti in linguaggio naturale per la documentazione o analizzando basi di codice su larga scala. I modelli LLM più recenti, che utilizzano un approccio basato sui trasformatori, possono fungere sia da modelli di sintesi del codice sia da generatori di codice. Queste funzioni sono possibili perché i modelli sono stati addestrati su grandi set di dati costruiti a partire da fonti quali i repository GitHub, che includono codice e commenti insieme alla documentazione relativa a quel codice.
Prima che gli LLM si diffondessero, gli approcci alla sintesi del codice richiedevano l'analisi semantica del codice e la generazione di un albero sintattico astratto (AST) per ciascun identificatore presente nel codice, che poteva poi essere utilizzato per generare la documentazione.1,2 Con l'avvento del deep learning e delle reti neurali, gli approcci radicati nell'informatica sono stati abbandonati a favore di altri che attingevano maggiormente dalla metodologia della traduzione automatica neurale3,4.
Per i modelli trasformatori, finestre di contesto più ampie portano a risultati migliori. Molti dei più recenti modelli Granite™ Code all'avanguardia, come Granite-8B-Code-Instruct-128K, dispongono di una finestra di contesto da 128k. Una finestra di contesto più ampia consente al modello di contenere più testo nella memoria di lavoro. Ciò aiuta a monitorare i momenti chiave e i dettagli in una conversazione prolungata, in un documento lungo o in un codice sorgente esteso. Questa memoria di lavoro permette a un chatbot basato su LLM di generare risposte che abbiano senso sia nel momento immediato che in un contesto più ampio, aiutandolo a superare i modelli con finestre di contesto più piccole sia nella valutazione umana che nelle metriche di valutazione.5
Finestre di contesto più ampie consentono ai modelli di conservare più testo nella loro memoria di lavoro, aiutandoli a tenere traccia dei momenti chiave e dei dettagli in chat prolungate, documenti lunghi o codice sorgente.
Quando ChatGPT è stato introdotto per la prima volta, la sua finestra di contesto era di 4.000 token. Se la conversazione superava il limite di 3.000 parole dell’interfaccia di chat, il chatbot rischiava di generare “allucinazioni” e di andare fuori tema. Oggi lo standard è di 32.000 token, con il settore che si sta orientando verso i 128.000 token. Si tratta all'incirca della lunghezza di un libro di 250 pagine. IBM dispone ora di due modelli Granite con una finestra da 128.000 token e altri sono in arrivo.
In questa fase, ti guideremo nella creazione di un account IBM per accedere a Jupyter Notebook.
1. Accedi a watsonx.ai™ utilizzando il tuo account IBM Cloud®.
2. Fai su + per creare un nuovo progetto.
a. Seleziona Crea un progetto vuoto.
b. Inserisci il nome del progetto nel campo Nome.
c. Crea un Cloud Object Storage per memorizzare gli asset del tuo progetto, se non è già stato creato.
d. Seleziona Crea.
3. Crea un Jupyter Notebook.
a. Seleziona la scheda Asset nel tuo ambiente di progetto.
b. Fai clic su Nuovo asset.
c. Seleziona l'opzione Lavorare con i modelli nel pannello di sinistra.
d. Fai clic su Lavorare con dati e modelli usando notebook Python e R.
e. Inserisci un nome per il tuo notebook nel campo Nome. Scegli Runtime 23.1 su Python (4 vCPU 16 GB RAM) per definire la configurazione.
f. Seleziona Crea.
4. Configura un'istanza di runtime watsonx.ai e una chiave API
a. Crea un'istanza di servizio watsonx.ai Runtime (seleziona l'area geografica appropriata e scegli il piano Lite, che è un'istanza gratuita).
b. Genera una chiave API.
c. Associa l'istanza del servizio watsonx.ai Runtime al progetto che hai creato in watsonx.ai.
Per prima cosa, installeremo la libreria open source Hugging Face Hub per scaricare i modelli:
Ora possiamo scaricare Granite-8B-Code-Instruct-128K:
Ora possiamo iniziare a utilizzare Granite Code Instruct.
Forniamo al nostro modello una chiamata di funzione piuttosto complessa dalla libreria GluonTS, presa dal relativo repository GitHub. Si tratta di un lungo blocco di codice da incollare in un prompt, quindi lo memorizzeremo in una variabile:
ll_func_2 = """ def call( self, data: torch.Tensor, weights: torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: assert ( data.shape == weights.shape ), "data and observed_indicator must have same shape" with torch.no_grad(): observed_data = torch.where(weights == 1, data, torch.nan) med = torch.nanmedian(observed_data, dim=self.dim, keepdim=True).values q1 = torch.nanquantile(observed_data, 0.25, dim=self.dim, keepdim=True) q3 = torch.nanquantile(observed_data, 0.75, dim=self.dim, keepdim=True) iqr = q3 - q1 # if observed data is all zeros, nanmedian returns nan loc = torch.where(torch.isnan(med), torch.zeros_like(med), med) scale = torch.where(torch.isnan(iqr), torch.ones_like(iqr), iqr) scale = torch.maximum(scale, torch.full_like(iqr, self.minimum_scale)) scaled_data = (data - loc) / scale if not self.keepdim: loc = torch.squeeze(loc, dim=self.dim) scale = torch.squeeze(scale, dim=self.dim) # assert no nans in scaled data, loc or scale assert not torch.any(torch.isnan(scaled_data)) assert not torch.any(torch.isnan(loc)) assert not torch.any(torch.isnan(scale)) assert not torch.any(scale == 0) return scaled_data, loc, scaledef call( self, data: torch.Tensor, weights: torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: assert ( data.shape == weights.shape ), "data and observed_indicator must have same shape"
Ora possiamo chiedere a Granite Code un riepilogo tokenizzando la nostra chat e poi chiamando callingmodel.generate()
chat = [
{ "role": "user", "content": "Explain what this function does" + ll_func_2 },
]
chat = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
# have the encoder turn text into output tokens
inputs = tokenizer(chat, return_tensors="pt").input_ids
start_index = inputs.shape[-1]
# generate output tokens
output = model.generate(inputs, max_new_tokens=1500)
# have the decoder turn output tokens into text
output = tokenizer.batch_decode(output[start_index:])
# loop over the batch to print, in this example the batch size is 1
print(output[0])
Verifichiamo il riassunto generato:
Risposta: Questa funzione esegue la normalizzazione dei dati utilizzando il metodo dell'intervallo interquartile (IQR). Accetta due argomenti: data e weights. L'argomento data è un tensore di forma (batch_size, sequence_length, hidden_size), dove batch_size è il numero di campioni in un batch, sequence_length è la lunghezza della sequenza di input e hidden_size è la dimensionalità dello stato nascosto. L'argomento weights è un tensore di forma (batch_size, sequence_length), dove ogni elemento è pari a 0 o 1, indicando se l’elemento corrispondente nel tensore dei dati è osservato o mancante. La funzione verifica innanzitutto che i tensori di dati e pesi abbiano la stessa forma. Quindi, calcola la mediana e l’IQR dei punti dati osservati (quelli con peso pari a 1) utilizzando le funzioni torch.nanmedian e torch.nanquantile. La funzione torch.nanmedian calcola la mediana lungo la dimensione specificata (`self.dim`), mentre la funzione torch.nanquantile calcola il 25° e il 75° percentile.Successivamente, la funzione calcola i parametri di posizione (`loc`) e scala (`scale`) della distribuzione normalizzata. La posizione viene impostata sulla mediana dei punti dati osservati, mentre la scala viene impostata sull'IQR dei punti dati osservati. Se i punti dati osservati sono tutti pari a zero, la funzione imposta la posizione a zero. La funzione garantisce inoltre che la scala sia almeno pari a un valore minimo (`self.minimum_scale`) per evitare la divisione per zero. Infine, la funzione calcola i dati normalizzati (`scaled_data`) sottraendo la posizione e dividendo per la scala. Se l’argomento keepdim è impostato su False, la funzione rimuove la dimensione specificata dai tensori di posizione e scala. La funzione esegue inoltre delle verifiche per assicurarsi che i tensori dei dati normalizzati, della posizione e della scala non contengano valori NaN né zeri nella scala. La funzione restituisce una tupla contenente i tensori dei dati normalizzati, della posizione e della scala.
La descrizione è molto dettagliata e illustra con precisione sia la funzione che abbiamo passato in parametro sia l'algoritmo sottostante.
La generazione della documentazione rappresenta una potente applicazione della sintesi del codice, poiché non solo spiega il codice a qualsiasi futuro utente che lo legga, ma può anche aiutare a generare la documentazione utilizzando uno strumento come pydoc.
Ora possiamo richiedere a Granite Code di generare stringhe di documentazione (docstring) per una funzione. Grazie alla finestra di contesto più ampia, possiamo passare un'intera funzione insieme a una richiesta di generazione di una docstring.
Scrivi una stringa di documentazione Python correttamente formattata per la seguente funzione:
Granite Code restituisce un'eccellente documentazione in formato Python:
Extracts a JSON object from the given HTML string using the specified key and stop character. Args: html (str): The input HTML string to extract the JSON object from. key (str): The key used to locate the start of the JSON object in the HTML string. num_chars (int, optional): The number of characters after the key to include in the extracted JSON object. Defaults to 2. stop (str, optional): The character that marks the end of the JSON object in the HTML string. Defaults to '"'. Returns: str: The extracted JSON object as a string.
Ora, se lo desideriamo, possiamo riutilizzare il prompt precedente, che è ancora presente nella finestra del contesto esteso:
chat = "Rewrite the previous function to be a class method and include the docstrings." # have the encoder turn text into output tokens inputs = tokenizer(chat, return_tensors="pt").input_ids start_index = inputs.shape[-1] # generate output tokens output = model.generate(inputs, max_new_tokens=1500) # have the decoder turn output tokens into text output = tokenizer.batch_decode(output[start_index:]) # loop over the batch to print, in this example the batch size is 1 print(output[0])
Questo restituisce
class HtmlJsonExtractor: def init(self, html): """ Initializes the HtmlJsonExtractor object with the given HTML string. Args: html (str): The input HTML string to extract JSON objects from. """ self.html = html @classmethod def extract(cls, html, key, num_chars=2, stop='"'): """ Extracts a JSON object from the given HTML string using the specified key and stop character. Args: html (str): The input HTML string to extract the JSON object from. key (str): The key used to locate the start of the JSON object in the HTML string. num_chars (int, optional): The number of characters after the key to include in the extracted JSON object. Defaults to 2. stop (str, optional): The character that marks the end of the JSON object in the HTML string. Defaults to '"'. Returns: str: The extracted JSON object as a string. """ pos_begin = html.find(key) + len(key) + num_chars pos_end = html.find(stop, pos_begin) return html[pos_begin:pos_end]
Il modello ha ora creato una classe con un nome appropriato, ha trasformato il codice precedente in un metodo di quella classe e ha inserito le nostre stringhe di documentazione.
Chiediamo ora al modello di aiutarci a ottimizzare un frammento di codice:
chat = """Please optimize the following code: def fib(n): if (n < 2): return 1 else: return fib(n-1) + fib(n-2)"""
Il codice restituito da Granite:
La funzione Python fib(n) fornita calcola l'ennesimo numero di Fibonacci utilizzando la ricorsione. Tuttavia, questa implementazione ha una complessità temporale di O(2^n), il che è inefficiente per input di grandi dimensioni. Possiamo
ottimizzarla utilizzando la programmazione dinamica o la memoizzazione per ridurre la complessità temporale a O(n). Ecco una versione ottimizzata della funzione:
```python
def fib_optimized(n, memo={}):
if n < 2:
return 1
elif n not in memo:
memo[n] = fib_optimized(n-1, memo) + fib_optimized(n-2, memo)
return memo[n]
```
Questa versione ottimizzata utilizza un dizionario `memo` per memorizzare i numeri di Fibonacci calcolati in precedenza. Quando calcola l'ennesimo numero di Fibonacci, verifica innanzitutto se è già stato calcolato e memorizzato in `memo`. In tal caso, restituisce il valore memorizzato invece di ricalcolarlo, il che fa risparmiare tempo e migliora le prestazioni.
In questo caso il modello non solo ha spiegato cosa non va nella funzione in termini di efficienza e ha attribuito all'implementazione una complessità Big-O, ma ha anche fornito un'implementazione migliore. Fornire revisioni e spiegazioni è un modo per utilizzare la sintesi e, nell'ambito di una più generale generazione di codice, aiutare gli sviluppatori a migliorare.
In questo tutorial hai appreso nozioni sulla sintesi del codice e hai utilizzato il modello Granite Code con una finestra di contesto estesa a 128k token per generare spiegazioni del codice Python. Abbiamo inoltre utilizzato dei prompt per generare nuova documentazione, aggiungere una classe attorno a un frammento di codice e utilizzare la finestra di contesto estesa per aggiungere tale documentazione a una nuova finestra di codice. Infine, abbiamo chiesto a Granite Code di analizzare e sintetizzare un frammento di codice e di spiegare come potrebbe essere migliorato.
Accelera la distribuzione del software con IBM® Bob, il tuo partner AI per uno sviluppo sicuro e consapevole degli intenti.
Sviluppa, implementa e gestisci le applicazioni AI più velocemente con strumenti pensati per le aziende.
Reinventa i sistemi legacy con la modernizzazione intelligente dell'AI.
1 Sonia Haiduc, Jairo Aponte, Andrian Marcus, “Supporting program comprehension with source code summarization,” ICSE ‘10: Proceedings of the 32nd ACM/IEEE International Conference on Software Engineering https://doi.org/10.1145/3377811.3380383.
2 Paul W. McBurney, Collin McMillan, “Automatic Source Code Summarization of Context for Java Methods,” https://ieeexplore.ieee.org/document/7181703.
3 Chen Lin, Zhichao Ouyang, Junqing Zhuang, Jianqiang Chen, Hui Li, Rongxin Wu, “Improving Code Summarization with Block-wise Abstract Syntax Tree Splitting” IEEE/ACM, International Conference on Program Comprehension (ICPC 2021) https://arxiv.org/abs/2103.07845.
4 Jian Zhang, Xu Wang, Hongyu Zhang, Hailong Sun, Xudong Liu, “Retrieval-based neural source code summarization,” ICSE ‘10: Proceedings of the 32nd ACM/IEEE International Conference on Software Engineering, https://doi.org/10.1145/1810295.1810335.
5 Xinyi Hou, Yanjie Zhao, Yue Huang, Zhou Yang, Kailong Wang, Li Li, Xiapu Luo, David Jin, John Grundy, Haoyu Wang, “Large Language Models for Software Engineering: A Systematic Literature Review”, https://arxiv.org/abs/2308.10620.